基于规则的路径选择 
==============================

本文主要介绍 OceanBase 数据库路径选择的规则体系。

目前 OceanBase 数据库路径选择的规则体系分为前置规则（正向规则）和 Skyline 剪枝规则（反向规则）。前置规则直接决定了一个查询使用什么样的索引，是一个强匹配的规则体系。

Skyline 剪枝规则会比较两个索引，如果一个索引在一些定义的维度上优于（Dominate）另外一个索引，那么不优的索引会被剪掉，最后没有被剪掉的索引会进行代价比较，从而选出最优的计划。

目前 OceanBase 数据库的优化器会优先使用前置规则选择索引，如果没有匹配的索引，那么 Skyline 剪枝规则会剪掉一些不优的索引，最后代价模型会在没有被剪掉的索引中选择代价最低的路径。

如下例所示，OceanBase 数据库的计划展示中会输出相应的路径选择的规则信息。

```javascript
obclient>CREATE TABLE t1(a INT PRIMARY KEY, b INT, c INT, d INT, e INT, 
            UNIQUE INDEX k1(b), INDEX k2(b,c), INDEX k3(c,d));
Query OK, 0 rows affected (0.38 sec)

obclient> EXPLAIN EXTENDED SELECT * FROM t1 WHERE b = 1;
+-----------------------------------------------------------------+
| Query Plan                                                                              |
+-----------------------------------------------------------------+
| =====================================
|ID|OPERATOR  |NAME  |EST. ROWS|COST|
-------------------------------------
|0 |TABLE SCAN|t1(k1)|2        |94  |
=====================================

Outputs & filters:
-------------------------------------
  0 - output([t1.a(0x7f3178058bf0)], [t1.b(0x7f3178058860)], [t1.c(0x7f3178058f80)], [t1.d(0x7f3178059310)], [t1.e(0x7f31780596a0)]), filter(nil),
      access([t1.b(0x7f3178058860)], [t1.a(0x7f3178058bf0)], [t1.c(0x7f3178058f80)], [t1.d(0x7f3178059310)], [t1.e(0x7f31780596a0)]), partitions(p0),
      is_index_back=true,
      range_key([t1.b(0x7f3178058860)], [t1.shadow_pk_0(0x7f31780784b8)]), range(1,MIN ; 1,MAX),
      range_cond([t1.b(0x7f3178058860) = 1(0x7f31780581d8)])
Optimization Info:
-------------------------------------
t1:optimization_method=rule_based, heuristic_rule=unique_index_with_indexback


obclient> EXPLAIN EXTENDED SELECT * FROM t1 WHERE c < 5 ORDER BY c;
+-----------------------------------------------------------------+
| Query Plan                                                                              |
+-----------------------------------------------------------------+
| ====================================
|ID|OPERATOR   |NAME|EST. ROWS|COST|
------------------------------------
|0 |SORT       |    |200      |1054|
|1 | TABLE SCAN|t1  |200      |666 |
====================================

Outputs & filters:
-------------------------------------
  0 - output([t1.a(0x7f3178059220)], [t1.b(0x7f31780595b0)], [t1.c(0x7f3178058e90)], [t1.d(0x7f3178059940)], [t1.e(0x7f3178059cd0)]), filter(nil), sort_keys([t1.c(0x7f3178058e90), ASC])
  1 - output([t1.c(0x7f3178058e90)], [t1.a(0x7f3178059220)], [t1.b(0x7f31780595b0)], [t1.d(0x7f3178059940)], [t1.e(0x7f3178059cd0)]), filter([t1.c(0x7f3178058e90) < 5(0x7f3178058808)]),
      access([t1.c(0x7f3178058e90)], [t1.a(0x7f3178059220)], [t1.b(0x7f31780595b0)], [t1.d(0x7f3178059940)], [t1.e(0x7f3178059cd0)]), partitions(p0),
      is_index_back=false, filter_before_indexback[false],
      range_key([t1.a(0x7f3178059220)]), range(MIN ; MAX)always true
t1:optimization_method=cost_based, avaiable_index_name[t1,k3], pruned_index_name[k1,k2]
```



