第 53 期:EXPLAIN 中最直观的 rows

作者:杨涛涛,爱可生技术专家。爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。MySQL 和大多数关系型数据库一样,SQL 语句执行计划的输出栏都有一行 rows,代表优化器执行这条 SQL 所需算子扫描的记录数

第 53 期:EXPLAIN 中最直观的 rows

作者:杨涛涛,爱可生技术专家。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

MySQL 和大多数关系型数据库一样,SQL 语句执行计划的输出栏都有一行 rows,代表优化器执行这条 SQL 所需算子扫描的记录数,是优化器根据表和索引的统计信息数据评估出来的结果。

如何根据 rows 值的大小判断 SQL 性能?

对于大多数场景来讲,可以直接凭借 rows 值的大小来判断 SQL 语句性能的高低,但也不能一概而论。

本篇就通过几个简单的示例,来列举三种 rows 值判断的情况。

  • rows 值小,性能高
  • rows 值小,性能不一定
  • 不适合看 rows 值

rows 值小,性能高

第一种情况就是同一条 SQL,只是用到索引不同,rows 值越小,SQL 性能越高。

示例 SQL:

select * from t1 where r1=2 and r2=2

如果不考虑真实业务逻辑,单从写法上来讲,这条 SQL 已经无法优化,因为已经足够简单。优化策略可简单的定义为过滤字段是否匹配索引、匹配的索引是否足够好的问题。比如可能有如下四种索引被用到:

idx_r1(r1) / idx_r2(r2) / idx_u1(r1,r2) / idx_u2(r2,r1)

对于以上几个索引,MySQL 可以根据统计信息、数据物理分布、成本模型等选择使用以上四个索引中任意一个,或者直接使用 INDEX MERGE 算法来选择合适的索引组合。

这种情况下,要看哪种索引对这条 SQL 最高效,除了之前介绍过的查看索引本身的数据外,还可以从执行计划的 rows 值直接来判断。

我们使用 force index 来指定优化器强制匹配不同的索引,来看这四个索引对应不同执行计划的 rows 值。

代码语言:javascript代码运行次数:0运行复制
localhost:ytt>desc select * from t1 force index (idx_r1) where r1  = 2 and r2 = 2\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ref
possible_keys: idx_r1
          key: idx_r1
      key_len: 5
          ref: const
         rows: 18638
     filtered: 0.10
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

localhost:ytt>desc select * from t1 force index (idx_r2) where r1  = 2 and r2 = 2\G
*************************** 1. row ***************************
...
         rows: 102
     filtered: 11.11
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

localhost:ytt>desc select * from t1 force index (idx_u1) where r1  = 2 and r2 = 2\G
*************************** 1. row ***************************
...
         rows: 12
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

localhost:ytt>desc select * from t1 force index (idx_u2) where r1  = 2 and r2 = 2\G
*************************** 1. row ***************************
...
         rows: 12
     filtered: 100.00
        Extra: NULL
1 row in set, 1 warning (0.00 sec)

根据查询计划可知(索引:rows 值):

  • idx_r1: 18638
  • idx_r2: 102
  • idx_u1: 12
  • idx_u2: 12

很明显,在此场景下走联合索引 idx_u1/idx_u2 扫描记录数最小,效率最高。

rows 值小,性能不一定

有些情况下,不能简单通过 rows 值作为判断 SQL 是否高效执行的标准。

示例 SQL:

select * from t1 where r1<5

这条 SQL 也很简单,就是对 r1 进行一个范围过滤完后取结果。依照之前文章里讲的,对于这样的查询,有时候不走索引反而效率更高,虽然单从走索引扫描的 rows 值一定会更小。来看下两条不同的执行计划:

代码语言:javascript代码运行次数:0运行复制
localhost:ytt>desc select * from t1  where r1  <5 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ALL
possible_keys: idx_r1,idx_u1
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 101745
     filtered: 50.00
        Extra: Using where
1 row in set, 1 warning (0.01 sec)

localhost:ytt>desc select * from t1 force index (idx_r1)  where r1  <5 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: idx_r1
          key: idx_r1
      key_len: 5
          ref: NULL
         rows: 50872
     filtered: 100.00
        Extra: Using index condition
1 row in set, 1 warning (0.00 sec)

根据查询计划可知:

  • 执行计划 1 的 rows 值:101745
  • 执行计划 2 的 rows 值:50872

如果仅从 rows 值来判断,那第二个执行计划更优,但事实并非如此。

