MySQL 大数据量分页查询优化-基于时间游标的分页方案

MySQL深度分页的问题及优化方案:千万级数据量如何快速分页 字节跳动面试官问我海量数据分页如何展示 阅读详情

MySQL 大数据量分页查询优化——基于时间游标的分页方案

场景与目标

场景:数据库表有 time 时间字段,并建了以 time 为最左前缀的索引(单列索引,或 time 为第一列的联合索引),需要按时间及其他业务条件分批查询,且要避免慢查询。

对数据库表的使用方式:按时间顺序把这些数据分批读取并处理,要求——

  • 不重不漏:每行恰好被处理一次
  • 每页查询代价有上界:不随总数据量和已处理深度增长
  • 断点续跑(常见诉求):处理进度可持久化,重启后从断点继续

本文方案:用 time 字段做分页游标——上一页的末端时间就是下一页的起点,把查询拆成小段 range scan,每页只扫描本页时间区间内的行。


需要注意的问题

把 time 当游标,直觉做法是"取一页、把页末端时间当下一页起点"循环下去。但有六个坑,任踩一个就会重复、漏数据或死循环:

#问题后果方案的应对
1同一时间值的行数可能超过页大小该时间点跨在两页边界上,区间切页要么重复要么遗漏;处理不当还会死循环情况 B:单点全取 + 推进 1 个精度单位(见"完整流程"②、“情况B方案对比”)
2业务过滤条件把整段数据滤空页内查不出数据,游标失去推进依据——死循环或误判结束而漏数据第一轮定界不带业务条件(见"完整流程"①、“第一轮取游标方案对比”)
3time 字段可空time >= :start_time 永远匹配不到 NULL 行,这些行被静默跳过且无任何报错使用前提,见"前提与限制"
4查询期间区间内有并发写入"不重不漏"所依赖的区间静态性失效;"只写当前时刻"并不自动安全使用前提与滞后要求,见"前提与限制"、“边界场景处理”
5同一时间值内部的行序不稳定下游若依赖该顺序会出错使用前提,见"前提与限制"
6没有确定的终止条件无限循环使用前提,见"前提与限制"

其中 1、2 是方案设计要解决的核心问题,3 ~ 6 是使用方必须满足的前提。另有两个使用边界:本方案只支持顺序推进(不支持按页码随机跳转)、不计算总数/总页数。


核心原理

用时间字段做"游标",把查询拆成小段 range scan:

  1. 第一轮只查 time 一个字段,走 time 索引,取 LIMIT page_size 条,代价极低
  2. 外层 MAX 聚合直接得到 page_end_time,只回 1 行(选型见"第一轮取游标方案对比"一章)
  3. 第二轮用 time 范围条件约束扫描区间,附加业务过滤条件,取实际数据
  4. 后续页按 ② 各情况的规则推进 start_time(右开区间推进到端点、单点全取推进到下一格),循环往复

为什么快:

  • 每轮第二轮扫描行数 ≈ 该时间区间内实际数据量
  • 不随总数据量增长而变慢
  • time 最左前缀索引保证 range scan 起始点精准

完整流程

初始化

start_time = 查询起始时间
end_time   = 查询终止时间(全局上限,可选)
page_size  = 每页游标条数(如 1000)

循环体

① 第一轮:取游标

SELECT MAX(time) FROM (
    SELECT time
    FROM t
    WHERE time >= :start_time
      AND time < :end_time   -- 可选;无上界时省略此行
    ORDER BY time ASC
    LIMIT :page_size
) AS page;
  • 内层子查询圈定候选区间内 time 最小的 page_size 行(走 time 索引,纯覆盖扫描),外层 MAX 聚合只返回 1 行 1 列,即 page_end_time
  • 升序 + LIMIT page_size 下,MAX(time) 恒等于"最后一条的 time",两种写法语义完全等价(选型见"第一轮取游标方案对比"一章)
  • 只回 1 行的收益:应用侧日志/调试不必面对 page_size 个时间值,传输与客户端内存开销最小
  • 内层 ORDER BY 与 LIMIT 均不可省:ORDER BY 保证取的是"最小的 page_size 条"(去掉后 LIMIT 变成任意抽样,MAX 无意义);LIMIT 是分页的承重墙(去掉即退化为全区间 MAX,见对比章方案 4)
  • 定界不带业务条件:这是问题 2 的解——业务条件可能把整段滤空,若游标推进依赖带条件的查询结果,游标将失去推进依据;第一轮只按 time 定界,推进永远有保障

结果为空时:

聚合查询永远返回 1 行,空集时值为 NULL——time NOT NULL 且 time >= :start_time 天然滤掉 NULL 行,故 NULL 只可能来自空集,page_end_time IS NULL 即 [:start_time, :end_time) 区间内无数据,不执行第二轮:

  • 有 end_time:置 start_time = end_time,循环结束
  • 无 end_time:start_time 之后已无任何数据(前提:区间静态无新增),直接终止

② 第二轮:取数据并推进游标

时间范围仅用于约束索引扫描区间,附加其他业务过滤条件。取数与推进一体,按情况处理:

情况 A:page_end_time != start_time

SELECT *
FROM t
WHERE time >= :start_time
  AND time < :page_end_time
  AND {其他业务条件}
ORDER BY time ASC;

ORDER BY time 引导优化器选择 time 索引:range scan 的输出顺序天然满足
排序,无需 filesort。注意 ORDER BY 本身不强制访问路径——优化器按成本
选择,若业务条件上存在选择性更好的索引,可能改走该索引(结果仍正确,仅扫描
路径不同);必须钉死路径时使用 FROM t FORCE INDEX(idx_time)。

本轮已处理区间为 [start_time, page_end_time)(右开),推进游标:

start_time = page_end_time

情况 B:page_end_time == start_time

SELECT *
FROM t
WHERE time = :start_time
  AND {其他业务条件};

= 走索引 ref 访问,该秒数据 ≤ 10000 条,一把全取,不排序。

本轮已处理区间为单点 [start_time, start_time](右闭),推进游标到该时间点的下一格
(秒精度 +1s,毫秒精度 +1ms):

start_time = start_time + 1 个时间精度单位

情况 B 不可推进到 page_end_time(它等于 start_time),否则游标原地踏步,
会无限循环并重复输出同一时间点的数据。这是问题 1 的解:同一时间点数据量
≥ page_size 时,整点全取、一次越过,而不是让该点横跨页边界。

回到 ①,直到 start_time >= end_time(或第一轮返回 NULL 时终止)。

整个循环可用伪代码概括:

游标 = start_time
循环:
    page_end_time = 第一轮: MAX(time) FROM (升序 LIMIT page_size 的候选页)
    若 page_end_time IS NULL:                # 区间内无数据
        结束
    若 page_end_time != 游标:                # 情况 A
        第二轮: time ∈ [游标, page_end_time) + 业务条件
        游标 = page_end_time                  # 已处理区间右开
    否则:                                    # 情况 B: 该时间点行数 ≥ page_size
        第二轮: time = 游标 + 业务条件
        游标 = 游标 + 1 个时间精度单位         # 已处理区间右闭
    若有 end_time 且 游标 >= end_time:
        结束

前提与限制

项说明
time 字段位于索引最左前缀:单列索引,或 time 为第一列的联合索引方案基础,见下注
time 字段 NOT NULL可空时 time >= :start_time 永远匹配不到 NULL 行,这些行被静默跳过且无任何报错
一秒内数据量 ≤ 10000(硬上限)start_time == page_end_time 时一把全取的内存/网络保障
查询执行期间,所处理的时间区间不会产生新记录分页期间该区间数据静态,是"不重不漏"的基础;若存在并发写入,须保证新记录的 time 均落在游标已推进位置之后
下游处理不依赖同一时间值内的行序情况 B 不排序(情况 A/B 见"完整流程"②;整体仍按 time 非降序,见下注)
查询有确定的终止时间防止无限循环
只支持顺序推进,不支持按页码随机跳转游标分页的固有形态:下一页起点由上一页末端决定
不计算总数/总页数方案只负责按时间区间取数,需要总数须另行统计

注:不要求 time 单调递增。游标按 time 值范围切分,与行的插入顺序无关
(已用乱序插入数据实测验证不重不漏)。

注:输出整体按 time 非降序——游标单调推进、情况 A 带 ORDER BY time、
情况 B 整批为同一时间值;需要稳定的只是同一时间值内部的行序(加 ORDER BY id)。

注:索引不要求单列,idx(time) 与 (time, 其他字段) 均可:第一轮按 time
前缀有序扫描——ORDER BY time 免 filesort、LIMIT 提前终止、仅查 time 为
覆盖扫描;情况 B 的 = 对最左前缀仍是 ref 访问;联合索引第二列恰为业务条件
字段时,业务条件可纳入索引访问(range/ref 命中两列,或经 ICP 在索引内过滤),
比单列索引更优。time 不在最左列(如 (status, time))不支持:第一轮无法
按 time 有序扫描、LIMIT 失去提前终止,情况 B 的等值定位退化为全表扫描。

注:查询与排序不依赖主键——排序键只有 time,游标推进只用 time 值。同一
时间值内部的行序未定义,idx(time) 与 (time, x) 两种索引下该内部顺序不同,
均不影响正确性。ORDER BY id、(time, id) 联合游标是特定场景的可选增强
(见"边界场景处理"),不是本方案的依赖。


数据量估算

场景扫描行数说明
第一轮(取游标)page_size(如 1000)只查 time 字段,纯索引覆盖;外层 MAX 聚合只回 1 行(服务端派生表物化 ≤ page_size 行)
第二轮(正常页)该时间区间内实际行数≤ page_size − 1(可证明,见下)
第二轮(同秒页)≤ 10000索引 ref 访问,一把全取
每页总代价O(log n + k),k 为区间内行数不随总表大小退化

情况 A 第二轮行数上界的证明:第一轮按 time 升序 LIMIT page_size,
返回的即候选区间内 time 最小的 page_size 行;升序序列中,time 值严格
小于 page_end_time 的行必然全部排在第 page_size 行之前,故
[start_time, page_end_time) 内的总行数(未筛选)≤ page_size − 1。
这正是第二轮敢不加 LIMIT 的依据;同时说明"区间静态"前提是承重墙——
若第一、二轮之间有新记录落入当前区间,该上界即失效。


避免慢查询的关键点

手段作用
第一轮只查 time 单字段最小化回表,纯索引覆盖
第一轮外层 MAX 聚合只回 1 行,最小化传输/日志/客户端内存
LIMIT page_size 限制游标扫描行数防止游标查询本身变慢
第二轮时间范围约束把扫描区间压到最小
page_end_time != start_time 时加 ORDER BY time引导走 time 索引 range scan、免 filesort(需强制时用 FORCE INDEX)
page_end_time == start_time 时用 =ref 访问,不走 range,不依赖排序
每页扫描行数有上限不会因数据增长导致单页查询变慢

