深分页问题:为什么你的分页查询会卡住?解决方案来了!

前言

Hello~大家好。我是秋天的一阵风

在业务开发中,分页是一个再常见不过的需求。无论是电商平台的商品列表,还是社交媒体的动态流,分页都是提升用户体验的重要手段。然而,当数据量逐渐增大时,分页问题就会悄然演变成一个"隐形杀手"------深分页问题。今天,我们就来深入探讨这个让无数开发者头疼的问题,并详细分析几种解决方案,尤其是Elasticsearch(ES)如何优雅地解决深分页问题。

一、什么是深分页问题?

深分页问题通常出现在需要查询大量数据的场景中。假设你有一个包含1000万条记录的表,用户想要查看第9999页的数据(每页10条)。传统的分页查询可能会使用类似以下的SQL语句:

sql 复制代码
SELECT * FROM table ORDER BY id LIMIT 99990, 10;

这条语句的逻辑是跳过前99990条记录,然后返回接下来的10条。看起来很简单,对吧?但问题在于,数据库在执行这条语句时,实际上需要扫描前99990条记录,然后再返回10条。随着偏移量的增加,查询的性能会急剧下降,甚至可能导致数据库崩溃。

这就是深分页问题的核心:偏移量越大,查询效率越低


二、深分页问题的解决方案

既然深分页问题如此棘手,那我们该如何应对呢?以下是几种常见的解决方案:

1. 游标分页(Cursor-based Pagination)

游标分页是一种基于唯一标识(如ID或时间戳)的分页方式。它的核心思想是记录上一次查询的最后一条记录的标识,下一次查询时直接从该标识之后开始查询。例如:

sql 复制代码
SELECT * FROM table WHERE id > last_id ORDER BY id LIMIT 10;

这种方式避免了偏移量的计算,性能非常稳定。但它有一个限制:用户无法直接跳转到某一页,只能一页一页地往下翻。

2. 子查询优化

在某些数据库中,可以通过子查询的方式优化深分页。例如:

sql 复制代码
SELECT * FROM table WHERE id >= (SELECT id FROM table ORDER BY id LIMIT 99990, 1) LIMIT 10;

这种方式通过子查询先定位到偏移量的起始位置,然后再查询数据,性能会比直接使用LIMIT offset, size好一些。但它仍然无法完全解决深分页的性能问题。并且这种方法只适用于 ID 是正序的。在复杂分页场景,往往需要通过过滤条件,筛选到符合条件的 ID,此时的 ID 是离散且不连续的。

