API Token Churn Rate
An easy SQL interview practice problem on DataDriven. Write the query and run it against a real database, with instant feedback.
- Domain
- SQL
- Difficulty
- easy
- Seniority
- L3
- Asked in
- Lyft
Problem
Compute the churn rate of the issued tokens in api_tokens as a decimal fraction between 0 and 1, for the platform health dashboard. A token has churned once its expiration date is in the past; a token with no expiration date never lapses, so it stays active while still counting as issued.
Tokens come and go. What's the turnover?
Tables
api_tokens
Columns: token_id INTEGER, owner_id INTEGER, scope TEXT, status TEXT, issued TEXT, expires TEXT, last_used TEXT, requests INTEGER
| token_id | owner_id | scope | status | issued | expires | last_used | requests |
|---|---|---|---|---|---|---|---|
| 1043 | 197 | write | revoked | 2025-02-02 | 2027-02-02 | 2026-02-02 | 47 |
| 1086 | 294 | admin | expired | 2025-03-03 | 2027-03-03 | 2026-03-03 | 94 |
| 1129 | 391 | read:users | Active | 2025-04-04 | 2027-04-04 | 2026-04-04 | 141 |
| 1172 | 488 | write:orders | REVOKED | 2025-05-05 | 2027-05-05 | 2026-05-05 | 188 |
| 1215 | 585 | read:analytics | active | 2025-06-06 | 2027-06-06 | 2026-06-06 | 235 |
| 1258 | 682 | full_access | revoked | 2025-07-07 | NULL | 2026-07-07 | 282 |
| 1301 | 779 | read | expired | 2025-08-08 | 2027-08-08 | 2026-08-08 | 329 |
| 1344 | 876 | write | Active | 2025-09-09 | 2027-09-09 | NULL | 0 |
| 1387 | 973 | admin | REVOKED | 2025-10-10 | 2027-10-10 | 2026-01-10 | 423 |
| 1430 | 1070 | read:users | active | 2025-11-11 | 2027-11-11 | 2026-02-11 | 470 |
Expected output
| churn_rate |
|---|
| 0.42 |
Worked solution and explanation
What this really is
This is a three-valued-logic problem dressed up as a dashboard metric. The ratio is one line; what separates candidates is the 32 tokens with a NULL in expires. Define churn as everything that is not still live and a missing expiry makes the live test unknown, the CASE falls through to its ELSE, and every never-expiring token lands in the churned pile: the rate jumps from 0.42 to 0.58. The status column is the second bait, and it is wrong too.
Step 1: Keep every issued token in the denominator
COUNT(*) over api_tokens is the cohort: all 200 issued tokens, never-expiring ones included. Filtering to expires IS NOT NULL first would shrink it to 168 and inflate the rate.
Step 2: Define live before churned
A token is live when it has no expiry at all or its expires is today or later. Churned is everything else, which is why the churn flag is the ELSE 1 branch rather than a condition of its own.
Step 3: Catch the missing expiry by name
expires >= CURRENT_DATE is unknown, not false, when expires is NULL, and an unknown WHEN drops to ELSE. The expires IS NULL OR arm is what keeps those 32 tokens live.
Step 4: Force real division
SUM(...) and COUNT(*) are both integers, so the engine divides integers and returns 0. Casting the numerator with CAST(... AS REAL) makes the ratio a decimal.
SELECT
CAST(SUM(CASE
WHEN expires IS NULL OR expires >= CURRENT_DATE THEN 0
ELSE 1
END) AS REAL) / COUNT(*) AS churn_rate
FROM api_tokens- Flag churn directly
CASE WHEN expires < CURRENT_DATE THEN 1 ELSE 0 ENDsurvives without a guard: theNULLcomparison is unknown, falls toELSE 0, and never-expiring tokens are never churned by accident.- Flag churn as not live
CASE WHEN expires >= CURRENT_DATE THEN 0 ELSE 1 ENDsends the same unknown toELSE 1. Here theexpires IS NULLarm is mandatory; drop it and the rate reads 0.58.
Common follow-up questions
- How would you split churn by `scope`? (Probes whether you add a grouping while keeping the same conditional-sum shape.)
- What if the team wants churn for tokens issued in the last 90 days only? (Tests cohort filtering on `issued` without breaking the never-expires logic.)
- How would you express this as a 30-day rolling churn rate? (Pushes you toward a date series or buckets over `expires`.)
- What changes if `expires` is a timestamp instead of a date? (Checks whether a token expiring later today is still treated as live.)