深度分页优化思路

深度分页优化思路

思考以下问题

查询以下SQL的流程是怎么样的呢?
为什么只查询10条数据需要7秒?

# 查询时间7秒
SELECT * FROM user ORDER BY age LIMIT 1000000, 10

问题分析

为什么分页查询随着翻页的深入,会变得越来越慢。

其实,问题的根本就在于:
第一数据量太大
第二数据库处理分页的方法太笨

你以为LIMIT 100000,10是直接跳过后10万条? 太天真了!

数据库的真实操作:

第一步: 把整张表的数据全捞出来(全表扫描),按年龄排好序(文件排序)。
第二步: 吭哧吭哧数到第100010条,再给你返回最后10条。

相当于:让你从新华字典第1页开始翻,翻到第1000页才找到字,谁能不炸?

最坑爹环节:回表查数据
如果用了普通索引(比如按年龄建的索引):

  • 先查索引:按年龄找到对应的主键ID(快速)
  • 再回表:用ID去主键索引里捞完整数据(慢!)

10万次回表 = 10万次IO操作,不卡你卡谁?

再说另一个常见的情况——排序

大多数时候,分页查询都会带有排序,比如按时间、按ID排序。

数据库不仅要查数据,还得根据你的排序要求重新排一次,特别是在数据量大的时候,排序的开销就变得非常大。

所以,翻越几百页的时候,你的查询可能就开始慢得像蜗牛。

单表场景 limit 深度分页 的优化方法

核心思路: 绕过全表扫描,直接定位到目标数据!

方案一:子查询优化

mysql> explain SELECT *-> FROM user-> WHERE id >= ->     (->         SELECT id->         FROM user->         ORDER BY age->         LIMIT 1000000, 1-> 	   )-> limit 10;
+----+-------------+--------------------+------------+-------+---------------+--------------+---------+-------+---------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows   | filtered | Extra       |
+----+-------------+--------------------+------------+-------+---------------+--------------+---------+-------+---------+----------+--------------------------+
|  1 | PRIMARY     | user  | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL  |     10 |   100.00 | Using where |
|  2 | SUBQUERY    | user  | NULL       | ref   | idx_age       | idx_age | 2       | const | 432791 |   100.00 | Using index |
+----+-------------+--------------------+------------+-------+---------------+--------------+---------+-------+---------+----------+--------------------------+
2 rows in set, 1 warning (0.15 sec)

原理: 用覆盖索引快速找到第100000条的ID,直接从这个ID开始拿数据,跳过前面10万次回表。

缺点是,不适用于结果集不以ID连续自增的分页场景。

在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的ID,此时的ID是离散且不连续的。如果使用上述的方式,并不能筛选出目标数据

方案二:延时关联

SELECT * FROM user t1
JOIN (SELECT id FROM user ORDER BY age LIMIT 1000000,10) t2 
ON t1.id = t2.id;
mysql> explain SELECT * -> FROM->     user t1->     JOIN (->         SELECT->             id->         FROM->             user->         ORDER BY->             age->         LIMIT 1000000,10->         ) AS t2 ->     ON t1.id = t2.id-> LIMIT 10;
+----+-------------+--------------------+------------+--------+---------------+--------------+---------+-------+---------+----------+--------------------------+
| id | select_type | table      | partitions | type   | possible_keys | key     | key_len | ref   | rows   | filtered | Extra       |
+----+-------------+--------------------+------------+--------+---------------+--------------+---------+-------+---------+----------+--------------------------+
|  1 | PRIMARY     | <derived2> | NULL       | ALL    | NULL          | NULL    | NULL    | NULL  | 432791 |   100.00 | NULL        |
|  1 | PRIMARY     | t1         | NULL       | eq_ref | PRIMARY       | PRIMARY | 8       | t2.id |      1 |   100.00 | NULL        |
|  2 | DERIVED     | user       | NULL       | ref    | idx_age       | idx_age | 9       | const | 432791 |   100.00 | Using index |
+----+-------------+--------------------+------------+--------+---------------+--------------+---------+-------+---------+----------+--------------------------+
3 rows in set, 1 warning (0.12 sec)

原理: 先用索引快速拿到10个目标ID,再一次性联表查完整数据,减少回表次数。

方案三:索引覆盖

索引覆盖(Index Covering)是指一个查询可以完全通过索引来执行,而无需通过回表来查询其他字段数据。

例如:

ALTER TABLE user ADD INDEX idx_age_name(age, name);  -- 查询+排序全走索引
SELECT age, name FROM user ORDER BY age LIMIT 1000000,10;  -- 0.08秒!

精髓: 索引里直接存了所有要查的字段,不用回表,直接起飞!
缺点: 如果每个查询都建对应的索引的话,会浪费更多的空间存储索引,也会影响插入时的速度。

