RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
SQL questions
MediumSQL#1076 min readJul 11, 2026

CTE

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: A CTE is like putting a sticky note on a messy SQL idea so the query reads like a story instead of a wall of nested parentheses.

Question: What is a CTE in SQL?

Answer: A CTE, or Common Table Expression, is a named result set that exists only for one SQL statement. You define it with WITH, then use it like a temporary table inside the main query. It is mainly for readability, but it can also help you reuse the same logic inside one statement, and recursive CTEs can walk trees like org charts or category hierarchies.

Interview-Ready Answer: I use a CTE when I want to break a big SQL statement into clear steps. It is a temporary named result set created with WITH and visible only to that single query. The big win is readability, and the important interview detail is that a CTE is not automatically faster than a subquery; depending on the database, it may be inlined or materialized.

🧠 Memory Map
Memory map — visual summary of this topic

What a CTE really is

Detailed Explanation: Think of a CTE as a named, temporary result set for one SQL statement. It is not a stored table, not a permanent view, and not something other queries can reuse later. It is just a clean way to split one complex statement into smaller logical pieces.

How it works under the hood

  1. The database reads the WITH clause first and assigns a name to each CTE.
  2. Later CTEs in the same WITH block can reference earlier ones, and the main query can reference any of them.
  3. For a nonrecursive CTE, the optimizer may either inline it, which means it substitutes the logic into the final plan, or materialize it, which means it computes rows first and stores the intermediate result.
  4. For a recursive CTE, there are two parts: an anchor query, which starts the result, and a recursive member, which keeps adding rows until no new rows appear.
  5. The final SELECT consumes the CTE like a normal source of rows.

CTE vs subquery vs temp table

FeatureCTESubqueryTemp table
ScopeOne statementOne statementSession / batch
ReadabilityHighMediumHigh
Reuse inside queryEasyPoorEasy
StorageMaybeNoYes
Best forStep-by-step logicSimple filtersMulti-step workflows

When and why to use it

Use a CTE when the query has stages: clean rows first, aggregate next, rank last. That makes SQL easier to debug and easier to discuss in an interview. Use recursive CTEs when the data is hierarchical, such as managers, folders, BOMs, or categories.

Performance notes that interviewers like

  • In many engines, a nonrecursive CTE costs about the same as a subquery if it is inlined.
  • If the engine materializes it, it may use extra memory or spill to disk. A few thousand rows is usually small; millions of wide rows can become expensive.
  • Space cost can be O(n) if the intermediate result must be stored.
  • Recursive CTEs can be efficient for tree-like data, often near O(V + E) for a clean hierarchy, but duplicates or cycles can make them much slower if you use UNION ALL without safeguards.
  • Version difference: PostgreSQL 12+ is notable because nonrecursive CTEs are often inlined by default unless you ask for MATERIALIZED; older PostgreSQL versions tended to treat many CTEs more like an optimization fence.

Important edge cases

  • ORDER BY inside the CTE does not guarantee final output order. Only the outer query’s ORDER BY guarantees order.
  • CTE scope lasts for one statement only.
  • Duplicate rows in recursive CTEs can explode quickly, so use UNION or cycle checks when needed.
  • Nulls and filters should be handled inside the CTE if they affect every later step.

Memory Hook: A CTE is a sticky note for a query: it helps you remember each step, but it does not turn into a permanent file cabinet.

Real-World Example: Imagine a checkout service for an e-commerce app. The analytics team wants daily revenue, but raw orders include canceled payments, null amounts, and refunds. A CTE is perfect here: one step cleans the orders, the next step aggregates revenue, and the final step ranks the best day. That makes the dashboard query easy to read and easy to audit when finance asks, 'Why did revenue change?'

What goes wrong when someone misunderstands CTEs? A developer assumes the WITH block permanently stores data or that ORDER BY inside the CTE controls the final result. In production, the revenue dashboard may look unstable: totals shift between runs, top-day rows appear in a different order, and support sees tickets like 'today’s sales are duplicated.' In logs, you might see nothing obviously broken, which is exactly why the bug is dangerous — the SQL is valid, but the logic assumption is wrong.

SQL
-- PostgreSQL-compatible demo: a CTE breaks one messy report into clean steps.
-- The sample data includes edge cases: a cancelled order and a NULL amount.

DROP TABLE IF EXISTS orders;
CREATE TABLE orders (
    order_id    INTEGER PRIMARY KEY,
    customer_id INTEGER NOT NULL,
    order_date  DATE NOT NULL,
    amount      NUMERIC(10,2),
    status      TEXT NOT NULL
);

INSERT INTO orders (order_id, customer_id, order_date, amount, status) VALUES
    (1, 101, DATE '2026-07-01',  50.00, 'paid'),
    (2, 101, DATE '2026-07-01',  25.00, 'paid'),
    (3, 102, DATE '2026-07-01',  80.00, 'cancelled'),
    (4, 103, DATE '2026-07-02', 120.00, 'paid'),
    (5, 104, DATE '2026-07-02',   NULL, 'paid'),
    (6, 105, DATE '2026-07-03',  40.00, 'paid');

