はじめに
「SQLにインデックスを貼ったのに速くならない」「どこを直せばいいのか分からない」
そんな経験はありませんか?
SQLの実行が遅いことは、ログや体感からある程度わかります。ただ、その原因をどう特定し、何を指針にして改善すればいいのか――そこが分からず、手探りになってしまう。私自身も、パフォーマンス問題に直面したとき、「このSQL、何が悪くてどこを直せばいいの?」と迷ったことが何度もありました。
そこで本記事では、最近私がパフォーマンスチューニング対応を行った実体験と学習した内容をもとに、チューニングに取り掛かるとき抑えておいた方がよいと感じた観点を整理して初心者向けに紹介します。
DBMSの内部構造に着目する
具体的な話をする前に前提として理解しておきたい話があります。
それはストレージアクセスによるデータの取得はメモリと比べて圧倒的に遅いということです。
DBMSがデータを格納・取得するために利用する記憶装置は、主にHDDとメモリの2つです。一般的に、HDDは低速で大容量、一方メモリは高速で容量は少ないといった特徴があります。
DBMSはデータを取得する際、対象のデータがメモリ上にあれば速く返せます。しかし、キャッシュにない場合はディスク(ストレージ)からデータを読み込む必要があり、これが非常に遅いのです。
つまり、ストレージの遅さをどうカバーするかが、SQLパフォーマンス改善の出発点になります。
この前提を踏まえたとき、パフォーマンスチューニングで押さえておきたい観点は大きく次の2つです。
- 冗長なストレージアクセスを減らす
- 効率のよいアクセスをする
他の観点もあるとは思いますが、今回はこの2つについて深堀っていきます。
✅ 冗長なストレージアクセスを減らす
シンプルですが、ストレージアクセスが遅いならストレージアクセス自体を減らせばいいじゃんという考え方です。
たとえば、以下のUNION ALLで何度もテーブルを読み込むようなクエリと実行計画を見てみます。
-- SQL
SELECT user_id, 'A' AS category FROM purchases WHERE amount >= 1000
UNION ALL
SELECT user_id, 'B' AS category FROM purchases WHERE amount BETWEEN 500 AND 999
UNION ALL
SELECT user_id, 'C' AS category FROM purchases WHERE amount 500
-- 実行計画
Append
-> Seq Scan on purchases (Filter: (amount >= 1000))
-> Seq Scan on purchases (Filter: (amount >= 500 AND amount 1000))
-> Seq Scan on purchases (Filter: (amount 500))
見てわかる通りpurchases テーブルを 3回 スキャンしています。これにより、ストレージアクセスが3回発生するため、特にデータ量が多い場合は実行時間に大きな差が出ることがあります。
そこで改善策として、以下のように CASE
文に置き換えることで、1回のスキャンで済ませることができます。
-- SQL改善例
SELECT
user_id,
CASE
WHEN amount >= 1000 THEN 'A'
WHEN amount BETWEEN 500 AND 999 THEN 'B'
ELSE 'C'
END AS category
FROM purchases;
-- 実行計画
Seq Scan on purchases
この対応によってSeq Scan
が1回に減っていることがわかります。無駄なストレージアクセスを減らすことができました。
以上が「冗長なストレージアクセスを減らす」の大まかなイメージになります。
※アクセスを減らす方法はインデックスオンリースキャンを使う方法やウィンドウ関数を使う方法など他にもありますが、細かい話になるためここで止めます。
✅ 効率よくデータを取得する:インデックスの活かし方
もうひとつは、ストレージアクセスが避けられない場合に、いかに効率よく必要なデータだけを取り出すかという考え方です。
これには、以下のように様々なアプローチが存在します。
- WHERE句やJOIN条件を見直して、DBMSが効率のよい絞り込み方を選択できるようにする
- 結合順序や書き方を調整し、効率的なアルゴリズムを誘導する
- 適切なインデックスを貼る/使わせることで、フルスキャンを回避する
どれも重要なアプローチですが今回はインデックスを例にして解説します。
🎯 インデックスを貼るべきカラムとは?
インデックスは貼れば速くなるというものではありません。
貼る対象のカラムを正しく選ばないと、効果が出ないこともあります。
ポイントをいくつか紹介します。
✅ インデックスを貼るべきカラム
- WHERE句やJOINの結合キーでよく使われるカラム
- カーディナリティが高いカラム
- ORDER BY / GROUP BYで使われるカラム
⚠️ インデックスを貼るときに注意すべきこと
- カーディナリティが低いカラムは効果が薄い
- インデックスをむやみに作成しすぎない
カーディナリティとは簡単に言うと「ばらつきの多さ」を示す指標で、あるカラムに「どれだけ多くの異なる値(ユニークな値)が含まれているか」を表します。
例えばIDカラムは全て異なるIDを持つのでカーディナリティが高いといえます。逆にSTATUSカラムは’active’ か’inactive’ぐらいしかないのでカーディナリティが低いと言えます。
カーディナリティが高いカラムほど、特定の値を検索したときに対象レコード数が少なくなりやすく、インデックスの効果が出やすいです。
そのため、インデックスを貼る前に「このカラム、値の種類どれくらいあるかな?」と考えてみるのがおすすめです。
また、インデックスを貼ると更新時のオーバーヘッドがかかるため、むやみにたくさん貼るのもNGです。
❌ インデックスが使われない原因
適切なインデックスを貼ったにも関わらずクエリが遅いことがあります。
その場合、そもそもインデックスが使われていない可能性があります。
インデックスが使われない原因をいくつか紹介します。
① インデックスのカラムに関数・演算を使っている
-- NG:インデックスが効かない
SELECT * FROM users WHERE LOWER(email) = '[email protected]';
② OR句で片方がインデックス非対象
-- NG:OR句の片方にインデックスが効かないカラムがある
SELECT * FROM users WHERE email = '[email protected]' OR region = '関東';
③ 否定形(NOT)を使っている
-- NG:否定形はインデックスが効かないことが多い
SELECT * FROM users WHERE NOT deleted;
SELECT * FROM users WHERE status != 'inactive';
上記の例はあくまでも一例で、他にも後方一致や中間一致のLIKEを使う、暗黙の型変換を行っているなども原因になります。
これらに該当した場合、インデックスが効く形にSQLをチューニングしてパフォーマンスを改善させます。
🔍 実行計画でインデックスの使用を確認する
インデックスが使われているかどうかの判断は簡単で、実行計画を使うことで確認できます。
実行計画の取得方法はDBの種類によって異なりますが、例えばPostgreSQLの場合は以下のように実行します。
-- EXPLAIN 実行計画を確認したいSQL
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
このコマンドを実行すると、実行計画が表示されます。
これを実行したときIndex ScanやIndex Only Scanが表示されていればインデックスが使われている、Seq Scanが表示されていればインデックスが使われずにフルスキャンが走っている、ということがわかります。
フルスキャンが走っている場合はインデックスが効いていない原因を調査して、SQLをチューニングする、インデックスを見直すなどの対応を行い、パフォーマンスの改善を行います。
おわりに
本記事では、SQLのパフォーマンスが悪化したときに「何を見て、どう改善すればよいか」という観点で以下の内容を初心者向けに解説しました。
パフォーマンスチューニングは正解が1つではないため、今回紹介した内容だけではすべてをカバーできるわけではなく、データの特性、クエリの意図、運用状況によって最適な対応は変わります。
完璧を目指すよりも、まずは「原因を理解し、試してみる」ことを積み重ねていくことが大事だと思います。
本記事が「どこを見ればいいのか分からない」という初学者の方にとって、少しでもその道しるべになれば幸いです。
参考
SQL実践入門──高速でわかりやすいクエリの書き方
効果的なインデックスを作る
その SQL、インデックス効いてないかも?
遅いSQLを爆速へ!SQLでインデックスが効きづらい書き方&対処法8選
Views: 0