performance30 分钟阅读
Oracle SQL 性能排查 100 条命令
SQL 性能问题在 Oracle 运维中很常见:同一条语句昨天还快,今天却跑了几分钟。原因可能在执行计划,也可能是锁、I/O、临时空间或并行执行。没有找到正在执行的游标,就直接重建索引或改参数,容易把问题带到别处。
2026年9月15日阅读—点赞—收藏—
dba100oraclescenario
100 条命令系列文章专栏
SQL 性能问题在 Oracle 运维中很常见:同一条语句昨天还快,今天却跑了几分钟。原因可能在执行计划,也可能是锁、I/O、临时空间或并行执行。没有找到正在执行的游标,就直接重建索引或改参数,容易把问题带到别处。
SQL 性能问题在 Oracle 运维中很常见:同一条语句昨天还快,今天却跑了几分钟。原因可能在执行计划,也可能是锁、I/O、临时空间或并行执行。没有找到正在执行的游标,就直接重建索引或改参数,容易把问题带到别处。
Oracle SQL 性能排查关系图
这篇按现场排查的顺序整理命令。先找会话和 SQL_ID,再看等待、执行次数和实际计划;涉及统计信息、索引及计划管理的操作放在后面。查过一次不一定记得住,建议收藏,用到时按现象找对应命令。
以下示例以 Oracle Database 19c 的业务 PDB 为例,查询需要有相应动态性能视图权限。SQL_ID、SID 等参数请替换为现场值。ASH/AWR 和 SQL Monitor 有许可限制,相关命令会单独标出。
1SELECT sid, serial#, username, machine, module, sql_id, event, status2FROM v$session3WHERE type = 'USER'4AND status = 'ACTIVE'5ORDER BY last_call_et DESC;ACTIVE 表示会话正在执行 SQL,但 EVENT 也可能是最近一次等待;要判断它此刻是否在等,还要看 STATE。
1SELECT sid, serial#, sql_id, sql_child_number,2 sql_exec_start, sql_exec_id, module, action3FROM v$session4WHERE sid = :sid;SQL_ID 标识语句,SQL_CHILD_NUMBER 指向当前子游标。SQL_EXEC_START 可能为空,不要据此断言会话未执行过这条 SQL。
1SELECT sid, sql_id, state, event, wait_class,2 wait_time_micro / 1000000 AS wait_seconds3FROM v$session4WHERE sid = :sid;只有 STATE = 'WAITING' 时,EVENT 才是当前等待事件;否则它记录最近一次等待。WAIT_TIME_MICRO 在这两种状态下分别表示本次和上次等待的时间,不要混为一谈。
1SELECT sid, serial#, sql_id, sql_exec_start, last_call_et,2 machine, module3FROM v$session4WHERE type = 'USER'5AND status = 'ACTIVE'6AND last_call_et >= 607ORDER BY last_call_et DESC;LAST_CALL_ET 是会话进入当前活动状态后的秒数,并非这次 SQL 的精确耗时;确认 SQL 起点还要看 SQL_EXEC_START。
1SELECT sid, serial#, username, module, action, sql_id, event2FROM v$session3WHERE type = 'USER'4AND module = :module_name5ORDER BY sid;应用没有设置 MODULE 时,此法不会找到它的会话,可改按用户名、客户端机器或服务定位。
1SELECT sid, serial#, username, sql_child_number,2 sql_exec_start, state, event3FROM v$session4WHERE sql_id = :sql_id5AND status = 'ACTIVE'6ORDER BY sql_exec_start;若多个会话同时变慢,先比较子游标和等待事件;不要把所有会话都归因于一个计划。
1SELECT sid, sql_id, prev_sql_id, prev_child_number,2 prev_exec_start3FROM v$session4WHERE sid = :sid;现场会话可能已经执行完问题 SQL,转去查询别的语句。这时 PREV_SQL_ID 比当前 SQL_ID 更有用,但它只保留上一条。
1SELECT sid, sql_id, opname, sofar, totalwork, units,2 elapsed_seconds, time_remaining3FROM v$session_longops4WHERE sid = :sid5AND sofar < totalwork6ORDER BY start_time DESC;视图只记录符合条件的长操作,并非每个慢 SQL 都有进度行。TIME_REMAINING 是估计值,不能当作完成时间承诺。
1SELECT sid, sql_id, event, blocking_session_status,2 blocking_instance, blocking_session,3 final_blocking_session_status, final_blocking_session4FROM v$session5WHERE sid = :sid;只有 BLOCKING_SESSION_STATUS = 'VALID' 时,阻塞会话编号才可靠。若处于锁等待,先查持锁事务,再决定是否处置,不能仅凭编号终止会话。
1-- 需要确认 Tuning Pack 使用权限2SELECT sql_id, sql_exec_id, sql_exec_start, status,3 elapsed_time / 1000000 AS elapsed_seconds4FROM v$sql_monitor5WHERE sql_id = :sql_id6AND status = 'EXECUTING'7ORDER BY sql_exec_start DESC;SQL Monitor 并不覆盖每次短查询;ELAPSED_TIME 的原始单位是微秒,而且执行中会持续更新。未获 Tuning Pack 使用授权时不要运行此查询。
1SELECT sql_id, last_active_time, executions,2 SUBSTR(sql_text, 1, 100) AS sql_text3FROM v$sqlstats4WHERE last_active_time >= SYSDATE - 1/245ORDER BY last_active_time DESC6FETCH FIRST 30 ROWS ONLY;这里按最近一小时筛选,LAST_ACTIVE_TIME 是统计更新的时间。结果能帮助找 SQL_ID,但不能把这 30 条当作“最慢 SQL”。
1SELECT sql_id, executions,2 ROUND(elapsed_time / 1000000, 2) AS total_db_seconds,3 last_active_time4FROM v$sqlstats5WHERE executions > 06ORDER BY elapsed_time DESC7FETCH FIRST 20 ROWS ONLY;ELAPSED_TIME 为微秒累计值;并行执行时可能包含协调进程和工作进程的时间,不能等同于用户看到的墙上时钟耗时。
1SELECT sql_id, executions,2 ROUND(elapsed_time / executions / 1000000, 3) AS avg_db_seconds,3 last_active_time4FROM v$sqlstats5WHERE executions >= 106ORDER BY elapsed_time / executions DESC7FETCH FIRST 20 ROWS ONLY;这里排除了执行次数少于 10 次的语句,只是减少一次性长查询的干扰。平均值由累计耗时除以执行次数,并非最近一次耗时;想查回归应比较相同时间窗口的样本。
1SELECT sql_id, executions, buffer_gets,2 ROUND(buffer_gets / executions) AS gets_per_exec3FROM v$sqlstats4WHERE executions > 05ORDER BY buffer_gets DESC6FETCH FIRST 20 ROWS ONLY;BUFFER_GETS 是累计逻辑读。先看总量还是单次值,要由业务问题决定:高频小查询和低频大查询对系统的影响不同。
1SELECT sql_id, executions,2 ROUND(cpu_time / 1000000, 2) AS cpu_seconds,3 ROUND(user_io_wait_time / 1000000, 2) AS io_wait_seconds4FROM v$sqlstats5WHERE sql_id = :sql_id;两列均为微秒累计值。CPU 高和 I/O 等待高指向不同检查方向,但不能只凭这两项解释全部耗时。
1SELECT sql_id, sql_fulltext2FROM v$sqlstats3WHERE sql_id = :sql_id;SQL_TEXT 只截取文本开头;长语句要看 SQL_FULLTEXT。复制 SQL 时仍应检查绑定变量与实际应用执行条件。
1SELECT sql_id, child_number, plan_hash_value,2 executions, last_active_time3FROM v$sql4WHERE sql_id = :sql_id5ORDER BY child_number;一个 SQL_ID 有多个子游标是正常现象;若计划哈希不同,再结合会话当前的 SQL_CHILD_NUMBER 找到业务实际使用的计划。
1SELECT child_number, plan_hash_value, executions,2 ROUND(elapsed_time / NULLIF(executions, 0) / 1000000, 3)3 AS avg_db_seconds,4 ROUND(buffer_gets / NULLIF(executions, 0)) AS gets_per_exec5FROM v$sql6WHERE sql_id = :sql_id7ORDER BY child_number;同一计划哈希也不保证每次执行表现相同。NULLIF 避免未执行游标除零;对比时留意各子游标的执行次数是否相差悬殊。
1SELECT child_number, executions, end_of_fetch_count,2 rows_processed3FROM v$sql4WHERE sql_id = :sql_id5ORDER BY child_number;END_OF_FETCH_COUNT 只在结果被完整取回时增加。应用只取前几行就关闭游标,EXECUTIONS 和完整取回次数会不同;计算“每次返回行数”前先看这个差别。
1SELECT child_number, plan_hash_value, first_load_time,2 last_load_time, last_active_time, invalidations3FROM v$sql4WHERE sql_id = :sql_id5ORDER BY child_number;这些字段能帮助确定缓存中计划何时装载、子游标是否失效过。V$SQL 只保存仍在共享池的游标,不能靠它单独还原昨天的计划历史。
1SELECT sid, sql_id, event, wait_class,2 ROUND(wait_time_micro / 1000000, 3) AS wait_seconds3FROM v$session4WHERE type = 'USER'5AND state = 'WAITING'6ORDER BY wait_time_micro DESC;这张表是查询瞬间的状态。等待很短的会话可能在两次查询之间出现又消失,不能用“查不到”证明没有等待。
1SELECT wait_class, COUNT(*) AS waiting_sessions2FROM v$session3WHERE type = 'USER'4AND state = 'WAITING'5GROUP BY wait_class6ORDER BY waiting_sessions DESC;这是等待会话数,不是等待耗时。应用、并发、用户 I/O 等类别要结合具体 EVENT 和 SQL_ID 继续看。
1SELECT event, total_waits,2 ROUND(time_waited_micro / 1000000, 2) AS waited_seconds3FROM v$session_event4WHERE sid = :sid5ORDER BY time_waited_micro DESC6FETCH FIRST 10 ROWS ONLY;结果从会话建立后累计,可能包含早已结束的等待。要分析此刻的慢 SQL,应在固定间隔取两次值比较增量。
1SELECT wait_class, total_waits,2 ROUND(time_waited / 100, 2) AS waited_seconds3FROM v$session_wait_class4WHERE sid = :sid5ORDER BY time_waited DESC;TIME_WAITED 的单位是百分之一秒。类别汇总适合先找方向;若需要知道具体资源,回到等待事件和参数。
以下第 25—27 条在 CDB 根容器查询实例级统计;业务 PDB 会话继续用前面的会话视图定位。
1SELECT event, wait_class, total_waits,2 ROUND(time_waited_micro / 1000000, 2) AS waited_seconds3FROM v$system_event4WHERE wait_class <> 'Idle'5ORDER BY time_waited_micro DESC6FETCH FIRST 20 ROWS ONLY;从 CDB 根容器查询此视图时会返回实例范围数据。累计榜单用于发现长期负担;判断这次故障时要对比故障窗口的差值。
1SELECT event, total_waits_fg,2 ROUND(time_waited_micro_fg / 1000000, 2) AS fg_wait_seconds3FROM v$system_event4WHERE wait_class <> 'Idle'5ORDER BY time_waited_micro_fg DESC6FETCH FIRST 20 ROWS ONLY;前台等待比系统总量更贴近业务会话,但仍是累计值;它不能直接说明目前哪个 SQL 负责。
1SELECT stat_name,2 ROUND(value / 1000000, 2) AS total_seconds3FROM v$sys_time_model4WHERE stat_name IN ('DB time', 'DB CPU',5 'sql execute elapsed time',6 'parse time elapsed')7ORDER BY stat_name;VALUE 单位为微秒,表示实例累计时间。DB time 和其中的子项有包含关系,不能把这四项相加当成总耗时。
1SELECT event, wait_class, COUNT(*) AS waiting_sessions2FROM v$session3WHERE type = 'USER'4AND sql_id = :sql_id5AND state = 'WAITING'6GROUP BY event, wait_class7ORDER BY waiting_sessions DESC;同一 SQL_ID 的会话可能等在不同事件上。高频执行的语句还需看执行次数、单次逻辑读和业务并发,避免只盯一个等待名称。
1SELECT sql_id,2 ROUND(application_wait_time / 1000000, 2) AS app_seconds,3 ROUND(concurrency_wait_time / 1000000, 2) AS concurrency_seconds,4 ROUND(user_io_wait_time / 1000000, 2) AS io_seconds5FROM v$sqlstats6WHERE sql_id = :sql_id;这些是该 SQL_ID 的累计统计,不是某一次执行的细节。若用户说“今天才变慢”,要找今天的执行样本或有授权的历史窗口。
1SELECT sql_id, plan_hash_value, executions,2 ROUND(elapsed_time / NULLIF(executions, 0) / 1000000, 3)3 AS avg_db_seconds,4 ROUND(buffer_gets / NULLIF(executions, 0)) AS gets_per_exec5FROM v$sqlstats_plan_hash6WHERE sql_id = :sql_id7ORDER BY plan_hash_value;这个视图按 SQL_ID + PLAN_HASH_VALUE 汇总,比 V$SQLSTATS 的单行粒度更适合比较不同计划。仍应核对各计划的执行时间和次数,不能把累计平均值当作最近一次结果。
1SELECT plan_table_output2FROM TABLE(dbms_xplan.display_cursor(:sql_id, :child_no, 'TYPICAL'));先从当前会话的 SQL_CHILD_NUMBER 找到子游标。省略这个参数可能只显示 0 号子游标,不一定是业务正在使用的计划。
1SELECT plan_table_output2FROM TABLE(dbms_xplan.display_cursor(:sql_id, NULL, 'TYPICAL'));这里把子游标参数设为 NULL,用于查看共享池中同一 SQL_ID 的不同计划。已经从共享池淘汰的计划不会出现。
1SELECT plan_table_output2FROM TABLE(dbms_xplan.display_cursor(:sql_id, :child_no,3 'TYPICAL PREDICATE NOTE'));看索引是否被使用时,不能只盯操作名;还要看访问谓词、过滤谓词以及动态统计或自适应计划的备注。
1SELECT plan_table_output2FROM TABLE(dbms_xplan.display_cursor(:sql_id, :child_no,3 'TYPICAL PROJECTION'));投影列能帮助判断某一步到底取了哪些列。怀疑回表或宽行传输时,比单看“INDEX RANGE SCAN”更有帮助。
1SELECT plan_table_output2FROM TABLE(dbms_xplan.display_cursor(:sql_id, :child_no,3 'ALLSTATS LAST'));LAST 只看该游标最近一次执行。需要执行时采集过行源统计,例如通过 gather_plan_statistics 提示或相应统计级别;没有实际统计列时,不能拿估算行数代替实际行数。
1SELECT id, parent_id, depth, operation, options,2 object_owner, object_name3FROM v$sql_plan4WHERE sql_id = :sql_id5AND child_number = :child_no6ORDER BY id;ID 是计划行号,PARENT_ID 表示上层操作。输出按 ID 排列便于查字段,并非数据库实际执行步骤的时间顺序。
1SELECT id, object_owner, object_name, cardinality, cost2FROM v$sql_plan3WHERE sql_id = :sql_id4AND child_number = :child_no5AND operation = 'TABLE ACCESS'6AND options = 'FULL'7ORDER BY id;全表扫描不一定有问题;大范围读取、并行任务或小表都可能适合它。先比较实际返回行数、读量和谓词,再决定是否需要索引。
1SELECT id, operation, options, object_name,2 access_predicates, filter_predicates3FROM v$sql_plan4WHERE sql_id = :sql_id5AND child_number = :child_no6AND (access_predicates IS NOT NULL7 OR filter_predicates IS NOT NULL)8ORDER BY id;访问谓词用于定位索引或访问结构中的行;过滤谓词可能在取出行后才处理。看清谓词所在行,比笼统说“索引失效”更准确。
1SELECT id, operation, options, object_name,2 partition_start, partition_stop3FROM v$sql_plan4WHERE sql_id = :sql_id5AND child_number = :child_no6AND partition_start IS NOT NULL7ORDER BY id;若预期只扫少数分区,却看到宽范围,继续检查谓词类型、隐式转换和分区键。这里显示的是计划里的分区范围,仍需结合运行统计确认读量。
1SELECT id, operation, options, object_name,2 cardinality AS estimated_rows,3 last_output_rows AS actual_rows_last4FROM v$sql_plan_statistics_all5WHERE sql_id = :sql_id6AND child_number = :child_no7ORDER BY id;CARDINALITY 是优化器估算,LAST_OUTPUT_ROWS 是最近一次执行的行源统计;后者可能因未采集统计而为空。估算和实际差得很大时,回头看对象统计信息和谓词选择率。
1SELECT id, operation, options, object_name,2 last_starts, last_output_rows3FROM v$sql_plan_statistics_all4WHERE sql_id = :sql_id5AND child_number = :child_no6ORDER BY last_starts DESC NULLS LAST;嵌套循环右侧的操作可能随外表行数重复启动。LAST_STARTS 很高时,接着看每次启动读了多少块,别只比较输出行数。
1SELECT id, operation, object_name, cardinality,2 last_starts, last_output_rows,3 ROUND(last_output_rows / NULLIF(cardinality * last_starts, 0), 2)4 AS actual_to_estimate5FROM v$sql_plan_statistics_all6WHERE sql_id = :sql_id7AND child_number = :child_no8AND last_output_rows IS NOT NULL9ORDER BY id;对重复启动的步骤,估算行数需要结合启动次数理解;这个比值只是快速定位线索。实际行数为零、估算为零或自适应计划时,还要看完整计划上下文。
1SELECT id, operation, options, object_name,2 last_cr_buffer_gets, last_cu_buffer_gets3FROM v$sql_plan_statistics_all4WHERE sql_id = :sql_id5AND child_number = :child_no6ORDER BY NVL(last_cr_buffer_gets, 0)7 + NVL(last_cu_buffer_gets, 0) DESC;CR 通常对应一致性读,CU 对 DML 当前模式读更有意义。行源统计存在父子包含关系,不要把各行的读量相加当作 SQL 总读量。
1SELECT id, operation, options, object_name, last_disk_reads2FROM v$sql_plan_statistics_all3WHERE sql_id = :sql_id4AND child_number = :child_no5ORDER BY last_disk_reads DESC NULLS LAST;物理读高时继续核对访问谓词、对象大小和缓存情况。计划行的统计未采集或不完整时,不应据此断言 I/O 路径。
1SELECT id, operation, options, object_name,2 ROUND(last_elapsed_time / 1000000, 3) AS elapsed_seconds3FROM v$sql_plan_statistics_all4WHERE sql_id = :sql_id5AND child_number = :child_no6ORDER BY last_elapsed_time DESC NULLS LAST;LAST_ELAPSED_TIME 是微秒。父操作的时间可能包含子操作,不能把各计划行时间累加当作整条 SQL 耗时。
1SELECT id, operation, options, object_name,2 last_execution, last_memory_used,3 ROUND(last_tempseg_size / 1024 / 1024, 2) AS temp_mb4FROM v$sql_plan_statistics_all5WHERE sql_id = :sql_id6AND child_number = :child_no7AND last_tempseg_size IS NOT NULL8ORDER BY last_tempseg_size DESC;这里主要是排序、哈希连接等 workarea。LAST_EXECUTION 可显示 optimal、one-pass 或 multi-pass;临时段大小为空通常表示最近一次没有落盘。
1SELECT id, operation, object_name, total_executions,2 optimal_executions, onepass_executions,3 multipasses_executions4FROM v$sql_plan_statistics_all5WHERE sql_id = :sql_id6AND child_number = :child_no7AND total_executions > 08ORDER BY multipasses_executions DESC;多次 multi-pass 说明这个 workarea 曾经无法在一次归并中完成,但这些是游标累计值。先确认发生在当前问题窗口,再看 PGA 和 SQL 的数据量。
1SELECT id, operation, options, object_name, last_degree2FROM v$sql_plan_statistics_all3WHERE sql_id = :sql_id4AND child_number = :child_no5AND last_degree IS NOT NULL6ORDER BY id;上次执行的并行度与优化器预计或语句请求的并行度未必一致。若并行查询变慢,后续还需查实际分配的 PX 进程和数据分布。
1-- 需要确认 Tuning Pack 使用权限2SELECT sid, process_name, plan_line_id,3 plan_operation, plan_object_name,4 starts, output_rows,5 ROUND(physical_read_bytes / 1024 / 1024, 2) AS read_mb6FROM v$sql_plan_monitor7WHERE sql_id = :sql_id8AND sql_exec_id = :sql_exec_id9AND sql_exec_start = :sql_exec_start10ORDER BY plan_line_id, sid;三个执行标识一起定位一次受监控的 SQL。并行执行时同一步骤可能出现多个进程的记录,先看 SID 和 PROCESS_NAME,不要把返回行当成唯一计划行。监控开始前的读量也可能没有计入。
1-- 需要确认 Tuning Pack 使用权限2SELECT sid, process_name, plan_line_id,3 plan_operation, plan_object_name,4 ROUND(workarea_mem / 1024 / 1024, 2) AS memory_mb,5 ROUND(workarea_tempseg / 1024 / 1024, 2) AS temp_mb6FROM v$sql_plan_monitor7WHERE sql_id = :sql_id8AND sql_exec_id = :sql_exec_id9AND sql_exec_start = :sql_exec_start10AND status = 'EXECUTING'11AND workarea_tempseg IS NOT NULL12ORDER BY workarea_tempseg DESC;WORKAREA_TEMPSEG 是执行中的临时空间用量,完成后可为空。并行 SQL 要按进程核对,不能将多行误当成同一步骤的重复值。要查峰值可再看 WORKAREA_MAX_TEMPSEG。未获 Tuning Pack 使用授权时不要运行此查询。
1SELECT sql_id, version_count, loaded_versions,2 open_versions, executions3FROM v$sqlarea4WHERE sql_id = :sql_id;VERSION_COUNT 是共享池里这个父游标下的子游标数。数量多时先找不共享原因,别直接归咎于绑定变量。
1SELECT sql_id, version_count,2 ROUND(sharable_mem / 1024 / 1024, 2) AS shared_mb3FROM v$sqlarea4WHERE sql_id = :sql_id;这列汇总子游标共享内存。版本数多且占用明显时,再结合硬解析、失效次数和 SQL 共享条件排查。
1SELECT child_number, optimizer_mode,2 optimizer_env_hash_value, plan_hash_value,3 executions4FROM v$sql5WHERE sql_id = :sql_id6ORDER BY child_number;环境哈希不同说明解析环境存在差异线索,不能仅凭哈希确定是哪一个参数不同;再看子游标不共享原因。
1SELECT child_number, optimizer_mismatch, bind_mismatch,2 bind_equiv_failure, user_bind_peek_mismatch,3 use_feedback_stats4FROM v$sql_shared_cursor5WHERE sql_id = :sql_id6ORDER BY child_number;Y 表示该条件导致子游标不能共享的线索。字段较多,先看命中的列,再核对对应子游标的计划和执行统计。
1SELECT child_number, stb_object_mismatch,2 optimizer_mode_mismatch, stats_row_mismatch3FROM v$sql_shared_cursor4WHERE sql_id = :sql_id5ORDER BY child_number;创建 SQL plan baseline、profile 或 patch 后,旧游标是只读的,可能需要新的子游标承载管理对象。先核对变更时间,不能把新增子游标一律当作故障。
1SELECT child_number, plan_hash_value,2 is_bind_sensitive, is_bind_aware, executions3FROM v$sql4WHERE sql_id = :sql_id5ORDER BY child_number;IS_BIND_SENSITIVE 和 IS_BIND_AWARE 描述游标共享状态。不同绑定值可能影响选择率,但这两个标志本身不能证明“绑定值造成这次变慢”。
1SELECT child_number, position, name,2 datatype_string, max_length, was_captured3FROM v$sql_bind_capture4WHERE sql_id = :sql_id5ORDER BY child_number, position;先核对每个子游标上的类型和长度,尤其是应用连接池里有不同数据类型的入参时。WAS_CAPTURED = 'NO' 不表示 SQL 没使用绑定变量。
1SELECT child_number, position, name,2 last_captured, value_string3FROM v$sql_bind_capture4WHERE sql_id = :sql_id5AND was_captured = 'YES'6ORDER BY child_number, position;这是过去某次执行的捕获值,不是当前会话或每次执行的实际入参。Oracle 对同一游标最多每 15 分钟捕获一次,部分类型或位置也不会记录;涉及敏感数据时不要直接外传查询结果。
1SELECT position, child_number, name,2 datatype_string, max_length3FROM v$sql_bind_capture4WHERE sql_id = :sql_id5ORDER BY position, child_number;这项用于找“相同 SQL 文本却传入不同类型或长度”的线索。类型不一致时先检查应用驱动和字段定义,不要在数据库里盲改共享参数。
1SELECT child_number, plan_hash_value,2 sql_profile, sql_patch, sql_plan_baseline,3 last_active_time4FROM v$sql5WHERE sql_id = :sql_id6ORDER BY child_number;一个游标可能用了 profile、patch 或 baseline。比较变更前后计划时,先查清这些对象是否存在及何时生效,再决定是否回退。
1SELECT owner, table_name, num_rows, blocks, sample_size,2 last_analyzed, stale_stats, stattype_locked3FROM dba_tab_statistics4WHERE owner = :owner5AND table_name = :table_name6AND object_type = 'TABLE';NUM_ROWS 是上次采集的估计行数,不是当前 COUNT(*)。统计时间早也不自动等于错误;先比对数据变化和计划估算。
1SELECT owner, table_name, num_rows, last_analyzed,2 stale_stats, stattype_locked3FROM dba_tab_statistics4WHERE owner = :owner5AND object_type = 'TABLE'6AND stale_stats = 'YES'7ORDER BY last_analyzed NULLS FIRST;STALE_STATS 是数据库的陈旧标记,适合缩小范围。是否马上重收仍要看业务窗口、统计锁和当前计划回归证据。
1SELECT table_owner, table_name, partition_name,2 inserts, updates, deletes, truncated, timestamp3FROM dba_tab_modifications4WHERE table_owner = :owner5AND table_name = :table_name6ORDER BY partition_name NULLS FIRST;插入、更新、删除数量是近似值,显示的是上次统计采集后的变化线索。若分区表只有局部分区变化,后面还要核对分区统计。
1SELECT owner, table_name, column_name, num_distinct,2 num_buckets, histogram, last_analyzed3FROM dba_tab_col_statistics4WHERE owner = :owner5AND table_name = :table_name6AND column_name = :column_name;直方图存在不保证计划正确。数据倾斜、绑定值和谓词写法不同,选择率也会不同;先看估算与实际行数差在哪一步。
1SELECT column_name, num_distinct, num_nulls,2 sample_size, last_analyzed3FROM dba_tab_col_statistics4WHERE owner = :owner5AND table_name = :table_name6ORDER BY column_name;NUM_DISTINCT 与 NUM_NULLS 都来自统计信息,不是实时精确计数。谓词列估算明显偏差时,这两项比泛泛重收整张表更有针对性。
1SELECT owner, index_name, index_type, uniqueness,2 status, partitioned, last_analyzed3FROM dba_indexes4WHERE table_owner = :owner5AND table_name = :table_name6ORDER BY index_name;STATUS 主要描述非分区索引状态。分区索引若怀疑 unusable,要进一步查看索引分区状态,不能只靠这一列。
1SELECT index_owner, index_name, column_position,2 column_name, descend3FROM dba_ind_columns4WHERE table_owner = :owner5AND table_name = :table_name6ORDER BY index_name, column_position;联合索引的列顺序影响能否使用访问谓词。计划没选索引时,先核对谓词、列顺序和选择率,不要仅凭“有索引”推断优化器必须用它。
1SELECT owner, index_name, blevel, leaf_blocks,2 distinct_keys, clustering_factor,3 last_analyzed, stale_stats4FROM dba_ind_statistics5WHERE table_owner = :owner6AND table_name = :table_name7AND object_type = 'INDEX'8ORDER BY index_name;聚簇因子接近表块数通常表示索引顺序与表中数据较接近;接近表行数则更离散。这是统计线索,不是单独决定是否重建索引的依据。
1SELECT partition_name, num_rows, blocks,2 last_analyzed, stale_stats3FROM dba_tab_statistics4WHERE owner = :owner5AND table_name = :table_name6AND object_type = 'PARTITION'7ORDER BY partition_position;只看表级统计容易漏掉最近增长很快的分区。若计划裁剪到某个分区,重点看该分区的行数和统计时间。
1SELECT object_type, partition_name, num_rows,2 last_analyzed, global_stats, stale_stats3FROM dba_tab_statistics4WHERE owner = :owner5AND table_name = :table_name6AND object_type IN ('TABLE', 'PARTITION')7ORDER BY object_type, partition_position NULLS FIRST;GLOBAL_STATS 说明统计信息是采集或增量维护的。表级与分区级数值不一定简单相加相等;排查计划回归时更应确认故障涉及哪一级统计。
1SELECT sid, sql_id, operation_id, operation_type,2 ROUND(actual_mem_used / 1024 / 1024, 2) AS memory_mb,3 number_passes4FROM v$sql_workarea_active5WHERE sql_id = :sql_id6ORDER BY actual_mem_used DESC;这个视图只显示当前分配的 workarea。NUMBER_PASSES = 0 表示还在 optimal 模式,非零时要继续看是否落盘及输入数据量。
1SELECT sid, sql_id, operation_id, operation_type,2 tablespace,3 ROUND(tempseg_size / 1024 / 1024, 2) AS temp_mb4FROM v$sql_workarea_active5WHERE sql_id = :sql_id6AND tempseg_size IS NOT NULL7ORDER BY tempseg_size DESC;TEMPSEG_SIZE 是当前为 workarea 创建的临时段大小,单位字节。SQL 结束后此行会消失,排查时应及时记录。
1SELECT username, sql_id, tablespace, segtype,2 extents, blocks3FROM v$tempseg_usage4WHERE sql_id = :sql_id5ORDER BY blocks DESC;BLOCKS 是临时段块数,转成字节还需该临时表空间的块大小。SEGTYPE 可以区分 SORT、HASH 等用途;不是每个临时段都来自排序。
1SELECT s.sid, s.serial#, s.sql_id AS current_sql_id,2 t.sql_id AS temp_sql_id, t.segtype, t.blocks3FROM v$tempseg_usage t4JOIN v$session s5 ON s.saddr = t.session_addr6 AND s.serial# = t.session_num7ORDER BY t.blocks DESC;临时段的创建 SQL 与会话当前 SQL 可能不同,故两列都保留。看到大临时段时先确认会话和业务,再决定是否处理。
1SELECT sql_id, tablespace, segtype,2 SUM(blocks) AS temp_blocks3FROM v$tempseg_usage4WHERE sql_id = :sql_id5GROUP BY sql_id, tablespace, segtype6ORDER BY temp_blocks DESC;这是查询瞬间仍分配的临时块,不是整次执行的累计消耗。若多个临时表空间块大小不同,不要把块数直接相加换算成统一容量。
1SELECT child_number, operation_id, operation_type,2 last_execution,3 ROUND(last_tempseg_size / 1024 / 1024, 2) AS temp_mb4FROM v$sql_workarea5WHERE sql_id = :sql_id6ORDER BY child_number, operation_id;LAST_EXECUTION 显示 optimal、one-pass 或 multi-pass。它只对应缓存中的游标及最近一次执行,分析今天的故障时还要核对该游标是否就是业务用的子游标。
1SELECT child_number, operation_id, operation_type,2 total_executions, optimal_executions,3 onepass_executions, multipasses_executions4FROM v$sql_workarea5WHERE sql_id = :sql_id6ORDER BY multipasses_executions DESC;这些是游标累计统计。即使历史上有多次落盘,也不能直接证明这次慢 SQL 是 PGA 不足;先比较执行数据量和当前 workarea。
1SELECT qcsid, sid, server_group, server_set,2 server#, degree, req_degree3FROM v$px_session4WHERE qcsid = :qc_sid5ORDER BY server_group, server_set, server#;REQ_DEGREE 是请求并行度,DEGREE 是实际使用的并行度。资源和并发可能使两者不同;执行进程分布也会影响整体耗时。
1-- 需要确认 Tuning Pack 使用权限2SELECT sql_id, sql_exec_id, sql_exec_start,3 px_servers_requested, px_servers_allocated,4 px_maxdop5FROM v$sql_monitor6WHERE sql_id = :sql_id7AND sql_exec_id = :sql_exec_id8AND sql_exec_start = :sql_exec_start9AND px_server# IS NULL;如果分配进程少于请求数,后续要看并行服务器可用量和业务同时运行的并行任务。SQL Monitor 不覆盖每次执行,未获授权时不要运行。
1SELECT statistic, value2FROM v$px_process_sysstat3WHERE statistic IN ('Servers In Use', 'Servers Available',4 'Servers Started', 'Servers HWM')5ORDER BY statistic;第 80 条在 CDB 根容器查询实例级统计。Servers In Use 与 Servers Available 是当前状态,其余值包含历史;不要仅凭一个瞬间的可用量调大并行参数。
1SELECT name, value2FROM v$parameter3WHERE name = 'control_management_pack_access';此参数说明管理包是否在实例中启用,不等于已经购买或获准使用。后面的 AWR 历史查询需先确认 Diagnostic Pack 授权。
1SELECT sql_handle, plan_name, parsing_schema_name,2 enabled, accepted, fixed, reproduced, created3FROM dba_sql_plan_baselines4WHERE sql_handle = :sql_handle5ORDER BY created;ACCEPTED、ENABLED 和 REPRODUCED 要分开看:计划存在,不代表优化器目前能重现并使用它。
1SELECT sql_handle, plan_name, last_executed,2 last_verified, last_modified3FROM dba_sql_plan_baselines4WHERE sql_handle = :sql_handle5ORDER BY plan_name;LAST_EXECUTED 为减少开销不会每次执行都立即更新。要确认当前游标是否使用 baseline,仍须结合第 60 条查看。
1SELECT plan_table_output2FROM TABLE(dbms_xplan.display_sql_plan_baseline(3 :sql_handle, :plan_name, 'TYPICAL'));展示的是 SQL 管理库中的计划。若 REPRODUCED = 'NO',优化器可能跳过它;同时对照当前 DISPLAY_CURSOR 的计划。
1SELECT name, category, status, force_matching,2 created, last_modified3FROM dba_sql_profiles4WHERE name = :profile_name;profile 是否 ENABLED 与当前游标是否实际采用是两件事。还要核对第 60 条的 SQL_PROFILE 和 SQL 匹配方式。
1SELECT name, category, status, force_matching,2 created, last_modified3FROM dba_sql_patches4WHERE name = :patch_name;若故障刚好发生在 patch 修改后,先记录对象状态和游标计划,不要在未确定影响范围时直接删除 patch。
1SELECT snap_id, dbid, instance_number,2 begin_interval_time, end_interval_time3FROM dba_hist_snapshot4WHERE end_interval_time >= :start_time5AND begin_interval_time <= :end_time6ORDER BY snap_id;第 87 条在 CDB 根容器核对快照范围。DBA_HIST_SNAPSHOT 属官方许可说明中的例外视图;它只提供快照时间,不能据此推断可以访问其它 AWR 统计视图。
1-- 需要 Diagnostic Pack 使用授权;在 CDB 根容器执行2SELECT s.snap_id, s.plan_hash_value, s.executions_delta,3 ROUND(s.elapsed_time_delta4 / NULLIF(s.executions_delta, 0) / 1000000, 3)5 AS avg_db_seconds6FROM dba_hist_sqlstat s7WHERE s.sql_id = :sql_id8AND s.dbid = :dbid9AND s.con_dbid = :pdb_dbid10AND s.snap_id BETWEEN :begin_snap AND :end_snap11ORDER BY s.snap_id, s.plan_hash_value;DELTA 是快照区间增量,不是 SQL 每次执行的完整明细。AWR 只捕获符合条件的部分 SQL;查不到不能证明那段时间 SQL 没运行。
1-- 需要 Diagnostic Pack 使用授权;在 CDB 根容器执行2SELECT plan_hash_value,3 SUM(executions_delta) AS executions,4 ROUND(SUM(elapsed_time_delta)5 / NULLIF(SUM(executions_delta), 0) / 1000000, 3)6 AS avg_db_seconds,7 ROUND(SUM(buffer_gets_delta)8 / NULLIF(SUM(executions_delta), 0)) AS gets_per_exec9FROM dba_hist_sqlstat10WHERE sql_id = :sql_id11AND dbid = :dbid12AND con_dbid = :pdb_dbid13AND snap_id BETWEEN :begin_snap AND :end_snap14GROUP BY plan_hash_value15ORDER BY plan_hash_value;在相同业务负载窗口比较不同计划才有意义。平均读量或耗时变化只是线索,还需确认绑定值、数据量和并行度是否也变了。
1-- 需要 Diagnostic Pack 使用授权;在 CDB 根容器执行2SELECT id, parent_id, operation, options,3 object_owner, object_name, cardinality4FROM dba_hist_sql_plan5WHERE sql_id = :sql_id6AND plan_hash_value = :plan_hash_value7AND dbid = :dbid8AND con_dbid = :pdb_dbid9ORDER BY id;这里是历史保存的计划结构,CARDINALITY 仍为优化器估算行数。要找“哪一步真实多读了块”,需有相应时间窗口的实际监控或已执行游标统计。
第 94—99 条会创建对象或改变优化器可用的统计信息、计划基线。执行前应确认业务 PDB、对象属主、变更窗口、基线计划和回退方案;第 98—99 条还要核对数据库版本与版本许可。
1SELECT dbms_stats.get_prefs('METHOD_OPT', :owner, :table_name)2 AS method_opt,3 dbms_stats.get_prefs('CASCADE', :owner, :table_name)4 AS cascade5FROM dual;METHOD_OPT 影响列统计与直方图,CASCADE 影响关联索引统计。计划回归发生在采集统计之后,先确认实际偏好再讨论是否重收。
1SELECT dbms_stats.report_gather_table_stats(2 ownname => :owner, tabname => :table_name) AS report3FROM dual;报告模式不会真正采集统计信息,适合在变更前核对目标。它也不能预言重收之后会产生哪一个执行计划。
1SELECT dbms_stats.get_stats_history_availability()2 AS oldest_available3FROM dual;这表示数据库仍保留的最早统计历史时间。即使有历史,也要确认目标表在所需时点确实有可恢复的统计;关键变更最好另行导出当前统计。
1-- 变更操作;在业务 PDB、具备权限的账号执行2BEGIN3 dbms_stats.create_stat_table(4 ownname => :stats_owner,5 stattab => 'SQL_TUNE_STATS_BAK');6END;7/备份表应放在有足够空间、可控权限的 schema。已有同名表时不要照搬创建;后续导出使用同一 stats_owner。
1-- 变更前备份;确认第 94 条的表已经创建2BEGIN3 dbms_stats.export_table_stats(4 ownname => :owner,5 tabname => :table_name,6 stattab => 'SQL_TUNE_STATS_BAK',7 statid => :change_id,8 statown => :stats_owner);9END;10/默认会包括相关列与索引统计。STATID 用于区分不同变更批次;备份完成后应核对备份表中确实有本次记录。
1-- 会改变优化器统计;已保存旧统计并确认影响范围2BEGIN3 dbms_stats.gather_table_stats(4 ownname => :owner,5 tabname => :table_name,6 estimate_percent => dbms_stats.auto_sample_size,7 cascade => TRUE,8 no_invalidate => dbms_stats.auto_invalidate);9END;10/新统计可能使依赖 SQL 在后续解析时换计划。执行后比较实际游标计划、耗时和读量,不应只看命令是否成功。
1-- 回退操作;确认 change_id 与备份对象完全一致2BEGIN3 dbms_stats.import_table_stats(4 ownname => :owner,5 tabname => :table_name,6 stattab => 'SQL_TUNE_STATS_BAK',7 statid => :change_id,8 statown => :stats_owner,9 no_invalidate => dbms_stats.auto_invalidate);10END;11/导入会把保存的统计写回数据字典,可能再次改变 SQL 计划。表有统计锁时默认不会强制覆盖,先查清锁的来源,不要随手设 force => TRUE。
1-- 变更操作;确认版本许可、SQL 文本和目标计划2DECLARE3 n PLS_INTEGER;4BEGIN5 n := dbms_spm.load_plans_from_cursor_cache(6 sql_id => :sql_id,7 plan_hash_value => :plan_hash_value,8 fixed => 'NO',9 enabled => 'YES');10 dbms_output.put_line('loaded=' || n);11END;12/只能加载仍在游标缓存中的计划,返回值为加载数量。SQL Plan Management 的功能范围随数据库版本与版本许可不同;加载后必须检查 DBA_SQL_PLAN_BASELINES 及业务实际采用的计划。
1-- 回退或止损操作;先确认还有可用的已接受计划2DECLARE3 n PLS_INTEGER;4BEGIN5 n := dbms_spm.alter_sql_plan_baseline(6 sql_handle => :sql_handle,7 plan_name => :plan_name,8 attribute_name => 'enabled',9 attribute_value => 'NO');10 dbms_output.put_line('altered=' || n);11END;12/禁用目标计划可能使下一次硬解析选用其他计划。只在明确目标 PLAN_NAME、已确认替代计划和回退路径时执行,不能按“平均耗时高”一项直接禁用。
1SELECT s.sid, s.serial#, s.sql_id, s.sql_child_number,2 q.plan_hash_value, q.sql_plan_baseline,3 q.executions,4 ROUND(q.elapsed_time / NULLIF(q.executions, 0) / 1000000, 3)5 AS avg_db_seconds6FROM v$session s7JOIN v$sql q8 ON q.sql_id = s.sql_id9 AND q.child_number = s.sql_child_number10WHERE s.type = 'USER'11AND s.sql_id = :sql_id;这能确认查询瞬间业务会话对应的子游标和计划,但平均耗时仍是游标累计值。最终验证还应比较相同负载窗口下的响应时间、逻辑读和错误率。
查慢 SQL,先拿到业务请求时间、会话和实际使用的子游标,再看等待、读量与计划步骤。累计统计适合找长期大户,当前会话适合找正在发生的故障;两者别混用。改统计或计划基线前留好旧值,改完用业务实际执行结果验证。
ORA100 DBA100 系列海报
更多场景命令收录在 ORA100 · DBA100:
微信里也可以搜索小程序 「三笠的百令册」。