Practice6problems on this pattern, easy to hardStart practising →
Pattern 22SQL course / Advanced SQL

SQL funnels and conversion analysis: measuring progress through ordered steps

Learn how to build ordered funnels from event data, calculate stage and overall conversion rates, measure drop-off, handle repeated events, and avoid counting users who completed steps in the wrong order.

8 min read10 sections2 predictions6 practice problems
01 · Foundation

What is a funnel?

Many products want to understand how users progress through a sequence of actions. For an online store, a purchase funnel might be view product, then add to cart, then start checkout, then purchase. Not every viewer adds to cart, not every cart begins checkout, and not every checkout completes. A funnel answers how many users reached each stage, where they drop out, and what percentage complete the journey. Consider the following event data:

Events11 rows
event_iduserevent_atevent_name
1Asha2026-07-01 09:00view_product
2Asha2026-07-01 09:05add_to_cart
3Asha2026-07-01 09:10checkout_started
4Asha2026-07-01 09:12purchase
5Ben2026-07-01 10:00view_product
6Ben2026-07-01 10:03checkout_started
7Ben2026-07-01 10:08add_to_cart
8Carla2026-07-01 11:00view_product
9Carla2026-07-01 11:04add_to_cart
10Dev2026-07-01 12:00purchase
11Esha2026-07-01 13:00view_product

The intended funnel is view_product then add_to_cart then checkout_started then purchase. At first glance it may seem enough to count how many users performed each event, but a funnel has another requirement: the stages must happen in the correct order. That changes the analysis.

02 · The naive count

Counting each event independently

We could begin with conditional aggregation, counting distinct users per event:

Query
SELECT
COUNT(DISTINCT CASE WHEN event_name = 'view_product' THEN user END) AS viewed_users,
COUNT(DISTINCT CASE WHEN event_name = 'add_to_cart' THEN user END) AS cart_users,
COUNT(DISTINCT CASE WHEN event_name = 'checkout_started' THEN user END) AS checkout_users,
COUNT(DISTINCT CASE WHEN event_name = 'purchase' THEN user END) AS purchase_users
FROM events;
Independent counts1 row
viewed_userscart_userscheckout_userspurchase_users
4322

The numbers look reasonable, but look at the users. Asha completed view, cart, checkout, purchase in order. Ben performed three funnel events, but checkout happened before add-to-cart. Dev has a purchase event but no earlier funnel events. So the independent counts say two users purchased, but only one user actually completed view then cart then checkout then purchase in the required order.

Key idea

Performing a stage is not the same as reaching that stage through the funnel.

For this lesson, a user reaches View with a view_product event, Cart when add_to_cart occurs after their qualifying view, Checkout when checkout_started occurs after their qualifying cart, and Purchase when purchase occurs after their qualifying checkout. The funnel therefore requires view_at < cart_at < checkout_at < purchase_at, and each stage depends on the previous one.

03 · Building the funnel

Each stage after the previous qualifying stage

Step 1: find the first funnel entry. First, find each user’s first product view:

Query
WITH first_view AS (
SELECT
user,
MIN(event_at) AS view_at
FROM events
WHERE event_name = 'view_product'
GROUP BY user
)
SELECT * FROM first_view
ORDER BY user;
first_view4 rows
userview_at
Asha09:00
Ben10:00
Carla11:00
Esha13:00

Dev does not appear because no qualifying view exists. Four users entered the funnel.

Step 2: find the first cart event after the view

Now find an add_to_cart event that occurs after each user’s view:

Query
first_cart AS (
SELECT
e.user,
MIN(e.event_at) AS cart_at
FROM events AS e
JOIN first_view AS v
ON v.user = e.user
WHERE e.event_name = 'add_to_cart'
AND e.event_at > v.view_at
GROUP BY e.user
)
first_cart3 rows
usercart_at
Asha09:05
Ben10:08
Carla11:04

