用 EXPLAIN 查看使用子查询的执行计划

    本文档所使用的示例表数据如下:

    以下示例中,IN 子查询会从表 t2 中搜索一列 ID。为保证语义正确性,TiDB 需要保证 t1_id 列的值具有唯一性。使用 EXPLAIN 可查看到该查询的执行计划去掉重复项并执行 Inner Join 内连接操作:

    1. EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2);
    1. +--------------------------------+----------+-----------+------------------------------+---------------------------------------------------------------------------------+
    2. | id | estRows | task | access object | operator info |
    3. +--------------------------------+----------+-----------+------------------------------+---------------------------------------------------------------------------------+
    4. | IndexMergeJoin_19 | 45.00 | root | | inner join, inner:TableReader_14, outer key:test.t2.t1_id, inner key:test.t1.id |
    5. | ├─HashAgg_38(Build) | 45.00 | root | | group by:test.t2.t1_id, funcs:firstrow(test.t2.t1_id)->test.t2.t1_id |
    6. | └─IndexReader_39 | 45.00 | root | | index:HashAgg_31 |
    7. | └─HashAgg_31 | 45.00 | cop[tikv] | | group by:test.t2.t1_id, |
    8. | └─TableReader_14(Probe) | 1.00 | root | | data:TableRangeScan_13 |
    9. | └─TableRangeScan_13 | 1.00 | cop[tikv] | table:t1 | range: decided by [test.t2.t1_id], keep order:true |
    10. +--------------------------------+----------+-----------+------------------------------+---------------------------------------------------------------------------------+
    11. 7 rows in set (0.00 sec)

    由上述查询结果可知,TiDB 首先执行 Index Join 索引连接(即 Merge Join 合并连接的变体)操作,开始读取 t2.t1_id 列的索引。先是 └─HashAgg_31 算子的部分任务在 TiKV 中对 t1_id 值进行去重,然后├─HashAgg_38(Build) 算子的部分任务在 TiDB 中对 值再次进行去重。去重操作由聚合函数 firstrow(test.t2.t1_id) 执行,之后会将操作结果与 t1 表的主键相连接。

    1. +-----------------------------+---------+-----------+------------------------------+---------------------------------------------------------------------------------+
    2. | id | estRows | task | access object | operator info |
    3. +-----------------------------+---------+-----------+------------------------------+---------------------------------------------------------------------------------+
    4. | IndexMergeJoin_20 | 999.00 | root | | inner join, inner:TableReader_15, outer key:test.t3.t1_id, inner key:test.t1.id |
    5. | ├─IndexReader_39(Build) | 999.00 | root | | index:IndexFullScan_38 |
    6. | └─IndexFullScan_38 | 999.00 | cop[tikv] | table:t3, index:t1_id(t1_id) | keep order:false |
    7. | └─TableReader_15(Probe) | 1.00 | root | | data:TableRangeScan_14 |
    8. | └─TableRangeScan_14 | 1.00 | cop[tikv] | table:t1 | range: decided by [test.t3.t1_id], keep order:true |
    9. 5 rows in set (0.00 sec)

    从语义上看,因为约束保证了 t3.t1_id 列值的唯一性,TiDB 可以直接执行 INNER JOIN 查询。

    在前两个示例中,通过 HashAgg 聚合操作或通过 UNIQUE 约束保证子查询数据的唯一性之后,TiDB 才能够执行 Inner Join 操作。这两种连接均使用了 Index JoinMerge Join 的变体)。

    下面的例子中,TiDB 优化器则选择了一种不同的执行计划:

    1. EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2 WHERE t1_id != t1.int_col);

    可以将原语句视为关联子查询,因为它引入了子查询外的 t1.int_col 列。然而,EXPLAIN 语句的返回结果显示的是关联子查询去关联后的执行计划。条件 t1_id != t1.int_col 会被重写为 t1.id != t1.int_col。TiDB 可以从表 t1 中读取数据并且在 └─Selection_21 中执行此操作,因此这种去关联和重写操作会极大提高执行效率。

    在以下示例中,除非子查询中存在 t3.t1_id,否则该查询将(从语义上)返回表 t3 中的所有行:

    1. EXPLAIN SELECT * FROM t3 WHERE t1_id NOT IN (SELECT id FROM t1 WHERE int_col < 100);
    1. +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------+
    2. | id | estRows | task | access object | operator info |
    3. +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------+
    4. | IndexMergeJoin_20 | 1598.40 | root | | anti semi join, inner:TableReader_15, outer key:test.t3.t1_id, inner key:test.t1.id |
    5. | ├─TableReader_28(Build) | 1998.00 | root | | data:TableFullScan_27 |
    6. | └─TableFullScan_27 | 1998.00 | cop[tikv] | table:t3 | keep order:false |
    7. | └─TableReader_15(Probe) | 1.00 | root | | data:Selection_14 |
    8. | └─Selection_14 | 1.00 | cop[tikv] | | lt(test.t1.int_col, 100) |
    9. | └─TableRangeScan_13 | 1.00 | cop[tikv] | table:t1 | range: decided by [test.t3.t1_id], keep order:true |
    10. 6 rows in set (0.00 sec)

    上述查询首先读取了表 t3,然后根据主键开始探测 (probe) 表 t1。连接类型是 anti semi join,即反半连接:之所以使用 anti,是因为上述示例有不存在匹配值(即 NOT IN)的情况;使用 则是因为仅需要匹配第一行后就可以停止查询。