每个月做报表,老板都要问一句:"这个月比上个月涨了多少?"
我最早的做法是写两个查询,一个算这个月,一个算上个月,再把两个结果拼到一起。后来学聪明点,用自连接把月份错开一位再 JOIN 回来------SQL 能跑,但每次写都要想半天连接条件。
其实 MySQL 里有个函数专门干这件事:LAG,把"上一行"的值直接搬到当前行来。
一行搞定,不用自连接,不用子查询。
一、本篇用到的练习表
和本系列每篇一样,先建库建表。整段可以直接复制运行,重复执行也不会报"表已存在"。
sql
-- SQL 系列统一练习表(复制即可运行,可重复执行)
CREATE DATABASE IF NOT EXISTS sql_practice DEFAULT CHARACTER SET utf8mb4;
USE sql_practice;
DROP TABLE IF EXISTS 员工表;
CREATE TABLE 员工表 (
员工id INT PRIMARY KEY,
姓名 VARCHAR(20),
部门 VARCHAR(20),
工资 DECIMAL(10,2),
邮箱 VARCHAR(50),
手机 VARCHAR(20),
入职日期 DATE
);
INSERT INTO 员工表 (员工id, 姓名, 部门, 工资, 邮箱, 手机, 入职日期) VALUES
(1, '张三', '技术部', 9000.00, 'zhangsan@demo.com', '13800000001', '2019-03-01'),
(2, '李四', '技术部', 9000.00, 'lisi@demo.com', '13800000002', '2020-07-15'),
(3, '王五', '销售部', 9200.00, 'wangwu@demo.com', '13800000003', '2018-01-10'),
(4, '赵六', '销售部', 9200.00, 'zhaoliu@demo.com', '13800000004', '2021-05-20');
DROP TABLE IF EXISTS 订单表;
CREATE TABLE 订单表 (
订单id INT PRIMARY KEY,
员工id INT,
订单金额 DECIMAL(10,2),
下单时间 DATETIME,
付款时间 DATETIME,
状态 VARCHAR(20)
);
INSERT INTO 订单表 (订单id, 员工id, 订单金额, 下单时间, 付款时间, 状态) VALUES
(101, 1, 3000.00, '2026-01-10 10:00:00', '2026-01-10 10:05:00', '已付款'),
(102, 1, 2500.00, '2026-01-15 14:00:00', '2026-01-15 14:10:00', '已付款'),
(103, 3, 4000.00, '2026-01-20 09:30:00', '2026-01-20 09:40:00', '已付款'),
(104, 2, 1500.00, '2026-01-25 16:00:00', NULL, '待付款'),
(105, 3, 6000.00, '2026-02-01 11:00:00', '2026-02-01 11:20:00', '已付款'),
(106, 1, 1200.00, '2026-02-05 15:00:00', '2026-02-05 15:10:00', '已付款'),
(107, 2, 8000.00, '2026-02-10 10:30:00', '2026-02-10 10:45:00', '已付款'),
(108, 3, 2200.00, '2026-02-15 17:00:00', '2026-02-15 17:30:00', '已付款'),
(109, 1, 5500.00, '2026-02-20 09:00:00', '2026-02-20 09:15:00', '已付款'),
(110, 2, 3300.00, '2026-02-25 14:20:00', '2026-02-25 14:40:00', '已付款');
表里是 2026 年 1 月和 2 月的订单,10 条。后面所有例子都跑在这张表上。

