Date Range and Freshness Validation in SQL

Catch stale data and malformed timestamps that standard quality checks miss.

· 10 min read
Cover illustration for “Date Range and Freshness Validation in SQL”
SQL Patterns for Quality · October 7, 2026 · 10 min read · 2,281 words

Date range and freshness validation in SQL asks a different question than the data quality checks most teams already run. Standard row-level checks (null checks, uniqueness checks, format checks) assert something about the rows that arrived. Freshness asserts something about the rows that did not arrive, and that distinction changes what kind of SQL you need to write and when you need to run it.

Freshness measures how old the newest row in a table is relative to the current moment. That makes it a table-level property, not a column-level one, and it explains a pattern familiar to anyone who has watched a dashboard go stale while every automated test stayed green: null checks pass, uniqueness checks pass, format checks pass, and the table is still four days behind. A source API changes its pagination scheme and starts returning empty pages that parse just fine. In each of these cases the table exists, the schema is intact, and every row sitting in it is perfectly valid. The missing rows are never written, and content-level checks have no mechanism for detecting an absence.

Date-range validation is the companion discipline, and it closes a different gap. Neither will a uniqueness check. These rows are present, parseable, and wrong in a way that only a check designed to ask "does this timestamp make sense" will ever surface.

Put the two disciplines side by side to see the shape of the problem. Freshness covers what is missing. Date-range validation covers what arrived malformed. They belong together as part of any serious data quality strategy, but they are not the same SQL pattern wearing different clothes, and treating them as interchangeable is how both gaps go unmonitored at once.

The two-clock problem: why a single timestamp query is not a complete freshness check

A complete freshness check depends on two independent clocks, and most implementations only read one of them. The first clock lives at the data layer: how old is the newest row in the table, right now. The second lives at the orchestration layer: did the pipeline that's supposed to populate that table actually run. Conflating these two facts into a single query leaves a blind spot that will eventually produce a stale dashboard nobody caught in time.

The data-layer check is the one most people write first: compare the maximum timestamp in a table against the current time. A check that only exists inside the pipeline cannot report on the pipeline's own absence. That's the core tension: the SQL pattern that measures data age is powerless to measure whether the measurement itself ever ran.

The practical consequence appears in a pipeline that fails before it writes anything. Only a comparison against the wall clock, run independently of the pipeline's own execution, catches that the table should have new rows and doesn't.

Freshness needs to be kept conceptually separate from latency: latency measures how long a single change takes to travel from its source to the warehouse, and it's measured per event. Freshness measures how old the newest data in a table is at the moment someone looks, and it's measured per table. A streaming pipeline can have excellent latency, moving each event through in seconds, and still serve badly stale data the moment it stops running, because freshness captures the state of the table now, not the historical speed of the pipeline that used to feed it. The SQL patterns that follow are built around this distinction: they measure table state at query time, and they need a threshold that accounts for how that state is expected to change over an interval, not an instant.

The core SQL pattern for a freshness check and choosing its threshold

The foundational freshness query is a single aggregation: SELECT MAX(event_timestamp) AS last_updated FROM user_events. That query returns the age of the newest row in the table. In an automated workflow, the returned value gets compared against CURRENT_TIMESTAMP, and the gap between the two shows whether the table falls inside or outside its acceptable window.

The column chosen for that comparison changes whether the check flags valid data as stale. An order placed last Tuesday and processed today still carries last Tuesday's order date; that's correct data, not stale data, and a freshness check anchored to the business timestamp would flag it incorrectly. If the source system doesn't provide any timestamp at all, the loader itself should add one at ingestion time, so the freshness check always has a column it can trust to reflect when the row actually landed.

Choosing a threshold is the next decision, and a sane starting default is twice the load interval. Setting the threshold at exactly one interval invites alert fatigue from routine timing jitter; setting it much looser than two intervals delays detection of a real problem.

Scheduling alignment is just as important as the threshold value itself. If an ingestion pipeline completes at 7:30 a.m. and the freshness check runs at 7:00 a.m., the check will fail every single day, and it will fail for the wrong reason: the data isn't stale, the check just ran before the data had a chance to arrive. That kind of false failure teaches the team to ignore the alert rather than investigate it, eroding trust in monitoring. The check's schedule has to trail the expected completion time of the load it's checking, with enough margin to absorb normal variance.

Writing a completeness check alongside the freshness check

A freshness check answers one question: how old is the newest row. It does not answer a closely related question: whether the expected volume of rows for this period actually arrived. That second question belongs to a completeness check, and the two together confirm that a load was both recent and real.

The SQL pattern for completeness on a time partition is straightforward: SELECT COUNT(*) FROM raw.orders WHERE updated_at::date = CURRENT_DATE. A zero count on a day that should have rows is a missed load entirely rather than merely an old table, and the distinction matters for diagnosis. A stale table suggests a slow or delayed pipeline. A zero-row day suggests the pipeline didn't write anything, which points investigation toward the source system or the job itself.

Bruin's custom_checks block demonstrates this two-check pattern in a form practitioners can deploy directly: a freshness assertion blocks downstream assets when the newest row exceeds the defined threshold, while a separate, non-blocking completeness assertion checks today's partition and raises a warning without halting the pipeline. The blocking-versus-non-blocking distinction here is deliberate. A slow but legitimate source shouldn't bring the whole pipeline to a stop over a partial day's data, but the anomaly still needs to appear somewhere a human will see it.

Date-range validation: catching structurally wrong timestamps that content checks miss

Date-range validation targets a category of error that both null checks and format checks pass cleanly: a timestamp that is present, well-formed, and logically impossible. The date itself makes no sense.

