スキーマ設計ファースト:早期のクエリチューニングよりも速いアプリ
初期のパフォーマンス改善は多くの場合スキーマ設計で得られます。適切なテーブル、キー、制約が遅いクエリや後方修正のコストを防ぎます。

スキーマとクエリ最適化:ここで言う意味
アプリが遅く感じられると、まず「SQLを直そう」と考えがちです。その直感は理解できます:単一のクエリは見えて計測でき、責任を負わせやすいからです。EXPLAIN を実行し、インデックスを追加し、JOIN を調整すると、たまに即効の改善が見られます。
しかしプロダクト初期では、速度の問題は特定のクエリ文よりも「データの形」に起因することが同じくらい多いです。スキーマがデータベースと戦わせる設計だと、クエリチューニングはいたちごっこになりがちです。
スキーマ設計(平易な説明)
スキーマ設計とはデータの整理方法です:テーブル、カラム、リレーション、ルールをどう決めるか。決定には次のようなものが含まれます:
- どんな「実体」が独立したテーブルに値するか(users, orders, events など)
- テーブル同士の関係(1対多、多対多)
- 一意や必須にすべき項目(制約)
- 状態や履歴の表現方法(タイムスタンプ、ステータス、監査レコード)
良いスキーマ設計は、データに対する自然な問いかけがそのまま高速な照会になるようにします。
クエリ最適化(平易な説明)
クエリ最適化はデータの取得・更新方法を改善することです:クエリを書き換え、インデックスを追加し、不必要な処理を減らし、大規模スキャンを誘発するパターンを避けます。
両方重要だが、順序が重要
この記事は「スキーマが良くてクエリが悪い」という話ではありません。順序の話です:まずデータベースのスキーマの基礎を正しく作り、その後で本当に必要なクエリをチューニングする、ということです。
これで、なぜ初期のパフォーマンスがスキーマの決定に支配されるのか、スキーマが本当のボトルネックかどうかを見分ける方法、そして成長に合わせて安全に進化させる方法を学べます。対象はプロダクトチーム、創業者、現実的なアプリを作る開発者向けで、DB専門家向けではありません。
なぜスキーマ設計が初期のパフォーマンスを左右するのか
初期のパフォーマンス問題は、巧妙なSQLというよりも、データベースが触らされるデータ量に関係することが多いです。
構造がスキャン量を決める
クエリの選択性はデータモデルに依存します。status、type、owner を構造化されていないフィールドに入れていたり、テーブルが一貫しない形で分散していると、データベースはマッチする行を探すために多くの行を走査しなければなりません。
良いスキーマは検索空間を自然に狭めます:明確なカラム、一貫したデータ型、適切に区切られたテーブルにより、クエリは早い段階でフィルタリングでき、ディスクやメモリから読むページ数が減ります。
キーが無いとコストが増える
主キーや外部キーがない(あるいは強制されていない)と、リレーションが推測に頼ることになり、クエリ層に余計な仕事を押し付けます:
- インデックス化された結合経路がないため、結合が大きくなる。
- 重複、NULL、あるいは「ほぼ一致」する値を補償するためにフィルタが複雑化する。
制約がないと不整合なデータが蓄積し、行が増えるほどクエリは遅くなります。
インデックスはスキーマに従う(万能ではない)
インデックスは、予測可能なアクセス経路(外部キーによる結合、明確なカラムでのフィルタ、一般的な列でのソート)にマッチするとき最も有用です。重要な属性が間違ったテーブルにあったり、意味が混在したカラムに入っていたり、テキスト解析に頼っていると、インデックスでは救えません—依然として大量のスキャンや変換が必要になります。
デフォルトで速くする
クリーンなリレーション、安定した識別子、妥当なテーブル区切りがあれば、多くの日常的なクエリが「デフォルトで速く」なります。触るデータ量が少なく、単純でインデックスに優しい述語が使えるからです。クエリチューニングは仕上げのステップになり、恒常的な火消しではなくなります。
初期段階の現実:変更は常に起こる
初期プロダクトには「要件の安定」はなく、実験があります。機能が出ては書き直され、消える。小さなチームはロードマップ、サポート、インフラをこなしつつ、古い決定を見直す時間が限られています。
最もよく変わるもの
最初に変わるのはSQL文ではなく、データの意味です:新しいステータス、新しい関係、"これも追跡する必要がある" というフィールド、ローンチ時に想像していなかったワークフローなど。この変化こそが初期にスキーマ選択が重要な理由です。
後でスキーマを直すのがクエリを直すより難しい理由
クエリを書き換えるのは多くの場合ローカルで可逆的です:改善をデプロイして計測し、必要ならロールバックできます。
スキーマを書き換えるのは違います。実際の顧客データを保存した後では、構造的な変更はプロジェクトになります:
- テーブルロックや書き込み遅延を引き起こすマイグレーション
- 新しいカラムを埋めるためのバックフィル
- 移行中にアプリを動かし続けるためのデュアルライトやシャドウテーブル
- オンラインでできない場合のダウンタイムリスク
良いツールがあっても、スキーマ変更は調整コストを招きます:アプリコードの更新、デプロイ順序、データ検証などです。
初期決定が複利的に効いてくる
データベースが小さいときは、粗いスキーマでも「問題ない」ように見えます。行数が数千から数百万に増えると、同じ設計がより大きなスキャン、重いインデックス、コストの高い結合を生み出します—そして新機能はその上に積み重なります。
初期の目標は完璧さではなく、プロダクトの学びごとに毎回リスキーなマイグレーションを強いることなく変化を吸収できるスキーマを選ぶことです。
遅いクエリを防ぐ設計の基本
初期の多くの「遅いクエリ」問題は、SQLトリックではなくデータモデルの曖昧さに起因します。スキーマがレコードの意味や関係を不明瞭にしていると、すべてのクエリは書くのも実行するのも維持するのも高コストになります。
コアエンティティを小さく始める
プロダクトにとって本質的に必要なものを名前で定義して始めます:users、accounts、orders、subscriptions、events、invoices 等。次に関係を明示します:1対多、多対多(通常は結合テーブルで)、および所有権(誰が何を「含む」か)。
実践的なチェック:各テーブルについて「このテーブルの行は ___ を表す」と文を完成できるべきです。できないなら、そのテーブルは概念を混ぜており、後で複雑なフィルタや結合を強いる可能性があります。
命名と所有権は“退屈”なくらい一貫させる
一貫性は偶発的な結合や混乱したAPI挙動を防ぎます。命名規則(snake_case vs camelCase、*_id、created_at/updated_at)を選び、それを守ってください。
またフィールドの所有先を決めます。たとえば billing_address は注文(その時点のスナップショット)に属するのか、ユーザー(現在のデフォルト)に属するのか?どちらも妥当ですが、明確な意図なしに混ぜると「真実を突き止める」ための遅くてエラーの多いクエリが生まれます。
実態に合ったデータ型を選ぶ
ランタイム変換を避ける型を使いましょう:
- タイムゾーンルールを理解した上でのタイムスタンプを使う。
- 金額には浮動小数点ではなく小数(decimal)を使う。
- 既知のカテゴリには enum / 参照テーブルを使う。
型が間違っていると、データベースは効率的に比較できず、インデックスの有用性が下がり、クエリでキャストが必要になることが増えます。
計画なしに事実を重複させない
同じ事実を複数箇所に保存すると(例:order_total と sum(line_items))、乖離が生じます。派生値をキャッシュする場合は、ソース・オブ・トゥルースを定義し、ドキュメント化し、一貫して更新する計画を立てます(多くの場合アプリロジックと制約による)。
キーと制約:データ整合が速さの出発点
速いデータベースは通常「予測可能」なデータベースです。キーと制約は不可能な状態(欠けた関係、重複ID、アプリが想定した意味と違う値)を防ぎ、データを予測可能にします。そのクリーンさはクエリプランの立てやすさに直結します。
主キー:すべてのテーブルに安定した識別子を
各テーブルは主キー(PK)を持つべきです:行を一意に識別し変更されないカラム(または小さなカラム集合)。これは効率的な結合、キャッシュ、安全な参照を可能にします。
安定したPKがあれば回避的な工夫が不要になります。識別子がないと、アプリがメール・名前・タイムスタンプや複数カラムの束で行を識別しようとし、広いインデックスや遅い結合、値が変わったときの例外が増えます。
外部キー:オプティマイザを助ける整合性
外部キー(FK)はリレーションを強制します:orders.user_id は users.id を指すべき、など。FKがないと無効な参照(削除されたユーザーの注文や存在しない投稿のコメント)が入り込み、クエリは防御的にフィルタしたり left-join を使って null を処理したりします。
FKがあるとクエリプランナーは結合をより確信を持って最適化でき、孤立行(orphan)によるテーブル・インデックスの膨張を防げます。
クリーンで速いデータのための制約
制約は官僚的なものではなくガードレールです:
- UNIQUE は重複を防ぎ、余計なルックアップやクリーンアップを防ぐ(例:一意の
users.email)。 - NOT NULL は三値論理を避け、意外な null 処理分岐を減らす。
- CHECK は値を既知の集合に制限する(例
status IN ('pending','paid','canceled'))。
データがクリーンであればクエリは単純になり、フォールバック条件や余計な結合が減ります。
遅くする一般的なアンチパターン
- 外部キーがない:後で孤立行の掃除や複雑なクエリが待っている。
- 重複した email フィールド(例:
users.emailとcustomers.email):身元衝突や重複インデックスを招く。 - フリーフォームのステータス文字列:
Cancelledとcanceledのようなタイプミスがフィルタやレポートを壊す。
早くするなら、悪いデータを保存しにくくすること。データベースはよりシンプルなプラン、小さいインデックス、少ないパフォーマンスの驚きで報いてくれます。
正規化 vs 非正規化:実用的なバランス
正規化は単純な考え方です:各事実は一箇所に保存し、データベース中に値を複製しない。値が複数テーブルやカラムにコピーされると、更新が危険になり、片方だけ変わって矛盾が生じます。
正規化(デフォルト):一つの事実は一つの場所
実務では正規化はエンティティを分け、更新がクリーンで予測可能になるようにします。例えば製品の名前と価格は products テーブルにあり、各注文行に繰り返して入れるべきではありません。カテゴリ名は categories にあり、IDで参照します。
これにより:
- データ重複が減る(ストレージと矛盾が減る)
- 更新ミスが減る(1回変更すればどこでも反映)
- 「どの値が正しい?」というバグが減る
過度な正規化が害になるとき
正規化をやりすぎて日常的な画面表示のために多数の小さなテーブルを常に結合しなければならなくなると、一般的な読み取りが遅く複雑になります。
典型的な初期症状:シンプルなページ(例:注文履歴リスト)に6〜10テーブルの結合が必要で、パフォーマンスがトラフィックやキャッシュの温まりに依存する。
実用的アプローチ:中核事実は正規化、ホットな読み取りは非正規化
実用的なバランスは:
- 中核事実と所有権は正規化(ソース・オブ・トゥルース)。
productsに属性、categoriesに名前、外部キーで関係を保つ。 - 最も熱い読み取りのために慎重に非正規化—利点と整合性維持方法を説明できる場合のみ。
非正規化は頻繁なクエリを安くするために小さなデータを意図的に複製することです(結合を減らしリストを高速化)。重要なのは 慎重に 行い、複製フィールドの更新方法を計画すること。
例:製品、カテゴリ、注文行
正規化されたセットアップの例:
products(id, name, price, category_id)categories(id, name)orders(id, customer_id, created_at)order_items(id, order_id, product_id, quantity, unit_price_at_purchase)
微妙な利点:order_items は unit_price_at_purchase を保存しています(非正規化の一種)。これは製品価格が後で変わっても購買時の履歴精度を保つために必要で、意図的かつ安定した複製です。
最も一般的な画面が「注文とアイテムのサマリ」なら、order_items に product_name を複製して products を毎回結合するのを避ける選択もあり得ます—ただし同期方法を用意するか、購入時のスナップショットとして受け入れることが前提です。
インデックス戦略はスキーマに従う、逆ではない
インデックスは魔法の「速さボタン」として扱われがちですが、基礎のテーブル構造が意味を成しているときにだけ効果的です。もしカラム名を頻繁に変えたり、テーブル分割を変えたり、レコードの関係を作り直しているなら、インデックスも頻繁に入れ替わります。インデックスは、カラムやアプリがそれらでフィルタ/ソートするやり方が十分に安定しているときに最も有効です。
アプリがよく聞く質問から始める
完璧な予測は不要ですが、重要なクエリの短いリストは必要です:
- "メールでユーザーを見つける"。
- "顧客の最近の注文を表示する"。
- "ステータス別に請求書を一覧、最新順"。
これらは直接どのカラムにインデックスが必要かを教えてくれます。これが言えないなら、それはたいていスキーマの明快さの問題であって、インデックスの問題ではありません。
複合インデックスを平易に説明
複合インデックスは複数カラムをカバーします。カラムの順序が重要で、DBは左から右へ使える部分を有効に使います。
たとえば customer_id でフィルタして created_at でソートすることが多ければ、(customer_id, created_at) のインデックスは有用です。逆の (created_at, customer_id) は同じクエリに対して同じ効果を発揮しないことがあります。
全てにインデックスを張らない
追加インデックスにはコストがあります:
- 書き込みが遅くなる:insert/update ごとに各インデックスを更新する必要がある。
- ストレージが増える:インデックスはDBサイズの大部分になり得る。
- 複雑さが増す:多すぎるインデックスは保守とチューニングを難しくする。
クリーンで一貫したスキーマは、実際のアクセスパターンと一致する小さな「正しい」インデックス群に絞れるようにして、書き込みとストレージのペナルティを抑えます。
書き込みパフォーマンス:雑なスキーマの隠れたコスト
遅いアプリが必ずしも読み取りに原因があるとは限りません。初期の多くのパフォーマンス問題は、挿入や更新(ユーザー登録、チェックアウト、バックグラウンドジョブ)で現れます。雑なスキーマは各書き込みに余計な仕事を増やします。
書き込みが高コストになる理由
いくつかのスキーマ選択が各変更のコストを密かに増やします:
- 幅の広い行:数十(または数百)のカラムを一つのテーブルに詰め込むと、行が大きくなりI/Oとキャッシュの回転が増える。
- インデックス過多:インデックスは読み取りを速くするが、insert/update ごとに更新される。
- トリガーやカスケード:トリガーは
INSERTの背後で追加の挿入/更新を隠すことがある。カスケード外部キーは正しく便利だが、関連データ量に応じて書き込み時間が増える。
読み取り重視か書き込み重視か:意図的に痛みを選ぶ
ワークロードが 読み取り重視(フィード、検索ページ)なら、より多くのインデックスや選択的な非正規化が許容されます。ワークロードが 書き込み重視(イベント取り込み、テレメトリ、高頻度注文)なら、書き込みをシンプルで予測可能に保ち、必要な箇所だけに読み取り最適化を追加してください。
書き込みを損なう初期の典型パターン
- 監査ログ:コンプライアンスに有用だが、更新ごとに巨大なスナップショットをログに残すのは避ける。
- イベントテーブル:append-only は拡張性が高いが、冗長なペイロードを保存すると膨張する。
- ソフトデリート:便利だがインデックスサイズを増やし、適切に計画しないと更新・検索が遅くなる。
履歴を保ちながら書き込みをシンプルに保つ
実用的な方法:
- 「現在の状態」は一つのテーブルに、履歴 は別の append-only テーブルに保存する。
- 履歴行は 狭く(本当に必要なものだけ:誰が/いつ/何を変更したか)保つ。
- 履歴には実際のアクセスパターンに基づいたインデックスを追加する(通常は
entity_id,created_at)。 - 監査にトリガーを最初から使うのは避け、コストが見えるようにアプリ側で明示的に書き込むことを優先する。
クリーンな書き込みパスは余裕を与え、後のクエリ最適化を遥かに容易にします。
ORM と API がスキーマ決定を増幅する仕組み
ORM はデータベース作業を手軽にします:モデルを定義してメソッドを呼べばデータが出てきます。しかし同時にORMは高コストなSQLを見えなくすることがあり、それが痛みとして現れるまで隠れてしまうことがあります。
ORM:便利さが遅いパターンを隠すことがある
二つのよくある罠:
- 非効率な結合:一見シンプルな
.include()やネストされたシリアライザが広い結合や重複行、大きなソートに変わることがある—特にリレーションが明確でない場合。 - N+1 クエリ:50件のレコードを取ると、ORM が黙って関連データをロードするためにさらに50件のクエリを実行してしまう。開発環境では動くが実トラフィックで崩れることが多い。
よく設計されたスキーマはこれらのパターンが出る可能性を減らし、出たときに検出しやすくします。
明確なリレーションはORM利用を安全にする
テーブルに明示的な 外部キー、ユニーク制約、NOT NULL ルールがあれば、ORM はより安全なクエリを生成でき、コードは一貫した仮定に依存できます。
例:orders.user_id に FK を付け、users.email をユニークにすることで、アプリ側で回避していた多くのエッジケースや追加クエリが不要になります。
API はスキーマ選択をプロダクト挙動に変える
API設計はスキーマの下流です:
- 安定した ID(一貫したキー型)は URL、キャッシュ、クライアント側状態を単純にする。
- ページネーション はインデックス化された単調増加カラム(多くは
created_at+id)で最も効率的に動く。 - フィルタ はカラムが実際の属性を表し(オーバーロードされた文字列やJSONではない)、制約が値をクリーンに保つと予測可能になる。
レスキュー任務でなくワークフローにする
スキーマ決定を一級のエンジニアリング作業として扱ってください:
- 変更はすべてマイグレーションで行い、コードと同様にレビューする(/blog/migrations)。
- 新しいエンドポイントに対して軽量な「スキーマレビュー」を行う:どのテーブル、どのキー、どの制約、どのクエリ形状か。
- ステージングでORMのクエリをログし、プロダクション前に N+1 パターンを検出する(/blog/orm-performance-checks)。
チャット駆動の開発ワークフローで素早く構築しているなら(例:React アプリと Go/PostgreSQL バックエンドを Koder.ai で生成する場合)、早い段階で「スキーマレビュー」を会話の一部にするのが役に立ちます。高速で反復できますが、制約、キー、マイグレーション計画はトラフィック到来前に意図的に決めておきたいところです。
スキーマがボトルネックかを示す初期警告サイン
一部のパフォーマンス問題は「悪いSQL」ではなく、データの形がDBと戦っている結果です。多くのエンドポイントやレポートで同じ問題が出るなら、それはスキーマのサインであり、クエリ調整の機会ではありません。
よくある症状
遅いフィルタリングは古典的な兆候です。"顧客別の注文を探す" や "作成日で絞る" のような単純な条件が常に遅い場合、欠けたリレーション、不一致な型、インデックス化できないカラムが原因かもしれません。
もう一つの赤旗は膨れ上がる結合数です:基本的な質問に対して本来 2–3 テーブルで足りるはずが、過度な正規化、ポリモーフィックパターン、あるいは"すべてを1テーブルに"設計のために 6–10 テーブルをチェーンするような状況です。
また、列が列挙型のように振る舞うのに値が不一致("active", "ACTIVE", "enabled", "on")だと、LOWER()、COALESCE()、ORチェーンのような防御的クエリが必要になり、それらはチューニングしても遅いままになります。
スキーマチェックリスト(素早く検証)
- 外部キーにインデックスがない(結合カラムでの全表走査につながる)。
- 型が間違っている(例:IDが文字列、日付がテキスト、金額が浮動小数)。
- コアデータに EAV(Entity–Attribute–Value)テーブルを使っている:柔軟だがフィルタやソートが多数の結合やインデックスしにくい述語になる。
単純でツールに依存しない診断
まずは現実を確認:テーブルごとの行数、主要カラムのカーディナリティ(異なる値の数)。status に 4 個の期待値があるはずなのに 40 個見つかったら、既にスキーマが複雑化している証拠です。
次に遅いエンドポイントのクエリプランを見ます。結合カラムでの順次走査や大きな中間結果が頻出するなら、スキーマとインデックスが根本原因です。
最後にスロークエリログや可視化を有効にして閲覧します。多数の異なるクエリが似たような遅さ(同じテーブル、同じ述語)を示すときは、モデルレベルで修正すべき構造的問題です。
成長に合わせてスキーマを安全に進化させる方法
初期のスキーマ選択はリアルユーザーとの初接触でしばしば変わります。目標は「完璧にする」ことではなく、プロダクションを壊さず、データを失わず、チームを数日間固まらせずに変更することです。
軽量で再現可能な変更プロセス
一人のアプリから大きなチームまでスケールする実用的なワークフロー:
- モデリング:新しい形(テーブル/カラム、リレーション、ソース・オブ・トゥルース)を書き出す。サンプルレコードとエッジケースも含める。
- マイグレーション:後方互換な方法で新構造を追加する(まずは新しいカラム/テーブルを追加し、すぐに削除やリネームは避ける)。
- バックフィル:既存データから新フィールドをバッチで埋める。進捗を追跡して再開可能にする。
- 検証:データがクリーンになったら制約(NOT NULL、外部キー)を追加する。旧出力と新出力を比較するチェックを実行する。
フィーチャーフラグとデュアルライト(慎重に使う)
多くのスキーマ変更は複雑なロールアウトを必要としません。"拡張してから収束する" パターンを好みます:古いと新しい両方を読めるコードを書き、書き込みを切り替えるのは自信が出てからにします。
フィーチャーフラグ や デュアルライト は、本当に段階的な切り替えが必要な場合(高トラフィック、長いバックフィル、複数サービス)だけ使ってください。デュアルライトするならドリフトを検出する監視を入れ、競合時にどちらが勝つか定義しておきます。
ロールバックと実データでのマイグレーションテスト
安全なロールバックは可逆なマイグレーションから始まります。"元に戻す"経路を練習してください:新しいカラムをドロップするのは簡単ですが、上書きしたデータを回復するのは難しい。
マイグレーションは現実的なデータ量でテストします。ラップトップで2秒の移行が本番では数分テーブルロックを引き起こすことがあります。本番に近い行数とインデックスで実行時間を測ってください。
この点でプラットフォームツールがリスクを減らします:信頼できるデプロイ、スナップショット/ロールバック機能、必要ならコードのエクスポート能力があれば、スキーマとアプリロジックを同時に反復しやすくなります。Koder.ai を使っているなら、マイグレーションの慎重なシーケンスが必要なときはスナップショットや計画モードを活用してください。
次の人(未来の自分)向けに決定をドキュメント化
短いスキーマログを保ちます:何を変えたか、なぜか、どんなトレードオフを受け入れたか。/docs やリポジトリの README からリンクするとよいです。例:「このカラムは意図的に非正規化された」「外部キーは 2025-01-10 のバックフィル後に追加された」など、将来の変更が過去のミスを繰り返さないように説明を残してください。
いつクエリを最適化するか(実用的な優先順位)
クエリ最適化は重要ですが、スキーマがあなたと戦っている限り効果は薄いです。テーブルに明確なキーがない、リレーションが一貫していない、"一つの実体につき一行" が破られている場合、来週また書き換えられるクエリに何時間もかけることになります。
実用的な優先順位の順序
- まずスキーマの障害を直す。欠けた主キー、不整合な外部キー、複数意味を持つカラム、正規化の破綻、型のミスマッチ(例:文字列として保存された日付)など、正しい照会を困難にするものを最初に対処する。
- アクセスパターンを安定させる。データモデルがアプリの振る舞いを反映し、今後数スプリントで大きく変わらないと判断できたらクエリのチューニングは持続的な効果を持つ。
- トップクエリを最適化する—全クエリではなく。ログや APM を使って頻度と遅さで上位を特定する。1日1万回呼ばれるエンドポイントの最適化は、たまに走る管理レポートより優先される。
初期のチューニングで効く 80/20
初期の勝ち筋は少数の作業で得られます:
- 最適なインデックスを追加(一般的なフィルタと結合のため)。追加後に本当に使われているか検証する。
- 返すカラムを減らす(
SELECT *を避ける、特に幅の広いテーブルで)。 - 不要な結合を避ける—時には結合があるのはスキーマが基本属性を見つけにくくしているだけ。
期待値の設定:継続的だが基盤が先
パフォーマンス対応は終わることはありませんが、予測可能にするのが目標です。クリーンなスキーマなら各新機能は漸増的な負荷を追加します。雑なスキーマだと、各機能が複雑さを複利的に増やします。
今週やることチェックリスト
- 遅い上位5 と 頻出上位5 のクエリをリストアップする。
- それぞれについて確認:主キーがあるか、結合は キー同士か、型は正しいか。
- 支配的なフィルタ/順序に合う 1つのインデックス を追加する。
- 1つのホットパスで
SELECT *をやめる。 - 再測定し、次スプリントのためにシンプルな「前後」メモを残す。
よくある質問
SQL クエリを最適化する前に、スキーマを修正すべきですか?
スキーマから始めましょう。明確なテーブル、安定した ID、外部キー、適切なデータ型によって、すべてのクエリが処理する作業を減らせます。そのうえで、ログから実際に重要だと分かるクエリをチューニングします。
スキーマがクエリを遅くしているかどうかは、どう判断できますか?
関連のない多数のエンドポイントが遅い、単純なフィルタでも大きなテーブルをスキャンする、基本的な画面に多くの結合が必要、といった場合はスキーマに問題がある可能性があります。繰り返される型変換、テキスト解析、防御的な null チェックも警告サインです。
なぜすべてのテーブルに主キーが必要なのですか?
すべてのテーブルには、各行を一意に識別し、変更されない主キーを持たせるべきです。結合や参照には、名前、メールアドレス、タイムスタンプではなく、その ID を使います。
外部キーはデータベースのパフォーマンス向上に役立ちますか?
外部キーは、存在しないユーザーを指す注文のような無効な関連付けを防ぎます。データをより整合性のある状態に保ち、結合も考えやすくなります。ただし、よく使う結合で使用する外部キーカラムには引き続きインデックスを作成してください。
すべてのカラムにインデックスを追加すべきですか?
いいえ。アプリがレコードの検索、結合、並べ替えに定期的に使うカラムにインデックスを作成します。インデックスを追加するたびにストレージ使用量が増え、挿入や更新の作業量も増えます。
複合インデックスとは何ですか?また、カラムの順序が重要なのはなぜですか?
複合インデックスは、複数のカラムを特定の順序で格納するインデックスです。customer_id で絞り込み、created_at で並べ替えるクエリには、customer_id の次に created_at を置くインデックスが、アクセスパターンによく合うことがあります。
データベースを非正規化するのは、どのようなときですか?
まず中核となる事実を正規化します。各事実の置き場所を一つに明確に定め、ID でテーブルを結びます。非正規化は、頻繁に実行され、計測済みの読み取りのためだけに行い、コピーした値が現在の値か履歴スナップショットかを記録してください。
よくあるパフォーマンス問題を防ぐデータ型はどれですか?
金額には小数型、時刻には実際の日付型またはタイムスタンプ型を使い、関連テーブル間では ID の型を一致させます。日付や ID をテキストとして保存すると変換が必要になり、インデックスを効率的に使えなくなることがあります。
本番環境のスキーマを変更する最も安全な方法は何ですか?
まず変更を後方互換にします。新しいテーブルまたはカラムを追加し、バッチでバックフィルを行い、データを検証してから、アプリケーションの読み取りと書き込みを段階的に切り替えます。新しい経路の信頼性が確認できてから、古い構造を削除してください。
スキーマが安定した後、何を最適化すべきですか?
最も遅いクエリと実行頻度が高いクエリを見つけ、実行計画を確認し、まずスキーマを検証します。その後、対象を絞ったインデックスを追加し、必要なカラムだけを取得し、不要な結合を削除して、次の変更を行う前に結果を測定します。