「FETCH FIRST 10 ROWS」でクエリが遅い? PostgreSQLの隠れた調整弁 `cursor_tuple_fraction` の話
やあ。最近、PostgreSQLのパフォーマンスチューニングでこんな経験はないかな?
「アプリ側で `LIMIT 10` をつけているのに、実行計画を見ると、なぜか重いフルスキャン(Seq Scan)を選択していて、結果が返ってくるまで数秒待たされる……」
これ、PostgreSQLを長く触っていると一度はぶち当たる壁なんだよね。インデックスも貼ってあるし、`LIMIT` も効いているはずなのに、オプティマイザがなぜか「全件スキャンしてソートしたほうが早い」と判断しちゃう。
実はこれ、PostgreSQLのオプティマイザが「カーソルでどれくらいの行数を取り出すか」を計算する際の、ある「決め打ち」が原因であることが多いんだ。今日はその正体である `cursor_tuple_fraction` について、現場目線で解説するよ。
—
「全件取得」を前提としたプランニングの罠
PostgreSQLのオプティマイザは、クエリの実行コストを計算するときに「このクエリは結果の何割くらいを読むのか?」という仮定を置くんだ。
ここで登場するのが `cursor_tuple_fraction` という設定項目。デフォルト値は `0.1`(10%)になっている。
これはどういうことかというと、「クエリがカーソル経由で実行された場合、とりあえず結果セットの最初の10%分くらいは読むだろう」という前提でコストを算出する、というものなんだ。
問題はここからだよ。
例えば、100万件あるテーブルに対して `SELECT FROM users ORDER BY created_at DESC LIMIT 10;` を投げたとしよう。
理論上、インデックスがあれば数件読むだけで済むはずだよね? でも、PostgreSQLはデフォルトの `0.1` を基準にするから、「100万件の10%である10万件分を処理するコスト」を見積もってしまうことがあるんだ。
その結果、「10万件も読むなら、インデックスを引くよりテーブルを全部舐めてクイックソートしたほうが早いな」という、「全件取得前提」の歪んだプランが生成されてしまうわけ。これが、`LIMIT` をつけても遅いクエリの正体だ。
—
実践:どう調整するのか?
この挙動を改善するには、セッションレベルで `cursor_tuple_fraction` を調整するのが一番手っ取り早い。
例えば、Webアプリの特定のエンドポイントなど、「常に数件しか取らない」と分かっている処理があるなら、このように設定してみよう。
— トランザクション内、あるいはセッション単位で設定
SET cursor_tuple_fraction = 0.0001;
— その後にクエリを実行
SELECT FROM users ORDER BY created_at DESC LIMIT 10;
こうすることで、「100万件の10%」ではなく「100万件の0.01%(=100件)」を基準にコスト計算してくれるようになる。これでオプティマイザは「あ、これならインデックスを辿るコストの方が圧倒的に低いな」と気づいてくれるはずだ。
—
注意点:魔法の杖ではない
もちろん、これを闇雲に全域で適用するのはおすすめしない。
- 汎用性が下がる: この設定をグローバル(`postgresql.conf`)で極端に小さくしてしまうと、逆に「全件取得したい」というクエリまでインデックスを無理やり使うようになり、全体的なパフォーマンスが悪化する可能性がある。
- 統計情報の鮮度: `cursor_tuple_fraction` をいじる前に、まずは `ANALYZE` が適切に走っているか、統計情報が古くないかを疑うのがエンジニアの鉄則だ。
僕が現場でよくやるのは、「特定の重いバッチや、どうしても遅いAPIエンドポイントの先頭でだけ設定する」というアプローチだね。
まとめ
`cursor_tuple_fraction` は、いわばオプティマイザの「先入観」を矯正するパラメータだ。
1. `LIMIT` をつけているのにフルスキャンが走っていたら、まずはここを疑う。
2. `SET` で値を小さくして、実行計画(`EXPLAIN`)がどう変わるか見てみる。
3. インデックス・スキャンが選ばれるようになったら大成功。
データベースはブラックボックスじゃない。オプティマイザが「なぜその判断をしたのか」という思考回路をトレースできるようになると、チューニングは一気に面白くなるよ。
もし君のプロジェクトで「どうしても実行計画がうまく倒れない」というクエリがあったら、一度試してみてくれ。健闘を祈る!
コメント