Skip to main content

The Five Pillars: Freshness and Volume Monitors in SQL

Level 10: Distributed Compute & Data Operationsmedium28 minfive observability pillarsfreshness SLA breach detectiontrailing-window baselinesvolume anomaly detectiondistribution driftwindow functionsdate arithmetic

Name the five observability pillars, then implement the freshness, volume, and distribution monitors as ordinary SQL over table metadata rather than over the data itself.

Five pillars, and why they are five

Data observability is usually framed as five pillars, and the framing is worth memorizing because interviewers use it as a checklist:

  1. Freshness. Is the data recent enough? A table that stopped loading at 2 a.m. looks perfectly correct and is completely wrong.
  2. Volume. Did the expected amount of data arrive? Half a load is the failure people notice last.
  3. Schema. Did the shape change? A dropped or retyped column breaks consumers that never touched the pipeline.
  4. Distribution. Are the values still shaped the way they were? A null rate that jumps from 1% to 6% is a broken join upstream.
  5. Lineage. What is upstream and downstream of this table? Without it you cannot answer "what else is affected".

The metric all five exist to shrink is data downtime: the total time your data was wrong, late, or missing. Framing your monitoring answer around reducing data downtime rather than around "adding alerts" is what separates an engineer from someone who has installed a tool.

Monitors read metadata, not data

Here is the design decision that makes always-on monitoring affordable. A monitor does not scan the table it watches. It reads the metadata a warehouse already keeps about that table: when it was last written, how many rows it holds, what its columns are. Scanning a billion-row fact table every fifteen minutes to ask "is it fresh" would cost more than the pipeline that built it.

So the monitors below run over two metadata tables. warehouse_tables is the catalog's own record of each table (owner, last write, agreed SLA). table_snapshots is the daily profile a monitoring job appends: row count and null rate per table per day. Both are tiny, and both can be queried every few minutes forever.

Table
Each pillar is one query shape over metadata. Schema and lineage are the next lesson; freshness, volume, and distribution are this one.
pillarwhat breaks when it failsthe SQL shape
freshnessthe dashboard shows yesterday and nobody noticesage from last_loaded_at compared to an SLA in minutes
volumehalf the rows arrived and the totals quietly droplatest row_count against a trailing-window AVG, with a deviation threshold
schemaa dropped or retyped column breaks every consumertwo catalog snapshots diffed with a self-join
distributiona broken join fills a column with NULLslatest null rate or category share against its own trailing average
lineageyou cannot answer what else is affectedan edges table walked with a recursive CTE
Each pillar is one query shape over metadata. Schema and lineage are the next lesson; freshness, volume, and distribution are this one.

Freshness: age against an SLA

Freshness is subtraction. Take the evaluation time, subtract last_loaded_at, convert to minutes, and compare to the table's agreed freshness_sla_minutes. In SQLite that is strftime('%s', ...) on both sides, which gives epoch seconds:

(strftime('%s', '2026-03-02 09:00:00') - strftime('%s', last_loaded_at)) / 60

Two details decide whether the monitor is trustworthy. First, an SLA is per table, not global: a streaming staging table is late after 30 minutes, a slowly changing dimension is fine for a day. Second, a table sitting at exactly its SLA is not yet breaching, so the comparison is strictly greater than. Getting that wrong is how a monitor starts paging on healthy tables and gets muted.

This is not the same freshness Level 9 measured. de-l9-freshness-slas measures the age of the DATA: how far behind real time the newest event in the table is, derived from event timestamps. That is the pipeline-side question and it needs the table's contents. This lesson measures the age of the WRITE: how long since anything landed, read from catalog metadata. A table can be freshly written and still full of stale events (the source stopped producing), and it can hold very recent events while the write itself is hours late. Interviewers ask which one you mean, so say it: catalog freshness is the cheap always-on watcher, event-time freshness is the accurate one that costs a scan.

Volume: today against its own history

A row count means nothing on its own. It means something against a baseline, and the standard baseline is the same table's trailing window. A window function gives you it directly:

AVG(row_count) OVER (
  PARTITION BY table_name ORDER BY snapshot_date
  ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING
)

1 PRECEDING as the upper bound is the part people get wrong. Including today in its own baseline drags the average toward the anomaly and hides small breaks. Then compare with a percentage deviation and a threshold, and make the comparison two-sided: a load that doubles is as suspicious as a load that halves, and duplicate-producing bugs are at least as common as missing-data bugs.

Distribution: the same query on a different column

