type
status
date
slug
summary
tags
category
password

1、什么是SQL调优

SQL 调优是指通过各种手段提升数据库查询性能的过程,旨在减少响应时间和资源消耗。SQL 调优的核心目标:
  1. 降低响应时间:让单条 SQL 执行得更快。
  1. 提高吞吐量:让数据库在单位时间内处理更多的请求。
  1. 减少资源消耗:降低 CPU、内存和 I/O 的压力。
SQL 调优是一个系统性的工程,涉及索引优化、查询语句重写、数据库参数调整等多个层面。一个高效的调优流程通常遵循以下步骤:
  1. 发现问题:通过监控、慢查询日志等找到需要优化的 SQL。
  1. 分析问题:使用工具(如 EXPLAIN)分析 SQL 的执行计划,定位瓶颈。
  1. 解决问题:根据分析结果,采取相应的优化手段(如加索引、改 SQL)。
  1. 验证效果:对比优化前后的性能,确认优化是否有效。

2、发现慢SQL

如果不知道哪些 SQL 慢,优化就无从谈起。
  1. 开启慢查询日志 (Slow Query Log)
    1. 这是最核心的方法。MySQL 可以将执行时间超过指定阈值的 SQL 语句记录到日志文件中。
  1. 使用性能模式 (Performance Schema) 和 sys 库
    1. MySQL 5.7+ 的 performance_schema 和 sys 库提供了更强大的内部性能数据视图。
  1. 应用程序监控
    1. 使用 APM (Application Performance Monitoring) 工具,如 SkyWalking, Pinpoint 等,从应用层面发现数据库调用瓶颈。

3、分析执行计划(EXPLAIN)

