CPCODELAB
PostgreSQL

CTEs & Window Functions

13 min read

CTEs & Window Functions

Common Table Expressions (WITH Queries)

A CTE (Common Table Expression) lets you define a named temporary result set at the top of a query. This makes complex queries far more readable by breaking them into logical, named steps.

sql
-- Find the top 5 authors by published post count
WITH author_stats AS (
  SELECT
    author_id,
    COUNT(*) AS post_count
  FROM posts
  WHERE published = true
  GROUP BY author_id
),
top_authors AS (
  SELECT author_id, post_count
  FROM author_stats
  ORDER BY post_count DESC
  LIMIT 5
)
SELECT
  u.full_name,
  u.email,
  ta.post_count
FROM top_authors ta
JOIN users u ON u.id = ta.author_id;

Recursive CTEs

CTEs can be recursive, which is essential for traversing hierarchical data like category trees, org charts, or threaded comments.

sql
CREATE TABLE categories (
  id        int PRIMARY KEY,
  name      text,
  parent_id int REFERENCES categories(id)
);

-- Traverse the entire category tree from the root
WITH RECURSIVE tree AS (
  -- Base case: root categories
  SELECT id, name, parent_id, 0 AS depth, name::text AS path
  FROM categories
  WHERE parent_id IS NULL

  UNION ALL

  -- Recursive case: children
  SELECT c.id, c.name, c.parent_id, t.depth + 1, t.path || ' > ' || c.name
  FROM categories c
  JOIN tree t ON t.id = c.parent_id
)
SELECT depth, path FROM tree ORDER BY path;

Window Functions

Window functions perform calculations across a set of rows that are related to the current row, without collapsing rows like GROUP BY does. They are invaluable for rankings, running totals, and lag/lead comparisons.

sql
-- Rank posts by view count within each category
SELECT
  title,
  category,
  view_count,
  ROW_NUMBER() OVER (PARTITION BY category ORDER BY view_count DESC) AS row_num,
  RANK()       OVER (PARTITION BY category ORDER BY view_count DESC) AS rank,
  DENSE_RANK() OVER (PARTITION BY category ORDER BY view_count DESC) AS dense_rank,
  SUM(view_count) OVER (PARTITION BY category)                       AS category_total,
  LAG(view_count)  OVER (PARTITION BY category ORDER BY view_count DESC) AS prev_views
FROM posts
WHERE published = true;
  • ROW_NUMBER() — assigns a unique number to each row; ties get different numbers.
  • RANK() — ties get the same number; the next rank skips (1, 1, 3).
  • DENSE_RANK() — ties get the same number; no gaps (1, 1, 2).
  • LAG(col, n) / LEAD(col, n) — access a value n rows before/after in the window.
  • SUM() / AVG() as window functions — running or partitioned totals without collapsing.

Window functions are evaluated after WHERE and GROUP BY but before ORDER BY and LIMIT. You can filter on window function results by wrapping the query in a CTE or subquery.

Ready to test yourself?
Practice PostgreSQL— quiz & coding exercises