边界场景处理

场景处理
第一轮返回 NULL(区间内无数据)有 end_time 时置 start_time = end_time 结束;无上界时直接终止
情况 B(同一时间点数据 ≥ page_size)全取该时间点后游标 +1 个精度单位(见 ② 情况 B),不可推进到自身
一秒内数据量超过 10000时间字段精度升级为毫秒(DATETIME(3)),逻辑不变
需要顺序稳定指同一时间值内的行序:情况 B 加 ORDER BY id(整体 time 非降序天然成立)
需要严格不重不漏游标改为 (time, id) 联合,用 time > ? OR (time = ? AND id > ?)
其他业务条件选择性极低考虑联合索引 (time, 高选择性字段)
查询期间区间内可能新增写入违反前提;须保证新记录 time 始终落在游标已推进位置之后(见下注:"只写当前时刻"并不自动满足),否则改用一致性快照或 (time, id) 联合游标

并发写入的滞后要求:“只写当前时刻"并不自动满足"新记录落在游标之后”。
反例:无 end_time 的扫描追到当前秒 T 时,情况 B 全取秒 T 后游标推进到 T 的
下一格;同一秒内随后写入的行(time = T,已落后于游标)被漏掉,且第一轮
查空后循环直接终止。安全条件是游标始终滞后写入前沿至少 1 个精度单位:
只扫历史区间,或让扫描上界与写入时刻之间留出足够的滞后窗口。


第一轮取游标方案对比

第一轮的职责是算游标:候选区间内 time 最小的 page_size 行中最大的 time
(即第 page_size 行的 time),记为 page_end_time。应用侧只需要这一个值,
但 SQL 有四种写法,正确性、代价与空结果语义各不相同。

公共内层(候选页)

SELECT time
FROM t
WHERE time >= :start_time
  AND time < :end_time   -- 可选;无上界时省略此行
ORDER BY time ASC
LIMIT :page_size;

四种方案都围绕这个内层展开。

候选方案

方案第一轮写法应用拿到服务端代价空结果语义结论
1公共内层,应用取结果集最后一条page_size 行纯覆盖索引扫描结果集为空可用,次优
2公共内层外包 MAX(time)1 行覆盖索引扫描 + 派生表物化 + 聚合值为 NULL采用
3公共内层改为 LIMIT :page_size - 1, 11 行覆盖索引扫描(跳到第 N 条)空 = 剩余不足一页否决
4去掉 LIMIT:SELECT MAX(time) FROM t WHERE ...1 行全区间扫描值为 NULL否决(错误)

方案 2(采用):外层 MAX 聚合

SELECT MAX(time) FROM (
    SELECT time
    FROM t
    WHERE time >= :start_time
      AND time < :end_time   -- 可选;无上界时省略此行
    ORDER BY time ASC
    LIMIT :page_size
) AS page;
  • 语义:升序序列的最后一条 = 页内最大值,MAX(time) 与"取最后一条的 time"恒等,
    方案性质(不重不漏、情况B触发判断、第二轮 ≤ page_size − 1 上界)完全不变
  • 收益:
    • 结果集 1 行 1 列——应用日志/调试不必面对 page_size 个时间值,
      传输与客户端内存开销最小
    • MAX 把"取这页的最大时间"的意图显式写进 SQL,自解释;“取最后一条”
      需要读者自行推出"升序末位 = 最大值"
  • 代价:内层含 LIMIT,派生表无法 merge 进外层(MySQL 优化器规则),
    须物化 ≤ page_size 行到内存临时表再聚合;页 1000 量级无感知
  • 兼容性:MySQL 5.x / 8.0、MariaDB 均支持;派生表必须带别名(AS page)
  • 注意:空结果判断从"结果集为空"改为 page_end_time IS NULL
    (time NOT NULL 保证 NULL 只来自空集)

方案 1(次优):应用取结果集最后一条

直觉写法:SQL 原样返回 page_size 行,应用代码取最后一行的 time。
正确性与方案 2 相同(升序末位即最大值),索引扫描量也相同。不如方案 2 之处:

  1. 应用拿到 page_size 行却只用 1 个值:日志/调试要面对上千个时间值,
    网络传输与客户端内存白费
  2. "最后一条 = 最大时间"的等价关系压在应用代码里,SQL 本身不自解释

方案 3(否决):LIMIT :page_size - 1, 1 只取第 N 条

把内层改为 ORDER BY time ASC LIMIT :page_size - 1, 1,直接返回第 page_size
行:同样只回 1 行,索引扫描量与方案 2 相同,且无派生表物化。否决理由:

  1. 空结果语义二义:返回空既可能是"区间内无数据",也可能是
    “剩余 1 ~ page_size − 1 条、不足一页”——后者 page_end_time 应取实际最大值,
    须再补一次 MAX 查询兜底。方案 1/2 中空结果只有一种含义,判断简单可靠
  2. 可读性:page_size − 1 的 offset 是魔法数字,读者须推演
    “跳过前 N−1 条、取第 N 条”,边界值稍错即引入重复或遗漏且不易察觉

方案 4(红线,否决):去掉 LIMIT 查全区间 MAX

SELECT MAX(time)
FROM t
WHERE time >= :start_time
  AND time < :end_time;

