Recursion is possible in SQL via the use of a type of subquery known as a Common Table Element (CTE). One handy feature of CTEs is that they have names which makes it easy for us to reference these subqueries when we want. Let’s name our cte cteRecursion.

Now, we can invoke cteRecursion from within cteRecursion. That is, we write a CTE that applies some transformation to some input recursively until some stopping condition is met.

Pseudocode

WITH cteRecursion AS (
	SELECT 
		anchorMembers
	UNION ALL SELECT 
		recursiveMembers
	FROM cteRecursion
	)
SELECT * FROM cteRecursion;

As you can see, the CTE contains a union between so-called anchor members and recursive members.

Handling Regex

One general area where I find recursion to be particularly useful in SQL is regex. For example, suppose we have a string of comma-separated values:

DECLARE @str = 'Eggs,Milk,Juice,Bread,';

Anchor

Now, let’s write a SQL query that can separate this string to a column of values. First we write the transformation for our anchor members output and remainder. These anchor transformations define what happens in our recursion on the first iteration.

SQL

SELECT
	SUBSTRING(@str,0,CHARINDEX(',',@str,1)) AS output
,	SUBSTRING(@str,CHARINDEX(',',@str,1) + 1, LEN(@str)) AS remainder

Output

\[\begin{array}{|c|c|} \hline \text{output} & \text{remainder} \\ \hline \text{eggs} & \text{Milk,Juice,Bread} \\ \hline \end{array}\]

We’ve split our string into two parts: output and remainder. Now we recurse through the latter on subsequent iterations.

Recursion

The transformations for the recursive members are identical to the transformations for the anchor members. But now we invoke cteRecursion and include a stopping condition, which stops the recursion when remainder contains no more commas.

SQL

WITH cteRecursion AS (
	SELECT  -- anchor members
		SUBSTRING(@str,0,CHARINDEX(',',@str,1)) AS output
	,	SUBSTRING(@str,CHARINDEX(',',@str,1) + 1, LEN(@str)) as remainder
	UNION ALL SELECT -- recursive members
		SUBSTRING(remainder,0,CHARINDEX(',',remainder,1))
	,	SUBSTRING(remainder,CHARINDEX(',',remainder,1) + 1, LEN(remainder))
	FROM cteRecursion -- invoke CTE
	WHERE CHARINDEX(',', remainder,1) <> 0 -- stopping condition
	)
SELECT 
	output
FROM cteRecursion;	

Final Output

So after some number of iterations, we return the following:

\[\begin{array}{|c|} \hline \text{output} \\ \hline \text{Eggs} \\ \text{Milk} \\ \text{Juice} \\ \text{Bread} \\ \hline \end{array}\]

Updated: