PERCENTILE_CONT不能用于移动窗口,因其属SQL标准“分布型窗口函数”,仅支持PARTITION BY+ORDER BY静态分组插值,不支持ROWS/RANGE帧定义;PostgreSQL可用子查询+WITHIN GROUP模拟,其余数据库需ROW_NUMBER()+COUNT()手动构造窗口子集。

PERCENTILE_CONT 不能直接用于移动窗口,写 OVER (ORDER BY ts ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) 必然报错。
为什么 PERCENTILE_CONT 不支持 ROWS BETWEEN
它属于 SQL 标准定义的“分布型窗口函数”,只接受静态分组(PARTITION BY)+ 全局排序(ORDER BY),不感知物理行偏移。数据库如 PostgreSQL、SQL Server、BigQuery 都会拒绝带 ROWS 或 RANGE 的写法,并抛出类似 function does not support window framing 的错误。
这不是语法疏漏,而是设计使然:它的目标是按完整分区插值求第 p 百分位,不是为滑动子集优化的。
- 你试图算“最近 7 行的中位数”,但
PERCENTILE_CONT(0.5) OVER (...)实际上是在整个分区里找第 50% 位置的值 - 即使加了
ORDER BY ts,没ROWS就等于对全部已排序行做计算,不是滚动 - 想绕过这个限制,必须把“窗口内数据”显式构造出来,再对其聚合
PostgreSQL 中用 WITHIN GROUP 模拟移动中位数
PostgreSQL 是目前唯一能较干净实现该需求的主流数据库,靠的是子查询 + PERCENTILE_CONT(...) WITHIN GROUP (ORDER BY ...) 组合。本质是:每行触发一次子查询,动态拉取其时间窗口内的数据集,再在该集合上求中位数。
示例:按时间戳 ts 计算向前含自身共 7 行的薪资中位数
SELECT ts, salary,
(SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY s.salary)
FROM employees s
WHERE s.ts = (SELECT MIN(ts) FROM (
SELECT ts FROM employees e2
WHERE e2.ts
-
LIMIT 7控制最多取 7 行,配合MIN(ts)算出窗口左边界 -
ts字段必须有 B-tree 索引,否则子查询全表扫描,性能断崖式下跌 - 若存在并列
ts,需额外加唯一排序字段(如id)避免非确定性结果
SQL Server / BigQuery 等不支持 WITHIN GROUP 的方案
只能退回到基于 ROW_NUMBER() + COUNT() 的手动中位数模拟。核心思路是:对每个窗口子集,先编号再取中间一或两个位置的值,最后用 AVG 合并(处理偶数长度)。
以 SQL Server 为例,计算每行向前 6 行(共 7 行)的中位数:
SELECT ts, salary,
AVG(1.0 * salary) AS rolling_median_7
FROM (
SELECT e1.ts, e1.salary, e2.salary AS win_salary,
ROW_NUMBER() OVER (PARTITION BY e1.ts ORDER BY e2.salary) AS rn,
COUNT(*) OVER (PARTITION BY e1.ts) AS cnt
FROM employees e1
INNER JOIN employees e2
ON e2.ts = DATEADD(day, -6, e1.ts) -- 注意:这里用日期差替代行数,更稳定
) t
WHERE rn IN ((cnt + 1) / 2, (cnt + 2) / 2)
GROUP BY ts, salary;
- 用
INNER JOIN替代子查询,避免重复执行;但要注意笛卡尔积风险,ts上必须有索引 - 用
DATEADD(SQL Server)或INTERVAL(BigQuery)代替LIMIT,避免因排序不稳定导致窗口行数浮动 - 如果窗口宽度大(如 1000 行),这种自连接开销极高,建议预生成带序号的物化视图
真正难的不是写出来,而是让结果既正确又快——窗口越宽、数据越稀疏、时间精度越高,ts 边界判断就越容易出偏差;而任何依赖 LIMIT 或 TOP 的写法,在并行执行或数据倾斜时都可能返回非预期行。动手前先确认你的数据库版本是否真支持 WITHIN GROUP,别在 SQL Server 里硬套 PostgreSQL 的语法。










