SQL CTE and WITH — Applied Multiple CTEs and Recursion

ADVCTEMultiple & Recursive CTEsAPI Use CasesPostgreSQL/BigQuery5 questions
1 / 5 · In progress 0 / 5 completed
QUESTION 1

Basic CTE (WITH Clause) — Replace a Subquery with a CTE for Better Readability

WITHCTEGROUP BYSummary API
Background

A CTE (Common Table Expression) is a temporary named query defined with WITH name AS (...). Extracting logic into a CTE instead of nesting a subquery makes the SQL readable from top to bottom.

WITH cte_name AS (
  -- Write a SELECT statement here (think of it as a temporary table)
  SELECT col1, SUM(col2) AS total
  FROM   some_table
  GROUP BY col1
)
-- Write the main SELECT after the CTE definition
SELECT *
FROM   cte_name
WHERE  total > 10000;

CTEs can be used with SELECT, INSERT, UPDATE, and DELETE. This quiz covers SELECT only.

Why use a CTE: A subquery is nested inside the outer SELECT, so deep nesting becomes hard to read. A CTE lets you declare, “define this calculation first, then use it,” making code review and maintenance dramatically easier. It is especially useful for complex SQL in API backends.
Problem

Using the orders table below, write a CTE query for an API that calculates the total sales, order count, and average order value for each month.

Return only months whose average order value is greater than 40,000.

Tables
▸ orders
order_idorder_monthamount
12024-0130000
22024-0145000
32024-0120000
42024-0255000
52024-0260000
62024-0380000
72024-0315000
82024-0325000
Expected Output
order_monthtotal_salesorder_countavg_order
2024-02115000257500
Model Answer
-- Define a CTE: WITH lets you treat a subquery like a temporary table
WITH monthly_summary AS (

  -- ① Build an intermediate table by aggregating orders by month
  SELECT                           -- SELECT: specify the columns to retrieve
    order_month,                   -- Key column used for monthly aggregation
    SUM(amount)   AS total_sales,  -- SUM: calculate monthly sales; AS assigns an alias
    COUNT(*)      AS order_count,  -- COUNT(*): count all rows = number of orders
    AVG(amount)   AS avg_order     -- AVG: calculate the average monthly order value
  FROM     orders                  -- FROM: specify the source table
  GROUP BY order_month             -- GROUP BY: group rows from the same month

)

-- ② Use the CTE result like a table in FROM
SELECT                              -- SELECT: specify the final output columns
  order_month,
  total_sales,
  order_count,
  ROUND(avg_order, 0) AS avg_order  -- ROUND(value, digits): round decimals (0 means an integer)
FROM   monthly_summary              -- FROM: specify the CTE defined above
WHERE  avg_order > 40000            -- WHERE: keep only matching rows (greater than 40000)
ORDER BY order_month;               -- ORDER BY: sort months in ascending order

/*
  Logical execution order:
  1. CTE FROM orders            → Read every row from the table
  2. CTE GROUP BY order_month   → Group by month and calculate SUM/COUNT/AVG
  3. Outer FROM monthly_summary → Reference the CTE result as a virtual table
  4. Outer WHERE avg_order > 40000 → Apply the filter
  5. Outer ORDER BY order_month → Sort in ascending order
  */
Explanation (table transitions & key points)
WITH monthly_summary AS ( SELECT order_month, SUM(amount) AS total_sales, COUNT(*) AS order_count, AVG(amount) AS avg_order FROM orders GROUP BY order_month ) SELECT order_month, total_sales, order_count, ROUND(avg_order, 0) AS avg_order FROM monthly_summary WHERE avg_order > 40000 ORDER BY order_month;
LEGEND
Rows read / loaded
① FROM
FROM ordersRead all 8 rows from orders. This is the input to the monthly_summary CTE.
1 / 4
order_idorder_monthamount
12024-0130,000
22024-0145,000
32024-0120,000
42024-0255,000
52024-0260,000
62024-0380,000
72024-0315,000
82024-0325,000
All 8 rows read
LEARNING POINTS
A CTE is a disposable named query: It exists only within the statement where it is defined and disappears when that statement finishes. Use a view (VIEW) or a table (CREATE TABLE AS) when you need persistence.
WHERE vs. HAVING: HAVING avg(amount) > 40000 normally produces the same result, but a CTE separates the aggregation logic from the filtering logic, making conditions easier to change.
ANTI-PATTERNS
Putting a semicolon in the middle of a CTE: WITH cte AS (...); SELECT ... is a syntax error. Everything from WITH through the end of SELECT is one SQL statement.
IN PRACTICE
Aggregation for dashboards: A monthly sales summary API is one of the most frequently called endpoints in an admin dashboard. If the rows aggregated by the WITH clause—all orders here—number in the tens of millions, calculating the result in real time on every request makes the API slow. A common production architecture precomputes the equivalent aggregation in an overnight batch and stores it in a summary table, or data mart.
QUESTION 2

