SQL Server · lesson 4 of 7
Recursive CTEs and the Default MAXRECURSION 100 Limit
A recursive CTE generating a date series past 100 rows fails with Msg 530 by default -- SQL Server caps recursion depth at 100 unless OPTION (MAXRECURSION n) says otherwise.
By SkillByExample · Published
A recursive CTE has a built-in safety limit most non-recursive queries never bump into — until one generates more than 100 rows recursively.
A non-recursive CTE: naming a step
WITH author_revenue AS (
SELECT a.name AS author_name, SUM(b.price * oi.quantity) AS revenue
FROM authors a
JOIN books b ON b.author_id = a.author_id
JOIN order_items oi ON oi.book_id = b.book_id
GROUP BY a.name
)
SELECT TOP 5 author_name, revenue
FROM author_revenue
ORDER BY revenue DESC;
author_name revenue
-------------- ---------
Author 9 22453.59
Author 49 22443.59
Author 19 22403.60
Author 39 22353.61
Author 29 22323.61
(5 rows affected)
author_revenue names the join-and-aggregate step so the outer SELECT reads cleanly — no
different from wrapping the same logic in a subquery, just more readable once a query has
several steps chained together.
The bug: a recursive CTE past the default recursion limit
WITH date_series AS (
SELECT CAST('2024-01-01' AS DATE) AS d
UNION ALL
SELECT DATEADD(DAY, 1, d) FROM date_series WHERE d < '2026-09-13'
)
SELECT COUNT(*) AS total_days FROM date_series;
Msg 530, Level 16, State 1, Server ..., Line 1
The statement terminated. The maximum recursion 100 has been exhausted before statement
completion.
The anchor (CAST('2024-01-01' AS DATE)) and the recursive member (DATEADD(DAY, 1, d) ...)
are both correct — the termination condition (d < '2026-09-13') does eventually become
false. The query simply needs 987 recursive steps to get there, and SQL Server’s default cap
is 100, specifically to catch runaway recursion before it happens. This isn’t runaway; it’s
just a real recursion depth past the conservative default.
The fix: OPTION (MAXRECURSION n)
WITH date_series AS (
SELECT CAST('2024-01-01' AS DATE) AS d
UNION ALL
SELECT DATEADD(DAY, 1, d) FROM date_series WHERE d < '2026-09-13'
)
SELECT COUNT(*) AS total_days FROM date_series
OPTION (MAXRECURSION 1100);
total_days
-----------
987
(1 rows affected)
OPTION (MAXRECURSION 1100) raises the cap enough to cover the real 987 steps needed, while
still keeping a ceiling — a genuinely broken recursive CTE (a termination condition that never
triggers) would still fail cleanly at 1100 instead of spinning forever. MAXRECURSION 0
removes the cap entirely; prefer a specific number when the real depth is knowable.
Practice
1. Use MAXRECURSION 0 instead of a specific number, and confirm it also works.
WITH date_series AS (
SELECT CAST('2024-01-01' AS DATE) AS d
UNION ALL
SELECT DATEADD(DAY, 1, d) FROM date_series WHERE d < '2026-09-13'
)
SELECT COUNT(*) AS total_days FROM date_series
OPTION (MAXRECURSION 0);
total_days
-----------
987
(1 rows affected)
Same result, no cap at all this time — works here, but offers no protection if the termination condition were ever wrong.
2. Chain two CTEs together — one referencing the other in the same WITH clause.
WITH book_totals AS (
SELECT book_id, SUM(quantity) AS total_qty
FROM order_items
GROUP BY book_id
),
top_books AS (
SELECT TOP 3 book_id, total_qty FROM book_totals ORDER BY total_qty DESC
)
SELECT b.title, t.total_qty
FROM top_books t JOIN books b ON b.book_id = t.book_id
ORDER BY t.total_qty DESC;
title total_qty
----------------- -----------
Book Title 687 9
Book Title 1420 9
Book Title 1443 9
(3 rows affected)
top_books references book_totals directly — a comma separates multiple CTE definitions in
one WITH clause, and later ones can build on earlier ones without repeating their logic.
3. Find the exact boundary: does a 100-step recursion succeed under the default limit?
WITH date_series AS (
SELECT CAST('2024-01-01' AS DATE) AS d
UNION ALL
SELECT DATEADD(DAY, 1, d) FROM date_series WHERE d < '2024-04-10'
)
SELECT COUNT(*) AS total_days FROM date_series;
total_days
-----------
101
(1 rows affected)
Succeeds with no OPTION needed — 100 days from 2024-01-01 to 2024-04-09 plus the anchor
row is 101 total rows from exactly 100 recursive invocations, right at the default limit
without exceeding it.
Next: transactions and isolation levels — a real dirty read, reproduced.