第五章 创建高性能的索引
第六章和第三章 查询性能优化和服务器性能剖析
MySql架构图:
一.优化查询需要掌握的基础知识
1.查询的执行过程
图比较复杂,先不看,看文字大概了解查询执行的过程
(1)客户端发送一条查询给服务器
(2)服务器先检查缓存,如果命中了缓存则立即返回存储在缓存中的结
果。否则进入下一阶段。
(3)服务器端进行SQL解析、预处理,在由优化器生成对应的执行计划。
(4)Mysql根据优化器生成执行计划,调用存储引擎的API来执行查询
(5)返回结果给客户端。
2.MySql服务器端、客户端的通信协议
半双工
3.查询状态
对于一个MySql连接,或者说一个线程任何时候都有一个状态,该状
态表示了MySql当前正在做什么。了解这些状态的基本含义非常有用,这
可以让你很快地了解当前“谁正在持球"。在一个繁忙的服务器上,可能会
看到大量的不正常的状态,例如statistics正占用大量的时间。这通常表示,
某个地方有异常了,可以通过使用第3章的一些技巧来诊断到底是哪个环
节出现了问题。
有多种方式可以查看状态,书上介绍使用
SHOW FULL PROCESSLIST命令
Sleep
线程正在等待客户端发送新的请求。
Query
线程正在执行查询或者正在将结果发送给客户端。
Locked
在MySQL服务器层,该线程正在等待表锁。在存储引擎级别实现的
锁,例如InnoDB的行锁,并不会体现在线程状态中。对于MylSAM
来说这是一个比较典型的状态,但在其他没有行锁的引擎中也经常
会出现。
Analyzing and statistics
线程正在收集存储引擎的统计信息,并生成查询的执行计划。
Copying to tmp table 【ondisk】
线程正在执行查询,并且将其结果集都复制到一个临时表中,这种
状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者
是UNION操作。如果这个状态后面有"ondisk"标记,那表示MYSQL
正在将一个内存临时表放到磁盘上。
Sorting result
线程正在对结果集进行排序。
二.介绍MySql的查询处理
1.查询缓存
在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会
优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对
大小写敏感的哈希查找实现的。查询和缓存中的查询即使只有一个字节
不同,那也不会匹配缓存结果(大小写、多一个空格等都会认为是不同
的sql,将不走缓存),这种情况下查询就会进人下一阶段的处理。如果
当前的查询恰好命中了查询缓存,那么在返回查询结果之前MySQL会检
查一次用户权限。这仍然是无须解析查询SQL语句的,因为在查询缓存
中已经存放了当前查询需要访问的表信息。如果权限没有问题,MySQL
会跳过所有其他阶段,直接从缓存中拿到结果并返回给客户端。这种情
况下,查询不会被解析,不用生成执行计划,不会被执行。
在第7章中的查询缓存一节,你将学习到更多细节。
2.查询优化处理
查询的生命周期的下一步是将一个SQL转换成一个执行计划(MySql
的执行计划是一个树状的数据结构,而不是和很多其他的关系型数据库那
样会生成对应的字节码。),MySQL再依照这个执行计划和存储引擎进
行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这
个过程中任何错误(例如语法错误)都可能终止查询。
在实际执行中,这几部分可能一起执行也可能单独执行。
(1)语法解析和预处理
首先,MysQL通过关键字将SQL语句进行解析,并生成一棵对
应的“解析树"。MYSQL解析器将使用MySQL语法规则验证和解析查
询。例如,它将验证是否使用错误的关键字,或者使用关键字的顺
序是否正确等,再或者它还会验证引号是否能前后正确匹配。预处
理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这
里将检查数据表和数据列是否存在,还会解析名字和别名,看看它
们是否有歧义。下一步预处理器会验证权限。这通常很快,除非服
务器上有非常多的权限配置。
(2)查询优化器
现在语法树被认为是合法的了,并且由优化器将其转化成执行
计划。一条查询可以有很多种执行方式,最后都返回相同的结果。
优化器的作用就是找到这其中最好的执行计划。MySQL使用基于成
本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,
并选择其中成本最小的一个。可以通过查询当前会话的
Last-query_cost的值来得知MySQL计算的当前查询的成本。
这个结果表示MySQL的优化器认为大概需要做1040个数据页的随
机查找才能完成上面的查询。这是根据一系列的统计信息计算得来的:
每个表或者索引的页面个数、索引的基数(索引中不同值的数量)、索
引和数据行的长度、索引分布情况。优化器在评估成本的时候并不考虑
任何层面的缓存,它假设读取任何数据都需要一次磁盘1/0。有很多种
原因会导致MySQL优化器选择错误的执行计划,如下所示:
注意:MySql选择的执行计划有时候不是最优的,列举其中的一些原因
如下:(下面的具体内容大概看看就行。)
(a)统计信息不准确。MySQL依賴存储引擎提供的统计信息来评估成
本,但是有的存储引擎提供的信息是谁确的,有的偏差可能非常
大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表
的行数的精确统计信息。
(b)执行计划中的成本估算不等同于实际执行的成本。所以即使统计
信息精准,优化器给出的执行计划也可能不是最优的。例如有时
候某个执行计划虽然需要读取更多的页面,但是它的成本却更小。
因为如果这些页面都是顺序读或者这些页面都已经在内存中的话,
那么它的访问成本将很小。MySQL层面并不知道哪些页面在内存
中、哪些在磁盘上,所以查询实际执行过程中到底需要多少次物
理1/0是无法得知的。
(c)MySQL的最优可能和你想的最优不一样。
你可能希望执行时间尽可能的短,但是MYSQL只是基于其成本模
型选择最优的执行计划,而有些时候这并不是最快的执行方式。
所以,这里我们看到根据执行成本来选择执行计划并不是完美的
模型。
(d)MySQL从不考虑其他并发执行的查询,这可能会影响到当前查询
的速度。MySQL也并不是任何时候都是基于成本的优化。有时也
会基于一些固定的规则,例如,如果存在全文搜索的MATCH0子
句,则在存在全文索引的时候就使用全文索引。即使有时候使用
别的索引和WHERE条件可以远比这种方式要快,MySQL也仍然
会使用对应的全文索引。
(e)MYSQL不会考虑不受其控制的操作的成本,例如执行存储过程
或者用户自定义函数的成本。
(f)后面我们还会看到,优化器有时候无法去估算所有可能的执行计
划,所以它可能错过实际上最优的执行计划。
MySql是如何执行关联查询的?(重点)
* 数据和索引的统计信息(知道服务层需要向存储引擎获取统计信息即
可)
如图1·1,MySQL架构由多个层次组成。在服务器层查询优化器,
却没有保存数据和索引的统计信息。统计信息由存储引擎实现。
不同的存储引擎可能会存储不同的统计信息(也可以按照不同的
格式存储统计信息)。某些引擎,例如Archive引擎,则根本就没有存
储任何统计信息!
因为服务器层没有任何统计信息,所以MySQL查询优化器在生成
查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎
则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页
面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分
布信息等。优化器根据这些信息来选择一个最优的执行计划。
* 关联查询基础知识(重点)
首先解释一下“关联”。“关联”一词所包含的意义比一般意义上理
解的要更广泛。总的来说,MySQL中“关联一并不仅仅是一个查询需
要到两个表MySql 认为任何一个查询都是一次“关联"。并不是一个查
询匹配到两张表才叫关联,所以在MySQL中,每一个查询,每一个
片段(包括子查询,甚至基于单表的SELECT)都可能是关联。例如
一个UNION查询,对于UN1ON查询,MySQL先将一系列的单个查
询结果放到一个临时表中,然后再重新读出临时表数据来完成UION
查询。在MySQL的概念中,每个查询都是一次关联,所以读取结果
临时表也是一次关联。
MYSQL关联执行的策略很简单:MySQL对任何关联都执行嵌套
循环关联操作,即MYSQL先在一个表中循环取出单条数据,然后再
嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中
匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个
列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最
后一个关联表无法找到更多的行以后,MySQL返回到上一层次关联
表,看是否能够找到更多的匹配记录,依此类推迭代执行。
例子,多表查询:
SQL如下:
伪代码如下:
上面的执行计划对于单表查询和多表关联查询都适用,如果是一个单
表查询,那么只需完成上面外层的基本操作。对于外连接上面的执行
过程仍然适用。例如,我们将上面查询修改如下:
伪代码:
另一种可视化查询执行计划的方法是根据优化器执行的路径
绘制出对应的“泳道图"。如图6·2所示,绘制了前面示例中内连接
的泳道图,请从左至右,从上至下地看这幅图。
从本质上说,MySQL对所有的类型的查询都以同样的方式运行。
例如,MySQL在FROM子句中遇到子查询时,先执行子查询并将其
结果放到一个临时表中,然后将这个临时表当作一个普通表对待
(正如其名“派生表")。MySQL在执行UNION查询时也使用类似的临
时表,在遇到右外连接的时候,MySQL将其改写成等价的左外连接。
简而言之,当前版本的MySQL会将所有的查询类型都转换成类似的
执行计划。不过,不是所有的查询都可以转换成上面的形式。例如,
全外连接就无法通过嵌套循环和回溯的方式完成,这时当发现关联表
中没有找到任何匹配行的时候,则可能是因为关联是恰好从一个没有
任何匹配的表开始。这大概也是MySQL并不支持全外连接的原因。
还有些场景,虽然可以转换成嵌套循环的方式,但是效率却非常差,
后面我们会看一个这样的例子。
* 关联查询优化器(重点):
MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时
的顺序。通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行
结果。关联查询优化器则通过评估不同顺序时的成本来选择一个代价最小的关
联顺序。重新定义关联的顺序是优化器非常重要的一部分功能。不过有的时候,
优化器给出的并不是最优的关联顺序。这时可以使用STRAIGHT_JOIN关键字
重写查询,让优化器按照你认为的最优的关联顺序执行。不过老实说,人的判
断很难那么精准。绝大多数时候,优化器做出的选择都比普通人的判断要更准
确。下面的查询可以通过不同顺序的关联最后都获得相同的结果:
容易看出,可以通过一些不同的执行计划来完成上面的查询。例如,MySQL可
以从film表开始,使用film_actor表的索引film_id来查找对应的actor_id值,然后
再根据actor表的主键找到对应的记录。Oracle用户会用下面的术语描述:“film
表作为驱动表先查找file_actor表,然后以此结果为驱动表再查找actor表"。这样
做效率应该会不错,我们再使用EXPLAIN看看MySQL将如何执行这个查询:
这和我们前面给出的执行计划完全不同。MySQL从actor表开始(我们从上
面的EXPLAIN结果的第一行输出可以看出这点),然后与我们前面的计划
按照相反的顺序进行关联。这样是否效率更高呢?我们来看看,我们先使
用STRAIGHT_JOIN关键字,按照我们之前的顺序执行,这里是对应的
EXPLAIN输出结果:
我们来比较一下执行计划:第一种按照MySql本身优化的顺序,那
么第一个关联表只需要扫描200行,第二个关联表和第三个关联表与第
二种情况相同,故不做比较;第二种按照我们自定义的关联顺序,那么
第一个关联表只需扫描951行,第二个关联表和第三个关联表同样不需
要做比较。
换句话说,倒转的关联顺序会让查询进行更少的嵌套循环和回溯操
作。为了验证优化器的选择是否正确,我们单独执行这两个查询,并且
看看对应的Last-query-cost状态值。我们看到倒转的关联顺序的预估成
本为241,而原来的查询的预估成本为1154。
扩展一:关联表太多时造成的影响(属于阅读内容,重点看一下标黑的
内容)
如果有超过”个表的关联,那么需要检查的阶乘种关联顺序。我
们称之为所有可能的执行计划的“搜索空间",搜索空间的增长速
度非常块一例如,若是10个表的关联,那么共有3628800种不
同的关联顺序!当搜索空间非常大的时候,优化器不可能逐一
评估每一种关联顺序的成本。这时,优化器选择使用“贪婪"搜索
的方式查找“最优"的关联顺序。实际上,当需要关联的表超过
optimizer-search-depth的限制的时候,就会选择“贪婪"搜索模
式了(optimizer-search_depth参数可以根据需要指定大小)。
在MySQL这些年的发展过程中,优化器积累了很多“启发式"
的优化策略来加速执行计划的生成。绝大多数情况下,这都是
有效的,但因为不会去计算每一种关联顺序的成本,所以偶尔
也会选择一个不是最优的执行计划。有时,各个查询的顺序并
不能随意安排,这时关联优化器可以根据这些规则大大减少搜
索空间,例如,左连接、相关子查询(后面我将继续讨论子查
询)。这是因为,后面的表的查询需要依赖于前面表的查询结
果。这种依赖关系通常可以帮助优化器大大减少需。
扩展二:MySQL能够处理的优化类型(大概看一下内容就可以):
(a)重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行。决定关
联的顺序是优化器很重要的一部分功能,本章后面将深人介绍
这一点
(b)将外连接转化成内连接
并不是所有的OUTER JOIN语句都必须以外连接的方式执行。
例如WHERE条件、库表结构都可能会让外连接等价于一个内
连接。MYSQL能够识别这点并重写查询,让其可以调整关联
顺序。
(c)使用等价变换规则
MYSQL可以使用一些等价变换来简化并规范表达式。它可以
合并和减少一些比较,还可以移除一些恒成立和一些恒不成立
的判断。例如,(5=5 AND a>5)将被改写为a>5。类似的,如果
有(a<b AND b=c) AND a=5则会改写为b>5 AND b=c AND
a=5。这些规则对于我们编写条件语句很有用,我们将在本章后
续继续讨论。
(d)优化COUNT0、MIN()和MAX()
索引和列是否可为空通常可以帮助MySQL优化这类表达式。例
如,要找到某一列的最小值,只需要查询对应B-Tree索引最左
端的记录,MySQL可以直接获取索引的第一行记录。在优化器
生成执行计划的时候就可以利用这一点,在B-Tree索引中,优
化器会将这个表达式作为一个常数对待。类似的,如果要查找一
个最大值,也只需读取索引的最后一条记录。如果MySQL使用了
这种类型的优化,那么在EXPLAIN中就可以看到
“Selecttablesoptimizedaway"。从字面意思可以看出,它表示优
化器已经从执行计划中移除了该表,并以一个常数取而代之。类
似的,没有任何WHERE条件的COUNT(*)查询通常也可以使用
存储引擎提供的一些优化(例如,MylSAM维护了一个变量来存
放数据表的行数)。
(e)预估并转化为常数表达式
当MySQL检测到一个表达式可以转化为常数的时候,就会一直
把该表达式作为常数进行优化处理。例如,一个用户自定义变量
在查询中没有发生变化时就可以转换为一个常数。数学表达式则
是另一种典型的例子。让人惊讶的是,在优化阶段,有时候甚至
一个查询也能够转化为一个常数。一个例子是在索引列上执行
MIN()函数。甚至是主键或者唯一键查找语句也可以转换为常数表
达式。如果WHERE子句中使用了该类索引的常数条件,MySQL
可以在查询开始阶段就先查找到这些值,这样优化器就能够知道
并转换为常数表达式。下面是一个例子:
(f)预估并转化为常数表达式
当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该
表达式作为常数进行优化处理。例如,一个用户自定义变量在查询
中没有发生变化时就可以转换为一个常数。数学表达式则是另一种
典型的例子。让人惊讶的是,在优化阶段,有时候甚至一个查询也
能够转化为一个常数。一个例子是在索引列上执行MIN()函数。甚至
是主键或者唯一键查找语句也可以转换为常数表达式。如果WHERE
子句中使用了该类索引的常数条件,MySQL可以在查询开始阶段就
先查找到这些值,这样优化器就能够知道并转换为常数表达式。下面
是一个例子:
MySQL分两步执行这个查询,就是上面执行计划的两行输出。
第一步步先从film表找到需要的行。因为在film_id字段上有主键
索引,所以MySQL优化器知道这只会返回一行数据,优化器在生成执
行计划的时候,就已经通过索引信息知道将返回多少行数据。因为优
化器已经明确知道有多少个值(WHERE条件中的值)需要做索引查询,
所以这里的表访问类型是const。
在执行计划的第二步,MySQL将第一步中返回的film_id列当作一
个已知取值的列来处理。因为优化器清楚在第一步执行完成后,该值
就会是明确的了。注意到正如第一步中一样,使用filmactor字段对表
的访问类型也是const另一种会看到常数条件的情况是通过等式将常数
值从一个表传到另一个表,这可以通过WHERE、USING或者ON语句
来限制某列取值为常数。在上面的例子中,因为使用了USING子句,
优化器知道这也限制了filmid在整个查询过程中都始终是一个常量一因
为它必须等于WHERE子句中的那个取值。
(g)覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使
用索引返回需要的数据,而无须查询对应的数据行,在前面的章节中
我们已经讨论过这点了。
(h)子查询优化
MySQL在某些情况下可以将子查询转换一种效率更高的形式,从而
减少多个查询多次对数据进行访问。
(i)提前终止查询
在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一
个典型的例子就是当使用了LIMIT子句的时候。除此之外,MySQL还
有几类情况也会提前终止查询,例如发现了一个不成立的条件,这时
MySQL可以立刻返回一个空结果。从下面的例子可以看到这一点。
从这个例子看到查询在优化阶段就已经终止。除此之外,MySQL在执
行过程中,如果发现某些特殊的条件,则会提前终止查询。当存储引
擎需要检索“不同取值"或者判断存在性的时候,MySQL都可以使用这
类优化。例如,我们现在需要找到没有演员的所有电影。
这个查询将会过滤掉所有有演员的电影。每一部电影可能会有很多的
演员,但是上面的查询一旦找到任何一个,就会停止并立刻判断下一
部电影,因为只要有一名演员,那么WHERE条件则会过滤掉这类电
影。类似这种“不同值/不存在"的优化一般可用于DISTINCT、
NOTEXIST()或者LEFTJOIN类型的查询。
(i)等值传播
如果两个列的值通过等式关联,那么MySQL能够把其中一个列的
WHERE条件传递到另一列上。例如,我们看下面的查询:
因为这里使用了film_id字段进行等值关联,MySQL知道这里的
WHERE子句不仅适用于film表,而且对于film_actor表同样适用。
如果使用的是其他的数据库管理系统,可能还需要手动通过一些
条件来告知优化器这个WHERE条件适用于两个表,那么写法就
会如下:
在MYSQL中这是不必要的,这样写反而会让查询更难维护。
(j)列表IN的比较
在很多数据库系统中,IN完全等同于多个OR条件的子句,因
为这两者是完全等价的。在MySQL中这点是不成立的,MySQL将
IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列
表中的值是否满足条件,这是一个O(logn)复杂度的操作,等价地
转换成OR查询的复杂度为O(n),对于IN列表中有大量取值的时候,
MySQL的处理速度将会更快。
上面列举的远不是MYSQL优化器的全部,MySQL还会做大量
其他的优化,即使本章全部用来描述也会篇幅不足,但上面的这些
例子已经足以让大家明白优化器的复杂性和智能性了。如果说从上
面这段讨论中我们应该学到什么,那就是“不要自以为比优化器更聪
明"。最终你可能会占点便宜,但是更有可能会使查询变得更加复杂
而难以维护,而最终的收益却为零。让优化器按照它的方式工作就
可以了。
当然,虽然优化器已经很智能了,但是有时候也无法给出最优的
结果。有时候你可能比优化器更了解数据,例如,由于应用逻辑使得
某些条件总是成立;还有时,优化器缺少某种功能特性,如哈希索引;
再如前面提到的,从优化器的执行成本角度评估出来的最优执行计划,
实际运行中可能比其他的执行计划更慢。
如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,
那么也可以帮助优化器做进一步的优化。例如,可以在查询添加hint
提示,也可以重写查询,或者重新设计更优的库表结构,或者添加更
合适的索引。
扩展三:查询优化器的优化策略简介(有空看,没空拉倒)
MYSQL的查询优化器是一个非常复杂的部件,它使用了很多优化策
略来生成一个最优的执行计划。优化策略可以简单地分为两种,一种是
静态优化,一种是动态优化。
静态优化可以直接对解析树进行分析,并完成优化。例如,优化器
可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。
静态优化不依賴于特别的数值,如WHERE条件中带人的一些常数等。
静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行
查询也不会发生变化。可以认为这是一种“编译时优化"。相反,动态
优化则和查询的上下文有关,也可能和很多其他因素有关,例如
WHERE条件中的取值、索引中条目对应的数据行数等。这需要在每
次查询的时候都重新评估,可以认为这是“运行时优化"。
在执行语句和存储过程的时候,动态优化和静态优化的区别非
常重要。MySQL对查询的静态优化只需要做一次,但对查询的动态优
化则在每次执行时都需要重新评估。有时候甚至在查询的执行过程中
也会重新优化。
* 执行计划(属于了解内容,知道其数据结构就可以)
执行计划需要“统计信息”,而统计信息存在存储引擎当中
如图1·1,MySQL架构由多个层次组成。在服务器层查询优化
器,却没有保存数据和索引的统计信息。统计信息由存储引擎实现。
不同的存储引擎可能会存储不同的统计信息(也可以按照不同
的格式存储统计信息)。某些引擎,例如Archive引擎,则根本就没
有存储任何统计信息!
执行计划的数据结构
因为服务器层没有任何统计信息,所以MySQL查询优化器在生成
查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎
则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页
面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分
布信息等。优化器根据这些信息来选择一个最优的执行计划。
和很多其他关系数据库不同,MySQL并不会生成查询字节码来执
行查询。MYSQL生成查询的一棵指令树,然后通过存储引擎执行完成
这棵指令树并返回结果。最终的执行计划包含了重构查询的全部信息。
如果对某个查询执行EXPLAIN EXTENDED后,再执行
SHOW WARNINGS,就可以看到重构的查询。
任何多表查询都可以使用一棵树表示,例如,可以按照图6·3执行
一个四表的关联操作。
在计算机科学中,这被称为一颗平衡树。但是,这并不是MySQL执
行查询的方式。正如我们前面章节介绍的,MySQL总是从一个表开始一
直嵌套循环、回溯完成所有表关联。所以,MySQL的执行计划总是如图
6一4所示,是一棵左测深度优先的树。
(4)查询执行引擎(非重点)
MySQL的查询执行引擎则根据解析和优化阶段生成的执行计划来完成
整个查询。这里执行计划是一个数据结构,而不是和很多其他的关系型数
据库那样会生成对应的字节码。
其它内容未摘录,看书。
(5)返回结果给客户端(非重点)
这是查询执行的最后一个阶段。即使查询不需要返回结果集给客户
端,MySql仍然会返回这个查询的一些信息,如该查询影响的行数。如
果查询可以被缓存,那么MySql在这个阶段也会将结果放到缓存中。
MySQL将结果集返回客户端是一个增量、逐步返回的过程。例如,
我们回头看看前面的关联操作,一旦服务器处理完最后一个关联表,开
始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。
(这样做的优点是:让MySql客户端更快的获取返回结果;节省服务器
资源。因为服务器无需缓存太多的结果)
3.查询优化(重点,本节是浓缩的产品,时间不赶趟可以直接看本节)
书上总结的衡量查询开销的指标(这个并不感觉很重要,记录在这是怕面
试问到):
* 响应时间
* 扫描行数
* 扫描行数和返回行数
(1)总结一下查询会慢的原因
(a)数据太多,通过分库、分表来解决
(b)SQL的问题,请求了不必要的数据。
* 检查查询是否查询了不需要的记录,加limit限定解决。比如先使
用select查询出大量的结果,然后在获取前面的N条数据后在关
闭结果集,这样MySql还是要返回全部结果集的,事实上比等待
返回全部结果集更消耗资源。
* 总是取出全部的列
总是取出全部列,会让优化器无法完成索引覆盖扫描这类优化,
还会为服务器带来额外的IO、内存和CPU的消耗(其实就是
select * 的问题,有些dba禁止这种写法总是取出全部列简化和
加快开发如果可以不计较这些性能损失,我们可以查出全部列)。
经典案例如下:
多表关联取出全部列:
select * from tablea innerjoin tableb on
tablea.id=tableb.aid
改为:
select column1,column2,column3,... from
tablea innerjoin tableb on tablea.id=tableb.aid
或:
select tablea.* from tablea innerjoin tableb on
tablea.id=tableb.aid
单表查出全部列:
select * from tablea
改为:
select column1,column2,column3,...from tablea
* 重复的查询没有缓存
(c)索引的问题
一些原因导致索引没有生效、索引没有覆盖等。体现在执行计划
的type上。解决方案是重写SQL或优化索引或优化库表结构
(d)where条件问题
where条件中的效率由好到坏依次是:
* 索引中使用WERE条件来过滤不匹配的记录。这是在存储引擎
层完成的。
* 使用索引覆盖扫描(在Extra列中出现了Usingindex)来返回
记录,直接从索引中过滤不需要的记录并返回命中的结果。这
是在MySQL服务器层完成的,但无须再回表查询记录。
* 从数据表中返回数据,然后过滤不满足条件的记录(在Extra
列中出现Using Where)。这在MYSQL服务器层完成,
MySQL需要先从数据表读出记录然后过滤。
where条件效率问题的解决方案是重写SQL或优化库表结构
(e)扫描行数
扫描行数一般和返回行数、访问类型(执行计划种的type)一起
使用。扫描行数大多数时候会等于返回行数(个别时候等于返回
行数),他们之间的差距越小越好;扫描行数和访问类型一起评
估查询消耗的资源。
如果发现查询需要扫描大量的数据但只返回少数的行,那么
通常可以尝试下面的技巧去优化它:
* 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存
储引擎无须回表获取对应行就可以返回结果了。
* 改变库表结构。例如使用单独的汇总表。
* 重写这个复杂的查询,让MYSQL优化器能够以更优化的方式
执行这个查询
(2)我们该如何重构查询
(a)一个复杂的查询改为多个简单的小查询
如果一个复杂的查询性能不好,我们就可以将它改为多个简单
的小查询。由于MySql建立和断开连接都是轻量级的,所以我
们可以这样做,不过如果复杂查询没有问题,我们就不要改,
还是执行单一复杂查询的效率和资源开销更优。
(b)切分查询(这一点需要注意,经常会有应用场景而我似乎在大
脑里没有加强)
有时候对于一个大查询我们需要“分而治之",将大查询切分成
小查询,每个查询功能完全一样,只完成一小部分,每次只返
回一小部分查询结果。删除旧的数据就是一个很好的例子。定
期地清除大量数据时,如果用一个大的语句一次性完成的话,
则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统
资源、阻塞很多小的但重要的查询。将一个大的DELETE语句
切分成多个较小的查询可以尽可能小地影响MYSQL性能,同
时还可以减少MySQL复制的延迟。例如,我们需要每个月运
行一次下面的语句:
就可以按如下做法:
(c)分解关联查询
很多高性能的应用都会对关联查询进行分解。简单地,可以对
每一个表进行一次单表查询,然后将结果在应用程序中进行关
联。例如,下面这个查询:
可以用如下SQL来代替
分解关联查询的好处:
* 让缓存的效率更高。许多应用程序可以方便地缓存单表查询
对应的结果对象。例如,上面查询中的tag已经被缓存了,
那么应用就可以跳过第一个查询。再例如,应用中已经缓存
了ID为123、567、9098的内容,那么第三个查询的in()中就
可以少几个IDO另外,对MySQL的查询缓存来说,如果关联
中的某个表发生了变化,那么就无法使用查询缓存了,而拆
分后,如果某个表很少改变,那么基于该表的查询就可以重
复利用查询缓存结果了。
* 将查询分解后,执行单个查询可以减少锁的竞争。
* 在应用层做关联,可以更容易对数据库进行拆分,更容易做
到高性能和可扩展。
* 查询本身效率也可能会有所提升。这个例子中,使用in()代替
关联查询,可以让MySQL按照ID顺序进行查询,这可能比随
机的关联要更高效。
* 可以减少冗余记录的查询。在应用层做关联查询,意味着对
于某条记录应用只需要查询一次,而在数据库中做关联查询,
则可能需要重复地访问一部分数据。从这点看,这样的重构
还可能会减少网络和内存的消耗。
* 更进一步,这样做相当于在应用中实现了哈希关联,而不是
使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高
很多。
二.服务器性能优化篇
1.些概念
(1)什么是MySql的性能优化
MySql的性能优化即响应时间优化
(2)需要掌握的技能
(a) 会查看慢查询日志
(b)掌握tcpdump和query-digest(书上介绍的,具体现在公司用啥不
知道)
(c)多掌握几款工具和自己在去找几个案例。
2.生成环境中的实战
(1)诊断间歇性问题诊断
第一阶段,判断是否是MySql的问题。
下面列举一些间歇性问题,有的是MySql的问题,有的不是MySql
的问题。同时说明有些问题的解决并不适用于“试错法”
* 应用通过“从一个运行得很慢的外部服务来获取汇率报价的数据。
* 缓存(redis等)中的一些重要条目过期,导致大量请求落到MySQL
以重新生成缓存条目。
* DNS查询偶尔会有超时现象。
* 可能是由于互斥锁争用,或者内部删除查询缓存的算法效率太低
的缘故,MySQL的查询缓存有时候会导致服务有短暂的停顿。
* 当并发度超过某个阈值时,InnoDB的扩展性限制导致查询计划的
优化需要很长的时
第二阶段,判断是MySql服务器的问题还是单条SQL的问题和捕获诊
断数据介绍
判断是MySql服务器的问题还是单条SQL的问题
首先,介绍一种粗粒度的方式
如果服务器上所有的程序都突然变慢,又突然都变好,
一条查询也都变慢了,那么慢查询可能就不一定是原
因,而是由于其他问题导致的结果。反过来说,如果
服务器整体运行没有问题,只有某条查询偶尔变慢,
就很有可能是这条查询的问题。
第二,比较细粒度的解决方式。有如下三种,大多数问题
通过如下三种方式都能解决。建议使用第一种和第二
种,因为这两种方式开销低,可以使用简单的shell脚
本或是反复执行查询来交互式的搜集数据。
第一种,使用SHOW GLOBAL STATUS
这个方法就是以较高的频率(比如一秒执行一次)
来捕获SHOW GLOBAL STATUS命令来获数据
(使用命令mysqladmin extended-status,ext是
extended命令的缩写)。问题出现时,则可以通过
某些计数器(Threads-running、
Threads connected、Questions和Queries)的“尖
刺"或者“凹陷"来发现。这个方法比较简单,所有人
都可以使用(不需要特殊的权限),对服务器的影
响也很小,所以是一个花费时间不多却能很好地了
解问题的好方法。
扩展:介绍一下MySql计数器的知识,用到哪个了解哪个就
可以了,不用专门学习。
计数器 | 计数器分析 |
Threads_connected | 表示当前有多少个客户连接该mysql服务器(可以察觉连接数是否过多、 网络是否存在问题),它是动态变化的,当达到最大连接数时,数据库 系统就不能提供更多的连接数了,这时,程序还想新建连接线程,数据 库系统就会拒绝 注:有人认为这个是数据库的连接池,下面的例子也说明因为使用连接 池,索引这个值不会有太大的变化。 |
Threads_running | thread_running状态变量记录了当前并发执行stmt/command的数量,执 行前加1执行后减1。如果数据库超负荷,将会得到一个正在(查询的语 句持续)增长的数值。这个值也可以小于预先设定的值。这个值在很短 的时间内超过限定值是没问题的。若超过预设值时且5秒内没有回落,要 同时监视其他的一些值。 下面是网友总结的thread_running突然飙高的可能原因: 1.客户端连接暴增; 2.系统性能瓶颈,如CPU,IO或者mem swap; 3.异常sql;
|
Aborted_clients | 客户端被异常中断的数值,对于一些应用程序是没有影响的,但对于另一 些应用程序可能要跟踪该值,因为异常中断连接可能表明一些应用程序有 问题 |
Questions | 每秒钟获得的查询数量,也可以是全部查询的数量。 |
Opened_tables | 指表缓存没有命中的数量。如果该值很大,就需要增加table_cache的数 值 |
Select_full_join | 没有主键(key)联合(join)的执行。该值可能是零。这是捕获开发错误的好 方法,因为这样的查询有可能降低系统的性能 |
Select_scan | 执行全表搜索查询的数量。如果发现该值持续增长,说明需要优化,缺乏 必要的索引或其他问题 |
Slow_queries | 超过该值(--long-query-time)的查询数量,或没有使用索引查询数量。对 于全部查询会有小的冲突。如果该值增长,表明系统有性能问 题 |
Threads_created | 该值一般较低。较高的值意味着需要增加thread_cache的数值,或遇到 了持续增加的连接,表明存在潜在的问题 |
举例介绍SHOW GLOBAL STATUS的使用:
这个命令每秒捕获一次SHOWGLOBALSTATUS
的数据,输出给w计算并输出每秒的查询数、
Threads_connected和Threadsrunning。这三个数据
的趋势对于服务器级别偶尔停顿的敏感性很高。一般
发生此类问题时,根据原因的不同和应用连接数据库
方式的不同,每秒的查询数一般会下跌,而其他两个
则至少有一个会出现尖刺。在这个例子中,应用使用
了连接池,所以Threads_connected没有变化。但正在
执行查询的线程数明显上升,同时每秒的查询数相比
正常数据有严重的下跌。
如何解析这个现象呢?凭猜测有一定的风险。但在
实践中有两个原因的可能性比较大。其中之一是服务器
内部碰到了某种瓶颈,导致新查询在开始执行前因为需
要获取老查询正在等待的锁而造成堆积。这一类的锁一
般也会对应用服务器造成后端压力,使得应用服务器也
出现排队问题。另外一个常见的原因是服务区突然遇到
了大量查询请求的冲击,比如前端的缓存突然失效导致
的查询风暴。
这个命令每秒输出一行数据,可以运行几个小时或
者几天,然后将结果绘制成图形,这样就可以方便地发
现是否有趋势的突变。如果问题确实是间歇性的,发生
的频率又较低,也可以根据需要尽可能长时间地运行此
命令,直到发现问题再回头来看输出结果。大多数情况
下,通过输出结果都可以更明确地定位问题。
第二种,使用SHOW [FULL] PROCESSLIST
这个方法通过不停地捕获SHOW PROCESSLIST的输出来
观察是否有大量的线程处于不正常的状态或者有其他不正常
的特征。例如查询很少会长时间处于"statistics”状态,这个状
态一般是指服务器在查询优化阶段如何确定表关联的顺序一
通常都是非常快的。另外,也很少会见到大量线程报告当前
连接用户是“未经验证的用户(Unauthenticated user)
使用SHOW PROCESSLIST命令时,在尾部加上\G可以
垂直的方式输出结果,这很有用,因为这样会将每一行记录
的每一列都单独输出为一行,这样可以方便地使用
sort|uniq|sort来计算各个列值出现的次数:
如果要查看不同的列,只需要修改grep的模式即可。在大
多数案例中,state列都非常有用。从这个例子的输出中可以看
到,有很多线程处于查询执行的结束部分的状态,包括:
“freeing items”、"cleaningup"、“end”、“cleaning up”和
“logging slow query”事实上,在案例中的这台服务器上,同样
模式或类似的输出采样出现了很多次。大量的线程处于
"freeing items”状态是出现了大量有问题查询的很明显的特征
和指示。
用这种技术查找问题,上面的命令行不是唯一的方法。
如果MySQL服务器的版本较新,也可以直接查询
INFORMATION_SCHEMA中的PROCESSLIST表;或者使
用innotop工具以较高的频率刷新,以观察屏幕上出现的不正
常查询堆积。上面演示的这个例子是由于InnoDB内部的争用
和脏块刷新所导致,但有时候原因可能比这个要简单得多。
一个经典的例子是很多查询处于"Locked”状态,这是
MylSAM的一个典型问题,它的表级别锁定,在写请求较多
时,可能迅速导致服务器级别的线程堆积。
第三种,使用慢查询日志
使用慢查询日志发现问题,需要开启慢查询日志并在全局级别
设置long_query_time为0,并且要确认所有的连接都采用了新
的设置。这可能需要重置所有连接以使新的全局设置生效;或
者使用PerconaServer的一个特性,可以在不断开现有连接的
情况下动态地使设置强制生效。
如果因为某些原因,不能设置慢查询日志记录所有的查
询,也可以通过tcpdump和query-digest工具来模拟替代。要
注意找到吞吐量突然下降时间段的日志。查询是在完成阶段
才写人到慢查询日志的,所以堆积会造成大量查询处于完成
阶段,直到阻塞其他查询的资源占用者释放资源后,其他的
查询才能执行完成。这种行为特征的一个好处是,当遇到吞
吐量突然下降时,可以归咎于吞吐量下降后完成的第一个查
询(有时候也不一定是第一个查询。当某些查询被阻塞时,
其他查询可以不受影响继续运行,所以不能完全依赖这个经
验)。
再重申一次,好的工具可以帮助诊断这类问题,否则要
人工去几百GB的查询日志中找原因。
例子:根据MySQL每秒将当前时间写人日志中的模式统计每秒的
查询数量
从上面的输出可以看到有吞吐量突然下降的情况发生,
而且在下降之前还有一个突然的高峰,仅从这个输出而不
去查询当时的详细信息很难确定发生了什么,但应该可以
说这个突然的高峰和随后的下降一定有关联。不管怎么说,
这种现象都很奇怪,值得去日志中挖掘该时间段的详细信
息(实际上通过日志的详细信息,可以发现突然的高峰时
段有很多连接被断开的现象,可能是有一台应用服务器重
启导致的。所以不是所有的问题都是MySQL的问题)。
上面的输出结果有时候还需要的工具:
因为上面的工具诊断时可能产生大量的输出结果,所
以我们需要一些绘图工具,如gnuplot或R或其它的绘图工
具,将结果绘制成图形。
捕获诊断数据(这种方式似乎不仅适用于MySql,而且还适用于其它服务
器)
该段讲解应该捕获什么样的问题即如何设计触发器
当出现间歇性问题时,需要尽可能多地收集所有数据,而不只
是问题出现时的数据。虽然这样会收集大量的诊断数据,但总比真
正能够诊断问题的数据没有被收集到的情况要好。
如何设计捕获数据的触发器?
* 寻找合适的指标去监控,使用这些触发触发器的执行
设计一个触发器的思路是找到一些和正常阈值进行比较的
指标,这通常是一个计数。比如前面例子展示的
Threads_running的趋势出现问题时比较敏感,而没有问
题时则比较平稳;另外SHOWPROCESSLIST中线程的异
常;SHOW INNODB STATUS的特定输出、服务器的平均
负载尖峰等。举个例子,统计正在运行的线程数量、处于
“freeing items”状态的线程数量:
* 指标阈值大小的选取
选择一个合适的阈值很重要,既要足够高,以确保在正常
时不会被触发;又不能太高,要确保问题发生时不会错过。
另外要注意,要在问题开始时就捕获数据,索引不能将阈
值设置得太高。问题持续上升的趋势一般会导致更多的问题发
生,如果在问题导致系统快要崩溃时才开始捕获数据,就很难
诊断到最初的根本原因。例如Threads_Connected偶尔会出现
非常高的尖峰值,在几分钟时间内会从100冲到5000或者更高,
所以设置阈值为4999也可以捕获到问题,但为什么非要等到这
么高的时候才收集数据呢?如果在正常时该值一般不超过150,
将阈值设置为200或者300会更好。
回到前面关于Threads-running的例子,正常情况下的并发
度不超过10。但是阈值设置为10并不是一个好注意,很可能会
导致很多误报。即使设置为15也不够,可能还是会有很多正常
的波动会到这个范围。当并发运行线程到15的时候可能也会有
少量堆积的况,但可能还没到问题的引爆点。但也应该在糟糕
到一眼就能看出问题前就清晰地识别出来,对于这个例子,我
们建议阀值可以设置为20。
我们当然希望在问题确实发生时能捕获到数据,但有时候
也需要稍微等待一下以确保不是误报或者短暂的尖峰。所以,
最后的触发条件可以这样设置:每秒监控状态值,如果
Threads-running连续5秒超过20,就开始收集诊断数据(顺便
说一句,我们的例子中问题只持续了3秒就消失了,这是为了
使例子简单而设置的。3秒的故障不容易诊断,而我们碰到过
的大部分问题持续时间都会更长一些)。
我们可以利用工具pt-stalk来监控服务器(自己写脚本也
可以,但是太麻烦)。它可以配置需要监控的变量、阈值、检查
的频率等。
* 通过触发器去收集数据
触发器已经定义好了,那么我们现在定义需要通过触发器
收集的数据。
需要收集什么样的数据呢?答案是尽可能收集所有能收集
的数据,但只在需要的时间段内收集。包括系统的状态、CPU
利用率、磁盘使用率和可用空间、ps的输出采样、内存利用率,
以及可以从MySQL获得的信息,如SHOWSTATUS、
SHOWPROCESSLIST和SHOW INNODBSTATUSO这些在诊断
问题时都需要用到(可能还会有更多)。
执行时间包括用于工作的时间和等待的时间。当一个未知问
题发生时,一般来说有两种可能:服务器需要做大量的工作,从
而导致大量消耗CPU;或者在等待某些资源被释放。所以需要用
不同的方法收集诊断数据,来确认是何种原因:剖析报告用于确
认是否有太多工作,而等待分析则用于确认是否存在大量等待。
如果是未知的问题,怎么知道将精力集中在哪个方面呢?没有更
好的办法,所以只能两种数据都尽量收集。
第三章重94页开始没有记录,有空在去看
2.剖析
2.慢查询日志
顾名思义就是“慢”的SQL的查询日志。慢查询是MySql当前版本中是开销最低,
精度最高的测量时间的查询工具。不必担心慢查询日志会带来额外的1/0开销。
我们在1/0密集型场景做过基谁测试,慢查询日志带来的开销可以忽略不计(实际
上在CPU密集型场景的影响还稍微大一些)。而需要担心的是日志可能消耗大量
的磁盘空间。如果长期开启慢查询日志需要部署日志轮转工具(log rotation),
或者不要长期启用慢查询日志。
1.步骤
(1)检查查询是否像数据库请求了不必要的数据,及是否只返回了需要的数
据。
* 检查查询是否查询了不需要的记录,加limit进行限定。
* 总是取出全部的列
总是取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为
服务器带来额外的IO、内存和CPU的消耗。
总是取出全部列有如下两种经典案例:
多表关联取出全部列:
select * from tablea innerjoin tableb on tablea.id=tableb.aid
可以改为:
select column1,column2,column3,... from tablea innerjoin tableb
on tablea.id=tableb.aid
或:
select tablea.* from tablea innerjoin tableb on tablea.id=tableb.aid
单表查出全部列:
select * from tablea
改为:
select column1,column2,column3,...from tablea
** 注:关于是否应该总是取出全部列的探讨**
缺点:总是取出全部列会让优化器无法完成索引覆盖扫描这类优
化,还会为服务器带来额外的IO、内存和CPU的消耗。因
此一些DBA严禁select * 这种写法,这样做有时候还能避免
有些列被修改带来的问题。
优点:简化和加快开发
如果可以不计较这些性能损失,我们可以查出全部列。
* 重复查询相同数据
注意这时候要使用缓存
(2)MySql是否在扫描额外的记录
需要补充的问题:
常用的SQL优化器优化的策