分桶是将有序数据按行数或值范围切分为固定数量组并标记每行所属桶,而非聚合;group by会破坏行序和位置信息,无法实现分桶所需的行级标记,故必须用ntile()或row_number()等窗口函数。

什么是分桶,以及为什么不用 GROUP BY
分桶(bucketing)本质是把有序数据按行数或值范围切分成固定数量的组,每组内部保持原始顺序。比如“把销售记录按时间排序后均分为4个时间段桶”,或“按销售额从低到高分成5档”。GROUP BY 会打乱顺序、聚合统计,无法保留行级位置信息——这正是窗口函数的用武之地。
核心思路:用 ROW_NUMBER() 或 NTILE() 生成序号,再通过整除或映射得到桶编号。关键不是“分组”,而是“标记每行所属桶”。
NTILE(n) 是最直接的分桶函数,但要注意它的行为边界
NTILE(4) 把结果集按当前 ORDER BY 排序后,尽量均分为 4 桶,编号为 1~4。但它不保证每桶行数绝对相等——当总行数不能被 4 整除时,前几桶会多 1 行。
- 例如 10 行数据用
NTILE(4):桶大小为 [3, 3, 2, 2],不是 [2, 2, 3, 3] - 它只接受常量整数,不能写成
NTILE(@k)(多数数据库不支持变量) - 必须配合
OVER (ORDER BY ...),否则报错:window function ntile requires an OVER clause - 如果
ORDER BY字段有重复值,NTILE仍会强制拆分(可能把相同值拆到不同桶),需提前用ROW_NUMBER()打散
示例:
SELECT id, amount,
NTILE(4) OVER (ORDER BY amount) AS bucket
FROM sales;
用 ROW_NUMBER() + 算术运算实现可控分桶
当需要严格控制桶大小(如每桶 100 行)、或桶数动态计算(如按总数的 10% 分桶)时,NTILE 不够灵活。ROW_NUMBER() 提供精确行序,再配合整除可稳定分桶。
- 每桶固定 N 行:
(ROW_NUMBER() OVER (ORDER BY ts) - 1) / 100 + 1→ 桶号从 1 开始 - 按总数百分比分桶(如 20% 一桶):
CEIL(100.0 * ROW_NUMBER() OVER (ORDER BY ts) / COUNT(*) OVER()) - 注意:整除在不同数据库写法不同 —— PostgreSQL/Oracle 用
/(整数除),MySQL 8+ 和 SQL Server 需显式FLOOR()或CAST(... AS SIGNED) - 减 1 再除是为了让第 1–100 行落在桶 1,而不是第 0–99 行(避免桶号从 0 起)
示例(MySQL 8+):
SELECT id, amount,
FLOOR((ROW_NUMBER() OVER (ORDER BY created_at) - 1) / 50) + 1 AS bucket_50
FROM orders;
分桶后做聚合?别忘了窗口和聚合的嵌套顺序
如果目标是“每个桶内求平均销售额”,不能直接在 SELECT 里写 AVG(amount) OVER (PARTITION BY bucket),因为 bucket 是计算字段,多数数据库不允许在同一层 SELECT 中引用别名。
- 方案一:子查询或 CTE 先算出
bucket,外层再聚合 - 方案二:用
AVG(amount) OVER (PARTITION BY NTILE(4) OVER (ORDER BY amount))—— 但嵌套窗口函数可读性差,且 PostgreSQL 不支持,SQL Server 要求列名唯一 - 更安全的做法是明确写出分桶逻辑两次,或用 CTE 提升可维护性
CTE 示例(推荐):
WITH bucketed AS ( SELECT *, NTILE(5) OVER (ORDER BY score) AS bucket_id FROM students ) SELECT bucket_id, AVG(score) AS avg_score FROM bucketed GROUP BY bucket_id;
分桶看似简单,真正容易出问题的是排序稳定性、空值处理(NULLS FIRST/LAST 影响桶分布)、以及跨数据库整除行为差异——尤其是 MySQL 和 PostgreSQL 对 INT/INT 除法返回浮点还是整数的默认策略不同。动手前先查清你用的数据库版本对 NTILE 和整除的定义。











