learn-data-observability-with-phoebe / Builder session 3 of 8
Learn Data Observability with Phoebe · Builder session 3 of 8

Volume and uniqueness

A row count is the cheapest signal in the warehouse and the easiest one to get wrong, because the number means nothing until you say what you are comparing it against. Tonight you build two monitors out of the same metrics table. The first catches a load that arrived with a third of its data missing. The second catches the failure class where the row count is perfect, freshness is perfect, the pipeline reported success, and every downstream sum is wrong.

🟡 Core Analytics engineers 45 min
0-3 · Welcome 3-20 · Counting is harder than it looks 20-40 · Build-along: two monitors, two failures 40-45 · Q&A
Part 0

Two signals, two failure classes

The metrics table already stores everything both monitors need: rows and distinct_keys, one row per table per night. analytics.orders_daily at Bellwether carries about 12,000 rows on a weekday and about 45 percent of that at the weekend, which is the fact that decides the shape of the volume monitor before you write a line of it. Uniqueness is the other half of tonight, and it exists as a separate monitor for one reason: its failure mode is invisible to every other check in the set.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. Two working monitors and the measured cost of getting the volume comparison wrong: a fixed plus-or-minus 30 percent band around 12,000 fires on all eight weekend nights in a 30-night window, which is eight false pages for zero catches, while the same-weekday median catches the partial load with zero. Plus the duplicate-rate query that is the only thing in the whole set that sees night 19.

LiveWhy a row count is not a number3 min▶

Twelve thousand rows. Is that fine? There is no answer to that question, and every volume monitor that gets muted was built by somebody who thought there was. The count only becomes a signal once you attach a comparison, and choosing the comparison is the design work. Three candidates come up every time:

  • A fixed band. Cheap to write, needs no history, and wrong on any table with a weekly rhythm. Bellwether's is the ordinary case, not an unusual one: grocery delivery is quieter at the weekend and no single band holds both patterns while still being tight enough to catch a missing shard.
  • The same weekday. Compares Tuesday against Tuesdays. Handles the weekly rhythm exactly, needs four weeks of history to be comfortable, and is the right default for anything driven by human behaviour.
  • A robust band around the median. For tables with no weekly rhythm at all, where the useful question is only "is tonight unlike the recent past" and you want the estimate not to be dragged around by the incident you are trying to detect.
Real world

Bellwether's night 16 arrived with one of three source shards missing: 4,920 rows against a same-weekday median of 11,986, which is 59 percent short. The fixed band caught it too. The difference is that the fixed band also paged on eight quiet weekend nights on the way there, and by the time night 16 arrived the on-call had learned to expect the alert.

Self-studyWhat each of the two monitors can and cannot see3 min▶

Worth being explicit, because the temptation is to think of volume as a general-purpose health check and it is nothing of the kind.

FailureVolume sees itUniqueness sees it
A source shard never arrivedYes, the count drops sharplyNo, the surviving rows are clean
A merge key changed and rows duplicatedOnly if the net count moves enoughYes, directly
Duplicates added and other rows droppedFrequently not, the two cancelYes, the rate moves regardless
The same correct rows loaded twice as a full replayYes, the count roughly doublesYes, every key is now duplicated
A column went null across every rowNo, the count is unchangedNo, unless the key is the null column

That last row is the honest boundary of tonight: a null-rate collapse leaves both of these monitors green, which is why distribution is a separate signal with its own session.

Part 1 · covers fixed bands, same-weekday medians and robust bands

Volume: the comparison is the monitor

9 min live

Plot the series before choosing the comparison. Thirty nights of orders_daily has a rhythm you can see from across the room, and the moment you can see it the fixed band stops looking like a reasonable simplification.

Thirty nights of row counts on analytics.orders_daily weekday about 12,000 rows; the weekend is about 45 percent of that weekday weekend night 16, partial load fixed band, 8,400 to 15,600 same-weekday median fixed ceiling 15,600 fixed floor 8,400 night 16: 4,920 rows vs median 11,986 = -59 percent 1 5 10 15 20 25 30 each amber square is a false page from the fixed band The fixed band pages on 8 weekend nights and catches nothing extra for them.
🔍 Click to zoom - the rhythm is visible from across the room, and the fixed band ignores it.
LiveThe fixed band, priced properly3 min▶

