FIRST_VALUE / LAST_VALUE are window functions that return the first and last values within a partition. Be careful: unless you explicitly specify the window frame (ROWS BETWEEN …), they may return unintended values.
FIRST_VALUE(channel) OVER ( PARTITION BY user_id ORDER BY touched_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- Use the whole partition as the frame ) AS first_channel
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. With this default, LAST_VALUE returns the current row's value, not the value from the final row. Always specify ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.From the touch_events table, retrieve each user's first-touch channel, last-touch channel, total touch count, and whether the two channels match. Return user_id, first_channel, last_channel, total_touches, is_same_channel in ascending user_id order.
| user_id | channel | touched_at |
|---|---|---|
| 1 | 2024-01-05 | |
| 1 | 2024-01-10 | |
| 1 | direct | 2024-01-15 |
| 2 | 2024-02-01 | |
| 2 | 2024-02-08 | |
| 3 | 2024-02-10 | |
| 3 | 2024-02-12 | |
| 3 | 2024-02-20 |
✱ user1: google → email → direct (3 channels, no match); user2: facebook only (quick decision); user3: google → email → google (returning pattern).
Expected output (ascending user_id):
| user_id | first_channel | last_channel | total_touches | is_same_channel |
|---|---|---|---|---|
| 1 | direct | 3 | FALSE | |
| 2 | 2 | TRUE | ||
| 3 | 3 | TRUE |
- 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
Sessionization is one of the most important patterns in web analytics. Accesses separated by more than a certain interval (typically 30 minutes) are split into a new session. The implementation measures the gap from the previous row with LAG, flags gaps over the threshold, and assigns session IDs with the cumulative sum.
-- Core sessionization logic: turn flags into sequence numbers with a cumulative SUM
new_session_flag cumulative SUM (= session_id)
1 ← first row → 1
0 ← within 30 minutes → 1
0 → 1
1 ← over 30 min = new session → 2
0 → 2
1 → 3
EXTRACT(EPOCH FROM (t2 - t1)) / 60 returns the number of minutes from t1 to t2.From the page_views table, assign a new session ID when more than 30 minutes have elapsed between page views for each user. Return user_id, viewed_at, gap_minutes, session_id in ascending user_id and viewed_at order.
| user_id | viewed_at |
|---|---|
| 1 | 2024-01-05 10:00 |
| 1 | 2024-01-05 10:15 |
| 1 | 2024-01-05 10:25 |
| 1 | 2024-01-05 11:30 |
| 1 | 2024-01-05 11:45 |
| 2 | 2024-01-05 14:00 |
| 2 | 2024-01-05 15:00 |
| 2 | 2024-01-05 15:10 |
✱ user1: 10:25→11:30 is a 65-minute gap (new session); user2: 14:00→15:00 is a 60-minute gap (new session). Gaps of 30 minutes or less stay in the same session.
Expected output (ascending user_id and viewed_at):
| user_id | viewed_at | gap_minutes | session_id |
|---|---|---|---|
| 1 | 2024-01-05 10:00 | NULL | 1 |
| 1 | 2024-01-05 10:15 | 15 | 1 |
| 1 | 2024-01-05 10:25 | 10 | 1 |
| 1 | 2024-01-05 11:30 | 65 | 2 |
| 1 | 2024-01-05 11:45 | 15 | 2 |
| 2 | 2024-01-05 14:00 | NULL | 1 |
| 2 | 2024-01-05 15:00 | 60 | 2 |
| 2 | 2024-01-05 15:10 | 10 | 2 |
- 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
Extend the GAP-AND-ISLAND pattern from the basic section to calculate each user's longest consecutive-purchase period and whether that streak is still active in one query. After detecting islands (consecutive periods) with GAP-AND-ISLAND, rank the longest island per user and check whether its end date matches the latest purchase date.
-- A design implemented with four CTE stages
1) numbered : Assign sequence numbers with ROW_NUMBER
2) grouped : Generate an island key with date - (rn-1)
3) streaks : Aggregate each island with MIN/MAX/COUNT
4) ranked : Rank the longest island with ROW_NUMBER and get the latest date with MAX(end) OVER
5) Final SELECT: Keep rnk=1 and determine is_active
ORDER BY streak_days DESC, streak_end DESC). In practice, the most recent longest record is more useful for deciding an action.From the daily_orders table, calculate each user's longest consecutive-purchase streak (start date, end date, and length) and whether that streak is still active. “Active” means that the longest streak's end date equals the user's latest purchase date. Return user_id, max_streak_days, streak_start, streak_end, is_active in ascending user_id order.
| user_id | order_date |
|---|---|
| 1 | 2024-01-01 |
| 1 | 2024-01-02 |
| 1 | 2024-01-03 |
| 1 | 2024-01-05 |
| 1 | 2024-01-06 |
| 2 | 2024-01-10 |
| 2 | 2024-01-11 |
| 2 | 2024-01-12 |
| 2 | 2024-01-13 |
| 2 | 2024-01-14 |
| 3 | 2024-01-20 |
| 3 | 2024-01-21 |
| 3 | 2024-01-25 |
✱ user1: 01-01–03 (3 days) and 01-05–06 (2 days). The longest is 3 days, but the latest date is 01-06 → not active. user2: 01-10–14 is a 5-day streak (matches the latest date → active). user3: 01-20–21 (2 days) and a single order on 01-25 → the longest is 2 days, but the latest date is 01-25 → not active.
Expected output (ascending user_id):
| user_id | max_streak_days | streak_start | streak_end | is_active |
|---|---|---|---|---|
| 1 | 3 | 2024-01-01 | 2024-01-03 | FALSE |
| 2 | 5 | 2024-01-10 | 2024-01-14 | TRUE |
| 3 | 2 | 2024-01-20 | 2024-01-21 | FALSE |
- 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
Basic Q9 used WITH RECURSIVE to generate a date sequence. Here we tackle its original purpose: exploring a hierarchical structure (tree). It is a standard technique for traversing self-referencing tables such as invitation relationships, organization charts, comment threads, and file systems.
-- Hierarchical recursion (traverse the invite tree from the root) WITH RECURSIVE tree AS ( -- Anchor: start with the root (a user with no inviter) SELECT user_id, 0 AS depth FROM users WHERE invited_by IS NULL UNION ALL -- Recursive member: self-reference JOIN to the previous result, depth +1 SELECT u.user_id, t.depth + 1 FROM tree t JOIN users u ON u.invited_by = t.user_id )
FROM tree references only the rows added in the previous recursive iteration, not the entire cumulative result. This makes the JOIN generate only the next hierarchy level.WHERE NOT visited or path-array tracking.From the users table (self-referencing: invited_by identifies the user who invited that user), retrieve each user's invite depth (depth: levels below the root) and the number of users they directly invited. Return user_id, name, depth, direct_invitees in ascending depth and user_id order.
| user_id | name | invited_by |
|---|---|---|
| 1 | Alice | NULL |
| 2 | Bob | 1 |
| 3 | Carol | 1 |
| 4 | Dave | 2 |
| 5 | Eve | 2 |
| 6 | Frank | 4 |
| 7 | Grace | 3 |
✱ Root: Alice (invited_by=NULL). Hierarchy: Alice(0) → Bob,Carol(1) → Dave,Eve,Grace(2) → Frank(3).
Expected output (ascending depth and user_id):
| user_id | name | depth | direct_invitees |
|---|---|---|---|
| 1 | Alice | 0 | 2 |
| 2 | Bob | 1 | 2 |
| 3 | Carol | 1 | 1 |
| 4 | Dave | 2 | 1 |
| 5 | Eve | 2 | 0 |
| 7 | Grace | 2 | 0 |
| 6 | Frank | 3 | 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
Basic Q10 introduced one-axis Monetary segmentation with NTILE(4). Here we tackle the R × M (Recency × Monetary) two-axis matrix used in real business settings. Apply NTILE(3) independently to each axis and assign a business label to each of the 3×3=9 cells.
ORDER BY last_purchase_date DESC, then reverse the resulting rank with 4 - r_rank. Ascending dates make older users score 3, reversing the meaning.From the customer_purchases table, apply NTILE(3) to each user's latest purchase date (Recency) and total purchase amount (Monetary), classify users into a 3×3=9-cell matrix, and assign segment labels. Return user_id, last_purchase_date, total_amount, r_score, m_score, segment in descending r_score and m_score order.
| user_id | purchase_date | amount |
|---|---|---|
| 1 | 2024-03-25 | 15000 |
| 2 | 2024-03-20 | 8000 |
| 3 | 2024-03-28 | 2000 |
| 3 | 2024-03-30 | 2500 |
| 4 | 2024-02-15 | 12000 |
| 5 | 2024-02-10 | 5000 |
| 5 | 2024-02-25 | 2000 |
| 6 | 2024-02-20 | 3000 |
| 7 | 2024-01-05 | 20000 |
| 7 | 2024-01-20 | 10000 |
| 8 | 2024-01-10 | 5000 |
| 9 | 2024-01-15 | 1500 |
✱ 9 users. After aggregation: user1=15000, user3=4500 (2 purchases), user5=7000 (2 purchases), user7=30000 (2 purchases), etc. With 9÷3=3, the design distributes three users cleanly into each bucket.
Expected output (descending r_score and m_score):
| user_id | last_purchase_date | total_amount | r_score | m_score | segment |
|---|---|---|---|---|---|
| 1 | 2024-03-25 | 15000 | 3 | 3 | VIP |
| 2 | 2024-03-20 | 8000 | 3 | 2 | Prospective VIP |
| 3 | 2024-03-30 | 4500 | 3 | 1 | New / nurture |
| 4 | 2024-02-15 | 12000 | 2 | 3 | Churn watch |
| 5 | 2024-02-25 | 7000 | 2 | 2 | Standard |
| 6 | 2024-02-20 | 3000 | 2 | 1 | General |
| 7 | 2024-01-20 | 30000 | 1 | 3 | Lost-customer risk |
| 8 | 2024-01-10 | 5000 | 1 | 2 | Dormant |
| 9 | 2024-01-15 | 1500 | 1 | 1 | Dormant |
- 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