OVER 子句详解
本节把 OVER 子句拆开讲透:PARTITION BY 怎么切分区、ORDER BY 怎么定顺序、WINDOW 命名子句怎么复用窗口定义、多个窗口函数如何共用一次排序,以及默认帧(Default Frame)带来的隐蔽语义差异。学完本节,你将能准确写出每个窗口函数的窗口范围,不再靠猜。
前置知识
阅读本节前,建议先了解:窗口函数概述
基础概念
OVER 子句(OVER Clause) 紧跟在窗口函数名与参数之后,用来描述「这个函数在哪些行、以什么顺序、按什么范围计算」。它由三块可选内容组成:分区(PARTITION BY)、排序(ORDER BY)、帧(ROWS / RANGE / GROUPS)。
| 概念 | 术语 | 说明 |
|---|---|---|
OVER 子句 | OVER Clause | 窗口函数的作用域声明 |
| 分区 | Partition | PARTITION BY 切分出的互不相交的组 |
| 窗口排序 | Window Ordering | ORDER BY 定义的窗口内行序 |
| 帧 | Frame | 分区内参与计算的连续行区间 |
| 默认帧 | Default Frame | 不写帧子句时系统采用的范围 |
| 命名窗口 | Named Window | 用 WINDOW 子句命名的可复用窗口定义 |
| 窗口继承 | Window Inheritance | 命名窗口可以被引用并追加 ORDER BY |
| 同行集 | Peer Group | 排序键取值相同的行 |
三块内容的可选性与语义:
| 组成 | 是否必填 | 不写时的行为 | 写了以后的影响 |
|---|---|---|---|
PARTITION BY | 可选 | 整个结果集视为一个分区 | 计算在分区内独立封闭 |
ORDER BY | 可选 | 行序未定义,帧为整个分区 | 帧变为「分区起点到当前行」 |
| 帧子句 | 可选 | 有 ORDER BY 时为 RANGE UNBOUNDED PRECEDING AND CURRENT ROW;无 ORDER BY 时为整个分区 | 精确控制参与计算的行区间 |
WINDOW 子句 | 可选 | 每个函数各自写完整定义 | 复用定义、减少重复书写 |
默认帧的两种情况必须记牢,这是窗口函数最经典的坑:
| 窗口定义 | 默认帧 | 效果 |
|---|---|---|
OVER () | 整个分区 | sum 返回全表总和,每行相同 |
OVER (PARTITION BY city) | 整个分区 | sum 返回该城市总和,组内每行相同 |
OVER (ORDER BY created_at) | 分区起点 → 当前行 | sum 返回累计和,逐行递增 |
OVER (PARTITION BY city ORDER BY created_at) | 分区起点 → 当前行 | sum 返回城市内累计和 |
-- 同一张表,三种 OVER 写法,三个完全不同的结果
SELECT
o.id,
o.amount,
sum(o.amount) OVER () AS 全表总额,
round(sum(o.amount) OVER (PARTITION BY u.city), 2) AS 城市总额,
round(sum(o.amount) OVER (
PARTITION BY u.city ORDER BY o.created_at, o.id
), 2) AS 城市内累计
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE u.city = '杭州' AND o.status <> 'cancelled'
ORDER BY o.created_at, o.id
LIMIT 5; id | amount | 全表总额 | 城市总额 | 城市内累计
----+--------+---------------+------------+------------
5 | 512.30 | 247362941.08 | 21991099.2 | 512.30
10 | 108.90 | 247362941.08 | 21991099.2 | 621.20
15 | 776.40 | 247362941.08 | 21991099.2 | 1397.60
20 | 233.10 | 247362941.08 | 21991099.2 | 1630.70
25 | 640.50 | 247362941.08 | 21991099.2 | 2271.20
(5 rows)语法与用法
函数名(参数) OVER (
[PARTITION BY 表达式 [, ...]]
[ORDER BY 表达式 [ASC | DESC] [NULLS FIRST | NULLS LAST] [, ...]]
[帧子句]
)
-- 命名窗口
SELECT
函数名(参数) OVER 窗口名,
函数名(参数) OVER (窗口名 ORDER BY ...)
FROM ...
[WINDOW 窗口名 AS (PARTITION BY ... [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);
CREATE INDEX ON orders (status, amount DESC);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;
INSERT INTO orders (order_no, user_id, amount, status, created_at)
SELECT
'SO' || lpad(i::text, 8, '0'),
1 + (i % 20000),
round((random() * 900 + 100)::numeric, 2),
(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;详细说明
PARTITION BY 的粒度与省略
PARTITION BY 决定计算的封闭范围。粒度越细,组内行数越少,计算越快;粒度越粗,越接近全表聚合。
-- 粒度 1:全表(最粗)
SELECT order_no, amount,
rank() OVER (ORDER BY amount DESC) AS 全表排名
FROM orders
LIMIT 3;
-- 粒度 2:按状态
SELECT order_no, status, amount,
rank() OVER (PARTITION BY status ORDER BY amount DESC) AS 状态内排名
FROM orders
LIMIT 3;
-- 粒度 3:按用户
SELECT order_no, user_id, amount,
rank() OVER (PARTITION BY user_id ORDER BY amount DESC) AS 用户内排名
FROM orders
LIMIT 3;| 分区粒度 | 分区数量级 | 单分区行数 | 典型用途 |
|---|---|---|---|
| 不分区 | 1 | 20 万 | 全表排名、全表累计 |
| 按状态 | 4 | 约 5 万 | 同状态内比较 |
| 按城市 | 5 | 约 4 万 | 城市榜 |
| 按用户 | 2 万 | 约 10 | 用户内 Top-N |
省略 PARTITION BY 时要格外小心:它意味着「所有参与计算的行属于同一组」,很容易写出语义不符的 SQL:
-- 错误!本意是「每笔订单占本城市总额的比例」,漏了分区就成了全表比例
SELECT u.city, o.order_no,
round(100.0 * o.amount / sum(o.amount) OVER (), 2) AS 占比
FROM orders o JOIN users u ON u.id = o.user_id
LIMIT 3;
-- 正确
SELECT u.city, o.order_no,
round(100.0 * o.amount / sum(o.amount) OVER (PARTITION BY u.city), 2) AS 占比
FROM orders o JOIN users u ON u.id = o.user_id
LIMIT 3;分区列可以同时出现在 SELECT 中,这与 GROUP BY 不同——GROUP BY 后只能选分组键,而窗口函数不影响输出列。
ORDER BY 的作用与 NULL 位置
ORDER BY 在 OVER 内部只决定行序,不影响输出顺序(输出顺序仍需在最外层写 ORDER BY)。行序对排名函数是必需的,对聚合窗口函数则会改变默认帧。
-- 只排名,不改变输出顺序
SELECT order_no, amount,
row_number() OVER (ORDER BY amount DESC) AS rn
FROM orders
LIMIT 5;
-- 输出顺序由这里的 LIMIT 前的执行计划决定,不保证按 rn 排列-- 正确:最外层补 ORDER BY
SELECT order_no, amount,
row_number() OVER (ORDER BY amount DESC) AS rn
FROM orders
ORDER BY rn
LIMIT 5; order_no | amount | rn
------------+--------+----
SO00019xxx | 999.99 | 1
SO00017xxx | 999.99 | 2
SO00011xxx | 999.98 | 3
SO00009xxx | 999.97 | 4
SO00005xxx | 999.96 | 5
(5 rows)NULL 在窗口排序中遵循 PostgreSQL 的默认规则:
| 排序方向 | 默认 NULL 位置 | 可用修饰 |
|---|---|---|
ASC | 最后(NULLS LAST) | NULLS FIRST |
DESC | 最前(NULLS FIRST) | NULLS LAST |
-- 默认:DESC 时 NULL 排最前,占了 1~3 名
SELECT order_no, discount,
row_number() OVER (ORDER BY discount DESC) AS rn
FROM orders
LIMIT 5;
-- 推荐:显式把 NULL 排在最后
SELECT order_no, discount,
row_number() OVER (ORDER BY discount DESC NULLS LAST) AS rn
FROM orders
LIMIT 5;排序键必须能唯一定序,否则结果不确定。row_number 尤其如此:
-- 不推荐:金额相同的行,序号每次执行可能不同
row_number() OVER (ORDER BY amount DESC)
-- 推荐:补唯一键做决胜条件
row_number() OVER (ORDER BY amount DESC, id)ORDER BY 是否加在窗口内,是 sum 语义的分水岭:
SELECT
id,
amount,
round(sum(amount) OVER (), 2) AS 全表总额,
round(sum(amount) OVER (ORDER BY id), 2) AS 累计金额,
round(avg(amount) OVER (ORDER BY id), 2) AS 累计均值
FROM orders
ORDER BY id
LIMIT 5; id | amount | 全表总额 | 累计金额 | 累计均值
----+--------+---------------+-----------+----------
1 | 736.10 | 247362941.08 | 736.10 | 736.10
2 | 180.20 | 247362941.08 | 916.30 | 458.15
3 | 654.60 | 247362941.08 | 1570.90 | 523.63
4 | 703.70 | 247362941.08 | 2274.60 | 568.65
5 | 512.30 | 247362941.08 | 2786.90 | 557.38
(5 rows)WINDOW 命名子句与窗口继承
当一条查询里有多个窗口函数时,重复书写相同的 OVER 定义既冗长又容易写错。WINDOW 子句允许给窗口定义命名,然后多次引用。
-- 不推荐:同样的窗口定义写了三遍
SELECT
order_no,
row_number() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS 序号,
rank() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS 排名,
dense_rank() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS 密集排名
FROM orders
LIMIT 5;
-- 推荐:命名一次,引用三次
SELECT
order_no,
row_number() OVER w AS 序号,
rank() OVER w AS 排名,
dense_rank() OVER w AS 密集排名
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC, id)
LIMIT 5;命名窗口可以被继承,在引用时追加或覆盖部分内容(只能追加 ORDER BY,不能覆盖 PARTITION BY):
SELECT
order_no,
sum(amount) OVER w AS 用户内累计,
sum(amount) OVER (w ORDER BY id) AS 用户内按 id 累计
FROM orders
WINDOW w AS (PARTITION BY user_id)
ORDER BY user_id, id
LIMIT 6; order_no | 用户内累计 | 用户内按 id 累计
------------+------------+------------------
SO00000001 | 736.10 | 736.10
SO00020001 | 735.20 | 735.20
SO00040001 | 464.40 | 464.40
SO00060001 | 892.80 | 892.80
SO00080001 | 149.20 | 149.20
SO00100001 | 861.40 | 861.40
(6 rows)多条窗口定义时,PostgreSQL 只在排序键完全相同时复用一次排序:
| 窗口定义组合 | 需要的排序次数 | 说明 |
|---|---|---|
两者 PARTITION BY 与 ORDER BY 完全相同 | 1 次 | 复用同一次排序 |
仅 PARTITION BY 相同,ORDER BY 不同 | 2 次 | 每个不同的排序键各排一次 |
PARTITION BY 不同 | 2 次 | 分区键不同,无法共用 |
-- 两次排序:两个窗口的排序键不同
EXPLAIN (ANALYZE, BUFFERS)
SELECT
rank() OVER (PARTITION BY user_id ORDER BY amount DESC) AS 金额排名,
rank() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS 时间排名
FROM orders
LIMIT 10;Limit (cost=... rows=10 width=16) (actual time=... rows=10 loops=1)
-> WindowAgg (cost=... rows=200000 width=16)
-> WindowAgg (cost=... rows=200000 width=16)
-> Sort (cost=... rows=200000 width=32)
Sort Key: user_id, created_at DESC
-> Sort (cost=... rows=200000 width=32)
Sort Key: user_id, amount DESC
-> Seq Scan on ordersWindowAgg 嵌套在另一个 WindowAgg 之上,底下是两层 Sort,代价相当可观。若两个窗口的排序键一致,计划里只会出现一个 Sort。
默认帧与显式帧子句
帧(Frame)是分区内更细的一层范围限定。只有聚合类窗口函数受帧影响,排名函数(row_number、rank 等)和取值函数(lag、lead)不受帧影响。
| 帧写法 | 含义 | 典型用途 |
|---|---|---|
RANGE UNBOUNDED PRECEDING AND CURRENT ROW | 分区起点到当前行(含同行集) | 默认行为,累计值 |
ROWS UNBOUNDED PRECEDING AND CURRENT ROW | 分区起点到当前物理行 | 逐行累计,不受同行集影响 |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | 前三行滑动窗口 | 三点移动平均 |
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW | 最近 7 行 | 七日移动平均 |
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING | 整个分区 | 与不写 ORDER BY 等价 |
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | 当前行到分区末尾 | 剩余量、倒数累计 |
RANGE 与 ROWS 的关键差别:RANGE 按排序键的值划定范围,会把所有排序键与当前行相同的行(同行集)一起纳入;ROWS 严格按物理行位置计数。
-- RANGE(默认):金额相同的行全部算入累计
SELECT
id, amount,
round(sum(amount) OVER (
ORDER BY amount RANGE UNBOUNDED PRECEDING AND CURRENT ROW
), 2) AS range累计
FROM orders
ORDER BY amount, id
LIMIT 6; id | amount | range累计
----+--------+-----------
7 | 100.00 | 200.00 -- 两笔 100.00 的订单一起算入
13 | 100.00 | 200.00
9 | 100.01 | 300.01
21 | 100.02 | 400.03
3 | 100.03 | 500.06
17 | 100.04 | 600.10
(6 rows)-- ROWS:严格逐行累加
SELECT
id, amount,
round(sum(amount) OVER (
ORDER BY amount, id ROWS UNBOUNDED PRECEDING AND CURRENT ROW
), 2) AS rows累计
FROM orders
ORDER BY amount, id
LIMIT 6; id | amount | rows累计
----+--------+----------
7 | 100.00 | 100.00 -- 严格逐行,第一行只算自己
13 | 100.00 | 200.00
9 | 100.01 | 300.01
21 | 100.02 | 400.03
3 | 100.03 | 500.06
17 | 100.04 | 600.10
(6 rows)两者的区别在第 1 行就显现了:RANGE 给出 200.00(把同值的第 13 行也算进来),ROWS 给出 100.00。
实战示例
场景:为用户详情页设计「该用户订单流水」表格,需要同时给出:金额排名、按时间累计金额、最近 7 笔的滑动平均、以及全站对比用的金额四分位。要求用一个 WINDOW 子句组织,避免重复定义。
步骤 1:先看单个用户的原始订单
SELECT id, order_no, amount, created_at
FROM orders
WHERE user_id = 42 AND status <> 'cancelled'
ORDER BY created_at, id; id | order_no | amount | created_at
----+------------+--------+------------
43 | SO00000043 | 736.10 | 2024-06-03
45 | SO00000045 | 654.60 | 2024-07-01
47 | SO00000047 | 180.90 | 2024-08-15
49 | SO00000049 | 233.10 | 2024-09-20
3 | SO00020003 | 703.70 | 2025-04-11
5 | SO00040005 | 512.30 | 2025-06-02
(6 rows)步骤 2:用一个命名窗口完成多个指标
SELECT
o.order_no,
o.amount,
o.created_at,
row_number() OVER w_order AS 流水序号,
rank() OVER w_amount AS 金额排名,
round(sum(o.amount) OVER w_order, 2) AS 时间累计,
round(avg(o.amount) OVER (
ORDER BY o.created_at, o.id
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS 三笔滑动均值
FROM orders o
WHERE o.user_id = 42 AND o.status <> 'cancelled'
WINDOW
w_order AS (ORDER BY o.created_at, o.id),
w_amount AS (ORDER BY o.amount DESC, o.id)
ORDER BY o.created_at, o.id; order_no | amount | created_at | 流水序号 | 金额排名 | 时间累计 | 三笔滑动均值
------------+--------+------------+----------+----------+----------+--------------
SO00000043 | 736.10 | 2024-06-03 | 1 | 1 | 736.10 | 736.10
SO00000045 | 654.60 | 2024-07-01 | 2 | 2 | 1390.70 | 695.35
SO00000047 | 180.90 | 2024-08-15 | 3 | 6 | 1571.60 | 523.87
SO00000049 | 233.10 | 2024-09-20 | 4 | 5 | 1804.70 | 356.20
SO00020003 | 703.70 | 2025-04-11 | 5 | 3 | 2508.40 | 372.57
SO00040005 | 512.30 | 2025-06-02 | 6 | 4 | 3020.70 | 483.03
(6 rows)步骤 3:验证帧子句确实按预期工作
SELECT
order_no,
amount,
round(sum(amount) OVER (
ORDER BY created_at, id ROWS UNBOUNDED PRECEDING
), 2) AS 物理累计,
round(sum(amount) OVER (
ORDER BY created_at, id RANGE UNBOUNDED PRECEDING
), 2) AS 键值累计
FROM orders
WHERE user_id = 42 AND status <> 'cancelled'
ORDER BY created_at, id; order_no | amount | 物理累计 | 键值累计
------------+--------+----------+----------
SO00000043 | 736.10 | 736.10 | 736.10
SO00000045 | 654.60 | 1390.70 | 1390.70
SO00000047 | 180.90 | 1571.60 | 1571.60
SO00000049 | 233.10 | 1804.70 | 1804.70
SO00020003 | 703.70 | 2508.40 | 2508.40
SO00040005 | 512.30 | 3020.70 | 3020.70
(6 rows)因为没有重复的时间戳,两者结果相同。换一个存在重复排序键的场景,差异立刻出现:
-- 同一持仓日期有多行,RANGE 会把同日的行一起算入
SELECT
created_at,
amount,
round(sum(amount) OVER (
ORDER BY created_at ROWS UNBOUNDED PRECEDING
), 2) AS rows累计,
round(sum(amount) OVER (
ORDER BY created_at RANGE UNBOUNDED PRECEDING
), 2) AS range累计
FROM orders
WHERE user_id = 42 AND status <> 'cancelled'
ORDER BY created_at
LIMIT 3;步骤 4:观察命名窗口对计划的影响
EXPLAIN (ANALYZE, BUFFERS)
SELECT
rank() OVER w AS 金额排名,
dense_rank() OVER w AS 密集排名,
ntile(4) OVER w AS 四分位
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC, id)
LIMIT 10;Limit (cost=... rows=10 width=24) (actual time=... rows=10 loops=1)
Buffers: shared hit=...
-> WindowAgg (cost=... rows=200000 width=24) (actual time=... rows=10 loops=1)
Window: w AS (PARTITION BY user_id ORDER BY amount DESC, id)
Buffers: shared hit=...
-> Sort (cost=... rows=200000 width=24) (actual time=... rows=200001 loops=1)
Sort Key: user_id, amount DESC, id
Sort Method: external merge Disk: 12288kB
Buffers: shared hit=...
-> Seq Scan on orders (cost=... rows=200000 width=24)三个函数共用一个 WindowAgg、一次 Sort。这就是命名窗口的收益。
步骤 5:给窗口排序建索引,消除排序节点
-- 排序键:user_id, amount DESC, id
CREATE INDEX ON orders (user_id, amount DESC, id);
ANALYZE orders;
EXPLAIN (ANALYZE, BUFFERS)
SELECT
rank() OVER w AS 金额排名
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC, id)
LIMIT 10;Limit (cost=... rows=10 width=8) (actual time=... rows=10 loops=1)
-> WindowAgg (cost=... rows=200000 width=8) (actual time=... rows=10 loops=1)
Window: w AS (PARTITION BY user_id ORDER BY amount DESC, id)
-> Index Scan using orders_user_id_amount_id_idx on orders (cost=... rows=200000 width=8)Sort 节点消失了,取而代之的是按索引顺序返回的 Index Scan,索引既能提供分区顺序,也能提供排序顺序。
步骤 6:用 EXCEPT 验证命名窗口与展开写法的等价性
WITH a AS (
SELECT id,
rank() OVER w AS r1, dense_rank() OVER w AS r2
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC, id)
),
b AS (
SELECT id,
rank() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS r1,
dense_rank() OVER (PARTITION BY user_id ORDER BY amount DESC, id) AS r2
FROM orders
)
SELECT 'only_in_a' AS 差异方向, count(*) FROM (
SELECT id, r1, r2 FROM a EXCEPT ALL SELECT id, r1, r2 FROM b
) t
UNION ALL
SELECT 'only_in_b', count(*) FROM (
SELECT id, r1, r2 FROM b EXCEPT ALL SELECT id, r1, r2 FROM a
) t; 差异方向 | count
-----------+-------
only_in_a | 0
only_in_b | 0
(2 rows)注意事项
- 命名窗口只能追加
ORDER BY,不能覆盖PARTITION BY:
-- 语法错误!不能对已定义了 PARTITION BY 的窗口重新指定分区
SELECT sum(amount) OVER (w PARTITION BY city)
FROM orders
WINDOW w AS (PARTITION BY user_id);
-- 报错:cannot override PARTITION BY clause of window "w"
-- 合法:追加排序
SELECT sum(amount) OVER (w ORDER BY amount DESC)
FROM orders
WINDOW w AS (PARTITION BY user_id);WINDOW子句必须写在HAVING之后、ORDER BY之前:
/* 子句顺序:
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ...
WINDOW ...
ORDER BY ... LIMIT ...
*/RANGE帧只支持单列排序键后的偏移量,且不同引擎行为一致:
-- 错误!RANGE 带偏移量时,ORDER BY 只能有一列
SELECT sum(amount) OVER (
ORDER BY created_at, id RANGE BETWEEN 1 PRECEDING AND CURRENT ROW
) FROM orders;
-- 报错:RANGE with offset PRECEDING/FOLLOWING requires exactly one ORDER BY columnROWS与RANGE在默认帧上不同,尤其当排序键有重复值。需要严格按行数计数时,必须显式写ROWS:
-- 不推荐:默认 RANGE,重复金额会把后续行一起算入
sum(amount) OVER (ORDER BY amount)
-- 推荐:需要逐行累加时明确写 ROWS
sum(amount) OVER (ORDER BY amount, id ROWS UNBOUNDED PRECEDING)GROUPS帧按「同行集的个数」计数,是 PostgreSQL 11+ 提供的第三种帧模式:
-- 当前同行集 + 前一行同集内的 2 个同行集
SELECT sum(amount) OVER (
ORDER BY created_at GROUPS BETWEEN 2 PRECEDING AND CURRENT ROW
) FROM orders;ORDER BY加在OVER内部不等于输出有序,最外层仍要写ORDER BY。这条规则在分页场景尤其重要。不要在一个
OVER里混用不同粒度的排序键来「省一次排序」。排序键不一致就必须重新排序,拆分反而更清晰:
-- 不推荐:硬凑成同一个窗口
SELECT
rank() OVER (ORDER BY amount DESC) AS 金额排名,
rank() OVER (ORDER BY created_at DESC, id) AS 时间排名
FROM orders;
-- 计划中仍会出现两次 Sort,并不会因为写在同一层而共用最佳实践
- 窗口定义统一用
WINDOW命名,即使只有两个函数,也便于后续扩展:
SELECT
row_number() OVER w AS 序号,
rank() OVER w AS 排名
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC, id);- 排序键一律补唯一列做决胜,保证结果可复现:
ORDER BY amount DESC, id
ORDER BY created_at, id- 需要行序的窗口,优先看能不能用索引消除排序:
| 窗口排序键 | 建议索引 |
|---|---|
(user_id, amount DESC, id) | CREATE INDEX ON orders (user_id, amount DESC, id); |
(city, created_at, id) | CREATE INDEX ON users (city, created_at, id); |
(status, amount DESC) | CREATE INDEX ON orders (status, amount DESC, id); |
NULL排序位置显式声明,不要依赖默认值:
ORDER BY discount DESC NULLS LAST需要「严格逐行」的累计,显式写
ROWS;需要「按值分组」的累计,显式写RANGE。不写会有隐患。把窗口函数放在最内层,过滤放在外层,这是唯一可行的组织方式(因为窗口函数不能出现在
WHERE中):
SELECT * FROM (
SELECT *, row_number() OVER w AS rn
FROM orders
WINDOW w AS (PARTITION BY user_id ORDER BY amount DESC, id)
) t
WHERE rn <= 3;- 调试时关注计划中
Sort节点与WindowAgg的层级关系:
| 计划形态 | 含义 | 优化方向 |
|---|---|---|
WindowAgg → Index Scan | 排序由索引提供 | 已最优 |
WindowAgg → Sort → ... | 显式排序 | 考虑建匹配索引 |
WindowAgg → WindowAgg → Sort ×2 | 多套排序键 | 尽量统一排序键 |
Sort Method: external merge | 排序溢出到磁盘 | 调大 work_mem 或减少排序行数 |
下一节
继续学习:排名函数