Mysql None-SPJ算子执行流程

Author: 陈江

背景

作者在polardb下一个版本开发中修复了大量带groupby+rollup集合结果不正确的bug, 顺带抽象总结一下None SPJ算子执行流程,不理解思想的话,单纯通过代码逆向反推逻辑会耗费大量精力,希望本篇文章对于入坑mysql同学有帮助。代码基于版本是mysql 8.4.2。 先抛出一个问题,我们查看一个常见groupby+order by sql执行计划时,是什么?为什么一定要加一张临时表?

  1. CREATE TABLE t1 (a INT, b INT);
  2. INSERT INTO t1 (a, b) VALUES (1,1), (1,2), (1,3);
  3. INSERT INTO t1 SELECT a + 1, b FROM t1;
  4. mysql> desc format=tree SELECT a*a, sum(b)+1, count(1)+1 as cnt FROM t1 GROUP BY a ORDER BY cnt DESC;
  5. +----------------------------------------------------------------------------------------------------------------------------------------------------+
  6. | EXPLAIN |
  7. +----------------------------------------------------------------------------------------------------------------------------------------------------+
  8. | -> Sort: cnt DESC
  9. -> Table scan on <temporary>
  10. -> Aggregate using temporary table
  11. -> Table scan on t1 (cost=0.85 rows=6)
  12. |
  13. +----------------------------------------------------------------------------------------------------------------------------------------------------+

关系代数算子执行顺序

  1. FROM, including JOINs
  2. WHERE
  3. GROUP BY + ROLLUP
  4. HAVING
  5. WINDOW functions
  6. DISTINCT
  7. ORDER BY
  8. LIMIT and OFFSET
  9. Projection

我们复习一下关系代数,上游算子执行结果作为下游算子输入,执行完下游算子再继续传递输出给下下游算子,可以简单理解为多个阶段执行 关键none-spj算子在mysql执行方式介绍

  • GROUP BY实现方式
    • filesort算子 + (Agg算子)AggregateIterator 先对输入进行排序,顺序scan到不同排序key值可以发现是否是新分组,同步的更新聚合函数value,值得注意的是聚合函数的值存放在group buffer中,没有跟server中的TABLE对象进行映射。
    • tmptable grouping,对应算子TemptableAggregateIterator 使用临时表进行聚合,为groupby建立索引列,每读上一行数据,groupby列作为KEY值在索引中查找,如能找到更新聚合函数值到临时表中,聚合函数值是存放在临时表中的,这个过程中是会写磁盘/内存 文件的。
  • Groupby+ROLLUP,因为带有rollup,rollup实现的特点是groupby列排好序,读上一个新行,很容易判断跟上一行有哪几列不一样,然后进行rollup,所以这个算子在mysql中只能选择filesort+AGG方式执行,逻辑也在AggregateIterator
  • HAVING:就是filter,比较简单,不在讨论范畴内
  • WINDOW:需要上游算子保证输出数据流在一张表里面,每个window算子自己会把结果写到一张临时表,输出绑定到Table对象上,算子复杂暂不讨论。
  • DISTINCT
    • 物化表+ deduplication实现,把上一算子结果物化到临时表的时候,加上去重逻辑,思路也是将可见列建立唯一二级索引,写临时表失败代表tuple重复了
    • filesort+ deduplication, 排好序后去重读结果
  • ORDER BY 采用filesort算子,外排,多个chunk排好序后,两两做merge sort. 在mysql实现中构造filesort需要传递Tables,要求它的上游算子输出的tuple必须是跟TABLE对象绑定好的。

    1. Filesort(THD *thd, Mem_root_array<TABLE *> tables, bool keep_buffers,
    2. ORDER *order, ha_rows limit_arg, bool remove_duplicates,
    3. bool force_sort_rowids, bool unwrap_rollup);

    比较奇葩的是在8.018版本中构造filesort传递的是单Table对象,需要上游算子保证输出数据流在一张表里面。在8.018里面一个简单的join+sort会额外转临时表一次

    1. mysql> desc format=tree select * from t1,t2 order by a;
    2. +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    3. | EXPLAIN |
    4. +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    5. | -> Sort: <temporary>.a
    6. -> Stream results
    7. -> Inner hash join (no condition) (cost=0.80 rows=2)
    8. -> Table scan on t1 (cost=0.45 rows=2)
    9. -> Hash
    10. -> Table scan on t2 (cost=0.35 rows=1)

    8.4.2 Hyper优化器中没这问题, 但旧优化器社区基本没重构,仍是原版本行为

    1. set optimizer_switch='hypergraph_optimizer=on';
    2. mysql> desc format=tree select * from t1,t2 order by t1.a,t2.a;
    3. +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    4. | EXPLAIN |
    5. +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    6. | -> Sort: t1.a, t2.a (cost=10.3..10.3 rows=18)
    7. -> Nested loop inner join (cost=0.0556..1 rows=18)
    8. -> Table scan on t2 (cost=0.0833..0.25 rows=3)
    9. -> Table scan on t1 (cost=0.0417..0.25 rows=6)

