SQL 优化实践:关于增强 OceanBase本地索引性能的案例

本文通过一个案例,帮用户了解在Oracle迁移Oceanbase时,应该如何选择全局索引和本地索引以带来更好的SQL性能。

作者:胡呈清,爱可生的DBA团队成员,擅长故障分析和性能优化。本文约 5000 字,预计阅读需要 15 分钟。
数据库版本:OceanBase V3.2.3.3

案例的问题描述

在进行 Oracle 到 OceanBase的迁移过程中,有张表无法关闭 row movement 功能,导致无法使用 OMS直接迁移数据。为了解决这个问题,在割接窗口期之前,采用 dbcat 工具单独迁移该表结构,在窗口期内,通过数据导入的方式特别处理了这张表。

这是一张分区表,在 Oracle 数据库中,其主键约束并未包含分区键。然而OceanBase数据库要求主键必须包含分区键。因此,在进行Oracle数据迁移至 OceanBase 的过程中,我们有两种不同的处理方式:

  1. OMS 工具:迁移时会将主键转成 全局唯一索引 +NOT NULL 约束,等价 Oracle 的主键约束。表没有显示主键,但会有一个隐式主键(分区键+隐藏自增列);
  2. dbcat 工具:迁移时会把分区键加入到主键中,这是个本地索引。

这里最主要的区别是:Oracle 上的主键是全局索引,用 dbcat 迁移到 OB 时会变成本地索引,用 OMS 迁移则还是全局索引。然后以下 SQL 做 nested-loop join 时关联字段是主键字段,每次到被驱动表上使用主键查找,需要对所有分区执行,因此慢了。

注:OMS、dbcat 都是迁移工具,不用深究,只需理解为什么会有这种区别即可,下面会做解释。
select
  *
from
  (
    SELECT
      a.act_Id as actId,
      a.data_Id as dataId,
      ...
    from
      T1 a,
      T2 b
    where
      a.data_Id = b.data_Id
      and a.cmp_Status not in ('08')
      and a.crt_Dttm >= to_date('2023-09-15 04:37:49', 'YYYY-mm-dd hh24:mi:ss')
      and a.crt_Dttm < to_date('2023-10-14 04:37:49', 'YYYY-mm-dd hh24:mi:ss')
      ...
    order by
      a.reserve_Begin_Dttm asc,
      a.act_Limit_Date asc,
      a.act_Id asc
  )
where
  rownum <= 10

关于全局索引和本地索引

OB 的官方文档上有非常详细的说明:局部索引和全局索引。因此本文只做些脉络上的补充。

1. 什么是全局索引、什么是本地索引?

首先只有分区表才有全局索引、本地索引的区分。先以 MySQL InnoDB 为例,分区表的每个分区实际上都有独立的表空间,完全可以把分区看成独立的表,因此对于一个索引来说,它也只能是每个分区维护各自的索引结构,这个就是本地索引,并且 InnoDB 只有本地索引,没有全局索引。

相反,一张表的所有分区如果只维护一个索引结构,这个就是全局索引。典型的 Oracle 支持全局索引,并且默认创建的都是全局索引。

2. 以 MySQL DBA 的视角来说,为什么要有全局索引?

从索引查找的效率上对比,分两种情况:

  • 如果 SQL 带分区键查询,分区裁剪后只需要查找少量几个分区,则只需要对这几个分区上的所有进行查找即可,可以降低系统资源的使用,效率更高;
  • 如果 SQL 不带分区键查询,没做分区裁剪,则本地索引需要对所有分区上的索引进行查找;同理,如果进行分区裁剪后还要查找多个分区也一样,会使用更多的系统资源,效率更慢。全局索引则只需要对一个大的索引进行查找,显然更节省成本。

3. Oracle 与 OB 主键的区别

Oracle 的主键约束 = 唯一索引+NOT NULL 约束;

OB 的数据结构上不同于 Oracle,Oracle 是堆表,索引上存的是数据行的指针,索引和数据是分开的。而 OB 是索引组织表,数据都在主键索引上,其他二级索引上存的是主键值。

因此对于分区表来说,OB 上每个分区的数据就是主键,主键必须是本地索引。然后由于主键有唯一约束,得保证全局唯一,而本地索引只能保证分区内唯一,怎么实现?不同的分区,分区键值一定是不一样的,所以可以通过分区键的唯一来保证主键的全局唯一,这就是为什么 OB 上的分区表要求主键必须包含分区键。

同理 Oracle 为什么不要求主键必须包含分区键?因为 Oracle 的主键约束默认创建的是全局唯一索引,它本身就能保证全局唯一,不需要携带分区键实现。Oracle 如果要创建本地唯一索引,也是要求包含分区键的。

4. OB 上全局索引带来的挑战

