2026년 06월 03일 | DBMS Error 가이드
이 글에서 다루는 내용
21000 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
21000 cardinality violation 는?
PostgreSQL 에러 코드 21000 (cardinality_violation)은 쿼리 또는 연산에서 기대하는 행(row)의 수와 실제 반환되는 행의 수가 일치하지 않을 때 발생하는 에러입니다. 가장 흔한 사례는 스칼라 서브쿼리(scalar subquery)가 단 하나의 값을 반환해야 하는데, 실제로는 여러 행을 반환하거나, 특정 연산자가 정확히 한 개의 행을 요구하는 상황에서 여러 행이 조회될 때 나타납니다. 이 에러는 애플리케이션 레벨에서도 자주 발생하며, 데이터 모델 설계나 쿼리 로직의 결함을 나타내는 중요한 신호이기도 합니다.
주요 발생 원인
1. 스칼라 서브쿼리(Scalar Subquery)가 여러 행을 반환하는 경우
가장 빈번하게 발생하는 원인입니다. SELECT 절이나 WHERE 절에서 괄호로 감싼 서브쿼리는 PostgreSQL이 반드시 0개 또는 1개의 행만 반환한다고 기대합니다. 그런데 실제 데이터에는 조건에 맞는 행이 여러 개 존재할 경우, 즉시 이 에러가 발생합니다. 예를 들어 특정 고객의 주문 금액을 서브쿼리로 조회할 때, 해당 고객의 주문이 여러 건이라면 문제가 발생합니다.
2. = 연산자와 함께 서브쿼리를 사용하는 경우
WHERE column = (서브쿼리) 형태로 쿼리를 작성할 때, 서브쿼리가 여러 행을 반환하면 cardinality violation이 발생합니다. 단일 값과의 비교를 위해 = 연산자를 사용했지만, 서브쿼리의 결과가 단일 행을 보장하지 않는 경우입니다. 이 상황은 IN 또는 ANY 연산자로 대체하여 해결할 수 있습니다.
3. PL/pgSQL의 SELECT INTO 구문에서 여러 행이 반환되는 경우
PL/pgSQL 함수나 프로시저 내부에서 SELECT INTO 구문을 사용해 변수에 값을 할당할 때, 쿼리 결과가 두 행 이상이면 에러가 발생합니다. 이는 런타임 시점에 데이터 증가로 인해 발생하는 경우가 많아, 처음 개발 당시에는 정상 동작하다가 운영 환경에서 갑자기 에러가 나타나는 패턴을 보입니다. STRICT 옵션 사용 여부에 따라 동작이 달라지므로 정확한 이해가 필요합니다.
해결 방법
원인 1 해결: 스칼라 서브쿼리에 집계 함수 또는 LIMIT 적용
서브쿼리가 여러 행을 반환하는 경우, MAX(), MIN(), SUM() 등 집계 함수를 사용하거나 LIMIT 1을 추가하여 단일 값이 반환되도록 수정합니다.
-- 문제가 되는 쿼리 (여러 주문이 있는 고객일 경우 에러 발생)
SELECT
customer_id,
customer_name,
(SELECT order_amount FROM orders WHERE customer_id = c.customer_id) AS last_order
FROM customers c;
-- 해결 방법 1: 집계 함수 사용
SELECT
customer_id,
customer_name,
(SELECT MAX(order_amount) FROM orders WHERE customer_id = c.customer_id) AS max_order
FROM customers c;
-- 해결 방법 2: LIMIT 1 + ORDER BY 사용 (최신 주문 1건만)
SELECT
customer_id,
customer_name,
(SELECT order_amount
FROM orders
WHERE customer_id = c.customer_id
ORDER BY created_at DESC
LIMIT 1) AS last_order
FROM customers c;
원인 2 해결: = 대신 IN 또는 ANY 연산자 사용
서브쿼리 결과가 다중 행일 가능성이 있다면 = 대신 IN 또는 = ANY()를 사용합니다.
-- 문제가 되는 쿼리
SELECT *
FROM products
WHERE category_id = (
SELECT category_id FROM categories WHERE status = 'active'
);
-- 해결 방법 1: IN 연산자 사용
SELECT *
FROM products
WHERE category_id IN (
SELECT category_id FROM categories WHERE status = 'active'
);
-- 해결 방법 2: = ANY 연산자 사용
SELECT *
FROM products
WHERE category_id = ANY(
SELECT category_id FROM categories WHERE status = 'active'
);
-- 해결 방법 3: EXISTS 활용 (성능상 유리한 경우 많음)
SELECT p.*
FROM products p
WHERE EXISTS (
SELECT 1 FROM categories c
WHERE c.category_id = p.category_id
AND c.status = 'active'
);
원인 3 해결: PL/pgSQL SELECT INTO 수정
PL/pgSQL 함수에서 SELECT INTO를 안전하게 처리하려면 LIMIT 1을 명시하거나 예외 처리를 추가합니다.
-- 문제가 되는 PL/pgSQL 함수
CREATE OR REPLACE FUNCTION get_customer_email(p_name TEXT)
RETURNS TEXT AS $$
DECLARE
v_email TEXT;
BEGIN
-- 동명이인이 있을 경우 에러 발생!
SELECT email INTO STRICT v_email
FROM customers
WHERE customer_name = p_name;
RETURN v_email;
END;
$$ LANGUAGE plpgsql;
-- 해결 방법 1: STRICT 제거 후 LIMIT 1 + ORDER BY 적용
CREATE OR REPLACE FUNCTION get_customer_email(p_name TEXT)
RETURNS TEXT AS $$
DECLARE
v_email TEXT;
BEGIN
SELECT email INTO v_email
FROM customers
WHERE customer_name = p_name
ORDER BY created_at DESC
LIMIT 1;
RETURN v_email;
END;
$$ LANGUAGE plpgsql;
-- 해결 방법 2: 예외 처리로 graceful degradation 구현
CREATE OR REPLACE FUNCTION get_customer_email_safe(p_name TEXT)
RETURNS TEXT AS $$
DECLARE
v_email TEXT;
BEGIN
SELECT email INTO STRICT v_email
FROM customers
WHERE customer_name = p_name;
RETURN v_email;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
RAISE WARNING '고객명 [%]에 해당하는 레코드가 여러 개 존재합니다.', p_name;
RETURN NULL;
WHEN NO_DATA_FOUND THEN
RAISE WARNING '고객명 [%]에 해당하는 레코드가 없습니다.', p_name;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
예방 방법
1. 스칼라 서브쿼리 사용 시 UNIQUE 제약 조건 또는 집계 함수 강제화
서브쿼리를 작성할 때, 반환 컬럼이 UNIQUE 또는 PRIMARY KEY 컬럼임을 보장하거나, 반드시 집계 함수(MAX, MIN, COUNT 등)를 함께 사용하는 코딩 규칙을 팀 내에서 수립합니다. 코드 리뷰 시 스칼라 서브쿼리에 GROUP BY 없이 집계 함수도 없는 경우를 체크리스트 항목으로 추가하면 사전에 많은 문제를 방지할 수 있습니다.
-- 안전한 패턴: 반환값이 항상 단일 행임을 보장
SELECT
c.customer_id,
c.customer_name,
-- PK 기반 조회는 안전
(SELECT email FROM customer_contacts WHERE contact_id = c.primary_contact_id) AS email,
-- 집계 함수 사용으로 안전
(SELECT COUNT(*) FROM orders WHERE customer_id = c.customer_id) AS order_count
FROM customers c;
2. PL/pgSQL 함수 작성 시 TOO_MANY_ROWS 예외 처리 표준화
프로시저 및 함수 개발 표준에 TOO_MANY_ROWS 예외 핸들러를 필수 포함 항목으로 지정합니다. 특히 외부 입력값(사용자 검색어 등)을 기반으로 데이터를 조회하는 함수에서는 데이터 증가에 따라 언제든지 이 에러가 발생할 수 있으므로, 방어적으로 작성하는 습관이 중요합니다.
관련 에러
- P0002
no_data_found:SELECT INTO STRICT구문에서 반환 행이 0개일 때 발생하며,TOO_MANY_ROWS(21000)와 쌍을 이루는 에러입니다. - P0003
too_many_rows: PL/pgSQL 내부에서STRICT옵션 사용 시 여러 행이 반환될 때 발생하는 에러로, 21000과 밀접하게 관련됩니다. - 42804
datatype_mismatch: 서브쿼리의 반환 타입이 기대하는 타입과 다를 때 발생하며, 서브쿼리 수정 과정에서 함께 마주칠 수 있는 에러입니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.