Data Quality Guide

Data quality checks that matter: the six worth waking someone for

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

Most data quality programmes fail the same way. Someone adds four hundred tests, a quarter of them go yellow on any given morning, the channel gets muted, and the real incident arrives unnoticed three weeks later. Six check types catch the overwhelming majority of production data incidents. Run those six at two specific boundaries, alert on them, and treat everything else as a report rather than a page.

8 min readFor analytics engineers whose alert channel is already muted

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

205/20003 credits left
Try:

No account needed · Editable canvas, not a picture

The six

Freshness. Time since the newest row, compared against the promise. This is the single highest-yield check because a pipeline that did not run produces no other symptom: every other test passes on yesterday's perfectly valid data.

Row volume. Today's row count against the recent range for the same weekday. Catches partial loads, a source that silently started filtering, and duplicate processing, all of which pass a schema check.

Schema drift. Column added, removed, or retyped upstream. Additions are usually harmless and worth a notification; removals and type changes break downstream models and deserve a failure.

Nulls on required columns. Restricted to columns that are actually required: keys, join fields, and anything a business rule depends on. Running a null check on every column is how the noise problem starts.

Primary key uniqueness. Duplicate keys are the failure that produces wrong numbers rather than missing ones, which means nobody notices until a total is questioned in a meeting.

Accepted values and ranges. Enums that grew a seventh value, amounts that went negative, a date that landed in 1970. This is the check that catches semantic change, and it is the one most often missing.

Two boundaries, not everywhere

Boundary one, at ingestion, before transformation. Check freshness, volume, and schema drift on the source. Failures here are upstream problems, and the correct response is to stop the run rather than transform bad input into confidently wrong output. Detecting at this boundary is also what makes the incident attributable to the source team rather than the data team.

Boundary two, at the published output, after transformation. Check uniqueness, nulls on required columns, and accepted values on the data product a consumer will read. Failures here are your logic or a semantic change that survived boundary one.

Intermediate models get tests in CI against a sample, not alerts in production. A failing test on a staging model that nobody consumes directly is information for the pull request, not a page.

One dbt detail that trips up copied examples: the tests key was renamed to data_tests in dbt 1.8, to separate data tests from unit tests. The old key still works and now warns, and the two cannot both appear on one resource, so a YAML file assembled from posts written on either side of that release fails in a way the error message does not immediately explain.

# dbt: boundary one, on the source
sources:
  - name: checkout
    loaded_at_field: _ingested_at
    freshness:
      warn_after: {count: 2, period: hour}
      error_after: {count: 6, period: hour}
    tables:
      - name: orders_raw

# dbt: boundary two, on the published product
models:
  - name: orders_fct
    columns:
      - name: order_id
        data_tests:
          - unique
          - not_null
      - name: status
        data_tests:
          - accepted_values:
              values: [placed, paid, shipped, cancelled]
      - name: total_amount
        data_tests:
          - dbt_utils.accepted_range:
              min_value: 0

Cadence: what runs per load, daily, and weekly

Per load: freshness, volume, schema drift, uniqueness, and nulls on keys. These are cheap, they gate the publish, and a failure means the load should not be advertised as complete.

Daily: accepted values, range checks, and distribution comparisons against the trailing window. These are the checks that find slow semantic drift, and they do not need to block a load to be useful.

Weekly or monthly: referential integrity across domains, reconciliation against a system of record, and a review of which checks fired and were then ignored. That last one is the maintenance task nobody schedules, and it is the reason alert channels rot. A check that has fired and been dismissed three times is either wrong or its threshold is wrong.

# Boundary one, before anything is transformed. Non-zero exit on error_after.
dbt source freshness --select source:checkout

# Transform, then run boundary two on what was built, in one command.
# --fail-fast stops at the first failure instead of building on bad input.
dbt build --select checkout+ --fail-fast

# In the pull request, only what this branch touched, deferring the rest
# to the production artefacts so the run stays small.
dbt build --select state:modified+ --defer --state ./prod-manifest

# What fired and was ignored this month, the review nobody schedules:
jq -r '.results[] | select(.status != "pass") | .unique_id' \
  target/run_results.json | sort | uniq -c | sort -rn

Thresholds, ownership, and the picture behind them

Set thresholds from measurement, not from intuition. Take thirty days of history, look at the actual variation, and set the bound outside the observed normal range rather than at a round number. A volume check set at "plus or minus twenty percent" on a table whose Monday is always triple its Sunday is a check that trains people to ignore it.

Route every check to the owning domain, not to a central data channel. A freshness failure on the checkout source is a checkout problem, and it gets fixed faster when it arrives in the checkout channel with the source name in the alert.

When an incident does land, the first question is always what else is downstream of this. That is a lineage question, and answering it from memory is where hours go. Datadef keeps column-level lineage on the canvas and regenerates the diagram from the connected repository on a daily sync, so the map used during an incident is the current one. Once a check is promised to a consumer rather than just run, it belongs in a contract with a number attached, covered in data quality SLAs.

FAQ

What are the most important data quality checks?

Six types cover most production incidents: freshness (time since the newest row), row volume against the recent range, schema drift, nulls on required columns, primary key uniqueness, and accepted values or ranges. Freshness is the highest yield, because a pipeline that did not run passes every other check on yesterday's valid data.

Where should data quality checks run in a pipeline?

At two boundaries. At ingestion, check source freshness, volume, and schema drift, and stop the run on failure so bad input is not transformed. At the published output, check uniqueness, nulls on required columns, and accepted values. Intermediate models get tests in CI rather than production alerts.

How often should data quality checks run?

Freshness, volume, schema, uniqueness, and null checks on keys run on every load and gate the publish. Accepted values, ranges, and distribution comparisons run daily. Referential integrity across domains and reconciliation against a system of record run weekly or monthly, along with a review of which alerts fired and were ignored, which the run results artefact answers in one query.

How do you stop data quality alerts from becoming noise?

Alert on the six check types only, set thresholds from thirty days of measured history rather than round numbers, route each alert to the owning domain instead of a central channel, and review dismissed alerts monthly. A check dismissed three times is either wrong or has the wrong threshold, and it should be fixed or removed.

Should every column have a null check?

No. Restrict null checks to columns that are genuinely required: primary keys, join keys, and fields a business rule depends on. Blanket null checks across every column are the main source of alert noise, and the noise is what causes real failures to be missed.