"The data is bad" is not actionable. Data quality dimensions break the vague complaint into specific, measurable properties, each with its own checks and owners. This page defines the six dimensions used in most frameworks, shows how to turn each one into a metric, where to run the checks in a pipeline, and how to avoid the common trap of hundreds of alerts that nobody reads.
Different frameworks (DAMA DMBOK, ISO 8000, ISO/IEC 25012) list between six and more than a dozen dimensions, but six appear almost everywhere. Accuracy: values correctly describe the real-world entity. Completeness: required records and fields are present. Consistency: the same fact has the same value across tables and systems. Timeliness: data is available and current when needed. Validity: values conform to types, formats, ranges and business rules. Uniqueness: each real-world entity is recorded once.
Quality is always relative to a use. A table that is fresh enough for a monthly report may be far too stale for fraud detection. That is why thresholds belong to the consumer's needs, ideally written into a data contract.
Bad data costs twice: once when a wrong decision or model is made, and again when engineers spend days tracing the cause. Naming the dimension speeds up both prevention and diagnosis. "Completeness of email dropped from 99.8% to 71% after Tuesday's deploy" points straight to a fix; "the marketing numbers look off" does not. Dimensions also make quality reportable, so data owners can set targets and track them like any other service level.
A single small table can fail every dimension. The email bob(at)x is invalid. An amount of 250,000 on a shop selling 20-dollar items is probably inaccurate, a typo for 25.00, but no rule can prove it; accuracy usually needs a trusted reference or a human. Order 1002 appears twice, breaking uniqueness. US and USA encode the same country differently, a consistency problem that will split group-by results. The null email is a completeness gap, and the row last updated nine days ago fails a one-day timeliness requirement.
| Dimension | Typical metric | Example check |
|---|---|---|
| Accuracy | % matching a trusted source; outlier rate | Compare sample to system of record; flag values beyond robust bounds |
| Completeness | % non-null; row count vs expected | email IS NOT NULL ≥ 99%; today's rows within 20% of 7-day average |
| Consistency | % agreeing across systems or with reference data | Order totals equal sum of line items; country in ISO list |
| Timeliness | Age of newest record; delivery delay | max(updated_at) within 1 hour |
| Validity | % passing format, type, range, enum rules | Email regex, amount ≥ 0, status in allowed set |
| Uniqueness | Duplicate rate on business key | COUNT(DISTINCT id) = COUNT(*) |
Express each metric as a ratio between 0 and 1 where possible, so they can be trended and compared. Then set a threshold per dataset based on what consumers need, not on what the data currently achieves.
Run cheap structural checks (schema, nulls, row counts) at ingestion, so bad loads are caught before they spread. Run relational and business-rule checks (referential integrity, uniqueness after joins, totals that must reconcile) after transformation. Run freshness and key business metrics at the publish gate, just before consumers see the data. Failures should quarantine rows or block publishing, and alert the team that owns the source; lineage tells you which downstream assets to mark as affected.
import pandas as pd
df = pd.DataFrame({
"order_id": [1001, 1002, 1002, 1004],
"email": ["ana@x.com", "bob(at)x", "bob(at)x", None],
"country": ["US", "US", "USA", "DE"],
"amount": [19.99, 250000.00, 25.00, 12.50],
"updated": pd.to_datetime(["2026-10-01", "2026-10-01", "2026-10-01", "2026-09-22"]),
})
now = pd.Timestamp("2026-10-01 12:00")
ref_countries = {"US", "DE", "GB", "FR"} # ISO-3166 alpha-2 subset
checks = {
"completeness: email not null": df["email"].notna().mean(),
"validity: email format": df["email"].dropna().str.match(r"^[^@\s]+@[^@\s]+\.\w+$").mean(),
"uniqueness: order_id": 1 - df["order_id"].duplicated().mean(),
"consistency: country in ref": df["country"].isin(ref_countries).mean(),
"timeliness: updated < 24h": (now - df["updated"] < pd.Timedelta("24h")).mean(),
# accuracy proxy: flag outliers vs a robust range (median +/- 10 * MAD)
"accuracy proxy: amount plausible": (
(df["amount"] - df["amount"].median()).abs()
<= 10 * (df["amount"] - df["amount"].median()).abs().median()).mean(),
}
THRESHOLD = 0.99
for name, score in checks.items():
status = "PASS" if score >= THRESHOLD else "FAIL"
print(f"{status} {score:6.1%} {name}")
Each check returns a score between 0 and 1 and is compared with a threshold. On the sample data, every dimension fails, matching the annotated figure above. The accuracy check is a proxy: it flags implausible amounts using a robust spread (median absolute deviation), but only a reference source can confirm a value is truly wrong. The same metrics expressed in SQL:
-- The same checks as SQL, runnable in most warehouses (one row per metric)
SELECT 'completeness_email' AS metric, AVG(CASE WHEN email IS NOT NULL THEN 1.0 ELSE 0 END) AS score FROM orders
UNION ALL
SELECT 'uniqueness_order_id', COUNT(DISTINCT order_id) * 1.0 / COUNT(*) FROM orders
UNION ALL
SELECT 'consistency_country', AVG(CASE WHEN country IN (SELECT code FROM ref_country) THEN 1.0 ELSE 0 END) FROM orders
UNION ALL
SELECT 'timeliness_hours_since_newest',
EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - MAX(updated))) / 3600 FROM orders;
Tools such as Great Expectations, Soda, dbt tests, Deequ (for Spark) and Databricks expectations package these checks with scheduling, history and alerting. The concepts are the same.
Rule-based checks encode what you know: an amount is never negative, an ID is unique. They are precise and explainable but only catch failures you anticipated. Anomaly-based monitoring learns the normal range of metrics such as row count, null rate, or the distribution of a column, and alerts on deviations. It catches surprises, like a silent 40% drop in volume after an upstream change, but produces more false alarms and needs history to learn from. Most mature teams combine them: explicit rules on critical fields, plus automated volume, freshness and distribution monitors on every important table.
Individual checks answer "is this batch OK?". Owners and consumers also need a longer view. Store every check result with a timestamp, dataset, dimension and score, and you can build a scorecard per dataset showing each dimension over time. That history turns quality into service levels: for example, "orders is complete to 99.9% and fresh within one hour on 99% of days this quarter". Consumers can then decide whether a dataset is fit for their purpose before they build on it, and owners can see whether quality is improving or slowly degrading.
Weight the scorecard by impact rather than averaging everything. A 2% null rate on an optional marketing field matters far less than a 0.1% duplicate rate on payment IDs. Many teams publish a simple status per dataset in the catalog (healthy, degraded, failing), derived from the critical checks only, which is easier to act on than a single blended score.