PostgreSQL テーブルサイズ ディスク容量 見積もり方法
参考サイト
http://www2b.biglobe.ne.jp/~caco/webdb-pdfs/vol24_214-221.pdf
Version
PostgreSQL 8.0 を対象に説明する。
データベース
テーブルやインデックスの本体はデータベース配下の「base」というディレクトリに 格納されるのは常識ですね。 base以下の数字のディレクトリ1つ1つが、1つの「データベース」に対応していて、 どのデータベースがどのディレクトリに対応しているかはpg_databaseでわかりますね。 # SELECT oid, datname from pg_database ;
ヒープファイルとブロック
データベースに所属するテーブルは、テーブルスペースを指定しない限り、データベースと 同じテーブルスペースに作成される。 テーブルの実体は、「ブロック」という固定長(デフォルトでは8192バイト)の領域が 0個以上131072個以下集まったもの。 テーブルを作った直後は対応するファイル(「ヒープ(heap)ファイル」と呼びます) の大きさは0です。 データを追加していくにつれてヒープファイルは大きくなります。ブロックが131072個、 すなわちヒープファイルのサイズが1Gバイトを超えると、新しいヒープファイルが作られます。 こうして、1つの論理的なテーブルは32Tバイト(=32×1024Gバイト)までの大きさを取ること ができます。 1つの論理的なテーブル | └ 複数のヒープファイル 32Tバイト(=32×1024Gバイト) | └ 複数のブロック 0個以上131072以下(=1Gバイト)
ページとタプル
ブロックの中には「ページ」があります。PostgreSQL(postgres)の歴史上ある時点では 1ブロック中に複数のページを納める計画もあったようですが、現在では1ブロックの中に 入るページ数が1以外になることはないので、実際にはブロック=ページになっています。 ただし、ブロックとページの使い分けが一応あり、ブロックは物理アクセスの単位であり、 ブロック内の情報の構造を問題にするときはページという言葉を使います。 ページの中には行が0個以上入ります。 ページの先頭には24バイトの管理領域があり、「ページヘッダ」と呼ばれています。 ※ PostgreSQL7.4 では、20バイトです。 ページヘッダの中にはトランザクションログの管理データのほか、このページの中の 空き領域を管理する情報が入っています。 ●ページの構造 ・ページヘッダ(24バイト) ・アイテムポイントA ⇒ タプルA先頭/末尾 を記憶 ・アイテムポイントB ⇒ タプルB先頭/末尾 を記録 ↓ ↓ ※アイテムポイントの成長する方向 フリースペース ↑ ↑ ※タプルの成長する方向 ・タプルA ・タプルB ・スペシャルスペース(通常0バイト) ●ページヘッダの構造(計算上では20バイトですが・・・・・24バイト) フィールド名 バイト長 用途 pd_lsn 8 LSN(Log Sequence Number) pd_tli 4 タイムラインID pd_lower 2 ページ内の空き領域の開始位置 pd_upper 2 ページ内の空き領域の終了位置 pd_special 2 ページ内のスペシャルスペースの終了位置 pd_pagesize_version 2 ページサイズとバージョンに関する情報 LSNは、PostgreSQLがトランザクションログを使ってデータベースをリカバリするときに使用します。 トランザクションログには「更新を行った」とか「新しいレコードを追加した」というような情報 が入っているので、これをデータベースに適用することによってデータをリカバリすることができ ます。このときにLSNを調べることにより、誤って同じログを2回適用してしまうことを防ぐことが できます。 「タイムライン(TimeLine)ID」もトランザクションログによるリカバリに関係しています。 PostgreSQLは、トラザンクションログを使ってリカバリをかける際に、直近ではなくて任意の時点 まで戻ることができ、このときにタイムラインIDが生成されます。タイムラインIDを使うことにより、 一旦リカバリをかけた後に再度別の時点を選んでリカバリすることができます。これは、リカバリを かけて戻るべき時点がはっきりしておらず、何度もリカバリをかけてデータ内容を確認しなければな らないときなどに有効です。 ページヘッダの後には「アイテムデータ」が続きます。 アイテムデータは可変長の領域で、1項目につき4バイトの大きさがあり、ページ内のタプルの位置を 管理しています。 タプルはページの後方から追加されていきます。タプルが1個追加されるたびにアイテムデータの1項目 がページヘッダの後ろに追加されます。 つまり、ページの中央の空き領域を、前からはアイテムデータ、後ろからはタプルが攻めていくような イメージでページの空き領域が埋まっていくわけです。ページの最後には「スペシャルスペース」があ る場合もありますが、通常のテーブルではこのサイズは0であり、使用されていません。
タプルの構造
タプルの先頭には管理領域である「タプルヘッダ」があります。 タプルヘッダは普通のPCでは27バイトの大きさです。 ※ PostgreSQL7.4 では、23バイトです。 ●タプルヘッダの構造 フィールド名 バイト長 用途 t_xmin 4 行を挿入したトランザクションID t_cmin 4 行を挿入したコマンドID t_xmax 4 行を削除したトランザクションID t_cmax/t_xvac 4 行を削除したコマンドIDまたはVACUUMによって 移動された行のバージョン t_ctid 6 この行あるいは新しい行のTID(タプルID) t_natts 2 列の数 t_infomask 2 フラグビット t_hoff 1 行データへのオフセット タプルヘッダの後ろには「ヌルビットマップ」があり、NULL データを含むタプルにおいて、 ある列がNULLかどうかを記録します。 ⇒ NULLを含まない行ではヌルビットマップは存在しません。 ⇒ NULLを許す列が1つでも含まれる場合は、列の数分のビットを持つヌルビットマップが存在します。 そのサイズは、(列の数+7)/8バイトになります。もしこのバイト数が4の倍数にならない場合は、 4の倍数に切り上げるためにパディングが行われます。 その後には4バイトのOID(オブジェクトID)があります。 「WITHOUT OID」を指定して作成されたテーブルでは、OIDは存在しません。
計算
テーブルサイズの計算の簡単な例
条件
●ブロックのサイズはデフォルトの8192バイト ●サンプルテーブル CREATE TABLE t1( i INTEGER NOT NULL, j INTEGER NOT NULL );
1. フリースペースの計算
ページの先頭には24バイトのページヘッダがあるので、 8192 - 24 = 8168バイト が使用可能である。
2. 格納タプル数の計算
タプルあたりの大きさ = タプルヘッダ(27バイト) + 1バイトのパディング + OID(4バイト) + i列の大きさ(4バイト) + j列の大きさ(4バイト) ※ ここで1 バイトのパディングが入っているのは、OID(OIDがない場合は列の先頭)が 4バイトバウンダリになることが要求されるからです。 このバウンダリ要求はアーキテクチャによって異なりますが、普通のPCでは4です。 以上から、1ページに格納できるタプル数は、 8168/(40+4)=185.63 を切り下げて185個になります。 ※ 4バイトは、アイテムデータのバイト数。
3. 必要ページ数の計算
したがってt1に4096行のデータを挿入したとすると、 4096/185=22.14 から、23ページ=23ブロックのテーブル領域が必要であることがわかります。 これはディスク容量で言うと、23×8192=188416バイト=184Kバイトが必要であることを示します。
4. ディスク容量が既に決まっている場合
たとえば、1Mバイトのヒープファイルに何行格納できるか計算してみましょう。 まず1Mバイト=1024×1024=1048576バイトですから、ブロック数で言うと 1048576/8192=128になります。 各ブロック=ページには185行入りますから、トータルでは128×185=23680行格納できることが わかりました。
データ型
●データ型の記憶容量 型 記憶容量(バイト) SMALLINT(INT2) 2 INTEGER(INT4) 4 BIGINT(INT8) 8 SERIAL(SERIAL4) 4 BIGSERIAL(SERIAL8) 4 REAL 4 DOUBLE PRECISION 8 FLOAT 8 FLOAT(1-24) 4 FLOAT(25-53) 8 TIMESTAMP 8 TIMESTAMP WITHOUT TIME ZONE 8 TIMESTAMP WITH TIME ZONE 8 INTERVAL 12 DATE 4 TIME 8 TIMEP WITHOUT TIME ZONE 8 TIME WITH TIME ZONE 12 BOOLEAN 1 XID 4 OID 4 ●可変長データ型の場合の計算方法 文字列型、NUMERIC、配列の場合は3つの理由に より容量計算がかなり難しくなります. ① ユーザが入力するデータ長によって容量が変わる ② データによっては自動圧縮が行われる ③ 文字コードによっては、文字数とバイト数の関係が 文字種によって変わる ①③については平均データ長を決めて計算すれば対 応できますが、②はプログラムの挙動がわからないと 計算できません. ●TEXT 型の場合 まず、TEXT型について調べてみましょう.ご存じ の方も多いと思いますが、TEXT型はPostgreSQL固 有のデータ型で、無限長(実際には圧縮後で1Gバイ ト)までの文字列を格納できます.文字数の制限がな いのでPostgreSQLのユーザには広く使われています. TEXT型の構造は、文字列の長さを格納する4バイ トのヘッダとその後に続く実際の文字データになって います.したがって、圧縮を考えなければ「4+文字 列バイト長」がTEXT型のバイト長です.
文字種とバイト長
ここで「漢字」と言っているのは、いわゆる全角文 字のことを意味します.ですから「あ」や「α」など もこの分類に入ります.使いたい文字がどの分類にな るかよくわからない場合は、octet_lengthを使ってバ イト長を確認できます(図4) ●文字種●文字コード●1文字あたりのバイト長 ASCII EUC-JP 1 1バイトカタカナEUC-JP 2 JIS X 0208漢字EUC-JP 2 JIS X 0212漢字EUC-JP 3 ASCII UTF-8 1 1バイトカタカナUTF-8 3 JIS X 0208漢字UTF-8 3 JIS X 0212漢字UTF-8 3
TOAST と圧縮を考慮した場合
このままでは8192バイトを超えるような大きなデ ータは格納できません.そこでPostgreSQL では、 TOASTテーブルという別のテーブルにはみ出た部分を 格納することによって大きなデータを格納できる
TOAST テーブルの構造
TOASTテーブルにはデータがBYTEA型に変換され た後「チャンク(chunk)」と呼ばれる単位に分割さ れて格納されます.1つのチャンクの大きさは最大で も通常1994バイトを超えない大きさです.TOASTテ ーブルは表5 のような構造を持っています.ここで chunk_idは、ある行のある列データを識別するため の固有のIDです.chunk_seqは複数のチャンクに分 割したデータに付けられた一連の番号です. chunk_dataがチャンク本体です.TOASTテーブルに はインデックスもあり、chunk_idとchunk_seqの複 合ユニークインデックスが作られます. ●列名 ●データ型 chunk_id OID chunk_seq INTEGER chunk_data BYTEA ● TOAST テーブルの中身を調べる 実際にデータを格納して確認してみましょう.文字 コードはEUC-JPを使用するものとします.格納する テーブルはリスト2のものを使用します. ▼リスト2 サンプルテーブル(2) CREATE TABLE t2( t TEXT NOT NULL ); test=# insert into tb_test values (1111,'4444'); INSERT 0 1 test=# select length(id), length(name) from tb_test ; length | length --------+-------- 4 | 4 テーブルに可変長の項目が含まれていると自動的に TOASTテーブルとインデックスが作成されます.TOAST テーブルは「pg_toast_テーブルのrelfilenode注5」、イ ンデックスは「pg_toast_テーブルのrelfilenode_index」 という命名規則があるので、以下に解説するようにし て名前と大きさ(ブロック数)が検索できます. 検索前に一度VACUUMを実行して、ブロック数に 関する情報を更新しておくのがポイントです(図6). pg_toast_17288がTOASTテーブルで、pg_toast_17288 _indexがインデックスです.それぞれ、1ブロック、2 ブロックの大きさがあります. 注5)relfilenodeはテーブルに割り当てられた固有のIDで、システムカタログのpg_classで管理されます. ●toast SELECT c2.relname, c2.relfilenode, c2.relpages FROM pg_class c,pg_class c2 WHERE (c2.relname = ('pg_toast_' || c.relfilenode) OR c2.relname = ('pg_toast_' || c.relfilenode || '_index')) AND c.relname like '%t2%' ; relname | relfilenode | relpages ----------------------+-------------+---------- pg_toast_16723_index | 16727 | 1 pg_toast_16723 | 16725 | 0 pg_toast_16718_index | 16722 | 1 pg_toast_16718 | 16720 | 0 ※テーブルを作成して時点で作成される。 ●table select relname,relfilenode,relpages from pg_class where relname like '%t2%' ; relname | relfilenode | relpages ---------+-------------+---------- t2_v2 | 16723 | 0 t2 | 16718 | 1 ※作成例 ls -l /psgdata/base/16643/1672* 31 bsaa49033:/psgdata/base/16643 root# ls -l /psgdata/base/16643/1672* -rw------- 1 psgadm dba 0 May 21 10:59 /psgdata/base/16643/16720 -rw------- 1 psgadm dba 8192 May 21 10:59 /psgdata/base/16643/16722 32 bsaa49033:/psgdata/base/16643 root# ls -l /psgdata/base/16643/1671* -rw------- 1 psgadm dba 8192 May 21 11:21 /psgdata/base/16643/16718 33 bsaa49033:/psgdata/base/16643 root# ls -l /psgdata/base/16643/16723 -rw------- 1 psgadm dba 0 May 21 11:46 /psgdata/base/16643/16723 34 bsaa49033:/psgdata/base/16643 root# ls -l /psgdata/base/16643/16725 -rw------- 1 psgadm dba 0 May 21 11:46 /psgdata/base/16643/16725 35 bsaa49033:/psgdata/base/16643 root# ls -l /psgdata/base/16643/16727 -rw------- 1 psgadm dba 8192 May 21 11:46 /psgdata/base/16643/16727 それではいよいよTOASTテーブルの中身を調べて みましょう.TOASTテーブルはpg_toastというTOAST 専用のスキーマに所属しており、またpg_toastはスキ ーマサーチパスに含まれていないので、検索するとき はスキーマ名をテーブル名の前に付けておきます注6 (図7).一連のチャンクには17294というIDが付けら れ、0から2までのシーケンス番号を持つ3つのチャン クに分割されています.0と1のチャンクは最大長で ある1994バイトで、残りのデータがチャンク2に格納 されています.ですから、TOASTデータの全長は5593 バイトということになります. ●TOAST テーブルの中身を検索 SELECT chunk_id, chunk_seq, length(chunk_data) FROM pg_toast.pg_toast_16718; chunk_id | chunk_seq | length ----------+-----------+-------- 16728 | 0 | 1994 16728 | 1 | 1994 16728 | 2 | 1994 16728 | 3 | 1994 16728 | 4 | 1994 16728 | 5 | 385 (6 rows) SELECT chunk_id, chunk_seq, length(chunk_data) FROM pg_toast.pg_toast_16723; test=# SELECT chunk_id, chunk_seq, length(chunk_data) test-# FROM pg_toast.pg_toast_16723; chunk_id | chunk_seq | length ----------+-----------+-------- 16729 | 0 | 1994 16729 | 1 | 1994 16729 | 2 | 1994 16729 | 3 | 1994 16729 | 4 | 1994 16729 | 5 | 385 test=# select length(t) from t2_v2; length -------- 328188 (1 row) 注6)chunk_dataの中身はBYTEAで、しかも圧縮がかかった後のバイナリ値を表現したものですから、 そのまま見ても「\346%\000\000\000\012\305\314\301...」のようになってしまうので、 ここでは長さだけを表示させています.⇒kinn確認済み ●ヒープファイル本体 ヒープファイル本体のほうにはどの程度の大 きさのデータが格納されているのでしょうか? contrib に登録されているpgstattupleを使えば確認できます. $ cd postgresql-8.0.0beta4/contrib/pgstattuple $ make $ make install $ psql -f pgstattuple.sql test これでインストールは完了です.さっそくpgstattuple を使ってヒープファイル本体のタプルの長さを調べて みましょう(図8).この中で、tuple_lenがテーブルの 中のタプルの長さの合計です.ここではタプルが1個だ けなので、52というのがそのタプルの長さです.tuple_len にはタプルヘッダも含まれており、その長さは、 タプルヘッダ(27バイト) +1バイトのパディング +OID(4バイト) =32 バイトです. ですから、データ本体の大きさは52- 32=20バイトということになります.20バイトの中 身は表6のようになっています.ご覧のように、すべ て管理データであり、ユーザデータ本体はTOASTテ ーブルに格納されていることがわかります. ここまで調べて、ようやくTEXT型でTOASTと圧 縮がある場合のデータ容量を計算できるようになりま した. 表6 ◎データ本体の中身 ●フィールド名●データ長●意味 va_header 4 フラグ+データ長※ va_rawsize 4 圧縮前のサイズ va_extsize 4 TOASTに格納されているサイズ va_valueid 4 chunk_id va_toastrelid 4 TOASTテーブルのID ※ va_header の頭1 ビットがオンならばTOAST テーブルを使用しているこ とを表し、次のビットがオンならば値が圧縮されていることを表します. したがって、今回のテストデータでは両方のビットがオンになっているは ずです.残りの30 ビットは全データ長です.30 ビットを使って表現でき る最大の数字は1G バイトです.このため、PostgreSQL では、データの 最大長は1Gバイトに制限されています. ▼図8 ヒープファイル本体のタプルの長 test=# \x Expanded display is on. test=# select * from pgstattuple('t2'); -[ RECORD 1 ]------+------ table_len | 8192 tuple_count | 1 tuple_len | 52 tuple_percent | 0.63 dead_tuple_count | 0 dead_tuple_len | 0 dead_tuple_percent | 0 free_space | 8112 free_percent | 99.02 ●データ容量の計算 圧縮がある場合は圧縮後の正確なデータ長を正確に 見積もるのが難しいのですが、ここではとりあえずテ ストデータで圧縮率8%(TOASTデータ長(5593)/ 元データ長(6022)=0.92から計算)として、 リスト2の定義を持つテーブルに10Kバイトの テキストを4096個格納した場合の容量見積もりを行っ てみましょう. !テーブル本体に格納されるタプル長は一律52 バイ ト.したがって1 ページには8168/(52+4)= 145.85から145タプルが格納できる.4096タプル ならば4096/145=28.24だから、29ブロック必 要になる. ⇒4バイトは、注4)参照。 !10K バイトのデータを圧縮すると(10 × 1024 × 0.92=9420.8)から9421バイトになる.これはチ ャンクに分割すると9421/1994=4.72から5つの チャンクになる.したがって4096個ならば4096× 5=20480チャンクが必要で、1つのブロックには4 チャンク格納できるので、20480/4=5120ブロ ックが必要. !以上から、テーブル本体とTOAST テーブルで合計 29+5120=5149ブロック必要.これは、5149× 8192/1024/1024=40.22から約41Mバイトに なる. 元データは4096×10×1024/1024/1024=40M バイトですから、ほぼ同じくらいのディスクスペース が必要であることがわかります. 5593 ⇒ TOASTのlengthの合計 6022 ⇒ テーブルのlength
TOAST と圧縮がない場合
PostgreSQLでは、圧縮をしてデータを格納するか どうかはデータの内容によることになります.すなわ ち、同じ文字が連続しているような場合には圧縮効率 がよくなるので、比較的小さいデータでも圧縮が行わ れます.したがってあらかじめデータ圧縮が行われる かどうか判断するのは難しいのですが、ソースコード 中のコメントによれば、1Kバイト以下のデータの場 合、圧縮が行われる確率は低いとのことです注8. 注8)src/backend/utils/adt/pg_lzcompress.c TOASTも圧縮も行われない場合は、タプル長は、 52+4+データ長 となります.たとえば512 バイトの文字列データを 4096個格納すると、8168/(52+4+512)=14.38 ですから、1ブロックには14タプル入ります.したが って、4096タプルでは4096/14=292.57から293ブ ロック必要になり、これは約2.3Mバイトです.元デ ータは512×4096/1024/1024=2Mバイトですか ら、PostgreSQLに格納する場合はちょっとだけデー タが余計に必要になっています.
NUMERIC 型の場合
NUMERIC型も可変長データ型ですが、データ本 体のバイト数の計算式は以下のようになります. 8+精度(スケール)/4×2 ですからリスト3のようなテーブルにおいて、タプ ルの長さは、28(タプルヘッダ)+4(OID)+8+ 128(スケール)/4×2=104バイトとなります. ▼リスト3 サンプルテーブル(3) CREATE TABLE t4( n NUMERIC(128) NOT NULL );
配列の場合
PostgreSQLでは任意のデータ型注9を配列にするこ とができます.データ量としては、基本的に今まで説 明したデータ型を配列要素数格納したときのデータ量 と考えてよいのですが、配列としての管理領域が加算 されることだけが異なります. 管理領域は全部で16+4×次元数+4×次元数バイ トで、内容は表6になります. 例を示します. CREATE TABLE t5( i INTEGER[] ); INSERT INTO t5 VALUES(ARRAY[1,2,3,4,5,6,7,8,9,10]); この場合、1次元配列なので、タプルの長さは28 (タプルヘッダ)+4(OID)+24(配列管理領域)+ 4(INTEGERのデータサイズ)×10=96バイトにな ります. 以下のような2次元配列を登録した場合は、 INSERT INTO t5 VALUES(ARRAY[[1,2,3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10]]); 28(タプルヘッダ)+4(OID)+32(配列管理領 域)+4(INTEGERのデータサイズ)×20=144バ イトになります. 表6 ◎管理領域の内容 ●フィールド名●バイト長●用途 size 4 この配列データのバイト長 ndim 4 次元数 flag 4 未使用 elemtype 4 データ型OID dim 4×次元数各次元のサイズ dim_lower 4×次元数次元の下限値
ご訪問頂き有難う御座います。
当サイトを効率良く使うためにまずは FrontPage を見て下さい。
検索方法、一覧表示などの各情報を纏めています。
当サイトの説明 → Frontpage