一、凌晨两点那个电话
2021 年 11 月,一个做同城配送的朋友找我,说他们的运单查询页「转圈转到用户投诉」。库是 Oracle 19.3 单实例(后来打了 RU 到 19.10),16 核 128G,数据量其实不大,ORDERS 表 6000 万行,ORDERS_DETAIL 2.4 亿行。
他在电话里的原话是:什么都没改,就改了个统计信息收集的时间。
我连上去抓了 AWR,Top SQL 里那条运单查询平均 42 秒。拿 SQL_ID 查 V$SQL_PLAN,ORDERS 表走了 FULL TABLE SCAN,而 WHERE 里那个 ORDER_NO 上明明有个唯一索引。再翻 DBA_TAB_COL_STATISTICS,前一天晚上 last_analyzed 更新了,NUM_DISTINCT 从 5800 万变成 6100 万,数据只涨了一点点,但直方图那一列从 FREQUENCY 变成了 NONE。
执行计划翻转,就这么来的。而且它不是突然崩的,是那天下午开始慢慢崩的——老的执行计划还在 library cache 里跑,跑完一批游标被挤出去,新的硬解析就开始选烂计划了。
二、AUTO_SAMPLE_SIZE 到底采了多少行
很多人的理解是「Oracle 自己决定采 10% 还是 30%」。不是这么回事。
11g 之前,ESTIMATE_PERCENT 默认值 DBMS_STATS.AUTO_SAMPLE_SIZE 的行为是内部先做小样本估算再决定比例。11g 之后 Oracle 换了个玩法:采样行数可以很小,但 NDV(不同值个数)改用近似算法算。12c 开始是 HyperLogLog,19c 还是这套。所以你看到 DBA_TAB_COL_STATISTICS.SAMPLE_SIZE 只有 5500,NUM_DISTINCT 依然能算得挺准。
我实测过:一张 8000 万行、52 列的表,AUTO_SAMPLE_SIZE 下 GATHER_TABLE_STATS 大约 35 到 50 秒;强制 ESTIMATE_PERCENT => 100 要 11 到 13 分钟。差 20 倍,绝大多数场景 AUTO 的结果够用。
代价在直方图。METHOD_OPT 默认是 FOR ALL COLUMNS SIZE AUTO,注意是 AUTO,不是 REPEAT 也不是 SKEWONLY。Oracle 自己判断哪列要不要直方图,依据大概是:这列在谓词里出现过吗、数据倾斜吗、是不是被 bind peeking 用过。这个判断不一定对。我那次的 ORDER_NO 是唯一索引列,本来就不该有直方图,但前一天有人手动跑了 GATHER_SCHEMA_STATS 带 METHOD_OPT => 'FOR ALL COLUMNS SIZE 254',灌进去 254 个 bucket,第二天自动任务又按 AUTO 给清了。
排查第一步其实不该是重新收集,是先看:
SELECT column_name, num_distinct, num_buckets, histogram, last_analyzed, sample_size
FROM dba_tab_col_statistics
WHERE owner = 'ERP' AND table_name = 'ORDERS'
ORDER BY column_name;
histogram 是 NONE 还是 FREQUENCY / HEIGHT BALANCED,比 NUM_DISTINCT 重要得多。
三、分区表的 INCREMENTAL,有个不写在文档里的前提
分区表是另一个坑。
一张 300 个分区的表,默认 GATHER_TABLE_STATS 要把所有分区扫一遍。11g 引入 INCREMENTAL 之后,只扫发生变动的分区,再从分区级统计信息合成全局的。听着很美。
但 INCREMENTAL 默认是 FALSE。而且——这是我踩过的——它必须在第一次收集之前就设好。表已经有全局统计信息了再打开 INCREMENTAL,第一次收集照样全扫一遍来建 SYNOPSIS。
EXEC DBMS_STATS.SET_TABLE_PREFS('ERP', 'ORDERS', 'INCREMENTAL', 'TRUE');
EXEC DBMS_STATS.SET_TABLE_PREFS('ERP', 'ORDERS', 'INCREMENTAL_LEVEL', 'PARTITION');
GRANULARITY 我一般直接 AUTO,在分区表上 AUTO 等价于 GLOBAL AND PARTITION。但别设成 PARTITION,那样全局统计信息永远不更新,成本算出来能离谱一个数量级。
SYNOPSIS 存在 SYS.WRI$_OPTSTAT_SYNOPSIS_HEAD$ 里。如果这张表被 DELETE_STATS 删过,或者从别的库 IMPDP 导进来,SYNOPSIS 跟真实数据能差出几倍。我遇到过一次:一张 200 分区的表,全局 NUM_ROWS 报 4.2 亿,实际 count(*) 是 1.9 亿。优化器按 4.2 亿算 cost,把走索引的查询改成了 HASH JOIN FULL SCAN。
修的办法不是重新收集,是先把 SYNOPSIS 清掉再全量收:
EXEC DBMS_STATS.DELETE_TABLE_STATS('ERP','ORDERS', cascade_parts => TRUE, cascade_columns => TRUE, no_invalidate => FALSE);
EXEC DBMS_STATS.GATHER_TABLE_STATS('ERP','ORDERS', granularity => 'AUTO', degree => 8);
no_invalidate 这个参数值得单独拎出来说。默认是 DBMS_STATS.AUTO_INVALIDATE,意思是收完之后不立刻让游标失效,等下次硬解析再看。这是 10g 为了治 library cache latch 加的。但代价就是:你收完统计信息,老计划还能再跑一阵子。所以上面我写了 FALSE,宁可让计划立刻失效。
四、19c 的 Real-Time Statistics,我只开一半
19c 加了个 Real-Time Statistics。传统 DML(走 conventional path 的 INSERT/UPDATE/DELETE)发生时,把变动的行数记在 SGA 里,优化器解析时把这部分增量叠加上去。
默认是关的。两个隐藏参数:
_optimizer_gather_stats_on_conventional_dml = TRUE
_optimizer_use_stats_on_conventional_dml = FALSE
注意这个组合:19c 默认一直在采集,但优化器不用。采集很轻,用了反而可能出问题。
我的做法跟不少人相反——跑批混合库我开,纯 OLTP 前置库我不开。
跑批库的问题是统计信息窗口经常够不着。白天跑完一批,表数据翻一倍,晚上 22 点才开始收,中间这七八个小时的计划全是按旧数据算的。实时统计能救这一段。
但纯 OLTP 前置库我不开。它的增量估算在高峰期会抖,我见过同一条 SQL 十分钟内换了 4 个执行计划,每次都是物理读。那种库我宁愿用 SPM 把计划钉死。
开的方式:
ALTER SYSTEM SET "_optimizer_use_stats_on_conventional_dml" = TRUE SCOPE=SPFILE;
-- 需要重启,或者至少让已有游标失效
ALTER SYSTEM FLUSH SHARED_POOL;
FLUSH SHARED_POOL 这个操作在生产上要掂量,别在上午十点干。
五、我自己现在的三条习惯
第一,核心表的统计信息我倾向于让它「稳定」而不是「新鲜」。STALE_PERCENT 默认 10%,我经常调到 30 甚至更高,个别表直接 LOCK:
EXEC DBMS_STATS.LOCK_TABLE_STATS('ERP','ORDERS');
LOCK 之后自动任务不碰它,但手动 GATHER_TABLE_STATS 带 FORCE => TRUE 还是能收。这个我挺喜欢:主动权在我手里,不会半夜被自动任务把计划改掉。
第二,重要的 SQL 上 SQL Plan Baseline,不指望统计信息永远选对。从游标缓存里抓:
DECLARE
l_plans PLS_INTEGER;
BEGIN
l_plans := DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(sql_id => '7y8k1m2n3p4q5');
DBMS_OUTPUT.PUT_LINE('loaded ' || l_plans);
END;
/
第三,把默认的自动收集窗口换掉。DEFAULT_MAINTENANCE_WINDOW_GROUP 是周一到周五 22:00 到 02:00,周末全天。跑批系统根本不在这个窗口里,纯属给数据库自己找活干。我一般直接关掉,自己写 Scheduler job 挂在跑批结束之后。
BEGIN
DBMS_AUTO_TASK_ADMIN.DISABLE(
client_name => 'auto optimizer stats collection',
operation => NULL,
window_name => NULL);
END;
/
关之前记得确认没有别的东西依赖这个窗口,比如段空间 Advisor 和 Automatic SQL Tuning Advisor 是共用窗口的。
这套东西没有一劳永逸的答案。同一个参数在两个库上结论完全相反,是常事。我现在做优化基本是这个顺序:先看 AWR 确认是不是计划的锅,再看 DBA_TAB_COL_STATISTICS 确认是不是统计信息的锅,最后才动手。顺序错了,改半天改不到点上,还容易把好好的库改坏。