Everything so far has cleaned data that’s already known to be dirty. This chapter shifts focus: writing queries whose entire purpose is to find problems proactively — before a report is built, before a pipeline loads data downstream, before a stakeholder notices something is wrong.
Data validation queries check for structural and logical correctness: missing required fields, foreign keys pointing to nothing, dates that don’t make sense, numeric values outside a valid business range. Unlike the cleaning techniques in earlier chapters, validation queries typically don’t fix anything — they flag, count, and report, so a human or an automated gate can decide what happens next.
Bad data is far cheaper to catch before it loads into a warehouse or reaches a dashboard than after. A validation layer at the point of ingestion (or as a scheduled check against existing tables) turns “we found out three weeks later that Q2 numbers were wrong” into “the pipeline flagged 12 invalid rows this morning, before anyone saw a bad report.”
An order with a customer_id that doesn’t exist in the customers table. An employee record with a hire date in the future. A product with a negative price. A patient record with an age of 214. None of these are hypothetical edge cases — they are the normal output of systems without strict validation, migrations between schemas, and manual data entry.
order.customer_id has a matching row in customers before the nightly revenue report runs.quantity_produced is never negative and that defect_count never exceeds quantity_produced.Validation checks catch referential integrity gaps (orphaned foreign keys), logical impossibilities (negative quantities, future birth dates), and completeness gaps (required fields left blank) before they silently corrupt downstream calculations.
Validation Gate Pattern
──────────────────────────
Raw / Incoming Data
│
▼
┌─────────────────┐
│ Validation Query │──── fails? ──► Quarantine / Alert / Reject
└─────────────────┘
│
passes
│
▼
Trusted Layer (safe for reporting)
-- Missing required field
WHERE required_column IS NULL
-- Orphaned foreign key
SELECT c.*
FROM child_table c
LEFT JOIN parent_table p ON c.parent_id = p.id
WHERE c.parent_id IS NOT NULL AND p.id IS NULL;
-- Invalid date range
WHERE event_date > CURRENT_DATE
OR event_date < '1900-01-01'
-- Negative value that should never be negative
WHERE quantity < 0
-- Impossible age
WHERE age < 0 OR age > 120
The orphaned foreign key pattern (LEFT JOIN ... WHERE parent.id IS NULL) is one of the most useful validation patterns in this chapter: it finds every child row whose foreign key points to a parent that doesn’t exist, which an INNER JOIN-based query would simply hide by excluding those rows entirely. This is the same NULL behavior from Chapter 01, now used deliberately as a detection tool rather than treated as a bug.
Range validation (age < 0 OR age > 120, event_date > CURRENT_DATE) encodes business rules directly into SQL, and should be revisited periodically — business rules change (a “impossible age” threshold might differ for a life insurance company vs. a pediatric clinic), so hardcoded thresholds should be documented, not just embedded silently.
Orphaned foreign key checks via LEFT JOIN ... WHERE ... IS NULL scale better on properly indexed foreign key columns than NOT IN subqueries, and unlike NOT IN, they aren’t vulnerable to unexpectedly returning zero rows if the subquery contains NULLs.
NOT IN (SELECT parent_id FROM parent_table) for orphan detection — silently breaks (returns zero rows) if any parent_id in the subquery is NULLage > 120 vs. age >= 120) — document the choice, since it’s easy to be off by one at the boundaryLEFT JOIN ... WHERE parent.id IS NULL over NOT IN for orphan/referential checksNOT IN risky for this compared to LEFT JOIN?Validation queries exist to catch bad data before it does damage, not after. The orphaned foreign key pattern (LEFT JOIN ... WHERE ... IS NULL) and simple range checks (negative values, impossible dates or ages) cover the majority of real-world validation needs, and should run as a gate ahead of any trusted reporting layer, not as an afterthought.
customer_id does not exist in the customers table.hire_date in the future.NOT IN is risky for finding orphaned foreign keys, with a concrete example involving a NULL value in the subquery.