Docs /Querying /Recursive CTEs
Querying

Recursive CTEs in BigQuery

WITH RECURSIVE in BigQuery needs UNION ALL, one self-reference, the same column types in both terms, and stops with an error after 500 iterations. Cycles in graph data must be cut by hand.

A recursive CTE is a WITH RECURSIVE query that refers to itself. It has a base term, which runs once, and a recursive term, which runs again on the rows the previous round produced, until a round produces no rows. In BigQuery the two terms are joined with UNION ALL (nothing else is allowed), there can be exactly one reference to the CTE in the recursive term, and the query fails after 500 iterations. Use it for hierarchies and graphs of unknown depth. For plain number or date series, GENERATE_ARRAY is simpler.

The shape

count from 1 to 5
123456
WITH RECURSIVE counter AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM counter WHERE n < 5
)
SELECT n FROM counter ORDER BY n
Result in BigQuery
n
11
22
33
44
55
5 rows

The base term SELECT 1 AS n produces the first row. The recursive term reads the rows from the previous round (FROM counter), adds 1, and stops when WHERE n < 5 filters everything out. The CTE’s result is the union of all rounds. Without an ORDER BY the order of rows is not defined, so sort if it matters.

Walking a hierarchy

The usual use is a table of parent and child ids. The base term picks the root, and each round joins the previous round’s rows to their children:

org chart with depth and path
1234567891011121314151617181920
WITH RECURSIVE
  employees AS (
    SELECT 1 AS id, CAST(NULL AS INT64) AS manager_id, 'Ada' AS name UNION ALL
    SELECT 2, 1, 'Ben' UNION ALL
    SELECT 3, 1, 'Cy' UNION ALL
    SELECT 4, 2, 'Di' UNION ALL
    SELECT 5, 4, 'Ed'
  ),
  chain AS (
    SELECT id, name, 0 AS depth, name AS path
    FROM employees
    WHERE manager_id IS NULL
    UNION ALL
    SELECT e.id, e.name, c.depth + 1, CONCAT(c.path, ' > ', e.name)
    FROM chain AS c
    JOIN employees AS e ON e.manager_id = c.id
  )
SELECT id, name, depth, path
FROM chain
ORDER BY id
Result in BigQuery
idnamedepthpath
11Ada0Ada
22Ben1Ada > Ben
33Cy1Ada > Cy
44Di2Ada > Ben > Di
55Ed3Ada > Ben > Di > Ed
5 rows

Each round adds one level of the tree, so the number of rounds is the depth of the deepest branch. A hierarchy deeper than 500 levels can’t be walked this way.

The limit counts the base term as the first iteration. A counter with WHERE n < 500 runs and returns 500 rows. With WHERE n < 501 it needs 501 iterations and fails with “A recursive CTE has reached the maximum number of iterations: 500.” (we ran both). These two runs aren’t examples on the page because the failing one takes over two minutes.

Graphs: paths multiply

In a graph where a node can be reached by several routes, UNION ALL keeps one row per route, not one row per node. In this graph, node 4 can be reached from 2 and from 3:

nodes 4 and 5 are reached twice
1234567891011121314151617
WITH RECURSIVE
  edges AS (
    SELECT 1 AS from_node, 2 AS to_node UNION ALL
    SELECT 1, 3 UNION ALL
    SELECT 2, 4 UNION ALL
    SELECT 3, 4 UNION ALL
    SELECT 4, 5
  ),
  reachable AS (
    SELECT 1 AS node
    UNION ALL
    SELECT e.to_node FROM reachable AS r JOIN edges AS e ON e.from_node = r.node
  )
SELECT node, COUNT(*) AS times_reached
FROM reachable
GROUP BY node
ORDER BY node
Result in BigQuery
nodetimes_reached
111
221
331
442
552
5 rows

If you only want the set of reachable nodes, finish with SELECT DISTINCT node. Counting routes can blow up fast on large graphs, because every route is a row that the next round has to process.

Cycles never end

If the data has a cycle, a recursive term that always finds a next row never returns an empty round. BigQuery stops it at 500 iterations. We ran this query on a three-node cycle a > b > c > a:

WITH RECURSIVE
  edges AS (
    SELECT 'a' AS from_node, 'b' AS to_node UNION ALL
    SELECT 'b', 'c' UNION ALL
    SELECT 'c', 'a'
  ),
  walk AS (
    SELECT 'a' AS node
    UNION ALL
    SELECT e.to_node FROM walk AS w JOIN edges AS e ON e.from_node = w.node
  )
SELECT COUNT(*) AS steps FROM walk

It ran for about 2 minutes and 17 seconds and ended with:

A recursive CTE has reached the maximum number of iterations: 500.

So each iteration takes a fraction of a second, even for three rows, and a runaway recursion is slow before it fails. This example is not on the page for that reason.

UNION DISTINCT is not a way out. BigQuery doesn’t allow it in a recursive CTE:

