本番DBでやってはいけないSQL|WHERE忘れ・TRUNCATE・確認手順
WHERE 忘れ、= NULL、TRUNCATE。本番 DB で事故になる SQL と、実行前に確認する手順をまとめます。
本番データベースでの事故は、難しい SQL ではなく短い SQL で起きます。WHERE 句を 1 つ書き忘れる、条件の書き方を少し間違える、取り消せない命令を取り消せると思い込む。どれもエラーにならずに実行が終わってしまうため、気づいたときには影響が広がっています。ダブルチェックをするベテランが SQL を見た瞬間に手を止める箇所は、だいたい決まっています。この記事では、その箇所と理由、実行前に確認する手順を順に説明します。
UPDATE・DELETE の WHERE 忘れは全行が対象になる
UPDATE と DELETE は、WHERE 句で対象を絞らなければ表の全行を対象にします。文法としては正しいので、データベースはエラーを返さず、そのまま全件を書き換えます。よくあるのは、エディタで複数行の SQL を書き、1 行目だけを選択して実行してしまうケースです。画面上には WHERE が書いてあっても、データベースに送られた文には含まれていません。
UPDATE usersSET status = 'inactive'WHERE id = 42;ベテランが見るのは、WHERE の中身より先に「WHERE があるか」「その文が 1 文として送られるか」です。1 行で書く、実行直前に文全体を読み上げる、クライアントの安全機能を使う、といった対策で、選択ミスによる全件更新を防ぎやすくなります。MySQL のコマンドラインクライアントには --safe-updates(別名 --i-am-a-dummy)というオプションがあり、キー列を使った WHERE も LIMIT も無い UPDATE・DELETE を拒否できます。
対話型クライアントでは、文末のセミコロンを入力して Enter を押した時点で文が実行されます。UPDATE users SET status = 'inactive'; と打ってから WHERE を足そうとしても間に合いません。WHERE 句まで書いてから最後にセミコロンを付ける順番にすると、途中で送信される事故を減らせます。
条件の書き間違いは「0 件」か「多すぎる件数」になる
= NULL は 1 行にも一致しない
SQL の NULL は「値が不明」を表します。NULL との比較は真でも偽でもなく不明(UNKNOWN)になり、WHERE 句は真の行だけを残すので、WHERE email = NULL は 1 行にも一致しません。UPDATE でこれを書くと「0 rows affected」で正常終了し、作業者は修正したつもりのまま作業を終えます。全件更新ほど目立ちませんが、直したはずのデータが直っていないという別の事故になります。NULL を探すときは IS NULL、NULL でない行は IS NOT NULL を使います。
-- 危ない例:どの行にも一致しないUPDATE users SET email_verified = 0 WHERE email = NULL;-- 安全な例UPDATE users SET email_verified = 0 WHERE email IS NULL;AND と OR の優先順位
AND は OR より先に評価されます。「企業 42 の、trial または expired のユーザー」のつもりで company_id = 42 AND plan = 'trial' OR plan = 'expired' と書くと、(company_id = 42 AND plan = 'trial') OR plan = 'expired' と解釈され、他社の expired ユーザーまで対象になります。OR を含む条件は括弧で範囲を明示するか、IN を使うと読み違いが起きにくくなります。
-- 危ない例UPDATE users SET plan = 'free'WHERE company_id = 42 AND plan = 'trial' OR plan = 'expired';-- 安全な例UPDATE users SET plan = 'free'WHERE company_id = 42 AND plan IN ('trial', 'expired');実行前に SELECT COUNT(*) で対象件数を確かめる
書き換える前に、同じ WHERE 句で SELECT を実行して対象を確認します。件数だけでなく、数行を実際に表示して「意図した行か」を目で見るのが基本です。手順書に「想定件数 3 件」のように事前の見込みが書いてあれば、そこと一致するかを比べます。見込みが無い場合は、件数が業務的に妥当かを作業者とチェッカーの両方で判断します。
SELECT COUNT(*) FROM orders WHERE status = 'canceled' AND created_at < '2026-01-01';SELECT id, status, created_at FROM orders WHERE status = 'canceled' AND created_at < '2026-01-01' LIMIT 10;DELETE FROM orders WHERE status = 'canceled' AND created_at < '2026-01-01';大事なのは、SELECT と DELETE で WHERE 句を一字一句同じにすることです。SELECT で確かめた後に DELETE を手で打ち直すと、そこで別の誤字が入ります。WHERE 句をコピーして先頭だけを付け替えると、確認した条件と実行する条件のずれを防げます。存在しない列名を書いた場合はエラーになりますが、別の実在する列名(たとえば order_id のつもりで id)を書いた場合はエラーにならず、違う行が対象になる点にも注意が必要です。
トランザクションで囲み、影響行数を見てから COMMIT する
多くのクライアントは自動コミット(autocommit)が有効で、1 文ごとに即座に確定します。BEGIN(START TRANSACTION)で明示的にトランザクションを始めると、COMMIT するまでは ROLLBACK で取り消せます。実行後にデータベースが返す影響行数(MySQL なら「Query OK, 3 rows affected」、PostgreSQL なら「UPDATE 3」)を、事前の SELECT COUNT(*) の結果と比べ、一致したときだけ COMMIT します。
BEGIN;UPDATE users SET status = 'inactive' WHERE id IN (101, 102, 103);-- 影響行数が 3 であることを確認するSELECT id, status FROM users WHERE id IN (101, 102, 103);COMMIT; -- 想定と違えば ROLLBACK;トランザクションを開いたまま長く放置すると、更新した行のロックが保持され、本番のアプリケーションが待たされます。BEGIN から COMMIT までの間に調べ物や相談を挟まないよう、確認用の SELECT まで含めて手順を事前に用意しておくと安全です。
TRUNCATE・DROP は取り消せない前提で扱う
DELETE が行を消すのに対し、TRUNCATE は表の中身を一括で空にし、DROP TABLE は表の定義ごと削除します。どちらも 1 行ずつ処理しないので速い反面、取り消しの可否がデータベースによって違います。
| 命令 | 消えるもの | ROLLBACK | 補足 |
|---|---|---|---|
| DELETE FROM t WHERE ... | 条件に合う行 | トランザクション内なら可能 | 影響行数が返る。大量だと遅い |
| TRUNCATE TABLE t | 全行 | MySQL は不可(暗黙のコミット)。PostgreSQL はトランザクション内なら可能 | SQLite には無く、WHERE の無い DELETE が相当する |
| DROP TABLE t | 表の定義と全行 | MySQL は不可。PostgreSQL はトランザクション内なら可能 | 参照するビューや権限設定にも影響する |
データベースの種類によって挙動が違うということ自体が、本番で危険な理由です。普段 PostgreSQL を使っている人が MySQL で同じ感覚で TRUNCATE すると、ROLLBACK が効きません。本番でこれらを実行する手順では、直前にバックアップを取得し、そこから戻す手順まで書いておくのが一般的です。小さな表なら CREATE TABLE users_bk_20261016 AS SELECT * FROM users; のように退避用の表を作っておく方法もあります。
LIMIT は安全装置にも事故の原因にもなる
MySQL では DELETE や UPDATE に LIMIT を付けて、1 回に処理する行数の上限を決められます。大量の行を少しずつ消すと、ロックの時間やレプリケーションの遅延を抑えられます。一方で、ORDER BY の無い LIMIT はどの行が選ばれるかが決まっていないため、「最初の 1 件だけ直す」つもりで使うと意図しない行が変わります。また PostgreSQL の UPDATE・DELETE には LIMIT がなく、SQLite も特定のコンパイルオプションが無いと使えません。別のデータベースで動いた SQL がそのまま通るとは限らない点も確認対象です。
実行前のチェックリスト
- 接続先が本番か検証かを、プロンプトや SELECT current_database() などで確認する。
- UPDATE・DELETE に WHERE 句があり、文全体が 1 文として送られることを確認する。
- NULL の比較が IS NULL になっているか、OR を含む条件に括弧があるかを確認する。
- 同じ WHERE 句で SELECT COUNT(*) と数行の SELECT を実行し、想定件数と比べる。
- BEGIN で開始し、影響行数が事前の件数と一致したら COMMIT、違えば ROLLBACK する。
- TRUNCATE・DROP・ALTER の前には、バックアップの取得と戻し方を手順に書いておく。
- 実行した SQL、影響行数、COMMIT した時刻を作業記録に残す。
これらはどれも、実際に起きた失敗から生まれた確認です。慣れた作業ほど確認を省きたくなりますが、本番の SQL は 1 文の誤りがそのまま顧客のデータに反映されます。下の問題で、危ない SQL を見抜けるか試してみてください。
問題で確かめる
この記事の内容を、制限時間つきの問題で確かめられます。
- id 42 だけ無効化したいのに全行が更新された。UPDATE 文を 1 行で正しく直して書いてください。SQL / 記述
- DELETE FROM users; を実行した結果は?SQL / 4 択
- DELETE FROM t; と DROP TABLE t; は同じ結果になる。SQL / ○×
- id が 42 のユーザーだけを無効化したい。直したものは?SQL / 4 択
- 更新を 1 つのトランザクションとして確定する正しい順番は?SQL / 4 択
- BEGIN の後に UPDATE を実行し、まだ COMMIT していない。取り消すには?SQL / 4 択
- メールアドレスが未登録のユーザーを出したい。直したものは?SQL / 4 択
- この SQL の最も危ない点は?実務シチュエーション / 4 択
- この SQL で最も危ない点は?実務シチュエーション / 4 択