GBase 8a 执行计划跑偏了?统计信息管理是关键
GBase 8a 执行计划跑偏了?统计信息管理是关键
有时候明明写了很简单的 SQL,执行计划却完全不符合预期,扫描了大量不必要的数据。这类问题十有八九是统计信息不准确导致优化器做出了错误判断。本文从统计信息的基本原理出发,结合实际运维场景,系统介绍如何管理统计信息、读懂执行计划、以及在优化器判断失误时如何手动干预。
一、什么是统计信息?
GBase 8a 的查询优化器在生成执行计划时,需要估算每个操作涉及的数据量,例如:
- 这张表大概有多少行?
- 某个过滤条件能过滤掉多少数据?
- 两张表 JOIN 后结果集大概多大?
这些估算的依据就是统计信息。统计信息不准确,优化器就会做出错误的判断,导致执行计划跑偏。
GBase 8a 的统计信息主要包含以下几类:
- 表级统计:行数、数据页数、平均行长度等
- 列级统计:列的最大值、最小值、NULL 比例、唯一值数量(基数)等
- 块级统计:粗糙索引记录的每个数据块的 min/max 范围
优化器综合利用这三类信息来估算各个执行路径的代价,从中选出代价最小的方案作为最终执行计划。当统计信息过期或失真时,优化器的代价估算就会出现偏差,轻则选错 JOIN 顺序,重则把本可以快速完成的范围扫描退化成全表扫描。
二、查看统计信息
在排查执行计划问题之前,先看清楚当前统计信息的状态,是定位问题的第一步。
-- 查看表的基本统计信息(行数、数据大小、更新时间等)
SHOW TABLE STATUS FROM analytics_db LIKE 'orders';
-- 查看列的统计信息
SELECT * FROM information_schema.COLUMN_STATISTICS
WHERE table_name = 'orders';
-- 查看索引统计信息(Cardinality 基数是重要指标)
SHOW INDEX FROM orders;
-- 查看统计信息最后更新时间
SELECT
table_name,
table_rows,
avg_row_length,
data_length,
update_time
FROM information_schema.TABLES
WHERE table_schema = 'analytics_db' AND table_name = 'orders';
重点关注 SHOW INDEX 输出中的 Cardinality(基数)字段,它表示该列估算的唯一值数量。如果 Cardinality 和实际差距很大,优化器对过滤效果的估算就会严重失真,这时需要立刻执行 ANALYZE 更新统计信息。
三、读懂 EXPLAIN 执行计划
更新统计信息之前,应该先用 EXPLAIN 看清楚当前的执行计划,明确问题出在哪里。
-- 基本执行计划
EXPLAIN SELECT SUM(amount) FROM orders
WHERE create_time >= '2024-01-01' AND region = '华东';
-- 查看分区裁剪情况
EXPLAIN PARTITIONS SELECT SUM(amount) FROM orders
WHERE create_time >= '2024-01-01';
-- 查看更详细的执行信息
EXPLAIN EXTENDED SELECT SUM(amount) FROM orders
WHERE create_time >= '2024-01-01';
EXPLAIN 输出中需要重点关注的字段:
| 字段 | 含义 | 需要警惕的情况 |
|---|---|---|
| type | 访问类型 | 出现 ALL 表示全表扫描 |
| rows | 预估扫描行数 | 数字远大于实际结果集,说明过滤效率差 |
| Extra | 额外操作 | 出现 Using filesort 或 Using temporary |
| partitions | 涉及的分区 | 本应裁剪却扫描了所有分区 |
| key | 实际使用的索引 | 为 NULL 表示没有用到任何索引 |
type 字段从好到差的顺序为:system > const > eq_ref > ref > range > index > ALL。生产环境中的分析查询出现 ALL 是需要立刻排查的信号。
四、手动更新统计信息
统计信息不会实时更新,GBase 8a 在后台会周期性地自动收集,但在数据变化较大的场景下,等待自动更新往往来不及,需要手动触发。
-- 更新单张表的统计信息
ANALYZE TABLE orders;
-- 同时更新多张表
ANALYZE TABLE orders, users, products;
-- 分区表只更新指定分区(比全表 ANALYZE 更快)
ALTER TABLE orders ANALYZE PARTITION p2024;
以下情况需要手动触发 ANALYZE:
- 大批量数据导入或删除后
- 查询执行计划突然变差,性能明显下降
- 新建表并导入数据后首次查询前
- 分区表新增或删除分区后
SHOW INDEX中 Cardinality 与实际数据量差距超过 50%
定期自动 ANALYZE 脚本示例,通过 crontab 在每天低峰期自动执行:
#!/bin/bash
# 每天凌晨3点自动更新核心表的统计信息
TABLES="orders users products regions"
for TABLE in $TABLES; do
gccli -u gbase -p'密码' analytics_db \
-e "ANALYZE TABLE $TABLE;" \
>> /var/log/gbase_analyze.log 2>&1
echo "[$(date)] ANALYZE $TABLE 完成"
done
# 加入 crontab
0 3 * * * /opt/scripts/gbase_analyze.sh
五、常见执行计划问题与解决方法
问题1:分区裁剪失效
分区裁剪失效通常是因为 WHERE 条件中对分区键做了函数运算,导致优化器无法静态推断分区范围,只能扫描全部分区。这是最常见的踩坑点之一,在实际项目迁移中非常高发。
-- 错误:YEAR() 函数导致分区裁剪失效,扫描全部分区
SELECT * FROM orders WHERE YEAR(create_time) = 2024;
-- 正确:直接范围条件,优化器能静态推断只扫描 p2024
SELECT * FROM orders
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01';
问题2:JOIN 顺序选错
当两张表数据量差异悬殊,但统计信息不准导致优化器误判了小表和大表的角色,会选出错误的 JOIN 驱动顺序,造成大量不必要的数据扫描。遇到这类问题,先执行 ANALYZE 更新统计信息,再用 EXPLAIN 确认执行计划是否恢复正常。
-- 通过 EXPLAIN 确认 JOIN 顺序(输出中第一行是驱动表)
EXPLAIN
SELECT o.order_id, u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.create_time >= '2024-01-01';
问题3:大表 JOIN 触发全量 shuffle
MPP 架构下,两张大表按非分片键做 JOIN,会触发全量跨节点数据传输,网络开销极大。优化器在统计信息准确的情况下会自动选择广播小表,但如果统计信息失真导致高估了小表的大小,就可能放弃广播策略,退化成全量 shuffle。
先更新统计信息让优化器重新评估,如果仍然不理想,再考虑用 Hint 强制广播。
-- 先更新统计信息
ANALYZE TABLE users;
-- 若仍不理想,强制广播小表
SELECT /*+ BROADCAST(u) */
o.order_id, u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id;
六、用 Hint 强制指定执行计划
当统计信息不准确或优化器判断持续失误时,可以用 Hint 手动干预执行计划。Hint 写在 SELECT 关键字后的注释块中,不影响 SQL 语义,也不会被不支持 Hint 的工具报错。
GBase 8a 常用的 Hint 类型如下:
-- 强制指定 JOIN 驱动顺序(orders 作为外层驱动表)
SELECT /*+ STRAIGHT_JOIN */
o.order_id, u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.create_time >= '2024-01-01';
-- 强制小表广播,避免大表跨节点 shuffle
SELECT /*+ BROADCAST(u) */
o.order_id, u.username
FROM orders o
JOIN users u ON o.user_id = u.user_id;
-- 强制指定查询并行度为4个线程
SELECT /*+ PARALLEL(4) */
region, SUM(amount)
FROM orders
GROUP BY region;
-- 组合使用多个 Hint
SELECT /*+ BROADCAST(u) PARALLEL(8) */
o.region, u.username, SUM(o.amount)
FROM orders o
JOIN users u ON o.user_id = u.user_id
GROUP BY o.region, u.username;
Hint 的使用原则:先排查统计信息,再考虑 SQL 改写,最后才用 Hint。Hint 是在统计信息无法及时更新的紧急情况下的临时手段,不应作为长期解决方案。一旦问题根因解决,应及时移除 Hint,否则后续数据分布变化后 Hint 反而可能成为新的性能瓶颈。
七、建立统计信息管理规范
一个稳定运行的生产环境,需要把统计信息管理纳入日常运维规范,而不是出了问题再临时处理。建议从以下几个方面建立规范:
1. 数据导入后必须 ANALYZE
任何批量数据导入(LOAD DATA、INSERT SELECT)完成后,将 ANALYZE TABLE 作为导入流程的最后一步,写入数据导入脚本中强制执行,避免遗漏。
2. 定期自动 ANALYZE
对高频写入的核心表,通过 crontab 每天低峰期自动执行 ANALYZE,确保统计信息不会因为持续写入而严重滞后。建议每天凌晨2~4点执行,避开业务高峰。
3. 监控 Cardinality 偏差
定期查询 information_schema.TABLES,对比 table_rows 与实际 COUNT(*) 的差距,差距超过 30% 即触发告警并自动执行 ANALYZE。
4. 执行计划变更监控
对核心查询定期跑 EXPLAIN,对比历史执行计划是否有变化。一旦 type 从 range 退化到 ALL,或 rows 估算值突然暴增,立刻介入排查统计信息是否失效。
八、注意事项
- ANALYZE TABLE 会消耗资源,大表分析耗时较长,建议在业务低峰期执行,避免影响在线查询
- 统计信息有一定滞后性,高频写入的表建议每天自动执行一次 ANALYZE,不能完全依赖自动收集
- Hint 是最后手段,不要滥用 Hint 绕过优化器,滥用 Hint 会使 SQL 维护成本大幅上升
- 执行计划不稳定时优先检查统计信息,而不是急着修改 SQL 或增加硬件投入
- 分区表统计信息按分区单独维护,新增分区后记得及时 ANALYZE,否则新分区数据的统计信息为空,优化器无法做出正确判断
- 不要在业务高峰期对大表执行 ANALYZE,ANALYZE 期间会对表加读锁,影响并发查询性能
评论
GBase 8a 作为列存 MPP 数据库,优化器100% 依赖统计信息生成执行计划 —— 统计信息不准,优化器就会误判数据分布、行数、过滤率,最终导致全表扫描、数据重分布倾斜、JOIN 顺序错误,简单 SQL 也会跑崩。
这篇是纯生产实战,从原理→问题定位→手动管理→强制纠正,一站式解决执行计划跑偏问题。
一、先搞懂:GBase 8a 统计信息是什么?
GBase 8a 的统计信息是优化器的 “导航地图”,核心存储 3 类关键数据:
表基础信息:总行数、数据块数、磁盘占用
列维度信息:唯一值个数(NDV)、最小值 / 最大值、空值数量
数据分布信息:列数据倾斜度、各节点数据分布
优化器靠这些信息决定:
走索引还是全表扫描
JOIN 用小表广播还是大表 Hash 重分布
过滤条件能筛掉多少数据,是否下推
统计信息不准 = 导航地图画错 → 执行计划必然跑偏。
二、最常见的 3 种 “执行计划跑偏” 场景(全是统计信息背锅)
1. 明明有过滤条件,却强制全表扫描
现象:WHERE id=100 只查 1 行,优化器却扫描全表
原因:统计信息显示该列无索引、唯一值极少、过滤率为 0,优化器认为索引不如全表扫快
2. JOIN 顺序完全错误,大表先 JOIN
现象:小表 JOIN 大表,优化器把大表放前面,产生海量中间数据
原因:统计信息行数不准,把小表识别成大表、大表识别成小表
3. 数据倾斜严重,查询卡在单个节点
现象:部分节点 CPU 100%,其他节点空闲,查询极慢
原因:统计信息未记录数据倾斜,优化器未做倾斜优化
三、核心:GBase 8a 统计信息手动管理(运维必用命令)
GBase 8a 不会自动实时更新统计信息,大批量导入、删除、更新数据后,必须手动收集,这是避免执行计划跑偏的核心。
1. 查看统计信息(判断是否准确)
sql
-- 1. 查看表的统计信息
SHOW TABLE STATUS LIKE '表名';
-- 2. 查看列的详细统计信息(最关键)
SHOW COLUMNS STATISTICS FOR 表名;
-- 3. 查看统计信息最后更新时间
SELECT TABLE_NAME, LAST_ANALYZE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='表名';
判断标准:
行数和实际COUNT(*)偏差超过10% → 不准
唯一值(NDV)明显不符合业务 → 不准
最后更新时间是几天前 / 几周前 → 不准
2. 手动收集统计信息(解决跑偏的第一步)
GBase 8a 专用命令,轻量级、不锁表、业务期可执行:
sql
-- 全量收集表+所有列统计信息(推荐)
ANALYZE TABLE 表名 COMPUTE STATISTICS;
-- 快速采样收集(大表专用,速度快,精度够用)
ANALYZE TABLE 表名 ESTIMATE STATISTICS SAMPLE 20 PERCENT;
-- 只收集单列统计信息(针对过滤/JOIN列精准优化)
ANALYZE TABLE 表名 COMPUTE STATISTICS FOR COLUMNS 列名1,列名2;
3. 删除过期统计信息(极端情况用)
统计信息严重失真时,先删除再重新收集:
sql
DELETE STATISTICS ON 表名;
四、实战:执行计划跑偏了?3 步快速修复
第 1 步:查看执行计划,确认问题
sql
-- 查看执行计划
EXPLAIN
SELECT * FROM 表名 WHERE 条件;
-- 重点看:
-- 1. 是否全表扫描(type: ALL)
-- 2. 预估行数(rows)和实际是否偏差极大
-- 3. JOIN顺序是否合理
第 2 步:更新统计信息(90% 问题直接解决)
sql
-- 直接执行,无需停业务
ANALYZE TABLE 表名 COMPUTE STATISTICS;
执行完成后,新查询会自动使用新统计信息,正在运行的查询不受影响。
第 3 步:手动干预(优化器仍判断失误时)
GBase 8a 支持SQL HINT强制指定执行计划,绕开错误统计信息:
sql
-- 1. 强制使用索引
SELECT /*+ INDEX(表名 索引名) */ * FROM 表名 WHERE 条件;
-- 2. 强制JOIN顺序
SELECT /*+ LEADING(小表 大表) */ * FROM 小表 JOIN 大表 ON 条件;
-- 3. 强制小表广播(解决JOIN慢)
SELECT /*+ BROADCAST(小表) */ * FROM 小表 JOIN 大表 ON 条件;
五、生产运维最佳实践(避免统计信息导致问题)
数据变更后必收集
大批量INSERT/DELETE/UPDATE、数据导入完成后,立即执行ANALYZE TABLE。
定期自动收集
核心大表:每天凌晨低峰期执行一次采样统计
小表:每周执行一次全量统计
优先收集关键列
只针对WHERE、JOIN、GROUP BY列收集,节省资源。
不盲目全表收集
TB 级大表用ESTIMATE STATISTICS采样,避免长时间占用 IO。
热门帖子
- 12025-12-01浏览数:182110
- 22023-05-09浏览数:24377
- 42023-09-25浏览数:17592
- 52020-05-11浏览数:16623