この記事は PostgreSQL 9.4 と 9.5 に基づいて記述している。
このページでは PostgreSQL をハックしたりエクステンション(extension)を開発する人向けに、PostgreSQL の扱っているデータ型の内部とタプルの内部構造について紹介する。
PostgreSQL の他の記事へのインデックスはここ。
更新履歴
(2016.09.18) 作成。
(2017.04.01) attisdropped の情報を追記。
目次
- 1. はじめに
- 2. 基本的なデータ型
- 3. varlena 型
- 4. Heap Tuple と Minimal Tuple
- 5. Tuple Descriptor
- 6. TupleTableSlot
- 参考文献
- コメント
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 のようになる。
分類 | 例 | ByVal | Length | 本当のデータサイズ |
---|---|---|---|---|
基本型 | int32、float4 | true | 実際のバイト数。int16 なら 2、int32 や float4 なら 4。 | Lengthに一致 |
ポインター(固定長) | name、(32ビット環境では) int64、float8 | false | ポインタ先のバイト数。int64 や float8 なら 8。 | Lengthに一致 |
varlena | text、varchar、numeric、decimal、array | false | -1 | varlena のサイズ |
cstring | cstring、unknown の 2 種類のみ | false | -2 | strlen(val) + 1 |
internal | internal のみ | true | sizeof(Datum) | 不明 |
データ型の情報は pg_type システムカタログに格納されている。 pg_type システムカタログ内の typlen と typbyval は特に重要なパラメータである。 tab-1 の Length と ByVal に転写している。
ByVal が true のデータ型は Datum に全データを直接格納できるデータ型である。 ByVal が false の場合はポインターだけを記録する。
Length はデータ型のデータサイズ(バイト数)を計算するために必要なデータを記録している。
- ByVal が true の場合、1 〜 sizeof(Datum) の値が入っている。
- ByVal が false の場合、1 以上の値、-1、-2 の値が入っている。
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 システムカタログの typinput と typoutput 列に定義されている。 SQL の問い合わせの出力結果は output 関数を使って変換している。
一部のデータ型はバイナリ列へ変換するための send 関数 と、バイナリ列からデータ型へ復元するための receive 関数 が定義されている。 send して receive した結果は意味的に同一になる必要がある。
input/output 関数は全データ型の必須だが、send/receive 関数は省略してもよい。
internal 型も input/output 関数を用意しているが、実際に呼ばれるとエラーが出る。 internal 型に send/receive 関数は実装されていない。
3. varlena 型
varlena 型は可変長を格納する基本的なデータ構造である。 ビッグ・エンディアン(big endian)とリトル・エンディアン(little endian)でデータフォーマットが微妙に異なるが、ここではリトル・エンディアンのみを説明する。
種類 | 先頭バイト | タグ | ヘッダー長 | 格納形式 |
---|---|---|---|---|
4B_U | xxxx,xx00 | 4 バイト長 | 行内の無圧縮データ。 | |
4B_C | xxxx,xx10 | 4 バイト長 | 行内の圧縮データ。 | |
1B | xxxx,xxx1 | 1 バイト長 | 行内の無圧縮データ。 | |
1B_E | 0000,0001 | INDIRECT(1) | 1 バイト長 | 行外インメモリのTOAST格納。クエリー処理中のみの形式でストレージ格納には使われない。 |
1B_E | 0000,0001 | ONDISK(18) | 1 バイト長 | 行外ディスク上へのTOAST格納。圧縮と非圧縮の両方がありえる。 |
varlena 型にはまずヘッダーが 1 バイトのものと 4 バイトのものが存在する。 ヘッダーが 1 バイトの varlena はヘッダーも含めて 1 〜 128 バイトのサイズをとることができる。 ヘッダーが 4 バイトの varlena はヘッダーも含めて 5 〜 1G バイトのサイズをとることができる。 ヘッダーが 1 バイトか 4 バイトかは、varlena の先頭の 1 バイトの最下位ビットを見れば判定できる。 1 バイトヘッダーの場合は必ず 1 に、4 バイトヘッダーは必ず 0 になる。
1 バイトヘッダーの場合、最下位ビット以外がオール 0 の場合とそれ以外に分かれる。
- 最下位ビット以外がオール 0 以外の場合は 1B 形式となる。最下位ビット以外の 7 ビットが varlena 型全体の 1〜127 のバイト数を示している。
- 最下位ビット以外がオール 0 の場合は 1B_E 形式となる。1 バイトヘッダーに続く 1 バイトがタグ領域になる。
- タグが 1 の場合は、この varlena はメモリ上にある別の varlena を間接的に参照している。 この varlena のサイズは 2 + sizeof(pointer)。実際のデータサイズは参照している varlena のサイズからヘッダー分(1バイト)を引いたものとなる。
- タグが 18 の場合は、この varlena は TOAST の中に格納されているデータを参照している。 続く 16 バイトに TOAST の情報が格納されている。 この varlena のサイズは 18 バイト。実際のデータサイズは var_rawsize に格納されている。
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 になる。
- 4B_U では varlena からヘッダー分(4バイト)を引いたものが実際のデータサイズとなる。
- 4B_C の場合は 4 バイトのヘッダーの続く 4 バイトに va_rawsize というフィールドがあり圧縮データを展開後の実際のデータサイズが入っている。
varlena のデータ構造を図示すると fig-1 となる。

