partition by字段区分度低会直接触发溢出,因数据库需为每个超大分区在内存中维护完整状态,导致内存不足而溢出至磁盘;典型信号是执行计划中出现sort method: external merge disk或created_tmp_disk_tables飙升。

为什么PARTITION BY字段区分度低会直接触发溢出
窗口函数在执行时,会在内存中为每个分区单独维护状态(比如累计和、行号计数器)。如果 PARTITION BY 字段只有几个取值(如 status 只有 'pending'/'shipped'/'cancelled'),整个千万级表就被压成 3 个超大分区,数据库必须把每个分区的全部数据载入内存排序或计算——这跟全表开窗没本质区别,只是换了个溢出路径。
PostgreSQL 的 EXPLAIN (ANALYZE) 里若看到 WindowAgg 节点下挂的是 Seq Scan 或 Index Scan 但未命中索引,且 Sort Method: external merge Disk 出现,就是分区过大+无索引导致的典型溢出信号;MySQL 则表现为 Sort_merge_passes 持续飙升,SHOW STATUS LIKE 'Created_tmp_disk_tables' 也同步上涨。
如何快速识别危险的PARTITION BY组合
别靠猜,用一句 SQL 直接暴露分区倾斜:
SELECT COUNT(*) AS cnt, status FROM orders GROUP BY status ORDER BY cnt DESC;
如果最大分区行数 > 100 万,或最大/最小分区比值 > 1000,就该警惕。更进一步,检查是否同时用了低基数列 + 高基数列组合:
-
PARTITION BY region, status:region 有 50 个值,status 有 3 个 → 最多 150 个分区,看似合理,但若 90% 订单集中在华东+shipped,那一个分区仍可能吞掉 800 万行 -
PARTITION BY YEAR(created_at), user_id:user_id 是高基数,但 YEAR(created_at) 只有 1–3 个值,实际分区数仍由 user_id 主导,相对安全
真正有效的分区瘦身策略
核心思路不是“减少分区数量”,而是“让每个分区变小”:
- 加 WHERE 过滤再套窗口:把
PARTITION BY user_id改成SELECT * FROM (SELECT user_id, amount, created_at FROM orders WHERE created_at >= '2026-01-01') t WINDOW (...) OVER (PARTITION BY user_id ...),先砍掉历史冷数据 - 用时间粒度替代原始字段:不要
PARTITION BY user_id,改用PARTITION BY user_id, DATE_TRUNC('month', created_at)(PostgreSQL)或PARTITION BY user_id, YEAR(created_at), MONTH(created_at)(MySQL),把大分区按月拆散 - 业务可接受时,主动降维:比如用户生命周期分析不需要精确到每笔订单,可先聚合到
user_id, day粒度,再对日汇总结果开窗 - 避免单独用低基数列:
PARTITION BY status是高危写法;必须用时,至少补一个高基数列,如PARTITION BY status, user_id % 10(取模分桶),人为打散单一分区压力
MySQL 8.0 和 PostgreSQL 的关键差异点
同一句 PARTITION BY 在不同引擎下表现可能天差地别:
- PostgreSQL 对
work_mem敏感:一个分区撑爆work_mem就会 spill 到磁盘,SET LOCAL work_mem = '512MB'可临时缓解,但并发高时反而引发全局 OOM - MySQL 8.0 不支持
ROWS BETWEEN显式限定窗口范围,所以SUM(amount) OVER (PARTITION BY user_id ORDER BY created_at)默认是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,遇到重复时间戳会隐式扩大窗口——必须补唯一键,如ORDER BY created_at, order_id - 两者都不支持在
PARTITION BY中用表达式下推过滤(如PARTITION BY CASE WHEN status='shipped' THEN user_id END),这种写法会让优化器彻底放弃分区裁剪,等同于全表扫描
最易被忽略的一点:PARTITION BY 字段即使建了索引,也不会自动用于窗口函数的分区定位——它只加速子查询里的 WHERE 或 JOIN,窗口本身的分区逻辑仍是哈希或排序驱动,索引只在你主动把它放进子查询过滤条件时才起作用。











