局部索引

局部索引又名分区索引,创建索引的分区关键字是 LOCAL,分区键等同于表的分区键,分区数等同于表的分区数,总之,局部索引的分区机制和表的分区机制一样,如下例所示:

  1. OceanBase(ADMIN@TEST)>create table t1(a int primary key, b int) partition by hash(a) partitions 5;
  2. Query OK, 0 rows affected (0.43 sec)
  3. OceanBase(ADMIN@TEST)>create index idx on t1(b) local;
  4. Query OK, 0 rows affected (0.80 sec)

局部索引是针对单个分区上的数据创建的索引,因此局部索引的索引键值跟表中的数据是一一对应的关系,即局部索引上的一个分区一定对应到一个表分区,它们具有相同的分区规则,因此对于局部唯一索引而言,它只能保证分区内部的唯一性,而无法保证表数据的全局唯一性,如果要使用局部唯一索引去对数据唯一性做约束,那么局部唯一索引中必须包含表分区键,如下例所示:

  1. OceanBase(ADMIN@TEST)>create table t2(a int primary key, b int) partition by hash(a) partitions 5;
  2. Query OK, 0 rows affected (0.24 sec)
  3. OceanBase(ADMIN@TEST)>create unique index uk on t2(b) local;
  4. ERROR-00600: internal error code, arguments: -5261, A UNIQUE INDEX must include all columns in the table's partitioning function
  5. OceanBase(ADMIN@TEST)>create unique index uk2 on t2(b, a) local;
  6. Query OK, 0 rows affected (2.63 sec)

在 OceanBase 数据库中,局部索引同样支持分区裁剪,使用分区裁剪,但前提是查询条件中能够指定分区键,可以减少在查询过程中读取的分区个数,从而能够提高查询检索的效率,如下例所示:

  1. explain select /*+index(t1 idx)*/ b from t1 where b=1 and a=1;
  2. | =====================================
  3. |ID|OPERATOR |NAME |EST. ROWS|COST|
  4. -------------------------------------
  5. |0 |TABLE GET|T1(IDX)|1 |52 |
  6. =====================================
  7. Outputs & filters:
  8. -------------------------------------
  9. 0 - output([T1.B]), filter(nil),
  10. access([T1.B]), partitions(p1)
  11. |

如果在查询中,没有指定分区键,那么局部索引将无法进行分区裁剪,这时会扫描所有分区,增加额外的扫描代价,如下例所示:

  1. explain select /*+index(t1 idx)*/ b from t1 where b=1;
  2. | ====================================================
  3. |ID|OPERATOR |NAME |EST. ROWS|COST|
  4. ----------------------------------------------------
  5. |0 |EXCHANGE IN DISTR | |4950 |5308|
  6. |1 | EXCHANGE OUT DISTR |:EX10000|4950 |3083|
  7. |2 | PX PARTITION ITERATOR| |4950 |3083|
  8. |3 | TABLE SCAN |T1(IDX) |4950 |3083|
  9. ====================================================
  10. Outputs & filters:
  11. -------------------------------------
  12. 0 - output([T1.B]), filter(nil)
  13. 1 - output([T1.B]), filter(nil), dop=1
  14. 2 - output([T1.B]), filter(nil)
  15. 3 - output([T1.B]), filter(nil),
  16. access([T1.B]), partitions(p[0-4])
  17. |

因此,合理的指定建表的分区键,使每个局部索引的查询条件都能够覆盖住分区键能够很大程度上的提升索引检索效率。

全局索引

全局索引的创建规则是在索引属性中指定 GLOBAL 关键字,与局部索引相比,全局索引最大的特点是全局索引的分区规则跟表分区是相互独立的,全局索引允许指定自己的分区规则和分区个数,不一定需要跟表分区规则保持一致,如下例所示:

  1. OceanBase(ADMIN@TEST)>create table t1(a int primary key, b int, c int) partition by hash(a) partitions 5;
  2. Query OK, 0 rows affected (0.21 sec)
  3. OceanBase(ADMIN@TEST)>create index gkey on t1(b) global partition by range(b) (
  4. partition p0 values less than (1),
  5. partition p1 values less than (2),
  6. partition p2 values less than (3)
  7. );
  8. Query OK, 0 rows affected (14.78 sec)

