Window functions add information such as a rank or aggregate within a group to each row without collapsing rows like GROUP BY. ROW_NUMBER() assigns consecutive numbers starting at 1 in ORDER BY order within each partition.
ROW_NUMBER() OVER ( PARTITION BY user_id -- Number rows independently for each user ORDER BY purchased_at ASC -- Number from oldest to newest (rn=1 is first) )
ROW_NUMBER still assigns unique numbers (which row receives 1 is unspecified). RANK gives tied rows the same rank and skips the next rank. To extract exactly one first event, ROW_NUMBER + WHERE rn = 1 is the safest pattern.From the purchase_events table, extract each user's first purchase record (user_id, first_item, first_purchase_date). Use a CTE to assign each row a number (rn) ordered by purchased_at from oldest to newest, then filter to rn=1 in the outer query. Return the rows in ascending user_id order.
| user_id | item_id | purchased_at |
|---|---|---|
| 1 | A001 | 2024-01-05 |
| 2 | B001 | 2024-01-10 |
| 1 | C002 | 2024-01-15 |
| 3 | F002 | 2024-01-18 |
| 3 | D001 | 2024-01-20 |
| 2 | E003 | 2024-02-01 |
| 1 | G004 | 2024-02-10 |
✱ user1=3 purchases / user2=2 / user3=2. Extract only the first purchase for each user.
Expected output (ascending user_id):
| user_id | first_item | first_purchase_date |
|---|---|---|
| 1 | A001 | 2024-01-05 |
| 2 | B001 | 2024-01-10 |
| 3 | F002 | 2024-01-18 |
user1: A001 is first (01-05). user2: B001 is first (01-10). user3: F002 is first (01-18, earlier than D001 on 01-20).
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
NTILE(n) is a window function that divides all rows into n buckets and assigns each row a bucket number (1–n). It is often used for Frequency scores in RFM analysis and for automatically classifying user engagement levels.
NTILE(4) OVER (ORDER BY purchase_count ASC) -- Split 5 users into 4 buckets: [2 rows, 1 row, 1 row, 1 row] -- The first bucket receives the extra row (bucket1 has 2 users) -- ORDER BY ASC → smaller values go to tile1 = low score
From the purchases table, aggregate purchase counts per user and assign a four-level frequency score (freq_score) with NTILE(4). Also flag users with freq_score=4 as is_high_value = true, and return the result in ascending user_id order. Output columns: user_id, purchase_count, freq_score, is_high_value.
| user_id | order_id | ordered_at |
|---|---|---|
| 1 | 101 | 2024-01-05 |
| 1 | 102 | 2024-01-20 |
| 1 | 103 | 2024-02-10 |
| 2 | 104 | 2024-01-08 |
| 2 | 105 | 2024-02-05 |
| 3 | 106 | 2024-01-12 |
| 3 | 107 | 2024-01-18 |
| 3 | 108 | 2024-01-24 |
| 3 | 109 | 2024-02-02 |
| 3 | 110 | 2024-02-15 |
| 4 | 111 | 2024-01-30 |
| 5 | 112 | 2024-01-09 |
| 5 | 113 | 2024-01-16 |
| 5 | 114 | 2024-02-08 |
| 5 | 115 | 2024-02-20 |
✱ user1=3 purchases / user2=2 / user3=5 / user4=1 / user5=4
Expected output (ascending user_id):
| user_id | purchase_count | freq_score | is_high_value |
|---|---|---|---|
| 1 | 3 | 2 | false |
| 2 | 2 | 1 | false |
| 3 | 5 | 4 | true |
| 4 | 1 | 1 | false |
| 5 | 4 | 3 | false |
Split 5 rows into 4 buckets: user4 (1 purchase) and user2 (2) → tile1, user1 (3) → tile2, user5 (4) → tile3, user3 (5) → tile4. Because the extra row goes to tile1, user4 and user2 share score 1.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
LAG(col) retrieves the value from the previous row in a window. The first row in each partition becomes NULL.
LAG(stepped_at) OVER ( PARTITION BY user_id -- Evaluate each user independently ORDER BY stepped_at ASC -- Chronological order ) AS prev_stepped_at -- The first row (page_view) is NULL; the next row (signup) receives page_view's timestamp
date - date returns an integer number of days. By contrast, timestamp - timestamp returns an interval. You can compare dates directly with stepped_at - prev_stepped_at <= 7 and express a within-seven-day conversion check in one line.From the step_events table, calculate the elapsed days from the previous step (page_view) to each user's signup step and whether the conversion occurred within 7 days. Use LAG() to retrieve the previous timestamp, then keep only signup rows with WHERE. Output columns: user_id, stepped_at, prev_stepped_at, days_from_prev, within_7days (ascending user_id).
| user_id | step | stepped_at |
|---|---|---|
| 1 | page_view | 2024-01-01 |
| 1 | signup | 2024-01-03 |
| 2 | page_view | 2024-01-05 |
| 2 | signup | 2024-01-07 |
| 3 | page_view | 2024-01-10 |
| 3 | signup | 2024-01-18 |
| 4 | page_view | 2024-01-15 |
| 4 | signup | 2024-01-16 |
✱ user3 takes 8 days from page_view to signup → outside the seven-day conversion window
Expected output (ascending user_id):
| user_id | stepped_at | prev_stepped_at | days_from_prev | within_7days |
|---|---|---|---|---|
| 1 | 2024-01-03 | 2024-01-01 | 2 | true |
| 2 | 2024-01-07 | 2024-01-05 | 2 | true |
| 3 | 2024-01-18 | 2024-01-10 | 8 | false |
| 4 | 2024-01-16 | 2024-01-15 | 1 | true |
Only user3 signs up after 8 days and falls outside the conversion window. The other three users are within 7 days.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
In basic Q2, we calculated retention for only one following month with INTERVAL '1 month'. Combining UNNEST(ARRAY[1,2,3]) with CROSS JOIN generates Month 1–3 retention for all periods in one query, without a loop.
-- Expand an array into rows with UNNEST SELECT UNNEST(ARRAY[1, 2, 3]) AS offset_month -- → 3 rows: offset=1, offset=2, offset=3 -- Generate a dynamic INTERVAL cohort_month + (offset_month || ' month')::interval -- offset=1 → cohort_month + 1 month → target month
From the users and login_events tables, calculate a cohort-based Month 1–3 retention-rate matrix. Use UNNEST to generate three rows for offset=1,2,3, CROSS JOIN to expand every cohort×offset combination, and LEFT JOIN to attach active months. Output columns: cohort_month, offset_month, cohort_size, retained, retention_pct (ascending cohort_month, then offset_month).
| user_id | registered_at |
|---|---|
| 1 | 2024-01-10 |
| 2 | 2024-01-15 |
| 3 | 2024-01-22 |
| 4 | 2024-02-05 |
| 5 | 2024-02-14 |
| 6 | 2024-02-20 |
| user_id | event_date |
|---|---|
| 1 | 2024-01-12 |
| 2 | 2024-01-18 |
| 3 | 2024-01-25 |
| 1 | 2024-02-05 |
| 3 | 2024-02-10 |
| 4 | 2024-02-07 |
| 5 | 2024-02-14 |
| 6 | 2024-02-22 |
| 1 | 2024-03-05 |
| 4 | 2024-03-08 |
| 6 | 2024-03-15 |
| 4 | 2024-04-03 |
✱ January cohort: M1=user1,3 / M2=user1 / M3=none. February cohort: M1=user4,6 / M2=user4 / M3=none.
Expected output (ascending cohort_month, then offset_month):
| cohort_month | offset_month | cohort_size | retained | retention_pct |
|---|---|---|---|---|
| 2024-01-01 | 1 | 3 | 2 | 66.7 |
| 2024-01-01 | 2 | 3 | 1 | 33.3 |
| 2024-01-01 | 3 | 3 | 0 | 0.0 |
| 2024-02-01 | 1 | 3 | 2 | 66.7 |
| 2024-02-01 | 2 | 3 | 1 | 33.3 |
| 2024-02-01 | 3 | 3 | 0 | 0.0 |
Both cohorts have retained=0 → 0.0% at offset=3. CROSS JOIN keeps the zero-match rows from disappearing.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free
WITH RECURSIVE is a CTE that references itself. Its structure consists of an “anchor” (initial row) + UNION ALL + a recursive step. Always specify a termination condition (WHERE), or the recursion becomes an infinite loop.
WITH RECURSIVE date_series AS ( SELECT '2024-01-01'::date AS dt -- ① Anchor (starting point) UNION ALL SELECT (dt + INTERVAL '1 day')::date -- ② Recursive step FROM date_series WHERE dt < '2024-01-07' -- ③ Termination condition (required) )
From the user_sessions table, output the daily DAU trend from 2024-01-01 through 2024-01-07, filling dates with no sessions with 0. Generate a date column (date_series) with WITH RECURSIVE, join the real data with LEFT JOIN, and convert NULL to 0 with COALESCE. Output columns: dt, dau (ascending dt).
| user_id | session_date |
|---|---|
| 1 | 2024-01-01 |
| 2 | 2024-01-01 |
| 1 | 2024-01-03 |
| 2 | 2024-01-03 |
| 1 | 2024-01-05 |
| 3 | 2024-01-05 |
| 1 | 2024-01-07 |
| 2 | 2024-01-07 |
✱ 01-02, 01-04, and 01-06 have no sessions → fill them with 0
Expected output (ascending dt):
| dt | dau |
|---|---|
| 2024-01-01 | 2 |
| 2024-01-02 | 0 |
| 2024-01-03 | 2 |
| 2024-01-04 | 0 |
| 2024-01-05 | 2 |
| 2024-01-06 | 0 |
| 2024-01-07 | 2 |
The recursive CTE generates a seven-day calendar, and dates without real data are filled with COALESCE(NULL, 0) = 0.
- Read every model answer, explanation and table-transition visualization across all 48 advanced sets (240 questions)
- One-time purchase — no subscription. Future advanced sets are included
- Background, problem and expected output stay free