Cloud Data Warehouse Modeling: Snowflake, BigQuery, and Redshift Patterns That Scale
A generation of dimensional-modeling wisdom was written for machines that no longer exist โ warehouses where storage was precious, compute was fixed, and a bad schema meant buying hardware. Snowflake, BigQuery, and Redshift changed the physics: storage is nearly free, compute is elastic and metered by the second (or the byte scanned), and columnar engines laugh at table widths that would have crashed a 2005 Oracle box. Some classic rules survived the transition untouched; others quietly inverted.
This post sorts out which is which: the universal principles, the platform-specific levers each engine gives you, the modern layering pattern dbt standardized, and the discipline the cloud actually added โ modeling for cost.
What the Cloud Changed (and Didnโt)
Changed:
- Storage stopped being a design constraint. Keeping full history, wide denormalized tables, and multiple materializations of the same data are all now rounding errors on the bill. The old instinct to normalize-to-save-bytes is dead.
- Compute became the bill. You pay per second of warehouse time (Snowflake, Redshift RA3) or per byte scanned (BigQuery on-demand). A model that forces full scans isnโt just slow โ itโs a recurring invoice. Cost is the new performance, and modeling decisions show up on it line by line.
- Rebuilds became cheap. Elastic compute plus ELT means reshaping a model is a code change and a backfill, not a capital project. Modeling became iterative โ which is why it moved into version-controlled tools like dbt.
Didnโt change:
- Grain still rules everything (one row = exactly what?), conformed dimensions still keep โcustomerโ meaning one thing, and slowly-changing-dimension policy is still a business decision โ all of it from the analytical modeling post in this series, fully intact.
- Understandability is still the scarce resource. Analysts navigate your schema with the same human brains they had in 2005. Stars survive in the cloud less for performance than because people can reason about them.
The Star vs. Wide-Table Question, Settled Pragmatically
The cloud-era debate โ โjust build One Big Table (OBT), joins are for boomersโ vs. โKimball foreverโ โ has resolved, in most experienced teams, into a layered both: model dimensionally, serve wide. Keep facts and dimensions as the governed core (reusable, one place to fix a definition, SCD handling in one spot), then materialize denormalized wide tables from the star for specific consumption: BI tools that struggle with joins, ML feature sets, latency-critical dashboards. The star is the source of meaning; OBTs are cached query results with a schema. When a wide table and the star disagree, you know which one is wrong โ thatโs what makes the pattern safe.
Two cloud-native adjustments to classic star design worth adopting:
- Nested types are legitimate. BigQuery especially rewards embedding order line items as an
ARRAY<STRUCT>inside the order row โ the orders-to-lines join vanishes, andUNNESTrecovers line grain when needed. (Bounded arrays only โ the document-modeling rules apply.) - Snapshot dimensions beat clever SCD plumbing when history is small. With storage free, a daily snapshot of the whole customer dimension (
dim_customer_daily, partitioned by snapshot date) answers โas-ofโ questions with a date filter instead of validity-range joins. Type 2 remains right for big dimensions; snapshots are the simpler tool the old cost model forbade.
Platform Levers: The Same Ideas, Three Dialects
None of the big three use traditional indexes; all are columnar with min/max metadata pruning. Your physical modeling job is arranging data so pruning works โ one idea, three vocabularies:
Snowflake. Micro-partitions are automatic; your lever is the clustering key on large tables (CLUSTER BY (order_date, customer_id)) so range and equality filters skip micro-partitions. Costs credits to maintain โ reserve it for multi-terabyte tables with consistent filter patterns; most tables under a TB need nothing. Zero-copy clones make dev/test environments a modeling workflow feature (test a remodel against full production data for pennies).
BigQuery. The lever pair is explicit: partition (almost always by date โ also caps how much a query can scan, which under per-byte pricing is literally a spending limit) plus clustering (up to four columns, ordered most-filtered-first, handling the high-cardinality keys partitioning canโt). Require partition filters on big tables (require_partition_filter=true) and an unbounded SELECT * becomes an error instead of an invoice.
Redshift. The most hands-on of the three: DISTKEY decides which node holds each row (distribute big facts on the join key so fact-to-fact joins stay local; small dimensions DISTSTYLE ALL โ copied to every node), SORTKEY plays the clustering role for range filters. Getting distribution wrong shows up as network shuffle on every join โ the closest the cloud trio comes to old-school physical modeling consequences.
-- The same fact table, three dialects of the same thought-- SnowflakeCREATE TABLE fct_orders (...) CLUSTER BY (order_date);-- BigQueryCREATE TABLE fct_orders (...)PARTITION BY DATE(order_date) CLUSTER BY customer_id, store_id;-- RedshiftCREATE TABLE fct_orders (...) DISTKEY(customer_id) SORTKEY(order_date);The dbt Layering Pattern: How Cloud Warehouses Get Modeled in Practice
The de facto standard structure โ worth adopting even if your tool isnโt dbt โ is a pipeline of modeling intents, cousin to the lakeโs medallion layers:
The load-bearing conventions: staging models touch one source each and do no joins (so source changes have one place to land); marts carry the tests, documentation, and grain declarations (this is where governance from earlier posts attaches); big facts build incrementally โ processing only new/changed rows per run, which on metered compute is the single biggest recurring-cost saver available:
-- dbt incremental: pay to process a day, not five years, per run{{ config(materialized='incremental', unique_key='order_id') }}SELECT ... FROM {{ ref('stg_orders') }}{% if is_incremental() %} WHERE updated_at > (SELECT max(updated_at) FROM {{ this }}){% endif %}Modeling for the Bill: The Genuinely New Discipline
Habits that separate cheap warehouses from surprising invoices, all of them modeling decisions:
- Prune-friendly keys everywhere. Every large table gets a date partition/cluster; every common filter path gets a clustering column. The query you didnโt scan is the dollar you didnโt spend.
- Incremental over full-refresh for anything big; schedule full rebuilds rarely and deliberately.
- Materialize whatโs read often, compute whatโs read rarely. A dashboard hit hourly deserves a table; a quarterly analysis deserves a view. Re-evaluate as usage shifts โ usage metadata (the catalogs post) tells you which is which.
- Watch the serving layerโs blast radius.
SELECT *against a 400-column OBT scans all 400 columns on a columnar engine. Wide tables for BI tools that generate tidy column lists: fine. Wide tables for ad-hoc humans: teach the habit or trim the table.
A Migration-Ready Checklist
- Grain declared and documented per mart table; conformed dimensions shared across stars.
- Date partitioning/clustering on every table past ~100 GB; platform-appropriate keys on the top ten query paths.
- Layered project: staging 1:1 with sources โ marts tested and owned โ serving tables derived, never hand-maintained.
- Incremental materialization on large facts; snapshot or Type 2 policy chosen per dimension, in writing.
- Cost review monthly: top ten most expensive queries traced back to the modeling decision that would fix them.
The cloud warehouse didnโt retire the dimensional modeler โ it promoted them. Freed from rationing bytes, the job is now equal parts semantics (what does a row mean?), economics (what does a query cost?), and product (can an analyst find and trust this?). The platforms will keep leapfrogging each other on benchmarks; the models that survive the leapfrogging are the ones where somebody answered those three questions on purpose.