其中 `optimization_method` 展示了具体的规则信息，它有以下两种形式:

* 如果显示 `optimization_method=rule_based`，说明命中了前置规则，同时会展示出具体命中的规则名称，`unique_index_with_indexback` 表示命中了前置规则的第三条规则（唯一性索引全匹配+回表+回表数量少于一定的阈值）。

  

* 如果显示 `optimization_method=cost_based`，说明基于代价选择规则，同时会展示 Skyline 剪枝规则剪掉了哪些访问路径（`pruned_index_name` 字段）以及保留了哪些访问路径（`avaiable_index_name` 字段）。

  




前置规则 
-------------

目前 OceanBase 数据库的前置规则只用于简单的单表扫描。因为前置规则是一个强匹配的规则体系，一旦命中，就直接选择命中的索引，所以要限制它的使用场景，避免选错计划。

目前 OceanBase 数据库根据"查询条件是否能覆盖所有索引键"和"使用该索引是否需要回表"这两个信息，将前置规则按照优先级划分成如下三种匹配类型：

* 匹配"唯一性索引全匹配+不需要回表（主键被当成唯一性索引来处理）"时，选择该索引。如果存在多个这样的索引，选择索引列数最小的一个。

  




<!-- -->

* 匹配"普通索引全匹配+不需要回表"时，选择该索引。如果存在多个这样的索引，选择索引列数最小的一个。

  

* 匹配"唯一性索引全匹配+回表+回表数量少于一定的阈值"，则选择该索引。如果存在多个此匹配类型的索引，选择回表数量最小的一个。

  




这里需要注意的是，索引全匹配是指在索引键上都存在等值条件（对应于 `get` 或者 `multi-get`）。

如下示例中，查询 Q1 命中了索引 `uk1`（唯一性索引全匹配+不需要回表）；查询 Q2 命中了索引 `uk2`（唯一性索引全匹配+回表+回表行数最多 4 行）。

```javascript
obclient>CREATE TABLE  test(a INT PRIMARY KEY, b INT, c INT, d INT, e INT, 
              UNIQUE KEY UK1(b,c), UNIQUE KEY UK2(c,d) );
Query OK, 0 rows affected (0.38 sec)

Q1: 
obclient>SELECT b,c FROM test WHERE (b = 1 OR b = 2) AND (c = 1 OR c =2);

Q2: 
obclient>SELECT * FROM test WHERE (c = 1 OR c =2) OR (d = 1 OR d = 2);
```



Skyline 剪枝规则 
---------------------

Skyline 算子是学术界在 2001 年提出的一个新的数据库算子（它并不是标准的 SQL 算子）。自此之后，学术界对 Skyline 算子进行了大量的研究（包括语法、语义和执行等）。

Skyline 从字面上的理解是指天空中的一些边际点，这些点组成搜索空间中最优解的集合。例如要寻找价格最低并且路途最短的一家旅馆，可以想象成一个二维空间，横轴表示价格，纵轴表示距离，二维空间上的每个点表示一个旅馆。如下图所示，最优解肯定是在这一条天空的边际线上。假设点 A 不在 Skyline 上，那么肯定能够在 Skyline 上找到在两个维度上都比 A 更优的点 B，在这个场景中就是距离更近，价格更便宜的旅馆，称为点 B Dominate A。所以当用户没办法去衡量多个维度的比重，或者多个维度不能综合量化（如果可以综合量化，使用 "SQL 函数+ `ORDER BY` "就可以解决了）时就可以使用 Skyline 算法。

![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1248744061/p167291.png "image")

Skyline 原理是在给定对象集 O 中找出不被别的对象所 Dominate 的对象集合。若一个对象 A 在所有维度都不被另一个对象 B 所 Dominate，并且 A 至少在一个维度上 Dominate B，则称 A Dominate B。所以在 Skyline 中比较重要的是维度的选择以及在每个维度上对 Dominate 关系的定义。假设有 N 个索引的路径 `<idx_1，idx_2，idx_3...idx_n>` 可以供优化器选择，对于查询 Q，索引 `idx_x` 在定义的维度上 Dominate 索引 `idx_y`，那就可以提前把索引 `idx_y` 剪掉，不让它参与最终代价的运算。

