公開日 2026-09-05

PostgreSQLの外部キー:RESTRICT と NO ACTION は何が違うのか

PostgreSQL の外部キーで `RESTRICT` と `NO ACTION` は何が違うのかを、既定では同じに見えて `DEFERRABLE INITIALLY DEFERRED` のときだけ差が出ることを `psql` の実行結果で確認する。

目次

  1. 前提環境
  2. 1. 何が同じで、何が違うのか
  3. 2. 最小デモ環境を作る
  4. 3. スキーマを用意する
  5. 4. 既定では RESTRICT と NO ACTION は見分けがつかない
  6. 5. DEFERRABLE を付けると差が出る
  7. 6. RESTRICT は DEFERRABLE を付けても遅延できない
  8. 7. どう使い分けるか

外部キーに 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既定文の実行時(即時)
RESTRICTDEFERRABLE INITIALLY DEFERRED文の実行時(即時のまま。遅延できない)
NO ACTION既定文の実行時(即時)
NO ACTIONDEFERRABLE 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 のアクション全体は 削除ルールの記事 にまとめてあります。