「サイトが重い」と連絡を受けて、ターミナルとデータベースの管理画面を並べて開き、どこから見るか考えている一人運用の保守担当者

急に重くなったDB|スロークエリの特定と一次対処の順番

「サイトが重いんですけど」

昼過ぎにその一言が届いて、開いてみたら確かに重い。昨日までは普通だったのに、今日は一覧ページが10秒待っても出てこない。サーバーのCPUを見ると、DBのプロセスだけが高い。

ここで、たぶん一度は思ったことがあるはずです。「DBが重いのは分かった。で、何が重いんだ?」

DBの遅さがやっかいなのは、エラーが出ないことです。落ちてくれれば原因を追えるのに、動いてはいる。ただ、遅い。ログにも「500」の文字はない。それでいて、電話とチャットだけが増えていきます。

この記事では、その状態から抜けるための順番を整理します。特別なツールは使いません。DBに標準で付いている機能だけで、犯人のクエリまでたどり着けます。

結論:急に重くなったDBは、次の順で見ます。①いま走っているクエリを一覧で見る(実行時間の長い順) → ②影響が大きく、止めても安全な1本だけを止める → ③スロークエリログを有効にして、記録として残す → ④落ち着いてから EXPLAIN で「なぜ遅いか」を読む → ⑤インデックスや処理の見直しは、検証してから本番へ。急いでいるときにやるのは①と②だけで十分です。③以降は、詰まりがほどけてからで間に合います。そして③は、次に同じことが起きたときの自分をいちばん助けてくれます。

なお、以下のコマンドや設定名はMySQL/MariaDB/PostgreSQLを前提に書いています。バージョンや構成(マネージドDBか自前かなど)で使える機能や書き方が変わるので、本番で実行する前に、必ずお使いのDBの公式ドキュメントで確認してください。

何が起きているか:「重い」の正体は、たいてい一点の詰まり

サイト全体が重いと、「サーバーの性能が足りないのでは」と考えたくなります。でも、急に重くなったのなら、その線は薄いことが多いです。サーバーの性能は昨日から変わっていないからです。

急な遅さで、実際によくあるのはこの3つです。

そして、詰まりは連鎖します。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か所だけ

一覧が出たら、上から数行だけ見ます。

同じクエリが何本も並んでいるときは、1本が遅いのではなく「遅いクエリに次々アクセスが来て、後ろに積み上がっている」状態です。原因のクエリは1種類です。

「本当にDBなのか」を10秒で確かめる

一覧を見ても長いクエリがなく、接続もスカスカなら、遅さの原因はDBの外かもしれません。アプリの処理、外部APIの応答待ち、ディスクの空き不足など、行き先はいくつもあります。切り分けの全体像は「サイトが重い」の切り分け手順に、上流から順に見る型はタイムアウトはどこで起きているかにまとめています。

ここで方向を間違えると、あとの時間がまるごと無駄になります。「DBに長いクエリが実際に見えたか」を、次に進む前の関門にしてください。

手順2:止めるのは、影響が大きくて安全な1本だけ

遅いクエリを見つける方法には「いま走っているものを一覧で見る」入口と「記録に残ったログから探す」入口の2つがあり、どちらからでも原因のクエリにたどり着けることを示した図
急いでいるときは「いま見る」、落ち着いてからは「ログで見る」。入口は2つあり、行き先は同じ

犯人らしき1本が見えたら、次は「止めるかどうか」の判断です。ここは慎重に、でも止めるときは迷わずに。

止めていいか、を分けるのは「読むだけか」どうか

