フューチャー技術ブログ

PostgreSQLパーティションプルーニングの動作を確認する

PostgreSQL 10までのパーティション機能を利用したプロジェクトにおいて、遅延SQLの調査をするなかで以下のような長い長い実行計画を目にすることがありました。

こちらはサンプルテーブルでそれを再現したものです。長いので畳みました。
Update on tr_part tgt  (cost=0.83..115.84 rows=4 width=44)
Update on tr_part_p_1809_01 tgt_1
Update on tr_part_p_1809_02 tgt_2
Update on tr_part_p_1809_03 tgt_3
Update on tr_part_p_9912_31 tgt_4
-> Nested Loop (cost=0.83..28.01 rows=1 width=38)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 tgt_1 (cost=0.42..8.44 rows=1 width=16)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.61 rows=4 width=26)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.17 rows=1 width=78)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Nested Loop (cost=0.83..28.01 rows=1 width=38)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16)
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 tgt_2 (cost=0.42..8.44 rows=1 width=16)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.61 rows=4 width=26)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.17 rows=1 width=78)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Nested Loop (cost=0.83..28.01 rows=1 width=38)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16)
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 tgt_3 (cost=0.42..8.44 rows=1 width=16)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.61 rows=4 width=26)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.17 rows=1 width=78)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Nested Loop (cost=0.57..31.81 rows=1 width=64)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.15..17.55 rows=1 width=58)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16)
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 tgt_4 (cost=0.15..5.50 rows=1 width=42)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..14.20 rows=4 width=26)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..4.65 rows=1 width=26)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..4.66 rows=1 width=26)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..4.66 rows=1 width=26)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.22 rows=1 width=78)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))

なぜこのようなことになっているのか、仮に性能問題(SQLの遅延)が発生しているとき、どのような対処が考えられるか説明してきます。

PostgreSQL 10での確認

以下のようにパーティションテーブルを用意しました。

--drop table tr_part;
CREATE TABLE tr_part(
part_date date
, key numeric
, data numeric
)
partition by range(part_date);
--パーティション作成
create table tr_part_p_1809_01 partition of tr_part for values from ( MINVALUE ) to ('20180902');
alter table tr_part_p_1809_01 add constraint pk_tr_part_p_1809_01 primary key(ymd,key);

create table tr_part_p_1809_02 partition of tr_part for values from ( '20180902' ) to ('20180903');
alter table tr_part_p_1809_02 add constraint pk_tr_part_p_1809_02 primary key(ymd,key);

create table tr_part_p_1809_03 partition of tr_part for values from ( '20180903' ) to ('20180904');
alter table tr_part_p_1809_03 add constraint pk_tr_part_p_1809_03 primary key(ymd,key);

create table tr_part_p_9912_31 partition of tr_part for values from ( '20180904' ) to ('99991231');
alter table tr_part_p_9912_31 add constraint pk_tr_part_p_9912_31 primary key(ymd,key);

insert into tr_part select '20180901',generate_series,round(generate_series * random() * 100) from generate_series(1,100000);
insert into tr_part select '20180902',generate_series,round(generate_series * random() * 100) from generate_series(100001,200000);
insert into tr_part select '20180903',generate_series,round(generate_series * random() * 100) from generate_series(200001,300000);
analyze tr_part;

データを投入した p_1809_01p_1809_02p_1809_03 のパーティションに注目すると次のようなイメージです。 part_date の値によってレコードがパーティションに振り分けられて格納されています。

続いて以下のような小さなテーブルを用意します。

--drop table wk_input;
create table wk_input(
key numeric
, related_key numeric
, target_date date
)
;
insert into wk_input select 1,1 ,'20180901';
insert into wk_input select 2,100001,'20180902';
insert into wk_input select 3,200001,'20180903';
analyze wk_input;

そして、以下のようなselectを実行するとどのような動作となるでしょうか。
ポイントはパーティションテーブルのパーティションキーに設定したpart_dateの列が結合条件としてのみ指定されていることです。