为什么需要临时表?

mysql有两种场景需要临时表

  • 用临时表来实现算子,如GB, distinct, derived table, cte,集合操作 临时表可以帮实现一些算子,比如groupby,distinct,建立唯一键约束,达到去重的效果,临时表可以写磁盘,突破内存容量的限制。
  • 作为桥接算子,单纯因为mysql实现方式 因为mysql上述算子自身的限制,当算子对于上游输出有要求后,上游又不能满足要求会增加streaming/materialize算子作为桥接[1]。 注[1]:mysql streaming算子是流式的,不会中断pipeline。作用是把上游算子输出拷贝临时表自定义字段上,作为下游算子输入, 参考StreamingIterator::Read

所以举个常见的Groupby+sort的例子,假定GB使用filesort+AGG实现,算子树就是

  1. FileSort
  2. |->Agg //到这实现了Groupby
  3. |->Filesort
  4. |->Scan table

因为Filesort必须绑定table限制,以及Agg算子输出没有绑定table对象[2],它们需要一张临时表做下转换。这个时候会在AGG算子上添加streaming算子,转成临时表,这样就满足了filesort对输入数据要求。如果filesort上游算子本身就提供了临时表,是不需要添加额外的桥接算子的, 如window + sort,window自身提供tmptable,sort就不再需要额外临时表。

[2]):参考AggregateIterator::Read, 聚合函数的结果是写到单独的group buff中,不在扫描的table record0中

  1. mysql> desc format=tree select c, count(1) as cnt from t2 force index(idx_c) group by c order by cnt ;
  2. +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  3. | EXPLAIN |
  4. +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
  5. | -> Sort: cnt
  6. -> Stream results (cost=1.6 rows=1.73) //添加streaming算子,新加的临时表
  7. -> Group aggregate: count(1) (cost=1.6 rows=1.73)
  8. -> Covering index scan on t2 using idx_c (cost=1.3 rows=3) //索引提供了order序,就不用filesort了

桥接算子介绍

上面我们介绍了None-SPJ所有算子的执行方式,这章我们介绍一下桥接算子

功能

用于将上一算子输出绑定到Table对象上,供下游算子使用,表象是写到一张新临时表里

包含具体算子

stream算子

代码StreamingIterator,具体实现就是一表达式求值写入新tuple buff

  • 创建一张临时表
  • 基于上一算子输出列,构建拷贝任务,求值写入到临时表的列
  • 流式的读上游一行,填充临时表一行,传递给下游算子用

    materialize算子

    代码MaterializeIterator,跟stream差不多,不过它的临时表数据需要落盘,不是流式的。 比如后面的filesort需要blob列进行排序,桥接算子会变成materialize

优化阶段

跟none-spj执行流程关系不大,我们只是记录一下前置内存状态。我们拿如下sql调试举例

  1. mysql> desc format=tree SELECT a*a, sum(b)+1, count(1)+1 as cnt FROM t1
  2. -> GROUP BY a ORDER BY cnt DESC;
  3. +----------------------------------------------------------------------------------------------------------------------------------------------------+
  4. | EXPLAIN |
  5. +----------------------------------------------------------------------------------------------------------------------------------------------------+
  6. | -> Sort: cnt DESC
  7. -> Table scan on <temporary>
  8. -> Aggregate using temporary table
  9. -> Table scan on t1 (cost=0.85 rows=6)

经过optimize groupby/optimize order后,queryblocks的fields及base_ref_items内存状态是这样的 base_ref_items内存布局

