Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
SQL

SQL: Use CTEs (WITH Clause) for Readable Complex Queries

- 22.03.26 - ErcanOPAK

📝 Name Your Subqueries

Nested subqueries everywhere? Unreadable mess? CTEs (Common Table Expressions) name intermediate results. Code becomes self-documenting.

The Subquery Nightmare

-- ❌ Nested subqueries: Hard to read
SELECT 
    u.name,
    (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count,
    (SELECT AVG(total) FROM orders o WHERE o.user_id = u.id) as avg_order
FROM users u
WHERE u.id IN (
    SELECT user_id 
    FROM orders 
    WHERE created_at > '2024-01-01'
    GROUP BY user_id
    HAVING COUNT(*) > 5
)
AND u.country IN (
    SELECT country 
    FROM countries 
    WHERE region = 'EU'
);

-- What does this do? Hard to understand at a glance

CTE Solution: Clear and Readable

-- ✅ WITH clause: Self-documenting
WITH 
    active_users AS (
        SELECT user_id
        FROM orders
        WHERE created_at > '2024-01-01'
        GROUP BY user_id
        HAVING COUNT(*) > 5
    ),
    eu_countries AS (
        SELECT country
        FROM countries
        WHERE region = 'EU'
    ),
    user_stats AS (
        SELECT 
            user_id,
            COUNT(*) as order_count,
            AVG(total) as avg_order
        FROM orders
        GROUP BY user_id
    )
SELECT 
    u.name,
    s.order_count,
    s.avg_order
FROM users u
INNER JOIN active_users a ON u.id = a.user_id
INNER JOIN eu_countries e ON u.country = e.country
LEFT JOIN user_stats s ON u.id = s.user_id;

-- Clear logic: Filter active users, EU countries, join stats

🎯 Basic CTE Syntax

WITH cte_name AS (
    SELECT column1, column2
    FROM table
    WHERE condition
)
SELECT *
FROM cte_name;

-- Multiple CTEs
WITH 
    cte1 AS (
        SELECT ...
    ),
    cte2 AS (
        SELECT ...
        FROM cte1  -- Can reference previous CTE
    )
SELECT *
FROM cte2;

Recursive CTE (Hierarchies)

-- Organization chart: Manager → Employee hierarchy
WITH RECURSIVE org_chart AS (
    -- Base case: Top-level managers
    SELECT 
        id,
        name,
        manager_id,
        1 as level,
        CAST(name AS VARCHAR(1000)) as path
    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,
        CAST(oc.path || ' -> ' || e.name AS VARCHAR(1000))
    FROM employees e
    INNER JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT 
    REPEAT('  ', level - 1) || name as hierarchy,
    level,
    path
FROM org_chart
ORDER BY path;

-- Output:
-- CEO              1  CEO
--   VP Sales       2  CEO -> VP Sales
--     Sales Rep    3  CEO -> VP Sales -> Sales Rep
--   VP Eng         2  CEO -> VP Eng
--     Dev Lead     3  CEO -> VP Eng -> Dev Lead

📊 Sales Report Example

WITH 
    monthly_sales AS (
        SELECT 
            DATE_TRUNC('month', order_date) as month,
            SUM(total) as revenue
        FROM orders
        WHERE order_date >= '2024-01-01'
        GROUP BY DATE_TRUNC('month', order_date)
    ),
    sales_with_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_sales
    )
SELECT 
    TO_CHAR(month, 'Mon YYYY') as period,
    revenue,
    prev_month_revenue,
    growth,
    ROUND((growth / NULLIF(prev_month_revenue, 0)) * 100, 2) as growth_pct
FROM sales_with_growth
ORDER BY month;

CTE vs Subquery vs Temp Table

Feature CTE Subquery Temp Table
Readability ✓ High ✗ Low ~ Medium
Reusability ✓ In query ✗ No ✓ Session
Performance ~ Same as subquery ~ Depends ✓ Can index
Recursive ✓ Yes ✗ No ✗ No

✅ When to Use CTEs

  • Complex queries: Break down into logical steps
  • Readability: Name intermediate results meaningfully
  • Hierarchies: Recursive CTEs for tree structures
  • Debugging: Test each CTE separately
  • Documentation: CTE names explain query logic

💡 Pro Tips

  • Name meaningfully: active_users not cte1
  • Limit recursion: Add WHERE level <= 10 to prevent infinite loops
  • Test incrementally: Run each CTE alone to verify logic
  • Don't overuse: Simple queries don't need CTEs

"Inherited 500-line query with 8 levels of nested subqueries. Rewrote with CTEs in 2 hours. Same logic, 10x more readable. Junior devs can now understand and modify it."

— Database Developer

Related posts:

Return number of rows affected by UPDATE statements in SQL

The Hidden Killer: Scalar Functions — How to Avoid Massive Slowdowns

SQL: Use COALESCE to Handle NULL Values

Post Views: 5

Post navigation

.NET Core: Add Health Checks to Monitor Application Status
SQL: Read Execution Plans to Find Performance Bottlenecks

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

April 2026
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
27282930  
« Mar    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (751)
  • Add Constraint to SQL Table to ensure email contains @ (578)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (564)
  • Average of all values in a column that are not zero in SQL (531)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (489)
  • Find numbers with more than two decimal places in SQL (447)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries
  • SQL: Use Window Functions for Advanced Analytical Queries
  • .NET Core: Use Background Services for Long-Running Tasks
  • .NET Core: Use Minimal APIs for Lightweight HTTP Services
  • Git: Use Cherry-Pick to Apply Specific Commits Across Branches
  • Git: Use Interactive Rebase to Clean Up Commit History Before Merge

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (950)
  • How to add default value for Entity Framework migrations for DateTime and Bool (858)
  • Get the First and Last Word from a String or Sentence in SQL (836)
  • How to select distinct rows in a datatable in C# (805)
  • How to make theater mode the default for Youtube (751)

Recent Posts

  • C#: Use Init-Only Setters for Immutable Objects After Construction
  • C#: Use Expression-Bodied Members for Concise Single-Line Methods
  • C#: Enable Nullable Reference Types to Eliminate Null Reference Exceptions
  • C#: Use Record Types for Immutable Data Objects
  • SQL: Use CTEs for Readable Complex Queries

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com