A funnel is a core event-analysis tool that measures the number of users who pass through multiple steps in sequence, such as view → cart → purchase. A standard pattern is to extract each user's first timestamp for each event with MIN(event_time) FILTER (WHERE event_type='...'), then determine step completion by comparing the timestamps.
-- Get the first occurrence time of each event type per user MIN(event_time) FILTER (WHERE event_type = 'view') AS t_view MIN(event_time) FILTER (WHERE event_type = 'cart') AS t_cart MIN(event_time) FILTER (WHERE event_type = 'purchase') AS t_purchase -- Ordering comparison: confirm that cart happened after view WHERE t_cart > t_view AND t_purchase > t_cart
NULL > any value returns NULL, and COUNT(*) FILTER excludes rows whose filter condition is NULL. Thus, a user who never triggered a cart event (t_cart is NULL) is automatically excluded from the step-2 count. SQL's three-valued logic means you do not need to write an explicit IS NOT NULL check.From the EC site's user_events table, aggregate a sequential funnel. (1) Determine whether each user completed view → cart → purchase in that order, (2) count the total number of users completing each step, and (3) calculate the view→cart and cart→purchase conversion rates as percentages rounded to one decimal place. Return one row with five columns.
| user_id | event_type | event_time |
|---|---|---|
| 1 | view | 2024-01-10 10:00 |
| 1 | cart | 2024-01-10 10:05 |
| 1 | purchase | 2024-01-10 10:30 |
| 2 | view | 2024-01-10 11:00 |
| 2 | cart | 2024-01-10 11:15 |
| 2 | purchase | 2024-01-10 11:45 |
| 3 | view | 2024-01-10 12:00 |
| 3 | cart | 2024-01-10 12:30 |
| 4 | view | 2024-01-10 14:00 |
| 5 | view | 2024-01-10 15:00 |
| 5 | purchase | 2024-01-10 15:30 |
※ user5 went directly from view to purchase and skipped cart. In a sequential funnel, user5 does not complete the cart step because t_cart is NULL.
Expected output (one row, five columns):
| step1_view | step2_cart | step3_purchase | view_to_cart_pct | cart_to_purchase_pct |
|---|---|---|---|---|
| 5 | 3 | 2 | 60.0 | 66.7 |
step1=5 (everyone completed view), step2=3 (users 1, 2, and 3 completed view→cart), and step3=2 (users 1 and 2 completed view→cart→purchase). user5 skipped cart and is excluded from both steps 2 and 3.
- 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
After detecting session boundaries (covered in Basic Q3), the next practical step is assigning session IDs. If a “session-start flag” is cumulatively summed with SUM() OVER (... ROWS UNBOUNDED PRECEDING), every row within a session receives the same number. This is a representative application of SQL's Gaps & Islands pattern.
-- Generate cumulative session numbers SUM(is_new_session) OVER ( PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS session_num -- If is_new_session is [1,0,1,0], the cumulative sum is [1,1,2,2] -- Rows in the same session receive the same number
SUM(is_new_session) OVER (... ORDER BY ...) also defaults to UNBOUNDED PRECEDING through CURRENT ROW (technically a RANGE frame), but stating ROWS explicitly is safer and makes the intent clear. Understanding this frame specification is key to mastering window aggregation.For the user_events table, treat an event gap of 30 minutes or more as a new-session boundary and assign each event row a session ID in the form user_id-session_num. Return user_id, session_id, event_type, event_time, ordered by user_id and event_time ascending.
| user_id | event_type | event_time |
|---|---|---|
| 1 | view | 2024-01-10 10:00 |
| 1 | click | 2024-01-10 10:05 |
| 1 | view | 2024-01-10 10:50 |
| 1 | purchase | 2024-01-10 10:55 |
| 2 | view | 2024-01-10 11:00 |
| 2 | click | 2024-01-10 11:10 |
| 2 | view | 2024-01-10 12:00 |
| 3 | view | 2024-01-10 09:00 |
※ user1: 10:05→10:50 is 45 minutes (new session). user2: 11:10→12:00 is 50 minutes (new session). user3 has only one event.
Expected output (ascending user_id / event_time):
| user_id | session_id | event_type | event_time |
|---|---|---|---|
| 1 | 1-1 | view | 10:00 |
| 1 | 1-1 | click | 10:05 |
| 1 | 1-2 | view | 10:50 |
| 1 | 1-2 | purchase | 10:55 |
| 2 | 2-1 | view | 11:00 |
| 2 | 2-1 | click | 11:10 |
| 2 | 2-2 | view | 12:00 |
| 3 | 3-1 | view | 09:00 |
user1 receives 2 sessions, user2 receives 2 sessions, and user3 receives 1 session. Rows within the same session share the same ID.
- 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
Cohort analysis tracks a group of users who share the same first-use month (signup month) and measures how many remain active over time. Retention, one of the most important metrics for subscription products such as SaaS, mobile apps, and ecommerce, is calculated with this pattern.
-- Determine each user's cohort (first-use month) SELECT user_id, DATE_TRUNC('month', MIN(event_time))::date AS cohort_month FROM user_events GROUP BY user_id -- Monthly retention for each cohort SELECT cohort_month, months_since_signup, active_users, FIRST_VALUE(active_users) OVER ( PARTITION BY cohort_month ORDER BY months_since_signup ) AS cohort_size -- initial size of each cohort
From the user_events table, determine each user's first-use month (cohort), then calculate monthly active-user counts and retention rates (%) by cohort. Return cohort_month, months_since_signup, active_users, cohort_size, retention_pct, ordered by cohort_month and months_since_signup ascending.
| user_id | event_time |
|---|---|
| 1 | 2024-01-15 |
| 1 | 2024-02-10 |
| 1 | 2024-03-05 |
| 2 | 2024-01-20 |
| 2 | 2024-02-15 |
| 3 | 2024-01-25 |
| 4 | 2024-02-05 |
| 4 | 2024-03-12 |
| 5 | 2024-02-20 |
| 6 | 2024-03-15 |
※ January cohort: users 1, 2, 3 (3 users) → February: users 1 and 2 remain (2/3=66.7%) → March: only user1 remains (1/3=33.3%). February cohort: users 4 and 5 (2 users) → only user4 remains in March (50%).
Expected output (ascending cohort_month / months_since_signup):
| cohort_month | months_since_signup | active_users | cohort_size | retention_pct |
|---|---|---|---|---|
| 2024-01-01 | 0 | 3 | 3 | 100.0 |
| 2024-01-01 | 1 | 2 | 3 | 66.7 |
| 2024-01-01 | 2 | 1 | 3 | 33.3 |
| 2024-02-01 | 0 | 2 | 2 | 100.0 |
| 2024-02-01 | 1 | 1 | 2 | 50.0 |
| 2024-03-01 | 0 | 1 | 1 | 100.0 |
The January cohort retains 33% after two months. The February cohort retains 50% after one month. This measures early cohort retention for the product.
- 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 marketing and UX improvement, the path showing “which events a user passed through, and in what order” is extremely valuable. For example, view → cart → purchase and view → purchase have very different contexts even though both end in purchase.
This question teaches two-stage aggregation with STRING_AGG (or ARRAY_AGG). First, turn each user's events into a time-ordered string; second, count users who share the same path. This extracts the Top N most common behavioral patterns.
-- Concatenate events in chronological order for each user STRING_AGG(event_type, ' → ' ORDER BY event_time)
Write a query that concatenates each user's events in firing order with “ → ”, then aggregates the user count and path length for each identical path.
First generate one path string per user by concatenating events chronologically, then count users per path. Sort by descending user count; ties are sorted by descending path length.
| user_id | event_type | event_time |
|---|---|---|
| 1 | view | 2024-01-15 10:00:00 |
| 1 | cart | 2024-01-15 10:05:00 |
| 1 | purchase | 2024-01-15 10:10:00 |
| 2 | view | 2024-01-15 11:00:00 |
| 2 | cart | 2024-01-15 11:03:00 |
| 2 | purchase | 2024-01-15 11:08:00 |
| 3 | view | 2024-01-15 12:00:00 |
| 3 | cart | 2024-01-15 12:02:00 |
| 4 | view | 2024-01-15 13:00:00 |
| 4 | purchase | 2024-01-15 13:01:00 |
| 5 | view | 2024-01-15 14:00:00 |
| 5 | purchase | 2024-01-15 14:05:00 |
Expected output (descending user count; ties by descending path length):
| path | user_count | path_length |
|---|---|---|
| view → cart → purchase | 2 | 3 |
| view → purchase | 2 | 2 |
| view → cart | 1 | 2 |
※ user1 and user2 fully converted (view → cart → purchase); user4 and user5 purchased after skipping cart (view → purchase); user3 dropped off partway through (view → cart).
- 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
When charting daily active users (DAU), a day with zero events may be absent from the data, causing a line chart to break unnaturally. The standard solution is to generate a date-series table containing every day in the analysis period and LEFT JOIN the real data to it.
This question dynamically generates a date series with WITH RECURSIVE. Recursive CTEs are difficult to grasp, so the explanation visualizes three things at every step: the current accumulated result, the row just added, and the next row to generate.
-- Basic WITH RECURSIVE structure WITH RECURSIVE date_series AS ( SELECT DATE '2024-01-10' AS day -- base case (first row) UNION ALL SELECT day + INTERVAL '1 day' -- recursive term (+1 day from the previous row) FROM date_series WHERE day < DATE '2024-01-15' -- termination condition )
For the six-day period from 2024-01-10 through 2024-01-15 (inclusive), aggregate daily DAU (distinct users) and output days with no events as 0.
Because aggregating only the events table drops empty days, generate a continuous date series with a recursive CTE or similar technique, then join it to the aggregate to fill missing dates.
| user_id | event_time |
|---|---|
| 1 | 2024-01-10 09:00 |
| 2 | 2024-01-10 14:00 |
| 1 | 2024-01-12 10:00 |
| 3 | 2024-01-12 16:00 |
| 2 | 2024-01-14 11:00 |
| 1 | 2024-01-15 09:30 |
※ No one was active on 2024-01-11 or 2024-01-13, so these dates do not exist in the events table.
Expected output (ascending day):
| day | active_users |
|---|---|
| 2024-01-10 | 2 |
| 2024-01-11 | 0 |
| 2024-01-12 | 2 |
| 2024-01-13 | 0 |
| 2024-01-14 | 1 |
| 2024-01-15 | 1 |
※ 01-11 and 01-13, which are absent from events, are filled with 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