Three users reached the second stage. Ben still qualifies for Cart: his cart (10:08) occurred after his view (10:00). The fact that he also had a checkout event at 10:03 does not stop the cart from qualifying. In this example we only require the qualifying funnel stages themselves to occur in order.

Step 3: find checkout after cart

Now the checkout must occur after the qualifying cart event (e.event_at > c.cart_at):

first_checkout1 row
usercheckout_at
Asha09:10

Ben no longer qualifies. His checkout was at 10:03, but his qualifying cart was 10:08, so checkout_at < cart_at. The stage happened, but not in the required funnel order.

Step 4: find purchase after checkout

Finally, the purchase must occur after the qualifying checkout (e.event_at > c.checkout_at):

first_purchase1 row
userpurchase_at
Asha09:12

Only Asha completed the complete ordered funnel.

04 · The featured failure

Counting stage events without enforcing the funnel order

Suppose a dashboard reports Viewed 4, Cart 3, Checkout 2, Purchased 2. Those values came from counting users who performed each event independently.

Pause and predictPredict before you continue

How many users actually completed View then Cart then Checkout then Purchase in that order?

The query
-- Dashboard (independent counts):
-- Viewed 4
-- Cart 3
-- Checkout 2
-- Purchased 2
-- A: 4 B: 3 C: 2 D: 1

Returning ordered funnel counts in one result

Once the stage CTEs are defined, the counts can be returned together. Now the numbers represent ordered progression, not independent occurrence:

Query
WITH first_view AS (
SELECT user, MIN(event_at) AS view_at
FROM events
WHERE event_name = 'view_product'
GROUP BY user
),
first_cart AS (
SELECT e.user, MIN(e.event_at) AS cart_at
FROM events AS e
JOIN first_view AS v ON v.user = e.user
WHERE e.event_name = 'add_to_cart'
AND e.event_at > v.view_at
GROUP BY e.user
),
first_checkout AS (
SELECT e.user, MIN(e.event_at) AS checkout_at
FROM events AS e
JOIN first_cart AS c ON c.user = e.user
WHERE e.event_name = 'checkout_started'
AND e.event_at > c.cart_at
GROUP BY e.user
),
first_purchase AS (
SELECT e.user, MIN(e.event_at) AS purchase_at
FROM events AS e
JOIN first_checkout AS c ON c.user = e.user
WHERE e.event_name = 'purchase'
AND e.event_at > c.checkout_at
GROUP BY e.user
)
SELECT
(SELECT COUNT(*) FROM first_view) AS viewed_users,
(SELECT COUNT(*) FROM first_cart) AS cart_users,
(SELECT COUNT(*) FROM first_checkout) AS checkout_users,
(SELECT COUNT(*) FROM first_purchase) AS purchase_users;
Ordered funnel1 row
viewed_userscart_userscheckout_userspurchase_users
4311
Watch out
A funnel should normally be monotonic: Stage 1 count >= Stage 2 count >= Stage 3 count >= Stage 4 count, because every later stage requires the previous one. If a downstream stage contains more users than an earlier required stage, that is a warning sign (stages counted independently, different populations, incorrect joins, duplicate events, or a wrong time window). A monotonic funnel is not proof the logic is correct, but a non-monotonic strict funnel is usually worth investigating.
05 · Rates

Conversion and drop-off

Raw stage counts are useful, but conversion rates make the funnel easier to interpret. Each stage-to-stage rate uses the previous stage as the denominator: View to Cart is 3 / 4 = 75%, Cart to Checkout is 1 / 3 = 33.3%, Checkout to Purchase is 1 / 1 = 100%. Overall funnel conversion asks what percentage of entrants eventually purchased, purchase / view = 1 / 4 = 25%, which is different from the 100% checkout-to-purchase rate. Both metrics are useful; they answer different questions.

