PostgreSQL の基本データ型とタプルの扱い

作成日:2016.09.18
修正日:2017.04.01

この記事は PostgreSQL 9.4 と 9.5 に基づいて記述している。

このページでは PostgreSQL をハックしたりエクステンション(extension)を開発する人向けに、PostgreSQL の扱っているデータ型の内部とタプルの内部構造について紹介する。

PostgreSQL の他の記事へのインデックスはここ


更新履歴
(2016.09.18) 作成。
(2017.04.01) attisdropped の情報を追記。


目次

1. はじめに

PostgreSQL の基本的な構造については鈴木 啓修の『PostgreSQL全機能バイブル』などに解説されているが、実際に PostgreSQL をハックしたりエクステンション(extension)を開発する場合には情報が不足している。 この文章では PostgreSQL の外側に見えるデータ型や行(タプル、レコード)が PostgreSQL の内部でどのように扱われているかを解説する。

その他の解説が PostgreSQL の覚え書きのインデックス からたどれる。

2. 基本的なデータ型

PostgreSQL にはたくさんのデータ型があり([1])、ユーザー定義のデータ型を追加することもできる。 これらはデータベース内の pg_type システムカタログ([2])に登録されている。 具体的には int8、int16、int32、int64、float4、float8 などの現代の CPU に直接該当する基本的なデータ型や、numeric、decimal、text などの複雑なデータ型である。

PostgreSQL のプログラムの中ではこれらのデータ型を Datum という基底型にまとめて格納することができる。 Datum は Java の Object 型をイメージすればよい。 Datum の実体は void * で、ポインターとポインターと同じビット幅のデータを記録できる。 そのため 32 ビット環境では 4 バイト、64 ビット環境では 8 バイトになる。 int16 のような Datum よりも小さなサイズのデータ構造は boxing されて格納される。 Datum よりも大きなデータ型は外部のメモリ空間に確保して、そのポインターを Datum に記録する。

Datum に格納することができるデータを分類すると tab-1 のようになる。

tab-1: Datum の分類
分類ByValLength本当のデータサイズ
基本型int32、float4true実際のバイト数。int16 なら 2、int32 や float4 なら 4。Lengthに一致
ポインター(固定長)name、(32ビット環境では) int64、float8falseポインタ先のバイト数。int64 や float8 なら 8。Lengthに一致
varlenatext、varchar、numeric、decimal、arrayfalse-1varlena のサイズ
cstringcstring、unknown の 2 種類のみfalse-2strlen(val) + 1
internalinternal のみtruesizeof(Datum)不明

データ型の情報は pg_type システムカタログに格納されている。 pg_type システムカタログ内の typlentypbyval は特に重要なパラメータである。 tab-1 の Length と ByVal に転写している。

ByVal が true のデータ型は Datum に全データを直接格納できるデータ型である。 ByVal が false の場合はポインターだけを記録する。

Length はデータ型のデータサイズ(バイト数)を計算するために必要なデータを記録している。

PostgreSQL はビルドされた環境により sizeof(Datum) が変わってくるが、int64 型や float8 型は常に 64 ビット(8 バイト) である。 そのため int64 型や float8 型は 64 ビット環境では ByVal が true となり Datum にそのまま入るが、32 ビット環境では外部メモリに配置しポインタで格納することになる。

varlena 型は PostgreSQL の基本的な可変長データ構造で、先頭に格納バイト数が格納されたデータ構造になる。 PostgreSQL は長過ぎるデータに対してインライン圧縮や TOAST 化を行うが、varlena のヘッダーにはこれらの情報も記録される。

internal 型は pg_type システムカタログ内で OID が 2281 番のデータ型になる。 これは特定のデータ型を意味しない。 PostgreSQL の内部で利用されるデータ構造の代表値として使われている。 プログラム内部の C 言語の構造体のポインターがそのまま Datum に格納される時に internal 型となる。 internal 型が実際にどの構造体を使っているかは外側から判定することができない。

cstring 型と internal 型はテーブルの定義には使えない。

OID が何なのかはPostgreSQL のテーブルとブロックのデータ構造1.2節を参照のこと。

ビルド時に configure のオプションに --enable-float8-byval を渡すと、32 ビット環境でも Datum を 64 ビットにすることはできる。 逆に --disable-float8-byval を渡すと、64 ビット環境でも Datum が 32 ビットになる。

