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

21000
2026년 08월 07일 | DBMS Error 가이드

이 글에서 다루는 내용

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

21000 cardinality violation 는?

PostgreSQL 에러 코드 21000 cardinality_violation은 SQL 쿼리에서 단일 값이 기대되는 곳에 여러 개의 행(row)이 반환되거나, 반대로 정확히 하나의 값이 필요한 컨텍스트에서 값의 수가 맞지 않을 때 발생합니다. 가장 흔한 사례는 스칼라 서브쿼리(scalar subquery)가 두 개 이상의 행을 반환하거나, = 연산자를 통해 비교하는 서브쿼리에서 다중 행이 반환될 때입니다. 이 에러는 데이터 무결성과 직결되기 때문에 운영 환경에서 발생하면 즉각적인 대응이 필요하며, 쿼리 설계 단계에서 미리 방지하는 것이 중요합니다.


주요 발생 원인

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

스칼라 서브쿼리란 SELECT 절이나 WHERE 절에서 단일 값처럼 사용되는 서브쿼리를 의미합니다. 이 서브쿼리가 실제로 2개 이상의 행을 반환하면 PostgreSQL은 즉시 cardinality_violation 에러를 발생시킵니다. 이는 운영 초기에는 데이터가 적어 문제없이 동작하다가, 데이터가 쌓이면서 갑자기 에러가 발생하는 형태로 나타나는 경우가 많아 특히 위험합니다.

2. = 연산자와 서브쿼리의 잘못된 조합

WHERE column = (subquery) 형태에서 서브쿼리가 단일 행만 반환해야 하는데, 실제로 여러 행을 반환할 경우 에러가 발생합니다. 개발자들이 IN 대신 =를 사용하는 실수를 범하거나, 비즈니스 로직상 하나의 값만 반환될 것이라고 가정하지만 실제 데이터가 그렇지 않을 때 자주 발생합니다. 이 경우 쿼리의 논리적 오류이기도 하기 때문에, 단순 수정을 넘어 비즈니스 요구사항을 재검토해야 할 수도 있습니다.

3. PL/pgSQL 함수 내 SELECT INTO 구문에서 다중 행 반환

PL/pgSQL 함수나 프로시저 내에서 SELECT INTO 구문을 사용할 때, 해당 쿼리가 변수에 하나의 값만 담아야 하는데 여러 행이 반환되는 경우입니다. 특히 STRICT 옵션을 사용한 경우, 결과가 0건이거나 2건 이상일 때 모두 에러가 발생합니다. 함수 내부에서 발생하는 에러이므로 디버깅이 더 어려울 수 있어 충분한 예외 처리 코드를 작성해두는 것이 중요합니다.


해결 방법

원인 1 해결: 스칼라 서브쿼리에서 다중 행 반환 수정

문제가 되는 쿼리:

-- 에러 발생: orders 테이블에서 특정 고객의 주문이 여러 건일 경우 실패
SELECT 
    employee_id,
    employee_name,
    (SELECT order_amount FROM orders WHERE customer_id = 100) AS last_order
FROM employees;

해결 방법 1 – LIMIT 1 사용:

-- 가장 최근 주문 1건만 가져오기
SELECT 
    employee_id,
    employee_name,
    (SELECT order_amount 
     FROM orders 
     WHERE customer_id = 100 
     ORDER BY order_date DESC 
     LIMIT 1) AS last_order
FROM employees;

해결 방법 2 – 집계 함수 사용:

-- 합계나 평균 등 집계 함수로 단일 값 보장
SELECT 
    employee_id,
    employee_name,
    (SELECT SUM(order_amount) 
     FROM orders 
     WHERE customer_id = 100) AS total_order_amount
FROM employees;

원인 2 해결: = 연산자를 IN 또는 ANY로 교체

문제가 되는 쿼리:

-- 에러 발생: dept_id가 여러 개 반환될 경우 실패
SELECT employee_name
FROM employees
WHERE department_id = (
    SELECT dept_id 
    FROM departments 
    WHERE location = 'Seoul'
);

해결 방법 – IN 연산자 사용:

-- IN으로 변경하여 다중 행 처리 가능
SELECT employee_name
FROM employees
WHERE department_id IN (
    SELECT dept_id 
    FROM departments 
    WHERE location = 'Seoul'
);

해결 방법 – ANY 연산자 사용:

-- ANY 연산자로도 동일하게 처리 가능
SELECT employee_name
FROM employees
WHERE department_id = ANY (
    SELECT dept_id 
    FROM departments 
    WHERE location = 'Seoul'
);

또는 JOIN으로 리팩토링:

-- 성능과 가독성을 모두 고려한 JOIN 방식
SELECT e.employee_name
FROM employees e
INNER JOIN departments d ON e.department_id = d.dept_id
WHERE d.location = 'Seoul';

원인 3 해결: PL/pgSQL 함수 내 SELECT INTO 수정

문제가 되는 함수:

-- 에러 발생 가능한 함수
CREATE OR REPLACE FUNCTION get_employee_salary(p_dept_name TEXT)
RETURNS NUMERIC AS $$
DECLARE
    v_salary NUMERIC;
BEGIN
    -- STRICT 사용 시 다중 행 반환하면 에러 발생
    SELECT salary INTO STRICT v_salary
    FROM employees
    WHERE department_name = p_dept_name;
    
    RETURN v_salary;
END;
$$ LANGUAGE plpgsql;

해결 방법 1 – 예외 처리 추가:

CREATE OR REPLACE FUNCTION get_employee_salary(p_dept_name TEXT)
RETURNS NUMERIC AS $$
DECLARE
    v_salary NUMERIC;