这不是"第一页的端点",而是整个剩余区间的端点,一旦这么写:

  • page_end_time 一步跳到区间末端,情况A第二轮 [start_time, page_end_time)
    一次扫出全部剩余行——"每页扫描行数有上限 / O(log n + k)"的核心性质被破坏,
    等价于不分页
  • 情况B(page_end_time == start_time)只有"全区间数据挤在同一个时间点"时
    才可能触发,正常永远走不到,密集秒保护失效
  • 方案 1/2/3 的全部性质(含"第二轮 ≤ page_size − 1"的证明)都建立在
    "page_end_time 是第 page_size 行的 time"之上,去掉 LIMIT 前提即不成立

结论

第一轮采用方案 2:内层"升序 + LIMIT page_size"圈定候选页,外层 MAX
聚合只回 1 行。ORDER BY 与 LIMIT 是两根都不可省的承重柱——前者保证取的是
“最小的 page_size 条”,后者保证分页性质本身。


情况B方案对比

第一轮查到的 page_size 条时间数据全部相同时(page_end_time == start_time,
说明该时间点数据量 ≥ page_size),第二轮为什么要用
“等于这个时间查询 + 游标推进 1 个时间精度单位”,而不是别的做法。
对应 ② 情况 B(取数据并推进游标)。

问题定义

第一轮(取游标):

SELECT MAX(time) FROM (
    SELECT time
    FROM t
    WHERE time >= :start_time
      AND time < :end_time   -- 可选;无上界时省略此行
    ORDER BY time ASC
    LIMIT :page_size
) AS page;

返回的 page_end_time == start_time:升序 + >= start_time,页内最大值等于
区间起点,意味着返回的 page_size 条全部落在该时间点。
此时两个问题必须分别回答:

  1. 本轮怎么取数(第二轮 SQL)
  2. 下一轮从哪开始(游标推进)

为什么不能沿用情况A的范围查询

情况A第二轮条件是 time >= :start_time AND time < :page_end_time。
情况B时 page_end_time == start_time,区间 [start_time, start_time) 是空集,
照抄情况A一条数据也查不出来。所以第二轮必须换条件:要么"等于",要么"单位区间"。

候选方案

方案第二轮取数游标推进正确性索引路径结论
1等于 time = :start_timestart_time = page_end_time(未推进)死循环 + 重复输出ref否决
2等于 time = :start_timestart_time + 1 个时间精度单位不重不漏ref(最优,免排序)采用
3范围 time >= :start_time AND time < :start_time + 1 单位同方案 2不重不漏range + 需 ORDER BY正确但次优

方案 1(否决):只用等于查询,游标不推进

直觉:“第二轮用等于查询已经把该时间点全部取完了,游标照旧推进到 page_end_time 即可。”

错误:page_end_time == start_time,推进 start_time = page_end_time 后游标原地不动:

  • 下一轮取游标查询 time >= :start_time LIMIT :page_size 取到的还是同一批数据
  • page_end_time == start_time 再次成立,再次进入情况B
  • 无限循环,且每轮都重复输出同一时间点的全部数据

实测(863 行的表、单秒 130 条、page_size=50):不推进时迭代 3000 次保护上限内
已输出约 39 万行重复数据,仍不终止。

结论:"等于查询"与"游标推进"是互补的两半,不是二选一:
等于查询负责本轮取数,+1 推进负责跳过已取的时间点。只有等于查询而不推进 = 死循环。

方案 2(采用):等于查询 + 游标推进 1 个时间精度单位

-- 第二轮:取数
SELECT *
FROM t
WHERE time = :start_time
  AND {其他业务条件};
-- 推进(该轮已处理区间为单点 [start_time, start_time],右闭)
start_time = start_time + 1 个时间精度单位

为什么正确:

  • 不漏:等于查询已取完该时间点的全部行,下一轮从 start_time + 1 单位 起,
    该点已无未处理数据
  • 不重:下一轮 time >= start_time + 1 单位 不会再命中该点
  • 推进量恰好越过已处理区间的右端点(右闭区间 → 推进到右端之外),这是游标分页的
    统一不变式:推进位置 = 已处理区间右端(开)或右端 +1 格(闭)

为什么最优:

  • time = 常量 走索引 ref 等值定位,直接命中该点全部行,无需排序
    (该点数据 ≤ 10000 条为前提)
  • 等值条件不引入"时间单位"概念,不存在精度换算

精度匹配要求:+1 的单位必须等于 time 字段的最小精度——
DATETIME(0) 用 +1s,DATETIME(3) 用 +1ms。单位大于字段精度会漏数据,
小于字段精度会产生空轮(不影响正确性,影响效率)。

方案 3(次优):范围查询 [start_time, start_time + 1 单位) + 推进

SELECT *
FROM t
WHERE time >= :start_time
  AND time < :start_time + 1 单位
  AND {其他业务条件}
ORDER BY time ASC;

正确性与方案 2 等价(区间恰好覆盖该时间点),但不如方案 2:

  1. 索引路径:范围条件走 range 扫描且通常要带 ORDER BY time 保证顺序;
    等值条件是 ref 定位,路径更直接、免排序
  2. 精度耦合:把"一个时间点"表达为"长度为 1 个精度单位的区间",隐含要求
    推进单位 = 字段最小精度,写错单位即漏/重;方案 2 的取数条件(等于)本身
    不涉及单位,风险只集中在推进这一处
  3. 可读性:time = :start_time 直白表达"取该时间点全部",无需读者自行
    推演"区间 [t, t+1) 恰好覆盖一个秒精度的点"

