工作中经常被问到“Oracle又慢了,怎么回事”,这大概是运维岗最高频的求助之一。不同于应用报错有明确的堆栈信息,性能问题的描述往往是模糊的——“系统很卡”“某个功能转半天”“晚上批量跑不完”,背后可能涉及SQL低效、等待事件异常、资源争用、配置缺陷等多层原因。我这些年排查过的性能问题不下百个,既踩过不少坑,也沉淀出一套相对固定的排查链路。这篇随笔就把我的做法、思路和翻车经验一并整理出来,供同行参考。
1. 性能故障的现场:从用户的一句“很慢”到缩小范围
1.1 先搞清“卡”在哪个环节
用户报障说“系统很慢”时,第一反应不要冲进数据库看一堆指标,而是先确认一个问题:这个“慢”是数据库造成的,还是应用、网络、存储引起的?鉴别方法其实很简单——问业务方几个关键问题:是所有模块都慢,还是某个特定页面/功能慢?是全天都慢还是某个时间段才慢?是从哪个版本更新后才出现的,还是一直都这样?
这几个问题能帮你快速判断排查方向。如果是单个功能慢,多半是这个功能对应的SQL有问题;如果是业务高峰时段才慢,大概率是资源争用或容量不足;如果是版本更新后变慢,首先要怀疑SQL执行计划发生了变化或者相关代码逻辑有调整。
实操中还会遇到一种情况:用户说数据库很慢,但你在数据库侧看负载并不高,CPU、IO、等待事件都很正常。这时问题往往出在应用层——并发线程池被占满、HTTP连接资源耗尽、或者中间件GC异常。我的习惯是拿到报障后,先同时观察数据库和操作系统两个层面的指标,用数据来判断“慢”的归属。
1.2 快速看一眼的关键指标
登录数据库主机后,我一般按顺序看这几样东西:
- 系统负载(load average)、CPU使用率、内存使用、swap使用情况
- 磁盘IO的读写延迟、队列长度,特别是数据文件所在磁盘组的情况
- 数据库层面的活动会话数、当前正在执行的SQL、锁等待情况
- 数据库的等待事件Top 10
这一套操作在两三分钟内就能完成,基本能确认问题方向。如果系统负载很高且CPU大部分消耗在用户态,问题多数在SQL或者应用逻辑;如果磁盘读写延时明显异常,要关注存储链路;如果活动会话不高但业务反应极慢,就要看是否有锁阻塞或者应用连接池配置问题。
我遇到过最典型的误导场景是:数据库所在主机CPU跑满,排查半天发现是备份任务和统计信息收集任务叠加导致的,业务本身并没有问题。所以看异常之前,先确认当前时间点有没有计划任务在跑,这一步能省掉后续大量无谓的排查。
1.3 排查过程要留痕
性能问题不像代码bug,不会稳定复现,很多信息转瞬即逝。我的习惯是排查过程中把每一条命令的输出、时间点、当时的大致状态都记录下来。一方面方便自己回头看时判断关联性,另一方面后续写故障报告时也有据可依。工作多年,我见过太多人排查完就说“好了”,但问不出是为什么好的、怎么好的,下次复现时又要从头来一遍。
后面涉及的所有排查语句,建议都以sysdba身份或者有相应权限的账号执行,避免因权限不足导致排查到一半卡住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 慢SQL排查的完整链路:从AWR/ASH到执行计划
2.1 AWR报告的正确阅读方式
当确认问题集中在数据库侧之后,我会先拿一个时间段的AWR报告,观察DB Time和Elapsed Time的关系。如果DB Time远大于Elapsed Time,说明这段时间系统确实忙得不行;如果二者接近甚至DB Time很小,数据库侧其实是空闲的,问题一定在外部。
看AWR报告不必逐项研究,抓几个关键点就够:
- Top 10 Foreground Wait Events,这些等待事件直接告诉你数据库时间被消耗在哪里
- SQL ordered by Elapsed Time / CPU Time,把最耗时的SQL列表拿下来
- IO Stats,看哪些表空间、数据文件存在明显的读写热点
- 实例级的活动会话数趋势
一套AWR报告看下来,如果能发现某个SQL的Elapsed Time明显异常,就可以直接进入SQL级排查。如果Top事件是db file sequential read,要重点看索引访问是否合理;如果是enq: TX - row lock contention,多半存在行锁竞争;如果是log file sync,问题在提交频率和日志写IO上。
2.2 实时的ASH视图用法
AWR报告是历史数据的聚合,适合事后分析。问题正在发生时,我更倾向于直接查V$ACTIVE_SESSION_HISTORY(ASH)视图,定位当前会话在干什么。下面这条语句是我最常用的,按样本数排序,能直接看到问题时间点最活跃的会话和对应SQL:
sql复制SELECT session_id, session_serial#, sql_id, event,
COUNT(*) AS sample_cnt,
ROUND(COUNT(*) * 10 / 100, 1) AS seconds_est
FROM v$active_session_history
WHERE sample_time > SYSDATE - INTERVAL '30' MINUTE
AND session_state = 'ON CPU'
GROUP BY session_id, session_serial#, sql_id, event
ORDER BY sample_cnt DESC
FETCH FIRST 10 ROWS ONLY;
样本是基于1秒采样获得的,所以样本数乘以10就可以估算该会话在采样时间段内消耗的秒数(按10秒采样间隔计)。执行完这条语句,如果能看到某个SQL_ID反复出现且样本数很多,基本就能锁定性能瓶颈的SQL对象。
2.3 拿到慢SQL之后的标准动作
锁定了SQL_ID之后,有三件事必须做:
- 抓取SQL完整文本和执行计划
- 查看该SQL的历史执行计划变化情况
- 确认SQL涉及的表、索引的数据量和统计信息状态
抓取SQL文本用:
sql复制SELECT sql_text FROM v$sql WHERE sql_id = '&sql_id';
这个SQL_ID若在共享池中被淘汰,文本可能已经不在v$sql中了,可以在DBA_HIST_SQLTEXT中查找,或者通过应用日志拿。拿到文本后,不要急着分析,先在测试环境跑一遍EXPLAIN PLAN FOR,拿到执行计划。
从v$sql_plan拿到的是最后一次执行的计划,而SQL性能往往是计划一变就开始变差,所以最好确认历史计划:
sql复制SELECT plan_hash_value, COUNT(*) cnt
FROM dba_hist_sqlstat
WHERE sql_id = '&sql_id'
GROUP BY plan_hash_value
ORDER BY cnt DESC;
如果有多个不同的plan_hash_value,而且变差的时机和计划变化的时间点吻合,那么方向就清楚了——执行计划出了问题。
2.4 一个真实的排查案例:从AWR到执行计划
去年遇到过一次典型的批量任务变慢:某数据仓库的夜间汇总任务原本1小时完成,某天突然变成4个小时。AWR报告显示DB Time集中在某个INSERT...SELECT语句上,等待事件是CPU和direct path write temp。
锁定的SQL涉及两张千万级大表的关联,执行计划显示其中一张表走了全表扫描,而历史计划是索引扫描。进一步检查发现统计信息已经超过两周没有更新,表的数据量翻了近一倍,优化器认为全表扫描成本更低,但实际上大量数据需要过滤,全表扫描引入了巨大的临时表空间排序开销。
处理方式很简单:先手动刷新这两张表的统计信息,再强制使用原来的执行计划,任务恢复在50分钟内完成。后来把这个表加入了统计信息自动收集的白名单,问题再没出现过。
这个案例说明一件事:所谓性能问题,很多时候不是SQL写得不好,而是SQL赖以生存的“环境信息”发生了偏差。这也是为什么我一直强调统计信息的重要性,后面第五节专门展开讲。
3. 执行计划解读:从访问路径到连接方式
3.1 读懂执行计划的前置条件
拿到执行计划,第一个要确认的是执行计划和执行SQL的绑定变量值是否一致。生产环境中的SQL大多使用绑定变量,优化器在生成执行计划时使用的是绑定变量的“平均值”,但实际传入的值可能差异很大。如果某个列上有严重的数据倾斜,同一个SQL在不同绑定变量值下性能表现会天差地别。
在分析执行计划之前,我会先确认以下信息:
- 相关表的数据量级、相关列的数据分布(直方图是否存在)
- 相关的索引情况:列组合、选择性、索引状态
- 表的统计信息最后收集时间
有了这些前置信息,才能判断执行计划的每一步是否合理。
3.2 访问路径的Cost逻辑:全表扫描未必是坏事
很多人一看到执行计划里出现TABLE ACCESS FULL就紧张,觉得数据库要走全表扫描就等于性能差。这个认知需要纠正——全表扫描是否合理,取决于“要取出多少数据”和“表有多小”。
优化器选择全表扫描,核心是它评估“扫描整张表 + 过滤”的成本低于其他方式。如果一张表就几千行,全表扫描的成本极低;如果一张上亿行的大表需要取出20%以上的数据,全表扫描很可能是最优解;但如果只需要取出几行,却走了全表扫描,那就要分析是不是索引没建对或者统计信息出了问题。
有一个判断执行计划是否合理的经验法则:对比计划中ROWS列和实际返回的行数差异。优化器估算的行数和实际行数差距过大(比如相差百倍以上),说明统计信息或直方图有问题,计划大概率不是最优的。
sql复制-- 查看实际执行统计信息的SQL
SELECT sql_id, plan_hash_value,
executions,
rows_processed,
buffer_gets,
disk_reads,
elapsed_time/1000000 AS elapsed_sec
FROM v$sql
WHERE sql_id = '&sql_id';
v$sql中buffer_gets和rows_processed的对比能很好地反映SQL的资源消耗效率。buffer_gets/rows_processed比值过高的SQL,即使在执行计划上看不到“大问题”,也值得进一步深挖——这类SQL往往是设计层面的逻辑缺陷导致的,比如不该有的嵌套循环或者逐行处理。
3.3 连接方式的选择逻辑:NESTED LOOP、HASH JOIN、SORT MERGE JOIN
多表关联时,优化器的连接方式选择直接决定SQL的生死。三种连接方式各有各的适用场景:
- NESTED LOOP适合小表驱动大表的场景,且内表关联列有索引。外层表数据量小,内层表每次通过索引精确定位。
- HASH JOIN适合两张大表关联,且关联列上过滤后仍有大量数据需要匹配。优化器选择把小表(build table)哈希化,再扫描大表(probe table)进行匹配。
- SORT MERGE JOIN适合关联列上有排序需求或者非等值关联(如范围关联)的场景,因为排序后可以顺序扫描匹配。
很多开发人员习惯性认为NESTED LOOP效率一定高,实则在两张大表的场景下完全可能是灾难——内层表被反复遍历数千次,执行时间呈指数级增长。我遇到过的最夸张案例,一个三表关联的SQL,执行计划走了嵌套循环,内层每次都要扫上百万行,整个SQL跑了几个小时;改成HASH JOIN只需要几分钟。
3.4 排序、临时表空间与直接路径
执行计划中如果出现SORT ORDER BY、SORT GROUP BY、BUFFER SORT等操作,意味着数据需要在内存或临时表空间中排序。排序量超过PGA的sort_area_size(或PGA自动管理下的可用空间)时,会触发磁盘排序,写临时表空间,等待事件表现为direct path write temp和direct path read temp。
我曾经排查过一个诡异的问题:SQL单独执行只要10秒,但在批量任务中却要跑30分钟。后来定位到是批量任务中大量SQL同时并发执行,每个SQL都需要较大排序空间,PGA被瓜分干净后,全部落盘到临时表空间,互相争夺IO。解决办法有两部:一是优化SQL减少排序量,二是调大PGA_AGGREGATE_TARGET,分配足够的排序内存。这个案例也说明了为什么性能问题不能只看单条SQL的表现——并发场景下的资源争用会放大单条SQL的问题。
3.5 执行计划优化的实操建议
拿到执行计划后按这个顺序处理,效率最高:
- 第一眼看最底层、最耗时的操作是什么(是全表扫描还是索引扫描,或者某种连接方式)
- 第二眼对比估算行数(ROWS)和实际行数,偏差大则有统计信息问题
- 第三眼看有没有意外的排序和临时表空间操作
- 第四眼验证索引是否被正确使用
如果要验证某个假设,可以在会话级别临时改变SQL的执行方式观察效果,但生产环境慎用hint。测试环境可以放心大胆地试,找到最优解后在应用代码层面固化。
4. 索引设计:建了索引不等于走索引
4.1 隐式转换:索引失效的头号杀手
这类问题我在工作中见到太多了。表字段是VARCHAR2类型,SQL条件里传了数字,Oracle会自动把字段做隐式类型转换(TO_NUMBER),此时即使该字段上有索引也无法使用,只能全表扫描。
sql复制-- 假设 phone_no 是 VARCHAR2 类型
SELECT * FROM t_user WHERE phone_no = 13800000000; -- 索引失效
SELECT * FROM t_user WHERE phone_no = '13800000000'; -- 索引可用
排查方法很简单,看执行计划里有没有TO_NUMBER("PHONE_NO")这样的谓词信息。出现这种表达式,索引一定无法使用。
还有一类是日期类型字段与字符串比较。表字段是DATE类型,SQL写create_date = '2024-01-01',Oracle会尝试把字符串转换为日期,这种转换通常不影响索引。问题往往出现在反向场景:字段是VARCHAR2,存的是日期文本,SQL里用TO_DATE来过滤,索引照样失效。
还有一种隐式转换发生在关联条件上。两表关联字段类型不一致时(一边是NUMBER,一边是VARCHAR2),Oracle会隐式转换,导致一边的索引无法使用。我排查过一个十几分钟跑不完的SQL,就是两表关联字段一个用INTEGER一个用VARCHAR2,等号两边类型不一致,优化器选择全表扫描+哈希关联,改成统一类型后秒回。
4.2 函数索引与表达式索引的使用边界
前端条件中使用了函数,普通索引就无法命中,除非在查询谓词中显示写出函数表达式。比如:
sql复制SELECT * FROM t_order WHERE TRUNC(create_time) = TRUNC(SYSDATE);
如果create_time上有普通索引,这个SQL不会走索引。解决办法有两种:
- 把条件改写为范围查询:
create_time >= TRUNC(SYSDATE) AND create_time < TRUNC(SYSDATE) + 1 - 在函数表达式上创建函数索引:
CREATE INDEX idx_t_order_trunc_ct ON t_order(TRUNC(create_time))
我建议优先考虑改写SQL为范围查询,原因在于:函数索引会增加写入负担且不灵活,业务条件一变就要新建索引;而范围查询可以利用已有的普通索引,可维护性更好。
4.3 复合索引的列顺序:区分度优先还是等值优先
复合索引(联合索引)的列顺序在Oracle中至关重要,它决定索引能被多少查询场景使用。原则可以概括为两条:
- 等值条件列放前面,范围条件列放后面
- 区分度高的列放前面,区分度低的列放后面
这两条原则冲突时,优先满足等值条件。举个例子,索引(col_a, col_b),查询条件是col_a = ? AND col_b > ?,可以充分利用索引;但如果是col_a > ? AND col_b = ?,col_b的等值条件无法利用索引,因为范围条件在前的列已经限制了索引树的访问顺序。
还有一个常见的误区:以为给所有可能被查询的列都建上复合索引就能覆盖一切。实际上索引不是越多越好——每个索引都会拖慢INSERT、UPDATE、DELETE的性能,占用存储空间,增加优化器选择时的计算负担。
4.4 索引的“假死”状态:统计信息与高水位
有个容易被忽视的问题:表数据删了一大批,但表的高水位线没有降下来。此时执行全表扫描仍然会扫描高水位线以下的所有块,速度并不会因为数据量减少而变快。这种情况下需要回收空间:
sql复制ALTER TABLE t_big_table ENABLE ROW MOVEMENT;
ALTER TABLE t_big_table SHRINK SPACE CASCADE;
ALTER TABLE t_big_table DISABLE ROW MOVEMENT;
还有一类情况是索引的聚类因子(CLUSTERING_FACTOR)过大。聚类因子衡量索引顺序与表数据物理存储顺序的匹配程度,聚集因子接近表块数时,索引扫描的成本会很高。优化器可能在索引扫描和全表扫描之间摇摆,而且聚集因子过大时即使走索引也要反复随机IO。
4.5 一张表该建哪些索引:实操中的取舍
索引设计没有银弹,但有底线。我个人在给表设计索引时会做这样几步:
- 找出业务核心查询,分析WHERE条件涉及的列
- 区分等值条件和范围条件,按优先级排列复合索引列顺序
- 明确主外键列必须有索引(否则外键列上的DML会锁父表)
- 周期性检查未使用索引,及时清理
一个实用工具是查看V$SQL_PLAN中实际使用的索引,再用DBA_INDEXES的LAST_ANALYZED和监控列的统计来判断索引使用频率。长期不用的索引果断删除,减少写负载和存储开销。
5. 统计信息与CBO优化器:计划为什么“跑偏”
5.1 统计信息过期的连锁反应
CBO(Cost-Based Optimizer)做决策时依赖的是表和索引的统计信息——行数、块数、列的唯一值数量、直方图等。统计信息和真实数据差距过大时,CBO就会基于错误信息做“正确”的判断,结果就是在真实世界中表现糟糕。
统计信息过期最常见的场景:
- 大表数据量成倍增长但未重新收集统计信息
- 表数据被大量删除但统计信息仍显示原有行数
- 批量任务之后未及时收集统计信息
- 直方图缺失导致CBO对数据分布严重倾斜的列估算不准
Oracle默认配置下,统计信息自动收集任务只在维护窗口运行。如果业务每天产生大量数据,但维护窗口设置不合理(例如凌晨1点开始,批量任务还没跑完),统计信息就可能长期不更新。
5.2 数据倾斜与直方图:绑定变量下的一方
绑定变量对稳定执行计划有很大帮助,但在数据严重倾斜的场景下会带来一个问题——优化器无法根据具体值判断最优计划,只能按平均选择性估算。
举个典型例子:订单表有个状态字段,99%的行是“已完成”,1%是“待支付”。用户要查“待支付”订单,如果走索引非常快;但优化器按平均选择性估算时,可能觉得大量数据需要访问,选择全表扫描。同样一个SQL,传入“已完成”时全表扫描没问题(反正要取出99%的数据),传入“待支付”时全表扫描就是灾难。
这个问题的破解思路有几种:
- 给状态字段创建直方图,让优化器知道数据分布
- 在应用层把高频与低频值的查询拆成不同SQL
- 用复合索引加过滤条件引导执行计划
我之前在一个订单系统里就是这样处理的:状态字段上建了直方图,并针对“待支付”场景单独建了一个部分索引(只包含待支付记录),配合应用改写SQL,查询时间从秒级降到毫秒级。
5.3 手动收集统计信息的正确姿势
日常运维中,以下情况需要手动收集统计信息:
- 大批量数据变更后(数据加载、删除超过10%的存量数据)
- 新建索引之后
- 表结构变更(新增列、修改列类型)
- 自动收集任务不可用或配置异常
手动收集的语句其实很直接:
sql复制EXEC DBMS_STATS.GATHER_TABLE_STATS(
ownname => 'APP_USER',
tabname => 'T_BIG_TABLE',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
method_opt => 'FOR ALL COLUMNS SIZE AUTO',
cascade => TRUE,
degree => 8
);
这里的参数有几个含义需要理解:
- ESTIMATE_PERCENT:采样比例。AUTO_SAMPLE_SIZE让Oracle自己决定采样规模,大数据集下会自动使用更高效的采样方式
- METHOD_OPT:直方图收集策略。SIZE AUTO让Oracle根据数据分布自动决定哪些列需要直方图
- CASCADE:级联收集索引统计信息
- DEGREE:并行度。大表收集统计信息时提高并行度能显著缩短时间,但要考虑主机负载
5.4 绑定变量窥探与自适应计划
12c以上版本引入的自适应执行计划(Adaptive Execution Plans)和自适应游标(Adaptive Cursor Sharing)在特定场景下能缓解绑定变量引起的问题。但自适应机制也有副作用——同一个SQL在不同次执行可能选择不同的计划,这在性能监控上会造成一些干扰。
如果确认某个SQL的执行计划已经被自适应机制搞乱,可以用以下方法重置:
sql复制-- 清理游标,强制重新硬解析
ALTER SYSTEM FLUSH SHARED_POOL;
但FLUSH SHARED_POOL会清空整个共享池,导致所有SQL重新硬解析,生产环境慎用。更精细的做法是只针对问题SQL处理:
sql复制-- 通过dbms_shared_pool.purge清理单个游标
EXEC DBMS_SHARED_POOL.PURGE('ADDRESS,HASH_VALUE', 'C');
5.5 统计信息收集的常见错误
有的同事习惯用GATHER_TABLE_STATS时指定固定的采样比例,比如10%。这在小表没啥问题,但在大表上容易造成统计信息不准确。另一个常见错误是所有表都使用同样的收集策略,忽略了表的更新频率和数据分布特征。
我的习惯是给不同表设定不同的收集策略:
| 表类型 | 特点 | 策略 |
|---|---|---|
| 核心交易表 | 更新频繁、数据量大 | 每日维护窗口收集,AUTO_SAMPLE_SIZE |
| 配置字典表 | 数据量小、基本不变 | 每周收集即可,完全采样 |
| 临时/中间表 | 生命周期短、重建频繁 | 不收集或使用默认NQ |
| 分区表 | 分区数据差异大 | 按分区增量收集,配合全局统计信息 |
6. 运维侧的高频调优手段:从SQL改写、参数调整到计划固化
6.1 SQL改写:成本最低的优化方式
很多性能问题本质上不是数据库的问题,是SQL语句的写法问题。改写SQL是成本最低、效果最明显的优化手段,不需要改表结构、不需要加硬件资源。
常见的改写思路包括:
- 把OR条件改写为UNION ALL:OR条件如果涉及多个索引列,优化器可能无法高效合并多个索引访问路径。
WHERE a = 1 OR b = 2改写为WHERE a = 1 UNION ALL WHERE b = 2常常有奇效。 - 避免SELECT *造成的不必要大字段传输:如果表里有CLOB字段,查询时带上会拖慢整体响应,尤其是在网络传输和排序场景下影响更明显。
- 分页查询使用ROWNUM而不是全量取出再过滤:大规模分页的性能差距能达到几个数量级。
Oracle处理分页时,最经典的高效写法是:
sql复制SELECT * FROM (
SELECT t.*, ROWNUM rn FROM (
SELECT * FROM t_order ORDER BY create_time DESC
) t WHERE ROWNUM <= 200
) WHERE rn > 100;
这里要特别注意内层ROWNUM的推入方式。如果条件写成WHERE rn > 100 AND rn <= 200,优化器无法在取出第一行时就开始过滤ROWNUM,需要先拿到全部结果再截断,性能会差很多。
6.2 WITH子句与临时结果物化
复杂查询中反复引用同一个子查询结果时,使用WITH子句(CTE)可以避免重复执行相同的子查询。Oracle对WITH子句的处理有两种方式:内联(INLINE)和物化(MATERIALIZE)。是否物化取决于优化器的成本评估,也可以通过/*+ MATERIALIZE */提示强制物化。
我记得处理过一个报表SQL,里面同一个子查询被引用了3次,每次都是对千万级表做聚合。优化器选择内联方式,子查询被重复执行3次。后来把子查询改成物化CTE,整个报表从20分钟降到3分钟。
sql复制WITH t AS (
SELECT /*+ MATERIALIZE */ dept_id, COUNT(*) cnt
FROM t_employee
GROUP BY dept_id
)
SELECT ... FROM t JOIN ...;
不过物化CTE也不是万能的,小数据集情况下物化反而增加开销,维护成本也高。实际场景中要对比测试后再决定用哪种方式。
6.3 并行执行:最后的加速手段
并行查询(Parallel Query)可以显著提升大数据量查询的速度,但它不是免费的午餐——并行会把一个会话的工作负载放大到多个并行进程上,整体消耗的IO和CPU资源更高。如果系统本身承载着大量在线业务,盲目开并行反而会造成资源争用,拖垮整体性能。
适合开并行的场景:
- 数据仓库中的大表扫描和聚合操作
- 数据库备份、统计信息收集等维护任务
- 报表类查询,对响应时间有要求但能容忍资源占用的业务
sql复制-- 表级别设置并行度
ALTER TABLE t_big_table PARALLEL 4;
-- SQL级别使用提示
SELECT /*+ PARALLEL(t, 4) */ COUNT(*) FROM t_big_table t;
并行度不是越高越好。我见过一个系统把并行度设置成32,结果并行进程互相争抢IO,性能反而下降。一般建议并行度不超过主机CPU核数的一半,同时观察主机负载动态调整。
6.4 分组顶层计划固化:SQL Plan Baseline与Profile
当SQL的执行计划反复被优化器改写,且业务方无法快速修改SQL文本时,SQL Plan Baseline是最实用的手段。它的核心逻辑是:把当前最优的执行计划“锁”下来,优化器只能在该计划不可用时才能跳回其他计划。
创建SQL Plan Baseline的方式:
sql复制-- 创建基线
DECLARE
ret PLS_INTEGER;
BEGIN
ret := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(
sql_id => '&sql_id',
plan_hash_value => &plan_hash_value
);
END;
/
创建后需要验证基线是否生效:
sql复制SELECT sql_handle, plan_name, enabled, accepted, fixed
FROM dba_sql_plan_baselines
WHERE sql_text LIKE '%关键字%';
Baseline的设置逻辑要清楚:它只是“让优化器优先使用指定计划”,如果后续数据量发生巨大变化,被固定的计划可能成为次优解。所以Baseline不是一劳永逸的方案,仍然需要周期性回顾和清理。
SQL Profile和SQL Patch是另外两种计划固化方式,它们的区别是:Profile是修正优化器的估算错误,Patch是直接修改执行计划结构。实际运维中我使用Baseline最多,因为它对应用透明,切换和回滚都简单。
6.5 高频调优参数:哪些值得动、哪些别乱动
Oracle的调优参数非常多,但生产环境中值得动的参数其实是有限的。我梳理一下几个和性能直接相关的核心参数:
- PGA_AGGREGATE_TARGET:PGA总大小,影响排序、哈希关联等内存操作。设置过小会导致大量磁盘排序,过大会造成内存浪费。
- SGA_TARGET / SGA_MAX_SIZE:SGA总大小,自动管理下由Oracle动态分配。设置不合理会导致buffer cache不足,物理读增多。
- DB_FILE_MULTIBLOCK_READ_COUNT:全表扫描时单次IO读取的块数,过小会让全表扫描变慢,过大在某些存储上反而效率下降。
- OPTIMIZER_MODE:ALL_ROWS还是FIRST_ROWS,影响优化器的目标取向。OLTP系统通常用ALL_ROWS,某些交互式查询可以局部设置FIRST_ROWS。
- STATISTICS_LEVEL:TYPICAL是标准配置,ALL会额外收集执行统计信息,通常只在排查问题时临时开启。
关于参数调整,我的切身建议是:每次只改一个参数,改完要有足够时间观察效果,不要同时改多个参数而无法分辨是谁起了作用。还要记录调整前的基线数据作为对比依据。生产系统上参数调整必须要走变更流程,改之前确认参数在文档中的官方说明和影响范围。
7. 锁与并发:性能问题的另一副面孔
7.1 行锁等待的特征与定位
锁等待在表现上经常和“慢SQL”混淆。用户说“查询很慢”,实际上是一直在等待某个事务提交后释放行锁。这类问题的特征是:SQL本身执行计划没问题,但就是迟迟不返回结果。
定位锁等待的核心视图是V$LOCK、V$SESSION、DBA_BLOCKERS、DBA_WAITERS:
sql复制-- 查看当前锁等待的会话
SELECT
b.sid AS blocker_sid,
b.username AS blocker_user,
w.sid AS waiter_sid,
w.username AS waiter_user,
w.blocking_session_status,
w.event,
w.seconds_in_wait
FROM v$session w
LEFT JOIN v$session b ON w.blocking_session = b.sid
WHERE w.blocking_session IS NOT NULL
ORDER BY w.seconds_in_wait DESC;
定位到阻塞者之后,可以通过V$SESSION的SQL_ID找到它在执行的SQL,看它是否长时间未提交。常见原因包括:
- 应用代码中事务忘记COMMIT,一直持有锁
- 事务内部有外部服务调用(HTTP请求等),导致事务长时间不结束
- 定时批量任务与在线事务互相竞争同一行数据
7.2 死锁的处理与预防
死锁在Oracle中会自动检测,其中一个会话会收到ORA-00060错误并被回滚。运维侧能做的是通过alert日志中的死锁信息定位涉及的对象和SQL,然后调整应用的加锁顺序。
常见预防策略:
- 多条记录的更新顺序统一(比如按主键升序)
- 事务保持简短,减少锁持有时间
- 避免在一个事务中同时更新过多行
死锁发生频率极低时,通常只需要应用开发调整一下代码逻辑;如果死锁频繁出现,就要审视整个业务的事务设计是否有问题。
7.3 热点块与缓冲区忙等待
另一种并发问题是热块争用,等待事件表现为buffer busy wait或read by other session。当一个数据块被多个会话同时请求修改时,可能出现这种争用。常见于序列生成的索引、集中插入的表等场景。
解决思路包括:
- 使用反向键索引分散索引更新热点
- 调整序列的CACHE大小,减少字典锁争用
- 分区表避免数据写入集中在一个分区
- 调整表的PCTFREE和INITRANS参数,允许更多并发事务访问同一块
这类问题在业务量暴涨时容易出现,但平时关注较少。等出现生产故障再处理往往已经晚了。
8. 实战过后的沉淀:一套可持续的Oracle健康巡检清单
8.1 日常巡检中最值得看的十项检查
结合前文提到的排查经验,我把自己日常巡检用的检查项整理如下,涵盖SQL、存储、锁、备份等维度,每项都有对应的SQL或操作,可以定期执行:
| 检查项 | 方法/视图 | 关注点 |
|---|---|---|
| 等待事件Top | V$SYSTEM_EVENT / V$SESSION_WAIT | 是否出现异常等待 |
| 慢SQL | DBA_HIST_SQLSTAT | Elapsed Time变化趋势 |
| 无效对象 | DBA_OBJECTS STATUS | 是否有需要重新编译的对象 |
| 表空间使用率 | DBA_TABLESPACE_USAGE_METRICS | 是否达到告警阈值 |
| 临时表空间 | V$TEMPSEG_USAGE | 是否被大面积占用 |
| 锁等待 | V$LOCK / DBA_BLOCKERS | 是否有长时间阻塞 |
| 归档日志 | V$ARCHIVED_LOG / V$FLASH_RECOVERY_AREA_USAGE | 归档空间是否会写满 |
| 告警日志 | alert_ |
ORA-错误、ORA-600等 |
| 统计信息 | DBA_TAB_STATS_LAST_ANALYZED | 是否有统计信息过期表 |
| 备份状态 | V$BACKUP / V$RMAN_STATUS | 备份是否正常完成 |
8.2 巡检数据怎么变成“会说话”的记录
巡检不能只是临时跑一次命令,更重要的是长期记录数据变化趋势。我建议把每次巡检的数据放入一张历史记录表,比如记录AWR的DB Time、TPS、慢SQL数量等指标,再过一段时间就能看到系统的性能趋势——是平稳、缓慢恶化,还是已经逼近临界点。
我的实际做法是每天凌晨跑一个巡检脚本,把关键指标写入一张自定义的历史表,再用报表工具定期展示趋势。这样业务报障时,我可以快速看到“系统前两周DB Time已经稳步上升”这样的趋势信息,定位时间点后就能快速判断是不是某个变更导致的。
8.3 Oracle版本差异带来的排查坑
不同版本的Oracle在排查方法上有一些差异,我这里补充几个容易踩的坑:
- 11g的AWR默认保留8天,12c以上默认保留8天。如果需要更长的历史数据,需要调整AWR保留策略
- 12c引入的多租户架构(CDB/PDB),很多视图需在PDB内查询或带CON_ID条件过滤
- 19c以后的部分参数默认值和视图结构有差异,网上搜到的旧资料不一定适用
- 12c以上版本查看执行计划时,需关注是否使用了自适应计划,同一SQL在不同执行中可能有不同计划
版本差异往往在升级或迁移过程中暴露出来。迁移后性能变差是常见问题,除了检查统计信息,还要注意新版本下默认参数和初始化配置的变化。
8.4 上线变更前的性能评估清单
在开发环境或测试环境做性能评估时,有几个环节容易遗漏:
- 确认SQL在非空表上的执行计划(空表上的执行计划没有参考价值)
- 用接近生产的数据量级测试(小数据集测不出问题)
- 测试真实的并发场景(单用户调优的结果在并发下可能完全失效)
- 收集测试环境的AWR报告和执行计划作为基线
上线之前,我会要求开发团队提交所有核心SQL的执行计划和预期数据量,至少能保证在真实的执行计划层面没有明显问题。
我个人最重视的一条经验是:数据库性能优化没有终点,数据量在增长,业务逻辑在变化,曾经最优的方案会在某个时间点变成瓶颈。与其等故障发生时才拼命排查,不如把巡检、监控、审查固化为日常运维的常规动作。每次排查完一个性能问题,把根因、排查过程、解决手段都记录下来——这些文字才是运维工程师最宝贵的资产。
