<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; 分页</title>
	<atom:link href="https://www.phppan.com/tag/%e5%88%86%e9%a1%b5/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sat, 25 Apr 2026 00:56:17 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>分页的秘密：OFFSET 性能问题与游标分页</title>
		<link>https://www.phppan.com/2024/12/efficient-pagination-mysql-offset-keyset-optimization/</link>
		<comments>https://www.phppan.com/2024/12/efficient-pagination-mysql-offset-keyset-optimization/#comments</comments>
		<pubDate>Sat, 21 Dec 2024 03:05:18 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[架构和远方]]></category>
		<category><![CDATA[分页]]></category>
		<category><![CDATA[性能]]></category>
		<category><![CDATA[性能优化]]></category>
		<category><![CDATA[架构]]></category>

		<guid isPermaLink="false">http://www.phppan.com/?p=2309</guid>
		<description><![CDATA[在我们日常使用的网站或应用中，无论是浏览电商商品列表、滚动社交媒体动态，还是搜索引擎上一页一页查找结果，分页无 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p style="color: #000000;" data-tool="mdnice编辑器">在我们日常使用的网站或应用中，无论是浏览电商商品列表、滚动社交媒体动态，还是搜索引擎上一页一页查找结果，分页无处不在。它看似简单，一页接着一页展示数据，但在背后，却隐藏着不少技术的「秘密」。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">分页处理得好，用户只会觉得流畅自然；但如果处理不好，页面加载迟缓、数据重复、甚至直接超时，崩溃，都会让用户体验大打折扣。而在应用架构过程中，分页更是一个绕不开的话题，尤其当涉及到<strong style="color: #0e88eb;">海量数据</strong> 时，分页的实现方式会直接影响到系统的性能和效率。</p>
<p style="color: #000000;" data-tool="mdnice编辑器"><strong style="color: #0e88eb;">OFFSET 性能问题</strong> 就是分页中最常见的「瓶颈」。它的核心问题在于，当数据规模变大时，传统分页方式的查询速度会急剧下降，甚至拖垮整个数据库。幸运的是，我们有解决方案：<strong style="color: #0e88eb;">游标分页</strong>。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">那么，为什么 OFFSET 性能会变差？游标分页又是如何解决这些问题的？今天，我们从分页开始，聊一下分页逻辑。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">1. 分页是什么</span></h1>
<p style="color: #000000;" data-tool="mdnice编辑器">分页是一个很常见的逻辑，也是大部分程序员入门的时候首先会掌握的一个通用的实现逻辑。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">分页是一种将大量数据分成多个小部分（页面）进行逐步加载和显示的技术方法。它是一种数据分割和展示的策略，常用于需要显示大量数据的场景，既能提升用户体验，又能改善系统性能。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">分页通常通过将数据按照固定的条目数分隔成多个页面，用户可以通过分页导航（如“上一页”、“下一页”、“跳转到第 N 页”等）浏览数据的不同部分。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">2. 分页的作用</span></h1>
<p style="color: #000000;" data-tool="mdnice编辑器">分页的主要作用包括以下几点：</p>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">提升用户体验</strong>：</p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">避免让用户一次性加载和浏览大量数据，从而减少信息过载。</section>
</li>
<li>
<section style="color: #010101;">通过分页导航（如页码按钮、上一页/下一页），让用户能够快速定位到感兴趣的数据。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">优化页面性能</strong>：</p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">限制页面加载的数据量，减少服务器和浏览器的资源消耗。</section>
</li>
<li>
<section style="color: #010101;">减少前端页面渲染的压力，提高页面加载速度和响应速度。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">降低后端和数据库压力</strong>：</p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">分页可以限制一次性查询的数据量，避免对数据库产生过高的查询负载。</section>
</li>
<li>
<section style="color: #010101;">避免将所有数据发送到前端，减少网络的传输压力。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">便于数据管理</strong>：</p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">在管理系统中，分页能够让管理员方便地查看、筛选和操作特定范围内的数据。</section>
</li>
</ul>
</li>
</ol>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3. 分页的实现方式</span></h1>
<p style="color: #000000;" data-tool="mdnice编辑器">分页的实现方式常见的是两种，传统分页和游标分页，根据应用场景和需求，选择合适的方案可以有效提升系统性能和用户体验。</p>
<h2 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3.1 OFFSET 分页（传统分页）</span></h2>
<p style="color: #000000;" data-tool="mdnice编辑器">传统分页，也称为基于 OFFSET 的分页，是最常见的一种分页方式。其核心思想是通过页码和偏移量（OFFSET）来定位查询结果的起始记录，并限定每次查询的记录数量（LIMIT）。这种方式通常与 SQL 的 LIMIT 和 OFFSET 关键字结合使用。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">传统分页的主要逻辑是根据用户请求的页码计算出需要跳过的记录数（OFFSET = (page &#8211; 1) * pageSize），然后查询从偏移量开始的指定数量的记录。</p>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">原理</span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">OFFSET 分页是最常见也是最简单的分页方式。它通过指定查询的起始位置和每页记录数，从数据库中获取相应的数据。例如，在 SQL 中可以通过<code style="color: #0e8aeb;">LIMIT</code> 和<code style="color: #0e8aeb;">OFFSET</code> 实现：</p>
<pre style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">SELECT</span> * 
<span style="color: #c678dd;">FROM</span> table_name
<span style="color: #c678dd;">ORDER</span> <span style="color: #c678dd;">BY</span> <span style="color: #c678dd;">id</span>
<span style="color: #c678dd;">LIMIT</span> <span style="color: #d19a66;">10</span> <span style="color: #c678dd;">OFFSET</span> <span style="color: #d19a66;">20</span>;
</code></pre>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">LIMIT 10</code>：表示每页显示 10 条记录。</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">OFFSET 20</code>：表示跳过前 20 条记录（即从第 21 条开始）。</section>
</li>
</ul>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">优点</span></h3>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">实现简单</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">逻辑清晰直观，基于<code style="color: #0e8aeb;">LIMIT</code> 和<code style="color: #0e8aeb;">OFFSET</code> 的 SQL 查询几乎所有数据库都支持。</section>
</li>
<li>
<section style="color: #010101;">开发和维护成本低，适合快速实现分页功能。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">支持随机跳页</strong></p>
<pre><code style="color: #0e8aeb;"> <span style="color: #c678dd;">SELECT</span> * 
<span style="color: #c678dd;">FROM</span><span style="color: #c678dd;">users</span>
<span style="color: #c678dd;">ORDER</span><span style="color: #c678dd;">BY</span><span style="color: #c678dd;">id</span><span style="color: #c678dd;">ASC</span>
<span style="color: #c678dd;">LIMIT</span><span style="color: #d19a66;">10</span><span style="color: #c678dd;">OFFSET</span><span style="color: #d19a66;">990</span>;
</code></pre>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">用户可以通过指定页码直接跳转到任意页，而无需逐页加载。例如，直接查询第 100 页的数据：</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">适用范围广</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">适合小规模或中等规模的数据分页场景，尤其是在数据集较小且性能要求不高时。</section>
</li>
</ul>
</li>
</ol>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">缺点</span></h3>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">性能问题</strong></p>
<pre><code style="color: #0e8aeb;"> <span style="color: #c678dd;">SELECT</span> * 
<span style="color: #c678dd;">FROM</span><span style="color: #c678dd;">users</span>
<span style="color: #c678dd;">ORDER</span><span style="color: #c678dd;">BY</span><span style="color: #c678dd;">id</span><span style="color: #c678dd;">ASC</span>
<span style="color: #c678dd;">LIMIT</span><span style="color: #d19a66;">10</span><span style="color: #c678dd;">OFFSET</span><span style="color: #d19a66;">100000</span>;
</code></pre>
<p style="color: #000000;">在这种情况下，数据库需要先扫描 100,000 条记录后，才能返回第 100,001 条到第 100,010 条记录。扫描的记录越多，查询耗时越长。</p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">当数据量很大时，<code style="color: #0e8aeb;">OFFSET</code> 会导致查询性能下降，因为数据库需要扫描并跳过<code style="color: #0e8aeb;">OFFSET</code> 指定的记录，即使这些记录不会返回。<br />
例如：</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">数据一致性问题</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">重复记录</strong>：如果在第一页和第二页之间插入了一条新记录，第二页可能会重复显示第一页的最后一条记录。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">记录丢失</strong>：如果在分页过程中删除了某些记录，可能会导致某些记录被跳过。</section>
</li>
</ul>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">如果在分页过程中数据发生变化（如插入或删除记录），可能会导致分页结果出现重复记录或跳过记录的情况。例如：</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">不适合实时更新的场景</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">当数据集频繁增删时，传统分页难以保证结果的准确性。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">消耗资源</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">每次分页查询都需要数据库执行完整的排序和偏移操作，对资源消耗较大，尤其在大数据集或深分页（偏移量很大）时问题更加明显。这种我们一般称之为<strong style="color: #0e88eb;">深分页</strong></section>
</li>
</ul>
</li>
</ol>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">适用场景</span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">适合小规模数据分页，或者数据更新不频繁的场景，如展示固定的商品列表或博客文章。</p>
<h2 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">3.2 Keyset 分页（游标分页）</span></h2>
<p style="color: #000000;" data-tool="mdnice编辑器">Keyset Pagination，也称为基于键的分页或游标分页，是一种高效的分页技术，用于解决传统分页方法（基于 OFFSET 和 LIMIT）在处理大数据集时的性能瓶颈问题。相较于传统分页，Keyset Pagination 不依赖页码或偏移量，而是通过上一页的最后一条记录的标识符（通常是主键或唯一索引）来标记分页的起始点，从而实现更高效、更稳定的分页。</p>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;">原理</span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">游标分页是一种基于游标的分页方式，通过使用上一页的最后一条记录的标识（如主键或时间戳）来确定下一页的数据，而不是依赖 OFFSET。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">示例查询：</p>
<pre style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">SELECT</span> * 
<span style="color: #c678dd;">FROM</span> table_name
<span style="color: #c678dd;">WHERE</span> <span style="color: #c678dd;">id</span> &gt; <span style="color: #d19a66;">100</span>
<span style="color: #c678dd;">ORDER</span> <span style="color: #c678dd;">BY</span> <span style="color: #c678dd;">id</span>
<span style="color: #c678dd;">LIMIT</span> <span style="color: #d19a66;">10</span>;
</code></pre>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">id &gt; 100</code>：表示从上一页最后一条记录的主键（<code style="color: #0e8aeb;">id=100</code>）之后开始查询。</section>
</li>
<li>
<section style="color: #010101;"><code style="color: #0e8aeb;">LIMIT 10</code>：每次获取 10 条记录。</section>
</li>
</ul>
<h4 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold;"><strong style="color: #0e88eb;">优点</strong></span></h4>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">性能优越</strong>：避免了 OFFSET 扫描的性能问题，查询直接从指定游标位置开始。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">数据一致性</strong>：即使数据在分页过程中发生变化，也能保证数据不会重复或丢失。</section>
</li>
</ul>
<h4 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold;"><strong style="color: #0e88eb;">缺点</strong></span></h4>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">跳页困难</strong>：无法直接跳转到第 N 页，需要依赖前置页的上下文。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">依赖排序字段</strong>：通常需要全局唯一且连续的排序字段（如主键或时间戳）。</section>
</li>
</ul>
<h4 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold;"><strong style="color: #0e88eb;">适用场景</strong></span></h4>
<p style="color: #000000;" data-tool="mdnice编辑器">适合处理海量数据或数据频繁更新的场景，如社交媒体动态流、消息列表、AIGC 的推荐图片流等。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">聊完了常见的两种分页，再聊一下 OFFSET 为什么会慢。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4. OFFSET 为什么会慢</span></h1>
<p style="color: #000000;" data-tool="mdnice编辑器">以 MySQL 为例。</p>
<p style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #0e8aeb;">LIMIT ... OFFSET ...</code> 是一种常用的分页查询方式，但随着<code style="color: #0e8aeb;">OFFSET</code> 值的增大，这种方式会带来严重的性能问题。其核心原因在于<strong style="color: #0e88eb;">MySQL 的查询执行机制</strong> 和<strong style="color: #0e88eb;">数据的存储与读取方式</strong>。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">在执行<code style="color: #0e8aeb;">LIMIT ... OFFSET ...</code> 查询时，MySQL 的行为是<strong style="color: #0e88eb;">扫描并跳过 OFFSET 指定的记录</strong>，即使这些记录不会返回到客户端，但是数据库仍然需要从磁盘读取记录，排序……</p>
<p style="color: #000000;" data-tool="mdnice编辑器">这不是执行问题，而是 OFFSET 设计方式：</p>
<pre style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #abb2bf;">…the rows are first sorted according to the &lt;order by clause&gt; and <span style="color: #c678dd;">then</span> limited by dropping the number of rows specified <span style="color: #c678dd;">in</span> the &lt;result offset clause&gt; from the beginning…

SQL:2016, Part 2, §4.15.3 Derived tables

翻译过来：……记录会首先根据 ORDER BY 子句 进行排序，然后通过丢弃从开头开始的 OFFSET 子句指定数量的行来限制结果……

</code></pre>
<h2 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4.1 OFFSET 执行过程</span></h2>
<p style="color: #000000;" data-tool="mdnice编辑器">比如下面的例子：</p>
<pre style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #abb2bf;"><span style="color: #c678dd;">SELECT</span> * 
<span style="color: #c678dd;">FROM</span> t1 
<span style="color: #c678dd;">ORDER</span> <span style="color: #c678dd;">BY</span> <span style="color: #c678dd;">id</span> <span style="color: #c678dd;">ASC</span> 
<span style="color: #c678dd;">LIMIT</span> <span style="color: #d19a66;">1000000</span>, <span style="color: #d19a66;">20</span>;
</code></pre>
<p style="color: #000000;" data-tool="mdnice编辑器">其执行过程如下：</p>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">全表扫描或索引扫描：</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">MySQL 根据<code style="color: #0e8aeb;">ORDER BY id</code> 对记录进行排序。即使只需要第 1000001 条到第 1000020 条记录，也必须先按查询条件读出前 100 万条记录。</section>
</li>
<li>
<section style="color: #010101;">如果有索引（如主键索引<code style="color: #0e8aeb;">id</code>），MySQL 会利用索引扫描；如果没有索引，则会进行全表扫描。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">跳过 OFFSET 记录：</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">MySQL 遍历查询结果集，并逐条丢弃前 100 万条记录（<code style="color: #0e8aeb;">OFFSET 1000000</code>）。</section>
</li>
<li>
<section style="color: #010101;">这种「丢弃」并不是直接跳过，而是逐行读取，然后丢弃，直到到达第 1000001 条记录。</section>
</li>
</ul>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">读取目标记录：</strong></p>
</section>
<ul class="list-paddingleft-1">
<li>
<section style="color: #010101;">到达第 1000001 条记录后，MySQL 开始读取接下来的 20 条数据（<code style="color: #0e8aeb;">LIMIT 20</code>），作为最终结果返回。</section>
</li>
</ul>
</li>
</ol>
<h2 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4.2 OFFSET 性能问题的根本原因</span></h2>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（1）扫描和跳过造成资源浪费</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">即使客户端只需要一小部分数据（例如 20 条），MySQL 在执行查询时，仍然需要扫描和处理大量的记录（前 100 万条）。这会带来以下问题：</p>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">耗费磁盘 I/O：</strong><br />
MySQL 需要从磁盘读取未返回的记录，即使这些记录最终会被丢弃。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">浪费内存和 CPU：</strong><br />
MySQL 扫描的所有记录会被加载到内存中，排序和过滤操作会消耗 CPU 资源。对于深分页（<code style="color: #0e8aeb;">OFFSET</code> 值很大）的查询，这种浪费会随着页码的增加而成倍增长。</section>
</li>
</ul>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（2）无法直接利用索引跳过记录</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">即使排序字段有索引（如主键索引<code style="color: #0e8aeb;">id</code>），MySQL 仍然需要逐条扫描记录，跳过 OFFSET 指定的记录。原因是：</p>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">索引扫描的局限性：</strong> MySQL 的索引只能用来快速定位起始记录（例如<code style="color: #0e8aeb;">id &gt; 1000000</code> 的情况），但在 OFFSET 查询中，MySQL 并不知道目标记录的具体位置，只能通过逐条遍历的方式来跳过。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">无指针跳转机制：</strong> MySQL 的存储引擎（如 InnoDB）在处理 OFFSET 查询时，不会直接跳过指定数量的记录，而是逐行读取和计数，直到到达目标记录。</section>
</li>
</ul>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（3）排序带来的额外开销</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">在使用<code style="color: #0e8aeb;">ORDER BY</code> 的情况下，MySQL 必须先对所有数据进行排序，然后再从中挑选目标记录：</p>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">如果排序字段没有索引，MySQL 会将数据加载到内存或临时表中，并在内存中完成排序（可能会涉及磁盘写入）。</section>
</li>
<li>
<section style="color: #010101;">如果排序字段有索引，MySQL 会利用索引加速排序，但仍需遍历和丢弃 OFFSET 指定的记录，资源浪费依然存在。</section>
</li>
</ul>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（4）深分页数据量巨大</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">当<code style="color: #0e8aeb;">OFFSET</code> 值较小时，MySQL 需要跳过的记录量较少，性能影响不明显。但随着<code style="color: #0e8aeb;">OFFSET</code> 值的增大，MySQL 需要扫描和丢弃的记录数呈线性增长，最终导致性能急剧下降。</p>
<h2 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4.3 OFFSET 性能问题的典型场景</span></h2>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（1）数据量庞大时的深分页</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">当表中的数据量达到百万级别时，深分页（如<code style="color: #0e8aeb;">OFFSET 1000000</code>）会导致查询性能显著下降。原因是 MySQL 在扫描前 100 万条记录时，消耗了大量的磁盘 I/O 和 CPU 资源。</p>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（2）查询结果动态变化</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">分页查询的同时，数据可能在不断更新（如新增或删除记录）。这种情况下：</p>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">MySQL 仍然会按照 OFFSET 值从头扫描，导致性能下降。</section>
</li>
<li>
<section style="color: #010101;">数据的插入或删除可能导致分页结果重复或遗漏。</section>
</li>
</ul>
<h3 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e88eb;"><strong>（3）排序字段没有索引</strong></span></h3>
<p style="color: #000000;" data-tool="mdnice编辑器">如果<code style="color: #0e8aeb;">ORDER BY</code> 的字段没有索引，MySQL 需要对全表数据进行排序，并将排序结果存储在临时表中。排序操作会进一步加剧性能问题。</p>
<h2 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">4.4 如何解决 OFFSET 性能问题？</span></h2>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">使用游标分页（Keyset Pagination）</strong><br />
通过记录上一页的最后一条记录的唯一标识符（如主键<code style="color: #0e8aeb;">id</code>）来定位下一页的起点，避免扫描和跳过无关记录：</section>
</li>
</ol>
<pre style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #abb2bf;">   <span style="color: #c678dd;">SELECT</span> * 
   <span style="color: #c678dd;">FROM</span> t1 
   <span style="color: #c678dd;">WHERE</span> <span style="color: #c678dd;">id</span> &gt; <span style="font-style: italic; color: #5c6370;">#{last_id} </span>
   <span style="color: #c678dd;">ORDER</span> <span style="color: #c678dd;">BY</span> <span style="color: #c678dd;">id</span> <span style="color: #c678dd;">ASC</span> 
   <span style="color: #c678dd;">LIMIT</span> <span style="color: #d19a66;">20</span>;
</code></pre>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">优势</strong>：直接定位目标记录，性能与<code style="color: #0e8aeb;">OFFSET</code> 无关。</section>
</li>
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">适用场景</strong>：连续分页（如滑动加载）。</section>
</li>
</ul>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">限制深分页范围</strong><br />
限制用户只能跳转到前后一段范围内的页码，避免深分页。</p>
</section>
</li>
<li>
<section style="color: #010101;">
<p style="color: #000000;"><strong style="color: #0e88eb;">子查询优化</strong><br />
使用子查询提取主键范围，然后通过主键关联查询：</p>
</section>
</li>
</ol>
<pre style="color: #000000;" data-tool="mdnice编辑器"><code style="color: #abb2bf;">   <span style="color: #c678dd;">SELECT</span> * 
   <span style="color: #c678dd;">FROM</span> t1 
   <span style="color: #c678dd;">JOIN</span> (
       <span style="color: #c678dd;">SELECT</span> <span style="color: #c678dd;">id</span> 
       <span style="color: #c678dd;">FROM</span> t1 
       <span style="color: #c678dd;">ORDER</span> <span style="color: #c678dd;">BY</span> <span style="color: #c678dd;">id</span> <span style="color: #c678dd;">ASC</span> 
       <span style="color: #c678dd;">LIMIT</span> <span style="color: #d19a66;">1000000</span>, <span style="color: #d19a66;">20</span>
   ) x <span style="color: #c678dd;">USING</span> (<span style="color: #c678dd;">id</span>);
</code></pre>
<ul class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">优势</strong>：减少排序和回表操作的开销。</section>
</li>
</ul>
<ol class="list-paddingleft-1" style="color: #000000;">
<li>
<section style="color: #010101;"><strong style="color: #0e88eb;">合理设计索引</strong><br />
对常用的查询字段和排序字段添加索引，最大化利用 MySQL 的索引能力。</section>
</li>
</ol>
<p style="color: #000000;" data-tool="mdnice编辑器">除以上的 4 种以外，还可以考虑倒序分页，延迟关联、分区表优化或业务逻辑分流等方案。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">OFFSET 的性能问题，归根结底是因为 MySQL 的查询执行机制无法直接跳过指定数量的记录，只能通过逐条扫描和丢弃的方式实现。这种机制在深分页时会导致严重的资源浪费。通过优化查询方式（如游标分页或子查询），可以显著减少无关记录的扫描量，从而提高查询性能。</p>
<h1 style="color: #000000;" data-tool="mdnice编辑器"><span style="font-weight: bold; color: #0e8aeb;">5. 小结</span></h1>
<p style="color: #000000;" data-tool="mdnice编辑器">分页是日常开发中非常常见的功能，但在数据量上来后，分页可能成为隐藏的性能杀手。传统的 OFFSET 分页尽管实现简单，但却无法避免扫描和跳过大量无用记录的性能瓶颈，尤其在处理海量数据时。这种情况下，优化分页逻辑显得尤为重要。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">通过引入游标分页、子查询优化、分区表设计等技术手段，并结合业务逻辑上的调整，几乎可以解决大部分分页场景的性能问题。在实际开发中，应根据业务特点和数据规模选择合适的优化方案，实现性能和用户体验的平衡。</p>
<p style="color: #000000;" data-tool="mdnice编辑器">分页的优化，不仅是一项技术能力，更是对业务场景理解的体现。希望通过本文的分析和总结，能帮助开发者更好地应对深分页的挑战，写出高效、稳健的分页逻辑！</p>
<p style="color: #000000;" data-tool="mdnice编辑器">以上。</p>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2024/12/efficient-pagination-mysql-offset-keyset-optimization/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