Query
SELECT
viewed_users,
cart_users,
checkout_users,
purchase_users,
100.0 * cart_users / NULLIF(viewed_users, 0) AS view_to_cart_pct,
100.0 * checkout_users / NULLIF(cart_users, 0) AS cart_to_checkout_pct,
100.0 * purchase_users / NULLIF(checkout_users, 0) AS checkout_to_purchase_pct,
100.0 * purchase_users / NULLIF(viewed_users, 0) AS overall_conversion_pct
FROM funnel_counts;

100.0 ensures decimal arithmetic where integer division would otherwise truncate, and NULLIF(..., 0) protects against division by zero when a stage has no users.

Drop-off

Conversion and drop-off describe the same transition from opposite directions. If 100 users reach View and 70 reach Cart, conversion is 70% and drop-off is 30%. In counts, drop-off = previous stage users - next stage users. For our View to Cart step, 4 - 3 = 1 user dropped; for Cart to Checkout, 3 - 1 = 2 users failed to reach Checkout after qualifying for Cart.

06 · Definitions that change the answer

Repeated events, grain, and time windows

Repeated events. Real event streams repeat actions (view, view, view, cart, cart, checkout). The funnel should not count that person three times at View, which is why each stage uses MIN(event_at) per user: it finds one qualifying timestamp so the user becomes one funnel participant. Event count is not user count.

Key idea

The first event is not always the right event. A user may have several attempts (an abandoned journey in the morning, a completed one in the afternoon). The business must decide whether the funnel means the first attempt, any successful attempt, one funnel per session, or one per product. SQL cannot choose; the funnel definition must come first.

User-level vs session-level vs product-level

A user who views on Monday and completes cart, checkout, and purchase on Friday completes a user-level funnel, but not a “purchase journey in one session” funnel. The correct grain might be user or user + session_id (a session funnel might need PARTITION BY user, session_id or equivalent grouping). Product funnels add another subtlety: if Asha views a Mouse but adds and purchases a Keyboard, a user-level funnel reports View to Cart to Purchase even though the stages concern different products. If the question is “how often does a viewed product become a purchased product,” the key must be user + product_id. Without the correct entity key, SQL can connect unrelated events into one artificial funnel.

Completion windows and the time-window trap

Many funnels require completion within a time limit, such as purchase within seven days of the initial view. The purchase stage then needs another condition, AND e.event_at <= v.view_at + INTERVAL '7 days', so the funnel requires correct stage order and completion within the window.

Watch out
Suppose two teams calculate “View to Purchase conversion.” Team A allows purchase at any future time; Team B allows purchase within 7 days. Their rates can differ substantially even with identical data. Neither query is wrong; they are measuring different funnels. A funnel metric is incomplete unless the stage and time rules are known. The same applies to cohort boundaries: a user who views on July 31 and purchases on August 2 counts under an entry-cohort definition (entered in July, conversions allowed afterward) but not under an event-window definition (every stage must occur in July).

Strict vs loose, and segments

A loose ordered funnel allows unrelated events between required stages; only the required stages must occur in order. A stricter funnel requires the events to occur consecutively with no intervening conflicting activity. The queries here use a loose ordered funnel. Funnels also become more useful compared across segments (mobile vs web, or new vs returning users), but the segmentation attribute should have a clear meaning, for example “channel at first funnel entry” rather than “some channel associated with the user.”

07 · Recognition cues

How to spot funnel problems

Think about funnel analysis when the requirement says users who progressed from one action to another, conversion rate, checkout or signup funnel, onboarding or activation steps, drop-off between stages, users who completed all steps, or completion within a time limit.

Watch for these common mistakes:

  • Stages are counted independently rather than sequentially.
  • Later-stage users did not complete earlier stages.
  • Repeated events inflate stage counts.
  • User-level events from different sessions are combined accidentally.
  • Events for different products are connected into one journey.
  • No time window is defined.
  • The denominator for conversion is unclear.
  • The entry cohort and completion period are mixed together.
  • Different teams use different funnel definitions for the same metric name.
08 · A practical mental model

Answer six questions before writing funnel SQL

1 · What entity is progressing?

user, user + session, or user + product.

2 · What are the required stages?

