計劃加速
本文介紹了計劃加速(PartitionedTableScan)的功能背景、使用方法以及性能對比等內(nèi)容。
背景
PolarDB PostgreSQL版(兼容Oracle)對分區(qū)表的分區(qū)數(shù)量沒有限制。當分區(qū)超過2級時,分區(qū)數(shù)量便會成倍增加。
例如,一個分區(qū)表有兩級分區(qū),一級分區(qū)按照哈希分區(qū),有100個分區(qū);二級分區(qū)按照哈希分區(qū),每個二級分區(qū)再次分成100個子分區(qū)。此時整個分區(qū)表共有10000個分區(qū)。此時如果對這個分區(qū)表進行查詢,查詢計劃如下:
explain analyze select * from part_hash;
QUERY PLAN
-----------------------------------------------------------------------------
Append (cost=0.00..344500.00 rows=16300000 width=22)
-> Seq Scan on part_hash_sys0102 (cost=0.00..26.30 rows=1630 width=22)
-> Seq Scan on part_hash_sys0103 (cost=0.00..26.30 rows=1630 width=22)
-> Seq Scan on part_hash_sys0104 (cost=0.00..26.30 rows=1630 width=22)
...
...
...
-> Seq Scan on part_hash_sys10198 (cost=0.00..26.30 rows=1630 width=22)
-> Seq Scan on part_hash_sys10199 (cost=0.00..26.30 rows=1630 width=22)
-> Seq Scan on part_hash_sys10200 (cost=0.00..26.30 rows=1630 width=22)
Planning Time: 3183.644 ms
Execution Time: 633.779 ms
(10003 rows)
Total Memory: 216852KB
從上述結果可以看到,查詢過程比較緩慢。這是因為分區(qū)表在優(yōu)化器中的原理可以簡單理解為:首先對每個分區(qū)生成最優(yōu)的Plan,然后使用Append
算子把這些Plan并聯(lián)起來作為分區(qū)表的最優(yōu)Plan。如果分區(qū)表的分區(qū)數(shù)量較少,這個過程會很快,對于用戶是無感知的;但是一旦達到一定規(guī)模的分區(qū)數(shù),這個過程變得逐漸明顯,用戶在查詢過程中感到分區(qū)表的查詢相比于普通表尤為緩慢。
如上面的SQL中,表part_hash
有10000個分區(qū),它的Planning Time
可以達到3秒左右,但普通表的查詢Planning Time
僅需0.1毫秒,達到了幾百倍的差距。并且除了Planning Time
上的差距,查詢進程內(nèi)存的占用也非常巨大,可能會引發(fā)OOM。
分區(qū)表的這個缺陷在使用連接查詢時更加明顯:
create table part_hash2 (a int, b int, c varchar(10))
PARTITION by HASH(a) SUBPARTITION by HASH (b) PARTITIONS 100 SUBPARTITIONS 100;
explain analyze select count(*) from part_hash a join part_hash2 b on a.a=b.b where b.c = '0001';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=48970442.90..48970442.91 rows=1 width=8) (actual time=6466.854..6859.935 rows=1 loops=1)
-> Gather (cost=48970442.68..48970442.89 rows=2 width=8) (actual time=397.780..6859.902 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=48969442.68..48969442.69 rows=1 width=8) (actual time=4.748..11.768 rows=1 loops=3)
-> Merge Join (cost=1403826.01..42177776.01 rows=2716666667 width=0) (actual time=4.736..11.756 rows=0 loops=3)
Merge Cond: (a.a = b.b)
-> Sort (cost=1093160.93..1110135.93 rows=6790000 width=4) (actual time=4.734..8.588 rows=0 loops=3)
Sort Key: a.a
Sort Method: quicksort Memory: 25kB
Worker 0: Sort Method: quicksort Memory: 25kB
Worker 1: Sort Method: quicksort Memory: 25kB
-> Parallel Append (cost=0.00..229832.35 rows=6790000 width=4) (actual time=4.665..8.518 rows=0 loops=3)
-> Parallel Seq Scan on part_hash_sys0102 a (cost=0.00..19.59 rows=959 width=4) (actual time=0.001..0.001 rows=0 loops=1)
-> Parallel Seq Scan on part_hash_sys0103 a_1 (cost=0.00..19.59 rows=959 width=4) (actual time=0.001..0.001 rows=0 loops=1)
-> Parallel Seq Scan on part_hash_sys0104 a_2 (cost=0.00..19.59 rows=959 width=4) (actual time=0.001..0.001 rows=0 loops=1)
...
-> Sort (cost=310665.08..310865.08 rows=80000 width=4) (never executed)
Sort Key: b.b
-> Append (cost=0.00..304150.00 rows=80000 width=4) (never executed)
-> Seq Scan on part_hash2_sys0102 b (cost=0.00..30.38 rows=8 width=4) (never executed)
Filter: ((c)::text = '0001'::text)
-> Seq Scan on part_hash2_sys0103 b_1 (cost=0.00..30.38 rows=8 width=4) (never executed)
Filter: ((c)::text = '0001'::text)
-> Seq Scan on part_hash2_sys0104 b_2 (cost=0.00..30.38 rows=8 width=4) (never executed)
Filter: ((c)::text = '0001'::text)
...
Planning Time: 221082.616 ms
Execution Time: 9500.148 ms
(30018 rows)
Total Memory: 679540KB
因此我們可以看到分區(qū)表在進行全表查詢時,因為沒有指定任何限定條件,無法將查詢集中在某個分區(qū)內(nèi),這導致在全表查詢場景下,分區(qū)表所有的優(yōu)勢不再存在,比普通表更加低效。盡管我們可以通過分區(qū)剪枝使查詢可以集中在少部分分區(qū),但是對于一些OLAP場景,必須對整個分區(qū)表進行全表掃描。
概述
為了解決這個問題,PolarDB PostgreSQL版(兼容Oracle)提供了PartitionedTableScan
算子。它是一個分區(qū)表的查詢算子,比Append更加高效,可以明顯降低Planning Time
,且使用更少的內(nèi)存,有效避免OOM。該算子用于解決分區(qū)表分區(qū)數(shù)量過多時,查詢性能慢的問題。
下方展示了當使用PartitionedTableScan
算子時,分別查詢這兩個SQL所用的Planning Time
和內(nèi)存。
explain analyze select * from part_hash;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
PartitionedTableScan on part_hash (cost=0.00..1.00 rows=1 width=22) (actual time=134.348..134.352 rows=0 loops=1)(Iteration partition number 10000)
Scan Partitions: part_hash_sys0102, part_hash_sys0103, ...part_hash_sys10198, part_hash_sys10199, part_hash_sys10200
-> Seq Scan on part_hash (cost=0.00..1.00 rows=1 width=22)
Planning Time: 293.778 ms
Execution Time: 384.202 ms
(5 rows)
Total Memory: 40276KB
explain analyze select count(*) from part_hash a join part_hash2 b on a.a=b.b where b.c = '0001';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=2.02..2.03 rows=1 width=8) (actual time=152.322..152.326 rows=1 loops=1)
-> Nested Loop (cost=0.00..2.02 rows=1 width=0) (actual time=152.308..152.311 rows=0 loops=1)
Join Filter: (a.a = b.b)
-> PartitionedTableScan on part_hash a (cost=0.00..1.00 rows=1 width=4) (actual time=152.305..152.306 rows=0 loops=1)(Iteration partition number 10000)
Scan Partitions: part_hash_sys0102, part_hash_sys0103,, part_hash_sys10198, part_hash_sys10199, part_hash_sys10200
-> Seq Scan on part_hash a (cost=0.00..1.00 rows=1 width=4)
-> PartitionedTableScan on part_hash2 b (cost=0.00..1.00 rows=1 width=4) (never executed)
-> Seq Scan on part_hash2 b (cost=0.00..1.00 rows=1 width=4)
Filter: ((c)::text = '0001'::text)
Planning Time: 732.952 ms
Execution Time: 436.927 ms
(11 rows)
Total Memory: 68104KB
可以看到,在本示例中,不管是Planning Time
還是內(nèi)存Total Memory
,使用PartitionedTableScan
算子后都顯著下降。具體數(shù)字對比示例如下表:
類型 |
|
|
Single Query Planning Time | 3183.644ms | 293.778ms |
Single Total Memory | 216852 KB | 40276 KB |
Join Query Planning Time | 221082.616ms | 732.952ms |
Join Total Memory | 679540 KB | 68104 KB |
使用限制
PolarDB PostgreSQL版(兼容Oracle)1.0和2.0都支持
PartitionedTableScan
算子。且內(nèi)核小版本需為V1.1.32及以上。PartitionedTableScan
目前僅支持Select
,不支持DML語句。PartitionedTableScan
不支持分區(qū)連接。如果您開啟了分區(qū)連接功能,將不會生成PartitionedTableScan
計劃。
PartitionedTable Scan
功能僅適用于內(nèi)核小版本為V1.1.32及以上的集群。若早于該內(nèi)核小版本的存量集群需要使用該功能,請聯(lián)系我們進行開啟。
使用說明
為了更好的說明PartitionedTableScan
功能,我們結合如下示例來進行介紹。
首先創(chuàng)建一張分區(qū)表。
CREATE TABLE prt1 (a int, b int, c varchar) PARTITION BY Hash(a) partitions 16;
通過參數(shù)啟用PartitionedTableScan
通過對參數(shù)polar_num_parts_for_partitionedscan
的設置調整,可以開啟或關閉PartitionedTableScan
功能。
參數(shù) | 取值范圍 | 默認值 | 說明 |
polar_num_parts_for_partitionedscan | -1至INT_MAX | 64 | 表示 例如,當為默認值時,即表示當分區(qū)表的子分區(qū)數(shù)量大于64時,自動開啟 該參數(shù)有如下兩個特殊取值:
|
示例如下:
SET polar_num_parts_for_partitionedscan to -1;
explain select * from prt1;
QUERY PLAN
-----------------------------------------------------------------
PartitionedTableScan on prt1 (cost=0.00..1.00 rows=1 width=40)
-> Seq Scan on prt1 (cost=0.00..1.00 rows=1 width=40)
(2 rows)
使用HINT
使用HINT語法PARTEDSCAN(table alias)
,示例如下:
EXPLAIN select /*+PARTEDSCAN(prt1) */ * from prt1;
QUERY PLAN
-----------------------------------------------------------------
PartitionedTableScan on prt1 (cost=0.00..1.00 rows=1 width=40)
-> Seq Scan on prt1 (cost=0.00..1.00 rows=1 width=40)
(2 rows)
并行查詢
PolarDB PostgreSQL版(兼容Oracle)支持分區(qū)表的并行查詢,它能很好的處理大規(guī)模數(shù)據(jù)的查詢。和Append
一樣,PartitionedTableScan
也支持并行查詢。但和Append不同的是,PartitionedTableScan
的并行查詢被稱之為PartitionedTableScan Append
,并行的方式只有兩種:分區(qū)間并行和混合并行。
分區(qū)間并行
分區(qū)間并行是指每個worker查詢一個分區(qū),以實現(xiàn)多個worker并行查詢整個分區(qū)表。
EXPLAIN (COSTS OFF) select /*+PARTEDSCAN(prt1) */ * from prt1;
QUERY PLAN
---------------------------------------------
Gather
Workers Planned: 4
-> Parallel PartitionedTableScan on prt1
-> Seq Scan on prt1
(4 rows)
如上所示,整個分區(qū)表啟動了4個并行的Worker(Workers Planned: 4),每個Worker負責查詢一個分區(qū)。其中,明顯的標志是有一個名為Parallel PartitionedTableScan
的算子。
混合并行
混合并行是指分區(qū)間和分區(qū)內(nèi)都可以并行執(zhí)行,從而達到分區(qū)表整體的并行執(zhí)行,這是并行度最高的一種并行查詢。
EXPLAIN (COSTS OFF) select /*+PARTEDSCAN(prt1) */ * from prt1;
QUERY PLAN
---------------------------------------------
Gather
Workers Planned: 8
-> Parallel PartitionedTableScan on prt1
-> Parallel Seq Scan on prt1
(4 rows)
如上所示,整個查詢使用了8個worker進行并行查詢(Workers Planned: 8),每個分區(qū)之間可以并行查詢,每個分區(qū)內(nèi)部也可以并行查詢。其中,明顯的標志是有一個名為Parallel PartitionedTableScan
的算子。
以上兩種并行方式都有自己的代價模型,優(yōu)化器會選擇最優(yōu)的一種。
分區(qū)剪枝
PartitionedTableScan
和Append
一樣,支持三個階段的分區(qū)剪枝。關于分區(qū)剪枝的詳細說明,請參見分區(qū)剪枝。
性能對比
PartitionedTableScan
相比于Append
更加高效。如下測試數(shù)據(jù)展示了PartitionedTableScan
和Append
的性能對比。
以下測試數(shù)據(jù)是開發(fā)環(huán)境測試出的臨時數(shù)據(jù),不是性能標準數(shù)據(jù)。不同配置、不同條件下測試出的數(shù)據(jù)可能不同。本測試的目的是根據(jù)單一變量原則,在環(huán)境配置一致的情況下,對比Append
和PartitionedTableScan
的性能差異。
如下為測試SQL:
explain select * from prt1 where b = 10;
explain select /*+PARTEDSCAN(prt1) */ * from prt1 where b = 10;
單條SQL的Planning Time
分區(qū)數(shù)量 | Append Planning Time | PartitionedTableScan Planning Time |
16 | 0.266ms | 0.067ms |
32 | 1.820ms | 0.258ms |
64 | 3.654ms | 0.402ms |
128 | 7.010ms | 0.664ms |
256 | 14.095ms | 1.247ms |
512 | 27.697ms | 2.328ms |
1024 | 73.176ms | 4.165ms |
Memory(單條SQL的內(nèi)存使用量)
分區(qū)數(shù)量 | Append Memory | PartitionedTableScan Memory |
16 | 1,170 KB | 1,044 KB |
32 | 1,240 KB | 1,044 KB |
64 | 2,120 KB | 1,624 KB |
128 | 2,244 KB | 1,524 KB |
256 | 2,888 KB | 2,072 KB |
512 | 4,720 KB | 3,012 KB |
1024 | 8,236 KB | 5,280 KB |
QPS(Query per Second)
pgbench -i --scale=10
pgbench -c 64 -j 64 -n -T60
Query:
explain select * from prt1 where b = 10;
explain select /*+PARTEDSCAN(prt1) */ * from prt1 where b = 10;
分區(qū)數(shù)量 | Append QPS | PartitionedTableScan QPS |
16 | 25,318 | 93,950 |
32 | 10,906 | 61,879 |
64 | 5,281 | 30,839 |
128 | 2,195 | 16,684 |
256 | 920 | 8,372 |
512 | 92 | 3,708 |
1024 | 21 | 1,190 |
結論
從上面的PartitionedTableScan
和Append
的對比可以看出,PartitionedTableScan
相比于Append
隨著分區(qū)數(shù)量增加時,性能提升明顯。因此,如果您在業(yè)務中分區(qū)表分區(qū)數(shù)量較多且Planning Time很大時,我們建議您使用PartitionedTableScan
進行一定程度的優(yōu)化。