Web Development

What Are CTEs and How Should I Format Them?

A comprehensive guide to understanding Common Table Expressions (CTEs) in SQL, their benefits, and best practices for formatting them for maximum readability and maintainability.

By Inventive HQ Team

Understanding Common Table Expressions in SQL

A Common Table Expression (CTE) is a named, temporary result set defined with the WITH keyword that exists only for the duration of a single SQL statement. You define it once, give it a readable name, and reference that name in the main query like an inline, disposable view. CTEs were standardized in SQL:1999 and are supported by PostgreSQL, SQL Server, Oracle, SQLite (3.8.3+), and MySQL 8.0+. They do not make queries faster on their own — their job is to turn a deeply nested, read-inside-out query into a top-to-bottom sequence of named steps.

That is the definition an AI overview will hand you. What a summary can't show you is shape — where the WITH goes, how the commas chain multiple CTEs, and how the reading order flips versus a nested subquery. The diagrams, the side-by-side comparison table, and the formatting rules below are the parts you actually have to see to get right.

Anatomy of a Common Table Expression A labeled breakdown of a CTE showing the WITH keyword, the CTE name, the AS clause, the CTE body in parentheses, and the main query that references the CTE by name. Anatomy of a CTE WITH recent_sales AS ( SELECT product_id, SUM(qty) AS sold FROM sales GROUP BY product_id ) SELECT * FROM recent_sales WHERE sold > 100; main query references the CTE by name

WITH · name · AS · (body) · main query

Introduced in the SQL:1999 standard and widely supported across modern database systems including PostgreSQL, SQL Server, MySQL, Oracle, and others, CTEs have become an essential tool for SQL developers. They provide a way to break down complex queries into logical, manageable pieces, much like how functions work in traditional programming languages.

A CTE is defined using the WITH clause followed by a query name, optional column list, and the query itself. Once defined, you can reference the CTE by name elsewhere in your main query, just as you would reference a regular table or view. The key difference is that CTEs exist only for the duration of the query—they're not stored in the database and disappear once the query completes.

The Basic Structure and Syntax of CTEs

The fundamental syntax of a CTE begins with the WITH keyword, followed by the CTE name, an optional column list in parentheses, the AS keyword, and finally the CTE query enclosed in parentheses. After defining one or more CTEs, you write your main query that references them.

A simple CTE might look like this: WITH sales_summary AS (SELECT product_id, SUM(quantity) as total_sold FROM sales GROUP BY product_id) SELECT * FROM sales_summary WHERE total_sold > 100. This example creates a CTE named sales_summary that calculates total quantities sold per product, then uses it in the main query to find products with high sales volumes.

CTEs can also be chained together, with subsequent CTEs referencing earlier ones. This allows you to build complex logic step by step, with each CTE representing a logical stage in your data transformation. You can define multiple CTEs in a single WITH clause by separating them with commas, creating a sequence of temporary result sets that work together to produce your final output.

The optional column list allows you to rename columns or specify explicit column names for your CTE, which can be particularly useful when the CTE query returns computed columns without natural names. However, if the CTE query's select list already provides suitable column names, you can omit this column list.

Why Use CTEs: Benefits and Advantages

CTEs offer several compelling advantages over alternative approaches like subqueries or temporary tables. The most immediate benefit is improved readability. By breaking complex queries into named, logical components, CTEs make your SQL code self-documenting. Instead of nested subqueries that force readers to work inside-out to understand the logic, CTEs present a top-down, linear flow that's much easier to follow.

Maintainability is another major advantage. When you need to modify a complex query, having logic organized into discrete CTEs makes it easier to locate and update specific parts without affecting the rest of the query. This modular approach reduces the risk of introducing bugs during maintenance and makes code reviews more efficient.

