08
Advanced  ·  Module 08 of 14

Performance
Optimization

A beautifully designed Data Vault that can’t answer queries fast enough is useless in production. Learn the indexing, partitioning, PIT, and parallel load strategies that make enterprise vaults fly.

5 ConceptsCore topics
~50 minReading time
AdvancedLevel
Modules 01–07Prerequisites
Module Progress 0%

Concept 01

Indexing Strategy for Data Vault

🧠 ELI5 — Explain Like I’m Five

Data Vault tables have predictable access patterns. You always look up by hash key. You always filter by load date. You often scan for NULL load_end_dates. If you design indexes around these patterns — not randomly — your queries will be fast even on billions of rows.

Hub Table Indexes

Hubs are the simplest case. Every Hub has exactly two index needs:

  • Primary key index on HUB_{ENTITY}_HK — created automatically by the PK constraint. Supports all join operations from Links and Satellites back to the Hub.
  • Index on {ENTITY}_BK — the business key lookup. When a staging row arrives with a raw business key rather than a pre-hashed HK, this index supports the “does this BK already exist?” check without a full Hub scan.

Link Table Indexes

Links participate in multi-directional traversal queries. You need:

  • PK index on LINK_{NAME}_HK — the link’s own surrogate key, created automatically.
  • Individual index on each Hub FK (HUB_A_HK, HUB_B_HK) — when a query starts from one Hub and needs to find all linked entities, the FK index enables an efficient nested-loop join without scanning the entire Link table.
  • Composite index on (HUB_A_HK, HUB_B_HK) — for bi-directional traversal queries that filter on both keys simultaneously.

Satellite Table Indexes

Satellites carry the most data and the most query traffic. Three index types are essential:

  • Composite PK on (HUB_{ENTITY}_HK, LOAD_DATE) — the standard satellite primary key. Enables point-in-time lookups by entity and exact timestamp.
  • Filtered/partial index on (HUB_{ENTITY}_HK) WHERE LOAD_END_DATE IS NULL — the single most impactful index in any vault. Current-record queries are the dominant workload, and a filtered index covering only IS NULL rows is dramatically smaller than a full index and serves this exact pattern exclusively.
  • Index on HASH_DIFF — supports the satellite loader’s change-detection query, which compares incoming HashDiff values against the current satellite HashDiff to determine whether an insert is needed.
SQL — Index DDL for Hub, Link & Satellite
-- ── HUB INDEXES ─────────────────────────────────────────────
CREATE UNIQUE INDEX idx_hub_customer_pk ON HUB_CUSTOMER (HUB_CUSTOMER_HK);
CREATE INDEX        idx_hub_customer_bk ON HUB_CUSTOMER (CUSTOMER_BK);

-- ── LINK INDEXES ─────────────────────────────────────────────
CREATE UNIQUE INDEX idx_link_co_pk        ON LINK_CUSTOMER_ORDER (LINK_CUSTOMER_ORDER_HK);
CREATE INDEX        idx_link_co_customer   ON LINK_CUSTOMER_ORDER (HUB_CUSTOMER_HK);
CREATE INDEX        idx_link_co_order      ON LINK_CUSTOMER_ORDER (HUB_ORDER_HK);
CREATE INDEX        idx_link_co_composite  ON LINK_CUSTOMER_ORDER (HUB_CUSTOMER_HK, HUB_ORDER_HK);

-- ── SATELLITE INDEXES ────────────────────────────────────────
-- Filtered index: covers the dominant "current record" query pattern
CREATE INDEX idx_sat_cd_current ON SAT_CUSTOMER_DEMOGRAPHICS (HUB_CUSTOMER_HK)
    WHERE LOAD_END_DATE IS NULL;    -- partial index: current records only

-- HashDiff index: change detection during satellite loader
CREATE INDEX idx_sat_cd_hdiff   ON SAT_CUSTOMER_DEMOGRAPHICS (HUB_CUSTOMER_HK, HASH_DIFF);

