1. 问题现象:前台执行慢如蜗牛,后台却快如闪电
那天下午刚泡好咖啡,业务部门的电话就打来了:"我们的财务对账模块卡死了,整个系统都在转圈圈!"作为DBA,这种紧急呼叫早已习以为常。登录数据库后,我发现了典型的"薛定谔式SQL"现象:
- 前台业务系统执行某条对账SQL耗时超过6分钟
- 后台手工执行相同SQL仅需0.2秒
- 数据库整体指标完全正常:
sql复制-- 系统负载检查 SELECT * FROM v$system_event WHERE wait_class != 'Idle' ORDER BY time_waited DESC; -- TOP SQL检查 SELECT * FROM ( SELECT sql_id, executions, elapsed_time/1000000 sec, elapsed_time/decode(executions,0,1,executions)/1000000 avg_sec FROM v$sqlarea ORDER BY elapsed_time DESC ) WHERE rownum <=10;
这种"前台龟速,后台飞驰"的差异往往暗示着执行环境的微妙区别。就像同样的汽车在4S店试驾时动力澎湃,到了用户手里却变成老爷车——问题通常不在发动机本身,而在使用场景的差异。
2. 排查三板斧:AWR报告里的蛛丝马迹
2.1 AWR报告分析
首先生成问题时段的AWR报告:
sql复制-- 生成AWR报告
@?/rdbms/admin/awrrpti.sql
报告中的关键数据令人震惊:
| 指标 | 数值 | 正常范围 |
|---|---|---|
| 单次执行时间 | 406,814 ms | <1000ms |
| Buffer Gets | 595,764,242 | <10,000 |
| 物理读 | 12,458 | <100 |
这条SQL就像黑洞一样吞噬着系统资源,但诡异的是它的执行计划看起来非常"健康":
sql复制-- 获取SQL执行计划
SELECT * FROM TABLE(dbms_xplan.display_cursor('&sql_id'));
2.2 执行计划的"完美假象"
执行计划显示:
code复制-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 200 | 15 |
| 1 | HASH GROUP BY | | 1 | 200 | 15 |
| 2 | NESTED LOOPS | | 1 | 200 | 14 |
| 3 | TABLE ACCESS FULL | GL_VERIFY_LOG | 1 | 100 | 3 |
| 4 | INDEX UNIQUE SCAN | PK_ASSET | 1 | 100 | 1 |
-----------------------------------------------------------------------------
所有访问路径都合理:
- 小表全表扫描(仅17,710行)
- 大表走主键索引
- 没有明显的性能反模式
这就像体检报告显示各项指标正常,但病人却高烧不退——我们显然漏掉了什么。
3. 深入追踪:统计信息与执行差异
3.1 统计信息验证
首先怀疑统计信息不准:
sql复制-- 对相关表进行100%采样统计
BEGIN
dbms_stats.gather_table_stats(
ownname => 'FINANCE',
tabname => 'GL_VERIFY_LOG',
estimate_percent => 100,
method_opt => 'FOR ALL COLUMNS SIZE AUTO'
);
END;
/
刷新后问题依旧,排除统计信息问题。
3.2 绑定变量检查
检查是否存在绑定变量问题:
sql复制-- 检查SQL是否使用绑定变量
SELECT sql_id, child_number, executions, is_bind_sensitive, is_bind_aware
FROM v$sql
WHERE sql_id = '&sql_id';
结果显示未使用绑定变量,排除bind peeking导致的执行计划突变。
4. 关键突破:动态采样的启示
在反复对比前后台执行差异时,一个细节引起了我的注意:
后台执行计划中出现了:
code复制Note
-----
- dynamic sampling used for this statement (level=2)
动态采样(Dynamic Sampling)是Oracle在硬解析时,为获取更准确基数估计而进行的实时数据采样。它常见于以下场景:
- 临时表参与查询
- 无统计信息的对象
- 复杂谓词条件
这就像法医在案发现场发现了一枚特别的指纹——临时表很可能就是我们的"嫌疑人"。
5. 真相大白:临时表的"双重人格"
检查SQL文本后确认使用了临时表ASSTEMPORA:
sql复制SELECT a.asset_name, SUM(v.amount)
FROM gl_verify_log v
JOIN asset a ON v.asset_id = a.asset_id
JOIN asstempora t ON a.asset_id = t.assid -- 临时表!
WHERE v.post_date BETWEEN :start AND :end
GROUP BY a.asset_name;
进一步排查发现:
- 前台执行:业务程序会先向ASSTEMPORA插入约50万条数据
- 后台执行:临时表为空表
这就解释了所有异常:
- 前台执行时,临时表数据量大且无索引,导致嵌套循环连接性能灾难
- 后台执行时,临时表为空,执行计划看似完美
6. 解决方案:给临时表穿上"跑鞋"
6.1 索引创建方案
为临时表创建合适索引:
sql复制-- 创建临时表索引
CREATE INDEX asstempora_idx1 ON asstempora(assid) TABLESPACE temp;
注意:临时表索引也需要放在TEMPORARY表空间,否则可能影响临时表特性
6.2 效果验证
优化后指标对比:
| 指标 | 优化前 | 优化后 | 下降幅度 |
|---|---|---|---|
| 执行时间 | 406s | 1.2s | 99.7% |
| Buffer Gets | 5.9亿 | 8,542 | 99.998% |
| 物理读 | 12,458 | 23 | 99.8% |
7. 深度思考:临时表的使用哲学
这次故障给我们上了宝贵的一课:
-
临时表≠无代价
即使会话级临时表,高数据量下也需要索引支持 -
执行环境决定性能
测试环境与生产环境的数据量差异会导致完全不同的性能表现 -
动态采样是重要线索
当执行计划出现动态采样时,要特别关注临时表或特殊对象 -
全链路监控
业务系统应该记录SQL执行时的上下文信息(如临时表数据量)
8. 进阶技巧:临时表性能优化手册
8.1 临时表设计规范
| 场景 | 推荐方案 | 备注 |
|---|---|---|
| 小数据量(<1万) | 无需索引 | 索引维护开销可能超过收益 |
| 中大数据量 | 必须建索引 | 优先考虑连接字段和过滤条件 |
| 频繁清空 | GLOBAL TEMPORARY | 保留表结构,仅清空数据 |
8.2 临时表索引创建原则
-
连接字段优先
sql复制-- 连接字段必须索引 CREATE INDEX temp_idx ON temp_table(join_column); -
考虑数据生命周期
对于事务级临时表,索引应在每次数据加载后重建 -
监控索引使用
sql复制-- 检查临时表索引使用情况 SELECT * FROM v$object_usage WHERE index_name = 'ASSTEMPORA_IDX1';
9. 避坑指南:临时表常见误区
误区1:"临时表数据量小,不需要索引"
事实:业务场景下的数据量可能远超测试环境
误区2:"临时表会自动优化"
事实:Oracle对临时表的优化与普通表无异
误区3:"临时表索引影响会话性能"
事实:恰当索引的收益远大于维护开销
误区4:"动态采样能解决所有问题"
事实:动态采样仅影响基数估计,不改变访问路径
10. 终极解决方案:临时表性能检查清单
每次使用临时表时,建议完成以下检查:
- [ ] 评估预期数据量
- [ ] 检查连接字段是否有索引
- [ ] 验证执行计划中的动态采样提示
- [ ] 对比空表与满载状态的执行计划差异
- [ ] 在生产环境模拟真实数据量测试
这次故障让我深刻明白:在数据库世界里,没有"临时"的性能问题。任何表——哪怕是临时表——当它承载业务数据时,都必须被严肃对待。就像临时工干正式员工的活,就该享受正式员工的待遇,临时表参与核心业务查询时,也理应获得完整的索引支持。