MySQL优化指南

MySQL优化指南

数据类型优化

原则

  1. 更小的通常更好:更小的数据类型通常更快,因为他们占用更少的磁盘、CPU和缓存,并且处理时需要的CPU周期更少。
  2. 简单就好:简单数据类型的操作需要更少的CPU周期,整型比字符操作代价更低,使用整型存储IP地址。
  3. 尽量避免NULL:NULL会使得索引、索引统计、值比较更复杂,需要更多的存储空间,索引列尽量可NULL。

当一个列可以选择多种数据类型时,应该优先考虑数字类型,其次是日期或二进制,最后是字符串。对于相同级别的数据类型,应该优先选择占用空间小的数据类型。

数字型

整数型
image

在定义表时的INT(N)仅用来显示字符的个数,不会限制值的合法范围。

实数型
  1. Float(4字节)和Double(8字节)支持标准浮点运算,运算速度较快。
  2. Decimal支持精确计算,最多支持65个数字,decimal(18,9)小数点两边各存储9个数字,一共使用9个字节,每9个数字需要4个字节,运算速度较慢。
  3. 在数据量较大时,可以考虑使用BIGINT代替DECIMAL,这样可以同时避免浮点运算不精确和DECIMAL精确运算代价高的问题。

字符串类型

VARCHAR
  1. 存储格式:VARCHAR使用1或2个额外字节记录字符串长度。VARCHAR节省了存储空间,所以对性能有帮助,但是UPDATE时需要做额外的工作,取决于存储引擎、如叶分裂。
  2. 适用场景:最大长度远大于平均长度,MEMORY引擎只支持定长列,所以会使用最大长度分配;列更新很少;使用了UTF8这样的复杂字符集。
CHAR
  1. 存储格式:MySQL存储时会删除所有的末尾空格,需要根据情况需要处理空格的剔除和填充问题。不易产生碎片。
  2. 适用场景:短字符串或长度接近的字符串,如MD5;对经常变更的数据更加友好;较短的列在存储上更有效率。
TEXT
  1. 类型限定:TINYTEXT、SMALLTEXT(TEXT)、MEDIUMTETXT、LONGTEXT,长度不同,用于保存长字符串。
  2. 存储方式:当存储太大时,使用专门的外部存储保存,行内使用指针访问
  3. 排序方式:只对每列的max_sort_length个字节排序。
  4. 索引方式:无法对TEXT的全部字符串进行索引,只会索引前缀。
  5. 临时表方式:MEMORY不支持TEXT,所以如果使用了临时表,将不得不用MYISAM磁盘临时表,尽量避免使用TEXT,或者使用SUBSTRING()函数进行转换。如果Explain的Extra中包含了Using temporary,则使用了临时表。

二进制型

VARBINARY、BINARY、 BLOB
  1. 关系同VARCHAR、CHAR、TEXT
  2. 不同点:二进制保存字节码而不是字符;二进制使用\0进行填充而不是空格;二进制按字节值而不是排序集比较,更简单更快。

枚举型

  1. 原理:把一系列不重复的字符串存储为一个预定义的集合,在.frm的
    保存数字-字符串的查找表,在表中实际存储为整数。
  2. 存储方式:因为在表中实际存储为整数,所以存储空间较小。
  3. 排序方式:排序时按照内部定义的这个整数而不是字符串字面量进行排序,取决于集合定义的顺序。
  4. 不好的地方:字符串列表是固定的,添加或者删除必须Alter Table,且只能在末尾添加元素。

时间类型

image
TIMESTAMP
  1. 存储格式:当用户把一个"2019-07-02 00:23:00"这样的数据写入到TIMESTAMP类型的数据时,会先按当前时区转换为UTC时间,然后把对应时间的时间戳写入到数据库中,在取出时,会把时间戳转换为当前时区的对应时间进行显示。
  2. 数据库保存的是时间戳,在写入和读出时,如果需要转换,则会考虑当前时区。
  3. 使用4个字节,比DATETIME效率更高。
  4. 默认情况下,如果插入时没有指定第一个TIMEDSTAMP列的值,则会只设置这个列的值为当前时间;在插入一行记录时,默认也会更新第一个TIMESTAMP列的值。
DATETIME
  1. 存储格式:把日期和时间封装到格式为YYYYMMDDHHMMSS[.fraction]的整数中,显示时和时区无关,可以认为保存的是“字符串”。
毫秒
  1. 使用BIGINT类型存储毫秒级别的时间戳;
  2. 使用DOUBLE类型存储毫秒,如12.345s;
  3. 使用MariaDB替代Mysql。
  4. MySQL5.6后DATETIME和TIMESTAMP最大可以保存微秒,使用DATETIME(6)和TIMESTAMP(6)定义。

