在报表开发场景中,识别数据项的变动情况是常见需求,比如统计用户状态变更、订单金额波动、库存数量变化等。传统方式需要自关联表获取上一周期数据,逻辑复杂且性能较差,而使用SQL的LAG窗口函数可以直接获取同一分组内前序行的数据,通过简单对比就能完成变动标记。

LAG函数基础语法
LAG函数属于窗口函数的一种,作用是返回当前行之前指定偏移量的行的数据,基础语法如下:
LAG(expression [, offset [, default_value]])
OVER (
[PARTITION BY partition_expression]
ORDER BY sort_expression [ASC|DESC]
)
参数说明:
- expression:要获取的列或表达式,即需要对比的旧值来源
- offset:偏移量,默认值为1,表示获取前1行的数据,也可以设置为2获取前2行的数据
- default_value:当没有前序行时返回的默认值,不设置的话默认返回NULL
- PARTITION BY:分组条件,可选,用于在不同分组内独立计算LAG值,比如按用户分组、按部门分组
- ORDER BY:排序条件,必选,用于确定行的先后顺序,通常按时间、序号等排序
基础变动标记示例
假设我们有一张用户状态变更记录表user_status_log,结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | INT | 用户ID |
| status | VARCHAR | 用户状态 正常/冻结/注销 |
| change_time | DATETIME | 状态变更时间 |
现在需要查询每个用户的状态变更记录,并标记当前状态相比上一次是否发生了变动,SQL实现如下:
SELECT
user_id,
status AS current_status,
change_time,
-- 获取上一次的状态,没有上一次记录则返回空字符串
LAG(status, 1, '') OVER (
PARTITION BY user_id
ORDER BY change_time ASC
) AS previous_status,
-- 标记是否变动,状态不同则为变动
CASE
WHEN LAG(status, 1, '') OVER (
PARTITION BY user_id
ORDER BY change_time ASC
) != status THEN '是'
ELSE '否'
END AS is_changed
FROM user_status_log
ORDER BY user_id, change_time ASC;
上述查询中,首先按user_id分组,保证每个用户的状态记录独立排序,再按change_time升序排列,拿到每个用户上一次的状态值,最后通过对比当前状态和上一次状态,完成变动标记。
多维度数值对比场景
如果是数值类数据的变动标记,比如每日销售额对比,我们可以同时计算变动值和变动比例。假设有销售日报表daily_sales,包含sale_date(销售日期)、product_id(商品ID)、sale_amount(销售额)三个字段,需求是标记每日销售额相比前一日的变动情况:
SELECT
product_id,
sale_date,
sale_amount AS current_amount,
-- 获取前一日的销售额
LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
) AS previous_amount,
-- 计算变动金额
sale_amount - LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
) AS change_amount,
-- 计算变动比例,避免除零错误
CASE
WHEN LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
) = 0 THEN NULL
ELSE ROUND(
(sale_amount - LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
)) / LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
) * 100, 2
)
END AS change_rate_percent,
-- 标记是否增长
CASE
WHEN sale_amount > LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
) THEN '增长'
WHEN sale_amount < LAG(sale_amount, 1, 0) OVER (
PARTITION BY product_id
ORDER BY sale_date ASC
) THEN '下降'
ELSE '持平'
END AS change_trend
FROM daily_sales
ORDER BY product_id, sale_date ASC;
边界情况处理
使用LAG函数时需要注意几个常见的边界情况:
- 首行数据没有前序行,此时如果不设置
default_value,LAG会返回NULL,对比时需要根据业务需求设置合理的默认值,比如数值类默认0,状态类默认空字符串 - 分组内排序必须明确,否则LAG获取的前序行可能不符合预期,比如按时间排序时一定要明确升序还是降序,避免拿到错误的前序数据
- 如果数据存在重复的时间或排序值,LAG会按照数据库的实际执行顺序取值,必要时可以增加次要排序字段保证顺序稳定
性能注意事项
LAG函数作为窗口函数,执行时会对分组和排序的数据做内存计算,当数据量较大时,建议给PARTITION BY和ORDER BY涉及的字段建立联合索引,比如上述用户状态表的(user_id, change_time)索引,可以大幅提升查询性能。另外尽量避免在窗口函数内做复杂的表达式计算,提前处理好字段可以减少运行时的计算开销。