Readineer
PATTERN 22SQL Patterns / 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 predictions
01Foundation

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:

Events
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.

02The 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 counts
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 article, 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.

03Building 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_view
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_cart
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_checkout
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_purchase
userpurchase_at
Asha09:12

Only Asha completed the complete ordered funnel.

04The 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 & PREDICTHow many users actually completed View then Cart then Checkout then Purchase in that order?
Query
-- Dashboard (independent counts):
-- Viewed     4
-- Cart       3
-- Checkout   2
-- Purchased  2

-- A: 4    B: 3    C: 2    D: 1
Your prediction

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 funnel
viewed_userscart_userscheckout_userspurchase_users
4311
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.
05Rates

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.

06Definitions 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.

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.”

07Recognition 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.
08A 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.
09Check 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 events
event_atevent_name
10:00view_product
10:03checkout_started
10:08add_to_cart
ONE MORE PREDICTIONDoes Ben reach the Checkout stage of this ordered funnel?
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
Your prediction
10Summary

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.

Ask Colearn

3 of 3 free questions left

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

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.

from the unit →
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.

from the unit →
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.

from the unit →
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.

from the unit →

That’s the free taste

Two ways to go deeper.

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

Related patterns

P23Pattern · 12 minCohorts & retentionThe other event-analysis pattern: measure whether users come back over time.P10Pattern · 9 minSubqueries & CTEsThe stage-by-stage CTE chain each funnel step is built from.P11Pattern · 7 minDates & timestampsThe completion-window condition that bounds a funnel to seven days.
Up next · Pattern 23
Cohort 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.

Continue to P23 →Browse all patterns