PolarDB · 特性分析 · Explain Format Tree 详解

前言

Mysql从8.0.18引入了Explain Format Tree的功能,PolarDB-2.0也支持了该功能。 本篇,我们来详细介绍下Explain Format Tree功能,以及Parallel Query对该功能的支持和变化。

Mysql Explain Format Tree 执行计划

Explain Format Tree依赖的是mysql新引入的iterator tree格式的执行计划。通过递归遍历iterator tree来完成Format Tree格式的执行计划的展示。 Iterator的组织形式的树形的,通常的Iterator都只有一个children(JOIN iterator有两个,分别是JOIN的外表和内表),所有在递归遍历过程中直接访问的Iterator都是RowIterator的子类。

下面是一个Explain Format Tree的例子和解读:

  1. mysql> explain format=tree select a1, a3, sum(b2) sum from t1 join t2 where t1.a1 = t2.b2 and t2.b1 > 0 group by a1,a3 order by sum limit 10\G
  2. *************************** 1. row ***************************
  3. EXPLAIN: -> Limit: 10 row(s)
  4. -> Sort: <temporary>.sum, limit input to 10 row(s) per chunk
  5. -> Table scan on <temporary>
  6. -> Aggregate using temporary table
  7. -> Nested loop inner join (cost=9301.16 rows=8737)
  8. -> Filter: (t2.b1 > 0) (cost=51.95 rows=171)
  9. -> Table scan on t2 (cost=51.95 rows=512)
  10. -> Filter: (t1.a1 = t2.b2) (cost=3.03 rows=51)
  11. -> Table scan on t1 (cost=3.03 rows=512)

上面是一个典型的Mysql查询,包含了join, group by, order by 和 limit 通过上面的计划我们可以知道整个SQL的执行过程是

  1. 对t2表进行scan和过滤,t2是驱动表
  2. 将结果和t1表进行nest loop join,join条件是 t1.a1 = t2.b2
  3. 对join的结果进行聚集计算,计算的通过临时表进行的,聚集结果也保存在临时表中
  4. 对临时表的内容进行排序
  5. 对排序结果limit 10输出

这个例子的Iterator Tree的样子是:

  1. LimitOffsetIterator
  2. |-> SortingIterator
  3. |-> TableScanIterator
  4. |-> TemptableAggregateIterator
  5. |-> NestLoopIterator
  6. |-> FilterIterator
  7. | |-> TableScanIterator
  8. |-> FilterIterator
  9. |-> TableScanIterator

可以看出结构和format tree的输出是完全一样的。

Iterator基本结构和组织方式

RowIterator是所有Iterator的基类,下面列出了explain相关的主要成员和方法:

  1. class RowIterator {
  2. public:
  3. struct Child {
  4. RowIterator *iterator;
  5. std::string description;
  6. };
  7. virtual std::vector<Child> children() const { return std::vector<Child>(); } // 返回iterator的children list,在递归中使用
  8. virtual std::vector<std::string> DebugString() = 0 // 输出Iterator的计划信息,不同的Iterator会有各自的实现
  9. JOIN *join_for_explain() const { return m_join_for_explain; } // 如果是join的root_iterator会返回join的指针,用来遍历join上的子查询并输出子查询的查询计划。
  10. private
  11. double m_estimated_cost = -1.0; // Iterator执行完成预期的代价
  12. double m_expected_rows = -1.0; // Iterator输出的行数
  13. };

RowIterator::children

返回当前iterator的children list,包装结构为Child,部分Iterator在实现该方法时除了返回childrend iterator的指针以外,还返回了description。 例如:

  1. mysql> explain format=tree select * from t1 join t2 on a1=b1 \G
  2. *************************** 1. row ***************************
  3. EXPLAIN: -> Inner hash join (t2.b1 = t1.a1) (cost=26269.36 rows=26214)
  4. -> Table scan on t2 (cost=0.01 rows=512)
  5. -> Hash
  6. -> Table scan on t1 (cost=54.20 rows=512)

