Batch Processing — Learn Running Totals, Pivots, and Recursive CTEs in Practice
AdvancedBatch ProcessingWindow FunctionsRunning Totals and PivotsRecursive CTEAPI PracticePostgreSQL/BigQuery Compatible5 questions
QUESTION 6
Multiple CTEs — Classify Users into Segments with RFM Scores
Multiple CTEsWITHRFMSegment Classification
Background

Listing multiple CTEs separated by commas creates a pipeline in which each later CTE can reference an earlier one. This organizes complex batch processing as a sequence of named steps.

WITH
step1 AS (
  SELECT user_id, COUNT(*) AS order_count
  FROM orders
  GROUP BY user_id
),
step2 AS (               -- can reference step1
  SELECT *,
    CASE WHEN order_count >= 5 THEN 'Loyal' ELSE 'General' END AS segment
  FROM step1
)
SELECT * FROM step2;
What is RFM:a method for scoring users on three metrics: Recency (most recent purchase date), Frequency (purchase frequency), and Monetary (purchase amount). It is extremely common in marketing batch processing.
Problem

From the orders table below, calculate each user’s R (days since the most recent purchase), F (purchase count), and M (total purchase amount), assign each metric a score from 1 to 3, and classify the final segment.

Reference date: 2024-05-01. R: within 30 days = 3, within 60 days = 2, otherwise = 1. F: 3 or more purchases = 3, 2 purchases = 2, 1 purchase = 1. M: at least 100000 = 3, at least 50000 = 2, otherwise = 1.
Classify the final segment as “VIP” when total_score is at least 8, “Loyal” when it is at least 6, and “General” otherwise.

Source table
▸ orders
order_iduser_idorder_dateamount
1U012024-04-2050000
2U012024-04-2870000
3U012024-04-3030000
4U022024-03-10120000
5U022024-03-2540000
6U032024-01-1530000
7U042024-04-25200000
8U042024-04-29150000
Expected Output
user_idr_scoref_scorem_scoretotal_scoresegment
U013339VIP
U043238VIP
U022237Loyal
U031113General
QUESTION 7
DENSE_RANK() — Rank Ties and Return the Top-Ranked Group
DENSE_RANKWindow FunctionsTie handlingRanking API
Background

When tied scores must include everyone through the tied third place, DENSE_RANK() is the right choice. Understand how it differs from ROW_NUMBER.

SELECT player, score,
  ROW_NUMBER()  OVER (ORDER BY score DESC) AS rn,    -- always assign consecutive numbers, even for ties
  RANK()        OVER (ORDER BY score DESC) AS rnk,   -- ties share a rank; the next rank is skipped
  DENSE_RANK()  OVER (ORDER BY score DESC) AS dr;    -- ties share a rank; rank numbers do not skip
Choosing the right function:Use ROW_NUMBER for the top N rows with a fixed count, DENSE_RANK for everyone tied through the top N ranks, and RANK when a displayed ranking should skip numbers. Choose based on the API requirement.
Problem

From the game_scores table below, rank players within each game using DENSE_RANK and return everyone ranked third or higher. Show every tied player at the same rank.

Source table
▸ game_scores
game_idplayer_namescore
G01Alice9500
G01Bob8200
G01Carol8200
G01Dave7800
G01Eve7100
G02Frank6000
G02Grace6000
G02Hank5500
G02Iris4800
Expected Output
game_idplayer_namescorerank
G01Alice95001
G01Bob82002
G01Carol82002
G01Dave78003
G02Frank60001
G02Grace60001
G02Hank55002
G02Iris48003

※ G01 Eve (7100) is excluded because DENSE_RANK = 4

QUESTION 8
LEFT JOIN + COALESCE — Build a Complete Monthly Report Including Zero-Sales Months
LEFT JOINCOALESCEMissing-data fill-inbatch report
Background

Months with no sales have no rows in the orders table. However, a monthly report may still need to include months with zero sales. LEFT JOIN + COALESCE is the standard pattern.

SELECT
  m.month,
  COALESCE(s.total_sales, 0) AS total_sales  -- replace NULL with 0
