7-Day Onboarding Conversion

A hard SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.

Domain
SQL
Difficulty
hard
Seniority
L5
Asked in
General Assembly

Problem

What share of the experiments entries made from 2023 onward led to a meaningful session in user_sessions, one with a recorded duration above zero, within 30 days of the entry date? The growth team reads it per variant, alphabetically, as total entries, converted entries and a conversion percentage that keeps its fractional part.

Signed up Monday. Still here by Sunday?

Tables

experiments

Columns: exp_id INTEGER, exp_name TEXT, variant TEXT, user_id INTEGER, outcome REAL, created TEXT, platform TEXT

experiments: 10 of 200 rows
exp_idexp_namevariantuser_idoutcomecreatedplatform
1053checkout_v3variant_a10011.32026-02-02android
1106onboard_flowvariant_b19722.62026-03-03web
1159pricing_tiervariant_c29433.92026-04-04iOS
1212search_rankholdout39145.22026-05-05Android
1265rec_model_bcontrol48856.52026-06-06NULL
1318notif_freqvariant_a585NULL2026-07-07WEB
1371home_layoutvariant_b68279.12026-08-08NULL
1424new_feed_algovariant_c77990.42026-09-09ios
1477checkout_v3holdout8761.72026-01-10android
1530onboard_flowcontrol973132026-02-11web

user_sessions

Columns: session_id INTEGER, user_id INTEGER, device_id INTEGER, session_start TEXT, session_duration_sec INTEGER, pages_viewed INTEGER

user_sessions: 10 of 200 rows
session_iduser_iddevice_idsession_startsession_duration_secpages_viewed
70611972302026-02-02 01:00:00533
71222942592026-03-03 02:00:00766
71833912882026-04-04 03:00:00999
72444883172026-05-05 04:00:0012212
73055853462026-06-06 05:00:0014515
73666823752026-07-07 06:00:0016818
74277794042026-08-08 07:00:0019121
7488876NULL2026-09-09 08:00:0021424
75499734622026-01-10 09:00:0023727
761010704912026-02-11 10:00:0026030

Expected output

First 5 of 5 rows
varianttotal_entriesconverted_entriesconversion_rate
control40410
holdout40512.5
variant_a20315
variant_b4037.5
variant_c40410

Worked solution and explanation

What this really is

This is an existence check per cohort member, dressed up as an experiment readout. The skill being probed is whether you can keep the population fixed while you score its behavior. Anyone can join experiments to user_sessions. The trap is where the 30-day window and the duration test live. Put either one in WHERE and every entry without a qualifying session drops out of the denominator. Since most entries on this data never come back inside their window, every variant then reports a conversion rate near 100%.

Step 1: Pin the cohort before you look at behavior

The population is every experiments row created from 2023 onward, one member per exp_id. A user who entered two experiments is two attempts, each with its own start date, so the grain is the entry, not the user. Fix that set in its own CTE, and nothing downstream is allowed to shrink it.

Step 2: Put the window in the ON clause

The window depends on both sides: the session day minus that entry's entry_date. That makes it a join condition. Inside ON, it only limits which sessions attach, and an entry with no sessions still survives as a single row of NULLs. Moved to WHERE, those NULL rows fail the comparison and disappear.

Step 3: Collapse each entry to one flag

COUNT(CASE WHEN us.session_duration_sec > 0 THEN 1 END) counts only real sessions. The inner CASE yields NULL for a missing duration and for the padding row an unmatched entry carries, and COUNT skips NULLs. The outer CASE turns that count into a clean 0 or 1, however many sessions the entry had.

Step 4: Divide in decimal

SUM(converted) and COUNT(*) are both integers. Integer division turns 3 converters out of 31 into 9 instead of 9.677. Casting the numerator before the division keeps the fraction the readout depends on.

Cohort, flag, total
WITH entrants AS (
    SELECT exp_id, variant, user_id, DATE(created) AS entry_date
    FROM experiments
    WHERE created >= '2023-01-01'
),
entry_outcomes AS (
    SELECT
        en.exp_id,
        en.variant,
        CASE
            WHEN COUNT(CASE WHEN us.session_duration_sec > 0 THEN 1 END) > 0
            THEN 1 ELSE 0
        END AS converted
    FROM entrants en
    LEFT JOIN user_sessions us
        ON us.user_id = en.user_id
       AND julianday(DATE(us.session_start)) - julianday(en.entry_date) BETWEEN 0 AND 30
    GROUP BY en.exp_id, en.variant
)
SELECT
    variant,
    COUNT(*) AS total_entries,
    SUM(converted) AS converted_entries,
    CAST(SUM(converted) AS REAL) * 100 / COUNT(*) AS conversion_rate
FROM entry_outcomes
GROUP BY variant
ORDER BY variant
Window in WHERE
The LEFT JOIN pads non-converters with NULL session_start, then WHERE throws those rows away. total_entries collapses to the converters, and every rate reads 100.
Window in ON
Unmatched entries keep their NULL row through the join. The inner CASE scores them 0, COUNT(*) still counts them, and the denominator is the whole cohort.

Common follow-up questions