Situation: You have the same CTE logic repeated multiple times in a query — e.g. compute filtered events or an aggregated customer snapshot — and want to rewrite it once and reference it multiple times.Refactor (example):-- before: repeated identical CTEs A1, A2...sql
WITH recent_orders AS (
SELECT user_id, SUM(amount) total, COUNT(*) cnt
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY user_id
),
user_metrics AS (
SELECT u.id, u.signup_date, r.total
FROM users u
LEFT JOIN recent_orders r ON r.user_id = u.id
),
-- then repeated: another CTE reusing same recent_orders logic
recent_orders_2 AS (
SELECT user_id, SUM(amount) total, COUNT(*) cnt
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY user_id
)
SELECT ...
-- after: single CTE referenced multiple timessql
WITH recent_orders AS (
SELECT user_id, SUM(amount) total, COUNT(*) cnt
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY user_id
),
user_metrics AS (
SELECT u.id, u.signup_date, r.total
FROM users u
LEFT JOIN recent_orders r ON r.user_id = u.id
),
churn_candidates AS (
SELECT u.id
FROM users u
LEFT JOIN recent_orders r ON r.user_id = u.id
WHERE COALESCE(r.cnt,0) = 0
)
SELECT um.*, (CASE WHEN c.id IS NOT NULL THEN 1 ELSE 0 END) as is_churn
FROM user_metrics um
LEFT JOIN churn_candidates c ON c.id = um.id;
Will the CTE be recomputed each reference?- Many engines treat WITH-CTE as syntactic inlineable subqueries and will re-optimize/inline them; this can lead to recomputation for each reference. Examples: - PostgreSQL historically treated CTEs as optimization fences (materialized) until v12; from v12 it inlines CTEs by default unless MATERIALIZED is specified. - Most commercial engines (BigQuery, Redshift, Snowflake, SQL Server) typically inline CTEs and may recompute them per reference unless the optimizer decides otherwise.- Behavior is engine-specific and depends on optimizer complexity and query plan.How to enforce reuse (materialization) if needed:1. Use engine-supported MATERIALIZED hint: - PostgreSQL 12+: WITH recent_orders AS MATERIALIZED ( ... ) forces materialization.2. Create a temporary or staging table:sql
CREATE TEMP TABLE tmp_recent_orders AS
SELECT ... ;
-- use tmp_recent_orders in downstream queries
3. Use a persistent/materialized view if reuse across sessions is desired:sql
CREATE MATERIALIZED VIEW mv_recent_orders AS SELECT ...;
REFRESH MATERIALIZED VIEW mv_recent_orders;
4. In some engines use query hints or result caching (engine-specific).Trade-offs and guidance:- Materializing avoids repeated computation and can dramatically reduce runtime for expensive aggregations, but increases IO, storage, and may serve stale data unless refreshed.- Prefer single CTE for readability and let the optimizer decide; if profiling shows recomputation, enforce materialization via MATERIALIZED, temp tables, or materialized views.- Always validate with EXPLAIN/EXPLAIN ANALYZE and measure before/after.This approach reduces duplication, clarifies intent for downstream reporting (dashboards), and gives options to enforce reuse when performance profiling requires it.