ntile不是分页函数,其“不均匀”是设计行为:按总行数÷n得基础桶大小,余数r行优先分配给第1至r桶,如17行分5桶结果必为4、4、3、3、3。

NTILE 本身不是分页函数,它不会“分页不均匀”——你遇到的其实是把 NTILE 当成分页工具用了,而它根本不是干这个的。
NTILE 分桶结果看起来“不均匀”是设计行为,不是 bug
NTILE(n) 的逻辑是:先算总行数 ÷ n 得到基础桶大小,余数 r 就往第 1 到第 r 个桶里各加 1 行。比如 17 行数据用 NTILE(5),结果必然是 4,4,3,3,3 —— 前两个桶多 1 行,这是确定的、可复现的分配方式。
常见误解:
- 以为“4 个桶”就该严格 25% / 25% / 25% / 25%,但 NTILE 只管行数,不管业务比例
- 看到
bucket = 1返回 26 行、bucket = 2返回 25 行,就认为“分歪了”,其实只是总行数不能被 4 整除(比如 101 行) - 在 JOIN 后直接套
NTILE(4),却没意识到 JOIN 已让行数膨胀,桶是按膨胀后行数分的,不是按原始用户数
为什么你用 NTILE 做“分页式切片”会出问题
NTILE 不支持 ROWS BETWEEN,不能指定“只取前 100 行再分桶”,它永远作用于整个窗口结果集。你想要的“每页 100 行、共 10 页”,和 NTILE 的“把全部 1000 行强行塞进 10 个桶”是两回事。
典型翻车场景:
-
SELECT *, NTILE(10) OVER (ORDER BY id) AS page FROM orders—— 这不是分页,是给全部订单打上 1–10 的标签;删掉几行,所有 page 编号重排 - 想取“第 3 页”,写
WHERE page = 3—— 结果行数不固定(可能是 99 或 101 行),且无法跳过前 200 行做高效偏移 - 在 WHERE 过滤后才用 NTILE,比如
WHERE status = 'paid'放在 NTILE 外层 —— 桶号基于过滤后结果重算,和你想的“全量分页”完全脱钩
真正需要分页时,别硬套 NTILE
如果你目标是稳定、可跳转、行数可控的分页,应该用标准分页手段:
- 物理分页:直接
LIMIT 100 OFFSET 200(MySQL/PostgreSQL)或OFFSET 200 ROWS FETCH NEXT 100 ROWS ONLY(SQL Standard) - 游标分页(推荐):用上一页最后一条的
id或created_at, id作为条件,例如WHERE created_at - 如果非要“按桶预分组再查”,必须用 CTE 固定桶结果:
WITH bucketed AS ( SELECT *, NTILE(10) OVER (ORDER BY id) AS bucket FROM orders WHERE status = 'paid' ) SELECT * FROM bucketed WHERE bucket = 3;
注意:这仍不等于分页,只是抽样切片
什么时候才该用 NTILE?明确它的适用边界
NTILE 的正经用途只有三个:
- 用户数量级分层:比如“找出消费行数排前 25% 的用户 ID”,不是金额前 25%
- 测试分组:A/B 测试需要大致均分用户数,且允许桶间行数差 ≤1
- 快速打标:给销售线索表加
quartile字段用于后续关联分析,不要求精确比例
关键提醒:只要你的需求里出现“第 N 页”“跳转到某页”“每页固定 N 行”,就立刻放弃 NTILE —— 它没有 offset、没有 fetch、不支持增量计算,强行用只会让逻辑越来越难维护。










