NTILE无法直接定位中间行,因其按行数均分组而非按数值或位置居中;它只返回桶编号,不保证某桶对应中位区间,真正取中间记录应使用ROW_NUMBER()配合CEIL(COUNT() OVER() 1.0 / 2)。

NTILE 本身不能直接取“中间记录”,但配合 CEIL 可以逼近中位数位置,前提是你要清楚自己要的是“分组内排序后居中的那一条”,还是“分组内数量为奇数时的正中间、偶数时的上中位或下中位”。
为什么 NTILE(n) 无法直接定位中间行
NTILE(3) 把数据分成三组,NTILE(5) 分五组——它只负责均匀打散,不保证某组对应“中位区间”。比如 10 行数据用 NTILE(4),会得到组号分布为 1,1,1,2,2,3,3,4,4,4,根本没有一个组稳定落在“中间”。真正能锚定中间位置的是行号比例,不是桶编号。
常见错误现象:WHERE NTILE(3) OVER (PARTITION BY x ORDER BY y) = 2 看似取“第二组”,实则可能包含 3 行或 4 行,且不一定含中位值;当分组行数少于桶数(如 2 行分 4 桶),NTILE(4) 会返回 1,2,此时 =2 只是末尾行,完全偏离中间语义。
- NTILE 是离散分桶,目标是“等量”,不是“等距”或“居中”
- 它不返回行在排序中的绝对位置,只返回相对桶序号
- 桶大小受总行数与桶数整除关系影响,不可控
用 CEIL + ROW_NUMBER 模拟中位位置更可靠
想取每个分组的中间记录(例如:按销售额排序后,每部门取第 ⌈N/2⌉ 行),应放弃 NTILE,改用 ROW_NUMBER() 配合 CEIL(COUNT(*) * 1.0 / 2)。
示例:取每个 department 中按 salary 升序排列的“上中位”行(即第 ⌈N/2⌉ 行):
SELECT department, name, salary
FROM (
SELECT department, name, salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary) AS rn,
CEIL(COUNT(*) OVER (PARTITION BY department) * 1.0 / 2) AS mid_pos
FROM employees
) t
WHERE rn = mid_pos;
-
COUNT(*) OVER (PARTITION BY ...)必须用窗口 COUNT,不能用子查询或 GROUP BY —— 否则丢失明细行 -
* 1.0是为避免整数除法截断(尤其在 SQL Server 或 older MySQL 中) - 若需“下中位”(偶数时取 N/2),把
CEIL换成FLOOR;若需两个都取,用rn IN (FLOOR(...), CEIL(...))
NTILE 和 CEIL 组合仅适用于“近似中位分箱”场景
只有当你不关心单条记录,而需要把数据粗略划分为“低/中/高”三档,并让每档记录数尽量接近(比如做客户分层看板),才适合用 NTILE(3)。此时可结合 CEIL 动态算桶数,但注意这不是取中间记录,而是控制分箱粒度。
例如:希望每组最多 1000 条,则桶数 = CEIL(COUNT(*) * 1.0 / 1000):
SELECT *,
NTILE(CEIL(COUNT(*) OVER () * 1.0 / 1000))
OVER (ORDER BY created_at) AS bucket
FROM logs;
- 这种写法在 PostgreSQL、SQL Server 支持,在 MySQL 8.0+ 中也支持,但 SQLite 不支持窗口函数嵌套表达式
-
CEIL的输入必须是标量,所以COUNT(*) OVER ()是安全的;但CEIL(COUNT(*) OVER (PARTITION BY x))在部分引擎(如旧版 SQL Server)可能报错,建议先用 CTE 提前算好 - 别指望靠
NTILE(CEIL(...)) = 2找中位——这只是碰巧在三桶时“第二桶”看起来居中,逻辑上不成立
真正要取中间记录,盯住 ROW_NUMBER 和 COUNT 窗口函数,NTILE 是分箱工具,不是定位工具。最容易被忽略的一点:很多人在写 CEIL(COUNT(*) OVER (...) / 2) 时忘了加 * 1.0,导致整除为 0 或向下取整,结果永远取不到最后一行。










