SQL 数据分析 · 指南 · 2026年10月29日 · 11 分钟阅读

SQL 窗口函数详解:ROW_NUMBER、RANK、LAG 与累计求和

透过一扇窗框看到一排佩戴名次徽章的人,天空中飞过一架飞机

窗口函数会为每一行计算一个值,计算依据是与它相关的一组行(即「窗口」),而且不会把这些行合并成一行。GROUP BY 会把 33,121 个航班压缩成六行状态统计;COUNT(*) OVER (PARTITION BY status) 则保留全部 33,121 个航班,并在每个航班旁边写上它所属状态的航班总数。就这一个思路,可以实现排名、分组取前 N、累计求和、移动平均、占比,以及和上一行的比较。

语法永远是同一个结构:

function(...) OVER (
  PARTITION BY ...   -- 可选:每个分组重新计算
  ORDER BY ...       -- 可选:窗口内行的顺序
  ROWS/RANGE ...     -- 可选:使用当前行前后的哪些行
)

所有例子都基于 Nomto SQL 课程的航空公司数据库(104 个机场、9 种机型、2026 年 7 月至 9 月的 33,121 个航班、262,788 笔订单,金额单位为美元),使用 PostgreSQL 语法。下面每个输出都是真实结果,你可以在 Nomto 的 SQL 练习场里运行任何一个查询来核对。

OVER ():在每一行旁边放一个聚合值

最简单的窗口就是整个结果集。比较每种机型的航程和全机队平均航程:

SELECT model, range,
       ROUND(AVG(range) OVER ()) AS fleet_avg,
       range - ROUND(AVG(range) OVER ()) AS diff
FROM aircrafts
ORDER BY range DESC
LIMIT 5;
model range fleet_avg diff
Boeing 777-300 11100 5344 5756
Boeing 767-300 7900 5344 2556
Airbus A319-100 6700 5344 1356
Airbus A320-200 5700 5344 356
Airbus A321-200 5600 5344 256

不用窗口函数的话,要把 fleet_avg 放到每一行上,就得写子查询,或者和一张只有一行的表做 JOIN。

占总数的比例

窗口函数在 GROUP BY 之后执行,所以可以把聚合函数套在窗口里。SUM(COUNT(*)) OVER () 就是所有分组计数之和:

SELECT status, COUNT(*) AS flights,
       ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights pct
Arrived 16707 50.4
Scheduled 15383 46.4
On Time 518 1.6
Cancelled 414 1.2
Departed 58 0.2
Delayed 41 0.1

100.0 很重要:如果都是整数,PostgreSQL 会做整数除法,大部分占比都会变成 0。

PARTITION BY:每个分组一个独立窗口

PARTITION BY 把行分成若干组,在每组内部计算窗口,效果类似 GROUP BY,但不合并行。下面计算每种机型内部各舱位的收入占比:

SELECT a.model, tf.fare_conditions,
       SUM(tf.amount) AS revenue,
       ROUND(100.0 * SUM(tf.amount) / SUM(SUM(tf.amount)) OVER (PARTITION BY a.model), 1) AS pct_of_model
FROM ticket_flights tf
JOIN flights f ON f.flight_id = tf.flight_id
JOIN aircrafts a ON a.aircraft_code = f.aircraft_code
WHERE a.aircraft_code IN ('773', 'SU9')
GROUP BY a.model, tf.fare_conditions
ORDER BY a.model, revenue DESC;
model fare_conditions revenue pct_of_model
Boeing 777-300 Economy 22398522.00 65.3
Boeing 777-300 Business 6252364.00 18.2
Boeing 777-300 Comfort 5661169.00 16.5
Sukhoi Superjet-100 Economy 35936340.00 70.3
Sukhoi Superjet-100 Business 15208507.00 29.7

百分比在每种机型内部加起来是 100,而不是整张表加起来是 100。

ROW_NUMBER、RANK 和 DENSE_RANK 的区别

三者都按 OVER 里的 ORDER BY 给行编号,唯一的区别在于并列:

按不同目的地的数量给机场排名。有意思的地方在第十名附近,那里有五个机场并列,都是 9 个目的地:

WITH d AS (
  SELECT departure_airport, COUNT(DISTINCT arrival_airport) AS destinations
  FROM flights
  GROUP BY departure_airport
), r AS (
  SELECT departure_airport, destinations,
         ROW_NUMBER() OVER (ORDER BY destinations DESC, departure_airport) AS row_num,
         RANK()       OVER (ORDER BY destinations DESC) AS rnk,
         DENSE_RANK() OVER (ORDER BY destinations DESC) AS dense_rnk
  FROM d
)
SELECT * FROM r
WHERE row_num BETWEEN 8 AND 15
ORDER BY row_num;
departure_airport destinations row_num rnk dense_rnk
ROV 11 8 8 8
KJA 10 9 9 9
IKT 9 10 10 10
KZN 9 11 10 10
PEE 9 12 10 10
SGC 9 13 10 10
TJM 9 14 10 10
ARH 8 15 15 11