For example View, Cart, Checkout, Purchase.

3 · Must the stages occur in order?

If yes, view_at < cart_at < checkout_at < purchase_at.

4 · Can unrelated events occur between stages?

This determines whether the funnel is loose or strict.

5 · Is there a completion window?

For example purchase within 7 days of view.

6 · What is each conversion's denominator?

Stage: next / previous. Overall: final / entry.

Key idea

Once these six questions are clear, the SQL becomes much easier to trust.

09 · Check your understanding

One more prediction

Consider Ben’s events below. The required funnel is View then Add to Cart then Checkout. Does Ben reach the Checkout stage of this ordered funnel?

Ben's events3 rows
event_atevent_name
10:00view_product
10:03checkout_started
10:08add_to_cart
Pause and predictPredict before you continue

Does Ben reach the Checkout stage of this ordered funnel?

The query
Required: View -> Add to Cart -> Checkout
-- A: yes, because a checkout event exists
-- B: yes, because all three event types exist
-- C: no, checkout happened before the qualifying cart event
-- D: no, because every funnel must end in purchase
10 · Summary

Summary

A funnel is not simply a collection of event counts; it represents ordered progression. For View then Cart then Checkout then Purchase, the required timestamps satisfy view_at < cart_at < checkout_at < purchase_at, so each stage is built from the previous qualifying stage, for example WHERE e.event_name = 'add_to_cart' AND e.event_at > v.view_at, then checkout after cart, then purchase after checkout. This prevents a user from being counted at a downstream stage merely because the event occurred somewhere in their history.

Key idea

Define the journey before counting the events. The definition should specify the entity (what moves through the funnel), the stages (which actions are required), the order (must they be sequential), the scope (same session, product, or journey), the time (how long is allowed), and the conversion denominator (which stage). If these rules are unclear, two valid SQL queries can calculate different “conversion rates” from the same event data. Reliable funnel analysis starts by making the progression rules explicit; then SQL can measure how many entities enter, advance, drop off, and finally convert.

Go deeper

Ask Colearn about this pattern

Not sure why your funnel counts users who skipped a step, or what grain and time window your funnel needs? Ask about the concept, or paste a simplified version of your query.

Instant answers, grounded in the same verified material the diagnostic grades against.

3 of 3 free questions left
Why is my funnel counting users who did the steps out of order?

Counting each event independently only checks that the event happened, not that it happened in funnel order. Performing a stage is not the same as reaching that stage through the funnel.

Build each stage from the previous qualifying stage, correlating on the same user and requiring a later timestamp: WHERE event_name = 'add_to_cart' AND event_at > v.view_at, then checkout after cart, then purchase after checkout.

Why use MIN(event_at) per user in a funnel stage?

Event streams repeat actions (view, view, view). MIN(event_at) per user finds one qualifying timestamp so the user becomes one funnel participant instead of several event rows.

Event count is not user count. If the funnel is user-based, repeated events from one user should not increase the number of participants.

How do I add a completion window, like purchase within 7 days?

Add a time condition to the final stage, for example AND e.event_at <= v.view_at + INTERVAL '7 days'. Now the funnel requires correct stage order and completion within the window.

A conversion rate is incomplete without its time window: 'purchase at any future time' and 'purchase within 7 days' measure different funnels from the same data.

Should my funnel be user-level or session-level?

It depends on the definition. A user who views on Monday and purchases on Friday completes a user-level funnel but not a 'purchase journey in one session' funnel.

Choose the grain deliberately: user, user + session_id, or user + product_id. The wrong key can connect unrelated events into one artificial funnel.

Don’t paste credentials, personal data, or confidential production records.

Practise this pattern

Now write it yourselfAll 6 Funnels & Conversion problems →

Graded problems on this pattern, easy to hard. They run in your browser against real tables – no sign-up needed.

Related patterns

Up next · Pattern 23Cohort and retention analysis: measuring whether users come back

Next, we stay in advanced SQL and learn how cohort and retention analysis measures whether users come back.