2.1 Datum の操作

PostgreSQL は Datum から特定のデータ型への変換や、特定のデータ型から Datum への変換するマクロが定義されている。 プログラムからはこのマクロを利用する。 マクロの大部分は postgres.h にあるが、それ以外に utils/date.h や utils/timestamp.h などに定義されているマクロもある。

#define DatumGetBool(X) ((bool) (GET_1_BYTE(X) != 0))
#define DatumGetChar(X) ((char) GET_1_BYTE(X))
#define DatumGetUInt8(X) ((uint8) GET_1_BYTE(X))
#define DatumGetInt16(X) ((int16) GET_2_BYTES(X))
#define DatumGetUInt16(X) ((uint16) GET_2_BYTES(X))
#define DatumGetInt32(X) ((int32) GET_4_BYTES(X))
#define DatumGetUInt32(X) ((uint32) GET_4_BYTES(X))
#define DatumGetObjectId(X) ((Oid) GET_4_BYTES(X))
#define DatumGetTransactionId(X) ((TransactionId) GET_4_BYTES(X))
#define DatumGetPointer(X) ((Pointer) (X))
#define DatumGetCString(X) ((char *) DatumGetPointer(X))

#define BoolGetDatum(X) ((Datum) SET_1_BYTE(X))
#define CharGetDatum(X) ((Datum) SET_1_BYTE(X))
#define Int8GetDatum(X) ((Datum) SET_1_BYTE(X))
#define UInt8GetDatum(X) ((Datum) SET_1_BYTE(X))
#define Int16GetDatum(X) ((Datum) SET_2_BYTES(X))
#define UInt16GetDatum(X) ((Datum) SET_2_BYTES(X))
#define Int32GetDatum(X) ((Datum) SET_4_BYTES(X))
#define UInt32GetDatum(X) ((Datum) SET_4_BYTES(X))
#define ObjectIdGetDatum(X) ((Datum) SET_4_BYTES(X))
#define TransactionIdGetDatum(X) ((Datum) SET_4_BYTES((X)))
#define PointerGetDatum(X) ((Datum) (X))
#define CStringGetDatum(X) PointerGetDatum(X)

2.2 データ長の取得

Datum からそのデータの本当のデータ長(バイト数)を取得する関数として datumGetSize() が定義されている。 この関数は Datum 値に加えて、格納されているデータ型の typbyval と typlen を第 2・第 3 引数に必要としている。

Size datumGetSize(Datum value, bool typByVal, int typLen);

ただし internal 型に対しては datumGetSize() は sizeof(Datum) を返す。

2.3 コピー操作

Datum をコピーすると関数として datumCopy() が定義されている。 Datum 値を渡すと deep copy されたデータが Datum 型になって戻ってくる。 この関数は Datum 値に加えて、格納されているデータ型の typbyval と typlen を第 2・第 3 引数に必要としている。

Datum datumCopy(Datum value, bool typByVal, int typLen);

ByVal が false の場合、データのコピーのために新しいメモリ領域の確保されるが、それは現在のメモリコンテキストから確保される。 そのため異なるメモリコンテキスト間でデータを受けた渡す場合は、以下のようにしてコピーを実行することが多い。

Datum newValue;
MemoryContext oldcontext;

oldcontext = MemoryContextSwitch(theMemoryContext);
newValue = datumCopy(oldValue, typByVal, typLen);
MemoryContextSwitch(oldcontext);

ただし internal 型に対しては datumCopy() は shallow copy となる。

2.4 入出力変換操作

PostgreSQL の全てのデータ型はテキストへ出力するための output 関数 と、テキストからデータ型へ変換するための input 関数 が定義されている。 これは pg_type システムカタログの typinputtypoutput 列に定義されている。 SQL の問い合わせの出力結果は output 関数を使って変換している。

一部のデータ型はバイナリ列へ変換するための send 関数 と、バイナリ列からデータ型へ復元するための receive 関数 が定義されている。 send して receive した結果は意味的に同一になる必要がある。

input/output 関数は全データ型の必須だが、send/receive 関数は省略してもよい。

internal 型も input/output 関数を用意しているが、実際に呼ばれるとエラーが出る。 internal 型に send/receive 関数は実装されていない。

3. varlena 型

varlena 型は可変長を格納する基本的なデータ構造である。 ビッグ・エンディアン(big endian)とリトル・エンディアン(little endian)でデータフォーマットが微妙に異なるが、ここではリトル・エンディアンのみを説明する。

