【后端开发】一次把 MySQL 深分页讲透:从 limit 1000000,10 到游标分页的工程化改造
🔥 个人主页:铁皮哥(欢迎关注)
📌 作者简介:28届校招生,后端开发/Agent 方向在学
📚 学习内容:Java、Python、计算机视觉、大语言模型、Agent开发
📝 专栏内容:从零开始的Claude Code零代码生活(持续更新中)
✨不只背八股,更想搞懂为什么这样设计
前言
假设我们有一张订单表,里面已经有 1000 万条数据。后台管理系统需要支持分页查询订单列表,最开始的 SQL 可能会写成这样:
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
从语义上看,这条 SQL 很简单:按照 id 排序,从第 1000000 条之后开始,取 20 条数据。
但问题也出在这里。
很多人第一次看到这条 SQL 时,容易下意识觉得:既然最后只返回 20 条数据,那查询成本应该也只和这 20 条数据有关。
可实际上,MySQL 并不是直接“跳到”第 1000000 条记录,然后取后面的 20 条。它需要先按照排序规则找到前面的 1000020 条记录,再丢掉前面的 1000000 条,最后才把剩下的 20 条返回。
这条 SQL 真正昂贵的地方,不是返回了多少数据,而是为了返回这 20 条数据,数据库需要先扫描并丢弃大量无用数据。
如果查询还是 SELECT *,情况会更糟。因为 MySQL 可能不仅要扫描索引,还要根据主键回表读取完整行数据。随着 offset 越来越大,这部分无效扫描和回表成本也会越来越高。
这就是深分页问题最核心的矛盾:
用户只想看 20 条数据,但数据库可能已经为此处理了上百万条记录。
一、复现深分页问题
这里我用一张订单表来模拟真实业务里的列表查询场景。
1.1 准备测试表
先创建一张 orders 表。
为了更接近实际业务,这张表不会只保留一个 id 字段,而是加上用户、订单状态、金额、创建时间、更新时间等字段。
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
status TINYINT NOT NULL COMMENT '订单状态',
amount DECIMAL(10,2) NOT NULL COMMENT '订单金额',
created_at DATETIME NOT NULL COMMENT '创建时间',
updated_at DATETIME NOT NULL COMMENT '更新时间',
INDEX idx_created_at (created_at),
INDEX idx_user_created (user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
这张表里有几个比较关键的点。
id 是自增主键,用来模拟最常见的主键分页场景。
created_at 表示订单创建时间,很多后台列表都会按照时间倒序查询,所以这里也单独给它建了一个索引。
user_id + created_at 是一个组合索引,用来模拟“查询某个用户的订单列表”这种业务场景。
1.2 准备测试数据
接下来需要往表里插入一批测试数据。
数据大致满足下面几个特点:
user_id:随机分布,模拟不同用户下单
status:在几个订单状态之间随机
amount:随机金额
created_at:分布在最近一年
updated_at:在 created_at 之后随机生成
可以用存储过程、Java 脚本、Go 脚本或者 Python 脚本批量插入。这里不展开完整造数脚本,只给一个大概思路。
伪代码类似这样:
INSERT INTO orders(user_id, status, amount, created_at, updated_at)
VALUES
(10001, 1, 99.90, '2026-01-01 10:00:00', '2026-01-01 10:05:00'),
(10002, 2, 199.00, '2026-01-01 10:01:00', '2026-01-01 10:06:00'),
...
;
实际插入时不要一条一条插,否则会非常慢。可以采用批量插入,比如每批插入 5000 条或 10000 条。
如果用 Java 或 Python 脚本造数,核心逻辑大概是:
循环生成订单数据
每 5000 条拼接成一批 INSERT
提交一次事务
重复执行,直到插入 1000 万条
这里有一个小细节:造数的时候最好让 created_at 有一定重复概率。
比如同一秒内生成多条订单。这样后面讲 created_at + id 复合游标分页时,才更容易解释为什么不能只用 created_at 做 cursor。
1.3 先看普通分页查询
数据准备好之后,我们先执行几条普通分页 SQL。
第一条是浅分页:
SELECT *
FROM orders
ORDER BY id
LIMIT 20, 20;
第二条是中等 offset:
SELECT *
FROM orders
ORDER BY id
LIMIT 100000, 20;
第三条是深分页:
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
从 SQL 语义上看,这三条语句都只返回 20 条数据。
但它们的区别在于 offset 不同。
| SQL | offset | size | 含义 |
|---|---|---|---|
LIMIT 20, 20 |
20 | 20 | 跳过 20 条,返回 20 条 |
LIMIT 100000, 20 |
100000 | 20 | 跳过 10 万条,返回 20 条 |
LIMIT 1000000, 20 |
1000000 | 20 | 跳过 100 万条,返回 20 条 |
这时候可以打开 profiling 或者直接用程序统计执行耗时。
比如测试结果可能类似这样:
| 查询语句 | offset | 返回条数 | 耗时 |
|---|---|---|---|
LIMIT 20, 20 |
20 | 20 | 5 ms |
LIMIT 100000, 20 |
100000 | 20 | 80 ms |
LIMIT 1000000, 20 |
1000000 | 20 | 700 ms |
这里的数字不一定和你的机器完全一致,因为它会受到很多因素影响,比如机器配置、MySQL 版本、Buffer Pool 大小、数据是否在内存中、磁盘性能等等。
但趋势通常是比较明显的:
offset 越大,查询越慢。
也就是说,哪怕最终都只返回 20 条数据,MySQL 的查询成本也不是固定的。
1.4 用 EXPLAIN 看一下执行计划
接下来可以用 EXPLAIN 看一下 MySQL 对这条 SQL 的执行计划。
EXPLAIN
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
因为这里按照主键 id 排序,MySQL 通常会走主键索引。
你可能会看到类似这样的结果:
type: index
key: PRIMARY
rows: 1000020
Extra:
重点看两个地方。
第一个是 key。
如果 key 是 PRIMARY,说明 MySQL 使用了主键索引。
第二个是 rows。
它表示 MySQL 预估需要扫描的行数。对于:
LIMIT 1000000, 20
来说,MySQL 并不是只扫描 20 行,而是要扫描大约 1000000 + 20 行。
这就能解释为什么深分页会慢。
很多人会有一个误区:既然这条 SQL 走了主键索引,那应该很快。
但实际上,走索引不代表一定快。
如果 offset 很大,即使走的是主键索引,MySQL 仍然需要沿着索引从前往后扫描很多条记录。索引可以让扫描更有序,但它不能让 MySQL 直接无成本地跳过前面的 100 万条数据。
1.5 LIMIT 1000000, 20 到底慢在哪里?
现在我们可以回到最核心的问题:
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
这条 SQL 的执行过程可以粗略理解成这样:
1. 按照 id 顺序扫描数据
2. 找到前 1000020 条记录
3. 丢弃前 1000000 条
4. 返回最后 20 条
用图表示就是:
[1] [2] [3] ... [999998] [999999] [1000000] [目标20条]
└──────────────── 被扫描但最终丢弃的数据 ────────────────┘
所以它慢的关键不是最后返回了 20 条,而是前面那 100 万条也被处理过了。
如果查询语句是:
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
由于查询的是 *,MySQL 需要返回整行数据。
如果排序字段和查询字段不能完全通过索引覆盖,就可能发生大量回表。
这也是为什么深分页里经常会提到两个关键词:
大量扫描
大量丢弃
如果是 SELECT *,还要额外加上:
大量回表
1.6 为什么 MySQL 不能直接跳到第 100 万条?
这里可能会有一个疑问:
既然有索引,为什么 MySQL 不能直接跳到第 100 万条?
原因是,B+ 树索引擅长的是按条件定位,比如:
WHERE id > 1000000
这种查询可以直接利用索引定位到 id = 1000000 附近,然后继续往后扫描。
但是:
LIMIT 1000000, 20
里面的 1000000 不是某个具体的 id 值,而是“跳过 1000000 行”。
这两者是不一样的。
id > 1000000 的意思是:
从 id 大于 1000000 的位置开始查
而 LIMIT 1000000, 20 的意思是:
按照排序结果,跳过前 1000000 行
前者有明确的索引定位条件,后者没有。
所以 MySQL 只能按照排序结果往后数,数够 1000000 行之后,再返回后面的 20 行。
二、四种常见解决方案
上一章我们已经把深分页慢的原因拆开了。
简单来说,LIMIT 1000000, 20 慢,不是因为它返回了 20 条数据,而是因为 MySQL 为了拿到这 20 条,需要先扫描并丢弃前面的 100 万条。
所以优化深分页的核心思路也很明确:
不要让数据库做大量无意义的 offset 扫描。
下面来看几种常见解决方案。
2.1 方案一:主键游标分页
如果列表是按照主键递增顺序展示的,那么最直接的优化方式就是把 offset 分页改成游标分页。
原来的分页 SQL 是这样:
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
这种写法的问题在于,1000000 表示要跳过前 100 万条记录。
MySQL 没办法直接跳过这些记录,它还是要按照 id 顺序往后扫描,扫描到第 1000020 条之后,再丢掉前面的 100 万条。
如果改成游标分页,SQL 可以写成这样:
SELECT *
FROM orders
WHERE id > 1000000
ORDER BY id
LIMIT 20;
这里的 1000000 就不再是 offset,而是上一页最后一条数据的 id。
也就是说,第一页查询:
SELECT *
FROM orders
ORDER BY id
LIMIT 20;
假设第一页最后一条数据的 id 是 100020,那么下一页就可以这样查:
SELECT *
FROM orders
WHERE id > 100020
ORDER BY id
LIMIT 20;
这样 MySQL 就可以直接利用主键索引定位到 id > 100020 的位置,然后继续往后扫描 20 条数据。
相比 LIMIT 1000000, 20,这种方式不需要从头扫描并丢弃大量数据。
对应到接口设计上,也可以从传统的页码分页:
GET /orders?page=50000&page_size=20
改成游标分页:
GET /orders?last_id=100020&page_size=20
接口返回时带上下一个游标:
{
"items": [
{
"id": 100021,
"user_id": 9527,
"status": 1,
"amount": 99.90
}
],
"next_cursor": 100040
}
下次请求时,前端把 next_cursor 传回来,后端继续从这个位置往后查。
这个方案的优点非常明显:
查询成本和页码深度关系不大,只和本次要取多少条数据有关。
它很适合下面这些场景:
订单流水
消息列表
评论列表
操作日志
Feed 流
任务执行记录
不过它也有一个缺点:不适合跳页。
比如用户想直接跳到第 50000 页,游标分页就不太好支持。因为它的设计思路不是“第几页”,而是“从上一次看到的位置继续往后查”。
所以主键游标分页更适合“上一页 / 下一页 / 加载更多”这种交互,而不适合强依赖页码跳转的后台表格。
2.2 方案二:created_at + id 复合游标分页
实际业务里,很多列表并不是按照 id 排序,而是按照创建时间倒序排序。
比如订单列表通常会把最新订单放在最前面:
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 1000000, 20;
这种写法同样会遇到深分页问题。
如果直接改成时间游标,可以写成:
SELECT *
FROM orders
WHERE created_at < '2026-05-01 12:00:00'
ORDER BY created_at DESC
LIMIT 20;
它的含义是:查询创建时间早于某个时间点的 20 条订单。
看起来没问题,但这里有一个很容易被忽略的细节:
created_at不是唯一的。
在高并发系统里,同一秒甚至同一毫秒内都可能产生多条订单。如果只用 created_at 作为游标,可能会出现数据重复或者数据丢失。
举个例子,假设第一页最后一条数据是:
id = 100
created_at = 2026-05-01 12:00:00
但数据库里还有几条数据的 created_at 也是这个时间:
id = 99 created_at = 2026-05-01 12:00:00
id = 98 created_at = 2026-05-01 12:00:00
id = 97 created_at = 2026-05-01 12:00:00
如果下一页查询条件写成:
WHERE created_at < '2026-05-01 12:00:00'
那么这些同一时间的数据就会被跳过。
所以更稳的做法是使用 created_at + id 作为复合游标。
排序也要保持一致:
ORDER BY created_at DESC, id DESC
下一页查询可以这样写:
SELECT *
FROM orders
WHERE
created_at < '2026-05-01 12:00:00'
OR (
created_at = '2026-05-01 12:00:00'
AND id < 100
)
ORDER BY created_at DESC, id DESC
LIMIT 20;
这条 SQL 的含义是:
如果创建时间更早,直接进入下一页;
如果创建时间相同,就继续比较
id,只取id更小的数据。
这样可以保证分页顺序稳定,不容易出现重复或丢数据。
对应的索引可以设计成:
CREATE INDEX idx_created_id ON orders(created_at, id);
如果查询方向是倒序,MySQL 也可以利用这个组合索引来减少扫描成本。
接口返回时,cursor 可以包含两个字段:
{
"items": [
{
"id": 100,
"created_at": "2026-05-01 12:00:00",
"status": 1
}
],
"next_cursor": {
"created_at": "2026-05-01 12:00:00",
"id": 100
}
}
真实项目里,一般不会直接把这个对象暴露给前端,而是做一层编码。
比如把:
{
"created_at": "2026-05-01 12:00:00",
"id": 100
}
编码成一个字符串:
eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMSAxMjowMDowMCIsImlkIjoxMDB9
前端不需要关心 cursor 内部结构,只需要在下一次请求时原样传回来。
GET /orders?cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0wMSAxMjowMDowMCIsImlkIjoxMDB9&limit=20
后端解析 cursor 后,再拼出对应的查询条件。
这种方式非常适合时间线类型的数据,比如:
订单列表
评论列表
消息列表
通知列表
Agent Run 记录
工具调用日志
会话历史
2.3 方案三:延迟关联
前面两种游标分页都很好,但它们有一个共同问题:
不适合任意跳页。
可是有些后台管理系统确实会要求保留页码分页。
比如运营后台、订单后台、财务对账系统,页面上可能就是一个传统表格:
第一页 第二页 第三页 ... 跳到第 N 页
如果产品形态暂时不能改成 cursor 分页,那可以考虑延迟关联。
原始 SQL 是:
SELECT *
FROM orders
ORDER BY id
LIMIT 1000000, 20;
这个写法会直接在大 offset 场景下读取完整行数据。
优化后可以改成:
SELECT o.*
FROM orders o
JOIN (
SELECT id
FROM orders
ORDER BY id
LIMIT 1000000, 20
) t ON o.id = t.id;
这条 SQL 分成两步理解。
第一步,子查询只查 id:
SELECT id
FROM orders
ORDER BY id
LIMIT 1000000, 20
因为 id 本身就是主键索引,所以这一步只需要扫描主键索引,拿到目标页的 20 个主键。
第二步,再根据这 20 个主键回表查询完整数据:
SELECT o.*
FROM orders o
JOIN (...) t ON o.id = t.id;
这样做的好处是:
前面那 100 万条被跳过的数据,只发生在更轻量的索引扫描上,而不是对每一行都读取完整数据。
尤其是当表字段很多、单行数据比较大时,这种优化会更明显。
比如订单表里如果还有很多字段:
收货地址
备注
优惠信息
扩展 JSON
发票信息
物流信息
直接 SELECT * 深分页,成本会比较高。
而延迟关联可以先在索引里找到目标页的主键,再只对最终需要返回的 20 条数据回表。
不过这个方案也不要神化。
延迟关联并没有从根上消灭 offset。
它依然需要扫描前面的 100 万个索引项,只是避免了对前面 100 万行都读取完整数据。
所以它更适合这种场景:
业务必须保留页码分页
表字段比较多
直接 SELECT * 深分页很慢
短期内不方便改成 cursor 分页
如果业务允许改接口,优先级通常还是 cursor 分页更高。
2.4 方案四:覆盖索引
还有一种常见思路是使用覆盖索引。
很多慢 SQL 之所以慢,是因为查询写成了:
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 1000000, 20;
但实际上,列表页可能根本不需要展示所有字段。
比如订单列表只展示:
订单 ID
用户 ID
订单状态
订单金额
创建时间
那就没必要 SELECT *。
可以把查询改成:
SELECT id, user_id, status, amount, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 1000000, 20;
然后设计一个覆盖索引:
CREATE INDEX idx_order_list
ON orders(created_at, id, user_id, status, amount);
覆盖索引的意思是:
查询需要的字段都能从索引里直接拿到,不需要再回表读取完整行数据。
这样可以减少大量回表成本。
如果列表页本来只需要几个字段,但是 SQL 却写成 SELECT *,那就会导致很多不必要的数据读取。
这一点在真实项目里非常常见。
因为一开始数据量小,SELECT * 看起来没什么问题,开发也方便。等数据量上来以后,列表查询越来越慢,再回头看就会发现,很多字段其实根本没有用到。
不过覆盖索引也不是越多越好。
索引本身是有成本的:
会占用更多磁盘空间
插入和更新数据时需要维护索引
索引字段太多会让索引变大
过多索引会影响写入性能
所以覆盖索引更适合那些查询频率高、字段相对固定、性能收益明显的列表接口。
比如订单列表、日志列表、消息列表这类接口,如果它们的查询字段很稳定,就可以考虑用覆盖索引优化。
2.5 几种方案对比
到这里,四种常见优化方案就讲完了。
它们的目标都是减少深分页带来的无效成本,但适用场景不太一样。
| 方案 | 核心思路 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 主键游标分页 | 用 id > last_id 替代 LIMIT offset |
按主键顺序浏览的数据 | 性能好,实现简单 | 不支持任意跳页 |
created_at + id 复合游标 |
用时间和主键共同定位下一页 | 时间线、订单、消息、日志 | 排序稳定,适合真实业务 | 接口和 SQL 都比普通分页复杂 |
| 延迟关联 | 先查目标页主键,再回表查完整数据 | 必须保留页码分页的后台系统 | 兼容原有分页模式,减少回表 | 仍然需要扫描 offset |
| 覆盖索引 | 查询字段全部从索引中获取 | 字段固定的高频列表查询 | 减少回表,提升查询效率 | 增加索引存储和维护成本 |
技术方案没有绝对最优,只有是否适合当前业务。
三、业务层面怎么解决深分页
前面讲的几种方案,主要还是站在 SQL 和索引的角度解决问题。
但在真实项目里,深分页很多时候不只是数据库问题。
比如下面这条 SQL:
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 1000000, 20;
从技术角度看,我们可以用游标分页、延迟关联、覆盖索引去优化它。
但从业务角度看,也应该反过来问一句:
用户真的需要翻到第 100 万条订单吗?
很多性能问题,并不是数据库不够强,而是业务查询方式本身太粗放。
如果产品允许用户在一个几千万数据量的列表里无限制翻页,那数据库迟早会被拖慢。
所以深分页优化不能只靠 SQL,还要从产品交互、接口设计和数据架构上一起处理。
3.1 限制最大页数
最直接的业务方案,是限制最大可翻页范围。
比如后台订单列表最多允许查询前 100 页:
每页 20 条,最多查看前 100 页,也就是最多查看 2000 条数据
如果用户继续往后翻,可以提示:
数据量过大,请缩小筛选范围后再查询
这个做法听起来有点“粗暴”,但在很多后台系统里其实很合理。
因为用户翻到非常靠后的页码时,通常不是为了“浏览”,而是想找某一类数据。
比如运营同学想查:
某个时间段的订单
某个用户的订单
某个状态下的异常订单
某个金额范围内的订单
这时继续让他一页一页翻,体验并不好。更合理的做法是引导他增加筛选条件。
限制最大页数的本质,不是禁止用户查数据,而是避免用户用低效的方式查数据。
这一点在后台系统里尤其重要。
后台系统不像普通用户端页面,很多查询都是直接打到核心业务表上。如果每个人都能随便翻到几万页,数据库压力会非常不可控。
所以我觉得,对于大数据量列表,可以提前定一个规则:
默认只支持查看最近数据
超过一定页数后,必须增加筛选条件
历史数据走专门的查询入口
这比单纯在 SQL 上做优化更稳定。
3.2 引导用户使用筛选条件
深分页出现频繁,往往说明查询范围太大。
比如用户直接查全量订单:
SELECT *
FROM orders
ORDER BY created_at DESC
LIMIT 1000000, 20;
这个查询没有任何过滤条件,只是按时间倒序翻页。
如果订单表有几千万条数据,越往后翻肯定越慢。
更合理的方式是让用户先缩小查询范围。
比如按时间筛选:
SELECT *
FROM orders
WHERE created_at >= '2026-05-01 00:00:00'
AND created_at < '2026-06-01 00:00:00'
ORDER BY created_at DESC
LIMIT 20;
或者按订单状态筛选:
SELECT *
FROM orders
WHERE status = 1
ORDER BY created_at DESC
LIMIT 20;
再或者组合多个条件:
SELECT *
FROM orders
WHERE created_at >= '2026-05-01 00:00:00'
AND created_at < '2026-06-01 00:00:00'
AND status = 1
ORDER BY created_at DESC
LIMIT 20;
这样做有两个好处。
第一,减少数据库需要扫描的数据量。
第二,更符合用户真实的查询意图。
很多时候,用户不是想看“第 50000 页”,而是想找到“5 月份未支付的订单”或者“某个用户最近的订单”。
从这个角度看,筛选条件其实比分页更重要。
分页解决的是“数据怎么分批返回”,筛选解决的是“用户到底想查哪部分数据”。
如果筛选条件设计得好,很多深分页问题自然就不会出现。
在实际项目里,可以优先考虑这些筛选维度:
| 筛选维度 | 示例 |
|---|---|
| 时间范围 | 最近 7 天、最近 30 天、自定义时间段 |
| 状态 | 待支付、已支付、已取消、退款中 |
| 用户维度 | 用户 ID、手机号、用户名 |
| 业务编号 | 订单号、任务 ID、流水号 |
| 金额范围 | 大于 100、小于 1000 |
| 异常类型 | 支付失败、库存不足、调用失败 |
对于后台系统来说,最好不要只给一个大列表,然后让用户从第一页慢慢翻。
更好的设计是:先筛选,再分页。
3.3 用“加载更多”替代“跳到第 N 页”
并不是所有列表都适合页码分页。
比如下面这些场景:
消息列表
评论列表
通知列表
Feed 流
操作日志
Agent 执行记录
工具调用日志
用户通常只关心最近的数据。
这时页面上如果还设计成:
第 1 页、第 2 页、第 3 页、跳到第 10000 页
其实并不符合使用习惯。
更自然的交互是:
下拉刷新
加载更多
继续查看更早记录
对应到后端接口,也可以从传统的 page 模式:
GET /orders?page=50000&page_size=20
改成 cursor 模式:
GET /orders?cursor=xxx&limit=20
这个变化看起来只是接口参数变了,但背后代表的是查询模型的变化。
page + page_size 关心的是:
我要第几页
cursor + limit 关心的是:
从上一次看到的位置继续往后查
对于大数据量列表来说,后者通常更稳定。
尤其是数据还在不断写入的时候,cursor 分页也更容易保证分页结果的连续性。
比如用户正在查看消息列表时,系统又插入了新的消息。
如果用页码分页,第二页的数据可能会因为新数据插入而发生偏移,导致用户看到重复数据或者漏掉数据。
而 cursor 分页是基于上一页最后一条记录继续查,受新增数据影响更小。
所以在设计列表接口时,可以先问自己一个问题:
这个列表真的需要跳到第 N 页吗?
如果答案是否定的,就没必要强行使用页码分页。
3.4 冷热数据拆分和历史数据归档
如果一张表的数据量持续增长,只靠分页优化是不够的。
比如订单表、日志表、消息表、任务执行记录表,都有一个共同特点:
数据一直在写入
最近数据访问频繁
历史数据访问较少
这类表很适合做冷热数据拆分。
比如订单数据可以拆成:
近 3 个月订单:orders_hot
3 个月以前订单:orders_archive
大多数在线查询只查热表:
SELECT *
FROM orders_hot
WHERE created_at >= '2026-05-01 00:00:00'
ORDER BY created_at DESC
LIMIT 20;
如果用户确实需要查询历史订单,再走历史查询入口。
这种设计的好处是,核心在线表的数据量可以控制在一个相对稳定的范围内,查询性能也更容易保障。
对于日志、Agent 执行记录这类数据,也可以采用类似思路。
比如:
最近 7 天日志放在 MySQL 热表
更早日志归档到 ClickHouse / Elasticsearch / 对象存储
所以当数据量越来越大时,不要把所有查询压力都压到一张 MySQL 表上。
分页优化只能缓解问题,数据分层才能从架构上降低压力。
写在文后
期待您的一键三连!如果有什么问题或建议欢迎在评论区交流!
更多推荐

所有评论(0)