登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 用 LAG 比较相邻记录时怎样处理首行

来源:17golang原创

时间:2026-10-17 22:05:34 303浏览 收藏

MySQL 用 LAG 比较相邻记录时怎样处理首行

MySQL 的 LAG() 比较相邻记录时,分组中的第一行没有上一行,所以默认结果是 NULL。如果业务需要把首行当作 0、某个标签或“无法比较”,应在第三个参数中显式传入 default;如果还要区分“首行缺少上一条”和“上一条真实值就是 NULL”,就同时输出行号或首行标记。关键是先固定 PARTITION BY 和稳定的 ORDER BY,再决定缺失值语义。

官方文档:https://dev.mysql.com/doc/refman/8.4/en/window-function-descriptions.html。本文只讨论 LAG() 的偏移、首行和默认值,不展开窗口帧优化。

先把“相邻”定义成分组内的稳定顺序

LAG(expr, 1) 不是按表的物理存储顺序找上一条记录,而是按窗口中的排序结果找偏移行。多租户、账户或商品历史通常需要先按业务键分组,再用时间和唯一键组成排序条件。只按时间排序时,如果同一时间有多条记录,哪一条算“上一条”可能不稳定。

-- 每个账户独立比较;event_id 用来打破相同时间的并列顺序。
SELECT
    account_id,
    event_id,
    event_time,
    amount,
    LAG(amount) OVER (
        PARTITION BY account_id
        ORDER BY event_time, event_id
    ) AS previous_amount
FROM account_events;

这里第一条记录的 previous_amount 是 NULL,分组切换后新分组的第一条也会重新得到 NULL。因此首行不是整张结果的唯一一行,而是每个窗口分区各有一行。

MySQL LAG 按窗口分区和业务日期比较相邻记录的静态结构图
图1:静态说明图展示分组边界、当前行与上一条记录的对应关系;它不是数据库截图,也不表示执行时序。

默认写法为什么会在首行返回 NULL

LAG() 的常用形式是 LAG(expr [, N [, default]])。不写 N 时偏移量是 1,不写 default 时缺少目标行返回 NULL。当 N 大于 1 时,分组开头会有多行没有足够的前置记录,这些行都会落入 default 规则。

-- 首行没有上一条记录,默认保留 NULL。
SELECT
    account_id,
    event_time,
    amount,
    LAG(amount, 1, NULL) OVER (
        PARTITION BY account_id
        ORDER BY event_time, event_id
    ) AS previous_amount
FROM account_events;

要注意两个看起来相同、含义却不同的情况:一是当前行确实是分组首行,根本不存在上一行;二是上一行存在,但上一行的 amount 本身是业务上的真实 NULL。只看 previous_amount IS NULL 无法把两者区分开。

需要可计算数值时,把 default 写成业务零值

如果后续要计算“当前金额减上一笔金额”,首行使用 0 往往比传播 NULL 更方便。但这不是 LAG() 的通用正确答案:零值代表业务含义,只有在业务确认“没有上一笔”等价于 0 时才这样写。

-- 只在业务约定首行基线为 0 时使用第三个参数。
SELECT
    account_id,
    event_time,
    amount,
    LAG(amount, 1, 0) OVER (
        PARTITION BY account_id
        ORDER BY event_time, event_id
    ) AS previous_amount,
    amount - LAG(amount, 1, 0) OVER (
        PARTITION BY account_id
        ORDER BY event_time, event_id
    ) AS amount_delta
FROM account_events;

这种写法会把首行的差值算成当前金额。它适合“从零开始累计”或“首笔没有历史值就按零计算”的报表;不适合需要把首行排除在环比、波动率或异常检测之外的场景。

用行号标记首行,避免把缺失和真实 NULL 混在一起

最稳妥的做法通常是保留 NULL,同时用同一个分区与排序规则计算 ROW_NUMBER()。这样后续可以按 row_number = 1 处理首行,也能继续保留上一条记录真实为空的事实。

-- 行号标识窗口首行;previous_amount 的 NULL 仍保留原始语义。
WITH ordered_events AS (
    SELECT
        account_id,
        event_id,
        event_time,
        amount,
        ROW_NUMBER() OVER (
            PARTITION BY account_id
            ORDER BY event_time, event_id
        ) AS row_number_in_account,
        LAG(amount) OVER (
            PARTITION BY account_id
            ORDER BY event_time, event_id
        ) AS previous_amount
    FROM account_events
)
SELECT
    account_id,
    event_id,
    event_time,
    amount,
    previous_amount,
    row_number_in_account = 1 AS is_first_row
FROM ordered_events;

如果数据库或团队约定不使用布尔表达式,也可以把最后一列写成 CASE WHEN row_number_in_account = 1 THEN 1 ELSE 0 END。注意两次窗口定义必须保持一致,否则行号标记和 LAG() 可能不是同一条“上一记录”。

MySQL LAG 区分真实 NULL、无上一行和业务默认值的静态结构图
图2:静态结构图对比保留 NULL、使用 default 0 和输出首行标记三种业务解释;它不是查询结果截图。

偏移量大于 1 时,首部会连续出现缺失值

当查询要比较前两条或前三条记录时,缺失范围不再只有第一行。例如 LAG(amount, 2) 在每个分组的前两行都会没有对应记录;如果填入 default 0,前两行都会得到 0。不要把这个 default 误认为真实的历史金额。

-- 偏移 2 会让每个分区的前两行进入 default 规则。
SELECT
    account_id,
    event_time,
    amount,
    LAG(amount, 2, NULL) OVER (
        PARTITION BY account_id
        ORDER BY event_time, event_id
    ) AS amount_two_rows_ago,
    ROW_NUMBER() OVER (
        PARTITION BY account_id
        ORDER BY event_time, event_id
    ) AS row_number_in_account
FROM account_events;

如果偏移量来自参数,仍要确认它是非负整数,并结合分组长度决定缺失值策略。偏移 0 表示读取当前行,不是“没有上一行”。

按现象检查 LAG 的首行问题

现象优先检查合适的处理
每个账户第一条都是 NULL是否刚好是分区首行保留 NULL,或显式设置业务 default
同一时间的上一条不稳定ORDER BY 是否缺少唯一键追加 event_id 等稳定排序键
首行被当作真实缺失金额是否只判断 previous_amount IS NULL增加 ROW_NUMBER 首行标记
前两行都出现默认值LAG 的 N 是否为 2 或更大按偏移量理解缺失范围
跨账户比较到了上一账户是否漏写 PARTITION BY把业务分组键写进窗口定义

结论:先保留语义,再决定是否填充值

处理 MySQL LAG() 首行时,可以按这个顺序做决定:先用稳定排序定义相邻关系;默认让无上一行返回 NULL;需要计算时才使用第三个参数填入业务默认值;需要审计或区分真实空值时,再输出 ROW_NUMBER() 或首行标记。这样既不会把首行误认为上一条记录为空,也不会因为方便计算而悄悄改变报表含义。

LAG 的首行为什么是 NULL?

因为分区首行不存在偏移 1 的前置记录;不提供 default 时,MySQL 按规则返回 NULL。

能不能用 COALESCE 代替 LAG 的 default?

可以写成 COALESCE(LAG(amount) OVER (...), 0),但它也会把上一行真实为 NULL 的情况改成 0。需要区分两种情况时,优先保留 NULL 并增加首行标记。

只按时间排序会有什么问题?

同一分区中时间相同的记录没有唯一顺序,上一条关系可能不稳定。追加唯一键或其他确定性字段,才能让比较结果可复现。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>