#Join调优介绍

# Join Orders介绍

Join是SQL语法中最常见的一个特性,只要稍微复杂一点的业务场景都会用到Join。在入门知识中,已经大体介绍了计划是各个算子排列组合成的。对于Join来说,常常会有非常多的组合方式。这无疑使得选出一个最优的Join组合变得异常困难。

假设有t1、t2、t3、t4四张表,对它们执行如下join查询:

EXPLAIN SELECT * FROM t1, t2, t3, t4  WHERE t1.c1 = t2.c1 AND t2.c1 = t3.c1 AND t4.c1 = t2.c1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 745528657
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  HASH JOIN INNER               |                      |            |         1|     2137( 0)|                                |
|  2 |   JOIN FILTER USE              |                      |            |    100000|      442( 0)|                                |
|* 3 |    TABLE ACCESS FULL           | T4                   | REGRESS    |    100000|      442( 0)|                                |
|* 4 |   JOIN FILTER CREATE           |                      |            |         1|     1499( 0)|                                |
|* 5 |    MERGE JOIN INNER            |                      |            |         1|     1499( 0)|                                |
|  6 |     MERGE SORT                 |                      |            |          |             |                                |
|* 7 |      HASH JOIN INNER           |                      |            |         3|     1068( 0)|                                |
|  8 |       TABLE ACCESS FULL        | T1                   | REGRESS    |    100000|      442( 0)|                                |
|  9 |       TABLE ACCESS FULL        | T2                   | REGRESS    |         3|      431( 0)|                                |
| 10 |     MERGE SORT                 |                      |            |          |             |                                |
| 11 |      TABLE ACCESS FULL         | T3                   | REGRESS    |         3|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access("T4"."C1" = "T2"."C1")
   3 - Predicate : RUNTIME FILTER(RUNTIME USE(0): "T4"."C1")
   4 - Predicate : RUNTIME FILTER(RUNTIME CREATE(0): "T2"."C1")
   5 - Predicate : access( "T1"."C1" = "T3"."C1" )
   7 - Predicate : access("T1"."C1" = "T2"."C1")

对于上述SQL语句来说,即使仅4层Join,在考虑Join算子种类、Join内Scan表的左右顺序的和Scan算子的种类的情况后,算子组合已高达几十种。

实际上,在数据库领域中的Join Orders本质就是一个NP hard难题。因此对于调优复杂Join来说,需要一些特定的技巧来规避如此大的解空间。

但在实际讲解如何进行Join调优之前,有必要对Join Orders相应的算法进行了解。这能有助于理解Join的生成方式,从而做到知其然,也知其所以然。

目前YashanDB中主要有两种Join算法:

  • 动态规划(DP,Dynamic Programming):小于等于8张表连接时,YashanDB会采用DP算法确定Join Orders。

    DP算法本质上是一种穷举法,基本思路为n张表join时,可以给定一个数字k,1 < k < nbest(1..n) = best(1..k) + best(k..n) + cost(join(1..k), join(k..n)),通过循环或递归实现最佳join order的生成。

    因此在DP算法和正确的cost model下,一定能够找最优的Join计划。但随着表的数量增加(超过8张表以上),DP算法的复杂度无法支撑高效的Join Orders生成。

  • 贪心算法(GOO,Greedy Operator Ordering):超过8张表连接时,YashanDB会采用GOO算法确定Join Orders。

    GOO算法对于n个节点所组成的集合,会先通过两两组合找到join cost最小的两个节点,然后将两者join作为新的节点(代替原来的两个节点);重复此过程直至得到一个Join Tree。由此可得,GOO算法的时间复杂度是O(n^3),空间复杂度为O(n)。

# 复杂Join的调优

复杂Join的调优需要遵循一个大体原则:能够低成本过滤掉大量数据的Join优先执行。

