postgresql-分页数据重复问题探索
postgresql-分页数据重复探索
问题背景
许多开发和测试人员都可能遇到过列表的数据翻下一页的时候显示了上一页的数据,也就是翻页会有重复的数据。
如何处理?
这个问题出现的原因是因为选择的排序字段有重复,常见的处理办法就是排序的时候加上唯一字段,这样在分页的过程中数据就不会重复了。
关于这个问题文档也有解释并非是一个bug。而是排序时需要选择唯一字段来做排序,不然返回的结果不确定
排序返回数据重复的根本原因是什么呢?
经常优化sql的同学可能会发现,执行计划里面会有Sort Method这个关键字,而这个关键字就是排序选择的方法。abase的排序分为三种
quicksort 快速排序
top-N heapsort Memory 堆排序
external merge Disk 归并排序
推测
分页重复的问题和执行计划选择排序算法的稳定性有关。
简单介绍下这三种排序算法的场景:
在有索引的情况下:排序可以直接走索引。 在没有索引的情况下:当表的数据量较小的时候选择快速排序(排序所需必须内存小于work_mem), 当排序有limit,且耗费的内存不超过work_mem时选择堆排序, 当work_mem不够时选择归并排序。
验证推测
1.创建表,初始化数据
abase=# create table t_sort(n_int int,c_id varchar(300));
CREATE TABLE
abase=# insert into t_sort(n_int,c_id) select 100,generate_series(1,9);
INSERT 0 9
abase=# insert into t_sort(n_int,c_id) select 200,generate_series(1,9);
INSERT 0 9
abase=# insert into t_sort(n_int,c_id) select 300,generate_series(1,9);
INSERT 0 9
abase=# insert into t_sort(n_int,c_id) select 400,generate_series(1,9);
INSERT 0 9
abase=# insert into t_sort(n_int,c_id) select 500,generate_series(1,9);
INSERT 0 9
abase=# insert into t_sort(n_int,c_id) select 600,generate_series(1,9);
INSERT 0 9
三种排序
--快速排序 quicksort
abase=# explain analyze select ctid,n_int,c_id from t_sort order by n_int asc;
QUERY PLAN
------------------------------------------------------------
Sort (cost=3.09..3.23 rows=54 width=12) (actual time=0.058..0.061 rows=54 loops=1)
Sort Key: n_int
Sort Method: quicksort Memory: 27kB
-> Seq Scan on t_sort (cost=0.00..1.54 rows=54 width=12) (actual time=0.021..0.032 rows=54 loops=1)
Planning time: 0.161 ms
Execution time: 0.104 ms
(6 rows)
--堆排序 top-N heapsort
abase=# explain analyze select ctid,n_int,c_id from t_sort order by n_int asc limit 10;
QUERY PLAN
------------------------------------------------------------
Limit (cost=2.71..2.73 rows=10 width=12) (actual time=0.066..0.068 rows=10 loops=1)
-> Sort (cost=2.71..2.84 rows=54 width=12) (actual time=0.065..0.066 rows=10 loops=1)
Sort Key: n_int
Sort Method: top-N heapsort Memory: 25kB
-> Seq Scan on t_sort (cost=0.00..1.54 rows=54 width=12) (actual time=0.022..0.031 rows=54 loops=1
)
Planning time: 0.215 ms
Execution time: 0.124 ms
(7 rows)
--归并排序 external sort Disk
--插入大量值为a的数据
abase=# insert into t_sort(n_int,c_id) select generate_series(1000,2000),'a';
INSERT 0 1001
abase=# set work_mem = '64kB';
SET
abase=# explain analyze select ctid,n_int,c_id from t_sort order by n_int asc;
QUERY PLAN
-------------------------------------------------------------
Sort (cost=18.60..19.28 rows=270 width=12) (actual time=1.235..1.386 rows=1055 loops=1)
Sort Key: n_int
Sort Method: external sort Disk: 32kB
-> Seq Scan on t_sort (cost=0.00..7.70 rows=270 width=12) (actual time=0.030..0.247 rows=1055 loops=1)
Planning time: 0.198 ms
Execution time: 1.663 ms
(6 rows)
快速排序
--快速排序
abase=# explain analyze select ctid,n_int,c_id from t_sort order by n_int asc;
QUERY PLAN
------------------------------------------------------------
Sort (cost=3.09..3.23 rows=54 width=12) (actual time=0.058..0.061 rows=54 loops=1)
Sort Key: n_int
Sort Method: quicksort Memory: 27kB
-> Seq Scan on t_sort (cost=0.00..1.54 rows=54 width=12) (actual time=0.021..0.032 rows=54 loops=1)
Planning time: 0.161 ms
Execution time: 0.104 ms
(6 rows)
--获取前20条数据
abase=# select ctid,n_int,c_id from t_sort order by n_int asc limit 20;
ctid | n_int | c_id
--------+-------+------
(0,7) | 100 | 7
(0,2) | 100 | 2
(0,4) | 100 | 4
(0,8) | 100 | 8
(0,3) | 100 | 3
(0,6) | 100 | 6
(0,5) | 100 | 5
(0,9) | 100 | 9
(0,1) | 100 | 1
(0,14) | 200 | 5
(0,13) | 200 | 4
(0,12) | 200 | 3
(0,10) | 200 | 1
(0,15) | 200 | 6
(0,16) | 200 | 7
(0,17) | 200 | 8
(0,11) | 200 | 2
(0,18) | 200 | 9
(0,20) | 300 | 2
(0,19) | 300 | 1
(20 rows) --分页获取前10条数据
abase=# select ctid,n_int,c_id from t_sort order by n_int asc limit 10 offset 0;
ctid | n_int | c_id
--------+-------+------
(0,1) | 100 | 1
(0,3) | 100 | 3
(0,4) | 100 | 4
(0,2) | 100 | 2
(0,6) | 100 | 6
(0,7) | 100 | 7
(0,8) | 100 | 8
(0,9) | 100 | 9
(0,5) | 100 | 5
(0,10) | 200 | 1
(10 rows)
--分页从第10条开始获取10条
abase=# select ctid,n_int,c_id from t_sort order by n_int asc limit 10 offset 10;
ctid | n_int | c_id
--------+-------+------
(0,13) | 200 | 4
(0,12) | 200 | 3
(0,10) | 200 | 1
(0,15) | 200 | 6
(0,16) | 200 | 7
(0,17) | 200 | 8
(0,11) | 200 | 2
(0,18) | 200 | 9
(0,20) | 300 | 2
(0,19) | 300 | 1
(10 rows) limit 10 offset 0,limit 10 offset 10,连续取两页数据
此处可以看到limit 10 offset 10结果中,第三条数据重复了第一页的最后一条: (0,10) | 200 | 1
并且n_int = 200 and c_id = 5这条数据被“遗漏”了。
堆排序
abase=# select count(*) from t_sort;
count
-------
1055
(1 row)
--设置work_mem 4MB
abase=# show work_mem ;
work_mem
----------
4MB
(1 row)
--top-N heapsort
abase=# explain analyze select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 0) td limit 10;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------
Limit (cost=2061.33..2061.45 rows=10 width=13) (actual time=15.247..15.251 rows=10 loops=1)
-> Limit (cost=2061.33..2063.83 rows=1001 width=13) (actual time=15.245..15.247 rows=10 loops=1)
-> Sort (cost=2061.33..2135.72 rows=29757 width=13) (actual time=15.244..15.245 rows=10 loops=1)
Sort Key: test.n_int
Sort Method: top-N heapsort Memory: 95kB
-> Seq Scan on test (cost=0.00..429.57 rows=29757 width=13) (actual time=0.042..7.627 rows=2
9757 loops=1)
Planning time: 0.376 ms
Execution time: 15.415 ms
(8 rows)
--获取limit 1001 offset 0,然后取10前10条数据
abase=# select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 0) td limit 10;
ctid | n_int | c_id
----------+-------+------
(0,6) | 100 | 6
(0,2) | 100 | 2
(0,5) | 100 | 5
(87,195) | 100 | 888
(0,3) | 100 | 3
(0,1) | 100 | 1
(0,8) | 100 | 8
(0,55) | 100 | 888
(44,12) | 100 | 888
(0,9) | 100 | 9
(10 rows)
---获取limit 1001 offset 1,然后取10前10条数据
abase=# select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 1) td limit 10;
ctid | n_int | c_id
----------+-------+------
(44,12) | 100 | 888
(0,8) | 100 | 8
(0,1) | 100 | 1
(0,5) | 100 | 5
(0,9) | 100 | 9
(87,195) | 100 | 888
(0,7) | 100 | 7
(0,6) | 100 | 6
(0,3) | 100 | 3
(0,4) | 100 | 4
(10 rows) ---获取limit 1001 offset 2,然后取10前10条数据
abase=# select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 2) td limit 10;
ctid | n_int | c_id
----------+-------+------
(0,5) | 100 | 5
(0,55) | 100 | 888
(0,1) | 100 | 1
(0,9) | 100 | 9
(0,2) | 100 | 2
(0,3) | 100 | 3
(44,12) | 100 | 888
(0,7) | 100 | 7
(87,195) | 100 | 888
(0,4) | 100 | 4
(10 rows) 堆排序使用内存: Sort Method: top-N heapsort Memory: 95kB 当offset从0变成1后,以及变成2后,会发现查询出的10条数据不是有顺序的。
归并排序
--将work_mem设置为64kb让其走归并排序。
abase=# set work_mem ='64kB';
SET
abase=# show work_mem;
work_mem
----------
64kB
(1 row)
-- external merge Disk
abase=# explain analyze select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 0) td limit 10;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Limit (cost=2061.33..2061.45 rows=10 width=13) (actual time=27.912..27.916 rows=10 loops=1)
-> Limit (cost=2061.33..2063.83 rows=1001 width=13) (actual time=27.910..27.913 rows=10 loops=1)
-> Sort (cost=2061.33..2135.72 rows=29757 width=13) (actual time=27.909..27.911 rows=10 loops=1)
Sort Key: test.n_int
Sort Method: external merge Disk: 784kB
-> Seq Scan on test (cost=0.00..429.57 rows=29757 width=13) (actual time=0.024..6.730 rows=29757 loops=1)
Planning time: 0.218 ms
Execution time: 28.358 ms
(8 rows) --同堆排序一样,获取limit 1001 offset 0,然后取10前10条数据
abase=# select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 0) td limit 10;
ctid | n_int | c_id
--------+-------+------
(0,1) | 100 | 1
(0,2) | 100 | 2
(0,4) | 100 | 4
(0,8) | 100 | 8
(0,9) | 100 | 9
(0,5) | 100 | 5
(0,3) | 100 | 3
(0,6) | 100 | 6
(0,55) | 100 | 888
(0,7) | 100 | 7
(10 rows) --同堆排序一样,获取limit 1001 offset 1,然后取10前10条数据
abase=# select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 1) td limit 10;
ctid | n_int | c_id
----------+-------+------
(0,2) | 100 | 2
(0,4) | 100 | 4
(0,8) | 100 | 8
(0,9) | 100 | 9
(0,5) | 100 | 5
(0,3) | 100 | 3
(0,6) | 100 | 6
(0,55) | 100 | 888
(0,7) | 100 | 7
(87,195) | 100 | 888
(10 rows) --同堆排序一样,获取limit 1001 offset 2,然后取10前10条数据
abase=# select * from ( select ctid,n_int,c_id from test order by n_int asc limit 1001 offset 2) td limit 10;
ctid | n_int | c_id
----------+-------+------
(0,4) | 100 | 4
(0,8) | 100 | 8
(0,9) | 100 | 9
(0,5) | 100 | 5
(0,3) | 100 | 3
(0,6) | 100 | 6
(0,55) | 100 | 888
(0,7) | 100 | 7
(87,195) | 100 | 888
(44,12) | 100 | 888
(10 rows) 减小work_mem使用归并排序的时候,offset从0变成1后以及变成2后,任然有序。
还有一种情况,那就是在查询前面几页的时候会有重复,但是越往后面翻就不会重复了,现在也可以解释清楚。
如果每页10条数据,当offse较小的时候使用的内存较少。当offse不断增大,所耗费的内存也就越多。
--设置work_mem =64kb
abase=# show work_mem;
work_mem
----------
64kB
(1 row)
--查询limit 10 offset 10
abase=# explain analyze select * from ( select ctid,n_int,c_id from test order by n_int asc limit 10 offset 10) td limit 10;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Limit (cost=1221.42..1221.54 rows=10 width=13) (actual time=12.881..12.884 rows=10 loops=1)
-> Limit (cost=1221.42..1221.44 rows=10 width=13) (actual time=12.879..12.881 rows=10 loops=1)
-> Sort (cost=1221.39..1295.79 rows=29757 width=13) (actual time=12.877..12.879 rows=20 loops=1)
Sort Key: test.n_int
Sort Method: top-N heapsort Memory: 25kB
-> Seq Scan on test (cost=0.00..429.57 rows=29757 width=13) (actual time=0.058..6.363 rows=29757 loops=1)
Planning time: 0.230 ms
Execution time: 12.976 ms
(8 rows)
--查询limit 10 offset 1000
abase=# explain analyze select * from ( select ctid,n_int,c_id from test order by n_int asc limit 10 offset 1000) td limit 10;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Limit (cost=2065.75..2065.88 rows=10 width=13) (actual time=27.188..27.192 rows=10 loops=1)
-> Limit (cost=2065.75..2065.78 rows=10 width=13) (actual time=27.186..27.188 rows=10 loops=1)
-> Sort (cost=2063.25..2137.64 rows=29757 width=13) (actual time=26.940..27.138 rows=1010 loops=1)
Sort Key: test.n_int
Sort Method: external merge Disk: 784kB
-> Seq Scan on test (cost=0.00..429.57 rows=29757 width=13) (actual time=0.026..6.374 rows=29757 loops=1)
Planning time: 0.207 ms
Execution time: 27.718 ms
(8 rows)
可以看到当offset从10增加到1000的时候,使用的内存增加,排序的方法从堆排序变成了归并排序。而归并排序为稳定排序,所以后面的分页不会再有后一页出现前一页数据的情况。
参考资料:PostgreSQL - repeating rows from LIMIT OFFSET
结语
1.关于分页重复数据的问题主要是排序字段不唯一并且执行计划走了快速排序和堆排序导致。
2.当排序有重复字段,但是如果查询是归并排序,便不会存在有重复数据的问题。
3.当用重复字段排序,前面的页重复,随着offset的增大导致work_mem不足以后使用归并排序,就不存在重复的数据了。
4.排序和算法的稳定性有关,当执行计划选择不同的排序算法时,返回的结果不一样。
5.处理重复数据的常见手段就是,排序的时候可以在排序字段d_larq(立案日期)后面加上c_bh(唯一字段)来排序。
order by d_larq,c_bh;
postgresql-分页数据重复问题探索的更多相关文章
- Mybatis oracle多表联合查询分页数据重复的问题
Mybatis oracle多表联合查询分页数据重复的问题 多表联合查询分页获取数据时出现一个诡异的现象:数据总条数正确,但有些记录多了,有些记录却又少了甚至没了.针对这个问题找了好久,最后发现是由于 ...
- SQL分页数据重复问题
对于关系数据库来说,直接写SQL拉数据在列表中显示是很常用的做法.但如此便带来一个问题:当数据量大到一定程度时,系统内存迟早会耗光.另外,网络传输也是问题.如果有1000万条数据,用户想看最后一条,这 ...
- MySQL中orderby和limit分页数据重复的问题
背景 读取规则是按照某表中sequence字段排序的,而这个字段是让人手工填写的.那么,可想而知,数据一多,难免会出现填写的值相同的情况. 综上所述,可能就会导致以下两条sql出现数据重叠的情况: s ...
- postgresql排序分页时数据重复问题
当同时排序又分页时,如果排序的字段X不是唯一字段,当多个记录的X字段有同一个值时顺序是随机的. 这个有可能造成分页时数据重复的问题.某一页又把上一页的数据查出来了,其实数据库只有一条记录. 解决办法: ...
- iOS不得姐项目--推荐关注模块(一个控制器控制两个tableView),数据重复请求的问题,分页数据的加载,上拉下拉刷新(MJRefresh)
一.推荐关注模块(一个控制器控制两个tableView) -- 数据的显示 刚开始加载数据值得注意的有以下几点 导航控制器会自动调整scrollView的contentInset,最好是取消系统的设置 ...
- 解决 MySQL 分页数据错乱重复
前言 一天,小明兴匆匆的在通讯工具上说:这边线上出现了个奇怪的问题,麻烦 DBA 大大鉴定下,执行语句 select xx from table_name wheere xxx order by 字段 ...
- oracle 分页查询数据重复问题
最近在做项目的时候发现一个问题,oracle 在查询分页数据的时候,有几条数据重复查询了,并且有几条数据在分页的时候消失了.百度了一下发现,ORACLE 在查询数据的时候返回的行不是固定的,他只是按照 ...
- MySQL 分页数据错乱重复
select xx from table_name wheere xxx order by 字段A limit offset;, 表数据总共 48 条,分页数量正常,但出现了结果混杂的情况,第一页的数 ...
- Oracle分页查询排序数据重复问题
参考资料: http://docs.oracle.com/database/122/SQLRF/ROWNUM-Pseudocolumn.htm#SQLRF00255 http://blog.csdn. ...
随机推荐
- C++中绝对值的运算
首先,输入-42333380005结果取出来的绝对值却是616292955. 开始我以为是long型的取值范围有问题,就把long型全部改为long long型的了,结果还是一样,就觉得绝对值这个函数 ...
- 【Django】重定向
view函数中使用重定向方法 return HttpResponseRedirect('redir2.html')的时候不自觉的在前面加了request参数,结果报错: TypeError at /b ...
- WD Elements 与 time machine
备份是很重要的问题. 之前买了一个 WD Elements,想要格式化成 HFS 的格式. 不然不能被 Time Machine 使用. 但是用磁盘工具不能成功,因为 EFI 分区的问题. 参考下面网 ...
- Android Studio修改项目中整体包名
莫名的需求,要把之前的apk分成三个不同的apk,还要在应用市场能够上线,麻麻滴这样一听那还不要各个apk包的包名不同以及apk签名文件也不同嘛(签名文件一般也用不同,为防止上线冲突嘛).所以就亲自尝 ...
- 【转】GT 的性能测试方案解析
前言 本文将整理腾讯GT各个性能测试项的测试方法,目的是为了帮助移动性能专项测试同学快速过一遍腾讯GT各个性能数据是如何获取的.另外对腾讯GT还不了解或者不知道它能做什么的同学可以看看这篇文章:htt ...
- 项目总结18-使用textarea无法判断空值之坑
项目总结18-使用textarea无法判断空值之坑 今天使用js判断textarea为空,发现怎么都无法成功仔细做了对比测试,发现结果如下: 1-JS代码 if($("#content&qu ...
- node.js中 koa 框架的基本使用方法
一.安装 koa npm install koa --save 二.简单使用 const koa = require('koa'); //注意使用koa需要new,跟express有点不同 let a ...
- Fiddler 手机抓包 手机联网异常解决方案
Fiddler在电脑里已经闲置很久了,之前看是不是服务器返回问题都是连上AS看输出的log,但是终归不如直接抓包来的方便 昨天搞了一下午,手机跟电脑都是连的公司的wifi,手机设置电脑ip和端口的网络 ...
- 三、putty工具常见设置
转载自:https://www.cnblogs.com/hdk1993/p/4769072.html Putty是一个免费小巧的Win32平台下的telnet,rlogin和ssh客户端. 它的主程序 ...
- [费用流][NOI2008]志愿者招募
志愿者招募 题目描述 申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管.布布刚上任就遇到了一个难 题:为即将启动的奥运新项目招募一批短期志愿者.经过估算,这个项目需要N 天才能完 ...