Day18 後半のゴール
「“どこが遅いか”から一歩進んで、“なぜ遅いか”まで言えるようになる」
前半で「どのノードが重いか」を特定するところまでは来ました。
後半では、そこからもう一歩踏み込んで「なぜそこが重くなっているのか」を分解し、
“原因のタイプ”まで言えるようになることを目指します。
ボトルネックの“原因タイプ”をざっくり分類する
「設計・インデックス・統計・呼び出し方」
まず、ボトルネックの原因をざっくり4つに分けておきます。
設計の問題
テーブルが巨大なのに期間条件がない。
正規化されておらず、同じ情報を何度も読んでいる。
インデックスの問題
必要なインデックスがない。
インデックスはあるが、クエリの書き方のせいで使われていない。
統計情報・プランナーの問題
EXPLAIN の rows と actual rows が大きくズレている。
そのせいで、プランナーが「間違った実行計画」を選んでいる。
呼び出し方の問題
1回のクエリは速いが、同じクエリを何十回も呼んでいる。
N+1 クエリになっていて、アプリ側のループがボトルネック。
Day18 後半では、「特定したボトルネックが、このどれに近いか」を考える癖をつけます。
例題1:設計+インデックスの問題を切り分ける
「“ordersのSeq Scanが重い”をもう一歩分解する」
前半の例をもう一度使います。
Seq Scan on orders o (actual time=0.050..250.000 rows=800000 loops=1)
Filter: (ordered_at >= ...)
ここから分かるのは、「orders を全件なめて、期間条件で絞っている」ということでした。
これを原因タイプに落とすと、こう分解できます。
設計側の問い
本当に「全期間の orders」から絞る必要があるのか。
アプリの仕様として、「直近1年だけ見ればいい」などの制約はないか。
もし「直近1年だけでいい」なら、テーブルを年ごとに分ける(パーティション)という設計の話になります。
インデックス側の問い
ordered_at にインデックスはあるか。
あるなら、WHERE ordered_at >= ... でちゃんと使われているか。
EXPLAIN ANALYZE で、Index Scan ではなく Seq Scan になっているなら、
インデックスがないか、統計情報のせいで使われていないかを疑えます。
このように、「Seq Scan が重い」という事実を、
「設計の問題か」「インデックスの問題か」に分けて考えることで、
次の一手が具体的になります。
例題2:インデックスが“使われていない”パターン
「クエリの書き方が原因になっていないかを見る」
インデックスがあるのに使われていない、というパターンもよくあります。
例えば、こういうインデックスがあるとします。
CREATE INDEX idx_orders_ordered_at
ON orders (ordered_at);
SQLなのに、クエリがこうなっている。
SELECT *
FROM orders
WHERE DATE(ordered_at) = CURRENT_DATE;
SQLこれを EXPLAIN ANALYZE すると、Seq Scan になりがちです。
理由は、「DATE(ordered_at) という関数をかけてしまっているから」です。
インデックスは「生の ordered_at」に対して張られているので、
関数を通した値にはそのまま使えません。
この場合の“原因タイプ”はこう言えます。
「インデックスはあるが、クエリの書き方(関数をかけているせい)で使われていない」
ボトルネック特定の観点では、
インデックスがないのか
インデックスがあるのに使われていないのか
を分けて考えることが重要です。
例題3:統計情報・プランナーの問題
「rowsの見積もりと実測がズレているとき」
EXPLAIN ANALYZE の出力で、こういうのを見たとします。
Index Scan on orders (cost=0.43..500.00 rows=1000 width=...)
(actual time=0.050..300.000 rows=500000 loops=1)
ここで注目したいのは、
コスト側 rows = 1000
actual rows = 500000
という大きなズレです。
これは、「プランナーが orders の行数や分布を過小評価している」可能性を示しています。
その結果、本当は別の実行計画(例えば別のJOIN順序)を選ぶべきなのに、
間違ったプランを選んでいるかもしれません。
この場合の“原因タイプ”はこう言えます。
「統計情報が古く、プランナーの見積もりが外れているせいで、実行計画の選択が最適でなくなっている」
ボトルネック特定の観点では、「rows の見積もりと実測が桁違いにズレていないか」を見るだけでも、
「これは統計の問題かもしれない」という当たりが付けられます。
例題4:呼び出し方の問題(N+1クエリ)
「SQL自体は速いのに、全体として遅い」
ボトルネックは、必ずしも1本のSQLの中にあるとは限りません。
アプリ側の呼び出し方が原因になることも多いです。
例えば、こういうコードを想像してください。
orders = SELECT * FROM orders WHERE user_id = 123;
for each order in orders:
items = SELECT * FROM order_items WHERE order_id = order.id;
SQLここで、order_items を取るクエリは1回あたりは速いかもしれません。
でも、注文が100件あれば、100回同じようなクエリが走ります。
この場合の“原因タイプ”はこう言えます。
「SQL自体は軽いが、呼び出し方がN+1になっていて、全体として遅くなっている」
ボトルネック特定の観点では、
1回のクエリが遅いのか
同じクエリが何度も呼ばれているのか
を分けて見ることが重要です。
EXPLAIN ANALYZE だけでは分からないので、
アプリ側のログやプロファイラもセットで見る必要があります。
ボトルネック特定の“言語化テンプレ”を持つ
「原因タイプ+具体的な場所+証拠」
最後に、「どう言語化するか」の型を1つ持っておくと便利です。
例えば、こういう形です。
「このクエリが遅い主な原因は、orders テーブルに対する Seq Scan が 250ms かかっていて 80万行流れていることであり、EXPLAIN ANALYZE の結果から、WHERE ordered_at >= ... に対応するインデックスが存在しない(または使われていない)ことが分かるので、“インデックス設計の問題”がボトルネックタイプだと判断できる。」
あるいは、
「このAPIが遅い主な原因は、1リクエスト内で SELECT * FROM order_items WHERE order_id = ? が 100回以上呼ばれていることであり、各クエリ自体は 5ms 程度だが、合計すると 500ms 以上になっているので、“呼び出し方(N+1)の問題”がボトルネックタイプだと判断できる。」
Day18 後半では、このレベルで「原因タイプ+具体的な場所+証拠」をセットで言えるようになることを目標にしてください。
Day18 後半のまとめ
ボトルネック特定を一歩進めるには、「どのノードが重いか」だけでなく、「それが設計の問題なのか(期間条件やパーティションの不足)」「インデックスの問題なのか(ない/使われていない)」「統計情報・プランナーの問題なのか(rows の見積もりと実測のズレ)」「呼び出し方の問題なのか(N+1クエリなど)」という“原因タイプ”まで落とし込んで考えることが重要になる。EXPLAIN ANALYZE で Seq Scan・Sort・Index Scan の time と rows を見ながら、「orders の Seq Scan が250msで80万行流れている」「order_items で Seq Scan+Sort のあとに LIMIT している」「インデックスはあるが関数をかけていて使われていない」「rows の見積もりが1000なのに実際は50万」といった具体的な証拠を拾い、それを「インデックス設計の問題」「統計の問題」などのタイプに結びつけて言語化することが、Day18 後半の着地点になる。