tab-2: varlena の分類(リトル・エンディアン)
種類先頭バイトタグヘッダー長格納形式
4B_Uxxxx,xx00 4 バイト長行内の無圧縮データ。
4B_Cxxxx,xx10 4 バイト長行内の圧縮データ。
1Bxxxx,xxx1 1 バイト長行内の無圧縮データ。
1B_E0000,0001INDIRECT(1)1 バイト長行外インメモリのTOAST格納。クエリー処理中のみの形式でストレージ格納には使われない。
1B_E0000,0001ONDISK(18)1 バイト長行外ディスク上へのTOAST格納。圧縮と非圧縮の両方がありえる。

varlena 型にはまずヘッダーが 1 バイトのものと 4 バイトのものが存在する。 ヘッダーが 1 バイトの varlena はヘッダーも含めて 1 〜 128 バイトのサイズをとることができる。 ヘッダーが 4 バイトの varlena はヘッダーも含めて 5 〜 1G バイトのサイズをとることができる。 ヘッダーが 1 バイトか 4 バイトかは、varlena の先頭の 1 バイトの最下位ビットを見れば判定できる。 1 バイトヘッダーの場合は必ず 1 に、4 バイトヘッダーは必ず 0 になる。

1 バイトヘッダーの場合、最下位ビット以外がオール 0 の場合とそれ以外に分かれる。

4 バイトヘッダーの場合、varlena は先頭 4 バイトがヘッダーになる(最初に判定のために使った 1 バイトもヘッダーの 4 バイトに含める)。 さらに最下位ビットの次のビットが 0 か 1 かで 2 種類に分かれる。 最下位ビットの次のビットが 0 の場合は 4B_U 形式で、1 の場合は 4B_C 形式である。 4B_U の場合は varlena のデータ部分は圧縮されておらず、4B_C の場合は圧縮されている。 varlena は 4 バイト(32 ビット)のうち下位 2 バイトを除いた 30 ビットがヘッダーを含めた varlena のサイズを示す(4B_C の場合は圧縮状態のサイズ)。 ただしヘッダーがすでに 4 バイトを消費し、実データは最低 1 バイト以上なので、最小は 5 バイトになる。 最大は 1G になる。

varlena のデータ構造を図示すると fig-1 となる。

fig-1: varlena のデータ構造(リトル・エンディアン)
varlena のデータ構造

リトル・エンディアンの場合もビッグ・エンディアンの場合も varlena の最初の 1 バイトを判定すれば、1 バイトヘッダーか 4 バイトヘッダーかを識別できるように構成されている。 そのためにリトル・エンディアンでは最下位の 2 ビットを使って形式を判定していたが、ビッグ・エンディアンでは最上位の 2 ビットを使う。

これは以下のような理由による。 例えば 4 バイト値 0x12345678 があった場合、リトル・エンディアンは 0x78 → 0x56 → 0x34 → 0x12 と並ぶが、ビッグ・エンディアンは 0x12 → 0x34 → 0x56 → 0x78 となる。 つまりリトル・エンディアンは最下位のバイトが先頭 1 バイトにきて、ビッグ・エンディアンでは最上位バイトが先頭 1 バイトにくる。

PostgreSQL のテーブルは ALTER TABLE コマンド の SET STORAGE を使うと列の保管モードを変更することができる。 ただし列の保管モードの varlena の形式が一体一に対応するのではない。 varlena 型のサイズによって、あるいは varlena 型を含めた行のサイズによって、複数の形式をとりえる。 この列の保管モードによって varlena 型のどの形式が利用されるかを tab-3 に示す。 ○がついている箇所は選択される可能性がある。 ついていない箇所の形式は決して選択されない。

tab-3: varlena の形式と列の保管モードの関係
種類PLAINEXTENDEDMAINEXTERNAL
4B_U
4B_C   
1B
1B_E (ONDISK) ○(圧縮)○(圧縮)○(非圧縮)
1B_E (INDIRECT)ストレージの保管形式として選択されることはない。

varlena 型を利用するための多数のマクロが用意されている。 以下のマクロをよく使う。

4. Heap Tuple と Minimal Tuple

PostgreSQL ではテーブル内に INSERT/UPDATE/DELETE で格納する行(row)のことをタプル(tuple)と呼ぶ。

