The previous module's top-N-per-group query buried its ranking logic inside a subquery, nested in a FROM clause, read from the inside out. A CTE names that same logic and puts it first, read top to bottom instead, exactly the cleaner rewrite that lesson promised.
Learning Objectives
- Define a CTE precisely: a named, temporary result set, usable only within the single query that defines it.
- Rewrite a subquery as a CTE, and explain when that's actually worth doing.
- Distinguish a CTE from both a subquery and a VIEW (Module 10).
WITH name AS (...)
Defined first, named
Temporary Result
Exists only for this query
Main Query
Reads the CTE by name
Result
Read top to bottom, not nested
What a CTE Actually Is
A CTE (Common Table Expression) is a named, temporary result set, defined with WITH before the main query, and referenced by that name inside it. It exists only for the duration of that one query, there's nothing left behind afterward, unlike a VIEW, which is saved under its name permanently until explicitly dropped.
Rewriting the Top-N-Per-Group Subquery as a CTE
Identical result to the subquery version from the previous module, read in the natural order a query actually executes in: define ranked first, then query it, instead of nesting the ranking logic inside a FROM clause and reading inside-out.
WITH ranked AS (
SELECT first_name, last_name, department_id, salary,
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
FROM employees
)
SELECT * FROM ranked
WHERE salary_rank <= 2;CTE vs Subquery vs VIEW
| Subquery | CTE | VIEW | |
|---|---|---|---|
| Lifespan | That one query only | That one query only | Saved permanently, until dropped |
| Named? | No, nested inline | Yes, given a name with WITH | Yes, given a name with CREATE VIEW |
| Readability | Nested, read inside-out | Sequential, read top to bottom | Hidden behind the view's name entirely |
| Reusable across queries? | No | No | Yes, by every future query |
A CTE and a subquery are functionally equivalent in most simple cases, the real difference is readability, not capability. PostgreSQL has one genuine behavioral quirk here: before PostgreSQL 12, a CTE was always an optimization barrier, evaluated separately and never merged into the surrounding query's plan; PostgreSQL 12+ can inline a simple CTE just like a subquery, unless it's explicitly marked WITH ranked AS MATERIALIZED (...) to force the old behavior.
Try It Yourself
Rewrite this subquery as a CTE: SELECT * FROM (SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id) dept_avg WHERE avg_salary > 70000;, on PostgreSQL.
WITH dept_avg AS ( SELECT department_id, AVG(salary) AS avg_salary FROM employees GROUP BY department_id ) SELECT * FROM dept_avg WHERE avg_salary > 70000;
Frequently Asked Questions
Does using a CTE instead of a subquery make a query faster?
Not inherently, on most of these databases a CTE and an equivalent subquery produce the same execution plan, the benefit is almost entirely readability. PostgreSQL before version 12 was the one real exception, where a CTE was always evaluated as a separate step, occasionally slower than an inlined subquery for the exact same logic.
Can a CTE be used in an INSERT, UPDATE, or DELETE statement, not just SELECT?
Yes, on every database in this course, a CTE defined with WITH can feed an INSERT ... SELECT, or be referenced by an UPDATE or DELETE statement that follows it, not just a plain SELECT.
Common Beginner Mistakes
Expecting a CTE to persist across multiple separate queries
It doesn't, a CTE only exists for the single statement that defines it, the moment that query finishes, it's gone. A VIEW is the tool for something that needs to be queried repeatedly across many separate statements.
Assuming every database needs no minimum version for CTEs
MySQL needs 8.0+ and MariaDB needs 10.2+, specifically, CTEs didn't exist on either before those versions, the same era window functions arrived in both. PostgreSQL, SQL Server, Oracle, and SQLite (3.8.3+) have all supported basic CTEs for considerably longer.
Best Practices
- Reach for a CTE over a subquery whenever naming the intermediate result would make the query noticeably easier to follow.
- Don't assume a CTE is a performance optimization by itself, verify with the actual execution plan if performance specifically matters.
- Use a VIEW instead of a CTE when the same named result genuinely needs to be queried across many separate statements, not just once.
Interview Questions
What's the real difference between a CTE and a subquery?
Functionally, in most simple cases, nothing, both produce a temporary result used only within one query. The real difference is structural and readability-focused: a CTE is named and defined before the main query, read top to bottom, while a subquery is nested inline inside a FROM or WHERE clause, read from the inside out. Neither persists beyond the single statement that defines it.
Why would a CTE and a VIEW with identical SELECT logic behave differently across two separate queries?
A VIEW is saved permanently under its name and can be queried by any future statement, exactly like a table. A CTE exists only for the single query that defines it in its WITH clause, a second, separate query has no idea it ever existed and would need its own WITH clause defining the same logic again.
Summary
A CTE is a named, temporary result set, defined with WITH and read before the query that uses it, functionally similar to a subquery but far more readable once logic gets nested more than one level deep. It never persists beyond its one query, that's what a VIEW is for instead. PostgreSQL before version 12 is the one database here where a CTE and an equivalent subquery could genuinely perform differently.
What's Next?
Multiple CTEs is next: defining more than one named result in a single WITH clause, with later ones able to build on earlier ones.
Ready to Practice SQL?
Write and run real SQL queries online, right in your browser, no install or signup required. Pick a playground based on which database you want to practice on.
Both open a third-party SQL playground in a new tab. External site terms and privacy policy apply.
Or try 50 SQL practice questions →