Max/Min 函数消除规则

在 SQL 中包含了 max/min 函数时,查询优化器会尝试使用 max/min 消除优化规则来将 max/min 聚合函数转换为 TopN 算子,从而能够有效地利用索引进行查询。

根据 select 语句中 max/min 函数的个数,这一优化规则有以下两种表现形式:

只有一个 max/min 函数时的优化规则

当一个 SQL 满足以下条件时,就会应用这个规则:

  • 只有一个聚合函数,且为 max 或者 min 函数。
  • 聚合函数没有相应的 group by 语句。

例如:

  1. select max(a) from t

这时 max/min 消除优化规则会将其重写为:

  1. select max(a) from (select a from t where a is not null order by a desc limit 1) t

这个新的 SQL 语句在 a 列存在索引(或 a 列是某个联合索引的前缀)时,能够利用索引只扫描一行数据来得到最大或者最小值,从而避免对整个表的扫描。

上述例子最终得到的执行计划如下:

  1. mysql> explain select max(a) from t;
  2. +------------------------------+---------+-----------+-------------------------+-------------------------------------+
  3. | id | estRows | task | access object | operator info |
  4. +------------------------------+---------+-----------+-------------------------+-------------------------------------+
  5. | StreamAgg_13 | 1.00 | root | | funcs:max(test.t.a)->Column#4 |
  6. | └─Limit_17 | 1.00 | root | | offset:0, count:1 |
  7. | └─IndexReader_27 | 1.00 | root | | index:Limit_26 |
  8. | └─Limit_26 | 1.00 | cop[tikv] | | offset:0, count:1 |
  9. | └─IndexFullScan_25 | 1.00 | cop[tikv] | table:t, index:idx_a(a) | keep order:true, desc, stats:pseudo |
  10. +------------------------------+---------+-----------+-------------------------+-------------------------------------+
  11. 5 rows in set (0.00 sec)

存在多个 max/min 函数时的优化规则

当一个 SQL 满足以下条件时,就会应用这个规则:

  • 有多个聚合函数,且所有的聚合函数都是 max/min
  • 聚合函数没有相应的 group by 语句。
  • 每个 max/min 聚合函数参数中的列都有索引能够保序。

下面是一个简单的例子:

  1. select max(a) - min(a) from t

优化规则会先检查 a 列是否存在索引能够为其保序,如果存在,这个 SQL 会先被重写为两个子查询的笛卡尔积:

  1. select max_a - min_a
  2. from
  3. (select max(a) as max_a from t) t1,
  4. (select min(a) as min_a from t) t2

这样,两个子句中的 max/min 函数就可以使用上述“只有一个 max/min 函数时的优化规则”分别进行优化,最终重写为:

  1. select max_a - min_a
  2. from
  3. (select max(a) as max_a from (select a from t where a is not null order by a desc limit 1) t) t1,
  4. (select min(a) as min_a from (select a from t where a is not null order by a asc limit 1) t) t2

同样的,如果 a 列能够使用索引保序,那这个优化只会扫描两行数据,避免了对整个表的扫描。但如果 a 列没有可以保序的索引,这个变换会使原本只需一次的全表扫描变成两次,因此这个规则就不会被应用。

最后得到的执行计划:

  1. mysql> explain select max(a)-min(a) from t;
  2. +------------------------------------+---------+-----------+-------------------------+-------------------------------------+
  3. | id | estRows | task | access object | operator info |
  4. +------------------------------------+---------+-----------+-------------------------+-------------------------------------+
  5. | Projection_17 | 1.00 | root | | minus(Column#4, Column#5)->Column#6 |
  6. | └─HashJoin_18 | 1.00 | root | | CARTESIAN inner join |
  7. | ├─StreamAgg_45(Build) | 1.00 | root | | funcs:min(test.t.a)->Column#5 |
  8. | └─Limit_49 | 1.00 | root | | offset:0, count:1 |
  9. | └─IndexReader_59 | 1.00 | root | | index:Limit_58 |
  10. | └─Limit_58 | 1.00 | cop[tikv] | | offset:0, count:1 |
  11. | └─IndexFullScan_57 | 1.00 | cop[tikv] | table:t, index:idx_a(a) | keep order:true, stats:pseudo |
  12. | └─StreamAgg_24(Probe) | 1.00 | root | | funcs:max(test.t.a)->Column#4 |
  13. | └─Limit_28 | 1.00 | root | | offset:0, count:1 |
  14. | └─IndexReader_38 | 1.00 | root | | index:Limit_37 |
  15. | └─Limit_37 | 1.00 | cop[tikv] | | offset:0, count:1 |
  16. | └─IndexFullScan_36 | 1.00 | cop[tikv] | table:t, index:idx_a(a) | keep order:true, desc, stats:pseudo |
  17. +------------------------------------+---------+-----------+-------------------------+-------------------------------------+
  18. 12 rows in set (0.01 sec)