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

参考资料: LIMIT and OFFSET

结语

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

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

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

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

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

order by d_larq,c_bh;

postgresql-分页数据重复问题探索的更多相关文章

  1. Mybatis oracle多表联合查询分页数据重复的问题

    Mybatis oracle多表联合查询分页数据重复的问题 多表联合查询分页获取数据时出现一个诡异的现象:数据总条数正确,但有些记录多了,有些记录却又少了甚至没了.针对这个问题找了好久,最后发现是由于 ...

  2. SQL分页数据重复问题

    对于关系数据库来说,直接写SQL拉数据在列表中显示是很常用的做法.但如此便带来一个问题:当数据量大到一定程度时,系统内存迟早会耗光.另外,网络传输也是问题.如果有1000万条数据,用户想看最后一条,这 ...

  3. MySQL中orderby和limit分页数据重复的问题

    背景 读取规则是按照某表中sequence字段排序的,而这个字段是让人手工填写的.那么,可想而知,数据一多,难免会出现填写的值相同的情况. 综上所述,可能就会导致以下两条sql出现数据重叠的情况: s ...

  4. postgresql排序分页时数据重复问题

    当同时排序又分页时,如果排序的字段X不是唯一字段,当多个记录的X字段有同一个值时顺序是随机的. 这个有可能造成分页时数据重复的问题.某一页又把上一页的数据查出来了,其实数据库只有一条记录. 解决办法: ...

  5. iOS不得姐项目--推荐关注模块(一个控制器控制两个tableView),数据重复请求的问题,分页数据的加载,上拉下拉刷新(MJRefresh)

    一.推荐关注模块(一个控制器控制两个tableView) -- 数据的显示 刚开始加载数据值得注意的有以下几点 导航控制器会自动调整scrollView的contentInset,最好是取消系统的设置 ...

  6. 解决 MySQL 分页数据错乱重复

    前言 一天,小明兴匆匆的在通讯工具上说:这边线上出现了个奇怪的问题,麻烦 DBA 大大鉴定下,执行语句 select xx from table_name wheere xxx order by 字段 ...

  7. oracle 分页查询数据重复问题

    最近在做项目的时候发现一个问题,oracle 在查询分页数据的时候,有几条数据重复查询了,并且有几条数据在分页的时候消失了.百度了一下发现,ORACLE 在查询数据的时候返回的行不是固定的,他只是按照 ...

  8. MySQL 分页数据错乱重复

    select xx from table_name wheere xxx order by 字段A limit offset;, 表数据总共 48 条,分页数量正常,但出现了结果混杂的情况,第一页的数 ...

  9. Oracle分页查询排序数据重复问题

    参考资料: http://docs.oracle.com/database/122/SQLRF/ROWNUM-Pseudocolumn.htm#SQLRF00255 http://blog.csdn. ...

随机推荐

  1. apt与apt-get命令的区别与解释

    [apt与apt-get命令的区别与解释] Ubuntu 16.04 发布时,一个引人注目的新特性便是 apt 命令的引入.其实早在 2014 年,apt 命令就已经发布了第一个稳定版,只是直到 20 ...

  2. zabbix 3.4 直接 发现端口并作存活监控(带服务名)

    客户端配置 1.脚本 [root@es1 home]# cat /home/port_service.sh #!/bin/bash#by Mr.lu#su rootportarray=(`sudo - ...

  3. HDU6030 Happy Necklace(递推+矩阵快速幂)

    传送门:点我 Little Q wants to buy a necklace for his girlfriend. Necklaces are single strings composed of ...

  4. [Java学习]多线程

    关于多进程与多线程 使用多进程的目的:提高CPU利用率. 使用多线程的目的:提高应用程序?利用率. 多线程与多进程区别:进程间内存独立:同一个进程的线程间共享"堆内存和方法区内存" ...

  5. 安装配置python环境,并跑一个推荐系统的例子

    1.官网下载python2.7,安装完后,在环境变量Path中加上这个路径 在控制台输入python,出现版本信息,就成功了. 2.我使用的是 pycharm,注册后,在 把自己的python.exe ...

  6. Mybatis 中实体类的编写

    一个实体类对应一个数据表 一个属性对应一个字段 默认情况下类名和属性名都采用 “下划线转驼峰” 的命名方式.但具体采用什么样的命名方式并不重要(方式一致即可),在后面使用这些对象的时候,可以通过 re ...

  7. cpp 区块链模拟示例(六) 交易

    交易(transaction)是比特币的核心所在,而区块链的唯一目的,也正是为了能够安全可靠地存储交易.在区块链中,交易一旦被创建,就没有任何人能够再去修改或是删除它.在今天的文章中,我们会实现交易的 ...

  8. PHP脚本命令行执行成功,CRON无法执行故障解决记录

    先来看看一个最简单的PHP文件(ip.php) <?php $myip = get_ip_cmd(); echo($myip); // get ip address function get_i ...

  9. flask通过form表单一次上传多个文件

    基本上,用了flask官网的示例代码(中文版,英文版),稍微做了修改. import os from flask import Flask, flash, request, redirect, url ...

  10. [C#.net]未在本地计算机上注册“microsoft.ACE.oledb.12.0”提供程序

    C#操作Execl文件常用的2种方式是NPOI和Microsoft.Jet.OleDb,前者需要引用dll,后者需要安装Office客户端 今天发现别人电脑使用Microsoft.Jet.OleDb能 ...