By now your queries have grown teeth — joins inside subqueries inside aggregations. Imagine emailing a 30-line monster to a teammate with "just run this whenever you need the sales summary". There's a better way: give the query a name. Name it permanently and you have a view; name it for just one statement and you have a CTE. This lesson is about both — plus the recursive trick that lets SQL walk an entire org chart.
Views — a saved query wearing a table costume
CREATE VIEW high_earners AS
SELECT employee_id, name, salary
FROM employees
WHERE salary > 70000;
-- now anyone can just:
SELECT * FROM high_earners;A view stores only the query, not the data — query the view and the engine runs the underlying SELECT against live tables, so results are always fresh. Two superpowers come free: simplification (a monster join hides behind one name, used by everyone identically) and security — notice high_earners exposes no department column, and could equally hide salary from juniors or restrict rows to one region. Grant access to the view, not the table, and the hidden columns simply don't exist for that user.
Updatable views — when can you write through one?
Can you UPDATE high_earners SET ...? Sometimes. The test is whether the engine can map your change back to exactly one row of one base table. A simple single-table, row-filtering view — yes. But add any of: aggregates, GROUP BY, DISTINCT, UNION, or most joins — and the mapping breaks. (Which underlying row would updating an average even mean?) Bonus interview point: WITH CHECK OPTION stops you updating a row out of the view — e.g. setting a high-earner's salary to 50000, which would make it vanish from the view it was edited through.
Materialized views — trading freshness for speed
A regular view re-runs its query every time — always fresh, repeatedly expensive. A materialized view flips the trade: it stores the result like a real table, so reads are instant, but the snapshot goes stale until you refresh it. Perfect for dashboard aggregations where "as of last hour" is fine. One MySQL honesty point: MySQL has no native materialized views — teams emulate them with summary tables refreshed by jobs; PostgreSQL and Oracle support them natively. View = always fresh, pay per read; materialized = instant reads, pay in staleness.
CTEs — naming steps inside one query
A Common Table Expression is a temporary named result that lives for exactly one statement — SQL's version of naming your intermediate steps:
WITH dept_avg AS (
SELECT department, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT department, avg_salary
FROM dept_avg
WHERE avg_salary > 70000;Result
Engineering | 84000.00
Read it like a recipe: first compute department averages, then keep the big ones. Chain more steps with commas — WITH step1 AS (…), step2 AS (…) SELECT … — each step allowed to use the previous ones. This top-to-bottom readability is why analysts write almost everything with CTEs.
CTE vs subquery vs temp table vs view — pick your tool
| Tool | Lives for | Reach for it when |
|---|---|---|
| Subquery | one spot in one statement | quick one-off filter or value |
| CTE | one statement | named steps; needed more than once; recursion |
| Temp table | the session | heavy intermediate reused across several statements |
| View | permanent | many queries/users/tools share the same logic |
Recursive CTEs — SQL climbs the org chart
Back in lesson 2, a self join found each employee's direct manager — one level. But "everyone who reports to Asha, directly or indirectly" needs to walk the whole tree, however deep. A recursive CTE has exactly two parts:
WITH RECURSIVE subordinates AS (
-- 1. Anchor: runs once — the starting rows
SELECT employee_id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id = 1
UNION ALL
-- 2. Recursive member: joins back onto what's found so far
SELECT e.employee_id, e.name, e.manager_id, s.level + 1
FROM employees e
JOIN subordinates s ON e.manager_id = s.employee_id
)
SELECT * FROM subordinates ORDER BY level;The anchor seeds the result (Asha's direct reports, level 1); the recursive member runs again and again — each pass descending one level — until it produces no new rows. That's the built-in stopping rule; forget a terminating condition (say, a date series with no upper bound) and the engine aborts at its recursion limit (MySQL's cte_max_recursion_depth, default 1000). Hierarchies, category trees, date-series generation — one pattern rules them all, and you'll write two of them in the practice.
Common mistakes
- Thinking a view stores data — it stores a query (that's the materialized view's job).
- Trying to UPDATE through an aggregated/grouped view — the row mapping doesn't exist.
- Creating permanent views for one-off analyses — that's CTE territory; views are shared, long-lived interfaces.
- Forgetting the
RECURSIVEkeyword in MySQL/PostgreSQL recursive CTEs (SQL Server manages without it). - Writing a recursive member whose join can't exhaust — infinite recursion until the depth limit errors out.
Quick recap
| Idea | One-liner |
|---|---|
| View | a stored, named query — fresh every read; simplification + security |
| Updatable view | only when changes map to one row of one table |
| Materialized view | stored results — instant reads, stale until refreshed |
| CTE | WITH name AS (…) — named steps, one statement's lifetime |
| Recursive CTE | anchor + recursive member via UNION ALL; walks trees |
Practice Zone — PYQs from real selection rounds
What does a regular (non-materialized) view actually store?
Asked in


What's the core trade-off of a materialized view versus a regular view?
Asked in


Which of these views is NOT updatable (no INSERT/UPDATE/DELETE through it)?
Asked in


You need the same computed result twice within one query, readably. Best tool?
Asked in


A recursive CTE has two parts joined by UNION ALL. What are they, and what happens with no termination condition?
Asked in


Using the employees table, create a view called high_earners showing the employee_id, name and salary of everyone earning more than 70000. Then select all rows from the view.
Asked in


employees5 rows
| employee_id | name | department | salary |
|---|---|---|---|
| 1 | Asha Rao | Engineering | 85000.00 |
| 2 | Ravi Kumar | Sales | 55000.00 |
| 3 | Meera Iyer | Engineering | 72000.00 |
| 4 | Kabir Singh | Marketing | 60000.00 |
| 5 | Divya Menon | Engineering | 95000.00 |
Using a CTE, write a query that first computes each department's average salary from the employees table, and then returns only the departments whose average exceeds 70000.
Asked in


employees5 rows
| employee_id | name | department | salary |
|---|---|---|---|
| 1 | Asha Rao | Engineering | 85000.00 |
| 2 | Ravi Kumar | Sales | 55000.00 |
| 3 | Meera Iyer | Engineering | 72000.00 |
| 4 | Kabir Singh | Marketing | 60000.00 |
| 5 | Divya Menon | Engineering | 95000.00 |
The employees table has employee_id and manager_id (NULL for the top manager). Write a recursive CTE that lists every employee who reports — directly or indirectly — to employee_id = 1, along with their depth level in the hierarchy.
Asked in


employees7 rows
| employee_id | name | manager_id |
|---|---|---|
| 1 | Asha Rao | NULL |
| 2 | Ravi Kumar | 1 |
| 3 | Meera Iyer | 1 |
| 4 | Kabir Singh | 2 |
| 5 | Divya Menon | 2 |
| 6 | Nikhil Das | 3 |
| 7 | Sara Thomas | 4 |
FAQ
Are CTEs faster than subqueries?
Usually the same plan — modern optimizers treat a CTE like an inlined subquery in most cases. Choose CTEs for readability and reuse, not speed. The one structural exception: recursion, which only CTEs can express.
Does querying a view hit the base tables every time?
For a regular view, yes — it's macro-expansion: the view's query merges into yours and runs against live data. That's why indexes on the base tables are what make views fast.
Can a CTE be used in UPDATE or DELETE?
Yes — WITH ... UPDATE/DELETE works in MySQL 8+ and PostgreSQL, and it's a clean way to identify complex row sets (say, duplicates ranked by ROW_NUMBER) before modifying them.
Final lesson: the four families every SQL command belongs to — Lesson 12: DDL, DML, DCL & TCL →


