COUNT()が遅い原因はどこ?実行計画で見抜く高速化の手順【SQL】
この記事でわかること(結論と全体像)
COUNT(*) が重いときは、まず実行計画で「どこで時間とI/Oを使っているか」を確定させるのが最短です。
COUNT(*) の遅さは「COUNTだから遅い」のではなく、実行計画上の走査範囲や結合の増幅、条件式の書き方が原因であることがほとんどです。
つまり、速くするコツは COUNT の書き方をいじることではなく、読んでいる行数と読み方を減らすことにあります。
COUNT(*) が遅いと感じたら、まず「どのテーブルを何行読んでいるか」を疑うと迷いません。
テーブル行数が多いほど、少しの読み取り増加でも秒単位の差になります。
とくに大きなテーブルでは、数えたい行が少なくても「数えるまでに読む行」が多いと、体感速度が一気に悪くなります。
読み取りが増えると、CPUより先にディスクI/Oやバッファの競合がボトルネックになることもあります。
バッファ競合が起きると、他のクエリも巻き込んで遅くなることがあります。
また、COUNT(*) は画面表示やバッチ集計など幅広く使われるため、1本遅いだけでシステム全体の待ち時間に波及しやすいです。
ピーク時のCOUNTはロックやラッチの競合を引き起こし、他の処理まで遅くすることがあります。
実務では「遅いCOUNTのせいでAPI全体が詰まる」という形で問題が表面化しがちです。
この記事は「実行計画→WHERE条件→インデックス→条件式の書き換え→JOIN整理→要件見直し」の順で、現場でそのまま使える手順に落とし込みます。
「どこから手を付けるべきか」を迷わないように、原因の分類と改善の順序をセットで示します。
改善は「原因の切り分け」と「効果の検証」を繰り返すほど成功率が上がります。
改善の途中で計画が変わることがあるため、比較用の計画ログを残すのも有効です。
途中で迷いがちな「インデックスを貼ったのに遅い」「COUNT(1)に変えても変わらない」といったつまずきも、原因別に回収します。
DB製品ごとの細かな違いがあっても、速くする考え方は共通部分が多いので、再利用できる判断軸として読めるようにしています。
「正確に数える」こと自体が重いケースでは、要件に合わせて設計を変える考え方も扱います。
最後には、実務でよく使う「次があるか判定」や「集計テーブル」のような現実解も整理します。
COUNT(*)が遅いときの最短ルート(実行計画→条件→インデックス/結合)
最初に実行計画でフルスキャンや結合順序、推定行数と実行行数のズレを確認します。
実行計画は「速いか遅いか」ではなく「何をどれだけ読んでいるか」を数字で示すので、改善の当たりを付けるのに向いています。
見るべきポイントを固定すると、環境や人が変わっても同じ手順で判断できるようになります。
実行計画の読み方を固定すると、議論が「体感」から「数字」に移って意思決定が早くなります。
次に WHERE 句の条件がインデックスに合っているか、条件式がインデックスを殺していないかを点検します。
この段階では、同じ意味の条件でも書き方を変えるだけでインデックスが効くことがある点が重要です。
あわせて、条件に使っている列のデータ分布が偏っていないかも意識すると、無駄な最適化を避けられます。
ヒット率が高すぎる条件は、インデックスが使われても「結局たくさん読む」ので改善幅が小さくなります。
最後に JOIN の必要性と集約の位置を見直して、数える前に行数が増えない形へ寄せます。
特に集約の位置が後ろにあるほど、途中で増えた行を抱えてから数えるので、負荷が跳ねやすいです。
集約の前にフィルタできる場所があるかを探すだけでも、体感が大きく変わることがあります。
JOINの役割を「絞り込み」と「参照」に分けると、削れる処理が見つかりやすくなります。
改善の順番を守ると、無駄なインデックス追加や無意味なSQL修正を減らせます。
正確な総件数が必要かを最初に判断する
画面表示やページング目的なら、正確な総件数が必須ではないケースが多いです。
要件が許すなら近似値や別集計、キャッシュに逃がすだけで、根本的に速くできる場合があります。
たとえば「だいたい何件あるか」が分かれば良いなら、毎回重いCOUNTを回さない設計に変えられます。
総件数の表示は便利ですが、負荷と価値が釣り合っているかを一度見直す価値があります。
逆に、毎回のリクエストで総件数を出す要件が固いほど、DBに継続的な負荷がかかります。
一方で監査や課金など正確性が必須なら、SQLと設計の両面で「正確に数えるコスト」を下げる方向で改善します。
この判断を先にしておくと、必要以上に複雑な最適化へ突っ込まずに済みます。
「正確さ」「鮮度」「コスト」のどれを優先するかを言葉にしておくと、関係者との合意も取りやすくなります。
要件が曖昧なままだと、SQLだけで無理に解決しようとして長期的に運用が苦しくなります。
設計の選択肢を持つために、まずは要件を一段抽象化して整理するのが有効です。
COUNT(*)が重くなる主な原因
COUNT(*) が重くなる理由は、SQLエンジンが「数えるために、結局は多くの行を読まないといけない」状態になっているからです。
件数の計算は軽そうに見えても、条件に合う行を確定するには読み取りが必要なので、読み取り量がそのまま重さになります。
読み取り量が増える原因は、インデックスが無い場合だけではなく、あっても使えない形になっている場合も含まれます。
さらに、統計情報やキャッシュ状態によって同じSQLでも計画が変わり、急に遅くなることがあります。
パラメータの値が変わるだけで計画が変わり、遅いケースだけ発生することもあります。
ここでは原因を4つに分けて、後の手順と接続できるように整理します。
全件走査になっている
対象テーブルのほとんどの行を読むフルスキャンが起きると、COUNT(*) はその分だけI/Oが増えて遅くなります。
典型的には、WHERE が無い、条件の絞り込みが弱い、または条件式が原因でインデックスが使えないときに発生します。
絞り込みが弱い例として、ステータスが数種類しかない列での条件指定は、件数が多いと読まれる行が減りにくいです。
また、期間が広すぎる日付条件も、実質的に全件走査に近い読み方になることがあります。
対象期間を狭められないなら、パーティションや集計設計の検討が必要になる場合もあります。
実行計画上で Seq Scan や Full Table Scan のような走査が見えていたら、まずここを疑います。
I/Oが支配的な場合は、同時実行数が増えるほど劣化が目立ちます。
この原因は手順1の実行計画確認で切り分け、手順2と手順3で改善します。
WHERE条件に合うインデックスがない
WHERE 句でよく使う列に適切なインデックスが無いと、絞り込みができずフルスキャンに寄ります。
単一列のインデックスがあっても、複数条件の組み合わせ次第では効かないことがあります。
複合インデックスは列順が重要で、左端一致のルールを外すと期待どおりに使われません。
さらに、条件の順序はSQLの見た目ではなく、インデックスの並び順と実行計画が決める点が落とし穴です。
よくある失敗として、頻出条件はあるのに「その条件で検索するクエリが多い」事実がインデックス設計に反映されていないケースがあります。
クエリログやAPMで頻出条件を把握すると、インデックス設計の精度が上がります。
同じ列でも「等価」と「範囲」で最適なインデックス設計が変わることがあります。
この原因は手順2のインデックス見直しで改善します。
絞り込み条件がインデックスを使いにくい
列に関数を当てる、型変換を起こす、前方一致でない LIKE、OR で条件を広げる、といった書き方はインデックスを使いにくくします。
インデックスが存在しても「使える形」で条件を書けていないと、結局は多くの行を読んでしまいます。
とくに日付の加工や文字列の部分一致は、現場で頻出のボトルネックです。
また、暗黙の型変換が起きる比較は見た目で気づきにくく、環境差で急に遅くなる原因になります。
NULL の扱いが絡む条件も、想定よりヒット件数が増えて読み取りが増える原因になり得ます。
条件式の改善は小さな修正でも効果が大きいことがあるため、早めに点検すると効率的です。
条件の正規化をアプリ側で行うだけで改善するケースもあります。
この原因は手順3の条件式の書き換えで改善します。
JOIN後に件数を数えている
JOIN してから COUNT(*) を取ると、結合によって行が増えてから数えるため、読み取りも集約も重くなります。
1対多の結合では、意図せず重複行が発生して「数える行数」そのものが増えることがあります。
本当に必要なのは「親テーブルの件数」なのに、子側の増幅を抱えたまま数えているのが典型です。
結合がフィルタ目的なのか表示目的なのかを分けて考えると、不要なJOINを削りやすくなります。
また、JOINのキーにインデックスが無いと、結合自体が重くなってCOUNT以前の段階で遅くなります。
JOINの重さはCOUNTの問題に見えづらいので、実行計画で結合部分のコストも必ず確認します。
重複を避けるためのDISTINCTは、正しさの代わりにコストが増える点も意識します。
この原因は手順4のJOIN整理と、必要に応じて手順5の要件見直しで改善します。
COUNT(*)を遅くしやすいSQLの例
ここでは「なぜ遅いのか」をイメージできるように、よくある悪い例と改善方向を示します。
DB製品によって最適化の癖はありますが、遅くなるパターンはかなり共通しています。
まずは「こう書くと遅くなりやすい」を知って、実行計画と照らし合わせるのが近道です。
遅い例を知っておくと、実行計画の読み方も自然に身につきやすくなります。
例を読むときは「このSQLはどの原因カテゴリに当てはまるか」を意識すると理解が早いです。
同じSQLでもデータ量が増えると急に遅くなるため、将来の増加も想定して読みます。
関数をWHEREで使う(悪い例→良い例)
悪い例として、列に関数を当ててから比較すると、インデックスが使えなくなることがあります。
例として `WHERE DATE(created_at) = ‘2026-03-31’` のような条件は、created_at のインデックスがあっても効きにくいです。
良い例として、範囲条件に書き換えて `WHERE created_at >= ‘2026-03-31’ AND created_at < ‘2026-04-01’` のようにします。
この書き換えは意味が同じでも、列を加工せず比較できるため、インデックス利用へ寄せやすいです。
同様に、`WHERE CAST(user_id AS CHAR) = ‘123’` のような型変換も、元の型に合わせた比較へ寄せます。
もしアプリ側の入力が文字列なら、SQL側で列を変換するのではなく、入力値を数値へ正規化して渡すのが安全です。
文字コードや照合順序が絡む比較も、意図せずインデックスが効かなくなることがあるので注意します。
関数をどうしても使う必要があるなら、関数インデックスや生成列などの仕組みも検討対象になります。
関数インデックスを使う場合は、更新コストも含めて採用可否を判断します。
JOINしてからCOUNTする(悪い例→良い例)
悪い例として、詳細テーブルを JOIN してから `COUNT(*)` を取ると、結合で行が増えて遅くなります。
例として、注文と注文明細を JOIN して `COUNT(*)` すると、明細の行数を数えることになります。
良い例として、必要な粒度が注文件数なら、明細を JOIN せず注文テーブルだけで数えるか、先に集約してから JOIN します。
このとき「JOINしているのは条件を絞るためか」を確認し、フィルタなら EXISTS やサブクエリが有利な場合もあります。
重複が避けられない場合は `COUNT(DISTINCT orders.id)` などで意味を明確にします。
DISTINCT は正しさを担保できますが重くなる場合もあるので、必要なら先に対象を減らす工夫もセットで考えます。
JOINが必要でも列参照が不要なら、結合結果を返さない形へ寄せると最適化されやすいです。
IN と EXISTS のどちらが効くかは製品差があるため、実行計画で最終判断します。
OR条件・曖昧検索でプランが崩れる例
OR 条件は最適化が難しく、インデックスが効いていてもプランが崩れて走査量が増えることがあります。
例として `WHERE status = ‘PAID’ OR status = ‘CANCEL’` は、場合によっては広い範囲を読みに行きます。
また `LIKE ‘%keyword%’` のような部分一致は、通常のB-treeインデックスでは効かないことが多いです。
こうした条件は、クエリ分割や専用インデックス、検索方式の見直しが必要になります。
特に曖昧検索は要件次第で手段が変わるので、まず「どの程度の検索品質が必要か」を明確にします。
検索品質が高いほど、専用の検索エンジンやインデックス方式へ寄せるのが現実的になることがあります。
条件の書き換えだけで解決しないときは、要件と仕組みの両面から打ち手を探します。
OR条件をUNIONに分割する際は、重複排除の必要性も合わせて確認します。
COUNT(*)を高速化する具体手順
ここからは、実際に速くするための作業手順を順番どおりに説明します。
大事なのは「当てずっぽうでインデックスを増やさない」ことで、実行計画で根拠を取ってから手を打つことです。
改善は一度に全部やらず、実行計画が変わったかを確認しながら段階的に進めると失敗しにくいです。
現場では「とりあえずインデックス追加」が起きがちですが、結果として運用コストだけが増えることもあります。
手順どおりに進めると、改善のやり直し回数が減って結果的に早く終わります。
実行計画が変わったら、想定どおりにI/Oが減ったかも合わせて確認します。
1. 実行計画を確認する(見るべき観点を固定)
まず EXPLAIN や実行計画で、どのテーブルがどれだけ読まれているかを確認します。
最初に見るのは、フルスキャンになっていないか、インデックスが使われているかです。
次に推定行数と実行行数の差を見て、統計情報がズレていないかを疑います。
推定が大きく外れていると、最適化は本来選ぶべきでない結合方式や順序を選ぶことがあります。
JOIN がある場合は、結合順序と結合方式(Nested Loop など)が妥当かを確認します。
最後に集約の位置がどこにあるかを見て、数える前に行が増えていないかを判断します。
この時点でボトルネックが「走査」「条件式」「結合」「集約」のどれかに分類できると、改善が早くなります。
確認結果はメモとして残し、改善前後で同じ観点を比較できるようにすると再現性が上がります。
実行計画の比較は、改善の効果を説明する材料にもなるため、ログとして残す価値があります。
計画の差分を残しておくと、将来データが増えたときの再調査も速くなります。
2. WHERE句に合ったインデックスを見直す(複合/左端一致/選択度)
次に WHERE 句で使っている列と、実際に使われているインデックスを突き合わせます。
単一条件が多いなら単一列インデックスで足りることがあります。
複数条件がセットで使われるなら、複合インデックスを検討します。
複合インデックスは左端一致が基本なので、よく使う絞り込み列を左側に置きます。
絞り込みが強い列ほど選択度が高いので、先頭に置くと効きやすいことが多いです。
ただし、等価条件と範囲条件が混ざると設計が変わるので、実際のクエリ頻度を基に並び順を決めます。
ソートやGROUP BYの列順も加味すると、COUNT周辺以外の性能にも効く場合があります。
ただし更新が多いテーブルでは、インデックス追加が書き込みコストを増やす点も評価します。
インデックスを増やしたら、作成時間やメンテナンス時間も含めた運用面の影響を見積もります。
インデックスの増やしすぎは運用を苦しくするので、目的を明確にして最小構成を目指します。
3. 列への関数適用を避ける(型変換・日付加工・LIKE・ORの扱い)
インデックスを活かすには、列を加工せずに比較できる形へ条件式を寄せます。
日付は等価比較より範囲検索にし、型が違う比較はリテラル側を列の型に合わせます。
LIKE は前方一致の `LIKE ‘abc%’` ならインデックスが効く場合があります。
部分一致の `LIKE ‘%abc%’` は、全文検索やN-gramなど別方式を検討します。
OR 条件はクエリを分割して UNION にするなど、最適化しやすい形に変えるのが有効なことがあります。
また、IN 句への置き換えが可能なら、オプティマイザが扱いやすい場合もあります。
最終的には実行計画でインデックスが使われたかを必ず確認します。
条件式を直したのに計画が変わらない場合は、統計情報やヒントの有無など、別の要因も疑います。
条件式の改善は、テーブルスキャンをインデックスレンジスキャンに変えるような大きな差になることがあります。
条件式の書き換えは副作用が出やすいため、意味が同じかをテストで確認します。
4. 不要なJOINを減らす(重複回避の選択肢と判断軸)
COUNT(*) で必要なのは「どの粒度の件数か」を明確にすることです。
親テーブルの件数が欲しいなら、子テーブルを JOIN しない形が最も速いです。
JOIN が必要でも、先に親側で絞り込んでから結合すると行の増幅を抑えられます。
重複が起きるなら、DISTINCT を使うか、子側を先に集約してから結合します。
判断軸は「重複が許されるか」「欲しい粒度はどこか」「結合の目的はフィルタか参照か」です。
さらに、結合先が巨大でフィルタにしか使っていないなら、EXISTS に寄せると計画が安定することがあります。
JOINのためだけに列を参照していないかを見直すと、削れる結合が見つかることがあります。
結合の順序を変えるだけでも効果が出る場合があるので、実行計画で順序と方式を確認します。
重複排除をするなら、どのキーで重複が発生しているかを先に特定します。
重複排除の手段は、正しさだけでなくコストも含めて選びます。
5. 本当に正確な総件数が必要か見直す(代替案の使い分け)
ページング表示のための総件数なら、近似値や遅延更新の件数でも要件を満たすことがあります。
例えば、別テーブルで日次集計しておき、画面はその値を使う設計にできます。
頻繁に同じ条件で数えるなら、結果をキャッシュして再利用するだけでも劇的に速くなります。
正確性が必須なら、ここまでのSQLとインデックス改善で「正確に数えるコスト」を下げます。
要件見直しは逃げではなく、システム全体の負荷を下げる正攻法として扱うのがポイントです。
「どこまで正確ならOKか」を決めるだけで、実装の選択肢が増えることもあります。
近似やキャッシュを採用するなら、どのタイミングで更新するかを決めて不整合の影響を抑えます。
更新タイミングを決めるときは、利用者が許容できる遅延の幅を明確にします。
許容できる遅延が決まると、現実的な設計に落とし込みやすくなります。
COUNT(*)高速化で確認したいポイント
改善したら終わりではなく、再現性のある方法で「本当に速くなったか」を確認します。
また、速くなった理由を説明できる状態にしておくと、似た問題の再発防止にも役立ちます。
測り方が曖昧だと「速くなった気がする」だけで終わるので、評価軸を固定します。
本番環境での体感に寄せるために、できる範囲で同等のデータ量と同等の負荷条件で測る意識も大切です。
評価軸を固定すると、改善が偶然ではなく再現できる形で積み上がっていきます。
評価前後で条件が変わると比較できないため、同じパラメータで測ることを徹底します。
実行時間だけでなくI/O・行数(推定/実行)を見る
実行時間は環境要因でブレるので、I/Oや読み取り行数の変化もセットで見ます。
実行計画の推定行数と実行行数が近づいたかを確認します。
同じ条件で複数回測り、キャッシュの影響を受けていないかも意識します。
統計情報が古いと判断が難しいので、必要なら統計情報更新も検討します。
加えて、改善前後で取得している行数が減っていないなら、インデックス追加だけでは根本解決になっていない可能性があります。
計画の比較は、差分として残しておくと、後から原因説明をする場面で役立ちます。
I/Oが減っていないのに速くなった場合は、キャッシュの影響を疑って追加で測定します。
指標を揃えることで「速いけど危ない改善」を避けられます。
指標が揃っているほど、改善の判断がブレにくくなります。
インデックス追加の副作用(更新コスト/ストレージ)を評価する
インデックスを増やすほど検索は速くなりやすいですが、更新や挿入が遅くなりやすいです。
ストレージも増えるので、テーブルの更新頻度とトレードオフで判断します。
不要になったインデックスは整理し、長期的に運用しやすい状態を保ちます。
「読み取りが速いが書き込みが遅い」状態になっていないかを、アプリ全体の指標で確認します。
保守の観点では、似た役割のインデックスが増えすぎていないかも定期的に棚卸しします。
削除候補のインデックスは、実際に使われていないことを確認してから整理します。
インデックスの利用状況を監視できるなら、定期レポート化すると整理が楽になります。
定期レポートがあると、インデックス整理を計画的に進められます。
COUNT(1)やCOUNT(列)に変えると速くなるのか
COUNT(*) の代わりに COUNT(1) を使うと速いという話を見かけますが、基本的に大きな差は期待しにくいです。
本質は「どれだけ行を読むか」と「どんな計画で読むか」にあります。
表記の差で悩むより、読ませる行を減らす改善に時間を使う方が結果に直結します。
とはいえ、表記の違いで意味が変わるケースがあるので、速度より先に意味の確認を行います。
速度の議論は、実行計画が同じであることを確認してから進めると無駄が減ります。
表記を変える前に、まずボトルネックが走査や結合にあるかを確認します。
COUNT(*)とCOUNT(1)の速度差は基本的に期待しにくい
多くのDBでは COUNT(*) と COUNT(1) は同等に最適化され、実行計画も同じになりやすいです。
表記を変えるより、WHERE とインデックス、JOIN の見直しの方が効果が出やすいです。
迷ったら実行計画を比較し、差が無いなら表記の議論に時間を使わない方が良いです。
実務では「速さの差」より「意味が同じか」を優先し、読み手が誤解しない表記を選ぶのが安全です。
「COUNT(1)の方が速い」という固定観念は、検証してから採用する姿勢が無難です。
実装が複数人に渡るなら、表記を統一してレビューコストを下げることも価値があります。
表記統一は、性能よりも運用の安定に効くことが多いです。
運用が安定すると、性能問題の切り分けも早くなります。
COUNT(列)は意味が変わる点に注意する(NULLと要件)
COUNT(列) は NULL を数えないため、COUNT(*) と同じ意味にはなりません。
例えば欠損がある列で COUNT(列) を使うと、件数が小さく出て要件を満たさない可能性があります。
「NULLを除いた件数」が欲しいときだけ COUNT(列) を選び、目的に合うかを必ず確認します。
「速いから使う」ではなく「要件がそれを求めているから使う」という整理にすると事故を防げます。
NULLの扱いは集計全般でミスが起きやすいので、仕様として明文化しておくと安心です。
COUNT(列)を採用するなら、どの列を数えるかで意味が変わる点も合わせて共有します。
列のNULL率が高いほど、COUNT(*)との差が大きくなることも意識します。
差が大きいほど、要件確認の重要度も上がります。
よくある質問(Q & A)
最後に、COUNT(*) でつまずきやすい質問をまとめます。
実行計画と条件式、そしてデータの分布を合わせて見ると解決が早くなります。
質問を読むときは、どの原因カテゴリに当てはまるかを先に当てると、調査が短縮できます。
困ったときほど、原因を分類して「次に見るもの」を固定するのが近道です。
現場では複数の原因が重なることも多いので、まず支配的な原因を1つ決めて潰します。
原因を1つずつ潰すと、残りの問題が見えやすくなります。
インデックスを貼ったのに遅いのはなぜ?(統計情報/選択度/条件式/JOIN膨張)
インデックスが貼ってあっても、条件式が関数や型変換でインデックスを使えなくしている場合があります。
絞り込みが弱くて多くの行がヒットするなら、インデックスを使っても結局は大量に読んで遅くなります。
統計情報が古いと、最適化が誤った計画を選ぶことがあるので、推定行数と実行行数のズレを確認します。
JOINで行が増えていると、インデックス以前に「数える対象の行数」が大きすぎることがあります。
結局は実行計画で「どこで増えているか」を見つけ、増える前に絞れる形へ寄せます。
同じクエリでもパラメータ値によって計画が変わる場合があるので、遅いケースの値で検証します。
改善が一点突破で効かないときは、条件式とインデックスとJOINの順に戻って再点検します。
計画が変わりやすい環境では、複数パターンの値で計画を確認します。
複数パターンで確認すると、再発の芽を早めに潰せます。
大規模テーブルで総件数を出す現実解は?(近似/別集計/キャッシュ/ページング)
正確な総件数が不要なら、近似値や別集計で高速化するのが現実的です。
例えば日次で集計テーブルを更新し、画面はその件数を使うと負荷が安定します。
条件の種類が限定されるなら、条件ごとにキャッシュして再利用する設計も有効です。
ページング目的なら、総件数の代わりに「次があるかどうか」を判定する方式に変える選択肢もあります。
要件を一段抽象化して「ユーザーが知りたいのは何か」を確認すると、無理のない設計に落ちやすいです。
総件数の表示がUXとして本当に価値があるかを見直すだけで、負荷を大きく下げられることもあります。
現実解を選ぶときは、正確性よりも可用性や応答性を優先すべき場面があることも意識します。
現実解は「正確に数える」以外の価値を満たすための選択だと理解すると判断しやすいです。
価値の整理ができると、関係者の合意も取りやすくなります。
まとめ
COUNT(*) が重いときは、まず実行計画でボトルネックを確定させるのが最短です。
次に WHERE 条件をインデックスが使える形へ整え、必要なら複合インデックスを設計します。
最後に JOIN と集約の位置を見直して、数える前に行数が増えない形へ寄せます。
要件が許すなら、正確な総件数にこだわらず近似や別集計、キャッシュも選択肢に入れます。
迷ったときは「読ませる行数を減らせているか」と「実行計画が改善しているか」の2点に立ち返ると、判断がブレにくいです。
継続的に運用するなら、頻出クエリの実行計画を定期的に観測し、データ増加に合わせて見直す習慣も効果的です。
改善は一度で終わらず、データ量の増加に合わせて定期的に手当てする前提で設計すると安定します。
定期観測の仕組みを作ると、問題が大きくなる前に手を打てます。