UNION DISTINCTBigQuery rejects this query
123456
WITH RECURSIVE counter AS (
  SELECT 1 AS n
  UNION DISTINCT
  SELECT n + 1 FROM counter WHERE n < 3
)
SELECT n FROM counter
Error in BigQuery

Only UNION ALL is supported for WITH RECURSIVE.

The usual fix is to carry the path in an array and refuse to step to a node already in it. The check is a plain NOT IN UNNEST(array) on a column of the previous round, not a subquery on the CTE:

stop when a node is already on the path
123456789101112131415161718
WITH RECURSIVE
  edges AS (
    SELECT 'a' AS from_node, 'b' AS to_node UNION ALL
    SELECT 'b', 'c' UNION ALL
    SELECT 'c', 'a' UNION ALL
    SELECT 'c', 'd'
  ),
  walk AS (
    SELECT 'a' AS node, ['a'] AS visited
    UNION ALL
    SELECT e.to_node, ARRAY_CONCAT(w.visited, [e.to_node])
    FROM walk AS w
    JOIN edges AS e ON e.from_node = w.node
    WHERE e.to_node NOT IN UNNEST(w.visited)
  )
SELECT node, ARRAY_TO_STRING(visited, '>') AS path
FROM walk
ORDER BY path
Result in BigQuery
nodepath
1aa
2ba>b
3ca>b>c
4da>b>c>d
4 rows

Node c links back to a, but a is already in the path a>b>c, so that route ends. Add a depth counter with a cap in the WHERE as a second guard if the path array could get large.

Rules the recursive term must follow

The rules are enforced with errors. These are the ones we ran into.

RECURSIVE is required. Without it the CTE name isn’t visible inside its own definition:

WITH without RECURSIVEBigQuery rejects this query
123456
WITH counter AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM counter WHERE n < 3
)
SELECT n FROM counter
Error in BigQuery

Table "counter" must be qualified with a dataset (e.g. dataset.table).

Only one reference to the CTE is allowed in the recursive term, so you can’t join it to itself, and a reference inside a subquery in WHERE counts as a second one:

joining the CTE to itselfBigQuery rejects this query
123456
WITH RECURSIVE counter AS (
  SELECT 1 AS n
  UNION ALL
  SELECT a.n + b.n FROM counter AS a JOIN counter AS b ON TRUE WHERE a.n < 3
)
SELECT n FROM counter
Error in BigQuery

Multiple recursive references to table 'counter' are not allowed at [4:43]

The recursive term must produce the type of the base term. BigQuery does not widen it: SELECT 1 is INT64, so a recursive term that returns FLOAT64 fails:

INT64 base term, FLOAT64 recursive termBigQuery rejects this query
123456
WITH RECURSIVE total AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 0.5 FROM total WHERE n < 3
)
SELECT n FROM total
Error in BigQuery

Cannot coerce type (FLOAT64) of column 1 in the recursive term to type (INT64) of column 1 in non-recursive term at [4:3]

Make the base term the wider type:

start from 1.0 instead of 1
123456
WITH RECURSIVE total AS (
  SELECT 1.0 AS n
  UNION ALL
  SELECT n + 0.5 FROM total WHERE n < 2
)
SELECT n FROM total ORDER BY n
Result in BigQuery
n
11.0
21.5
32.0
3 rows

The recursive term can’t have ORDER BY:

ORDER BY in the recursive termBigQuery rejects this query
123456
WITH RECURSIVE counter AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM counter WHERE n < 3 ORDER BY n
)
SELECT n FROM counter
Error in BigQuery

A recursive query may not use ORDER BY at [4:41]

The dry run does not catch every rule

Google’s reference lists aggregation and window functions on the CTE as disallowed in the recursive term. A dry run of such queries passes, and the failure comes only when the query runs. This one fails at run time with a message that doesn’t match the cause:

GROUP BY in the recursive termBigQuery rejects this query
123456
WITH RECURSIVE counter AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM counter WHERE n < 3 GROUP BY n
)
SELECT n FROM counter
Error in BigQuery

An unsupported query pattern using WITH RECURSIVE was detected (such as using IN or EXISTS within the recursive term). Please rewrite the query.

The message mentions IN and EXISTS, but the query has neither. We saw the same message for SUM(n) OVER () in the recursive term. A dry run of GROUP BY, SUM() OVER and COUNT(*) all validated with 0 bytes, so a clean dry run is not proof that a recursive query will run.

Alternatives for series

If the number of rounds is known, you don’t need recursion. A series of dates or numbers is one function call, and it is a single step instead of up to 500 rounds:

five days with GENERATE_DATE_ARRAY
123
SELECT day
FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2026-03-01', DATE '2026-03-05')) AS day
ORDER BY day
Result in BigQuery
day
12026-03-01
22026-03-02
32026-03-03
42026-03-04
52026-03-05
5 rows

For graph traversal with a known maximum depth, a loop in scripting (REPEAT with an INSERT into a temp table) is the other option. Google’s guide suggests that loop for finding out whether a recursion terminates.

Materialization

Google’s reference notes that BigQuery materializes the result of a recursive CTE but not of a non-recursive one, which is executed once per reference.