Heap tuple も Minimal tuple も連続したバイト列のデータである。 ただし TOAST などによって別テーブルに格納されたデータが埋め込まれていることはある。 Heap tuple と minimal tuple の違いは、heap tuple にはトランザクション情報などがヘッダーに埋め込まれているが、minimal tuple はそれらが省略されておりメモリを節約している点である。

Heap tuple も minimal tuple も自身がどのようなフォーマットなのかを示す情報を自身の中には含んでいない(heap tuple/minimal tuple 中にNULL が含まれているかどうかと属性数は保持している4.1.1節)。 フォーマットは Tuple Descriptor 5章 で保持するので、heap tuple/minimal tuple を生成・アクセスする場合は必ず Tuple Descriptor が必要になる。

4.1 Heap Tuple

Heap tuple は HeapTupleData 構造体で示されるヘッダーがある。 その先頭へのポインターは HeapTuple 型となる。 HeapTupleData 構造体の先頭の 4 バイトが t_len でありヘッダーを含めた heap tuple 全体のサイズが入っている。 ヘッダーの続く部分には Xmin/Xmax などのトランザクションに関する情報、NULL のビットマップが入っている。 その後は属性(カラム)データをあらわすバイト列が入っている。

tab-4: HeapTupleData 構造体の定義
メンバー名サブメンバー名データ型バイト数説明
t_lenuint324HeapTuple の全体のバイト数。
t_selfItemPointerData6 (2バイト境界)この heap tuple のテーブル内の位置
 ip_blkidBlockIdData4 (2バイト境界)DB ブロック番号
ip_posidOffsetNumber2DB ブロック内のオフセット番号
t_tableOidOid4この HeapTuple を含むテーブル(relation)の Oid。行(タプル)の Oid ではない。
t_dataHeapTupleHeader heap tuple の詳細情報のヘッダー部分。
 t_choice共用体12HeapTupleFields か DatumTupleFiels のいずれかが入る共用体
t_ctidItemPointerData6(2バイト境界)この行(タプル)のテーブル内の位置。初期は t_self と同じだが、UPDATE が実行された場合には新しい行(タプル)を指すように更新される。
t_infomask2uint162フラグ領域2
t_infomaskuint162フラグ領域
t_hoffuint162heap tuple の最初の属性の開始位置を示すオフセット。
t_bits[1]bits81NULL ビットマップの最初の 1 バイト。

t_data.t_choice の共用体部分はさらに以下のような構造体に入っている。

tab-5: HeapTupleFiels 構造体の定義
メンバー名サブメンバー名データ型バイト数説明
t_xminTransactionId4挿入された XID。
t_xmaxTransactionId4削除またはロックされた XID。
t_field3共用体4 
 t_cidCommandId4Combo Command Id(CCI)。CCI の何なのかは「PostgreSQL のトランザクション & MVCC & スナップショットの仕組み」の3.3 節3.5節 を参照。
t_xvacTransactionId4古いスタイルの VACUUM FULL 用 XID。
tab-6: DatumTupleFiels 構造体の定義
メンバー名データ型バイト数説明
datum_lenint324現在は未使用のメンバー変数。
datum_typmodint324-1 またはレコードタイプ固有の識別子。
datum_typeidOid4要素の OID または RECORDOID。

Heap tuple のヘッダーと属性(カラム)データを示したのが fig-2 になる。

fig-2: Heap tuple のデータ構造
Heap tuple のデータ構造

ヘッダー中の t_hoff が最初の属性(カラム)データが入っている。 ヘッダーと最初の属性の隙間は NULL ビットマップとなっている。 Heap tuple の中で NULL となっている属性に 1 が立つ。 NULL となった属性は属性データとして記録されない(スキップされる)。 これは t_bits[] メンバー変数でアクセスできる。

またテーブルの各行に OID をつける設定の時(WITH OIDS)は NULL ビットマップの後に Oid が格納される。 OID をつけない設定の時(WITHOUT OIDS)の場合は Oid の領域はない。

