<< Home

Chapter 22: Advanced Queries

Advanced SQL features like window functions, pivoting, and recursive common table expressions (CTEs) let you express complex analytical and hierarchical logic directly in queries. These constructs build on core SELECT syntax but operate across multiple rows at once, providing cumulative, comparative, and hierarchical insights.

šŸ’” Learn to read advanced SQL as declarative pipelines. Each clause refines or enriches results rather than looping or mutating data step by step.

Window functions (OVER, PARTITION BY, RANK, LAG/LEAD)

Window functions compute values across related rows without collapsing them into groups. They use the OVER() clause, which can define partitions (logical subgroups) and orderings. Typical examples include running totals, row numbers, moving averages, and value comparisons.

Basic structure

A window function looks like an aggregate but uses OVER() to define its scope. The result set retains one row per input record.

SELECT
  customer_id,
  order_date,
  amount,
  SUM(amount) OVER (PARTITION BY customer_id) AS total_per_customer
FROM orders;

This computes the total amount per customer without grouping rows together. Each order retains its own detail.

ROW_NUMBER(), RANK(), and DENSE_RANK()

Ranking functions assign order within each partition. They differ in how they handle ties.

SELECT
  customer_id,
  amount,
  ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS row_num,
  RANK()       OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rank_num,
  DENSE_RANK() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS dense_rank_num
FROM orders;
Function Behavior
ROW_NUMBER() Sequential numbering (no gaps)
RANK() Gaps when ties occur
DENSE_RANK() No gaps; next rank follows immediately
āš ļø When using ranking for pagination, always provide a deterministic ORDER BY with unique keys to avoid inconsistent results between runs.

LAG() and LEAD()

These functions look backward or forward in the ordered set to compare values across rows. They are useful for computing differences between consecutive events or detecting changes.

SELECT
  customer_id,
  order_date,
  amount,
  LAG(amount)  OVER (PARTITION BY customer_id ORDER BY order_date) AS prev_amount,
  LEAD(amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS next_amount,
  amount - LAG(amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS diff_prev
FROM orders;
šŸ’” Use COALESCE() with LAG() or LEAD() to substitute defaults for missing neighbors, such as COALESCE(LAG(value) …, 0).

Moving averages and cumulative totals

By defining a frame with ROWS or RANGE, you can compute sliding windows such as a 7-day moving average or cumulative sums.

-- 7-day moving average by date
SELECT
  order_date,
  AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg
FROM orders;

-- cumulative total
SELECT
  customer_id,
  order_date,
  SUM(amount) OVER (PARTITION BY customer_id ORDER BY order_date ROWS UNBOUNDED PRECEDING) AS running_total
FROM orders;
āš ļø Frame units (ROWS vs RANGE) differ: ROWS counts physical rows, while RANGE uses value intervals (for example, numeric or time-based distances).

Pivoting and unpivoting data

Pivoting transforms rows into columns, while unpivoting reverses that process. These operations help reshape data for reports or analytics.

Pivoting with aggregates

The concept is universal, though syntax varies widely. You can use conditional aggregation or vendor-specific PIVOT operators.

-- Portable approach: conditional aggregation
SELECT
  customer_id,
  SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS pending_total,
  SUM(CASE WHEN status = 'paid'    THEN amount ELSE 0 END) AS paid_total,
  SUM(CASE WHEN status = 'shipped' THEN amount ELSE 0 END) AS shipped_total
FROM orders
GROUP BY customer_id;

SQL Server and Oracle include a dedicated PIVOT clause:

-- SQL Server example
SELECT *
FROM (
  SELECT customer_id, status, amount FROM orders
) AS src
PIVOT (
  SUM(amount) FOR status IN ([pending], [paid], [shipped])
) AS p;
šŸ’” For dynamic pivoting (unknown categories), generate the pivot list using application logic or dynamic SQL, as column names must be explicit in static queries.

Unpivoting (normalizing columns)

Unpivoting converts columns back into rows, often for cleaner storage or joining with metadata tables.

-- SQL Server example
SELECT customer_id, status, amount
FROM orders_pivoted
UNPIVOT (
  amount FOR status IN ([pending], [paid], [shipped])
) AS u;

Portable alternatives rely on UNION ALL constructs:

-- Portable UNION ALL approach
SELECT customer_id, 'pending' AS status, pending_total AS amount FROM orders_pivoted
UNION ALL
SELECT customer_id, 'paid', paid_total FROM orders_pivoted
UNION ALL
SELECT customer_id, 'shipped', shipped_total FROM orders_pivoted;
āš ļø Pivoted or unpivoted datasets can be large. Apply filters or summaries before reshaping whenever possible to reduce memory and I/O cost.

Recursive CTEs

Recursive common table expressions let you query hierarchical or graph-like data. They start with a base case, then repeatedly join or union new rows until no further matches appear. Use them for trees, bill-of-materials, or dependency chains.

Structure of a recursive CTE

A recursive CTE has two parts: an initial query (the base case) and a recursive query that references the CTE itself. Combine them with UNION ALL.

WITH RECURSIVE subordinates AS (
  -- base case
  SELECT employee_id, manager_id, full_name, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- recursive step
  SELECT e.employee_id, e.manager_id, e.full_name, s.level + 1
  FROM employees e
  JOIN subordinates s ON e.manager_id = s.employee_id
)
SELECT * FROM subordinates ORDER BY level, employee_id;
šŸ’” Think of the recursive part as adding one level of the hierarchy at a time. The process stops automatically when no new rows are produced.

Limiting recursion depth

Engines typically restrict recursion depth for safety. You can control it explicitly.

-- PostgreSQL / SQLite
WITH RECURSIVE t(n) AS (
  SELECT 1
  UNION ALL
  SELECT n + 1 FROM t WHERE n < 10
)
SELECT * FROM t;

-- SQL Server
WITH t AS (
  SELECT 1 AS n
  UNION ALL
  SELECT n + 1 FROM t WHERE n < 10
)
SELECT * FROM t
OPTION (MAXRECURSION 100);
āš ļø Missing termination conditions can create infinite recursion. Always include a stopping predicate (such as WHERE level < 10 or MAXRECURSION) to prevent runaway loops.

Practical uses

šŸ’” Recursive CTEs express loops declaratively. Test on small sets before applying to large trees, and add indexes on join keys to improve performance.