CTEs in PostgreSQL
Definition
PostgreSQL CTEs support recursive queries, writeable CTE operations (INSERT/UPDATE/DELETE), and explicit MATERIALIZED/NOT MATERIALIZED hints for performance control.
Introduction to CTEs in PostgreSQL
PostgreSQL's CTE implementation is among the most feature-rich available. Unique to PostgreSQL are writeable CTEs that let you chain data-modifying statements, and the MATERIALIZED/NOT MATERIALIZED keywords added in PostgreSQL 12 for precise control over CTE execution.
Syntax
-- Writeable CTE (PostgreSQL-specific) WITH moved_rows AS ( DELETE FROM orders_old WHERE status = 'archived' RETURNING * ) INSERT INTO orders_archive SELECT * FROM moved_rows; -- Force materialization WITH data AS MATERIALIZED ( SELECT ... expensive query ... ) SELECT * FROM data WHERE ... UNION SELECT * FROM data WHERE ...;
Examples
Writeable CTE — Move Records
-- Atomically move old orders to archive table WITH archived AS ( DELETE FROM orders WHERE created_at < NOW() - INTERVAL '2 years' RETURNING * ) INSERT INTO orders_archive SELECT * FROM archived; SELECT 'Archived: ' || COUNT(*) FROM archived;
Writeable CTEs allow DELETE + INSERT in a single atomic statement. The RETURNING clause captures deleted rows for the INSERT.
Try in PlaygroundCommon Mistakes
Pre-PostgreSQL 12: CTEs were always materialized (could not be optimized as inline subqueries)
Forgetting CYCLE clause in recursive CTEs traversing cyclic graphs — causes infinite loops
Assuming writeable CTE side effects are visible within the same CTE — they are not
Frequently Asked Questions
Are PostgreSQL CTEs materialized?
What are writeable CTEs in PostgreSQL?
Related SQL Topics
Practice This in the SQL Playground
Write real queries, see live results, and master CTEs in PostgreSQL hands-on. 100% free.