SQL:インデックスとパフォーマンス
1学習の目的
- INDEX(索引)の役割を理解し、検索を高速化できるようになる。数件のテーブルでは体感できない差が、実務の大量データでは決定的になる。
- EXPLAIN QUERY PLAN でSQLの実行計画を読み、インデックスが使われているかを確認できるようになる。「なぜこのクエリは遅いのか」を自分で調べられるようになる。
2基礎解説
本の索引(さくいん)を思い浮かべてほしい。索引が無ければ、探したい言葉を1ページ目から最後までめくって探すしかない。INDEXはテーブルの索引で、特定の列の検索を劇的に速くする。
| やりたいこと | 書き方 | 効果 |
|---|---|---|
| 索引を作る | CREATE INDEX idx_category ON products(category); | その列の検索が速くなる |
| 重複禁止の索引 | CREATE UNIQUE INDEX … | 索引と制約を兼ねる |
| 索引を消す | DROP INDEX idx_category; | 元の速度に戻る |
| 実行計画を見る | EXPLAIN QUERY PLAN SELECT … | SCAN か SEARCH かが分かる |
- PRIMARY KEY には自動でインデックスが付く。WHERE id = 3 のような検索が速いのはこのため。だからこそ、id以外の列で頻繁に検索するなら、自分でインデックスを追加する必要がある。
- EXPLAIN QUERY PLAN を付けると、実行される計画が分かる。SCAN は全行を1つずつ確認(遅い)、SEARCH … USING INDEX は索引を使って直接たどり着く(速い)。
- インデックスはタダではない。作れば検索は速くなるが、INSERT や UPDATE のたびに索引も更新されるので、その分だけ書き込みは遅くなる。「よく検索される列」にだけ作るのが基本方針。
- WHERE や JOIN の ON で頻繁に使う列がインデックスの候補になる。ほとんど検索に使わない列に付けても効果は薄い。
- UNIQUE INDEX は索引と重複禁止制約を同時に持つ。SQL15のUNIQUE制約は、実は内部でこのインデックスを自動的に作っている。
現場使用例:ユーザーIDでの高速検索、メールアドレスでのログイン、注文日での絞り込み、外部キー列への索引付け。数百万件のテーブルで「検索が遅い」と言われたら、まずインデックスの有無を疑う。
-- インデックス無しでの検索(実行計画を見る) EXPLAIN QUERY PLAN SELECT * FROM products WHERE category = 'PC'; -- → SCAN products (全7行を1つずつ確認) -- インデックスを作る CREATE INDEX idx_category ON products(category); -- 同じクエリをもう一度 EXPLAIN QUERY PLAN SELECT * FROM products WHERE category = 'PC'; -- → SEARCH products USING INDEX idx_category (category=?) -- (索引を使って直接たどり着く) -- 主キー検索は最初から速い(自動でインデックスがある) EXPLAIN QUERY PLAN SELECT * FROM products WHERE id = 3; -- → SEARCH products USING INTEGER PRIMARY KEY (rowid=?) -- 不要になったら削除する DROP INDEX idx_category;
3基本ドリル(10問)
idx_categoryが作成される
CREATE INDEX idx_category ON products(category); と書く。
CREATE INDEX idx_category ON products(category);
「どのテーブルの」「どの列に」「何という名前で」を指定する。作成しただけでは何も変わらないが、以降その列での検索が速くなる。
A. インデックスを使って高速に検索した B. テーブルの全行を1つずつ確認した C. エラーが発生した D. 結果が0件だった 選択★☆☆無料
解答 == B
「走査する」という意味の英単語。
B
SCANは「端から端まで見る」という意味。インデックスが無い列で検索すると、この方式になる。行数が少なければ問題にならないが、数百万件になると致命的に遅くなる。
idx_categoryが削除される
DROP INDEX idx_category; と書く。
DROP INDEX idx_category;
インデックスの削除はテーブル自体には影響しない。データは残ったまま、索引だけが無くなり、検索速度が元に戻る。
____ QUERY PLAN SELECT * FROM products WHERE price > 10000;
実行計画が表示される
「説明する」という意味の7文字の英単語。
EXPLAIN
EXPLAIN QUERY PLAN を先頭に付けるだけで、そのSQLが実際にはどう実行されるかが分かる。デバッグや性能調査の第一歩。
A. 自分で作る必要がある B. 自動的に作られている C. インデックスは使えない D. 常にSCANになる 選択★☆☆無料
解答 == B
products.id での検索は最初から速い。
B
PRIMARY KEY には自動でインデックスが付く。だから id での検索は何もしなくても速い。自分で作る必要があるのは、それ以外の列で頻繁に検索する場合。
インデックス作成後、SEARCHが使われる
CREATE INDEXの後に、別のSQL文としてEXPLAIN QUERY PLANを書く。
CREATE INDEX idx_price ON products(price); EXPLAIN QUERY PLAN SELECT * FROM products WHERE price = 3200;
作成した直後から効果が反映される。実行計画がSCANからSEARCHに変わっていれば、インデックスが正しく使われている証拠。
CREATE ____ INDEX idx_name ON products(name);
idx_nameが作成される
「一意」を意味する6文字のキーワード(SQL15で学んだ制約と同じ単語)。
UNIQUE
UNIQUE INDEXは索引と重複禁止を同時に実現する。SQL15のUNIQUE制約は、内部的にはこの仕組みで動いている。
A. 何も問題は起きない B. 検索は速くなり続けるが、INSERT/UPDATEは遅くなる C. 検索もINSERTも両方遅くなる D. データベースが壊れる 選択★★☆無料
解答 == B
インデックスは検索用の索引。データが書き込まれるたびに、索引も更新する必要がある。
B
インデックスはタダではない。書き込みのたびに索引の更新コストがかかるので、「よく検索される列」にだけ絞って作るのが基本方針。全列に付けるのは逆効果になりうる。
インデックスが一覧に表示される
CREATE INDEXのあと、SELECT name FROM sqlite_master WHERE type='index'; で確認する。
CREATE INDEX idx_category ON products(category); SELECT name, tbl_name FROM sqlite_master WHERE type='index';
sqlite_master はデータベースの構造そのものを保存しているテーブル。作成したテーブルやインデックスの一覧を、ここから確認できる。
インデックス作成後の実行計画が表示される
作成 → EXPLAIN QUERY PLAN の順で2本書く。
CREATE INDEX idx_stock ON products(stock); EXPLAIN QUERY PLAN SELECT * FROM products WHERE stock < 10;
範囲検索(< や BETWEEN)にもインデックスは効く。完全一致だけでなく、大小比較でも索引を使って絞り込みができる。
4実践シナリオ(5問)
idx_areaが作成される
CREATE INDEX idx_area ON customers(area); と書く。
CREATE INDEX idx_area ON customers(area);
「エリア別の顧客一覧」を頻繁に検索するなら、この列にインデックスを作る価値がある。SQL09〜11で何度も使ったWHERE c.area='東京'のような検索が、これで速くなる。
インデックス作成後の実行計画が表示される
CREATE INDEX idx_orders_customer ON orders(customer_id); のあと、EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id=1; を書く。
CREATE INDEX idx_orders_customer ON orders(customer_id); EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1;
JOINのON句で使われる列は、インデックスの最有力候補。SQL09〜11で書いた ON o.customer_id = c.id のような結合が、大量データでは大きく速くなる。
1回目はSCAN、2回目はSEARCHになる
EXPLAIN QUERY PLAN → CREATE INDEX → EXPLAIN QUERY PLAN の順に3本書く。
EXPLAIN QUERY PLAN SELECT * FROM products WHERE name = 'マウス'; CREATE INDEX idx_name ON products(name); EXPLAIN QUERY PLAN SELECT * FROM products WHERE name = 'マウス';
同じクエリでも、インデックスの有無で実行計画がSCANからSEARCHに変わるのを自分の目で確認できた。これがインデックスの効果を体感する最も直接的な方法。
2つのインデックスが作成され、件数が確認できる
2本のCREATE INDEXのあと、COUNT(*)で数える。
CREATE INDEX idx_price ON products(price); CREATE INDEX idx_stock ON products(stock); SELECT COUNT(*) AS インデックス数 FROM sqlite_master WHERE type='index' AND tbl_name='products';
1つのテーブルに複数のインデックスを持たせられる。ただしD08で学んだとおり、増やすほど書き込みコストも増える。本当に必要な列だけに絞る判断が実務では求められる。
削除後、idx_categoryが一覧に無い
最初にCREATE INDEXしてから、DROP INDEXし、最後にSELECTで確認する。
CREATE INDEX idx_category ON products(category); DROP INDEX idx_category; SELECT name FROM sqlite_master WHERE type='index' AND name='idx_category';
使われなくなったインデックスは削除するのが正しい運用。残しておいても検索は速くならず、書き込みコストだけが無駄にかかり続ける。
5仕上げ課題
手順
① まず、顧客ID(customer_id)で注文を検索したときの現在の実行計画を確認する(SELECT * FROM orders WHERE customer_id = 1;)
② 商品ID(product_id)で注文を検索したときの現在の実行計画も確認する
③ 両方の列にインデックスを作成する(idx_orders_customer と idx_orders_product)
④ ①②と同じ2つのクエリの実行計画を再度確認し、改善されたことを確かめる
⑤ 最後に、ordersテーブルに関連するインデックスの一覧を sqlite_master から取得する(type と name の2列)
期待される結果(⑤の出力):
type | name
index | idx_orders_customer
index | idx_orders_product
SQLは①→②→③→④(2本)→⑤の順に、合計6本を書くこと。
⑤の結果が一致し、①②はSCAN、④はSEARCHになる
①②はEXPLAIN QUERY PLANでSCAN products/ordersが出るはず。③で2つのインデックスを作成。④で同じ2つのクエリを再実行するとSEARCHに変わる。⑤はSELECT type, name FROM sqlite_master WHERE type='index' AND tbl_name='orders';
-- ① customer_id検索の現状確認 EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1; -- ② product_id検索の現状確認 EXPLAIN QUERY PLAN SELECT * FROM orders WHERE product_id = 1; -- ③ インデックス作成 CREATE INDEX idx_orders_customer ON orders(customer_id); CREATE INDEX idx_orders_product ON orders(product_id); -- ④ 改善後の確認 EXPLAIN QUERY PLAN SELECT * FROM orders WHERE customer_id = 1; EXPLAIN QUERY PLAN SELECT * FROM orders WHERE product_id = 1; -- ⑤ インデックス一覧の確認 SELECT type, name FROM sqlite_master WHERE type='index' AND tbl_name='orders';
これが実務での「遅いクエリの調査」の標準的な流れだ。①②で問題を確認し(SCANが出れば原因が分かる)、③で対策を打ち、④で本当に改善したかを検証する。「たぶん速くなっただろう」ではなく、実行計画という客観的な証拠で確認するのがプロの仕事の仕方になる。
orders テーブルは、SQL09〜13で何度もJOINの対象にしてきた。customer_id と product_id はまさにJOINのON句で使われてきた列であり、ここにインデックスを張ることで、これまで書いてきた結合クエリすべてが速くなる。テーブル設計の最初の時点でこの2列にインデックスを用意しておくのが、実務では一般的な設計になる。
18章にわたって学んできたSQLは、ここでいったん一区切りとなる。SELECTでデータを見る力、WHEREとJOINで絞り込み結合する力、GROUP BYで集計する力、INSERT/UPDATE/DELETEで書き換える力、CREATE TABLEで設計する力、そしてINDEXで速くする力——これらすべてが揃って、初めて「実務で通用するSQL」と言える。
次章はいよいよ最終回。ここまでのすべてを使い、1つの大きな分析課題に取り組む。