GBase 8a 列统计信息与查询优化器调优
执行计划的好坏,根本上取决于优化器手里的信息准不准。GBase 8a 的优化器依赖列统计信息来估算每个算子的数据量,进而决定是 Hash Redistribute 还是 Broadcast、是先过滤再 JOIN 还是先 JOIN 再过滤。本文从统计信息的采集原理讲起,覆盖统计信息过期的诊断、手动触发更新、直方图配置,以及通过 Hint 干预优化器决策的方法。
一、统计信息在执行计划中的作用
GBase 8a 的查询优化器是基于代价(Cost-Based Optimizer,CBO)的。所谓代价模型,是指优化器对每种可能的执行路径都计算一个"代价分",选择代价最低的那条路径执行。代价的计算依赖一系列估算,而这些估算的准确程度,直接由列统计信息决定。
统计信息主要包括以下几类数据:表的总行数(用于估算扫描代价)、每列的唯一值数量(NDV,Number of Distinct Values,用于估算 GROUP BY 后的行数和 Hash 桶数量)、列的最小值和最大值(用于估算 WHERE 过滤后的行数)、空值比例,以及对于分布不均匀的列,还有直方图(Histogram,描述值的分布情况)。
当统计信息与实际数据严重脱节时,优化器的估算就会出错,进而生成糟糕的执行计划。最典型的症状是:某张表已经有 5 亿行,但统计信息还是刚建表时收集的 100 万行,优化器认为它是一张小表,选择了 Broadcast 策略,实际执行时把 5 亿行数据广播到所有节点,把内存和网络都压垮了。统计信息过期是生产环境中"莫名其妙"出现超慢查询的高频根因之一,值得高度重视。
二、查看当前统计信息
2.1 表级统计信息
-- 查看表的行数统计(优化器视角)
SELECT
table_name,
table_rows,
data_length,
create_time,
update_time
FROM information_schema.tables
WHERE table_schema = 'sales_db'
ORDER BY table_rows DESC;
table_rows 是优化器使用的估算行数,不是实时精确值。如果这个数字与你对实际数据量的预期差距超过一倍,说明统计信息需要更新。
2.2 列级统计信息
-- 查看各列的统计信息(NDV、最小值、最大值、空值率)
SELECT
column_name,
cardinality, -- NDV,唯一值数量
nullable,
data_type
FROM information_schema.statistics
WHERE table_schema = 'sales_db'
AND table_name = 'orders'
ORDER BY cardinality DESC;
cardinality 字段对优化器的决策影响最大:它决定了 GROUP BY 后估算的输出行数,进而影响聚合算子的内存预算;也直接影响 JOIN 时估算的匹配行数,进而影响 HashJoin 还是 NestLoop 的选择。
2.3 直方图信息
对于数据分布严重不均匀的列(比如某个 status 字段中 90% 的值都是 1,但只有 10% 的查询会带 status = 0 的过滤条件),普通的 NDV 统计无法描述这种不均匀性,需要直方图才能让优化器做出准确的选择性估算。
-- 查看某列是否已有直方图统计
SELECT
table_name,
column_name,
histogram
FROM information_schema.columns
WHERE table_schema = 'sales_db'
AND table_name = 'orders'
AND column_name IN ('status', 'dept_id', 'order_date');
三、统计信息的收集
3.1 ANALYZE TABLE:手动触发统计信息收集
-- 收集单张表的统计信息
ANALYZE TABLE orders;
-- 收集指定列的统计信息(只更新关键列,速度更快)
ANALYZE TABLE orders UPDATE HISTOGRAM ON (customer_id, dept_id, order_date);
ANALYZE TABLE 会扫描表中的数据来计算各项统计指标。对于大表,这个操作本身会产生一定的 I/O 负载。在生产环境中,建议把 ANALYZE 安排在业务低峰期(如凌晨)执行,避免与重要查询竞争 I/O 资源。
执行 ANALYZE 后,统计信息会立即刷新到内存,后续的查询计划生成就会使用新的统计数据,不需要重启任何进程。
3.2 统计信息自动收集参数
GBase 8a 支持配置统计信息自动收集的触发条件:当表的数据变化量超过一定比例时,自动触发后台统计信息更新。
# gcluster 和 gnode 的 gbase.cnf
# 开启自动统计信息收集
_gbase_auto_analyze = 1
# 触发自动收集的数据变化行数阈值(绝对值)
_gbase_auto_analyze_threshold_rows = 1000000
# 触发自动收集的数据变化比例(相对于上次 ANALYZE 时的行数)
_gbase_auto_analyze_threshold_ratio = 0.1 # 变化超过 10% 触发
# 自动收集的时间窗口(只在此时间段内执行,避免影响业务)
_gbase_auto_analyze_start_time = '01:00:00'
_gbase_auto_analyze_end_time = '05:00:00'
配置自动收集的注意事项:阈值不要设得太低,否则每次小批量数据加载后都触发 ANALYZE,会频繁产生后台 I/O 负载。对于每天有固定批量数据加载的表,建议关闭自动收集,在加载完成后在 ETL 脚本中显式调用 ANALYZE TABLE,这样时机更可控。
3.3 批量更新所有表的统计信息
在数据仓库首次上线、或者做了大规模数据迁移之后,通常需要对所有表做一次统计信息全量更新:
#!/bin/bash
# refresh_all_statistics.sh
# 对指定数据库的所有表执行 ANALYZE,适合在数据仓库上线初期或迁移后执行
DB="sales_db"
GCCLI="gccli -u dba -pdba_password $DB"
# 获取所有表名
TABLES=$($GCCLI -e "SHOW TABLES" 2>/dev/null | grep -v Tables_in)
SUCCESS=0
FAILED=0
for table in $TABLES; do
echo -n "ANALYZE $table ... "
START=$(date +%s)
$GCCLI -e "ANALYZE TABLE $table" > /dev/null 2>&1
STATUS=$?
END=$(date +%s)
if [ $STATUS -eq 0 ]; then
echo "OK ($(( END - START ))s)"
SUCCESS=$((SUCCESS + 1))
else
echo "FAILED"
FAILED=$((FAILED + 1))
fi
done
echo ""
echo "完成:成功 ${SUCCESS} 张,失败 ${FAILED} 张"
四、统计信息过期的诊断
4.1 对比估算行数与实际行数
统计信息过期最直接的表现,是 EXPLAIN 中的 rows 估算与实际执行时扫描的行数相差悬殊。
-- 步骤 1:看 EXPLAIN 的估算行数
EXPLAIN
SELECT dept_id, SUM(amount)
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY dept_id;
-- 注意 SeqScan 算子的 rows 字段
-- 步骤 2:与实际行数对比
SELECT COUNT(*) FROM orders WHERE order_date >= '2024-01-01';
如果 EXPLAIN 显示 rows=1000000,但实际 COUNT 返回 250000000,说明统计信息严重滞后,优化器低估了数据量,可能选择了不适合大数据量的执行策略。
4.2 通过 dql_statistic 发现估算偏差
-- 查看最近慢查询的 rows_examined(实际扫描行数)
-- 与 EXPLAIN 中的 rows 估算对比
SELECT
LEFT(sql_text, 150) AS sql,
query_time,
rows_examined,
rows_sent
FROM gclusterdb.slow_log
WHERE start_time >= CURDATE() - INTERVAL 3 DAY
ORDER BY query_time DESC
LIMIT 20;
当 rows_examined 远大于你对这条 SQL 应该扫描行数的预期时,有两种可能:分区裁剪未生效(最常见),或者统计信息过期导致优化器选择了全表扫描路径。用 EXPLAIN 结合 ANALYZE TABLE 后重新 EXPLAIN,可以区分这两种情况。
五、直方图:处理数据分布不均的利器
5.1 什么时候需要直方图
当一列的数据分布严重不均匀,普通的 NDV 统计(只告诉优化器"有多少个不同值")不足以准确估算过滤条件的选择性时,需要直方图。
典型场景:status 列有 4 个不同值(0/1/2/3),但 98% 的数据是 status = 1,只有 2% 是其他值。如果查询带 WHERE status = 0,实际只需扫描约 0.5% 的数据,但没有直方图时优化器会用 1/4(25%)来估算,大幅高估扫描量,可能因此选择了不必要的全表 Broadcast。有了直方图,优化器就能知道 status = 0 的选择性约为 0.5%,从而做出更准确的代价估算。
类似场景还有:按时间范围过滤时,近期数据密集而历史数据稀疏(数据随时间增长的典型特征);地区分布极不均匀的数据(一线城市数据量远大于三四线城市)。
5.2 收集直方图
-- 对分布不均匀的列收集直方图
ANALYZE TABLE orders UPDATE HISTOGRAM ON (status, province, dept_id);
-- 查看收集结果
SELECT
column_name,
histogram
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'orders'
AND column_name IN ('status', 'province');
5.3 直方图的桶数配置
直方图用等宽或等频的"桶"来描述数据分布,桶数越多,描述越精细,但占用的元数据空间也越多。对于基数不高的列(如 status、province),默认桶数通常够用;对于连续型的数值列(如 amount),可能需要更多桶才能描述分布形态:
# gbase.cnf
# 直方图的最大桶数(默认 64)
_gbase_histogram_buckets = 128
六、优化器 Hint:在统计信息不可靠时强制干预
统计信息更新需要时间,而且对于某些特殊查询(比如临时性的报表 SQL,只跑一次,来不及等 ANALYZE),直接用 Hint 告诉优化器"怎么做"是更直接的方式。
6.1 控制 JOIN 的数据分发方式
-- 强制对小表使用 Broadcast(不做 Hash Redistribute)
SELECT /*+ BROADCAST(d) */
o.order_id,
d.dept_name,
o.amount
FROM orders o
JOIN dept d ON o.dept_id = d.dept_id
WHERE o.order_date = '2024-06-01';
-- d 是 dept 表的别名,Hint 告诉优化器把 dept 广播到所有节点
-- 强制使用 Hash Redistribute(禁止 Broadcast)
SELECT /*+ REDISTRIBUTE(o, d) */
o.order_id, d.dept_name, o.amount
FROM orders o
JOIN dept d ON o.dept_id = d.dept_id;
BROADCAST 适用于明确知道某张表行数很少(几十万行以内),但统计信息滞后导致优化器误判其大小的情况。REDISTRIBUTE 适用于优化器错误地选择了 Broadcast(表实际上不小,但旧统计信息低估了行数)的情况。
6.2 控制 GROUP BY 的执行策略
-- 强制使用两阶段聚合(先局部聚合再重分布,适合高基数 GROUP BY)
SELECT /*+ AGG_MULTI_REDIST */
customer_id,
COUNT(*),
SUM(amount)
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id;
6.3 控制 JOIN 顺序
当多表 JOIN 时,优化器有时会选择不合理的 JOIN 顺序(先 JOIN 两张大表,产生巨大中间结果,再 JOIN 小表),可以用 Hint 指定 JOIN 顺序:
-- 强制先过滤小结果集,再做后续 JOIN
SELECT /*+ JOIN_ORDER(f, d, r) */
f.order_id, d.dept_name, r.region_name
FROM orders f
JOIN dept d ON f.dept_id = d.dept_id
JOIN region r ON d.region_id = r.region_id
WHERE f.order_date = '2024-06-01';
-- 指定 JOIN 顺序:f(orders,已过滤)先与 d(dept)JOIN,再与 r(region)JOIN
七、统计信息管理的生产建议
在实际运维中,统计信息管理往往是被忽略的环节,直到出现莫名其妙的慢查询才被想起来。以下几条原则有助于把统计信息维护纳入日常流程。
与数据加载流程绑定:每次批量数据加载完成后,立即对被加载的表执行 ANALYZE TABLE。这样可以确保统计信息始终反映最新的数据状态,而不是依赖自动触发机制的滞后更新。一条 ANALYZE TABLE orders; 加在 ETL 脚本的末尾,是成本最低的统计信息维护手段。
建表初期主动收集:新表建完并完成首次数据加载后,务必手动执行一次 ANALYZE TABLE。GBase 8a 新建的空表统计信息为零,在首次加载后如果不手动触发,优化器依然认为这张表是空表,会产生严重错误的执行计划。
关注 NDV 差异极大的列:对于分布键、常用 JOIN 列和高频 WHERE 过滤列,定期检查其 cardinality 值是否与实际 COUNT(DISTINCT col) 的结果接近。两者差距超过 20% 时,应该触发 ANALYZE。
直方图只针对问题列:不要对所有列都收集直方图,直方图的收集和存储是有代价的。只对已经观察到统计信息导致计划偏差的列、以及已知分布严重不均的列启用直方图,这样既能解决问题又不会带来不必要的开销。
Hint 是临时手段而非长期方案:使用 Hint 强制干预执行计划,虽然能立即解决问题,但会带来维护负担——SQL 一旦加了 Hint,后续表结构变化或数据分布变化后,这个 Hint 可能不再是最优选择,甚至变成负优化。优先通过更新统计信息和优化表设计来解决执行计划问题,把 Hint 作为"紧急止血"的最后手段而非常规工具。
八、常见统计信息问题速查
| 现象 | 可能根因 | 验证方法 | 解决方案 |
|---|---|---|---|
| 查询突然变慢,EXPLAIN 的 rows 估算偏低 | 大量新数据写入后统计信息未更新 | COUNT(*) 与 table_rows 对比 | ANALYZE TABLE |
| 小表被错误地做了 Hash Redistribute | NDV 统计过时,优化器误判表大小 | SHOW CREATE TABLE 看行数 | ANALYZE 后验证,或加 BROADCAST Hint |
| 带低选择性过滤的查询扫描了过多数据 | 缺少直方图,优化器均匀估算选择性 | 对比实际数据分布 | ANALYZE ... UPDATE HISTOGRAM |
| 多表 JOIN 的中间结果异常膨胀 | JOIN 顺序不合理,先连接了两张大表 | EXPLAIN 看 JOIN 算子顺序 | JOIN_ORDER Hint 或重写 SQL |
| 新表首次加载后查询很慢 | 建表后未收集统计信息 | table_rows = 0 | 加载完成后立即 ANALYZE TABLE |
评论
热门帖子
- 12025-12-01浏览数:183411
- 22023-05-09浏览数:26148
- 42023-09-25浏览数:19815
- 52020-05-11浏览数:18465