記事の目的
SQL の DELETE 文を使って、テーブルから行を削除できるようになります。
WHERE 句で条件を付けて特定の行だけ消す書き方、全件削除、よく似た TRUNCATE ・ DROP との違い、外部キー制約があるときの挙動、トランザクションでロールバックする方法までを、PostgreSQL・MySQL・SQL Server の製品差に注意しながら整理します。
重要な注意(DELETE はデータを失わせうる破壊的操作です)
DELETE で削除した行は、トランザクションを使わずに実行した場合、原則として元に戻せません。WHERE 句を書き忘れると全件削除になり、復旧はバックアップからのリストアしか方法がありません。
本記事のコードを本番環境で実行する前に、必ずバックアップを取得し、テスト環境で同じ操作を試してから実行してください。実行前には、DELETE を SELECT に置き換えて影響範囲を確認する、または明示的なトランザクション( BEGIN; … ROLLBACK / COMMIT; )で囲む、といった習慣を強くおすすめします。
記事のサンプルテーブルの前提
本記事では、説明用に次のような orders テーブルがある前提で進めます。
- id :自動採番(注文番号)
- customer_id :顧客 ID(文字列)
- amount :金額(整数。
NULLを含む可能性あり) - status :注文ステータス(
'pending'/'shipped'/'cancelled'のいずれか) - order_date :注文日(日付型)
SELECT * FROM orders;
id | customer_id | amount | status | order_date
---+-------------+--------+-----------+-----------
1 | C001 | 1500 | shipped | 2026-01-10
2 | C002 | 800 | pending | 2026-02-05
3 | C001 | 3000 | shipped | 2026-02-15
4 | C003 | 500 | cancelled | 2026-03-01
5 | C002 | NULL | pending | 2026-03-10
最短の答え ── 安全な DELETE 手順
DELETE は元に戻せない可能性があるため、いきなり実行せず、次の順で進めるのが安全です。下の例 PostgreSQL のトランザクション構文で書いています。MySQL / SQL Server の構文差は後述の「トランザクションとロールバック」を参照してください。
SELECT COUNT(*) FROM orders WHERE status = 'cancelled';
SELECT * FROM orders WHERE status = 'cancelled';
BEGIN;
DELETE FROM orders WHERE status = 'cancelled';
SELECT * FROM orders;
SELECT COUNT(*) FROM orders WHERE status = 'cancelled'; -- 0 になっているはず
COMMIT; -- 確定
-- または
ROLLBACK; -- 取り消して元の状態に戻す- 手順 1:SELECT で削除対象を確認
- 手順 2:トランザクションを開始(PostgreSQL の例)
- 手順 3:DELETE を実行
- 手順 4:削除後の状態を確認
- 手順 5:問題なければ COMMIT、間違いなら ROLLBACK
「全件削除」「 TRUNCATE での高速で空にする」は別物として扱うべき操作なので、本文の「全件削除(WHERE を書かない)」「TRUNCATE との違い」「DROP との違い」のセクションで個別に説明します。
ここから先は、DELETE の基本構文・ WHERE 条件・全件削除・ TRUNCATE / DROP との違い・外部キー制約・ロールバックまでを順に解説します。
DELETE 構文の基本
DELETE は、テーブルから条件に合う行を削除する DML(データ操作言語)です。
DELETE FROM テーブル名
WHERE 条件;DELETE FROMのあとに、行を消したいテーブル名を書きます。WHERE句で「どの行を消すか」を指定します。WHERE句を書かないと全行が削除対象になります。
列を指定する SELECT と違い、DELETE は行単位の削除です。「特定の列だけ消したい」場合は UPDATE 〜 SET 列 = NULL などで列の値を変えます。
元のテーブルは行単位で変化します。SELECT のように結果を返すだけの操作ではありません。
WHERE 句で条件付き削除
実務でもっとも使うのは、WHERE で対象を絞った削除です。
-- 例1: キャンセル済みの注文を削除
DELETE FROM orders
WHERE status = 'cancelled';
-- 例2: 2026年1月より前の注文を削除
DELETE FROM orders
WHERE order_date < '2026-01-01';
-- 例3: 複数条件を組み合わせる
DELETE FROM orders
WHERE status = 'cancelled'
AND order_date < '2026-01-01';WHERE の書き方は SELECT で使うものと同じです。詳しくは別記事「SQL WHERE 句の使い方」を参照してください。
実行前に SELECT で影響範囲を確認する
DELETE の WHERE を書いたら、そのまま SELECT に置き換えて件数と内容を確認するのが事故防止の基本です。
-- まず SELECT で確認
SELECT COUNT(*) FROM orders WHERE status = 'cancelled';
SELECT * FROM orders WHERE status = 'cancelled';
-- 想定通りの件数・内容であれば DELETE を実行
DELETE FROM orders WHERE status = 'cancelled';WHERE 句の条件が思い込みで書かれていないかを、SELECT の結果で必ず一度見てから DELETE に切り替える、というルーティンをおすすめします。
PostgreSQL の RETURNING(補足)
PostgreSQL では DELETE の末尾に RETURNING を付けると、削除した行を結果として返せます。実行内容のログ取りや、削除と同時にバックアップを取りたい場面で便利です。詳しくは PostgreSQL 固有の別記事で扱います。
-- PostgreSQL のみ:削除した行を返す
DELETE FROM orders
WHERE status = 'cancelled'
RETURNING *;全件削除(WHERE を書かない)
WHERE を付けない DELETE は、テーブルの全行を削除します。書き忘れや誤コピーで本番データを全消しする事故の典型です。
DELETE FROM orders; -- すべての行を削除次のような工夫で防ぎます。
- 全件削除のつもりがない場合、
WHEREを書く前にエディタ上でDELETE FROM orders WHEREまで先に打ち、WHERE句を埋めるクセを付ける。 - 全件削除のつもりでも、必ず明示的なトランザクション(
BEGIN;…ROLLBACK/COMMIT;)で囲み、件数確認後にCOMMITする。 - 本番環境では、
WHEREのないDELETEを実行しないようにレビューを必須にする。
「本当にテーブルを空にしたい」場合、次節の TRUNCATE も選択肢になります。
TRUNCATE との違い(製品差が大きい)
TRUNCATE TABLE は、テーブル構造を残したまま全行を高速に削除するためのコマンドです。MySQL では DDL として扱われ暗黙的にコミットされますが、PostgreSQL や SQL Server ではトランザクション内でロールバックできるなど、製品によって挙動が異なります。
DELETE との大枠の違いは次の通りです。
| 観点 | DELETE FROM テーブル; | TRUNCATE TABLE テーブル; |
|---|---|---|
| 行を消す単位 | 1 行ずつ削除(ログに記録) | テーブルの中身を一括で空にする |
| WHERE 句 | 使える | 使えない(全件のみ) |
| 大きなテーブルでの速度 | 行数に比例して遅い | 一般に高速 |
| トリガー(DELETE トリガー) | 発火する | 製品により異なる(多くは発火しない) |
TRUNCATE の製品差(要確認)
| 製品 | ロールバック | 自動採番のリセット | DELETE トリガー | 外部キー参照時 |
|---|---|---|---|---|
| PostgreSQL | トランザクション内で可 | 既定ではリセットしない( RESTART IDENTITY 指定時のみリセット) | 発火しない(TRUNCATE トリガーが別途ある) | 参照されていると失敗( CASCADE で連鎖 TRUNCATE 可) |
| MySQL | 不可(DDL 扱いで暗黙 COMMIT が発生) | AUTO_INCREMENT がリセットされる | 発火しない | 参照されていると基本失敗 |
| SQL Server | トランザクション内で可 | IDENTITY がリセットされる | 発火しない | 参照されているテーブルには基本使えない |
実務での使い分けの目安は次の通りです。
- 行の一部だけ削除したい →
DELETE+WHERE - 全件削除+ロールバック可能性が必要 →
DELETE FROM テーブル;をトランザクション内で - 全件削除+テーブルをまっさらにしたい・高速にしたい →
TRUNCATE TABLE テーブル;(製品差を確認したうえで)
上記の挙動は版や設定で変わります。本番で使う前に、各製品の現行ドキュメントで「ロールバック可否」「自動採番のリセット」「外部キー参照時の挙動」を必ず確認してください。
DROP との違い
「全部消したい」系の操作には DROP TABLE もあります。DELETE ・ TRUNCATE ・ DROP の違いを表にまとめます。
| 操作 | 消えるもの | テーブル構造 | 主な用途 |
|---|---|---|---|
| DELETE | 条件に合う行 | 残る | 一部の行を削除( WHERE 必須が原則) |
| TRUNCATE | すべての行 | 残る | テーブルを高速に空にする |
| DROP | テーブル自体 | 消える | テーブル定義ごと削除 |
DROP TABLE は、表のメタデータ・インデックス・トリガー・制約まで丸ごと消す DDL です。よくある事故が「行を全部消したかっただけなのに DROP TABLE してテーブル定義ごと失った」というもので、TRUNCATE と DROP を取り違えないように注意します。
外部キー制約と DELETE
orders.customer_id が customers.id を参照する外部キー制約を持っている、という状態を考えます。逆向きに、customers 側の行を DELETE しようとすると、orders から参照されている行はそのままでは削除できません(外部キー違反のエラー)。
対処は次のいずれかです。
- 先に
orders側で対応する行を削除してから、customersを削除する - 外部キー制約に
ON DELETE CASCADEを指定しておく(親を消すと、参照していた子も連鎖削除) - 外部キー制約に
ON DELETE SET NULLを指定しておく(子の参照列をNULLに書き換える)
ON DELETE CASCADE は便利ですが、意図しない大量削除を引き起こす可能性が高いため、運用ルールと合わせて慎重に設計してください。詳しい外部キー制約の話は別記事「主キーと外部キーとは?違いと設定方法」で扱います。
トランザクションとロールバック
DELETE を本番で実行するときは、明示的なトランザクションで囲み、件数を確認してから確定する、というのが安全です。
-- PostgreSQL の例
BEGIN;
DELETE FROM orders WHERE status = 'cancelled';
SELECT COUNT(*) FROM orders WHERE status = 'cancelled'; -- 0 になるはず
COMMIT; -- 確定
-- ROLLBACK; -- 間違っていたら戻す製品差
3 製品とも、DELETE を明示的なトランザクションで囲めば ROLLBACK できます。書き方は少し異なります。
| 製品 | トランザクションの書き方 |
|---|---|
| PostgreSQL | BEGIN; … COMMIT; / BEGIN; … ROLLBACK; |
| MySQL | START TRANSACTION; … COMMIT; または BEGIN; … COMMIT; (InnoDB エンジンで動作) |
| SQL Server | BEGIN TRANSACTION; … COMMIT; または ROLLBACK; |
なお、3 製品とも既定では自動コミット(autocommit)が有効です。明示的にトランザクションを開始しないと、DELETE を実行した時点で確定してしまい、ROLLBACK できません。詳しいトランザクションの話は別記事で扱います。
落とし穴・注意点
以下は本番環境で実際に問題になりやすいポイントです。実行前に必ず確認してください。
WHERE を書き忘れると全件削除
最も多い事故が、DELETE FROM orders WHERE … と書こうとして WHERE 以降を打つ前に Enter を押し、DELETE FROM orders; を実行してしまうパターンです。
予防策として有効なのは次のような習慣です。
SELECTで件数を確認してからDELETEに置き換える- 必ず明示的なトランザクション(
BEGIN;…COMMIT;)で囲む - 本番ではアプリケーション経由でしか実行しない/レビュー必須にする
NULL の扱い
WHERE 句で NULL を比較するときは、= NULL ではなく IS NULL を使います。これは DELETE でも同じです。
-- amount が NULL の行を削除したい
DELETE FROM orders WHERE amount IS NULL; -- 正しい
DELETE FROM orders WHERE amount = NULL; -- ヒットしない(UNKNOWN になる)大量削除のロックとパフォーマンス
大量の行を一度に DELETE すると、テーブルや行に対するロックが長時間取られ、他のクエリが待たされることがあります。本番運用では、WHERE で対象を絞り、バッチ単位で繰り返し削除するのが定石です。書き方は製品で異なります。
| 製品 | バッチ削除の書き方 |
|---|---|
| MySQL | DELETE FROM orders WHERE status = 'cancelled' LIMIT 1000; を繰り返す |
| SQL Server | DELETE TOP (1000) FROM orders WHERE status = 'cancelled'; を繰り返す |
| PostgreSQL | DELETE 文に直接 LIMIT は付かないため、CTE で対象を絞る: WITH t AS (SELECT id FROM orders WHERE status = 'cancelled' LIMIT 1000) DELETE FROM orders USING t WHERE orders.id = t.id; |
その他、次の点もセットで考えます。
- 大量削除のあと、製品によってはインデックスや統計情報の再構築が必要になる
- 本当に全件で十分なら、
DELETEではなくTRUNCATE TABLEを検討する
トリガーと連鎖削除
BEFORE DELETE / AFTER DELETE トリガーが設定されていると、DELETE のたびにトリガーが発火します。ON DELETE CASCADE の連鎖削除も同様に他テーブルに影響します。意図しない削除が起きていないかを、トリガー定義と外部キー設定で必ず確認してください。
文字列リテラルはシングルクォート
WHERE status = "cancelled" のようにダブルクォートを使うと、製品によっては「識別子(列名)」と解釈されエラーになります。文字列はシングルクォート ' で囲みます。詳しくは別記事「SQL WHERE 句の使い方」を参照。
まとめ
DELETE 文はテーブルから行を消す基本構文ですが、WHERE の書き忘れによる全件削除・ TRUNCATE / DROP との違い(特に TRUNCATE はロールバック可否や自動採番リセットで製品差が大きい)・外部キー制約・トランザクションでのロールバック、の 4 点を押さえれば、現場の削除はほとんど安全に行えます。本番環境では、バックアップ → SELECT で確認 → BEGIN; DELETE; COMMIT; の流れを毎回守ることをおすすめします。
