はじめに
今回はpg-strom1の開発でお馴染み、海外さん(@kkaigai) さんが作成したツールpg2arrowを久々に試してみました。
実は、以前(1月頃)、開発途中版を少し使ってみたことはあったが、自身の入院やら期末の仕事やら期首の仕事やらに悩殺されていて、しばらく動かしてなかった。そして、arrow_fdw2とpg2arrowの正式版(でいいのかな)が4月後半にリリースされたので、再び試してみることにしました。
Apache Arrowとは
Apache arrowというのは最近検討されている列指向のファイルフォーマットです。
Apache Arrowの特徴や、どういった目的で作られたものか、といった情報は、Apache Arrowの詳細は公式ページや、クリアコード社の須藤さんのブログ (Apache Arrowの最新情報(2018年9月版))などを参考にすると良いと思います。
pg2arrowとは
pg2arrowとは、ごく簡単に言えば、PostgreSQLの検索結果を元に、Apache arrow形式のファイルを生成する、というユーティリティです。
また、デバッグ用/確認用として、--dumpオプションを付与することで、Apache Arrow形式のファイルのメタ情報を可視化することができます。
pg2arrowを使ってみる
ビルド方法や使い方などは、github上のWikiにも記載されているのでそっちを参照してもらえばと。現状はRPMパッケージの提供等はないので、ソースからビルドする必要があります。3
また、pg2arrowはPostgreSQLのクライアントユーティリティと同様、libpqに依存しているので、libpqが使えるように事前に設定しておく必要があります。
pg2arrowのオプションはこんな感じ。psqlを使い慣れている人なら、特に違和感なく使えると思います。
$ pg2arrow --help
Usage:
pg2arrow [OPTION]... [DBNAME [USERNAME]]
General options:
-d, --dbname=DBNAME database name to connect to
-c, --command=COMMAND SQL command to run
-f, --file=FILENAME SQL command from file
(-c and -f are exclusive, either of them must be specified)
-o, --output=FILENAME result file in Apache Arrow format
(default creates a temporary file)
Arrow format options:
-s, --segment-size=SIZE size of record batch for each
(default: 256MB)
Connection options:
-h, --host=HOSTNAME database server host
-p, --port=PORT database server port
-U, --username=USERNAME database user name
-w, --no-password never prompt for password
-W, --password force password prompt
Debug options:
--dump=FILENAME dump information of arrow file
--progress shows progress of the job.
Report bugs to <pgstrom@heterodb.com>.
$
クエリ結果をApache Arrowファイルに出力する
一番、シンプルな使い方は、-c
オプションでクエリを指定して、-o
オプションで指定したファイルに出力するというものです。
あるいは-f
でクエリファイルを指定して実行しても良い。複雑なクエリの結果をApache Arrow化する場合には、こっちを使うほうが楽かもしれません。
なお、最初に書いたように、psqlとpg2arrowのオプションは類似しているので、pg2arrowを使ってファイルを生成する前に、psqlで試した結果を確認し、その後に実行ファイル名をpsqlからpg2arrowに変更する、という試し方がおすすめです。
PostgreSQLユーザならお馴染み、pgbenchのpgbench_accountsテーブル
$ psql testdb -c "\d pgbench_accounts"
Unlogged table "public.pgbench_accounts"
Column | Type | Collation | Nullable | Default
----------+---------------+-----------+----------+---------
aid | integer | | not null |
bid | integer | | |
abalance | integer | | |
filler | character(84) | | |
Indexes:
"pgbench_accounts_pkey" PRIMARY KEY, btree (aid)
を全件検索した結果からApache Arrowファイルを生成する例を示します。
$ pg2arrow testdb -c "SELECT * FROM pgbench_accounts" -o /tmp/accounts.dat
$
pg2arrowも標準のPostgreSQLユーティリティ(createdbとかみたいに)っぽく、成功すると静かに終わります。
大きいサイズの結果を生成するときに、「頑張って動いてるのかな~」というのが気になる場合には、--progress
オプションをつけて進行状況を表示してみるのも良いでしょう。
$ pg2arrow --progress testdb -c "SELECT * FROM pgbench_accounts" -o /tmp/accounts.dat
RecordBatch 0: offset=356 length=268435800 (meta=280, body=268435520)
RecordBatch 1: offset=268436156 length=268435800 (meta=280, body=268435520)
RecordBatch 2: offset=536871956 length=268435800 (meta=280, body=268435520)
RecordBatch 3: offset=805307756 length=154694296 (meta=280, body=154694016)
$
生成されたファイルを確認します。
$ ls -l /tmp/accounts.dat
-rw-r--r-- 1 ec2-user ec2-user 960002518 May 5 00:43 /tmp/accounts.dat
[ec2-user@ip-10-0-1-10 ~]$
Apache Arrowファイルのメタ情報を確認する
pg2arrowの実行時に、--dump
オプションを付与することで、生成したApache Arrowファイルのメタ情報を出力することができます。
さきほど生成した/tmp/accounts.dat
ファイルを--dump
オプションつきで実行した例を示します。
$ pg2arrow --dump /tmp/accounts.dat
[Footer]
{Footer: version=3, schema={Schema: endianness=little, fields=[{Field: name=aid, nullable=true, type={Int32}, children=[], custom_metadata=[]}, {Field: name=bid, nullable=true, type={Int32}, children=[], custom_metadata=[]}, {Field: name=abalance, nullable=true, type={Int32}, children=[], custom_metadata=[]}, {Field: name=filler, nullable=true, type={FixedSizeBinary: byteWidth=84}, children=[], custom_metadata=[]}], custom_metadata []}, dictionaries=[], recordBatches=[{Block: offset=356, metaDataLength=280 bodyLength=268435520}, {Block: offset=268436156, metaDataLength=280 bodyLength=268435520}, {Block: offset=536871956, metaDataLength=280 bodyLength=268435520}, {Block: offset=805307756, metaDataLength=280 bodyLength=154694016}]}
[Record Batch 0]
{Message: version=3, body={RecordBatch: length=2796202, nodes=[{FieldNode: length=2796202, null_count=0}, {FieldNode: length=2796202, null_count=0}, {FieldNode: length=2796202, null_count=0}, {FieldNode: length=2796202, null_count=0}], buffers=[{Buffer: offset=0, length=0}, {Buffer: offset=0, length=11184832}, {Buffer: offset=11184832, length=0}, {Buffer: offset=11184832, length=11184832}, {Buffer: offset=22369664, length=0}, {Buffer: offset=22369664, length=11184832}, {Buffer: offset=33554496, length=0}, {Buffer: offset=33554496, length=234881024}]}, bodyLength=268435520}
(中略)
[Record Batch 3]
{Message: version=3, body={RecordBatch: length=1611394, nodes=[{FieldNode: length=1611394, null_count=0}, {FieldNode: length=1611394, null_count=0}, {FieldNode: length=1611394, null_count=0}, {FieldNode: length=1611394, null_count=0}], buffers=[{Buffer: offset=0, length=0}, {Buffer: offset=0, length=6445632}, {Buffer: offset=6445632, length=0}, {Buffer: offset=6445632, length=6445632}, {Buffer: offset=12891264, length=0}, {Buffer: offset=12891264, length=6445632}, {Buffer: offset=19336896, length=0}, {Buffer: offset=19336896, length=135357120}]}, bodyLength=154694016}
- 最初に出力される
[Footer]
には、フォーマットバージョン、スキーマ(どういうApache Arrowデータタイプを持っているか、NULL可なデータなのか等)の情報が出力されます。- PostgreSQLデータタイプと、Apache Arrowデータタイプとの対応は、pgaeeowのWikiに詳細に説明があります。[^4]
- データそのものが格納されるのは、
[Record Batch N]
で示した領域のようです。--dump
各列のデータを格納しているオフセットや長さをオプションでは、各列の格納領域のセグメント内でのオフセットと長さを表示しています。Apache Arrowにアクセスするプログラムは、このオフセットや長さを元に列単位でデータを取得する、ということが出来るのでしょう。
Apache Arrowファイルの中身を見る
Apache Arrowファイルはバイナリファイルなので、catコマンドやvimの通常モードでは参照できない。Apache Arrowファイルの中身を見るためには、hexdumpなどのバイナリダンプコマンドや、vimのバイナリ表示/編集モード(vim -b
→ :%!xxd
)を使う必要があります。
(あとで、hexdumpによるApache Arrowファイルの出力例もだします)
その他のオプション
--segments-size (-s) オプション
Apache Arrowではデータ格納領域をセグメントというもので区切って格納しているっぽいです。pg2arrowではデフォルトでは256MBの単位で1つの格納領域を区切ってデータを詰めているようですが、その1つの格納領域のサイズを指定するというオプションです。
例えば、セグメントサイズを64MBに変更して実行してみます。
$ pg2arrow --progress -s 64MB testdb -c "SELECT * FROM pgbench_accounts" -o /tmp/accounts.dat
RecordBatch 0: offset=356 length=67109208 (meta=280, body=67108928)
(中略)
RecordBatch 13: offset=872420060 length=67109208 (meta=280, body=67108928)
RecordBatch 14: offset=939529268 length=20477272 (meta=280, body=20476992)
[ec2-user@ip-10-0-1-10 ~]$
デフォルト(256MB)と比べて--progress
で表示されたRecordBatch
の番号が0~3から0~14に変わっているのがわかると思います。また、セグメント毎のデータの大きさ(length)が
おおよそ64M'67,108,864)になっています。(多少値が違うのはセグメントのヘッダ情報とかが入っているからなのかな?)
細かすぎてどうでもいい話
pg2arrowには任意のクエリが記述できるので、(通常は役にたたないけど)ちょっと変わったクエリを試したくなるもの。いくつかやってみました。これは実用上はわりとどうでもいい話だとは思いますが・・・
単なる式
PostgreSQLではFROM
句のない単なる式もSELECTで記述できます。そういうクエリであっても、pg2arrowは特に気にせずファイルを出力します。
$ pg2arrow postgres -c "SELECT 'pg-strom' result" -o /tmp/expr.dat
$ ls -l /tmp/expr.dat
-rw-r--r-- 1 ec2-user ec2-user 542 May 5 02:20 /tmp/expr.dat
$
せっかくなので出力ファイルもhexdumpで表示してみましょう。
$ hexdump -C /tmp/expr.dat
$ hexdump -C /tmp/expr.dat
00000000 41 52 52 4f 57 31 00 00 64 00 00 00 10 00 00 00 |ARROW1..d.......|
00000010 00 00 0a 00 0c 00 04 00 06 00 08 00 0a 00 00 00 |................|
00000020 03 00 01 00 0c 00 00 00 08 00 08 00 00 00 04 00 |................|
00000030 08 00 00 00 04 00 00 00 01 00 00 00 14 00 00 00 |................|
00000040 10 00 10 00 04 00 08 00 09 00 00 00 00 00 0c 00 |................|
00000050 10 00 00 00 0c 00 00 00 01 05 00 00 10 00 00 00 |................|
00000060 06 00 00 00 72 65 73 75 6c 74 00 00 00 00 00 00 |....result......|
00000070 94 00 00 00 10 00 00 00 0c 00 18 00 04 00 06 00 |................|
00000080 08 00 10 00 0c 00 00 00 03 00 03 00 1a 00 00 00 |................|
00000090 00 00 00 00 80 00 00 00 00 00 00 00 0a 00 18 00 |................|
000000a0 08 00 10 00 14 00 0a 00 00 00 00 00 00 00 01 00 |................|
000000b0 00 00 00 00 00 00 08 00 00 00 18 00 00 00 01 00 |................|
000000c0 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000000d0 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 |..............@.|
000000f0 00 00 00 00 00 00 40 00 00 00 00 00 00 00 40 00 |......@.......@.|
00000100 00 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 |................|
00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000140 00 00 00 00 00 00 00 00 70 67 2d 73 74 72 6f 6d |........pg-strom|
00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000180 00 00 00 00 00 00 00 00 10 00 00 00 0c 00 14 00 |................|
00000190 04 00 08 00 0c 00 10 00 0c 00 00 00 03 00 00 00 |................|
000001a0 14 00 00 00 50 00 00 00 50 00 00 00 08 00 08 00 |....P...P.......|
000001b0 00 00 04 00 08 00 00 00 04 00 00 00 01 00 00 00 |................|
000001c0 14 00 00 00 10 00 10 00 04 00 08 00 09 00 00 00 |................|
000001d0 00 00 0c 00 10 00 00 00 0c 00 00 00 01 05 00 00 |................|
000001e0 10 00 00 00 06 00 00 00 72 65 73 75 6c 74 00 00 |........result..|
000001f0 00 00 00 00 00 00 00 00 01 00 00 00 70 00 00 00 |............p...|
00000200 00 00 00 00 98 00 00 00 00 00 00 00 80 00 00 00 |................|
00000210 00 00 00 00 8c 00 00 00 41 52 52 4f 57 31 |........ARROW1|
0000021e
[ec2-user@ip-10-0-1-10 ~]$
$
なんか、さっきのクエリにあった列名(result
)や検索結果(pg-strom
)が入ってますね。
0件の結果を返すクエリ
次に、結果が0件になるクエリだとどうなるか見てみる。
$ psql postgres -c 'SELECT 1 WHERE 1=0'
?column?
----------
(0 rows)
$ pg2arrow postgres -c 'SELECT 1 WHERE 1=0' -o /tmp/notresult.dat
pg2arrow.c:646 SQL command returned an empty result
$
pg2arrowでは、0件となるクエリはエラーとなるので注意。
列が存在しない行を返すクエリ
今度は列が存在しない結果だとどうなるか確認してみる。
PostgreSQL 9.4以降、SELECTリストが存在しないSELECT文が記述できるようになった(冗長化機能のヘルスチェック用にSELECT 1
というクエリを投げることがあると思いますが、PostgreSQL 9.4以降はSELECT
だけでもOKだったりします。どうでもいいけど)。
では、そんなクエリを与えてもpg2arrowは動作するのか。やってみました。
$ psql postgres -c 'SELECT FROM (SELECT generate_series(1,5)) t'
--
(5 rows)
$
列のない結果をもつ5行の結果を返すクエリをpg2arrowに与えてみます。
$ pg2arrow postgres --progress -c 'SELECT FROM (SELECT generate_series(1,5)) t' -o /tmp/nocol.dat
RecordBatch 0: offset=48 length=76 (meta=76, body=0)
$ pg2arrow --dump /tmp/nocol.dat
[Footer]
{Footer: version=3, schema={Schema: endianness=little, fields=[], custom_metadata []}, dictionaries=[], recordBatches=[{Block: offset=48, metaDataLength=76 bodyLength=0}]}
[Record Batch 0]
{Message: version=3, body={RecordBatch: length=5, nodes=[], buffers=[]}, bodyLength=0}
$
特に問題なく実行され、Apache Arrow形式のファイルも生成されているようですね。
dump情報を見るとスキーマ情報は実質上からっぽです。
でも、5行分の空のデータを持つファイルが作成されています。ちょっと面白い。。
PostgreSQLでは列や行もない結果を返すクエリだって記述できるけど、最初に書いたように0件の結果はpg2arrowでは受け付けないので、同様のエラーになります。
(そもそもそんなクエリの結果からApache Arrowファイルを作成する意味もないけど)
ユーザ定義型の扱い
通常はあまり使うことはないけど、PostgreSQLでは自分で定義したデータ型(ユーザ定義型)を使うことができる。有名どころだと、orafce(Oracle’s compatibility functions and packages)が提供している、Oracle互換型のようなものがあります。
自分は変なユーザ定義型を作るのが好きなので、pg_fraction(分数の演算を行えるデータ型)なんかを作っています。じゃあ、そのユーザ定義型を使ったクエリをpg2arrowに与えたらどうなるのかやってみました。
pg_fractionの実行例。
$ psql testdb -c "SELECT '1/2'::fraction + '3/4::fraction'"
?column?
----------
5/4
(1 row)
で、同じクエリをpg2arrowにかけてみると・・・
$ pg2arrow testdb -c "SELECT '1/2'::fraction + '3/4::fraction'" -o /tmp/udt.dat
pg2arrow.c:329 SQL execution failed: ERROR: no binary output function available for type fraction、
こんなエラーになってしまいます。これはfraction型がバイナリ出力関数を実装していないためです。もし、バイナリ出力関数を実装していたら、Apache ArrowのBinary
データ型としてマッピングされるはずです(これは実装していない私の怠慢)。
では、fraction型を含むクエリは処理できないのか?というと、そんなことはなくて、ユーザ定義型の外部出力形式、つまりTEXT型としてマッピングすれば良いわけです。
$ pg2arrow testdb -c "SELECT ('1/2'::fraction + '3/4'::fraction)::text" -o /tmp/udt.dat
$
fraction型をTEXT型でキャストした結果をもとに生成するので、type={Utf8}
のデータとしてApache Arrowファイルを生成します。。
$ pg2arrow --dump /tmp/udt.dat
[Footer]
{Footer: version=3, schema={Schema: endianness=little, fields=[{Field: name=text, nullable=true, type={Utf8}, children=[], custom_metadata=[]}], custom_metadata []}, dictionaries=[], recordBatches=[{Block: offset=112, metaDataLength=152 bodyLength=128}]}
[Record Batch 0]
{Message: version=3, body={RecordBatch: length=1, nodes=[{FieldNode: length=1, null_count=0}], buffers=[{Buffer: offset=0, length=0}, {Buffer: offset=0, length=64}, {Buffer: offset=64, length=64}]}, bodyLength=128}
$
データファイルのサイズ
これはpg2arrowそのものというよりは、Apache Arrow形式のファイルの話になるが、列形式になったことでファイルサイズはどうなるのか確認してみました。
|データの形式|サイズ(byte)|備考|
|:--|--:|:--||
|PostgreSQLのリレーション|1,342,955,520|pg_relation_size()の結果|
|TSV形式|978,088,897|COPY TOで出力されたファイルサイズ|
|Apache Arrow形式|960,002,518|pg2arrowで出力されたファイルサイズ|
TSV形式でもApache Arrow形式でもそれほどサイズの差はないです。pg2arrowで生成したApache Arrow形式のファイルではデータは圧縮されていません。これは最初に上げていた須藤さんのブログ内にあった、以下の記述
Apache Arrowはシリアライズ・デシリアライズコストを低くすることを重視しているので、サイズを小さくすることに関してはそれほどがんばれません。たとえば、Zstandardなどで圧縮するとApache Arrowのメリットの一部が薄れてしまいます。そのメリットとは「ゼロコピー」です。
という話と関連しているんだと思います。
おわりに
PostgreSQLの検索結果から、Apache Arrow形式のファイルを作成するユーティリティpg2arrow
を試してみました。
pg2arrow
で作成されたファイルは、もともとはpg-stromのarrow_fdwのデータTソースとして使うのが主目的なんだと思いますが、今自分が使える環境では、pg-stromはビルドできないので、残念ながら今のところ、Apache Arrow形式のファイルに対する検索の検証はすぐにはできません。
ただ、Apache Arrow形式ファイルに対応した他のデータ検索エンジンなどでもこのファイルは使えるはずなので、手軽に試せるものがあれば、そういう検索エンジンを使って、PostgreSQL→Apache Arrow形式ファイル→他の検索エンジン、みたいなことも試してみたいです。
-
すごく簡単に書くと、SQLワークロードをGPU上で非同期かつ並列に実行するPostgreSQLの拡張機能です。用途としては複雑なSQLを処理するOLAP系など。新バージョンでは、巨大な列形式ファイルへの高速な並列実行なども想定されています。 ↩
-
arrow_fdw単は体のパッケージとして提供されているわけではなく、pg-stromパッケージの一部として提供されています。詳細はこのページ(Dive into Apache Arrow(その1))が参考になると思います。 ↩
-
今回は開発中のバージョン(PostgreSQL 12-devel)をインストールした環境で確認したが、おそらくユーティリティの性質上、PostgreSQL 11, 10, 9.6等の少し古いPostgreSQLバージョンでもビルド&実行はできると思います。 ↩