MySQL 自主选择了第一个执行计划(全表扫描)。其实就是优化器基于一定的数据基础评估,走全表扫的成本要比走索引后再来回表来的更优化。

为了继续验证我们的判断, 查看 EXPLAIN ANALYZE 结果:

代码语言:javascript代码运行次数:0运行复制
localhost:ytt>desc analyze select * from t1  where r1  <5 \G
*************************** 1. row ***************************
EXPLAIN: -> Filter: (t1.r1 < 5)  (cost=10262.75 rows=50872) (actual time=0.044..104.673 rows=40854 loops=1)
    -> Table scan on t1  (cost=10262.75 rows=101745) (actual time=0.041..90.898 rows=101806 loops=1)

1 row in set (0.12 sec)

localhost:ytt>desc analyze select * from t1 force index (idx_r1)  where r1  <5 \G
*************************** 1. row ***************************
EXPLAIN: -> Index range scan on t1 using idx_r1, with index condition: (t1.r1 < 5)  (cost=22892.66 rows=50872) (actual time=0.437..146.003 rows=40854 loops=1)

1 row in set (0.16 sec)

结果很明显,走全表扫无论成本和最终时间都比走索引有优势。

不适合看 rows 值

前两个情况都是基于单表检索,我们再来看下多表联接的例子。

示例 SQL:

select a.* from t1 a join t2 b using(f0,f1)

这条 SQL 没有过滤条件,仅仅是两表内联,而且表 t1 有 10W 行记录,表 t2 只有 5W 行记录。正常情况,应该走基于主键的 NLJ 算法,表 t2 驱动表 t1

来看下执行计划:

代码语言:javascript代码运行次数:0运行复制
localhost:ytt>desc select  a.* from t1 a join t2 b using(f0,f1)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
   partitions: NULL
         type: ALL
...
         rows: 101745
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: b
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: ytt.a.f0,ytt.a.f1
         rows: 1
     filtered: 100.00
        Extra: Using index
2 rows in set, 1 warning (0.00 sec)

从执行计划结果来看,表 t1 被放在驱动表的位置,rows 值显示需要扫 10W 行记录(全表扫描);表 t2 随后作为被驱动表来检索(走主键),对于表 t2 的效率很高。

这个结果和我们的认知刚好相反(表 t2 的扫描行数仅仅是针对 NLJ 算法的内表来讲,每次扫描的行数,而不是整体扫描的行数),并且两表 JOIN 的顺序不对,我们强制手动收集统计信息再次进行优化:

代码语言:javascript代码运行次数:0运行复制
localhost:ytt>analyze table t1,t2;
+--------+---------+----------+----------+
| Table  | Op      | Msg_type | Msg_text |
+--------+---------+----------+----------+
| ytt.t1 | analyze | status   | OK       |
| ytt.t2 | analyze | status   | OK       |
+--------+---------+----------+----------+
2 rows in set (0.43 sec)

收集完后,再次查看执行计划:

代码语言:javascript代码运行次数:0运行复制
localhost:ytt>desc select  a.* from t1 a join t2 b using(f0,f1)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: b
   partitions: NULL
         type: index
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: NULL
         rows: 48339
     filtered: 100.00
        Extra: Using index
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 8
          ref: ytt.b.f0,ytt.b.f1
         rows: 1
     filtered: 100.00
        Extra: NULL
2 rows in set, 1 warning (0.00 sec)

两表执行顺序做了置换,并且总体的 rows 值都变小;表 t2 为驱动表,rows 值接近 5W,表 t1 做为被驱动表进行内部判断。

总结

在不同的情况下,执行计划 rows 值展示出来的信息有不同的参考价值,并不能直接作为 SQL 高效与否的判断标准。

MySQL 的 SQL 到底是走何种执行计划,与执行计划成本模型、表统计信息、索引统计信息、表的数据分布等都有关系,不能仅凭执行计划 rows 值的大小来判断,需要这些因素来综合决定一个最优的执行计划。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-03-20,如有侵权请联系 cloudcommunity@tencent 删除rows索引统计性能优化

发布者:admin,转转请注明出处:http://www.yc00.com/web/1748198763a4746400.html

相关推荐

  • 第 53 期:EXPLAIN 中最直观的 rows

    作者:杨涛涛,爱可生技术专家。爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。MySQL 和大多数关系型数据库一样,SQL 语句执行计划的输出栏都有一行 rows,代表优化器执行这条 SQL 所需算子扫描的记录数

    3小时前
    10

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信