1. 程式人生 > >postgresql-分頁數據重復問題探索

postgresql-分頁數據重復問題探索

nta 選擇排序 fence analyze ade 1.3 tps word table


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-分頁數據重復問題探索