Data Vault 2.0 Explained: Hubs, Links and Satellites in SQL

Data Vault 2.0 Explained: Hubs, Links and Satellites in SQL

Your source systems change every quarter. A new CRM arrives, a billing platform is replaced, an acquisition brings a third customer database, and every change ripples through a carefully built star schema. Data Vault 2.0 was designed for exactly this problem. It separates what a business entity is from how entities relate and from what we know about them over time, so new sources are added rather than retrofitted.

This guide is for data engineers who build warehouses or lakehouses on Snowflake, Databricks, BigQuery or Microsoft Fabric. It explains the three core building blocks, shows the SQL to create and load them, and covers the query-side helpers (PIT and bridge tables) and the mistakes that make Data Vault projects slow and painful.

The three building blocks

A raw Data Vault has only three table types. Everything else is built from them.

Hubs: the business keys

A hub holds the unique list of business keys for one core concept, such as customer, product or order. A business key is the identifier the business actually uses (a customer number or an order reference), not a surrogate ID generated by one application. A hub row never changes once it is written. Its columns are the hash key, the business key, the load date and the record source.

Links: the relationships

A link records that two or more business keys were seen together, for example customer placed order. Links are always many-to-many, even when today’s source enforces one-to-many. That one decision is what lets the model absorb a change in business rules without restructuring tables.

Satellites: the history

A satellite hangs off one hub or one link and stores the descriptive attributes (name, email, status, amount) with full history. Each change creates a new row; nothing is updated in place. Satellites are usually split by source system and by rate of change, so a fast-moving status column does not force you to copy slow-moving address columns every day.

Hash keys, load dates and record sources

Data Vault 2.0 replaced the sequence-based surrogate keys of Data Vault 1.0 with hash keys: a deterministic hash (commonly MD5 or SHA-256) of the cleaned business key. Because the same input always gives the same hash, hubs, links and satellites can be loaded in parallel with no lookups between them, and loads from different platforms produce matching keys.

Two more standard columns appear on every table:

  • load_dts: when the row arrived in the vault, set by the load process (not the source’s own timestamp).
  • record_source: where the row came from, such as crm.customers. This gives you audit and lineage for free.

Satellites add a hashdiff: a hash of all the descriptive columns. Comparing one hash is far cheaper than comparing twenty columns when you check whether anything changed.

Creating the tables

The DDL below uses Snowflake-style SQL, but it maps directly to Databricks SQL, BigQuery or a Fabric Warehouse. Hash keys are stored as fixed-length strings.

CREATE TABLE raw_vault.hub_customer (
    customer_hk     CHAR(32)      NOT NULL,  -- MD5 of business key
    customer_number VARCHAR(50)   NOT NULL,  -- business key
    load_dts        TIMESTAMP_NTZ NOT NULL,
    record_source   VARCHAR(100)  NOT NULL
);

CREATE TABLE raw_vault.link_customer_order (
    customer_order_hk CHAR(32)      NOT NULL,  -- hash of both business keys
    customer_hk       CHAR(32)      NOT NULL,
    order_hk          CHAR(32)      NOT NULL,
    load_dts          TIMESTAMP_NTZ NOT NULL,
    record_source     VARCHAR(100)  NOT NULL
);

CREATE TABLE raw_vault.sat_customer_crm (
    customer_hk    CHAR(32)      NOT NULL,
    load_dts       TIMESTAMP_NTZ NOT NULL,
    hashdiff       CHAR(32)      NOT NULL,
    customer_name  VARCHAR(200),
    email          VARCHAR(200),
    status         VARCHAR(20),
    record_source  VARCHAR(100)  NOT NULL
);

Loading a hub and a satellite

Every load starts from a staging view that cleans the business key and computes the hashes once. Cleaning matters: trim spaces and standardise case before hashing, and use a delimiter between columns so that 'AB' || 'C' and 'A' || 'BC' do not collide.

CREATE OR REPLACE VIEW stage.stg_crm_customers AS
SELECT
    MD5(UPPER(TRIM(customer_number)))                          AS customer_hk,
    UPPER(TRIM(customer_number))                               AS customer_number,
    MD5(CONCAT_WS('||',
        COALESCE(TRIM(customer_name), ''),
        COALESCE(LOWER(TRIM(email)), ''),
        COALESCE(UPPER(TRIM(status)), '')))                    AS hashdiff,
    customer_name, email, status,
    CURRENT_TIMESTAMP()                                        AS load_dts,
    'crm.customers'                                            AS record_source
FROM landing.crm_customers;

The hub load is insert-only: add business keys the hub has not seen before.

INSERT INTO raw_vault.hub_customer (customer_hk, customer_number, load_dts, record_source)
SELECT customer_hk, customer_number, MIN(load_dts), MIN(record_source)
FROM stage.stg_crm_customers s
WHERE NOT EXISTS (
    SELECT 1 FROM raw_vault.hub_customer h
    WHERE h.customer_hk = s.customer_hk
)
GROUP BY customer_hk, customer_number;

