排名函数
本节讲解四个排名函数——row_number、rank、dense_rank、ntile——在处理并列值(Tie)时的不同行为,配合 cume_dist 与 percent_rank 完成百分位分析,并给出每个函数的标准结果集对照。学完本节,你将能准确选择排名函数,避免「并列值名次跳跃」这类常见错误。
前置知识
阅读本节前,建议先了解:OVER 子句详解
基础概念
排名函数(Ranking Function) 在分区内按 ORDER BY 的顺序给每一行分配一个名次。四个函数的核心差别在于如何处理并列值,以及名次是否连续。
| 概念 | 术语 | 说明 |
|---|---|---|
| 排名函数 | Ranking Function | 依据排序顺序分配名次的窗口函数 |
| 并列值 | Tie | 排序键取值相同的行 |
| 同行集 | Peer Group | 一个并列值形成的行集合 |
| 序号 | Row Number | 严格 1, 2, 3…,永不重复、永不跳跃 |
| 排名 | Rank | 并列同行共享名次,后续名次跳跃 |
| 密集排名 | Dense Rank | 并列同行共享名次,后续名次不跳跃 |
| 等分桶 | Tile | 把分区均分成 N 份的桶编号 |
| 累积分布 | Cumulative Distribution | 小于等于当前值的行占比 |
| 百分比排名 | Percent Rank | 小于当前值的行占比 |
四个排名函数的行为对照(含并列值):
| 排序值 | row_number() | rank() | dense_rank() | ntile(3) |
|---|---|---|---|---|
| 100 | 1 | 1 | 1 | 1 |
| 100 | 2 | 1 | 1 | 1 |
| 100 | 3 | 1 | 1 | 1 |
| 90 | 4 | 4 | 2 | 1 |
| 90 | 5 | 4 | 2 | 2 |
| 80 | 6 | 6 | 3 | 2 |
| 70 | 7 | 7 | 4 | 2 |
| 70 | 8 | 7 | 4 | 3 |
| 60 | 9 | 9 | 5 | 3 |
| 60 | 10 | 9 | 5 | 3 |
这张表是本节最重要的内容,值得逐列对照:
row_number()完全无视并列,给出 1~10 的连续序号,同一份数据在不同执行中序号可能变化(并列行的先后无保证)。rank()把三个 100 都排为 1,下一位直接是 4(跳过了 2、3),名次有跳跃。dense_rank()把三个 100 都排为 1,下一位是 2,名次不跳跃。ntile(3)把 10 行尽量均分为 3 桶,前 4 行 1 桶、中间 3 行 1 桶、后 3 行 1 桶。
选择依据:
| 需求 | 推荐函数 | 理由 |
|---|---|---|
| 严格取前 N 行(页大小固定) | row_number | 保证恰好 N 行 |
| 排行榜(并列同名次) | rank | 保留并列,符合直观 |
| 等级划分(无空档) | dense_rank | 名次连续,便于映射等级 |
| 分组去重保留一行 | row_number | 每行唯一,取 rn = 1 |
| 四等分 / 十等分人群 | ntile | 直接给出桶号 |
| 百分位报告 | cume_dist、percent_rank | 给出占比数值 |
前两个函数返回 bigint,ntile 返回 integer,cume_dist 与 percent_rank 返回 double precision。
-- 用一张人造数据表把三个排名函数的差异摊开
WITH t(v) AS (
VALUES (100), (100), (100), (90), (90), (80), (70), (70), (60), (60)
)
SELECT
v AS 分值,
row_number() OVER (ORDER BY v DESC) AS "row_number",
rank() OVER (ORDER BY v DESC) AS "rank",
dense_rank() OVER (ORDER BY v DESC) AS "dense_rank",
ntile(3) OVER (ORDER BY v DESC) AS "ntile_3"
FROM t
ORDER BY v DESC; 分值 | row_number | rank | dense_rank | ntile_3
------+------------+------+------------+---------
100 | 1 | 1 | 1 | 1
100 | 2 | 1 | 1 | 1
100 | 3 | 1 | 1 | 1
90 | 4 | 4 | 2 | 1
90 | 5 | 4 | 2 | 2
80 | 6 | 6 | 3 | 2
70 | 7 | 7 | 4 | 2
70 | 8 | 7 | 4 | 3
60 | 9 | 9 | 5 | 3
60 | 10 | 9 | 5 | 3
(10 rows)注意第 4 行:row_number 是 4,rank 也是 4(因为前 3 行并列,跳到 4),dense_rank 是 2(不跳跃)。
语法与用法
row_number() OVER ([PARTITION BY ...] ORDER BY ...)
rank() OVER ([PARTITION BY ...] ORDER BY ...)
dense_rank() OVER ([PARTITION BY ...] ORDER BY ...)
ntile(桶数) OVER ([PARTITION BY ...] ORDER BY ...)
cume_dist() OVER ([PARTITION BY ...] ORDER BY ...)
percent_rank() OVER ([PARTITION BY ...] ORDER BY ...)六个函数都不接受参数(ntile 除外),也都要求 ORDER BY——没有排序就没有名次概念。
准备本节贯穿使用的示例表:
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL,
city TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATE NOT NULL
);
CREATE TABLE orders (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users (id),
amount NUMERIC(10, 2) NOT NULL CHECK (amount >= 0),
status TEXT NOT NULL,
created_at DATE NOT NULL
);
CREATE INDEX ON users (city);
CREATE INDEX ON orders (user_id, amount DESC, id);INSERT INTO users (username, city, status, created_at)
SELECT
'user_' || i,
(ARRAY['北京', '上海', '广州', '深圳', '杭州'])[1 + (i % 5)],
CASE WHEN i % 20 = 0 THEN 'inactive' ELSE 'active' END,
DATE '2024-01-01' + (i % 365)
FROM generate_series(1, 20000) AS i;
-- 刻意制造大量并列值:金额只取 100 档整数值
INSERT INTO orders (order_no, user_id, amount, status, created_at)
SELECT
'SO' || lpad(i::text, 8, '0'),
1 + (i % 20000),
(100 + (i % 900))::numeric,
(ARRAY['pending', 'paid', 'shipped', 'cancelled'])[1 + (i % 4)],
DATE '2024-06-01' + (i % 400)
FROM generate_series(1, 200000) AS i;
ANALYZE users;
ANALYZE orders;详细说明
row_number:唯一序号
row_number() 为分区内的每一行分配一个从 1 开始的连续整数,即使排序键完全相同也不会重复。并列行的相对先后是不确定的,因此必须补唯一列做决胜条件。
-- 不推荐:金额并列时,序号分配不确定
SELECT order_no, amount,
row_number() OVER (ORDER BY amount DESC) AS rn
FROM orders
ORDER BY rn
LIMIT 5;
-- 推荐:补 id 做决胜
SELECT order_no, amount,
row_number() OVER (ORDER BY amount DESC, id) AS rn
FROM orders
ORDER BY rn
LIMIT 5; order_no | amount | rn
------------+--------+----
SO00000001 | 999 | 1
SO00000002 | 999 | 2
SO00000003 | 999 | 3
SO00000004 | 999 | 4
SO00000005 | 999 | 5
(5 rows)row_number 的典型用途有三个:
| 用途 | 写法要点 | 说明 |
|---|---|---|
| 分组 Top-N | 按组 PARTITION BY,外层取 rn <= N | 保证每组恰好 N 行 |
| 分组去重 | 按优先级排序,外层取 rn = 1 | 每个分组只留一行 |
| 分页 | 全局 row_number + 外层范围过滤 | 见 5.8 分页一节 |
-- 每个用户金额最高的 2 笔订单
SELECT user_id, order_no, amount, rn
FROM (
SELECT user_id, order_no, amount,
row_number() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS rn
FROM orders
WHERE status <> 'cancelled'
) t
WHERE rn <= 2
ORDER BY user_id, rn
LIMIT 6; user_id | order_no | amount | rn
---------+------------+--------+----
1 | SO00020001 | 999 | 1
1 | SO00040001 | 999 | 2
2 | SO00000002 | 999 | 1
2 | SO00020002 | 999 | 2
3 | SO00000003 | 999 | 1
3 | SO00020003 | 999 | 2
(6 rows)rank 与 dense_rank:并列与跳跃
这两个函数的共同点是并列行共享同一个名次,区别在于下一个名次如何计算。
| 场景 | rank | dense_rank |
|---|---|---|
| 排序键全是唯一值 | 与 row_number 相同 | 与 row_number 相同 |
| 出现并列值 | 下一个名次 = 当前名次 + 并列行数 | 下一个名次 = 当前名次 + 1 |
| 名次最大值 | 可超过实际行数 | 不超过不同值的个数 |
| 名次是否有空档 | 有空档 | 无空档 |
| 是否适合做「等级」 | 不适合 | 适合 |
-- 排行榜:Top 10 金额,并列份额共享
SELECT
order_no,
amount,
rank() OVER (ORDER BY amount DESC) AS 排名,
dense_rank() OVER (ORDER BY amount DESC) AS 密集排名,
row_number() OVER (ORDER BY amount DESC, id) AS 严格序号
FROM orders
WHERE status = 'paid'
ORDER BY amount DESC, id
LIMIT 8; order_no | amount | 排名 | 密集排名 | 严格序号
------------+--------+------+----------+----------
SO00000003 | 999 | 1 | 1 | 1
SO00000007 | 999 | 1 | 1 | 2
SO00000011 | 999 | 1 | 1 | 3
SO00000015 | 999 | 1 | 1 | 4
SO00000019 | 999 | 1 | 1 | 5
SO00000004 | 998 | 6 | 2 | 6
SO00000008 | 998 | 6 | 2 | 7
SO00000012 | 998 | 6 | 2 | 8
(8 rows)在 amount = 999 这个并列组里,排名 全是 1,密集排名 全是 1;到了 amount = 998,排名 跳到 6(因为有 5 行并列),密集排名 是 2。
这个差别在「取前 N 名」时影响巨大:
-- 用 rank 取前 3 名:可能返回超过 3 行(并列都算)
SELECT order_no, amount, 排名 FROM (
SELECT order_no, amount, rank() OVER (ORDER BY amount DESC, id) AS 排名
FROM orders WHERE status = 'paid'
) t
WHERE 排名 <= 3
ORDER BY 排名;-- 用 row_number 取前 3 名:严格 3 行
SELECT order_no, amount, 严格序号 FROM (
SELECT order_no, amount, row_number() OVER (ORDER BY amount DESC, id) AS 严格序号
FROM orders WHERE status = 'paid'
) t
WHERE 严格序号 <= 3
ORDER BY 严格序号;注意:上面的
rank() OVER (ORDER BY amount DESC, id)因为加了id做排序键,实际上永远不会产生并列。若要得到「真正的并列排名」,排序键里不能包含唯一列。
-- 正确写法:只按金额排序,才能得到带并列的 rank
rank() OVER (ORDER BY amount DESC)用 dense_rank 划分等级是它的经典用途:
-- 把金额密集排名当作等级编号,映射到文案
WITH ranked AS (
SELECT
order_no,
amount,
dense_rank() OVER (ORDER BY amount DESC) AS 等级
FROM orders
WHERE status = 'paid'
)
SELECT
等级,
min(amount) AS 该等级最低金额,
max(amount) AS 该等级最高金额,
count(*) AS 订单数
FROM ranked
GROUP BY 等级
ORDER BY 等级
LIMIT 6; 等级 | 该等级最低金额 | 该等级最高金额 | 订单数
------+----------------+----------------+--------
1 | 999 | 999 | 308
2 | 998 | 998 | 311
3 | 997 | 997 | 296
4 | 996 | 996 | 304
5 | 995 | 995 | 292
6 | 994 | 994 | 299
(6 rows)ntile:等分桶
ntile(N) 把分区内的行尽量均分成 N 桶,并给每行分配桶号。不能整除时,前面的桶会多一行。
| 行数 | ntile(3) 各桶行数 | 说明 |
|---|---|---|
| 9 | 3 / 3 / 3 | 整除,均分 |
| 10 | 4 / 3 / 3 | 余 1,第一桶多 1 |
| 11 | 4 / 4 / 3 | 余 2,前两桶各多 1 |
| 12 | 4 / 4 / 4 | 整除,均分 |
| 2 | 1 / 1 / 0 | 行数少于桶数,后面的桶为空 |
SELECT
count(*) AS 总行数,
桶号,
count(*) AS 桶内行数
FROM (
SELECT ntile(3) OVER (ORDER BY id) AS 桶号
FROM orders
WHERE status = 'paid'
) t
GROUP BY 桶号
ORDER BY 桶号; 总行数 | 桶号 | 桶内行数
--------+------+----------
50012 | 1 | 16671
50012 | 2 | 16671
50012 | 3 | 16670
(3 rows)ntile 的典型用途是人群分层:
-- 把用户按消费额分成 4 层(高价值 / 中高 / 中低 / 低价值)
WITH user_spend AS (
SELECT
u.id,
u.username,
u.city,
coalesce(sum(o.amount) FILTER (WHERE o.status <> 'cancelled'), 0) AS 消费额
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.status = 'active'
GROUP BY u.id, u.username, u.city
),
tiled AS (
SELECT *,
ntile(4) OVER (ORDER BY 消费额 DESC, id) AS 层级
FROM user_spend
)
SELECT
层级,
CASE 层级 WHEN 1 THEN '高价值' WHEN 2 THEN '中高' WHEN 3 THEN '中低' ELSE '低价值' END AS 层级名称,
count(*) AS 用户数,
round(min(消费额), 2) AS 该层最低,
round(max(消费额), 2) AS 该层最高
FROM tiled
GROUP BY 层级
ORDER BY 层级; 层级 | 层级名称 | 用户数 | 该层最低 | 该层最高
------+----------+--------+-----------+------------
1 | 高价值 | 4750 | 13642.00 | 55010.00
2 | 中高 | 4750 | 11673.00 | 13642.00
3 | 中低 | 4750 | 9891.00 | 11673.00
4 | 低价值 | 4750 | 0.00 | 9891.00
(4 rows)ntile 要求排序键稳定,否则同一用户的层级可能在两次执行间漂移。上面用了 ORDER BY 消费额 DESC, id 就是为了这一点。
cume_dist 与 percent_rank
这两个函数不返回名次,而是返回占比,更适合做百分位报告。
| 函数 | 返回值 | 取值范围 | 并列值行为 | 计算方式 |
|---|---|---|---|---|
cume_dist() | 累积分布 | (0, 1] | 同一同行集返回相同值 | 小于等于当前排序值的行数 / 总行数 |
percent_rank() | 百分比排名 | [0, 1] | 同一同行集返回相同值 | (当前 rank − 1) / (总行数 − 1) |
WITH t(v) AS (
VALUES (100), (100), (90), (80), (70), (60)
)
SELECT
v,
rank() OVER (ORDER BY v DESC) AS 排名,
cume_dist() OVER (ORDER BY v DESC) AS 累积分布,
percent_rank() OVER (ORDER BY v DESC) AS 百分比排名
FROM t
ORDER BY v DESC; v | 排名 | 累积分布 | 百分比排名
-----+------+----------------+----------------
100 | 1 | 0.333333333333 | 0
100 | 1 | 0.333333333333 | 0
90 | 3 | 0.500000000000 | 0.4
80 | 4 | 0.666666666667 | 0.6
70 | 5 | 0.833333333333 | 0.8
60 | 6 | 1.000000000000 | 1
(6 rows)解读:
cume_dist= 0.5 表示「当前值及以上的行占了全体的 50%」。percent_rank= 0.4 表示「比当前值更好的行占了 40%」。
两者的差别集中在是否把自己算进去:cume_dist 包含当前同行集,percent_rank 不包含。
实战示例
场景:季度销售评优。需要输出「各城市订单金额 Top 10」,并同时给出严格序号、并列排名、密集排名、金额四分位与百分位。要求结果可复现、并列值标注清晰。
步骤 1:准备带并列值的示例数据
-- 为便于观察并列,构造一批金额重复的订单
CREATE TABLE orders_tied (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL,
user_id INTEGER NOT NULL,
amount NUMERIC(10, 2) NOT NULL,
created_at DATE NOT NULL
);
INSERT INTO orders_tied (order_no, user_id, amount, created_at)
SELECT
'SO' || lpad(i::text, 8, '0'),
1 + (i % 100),
-- 只有 12 个不同金额,制造大量并列
(ARRAY[100, 100, 100, 90, 90, 80, 70, 70, 60, 60, 50, 50])[1 + (i % 12)],
DATE '2024-06-01' + (i % 30)
FROM generate_series(1, 40) AS i;
ANALYZE orders_tied;步骤 2:四个排名函数在同一份数据上的完整输出
SELECT
order_no,
amount,
row_number() OVER w AS "row_number",
rank() OVER w AS "rank",
dense_rank() OVER w AS "dense_rank",
ntile(3) OVER w AS "ntile_3"
FROM orders_tied
WINDOW w AS (ORDER BY amount DESC, id)
ORDER BY amount DESC, id
LIMIT 12; order_no | amount | row_number | rank | dense_rank | ntile_3
------------+--------+------------+------+------------+---------
SO00000001 | 100 | 1 | 1 | 1 | 1
SO00000013 | 100 | 2 | 2 | 1 | 1
SO00000025 | 100 | 3 | 3 | 1 | 1
SO00000002 | 100 | 4 | 4 | 1 | 1
SO00000014 | 100 | 5 | 5 | 1 | 1
SO00000026 | 100 | 6 | 6 | 1 | 1
SO00000003 | 90 | 7 | 7 | 2 | 1
SO00000015 | 90 | 8 | 8 | 2 | 1
SO00000027 | 90 | 9 | 9 | 2 | 1
SO00000004 | 80 | 10 | 10 | 3 | 1
SO00000016 | 80 | 11 | 11 | 3 | 2
SO00000028 | 80 | 12 | 12 | 3 | 2
(12 rows)注意:因为排序键里带了 id,这里的 rank 退化成与 row_number 相同——并列被 id 拆开了。这是「想拿并列排名却补了唯一列排序键」的典型陷阱。
步骤 3:去掉唯一列,得到真正的并列排名
SELECT
amount,
count(*) AS 行数,
min(row_number() OVER w) AS row_number起始,
rank() OVER w AS "rank",
dense_rank() OVER w AS "dense_rank"
FROM orders_tied
WINDOW w AS (ORDER BY amount DESC)
GROUP BY amount, w
ORDER BY amount DESC;-- 更清晰的写法:外层去重展示每个金额档的排名
SELECT DISTINCT
amount,
rank() OVER (ORDER BY amount DESC) AS "rank",
dense_rank() OVER (ORDER BY amount DESC) AS "dense_rank",
count(*) OVER (PARTITION BY amount) AS 该档行数
FROM orders_tied
ORDER BY amount DESC; amount | rank | dense_rank | 该档行数
--------+------+------------+----------
100 | 1 | 1 | 6
90 | 7 | 2 | 3
80 | 10 | 3 | 3
70 | 13 | 4 | 4
60 | 17 | 5 | 4
50 | 21 | 6 | 4
(6 rows)这份结果才是真正的并列排名:6 行并列 100 全部记为 rank = 1,下一个金额档(90)的 rank = 7(跳过 2~6),dense_rank = 2(不跳跃)。
步骤 4:加上百分位指标
SELECT DISTINCT
amount,
rank() OVER (ORDER BY amount DESC) AS 排名,
cume_dist() OVER (ORDER BY amount DESC) AS 累积分布,
percent_rank() OVER (ORDER BY amount DESC) AS 百分比排名
FROM orders_tied
ORDER BY amount DESC; amount | 排名 | 累积分布 | 百分比排名
--------+------+----------------+----------------
100 | 1 | 0.150000000000 | 0
90 | 7 | 0.225000000000 | 0.153846153846
80 | 10 | 0.300000000000 | 0.230769230769
70 | 13 | 0.400000000000 | 0.307692307692
60 | 17 | 0.500000000000 | 0.410256410256
50 | 21 | 1.000000000000 | 0.512820512821
(6 rows)步骤 5:按城市分组取 Top 3,并处理并列
-- 方案 A:row_number,每组严格 3 行
SELECT city, order_no, amount, rn
FROM (
SELECT
u.city, o.order_no, o.amount,
row_number() OVER (PARTITION BY u.city ORDER BY o.amount DESC, o.id) AS rn
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.status <> 'cancelled'
) t
WHERE rn <= 3
ORDER BY city, rn; city | order_no | amount | rn
------+------------+--------+----
上海 | SO00000003 | 999 | 1
上海 | SO00000007 | 999 | 2
上海 | SO00000011 | 999 | 3
北京 | SO00000001 | 999 | 1
北京 | SO00000009 | 999 | 2
北京 | SO00000013 | 999 | 3
广州 | SO00000002 | 999 | 1
广州 | SO00000006 | 999 | 2
广州 | SO00000010 | 999 | 3
深圳 | SO00000004 | 999 | 1
深圳 | SO00000008 | 999 | 2
深圳 | SO00000012 | 999 | 3
杭州 | SO00000005 | 999 | 1
杭州 | SO00000017 | 999 | 2
杭州 | SO00000019 | 999 | 3
(15 rows)-- 方案 B:rank,并列全部保留(可能超过 3 行)
SELECT city, order_no, amount, rk
FROM (
SELECT
u.city, o.order_no, o.amount,
rank() OVER (PARTITION BY u.city ORDER BY o.amount DESC) AS rk
FROM orders o JOIN users u ON u.id = o.user_id
WHERE o.status <> 'cancelled'
) t
WHERE rk <= 3
ORDER BY city, rk
LIMIT 10; city | order_no | amount | rk
------+------------+--------+----
上海 | SO00000003 | 999 | 1
上海 | SO00000007 | 999 | 1
上海 | SO00000011 | 999 | 1
上海 | SO00000015 | 999 | 1
上海 | SO00000019 | 999 | 1
上海 | SO00000023 | 999 | 1
上海 | SO00000027 | 999 | 1
上海 | SO00000031 | 999 | 1
上海 | SO00000035 | 999 | 1
上海 | SO00000039 | 999 | 1
(10 rows)两个方案的取舍:
| 需求 | 选 row_number | 选 rank |
|---|---|---|
| 前端固定显示 3 行 | ✓ | ✗(可能几十行) |
| 榜单要「并列第一」 | ✗(并列被强行拆开) | ✓ |
| 导出数据统计口径固定 | ✓ | ✗(行数不定) |
| 评优「并列同奖」 | ✗ | ✓ |
步骤 6:验证结果可复现
-- 同一查询执行两次,比较结果是否完全一致
WITH run1 AS (
SELECT id, row_number() OVER (ORDER BY amount DESC) AS rn FROM orders_tied
),
run2 AS (
SELECT id, row_number() OVER (ORDER BY amount DESC) AS rn FROM orders_tied
)
SELECT '不稳定的行数' AS 检查项, count(*) FROM (
SELECT id, rn FROM run1 EXCEPT ALL SELECT id, rn FROM run2
) t
UNION ALL
SELECT '稳定写法差异行数', count(*) FROM (
SELECT id, rn FROM (
SELECT id, row_number() OVER (ORDER BY amount DESC, id) AS rn FROM orders_tied
) a EXCEPT ALL
SELECT id, rn FROM (
SELECT id, row_number() OVER (ORDER BY amount DESC, id) AS rn FROM orders_tied
) b
) t; 检查项 | count
-------------------+-------
不稳定的行数 | 0
稳定写法差异行数 | 0在单次会话内并列行的扫描顺序通常是稳定的,所以差异为 0。但跨版本、跨计划变化、并行扫描时并不保证,因此排序键补唯一列依然是必须的。
注意事项
rank与row_number在加了唯一列排序键后会失去并列语义,这是最常见的误用:
-- 错误!期望并列排名,但 id 拆开了并列
rank() OVER (ORDER BY amount DESC, id)
-- 正确
rank() OVER (ORDER BY amount DESC)row_number取 Top-N 会「吃掉」并列行。若业务要求并列同奖,必须用rank:
-- 不推荐:并列第 1 的行只留了一行
SELECT * FROM (SELECT *, row_number() OVER (ORDER BY score DESC) AS rn FROM t) x
WHERE rn = 1;
-- 推荐
SELECT * FROM (SELECT *, rank() OVER (ORDER BY score DESC) AS rk FROM t) x
WHERE rk = 1;ntile的并行度会整体漂移。给某一行加 1 分,可能让后面所有行的桶号都变,不要用它做稳定的用户分群:
-- 不推荐:数据增删后层级会变
ntile(4) OVER (ORDER BY 消费额 DESC)
-- 推荐:用固定阈值切分
CASE WHEN 消费额 >= 10000 THEN 1
WHEN 消费额 >= 8000 THEN 2
WHEN 消费额 >= 5000 THEN 3
ELSE 4 END AS 层级ntile的桶数不能是表达式或列,必须是正整数常量:
-- 错误!桶数必须是常量
SELECT ntile(col) OVER (ORDER BY id) FROM t;
-- 报错:argument of ntile must be greater than zerocume_dist与percent_rank的分母不同,混用会得到不一致的百分位:
| 指标 | 第一个同行集的值 | 最后一个同行集的值 |
|---|---|---|
cume_dist | 大于 0,等于首个同行集占比 | 恒为 1 |
percent_rank | 恒为 0 | 小于 1 |
- 排名函数不受帧子句影响。写
ROWS BETWEEN ...对rank没有任何作用,只对聚合类窗口函数有效:
-- ROWS 子句在这里是多余的,rank 的结果不会改变
rank() OVER (ORDER BY amount DESC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW)- 排名函数不能出现在
WHERE中,必须外套一层子查询:
-- 错误!
SELECT * FROM orders WHERE rank() OVER (ORDER BY amount DESC) <= 3;
-- 正确
SELECT * FROM (SELECT *, rank() OVER (ORDER BY amount DESC) AS rk FROM orders) t
WHERE rk <= 3;最佳实践
- 按需求选函数,记住这张速查表:
| 需求 | 函数 | 并列行为 |
|---|---|---|
| 恰好 N 行 | row_number | 并列被拆开 |
| 榜单同奖 | rank | 并列同名次,名次跳跃 |
| 等级编号 | dense_rank | 并列同名次,名次连续 |
| 人群分层 | ntile | 均分桶,数据变动会漂移 |
| 百分位 | cume_dist / percent_rank | 同行集共享值 |
- 排序键一律补唯一列,只是要注意排序键的顺序:
-- 需要并列排名:唯一列不能进排序键
rank() OVER (PARTITION BY city ORDER BY amount DESC)
-- 需要稳定序号:唯一列必须进排序键
row_number() OVER (PARTITION BY city ORDER BY amount DESC, id)row_number永远不在ORDER BY里放唯一列之外的东西做决胜,决胜列越少越稳定。分组 Top-N 统一用下面的骨架,性能与可读性都好:
SELECT * FROM (
SELECT
分组列, 明细列,
row_number() OVER (PARTITION BY 分组列 ORDER BY 排序键 DESC, 唯一列) AS rn
FROM 表
WHERE 过滤条件
) t
WHERE rn <= N;- 给排名窗口建复合索引,索引列顺序与
PARTITION BY+ORDER BY一致:
CREATE INDEX ON orders (user_id, amount DESC, id);
CREATE INDEX ON orders (status, amount DESC, id);- 数据量大时,先过滤再排名,把
WHERE放在窗口函数所在的那一层:
-- 好:先过滤掉 75% 的已取消订单,再对 5 万行排名
SELECT * FROM (
SELECT *, row_number() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS rn
FROM orders
WHERE status = 'paid'
) t WHERE rn <= 3;
-- 差:对 20 万行排名后再过滤
SELECT * FROM (
SELECT *, row_number() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS rn
FROM orders
) t WHERE rn <= 3 AND status = 'paid';- 调试排名结果时,先补一个
count(*) OVER ()看总行数,便于判断rank空档是否超出预期:
SELECT
order_no, amount,
rank() OVER w AS 排名,
count(*) OVER () AS 总行数
FROM orders
WINDOW w AS (ORDER BY amount DESC)
ORDER BY 排名
LIMIT 10;下一节
继续学习:分析函数