リトル・エンディアンの場合もビッグ・エンディアンの場合も 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 に示す。 ○がついている箇所は選択される可能性がある。 ついていない箇所の形式は決して選択されない。
種類 | PLAIN | EXTENDED | MAIN | EXTERNAL |
---|---|---|---|---|
4B_U | ○ | ○ | ○ | ○ |
4B_C | ○ | |||
1B | ○ | ○ | ○ | ○ |
1B_E (ONDISK) | ○(圧縮) | ○(圧縮) | ○(非圧縮) | |
1B_E (INDIRECT) | ストレージの保管形式として選択されることはない。 |
varlena 型を利用するための多数のマクロが用意されている。 以下のマクロをよく使う。
VARSIZE_ANY()
は varlena 型のヘッダーを含めたバイト数を返す(圧縮を解いた実際のデータのサイズではない)。VARSIZE_ANY_EXHDR()
は varlena 型のヘッダーを外したバイト数を返す(圧縮を解いた実際のデータのサイズではない)。VARDATA_ANY()
は varlena 型のヘッダーの次にある実データへのポインタを返す。これは TOAST 化されていたり圧縮されているデータでは正しい値を返さない。つまり 1B と 4B_U 形式でしか動作しない。PG_DETOAST_DATUM()
は 4B_U 形式以外の varlena から 4B_U 形式の varlena を作成する。作成時にはpalloc()
で新しいメモリ領域を確保する。1B 形式も 4B_U 形式に変換される(ヘッダーのサイズが増えるがデータ形式が揃う)。元から 4B_U を入力すると何もしない。PG_DETOAST_DATUM_PACKED()
は TOAST 化されていたり圧縮されている varlena から展開された varlena を作成する。つまり 1B_E と 4B_C を 1B または 4B_U の形式に変換したものを作成する。作成時にはpalloc()
で新しいメモリ領域を確保する。元から 1B または 4B_U を入力すると何もしない。
4. Heap Tuple と Minimal Tuple
PostgreSQL ではテーブル内に INSERT/UPDATE/DELETE で格納する行(row)のことをタプル(tuple)と呼ぶ。
- ストレージに格納する形式を Heap Tuple と呼ぶ。
- クエリー処理の途中で使う形式を Minimal 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 のビットマップが入っている。 その後は属性(カラム)データをあらわすバイト列が入っている。
メンバー名 | サブメンバー名 | データ型 | バイト数 | 説明 |
---|---|---|---|---|
t_len | uint32 | 4 | HeapTuple の全体のバイト数。 | |
t_self | ItemPointerData | 6 (2バイト境界) | この heap tuple のテーブル内の位置 | |
ip_blkid | BlockIdData | 4 (2バイト境界) | DB ブロック番号 | |
ip_posid | OffsetNumber | 2 | DB ブロック内のオフセット番号 | |
t_tableOid | Oid | 4 | この HeapTuple を含むテーブル(relation)の Oid。行(タプル)の Oid ではない。 | |
t_data | HeapTupleHeader | heap tuple の詳細情報のヘッダー部分。 | ||
t_choice | 共用体 | 12 | HeapTupleFields か DatumTupleFiels のいずれかが入る共用体 | |
t_ctid | ItemPointerData | 6(2バイト境界) | この行(タプル)のテーブル内の位置。初期は t_self と同じだが、UPDATE が実行された場合には新しい行(タプル)を指すように更新される。 | |
t_infomask2 | uint16 | 2 | フラグ領域2 | |
t_infomask | uint16 | 2 | フラグ領域 | |
t_hoff | uint16 | 2 | heap tuple の最初の属性の開始位置を示すオフセット。 | |
t_bits[1] | bits8 | 1 | NULL ビットマップの最初の 1 バイト。 |
t_data.t_choice の共用体部分はさらに以下のような構造体に入っている。
メンバー名 | サブメンバー名 | データ型 | バイト数 | 説明 |
---|---|---|---|---|
t_xmin | TransactionId | 4 | 挿入された XID。 | |
t_xmax | TransactionId | 4 | 削除またはロックされた XID。 | |
t_field3 | 共用体 | 4 | ||
t_cid | CommandId | 4 | Combo Command Id(CCI)。CCI の何なのかは「PostgreSQL のトランザクション & MVCC & スナップショットの仕組み」の3.3 節、3.5節 を参照。 | |
t_xvac | TransactionId | 4 | 古いスタイルの VACUUM FULL 用 XID。 |
メンバー名 | データ型 | バイト数 | 説明 |
---|---|---|---|
datum_len | int32 | 4 | 現在は未使用のメンバー変数。 |
datum_typmod | int32 | 4 | -1 またはレコードタイプ固有の識別子。 |
datum_typeid | Oid | 4 | 要素の OID または RECORDOID。 |
Heap tuple のヘッダーと属性(カラム)データを示したのが fig-2 になる。

