Small warehouses fail quietly first.
A metric is renamed. A nullable field becomes mostly null. A dashboard keeps loading after the meaning changed. Size only delays the cost.
Start with grain: what does one row mean? Event, user, day, account, file, model run, or generated answer? A vague grain turns joins into folklore.
Dimensions drift too. Status, plan, severity, source, category, route, model, and region values change with the product. Record allowed values, owner, source, deprecated values, null behavior, backfill rules, and dashboard dependencies.
Freshness should be measured:
select metric_name, max(observed_at) as last_seen
from metric_observations
group by metric_name;
Every decision-facing table needs an expectation: event freshness within 15 minutes, daily facts by 06:00 UTC, model summaries hourly, billing snapshots daily. If freshness does not matter, say so.
Retention is policy. Name it by data class, including raw-log lifetime, aggregate lifetime, deletion or anonymization behavior, prompt redaction, and derived metrics.
Null rates, duplicate keys, and join coverage catch cheap failures. A dimension miss can disappear through an inner join; unknown is often better than invisible.
Metrics need versioned definitions: name, owner, source tables, filters, and effective date. A stable number with a changed meaning is not stable.
A small warehouse stays useful when grain, dimensions, freshness, nulls, joins, retention, and metric definitions remain explicit.
Related posts

About Jeremy London
Engineering leader and builder in Denver. I write about AI platforms, agents, security, reliability, homelab infrastructure, and the parts of engineering work that have to survive production.