止め方(ピンポイントで、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つあります。

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つ

専門用語がずらりと出ますが、最初に見るのはここだけで大丈夫です。

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. 呼ぶ回数を減らせないか。 一覧ページで1行ごとにクエリが飛んでいる(いわゆるN+1)なら、まとめて取る形に変えるだけで劇的に軽くなります。インデックスを足すより、そもそも呼ばないほうが速いです。
  2. 取る量を減らせないか。 SELECT * を必要な列だけにする、件数の上限を付ける、期間で絞る。管理画面の「全件表示」が原因だった、というのは本当によくあります。
  3. 重い処理の時間帯をずらせないか。 集計バッチを日中から夜間に移すだけで、昼のピークが消えることがあります。コードを1行も触らずに済む、いちばん安い一手です。
  4. インデックスを足す。 効果は大きいですが、追加そのものがテーブルをロックすることがあります。PostgreSQLなら CREATE INDEX CONCURRENTLY、MySQL 8.0以降なら多くのケースでオンラインDDLが使えますが、構成とバージョンで挙動が違います。必ず検証環境で所要時間を測ってから、本番反映前チェックリストとDBマイグレーションを本番へ安全に当てる手順の型に乗せて実施してください。インデックスは書き込みを少しずつ遅くするので、「とりあえず全部の列に」は逆効果です。

そして、どの手を打つときも記録を残してください。「いつ・どのクエリに・何をしたか」を変更管理台帳に1行。急いで直したものほど、半年後の自分が理由を思い出せません。応急処置で終えた場合は、暫定対応と恒久対応を分けて管理するの形で「戻す・直す」を残しておくと、置き去りになりません。

具体例:一覧ページだけが重くなった木曜日

社内の受注管理システムを一人で保守している担当者の例です。

振り返ってみると、この日いちばん効いたのは高度な技術ではありません。13時23分に「processlistを実行時間順で見た」ことです。そこで犯人が確定したから、その後の1時間が調査に使えました。

影響:記録が残ると、何が変わるか

一方で、増える手間もあります。ログのぶんディスクを使いますし、pg_stat_statements はわずかですがオーバーヘッドがあります。ただ、遅さの原因が分からないまま何時間も探す消耗に比べれば、ずっと軽い負担です。

明日やること:3つとも、5分で終わります

障害が起きていない今日のうちに、やっておくと効くことです。どれもサービスに影響しません。

  1. いまスロークエリログが有効かどうかを見るだけ。
   -- MySQL / MariaDB
   SHOW VARIABLES LIKE 'slow_query_log';
   SHOW VARIABLES LIKE 'long_query_time';
   -- PostgreSQL
   SHOW log_min_duration_statement;

OFF や -1 なら、記録は残っていません。「いざというとき、遡れないんだ」と知っておくだけで違います。

  1. processlist / pg_stat_activity を引くSQLを、メモに1行貼っておく。

障害の最中にコマンドを思い出そうとすると、それだけで数分溶けます。手順書に貼っておけば、コピーして貼るだけです。運用ドキュメントの最低限テンプレートの中に置き場所を作っておくと、引き継ぎでもそのまま使えます。

  1. ログが有効なら、いまたまっているぶんを一度だけ眺めてみる。
   mysqldumpslow -s t -t 5 /var/log/mysql/slow.log

何も出なければ、いまは健康ということです。それが分かるのも収穫です。

スロークエリ対応チェックリスト

全部そろえる前提ではありません。上から優先順位で並べています。

重くなっている、まさにいま

落ち着いたら

直すとき

次に備える

「重くなっている、まさにいま」の最初の2つができれば、その日は乗り切れます。残りに○が付かない日があっても大丈夫です。

よければ、こちらも

DBの遅さは、前後の切り分けとつなげると一段ラクになります。

対応の翌朝、遅かった画面がすぐに表示されるようになったことを確認して、穏やかに息をついている保守運用担当者

DBが急に重くなったとき、いちばんつらいのは「原因が見えないまま、時間だけが過ぎること」だと思います。落ちてくれれば追えるのに、動いてはいる。その中途半端さが、いちばん人を消耗させます。

でも、DBはちゃんと答えを持っています。いま何をしているかも、さっきまで何に時間をかけていたかも、聞けば教えてくれます。 必要なのは、聞き方をひとつ知っておくことだけです。

今日、processlist を引くSQLをメモに貼るところからで十分です。次に「重いんですけど」と言われたとき、その1行があるかないかで、最初の5分がまるで変わります。

ほかの実務ヒントは記事一覧からどうぞ。保守運用の小さな備えを、メールでも少しずつお届けしています。

関連用語