MPP数据库分布键怎么选?用SQL定位数据倾斜

大家好,我是数据库小学妹👋我踩过的坑,你别再踩。

上周二凌晨,一张 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 数据库表,分布键选的是业务主键还是关联键?跑过分布检查吗?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

相关推荐
Linux实战笔记2 天前
MySQL 索引失效的三种典型场景与 EXPLAIN 分析
mysql·索引失效·explain·执行计划·sql 调优
ly76892 天前
PostgreSQL 执行计划深读:从 EXPLAIN 输出到代价模型驱动的 SQL 改写
数据库·sql·postgresql·explain·执行计划·代价模型
ly76896 天前
读懂 PostgreSQL 执行计划:从 EXPLAIN 输出定位 Spring Boot 慢 SQL 的代价估算偏差
spring boot·sql·postgresql·执行计划·explain analyze·慢sql优化
骑着蜗牛撵大象3277 天前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 里的建表与增删改查手法
数据库·sql·mysql·编辑器·crud·执行计划
余槐i20 天前
MySQL 深分页从 4 秒到 60 毫秒:延迟关联减少回表的实测与代价
mysql·性能优化·sql优化·执行计划·慢查询优化
隔窗听雨眠1 个月前
SQL分页优化深度实战:从深分页性能瓶颈到等价改写完整方案
sql·sql优化
Gl�ria1 个月前
Hadoop MapReduce / Hive 场景:ReduceTask 热点 Key 排查
运维·hadoop·数据倾斜
PawSQL2 个月前
给 DBeaver 请一位 SQL 体检医生:PawSQL Client
sql优化·dbeaver·pawsql·sql审核
智码看视界2 个月前
Spark 3.5 AQE 调优:10 个生产环境案例让作业提速 3-10 倍
大数据·spark·性能调优·aqe·sparksql·数据倾斜·etl优化