昨日までのエントリで、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が増えてしまう可能性が高まります。追記型のストレージアーキテクチャを持つ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は、特に更新処理の多いワークロードで性能の安定に寄与します。一方で、「空き領域を確保しておく」という機能の特性上、テーブルファイル全体のサイズは大きくなります。
このようなトレードオフが存在しますので、自身のデータベースのワークロードのタイプをよく見極めて、これらの機能をうまく使いこなしていただければと思います。
では、また。
我是一个商人,他通过上帝派来的贷款人本杰明·李(BenjaminLee)的贷款顾问的帮助,重振了他垂死的伐木业。我是叶卡捷琳堡的居民。嗯, 你是想创业, 解决你的债务, 扩大你现有的, 需要钱来购买用品。您是否在尝试获得良好信贷机制时遇到问题,我想让你知道本杰明先生会看穿你的。是解决所有财务问题的正确地方, 因为我是活生生的见证, 当别人正在寻找一种财务提升的方法时, 我不能把这一点留在自己身上。我希望你们都联系这个上帝发送者使用细节,如其他说,成为这个伟大的机会电子邮件的一部分:Lfdsloans@outlook.com或 WhatsApp/文本 +1-989-394-3740。
返信削除