The band is 12,000 plus or minus 30 percent, so a floor of 8,400 and a ceiling of 15,600. A weekend night at about 5,400 rows is not near that floor, it is 36 percent below it, so every weekend night is an alert by construction. In a 30-night window that is eight false pages, and they buy nothing: night 16 at 4,920 rows is caught by the same-weekday comparison too.

Widening the band does not rescue it. To hold a weekend night inside the band you need a floor below 5,200, which means a band of roughly plus or minus 57 percent around 12,000, and a weekday night can then lose 6,000 rows without a peep. That is the trade in its clearest form: a band wide enough to be quiet is too wide to detect the thing you built it for.

The tell that you have the wrong comparison. Your false pages cluster. If every alert lands on a Saturday, or at month end, or on the first working day after a holiday, the monitor has discovered a real pattern in your business and is reporting it as a fault. Move the pattern into the comparison rather than into an exclusion list.

LiveThe same-weekday median, and why median3 min▶

Compare tonight against the median of the last four same weekdays, and alert outside plus or minus 25 percent. On Bellwether's history that monitor produces zero false pages in 30 nights and catches night 16 with the sentence 4920 rows vs a same-weekday median of 11986, -59 percent, which is a message somebody can act on without opening anything.

Median rather than mean, for a reason that matters more than it sounds. Your history contains the incidents you are trying to detect. A mean of four values is moved by roughly a quarter of any outlier in the window, so the night after a partial load your comparison point has dropped and your threshold has quietly loosened exactly when you need it tight. A median of four values is unmoved by one outlier at either extreme. The property you want in a monitor is that a past incident does not raise the bar for detecting the next one.

  • Four same weekdays is four weeks of calendar, which is long enough to be stable and short enough that a real change in the business washes through within a month.
  • Twenty-five percent is wide enough to absorb ordinary week-to-week movement on this table and tight enough that a missing shard of any size clears it. Set yours from your own spread, not from this page.
  • Both directions. A count 40 percent above the median is a duplicate replay or a backfill running into today's partition. Firing only on the low side is the most common half-built volume monitor.
  • Guard the cold start. With fewer than two same weekdays in history the monitor must return "not enough history" rather than a comparison against one night.
Self-studyRobust bands with MAD, for tables with no rhythm4 min▶

Plenty of tables have no weekly pattern: a machine-generated event stream, a reference table, a slowly growing dimension. There the same-weekday comparison throws away three quarters of your history for nothing, and what you want is a band around the recent median that is not dragged around by outliers.

Median absolute deviation is the standard tool. Take the median of the series, take the absolute deviation of every point from it, take the median of those. Multiply by 1.4826 and you have a robust stand-in for a standard deviation on roughly normal data, which lets you keep thinking in familiar multiples.

★ Try it now with m as ( select rows from analytics.obs_metrics where table_name = 'analytics.orders_daily' and load_date >= current_date - 30 ), c as ( select percentile_disc(0.5) within group (order by rows) as med from m ) select c.med, percentile_disc(0.5) within group (order by abs(m.rows - c.med)) as mad, 1.4826 * percentile_disc(0.5) within group (order by abs(m.rows - c.med)) as sd_robust from m cross join c group by c.med;

Then band at the median plus or minus three times sd_robust. Two cautions. On a strongly seasonal table this is the wrong tool and will simply widen until it holds the weekend, so check for rhythm first. And on a table with genuinely tiny variance the MAD can collapse towards zero, which makes the band absurdly tight; floor it at some absolute minimum that means something in the units of the table, exactly as a percentage floor is used on a null-rate monitor.

Real world

The failure this avoids is well worth seeing once. Set a mean-plus-3sd band on 30 nights that include one 60-percent-short night, and the outlier both drops the mean and inflates the standard deviation, so the lower bound moves down twice. The band that was supposed to be tightened by the incident is wider afterwards than it was before.

Part 2 · covers duplicate rate, grain and the dual failure

Uniqueness: the failure that leaves the row count healthy

8 min live

On Bellwether's night 19 a merge key changed upstream. The duplicate rate went from 1.0 to 8.4 percent and the row count stayed normal, so the pipeline reported success, the freshness check passed and the volume monitor passed. Nothing in the set saw it except a rate that nobody had built yet.

