外部キーに ON DELETE を書かないと、既定は NO ACTION になります。これを RESTRICT と見比べると、どちらも「子行が残っている親を削除しようとするとエラーで止まる」という同じ動きに見えます。実際、ほとんどの場面で区別はつきません。違いは1点、制約チェックを遅延できるかどうかに集約されます。
この記事では customers / orders の小さなスキーマを使い、psql から両方を実際に動かして、差が出る条件だけを確認します。
先に整理すると:
- 既定(非
DEFERRABLE)ではRESTRICTとNO ACTIONは同じに見える。どちらも親削除の文でその場でエラーになる - 違いが出るのは
DEFERRABLE INITIALLY DEFERREDを付けたとき。NO ACTIONはチェックをCOMMIT時まで遅延できるが、RESTRICTは遅延できず常に即時 CASCADE/SET NULLを含むON DELETE全体の整理は PostgreSQLの外部キーと削除ルールを整理する(CASCADE / RESTRICT / SET NULL) にある。ここはRESTRICTとNO ACTIONの差分だけに絞る
特定のプログラミング言語や ORM は対象外です。再現範囲は Docker + PostgreSQL + psql に絞ります。
前提環境
- Windows 11
- WSL2(Ubuntu)
- VS Code(Remote - WSL)
- Docker Desktop
- PostgreSQL 17
psql
以降のコマンド実行場所は、特記がない限り WSL 側ターミナルです。
1. 何が同じで、何が違うのか
RESTRICT と NO ACTION は、ON DELETE に指定できるアクションの中では「親削除を止める」側です。CASCADE が子ごと消し、SET NULL が子の参照列を NULL にするのに対し、この2つは子行が残っている間、親行の削除を外部キー違反エラーにします(親キーの更新側の挙動は ON UPDATE の指定で決まる別の話で、この記事では扱いません)。ここまでは共通です。
差は、公式ドキュメントの一文に集約されています。RESTRICT は NO ACTION と同じ動作で、ただしチェックを遅延できない、という点だけが違います。
「遅延」とは、制約チェックをその文の実行時ではなく、トランザクションの終わり(COMMIT)まで先送りする仕組みです。制約を DEFERRABLE INITIALLY DEFERRED で宣言したときに有効になります。宣言しなければチェックは即時で、その場合この2つの観測できる挙動は一致します。
flowchart TD
A["親を DELETE"] --> B{"参照する子行が残っている?"}
B -->|いいえ| Z["削除できる"]
B -->|はい| C{"制約は DEFERRABLE INITIALLY DEFERRED?"}
C -->|"いいえ(既定)"| E["その場でエラー<br/>RESTRICT / NO ACTION とも同じ"]
C -->|はい| D{"アクションは?"}
D -->|NO ACTION| F["COMMIT まで先送り<br/>途中で子を直せば通る"]
D -->|RESTRICT| E
以降、この分岐を psql で1つずつ踏んで確認します。
2. 最小デモ環境を作る
記事用の空ディレクトリを用意します。
mkdir -p ~/projects/postgresql-foreign-key-restrict-vs-no-action-demo
cd ~/projects/postgresql-foreign-key-restrict-vs-no-action-demo
mkdir -p sql
code .
構成は次の3ファイルです。
postgresql-foreign-key-restrict-vs-no-action-demo/
├─ compose.yml
├─ .env.example
└─ sql/
└─ 01-schema.sql
compose.yml を作成します。アプリケーションコンテナは置かず、DB の挙動だけを見ます。
services:
db:
image: postgres:17
working_dir: /workspace
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
- ./:/workspace
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 20
volumes:
db-data:
.env.example を作成します。
POSTGRES_DB=app
POSTGRES_USER=app
POSTGRES_PASSWORD=app
起動して、接続できることを確認します。
cp .env.example .env
docker compose up -d
docker compose exec db pg_isready -U app -d app
docker compose exec db psql -U app -d app -c "SELECT version();"
/var/run/postgresql:5432 - accepting connections
version
--------------------------------------------------------------------------------------------------------------------
PostgreSQL 17.9 (Debian 17.9-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)
詰まったときは、ログを見てから作り直します。
docker compose logs db
docker compose down -v
3. スキーマを用意する
sql/01-schema.sql を作成します。まずは orders.customer_id を ON DELETE RESTRICT で作ります。customers 1 と 2 を入れ、1 には注文を2件ぶら下げておきます。
DROP TABLE IF EXISTS orders, customers CASCADE;
CREATE TABLE customers (
id bigint PRIMARY KEY,
name text NOT NULL
);
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer_id bigint NOT NULL,
CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT
);
INSERT INTO customers (id, name) VALUES (1, 'Alice'), (2, 'Bob');
INSERT INTO orders (id, customer_id) VALUES (100, 1), (101, 1), (200, 2);
流します。
docker compose exec db psql -U app -d app -f sql/01-schema.sql
以降は対話セッションで確認します。
docker compose exec db psql -U app -d app
4. 既定では RESTRICT と NO ACTION は見分けがつかない
まず RESTRICT のまま、注文が残っている customers 1 を消してみます。あとで注文も消すつもりで、トランザクションで囲みます。
BEGIN;
DELETE FROM customers WHERE id = 1;
ROLLBACK;
BEGIN
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
DETAIL: Key (id)=(1) is still referenced from table "orders".
ROLLBACK
DELETE の文がその場でエラーになります。「このあと注文を消すから」という意図は通じません。親を消す文の時点で、参照する子行が残っていれば止まります。
次に、制約を NO ACTION に張り替えて同じことをします。ON DELETE を書かなかったときの既定がこれです。
ALTER TABLE orders DROP CONSTRAINT orders_customer_id_fkey;
ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE NO ACTION;
BEGIN;
DELETE FROM customers WHERE id = 1;
ROLLBACK;
ALTER TABLE
ALTER TABLE
BEGIN
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
DETAIL: Key (id)=(1) is still referenced from table "orders".
ROLLBACK
エラー文も止まる位置も、RESTRICT のときと同じです。既定のままでは、この2つに観測できる違いはありません。
「NO ACTION は文の末尾で、RESTRICT は即時にチェックする」と説明されることがあります。内部の仕組みとしてはその区別がありますが、非 DEFERRABLE では観測できる差にはなりません。1つの文で親と子をまとめて消しても(自己参照テーブルの一括 DELETE や、WITH を使った親子の同時削除でも)、既定の RESTRICT と NO ACTION はどちらも通ります。挙動が分かれるのは、次章で付ける DEFERRABLE のときだけです。
5. DEFERRABLE を付けると差が出る
制約を NO ACTION DEFERRABLE INITIALLY DEFERRED に張り替えます。これで、この制約のチェックはトランザクションの終わりまで先送りされます。
ALTER TABLE orders DROP CONSTRAINT orders_customer_id_fkey;
ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED;
同じ「親を消してから子を消す」を、今度は1つのトランザクションで通します。
BEGIN;
DELETE FROM customers WHERE id = 1;
DELETE FROM orders WHERE customer_id = 1;
COMMIT;
SELECT id, name FROM customers ORDER BY id;
BEGIN
DELETE 1
DELETE 2
COMMIT
id | name
----+------
2 | Bob
(1 row)
customers 1 を消した時点ではエラーになりません。参照の整合性チェックが COMMIT まで先送りされているためです。その間に注文2件を消し、参照が誰も残っていない状態にしてから COMMIT すると通ります。文の順番を「子が先、親が後」に組み替えなくても済むのが、遅延の効果です。
では、子を直さずに COMMIT するとどうなるか。customers 2(注文200が参照している)で試します。
BEGIN;
DELETE FROM customers WHERE id = 2;
COMMIT;
BEGIN
DELETE 1
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
DETAIL: Key (id)=(2) is still referenced from table "orders".
DELETE は DELETE 1 として通り、エラーは COMMIT を実行した時点で出ます。チェックが文から COMMIT へ移ったことが、ここで見えます。出力に COMMIT の行が無いのは、COMMIT 文そのものがエラーを返したためです。この時点でトランザクションは終了していて、追加の ROLLBACK は要りません。エラーになった COMMIT はロールバックされるので、customers 2 は残ります。
6. RESTRICT は DEFERRABLE を付けても遅延できない
ここが RESTRICT と NO ACTION を分ける唯一の点です。RESTRICT を DEFERRABLE INITIALLY DEFERRED で宣言しても、チェックは遅延されません。
ALTER TABLE orders DROP CONSTRAINT orders_customer_id_fkey;
ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
BEGIN;
DELETE FROM customers WHERE id = 2;
ROLLBACK;
ALTER TABLE
ALTER TABLE
BEGIN
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
DETAIL: Key (id)=(2) is still referenced from table "orders".
ROLLBACK
DEFERRABLE INITIALLY DEFERRED と書いてあるのに、DELETE の文でその場でエラーになります。RESTRICT のチェックは遅延できない、という仕様がそのまま出ています。5章の NO ACTION と同じ宣言でも、結果が変わります。
7. どう使い分けるか
違いを表にすると、チェックの時点だけが動きます。
ON DELETE | 宣言 | チェックの時点 |
|---|---|---|
RESTRICT | 既定 | 文の実行時(即時) |
RESTRICT | DEFERRABLE INITIALLY DEFERRED | 文の実行時(即時のまま。遅延できない) |
NO ACTION | 既定 | 文の実行時(即時) |
NO ACTION | DEFERRABLE INITIALLY DEFERRED | トランザクション終了時(既定では COMMIT) |
判断の目安です。
- 普段はどちらでもよい。既定のままなら
RESTRICTとNO ACTIONの実挙動は同じで、親を消したい文で即座に止まる。意図を読み手に示す目的でRESTRICTを選ぶ、という整理は 削除ルールの記事 のとおり - 遅延が要るのは限られた場面だけ。1つのトランザクションの途中で参照を一時的に壊し、
COMMITまでに直す操作(親を消してから子を差し替える、相互に参照するテーブルの入れ替えなど)を、文の順番に縛られず書きたいとき。このときだけNO ACTION DEFERRABLE INITIALLY DEFERREDを選ぶ(宣言をINITIALLY IMMEDIATEにして、必要な文の前だけSET CONSTRAINTS ... DEFERREDで遅延に切り替える手もあるが、本記事の範囲外) - 遅延には副作用がある。間違った削除の検出が
COMMITまで遅れるため、失敗が分かる位置が後ろにずれる。既定を非DEFERRABLEのままにしておく理由でもある
RESTRICT は「遅延させない NO ACTION」として振る舞います。制約そのものの一覧は PostgreSQLの制約入門(PRIMARY KEY / UNIQUE / CHECK / FOREIGN KEY)、ON DELETE のアクション全体は 削除ルールの記事 にまとめてあります。