PostgreSQL Advent Calendar 2012(全部俺)のDay 19です。
昨日までのエントリで、PostgreSQLの追記型アーキテクチャの基本的な仕組みと、「追記型であるがゆえの課題」にどのように対処しているのかを解説してきました。
ストレージアーキテクチャ小咄シリーズの最終回である今回は、「FILLFACTOR」と呼ばれる仕組みについて、その動作している様子を見ながら解説します。
(他のRDBMSをご存じの方のボキャブラリーに直すと、OracleやDB2で言うところのPCTFREE、SQL ServerのFILLFACTORと似たような役割の機能になります。)
ブロック内部が既にいっぱいで空き領域が無い時にレコードを追加または更新しようとした場合、特に前々回に解説したPage Pruningを実施しても空き領域が無かった場合、PostgreSQLではどのように処理されるのでしょうか。
結論から言うと、同一ブロック内に新しいレコードを追記する領域が無いため、他のブロックに新しいレコードを書き込むことになります。
テーブルのブロック構造を思い出しながら、具体的な例で見てみましょう。
以下の例は、テーブルに25件のレコードをINSERTしていった状態のテーブルです。
1つのブロックに25件のレコードが含まれていて、テーブルがその1つのブロックのみでできています。既にブロック全体にレコードが配置されており、これ以上レコードを追記する空き領域が無い状態になっています。
これは、もともと25件のレコードが1つのブロックに収まっていたものの、更新する際に(追記するための)空き領域が足りないため新しいブロックを作成した(テーブルファイルを拡張した)ということを示しています。
では、この「別ブロックへの更新処理」の何が問題なのでしょうか。今まで見てきた「同じブロック内での更新」と比べて何が違うのでしょうか。
それは、「更新処理の際に別ブロックへのレコード追記を行うと、余分なI/O処理が発生する」ということです。
同一のブロック内での更新処理であれば、共有バッファ内の(8kBの)読み書きだけで処理が完結することが担保されます。データベースのI/O処理はブロック単位で行われるので、これは当然のことです。
それに対して、異なるブロックへの更新処理になると、「異なるブロックを読み込む」というI/O処理が発生する可能性がある、ということです。もう少し正確に言うと、
そもそも、データベースにおいてはストレージが追記型構造であるかどうかに関わらず、ブロックへのアクセスを如何に削減するか、余計なブロックアクセスを減らすか、ということが重要です。
にも関わらずブロックを一杯まで使ってしまうと、異なるブロックへの更新処理が頻発し、ディスクI/Oが増えてしまう可能性が高まります。追記型のストレージアーキテクチャを持つPostgreSQLでは、この可能性が特に高くなります。
また、当然ながら更新の時だけではなく、(前回までに見てきたように)参照の時にもPostgreSQLの内部では古いタプルからチェーンを辿るようにして最新のタプルを探しますので、更新のチェーンが異なるブロックにまたがっているということは、参照の時にも余計なI/O処理を発生させることになり、パフォーマンスに影響を与えます。
というわけで、前述のような状況(別ブロックへの追記)が発生すると、「あぁ、同じブロックに空き領域があればなぁ」と考えるのが人情というものでしょう。この「UPDATEに備えて同じブロックに意図的に空き領域を確保しておく」ための仕組みが「FILLFACTOR」と呼ばれるものです。
これは、INSERTをする際には指定した割合の空き領域を確保しておき、UPDATEをする時にその空き領域を使う、という仕組みです。このように領域を予約しておくことによって、「更新処理の際に同じブロックの空き領域を使える」という可能性が飛躍的に高まります。
実際の動作を見てみましょう。
以下は、先の例とまったく同じレコードを25件INSERTしたテーブルの状態です。
先ほどと違うのは、同じ25件のレコードであるにも関わらず、今回は1ブロックに収まらず既に2ブロック使っていること、そして1ブロック目に少し空き領域があることです(最後のlp_offが1000バイト以上あります)。
FILLFACTORの設定方法はCREATE TABLEにオプションを付ける方法、ALTER TABLEで設定する方法などがあります。詳細はマニュアルを参照してください。
CREATE TABLE
http://www.postgresql.jp/document/9.0/html/sql-createtable.html
ALTER TABLE
http://www.postgresql.jp/document/9.0/html/sql-altertable.html
なお、FILLFACTORの値は「10~100(%)」で指定しますが、テーブルにおける設定のデフォルトは「100」、つまり「空き領域無しで全部詰め込む」という設定になっています。つまり、何も設定しないとFILLFACTORの恩恵はまったく受けられませんのでご注意ください。
今回は、UPDATE処理の際にできるだけ同一ブロック内の空き領域を使うための「FILLFACTOR」の仕組みについて、その実際の動作を見ながら解説してきました。
FILLFACTORは、特に更新処理の多いワークロードで性能の安定に寄与します。一方で、「空き領域を確保しておく」という機能の特性上、テーブルファイル全体のサイズは大きくなります。
このようなトレードオフが存在しますので、自身のデータベースのワークロードのタイプをよく見極めて、これらの機能をうまく使いこなしていただければと思います。
では、また。
昨日までのエントリで、PostgreSQLの追記型アーキテクチャの基本的な仕組みと、「追記型であるがゆえの課題」にどのように対処しているのかを解説してきました。
ストレージアーキテクチャ小咄シリーズの最終回である今回は、「FILLFACTOR」と呼ばれる仕組みについて、その動作している様子を見ながら解説します。
(他のRDBMSをご存じの方のボキャブラリーに直すと、OracleやDB2で言うところのPCTFREE、SQL ServerのFILLFACTORと似たような役割の機能になります。)
■ブロックに空き領域が無い時の更新処理
ブロック内部が既にいっぱいで空き領域が無い時にレコードを追加または更新しようとした場合、特に前々回に解説したPage Pruningを実施しても空き領域が無かった場合、PostgreSQLではどのように処理されるのでしょうか。
結論から言うと、同一ブロック内に新しいレコードを追記する領域が無いため、他のブロックに新しいレコードを書き込むことになります。
テーブルのブロック構造を思い出しながら、具体的な例で見てみましょう。
以下の例は、テーブルに25件のレコードをINSERTしていった状態のテーブルです。
1つのブロックに25件のレコードが含まれていて、テーブルがその1つのブロックのみでできています。既にブロック全体にレコードが配置されており、これ以上レコードを追記する空き領域が無い状態になっています。
この状態でレコードを更新すると、どうなるのでしょうか。
testdb=# INSERT INTO t1 VALUES ( 125, 'insert 125 ' );
INSERT 0 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46798 | 0
2 | 7568 | 1 | 311 | 46799 | 0
3 | 7256 | 1 | 311 | 46800 | 0
4 | 6944 | 1 | 311 | 46801 | 0
5 | 6632 | 1 | 311 | 46802 | 0
6 | 6320 | 1 | 311 | 46803 | 0
7 | 6008 | 1 | 311 | 46804 | 0
8 | 5696 | 1 | 311 | 46805 | 0
9 | 5384 | 1 | 311 | 46806 | 0
10 | 5072 | 1 | 311 | 46807 | 0
11 | 4760 | 1 | 311 | 46808 | 0
12 | 4448 | 1 | 311 | 46809 | 0
13 | 4136 | 1 | 311 | 46810 | 0
14 | 3824 | 1 | 311 | 46811 | 0
15 | 3512 | 1 | 311 | 46812 | 0
16 | 3200 | 1 | 311 | 46813 | 0
17 | 2888 | 1 | 311 | 46814 | 0
18 | 2576 | 1 | 311 | 46815 | 0
19 | 2264 | 1 | 311 | 46816 | 0
20 | 1952 | 1 | 311 | 46817 | 0
21 | 1640 | 1 | 311 | 46818 | 0
22 | 1328 | 1 | 311 | 46819 | 0
23 | 1016 | 1 | 311 | 46820 | 0
24 | 704 | 1 | 311 | 46821 | 0
25 | 392 | 1 | 311 | 46822 | 0
(25 rows)
testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
8192
(1 row)
testdb=#
空き領域が無い状態でレコードを更新(UPDATE)すると、更新前の古いレコード(lp==1)は削除(t_xmaxが設定される)されますが、同じブロックに空き領域が無い(Page Pruningをしても空き領域を作れない)ため、新しいレコードは異なる新しい2番目のブロックに書き込まれています。新しいブロックが作成されたことで、pg_relation_sizeで取得されるテーブルサイズが8kBから16kBに増加していることが分かります。
testdb=# UPDATE t1 SET uname = 'update 101 ' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46798 | 46823
2 | 7568 | 1 | 311 | 46799 | 0
3 | 7256 | 1 | 311 | 46800 | 0
4 | 6944 | 1 | 311 | 46801 | 0
5 | 6632 | 1 | 311 | 46802 | 0
6 | 6320 | 1 | 311 | 46803 | 0
7 | 6008 | 1 | 311 | 46804 | 0
8 | 5696 | 1 | 311 | 46805 | 0
9 | 5384 | 1 | 311 | 46806 | 0
10 | 5072 | 1 | 311 | 46807 | 0
11 | 4760 | 1 | 311 | 46808 | 0
12 | 4448 | 1 | 311 | 46809 | 0
13 | 4136 | 1 | 311 | 46810 | 0
14 | 3824 | 1 | 311 | 46811 | 0
15 | 3512 | 1 | 311 | 46812 | 0
16 | 3200 | 1 | 311 | 46813 | 0
17 | 2888 | 1 | 311 | 46814 | 0
18 | 2576 | 1 | 311 | 46815 | 0
19 | 2264 | 1 | 311 | 46816 | 0
20 | 1952 | 1 | 311 | 46817 | 0
21 | 1640 | 1 | 311 | 46818 | 0
22 | 1328 | 1 | 311 | 46819 | 0
23 | 1016 | 1 | 311 | 46820 | 0
24 | 704 | 1 | 311 | 46821 | 0
25 | 392 | 1 | 311 | 46822 | 0
(25 rows)
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46823 | 0
(1 row)
testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
16384
(1 row)
testdb=#
これは、もともと25件のレコードが1つのブロックに収まっていたものの、更新する際に(追記するための)空き領域が足りないため新しいブロックを作成した(テーブルファイルを拡張した)ということを示しています。
■「別ブロックへの更新処理」の何が問題か
では、この「別ブロックへの更新処理」の何が問題なのでしょうか。今まで見てきた「同じブロック内での更新」と比べて何が違うのでしょうか。
それは、「更新処理の際に別ブロックへのレコード追記を行うと、余分なI/O処理が発生する」ということです。
同一のブロック内での更新処理であれば、共有バッファ内の(8kBの)読み書きだけで処理が完結することが担保されます。データベースのI/O処理はブロック単位で行われるので、これは当然のことです。
それに対して、異なるブロックへの更新処理になると、「異なるブロックを読み込む」というI/O処理が発生する可能性がある、ということです。もう少し正確に言うと、
- 空き領域のあるブロックを探す
- 空き領域のあるブロックが見つからない場合は、ファイルを拡張して新しいブロックを作る
- ブロックをディスクから共有バッファに読み込む
- 共有バッファへ読み込んだブロックに対して新しいレコードを書き込む
そもそも、データベースにおいてはストレージが追記型構造であるかどうかに関わらず、ブロックへのアクセスを如何に削減するか、余計なブロックアクセスを減らすか、ということが重要です。
にも関わらずブロックを一杯まで使ってしまうと、異なるブロックへの更新処理が頻発し、ディスクI/Oが増えてしまう可能性が高まります。追記型のストレージアーキテクチャを持つPostgreSQLでは、この可能性が特に高くなります。
また、当然ながら更新の時だけではなく、(前回までに見てきたように)参照の時にもPostgreSQLの内部では古いタプルからチェーンを辿るようにして最新のタプルを探しますので、更新のチェーンが異なるブロックにまたがっているということは、参照の時にも余計なI/O処理を発生させることになり、パフォーマンスに影響を与えます。
■強制的に空き領域を確保して更新(UPDATE)に備える
というわけで、前述のような状況(別ブロックへの追記)が発生すると、「あぁ、同じブロックに空き領域があればなぁ」と考えるのが人情というものでしょう。この「UPDATEに備えて同じブロックに意図的に空き領域を確保しておく」ための仕組みが「FILLFACTOR」と呼ばれるものです。
これは、INSERTをする際には指定した割合の空き領域を確保しておき、UPDATEをする時にその空き領域を使う、という仕組みです。このように領域を予約しておくことによって、「更新処理の際に同じブロックの空き領域を使える」という可能性が飛躍的に高まります。
実際の動作を見てみましょう。
以下は、先の例とまったく同じレコードを25件INSERTしたテーブルの状態です。
先ほどと違うのは、同じ25件のレコードであるにも関わらず、今回は1ブロックに収まらず既に2ブロック使っていること、そして1ブロック目に少し空き領域があることです(最後のlp_offが1000バイト以上あります)。
この状態でUPDATE処理を行ってみます。
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46856 | 0
2 | 7568 | 1 | 311 | 46857 | 0
3 | 7256 | 1 | 311 | 46858 | 0
4 | 6944 | 1 | 311 | 46859 | 0
5 | 6632 | 1 | 311 | 46860 | 0
6 | 6320 | 1 | 311 | 46861 | 0
7 | 6008 | 1 | 311 | 46862 | 0
8 | 5696 | 1 | 311 | 46863 | 0
9 | 5384 | 1 | 311 | 46864 | 0
10 | 5072 | 1 | 311 | 46865 | 0
11 | 4760 | 1 | 311 | 46866 | 0
12 | 4448 | 1 | 311 | 46867 | 0
13 | 4136 | 1 | 311 | 46868 | 0
14 | 3824 | 1 | 311 | 46869 | 0
15 | 3512 | 1 | 311 | 46870 | 0
16 | 3200 | 1 | 311 | 46871 | 0
17 | 2888 | 1 | 311 | 46872 | 0
18 | 2576 | 1 | 311 | 46873 | 0
19 | 2264 | 1 | 311 | 46874 | 0
20 | 1952 | 1 | 311 | 46875 | 0
21 | 1640 | 1 | 311 | 46876 | 0
22 | 1328 | 1 | 311 | 46877 | 0
23 | 1016 | 1 | 311 | 46878 | 0
(23 rows)
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46879 | 0
2 | 7568 | 1 | 311 | 46880 | 0
(2 rows)
testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
16384
(1 row)
testdb=#
UPDATE処理を行った結果を見てみると、1番目のブロックのlp==1のレコードが削除され、同じブロックにlp==24として更新されていることが分かります。つまり、1番目のブロックに空き領域が確保されていたために、UPDATE処理の際にその領域を使うことができている、ということが分かります。
testdb=# UPDATE t1 SET uname = 'update 101 ' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46856 | 46881
2 | 7568 | 1 | 311 | 46857 | 0
3 | 7256 | 1 | 311 | 46858 | 0
4 | 6944 | 1 | 311 | 46859 | 0
5 | 6632 | 1 | 311 | 46860 | 0
6 | 6320 | 1 | 311 | 46861 | 0
7 | 6008 | 1 | 311 | 46862 | 0
8 | 5696 | 1 | 311 | 46863 | 0
9 | 5384 | 1 | 311 | 46864 | 0
10 | 5072 | 1 | 311 | 46865 | 0
11 | 4760 | 1 | 311 | 46866 | 0
12 | 4448 | 1 | 311 | 46867 | 0
13 | 4136 | 1 | 311 | 46868 | 0
14 | 3824 | 1 | 311 | 46869 | 0
15 | 3512 | 1 | 311 | 46870 | 0
16 | 3200 | 1 | 311 | 46871 | 0
17 | 2888 | 1 | 311 | 46872 | 0
18 | 2576 | 1 | 311 | 46873 | 0
19 | 2264 | 1 | 311 | 46874 | 0
20 | 1952 | 1 | 311 | 46875 | 0
21 | 1640 | 1 | 311 | 46876 | 0
22 | 1328 | 1 | 311 | 46877 | 0
23 | 1016 | 1 | 311 | 46878 | 0
24 | 704 | 1 | 311 | 46881 | 0
(24 rows)
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46879 | 0
2 | 7568 | 1 | 311 | 46880 | 0
(2 rows)
testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
16384
(1 row)
testdb=#
FILLFACTORの設定方法はCREATE TABLEにオプションを付ける方法、ALTER TABLEで設定する方法などがあります。詳細はマニュアルを参照してください。
CREATE TABLE
http://www.postgresql.jp/document/9.0/html/sql-createtable.html
ALTER TABLE
http://www.postgresql.jp/document/9.0/html/sql-altertable.html
なお、FILLFACTORの値は「10~100(%)」で指定しますが、テーブルにおける設定のデフォルトは「100」、つまり「空き領域無しで全部詰め込む」という設定になっています。つまり、何も設定しないとFILLFACTORの恩恵はまったく受けられませんのでご注意ください。
■まとめ
今回は、UPDATE処理の際にできるだけ同一ブロック内の空き領域を使うための「FILLFACTOR」の仕組みについて、その実際の動作を見ながら解説してきました。
FILLFACTORは、特に更新処理の多いワークロードで性能の安定に寄与します。一方で、「空き領域を確保しておく」という機能の特性上、テーブルファイル全体のサイズは大きくなります。
このようなトレードオフが存在しますので、自身のデータベースのワークロードのタイプをよく見極めて、これらの機能をうまく使いこなしていただければと思います。
では、また。