2026년 07월 05일 | DBMS Error 가이드
이 글에서 다루는 내용
42P19 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
42P19 invalid recursion 는?
PostgreSQL 에러 코드 42P19는 invalid recursion 에러로, CTE(Common Table Expression) 또는 재귀 쿼리를 작성할 때 잘못된 재귀 구조가 감지될 경우 발생합니다. 주로 WITH RECURSIVE 구문에서 재귀 참조가 허용되지 않는 위치에 사용되거나, 재귀 쿼리의 구조적 규칙을 위반했을 때 발생합니다. 이 에러는 쿼리 실행 전 파싱 및 계획 단계에서 탐지되므로, 문법적으로 올바르더라도 재귀 로직의 의미론적 구조가 PostgreSQL의 규칙을 따르지 않으면 반드시 발생합니다.
주요 발생 원인
1. 재귀 CTE에서 집계 함수 또는 GROUP BY 내 재귀 참조 사용
WITH RECURSIVE 구문에서 재귀 쿼리 부분(recursive term)에 GROUP BY, DISTINCT, HAVING, 집계 함수(COUNT, SUM 등), 또는 UNION 대신 UNION ALL 이외의 집합 연산자를 사용하면 에러가 발생합니다. PostgreSQL은 재귀 CTE에서 이러한 구조를 허용하지 않는데, 이는 재귀가 무한 루프에 빠지거나 의미론적으로 정의되지 않은 결과를 낳을 수 있기 때문입니다. 이 경우 쿼리 구조를 재귀 부분과 집계 부분으로 분리해야 합니다.
2. 재귀 참조가 서브쿼리 내부에 중첩된 경우
재귀 CTE의 재귀 항(recursive term)에서 재귀 이름(CTE 이름)을 서브쿼리 안에 중첩하여 참조하면 42P19 에러가 발생합니다. PostgreSQL의 재귀 CTE 구현 규칙상 재귀 참조는 반드시 최상위 FROM 절에서만 직접 참조되어야 하며, EXISTS, IN, 스칼라 서브쿼리 등 내부에 위치할 수 없습니다. 이는 재귀의 제어 흐름을 명확히 유지하고 무한 재귀를 방지하기 위한 엔진 레벨의 제약입니다.
3. 재귀 CTE에서 비재귀 항과 재귀 항의 구조적 오류
WITH RECURSIVE는 반드시 비재귀 항(base case)과 재귀 항(recursive case)이 UNION 또는 UNION ALL로 결합된 형태여야 합니다. 재귀 항만 단독으로 작성하거나, 두 항의 컬럼 수 및 타입이 일치하지 않거나, 재귀 참조가 UNION의 왼쪽에 위치하는 경우에도 이 에러가 유발될 수 있습니다. 재귀 CTE의 구조적 규칙을 정확히 이해하지 못한 채 작성된 쿼리에서 자주 발생하는 실수입니다.
해결 방법
원인 1 해결: 집계 함수를 재귀 외부로 분리
잘못된 예:
-- 에러 발생: 재귀 항에서 집계 함수 사용
WITH RECURSIVE dept_hierarchy AS (
SELECT department_id, manager_id, 1 AS depth
FROM departments
WHERE manager_id IS NULL
UNION ALL
SELECT d.department_id, d.manager_id, COUNT(*) AS depth -- 에러 발생
FROM departments d
JOIN dept_hierarchy dh ON d.manager_id = dh.department_id
GROUP BY d.department_id, d.manager_id
)
SELECT * FROM dept_hierarchy;
올바른 예:
-- 재귀와 집계를 분리하여 처리
WITH RECURSIVE dept_hierarchy AS (
-- 비재귀 항 (base case)
SELECT department_id, manager_id, 1 AS depth
FROM departments
WHERE manager_id IS NULL
UNION ALL
-- 재귀 항: 집계 없이 단순 조인만 사용
SELECT d.department_id, d.manager_id, dh.depth + 1
FROM departments d
JOIN dept_hierarchy dh ON d.manager_id = dh.department_id
)
-- 집계는 최종 SELECT에서 처리
SELECT manager_id, COUNT(*) AS subordinate_count, MAX(depth) AS max_depth
FROM dept_hierarchy
GROUP BY manager_id;
원인 2 해결: 재귀 참조를 서브쿼리 밖으로 이동
잘못된 예:
-- 에러 발생: 재귀 참조가 서브쿼리 내부에 중첩
WITH RECURSIVE employee_tree AS (
SELECT employee_id, manager_id, name
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.employee_id, e.manager_id, e.name
FROM employees e
WHERE e.manager_id IN (
SELECT employee_id FROM employee_tree -- 에러: 서브쿼리 내 재귀 참조
)
)
SELECT * FROM employee_tree;
올바른 예:
-- 재귀 참조를 직접 JOIN으로 변경
WITH RECURSIVE employee_tree AS (
SELECT employee_id, manager_id, name, 0 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 재귀 참조는 반드시 직접 JOIN으로만 사용
SELECT e.employee_id, e.manager_id, e.name, et.level + 1
FROM employees e
JOIN employee_tree et ON e.manager_id = et.employee_id
)
SELECT employee_id, name, level
FROM employee_tree
ORDER BY level, name;
원인 3 해결: 재귀 CTE 구조 올바르게 작성
잘못된 예:
-- 에러 발생: base case 없이 재귀 항만 존재하거나 구조 오류
WITH RECURSIVE category_path AS (
-- base case가 없거나, 재귀 참조가 UNION 왼쪽에 위치
SELECT c.category_id, c.name
FROM category_path cp -- 에러: base case 없음
JOIN categories c ON c.parent_id = cp.category_id
)
SELECT * FROM category_path;
올바른 예:
-- 올바른 재귀 CTE 구조
WITH RECURSIVE category_path AS (
-- 1단계: 반드시 base case(비재귀 항)가 먼저
SELECT category_id, name, parent_id,
name::TEXT AS full_path,
0 AS depth
FROM categories
WHERE parent_id IS NULL -- 루트 카테고리
UNION ALL
-- 2단계: 재귀 항은 UNION/UNION ALL 오른쪽에 위치
SELECT c.category_id, c.name, c.parent_id,
cp.full_path || ' > ' || c.name,
cp.depth + 1
FROM categories c
JOIN category_path cp ON c.parent_id = cp.category_id
)
SELECT category_id, full_path, depth
FROM category_path
ORDER BY full_path;
무한 재귀 방지를 위한 안전 장치 추가:
-- 실무에서는 반드시 깊이 제한을 추가하여 무한 루프 방지
WITH RECURSIVE safe_hierarchy AS (
SELECT node_id, parent_id, name, 0 AS depth,
ARRAY[node_id] AS path -- 순환 감지용 경로 배열
FROM nodes
WHERE parent_id IS NULL
UNION ALL
SELECT n.node_id, n.parent_id, n.name,
sh.depth + 1,
sh.path || n.node_id
FROM nodes n
JOIN safe_hierarchy sh ON n.parent_id = sh.node_id
WHERE sh.depth < 100 -- 깊이 제한
AND NOT n.node_id = ANY(sh.path) -- 순환 참조 방지
)
SELECT * FROM safe_hierarchy;
예방 방법
1. 재귀 CTE 작성 전 구조 설계 원칙 준수
WITH RECURSIVE 쿼리를 작성하기 전에 반드시 base case → recursive case → 종료 조건의 3단계 구조를 사전에 설계하는 습관을 들이세요. 특히 재귀 항에는 집계 함수, DISTINCT, GROUP BY, HAVING을 절대 사용하지 않으며, 재귀 CTE 이름은 항상 FROM 절에서 직접 조인 형태로만 참조해야 합니다. 팀 내 코드 리뷰 체크리스트에 이 항목을 추가하면 실수를 사전에 방지할 수 있습니다.
2. 재귀 깊이 제한 및 순환 감지 로직 항상 포함
실무 데이터베이스에서는 데이터 품질 문제로 인해 순환 참조(circular reference)가 발생할 수 있으므로, 모든 재귀 CTE에 depth < N 조건과 경로 배열(ARRAY 타입)을 이용한 순환 감지 로직을 반드시 포함하세요. statement_timeout 설정을 통해 장시간 실행되는 재귀 쿼리를 자동 차단하고, 개발 환경에서 SET max_recursive_iterations(PostgreSQL 버전에 따라 다름) 관련 파라미터를 테스트해 두는 것이 좋습니다. 이는 42P19 에러뿐만 아니라 실제 무한 루프로 인한 서비스 장애도 방지합니다.
관련 에러
42601(syntax_error):WITH RECURSIVE구문 자체의 문법 오류로 발생하며,42P19와 유사한 상황에서 함께 나타날 수 있습니다.42P20(windowing_error): 윈도우 함수를 잘못 사용했을 때 발생하는 에러로, CTE 내에서 윈도우 함수와 재귀를 혼용할 때 연관될 수 있습니다.54001(statement_too_complex): 재귀 깊이가 매우 깊어지거나 CTE 체인이 복잡해질 경우 발생할 수 있으며, 재귀 쿼리 최적화와 관련됩니다.XX000(internal_error): 극단적으로 잘못된 재귀 구조가 파서 레벨에서 처리되지 못할 경우 내부 오류로 번질 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.