对于新闻网站、电商平台和内容管理系统而言,分页查询不仅关系到页面能否正常显示,还直接影响数据库的索引利用率、磁盘读取量、服务器负载以及用户浏览体验。当一张数据表只有几千条记录时,使用 LIMIT 和 OFFSET 通常就能满足需求;但随着文章数量增长到几十万条甚至数百万条,深分页可能逐渐成为数据库性能瓶颈。如何在保持查询结果正确的同时减少不必要的数据读取,需要结合索引结构、排序规则、数据更新频率和业务访问方式进行设计。
一、传统分页为什么会越来越慢
MySQL 最常见的分页方式是使用 LIMIT 配合 OFFSET。例如,一个商品列表每页显示10条记录,需要查询第三页时,可以使用以下SQL语句:
SELECT id, name, price
FROM products
WHERE category = '电子产品'
ORDER BY id DESC
LIMIT 20, 10;
这里的 LIMIT 20, 10 表示跳过前20条符合条件的记录,再返回接下来的10条。MySQL 也支持更直观的写法 LIMIT 10 OFFSET 20,两种写法表达的是相同的分页要求。
这种方法的优势是实现简单,能够直接支持第一页、第二页、第十页等任意页码,也便于计算总页数和构建页码导航。对于后台管理系统、规模不大的商品目录以及需要直接跳转到指定页码的文章列表,传统分页仍然具有实际价值。
真正的问题出现在深分页。例如:
SELECT id, title, created_at
FROM news
ORDER BY id DESC
LIMIT 99990, 10;
假设查询结果按照 id 降序排列,这条语句需要跳过前面的99990条记录,再返回10条。即使 id 上存在合适的索引,数据库通常仍然需要沿着索引读取并越过大量记录,才能到达目标位置。OFFSET 越大,需要处理的索引记录通常也越多。
需要注意的是,深分页并不意味着 MySQL 必然进行全表扫描。如果排序字段有合适的索引,查询可能主要沿着索引扫描;如果查询条件与排序方式不匹配,则可能出现大量行读取、额外排序或临时表操作。具体成本取决于执行计划、数据分布、索引结构以及是否需要回表读取其他字段。
因此,不能简单地把10万条记录当作传统分页的性能分界线。即使只有几万条记录,如果查询条件复杂、索引缺失或返回大量字段,也可能出现性能问题;反过来,经过合理优化的百万级数据表,在浅分页场景下仍然可能运行良好。
二、索引优化是分页设计的第一步
在改用更复杂的分页方案之前,应先检查查询条件和排序字段是否能够有效利用索引。
例如,商品列表通常根据 category 筛选商品,再按照 id 从大到小排列,可以建立联合索引:
CREATE INDEX idx_category_id
ON products (category, id);
对于以下查询:
SELECT id, name, price
FROM products
WHERE category = '电子产品'
ORDER BY id DESC
LIMIT 10;
联合索引首先按照 category 排列,同一类别内再按照 id 排列。由于 category 是等值查询条件,MySQL 可以利用索引定位对应类别的数据,并按照 id 的顺序读取记录。在支持反向索引扫描的 MySQL 版本和适用的执行计划中,降序读取也不一定需要额外排序。
不过,联合索引并非越多越好。索引会占用磁盘空间,也会增加 INSERT、UPDATE 和 DELETE 的维护成本。如果查询经常只使用 id 排序,而不限制 category,就需要根据实际访问模式评估是否还需要单独的 id 索引。
另一个容易被忽略的问题是 SELECT *。当一条记录包含长文本、JSON、图片地址或其他较大的字段时,直接读取全部列可能增加磁盘 I/O、缓冲池压力和网络传输量。列表页面通常只需要文章 ID、标题、发布时间和少量摘要信息,没有必要把完整正文一起取出。
判断索引是否真正发挥作用,应使用 EXPLAIN 检查执行计划,并结合慢查询日志观察实际耗时、扫描行数和访问频率。对于重要查询,还可以在适用的 MySQL 版本中使用 EXPLAIN ANALYZE,观察实际执行过程,而不是仅凭 SQL 语句表面上的简洁程度判断性能。
三、游标分页如何避免深度 OFFSET
当数据规模较大,而且用户通常按照时间或唯一 ID 向后浏览时,可以使用基于游标的分页,也称为 Keyset Pagination。
假设商品按照 id 降序排列,第一页查询10条记录:
SELECT id, name, price
FROM products
WHERE category = '电子产品'
ORDER BY id DESC
LIMIT 10;
假设这一页最后一条记录的 id 是10000,下一页便可以使用该记录作为游标:
SELECT id, name, price
FROM products
WHERE category = '电子产品'
AND id < 10000
ORDER BY id DESC
LIMIT 10;
第二页不再要求数据库跳过前面所有记录,而是直接从符合条件的索引位置继续读取。对于具有合适索引的查询,这种方式通常能够显著降低深分页的扫描成本。
如果联合索引是 (category, id),上述查询的筛选条件和排序字段就能够较好地配合索引。游标分页的性能优势来自定位方式的变化,而不是游标本身具有特殊的加速能力。如果没有合适的索引,或者排序字段无法有效利用索引,游标分页同样可能很慢。
实现时,应该保存上一页最后一条记录的排序键,并使用严格的小于或大于条件确定下一页的起点。例如,降序查询使用 id < last_id,可以避免把上一页最后一条记录再次返回。首次请求则不需要游标条件,而不是简单地把所有场景中的游标默认设置为0。
游标分页尤其适合新闻时间线、日志列表、评论流和无限滚动页面。但它也有明显限制:用户通常不能直接跳到第100页,因为每一页的起点依赖前一页的结果。如果业务要求随意输入页码、准确显示总页数或跳转到指定页,就需要继续使用传统分页,或者设计额外的定位机制。
四、按照发布时间分页,需要处理相同时间的问题
新闻网站和日志系统往往按照发布时间排列,而不是单纯按照自增 ID 排序。此时可以使用 create_time 作为主要排序字段,但如果多条记录具有相同的时间戳,只使用时间作为游标就可能导致重复或遗漏。
例如,下面的查询使用发布时间降序排列:
SELECT id, title, created_at
FROM news
ORDER BY created_at DESC, id DESC
LIMIT 10;
这里同时使用 created_at 和 id 排序。发布时间决定主要顺序,id 则作为第二排序键,确保相同时间的记录仍然具有确定的先后顺序。
假设上一页最后一条记录的发布时间是 2026-10-07 12:00:00,ID为5000,下一页可以这样查询:
SELECT id, title, created_at
FROM news
WHERE created_at < '2026-10-07 12:00:00'
OR (
created_at = '2026-10-07 12:00:00'
AND id < 5000
)
ORDER BY created_at DESC, id DESC
LIMIT 10;
这种条件表示:读取发布时间更早的记录,或者读取发布时间相同但 ID 更小的记录。它将两个排序字段组合成一个明确的游标边界,避免只使用时间戳时发生分页边界错误。
可以考虑建立与查询模式对应的联合索引:
CREATE INDEX idx_news_created_id
ON news (created_at, id);
如果查询还需要筛选 publish_status、category 等字段,索引设计则应结合真实 SQL 和数据分布进一步调整。不能机械地把所有 WHERE 字段都放进索引,因为联合索引的字段顺序会影响索引能否用于定位、排序和范围扫描。
此外,游标分页并不自动保证整个浏览过程看到的是同一时刻的数据。如果浏览期间有新文章发布、记录被删除,或者排序字段发生修改,后续页面仍可能与第一次请求时的完整数据集合不同。对于普通新闻浏览,这通常可以接受;对于财务记录、审计数据或要求严格一致性的导出任务,则应考虑一致性快照、事务隔离级别或固定的数据截止边界。
五、只取 ID 再读取正文,能否改善深分页
对于需要保留页码跳转的传统分页,还可以采用延迟读取,也称为 Deferred Join 或延迟关联。核心思路是先通过索引找到目标页的 ID,再读取这批记录的其他字段,减少深分页阶段不必要的宽记录访问。
例如:
SELECT n.id, n.title, n.created_at
FROM news AS n
JOIN (
SELECT id
FROM news
ORDER BY id DESC
LIMIT 99990, 10
) AS page_ids
ON n.id = page_ids.id
ORDER BY n.id DESC;
内层查询只获取 ID,外层再根据 ID 读取标题和发布时间。对于包含长正文或大量字段的表,这种方法可能减少回表读取的开销,尤其是在目标查询原本需要处理较宽的记录时。
不过,延迟关联并没有消除 OFFSET 本身的成本。内层查询依然需要跳过99990条记录,因此它不能完全解决深分页随页码增加而变慢的问题。如果页面数据量很大,且用户主要是顺序向后浏览,游标分页通常更值得优先考虑;如果必须支持任意页码,延迟关联则可以作为传统分页的优化手段之一。
实际使用时,还应保证内外层查询采用一致的筛选条件。假如文章列表只显示已发布内容,内层查询就应该在选择 ID 时过滤发布状态,否则可能先选出大量不应展示的记录,导致最终页数不足或结果不符合预期。
六、分页总数、缓存和并发更新如何处理
很多网站不仅需要返回当前页的数据,还需要显示总记录数和总页数。传统页码导航经常通过 COUNT(*) 获取符合条件的记录数量,再计算总页数。但对于复杂筛选条件和大型数据表,精确统计本身也可能消耗较多资源。
如果产品设计并不要求精确显示总页数,可以只查询当前页需要的数据,并额外多取一条记录。例如每页显示10条,就查询11条:如果实际返回11条,说明通常还有下一页;如果只有10条或更少,则可以据此判断是否还有更多数据。这样能够避免每次翻页都执行精确计数,但它无法提供精确的总记录数。
缓存也可以降低热门分页请求对数据库的压力。对于访问量较高、更新频率较低的分类页面,可以缓存一段时间内的查询结果,并在文章发布、删除或分类变化时更新或失效相关缓存。缓存键应包含影响结果的筛选条件、排序方式、页码或游标等信息,避免不同查询错误地共用同一份结果。
然而,缓存并不是所有分页查询的必要条件。对更新频繁的新闻首页,过度缓存可能导致最新文章无法及时显示;对冷门页面,缓存带来的收益也可能有限。应根据访问频率、查询成本、数据更新规律和可接受的延迟制定缓存策略,而不是仅仅因为数据库记录多就引入 Redis。
并发更新则是另一个需要考虑的问题。使用 OFFSET 时,如果新记录插入到列表前端,用户翻到下一页时可能再次看到上一页出现过的记录;删除记录也可能使某些条目被跳过。游标分页通过固定排序键继续读取,通常能减少这种由前端插入导致的位移问题,但它仍不能自动提供完整的一致性快照。对于需要严格复现某一时刻数据集合的业务,应当明确数据快照或截止边界,而不能仅依赖分页算法。
七、PHP 网站应该怎样选择分页方案
在 PHP 网站中,分页设计应同时考虑数据库性能、前端交互和业务要求。传统页码分页适合后台管理、需要任意页码跳转的文章目录和规模适中的分类列表;游标分页更适合新闻流、日志、评论和无限滚动;延迟关联则适用于必须保留传统页码导航、同时希望减少深分页宽记录访问的场景。
以 PDO 为例,查询条件和游标值应使用预处理语句绑定,而不能直接把用户输入拼接到 SQL 中。对于 LIMIT 和 OFFSET 这类整数参数,应先验证并转换为合法的非负整数;对于动态排序字段,则应从服务器端预先定义的白名单中选择,因为列名和 ASC、DESC 等 SQL 结构通常不能像普通值一样通过占位符安全绑定。
例如,游标查询可以写成:
$sql = "
SELECT id, title, created_at
FROM news
WHERE publish_status = :status
AND id < :last_id
ORDER BY id DESC
LIMIT 10
";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':status', 'published', PDO::PARAM_STR);
$stmt->bindValue(':last_id', $lastId, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
这段示例假设 $lastId 已经根据请求情况完成初始化和验证,并且使用 ID 作为稳定的降序游标。首次访问时,应使用不带游标条件的第一页查询,或由后端明确区分首次请求与后续请求。若改为按照发布时间和 ID 联合排序,就必须同步调整 SQL 条件、索引和游标内容,不能只修改 ORDER BY。
对于公开接口,还应避免完全信任客户端提交的游标。可以将排序键、筛选条件或版本信息封装在经过服务器验证的游标令牌中,防止客户端随意修改参数造成异常查询。即便使用签名令牌,也仍然需要进行权限检查、参数校验和合理的查询限制。
八、分页性能优化应从真实执行计划出发
分页设计没有适用于所有网站的唯一答案。判断方案是否合适,首先应明确数据表规模、访问模式、筛选条件、排序规则以及用户是否需要任意页码跳转,然后利用 EXPLAIN、慢查询日志和实际负载测试验证索引与查询方案。不能仅凭表中有多少条记录,就决定必须使用游标分页或缓存。
对于一般的文章列表,使用 LIMIT 和 OFFSET 并配合适当索引,通常已经足够。对于需要持续向后浏览的大型数据集,基于唯一排序键的游标分页能够避免深度 OFFSET 的主要开销。对于必须支持深分页的页码导航,可以评估延迟关联、限制最大页数或设计专门的定位方案。至于高并发场景,还需要结合缓存、连接池、查询监控和数据库资源配置综合优化。
真正可靠的分页系统,不是简单地把一种 SQL 写法替换成另一种,而是让查询条件、索引、排序、数据一致性和用户交互形成协调的整体。只有通过实际执行计划和负载数据验证优化效果,才能在查询速度、开发复杂度与维护成本之间找到适合具体业务的平衡。
MySQL 分页查询怎么设计?从传统分页到高效分页逐步理解
图片说明:示意图 图片来源:Public Domain(公有领域)
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP
栏目导航
探索不同领域的新闻、知识、财经、科技、历史与北美生活资讯。