深度分页优化思路

news2025/4/1 22:33:45

深度分页优化思路

思考以下问题

查询以下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.coloradmin.cn/o/2322937.html

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

相关文章

K8S学习之基础五十四:jenkins新建测试流水线

jenkins新建测试流水线 新建任务 node(testak) {stage(第1步:从gitee上下载源代码) {git url: "https://gitee.com/akang007/jenkins-sample"script {build_tag sh(returnStdout: true, script: git rev-parse --short HEAD).trim()}}stage(第2步&#xff1a;基…

HarmonyOS NEXT(九) :图形渲染体系

HarmonyOS NEXT&#xff08;九&#xff09; &#xff1a;图形渲染体系 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;可以分享一下给大家。点击跳转到网站。 https://www.captainbed.cn/ccc 文章目录 HarmonyOS NEXT&#xff0…

SQLAlchemy关键词搜索技术深度解析:从基础过滤到全文检索

在数据驱动的应用开发中&#xff0c;基于关键词的模糊查询是常见的业务需求。SQLAlchemy作为Python生态中最流行的ORM框架&#xff0c;提供了多种实现关键词搜索的技术方案。本文将从性能、适用场景和技术复杂度三个维度&#xff0c;系统对比分析SQLAlchemy中关键词搜索的最佳实…

ES数据过多,索引拆分

公司企微聊天数据存储在 ES 中&#xff0c;虽然按照企业分储在不同的ES 索引中&#xff0c;但某些常用的企微主体使用量还是很大。4年中一个索引存储数据已经达到46多亿条数据&#xff0c;占用存储3.1tb, ES 配置 由于多一个副本&#xff0c;存储得翻倍&#xff0c;成本考虑…

uni-app自动升级功能

效果图 一、VUE login.vue <template><view><view class"uni-common-mt"><view class"uni-flex uni-column"><view class"flex-item flex-item-V"><view class"logo"><image src"/st…

使用AI一步一步实现若依(26)

功能26&#xff1a;新增一个新员工培训页面 功能25&#xff1a;角色管理 功能24&#xff1a;菜单管理 功能23&#xff1a;从后端获取路由/菜单数据 功能22&#xff1a;用户管理 功能21&#xff1a;使用axios发送请求 功能20&#xff1a;使用分页插件 功能19&#xff1a;集成My…

逻辑回归(Logistic Regression)模型的概率预测函数

以二分类问题为例&#xff0c;常见的损失函数有 负对数似然损失(neg log-likelihood loss)&#xff0c;交叉熵损失(cross entropy loss)&#xff0c;deviance loss指数损失(exponential loss)。 前三者虽然名字不同&#xff0c;但却具有相同的表达形式。此外&#xff0c;neg …

【零基础学python】python高级语法(四)

接续上面的系列文章&#xff1a; 【零基础学python】python基础语法&#xff08;一&#xff09;-CSDN博客 【零基础学python】python基础语法&#xff08;二&#xff09;-CSDN博客 【零基础学python】python高级语法&#xff08;三&#xff09;-CSDN博客 目录 2&#xff0c…

HarmonyOS 之 @Require 装饰器自学指南

在 HarmonyOS 应用开发工作中&#xff0c;我频繁碰到组件初始化传参校验的难题。在复杂的组件嵌套里&#xff0c;要是无法确保必要参数在构造时准确传入&#xff0c;就极易引发运行时错误&#xff0c;而且排查起来费时费力。一次偶然的机会&#xff0c;我接触到了 Require 装饰…

基于CNN的FashionMNIST数据集识别6——ResNet模型

前言 之前我们在cnn已经搞过VGG和GoogleNet模型了&#xff0c;这两种较深的模型出现了一些问题&#xff1a; 梯度传播问题 在反向传播过程中&#xff0c;梯度通过链式法则逐层传递。对于包含 L 层的网络&#xff0c;第 l 层的梯度计算为&#xff1a; 其中 a(k) 表示第 k层的…

0323-B树、B+树

多叉树---->B树&#xff08;磁盘&#xff09;、B树 磁盘由多个盘片组成&#xff0c;每个盘片分为多个磁道和扇区。数据存储在这些扇区中&#xff0c;扇区之间通过指针链接&#xff0c;形成链式结构。 内存由连续的存储单元组成&#xff0c;每个单元有唯一地址&#xff0c;数…

【工作记录】F12查看接口信息及postman中使用

可参考 详细教程&#xff1a;如何从前端查看调用接口、传参及返回结果&#xff08;附带图片案例&#xff09;_f12查看接口及参数-CSDN博客 1、接口信息 接口基础知识2&#xff1a;http通信的组成_接口请求信息包括-CSDN博客 HTTP类型接口之请求&响应详解 - 三叔测试笔记…

2024年认证杯SPSSPRO杯数学建模B题(第二阶段)神经外科手术的定位与导航全过程文档及程序

2024年认证杯SPSSPRO杯数学建模 B题 神经外科手术的定位与导航 原题再现&#xff1a; 人的大脑结构非常复杂&#xff0c;内部交织密布着神经和血管&#xff0c;所以在大脑内做手术具有非常高的精细和复杂程度。例如神经外科的肿瘤切除手术或血肿清除手术&#xff0c;通常需要…

Android 12系统源码_系统启动(二)Zygote进程

前言 Zygote&#xff08;意为“受精卵”&#xff09;是 Android 系统中的一个核心进程&#xff0c;负责 孵化&#xff08;fork&#xff09;应用进程&#xff0c;以优化应用启动速度和内存占用。它是 Android 系统启动后第一个由 init 进程启动的 Java 进程&#xff0c;后续所有…

MOSN(Modular Open Smart Network)-05-MOSN 平滑升级原理解析

前言 大家好&#xff0c;我是老马。 sofastack 其实出来很久了&#xff0c;第一次应该是在 2022 年左右开始关注&#xff0c;但是一直没有深入研究。 最近想学习一下 SOFA 对于生态的设计和思考。 sofaboot 系列 SOFAStack-00-sofa 技术栈概览 MOSN&#xff08;Modular O…

Flink介绍与安装

Apache Flink是一个在有界数据流和无界数据流上进行有状态计算分布式处理引擎和框架。Flink 设计旨在所有常见的集群环境中运行&#xff0c;以任意规模和内存级速度执行计算。 一、主要特点和功能 1. 实时流处理: 低延迟: Flink 能够以亚秒级的延迟处理数据流&#xff0c;非常…

【gradio】从零搭建知识库问答系统-Gradio+Ollama+Qwen2.5实现全流程

从零搭建大模型问答系统-GradioOllamaQwen2.5实现全流程&#xff08;一&#xff09; 前言一、界面设计&#xff08;计划&#xff09;二、模块设计1.登录模块2.注册模块3. 主界面模块4. 历史记录模块 三、相应的接口&#xff08;前后端交互&#xff09;四、实现前端界面的设计co…

PowerBI,用度量值实现表格销售统计(含合计)的简单示例

假设我们有产品表 和销售表 我们想实现下面的效果 表格显示每个产品的信息&#xff0c;以及单个产品的总销量 有一个切片器能筛选各个门店的产品销量 还有一个卡片图显示所筛选条件下&#xff0c;所有产品的总销量 实现方法&#xff1a; 1.我们新建一个计算表&#xff0c;把…

26考研——查找_树形查找_二叉排序树(BST)(7)

408答疑 文章目录 三、树形查找二叉排序树&#xff08;BST&#xff09;二叉排序树中结点值之间的关系二叉树形查找二叉排序树的查找过程示例 向二叉排序树中插入结点插入过程示例 构造二叉排序树的过程构造示例 二叉排序树中删除结点的操作情况一&#xff1a;被删除结点是叶结点…

【行驶证识别】批量咕嘎OCR识别行驶证照片复印件图片里的文字信息保存表格或改名字,基于QT和腾讯云api_ocr的实现方式

项目背景 在许多业务场景中,如物流管理、车辆租赁、保险理赔等,常常需要处理大量的行驶证照片复印件。手动录入行驶证上的文字信息,像车主姓名、车辆型号、车牌号码等,不仅效率低下,还容易出现人为错误。借助 OCR(光学字符识别)技术,能够自动识别行驶证图片中的文字信…