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

21000
2026년 10월 11일 | DBMS Error 가이드

이 글에서 다루는 내용

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

21000 cardinality violation 는?

PostgreSQL 에러 코드 21000 (cardinality violation)은 단일 값을 반환해야 하는 문맥에서 서브쿼리나 함수가 둘 이상의 행(row)을 반환할 때 발생합니다. 예를 들어 = 연산자와 함께 사용된 스칼라 서브쿼리가 여러 행을 반환하거나, PL/pgSQL 함수 내부에서 SELECT INTO 구문이 복수의 결과를 받으려 할 때 이 에러가 트리거됩니다. 데이터 모델의 기대치(단일 값)와 실제 쿼리 결과(복수 값) 사이의 불일치로 인해 트랜잭션이 즉시 중단되며, 이는 운영 환경에서 서비스 장애로 이어질 수 있습니다.


주요 발생 원인

1. 스칼라 서브쿼리에서 다중 행 반환

가장 빈번하게 발생하는 원인입니다. WHERE 절이나 SELECT 컬럼 자리에 서브쿼리를 배치할 때, 해당 서브쿼리가 단 하나의 행만 반환한다고 가정하지만 실제로는 여러 행이 반환되는 경우입니다. 특히 외래 키 관계가 1:N인 테이블에서 무심코 서브쿼리를 작성할 때 자주 발생하며, 테스트 환경에서는 데이터가 적어 통과되다가 운영 환경에서 데이터가 증가한 후에야 에러가 발생하는 케이스가 많습니다.

2. PL/pgSQL 함수 내 SELECT INTO 다중 결과

PL/pgSQL 코드 블록 안에서 SELECT INTO 구문을 사용할 때, 조건절이 불충분하여 복수의 행이 변수에 담기려 하면 이 에러가 발생합니다. 함수 로직 작성 시 “이 쿼리는 항상 1건만 나온다”고 개발자가 암묵적으로 가정하는 경우가 많은데, 데이터가 변화함에 따라 이 가정이 깨지게 됩니다. 운영 중인 프로시저나 함수에서 갑자기 에러가 발생하는 경우 이 원인을 먼저 의심해야 합니다.

3. = 연산자와 함께 사용된 다중 행 반환 서브쿼리

UPDATE, DELETE, SELECT 문에서 WHERE column = (서브쿼리) 형태로 작성할 때, 서브쿼리가 여러 값을 반환하면 발생합니다. 이 경우 PostgreSQL은 단일 값을 기대하는 = 연산자에 다중 행이 제공되었다고 판단하고 즉시 에러를 발생시킵니다. IN이나 ANY를 사용해야 할 자리에 =를 사용하는 실수가 전형적인 사례입니다.


해결 방법

원인 1 해결: 스칼라 서브쿼리 수정

잘못된 예제 (에러 발생):

-- orders 테이블에서 customer_id가 여러 개일 경우 에러 발생
SELECT *
FROM customers
WHERE customer_id = (
    SELECT customer_id
    FROM orders
    WHERE product_id = 101
);

올바른 해결책 1 – IN 사용:

SELECT *
FROM customers
WHERE customer_id IN (
    SELECT customer_id
    FROM orders
    WHERE product_id = 101
);

올바른 해결책 2 – ANY 사용:

SELECT *
FROM customers
WHERE customer_id = ANY (
    SELECT customer_id
    FROM orders
    WHERE product_id = 101
);

올바른 해결책 3 – EXISTS 사용 (성능 최적화):

SELECT c.*
FROM customers c
WHERE EXISTS (
    SELECT 1
    FROM orders o
    WHERE o.customer_id = c.customer_id
      AND o.product_id = 101
);

원인 2 해결: PL/pgSQL SELECT INTO 수정

잘못된 예제 (에러 발생):

CREATE OR REPLACE FUNCTION get_customer_email(p_product_id INT)
RETURNS TEXT AS $$
DECLARE
    v_email TEXT;
BEGIN
    -- 이 서브쿼리가 여러 행을 반환하면 에러 발생
    SELECT email
    INTO v_email
    FROM customers
    WHERE customer_id = (
        SELECT customer_id FROM orders WHERE product_id = p_product_id
    );

    RETURN v_email;
END;
$$ LANGUAGE plpgsql;

올바른 해결책 – LIMIT 1 또는 집계함수 사용:

CREATE OR REPLACE FUNCTION get_customer_email(p_product_id INT)
RETURNS TEXT AS $$
DECLARE
    v_email TEXT;
BEGIN
    -- 방법 1: LIMIT 1 으로 단일 행 보장
    SELECT email
    INTO v_email
    FROM customers
    WHERE customer_id IN (
        SELECT customer_id FROM orders WHERE product_id = p_product_id
    )
    ORDER BY customer_id
    LIMIT 1;

    RETURN v_email;
