One of the most common preprocessing tasks in event modeling is sessionization. It groups consecutive events from the same user into sessions, using a fixed period of inactivity (30 minutes, for example) as the boundary. Get the preceding event time with the window function LAG, then cumulatively sum a 0/1 gap flag to assign session IDs.
-- Get the value one row earlier within the same PARTITION LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) -- Assign session IDs with a cumulative SUM (cumulative boundary flags = session number) SUM(is_new_session) OVER (PARTITION BY user_id ORDER BY event_time)
From the user_events table, start a new session when the gap from the same user's previous event is longer than 30 minutes, then aggregate the session count, total event count, and average events per session for each user. Return user_id, session_count, total_events, avg_events_per_session (one decimal place), ordered by user_id ascending.
| user_id | event_time |
|---|---|
| 1 | 2024-01-10 10:00 |
| 1 | 2024-01-10 10:05 |
| 1 | 2024-01-10 10:20 |
| 1 | 2024-01-10 12:00 |
| 1 | 2024-01-10 12:10 |
| 2 | 2024-01-10 09:00 |
| 2 | 2024-01-10 09:15 |
| 2 | 2024-01-10 11:30 |
| 3 | 2024-01-11 14:00 |
※ user1: the gap from 10:20 to 12:00 is 100 minutes → a new session. user2: the gap from 09:15 to 11:30 is 135 minutes → a new session.
Expected output (ordered by user_id):
| user_id | session_count | total_events | avg_events_per_session |
|---|---|---|---|
| 1 | 2 | 5 | 2.5 |
| 2 | 2 | 3 | 1.5 |
| 3 | 1 | 1 | 1.0 |
user1: 5 events ÷ 2 sessions = 2.5. user2: 3 ÷ 2 = 1.5. user3: 1 ÷ 1 = 1.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
Attribution is at the heart of marketing analysis. It assigns a conversion (a purchase) to the channels that contributed to it, using models such as first touch, last touch, and multi-touch. The basic technique is to attach an always-available first value and the preceding value to each user's behavioral history with window functions.
-- FIRST_VALUE: propagate the first row's value within a PARTITION to every row FIRST_VALUE(channel) OVER (PARTITION BY user_id ORDER BY event_time) -- LAG: get the previous row's value (used for the touch immediately before purchase) LAG(channel) OVER (PARTITION BY user_id ORDER BY event_time)
ORDER BY for a window function gives the default frame RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. FIRST_VALUE returns the first value in the frame, so with ORDER BY it propagates the partition's first value to every row, which is exactly what first-touch extraction needs.From the user_events table, extract for each purchase event the user's first-touch channel (first_touch) and the channel immediately before purchase (last_touch). Return user_id, first_touch, last_touch, purchase_time, ordered by user_id ascending. Exclude users with no purchase.
| user_id | event_time | event_type | channel |
|---|---|---|---|
| 1 | 2024-01-10 10:00 | view | google_ads |
| 1 | 2024-01-10 11:00 | view | organic |
| 1 | 2024-01-10 12:00 | click | facebook_ads |
| 1 | 2024-01-10 13:00 | purchase | direct |
| 2 | 2024-01-11 09:00 | view | organic |
| 2 | 2024-01-11 10:00 | purchase | |
| 3 | 2024-01-11 11:00 | view | google_ads |
| 3 | 2024-01-11 12:00 | click | google_ads |
| 4 | 2024-01-12 09:00 | view | twitter_ads |
※ user3 and user4 did not reach purchase and are excluded.
Expected output (ordered by user_id):
| user_id | first_touch | last_touch | purchase_time |
|---|---|---|---|
| 1 | google_ads | facebook_ads | 2024-01-10 13:00 |
| 2 | organic | organic | 2024-01-11 10:00 |
user1: first = google_ads, previous = facebook_ads (the click row). user2: both first and previous are organic (view → direct purchase).
- 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 Q6 and Q7 covered funnel reach and order validation, but in practice, time between steps is also an important measure of conversion quality. If the average view → click time is too long, users may lack decision-making information; if click → purchase takes too long, the checkout flow may have friction.
-- Get the time difference from the previous event in seconds EXTRACT(EPOCH FROM (event_time - prev_event_time)) -- Divide by 60 for minutes, then ROUND to one decimal place ROUND(AVG(...) / 60, 1)
timestamp - timestamp returns an interval, and extracting EPOCH gives a numeric value in seconds. This serves the same role as MySQL's TIMESTAMPDIFF(SECOND, t1, t2) and BigQuery's TIMESTAMP_DIFF(t2, t1, SECOND). Convert seconds with /60 → minutes or /3600 → hours.From the user_events table, calculate the elapsed time for each consecutive funnel event (view → click and click → purchase), then output the count and average minutes for each transition type. Return transition, event_count, avg_minutes (one decimal place), in view → click and click → purchase order.
| user_id | event_type | event_time |
|---|---|---|
| 1 | view | 2024-01-10 10:00 |
| 1 | click | 2024-01-10 10:10 |
| 1 | purchase | 2024-01-10 11:00 |
| 2 | view | 2024-01-11 09:00 |
| 2 | click | 2024-01-11 09:30 |
| 3 | view | 2024-01-11 14:00 |
| 3 | click | 2024-01-11 14:05 |
| 3 | purchase | 2024-01-11 14:35 |
※ user1: 10 minutes → 50 minutes; user2: 30 minutes; user3: 5 minutes → 30 minutes.
Expected output (transition order):
| transition | event_count | avg_minutes |
|---|---|---|
| view -> click | 3 | 15.0 |
| click -> purchase | 2 | 40.0 |
view → click average: (10+30+5) ÷ 3 = 15.0 minutes. click → purchase average: (50+30) ÷ 2 = 40.0 minutes.
- 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
The biggest trap when visualizing a DAU (Daily Active Users) time series is that days with no events disappear from the result. A straightforward GROUP BY date produces no row for a day with zero activity, so the graph is not continuous and can give a misleading impression. Generate a date dimension with a recursive CTE to prevent this.
WITH RECURSIVE date_series(d) AS ( SELECT MIN(event_time::date) FROM user_events -- Anchor UNION ALL SELECT d + 1 -- Recursive step: generate a row one day after each accumulated row FROM date_series WHERE d < (SELECT MAX(event_time::date) FROM user_events) )
From the user_events table, output every date from the first event date through the last event date and calculate the DAU (number of active users) for each date. Output DAU=0 for dates with no events. Return event_date, dau, ordered by event_date ascending.
| user_id | event_time |
|---|---|
| 1 | 2024-01-10 |
| 2 | 2024-01-10 |
| 1 | 2024-01-12 |
| 3 | 2024-01-12 |
| 1 | 2024-01-14 |
※ There are no events on 1/11 or 1/13. Each must still be output with DAU=0.
Expected output (ordered by event_date):
| event_date | dau |
|---|---|
| 2024-01-10 | 2 |
| 2024-01-11 | 0 |
| 2024-01-12 | 2 |
| 2024-01-13 | 0 |
| 2024-01-14 | 1 |
Rows are generated even for the zero-event days 1/11 and 1/13. This is the effect of the date dimension × LEFT JOIN.
- 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
Detecting consecutive blocks (streaks), such as consecutive login or purchase days, is solved with the classic Gaps and Islands pattern. Assign ROW_NUMBER() to each row and calculate the curious difference date − row number: the value stays the same within a consecutive block and changes when a gap appears.
-- The magic key that identifies an island: date - row_number(day) login_date - ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY login_date ) * INTERVAL '1 day' AS streak_group
From the user_logins table, calculate each user's longest consecutive login streak (longest_streak). Return user_id, longest_streak, ordered by user_id ascending.
| user_id | login_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 |
| 3 | 2024-01-15 |
| 3 | 2024-01-16 |
| 3 | 2024-01-17 |
| 3 | 2024-01-18 |
※ user1 has two streaks: 1/1–1/3 (3 days) and 1/5–1/6 (2 days). user3 has four consecutive days from 1/15–1/18.
Expected output (ordered by user_id):
| user_id | longest_streak |
|---|---|
| 1 | 3 |
| 2 | 1 |
| 3 | 4 |
user1's longest is the first three-day streak. user2 has only one day. user3's longest streak is four consecutive 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