Where Quality Breaks

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
Asked in
Deloitte

Problem

Each row of dq_checks is one quality check run against a source table, with passed recording whether it flagged a problem and fail_pct the share of values that failed. Looking only at the flagged checks, show the source tables whose average fail percentage is above 30, worst first, with how many flagged checks each has and how many different rules they involved.

Which tables the checks keep flagging.

Tables

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 5 rows
tbl_namefailed_checksdistinct_rulesavg_fail_pct
events6380
ad_impressions6365
products6361.666666666666664
users6350
transactions6335

Worked solution and explanation

What this problem is really testing

Strip the data quality framing away and this is a filtered grouped aggregation whose last cut is a condition on each group, not on any row. The real skill is knowing where each cut belongs. passed = 0 is a fact about one row, so it goes in WHERE. The above-30 cut is a fact about a table's average, and that average does not exist yet when WHERE runs. Write WHERE avg_fail_pct > 30 and the query errors. Push the cut onto fail_pct row by row and you quietly keep and drop the wrong tables. The other quiet bug is reaching for COUNT(*) when the report asks how many different rules flagged the table. On this data every surviving table has 6 flagged checks but only 3 rules.


Build it in four moves

Step 1: Scope to the flagged checks

WHERE passed = 0 keeps only the checks that flagged a problem. Do not lean on fail_pct being blank instead: passing checks are not reliably empty, and some log 0.0. The flag column is the honest filter, and everything after this sees flagged rows only.

Step 2: Bucket by table

GROUP BY tbl_name collapses the flagged rows into one row per source table. Lock this grain in before writing the aggregates, because the average and both counts are computed within it.

Step 3: Average, and count the right thing

AVG(fail_pct) gives each table its average fail rate, and COUNT(DISTINCT rule) counts how many different rule labels flagged it. COUNT(*) answers a different question (how many flagged rows), and the report wants both numbers, so keep both.

Step 4: Filter on the average, worst first

HAVING AVG(fail_pct) > 30 keeps only the tables whose own average clears the bar. Here that drops sessions at 20 and orders at 7.5. A trailing ORDER BY avg_fail_pct DESC puts the worst table on top.


The query

Average fail rate per table, flagged checks only, above the 30 percent bar
SELECT tbl_name,
       COUNT(*) AS failed_checks,
       COUNT(DISTINCT rule) AS distinct_rules,
       AVG(fail_pct) AS avg_fail_pct
FROM dq_checks
WHERE passed = 0
GROUP BY tbl_name
HAVING AVG(fail_pct) > 30
ORDER BY avg_fail_pct DESC
Count the rows (wrong)
COUNT(*) returns how many flagged checks a table has (6 here), not how many different rules were involved. If the ask is rule variety, this silently over-reports.
Count different rules (right)
COUNT(DISTINCT rule) returns 3 for these tables: the number of different rule labels that flagged them. That is the variety the report wants.

Common follow-up questions