Night 19: a merge key changed upstream Row count count(*) the number everybody checks 12,000 12,000 typical night night 19 passes, looks normal Distinct keys count(distinct order_id) scale starts at 10,800 11,880 10,992 typical night night 19 888 keys short, no rule broken Duplicate rate 1 - distinct(key)/count(*) limit from history: 2.0 percent 1.0 percent 8.4 percent typical night night 19 fires, 8.4 against a 2.0 limit pipeline success: pass freshness: pass volume: pass duplicate rate: fires A normal row count is what makes a dedup failure silent. Only the rate sees it.
🔍 Click to zoom - three panels, one failure, and only the third monitor notices.
LiveA rate, not an assertion4 min▶

The duplicate rate is 1 - count(distinct key) / count(*), and on a large table it is a better monitor than a hard uniqueness assertion for three reasons that all show up in the first month.

  • It degrades gracefully. A hard assertion has two states. A rate has a history, so you can see 0.6 percent creeping to 1.4 percent over a fortnight and fix the cause before it is an incident. A pass-or-fail check gives you no warning at all and then a build failure.
  • It gives you a sentence. "Duplicate rate 8.4 percent against a limit of 2.0, up from 1.0 last night" is a message that survives being forwarded to somebody who does not know the table. "unique_order_id failed" is a message that requires a query before anybody can act.
  • It tolerates a known floor. Many real tables have a small, understood duplicate population: a replayed event, a soft-deleted row, a late correction. A hard assertion on such a table is either disabled or wrapped in an exclusion list that nobody maintains. A rate lets you set the threshold above the known floor and monitor the deviation from it.

Set the threshold from the history, the way you set the freshness threshold. On this table the clean nights sit between about 0.6 and 1.4 percent, so a 2.0 percent limit is above every clean night and far below the 8.4 percent of the incident. It fires once in 30 nights and that once is night 19.

LiveComposite keys and the grain question4 min▶

This is where most duplicate monitors are actually wrong, and the error is invisible because the monitor is green. Before you can measure uniqueness you have to answer one question about the table: what is one row? One order? One order line? One order line per warehouse per day? A duplicate monitor is only meaningful against the declared grain, and if the grain in your head differs from the grain in the table, the monitor measures nothing.

★ Try it now select count(*) as rows, count(distinct order_id) as distinct_orders, count(distinct order_id || '|' || coalesce(status, '~')) as distinct_grain, 1 - count(distinct order_id) / count(*)::float as dup_rate_order, 1 - count(distinct order_id || '|' || coalesce(status, '~')) / count(*)::float as dup_rate_grain from analytics.orders_daily where load_date = current_date;

Run both rates side by side on any table you are unsure about. If dup_rate_order is 40 percent and dup_rate_grain is 0, the table is not one row per order and a monitor on order_id alone would page every single night until somebody muted it. If both are 0, one row is one order and the simple monitor is correct.

  • Concatenate carefully. A naive a || b makes ('ab', 'c') and ('a', 'bc') collide. Use a separator that cannot occur in the values, and coalesce the nulls, or the whole key silently becomes null and the distinct count collapses.
  • Nulls in a key are not distinct in the way you expect. count(distinct) ignores nulls entirely, so a key column that is 30 percent null produces a duplicate rate that looks alarming for the wrong reason. Monitor the key's null rate alongside its duplicate rate.
  • Write the grain down next to the table. One sentence in the model's documentation. This is the single cheapest piece of documentation in a warehouse and the one most often missing.
Self-studyThe dual failure: duplicated and dropped at once4 min▶

The nastiest version of this failure is the one where a merge duplicates some rows and drops others, so the net row count barely moves. It is common because it has a single cause: a join key that changed cardinality upstream will both fan out the rows that now match several times and lose the rows that now match nothing.

What that does to your monitors is worth spelling out. Volume sees a net change of maybe one percent and passes. Freshness passes. The pipeline passes. The duplicate rate rises, because the fanned-out rows are genuinely duplicated at the declared grain, and it is the only signal that moves enough to cross a threshold.