维度的定义 
--------------

Skyline 剪枝规则对每个索引（主键也是一种索引）定义了如下三个维度:

* 是否回表

  

* 是否存在 Intersting Order

  

* 索引前缀能否抽取 Query Range

  




通过如下示例进行分析：

```javascript
 obclient>CREATE TABLE skyline(
        pk INT PRIMARY KEY, a INT, b INT, c INT,
        KEY idx_a_b(a, b),
        KEY idx_b_c(b, c),
        KEY idx_c_a(c, a));
Query OK, 0 rows affected (0.09 sec)
```



* 回表：该查询是否需要需要回查主表。

  ```javascript
  /* 如果走索引 idx_a_b 就需要回查主表，因为索引 idx_a_b 没有 c 列*/ 
  obclient>SELECT /*+INDEX(skyline idx_a_b)*/ * FROM skyline;
  ```

  




<!-- -->

* Intersting Order: 考虑是否有合适的序可以利用。

  ```javascript
  /* 索引 idx_b_c 可以把 ORDER BY 语句消除*/ 
  obclient>SELECT pk, b FROM skyline ORDER BY b;
  ```

  




<!-- -->

* 索引前缀能否抽取 Query Range。

  ```javascript
  /*走索引 idx_c_a 可以快速定位到需要的行的范围，不用全表扫描*/
  obclient>SELECT pk, b FROM skyline WHERE c > 100 AND c < 2000;
  ```

  




基于这三个维度，Skyline 定义了索引之间的 Dominate 关系，如果索引 A 在三个维度上都不比索引 B 差，并且其中至少有一个维度比 B 好，可以推断出基于索引 B 最后生成的计划肯定不会比索引 A 好，那么就可以直接把索引 B 剪掉。判断依据如下：

* 如果索引 `idx_A` 不需要回表，而索引 `idx_B` 需要回表，那么在这个维度上索引 `idx_A` Dominate `idx_B`。

  

* 如果在索引 `idx_A`上抽取出来的 Intersting Order 是向量 `Va<a1, a2, a3 ...an>`， 在索引 `idx_B` 上抽出来的Intersting Order 是向量 `Vb<b1, b2, b3...bm>`，并且 `n > m` , 对于 `ai = bi (i=1..m)`, 那么在这个维度上索引 `idx_A` Dominate `idx_B`。

  

* 如果在索引 idx_A 能用来抽取的 Query Range 的列集合是 `Sa<a1, a2, a3 ...an>`，在索引 `idx_B` 上能用来抽取 Query Range 的列集合是 `Sb <b1, b2, b3...bm>`, 如果 `Sa` 是 `Sb` 的 Super Set, 那么在这个维度上索引 `idx_A` Dominate `idx_B`。

  




#### **回表** 

回表就是查询所需列是否在索引中。需要特殊考虑某些场景，例如当主表和索引表都没有 Interesting Order 并且抽取不了 Query Range 时，直接走主表不一定是最优解。

```javascript
obclient>CREATE TABLE t1(
         pk INT PRIMARY KEY, a INT, b INT, c INT, v1 VARCHAR(1000), 
         v2 VARCHAR(1000), v3 VARCHAR(1000), v4 VARCHAR(1000),INDEX idx_a_b(a, b));
Query OK, 0 rows affected (0.09 sec)

obclient>SELECT a, b,c FROM t1 WHERE b = 100;
```




| **索引**  | **Index Back** | **Interesting Order** | **Query Range** |
|---------|----------------|-----------------------|-----------------|
| primary | no             | no                    | no              |
| idx_a_b | yes            | no                    | no              |



由于主表很宽，索引表很窄，从维度上分析主表 Dominate 索引 `idx_a_b`，然而，索引扫描加回表不一定会比主表全表扫描的代价高。简单来说，索引表可能只需要读一个宏块，而主表可能需要十个宏块。这种情况下，需要放宽规则，综合考虑具体的过滤条件。

#### **Interesting Order** 