属性データはデータ型に応じて決まったアライメントに従って並べる。 アライメントによるパディングは 0 で埋める。 アライメントはデータ型毎に決まっており pg_type システムカタログtypalign に記録されている。 ただし varlena 型はそれが 1 バイトヘッダー形式か 4 バイトヘッダー形式なのかによってアライメントが異なる。 1 バイトヘッダー形式の場合は 1 バイト境界だが、4 バイトヘッダー形式の場合は 4 バイト境界となる。 Heap tuple をデコードする時は、次の属性が varlena 型なら次のバイトが 0 かどうかをチェックする。 0 ならパディングなので 4 バイトヘッダー形式であると分かる。 非 0 なら 1 バイトヘッダー形式である。

PostgreSQL のメモリ確保は 8 バイト境界に沿っており、HeapTuple の先頭位置が 8 バイト境界に沿っている。 また t_hoff も 8 バイト境界にそろえるので、属性データの先頭位置も 8 バイト境界にしたがっている。 属性データの末尾も 8 バイト境界にそろえてパディングされる。

4.1.1 t_infomask、t_infomask2

HeapTupleData 構造体の t_infomaskt_infomask2 はそれぞれ 16 ビットの領域だが、この 2 つは heap tuple のフラグを記憶している。

t_infomask のフラグの意味は tab-7 に、t_infomask2 のビットの意味またはフラグの意味は tab-8 に記した。

tab-7 と tab-8 の「ヒント」の列は、そのビットがヒントビットであることを意味している。 ヒントビットであるとは、t_infomaskt_infomask2 以外の別のパラメータによって決まる状態があり、いったんその状態であると判断されたことをヒントとして記録しておくためのビットである。 そのようなヒントビットが 1 である場合、「その状態である」と言える。 しかしヒントビットが 0 の場合は、「その状態でない」ことを意味しない。 別のパラメータによる判定処理を行い状態を確認することができる。

tab-7: t_infomask のフラグ
マクロ数値ヒント説明
HEAP_HASNULL0x0001Noheap tuple の中に NULL が含まれている場合は 1 が立つ。0 なら NULL の属性はない。
HEAP_HASVARWIDTH0x0002Noheap tuple の中に tab-1 の varlena または cstring が含まれている場合に 1 が立つ。そうでないなら 0。
HEAP_HASEXTERNAL0x0004Noheap tuple の中に外部 TOAST に他する参照が含まれている場合に 1 が立つ。そうでないなら 0。
HEAP_HASOID0x0008Noheap tuple に行としての Oid が付く場合に 1 が立つ。Oid が付かない場合には 0。
HEAP_XMAX_KEYSHR_LOCK0x0010Not_xmax を key-shared locker として使っている場合に 1 が立つ。そうでないなら 0。
HEAP_COMBOCID0x0020Not_field3.t_cid が Combo CID の場合は 1 が立つ。そうでないなら 0。
HEAP_XMAX_EXCL_LOCK0x0040Not_xmax を exclusive locker として使っている場合に 1 が立つ。そうでないなら 0。
HEAP_XMAX_LOCK_ONLY0x0080Not_xmax を only locker として使っている場合に 1 が立つ。そうでないなら 0。
HEAP_XMIN_COMMITTED0x0100Yest_xmin の値が CLOG 上でコミットしている場合、そのヒントビットとして 1 を立てる。
HEAP_XMIN_INVALID0x0200Yest_xmin の値が CLOG 上でアボートしている場合、そのヒントビットとして 1 を立てる。
HEAP_XMIN_FROZEN0x0200Yest_xmin が VACUUM によって凍結された時に 1 を立てる。
HEAP_XMAX_COMMITTED0x0400Yest_xmax の値が CLOG 上でコミットしている場合、そのヒントビットとして 1 を立てる。
HEAP_XMAX_INVALID0x0800Yest_xmax の値が CLOG 上でアボートしている場合や t_xmax をロックのために使ったがそのロックがすでに解除された場合、そのヒントビットとして 1 を立てる。
HEAP_XMAX_IS_MULTI0x1000Not_xmax が MultiXaxtId として使われているなら 1 が立つ。そうでないなら 0。
HEAP_UPDATE0x2000Noこの heap tuple が UPDATE によって更新された行の更新後の heap tuple なら 1 が立つ。そうでないなら 0。
HEAP_MOVED_OFF0x4000NoPostgreSQL 9.0 では使わない。
HEAP_MOVED_OFF0x8000NoPostgreSQL 9.0 では使わない。
tab-8: t_infomask2 のフラグ
マクロ数値ヒント説明
HEAP_NATTS_MASK0x07FFNo t_infomask2 の下位 11 ビットは 0 〜 2047 までの数値を埋め込み、この heap tuple の属性数を記憶している。これを HeapTupleHeaderGetNatts() で取得することができる。heap tuple 毎の属性数を持つことで、ALTER TABLE ADD/DROP COLUMN によってテーブルの列数(属性数)が変更された場合に対応可能になっている。
HEAP_KEYS_UPDATE0x2000No 
HEAP_HOT_UPDATE0x4000No 
HEAP_ONLY_TUPLE0x4000Noこの heap tuple が UPDATE によって挿入された heap tupleで、かつインデックスのキーとなる列に更新がない場合に 1 が立つ。つまりインデックスが挿入されない heap tuple に対して 1 が立つ。そうでないなら 0。

