本節ではTOAST(過大属性格納技法:The Oversized-Attribute Storage Technique)の概要について説明します。
PostgreSQLは固定長のページサイズ(通常8キロバイト)を使用し、複数ページにまたがるタプルを許しません。 そのため、大規模なフィールド値を直接格納できません。 この限界を克服するため、大規模なフィールド値を圧縮したり、複数の物理的な行に分割したりしています。 これはユーザからは透過的に発生し、また、バックエンドのコード全体には小さな影響しか与えません。 この技法はTOAST(または「パンをスライスして以来最善のもの」)という愛称で呼ばれます。 [訳注:TOASTはパンのトーストと綴りが同じなので、スライスしたパンを美味しく食べる方法に掛けて洒落ています。] TOASTの基盤は大きなデータ値のインメモリで処理の改善にも使用されています。
一部のデータ型のみがTOASTをサポートします。
大規模なフィールド値を生成することがないデータ型にオーバーヘッドを負わせる必要はありません。
TOASTをサポートするためには、データ型は可変長(varlena)表現を持たなければなりません。
通常は、格納する値の最初の4バイトワードには値の長さ(このワード自体を含む)がバイト単位で含まれます。
TOASTは残りのデータ型の表現について制限しません。
TOAST化された値として集合的に呼ばれる特別な表現は、この先頭の長さのワードを更新または再解釈することで動作します。
したがって、TOAST可能なデータ型をサポートするC言語関数は、潜在的にTOAST化されている入力値の扱い方に注意しなければなりません。
つまり、入力がTOAST解除されなければ、それは実際には4バイトの長さのワードと内容から構成されていないかもしれないのです。
(通常これは、入力に対して何か作業をする前にPG_DETOAST_DATUM
を呼び出すことで行われますが、もっと効率的な方法が可能な場合もあります
詳しくは37.13.1を参照してください)。
TOASTはvarlenaの長さワードの2ビット(ビッグエンディアンのマシンでは上位ビット、リトルエンディアンのマシンでは下位ビット)を勝手に使用します。 そのため、すべてのTOAST可能なデータ型の値の論理サイズは1ギガバイト(230 - 1バイト)までになります。 両ビットが0の場合、値はそのデータ型の普通のTOAST化されていない値となり、長さワードの残りのビットはデータの(長さワードを含む)総サイズ(バイト単位)となります。 上位側または下位側のどちらか片方のビットが設定された場合、値は通常の4バイトのヘッダを持たず1バイトのヘッダを持ちます。 また、そのバイトの残りビットはデータの(長さワードを含む)総サイズ(バイト単位)となります。 この方式により、127バイトより短い値の効率的な格納をサポートする一方で、データ型が必要なら1GBにまで大きくなることを可能にしています。 1バイトのヘッダを持つ値は特定の境界に整列されませんが、4バイトのヘッダを持つ値は少なくとも4バイト境界の上に整列されます。 このように整列のためのパディングを省略することで、短い値と比べて重要な追加のスペース節約ができます。 特殊な状況として、1バイトのヘッダの残りビットがすべて0(自身の長さを含む場合はありえません)の場合、その値は行外データへのポインタで、以下に述べるようにいくつかの可能性があります。 そのようなTOASTポインタの型とサイズはデータの2番目のバイトに格納されるコードによって決定されます。 最後に上位側または下位側のビットが0で隣のビットが設定されている場合、データの内容は圧縮され、使用前に伸長しなければなりません。 この場合、4バイトの長さワードの残りビットは元データのサイズではなく圧縮したデータの総サイズになります。 圧縮が行外データでも起こりえますが、varlenaヘッダには圧縮されているかどうかについての情報がないことに注意してください。 その代わりTOASTポインタの内容にこの情報が含まれています。
前に触れたように、TOASTポインタデータにはいくつかの型があります。
最も古くて一般的な型はTOASTテーブルに格納されている行外データへのポインタです。
TOASTテーブルは、TOASTポインタデータ自体を含むテーブルとは別の、しかし関連付けられるテーブルです。
これらのディスク上のポインタデータは、ディスク上に格納されるタプルが、そのまま格納するには大きすぎる時に、TOAST管理コード(access/heap/tuptoaster.c
にあります)によって作られます。
更なる詳細は68.2.1に記述されています。
あるいはTOASTポインタデータは、メモリ内のどこかにある行外データへのポインタのこともあります。
そのようなデータは短命で、ディスク上に現れることは決してありませんが、大きなデータ値を複製し、余分な処理をするのを避けるために有用です。
更なる詳細は68.2.2に記述されています。
行内あるいは行外の圧縮データで使用される圧縮技術は、LZ系の圧縮技術の1つで単純かつ非常に高速なものです。
詳細はsrc/common/pg_lzcompress.c
を参照してください。
テーブルの列に1つでもTOAST可能なものがあれば、そのテーブルには連携したTOASTテーブルがあり、そのOIDがテーブルのpg_class
.reltoastrelid
エントリに格納されます。
ディスク上のTOAST化された値は以下で詳しく説明する通り、TOASTテーブルに保持されます。
行外の値は(圧縮される場合は圧縮後に)最大TOAST_MAX_CHUNK_SIZE
バイトの塊に分割されます
(デフォルトではこの値は4チャンク行が1ページに収まり、およそ2000バイトになるように選ばれます)。
各塊は、データを持つテーブルと連携するTOASTテーブル内に個別の行として格納されます。
すべてのTOASTテーブルはchunk_id
列(特定のTOAST化された値を識別するOID)、chunk_seq
列(値の塊に対する連番)、chunk_data
(塊の実際のデータ)列を持ちます。
chunk_id
とchunk_seq
に対する一意性インデックスは値の抽出を高速化します。
したがって、行外のディスク上のTOAST化された値を示すポインタデータには、検索先となるTOASTテーブルのOIDと指定した値のOID(chunk_id
)を格納しなければなりません。
簡便性のために、ポインタデータには論理データサイズ(元々の非圧縮のデータ長)と物理的な格納サイズ(圧縮時には異なります)も格納されます。
varlenaヘッダバイトに収納するためにディスク上のTOASTポインタデータの総サイズは、表現される値の実サイズに関係なく、18バイトになります。
TOAST管理のコードは、テーブル内に格納される値がTOAST_TUPLE_THRESHOLD
バイト(通常2キロバイト)を超える時にのみ実行されます。
TOASTコードは、行の値がTOAST_TUPLE_TARGET
バイト(こちらも通常2キロバイト、調整可能)より小さくなるかそれ以上の縮小ができなくなるまで、フィールド値の圧縮や行外への移動を行います。
更新操作中、変更されない値は通常そのまま残ります。
行外の値を持つ行の更新では、行外の値の変更がなければTOASTするコストはかかりません。
TOAST管理のコードでは、ディスク上にTOAST可能な列を格納するために、以下の4つの異なる戦略を認識します。
PLAIN
は圧縮や行外の格納を防止します。
さらにvarlena型での単一バイトヘッダの使用を無効にします。
これはTOAST化不可能のデータ型の列に対してのみ取り得る戦略です。
EXTENDED
では、圧縮と行外の格納を許します。
これはほとんどのTOAST可能のデータ型のデフォルトです。
圧縮がまず行われ、それでも行が大き過ぎるのであれば行外に格納します。
EXTERNAL
は非圧縮の行外格納を許します。
EXTERNAL
を使用すると、text
とbytea
列全体に対する部分文字列操作が高速化されます。
こうした操作は非圧縮の行外の値から必要な部分を取り出す時に最適化されるためです
(格納領域が増加するという欠点があります)。
MAIN
は圧縮を許しますが、行外の格納はできません
(実際にはこうした列についても行外の格納は行われます。
しかし、他に行を縮小させページに合わせる方法がない場合の最後の手段としてのみです)。
TOAST可能なデータ型はそれぞれ、そのデータ型の列用のデフォルトの戦略を指定します。
しかしALTER TABLE ... SET STORAGE
を使用して、あるテーブル列の戦略を変更することができます。
TOAST_TUPLE_TARGET
はALTER TABLE ... SET (toast_tuple_target = N)
を使って各テーブルで調整できます。
この機構には、ページをまたがる行の値を許可するといった素直な手法に比べて多くの利点があります。 通常問い合わせは比較的小さなキー値に対する比較で条件付けされるものと仮定すると、エクゼキュータの仕事のほとんどは主だった行の項目を使用して行われることになります。 TOAST化属性の大規模な値は、(それが選択されている時)結果集合をクライアントに戻す時に引き出されるだけです。 このため、主テーブルは行外の格納を使用しない場合に比べて、かなり小さくなり、その行は共有バッファキャッシュにより合うようになります。 ソート集合もまた小さくなり、ソートが完全にメモリ内で行われる頻度が高くなります。 小規模な試験結果ですが、典型的なHTMLページとそのURLを持つテーブルでは、TOASTテーブルを含め、元々のデータサイズのおよそ半分で格納でき、さらに、主テーブルには全体のデータのおよそ10%のみ(URLと一部の小さなHTMLページ)が格納されました。 すべてのHTMLページを7キロバイト程度に切り詰めたTOAST化されない比較用テーブルと比べ、実行時間に違いはありませんでした。
TOASTポインタは、ディスク上にあるデータだけでなく、現在のサーバプロセスのメモリ内の場所を指すこともできます。 そのようなポインタは明らかに短命ですが、それでも有用です。 現在のところ、間接データへのポインタと、展開データへのポインタの2つのケースがあります。
間接TOASTポインタは、単にメモリ上のどこかに格納されている間接的でないvarlena値を指すだけです。 このケースは元々は単なる概念実証として作られたのですが、現在はロジカルデコーディング時に1GBを越える物理的タプルを作成する可能性を防ぐために使用されています。 (すべての行外フィールド値をタプルに持ってこようとすると、そうなるかもしれません。) このケースでは、ポインタデータの作成者はポインタが存在可能な限り参照データが存在し続けることに全責任を負うため、利用が限られ、またこれを支援するための基盤もありません。
展開TOASTポインタは、ディスク上の表現が計算目的にあまり適さない複雑なデータ型で有用です。
例えばPostgreSQLの配列の標準varlena表現には、次元の情報、NULLの要素があればNULLのビットマップ、そしてすべての要素の値が順番どおりに含まれます。
要素型自体が可変長だと、N
番目の要素を探す唯一の方法は前にある要素のすべてをスキャンすることです。
この表現は、そのサイズの小ささからディスク上の記録には適していますが、配列を使った計算では、すべての要素の開始位置が特定されている「展開」または「解体」された表現があるとずっと良いです。
TOASTポインタの機構では、参照渡しのデータが、標準のvarlena値(ディスク上の表現)あるいはメモリ上のどこかにある展開表現を指すTOASTポインタを指すことを許すことで、このニーズに応えています。
この展開表現の詳細はデータ型に依存しますが、標準ヘッダを持ち、src/include/utils/expandeddatum.h
にある他のAPIの要求を満たす必要があります。
データ型を処理するc言語の関数は、どちらかの表現を扱うことを選ぶことができます。
展開表現を認識せず、入力データに単にPG_DETOAST_DATUM
を適用するだけの関数は、自動的に伝統的なvarlena表現を受け取ります。
従って、展開表現のサポートは徐々に、1回に1つの関数だけ導入することができます。
展開された値へのTOASTポインタは、さらに読み書きのポインタと読み取りのみのポインタに分類されます。 指された先の表現はどちらでも同じですが、読み書きのポインタを受け取った関数は、そこにある参照値を変更できるのに対し、読み取りのみのポインタを受け取った関数では変更が許されないため、値を変更したバージョンを作りたければ、まずその複製を作る必要があります。 この区別と、関連したいくつかの慣習により、問い合わせの実行時に展開された値を不必要に複製するのを避けることが可能になります。
すべてのタイプのインメモリのTOASTポインタについて、TOAST管理のコードはそのようなポインタデータが偶然、ディスクに保存されてしまうことが決して起こらないようにします。 インメモリのTOASTポインタは保存される前に自動的に展開されて通常の行内のvarlena値になります。 その後、含んでいるタプルが大きすぎるような時には、ディスク上のTOASTポインタに変換されることもあります。