五个机场并列之后,RANK 直接跳到 15(四个名次被「占用」了),而 DENSE_RANK 接着排 11。ROW_NUMBER 里的 departure_airport 决胜列让结果可复现;没有它,IKT、KZN、PEE、SGC 和 TJM 换一次运行可能以任意顺序出现。

怎么选:

分组取前 N 名

ROW_NUMBER 搭配 PARTITION BY 的经典用法:莫斯科每个机场航班最多的两个目的地。

WITH r AS (
  SELECT departure_airport, arrival_airport, COUNT(*) AS flights,
         ROW_NUMBER() OVER (PARTITION BY departure_airport
                            ORDER BY COUNT(*) DESC, arrival_airport) AS rn
  FROM flights
  WHERE departure_airport IN ('SVO', 'DME', 'VKO')
  GROUP BY departure_airport, arrival_airport
)
SELECT departure_airport, arrival_airport, flights, rn
FROM r
WHERE rn <= 2
ORDER BY departure_airport, rn;
departure_airport arrival_airport flights rn
DME LED 244 1
DME BZK 183 2
SVO LED 305 1
SVO BZK 183 2
VKO BZK 183 1
VKO LED 183 2

有两点值得注意:

  1. 不能在 WHERE 里筛选窗口函数。 WHERE 在窗口计算之前执行,所以 WHERE ROW_NUMBER() OVER (...) <= 2 会报错。先在 CTE 或子查询里算出编号,再在外层筛选。(有些数据库提供 QUALIFY 子句来做这件事,PostgreSQL 没有。)
  2. 检查并列。 从伏努科沃(VKO)出发,BZK 和 LED 都是 183 个航班;ROW_NUMBER 把 BZK 排在前面,只是因为按字母排序的决胜列。如果并列的都应该显示,就用 RANK() ... <= 2,这样每组可能返回超过两行。

去重:每个键只留最新的一行

同样的模式可以让每个键只保留一行。下面取喀山(KZN)出发的每个航班号最近一次已到达的航班:

WITH ranked AS (
  SELECT flight_no, arrival_airport, scheduled_departure,
         ROW_NUMBER() OVER (PARTITION BY flight_no ORDER BY scheduled_departure DESC) AS rn
  FROM flights
  WHERE departure_airport = 'KZN' AND status = 'Arrived'
)
SELECT flight_no, arrival_airport, scheduled_departure
FROM ranked
WHERE rn = 1
ORDER BY flight_no
LIMIT 5;
flight_no arrival_airport scheduled_departure
PG0039 KVX 2026-08-11 17:25:00
PG0203 DME 2026-08-10 17:45:00
PG0446 MRV 2026-08-11 11:25:00
PG0498 IKT 2026-08-11 10:15:00
PG0508 LED 2026-08-11 09:45:00

累计求和

在聚合函数的 OVER 里加上 ORDER BY,它就变成累计值。8 月第一周每天的订单数及累计订单数:

WITH daily AS (
  SELECT CAST(book_date AS date) AS day, COUNT(*) AS bookings
  FROM bookings
  WHERE book_date >= '2026-08-01' AND book_date < '2026-08-08'
  GROUP BY CAST(book_date AS date)
)
SELECT day, bookings,
       SUM(bookings) OVER (ORDER BY day) AS running_total,
       LAG(bookings) OVER (ORDER BY day) AS prev_day,
       bookings - LAG(bookings) OVER (ORDER BY day) AS change
FROM daily
ORDER BY day;
day bookings running_total prev_day change
2026-08-01 5746 5746 NULL NULL
2026-08-02 5761 11507 5746 15
2026-08-03 5665 17172 5761 -96
2026-08-04 5813 22985 5665 148
2026-08-05 5937 28922 5813 124
2026-08-06 6081 35003 5937 144
2026-08-07 6184 41187 6081 103

再加上 PARTITION BY,累计值就会在每个分组(比如每个机场、每个月)重新开始。

LAG 和 LEAD:上一行与下一行

LAG(x) 返回窗口顺序中上一行的 x,LEAD(x) 返回下一行的。上表就是用 LAG 计算日环比变化。第一行没有上一行,所以结果是 NULL;LAG(bookings, 1, 0) 会改为返回 0,LAG(bookings, 7) 则往回看一周。

LEAD 回答的是「接下来会怎样」。航班 PG0579 多久飞一次,最后一班是哪天?

SELECT flight_no,
       CAST(scheduled_departure AS date) AS day,
       LEAD(CAST(scheduled_departure AS date)) OVER (ORDER BY scheduled_departure)
         - CAST(scheduled_departure AS date) AS days_to_next