Chained CTEs — Calculate User Purchase Ranks in Separate Steps

WITHMultiple CTEsJOINRanking APINTILE
Background

You can define multiple comma-separated CTEs after WITH, and each later CTE can reference an earlier one. This expresses a SQL pipeline in which “step ② uses the result of step ①.”

WITH
step1 AS (
  SELECT user_id, SUM(amount) AS total
  FROM   orders
  GROUP BY user_id
),
-- step2 can reference step1 (define consecutive CTEs separated by commas)
step2 AS (
  SELECT *,
         NTILE(4) OVER (ORDER BY total DESC) AS quartile
  FROM   step1   -- Reference the preceding CTE like a table
)
SELECT * FROM step2;
NTILE(n): A window function that divides rows into n groups and assigns bucket numbers from 1 through n. NTILE(4) classifies the top 25% as 1, the next 25% as 2, and so on into quartiles. It is common in customer segmentation analysis.
Problem

Using the users and orders tables below, build an API query that calculates each user's total purchases and ranks users from highest to lowest into quartiles with NTILE.

quartile=1 is the highest rank. Also join user_name from the users table into the final output.

Tables
▸ users
user_iduser_name
U01Alice
U02Bob
U03Carol
U04Dave
U05Eve
U06Frank
U07Grace
U08Hank
▸ orders
order_iduser_idamount
1U01120000
2U0150000
3U0230000
4U03200000
5U0475000
6U0510000
7U0695000
8U0740000
9U08160000
10U0620000
Expected Output
user_iduser_nametotal_amountquartile
U03Carol2000001
U01Alice1700001
U08Hank1600002
U06Frank1150002
U04Dave750003
U07Grace400003
U02Bob300004
U05Eve100004
Model Answer
WITH
user_totals AS (                  -- ① Sum orders by user (intermediate aggregation)
  SELECT                          -- SELECT: specify the columns to retrieve
    user_id,
    SUM(amount) AS total_amount   -- SUM: add multiple orders; AS assigns an alias
  FROM     orders                 -- FROM: source table
  GROUP BY user_id                -- GROUP BY: group at user level
),
user_ranked AS (                  -- ② Use user_totals and NTILE to assign quartiles (reference the previous CTE)
  SELECT
    user_id,
    total_amount,
    NTILE(4) OVER (               -- NTILE(4): divide all rows into four groups and assign bucket numbers
      ORDER BY total_amount DESC  -- ORDER BY ... DESC: sort purchase totals from highest to lowest
    ) AS quartile
  FROM   user_totals              -- FROM: reference the preceding user_totals CTE as a table
)

-- ③ Main query: JOIN users to user_ranked to add names
SELECT
  r.user_id,
  u.user_name,                           -- Retrieve the name from users and add it through the join
  r.total_amount,
  r.quartile
FROM       user_ranked r                 -- FROM: r is the alias (short name) for user_ranked
INNER JOIN users       u                 -- INNER JOIN: combine only rows present in both tables
        ON r.user_id = u.user_id         -- ON: join rows whose user_id values match
ORDER BY  r.quartile, r.total_amount DESC; -- ORDER BY: quartile ascending, then purchase total descending

/*
  Logical execution order:
  1. CTE user_totals: FROM orders                      → GROUP BY user_id
  2. CTE user_ranked: FROM user_totals
  3. Main: FROM user_ranked INNER JOIN users ON user_id → Add names
  4. Main: ORDER BY quartile, total_amount DESC
  */
