Data Warehouse Architecture
Quote
“Dimension tables are the soul of the data warehouse.”
— Ralph Kimball, The Data Warehouse Toolkit (2013)
Summary
This note defines the data warehouse as the analytical side of the platform, covering the OLTP versus OLAP split, dimensional modeling, enterprise integration alternatives, and the performance and cost patterns that determine when a warehouse architecture actually fits the workload.
Warehouse foundations
- Explains why warehouses exist by contrasting OLAP workloads with OLTP systems, then anchors the architecture around Kimball-style dimensional modeling and explicit grain declaration.
- Treats grain, fact tables, and dimension tables as the primary modeling contract for analytical correctness and performance.
History and modeling patterns
- Covers fact table types, dimension patterns, and slowly changing dimensions so historical behavior, descriptive context, and analytical shape are handled deliberately instead of ad hoc.
- Shows how star and snowflake choices affect join cost, human readability, and warehouse query behavior.
Enterprise alternatives and data flow
- Compares Kimball with Inmon-style enterprise warehouses and Data Vault 2.0, then positions ELT versus ETL inside the warehouse architecture decision.
- Keeps integration strategy, history preservation, and warehouse loading model tied to the same design discussion.
Operations and safety
- Covers materialized views, rollups, sizing, and cost patterns, then uses grain and model-shape warnings to show where analytical correctness is easy to break.
- Warnings: grain violations, over-normalization for analytics, and mismatched fact or dimension design cause both incorrect results and expensive queries.
Glossary
Data warehouse
A subject-oriented analytical store designed for large-scale historical querying, aggregation, and business reporting.
It matters here because the whole note treats warehouse architecture as a deliberate response to analytical workloads rather than as a generic database choice.
Built for analysis
A warehouse is optimized for reading and summarizing history, not for serving as the row-by-row transactional system of record.
OLTP
Online Transaction Processing workloads centered on small row-level writes, updates, and point lookups.
It matters here because the note contrasts OLTP design goals with the analytical behavior a warehouse must optimize for instead.
Wrong optimization target
Designing an analytical system like an OLTP database usually preserves transactional purity at the cost of slow and expensive reporting queries.
OLAP
Online Analytical Processing workloads centered on scanning, joining, aggregating, and slicing large historical datasets.
It matters here because warehouse architecture exists to serve this access pattern efficiently.
Read-heavy workload
OLAP is less about one query and more about repeated analytical questions over large time horizons and many dimensions.
Grain
The exact level of detail represented by one row in a fact table.
It matters here because every later fact and dimension decision must stay consistent with the chosen grain.
Grain is binding
If rows of different detail levels mix in one fact table, aggregates become misleading even when the SQL itself looks correct.
Fact table
The warehouse table that stores measurable events or states at a declared grain.
It matters here because facts carry the numeric values analysts aggregate, compare, and trend over time.
Measures live here
If a number answers how much, how many, or how often, it probably belongs in a fact table rather than a dimension.
Dimension table
The descriptive table that gives business context to facts such as who, what, when, where, or how.
It matters here because human-readable analytics depend on joining measures to stable descriptive context.
Meaning around measures
Dimensions are what make warehouse queries intelligible to analysts rather than only to engineers.
Slowly Changing Dimension
A pattern for handling changing descriptive attributes over time without losing required history.
It matters here because warehouses often need both current values and historically accurate attributes for past events.
History policy choice
Choosing the wrong SCD strategy either destroys history or creates needless complexity, so it should be a deliberate business decision.
Conformed dimension
A shared dimension reused across multiple fact tables or business processes with consistent meaning and keys.
It matters here because cross-process reporting depends on dimensions that mean the same thing everywhere they appear.
Integration through shared context
Conformed dimensions are one of the main reasons a warehouse can support enterprise-wide analysis instead of isolated marts.
Enterprise Data Warehouse
A centralized integrated warehouse architecture often associated with Inmon-style normalized enterprise storage.
It matters here because the note compares it with Kimball marts and Data Vault as alternative integration strategies.
Integration-first alternative
An EDW emphasizes central consistency and enterprise integration, sometimes before user-facing dimensional marts are built.
Data Vault 2.0
A warehouse modeling approach that separates business keys, relationships, and descriptive history into hubs, links, and satellites.
It matters here because it provides a different trade-off between auditability, parallel loading, and query simplicity.
History-friendly integration layer
Data Vault often works best as an integration foundation that feeds easier-to-query downstream marts rather than as the direct analytics surface.
OLTP vs OLAP: The Fundamental Distinction
Understanding why a data warehouse exists requires understanding what it is not.
| Dimension | OLTP (Online Transaction Processing) | OLAP (Online Analytical Processing) |
|---|---|---|
| Primary workload | INSERT / UPDATE / DELETE of individual rows | SELECT with aggregations over millions of rows |
| Query pattern | Simple, indexed lookups by primary key | Complex multi-table joins, GROUP BY, window functions |
| Data model | Third Normal Form (3NF) — minimizes redundancy | Denormalized (star/snowflake) — minimizes joins |
| Optimization target | Write throughput, row-level locks | Read throughput, full scans, columnar compression |
| Row count | Thousands to millions of live records | Billions to trillions of historical records |
| Concurrency | Hundreds of concurrent writers | Dozens of concurrent analysts |
| Examples | SQL Server OLTP, PostgreSQL, MySQL | BigQuery, Snowflake, Redshift, Azure Synapse |
| Freshness | Real-time / near-real-time | Batch (hourly, daily) or near-real-time |
SQL Server Can Do Both
SQL Server is primarily an OLTP system but supports OLAP workloads through columnstore indexes, read replicas (Always On Availability Groups readable secondaries), and In-Memory OLTP. See always-on-availability-groups and index-types-and-strategy for the mechanics. BigQuery and Snowflake are purpose-built OLAP engines — they do not support row-level transactions or real-time writes at OLTP scale.
The core architectural implication: OLTP → normalize to reduce write amplification. OLAP → denormalize to reduce join overhead at query time.
The Kimball Methodology: Dimensional Modeling
Ralph Kimball’s The Data Warehouse Toolkit (first published 1996, now in its 3rd edition) defined the dimensional modeling approach that remains the dominant paradigm for analytical data warehouses. The Kimball methodology is bottom-up: build data marts first, integrated through shared conformed dimensions.
The Grain Declaration
Before designing any fact table, you must declare the grain — the lowest level of detail that a single row represents. This is not optional and not adjustable later without a rebuild.
Examples of grain declarations:
- “One row per sales order line item”
- “One row per financial instrument per trading day”
- “One row per patient admission”
- “One row per page view”
The grain determines what goes in the fact table (the numeric measures at that grain) and what goes in the dimensions (the descriptive attributes of that grain).
Grain Violation Destroys Accuracy
Mixing rows of different grains in a single fact table is one of the most destructive modeling errors. If your grain is “one row per order line” but you add a row representing the order header total, any SUM of amounts double-counts. Always state the grain in the table description comment and enforce it at load time.
Safe Pattern: Enforce a Single Grain
Document the grain in the table’s description or a comment (e.g.,
-- grain: one row per order line). At load time, assert that no row violates the grain using a uniqueness check on the natural key(s) that define it. If header-level totals are needed, store them in a separate summary fact table or an aggregation table — never mix them into the line-level fact.
Star Schema
The canonical Kimball structure: one central fact table surrounded by dimension tables joined via surrogate keys. It looks like a star when drawn. See dimensional-modeling for the full Kimball four-step design process with complete DDL examples.
flowchart LR date["dim_date"] inst["dim_inst"] exch["dim_exch"] curr["dim_curr"] fact["fact_prices"] date --- fact inst --- fact exch --- fact curr --- fact style fact fill:#1a1a2e,stroke:#22d3ee,color:#fff style date fill:#1a1a2e,stroke:#bb9af7,color:#fff style inst fill:#1a1a2e,stroke:#bb9af7,color:#fff style exch fill:#1a1a2e,stroke:#bb9af7,color:#fff style curr fill:#1a1a2e,stroke:#bb9af7,color:#fff
Advantages of star schema
- Queries need only one join level (fact → dim) — no intermediate joins
- Optimizers handle star joins efficiently; BigQuery and Snowflake both recognize star patterns
- Analysts understand the pattern immediately — fact table contains measures, dims contain descriptions
- Conformed dimensions enable cross-process analysis
Snowflake Schema
A normalized variant where dimension tables themselves have parent dimension tables, creating a multi-level hierarchy:
dim_product → dim_subcategory → dim_category
When to use snowflake schema
- Dimensions have very high cardinality attributes that would dominate table size
- Storage is severely constrained (less relevant in cloud)
- Strict normalization requirements from governance
Prefer Star Over Snowflake in Cloud Warehouses
BigQuery and Snowflake store data in columnar compressed format. The storage penalty of denormalized star schemas is minimal compared to the query performance benefit of avoiding extra joins. In cloud DWH environments, choose star schema unless there is a specific, justified reason to normalize a dimension.
Fact Table Types
Kimball identifies three fundamental fact table types based on the business process being modeled.
Transactional Fact Table
Grain: One row per discrete business event.
This is the most common type. Each row records something that happened at a specific point in time. Measures are additive across all dimensions (you can SUM revenue across any combination of time, product, geography).
-- Transactional fact: one row per financial trade
CREATE TABLE fact_trades (
trade_sk BIGINT PRIMARY KEY, -- surrogate key
trade_id VARCHAR(50), -- degenerate dimension (source system key)
date_sk INT REFERENCES dim_date,
instrument_sk INT REFERENCES dim_instrument,
counterparty_sk INT REFERENCES dim_counterparty,
venue_sk INT REFERENCES dim_venue,
-- measures (additive):
quantity DECIMAL(18,6),
price DECIMAL(18,6),
notional_usd DECIMAL(18,2),
commission_usd DECIMAL(18,2),
-- metadata:
load_timestamp TIMESTAMP
);Additive, semi-additive, and non-additive measures
| Measure type | SUM across time? | SUM across other dims? | Example |
|---|---|---|---|
| Additive | Yes | Yes | Revenue, quantity sold |
| Semi-additive | No | Yes | Account balance (summing balance across days double-counts) |
| Non-additive | No | No | Ratios, percentages, averages |
Semi-Additive Measure Trap
Never SUM a balance or inventory count across time periods — you get the sum of every snapshot, not the current total. Use LAST_VALUE or MAX with appropriate window framing instead. See gold-transforms for practical patterns.
Safe Pattern: Window Function for Period-End Balance
Use
LAST_VALUE(closing_balance) OVER (PARTITION BY account_sk ORDER BY snapshot_date_sk ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)to retrieve the end-of-period balance, orMAX(closing_balance)when a single-period snapshot is required. Document each semi-additive measure with an explicit note on valid aggregation axes.
Periodic Snapshot Fact Table
Grain: One row per entity per standard time period (day, week, month).
Takes a snapshot of a measured condition at regular intervals regardless of whether anything changed. Essential for trend analysis and period-over-period comparisons.
-- Periodic snapshot: daily account balance
CREATE TABLE fact_account_daily (
snapshot_date_sk INT REFERENCES dim_date,
account_sk INT REFERENCES dim_account,
-- measures (semi-additive — can sum across accounts, NOT across dates):
closing_balance DECIMAL(18,2),
open_positions INT,
margin_utilization DECIMAL(5,4),
-- metadata:
load_timestamp TIMESTAMP,
PRIMARY KEY (snapshot_date_sk, account_sk)
);Periodic snapshot characteristics
- Rows are populated even when nothing changes (fill-forward logic required for missing periods)
- All rows for the same snapshot date are loaded in a single batch
- Enables easy period-over-period queries: join to itself on
date_sk - 1 - See silver-transforms for fill-forward implementation patterns
Accumulating Snapshot Fact Table
Grain: One row per workflow instance (updated as the workflow progresses through milestones).
Models a business process with a defined lifecycle: order → pick → pack → ship → deliver. A single row is updated in place as milestones are reached. This is the only fact table type where rows are updated after initial insert.
-- Accumulating snapshot: trade settlement lifecycle
CREATE TABLE fact_settlement_lifecycle (
trade_sk BIGINT PRIMARY KEY,
-- milestone date FKs (NULL until milestone is reached):
trade_date_sk INT REFERENCES dim_date,
confirmation_date_sk INT REFERENCES dim_date,
clearing_date_sk INT REFERENCES dim_date,
settlement_date_sk INT REFERENCES dim_date,
-- lag measures (days between milestones):
days_to_confirm INT,
days_to_clear INT,
days_to_settle INT,
-- current status:
lifecycle_status VARCHAR(20) -- TRADED | CONFIRMED | CLEARED | SETTLED | FAILED
);Dimension Table Patterns
Conformed Dimensions
A dimension shared across multiple fact tables or data marts. The classic example: dim_date used by both fact_sales and fact_inventory with identical keys and attributes. Conformed dimensions are what make cross-process analysis possible — if both facts share the same date dimension, you can compare sales vs inventory by day in a single query.
Build dim_date Once, Use Everywhere
Generate a complete date dimension covering 20+ years, populated once. It should include day of week, week number, fiscal calendar, holidays, trading day flags, and any domain-specific date attributes your business needs. Never compute these at query time — they belong in the dimension.
Standard dim_date columns for a financial data warehouse
CREATE TABLE dim_date (
date_sk INT PRIMARY KEY, -- YYYYMMDD integer for fast joins
full_date DATE,
year SMALLINT,
quarter TINYINT,
month TINYINT,
month_name VARCHAR(9),
week_of_year TINYINT,
day_of_week TINYINT, -- 1=Monday ISO
day_name VARCHAR(9),
is_weekend BIT,
is_trading_day BIT, -- exchange-specific
is_us_holiday BIT,
is_uk_holiday BIT,
fiscal_year SMALLINT, -- fiscal calendar (may differ from calendar year)
fiscal_quarter TINYINT,
fiscal_month TINYINT
);Degenerate Dimensions
A dimension attribute that has no corresponding dimension table — it lives directly in the fact table as a key. Most commonly a transaction ID or order number from the source system that has no descriptive attributes worth modeling as a full dimension.
-- order_number is a degenerate dimension — it's a source system key
-- but "order" has no attributes of its own beyond what's already in the fact
fact_order_lines.order_number VARCHAR(20) -- degenerate dimensionJunk Dimensions
A collection of low-cardinality flags and indicators that don’t belong to any natural dimension. Rather than adding a dozen boolean columns to the fact table, group them into a single dimension.
-- Without junk dimension: 12 columns on fact table
fact_trade.is_short_sale BIT
fact_trade.is_algorithmic BIT
fact_trade.is_cross_listed BIT
...
-- With junk dimension: one FK
fact_trade.trade_flag_sk INT REFERENCES dim_trade_flags
-- dim_trade_flags has all combinations:
-- (is_short_sale, is_algorithmic, is_cross_listed) → small table (~8 rows for 3 booleans)Slowly Changing Dimensions (SCD)
Slowly Changing Dimensions (SCD), also called historical dimension tracking, handle the problem of dimension attributes that change over time. A customer moves city, a product changes category, an analyst changes desk. How you preserve (or discard) that history depends on the SCD type.
SCD in dbt
dbt’s
snapshotfeature implements SCD Type 2 natively using a check strategy or a timestamp strategy. See dbt-transformation-layer for implementation details and merge-and-upsert for the underlying MERGE statement mechanics.
SCD Type 1 — Overwrite (No History)
Overwrite the old value with the new value. No history is kept. Use when the old value is simply wrong (data correction) or when historical values are genuinely irrelevant to analysis.
-- Type 1: update in place
UPDATE dim_customer
SET city = 'London',
region = 'EMEA'
WHERE customer_sk = 12345;Use when: Correcting data quality errors. Customer’s birth date was wrong and is now fixed — you never want to report on the wrong date.
Avoid when: The change represents a real business event with analytical meaning (then use Type 2).
SCD Type 2 — Add Row (Full History)
Insert a new row for each change, marking the old row as expired. Every version of the record is permanently preserved with its effective date range. This is the most commonly used SCD type in practice.
-- Type 2: expire old row, insert new row
UPDATE dim_analyst
SET effective_end_date = CURRENT_DATE - INTERVAL '1 day',
is_current = FALSE
WHERE analyst_id = 'A001'
AND is_current = TRUE;
INSERT INTO dim_analyst (
analyst_sk, analyst_id, name, desk, region,
effective_start_date, effective_end_date, is_current
)
VALUES (
nextval('analyst_sk_seq'), 'A001', 'Jane Smith', 'Equity Research', 'APAC',
CURRENT_DATE, '9999-12-31', TRUE
);Resulting table
| analyst_sk | analyst_id | desk | region | effective_start | effective_end | is_current |
|---|---|---|---|---|---|---|
| 101 | A001 | Fixed Income | EMEA | 2022-01-01 | 2024-06-30 | FALSE |
| 208 | A001 | Equity Research | APAC | 2024-07-01 | 9999-12-31 | TRUE |
Joining to fact tables: Always join on both the business key AND the effective date range to get the dimension version that was valid at the time of the transaction:
SELECT f.trade_date, a.desk, SUM(f.notional_usd)
FROM fact_trades f
JOIN dim_analyst a
ON f.analyst_id = a.analyst_id
AND f.trade_date BETWEEN a.effective_start_date AND a.effective_end_date
GROUP BY f.trade_date, a.desk;Use when: You need to report “what was the analyst’s desk at the time of the trade?” — the most common business requirement in financial data warehouses.
SCD Type 3 — Previous Value Column
Add a column to store the previous value alongside the current value. Only one level of history is kept. Trades recency for simplicity.
ALTER TABLE dim_customer ADD COLUMN previous_city VARCHAR(100);
UPDATE dim_customer
SET previous_city = city,
city = 'Manchester'
WHERE customer_sk = 12345;Use when: Business users only ever compare current vs previous (e.g., “what changed last quarter?”). Rarely appropriate for analytical systems with genuine historical depth requirements.
Limitation: Only tracks one prior state. A third change overwrites the previous-value column, losing the intermediate history.
SCD Type 4 — Mini-Dimension (Rapidly Changing Attributes)
Extract frequently changing attributes into a separate “mini-dimension” table, leaving the slowly changing attributes in the main dimension. The fact table carries FKs to both.
-- Main dimension: slowly changing
dim_customer(customer_sk, customer_id, name, date_of_birth, ...)
-- Mini-dimension: rapidly changing
dim_customer_profile(profile_sk, credit_score_band, income_band, risk_tier, eff_date)
-- Fact table carries both FKs:
fact_transactions(customer_sk, profile_sk, ...)Use when: Some attributes change far more frequently than others, and tracking each change as a Type 2 row would explode the main dimension table. Common for behavioral scores, credit ratings, and risk tiers in financial systems.
SCD Type 6 — Hybrid (1 + 2 + 3)
Combines Type 1, 2, and 3. Maintains full history (Type 2 rows) while also carrying the current value on all historical rows (Type 1 overwrite of a current_* column) and the previous value (Type 3). Maximally flexible but complex to implement and maintain.
-- Type 6 dimension: full history + current value on all rows
dim_customer (
customer_sk INT,
customer_id VARCHAR, -- business key
-- current value on ALL rows (Type 1 — keeps in sync across history):
current_region VARCHAR(50),
-- historical value for THIS row's effective period (Type 2):
region VARCHAR(50),
-- previous value (Type 3):
previous_region VARCHAR(50),
-- Type 2 tracking:
effective_start_date DATE,
effective_end_date DATE,
is_current BIT
)Use when: You need both: “what was the region at time of transaction?” (join on date range, use region) AND “what is this customer’s current region?” (filter is_current = 1, use current_region). Avoids needing two separate queries.
The Inmon Methodology: Enterprise Data Warehouse
Bill Inmon’s approach is top-down: build an integrated, normalized Enterprise Data Warehouse (EDW) first in Third Normal Form (3NF), then derive department-specific data marts from it.
Inmon’s 4 characteristics of a data warehouse
- Subject-oriented — organized around subjects (Customer, Instrument, Position), not business processes
- Integrated — single, consistent representation across all source systems
- Non-volatile — data is never updated or deleted; only loaded
- Time-variant — every record has a timestamp; history is preserved by design
The Inmon flow
flowchart TD oltp["Source Systems (OLTP)"] ods["ODS<br/>Operational Data Store<br/>(optional, near-real-time staging)"] edw["EDW<br/>3NF, integrated, atomic"] martA["Data Mart A<br/>Finance — star schema"] martB["Data Mart B<br/>Risk — star schema"] martC["Data Mart C<br/>Compliance — star schema"] oltp --> ods --> edw edw --> martA edw --> martB edw --> martC style oltp fill:#1a1a2e,stroke:#7aa2f7,color:#fff style ods fill:#1a1a2e,stroke:#e0af68,color:#fff style edw fill:#1a1a2e,stroke:#bb9af7,color:#fff style martA fill:#1a1a2e,stroke:#9ece6a,color:#fff style martB fill:#1a1a2e,stroke:#9ece6a,color:#fff style martC fill:#1a1a2e,stroke:#9ece6a,color:#fff
The EDW itself is not queried by business users. It is a normalized integration layer. Business users query dimensional data marts derived from it.
Kimball vs Inmon: Comparison
| Dimension | Kimball (Bottom-Up) | Inmon (Top-Down) |
|---|---|---|
| Starting point | Data marts first, EDW emerges | EDW first, data marts derived |
| Data model | Dimensional (star/snowflake) | 3NF normalized |
| Time to first value | Weeks (one mart at a time) | Months (EDW must exist first) |
| Integration mechanism | Conformed dimensions | Common EDW data model |
| Query layer | Directly on star schema | Data marts layered over EDW |
| Flexibility | High (add marts independently) | Lower (changes to EDW propagate) |
| Consistency | Good (via conformed dimensions) | Excellent (single source) |
| Storage | Higher (denormalized) | Lower (normalized) |
| Organizational fit | Agile, department-driven | Enterprise-wide governance |
| Common in | Mid-sized companies, cloud-native | Large enterprises, financial services |
In Practice: Hybrid Wins
Most real-world warehouses are hybrid. A normalized staging/integration layer (Inmon-style) feeds dimensional data marts (Kimball-style). This is exactly what the medallion-architecture implements: normalized Silver → denormalized Gold. The dbt-transformation-layer typically handles the Gold layer modeling.
Data Vault 2.0
Data Vault 2.0 (DV2) is a third modeling methodology designed specifically for large enterprise warehouses that must integrate many source systems with evolving schemas and auditability requirements. It was designed by Dan Linstedt.
Core Components
Hubs — The list of unique business keys for an entity. One row per business key, forever. No updates, ever.
CREATE TABLE hub_instrument (
instrument_hk BINARY(16) PRIMARY KEY, -- hash of business key
load_date TIMESTAMP NOT NULL,
record_source VARCHAR(100) NOT NULL, -- which system loaded this
instrument_id VARCHAR(50) NOT NULL -- business key from source
);Links — Relationships between hubs. One row per unique combination of related business keys.
CREATE TABLE link_trade_instrument (
link_hk BINARY(16) PRIMARY KEY,
load_date TIMESTAMP NOT NULL,
record_source VARCHAR(100) NOT NULL,
-- FKs to hubs:
trade_hk BINARY(16) REFERENCES hub_trade,
instrument_hk BINARY(16) REFERENCES hub_instrument,
counterparty_hk BINARY(16) REFERENCES hub_counterparty
);Satellites — Descriptive attributes of hubs and links. Support full history through effective date ranges. Each satellite tracks one “rate of change” grouping of attributes.
CREATE TABLE sat_instrument_market_data (
instrument_hk BINARY(16) REFERENCES hub_instrument,
load_date TIMESTAMP NOT NULL, -- effective start (no end date — use next load_date)
record_source VARCHAR(100) NOT NULL,
hash_diff BINARY(16), -- hash of all attributes for fast change detection
-- attributes:
last_price DECIMAL(18,6),
bid DECIMAL(18,6),
ask DECIMAL(18,6),
volume BIGINT,
PRIMARY KEY (instrument_hk, load_date)
);When to Use Data Vault
| Situation | Use Data Vault? |
|---|---|
| 5+ heterogeneous source systems that evolve independently | Yes |
| Strict auditability: every row must be traceable to source and load time | Yes |
| Schema changes are frequent and breaking | Yes |
| Single source of truth, well-understood schema | No — use Kimball |
| Small-to-medium team with tight delivery deadlines | No — Kimball faster |
| Real-time streaming loads at high velocity | Yes (DV2 is insert-only, parallel-friendly) |
| BI users query the warehouse directly | No — expose Kimball marts on top of DV2 |
Data Vault + Kimball Together
Most production DV2 implementations expose Information Marts (Kimball-style star schemas) on top of the DV2 raw vault for BI tools and analysts. The DV2 raw vault is the system of record; the information marts are the reporting layer. This maps directly to medallion-architecture where Silver = DV2 raw vault, Gold = information marts.
Cloud Data Warehouse Comparison
Modern cloud data warehouses have largely converged on columnar storage, MPP (Massively Parallel Processing) execution, and separation of compute from storage. The differences lie in pricing model, SQL dialect, ecosystem integrations, and operational characteristics.
| Dimension | BigQuery | Snowflake | Redshift | Azure Synapse |
|---|---|---|---|---|
| Vendor | Google Cloud | Independent (on AWS/Azure/GCP) | AWS | Microsoft |
| Compute model | Serverless (slots) or reservations | Virtual warehouses (auto-suspend) | Node-based clusters | DWU-based pools |
| Pricing model | Per-TB scanned (on-demand) or flat reservation | Per-second of active compute | Per-hour per node | Per-DWU-hour |
| Storage | Colossus (GCS-backed), auto-replicated | S3/ADLS/GCS, customer-managed | S3, customer-managed | ADLS Gen2 |
| SQL dialect | GoogleSQL (ANSI + extensions) | Snowflake SQL (ANSI + extensions) | PostgreSQL-based | T-SQL (SQL Server) |
| Strengths | Ad hoc queries, zero ops, GCP integration | Multi-cloud, data sharing, concurrency | AWS ecosystem, Redshift Spectrum | SQL Server continuity, Azure integration |
| Weaknesses | Cost unpredictability (on-demand), limited DML | Cost at scale, query contention | Operational overhead, no true serverless | Complex pricing, slower innovation |
| External tables | Yes (BigLake, GCS, Drive) | Yes (S3, Azure, GCS) | Yes (Redshift Spectrum) | Yes (ADLS, Blob) |
| Time travel | 7 days (INFORMATION_SCHEMA.TABLE_SNAPSHOTS) | 0–90 days (configurable) | No native | No native |
| Best for | GCP-native shops, ad hoc analytics | Multi-cloud, data sharing at scale | AWS shops, existing Redshift investment | Microsoft/Azure shops |
BigQuery Cost Control
BigQuery on-demand pricing charges per byte scanned. The three most impactful cost controls: (1) partition tables on date columns — queries that filter on the partition key scan only matching partitions, (2) cluster tables by frequently-filtered columns, (3) never
SELECT *. See querying-and-cost-optimization for dry run commands and detailed optimization practices.
ELT vs ETL in Warehouse Context
Modern cloud warehouses favor ELT (Extract → Load → Transform) over traditional ETL (Extract → Transform → Load). The distinction matters because it determines where transformation compute runs and who pays for it.
ETL paradigm (legacy)
- Extract from source
- Transform in a middleware engine (Informatica, SSIS, Spark, Python)
- Load clean data into warehouse
ELT paradigm (modern cloud)
- Extract from source
- Load raw data into warehouse (cheap columnar storage)
- Transform inside the warehouse using SQL (leverages the warehouse’s MPP engine)
Why ELT Won
Cloud warehouses have essentially unlimited compute at linear per-query cost. It is cheaper and simpler to run SQL transforms inside BigQuery than to spin up and maintain a separate Spark cluster. ELT also preserves the raw data (enabling re-derivation when business rules change) and leverages the warehouse’s optimizer rather than fighting it.
See dbt-transformation-layer for the standard ELT implementation tool.
Materialized Views, Aggregation Tables, and Pre-Computed Rollups
When analytical queries are expensive but predictable, pre-computing results reduces both latency and cost.
Materialized Views
A materialized view persists the result of a query as physical storage, refreshed on a schedule or incrementally.
BigQuery materialized views
-- BigQuery: materialized view with incremental refresh
CREATE MATERIALIZED VIEW `project.dataset.daily_volume_mv`
OPTIONS (enable_refresh = true, refresh_interval_minutes = 60)
AS
SELECT
DATE(trade_timestamp) AS trade_date,
instrument_id,
SUM(quantity) AS total_volume,
SUM(notional_usd) AS total_notional
FROM `project.dataset.fact_trades`
GROUP BY 1, 2;SQL Server materialized views (indexed views)
-- SQL Server: indexed view (must use SCHEMABINDING, WITH NOEXPAND hint)
CREATE VIEW dbo.vw_daily_volume
WITH SCHEMABINDING
AS
SELECT
CAST(trade_timestamp AS DATE) AS trade_date,
instrument_id,
SUM(quantity) AS total_volume,
COUNT_BIG(*) AS row_count -- required by SQL Server
FROM dbo.fact_trades
GROUP BY CAST(trade_timestamp AS DATE), instrument_id;
CREATE UNIQUE CLUSTERED INDEX IX_vw_daily_volume
ON dbo.vw_daily_volume (trade_date, instrument_id);Aggregation Tables
Explicit pre-aggregated tables maintained by the ETL/ELT pipeline, not a database engine feature. More portable and controllable than materialized views.
-- Gold-layer aggregation table: monthly P&L summary
CREATE TABLE gold.monthly_pnl_summary (
year_month CHAR(7), -- 'YYYY-MM'
desk VARCHAR(50),
region VARCHAR(50),
total_trades INT,
total_notional DECIMAL(18,2),
realized_pnl DECIMAL(18,2),
unrealized_pnl DECIMAL(18,2),
net_pnl DECIMAL(18,2),
refreshed_at TIMESTAMP
);Load this table as part of the gold-transforms pipeline step on a daily cadence.
Pre-Computed Rollups Pattern
For dashboards with fixed dimensions and known query patterns, pre-compute every combination at load time using a CUBE or ROLLUP query:
-- Pre-compute all rollup levels (SQL Server / BigQuery compatible)
INSERT INTO gold.pnl_rollup (trade_date, desk, region, currency, total_notional)
SELECT
trade_date,
GROUPING_ID(desk, region, currency) AS rollup_level,
desk,
region,
currency,
SUM(notional_usd) AS total_notional
FROM silver.trades
GROUP BY ROLLUP(trade_date, desk, region, currency);Warehouse Sizing and Cost Patterns
Cloud warehouses do not require traditional capacity planning, but understanding cost drivers prevents bill shock.
BigQuery Cost Model
| Cost component | Driver | Optimization |
|---|---|---|
| Query (on-demand) | Bytes scanned per query | Partitioning, clustering, column selection |
| Storage (active) | Bytes in tables | Lifecycle policies, partition expiration |
| Storage (long-term) | Tables unmodified 90+ days | Automatically discounted 50% |
| Streaming inserts | Rows inserted via streaming API | Use batch loads where latency allows |
| Slot reservations | Fixed monthly commitment | Use when predictable high volume |
Rule of thumb: Tables over 1 TB should be partitioned. Tables over 10 TB should be both partitioned and clustered. See querying-and-cost-optimization for mechanics. For SQL Server warehouse tables, partitioning-strategies covers partition functions, schemes, and sliding window maintenance.
Snowflake Cost Model
| Cost component | Driver | Optimization |
|---|---|---|
| Compute credits | Warehouse size × active time | Auto-suspend (default 10 min), right-size XS→XL |
| Storage | Data + Time Travel + Fail-safe | Reduce Time Travel window for transient tables |
| Cloud services | Metadata, compilation | < 10% of compute = free; > 10% = extra charge |
| Data transfer | Cross-cloud egress | Avoid cross-cloud queries |
For guidance on building the metadata and context layers that make warehouse data self-describing and auditable, see context-and-metadata-architecture.
Warehouse Architecture Checklist
Before declaring a warehouse schema production-ready, verify:
- Every fact table has an explicit grain declaration in its description
- All foreign keys join to the same grain or higher — no grain mixing
- Date dimension (
dim_date) covers the full historical range plus 5 future years - Surrogate keys (integers or hashes) are used for all joins — never business keys as FKs
- SCD Type 2 tables have
effective_start_date,effective_end_date, andis_currentcolumns - Conformed dimensions are defined in a shared schema layer accessible to all marts
- All fact table measures are classified as additive, semi-additive, or non-additive in docs
- Materialized views or aggregation tables exist for the 5 most expensive recurring queries
- Partition pruning is verified for the primary query patterns (run EXPLAIN / dry run)
- Data quality checks run at load time (null rate, row count, min/max date assertions)
Related Notes
- data-lake-architecture — The complementary storage architecture; data lakes feed data warehouses
- medallion-architecture — Practical Bronze/Silver/Gold implementation pattern
- open-table-formats — Apache Iceberg, Delta Lake, and the lakehouse convergence of lake + warehouse
- dbt-transformation-layer — The standard tool for implementing ELT transforms in a warehouse
- idempotent-pipeline-design — How to safely load and reload warehouse data
- querying-and-cost-optimization — BigQuery-specific cost optimization mechanics
- merge-and-upsert — MERGE statement for SCD Type 2 implementation in SQL Server
- partitioning-strategies — SQL Server partitioning (compare to BigQuery partition pruning)
- serialization-formats — Parquet and columnar storage formats underpinning cloud DWH storage
- silver-transforms — Silver-layer cleaning patterns that feed warehouse staging
- gold-transforms — Gold-layer aggregation patterns for analytical consumption
- five-pillars-of-data-engineering — Architectural principles every DWH design should satisfy