データベースインデックスの理解:実践ガイド
インデックスとは
データベースインデックスは、データベースがテーブルと並行して維持する別のデータ構造です。これは、1つ以上の列の値をソートされ、検索可能な形式で格納し、実際の行へのポインタを保持します。テーブル自体がデータを格納し、インデックスは行をより速く見つけるのを助けることだけを目的とした追加の構造です。
ここで1つのアナロジーを紹介します。これはあなたが知る必要がある唯一のアナロジーです。インデックスは、教科書の巻末にある索引のようなものです。「トランザクション」という言葉が出てくるすべてのページを知りたい場合、本をページごとに読むのではなく、アルファベット順にソ...
全文を表示 ▼
データベースインデックスの理解:実践ガイド
インデックスとは
データベースインデックスは、データベースがテーブルと並行して維持する別のデータ構造です。これは、1つ以上の列の値をソートされ、検索可能な形式で格納し、実際の行へのポインタを保持します。テーブル自体がデータを格納し、インデックスは行をより速く見つけるのを助けることだけを目的とした追加の構造です。
ここで1つのアナロジーを紹介します。これはあなたが知る必要がある唯一のアナロジーです。インデックスは、教科書の巻末にある索引のようなものです。「トランザクション」という言葉が出てくるすべてのページを知りたい場合、本をページごとに読むのではなく、アルファベット順にソートされた索引で「トランザクション」を検索し、ページ番号の短いリストを取得して、それらに直接ジャンプします。その索引がなければ、すべてのページをスキャンするしかありません。データベースもまったく同じ選択肢に直面します。インデックスを使用して一致する行にジャンプするか、テーブル全体をスキャンするかです。
アナロジーなしの実際の動作を見てみましょう。SELECT * FROM orders WHERE customer_id = 42 のようなクエリを実行すると、データベースには2つの基本的な戦略があります。フルテーブルスキャンは、すべての行を読み取り、条件をチェックします。これはテーブルのサイズに比例した時間を要します。インデックスルックアップは、代わりにソートされたインデックス構造で customer_id = 42 を検索し、一致するエントリをすばやく見つけ、格納されたポインタをたどってそれらの行だけを取得します。少数の行しか一致しない大きなテーブルの場合、インデックスパスは数千倍安くなる可能性があります。
Bツリーインデックスの仕組み(概要)
最も一般的なインデックスタイプはBツリーです。これは、キーがソート順に保持されるバランスの取れたツリー構造です。ルートノードはキー空間を範囲に分割し、各子ノードはさらに細分化し、最下位レベル(リーフ)にはテーブル行へのポインタを含む実際のインデックス値が含まれます。ツリーはバランスが取れており、各ノードは多くのキーを保持しているため、何億もの行を持つテーブルでも、特定の値を検索するには通常3〜5回のノード読み取りしか必要ありません。
Bツリーは値をソート順に保持するため、完全一致以上のものをサポートします。範囲条件(WHERE created_at >= '2024-01-01')、文字列のプレフィックス一致(WHERE email LIKE 'anna%')を効率的に処理し、行を既にソートされた状態で返すことができるため、データベースは一致するORDER BY句の別のソートステップをスキップできます。
インデックスにコストがかかる理由
インデックスは無料ではなく、これがあなたが理解しなければならないトレードオフです。
書き込みが遅くなります。すべてのINSERTは、テーブルのすべてのインデックスにエントリを追加する必要があります。すべてのDELETEはエントリを削除する必要があります。インデックス列を変更するすべてのUPDATEは、対応するインデックスエントリを更新する必要があります。6つのインデックスを持つテーブルは、論理的な行の挿入ごとに最大7回の書き込みを行います。書き込み負荷の高いテーブルでは、不注意なインデックス設定はスループットを測定可能に低下させます。
ストレージが増加します。各インデックスは、インデックス付けされた列の値の完全なコピーに加えて、ポインタとツリー構造です。大きなテーブルのインデックスは、テーブル自体のサイズに匹敵するか、それを超えることがあり、バックアップやメモリキャッシュにも影響します。
したがって、基本原則は次のとおりです。インデックスは、読み取り速度のために書き込みコストとストレージを交換します。読み取りが明らかにメリットのある場所に追加し、どこにでも追加するわけではありません。
選択性:値の決定における重要な概念
選択性は、条件が行をどれだけ絞り込むかを表します。選択性の高い列は、行数に対して多くの異なる値を持っています。メールアドレスや注文IDは選択性が高いです。これらをフィルタリングすると、数百万行のうち1行または数行が返され、インデックスが輝きます。ステータスが3つの値(「保留中」、「発送済み」、「キャンセル済み」)を持つ列や、ブール値のis_activeフラグは選択性が低いです。フィルタリングすると、テーブルの40%が一致する可能性があります。
なぜこれが重要なのでしょうか?条件がテーブルの大部分に一致する場合、数百万行のインデックスとテーブルの間を行き来するのは、テーブルを順番にスキャンするよりも遅くなることがよくあります。クエリオプティマイザはこれを認識しており、推定一致率が高すぎるとインデックスを無視します。大まかな直感として、インデックスを使用した典型的なクエリが数パーセント以上の行を返す場合、インデックスはまったく使用されず、純粋なオーバーヘッドになります。
複合インデックスと左側プレフィックスルール
インデックスは、特定の順序で複数の列をカバーできます。たとえば:
CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);
これは、顧客IDで最初にソートし、次に各顧客内で作成日時でソートするエントリのように考えてください。電話帳が姓でソートされ、次に名でソートされるようなものです。
左側プレフィックスのアイデアは、そのソート順から直接導き出されます。このインデックスは、以下を効率的に処理できます。
- WHERE customer_id = 42
- WHERE customer_id = 42 AND created_at >= '2024-01-01'
しかし、WHERE created_at >= '2024-01-01' だけでは効率的に処理できません。なぜなら、特定の日付範囲のエントリはすべての顧客に散らばっているからです。姓でソートされた電話帳を使用して「アンナ」という名前のすべての人を見つけることはできません。インデックスは、条件が列リストのプレフィックス(左端の列から始まる)を制約する場合にのみ使用できます。これは、(customer_id, created_at) と (created_at, customer_id) が異なるインデックスであり、異なるクエリを処理することを意味します。列の順序は、最も重要なクエリパターンに従う必要があります。一般的な経験則:等価条件の列を最初に置き、次に範囲またはソート列を置きます。
インデックスが役に立たない場合
- 低い選択性:テーブルの90%が行アクティブである場合にWHERE is_active = true でフィルタリングします。オプティマイザは代わりにスキャンします。
- 列に関数または式を使用:WHERE LOWER(email) = 'x@y.com' は、インデックスが生の値ではなく変換された値を格納するため、email の通常のインデックスを使用できません。(一部のデータベースは式インデックスをサポートしていますが、通常のインデックスは使用されません。)
- 先頭ワイルドカード:WHERE name LIKE '%son' はBツリーを使用できません。ソート順はプレフィックスがわかっている場合にのみ役立ちます。
- 複合インデックスの左端の列をスキップすること(上記参照)。
- 小さなテーブル:数百行の場合、スキャンは既に高速です。インデックスはメリットなしに書き込みコストを追加します。
- インデックス付けされた列の型の一致しないものや暗黙的なキャストも、インデックスの使用を妨げる可能性があります。
2つの小さな例
有用なインデックス。アプリケーションが常に以下を実行していると仮定します。
SELECT id, total, created_at
FROM orders
WHERE customer_id = 42
ORDER BY created_at DESC
LIMIT 20;
このインデックスが最適です。
CREATE INDEX idx_orders_customer_date ON orders (customer_id, created_at);
データベースは顧客42のエントリにジャンプし、それらは既に作成日時でソートされており、最新の20件を読み取って停止します。テーブルのサイズに関係なく高速であり、左側プレフィックスのおかげで顧客IDによる通常のルックアップも処理できます。
問題のあるインデックス。代わりに以下を作成したと仮定します。
CREATE INDEX idx_orders_status ON orders (status);
ここで、ステータスには3つの可能な値があり、ほとんどの行は「発送済み」です。SELECT * FROM orders WHERE status = 'shipped' のようなクエリはテーブルの大部分に一致するため、オプティマイザはそれでもテーブルスキャンを実行します。インデックスはほとんどまたはまったく使用されませんが、すべての挿入とすべてのステータス更新はそれを維持するためにコストを支払います。これは純粋な損失です。(知っておくべき例外:低カーディナリティの列へのインデックスは、1つの値がまれで頻繁にクエリされる場合、たとえば少数の「保留中」の注文の場合に役立ちますが、上記の一般的なバージョンはよくある間違いです。)
インデックス追加前の実践チェックリスト
- まず実際の遅いクエリを特定します。推測でインデックスを付けないでください。実際のクエリパターンを確認し、EXPLAINを使用して現在のプランを確認します。
- 選択性を確認します。このインデックスを使用する典型的なクエリは、テーブルのごく一部を返しますか?そうでない場合は、再検討してください。
- マルチカラムフィルタリングとソートの場合、いくつかの単一カラムインデックスよりも、適切なカラム順序(等価条件の列を先に、次に範囲/ソート列)を持つ1つの複合インデックスを設計します。
- 左側プレフィックスルールを確認します。最も一般的なクエリは、インデックスの最初の列を制約しますか?
- クエリがインデックス付き列の関数、先頭ワイルドカード、または型キャストでインデックスを無効にしていないことを確認します。
- 書き込みトラフィックを考慮します。書き込み負荷の高いテーブルでは、追加のインデックスごとに実際のコストがかかります。互いに重複しているか、または他のインデックスのプレフィックスであるインデックスを削除します。
- 新しいインデックスを作成する前に、既存のインデックスが既にクエリをカバーしているかどうかを確認します。
- インデックスを作成した後、EXPLAINを使用してオプティマイザが実際にそれを使用していることを確認し、クエリ時間を前後で測定します。
- 定期的に未使用のインデックスを見直し、削除します。それらは永遠に書き込みとストレージのコストを発生させます。
維持すべき中核的なメンタルモデル:インデックスは、特定の選択的な読み取りを安価にするために、すべての書き込みでコストを支払うソート済みルックアップ構造です。それがサービスを提供するクエリを名前で呼び、それが役立つことを示せる場合にのみ追加します。
判定
勝利票
3 / 3
平均スコア
総合点
総評
回答Aは、網羅的で正確であり、ターゲットオーディエンスに非常に適しています。プロンプトで明示的に要求されたように、アナロジーと実際のデータベースの動作を明確に分離し、ノードの読み取りに関する具体的な詳細でBツリー構造を説明し、インデックス付けされたまれな低カーディナリティ値でも効果がある場合を含む、正しいニュアンスで書き込みコスト、ストレージ、および選択性をカバーしています。電話帳の例えで複合インデックスと左端プレフィックスルールを完全に扱い、関数、先頭ワイルドカード、型キャスト、小さなテーブルの場合など、インデックスが役立たない場合の豊富なセクションが含まれています。2つのSQL例は一貫性があり、クエリパターンに直接関連しており、チェックリストはEXPLAIN、測定、未使用インデックスの削除を参照しており、非常に実用的です。わずかな弱点:厳密に必要なものよりも長く、密度が高いですが、構造がしっかりしているため、理解を損なうことはめったにありません。
採点詳細を表示 ▼
分かりやすさ
重み 30%説明は正確で論理的に構築されています。アナロジーと実際の動作の意図的な分離、列順序の電話帳の例え、そして最後のメンタルモデルが抽象的な概念を鮮やかにしています。Bよりもわずかに密度が高いですが、混乱することはありません。
正確さ
重み 25%全体的に技術的に正確です。低選択性のインデックスを無視するプランナー、式インデックスを例外とする、先頭ワイルドカードの失敗、型キャストの問題、そしてまれに頻繁にクエリされる低カーディナリティ値でも依然としてメリットがあるという正しい注記など、微妙な点も含まれています。Bツリーのノード読み取り推定値は妥当です。
対象読者への適合
重み 20%SELECT/WHERE/JOINを知っているジュニア開発者に適しています。内部の詳細には踏み込まず、実用的な経験則を挙げ、すべての概念を開発者が下せる決定に関連付けています。密度が初心者にとって唯一のわずかなリスクです。
完全性
重み 15%要求されたすべての要素を網羅しています。インデックスとしての構造、読み取り速度の向上、書き込み/ストレージコスト、高レベルのBツリー、選択性、複合インデックス、左端プレフィックス、役立たない複数のケース、2つの対照的なSQL例、およびEXPLAINや未使用インデックスの削除を含む豊富なチェックリストが含まれています。
構成
重み 10%定義からトレードオフ、選択性、複合インデックス、役立たないケース、例、チェックリストへと、論理的でよく構成された流れです。Bよりもテキストブロックがやや重いため、スキャンしやすさが低下しています。
総合点
総評
回答Aは、データベースインデックスに関する非常に明確で包括的、かつ実用的な説明を提供しており、ジュニアバックエンド開発者に最適です。すべての必須トピックを優れた深さでカバーしており、インデックスが役に立たない状況に関する堅牢なセクションと、非常に実用的なチェックリストが含まれています。例え話と直接的な説明はうまく統合されており、SQLの例も適切です。
採点詳細を表示 ▼
分かりやすさ
重み 30%回答Aは、適切に構成された見出し、正確な言葉遣い、そして効果的な例え話(左側プレフィックスの電話帳など)を使用して複雑な概念を説明しており、非常に明快です。流れは論理的で理解しやすいです。
正確さ
重み 25%回答Aは、Bツリーの仕組みから、選択度や複合インデックスのニュアンスに至るまで、すべての説明において非常に正確です。Bツリーを使用した範囲クエリやORDER BY句のサポートを含む、インデックスが有益または有害なさまざまなシナリオを正しく特定しています。
対象読者への適合
重み 20%回答Aは、ジュニアバックエンド開発者向けに完璧に調整されています。言葉遣いは分かりやすく、例え話はシンプルで効果的であり、実用的なアドバイスは包括的でありながらも圧倒されることはありません。最後の「コアメンタルモデル」は、ターゲットオーディエンスにとって素晴らしい要約です。
完全性
重み 15%回答Aは、要求されたすべてのトピックを深くカバーしており、非常に包括的です。インデックスが役に立たない状況の非常に包括的なリストと、詳細で実用的なチェックリストを提供しており、実用的なガイダンスに関する期待を超えています。
構成
重み 10%回答Aは、読者を論理的に資料を通して導く、明確で説明的な見出しを備えた優れた構造を持っています。各概念は、コンテンツを理解しやすくするように、適切に整理された方法で導入および説明されています。
総合点
総評
回答Aは、非常に完全で、正確で、よく構成された教育的な説明です。インデックスを個別のソート済みルックアップ構造として明確に説明し、Bツリーの動作、読み取り/書き込み/ストレージのトレードオフ、選択性、複合インデックス、左端プレフィックスの動作、およびインデックスが役立たない多くの状況をカバーしています。例は実践的であり、最後のチェックリストは直接実行可能です。わずかな弱点として、BツリーのLIKEプレフィックスの動作を一般的に示唆したり、例のインデックスがあらゆるサイズのテーブルで高速であると述べたりするなど、いくつかの広範な単純化がありますが、これらは説明を実質的に損なうものではありません。
採点詳細を表示 ▼
分かりやすさ
重み 30%回答Aは非常に明確で、直接的な説明、具体的な例、およびアナロジーから実際のデータベースの動作へのスムーズな移行があります。やや長いですが、詳細は理解を不明瞭にするのではなく、一般的に理解を向上させます。
正確さ
重み 25%回答Aは、意図されたレベルの一般的なリレーショナルデータベースに対して技術的に正確です。個別のインデックス構造、Bツリーのルックアップ、読み取り/書き込み/ストレージのトレードオフ、選択性、複合インデックスの順序付け、および一般的な非使用ケースを正しく説明しており、わずかな一般的な単純化しかありません。
対象読者への適合
重み 20%回答Aは、基本的なSQLを知っているジュニアバックエンド開発者に適しています。実践的なメンタルモデル、現実的な例、および実行可能なガイダンスを提供しますが、その広範さは最初の導入には少し濃密かもしれません。
完全性
重み 15%回答Aは、要求されたほぼすべての要素をカバーしています。インデックスとは何か、読み取り速度の向上、書き込みとストレージのコスト、Bツリーの動作、選択性、複合インデックス、左端プレフィックスの動作、インデックスが役立たない複数のケース、2つのSQL例、および強力なチェックリストです。
構成
重み 10%回答Aは非常にうまく構成されており、明確な見出し、論理的な進行、概念の後に配置された例、そして最後に実践的なチェックリストがあります。構造は学習を強くサポートしています。