-- ── CLOUD PLATFORMS (Snowflake / BigQuery) ───────────────────
-- Traditional B-tree indexes replaced by clustering keys
ALTER TABLE SAT_CUSTOMER_DEMOGRAPHICS
    CLUSTER BY (HUB_CUSTOMER_HK);   -- co-locate all rows for the same entity
☁

Cloud platforms (Snowflake, BigQuery, Redshift): Traditional row-level B-tree indexes don’t exist. Instead use clustering keys on Snowflake or partition & cluster columns on BigQuery. The effect is equivalent — data for the same hash key is physically co-located in the same micro-partition, enabling micro-partition pruning that cuts I/O dramatically. On Snowflake, automatic clustering maintenance runs in the background and requires no manual index rebuilds.

Concept 02

PIT Table Performance Deep Dive

🧠 ELI5 — Explain Like I’m Five

We covered what PIT tables do in Module 07. Now let’s talk about how to make them fast to build and query. The two tricks are: incremental refresh (only rebuild rows that changed) and partitioning the PIT table by snapshot date.

The Full-Rebuild Problem

A naive PIT implementation rebuilds the entire table every load cycle. If you have 10 million customers and 730 daily snapshot dates (two years of history), that’s 7.3 billion rows to recompute every single day — even though only a small fraction of customers receive new satellite data in any given load window.

The solution is incremental PIT refresh: only insert PIT rows for entities that have new satellite data since the last PIT run, and only for snapshot dates not yet covered.

Two Guards for the Incremental Pattern

  1. EXISTS guard — only process Hub entities where at least one satellite has a LOAD_DATE newer than the entity’s most recent PIT row.
  2. NOT EXISTS guard — skip insertion if a PIT row for this (HK, SNAPSHOT_DATE) pair already exists (idempotent re-run protection).
SQL — Incremental PIT Refresh (Today’s Snapshot Only)
-- Instead of rebuilding the entire PIT table daily,
-- only insert rows for dates that changed since last run
INSERT INTO PIT_CUSTOMER
SELECT
    h.HUB_CUSTOMER_HK,
    d.SNAPSHOT_DATE,
    (SELECT MAX(s.LOAD_DATE) FROM SAT_CUSTOMER_DEMOGRAPHICS s
     WHERE s.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
       AND s.LOAD_DATE <= d.SNAPSHOT_DATE
    ) AS SAT_CUSTOMER_DEMOGRAPHICS_LDT,
    (SELECT MAX(s.LOAD_DATE) FROM SAT_CUSTOMER_CONTACT s
     WHERE s.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
       AND s.LOAD_DATE <= d.SNAPSHOT_DATE
    ) AS SAT_CUSTOMER_CONTACT_LDT
    -- ... repeat correlated subquery for each satellite in PIT scope
FROM HUB_CUSTOMER h
CROSS JOIN (SELECT CURRENT_DATE() AS SNAPSHOT_DATE) d

-- Guard 1: only process entities with NEW satellite data since last PIT refresh
WHERE EXISTS (
    SELECT 1 FROM SAT_CUSTOMER_DEMOGRAPHICS s
    WHERE s.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
      AND s.LOAD_DATE >= (
          SELECT COALESCE(MAX(LOAD_DATE), '1900-01-01')
          FROM   PIT_CUSTOMER p
          WHERE  p.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
      )
)

-- Guard 2: skip if this (HK, SNAPSHOT_DATE) row already exists
AND NOT EXISTS (
    SELECT 1 FROM PIT_CUSTOMER p
    WHERE p.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
      AND  p.SNAPSHOT_DATE   = d.SNAPSHOT_DATE
);

Modern Alternative: LATERAL JOIN

