数据库中的表通常会有一些辅助的索引来加速对这些表的访问。OceanBase 数据库索引和主表的实现方式是一致的,所以可以认为主表也是索引。

索引和主表的唯一区别在于主表一定包含所有的列,而索引通常只包含一部分主表的列。因为索引通常只包含一部分主表的列,所以当一个查询需要访问该索引上不包含的列时,就需要通过索引上的主表主键去主表上找到相关列,这个过程称之为回表。

为了支持回表,OceanBase 数据库主表的主键列会自动地填充到索引列中。比如用户创建了一个 (a,b) 的联合索引,并且该表的主键是 c, 那么该索引的索引键其实是 (a,b,c)。

访问路径是指数据库中访问表的方法,既使用哪个索引来访问表。OceanBase 数据库的一个访问路径主要有三个特征来描述。这些特征都可以在计划展示中显示出来,并且这些特征基本上决定了一个访问路径的代价。

索引扫描范围

OceanBase 数据库将访问路径的扫描开始和结束位置称为 Query range。Query range 是一个 n 维向量,其中 n的个数为索引列的个数,每一个维度对应一个列,该维度的值代表该列的扫描开始和结束位置。在 OceanBase 数据库中,索引在内存中的数据是按照索引列以 B+ 树来组织数据,索引在磁盘中的数据是按照索引列有序存储的,所以当用户给了一些特定的谓词时,可以很快的定位到扫描开始/扫描结束的位置。

比如我们有一个包含两列的联合索引(a,b), 当用户给了条件 a = 1 and b > 1 的时候,我们可以很快定位到索引扫描开始的位置(从第一个满足a = 1 and b > 1的地方开始扫描)。如果用户给的条件是 a < 5, 我们可以快速定位到扫描结束的位置(既最后一个满足a<5的位置)。这种快速定位的特征可以大大减少访问路径的数据扫描量,从而提升性能。

如下例所示,其中 range(1,1,MIN ; 1,5,MAX) 定义了下图中的查询扫描开始和扫描结束的位置,该 range 有三个维度分别对用了三个索引列 (a,b,c), 其中 MIN 表示从最小值,MAX 表示最大值。

  1. OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
  2. Query OK, 0 rows affected (1.94 sec)
  3. OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ * from t1 where b = 1 and c <= 5 and c >= 1;
  4. | =====================================
  5. |ID|OPERATOR |NAME |EST. ROWS|COST|
  6. -------------------------------------
  7. |0 |TABLE SCAN|t1(k1)|1 |88 |
  8. =====================================
  9. Outputs & filters:
  10. -------------------------------------
  11. 0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
  12. access([t1.b], [t1.c], [t1.a], [t1.d]), partitions(p0),
  13. is_index_back=true,
  14. range_key([t1.b], [t1.c], [t1.a]), range(1,1,MIN ; 1,5,MAX),
  15. range_cond([t1.b = 1], [t1.c <= 5], [t1.c >= 1])

覆盖索引

该访问路径是否需要回表。如果一个访问路径中包含了该查询所需要的所有列,那么该路径就不需要回表,反之,该路径就需要回表。

如下例所示,is_index_back 字段表示该路径是否需要回表。

  1. OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
  2. Query OK, 0 rows affected (1.94 sec)
  3. OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ b,c from t1;
  4. | =====================================
  5. |ID|OPERATOR |NAME |EST. ROWS|COST|
  6. -------------------------------------
  7. |0 |TABLE SCAN|t1(k1)|1000 |382 |
  8. =====================================
  9. Outputs & filters:
  10. -------------------------------------
  11. 0 - output([t1.b], [t1.c]), filter(nil),
  12. access([t1.b], [t1.c]), partitions(p0),
  13. is_index_back=false,
  14. range_key([t1.b], [t1.c], [t1.a]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
  15. OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ * from t1;
  16. | ===================================
  17. |ID|OPERATOR |NAME|EST. ROWS|COST|
  18. -----------------------------------
  19. |0 |TABLE SCAN|t1 |1000 |476 |
  20. ===================================
  21. Outputs & filters:
  22. -------------------------------------
  23. 0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
  24. access([t1.a], [t1.b], [t1.c], [t1.d]), partitions(p0),
  25. is_index_back=false,
  26. range_key([t1.a]), range(MIN ; MAX)always true

回表前谓词与回表后谓词

访问路径的谓词计算方式,既哪些谓词可以在索引上计算,哪些谓词需要回表之后才能计算。如果一个谓词涉及到的列都在索引上,那么很显然地,该谓词可以在索引上直接计算,否则该谓词需要回表之后才能进行计算。理论上来说,能够在索引上计算的谓词应该尽量在访问索引的时候进行计算,因为这样可以减少回表的行数。

对于如下查询例子,该查询有两个谓词 c = 1 and d < 1, filter_before_indexback 这个数组分别对应了该两个谓词的计算形式,true 代表该谓词可以在索引上直接计算,false 代表该谓词需要在回表之后计算。因为k1这个索引包含了 a,b,c 三列,所以很显然谓词 c = 1 可以在索引上直接计算,而谓词 d < 1 需要回表之后才能计算。

  1. OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
  2. Query OK, 0 rows affected (1.94 sec)
  3. OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ * from t1 where c = 1 and d < 1;
  4. | =====================================
  5. |ID|OPERATOR |NAME |EST. ROWS|COST|
  6. -------------------------------------
  7. |0 |TABLE SCAN|t1(k1)|1 |563 |
  8. =====================================
  9. Outputs & filters:
  10. -------------------------------------
  11. 0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter([t1.c = 1], [t1.d < 1]),
  12. access([t1.c], [t1.d], [t1.a], [t1.b]), partitions(p0),
  13. is_index_back=true, filter_before_indexback[true,false],
  14. range_key([t1.b], [t1.c], [t1.a]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true

访问路径选择是指数据库中选择访问路径的方法。OceanBase 数据库的路径选择方法融合了基于规则的路径选择方法和基于代价的路径选择方法,OceanBase 数据库首先会使用基于规则的路径选择方法,如果基于规则的路径选择方法之后只有一个可选择的路径, 那么就直接使用该路径,否则就再使用基于代价的路径选择方法选择一个代价最小的路径。

指定访问路径

在 OceanBase 数据库中,用户也可以通过 hint 来指定访问路径。访问路径的 hint 形式如下:/``*+index(table_name index_name)*``/, 其中 table_name 表示表的名字,index_name 表示索引的名字, 如果 index_name 是 primary 的话,就代表选择主表扫描路径。

如下为用 hint 来指定访问路径的示例:

  1. OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
  2. Query OK, 0 rows affected (1.94 sec)
  3. OceanBase (root@test)> explain select/*+index(t1 primary)*/ * from t1;
  4. | ===================================
  5. |ID|OPERATOR |NAME|EST. ROWS|COST|
  6. -----------------------------------
  7. |0 |TABLE SCAN|t1 |1000 |476 |
  8. ===================================
  9. Outputs & filters:
  10. -------------------------------------
  11. 0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
  12. access([t1.a], [t1.b], [t1.c], [t1.d]), partitions(p0)
  13. OceanBase (root@test)> explain select/*+index(t1 k1)*/ * from t1;
  14. | =====================================
  15. |ID|OPERATOR |NAME |EST. ROWS|COST|
  16. -------------------------------------
  17. |0 |TABLE SCAN|t1(k1)|1000 |5656|
  18. =====================================
  19. Outputs & filters:
  20. -------------------------------------
  21. 0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
  22. access([t1.a], [t1.b], [t1.c], [t1.d]), partitions(p0)