首页 > 数据库 > 国内数据库 > OceanBase 局部索引和全局索引
在 OceanBase 数据库中, 局部索引(Local Index) 是默认且最常用的索引类型。它的核心特征是 索引的分区规则与主表完全保持一致。
局部索引必须和主表保持相同的分区关系,索引的每个分区与主表对应分区绑定。
也就是:主表某个分区的数据,对应的局部索引数据也在相应的索引分区中。 对普通局部索引而言,索引列不要求包含主表分区键。
Tablet)与主表的分片是
一对一绑定的。也就是说,主表的第 N 个分区和该局部索引的第 N 个分区,其数据必然存储在同一台
OBServer 节点上。INSERT、
UPDATE 或
DELETE 时,对应的局部索引变更仅在该节点本地完成,
不需要发起跨节点的分布式事务(无需 2PC 两阶段提交)。优势:
TRUNCATE PARTITION 或
DROP PARTITION),对应的局部索引分区会被自动快速清理,无需重构整个索引。局限性:
RPC 开销较大。在创建索引时添加
LOCAL 关键字(在分区表上如果不显式指定,默认即为
LOCAL):
-- 建表并插入数据
CREATE TABLE user_table (
user_id INT NOT NULL,
phone_number VARCHAR(20) NOT NULL,
user_name VARCHAR(50),
age INT,
PRIMARY KEY (user_id)
)
PARTITION BY HASH(user_id) PARTITIONS 4;
-- 插入测试数据
INSERT INTO user_table (user_id, phone_number, user_name, age)
SELECT
idx AS user_id,
CONCAT('138', LPAD(idx, 8, '0')) AS phone_number,
CONCAT('User_', idx) AS user_name,
(20 + (idx % 30)) AS age
FROM (
SELECT @row := @row + 1 AS idx
FROM
(SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t1,
(SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t2,
(SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t3,
(SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t4,
(SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5) t5,
(SELECT @row := 1000) r -- 设置起始 ID 为 1000(避免与之前的测试数据冲突)
LIMIT 10000
) sub;
-- 索引只包含 age , 没有 user_id, 仍然可以是局部索引。 CREATE INDEX idx_age_local ON user_table(age) LOCAL;
UNIQUE LOCAL INDEX,为了保证全表范围的唯一性,
唯一局部索引通常需要包含表的分区键。CREATE UNIQUE INDEX IDX_user_id_age ON user_table(user_id, age) LOCAL;
(1) 局部索引查询 — 带主表分区键(极速点查)
当查询条件同时包含索引列和主表分区键
user_id 时,优化器会直接进行
分区裁剪(
Partition Pruning),仅精准访问 1 个 Tablet:
EXPLAIN SELECT * FROM user_table WHERE age = 25 AND user_id = 1001; +---------------------------------------------------------------------------------------------------------------------------------------------------+ | Query Plan | +---------------------------------------------------------------------------------------------------------------------------------------------------+ | =============================================== | | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | | ----------------------------------------------- | | |0 |TABLE GET|user_table|1 |7 | | | =============================================== | | Outputs & filters: | | ------------------------------------- | | 0 - output([user_table.user_id], [user_table.phone_number], [user_table.user_name], [user_table.age]), filter([user_table.age = 25]), rowset=16 | | access([user_table.user_id], [user_table.age], [user_table.phone_number], [user_table.user_name]), partitions(p1) | | is_index_back=false, is_global_index=false, filter_before_indexback[false], | | range_key([user_table.user_id]), range[1001 ; 1001], | | range_cond([user_table.user_id = 1001]) | +---------------------------------------------------------------------------------------------------------------------------------------------------+
partitions(p1) —— 命中分区裁剪,由于查询条件带上了分区键
user_id = 1001,
OceanBase 计算出数据必然存放在
p1 分区,因此直接裁剪掉其他 3 个分区,避免了全表/全分区广播。range[1001 ; 1001] &
range_cond(...) —— 定位主键,明确告知存储引擎直接去主表定位
user_id = 1001 这条记录。is_index_back=false —— 无回表开销 ,直接读取的主表,拿到了整行所有列,
不需要回表。filter([user_table.age = 25]) —— 二级过滤 定位到
user_id = 1001 的记录后,内存中检查其
age 是否等于 25。若匹配则输出,不匹配则抛弃。rowset=16 —— 向量化引擎支持,表明开启了 OceanBase 的向量化执行引擎(以
Rowset 为单位批处理数据),提升
CPU 缓存利用率。(2) 局部索引查询 — 不带主表分区键(广播全分区扫描)
如果仅按
age 查询,由于局部索引打散在各个分区中,数据库不知道目标数据存放在哪个
Tablet:
EXPLAIN SELECT * FROM user_table WHERE age = 25; +----------------------------------------------------------------------------------------------------------------------------------------------------+ | Query Plan | +----------------------------------------------------------------------------------------------------------------------------------------------------+ | =============================================================== | | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | | --------------------------------------------------------------- | | |0 |PX COORDINATOR | |104 |410 | | | |1 |└─EXCHANGE OUT DISTR |:EX10000 |104 |328 | | | |2 | └─PX PARTITION ITERATOR| |104 |141 | | | |3 | └─TABLE FULL SCAN |user_table|104 |141 | | | =============================================================== | | Outputs & filters: | | ------------------------------------- | | 0 - output([INTERNAL_FUNCTION(user_table.user_id, user_table.phone_number, user_table.user_name, user_table.age)]), filter(nil), rowset=256 | | 1 - output([INTERNAL_FUNCTION(user_table.user_id, user_table.phone_number, user_table.user_name, user_table.age)]), filter(nil), rowset=256 | | dop=1 | | 2 - output([user_table.user_id], [user_table.age], [user_table.phone_number], [user_table.user_name]), filter(nil), rowset=256 | | force partition granule | | 3 - output([user_table.user_id], [user_table.age], [user_table.phone_number], [user_table.user_name]), filter([user_table.age = 25]), rowset=256 | | access([user_table.user_id], [user_table.age], [user_table.phone_number], [user_table.user_name]), partitions(p[0-3]) | | is_index_back=false, is_global_index=false, filter_before_indexback[false], | | range_key([user_table.user_id]), range(MIN ; MAX)always true | +----------------------------------------------------------------------------------------------------------------------------------------------------+
执行计划关键特征:
EXCHANGE IN/OUT /
PX COORDINATOR:生成并行/分布式扫描计划。partitions(p0-3):扇出(Fan-out)到
所有 4 个分区并行扫描局部索引,汇总结果后再返回。在大表场景下 RPC 开销显著增加。(3) 查询条件命中复合索引(等值查询)
由于索引包含了
user_id(主表分区键兼索引第一列)和
age,且这是一个
UNIQUE 局部索引:
EXPLAIN SELECT user_id, age FROM user_table WHERE user_id = 1001 AND age = 25; +------------------------------------------------------------------------------------------------+ | Query Plan | +------------------------------------------------------------------------------------------------+ | =============================================== | | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | | ----------------------------------------------- | | |0 |TABLE GET|user_table|1 |7 | | | =============================================== | | Outputs & filters: | | ------------------------------------- | | 0 - output([user_table.user_id], [user_table.age]), filter([user_table.age = 25]), rowset=16 | | access([user_table.user_id], [user_table.age]), partitions(p1) | | is_index_back=false, is_global_index=false, filter_before_indexback[false], | | range_key([user_table.user_id]), range[1001 ; 1001], | | range_cond([user_table.user_id = 1001]) | +------------------------------------------------------------------------------------------------+
OPERATOR: TABLE GET:因为是唯一索引(
UNIQUE),且
(user_id, age) 的条件全部精确提供,优化器知道最多只有 1 条记录,直接定位取数。partitions(p1):带上了分区键
user_id = 1001,精准裁减到
p1 分区。is_index_back=false:
索引覆盖!由于查询只要求返回
user_id 和
age,索引本身就包含了这两个字段,因此无需回表查主表,性能达到最优。局部索引的使用可以总结成一句话:
Local Index 与主表分区一一对应,索引分区与对应的主表分区绑定。普通
Local Index 不要求索引键包含分区键;
Unique Local Index 为保证全局唯一性,需要关注分区键包含在唯一键中。
全局索引(
Global Index)是
OceanBase 数据库在分布式架构下,为解决非分区键查询性能问题而设计的关键机制。
在
OceanBase 的分布式表结构中,如果一张表按
user_id 作了
Hash 分区,当根据
phone_number(非分区键)进行查询时,数据库由于不知道目标数据在哪个分区,必须向所有分区(
OBServer 节点)发送查询请求(广播),这会导致极高的网络
RPC 延迟。
Tablet)可以分布在集群内的任意
OBServer 上。索引列 + 主表主键 + (主表分区键) 构成。WHERE 条件中强行带上主表分区键。Global Partitioned Index),例如主表按
Range(gmt_create) 切分,全局索引可以按
Hash(phone_number) 切分。INSERT/UPDATE/DELETE 时,系统必须使用两阶段提交协议(2PC)来保证主表数据与全局索引数据的强一致性,这对写入吞吐量有一定损耗。-- 在 user_name 列上创建全局非分区索引 CREATE INDEX idx_username_global ON user_table(user_name) GLOBAL;
CREATE INDEX idx_phone_part ON user_table(phone_number) GLOBAL PARTITION BY KEY(phone_number) PARTITIONS 8;
(1) 全局非分区索引的单点查询
EXPLAIN SELECT * FROM user_table WHERE user_name = 'Alice'; +------------------------------------------------------------------------------------------------------------------------------ | Query Plan +------------------------------------------------------------------------------------------------------------------------------ | ======================================================================================= | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | --------------------------------------------------------------------------------------- | |0 |DISTRIBUTED TABLE RANGE SCAN|user_table(idx_username_global)|1 |6 | | ======================================================================================= | Outputs & filters: | ------------------------------------- | 0 - output([user_table.user_id], [user_table.phone_number], [user_table.user_name], [user_table.age]), filter(nil), rowset= | access([user_table.user_id], [user_table.user_name], [user_table.phone_number], [user_table.age]), partitions(p0) | is_index_back=true, is_global_index=true, | range_key([user_table.user_name], [user_table.user_id]), range(Alice,MIN ; Alice,MAX), | range_cond([user_table.user_name = 'Alice']) +-----------------------------------------------------------------------------------------------------------------------------
执行计划解读:
OPERATOR: DISTRIBUTED TABLE RANGE SCANuser_name = 'Alice',但由于索引键是复合结构
(user_name, user_id),优化器构建了一个物理闭区间范围
[Alice, MIN ; Alice, MAX],在该区间内进行范围扫描。NAME: user_table(idx_username_global)idx_username_global。is_global_index=true &
is_index_back=trueSELECT * 需要获取索引中未包含的列,如
phone_number 和
age)。partitions(p0)idx_username_global 是全局非分区索引,它只有一个默认分片
p0,所以精准扫描索引的
p0 分区。range_key 与
range_condrange_key:全局索引在底层存储的实际
Physical Key,由
(user_name, user_id) 组成。range_cond([user_table.user_name = 'Alice']):定位条件,只扫描
user_name 等于
'Alice' 的索引项。(2) 全局分区索引查询
按非分区键
phone_number 进行查询,虽然没有提供主表分区键
user_id,但由于全局索引按
phone_number 独立分区,路由可以直接命中对应的全局索引
Tablet 。
EXPLAIN SELECT * FROM user_table WHERE phone_number = '13800000003'; | Query Plan +------------------------------------------------------------------------------------------------------------------------------ | ================================================================================== | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | ---------------------------------------------------------------------------------- | |0 |DISTRIBUTED TABLE RANGE SCAN|user_table(idx_phone_part)|1 |6 | | ================================================================================== | Outputs & filters: | ------------------------------------- | 0 - output([user_table.user_id], [user_table.phone_number], [user_table.user_name], [user_table.age]), filter(nil), rowset= | access([user_table.user_id], [user_table.phone_number], [user_table.user_name], [user_table.age]), partitions(p1) | is_index_back=true, is_global_index=true, | range_key([user_table.phone_number], [user_table.user_id]), range(13800000003,MIN ; 13800000003,MAX), | range_cond([user_table.phone_number = '13800000003']) +------------------------------------------------------------------------------------------------------------------------------
执行计划解读:
OPERATOR: DISTRIBUTED TABLE RANGE SCANRPC 调度。phone_number = '13800000003' 进行范围构建
[13800000003, MIN ; 13800000003, MAX] 扫描。NAME: user_table(idx_phone_part),优化器成功选中了刚才创建的全局分区索引
idx_phone_part。EST.ROWS: 1 &
EST.TIME(us): 6 ,预估只需返回
1 行 数据,预估开销仅
6 微秒(之前全表扫描是 155 微秒),性能大幅提升。partitions(p1) —— 全局索引分区裁剪
,优化器计算
phone_number = '13800000003' 的分区路由,精准裁剪并只扫描全局索引 8 个分区中的
p1 分区**。is_global_index=true —— 命中全局索引**,确认使用了全局索引。is_index_back=true —— 触发回表**,因为使用了
SELECT *,需要获取索引未存储的字段(
user_name 和
age),所以拿着查到的主键
user_id 跨节点回主表拉取全量数据。range_key 与
range_condrange_key**:索引表底层物理存储主键为
(phone_number, user_id)。range_cond**:检索下推条件为
phone_number = '13800000003'。为了避免分布式回表带来的性能损耗,可以通过
STORING 子句将常用查询列直接存储在全局索引中:
-- 将 user_name 存储在索引中,查询 phone_number 和 user_name 时无需回表 CREATE INDEX idx_phone_name ON user_table(phone_number) STORING(user_name) GLOBAL; EXPLAIN SELECT user_id, phone_number, user_name FROM user_table WHERE phone_number = '13800000003'; +---------------------------------------------------------------------------------------------------------------+ | Query Plan | +---------------------------------------------------------------------------------------------------------------+ | ====================================================================== | | |ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)| | | ---------------------------------------------------------------------- | | |0 |TABLE RANGE SCAN|user_table(idx_phone_name)|1 |3 | | | ====================================================================== | | Outputs & filters: | | ------------------------------------- | | 0 - output([user_table.user_id], [user_table.phone_number], [user_table.user_name]), filter(nil), rowset=16 | | access([user_table.user_id], [user_table.phone_number], [user_table.user_name]), partitions(p0) | | is_index_back=false, is_global_index=true, | | range_key([user_table.phone_number], [user_table.user_id]), range(13800000003,MIN ; 13800000003,MAX), | | range_cond([user_table.phone_number = '13800000003']) | +---------------------------------------------------------------------------------------------------------------+
适用:需要包含的辅助字段 数量少、长度固定或较短(如状态码、名称、时间戳),且对应的查询属于高频热点 Query。
Local Index),以获得最佳的写入性能和最小的维护开销。局部索引(Local Index) 与全局索引(Global Index)是两种核心的索引组织形式。它们最本质的区别在于: 索引的分区规则是否与主表严格绑定。
| 对比维度 | 局部索引 (Local Index) | 全局索引 (Global Index) |
|---|---|---|
| 物理分布 | 与主表对应分区 物理同置(同节点、同生命周期) | 独立划分为 Tablet,物理分布在集群 任意 OBServer 上 |
| 分区规则 | 必须与主表完全一致 | 可以与主表完全解耦(可不分区,也可按不同列与主表不同分区) |
| 唯一约束限制 |
必须包含主表的分区键(如
PRIMARY KEY / UNIQUE) |
可以是任何列, 无需包含主表的分区键 |
| 带分区键查询 | 1 次 RPC 精准点查(触发主表分区裁剪) | 1 次 RPC 精准点查 |
| 不带分区键查询 | 全分区广播扫描(Fan-out),延迟随分区数增加而上升 | 1 次 RPC 精准点查 |
| 写入与 DML 开销 | 单阶段提交,无跨节点开销,写入吞吐高 | 两阶段提交(2PC),主表与索引不在同节点时有网络延迟开销 |
| 回表开销 | 本地回表,在同节点完成 | 分布式回表,需跨节点 RPC 查主表(除非命中覆盖索引) |
| DDL 运维 | 删除/清空主表单个分区, 索引分区自动同步释放 | 删除/清空主表单个分区, 需要重构或异步清理全局索引 |
生产环境中,能通过合理分区键 +
Local Index 解决的,优先
Local Index;只有确实存在大量 不带分区键的跨分区查询 时,再考虑
Global Index。
OceanBase 的 分区索引和全局索引先写到这里,
如果本文对你有所帮助,欢迎点赞、推荐和转发,也欢迎关注后续文章,一起学习
OceanBase 分布式数据库。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31444449/viewspace-3181037/ ,如需转载,请注明出处,否则将追究法律责任。