Partition-wise GpuJoin/GpuPreAgg

PostgreSQL v10以降ではテーブルパーティショニングの機能が入っており、値の範囲、または値のリストによってテーブルをいくつかのパーティションに分割する事が可能となっている。

遅まきながら、PG-Stromにパーティションを意識した実行計画を作成するよう機能拡張を行ってみた。

以下の実行計画を見てもらいたい。これは、従来のPG-Stromを使って、DATE型の列 ymd の値を元に pt_2010, pt_2011, ..., pt_2019 まで一年ごとにデータを別テーブルに分割する構成を取っている。

検索条件にWHERE ymd > '2017-01-01'::dateが含まれているため、明らかに検索条件に該当しない子テーブルはスキャンの対象から外されている。したがって、実際にスキャンが行われるのは pt_2017, pt_2018, pt_2019 の3テーブルのみである。

postgres=# EXPLAIN SELECT cat,count(*),avg(ax) FROM pt NATURAL JOIN t1 WHERE ymd > '2017-01-01'::date GROUP BY cat;
                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Finalize GroupAggregate  (cost=341392.92..341399.42 rows=200 width=48)
   Group Key: pt.cat
   ->  Sort  (cost=341392.92..341393.92 rows=400 width=72)
         Sort Key: pt.cat
         ->  Gather  (cost=341333.63..341375.63 rows=400 width=72)
               Workers Planned: 2
               ->  Partial HashAggregate  (cost=340333.63..340335.63 rows=200 width=72)
                     Group Key: pt.cat
                     ->  Parallel Custom Scan (GpuJoin)  (cost=283591.92..283591.92 rows=7565562 width=40)
                           Depth 1: GpuHashJoin  (nrows 3152318...7565562)
                                    HashKeys: pt.aid
                                    JoinQuals: (pt.aid = t1.aid)
                                    KDS-Hash (size: 10.78MB)
                           ->  Append  (cost=28540.80..200673.34 rows=3152318 width=36)
                                 ->  Parallel Custom Scan (GpuScan) on pt_2017  (cost=28540.80..66891.11 rows=1050772 width=36)
                                       GPU Filter: (ymd > '2017-01-01'::date)
                                 ->  Parallel Custom Scan (GpuScan) on pt_2018  (cost=28540.81..66883.43 rows=1050649 width=36)
                                       GPU Filter: (ymd > '2017-01-01'::date)
                                 ->  Parallel Custom Scan (GpuScan) on pt_2019  (cost=28540.80..66898.79 rows=1050896 width=36)
                                       GPU Filter: (ymd > '2017-01-01'::date)
                           ->  Seq Scan on t1  (cost=0.00..1935.00 rows=100000 width=12)
(21 rows)

しかしこの実行計画には問題がある。
テーブルスキャンを実行するのに GpuScan が選択されており、この出力を Append が受け取った後で、再び GpuJoin がこれを実行する。
つまり、GPU -> ホストRAM -> GPU -> ホストRAM というデータのピンポンが発生しており、条件句の評価やJOIN処理と言ったCPU喰いの処理のオフロードよりもむしろ、PCIeバスを通じたデータの移動に時間を取られやすいという事が容易に想像できる。

次に、パーティション対応を加えた PG-Strom だとどうなるか。

postgres=# EXPLAIN SELECT cat,count(*),avg(ax) FROM pt NATURAL JOIN t1 WHERE ymd > '2017-01-01'::date group by cat;
                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 GroupAggregate  (cost=196450.44..196476.94 rows=200 width=48)
   Group Key: pt_2017.cat
   ->  Sort  (cost=196450.44..196453.44 rows=1200 width=72)
         Sort Key: pt_2017.cat
         ->  Gather  (cost=66085.69..196389.07 rows=1200 width=72)
               Workers Planned: 2
               ->  Parallel Append  (cost=65085.69..195269.07 rows=600 width=72)
                     ->  Parallel Custom Scan (GpuPreAgg)  (cost=65085.69..65089.69 rows=200 width=72)
                           Reduction: Local
                           Combined GpuJoin: enabled
                           ->  Parallel Custom Scan (GpuJoin) on pt_2017  (cost=32296.64..74474.20 rows=1050772 width=40)
                                 Outer Scan: pt_2017  (cost=28540.80..66891.11 rows=1050772 width=36)
                                 Outer Scan Filter: (ymd > '2017-01-01'::date)
                                 Depth 1: GpuHashJoin  (nrows 1050772...2521854)
                                          HashKeys: pt_2017.aid
                                          JoinQuals: (pt_2017.aid = t1.aid)
                                          KDS-Hash (size: 10.78MB)
                                 ->  Seq Scan on t1  (cost=0.00..1935.00 rows=100000 width=12)
                     ->  Parallel Custom Scan (GpuPreAgg)  (cost=65078.35..65082.35 rows=200 width=72)
                           Reduction: Local
                           Combined GpuJoin: enabled
                           ->  Parallel Custom Scan (GpuJoin) on pt_2018  (cost=32296.65..74465.75 rows=1050649 width=40)
                                 Outer Scan: pt_2018  (cost=28540.81..66883.43 rows=1050649 width=36)
                                 Outer Scan Filter: (ymd > '2017-01-01'::date)
                                 Depth 1: GpuHashJoin  (nrows 1050649...2521557)
                                          HashKeys: pt_2018.aid
                                          JoinQuals: (pt_2018.aid = t1.aid)
                                          KDS-Hash (size: 10.78MB)
                                 ->  Seq Scan on t1  (cost=0.00..1935.00 rows=100000 width=12)
                     ->  Parallel Custom Scan (GpuPreAgg)  (cost=65093.03..65097.03 rows=200 width=72)
                           Reduction: Local
                           Combined GpuJoin: enabled
                           ->  Parallel Custom Scan (GpuJoin) on pt_2019  (cost=32296.65..74482.64 rows=1050896 width=40)
                                 Outer Scan: pt_2019  (cost=28540.80..66898.79 rows=1050896 width=36)
                                 Outer Scan Filter: (ymd > '2017-01-01'::date)
                                 Depth 1: GpuHashJoin  (nrows 1050896...2522151)
                                          HashKeys: pt_2019.aid
                                          JoinQuals: (pt_2019.aid = t1.aid)
                                          KDS-Hash (size: 10.78MB)
                                 ->  Seq Scan on t1  (cost=0.00..1935.00 rows=100000 width=12)
(40 rows)

クエリは同一なので、pt_2017, pt_2018, pt_2019 の3テーブルだけを読み出す事は共通だが、生成された実行計画が大きく異なる事が分かる。
パーティションの各子テーブルから読み出した内容を結合する Append より前に、GpuJoinおよびGpuPreAggが押し込まれ、特に GROUP BY での行数削減効果が大きいため、実際に Append で処理されるのは高々数百行程度に過ぎない事がわかる。
しかも、GpuJoinからGpuPreAgg間はCombined GpuJoin: enabledとあるので、ホストシステムを介さずにGPUバイスメモリ上でJOIN結果をGROUP BY処理へ受け渡す事になっている。つまり、ホストシステムで動作する Append とその後の GroupAggregate にとっては、パーティションに分割するレベルの大きさのデータであっても、実際に処理しなければならないのは(大半はGPUによって消し込まれているため)数百行程度にしかならないという事である。

パーティション設定を物理的なデータ配置と重ね合わせると面白そうな事になるのが分かる。
ホストシステムからはI/O拡張ボックスを使ってGPUSSDを接続するものとし、I/O拡張ボックスに搭載したSSDごとにパーティションを切るものとする。
そうすると、SCAN -> JOIN -> GROUP BY までの処理は、SSD-to-GPUダイレクトSQL実行を用いると一貫してI/O拡張ボックスで処理する事が可能となり、ホストシステムの負荷は極めて小さなものとなる。

PostgreSQL v10ではAppend配下の子テーブルの処理を並列に実行する事ができないため、複数のI/O拡張ボックスを備えていたとしても同時にアクティブになるのは1個だけだが、これはPostgreSQL v11で改善される。そのため、v11がリリースされる頃には、I/O拡張ボックスを増設すれば増設するだけ処理性能と容量を同時に拡張する事ができるというシステムが現実味を帯びる事になる。
現状、I/O拡張ボックスを使った性能測定ではSSDx2枚を用いて少なくとも4.8GB/sのスループットまで出る事は確認できている。ワークロードを選ぶことは確かだが、うまくハマればI/O拡張ボックスの数に比例してスケールするため、イマドキのハイエンドDWHのスペックである数十GB/sのクエリ処理性能も夢ではない。

現状、ひとまず実行計画でパーティションを意識するように追加の実行パスを生成するように修正を加えただけである。
まだ欠けている機能としては以下の2つ。

  • GpuJoinのRight-Tree側を何度も読まずに済むようにする。
  • バックグラウンドワーカがGPUを初期化する時に、SSDに近傍のものを選択する。

これは、上記のEXPLAINの例ではテーブル t1 が相当するものだが、GpuJoinが3個のパーティション子テーブル側にPush-downされた結果、テーブル t1 の読出しを3回も行う必要が出てきたというもの。
これはもう少し実装を頑張れば、例えばGpuJoin用のハッシュテーブルを共有メモリ上に展開するなどして、一度だけ読み出せば全てのバックグラウンドワーカがハッシュテーブルへの参照を共有できる。

また、PG-Strom v2.0では(設計単純化を目的として)各プロセスが同時に利用するGPUは一個だけとし、複数GPUを用いる場合には必然的にCPUパラレルクエリを前提とするという設計になった。そのため、プロセスがGPUを初期化する際に、自分がこれからスキャンしようとする子テーブルに(物理的に)近傍のGPUを選択する事がよい戦略となる。

この辺の課題を解決しつつ、今週リリース予定のPostgreSQL v11に合わせてパーティション機能への対応を強化していきたい。