- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理
- DB保存今ここ
前回、公式がインデックスの構造を B-tree と表記していることを見ました。今回はその索引が「あるのに使われない」場面を扱います。対象は MySQL 8.4 と PostgreSQL 18 です。
まず結論
複合インデックスは**左端プレフィックス**として使われます。`(col1, col2, col3)` なら `(col1)` `(col1, col2)` `(col1, col2, col3)` で引けます。ただし公式は**反例**も並べています——`WHERE last_name='Jones' OR first_name='John'` は、第1列を含んでいるのに**使われません**。
もうひとつ効くのが**プランナが索引を選ばない**ケースです。索引があっても順次読みが安いと判断されれば使われません。そして索引は読み側だけの話でもありません。MySQL は不要な索引が「**どれを使うか決める時間**」も浪費すると明記しています。
`INDEX name (last_name, first_name)` があるとき、`SELECT * FROM test WHERE last_name='Jones' OR first_name='John';` で索引は使われるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
左端プレフィックスと、その公式の反例
複合インデックスの使われ方には規則があります。
If the table has a multiple-column index, any leftmost prefix of the index can be used by the optimizer to look up rows.
For example, if you have a three-column index on
(col1, col2, col3), you have indexed search capabilities on(col1),(col1, col2), and(col1, col2, col3).
逆側も明記されています。
MySQL cannot use the index to perform lookups if the columns do not form a leftmost prefix of the index.
ここまでなら「左端から順に書けばよい」と覚えられます。ところが公式は、使われるクエリと使われないクエリを実例で並べています。INDEX name (last_name, first_name) が前提です。使われる側はこうです。
SELECT * FROM test WHERE last_name='Jones';
SELECT * FROM test WHERE last_name='Jones' AND first_name='John';
SELECT * FROM test WHERE last_name='Jones' AND (first_name='John' OR first_name='Jon');
SELECT * FROM test WHERE last_name='Jones' AND first_name >='M' AND first_name < 'N';
そして “the name index is not used for lookups in the following queries” として挙がるのがこの2つです。
SELECT * FROM test WHERE first_name='John';
SELECT * FROM test WHERE last_name='Jones' OR first_name='John';
1つ目は左端を欠いているので納得できます。問題は2つ目です。last_name は書いてあります。それでも使われません。3行目の使われる例と比べると、違いは OR が AND の内側にあるか外側にあるかだけです。
「左端さえ含めればよい」は、公式自身の例で破れます。
使われる WHERE last_name='Jones' AND first_name='John'使われる WHERE last_name='Jones' AND (first_name='John' OR first_name='Jon')使われる WHERE last_name='Jones' AND first_name >='M' AND first_name < 'N'使われない WHERE first_name='John'使われない WHERE last_name='Jones' OR first_name='John' ← 左端を含むのに
`(col1, col2, col3)` の複合インデックスで、検索に使える列の組み合わせはどれですか。
索引があっても、選ばれないことがある
条件の書き方が正しくても、プランナが索引を使わない判断をする場合があります。PostgreSQL は極端な例で説明します。
An extreme example is that on a table that only occupies one disk page, you'll nearly always get a sequential scan plan whether indexes are available or not. The planner realizes that it's going to take one disk page read to process the table in any case, so there's no value in expending additional page reads to look at an index.
1ページしかないなら、索引を読む分が純粋な追加コストになります。コストの比較軸も書かれています。
Fetching rows separately is much more expensive than reading them sequentially, but because not all the pages of the table have to be visited, this is still cheaper than a sequential scan.
MySQL 側も同じ方向の記述です(こちらは How MySQL Uses Indexes のページ)。
Indexes are less important for queries on small tables, or big tables where report queries process most or all of the rows. When a query needs to access most of the rows, reading sequentially is faster than working through an index. Sequential reads minimize disk seeks, even if not all the rows are needed for the query.
小さいテーブルと大半の行を読む問い合わせが挙げられています。ランダムアクセスは順次読みより高価だが、訪ねるページが少なければ得になる——この綱引きの結果として計画が決まります。
そして索引は「作れば得」ではありません。書き込み側のコストがあります。
After an index is created, the system has to keep it synchronized with the table. This adds overhead to data manipulation operations.
Therefore indexes that are seldom or never used in queries should be removed.
MySQL はもう一歩踏み込んで、読み側のコストにも言及します。
Indexes also add to the cost of inserts, updates, and deletes because each index must be updated.
Although it can be tempting to create an indexes for every possible column used in a query, unnecessary indexes waste space and waste time for MySQL to determine which indexes to use.
「索引のコスト = 書き込みが遅くなる」だけではありません。どれを使うか決める時間も消費されます。だから公式の結論はこうなります——“You must find the right balance to achieve fast queries using the optimal set of indexes.”
式インデックスは「一致」が条件
関数を使った条件については、公式の言い方を正確に取る必要があります。PostgreSQL は索引を式の上に作れると書きます。
An index column need not be just a column of the underlying table, but can be a function or scalar expression computed from one or more columns of the table.
This query can use an index if one has been defined on the result of the
lower(col1)function:CREATE INDEX test1_lower_col1_idx ON test1 (lower(col1));
検索時に式を計算し直さない点も明記されています——“the index expressions are not recomputed during an indexed search, since they are already stored in the index.”
MySQL 側は一致の条件を例で示します。
For a functional key part containing the
SUBSTRING()function to be used in a query, theWHEREclause must containSUBSTRING()with the same arguments. In the following example, only the secondSELECTis able to use the index because that is the only query in which the arguments toSUBSTRING()match the index specification
CREATE TABLE tbl (col1 LONGTEXT, INDEX idx1 ((SUBSTRING(col1, 1, 10))));
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 9) = '123456789';
SELECT * FROM tbl WHERE SUBSTRING(col1, 1, 10) = '1234567890';
使えない理由は “The index is not used because the expressions in the query and the index differ.” です。引数が1違うだけで別の式になります。
なお MySQL の説明文には原文のまま引用すべき箇所があります——“Functional key parts that index expression values canalso be used in place of column or column prefix values.” "canalso" は原文のタイポで、直さずに引用します。実装の形も書かれています——“Functional indexes are implemented as hidden virtual generated columns”。
PostgreSQL の index-only scan が、索引だけで完結せずヒープを見に行く場合があるのはなぜですか。
MySQL — covering index
クエリに必要な値がすべて索引に含まれていれば、行本体を見ずに索引から返せます。InnoDB の二次索引は主キー列を含むため、この最適化が効く範囲が広いと書かれています。ただしトランザクションが変更中のテーブルには適用できないという限定が明記されています。
定義 "An index that provides all the necessaryresults for a query is called acovering index."利点 "the query returns values from the indexstructure, saving disk I/O"限定 "InnoDB cannot apply this technique forqueries against tables modified by atransaction, until that transaction ends."
PostgreSQL — index-only scan
索引だけで答えを返す走査です。B-tree は常に対応しており、必要な列が索引にあることが条件です。しかし可視性の確認という追加要件があり、可視性マップのビットが立っていなければヒープを訪ねます。その場合は通常の index scan に対する優位が無くなると明記されています。
条件 "The index type must support index-onlyscans. B-tree indexes always do.""The query must reference only columnsstored in the index."障壁 "Visibility information is not stored inindex entries, only in heap entries; ..."結果 ビットが立っていなければヒープへ→ "no performance advantage is gainedover a standard index scan"
どちらも「索引だけで答えを返す」最適化です。では「必要な列を全部索引に入れたから速くなる」という期待は、どこで裏切られるでしょうか。
それぞれの公式記述に、適用できない条件が書かれています。何が邪魔をしているでしょうか。
この章のまとめ
- 複合インデックスは左端プレフィックスで使われる。
(col1, col2, col3)なら(col1)(col1, col2)(col1, col2, col3)。 - ただし公式の反例がある。
WHERE last_name='Jones' OR first_name='John'は左端を含むのに使われない。ANDの内側のORは使われる。 - 索引があってもプランナが選ばないことがある。逐語で強いのは小さいテーブルと大半の行を読む問い合わせ。
- 索引のコストは書き込みだけではない。MySQL は不要な索引が「どれを使うか決める時間」も浪費すると明記。
- 式インデックスはクエリ側の式が定義と一致したときだけ使われる。「関数で包むと使えない」という一般命題は本教材では確認していない。
- covering index と index-only scan は中核が一致するが、「別名」と断定はしない。PostgreSQL では可視性の確認が必要で、ヒープを訪ねると優位が消える。
- 主キー未定義でも
GEN_CLUST_INDEXが 6バイト row ID 上に作られる。「クラスタ化インデックスが無い」は誤り。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
MySQL の公式ページが「索引が使われない」例として挙げているクエリはどれですか(`INDEX name (last_name, first_name)` が前提)。
用語集(4語)
- leftmost prefix(左端プレフィックス)
- “If the table has a multiple-column index, any leftmost prefix of the index can be used by the optimizer to look up rows.” 逆に “MySQL cannot use the index to perform lookups if the columns do not form a leftmost prefix of the index.”
- covering index(MySQL の定義)
- “An index that provides all the necessary results for a query is called a covering index.” 用語集は “Instead of using the index values as pointers to find the full table rows, the query returns values from the index structure, saving disk I/O.” と説明する。
- index-only scan(PostgreSQL)
- “PostgreSQL supports _index-only scans_, which can answer queries from an index alone without any heap access.” 条件は “The query must reference only columns stored in the index.” だが可視性の確認が別途必要になる。
- GEN_CLUST_INDEX
- 主キーも適切な UNIQUE 索引も無いときに作られる隠しクラスタ化インデックス。“on a synthetic column that contains row ID values”。row ID は **6-byte** で単調増加するため、行は物理的に挿入順に並ぶ。
出典(一次ソース)
- MySQL 8.4 — Multiple-Column Indexes ↗
- MySQL 8.4 — How MySQL Uses Indexes ↗
- MySQL 8.4 — CREATE INDEX Statement ↗
- MySQL 8.4 — Optimization and Indexes ↗
- MySQL 8.4 — Clustered and Secondary Indexes ↗
- MySQL 8.4 — Physical Structure of an InnoDB Index ↗
- MySQL 8.4 — MySQL Glossary(covering index) ↗
- PostgreSQL 18 — Index-Only Scans and Covering Indexes ↗
- PostgreSQL 18 — Indexes on Expressions ↗
- PostgreSQL 18 — Using EXPLAIN ↗
- PostgreSQL 18 — Introduction (Indexes) ↗