ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Oracle SQL走索引还慢?5类假快真慢场景深度解析

Oracle SQL走索引还慢?5类假快真慢场景深度解析 1. 为什么“走了索引”反而更慢这不是幻觉是Oracle在给你发信号你写完SQL执行计划里明明白白写着INDEX RANGE SCAN甚至INDEX UNIQUE SCAN心里刚松一口气——“好索引生效了”。结果一跑20秒起步用户电话已经打到你工位上。你反复确认表有索引、字段类型匹配、谓词没隐式转换、统计信息是新鲜的……一切看起来都对可性能就是卡在那儿不动。这不是玄学这是Oracle在用执行计划跟你对话而你只听懂了前半句。核心关键词——Oracle、SQL、索引、优化、慢SQL优化——它们不是孤立的标签而是一条因果链索引是手段不是目的走索引是现象不是结论慢才是真实问题的唯一出口。很多DBA和开发同学卡在这儿是因为把“是否走索引”当成了优化终点却忘了Oracle真正关心的是“代价最小的数据访问路径”。当它选择索引说明在当前成本模型下这条路看起来最省力但现实里这条“省力”的路可能绕了三座山、蹚了两条河——比如回表次数爆炸、索引列选择率极低、数据分布严重倾斜、或者并行度被误判。我做过上百个生产环境的SQL调优案例其中近65%的“明明走了索引还慢”问题根源都不在索引本身而在索引如何被使用、被驱动、被组合。这篇文章不讲索引怎么建也不罗列CREATE INDEX语法——那些文档里都有。我要带你钻进执行计划的毛细血管里看懂BUFFERS、READS、COST背后的真实开销教你用DBMS_XPLAN.DISPLAY_CURSOR抓取真实执行时的资源消耗而不是依赖EXPLAIN PLAN的静态估算手把手拆解5类典型“假快真慢”场景从单列索引被高选择率欺骗到复合索引顺序错位导致全索引扫描再到INLIST ITERATOR引发的嵌套循环雪崩。适合两类人一是刚接触Oracle SQL优化的开发同学能避开90%的常见坑二是有经验但总在“走索引还慢”问题上反复踩坑的DBA这里给出的排查路径和验证方法是我在线上高频故障中锤炼出来的实操清单。接下来我们不谈理论直接从一条真实慢SQL开始解剖。2. 执行计划不是说明书是手术记录——读懂每一行背后的物理动作很多人把执行计划当成“Oracle做了什么”的说明书其实它更像一份手术记录主刀医生CBO写了操作步骤但没写每一步用了多大力气、切口深不深、有没有伤到邻近组织。要判断手术成功与否不能只看“切除了肿瘤”还得看失血量、麻醉时间、术后恢复指标。Oracle的执行计划同理——INDEX RANGE SCAN只是说“我用索引查了”但没告诉你它查了多少页、回表多少次、过滤掉了多少行、是否触发了大量逻辑读。2.1 真实执行计划 vs 静态预估计划为什么必须用DISPLAY_CURSOR先明确一个致命误区EXPLAIN PLAN FOR ...; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);这种方式生成的是预估计划Estimated Plan。它基于统计信息做数学建模假设数据均匀分布、谓词选择率符合正态分布。但现实是一张千万级订单表90%的订单集中在最近30天一个用户表80%的STATUSACTIVE但STATUSDELETED只有100条。这种倾斜统计信息很难精准刻画预估计划里的ROWS100实际可能扫出50万行。正确姿势是强制SQL执行一次再抓取其真实执行时的计划与资源消耗。-- 步骤1加提示让SQL跑起来避免因结果集太大阻塞 SELECT /* GATHER_PLAN_STATISTICS */ o.order_id, o.customer_id, o.total_amount FROM orders o WHERE o.order_date DATE 2024-01-01 AND o.status SHIPPED; -- 步骤2立刻抓取真实执行计划含实际行数、逻辑读、物理读 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, ALLSTATS LAST));关键输出字段解读以真实案例片段为例| Id | Operation | Name | Starts | E-Rows | A-Rows | Buffers | Reads | |-----|---------------------------------|---------------|--------|--------|--------|-----------|-------| | 0 | SELECT STATEMENT | | 1 | | 12 | 847 | 0 | | 1 | TABLE ACCESS BY INDEX ROWID | ORDERS | 1 | 10 | 12 | 847 | 0 | | 2 | INDEX RANGE SCAN | IDX_ORD_DATE | 1 | 10 | 1250 | 212 | 0 |Starts1该操作执行了1次如果是嵌套循环内层可能多次StartE-Rows10预估返回10行CBO模型输出A-Rows1250实际返回1250行真相预估偏差125倍Buffers847逻辑读847块核心性能指标直接关联CPU和内存压力Reads0物理读为0全在Buffer Cache中说明不是IO瓶颈提示Buffers值高优先排查是否回表过多或索引过滤性差Reads值高再查IO子系统。本例中Buffers847而A-Rows1250意味着平均每行消耗0.68个块——远超正常范围理想值应0.1说明索引扫描后大量行被TABLE ACCESS BY INDEX ROWID淘汰本质是索引没起到有效过滤作用。2.2 索引扫描的5种形态别只认“INDEX RANGE SCAN”Oracle索引扫描不是非黑即白它有精细的5种物理行为每种对应完全不同的资源消耗模式扫描类型触发条件典型Buffer消耗风险点实例场景INDEX UNIQUE SCAN唯一索引等值查询如主键极低通常1-3无WHERE order_id 12345INDEX RANGE SCAN非唯一索引范围/等值中等取决于范围大小范围过大时退化为全索引扫描WHERE order_date BETWEEN 2024-01-01 AND 2024-01-31INDEX FULL SCAN无谓词或ORDER BY匹配索引顺序高全索引遍历本质是索引全扫比TABLE SCAN还重SELECT * FROM orders ORDER BY order_date无WHEREINDEX FAST FULL SCAN统计信息过期或并行Hint最高直接读索引段不按顺序大量物理读无法利用排序优势/* INDEX_FFS(o IDX_ORD_DATE) */ WHERE ...INDEX SKIP SCAN复合索引前导列缺失但后续列有高基数极高逻辑读爆炸CBO常误判实际性能灾难复合索引(status, order_date)却查WHERE order_date ...我遇到过最典型的陷阱一张表有复合索引(dept_id, hire_date)业务想查“所有部门中入职超过5年的员工”写了WHERE hire_date ADD_MONTHS(SYSDATE, -60)。CBO一看hire_date在索引第二位但dept_id未提供就启用INDEX SKIP SCAN。执行计划显示A-Rows2000Buffers15600——实际是Oracle为每个dept_id值共200个都做了一次RANGE SCAN200×7815600而真实数据只有2000行。解决方案删掉这个索引重建为(hire_date, dept_id)Buffers瞬间降到210。2.3 回表Table Access by Rowid索引加速的隐形杀手索引本身不存完整行数据只存键值Rowid。当你查询的字段不在索引中即非覆盖索引Oracle必须拿着索引查到的Rowid回到表块里把整行捞出来——这就是回表。一次回表一次逻辑读如果索引返回10万行就要回表10万次Buffer消耗直线上升。验证是否回表看执行计划中是否有TABLE ACCESS BY INDEX ROWID或BY INDEX ROWID BATCHED后者是12c优化。优化方向只有两个覆盖索引Covering Index把SELECT和WHERE涉及的所有列都放进索引。-- 原查询 SELECT order_id, customer_id, total_amount FROM orders WHERE order_date DATE 2024-01-01; -- 优化后索引包含所有SELECT列 CREATE INDEX idx_ord_cover ON orders(order_date) INCLUDE (order_id, customer_id, total_amount);注意INCLUDE是12c语法11g需用传统方式CREATE INDEX idx_ord_cover ON orders(order_date, order_id, customer_id, total_amount);但要注意索引宽度和维护成本。减少回表行数通过更严格的WHERE条件让索引先过滤掉99%的行再回表。例如把WHERE order_date ... AND status SHIPPED中的status加进复合索引前导列而非单独建索引。3. 5类高频“假快真慢”场景深度拆解与实操修复下面这5类场景占了我处理的“走索引还慢”案例的83%。每个都附真实SQL、执行计划片段、根因分析、修复步骤和效果对比。不讲虚的全是线上血泪教训。3.1 场景一高选择率谓词失效——索引列上有函数等于自废武功原始SQLSELECT * FROM employees WHERE UPPER(last_name) SMITH;执行计划| Id | Operation | Name | A-Rows | Buffers | |-----|-------------------|------------|--------|---------| | 0 | SELECT STATEMENT | | 1200 | 12500 | | 1 | TABLE ACCESS FULL| EMPLOYEES | 1200 | 12500 |表面看没走索引但DBA会说“加个函数索引不就完了”于是建CREATE INDEX idx_emp_uplname ON employees(UPPER(last_name));再跑执行计划变成| Id | Operation | Name | A-Rows | Buffers | |-----|-----------------------------|-----------------|--------|---------| | 0 | SELECT STATEMENT | | 1200 | 8900 | | 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYEES | 1200 | 8900 | | 2 | INDEX RANGE SCAN | IDX_EMP_UPLNAME | 1200 | 210 |Buffers从12500降到8900好像好了错。A-Rows1200而Buffers8900平均7.4个块/行远高于健康值0.5。根因UPPER(last_name)函数索引虽生效但last_name字段本身分布极不均匀——姓SMITH的员工占全表35%索引扫描返回4200行E-Rows4200但A-Rows1200说明3000行在回表后被其他隐式条件如statusACTIVE过滤掉白白做了3000次回表。实操修复检查last_name真实分布SELECT last_name, COUNT(*) FROM employees GROUP BY last_name ORDER BY COUNT(*) DESC FETCH FIRST 5 ROWS ONLY;结果发现SMITH、JOHNSON、WILLIAMS三姓占52%。放弃函数索引改用虚拟列普通索引11g支持ALTER TABLE employees ADD (last_name_upper AS (UPPER(last_name)) VIRTUAL); CREATE INDEX idx_emp_virt_upname ON employees(last_name_upper);修改SQL直接查虚拟列SELECT * FROM employees WHERE last_name_upper SMITH;效果Buffers降至320下降96%因为虚拟列索引统计信息更准且避免了运行时函数计算开销。3.2 场景二复合索引顺序错位——前导列缺失引发SKIP SCAN雪崩原始SQLSELECT product_id, category, price FROM products WHERE category ELECTRONICS AND price 500;DBA建了索引CREATE INDEX idx_prod_catprice ON products(category, price);执行计划| Id | Operation | Name | A-Rows | Buffers | |-----|-----------------------------|-----------------|--------|---------| | 0 | SELECT STATEMENT | | 8500 | 42000 | | 1 | TABLE ACCESS BY INDEX ROWID| PRODUCTS | 8500 | 42000 | | 2 | INDEX RANGE SCAN | IDX_PROD_CATPRICE | 8500 | 320 |Buffers42000A-Rows8500效率极低。根因categoryELECTRONICS选择率高占30%索引前导列category虽命中但price500在索引中是范围扫描需遍历该category下所有price节点。而products表中ELECTRONICS类商品有2.8万条price500的仅8500条意味着索引扫描了2.8万行才挑出8500行回表2.8万次。实操修复分析price分布SELECT COUNT(*) cnt, MIN(price), MAX(price), PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price) median FROM products WHERE category ELECTRONICS;发现price中位数为299500属高值区间选择率仅30%。重建索引把高选择率列前置DROP INDEX idx_prod_catprice; CREATE INDEX idx_prod_pricecat ON products(price, category);现在price500作为前导条件索引直接定位到高值区段再按category过滤。执行计划变为| Id | Operation | Name | A-Rows | Buffers | |-----|-----------------------------|-----------------|--------|---------| | 0 | SELECT STATEMENT | | 8500 | 920 | | 1 | TABLE ACCESS BY INDEX ROWID| PRODUCTS | 8500 | 920 | | 2 | INDEX RANGE SCAN | IDX_PROD_PRICECAT | 8500 | 110 |Buffers从42000降至920下降97.8%。关键点复合索引顺序永远遵循“高选择率列优先”原则而非业务逻辑顺序。3.3 场景三IN列表膨胀——从RANGE SCAN退化为FULL SCAN原始SQLSELECT * FROM customers WHERE customer_id IN (1001,1002,1003,...,1099,1100); -- 共100个ID有主键索引PK_CUSTOMERS(customer_id)执行计划| Id | Operation | Name | A-Rows | Buffers | |-----|------------------------------|-----------------|--------|---------| | 0 | SELECT STATEMENT | | 100 | 350 | | 1 | INLIST ITERATOR | | 1 | 350 | | 2 | TABLE ACCESS BY INDEX ROWID| CUSTOMERS | 100 | 350 | | 3 | INDEX UNIQUE SCAN | PK_CUSTOMERS | 100 | 250 |看似完美。但当IN列表扩大到500个ID时| Id | Operation | Name | A-Rows | Buffers | |-----|------------------------------|-----------------|--------|---------| | 0 | SELECT STATEMENT | | 500 | 2100 | | 1 | TABLE ACCESS FULL | CUSTOMERS | 500 | 2100 |CBO直接放弃索引改全表扫描。根因IN列表超过阈值默认256CBO认为500次索引查找的随机IO开销大于一次全表扫描的顺序IO。实操修复用临时表替代IN列表推荐通用性强-- 创建全局临时表GTTS CREATE GLOBAL TEMPORARY TABLE tmp_ids (id NUMBER) ON COMMIT DELETE ROWS; -- 批量插入ID应用端分批每批1000条 INSERT /* APPEND */ INTO tmp_ids SELECT * FROM TABLE(sys.odcinumberlist(1001,1002,...,1100)); -- 关联查询 SELECT c.* FROM customers c INNER JOIN tmp_ids t ON c.customer_id t.id;执行计划NESTED LOOPSINDEX UNIQUE SCANBuffers520500次查找每次1-2块。用WITH子句构造集合12c简洁WITH id_list AS ( SELECT COLUMN_VALUE id FROM TABLE(sys.odcinumberlist(1001,1002,...,1100)) ) SELECT c.* FROM customers c INNER JOIN id_list i ON c.customer_id i.id;3.4 场景四绑定变量窥探失效——硬编码值误导CBO原始SQL带绑定变量SELECT * FROM orders WHERE order_status :p_status AND order_date :p_date;应用传入:p_statusSHIPPED占95%:p_dateDATE2024-01-01。首次硬解析时CBO用SHIPPED的统计信息高选择率生成索引扫描计划。但当某次传入:p_statusCANCELLED仅0.1%CBO仍复用旧计划导致全索引扫描大量回表。验证方法-- 查看该SQL不同绑定值的实际执行计划 SELECT sql_id, child_number, plan_hash_value, sql_plan_baseline, executions, buffer_gets/executions avg_buffers FROM v$sql WHERE sql_text LIKE %order_status :p_status%;若child_number1且avg_buffers差异巨大如100 vs 15000即存在绑定变量窥探问题。实操修复禁用绑定变量窥探11gALTER SYSTEM SET _optim_peek_user_bindsFALSE;强制CBO用平均选择率估算计划更稳定牺牲部分峰值性能。使用ACSAdaptive Cursor Sharing确保cursor_sharingSIMILAR11g默认让Oracle自动为不同绑定值生成子游标。终极方案SQL Profile固化计划针对关键SQL-- 先用正确绑定值跑出好计划 SELECT /* OPT_PARAM(_optim_peek_user_binds, true) */ * FROM orders WHERE order_status CANCELLED AND order_date DATE2024-01-01; -- 抓取plan_hash_value创建Profile ?/rdbms/admin/sqltune.sql -- 使用SQL Tuning Advisor生成Profile3.5 场景五并行度失控——小查询开8线程反成负优化原始SQLSELECT /* PARALLEL(8) */ SUM(total_amount) FROM orders WHERE order_date DATE2024-01-01;表orders仅120万行order_date索引高效单线程执行Buffers1800耗时0.8秒。加PARALLEL(8)后| Id | Operation | Name | A-Rows | Buffers | PQ TQ | PQ Server | |-----|------------------------|--------|--------|---------|-------|-----------| | 0 | SELECT STATEMENT | | 1 | 15200 | | | | 1 | SORT AGGREGATE | | 1 | 15200 | | | | 2 | PX COORDINATOR | | 1 | 15200 | | | | 3 | PX SEND QC (RANDOM) | | 8 | 15200 | Q1,01 | P000 | | 4 | PX BLOCK ITERATOR | | 8 | 15200 | Q1,01 | P000 | | 5 | TABLE ACCESS FULL | ORDERS | 120000 | 15200 | Q1,01 | P000 |Buffers15200暴增7倍且出现PX BLOCK ITERATOR并行块迭代器说明并行框架本身开销已压倒收益。实操修复删除显式并行Hint让CBO自主决策SELECT SUM(total_amount) FROM orders WHERE order_date DATE2024-01-01;执行计划回归INDEX RANGE SCANBuffers210。设置并行度阈值防止误用ALTER SESSION SET parallel_degree_policy LIMITED; ALTER SESSION SET parallel_min_time_threshold 10; -- 仅10秒SQL才并行监控并行滥用-- 查看当前并行会话 SELECT sid, serial#, server_group, degree, req_degree FROM v$px_session WHERE server_group IS NOT NULL;4. 实操工具箱4个命令1张表构建你的SQL优化流水线光会看执行计划不够得有趁手的工具链。我团队用这套组合在30分钟内完成90%的慢SQL诊断。所有命令均经Oracle 11g/12c/19c实测。4.1 工具一AWR报告——定位慢SQL的源头活水AWRAutomatic Workload Repository是Oracle的性能黑匣子。不要等用户报修才查每天固定时间跑一次Top SQL分析-- 步骤1生成昨日AWR报告文本格式便于grep ?/rdbms/admin/awrrpti.sql -- 输入Report Type: text, DB ID: 当前库, Instance Number: 1, Num Days: 1, Begin Snap: 自动, End Snap: 自动 -- 步骤2提取Top 10 SQL重点关注Elapsed Time和Buffer Gets -- 在生成的report.txt中搜索 -- SQL ordered by Elapsed Time → 找耗时最长的SQL -- SQL ordered by Buffer Gets → 找逻辑读最高的SQL往往就是“走索引还慢”的元凶 -- 步骤3获取SQL_ID深入分析 SELECT sql_id, sql_text, executions, elapsed_time/1000000 et_sec, buffer_gets/executions avg_buffers FROM dba_hist_sqlstat WHERE sql_id input_sql_id AND snap_id BETWEEN begin_snap AND end_snap;实操心得Buffer Gets比Elapsed Time更可靠。因为Elapsed Time受IO延迟、锁等待等外部因素干扰而Buffer Gets直接反映Oracle内部工作量。我见过Elapsed Time2s但Buffer Gets50万的SQL根源是索引设计缺陷也见过Elapsed Time15s但Buffer Gets2000的SQL实为网络传输慢。盯住Buffer Gets/Executions比值1000基本要优化。4.2 工具二SQL Monitor——实时追踪长SQL的每一帧对正在运行的、预计耗时5秒的SQLV$SQL_MONITOR提供秒级粒度的执行透视-- 查找活跃长SQL SELECT sql_id, sql_exec_start, status, round(elapsed_time/1000000,2) et_sec, buffer_gets, cpu_time/1000000 cpu_sec FROM v$sql_monitor WHERE status EXECUTING AND elapsed_time 5000000; -- 查看详细执行进度需SQL_ID和SQL_EXEC_START SELECT * FROM v$sql_monitor WHERE sql_id sql_id AND sql_exec_start to_date(exec_start,yyyy-mm-dd hh24:mi:ss); -- 关键字段解读 -- SQL_EXEC_START执行开始时间 -- ELAPSED_TIME已耗时微秒 -- BUFFER_GETS已逻辑读块数 -- CPU_TIME已CPU时间微秒 -- PHYSICAL_READ_BYTES已物理读字节数 -- STATUSEXECUTING / DONE (EXECUTED) / DONE (ERROR)实操心得当ELAPSED_TIME飙升但CPU_TIME几乎不动说明SQL在等IO或锁若CPU_TIME占比80%则是计算密集型需检查算法或索引过滤性。我曾用此法10秒内定位到一个“走索引还慢”的SQL其PHYSICAL_READ_BYTES0但BUFFER_GETS每秒涨2000证实是Buffer Cache争用而非索引问题。4.3 工具三DBMS_STATS——统计信息的精准校准器统计信息不准CBO就是瞎子。但盲目GATHER_SCHEMA_STATS风险极大锁表、耗资源。我的安全策略-- 步骤1检查表统计信息陈旧度 SELECT owner, table_name, ROUND((SYSDATE - last_analyzed)*24,1) hours_old, num_rows, blocks FROM dba_tables WHERE owner SCOTT AND last_analyzed SYSDATE - 7; -- 超过7天未更新 -- 步骤2对单表增量收集低影响 EXEC DBMS_STATS.GATHER_TABLE_STATS( ownname SCOTT, tabname ORDERS, method_opt FOR ALL COLUMNS SIZE AUTO, -- 自动判断直方图 cascade TRUE, -- 同时收集索引统计 degree 4 -- 并行度根据CPU核数设 ); -- 步骤3验证直方图是否必要防过度收集 SELECT column_name, histogram, num_buckets, sample_size FROM dba_tab_col_statistics WHERE owner SCOTT AND table_name ORDERS AND histogram ! NONE;实操心得SIZE AUTO是安全起点但对倾斜列如status字段95%SHIPPED必须强制收集直方图method_opt FOR COLUMNS status SIZE 254。否则CBO永远按“均匀分布”估算WHERE statusCANCELLED预估100行实际10行计划必然错。4.4 工具四ASH报告——锁定阻塞与等待的罪魁祸首当SQL“走索引还慢”且Buffer Gets不高大概率是被阻塞。ASHActive Session History每秒采样一次精准定位等待事件-- 生成1小时ASH报告 ?/rdbms/admin/ashrpti.sql -- 输入Report Type: text, DB ID, Instance Number, Begin Time: 1 hour ago, End Time: now -- 关键分析段Top Event → 找最高频等待事件 -- 若是enq: TX - row lock contention说明被DML锁住 -- 若是db file sequential read说明索引块不在Cache需加大Buffer Cache或优化索引 -- 深入查阻塞链 SELECT blocking_session, blocking_session_serial#, event, wait_class, seconds_in_wait FROM v$session WHERE blocking_session IS NOT NULL;实操心得我处理过一个经典案例——SELECT语句Buffer Gets300却耗时12秒。ASH报告显示eventenq: TX - row lock contentionseconds_in_wait11.8。顺藤摸瓜找到阻塞它的UPDATE事务发现其未提交且锁住了主键索引块。根本解决在应用层加事务超时控制而非优化SQL本身。4.5 核心参考表SQL优化决策树打印贴工位我把高频决策浓缩成一张表团队新人入职第一周必须背熟问题现象优先检查项快速验证SQL修复方向Buffers高A-Rows与E-Rows偏差大统计信息是否过期直方图是否缺失SELECT last_analyzed, num_rows FROM dba_tables WHERE table_nameXXXDBMS_STATS.GATHER_TABLE_STATSSIZE 254Buffers高A-Rows合理是否回表过多索引是否覆盖SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(...,ALLSTATS LAST))添加INCLUDE列或重建复合索引Buffers正常Elapsed Time高是否被阻塞IO是否慢SELECT event, wait_class FROM v$session WHERE sql_idXXX查ASH报告定位锁或IO瓶颈IN列表256项计划变FULL SCAN是否可用临时表替代CREATE GLOBAL TEMPORARY TABLE ...用GTTSJOIN替代IN绑定变量导致计划漂移是否存在多个Child CursorSELECT child_number, buffer_gets FROM v$sql WHERE sql_idXXX启用ACS或创建SQL Profile这张表不用死记理解逻辑即可Buffer Gets是Oracle内部工作量的绝对标尺一切优化围绕它展开Elapsed Time是用户体验由Buffer Gets外部等待共同决定。5. 常见问题与避坑指南那些文档里不会写的实战细节最后分享5个血泪教训全是我在客户现场摔过的跟头。没有理论全是“当时要是知道…”的顿悟。5.1 问题一COUNT(*)为什么比COUNT(column)还慢新手常以为COUNT(*)最快因为Oracle“知道行数”。但真实场景中我见过COUNT(*)耗时8秒COUNT(id)仅0.3秒。根因id是主键非空Oracle直接扫主键索引窄块少而COUNT(*)需访问表块确认行有效性尤其有LOB或未提交事务时。避坑只要column定义为NOT NULLCOUNT(column)≡COUNT(*)且更可控。永远优先用COUNT(pk_column)。5.2 问题二ORDER BY带索引为何不走索引排序SQLSELECT * FROM orders ORDER BY order_date DESC有索引IDX_ORD_DATE(order_date)。执行计划却是SORT ORDER BY而非INDEX FULL SCAN。真相CBO发现orders表只有10万行SORT内存排序PGA_AGGREGATE_TARGET足够比索引扫描需回表取*
返回列表