大家好,我是数据库小学妹👋我踩过的坑,你别再踩。
上周二凌晨,一张 120 万行的订单表把我堵在工位上。集群有 8 个节点,跑的是 MPP 数据库。7 个节点 CPU 在 20% 上下,1 个节点跑到 92%。一条按城市汇总的 SQL,平时 30 秒,那天跑了 2 分 06 秒。我第一反应是 SQL 写歪了。把执行计划拉出来一看,问题不在 SQL,在分布键。
这篇写给已经在用 MPP、或者正准备上 MPP 的数据开发和 DBA。我们不绕概念,只讲三件事。先用 SQL 把倾斜找出来。再看分布键选得对不对,最后验证改完的效果。
一、先看数据落在哪,别急着改 SQL
MPP 数据库会把一张表切成很多片,按分布键散到不同节点。查询时每个节点算自己那一份,最后汇总。分布键选得好,大家活差不多。选得不好,一个节点干 62%,其余节点看戏。先建一张演示表。不同产品的分布语法不一样。DISTRIBUTED BY 只是示例,执行前换成你所用产品的关键字。
sql
CREATE TABLE fact_order (
order_id bigint,
customer_id bigint,
city_id int,
order_date date,
amount numeric(18,2)
) DISTRIBUTED BY (city_id);
这张表按 city_id 分布。接下来看看城市维度的数据长什么样。
sql
SELECT city_id, count(*) AS cnt
FROM fact_order
GROUP BY city_id
ORDER BY cnt DESC
LIMIT 10;
我造的数据里,结果是这样的:
| city_id | 行数 | 占比 |
|---|---|---|
| 310000 | 744,000 | 62.0% |
| 110000 | 168,000 | 14.0% |
| 440000 | 108,000 | 9.0% |
| 其他 27 个城市 | 180,000 | 15.0% |
310000 这一个城市占了 62%。如果分布键是 city_id,这个城市的所有行都会落到同一个节点。8 个节点里,1 个节点扛 62% 的数据,另外 7 个分剩下的 38%。这就是数据倾斜。它不是数据量的问题,是分布的问题。上面的数据是为了演示造的,120 万行,8 节点。只用来讲排查方法,不代表任何产品的 benchmark。你拿自己的表跑,数字会不一样,判断逻辑是一样的。
二、分布键选错,代价藏在执行计划里
MPP 数据库执行一条聚合 SQL,通常分两步。每个节点先算本地的部分结果。之后按分组键重分布到其他节点,做最终汇总。重分布走网络。网络一堵,最慢节点决定整个 stage 的时长。执行计划我简化成了下面这几层:
text
Gather
-> HashAggregate
-> Redistribute (city_id)
-> Seq Scan on fact_order
Redistribute 那一步,就是把各节点的部分结果按 city_id 重新洗一遍。洗完之后,某个节点如果接到 62% 的数据。后面的 HashAggregate 就只能它一个人扛。其他节点算完自己的份额,早早进入等待。你看到的 CPU 曲线,就是一个节点冒尖,其余节点趴着。