使用 EXPALIN 可以模拟优化器执行 SQL 查询语句,从而知道 MySQL 怎么处理你的SQL语句的,进而分析查询语句的性能瓶颈。注意 EXPALIN 只能解释 SELECT 操作,其他操作要重写为 SELECT 后查看执行计划。
字段解析如下:
  • id:每个 SELECT 关键字都对应一个唯一的id。如果是简单的单表查询,只有一个 SELECT 关键字,则只会有一条 id=1 的记录;如果是包含子查询的SQL语句,就可能有多个SELECT关键字,那么每个SELECT关键字都会对应一个唯一的id值。
  • select_type:查询的类型。
    • SIMPLE:SIMPLE 就是非常简单的查询,意味着不包含 UNION 或子查询,连接查询也算是 SIMPLE 类型。
    • PRIMARY:如果查询有任何复杂的子部分,如 UNION、UNION ALL、子查询,则最外层部分标记为 PRIMARY。
    • UNION:在 UNION 或 UNION ALL 子句中的第二个之后的 SELECT 都会被标记为 UNION。
    • UNION RESULT:UNION 查询需要使用内存临时表来去重,针对该临时表的查询的 select_type 就是 UNION RESULT,上面介绍 UNION 时可以看到这个类型。
    • SUBQUERY:包含在 SELECT 列表中的子查询中的 SELECT 会标记为 SUBQUERY,不包含 FROM 子查询。
    • DERIVED:DERIVED 用来表示包含在 FROM 子句的子查询中的 SELECT,MySQL 会将子查询的结果物化,放到一个临时表中,也称为 派生表,因为该临时表是从子查询中派生来的。
    • DEPENDENT UNION、DEPENDENT SUBQUERY、DEPENDENT DERIVED:DEPENDENT 意味着 SELECT 依赖于外层查询中发现的数据。
    • MATERIALIZED:MATERIALIZED 意味着SQL中包含子查询(非FROM子查询),且子查询是物化的方式和外层查询进行连接查询的。
    • UNCACHEABLE SUBQUERY、UNCACHEABLE UNION:UNCACHEABLE 意味着 SELECT 中的某些特性阻止结果被缓存。
  • table:查询的表(别名),可以从这一列中从上往下观察SQL执行的关联顺序。
  • partitions:分区信息,一般情况下查询语句的执行计划的 partitions 列的值都是 NULL。
  • type:访问类型或者访问方法,就是MySQL决定如何查找表中的行。这个属性是分析执行计划时需要重点关注的信息,它告诉我们SQL是如何查询这张表的数据,有没有用到索引等等。优先级为:NULL > system > const > eq_ref > ref > ref_or_null > index_merge > range > index > ALL
    • NULL:这种访问方式意味着MySQL能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。
    • system:当引擎是 MyISAM 或者 Memory 的时候并且只有一条记录,就是 system,表示可以系统级别的精准访问,这个不常见可以忽略。
    • const:查询命中的是主键或者唯一二级索引等值匹配的时候。比如 where id = 1
    • eq_ref:通过主键或者唯一二级索引列等值匹配的方式进行查询,最多只会返回一条符合条件的记录。
    • ref:通过普通二级索引列与常量进行等值匹配时来查询某个表。相比 eq_ref,ref 可能返回多条符合条件的记录。
    • ref_or_null:类似于 ref,只是 ref_or_null 表示查询条件是 where second_key is null
    • index_merge:一般情况下对于某个表的查询只会用到一个索引,但也可能同时使用多个索引,使用索引合并的方式来执行查询。不常见,可跳过。
    • unique_subquery:在一些包含 IN 子查询的SQL语句中,in 语句的时候命中了唯一索引。
    • index_subquery:类似于 unique_subquery,in 语句的时候命中了普通索引。
    • range:表示基于索引的范围扫描,比 index 全索引扫描好一些,因为它不用遍历全部索引。
    • index:命中了索引,但是需要扫描全部索引。index 类似于 ALL 全表扫描,不过 index 是顺序扫描索引的全部记录。它的主要优点是避免了排序,但如果要回表的话,开销可能比 ALL 更大。
    • ALL:没有使用索引,走的是全表扫描。应尽量避免这种方法的出现。
  • possible_keys:可能用到的索引
  • key:实际上使用的索引,也可能不在 possible_keys 中。想强制 MySQL 使用或忽视 possible_keys 列中的索引,在查询中使用 FORCE INDEXUSE INDEX 或者 IGNORE INDEX。这个也是重点关注部分。
  • key_len:实际使用到的索引长度,不损失精确性的情况下,长度越短越好
  • ref:使用索引列等值查询时,与索引列进行等值匹配的对象信息
  • rows:预估需要读取的记录条数
  • filtered:某个表经过搜索条件过滤后剩余记录条数的百分比。其实 filtered 在单表查询中意义不是很大,一般在多表连接中,则可用于预测被驱动表会被查询多少次。
  • Extra:扩展信息,但是非常重要,可以更好的辅助你定位 MySQL 到底如何执行的这个语句。从上往下性能越来越差。
    • Impossible WHERE:查询语句的 WHERE 子句永远为 FALSE 时将会提示该额外信息。
    • Using index性能最高,这意味着使用了覆盖索引,通过索引可以直接定位到满足查询条件的数据行,不需要进行回表查询。当我们查询条件和返回内容都存在索引里面,就可以走覆盖索引,不需要回表Using index 同时适用于主键索引和二级索引。比如 select second_key from test where second_key = 10
    • Using index condition经典的索引下推(ICP),将原来在 server 层进行的 table filter 中可以进行 index filter 的部分,在引擎层面使用 index filter 进行处理,不再需要回表进行 table filter。Using index condition 仅适用于二级索引,一般发生在查询字段无法被二级索引覆盖的场景。比如 select * from test where second_key > 10 and second_key like '%0’
    • Using where表示使用 where 的条件过滤数据,有可能命中了索引,也有可能没命中,不管是否命中都需要回表查询所需的数据。一般发生在查询的列未被索引覆盖,且 where 筛选条件是索引列前导列的一个范围,或者是索引列的非前导列,或者是非索引列等场景。因为需要回表,所以性能比前两者差。比如 select * from test where text = 't'
    • Using temporary:出现 Using temporary 意味着 MySQL 内部使用了临时表来完成一些功能,比如去重、排序之类的,前面演示 UNION 时,因为 UNION 要对结果集去重,所以就使用了临时表。
    • Using filesort:查询没有命中任何索引,需要将数据查到内存中,或者在数据较大时放到磁盘中,再进行排序。比如 select * from test where text = 't' order by text desc limit 10
    • Using join buffer (Block Nested Loop):在连接查询执行过程中,当被驱动表没有索引或者不能有效的利用索引加快访问速度时,MySQL一般会为其分配一块名叫 join buffer 的内存块,然后将数据读到这块内存块中,再基于块中的数据做嵌套循环查询,以此来加快查询速度。
