首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

EXPLAIN:解说一条简单 SQL 语句的执行计划

  • 24-12-12 14:23
  • 2901
  • 27538
www.oschina.net
鸿蒙原生应用开发者激励计划发布!最高获百万现金!点击立即参与

在 MySQL 里,当通过各种手段抓取到影响系统性能的慢 SQL 语句后,此刻想知道这条 SQL 语句为何会慢的答案。

> 作者:杨涛涛,爱可生技术专家。 > > 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

在 MySQL 里,当通过各种手段抓取到影响系统性能的慢 SQL 语句后,此刻想知道这条 SQL 语句为何会慢的答案。大致有以下方式:

  1. 凭借个人已知的 SQL 优化经验进行 SQL 改写。
  2. 对照公司严格要求的开发规范进行 SQL 改写。
  3. 查看对应 SQL 语句的执行计划,从结果中分析 SQL 是否使用合适的索引、是否走了合适的表关联顺序、是否走了合适的表关联算法等。

通过以上三种方式审阅后,是否需要反馈给业务方来修改 SQL 语句的逻辑或者说是变更 SQL 语句涉及到的表结构。

虽然以上几种方式一般都需要互相结合来验证 SQL 的优化效果,仅凭一种方式来直接定位出 SQL 是否足够优化不太可能,但是每种方式都需要依赖第三种方式,通过直接和数据库交互来查看数据库的执行计划,能够比较直观的展示 SQL 执行的过程。在 MySQL 里查看 SQL 语句执行计划的方式是使用 EXPLAIN 语句。

EXPLAIN(对应同义词:DESC、DESCRIBE) 语句可以用来查看一条 SQL 是否匹配到合适的索引、是否使用到中间临时表、是否需要额外的排序等等不利因素;并且 EXPLAIN 语句还有几种定制化的输出格式(表格、JSON、树状),每种格式输出内容的详细程度不一样;当一条 SQL 语句被带入 EXPLAIN 执行后,从执行结果就能大致判断这条 SQL 目前能否做进一步优化。

本篇使用 EXPLAIN 来验证一条简单的 SQL 语句,来帮助我们进行后续调优。以下是此语句的 EXPLAIN 执行结果:

debian-ytt1:ytt>desc  select * from t1 where r1 = 4\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_r1,idx_u1
          key: idx_u1
      key_len: 5
          ref: const
         rows: 29324
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.01 sec)

debian-ytt1:ytt>show warnings\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select `ytt`.`t1`.`id` AS `id`,`ytt`.`t1`.`r1` AS `r1`,`ytt`.`t1`.`r2` AS `r2`,`ytt`.`t1`.`r3` AS `r3`,`ytt`.`t1`.`log_date` AS `log_date` from `ytt`.`t1` where (`ytt`.`t1`.`r1` = 4)
1 row in set (0.00 sec)

上面是对一条极其简单的 SQL 语句执行 EXPLAIN 的结果,有几个最直观的点需要关注:

  1. possible_keys: 表示这条 SQL 可能会用到的索引, MySQL 会把所有这条 SQL 有可能会用到的索引罗列到这里,最终基于当前关系表的统计信息、当前索引表的统计信息选择一个最优的索引来执行。
  2. key: 表示这条 SQL 使用到的最终索引,如果不是 NULL,就表示执行计划使用了指定的索引来执行这条 SQL。比如这里用了索引 idx_u1:这条索引是一个联合索引。
  3. rows: 表是执行这条 SQL 需要大致扫描多少行才能出来结果,这里扫描了 2.9W 行。
  4. Extra: 额外的信息。 这条 SQL 的 EXTRA 栏里是 NULL,表示没有额外信息展示出来。
  5. warnings: warnings 结果表示最终这条语句在 MySQL 的 SQL 解析层最终被转换后的 SQL 形式。

读到这里,可能会启发几个问题:

  1. 虽然这条 SQL 可以正确用到索引,但是查询性能依然没有达到我的要求,有没有什么方法可以做进一步优化?
  2. 虽然从结果可以看到这条 SQL 扫描的大致行数,但是此值依然很大。比如这里是 2.9W 行,有办法减少吗?
  3. 还有就是如果 EXTRA 栏里的内容我知道了,我该怎么去二次优化?

针对这条 SQL 语句,如果单纯从语句写法上来看,我觉得已经无法二次优化,因为它已经匹配到了索引。现在的问题是 MySQL 用到的这个索引是否足够好?从执行计划结果来猜测:这个索引本身的选择性太差,导致仅执行一行等值检索,就要耗费大量的资源去扫描接近 3W 行的数据记录。

接下来,我们给这条语句做些变化,让他执行计划输出的 EXTRA 栏里有点内容,而且还是看起来需要优化的内容。比如 "using filesort":给这条 SQL 加条排序子句。

debian-ytt1:ytt>desc  select * from t1 where r1 = 4 order by id desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_r1,idx_u1
          key: idx_u1
      key_len: 5
          ref: const
         rows: 29324
     filtered: 100.00
        Extra: Using filesort
1 row in set, 1 warning (0.00 sec)

接下来回答之前提到的几个问题:

第一个问题: 选择了索引,这条 SQL 执行时间依然很长?

其实这个在我的专栏里索引设计那一块有讲过,并不是说 SQL 语句使用了索引,查询性能就一定会很好, 使用索引和查询性能提升两者并没有什么必然联系。比如这条 SQL 语句, 执行计划结果显示用了索引 idx_u1,但是基于这个索引还得扫描 2.9W 行记录才行。 那我们来进一步看看这条索引在表 t1 里的基数值:

