mysql 8.0+ 中 avg() 配合 over() 是计算移动平均的唯一合理方案,需用 partition by 分组、order by 排序及 rows between 2 preceding and current row 精确控制窗口范围,mysql 不支持 range 时间偏移,故“过去7天”需额外处理。

MySQL 8.0+ 用 AVG() 配合 OVER() 窗口函数最直接
MySQL 5.7 及更早版本不支持窗口函数,强行用子查询或自连接算移动平均容易超时或出错;8.0+ 版本里,AVG() + OVER() 是唯一合理选择。关键不是“能不能算”,而是“怎么定义窗口范围”。
比如按时间排序、对每个用户计算最近 3 条记录的销售额移动平均:
SELECT
user_id,
sale_date,
amount,
AVG(amount) OVER (
PARTITION BY user_id
ORDER BY sale_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3
FROM sales;
-
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW表示包含当前行和它前面 2 行(共 3 行),不是“过去 3 天” -
PARTITION BY user_id必须加,否则不同用户的记录会混在一起算 - 如果
sale_date有重复,ORDER BY里要加唯一字段(如id)避免非确定性排序
PostgreSQL 和 SQL Server 的语法几乎一致,但要注意 ROWS 和 RANGE 的区别
ROWS 按物理行数截取,RANGE 按排序值范围截取——这是最容易写错的地方。例如用 RANGE BETWEEN INTERVAL '2 days' PRECEDING AND CURRENT ROW 在 PostgreSQL 中可行,但在 MySQL 8.0+ 不支持 RANGE 配时间间隔。
- 想算“过去 7 天内”的移动平均,MySQL 必须先生成日期序列或用自连接,不能靠
RANGE - PostgreSQL 支持
RANGE+INTERVAL,但性能通常比ROWS差,尤其数据量大时 - SQL Server 2012+ 支持
ROWS,但不支持RANGE时间偏移,和 MySQL 一样受限
SQLite 不原生支持窗口函数,得用相关子查询硬扛
SQLite 3.25+ 虽然支持窗口函数,但很多生产环境仍在用旧版本(如 Android 系统自带 SQLite)。此时只能用相关子查询,但必须小心性能和边界行为:
SELECT
t1.user_id,
t1.sale_date,
t1.amount,
(SELECT AVG(t2.amount)
FROM sales t2
WHERE t2.user_id = t1.user_id
AND t2.sale_date = (
SELECT MAX(t3.sale_date)
FROM sales t3
WHERE t3.user_id = t1.user_id
AND t3.sale_date
- 这个写法在 SQLite 中能跑,但每行都触发两次子查询,10 万行数据可能秒变秒级延迟
-
LIMIT 1 OFFSET 2是为了找“往前数第 3 天”,但若某用户数据稀疏(比如隔周才一条),结果会错 - 没有
PARTITION BY的等价机制,WHERE条件必须手动对齐分组字段
NULL 值和首尾几行的处理逻辑必须显式确认
所有数据库对窗口开头不足 N 行的情况,都默认只算实际存在的行(即前两行的移动平均是 1 行或 2 行的均值),不会补 NULL 或 0。但如果你业务要求“不满 3 行就返回 NULL”,就得自己加判断:
- MySQL 可用
COUNT(*) OVER(...)配合CASE过滤:CASE WHEN COUNT(*) OVER(...) - 首行的移动平均值等于它自己,这不是 bug,是定义如此;但报表里常被误认为异常,需提前和业务方对齐
- 如果原始数据本身含
NULL的amount,AVG()会自动忽略它们——这点和聚合函数一致,但容易被忽略
移动平均本身不难,难的是窗口定义是否贴合业务场景、数据库版本是否兜底、以及 NULL 和边界值是否符合预期。别光看结果数字对不对,先盯住那行 ROWS BETWEEN ... 有没有写错。