Modern SQL engines (Snowflake, DuckDB, PostgreSQL 9.3+) support LATERAL joins, which act like a correlated subquery executed once per row of the left table. For PIT-style queries they eliminate the need for a pre-built PIT table entirely — computing the as-of lookup inline.

Use LATERAL joins when the snapshot date is ad-hoc or when you’re building a one-off historical slice that doesn’t justify materializing a full PIT table.

SQL — LATERAL JOIN as PIT Alternative (Snowflake / DuckDB / PostgreSQL)
-- Modern SQL: LATERAL JOIN for ad-hoc point-in-time satellite lookups
-- No pre-built PIT table required
SELECT
    h.CUSTOMER_BK,
    d_snap.FIRST_NAME,
    d_snap.LAST_NAME,
    c_snap.EMAIL
FROM HUB_CUSTOMER h
CROSS JOIN (SELECT DATE '2023-01-01' AS snap_date) p

-- LATERAL: for each Hub row, find the as-of demographics row
JOIN LATERAL (
    SELECT FIRST_NAME, LAST_NAME
    FROM  SAT_CUSTOMER_DEMOGRAPHICS s
    WHERE s.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
      AND s.LOAD_DATE <= p.snap_date
    ORDER BY s.LOAD_DATE DESC LIMIT 1
) d_snap ON TRUE

-- LATERAL: for each Hub row, find the as-of contact row
JOIN LATERAL (
    SELECT EMAIL
    FROM  SAT_CUSTOMER_CONTACT s
    WHERE s.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
      AND s.LOAD_DATE <= p.snap_date
    ORDER BY s.LOAD_DATE DESC LIMIT 1
) c_snap ON TRUE

ORDER BY h.CUSTOMER_BK;

Decision guide: If you query the same snapshot dates repeatedly across millions of entities — build a PIT table with incremental refresh. If the snapshot date is ad-hoc and you only need a slice of entities — LATERAL join is simpler and avoids PIT maintenance overhead. On Snowflake, evaluate Dynamic Tables as a managed auto-refreshing alternative to manually scheduled PIT rebuilds.

Concept 03

Partitioning Strategies

🧠 ELI5 — Explain Like I’m Five

Partitioning splits a large table into smaller physical chunks. When a query only needs data from one partition, the database skips all the others — dramatically reducing I/O. For Data Vault, the two most powerful partitioning choices are LOAD_DATE and Hash Key range.

Partitioning by LOAD_DATE

LOAD_DATE partitioning is the most universally applicable strategy across all three vault table types:

  • Best for: time-bounded queries (“give me all data loaded in Q1 2023”) and audit queries that reference a known date range.
  • Partition granularity: monthly is standard. For extremely high-volume tables (hundreds of millions of rows per day), switch to daily partitions.
  • Pruning benefit: when the satellite change-detection query filters by today’s LOAD_DATE during vault loading, it scans only today’s partition — not five years of history.
  • Works for: all vault table types — Hub, Link, and Satellite — since every table carries LOAD_DATE.
SQL — LOAD_DATE Partitioning (Snowflake & PostgreSQL)
-- ── Snowflake: cluster by load date ─────────────────────────
-- Automatic micro-partition pruning on the clustered column
ALTER TABLE SAT_CUSTOMER_DEMOGRAPHICS
    CLUSTER BY (TO_DATE(LOAD_DATE));

-- ── PostgreSQL: declarative range partitioning ───────────────
CREATE TABLE SAT_CUSTOMER_DEMOGRAPHICS (
    HUB_CUSTOMER_HK  CHAR(32)      NOT NULL,
    LOAD_DATE        TIMESTAMP     NOT NULL,
    LOAD_END_DATE    TIMESTAMP         NULL,
    HASH_DIFF        CHAR(32)      NOT NULL,
    RECORD_SOURCE    VARCHAR(100)  NOT NULL,
    FIRST_NAME       VARCHAR(100),
    LAST_NAME        VARCHAR(100),
    EMAIL            VARCHAR(200),
    PRIMARY KEY (HUB_CUSTOMER_HK, LOAD_DATE)
) PARTITION BY RANGE (LOAD_DATE);

