Data Warehouse Best Practices That Prevent Expensive Rework
Practical guidance for warehouse grain, layers, idempotent pipelines, timestamps, data quality, privacy, cost, and ownership.
A data warehouse rarely fails because the database cannot run a GROUP BY. It fails because nobody agrees what a row represents, two dashboards define revenue differently, a retry duplicates yesterday’s load, or a pipeline silently stops before an executive meeting.
The practices below are deliberately unglamorous. They are the choices that keep a warehouse understandable after the original builder moves on.
Define the grain before the columns#
The grain is the fact represented by one row. Write it in plain language before creating a table:
- one row per completed order
- one row per API request
- one row per account per billing day
- one row per user’s first activation
If a table mixes grains, every aggregate becomes a trap. An order with three line items cannot share a row shape with an order-level shipping charge without introducing double counting.
Put the grain in the table description and encode it in the key where possible. A name such as fct_orders is less useful than documentation that says “one row per accepted order, excluding test mode.”
Separate raw, cleaned, and semantic layers#
A maintainable warehouse usually has at least three conceptual layers:
- Raw: a faithful, append-oriented copy of source data with ingestion metadata.
- Cleaned: typed, deduplicated, normalized records with source quirks handled once.
- Semantic: business entities and metrics with names the rest of the company can use.
Raw data gives you a recovery point when a transformation is wrong. Cleaned data prevents every analyst from parsing the same timestamp or status in a different way. The semantic layer stops “active customer” from acquiring five definitions.
Do not make raw tables a permanent public interface. Their schema belongs to the source system and can change without regard for your dashboards.
Make every load idempotent#
If running a job twice changes the result, an ordinary retry can corrupt the warehouse.
Common approaches include:
- stable source ids with merge or upsert logic
- partition replacement for bounded time windows
- append-only facts plus deduplication on a deterministic event id
- checkpoints recorded only after a batch commits
Test the behavior explicitly: interrupt a job after the destination writes but before the orchestrator records success, then rerun it. Exactly-once delivery is often marketing language; idempotent processing is the practical engineering property you need.
Keep event time and ingestion time#
An event can occur Monday and arrive Wednesday. A backfill can arrive months later. If the warehouse stores only one timestamp, you lose either the business truth or the pipeline truth.
Use:
occurred_atfor when the source says the event happenedingested_atfor when the analytics system received itloaded_atwhen a transformation wrote the current table, if useful
Normalize to UTC and retain the original time zone when legal or business rules depend on local time. Be explicit about timestamp units at the ingestion boundary; seconds interpreted as milliseconds can move an event to 1970 without producing a type error.
Model changes deliberately#
Operational records change. A customer moves plans, an order is refunded, and a sales territory is reassigned.
Decide whether the warehouse needs:
- current state, where the latest value replaces the old one
- event history, where every change is an immutable fact
- slowly changing dimensions, where attribute validity has start and end times
The choice depends on the question. “What plan is this customer on?” needs current state. “What plan were they on when this invoice was issued?” needs history.
Avoid silently overwriting a field and discovering later that a historical report now reflects today’s value.
Put data quality beside the pipeline#
Row counts are necessary and insufficient. A pipeline can deliver the expected number of rows with every amount set to zero.
Add tests for:
- uniqueness of primary business keys
- required fields and accepted enumerations
- relationships between facts and dimensions
- freshness of important tables
- volume changes outside a reasonable range
- impossible values, such as negative durations
- reconciliation against source totals for revenue and billing
Every critical dashboard should expose its data freshness. “Updated 18 minutes ago” turns a hidden pipeline failure into an understandable product state.
Treat metric definitions as code#
A metric needs more than a name. Define:
- the grain and eligible population
- inclusion and exclusion rules
- the event or source field
- the time zone and attribution window
- how late data and corrections behave
- the owner responsible for interpretation
Store definitions with version-controlled transformations when possible. A dashboard should consume the shared definition, not reimplement it with a slightly different filter.
Design for deletion and retention#
Privacy and cost both improve when data has a lifecycle.
Classify fields before ingestion. Avoid credentials and sensitive personal data entirely. Keep a path to delete data associated with an account or user where required. Use short retention for high-volume operational logs and longer retention for facts with durable analytical value.
“Keep everything forever” postpones a decision while making the eventual cleanup harder.
Control cost at query and storage time#
Warehouse cost comes from stored bytes, scanned bytes, compute concurrency, transformation frequency, and the human time required to operate it.
Practical controls include:
- partition pruning through mandatory time filters
- columnar formats and selecting only needed columns
- incremental transformations rather than full rebuilds
- pre-aggregations for a small set of repeated expensive queries
- workload limits for exploratory queries
- retention policies on raw and intermediate tables
- cost attribution by team or workload
Do not optimize solely for the smallest bill. A cheap warehouse nobody trusts is expensive in a different way.
Assign ownership#
Every important model and dashboard needs an owner who can answer:
- What does this number mean?
- Which source produced it?
- When was it last updated?
- What breaks if the source schema changes?
- Who decides whether a definition changes?
Ownership can be lightweight on a small team. It cannot be absent.
Know when you do not need a warehouse yet#
A warehouse is valuable when many sources, durable transformation logic, governance, and cross-functional reporting justify the system. It is overkill when one application needs to answer questions about its own event stream.
GraphJSON covers that earlier stage: send JSON events, explore fields immediately, use ClickHouse SQL when needed, and turn answers into dashboards or alerts. You can export the data later if a larger warehouse becomes necessary.
Start with the event schema guide and a deliberate collection retention policy. Good warehouse habits begin before there is a warehouse.

Written by JR
Founder and builder of GraphJSON.