MySQL 慢查询优化
为什么查询速度会慢
1.慢是指一个查询的响应时间长。一个查询的过程:
- 客户端发送一条查询给服务器
- 服务器端先检查查询缓存,如果命中了缓存,则立可返回存储在缓存中的结果。否则进入下一个阶段
- 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。
- MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
- 将结果返回给客户端
2.数据访问
- 是否向数据库请求了不需要的数据
- 是否扫描额外的记录
3.查询的方式
- 一个复杂的查询还是多个简单的查询
- 切分查询(将大查询切分成小查询,循环完成小查询)
- 分解关联查询
慢查询分析
问题SQL
把复杂的SQL分成多个简单SQL并执行,查看具体那个字段会慢,区分度不高。
EXPLAIN
显示SQL如何使用索引的执行计划。
执行计划的参数:
table 显示这一行的数据是关于哪张表的
type 显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和ALL
possible_keys 显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从WHERE语句中选择一个合适的语句
key 实际使用的索引。如果为NULL,则没有使用索引。很少的情况下,MYSQL会选择优化不足的索引。这种情况下,可以在SELECT语句中使用USE INDEX(indexname)来强制使用一个索引或者用IGNORE INDEX(indexname)来强制MYSQL忽略索引
key_len 使用的索引的长度。在不损失精确性的情况下,长度越短越好
ref 显示索引的哪一列被使用了,如果可能的话,是一个常数
rows 扫描请求数据的行数
Extra 关于MYSQL如何解析查询的额外信息
PROFILE
显示SQL执行消耗系统资源的信息。
查询执行的过程
MySQL客户端/服务器通信协议是“半双工”的。客服端/服务器端都可以向对方发送数据,但不能同时发生。所以我们无法也无须将一个消息切成小块独立来发送。
这种协议没办法进行流量控制。
客户端发送请求的数据包大小由参数max_allowed_packet限制。如果查询太大,服务端会拒绝接受更多的数据并抛出相应的错误。
服务器端返回的多个数据包,客户端必须完整接受。
1.查询状态 SHOW FULL PROCESSLIST
mysql>SHOW FULL PROCESSLIST; Id User Host db Command Time State Info ------ ------ --------------- ------------ ------- ------ ------ ----------------------- 1 root localhost:61316 laravel_blog Query 0 (NULL) show FULL processlist 2 root localhost:61319 (NULL) Sleep 94 (NULL)
对于一个连接,或者说一个线程,任何时刻都有一个状态,该状态表示了MySQL当前正在做什么。
2.查询缓存
-- 查看缓存是否开启 (query_cache_type 为 ON 表示已经开启
mysql> show variables like '%query_cache%';
+------------------------------+----------+ | Variable_name | Value | +------------------------------+----------+ | have_query_cache | YES | | query_cache_limit | 1048576 | | query_cache_min_res_unit | 4096 | | query_cache_size | 20971520 | | query_cache_type | ON | | query_cache_wlock_invalidate | OFF | +------------------------------+----------+
检查sql是否命中缓存。命中则检查一次用户权限后返回,这个检查是通过一个对大小写敏感的哈希查找实现的。两次查询只要有一个字节的不同就会失败。否则将进入下一个阶段。
当sql中有不确定的数据时,则不会被缓存。例如用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,其查询结果都不会被缓存。
3.查询优化
语法解析器和预处理
MySQL通过关键字将sql语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如sql中是否使用了错误的关键字或者关键字的顺序是否正确等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等。
查询优化器
经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成查询计划。多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,通过计算成本选择其中最小的一个。通过SHOW STATUS LIKE 'Last_query_cost';查看成本。成本的最小单位是随机读取一个4K数据页的成本。
MySQL的查询优化器是一个非常复杂的部件,它使用了非常多的优化策略来生成一个最优的执行计划:
-
-
- 重新定义关联表的顺序
- 将外连接转化成内连接
- 使用等价变换规则
- 优化count()、min()、max()
- 预估并转化为常数表达式
- 覆盖索引描述
- 子查询优化
- 提前终止查询
- 等值传播
- 列表IN()的比较
-
上面列举了一些,随着MySQL的不断发展,优化器使用的优化策略也在不断的进化。
查询执行引擎
在完成解析和优化阶段以后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为handler API。查询过程中的每一张表由一个handler实例表示。实际上,MySQL在查询优化阶段就为每一张表创建了一个handler实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。
返回结果
查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等。
如果查询缓存被打开且这个查询可以被缓存,MySQL也会将结果存放到缓存中。
结果集返回客户端是一个增量且逐步返回的过程。有可能MySQL在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。需要注意的是,结果集中的每一行都会以一个满足①中所描述的通信协议的数据包发送,再通过TCP协议进行传输,在传输过程中,可能对MySQL的数据包进行缓存然后批量发送。
性能优化
优化count()查询
count()是一个特殊的函数。可以统计行数、某个列值的数量。在统计列值时要求列值是非空的(不统计NULL)。在统计行数时count(*)不会被扩展成所有的列,而是忽略所有的列。这样写意义清晰,性能好。
-
- 在不要求完全精确时,EXPLAIN返回一个优化器估算的近似值
- 快速,精确和实现简单,三者永远只能满足其二,必须舍掉其中一个。增加一个汇总表也是
优化LIMIT分页
LIMIT 10000,20这样的查询,MySQL需要查询10020条记录后返回最后20记录。一般优化为WHERE id>10000 LIMIT 20。其他优化关联一个冗余表,冗余表只包含主键列和需要排序的数据列。
数据类型优化
选择数据类型的原则:更小的通常更好、简单就好、尽量避免NULL。
更小的数据类型通常会更快,因为占用更少的磁盘、内存和CPU缓存。
简单的数据类型需要更少的CPU周期。例:int比char的操作代价低。
这里总结几个可能容易理解错误的技巧:
-
通常来说把可为NULL的列改为NOT NULL不会对性能提升有多少帮助,只是如果计划在列上创建索引,就应该将该列设置为NOT NULL。
-
对整数类型指定宽度,比如INT(11),没有任何卵用。INT使用32位(4个字节)存储空间,那么它的表示范围已经确定,所以INT(1)和INT(20)对于存储和计算是相同的。
-
UNSIGNED表示不允许负值,大致可以使正数的上限提高一倍。比如TINYINT存储范围是-128 ~ 127,而UNSIGNED TINYINT存储的范围却是0 - 255。
-
通常来讲,没有太大的必要使用DECIMAL数据类型。即使是在需要存储财务数据时,仍然可以使用BIGINT。比如需要精确到万分之一,那么可以将数据乘以一百万然后使用BIGINT存储。这样可以避免浮点数计算不准确和DECIMAL精确计算代价高的问题。
-
TIMESTAMP使用4个字节存储空间,DATETIME使用8个字节存储空间。因而,TIMESTAMP只能表示1970 - 2038年,比DATETIME表示的范围小得多,而且TIMESTAMP的值因时区不同而不同。
-
大多数情况下没有使用枚举类型的必要,其中一个缺点是枚举的字符串列表是固定的,添加和删除字符串(枚举选项)必须使用ALTER TABLE(如果只只是在列表末尾追加元素,不需要重建表)。
-
schema的列不要太多。原因是存储引擎的API工作时需要在服务器层和存储引擎层之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列,这个转换过程的代价是非常高的。如果列太多而实际使用的列又很少的话,有可能会导致CPU占用过高。
-
大表ALTER TABLE非常耗时,MySQL执行大部分修改表结果操作的方法是用新的结构创建一个张空表,从旧表中查出所有的数据插入新表,然后再删除旧表。尤其当内存不足而表又很大,而且还有很大索引的情况下,耗时更久。当然有一些奇技淫巧可以解决这个问题,有兴趣可自行查阅。
-
索引的设计
索引的优点:大大减少了服务器需要扫描的数据量、帮主服务器避免排序和临时表、可以将随机I/O变为顺序I/O;
“三星系统”:索引将相关的记录放到一起则获得一星;如果索引中的数据顺序和查找中的排序顺序一致则获得二星;如果索引中的列包含了查询中需要的全部列则获得三星。
-
- 独立的列:索引列不能是表达式的一部分,也不能是函数的参数。索引选择区分度高。
- 前缀索引:如果列很长,通常索引开始的部分字符,可以大大节约索引空间,从而提高索引效率。
- 联合索引:当多个索引and时,通常是一个包含所有相关列的索引好过多个单独索引;当多个索引or时,通常是分开查询好过单次查询。
注:如果在EXPLAIN中看到有索引合并(Extra字段出现Using union),应该好好检查一下查询和表的结构,看是不是已经是最优的。
参考资料
Baron Scbwartz 等著;宁海元 周振兴等译;高性能MySQL(第三版); 电子工业出版社, 2013