The satellite load inserts a row only when the hashdiff differs from the latest row for that key. This is what keeps satellites small: an unchanged customer adds nothing, however often you reload.

INSERT INTO raw_vault.sat_customer_crm
    (customer_hk, load_dts, hashdiff, customer_name, email, status, record_source)
WITH latest AS (
    SELECT customer_hk, hashdiff
    FROM raw_vault.sat_customer_crm
    QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_hk ORDER BY load_dts DESC) = 1
)
SELECT s.customer_hk, s.load_dts, s.hashdiff,
       s.customer_name, s.email, s.status, s.record_source
FROM stage.stg_crm_customers s
LEFT JOIN latest l
       ON l.customer_hk = s.customer_hk
WHERE l.customer_hk IS NULL          -- brand new key
   OR l.hashdiff <> s.hashdiff;     -- attributes changed

QUALIFY works in Snowflake, Databricks SQL and BigQuery. On platforms without it, wrap the window function in a subquery. Links load exactly like hubs: insert the combined hash key if it is not already present.

Querying the vault: PIT and bridge tables

Data Vault is built for loading and auditing, not for dashboards. Reporting users should never join raw satellites themselves. Two helper structures, usually kept in a business vault layer, make querying fast:

  • Point-in-time (PIT) tables store, for each hub key and snapshot date, the load_dts of the valid row in each satellite. A query then uses simple equi-joins instead of range joins across several satellites.
  • Bridge tables pre-join a chain of hubs and links (for example customer → order → product) so marts do not repeat the same multi-hop join.

A minimal daily PIT build for one satellite looks like this:

INSERT INTO business_vault.pit_customer (customer_hk, snapshot_date, sat_crm_load_dts)
SELECT h.customer_hk,
       d.snapshot_date,
       MAX(s.load_dts) AS sat_crm_load_dts
FROM raw_vault.hub_customer h
CROSS JOIN (SELECT CURRENT_DATE() AS snapshot_date) d
LEFT JOIN raw_vault.sat_customer_crm s
       ON s.customer_hk = h.customer_hk
      AND s.load_dts < DATEADD(day, 1, d.snapshot_date)
GROUP BY h.customer_hk, d.snapshot_date;

On top of PITs and bridges you build information marts: ordinary star schemas, often as views, that BI tools and analysts use. Data Vault does not replace dimensional modelling; it sits underneath it.

Data Vault vs star schema

AspectData Vault 2.0Star schema (Kimball)
Main purposeIntegration, history and auditFast, simple reporting
Adding a new sourceNew satellites and links; existing tables untouchedOften changes dimensions and fact grain
LoadingInsert-only, highly parallelUpdates for SCD Type 1/2, ordered loads
Number of tablesHighLow
Query complexityHigh without PIT and bridge tablesLow
Best fitMany changing sources, regulated industriesStable sources, small teams, BI-first

A common lakehouse pattern is raw Data Vault in the silver layer and star-schema marts in gold. If you have one or two stable sources, a well-built star schema with SCD Type 2 dimensions is usually enough.

Automating it with dbt

Data Vault is very repetitive: every hub load looks the same, as does every satellite load. That makes it ideal for code generation. In dbt, the open-source AutomateDV package (formerly dbtvault) provides macros for staging, hubs, links, satellites and PIT tables, so each model is a few lines of metadata. Pair it with dbt tests for uniqueness of hash keys and not-null business keys, and you catch most load errors before they reach the marts.

Common pitfalls

  • Using source surrogate IDs as business keys. If the CRM’s internal ID changes after a migration, your hub breaks. Use the key the business recognises.
  • Inconsistent hashing. One pipeline trims and upper-cases, another does not, and the same customer gets two hash keys. Put the hashing rules in one shared macro or view.
  • NULLs in hash inputs. CONCAT with a NULL can return NULL on some platforms. Always COALESCE to an empty string or a fixed placeholder.
  • One giant satellite per hub. Splitting by source and by rate of change keeps satellites narrow and cuts storage.
  • Letting analysts query the raw vault. Without PITs, bridges and marts, queries become slow multi-way range joins and people lose trust in the model.
  • Putting business rules in the raw vault. The raw vault should store source data as received. Derived values and cleansing rules belong in the business vault, so you can change them and rebuild.

Key takeaways

  • Hubs hold business keys, links hold relationships, satellites hold history. Everything is insert-only.
  • Hash keys let you load hubs, links and satellites in parallel; a hashdiff makes change detection cheap.
  • Standardise and delimit inputs before hashing, and handle NULLs explicitly.
  • Use PIT and bridge tables, then star-schema marts, for reporting.
  • Choose Data Vault when sources are many and changing, and audit matters. For a small, stable estate, a star schema is simpler.
  • Generate the repetitive SQL with tools such as AutomateDV rather than writing it by hand.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *