以下の内容はhttps://kaigai.hatenablog.com/entry/2025/04/04/100000より取得しました。


PG-Strom v6.0特集:Arrow_Fdw仮想列

PG-Strom v6.0をリリースしました。

GPU-Sortと一部のWindow関数対応
マルチGPUのPinned Inner Buffer対応
Arrow_Fdwの仮想列機能
GPUでの完全な集計値の生成
といった、いくつかの重要機能を含むリリースで、特にGPU-Sortによって新しいワークロードへの対応が広がったという事でバージョン6.x系列としました。
その主要機能について、何回かに分けて解説していきたいと思います。

Arrow_Fdwで時系列のデータを管理する

トランザクショナルなデータを集計・分析する時に、こんな感じのデータ管理を考えた事はないでしょうか?

  • 更新・削除の可能性がある直近のデータはPostgreSQLのテーブル(Heapフォーマット/行形式)に保管しておこう。
  • 一定期間が過ぎたら、もう更新・削除が行われる事もないので、Arrow_Fdw(Arrowフォーマット/列形式)に変換しよう。
  • その時、Arrowファイルはパッと見で分類できるように、YYYYMMDDで日付や、店舗、地域などでカテゴリ分けしよう。

マメな人なら、ファイル名の一部が何らかのコンテンツの属性を代表しているという事もそう珍しい事ではないでしょう。

では、例えばこのような感じのArrowファイルを考えてみる事にします。

$ ls /opt/arrow/mydata/
f_lineorder_1993_AIR.arrow    f_lineorder_1995_RAIL.arrow
f_lineorder_1993_FOB.arrow    f_lineorder_1995_SHIP.arrow
f_lineorder_1993_MAIL.arrow   f_lineorder_1995_TRUCK.arrow
f_lineorder_1993_RAIL.arrow   f_lineorder_1996_AIR.arrow
f_lineorder_1993_SHIP.arrow   f_lineorder_1996_FOB.arrow
f_lineorder_1993_TRUCK.arrow  f_lineorder_1996_MAIL.arrow
f_lineorder_1994_AIR.arrow    f_lineorder_1996_RAIL.arrow
f_lineorder_1994_FOB.arrow    f_lineorder_1996_SHIP.arrow
f_lineorder_1994_MAIL.arrow   f_lineorder_1996_TRUCK.arrow
f_lineorder_1994_RAIL.arrow   f_lineorder_1997_AIR.arrow
f_lineorder_1994_SHIP.arrow   f_lineorder_1997_FOB.arrow
f_lineorder_1994_TRUCK.arrow  f_lineorder_1997_MAIL.arrow
f_lineorder_1995_AIR.arrow    f_lineorder_1997_RAIL.arrow
f_lineorder_1995_FOB.arrow    f_lineorder_1997_SHIP.arrow
f_lineorder_1995_MAIL.arrow   f_lineorder_1997_TRUCK.arrow

これは、Star Schema Benchmarkのlineorderテーブルを、そのタイムスタンプ(lo_orderdate列)とカテゴリ(lo_shipmode)毎に分類し、別々のファイルに保存したものです。
シェルスクリプトで記述すると以下のようになります。

for y in 1993 1994 1995 1996 1997 1998;
do
  for m in AIR RAIL FOB SHIP MAIL TRUCK RAIL;
  do
    pg2arrow -d ssbm -c "SELECT * FROM lineorder WHERE lo_orderdate BETWEEN ${y}0101 AND ${y}1231 AND lo_shipmode = '$m'" -o /opt/arrow/f_lineorder_${y}_${m}.arrow;
  done;
done

計算機ではなく人間がこれらのファイルを参照してデータを検索するとした場合、例えば『1996年3月のトラック輸送、または鉄道輸送の記録・・・』と検索のオーダーに対して、f_lineorder_1993_SHIP.arrowf_lineorder_1997_AIR.arrowのファイルをわざわざ探そうとするでしょうか?しませんよね😅

PG-Strom v6.0で導入されたArrow_Fdwの仮想列機能は、そうしたデータ管理のHeuristicsを一部の検索・集計にも付け加えてやろうというものです。

Arrow_Fdw仮想列の定義

こういったファイル名の命名規則は、サイトによっても異なりますし、管理者によっても異なります。
そこで、Arrow_Fdwはファイル名のどの部分に意味ある情報が含まれているかをオプションによって指定する事を可能にしました。

postgres=# IMPORT FOREIGN SCHEMA f_lineorder FROM SERVER arrow_fdw INTO public
           OPTIONS (dir '/opt/arrow/mydata', pattern 'f_lineorder_@{year}_${shipmode}.arrow');
IMPORT FOREIGN SCHEMA
postgres=# \d f_lineorder
                             Foreign table "public.f_lineorder"
       Column       |     Type      | Collation | Nullable | Default |     FDW options
--------------------+---------------+-----------+----------+---------+----------------------
 lo_orderkey        | numeric       |           |          |         |
 lo_linenumber      | integer       |           |          |         |
 lo_custkey         | numeric       |           |          |         |
 lo_partkey         | integer       |           |          |         |
 lo_suppkey         | numeric       |           |          |         |
 lo_orderdate       | integer       |           |          |         |
 lo_orderpriority   | character(15) |           |          |         |
 lo_shippriority    | character(1)  |           |          |         |
 lo_quantity        | numeric       |           |          |         |
 lo_extendedprice   | numeric       |           |          |         |
 lo_ordertotalprice | numeric       |           |          |         |
 lo_discount        | numeric       |           |          |         |
 lo_revenue         | numeric       |           |          |         |
 lo_supplycost      | numeric       |           |          |         |
 lo_tax             | numeric       |           |          |         |
 lo_commit_date     | character(8)  |           |          |         |
 lo_shipmode        | character(10) |           |          |         |
 year               | bigint        |           |          |         | (virtual 'year')
 shipmode           | text          |           |          |         | (virtual 'shipmode')
Server: arrow_fdw
FDW options: (dir '/opt/arrow/mydata', pattern 'f_lineorder_@{year}_${shipmode}.arrow')

PG-Strom v6.0では新たにArrow_Fdwのオプションとしてpatternが追加されました。
このオプションにより、dirで指定したディレクトリに存在するArrowファイル(複数可)をマッピングした外部テーブルを定義する際に、patternで指定したファイル名だけしか対象に含まれなくなります。
patternにはワイルドカードを含める事ができ、@{xxxx}${xxxx}で囲まれた部分はワイルドカードとして解釈されます。つまり、マッチングした部分に名前が付いていると理解してください。

IMPORT FOREIGN SCHEMAでこれらのArrowファイルをインポートすると、元となったlineorder表には含まれていない列が二つほど追加されているのが見えます。
year列は、ファイル名の@{year}部分にマッチした内容を示すもので、整数値に変換できることを前提にbigint型のデータとしてマップされます。
shipmode列は、ファイル名の${shipmode}部分にマッチした内容を示すもので、単純にマッチした部分をtext型のデータとしてマップされます。

ここで定義したf_lineorder表は30個のArrowファイルを含んでいるため、何も考えずにスキャンすれば30個のArrowファイルを順にスキャンする事になりますが、ある特定のArrowファイルを読み出している間はこれらの仮想列(ファイル名の一部であるyear列やshipmode列)は原理的に変わる事はあり得ません。
Arrow_Fdwがf_lineorder_1993_TRUCK.arrowのスキャンを終えたので、次にf_lineorder_1994_AIR.arrowを読み出そう、というタイミングで仮想列の内容が変化するだけですね。

Arrow_Fdw仮想列を使った「読み飛ばし」

では、このArrow_Fdw仮想列を使って問い合わせを最適化してみる事にします。

以下のクエリはStar Schema BenchmarkのQ1_1ですが、このベンチマークは日付の絞り込みにdate1テーブルとのJOINを行って、そのdate1テーブルのフィールドをd_year = 1993のように指定して絞り込みを行うため、Arrow_Fdw仮想列の絞り込みが効きません。

postgres=# explain
select sum(lo_extendedprice*lo_discount) as revenue
from f_lineorder,date1
where lo_orderdate = d_datekey
and d_year = 1993
and lo_discount between 1 and 3
and lo_quantity < 25;
                                                                         QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=178617.28..178617.29 rows=1 width=32)
   ->  Gather  (cost=178617.17..178617.28 rows=1 width=32)
         Workers Planned: 2
         ->  Parallel Custom Scan (GpuPreAgg) on f_lineorder  (cost=177617.17..177617.18 rows=1 width=32)
               GPU Projection: pgstrom.psum((lo_extendedprice * lo_discount))
               GPU Scan Quals: ((lo_discount >= '1'::numeric) AND (lo_discount <= '3'::numeric) AND (lo_quantity < '25'::numeric)) [plan: 65062080 -> 45182]
               GPU Join Quals [1]: (lo_orderdate = d_datekey) [plan: 45182 -> 6452]
               GPU Outer Hash [1]: lo_orderdate
               GPU Inner Hash [1]: d_datekey
               GPU Group Key:
               referenced: lo_orderdate, lo_quantity, lo_extendedprice, lo_discount
               file0: /opt/arrow/mydata/f_lineorder_1996_MAIL.arrow (read: 107.83MB, size: 427.16MB)
               file1: /opt/arrow/mydata/f_lineorder_1996_SHIP.arrow (read: 107.82MB, size: 427.13MB)
                   :                     :                       :
               file28: /opt/arrow/mydata/f_lineorder_1995_MAIL.arrow (read: 107.43MB, size: 425.58MB)
               file29: /opt/arrow/mydata/f_lineorder_1993_TRUCK.arrow (read: 107.51MB, size: 425.91MB)
               Scan-Engine: GPU-Direct with 2 GPUs <0,1>
               ->  Parallel Seq Scan on date1  (cost=0.00..65.79 rows=215 width=4)
                     Filter: (d_year = 1993)
(19 rows)

しかし、lo_orderdateが1993年のものという条件で検索をするのであれば、以下のようにクエリを書き換えても良いはずです。
date1テーブルへのJOINを外し、代わりに仮想列であるyearを参照しyear = 1993という条件を付加しました。

postgres=# explain analyze
select sum(lo_extendedprice*lo_discount) as revenue
from f_lineorder
where year = 1993
and lo_discount between 1 and 3
and lo_quantity < 25;
                                                                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=167002.79..167002.80 rows=1 width=32) (actual time=74.534..74.614 rows=1 loops=1)
   ->  Gather  (cost=167002.67..167002.78 rows=1 width=32) (actual time=74.516..74.600 rows=2 loops=1)
         Workers Planned: 2
         Workers Launched: 2
         ->  Parallel Custom Scan (GpuPreAgg) on f_lineorder  (cost=166002.67..166002.68 rows=1 width=32) (actual time=25.371..25.372 rows=1 loops=3)
               GPU Projection: pgstrom.psum((lo_extendedprice * lo_discount))
               GPU Scan Quals: ((year = 1993) AND (lo_discount <= '3'::numeric) AND (lo_quantity < '25'::numeric) AND (lo_discount >= '1'::numeric)) [plan: 65062080 -> 226, exec: 13010375 -> 1703647]
               GPU Group Key:
               referenced: lo_quantity, lo_extendedprice, lo_discount, year
               Stats-Hint: (year = 1993)  [loaded: 12, skipped: 48]
               file0: /opt/arrow/mydata/f_lineorder_1996_MAIL.arrow (read: 99.53MB, size: 427.16MB)
               file1: /opt/arrow/mydata/f_lineorder_1996_SHIP.arrow (read: 99.52MB, size: 427.13MB)
                   :                     :                       :
               file28: /opt/arrow/mydata/f_lineorder_1995_MAIL.arrow (read: 99.16MB, size: 425.58MB)
               file29: /opt/arrow/mydata/f_lineorder_1993_TRUCK.arrow (read: 99.24MB, size: 425.91MB)
               Scan-Engine: GPU-Direct with 2 GPUs <0,1>; direct=76245, ntuples=13010375
 Planning Time: 0.718 ms
 Execution Time: 75.027 ms
(18 rows)

そうすると、EXPLAINの出力にはStats-Hint: (year = 1993) [loaded: 12, skipped: 48]という出力が表示されています。
これは、条件句にyear = 1993での絞り込みが含まれており、ファイル名に 1993 を含まないものは明らかに検索条件に合致しないために読み飛ばしているという事を意味します。
これらのArrowファイルはそれぞれ425MB前後とそれほど大きくはないですが、1ファイルあたり2個のRecord-Batchを含んでおり、1993年のファイル6個に対しては12個のRecord-Batchを読み出す必要があったという事を意味しています。

ここで利用できるのは等価演算子だけでなく、例えば不等号であっても読み飛ばしを行うことは可能です。

この機能は、特に非常に多くのファイルを一定の命名規則の下に管理している場合には非常に有効で、実際に2000個を越えるArrowファイルを仮想列の機能を用いて管理しているといった事例も存在します。




以上の内容はhttps://kaigai.hatenablog.com/entry/2025/04/04/100000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14