Data Quality Checks and Quality Gates
A pipeline can succeed and still write garbage. The check families a junior is expected to name (uniqueness, completeness, validity, volume, referential integrity) and the quality gate that stops bad data, all written as plain SQL.
"Validated" is not a wish, it is a set of checks
The Silver layer earned the words "cleaned, validated, deduplicated" in the last lesson, and this lesson is where that word gets teeth. A pipeline can finish with a green status and still write garbage: a broken upstream export sends half the rows, a join fans out and duplicates every order, a bug lets NULLs into a key column. "The job succeeded" and "the data is correct" are two different questions, and a data engineer is expected to check the second one. "How do you know your data is correct, and how would you test a pipeline?" is one of the most common junior DE interview questions.
The check families
Data-quality checks fall into a handful of families, and almost every real test is one of these:
| check family | catches | SQL shape |
|---|---|---|
| uniqueness / duplicates | a key that repeats (broken dedup, fan-out join) | GROUP BY key HAVING COUNT(*) > 1 |
| completeness / null rate | missing values in a required column | SUM(CASE WHEN col IS NULL ...) / COUNT(*) |
| validity / range | values outside the allowed domain | WHERE col < 0 OR col NOT IN (...) |
| volume anomaly | a load far bigger or smaller than usual | compare COUNT(*) to a recent baseline |
| referential integrity | a foreign key with no parent row | LEFT JOIN dim WHERE dim.id IS NULL |
- Uniqueness / duplicates. A key that should be unique (one row per
order_id) but is not signals a broken dedup or a fan-out join. This is also how you catch a non-idempotent re-run: run a job twice without an idempotent write and every key doubles. You find it withGROUP BY key HAVING COUNT(*) > 1. - Completeness / null rate. A column that should always be present but is NULL or blank. You measure the null rate and alert when it crosses a threshold.
- Validity / range. A value outside its allowed domain: a negative price, a status not in the allowed set, a date in the future.
- Volume / row-count anomaly. Today's load is a tenth of yesterday's, or triple. The row count itself is a signal (the
rows_outyour run log records). - Referential integrity. A
user_idin the fact table that does not exist in the users dimension (an orphan).
Quality gates (naming what you already built)
You have already written both halves of this. In Level 4 you wrote the four dbt tests (unique, not_null, accepted_values, relationships) as zero-rows-means-pass queries, and in Level 5 you built a write-audit-publish (WAP) gate that staged a batch, audited it with checks, and published only on a clean pass. The vocabulary consolidates here: a quality gate runs those checks and fails the pipeline (or quarantines the batch) when one breaches its threshold, so bad data never reaches the Gold tables the dashboards read. Frameworks like dbt tests or Great Expectations are how you wire these into production, but every one of them is a SQL query underneath, exactly the ones you wrote by hand.
Writing the checks with SQL
The checks are exactly the SQL you already know: a GROUP BY ... HAVING for duplicates, a conditional SUM for a null rate, a CASE for a pass or fail gate. The exercises run real checks over a staged_events batch: find the duplicated ids, then compute a null rate and decide whether the batch passes its gate.
Common mistake: running quality checks only after the data is published. By then the bad data is already in the Gold tables the dashboards read, so the checks belong in a gate that runs before publish (write-audit-publish).
Interview nuance: "how would you test this pipeline" wants the check families by name (uniqueness, completeness, validity, volume, referential integrity) and the idea of a quality gate that fails the run. Bonus points for naming dbt tests or Great Expectations, and for saying the checks run in a write-audit-publish step before the data is published.
On a real platform this differs. Real quality checks run as dbt tests or Great Expectations suites wired into the pipeline, emitting pass or fail per check with row-level samples of the failures. The
staged_eventstable here lets you write the same checks as plain SQL so you can see what each framework runs for you underneath.
CREATE TABLE staged_events (
event_id INTEGER, -- should be unique per event
user_id INTEGER,
amount REAL,
country TEXT
);
INSERT INTO staged_events (event_id, user_id, amount, country) VALUES
(1, 100, 12.50, 'US'),
(2, 101, 30.00, 'US'),
(3, NULL, 15.00, 'CA'), -- null user_id (completeness issue)
(4, 102, -5.00, 'GB'), -- negative amount (validity issue)
(5, 103, 20.00, 'US'),
(2, 101, 30.00, 'US'), -- duplicate event_id 2
(5, 103, 20.00, 'US'), -- duplicate event_id 5
(5, 103, 20.00, 'US'), -- event_id 5 appears three times
(6, NULL, 8.00, NULL), -- null user_id and null country
(7, 104, 40.00, 'US');-- A one-row quality scorecard for the staged batch before it is published.
SELECT COUNT(*) AS total_rows,
SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) AS null_user_ids,
SUM(CASE WHEN amount <= 0 THEN 1 ELSE 0 END) AS bad_amounts
FROM staged_events;Apply
Your turn
The task this lesson builds to.
Write a query that returns every event_id that appears more than once, with how many copies it has, as (event_id, copies), most copies first, over staged_events(event_id, user_id, amount, country).
event_id should be unique per event, so any id with more than one row is a duplicate a dedup step must resolve. Order by copies descending, then event_id.
3 hints and 1 automated check are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, plus 4 bonus drills.
Write a query that returns the batch's total row count, its null user_id rate as a percentage, and whether it passes a quality gate, as (total_rows, null_user_pct, gate), over the same staged_events table.
The gate requires the null user_id rate to be under 5 percent: return 'pass' when it is and 'fail' otherwise. Round null_user_pct to 2 decimals.
3 hints and 1 automated check are waiting in the workspace.