结论

情况B采用 “等于这个时间查询(本轮取数) + 游标推进 1 个时间精度单位(下一轮起点)”:

关注点由谁解决只取一半的后果
本轮取数等于查询 time = :start_time(ref 最快、免排序)不推进 → 死循环 + 重复输出(方案1)
跳过已处理时间点游标 +1 个精度单位(越过右闭区间右端)换成范围查询 → 正确但次优(方案3)

keyset 翻页方案对比

(time, id) keyset 是另一种游标分页:游标为 (last_time, last_id) 二元组,
按 (time, id) 全序取页:

SELECT *
FROM t
WHERE (time > :last_time OR (time = :last_time AND id > :last_id))
  AND {其他业务条件}
ORDER BY time, id
LIMIT :page_size;

两者同属游标分页,都避开 OFFSET,总扫描量同级,查询效率不是本质差异:
朴素 keyset 把过滤条件放进定位查询,单条查询扫描量随过滤命中率退化,但该
问题可以移植本方案的"两轮定界"技巧解决——第一轮不带业务条件按 (time, id)
取 page_size 行定界,第二轮限定元组区间取数,单条查询同样有上界(且
(time, id) 全序无并列,连情况 B 都不存在)。本质差异在索引依赖、
游标形态与批次语义。

本方案的优势

1. 不依赖 (time, id) 序(核心差异)

本方案的排序与游标只用 time 值,不需要 (time, id) 序——既不需要显式创建
(time, id) 联合索引,也不依赖"单列 idx(time) 在 InnoDB 中实际对应
(time, 主键)"这一实现细节:

  • keyset 走单列 idx(time),靠的是 InnoDB 二级索引隐含主键后缀 + 优化器对
    扩展索引的识别(MariaDB 10.0.36 实测 key_len=16,两列均进入区间)——这是
    实现层面的隐式约定而非显式契约;若要摆脱该依赖,须显式建 (time, id)
    联合索引,多一份索引维护成本
  • 依赖隐含后缀意味着索引形态演化即退化:实测把 idx(time) 改建为
    (time, biz_status) 后(MariaDB 10.0.36),keyset 的 ORDER BY time, id
    出现 filesort,仅当过滤列恰为等值条件时例外;本方案在同一次实测中仍是
    range 访问、免 filesort,且多了索引内过滤。主键定义变更(组合主键、换
    主键列)对本方案无影响。退化表现为变慢而非报错
  • 本方案只要求"time 在索引最左前缀"(见"前提与限制"注):time 之后加列、
    换列均不影响正确性与执行路径。若 time 被移出最左列,两个方案都不可用

2. 游标是单值 time

断点续跑只需持久化一个时间值,按时间段重放天然幂等,游标本身可读可监控
(“处理到 12:00:00”)。keyset 需原子保存 (time, id) 一对,id 对运维不可读。

3. 批次按时间区间对齐

每轮输出一个完整 [start_time, page_end_time) 区间或单个时间点,与按时间窗
对齐的下游聚合、按时间段重跑吻合。keyset 的页边界是行数边界:一页可跨多个
时间值、一个时间值可跨多页,无法回答"这批数据属于哪个时间段"。

4. 谓词简单

本方案两轮均为单列区间/等值谓词。keyset 的定位谓词是元组 OR 形式;若再借
两轮定界,第二轮须两个元组区间相交,谓词复杂度进一步上升。

keyset 的优势(如实列出)

  • (time, id) 全序、无并列:不存在"同一时间点行数 ≥ page_size"的情况
    B,无需单点全取,无"一秒内数据量 ≤ 10000"前提,无需精度升级
    DATETIME(3)
  • 每页恰好 page_size 行(本方案情况 A ≤ page_size − 1)
  • 朴素形态每页一条 SQL(本方案两条)。实现负担各在一处:本方案须正确
    处理情况 A/B 推进——漏写情况 B 推进会死循环(实测复现:游标推进到表尾时
    剩余行全落在同一时间点,page_end_time == start_time 使第二轮空区间查询
    永远返回 0 行且游标原地踏步,即"情况B方案对比"章方案 1 的失败模式);
    keyset 的负担在元组谓词与索引依赖
  • 并发同秒写入略宽容:游标未离开秒 T 期间,同秒后插入(id 更大)的行仍
    可见;本方案情况 B 全取 +1s 后,同秒后到的写入立即落后于游标

选型结论

  • 只维护 time 最左前缀索引、需要单值断点续跑、按时间窗对齐批处理 →
    本方案
  • 已有显式 (time, id) 索引且索引形态不会演化、追求无并列分支的推进逻辑 →
    keyset

验证

配套验证脚本(两个文件):

  • create_table.sql:验证表 DDL,time 单列索引 + 自增主键,每次运行前 DROP 重建
  • time_cursor_paging_verify.py:乱序插入数据,逐场景与"全量基准查询"比对,
    断言集合一致、无重复、按 time 非降序

实测环境:本地 MariaDB 10.0.36。数据为乱序插入 863 行,覆盖连续秒、密集秒
(单秒 130/80 条,超过页大小触发情况B)、空时间段、整秒被业务条件滤空、
非整除页大小等场景;page_size 覆盖 100000 / 50 / 7 / 3 / 1。

实测结果:15 个场景全部通过,与全量基准集合完全一致(不重不漏)。