EXPLAIN SELECT * FROM t1, t2, t3, t4  WHERE t1.c1 = t2.c1 AND t2.c1 = t3.c1 AND t4.c1 = t2.c1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 3320065567
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  MERGE JOIN INNER              |                      |            |         4|     1724( 0)|                                |
|  2 |   MERGE SORT                   |                      |            |          |             |                                |
|* 3 |    MERGE JOIN INNER            |                      |            |         1|     1293( 0)|                                |
|  4 |     MERGE SORT                 |                      |            |          |             |                                |
|* 5 |      HASH JOIN INNER           |                      |            |         1|      862( 0)|                                |
|  6 |       TABLE ACCESS FULL        | T1                   | REGRESS    |        79|      431( 0)|                                |
|  7 |       TABLE ACCESS FULL        | T2                   | REGRESS    |         3|      431( 0)|                                |
|  8 |     MERGE SORT                 |                      |            |          |             |                                |
|  9 |      TABLE ACCESS FULL         | T3                   | REGRESS    |         3|      431( 0)|                                |
| 10 |   MERGE SORT                   |                      |            |          |             |                                |
| 11 |    TABLE ACCESS FULL           | T4                   | REGRESS    |        13|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access( "T2"."C1" = "T4"."C1" )
   3 - Predicate : access( "T1"."C1" = "T3"."C1" )
   5 - Predicate : access("T1"."C1" = "T2"."C1")

对于T1、T2这种能够提前过滤掉大部分数据的Join,在复杂Join调优时应让其提前。若先执行了这类过滤性强的Join,后续需要执行的条数将大幅降低,从而可以降低整体SQL的执行成本。

正常来说,如果连接表数量低于8,SQL引擎本身的DP算法已经足够优秀去选出较好的Join Orders。而在连接表数量超过8时,由于GOO算法本身的不足,Join Orders可能会出现不够优秀的问题,导致SQL执行效率不理想,此时需要人工介入对这种复杂Join进行调优。

对于复杂Join的SQL来说,穷举所有Join Orders的情况进行调优是比较困难的。因此需要基于某些规则进行调优的尝试。大体的规则如下:

  • 选择率判断:判断各个算子的Rows评估是否准确。

  • 算子选择判断:判断各个算子的选择是否合理。

# 选择率判断

如果算子上存在filter,会对算子生成的rows进行一层过滤,过滤多少的预估由选择率实现。如果选择率的估计不够准确,会导致rows评估偏差较大,进而导致cost model产生误判(选出的Join Orders可能不是最优解)。

因此,在实际调优Join Orders中,第一步通常是查看选择率的判断是否准确。

EXPLAIN SELECT * FROM t1, t2, t3  WHERE t1.c1 = t2.c1 SELECTIVITY 0.0001 AND t2.c1 = t3.c1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 234816895
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  MERGE JOIN INNER              |                      |            |         1|     1293( 0)|                                |
|  2 |   MERGE SORT                   |                      |            |          |             |                                |
|* 3 |    HASH JOIN INNER             |                      |            |         1|      862( 0)|                                |
|  4 |     TABLE ACCESS FULL          | T1                   | REGRESS    |        79|      431( 0)|                                |
|  5 |     TABLE ACCESS FULL          | T2                   | REGRESS    |         3|      431( 0)|                                |
|  6 |   MERGE SORT                   |                      |            |          |             |                                |
|  7 |    TABLE ACCESS FULL           | T3                   | REGRESS    |         3|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access( "T1"."C1" = "T3"."C1" )
   3 - Predicate : access("T1"."C1" = "T2"."C1")

SELECT COUNT(*) FROM t1, t2 WHERE t1.c1 = t2.c1 SELECTIVITY 0.0001;

COUNT(*)
---------------------
                   79

在上述例子中,由于错误地估计了Join后的剩余条数,使得T1和T2先进行了Join,导致Join Orders的判断错误,最终计划不佳。在实际调优时,YashanDB提供了两个能力判断Rows的评估是否准确。

# explain

如上述例子所示,通过比较实际select出来的值与explain出来的值判断rows评估是否准确,具体使用请查阅EXPLAIN