-- Create quarterly child partitions
CREATE TABLE SAT_CUSTOMER_DEMOGRAPHICS_2023_Q1
    PARTITION OF SAT_CUSTOMER_DEMOGRAPHICS
    FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');

CREATE TABLE SAT_CUSTOMER_DEMOGRAPHICS_2023_Q2
    PARTITION OF SAT_CUSTOMER_DEMOGRAPHICS
    FOR VALUES FROM ('2023-04-01') TO ('2023-07-01');

Partitioning by Hash Key Range

Hash key range partitioning assigns satellite rows to partitions based on the numeric value of the hash key. This enables two valuable properties:

  • Parallel loading without lock contention: each parallel loader thread owns one partition exclusively. Thread 1 loads entities whose HK starts with hex 0–3; Thread 2 loads 4–7; and so on. No thread ever touches another’s partition, eliminating row-level lock contention entirely.
  • Partition pruning on Hub-to-Satellite joins: when HUB_CUSTOMER and SAT_CUSTOMER_DEMOGRAPHICS share the same HK range partition scheme, the optimizer can eliminate entire partition pairs from a join rather than joining all partitions.

This strategy is most impactful on on-premises MPP platforms (Teradata, Oracle Exadata, SQL Server). On Snowflake the engine’s built-in virtual warehouse parallelism handles load concurrency without explicit HK range partitioning.

SQL — Composite Clustering for Maximum Pruning (Snowflake)
-- Cluster on BOTH hash key AND load date for maximum pruning efficiency
-- Satellite queries filter on HK (entity lookup) AND LOAD_DATE (time slice)
ALTER TABLE SAT_CUSTOMER_DEMOGRAPHICS
    CLUSTER BY (HUB_CUSTOMER_HK, TO_DATE(LOAD_DATE));

-- Query 1: entity-scoped history — prunes on HUB_CUSTOMER_HK
SELECT * FROM SAT_CUSTOMER_DEMOGRAPHICS
WHERE  HUB_CUSTOMER_HK = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4'
ORDER BY LOAD_DATE DESC;

-- Query 2: time-bounded audit — prunes on LOAD_DATE
-- scans only January 2023 micro-partitions, not all history
SELECT * FROM SAT_CUSTOMER_DEMOGRAPHICS
WHERE  LOAD_DATE >= '2023-01-01'
  AND  LOAD_DATE  <  '2023-02-01';
💡

Partition pruning is free performance. It requires no query changes — the database engine applies pruning automatically when it detects filter predicates that align with partition columns. The only cost is the initial design decision and DDL. Once in place, every query that filters on LOAD_DATE or HUB_{ENTITY}_HK gets faster automatically as the table grows.

Concept 04

Materialized Views & Parallel Load Architecture

🧠 ELI5 — Explain Like I’m Five

Materialized views are a “pre-computed result set” stored as a real table. For Data Vault, they’re useful in the Information Mart layer where queries would otherwise recompute the same vault-to-mart transformation repeatedly. But they must be used carefully — they add storage and refresh overhead, and they must never appear in the Raw Vault.

Where Materialized Views Make Sense in Data Vault

Three scenarios justify the overhead:

  • Current-record views: a materialized view over each satellite that always shows the latest row (LOAD_END_DATE IS NULL). Refreshed after each vault load cycle. Eliminates the per-query correlated subquery cost entirely.
  • Pre-joined Hub + Satellite current state: combine the Hub’s business key with all current satellite attributes into a single wide row per entity. This is the “Customer 360” pattern — one row per customer with all current attributes across demographics, contact, risk score, and more. BI tools query one flat table instead of doing vault joins.
  • Star schema facts derived from vault joins: a FACT table in the Information Mart that joins the Raw Vault once and stores the result, rather than re-joining every time a BI tool queries it.
