🗄️SQL Templates

Common Table Expressions (WITH)

Use WITH clause for readable, reusable temporary result sets

Explanation

CTEs create named temporary result sets that improve query readability and maintainability.

Examples

Simple CTE
Input
WITH active_users AS (SELECT ...)
Output
Named temporary result set
Multiple CTEs
Input
WITH cte1 AS (...), cte2 AS (...)
Output
Chain multiple temporary sets

Code Examples

SQL
-- Simple CTE
WITH active_users AS (
  SELECT * 
  FROM users 
  WHERE status = 'active'
)
SELECT * 
FROM active_users 
WHERE country = 'US';

-- Multiple CTEs
WITH 
  active_users AS (
    SELECT * 
    FROM users 
    WHERE status = 'active'
  ),
  recent_orders AS (
    SELECT * 
    FROM orders 
    WHERE created_at > NOW() - INTERVAL '30 days'
  )
SELECT 
  u.name,
  COUNT(o.id) AS order_count
FROM active_users u
LEFT JOIN recent_orders o ON u.id = o.user_id
GROUP BY u.id, u.name;

-- CTE with aggregation
WITH user_stats AS (
  SELECT 
    user_id,
    COUNT(*) AS order_count,
    SUM(total) AS total_spent
  FROM orders
  GROUP BY user_id
)
SELECT 
  u.name,
  us.order_count,
  us.total_spent
FROM users u
JOIN user_stats us ON u.id = us.user_id
WHERE us.total_spent > 1000;

-- Recursive CTE (organizational hierarchy)
WITH RECURSIVE org_chart AS (
  -- Base case: top-level employees
  SELECT 
    id,
    name,
    manager_id,
    1 AS level
  FROM employees
  WHERE manager_id IS NULL
  
  UNION ALL
  
  -- Recursive case: employees under managers
  SELECT 
    e.id,
    e.name,
    e.manager_id,
    oc.level + 1
  FROM employees e
  INNER JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart ORDER BY level, name;

-- Recursive CTE (category tree)
WITH RECURSIVE category_tree AS (
  SELECT 
    id,
    name,
    parent_id,
    CAST(name AS VARCHAR(1000)) AS path
  FROM categories
  WHERE parent_id IS NULL
  
  UNION ALL
  
  SELECT 
    c.id,
    c.name,
    c.parent_id,
    CONCAT(ct.path, ' > ', c.name)
  FROM categories c
  INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree;

-- CTE for date series
WITH RECURSIVE dates AS (
  SELECT CAST('2024-01-01' AS DATE) AS date
  UNION ALL
  SELECT date + INTERVAL '1 day'
  FROM dates
  WHERE date < '2024-12-31'
)
SELECT 
  d.date,
  COUNT(o.id) AS orders
FROM dates d
LEFT JOIN orders o ON DATE(o.created_at) = d.date
GROUP BY d.date;

-- Multiple CTEs with references
WITH 
  monthly_revenue AS (
    SELECT 
      DATE_TRUNC('month', created_at) AS month,
      SUM(total) AS revenue
    FROM orders
    GROUP BY month
  ),
  revenue_growth AS (
    SELECT 
      month,
      revenue,
      LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
      revenue - LAG(revenue) OVER (ORDER BY month) AS growth
    FROM monthly_revenue
  )
SELECT 
  month,
  revenue,
  prev_month_revenue,
  growth,
  ROUND(growth / prev_month_revenue * 100, 2) AS growth_percentage
FROM revenue_growth;

💡 Tips

  • CTEs improve readability vs nested subqueries
  • Can reference earlier CTEs in later ones
  • Recursive CTEs need base case + recursive case
  • Add RECURSIVE keyword for recursive CTEs
  • Use for complex queries with multiple steps
  • CTEs are materialized once per query
  • Better for maintenance than inline subqueries

⚠️ Common Pitfalls

  • CTEs may not be optimized as well as views
  • Recursive CTEs need termination condition
  • Infinite recursion causes errors
  • Some DBs don't support CTEs (older MySQL)
  • Can't create indexes on CTEs
  • Performance may vary vs subqueries