4.2 Minimal Tuple

Minimal tuple はプランノード間のデータの受け渡しなどに使われるデータ形式である。 Heap tuple とほぼ同等だが、ヘッダー部分にある MinimalTupleData 構造体は HeapTupleData 構造体と異なりトランザクション情報等が省略されている。 その分、メモリ量が少し小さい。

Minimal tuple は heap tuple のサブセットとなっており、ヘッダー構造体の意味などは heap tuple を参照すること。

tab-9: MinimalTupleData 構造体の定義
メンバー名データ型バイト数説明
t_lenuint324Minimal tuple の全体のバイト数。
mt_paddingchar[4]4次の t_infomask2 を 8 バイト境界にそろえるためのパディング
t_infomask2uint162フラグ領域2
t_infomaskuint162フラグ領域
t_hoffuint162Minimal tuple の最初の属性の開始位置を示すオフセット。
t_bits[1]bits81NULL ビットマップの最初の 1 バイト。

Minimal tuple のヘッダーと属性(カラム)データを示したのが fig-3 になる。 配置ルールなども heap tuple と同じである。

fig-3: MinimalTuple のデータ構造
MinimalTuple のデータ構造

5. Tuple Descriptor

Heap tuple も minimal tuple もタプル内の属性の名前、データ型などのメタ情報を、自分の中には保持していない。

PostgreSQL のテーブルの情報の基本部分は pg_class システムカタログに記載されているが、テーブルの列情報は pg_class システムカタログではなく pg_attribute システムカタログに記載されている。 しかも pg_attribute システムカタログは 1 つの列の情報が pg_attribute システムカタログの 1 つの行に格納されている。

