Time Served
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
- L4
- Asked in
- Yammer
Problem
Security is profiling the API tokens still in service, split out by permission scope. A token counts as in service when it has no expiration date, or its expiration is today or later. Within each scope, report the day span between its oldest and newest tokens, along with how many tokens were issued on that oldest day and how many on that newest day, widest span first.
The tokens still in service each carry a history; measure how far it stretches, scope by scope.
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
| scope | day_spread | tokens_at_earliest | tokens_at_latest |
|---|---|---|---|
| full_access | 1553 | 1 | 1 |
| read:users | 1293 | 1 | 1 |
| read:analytics | 1103 | 1 | 1 |
| write | 1103 | 1 | 1 |
| read | 907 | 1 | 1 |
Worked solution and explanation
What this really tests
Beneath the security framing this is a per-group min/max with tie counts, and two traps decide it. First, 'in service' is a NULL-aware predicate: a token with no expiration date never expires, so a plain expires >= today silently drops it because comparing anything to NULL yields NULL, not true. Second, the counts at each end have to stay inside their own scope. A tie count that forgets to correlate on scope will sweep in tokens from every scope and inflate both boundary columns. Anyone can write the CTEs; the separation is whether the exclusion of NULLs and the scoping of the tie counts are both correct.
Working through it
Step 1: Define the active population
Build active_tokens with WHERE expires IS NULL OR expires >= DATE('now'). The IS NULL branch keeps tokens that never expire; the >= today branch keeps tokens whose expiration is today or in the future. Drop the IS NULL branch and every never-expiring token vanishes from the population.
Step 2: Per-scope issue-date extremes
Group active_tokens by scope and take MIN(issued) and MAX(issued). Each scope gets one row carrying its earliest and latest issue dates. This is the grain of the final answer: one row per scope.
Step 3: Day span between the extremes
Convert the two dates to a whole-day gap: CAST(JULIANDAY(max_issued) - JULIANDAY(min_issued) AS INTEGER). JULIANDAY handles the calendar math and the CAST drops the fractional part. On Postgres the same idea is (max_issued - min_issued) cast to integer days.
Step 4: Count ties at each boundary, scoped
For each scope row, run a scalar subquery over active_tokens counting rows where issued = min_issued, and another where issued = max_issued. The key detail: both subqueries carry a.scope = b.scope so the count stays inside the current scope. Project scope, day_spread, tokens_at_earliest, tokens_at_latest, widest span first.
The solution
WITH active_tokens AS (
SELECT scope, issued
FROM api_tokens
WHERE expires IS NULL OR expires >= DATE('now')
),
bounds AS (
SELECT scope,
MIN(issued) AS min_issued,
MAX(issued) AS max_issued
FROM active_tokens
GROUP BY scope
)
SELECT
b.scope,
CAST(JULIANDAY(b.max_issued) - JULIANDAY(b.min_issued) AS INTEGER) AS day_spread,
(SELECT COUNT(*) FROM active_tokens a WHERE a.scope = b.scope AND a.issued = b.min_issued) AS tokens_at_earliest,
(SELECT COUNT(*) FROM active_tokens a WHERE a.scope = b.scope AND a.issued = b.max_issued) AS tokens_at_latest
FROM bounds b
ORDER BY day_spread DESC, b.scope- api_tokens: token_id int PK, scope text, status text, issued date, expires date
Common follow-up questions
- If issued were a TIMESTAMP instead of a DATE, what changes about the day span calculation? (Tests handling timestamps and timezones in date arithmetic.)
- How would you produce the same four columns using MIN, MAX, and COUNT with FILTER in a single grouped SELECT, without the scalar subqueries? (Tests collapsing the CTEs and subqueries into one grouped pass.)
- How would the query change if security wanted a single global spread across all active tokens instead of one row per scope? (Tests reasoning about the global rollup versus the per-scope grain.)
- If `api_tokens` grew to 100M rows, what index would let the active filter and the per-scope bounds avoid a full scan? (Tests partial-index design for selective filters at scale.)