はじめに
新たなプロジェクトの面談を受けるに際し、SQLのパフォーマンスチューニングの知見が必要になったため、どうせなら記事で残そう、と思い、執筆しました。
キャリア歴
ITエンジニアとしてのキャリアは4年強になります。おもにjavaを扱い、PostgreSQL、SQLServerなどでデータベース作成やSQLを使用したデータ取得は行ってきました。
こんな人向けの記事
上記のように、製造は何となくできるけど、SQLは苦手で、パフォーマンスチューニングって何?おいしいの?という人向けの記事になります。正直、現段階では知見無なので、この記事を書きつつ、知見をためていきたいと思っています。
SQLとクエリって何が違うの?
そうです。このレベルから???でした。
SQLって何?
SQLはデータベースを操作するための言語
クエリって何?
クエリはその言語を用いてデータベースに指示を出す命令文
じゃあ、クエリはSQLからデータを取得したいときに使用するってこと?
このような疑問を持ったのですが、「クエリ」はデータベースからデータを取得するための命令なので、SQLを使用しないデータベース(リレーショナルデータベース以外)からデータを取得する際にも**「クエリ」**といいます。
ここからが本題
Q.そもそもなぜSQLのパフォーマンスチューニングが必要なのか?
A.遅いとイライラするから。
・・・・ではどうすればいいの??
解決方法→SQLパフォーマンスチューニング
-
遅いクエリを見つける
-
実行計画を読み解く
-
インデックスを中心にクエリを改善する
-
効果を再確認する
SQLパフォーマンスチューニングはこの繰り返しのようです。
遅いクエリを見つける
①データの量が多すぎる
クエリが処理する行数が多ければ多いほど、時間がかかります。
不要なデータを取得していないかなどを調べます。
→SQLの実行計画を読み解くことで確認できます。
実行計画で「処理する行数」や「不要なデータ取得」を調べる方法
各データベースの EXPLAIN 系コマンドを使って実行計画を表示させ、以下の点に注目します。
-
テーブルスキャン(Full Table Scan)が行われているか?
確認方法: 実行計画の出力で、「Full Table Scan」「TABLE ACCESS FULL」「Seq Scan(PostgreSQLのシーケンシャルスキャン)」といった記述を探します。
これは、データベースがテーブルの全行を読み込んでいることを意味します。WHERE句で絞り込み条件を指定しているにもかかわらず、これが表示される場合、その条件にインデックスが効いていない可能性が高いです。インデックスがあれば、必要な行だけを効率的に検索できます。 -
「不要なデータ取得」との関連
全行を読み込むということは、最終的に必要な数行のために、不要な大量の行も一時的にメモリに読み込んだり、ディスクから読み込んだりしていることになります。これが「不要なデータを取得している」状態の一例です。
②インデックスが適切でない、または不足している
電話帳にインデックス(五十音順の並び)がないと、目的の人物を探すのに端から順に見ていかなければならないのと同じです。
データベースも、インデックスがないとテーブル全体をスキャン(全件検索)することになり、非常に時間がかかります。
しかし、やみくもにインデックスを貼っても逆効果になることがあります。
インデクス作成の目的
-
検索速度の向上
WHERE 句などで特定のデータを高速に探し出す。 -
結合速度の向上
JOIN 句で関連するデータを効率的に結合する。 -
ソート/集約の効率化
ORDER BY や GROUP BY 処理の負荷を軽減する。
デメリット
-
書き込み性能の低下
INSERT、UPDATE、DELETE 時にインデックスも更新する必要があるため、これらの処理が遅くなる。 -
ディスク容量の消費
インデックス自体がディスク領域を消費する。 -
クエリオプティマイザへの影響
インデックスが多すぎると、データベースのクエリオプティマイザ(最適な実行計画を立てる部分)が最適な計画を選択しにくくなることがある。
適切なインデックスのつけ方:基本的な考え方
インデックスを貼るべきかどうかの判断は、基本的に「その列がどれだけ検索や結合の条件として使われるか」と「その列のデータの特性」によって決まります。
-
最も重要なのは WHERE 句と ON 句
検索条件 (WHERE column = 'value')
結合条件 (JOIN table2 ON table1.column = table2.column)
これらに使われる列は、インデックスの最優先候補です。 -
ORDER BY と GROUP BY も考慮
ソートや集約の対象となる列も、インデックスがあることで処理が高速化される場合があります。特に、WHERE 句と ORDER BY 句の両方に同じインデックスが効くと、Using filesort(MySQL)のような遅い処理を回避できます。 -
カーディナリティ(値の多様性)が高い列
重複する値が少ない列(例: ユーザーID、メールアドレス、商品コード、ISBN)は、インデックスの効果が高いです。
性別や都道府県のように値の種類が少ない(カーディナリティが低い)列は、インデックスを貼ってもあまり効果がないか、逆にオーバーヘッドになることがあります。なぜなら、インデックスを使っても多くの行がヒットするため、結局テーブル全体をスキャンするのと大差なくなる場合があるからです。 -
データ型を考慮
数値型は文字列型よりも比較が高速なため、インデックスも効率が良い傾向があります。
文字列型の場合は、前方一致検索(LIKE 'prefix%')ならインデックスが効きますが、後方一致(LIKE '%suffix')や中間一致(LIKE '%middle%')では効きません。 -
データ量が少ないテーブルには不要
数十行、数百行程度の小さなテーブルであれば、インデックスなしでも十分高速なことがほとんどです。インデックスのオーバーヘッドの方が大きくなる可能性があります。
インデックスの種類
-
単一列インデックス (Single-Column Index)
一つの列に貼るインデックス。最も基本的。
例:CREATE INDEX idx_customers_name ON customers (name); -
複合インデックス (Composite Index / Multi-Column Index)
複数の列を組み合わせて貼るインデックス。最も重要で、理解が難しい部分でもあります。
列の順序が非常に重要です。 インデックスは、指定された列の順序でデータが並べ替えられます。
例:CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);
このインデックスは、customer_id でソートされ、次に order_date でソートされます。
WHERE customer_id = 123
WHERE customer_id = 123 AND order_date >= '2024-01-01'
WHERE customer_id = 123 ORDER BY order_date
これらには有効ですが、WHERE order_date >= '2024-01-01'だけではcustomer_idが最左にないため、このインデックスは使われません(あるいは部分的にしか使われません)。 -
最左プレフィックスルール
複合インデックスが有効になるのは、定義した列の左から順に条件が指定されている場合です。
INDEX (A, B, C) の場合:
WHERE A = X:OK
WHERE A = X AND B = Y:OK
WHERE A = X AND B = Y AND C = Z:OK
WHERE A = X AND C = Z:OK(BはスキップされるがAが使われる)
WHERE B = Y:NG(Aがないため)
WHERE C = Z:NG(AもBもないため) -
ユニークインデックス (Unique Index
インデックスを貼った列(または複合列)の値が重複しないことを保証するインデックス。
検索性能向上とデータの一意性制約の両方を兼ねる。
例:CREATE UNIQUE INDEX uidx_users_email ON users (email); -
主キーインデックス (Primary Key Index)
テーブルの主キーに自動的に作成されるユニークインデックス。最も重要なインデックス。
複合インデックスの設計におけるヒント
複合インデックスは、多くのパフォーマンス問題の解決策となりえますが、設計が難しいです。
-
等価条件 (=) の列を先に、範囲条件 (<, >, BETWEEN, LIKE 'prefix%') の列を後に
WHERE customer_id = 123 AND order_date >= '2024-01-01'の場合、customer_idは等価条件、order_dateは範囲条件です。
この場合、INDEX (customer_id, order_date)が最適です。customer_idで絞り込んでから、その中でorder_dateを範囲検索します。 -
ORDER BY や GROUP BY を考慮したカバリングインデックス (Covering Index)
クエリで SELECT するすべての列がインデックスに含まれている場合、データベースはテーブル自体にアクセスすることなく、インデックスだけでクエリを完結させることができます。これを「カバリングインデックス」と呼び、非常に高速です。
例:SELECT customer_name FROM customers WHERE customer_id = 123;
もしcustomersテーブルにINDEX (customer_id, customer_name)があれば、テーブル本体を見に行く必要がなくなります。
ただし、不要な列をインデックスに含めると、インデックスのサイズが大きくなり、書き込み性能に影響が出ます。本当に必要な場合のみ検討しましょう。
インデックスを貼るべきでない(または効果が薄い)ケース
-
カーディナリティが低い列
性別、フラグ(有効/無効)など。 -
頻繁に更新される列
更新が多いと、インデックスの更新オーバーヘッドが大きくなります。 -
データ量が非常に少ないテーブル
インデックスによるオーバーヘッドの方が大きくなる可能性があります。 -
ほとんど検索されない列
インデックスはディスク容量と書き込み性能を犠牲にするため、必要ないインデックスは貼るべきではありません。 -
LIKE '%keyword' のように先頭にワイルドカードがある検索
インデックスが利用されません。全文検索エンジンなどを検討しましょう。 -
OR 条件
WHERE column1 = 'A' OR column2 = 'B'のように複数の列にORを使うと、インデックスが効きにくいことがあります。可能であればUNION ALLで分割することを検討します。 -
関数を使った条件
WHERE FUNCTION(column) = 'value'のようにすると、インデックスが使えません。計算結果を別の列に格納するか、関数を使わない条件に変換できないか検討します。
③非効率なJOIN
複数のテーブルを結合する際に、結合条件が適切でなかったり、結合するテーブルの順序が悪かったりすると、多くのリソースを消費します。
④複雑すぎるクエリ
サブクエリの多用、複数のUNION、複雑な関数など、人間が理解しにくいクエリは、データベースも最適化しにくい場合があります。
⑤データベースの統計情報が古い
データベースは、クエリの実行計画を立てるために統計情報(各列の値の分布など)を利用します。この情報が古いと、最適な実行計画を立てられず、非効率な処理をしてしまうことがあります。
⑥ハードウェアリソースの限界
CPU、メモリ、ディスクI/Oなどがボトルネックになっている場合、どんなにSQLを最適化しても限界があります。これはSQLチューニングの範囲外ですが、考慮すべき点です。
実行計画を読み解く
これがパフォーマンスチューニングの最も重要な部分です。データベースは、SQLクエリを受け取ると、それをどのように実行するかという「計画」を立てます。これを「実行計画」と呼びます。
ほとんどのRDBMS(リレーショナルデータベース管理システム)には、この実行計画を表示するコマンドがあります。
- MySQL: EXPLAIN
- PostgreSQL: EXPLAIN または EXPLAIN ANALYZE(実際に実行して統計も表示)
- Oracle: EXPLAIN PLAN FOR
- SQL Server: SET SHOWPLAN_ALL ON または SQL Server Management Studioの「実行プランを含める」
実行計画から何がわかるの?
実行計画は、データベースがどのような手順でデータを取得したかを示します。特に注目すべきは以下の点です。
-
テーブルスキャン(Full Table Scan)
テーブル全体を最初から最後まで読み込んでいる状態。これが表示されたら、インデックスが不足している可能性が高いです。データ量が少ないテーブルなら問題ないこともありますが、大量のデータの場合は致命的な遅さになります。 -
インデックススキャン(Index Scan)
インデックスを使って必要なデータだけを効率的に見つけている状態。これが理想です。 -
JOINの種類
Nested Loop Join, Hash Join, Merge Joinなど、様々な結合方法があります。それぞれ得意な状況があり、実行計画でどの結合が使われているかを確認します。 -
ソート(Sort)
ORDER BYやGROUP BYでソートが必要な場合、データ量が多いと時間がかかります。インデックスでソート順が担保できれば、ソート処理を回避できることがあります。 -
一時テーブルの利用(Using temporary)
複雑なクエリやソートでメモリに収まらない場合などに、一時的にディスクにデータを書き込むことがあります。これもパフォーマンス低下の原因になります。
インデックスを中心にクエリを改善する
実行計画を見て、問題点が見つかったら、それを解消するための改善策を講じます。最も頻繁に行われるのが**「インデックスの追加・修正」**です。
インデックスの原則
WHERE句の条件に使われる列: WHERE column = 'value' や WHERE column BETWEEN 'a' AND 'b' など。
JOIN句の条件に使われる列: ON table1.column = table2.column など。
ORDER BY句やGROUP BY句で使われる列: ソート処理を効率化できます。
カーディナリティが高い列: 重複する値が少ない列(例:ユーザーID、メールアドレス)ほどインデックスの効果が高いです。性別のようなカーディナリティが低い列は効果が薄い場合があります。
インデックスの注意点
インデックスは諸刃の剣: 検索は速くなりますが、データの挿入(INSERT)、更新(UPDATE)、削除(DELETE)時にはインデックスも更新する必要があるため、これらの処理が遅くなります。必要最低限のインデックスに留めることが重要です。
-
複合インデックス
複数の列を組み合わせたインデックスです。WHERE column1 = 'A' AND column2 = 'B' のように、複数条件で絞り込む場合に有効です。この時、インデックスの列の順序が重要になることがあります。 -
ワイルドカード(%)の先頭一致
LIKE '%keyword' のように先頭にワイルドカードがあると、インデックスが使われないことが多いです。LIKE 'keyword%' なら使われる可能性があります。
その他のクエリ改善点
-
不要な列の取得を避ける
SELECT * ではなく、必要な列だけを指定する (SELECT col1, col2)。 -
サブクエリの見直し
複雑なサブクエリはJOINで代替できないか検討する。 -
UNION ALLとUNION
重複排除が必要なければ UNION ALL を使う(UNIONより速い)。 -
集約関数の使い方
COUNT(*) よりも COUNT(column_name) の方が良い場合がある(NULL値の扱いによる)。 -
OR条件
OR 条件はインデックスが効きにくいことがあります。UNION ALL で分割できないか検討する。 -
関数を条件句で使わない
WHERE FUNCTION(column) = 'value' のようにするとインデックスが使えなくなります。計算結果を別途格納するか、条件を工夫する。
インデックスの付け方の手順
-
遅いクエリを特定する
EXPLAIN を使って実行計画を確認し、Full Table ScanやFilesortなどの問題箇所を見つける。 -
WHERE 句と JOIN 句の条件列を特定する
最優先でインデックスを検討する列。 -
列のカーディナリティを確認する
重複が少ないか。 -
クエリの他の句 (ORDER BY, GROUP BY) も考慮する
これらの列も複合インデックスの候補。 -
複合インデックスの列順を慎重に決める
等価条件を先に、範囲条件を後に。最左プレフィックスルールを意識する。 -
インデックスを作成する
CREATE INDEX ...コマンドを使用。 -
再度 EXPLAIN で実行計画を確認する
インデックスが使われているか、パフォーマンスが改善したかを確認。 -
*書き込み性能への影響も確認する
本番環境に適用する前に、テスト環境で INSERT, UPDATE, DELETE の速度もテストする。
例:具体的なインデックス設計
シナリオ: orders テーブルがあり、大量の注文データが格納されています。
order_id (PRIMARY KEY)
customer_id
order_date
status (注文ステータス: 'pending', 'shipped', 'delivered' など)
total_amount
よく実行されるクエリ
例1, 「ある顧客の特定期間の注文履歴を新しい順に表示する」
SELECT *
FROM orders
WHERE customer_id = 123
AND order_date >= '2024-01-01'
AND order_date < '2024-02-01'
ORDER BY order_date DESC;
-
検討
WHERE 句にcustomer_idとorder_date、ORDER BYにorder_date DESCがあります。
customer_idは等価条件、order_dateは範囲条件であり、かつソートにも使われます。 -
最適なインデックス
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);
customer_idで絞り込み、その中でorder_dateを範囲検索し、さらにorder_dateでソートする際にこのインデックスが使われます。非常に効率的です。
例2, 「特定のステータスの注文で、総額が1000以上のものを抽出する」
SELECT *
FROM orders
WHERE status = 'pending'
AND total_amount >= 1000;
-
検討
statusはカーディナリティが低い(数種類しかない)可能性があります。total_amount は数値の範囲検索です。 -
インデックスの難しさ
statusのカーディナリティが低い場合、statusを先にしてもあまり効果がないかもしれません。逆にtotal_amountを先にするか、あるいはこのクエリのためだけに複合インデックスを貼るか、他のクエリとの兼ね合いで検討が必要です。 -
可能性のあるインデックス
CREATE INDEX idx_orders_status_amount ON orders (status, total_amount);
CREATE INDEX idx_orders_amount_status ON orders (total_amount, status); -
判断のヒント
もしstatusが非常に偏っていて、'pending'のデータが全体の90%を占めるようなら、インデックスはあまり効果がないかもしれません(ほとんど全件スキャンになるため)。
もしstatusが均等に分散しているなら、statusを最左にすることで効率的な絞り込みが期待できます。
EXPLAIN を使って両方のインデックスの効果を比較するのが一番確実です。
このように、単に列にインデックスを貼るだけでなく、クエリの特性(どの列が等価条件か、範囲条件か、ソートに使われるか)と、列のデータ特性(カーディナリティ)を考慮し、複合インデックスの列順を慎重に決定することが、適切なインデックス設計の鍵となります。
効果を再確認する
改善策を適用したら、必ず再度クエリを実行し、パフォーマンスが改善したかを確認します。
実行時間の計測
EXPLAIN を再度実行し、実行計画が改善されたかを確認。特に、テーブルスキャンがインデックススキャンに変わったか、ソートが回避されたかなど。
チューニングの心構え
ボトルネックを特定する
闇雲に修正するのではなく、「どこが一番時間を食っているか」を正確に特定することが重要です。実行計画がその手がかりになります。
トレードオフを理解する
パフォーマンスチューニングは、常に何かのトレードオフ(例:検索速度 vs 更新速度、CPU vs メモリ)を伴います。
段階的に行う
一度に複数の変更を加えず、一つずつ変更して効果を確認しましょう。
テスト環境で試す
本番環境でいきなり変更を適用するのではなく、必ずテスト環境で十分な検証を行ってからデプロイしましょう。
統計情報を更新する
データの増減があった場合、データベースの統計情報を定期的に更新することで、最適な実行計画が立てやすくなります。
まとめ
今回あらためてSQLパフォーマンスチューニングについて調べてみました。なかなか奥が深そう(考察が大変)であると同時に、製造の初期段階でこれらを考慮して作成できると、パフォーマンスチューニングの必要もなくなるため、気を付けていきたいと思います。