方案四:书签记录

从原理上说,属于是一种滚动查询。也就是说我们必须从第一页开始查询,然后获取本页最大的记录 ID,然后再根据大于最大记录 ID 的数据向后持续滚动。也就是说,我们如果想查询大于 1000000 后记录的 10 条,那我们就得知道 1000000 条的 ID。因为 ID 是递增的,所以直接查询即可。

SELECT * FROM user WHERE id > 1000000 LIMIT 10; -- 500微秒!

精髓: 每次查询都用上次查询结果做书签,直接走主键索引
缺点: 不支持条页查询,主键必须是自增的。

分库分表后,翻页为什么更慢了?

分库分表的翻页逻辑

假设订单表分了3个库,每个库分了2张表(共6张表),按用户ID分片。
当你执行:

SELECT * FROM orders ORDER BY create_time DESC LIMIT 1000000, 10;  

你以为数据库的操作:
智能跳过100万条,从6张表各拿10条,合并完事?

实际上的操作:

  • 每张表都老老实实查100万+10条数据(共600万+60条)。
  • 把所有数据汇总到内存,重新排序(600万条数据排序,内存直接炸穿)。
  • 最后忍痛扔掉前650万条,给你10条结果。

结果: 查一次耗时10秒+,数据库CPU 100%!


分库分表翻页的存在的3个问题

  • 数据分散,全局排序难
    各分片数据独立排序,合并后可能乱序,必须全量捞数据重排。
  • 深分页=分片全量扫描
    每张表都要查 offset + limit 条数据,性能随分片数量指数级下降。
  • 内存归并压力大
    100万条数据 × 6个分片 = 600万条数据在内存排序,分分钟OOM!

一句话总结: 分库分表后,翻页越深,死得越惨!


3种解决分库分表深度翻页方案

方案1:禁止跳页(青铜方案)

核心思想: 别让用户随便跳页,只能一页一页翻!其实就是上面的书签记录

实现方法:
1.第一页查询:

-- 按时间倒序,拿前10条  
SELECT * FROM orders  
WHERE user_id = 123  
ORDER BY create_time DESC  
LIMIT 10;  

2.翻下一页:

-- 记住上一页最后一条的时间  
SELECT * FROM orders  
WHERE user_id = 123  
AND create_time < '2023-10-01 12:00:00'  -- 上一页最后一条的时间  
ORDER BY create_time DESC  
LIMIT 10;  

优点:

  • 性能:每页查询只扫索引的10条,0回表。
  • 内存:无需全量排序。

缺点:

  • 用户不能跳页(比如从第1页直接跳到第100页)。
  • 适合Feed流场景(如朋友圈、抖音),不适合后台管理系统。

方案2:二次查询法(黄金方案)

核心思想: 把分库分表的“大海捞针”,变成“精准狙击”!

实现步骤:
1. 第一轮查询:每张分片查缩小范围的数据

-- 每张分片查 (offset / 分片数量) + limit 条  
SELECT create_time FROM orders  
ORDER BY create_time DESC  
LIMIT 166666, 10;  -- 假设总offset=100万,分6个分片:100万/6 ≈ 166666  

1.确定全局最小时间戳:
从所有分片结果中,找到最小的 create_time(比如 2023-09-20 08:00:00)。
2.第二轮查询:根据最小时间戳查全量数据

SELECT * FROM orders  
WHERE create_time >= '2023-09-20 08:00:00'  
ORDER BY create_time DESC  
LIMIT 10;  

优点:

  • 避免全量数据排序,性能提升6倍。
  • 支持跳页查询(如直接从100万页开始查)。

缺点:

  • 需要两次查询,逻辑复杂。
  • 极端情况下可能有误差(需业务容忍)。

方案3:ES+HBase核弹方案(王者方案)

核心思想: 让专业的人干专业的事!

  • ES:负责海量数据搜索+分页(倒排索引碾压数据库)。
  • HBase:负责存储原始数据(高并发读取无压力)。
    架构图:
    图片

实现步骤:

  1. **写入时:**订单数据同时写MySQL(分库分表)、ES、HBase。
  2. 查询时:
GET /orders/_search  
{  "query": { "match_all": {} },  "sort": [{"create_time": "desc"}],  "from": 1000000,  "size": 10  
}  
List<Order> orders = es.search(...); // 从ES拿到10个ID  
List<Order> details = hbase.batchGet(orders); // 从HBase拿详情  
  • Step2:用ES返回的ID,去HBase批量查数据。
  • Step1:用ES查分页(只查ID和排序字段)。
    优点:
  • 分页性能碾压数据库,百万级数据毫秒响应。
  • 支持复杂搜索条件(ES的强项)。
    缺点:
  • 架构复杂度高,成本飙升(ES集群要钱,HBase要运维)。
  • 数据一致性难保证(延迟可能秒级)。