位数据类型(技术上是字符串类型)

BIT
  1. Myisam会打包存储所有的BIT列,但InnoDB不会;
  2. MySQL把BIT当作字符串类型;值b'00111001'在字符串上下中是"9"(字符码为57),在数字上下文中,是数字57,因为这种特性,需要谨慎使用BIT类型;
  3. 如果需要保存一个bool值,可以使用CHAR(0),NULL表示false,''表示true。
SET
  1. 存储格式:以一系列打包的位的集合来表示,可以有效地利用存储空间;
  2. 缺点:改变列的定义的代价较高:需要Alter Table,也无法在SET上通过索引查找。
  3. 替代方案:使用一个整数包装一系列位,在代码中自己处理,缺点是不易读。

主键类型选择

  1. 使用整数类型,因为很快且可以AUTO_INCRMENT,递增可以使得主键聚簇索引插入是顺序的,这会避免产生碎片、页分裂、随机磁盘访问、访问局部性失效。
  2. 如果主键是UUID,去除中间的“-”,用UNHEX()转换为16字节数字,保存在BINARY(16)的数字中;UUID作为主键不如递增的整数好用。

Schema涉及陷阱

  1. 太多的列:MySQL存储引擎API工作时需要在服务器层和存储引擎之间通过行缓冲格式拷贝数据,然后在服务器层将缓冲内容解码成各个列。从行缓冲中将编码过的列转换成行数据结构的操作代价是非常高的。
  2. 太多的关联:如果希望查询执行得快速且并发性好,单个查询最好在12个表内组关联。
  3. 全能的枚举:要注意MySQL的Alter Table操作非常昂贵。
  4. 变相的枚举:当只会出现一个值时,不要使用SET来替代ENUM。
  5. NULL陷阱:避免使用NULL是好的,但是不要走极端。

范式与反范式

范式优点
  1. 范式化的更新操作通常较快;
  2. 范式化的表通常更小,可以更好的放在内存里;
  3. 范式化的冗余数据较少,会有更少的DISTINCT或者GROUP BY语句;
范式缺点
  1. 范式化的表往往需要关联
  2. 更难进行索引优化
表设计过程

可以先通过范式化设计设计出表,再按照反范式化设计提高语句的执行效率。

数据冗余
  1. 最常见的反范式化是复制或者缓存,在不同的表中存储相同的特定列。可以使用触发器更新缓存值,这使得实现这样的方案变得更简单。
  2. 另一个从父表冗余一些数据到子表的理由是排序的需要。
  3. 缓存衍生值也是有用的。
缓存表和汇总表
  1. 缓存表用户存储那些可以比较简单地从schema其他表获取(但是每次获取都比较慢)数据的表,这些数据需要特殊的表和索引结构,可以对缓存表使用不同的存储引擎。
  2. 汇总表保存使用Group By语句聚合数据的表,当遇到热点写时,可以考虑水平拆分汇总表中的行。
  3. 在使用缓存表和汇总表时,必须决定是实时维护还是定期重建,哪个更好依赖于应用程序,但是定期重建并不只是节省资源,也可以保持表不会有很多碎片,以及有完全顺序组织的索引。
  4. 当重建汇总表和缓存表时,通常需要保证数据在操作时依然可用,这需要通过影子表来实现,当完成建表操作后,通过一个原子的重命名切换。

索引优化

索引类型

在MySQL中,索引是在存储引擎层而不是服务器层实现的,不同存储引擎的索引的工作方式并不一样。

B-Tree:
  1. 实现:InnoDB使用B+Tree来实现,索引对多个值进行排序的依据是CREATE TABLE语句中定义索引时列的顺序。
  2. 适用查询类型:全值匹配、匹配最左前缀、匹配列前缀、匹配范围值、精确匹配某一列并范围匹配另外一列、只访问索引的查询、顺序查找;
  3. 限制:如果不是按照索引的最左列开始查找,则无法适用索引;不能跳过索引中的列;如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找。
哈希索引
  1. 实现:基于哈希表实现,仅Memory引擎支持。
  2. 限制:只存储hash值和行指针,不能避免回表;不按索引值排序,无法用于排序;不支持部分索引列匹配,只支持等值比较;维护代价较高。
  3. 自适应哈希索引:当InnoDB注意到某些索引值被使用的很频繁时,会在内存中基于B-Tree索引再创建一个哈希索引,但用户无法控制或配置。
  4. 自定义哈希索引:如果列存储的内容很大,可以对存储内容做哈希如url_crc(不要使用sha1和md5,哈希值太长)从而得到一个选择性高体积小的列,基于该列做查找,为处理哈希冲突,需要在where子句中进行验证;缺陷是需要维护哈希值,可以手动维护,也可以使用触发器实现。