二、LAG 是干嘛的
2.0 之前我是怎么写的:自连接
在没有 LAG 的时候,要跟上个月比,最常见的写法是把月份错开一位,自己连自己:
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT a.月份, a.销售额 本月, b.销售额 上月
FROM 月度 a
LEFT JOIN 月度 b ON b.月份 = DATE_FORMAT(DATE_SUB(
STR_TO_DATE(CONCAT(a.月份, '-01'), '%Y-%m-%d'), INTERVAL 1 MONTH), '%Y-%m');
能跑,结果也对。但你得先算"上一个月是几月",再拿这个月份去连自己------月份要是断了(比如 2 月没有数据),LEFT JOIN 出来就是 NULL,还得再处理一次。
换成 LAG,同样的结果:
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT 月份, 销售额 本月, LAG(销售额) OVER (ORDER BY 月份) 上月
FROM 月度 ORDER BY 月份;
两种方式对比一下:
| 自连接 | LAG | |
|---|---|---|
| 代码量 | 要先算上一个月,还要写连接条件 | 一行 |
| 月份断档 | 断档那个月连出来是 NULL | 取的是"上一行",跟月份连不连续无关 |
| 按部门各比各的 | 连接条件里要带上部门 | 加一个 PARTITION BY |
| 读起来 | 得反过来想"谁连谁" | 直说"取上一次" |
"取上一行"是位置关系,不是值的关系。 自连接是在值上做匹配(找上一个月是哪个月),LAG 是在排好队的行上按位置取(往前挪一行)。月份断不断、编号连不连续,跟 LAG 没关系。
2.1 先把月度数据算出来
要比"这个月 vs 上个月",得先有一份按月汇总的数据:
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT * FROM 月度 ORDER BY 月份;
| 月份 | 销售额 |
|---|---|
| 2026-01 | 11000.00 |
| 2026-02 | 26200.00 |
2.2 一行取出上一次
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT 月份, 销售额,
LAG(销售额) OVER (ORDER BY 月份) 上月销售额
FROM 月度 ORDER BY 月份;
| 月份 | 销售额 | 上月销售额 |
|---|---|---|
| 2026-01 | 11000.00 | NULL |
| 2026-02 | 26200.00 | 11000.00 |
LAG(销售额) 的意思是"把上一行的销售额搬到这一行来"。第一行前面没有行,所以是 NULL。
一个字都没改表结构,没写自连接,一行就比出来了。
2.3 首行那个 NULL 怎么处理
LAG 其实有三个参数,平时只用第一个:
sql
-- LAG 三个参数的写法(这不是能执行的语句,是参数说明)
-- LAG(列名, 往前第几行, 取不到时给什么)
所以想把首行的 NULL 变成 0,写第二个和第三个参数就行:
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT 月份, 销售额,
LAG(销售额, 1, 0) OVER (ORDER BY 月份) 上月销售额
FROM 月度 ORDER BY 月份;
| 月份 | 销售额 | 上月销售额 |
|---|---|---|
| 2026-01 | 11000.00 | 0.00 |
| 2026-02 | 26200.00 | 11000.00 |
第二个参数 1 是"往前一行",改成 2 就是"往前两行"。第三个参数是取不到时填的默认值,不写就是 NULL。
三个参数放在一起看:
| 写法 | 意思 | 首行结果 |
|---|---|---|
LAG(销售额) |
往前 1 行,取不到给 NULL | NULL |
LAG(销售额, 1) |
同上,1 是默认值 | NULL |
LAG(销售额, 1, 0) |
往前 1 行,取不到给 0 | 0 |
LAG(销售额, 2) |
往前 2 行 | 前两行都是 NULL |
第二个参数不写默认是 1,第三个参数不写默认是 NULL。要注意的是第三个参数的类型得跟第一列对得上------销售额是小数,填 0 就写 0;如果是日期列,别填 0,要么不填让它 NULL,要么填一个具体日期。
三、LAG 排在第几步,以及三个必踩的坑
3.0 先看它在整个 SQL 里的位置
MySQL 执行一句查询,大致是这个顺序:
text
FROM → WHERE → GROUP BY → 聚合函数
→ HAVING → 窗口函数 → ORDER BY → LIMIT
窗口函数排在聚合之后、排序之前。这个位置决定了三件事:
| 想在哪儿用 LAG | 行不行 | 原因 |
|---|---|---|
| SELECT 里 | 行 | 它就在这步算 |
| WHERE 里 | 不行 | WHERE 比它早,那时候还没算出来 |
| HAVING 里 | 不行 | HAVING 也比它早 |
| ORDER BY 里 | 行 | 排序在它之后,能拿到结果 |
| 再套一层的外层 WHERE | 行 | 内层已经算完了 |
所以 3.2、3.3 里那两个报错,不是 LAG 的毛病,是你把它放到了它还没出生的地方。
3.1 ORDER BY 决定"上一个"是谁(不报错,结果全变)
这是 LAG 最阴的地方:括号里的 ORDER BY 定义了什么叫"上一行"。写错了不报错,只是结果完全不对。
同一份数据,按时间排:
sql
SELECT 订单id, 订单金额,
LAG(订单金额) OVER (ORDER BY 下单时间) 上一笔
FROM 订单表 ORDER BY 下单时间;
| 订单id | 订单金额 | 上一笔 |
|---|---|---|
| 101 | 3000.00 | NULL |
| 102 | 2500.00 | 3000.00 |
| 103 | 4000.00 | 2500.00 |
| 104 | 1500.00 | 4000.00 |
| 105 | 6000.00 | 1500.00 |
只把 ORDER BY 换成订单金额,其它一个字不动:
sql
SELECT 订单id, 订单金额,
LAG(订单金额) OVER (ORDER BY 订单金额) 按金额的上一个
FROM 订单表 ORDER BY 订单id;
| 订单id | 订单金额 | 按金额的上一个 |
|---|---|---|
| 101 | 3000.00 | 2500.00 |
| 102 | 2500.00 | 2200.00 |
| 103 | 4000.00 | 3300.00 |
| 104 | 1500.00 | 1200.00 |
| 105 | 6000.00 | 5500.00 |
101 号订单的"上一个",从 NULL 变成了 2500。没有任何报错提示。
要的是"上一笔订单",ORDER BY 就得写时间;要的是"上一档金额",才写金额。括号里的排序 = 你对"上一次"的定义。
3.1.5 忘写 PARTITION BY:数据会串到别的组
先说结论:不写 PARTITION BY,LAG 会把整张表当成一队。
还是刚才那份部门月度数据,把 PARTITION BY 去掉:
sql
WITH 部门月度 AS (
SELECT e.部门, DATE_FORMAT(o.下单时间, '%Y-%m') 月份, SUM(o.订单金额) 销售额
FROM 员工表 e JOIN 订单表 o ON e.员工id = o.员工id
GROUP BY e.部门, DATE_FORMAT(o.下单时间, '%Y-%m')
)
SELECT 部门, 月份, 销售额,
LAG(销售额) OVER (ORDER BY 月份, 部门) 不分组的上一个
FROM 部门月度 ORDER BY 月份, 部门;
| 部门 | 月份 | 销售额 | 不分组的上一个 |
|---|---|---|---|
| 技术部 | 2026-01 | 7000.00 | NULL |
| 销售部 | 2026-01 | 4000.00 | 7000.00 |
| 技术部 | 2026-02 | 18000.00 | 4000.00 |
| 销售部 | 2026-02 | 8200.00 | 18000.00 |
销售部 1 月的"上一个"变成了技术部 1 月的 7000------跨部门串了。加了 PARTITION BY 部门,每组才各自从 NULL 开始。
判断方法很简单:跑完看每组第一个的"上一次"是不是 NULL。不是 NULL,就是分组漏了。
3.2 WHERE 里用不了 LAG
算出上月之后,有人会想"只保留比上月高的行",于是写成:
sql
SELECT 订单id, 订单金额
FROM 订单表
WHERE LAG(订单金额) OVER (ORDER BY 下单时间) > 2000;
MySQL 直接报错:
ERROR 3593 (HY000): You cannot use the window function 'lag' in this context.
原因是执行顺序:WHERE 比窗口函数先跑,WHERE 执行的时候 LAG 还没算出来。
要过滤窗口函数的结果,就得套一层子查询(或者用 CTE),在外层过滤。
3.3 聚合函数里也套不了
sql
SELECT SUM(LAG(订单金额) OVER (ORDER BY 下单时间)) FROM 订单表;
同样报:
ERROR 3593 (HY000): You cannot use the window function 'lag' in this context.
窗口函数不能当聚合函数的参数。窗口函数是在聚合之后才跑的 ,想对窗口结果再做聚合,一样得套一层。

