03-Oracle索引深入:不只是“加个索引就快了“

发布时间:2026/6/18 16:16:28

03-Oracle索引深入:不只是“加个索引就快了“ Oracle索引深入不只是加个索引就快了从一个常见误区说起很多人以为索引是万能的表慢了加索引。还是慢再加一个。但实际工作中你会发现有些SQL明明有索引却不走索引还是全表扫描有些SQL走了索引反而比全表扫描更慢有些索引建了之后查询没快多少INSERT反而变慢了索引不是银弹。理解索引的内部结构和工作原理才能知道什么时候该用、怎么用、为什么不用。B-Tree索引的内部结构Oracle默认的索引类型是B-Tree平衡树索引。理解它的结构是一切的基础。三层结构[根块 Root Block] / | \ [分支块] [分支块] [分支块] / \ / \ / \ [叶子] [叶子] [叶子] [叶子] [叶子] [叶子] ↓ ↓ ↓ ↓ ↓ ↓ ROWID ROWID ROWID ROWID ROWID ROWID根块Root Block索引的入口存放指向分支块的指针分支块Branch Block存放索引键值范围和指向下一层的指针叶子块Leaf Block存放实际的索引键值和对应的ROWID叶子块之间通过双向链表连接查找过程查询WHERE dept_id 10时读根块判断10在哪个分支范围内读分支块进一步缩小范围读叶子块找到dept_id10的所有ROWID回表根据ROWID去表里取完整行数据[!note] 关键理解B-Tree的高度BLEVEL决定了查找需要读多少次索引块。一般情况下小表几千行BLEVEL 0只有叶子块没有分支中等表几十万行BLEVEL 1根 叶子大表千万行以上BLEVEL 2-3根 分支 叶子BLEVEL很少超过4因为B-Tree的扇出很大一个块能存很多指针。为什么叶子块要双向链表因为范围查询需要顺序扫描。SELECT*FROMemployeesWHEREdept_idBETWEEN10AND30;找到dept_id10的叶子块后沿着链表向右扫描直到dept_id30为止。这就是INDEX RANGE SCAN的原理。回表的代价聚簇因子上一篇你问为什么索引扫描返回大量ROWID每个都要回表一次核心就是聚簇因子Clustering Factor。什么是聚簇因子衡量索引顺序和表的物理存储顺序的匹配程度。场景1聚簇因子低好索引顺序 1 2 3 4 5 6 7 8 9 10 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 表的块 [1,2,3] [4,5,6] [7,8,9] [10]索引顺序和表的存储顺序一致回表时大部分数据在同一个块里I/O少。场景2聚簇因子高差索引顺序 1 2 3 4 5 6 7 8 9 10 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 表的块 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]每次回表都要读不同的块I/O暴增。查看聚簇因子SELECTindex_name,clustering_factor,num_rows,leaf_blocksFROMuser_indexesWHEREtable_nameEMPLOYEES;判断标准clustering_factor接近表的块数blocks好数据排列紧凑clustering_factor接近表的行数num_rows差数据分散实际案例假设表有1000万行占10万个数据块。索引Aclustering_factor 120000接近块数索引扫描返回1000行可能只需要读120个数据块索引Bclustering_factor 9500000接近行数索引扫描返回1000行可能需要读1000个数据块这就是为什么同样的查询走不同的索引性能可能天差地别。如何改善聚簇因子方法1重建表按索引列排序-- 创建新表按dept_id排序CREATETABLEemployees_newASSELECT*FROMemployeesORDERBYdept_id;-- 删除旧表重命名新表DROPTABLEemployees;RENAMEemployees_newTOemployees;-- 重建索引CREATEINDEXemp_dept_ixONemployees(dept_id);方法2使用索引组织表IOTCREATETABLEemployees(employee_id NUMBERPRIMARYKEY,dept_id NUMBER,employee_name VARCHAR2(100),...)ORGANIZATIONINDEX;IOT的数据本身就按主键排序存储天然聚簇因子低。但有限制不适合频繁UPDATE的表。[!warning] 实战建议重建表是高风险操作需要停机维护窗口。大多数情况下接受聚簇因子高的现实通过其他方式优化比如覆盖索引、分区。覆盖索引避免回表如果查询的所有列都在索引中就不需要回表了。普通索引CREATEINDEXemp_dept_ixONemployees(dept_id);-- 这个查询需要回表SELECTemployee_name,salaryFROMemployeesWHEREdept_id10;索引里只有dept_id和ROWID要取employee_name和salary必须回表。覆盖索引复合索引CREATEINDEXemp_dept_name_sal_ixONemployees(dept_id,employee_name,salary);-- 这个查询不需要回表SELECTemployee_name,salaryFROMemployeesWHEREdept_id10;所有需要的列都在索引里直接从索引叶子块返回数据执行计划会显示INDEX FAST FULL SCAN或INDEX RANGE SCAN但没有TABLE ACCESS BY INDEX ROWID。性能提升减少I/O不读表的数据块减少Buffer Cache压力特别适合OLAP类查询读多写少代价索引变大存了更多列INSERT/UPDATE/DELETE变慢要维护更大的索引复合索引的列顺序复合索引的列顺序极其重要直接影响能否使用索引。最左前缀原则索引(A, B, C)可以支持WHERE A ?WHERE A ? AND B ?WHERE A ? AND B ? AND C ?但不能支持WHERE B ?跳过了AWHERE C ?跳过了A和BWHERE B ? AND C ?跳过了A为什么因为B-Tree索引是先按A排序A相同的再按B排序B相同的再按C排序。索引内容 (1, Alice, 100) (1, Bob, 200) (2, Charlie, 150) (2, David, 180)如果你查WHERE B Bob索引无法定位因为B的值在索引中不是连续的。列顺序的选择原则原则1等值条件的列放前面范围条件的列放后面-- 不好CREATEINDEXidx1ONorders(order_date,customer_id);SELECT*FROMordersWHEREcustomer_id100ANDorder_date2024-01-01;-- order_date是范围放前面导致customer_id的过滤效果差-- 好CREATEINDEXidx2ONorders(customer_id,order_date);-- 先用customer_id精确定位再在这个范围内扫描order_date原则2选择性高的列放前面选择性 DISTINCT值的数量 / 总行数-- 假设表有100万行-- gender列只有M/F两个值选择性 2/1000000 0.000002极低-- employee_id列100万个不同值选择性 1000000/1000000 1极高-- 不好CREATEINDEXidx1ONemployees(gender,employee_id);-- 好CREATEINDEXidx2ONemployees(employee_id,gender);原则3考虑实际查询模式如果90%的查询都是WHERE A ? AND B ?只有10%是WHERE A ?那就按(A, B)建索引不要为了那10%建两个索引。索引失效的常见场景有索引不代表会用索引。以下场景会导致索引失效1. 对索引列使用函数-- 索引失效SELECT*FROMemployeesWHEREUPPER(employee_name)JOHN;-- 索引有效SELECT*FROMemployeesWHEREemployee_nameJOHN;原因索引存的是employee_name的原始值不是UPPER(employee_name)的值。解决创建函数索引CREATEINDEXemp_name_upper_ixONemployees(UPPER(employee_name));2. 隐式类型转换-- dept_id是NUMBER类型-- 索引失效SELECT*FROMemployeesWHEREdept_id10;-- 字符串10-- 索引有效SELECT*FROMemployeesWHEREdept_id10;-- 数字10原因Oracle会隐式转换为TO_NUMBER(dept_id) 10相当于对索引列使用了函数。3. 使用NOT、!、-- 索引失效SELECT*FROMemployeesWHEREdept_id!10;SELECT*FROMemployeesWHEREdept_id10;SELECT*FROMemployeesWHERENOTdept_id10;原因B-Tree索引不存储NULL且不等于的范围太大优化器认为全表扫描更快。4. 使用OR连接不同列-- 索引失效假设dept_id和salary分别有索引SELECT*FROMemployeesWHEREdept_id10ORsalary50000;原因需要分别走两个索引再合并结果成本高。解决改写为UNIONSELECT*FROMemployeesWHEREdept_id10UNIONSELECT*FROMemployeesWHEREsalary50000;5. LIKE以通配符开头-- 索引失效SELECT*FROMemployeesWHEREemployee_nameLIKE%John%;SELECT*FROMemployeesWHEREemployee_nameLIKE%John;-- 索引有效SELECT*FROMemployeesWHEREemployee_nameLIKEJohn%;原因B-Tree索引是按字符串从左到右排序的%开头无法定位起始位置。解决如果必须模糊查询考虑全文索引Oracle Text。6. 数据分布导致优化器放弃索引-- 假设表有100万行其中95万行的statusACTIVESELECT*FROMordersWHEREstatusACTIVE;即使status有索引优化器也会选择全表扫描因为走索引扫描95万个ROWID回表95万次全表扫描顺序读所有数据块可能只需要读几万个块这不是索引失效而是优化器的正确决策。其他索引类型位图索引Bitmap Index适合低基数列distinct值很少的列如性别、状态、省份等。CREATEBITMAPINDEXemp_gender_bixONemployees(gender);优点存储空间小多个位图索引可以高效组合位运算缺点不适合高并发DMLUPDATE/INSERT/DELETE会锁整个位图段只适合数据仓库场景读多写少函数索引对列的函数结果建索引。CREATEINDEXemp_name_upper_ixONemployees(UPPER(employee_name));CREATEINDEXemp_hire_year_ixONemployees(EXTRACT(YEARFROMhire_date));反向键索引Reverse Key Index把索引键值反转后存储用于解决索引热块问题。CREATEINDEXemp_id_rev_ixONemployees(employee_id)REVERSE;场景序列生成的主键所有INSERT都集中在索引的最右边叶子块导致热块竞争。反转后值分散到不同叶子块。代价不支持范围查询BETWEEN、、。分区索引表分区后索引也可以分区。分为本地分区索引Local索引分区和表分区一一对应全局分区索引Global索引分区独立于表分区后面讲分区表时会详细展开。索引维护索引不是建了就不管了需要定期维护。索引碎片频繁的DML操作会导致索引碎片删除数据后索引叶子块的空间没有释放索引块的填充率下降导致索引变大、查询变慢检查索引健康度-- 分析索引ANALYZEINDEXemp_dept_ix VALIDATE STRUCTURE;-- 查看碎片情况SELECTname,height,lf_rows,del_lf_rows,(del_lf_rows/lf_rows*100)ASfragmentation_pctFROMindex_statsWHEREnameEMP_DEPT_IX;height索引高度BLEVEL 1lf_rows叶子块中的行数del_lf_rows已删除但未清理的行数fragmentation_pct碎片率重建索引-- 在线重建不锁表但需要额外空间ALTERINDEXemp_dept_ix REBUILD ONLINE;-- 离线重建锁表但更快ALTERINDEXemp_dept_ix REBUILD;什么时候需要重建碎片率超过20%-30%索引高度增加比如从2变成3聚簇因子严重恶化[!warning] 注意不要盲目定期重建所有索引。很多索引根本不需要重建反而浪费资源。只重建确实有问题的索引。索引设计的实战原则1. 不是所有列都需要索引需要索引的列WHERE条件中频繁出现的列JOIN连接列ORDER BY / GROUP BY的列外键列避免子表锁父表不需要索引的列选择性极低的列如性别除非是位图索引频繁更新的列小表几百行的表全表扫描更快2. 索引不是越多越好每个索引都有代价占用存储空间INSERT/UPDATE/DELETE时要维护索引降低写入性能优化器要评估更多执行计划增加解析时间经验值一张表的索引数量一般不超过5-6个。3. 定期检查无用索引-- 查看索引使用情况需要先开启监控ALTERINDEXemp_dept_ix MONITORINGUSAGE;-- 一段时间后查看SELECT*FROMv$object_usageWHEREindex_nameEMP_DEPT_IX;如果USED NO说明这个索引从未被使用过可以考虑删除。4. 考虑索引的维护成本OLTP系统高并发写入少建索引只建最必要的OLAP系统读多写少可以多建索引甚至覆盖索引思考题0_o假设你有一个查询SELECT * FROM orders WHERE customer_id 100 AND order_date 2024-01-01你会建(customer_id, order_date)还是(order_date, customer_id)的索引为什么提示回顾复合索引的列顺序原则。为什么说索引不是越多越好你在工作中见过因为索引太多导致的问题吗提示想想INSERT/UPDATE的性能。什么情况下即使有索引全表扫描反而更快提示回顾聚簇因子和数据分布。你们的生产环境有定期重建索引的策略吗是定期重建所有索引还是按需重建

相关新闻