SQL — Materialized Current-Customer View (Hub + 3 Satellites)
-- Materialized current-customer view (refreshed after each vault load)
-- Lives in the Information Mart or Business Vault layer — NEVER Raw Vault
CREATE MATERIALIZED VIEW MV_CUSTOMER_CURRENT AS
SELECT
    h.HUB_CUSTOMER_HK,
    h.CUSTOMER_BK,
    -- From demographics satellite (current row only)
    d.FIRST_NAME,
    d.LAST_NAME,
    -- From contact satellite (current row only)
    c.EMAIL,
    c.PHONE,
    -- From risk score Business Satellite (current row only)
    f.CREDIT_LIMIT,
    f.RISK_CATEGORY
FROM HUB_CUSTOMER h

-- LEFT JOIN so customers with no satellite row still appear
LEFT JOIN SAT_CUSTOMER_DEMOGRAPHICS d
    ON  d.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
    AND d.LOAD_END_DATE IS NULL

LEFT JOIN SAT_CUSTOMER_CONTACT c
    ON  c.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
    AND c.LOAD_END_DATE IS NULL

LEFT JOIN BSAT_CUSTOMER_RISK_SCORE f
    ON  f.HUB_CUSTOMER_HK = h.HUB_CUSTOMER_HK
    AND f.LOAD_END_DATE IS NULL;

-- Refresh after each vault load wave completes (PostgreSQL syntax):
-- REFRESH MATERIALIZED VIEW MV_CUSTOMER_CURRENT;
-- On Snowflake: replace with a Dynamic Table for managed incremental refresh
⚠

Raw Vault is a materialized-view-free zone. The Raw Vault must remain a pure, unmodified record of source data. An MV in the Raw Vault creates a derived object whose refresh SQL could contain bugs — corrupting the audit record without anyone noticing. All materialized views belong in the Business Vault or Information Mart layers, where derived data is explicitly separated from raw source data.

The 5-Wave Parallel Load Pipeline

The single most impactful performance improvement for most Data Vault implementations is ensuring the load pipeline is truly parallel. Many teams have sequential steps that don’t need to be. The correct architecture runs in five waves:

  • Waves 1, 2, and 3 are fully parallel within each wave — every job in a wave runs simultaneously. No job in Wave N waits for another job in the same wave.
  • Wave gates are the only required sequencing — Wave 4 cannot start until Wave 3 is 100% complete. Wave 5 cannot start until Wave 4 is complete.

// Parallel Load Architecture — 5-Wave Pipeline

Wave 1 ⚡ Parallel

Source Extraction

CRM ERP Billing Web Events + N sources

All source extracts run simultaneously in separate parallel jobs

↓ gate: all Wave 1 jobs complete — trigger Wave 2 ↓
Wave 2 ⚡ Parallel

Staging Loads

STG_CUSTOMER STG_ORDER STG_PRODUCT STG_N…

Hashing, deduplication, HashDiff computation — all staging tables load in parallel

↓ gate: all staging loads complete — trigger Wave 3 ↓
Wave 3 ⚡ Parallel

Raw Vault Loads

HUB_CUSTOMER LINK_CO SAT_CUST_DEMO SAT_CUST_CNT all simultaneously

All Hubs, Links, and Satellites load at the same time — no cross-table dependencies exist

↓ gate: raw vault 100% complete — Wave 4 begins ↓
Wave 4 ⚙ After W3

Business Vault Refresh

PIT_CUSTOMER PIT_ORDER BRIDGE_CO BSAT_RISK

PIT and Bridge tables refresh in parallel — but only after all Raw Vault loads are confirmed complete

↓ gate: business vault 100% complete — Wave 5 begins ↓
Wave 5 📈 After W4

Information Mart Rebuild

DIM_CUSTOMER FACT_ORDERS MV_CUSTOMER_CURRENT