另外验证了第一轮两种取游标写法的等价性:外层 MAX 聚合与"取结果集最后一条"
各跑一遍 15 个场景,全部通过且各场景页数逐场景一致
(全范围/页50 均为 20 页、密集秒单点均 1 页、页1 均 100 页)。

索引形态补充实测(20 万行表、EXPLAIN):

  • 联合索引 (time, biz_status):第一轮内层走索引有序扫描且覆盖(LIMIT 提前
    终止);情况 A 为 range 访问、业务条件纳入索引(key_len 含第二列,行数估算
    低于单列索引);情况 B 为 ref 且命中两列。均无 filesort
  • 反例 (biz_status, time)(time 不在最左列):第一轮内层退化为全索引扫描 +
    filesort(LIMIT 提前终止失效)、情况 B 等值查询退化为全表扫描,成本模型不成立
  • 数百行小表上,情况 A 无论单列还是联合索引都可能被优化器选全表扫描 + filesort,
    属表规模成本现象(大表无 hint 自然走索引),需钉死路径时用 FORCE INDEX

小结

  • 时间游标分页每页扫描行数有上限(第一轮 page_size、情况 A 第二轮
    ≤ page_size − 1、情况 B 单点全取 ≤ 10000),不随总数据量和页码退化
  • 正确性的关键是游标推进规则:情况 A 推进到 page_end_time(已处理区间右开),
    情况 B 单点全取后推进 1 个精度单位(右闭)
  • 第一轮取游标用"升序 LIMIT + 外层 MAX"只回 1 行;
    内层 ORDER BY 与 LIMIT 都不可省
  • 正确性依赖"区间静态"前提,并发写入须满足游标滞后于写入前沿(见"边界场景处理")
  • 与 (time, id) keyset 翻页总扫描量同级、定界技巧可互通,本质差异在索引
    依赖:本方案不依赖 (time, id) 序(无需显式联合索引,也不依赖单列索引
    隐含的主键后缀),索引形态演化不退化;keyset 全序无并列、无单点数据量
    上限(见"keyset 翻页方案对比")
MySQL 社招必考题:如何优化LIMIT分页? 大家好,我是小米,31岁,依旧是个爱折腾数据库的程序员。今天想跟大家聊聊我在一次社招面试中遇到的一个问题 —— “MySQL LIMIT 分页怎么优化?” 阅读详情

相关推荐

MySQL实战:基于游标与覆盖索引的高效分页技巧解决分页查询卡顿的问题

摘要: 传统分页查询在大数据量下性能急剧下降(如全表扫描、临时排序),导致高延迟。本文提出两种核心优化方案: 游标分页:基于有序唯一值定位,避免遍历历史数据,实测百万级数据分页从秒级降至毫秒级; 覆盖索引分页:通过索引覆盖与延迟关联技术减少回表查询,结合分区表、读写分离等架构优化。 组合方案(游标+延迟关联)可进一步提升性能,适用于电商订单、日志分析等高频场景,实现扫描行数从10万行降至10行的突破性优化。

IT技术分享社区 946

MySQL 大数据量分页查询优化实战:从 90秒到 965毫秒的性能飞跃

在日常开发中,我们经常需要对数据库中的数据进行分页展示。特别是当表数据量达到几十万甚至上百万级时,传统的 `LIMIT` 分页方式会面临严重的性能瓶颈。今天,我将分享一个真实的性能优化案例,通过模拟大页码查询的现场,从90秒缩短到 965 毫秒,显著提升了查询效率。本篇文章将从问题出现的原因、索引原理、优化思路和最终实战效果等方面,为你全面讲解如何高效处理 MySQL 大数据分页查询问题。

qq_46371374的博客 2728

关于MySQL深分页的问题及优化方案

本文探讨MySQL深分页(如LIMIT 1000000,100)的性能问题及优化方案。传统分页随着offset增大,会产生大量数据扫描、锁竞争和I/O瓶颈。主要优化策略包括:1)基于主键游标分页避免offset;2)延迟关联减少数据扫描;3)覆盖索引避免回表;4)分区表缩小查询范围;5)缓存高频访问数据;6)业务层限制最大页数或预加载。核心思路是通过减少数据扫描量和避免全表offset来提升性能,建议根据业务场景组合使用游标分页、延迟关联等方案,并配合索引优化和业务逻辑调整。

weixin_50055999的博客 1594

mysql大数据量分页查询性能优化

mysql数据量比较大的时使用分页,随着页码的增加,查询性能随之大幅下降,分享下个人工作中的解决方法 。仅供参考,不喜勿喷......

weixin_46231609的博客 445

Python处理MySQL大数据量:分页查询与性能优化

本文面向需要处理MySQL大数据量表分页查询的Python开发者,重点解决“百万级数据分页慢”的核心问题。覆盖传统分页的性能瓶颈分析、4种优化方案(索引分页/覆盖索引/游标分页/预计算分页)的原理与实现,以及Python代码实战。本文从“图书馆找书”的生活案例引入分页原理,逐步拆解传统的性能痛点,通过代码+执行计划分析优化方案,最后结合电商商品列表场景给出完整实战案例。传统分页简单但低效,OFFSET越大越慢。索引分页:基于自增主键或有序索引,通过快速定位,时间与页数无关。覆盖索引。

AI天才研究院 1277

mipi deskew原理理解