ヘッダー中の 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_infomask と t_infomask2 はそれぞれ 16 ビットの領域だが、この 2 つは heap tuple のフラグを記憶している。
t_infomask のフラグの意味は tab-7 に、t_infomask2 のビットの意味またはフラグの意味は tab-8 に記した。
tab-7 と tab-8 の「ヒント」の列は、そのビットがヒントビットであることを意味している。 ヒントビットであるとは、t_infomask と t_infomask2 以外の別のパラメータによって決まる状態があり、いったんその状態であると判断されたことをヒントとして記録しておくためのビットである。 そのようなヒントビットが 1 である場合、「その状態である」と言える。 しかしヒントビットが 0 の場合は、「その状態でない」ことを意味しない。 別のパラメータによる判定処理を行い状態を確認することができる。
マクロ | 数値 | ヒント | 説明 |
---|---|---|---|
HEAP_HASNULL | 0x0001 | No | heap tuple の中に NULL が含まれている場合は 1 が立つ。0 なら NULL の属性はない。 |
HEAP_HASVARWIDTH | 0x0002 | No | heap tuple の中に tab-1 の varlena または cstring が含まれている場合に 1 が立つ。そうでないなら 0。 |
HEAP_HASEXTERNAL | 0x0004 | No | heap tuple の中に外部 TOAST に他する参照が含まれている場合に 1 が立つ。そうでないなら 0。 |
HEAP_HASOID | 0x0008 | No | heap tuple に行としての Oid が付く場合に 1 が立つ。Oid が付かない場合には 0。 |
HEAP_XMAX_KEYSHR_LOCK | 0x0010 | No | t_xmax を key-shared locker として使っている場合に 1 が立つ。そうでないなら 0。 |
HEAP_COMBOCID | 0x0020 | No | t_field3.t_cid が Combo CID の場合は 1 が立つ。そうでないなら 0。 |
HEAP_XMAX_EXCL_LOCK | 0x0040 | No | t_xmax を exclusive locker として使っている場合に 1 が立つ。そうでないなら 0。 |
HEAP_XMAX_LOCK_ONLY | 0x0080 | No | t_xmax を only locker として使っている場合に 1 が立つ。そうでないなら 0。 |
HEAP_XMIN_COMMITTED | 0x0100 | Yes | t_xmin の値が CLOG 上でコミットしている場合、そのヒントビットとして 1 を立てる。 |
HEAP_XMIN_INVALID | 0x0200 | Yes | t_xmin の値が CLOG 上でアボートしている場合、そのヒントビットとして 1 を立てる。 |
HEAP_XMIN_FROZEN | 0x0200 | Yes | t_xmin が VACUUM によって凍結された時に 1 を立てる。 |
HEAP_XMAX_COMMITTED | 0x0400 | Yes | t_xmax の値が CLOG 上でコミットしている場合、そのヒントビットとして 1 を立てる。 |
HEAP_XMAX_INVALID | 0x0800 | Yes | t_xmax の値が CLOG 上でアボートしている場合や t_xmax をロックのために使ったがそのロックがすでに解除された場合、そのヒントビットとして 1 を立てる。 |
HEAP_XMAX_IS_MULTI | 0x1000 | No | t_xmax が MultiXaxtId として使われているなら 1 が立つ。そうでないなら 0。 |
HEAP_UPDATE | 0x2000 | No | この heap tuple が UPDATE によって更新された行の更新後の heap tuple なら 1 が立つ。そうでないなら 0。 |
HEAP_MOVED_OFF | 0x4000 | No | PostgreSQL 9.0 では使わない。 |
HEAP_MOVED_OFF | 0x8000 | No | PostgreSQL 9.0 では使わない。 |
マクロ | 数値 | ヒント | 説明 |
---|---|---|---|
HEAP_NATTS_MASK | 0x07FF | No | t_infomask2 の下位 11 ビットは 0 〜 2047 までの数値を埋め込み、この heap tuple の属性数を記憶している。これを HeapTupleHeaderGetNatts() で取得することができる。heap tuple 毎の属性数を持つことで、ALTER TABLE ADD/DROP COLUMN によってテーブルの列数(属性数)が変更された場合に対応可能になっている。 |
HEAP_KEYS_UPDATE | 0x2000 | No | |
HEAP_HOT_UPDATE | 0x4000 | No | |
HEAP_ONLY_TUPLE | 0x4000 | No | この 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 を参照すること。
メンバー名 | データ型 | バイト数 | 説明 |
---|---|---|---|
t_len | uint32 | 4 | Minimal tuple の全体のバイト数。 |
mt_padding | char[4] | 4 | 次の t_infomask2 を 8 バイト境界にそろえるためのパディング |
t_infomask2 | uint16 | 2 | フラグ領域2 |
t_infomask | uint16 | 2 | フラグ領域 |
t_hoff | uint16 | 2 | Minimal tuple の最初の属性の開始位置を示すオフセット。 |
t_bits[1] | bits8 | 1 | NULL ビットマップの最初の 1 バイト。 |
Minimal tuple のヘッダーと属性(カラム)データを示したのが fig-3 になる。 配置ルールなども heap tuple と同じである。

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
構造体によって管理される。
メンバー名 | データ型 | バイト数 | 説明 |
---|---|---|---|
natts | int | 4 | 属性数。 |
atts | Form_pg_attribute[] | sizeof(void*) | 各属性の情報。pg_attribute システムカタログの情報を引き生成される。 |
constr | TupleConstr[] | sizeof(void*) | 制約。 |
tdtypeid | Oid | 4 | Tuple descriptor に相当するタイプが pg_type システムカタログ上にあればその Oid。 |
tdtypmod | int32 | 4 | Tuple desciptor のタイプ修飾。-1 なら無効。 |
tdrefcount | int | 4 | Tuple 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 つのモードが存在する。 ただしソースコード中にそのような単語が存在しているわけではない。
メンバー名 | データ型 | 説明 |
---|---|---|
type | NodeTag | ノード種類を示すタグ。T_TupleTableSlot を入れる。ノード種類の詳細については「PostgreSQL プラン・ツリーの概要」の3.1 節を参照のこと。 |
tts_isempty | bool | TupleTableSlot が空でデータを持っていない状態を示す。これは初期化後や ExecClearTuple() 後に true となり、TubleTableSlot にデータを格納する関数を実行すると false になる。 |
tts_shouldFree | bool | Heap Tuple モードを解除する関数を呼び出すタイミングで tts_tuple の先にある HeapTuple を heap_freetuple() で解放する場合には true を指定する。false ならケアしない。 |
tts_shouldFreeMin | bool | Minimal Tuple モードを解除する関数を呼び出すタイミングで tts_mintuple の先にある HeapTuple を heap_free_minimal_tuple() で解放する場合には true を指定する。false ならケアしない。 |
tts_slow | bool | slot_deform_tuple() を呼び出した際に、slot_deform_tuple() が内部処理に利用する。 |
tts_tuple | HeapTuple | Heap Tuple モードで HeapTuple を指すポインター。 |
tts_tupleDescriptor | TupleDesc | この TupleTableSlot の形式を記録する TupleDesc へのポインター。 |
tts_mcxt | MemoryContext | この TupleTableSlot を介した処理で新しいメモリを確保する際に使うメモリコンテキストへのポインター |
tts_buffer | Buffer | テーブル(リレーション)のスキャンを行う際に、現在読み込み中のページをピンするために用いる。ExecStoreTuple() で設定される。未使用の場合には InavlidBuffer を入れておく。 |
tts_nvalid | int | Virtual Tuple モードで利用する。 |
tts_values | Datum * | Virtual Tuple モードで利用する。 |
tts_isnull | isnull * | Virtual Tuple モードで利用する。 |
tts_minituple | MinimalTuple | Minimal Tuple モードで MinimalTuple を指すポインター。 |
tts_minhdr | HeapTupleData | Minimal Tuple モードで利用する。 |
tts_off | long | slot_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 のようになる。

この 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 が入る。

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_minituple と tts_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 に記録されている。
参考文献
- [1] PostgreSQL 9.5.4 文書 9.5.4 文書 第8章 データ型
- [2] PostgreSQL 9.5.4 文書 49.54 pg_type