数据库中的表通常会有一些辅助的索引来加速对这些表的访问。在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表示最大值。
OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
Query OK, 0 rows affected (1.94 sec)
OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ * from t1 where b = 1 and c <= 5 and c >= 1;
| =====================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
-------------------------------------
|0 |TABLE SCAN|t1(k1)|1 |88 |
=====================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
access([t1.b], [t1.c], [t1.a], [t1.d]), partitions(p0),
is_index_back=true,
range_key([t1.b], [t1.c], [t1.a]), range(1,1,MIN ; 1,5,MAX),
range_cond([t1.b = 1], [t1.c <= 5], [t1.c >= 1])
覆盖索引
该访问路径是否需要回表。如果一个访问路径中包含了该查询所需要的所有列,那么该路径就不需要回表,反之,该路径就需要回表。如下图所示,is_index_back字段表示该路径是否需要回表。
OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
Query OK, 0 rows affected (1.94 sec)
OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ b,c from t1;
| =====================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
-------------------------------------
|0 |TABLE SCAN|t1(k1)|1000 |382 |
=====================================
Outputs & filters:
-------------------------------------
0 - output([t1.b], [t1.c]), filter(nil),
access([t1.b], [t1.c]), partitions(p0),
is_index_back=false,
range_key([t1.b], [t1.c], [t1.a]), range(MIN,MIN,MIN ; MAX,MAX,MAX)always true
OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ * from t1;
| ===================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
-----------------------------------
|0 |TABLE SCAN|t1 |1000 |476 |
===================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
access([t1.a], [t1.b], [t1.c], [t1.d]), partitions(p0),
is_index_back=false,
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 需要回表之后才能计算。
OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
Query OK, 0 rows affected (1.94 sec)
OceanBase (root@test)> explain extended_noaddr select/*+index(t1 k1)*/ * from t1 where c = 1 and d < 1;
| =====================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
-------------------------------------
|0 |TABLE SCAN|t1(k1)|1 |563 |
=====================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter([t1.c = 1], [t1.d < 1]),
access([t1.c], [t1.d], [t1.a], [t1.b]), partitions(p0),
is_index_back=true, filter_before_indexback[true,false],
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可以参考链接。下图展示了一些用hint来指定访问路径的示例:
OceanBase (root@test)> create table t1(a int primary key, b int, c int, d int, index k1(b,c));
Query OK, 0 rows affected (1.94 sec)
OceanBase (root@test)> explain select/*+index(t1 primary)*/ * from t1;
| ===================================
|ID|OPERATOR |NAME|EST. ROWS|COST|
-----------------------------------
|0 |TABLE SCAN|t1 |1000 |476 |
===================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
access([t1.a], [t1.b], [t1.c], [t1.d]), partitions(p0)
OceanBase (root@test)> explain select/*+index(t1 k1)*/ * from t1;
| =====================================
|ID|OPERATOR |NAME |EST. ROWS|COST|
-------------------------------------
|0 |TABLE SCAN|t1(k1)|1000 |5656|
=====================================
Outputs & filters:
-------------------------------------
0 - output([t1.a], [t1.b], [t1.c], [t1.d]), filter(nil),
access([t1.a], [t1.b], [t1.c], [t1.d]), partitions(p0)