LEAD 函数解决的典型问题可以概括为:在一条 SQL 查询中,不写自连接或相关子查询,也能读取同一结果集中下一行的字段值。例如员工表按薪资排序后,想知道当前员工与下一位员工的薪资差,传统做法要给表起两个别名再关联,而窗口函数 LEAD(column, offset, default) 配合 OVER 子句可以一步完成。

从实现角度看,LEAD 读取下一行数据并非真正修改表结构,而是在查询结果生成后的窗口框架内,按 ORDER BY 指定的顺序向前定位行。偏移量为 1 表示紧邻的下一行,为 2 表示下下行。若超出分区末尾,则返回 default 设定的默认值,未指定时返回 NULL。换句话说,LEAD 只关心排序和分区,不需要额外的关联键。
一、LEAD 函数的基本语法与参数含义
LEAD 函数的标准语法并不复杂,它属于窗口函数的一种,必须与 OVER 子句一起使用。语法结构如下:
LEAD(expression [, offset [, default]]) OVER (
[PARTITION BY partition_columns]
ORDER BY sort_columns
)
第一个参数 expression 可以是字段名、表达式或者计算列,表示要读取下一行的哪一个值。第二个参数 offset 是偏移量,必须为大于等于 0 的整数,默认值是 1。如果传入 0,就相当于读取当前行本身。第三个参数 default 是默认值,当下一行不存在时返回。默认值的数据类型应当与 expression 的结果类型兼容,否则可能会出现隐式转换或者报错。
OVER 子句中的 PARTITION BY 是可选的,它用于把结果集划分成多个独立窗口。每个分区内部单独计算下一行,分区之间互不干扰。ORDER BY 则是必须提供的排序依据,它决定了什么是下一行。如果省略 PARTITION BY,整个查询结果会被当作一个窗口。需要特别注意的是,如果 ORDER BY 排序字段存在重复值,LEAD 取哪一行就可能受到物理扫描顺序影响,结果有时不够稳定,因此建议在排序字段末尾追加一个唯一键,确保结果可预期。
二、用 LEAD 函数获取下一行数据的实现示例
下面用一个部门的员工薪资表来演示 LEAD 的实际用法。先创建示例表并插入一些测试数据:
-- 创建示例表
CREATE TABLE employee_salary (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(50),
dept_id INT,
salary DECIMAL(10,2)
);
-- 插入数据
INSERT INTO employee_salary VALUES
(1, '张宁', 10, 8500.00),
(2, '李远', 10, 9200.00),
(3, '王珂', 20, 7600.00),
(4, '陈松', 20, 8800.00),
(5, '赵颖', 20, 8800.00),
(6, '刘畅', 10, 7800.00);
需求是查询每个部门内部按照薪资从低到高排序后,当前员工的下一位员工薪资是多少。使用 LEAD 可以这样写:
SELECT
emp_id,
emp_name,
dept_id,
salary,
LEAD(salary, 1, 0) OVER (
PARTITION BY dept_id
ORDER BY salary, emp_id
) AS next_salary
FROM employee_salary
ORDER BY dept_id, salary, emp_id;
在这个查询中,PARTITION BY dept_id 把员工按照部门分开。每个部门内部按照薪资升序排列,如果薪资相同则按照 emp_id 升序排列。LEAD 读取的就是相同部门内下一条记录的薪资。对于每个部门薪资最高的那位员工,因为后面没有更多记录,所以返回默认值 0。这样就能直观地看到当前薪资与下一档薪资的差距。
除了直接获取下一行薪资,LEAD 还可以用来计算环比增长率。比如把查询包一层子查询,然后用 (next_salary - salary) / salary 计算增长比例。需要注意的是,如果当前薪资为 0,除法会产生错误,因此实际使用时要做好 CASE WHEN 判断。也可以把 LEAD 的第一个参数换成 emp_name,直接获取下一名员工的姓名,应用方式非常灵活。MySQL 8.0、PostgreSQL、SQL Server、Oracle 等主流数据库都支持 LEAD,但 MySQL 5.7 及以下版本没有窗口函数,需要另寻方案。
三、LEAD 与 LAG 及自连接方案对比
LAG 是和 LEAD 方向相反的函数,LAG 读取的是上一行数据,LEAD 读取的是下一行数据。两者参数完全一致,只是取值方向不同。假设还是按照薪资排序,LAG(salary) 返回的是前一名员工的薪资,LEAD(salary) 返回的是后一名员工的薪资。在同一个查询中同时使用两个函数,可以轻松计算当前行与前后行的差值,适合做趋势分析和区间边界判断。
SELECT
emp_id,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary, emp_id) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary, emp_id) AS next_salary
FROM employee_salary;
在没有窗口函数之前,很多人会使用自连接来实现类似的下一行读取。例如按照部门和薪资找下一行,可能会写出下面这样复杂的 SQL:
SELECT a.emp_id,
a.salary AS current_salary,
b.salary AS next_salary
FROM employee_salary a
LEFT JOIN employee_salary b
ON a.dept_id = b.dept_id
AND b.salary > a.salary
AND NOT EXISTS (
SELECT 1
FROM employee_salary c
WHERE c.dept_id = a.dept_id
AND c.salary > a.salary
AND c.salary < b.salary
)
ORDER BY a.dept_id, a.salary;
这段自连接不仅要处理大于关系,还要通过 NOT EXISTS 排除中间记录,逻辑非常绕。如果薪资出现重复值,还要额外处理相等情况,稍不注意就会漏数据或多数据。LEAD 把判断下一行的逻辑全部交给窗口框架,代码更短,语义也更清晰。从执行层面看,LEAD 只需要在排序后的数据上向前读取,自连接则可能产生多次表扫描和嵌套循环,数据量大时性能差距明显。不过具体优化效果仍然取决于数据库执行计划,必要时可以用 EXPLAIN 对比验证。
四、LEAD 函数在业务场景中的实践与注意点
LEAD 在时间序列分析中非常实用。比如订单表记录了每个客户的下单时间,想要计算每笔订单与下一笔订单之间的时间间隔,可以按照客户分组、按下单时间排序,然后使用 LEAD 读取下一笔订单的时间,再做时间差计算。类似的需求还包括用户访问日志的会话间隔、库存流水的下一状态变化、传感器数据的下一读数等。
SELECT
order_id,
customer_id,
order_time,
LEAD(order_time) OVER (
PARTITION BY customer_id
ORDER BY order_time
) AS next_order_time
FROM orders;
使用 LEAD 时有几个细节值得留意。第一,默认值不要随便用 0,如果字段本身含义不是金额数量,0 可能会造成误解,可以根据业务返回 NULL 或者一个有意义的标记。第二,偏移量不能是负数,需要向前取时应该改用 LAG。第三,分区键和排序键的选择会直接影响结果,分区格粒度太粗或太细都会带来语义偏差。第四,排序字段重复时务必追加唯一键,否则同值行的顺序不稳定,LEAD 的结果可能每次查询都不一样。第五,遇到 NULL 值需要提前规划,比如用 COALESCE 替换默认值,避免后续计算被 NULL 吞掉。
性能方面,LEAD 作为窗口函数通常需要先对数据排序。如果查询经常按照某个字段排序并进行 LEAD 计算,可以在该字段上建立索引,减少文件排序开销。对于只需要计算少量行的场景,窗口函数通常比自连接更快。但如果业务逻辑要求跨多行做聚合判断,还是要结合 ROWS BETWEEN 这类窗口框架语法来综合设计。理解 LEAD 的实际读取机制,比只会套语法更能解决真实问题。