来自:James Pan's Blog
By 潘家邦
链接:http://blog.jamespan.me/2015/01/22/trick-of-paging-query/
背景
从上周开始我就一直在做数据清洗的工作,这次算是体会到了什么叫做“抛开数据量谈实现就是耍流氓”。
我设计方案和调试代码连接的都是日常环境的数据库,里面的单表数据量在百级,无论我怎么实现都是瞬间洗完。到了性能测试的时候用的就是性能库,双 11 之前@W君做性能测试的时候,往里面写入了 2000W 的数据,足够我战个痛快。
深坑
一开始的时候,分页查询用的是 limit 子句,SQL 语句形态如下。
select * from table where xxx in (1,2,3) order by id limit #offset#, 200
limit 子句的优点很明显,简单好用。缺点平时不显著,数据量一大就暴露了。数据库会完整扫描 offset 的行,然后继续扫描 200 行之后才把结果集返回。 offset 在 400W 的时候,这样的 SQL 做一次分页查询就已经至少耗时 5s 了。
挣扎
顿时感觉自己陷入了一个泥水坑,赶紧找老司机请教。@Y君给了我一个方案,不使用 limit,直接使用主键索引 + 左右范围查找,SQL 语句形态如下。
select * from table where xxx in (1,2,3) and id >= #minId# and id < #maxId#
其中minId 和maxId 由我的代码给出,启动时先算出当前表的 id 范围,然后在此范围内以 200 为步长分页读取,即 maxId - minId = 200,有可能读不到数据,有可能读到 200 条。
在数据库里面试着跑了几条这样的查询,果然效率高了很多。赶紧吭哧吭哧在代码中实现这种分页逻辑,信心满满想要性能飙升,结果。。。
在日常数据库测试的时候就发现问题了。我的数据库的 id 不是由 MySQL 自动生成的连续的自增主键,而是通过其他中间件产生的,从整体来看,整个 id 的分布比较离散,步长 200 的时候,一次查询根本查不出几条数据。如果整张表的 maxId 比 minId 大出很多很多,会产生很多次无意义的查询。要想有比较好的命中率,还需要关心表里面 id 的分布,根据分布情况调整步长。
接下来我就在 limit 和 min-max-id 两种分页方案之间纠结,开始去分析线上表的数据分布,然后考虑把大表和小表区分对待,使用不同的分页策略等等。
灵光
白天被分页查询的性能泥潭弄得心力憔悴,晚上回家的路上冷风吹着头脑稍微清醒一些,想到一个条似乎可行的 SQL 语句。
select * from table where id >= #minId# and xxx in (1,2,3) limit 200
回到家之后迫不及待开始写代码,先把这条 SQL 换着参数在数据库里面一遍遍执行,感觉这种分页方式完全符合我的要求,查询使用的主键索引,虽然从执行计划看影响行数在百万级,但是实际执行的时候影响行数不过百级,还不需要考虑 id 的分布,每次都能实打实的给我捞出 200 条数据。
不过使用这样的 SQL 语句做分页,需要注意调整 minId 的值,扫表过程中需要做到不重不漏。一种可行的方案是将本次查出的结果集中的最大的 id,自增 1 后作为下次查询的 minId。
max_id, min_id = select min(id), max(id) from table
while min_id <= max_id:
objs = select * from table where id >= min_id and xxx in (1,2,3) limit 200
max_id_in_page = max(map(lambda x: x.id, objs))
min_id = max_id_in_page + 1
整体来说,这种分页方式避免了使用 limit 时候遍历 offset 带来的无谓的性能开销,避免了对 id 使用左右范围查询时候 id 的离散分布对命中率的影响,代价是需要在内存中遍历结果集获取当前分页中 id 的最大值,局限是只能在对全表唯一的字段做分页时使用。
扩展
关于分页查询,不同的数据库在做 offset 的时候语法也不太一样,比如 M$ 家的语法就是 TOP n,MySQL 的语法就是 limit n。
网上的很多博客都提到一种利用子查询来提高性能的做法,大体的意思是先使用普通的 offset 语法获取目标分页的记录的 id 集合,再根据 id 集合去获取完整的目标数据,SQL 语句形态如下。
select * from table where id in (select id from table order by id limit #offset#, #size#)
其实有些版本尤其低版本的 MySQL 是不支持直接对带 limit 的子查询的结果做 in 子句的,执行的时候会报错,信息如下。
This version of MySQL doesn’t yet support ‘LIMIT & IN/ALL/ANY/SOME subquery’
针对这个问题,他们就有了一个改进之后的子查询版本。
select * from table where id >= (select id from table order by id limit #offset#, 1)
其实是先取出目标分页的第一条记录的id, 然后根据 id 做范围查找和条数限制。这条 SQL 语句的效率在 offset 达到百万级时相比直接 limit 有数倍的提升,但是注意到 MySQL 子查询其实是一个坑,这条语句不但没有避免遍历 offset,还做了大量的无用重复工作。
本质上,我最终使用的分页方案是对这条 SQL 语句的优化,借助 id 的有序性和唯一性,使用max(map(lambda x: x.id, objs)) + 1 替代了子查询。
有人为了绕过 MySQL 不支持对子查询结果做 in 子句的限制,脑洞大开写出了如下查询。
select * from table where id in (select id from (select id from table order by id limit #offset#, #size#) as tmp)
既然不允许直接对带 limit 的子查询做 in,那么干脆用子查询套子查询,也是醉了。这条 SQL 语句的效率还不如直接 limit。
还有一种改进方案就是传说中的“查两次”。第一次先查出目标分页的 id 集合,因为只查 id,大部分情况可以直接命中索引然后返回,速度还是可以接受的。然后第二次直接根据 id 集合做 in 子句查询,走的主键索引,这次就是秒出了。
其实查两次对性能的影响需要具体到情景来分析,不能当做是万金油。
我在最终使用的分页方案,在我的应用场景下,性能是超过上面其余几种分页方案的。
现实
在现实世界的开发中,分页远比预料的要复杂得多。
如果 Web 页面通过记录的创建时间来分页,那么很可能我最终使用的分页方案就不能套用了,因为时间不是一个全表唯一的值,很难在不使用 limit 的情况下做到不重不漏。如果运气不错,使用的是数据库的自增主键,那么可以认为主键的变化趋势和创建时间的变化趋势是等价的,可以将对时间的分页映射成对主键的分页。
在分页的交互逻辑上,天朝的产品经理似乎偏好于电梯式的分页控件。其实还是“抛开数据量谈实现就是耍流氓”,在数据量小的时候,使用什么样的分页方式对系统性能都没什么影响,但是在数据量大到一定程度的时候,电梯式的分页对系统性能的大量消耗反而会伤害所谓的用户体验。
关于大数据量下的分页实现,业界其实已经有了好几种策略,基于不同的假设。
假设随着时间的推移,越早产生的数据,价值越小。如果假设成立,我们可以认为绝大多数用户是没有这样一个需求要去查看他在系统中产生的第一条数据的。如果第一条数据对用户真的很重要,即使再困难他也会想办法得到。
基于这样的假设,在数据量足够大的情况下,我们就可以对系统实现和交互体验做出很多优化,比如不再提供精确的电梯式分页,取而代之的是下一页、上一页;不再提供精确的总页数,而是提供一个大概的条目总数,可以通过查看 SQL 的执行计划得到。
之前看过一个关于上一页、下一页的实现技巧。假如每页显示 20 条数据,那么查询数据库的时候,用limit #offset#, 21 取出 21 条记录,页面展现20条。如果取到了 21 条,说明下一页还有数据,在页面展示下一页按钮。如果结果集数量不足 21,说明已经到了最后一页,无需显示下一页按钮了。这种方式完全避免了在分页查询时对总条目数量的查询。
还有一种策略是基于这样的假设:用户比较关心的是最近产生的一小部分数据。在用户查询的时候,我们可以一次性从数据库查询符合条件的 N 条数据缓存起来,足够用户翻个几页,这样哪怕是使用电梯式分页,在计算总页数时也无需查询数据库了。
总结
被分页问题坑了,其实还是说明一个问题:图样图森破,姿势水平有待提高。这次写洗数据的代码经历好几次方案变换和代码重写,才得到一个勉强拿得出手的版本,期间各种涨姿势,好有收获。
相关推荐
SQLite 是一个轻量级的、开源的嵌入式关系型...通过掌握以上技巧,你可以在SQLite中有效地进行分页查询和获取数据总数,同时确保应用的性能和响应速度。在实际开发中,还需要根据具体需求和数据规模灵活运用这些知识。
HBase作为一个NoSQL数据库,具有高性能、高可扩展性和高可靠性等特点,但是在查询方面却存在一些限制,例如不支持分页查询。这就使得开发者需要自己实现分页查询功能。本文将讲解如何使用Java语言实现HBase的分页...
本篇文章将详细讲解如何在Android中实现数据库的分页查询功能,以高效、流畅地加载和展示数据。 一、SQLite数据库基础 Android系统内置了SQLite数据库,它是一个轻量级的数据库引擎,可以方便地进行数据存储和检索...
本篇文章将详细探讨如何基于Spring MVC实现分页查询,这对于任何处理大量数据的Web应用都是至关重要的。 首先,理解分页的基本概念。分页是将大型数据集划分为较小、更易管理的部分,以提高用户体验并减少服务器...
总结来说,易语言结合MySQL数据库的分页查询是通过建立连接、构建分页SQL、执行查询、处理结果和关闭连接等一系列步骤来实现的。理解这些步骤,并结合数据库优化技巧,可以帮助开发者构建出高效、友好的数据展示系统...
### Oracle分页查询详解 #### 一、分页查询的重要性 在数据库操作中,分页查询是一项非常重要的技术。...合理地运用基于`ROWNUM`的分页查询方法及其优化技巧,可以显著提升系统的性能表现和用户体验。
### Java多线程分页查询知识点详解 #### 一、背景与需求分析 在实际的软件开发过程中,尤其是在处理大量数据时,如何高效地进行数据查询成为了一个关键问题。例如,在一个用户众多的社交平台上,当用户需要查看...
本教程将详细讲解如何在Java中实现不分框架的分页查询,同时涵盖对MySQL和SQL Server数据库的支持。 一、基础知识 1. 分页概念:分页是将大量数据按一定数量分成若干部分,每次只加载一部分到内存中显示,用户可以...
在HBase这个分布式列式数据库中,数据存储和...总的来说,HBase的分页查询涉及到对数据分布和查询策略的理解,以及在代码层面上的巧妙设计。掌握好这些技巧,能帮助我们在处理大规模数据时,更高效地获取和展示信息。
问题在于,当你在使用MybatisPlus进行分页查询时,如果未正确配置PaginationInterceptor,分页参数将无法生效,你会观察到所有的数据都被查询出来。要解决这个问题,你需要在你的项目配置中添加...
在实际应用中,你可能会遇到更多复杂的查询需求,例如嵌套的分页查询、联接查询、聚合函数等,JPA和Spring Data JPA都提供了丰富的API来支持这些操作。例如,你可以使用`@Query`注解自定义SQL或HQL查询,或者利用`...
总的来说,"数据分页以及高级查询"这一主题涵盖了数据库查询的基本技巧和高级特性,是Java Web开发中的核心技能。通过学习和实践这些知识,开发者能够创建出高效、用户友好的数据交互界面,处理各种复杂的数据需求。
创建一个SqlCommand对象,设置SQL语句为带有分页功能的查询,例如:`SELECT * FROM TableName OFFSET @PageIndex ROWS FETCH NEXT @PageSize ROWS ONLY`,其中`@PageIndex`和`@PageSize`分别是当前页码和每页显示的...
项目主体结构是dao+db+filter+pojo+servlet, 使用技术Servlet转发,代码中有注释帮助学者理解,数据库为MySQL资源...实现的数据库内容分页,查询分页,对初学者难点是根据get请求的中的url地址进行查询后的分页效果。
1. 数据库层面:在SQL查询语句中,可以使用LIMIT和OFFSET关键字来实现分页。LIMIT用于指定每页显示的数据量,OFFSET则用来设置从哪一条记录开始获取数据。例如,查询第2页,每页10条记录的SQL可能如下: ``` SELECT ...
本文将讲解 Oracle 中的分页查询,包括使用 ROWNUM 伪列和 ORDER BY 子句对查询结果进行排序和分页。 一、使用 ROWNUM 伪列实现分页查询 在 Oracle 中,ROWNUP 伪列是一个特殊的列,可以用来实现分页查询。例如,...
在数据库层面,我们需要使用SQL的`LIMIT`和`OFFSET`或者`ROWNUM`等语法来实现分页查询。 接着,我们关注JDBC部分。JDBC是Java连接数据库的标准API,通过它可以执行SQL语句并获取结果。在分页查询中,我们首先需要...
JPA复杂查询加分页查询的快速开发 JPA(Java Persistence API)是 Java 的持久层 API,用于访问、持久化数据。使用 JPA,可以快速开发复杂查询,实现高效的数据访问。下面是 JPA 复杂查询加分页查询的快速开发知识...
这里 `startRow` 和 `endRow` 分别表示分页查询的起始行号和结束行号。 #### DB2 分页查询 DB2 支持使用 `ROWNUMBER()` 函数来进行分页。该函数会根据指定的排序规则为每行分配一个行号,之后可以通过 `BETWEEN` ...
3. **动态生成分页查询语句**:根据当前页码和每页记录数计算出行号范围,并构造分页查询语句。 4. **执行分页查询**:使用动态生成的分页查询语句打开游标返回查询结果。 #### 存储过程代码详解 ```sql CREATE OR...