debian-ytt1:ytt>select table_name,index_name,column_name,cardinality 
    -> from information_schema.statistics 
    -> where table_schema='ytt' and table_name='t1';
+------------+--------------+-------------+-------------+
| TABLE_NAME | INDEX_NAME   | COLUMN_NAME | CARDINALITY |
+------------+--------------+-------------+-------------+
| t1         | idx_log_date | log_date    |         335 |
| t1         | idx_r1       | r1          |           5 |
| t1         | idx_r2       | r2          |         100 |
| t1         | idx_r3       | r3          |         100 |
| t1         | idx_u1       | r1          |           5 |
| t1         | idx_u1       | r2          |         599 |
| t1         | idx_u1       | r3          |       50118 |
| t1         | PRIMARY      | id          |      102046 |
+------------+--------------+-------------+-------------+
8 rows in set (0.00 sec)

可以看到,索引 idx_u1 是一个组合索引,字段 r1 在此索引里的基数只有 5,而主键的基数是 10W。 索引 idx_u1 或者索引 idx_r1 的基数都非常低,也就是说对每一条对应的 r1 字段匹配,都会扫描很多行记录出来,进而导致基于字段 r 的索引可选择性非常差。 所以此时针对这条 SQL 的优化,单单从语句层面已经没有办法继续下去,这种情况就需要 DBA 和业务一起联动来进一步讨论能否选择其他字段来做过滤条件、减少这条 SQL 的运行频次等手段来进行非常规优化。

第二个问题:如果索引选择合适或者说过滤条件很优化,那扫描行数也会相应减少。这条 SQL 过滤条件非常简单,只有一个等值检索,对于关系表来讲,如果业务逻辑不改,还是用原来的索引来过滤,也算是优化到了尽头。

最后来到第三个问题:针对 EXTRA 栏里显示的必须要优化的信息进行二次优化。 比如我们修改过的 SQL, EXTRA 栏显示 "using filesort" ,避免排序只需要把被排序字段加现有索引上即可, 这样的好处是排序直接走索引表本身的顺序而不用二次排序。

debian-ytt1:ytt>alter table t1 add key idx_r1_id_desc (r1,id desc);
Query OK, 0 rows affected (1.06 sec)
Records: 0  Duplicates: 0  Warnings: 0

我们再重新看下执行计划:

debian-ytt1:ytt>desc  select  * from t1  where r1 = 4 order by id desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_r1,idx_u1,idx_r1_id_desc
          key: idx_u1
      key_len: 5
          ref: const
         rows: 29324
     filtered: 100.00
        Extra: Using filesort
1 row in set, 1 warning (0.00 sec)

虽然我们加了新索引,不过 MySQL 依然选择索引 idx_u1,"using filesort" 并没有拿掉。

这里只能证明一点: 基于表 t1 当前的统计信息,执行计划选择了走索引 idx_u1,而不是索引 idx_r1_id_desc。也就是说针对这条 SQL 来讲,优化器综合判断排序比不排序反而要高效。 至于为什么排序更高效,我们继续来看。

那强制用这个新索引的执行计划又是什么样呢?

debian-ytt1:ytt>desc  select  * from t1  force index(idx_r1_id_desc) where r1 = 4 order by id desc\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_r1_id_desc
          key: idx_r1_id_desc
      key_len: 5
          ref: const
         rows: 32176
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

从执行计划结果来看,虽然避免了排序,但是扫描行数比用索引 idx_u1 要多。这里就在于要消耗更多 CPU 来排序、还是消耗更多 IO 来扫更多行两者之间做个权衡。从 EXPLAIN 结果暂时还看不到这些信息,可以更进一步用 EXPLAIN ANALYZE 语句来看这条 SQL 选择不同索引的成本信息来做个对比:

debian-ytt1:ytt>desc analyze select  * from t1  force index(idx_r1_id_desc) where r1 = 4 order by id desc\G
*************************** 1. row ***************************
EXPLAIN: -> Index lookup on t1 using idx_r1_id_desc (r1=4)  (cost=3434.35 rows=32176) (actual time=0.225..32.290 rows=17093 loops=1)

1 row in set (0.04 sec)

debian-ytt1:ytt>desc analyze select  * from t1  where r1 = 4 order by id desc\G
*************************** 1. row ***************************
EXPLAIN: -> Sort: t1.id DESC  (cost=3149.15 rows=29324) (actual time=59.961..62.682 rows=17093 loops=1)
    -> Index lookup on t1 using idx_u1 (r1=4)  (actual time=0.046..42.779 rows=17093 loops=1)

1 row in set (0.07 sec)

很显然,避免排序的成本为 3434.25,而排序的成本是 3149.15,MySQL 基于成本生成了最优匹配的执行计划,那我们新加的这个索引就没有意义,可以删掉了。

本篇就进行到这里,欢迎继续关注我的专栏,我将继续更新更多的 SQL 优化知识。

更多技术文章,请访问:https://opensource.actionsky.com/

关于 SQLE

SQLE 是一款全方位的 SQL 质量管理平台,覆盖开发至生产环境的 SQL 审核和管理。支持主流的开源、商业、国产数据库,为开发和运维提供流程自动化能力,提升上线效率,提高数据质量。

✨ Github:https://github.com/actiontech/sqle

📚 文档:https://actiontech.github.io/sqle-docs/

💻 官网:https://opensource.actionsky.com/sqle/

👥 微信群:请添加小助手加入 ActionOpenSource

🔗 商业支持:https://www.actionsky.com/sqle

注:本文转载自www.oschina.net的爱可生开源社区的文章"https://my.oschina.net/actiontechoss/blog/16684752"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

112
数据库
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top