空间数据索引
  1. 仅MyISAM支持,地理数据存储,空间索引会从所有维度来索引数据,可以使用任意维度来组合查询,MySQL的GIS支持并不完善。
全文索引
  1. 用于查找文本中的关键词,类似于搜索引擎,适用于MATCH AGAINST操作而不是WHERE条件匹配。

索引优点

  1. 大大减少服务器需要扫描的数据量;
  2. 可以帮助服务器避免排序和临时表;
  3. 可以将随机I/O变成顺序I/O。

三星索引评价

  1. 窄索引片星:索引将相关的记录放到一起;
  2. 排序星:索引中的数据顺序和查找中的排列顺序一致;
  3. 覆盖星:索引中的列包含了查询中需要的全部列。

高性能索引策略

  1. 独立的列:索引列不能是表达式的一部分,也不能是函数的参数,始终将索引
  2. 前缀\后缀索引:很长的列会使得索引变得大且慢,可以通过索引列中的部分数据,节约索引空间;要点在于要选择足够长的前缀以保证较高的选择性,同时又不能太长。选择索引长度有两种方法:一种是找到最常见的值的列表,然后和最常见的前缀列表进行比较[select count(*) as cnt, city from cities group by city order by cnt desc limit 10,select count(*) as cnt, left(city, t) as pref from cities group by pref order by cnt desc limit 10];另一种是计算完整列的选择性,并使前缀的选择性接近于完整列的选择性[select count(distinct city)/count(*) from cities, select count(city, t)/count(*) from cities]。前缀索引能使索引更小、更快,但是前缀索引无法用于排序,也无法进行索引覆盖。
  3. 多列索引:为每个列创建独立的索引或者按照错误的顺序创建多列索引是错误的。对于多列索引,MySQL可能会使用“索引合并”的策略进行优化,可以通过Extra看到,但实际上这种策略会有很多隐形的开销,多数时候这种策略意味着索引建立的很糟糕。
  4. 选择合适的索引列顺序:在不考虑排序和分组时,将选择性最高的列放在前面通常是很好的。然而,性能不仅依赖于索引列的选择性,也和查询条件的值分布有关。可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。
  5. 聚簇索引:聚簇表示数据行和相邻的键值紧凑地存储在一起,InnoDB只聚集在同一个页面中的记录,包含相邻键值的页面可能会相距甚远。因为聚簇索引的特性,插入速度严重依赖于插入顺序,为了避免插入时的页分裂和页碎片,我们尽量按主键顺序插入行,可以通过一个自增的、应用无关的主键来实现。所有的插入都发生在主键的上界时,可能会造成争用现象,可以通过重新设计表、应用或者更改innodb_autoinc_lock_mode配置来解决。
  6. 覆盖索引:索引中包含所有需要查询的字段的值,在Extra列中可以看到“Using index”。注意:如果查询优化器发现索引只是覆盖了WHERE条件中的字段,而不是整个查询设计的字段,在5.5之前也会进行回表过滤,这时候可以使用延迟关联技巧,在5.5之后由于MySQL支持了索引条件下推,会使用索引字段进行过滤。
  7. 索引扫描:MySQL可以通过两种方式生成有序的结果:一是排序,二是索引扫描,索引扫描时会在type列中显示“index”。索引扫描很快,但是如果无法使用索引覆盖,则会每扫描一条记录就要回表,而回表操作是随机IO,所以一般索引扫描会比顺序全表扫描要慢。只有当索引的列顺序和ORDERBY子句顺序完全一致且列方向一致时,才能使用索引进行排序。如果是多表关联,则只有当ORDERBY子句引用的字段全为第一张表时,才能使用所以进行排序。ORDERBY子句的限制和查找行查询限制相同。当WHERE子句或者JOIN子句中对前导列指定了常量,仍然可以使用索引扫描。
  8. 压缩索引:MyISAM可以通过前缀压缩来减少索引的大小,但是代价是某些操作更慢,因为前缀压缩依赖于前面的值,所以MyISAM查找时无法在索引块使用二分查找而只能从头开始扫描。
  9. 冗余和重复索引:指在相同的列上按照相同的顺序创建相同类型的索引,会影响查询性能(优化器选择索引),也会影响插入性能(索引的维护成本),应该立即移除。MySQL的唯一限制和主键限制都是通过索引实现的。尽量扩展已有的索引而不是创建新索引,特殊情况下考虑性能可以创建冗余索引,因为大索引会影响其它查询语句的性能。可以通过pt工具箱中的pt-duplicate-key-checker检查冗余和重复索引,通过pt-upgrade检查索引变更。
  10. 未使用的索引:永远不用的索引建议删除,可以使用pt-index-usage找到未使用的索引。
  11. 索引和锁:InnoDB只有在访问行的时候才会对其加锁,而索引能够减少InnoDB访问的行数,从而减少锁的数量。MySQL在InnoDB存储层通过索引找到并通过索引条件过滤出相关的行进行加锁,服务器层通过where条件将不符合条件的行解锁,剩下的将会是要锁定的行。如果不能使用索引查找并锁定行的话,将会做全表扫描并锁住所有的行。InnoDB在二级索引上使用共享锁,在访问主键索引需要排他锁,这会使得无法使用索引覆盖,进而使得select for update比lock in share mode和非锁定读慢得多。