四、PARTITION BY:各部门自己跟自己比
上面的例子是整张表按一个顺序排。如果数据里有多个部门、多个产品,每个都要跟自己的上一次比,就得加 PARTITION BY:
sql
WITH 部门月度 AS (
SELECT e.部门, DATE_FORMAT(o.下单时间, '%Y-%m') 月份, SUM(o.订单金额) 销售额
FROM 员工表 e JOIN 订单表 o ON e.员工id = o.员工id
GROUP BY e.部门, DATE_FORMAT(o.下单时间, '%Y-%m')
)
SELECT 部门, 月份, 销售额,
LAG(销售额) OVER (PARTITION BY 部门 ORDER BY 月份) 本部门上月
FROM 部门月度 ORDER BY 部门, 月份;
| 部门 | 月份 | 销售额 | 本部门上月 |
|---|---|---|---|
| 技术部 | 2026-01 | 7000.00 | NULL |
| 技术部 | 2026-02 | 18000.00 | 7000.00 |
| 销售部 | 2026-01 | 4000.00 | NULL |
| 销售部 | 2026-02 | 8200.00 | 4000.00 |
PARTITION BY 的作用是"分组后各自排队":技术部在自己的队列里比,销售部也在自己的队列里比,每组的第一个月都是 NULL,不会串到别的部门去。
不加 PARTITION BY,销售部的 1 月就会拿技术部的 2 月当"上一次",结果完全错。
还有个固定的报错要记住:PARTITION BY 必须写在 ORDER BY 前面,写反了直接语法错误:
sql
SELECT 月份, LAG(销售额) OVER (ORDER BY 月份 PARTITION BY 月份) 上月 FROM 月度;
ERROR 1064 (42000): You have an error in your SQL syntax; ...
五、LEAD:取下一次
LEAD 和 LAG 长得一模一样,只是方向相反------取"下一行":
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT 月份, 销售额,
LEAD(销售额) OVER (ORDER BY 月份) 下月销售额
FROM 月度 ORDER BY 月份;
| 月份 | 销售额 | 下月销售额 |
|---|---|---|
| 2026-01 | 11000.00 | 26200.00 |
| 2026-02 | 26200.00 | NULL |
这回 NULL 出现在最后一行 。LEAD 同样支持三个参数,LEAD(列名, 2, 0) 就是"往后两行,取不到填 0"。
六、4 个能直接抄的模板
模板 1:算环比百分比
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT 月份, 销售额,
LAG(销售额) OVER (ORDER BY 月份) 上月,
ROUND((销售额 - LAG(销售额) OVER (ORDER BY 月份))
/ LAG(销售额) OVER (ORDER BY 月份) * 100, 1) 环比百分比
FROM 月度 ORDER BY 月份;
| 月份 | 销售额 | 上月 | 环比百分比 |
|---|---|---|---|
| 2026-01 | 11000.00 | NULL | NULL |
| 2026-02 | 26200.00 | 11000.00 | 138.2 |
首行是 NULL 不会报错------任何值跟 NULL 做四则运算,结果都是 NULL,这反而是好事,不会算出个 100% 出来。
模板 2:算增长额(首月当 0)
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
)
SELECT 月份, 销售额,
销售额 - LAG(销售额, 1, 0) OVER (ORDER BY 月份) 比上月多
FROM 月度 ORDER BY 月份;
| 月份 | 销售额 | 比上月多 |
|---|---|---|
| 2026-01 | 11000.00 | 11000.00 |
| 2026-02 | 26200.00 | 15200.00 |
模板 3:分组各自比
sql
WITH 部门月度 AS (
SELECT e.部门, DATE_FORMAT(o.下单时间, '%Y-%m') 月份, SUM(o.订单金额) 销售额
FROM 员工表 e JOIN 订单表 o ON e.员工id = o.员工id
GROUP BY e.部门, DATE_FORMAT(o.下单时间, '%Y-%m')
)
SELECT 部门, 月份, 销售额,
LAG(销售额) OVER (PARTITION BY 部门 ORDER BY 月份) 本部门上月
FROM 部门月度 ORDER BY 部门, 月份;
模板 4:取前 N 次
sql
SELECT 订单id, 订单金额,
LAG(订单金额, 2) OVER (ORDER BY 下单时间) 前两笔
FROM 订单表 ORDER BY 下单时间;
| 订单id | 订单金额 | 前两笔 |
|---|---|---|
| 101 | 3000.00 | NULL |
| 102 | 2500.00 | NULL |
| 103 | 4000.00 | 3000.00 |
| 104 | 1500.00 | 2500.00 |
前两行都是 NULL,从第三行开始才有值。