另一种基于子查询的优化写法(不依赖于id有序,通用性更强

sql 复制代码
select t1.* FROM account t1, (select id from account where update_time >= '2020-09-19' limit 100000, 10) t2 where t1.id = t2.id;

3. 延迟查询

延迟关联的优化思路,跟子查询的优化思路其实是一样的 :都是希望减少回表。不同点是,延迟关联使用了inner join代替子查询

阿里巴巴《Java 开发手册》中也有对应的描述:

利用延迟关联或者子查询优化超多分页场景。

sql 复制代码
SELECT t.*
FROM table AS t
INNER JOIN (
    SELECT id
    FROM table
    ORDER BY id
    LIMIT 99990, 10  -- 获取目标分页的主键范围
) AS sub ON t.id = sub.id;
  1. 子查询的作用

    • 子查询 (SELECT id FROM table ORDER BY id LIMIT 99990, 10) 的目的是快速定位到目标分页的主键范围(id)。这里直接获取了目标分页的 10 条记录的 id,而不是像原始 SQL 那样只获取一个起始点。
    • 这样可以避免主查询中因 id >= ... 导致的范围扫描,直接定位到目标记录。
  2. 主查询的作用

    • 主查询通过 INNER JOIN 将子查询返回的 id 与原表进行关联,只返回这些 id 对应的完整记录。
    • 这种方式可以利用索引快速定位到目标记录,减少主查询的扫描范围。

4. 缓存分页数据

对于一些不经常变动的数据,可以将分页结果缓存起来。例如,使用Redis缓存前几页的数据,减少数据库的压力。但这种方式只适用于数据更新频率较低的场景。

5. 业务限制

以京东 web 端为例,根据关键词搜索历史订单,时间维度默认为近三个月 ,以年为单位允许用户手动切换,但不允许查询全量数据。

除此之外还有各大搜索网站在分页主件上做了限制:

百度

Google

Github

4. Elasticsearch的Search After

接下来,我们重点介绍Elasticsearch如何解决深分页问题。

三、为什么ES可以解决深分页问题?

Elasticsearch(ES)是一个分布式的搜索引擎,天生适合处理海量数据的查询。它提供了多种分页方式,其中最适合解决深分页问题的是Search After

「基本原理」

es维护一个实时游标,它以上一次查询的最后一条记录为游标,方便对下一页的查询,它是一个无状态的查询,因此每次查询的都是最新的数据。

由于它采用记录作为游标,因此 「SearchAfter要求doc中至少有一条全局唯一变量(每个文档具有一个唯一值的字段应该用作排序规范)」

ES的Search After机制与游标分页类似,但它更加强大。它的核心思想是:基于上一页的最后一条记录的排序值,作为下一页查询的起始点。这种方式完全避免了偏移量的计算,因此性能非常稳定。

举个例子,假设我们有一个索引存储了用户的订单数据,我们需要分页查询这些订单。使用Search After的方式如下:

  1. 第一次查询:

    json 复制代码
    {
      "size": 10,
      "sort": [
        {"order_date": "asc"},
        {"_id": "asc"}
      ]
    }

    返回的结果中,每条记录都会包含排序字段的值(如order_date_id)。

  2. 第二次查询时,使用上一页最后一条记录的排序值作为起始点:

    json 复制代码
    {
      "size": 10,
      "sort": [
        {"order_date": "asc"},
        {"_id": "asc"}
      ],
      "search_after": [last_order_date, last_id]
    }

    通过这种方式,ES可以高效地返回下一页的数据,而无需扫描前面的所有记录。

  • 性能稳定:无论查询第几页,性能都不会下降。
  • 适合海量数据:ES的分布式架构可以轻松处理亿级甚至更大规模的数据。
  • 灵活性高:支持多字段排序,适用于复杂的业务场景。
  • 无法跳页:和游标分页一样,用户只能一页一页地往下翻。
  • 依赖排序字段 :必须指定一个唯一的排序字段(如_id),否则可能会导致分页结果不准确。

总结

深分页问题是业务开发中一个常见的性能瓶颈,尤其是在数据量庞大的场景下。传统的LIMIT offset, size方式虽然简单,但在深分页时性能极差。通过游标分页、子查询优化、缓存分页等方式,我们可以在一定程度上缓解这个问题。而Elasticsearch的Search After机制,则为我们提供了一种更加优雅和高效的解决方案。

相关推荐
10km24 分钟前
java:Apache Commons Configuration2占位符解析异常的正确解法:${prefix:name:-default}
java·apache·configuration2·变量插值·interpolation
customer0824 分钟前
【开源免费】基于SpringBoot+Vue.JS个人博客系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
qq_4592384928 分钟前
SpringBoot整合Redis和Redision锁
spring boot·redis·后端
灰色人生qwer32 分钟前
SpringBoot 项目配置日志输出
java·spring boot·后端
2301_7930698242 分钟前
Spring Boot +SQL项目优化策略,GraphQL和SQL 区别,Spring JDBC 等原理辨析(万字长文+代码)
java·数据库·spring boot·sql·jdbc·orm
阿华的代码王国1 小时前
【从0做项目】Java搜索引擎(6)& 正则表达式鲨疯了&优化正文解析
java·后端·搜索引擎·正则表达式·java项目·从0到1做项目
服务端相声演员1 小时前
Oracle JDK、Open JDK zulu下载地址
java·开发语言
是姜姜啊!1 小时前
java连接redis
java·redis
hhw1991121 小时前
spring boot知识点5
java·数据库·spring boot
EQUINOX11 小时前
lab4 CSAPP:Cachelab
java·后端·spring