约 767 字
10 分钟
数据库中的窗口函数
2026年7月22日
2026年7月27日
随手笔记
摘要
PostgreSQL 的窗口函数(Window Functions)是数据分析的神器,它允许你在不减少行数的前提下,对与当前行相关的一组行(即“窗口”)执行聚合或排名计算。
先创建一个订单
CREATE TABLE orders
(
order_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
amount NUMERIC(10, 2) NOT NULL,
category VARCHAR(20) NOT NULL
);
-- 插入模拟数据(3个客户,跨3个月,含同金额并列场景)
INSERT INTO orders (customer_id, order_date, amount, category)
VALUES (1001, '2025-01-05', 200.00, '电子'),
(1001, '2025-01-18', 350.00, '电子'),
(1001, '2025-02-10', 350.00, '家居'),
(1001, '2025-03-01', 500.00, '电子'),
(1002, '2025-01-10', 150.00, '家居'),
(1002, '2025-02-14', 600.00, '电子'),
(1002, '2025-02-28', 400.00, '家居'),
(1003, '2025-01-20', 800.00, '电子'),
(1003, '2025-02-05', 300.00, '家居'),
(1003, '2025-03-15', 900.00, '电子');
窗口函数的语法结构
function_name() OVER (
[PARTITION BY column] -- 分组:将数据划分为多个分区(类似 GROUP BY,但不折叠行)
[ORDER BY column] -- 排序:定义窗口内的计算顺序
[frame_clause] -- 帧范围:精确定义"窗口"包含哪些行
)基本函数
| 函数 | 说明 | 并列处理 |
|---|---|---|
ROW_NUMBER() |
连续唯一序号 | 无并列,强制递增 |
RANK() |
跳跃排名 | 并列同名次,后续跳号 (1,1,3) |
DENSE_RANK() |
密集排名 | 并列同名次,后续不跳号 (1,1,2) |
NTILE(n) |
等分桶编号 | 将分区分为 n 桶,返回桶号 |
所有标准聚合函数都可作窗口函数使用:SUM, AVG, COUNT, MIN, MAX 等。
举例 1.每个客户按订单金额排名(处理并列)
SELECT customer_id,
order_date,
amount,
RANK() OVER w AS rank_jump,
DENSE_RANK() OVER w AS rank_dense, -- 推荐写
ROW_NUMBER() OVER w AS row_num -- 强制唯一: 1,2,3
FROM orders
WINDOW w AS (PARTITION BY customer_id ORDER BY amount DESC);结果
| customer_id | order_date | amount | rank_jump | rank_dense | row_num |
|---|---|---|---|---|---|
| 1001 | 2025-03-01 | 500.00 | 1 | 1 | 1 |
| 1001 | 2025-01-18 | 350.00 | 2 | 2 | 2 |
| 1001 | 2025-02-10 | 350.00 | 2 | 2 | 3 |
| 1001 | 2025-01-05 | 200.00 | 4 | 3 | 4 |
| 1002 | 2025-02-14 | 600.00 | 1 | 1 | 1 |
| 1002 | 2025-02-28 | 400.00 | 2 | 2 | 2 |
| 1002 | 2025-01-10 | 150.00 | 3 | 3 | 3 |
| 1003 | 2025-03-15 | 900.00 | 1 | 1 | 1 |
| 1003 | 2025-01-20 | 800.00 | 2 | 2 | 2 |
| 1003 | 2025-02-05 | 300.00 | 3 | 3 | 3 |
可以看到三个排序的区别,其中 rank_dense不会跳过
偏移/取值函数
| 函数 | 说明 |
|---|---|
LAG(col, n, default) |
取前第 n 行的值 |
LEAD(col, n, default) |
取后第 n 行的值 |
FIRST_VALUE(col) |
窗口帧内第一行的值 |
LAST_VALUE(col) |
窗口帧内最后一行的值 |
NTH_VALUE(col, n) |
窗口帧内第 n 行的值 |
下面举例写法仅用于解释,不考虑运行性能
2.计算每个客户的累计消费 & 月环比增长
SELECT customer_id,
order_date,
amount,
-- 截至当前订单的累计消费 可以看出是每次递增
SUM(amount) OVER w AS running_total,
-- 上一笔订单金额 LAG()取前1个的值
LAG(amount, 1) OVER w AS prev_amount,
-- 环比增长率(%) = (本期金额 - 上期金额) / 上期金额 * 100
ROUND(
(amount - LAG(amount, 1) OVER w) / NULLIF(LAG(amount, 1) OVER w, 0) * 100
, 2) AS mom_growth_pct
FROM orders
WINDOW w AS (PARTITION BY customer_id ORDER BY order_date)
ORDER BY customer_id, order_date;可以详细分析
| customer_id | order_date | amount | running_total | prev_amount | mom_growth_pct |
|---|---|---|---|---|---|
| 1001 | 2025-01-05 | 200.00 | 200 | null | null |
| 1001 | 2025-01-18 | 350.00 | 550 | 200 | 75 |
| 1001 | 2025-02-10 | 350.00 | 900 | 350 | 0 |
| 1001 | 2025-03-01 | 500.00 | 1400 | 350 | 42.86 |
| 1002 | 2025-01-10 | 150.00 | 150 | null | null |
| 1002 | 2025-02-14 | 600.00 | 750 | 150 | 300 |
| 1002 | 2025-02-28 | 400.00 | 1150 | 600 | -33.33 |
| 1003 | 2025-01-20 | 800.00 | 800 | null | null |
| 1003 | 2025-02-05 | 300.00 | 1100 | 800 | -62.5 |
| 1003 | 2025-03-15 | 900.00 | 2000 | 300 | 200 |
3.找出每个客户金额最高的 Top 2
-- 使用 ROW_NUMBER() 严格保证只有两条数据, RANK()不是严格递增
SELECT *,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
FROM orders;
-- 最后结果
WITH ranked AS (SELECT *,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
FROM orders)
SELECT customer_id, order_date, amount, category
FROM ranked
WHERE rn <= 2
ORDER BY customer_id, amount DESC;
| customer_id | order_date | amount | category |
|---|---|---|---|
| 1001 | 2025-03-01 | 500.00 | 电子 |
| 1001 | 2025-01-18 | 350.00 | 电子 |
| 1002 | 2025-02-14 | 600.00 | 电子 |
| 1002 | 2025-02-28 | 400.00 | 家居 |
| 1003 | 2025-03-15 | 900.00 | 电子 |
| 1003 | 2025-01-20 | 800.00 | 电子 |
4.每笔订单占该客户总消费的百分比
SELECT customer_id,
order_date,
amount,
SUM(amount) OVER (PARTITION BY customer_id ORDER BY amount DESC) AS customer_total1,
SUM(amount) OVER (PARTITION BY customer_id) AS customer_total2, -- 注意:无 ORDER BY = 整个分区
ROUND(amount / SUM(amount) OVER (PARTITION BY customer_id) * 100, 2) AS pct_of_customer
FROM orders
ORDER BY customer_id, amount DESC;可以看到 customer_total1 有 ORDER BY 后逐行递增,无 ORDER BY 整个行都是相同
可以看到每个订单明显占比
| customer_id | order_date | amount | customer_total1 | customer_total2 | pct_of_customer |
|---|---|---|---|---|---|
| 1001 | 2025-03-01 | 500.00 | 500 | 1400 | 35.71 |
| 1001 | 2025-01-18 | 350.00 | 1200 | 1400 | 25 |
| 1001 | 2025-02-10 | 350.00 | 1200 | 1400 | 25 |
| 1001 | 2025-01-05 | 200.00 | 1400 | 1400 | 14.29 |
| 1002 | 2025-02-14 | 600.00 | 600 | 1150 | 52.17 |
| 1002 | 2025-02-28 | 400.00 | 1000 | 1150 | 34.78 |
| 1002 | 2025-01-10 | 150.00 | 1150 | 1150 | 13.04 |
| 1003 | 2025-03-15 | 900.00 | 900 | 2000 | 45 |
| 1003 | 2025-01-20 | 800.00 | 1700 | 2000 | 40 |
| 1003 | 2025-02-05 | 300.00 | 2000 | 2000 | 15 |
数据库