gdb打印fields每个item对象, orderby, groupby的item是二级指针,指向base_ref_items的槽位。当前base_ref_items存放的fields就是初始slice0

  1. `--$m1 (Query_block *) 0x7fdf0817f398 tables: ($n0)t1 t1 <-
  2. (gdb) p $m1->fields
  3. $11 = mem_root_deque<Item*> = {$o0 = (Item *) 0x7fdf081897d0, $o1 = (Item *) 0x7fdf08180760, $o2 = (Item *) 0x7fdf08180118, $o3 = (Item *) 0x7fdf0817f928, $o4 = (Item *) 0x7fdf081803f0, $o5 = (Item *) 0x7fdf08180990}
  4. (gdb) my e $o0
  5. $p0 (Item_field *) 0x7fdf081897d0 field = t1.a
  6. (gdb) my e $o1
  7. $q0 (Item_sum_count *) 0x7fdf08180760
  8. `--$q1 (Item_int *) 0x7fdf081805d8 value = 1
  9. (gdb) my e $o2
  10. $r0 (Item_sum_sum *) 0x7fdf08180118
  11. `--$r1 (Item_field *) 0x7fdf081887a0 field = t1.b
  12. (gdb) my e $o3
  13. $s0 (Item_func_mul *) 0x7fdf0817f928
  14. |--$s1 (Item_field *) 0x7fdf081884b0 field = t1.a
  15. `--$s2 (Item_field *) 0x7fdf08188620 field = t1.a
  16. (gdb) my e $o4
  17. $t0 (Item_func_plus *) 0x7fdf081803f0
  18. |--$t1 (Item_aggregate_ref *) 0x7fdf0818ac48 field =
  19. | `--$t2 (Item_sum_sum *) 0x7fdf08180118
  20. | `--$t3 (Item_field *) 0x7fdf081887a0 field = t1.b
  21. `--$t4 (Item_int *) 0x7fdf08180328 value = 1
  22. (gdb) my e $o5
  23. $u0 (Item_func_plus *) 0x7fdf08180990
  24. |--$u1 (Item_aggregate_ref *) 0x7fdf0818ad68 field =
  25. | `--$u2 (Item_sum_count *) 0x7fdf08180760
  26. | `--$u3 (Item_int *) 0x7fdf081805d8 value = 1
  27. `--$u4 (Item_int *) 0x7fdf081808c8 value = 1
  28. //groupby
  29. (gdb) my e $v1->group_list.first->item
  30. $x0 (Item_field *) 0x7fdf081897d0 field = t1.a
  31. //orderby
  32. (gdb) my e * $v1->order_list.first->item
  33. $y0 (Item_func_plus *) 0x7fdf08180990
  34. |--$y1 (Item_aggregate_ref *) 0x7fdf0818ad68 field =
  35. | `--$y2 (Item_sum_count *) 0x7fdf08180760
  36. | `--$y3 (Item_int *) 0x7fdf081805d8 value = 1
  37. `--$y4 (Item_int *) 0x7fdf081808c8 value = 1

生成预执行数据结构

添加新临时表思想

因为filesort算子对于输入数据的要求(绑定table对象)及streaming AGG算子输出数据的限制(聚合值没绑定到Table上),我们视情况添加临时表进行桥接。BTW,mysql原生的make_tmp_tables_info代码逻辑乱的令人发指,完全背离低耦合,高内聚编程思想,想要逆向推导作者设计思想非常难,完全是一坨屎山代码。

  • ApplyGroupBy:
    • 如果输入流满足group by列要求的order,比如扫描索引了,这个时候直接Apply AGG算子完成GroupBY,直接AGG就是最优的
    • 如果不满足
      • 其实可以添加filesort + AGG实现,这其实是当前的hyper优化器路径。
      • 当前生产版本会使用tmp table grouping方式完成GB,对应代码算子是TemptableAggregateIterator,集合函数结果都存放在临时表内,需要生成新临时表
  • ApplyHaving: 在上一stage结果上添加filter算子,执行过滤
  • pre-ApplyWindow算子:如果有window算子 如果当前输入流是streamingAgg这种没绑定table对象tuples,需要添加stream算子,会创建新的临时表。如果是tmp table grouping,输入已经绑定到了临时表上就不需要。
    • 没有blob列(!need_rowid) 创建streaming算子
    • 否则:创建Materialize算子,会创建新临时表,上一stage结果物化下来
  • ApplyWindows: 每个win窗口函数会创建一个新临时表,如果window算子参数是blob_field,每次结果都要物化保存下来。
  • ApplyDistinct: 也是filesort + deduplication思路,如果前序流输出没绑定到table对象,则 需要额外的streaming临时表,否则不需要。例如前序算子是window,filesort可以直接使用windows算子的临时表
  • ApplyOrderBy: filesort算子,如果前序流输出没绑定到table对象,则 需要额外的streaming临时表,否则不需要。例如前面算子有streaming group&没有window算子 ,会创建额外streaming用的临时表。 如果有windows算子,filesort可以直接用windows算子的临时表

对比成熟商业数据库

这种桥接算子越少越好,mysql目前最多添加1-2张额外临时表,会带来如下消耗

  • 额外增加了一层虚函数调用
  • streaming算子还算轻量,额外增加一次内存拷贝操作
  • materialize算子比较重,会物化临时表,写盘 特意找了些bad case, 原生的优化器对于None-SPJ算子执行选择远不是最优

    1. mysql> desc format=tree select distinct sum(b) from t2 group by a with rollup having count(1)+1 > 10;
    2. +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    3. | EXPLAIN |
    4. +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    5. | -> Table scan on <temporary> (cost=5..5 rows=0)
    6. -> Temporary table with deduplication (cost=2.5..2.5 rows=0) #物化去重实现distinct
    7. -> Table scan on <temporary> (cost=2.5..2.5 rows=0)
    8. -> Temporary table (cost=0..0 rows=0) #桥接物化,绑定到Table对象上
    9. -> Filter: ((rollup_sum_switcher(count(1)) + 1) > 10)
    10. -> Group aggregate with rollup: count(1), sum(t2.b) (cost=0.85 rows=2.73)
    11. -> Sort: a (cost=0.55 rows=3)
    12. -> Table scan on t2 (cost=0.55 rows=3)

    对比之下hyper:添加streaming算子,避免物化写盘,实际执行效率也是优于原生

    1. mysql> set optimizer_switch='hypergraph_optimizer=on';
    2. Query OK, 0 rows affected, 1 warning (0.00 sec)
    3. mysql> desc select distinct sum(b) from t2 group by a with rollup having count(1)+1 > 10;
    4. +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    5. | EXPLAIN |
    6. +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    7. | -> Sort with duplicate removal: `sum(b)` (cost=2.24..2.24 rows=1.65)#filesort实现distinct
    8. -> Stream results (cost=0.576..1.57 rows=2.73) #streaming桥接,绑定到Table对象上
    9. -> Filter: ((rollup_sum_switcher(count(1)) + 1) > 10) (cost=0.576..1.57 rows=2.73)
    10. -> Group aggregate with rollup: count(1), sum(t2.b) (cost=0.476..1.3 rows=2.73)
    11. -> Index scan on t2 using idx_a (cost=0.333..1 rows=3)

    理想的执行plan,只需一次去重物化实现distinct,魔改mysql是可以做到

    1. +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    2. | EXPLAIN |
    3. +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    4. | -> Table scan on <temporary> (cost=5..5 rows=0)
    5. -> Temporary table with deduplication (cost=2.5..2.5 rows=0) #物化去重实现distinct,groupbuff中的聚合结果直接物化
    6. -> Filter: ((rollup_sum_switcher(count(1)) + 1) > 10)
    7. -> Group aggregate with rollup: count(1), sum(t2.b) (cost=0.85 rows=2.73)
    8. -> Index scan on t2 using idx_a (cost=0.333..1 rows=3)

创建临时表及slice

示例SQL groupby算子选择了tmptable grouping,orderby算子物理实现是filesort。 tmptable grouping会额外创建一张临时表,每张临时表代表一个新stage,会额外新申请一个slice,存放该阶段的fields,例子中是REF_SLICE_TMP1(第一张临时表) slice存放的item都是临时表的fields以及上一stage没计算的算子。 例子中REF_SLICE_TMP1 内存状态如下图:(Tmp是临时表) 本地图片

REF_SLICE_ACTIVE永远指向base_ref_items数组,当前就是上面GDB打印的fields. REF_SLICE_TMP1 slice是从上一stage构建而来的。这一张临时表会完成聚合函数运算,存储t1.a, count(1), sum(t1.b), t1.a*t1.a到临时表 构建REF_SLICE_TMP1遵循如下规则

  • Item_field, Item_sum, 不含聚合函数的Item_func都能完成计算,在临时表Tmp有field存着,会推入REF_SLICE_TMP1 slice.使用临时表的field代替。
    • 这些item从slice_active写到临时表tuple buff(mysql中的record0) 是通过Temp_table_param.items_to_copy实现,可以简单理解为一次内存拷贝
  • 包含聚合函数的Item_func在groupby算子没计算的,原样写入REF_SLICE_TMP1 slice
  • 新建的Table对象会记录slice_no, 这样groupby算子做完,切换到tmptable记录的slice_no,后续算子使用新stage中的表达式。例子里的order by stage直接使用REF_SLICE_TMP1 slice中的表达式就好了。

每新生成一个临时表就是一个新stage,join算子是stage0,也就是初始的fields,一开始就会暂存到 REF_SLICE_SAVED_BASE。 none-SPJ算子会构建多张临时表,所以stage构建出下图: 本地图片

  • 构建时
    • 上图按TMP1->TMP2->WIN_1 …->WIN_N顺序构建slice,每个新slice都是以上一个slice为模板构建,slice的临时表Temp_table_param.items_to_copy会记录这种求值-写入目标关系。
  • 执行时候
    • 先切到saved_base slice,记录的是初始的fields。比如groupby的item是指向active slice槽位的二级指针,就能读到基表的列值。
    • 运算到None SPJ算子,先将输入slice_no对应的items替换掉Active, 因为Item_ref引用的永远都是Active slice里面的槽位,替换了才能保证Item_ref读到上一stage的输出,才能正确求值
    • 运算当前None SPJ算子, 执行完切换到当前临时表的slice_no,将临时表的slice写回active slice。 后续算子就能读到这个stage的输出了
  • tmptable打上标记
    • OT_AGGREGATE_INTO_TMP_TABLE: groupby的temptable grouping执行方式
    • OT_AGGREGATE_THEN_MATERIALIZE: groupby的AGG+ streaming/materialize执行方式,输入流肯定是能保证GB需要的order的
    • OT_WINDOWING_FUNCTION:window算子+物化算子,算子执行物化下来
    • OT_MATERIALIZE:物化算子,上stage表达式求值写到临时表tuple buff,物化下来

生成执行器

在JOIN::create_root_access_path_for_join中

  1. 先生成spj的AccessPath, 主要是各种tablescan+join
  2. 生成none-spj AccessPath,遍历每张临时表的QEP_TAB,看标记类型

    • OT_AGGREGATE_INTO_TMP_TABLE->生成TEMPTABLE_AGGREGATE AccessPath->生成TemptableAggregateIterator
    • OT_AGGREGATE_THEN_MATERIALIZE: AGG+Streaming/Materialize算子

    • AGG:->生成AGGREGATE AccessPath->生成AggregateIterator

    • Streaming或者Materialize, 如果后面的filesort有blob排序列或后面有distinct算子,或没有filesort,使用Streaming算子,否则使用Materialize算子.
    • Streaming->生成STREAM AccessPath->生成StreamingIterator
    • Materialize->生成MATERIALIZE AccessPath->生成MaterializeIterator * OT_MATERIALIZE:物化算子,上面介绍过了

执行阶段

上面生成新slice也介绍了怎么把上一stage数据写入当前临时表tuple buff,以及写完切slice,作为下一stage的读取,下一stage就能看到正确的表达式了。下面我们结合例子看下调试信息 TemptableAggregateIterator:输入slice是saved,也就是原始的fields,输出slice是1

TemptableAggregateIterator::Init, 大致逻辑:

  • 从底层算子读上一条数据,调用copy_field,上一slice的Item_field数据拷贝到临时表对应的field,groupby算子可以准备求值了。
  • groupby每列算子求值,得到检索值,在tmptable按索引查找

    • 如找到:旧组,更新聚合值,写回tmptable中
    • 未找到:新组,调用copy_funcs,会求值Item_fields&Item_func并写入tmp table的field,带上聚合函数初始值写入tmptable中

      1. (gdb) p *param->items_to_copy
      2. $205 = Func_ptr_array = {$ih0 = (Func_ptr) {m_func = 0x7fdef80c6028, m_result_field = 0x7fdef80cc130, m_result_item = 0x0, m_func_bits = 33}, $ih1 = (Func_ptr) {m_func = 0x7fdef80c9028, m_result_field = 0x7fdef80cbe68, m_result_item = 0x0, m_func_bits = 289}}
      3. (gdb) my e $ih0.m_func
      4. $ii0 (Item_func_mul *) 0x7fdef80c6028
      5. |--$ii1 (Item_field *) 0x7fdef80c7d08 field = t1.a
      6. `--$ii2 (Item_field *) 0x7fdef80c7e78 field = t1.a
      7. (gdb) my e $ih0.m_result_field
      8. $ik0 (Field_longlong *) 0x7fdef80cc130 field = <temporary>.a*a // a*a 写入 tmp."a*a" 中
      9. (gdb) my e $ih1.m_func
      10. $il0 (Item_field *) 0x7fdef80c9028 field = t1.a
      11. (gdb) my e $ih1.m_result_field
      12. $ij0 (Field_long *) 0x7fdef80cbe68 field = <temporary>.a // t1.a 写入 tmp.a 中

