在SQL数据处理的实际工作中,我们经常会遇到需要对比当前行和上一行数据的需求,比如计算相邻两天的销售额差值、判断用户连续登录的状态变化、统计订单的环比增长情况等。这类需求如果采用传统的自连接或者子查询方式实现,不仅代码逻辑繁琐,还可能在大数据量下出现性能问题。而LAG窗口函数作为SQL标准中的内置函数,能够直接获取当前行之前指定偏移量的行数据,完美解决上一行数据获取的难题。

LAG窗口函数的基本语法
LAG函数的核心作用是返回当前行之前第N行的数据,其基本语法结构如下:
-- 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:必须的排序子句,指定每个分区内行的排序规则,LAG函数会按照这个排序来确定上一行的位置。
基础使用示例:获取上一行数据
假设我们有一张用户登录记录表user_login_log,表结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| user_id | INT | 用户ID |
| login_date | DATE | 登录日期 |
现在我们需要查询每个用户的每次登录日期,以及该用户上一次的登录日期,使用LAG函数可以实现如下:
-- 查询用户登录记录及上一次登录日期
SELECT
user_id,
login_date,
-- 获取当前用户上一次登录日期,没有的话返回'无记录'
LAG(login_date, 1, '无记录') OVER (
PARTITION BY user_id
ORDER BY login_date ASC
) AS last_login_date
FROM user_login_log
ORDER BY user_id, login_date;
上面的查询中,我们首先按照user_id进行分区,保证每个用户的数据独立计算,然后按照login_date升序排序,LAG函数会获取每个用户当前登录日期的前一行登录日期,第一行因为没有上一行,所以返回我们设置的默认值无记录。
进阶场景:行数据对比计算
接下来我们看一个更常见的业务场景,假设有一张每日销售额表daily_sales,结构如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| sale_date | DATE | 销售日期 |
| amount | DECIMAL(10,2) | 当日销售额 |
现在需要计算每天的销售额和前一天的销售额差值,也就是环比增长额,使用LAG函数可以轻松实现:
-- 计算每日销售额环比增长额
SELECT
sale_date,
amount AS current_day_amount,
-- 获取前一天的销售额
LAG(amount, 1, 0) OVER (
ORDER BY sale_date ASC
) AS last_day_amount,
-- 计算差值
amount - LAG(amount, 1, 0) OVER (
ORDER BY sale_date ASC
) AS growth_amount
FROM daily_sales
ORDER BY sale_date;
这里我们没有使用PARTITION BY,因为整个表就是按日期排序的全量销售数据,LAG函数默认在所有数据行上计算,获取前一天的销售额后直接和当前销售额做减法,就得到了环比增长额,第一行因为没有前一天数据,所以用默认值0计算,增长额就是当天的销售额。
传统实现方式与LAG函数的对比
在LAG函数出现之前,开发者通常会用自连接的方式实现获取上一行数据的需求,比如上面获取上一行登录日期的需求,用自连接实现如下:
-- 自连接实现获取上一行登录日期
SELECT
a.user_id,
a.login_date,
IFNULL(b.login_date, '无记录') AS last_login_date
FROM user_login_log a
LEFT JOIN user_login_log b
ON a.user_id = b.user_id
AND b.login_date = (
SELECT MAX(login_date)
FROM user_login_log c
WHERE c.user_id = a.user_id
AND c.login_date < a.login_date
)
ORDER BY a.user_id, a.login_date;
对比两种方式可以看出,自连接实现不仅需要多次关联同一张表,还需要嵌套子查询来获取最大的小于当前日期的登录日期,代码逻辑复杂,而且当数据量较大时,多次表关联会带来较大的性能开销。而LAG函数只需要一次遍历就能完成计算,代码简洁,执行效率更高。
使用LAG函数的注意事项
- LAG函数属于窗口函数,只能在SELECT子句和ORDER BY子句中使用,不能在WHERE、GROUP BY等子句中使用。
- 如果设置了
PARTITION BY,偏移量的计算会在每个分区内独立进行,不会跨分区获取上一行数据。 - 排序规则
ORDER BY是必须的,如果没有指定排序,行的顺序是不确定的,获取的上一行数据也会没有意义。 - 当偏移量超过分区内的行数时,会返回设置的默认值,如果没有设置默认值则返回NULL,需要根据业务场景合理设置默认值。
需要注意的是,LAG函数是SQL标准函数,主流的关系型数据库如MySQL 8.0+、PostgreSQL、SQL Server、Oracle等都支持该函数,但是部分低版本的数据库可能不支持,使用前需要确认数据库的版本兼容性。
通过上面的介绍可以看出,LAG窗口函数是解决SQL中获取上一行数据对比需求的高效工具,相比传统的自连接、子查询方式,代码更简洁,性能也更好,掌握它的使用方法能够大幅提升我们处理行对比类需求的效率。