模板 5:判断是不是连续增长
想知道"这个月比上个月高还是低",把两次的值直接比一下就行:
sql
WITH 月度 AS (
SELECT DATE_FORMAT(下单时间, '%Y-%m') 月份, SUM(订单金额) 销售额
FROM 订单表
GROUP BY DATE_FORMAT(下单时间, '%Y-%m')
),
带上月 AS (
SELECT 月份, 销售额,
LAG(销售额) OVER (ORDER BY 月份) 上月
FROM 月度
)
SELECT 月份, 销售额, 上月,
CASE WHEN 上月 IS NULL THEN '首月无对比'
WHEN 销售额 > 上月 THEN '涨'
WHEN 销售额 < 上月 THEN '跌'
ELSE '持平' END 走势
FROM 带上月 ORDER BY 月份;
| 月份 | 销售额 | 上月 | 走势 |
|---|---|---|---|
| 2026-01 | 11000.00 | NULL | 首月无对比 |
| 2026-02 | 26200.00 | 11000.00 | 涨 |
这里用了两个 CTE:第一个段算月度,第二个段算 LAG。因为窗口函数不能跟 CASE 的判断写在同一层(它比 CASE 晚跑),所以拆成两段最省事------这也是上一篇 WITH 多段串联最典型的用法。
注意 上月 IS NULL 这个判断要写在最前面:先排除首月,再做大小比较,不然 NULL 参与比较会得到"未知",三个分支一个都不进,走势会显示成"持平"。
七、和 WITH 配合:一份完整的环比报表
把前一篇的多段串联和这一篇的 LAG 放在一起,就是一份能直接交上去的报表:
sql
WITH 部门月度 AS (
SELECT e.部门, DATE_FORMAT(o.下单时间, '%Y-%m') 月份, SUM(o.订单金额) 销售额
FROM 员工表 e JOIN 订单表 o ON e.员工id = o.员工id
GROUP BY e.部门, DATE_FORMAT(o.下单时间, '%Y-%m')
)
SELECT 部门, 月份, 销售额,
LAG(销售额) OVER (PARTITION BY 部门 ORDER BY 月份) 上月,
ROUND((销售额 - LAG(销售额) OVER (PARTITION BY 部门 ORDER BY 月份))
/ LAG(销售额) OVER (PARTITION BY 部门 ORDER BY 月份) * 100, 1) 环比百分比
FROM 部门月度 ORDER BY 部门, 月份;
| 部门 | 月份 | 销售额 | 上月 | 环比百分比 |
|---|---|---|---|---|
| 技术部 | 2026-01 | 7000.00 | NULL | NULL |
| 技术部 | 2026-02 | 18000.00 | 7000.00 | 157.1 |
| 销售部 | 2026-01 | 4000.00 | NULL | NULL |
| 销售部 | 2026-02 | 8200.00 | 4000.00 | 105.0 |
四个部门的环比一次出来,各部门互不干扰。这就是 WITH + 窗口函数的组合价值:WITH 负责把复杂查询拆成能读懂的几段,窗口函数负责在同一段里做"跨行"的计算。
八、练习题
用本篇的练习表做下面三题,答案附在后面。
第 1 题:查出每笔订单,并带上这个人"上一笔订单"的金额。
第 2 题:在第 1 题的基础上,只要"有上一笔"的记录(把每人第一笔过滤掉)。
第 3 题:用 LEAD 查出每笔订单的"下一笔"金额,看看最后一笔是什么值。
答案
第 1 题:
sql
SELECT o.员工id, e.姓名, o.订单id, o.订单金额, o.下单时间,
LAG(o.订单金额) OVER (PARTITION BY o.员工id ORDER BY o.下单时间) 上一次金额
FROM 订单表 o JOIN 员工表 e ON e.员工id = o.员工id
ORDER BY o.员工id, o.下单时间;
| 员工id | 姓名 | 订单id | 订单金额 | 下单时间 | 上一次金额 |
|---|---|---|---|---|---|
| 1 | 张三 | 101 | 3000.00 | 2026-01-10 10:00:00 | NULL |
| 1 | 张三 | 102 | 2500.00 | 2026-01-15 14:00:00 | 3000.00 |
| 1 | 张三 | 106 | 1200.00 | 2026-02-05 15:00:00 | 2500.00 |
| 1 | 张三 | 109 | 5500.00 | 2026-02-20 09:00:00 | 1200.00 |
| 2 | 李四 | 104 | 1500.00 | 2026-01-25 16:00:00 | NULL |
| 2 | 李四 | 107 | 8000.00 | 2026-02-10 10:30:00 | 1500.00 |
| 2 | 李四 | 110 | 3300.00 | 2026-02-25 14:20:00 | 8000.00 |
| 3 | 王五 | 103 | 4000.00 | 2026-01-20 09:30:00 | NULL |
| 3 | 王五 | 105 | 6000.00 | 2026-02-01 11:00:00 | 4000.00 |
| 3 | 王五 | 108 | 2200.00 | 2026-02-15 17:00:00 | 6000.00 |
赵六(员工id 4)没有订单,用 JOIN 就查不出来。要让他也出现在结果里(上一次金额显示 NULL),把 JOIN 换成 LEFT JOIN:
sql
SELECT e.姓名, o.订单id, o.订单金额,
LAG(o.订单金额) OVER (PARTITION BY e.员工id ORDER BY o.下单时间) 上一次金额
FROM 员工表 e LEFT JOIN 订单表 o ON e.员工id = o.员工id
ORDER BY e.员工id, o.下单时间;
赵六那一行订单信息全是 NULL,上一次金额也是 NULL。这里有个细节:PARTITION BY e.员工id 要写员工表的 id,不能写 o.员工id------LEFT JOIN 补出来的那一行,订单表侧的员工id 是 NULL,用它分组会把赵六单独分成一组,算出来的"上一次"就不对了。
第 2 题:窗口函数不能写在 WHERE 里,所以要套一层:
sql
WITH t AS (
SELECT 员工id, 订单id, 订单金额,
LAG(订单金额) OVER (PARTITION BY 员工id ORDER BY 下单时间) 上一次金额
FROM 订单表
)
SELECT * FROM t WHERE 上一次金额 IS NOT NULL ORDER BY 员工id;
一共 7 行(10 笔订单减去 3 个人的第一笔)。
第 3 题:
sql
SELECT 订单id, 员工id, 订单金额,
LEAD(订单金额) OVER (PARTITION BY 员工id ORDER BY 下单时间) 下一次金额
FROM 订单表 ORDER BY 员工id, 下单时间;
每人的最后一笔,下一次金额都是 NULL。
九、LAG / LEAD 速查表
| 想干什么 | 写法 |
|---|---|
| 取上一次 | LAG(列) OVER (ORDER BY 排序列) |
| 取下一次 | LEAD(列) OVER (ORDER BY 排序列) |
| 首行不显示 NULL | LAG(列, 1, 0) |
| 取前两行 | LAG(列, 2) |
| 分组各自比 | 加 PARTITION BY 分组列(必须写在 ORDER BY 前) |
| 算出结果后要过滤 | 套一层 CTE 或子查询,WHERE 里写不了 |
记一句话就够:括号里的 ORDER BY,定义的就是"上一次"是谁。
十、几个常被问到的问题
Q:LAG 和 GROUP BY 有什么区别?
GROUP BY 是把多行压成一行(汇总),LAG 是在保留原有行数的前提下多带一列出来。行数不一样:GROUP BY 之后行数变少,LAG 之后行数不变。
Q:LAG 能配合 WHERE 条件只算部分数据吗?
能,但要分清楚 WHERE 过滤的是"参与排队的行"。写在最里层的 WHERE 先筛数据,筛完再排队取上一次;而"取出上一次之后再做筛选"必须套一层(见 3.2)。
Q:首行那个 NULL 一定要处理吗?
看用途。算增长额时可以留着(NULL 就是"没有上月"),但要参与加减乘除时结果也会变 NULL,这时候用 LAG(列, 1, 0) 补 0 最省事。做展示时一般用 CASE 换成"首月无对比"更好读。
Q:自连接和 LAG 哪个更快?
数据量小的时候感觉不出来。数据量大时 LAG 通常更好,因为它只排一次队、扫一遍;自连接要把同一份数据连一遍,等于多扫一次。真正在意的场景可以两边都跑一下,用 EXPLAIN 看扫描行数再决定。
Q:MySQL 5.7 能用吗?
不能。窗口函数是 MySQL 8.0 才有的,5.7 只能回到自连接或者变量的老办法。这也是很多老项目升级到 8.0 的主要动力之一。
前一篇讲的是 WITH 多段串联(把复杂查询拆成几段),这一篇讲 LAG/LEAD(跟上一次比)。两篇连起来用,先算月度、再比环比,一整份报表就出来了。
你在做报表时,"跟上个月比"是怎么写的?自连接、子查询,还是别的办法?