END;
$$ LANGUAGE plpgsql;

STRICT 키워드를 사용한 명시적 에러 제어:

CREATE OR REPLACE FUNCTION get_single_customer_email(p_customer_id INT)
RETURNS TEXT AS $$
DECLARE
    v_email TEXT;
BEGIN
    -- STRICT: 정확히 1건이 아니면 에러를 명확하게 발생
    SELECT email
    INTO STRICT v_email
    FROM customers
    WHERE customer_id = p_customer_id;

    RETURN v_email;
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE EXCEPTION '고객 정보를 찾을 수 없습니다: %', p_customer_id;
    WHEN TOO_MANY_ROWS THEN
        RAISE EXCEPTION '중복된 고객 데이터가 존재합니다: %', p_customer_id;
END;
$$ LANGUAGE plpgsql;

원인 3 해결: = 연산자를 적절한 연산자로 교체

잘못된 예제 (에러 발생):

-- department_id가 여러 개 반환될 경우 에러
UPDATE employees
SET salary = salary * 1.1
WHERE department_id = (
    SELECT department_id
    FROM departments
    WHERE location = 'Seoul'
);

올바른 해결책:

-- 방법 1: IN 사용
UPDATE employees
SET salary = salary * 1.1
WHERE department_id IN (
    SELECT department_id
    FROM departments
    WHERE location = 'Seoul'
);

-- 방법 2: JOIN을 활용한 UPDATE (더 효율적)
UPDATE employees e
SET salary = e.salary * 1.1
FROM departments d
WHERE e.department_id = d.department_id
  AND d.location = 'Seoul';

예방 방법

1. 코드 리뷰 시 스칼라 서브쿼리 패턴 점검 및 STRICT 활용

서브쿼리가 = 연산자와 함께 사용되는 모든 코드를 리뷰할 때, 해당 서브쿼리가 항상 0건 또는 1건만 반환한다는 것을 데이터 제약조건(UNIQUE, PRIMARY KEY)으로 보장할 수 있는지 확인해야 합니다. PL/pgSQL 함수에서는 SELECT INTO STRICT를 기본 패턴으로 사용하여 다중 행 반환 시 명시적으로 TOO_MANY_ROWS 예외를 처리하도록 코딩 표준을 수립하면 잠재적 버그를 조기에 발견할 수 있습니다.

-- 코드 리뷰 시 확인: 서브쿼리 결과가 UNIQUE 보장되는지 검증
SELECT COUNT(*), customer_id
FROM orders
WHERE product_id = 101
GROUP BY customer_id
HAVING COUNT(*) > 1;
-- 결과가 없어야 = 연산자와 함께 안전하게 사용 가능

2. 개발/테스트 환경에서 운영 수준의 데이터 볼륨 유지

cardinality violation 에러의 가장 큰 함정은 적은 테스트 데이터 환경에서는 에러가 발생하지 않다가 운영 데이터가 쌓인 후 뒤늦게 발생한다는 점입니다. 이를 예방하기 위해 개발 및 스테이징 환경에서 운영 DB의 익명화된 데이터 스냅샷을 주기적으로 동기화하거나, 최소한 데이터 생성 스크립트를 통해 다양한 카디널리티 케이스를 시뮬레이션하는 테스트를 CI/CD 파이프라인에 포함해야 합니다.

-- 테스트 데이터 다양성 검증 쿼리 예시
-- 1:N 관계의 N 쪽 최대 카운트를 확인
SELECT
    product_id,
    COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
GROUP BY product_id
ORDER BY unique_customers DESC
LIMIT 10;

관련 에러

  • 20000 (case_not_found): PL/pgSQL의 CASE 문에서 일치하는 조건이 없을 때 발생하며, 데이터 기대치 불일치라는 점에서 21000과 맥락이 비슷합니다.
  • P0002 (no_data_found): SELECT INTO STRICT 사용 시 결과가 0건일 때 발생하는 에러로, 21000과 쌍을 이루는 에러입니다. 21000이 “너무 많은 행”, P0002는 “행이 없음”을 의미합니다.
  • P0003 (too_many_rows): 21000과 동일한 상황에서 PL/pgSQL 내부의 SELECT INTO STRICT 컨텍스트에서 발생하는 에러 코드입니다. 두 에러는 발생 컨텍스트(SQL 레벨 vs PL/pgSQL 레벨)에 따라 구분됩니다.
  • 42803 (grouping_error): 집계 함수와 GROUP BY 불일치 시 발생하며, 서브쿼리 작성 실수와 관련된 에러 그룹에 속합니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기