BEGIN
    SELECT salary INTO STRICT v_salary
    FROM employees
    WHERE department_name = p_dept_name;
    
    RETURN v_salary;
    
EXCEPTION
    WHEN TOO_MANY_ROWS THEN
        RAISE EXCEPTION '부서 [%]에 여러 명의 직원이 존재합니다. 더 구체적인 조건을 사용하세요.', p_dept_name;
    WHEN NO_DATA_FOUND THEN
        RAISE EXCEPTION '부서 [%]에 해당하는 직원이 없습니다.', p_dept_name;
END;
$$ LANGUAGE plpgsql;

해결 방법 2 – 집계 함수로 단일 값 보장:

CREATE OR REPLACE FUNCTION get_avg_salary_by_dept(p_dept_name TEXT)
RETURNS NUMERIC AS $$
DECLARE
    v_avg_salary NUMERIC;
BEGIN
    -- AVG() 집계 함수로 항상 단일 값 반환 보장
    SELECT AVG(salary) INTO v_avg_salary
    FROM employees
    WHERE department_name = p_dept_name;
    
    -- 결과가 NULL인 경우(해당 부서 없음) 처리
    IF v_avg_salary IS NULL THEN
        RAISE WARNING '부서 [%]에 해당하는 데이터가 없습니다.', p_dept_name;
        RETURN 0;
    END IF;
    
    RETURN v_avg_salary;
END;
$$ LANGUAGE plpgsql;

해결 방법 3 – 커서를 활용하여 다중 행 처리:

CREATE OR REPLACE FUNCTION process_dept_salaries(p_dept_name TEXT)
RETURNS TABLE(emp_name TEXT, emp_salary NUMERIC) AS $$
BEGIN
    -- 다중 행을 반환해야 할 경우 RETURNS TABLE 사용
    RETURN QUERY
    SELECT employee_name::TEXT, salary
    FROM employees
    WHERE department_name = p_dept_name;
END;
$$ LANGUAGE plpgsql;

예방 방법

1. 스칼라 서브쿼리는 반드시 단일 행 보장 여부를 검증하는 코드 리뷰 프로세스 도입

스칼라 서브쿼리를 코드에 작성할 때는 해당 서브쿼리가 항상 단일 행만 반환할 수 있는지 논리적으로 검토해야 합니다. 특히 UNIQUE 또는 PRIMARY KEY 컬럼을 기준으로 조회하지 않는 경우, 미래 데이터 증가에 따라 다중 행이 반환될 가능성이 있습니다. 코드 리뷰 체크리스트에 “스칼라 서브쿼리 단일 행 반환 보장 여부”를 항목으로 추가하고, 가능하다면 LIMIT 1이나 집계 함수를 항상 명시적으로 사용하는 팀 컨벤션을 수립하세요.

-- 예방적 패턴: 스칼라 서브쿼리에 항상 집계 또는 LIMIT 적용
-- 나쁜 예 (언제든 에러 가능)
SELECT (SELECT price FROM products WHERE category = 'A') FROM orders;

-- 좋은 예 (항상 단일 값 보장)
SELECT (SELECT MAX(price) FROM products WHERE category = 'A') FROM orders;
SELECT (SELECT price FROM products WHERE category = 'A' ORDER BY created_at DESC LIMIT 1) FROM orders;

2. PL/pgSQL 함수 작성 시 STRICT 키워드와 예외 처리 패턴 표준화

팀 내에서 PL/pgSQL 함수를 작성할 때는 SELECT INTO 구문의 사용 패턴을 표준화하고, TOO_MANY_ROWSNO_DATA_FOUND 예외를 항상 처리하는 템플릿을 공유하세요. STRICT 키워드는 데이터 정확성을 강제하는 좋은 도구이지만, 반드시 예외 처리 블록과 함께 사용해야 안전합니다. 아래와 같은 표준 템플릿을 팀 위키에 등록하여 모든 개발자가 동일한 패턴으로 함수를 작성하도록 유도하세요.

-- 팀 표준 PL/pgSQL 함수 템플릿
CREATE OR REPLACE FUNCTION standard_fetch_function(p_id INT)
RETURNS some_type AS $$
DECLARE
    v_result some_type;
BEGIN
    SELECT col INTO STRICT v_result
    FROM some_table
    WHERE id = p_id;

    RETURN v_result;

EXCEPTION
    WHEN TOO_MANY_ROWS THEN
        RAISE EXCEPTION '[SQLSTATE 21000] ID %에 대해 여러 행이 반환되었습니다.', p_id
            USING HINT = '조건을 더 구체화하거나 UNIQUE 제약 조건을 확인하세요.';
    WHEN NO_DATA_FOUND THEN
        RAISE EXCEPTION '[SQLSTATE 02000] ID %에 해당하는 데이터가 없습니다.', p_id;
END;
$$ LANGUAGE plpgsql;

관련 에러

  • 02000 (no_data_found): SELECT INTO STRICT 구문에서 결과 행이 0개일 때 발생합니다. 21000과 쌍으로 처리해야 하는 에러입니다.
  • 21000의 하위 에러 21P01 (too_many_rows): PL/pgSQL에서 STRICT 옵션 사용 시 두 개 이상의 행이 반환될 때 발생하는 구체적인 에러 코드입니다.
  • 42804 (datatype_mismatch): 서브쿼리의 반환 타입이 기대 타입과 다를 때 발생하며, 스칼라 서브쿼리 수정 과정에서 함께 마주칠 수 있습니다.
  • 2202E (array_subscript_error): 배열 서브스크립트 관련 cardinality 문제로 발생할 수 있으며, 배열을 다루는 쿼리에서 간접적으로 연관됩니다.

DBMS 에러 코드 시리즈

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

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

댓글 남기기