読者です 読者をやめる 読者になる 読者になる

TOASTメカニズム

OSS/Linux

PostgreSQLで可変長データ型を扱う時、内部的にはTOASTと呼ばれる機構を利用して、別の隠しテーブルに可変長のデータを格納するようになっている。この時、可変長のデータは適正な長さに分割されて格納されるので、タプル一個あたりのデータ長がブロックサイズを超える事はない。

この辺の処理を見てみたので、後々のためのメモ。本当は LargeObject の格納に使ったり、巨大な Bytea データの一部分を取り出すような関数を実装するために使えないかと思ってみたり。

TOASTテーブルの構造

テーブルを定義すると、こっそりとそのテーブル専用のTOASTテーブルというものが作成される。SQL的に記述すると以下のような構造を持っており、利用者は直接アクセスできない。

CREATE TABLE pg_toast_<relid> (
    chunk_id        oid,
    chunk_seq       int4,
    chunk_data      bytea,
    PRIMARY KEY(chunk_id, chunk_seq)
)

可変長の値の入力

heap_insert()heap_update()で新しいタプルを挿入/更新するとき、可変長のデータ(列のattlenが負)でかつタプルのデータ長が2Kバイト(TOAST_TUPLE_TARGET)を越える時、この可変長データは別のテーブルに格納され、元のタプルには外部テーブルへのポインタに相当する値が書き込まれる。

TextやByteaなどの可変長データは、内部的にはvarlenaという構造体に格納される。ぱっと見、頭の4Byteがデータ長で、後ろにデータが詰まっているデータ構造に見える。

struct varlena
{
    char        vl_len_[4];
    char        vl_dat[1];
};
      :
typedef struct varlena bytea;
typedef struct varlena text;

しかし、varlena構造体の先頭1byteには特殊な意味を持っており、varlena構造体は多様性を持つ。

xxxxxx00
非圧縮、可変長データ(最大1G)
xxxxxx10
圧縮済み、可変長データ(最大1G)
00000001
TOASTポインタ
xxxxxxx1
非圧縮、可変長データ(最大126byte)

で、入力された可変長データが大きすぎる場合、これをTOASTポインタに変換するという作業が発生する。

TOASTポインタへの変換

TOASTポインタへの変換を行うのはtoast_save_datum()関数。
ここまで来たらやる事は簡単で、

  1. ユーザから不可視なTOASTテーブルをオープン
  2. ユニークなoidを一つ割り当てる
  3. 固定長のブロック毎に、TOASTテーブルにデータを挿入する。この時、chunk_seqは0から順にインクリメントされる
  4. 書くべきデータを全て書き終わるまで繰り返し
  5. TOASTテーブルをクローズ

TOASTポインタの実体はvaratt_external構造体で、以下のように定義されている。

struct varatt_external
{
    int32       va_rawsize;     /* Original data size (includes header) */
    int32       va_extsize;     /* External saved size (doesn't) */
    Oid         va_valueid;     /* Unique ID of value within TOAST table */
    Oid         va_toastrelid;  /* RelID of TOAST table containing it */
};

実データの代わりに、こいつを元々の対象テーブルに書き込むわけだ。

TOASTデータの読み出し

TOASTに保存されたデータを元通りに戻すには、これと逆の操作を行ってやればよい。それを実行するのがtoast_fetch_datum()関数である。
TOASTポインタを参照すれば、TOASTテーブルのoid、chunk_idが分かるので、あとはTOASTテーブルをオープンしてブロックを順に読み出し、それ以外の形式のvarlenaデータに展開してやればよい。

狙い

なぜTOASTの扱いを調べていたかというと、狙いはこれをLargeObjectsの格納に応用できないかという点。

pg_largeobjectシステムカタログは以下の構造を持っているが、これはTOASTテーブルと定義が全く同じなのである。

CREATE TABLE pg_largeobject (
    loid        oid,
    pageno      int4,
    data        bytea,
    PRIMARY KEY (loid, pageno)
);

LargeObjectsのアクセス制御を考えると、1個のオブジェクトが複数のタプルで構成されているというのが非常に扱いにくい(無論、可能だが)。
なので、必然的に1個のメタデータ+複数のページフレームという形にならざるを得ないが、そうすると、ページフレームの部分が利用者に可視である意味はあるのか?という考えに至らざるを得ない。

ラージオブジェクトがTOASTに格納されたByteaデータであれば、利用者にとって個々のページフレームは不可視であり、メタデータを保持するタプルに格納したセキュリティ属性によってのみアクセス制御を行えばよいという、非常にシンプルな形となる。

また、これを応用して、一般レコードの可変長データの一部分だけを取り出すというようなインターフェースも作ることができるようになるだろう。
そうすると、長大なデータであっても普通にレコードの中に埋めて、必要な部分だけ取り出すというような使い方が可能となり、いずれ、現在のLargeObjectは歴史的な役割を終えるという事になるかもしれない。

上の説明からも分かるように、現状では、長大なデータを挿入する際には、それを一度オンメモリで保持し、順にINSERT/DELETEするという非効率な構造になっているので、そもそも長大なデータをテーブル構造に埋め込んで使うという事がどだい無理なのである。