KingbaseES(Oracle兼容模式)LIKE前缀匹配 COLLATE "C" 索引优化
1 文档概述
1.1 业务背景
业务中存在大量前缀模糊查询场景,SQL语句使用 字段 LIKE '前缀%'
语法检索数据。在KingbaseES默认中文排序规则下,普通索引无法将前缀模糊查询转化为索引区间扫描,只能全量扫描数据后再过滤,数据量越大查询性能越差,生产环境出现秒级、分钟级慢查询。
通过对查询字段建立 COLLATE "C"
字节排序索引,可让数据库优化器自动将 LIKE 'xxx%' 前缀匹配,等价转换为
字段 >= 前缀 AND 字段 < 前缀+1
索引区间扫描,实现索引层精准过滤,大幅降低IO开销、提升查询效率。
1.2 实验环境
- 数据库版本:KingbaseES V8
- 运行模式:Oracle兼容模式
- 实验场景:前缀模糊查询 + 窗口函数分组取最新数据
1.3 实验目标
- 验证普通索引:前缀LIKE查询无法生成索引区间条件,触发全量数据扫描与大量无效过滤;
- 验证COLLATE
"C"索引:优化器自动生成索引区间条件,实现精准索引范围扫描;
- 对比两种索引方案的执行计划、缓冲区开销、执行耗时差异;
- 总结适配Oracle模式的索引优化规范与落地避坑要点。
2 实验环境搭建
2.1 Oracle兼容模式
2.2 创建业务测试表
模拟生产项目表结构,完全适配业务查询场景:
sql
CREATE SCHEMA IF NOT EXISTS bdcdj_pz;
CREATE TABLE bdcdj_pz.bdc_xm (
id BIGSERIAL PRIMARY KEY,
bdcdyh VARCHAR2(100), -- 单元号(核心查询字段)
bdclx VARCHAR2(20), -- 类型
djsj TIMESTAMP, -- 登记时间
other_col VARCHAR2(200) -- 业务冗余字段
);
2.3 构造实验测试数据
数据构造规则:少量目标前缀数据(模拟业务重复数据)+
大量无关数据(模拟海量脏数据),精准复现生产慢查询现象:
sql
-- 插入目标测试数据(同单元多条记录,用于ROW_NUMBER分组取第一条)
INSERT INTO bdcdj_pz.bdc_xm(bdcdyh,bdclx,djsj,other_col)
VALUES
('520115103038GB00597001','02','2025-01-10
08:20:00','目标记录1'),
('520115103038GB00597001','02','2025-01-09
08:20:00','目标记录2'),
('520115103038GB00597001','03','2025-02-05
10:10:00','目标记录3'),
('520115000000AA00001','01','2025-01-01 10:00:00','无关数据'),
('520115000000AA00002','02','2025-01-02 10:00:00','无关数据'),
('520115000000AA00004',NULL,'2025-01-04
10:00:00','bdclx为空数据'),
('520115000000AA00005','03',NULL,'djsj为空数据');
-- 批量插入海量无关数据,模拟大数据量场景
INSERT INTO bdcdj_pz.bdc_xm(bdcdyh,bdclx,djsj,other_col)
SELECT
'520115'||(100000 + generate_series(1,500))::VARCHAR2,
'01',
now(),
'批量无关业务数据'
FROM generate_series(1,500);
-- 收集统计信息(保证优化器代价估算准确,实验必备)
ANALYZE bdcdj_pz.bdc_xm;
2.4 待优化业务原始SQL
完全复用生产业务SQL,实现分组取最新登记数据、前缀模糊查询逻辑:
sql
explain (analyze ,buffers)
SELECT rownum AS xh, n.bdcdyh, n.bdclx, NULL AS bz
FROM (
SELECT m.bdcdyh, m.bdclx
FROM (
SELECT ROW_NUMBER() OVER (PARTITION BY x.bdcdyh ORDER BY x.djsj ASC) AS
rn, x.bdcdyh, x.bdclx, x.djsj
FROM bdcdj_pz.bdc_xm x
WHERE x.bdcdyh LIKE ('520115103038GB00597%')
AND x.bdclx IS NOT NULL
AND x.djsj IS NOT NULL
) m
WHERE m.rn = 1
ORDER BY m.bdclx, m.djsj
) n;
3 实验场景一:普通索引
3.1 创建普通索引
sql
-- 清理残留索引,保证实验纯净
DROP INDEX IF EXISTS bdcdj_pz.idx_bdc_xm_normal;
DROP INDEX IF EXISTS bdcdj_pz.idx_bdcdyh_c;
-- 创建常规业务复合索引
CREATE INDEX idx_bdc_xm_normal ON bdcdj_pz.bdc_xm(bdcdyh,djsj);
3.2 执行计划核心特征
bash
test=# CREATE INDEX idx_bdc_xm_normal ON bdcdj_pz.bdc_xm(bdcdyh,djsj);
CREATE INDEX
test=# explain (analyze ,buffers)
test-# SELECT rownum AS xh, n.bdcdyh, n.bdclx, NULL AS bz
test-# FROM (
test(# SELECT m.bdcdyh, m.bdclx
test(# FROM (
test(# SELECT ROW_NUMBER() OVER (PARTITION BY x.bdcdyh ORDER BY x.djsj ASC) AS rn, x.bdcdyh, x.bdclx, x.djsj
test(# FROM bdcdj_pz.bdc_xm x
test(# WHERE x.bdcdyh LIKE ('520115103038GB00597%')
test(# AND x.bdclx IS NOT NULL
test(# AND x.djsj IS NOT NULL
test(# ) m
test(# WHERE m.rn = 1
test(# ORDER BY m.bdclx, m.djsj
test(# ) n;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Count (cost=5447.20..5447.22 rows=0 width=56) (actual time=61.134..61.171 rows=1 loops=1)
Buffers: shared hit=2605
-> Subquery Scan on n (cost=5447.20..5447.21 rows=1 width=16) (actual time=61.131..61.168 rows=1 loops=1)
Buffers: shared hit=2605
-> Sort (cost=5447.20..5447.20 rows=1 width=24) (actual time=61.131..61.166 rows=1 loops=1)
Sort Key: m.bdclx, m.djsj
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=2605
-> Subquery Scan on m (cost=5446.37..5447.19 rows=1 width=24) (actual time=61.121..61.160 rows=1 loops=1)
Filter: (m.rn = 1)
Rows Removed by Filter: 2
Buffers: shared hit=2605
-> WindowAgg (cost=5446.37..5446.87 rows=25 width=32) (actual time=61.119..61.157 rows=3 loops=1)
Buffers: shared hit=2605
-> Sort (cost=5446.37..5446.44 rows=25 width=24) (actual time=61.070..61.106 rows=3 loops=1)
Sort Key: x.bdcdyh, x.djsj
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=2605
-> Gather (cost=1000.00..5445.79 rows=25 width=24) (actual time=0.203..61.097 rows=3 loops=1)
Workers Planned: 1
Workers Launched: 1
Buffers: shared hit=2605
-> Parallel Seq Scan on bdc_xm x (cost=0.00..4443.29 rows=15 width=24) (actual time=2.731..33.159 rows=2 loops=2)
Filter: ((bdclx IS NOT NULL) AND (djsj IS NOT NULL) AND ((bdcdyh)::text ~~ '520115103038GB00597%'::text))
Rows Removed by Filter: 125002
Buffers: shared hit=2605
Planning Time: 0.295 ms
Execution Time: 61.216 ms
(28 行记录)
3.3 实验现象分析
- 扫描方式:并行全表扫描(Parallel Seq Scan),未走索引范围扫描;
- 过滤逻辑:LIKE前缀查询仅作为后置Filter条件,无索引区间(Index
Cond)下推;
- 资源损耗:全量扫描数据表,丢弃 125002条无效数据,IO开销极大;
- 性能指标:共享缓冲区命中 shared hit=2605 ,执行耗时
61.216ms。
根本原因:数据库默认中文区域排序规则(zh_CN.UTF-8)排序逻辑复杂,优化器无法安全将前缀LIKE匹配等价转化为区间查询,只能全表扫描后过滤。
4 实验场景二:COLLATE "C"索引(优化方案)
4.1 创建字节排序索引
sql
-- 删除普通索引
DROP INDEX IF EXISTS bdcdj_pz.idx_bdc_xm_normal;
-- 创建COLLATE "C"字节排序索引(生产推荐CONCURRENTLY无锁创建)
CREATE INDEX CONCURRENTLY idx_bdcdyh_c ON bdcdj_pz.bdc_xm(bdcdyh
COLLATE "C");
4.2 执行计划核心特征
bash
test=# DROP INDEX IF EXISTS bdcdj_pz.idx_bdc_xm_normal;
DROP INDEX
test=# CREATE INDEX CONCURRENTLY idx_bdcdyh_c ON bdcdj_pz.bdc_xm(bdcdyh COLLATE "C");
CREATE INDEX
test=#
test=# explain (analyze ,buffers)
test-# SELECT rownum AS xh, n.bdcdyh, n.bdclx, NULL AS bz
test-# FROM (
test(# SELECT m.bdcdyh, m.bdclx
test(# FROM (
test(# SELECT ROW_NUMBER() OVER (PARTITION BY x.bdcdyh ORDER BY x.djsj ASC) AS rn, x.bdcdyh, x.bdclx, x.djsj
test(# FROM bdcdj_pz.bdc_xm x
test(# WHERE x.bdcdyh LIKE ('520115103038GB00597%')
test(# AND x.bdclx IS NOT NULL
test(# AND x.djsj IS NOT NULL
test(# ) m
test(# WHERE m.rn = 1
test(# ORDER BY m.bdclx, m.djsj
test(# ) n;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------
Count (cost=1270.56..1270.59 rows=0 width=56) (actual time=0.043..0.044 rows=1 loops=1)
Buffers: shared hit=4
-> Subquery Scan on n (cost=1270.56..1270.58 rows=1 width=16) (actual time=0.042..0.043 rows=1 loops=1)
Buffers: shared hit=4
-> Sort (cost=1270.56..1270.57 rows=1 width=24) (actual time=0.042..0.042 rows=1 loops=1)
Sort Key: m.bdclx, m.djsj
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=4
-> Subquery Scan on m (cost=1269.74..1270.55 rows=1 width=24) (actual time=0.035..0.037 rows=1 loops=1)
Filter: (m.rn = 1)
Rows Removed by Filter: 2
Buffers: shared hit=4
-> WindowAgg (cost=1269.74..1270.24 rows=25 width=32) (actual time=0.034..0.036 rows=3 loops=1)
Buffers: shared hit=4
-> Sort (cost=1269.74..1269.80 rows=25 width=24) (actual time=0.028..0.029 rows=3 loops=1)
Sort Key: x.bdcdyh, x.djsj
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=4
-> Bitmap Heap Scan on bdc_xm x (cost=13.41..1269.16 rows=25 width=24) (actual time=0.018..0.019 rows=3 loops=1)
Filter: ((bdclx IS NOT NULL) AND (djsj IS NOT NULL) AND ((bdcdyh)::text ~~ '520115103038GB00597%'::text))
Heap Blocks: exact=1
Buffers: shared hit=4
-> Bitmap Index Scan on idx_bdcdyh_c (cost=0.00..13.40 rows=498 width=0) (actual time=0.011..0.011 rows=3 loops=1)
Index Cond: (((bdcdyh)::text >= '520115103038GB00597'::text) AND ((bdcdyh)::text < '520115103038GB00598'::text))
Buffers: shared hit=3
Planning Time: 12.266 ms
Execution Time: 0.088 ms
(27 行记录)
4.3 实验现象分析
- 扫描方式:Bitmap索引范围扫描,精准定位目标数据区间;
- 核心优化:优化器自动将 LIKE
'xxx%'前缀匹配转换为索引区间条件,实现索引层前置过滤;
- 过滤逻辑:索引粗筛+少量数据兜底Filter校验,无大量无效数据扫描;
- 性能指标:共享缓冲区命中 shared hit=4 ,执行耗时降至
0.088ms,性能提升数百倍。
5 实验结果对比汇总
对比维度 普通复合索引 COLLATE "C" 索引
数据扫描方式 并行全表扫描 Bitmap索引范围扫描
索引区间条件 无 自动生成前缀区间条件
过滤位置 全量读取后后置过滤 索引前置粗筛+少量兜底过滤
共享缓冲区Hit数 2605 4
执行耗时 61.216ms 0.088ms
无效过滤行数 125002行 极少
6 核心优化原理
6.1 COLLATE "C" 排序规则机制
COLLATE "C"
为字节级排序规则 ,不依赖中文、英文等区域语言规则,直接按照字符串二进制字节比对大小。KES/PG内核针对该规则内置专属优化:仅C排序索引支持前缀LIKE查询等价转为区间范围扫描。
6.2 普通索引无法优化的原因
数据库默认采用中文区域排序规则,字符排序、匹配逻辑复杂,无法保证前缀模糊匹配与数值区间的绝对等价性,优化器为保证数据准确性,放弃索引范围下推,只能执行全量扫描过滤。
6.3 优化适用范围限制
- 仅支持 右通配前缀查询 LIKE 'xxx%';
- 不支持左通配 LIKE '%xxx'、全模糊 LIKE '%xxx%';
- 兼容KES Oracle模式、PG原生模式,适配VARCHAR2、TEXT字段类型。
7 总结
在KingbaseES
Oracle兼容模式下,针对固定前缀后缀模糊查询 场景,普通字符串索引无法发挥优化作用,大数据量下极易产生慢查询。通过建立
字段 COLLATE "C"
字节排序索引,可让数据库自动优化前缀LIKE查询,实现索引层精准范围过滤,大幅减少IO扫描量,查询性能提升数百倍。该优化方案适配生产真实业务场景、改造成本低、稳定性高,可广泛应用于各类编号、编码、单元号前缀查询业务。