GBase 8c 统计信息不准时,优化器会把路带偏
GBase 8c 统计信息不准时,优化器会把路带偏
我最近看 GBase 8c 维护和调优资料时,对 ANALYZE、VACUUM、autovacuum 这几块又重新做了一次归纳。以前现场遇到慢 SQL,我很容易先盯索引、SQL 写法、锁等待、资源队列,但后来发现有一类问题特别容易被忽略:表数据已经变了很多,统计信息还停留在旧状态,优化器拿着不准的基数去选计划,最后看起来就像“同一条 SQL 突然不认识路了”。
这种问题在 GBase 8c 里并不少见,尤其是业务表每天有批量导入、状态字段分布变化快、按时间滚动写入、分区表只热写最近几个分区的场景。SQL 本身可能没有变,索引也还在,但执行计划从索引扫描变成全表扫描,或者 join 顺序突然反过来,现场排查时就会比较绕。
统计信息不是附属信息
我自己理解下来,GBase 8c 优化器做执行计划时,最关心的不是表名和 SQL 长什么样,而是“预计会返回多少行”“过滤条件选择率多高”“join 后大概膨胀还是收缩”。这些判断依赖统计信息。
常见统计信息大致包括这些内容:
| 统计对象 | 主要用途 | 不准时的典型影响 |
|---|---|---|
| 表行数、页数 | 判断扫描成本 | 小表被当成大表,大表被当成小表 |
| 列值分布 | 估算过滤条件选择率 | 低选择性条件被误判为高选择性 |
| NULL 比例 | 判断 IS NULL、外连接过滤 |
NULL 多的列估算偏差明显 |
| distinct 值数量 | 判断分组、去重、join 扩散 | 聚合、hash join 内存估算偏小 |
| 多列相关性 | 判断组合条件选择率 | 地区+状态 这类组合条件经常估错 |
GBase 8c 支持集中式和分布式部署。真正落到分布式现场时,还要多看一层:SQL 通常从 CN 进入并生成计划,数据访问发生在 DN 上。如果业务表的数据变化集中在某几个 DN,或者分布式表的热点列分布快速变化,统计信息偏差可能会放大成执行计划偏差。单机上只是一次错误扫描,分布式场景里还可能变成不必要的数据重分布、远程扫描增多、DN 间负载不均。
所以我现在看慢 SQL,除了看执行计划节点,也会顺手确认统计信息更新时间。这个动作成本很低,但经常能少走很多弯路。
哪些现象更像统计信息问题
下面这些现象,我会优先把统计信息列入排查范围。
| 现场现象 | 我会重点怀疑的方向 | 先做的动作 |
|---|---|---|
| SQL 没改,数据批量导入后突然变慢 | 表行数、列分布已经变了 | 查 last_analyze、执行 ANALYZE 后对比计划 |
| 新增索引后效果不稳定 | 优化器估算成本不准 | 对相关表和相关列刷新统计信息 |
| 查询条件命中很少数据,却走了全表扫描 | 选择率估算偏高 | 看过滤列是否长期未分析 |
| join 顺序突然变化 | 两边表基数估错 | 对 join key、过滤列做分析 |
| 分区表只查最近分区仍然慢 | 热分区统计信息滞后 | 单独关注近期分区或高频变更对象 |
count/group by/distinct 忽快忽慢 |
distinct 值变化明显 | 检查统计目标值和列分布 |
有些慢 SQL 看起来像“索引失效”,但从落地角度看,索引只是优化器可选路径之一。优化器认为走索引不划算时,就算索引存在,也可能不会选。问题不在索引本身,而在估算输入已经偏了。
我常用的第一轮检查 SQL
第一轮我不急着改 SQL,而是先看对象最近有没有被分析、自动分析有没有跑、死亡元组是不是明显堆积。示例里用的是业务库常见命名,实际环境替换 schema 和表名即可。
-- 1. 先确认自动维护相关参数状态
SHOW autovacuum;
SHOW track_counts;
SHOW autovacuum_max_workers;
SHOW autovacuum_naptime;
SHOW default_statistics_target;
-- 2. 看用户表统计信息更新时间和维护次数
SELECT
schemaname,
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum,
last_analyze,
last_autoanalyze,
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count
FROM pg_stat_user_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY COALESCE(last_autoanalyze, last_analyze) NULLS FIRST,
n_live_tup DESC
LIMIT 30;
-- 3. 对单表进一步看 reltuples 和 relpages
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
c.reltuples,
c.relpages
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'dwd'
AND c.relname = 'order_fact';
这里我会特别看三个点。
第一,last_autoanalyze 长期为空或者很久没变,说明自动分析可能没触发,也可能参数没有生效。第二,n_live_tup 和真实业务量差距很大时,说明优化器参考的行数已经失真。第三,n_dead_tup 很高时,不仅可能影响扫描成本,也说明表经历了大量更新或删除,统计信息大概率也需要同步刷新。
一个比较贴近现场的例子
假设有一张订单事实表,白天持续写入,夜里有状态回写任务。业务查询按地区、订单状态、创建日期筛选近 7 天订单。
SELECT
region_id,
count(*) AS order_cnt,
sum(pay_amount) AS pay_amt
FROM dwd.order_fact
WHERE create_time >= current_date - interval '7 day'
AND order_status IN ('PAID', 'REFUNDING')
AND region_id = '3201'
GROUP BY region_id;
这类 SQL 的特点是:时间条件、状态条件、地区条件叠在一起。单看 region_id 可能选择率不低,单看 order_status 也不一定特别低,但组合在一起以后,返回数据可能很少。统计信息旧的时候,优化器可能把组合条件估得过宽,于是倾向于全表扫描或不合适的 join 顺序。
排查时我会先保存刷新前的计划。
EXPLAIN VERBOSE
SELECT
region_id,
count(*) AS order_cnt,
sum(pay_amount) AS pay_amt
FROM dwd.order_fact
WHERE create_time >= current_date - interval '7 day'
AND order_status IN ('PAID', 'REFUNDING')
AND region_id = '3201'
GROUP BY region_id;
然后只对相关对象做一次分析,不先动 SQL。
ANALYZE VERBOSE dwd.order_fact;
EXPLAIN VERBOSE
SELECT
region_id,
count(*) AS order_cnt,
sum(pay_amount) AS pay_amt
FROM dwd.order_fact
WHERE create_time >= current_date - interval '7 day'
AND order_status IN ('PAID', 'REFUNDING')
AND region_id = '3201'
GROUP BY region_id;
如果刷新统计信息以后,计划里的估算行数明显贴近实际、扫描方式发生变化、join 顺序恢复合理,这时我通常不会马上去改 SQL,而是反过来查自动分析为什么没及时跟上。否则只是手工 ANALYZE 救了一次,下一个批处理窗口之后还会复现。
ANALYZE、VACUUM、VACUUM FULL 要分清
我以前也见过一种处理方式:只要慢,就先 VACUUM FULL。从维护角度看,这个动作太重,而且容易把问题混在一起。GBase 8c 里这些动作关注点不同,现场最好分开判断。
| 操作 | 主要作用 | 对业务影响 | 适合场景 |
|---|---|---|---|
ANALYZE |
收集统计信息 | 相对轻量,会消耗一定 I/O | 数据分布变化后,计划估算不准 |
VACUUM |
清理可回收元组、维护可见性信息 | 可与多数 DML 并行,但有 I/O 压力 | 更新、删除较多,死亡元组积累 |
VACUUM ANALYZE |
清理并刷新统计信息 | 比单独分析更重 | 表同时存在膨胀和统计滞后 |
VACUUM FULL |
重写表并回收更多磁盘空间 | 锁更重,对业务影响更大 | 明确需要空间回收,且有维护窗口 |
我更倾向于先用 ANALYZE 验证计划问题,再决定是否需要 VACUUM。如果只是统计信息过期,直接上 VACUUM FULL 既慢,也可能引入锁等待。特别是核心交易表,重操作要放到维护窗口里评估。
自动分析参数不要只看默认值
GBase 8c 提供 autovacuum 机制,自动执行清理和分析。这里有一个容易忽略的前提:统计收集要开着,自动清理线程也要能运行。在分布式环境中,CN、DN 的参数状态都需要核对,不能只看入口节点。
我一般按下面这张表理解这些参数。
| 参数 | 关注点 | 调整思路 |
|---|---|---|
autovacuum |
是否启用自动清理/分析 | 通常保持开启 |
track_counts |
是否收集运行统计 | autovacuum 依赖它判断触发条件 |
autovacuum_max_workers |
自动维护并发能力 | 大量热表时过小会排队 |
autovacuum_naptime |
检查周期 | 变更频繁的库可以适当缩短 |
autovacuum_analyze_threshold |
触发自动分析的基础行数 | 小表避免过于频繁,大表避免长期不触发 |
autovacuum_analyze_scale_factor |
按表规模计算分析阈值 | 大表通常需要调低 |
autovacuum_vacuum_threshold |
触发自动清理的基础行数 | 配合死亡元组治理 |
autovacuum_vacuum_scale_factor |
按表规模计算清理阈值 | 更新删除多的大表建议单独设置 |
vacuum_cost_delay |
控制清理节奏 | 降低维护动作对业务 I/O 的冲击 |
maintenance_work_mem |
维护操作可用内存 | 大表维护时适当提高效率更好 |
default_statistics_target |
默认统计采样目标 | 列分布复杂时可评估调高 |
自动分析触发可以简单理解成:当表变更行数超过“基础阈值 + 表规模 × 比例因子”时,就可能触发分析。大表的问题在于,比例因子看着不大,乘上表规模以后就是一个很大的数。例如千万级表用 10% 的比例,意味着大量数据已经变化后才触发,这对状态分布变化快的表并不友好。
所以我更常用表级参数,而不是全库一刀切。
ALTER TABLE dwd.order_fact SET (
autovacuum_analyze_scale_factor = 0.02,
autovacuum_analyze_threshold = 5000,
autovacuum_vacuum_scale_factor = 0.05,
autovacuum_vacuum_threshold = 10000
);
-- 调整后主动刷新一次,避免等待下一轮自动触发
ANALYZE dwd.order_fact;
这种方式适合高频变更的大表。对于很少更新的维表,我不会刻意调得太激进,否则维护动作本身也会产生额外负担。
多列条件经常误估时,可以考虑扩展统计
还有一类问题,单纯刷新统计信息也只能缓解,不能完全解决。比如 region_id 和 branch_id 强相关,order_status 和 finish_time 强相关。优化器如果按独立列估算,组合条件就容易偏。
GBase 8c 支持创建扩展统计对象,适合处理有关联的多列条件。示例:
CREATE STATISTICS st_order_region_status (dependencies)
ON region_id, order_status
FROM dwd.order_fact;
ANALYZE dwd.order_fact;
如果问题集中在多列 distinct 估算,也可以根据版本能力选择 ndistinct 类型。我的习惯是先通过执行计划确认“估算行数和实际行数偏差很大”,再考虑扩展统计。否则统计对象建得太多,也会增加维护复杂度。
批处理和分区表要把统计刷新纳入流程
数据仓库、报表库或者混合负载环境里,很多表不是持续均匀变化,而是批量变化。比如夜间导入一批事实数据,凌晨回写状态,早上报表集中查询。这个时候只依赖自动分析,时间上可能刚好错开。
我更倾向于把统计刷新作为批处理收尾动作之一。
-- 批量装载或状态回写结束后
ANALYZE dwd.order_fact;
ANALYZE dwd.order_detail;
ANALYZE dim.region_info;
如果是分区表,近期热分区变化最明显,治理重点也应该放在热分区和全局查询路径上。不要只看整张表有没有分析过,还要结合查询命中的分区、分区裁剪是否生效、最近分区数据量是否突增。
批处理流程里我会加一个简单检查,避免任务成功但统计信息没刷新。
SELECT
schemaname,
relname,
last_analyze,
last_autoanalyze,
n_live_tup
FROM pg_stat_user_tables
WHERE schemaname = 'dwd'
AND relname IN ('order_fact', 'order_detail')
ORDER BY relname;
这个检查不复杂,但对第二天的报表稳定性很有帮助。
现场处理我会按这个顺序走
真正落到现场时,我一般不直接调一堆参数,而是按闭环处理。
| 步骤 | 动作 | 判断依据 |
|---|---|---|
| 1 | 保存慢 SQL 的原始执行计划 | 看估算行数、扫描方式、join 顺序 |
| 2 | 查相关表统计时间和变更规模 | last_analyze、n_live_tup、n_dead_tup |
| 3 | 手工 ANALYZE 相关表 |
先验证是不是统计问题 |
| 4 | 再次查看执行计划 | 计划是否恢复合理 |
| 5 | 调整表级 autovacuum/analyze 参数 | 避免批处理后反复复现 |
| 6 | 对强相关组合列评估扩展统计 | 处理多列估算偏差 |
| 7 | 把统计刷新放进作业流程 | 批处理后形成稳定闭环 |
这套顺序的好处是改动逐步放大。先验证,再固化;先表级,再全局;先轻操作,再重维护。尤其是生产环境,能少动全局参数就少动全局参数,能避开 VACUUM FULL 就先避开。
几个容易踩的点
第一,ANALYZE 不是越频繁越好。频繁分析会消耗 I/O 和 CPU,对超高频写入表要结合业务窗口设置。
第二,default_statistics_target 不是调高就能解决所有计划问题。它会影响采样和统计质量,但也会增加分析成本。对少数关键列、关键表定向处理,比全库盲目调高更稳。
第三,自动分析触发依赖变更规模。如果表很大但每天只改热点小范围数据,默认比例可能让它迟迟不触发。这种表最适合做表级参数。
第四,扩展统计适合解决列相关性问题,不是替代索引。过滤条件本身没有合适访问路径时,统计再准也只能让优化器更清楚地知道“代价很高”。
第五,分布式环境下不要只在一个节点看参数。CN、DN 角色不同,统计收集、自动维护、执行计划生成之间有联动关系,巡检时要按集群视角看。
小结
我现在更愿意把统计信息看成执行计划质量的基础设施,而不是慢 SQL 排查里的附属项。GBase 8c 的 ANALYZE、VACUUM、autovacuum、扩展统计这些能力,单独看都不复杂,难点在于现场要判断什么时候该用哪个、影响面有多大、后续怎样避免复发。
如果一条 SQL 在数据变化后突然变慢,我会先问三个问题:统计信息是不是旧的,估算行数是不是偏得离谱,自动分析为什么没跟上。把这三个问题弄清楚,很多“计划漂移”的问题就不需要靠反复改 SQL 来碰运气了。
参考资料
南大通用GBase 8c例行维护之VACUUM语法说明 https://www.gbase.cn/community/post/4414
GBase 8c SQL参考 SQL语法 https://www.gbase.cn/docs/gbase-8c/05%20SQL%E5%8F%82%E8%80%83/SQL%E8%AF%AD%E6%B3%95
GBase 8c 开发者指南 系统模式 https://www.gbase.cn/docs/gbase-8c/03%20%E5%BC%80%E5%8F%91%E8%80%85%E6%8C%87%E5%8D%97/%E7%B3%BB%E7%BB%9F%E6%A8%A1%E5%BC%8F
GBase 8c数据库的自动清理功能 https://www.modb.pro/db/329527
GBASE 8C SQL参考 CREATE STATISTICS https://www.modb.pro/db/428883
评论
热门帖子
- 12025-12-01浏览数:183408
- 22023-05-09浏览数:26146
- 42023-09-25浏览数:19813
- 52020-05-11浏览数:18463