同一份执行计划的示意图。左边是计划层级,右边是节点负载。倾斜就发生在 Redistribute 之后。
分布键不是随便挑一列就行。我把常见的候选列和风险列了一张表:
| 候选列 | 适合做分布键吗 | 风险 |
|---|---|---|
| 订单号、流水号这类高基数主键 | 适合 | 和高频 join 键不一致时,join 仍要重分布 |
| 客户号、商户号这类关联键 | 看情况 | 大客户、大商户会把数据吸到一个节点 |
| 城市、地区、日期 | 谨慎 | 热点城市、热点日期会造成明显倾斜 |
| 状态位、性别、类型码 | 不适合 | 基数低,业务一偏就全挤到一个值 |
| 空值多的列 | 不适合 | 空值通常落到同一个节点 |
我自己的判断顺序是先看基数。再看数据匀不匀。最后看是不是高频 join 键。三条都满足,再考虑复合键。复合键能提高均匀度。但 join 只用其中一列时,数据就不一定还在同一个节点上。
这里补一句 join 的事。MPP 数据库里,两张表按同一个键分布,join 在本地就能完成。数据不用搬。如果分布键和 join 键对不上,优化器就要做重分布。重分布不一定慢,但它会把网络变成瓶颈。所以选分布键时,不能只看单表匀不匀。还要看它参不参与高频 join。
小表 join 大表是另一种情况。优化器可能会把很小的表广播到每个节点,避免大表重分布。广播不是越多越好,小表一大,网络和内存都吃不消。具体走哪条路,看执行计划里有没有 Broadcast 或 Redistribute。
很多人把分布键和分区键混着叫。分布键决定数据去哪个节点,分区键决定数据在节点内怎么切。分布键选错,节点之间不均;分区键选错,单节点内扫描量大。两个都要看,别只改一个。MPP 数据库的分布设计,先把这两层分清楚,再谈调优。
电科金仓 KADB 走的也是 MPP 这条路。它的定位是分析型负载。数据分片、查询并行、结果汇总,和前面讲的一致。这类产品的好处很直接。单机扛不住的大表扫描和聚合,可以拆到多个节点并行做。选型时我会重点看两件事。优化器能不能根据分布键选对 join 方式。扩容时重分布流程顺不顺。
三、三条 SQL,把倾斜定位出来
第一条,看业务维度的分布。上面那条 GROUP BY city_id 就是。它回答的是:哪个值最大,最大占多少。
第二条,算倾斜比和最大占比,这条更适合放进巡检脚本。
sql
WITH dist AS (
SELECT city_id, count(*) AS cnt
FROM fact_order
GROUP BY city_id
)
SELECT
round(max(cnt)::numeric / avg(cnt), 2) AS skew_ratio,
round(max(cnt)::numeric / sum(cnt), 4) AS max_share
FROM dist;
演示数据跑出来是 skew_ratio = 18.6,max_share = 0.62。8 节点集群的理想最大占比是 12.5%。62% 已经是理想值的五倍。看到这种数字,不用再猜了,先查分布键。
第三条,看空值和基数。空值多、基数低的列,做分布键很容易出事。
sql
SELECT
count(*) AS total_rows,
count(*) FILTER (WHERE customer_id IS NULL) AS null_rows,
count(DISTINCT customer_id) AS customer_ndv,
round(count(*)::numeric / count(DISTINCT customer_id), 2) AS rows_per_customer
FROM fact_order;
如果 rows_per_customer 很大,说明少数客户贡献了大量行。这种列做分布键,大客户就是天然热点。
这三条 SQL 是 MPP 数据库的通用排查顺序。跑完,基本能判断问题出在分布键,还是 SQL。
四、如果暂时不能改分布键,先做这三件事
改分布键要搬数据,不是每次都能立刻做。在 MPP 数据库上,生产先止血,可以试这三招。
第一招,把过滤条件往前推。先缩小数据范围,再 join、再聚合。很多慢查询不是分布键一个人的锅,是中间结果太大。
sql
WITH filtered AS (
SELECT *
FROM fact_order
WHERE order_date >= DATE '2026-09-01'
AND order_date < DATE '2026-10-01'
)
SELECT city_id, sum(amount)
FROM filtered
GROUP BY city_id;
第二招,小表广播。维表很小的时候,让优化器把它复制到每个节点。这样能避免大表重分布。具体能不能广播,看执行计划里有没有 Broadcast。小表一大,广播反而会把网络打满。
第三招,先局部聚合,再全局聚合。把能在本地算完的活先算掉,减少重分布的数据量。
sql
WITH partial AS (
SELECT city_id, order_date, sum(amount) AS amount
FROM fact_order
GROUP BY city_id, order_date
)
SELECT city_id, sum(amount)
FROM partial
GROUP BY city_id;
这三招都是止血,不是治本。分布键选错,数据还是会往一个节点挤。止血做完,还是要排期改表。
这套止血思路,在金仓 KADB 上同样成立。过滤条件下推、小表广播、局部聚合,都是 MPP 优化器常见的手段。具体命令和参数,以版本手册为准。
五、改分布键,别在生产高峰动
定位到问题,下一步是换分布键。我用 order_id 做了一版对照表。
sql
CREATE TABLE fact_order_new (
LIKE fact_order INCLUDING ALL
) DISTRIBUTED BY (order_id);
INSERT INTO fact_order_new
SELECT * FROM fact_order;
ANALYZE fact_order_new;
然后跑同一条聚合 SQL,对比前后:
| 指标 | 改前(city_id) | 改后(order_id) |
|---|---|---|
| 总耗时 | 126 秒 | 34 秒 |
| 最慢节点耗时 | 118 秒 | 31 秒 |
| 最大节点占比 | 62.0% | 13.1% |
| 落盘次数 | 3 次 | 0 次 |
改完之后,order_id 的基数高,hash 分布更均匀。最慢节点从 118 秒降到 31 秒,总耗时跟着下来。在 MPP 数据库里,改分布键的代价,是把数据重新搬一遍。120 万行还好。上亿行就要算窗口、算磁盘、算同步链路的影响。
我的做法是低峰期用 CTAS 建新表。灌完数据,对一遍行数和金额,再切视图或改表名。切之前把依赖查清楚。视图、物化视图、索引、外键、同步任务,一个都别漏。漏一个,第二天报表就可能空给你看。
迁移顺序我一般这么走。先选低峰窗口,确认同步链路能停多久。再建新表,显式指定分布键。灌完数据对账行数和金额,跑三条典型 SQL 对比最慢节点。查完依赖,切表或切视图。最后观察一周,保留回滚表。步骤不复杂,难的是每一步都别省。
金仓 KADB 这类国产 MPP,还有一个现实优势是本地化支持。改分布键、扩容、故障处理,遇到问题能找到人一起看。技术上能不能做是一回事,出问题时有没有人兜底,是另一回事。具体服务范围以厂商承诺为准。
六、把检查固化成巡检
一次排查靠人,长期稳定靠巡检。我现在会把下面这条 SQL 放进周巡检,跑最近 7 天的数据。这套方法对大多数 MPP 数据库都适用。金仓 KADB 也一样,系统视图名换一下就行。
sql
WITH dist AS (
SELECT city_id, count(*) AS cnt
FROM fact_order
WHERE order_date >= CURRENT_DATE - 7
GROUP BY city_id
)
SELECT
count(*) AS city_cnt,
round(max(cnt)::numeric / avg(cnt), 2) AS skew_ratio,
round(max(cnt)::numeric / sum(cnt), 4) AS max_share
FROM dist;
阈值我一般这么定。skew_ratio 小于 1.5、max_share 小于 15%,算正常。1.5 到 3 之间,或者 max_share 到 25%,先观察。超过 3,或者 max_share 超过 25%,就排期处理。不过这不是死标准。节点数越多,理想占比越低,阈值要跟着调。8 节点集群的理想 max_share 是 12.5%,16 节点是 6.25%。拿理想值做参照,比拍脑袋定阈值靠谱。
七、不是所有慢,都是分布键
MPP 数据库慢,不一定都是分布键。这句话我得单独说。分布键只解决数据落得匀不匀。SQL 写得对不对,它管不了。
一条点查、高并发小查询,MPP 的调度成本可能比计算还高。这种场景,分布键怎么调都救不了。应该考虑别的路线。SQL 本身少了过滤条件,或者 join 顺序不对,先改 SQL。集群网络带宽不够,加节点只会加更多跨节点流量。
我后来在另一套国产 MPP 的测试环境里对照过这套排查路径。执行计划里同样能看到重分布这一层。具体语法和系统视图以版本手册为准,排查思路是通用的。
八、避坑清单
先说一个最容易被忽略的:别拿低基数列做分布键。状态位、性别、地区码,看着均匀,业务一偏就倾斜。我们有个表用 status 做分布键。上线三个月后,80% 的行挤在一个值上,查一次卡一次。
还有个习惯要改,别只看平均。平均 30% 可能掩盖一个 95% 的节点。看最慢节点,看 max_share,看每个 stage 的耗时分布,比看总耗时有用。
我最想提醒的是改分布键前先查依赖。视图、物化视图、索引、同步链路,一个都别漏。我漏过一个物化视图,重分布后报表空了一天,被业务追着问。后来我把依赖检查写进了变更单。这条坑才算堵上。
九、写在最后
这篇写到这儿,方法其实就三步。先用 SQL 看数据落在哪。再看执行计划里有没有重分布。最后改分布键,做一次前后对比。三步都不复杂,难的是每次都按顺序走。
我现在上新表,会先跑一遍分布检查。不等到凌晨报警,再回头翻执行计划。MPP 数据库的很多慢,不是机器不够,是数据没分匀。先看分布,再谈扩容,这个顺序别反过来。
产品会换,视图名会变,判断顺序不会变。先看数据分布,再看执行计划,最后才轮到加机器。
如果你正在做 MPP 选型,金仓 KADB 可以放进 POC 清单。它的定位是分析型 MPP,优势在大表扫描、复杂 join 和聚合的并行处理。国产化环境里的适配和本地服务,也是它会被考虑的原因。国产 MPP 不止一家,多跑几家再决定。别只看厂商的演示数据。拿你的分布键、你的大表、你的慢 SQL 去跑一遍。选型这事,跑过才有答案。
你现在的 MPP 数据库表,分布键选的是业务主键还是关联键?跑过分布检查吗?评论区聊聊。
我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