# autotrace

直接开启autotrace可以替代上述比较过程,具体使用请查阅SET AUTOTRACE

SELECT * FROM t1, t2, t3  WHERE t1.c1 = t2.c1 SELECTIVITY 0.0001 AND t2.c1 = t3.c1;

Execution Plan
----------------------------------------------------------------
SQL hash value: 234816895
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+----------+-------------+----------+----------+----------+----------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | E - Rows | A - Rows | Cost(%CPU)  | A - Time | Loops    | Memory   | Disk     | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+----------+-------------+----------+----------+----------+----------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |        79|             |       354|        79|          |          |                                |
|* 1 |  MERGE JOIN INNER              |                      |            |         1|        79|     1293( 0)|       341|        79|          |          |                                |
|  2 |   MERGE SORT                   |                      |            |          |        79|             |       242|        79|          |          |                                |
|* 3 |    HASH JOIN INNER             |                      |            |         1|        79|      862( 0)|       188|        79|          |          |                                |
|  4 |     TABLE ACCESS FULL          | T1                   | REGRESS    |        79|        79|      431( 0)|        24|        79|          |          |                                |
|  5 |     TABLE ACCESS FULL          | T2                   | REGRESS    |         3|         3|      431( 0)|        53|         3|          |          |                                |
|  6 |   MERGE SORT                   |                      |            |          |        59|             |        42|        59|          |          |                                |
|  7 |    TABLE ACCESS FULL           | T3                   | REGRESS    |         3|         3|      431( 0)|        26|         3|          |          |                                |
+----+--------------------------------+----------------------+------------+----------+----------+-------------+----------+----------+----------+----------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access( "T1"."C1" = "T3"."C1" )
   3 - Execution : [BUILD] Memory : 327680  [BUILD] Time : 133  [HDT] RehashTimes : 0  [HDT] OriginPrime : 131071
       Predicate : access("T1"."C1" = "T2"."C1")

# 算子选择判断

除了选择率判断之外,选择的算子是否准确也至关重要,算子选择不恰当也会引起严重的效率下降。

EXPLAIN SELECT /*+LEADING(t1,t2) USE_HASH(t1,t2)*/ * FROM t1, t2, t3  WHERE t1.c1 = t2.c1 AND t2.c1 = t3.c1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 4196032415
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  HASH JOIN INNER               |                      |            |   1651424|     4545( 0)|                                |
|* 2 |   HASH JOIN INNER              |                      |            |   1651424|      885( 0)|                                |
|  3 |    TABLE ACCESS FULL           | T1                   | REGRESS    |      2528|      431( 0)|                                |
|  4 |    TABLE ACCESS FULL           | T2                   | REGRESS    |      1899|      431( 0)|                                |
|  5 |   TABLE ACCESS FULL            | T3                   | REGRESS    |         3|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access("T1"."C1" = "T3"."C1")
   2 - Predicate : access("T1"."C1" = "T2"."C1")


EXPLAIN SELECT /*+LEADING(t1,t2) USE_NL(t1)*/ * FROM t1, t2, t3  WHERE t1.c1 = t2.c1 AND t2.c1 = t3.c1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 462255180
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  HASH JOIN INNER               |                      |            |   1651424|     4848( 0)|                                |
|* 2 |   NESTED LOOPS INNER           |                      |            |   1651424|     1189( 0)|                                |
|  3 |    TABLE ACCESS FULL           | T1                   | REGRESS    |      2528|      431( 0)|                                |
|  4 |    TABLE ACCESS FULL           | T2                   | REGRESS    |      1899|      431( 0)|                                |
|  5 |   TABLE ACCESS FULL            | T3                   | REGRESS    |         3|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access("T1"."C1" = "T3"."C1")
   2 - Predicate : filter("T1"."C1" = "T2"."C1")