索引维护

  1. 找到并修复损坏的表:Check Table,Replair Table,no-op的Alter,innodb_force_recovery,InnoDB Data Recovery Toolkit。
  2. 更新索引统计信息:MySQL优化器基于成本模型(需要扫描多少行)来衡量执行计划,可以通过ANALYZE TABLE来重新生成更准确的统计信息,可以通过参数innodb_stats_sample_pages来设置抽样索引页面的取样数。在打开Information_schema或者show table status或者show index时触发索引统计信息更新,这可能会导致大量的锁。
  3. 减少索引和数据的碎片:对于数据碎片(行碎片、行间碎片、剩余空间碎片)可以通过OPTIMIZE TABLE或者no-op操作来重新整理数据。对于索引碎片,可以通过"在线"删除重建索引来消除碎片。应该通过实际测量而不是随意假设来确实是否需要消除索引和表的碎片化,可以使用XtraBackup的--stats参数来获取索引和表的统计情况。

查询优化

查询速度慢的原因

  1. 查询是从一系列子任务完成的,大概包括:从客户端,到服务器,语句解析,生成查询计划,执行,返回结果到客户端。
  2. 在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划,锁等待,存储引擎检索数据、可能的上下文切换和系统调用等操作。
  3. 我们优化查询的目的是减少和消除这些操作所花费的时间。

慢查询基础:优化数据访问

  1. 确认应用程序是否在检索大量超过需要的数据,通常意味着请求了不必要的行或者列,比如:没用LIMIT,SELECT *,不做去重聚合重复查询
  2. 确认MySQL服务器层是否在分析大量超过需要的数据行。衡量查询的指标:响应时间,扫描行数,返回行数,这些会记录在慢日志中,在Explain中可以看到预估扫描行数。理想情况下扫描行数和返回行数应该是相同的,一般情况下在1:1和10:1之间。当扫描行数远大于返回行数时,需要优化:使用索引覆盖,改变库表结构,重写查询。
  3. Where条件过滤方式,从好到坏分别是:在存储引擎层利用B+树索引使用Where条件直接过滤;使用索引覆盖扫描来返回记录到服务器层,在服务器层索引结果中过滤,在5.6以后通过索引条件下推可以直接在存储引擎层过滤;在服务器层从数据表取出记录进行Where条件过滤。

重构查询方式

  1. 水平切分大查询以减轻服务器瞬时压力:将大查询切分成小查询,如定期清理大量数据,每个查询功能完全一样,只完成一小部分,每次返回一小部分查询结果,可以防止锁住大量数据行、占满事务日志、耗尽系统资源、阻塞重要查询、主从延迟过高,瞬时压力过大等。
  2. 分解关联查询转化为应用层关联:可以做应用程序层单表数据缓存;单表查询可以减少锁竞争;可以使用IN()代替关联查询提高查询效率;应用层关联易于数据库拆分以高性能、高扩展;相当于在应用的哈希关联代替MySQL的嵌套循环关联等。

查询执行基础

查询执行流程
客户端发送查询给服务器
  1. MySQL客户端和服务器之间的通信协议是半双工的,客户端用一个单独的数据包将查询发送给服务器,服务器返回的数据通常较多,由多个数据包组成,一旦客户端开始接受,就必须接受完整个返回结果,所以必要时要加上LIMIT。
  2. MySQL客户端库函数一般有两种服务处理MySQL发来的数据:默认是获取全部结果缓存到内存中,此时库函数会花费较长时间和内存来存储结果集,MySQL服务器友好;另一种是逐行读取数据,此时MySQL必须要保留查询相关的数据直到客户端读取完成才能释放,MySQL客户端友好。
服务器检查查询缓存
  1. 如果命中缓存直接返回缓存结果,查询缓存按查询语句缓存,查询缓存检查通过一个对大小写敏感的哈希查找实现。
