Fillfactorという「余白」の美学:PostgreSQLの性能を極限まで引き出すために
PostgreSQLを長く触っていると、ある段階で必ず「なぜ、性能が頭打ちになるのか?」という壁にぶつかります。特に更新頻度の高いトランザクション環境では、チューニングの定石である「インデックスを減らす」「クエリを最適化する」といった処方箋だけではどうにもならないケースがある。
その解決の糸口として、今日はあえて「Fillfactor」という少し地味ながらも強力な武器について語ろうと思います。
なぜ、ページに「空き」を作る必要があるのか
PostgreSQLのアーキテクチャにおいて、最も厄介な敵の一つが「ページ分割(Page Split)」です。
PostgreSQLはデータを8KB(デフォルト)の固定長ページに格納します。更新(UPDATE)が発生した際、もし更新後の行が元のページに入り切らなければ、PostgreSQLはページを分割し、新しいページを割り当てます。これが頻発するとどうなるか。物理的なI/Oコストが跳ね上がるだけでなく、インデックスの断片化が急速に進み、最終的には検索性能の劣化という形でツケが回ってきます。
そこで登場するのが Fillfactor です。
Fillfactorは、テーブルやインデックスのページ内に「どれだけデータを詰め込むか」をパーセンテージで指定する設定値。例えば `FILLFACTOR = 90` と設定すれば、ページ全体の10%を最初から「空き」として確保しておきます。
一見、ストレージ効率を犠牲にしているように見えるかもしれません。しかし、この「余白」こそが、更新時のインデックス再配置や行のアップデートをページ内で完結させるための「バッファ」として機能するのです。
現場で直面する「Page Split」の兆候
私がトラブルシューティングで現場に入った際、最初に確認するのが `pgstattuple` 拡張です。
SELECT FROM pgstatindex(‘idx_my_table_column’);
ここで `leaf_fragmentation`(リーフページの断片化率)が異常に高い場合、あるいは `avg_leaf_density` が低いにもかかわらず更新待ちで性能が出ない場合、それは高頻度なページ分割のサインです。
特に、連番ではない値(UUIDなど)をインデックス化している場合や、頻繁にUPDATEが走るカラムを持つテーブルでは、デフォルトの `FILLFACTOR = 100` では遅すぎます。物理的に新しいデータを書き込む場所が足りず、インデックスの末尾が常に「ページ分割の戦場」になってしまうからです。
経験則:どこまで削るのが最適解か
では、Fillfactorをどこまで下げるべきか?
私の経験上、魔法の数字はありませんが、基準はあります。
- INSERTメインのテーブル: デフォルト(100)でOK。無理に下げる必要はありません。
- UPDATEが頻発するテーブル: 80〜90を検討する。
- インデックス(特にUUIDやランダムな更新が発生するもの): 70〜80まで落とすこともあります。
ここで注意してほしいのは、「下げれば下げるほど良い」わけではないという点です。Fillfactorを下げすぎると、キャッシュ効率が悪化し、スキャンするページ数が増えることで、結局メモリ(Shared Buffers)を無駄に浪費することになります。
トレードオフのバランスを見極めるには、ベンチマークが不可欠です。本番同等の負荷をかけた状態で、`pg_stat_user_indexes` を見ながら、インデックスの「更新効率」と「検索効率」のスイートスポットを根気強く探してください。
最適化のあとに待っているもの
Fillfactorの調整は、言ってみれば「データベースの呼吸を整える」作業です。
ページ分割によるI/O待ちが減れば、CPUは本来処理すべきクエリの実行プラン評価にリソースを割けるようになります。これは小手先の最適化以上に、システム全体の安定性を底上げするアプローチです。
もちろん、これを適用するには `VACUUM FULL` や `REINDEX` を伴うメンテナンスが必要になることもあります。だからこそ、このチューニングは、システムの特性を深く理解したエンジニアだけが実行を許される「特権」のようなものだと私は思っています。
「なぜこの設定値なのか?」と聞かれたときに、アーキテクチャの挙動を交えて答えられるか。それが、熟練エンジニアとそうでないエンジニアを分かつ境界線なのかもしれません。
皆さんのPostgreSQLが、より美しく、そして軽やかに動くことを願っています。次はどのパラメータを深掘りしましょうか?また次回お会いしましょう。
コメント