The Upgrade Divide

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

Domain
SQL
Difficulty
medium
Seniority
L4
Asked in
Apple

Problem

Our growth team wants to know how far the latest phone releases, iOS or Android 16, 17, and 18, have spread across our age groups. For each age group, show the number of users who ran a session on one of those releases next to the total number of users with any session.

The install numbers don't match the hype.

Tables

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

devices

Columns: device_id INTEGER, device_type TEXT, os_name TEXT, os_version TEXT, browser TEXT

devices: 10 of 200 rows
device_iddevice_typeos_nameos_versionbrowser
201desktopAndroid15.1Chrome
230tabletWindows16.2Firefox
259smart_tvmacOS17.0Edge
288wearableLinux11Opera
317mobiletvOS12Samsung Internet
346desktopwatchOS13NULL
375tabletiOS10.0.19041Safari
404smart_tvAndroid14.1Chrome
433wearableWindowsNULLFirefox
462mobilemacOS14.0Edge

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 6 rows
age_bucketupgraded_userstotal_users
18-2408
25-3407
35-4418
45-5419
65+49

Worked solution and explanation

What this really is

This is a conditional segment count wearing an adoption-metrics costume. The real question: can you put a filtered count and a grand total side by side, over the same groups, in one pass, without letting the session table double count anyone? Three things separate the strong answers. First, a user shows up once per session, so a plain COUNT inflates every bucket: you need COUNT(DISTINCT user_id). Second, 'on a recent release' means a mobile OS name AND a version 16/17/18, because version strings like '16.2' also live on Windows, macOS and Linux rows: match the version alone and desktop platforms leak straight into your number. Third, the total is every user who logged a session, even one whose device we never captured, so you reach devices with a LEFT join and keep those users in the denominator instead of silently dropping them.


Building it

Step 1: Reach devices through sessions

You can only read os_name and os_version by going through user_sessions, which links a user_id to a device_id. That path is one-to-many: a user with forty sessions becomes forty rows here. Hold onto that fact, because it is the source of every double count in this problem.

Step 2: Count the upgraded slice and the total together

COUNT(DISTINCT u.user_id) collapses the session fan-out back to real people. For the upgraded slice, wrap the same idea in a CASE that returns user_id only when the device is a phone platform, (d.os_name = 'iOS' OR d.os_name = 'Android'), AND its os_version starts with 16, 17, or 18; the DISTINCT then counts each qualifying user once even when they ran several matching sessions. Prefix matching with LIKE '16%' catches '16.2' and '16.0.1' without hard coding every point release. Mind the parentheses: the OS group and the version group are each an OR, joined by AND, so both must be wrapped or precedence quietly widens the match. Join devices with a LEFT join: a session whose device_id we never captured contributes nothing to the upgraded count but still keeps its user in the total.

Step 3: Drop the missing bucket, then order

Rows with a null age_bucket are not an age group, so filter them in WHERE before grouping. Because you reach the buckets through the inner join to user_sessions, only groups with at least one session survive, which is exactly the requirement. Then order by total_users descending, largest groups first, and break ties on age_bucket descending so the order is stable and matches the preview.

Conditional count over a three-table join
SELECT u.age_bucket,
    COUNT(DISTINCT CASE
        WHEN (d.os_name = 'iOS' OR d.os_name = 'Android')
         AND (d.os_version LIKE '16%' OR d.os_version LIKE '17%' OR d.os_version LIKE '18%')
        THEN u.user_id
    END) AS upgraded_users,
    COUNT(DISTINCT u.user_id) AS total_users
FROM users u
JOIN user_sessions s ON u.user_id = s.user_id
LEFT JOIN devices d ON s.device_id = d.device_id
WHERE u.age_bucket IS NOT NULL
GROUP BY u.age_bucket
Version only (wrong)
COUNT(DISTINCT CASE WHEN d.os_version LIKE '16%' OR ... THEN u.user_id END). Counts every platform that ever shipped a version 16, so Windows, macOS and Linux users inflate the mobile-upgrade number.
Mobile OS plus version (right)
COUNT(DISTINCT CASE WHEN (d.os_name = 'iOS' OR d.os_name = 'Android') AND (d.os_version LIKE '16%' OR ...) THEN u.user_id END). Scopes the count to phone platforms on the target releases, which is what upgrade adoption actually means.

Common follow-up questions