PostgreSQL 23001 오류 원인과 해결 방법 완벽 가이드

23001
2026년 10월 29일 | DBMS Error 가이드

이 글에서 다루는 내용

23001 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

23001 restrict violation 는?

PostgreSQL 에러 코드 23001, restrict_violation은 외래 키(Foreign Key) 제약 조건이 RESTRICT 옵션으로 설정된 상황에서 참조되고 있는 부모 테이블의 레코드를 삭제하거나 업데이트하려 할 때 발생합니다. 즉, 자식 테이블에서 여전히 해당 레코드를 참조하고 있기 때문에 데이터 무결성을 지키기 위해 PostgreSQL이 해당 작업을 즉시 차단하는 것입니다. 이 에러는 NO ACTION 위반(23503)과 유사해 보이지만, RESTRICT는 트랜잭션 내에서도 즉시 검사가 이루어진다는 점에서 차이가 있습니다.

주요 발생 원인

  • RESTRICT 옵션이 설정된 외래 키 관계에서 부모 레코드 삭제 시도

가장 흔한 원인으로, 부모 테이블의 레코드를 삭제하려 할 때 자식 테이블에 해당 키를 참조하는 행이 남아 있을 경우 발생합니다. 예를 들어 customers 테이블의 고객을 삭제하려 하지만 orders 테이블에 해당 고객의 주문이 남아 있는 경우가 대표적입니다. RESTRICT는 트랜잭션 내 어느 시점에도 이 위반을 허용하지 않으므로, DEFERRED 체크와 달리 즉시 에러가 발생합니다.

  • RESTRICT 옵션이 설정된 외래 키 관계에서 부모 레코드의 참조 키 값 업데이트 시도

부모 테이블의 기본 키(Primary Key) 또는 참조 대상 컬럼 값을 변경하려 할 때, 자식 테이블이 기존 값을 여전히 참조하고 있으면 이 에러가 발생합니다. 예를 들어 products 테이블의 product_id를 수정하려 하는데 order_items 테이블이 기존 product_id를 참조하고 있는 경우입니다. ON UPDATE RESTRICT가 명시적으로 또는 기본값으로 설정된 경우 모두 해당됩니다.

  • 데이터 마이그레이션 또는 배치 작업 중 삭제/업데이트 순서 오류

대규모 데이터 마이그레이션이나 배치 삭제 작업 시, 자식 테이블의 데이터를 먼저 정리하지 않고 부모 테이블부터 삭제하는 순서 오류로 인해 발생하는 경우도 매우 많습니다. 특히 여러 테이블이 복잡하게 참조 관계를 맺고 있을 때 삭제 순서를 잘못 설계하면 운영 중 갑작스러운 에러로 이어질 수 있습니다. 스크립트 작성 시 참조 계층 구조를 반드시 파악하고 하위(자식)부터 상위(부모) 순으로 삭제해야 합니다.

해결 방법

원인 1 해결: 자식 데이터를 먼저 삭제한 후 부모 삭제

-- 잘못된 순서 (에러 발생)
DELETE FROM customers WHERE customer_id = 101;
-- ERROR:  update or delete on table "customers" violates foreign key constraint
-- "orders_customer_id_fkey" on table "orders"
-- DETAIL:  Key (customer_id)=(101) is still referenced from table "orders".

-- 올바른 순서 (자식 → 부모 순서로 삭제)
BEGIN;

-- 1단계: 자식 테이블 데이터 먼저 삭제
DELETE FROM order_items
WHERE order_id IN (
    SELECT order_id FROM orders WHERE customer_id = 101
);

-- 2단계: 중간 자식 테이블 삭제
DELETE FROM orders WHERE customer_id = 101;

-- 3단계: 부모 테이블 삭제
DELETE FROM customers WHERE customer_id = 101;

COMMIT;

원인 2 해결: ON UPDATE CASCADE로 외래 키 재설정

-- 기존 RESTRICT 외래 키 확인
SELECT conname, confupdtype, confdeltype
FROM pg_constraint
WHERE conrelid = 'order_items'::regclass
  AND contype = 'f';

-- 기존 제약 조건 삭제 후 CASCADE로 재생성
ALTER TABLE order_items
    DROP CONSTRAINT order_items_product_id_fkey;

ALTER TABLE order_items
    ADD CONSTRAINT order_items_product_id_fkey
    FOREIGN KEY (product_id)
    REFERENCES products(product_id)
    ON UPDATE CASCADE
    ON DELETE RESTRICT;

-- 이제 부모 키 업데이트가 자식에도 자동 반영됨
UPDATE products SET product_id = 999 WHERE product_id = 1;