select
*
from
wk_input a
, tr_part b
where 1 = 1
and a.related_key = b.key
and a.target_date = b.part_date
;

このとき期待するのは次図の赤線のような動作でしょう。

wk_input の target_date が 20180901 のレコードに対し、tr_partのpart_dateが 20180901 のパーティションにアクセスし、
wk_input の target_date が 20180902 のレコードに対し、tr_partのpart_dateが 20180902 のパーティションにアクセスし、
wk_input の target_date が 20180903 のレコードに対し、tr_partのpart_dateが 20180903 のパーティションにアクセスする。

ORACLEの場合はまさにそのような動作になります。
実行計画でみると以下のようになります。

SELECT STATEMENT
NESTED LOOPS
TABLE ACCESS FULL WK_INPUT
PARTITION RANGE ITERATOR
TABLE ACCESS BY ROWID TR_PART
INDEX UNIQUE SCAN PK_TR_PART

PARTITION RANGE ITERATORのところがまさに、WK_INPUTの各行に対応するパーティションへのアクセスを示しています。

では、PostgreSQL 10ではどのようになるかというとexplain analyzeで先のselect文を実行すると以下のような出力になりました。

Nested Loop  (cost=0.42..93.60 rows=1 width=31) (actual time=66.130..275.215 rows=3 loops=1)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=15) (actual time=16.125..16.261 rows=3 loops=1)
-> Append (cost=0.42..30.82 rows=4 width=16) (actual time=69.391..86.303 rows=1 loops=3)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..8.44 rows=1 width=16) (actual time=48.916..51.817 rows=0 loops=3★)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..8.44 rows=1 width=16) (actual time=17.012..21.819 rows=0 loops=3★)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..8.44 rows=1 width=16) (actual time=12.144..12.594 rows=0 loops=3★)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..5.50 rows=1 width=68) (actual time=0.013..0.013 rows=0 loops=3★)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
Planning time: 118.634 ms
Execution time: 275.380 ms

注目は★マークを付けた4,6,8,10行目の loops=3 のところでしょうか。どのパーティションにもwk_inputの3行に対し3回のアクセスがあることが確認できます。

図にすると次のようなイメージです。

つまりPostgreSQL 10ではクエリ実行時にwk_inputのレコードの値をみて、パーティションプルーニングするような動作はできないことがわかります。
これを踏まえたうえで、パーティションテーブルとパーティションテーブルの結合を考えてみます。

冒頭の実行計画は以下のクエリをexplainしたものです。

update tr_part tgt
set
data = b.data *0.8
from
wk_input a
, tr_part b
where 1 = 1
and a.related_key = b.key
and a.target_date = b.part_date
and b.key = tgt.key
and b.part_date = tgt.part_date
;

冒頭の実行計画の先頭部分を抜き出して以下に貼り付けました。

 Update on tr_part tgt  (cost=0.83..115.84 rows=4 width=44)