Three failure modes recur across production systems often enough to be treated as canonical. Epoch-zero dates, stamped 1970-01-01 00:00:00, appear when an uninitialized Unix timestamp defaults to zero. None of these trip a null check, since the field isn't empty, and none trip a format check, since the value is a syntactically valid timestamp.

A base range-check query looks for exactly these conditions: SELECT COUNT(*) FROM orders WHERE order_date > CURRENT_DATE OR order_date < '1900-01-01'. The specific bounds chosen here are a business decision that has to be made explicitly and documented. An undocumented magic date embedded in a WHERE clause becomes a maintenance hazard the moment the person who wrote it moves to a different team.

The future-date case deserves particular attention in event tables feeding forecasting models. The two disciplines are separate, but a failure in one can propagate into a false reading in the other.

This same conceptual separation appears in how platforms structure their built-in rule engines. Microsoft Purview's data quality framework treats conformity, which ensures data follows formatting standards and allowed values, as a distinct dimension from timeliness, which measures freshness. Range checks enforce structural plausibility; freshness checks enforce recency. AWS Glue's DQDL expresses the same split natively: a DataFreshness rule on an Order_Date column checks recency against a configurable duration threshold, while a separate ColumnValues rule bounds that same column's acceptable range. Both platforms define these as two distinct rule types for the same reason the SQL patterns above are written separately: recency and plausibility are different claims about a timestamp, and conflating them into one check loses the ability to diagnose which one failed.

Timezone drift and UTC normalization as a source of false failures and silent staleness

Timezone-naive comparisons are one of the most common causes of both false freshness failures and silently accepted bad dates in any pipeline that spans more than one region or source system. The failure is subtle because the SQL runs without error. It just returns the wrong answer.

The mechanism is simple once named. A table stores timestamps in local time, say US/Eastern. The freshness check runs CURRENT_TIMESTAMP in UTC. The lag calculated between the two is now systematically wrong by the UTC offset: a table that finished loading at 11 p.m. Depending on the threshold set, that offset alone can be enough to trigger a false alert, or, in the opposite direction, to mask real staleness that happens to fall within the offset window.

Range checks carry the same exposure in the other direction. A future-date check comparing a locally stored timestamp against CURRENT_DATE in UTC can flag perfectly legitimate records that were loaded late in the evening in a UTC+N timezone, since local evening hours roll over into the next UTC calendar day. These false positives do the same damage to trust in the check that scheduling misalignment does to freshness alerts: a team that sees the same harmless flag every evening learns to ignore it, and the check stops functioning as a check.

Multi-source pipelines carry the sharpest version of this risk. Normalizing every timestamp to UTC at the point of comparison, rather than trusting the stored value's implicit zone, is the discipline that prevents this class of error from reaching production undetected.

Missing partitions as a freshness failure that timestamp comparison cannot catch alone

A freshness check built on MAX() has a structural blind spot: it cannot detect a missing partition when the partitions before it are also absent, because MAX() simply returns the most recent row that exists, with no awareness of the row that should exist and doesn't.

The gap sits invisible in the table until some downstream query aggregates across a date range and returns a total that's quietly short, with no error anywhere in the chain to explain why.

Closing this gap requires a different SQL shape than the aggregation used so far: a generated reference series of expected partition dates, left-joined against the actual table, where any NULL in the join result marks a missing partition. expected_date FROM (SELECT generate_series(CURRENT_DATE - 7, CURRENT_DATE, INTERVAL '<step>')::date AS expected_date) d LEFT JOIN partitioned_table t ON t.event_date = d.expected_date WHERE t.event_date IS NULL`. BigQuery and Snowflake each offer their own equivalent approaches to generating this kind of date spine, and the underlying logic, a reference series joined against reality to expose the gaps, holds regardless of which platform's syntax implements it.

Adding a GROUP BY event_date HAVING COUNT(*) = 0 clause to the join surfaces zero-row partitions separately from genuinely missing ones. The two point to different root causes upstream, so distinguishing them matters for diagnosis.

Partition-gap checks need to run after a load completes, not during it.

Where in the pipeline each check type should run

Freshness, completeness, and date-range checks each belong at a different stage of the data lifecycle. Deploying all of them at ingestion, or all of them only at the serving layer, misses structural errors that are only visible at one specific point in the pipeline.

This is the fastest and cheapest point in the pipeline to reject malformed data, before it has a chance to propagate into downstream tables and corrupt anything built on top of it.

During transformation, the relevant checks shift toward relationships between tables. So does cross-table consistency, confirming that the same event timestamp agrees across tables that were joined from different sources.

Before serving, freshness checks and partition-gap checks belong as the final gate. These checks only produce a meaningful result once the full load has completed, and they sit directly between the pipeline and whatever consumes its output, whether that's a dashboard, a model, or an AI workflow built on top of the warehouse. A table that passes every check at ingestion and transformation can still be the wrong table to serve if it's stale or missing a partition. This stage carries its own distinct checks.

Platforms increasingly bake this scheduling logic into their own monitoring layers rather than leaving it to hand-written SQL run on an ad hoc basis. Snowflake's data metric functions run on a defined schedule and log their results automatically, replacing one-off queries with a repeatable layer, though the scheduling still has to align with load completion or the results will produce the same false failures a misaligned manual check would. Microsoft Purview offers a pre-built, no-code timeliness rule that scores 100 for a pass and 0 for a fail, a useful shortcut for governance-oriented workflows, though its platform support is limited and doesn't currently extend to Snowflake, Azure Databricks Unity Catalog, Google BigQuery, Synapse, or Microsoft Azure SQL. The lifecycle placement, ingestion, transformation, serving, matters more than which specific tool executes the check at each stage: it's the framework that determines which failure modes get caught at each point.

Sources

  1. Create Data Quality Rules in Unified Catalog

More in SQL Patterns for Quality