索引看起来只是一个CREATE INDEX,实际却很容易建多、建错或者建了没效果。判断一个索引有没有用,不只找计划里有没有索引名,而是一起看扫描方式、估算行数、实际结果和回表规模。
本次实验在DM8中构造50000行订单数据,分别测试客户单列索引、客户与时间复合索引、函数索引和状态索引,并记录索引创建前后的执行计划。
| 项目 | 内容 |
|---|---|
| 数据库 | 本机DM8实例DMDISDEMO |
| 实验表 | CODEX_IDX_ORDER |
| 数据量 | 50000行 |
| 客户数量 | 9000 |
| 状态数量 | 4 |
| 渠道数量 | 5 |
| 计划查看 | EXPLAIN |
实验数据中,客户8527只有5笔订单,适合验证高选择性查询。状态列分布如下:
| 状态 | 行数 | 占比 |
|---|---|---|
| FINISHED | 35000 | 70% |
| CANCELLED | 5000 | 10% |
| NEW | 5000 | 10% |
| PAID | 5000 | 10% |
同一张表同时具备高选择性和低选择性条件。客户8527只命中5行,状态FINISHED却命中35000行,这两种查询对索引的需求完全不同。
| 索引类型 | 比较常见的用途 |
|---|---|
| 普通二级索引 | 客户编号、时间、状态等查询入口 |
| 唯一索引 | 订单号、证件号组合等唯一业务键 |
| 复合索引 | 多个条件经常一起出现的查询 |
| 函数索引 | 固定使用UPPER、日期函数或计算表达式的查询 |
| 位图索引 | 读多写少、取值少的分析数据 |
| 分区索引 | 配合分区表进行局部或全局访问 |
| 全文索引 | 长文本分词和词项检索 |
如果订单号要求不重复,我会优先用UNIQUE约束表达业务规则,而不是只建一个名字看起来像唯一键的索引。位图索引更适合历史分析,放到高并发更新表上可能带来额外维护压力。全文索引也不是普通B树索引的替代品,它解决的是分词检索。
CREATE INDEX IDX_ORD_CUSTOMER
ON SALES_ORDER(CUSTOMER_ID);
CREATE UNIQUE INDEX UDX_ORD_NO
ON SALES_ORDER(ORDER_NO);
SELECT INDEX_NAME, TABLE_NAME, UNIQUENESS
FROM USER_INDEXES
WHERE TABLE_NAME='SALES_ORDER';
ALTER INDEX IDX_ORD_CUSTOMER REBUILD;
DROP INDEX IDX_ORD_CUSTOMER;
查询客户8527的订单:
EXPLAIN
SELECT ORDER_ID, CUSTOMER_ID, ORDER_TIME, TOTAL_AMOUNT
FROM CODEX_IDX_ORDER
WHERE CUSTOMER_ID=8527;
计划底层为:
CSCN2 / CLUSTER SCAN
扫描对象记录数为50000,计划代价为6。实际查询只返回5行。
CREATE INDEX CODEX_IDX_ORDER_CUSTOMER
ON CODEX_IDX_ORDER(CUSTOMER_ID);
再次执行相同SQL,计划变为:
SSEK2 / SECONDARY INDEX SEEK
BLKUP2 / BOOKMARK LOOKUP
结果对比如下:
| 项目 | 索引前 | 索引后 |
|---|---|---|
| 访问节点 | CSCN2 | SSEK2 + BLKUP2 |
| 计划代价 | 6 | 1 |
| 表数据量 | 50000行 | 50000行 |
| 实际结果 | 5行 | 5行 |
| 估算结果 | 扫描50000行 | 1250行 |
客户索引将访问方式由聚集扫描变为二级索引查找,代价由6降到1。由于查询列没有全部包含在索引中,数据库通过BLKUP2回到基表取得订单时间和金额。计划估算1250行,实际只有5行,说明索引虽然生效,估算仍与真实分布存在明显偏差。这也是我这次实验里很有感触的一点:计划中的数字是优化器用来选路的估计,不是SQL真正返回的行数。
BLKUP2出现并不等于计划有问题。客户8527只有5行,先用索引定位5个位置,再回表取金额和时间,成本通常不高。如果条件返回35000行,就可能发生35000次左右的定位和取数,此时回表成本会迅速放大。
一种思路是把查询需要的列也放进索引,减少回表,但索引会变宽。宽索引占空间更多,写入时维护内容也更多。实际处理时,我会先看SQL频率和回表数量,不会为了让BLKUP2消失就不断往索引里加列。
客户订单列表通常还会附带时间范围,因此创建(CUSTOMER_ID, ORDER_TIME)复合索引:
CREATE INDEX CODEX_IDX_ORDER_CUST_TIME
ON CODEX_IDX_ORDER(CUSTOMER_ID, ORDER_TIME);
EXPLAIN
SELECT ORDER_ID, ORDER_TIME, TOTAL_AMOUNT
FROM CODEX_IDX_ORDER
WHERE CUSTOMER_ID=8527
AND ORDER_TIME>=TIMESTAMP '2026-01-01 00:00:00';
计划继续使用SSEK2,代价为1。加入时间范围后,估算记录数由1250下降到62,说明索引第二列参与了范围过滤。
随后只保留时间条件:
EXPLAIN
SELECT COUNT(*)
FROM CODEX_IDX_ORDER
WHERE ORDER_TIME>=TIMESTAMP '2026-01-01 00:00:00';
此时计划没有使用精确的SSEK2查找,而是出现:
SSCN / SECOND INDEX SCAN
结果说明,CUSTOMER_ID等值加ORDER_TIME范围与当前列序匹配;只有ORDER_TIME条件时,缺少前导列,数据库可能扫描二级索引,但无法按客户值缩小起始范围。复合索引的列顺序要跟真实SQL走。它不是字段越多越好,过宽的索引会增加空间、写入和缓存成本。
查询按大写名称进行匹配:
EXPLAIN
SELECT ORDER_ID
FROM CODEX_IDX_ORDER
WHERE UPPER(CUSTOMER_NAME)='CUSTOMER_8527';
没有函数索引时,计划使用CSCN2扫描50000行,并在上层计算UPPER后过滤,代价为6。
创建与查询表达式一致的函数索引:
CREATE INDEX CODEX_IDX_ORDER_NAME_UPPER
ON CODEX_IDX_ORDER(UPPER(CUSTOMER_NAME));
再次查看计划,访问节点变为SSEK2 + BLKUP2,代价由6降到1。
普通索引保存原始列值,查询对列使用函数后,原索引不一定能完成同样的定位。函数索引能解决这一问题,不过查询表达式要和索引定义对得上。
日期条件也有相同问题。相比:
WHERE TRUNC(ORDER_TIME)=DATE '2026-08-13'
普通时间索引通常更容易配合范围写法:
WHERE ORDER_TIME>=TIMESTAMP '2026-08-13 00:00:00' AND ORDER_TIME< TIMESTAMP '2026-08-14 00:00:00'
如果应用层能够统一大小写或提前保存标准化值,很多时候比建立多个函数索引更简单。函数索引适合稳定、高频、确实无法改写的表达式,而不是看到函数就建一个。
状态列只有4个取值,但分布并不均匀。创建状态索引后,分别查看FINISHED和CANCELLED:
CREATE INDEX CODEX_IDX_ORDER_STATUS
ON CODEX_IDX_ORDER(STATUS);
EXPLAIN SELECT * FROM CODEX_IDX_ORDER
WHERE STATUS='FINISHED';
EXPLAIN SELECT * FROM CODEX_IDX_ORDER
WHERE STATUS='CANCELLED';
两个条件都得到SSEK2 + BLKUP2,计划代价均为1,估算行数均为1250。但实际结果分别为35000和5000。
| 查询值 | 估算行数 | 实际行数 | 实际占比 |
|---|---|---|---|
| FINISHED | 1250 | 35000 | 70% |
| CANCELLED | 1250 | 5000 | 10% |
同一个估算值没有反映真实倾斜。FINISHED要返回70%的数据,大量索引定位和回表可能比直接扫描更贵。这里不适合简单得出“状态列能建”或“状态列不能建”的结论,更实际的做法是把数据分布和SQL一起看。
数据批量装载或分布变化后,可以先更新统计信息,再比较计划和真实耗时。如果统计信息长期停留在旧分布上,优化器再聪明也只能根据旧数据做选择。
假设订单号保存为字符,应用却用数字参数比较,数据库可能转换列或参数。转换发生在索引列一侧时,原来的访问方式可能发生变化。比较稳妥的做法是让绑定变量类型与列类型一致。
-- ORDER_NO为字符时,参数也按字符传入
WHERE ORDER_NO='202608250001'
WHERE CUSTOMER_NAME LIKE 'ZHANG%'
这种写法有明确前缀,通常更容易形成范围。下面的写法缺少已知起点:
WHERE CUSTOMER_NAME LIKE '%ZHANG'
如果业务经常做任意位置包含查询,可以考虑全文检索或专门的搜索方案,普通B树索引很难解决所有模糊匹配。
多个OR可能让优化器选择多条访问路径,也可能直接扫描。某些场景可以尝试拆成UNION ALL,但要先确认两部分是否会产生重复行。<>、NOT和IS NOT NULL往往返回较多数据,也不适合只凭语法判断索引一定有效。
本次实验出现的主要节点如下:
| 节点 | 含义 |
|---|---|
| CSCN2 / CLUSTER SCAN | 聚集表扫描 |
| SSEK2 / SECONDARY INDEX SEEK | 二级索引查找 |
| SSCN / SECOND INDEX SCAN | 二级索引扫描 |
| BLKUP2 / BOOKMARK LOOKUP | 根据索引结果回表取列 |
执行计划一般从底层向上看:
先看访问表还是索引
→ 再看查找、范围扫描还是全扫描
→ 再看过滤和回表位置
→ 最后比较估算行数与实际结果
计划代价是优化器内部比较值,不等同于毫秒。要判断实际快慢,还是要多跑几次,并结合耗时、逻辑读、物理读、CPU和并发影响。
查询返回表中大部分数据时,扫描表可能比索引加回表更合适。这不是“索引失效”,而是优化器认为另一条路成本更低。
我会检查是否只用了复合索引后续列、对索引列套了函数、发生隐式类型转换,或者使用了LIKE '%ABC'这类没有固定起点的条件。
估算与实际相差较大时,先看统计信息是否过旧,再看列值是否明显倾斜。本次1250对5、1250对35000,就是很直观的例子。
主键和唯一约束可能已经生成对应结构。(A,B)与(A)是否重复,要结合真实SQL判断;(A,B)和(B,A)服务的条件也不同。只看名字相似就删除,风险很大。
索引会随INSERT、UPDATE和DELETE维护。一个只改善低频报表、却拖慢核心写入的索引,整体上可能并不划算。
四组实验结果可以概括为:
| 场景 | 关键结果 |
|---|---|
| 客户单列索引 | CSCN2变为SSEK2+BLKUP2,代价6降到1 |
| 客户+时间复合索引 | 估算行数由1250降到62 |
| 仅使用复合索引后续列 | 变为SSCN扫描,不能精确按前导列定位 |
| UPPER函数索引 | 代价6降到1 |
| 状态索引 | 两个条件均估算1250,但实际为35000和5000 |
索引优化的目标不是让每条SQL都出现索引名,而是让数据库用更合适的路径拿到数据。计划、数据分布和真实执行结果三者能对得上,索引才算真正建到了点上。
文章
阅读量
获赞