Explanation (table transitions & key points)
WITH user_totals AS ( SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id ), user_ranked AS ( SELECT user_id, total_amount, NTILE(4) OVER ( ORDER BY total_amount DESC ) AS quartile FROM user_totals ) SELECT r.user_id, u.user_name, r.total_amount, r.quartile FROM user_ranked r INNER JOIN users u ON r.user_id = u.user_id ORDER BY r.quartile, r.total_amount DESC;
LEGEND
Rows read / loaded
① FROM
FROM ordersRead all 10 rows from orders. This is the input to the first CTE, user_totals.
1 / 5
order_iduser_idamount
1U01120,000
2U0150,000
3U0230,000
4U03200,000
5U0475,000
6U0510,000
7U0695,000
8U0740,000
9U08160,000
10U0620,000
All 10 rows read
LEARNING POINTS
Production value of a CTE pipeline: It declaratively separates aggregation, ranking, and joining into three steps. To switch from quartiles to quintiles, you only need to modify the user_ranked CTE.
NTILE vs. RANK: RANK() assigns positions based on value order, while NTILE(n) divides the row count into n groups and assigns bucket numbers. Unlike RANK, it forces rows into separate buckets even when values tie.
ANTI-PATTERNS
Referencing a later CTE from an earlier CTE: CTEs can only be referenced in definition order. step2 cannot use step3, except in the special case of a recursive CTE.
IN PRACTICE
Segmented CRM campaigns: NTILE-based ranking can directly drive newsletter or push-notification segments—for example, an exclusive VIP offer for the top 25% and a return-visit coupon for the bottom 25%. In production, it is common to reduce the data first with a WHERE condition such as “the last year,” rather than rank all-time purchases, to save compute resources.
QUESTION 3

CTE + ROW_NUMBER — Extract the Top N Products in Each Category

CTEROW_NUMBERPARTITION BYTop-N ExtractionRanking API
Background

APIs that retrieve only the top N rows in each category are extremely common. ROW_NUMBER() combined with a CTE is the simplest and most versatile pattern.

WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY category      -- Number rows independently within each category
      ORDER BY     sales DESC  -- Number from 1 in descending sales order
    ) AS rn
  FROM products
)
SELECT *
FROM   ranked
WHERE  rn <= 2;   -- Keep only the top two rows in each category
Why a CTE is necessary:The result of a window function such as ROW_NUMBER is evaluated in the SELECT phase. Because WHERE is evaluated earlier, you cannot use a ROW_NUMBER result in WHERE within the same SELECT. A CTE is needed to expose the result to an outer query.
Problem

From the product_sales table below, extract the two products with the highest sales in each category.

Even when values tie, use ROW_NUMBER to force the result to exactly two products.

Tables
▸ product_sales
product_idcategoryproduct_namesales
P01FoodApple85000
P02FoodBanana62000
P03FoodOrange62000
P04FoodGrapes41000
P05DrinkGreen Tea95000
P06DrinkCoffee78000
P07DrinkJuice78000
P08DrinkWater55000
Expected Output
categoryproduct_namesalesrn
DrinkGreen Tea950001
DrinkCoffee780002
FoodApple850001
FoodBanana620002
Model Answer
WITH ranked_products AS (

  -- ① Assign every product a sales position within its category
  SELECT                     -- SELECT: specify the columns to retrieve
    category,
    product_name,
    sales,
    ROW_NUMBER() OVER (      -- ROW_NUMBER(): window function that assigns sequential numbers starting at 1
      PARTITION BY category  -- PARTITION BY: split by category and reset numbering for each one
      ORDER BY sales DESC, product_id  -- Break equal sales by product_id ascending
    ) AS rn                  -- AS rn: define a short alias for row_number
  FROM product_sales         -- FROM: target table
)                            -- rn cannot be used in WHERE here; a window result is unavailable within the same SELECT

-- ② Keep only CTE rows where rn is 2 or less (the top two)
SELECT
  category,
  product_name,
  sales,
  rn
FROM   ranked_products  -- FROM: reference the CTE defined above
WHERE  rn <= 2            -- WHERE: keep positions 1 and 2 in each category
ORDER BY category, rn;  -- ORDER BY: sort by category ascending, then position ascending

/*
  Logical execution order:
  1. CTE FROM product_sales      → Read every product row
  2. CTE ROW_NUMBER() OVER(...)  → Number each category in descending sales order
  3. Outer FROM ranked_products      → Reference the CTE result
  4. Outer WHERE rn <= 2             → Keep the top two rows in each category
  5. Outer ORDER BY category, rn     → Sort and return the output
  */
