
急に重くなったDB|スロークエリの特定と一次対処の順番
「サイトが重いんですけど」
昼過ぎにその一言が届いて、開いてみたら確かに重い。昨日までは普通だったのに、今日は一覧ページが10秒待っても出てこない。サーバーのCPUを見ると、DBのプロセスだけが高い。
ここで、たぶん一度は思ったことがあるはずです。「DBが重いのは分かった。で、何が重いんだ?」
DBの遅さがやっかいなのは、エラーが出ないことです。落ちてくれれば原因を追えるのに、動いてはいる。ただ、遅い。ログにも「500」の文字はない。それでいて、電話とチャットだけが増えていきます。
この記事では、その状態から抜けるための順番を整理します。特別なツールは使いません。DBに標準で付いている機能だけで、犯人のクエリまでたどり着けます。
結論:急に重くなったDBは、次の順で見ます。①いま走っているクエリを一覧で見る(実行時間の長い順) → ②影響が大きく、止めても安全な1本だけを止める → ③スロークエリログを有効にして、記録として残す → ④落ち着いてから EXPLAIN で「なぜ遅いか」を読む → ⑤インデックスや処理の見直しは、検証してから本番へ。急いでいるときにやるのは①と②だけで十分です。③以降は、詰まりがほどけてからで間に合います。そして③は、次に同じことが起きたときの自分をいちばん助けてくれます。
なお、以下のコマンドや設定名はMySQL/MariaDB/PostgreSQLを前提に書いています。バージョンや構成(マネージドDBか自前かなど)で使える機能や書き方が変わるので、本番で実行する前に、必ずお使いのDBの公式ドキュメントで確認してください。
何が起きているか:「重い」の正体は、たいてい一点の詰まり
サイト全体が重いと、「サーバーの性能が足りないのでは」と考えたくなります。でも、急に重くなったのなら、その線は薄いことが多いです。サーバーの性能は昨日から変わっていないからです。
急な遅さで、実際によくあるのはこの3つです。
- データが増えて、あるクエリだけが一線を越えた。 10万件のときは0.2秒で返っていた検索が、100万件になって2秒かかるようになった。全件を順番に見る処理は、件数に比例して素直に遅くなります。ある日突然「体感できる遅さ」に変わります。
- 実行計画が変わった。 同じSQLでも、DBは「どうやって取りに行くか」をその時々のデータの偏りから決めます。データの分布が変わったり統計情報が古くなったりすると、昨日までインデックスを使っていたクエリが、今日から全件走査に切り替わることがあります。
- 重い処理が同時に走った。 月次バッチ、大きな集計、バックアップ、インポート。ひとつひとつは想定内でも、重なった瞬間に全体が詰まります。
そして、詰まりは連鎖します。1本のクエリが3秒かかると、その接続は3秒占有されます。1件0.5秒のクエリが1秒あたり20回来る状態なら、常に10本が同時に走っている計算(20回 × 0.5秒 = 10本)になります。これが0.5秒から2秒に伸びると、同時に走るのは40本。接続数の上限やワーカー数にぶつかると、遅いどころか、つながらなくなります。
つまり、探すべきものははっきりしています。いちばん時間を使っているクエリを1本見つけること。全体を速くしようとしなくて大丈夫です。
手順1:いま走っているクエリを、実行時間の長い順に見る
まずは現在進行形の状況を見ます。設定変更も再起動も要りません。読むだけです。
MySQL / MariaDB の場合
-- いま走っている処理を、実行時間の長い順に並べる
SELECT id, user, host, db, command, time, state, LEFT(info, 200) AS query
FROM information_schema.processlist
WHERE command <> 'Sleep'
ORDER BY time DESC
LIMIT 20;
SHOW FULL PROCESSLIST; でも同じ情報が見られますが、上のように並べ替えられるぶん、information_schema から引くほうが探しやすいです。
PostgreSQL の場合
-- いま走っているクエリを、実行時間の長い順に並べる
SELECT pid, usename, state, now() - query_start AS duration, wait_event_type, query
FROM pg_stat_activity
WHERE state = 'active' AND pid <> pg_backend_pid()
ORDER BY duration DESC;
見るのは3か所だけ
一覧が出たら、上から数行だけ見ます。
- 実行時間(
time/duration):他より桁違いに長い行があるか。数十秒〜数分の行が居座っていれば、それが有力候補です。 - 状態(
state/wait_event_type):Sending data(MySQL)が長ければ、大量の行を読んでいるサイン。Lock待ち(PostgreSQL)やWaiting for ... lock(MySQL)なら、遅いのではなく待たされている状態です。この場合は待たせている側を探す話になるので、DBコネクション枯渇・デッドロックの見つけ方と一次対処のほうが近いです。 - クエリ本文(
query/info):どのテーブルを触っているか。同じ形のクエリが何本も並んでいたら、それが今日の主役です。
同じクエリが何本も並んでいるときは、1本が遅いのではなく「遅いクエリに次々アクセスが来て、後ろに積み上がっている」状態です。原因のクエリは1種類です。
「本当にDBなのか」を10秒で確かめる
一覧を見ても長いクエリがなく、接続もスカスカなら、遅さの原因はDBの外かもしれません。アプリの処理、外部APIの応答待ち、ディスクの空き不足など、行き先はいくつもあります。切り分けの全体像は「サイトが重い」の切り分け手順に、上流から順に見る型はタイムアウトはどこで起きているかにまとめています。
ここで方向を間違えると、あとの時間がまるごと無駄になります。「DBに長いクエリが実際に見えたか」を、次に進む前の関門にしてください。
手順2:止めるのは、影響が大きくて安全な1本だけ