The pair of numbers that identifies it. Store rows and distinct_keys separately, never just the rate. When the rate rises you then want to know which of the two moved: distinct keys falling while rows hold steady is a fan-out, and both falling together is a fan-out plus a drop. That distinction is the difference between a re-run fixing it and a re-run making it worse.

Build-along

Two monitors that catch two different failures

★ 20 min · everyone builds

Five steps. Build the same-weekday monitor, build the fixed band beside it so you can count its pages yourself rather than take the number on trust, build the duplicate rate, run all three over the history, then write down the evidence for each threshold.

Step 1. The same-weekday median. Four weeks back gives up to four same weekdays. As with the freshness percentile, a moving median is awkward as a windowed aggregate in portable SQL, so join the metrics table to itself on weekday and date range and aggregate.

★ Try it now with m as ( select load_date, extract(dow from load_date) as weekday, rows from analytics.obs_metrics where table_name = 'analytics.orders_daily' ), same_wd as ( select m.load_date, m.rows, h.rows as hist_rows from m join m as h on h.weekday = m.weekday and h.load_date < m.load_date and h.load_date >= m.load_date - 28 ) select load_date, max(rows) as rows, count(*) as same_weekdays_seen, percentile_disc(0.5) within group (order by hist_rows) as wd_median from same_wd group by load_date order by load_date;

Step 2. Turn it into a monitor with a tolerance and a cold-start guard. Save step 1 as a view called weekday_history. Both directions, 25 percent, and a refusal to compare against a single night of history.

★ Try it now select load_date, rows, wd_median, round((rows - wd_median) * 100.0 / wd_median) as delta_pct, case when same_weekdays_seen < 2 then 'not enough history' when abs(rows - wd_median) > 0.25 * wd_median then 'FIRE' else 'ok' end as monitor from weekday_history order by load_date;

Night 16 comes out as 4920 rows vs a same-weekday median of 11986, -59 percent. Every other night in the 30 is inside the tolerance, so this monitor costs zero false pages.

Step 3. Build the fixed band beside it and count its pages. Do not take the eight on trust; the point of running it is that you see which nights they are.

★ Try it now with banded as ( select load_date, rows, case when extract(dow from load_date) in (0, 6) then 'weekend' else 'weekday' end as day_type, case when rows < 12000 * 0.70 then 'FIRE low' when rows > 12000 * 1.30 then 'FIRE high' else 'ok' end as fixed_band from analytics.obs_metrics where table_name = 'analytics.orders_daily' ) select day_type, count(*) as pages from banded where fixed_band <> 'ok' group by day_type order by day_type;

Two rows come back: weekend, 8 pages, and weekday, 1 page. All eight weekend nights fire, and the single weekday page is night 16, which the same-weekday monitor also caught. Eight false pages for zero additional catches, in one query you can show anybody. Drop the group by and read the night numbers if you want to see them individually.

Step 4. The duplicate rate. Straight out of the two columns the metrics table already stores, with a threshold set above every clean night.

★ Try it now select load_date, rows, distinct_keys, round((1 - distinct_keys / rows::float) * 100, 1) as dup_rate_pct, case when 1 - distinct_keys / rows::float > 0.02 then 'FIRE' else 'ok' end as monitor from analytics.obs_metrics where table_name = 'analytics.orders_daily' order by load_date;

One night fires: night 19, at 8.4 percent against a 2.0 percent limit, up from 1.0 percent. Look at that night's other columns while you are there. Rows are normal, lateness is normal, the column count is unchanged. This is the whole argument for uniqueness being its own monitor rather than a footnote on volume.

Step 5. Run all three over the full history together. Same-weekday volume: 1 catch, 0 false pages. Fixed band: 1 catch, 8 false pages. Duplicate rate: 1 catch, 0 false pages. Two monitors, two catches, no noise, and one alternative version of the volume monitor that costs eight pages for nothing.

Step 6. Write the evidence line for each threshold. Four fields: the comparison, the tolerance, the widest clean deviation in your history, and the pages it would have sent. For this table: same weekday median, 25 percent, widest clean deviation well inside it, zero pages. Duplicate rate, 2.0 percent, clean nights between 0.6 and 1.4 percent, one page and it was real.

