Revoked Tokens by Scope
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
- Southwest Airlines
Problem
Count our revoked API tokens in api_tokens per scope, alphabetically by scope, keeping only those whose expires had not yet passed on December 1, 2025. A token with no expires never lapses, and status capitalization was never cleaned up, so 'REVOKED' and 'revoked' are the same.
Banned tokens, sorted by what they had access to.
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 | blocked_count |
|---|---|
| admin | 6 |
| full_access | 6 |
| read | 7 |
| read:analytics | 8 |
| read:users | 7 |
Worked solution and explanation
What this really is
This is a liveness check against a cutoff date wearing a security dashboard's badge. Every token's life ends at expires, December 1, 2025 is the cutoff, and the question is which revocations were still guarding something on that day. Anyone can write GROUP BY scope. What separates candidates is remembering that a missing `expires` is a life with no end, and then grouping that alternative so OR does not leak past the status test. Miss the NULL and every permanent ban vanishes; drop the parentheses and active tokens start showing up as revoked.
Build the filter piece by piece
Step 1: Normalize the status
The column holds 'revoked' and 'REVOKED' side by side. LOWER(status) = 'revoked' folds them together; a raw equality quietly undercounts every scope.
Step 2: Keep tokens that had not expired by the cutoff
expires >= '2025-12-01' keeps tokens still alive on the first. A NULL expires makes that comparison unknown, and WHERE discards unknown, so expires IS NULL has to be an explicit alternative.
Step 3: Parenthesize the alternative
AND binds tighter than OR. Written bare, LOWER(status) = 'revoked' AND expires IS NULL OR expires >= ... parses as two separate branches, and the second one admits active and expired tokens of any status.
- Unparenthesized
LOWER(status) = 'revoked' AND expires IS NULL OR expires >= '2025-12-01': any token expiring after November slips in, whatever its status.- Grouped
LOWER(status) = 'revoked' AND (expires IS NULL OR expires >= '2025-12-01'): the expiry check is one condition inside the chain, so the status test still applies to every row.
The solution
SELECT scope, COUNT(*) AS blocked_count
FROM api_tokens
WHERE LOWER(status) = 'revoked'
AND (expires IS NULL OR expires >= '2025-12-01')
GROUP BY scope
ORDER BY scopeCommon follow-up questions
- Security now wants only tokens whose life actually overlaps December 2025. What extra condition does that need? (Tests whether the candidate sees that overlap needs a start edge on `issued` as well as an end edge.)
- What if `expires` could also be an empty string instead of `NULL`? (Tests whether the candidate guards both encodings of a missing value.)
- Security wants scopes with zero qualifying tokens listed too. What changes? (Tests whether the candidate reaches for the full list of scopes rather than only the ones the filter keeps.)