服务器进行SQL解析和预处理,优化器生成执行计划
  1. 解析SQL语句,生成解析树,使用MySQL语法规则验证和解析查询。
  2. MySQL使用基于成本的优化器尝试预测一个查询使用某种执行计划时的成本,可以通过查询当前会话的last_query_cost值来得知成本值,单位是随机查找数据页的次数。
  3. 多种原因会导致MySQL优化器不准确,从而选择错误的执行计划
    • 统计信息不准确;
    • 优化器在评估成本时不考虑任何层面的缓存而是假设读取任何数据都需要一次磁盘I/O,从而使得执行计划中的成本估算不等同于实际执行的成本;
    • MySQL选择的是随机查找数据页次数最少的,而我们希望是响应时间最少的;
    • 从不考虑其它并发执行的查询;
    • 并不是所有时候都是基于成本的,也有基于固定规则的,如MATCH子句总是优先使用全文索引;
    • 不考虑不受其控制的操作的成本,如存储过程和用户自定义函数;
    • 有时候无法估算所有可能的执行计划。
  4. MySQL能够处理的部分优化类型如下,不要自认为比优化器更聪明,让优化器按照它的方式工作:
    • 重新定义表的关联顺序;
    • 将一部分外连接转化为内连接;
    • 使用等价变换规则来简化并规范表达式;
    • 优化COUNT()、MIN()、MAX(),可能转化为常数;
    • 预估并转化为常数表达式;
    • 覆盖索引扫描;
    • 将子查询转换为一种效率更高的形式从而减少多个查询多次对数据的访问;
    • 提前种植查询:如LIMIT,DISTINCT,EXIST,LEFTJOIN等;
    • 等值传播:如果两个列的值通过等式关联,MySQL能够把其中一个列的WHERE条件传递到另一列上;
    • 列表IN()比较:有索引时,会先进行排序,再使用二分查找,从O(n)优化为O(logn)
    • 等等。
  5. 如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,那么也可以帮助优化器做进一步的优化:
    • 在查询中添加Hint提示;
    • 重写查询;
    • 重新设计更优的库表结构;
    • 添加更合适的索引信息等。
  6. 关联查询:
    • 原理:MySQL对任何关联查询都执行嵌套循环关联操作,即MySQL先在一个表(包括临时表)中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止,然后根据各个表匹配的行,返回查询中需要的各个列。最高复杂度为O(len(tabel1) * len(table2) * ... );
    • 优化器通过评估多个表关联时的不同关联顺序选择一个代价(读取数据页数,一般可以用读取记录数反映出来)最小的关联顺序,当len(alltables)<=optimizer_search_depth时,关联优化器会尝试len(alltables)!种关联顺序,嵌套循环计算执行代价,取最优的;否则则使用贪心搜索模式,这时可能找不到最优解。
  7. 排序优化:
    • 当不能使用索引排序时,如果数据量小,MySQL将在内存中进行快排,否则将进行磁盘排序(在内存中对数据进行分块,每块使用快排,结果放在磁盘,然后合并结果);
    • 排序需要从表中提取数据,当查询需要所有列的总长度不超过max_length_sort_data是,使用单次传输排序;否则使用两次传输排序。
    • 排序时每个记录都会分配一个足够长的定长空间来存放,该空间必须要能容纳最长的记录;
    • 在关联查询排序中,如果ORDEYBY所有列都来自第一张表,将在关联处理第一张表进行排序,此时可以利用到索引;如果不是,将先进行关联,然后在最终的临时表上进行排序,此时无法利用到任何索引。
调用存储引擎的API来执行查询
  1. 在解析和优化阶段,MySQL会生成执行计划,该执行计划是一个数据结构,在查询执行阶段,MySQL根据执行计划给出的指令调用存储引擎层提供的接口来完成查询操作。
服务器将结果返回给客户端
  1. MySQL将结果返回给客户端是一个增量、逐步返回的过程,当开始生成第一条结果时,MySQL就开始向客户端逐步返回结果集了,结果集中的每一行都会以一个满足MySQL通信协议的封包发送。
查询状态

通过SHOW FULL PROCESSLIST命令可以看到MySQL连接线程处于什么状态。

  • Sleep:线程正在等待客户端发来新的请求;
  • Query:线程正在执行查询或者正在将结果发送给客户端;
  • Locked:在MySQL服务器层,该线程正在等待表锁,存储引擎中的锁等待不会显示在线程状态中;
  • Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划;
  • Copying to tmp table [on disk]:线程正在执行查询,并把结果集复制到临时表中,在做GROUPBY\文件排序\UNION操作;
  • Sorting result:线程正在对结果集进行排序;
  • Sending data:线程可能在多个状态间传送数据,或者在生成结果集,或者在向客户端返回数据。

