Age of Discovery

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

Problem

The search team wants to see how well different age cohorts find what they're after on our shopping platform. For every cohort with an age band on file, show the total searches, how many of those clicked through to a result, and the success rate as a decimal, listed alphabetically by cohort.

Every generation searches differently. See who finds what they came for.

Tables

search_queries

Columns: query_id INTEGER, user_id INTEGER, search_term TEXT, results_count INTEGER, clicked_result INTEGER, query_time TEXT

search_queries: 10 of 200 rows
query_iduser_idsearch_termresults_countclicked_resultquery_time
2071100laptop deals1722026-02-02 01:11:00
2142197phone case iphone 153432026-03-03 02:22:00
2213294usb-c cable 6ft51NULL2026-04-04 03:33:00
2284NULLgaming mouse6852026-05-05 04:44:00
2355488mechanical keyboard8562026-06-06 05:55:00
24265854k monitor102NULL2026-07-07 06:06:00
2497682standing desk11982026-08-08 07:17:00
2568NULLwebcam 1080p13692026-09-09 08:28:00
2639876bluetooth speaker153NULL2026-01-10 09:39:00
2710973smart watch17012026-02-11 10:50:00

users

Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT

users: 10 of 200 rows
user_idusernameemailsignup_dateaccount_statusage_bucket
100alicealice@example.com2025-02-02inactive25-34
197aaron42aaron42@example.com2026-03-03suspended35-44
294 ameliaamelia@example.com2024-04-04pending_verification45-54
391arjun arjun@example.com2025-05-05active55-64
488ava99ava99@example.com2026-06-06inactive65+
585andrew_kandrew_k@example.com2024-07-07suspendedNULL
682anikaanika@example.com2025-08-08 16:34:00pending_verification18-24
779aidenaiden@example.com2026-09-09 14:53:00active25-34
876aria_baria_b@example.com2024-10-10inactive35-44
973brianbrian@example.com2025-11-11suspended45-54

Expected output

First 5 of 6 rows
age_buckettotal_searchessuccessful_searchessuccess_rate
18-2422100.45454545454545453
25-3422140.6363636363636364
35-4422140.6363636363636364
55-6420140.7
65+20140.7

Worked solution and explanation

What you're actually being asked

Strip the shopping-platform costume and this is a per-group ratio: for each cohort, successful searches over total searches. Anyone can write the grouping. Two quieter decisions separate the candidates who get hired. First, which searches are even allowed into the denominator. Second, keeping that ratio in floating point so it doesn't collapse to zero.

The build, step by step

Step 1: Decide which rows count

Two kinds of noise sit in search_queries. Rows with a null user_id can't be tied to anyone, and users with no age band on file can't be placed in a cohort. An inner join to users drops the orphan searches for free, and a WHERE u.age_bucket IS NOT NULL removes the recorded-but-blank cohort. Both belong in the scoping, not the SELECT list.

Step 2: Count success conditionally

A search is successful when the user clicked through, which the data records as a non-null clicked_result (the position they clicked, so 1, 2, 8, whatever). SUM(CASE WHEN sq.clicked_result IS NOT NULL THEN 1 ELSE 0 END) counts exactly those, alongside COUNT(*) for the denominator. Reading clicked_result = 1 instead would only catch people who clicked the top result and quietly undercount every cohort.

Step 3: Divide in floating point

Take the conditional sum, cast it to REAL, and divide by COUNT(*). Grouping by age_bucket collapses everything to one row per cohort, and ordering by the label lists them 18-24 first through 65+ last.

Success rate per age cohort
SELECT
    u.age_bucket,
    COUNT(*) AS total_searches,
    SUM(CASE WHEN sq.clicked_result IS NOT NULL THEN 1 ELSE 0 END) AS successful_searches,
    CAST(SUM(CASE WHEN sq.clicked_result IS NOT NULL THEN 1 ELSE 0 END) AS REAL)
        / COUNT(*) AS success_rate
FROM search_queries sq
INNER JOIN users u ON sq.user_id = u.user_id
WHERE u.age_bucket IS NOT NULL
GROUP BY u.age_bucket
ORDER BY u.age_bucket;

One pass, one join: scope the rows, count conditionally, cast then divide.

Naive
LEFT JOIN users, no age_bucket filter, and successful_searches / total_searches. Orphan and blank-cohort searches leak into the counts, and integer division flattens every rate to 0.
Correct
INNER JOIN plus WHERE age_bucket IS NOT NULL scopes the rows, and CAST(... AS REAL) keeps the ratio a decimal. Counts and rates both hold up.

Common follow-up questions