원인 3 해결: 배치 삭제 시 올바른 순서 적용

-- 참조 관계 계층 파악 쿼리
SELECT
    tc.table_name AS child_table,
    kcu.column_name AS child_column,
    ccu.table_name AS parent_table,
    ccu.column_name AS parent_column,
    rc.delete_rule
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
    ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
    ON ccu.constraint_name = tc.constraint_name
JOIN information_schema.referential_constraints AS rc
    ON rc.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
ORDER BY parent_table, child_table;

-- 올바른 배치 삭제 예시 (계층 순서 준수)
BEGIN;

DELETE FROM order_items WHERE order_id IN (SELECT order_id FROM orders WHERE created_at < '2020-01-01');
DELETE FROM order_payments WHERE order_id IN (SELECT order_id FROM orders WHERE created_at < '2020-01-01');
DELETE FROM orders WHERE created_at < '2020-01-01';

COMMIT;

임시 해결: 제약 조건을 일시적으로 비활성화 (주의 필요)

-- 특정 테이블의 외래 키 트리거 비활성화 (슈퍼유저 또는 테이블 소유자 필요)
-- 운영 환경에서는 신중하게 사용할 것!
ALTER TABLE order_items DISABLE TRIGGER ALL;

DELETE FROM products WHERE product_id = 5;

ALTER TABLE order_items ENABLE TRIGGER ALL;

-- 또는 세션 수준에서 모든 트리거 비활성화
SET session_replication_role = 'replica';
DELETE FROM customers WHERE customer_id = 200;
SET session_replication_role = 'DEFAULT';

예방 방법

  • 외래 키 설계 시 ON DELETE / ON UPDATE 정책을 명확히 정의하라

테이블 설계 단계에서부터 부모-자식 관계의 삭제 및 업데이트 정책을 명확히 문서화하고 구현해야 합니다. 단순히 기본값(RESTRICT)을 사용하는 것이 아니라, 비즈니스 로직에 따라 CASCADE, SET NULL, SET DEFAULT 중 적절한 옵션을 선택해야 합니다. 아래는 권장 설계 패턴입니다.

“`sql

— 권장: 삭제 정책을 명확히 명시한 외래 키 설계

CREATE TABLE orders (

order_id SERIAL PRIMARY KEY,

customer_id INT NOT NULL,

created_at TIMESTAMPTZ DEFAULT NOW(),

CONSTRAINT fk_orders_customer

FOREIGN KEY (customer_id)

REFERENCES customers(customer_id)

ON DELETE RESTRICT — 고객 삭제 전 주문 먼저 처리 강제

ON UPDATE CASCADE — 고객 ID 변경 시 자동 반영

);

“`

  • 애플리케이션 레이어에서 삭제 전 참조 여부를 사전 검증하라

실제 삭제 작업 전에 참조 존재 여부를 미리 확인하는 검증 로직을 애플리케이션 또는 저장 프로시저 내에 구현하면 사용자에게 더 친절한 에러 메시지를 제공할 수 있습니다.

“`sql

— 삭제 가능 여부 사전 확인 함수

CREATE OR REPLACE FUNCTION can_delete_customer(p_customer_id INT)

RETURNS TABLE(can_delete BOOLEAN, reason TEXT) AS $$

BEGIN

IF EXISTS (SELECT 1 FROM orders WHERE customer_id = p_customer_id) THEN

RETURN QUERY SELECT FALSE, ‘해당 고객에게 연결된 주문이 존재합니다.’::TEXT;

ELSE

RETURN QUERY SELECT TRUE, ‘삭제 가능합니다.’::TEXT;

END IF;

END;

$$ LANGUAGE plpgsql;

— 사용 예

SELECT * FROM can_delete_customer(101);

“`

관련 에러

  • 23503 foreign_key_violation: RESTRICT와 가장 유사한 에러로, NO ACTION 옵션에서 발생합니다. RESTRICT와의 차이는 NO ACTION은 트랜잭션 종료 시점에 검사되어 DEFERRABLE 설정이 가능하지만, RESTRICT는 즉시 검사된다는 점입니다.
  • 23000 integrity_constraint_violation: 모든 무결성 제약 위반의 상위 에러 클래스입니다.
  • 23502 not_null_violation: SET NULL 정책 사용 시 NOT NULL 컬럼에 NULL 설정을 시도할 때 발생할 수 있는 연관 에러입니다.
  • 23505 unique_violation: 외래 키 참조 대상 컬럼이 고유 제약을 위반할 때 함께 나타날 수 있습니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기