-- Step 1: clean the data once.
-- We remove cancelled rows and NULL/invalid amounts so later logic does not repeat the same filters.
-- If this CTE returns zero rows, the rest of the query still works safely.
WITH cleaned_orders AS (
    SELECT order_id, customer_id, order_date, amount
    FROM orders
    WHERE status = 'paid'
      AND amount IS NOT NULL
      AND amount > 0
),
-- Step 2: aggregate the cleaned data.
-- This makes the business rule easy to read: revenue is the sum of valid paid orders per day.
daily_revenue AS (
    SELECT order_date, SUM(amount) AS revenue
    FROM cleaned_orders
    GROUP BY order_date
),
-- Step 3: rank the result so the outer query can ask for the best day.
ranked_days AS (
    SELECT
        order_date,
        revenue,
        ROW_NUMBER() OVER (ORDER BY revenue DESC, order_date ASC) AS rn
    FROM daily_revenue
)
SELECT order_date, revenue
FROM ranked_days
WHERE rn = 1;

-- Edge-case check: a CTE can also be used to prove that filters remove bad data cleanly.
WITH qualifying_orders AS (
    SELECT order_id
    FROM orders
    WHERE status = 'paid'
      AND amount IS NOT NULL
      AND amount > 1000
)
SELECT COUNT(*) AS qualifying_order_count
FROM qualifying_orders;

Follow-up & Tricky Questions:

  • Can a CTE reference another CTE? Yes, later CTEs can use earlier ones in the same WITH block. That is one reason they are great for building a query in small, readable stages.
  • Are CTEs faster than subqueries? Not by default. A CTE is often just a readability tool; the optimizer may rewrite it like a subquery, or it may materialize it depending on the engine and version.
  • What is a recursive CTE used for? It is used for hierarchical or repeated relationships, like managers, folder trees, bill of materials, or parent-child categories.
  • Can you update from a CTE? In many databases, yes, if the syntax supports it. The CTE supplies rows, and the update statement uses that result set as a source.
  • Does a CTE persist after the query finishes? No. It disappears as soon as that statement ends.
  • Tricky: Does ORDER BY inside the CTE guarantee the final order? No. Only the outermost query’s ORDER BY guarantees the output order.
  • Tricky: If I reference the same CTE twice, is it always computed once? Not always. Some engines may inline it, some may materialize it, and the choice can depend on the database and version.
  • Tricky: Is a CTE the same as a temporary table? No. A temp table is a real table object for the session; a CTE is only a named query result for one statement.

Common Mistakes:

  • Thinking a CTE is always faster. Correction: it is mainly for clarity; performance depends on the optimizer and whether the CTE is inlined or materialized.
  • Using ORDER BY inside the CTE and trusting it later. Correction: sort in the outer query if the final row order matters.
  • Forgetting CTE scope is one statement only. Correction: if you need the result later, use a temp table or permanent table.
  • Letting recursive CTEs loop forever on cyclic data. Correction: use UNION, depth limits, or cycle checks.

Memory Hook: CTE = Clean Temporary Explanation. It is the query’s scratchpad: perfect for staging ideas, useless after the sentence ends.

Cheat Sheet:

  • CTE means Common Table Expression.
  • Defined with WITH, used in one statement only.
  • Great for breaking one hard query into readable steps.
  • Can be recursive for hierarchies.
  • Not automatically faster than a subquery.
  • Final order comes from the outer ORDER BY, not the CTE.

Practice Tasks:

  • Rewrite a nested subquery into two CTE steps: filter first, aggregate second.
  • Add a third CTE that ranks the aggregated rows and returns only the top result.
  • Build a recursive CTE for a simple employee-manager tree or category tree.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

-- PostgreSQL-compatible demo: a CTE breaks one messy report into clean steps. -- The sample data includes edge cases: a cancelled order and a NULL amount. DROP TABLE IF EXISTS orders; CREATE TABLE orders ( order_id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL, order_date DATE NOT NULL, amount NUMERIC(10,2), status TEXT NOT NULL ); INSERT INTO orders (order_id, customer_id, order_date, amount, status) VALUES (1, 101, DATE '2026-07-01', 50.00, 'paid'), (2, 101, DATE '2026-07-01', 25.00, 'paid'), (3, 102, DATE '2026-07-01', 80.00, 'cancelled'), (4, 103, DATE '2026-07-02', 120.00, 'paid'), (5, 104, DATE '2026-07-02', NULL, 'paid'), (6, 105, DATE '2026-07-03', 40.00, 'paid'); -- Step 1: clean the data once. -- We remove cancelled rows and NULL/invalid amounts so later logic does not repeat the same filters. -- If this CTE returns zero rows, the rest of the query still works safely. WITH cleaned_orders AS ( SELECT order_id, customer_id, order_date, amount FROM orders WHERE status = 'paid' AND amount IS NOT NULL AND amount > 0 ), -- Step 2: aggregate the cleaned data. -- This makes the business rule easy to read: revenue is the sum of valid paid orders per day. daily_revenue AS ( SELECT order_date, SUM(amount) AS revenue FROM cleaned_orders GROUP BY order_date ), -- Step 3: rank the result so the outer query can ask for the best day. ranked_days AS ( SELECT order_date, revenue, ROW_NUMBER() OVER (ORDER BY revenue DESC, order_date ASC) AS rn FROM daily_revenue ) SELECT order_date, revenue FROM ranked_days WHERE rn = 1; -- Edge-case check: a CTE can also be used to prove that filters remove bad data cleanly. WITH qualifying_orders AS ( SELECT order_id FROM orders WHERE status = 'paid' AND amount IS NOT NULL AND amount > 1000 ) SELECT COUNT(*) AS qualifying_order_count FROM qualifying_orders;