犯人らしき1本が見えたら、次は「止めるかどうか」の判断です。ここは慎重に、でも止めるときは迷わずに。
止めていいか、を分けるのは「読むだけか」どうか
SELECT(読むだけ):止めても、データは変わりません。比較的安全です。UPDATE/DELETE/INSERT/ALTER(書き込む):止めると、そこまでの変更を巻き戻す処理(ロールバック)が走ります。巻き戻しのほうが時間がかかることもあります。長時間走っている大きな更新は、止めるほうが傷が深くなることがあるので、まず「これは何の処理か」を確かめてください。バッチや誰かの手作業なら、本人に一声かけるのがいちばん早いです。
止め方(ピンポイントで、1本ずつ)
PostgreSQL は、いきなり接続を切らずに、まずクエリだけキャンセルします。
-- ① まずクエリのキャンセルを試す(接続は残る・こちらが穏やか)
SELECT pg_cancel_backend(12345);
-- ② それでも止まらないときだけ、接続ごと切る
SELECT pg_terminate_backend(12345);
MySQL / MariaDB は KILL を使います。
-- 実行中のクエリだけを止める(接続は残る)
KILL QUERY 12345;
-- 接続ごと切る(QUERY より強い)
KILL 12345;
どちらも、指定するIDを取り違えないことが唯一にして最大の注意点です。実行前に、そのIDの query 本文をもう一度見てください。関係ない処理を巻き込むと、遅いだけだった障害が、データの障害に変わります。
止めても、また来る
1本止めると、体感は一気に軽くなります。ただ、同じクエリを呼ぶページにアクセスが来れば、また同じことが起きます。だからここで終わりにせず、次に進みます。
対応が長引きそうなら、この時点で関係者に状況を伝えておくと後が楽です。伝え方の型は障害の第一報テンプレート、復旧の見込みが立たないときの間の埋め方は障害が長引くときの中間報告にまとめています。
手順3:スロークエリログを有効にして、記録に残す
ここからが本題です。手順1は「いま見えているもの」しか捕まえられません。5分で終わる遅さや、夜間バッチの裏で起きる遅さは、見に行った頃には消えています。
だから、DBに記録させます。
MySQL / MariaDB
-- いまの設定を確認する
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
-- 有効にする(1秒を超えたクエリを記録)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL での変更は再起動で消えるので、続けるなら設定ファイル(my.cnf など)にも書きます。設定ファイルを触るときの手順は設定ファイル変更を安全に反映が使えます。
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
なお、long_query_time の変更は新しく張られた接続から効く点に注意してください。コネクションプールを使っていると、既存の接続が残っているあいだは古い値のままです。しばらく待つか、アプリ側を再起動すると揃います。
たまったログは、mysqldumpslow でまとめて見ると早いです。
# 合計実行時間が長い順に、上位10件をまとめて表示する
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
このとき見たいのは、1回の遅さより「合計でどれだけ時間を食っているか」です。3秒かかるクエリが1日1回より、0.3秒のクエリが1日10万回のほうが、サーバーへの負荷はずっと大きくなります(0.3秒 × 10万回 = 約8.3時間ぶん)。-s t(合計時間順)で並べるのは、そのためです。
PostgreSQL
-- 1秒(1000ミリ秒)を超えたSQLをログに記録する
ALTER SYSTEM SET log_min_duration_statement = '1s';
SELECT pg_reload_conf();
ALTER SYSTEM が使えない環境(一部のマネージドDBなど)では、管理コンソールのパラメータ設定から同じ項目を変更します。
さらに、pg_stat_statements を有効にしておくと、クエリごとの累計実行時間・回数がDBの中に貯まります。ログを掘るより、こちらのほうがずっと楽です。
-- 拡張を有効化(設定ファイルへの追記と再起動が必要)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- 合計実行時間が長い順に上位10件
SELECT calls, round(total_exec_time::numeric, 1) AS total_ms, query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
しきい値は、まず1秒から
「0.1秒にしたほうが細かく分かるのでは」と思うかもしれません。でも、最初は1秒にしてください。理由は2つあります。
- ログが膨らんでディスクを圧迫するからです。遅いことより、ディスクが埋まるほうが重い障害になります。ログの扱いはディスク使用率100%を防ぐログローテーションと監視設定とセットで考えてください。
- 1秒を超えるクエリが数本見つかれば、当面はそれで十分だからです。まずいちばん重い1本を直せば、体感はかなり変わります。細かく見るのは、そのあとで間に合います。
MySQLの log_queries_not_using_indexes(インデックスを使っていないクエリを全部記録する設定)も便利ですが、小さなテーブルへの正常なクエリまで大量に記録されます。使うなら、調査中の短い時間だけにしておくのが安全です。
手順4:EXPLAIN で「なぜ遅いか」を読む
犯人のSQLが分かったら、DBに理由を聞きます。EXPLAIN は、そのクエリをどうやって処理するつもりかを、実行せずに教えてくれるコマンドです。
-- MySQL / MariaDB
EXPLAIN SELECT * FROM orders WHERE customer_id = 1234 ORDER BY created_at DESC;
-- PostgreSQL(計画だけを見る。実行はしない)
EXPLAIN SELECT * FROM orders WHERE customer_id = 1234 ORDER BY created_at DESC;
見るところは、実質3つ
専門用語がずらりと出ますが、最初に見るのはここだけで大丈夫です。
- 全件走査になっていないか。 MySQLなら
typeがALL、PostgreSQLならSeq Scanと出ていれば、テーブルを頭から全部読んでいます。件数が増えるほど遅くなるのは、これが理由です。 - 読む予定の行数(
rows)。 実際に返したい件数が10件なのに、rowsが数十万になっていたら、無駄に読んでいます。 - 並べ替えや一時領域を使っていないか。 MySQLの
ExtraにUsing filesortやUsing temporaryがあると、メモリやディスクで作業しています。件数が多いほど重くなります。
EXPLAIN ANALYZE は「実際に実行する」
より正確に見たいときは実測できますが、このコマンドはクエリを本当に実行します。SELECT なら問題ありませんが、UPDATE や DELETE に付けるとデータが変わります。PostgreSQLで更新系を調べたいときは、トランザクションで囲んで巻き戻してください。
BEGIN;
EXPLAIN (ANALYZE, BUFFERS) UPDATE orders SET status = 'done' WHERE id = 1234;
ROLLBACK; -- 必ず巻き戻す
MySQLでは EXPLAIN ANALYZE は SELECT などの参照系が対象です。いずれにせよ、本番で試す前に検証環境で1回通すのが安心です。
「同じSQLなのに急に遅くなった」ときは統計情報も疑う
DBは、テーブルの中身の傾向(統計情報)をもとに実行計画を決めます。データが大きく変わったのに統計が古いままだと、判断を誤ることがあります。
-- MySQL / MariaDB
ANALYZE TABLE orders;
-- PostgreSQL
ANALYZE orders;
負荷は比較的軽い処理ですが、大きなテーブルでは時間がかかることもあります。実行するなら、利用の少ない時間帯を選んでください。
手順5:直し方は、リスクの小さい順に
原因が見えても、すぐ本番を触らないのがコツです。上から順に、リスクの小さい打ち手です。
- 呼ぶ回数を減らせないか。 一覧ページで1行ごとにクエリが飛んでいる(いわゆるN+1)なら、まとめて取る形に変えるだけで劇的に軽くなります。インデックスを足すより、そもそも呼ばないほうが速いです。
- 取る量を減らせないか。
SELECT *を必要な列だけにする、件数の上限を付ける、期間で絞る。管理画面の「全件表示」が原因だった、というのは本当によくあります。 - 重い処理の時間帯をずらせないか。 集計バッチを日中から夜間に移すだけで、昼のピークが消えることがあります。コードを1行も触らずに済む、いちばん安い一手です。
- インデックスを足す。 効果は大きいですが、追加そのものがテーブルをロックすることがあります。PostgreSQLなら
CREATE INDEX CONCURRENTLY、MySQL 8.0以降なら多くのケースでオンラインDDLが使えますが、構成とバージョンで挙動が違います。必ず検証環境で所要時間を測ってから、本番反映前チェックリストとDBマイグレーションを本番へ安全に当てる手順の型に乗せて実施してください。インデックスは書き込みを少しずつ遅くするので、「とりあえず全部の列に」は逆効果です。
そして、どの手を打つときも記録を残してください。「いつ・どのクエリに・何をしたか」を変更管理台帳に1行。急いで直したものほど、半年後の自分が理由を思い出せません。応急処置で終えた場合は、暫定対応と恒久対応を分けて管理するの形で「戻す・直す」を残しておくと、置き去りになりません。
具体例:一覧ページだけが重くなった木曜日
社内の受注管理システムを一人で保守している担当者の例です。
- 13時20分:「受注一覧が開かない」と連絡。開いてみると確かに20秒待っても表示されない。他の画面は普通に動く。
- 13時23分:
information_schema.processlistを実行時間順で確認。同じSELECTが7本、それぞれ15〜40秒走っていた。テーブルはorders。この時点で「DBのこのクエリ」と確定。 - 13時25分:
SELECTなので止めても安全と判断し、いちばん古い1本をKILL QUERYで停止。残りも順に落ちて、一覧が表示されるように。体感は復旧。ただし原因は残ったまま。 - 13時30分:チャットで「受注一覧の表示遅延、いったん復旧しました。原因調査中で、また重くなる可能性があります」と一報。
- 13時40分:
slow_query_logを有効化し、long_query_time = 1に設定。記録を取り始める。 - 14時10分:ログに同じクエリが12件。
EXPLAINするとtype: ALL、rowsが約48万。全件走査していた。条件に使っているcreated_atにインデックスがない。 - 14時30分:git の履歴を見ると、3週間前に一覧の絞り込み条件が追加されていた。当時はデータが少なく問題にならなかったが、期末で受注データが増えて表面化したと分かる。
- 翌日:検証環境で
created_atのインデックス追加を試し、所要45秒・実行時間は約20秒から0.1秒に。夜間の利用が少ない時間帯に本番へ適用し、台帳に記録。 - 1週間後:スロークエリログを再確認。1秒超のクエリはゼロ。
long_query_timeはそのまま1秒で運用を継続。
振り返ってみると、この日いちばん効いたのは高度な技術ではありません。13時23分に「processlistを実行時間順で見た」ことです。そこで犯人が確定したから、その後の1時間が調査に使えました。
影響:記録が残ると、何が変わるか
- 原因の特定が、勘から手順になる。 「たぶんあのページでは」ではなく、「ログのこの行」で話せます。当てずっぽうで直して、直らなくて焦る時間が減ります。
- 説明できるようになる。 「データが増えて、この検索が全件走査になっていました」と言えると、上司や依頼者の不安が下がります。「サーバーが古いので」より、ずっと納得してもらえます。
- サーバー増強の前に、選択肢が増える。 増強は最後の手でも間に合うことが多いです。1本のクエリを直すほうが、安く早く効きます。
- 再発に気づけるようになる。 週に1回ログを見る習慣がつくと、「遅くなり始めた」段階で気づけます。悲鳴が来る前に動けるのは、精神的にかなり違います。
- 深夜の遅さも捕まえられる。 見に行けない時間帯の犯人も、記録は覚えていてくれます。
一方で、増える手間もあります。ログのぶんディスクを使いますし、pg_stat_statements はわずかですがオーバーヘッドがあります。ただ、遅さの原因が分からないまま何時間も探す消耗に比べれば、ずっと軽い負担です。
明日やること:3つとも、5分で終わります
障害が起きていない今日のうちに、やっておくと効くことです。どれもサービスに影響しません。
- いまスロークエリログが有効かどうかを見るだけ。
-- MySQL / MariaDB
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
-- PostgreSQL
SHOW log_min_duration_statement;
OFF や -1 なら、記録は残っていません。「いざというとき、遡れないんだ」と知っておくだけで違います。
processlist/pg_stat_activityを引くSQLを、メモに1行貼っておく。
障害の最中にコマンドを思い出そうとすると、それだけで数分溶けます。手順書に貼っておけば、コピーして貼るだけです。運用ドキュメントの最低限テンプレートの中に置き場所を作っておくと、引き継ぎでもそのまま使えます。
- ログが有効なら、いまたまっているぶんを一度だけ眺めてみる。
mysqldumpslow -s t -t 5 /var/log/mysql/slow.log
何も出なければ、いまは健康ということです。それが分かるのも収穫です。
スロークエリ対応チェックリスト
全部そろえる前提ではありません。上から優先順位で並べています。
重くなっている、まさにいま
-
processlist/pg_stat_activityを実行時間の長い順に見たか - 長く走っているクエリが実際にあったか(なければDB以外を疑う)
- そのクエリが
SELECTか、書き込み系かを確認したか - 止めるIDと、そのクエリ本文を突き合わせて確認したか
-
KILL QUERY/pg_cancel_backendから試したか(いきなり接続を切っていないか) - 止めた内容と時刻をメモに残したか
- 関係者に一報を入れたか(原因調査中である旨を含めて)
落ち着いたら
- スロークエリログ(
slow_query_log/log_min_duration_statement)を有効にしたか - しきい値をまず1秒にしたか
- ログの出力先とディスク残量、ローテーションを確認したか
- 設定ファイルにも書いて、再起動後も残るようにしたか
- 合計実行時間の長い順に並べて、上位を見たか
- 犯人クエリを
EXPLAINで確認したか(全件走査・読む行数・並べ替え) - いつから遅くなったかを、変更履歴やデータ量の推移と突き合わせたか
直すとき
- 呼ぶ回数・取る量を減らせないか、先に検討したか
- 時間帯をずらすだけで済まないか検討したか
- インデックス追加は検証環境で所要時間を測ったか
- 本番反映の手順(バックアップ・切り戻し)を用意したか
- 変更管理台帳に記録したか
- 暫定対応で終えた場合、恒久対応の予定を残したか
次に備える
- スロークエリログを定期的に見る日を決めたか(週1回など)
- 応答時間の監視やアラートのしきい値を見直したか
- 今回の内容をポストモーテムとして1枚残したか
「重くなっている、まさにいま」の最初の2つができれば、その日は乗り切れます。残りに○が付かない日があっても大丈夫です。
よければ、こちらも
DBの遅さは、前後の切り分けとつなげると一段ラクになります。
- 「サイトが重い」の切り分け手順|どこが遅いかを順番に絞る:そもそもDBが原因なのかを、上流から確かめる型です。
- DBコネクション枯渇・デッドロックの見つけ方と一次対処:遅いのではなく「待たされている」ときの調べ方です。
- リクエストが詰まって返らない|ワーカー枯渇の見つけ方と一次対処:DBの遅さがアプリ側に波及したときに見る場所です。
- 監視アラートの鳴らしすぎを減らす|閾値の決め方と運用のコツ:「遅くなり始めた」で気づくための、しきい値の考え方です。
- ポストモーテムの書き方|障害振り返りテンプレと再発防止の落とし込み:直したあと、1枚残しておくための型です。

DBが急に重くなったとき、いちばんつらいのは「原因が見えないまま、時間だけが過ぎること」だと思います。落ちてくれれば追えるのに、動いてはいる。その中途半端さが、いちばん人を消耗させます。
でも、DBはちゃんと答えを持っています。いま何をしているかも、さっきまで何に時間をかけていたかも、聞けば教えてくれます。 必要なのは、聞き方をひとつ知っておくことだけです。
今日、processlist を引くSQLをメモに貼るところからで十分です。次に「重いんですけど」と言われたとき、その1行があるかないかで、最初の5分がまるで変わります。
ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。