Iterator tree的结构如下:

  1. HashJoinIterator
  2. --> TableScanIterator // t2, probe side
  3. --> TableScanIterator // t1, build side

HashJoinIterator的children方法返回的内容如下:

  1. {<TableScanIterator(t2), null>,
  2. <TableScanIterator(t1), "Hash">}

其中Hash是description, 这里表示对t1表build hash然后再去join t2表。

RowIterator::DebugString()

产生Iterator的格式化字符串, 每个Iterator都有自己的实现,主要是描述Iterator的行为,包括Table Scan的方式, 过滤的Condition,Join method等等, 通常都是一行。

Explain Format Tree 介绍

执行过程

Explain

  1. - handle_query()
  2. - SELECT_LEX_UNIT::optimize()
  3. - SELECT_LEX::optimize()
  4. - JOIN::optimize()
  5. - JOIN::create_iterators() // 创建join的iterators
  6. - JOIN::create_table_iterators()
  7. - JOIN::create_root_iterator_for_join()
  8. - JOIN::attach_iterators_for_having_and_limit()
  9. - SELECT_LEX_UNIT::create_iterators() // 创建union的iterators
  10. - explain_query()
  11. - ExplainIterator() // 如果explain_format = tree,调用此接口输出执行计划
  12. - PrintQueryPlan() // 将iterator的信息格式化为tree格式的执行计划字符串,该接口会对iteratortree进行递归
  13. - FullDebugString() // 生成iterator的计划信息

PrintQueryPlan()

  1. std::string PrintQueryPlan(int level, RowIterator *iterator) {
  2. string ret;
  3. if (iterator == nullptr) {
  4. ret.assign(level * 4, ' ');
  5. return ret + "<not executable by iterator executor>\n";
  6. }
  7. int top_level = level; // 当前缩进level, 每个level缩进4个空格
  8. // 输出自身的计划信息
  9. for (const string &str : FullDebugString(current_thd, *iterator)) {
  10. ret.append(level * 4, ' ');
  11. ret += "-> ";
  12. ret += str;
  13. ret += "\n";
  14. ++level;
  15. }
  16. // 遍历所有的children iterator, 递归输出计划信息
  17. for (const RowIterator::Child &child : iterator->children()) {
  18. if (!child.description.empty()) {
  19. ret.append(level * 4, ' ');
  20. ret.append("-> ");
  21. ret.append(child.description);
  22. ret.append("\n");
  23. ret += PrintQueryPlan(level + 1, child.iterator);
  24. } else {
  25. ret += PrintQueryPlan(level, child.iterator);
  26. }
  27. }
  28. //
  29. if (iterator->join_for_explain() != nullptr) {
  30. for (const auto &child :
  31. GetIteratorsFromSelectList(iterator->join_for_explain())) {
  32. ret.append(top_level * 4, ' ');
  33. ret.append("-> ");
  34. ret.append(child.description);
  35. ret.append("\n");
  36. ret += PrintQueryPlan(top_level + 1, child.iterator);
  37. }
  38. }
  39. return ret;
  40. }

FullDebugString()

通过本方法输出一个iterator的完整的计划信息,包括iterator本身的信息,cost信息和执行信息

  1. vector<string> FullDebugString(const THD *thd, const RowIterator &iterator) {
  2. vector<string> ret = iterator.DebugString(); // 生成iterator的计划信息
  3. if (iterator.expected_rows() >= 0.0) { // 生成cost info
  4. // NOTE: We cannot use %f, since MSVC and GCC round 0.5 in different
  5. // directions, so tests would not be reproducible between platforms.
  6. // Format/round using my_gcvt() and llrint() instead.
  7. char cost_as_string[FLOATING_POINT_BUFFER];
  8. my_fcvt(iterator.estimated_cost(), 2, cost_as_string, /*error=*/nullptr);
  9. char str[512];
  10. snprintf(str, sizeof(str), " (cost=%s rows=%lld)", cost_as_string,
  11. llrint(iterator.expected_rows()));
  12. ret.back() += str;
  13. }
  14. if (thd->lex->is_explain_analyze) { // 生成执行信息
  15. if (iterator.expected_rows() < 0.0) {
  16. // We always want a double space between the iterator name and the costs.
  17. ret.back().push_back(' ');
  18. }
  19. ret.back().push_back(' ');
  20. ret.back() += iterator.TimingString();
  21. }
  22. return ret;
  23. }