Update on tr_part_p_1809_01 tgt_1
Update on tr_part_p_1809_02 tgt_2
Update on tr_part_p_1809_03 tgt_3
Update on tr_part_p_9912_31 tgt_4
-> Nested Loop (cost=0.83..28.01 rows=1 width=38)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01★ tgt_1 (cost=0.42..8.44 rows=1 width=16)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.61 rows=4 width=26)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01● b (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02● b_1 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03● b_2 (cost=0.42..0.48 rows=1 width=26)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31● b_3 (cost=0.15..0.17 rows=1 width=78)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
...省略

パーティションテーブルであるtr_partに注目します。
10行目にあるtr_partテーブルの p_1809_01★ に対し、13~19行目のtr_partテーブルの p_1809_01●p_1809_02●p_1809_03●p_9912_31● が参照されています。

パーティションテーブルtr_partに着目すると、期待する動きは次図ですが、、、

実際は次図のようになっているということです。

パーティション数が多いと、PostgreSQLのこのような動作がかなりな性能遅延を引き起こします。

PostgreSQLでは1テーブルに100を超えるほどのパーティションを定義することはあまり無いでしょう。
しかし、例えば1月分のデータを日次のパーティションで保持している場合の約30パーティションのテーブル同士の結合を想定すると、30×30で900通りのパーティション間の結合を試みることになります。
これがどれほど非効率かは想像にかたくありません。

PostgreSQLのこのような動作に起因して性能劣化が見られる場合は、ユーザからアクセスすべきパーティションを教えてあげる必要があります。
つまり、この例ではアクセス対象のパーティションはwk_inputに保持されているtarget_dateの値で決まっています。
そのため、select distinct target_date from wk_iputのように一度target_dateの一覧を抽出します。
そのうえで、以下のようにパーティションキーのpart_dateの値を以下のクエリの/*あらかじめ取得した値*/のところで指定してループ実行します。

update tr_part tgt
set
data = b.data *0.8
from
wk_input a
, tr_part b
where 1 = 1
and a.related_key = b.key
and a.target_date = b.part_date
and b.key = tgt.key
and b.part_date = tgt.part_date
and b.part_date = /*あらかじめ取得した値*/
;

パーティション数が多く、アクセスが非効率になっているような場合は、このようにパーティションをユーザから特定してあげることで大きな改善がみられる場合があります。

PostgreSQL 11での確認

さて、ある日dockerで環境構築をしていてふとPostgreSQL 11(β版)がpullできるようになっていることに気づきましたので、
ちょろっと触ってみようと思い上記と同様にパーティションプルーニングの動作を確認してみました。

やはりとても長い実行計画が確認できました。。
Update on tr_part tgt  (cost=0.83..115.93 rows=4 width=70) (actual time=235.751..235.751 rows=0 loops=1)
Update on tr_part_p_1809_01 tgt_1
Update on tr_part_p_1809_02 tgt_2
Update on tr_part_p_1809_03 tgt_3
Update on tr_part_p_9912_31 tgt_4
-> Nested Loop (cost=0.83..28.03 rows=1 width=64) (actual time=61.837..72.983 rows=1 loops=1)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32) (actual time=61.790..62.534 rows=1 loops=1)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16) (actual time=17.805..17.811 rows=3 loops=1)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 tgt_1 (cost=0.42..8.44 rows=1 width=16) (actual time=14.901..14.901 rows=0 loops=3)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.63 rows=4 width=26) (actual time=0.036..10.437 rows=1 loops=1)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..0.48 rows=1 width=26) (actual time=0.027..10.424 rows=1 loops=1)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..0.48 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..0.48 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.17 rows=1 width=78) (never executed)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Nested Loop (cost=0.83..28.03 rows=1 width=64) (actual time=56.255..96.389 rows=1 loops=1)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32) (actual time=56.211..74.941 rows=1 loops=1)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16) (actual time=0.011..0.020 rows=3 loops=1)
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 tgt_2 (cost=0.42..8.44 rows=1 width=16) (actual time=24.968..24.968 rows=0 loops=3)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.63 rows=4 width=26) (actual time=0.033..21.437 rows=1 loops=1)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..0.48 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..0.48 rows=1 width=26) (actual time=0.028..21.431 rows=1 loops=1)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..0.48 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.17 rows=1 width=78) (never executed)
Index Cond: ((part_date = tgt_2.part_date) AND (key = tgt_2.key))
-> Nested Loop (cost=0.83..28.03 rows=1 width=64) (actual time=65.428..66.187 rows=1 loops=1)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32) (actual time=65.375..65.377 rows=1 loops=1)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16) (actual time=0.021..0.028 rows=3 loops=1)
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 tgt_3 (cost=0.42..8.44 rows=1 width=16) (actual time=21.778..21.778 rows=0 loops=3)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.63 rows=4 width=26) (actual time=0.038..0.794 rows=1 loops=1)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..0.48 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..0.48 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..0.48 rows=1 width=26) (actual time=0.031..0.785 rows=1 loops=1)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.17 rows=1 width=78) (never executed)
Index Cond: ((part_date = tgt_3.part_date) AND (key = tgt_3.key))
-> Nested Loop (cost=0.57..31.83 rows=1 width=90) (actual time=0.039..0.039 rows=0 loops=1)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.15..17.55 rows=1 width=58) (actual time=0.039..0.039 rows=0 loops=1)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16) (actual time=0.018..0.019 rows=3 loops=1)
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 tgt_4 (cost=0.15..5.50 rows=1 width=42) (actual time=0.005..0.005 rows=0 loops=3)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..14.22 rows=4 width=26) (never executed)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01 b (cost=0.42..4.65 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02 b_1 (cost=0.42..4.66 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03 b_2 (cost=0.42..4.66 rows=1 width=26) (never executed)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31 b_3 (cost=0.15..0.22 rows=1 width=78) (never executed)
Index Cond: ((part_date = tgt_4.part_date) AND (key = tgt_4.key))

残念…と思いきやexplain analyzeの結果を見ると動作が改善されていることがわかりました。
以下に冒頭部分を抜き出しました。

 Update on tr_part tgt  (cost=0.83..115.93 rows=4 width=70) (actual time=235.751..235.751 rows=0 loops=1)
Update on tr_part_p_1809_01 tgt_1
Update on tr_part_p_1809_02 tgt_2
Update on tr_part_p_1809_03 tgt_3
Update on tr_part_p_9912_31 tgt_4
-> Nested Loop (cost=0.83..28.03 rows=1 width=64) (actual time=61.837..72.983 rows=1 loops=1)
Join Filter: ((a.related_key = b.key) AND (a.target_date = b.part_date))
-> Nested Loop (cost=0.42..26.34 rows=1 width=32) (actual time=61.790..62.534 rows=1 loops=1)
-> Seq Scan on wk_input a (cost=0.00..1.03 rows=3 width=16) (actual time=17.805..17.811 rows=3 loops=1)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01★ tgt_1 (cost=0.42..8.44 rows=1 width=16) (actual time=14.901..14.901 rows=0 loops=3)
Index Cond: ((part_date = a.target_date) AND (key = a.related_key))
-> Append (cost=0.42..1.63 rows=4 width=26) (actual time=0.036..10.437 rows=1 loops=1)
-> Index Scan using pk_tr_part_p_1809_01 on tr_part_p_1809_01● b (cost=0.42..0.48 rows=1 width=26) (actual time=0.027..10.424 rows=1 loops=1■)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_02 on tr_part_p_1809_02▲ b_1 (cost=0.42..0.48 rows=1 width=26) (never executed▼)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_1809_03 on tr_part_p_1809_03▲ b_2 (cost=0.42..0.48 rows=1 width=26) (never executed▼)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key))
-> Index Scan using pk_tr_part_p_9912_31 on tr_part_p_9912_31▲ b_3 (cost=0.15..0.17 rows=1 width=78) (never executed▼)
Index Cond: ((part_date = tgt_1.part_date) AND (key = tgt_1.key)
...省略

10行目のtr_partテーブルの p_1809_01★ に対し、13行目の●の同パーティションに、13行目最右の■部分でアクセスがあったことが確認できます。

これに対して、15行目移行の▲で目印をした p_1809_02p_1809_03p_9912_31 のパーティションに対しては、▼部分(never executedと書いていますね)で実際の実行がスキップされていることが確認できます。
PostgreSQLがバージョン11になって、パーティション p_1809_02p_1809_03p_9912_31 の結合を試みても仕方のないものとしてスキップを判断できるようになっています。

長年ORACLEを使い倒してきて、ふとPostgreSQLを使うと、こんなこともできないのか、と思うことがあります。
しかし、日々成長してきていることも感じられ、愛おしくも思えてくるのがPostgreSQLのいいところですね。