Explanation (table transitions & key points)
WITH ranked_products AS ( SELECT category, product_name, sales, ROW_NUMBER() OVER ( PARTITION BY category ORDER BY sales DESC, product_id ) AS rn FROM product_sales ) SELECT category, product_name, sales, rn FROM ranked_products WHERE rn <= 2 ORDER BY category, rn;
LEGEND
Rows read / loaded
① FROM
FROM product_salesRead all 8 rows from product_sales. This is the input to the ranked_products CTE.
1 / 4
product_idcategoryproduct_namesales
P01FoodApple85,000
P02FoodBanana62,000
P03FoodOrange62,000
P04FoodGrapes41,000
P05DrinkGreen Tea95,000
P06DrinkCoffee78,000
P07DrinkJuice78,000
P08DrinkWater55,000
All 8 rows read
LEARNING POINTS
The basic top-N API pattern:Retrieving category-by-top-N results appears in every kind of system, including popular products by category in e-commerce and the top three stores by region. Learning the CTE + ROW_NUMBER pattern gives you a reusable solution.
SQL evaluation order:SQL is evaluated in the order FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY. Window functions run at the SELECT stage, after WHERE. That is why the result must first be exposed through a CTE.
ANTI-PATTERNS
Using ROW_NUMBER in WHERE without a CTE:SELECT *, ROW_NUMBER() OVER(...) AS rn FROM t WHERE rn <= 2 is an error. Always use a CTE or subquery.
IN PRACTICE
Popular-items-by-category API: Top-N extraction is often used to show recommended products by category on an e-commerce home page. Computing ROW_NUMBER across every product for every request is inefficient, so good performance tuning reduces the CTE input—for example, by ranking sales from only the last week rather than all historical sales.
QUESTION 4

CTE + LAG — Calculate Month-over-Month Growth

LAGCTEMonth-over-MonthGrowth RateTime-Series API
Background

LAG(col, n) is a window function that returns the value n rows before the current row. It is essential for time-series comparisons such as month-over-month, year-over-year, and day-over-day changes.

LAG(amount, 1) OVER (ORDER BY order_month)
-- Return amount from the preceding row when ordered by order_month
-- The first row has no preceding row, so it returns NULL

Month-over-month formula: (current month − previous month) ÷ previous month × 100. Use NULLIF to prevent division by zero when the previous month is NULL or 0.

LAG’s third argument (default value):LAG(col, 1, 0) returns 0 instead of NULL. Use it when an API response should avoid NULL.
The corresponding LEAD function references a following row.
Problem

Using the monthly_sales table below, create an API query that calculates each month’s sales, previous-month sales, and month-over-month growth rate (%).

For the first month, which has no preceding month, output NULL for both prev_sales and mom_rate.

Tables
▸ monthly_sales
order_monthsales
2024-01100000
2024-02120000
2024-03108000
2024-04135000
2024-05135000
2024-06150000
Expected Output
order_monthsalesprev_salesmom_rate
2024-01100000NULLNULL
2024-02120000100000+20.0
2024-03108000120000-10.0
2024-04135000108000+25.0
2024-051350001350000.0
2024-06150000135000+11.1
Model Answer
WITH sales_with_lag AS (

  -- ① Use LAG to add the preceding row’s sales to each row
  SELECT
    order_month,
    sales,
    LAG(sales, 1) OVER (    -- LAG(column, n): get the value n rows earlier (NULL if absent)
      ORDER BY order_month  -- ORDER BY: sort months ascending to define what “previous” means
    ) AS prev_sales         -- AS: name the column
  FROM monthly_sales        -- FROM: source table

)

-- ② Use the CTE to calculate mom_rate (month-over-month growth)
SELECT
  order_month,
  sales,
  prev_sales,
  ROUND(                     -- ROUND(value, digits): round to the specified number of digits
    (sales - prev_sales)     -- Difference between current and previous month (the increase)
    * 100.0                  -- Multiply by 100.0 for a percentage and floating-point division
    / NULLIF(prev_sales, 0)  -- NULLIF(a,b): return NULL when a equals b, preventing division by zero
  , 1)                       -- Second argument: round to one decimal place
  AS mom_rate                -- Name the calculated result mom_rate