PostgreSQL は SQL クエリーから実行プランを作成した時、pg_attribute システムカタログの情報を参照する。 プランツリーの各プランノードはターゲットリスト(Target List)を持つ((「PostgreSQL プラン・ツリーの概要」の3.5 節)。 ターゲットリストは下位のプランノードが上位のプランノードに返す「タプル」の処理方法を記述していると共に、タプルの属性数、各属性の名前、各属性のデータ型を保持したメタ情報でもある。

クエリーの実行の開始前のExecturoStart フェーズで plan tree を plan state tree へ変換する際に、ターゲットリストはその中のメタ情報だけをアクセスし易い Tuple Descriptor というデータ構造を構築する。 Tuple Descriptor は tupleDesc 構造体によって管理される。

tab-10: tupleDesc 構造体の定義
メンバー名データ型バイト数説明
nattsint4属性数。
attsForm_pg_attribute[]sizeof(void*)各属性の情報。pg_attribute システムカタログの情報を引き生成される。
constrTupleConstr[]sizeof(void*)制約。
tdtypeidOid4Tuple descriptor に相当するタイプが pg_type システムカタログ上にあればその Oid。
tdtypmodint324Tuple desciptor のタイプ修飾。-1 なら無効。
tdrefcountint4Tuple descritpor を参照者のリファレンスカウンター。-1 ならカウントしない。

ExecTypeFromTL() を使うとターゲットリストから Tuple Descriptor を構築することができる。

6. TupleTableSlot

Heap tuple や minimal tuple はコンパクトな形式だが、任意の属性にアクセスすることができない。 そこでクエリー実行時には heap tuple や minimal tuple を展開して参照できるようにした TupleTableSlot を介してアクセスする。 TupleTableSlot は TTS と略されることが多い。

TupleTableSlot には筆者が Heap Tuple モードMinimal Tuple モードVirtual Tuple モード と名付けた 3 つのモードが存在する。 ただしソースコード中にそのような単語が存在しているわけではない。

tab-11: TupleTableSlot 構造体の定義
メンバー名データ型説明
typeNodeTagノード種類を示すタグ。T_TupleTableSlot を入れる。ノード種類の詳細については「PostgreSQL プラン・ツリーの概要」の3.1 節を参照のこと。
tts_isemptyboolTupleTableSlot が空でデータを持っていない状態を示す。これは初期化後や ExecClearTuple() 後に true となり、TubleTableSlot にデータを格納する関数を実行すると false になる。
tts_shouldFreeboolHeap Tuple モードを解除する関数を呼び出すタイミングで tts_tuple の先にある HeapTuple を heap_freetuple() で解放する場合には true を指定する。false ならケアしない。
tts_shouldFreeMinboolMinimal Tuple モードを解除する関数を呼び出すタイミングで tts_mintuple の先にある HeapTuple を heap_free_minimal_tuple() で解放する場合には true を指定する。false ならケアしない。
tts_slowboolslot_deform_tuple() を呼び出した際に、slot_deform_tuple() が内部処理に利用する。
tts_tupleHeapTupleHeap Tuple モードで HeapTuple を指すポインター。
tts_tupleDescriptorTupleDescこの TupleTableSlot の形式を記録する TupleDesc へのポインター。
tts_mcxtMemoryContextこの TupleTableSlot を介した処理で新しいメモリを確保する際に使うメモリコンテキストへのポインター
tts_bufferBufferテーブル(リレーション)のスキャンを行う際に、現在読み込み中のページをピンするために用いる。ExecStoreTuple() で設定される。未使用の場合には InavlidBuffer を入れておく。
tts_nvalidintVirtual Tuple モードで利用する。
tts_valuesDatum *Virtual Tuple モードで利用する。
tts_isnullisnull *Virtual Tuple モードで利用する。
tts_minitupleMinimalTupleMinimal Tuple モードで MinimalTuple を指すポインター。
tts_minhdrHeapTupleDataMinimal Tuple モードで利用する。
tts_offlongslot_deform_tuple() を呼び出した際に、slot_deform_tuple() が内部処理に利用する。

6.1 Heap Tuple モード

与えられた heap tuple からクエリー実行中に属性を取り出したい場合、TupleTableSlot 構造体の tts_tuple の先に heap tuple をつなげる。 プログラムでは TupleTableSlot への heap tuple の設定は ExecStoreTuple() で行う。

例えば Attr0、Attr1、Attr2、Attr3 の 3 つの属性を持つ heap tuple があったとする。 このうち Attr1 は varlena 型で、Attr2 は NULL だとする。 このような heap tuple を TupleTableSlot が保持すると、fig-4 のようになる。

fig-4: Heap Tuple モードの TupleTableSlot(未展開)
Heap Tuple モードの TupleTableSlot(未展開)

この HeapTupleSlot に対して属性を取得するためには、先頭の属性から指定の属性まで展開を行う必要がある。 実際の展開処理は、slot_deform_tuple() で TupleTableSlot の先頭から natts 個分の属性までを展開する。

static void slot_deform_tuple(TupleTableSlot *slot, int natts);

slot_deform_tuple() は heaptuple.c の内部関数なので、実際には slot_deform_tuple() を呼び出している slot_getattr()slot_getallattrs()slot_getsomeattrs() などの外部関数を使う。

仮に 3 番目(Attr2) までの属性まで展開すると fig-5 のようになる。 tts_values[]tts_isnull[] はそれぞれ 4 要素の Datum 配列と bool 配列だったのが、3 番目まで展開される。 3 つめまで展開されたことは tts_nvalid に記録される。

Attr0 は byval が true のデータ型だったので tts_values[0] にコピーされて終わりである。 一方、Attr1 は byval が false のデータ型である。 tts_values[1] は heap tuple の中の varlena データの先頭ポインタを指すように設定される。 Attr2 は NULL だったので tts_isnull[2] が true となる。tts_values[2] にはダミーの値として 0 が入る。

fig-5: Heap Tuple モードの TupleTableSlot(属性2まで展開)
Heap Tuple モードの TupleTableSlot(属性2まで展開)

tts_off には heap tuple の中でどの位置までをデコードしたか記録されている。 もし 4 番目の属性(Attr3)を取得する場合、tts_off からデコードを開始することができる。

TupleTableSlot を使うプログラムは tts_nvalid で有効な属性範囲を確認しながら、tts_values[i]tts_isnull[i] から属性値を参照することができる。

HeapTuple は N 番目の属性をデコードするためには、それ以前の属性をデコードする必要がある。 このためテーブルに列数が多い場合は、先頭の属性を取り出すのと最後の属性を取り出すのでは時間が大きく異なる。

PostgreSQL は ALTER TABLE table_name DROP COLUMN column_name コマンドによって、テーブルの定義から特定の列を落とすことができる。 この時、ディスク内のタプルの中身を書き換えると処理時間が大きくなるので、「列」の定義に中に削除済みのマークを付けてコマンドを完了させる。 削除済みのマークは pg_attribute システムカタログの attisdropped になる。

このため heap tuple を読む際に、運用中に列が削除されていないのかを毎回チェックする必要がある。 具体的には以下の TupleDesc 内の変数を見る。

tupDesc->attrs[i]->attisdropped

slot_deform_tuple() は内部でこの処理を行っている。

6.2 Minimal Tuple モード

Heap tuple 同様に minimal tuple を TableTableSlot へ設定することもできる。 この場合、tts_minitupletts_minhdr を参照する。 プログラムでは TupleTableSlot への minimal tuple の設定は ExecStoreMinimalTuple() で行う。

動作は Heap Tuple モードとほぼ同じなので、詳細は割愛する。

6.3 Virtual Tuple モード

HeapTupleSlot は heap tuple や minimal tuple を作らずに利用することもできる。 この場合、virtual tuple と呼ぶ。

Virtual tuple 時にはプログラムの中で tts_values[]tts_isnull[] を手動で編集してゆく。 Heap tuple モードでは heap tuple の一部の属性だけを展開することができたが、virtual tuple では tuple descriptor に記された全属性分のデータを tts_values[]tts_isnull[] に作る必要がある。

TupleTableSlot 構造体の tts_values[]tts_isnull[] を手動で編集し後は、ExecStoreVirtualTuple() を呼び出し、virtual tuple であることを完成させる。 この TupleTableSlot をクエリー実行のための関数に渡して処理をさせることができる。 ExecStoreVirtualTuple() を呼ばない場合 virtual tuple として完成しておらず、「tts_isempty が true である」というエラーが表示されてアボートする可能性がある。

実際にプログラムを使う流れは以下のようになる。

TupleTableSlot *slot;

/* 前の TupleTableSlot のデータをクリアする */
ExecClearTuple(slot);

/* tts_vaules[] と tts_is_null[] に値を設定 */
slot->tts_values[0] = Item32GetDatum(1);
slot->tts_values[1] = Item32GetDatum(1);

slot->tts_isnull[0] = false;
slot->tts_isnull[1] = false;

/* virtual tuple が構築完了したことを設定 */
ExecStoreVirtualTuple(slot);

6.4 slot_deform_tuple() の最適化

slot_deform_tuple()は TupleTableSlot 構造体にリンクされた heap tuple や minimal tuple を tts_values[]tts_isnull[] に展開する関数である。 しかしこの処理は結構コストが掛るので高速化のための工夫がほどこされている。

slot_deform_tuple(slot, natts) は複数回呼ばれる。 前回までに heap tuple の属性データのうち解析した最後の位置を tts_off に記録しておく。 そのため tts_nvalid よりも大きな natts を指定して slot_deform_tuple(slot, natts) が呼び出された場合は、tts_off の位置から解析の続きを行うことができる。

Heap tuple は可変長サイズか NULL が指定された属性データがあらわれた場合は、それ以降の属性データのオフセットは実際に解析するまで分からなくなる。 逆に言うと固定長サイズで NOT NULL の属性データが続いている場合、n 番目の属性データのオフセットは事前に計算が可能になる。 そこで slot_deform_tuple() は最初は固定長で NOT NULL の属性データが続いているという仮定で高速なオフセット位置を計算し、可変長または NULL が出現した時に低速な計算に戻る。 TupleTableSlot 構造体の tts_slow はこれを示すフラグである。 最初は tts_slow は false で、slot_deform_tuple() 中に可変長サイズか NULL が見つかった場合に true を設定する。 高速なオフセット位置計算では、各属性のオフセット位置が TupleDesc 内の attcacheoff に記録されている。

参考文献

コメント

コメントを書き込む

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Email: nminoru atmark nminoru dot jp, Twitter:@nminoru_jp