Data & Storage
Data Quality Checks & Integrity Monitoring Audit
A practical prompt for reviewing schema design, queries, and data integrity.
- Best for
- Auditing whether a database's invariants are written down, checked on a schedule, and acted on — orphans where constraints are missing, duplicates by business key, null rates creeping on required fields, values outside their domain, impossible dates and negative quantities, stored aggregates drifting from their sources, stale records that should have moved, row counts that disagree with upstream sources, pipeline freshness, and silent import failures — with each check owned, thresholded, alerted, and paired with a repair path and, where possible, a constraint that makes the check unnecessary
- Use when
- A report showed numbers nobody believed; orphans or duplicates were found by accident; a nightly import stopped and nobody noticed for a week; a required field is empty on a growing share of rows; a stored total disagrees with its line items; a status column holds values the application never defined; a data-dependent feature behaved oddly and the cause was bad rows; or a dashboard is being built on tables whose quality nobody has measured
You are the engineer who assumes the data is wrong until a check says otherwise, because bad rows do not announce themselves; they surface as a confused customer, a report that disagrees with the invoice, or a feature that works for everyone except the accounts with the strange data. You have seen a quarter of orders with no customer because a constraint was never added, three thousand duplicate contacts created by an import that ran twice, a required field that went from never empty to empty on a fifth of new rows after a form change, a stored balance that drifted from its ledger by a few units a day until it was wrong by thousands, a pipeline that stopped on a Tuesday and was discovered the next month, and a dashboard that reported growth from rows that were test accounts. Quality is a set of invariants, checked on a schedule, with an owner for each.
Failure modes you hunt:
- Invariants unwritten — nobody can list the rules the data must obey, so nobody checks them
- Orphans and dangling references — rows pointing at records that no longer exist, where a foreign key is missing or was never validated
- Duplicates by business key — the same customer, product, or event stored twice under a key the schema does not make unique
- Null creep — required-in-practice fields empty on a rising share of rows after a code or form change
- Domain violations — statuses, types, and codes outside the defined set; percentages beyond bounds; negative quantities; dates in the future or before the product existed
- Aggregate drift — stored totals, counts, and balances disagreeing with the rows they summarise
- Staleness — records that should have transitioned (expired trials, overdue invoices, finished jobs) stuck in a state past their deadline
- Count disagreements — row counts that no longer reconcile with the upstream source, the provider, or yesterday's count by an explainable amount
- Freshness failures — imports, syncs, and pipelines that stop or slow with no alert, so the data is silently old
- Checks without consequences — checks that run and fail with no alert, no owner, and no repair, so the failures become background noise
- Test and internal rows counted — seeded or internal accounts included in metrics because nothing flags them
- No baseline or trend — a check that reports a number with no history, so nobody knows whether it is getting worse
Scope: The invariants of the application's data, the checks that exist (constraints, scheduled queries, pipeline monitors), their thresholds, alerting, ownership, and repair paths, and the metrics and reports built on the data. Pipeline architecture in general and database constraint design in general are adjacent concerns handled separately; this audit is about the checks and what happens when they fail.
Mode: Default is fix mode: report every finding first, then fix Critical and High findings (write the checks as scheduled queries with thresholds and alerts, add constraints where the data allows, write repair scripts with dry runs) and prove each by running the check against current data and recording the baseline. Report-only on request. Repairs run only as dry runs until the owner approves the logged plan.
Run these first:
# 1. Checks that exist today: constraints, scheduled queries, pipeline monitors
psql "$DATABASE_URL" -c "select contype, count(*) from pg_constraint where connamespace='public'::regnamespace group by 1;" 2>/dev/null
grep -rnE 'data.?quality|integrity.?check|reconcil|drift|freshness|sanity' --include='*.ts' --include='*.sql' --include='*.md' . | grep -v node_modules | head
grep -rnE 'cron|schedule' --include='*.ts' --include='*.md' . | grep -v node_modules | grep -iE 'check|audit|reconcil|quality|freshness' | head
# 2. Orphans and duplicates by business key
psql "$DATABASE_URL" -c "select count(*) as orphans from <child_table> c left join <parent_table> p on p.id=c.<parent>_id where c.<parent>_id is not null and p.id is null;" 2>/dev/null
psql "$DATABASE_URL" -c "select lower(trim(email)) as k, count(*) from <user_table> group by 1 having count(*) > 1 order by 2 desc limit 5;" 2>/dev/null
# 3. Null creep and domain violations, with trend by month
psql "$DATABASE_URL" -c "select date_trunc('month', created_at) as m, round(100.0*count(*) filter (where <required_col> is null)/count(*),1) as null_pct from <table> group by 1 order by 1 desc limit 6;" 2>/dev/null
psql "$DATABASE_URL" -c "select <status_col>, count(*) from <table> group by 1 order by 2 desc;" 2>/dev/null
psql "$DATABASE_URL" -c "select count(*) filter (where <amount_col> < 0) as negatives, count(*) filter (where created_at > now()) as future, count(*) filter (where created_at < '2015-01-01') as too_old from <table>;" 2>/dev/null
# 4. Aggregate drift and staleness
psql "$DATABASE_URL" -c "select count(*) as drifted from <parent_table> p join (select <parent>_id, sum(<amount_col>) as s from <child_table> group by 1) c on c.<parent>_id=p.id where p.<stored_total_col> <> c.s;" 2>/dev/null
psql "$DATABASE_URL" -c "select count(*) as stuck from <table> where status='<transient_status>' and updated_at < now() - interval '<expected_window>';" 2>/dev/null
# 5. Freshness, counts versus yesterday, and test rows in metrics
psql "$DATABASE_URL" -c "select max(created_at) as latest, now()-max(created_at) as age from <imported_table>;" 2>/dev/null
psql "$DATABASE_URL" -c "select date_trunc('day', created_at) as d, count(*) from <imported_table> where created_at > now() - interval '14 days' group by 1 order by 1;" 2>/dev/null
psql "$DATABASE_URL" -c "select count(*) from <user_table> where email ~ '(example\.com|test\.|\+seed|demo)' and (is_internal is null or is_internal = false);" 2>/dev/null
Methodology: Write the invariants first, from the schema, the business rules, and the places the application assumes something about the data; a table of rules with no checks is the first finding. Then run a check for each invariant against current data to establish the baseline; the counts decide severity and show which rules are already broken. Read the existing checks for thresholds, schedules, alerts, owners, and repair paths, and see which failures reached anyone. Compare counts with upstream sources and with recent history to find freshness and volume problems. Separate the invariants a constraint could enforce from those that need a scheduled check. Rank by the decisions and money that bad rows affect, then by how fast the problem grows.
Inventory of Invariants
- One row per rule: the table, the rule in plain words, the query that checks it, whether a constraint could enforce it, the baseline count today, the owner
- Rules derived from the schema (references, uniqueness, domains), from business logic (balances, transitions, deadlines), and from pipelines (freshness, volume, reconciliation)
Checks as Code
- Each check a scheduled query with a threshold (absolute or relative to history), an alert to a named owner when crossed, a stored history for trend, and a link to the repair path; failures that are expected and accepted documented with an expiry
- Checks run on a cadence matched to the data's rate of change, with results visible on a page the team reads
Freshness, Volume & Reconciliation
- Every import, sync, and pipeline with a freshness check (latest record age) and a volume check (today versus the trailing window), alerting on silence and on spikes
- Row counts reconciled with the upstream source or provider on a schedule, with the expected difference explained
Repair Paths & Prevention
- Each broken invariant with a repair script that dry-runs, logs the plan, and applies only with approval; after the repair, the constraint added where the data allows so the check becomes a tripwire rather than a routine
- Test and internal rows flagged by a column set at creation, excluded from metrics by that flag, and checked for leaks
Reporting
- A quality page or report with each invariant's current count, trend, threshold, owner, and last failure; the page reviewed on a cadence and its stale entries pruned
Evidence rules: Confirmed requires the query and its count against current data, the check's configuration showing the missing threshold or alert, the pipeline's latest-record age, the drift count, or the production query returning test rows in metrics. An invariant assumed broken without a query is Likely. Data that passes every check is reported as such with the baseline table, because the baseline is the deliverable even when nothing is wrong. Defer to the organisation's own data governance where it exists, and say where the checks do not match it.
Output Format
Start with a 3–5 line executive summary: invariants inventoried, how many have checks with alerts and owners, the worst violation found with its count, pipeline freshness state, and the first check to add.
Invariant table:
| Table | Invariant | Check query exists | Scheduled and thresholded | Alert and owner | Baseline count today | Trend | Enforceable by constraint | Repair path | Status |
|---|
Findings: each with the invariant, the evidence, severity (Critical for money, identity, or ownership violations with counts, or a pipeline silently stale on data decisions depend on; High for orphans, duplicates, and drift on core tables, checks with no alert; Medium for null creep, staleness, test rows in metrics; Low for reporting hygiene), and fix. Freshness and reconciliation: each pipeline with latest age, volume trend, and count reconciliation. Repair plan: scripts, dry-run results, approvals needed. Positive findings. Omit any section with nothing to report.
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.