优化器通过 Interesting Order 利用底层的序，就不需要对底层扫描的行做排序，还可以消除 `ORDER BY`，进行 `MERGE GROUP BY`，提高 Pipeline（不需要进行物化）等。

```javascript
obclient>CREATE TABLE skyline(
        pk INT PRIMARY KEY, v1 INT, v2 INT, v3 INT, v4 INT, v5 INT,
        KEY idx_v1_v3_v5(v1, v3, v5),
        KEY idx_v3_v4(v3, v4));
Query OK, 0 rows affected (0.10 sec)

obclient>CREATE TABLE tmp (c1 INT PRIMARY KEY, c2 INT, c3 INT);
Query OK, 0 rows affected (0.06 sec)

obclient>(SELECT DISTINCT v1, v3 FROM skyline JOIN tmp WHERE skyline.v1 = tmp.c1 
          ORDER BY v1, v3) UNION (SELECT c1, c2 FROM tmp);
```





![image](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1248744061/p167292.png "image")

从执行计划可以看到，`ORDER BY` 被消除了，同时使用了 `MERGE DISTINCT`，`UNION` 也没有做 `SORT`。可以看到，从底层 `TABLE SCAN` 吐出来的序，可以被上层的算子使用。换句话说，保留 `idx_v1_v3_v5` 吐出来的行的顺序，可以让后面的算子在保序的情况下执行更优的操作。优化器在识别这些序的情况下，才能生成更优的执行计划。

所以 Skyline 剪枝对 Interesting Order 的判断，需要充分考虑各个索引能够最大利用的序。例如上述示例中最大的序其实是 `(v1, v3)` 而不仅仅是 `v1`，它从 `MERGE JOIN` 吐出来的序 `(v1, v3)` 可以到 `MERGE DISINCT` 算子, 再到最后的 `UNISON DISTINCT` 算子。

#### **Query Range** 

Query Range 的抽取可以方便底层直接根据抽取出来的 Range 定位到具体的宏块，从而减少存储层的 IO。

例如 `SELECT * FROM t1 WHERE pk < 100 AND pk > 0` 就可以直接根据一级索引的信息定位到具体的宏块，加速查询，越精确的 Query Range 能够让数据库扫描更少的行。

```javascript
obclient> CREATE TABLE t1 (
       pk INT PRIMARY KEY, a INT, b INT,c INT,
        KEY idx_b_c(b, c),
        KEY idx_a_b(a, b));
Query OK, 0 rows affected (0.12 sec)

obclient>SELECT b FROM t1 WHERE a = 100 AND b > 2000;
```



索引 `idx_b_c` 能抽出 Query Range 的索引前缀是 (b)，索引 `idx_a_b` 能抽出 Query Range 的索引前缀是 (a, b)，所以在这个维度上，索引 `idx_a_b` Dominate `idx_b_c`。

综合示例 
-------------

```javascript
obclient>CREATE TABLE skyline(
        pk INT PRIMARY KEY, v1 INT, v2 INT, v3 INT, v4 INT, v5 INT,
        KEY idx_v1_v3_v5(v1, v3, v5),
        KEY idx_v3_v4(v3, v4));
Query OK, 0 rows affected (0.10 sec)

obclient>CREATE TABLE tmp (c1 INT PRIMARY KEY, c2 INT, c3 INT);
Query OK, 0 rows affected (0.06 sec)

obclient>SELECT MAX(v5) FROM skyline WHERE v1 = 100 AND v3 > 200 GROUP BY v1;
```




|    **索引**    | **Index Back** | **Interesting order** | **Query range** |
|--------------|----------------|-----------------------|-----------------|
| primary      | Not need       | No                    | No              |
| idx_v1_v3_v5 | Not need       | (v1)                  | (v1, v3)        |
| idx_v3_v4    | Need           | No                    | (v3)            |



可以看到索引 `idx_v1_v3_v5` 在三个维度上都不比主键索引或索引 `idx_v3_v4` 差。所以根据 Skyline 剪枝规则 ，会直接剪掉主键索引和索引 `idx_v3_v4`。维度的合理定义，决定了 Skyline 剪枝是否合理。错误的维度，将会导致该索引提前被剪掉，可能永远生成不了最优的计划。