Star schema dimensions, facts, and materialized views refresh for BI tool consumption

Pipeline anti-pattern to eliminate: any sequential dependency within a wave. A common mistake is loading HUB_CUSTOMER, then waiting for it to complete before starting SAT_CUSTOMER_DEMOGRAPHICS. These tables have no DV foreign key enforcement and can always load in parallel. Audit your orchestration DAG (Airflow, ADF, dbt) and confirm that all objects in Wave 3 have only Wave 2 as their upstream dependency — not each other.

Concept 05

Interview Q&A: Performance

What indexes does a Satellite table need and why? ▶

Three index types cover the full satellite workload pattern:

1. Composite primary key on (HUB_{ENTITY}_HK, LOAD_DATE) — the standard satellite PK, created automatically by the constraint. It enables efficient lookups by entity and exact timestamp and supports the satellite loader’s duplicate-check query (which looks up existing rows by HK + LOAD_DATE before inserting).

2. Filtered/partial index on (HUB_{ENTITY}_HK) WHERE LOAD_END_DATE IS NULL — the single most impactful satellite index. The dominant satellite query pattern is “give me the current record for this entity.” A filtered index covering only IS NULL rows is dramatically smaller than a full-table index and directly serves this exact pattern without ever reading historical rows. At scale — billions of satellite rows and hundreds of concurrent BI queries — this index is the difference between sub-second and a full table scan timeout.

3. Index on HASH_DIFF (composite with HK) — supports the satellite loader’s change-detection join. The loader compares incoming HashDiff values against the current satellite HashDiff; without an index this forces a full satellite scan for every load cycle.

On cloud platforms (Snowflake, BigQuery), B-tree indexes are replaced by clustering keys. Cluster Satellites on HUB_{ENTITY}_HK to co-locate all rows for the same entity in the same micro-partition. Automatic clustering maintenance keeps the benefit as rows accumulate, without manual index rebuilds.

When would you use a materialized view in a Data Vault architecture and when wouldn’t you? ▶

Use an MV when: the transformation is expensive (many vault joins, aggregations across large satellites), it is queried frequently by many concurrent users, and the underlying data changes infrequently relative to query frequency. Strong candidates: a current-state customer view joining Hub + all current Satellite rows (the “Customer 360” MV); a daily KPI summary built from a vault-to-mart query that aggregates millions of rows.

Don’t use MVs when: the vault loads very frequently (MV refresh overhead exceeds the benefit); the query is ad-hoc (materializing a one-off query wastes storage and refresh time); or on Snowflake, where Dynamic Tables are a superior alternative — they auto-refresh based on a configurable lag target and handle incremental updates far more efficiently than full MV rebuilds triggered manually.

Data Vault rule: never add a materialized view to the Raw Vault layer. The Raw Vault must remain a pure, unmodified record of source data. An MV in the Raw Vault creates a derived object whose definition could contain bugs — silently corrupting the audit record. All MVs belong in the Business Vault or Information Mart layers, where derived data is explicitly distinct from raw source data.

How would you reduce the vault load window from 4 hours to under 1 hour? ▶

Four levers, applied in order of impact:

1. Parallelism audit — open the orchestration tool (Airflow, ADF, dbt DAG) and verify that all Hub, Link, and Satellite loads in Wave 3 are truly running simultaneously. Any sequential dependency within Wave 3 is an immediate win to remove. Most teams find at least one table that loads serially because someone added a dependency that isn’t needed.

2. Staging SQL profiling — the staging layer does the heaviest computation: hashing, deduplication, HashDiff generation, filtering. Profile staging query execution plans first before tuning vault loads. Fixing a hash key computation that recalculates on every row, or adding a date-range filter that was missing, can cut staging time by 50%+ before touching the vault layer at all.

