PostgreSQL Error 42P19: invalid recursion — Causes, Fixes & Prevention
What Is Error 42P19?
PostgreSQL error code 42P19 (invalid_recursion) is raised when the database engine detects a structurally invalid recursive query during the parse and analysis phase. It most commonly occurs with WITH RECURSIVE CTEs when the recursive reference appears in a disallowed position, or when forbidden clauses like GROUP BY, DISTINCT, or aggregate functions are used inside the recursive term. Because this error is caught before execution begins, no data is ever processed — it's purely a query structure problem.
Top 3 Causes
1. Recursive Reference Placed in the Non-Recursive (Base) Term
The WITH RECURSIVE syntax requires a strict structure: a non-recursive base term, then UNION ALL, then a recursive term. Placing the self-reference in the wrong half immediately triggers 42P19.
-- WRONG: recursive reference in the base (non-recursive) term
WITH RECURSIVE emp_tree AS (
SELECT e.id, e.name, e.manager_id
FROM employees e
JOIN emp_tree et ON e.id = et.manager_id -- ERROR: self-ref in base term!
UNION ALL
SELECT e.id, e.name, e.manager_id
FROM employees e
WHERE e.manager_id IS NULL
)
SELECT * FROM emp_tree;
-- CORRECT: base term first, recursive term second
WITH RECURSIVE emp_tree AS (
-- Base case: root nodes
SELECT id, name, manager_id, 1 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- Recursive term: expand children
SELECT e.id, e.name, e.manager_id, et.depth + 1
FROM employees e
JOIN emp_tree et ON e.manager_id = et.id -- self-ref only here
)
SELECT * FROM emp_tree ORDER BY depth;
2. Aggregate Functions, DISTINCT, or GROUP BY Inside the Recursive Term
PostgreSQL strictly forbids COUNT, SUM, DISTINCT, GROUP BY, HAVING, LIMIT, and OFFSET inside the recursive term of a CTE. These constructs prevent the engine from correctly evaluating termination conditions.
-- WRONG: GROUP BY inside the recursive term
WITH RECURSIVE category_path 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, cp.level + 1
FROM categories c
JOIN category_path cp ON c.parent_id = cp.id
GROUP BY c.id, c.name, c.parent_id, cp.level -- ERROR: forbidden here!
)
SELECT * FROM category_path;
-- CORRECT: move aggregation outside the CTE
WITH RECURSIVE category_path 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, cp.level + 1
FROM categories c
JOIN category_path cp ON c.parent_id = cp.id
)
-- Aggregate in the outer query only
SELECT level, COUNT(*) AS total_nodes
FROM category_path
GROUP BY level
ORDER BY level;
3. Improper Self-Reference in Recursive Views
Creating a regular view that references itself without using CREATE RECURSIVE VIEW — or creating circular view dependencies — also raises 42P19.
-- WRONG: standard CREATE VIEW with self-reference
-- 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;
-- → ERROR 42P19!
-- CORRECT: use 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
);
SELECT * FROM org_chart ORDER BY depth, name;
Quick Fix Checklist
When you hit 42P19, run through this checklist:
-- Safe recursive CTE template
WITH RECURSIVE cte AS (
-- [1] Base term: NO self-reference allowed here
SELECT col1, col2, 1 AS depth
FROM my_table
WHERE <termination_condition>
UNION ALL -- prefer UNION ALL over UNION for performance
-- [2] Recursive term: NO aggregates, DISTINCT, GROUP BY, LIMIT
SELECT t.col1, t.col2, c.depth + 1
FROM my_table t
JOIN cte c ON <join_condition>
WHERE c.depth < 100 -- [3] Always include a depth guard!
)
SELECT * FROM cte;
Prevention Tips
1. Always add a depth guard and cycle detection. Even if 42P19 is resolved, a logically infinite loop will exhaust server resources at runtime. Use a depth counter and optionally track visited IDs.
WITH RECURSIVE safe_cte AS (
SELECT id, parent_id, 1 AS depth, ARRAY[id] AS path
FROM nodes WHERE parent_id IS NULL
UNION ALL
SELECT n.id, n.parent_id, s.depth + 1, s.path || n.id
FROM nodes n
JOIN safe_cte s ON n.parent_id = s.id
WHERE s.depth < 50 -- depth limit
AND NOT (n.id = ANY(s.path)) -- cycle guard
)
SELECT * FROM safe_cte;
2. Set statement_timeout in development. Since 42P19 is caught at parse time, logical infinite loops that slip through will run forever. Protect your server:
-- In development / testing sessions
SET statement_timeout = '10s';
-- Verify recursive query structure with EXPLAIN before running on production
EXPLAIN WITH RECURSIVE my_cte AS ( ... ) SELECT * FROM my_cte;
Related Errors
| Code | Name | Notes |
|---|---|---|
42P20 |
windowing_error |
Invalid window function usage, can co-occur with recursive CTEs |
42601 |
syntax_error |
Missing WITH RECURSIVE keyword or UNION/UNION ALL
|
57014 |
query_canceled |
Runtime cancel when a logical infinite loop isn't caught at parse time |
54001 |
statement_too_complex |
Triggered when recursion depth or nesting becomes excessive |
📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.
Top comments (0)