MySQL查询优化器的局限性

关联子查询
  1. 在早期版本的MySQL实现中,关联子查询实现很糟糕,特别是IN()的实现,可以通过将关联子查询改写成连接等方式进行优化,在新版MySQL中,关联子查询的性能得到了很大的优化;
  2. 并不是所有情况下连接都优于关联子查询,需要用测试来验证对子查询的执行计划和响应时间的假设。
UNION限制
  1. 在UNION语句中,MySQL无法将限制条件从外层推送到内层查询,使得原本能够限制返回结果的条件无法应用的内层,需要自己在UNION子句中分别使用这些条件。
  2. 各个子查询的结果可能是有序的,但是合并到临时表中的顺序不一定是有序的,需要在外层使用ORDERBY操作。
索引合并优化
  1. 当Where子句中包含多个复杂条件时,MySQL能够访问单表的多个索引以合并和交叉过滤的方式来定位需要查找的行。
等值传递
  1. 当要传递的值非常大时,等值传递在复制到关联的各个表中时,可能会非常慢。
并行执行
  1. MySQL无法利用多核特性来并行执行查询,所以不要花时间来尝试寻找并行执行查询的方法。
哈希关联
  1. MySQL并不支持哈希关联,所有的关联都是嵌套循环关联,可以通过建立一个哈希索引来曲线地实现哈希关联,或者使用MariaDB。
松散索引扫描
  1. 使用松散索引扫描的效率比全表扫描高,但目前MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引,MySQL的索引扫描需要定义一个起点和终点,即使需要的数据只是这段索引中的少数几个,仍需要扫描每一个条目。在MySQL5.6之后,松散索引扫描的一些限制通过索引条件下推方式解决了。
最大值和最小值优化
  1. MySQL对MIN()和MAX()的优化并不好,如寻找满足指定条件的最小主键列,可以通过改写语句,可以利用use主键索引扫描和limit1来完成。
在同一个表上查询和更新
  1. MySQL不允许对同一张表同时进行查询和更新,可以通过表关联来实现。

查询优化器提示

DELAYED
  1. DELAYED对INSERT和REPLACE有效,MySQL会立即返回客户端,并将插入的行数据放入缓冲区,在表空闲时批量插入,适合日志系统这类需要写入大量数据但不需要等待的语句。
STRAIGHT_JOIN
  1. 当放置在select语句之后,表示让查询中所有的表按照在语句中出现的顺序进行关联;当放置在任意两个关联表名字之间时,表示固定其前后两个表关联的顺序。
  2. 当MySQL没能选择正确的关联顺序时,或者由于关联顺序太多导致无法评估太长时(大量时间花费在statistic状态),可以大大减少优化器的搜索空间,可以使用explain查看优化器选择的关联顺序,然后使用该提示重写查询,再查看关联顺序。
SQL_SMALL_RESULT和SQL_BIG_RESULT
  1. SQL_SMALL_RESULT告诉优化器可以将结果集存放在内存中的索引临时表中,以避免排序操作;SQL_BIG_RESULT告诉优化器建议使用磁盘临时表进行排序操作。
SQL_BUFFER_RESULT
  1. 告诉优化器将查询结果放入到一个临时表,以尽快释放表锁,代价时服务器端需要更多内存。
SQL_CACHE和SQL_NO_CACHE
  1. 告诉MySQL这个结果集是否应该缓存在查询缓存中。
FOR UPDATE和LOCK IN SHARE MODE
  1. 对符合查询条件的数据行加锁,仅支持InnoDB;这两个提示会让某些优化无法正常使用,如索引覆盖扫描,应该尽可能不用,通常都有其它更好的方式实现。
USE INDEX、IGNORE INDEX和FORCE INDEX
  1. 告诉优化器使用或不使用哪些索引来查询记录,FORCE INDEX会告诉优化器全表扫描的成本会远高于索引扫描,可以使用FOR ORDER BY和FOR GROUP BY来指定是否对排序和分组有效。
SQL_CALL_FOUND_ROWS
  1. 这个提示不会告诉优化器任何关于执行计划的东西,加上该提示MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集。

优化特定类型查询

优化COUNT()查询
  1. 如果在count()的括号里指定了列或列表达式,则统计该表达式有值(非NULL)的结果数;如果使用count(*)则表示统计行数,这时*不会被扩展成所有列。
  2. Myisam的无条件count(*)很快,会从存储引擎直接获得该值,如果MySQL知道某列不可能为空,则会转化为count(*),有条件的count()速度和其它引擎一样,可以利用无条件count(*)来改写查询。
  3. 有些业务场景不要求完全精确的count值,可以直接使用explain估算出来的行数作为近似值;也可以使用汇总表或者redis缓存计数值。快速、简单、精确三者只能满足其二。
