The Undone
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
Problem
Using the migrations table, report each database's total migration count, how many of its migrations have an applied status and how many were rolled back, most rollbacks first. The reliability reviewer only wants databases with at least one rollback for every two applied migrations, and a status means the same outcome whatever its letter case.
Some migrations don't stick. Find the databases where they keep coming undone.
Tables
migrations
Columns: migr_id INTEGER, version TEXT, status TEXT, applied TEXT, rollback TEXT, dur_ms INTEGER, author TEXT, db_name TEXT
| migr_id | version | status | applied | rollback | dur_ms | author | db_name |
|---|---|---|---|---|---|---|---|
| 123 | 002_add_users | pending | 2026-02-02 01:00:00 | DROP TABLE IF EXISTS tbl_1 | 41 | bob | staging_main |
| 146 | 003_add_orders | failed | 2026-03-03 02:00:00 | DROP TABLE IF EXISTS tbl_2 | 72 | charlie | analytics |
| 169 | 004_indexes | rolled_back | 2026-04-04 03:00:00 | DROP TABLE IF EXISTS tbl_3 | 103 | dana | reporting |
| 192 | 005_add_products | Applied | 2026-05-05 04:00:00 | DROP TABLE IF EXISTS tbl_4 | 134 | migration-bot | archive |
| 215 | 006_add_sessions | PENDING | NULL | DROP TABLE IF EXISTS tbl_5 | NULL | Alice | prod_main |
| 238 | 007_add_events | applied | 2026-07-07 06:00:00 | DROP TABLE IF EXISTS tbl_6 | 196 | alice | staging_main |
| 261 | 008_add_search | pending | 2026-08-08 07:00:00 | NULL | 227 | bob | analytics |
| 284 | v009_payments | failed | 2026-09-09 08:00:00 | DROP TABLE IF EXISTS tbl_8 | 258 | charlie | reporting |
| 307 | V010_analytics | rolled_back | 2026-01-10 09:00:00 | DROP TABLE IF EXISTS tbl_9 | 289 | dana | archive |
| 330 | 001_init | Applied | NULL | DROP TABLE IF EXISTS tbl_10 | NULL | migration-bot | prod_main |
Expected output
| db_name | total_migrations | applied_migrations | total_rollbacks |
|---|---|---|---|
| reporting | 40 | 12 | 8 |
| archive | 40 | 14 | 8 |
| analytics | 40 | 12 | 6 |
Worked solution and explanation
What this problem is really testing
This is a ratio of two conditional counts dressed up as a reliability audit. Every database in migrations has seen a rollback, so 'has a rollback' filters nothing. The real bar is rollbacks measured against clean applies: at least one for every two. Anyone can count rollbacks. The trick is the other side of the ratio, because an applied status is written as both 'applied' and 'Applied'. Match it literally and you count half the applies, the bar drops, and prod_main and staging_main get into an audit they should not be in.
Building the query
Step 1: Count everything in one grouped pass
Group by db_name. COUNT(*) is the total across every status, so the restriction to rolled-back rows has to live inside its own count, SUM(CASE WHEN status = 'rolled_back' THEN 1 ELSE 0 END). Putting it in a WHERE would shrink the total down to the rollbacks.
Step 2: Count applies case-blind
SUM(CASE WHEN LOWER(status) = 'applied' THEN 1 ELSE 0 END) gives 12 or 14 per database. The bare comparison gives 6 or 8, and the bar in the next step inherits that error.
Step 3: Apply the bar after aggregating, then sort
At least one rollback per two applies is 2 * rollbacks >= applies. Multiplying keeps the comparison in integers and never divides by a zero apply count. The >= keeps analytics, which sits exactly on the line at 6 against 12. Then sort by total_rollbacks descending.
SELECT
db_name,
COUNT(*) AS total_migrations,
SUM(CASE WHEN LOWER(status) = 'applied' THEN 1 ELSE 0 END) AS applied_migrations,
SUM(CASE WHEN status = 'rolled_back' THEN 1 ELSE 0 END) AS total_rollbacks
FROM migrations
GROUP BY db_name
HAVING 2 * SUM(CASE WHEN status = 'rolled_back' THEN 1 ELSE 0 END)
>= SUM(CASE WHEN LOWER(status) = 'applied' THEN 1 ELSE 0 END)
ORDER BY total_rollbacks DESC- Literal status match
status = 'applied'counts 6 applies onprod_mainand 8 onstaging_main. Both clear2 * 6 >= 8, so all five databases come back.- Case-folded match
LOWER(status) = 'applied'counts 14 on both.2 * 6 >= 14fails, so onlyreporting,archiveandanalyticssurvive.
Common follow-up questions
- How would you report the rollback rate itself, and why does dividing by the applied count need a guard? (A database with zero applies divides by zero; `NULLIF` or the multiplied form avoids it.)
- What if `status` also carried stray whitespace, like 'applied '? (Tests stacking `TRIM` under `LOWER` and noticing the dirt before it bites.)
- Could you add the average `dur_ms` of the rolled-back migrations per database? (A conditional average; `dur_ms` has NULLs, which `AVG` skips.)