Recipe Tableau and SQL
How do I build a conversion funnel from an event log?
Count a user at a step only if they reached it after the step before, within a time window. Counting everyone who fired each event, this meal-kit site's funnel says 149 of 717 visitors (21%) subscribe and the weakest step is signing up. Counted in order within 7 days, 8.4% subscribe, and the weakest step is Subscribe.
The data
Basil Box meal-kit site, product events, September to October 2025
797 people on a meal-kit site over two months: visit, sign up, build a box, subscribe. Some are existing subscribers renewing, some block tracking, some build a box as a guest before signing up.
user_id | event_date | event |
|---|---|---|
| U-0387 | 2025-09-01 | visit |
| U-0390 | 2025-09-01 | visit |
| U-0418 | 2025-09-01 | visit |
| U-0520 | 2025-09-01 | visit |
Why do the two funnels disagree?
Because an event log holds more than your funnel. Count everyone who fired each event and 149 of 717 visitors appear to subscribe. But 67 of those subscribers never visited in this data: existing customers renewing, and people who block tracking. Count only people who visited, signed up, built a box and subscribed, in that order and within 7 days, and 60 of 717 remain. The open funnel puts the weakest step at signing up; in order, it is Subscribe (37.3%).
Step 1
Count everyone who fired each event
This is the open funnel, and the one most dashboards show. It mixes in people who never did the earlier steps in this data: existing subscribers renewing, visitors with ad blockers, guests. [Step] is a CASE on [event] that names the four steps; sort it by hand, not alphabetically.
Tableau
COUNTD([user_id])
// with [Step] on Columns, sorted 1 to 4SQL
SELECT
CASE event
WHEN 'visit' THEN 1
WHEN 'signup' THEN 2
WHEN 'build_box' THEN 3
ELSE 4
END AS step_no,
CASE event
WHEN 'visit' THEN 'Visit'
WHEN 'signup' THEN 'Sign up'
WHEN 'build_box' THEN 'Build a box'
ELSE 'Subscribe'
END AS step,
COUNT(DISTINCT user_id) : : INTEGER AS users
FROM input
GROUP BY ALL
ORDER BY step_noRows
| Step no | Step | Users |
|---|---|---|
| 1 | Visit | 717 |
| 2 | Sign up | 319 |
| 3 | Build a box | 228 |
| 4 | Subscribe | 149 |
Step 2
Find each user's steps, in order
One row per visitor. Each date is the first time the user did that step after the step before and within the window from the visit. In Tableau, chain four FIXED fields the same way: [Visit date] is { FIXED [user_id] : MIN(IF [event] = 'visit' THEN [event_date] END) }, [Signup date] is the formula above, and [Box date] and [Subscribe date] each start from the date before them. An empty cell is where the user dropped off.
Tableau
{ FIXED [user_id] : MIN(IF [event] = 'signup' AND [event_date] >= [Visit date] AND [event_date] <= DATEADD('day', 7, [Visit date]) THEN [event_date] END) }SQL
WITH v AS (
SELECT user_id, MIN(event_date) AS visit_date
FROM input
WHERE event = 'visit'
GROUP BY user_id
HAVING '{knob.cohort}' = 'All visitors' OR MIN(event_date) <= (
SELECT MAX(event_date)
FROM input
) - 7
), s AS (
SELECT v.user_id, v.visit_date, MIN(e.event_date) AS signup_date
FROM v JOIN input e ON e.user_id = v.user_id AND e.event = 'signup' AND e.event_date BETWEEN v.visit_date AND v.visit_date + 7
GROUP BY v.user_id, v.visit_date
), b AS (
SELECT s.user_id, s.visit_date, MIN(e.event_date) AS box_date
FROM s JOIN input e ON e.user_id = s.user_id AND e.event = 'build_box' AND e.event_date BETWEEN s.signup_date AND s.visit_date + 7
GROUP BY s.user_id, s.visit_date
), p AS (
SELECT b.user_id, MIN(e.event_date) AS subscribe_date
FROM b JOIN input e ON e.user_id = b.user_id AND e.event = 'subscribe' AND e.event_date BETWEEN b.box_date AND b.visit_date + 7
GROUP BY b.user_id
)
SELECT
v.user_id,
v.visit_date,
s.signup_date,
b.box_date,
p.subscribe_date
FROM v LEFT JOIN s USING (user_id) LEFT JOIN b USING (user_id) LEFT JOIN p USING (user_id)
ORDER BY v.visit_date, v.user_idRows
| User ID | Event date | Signup date | Box date | Subscribe date |
|---|---|---|---|---|
| U-0387 | 2025-09-01 | 2025-09-02 | 2025-09-03 | – |
| U-0390 | 2025-09-01 | – | – | – |
| U-0418 | 2025-09-01 | – | – | – |
| U-0520 | 2025-09-01 | – | – | – |
| U-0612 | 2025-09-01 | 2025-09-02 | 2025-09-02 | – |
| U-0637 | 2025-09-01 | 2025-09-03 | 2025-09-03 | – |
| U-0713 | 2025-09-01 | – | – | – |
| U-0069 | 2025-09-02 | – | – | – |
| U-0267 | 2025-09-02 | – | – | – |
| U-0324 | 2025-09-02 | – | – | – |
| U-0383 | 2025-09-02 | 2025-09-02 | 2025-09-02 | 2025-09-02 |
| U-0610 | 2025-09-02 | 2025-09-02 | 2025-09-05 | 2025-09-06 |
| U-0643 | 2025-09-02 | – | – | – |
| U-0660 | 2025-09-02 | 2025-09-07 | – | – |
| U-0747 | 2025-09-02 | 2025-09-02 | – | – |
| U-0151 | 2025-09-03 | – | – | – |
| U-0549 | 2025-09-03 | – | – | – |
| U-0622 | 2025-09-03 | 2025-09-03 | 2025-09-03 | – |
| U-0694 | 2025-09-03 | 2025-09-04 | – | – |
| U-0702 | 2025-09-03 | – | – | – |
| U-0742 | 2025-09-03 | – | – | – |
| U-0752 | 2025-09-03 | 2025-09-03 | 2025-09-03 | 2025-09-03 |
| U-0278 | 2025-09-04 | 2025-09-06 | 2025-09-06 | – |
| U-0355 | 2025-09-04 | – | – | – |
| U-0366 | 2025-09-04 | 2025-09-04 | 2025-09-04 | – |
| U-0417 | 2025-09-04 | – | – | – |
| U-0535 | 2025-09-04 | – | – | – |
| U-0542 | 2025-09-04 | 2025-09-04 | 2025-09-04 | – |
| U-0632 | 2025-09-04 | 2025-09-04 | – | – |
| U-0738 | 2025-09-04 | – | – | – |
| U-0003 | 2025-09-05 | – | – | – |
| U-0040 | 2025-09-05 | – | – | – |
| U-0113 | 2025-09-05 | – | – | – |
| U-0165 | 2025-09-05 | – | – | – |
| U-0204 | 2025-09-05 | 2025-09-05 | – | – |
| U-0261 | 2025-09-05 | – | – | – |
| U-0295 | 2025-09-05 | – | – | – |
| U-0307 | 2025-09-05 | 2025-09-05 | 2025-09-05 | – |
| U-0400 | 2025-09-05 | – | – | – |
| U-0444 | 2025-09-05 | – | – | – |
| U-0555 | 2025-09-05 | 2025-09-05 | – | – |
| U-0574 | 2025-09-05 | 2025-09-06 | 2025-09-06 | – |
| U-0591 | 2025-09-05 | – | – | – |
| U-0626 | 2025-09-05 | – | – | – |
| U-0733 | 2025-09-05 | 2025-09-07 | – | – |
| U-0081 | 2025-09-06 | – | – | – |
| U-0133 | 2025-09-06 | – | – | – |
| U-0192 | 2025-09-06 | – | – | – |
| U-0209 | 2025-09-06 | – | – | – |
| U-0225 | 2025-09-06 | – | – | – |
First 50 of 717 rows.
Step 3
Count users who reached each step in order
[Step date] is a CASE that returns [Visit date] on visit rows, [Signup date] on signup rows, and so on, so only the event that counts for the funnel matches. Every bar is now a subset of the one before it, and the drop between two bars is real drop-off.
Tableau
COUNTD(IF [event_date] = [Step date] THEN [user_id] END)SQL
WITH v AS (
SELECT user_id, MIN(event_date) AS visit_date
FROM input
WHERE event = 'visit'
GROUP BY user_id
HAVING '{knob.cohort}' = 'All visitors' OR MIN(event_date) <= (
SELECT MAX(event_date)
FROM input
) - 7
), s AS (
SELECT v.user_id, v.visit_date, MIN(e.event_date) AS signup_date
FROM v JOIN input e ON e.user_id = v.user_id AND e.event = 'signup' AND e.event_date BETWEEN v.visit_date AND v.visit_date + 7
GROUP BY v.user_id, v.visit_date
), b AS (
SELECT s.user_id, s.visit_date, MIN(e.event_date) AS box_date
FROM s JOIN input e ON e.user_id = s.user_id AND e.event = 'build_box' AND e.event_date BETWEEN s.signup_date AND s.visit_date + 7
GROUP BY s.user_id, s.visit_date
), p AS (
SELECT b.user_id, MIN(e.event_date) AS subscribe_date
FROM b JOIN input e ON e.user_id = b.user_id AND e.event = 'subscribe' AND e.event_date BETWEEN b.box_date AND b.visit_date + 7
GROUP BY b.user_id
), steps AS (
SELECT 1 AS step_no, 'Visit' AS step, COUNT(*) AS users
FROM v
UNION ALL
SELECT 2, 'Sign up', COUNT(*)
FROM s
UNION ALL
SELECT 3, 'Build a box', COUNT(*)
FROM b
UNION ALL
SELECT 4, 'Subscribe', COUNT(*)
FROM p
)
SELECT step_no, step, users : : INTEGER AS users
FROM steps
ORDER BY step_noRows
| Step no | Step | Users |
|---|---|---|
| 1 | Visit | 717 |
| 2 | Sign up | 294 |
| 3 | Build a box | 161 |
| 4 | Subscribe | 60 |
Step 4
Add step-to-step and overall conversion
LOOKUP(..., -1) reads the step on the left, so each rate is that step's users over the previous step's. LOOKUP(..., FIRST()) reads the first step, which gives the share of all visitors. In SQL these are LAG and FIRST_VALUE over the step number. Both depend on the steps being in funnel order.
Tableau
[Strict users] / LOOKUP([Strict users], -1)
// [Strict users] is the COUNTD from the step before; use LOOKUP([Strict users], FIRST()) for the share of visitors; Compute Using: StepSQL
WITH v AS (
SELECT user_id, MIN(event_date) AS visit_date
FROM input
WHERE event = 'visit'
GROUP BY user_id
HAVING '{knob.cohort}' = 'All visitors' OR MIN(event_date) <= (
SELECT MAX(event_date)
FROM input
) - 7
), s AS (
SELECT v.user_id, v.visit_date, MIN(e.event_date) AS signup_date
FROM v JOIN input e ON e.user_id = v.user_id AND e.event = 'signup' AND e.event_date BETWEEN v.visit_date AND v.visit_date + 7
GROUP BY v.user_id, v.visit_date
), b AS (
SELECT s.user_id, s.visit_date, MIN(e.event_date) AS box_date
FROM s JOIN input e ON e.user_id = s.user_id AND e.event = 'build_box' AND e.event_date BETWEEN s.signup_date AND s.visit_date + 7
GROUP BY s.user_id, s.visit_date
), p AS (
SELECT b.user_id, MIN(e.event_date) AS subscribe_date
FROM b JOIN input e ON e.user_id = b.user_id AND e.event = 'subscribe' AND e.event_date BETWEEN b.box_date AND b.visit_date + 7
GROUP BY b.user_id
), steps AS (
SELECT 1 AS step_no, 'Visit' AS step, COUNT(*) AS users
FROM v
UNION ALL
SELECT 2, 'Sign up', COUNT(*)
FROM s
UNION ALL
SELECT 3, 'Build a box', COUNT(*)
FROM b
UNION ALL
SELECT 4, 'Subscribe', COUNT(*)
FROM p
)
SELECT
step_no,
step,
users : : INTEGER AS users,
ROUND(users / LAG(users) OVER (ORDER BY step_no), 3) AS step_conversion,
ROUND(users / FIRST_VALUE(users) OVER (ORDER BY step_no), 3) AS share_of_visitors
FROM steps
ORDER BY step_noRows
| Step no | Step | Users | Step conversion | Share of visitors |
|---|---|---|---|---|
| 1 | Visit | 717 | – | 100% |
| 2 | Sign up | 294 | 41% | 41% |
| 3 | Build a box | 161 | 54.8% | 22.5% |
| 4 | Subscribe | 60 | 37.3% | 8.4% |