侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

postgresql分页数据重复问题的深入理解

数据库  /  管理员 发布于 5年前   272

问题背景

许多开发和测试人员都可能遇到过列表的数据翻下一页的时候显示了上一页的数据,也就是翻页会有重复的数据。

如何处理?

这个问题出现的原因是因为选择的排序字段有重复,常见的处理办法就是排序的时候加上唯一字段,这样在分页的过程中数据就不会重复了。 关于这个问题文档也有解释并非是一个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 TABLEabase=# insert into t_sort(n_int,c_id) select 100,generate_series(1,9);INSERT 0 9abase=# insert into t_sort(n_int,c_id) select 200,generate_series(1,9);INSERT 0 9abase=# insert into t_sort(n_int,c_id) select 300,generate_series(1,9);INSERT 0 9abase=# insert into t_sort(n_int,c_id) select 400,generate_series(1,9);INSERT 0 9abase=# insert into t_sort(n_int,c_id) select 500,generate_series(1,9);INSERT 0 9abase=# insert into t_sort(n_int,c_id) select 600,generate_series(1,9);INSERT 0 9

三种排序

--快速排序 quicksortabase=# 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 heapsortabase=# 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 1001abase=# set work_mem = '64kB';SETabase=# 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 4MBabase=# show work_mem ; work_mem ---------- 4MB(1 row)​--top-N heapsortabase=# 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=29757 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';SETabase=# show work_mem; work_mem ---------- 64kB(1 row)​-- external merge Diskabase=# 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 =64kbabase=# show work_mem; work_mem ---------- 64kB(1 row)--查询limit 10 offset 10abase=# 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 1000abase=# 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

参考资料: LIMIT and OFFSET

结语

1.关于分页重复数据的问题主要是排序字段不唯一并且执行计划走了快速排序和堆排序导致。

2.当排序有重复字段,但是如果查询是归并排序,便不会存在有重复数据的问题。

3.当用重复字段排序,前面的页重复,随着offset的增大导致work_mem不足以后使用归并排序,就不存在重复的数据了。

4.排序和算法的稳定性有关,当执行计划选择不同的排序算法时,返回的结果不一样。

5.处理重复数据的常见手段就是,排序的时候可以在排序字段d_larq(立案日期)后面加上c_bh(唯一字段)来排序。

order by d_larq,c_bh;

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对AIDI的支持。


  • 上一条:
    Postgresql查询效率计算初探
    下一条:
    PostgreSQL实现批量插入、更新与合并操作的方法
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 分库分表的目的、优缺点及具体实现方式介绍(0个评论)
    • DevDB - 在 VS 代码中直接访问数据库(0个评论)
    • 在ubuntu系统中实现mysql数据存储目录迁移流程步骤(0个评论)
    • 在mysql中使用存储过程批量新增测试数据流程步骤(0个评论)
    • php+mysql数据库批量根据条件快速更新、连表更新sql实现(0个评论)
    • 近期文章
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • Laravel 11.15版本发布 - Eloquent Builder中添加的泛型(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2017-06
    • 2017-08
    • 2017-09
    • 2017-10
    • 2017-11
    • 2018-01
    • 2018-05
    • 2018-10
    • 2018-11
    • 2020-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-08
    • 2020-09
    • 2021-02
    • 2021-04
    • 2021-07
    • 2021-08
    • 2021-11
    • 2021-12
    • 2022-02
    • 2022-03
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-07
    • 2023-08
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-03
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客