TemptableAggregateIterator::Read

因为是火山模型,从临时表读一条数据,并切换到当前slice,未执行的表达式可以准备求值了

  1. int TemptableAggregateIterator<Profiler>::Read() {
  2. /*
  3. Enable the items which one should use if one wants to evaluate
  4. anything (e.g. functions in WHERE, HAVING) involving columns of this
  5. table.
  6. */
  7. if (m_join != nullptr && m_ref_slice != -1) {
  8. if (!m_join->ref_items[m_ref_slice].is_null()) {
  9. m_join->set_ref_item_slice(m_ref_slice);
  10. }
  11. }
  12. int err = m_table_iterator->Read();
  13. }
  14. (gdb) p m_join->current_ref_item_slice
  15. $210 = 1 //当前slice=1
  16. (gdb) my e m_join->ref_items[m_ref_slice].m_array[1]
  17. $jc0 (Item_func_plus *) 0x7fdef80c6af0 //可以看到这个未求值expr变为<temporary>.`sum(t1.b)`+1
  18. |--$jc1 (Item_aggregate_ref *) 0x7fdef80ca6c8 field =
  19. | `--$jc2 (Item_field *) 0x7fdef80ccd08 field = <temporary>.sum(t1.b)
  20. `--$jc3 (Item_int *) 0x7fdef80c6a28 value = 1
  21. (gdb) my e m_join->ref_items[m_ref_slice].m_array[2] //可以看到这个未求值expr变为<temporary>.`count(1)`+1
  22. $jd0 (Item_func_plus *) 0x7fdef80c7090
  23. |--$jd1 (Item_aggregate_ref *) 0x7fdef80ca7e8 field =
  24. | `--$jd2 (Item_field *) 0x7fdef80ccb98 field = <temporary>.count(1)
  25. `--$jd3 (Item_int *) 0x7fdef80c6fc8 value = 1
  26. 执行到filesort算子时,可以cnt表达式的值了
  27. $jm0 (Item_func_plus *) 0x7fdef80c7090
  28. |--$jm1 (Item_aggregate_ref *) 0x7fdef80ca7e8 field =
  29. | `--$jm2 (Item_field *) 0x7fdef80ccb98 field = <temporary>.count(1)
  30. `--$jm3 (Item_int *) 0x7fdef80c6fc8 value = 1
  31. (gdb) p item->val_int()
  32. $215 = 4
  33. (gdb) f
  34. #0 (anonymous namespace)::make_sortkey_from_item (item=0x7fdef80c7090, result_type=INT_RESULT, dst_length=std::optional<unsigned long> = {...}, tmp_buffer=0x7fdef80cd088, to=0x7fdef80d0d80 '\217' <repeats 200 times>...,
  35. to_end=0x7fdef80d8d80 "\217\217\217\217\217\217\217\217\021\210", maybe_null=0x7fdfe45f0347, hash=0x7fdfe45f0348) at /home/jacob.cj/mysql-bld/sql/filesort.cc:1314

groupby using filesort+agg

受限于本文篇幅,就不再展开介绍了,读者可以根据提供的sql自行调试,mysql GB+rollup一定是流式聚集的,保序才能Rollup

  1. mysql> desc format=tree SELECT a*a, sum(b)+1, count(1)+1 as cnt FROM t1 GROUP BY a with rollup ORDER BY cnt DESC;
  2. | -> Sort: cnt DESC
  3. -> Stream results (cost=1.45 rows=3.45)
  4. -> Group aggregate with rollup: count(1), sum(t1.b) (cost=1.45 rows=3.45)
  5. -> Sort: a (cost=0.85 rows=6)
  6. -> Table scan on t1 (cost=0.85 rows=6)

总结

本文聚集于None-SPJ算子执行流程,涉及到JOIN::optimize里面辅助结构构建,Iterator构建,以及算子具体实现。覆盖的代码非常多,模块之间耦合度非常大,需要通盘整体看下来,只看部分模块逻辑非常容易困惑,只见数据不见森林。了解整体思想后,非常有助于源码理解。PolarDB-mysql对于这些复杂算子也是支持多阶段并行的,能大大加速查询,欢迎使用。

原文:http://mysql.taobao.org/monthly/2024/09/03/