前言
三驾马车:查询优化、索引优化、库表结构优化
为什么查询速度会慢
- 对应查询来说,真正重要的是响应时间
- 查询的大致生命周期
- 客户端 -> 服务器(解析、生成执行计划、执行、返回结果)-> 客户端
- 执行:包括了检索数据到存储引擎的调用以及调用后的数据处理(排序、分组等
- 查询需要在多个地方花费时间(网络、CPU计算、生成统计信息、执行计划、锁等待等
- 优化查询的目的就是减少和消除这些操作所花费的时间
慢查询基础:优化数据访问
- 查询低下最基本原因:访问的数据太多
- 分析步骤
- 应用程序是否检索大量超过需要的数据(业务层面
- MySQL服务器层是否分析大量超过需要的数据行(深度翻页 Limit 等
- 是否向数据库请求了不需要的数据
- 典型案例
- 查询不需要的记录(请求1000条,只展示10条
- 多表关联时返回全部列(通常是 SELECT * 导致
- 总是取出全部列
- 缺点:额外的 I/O 、内存和 CPU 消耗,同时无法使用索引覆盖扫描这类优化
- 优点:能提高相同代码片段的复用性
- 重复查询相同的数据
- 典型案例
- MySQL 是否在扫描额外的记录
- 衡量查询开销的三个指标(已记录在慢日志中
- 响应时间
- 扫描的行数
- 返回的行数
- 响应时间 = 服务时间 + 排队时间
- 服务时间:数据库处理这个查询真正花费的时间
- 排队时间:等待某些资源而没有真正执行查询的时间( I/O 操作、锁等待等
- 受影响:存储引擎的锁(表锁、行锁)、高并发资源竞争、硬件响应等
- 扫描的行数和返回的行数
- 用于判断查询找到需要数据的效率
- 理想情况下扫描的行数与返回的行数应该相同
- 扫描的行数和访问类型
- 访问类型:全表扫描、索引扫描、范围扫描、唯一索引查询、常数引用(慢 -> 快
- MySQL 使用三种方式应用 WHERE 条件
- 索引中使用 WHERE 条件过滤(存储引擎层
- 索引覆盖扫描返回记录,无需回表查询(MySQL 服务层
- 从数据表中返回数据,然后过滤不满足的条件(MySQL 服务器,先读出数据再过滤
- 好的索引可以让查询使用合适的访问类型,尽可能只扫描需要的数据行
- 当发现查询扫描大量行但只返回少数的行时,优化的技巧
- 索引覆盖扫描
- 改变库表结构(使用额外的汇总表
- 重写复杂的查询
- 衡量查询开销的三个指标(已记录在慢日志中
重构查询的方式
- 优化查询时,目标应该是找到一个更优的方法来获得实际需要的结果,而不是一定总数需要从 MySQL 获取一模一样的结果集
-
一个复杂查询还是多个简单查询
- 需要考虑是否可以将一个复杂的查询分成多个简单的查询(类似于 MapReduce
- MySQL 从设计上让 连接 和 断开连接 都很轻量级,返回小查询结果方面很高效
-
切分查询
- 大查询切分成多个小查询(分而治之
- 大查询可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询
- 小查询还可以减少 MySQL 复制延迟,将压力分散到一个很长的时间段中(削峰)
-
分解关联查询
-
含义:将原本需要在 MySQL 服务层的关联查询分解成多个单表查询,在应用层做关联
-
分解优势
- 缓存效率更高(单表缓存
- 单个查询可以减少锁竞争
- 更容易对数据库进行拆分,高性能和可扩展性
- 查询本身效率也会提升(使用 IN ( ) 代替关联查询
- 减少冗余记录的查询
- 相当于在应用中实现了哈希关联,效率更高(MySQL 用的嵌套循环
-
查询执行的基础
MySQL 执行一个查询的过程
-
MySQL 客户端 / 服务器通信协议
- 客户端与服务器之间的通信协议是 半双工 的
- 同一时刻,要么是 客户端 -> 服务器,要么是 服务器 -> 客户端
- 无法也无须将一个消息切成小块独立来发送
- 存在一个明细的限制:无法进行流量控制
- 一旦一端开始发送消息,另一端要接收完整个消息才能响应它
- 关联场景:在必要的时候一定要在查询中加上 LIMIT 限制
- 查询状态(SHOW FULLPROCESSLIST 命令
- Sleep:线程等待客户端发送新的请求
- Query:线程正在执行查询 或者 正在将结果发送给客户端
- Locked:MySQL 服务器层,该线程正在等待表锁(InnoDB 实现的行数不会体现在线程状态中
- Analyzing and statistics:线程正在收集存储引擎的统计信息并生产执行计划
- Copying to tmp table [on disk ]:线程正在执行查询,并将结果集复制到临时表中
- 通常发生在 GROUP BY 操作、文件排序、UNION操作
- 若状态带上“on disk”标记,意味着正在将一个内存临时表放到磁盘上
- Sorting result:现在正在对结果集进行排序
- Sending data:线程可能在多个状态间传送数据 或 生成结果集 或 向客户端返回数据
- 客户端与服务器之间的通信协议是 半双工 的
-
查询缓存
- 解析查询语句时,若允许查询缓存,会优先检查这个查询是否命中缓存中的数据(哈希查找实现
-
查询优化处理
- 执行步骤
- SQL -> 执行计划 -> 存储引擎交互
- 包含子阶段:解析SQL、预处理、优化SQL执行计划(会进行语法强校验
- 语法解析器和预处理
- SQL -> 解析树(解析器验证语法规则和解析查询,包括关键字,关键字的顺序,符合等
- 预处理器根据规则进一步校验解析树(数据表、数据列是否存在,列名是否存在歧义等
- 预处理器验证权限
- 查询优化器
- 一条查询可能存在多种执行计划,优化器的作用就是找到其中最好的执行计划
- MySQL 使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择成本最小的
- SHOW STATUS LIKE ‘Last_query_cost’:当前查询的成本
- 根据每个表或者索引的页面个数、索引的基数、索引和数据行的长度、索引分别情况等计算而得
- 不考虑任何缓存,假设读取任何数据都需要一次磁盘 I/O
- 导致 MySQL 优化器选择错误的执行计划的主要原因
- 统计信息不准确(存储引擎提供的统计信息可能不精确,例如 InnoDB
- 执行计划中的成本估算不等同于实际执行的成本(顺序读与随机读,是否缓存等
- 基于成本模型选择的最优执行计划可能不是最快执行的方式
- MySQL 不考虑并发执行的查询,这可能会影响当前查询速度
- MySQL 不是任何时候都基于成本的优化,有时会基于一些固有的规则(全文搜索时优先全文索引
- 不考虑不受其控制的操作成本(执行存储过程或自定义函数的成本
- 有时无法估算索引可能的执行计划,导致选择非最优执行计划
- 优化策略
- 静态优化(编译时优化):直接对解析树进行分析(简单的代数变换、WHERE 条件等价转换等
- 动态优化(运行时优化):与查询的上下文等其他因素有关(WHERE 条件取值、索引对应行数等
- 单次查询中,存在一次静态优化和多次动态优化
- 优化类型
- 重新定义关联表的顺序
- 将外连接转化成内连接
- 使用等价变换规则
- 优化 COUNT( )、MIN( )、MAX( )
- 最大最小值可以直接查询对应 B-Tree 索引的最左、最右的节点记录
- 没有任何 WHERE 条件的 COUNT(*) 可以使用存储引擎上提供的统计信息中总行数(MyISAM
- 预估并转行为常数表达式(MySQL 有时可以将一个表达式甚至一个查询作为常数进行优化处理
- 覆盖索引扫描
- 子查询优化
- 提前终止查询
- 等值传播
- 列表 IN( ) 的比较(将 IN( ) 列表中的数据先排序在进行二分查找判断是否满足条件
- 前人之鉴:不要自以为比优化器更聪明
- 如果能够确认优化器给出的不是最优解,并清楚背后的原理,那就可以为所欲为
- 数据和索引的统计信息
- 统计信息由存储引擎实现,不同存储引擎可能会存储不同的统计信息
- 统计信息:每个表或者索引的页面个数、索引的基数、索引和数据行的长度、索引分别情况等
- MySQL 如何执行关联查询
- MySQL 中关联的定义:每一个查询,每一个片段(子查询,单表的 SELECT)都可能是关联
- 关联执行的策略:嵌套循环关联
- 遇到子查询是,先执行子查询并将其结果放到一个临时表,然后再将其当作一个普通表对待
- 提示:MySQL 的临时表是没有任何索引的,复杂的子查询得当心!union 查询也一样
- 执行计划
- 其他关系数据库:通过生成查询字节码来执行查询
- MySQL:生成查询的一棵指令树,存储引擎执行完成这棵指令树
- EXPLAIN EXTENDED —> SHOW EXTENDED
- MySQL 总是从一个表开始一直嵌套循环、回溯完成所有表关联,是一棵左侧深度优先树
- 关联查询优化器
- 关联查询优化器通过评估不同顺序时的成本来选择一个代价最小的关联顺序
- 方向:扫描更少的行数,进行更少的嵌套循环和回溯操作
- 当需要关联的表超过
optimizer_search_depth
的限制时,会选择“贪婪”搜索模式 - 各个查询的顺序至关重要,后面的表的查询可能需要依赖前面表的查询结果
- 关联查询优化器通过评估不同顺序时的成本来选择一个代价最小的关联顺序
- 排序优化
- 排序是一个成本很高的操作,应尽可能避免排序或避免对大量数据排序
- 数据量小时,在内存中进行排序(快排
- 数据量大时,需要使用磁盘进行排序(分区,快排,合并
- 比较的对象是“排序缓冲区”,同时两者都称为:文件排序
- 排序算法
- 两次传输排序(读取行指针和需要排序的字段,排序完成后,需要再回表查询
- 单次传输排序(读取查询所需要的所有列,再根据需要排序的列进行排序,完成后直接返回结果
- 当关联查询的时候需要排序时
- 如何排序的字段均来自于关联的第一张表,那么在关联处理第一个表时就进行文件排序
- 特征:Extra 字段显示 “Using filesort”
- 除此之外的所有情况,都需要将关联结果存放在临时表中,关联结束后,进行文件排序
- 特征:Extra 字段显示 “Using temporaty;Using filesort”
- 即使存在 LIMIT 关键字,临时表和需要排序的数据仍然会非常大
- 如何排序的字段均来自于关联的第一张表,那么在关联处理第一个表时就进行文件排序
- 排序是一个成本很高的操作,应尽可能避免排序或避免对大量数据排序
- 执行步骤
-
查询执行引擎
- 查询执行引擎根据执行计划来完成整个查询(执行计划是一个数据结构,而非字节码
- “handler API”:存储引擎实现的接口
- 查询中每一个表由一个 handler 的实例表示
- 实例包含的信息:所有列名、索引统计信息等
- 并不是索引的操作均由 handler 完成(例如表锁
-
返回结果给客户端
- 如果允许缓存,会先将结果存放到查询缓存中,再返回给客户端
- 即使不需要返回结果集,也会将该查询影响到的行数返回给客户端
- 结果集返回客户端是一个增量、逐步返回的过程
- 服务器端无需存储太多结果,也不会因返回太多结果而消耗太多内存
- 客户端能第一时间获得返回的结果
MySQL 查询优化器的局限性
- 关联子查询
- MySQL 的子查询实现得非常糟糕,特别是 WHERE 条件中包含 IN ( ) 的子查询
- 优化方向:IN ( ) -> EXISTS ( )
- 如何用好关联子查询
- DISTINCT 和 GROUP BY 在查询执行过程中,通常需要产生临时中间表,可通过 EXISTS + 子查询 优化
- Extra 列 出现 “Not exists”:提前终止算法(一旦匹配,立刻停止扫描
- MySQL 的子查询实现得非常糟糕,特别是 WHERE 条件中包含 IN ( ) 的子查询
- UNION 的限制
- MySQL 无法将限制条件从外层 “下推” 到内层
- 如果希望 UNION 的各个子句都能根据 LIMIT 只取部分结果集(或先排好序再合并),只能在各个子句上分别使用
- 多个联合子查询(分别已排序)产生的临时表中的数据并不一定是有序
- MySQL 无法将限制条件从外层 “下推” 到内层
- 索引合并优化
- MySQL 能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行
- 等值传递
- 等值传递有时会带来一些意想不到的额外消耗(巨大 IN ( ) 列表复制应用到关联的各个表中时
- 并行执行
- MySQL 无法利用多核特性来并行执行查询
- 哈希关联
- 当前 MySQL 并不支持哈希关联(只能通过嵌套循环关联
- 松散索引扫描
- MySQL 并不支持松散索引扫描,无法按照不连续的方式扫描一个索引
- 可以通过前面的列加上可能的常数值绕开限制( IN (常数A,常数B)
- 在某些特殊场景下是可以使用松散索引扫描
- 分组查询中找到分组的最大值和最小值
- Extra 字段:Using index for group-by
- MySQL 5.6之后的版本,限制可以用个 “索引条件下推” 的方式解决
- MySQL 并不支持松散索引扫描,无法按照不连续的方式扫描一个索引
- 最大值和最小值优化
- 对于 MIN ( ) 和 MAX ( ) 的优化做得不够好,如果 WHERE 条件未使用上索引,会进行全表扫描
- 在特定的场景,可以根据索引最左值和最右值分别找出 MIN ( ) 和 MAX ( )
- 在同一表上查询和更新
- MySQL 不允许对同一张表同时进行查询和更新
- 通过使用生成表(临时表)绕开限制
- MySQL 不允许对同一张表同时进行查询和更新
查询优化器的提示(hint)
-
通过在查询中加入相应的提示,就可以控制该查询的执行计划
-
常用提示(是否有效跟 MySQL 版本强关联
- HIGH_PRIORITY 和 LOW_PRIORITY
- 内部维护一个访问某个数据表的队列顺序
- 只对使用表锁的存储引擎有效
- 千万不要在有细粒度锁机制和并发控制的引擎中使用
- DELAYED
- INSERT 和 REPLACE 有效
- 执行后立即返回客户端,插入的数据放入缓冲区,表空闲时再写入
- 适合日志系统等其他不关心单条语句 I/O 的应用
- 不是所有的存储引擎都支持 且 导致 LAST_INSERT_ID 无法正常工作
- STRAIGHT_JOIN(与 MySQL 版本有关
- 用于控制关联顺序
- 减少优化器花费在 “statistics” 状态的时间,大大减少优化器的搜索空间
- SQL_SMALL_RESULT 和 SQL_BIG_RESULT
- 只对 SELECT 起效,如何使用使用临时表及排序
- 前者告诉优化器结果集很小,将结果集存放在内存中的索引临时表
- 后着告诉优化器结果集很大,使用磁盘临时表做排序操作
- 只对 SELECT 起效,如何使用使用临时表及排序
- SQL_BUFFER_RESULT
- 将结果存放到一个临时表,尽快释放表锁(服务器端缓存
- SQL_CACHE 和 SQL_NO_CACHE
- 是否缓存在查询缓存中
- SQL_CALC_FOUND_ROWS
- 查询中返回的结果集总数(LIMIT 后也会返回
- FOR UPDATE 和 LOCK IN SHARE MODE
- 控制 SELECT 语句的锁机制
- 只对实现了行级锁的存储引擎起效(内置引擎的只有 InnoDB
- 使用会导致某些优化无法使用(索引覆盖扫描等
- USE INDEX、IGNORE INDEX 和 FORCE INDEX
- 告诉优化器使用或不使用某些索引
- 控制优化器的参数
- optimizer_serach_depth(穷举执行计划时的限度
- optimizer_prune_level(根据需要扫描的行数来决定是否跳过某些执行计划
- optimizer_switch(开启/关闭优化器特性的标志位,例如索引合并
优化特定类型的查询
- 本节介绍的多数优化都只与特定的版本有关,未来的 MySQL 版本未必有效
- 优化 COUNT ( ) 查询
- 作用
- 统计某个列值的数量(不包含 NULL
- 统计结果集行数
- 关于 MyISAM 的神话
- 存储引擎中的统计信息有当前所有的行数值
- 只对不带任何 WHERE 条件的 COUNT (*) 有用
- 简单优化
- 排除法(所有的值 - 不需要的值
- 查询阶段会将其子查询直接当成一个常数处理(Select tables optimized away
- 使用近似值
- 当不要求完全精确的 COUNT 值时,使用近似值代替(EXPLAIN - ROWS
- 更复杂的优化
- 索引覆盖扫描
- 增加汇总表
- 快速,精确和实现简单,三者永远只能满足其二,必须舍掉其中一个
- 作用
- 优化关联查询
- 确保 ON 或者 USING 子句中的列上的有索引(关联顺序中第二表上
- GROUP BY 和 ORDER BY 中的表达式只涉及到一个表中的列
- 升级 MySQL 是需要注意:关联语法、运算符优先级等是否发生变化
- 优化子查询
- 尽可能使用关联查询代替
- 优化 GROUP BY 和 DISTINCT
- 尽量都使用上索引(当未使用上时,会使用临时表或者文件排序进行分组
- 使用提示(hint
- 尽量采用查找表的标识列分组(效率比其他列更高
- 使用分组后,结果集会自动按分组的字段进行排序
- 若不关心结果集顺序,而默认排序又导致了文件排序,可以使用 ORDER BY NULL
- GROUP BY xxx DESC/ASC
- 优化 GROUP BY WITH ROLLUP
- 作用:超级聚合
- 尽量将 WITH ROLLUP 功能转移至应用程序中处理
- 优化 LIMIT 分页
- 索引覆盖扫描(延迟关联
- 避免使用 OFFSET,取前一次查询数据的位置,以此位置开始扫描
- 预先计算汇总表
- 优化 SQL_CALC_FOUND_ROWS
- MySQL 都会扫描所有满足条件的行,然后抛弃不需要的行
- 解决
- 将具体的页数换成 “下一页”(若存在第 N + 1 条,则显示
- 先获取并缓存较多的数据
- 使用近似值( EXPLAIN 结果中的 ROWS 的值
- 确定需要使用精确结果时,尽量使用上索引覆盖扫描
- 优化 UNION 查询
- 除非确实需要服务器消除重复的行,否则一定使用 UNION ALL
- 若没有 ALL 关键字,MySQL 会给临时表加上 DISTINCT,做唯一性检查
- 除非确实需要服务器消除重复的行,否则一定使用 UNION ALL
- 静态查询分析
- 使用工具解析查询日志、分析查询模式(例如 pt-query-advisor
- 使用用户自定义变量
- 查询中混合使用过程化和关系化逻辑的时,自定义变量可能会非常有用
- HIGH_PRIORITY 和 LOW_PRIORITY
总结
- 要想写好一个好的查询,必须要理解 schema 设计、索引设计等,反之亦然
- 理解查询是如何被执行的以及时间都消耗在哪些地方
- 优化通常需要三管齐下:不做、少做、快速地做