全局索引的分区键一定是索引键本身,因此在使用全局索引的过程中就会指定索引分区键的查询条件,可以针对索引的分区规则进行分区裁剪,在查询到索引键值后可以利用索引表中存储的主键信息计算出主表的分区位置,进而对主表也能进行快速的分区定位,避免扫描主表的所有分区,因此对于无法指定主表分区键的查询而言,全局索引在一定条件下能够加速查询的检索效率,如下例所示:

  1. explain select /*+index(t1 gkey)*/ * from t1 where b=1;
  2. | ==========================================
  3. |ID|OPERATOR |NAME |EST. ROWS|COST |
  4. ------------------------------------------
  5. |0 |TABLE LOOKUP|T1 |4950 |38645|
  6. |1 | TABLE SCAN |T1(GKEY)|4950 |1115 |
  7. ==========================================
  8. Outputs & filters:
  9. -------------------------------------
  10. 0 - output([T1.A], [T1.B], [T1.C]), filter(nil),
  11. partitions(p[0-4])
  12. 1 - output([T1.A]), filter(nil),
  13. access([T1.A]), partitions(p1)
  14. |

对于该查询,首先通过 where 条件中的 b=1 裁剪出全局索引的分区 p1,然后对全局索引进行 table scan 操作,得到对应的主键,利用 table lookup 算子对主表进行精确的分区扫描,避免扫描主表的所有分区。

注意事项

在 OceanBase 数据库中,创建索引的 GLOBAL/LOCAL 关键字可以缺省,如果索引属性关键字没有指定,那么默认的索引属性是GLOBAL 属性,即创建的索引是全局索引,并且索引表只有一个分区。如果主表没有指定分区键或者指定的分区数为1,那么主表也只有一个分区,这个时候,全局索引的数据和主表数据的物理位置是相互绑定在一起的,无论是迁移还是副本 leader 发生切换,它们都是作为一个整体进行变换,不会存在中间状态。但如果全局索引的分区规则和主表的分区规则相同并且分区数相同,我们也不保证索引分区和主表分区的物理位置在集群中是相同的,如果要保证物理位置相同,可以将全局索引和主表指定在一个 table group 中,这个时候它实际上等同于一个局部索引,由于全局索引的维护代价更大,因此对于这种情况,我们不推荐创建一个跟主表分区一一对应的全局索引,而是推荐创建一个局部索引。

特征对比

同局部索引相比,由于全局索引有独立的分区规则,因此索引表中一个分区的索引值可能对应着主表的多个分区内的数据,由于索引的分区规则和主表的分区规则不一定相同,因此在分布式环境中,索引数据和主表数据存储的位置也无法保证始终在一起,不可避免的会引入读写的 RPC 代价和分布式事务的代价。例如在查询中,当主表的分区和全局索引的分区不在同一个物理位置上,TABLE LOOKUP 算子中就包含一次 RPC 操作,到远端机器上去获取主表数据。因此全局索引相比局部索引有更高的维护代价,用户应当充分评估主表的分区规则,合理的选择分区键,尽量使更多的查询条件能够覆盖主表的分区键,从而尽可能的避免使用全局索引。

使用限制

在分布式环境中,全局索引不可避免的会涉及到分布式事务和跨机的查询,因此全局索引依赖 GTS 维护全局的一致性快照,所以全局索引只能在 GTS 开启的时候使用。如果没有开启 GTS,则创建全局索引失败。如下例所示:

  1. obclient> SET GLOBAL ob_timestamp_service=LTS;
  2. Query OK, 0 rows affected (0.06 sec)
  3. obclient> CREATE TABLE t1(a int, b int, PRIMARY KEY(a));
  4. Query OK, 0 rows affected (0.17 sec)
  5. obclient> CREATE INDEX gkey ON t1(b) PARTITION BY range(b)
  6. (PARTITION p0 VALUES LESS THAN (1), PARTITION p1 VALUES LESS THAN (2),
  7. PARTITION p2 VALUES LESS THAN(3));
  8. ERROR 1235 (0A000): create global index when GTS is off not supported

由于 OceanBase 数据库的表是索引组织表(IOT),对于分区表而言,为了保证给定主键的查询能很快定位到表所在的分区,所以分区键必须是主键的子集。如果这个表里面还含有局部分区唯一索引(Local Partitioned Unique Index),那么分区键就必须是所有分区唯一索引列(包括主键列)交集的子集,而对于全局分区唯一索引(Global Partitioned Unique Index)并没有这个限制。如下例所示:

  1. obclient>create table test(pk int,c2 int ,c3 int, primary key(pk)) partition by hash(pk) partitions 5;
  2. Query OK, 0 rows affected (0.20 sec)
  3. obclient>create unique index idx on test(c2) LOCAL;
  4. ERROR-00600: internal error code, arguments: -5261, A UNIQUE INDEX must include all columns in the table's partitioning function
  5. obclient> create unique index idx on test(c2, pk) LOCAL;
  6. Query OK, 0 rows affected (5.34 sec)
  7. obclient> drop index idx;
  8. Query OK, 0 rows affected (0.02 sec)
  9. obclient> create unique index idx on test(c2) GLOBAL;
  10. Query OK, 0 rows affected (17.47 sec)