Quantcast
Channel: PostgreSQL Deep Dive
Viewing all articles
Browse latest Browse all 94

PostgreSQLのストレージアーキテクチャ(FILLFACTOR編)

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 19です。

昨日までのエントリで、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=#
この状態でレコードを更新すると、どうなるのでしょうか。

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=#
空き領域が無い状態でレコードを更新(UPDATE)すると、更新前の古いレコード(lp==1)は削除(t_xmaxが設定される)されますが、同じブロックに空き領域が無い(Page Pruningをしても空き領域を作れない)ため、新しいレコードは異なる新しい2番目のブロックに書き込まれています。新しいブロックが作成されたことで、pg_relation_sizeで取得されるテーブルサイズが8kBから16kBに増加していることが分かります。

これは、もともと25件のレコードが1つのブロックに収まっていたものの、更新する際に(追記するための)空き領域が足りないため新しいブロックを作成した(テーブルファイルを拡張した)ということを示しています。

■「別ブロックへの更新処理」の何が問題か


では、この「別ブロックへの更新処理」の何が問題なのでしょうか。今まで見てきた「同じブロック内での更新」と比べて何が違うのでしょうか。

それは、「更新処理の際に別ブロックへのレコード追記を行うと、余分なI/O処理が発生する」ということです。

同一のブロック内での更新処理であれば、共有バッファ内の(8kBの)読み書きだけで処理が完結することが担保されます。データベースのI/O処理はブロック単位で行われるので、これは当然のことです。

それに対して、異なるブロックへの更新処理になると、「異なるブロックを読み込む」というI/O処理が発生する可能性がある、ということです。もう少し正確に言うと、
  • 空き領域のあるブロックを探す
  • 空き領域のあるブロックが見つからない場合は、ファイルを拡張して新しいブロックを作る
  • ブロックをディスクから共有バッファに読み込む
  • 共有バッファへ読み込んだブロックに対して新しいレコードを書き込む
という処理が行われることになり、(パフォーマンス的に)最悪のケースではディスクをファイルを拡張したり、読み込んだり、といったディスクI/Oが発生することになり、このことがパフォーマンスに大きな悪影響を与えます。なぜなら、メモリへの操作に対してディスクI/Oは、3ケタ近く処理速度が遅いからです。

そもそも、データベースにおいてはストレージが追記型構造であるかどうかに関わらず、ブロックへのアクセスを如何に削減するか、余計なブロックアクセスを減らすか、ということが重要です。

にも関わらずブロックを一杯まで使ってしまうと、異なるブロックへの更新処理が頻発し、ディスクI/Oが増えてしまう可能性が高まります。追記型のストレージアーキテクチャを持つPostgreSQLでは、この可能性が特に高くなります。

また、当然ながら更新の時だけではなく、(前回までに見てきたように)参照の時にもPostgreSQLの内部では古いタプルからチェーンを辿るようにして最新のタプルを探しますので、更新のチェーンが異なるブロックにまたがっているということは、参照の時にも余計なI/O処理を発生させることになり、パフォーマンスに影響を与えます。

■強制的に空き領域を確保して更新(UPDATE)に備える


というわけで、前述のような状況(別ブロックへの追記)が発生すると、「あぁ、同じブロックに空き領域があればなぁ」と考えるのが人情というものでしょう。この「UPDATEに備えて同じブロックに意図的に空き領域を確保しておく」ための仕組みが「FILLFACTOR」と呼ばれるものです。


これは、INSERTをする際には指定した割合の空き領域を確保しておき、UPDATEをする時にその空き領域を使う、という仕組みです。このように領域を予約しておくことによって、「更新処理の際に同じブロックの空き領域を使える」という可能性が飛躍的に高まります。

実際の動作を見てみましょう。

以下は、先の例とまったく同じレコードを25件INSERTしたテーブルの状態です。

先ほどと違うのは、同じ25件のレコードであるにも関わらず、今回は1ブロックに収まらず既に2ブロック使っていること、そして1ブロック目に少し空き領域があることです(最後のlp_offが1000バイト以上あります)。

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処理を行ってみます。

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=#
UPDATE処理を行った結果を見てみると、1番目のブロックのlp==1のレコードが削除され、同じブロックにlp==24として更新されていることが分かります。つまり、1番目のブロックに空き領域が確保されていたために、UPDATE処理の際にその領域を使うことができている、ということが分かります。

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は、特に更新処理の多いワークロードで性能の安定に寄与します。一方で、「空き領域を確保しておく」という機能の特性上、テーブルファイル全体のサイズは大きくなります。

このようなトレードオフが存在しますので、自身のデータベースのワークロードのタイプをよく見極めて、これらの機能をうまく使いこなしていただければと思います。

では、また。

Viewing all articles
Browse latest Browse all 94