PostgreSQL 10までのパーティション機能を利用したプロジェクトにおいて、遅延SQLの調査をするなかで以下のような長い長い実行計画を目にすることがありました。
こちらはサンプルテーブルでそれを再現したものです。長いので畳みました。
Update on tr_part tgt (cost=0.83..115.84 rows=4 width=44) |
なぜこのようなことになっているのか、仮に性能問題(SQLの遅延)が発生しているとき、どのような対処が考えられるか説明してきます。
PostgreSQL 10での確認
以下のようにパーティションテーブルを用意しました。
--drop table tr_part; |
データを投入した p_1809_01、 p_1809_02、 p_1809_03 のパーティションに注目すると次のようなイメージです。 part_date の値によってレコードがパーティションに振り分けられて格納されています。
続いて以下のような小さなテーブルを用意します。
--drop table wk_input; |
そして、以下のようなselectを実行するとどのような動作となるでしょうか。
ポイントはパーティションテーブルのパーティションキーに設定したpart_dateの列が結合条件としてのみ指定されていることです。
select |
このとき期待するのは次図の赤線のような動作でしょう。
- 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 |
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) |
注目は★マークを付けた4,6,8,10行目の loops=3 のところでしょうか。どのパーティションにもwk_inputの3行に対し3回のアクセスがあることが確認できます。
図にすると次のようなイメージです。
つまりPostgreSQL 10ではクエリ実行時にwk_inputのレコードの値をみて、パーティションプルーニングするような動作はできないことがわかります。
これを踏まえたうえで、パーティションテーブルとパーティションテーブルの結合を考えてみます。
冒頭の実行計画は以下のクエリをexplainしたものです。
update tr_part tgt |
冒頭の実行計画の先頭部分を抜き出して以下に貼り付けました。
Update on tr_part tgt (cost=0.83..115.84 rows=4 width=44) |
パーティションテーブルである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 |
パーティション数が多く、アクセスが非効率になっているような場合は、このようにパーティションをユーザから特定してあげることで大きな改善がみられる場合があります。
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) |
残念…と思いきや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) |
10行目のtr_partテーブルの p_1809_01★ に対し、13行目の●の同パーティションに、13行目最右の■部分でアクセスがあったことが確認できます。
これに対して、15行目移行の▲で目印をした p_1809_02、 p_1809_03、 p_9912_31 のパーティションに対しては、▼部分(never executedと書いていますね)で実際の実行がスキップされていることが確認できます。
PostgreSQLがバージョン11になって、パーティション p_1809_02、 p_1809_03、 p_9912_31 の結合を試みても仕方のないものとしてスキップを判断できるようになっています。
長年ORACLEを使い倒してきて、ふとPostgreSQLを使うと、こんなこともできないのか、と思うことがあります。
しかし、日々成長してきていることも感じられ、愛おしくも思えてくるのがPostgreSQLのいいところですね。