面试怎么答?

1. 面试官要什么?

  • 原理理解: 知道分库分表后翻页的痛点(数据分散、归并排序)。
  • 方案灵活: 根据场景选方案(禁止跳页、二次查询、ES+HBase)。
  • 实战经验: 遇到过真实问题,用过二次查询或ES。

2. 标准答案模板

分库分表后深度分页的难点在于全局排序和内存压力。
我们有三种方案:

  • 禁止跳页: 适合C端Feed流,用连续查询代替跳页。
  • 二次查询法: 通过两次查询缩小范围,适合管理后台。
  • ES+HBase: 扛住亿级数据分页,适合高并发大厂场景。
    在实际的场景中,订单查询需要支持搜索条件,我们最终用ES+HBase,性能从10秒降到50毫秒。”

加分的骚操作:

  • 画架构图(分库分表+ES+HBase数据流向)。
  • 给性能对比数据(ES分页 vs 数据库分页)。
  • 提一致性解决方案(监听MySQL Binlog同步到ES)。

总结

分库分表后的深度分页,本质是 “分布式数据排序” 的难题。

  • 百万以内数据: 二次查询法性价比最高。
  • 高并发大厂场景: ES+HBase是唯一选择。
  • 千万别硬刚: LIMIT 1000000,10 就是自杀式操作!
    最后一句忠告:
    面试被问分页,先拍桌子喊出“禁止跳页”,再掏出ES,面试官绝对眼前一亮!

本文改编自

  • 京东二面:分库分表后翻页100万条,怎么设计?答对这题直接给P7!
  • 《牛券oneCoupon优惠券系统设计》第21小节:优惠券分发失败记录深分页优化

如侵权,请联系删除

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/75724.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用 Vite 提升前端开发体验:入门与配置指南

在现代前端开发中&#xff0c;构建工具的选择对开发效率和项目性能有着至关重要的影响。Vite 是一个新兴的前端构建工具&#xff0c;由 Vue.js 的作者尤雨溪开发&#xff0c;旨在通过利用现代浏览器的原生 ES 模块特性&#xff0c;提供更快的开发服务器启动速度和更高效的热更新…

MYSQL基本语法使用

目录 一、mysql之DML 增加语句 删除语句和truncate 更新语句 replace语句 select查询语句 二、select多种用法 查询时的别名使用 分组 分组后的筛选 结果排序 分页功能 分表 多表关联查询 练习题 一、单表查询 二、多表查询 前面已经学习了mysql的安装和基本语…

自动化测试selenium(Java版)

1.准备工作 1.1.下载浏览器 自动化测试首先我们要准备一个浏览器,我们这里使用谷歌(chrome)浏览器. 1.2.安装驱动管理 每一个浏览器都是靠浏览器驱动程序来启动,但是浏览器的版本更新非常快,可能我们今天测试的是一个版本,第二天发布了一个新的版本,那么我们就要重构代码,很…

HarmonyOS Next应用架构设计与模块化开发详解

引言 在HarmonyOS Next开发中&#xff0c;合理的应用架构设计和模块化开发是构建高效、可维护应用的关键。本文将深入探讨HarmonyOS Next应用的架构设计思路&#xff0c;并通过实际代码示例展示如何实现模块化开发。 应用架构设计 HarmonyOS Next应用通常采用分层架构设计&…

伊利工业旅游4.0,近距离感受高品质的魅力

3月24日&#xff0c;在2025年第112届全国糖酒会&#xff08;简称春糖&#xff09;前夕&#xff0c;伊利集团“可感知高品质探寻荟”活动在成都召开&#xff0c;记者走进伊利在西南地区最大的乳制品生产基地—邛崃工厂&#xff0c;零距离见证液态奶、酸奶、冷饮等乳制品的诞生&a…

测试用例生成平台通过大模型升级查询功能,生成智能测试用例

在测试工作中&#xff0c;查询功能是各类系统的核心模块&#xff0c;传统的测试用例编写往往耗时且重复。如何让老旧平台焕发新活力&#xff1f;本文将结合大模型技术&#xff0c;通过用户输入的字段信息&#xff0c;自动化生成高效、精准的测试用例。同时&#xff0c;我们还将…

基于javaweb的SpringBoot雪具商城系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…

【AI学习笔记】Coze平台实现将Excel文档批量导入数据库全过程

背景前摇&原视频教程&#xff1a; 最近看到很多同学都在用Coze平台操作数据&#xff0c;我也想了解一下工作流的搭建和数据处理过程&#xff0c;但是一下子又看不懂太复杂的逻辑&#xff0c;于是上B站搜索相关的基础教程。 Coze官方教程&#xff1a; 之前有看过Coze平台…