FROM flights
WHERE flight_no = 'PG0579'
ORDER BY scheduled_departure DESC
LIMIT 3;
flight_no day days_to_next
PG0579 2026-09-09 NULL
PG0579 2026-09-02 7
PG0579 2026-08-26 7

这是每周一班的航班;时刻表里的最后一班没有下一班。注意,窗口里的 ORDER BY(升序)和整个查询的 ORDER BY(降序)互不影响:先计算窗口,再对输出排序。

同时比较多个对象的行时,一定要分区:LEAD(...) OVER (PARTITION BY flight_no ORDER BY scheduled_departure)。不分区的话,一个航班号的最后一班会被拿来和下一个航班号的第一班比较。

窗口框架:ROWS 和 RANGE

框架决定函数能看到当前行前后的哪些行。有两点要记住。

默认框架。 有 ORDER BY 但没写框架时,默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。RANGE 把 ORDER BY 值相同的所有行当作一步,所以并列的行会得到相同的累计值:

WITH d AS (
  SELECT departure_airport, COUNT(DISTINCT arrival_airport) AS destinations
  FROM flights
  GROUP BY departure_airport
), w AS (
  SELECT departure_airport, destinations,
         SUM(destinations) OVER (ORDER BY destinations DESC) AS range_total,
         SUM(destinations) OVER (ORDER BY destinations DESC, departure_airport
                                 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rows_total
  FROM d
)
SELECT * FROM w
WHERE destinations BETWEEN 9 AND 10
ORDER BY destinations DESC, departure_airport;
departure_airport destinations range_total rows_total
KJA 10 216 216
IKT 9 261 225
KZN 9 261 234
PEE 9 261 243
SGC 9 261 252
TJM 9 261 261

用 RANGE 时,五个并列机场一下子都跳到 261;用 ROWS 时,累计值一行一行地增长。如果你的累计值在相同时间戳上出现「台阶」,原因就在这里。想要逐行累计,就用 ROWS,并保证排序唯一。

移动平均。 像 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 这样的框架,就是一个 3 行的移动窗口:

WITH daily AS (
  SELECT CAST(book_date AS date) AS day, COUNT(*) AS bookings
  FROM bookings
  WHERE book_date >= '2026-08-01' AND book_date < '2026-08-08'
  GROUP BY CAST(book_date AS date)
)
SELECT day, bookings,
       ROUND(AVG(bookings) OVER (ORDER BY day ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 1) AS avg_3d
FROM daily
ORDER BY day;
day bookings avg_3d
2026-08-01 5746 5746
2026-08-02 5761 5753.5
2026-08-03 5665 5724
2026-08-04 5813 5746.3
2026-08-05 5937 5805
2026-08-06 6081 5943.7
2026-08-07 6184 6067.3

前两行的平均值不足三天,因为在这个窗口里 8 月 1 日之前没有数据。ROWS 数的是行,不是天:如果 daily 里缺了某一天,所谓的「3 日」平均会悄悄跨越四个日历日。可以先用日期序列补齐缺口,或者在日期列上使用 RANGE BETWEEN INTERVAL '2 days' PRECEDING AND CURRENT ROW。

窗口函数在查询执行顺序中的位置

从逻辑上说,窗口在 FROM、WHERE、GROUP BY 和 HAVING 之后计算,在 ORDER BY 和 LIMIT 之前计算。由此可以推出三条规则:

WHERE、GROUP BY 和 HAVING 之间的关系,见 GROUP BY 和 HAVING 的区别。排名和「分组取前 N」也是最常见的 SQL 面试题 之一;想动手练习,可以做 SQL 练习题 里的第 14、15 题。

常见问题

窗口函数和 GROUP BY 有什么区别?

GROUP BY 每组返回一行。窗口函数返回每一个输入行,并附加一个在一组行上计算出的值。两者可以在同一个查询里一起用:窗口作用于分组后的结果。

RANK 和 DENSE_RANK 有什么区别?

两者都给并列的行相同的编号。并列之后,RANK 会跳号(1、2、2、4),DENSE_RANK 不跳号(1、2、2、3)。在上面机场的例子里,五个机场并列第 10 名,下一个机场按 RANK 是第 15 名,按 DENSE_RANK 是第 11 名。

为什么我的累计值在好几行上都一样?

因为默认框架是 RANGE,它把 ORDER BY 值相同的行视为同一组,一次性全部计入。改为指定 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,并让排序唯一。

可以在 WHERE 里使用窗口函数吗?

不可以。先在 CTE 或子查询里计算,再在外层查询里筛选。

ROW_NUMBER 一定要写 ORDER BY 吗?

PostgreSQL 允许 ROW_NUMBER() OVER (),但这样编号是随机的。只要你希望结果可复现,就给它一个带唯一决胜列的 ORDER BY。

其他语言: Português (Brasil), Tiếng Việt, English, Español