注册
索引访问路径学习笔记
专栏/技术分享/ 文章详情 /

索引访问路径学习笔记

巨浪 2026/08/28 110 0 0
摘要

索引看起来只是一个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;

三、单列索引实验

3.1 索引创建前

查询客户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行。

3.2 创建客户索引

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真正返回的行数。

3.3 回表到底贵不贵

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个取值,但分布并不均匀。创建状态索引后,分别查看FINISHEDCANCELLED

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一起看。

数据批量装载或分布变化后,可以先更新统计信息,再比较计划和真实耗时。如果统计信息长期停留在旧分布上,优化器再聪明也只能根据旧数据做选择。

七、容易让索引效果变差的SQL写法

7.1 隐式类型转换

假设订单号保存为字符,应用却用数字参数比较,数据库可能转换列或参数。转换发生在索引列一侧时,原来的访问方式可能发生变化。比较稳妥的做法是让绑定变量类型与列类型一致。

-- ORDER_NO为字符时,参数也按字符传入 WHERE ORDER_NO='202608250001'

7.2 LIKE前导通配符

WHERE CUSTOMER_NAME LIKE 'ZHANG%'

这种写法有明确前缀,通常更容易形成范围。下面的写法缺少已知起点:

WHERE CUSTOMER_NAME LIKE '%ZHANG'

如果业务经常做任意位置包含查询,可以考虑全文检索或专门的搜索方案,普通B树索引很难解决所有模糊匹配。

7.3 OR和不等值条件

多个OR可能让优化器选择多条访问路径,也可能直接扫描。某些场景可以尝试拆成UNION ALL,但要先确认两部分是否会产生重复行。&lt;>NOTIS NOT NULL往往返回较多数据,也不适合只凭语法判断索引一定有效。

八、计划节点的含义

本次实验出现的主要节点如下:

节点 含义
CSCN2 / CLUSTER SCAN 聚集表扫描
SSEK2 / SECONDARY INDEX SEEK 二级索引查找
SSCN / SECOND INDEX SCAN 二级索引扫描
BLKUP2 / BOOKMARK LOOKUP 根据索引结果回表取列

执行计划一般从底层向上看:

先看访问表还是索引
→ 再看查找、范围扫描还是全扫描
→ 再看过滤和回表位置
→ 最后比较估算行数与实际结果

计划代价是优化器内部比较值,不等同于毫秒。要判断实际快慢,还是要多跑几次,并结合耗时、逻辑读、物理读、CPU和并发影响。

九、索引没达到预期时怎么查

7.1 返回比例

查询返回表中大部分数据时,扫描表可能比索引加回表更合适。这不是“索引失效”,而是优化器认为另一条路成本更低。

7.2 谓词写法

我会检查是否只用了复合索引后续列、对索引列套了函数、发生隐式类型转换,或者使用了LIKE '%ABC'这类没有固定起点的条件。

7.3 统计信息

估算与实际相差较大时,先看统计信息是否过旧,再看列值是否明显倾斜。本次1250对5、1250对35000,就是很直观的例子。

7.4 重复索引

主键和唯一约束可能已经生成对应结构。(A,B)(A)是否重复,要结合真实SQL判断;(A,B)(B,A)服务的条件也不同。只看名字相似就删除,风险很大。

7.5 写入成本

索引会随INSERTUPDATEDELETE维护。一个只改善低频报表、却拖慢核心写入的索引,整体上可能并不划算。

十、实验结论

四组实验结果可以概括为:

场景 关键结果
客户单列索引 CSCN2变为SSEK2+BLKUP2,代价6降到1
客户+时间复合索引 估算行数由1250降到62
仅使用复合索引后续列 变为SSCN扫描,不能精确按前导列定位
UPPER函数索引 代价6降到1
状态索引 两个条件均估算1250,但实际为35000和5000

索引优化的目标不是让每条SQL都出现索引名,而是让数据库用更合适的路径拿到数据。计划、数据分布和真实执行结果三者能对得上,索引才算真正建到了点上。

评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服