【Axure高保真原型】纵向图片轮播

今天和大家分享纵向图片轮播的原型模版&#xff0c;载入后会自动循环轮播&#xff0c;鼠标移入图片后停止轮播&#xff0c;可以通过点击上下箭头&#xff0c;向上或向下滑动切换上一张或下一张图片&#xff0c;也可以点击右侧小圆点快速切换至对应图片……具体效果可以打开下方…

力扣32.最长有效括号(栈)

32. 最长有效括号 - 力扣&#xff08;LeetCode&#xff09; 代码区&#xff1a; #include<stack> #include<string> /*最长有效*/ class Solution { public:int longestValidParentheses(string s) {stack<int> st;int ans0;int ns.length();st.push(-1);fo…

如何在 React 项目中使用React.lazy和Suspense实现组件的懒加载?

大白话如何在 React 项目中使用React.lazy和Suspense实现组件的懒加载&#xff1f; 在 React 项目里&#xff0c;有时候组件功能多、体积大&#xff0c;要是一次性把所有组件都加载进来&#xff0c;网页加载速度就会变慢。而 React 提供了 React.lazy 和 Suspense 这两个好东西…

ffmpeg-将多个视频切片成一个新的视频

使用 ffmpeg 工具可以轻松完成将多个视频切片合并为一个新的视频。以下是实现这一目标的具体步骤和命令。 步骤概览 1、将多个视频切片。 2、创建文本文件列出切片的视频片段。 3、使用 ffmpeg 合并这些切片为一个新的视频。 一&#xff1a;安装 ffmpeg 确保你的系统中已经安…

【第2月_day10】Pandas数据查看与选择

以下是专为小白设计的 Pandas数据查看与选择 学习内容&#xff0c;从基础到应用逐步讲解&#xff0c;附带清晰示例和注意事项&#xff1a; 一、数据查看&#xff1a;快速了解你的数据 1. head() 和 tail() 作用&#xff1a;查看数据的前几行或后几行&#xff0c;默认显示5行。…

Jetpack LiveData 使用与原理解析

一、引言 在 Android 开发中&#xff0c;数据的变化需要及时反映到界面上是一个常见的需求。然而&#xff0c;传统的方式可能会导致代码复杂、难以维护&#xff0c;并且容易出现内存泄漏等问题。Jetpack 组件中的 LiveData 为我们提供了一种优雅的解决方案&#xff0c;它是一种…

Unity2D 五子棋 + Photon联网双人对战

开发环境配置 Unity版本2022.3 创建Photon账号以及申请Photon中国区服务 官网申请账号&#xff1a;Multiplayer Game Development Made Easy Photon Engine 中国区服务&#xff1a; 光子引擎photonengine中文站 成都动联无限科技有限公司(vibrantlink.com) 导入PUN2插件以及…

(UI自动化测试web端)第二篇:元素定位的方法_css定位之属性选择器

看代码里的【find_element_by_css_selector( )】( )里的表达式怎么写&#xff1f; 文章介绍了第四种写法属性选择器 &#xff0c;你要根据网页中的实际情况来判断自己到底要用哪一种方法来进行元素定位。每种方法都要多练习&#xff0c;全都熟了之后你在工作当中使用起来元素定…

预编译能否 100%防 sql 注入?

&#x1f31f; 什么是 SQL 注入&#xff1f; SQL 注入&#xff08;SQL Injection&#xff09;是指攻击者利用特殊输入&#xff0c;让数据库执行它本来不应该执行的代码&#xff0c;从而获取或篡改数据。 就像在考试的时候偷偷改题目&#xff0c;让老师改成你想要的内容&#…

第十五章 | Layer2、Rollup 与 ZK 技术实战解析

&#x1f4da; 第十五章 | Layer2、Rollup 与 ZK 技术实战解析 ——构建下一代高性能区块链应用&#xff0c;从 Solidity 到 zkSync&#xff01; ✅ 本章导读 Layer2 和零知识证明&#xff08;ZK&#xff09;正成为区块链发展的核心方向。 随着主网 Gas 居高不下、TPS 无法满…

2025-03-26 学习记录--C/C++-PTA 6-3 求链式表的表长

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 6-3 求链式表的表长 本题要求实现一个函数&#xff0c;求链式表的表长。 函数接口定义&#xff1a; &…

【Linux】Linux_Ubuntu与Windows之间的文件传输

一、Linux终端命令的复制粘贴 1.打开linux 终端&#xff0c;输入以下命令&#xff1a;&#xff08;注意&#xff0c;需要联网&#xff09; 2.命令行下载&#xff1a; sudo apt-get autoremove open-vm-tools 3.命令行安装&#xff1a; sudo apt-get install open-vm-tools-…