Extra 还有一些组合情况,比如:
  • Using where;Using index:表示查询的列被索引覆盖,且 where 筛选条件是索引列前导列的一个范围,或者是索引列的非前导列。一般发生在联合索引,且查询条件和返回内容都存在索引里面,这种效率也很高。例如:select id from test where id > 5;
另外注意 type=indexExtra=Using index 的区别:
  • type=index 表示命中了索引。
  • Extra=Using index 表示查询使用了覆盖索引,没有回表查询。

4、主要的优化手段

4.1 索引优化

索引优化是最有效的手段,我们先了解索引失效的场景。

4.1.1 索引失效的情况

  1. 联合索引不满足最左匹配原则:创建了一个 (a, b, c) 联合索引,如果查询条件是 where b=2 或者 where b=2 and c=3 ,因为不满足最左匹配原因导致索引失效。
  1. LIKE 以通配符 % 开头:当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。
  1. 对索引使用函数: select * from t_user where length(name) = 6; 对 name 字段使用了 LENGTH 函数,导致索引失效。
  1. 对索引进行表达式计算:select * from t_user where id + 1 = 10; 对 id 字段进行计算后再比较,导致索引失效。如果把查询语句的条件改成 where id = 10 - 1 就可以走索引查询了。
  1. 对索引隐式类型转换:参数类型与字段类型不匹配,导致类型发生了隐式转换,索引失效。例如如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,例如 select * from t_user where phone = 1300000001; 会导致索引失效。但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描,例如 select * from t_user where id = '1'; 。原因是 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较
  1. 使用 OR 条件但未对所有条件建立索引:如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
  1. 使用不等于操作(!=,<>,NOT LIKE):当查询条件为字符串时,使用 <>!= 作为条件查询有可能会导致索引失效。Mysql 优化器会自动判断,当查询结果集占比比较小时会走索引,占比比较大时不会走索引。
  1. IS NOT NULL:查询条件使用 is null 时正常走索引,使用 is not null 时,不走索引。
  1. NOT IN 和 NOT EXISTS:查询条件使用 not in 时,如果是主键则走索引,如果是普通索引,则索引失效。查询条件使用 not exists 时,索引失效。
  1. ORDER BY 使用不当:当查询条件涉及到 order bylimit 等条件时,是否走索引情况比较复杂,而且与 Mysql 版本有关,通常普通索引,如果未使用 limit,则不会走索引。 order by 多个索引字段时,可能不会走索引。其他情况,建议在使用时进行 expain 验证。

4.1.2 索引的最佳实践

  1. 选择合适的列建立索引:只为经常出现在 WHERE 子句或 GROUP BY 子句中的列创建索引。索引字段要求
    1. 离散度高:离散度低会导致查到大量的重复数据,发生大量的回表操作
    2. 更新频率低:频繁更新会导致数据页面分裂,影响性能。
  1. 控制索引数量:索引会占用存储空间并影响写入性能,小表通常不需要索引,数据频繁更新且查询不频繁的表不宜过多索引,通常建议不超过 5-6 个索引。
  1. 多字段使用联合索引,联合索引遵循最左原则:如果是查询多字段,建立联合索引,并把将选择性高的列放在前面。
  1. 使用覆盖索引和索引下推:如果 SELECT 查询的所有列都包含在索引中,这种索引称为覆盖索引。覆盖索引可以发挥索引下推的特性,把 WHERE 条件下推到存储引擎层去过滤数据,减少回表操作,提高查询性能。
  1. 对于长字符串列,考虑使用前缀索引:可以为该列的前几个字符创建索引,也就是在二级索引的记录中只会保留字符串的前几个字符。比如我们可以为 phone 列创建索引,索引只保留手机号的前3位:ALTER TABLE user_innodb ADD INDEX IDX_PHONE_3 (phone(3));