Once you have the trailing-window shape you get the distribution pillar almost free. Swap row_count for null_user_id_rate and you are monitoring whether a column's null rate suddenly doubled, which is what a silently broken join looks like from the outside. The same shape monitors a category's share of rows, an average order value, or a percentage of negative amounts.

Common mistake: including the current day inside its own trailing baseline. With a 7-day window that pulls the average about 12% toward whatever today did, which is exactly the direction that hides a real anomaly. Bound the window at 1 PRECEDING.

Interview nuance: "how would you monitor this pipeline in production" is scored on two things: naming the five pillars without hesitating, and being able to sketch the freshness and volume queries on a whiteboard. Freshness and volume are the pair an intern is expected to implement, and they catch most real incidents. Mentioning that the monitors read metadata rather than the tables themselves is the detail that shows you have thought about the cost.

On a real platform this differs. Monte Carlo, Soda, and Bigeye ship these monitors as configuration rather than SQL, and they learn the thresholds from history instead of taking a hardcoded 50%. dbt has dbt source freshness for the freshness pillar specifically, with warn_after and error_after in the source config. The metadata is also already there in real warehouses: Snowflake's INFORMATION_SCHEMA.TABLES carries LAST_ALTERED and ROW_COUNT, BigQuery's __TABLES__ carries last_modified_time and row_count, and Glue keeps partition-level metadata. The queries you are about to write are the ones those products run for you.

Sample data for this example
CREATE TABLE warehouse_tables (
  table_name            TEXT,
  owner                 TEXT,
  last_loaded_at        TEXT,     -- catalog metadata: when the table last received a write
  freshness_sla_minutes INTEGER   -- how stale the table is allowed to get before it breaches
);
INSERT INTO warehouse_tables (table_name, owner, last_loaded_at, freshness_sla_minutes) VALUES
  ('fct_orders',           'analytics', '2026-03-02 08:45:00',   60),
  ('dim_customer',         'analytics', '2026-03-02 06:30:00',  180),
  ('fct_web_events',       'growth',    '2026-03-02 02:10:00',  120),
  ('stg_clickstream',      'growth',    '2026-03-02 08:10:00',   30),
  ('mart_marketing_spend', 'growth',    '2026-03-02 04:45:00',  240),
  ('dim_channel',          'growth',    '2026-03-02 06:00:00',  180),
  ('mart_daily_revenue',   'finance',   '2026-03-01 23:00:00',  480),
  ('fct_payments',         'finance',   '2026-03-01 18:20:00',  360),
  ('dim_product',          'merch',     '2026-03-02 07:55:00',   90),
  ('dim_store',            'merch',     '2026-02-28 21:00:00', 1440);
Worked example (SQL)
-- The freshness monitor, evaluated at a fixed 2026-03-02 09:00. Note dim_channel: stale by
-- exactly its SLA, and therefore not yet breaching.
SELECT table_name,
       owner,
       (strftime('%s', '2026-03-02 09:00:00') - strftime('%s', last_loaded_at)) / 60 AS minutes_stale,
       freshness_sla_minutes,
       CASE
         WHEN (strftime('%s', '2026-03-02 09:00:00') - strftime('%s', last_loaded_at)) / 60 > freshness_sla_minutes
           THEN 'BREACHED'
         ELSE 'fresh'
       END AS freshness_status
FROM warehouse_tables
ORDER BY minutes_stale DESC;

Apply

Your turn

The task this lesson builds to.

Write a query that returns every table breaching its freshness SLA as of '2026-03-02 09:00:00', as (table_name, minutes_late), most late first, over warehouse_tables(table_name, owner, last_loaded_at, freshness_sla_minutes).

A table breaches when its age in minutes is strictly greater than freshness_sla_minutes, and minutes_late is how many minutes past the SLA it is. Take the age from epoch seconds, the way the demo does, so whole minutes come from integer division rather than from rounding. Alias the computed column exactly minutes_late.

3 hints and 1 automated check are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, plus 3 bonus drills.

Write a query that returns every table whose latest row count deviates by more than 50% from its trailing 7-day average, as (table_name, latest_rows, trailing_avg, deviation_pct), biggest absolute deviation first, over table_snapshots(table_name, snapshot_date, row_count, null_user_id_rate).

The trailing average covers the 7 snapshot days before the latest one and must not include the latest day itself. deviation_pct is the signed percentage difference from that average, so a collapsed load comes back negative and a doubled load comes back positive. Round trailing_avg and deviation_pct to 1 decimal place and alias every column exactly.

3 hints and 1 automated check are waiting in the workspace.