FROM       months        m        -- master list of all months
LEFT JOIN  sales_summary s        -- exists only for months with sales (some months become NULL)
        ON m.month = s.month;    -- join by month
LEFT JOIN vs INNER JOIN:INNER JOIN returns only matching rows from both tables, so zero-sales months disappear. LEFT JOIN preserves every row from the left table and returns NULL when the right side has no match. Converting that NULL to 0 with COALESCE is the standard zero-sales fill-in idiom.
Problem

Create a monthly summary for every month from January through June 2024. For months with no sales, set total_sales = 0 and order_count = 0.

Source table
▸ orders (sales data)
order_idorder_monthamount
12024-0130000
22024-0150000
32024-0380000
42024-0540000
52024-0560000
62024-0690000
▸ month_master (month master)
month
2024-01
2024-02
2024-03
2024-04
2024-05
2024-06
Expected Output
monthtotal_salesorder_count
2024-01800002
2024-0200
2024-03800001
2024-0400
2024-051000002
2024-06900001
QUESTION 9
FIRST_VALUE() / LAST_VALUE() — Put First and Latest Purchases on One Row
FIRST_VALUELAST_VALUEFirst/latestPurchase analysis
Background

FIRST_VALUE() and LAST_VALUE() return values from the first and last rows in a window. They are useful for purchase-history analysis such as showing each user’s first and latest product on one row.

FIRST_VALUE(product) OVER (
  PARTITION BY user_id          -- independent for each user
  ORDER BY     order_date        --  days → of rowsis  timespurchase
) AS first_product

LAST_VALUE(product) OVER (
  PARTITION BY user_id
  ORDER BY     order_date
  ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- look at all rows — important
) AS last_product
LAST_VALUE pitfall:If ROWS BETWEEN is omitted, the default is “through the current row,” so rows before the last cannot see the latest value.ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING in “allrows”please。
Problem

From the purchase_history table below, return each user’s first purchase date, first product, latest purchase date, and latest product on one row. Return exactly one row per user.

Source table
▸ purchase_history
user_idorder_dateproduct
U012024-01-10Apple
U012024-02-20Banana
U012024-04-05Orange
U022024-01-15Green tea
U022024-03-30Coffee
U032024-02-01Water
Expected Output
user_idfirst_datefirst_productlast_datelast_product
U012024-01-10Apple2024-04-05Orange
U022024-01-15Green tea2024-03-30Coffee
U032024-02-01Water2024-02-01Water
QUESTION 10
Recursive CTE (RECURSIVE) — Expand Hierarchical Categories Across All Levels
RECURSIVERecursive CTEHierarchyTree expansion
Background

A recursive CTE (WITH RECURSIVE) is a self-referencing query. It expands data with hierarchical parent-child relationships, such as category trees, organization charts, and comment threads.

WITH RECURSIVE tree AS (

  -- Anchor: get the first row (root node)
  SELECT id, parent_id, name, 0 AS depth
  FROM   categories
  WHERE  parent_id IS NULL          -- no parent = root

  UNION ALL                          -- connect the anchor and recursive members (do not remove duplicates)

  -- Recursive member: use the previous step to get children
  SELECT c.id, c.parent_id, c.name, t.depth + 1
  FROM       categories c
  INNER JOIN tree       t  -- reference tree from the previous step (recursive)
          ON c.parent_id = t.id
)
SELECT * FROM tree;
Recursive termination:Recursion ends when the recursive member produces no new rows. A cycle such as A → B → A can loop forever, so production queries should enforce a maximum depth.
Problem

The categories table below stores parent-child relationships for product categories. Expand every level with a recursive CTE and display each category’s depth and hierarchy path (for example: Electronics > Smartphone > Android).

Source table
▸ categories
idparent_idname
1NULLElectronics
21Smartphone
31PC
42Android
52iPhone
63Notebook PC
73Desktop
Expected Output
idnamedepthpath
1Electronics0Electronics
2Smartphone1Electronics > Smartphone
3PC1Electronics > PC
4Android2Electronics > Smartphone > Android
5iPhone2Electronics > Smartphone > iPhone
6Notebook PC2Electronics > PC > Notebook PC
7Desktop2Electronics > PC > Desktop