Snowflake Subqueries and CTEs
Complex questions break into smaller questions. Subqueries and common table expressions, called CTEs, let SQL follow the same path. Each small step gets a name, and the final query reads like a clear recipe rather than a tangled knot.
The Recipe Card Comparison
A cooking recipe lists steps: chop vegetables, make the sauce, cook the pasta, combine everything. Nobody tries to do every task in a single move. A CTE works like a numbered step on that card. Each step produces something small, and the last step combines the results.
STEP BY STEP WITH CTEs
======================
WITH monthly_sales AS (...) step 1: total per month
, best_months AS (...) step 2: keep months above average
SELECT ... FROM best_months; final: show the answer
monthly_sales ---> best_months ---> final resultSubqueries
A subquery is a query inside another query. It can appear in the WHERE clause, the FROM clause, or the SELECT list.
-- Scalar subquery: returns one value
SELECT order_id, amount
FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);
-- IN subquery: returns a list
SELECT name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders WHERE amount > 500);
-- EXISTS subquery: checks for any match
SELECT c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id);
-- Correlated subquery: refers to the outer row
SELECT o.order_id, o.amount
FROM orders o
WHERE o.amount > (SELECT AVG(amount) FROM orders i
WHERE i.customer_id = o.customer_id);The correlated version recalculates the inner average for every customer, which answers the question "which orders beat that customer's own average?"
Common Table Expressions
The WITH keyword defines named results before the main query. The same query written with CTEs reads from top to bottom.
WITH monthly_sales AS (
SELECT DATE_TRUNC('month', order_date) AS month,
SUM(amount) AS revenue
FROM orders
GROUP BY 1
),
average_month AS (
SELECT AVG(revenue) AS avg_revenue FROM monthly_sales
)
SELECT m.month, m.revenue
FROM monthly_sales m
CROSS JOIN average_month a
WHERE m.revenue > a.avg_revenue
ORDER BY m.month;The first CTE feeds the second, and the main query uses both. Each block can run alone in a worksheet for easy testing.
Subquery or CTE?
SITUATION BEST CHOICE
-------------------------------------- -------------
One quick filter value Subquery
Same result used twice in one query CTE
Several steps that build on each other CTE
Existence check EXISTS subquery
Walking a hierarchy Recursive CTERecursive CTEs for Hierarchies
Organization charts, folder trees, and parts lists have unknown depth. A recursive CTE starts with a base row set and repeatedly adds one more level until nothing new appears.
WITH RECURSIVE org AS (
-- anchor: the top manager
SELECT emp_id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- repeat: everyone who reports to someone already found
SELECT e.emp_id, e.name, e.manager_id, o.level + 1
FROM employees e
JOIN org o ON e.manager_id = o.emp_id
)
SELECT level, name FROM org ORDER BY level, name;
RECURSION WALK
==============
Level 1: Meena (CEO)
Level 2: Arun, Divya report to Meena
Level 3: Kabir, Sana, Tarun report to Arun or DivyaSnowflake supports the CONNECT BY clause for the same purpose. Both styles suit trees and graphs.
Performance Habits
- Snowflake optimizes CTEs like subqueries, so naming a step costs nothing extra.
- Filter early inside each CTE to shrink the data before joins.
- Avoid correlated subqueries on huge tables when a join gives the same answer.
- Select only needed columns inside every step.
A Practical Scenario
A marketing analyst wants customers whose latest purchase exceeded their personal average. The analyst builds one CTE for per-customer averages, a second CTE for each customer's latest order, and a final join that compares the two. Each block gets tested alone. A colleague reads the query a month later and understands it in a minute.
Frequently Asked Questions
Does a CTE store its result?
A CTE is a name for a query, and Snowflake decides at run time how to execute it. Define a temporary table when a result needs to persist across several statements.
When does a correlated subquery slow a query?
A correlated subquery may repeat work for every outer row. Rewrite it as a join with an aggregate when the outer table holds millions of rows, and compare the two versions in Query Profile.
Can I nest CTEs inside CTEs?
A CTE may contain its own WITH clause, though flat lists of CTEs read more clearly. Keep each block short and give it a descriptive name such as monthly_sales or latest_orders.
Key Points
- Subqueries nest a query inside WHERE, FROM, or SELECT.
- CTEs name each step with WITH and read from top to bottom.
- EXISTS checks for matches efficiently.
- Recursive CTEs walk hierarchies of unknown depth.
- Small named steps make long queries easier to test and maintain.