CTEs also eliminate code duplication. If you need to reference the same subquery multiple times in a complex query, traditional subqueries would require writing out that logic repeatedly. With CTEs, you define the logic once and reference it by name as many times as needed. This follows the DRY (Don't Repeat Yourself) principle and ensures consistency across all references.

For recursive operations—like hierarchical data traversal, organizational charts, or bill-of-materials queries—CTEs provide capabilities that are difficult or impossible to achieve with standard SQL. Recursive CTEs can reference themselves, allowing you to write queries that walk tree structures or perform iterative calculations.

Debugging becomes significantly easier with CTEs. You can test each CTE independently by temporarily making it the final SELECT statement, verifying its output before incorporating it into the larger query. This incremental testing approach helps isolate issues quickly.

Formatting Best Practices for Readability

Proper formatting is crucial for maintaining CTE readability as queries grow in complexity. The most fundamental rule is consistent indentation. Each level of nesting should be indented by a fixed number of spaces (typically two to four), making the query's structure visually apparent at a glance.

When formatting the WITH clause, start it on its own line, aligned with your main SELECT statement. Each CTE definition should begin on a new line, with the CTE name at the same indentation level. The AS keyword and opening parenthesis of the CTE query can appear on the same line as the CTE name for simple CTEs, or on a new line for complex ones.

Within each CTE's query definition, format the SQL using standard formatting practices: SELECT keyword on its own line, each selected column on its own line (for non-trivial selects), FROM, WHERE, JOIN, and GROUP BY clauses on separate lines, and proper indentation for any subcomponents. This makes each CTE's logic clear and easy to verify.

When defining multiple CTEs, separate them with commas and ensure each subsequent CTE starts on a new line. Some developers prefer to place the comma at the beginning of the line rather than the end, making it easier to comment out or rearrange CTEs during development. Either style works as long as you're consistent.

For the main query that follows your CTE definitions, maintain the same indentation level as the WITH clause. This visual alignment helps readers distinguish between the CTE definitions and the final query that uses them.

Here is a chained, multi-CTE query using the leading-comma convention — the layout most teams settle on once queries get long:

WITH monthly_sales AS (
    SELECT
        product_id,
        date_trunc('month', sold_at) AS month,
        SUM(quantity)               AS units
    FROM sales
    GROUP BY product_id, date_trunc('month', sold_at)
)

, ranked AS (
    SELECT
        product_id,
        month,
        units,
        RANK() OVER (PARTITION BY month ORDER BY units DESC) AS rnk
    FROM monthly_sales
)

SELECT product_id, month, units
FROM ranked
WHERE rnk <= 3
ORDER BY month, rnk;

Notice the leading comma before ranked: you can comment out or reorder any CTE below the first without touching the previous line's trailing punctuation. The blank line between CTEs, the aligned AS aliases, and one clause per line are what keep a four-stage query scannable. If you inherit a query that ignores all of this, paste it into the SQL formatter to normalize indentation before you try to read it.

Naming Conventions and Documentation

Choose descriptive, meaningful names for your CTEs that clearly indicate what data they contain or what transformation they perform. Good CTE names act as inline documentation, making your query self-explanatory. Use names like customer_orders, monthly_sales_totals, or active_subscriptions rather than generic names like cte1, temp, or result.

Follow your organization's or project's naming conventions for table and view names when naming CTEs. This consistency makes CTEs feel like natural parts of your database schema, even though they're temporary. Common conventions include using lowercase with underscores (snake_case), avoiding abbreviations unless they're widely understood, and keeping names concise but descriptive.

For complex CTEs or those with non-obvious logic, add comments explaining what the CTE does and why. SQL comments can appear before the CTE definition or inline within the query itself. These comments are invaluable for future maintainers trying to understand the query's purpose and logic.

Consider using prefixes or suffixes to categorize CTEs by their role in the query. For example, you might prefix all CTEs that perform aggregations with "agg_" or suffix CTEs that filter data with "_filtered". This categorization helps readers quickly understand each CTE's purpose within the larger query structure.

Advertisement

Common Formatting Patterns and Examples

For simple CTEs that fit comfortably on a few lines, a compact format often works well: WITH product_sales AS (SELECT product_id, SUM(amount) as total FROM sales GROUP BY product_id). The key elements are on separate lines but the overall structure is tight and easy to scan.

For complex CTEs with multiple joins, conditions, and columns, use expanded formatting with careful indentation. Start each major clause (SELECT, FROM, JOIN, WHERE, GROUP BY) on its own line, indent subordinate elements, and use line breaks to separate logical groupings of columns or conditions. This makes the structure immediately apparent.

When chaining multiple CTEs together, consider adding blank lines between CTE definitions to create visual separation. This is especially helpful when you have three or more CTEs in sequence, as it prevents them from visually blurring together.

For recursive CTEs, which have a special structure with an anchor member and a recursive member separated by UNION or UNION ALL, use formatting that clearly distinguishes these two parts. The anchor query (the base case) and the recursive query (the recursive case) should be visually distinct, often with extra indentation for the recursive portion.

Performance Considerations and Query Optimization

While CTEs excel at improving code readability and maintainability, it's important to understand their performance characteristics. In most database systems, CTEs are executed once per reference in your query. If you reference a CTE three times, the database might execute that CTE query three times, unlike a temporary table which would be computed once and stored.

Some database systems (like PostgreSQL 12 and later) can "inline" simple CTEs, treating them like subqueries and potentially optimizing them more aggressively. However, this optimization isn't guaranteed and varies by database system and query complexity.

For queries where performance is critical and you're referencing a CTE many times, consider whether a temporary table might be more appropriate. Temporary tables are materialized—computed once and stored temporarily—which can be more efficient for complex CTEs referenced multiple times. However, this comes at the cost of the readability and convenience that CTEs provide.

Use EXPLAIN or EXPLAIN ANALYZE (or your database's equivalent) to understand how your CTEs are being executed and where optimization opportunities exist. The query execution plan will show whether CTEs are being scanned multiple times and where the performance bottlenecks are.

When to Use CTEs vs. Alternatives

The four tools people reach for — CTE, subquery, temporary table, and view — overlap, but each has a lane. This table is the decision most engineers are actually trying to make:

CTESubqueryTemp tableView
LifetimeOne statementOne statementWhole sessionPermanent DB object
Materialized (written once)?Usually noNoYesNo
Can be indexed?NoNoYesNo (indexes the base tables)
Reusable across queries?NoNoYes (in session)Yes (everywhere)
Recursion supported?YesNoNoNo
Reads top-to-bottom?YesNo (inside-out)YesYes
Best readability for complex logicExcellentPoor when nestedGoodGood
Which should I use / whenBreak one complex query into named steps, dedupe a repeated subquery, or walk a hierarchyA one-off filter or a correlated lookup that isn't reusedYou reference the same heavy result set many times or need an index on itLogic reused by many queries or apps, or an access-control layer

The most common mistake is reaching for a temp table when a CTE would do (adds session state and I/O for nothing) or expecting a CTE to speed things up when it's really just renaming a subquery. Below is why the readability win is real — the reading order literally flips.

CTE reading order versus nested subquery reading order A nested subquery must be read from the innermost query outward, while a CTE chain reads top to bottom in the order the logic runs. Nested subquery — read inside-out SELECT ... FROM ( step 3 SELECT ... FROM ( step 2 SELECT ... step 1 eyes jump inward first CTE chain — read top-to-bottom WITH step1 AS (...), step 1 step2 AS (...) step 2 SELECT * FROM step2 step 3 eyes flow straight down

CTEs are ideal when you need to improve query readability, eliminate code duplication by referencing the same result set multiple times, break down complex logic into manageable steps, or implement recursive queries. They shine in scenarios where the primary concern is code maintainability and the performance overhead of CTEs is acceptable.

Temporary tables are better suited for situations where you're processing large volumes of data and need to reference the result set many times, where you need to index the intermediate results for better query performance, or where you want to inspect intermediate results during debugging. Temporary tables persist for the duration of the session and can be indexed and analyzed, providing optimization opportunities that CTEs don't offer.

Views are appropriate when you need to encapsulate and reuse query logic across multiple queries or applications, when you want to provide a simplified or restricted interface to underlying tables, or when you need to implement security by controlling what data users can access. Views are stored database objects, unlike CTEs which exist only for a single query.

Subqueries remain useful for simple, one-time transformations that don't need to be referenced multiple times and for correlated subqueries where the subquery depends on the outer query. However, for complex nested subqueries or logic that's referenced multiple times, CTEs usually provide better readability.

Advanced CTE Techniques

Recursive CTEs unlock powerful capabilities for hierarchical data. They consist of two parts: an anchor member that provides the base case, and a recursive member that references the CTE itself to build on previous results. The recursion continues until the recursive member returns no rows, at which point all results are combined with UNION or UNION ALL.

Common use cases for recursive CTEs include organizational hierarchy traversal (finding all employees under a manager), bill of materials expansions (finding all components in a product), graph traversal (finding all connected nodes), and generating number sequences or date ranges.

You can combine CTEs with other SQL features like window functions, set operations, and complex joins to create sophisticated data transformations. CTEs can contain almost any valid SELECT statement, giving you tremendous flexibility in how you structure your queries.

Multiple CTE chains allow you to build data transformations in stages, with each CTE refining or combining results from previous ones. This pipeline approach mirrors functional programming concepts and creates queries that are easy to understand and modify.

Debugging and Testing CTEs

One of CTEs' greatest strengths for development is how easy they make debugging. To test a specific CTE, temporarily change your final query to select from just that CTE. This lets you inspect the CTE's output in isolation, verifying its logic before incorporating it into the larger query.

Add diagnostic columns to CTEs during development to help understand what the query is doing. These might include row counts, flags indicating why a row was included, or intermediate calculation steps. You can remove these diagnostic columns once the query is working correctly.

Use incremental development when building complex queries with multiple CTEs. Start with the first CTE, test it, then add the second CTE and test their combination, and so on. This step-by-step approach makes it easy to identify exactly where problems arise.

Conclusion

Common Table Expressions are an indispensable tool for modern SQL development, providing a way to write complex queries that remain readable, maintainable, and testable. By following proper formatting conventions—consistent indentation, descriptive naming, logical grouping, and clear visual structure—you create SQL code that communicates its intent effectively and stands the test of time. While CTEs aren't always the right tool for every situation, understanding when and how to use them effectively will make you a more capable SQL developer. The investment in learning to format and structure CTEs well pays dividends in code quality, maintenance efficiency, and the ability to tackle increasingly complex data problems with confidence. Whether you're working with simple aggregations or complex recursive hierarchies, CTEs provide the abstraction and organization needed to keep your SQL queries manageable and professional.

Frequently Asked Questions

What is a CTE in SQL?

A CTE (Common Table Expression) is a named, temporary result set defined with the WITH keyword that exists only for the duration of a single query. You define it once and reference it by name in the main query, like a disposable inline view. It was standardized in SQL:1999 and is supported by PostgreSQL, SQL Server, MySQL 8+, Oracle, SQLite 3.8.3+, and others.

How do you format a CTE for readability?

Put WITH on its own line, indent each column in the CTE body, place each major clause (SELECT, FROM, JOIN, WHERE, GROUP BY) on its own line, and add a blank line between chained CTEs. When you have multiple CTEs, many developers use a leading comma before each new CTE name so a definition can be commented out or reordered without breaking the query.

Are CTEs faster than subqueries?

Usually no. A CTE and an equivalent subquery typically produce the same execution plan, so performance is comparable. The exception is materialized CTEs: in PostgreSQL before version 12, and whenever you use WITH ... AS MATERIALIZED, the CTE is computed once into a temporary result, which can be faster if referenced many times but slower if it blocks predicate pushdown. CTEs are a readability tool, not an optimization.

What is the difference between a CTE and a temporary table?

A CTE exists only for one statement and is never written to disk or the catalog; a temporary table is materialized, persists for the whole session, can be indexed, and can be referenced by many separate queries. Use a CTE for readability within one query; switch to a temp table when you reference the same heavy result set many times or need an index on the intermediate data.

Can a CTE reference itself?

Yes. A recursive CTE (WITH RECURSIVE in most databases) has an anchor member that provides the base rows and a recursive member that references the CTE by name, joined with UNION ALL. Recursion stops when the recursive member returns no new rows. Recursive CTEs are the standard way to walk hierarchies, org charts, bills of materials, and graphs.

Should the comma go before or after each CTE?

Either works, but leading commas (comma at the start of each new CTE line) are popular because you can comment out or reorder any CTE except the first without touching neighboring lines. The important rule is to pick one style and apply it consistently across the codebase.

Do all databases support CTEs?

Most modern ones do. PostgreSQL, SQL Server, Oracle, and SQLite have supported them for years. MySQL added CTE support in version 8.0 (2018); MySQL 5.7 and earlier cannot use them. Recursive CTE syntax and materialization behavior vary slightly between engines, so check your specific version.

Can you use multiple CTEs in one query?

Yes. Write WITH once, then separate each CTE definition with a comma. Later CTEs can reference earlier ones, letting you build a data-transformation pipeline stage by stage. Only the final CTE needs to be consumed by the main SELECT, but each intermediate CTE must be referenced somewhere or it is unused.

SQLdatabasequery optimizationcode formatting