Users With Admin Tokens
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
- L3
Problem
A security review needs every user who holds at least one API token whose scope contains the substring 'admin'. Return each qualifying token's user_id, username, email, token_id, and scope, walking up from the lowest user_id and, within a single user, from their earliest-issued token onward to their most recent.
Admin tokens. Who holds them?
Tables
users
Columns: user_id INTEGER, username TEXT, email TEXT, signup_date TEXT, account_status TEXT, age_bucket TEXT
| user_id | username | signup_date | account_status | age_bucket | |
|---|---|---|---|---|---|
| 100 | alice | alice@example.com | 2025-02-02 | inactive | 25-34 |
| 197 | aaron42 | aaron42@example.com | 2026-03-03 | suspended | 35-44 |
| 294 | amelia | amelia@example.com | 2024-04-04 | pending_verification | 45-54 |
| 391 | arjun | arjun@example.com | 2025-05-05 | active | 55-64 |
| 488 | ava99 | ava99@example.com | 2026-06-06 | inactive | 65+ |
| 585 | andrew_k | andrew_k@example.com | 2024-07-07 | suspended | NULL |
| 682 | anika | anika@example.com | 2025-08-08 16:34:00 | pending_verification | 18-24 |
| 779 | aiden | aiden@example.com | 2026-09-09 14:53:00 | active | 25-34 |
| 876 | aria_b | aria_b@example.com | 2024-10-10 | inactive | 35-44 |
| 973 | brian | brian@example.com | 2025-11-11 | suspended | 45-54 |
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
| user_id | username | token_id | scope | |
|---|---|---|---|---|
| 100 | alice | alice@example.com | 5300 | admin |
| 100 | alice | alice@example.com | 10005400 | admin |
| 197 | aaron42 | aaron42@example.com | 3193 | admin |
| 197 | aaron42 | aaron42@example.com | 10005351 | admin |
| 294 | amelia | amelia@example.com | 1086 | admin |
Worked solution and explanation
A security audit wearing a join costume
This is a substring match on a free-text permission string, joined to the people who own it. Anyone can write the join. The trap is the predicate: 'contains admin' is not scope = 'admin'. Equality only catches the literal scope, and the privileged tokens in a real audit are the ones named 'admin:read', 'read,admin' or 'superadmin'. Miss those and the report under-counts exactly the accounts a reviewer cares about.
- users: user_id PK, username, email
- api_tokens: token_id PK, owner_id FK, scope, status
- api_tokens.owner_id references users.user_id
Building it in three decisions
Step 1: Join on the owner foreign key
username and email live only in users, while scope lives only in api_tokens. A scan of api_tokens alone cannot project the columns the review wants, so users.user_id = api_tokens.owner_id is required. An inner join is right: a token with no matching user is not something to report here.
Step 2: Filter with a wildcard on both sides
scope LIKE '%admin%' matches the substring anywhere. The leading % catches 'superadmin' and 'read,admin', the trailing % catches 'admin:write'.
Step 3: Leave the grain alone
The ask is each qualifying token, so there is no DISTINCT and no GROUP BY. A user holding two admin tokens should appear twice. The ORDER BY just makes the output walk up by user.
SELECT u.user_id, u.username, u.email, at.token_id, at.scope FROM users u JOIN api_tokens at ON u.user_id = at.owner_id WHERE at.scope LIKE '%admin%' ORDER BY u.user_id, token_idA single pass: join, substring filter, no aggregation.
- Exact equality
scope = 'admin'returns only tokens whose whole scope is the word admin. It runs fast and looks right on a quick eyeball, but it silently drops every compound scope.- Substring match
scope LIKE '%admin%'returns every token whose scope mentions admin anywhere. It costs the same single scan here and is the only form that matches the stated requirement.
Common follow-up questions
- The review now wants one row per user with a count of their admin tokens. What changes? (Tests switching grain deliberately: `GROUP BY u.user_id` with `COUNT(*)`, after the same join and `LIKE` filter.)
- Scopes are comma-separated and 'superadmin' must not match. How do you match only the whole token 'admin'? (Tests tightening a loose `LIKE` with delimiter-aware patterns or splitting the list, and knowing the cost of each.)
- `api_tokens` has 500 million rows. Does `LIKE '%admin%'` use an index on `scope`? (A leading wildcard defeats a B-tree index. Probes trigram indexes or a normalized scopes table.)