子查询实现移动平均的核心是相关子查询:对当前行,用where关联外层时间或序号,查出向前n行(含自身)的记录再avg()聚合;必须确保排序字段有序无重复,否则窗口错位,且性能为o(n²),远低于窗口函数。

子查询实现移动平均线的核心逻辑
SQL里没有内置的移动平均函数,必须靠子查询或窗口函数模拟滑动窗口。用子查询实现的关键是:对当前行,查出它往前N行(含自身)的记录,再用AVG()聚合。但要注意——子查询必须关联到外层行的时间/序号基准,否则算出来的是全量均值,不是“移动”的。
常见错误现象:SELECT price, (SELECT AVG(price) FROM stocks s2) FROM stocks s1 这种写法返回每行都是整个表的平均值,完全没滑动。
- 必须用相关子查询(correlated subquery),即子查询里引用外层表别名(如
s1.date) - 时间字段必须严格有序且无重复,否则
WHERE date BETWEEN ...会漏数或多算 - 若用序号(如
id),需确保id连续且递增;不连续时要用ROW_NUMBER()先生成逻辑序号
用日期范围实现3日移动平均(MySQL/PostgreSQL通用)
适用于有明确date字段、每日一条记录的场景。假设表叫stock_prices,字段为date和close_price:
SELECT date, close_price, (SELECT AVG(close_price) FROM stock_prices s2 WHERE s2.date BETWEEN s1.date - INTERVAL '2 days' AND s1.date) AS ma3 FROM stock_prices s1 ORDER BY date;
注意点:
-
INTERVAL '2 days'在PostgreSQL中有效;MySQL要写成DATE_SUB(s1.date, INTERVAL 2 DAY) - 如果某天缺数据(比如周末休市),这个写法会自动跳过,导致实际窗口不足3天——这不是bug,是预期行为
- 性能差:每行都触发一次子查询,数据量大时明显变慢;超过1万行建议改用窗口函数
用行号模拟窗口(兼容SQL Server 2005+、MySQL 8.0前)
当时间不连续、或需要严格按交易顺序(而非日历)计算时,得先生成序列号。以MySQL 5.7为例:
SELECT
date,
close_price,
(SELECT AVG(close_price)
FROM (
SELECT close_price, @row := @row + 1 AS rn
FROM stock_prices, (SELECT @row := 0) r
ORDER BY date
) t2
WHERE t2.rn BETWEEN t1.rn - 2 AND t1.rn) AS ma3
FROM (
SELECT date, close_price, @row2 := @row2 + 1 AS rn
FROM stock_prices, (SELECT @row2 := 0) r
ORDER BY date
) t1
ORDER BY date;
关键细节:
- 两个变量
@row和@row2必须分开初始化,否则嵌套子查询会污染变量状态 - 子查询里的
ORDER BY不能省——MySQL 5.x中子查询ORDER BY无效,但这里用于变量赋值,必须保留 - 这种写法在高并发写入场景下不稳定,仅适合离线计算
为什么现在更推荐窗口函数而不是子查询
不是子查询写得不对,而是它本质是O(n²)复杂度:10万行就要执行10万次子查询扫描。而AVG() OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)是单趟扫描,性能差距可达百倍。
如果你的数据库支持窗口函数(PostgreSQL 8.4+、SQL Server 2005+、MySQL 8.0+、Oracle 8i+),就别硬扛子查询。子查询方案真正有用的场景只剩两个:老版本数据库无法升级,或者需要在不支持窗口函数的嵌入式SQL引擎(比如某些BI工具的自定义SQL框)里临时凑合。
最易被忽略的一点:子查询移动平均默认不处理NULL值——只要窗口内任意close_price为NULL,整条AVG()结果就是NULL。而窗口函数同样如此,得提前用COALESCE()填充,否则图表会断掉。










