GCP-220 · Lesson 10 of 12 · 19 min
Data quality checks
▶ Video coming soon A video version of this lesson is on the way.
By the first week of November, three feeds are loading every night: store sales, returns and the Google Ads transfer. On Thursday, November 5, Ravi asks a blunt question: "If one of them goes wrong tonight, who finds out, and when?" At the moment the honest answer is "Finance, at nine in the morning, when the flash report looks odd." This session turns the profile findings from lesson 3 and the rules from lesson 4 into automated checks that find problems first.
The dimensions of data quality
Data quality is easier to manage when it is broken into dimensions, each with its own kind of check. The names vary between books, but the ideas are consistent, and the exam expects you to recognize them.
| Dimension | Question | Example check at Birchline |
|---|---|---|
| Completeness | Is everything there? | All open stores sent a file; no required field is empty |
| Validity | Do values follow the rules? | qty between -50 and 500; tender in an approved list; sku matches the pattern |
| Accuracy | Do values match reality? | Loaded daily totals match the tills' own end-of-day totals |
| Consistency | Do related data agree? | Every sku in sales exists in the product catalog |
| Uniqueness | Is each real thing recorded once? | No duplicate lines after staging |
| Timeliness | Is the data fresh enough? | Last night's sales loaded by 04:00 |
Accuracy is the hardest dimension to check automatically, because it needs something to compare with. For store sales, the tills produce an end-of-day total that the store manager signs off, and comparing the loaded total with that figure is the strongest single check the team has. Lesson 6's manifest check is a completeness check; this one goes further.
Where checks belong
Checks can run at each layer of the pipeline, and they catch different things. Checks at ingestion catch missing or malformed files before anything is loaded. Checks in staging catch rule violations after cleaning, such as unknown stores or SKUs. Checks on curated tables catch business-level surprises, such as a store's daily sales falling to a tenth of its usual level. A good design has a few checks at each layer rather than many at one.
Four ways to run checks on Google Cloud
| Method | How it works | Good for |
|---|---|---|
| SQL checks in the pipeline (ASSERT, or queries that count failures) | A query fails the job if a condition is not met, or writes failure counts to a table | Simple checks that must stop a pipeline |
| Dataform assertions | Declared next to each SQL transformation; Dataform runs them after building the table | Teams already managing SQL in Dataform |
| Dataplex data quality scans | Rules on a table, run on a schedule or on demand, with scores and history | Standard checks across many tables, with results kept and alerts sent |
| Validation steps in BigQuery data preparation and Data Fusion | Rows failing a rule go to an error table | Separating bad rows during preparation |
The simplest form is a SQL check that fails the job. BigQuery's scripting language has an ASSERT statement: if its condition is false, the script stops with an error, and any scheduled query or orchestration step running it is marked as failed. Placed after the staging build, it stops bad data from flowing into curated tables.
ASSERT (
SELECT COUNT(*) FROM `bl-data-prod.staging.pos_sales` AS s
LEFT JOIN `bl-data-prod.reference.products` AS p USING (sku)
WHERE s.business_date = @run_date AND p.sku IS NULL
) = 0 AS 'Sales lines with an sku missing from reference.products';
ASSERT (
SELECT COUNT(DISTINCT store_id) FROM `bl-data-prod.staging.pos_sales`
WHERE business_date = @run_date
) >= (SELECT COUNT(*) FROM `bl-data-prod.reference.stores_open` WHERE open_date = @run_date)
AS 'Fewer stores loaded than were open';Dataplex data quality scans
For checks that should run across many tables with a history of results, Birchline uses data quality scans in Dataplex Universal Catalog, the companion of the profile scans from lesson 3. A scan applies a set of rules to a table. Each rule checks one column or the whole table, and has a threshold: the share of rows that must pass for the rule to pass. The common rule types are listed below.
| Rule type | Checks that | Birchline rule on staging.pos_sales |
|---|---|---|
| Non-null | A column has no empty values | store_id, sku and txn_ts non-null; threshold all rows |
| Range | Values fall between limits | qty between -50 and 500; threshold 999 in 1,000 rows |
| Set | Values come from a list | tender in CARD, CASH, GIFT, PROMO; threshold 995 in 1,000 |
| Regular expression | Values match a pattern | sku matches the product code pattern; threshold all rows |
| Uniqueness | Values are not repeated | The combination used for deduplication is unique |
| Statistic range | An aggregate falls within limits | Average line value between 20 and 150 dollars |
| Row condition (SQL) | A SQL expression is true for each row | discount is not larger than qty times unit_price |
| Table condition (SQL) | A SQL expression is true for the table | Row count for the day is above 15,000 |
Each scan run produces a pass or fail for every rule and an overall score, and the results can be published so that people browsing the table in the catalog see its quality, and exported to a BigQuery table for history. Scans can be scheduled, filtered to only the newest partition so they read only last night's data, and set to send notifications when a rule fails. Dataplex can also suggest rules from a profile scan, which is a quick way to start: Mateo accepts the suggested non-null and set rules and writes the range and SQL rules himself.
Thresholds are a judgment. A threshold of all rows for sku being non-null means one empty SKU fails the rule, which is right, because an empty SKU means a broken till. A threshold of 995 in 1,000 for the tender set allows the occasional new payment type, such as the voucher found in lesson 4, without paging anyone at night, while still catching a real problem such as a till sending garbage.
# Scoring a night's quality rules against their thresholds.
rows = 31_204
rules = [ # (rule, rows passing, threshold as rows that must pass per 1,000)
("sku non-null", 31_204, 1000),
("qty in range", 31_190, 999),
("tender in set", 31_031, 995),
("sku matches pattern", 31_204, 1000),
("discount <= line value", 31_203, 1000),
]
for name, passing, need in rules:
per_1000 = passing / rows * 1000
status = "PASS" if per_1000 >= need else "FAIL"
print(f"{name:24s} {per_1000:7.2f} per 1,000 (needs {need}) {status} failing rows: {rows - passing}")
passed = sum(1 for _, p, n in rules if p / rows * 1000 >= n)
print(f"Score: {passed} of {len(rules)} rules passed")sku non-null 1000.00 per 1,000 (needs 1000) PASS failing rows: 0 qty in range 999.55 per 1,000 (needs 999) PASS failing rows: 14 tender in set 994.46 per 1,000 (needs 995) FAIL failing rows: 173 sku matches pattern 1000.00 per 1,000 (needs 1000) PASS failing rows: 0 discount <= line value 999.97 per 1,000 (needs 1000) FAIL failing rows: 1 Score: 3 of 5 rules passed
What this code does
For each quality rule it divides the rows that passed by the total rows to get a rate per thousand, and compares that with the rule's threshold. It prints whether each rule passed and how many rows failed it, then counts how many of the five rules passed overall. In this night's data, the tender rule and the discount rule fail, which tells the team exactly which checks to investigate and how many rows are involved.
The output tells a clear story. Fourteen rows have a quantity outside the range, but that is within the threshold. One hundred and seventy-three rows have an unexpected tender value, which breaks the threshold of 995 in 1,000 and deserves a look in the morning. One row has a discount larger than the line's value, and since that rule allows no exceptions, it fails. Each failing rule comes with a count of rows, which makes the investigation quick.
Reconciliation
The strongest check of all compares the pipeline's output with an independent figure. Each store's till prints an end-of-day total that the manager signs; the store systems team sends those totals as a small file of its own. A nightly query compares the totals in curated.daily_store_sales with the signed totals, store by store, and lists any store that differs by more than a dollar. Most nights the list is empty. When it is not, the difference is usually explained by a duplicate that staging removed, a late file, or a refund processed after the till closed, and each explanation goes into the incident log.
Freshness and volume checks
Two simple checks catch many real problems. A freshness check asks whether the latest data is as recent as it should be: if the newest business_date in staging.pos_sales is not yesterday by 04:00, something upstream has stalled. A volume check compares today's row count with the usual range for that day of the week; a sudden drop or doubling usually means a missing or duplicated file. Both are cheap, because they read only metadata or one partition, and both catch failures that row-level rules miss entirely. A table with zero rows passes every non-null rule.
Checks need testing too. A rule that has never failed may simply be wrong, for example a range rule on the wrong column or a set rule that was copied with a typing mistake. Before trusting the new scans, Mateo copies one night of staging data into a sandbox dataset and breaks it on purpose: he deletes one store's rows, blanks twenty SKUs, adds a tender value of XYZ and duplicates a hundred lines. Then he runs every check against the broken copy and confirms that each problem is caught by the check meant to catch it, with the expected severity. Two checks failed to fire the first time. The volume check compared with the wrong day of the week, and the uniqueness rule used a column list that did not match the staging deduplication. Both were fixed before the checks went live, and the broken copy is kept so that the test can be repeated whenever a rule changes.
When a check fails
A check is only useful if a failure leads to action. Ravi sets up a short playbook. Failures that make the data wrong, such as missing stores or failed reconciliation, stop the pipeline before curated tables are updated, so the morning report shows yesterday's numbers with a note instead of wrong numbers. Failures that are warnings, such as a tender threshold breach, let the pipeline continue and open a ticket. Every failure is recorded in a quality incident log with the date, the rule, the rows affected, the cause and the fix.
| Severity | Examples | Pipeline behavior | Who is told |
|---|---|---|---|
| Blocking | Missing store files, reconciliation difference, unknown SKUs | Stop before curated; keep yesterday's report | Analytics on-call by alert; Finance by 08:00 note |
| Warn | Tender threshold breached, range rule within threshold | Continue; open a ticket | Mateo in the morning review |
| Information | Volume slightly outside the usual range | Continue; record | Weekly quality review |
Common mistakes
MistakeChecking only that the load job succeeded.
InsteadCheck completeness, validity, consistency and accuracy, not just job status.
MistakeSetting every threshold to all rows.
InsteadChoose thresholds per rule: strict where one failure means a broken system, looser where rare new values are normal.
MistakeLetting failed checks only write to a log.
InsteadSend alerts, stop the pipeline for blocking failures, and record each incident.
MistakeRelying on row-level rules alone.
InsteadAdd freshness, volume and reconciliation checks; an empty table passes every non-null rule.
Worked problem
Choosing checks for the returns feed
The returns feed loads about 300 records a night. Problems seen so far: one night the export was empty because the portal failed; some records had no reason_code; one record had a refund larger than the original sale. Which checks should Mateo add?
Show solution
- Volume: a table condition that the night's row count is above 50, which catches an empty or truncated export.
- Freshness: the newest returned_at date is yesterday by 04:00.
- Completeness: reason_code non-null, with a threshold of about 980 in 1,000, since a few are missing legitimately.
- Validity: a row condition that refund_amount is not greater than the matching sale's line value.
- Make the volume and freshness checks blocking and the others warnings.
Answer: Volume and freshness as blocking checks, a reason_code completeness rule with a modest threshold, and a refund-versus-sale validity rule as a warning.
Summary and key terms
- Data quality has dimensions: completeness, validity, accuracy, consistency, uniqueness and timeliness.
- Put a few checks at each layer: ingestion, staging and curated.
- Checks can be SQL ASSERT statements, Dataform assertions, Dataplex data quality scans or validation steps that route bad rows to error tables.
- Dataplex scans apply rules such as non-null, range, set, regex, uniqueness, statistic range and SQL conditions, each with a threshold, and keep scores and history.
- Reconciliation with an independent total is the strongest accuracy check.
- Freshness and volume checks catch stalled or missing data that row rules miss.
- Every failure should lead to an alert, a decision to stop or continue, and an incident record.
- Completeness
- Whether all expected data and fields are present.
- Validity
- Whether values follow the defined rules and formats.
- Accuracy
- Whether values match reality or an independent source.
- Consistency
- Whether related datasets agree with each other.
- ASSERT
- A BigQuery scripting statement that stops a script with an error if its condition is false.
- Data quality scan
- A Dataplex scan that checks a table against rules with thresholds.
- Threshold
- The share of rows that must pass for a quality rule to pass.
- Reconciliation
- Comparing a pipeline's results with an independent figure.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Why can a table with zero rows pass every non-null rule?
Show answer
There are no rows to fail; only volume or freshness checks catch it.
Which check compares loaded totals with the tills' signed end-of-day totals?
Show answer
A reconciliation check, which tests accuracy.
What is a threshold in a Dataplex quality rule?
Show answer
The share of rows that must pass for the rule to pass.
What happens at Birchline when a blocking check fails?
Show answer
The pipeline stops before curated tables are updated and the on-call analyst is alerted.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.