优化关联查询
  1. 确保ON或者USING子句上的列有索引,只需要在关联顺序的第二个表的相应列上创建索引。
  2. 确保GROUPBY和ORDERBY只涉及到一个表上的列,这样才可能使用索引来优化查询。
  3. 当升级MySQL时需注意:关联语法、运算符优先级等其它可能会发生变化的地方。
优化子查询
  1. 尽可能使用关联查询代替,如果是5.6可以忽略该建议。
优化GROUPBY和DISTINCT查询
  1. MySQL在内部处理时会相互转化这两种查询,它们都可以使用索引来优化;
  2. 当无法使用索引时,会使用临时表或者文件排序来做分组,可以通过SQL_BIG_RESULT和SQL_SMALL_RESULT来让优化器按照你希望的方式运行。
  3. 如果需要对关联查询做分组,如果是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列高。
  4. 在分组查询的select中直接使用非分组列通常不好(虽然可以执行,但语义上不正确),这样的结果通常是不定的,当索引改变或者优化器选择不同的优化策略时,可能会故障,建议SQL_MODE设置为包含ONLY_FULL_GROUP_BY,这样会返回一个错误提醒你重写。
  5. 如果没有通过ORDERBY子句显式地指定排序列,当使用GROUPBY子句时,结果集将会自动按照分组的字段进行排序。如果不关心结果集顺序,而这种排序又导致了需要文件排序,则可以使用GROUPBY NULL,让MySQL不再进行文件排序。
优化GROUP BY WITH ROLLUP
  1. 这个子句可以在分组统计数据的基础上再进行统计汇总,即用来得到group by的汇总信息,但可能不够优化;
  2. 可以通过explain来观察其执行计划,特别要注意分组是否是通过文件排序或者临时表实现,然后去掉with rollup看执行计划是否相同。
  3. 如果可以,尽量在应用内做统计汇总;也可以在FROM子句中嵌套使用子查询,或者通过一个临时表存放中间数据,然后和临时表执行UNION来得到最终结果。
优化LIMIT分页
  1. 在偏移量非常大的时候,limit需要扫描的行数非常多,可能会对应能造成很大的影响,要优化这种查询,要么在页面中限制分页的数量,要么是优化大偏移量的性能。
  2. 尽可能地使用索引覆盖扫描,而不是查询所有的列,然后根据需要做一次延迟关联操作再返回所需要的列。有时候也可以将LIMIT查询转换成已知位置的查询,让MySQL通过范围扫描获得对应的结果。
  3. 如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET。
  4. 其他优化方法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列,还可以使用Sphinx优化一些搜索操作。
优化SQL_CALC_FOUND_ROWS
  1. 加上这个提示之后,不管是否需要,MySQL都会扫描所有满足条件的行,然后再抛弃掉不需要的行,而不是在满足LIMIT的行数后就终止扫描,所以该提示的代价可能非常高。
  2. 可以将具体的页数换成“下一页”的按钮,我们可以通过limit参数设置为pageCount+1,来判断是否还有下一页。
  3. 另一种做法是先获取并缓存较多结果集的数据,然后让应用程序根据结果集的大小采取不同的策略。
  4. 也可以考虑使用explain结果中的rows列值作为近似值,在需要精确值时,再使用count(*)来满足需求。
优化UNION查询
  1. MySQL总是通过创建并填充临时表的方式来执行UNION查询,经常需要手工地将where、limit、orderby等子句下推到UNION的各子查询中,以便优化器进行优化。
  2. 除非确实需要服务器消除重复的行,否则就一定要使用UNIONALL。没有ALL时,MySQL会给临时表加上DISTINCT选项从而做唯一性检查,即使有ALL,也会使用临时表存储结果。
静态查询分析
  1. 可以使用pt-query-advisor解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议。
使用用户自定义变量
  1. 属性和限制:
    • 无法使用查询缓存;
    • 不能在使用常量或者标识符的地方使用自定义变量;
    • 用户自定义变量是连接范围有效的,不能做连接间通信,当使用连接池的时候可能会出现问题;
    • 自定义变量是动态类型,不能显式地声明自定义变量的类型;
    • MySQL优化器在某些场景下可能会将这些变量优化掉,而这可能导致代码不按预想的方式运行;
    • 赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定;
    • 赋值符号的优先级非常低,要使用明确的括号;
  2. 优化:
    • 优化可并列排名语句:使用三个变量实现:一个用来记录当前排名,一个用来记录前一排名,一个用户记录当前评分。
    • 避免重复查询刚刚更新的数据:更新同时返回改行信息,可以使用变量来记录刚刚更新的值而无须回表。
    • 统计更新和插入的数量:insert into t1(c1,c2) values(4,4),(2,1) on duplicate key update c1=values(c1) + 0 * ( @x := @x + 1):
    • 编写偷懒的UNION:select greatest(@found := -1,id) as id, 'users' as which_tbl from users where id = 1 union all select id, 'user_archived' where id = 1 and @found is null union all select 1, 'reset' from dual where (@found := null) is not null

