In this file: what a CTE actually is, how the
WITHclause works, its scope and lifetime, and the three foundational CTEs (employes_CTE,dept_CTE,location_CTE) every later file in this module builds on.
How can we create a temporary, reusable, named result set — one we can
SELECT from just like a table — without creating a physical table on
disk?
Every analyst eventually writes a query that’s correct but unreadable: five
levels of nested subqueries, or a nine-way join with column names that no
longer map cleanly to a business concept. A CTE is the fix. It lets you name
an intermediate step — employes_CTE, active_departments,
high_value_customers — so the final query reads like the sentence a
stakeholder would use to describe it, not like a wall of parentheses.
See 01_Basic_CTE.sql for the runnable version of every
query below.
WITH employes_CTE AS (
SELECT
emp_id,
emp_name,
dept_id,
manager_id
FROM
EMPLOYES
)
SELECT
*
FROM
employes_CTE;
A CTE is introduced with the WITH keyword, given a name, and defined by a
query in parentheses. That name then behaves like a table for the
single statement that follows it:
WITH <name> AS ( <query> ) — define the temporary result set.SELECT, JOIN, or filter against
<name> exactly as it would against a real table.<name> no longer exists —
there is nothing left to clean up, and nothing was ever written to disk.This file builds three such CTEs — one per source table:
| CTE | Wraps | Columns exposed |
|---|---|---|
employes_CTE |
EMPLOYES |
emp_id, emp_name, dept_id, manager_id |
dept_CTE |
departments |
dept_id, dept_name, location_id |
location_CTE |
locations |
location_id, city |
employes_CTE returned every row from EMPLOYES unchanged — a CTE doesn’t
filter or transform data on its own; it simply gives a query a name you can
reuse. The underlying EMPLOYES table was never modified, and nothing
persisted after the statement completed. This is the core trade CTEs make:
zero storage cost, in exchange for a scope that ends the moment the query
does.
| CTE | Subquery | View | |
|---|---|---|---|
| Named | Yes | No (anonymous) | Yes |
| Reusable within the same query | Yes | No — must be repeated | Yes, across queries |
| Persisted to disk | No | No | Yes (metadata) |
| Scope | One statement | The clause it’s nested in | Permanent, until dropped |
| Typical use | Readability, staged logic | One-off inline filtering | Shared, long-lived logic |
WITH keyword entirely and writing a bare subquery instead.AS employes_CTE, not just (...).Cross-engine note: In PostgreSQL 12+, a non-recursive CTE is inlined into the outer query by default (like a subquery) unless it’s referenced more than once, is recursive, or has side effects — you can force the old “always materialize” behavior with
MATERIALIZED. MySQL 8.0+ and SQL Server generally treat CTEs as inlined views for the optimizer’s purposes. Don’t assume a CTE guarantees a performance win over an equivalent subquery — verify withEXPLAIN.
manager_id
for someone else).employes_CTE query as a plain subquery — confirm the result
is identical.Next: 02_Multiple_CTEs.md — chaining more than
one CTE together in a single WITH clause.