FROM     sales_with_lag      -- FROM: reference the CTE defined above
ORDER BY order_month;        -- ORDER BY: sort months ascending

/*
  Logical execution order:
  1. CTE FROM monthly_sales      → Read all rows
  2. CTE LAG(sales,1) OVER(...)  → Add previous-month sales
  3. Outer FROM sales_with_lag       → Reference the CTE result
  4. Outer ROUND(...)                → Calculate month-over-month growth
  5. Outer ORDER BY order_month      → Sort by month
  */
Explanation (table transitions & key points)
WITH sales_with_lag AS ( SELECT order_month, sales, LAG(sales, 1) OVER ( ORDER BY order_month ) AS prev_sales FROM monthly_sales ) SELECT order_month, sales, prev_sales, ROUND( (sales - prev_sales) * 100.0 / NULLIF(prev_sales, 0) , 1) AS mom_rate FROM sales_with_lag ORDER BY order_month;
LEGEND
Rows read / loaded
① FROM
FROM monthly_salesRead all 6 rows from monthly_sales. This is the input to the sales_with_lag CTE.
1 / 3
order_monthsales
2024-01100,000
2024-02120,000
2024-03108,000
2024-04135,000
2024-05135,000
2024-06150,000
All 6 rows read
LEARNING POINTS
Production uses for LAG:It is essential for time-series analysis such as month-over-month sales, week-over-week user counts, and day-over-day stock prices. Dashboard APIs use it to return data for up and down arrows.
Why extract the calculation into a CTE:To calculate mom_rate from the prev_sales produced by LAG, first finalize prev_sales in a CTE and use it in the next SELECT, because a LAG result alias cannot be referenced in the same SELECT.
ANTI-PATTERNS
Using LAG without ORDER BY:LAG uses ORDER BY to define “previous.” Without ORDER BY, the result is unstable, so always specify it.
Integer division:(sales - prev_sales) / prev_sales performs integer division, truncates the fraction, and returns zero.* 100.0 must be used first to convert the calculation to floating point.
IN PRACTICE
Visualizing time-series trends and month-over-month change:MoM and YoY growth calculations constantly run behind BI tools and dashboard APIs. They provide the data for frontend labels such as “up 15% month over month.” Production quality depends on safely handling a previous month with zero sales (division by zero) or no previous month (NULL).
QUESTION 5

Recursive CTE — Expand Every Level of a Manager-to-Report Hierarchy

Recursive CTERECURSIVEHierarchy QueryOrganization APIWITH RECURSIVE
Background

A recursive CTE uses WITH RECURSIVE to process a hierarchy iteratively by referencing itself. Common uses include organization charts, category hierarchies, and bills of materials (BOMs).

WITH RECURSIVE org_tree AS (
  -- ① Anchor member: recursion starting point (select the top row)
  SELECT id, name, manager_id, 0 AS depth
  FROM   employees
  WHERE  manager_id IS NULL    -- No manager means the top level

  UNION ALL                    -- Vertically combine anchor and recursive rows, including duplicates

  -- ② Recursive member: keep adding direct reports to the preceding result
  SELECT e.id, e.name, e.manager_id, t.depth + 1
  FROM   employees e
  JOIN   org_tree   t ON e.manager_id = t.id
)
SELECT * FROM org_tree;
Recursion termination condition:Recursion ends when the recursive member returns zero rows. In production, add a depth limit such as WHERE depth < 10 to prevent infinite loops.
Problem

From the self-referencing employees table below, expand the hierarchy of every employee starting from the CEO, whose manager_id is NULL.

For each employee, also output depth (hierarchy depth, with CEO = 0) and path (for example, "CEO→Director→Manager").

