前置:/clickhouse-sql-performance/(SQL 编写与性能优化)、/clickhouse-query-optimizer/(查询优化器)、/clickhouse-materialized-views/(物化视图)、/clickhouse-real-time-analytics/(实时分析)。
目录
- 1. 窗口函数是什么:与 GROUP BY 的分工
- 2. 窗口函数的语法:PARTITION BY、ORDER BY 与窗口框
- 3. 排名类窗口函数:ROW_NUMBER、RANK、DENSE_RANK
- 4. 聚合类窗口函数:累计、滑动与移动平均
- 5. 偏移类窗口函数:LAG、LEAD 与差值计算
- 6. 分布类窗口函数:PERCENT_RANK、CUME_DIST 与分位数
- 7. ClickHouse 窗口函数特性:默认窗口框与性能注意
- 8. 窗口函数的进阶玩法:行转列、去重、TopN
- 9. 窗口函数 vs 其他方案:物化视图、标量子查询、GROUP BY
- 10. 速查表与一句话记忆
- 延伸阅读
1. 窗口函数是什么:与 GROUP BY 的分工
先区分两个易混概念:
GROUP BY:
□ 把多行「折叠」成一行(聚合后行数变少)
□ 丢失「组内逐行」的视角——看不到每条明细
□ 例:SELECT user_id, count() FROM t GROUP BY user_id
窗口函数:
□ 不折叠行——每条明细仍在,额外算出「组内统计」
□ 在每一行上看到「所属分区的聚合/排名/偏移」
□ 例:SELECT user_id, sum(x) OVER (PARTITION BY user_id) FROM t
分工:
□ 要「一行一组的汇总」→ GROUP BY
□ 要「每条明细带上组内统计/排名」→ 窗口函数
□ 两者常配合:先窗口算出指标,再 GROUP BY 汇总
直观示例:
订单表按 user 分区、按 time 排序 →
每行新增列:
row_number(用户第几单)
running_total(累计金额)
prev_amount(上一单金额)
→ 明细不丢,统计信息加进来
工程要点:窗口函数是「在每一行上看到分区统计」——不折叠明细,是 GROUP BY 的互补(GROUP BY 折叠、窗口函数保留明细加统计)。判断用哪个:要明细行 → 窗口函数;要一组的行 → GROUP BY。两者可嵌套配合。
2. 窗口函数的语法:PARTITION BY、ORDER BY 与窗口框
语法骨架是三件套 + 可选窗口框:
<函数> OVER (
PARTITION BY 分区列 -- 分组(可选)
ORDER BY 排序列 -- 组内排序(可选但常需)
ROWS/RANGE 窗口框 -- 滑动范围(可选)
)
三件套含义:
□ PARTITION BY:分区——函数在「每个分区内」独立计算
(不写 = 全表一个分区)
□ ORDER BY:组内排序——排名/累计/偏移都依赖它
(不写 = 无固定顺序,聚合型可用)
□ 窗口框(frame):「当前行」往前后看多少行
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 前 2 行到当前
RANGE ...(按值范围)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 累计到当前
窗口框的粒度:
□ ROWS:按「物理行数」定界(前 N 行)
□ RANGE:按「排序键值差」定界(值范围内所有行)
-- 累计金额(从分区头到当前行)
sum(amount) OVER (
PARTITION BY user_id
ORDER BY event_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
-- 前 2 行的移动平均
avg(amount) OVER (
PARTITION BY user_id
ORDER BY event_time
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)
工程要点:窗口函数语法是「函数 + OVER(分区 + 排序 + 窗口框)」——PARTITION BY 定范围、ORDER BY 定顺序、窗口框定「当前行往哪看」。窗口框是「滑动计算」的关键:不写框架,不同数据库默认不同(ClickHouse 默认整分区——见第 7 节),写滑动平均/累计必须显式指定框。
3. 排名类窗口函数:ROW_NUMBER、RANK、DENSE_RANK
三个排名函数的区别在「并列怎么排」:
排名函数对比(值: 10, 20, 20, 30):
□ ROW_NUMBER:物理序号,并列也分先后 → 1, 2, 3, 4
□ RANK:并列同号,下一个跳过 → 1, 2, 2, 4
□ DENSE_RANK:并列同号,下一个不跳 → 1, 2, 2, 3
选择:
□ 只要唯一序号(分页/去重)→ ROW_NUMBER
□ 竞赛排名(并列占位)→ RANK
□ 要「连续的名次数字」→ DENSE_RANK
典型场景:
□ 取每组 TopN(配合 ROW_NUMBER 过滤)
□ 去重(ROW_NUMBER = 1 保留每组最新)
□ 排行榜(RANK/DENSE_RANK)
-- 每组最新一条(ROW_NUMBER 去重)
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY event_time DESC
) AS rn
FROM events
) WHERE rn = 1
-- 每日金额排名(DENSE_RANK)
SELECT day, user_id, amount,
DENSE_RANK() OVER (PARTITION BY day ORDER BY amount DESC) AS dr
FROM daily_stats
工程要点:三个排名函数本质是「并列处理策略」——ROW_NUMBER 唯一序号、RANK 并列跳号、DENSE_RANK 并列不跳。工程上最常用 ROW_NUMBER(去重、TopN、分页都需要唯一序号);排行榜用 DENSE_RANK 拿连续名次。TopN/去重是「子查询 + 窗口过滤」的标准组合。
4. 聚合类窗口函数:累计、滑动与移动平均
聚合函数(sum/avg/count/min/max)放进 OVER 就是「滑动聚合」:
滑动聚合的三种形态:
□ 累计(Running):从分区头到当前行累加
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
□ 滑动(Sliding):固定窗口随行滚动(前 N 行到当前行)
ROWS BETWEEN N PRECEDING AND CURRENT ROW
□ 分区统计:整个分区一个值(每行相同)
(不写窗口框时 ClickHouse 默认整分区)
典型场景:
□ 累计销售额 / 余额流水(累计)
□ 移动平均(MA5/MA10 股票、销量平滑)(滑动)
□ 同比环比差值(配合 LAG,见第 5 节)
□ 每行占比(amount / sum(amount) OVER (PARTITION BY ...))
-- 移动平均 MA3(近 3 点平滑)
avg(price) OVER (
PARTITION BY symbol
ORDER BY ts
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS ma3
-- 每行占分区百分比
amount / sum(amount) OVER (PARTITION BY day) * 100 AS pct
工程要点:聚合窗口函数是「把聚合放进 OVER 让它逐行滑动」——**累计(头到当前)、滑动(前 N 到当前)、占比(分区合计)**三个模式覆盖大多数场景。注意:不写窗口框时 ClickHouse 默认整个分区(与其他数据库不同),滑动/累计必须显式写 ROWS 框。
5. 偏移类窗口函数:LAG、LEAD 与差值计算
LAG/LEAD 访问「相邻行的值」,是算差值/同比环比的主力:
偏移函数:
□ LAG(col, n):取「往前 n 行」的值(n 默认 1)
□ LEAD(col, n):取「往后 n 行」的值
□ 配合 ORDER BY:按排序后的行序偏移
□ 可选 default:越界时返回(默认 NULL)
典型场景:
□ 环比:当前值 - 上一行值(LAG 差值)
□ 同比:今年 - 去年同窗口(跨分区偏移)
□ 会话时长:当前事件时间 - 上一事件时间
□ 涨跌标记:price - LAG(price) 正负
-- 每行对比上一行
event_time - LAG(event_time) OVER (
PARTITION BY user_id ORDER BY event_time
) AS gap_seconds
-- 日环比增量
amount - LAG(amount) OVER (
PARTITION BY store_id ORDER BY day
) AS day_over_day
-- 越界给默认值
LAG(price, 2, 0) OVER (ORDER BY day) AS price_2_ago
工程要点:LAG/LEAD 是「相邻行取值」的窗口函数——环比差值、事件间隔、涨跌判断全靠它。核心是「偏移量 n + 越界默认值」,配合 ORDER BY 才有意义。注意区分:LAG 是「往前看」(历史),LEAD 是「往后看」(未来);同比/环比常在「分区对齐」后再偏移。
6. 分布类窗口函数:PERCENT_RANK、CUME_DIST 与分位数
分布类函数把「行在分区中的相对位置」变成数值:
分布函数:
□ PERCENT_RANK:当前行排名百分比
(RANK-1) / (行数-1),范围 [0,1]
□ CUME_DIST:累计分布(当前行<=值的比例)
值小于等于当前的行数 / 总行数
□ 分位数近似:ClickHouse 用 quantile* 聚合做分位
(窗口内分位:quantile(0.95)(x) OVER (...))
典型场景:
□ 用户分层:按金额排百分位 → 高/中/低频用户
□ 异常检测:值落在分位分布的两端 → 可疑
□ 性能监控:P95/P99 延迟(窗口内 quantile)
配合使用:
□ PERCENT_RANK → 「这行排前百分之几」
□ CUME_DIST → 「多少比例的行 <= 这行」
□ 两者常与排名函数搭配看「分布形状」
-- 用户金额百分位分层
SELECT user_id, amount,
PERCENT_RANK() OVER (ORDER BY amount DESC) AS pct_rank,
CASE WHEN PERCENT_RANK() OVER (ORDER BY amount DESC) < 0.2
THEN 'high' ELSE 'low' END AS tier
FROM user_stats
-- 窗口内 P95 延迟
quantile(0.95)(latency) OVER (
PARTITION BY service ORDER BY ts
ROWS BETWEEN 100 PRECEDING AND CURRENT ROW
) AS p95
工程要点:分布函数回答「这行在分布里排多靠前」——PERCENT_RANK 是排名百分比、CUME_DIST 是累计覆盖比例。工程上更实用的是「窗口内分位数」(quantile OVER)——滚动 P95/P99 监控、用户分层、异常检测的通用手段。
7. ClickHouse 窗口函数特性:默认窗口框与性能注意
ClickHouse 的窗口函数实现有几个「与教科书不同」的点:
ClickHouse 默认窗口框:
□ 不写窗口框时 = 整个分区(UNBOUNDED PRECEDING 到 UNBOUNDED FOLLOWING)
→ 和 PostgreSQL/MySQL 默认(到当前行)不同!
□ 影响:不写框的 sum(x) OVER (PARTITION BY ...) = 分区总合计
→ 想要「累计到当前行」必须显式写 ROWS 框
性能注意:
□ 窗口函数需要「分区内排序」→ 消耗内存与排序
□ 大数据集:PARTITION BY 列应尽量「低基数」
(高基数分区 → 大量小分区 → 排序碎片化)
□ 内存:极端情况下用 external 排序(disk)保稳定
□ 尽量在「已按主键排序」的列上 ORDER BY → 少一次排序
版本支持:
□ ClickHouse 窗口函数从 21.x 起功能完善
□ 早期版本缺某些框语法 → 确认版本能力
□ 与物化视图/预聚合结合:能下推则少算
-- 正确区分两种"合计"
sum(amount) OVER (PARTITION BY user_id) -- 分区总合计
sum(amount) OVER (PARTITION BY user_id
ORDER BY event_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) -- 累计到当前
工程要点:ClickHouse 窗口函数的两大坑是**「默认窗口框 = 整分区」和「排序开销」**——前者让「累计」必须显式写 ROWS 框,后者要求「低基数分区 + 尽量复用主键序」。写窗口查询时:先确认框语义,再看排序代价。高基数 PARTITION BY 是性能杀手。
8. 窗口函数的进阶玩法:行转列、去重、TopN
窗口函数不只是排名——它是「分组内任意逐行计算」的瑞士军刀:
进阶玩法:
□ 去重取最新:ROW_NUMBER + 子查询过滤 rn=1
→ 每组保留最新/最新状态(比 GROUP BY max 更灵活)
□ TopN 每组:ROW_NUMBER 排序 + 过滤 n 内
→ 每组 Top 3 商品 / 每部门 Top 员工
□ 行转列(透视):先窗口算每组的列值,再交叉表
→ 配合 CASE + GROUP BY 或数据透视
□ 保留明细同时聚合:窗口聚合 + 明细字段并存的报表
□ 复杂比例:分区内占比、与平均的差、距极值的偏移
组合技巧:
□ 窗口函数结果做「二次过滤」→ 必须子查询包一层
□ 多个窗口函数共享 OVER → 写一次复用
□ 与数组函数/字典结合 → 更复杂的分组逻辑
-- 每商品 Top3 的日期(TopN)
SELECT * FROM (
SELECT product, day, sales,
ROW_NUMBER() OVER (PARTITION BY product ORDER BY sales DESC) AS rn
FROM daily_sales
) WHERE rn <= 3
-- 行转列:每天各渠道占比透视(示意)
SELECT day,
max(CASE WHEN channel='web' THEN amount ELSE 0 END) AS web,
max(CASE WHEN channel='app' THEN amount ELSE 0 END) AS app
FROM (
SELECT day, channel, sum(amount) OVER (PARTITION BY day, channel) AS amount
FROM events
) GROUP BY day
工程要点:窗口函数进阶的核心是「分组内逐行计算 + 子查询二次加工」——去重最新(ROW_NUMBER)、每组 TopN、行转列透视、明细与聚合并存。记住铁律:窗口结果想「过滤」必须包子查询(SQL 不允许在同一层 WHERE 用窗口别名);多个窗口共用 OVER 可复用。
9. 窗口函数 vs 其他方案:物化视图、标量子查询、GROUP BY
窗口函数不是唯一答案——选型要看「计算频率与数据量」:
方案对比:
□ 窗口函数:查询时动态算,灵活,但每次查询重算排序
□ 物化视图:预聚合存结果,查询快,但改查询要重建
→ 固定口径的高频报表 → 物化视图
→ 灵活/临时分析 → 窗口函数
□ GROUP BY + 子查询:先聚合再 join 回明细
→ 内存省,但 SQL 复杂、多表 join
□ 标量子查询:简单单值,性能差(逐行执行)
→ 别在大表用
选型决策:
□ 一次性/临时分析 → 窗口函数(灵活)
□ 固定报表高频查 → 物化视图(预计算)
□ 只要聚合不要明细 → GROUP BY(最省)
□ 明细 + 聚合并存 → 窗口函数(或聚合后 join)
性能考量:
□ 窗口函数 = 每次查询「分区 + 排序」
□ 数据大、查询频繁 → 预聚合(物化视图)更划算
□ 两者结合:物化视图存「分区预聚合」,窗口函数做「组内精细」
决策流:
需要明细行?
是 → 高频固定?→ 物化视图 + 窗口
→ 临时分析?→ 窗口函数
否 → GROUP BY 聚合(最省)
工程要点:窗口函数 vs 物化视图是「灵活 vs 预计算」的权衡——临时分析/灵活口径用窗口函数(每次重算),固定高频报表用物化视图(预聚合)。工程实践常两者结合:物化视图粗粒度预聚合,窗口函数做组内精细计算。大表高频查询,别让窗口函数每次都重新排序。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 是什么 | 分组内逐行计算,不折叠明细 |
| 语法 | 函数 OVER(分区 + 排序 + 窗口框) |
| 排名 | ROW_NUMBER 唯一、RANK 并列跳号、DENSE_RANK 并列不跳 |
| 聚合窗口 | 累计(头到当前)/滑动(前 N 到当前)/分区占比 |
| 偏移 | LAG 往前、LEAD 往后,配合 ORDER BY |
| 分布 | PERCENT_RANK 排名百分比、CUME_DIST 累计覆盖、quantile 分位 |
| ClickHouse 坑 | 默认窗口框 = 整分区,累计要显式 ROWS 框 |
| 性能 | 低基数分区、复用主键序、避免高基数 PARTITION BY |
| 进阶 | 去重最新、TopN、行转列都靠子查询二次加工 |
| 选型 | 临时分析用窗口、固定报表用物化视图 |
一句话记忆:窗口函数 = OVER(分区 + 排序 + 窗口框) 在每行算组内统计——排名三兄弟(ROW_NUMBER/RANK/DENSE_RANK 区别在并列)、滑动聚合(累计/滑动/占比)、偏移(LAG/LEAD 环比间隔)、分布(PERCENT_RANK/quantile 分位)——记住 ClickHouse 默认框是整分区、要累计必须写 ROWS 框、过滤窗口结果必须包子查询、固定报表用物化视图。
延伸阅读
- /clickhouse-sql-performance/ — SQL 编写与性能优化
- /clickhouse-query-optimizer/ — 查询优化器原理
- /clickhouse-materialized-views/ — 物化视图与预聚合
- /clickhouse-real-time-analytics/ — 实时分析实践
- /clickhouse-merge-tree-principle/ — MergeTree 存储原理
- 数据工程专题 — 数据处理与分析
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。