4. 窗口函数入门:ROW_NUMBER、RANK、LAG、LEAD 在时间序列分析中的使用
在组合风险绩效归因中,时间序列数据是最常见的分析对象——每日收益率、滚动波动率、持仓权重变化、因子暴露漂移等。要高效处理这类数据,SQL 窗口函数(Window Functions)是必不可少的工具。与普通聚合函数(GROUP BY)不同,窗口函数不会压缩行数,而是为每一行保留原始记录的同时,计算基于指定窗口的聚合、排序或偏移值。
本章聚焦四个最常用的窗口函数:ROW_NUMBER、RANK、LAG、LEAD,并结合组合绩效归因的实际场景,演示它们在时间序列分析中的具体用法。
4.1 窗口函数核心语法
所有窗口函数都遵循以下基本语法结构:
<窗口函数> OVER (
[PARTITION BY 分区列]
[ORDER BY 排序列 [ASC|DESC]]
[ROWS/RANGE 窗口帧子句]
)
| 子句 | 作用 | 在时间序列中的典型用法 |
|---|---|---|
| PARTITION BY | 将数据分组,窗口函数在每个组内独立计算 | 按资产代码、组合ID、因子类别分组 |
| ORDER BY | 定义组内行的排序顺序 | 按交易日、报告期、计算日期排序 |
| ROWS/RANGE | 限定窗口帧的范围(默认从分区第一行到当前行) | 滚动窗口:ROWS BETWEEN 21 PRECEDING AND CURRENT ROW |
关键区别: 窗口函数在
SELECT 阶段执行,位于 WHERE 和 GROUP BY 之后,ORDER BY 之前。这意味着你可以在同一查询中同时使用聚合和窗口计算。
4.2 ROW_NUMBER 与 RANK:排序与去重
ROW_NUMBER() 为分区内的每一行分配一个唯一的连续整数,从 1 开始,即使排序值相同也不会重复。而 RANK() 在遇到相同排序值时,会分配相同的排名,并在后续排名中跳过相应数量。
应用场景: 在组合绩效归因中,经常需要找出每个资产在特定时间窗口内的最大回撤日、最高收益率日,或者剔除重复的持仓快照记录。
-- 示例:找出每个资产在2024年Q1内收益率最高的前3个交易日
WITH ranked_returns AS (
SELECT
trade_date,
asset_code,
daily_return,
ROW_NUMBER() OVER (
PARTITION BY asset_code
ORDER BY daily_return DESC
) AS rn
FROM daily_returns
WHERE trade_date BETWEEN '2024-01-01' AND '2024-03-31'
)
SELECT trade_date, asset_code, daily_return
FROM ranked_returns
WHERE rn <= 3
ORDER BY asset_code, rn;
| 函数 | 相同排序值处理 | 后续排名 | 典型用途 |
|---|---|---|---|
| ROW_NUMBER() | 强制分配不同序号(按物理顺序) | 连续递增 | 去重、Top-N 分析、分页 |
| RANK() | 分配相同排名 | 跳过重复数量 | 排名竞赛、百分位分组 |
| DENSE_RANK() | 分配相同排名 | 不跳过,连续递增 | 密集排名、等级划分 |
去重陷阱: 使用
ROW_NUMBER() 去重时,如果 ORDER BY 子句不能唯一确定行的顺序,多次运行可能得到不同的结果。建议在 ORDER BY 中加入足够多的排序列(如 trade_date, asset_code, record_id)来保证确定性。
4.3 LAG 与 LEAD:访问前后行数据
LAG(column, offset, default) 返回分区内当前行之前第 offset 行的值;LEAD(column, offset, default) 返回之后第 offset 行的值。这是时间序列分析中最强大的工具之一,用于计算收益率差分、滚动变化量、同比/环比对比等。
核心应用: 计算日收益率、资产权重变化、因子暴露漂移、风险指标(如 VaR)的逐日变化。
-- 示例:计算每个资产的日收益率(基于复权净值)
SELECT
trade_date,
asset_code,
nav,
LAG(nav) OVER (
PARTITION BY asset_code
ORDER BY trade_date
) AS prev_nav,
(nav / LAG(nav) OVER (
PARTITION BY asset_code
ORDER BY trade_date
) - 1) AS daily_return
FROM asset_nav
WHERE trade_date BETWEEN '2024-01-01' AND '2024-06-30'
ORDER BY asset_code, trade_date;
| 函数 | 方向 | 偏移量参数 | 默认值参数 | 在归因中的典型用途 |
|---|---|---|---|---|
| LAG(expr, n, default) | 向前(历史方向) | n 行之前(n≥1) | 当超出分区边界时返回 default | 计算收益率、权重变化、风险指标差分 |
| LEAD(expr, n, default) | 向后(未来方向) | n 行之后(n≥1) | 当超出分区边界时返回 default | 预测验证、未来收益率对齐、事件后分析 |
实战技巧: 在计算滚动指标(如 21 日滚动波动率)时,可以先用
LAG 计算日收益率,再在外层查询中用 AVG() OVER (ROWS BETWEEN 20 PRECEDING AND CURRENT ROW) 计算滚动标准差。两步窗口函数嵌套是绩效归因中的高频操作。
4.4 窗口函数在时间序列分析中的综合应用
下面通过一个完整的组合风险绩效归因场景,展示四个窗口函数的协同使用:
-- 综合示例:组合风险绩效归因时间序列分析
WITH daily_metrics AS (
SELECT
trade_date,
portfolio_id,
daily_return,
-- 计算累计收益率(从组合成立日算起)
SUM(1 + daily_return) OVER (
PARTITION BY portfolio_id
ORDER BY trade_date
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) - 1 AS cumulative_return,
-- 计算日收益率排名(用于识别极端收益日)
RANK() OVER (
PARTITION BY portfolio_id
ORDER BY daily_return DESC
) AS rank_best,
RANK() OVER (
PARTITION BY portfolio_id
ORDER BY daily_return ASC
) AS rank_worst,
-- 计算前一日收益率(用于自相关分析)
LAG(daily_return, 1) OVER (
PARTITION BY portfolio_id
ORDER BY trade_date
) AS prev_return,
-- 计算后一日收益率(用于验证动量策略)
LEAD(daily_return, 1) OVER (
PARTITION BY portfolio_id
ORDER BY trade_date
) AS next_return
FROM portfolio_returns
WHERE trade_date BETWEEN '2024-01-01' AND '2024-12-31'
)
SELECT
trade_date,
portfolio_id,
ROUND(daily_return * 100, 4) AS daily_return_pct,
ROUND(cumulative_return * 100, 2) AS cumulative_return_pct,
CASE
WHEN rank_best <= 5 THEN 'TOP5 最佳日'
WHEN rank_worst <= 5 THEN 'TOP5 最差日'
ELSE '普通日'
END AS day_type,
ROUND(prev_return * 100, 4) AS prev_return_pct,
ROUND(next_return * 100, 4) AS next_return_pct
FROM daily_metrics
WHERE rank_best <= 5 OR rank_worst <= 5
ORDER BY portfolio_id, trade_date;
4.5 窗口函数性能优化要点
在大型时间序列数据集(如全市场日频数据、多年历史回测数据)上使用窗口函数时,性能是关键考量:
- 分区键选择:
PARTITION BY列应具有高基数且均匀分布,避免数据倾斜导致单个分区过大。 - 排序索引: 为
PARTITION BY和ORDER BY列建立复合索引,可大幅提升窗口函数执行效率。 - 避免不必要的窗口帧: 默认窗口帧(
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)在ORDER BY列有重复值时可能产生意外结果,建议显式指定ROWS帧。 - 嵌套窗口函数: 多层窗口函数嵌套时,考虑使用 CTE 或子查询分层计算,提高可读性和执行计划的可预测性。
4.6 常见错误与避坑指南
- 忽略 PARTITION BY: 如果不指定
PARTITION BY,窗口函数会在整个结果集上计算,这在多资产组合中会导致跨资产的错误排序和偏移。 - ORDER BY 列重复: 当
ORDER BY列存在重复值时,ROW_NUMBER()的行为可能不确定,建议添加唯一排序列(如交易时间戳、记录ID)。 - LAG/LEAD 默认值: 当偏移量超出分区边界时,
LAG和LEAD返回NULL。在计算收益率时,第一行的prev_nav为NULL,需要用COALESCE或IS NOT NULL过滤。 - 窗口帧与 ORDER BY 的交互: 默认窗口帧在
ORDER BY存在时是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这在有重复排序值时可能导致意外的行包含。显式使用ROWS帧可以避免歧义。
最佳实践: 在编写窗口函数查询时,始终先在小样本数据上验证结果。使用
SELECT * 临时查看中间结果,确认 PARTITION BY 和 ORDER BY 的组合产生了预期的分区和排序顺序,再逐步添加窗口计算列。