3. Partition pruning — ensure satellite change-detection queries only scan recent satellite data, not full history. Without partitioning, a satellite with five years of history forces the loader to scan all five years to find the current row per entity for comparison. Adding LOAD_DATE partitioning makes the loader scan only today’s or this-month’s partition.

4. PIT table decoupling — if PIT refresh is on the Wave 3 critical path, move it to Wave 4 where it belongs. PIT tables are Business Vault objects and must only start after Raw Vault is complete anyway. Also switch to incremental PIT refresh (only changed entities) if currently running a full rebuild every cycle.

Finally, check for lock contention: if multiple parallel sessions load the same satellite table without partition isolation, they may serialize at the storage layer. On on-premises platforms, partition by hash key range and pin each load thread to its own partition to eliminate contention entirely.

What is the “current record” query problem in Data Vault and how do you solve it? ▶

Every satellite can have many rows per entity, only one of which is “current.” Finding the current record requires either a correlated subquery:

WHERE LOAD_DATE = (SELECT MAX(LOAD_DATE) FROM sat WHERE HK = ?)

or a sentinel column filter: WHERE LOAD_END_DATE IS NULL

At scale — billions of satellite rows, hundreds of concurrent BI queries — the correlated MAX(LOAD_DATE) subquery forces a satellite scan per entity per query. Even with indexes, at billion-row scale this fails to meet sub-second SLAs under concurrent load.

Three solutions in increasing implementation complexity:

1. Maintain LOAD_END_DATE — when a new satellite row arrives, update the previous row’s LOAD_END_DATE to the new row’s LOAD_DATE. This enables a simple WHERE LOAD_END_DATE IS NULL filter. Combined with the filtered index on IS NULL rows, current-record lookups become single-row seeks. Trade-off: the satellite requires UPDATE operations alongside INSERTs, breaking the pure insert-only model. Most architects accept this trade-off for the performance gain.

2. PIT tables — pre-materialize the MAX(LOAD_DATE) lookup for a fixed set of snapshot dates. Every query that joins through a PIT table gets the correct satellite LOAD_DATE without any aggregation at query time. Best suited for scheduled batch reporting against known, recurring snapshot dates.

3. Current-state materialized views — pre-join Hub + all satellite current rows into a fast lookup table (e.g., MV_CUSTOMER_CURRENT) refreshed after each load cycle. BI tools query the MV directly — no vault joins, no correlated subqueries. Best for operational dashboards with high concurrency and strict sub-second latency requirements.

✅ Module 08 Key Takeaways

  • Index every Satellite for three access patterns: composite PK on (HK, LOAD_DATE) for point-in-time lookups, a filtered index WHERE LOAD_END_DATE IS NULL for current-record queries (the dominant workload), and an index on HASH_DIFF for change detection during loads. On cloud platforms, replace all three with clustering keys.
  • PIT tables solve the expensive MAX(LOAD_DATE) correlated subquery problem. Use incremental refresh — only rebuild PIT rows for entities with new satellite data and snapshot dates not yet covered — to keep PIT refresh fast at any scale. Consider LATERAL joins for ad-hoc, non-recurring snapshot queries.
  • Partition vault tables by LOAD_DATE for time-bounded audit queries and by hash key range for parallel load isolation. On Snowflake use composite clustering on (HUB_{ENTITY}_HK, TO_DATE(LOAD_DATE)) to gain both benefits simultaneously with automatic maintenance.
  • Materialized views belong in the Business Vault and Information Mart layers — never in the Raw Vault. On Snowflake prefer Dynamic Tables over traditional MVs for managed, incremental auto-refresh behavior without manual scheduling.
  • The 5-wave parallel load pipeline (Extract → Stage → Raw Vault → Business Vault → Info Mart) eliminates sequential bottlenecks. Waves 1–3 are fully parallel within each wave. Waves 4 and 5 are gated behind their upstream wave. Audit your DAG to confirm true intra-wave parallelism — it is the fastest single fix for long load windows.