在上述示例中,可以发现选择Nested Loops Inner比选择Hash Join Inner代价更大。Join相关算子均有各自的适用场景,例如,大表之间的Join可能更适合选择Hash Join,如果两个表的Join列都是有序的可能更适合选择Merge Join。

在一些复杂的场景中,很容易出现类似Join相关算子选择错误的问题。例如在大表Join时没有索引的情况下选择了Nest Loop Join而未选择Hash Join,以及选择Hash Join时采用了大表进行Hash表的创建。因此如果要对算子进行判断,需要了解各个算子相关的适用场景。

# hint调优

结合上述的两类问题,主要有以下hint可以对Join Orders进行调节。

  • Join Hint
  • Index Hint
  • Selectivity Hint

# 选择率调整

上述出现选择率问题时,可以通过Selectivity Hint进行调整。

EXPLAIN SELECT * FROM t1, t2, t3  WHERE t1.c1 = t2.c1 SELECTIVITY 0.0001 AND t2.c1 = t3.c1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 234816895
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  HASH JOIN INNER               |                      |            |       480|     1317( 0)|                                |
|* 2 |   HASH JOIN INNER              |                      |            |       480|      885( 0)|                                |
|  3 |    TABLE ACCESS FULL           | T1                   | REGRESS    |      2528|      431( 0)|                                |
|  4 |    TABLE ACCESS FULL           | T2                   | REGRESS    |      1899|      431( 0)|                                |
|  5 |   TABLE ACCESS FULL            | T3                   | REGRESS    |         3|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access("T1"."C1" = "T3"."C1")
   2 - Predicate : access("T1"."C1" = "T2"."C1")


EXPLAIN SELECT * FROM t1, t2, t3  WHERE t1.c1 = t2.c1 AND t2.c1 = t3.c1 SELECTIVITY 1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 2568121636
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|* 1 |  HASH JOIN INNER               |                      |            |   1651424|     1321( 0)|                                |
|* 2 |   HASH JOIN INNER              |                      |            |      2528|      866( 0)|                                |
|  3 |    TABLE ACCESS FULL           | T1                   | REGRESS    |      2528|      431( 0)|                                |
|  4 |    TABLE ACCESS FULL           | T3                   | REGRESS    |         3|      431( 0)|                                |
|  5 |   TABLE ACCESS FULL            | T2                   | REGRESS    |      1899|      431( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Predicate : access("T1"."C1" = "T2"."C1")
   2 - Predicate : access("T1"."C1" = "T3"."C1")

YashanDB通过Selectivity Hint的方式可以立竿见影地完成对Join Orders的调整。但这类的调整由于粒度较小,需要对选择率相关的处理逻辑有一定的了解,选择率相关介绍请查阅选择率与统计信息

# 算子调整

算子调整的粒度更大,直接通过hint的方式将执行的算子进行改变。在进行这类操作之前,需要对各个算子的原理以及优劣了解清楚。

EXPLAIN SELECT /*+INDEX(t1)*/ c1 FROM t1;

PLAN_DESCRIPTION
----------------------------------------------------------------
SQL hash value: 1102139983
Optimizer: ADOPT_C

+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
| Id | Operation type                 | Name                 | Owner      | Rows     | Cost(%CPU)  | Partition info                 |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+
|  0 | SELECT STATEMENT               |                      |            |          |             |                                |
|  1 |  COL TO ROW                    |                      |            |          |             |                                |
|  2 |   INDEX FAST FULL SCAN         | C1_T1                | REGRESS    |         8|      142( 0)|                                |
+----+--------------------------------+----------------------+------------+----------+-------------+--------------------------------+

Operation Information (identified by operation id):
---------------------------------------------------

   1 - Projection: RemoteTable[1][INTEGER]
   2 - Projection: Tuple[0, 0][INTEGER]

上述例子中,只需要取c1列数据不需要回表,可以直接使用hint指定采用index scan,从而手动使得计划最优。对于其他的算子使用不恰当场景也是同理,例如在选错成nested loop join时,可以使用Hint指定使用hash join进行规避。