That line is what makes a threshold reviewable. A threshold with no recorded evidence is indistinguishable from a threshold somebody invented, and after two staff changes there is nobody left who can tell you which one it was.

Set thresholds from history, not from a feeling. Every number on this page came out of a query over recorded nights: 25 percent because the clean deviations are far inside it, 2.0 percent because the clean nights top out at 1.4. If you cannot produce the query that justifies a threshold, you have a preference rather than a monitor, and preferences do not survive their first argument at 07:00.

Homework

Before session 4

30 min
  1. Plot 30 nights of row counts for your table and say out loud whether it has a weekly rhythm. That single answer picks between the same-weekday median and a robust MAD band, and it takes one chart.
  2. Backtest whichever you picked over the history you have and count the pages. If it would have fired more than about once a week, the tolerance is wrong, not the method.
  3. Answer the grain question in writing for one table: what is one row? Then run the two duplicate rates side by side and check that the answer in your head matches the answer in the data.
  4. Put the duplicate-rate monitor on the key of that table with a threshold above every clean night in your recorded history, and note the highest clean value you saw.
  5. Check whether your metrics table stores rows and distinct_keys as separate columns rather than only the derived rate. If it stores only the rate, fix that tonight; the missing count is the one you will want during an incident.
Check yourself

Three questions

Bellwether's weekend volume is about 45 percent of a weekday. A fixed plus-or-minus 30 percent band around 12,000 rows runs over a 30-night window. What does it cost?

The floor is 8,400 and a weekend night is about 5,400, which is 36 percent below it, so every weekend night alerts by construction. Night 16 is caught by the same-weekday median too, so the eight pages buy nothing.

Why compare against the median of the last four same weekdays rather than the mean of them?

After a partial load, a mean-based comparison point has dropped, so the threshold has loosened exactly when you need it tight. A past incident must not raise the bar for detecting the next one.

On night 19 a merge key changed upstream. The duplicate rate went from 1.0 to 8.4 percent and the row count stayed normal. Which checks pass?

A key change can duplicate some rows and drop others, landing within a percent of a normal night. That is the failure class uniqueness exists for, and it is why the rate is stored separately from the count.

Sources covered

What tonight maps to

✓ Same-weekday medians as a seasonal volume threshold The window length, the tolerance, both directions, the cold-start guard, and the fixed band priced beside it at eight false pages.
✓ Robust bands with median absolute deviation For tables with no weekly rhythm, including the 1.4826 scaling, the collapsing-variance trap and why the median beats the mean on a history containing incidents.
✓ Duplicate rate versus a hard uniqueness assertion Graceful degradation, the message it produces, the known-duplicate floor, composite keys and the grain question, and the dual failure that moves the net count barely at all.
◐ Statistical and machine-learning anomaly detectors Percentiles, medians and MAD used here as thresholds on metadata. Seasonal decomposition, isolation forests, one-class models and the rest belong to learn-anomaly-detection-with-phoebe.
◐ Scoring the two monitors against a full history Counted here by hand over 30 nights. Session 8 runs the whole set and prints catches, false pages and hours to detect together.
· Build-time uniqueness assertions in a transform project Not covered by design. A pass-or-fail test on data you produce is a different discipline; that is what the analytics engineering track is for.

Session 3 cheat sheet

The comparison is the monitorA row count means nothing alone. Choosing what it is compared against is the whole design decision.
Fixed band, priced12,000 plus or minus 30 percent: 8 weekend false pages in 30 nights, zero catches the weekday median missed.
Same-weekday medianLast 4 same weekdays, plus or minus 25 percent, both directions. Night 16: 4,920 vs 11,986, -59 percent, 0 false pages.
Median, not meanYour history holds the incidents. A mean-based comparison loosens the threshold right after one.
MAD for rhythmless tablesMedian of absolute deviations, times 1.4826, band at 3 of those. Floor it or tiny variance makes it absurd.
Duplicate rate1 - distinct(key)/count(*). Degrades gracefully, gives you a sentence, tolerates a known floor.
Answer the grain questionWhat is one row? Run the single-key and composite-key rates side by side before trusting either.
Night 19Dup rate 1.0 to 8.4 percent, row count normal. Pipeline, freshness and volume all pass. Store rows and keys separately.