KingbaseES(Oracle兼容模式)LIKE前缀匹配 COLLATE _C_ 索引优化

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 实验目标

  1. 验证普通索引:前缀LIKE查询无法生成索引区间条件,触发全量数据扫描与大量无效过滤;
  1. 验证COLLATE
    "C"索引:优化器自动生成索引区间条件,实现精准索引范围扫描;
  1. 对比两种索引方案的执行计划、缓冲区开销、执行耗时差异;
  1. 总结适配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扫描量,查询性能提升数百倍。该优化方案适配生产真实业务场景、改造成本低、稳定性高,可广泛应用于各类编号、编码、单元号前缀查询业务。

相关推荐
大鹏说大话1 天前
移动应用打包常见报错汇总
数据库
数安3000天1 天前
全行“手机号“字段有20多个名字——字段命名不统一,数据分类分级怎么做?
大数据·数据库·分类
caishenzhibiao1 天前
无极多空共振 同花顺期货通指标
java·c语言·c#
IT_Octopus1 天前
Redis 数据存储与序列化完全指南:从 byte[] 到 Object:Redis 序列化机制与生产环境选型
数据库·redis·bootstrap
丙氨酸長鏈1 天前
用过redis哪些数据类型?Redis String 类型的底层实现是什么?
数据库·redis·缓存
@Mike@1 天前
05-数据库学习笔记(缓冲池优化和存储模型)
数据库·笔记·学习
风曦Kisaki1 天前
MySql常用命令速查表
数据库·sql·mysql
南京码讯光电技术有限公司1 天前
WiFi 5 vs WiFi 6 vs WiFi 7 Industrial Modules
网络·数据库·工业通信·wifi module
赤壁小虾1 天前
InnoDB为什么不用跳表,Redis为什么不用B+树?
数据库·redis·b树
modelmd1 天前
C 语言基础数据类型详解:大小与内存存储
c语言