避免在索引列上使用函数
高优先级- 问题现象
- 业务查询条件对索引列套了函数,走全表扫描,10 万行表查询从毫秒级变成数秒。
- 业务场景
- CRM 系统中客户表 t_customer(cust_id, cust_name, id_card, create_time),运营需要按客户姓名模糊查找。开发写成 UPPER(cust_name) LIKE 'ZHANG%' 以忽略大小写,上线后列表页加载超过 4 秒。
SELECT * FROM t_customer WHERE UPPER(cust_name) LIKE 'ZHANG%';
-- 方案一:建函数索引(推荐,业务写法不变)
CREATE INDEX idx_cust_name_up ON t_customer(UPPER(cust_name));
-- 方案二:数据入库时统一清洗为小写,查询直接走普通索引
SELECT * FROM t_customer WHERE cust_name LIKE 'zhang%';
原理说明索引存的 key 是列的原值。一旦对列套函数,索引 B 树的 key 与 UPPER(cust_name) 的结果不再对应,优化器无法用索引定位,只能全表扫描并逐行计算函数。函数索引本质上把「函数结果」也建成了索引 key,因此能重新命中。
示例效果(非实测)全表扫描 → 索引范围扫描,逻辑读从数万块降到几十块,响应时间通常改善 1~3 个数量级。
-- 确认是否走索引
EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 反复执行后看真实统计
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL,NULL,'ALLSTATS LAST'));
-- 查看是否存在可用函数索引
SELECT index_name, column_expression FROM user_ind_expressions;
-- 索引类型可另查 USER_INDEXES.INDEX_TYPE
警惕隐式类型转换导致索引失效
高优先级- 问题现象
- 字段是 VARCHAR2,传参传了数字;字段是 DATE,传参传了字符串,索引静默失效。
- 业务场景
- 订单表 t_order(order_id VARCHAR2(32) PRIMARY KEY, cust_id VARCHAR2(20), amount NUMBER)。前端下单接口传入的 cust_id 被框架反序列化成数字 1000001,SQL 里绑成了 NUMBER 类型。查该客户订单时全表扫描。
-- cust_id 是 VARCHAR2,传入 NUMBER → Oracle 对列做 TO_NUMBER 转换
SELECT * FROM t_order WHERE cust_id = 1000001;
-- 另一种:绑定变量类型不匹配
-- JDBC 中 setInt(1, 1000001) 而列是 VARCHAR2
-- 保证参数类型与列类型一致
SELECT * FROM t_order WHERE cust_id = '1000001';
-- JDBC 中显式使用 setString
-- ps.setString(1, "1000001");
-- 或由框架统一对齐参数类型
原理说明数据类型优先级规则下,Oracle 会把「索引列」向更高优先级类型转换(VARCHAR2 向 NUMBER 转),相当于对列套了 TO_NUMBER,索引失效。VARCHAR2 与 NVARCHAR2、DATE 与字符串之间同理。这类问题最隐蔽的地方是 SQL 文本看不出异常,必须看执行计划。
示例效果(非实测)索引失效是典型「一行代码拖垮一个页面」的元凶,修正后通常从秒级回到毫秒级,且能显著降低 buffer gets 与 CPU 消耗。
-- V$SQL_BIND_CAPTURE 查看绑定变量的真实类型
SELECT sql_id, name, datatype_string, value_string
FROM v$sql_bind_capture WHERE sql_id = '&sql_id';
-- 执行计划中若出现 INTERNAL_FUNCTION 包住索引列,即为隐式转换
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('&sql_id', NULL, 'ALLSTATS LAST'));
复合索引遵循最左前缀原则
高优先级- 问题现象
- 建了复合索引却用不上,或只用了前导列导致过滤不充分。
- 业务场景
- 物流系统轨迹表 t_track(track_id, waybill_no, scan_time, city_code, status),业务有两个典型查询:①按运单号+扫描时间查轨迹;②按城市+状态查异常件。开发建了一个索引 idx_a(waybill_no, scan_time, city_code, status)。查询②完全用不上。
-- 查询②:前导列 waybill_no 未参与条件,索引无法定位
SELECT * FROM t_track WHERE city_code = '310100' AND status = 'EXCEPTION';
-- 结果:INDEX SKIP SCAN 或 FULL TABLE SCAN
-- 按真实访问路径分别建索引,前导列选区分度高且必用的列
CREATE INDEX idx_track_waybill ON t_track(waybill_no, scan_time); -- 查询①
CREATE INDEX idx_track_city_status ON t_track(city_code, status, scan_time); -- 查询②
-- 若查询②还带时间范围,把时间放最后做范围扫描
SELECT * FROM t_track
WHERE city_code = '310100' AND status = 'EXCEPTION'
AND scan_time >= SYSDATE - 1;
原理说明B 树索引按列顺序逐层排序,只能从最左列开始做等值/范围定位。跳过前导列就失去了树的导航能力(除非用 INDEX SKIP SCAN,但效率远低于正常前缀扫描,且要求前导列区分度低)。
示例效果(非实测)从全表扫描变为索引范围扫描;若查询②数据占比小,逻辑读可下降 90% 以上。
-- 检查索引前导列与选择性
SELECT index_name, column_position, column_name FROM user_ind_columns
WHERE table_name='T_TRACK' ORDER BY index_name, column_position;
-- 查看列的选择性(越接近 1 越适合做前导列)
SELECT COUNT(DISTINCT city_code)/COUNT(*) sel_city, COUNT(DISTINCT waybill_no)/COUNT(*) sel_wb
FROM t_track;
低区分度列不适合单独建索引
中优先级- 问题现象
- 在状态、性别、是否删除这类列上建索引,优化器根本不用,反而拖慢 DML。
- 业务场景
- 工单表 t_ticket(ticket_id, status, is_deleted, create_time)。status 取值只有 5 种(待处理/处理中/已完成/已关闭/已取消),且 90% 的数据是「已完成」。开发给 status 单独建了索引,结果查询仍然全表扫描,同时插入性能下降。
CREATE INDEX idx_ticket_status ON t_ticket(status);
-- 查询仍走全表扫描,且每次 INSERT/UPDATE 都要维护这个无用索引
-- 方案一:组合索引,把低区分度列作为后续列
CREATE INDEX idx_ticket_dept_status ON t_ticket(dept_id, status, create_time);
-- 方案二:若只关心少量「活跃」数据,建函数索引(部分索引效果)
CREATE INDEX idx_ticket_active ON t_ticket(CASE WHEN status IN ('待处理','处理中') THEN status END);
SELECT * FROM t_ticket
WHERE CASE WHEN status IN ('待处理','处理中') THEN status END IS NOT NULL;
-- 方案三:数据量不大就别建,直接全表扫描更快
原理说明当某值占比超过约 20%~30% 时,走索引再回表读随机块的成本高于直接全表扫描的顺序读。CBO 会算出索引路径代价更高从而放弃索引。索引不是越多越好,每个索引都会增加 DML 维护开销与存储。
示例效果(非实测)删除无用索引后,高频写入表的 DML 响应时间可改善 10%~30%,同时减少索引段空间占用。
-- 找出从未被使用过的索引
SELECT index_name, table_name FROM v$object_usage; -- 需先 ALTER INDEX ... MONITORING USAGE
-- 或从游标缓存反查索引使用情况
SELECT * FROM v$sql_plan WHERE object_name = 'IDX_TICKET_STATUS' AND object_type='INDEX';
索引过多拖慢写入,需定期清理
中优先级- 问题现象
- 一个表挂了十几个索引,批量导入时慢得离谱,单条 INSERT 也要几十毫秒。
- 业务场景
- 数据中台宽表 dw_order_detail 有 42 个字段,历史上不同需求方各自加索引,累计 14 个索引。每日凌晨跑 200 万行批量导入,耗时从 8 分钟涨到 55 分钟,导致下游 ETL 全部延迟。
-- 历史遗留,无人清理
CREATE INDEX idx1 ...; CREATE INDEX idx2 ...; ... CREATE INDEX idx14 ...;
-- 每个索引都要在导入时更新 B 树、生成 redo/undo
-- 1) 开启监控,观察一个业务周期
ALTER INDEX idx_dw_01 MONITORING USAGE;
-- 2) 一个周期后查看使用情况
SELECT index_name, used, start_monitoring, end_monitoring FROM v$object_usage;
-- 3) 确认无用后逐个清理
DROP INDEX idx_dw_09;
-- 4) 批量导入前后可临时设为不可用再重建(谨慎评估)
-- ALTER INDEX idx_dw_10 UNUSABLE; -- 导入后 REBUILD
原理说明每多一个索引,一次 DML 就要多维护一棵 B 树,写入放大效应线性叠加,同时产生更多 redo 与 undo。写多读少的表,索引数量必须严格克制。
示例效果(非实测)14 个索引精简到 5 个后,200 万行批量导入耗时从 55 分钟降到约 18 分钟,ETL 窗口恢复正常。
SELECT table_name, COUNT(*) idx_cnt FROM user_indexes
GROUP BY table_name HAVING COUNT(*) > 8 ORDER BY 2 DESC;
-- 索引占用空间排名
SELECT segment_name, ROUND(bytes/1024/1024,1) mb FROM user_segments
WHERE segment_type='INDEX' ORDER BY bytes DESC FETCH FIRST 20 ROWS ONLY;
正确理解索引范围扫描与排序消除
中优先级- 问题现象
- ORDER BY 仍然产生 SORT ORDER BY,排序成为瓶颈。
- 业务场景
- 消息中心 t_message(msg_id, user_id, create_time, content),列表页按用户查询最近消息:WHERE user_id=? ORDER BY create_time DESC。索引建的是 idx_user(create_time, user_id),排序无法消除。
CREATE INDEX idx_msg_a ON t_message(create_time, user_id);
SELECT * FROM t_message WHERE user_id = 1001 ORDER BY create_time DESC;
-- 执行计划出现 SORT ORDER BY,大量临时表空间 I/O
-- 把「等值条件列」放前,「排序列」紧跟其后,即可消除排序
CREATE INDEX idx_msg_b ON t_message(user_id, create_time DESC);
SELECT * FROM t_message WHERE user_id = 1001 ORDER BY create_time DESC;
-- 执行计划变为 INDEX RANGE SCAN DESCENDING,无 SORT
原理说明索引本身就是有序结构。当 WHERE 等值列构成索引前导前缀、ORDER BY 列紧随其后时,命中的索引区间天然有序,优化器可省去排序操作。若排序列在前导位置而条件列在后,则无法保证过滤后的数据有序。
示例效果(非实测)消除排序后,翻页查询不再写临时表空间,大数据量列表页响应时间可下降 50%~90%。
-- 确认计划中无 SORT ORDER BY,且是 INDEX RANGE SCAN DESCENDING
EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 查看排序相关统计
SELECT name, value FROM v$sysstat WHERE name LIKE 'sorts%';
索引监视与碎片重建的正确时机
中优先级- 问题现象
- 索引被误判为「碎片严重」而频繁重建,白天重建导致业务抖动。
- 业务场景
- 运维手册要求每月重建所有索引。某次白天对 8000 万行的 t_order 执行 ALTER INDEX REBUILD,锁等待导致线上订单创建接口大面积超时。
-- 盲目重建,且未评估是否真的需要
ALTER INDEX idx_order_cust REBUILD;
-- 高峰期执行,且 ONLINE 未加,阻塞 DML
-- 1) 先判断 B 树是否真的失衡(BLEVEL 与碎片率)
SELECT index_name, blevel, leaf_blocks, num_rows, distinct_keys,
ROUND((del_lf_rows/decode(lf_rows,0,1,lf_rows))*100,2) del_pct
FROM index_stats; -- 需先 ANALYZE INDEX idx_order_cust VALIDATE STRUCTURE;
-- 2) 确需重建时,加 ONLINE 避免锁表,并放在低峰窗口
ALTER INDEX idx_order_cust REBUILD ONLINE;
-- 3) 或改用 coalesce 收缩碎片(更轻量)
ALTER INDEX idx_order_cust COALESCE;
原理说明B 树索引有自平衡能力,常规 DML 产生的碎片影响有限。只有当 del_lf_rows/lf_rows 比例很高(如超过 20%)且 BLEVEL 异常增长时才需要重建。REBUILD 不加 ONLINE 会对表加锁,阻塞所有 DML,风险远大于碎片收益。
示例效果(非实测)避免白天重建锁表事故;按需重建可减少 70% 以上的无效重建工时,碎片治理更精准。
ANALYZE INDEX idx_order_cust VALIDATE STRUCTURE;
SELECT name, height, blocks, lf_blks, del_lf_rows, lf_rows FROM index_stats;
-- 也可见 v$segment_statistics 观察逻辑读变化趋势
利用索引组织表与覆盖索引减少回表
低优先级- 问题现象
- SQL 只查几个字段,却因回表产生大量随机 I/O。
- 业务场景
- 报表按商品统计销量,SQL 只用到 sku_id 与 qty:SELECT sku_id, SUM(qty) FROM t_order_item WHERE sku_id IN (...) GROUP BY sku_id。索引 idx_sku(sku_id) 命中后每行都要回表取 qty,随机读很多。
CREATE INDEX idx_item_sku ON t_order_item(sku_id);
SELECT sku_id, SUM(qty) FROM t_order_item WHERE sku_id = 'S001' GROUP BY sku_id;
-- 回表取 qty,随机 I/O 大
-- 把查询涉及的列全部放进索引,形成覆盖索引,无需回表
CREATE INDEX idx_item_sku_qty ON t_order_item(sku_id, qty);
SELECT sku_id, SUM(qty) FROM t_order_item WHERE sku_id = 'S001' GROUP BY sku_id;
-- 计划显示 INDEX RANGE SCAN,无 TABLE ACCESS BY INDEX ROWID
原理说明普通二级索引的叶子节点只存索引列 + ROWID。查询若需要其他列,必须靠 ROWID 回表读数据块,每行一次随机 I/O。覆盖索引让所需数据全部在索引叶子节点中,直接读索引即可返回结果。
示例效果(非实测)消除回表后,随机 I/O 降为 0,聚合类查询在大表上常见 3~10 倍提速。
-- 计划中若没有 TABLE ACCESS BY INDEX ROWID,即为覆盖索引生效
EXPLAIN PLAN FOR SELECT ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 观察 consistent gets 是否显著下降
父键会更新或删除时优先为外键列建索引
低优先级- 问题现象
- 主表删除或更新主键时,子表因缺少外键索引而长时间锁表。
- 业务场景
- 订单表 t_order 与订单明细 t_order_item 通过 order_id 关联,且有外键约束。清理历史订单时若子表 order_id 无索引,Oracle 可能扫描子表并对其加表级锁,从而影响明细表上的并发 DML。
-- 子表无外键索引,删除父行时需全表扫描子表校验外键
ALTER TABLE t_order_item ADD CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES t_order(order_id);
-- order_id 上没有索引
-- 子表外键列建立索引
CREATE INDEX idx_item_order ON t_order_item(order_id);
-- 大批量删除改为分区级操作更稳妥
ALTER TABLE t_order DROP PARTITION p_2024q1 UPDATE GLOBAL INDEXES;
原理说明删除/更新父表被引用键时,Oracle 必须检查子表是否存在引用行,若无索引则需全表扫描子表,并对全表加锁(TM 锁),锁范围大、耗时长,极易引发连锁阻塞。
示例效果(非实测)有外键索引时,父键删除或更新通常无需对子表加全表锁,可降低并发阻塞风险;具体收益取决于数据量和操作频率。
-- 找出缺失索引的外键
SELECT c.table_name, c.constraint_name, cc.column_name
FROM user_constraints c JOIN user_cons_columns cc ON c.constraint_name = cc.constraint_name
WHERE c.constraint_type = 'R'
AND NOT EXISTS (SELECT 1 FROM user_ind_columns i
WHERE i.table_name = cc.table_name AND i.column_name = cc.column_name);