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

MySQL 窗口函数 RANGE frame 怎样确定边界

来源:17golang原创

时间:2026-11-14 18:39:30 437浏览 收藏

窗口函数结果看起来“多算了一行”时,先别急着改聚合表达式。很多时候,真正改变结果的是 window frame:RANGE 按 ORDER BY 的值域找行,而不是按物理行号找行。当前排序值相同的记录会被当作 peers,一起进入当前 frame。

判断 MySQL RANGE 边界可以记成一句话:先看当前行的 ORDER BY 值,再按值的前后范围取行;其中 CURRENT ROW 表示当前值对应的整组 peers。没有显式 frame 时,带 ORDER BY 的默认值等价于 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。

官方文档:https://dev.mysql.com/doc/refman/8.4/en/window-functions-frames.html

要点速览
  • ROWS 按当前行前后多少行定位,RANGE 按排序值的差值定位。
  • RANGE ... CURRENT ROW 会包含与当前行 ORDER BY 值相同的 peers,所以重复值可能让累计结果一次跳两行。
  • 数值范围使用数值偏移,时间范围使用 INTERVAL;带偏移的 RANGE 需要匹配数值或时间类型的排序表达式。
  • 需要稳定、逐行推进的累计时显式写 ROWS;需要“同一业务值同一窗口”时显式写 RANGE。

先用两个相同的 score 重现问题

准备一组按部门统计成绩金额的数据。刻意让 score=10 出现两次,这样才能看出“当前行”和“当前值对应的 peers”不是同一个概念。

CREATE TEMPORARY TABLE score_amount (
    id INT PRIMARY KEY,
    dept VARCHAR(20) NOT NULL,
    score INT NOT NULL,
    amount INT NOT NULL
);

INSERT INTO score_amount (id, dept, score, amount) VALUES
    (1, 'A', 10, 5),
    (2, 'A', 10, 7),
    (3, 'A', 20, 8),
    (4, 'A', 25, 4);