OB 是一个分布式数据库,全局索引和分区数据的分布位置肯定是不一样的,因此如果查找全局索引后要回表,很容易产生分布式事务,如果要回表的数据量很大,需要多次 rpc 交互,查询效率会下降很明显。

通常 OB 上适合使用全局索引的场景是:

  1. 基数很大的索引(即效率很高),高频的点查,并且 WHERE 条件中没有分区键,无法进行分区裁剪;
  2. 非分布式架构。

分析过程

介绍完本地索引和全局索引,下面回到慢 SQL 的分析上。

1. 测试复现

迁移到 OB 上被驱动表 b 的相关索引是:PRIMARY KEY(“DATA_ID”, “POLICY_VALID_DATE”),Oracle 上对应的索引是:GLOBAL UNIQUE(“DATA_ID”)。

为了方便测试,在 OB 上再新建一张表,将两个索引都建上:PRIMARY KEY(“DATA_ID”, “POLICY_VALID_DATE”),CONSTRAINT “UIDX_DATA_ID2” UNIQUE (“DATA_ID”)。

复现情况如下:

  • 被驱动表默认走主键,进行 nested-loop join,耗时 90 秒
  • 加 hint /*+ index(b UIDX_DATA_ID2) */ 执行,被驱动表强制走全局唯一索引,进行 nested-loop join,耗时只需要 5 秒

注意:这里驱动表输出 8 万行,join 结果也是 8 万行。

执行计划对比,走主键的执行计划:

==================================================================
|ID|OPERATOR                           |NAME    |EST. ROWS|COST  |
------------------------------------------------------------------
|0 |LIMIT                              |        |10       |237614|
|1 | PX COORDINATOR MERGE SORT         |        |10       |237614|
|2 |  EXCHANGE OUT DISTR               |:EX10001|10       |237565|
|3 |   LIMIT                           |        |10       |237565|
|4 |    TOP-N SORT                     |        |10       |237565|
|5 |     NESTED-LOOP JOIN              |        |353      |237420|
|6 |      EXCHANGE IN DISTR            |        |58       |234466|
|7 |       EXCHANGE OUT DISTR (BC2HOST)|:EX10000|58       |234297|
|8 |        PX PARTITION ITERATOR      |        |58       |234297|
|9 |         TABLE SCAN                |A       |58       |234297|
|10|      PX PARTITION ITERATOR        |        |7        |49    |
|11|       TABLE SCAN                  |B       |7        |49    |
==================================================================

