<!DOCTYPE html>
<html lang="zh-Hans">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="renderer" content="webkit">
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=5">
    <title>MySql大表分页(附独门秘技) | 云图网</title>
    <meta name="description" content="问题背景 MySql(InnoDB)中的订单表需要按时间顺序分页查询，且主键不是时间维度递增，订单表在百万以上规模，此时如何高性能地实现该需求？ 注：本文并非主要讲解如何建立索引，以下的分析均建立在有合适的索引的前提下 初步方案1 众所周知，MySql中，有一个limit offset, pageSize的用法，可以实…">
<meta property="og:type" content="article">
<meta property="og:url" content="https://blog.ytso.com/tech/pnotes/175424.html">
<meta property="og:site_name" content="云图网">
<meta property="og:title" content="MySql大表分页(附独门秘技)">
<meta property="og:description" content="问题背景 MySql(InnoDB)中的订单表需要按时间顺序分页查询，且主键不是时间维度递增，订单表在百万以上规模，此时如何高性能地实现该需求？ 注：本文并非主要讲解如何建立索引，以下的分析均建立在有合适的索引的前提下 初步方案1 众所周知，MySql中，有一个limit offset, pageSize的用法，可以实…">
<link rel="canonical" href="https://blog.ytso.com/tech/pnotes/175424.html">
<meta name="applicable-device" content="pc,mobile">
<meta http-equiv="Cache-Control" content="no-transform">
<link rel="shortcut icon" href="https://imgcdn.ytso.com/wp-content/uploads/2024/10/20241015181503433.jpg">
<link rel='dns-prefetch' href='//cdnjs.cloudflare.com' />
<style id='wp-img-auto-sizes-contain-inline-css' type='text/css'>
img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px}
/*# sourceURL=wp-img-auto-sizes-contain-inline-css */
</style>
<link rel='stylesheet' id='stylesheet-css' href='https://blog.ytso.com/wp-content/themes/justnews/css/style.css?ver=6.21.5' type='text/css' media='all' />
<link rel='stylesheet' id='material-icons-css' href='https://blog.ytso.com/wp-content/themes/justnews/themer/assets/css/material-icons.css?ver=6.21.5' type='text/css' media='all' />
<link rel='stylesheet' id='remixicon-css' href='https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.2.0/remixicon.min.css?ver=6.9.4' type='text/css' media='all' />
<link rel='stylesheet' id='font-awesome-css' href='https://blog.ytso.com/wp-content/themes/justnews/themer/assets/css/font-awesome.css?ver=6.21.5' type='text/css' media='all' />
<style id='wp-block-library-inline-css' type='text/css'>
:root{--wp-block-synced-color:#7a00df;--wp-block-synced-color--rgb:122,0,223;--wp-bound-block-color:var(--wp-block-synced-color);--wp-editor-canvas-background:#ddd;--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0,124,186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0,107,160.5;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0,90,135;--wp-admin-border-width-focus:2px}@media (min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px}}.wp-element-button{cursor:pointer}:root .has-very-light-gray-background-color{background-color:#eee}:root .has-very-dark-gray-background-color{background-color:#313131}:root .has-very-light-gray-color{color:#eee}:root .has-very-dark-gray-color{color:#313131}:root .has-vivid-green-cyan-to-vivid-cyan-blue-gradient-background{background:linear-gradient(135deg,#00d084,#0693e3)}:root .has-purple-crush-gradient-background{background:linear-gradient(135deg,#34e2e4,#4721fb 50%,#ab1dfe)}:root .has-hazy-dawn-gradient-background{background:linear-gradient(135deg,#faaca8,#dad0ec)}:root .has-subdued-olive-gradient-background{background:linear-gradient(135deg,#fafae1,#67a671)}:root .has-atomic-cream-gradient-background{background:linear-gradient(135deg,#fdd79a,#004a59)}:root .has-nightshade-gradient-background{background:linear-gradient(135deg,#330968,#31cdcf)}:root .has-midnight-gradient-background{background:linear-gradient(135deg,#020381,#2874fc)}:root{--wp--preset--font-size--normal:16px;--wp--preset--font-size--huge:42px}.has-regular-font-size{font-size:1em}.has-larger-font-size{font-size:2.625em}.has-normal-font-size{font-size:var(--wp--preset--font-size--normal)}.has-huge-font-size{font-size:var(--wp--preset--font-size--huge)}.has-text-align-center{text-align:center}.has-text-align-left{text-align:left}.has-text-align-right{text-align:right}.has-fit-text{white-space:nowrap!important}#end-resizable-editor-section{display:none}.aligncenter{clear:both}.items-justified-left{justify-content:flex-start}.items-justified-center{justify-content:center}.items-justified-right{justify-content:flex-end}.items-justified-space-between{justify-content:space-between}.screen-reader-text{border:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;word-wrap:normal!important}.screen-reader-text:focus{background-color:#ddd;clip-path:none;color:#444;display:block;font-size:1em;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}html :where(.has-border-color){border-style:solid}html :where([style*=border-top-color]){border-top-style:solid}html :where([style*=border-right-color]){border-right-style:solid}html :where([style*=border-bottom-color]){border-bottom-style:solid}html :where([style*=border-left-color]){border-left-style:solid}html :where([style*=border-width]){border-style:solid}html :where([style*=border-top-width]){border-top-style:solid}html :where([style*=border-right-width]){border-right-style:solid}html :where([style*=border-bottom-width]){border-bottom-style:solid}html :where([style*=border-left-width]){border-left-style:solid}html :where(img[class*=wp-image-]){height:auto;max-width:100%}:where(figure){margin:0 0 1em}html :where(.is-position-sticky){--wp-admin--admin-bar--position-offset:var(--wp-admin--admin-bar--height,0px)}@media screen and (max-width:600px){html :where(.is-position-sticky){--wp-admin--admin-bar--position-offset:0px}}
/*wp_block_styles_on_demand_placeholder:69cb853a29a7c*/
/*# sourceURL=wp-block-library-inline-css */
</style>
<style id='classic-theme-styles-inline-css' type='text/css'>
/*! This file is auto-generated */
.wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none}
/*# sourceURL=/wp-includes/css/classic-themes.min.css */
</style>
<link rel='stylesheet' id='wpcom-member-css' href='https://blog.ytso.com/wp-content/plugins/wpcom-member/css/style.css?ver=1.7.19' type='text/css' media='all' />
<script type="text/javascript" src="https://blog.ytso.com/wp-includes/js/jquery/jquery.min.js?ver=3.7.1" id="jquery-core-js"></script>
<script type="text/javascript" src="https://blog.ytso.com/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1" id="jquery-migrate-js"></script>
<link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://blog.ytso.com/xmlrpc.php?rsd" />
<meta name='description' content='问题背景 MySql(InnoDB)中的订单表需要按时间顺序分页查询，且主键不是时间维度递增，订单表在百万以上规模，此时如何高性能地实现该需求？ 注：本文并非主要讲解如何建立索引，以下的分析均建立在有合适的索引的前…' />
<style>:root{--theme-color: #08c; --theme-hover: #07c; --logo-height: 32px; --logo-height-mobile: 26px; --menu-item-gap: 28px; --member-login-bg: url('https://blog.ytso.com/loginwall.jpg'); --header-bg-color: #fff; --header-bg-image: none; --theme-border-radius-s: 3px; --theme-border-radius-m: 5px; --theme-border-radius-l: 8px; --theme-border-radius-xl: 12px; --thumb-ratio-default: 480 / 300; --thumb-ratio-post: 480 / 300; --post-video-ratio: 860 / 482;}</style>
<link rel="icon" href="https://imgcdn.ytso.com/wp-content/uploads/2024/10/20241015181503433.jpg" sizes="32x32" />
<link rel="icon" href="https://imgcdn.ytso.com/wp-content/uploads/2024/10/20241015181503433.jpg" sizes="192x192" />
<link rel="apple-touch-icon" href="https://imgcdn.ytso.com/wp-content/uploads/2024/10/20241015181503433.jpg" />
<meta name="msapplication-TileImage" content="https://imgcdn.ytso.com/wp-content/uploads/2024/10/20241015181503433.jpg" />
    <!--[if lte IE 11]><script src="https://blog.ytso.com/wp-content/themes/justnews/js/update.js"></script><![endif]-->
</head>
<body class="wp-singular post-template-default single single-post postid-175424 single-format-standard wp-theme-justnews lang-cn el-boxed header-fixed">
<header class="header header-fluid">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse" aria-label="menu">
                <span class="icon-bar icon-bar-1"></span>
                <span class="icon-bar icon-bar-2"></span>
                <span class="icon-bar icon-bar-3"></span>
            </button>
                        <div class="logo">
                <a href="https://blog.ytso.com/" rel="home">
                    <img src="https://imgcdn.ytso.com/wp-content/uploads/2021/12/20211207181156143.png" alt="云图网">
                </a>
            </div>
        </div>
        <div class="collapse navbar-collapse mobile-style-0">
            <nav class="primary-menu"><ul id="menu-justnews-footer-menu" class="nav navbar-nav wpcom-adv-menu"><li class="menu-item"><a href="https://blog.ytso.com/category/industrynews">业界资讯</a></li>
<li class="menu-item current-post-ancestor active menu-item-style menu-item-style-1 dropdown"><a target="_blank" href="https://blog.ytso.com/category/tech" class="dropdown-toggle">技术专区</a>
<ul class="dropdown-menu menu-item-wrap menu-item-col-5">
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/cloud">云计算</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/ai">人工智能</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/safety">信息安全</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/bigdata">大数据</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/dev">研发管理</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/webdev">大前端</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/opensource">开源</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/aiops">智能运维</a></li>
	<li class="menu-item current-post-ancestor current-post-parent active"><a href="https://blog.ytso.com/category/tech/pnotes">编程笔记</a></li>
	<li class="menu-item"><a href="https://blog.ytso.com/category/tech/wp">WordPress</a></li>
</ul>
</li>
<li class="menu-item"><a href="https://blog.ytso.com/category/enterprise-strategic-planning">企业战略规划</a></li>
<li class="menu-item"><a href="https://blog.ytso.com/category/download">下载专区</a></li>
<li class="menu-item"><a href="https://blog.ytso.com/category/it%e6%b1%9f%e6%b9%96%e5%8f%b2">江湖史</a></li>
<li class="menu-item"><a href="https://blog.ytso.com/category/notes">随笔记录</a></li>
</ul></nav>            <div class="navbar-action">
                <div class="navbar-search-icon j-navbar-search"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-search"></use></svg></i></div><form class="navbar-search" action="https://blog.ytso.com/" method="get" role="search"><div class="navbar-search-inner"><i class="wpcom-icon wi navbar-search-close"><svg aria-hidden="true"><use xlink:href="#wi-close"></use></svg></i><input type="text" name="s" class="navbar-search-input" autocomplete="off" maxlength="100" placeholder="输入关键词搜索..." value=""><button class="navbar-search-btn" type="submit" aria-label="搜索"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-search"></use></svg></i></button></div></form>                    <div id="j-user-wrap">
                        <a class="login" href="https://blog.ytso.com/login">登录</a>
                        <a class="login register" href="https://blog.ytso.com/register">注册</a>
                    </div>
                                            <a class="wpcom-btn btn-primary btn-xs publish" href="https://blog.ytso.com/contribute">
                            <i class="fa fa-edit"></i> 投稿                        </a>
                                </div>
        </div>
    </div><!-- /.container -->
</header>

<div id="wrap">    <div class="wrap container">
        <ol class="breadcrumb" vocab="https://schema.org/" typeof="BreadcrumbList"><li class="home" property="itemListElement" typeof="ListItem"><a href="https://blog.ytso.com" property="item" typeof="WebPage"><span property="name" class="hide">云图网</span>首页</a><meta property="position" content="1"></li><li property="itemListElement" typeof="ListItem"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-arrow-right-3"></use></svg></i><a href="https://blog.ytso.com/category/tech" property="item" typeof="WebPage"><span property="name">技术专区</span></a><meta property="position" content="2"></li><li property="itemListElement" typeof="ListItem"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-arrow-right-3"></use></svg></i><a href="https://blog.ytso.com/category/tech/pnotes" property="item" typeof="WebPage"><span property="name">编程笔记</span></a><meta property="position" content="3"></li></ol>        <main class="main">
                            <article id="post-175424" class="post-175424 post type-post status-publish format-standard hentry category-pnotes entry">
                    <div class="entry-main">
                                                                        <div class="entry-head">
                            <h1 class="entry-title">MySql大表分页(附独门秘技)</h1>
                            <div class="entry-info">
                                                                <time class="entry-date published" datetime="2021-10-12T05:11:51+08:00" pubdate>
                                    2021年10月12日 05:11                                </time>
                                <span class="dot">•</span>
                                <a href="https://blog.ytso.com/category/tech/pnotes" rel="category tag">编程笔记</a>                                                            </div>
                        </div>
                        
                                                <div class="entry-content text-indent text-justify">
                            <div id="toc">
<p class="toc-title"><strong>文章目录</strong><span class="toc-controller toc-controller-show">[隐藏]</span></p>
<ul>
<li class="toc-level2"><a href="#toc_1">问题背景</a></li>
<li class="toc-level2"><a href="#toc_2">初步方案1</a>
<ul>
<li class="toc-level3"><a href="#toc_3">点评</a></li>
</ul>
</li>
<li class="toc-level2"><a href="#toc_4">&nbsp;深入分析</a></li>
<li class="toc-level2"><a href="#toc_5">方案2</a>
<ul>
<li class="toc-level5"><a href="#toc_6">注意：</a></li>
</ul>
<li class="toc-level3"><a href="#toc_7">点评</a></li>
</ul>
</li>
<li class="toc-level2"><a href="#toc_8">进一步深入分析</a></li>
<li class="toc-level2"><a href="#toc_9">&nbsp;方案3</a>
<ul>
<li class="toc-level5"><a href="#toc_10">&nbsp;注意：</a></li>
</ul>
</li>
<li class="toc-level2"><a href="#toc_11">总结</a>
<ul>
<li class="toc-level5"><a href="#toc_12">题外话</a></li>
</ul>
<li class="toc-level3"><a href="#toc_13"></a></li>
</ul>
</li>
</ul>
</div>
<h2 id="toc_1">问题背景</h2>
<p>MySql(InnoDB)中的订单表需要按时间顺序分页查询，且主键不是时间维度递增，订单表在百万以上规模，此时如何高性能地实现该需求？</p>
<p>注：本文并非主要讲解如何建立索引，以下的分析均建立在有合适的索引的前提下</p>
<p><span id="more-48435"></span></p>
<h2 id="toc_2">初步方案1</h2>
<p>众所周知，MySql中，有一个limit offset, pageSize的用法，可以实现分页查询</p>
<div class="cnblogs_code">
<pre><span style="color: #0000ff">select</span> <span style="color: #808080">*</span> <span style="color: #0000ff">from</span> <span style="color: #0000ff">order</span> <span style="color: #0000ff">where</span> <span style="color: #ff00ff">user_id</span> <span style="color: #808080">=</span> xxx <span style="color: #808080">and</span> 【其它业务条件】 <span style="color: #0000ff">order</span> <span style="color: #0000ff">by</span> created_time, id limit offset, pageSize</pre>
</div>
<p>因为created_time可能重复，所以order by时应加上id，保证顺序的确定性</p>
<h3 id="toc_3">点评</h3>
<p>该方案在表规模较小的时候，不会暴露出问题，当order表增长到十万级，并且查询后面几页的时候，执行速度明显变慢，可能降到100ms的量级，如果数据量增长到百万级，则耗时达到秒级，如果增长到千万级，那耗时就变得完全不可接受了（曾排查过这样的线上慢SQL）</p>
<hr class="wp-block-separator is-style-wide"/>
<h2 id="toc_4">&nbsp;深入分析</h2>
<p>方案1为啥在大表中表现这么差呢？我们可以来揣测一下MySql是怎么执行这个查询的</p>
<p>假设我们在user_id，created_time，以及【其它业务条件】建立了联合索引，当我要查找第100000条到100049条的记录时，因为MySql的索引是b+ tree结构，不像数组可以随机定位到第N条记录，它需要花不小的成本去找到N的位置，N越大，成本越大</p>
<p>抛开b+ tree的细节不讲，我们还可以借助统计表记录总数的SQL来理解</p>
<div class="cnblogs_code">
<pre><span style="color: #0000ff">select</span> <span style="color: #ff00ff">count</span>(<span style="color: #800000;font-weight: bold">1</span>) <span style="color: #0000ff">from</span> <span style="color: #0000ff">order</span></pre>
</div>
<p>如果能非常高效地定位第N条记录，那么上述统计也能非常高效的执行，但实际上，在大表中统计记录总条数，也是非常慢的(本文是在InnoDB的场景下)</p>
<p>方案1低效的根本原因在于：定位到offset的成本过高，未能充分利用索引的有序性</p>
<hr class="wp-block-separator is-style-wide"/>
<h2 id="toc_5">方案2</h2>
<p>索引(b+ tree)的特点在于，数据是有序的，虽然找到第N条记录的效率比较低，但找到某一条数据在索引中的位置，其效率是很高的（索引本来就是解决这个问题的）</p>
<p>我们换一种思路，每次取50条记录，第一次取的时候，指定从上次结束的位置继续往后取50条，这样，我们便可以利用上索引的有序性了</p>
<p>我们先看一个以id为序，进行分页查询的例子</p>
<div class="cnblogs_code">
<pre><span style="color: #0000ff">select</span> <span style="color: #808080">*</span> <span style="color: #0000ff">from</span> <span style="color: #0000ff">order</span> <span style="color: #0000ff">where</span> id <span style="color: #808080">&gt;</span> <span style="color: #ff0000">'</span><span style="color: #ff0000">pre max id</span><span style="color: #ff0000">'</span> <span style="color: #0000ff">order</span> <span style="color: #0000ff">by</span> id limit <span style="color: #800000;font-weight: bold">50</span></pre>
</div>
<p>第一次查询不用带条件，后续查询则传入前一次查询的最大id，简单分析可知，MySql在执行时，先定位到pre max id的位置(id是有序的，定位非常快)，然后从这往后取50条记录即可，整个过程非常高效</p>
<p>我们回到最开始的问题，“按时间顺序分页查询，且主键不是时间维度递增”，此时我们不能用id作为分页的条件，因为按它去分页，便不是按时间顺序了，但也不能直接把id换成时间，因为时间可能会重复，我们来分析一下</p>
<figure class="wp-block-table is-style-regular">
<table>
<thead>
<tr>
<th class="has-text-align-center" data-align="center">id</th>
<th class="has-text-align-center" data-align="center">username</th>
<th class="has-text-align-center" data-align="center">created_time</th>
</tr>
</thead>
<tbody>
<tr>
<td class="has-text-align-center" data-align="center">xxx</td>
<td class="has-text-align-center" data-align="center">zhangsan</td>
<td class="has-text-align-center" data-align="center">2019-01-01</td>
</tr>
<tr>
<td class="has-text-align-center" data-align="center">ddd</td>
<td class="has-text-align-center" data-align="center">zhangsan</td>
<td class="has-text-align-center" data-align="center">2019-02-03</td>
</tr>
<tr>
<td class="has-text-align-center" data-align="center">yyy</td>
<td class="has-text-align-center" data-align="center">zhangsan</td>
<td class="has-text-align-center" data-align="center">2019-02-03</td>
</tr>
<tr>
<td class="has-text-align-center" data-align="center">abc</td>
<td class="has-text-align-center" data-align="center">zhangsan</td>
<td class="has-text-align-center" data-align="center">2019-02-05</td>
</tr>
<tr>
<td class="has-text-align-center" data-align="center">aaa</td>
<td class="has-text-align-center" data-align="center">zhangsan</td>
<td class="has-text-align-center" data-align="center">2020-08-01</td>
</tr>
</tbody>
</table>
</figure>
<p class="has-text-align-left">假如前一次分页的最后一条记录为id=ddd的这条（created_time为2019-02-03），下一次查询使用created_time&gt;2019-02-03作为条件时，则会把id=yyy的这条记录漏掉，如果换成created_time&gt;=2019-02-03也不行，id=ddd的这条记录就又被查出来了</p>
<p class="has-text-align-left">对于这个数据遗漏或重复的问题，我看到一种解决方案是这样的：</p>
<p class="has-text-align-left">分三种情况进行查询</p>
<h5 class="has-text-align-left" id="toc_6">注意：</h5>
<p class="has-text-align-left">created_time不能为null，否=和&gt;会返回null，导致对应结果查不出来，如果存在为null的情况，则需要对部分查询把=和&gt;分别改为is null和is not null来查询</p>
<h3 class="has-text-align-left" id="toc_7">点评</h3>
<p class="has-text-align-left">上述方法确实可以解决漏掉数据或重复的问题，并且也有着不错的性能，但缺点也比较明显，查询过于复杂，得分情况执行不同的SQL，并且分页不稳定，中间查询出来的记录数可能小于pageSize（如果没有重复项，那会多出一倍的结果为空的查询），实际上后面还有数据</p>
<hr class="wp-block-separator is-style-wide"/>
<h2 id="toc_8">进一步深入分析</h2>
<p>我尝试在网上找过资料，只找到了以id为分页顺序，然后用id&gt;&#8217;pre max id&#8217;这种方式来查，而我们要以可重复的created_time为分页顺序，如何写出简洁高效的SQL呢？</p>
<p>如果要成为一个优秀的程序员，我觉得分析&amp;解决新问题的能力，是必不可少的，即使在网上能找到解决方案，优秀的分析能力也有助于借鉴并结合自己的场景，优化出更好的个性化方案。</p>
<p>我们在(user_id,created_time)建立了索引，并且我们知道InnoDB的辅助索引是包含了主键的，且主键一定不会重复，这意味着在索引上，每条记录的顺序是完全确定的，不存在重复的情况</p>
<p>我们要分页的顺序跟此索引的顺序是吻合的，只需要沿着索引，一批一批地取数据就可以了，这是一个对索引很直接的利用，为什么现在我没办法做到？</p>
<p>如果我是MySql的设计人员，针对这种很常见很直接的需求，我怎么去提供支持？还是说不支持？</p>
<p>我举一个例子，像java中的基于排序的TreeSet，我猜它一定有floor和ceiling这样的方法(返回Set中，大于或小于指定元素的第一个元素)，这是基于排序的数据结构该有的东西，如果它没有，那早被人喷了然后加上去了</p>
<p>回到索引的话题，这种直接的需求，它应该支持，否则说不过去，现在的问题变成了：用什么语法来，来实现在组合索引上，基于组合(user_id,created_time,id的组合)顺序的遍历？</p>
<p>此时脑海里便回想起以前用过的(a,b) in ((1,2),(3,4),(7,4))这样的组合写法，然后猜测它也支持大于小于这类比较，跑去MySql中验证一下：</p>
<div class="cnblogs_code">
<pre><span style="color: #0000ff">select</span> (<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">7</span>)<span style="color: #808080">&gt;</span>(<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">7</span>),    (<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">6</span>)<span style="color: #808080">&gt;</span>(<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">7</span>),    (<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">8</span>)<span style="color: #808080">&gt;</span>(<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">7</span>),    (<span style="color: #800000;font-weight: bold">4</span>,<span style="color: #800000;font-weight: bold">7</span>)<span style="color: #808080">&gt;</span>(<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">7</span>),    (<span style="color: #800000;font-weight: bold">4</span>,<span style="color: #800000;font-weight: bold">2</span>)<span style="color: #808080">&gt;</span>(<span style="color: #800000;font-weight: bold">3</span>,<span style="color: #800000;font-weight: bold">7</span><span style="color: #000000">);
返回：
</span><span style="color: #800000;font-weight: bold">0</span>    <span style="color: #800000;font-weight: bold">0</span>    <span style="color: #800000;font-weight: bold">1</span>    <span style="color: #800000;font-weight: bold">1</span>    <span style="color: #800000;font-weight: bold">1</span></pre>
</div>
<p>如此一来，这问题就变得和id&gt;&#8217;pre max id&#8217;这种一样简单了。</p>
<p>这种写法在官方文档中也找到了对应的资料，官方称这类运算为“行比较”（row comparisons）</p>
<p><a href="https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html#operator_greater-than" target="_blank" rel="noreferrer noopener">https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html#operator_greater-than</a></p>
<p>看到这里，也许你跟我当时一样，即开心又兴奋，一个完美的方案就在眼前，然而MySql优化器没有我们想像的聪明，在“行比较”面前，就变成了二傻子，不能很好地使用索引了</p>
<p>此时我又回过头去试验了一下“行比较”对应的等价写法</p>
<div class="cnblogs_code">
<pre>(a,b)<span style="color: #808080">&gt;</span><span style="color: #000000">(x,y)
等价于
a</span><span style="color: #808080">&gt;</span>x <span style="color: #808080">or</span> (a<span style="color: #808080">=</span>x <span style="color: #808080">and</span> b<span style="color: #808080">&gt;</span>y)</pre>
</div>
<p>发现这种看似很复杂且还有or的写法，竟然能很好地使用索引，效率非常高，即使像(a,b,c)&gt;(x,y,z)，改成很复杂的等价写法：</p>
<div class="cnblogs_code">
<pre>a<span style="color: #808080">&gt;</span>x <span style="color: #808080">or</span> (a<span style="color: #808080">=</span>x <span style="color: #808080">and</span> (b<span style="color: #808080">&gt;</span>y <span style="color: #808080">or</span> (b<span style="color: #808080">=</span>y <span style="color: #808080">and</span> c<span style="color: #808080">&gt;</span>z)))</pre>
</div>
<p>也能很好地使用索引，此时真不知道该夸它还是骂它，唉</p>
<p>关于“行比较”的索引选择，在官网找到这样一份资料，文中说索引覆盖不到时，建议拆开成普通写法，这样看来，也许人家是有什么苦衷吧</p>
<p><a href="https://dev.mysql.com/doc/refman/5.7/en/row-constructor-optimization.html" target="_blank" rel="noreferrer noopener">https://dev.mysql.com/doc/refman/5.7/en/row-constructor-optimization.html</a></p>
<h2 class="has-text-align-left" id="toc_9">&nbsp;方案3</h2>
<p>由于有了a&gt;x or (a=x and b&gt;y)这种等价于组合比较的语法，且能正确地使用索引，所以可以写出高效且还算简洁的SQL</p>
<div class="cnblogs_code">
<pre><span style="color: #0000ff">select</span> <span style="color: #808080">*</span> <span style="color: #0000ff">from</span> <span style="color: #0000ff">order</span> <br /><br /><span style="color: #0000ff">where</span> <span style="color: #ff00ff">user_id</span> <span style="color: #808080">=</span> xxx <span style="color: #808080">and</span> 【其它业务条件】 <span style="color: #808080">and</span> (created_time <span style="color: #808080">&gt;</span> <span style="color: #ff0000">'</span><span style="color: #ff0000">created_time of latest recode</span><span style="color: #ff0000">'</span> <span style="color: #808080">or</span> (created_time <span style="color: #808080">=</span> <span style="color: #ff0000">'</span><span style="color: #ff0000">created_time of latest recode</span><span style="color: #ff0000">'</span> <span style="color: #808080">and</span> id <span style="color: #808080">&gt;</span> <span style="color: #ff0000">'</span><span style="color: #ff0000">id of latest recode</span><span style="color: #ff0000">'</span>))  <br /><br /><span style="color: #0000ff">order</span> <span style="color: #0000ff">by</span> created_time, id limit pageSize</pre>
</div>
<p>此方式跟以id为序的分页查询是一样的，首次查询去掉组合条件即可，代码略显复杂，好在可以利用上组合索引，十分高效，耗时稳定，不会因为遍历到末尾而性能降低</p>
<p>遗憾地是，最优雅的方式却撞见个二傻子优化器，按理说用他们支持的特定语法(变化范围更小，模式更固定)去精确地表达查询需求，应该更容易被优化器识别出来并用最优方案去执行才说得通，结果却不如人意</p>
<p>希望以后能MySql更好地支持“行比较”吧(8.0.19仍存在问题)</p>
<h5 id="toc_10">&nbsp;注意：</h5>
<p>这里也不允许created_time为null，因为null值参与&gt;和=运算，结果一律为null，即条件不成立，相应结果查不出来。</p>
<p>如果存在为null的情况，则要作一些调整，如果前一批数据的最后一条记录的created_time为null（null在索引中被视作极小值），则可以这样改：</p>
<div class="cnblogs_code">
<pre>(created_time <span style="color: #0000ff">is</span> <span style="color: #808080">not</span> <span style="color: #0000ff">null</span> <span style="color: #808080">or</span> (created_time <span style="color: #0000ff">is</span> <span style="color: #0000ff">null</span> <span style="color: #808080">and</span> id <span style="color: #808080">&gt;</span> <span style="color: #ff0000">'</span><span style="color: #ff0000">id of latest recode</span><span style="color: #ff0000">'</span>))</pre>
</div>
<p>仍旧可以走索引，实现高效分页查询</p>
<hr class="wp-block-separator is-style-wide"/>
<h2 id="toc_11">总结</h2>
<p>方案1在小表的情况下，简单方便，只用传页码和页大小即可，还可以随机跳到指定页，具有一定优势</p>
<p>方案2和方案3在大表的情况下，有着优异的性能，以及稳定性，缺点是不能随机地跳转页面，需要传入上一页的排序字段。这个弊端在一定程度上可以规避，比如现在很多分页都是一页一页地往下翻，比如微博、朋友圈动态等，或者是分批处理全表数据，不需要随机跳转</p>
<p>细心的同学可能发现，where条件里还有【其它业务条件】，这样还能正常走索引吗？是否会发生全表扫描？这个问题其实是可以规避的，有空再写一篇执行计划并不完全可靠的案例。</p>
<p>注：执行计划有时不能正确地反映实际执行效果，所以我没有贴执行计划；我使用的MySql版本为5.7.23和8.0.19</p>
<h5 id="toc_12">题外话</h5>
<p>方案3的写法是我自己琢磨出来的，在网上也没找到类似的资料，算独门秘技吧，除此之外，我觉得同样很有价值的是【进一步深入分析】中的思考过程，如果养成这种思考习惯，有利于创新，去解决别人没遇到过的问题，在未知的领域，知道该从哪个方向去寻找答案；或者找到新的方法更好地去解决旧问题。</p>
<p>如果本文有帮助到你，或者觉得有价值，麻烦点个赞，这样我会更有动力去更多地分享自己的经验</p>
<hr class="wp-block-separator is-style-wide"/>
<p>转载请注明出处及作者</p>
</p>
<h3 id="toc_13"></h3>
<div class="entry-readmore"><div class="entry-readmore-btn"></div></div>                                                        <div class="entry-copyright"><p>原创文章，作者：奋斗，如若转载，请注明出处：https://blog.ytso.com/tech/pnotes/175424.html</p></div>                        </div>

                        <div class="entry-tag"></div>
                        <div class="entry-action">
                            <div class="btn-zan" data-id="175424"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-thumb-up-fill"></use></svg></i> 赞 <span class="entry-action-num">(0)</span></div>
                                                    </div>

                        <div class="entry-bar">
                            <div class="entry-bar-inner">
                                                                <div class="entry-bar-info entry-bar-info2">
                                    <div class="info-item meta">
                                                                                    <a class="meta-item j-heart" href="javascript:;" data-id="175424"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-star"></use></svg></i> <span class="data">0</span></a>                                        <a class="meta-item" href="#comments"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-comment"></use></svg></i> <span class="data">0</span></a>                                                                            </div>
                                    <div class="info-item share">
                                                                                    <a class="meta-item mobile j-mobile-share" href="javascript:;" data-id="175424" data-qrcode="https://blog.ytso.com/tech/pnotes/175424.html">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-share"></use></svg></i> 生成海报                                            </a>
                                                                                    <a class="meta-item wechat" data-share="wechat" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-wechat"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item weibo" data-share="weibo" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-weibo"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item qq" data-share="qq" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-qq"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item qzone" data-share="qzone" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-qzone"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item douban" data-share="douban" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-douban"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item linkedin" data-share="linkedin" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-linkedin"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item facebook" data-share="facebook" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-facebook"></use></svg></i>                                            </a>
                                                                                    <a class="meta-item twitter" data-share="twitter" target="_blank" rel="nofollow noopener noreferrer" href="#">
                                                <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-twitter"></use></svg></i>                                            </a>
                                                                            </div>
                                    <div class="info-item act">
                                        <a href="javascript:;" id="j-reading"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-article"></use></svg></i></a>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                                            <div class="entry-page">
                    <div class="entry-page-prev entry-page-nobg">
                <a href="https://blog.ytso.com/tech/pnotes/175423.html" title="深度解析springcloud分布式微服务的实现" rel="prev">
                    <span>深度解析springcloud分布式微服务的实现</span>
                </a>
                <div class="entry-page-info">
                    <span class="pull-left"><i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-arrow-left-double"></use></svg></i> 上一篇</span>
                    <span class="pull-right">2021年10月12日 04:00</span>
                </div>
            </div>
                            <div class="entry-page-next entry-page-nobg">
                <a href="https://blog.ytso.com/tech/pnotes/175425.html" title="如何基于jackson动态序列化指定字段" rel="next">
                    <span>如何基于jackson动态序列化指定字段</span>
                </a>
                <div class="entry-page-info">
                    <span class="pull-right">下一篇 <i class="wpcom-icon wi"><svg aria-hidden="true"><use xlink:href="#wi-arrow-right-double"></use></svg></i></span>
                    <span class="pull-left">2021年10月12日 05:11</span>
                </div>
            </div>
            </div>
                                        <error>
    <code>wp_die</code>
    <title><![CDATA[WordPress &amp;rsaquo; Error]]></title>
    <message><![CDATA[&lt;h1&gt;Error establishing a Redis connection&lt;/h1&gt;
&lt;p&gt;To disable Redis, delete the &lt;code&gt;object-cache.php&lt;/code&gt; file in the &lt;code&gt;/wp-content/&lt;/code&gt; directory.&lt;/p&gt;
]]></message>
    <data>
        <status>500</status>
    </data>
</error>
<!--
Performance optimized by Redis Object Cache. Learn more: https://wprediscache.com

Retrieved 1726 objects (296 KB) from Redis using Predis (v2.4.0).
-->
