Unreviewed Models

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

Problem

A machine-learning platform team keeps a registry of models in ml_models beside a log of data-quality checks in dq_checks, where a check that fails on a given day flags bad data landing that day. Among the models with a recorded training date, find the ones that trained on a day when no check was failing, along with each model's version, framework, and status.

Models that have never been evaluated.

Tables

ml_models

Columns: model_id INTEGER, mdl_name TEXT, version TEXT, accuracy REAL, status TEXT, train_at TEXT, framework TEXT

ml_models: 10 of 200 rows
model_idmdl_nameversionaccuracystatustrain_atframework
123rec_enginev1.10.72training2026-02-02 01:00:00TensorFlow
146churn_predv2.00.89archived2026-03-03 02:00:00XGBoost
169spam_filter3.00.61failed2026-04-04 03:00:00sklearn
192img_classifyv2.1-beta0.78validating2026-05-05 04:00:00pytorch
215sentiment1.0.00.95Deployed2026-06-06 05:00:00tensorflow
238demand_fcstv1.00.67deployedNULLPyTorch
261price_optv1.10.84Training2026-08-08 07:00:00TensorFlow
284fraud_detectv2.0NULLarchived2026-09-09 08:00:00XGBoost
307rec_engine3.00.73failed2026-01-10 09:00:00sklearn
330churn_predv2.1-beta0.9validating2026-02-11 10:00:00pytorch

dq_checks

Columns: check_id INTEGER, tbl_name TEXT, col_name TEXT, rule TEXT, passed INTEGER, fail_pct REAL, run_at TEXT, severity TEXT

dq_checks: 10 of 200 rows
check_idtbl_namecol_namerulepassedfail_pctrun_atseverity
131usersemailunique1NULL2026-02-02 01:00:00high
162productsamountrange1NULL2026-03-03 02:00:00medium
193transactionsstatusformat1NULL2026-04-04 03:00:00low
224eventscreated_atreferential1NULL2026-05-05 04:00:00Critical
255sessionspricefreshness0152026-06-06 05:00:00HIGH
286ad_impressionsregionNot_Null1NULL2026-07-07 06:00:00critical
317ordersuser_idUNIQUE102026-08-08 07:00:00high
348usersemailnot_null1NULL2026-09-09 08:00:00medium
379productsamountunique1NULL2026-01-10 09:00:00low
410transactionsstatusrange0302026-02-11 10:00:00Critical

Expected output

First 5 of 122 rows
mdl_nameversionframeworkstatus
rec_enginev1.1TensorFlowtraining
spam_filter3.0sklearnfailed
img_classifyv2.1-betapytorchvalidating
fraud_detectv2.0XGBoostarchived
spam_filter1.0.0tensorflowDeployed

Worked solution and explanation

This is an anti-join hiding under a data-governance story: for each model you have to prove a negative, that no data-quality check was failing on the day it trained. A plain filter cannot express absence, and an inner join keeps the exact opposite set: the models that DID coincide with a failure. The move is to LEFT JOIN the checks on, then keep only the rows where nothing attached. The trap is the join key. train_at and run_at are both timestamps, so comparing them raw asks 'trained at the exact second a check ran', not 'trained the same day'. Get that wrong and you flag the wrong models and let every model with a missing training date slip straight through.

What you're really being asked

Flip the question around. For every model with a known training date, was there any failing check that day? If yes, it trained on a dirty day and is out. If no, it had a clean run and belongs in the answer. That 'no failing check that day' is an absence, and absence in SQL is an anti-join, not a WHERE.

How to build it

Step 1: Join the checks onto each model by day

LEFT JOIN dq_checks d onto ml_models m, matching date(m.train_at) = date(d.run_at). Truncating both timestamps to their date is what turns 'same instant' into 'same day': a check that ran at 09:00 still counts against a model trained at 02:00 that morning.

Step 2: Attach only the failing checks

Add AND d.passed = 0 to the ON clause. A passing check that day is harmless, so only failures should attach. Putting d.passed = 0 in the WHERE instead filters on a column that is NULL for every unmatched row, quietly collapsing the LEFT JOIN back into an inner join and deleting the very clean-day models you are hunting.

Step 3: Keep the rows that found nothing

WHERE d.check_id IS NULL keeps the models where no failing check attached: that is the anti-join. Then AND m.train_at IS NOT NULL drops models with no recorded training day, because a NULL training date never matches any run_at and would otherwise survive as a false clean run.

passed = 0 in the WHERE
... WHERE d.check_id IS NULL AND d.passed = 0: d.passed is NULL for every unmatched row, so this keeps nothing. The outer join is silently undone into an inner join.
passed = 0 in the ON
... ON date(m.train_at) = date(d.run_at) AND d.passed = 0 ... WHERE d.check_id IS NULL: failures attach, everything else stays NULL, and the anti-join returns the clean-day models.
Clean-day models
SELECT m.mdl_name, m.version, m.framework, m.status
FROM ml_models m
LEFT JOIN dq_checks d
  ON date(m.train_at) = date(d.run_at)
 AND d.passed = 0
WHERE d.check_id IS NULL
  AND m.train_at IS NOT NULL

LEFT JOIN on the training day with the failing-check filter in the ON, then keep the rows that never matched.

Common follow-up questions