Parallel Query 中 Explain Format Tree 的变化

PolarDB的Parallel Query功能引入了新的exchange算子, 同时多阶段并行计划在生成计划的过程中是基于Cost选择的最优计划,我们对Parallel Query计划Format Tree的输出信息进行了完善和补充, 下面是Parallel Query计划的Explain Format Tree的一个简单例子。

  1. mysql> explain format=tree select a1, a3, sum(b2) sum from t1 join t2 where t1.a1 = t2.b2 and t2.b1 > 0 group by a1,a3 order by sum limit 10\G
  2. *************************** 1. row ***************************
  3. EXPLAIN: -> Limit: 10 row(s) (cost=2825.63 rows=10)
  4. -> Sort: <temporary>.sum, limit input to 10 row(s) per chunk (cost=2825.63 rows=51)
  5. -> Stream results
  6. -> Gather (slice: 1; workers: 4) (cost=2815.55 rows=51)
  7. -> Table scan on <temporary>
  8. -> Aggregate using temporary table (cost=2802.35 rows=13)
  9. -> Nested loop inner join (cost=2363.21 rows=2184)
  10. -> Repartition (hash keys: t2.b2; slice: 2; workers: 2) (cost=50.91 rows=43)
  11. -> Filter: (t2.b1 > 0) (cost=25.97 rows=85)
  12. -> Parallel table scan on t2, with parallel partitions: 2 (cost=25.97 rows=256)
  13. -> Filter: (t1.a1 = t2.b2) (cost=3.12 rows=51)
  14. -> Table scan on t1 (cost=3.12 rows=512)

从上面的计划可以看出相对于Mysql原版计划,parallel query的计划作出了下面的变化

  1. 对 group by/ distinct / order by / window / limit iterator增加了代价显示的支持
  2. 新增了exchange算子和exchange算子详细信息的显示,包括算子类型(gather/repartition/nroadcast), slice id, parallel worker number。 对于repartition的exchange算子,同时还显示了repartition column。

附录

Iterator List

  1. RowIterator <|--- TimingIterator
  2. |- UnqualifiedCountIterator
  3. |- FakeSingleRowIterator
  4. |- ZeroRowsIterator
  5. |- ZeroRowsAggregatedIterator
  6. |- FollowTailIterator
  7. |- FilterIterator
  8. |- LimitOffsetIterator
  9. |- AggregateIterator
  10. |- PrecomputedAggregateIterator
  11. |- NestedLoopIterator
  12. |- CacheInvalidatorIterator
  13. |- WeedoutIterator
  14. |- RemoveDuplicatesIterator
  15. |- NestedLoopSemiJoinWithDuplicateRemovalIterator
  16. |- WindowingIterator
  17. |- BufferingWindowingIterator
  18. |- MaterializeInformationSchemaTableIterator
  19. |- AppendIterator
  20. |- HashJoinIterator
  21. |- SortingIterator
  22. |- TableRowIterator <|--- TableScanIterator
  23. |- IndexScanIterator
  24. |- IndexRangeScanIterator
  25. |- SortBufferIterator
  26. |- SortBufferIndirectIterator
  27. |- SortFileIterator
  28. |- SortFileIndirectIterator
  29. |- MaterializeIterator
  30. |- StreamingIterator
  31. |- TemptableAggregateIterator
  32. |- MaterializedTableFunctionIterator
  33. |- RefIterator
  34. |- RefOrNullIterator
  35. |- EQRefIterator
  36. |- ConstIterator
  37. |- FullTextSearchIterator
  38. |- DynamicRangeIterator
  39. |- PushedJoinRefIterator
  40. |- AlternativeIterator

注:文中引用代码是基于mysql-8.0.20版本的。