4.2 SQL语句优化

 SQL 语句优化的目的是减少无效的查询,并发挥索引的高效性能。常见的 SQL 语句优化方法:
  • 避免 SELECT *:只取需要的字段。特别是 TEXT/BLOB 字段,SELECT * 会导致大量不必要的数据传输和缓存浪费。
  • 优化 JOIN 操作:小表驱动大表,并确保 ON 或 USING 子句中的列上有索引。让结果集小的表作为驱动表,这样去遍历被驱动的大表表的次数也会少,可以减少磁盘IO,提高性能。驱动表的定义原则:
    • LEFT JOIN 左连接,左边为驱动表,右边为被驱动表
    • RIGHT JOIN 右连接,右边为驱动表,左边为被驱动表
    • INNER JOIN 内连接,Mysql 会选择数据量比较小的表作为驱动表,大表作为被驱动表。
  • 优化分页查询 LIMIT:对于 LIMIT 1000000, 20 这种深度分页,效率极低。优化方案:使用延迟关联。
    • 避免在 WHERE 子句中对字段进行函数操作或表达式计算:这会导致索引失效。
      • 使用 UNION ALL 替代 OR:有时 OR 会导致索引失效,而 UNION ALL 效率更高(注意去重问题)。
      • 使用 EXISTS 代替 IN:对于子查询,EXISTS 通常比 IN 性能更好。

        4.3 数据库设计优化

        • 选择合适的数据类型:越小越好,越简单越好。例如,用 INT 而不是 VARCHAR 存储数字,用 DATETIME 而不是 VARCHAR 存储时间。
        • 范式与反范式的平衡:适当的反范式设计(如冗余字段)可以减少 JOIN 操作,以空间换时间。
        • 垂直拆分:将包含大量 TEXT/BLOB 等大字段的表拆分开,避免影响核心表的查询性能。
        • 水平拆分(分库分表):当单表数据量过大时(如千万级以上),考虑进行分库分表。

        4.4 服务器参数调优

        • InnoDB Buffer Pool Size (innodb_buffer_pool_size)这是最重要的参数。它定义了 InnoDB 缓存数据和索引的内存大小。通常设置为可用物理内存的 70%-80%。
        • InnoDB Log File Size (innodb_log_file_size): redo log 文件大小。更大的 log file 可以减少磁盘 I/O,但会增加恢复时间。
        • Query Cache (query_cache_type and query_cache_size): 注意:MySQL 8.0 中已移除该功能。在 5.7 版本中,如果查询重复度不高,建议关闭(query_cache_type = 0),因为维护缓存的开销可能大于收益。
        警告:不要盲目修改参数,最好在测试环境充分测试。调优应基于监控指标(如缓存命中率)。

        4.5 定期分析和优化表

        4.5.1 分析表ANALYZE TABLE 

        ANALYZE TABLE 用于更新表的统计信息,例如表的键分布统计信息,这些信息帮助优化器生成最佳的执行计划,例如决定表的连接顺序和索引使用。可以使用 show index from t_log_iteminfo 命令查看表的键分布统计信息。
        注意字段 Cardinality,表示某个索引对应的列包含多少个不同的值。如果 Cardinality 和数据的实际数量相差太远,那么索引就基本失效了。例如上面的 t_log_iteminfo 全表才 226 万数据,但主键的索引基数居然有 509 万,说明统计信息不准确了。这时候就需要使用ANALYZE TABLE 来更新表的统计信息,帮助生成更准确的执行计划。
        基本语法:
        • NO_WRITE_TO_BINLOG:设置 ANALYZE 操作不记录 binlog。默认会记录 binlog。
        • LOCALNO_WRITE_TO_BINLOG 的别名,效果等同 NO_WRITE_TO_BINLOG
        示例:

        4.5.2 优化表OPTIMIZE TABLE

        OPTIMIZE TABLE用于重组表的物理存储(重建表并更新表的统计信息),可以减少存储空间并提高 I/O 效率。OPTIMIZE TABLE对于有大量更新/删除操作的表特别有用,因为这些操作会产生大量的空间碎片。
        OPTIMIZE TABLE 会进行锁表,在不同的执行引擎下有不同的行为:
        • 对于 InnoDB 表:执行在线 ALTER TABLE 操作重建表,减少空间碎片。
        • 对于 MyISAM 表:修复碎片化的数据文件并重建索引。
        可以执行 show table status like 't_log_iteminfo' 命令查看表的空间碎片情况。
        重点关注几个字段:
        • Data_free:已分配但未使用的空间(字节),可以简单理解为内存碎片。
        • Data_length:数据总长度(字节)。
        • Index_length:索引总长度(字节)。
        一般来说 表大小 = Data_length + Index_length,这里 t_log_iteminfo 的表大小是 319MB,但 Data_free 却有 473MB,说明碎片很多了。需要执行 OPTIMIZE TABLE 命令进行碎片整理。
        基本语法:
        • NO_WRITE_TO_BINLOG:设置 OPTIMIZE 操作不记录 binlog。默认会记录 binlog。
        • LOCALNO_WRITE_TO_BINLOG 的别名,效果等同 NO_WRITE_TO_BINLOG
        示例:
        另外 InnoDB 表不完全支持传统的 OPTIMIZE TABLE 命令,执行 OPTIMIZE TABLE 命令可能会报错 Table does not support optimize, doing recreate + analyze instead。因为 InnoDB 有自己的空间管理机制,使用聚簇索引,数据存储方式与 MyISAM 不同。可以使用以下命令替代
        OPTIMIZE TABLEANALYZE TABLE 的区别是什么?
        特性
        ANALYZE TABLE
        OPTIMIZE TABLE
        主要目的
        更新统计信息
        重组物理存储
        影响性能
        轻微影响
        可能影响较大(表重建)
        使用频率
        可较频繁使用
        应在必要时使用
        适用场景
        查询计划不佳时
        大量更新/删除后
        最佳实践:
        • 在长期运行的系统中间歇性使用 ANALYZE TABLE ,在大量数据变更后使用 OPTIMIZE TABLE ,并确保在低峰期执行。
        • 因为 OPTIMIZE TABLE 也会更新表的统计信息,所以如果执行了 OPTIMIZE TABLE 就不需要再执行 ANALYZE TABLE

        5、SQL调优最佳实践

        1. 开启慢查询日志,找到最耗时的 SQL。
        1. 执行前先用 EXPALIN 查看执行计划
        1. 检查是否使用了索引 (type 字段),如果没有,考虑添加。
        1. 检查索引是否高效
          1. 索引选择性如何?
          2. 是否满足最左前缀?
          3. 是否发挥了覆盖索引和索引下推的特性?
        1. 检查 SQL 写法
          1. 是否有 SELECT *?应该仅列出需要查询的字段。
          2. 避免使用子查询,优先使用 JOIN 查询,遵循小表驱动大表原则。
          3. 避免在索引列上使用函数或表达式计算。
        1. 检查数据库设计
          1. 字段类型是否合适?
          2. 是否需要反范式优化?
        1. (进阶)调整核心服务器参数,如 innodb_buffer_pool_size
        1. 定期分析和优化索引
          1. 分析表:ANALYZE TABLE table_name ,用于更新表的统计信息,帮助优化器生成更好的执行计划。
          2. 优化表:OPTIMIZE TABLE table_name,用于重组表的物理存储(重建表并更新表的统计信息),可以减少存储空间并提高I/O效率,会锁表。
        Mysql索引篇:InnoDB索引(B+树结构、聚簇索引)Mysql存储引擎篇:InnoDB缓冲池Buffer Pool
        Loading...