pg2arrowを使ってApache Arrowファイルを作成してみる 

# はじめに

今回は[pg-strom](https://heterodb.github.io/pg-strom/ja/)[^1]の開発でお馴染み、海外さん([@kkaigai](https://twitter.com/kkaigai)) さんが作成したツール[pg2arrow](https://github.com/heterodb/pg2arrow)を久々に試してみました。

実は、以前(1月頃)、開発途中版を少し使ってみたことはあったが、自身の入院やら期末の仕事やら期首の仕事やらに悩殺されていて、しばらく動かしてなかった。そして、arrow_fdw[^2]とpg2arrowの正式版（でいいのかな）が4月後半にリリースされたので、再び試してみることにしました。

# Apache Arrowとは

Apache arrowというのは最近検討されている列指向のファイルフォーマットです。
Apache Arrowの特徴や、どういった目的で作られたものか、といった情報は、Apache Arrowの詳細は[公式ページ](https://arrow.apache.org/)や、[クリアコード社の須藤さんのブログ ](https://www.clear-code.com/blog/2018/9/5.html)（Apache Arrowの最新情報（2018年9月版））などを参考にすると良いと思います。

# pg2arrowとは

pg2arrowとは、ごく簡単に言えば、PostgreSQLの検索結果を元に、Apache arrow形式のファイルを生成する、というユーティリティです。
また、デバッグ用/確認用として、--dumpオプションを付与することで、Apache Arrow形式のファイルのメタ情報を可視化することができます。

# pg2arrowを使ってみる

ビルド方法や使い方などは、[github上のWiki](https://github.com/heterodb/pg2arrow/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ファイルを生成する例を示します。

```bash
$ pg2arrow testdb -c "SELECT * FROM pgbench_accounts" -o /tmp/accounts.dat
$ 
```

pg2arrowも標準のPostgreSQLユーティリティ(createdbとかみたいに)っぽく、成功すると静かに終わります。
大きいサイズの結果を生成するときに、「頑張って動いてるのかな～」というのが気になる場合には、``--progress``オプションをつけて進行状況を表示してみるのも良いでしょう。

```bash
$ 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](https://github.com/heterodb/pg2arrow/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件になるクエリだとどうなるか見てみる。

```bash
$ 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は動作するのか。やってみました。

```bash
$ psql postgres -c 'SELECT FROM (SELECT generate_series(1,5)) t'
--
(5 rows)

$
```
列のない結果をもつ5行の結果を返すクエリをpg2arrowに与えてみます。

```bash
$ 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](https://github.com/orafce/orafce)(Oracle’s compatibility functions and packages)が提供している、Oracle互換型のようなものがあります。

自分は変なユーザ定義型を作るのが好きなので、[pg_fraction](https://github.com/nuko-yokohama/pg_fraction)(分数の演算を行えるデータ型)なんかを作っています。じゃあ、そのユーザ定義型を使ったクエリをpg2arrowに与えたらどうなるのかやってみました。

pg_fractionの実行例。

```bash
$ psql testdb -c "SELECT '1/2'::fraction + '3/4::fraction'"
 ?column?
----------
 5/4
(1 row)
```

で、同じクエリをpg2arrowにかけてみると・・・

```bash
$ 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型としてマッピングすれば良いわけです。

```bash
$ pg2arrow testdb -c "SELECT ('1/2'::fraction + '3/4'::fraction)::text" -o /tmp/udt.dat
$ 
```

fraction型をTEXT型でキャストした結果をもとに生成するので、``type={Utf8}``のデータとしてApache Arrowファイルを生成します。。

```bash
$ 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形式のファイルではデータは圧縮されていません。これは最初に上げていた[須藤さんのブログ](https://www.clear-code.com/blog/2018/9/5.html)内にあった、以下の記述

> Apache Arrowはシリアライズ・デシリアライズコストを低くすることを重視しているので、サイズを小さくすることに関してはそれほどがんばれません。たとえば、Zstandardなどで圧縮するとApache Arrowのメリットの一部が薄れてしまいます。そのメリットとは「ゼロコピー」です。

という話と関連しているんだと思います。

#おわりに
PostgreSQLの検索結果から、Apache Arrow形式のファイルを作成するユーティリティ``pg2arrow``を試してみました。
``pg2arrow``で作成されたファイルは、もともとはpg-stromのarrow_fdwのデータTソースとして使うのが主目的なんだと思いますが、今自分が使える環境では、pg-stromはビルドできないので、残念ながら今のところ、Apache Arrow形式のファイルに対する検索の検証はすぐにはできません。
ただ、Apache Arrow形式ファイルに対応した他のデータ検索エンジンなどでもこのファイルは使えるはずなので、手軽に試せるものがあれば、そういう検索エンジンを使って、PostgreSQL→Apache Arrow形式ファイル→他の検索エンジン、みたいなことも試してみたいです。

[^1]: すごく簡単に書くと、SQLワークロードをGPU上で非同期かつ並列に実行するPostgreSQLの拡張機能です。用途としては複雑なSQLを処理するOLAP系など。新バージョンでは、巨大な列形式ファイルへの高速な並列実行なども想定されています。
[^2]: arrow_fdw単は体のパッケージとして提供されているわけではなく、pg-stromパッケージの一部として提供されています。詳細はこのページ（[Dive into Apache Arrow（その１）](http://kaigai.hatenablog.com/entry/2019/01/14/144040)）が参考になると思います。
[^3]: 今回は開発中のバージョン(PostgreSQL 12-devel)をインストールした環境で確認したが、おそらくユーティリティの性質上、PostgreSQL 11, 10, 9.6等の少し古いPostgreSQLバージョンでもビルド＆実行はできると思います。