Docs
Common Table Expressions (CTEs)

Common Table Expressions (CTEs)

Differences Between Quantitative and Qualitative Data

Common Table Expressions (CTEs) are one of the best tools in SQL for writing readable, maintainable, and reusable queries. If you've ever stared at a long, nested SQL statement and thought, "there has to be a better way"—CTEs are it.

Let’s walk through how to use CTEs step by step, with real-world use cases like SELECT, INSERT, UPDATE, and integrating with stored procedures, functions, triggers, and views.


What is a CTE?

A Common Table Expression is a temporary result set, defined by a WITH clause, that you can reference in a subsequent query. Think of it like defining a named subquery at the top of your SQL—clean, readable, and reusable.

WITH cte_name AS (
  -- some SELECT statement
)
SELECT * FROM cte_name;

Why Use a CTE?

  • ✅ Improves readability: Break down complex queries into logical steps.
  • ✅ Encourages reuse: Use the CTE name instead of repeating logic.
  • ✅ Makes debugging easier: Isolate logic during development.
  • ✅ Enables modular queries: Like functions in programming.

Using CTEs the Right Way

Let’s walk through the process of writing a CTE in three clear steps.


✅ Create the Raw Table Expression

The CTE itself is always a SELECT statement. Here's a simple example that filters a raw table:

WITH active_customers AS (
  SELECT id, name, email
  FROM customers
  WHERE status = 'active'
)

This active_customers CTE gives us a clean alias for later use.


✅ Select from the CTE

Once the CTE is defined, you can use it just like a table:

SELECT *
FROM active_customers
ORDER BY name;

This is much more readable than repeating the SELECT and WHERE logic inline.


✅ Chain CTEs for Complex Logic

You can define multiple CTEs in one WITH clause and build up your logic step by step:

WITH active_customers AS (
  SELECT id, name FROM customers WHERE status = 'active'
),
recent_orders AS (
  SELECT customer_id, order_date
  FROM orders
  WHERE order_date > CURRENT_DATE - INTERVAL '30 days'
)
SELECT ac.name, ro.order_date
FROM active_customers ac
JOIN recent_orders ro ON ac.id = ro.customer_id;

This approach is easy to read and maintain, especially compared to deeply nested subqueries.


CTEs with INSERT, UPDATE, and DELETE

CTEs aren’t just for reading data—you can use them with INSERT, UPDATE, and DELETE too.


Insert with a CTE

WITH new_users AS (
  SELECT id, email FROM staging_users WHERE imported = true
)
INSERT INTO users (id, email)
SELECT id, email FROM new_users;

Update with a CTE

WITH flagged_orders AS (
  SELECT id FROM orders WHERE total > 1000
)
UPDATE orders
SET priority = 'high'
WHERE id IN (SELECT id FROM flagged_orders);

Delete with a CTE

WITH expired_sessions AS (
  SELECT session_id FROM sessions WHERE last_active < CURRENT_DATE - INTERVAL '90 days'
)
DELETE FROM sessions
WHERE session_id IN (SELECT session_id FROM expired_sessions);

Bonus: Use CTEs in Views, Functions, and More

CTEs are supported inside many SQL structures, which helps keep everything modular.


CTEs in Views

CREATE VIEW high_value_customers AS
WITH cte AS (
  SELECT id, name, total_spent FROM customers WHERE total_spent > 5000
)
SELECT * FROM cte;

This way, the logic lives in one place and can be reused by anyone querying the view.


CTEs in Functions or Stored Procedures

CREATE OR REPLACE FUNCTION get_recent_orders()
RETURNS TABLE(customer_id INT, order_date DATE) AS $$
BEGIN
  RETURN QUERY
  WITH recent_orders AS (
    SELECT customer_id, order_date
    FROM orders
    WHERE order_date > CURRENT_DATE - INTERVAL '7 days'
  )
  SELECT * FROM recent_orders;
END;
$$ LANGUAGE plpgsql;

CTEs in Triggers

Within a trigger function, CTEs can be used to make logic more readable:

CREATE OR REPLACE FUNCTION log_order_update()
RETURNS TRIGGER AS $$
BEGIN
  WITH updated_order AS (
    SELECT NEW.id AS order_id, NEW.status AS new_status
  )
  INSERT INTO order_audit (order_id, change)
  SELECT order_id, new_status FROM updated_order;
 
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Happy querying!