-- 用重复 score 观察 RANGE 和 ROWS 的 frame 成员差别
SELECT
    id,
    score,
    amount,
    SUM(amount) OVER (
        PARTITION BY dept
        ORDER BY score
        RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS range_total,
    SUM(amount) OVER (
        PARTITION BY dept
        ORDER BY score
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS rows_total
FROM score_amount
ORDER BY score, id;

按这个数据理解结果,RANGE 的两个 score=10 行都会看到累计值 12,因为它们的当前排序值相同,彼此属于 peers。ROWS 则是一行一行向前扩展,前两行分别是 5 和 12;由于 ORDER BY score 没有为同分行指定唯一顺序,实际逐行顺序不应被当作业务排序依据。

MySQL RANGE 与 ROWS 在重复 score peers 上的累计边界对比图

图1:相同 ORDER BY 值在 RANGE 中成组进入 frame,在 ROWS 中按行位置逐步推进。

RANGE 的边界到底由什么决定

可以把 frame 拆成三层来读:PARTITION BY 决定候选数据集,ORDER BY 给当前行一个排序值,RANGE 再围绕这个值计算上下界。它不是在问“当前行前面有几行”,而是在问“哪些行的排序值落在这个值域内”。

写法边界含义适合回答的问题
ROWS ... CURRENT ROW当前物理行到第几条记录为止的累计
RANGE ... CURRENT ROW当前值及其 peers到当前分数、价格或日期值为止的累计
RANGE 5 PRECEDING当前排序值减 5 到当前值数值区间内的滚动统计
RANGE INTERVAL 1 DAY PRECEDING当前时间向前一天到当前时间按时间值而不是按行数统计

例如当前 score=20,写成 RANGE BETWEEN 5 PRECEDING AND CURRENT ROW 时,候选值域是 15 到 20。只要某行的排序值等于 15、16、17、18、19 或 20,它就可能进入当前 frame;这里不要求它在结果集里紧挨着当前行。

MySQL RANGE frame 围绕 ORDER BY 当前值计算数值边界并包含同值 peers 的说明图

图2:数值型 RANGE 用当前排序值和偏移量计算上下界,CURRENT ROW 覆盖同值 peers。

CURRENT ROW 为什么不是“这一行”

这是最容易误读的一处。对于 ROWS,CURRENT ROW 确实指当前物理行;对于 RANGE,它指当前行的 peers,也就是按窗口 ORDER BY 判断为同值的行。

因此下面两个 frame 的业务含义不同:

-- RANGE:同一个 score 的记录在同一价值边界内一起计算
SUM(amount) OVER (
    PARTITION BY dept
    ORDER BY score
    RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS total_by_score,

-- ROWS:每向下移动一条物理记录,frame 才扩大一行
SUM(amount) OVER (
    PARTITION BY dept
    ORDER BY score
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS total_by_row

如果报表要求“同分人员显示同一个累计金额”,使用 RANGE 更接近需求;如果要求“按明细行逐条累加”,就要使用 ROWS,并给 ORDER BY 补充能表达业务先后关系的列,例如唯一编号或明确的创建时间。

默认 frame 可能让结果悄悄变化

窗口函数没有写 frame 时,不能只凭“看起来像累计”来猜。带 ORDER BY 时,MySQL 的默认 frame 等价于:

-- 显式写出带 ORDER BY 时的默认 RANGE 语义
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

也就是说,给原查询新增 ORDER BY,可能同时把“整分区聚合”变成“从分区开头累计到当前值”。没有 ORDER BY 时,默认 frame 覆盖整个 partition。为了避免后续维护者误解,报表 SQL 最好显式写出 frame,不把关键业务含义交给默认值。

一个常见的改动是把总额查询改成带排序的窗口查询:

-- 明确要求每个部门都显示整个部门总额,不随 score 递增
SELECT
    id,
    dept,
    score,
    SUM(amount) OVER (
        PARTITION BY dept
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) AS dept_total
FROM score_amount;

这里故意不写窗口 ORDER BY,因为目标是整分区总额;如果还要按 score 排序,把排序放在查询最外层,不要让显示顺序和聚合 frame 混在一起。

数值范围和时间范围分别怎么写

带偏移量的 RANGE 需要能进行范围计算的排序表达式。数值排序使用非负数值偏移,时间排序使用 INTERVAL。例如下面的数值滚动窗口表示“当前 score 向前 10 分以内”。

-- 数值 RANGE:当前 score 及其之前 10 分内的金额
SELECT
    id,
    score,
    SUM(amount) OVER (
        PARTITION BY dept
        ORDER BY score
        RANGE BETWEEN 10 PRECEDING AND CURRENT ROW
    ) AS amount_in_score_band
FROM score_amount
ORDER BY score, id;

时间字段的写法则把偏移量写成时间间隔。这里的“前一天”是时间值域,不是结果中前一行;某个时间段即使有很多条记录,也会一起进入 frame。

-- 时间 RANGE:当前时间向前一天的记录都纳入统计
SELECT
    event_time,
    amount,
    SUM(amount) OVER (
        PARTITION BY account_id
        ORDER BY event_time
        RANGE BETWEEN INTERVAL 1 DAY PRECEDING AND CURRENT ROW
    ) AS rolling_amount
FROM account_events
ORDER BY event_time;

哪些窗口函数真的受 frame 影响

聚合类窗口函数以及 FIRST_VALUE()、LAST_VALUE()、NTH_VALUE() 会读取当前 frame,所以 RANGE 和 ROWS 的选择会直接改变结果。排名和偏移类函数的语义不同,不能因为它们写在 OVER (... frame ...) 里,就认为 frame 一定会限制它们。

函数类别排查重点常见误区
SUM/AVG/MIN/MAX当前 frame 包含哪些行把 RANGE 当成前 N 行
FIRST_VALUE/LAST_VALUEframe 末端是否到当前值以为 LAST_VALUE 总能取到整个分区最后一行
ROW_NUMBER/RANK/DENSE_RANK分区和排序规则用 frame 试图限制排名范围
LAG/LEAD偏移位置与默认值把偏移行当作 RANGE 值域

特别是 LAST_VALUE():默认 frame 只到当前排序值,所以它经常返回当前 frame 的最后值,而不是整个 partition 的最后值。若需求真的是分区末端值,需要显式把 frame 延伸到 UNBOUNDED FOLLOWING。

生产查询上线前的四项检查

  1. 先问业务单位。需求说的是“前 10 行”,还是“前 10 分钟/前 10 分/同一个分数”?前者通常对应 ROWS,后者通常对应 RANGE。
  2. 列出重复值。检查 ORDER BY 列是否会重复,尤其是日期、金额、状态分数。重复值会让 RANGE 一次包含多行。
  3. 确认类型。数值偏移配数值排序,时间间隔配时间排序;不要把字符串格式的日期直接当作时间范围依据。
  4. 把 frame 写完整。不要依赖默认 frame;同时确认 CURRENT ROW 在当前 frame 单位下是物理行还是 peers。

如果结果出现“同值行相同、下一组突然跳高”,这往往不是重复计算,而是 RANGE 的预期行为。把同一条表达式改写为 ROWS 做对照,通常几分钟就能定位 frame 是否选错。

常见问题

RANGE 和 ROWS 可以互换吗?

不建议。ROWS 面向行位置,RANGE 面向排序值;重复排序值、缺失日期和不规则数据间隔都会让两者产生不同结果。

RANGE CURRENT ROW 会只包含当前 id 吗?

不会。它会包含与当前窗口 ORDER BY 值相同的 peers。若想只推进一条物理记录,使用 ROWS,并提供清晰的唯一排序顺序。

为什么给窗口函数加 ORDER BY 后总额变了?

因为默认 frame 发生了变化:带 ORDER BY 时默认是从分区开始到当前值的 RANGE 累计;不带 ORDER BY 时默认覆盖整个分区。

RANGE 里的 5 PRECEDING 是前五行吗?

不是。它表示当前排序值减去 5 的值域下界;前五行应写 ROWS 语义,并且还要明确行的排序规则。

应该总是显式写 frame 吗?

对于报表、账务累计和滚动指标,建议显式写。这样后续新增排序列或调整查询结构时,frame 的业务含义不会依赖默认规则。

小结

读 MySQL 窗口函数的 RANGE frame,顺序是“先看 partition,再看 ORDER BY 当前值,最后按值域解释边界”。CURRENT ROW 在 RANGE 中代表当前值对应的 peers,这正是重复排序值导致累计结果一起跳变的原因。把“按行”还是“按值”先说清,再显式选择 ROWS 或 RANGE,窗口结果就会变得可解释、可维护。

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