2026년 09월 08일 | DBMS Error 가이드
이 글에서 다루는 내용
42P19 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
42P19 invalid recursion 는?
PostgreSQL 에러 코드 42P19 (invalid recursion)는 재귀 쿼리(CTE, Common Table Expression)나 뷰(View) 등을 정의할 때, 올바르지 않은 재귀 구조가 감지될 경우 발생하는 에러입니다. 주로 WITH RECURSIVE 구문에서 재귀 참조가 허용되지 않는 위치에 사용되거나, 재귀 CTE 내부에서 집계 함수, DISTINCT, GROUP BY, LIMIT 등의 제한된 구문과 함께 재귀 항이 잘못 구성될 때 트리거됩니다. 이 에러는 쿼리 파싱 및 분석 단계에서 발생하므로, 실제 데이터가 처리되기 전에 PostgreSQL이 구조적 문제를 미리 탐지하여 실행을 중단합니다.
주요 발생 원인
1. 재귀 CTE에서 재귀 항을 비재귀 항에서 참조하는 경우
WITH RECURSIVE 구문은 반드시 비재귀 항(base case)과 재귀 항(recursive case)을 UNION 또는 UNION ALL로 결합해야 합니다. 그런데 재귀 참조(자기 자신을 참조하는 부분)가 비재귀 항에 포함되거나, UNION의 잘못된 쪽에 위치하면 PostgreSQL은 이를 유효하지 않은 재귀 구조로 판단하여 42P19 에러를 발생시킵니다. 특히 처음 재귀 CTE를 작성하는 개발자들이 base case와 recursive case의 순서를 혼동하는 경우가 많습니다.
2. 재귀 CTE 내에서 집계 함수 또는 DISTINCT 사용
PostgreSQL의 재귀 CTE 명세에 따르면, 재귀 항(recursive term) 내에서는 집계 함수(COUNT, SUM, MAX 등), DISTINCT, GROUP BY, HAVING, LIMIT, OFFSET 등의 구문을 사용할 수 없습니다. 이러한 구문들이 재귀 항에 포함되면, PostgreSQL은 재귀의 종료 조건을 올바르게 평가할 수 없게 되므로, 쿼리 분석 단계에서 42P19 에러를 즉시 반환합니다. 이는 SQL 표준의 재귀 CTE 제약 조건을 엄격하게 따르는 PostgreSQL의 특성입니다.
3. 재귀 뷰(Recursive View) 또는 규칙(Rule)에서의 잘못된 자기 참조
뷰를 생성할 때 해당 뷰 자신을 직접 참조하거나, PostgreSQL 규칙(Rule) 시스템에서 자기 참조 루프가 형성될 때도 이 에러가 발생할 수 있습니다. CREATE RECURSIVE VIEW 구문을 사용하지 않고 일반 CREATE VIEW에서 자기 자신을 참조하려 하면, PostgreSQL은 이를 순환 의존성으로 판단합니다. 또한 트리거나 함수 내에서 재귀적 뷰 참조가 발생하는 경우에도 동일한 에러 코드가 반환될 수 있습니다.
해결 방법
원인 1 해결: 재귀 CTE 구조 올바르게 구성하기
잘못된 예시 (비재귀 항에 재귀 참조 포함):
-- 잘못된 쿼리: 재귀 참조가 잘못된 위치에 존재
WITH RECURSIVE emp_hierarchy AS (
-- 재귀 항이 먼저 오거나, UNION 구조가 잘못된 경우
SELECT e.id, e.name, e.manager_id
FROM employees e
JOIN emp_hierarchy eh ON e.id = eh.manager_id -- 비재귀 항에 재귀 참조 (오류!)
UNION ALL
SELECT e.id, e.name, e.manager_id
FROM employees e
WHERE e.manager_id IS NULL
)
SELECT * FROM emp_hierarchy;
올바른 예시:
-- 올바른 쿼리: 비재귀 항(base case)이 먼저, 재귀 항이 나중에 위치
WITH RECURSIVE emp_hierarchy AS (
-- 1단계: 비재귀 항 (루트 노드 선택)
SELECT e.id, e.name, e.manager_id, 1 AS depth
FROM employees e
WHERE e.manager_id IS NULL -- 최상위 관리자
UNION ALL
-- 2단계: 재귀 항 (자식 노드 탐색)
SELECT e.id, e.name, e.manager_id, eh.depth + 1
FROM employees e
JOIN emp_hierarchy eh ON e.manager_id = eh.id -- 재귀 참조는 반드시 재귀 항에!
)
SELECT id, name, depth
FROM emp_hierarchy
ORDER BY depth, name;
원인 2 해결: 재귀 항에서 집계 함수 제거 또는 외부로 분리
-- 잘못된 쿼리: 재귀 항 내에 GROUP BY 사용 (42P19 에러 발생)
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 AS level
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- 아래처럼 재귀 항 내에 집계를 쓰면 에러 발생!
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
GROUP BY c.id, c.name, c.parent_id, ct.level -- 오류: 재귀 항 내 GROUP BY 금지
)
SELECT * FROM category_tree;
-- 올바른 쿼리: 집계는 재귀 CTE 바깥으로 분리
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 AS level
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
-- 재귀 항 내에는 단순 조인과 컬럼 참조만 사용
)
-- 집계 함수는 최종 SELECT에서 사용
SELECT
level,
COUNT(*) AS node_count,
MAX(name) AS sample_name
FROM category_tree
GROUP BY level
ORDER BY level;
원인 3 해결: 재귀 뷰 올바르게 생성하기
-- 잘못된 방법: 일반 CREATE VIEW로 자기 참조 시도
-- CREATE VIEW org_chart AS
-- SELECT id, name, manager_id FROM employees
-- UNION ALL
-- SELECT e.id, e.name, e.manager_id
-- FROM employees e JOIN org_chart oc ON e.manager_id = oc.id;
-- → 42P19 에러 발생!
-- 올바른 방법 1: CREATE RECURSIVE VIEW 사용
CREATE RECURSIVE VIEW org_chart (id, name, manager_id, depth) AS (
-- 비재귀 항
SELECT id, name, manager_id, 0 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 재귀 항
SELECT e.id, e.name, e.manager_id, oc.depth + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.id
);
-- 올바른 방법 2: WITH RECURSIVE를 사용하는 일반 뷰로 감싸기
CREATE VIEW org_chart_v2 AS
WITH RECURSIVE hierarchy AS (
SELECT id, name, manager_id, 0 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, h.depth + 1
FROM employees e
JOIN hierarchy h ON e.manager_id = h.id
)
SELECT * FROM hierarchy;
-- 뷰 조회 테스트
SELECT * FROM org_chart ORDER BY depth, name;
무한 재귀 방지를 위한 깊이 제한 추가
-- 실무에서는 반드시 무한 루프 방지 조건 추가 권장
WITH RECURSIVE safe_traversal AS (
SELECT id, name, parent_id, 1 AS depth,
ARRAY[id] AS visited_path -- 방문 경로 추적
FROM nodes
WHERE parent_id IS NULL
UNION ALL
SELECT n.id, n.name, n.parent_id, st.depth + 1,
st.visited_path || n.id
FROM nodes n
JOIN safe_traversal st ON n.parent_id = st.id
WHERE st.depth < 100 -- 최대 깊이 제한
AND NOT (n.id = ANY(st.visited_path)) -- 순환 참조 방지
)
SELECT * FROM safe_traversal;
예방 방법
1. 재귀 CTE 작성 시 구조 체크리스트 준수
재귀 CTE를 작성할 때마다 아래 체크리스트를 반드시 확인하는 습관을 가지세요. 첫째, UNION ALL 이전의 비재귀 항(base case)에는 절대로 CTE 자기 자신을 참조하지 않는지 확인합니다. 둘째, 재귀 항(recursive term)에는 DISTINCT, GROUP BY, HAVING, LIMIT, OFFSET, 집계 함수를 사용하지 않도록 하고, 이러한 연산이 필요하다면 반드시 최종 SELECT문 또는 외부 CTE로 분리합니다. 셋째, 운영 환경 배포 전에 반드시 EXPLAIN 명령으로 쿼리 플랜을 검토하고, 데이터 규모가 큰 테이블에서는 깊이 제한(depth < N) 또는 방문 경로 배열(visited_path)을 활용하여 무한 재귀를 방지합니다.
-- 재귀 쿼리 안전성 검토용 템플릿
WITH RECURSIVE cte_name AS (
-- [체크 1] 비재귀 항: cte_name 참조 없어야 함
SELECT col1, col2, 1 AS depth
FROM source_table
WHERE <종료 조건>
UNION ALL -- UNION ALL 권장 (UNION은 DISTINCT 연산 포함으로 성능 저하)
-- [체크 2] 재귀 항: 집계/DISTINCT/LIMIT 없어야 함
SELECT t.col1, t.col2, c.depth + 1
FROM source_table t
JOIN cte_name c ON <조인 조건>
WHERE c.depth < 50 -- [체크 3] 반드시 종료 조건 포함
)
SELECT * FROM cte_name;
2. 코드 리뷰 및 테스트 환경에서 재귀 쿼리 전용 검증 절차 수립
재귀 쿼리는 일반 쿼리보다 오류 발생 가능성이 높고, 무한 루프로 인한 서버 자원 고갈 위험이 있으므로 별도의 검증 절차를 수립해야 합니다. 개발 환경에서는 statement_timeout을 짧게 설정하고, 소량의 샘플 데이터로 먼저 테스트하여 재귀 구조의 정확성을 검증합니다. 또한 팀 내 코드 리뷰 프로세스에서 재귀 CTE가 포함된 쿼리는 반드시 시니어 DBA의 검토를 거치도록 정책을 수립하면, 운영 환경에서의 42P19 에러 및 관련 성능 문제를 사전에 예방할 수 있습니다.
-- 개발/테스트 환경에서 재귀 쿼리 안전 실행 설정
SET statement_timeout = '5s'; -- 5초 초과 시 자동 중단
SET max_execution_time = 5000; -- 추가 안전장치
-- 소량 데이터로 먼저 검증
WITH RECURSIVE test_cte AS (
SELECT id, name, parent_id, 1 AS depth
FROM categories
WHERE parent_id IS NULL
LIMIT 10 -- 비재귀 항에서만 LIMIT 허용 (검증용)
UNION ALL
SELECT c.id, c.name, c.parent_id, tc.depth + 1
FROM categories c
JOIN test_cte tc ON c.parent_id = tc.id
WHERE tc.depth < 5 -- 테스트 시 낮은 깊이 제한
)
SELECT COUNT(*), MAX(depth) FROM test_cte;
관련 에러
- 42P20 (
windowing_error): 윈도우 함수(OVER절) 사용 시 잘못된 구문이나 컨텍스트에서 발생하는 에러로, 재귀 CTE 내에서 윈도우 함수를 잘못 사용할 때 42P19와 함께 발생할 수 있습니다. - 42601 (
syntax_error): 재귀 CTE 구문 자체가 잘못 작성된 경우(예:WITH RECURSIVE키워드 누락,UNION/UNION ALL누락) 발생하며, 42P19보다 먼저 감지됩니다. - 54001 (
statement_too_complex): 재귀 깊이가 매우 깊어지거나 재귀 CTE가 과도하게 중첩될 때 발생할 수 있으며, 무한 재귀가 방치될 경우 이 에러로 이어질 수 있습니다. - 57014 (
query_canceled):statement_timeout설정 없이 무한 재귀 쿼리가 실행될 경우, 수동으로 쿼리를 취소할 때 발생하는 에러입니다. 42P19로 컴파일 단계에서 잡히지 않은 논리적 무한 루프가 런타임에서 이 에러로 나타날 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.