Explain

基础

  1. 使用explain extended可以增加一个新列filtered,并且使用show warnings可以看到优化后的查询计划生成的查询语句;
  2. 5.6以下版本的MySQL中的explain只能解释select查询,并不会对存储程序调用和insert、update、delete或其他语句做解释。为了达到解释的目的,可以通过将任何提及的列都包含在select列表、关联子句、where子句中来转化成一个等价的访问所有相同列的select语句。5.6版本将允许解释select查询。

id列
  1. 用来标识查询所属的行,按照查询在其原始语句中出现的顺序排列。
select_type列
  1. select查询的类型:
    • simple:简单查询;
    • primary:在复杂查询中,最外层的查询;
    • subquery:简单子查询,包含在select子句中的查询;
    • derived:派生表子查询:=,在FROM子句的查询,MySQL会递归执行并将结果放到一个临时表中;
    • union: 在union中的第二个或者随后的查询;
    • union result: 用来从union匿名表检索结果的查询;
    • dependent: 意味着select依赖于外层查询中发现的数据;
    • uncacheable:意味着查询中的某些特性阻止结果被缓存到一个item_cache中。
table列
  1. 显示了对应行正在访问哪个表,可以从这一列中从上往下观察MySQL的关联优化器为查询选择的关联顺序。
  2. 将table列向左侧推倒,形成的左侧深度优先树就是执行计划,也可以使用pt-visual-explain工具来生成。
type列
  1. 访问类型,从最差到最优分别是:
    • all:全表扫描,当extra中显示"Using distinct/not exists"会提前终止;
    • index:索引扫描,优点是避免了排序,缺点是要按索引次序随机回表,当extra中显示"Using index",表示用到了索引覆盖;
    • range: 范围索引扫描,开始于索引里的某一点,返回匹配这个值域的行,当MySQL使用索引去查找一系列值时,例如IN()或者OR()列表,也会显示为范围索引扫描,但实际上是不同的访问类型,性能上有重要差异。
    • ref: 索引查找,返回所有匹配某个单个值的行,是查找和扫描的混合体,只有在非唯一性索引或唯一性索引的非唯一性前缀时才会发生。ref_or_null意味着MySQL需要在初次查找的结果里进行第二次查找以找出NULL条目。
    • eq_ref: 只返回一条记录时的索引查找,在主键索引或者唯一性索引上使用,MySQL对这类的访问优化得非常好。
    • const\system: 当MySQL能对查询的某部分进行优化并将其转换为一个常量时,就会使用这些访问类型,比如where子句为主键查询。
    • null: 意味着MySQL能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。
possible_key列
  1. 查询可以使用哪些索引,基于查询访问的列和使用的比较操作符来判断,在优化过程的早期创建。
key列
  1. MySQL决定使用的索引,这个索引在评估执行计划时查询成本最小。
key_len列
  1. MySQL在索引里可能使用的最大字节数,如果MySQL只使用索引里的某些列,可以用这个值计算出具体是哪些列,当计算列时需要把字符列的字符集也计算进去。
ref列
  1. 之前的表在key列记录的索引中查找值所用的列或者常量。
rows列
  1. MySQL估计为了找到所需的行而要读取的行数,是以内嵌循环关联计划里的循环数目,是MySQL认为它要检查的行数,不考虑是否有任何层次的缓存。
filtered列
  1. 针对表里符合某个条件(where子句或链接条件)的记录数的百分比所做的一个悲观估算。
  2. 把rows列和这个百分比相乘,可以看到MySQL估算它将和查询计划里前一个表关联的行数。
extra列
  1. using index:使用了索引覆盖;
  2. using where:MySQL服务器将在存储引擎检索行后再在服务器层进行过滤,往往意味着可以可以考虑使用索引覆盖或者索引条件下推或者延迟关联优化;
  3. using temporary:在对查询结果排序时使用了临时表。
  4. using filesort:MySQL会对结果使用一个外部索引排序,而不是索引扫描,不确定是内存排序还是磁盘排序。
  5. range checked for each record(index map:N):没有好用的索引,新的索引将在连接的每一行上重新估算。N是显示在possible_keys列中索引的位图,且是冗余的。

你可能感兴趣的:(MySQL优化指南)