Tables
▸ employees
emp_idnamemanager_id
1Alex (CEO)NULL
2Blake (Sales Director)1
3Casey (Engineering Director)1
4Drew (Sales Manager)2
5Emery (Sales Rep)4
6Finley (Tech Lead)3
Expected Output
emp_idnamemanager_iddepthpath
1Alex (CEO)NULL0Alex (CEO)
2Blake (Sales Director)11Alex (CEO)→Blake (Sales Director)
3Casey (Engineering Director)11Alex (CEO)→Casey (Engineering Director)
4Drew (Sales Manager)22Alex (CEO)→Blake (Sales Director)→Drew (Sales Manager)
6Finley (Tech Lead)32Alex (CEO)→Casey (Engineering Director)→Finley (Tech Lead)
5Emery (Sales Rep)43Alex (CEO)→Blake (Sales Director)→Drew (Sales Manager)→Emery (Sales Rep)
Model Answer
-- Recursive CTEs use WITH RECURSIVE (PostgreSQL and BigQuery)
WITH RECURSIVE org_tree AS (

  -- ① Anchor member: select the CEO row that starts recursion
  SELECT                    -- SELECT: columns to retrieve
    emp_id,
    name,
    manager_id,
    0    AS depth,          -- Set the starting depth to 0 and name the column with AS
    name AS path            -- Initialize path with the employee’s own name
  FROM  employees           -- FROM: data source
  WHERE manager_id IS NULL  -- WHERE ... IS NULL: keep the row with no manager, the top level

  UNION ALL  -- UNION ALL: vertically combine SELECT results, including duplicates

  -- ② Recursive member: keep adding direct reports to org_tree
  SELECT
    e.emp_id,
    e.name,
    e.manager_id,
    t.depth + 1,                      -- Add 1 to the parent depth to descend one level
    t.path || '→' || e.name           -- ||: string concatenation operator in PostgreSQL and similar databases
  FROM       employees  e             -- FROM: e is the report side, the next level
  INNER JOIN org_tree   t             -- INNER JOIN: t is the parent row established in the preceding step
          ON e.manager_id = t.emp_id  -- ON: join where the report’s manager ID equals the parent emp_id
  WHERE t.depth < 10                   -- WHERE: limit recursion to 10 levels to prevent infinite loops

)

SELECT
  emp_id,
  name,
  manager_id,
  depth,
  path
FROM     org_tree        -- FROM: reference the completed recursive CTE
ORDER BY depth, emp_id;  -- ORDER BY: sort by depth ascending, then emp_id ascending

/*
  Logical execution order (recursive iterations):
  1. Anchor                    → Generate the CEO starting row
  2. Iteration 1               → Expand children
  3. Iteration 2               → Expand children
  4. Iteration 3               → Expand children
  5. Iteration 4               → Stop because no rows are added
  6. UNION ALL → ORDER BY  → Stack rows and sort
  */
Explanation (table transitions & key points)
WITH RECURSIVE org_tree AS ( SELECT emp_id, name, manager_id, 0 AS depth, name AS path FROM employees WHERE manager_id IS NULL UNION ALL SELECT e.emp_id, e.name, e.manager_id, t.depth + 1, t.path || '→' || e.name FROM employees e INNER JOIN org_tree t ON e.manager_id = t.emp_id WHERE t.depth < 10 ) SELECT emp_id, name, manager_id, depth, path FROM org_tree ORDER BY depth, emp_id;
LEGEND
Rows read / loaded
⓪ FROM
FROM employeesRead all 6 rows from employees. This self-referencing data is the source used to build the organization tree.
1 / 4
emp_idnamemanager_id
1Alex (CEO)NULL
2Blake (Sales Director)1
3Casey (Engineering Director)1
4Drew (Sales Manager)2
5Emery (Sales Rep)4
6Finley (Tech Lead)3
All 6 rows read
LEARNING POINTS
Production uses for recursive CTEs:They are essential in systems that handle hierarchies, including organization APIs, e-commerce category trees, folder structures, and product BOM expansion. Ordinary joins cannot handle a hierarchy whose number of levels is unknown.
Roles of the anchor and recursive members:The anchor is the initial seed, and the recursive member grows the next shoots from that seed. Processing ends when no more shoots appear—that is, when the join returns zero rows.
ANTI-PATTERNS
Using cyclic data without a depth limit:A→B→A creates an infinite loop. Always set an upper bound with WHERE depth < N.
IN PRACTICE
Building trees from hierarchical data:Recursive CTEs power not only organization charts but also threaded comments with reply trees and APIs that retrieve file-system directory structures. If bad production data creates a cycle—for example, A manages B while B manages A—the query can loop forever and exhaust database resources. A professional implementation therefore always adds a depth failsafe such as WHERE depth < 10.