何为skew, 从字面意思看是倾斜了,不对齐了;放在mipi的传输中意思就是clock lane和data lane 及data lane与data lane间出现了相位差;一旦出现相位差,大家采样数据时就可能不在同一个节拍上,会出现传输错误。所以就要做deskew,通过做deskew来使得大家重新对齐,在同一个节奏下传输数据. 一般当data rate高于1.5G时建议开启deskew。

lx123010的专栏 8102

MySQL 的分页查询优化:游标分页 vs LIMIT OFFSET

在 Web 应用中,分页查询是非常常见的需求,如 **文章列表、商品列表、用户评论** 等。当数据量较小时,`LIMIT OFFSET` 分页方案可以正常工作,但当数据量**上百万甚至上亿**时,`LIMIT OFFSET` 可能会导致 **性能下降**,查询速度变慢。本篇文章将介绍 **分页查询的常见方案**,分析 **`LIMIT OFFSET` 的性能问题**,并提供 **游标分页优化方案**,帮助开发者提升 MySQL 查询性能。

1808

从零开始用飞牛fnas搭建集成DeepSeek-R1的远程AI工作台

从零开始用飞牛fnas搭建集成DeepSeek-R1的远程AI工作台

AI+全栈,AI+应用,实操落地,前沿技术 3808

大数据量下分页查询性能优化实践(SpringBoot+MyBatis-Plus)

大数据量分页查询优化方案 分页查询是高频需求,但数据量大时传统方案存在性能瓶颈。本文基于SpringBoot+MyBatis-Plus技术栈,从三个层级提供解决方案: 初级方案:MyBatis-Plus原生分页,适合万级数据,但count查询和limit offset在数据量大时性能下降明显。 中级优化:通过禁用count查询、直接使用limit语句提升十万级数据场景的性能,需注意SQL注入风险。 高级方案:游标分页(基于唯一键)解决百万级以上数据问题,性能稳定但仅支持顺序分页

Good good study,day day up 1997

基于python的电商评论文本分析

本文基于京东平台安井手抓饼2000余条评论数据,运用Python文本分析技术(包括分词、词频统计、LDA主题模型等)探究消费者评价特征。研究发现:1)产品评价聚焦口味("好吃")、性价比("一分钱")和便捷性("早餐");2)京东物流服务获得高度认可;3)销售旺季集中在11月至次年1月。分析揭示了速冻食品消费者的核心关注点,为商家优化产品与服务提供数据支持,同时展示了文本挖掘技术在电商评论分析中的实用价值。

weixin_50902344的博客 8347

NMS非极大值抑制算法原理

NMS非极大值抑制算法

pywin 3501

‌MySQL快速分页查询优化方案

分页优化,慢查询

hellotutu的博客 962

ic后端学习笔记-floorplan

floorplan是布图规划,规划包括定义芯片的大小,定义芯片内各种模块、hard macros(硬核。比如RAM,ROM及其他IP模块等)、IO等的大小和位置,有时候还需要提前定义一些placement blockages,groups/regions等等,是后端设计的第一步,也是后续所有P&R工作的基础。floorplan与placement的区别是。

tfrofc的博客 5831

SQL 实战:分页查询的多种方式对比与优化

然而,在数据量较大或复杂查询中,简单的分页方式可能导致性能下降。本文将探讨 SQL 中实现分页查询的不同方式,并对比它们的优缺点,帮助优化大数据量场景下的分页方案。在处理大数据表时,分页查询是非常常见的需求。分页不仅可以提高用户体验,还能有效减少数据库查询返回的数据量,避免一次性加载大量记录引起的性能瓶颈。查询订单表第 21-40 条记录,同时返回总记录数,用于前端分页展示。查询订单表中,按订单金额降序排列的第 51-60 条记录。查询比上次加载时间更早的订单记录,每次加载 20 条记录。

秋元的博客 1847

避坑指南:PyCINRAD处理CINRAD雷达数据时遇到的5个典型报错及解决方案

本文针对使用PyCINRAD库处理CINRAD雷达基数据时常见的5个典型报错,提供了详细的解决方案。内容涵盖环境配置、数据读取、PPI可视化及性能优化等核心环节,重点解决了版本兼容性、数据异常处理和中文乱码等实际问题,帮助用户高效处理雷达基数据并生成准确的PPI图像。

cc789的博客 1068

Qt入门教程【环境搭建篇】Qt6+Mingw+QMake+QtCreator

Qt Windows平台安装

小鱼酱的博客 2万+

Bun ORM分页查询实现:游标分页与偏移分页对比

在现代Web应用中,高效的数据分页是提升用户体验和系统性能的关键技术。Bun ORM作为一款SQL-first的Golang ORM框架,提供了两种主流的分页实现方式:游标分页(Cursor Pagination)和偏移分页(Offset Pagination)。本文将深入分析这两种分页技术的原理、实现方式、性能对比以及适用场景。 ## ???? 分页技术对比概览 | 特性 | 游标分页 | 偏移...

gitblog_00956的博客 1136

从零搭建 C# + YOLO 工业视觉系统:从模型训练到上位机部署(工业级落地)

做工业质检、自动化生产线的同学都懂:一套能用的工业视觉系统,既要“检测准”,又要“界面易操作”,还要“部署稳”。之前帮某电子厂做PCB缺陷检测,一开始用Python+YOLO做检测核心,QT做上位机,结果踩了无数坑:Python推理速度波动大,QT和Python跨语言集成繁琐,数据传递容易卡顿;后来换成C# + YOLO的组合,直接打通了“模型训练-上位机开发-工业部署”全流程——C#的WinForms/WPF开发上位机高效,工业相机SDK支持完善;

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 737

MySQL中对大量数据进行分页查询的优化策略

MySQL大数据量分页查询优化策略:传统LIMIT分页在大数据量下性能急剧下降,本文提出八大优化方案。覆盖索引+延迟关联可提升性能20倍以上,书签记录法适合连续分页,分区表优化减少无效扫描,游标分页适用于逐行处理,汇总表预计算提升聚合查询效率。最佳方案需结合具体场景选择,如中小数据量优先使用覆盖索引,超大数据量建议分布式中间件。通过合理优化,分页查询性能可提升10-20倍。

山路曲折盘旋,但毕竟朝着顶峰延伸 1054

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

MySQL8分页查询方法总结

方法语法优点缺点适用场景标准分页简单,通用,支持跳页深分页性能差大多数普通分页需求简化写法简洁参数易混淆,不推荐不推荐使用窗口函数+ 子查询解决复杂分组排名问题语法复杂分组Top-N、复杂分页逻辑游标分页性能极佳不支持随机跳页无限滚动、连续翻页对于简单的后台分页,使用 ORDER BY。对于数据量巨大的 C 端应用(如微博、新闻流),强烈推荐使用游标分页。对于需要复杂分组排序的分页,使用窗口函数。

ITHomeZSL的博客 823

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

JDBC--05--MySQL大数据量操作---常规查询、游标查询、流式查询,

MySql JDBC 大数据量操作 Java Web 程序里,能够被称为大数据量的,几十万到千万不等,再高的话 Java(WEB 应用)处理就不怎么合适了 举个例子,现在业务系统需要从 MySQL 数据库里读取 500w 数据行进行处理,应该怎么做? 常规查询,一次性读取 500w 数据到 JVM 内存中,或者分页读取 流式查询,建立长连接,利用服务端游标,每次读取一条加载到 JVM 内存 游标查询,和流式一样,通过 fetchSize 参数,控制一次读取多少条数据 常规查询 默认情况下,完整的检索

时光里的博客 5556

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

如何在面试中完美回答SQL分页查询问题?这4种方案你必须掌握

掌握SQL分页查询的4种高效方案,轻松应对SQL面试高频题。涵盖LIMIT OFFSET、ROW_NUMBER()、游标分页与键值分页,解析各自适用场景与性能优势,助你写出稳定高效的分页SQL,值得收藏。

CompiGap的博客 1011

达梦 DM8 LIMIT 分页全解析:四种分页语法对比、大数据量瓶颈突破与游标分页实现

达梦DM8数据库分页查询优化研究 摘要:本文深入分析达梦DM8数据库的四种分页实现方式(TOP、LIMIT/OFFSET、FETCH NEXT、ROWNUM),揭示LIMIT分页在大数据量场景下的性能瓶颈原理。通过建立"分页查询响应时间=基础查询时间+排序成本+OFFSET处理成本+数据传输成本"的性能模型,指出OFFSET值增大导致的性能劣化问题。研究提供了EXPLAIN分析方法和基准测试方案,对比不同分页语法的执行效率,为达梦数据库的高效分页查询提供优化方向。测试数据显示,LIMIT

本博客聚焦 YOLOv11 全流程落地,涵盖架构优化、数据集处理、训练技巧与多场景部署,兼及国产数据库、Java 开发与 AI 模型应用,内容兼顾理论与工程实践,为开发者提供系统干货,助力高效提升技术能力。 2494

MySQL/Mongo大数据量分页查询优化

分页的2种写法 SELECT keyword FROM keyword_rank WHERE advertiserid='59' order by keyword LIMIT 2 OFFSET 1; 比如这个SQL ,limit后面跟的是2条数据,offset后面是从第1条开始读取 SELECT keyword FROM keyword_rank WHERE advertiserid=...

hudeyong926的专栏 1347

MyBatis 和 MyBatis Plus 分页查询大数据量的性能优化技巧

MyBatis/MyBatis Plus大数据分页优化摘要 针对百万级数据量分页查询的性能瓶颈,本文总结了以下优化方案: 游标分页:基于ID连续查询,避免OFFSET扫描,性能稳定但无法跳页 延迟关联:子查询先获取ID再关联详情,利用覆盖索引减少回表 COUNT优化:缓存结果/使用近似值/异步查询,避免频繁全表统计 索引设计:为分页字段创建复合索引,确保排序和条件字段顺序合理 分页插件配置:MyBatis Plus中设置最大限制、COUNT SQL解析优化等 典型场景建议: 移动端无限加载使用游标分页 后台

11年java老开发 940

MySQL Limit 深分页优化方案

摘要:MySQL的LIMIT深分页查询存在性能问题,数据量越大查询越慢。本文分析了其原理,并提供了5种优化方案:1)延迟关联+子查询优化;2)基于ID游标翻页;3)覆盖索引优化;4)记住上次分页条件;5)数据冗余+缓存。性能对比显示游标翻页最优(0.01s),但不能随机跳页。建议优先使用游标翻页,随机跳页时采用延迟关联,超大数据量配合缓存或搜索引擎。核心优化思路是减少无效扫描,避免OFFSET,结合缓存应对极限场景。

xiaomiemie6的博客 1254
上一篇: Claude Code人工确认与任务结束提醒配置
下一篇: 浏览器自动化——AI 生成网页操作脚本
adrninistrat0r
博客等级 码龄18年 395粉丝 136原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值