Outputs & filters: 
-------------------------------------
  0 - output([A.ACT_ID(0x7e7d01e62040)], [A.DATA_ID(0x7e7d01e575a0)], [A.CMP_STATUS(0x7e7d01e5add0)], [A.CMP_TYPE_CD(0x7e7d01e59d00)], [A.CMP_TYPE_ID(0x7e7d01e62fb0)], [A.COUT_MODE(0x7e7d01e635c0)], [A.ASSOCIATE_ID(0x7e7d01e63bd0)], [A.ORGAN_ID(0x7e7d01e641e0)], [A.CUSTOMER_TYPE(0x7e7d01e647f0)], [A.CUSTOMER_NAME(0x7e7d01e64e00)], [A.RES_CODE(0x7e7d01e65410)], [A.CRT_DTTM(0x7e7d01e55070)], [A.USER_GRP_ID(0x7e7d01e65d40)], [A.USER_ID(0x7e7d01e66350)], [A.ACT_LIMIT_DATE(0x7e7d01e68970)], [A.CALL_CRT_DTTM(0x7e7d01e68f80)], [A.CONTACT_TEL1(0x7e7d01e69590)], [A.CONTACT_TEL2(0x7e7d01e77bb0)], [B.POLICY_PAY_ADDR(0x7e7d01e781c0)], [A.ORGAN3_ID(0x7e7d01e787d0)], [A.FAIL_COUT_NUM(0x7e7d01e78de0)], [A.SYS_TYPE(0x7e7d01e793f0)], [A.RESERVE_BEGIN_DTTM(0x7e7d01e79a00)], [A.RESERVE_END_DTTM(0x7e7d01e7a010)], [B.SALE_CHANNEL(0x7e7d01e7a620)], [B.POLICY_NO(0x7e7d01e7ac30)], [B.FREE_LOOK_PERIOD(0x7e7d01e7b240)], [B.HOLDER_SEX(0x7e7d01e7b850)], [B.ORGAN4_ID(0x7e7d01e7be60)], [A.ASSIGN_DTTM(0x7e7d01e7c470)], [A.HANDLER_AGENT_CODE(0x7e7d01e7ca80)], [B.CANVASSER_CODE(0x7e7d01e7d090)], [B.CANVASSER_NAME(0x7e7d01e7d6a0)], [B.POLICY_OPE_DATE(0x7e7d01e7dcb0)], [A.APP_FLG(0x7e7d01e7e2c0)], [A.INTER_FLG(0x7e7d01e7e8d0)]), filter(nil), limit(?), offset(nil)
  1 - output([A.ACT_ID(0x7e7d01e62040)], [A.DATA_ID(0x7e7d01e575a0)], [A.CMP_STATUS(0x7e7d01e5add0)], [A.CMP_TYPE_CD(0x7e7d01e59d00)], [A.CMP_TYPE_ID(0x7e7d01e62fb0)], [A.COUT_MODE(0x7e7d01e635c0)], [A.ASSOCIATE_ID(0x7e7d01e63bd0)], [A.ORGAN_ID(0x7e7d01e641e0)], [A.CUSTOMER_TYPE(0x7e7d01e647f0)], [A.CUSTOMER_NAME(0x7e7d01e64e00)], [A.RES_CODE(0x7e7d01e65410)], [A.CRT_DTTM(0x7e7d01e55070)], [A.USER_GRP_ID(0x7e7d01e65d40)], [A.USER_ID(0x7e7d01e66350)], [A.ACT_LIMIT_DATE(0x7e7d01e68970)], [A.CALL_CRT_DTTM(0x7e7d01e68f80)], [A.CONTACT_TEL1(0x7e7d01e69590)], [A.CONTACT_TEL2(0x7e7d01e77bb0)], [B.POLICY_PAY_ADDR(0x7e7d01e781c0)], [A.ORGAN3_ID(0x7e7d01e787d0)], [A.FAIL_COUT_NUM(0x7e7d01e78de0)], [A.SYS_TYPE(0x7e7d01e793f0)], [A.RESERVE_BEGIN_DTTM(0x7e7d01e79a00)], [A.RESERVE_END_DTTM(0x7e7d01e7a010)], [B.SALE_CHANNEL(0x7e7d01e7a620)], [B.POLICY_NO(0x7e7d01e7ac30)], [B.FREE_LOOK_PERIOD(0x7e7d01e7b240)], [B.HOLDER_SEX(0x7e7d01e7b850)], [B.ORGAN4_ID(0x7e7d01e7be60)], [A.ASSIGN_DTTM(0x7e7d01e7c470)], [A.HANDLER_AGENT_CODE(0x7e7d01e7ca80)], [B.CANVASSER_CODE(0x7e7d01e7d090)], [B.CANVASSER_NAME(0x7e7d01e7d6a0)], [B.POLICY_OPE_DATE(0x7e7d01e7dcb0)], [A.APP_FLG(0x7e7d01e7e2c0)], [A.INTER_FLG(0x7e7d01e7e8d0)]), filter(nil), sort_keys([A.RESERVE_BEGIN_DTTM(0x7e7d01e79a00), ASC], [A.ACT_LIMIT_DATE(0x7e7d01e68970), ASC], [A.ACT_ID(0x7e7d01e62040), ASC])
  2 - output([A.ACT_ID(0x7e7d01e62040)], [A.DATA_ID(0x7e7d01e575a0)], [A.CMP_STATUS(0x7e7d01e5add0)], [A.CMP_TYPE_CD(0x7e7d01e59d00)], [A.CMP_TYPE_ID(0x7e7d01e62fb0)], [A.COUT_MODE(0x7e7d01e635c0)], [A.ASSOCIATE_ID(0x7e7d01e63bd0)], [A.ORGAN_ID(0x7e7d01e641e0)], [A.CUSTOMER_TYPE(0x7e7d01e647f0)], [A.CUSTOMER_NAME(0x7e7d01e64e00)], [A.RES_CODE(0x7e7d01e65410)], [A.CRT_DTTM(0x7e7d01e55070)], [A.USER_GRP_ID(0x7e7d01e65d40)], [A.USER_ID(0x7e7d01e66350)], [A.ACT_LIMIT_DATE(0x7e7d01e68970)], [A.CALL_CRT_DTTM(0x7e7d01e68f80)], [A.CONTACT_TEL1(0x7e7d01e69590)], [A.CONTACT_TEL2(0x7e7d01e77bb0)], [B.POLICY_PAY_ADDR(0x7e7d01e781c0)], [A.ORGAN3_ID(0x7e7d01e787d0)], [A.FAIL_COUT_NUM(0x7e7d01e78de0)], [A.SYS_TYPE(0x7e7d01e793f0)], [A.RESERVE_BEGIN_DTTM(0x7e7d01e79a00)], [A.RESERVE_END_DTTM(0x7e7d01e7a010)], [B.SALE_CHANNEL(0x7e7d01e7a620)], [B.POLICY_NO(0x7e7d01e7ac30)], [B.FREE_LOOK_PERIOD(0x7e7d01e7b240)], [B.HOLDER_SEX(0x7e7d01e7b850)], [B.ORGAN4_ID(0x7e
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值