Hubs, links, and satellites: an auditable, scalable, parallel-loadable pattern for enterprise data warehouses.
⏱ 19 min readTopics chapter readerLevel · Dimensional & Warehouse
01 · Overview
Start here
⏱ 7 min · Topic 1 of 14
Data Vault is a way to build a warehouse from many source systems. The chapter Inmon vs Kimball vs Data Vault gave the overview. This chapter shows how each part works.
Recap in one line: a hub holds business keys, a link holds relationships, and a satellite holds details and history. Data Vault 2.0 also describes a way of working. This chapter covers the table design.
The raw vault keeps data as the sources sent it. The business vault adds tables built by business rules. Marts sit on top, and people query them.
You can use earlier chapters here: Keys & Identity, Grain: What One Row Means and Slowly Changing Dimensions (SCD 0–7).
The story is the shop again. It has customers 1 to 5 and orders 101 to 106. Asha moves from IN to DE on 2026-01-08.
A second source joins: a CRM (customer relationship management) tool with its own customer codes, such as A-301. The CRM rows, and a few other rows, are made up. The text says so each time.
Hash keys in the tables are real MD5 values, cut to the first 8 characters.
What you will learn
Why you would pay for a vault, and what changes when a source changes.
Hubs, links and satellites, with real rows and loads.
Hash keys, hash diffs, and why loads can run at the same time.
Same-as links, for one person with two keys.
Multi-active and effectivity satellites.
PIT tables, bridge tables and marts, which make a vault easy to read.
The costs, and when to skip Data Vault.
02 · Why
Why use Data Vault?
⏱ 6 min · Topic 2 of 14
Take Asha. The shop stores her as customer 1. The CRM stores her as A-301, with the country written as Germany.
Two sources, one person (CRM row is made up)
system
id
name
country
shop
1
Asha
DE
crm
A-301
Asha
Germany
Data Vault is built for three pressures:
Audit: you must prove what arrived, from which system, and when.
Change: sources add columns, and new sources join.
Parallel loading: many loads must run at the same time.
Each pressure shapes the design. Rows are never updated or deleted, so you can prove the past. New sources add tables, and hash keys let loads run without waiting for each other.
Now see what happens when something changes:
Four changes
Change
What you do
What is left alone
A new source arrives (the CRM)
Add hub rows and a new satellite
All old rows, and the shape of every old table
A source adds a column
Add a new satellite
All old rows
A business rule changes
Change the business vault or a mart
The raw vault
The business key is redefined (another column becomes the key)
Build a new hub, then reload its links and satellites
Very little. This is costly in any model.
Remember
The price is more tables and more joins. People do not query a vault directly. They read marts built on top of it, and later sections show how.
03 · Hubs
Hubs: business keys
⏱ 8 min · Topic 3 of 14
A hub is a table of business keys. A business key is the id the business uses, such as customer_id 1. A hub holds the key and nothing that changes.
Every hub row has four columns:
customer_hk: the hash key, a code made from the business key.
customer_bk: the business key itself, as text.
load_date: the day the warehouse first saw this key.
record_source: the system that sent it.
A hash function turns any text into a fixed-length code. MD5 gives 32 letters and digits, and the same text always gives the same code. Tables here show the first 8 characters.
This is hub_customer after both sources loaded. The shop loaded on 2026-01-01 and the CRM on 2026-01-12:
hub_customer (CRM rows are made up) (8 rows)
customer_hk
customer_bk
load_date
record_source
c4ca4238…
1
2026-01-01
shop_db
c81e728d…
2
2026-01-01
shop_db
eccbc87e…
3
2026-01-01
shop_db
a87ff679…
4
2026-01-01
shop_db
e4da3b7f…
5
2026-01-01
shop_db
e7f829de…
A-301
2026-01-12
crm
4c6cf346…
A-302
2026-01-12
crm
420afaed…
A-303
2026-01-12
crm
Asha has two hub rows, because the two systems use two keys. A later section shows how the vault says they are one person.
Load a hub
The loader adds a row only for a key the hub does not have yet. It cleans the key first: it removes spaces at the start and end, and uses capital letters. The CRM export of 2026-01-12 repeats one code, typed badly:
Load new CRM keys into the hubworked example
SQL
Input data
hub_customer (before the load)5 rows
customer_hk
customer_bk
load_date
record_source
c4ca4238…
1
2026-01-01
shop_db
c81e728d…
2
2026-01-01
shop_db
eccbc87e…
3
2026-01-01
shop_db
a87ff679…
4
2026-01-01
shop_db
e4da3b7f…
5
2026-01-01
shop_db
stage_crm_contacts (made up)4 rows
contact_code
name
country
A-301
Asha
Germany
A-302
Ben
United Kingdom
A-303
Priya
India
a-301
Asha
Germany
The last row is the same code as the first, with a space and a lowercase a.
The stage has 4 rows, but the hub gets 3. After cleaning, " a-301" and "A-301" are the same key, and DISTINCT keeps one.
A hub row never changes after it is inserted. A key that stops being used stays in the hub.
04 · Links
Links: relationships
⏱ 8 min · Topic 4 of 14
A link is a table of relationships between hubs. One link row says: this customer placed this order.
A link row has its own hash key, the hash keys of the hubs it joins, a load_date and a record_source. Its own hash key comes from the business keys joined with a separator, such as 1|101.
This is link_customer_order for the six shop orders. The load_date is the order date, to keep the story short:
link_customer_order (6 rows)
link_hk
customer_hk
order_hk
load_date
record_source
49b8726d…
c4ca4238…
38b3eff8…
2026-01-03
shop_db
491b9ef1…
c4ca4238…
ec895663…
2026-01-10
shop_db
98ae4af1…
c81e728d…
6974ce5a…
2026-01-11
shop_db
50a84138…
eccbc87e…
c9e1074f…
2026-01-15
shop_db
6a1e1427…
eccbc87e…
65b9eea6…
2026-01-20
shop_db
efdabc63…
a87ff679…
f0935e4c…
2026-01-21
shop_db
A link holds no details. Details about the relationship go in a satellite on the link. A later section shows one.
Many-to-many by design
A link has no rule such as "one customer per order". Many customers can point to many orders. A table with one customer_id column per order would need a schema change to allow two customers.
Suppose a made-up order 301 on 2026-01-22 is shared by customers 3 (Carla) and 5 (Omar). The loader inserts two link rows, and no table changes shape. This query finds orders with more than one customer:
Find orders with more than one customerworked example
SQL
Input data
link_customer_order (8 rows, order 301 is made up)8 rows
Only order 301 has two link rows. Orders 101 to 106 have one each, so HAVING removes them.
Each combination of customer and order appears once. Before an insert, the loader checks the link hash key.
05 · Satellites
Satellites: details and history
⏱ 9 min · Topic 5 of 14
A satellite holds the details of a hub or link, and their history. Details are the columns that change, such as name and country.
Each satellite row has these columns:
The hash key of its hub or link.
load_date: when the warehouse loaded this version.
record_source: the system that sent it.
hash_diff: a hash of all the detail columns.
The detail columns themselves.
This is sat_customer_details after the load of 2026-01-08. Asha has two rows, because she moved:
sat_customer_details (6 rows)
customer_hk
load_date
record_source
hash_diff
name
country
c4ca4238…
2026-01-01
shop_db
07e23296…
Asha
IN
c81e728d…
2026-01-01
shop_db
f6766c76…
Ben
GB
eccbc87e…
2026-01-01
shop_db
55079836…
Carla
US
a87ff679…
2026-01-01
shop_db
4ca63da6…
Dev
NULL
e4da3b7f…
2026-01-01
shop_db
4ec0f389…
Omar
IN
c4ca4238…
2026-01-08
shop_db
e2329616…
Asha
DE
The CRM gets its own satellite. The two loads never touch each other. The CRM keeps its own words, so the country is Germany, not DE:
sat_customer_crm (made up) (3 rows)
customer_hk
load_date
record_source
hash_diff
name
country
e7f829de…
2026-01-12
crm
9f1dca00…
Asha
Germany
4c6cf346…
2026-01-12
crm
e4f1a5fa…
Ben
United Kingdom
420afaed…
2026-01-12
crm
74fad5bb…
Priya
India
What hash_diff does
The hash_diff is a hash of the detail columns, joined with a separator. The loader compares it with the newest row for the same key. The same value means no change, so nothing is inserted.
The shop sends all five customers again on 2026-01-08. The stage table already holds the hash key and hash_diff:
Four hash_diff values match the newest row, so those customers add nothing. Asha changed from 07e23296 to e2329616, so one row is inserted.
Watch out
A NULL breaks the hash: in PostgreSQL, joining text with NULL gives NULL. A comparison with NULL is never true, so a real change to Dev would be skipped. Wrap country in COALESCE(country, '') first.
06 · Keys
Hash keys and parallel loads
⏱ 9 min · Topic 6 of 14
An older design uses a sequence key: the hub counts up and gives each new business key the next number. A hash key is computed from the business key instead. Data Vault 2.0 uses hash keys.
Look at what the link loader needs in each design:
Sequence key or hash key
Question
Sequence key
Hash key
How is the key made?
The hub counts up
A hash of the business key
Does the link loader read the hub?
Yes, to find the number
No, it computes the hash
Can hub and link load together?
No, the hub goes first
Yes
How big is the key?
A small number
32 characters for MD5
Here the link loader gets orders 101 to 103 and computes both keys itself:
Four texts give four hashes for one customer. The hub would get four rows.
Write one hashing rule and share it. Trim spaces, use capitals, and turn NULL into empty text. Join columns with a fixed separator.
Without a separator, keys 1 and 11 give 111, and so do 11 and 1. With the separator |, they give 1|11 and 11|1.
Two different keys can give the same hash. This is called a collision. It is very rare, and some teams choose a longer hash, such as SHA-256, to lower the risk.
Without a lookup, a link row can arrive before its hub row. After each load, check that every hash key in a link or satellite has a hub row.
07 · Many sources
Same person, two keys
⏱ 8 min · Topic 7 of 14
The hub has two rows for Asha: customer 1 and A-301. The raw vault keeps both, as each source sent them. It does not guess that they are one person.
A same-as link says that two hub rows are the same thing. It holds the hash key of the main row and the hash key of the duplicate. Its own link hash key is left out to stay short.
Deciding the match is a separate job. The chapter Master Data Management (MDM) teaches how to do it.
A rule creates a same-as link, so it usually lives in the business vault. Here are two made-up matches:
link_same_as_customer (made up) (2 rows)
main_hk
duplicate_hk
load_date
record_source
c4ca4238…
e7f829de…
2026-01-12
match_rules
c81e728d…
4c6cf346…
2026-01-12
match_rules
Priya (A-303) is only in the CRM, so she has no row. This query joins the hub twice, once for each side:
Which CRM code belongs to which shop customerworked example
The first link row joins hub row c4ca4238 (shop 1) to e7f829de (A-301). A-303 is missing, as expected.
The CRM also writes the country as a word, and the shop uses a code. A reference table maps one to the other. It is a small lookup table that sits outside hubs, links and satellites:
ref_country (made up) (3 rows)
crm_text
country_code
load_date
record_source
Germany
DE
2026-01-12
reference
United Kingdom
GB
2026-01-12
reference
India
IN
2026-01-12
reference
Watch out
Do not merge the two hub rows to remove the duplicate. A wrong match is only a bad link row, and you can add a corrected one. A wrong merge destroys the original keys, and you cannot get them back.
08 · Edge cases
Two special satellites
⏱ 8 min · Topic 8 of 14
Most satellites hold one current row for each key. Two cases need something more.
Multi-active satellite
Sometimes one key has several active rows at once. Asha has a home phone and a work phone, so the satellite key gets one more column, phone_type. The shop sends the full set each time, and these rows are made up:
The newest load_date is 2026-01-20, and it has one row. The work phone is gone from the current set, but its old row stays for audit.
Effectivity satellite
A link row never ends, because the vault never deletes rows. An effectivity satellite says when the relationship was valid. In this made-up case, sales rep R-310 had Asha from 2026-01-12, and R-311 from 2026-01-20:
link_customer_rep (made up) (2 rows)
link_hk
customer_hk
rep_hk
load_date
record_source
5ff9ec8c…
c4ca4238…
aeb2c320…
2026-01-12
shop_db
9bf9d9ec…
c4ca4238…
5c76468a…
2026-01-20
shop_db
The effectivity satellite has three rows. The second row closes the first link. It is a new row, not an update:
sat_customer_rep_effectivity (made up) (3 rows)
link_hk
load_date
effective_from
effective_to
5ff9ec8c…
2026-01-12
2026-01-12
9999-12-31
5ff9ec8c…
2026-01-20
2026-01-12
2026-01-20
9bf9d9ec…
2026-01-20
2026-01-20
9999-12-31
Today’s rep is the link whose newest effectivity row ends on 9999-12-31. That is 9bf9d9ec, so R-311.
09 · Query helpers
PIT and bridge tables
⏱ 11 min · Topic 9 of 14
A question about one customer now touches a hub and several satellites. For each satellite, you must find the row that was current on a date. That is slow and easy to get wrong.
A point-in-time (PIT) table does this work in advance. It holds one row for each customer and snapshot date. Each row names the satellite row that was current, by its key and load_date.
The shop also sends marketing choices, so customers get a second satellite. It leaves out hash_diff to stay short, and its rows are made up.
The satellite also holds one ghost row, an empty row that means "no data yet". Its key is all zeros, and its load_date is 1900-01-01:
sat_customer_marketing (made up) (4 rows)
customer_hk
load_date
record_source
opt_in
00000000…
1900-01-01
SYSTEM
NULL
c4ca4238…
2026-01-01
shop_db
false
c4ca4238…
2026-01-15
shop_db
true
c81e728d…
2026-01-01
shop_db
true
The PIT points at it for every customer with no row, so the joins keep Carla, Dev and Omar.
For snapshot 2026-01-10, Asha’s row would point at 2026-01-08 and 2026-01-01. This is pit_customer for the snapshot date 2026-01-22:
pit_customer (5 rows)
customer_hk
snapshot_date
details_load_date
marketing_hk
marketing_load_date
c4ca4238…
2026-01-22
2026-01-08
c4ca4238…
2026-01-15
c81e728d…
2026-01-22
2026-01-01
c81e728d…
2026-01-01
eccbc87e…
2026-01-22
2026-01-01
00000000…
1900-01-01
a87ff679…
2026-01-22
2026-01-01
00000000…
1900-01-01
e4da3b7f…
2026-01-22
2026-01-01
00000000…
1900-01-01
Now the join needs no "latest row" subquery. Each satellite joins on two equal columns:
Asha shows DE and true, because the PIT points at her newest rows. Carla, Dev and Omar have NULL for opt_in: the ghost row holds no value.
A PIT adds one join, but each join is simple. The gain is that no query has to search for the latest row.
Watch out
Build the PIT after the last satellite load of the batch. If it runs earlier, it points at old rows, and the answers are quietly out of date.
Bridge tables
A bridge table does the same for a path across hubs. It stores the result of joining hubs and links in advance. This is bridge_customer_order, built from the link, with the business keys added:
bridge_customer_order (8 rows)
customer_hk
customer_bk
order_hk
order_bk
c4ca4238…
1
38b3eff8…
101
c4ca4238…
1
ec895663…
102
c81e728d…
2
6974ce5a…
103
eccbc87e…
3
c9e1074f…
104
eccbc87e…
3
65b9eea6…
105
a87ff679…
4
f0935e4c…
106
eccbc87e…
3
34ed066d…
301
e4da3b7f…
5
34ed066d…
301
A report can now go from customer to order with no hub joins. A bridge can also cover several links in a row. This one has a single link, to stay short.
10 · Serving
Marts for readers
⏱ 5 min · Topic 10 of 14
People read marts, not the raw vault. A mart is often a Kimball star, and PIT and bridge tables build it fast. A fact table can be built from a link and its satellite.
Here is a small customer summary. It uses the PIT for the current name and country, the bridge for orders, and the order satellite for status and amount:
The total is 160.50, the same as the paid total of the shop orders. The condition status = 'paid' sits in the ON clause, so Ben and Omar stay in the result with 0.
A mart can be a view or a table. If it is wrong, fix the rule and rebuild it. The raw vault still holds every source row.
Each order here has one satellite row. With more rows, use a PIT on orders too.
Watch out
Joint order 301 appears twice in the bridge, once for each customer. If it were paid, a sum over customers would count it twice. Decide who gets the revenue before you add up a total.
11 · Decide
Costs and when to skip
⏱ 5 min · Topic 11 of 14
Count the tables. In the SQL chapters, the shop has 2 tables: customers and orders. In the raw vault they become 5: hub_customer, hub_order, link_customer_order, sat_customer_details and sat_order_shop.
What you gain and what you pay
You gain
You pay
Proof of what arrived, and when
More tables and more joins
Loads that run at the same time
Hash rules that every loader must follow
New sources that add tables
A mart layer before anyone can query
Rules that change without touching raw data
A team that must learn the method
Skip Data Vault in these cases:
You have one or two stable sources.
No one asks you to prove the past.
A small team needs a first report fast.
People will query the tables directly.
A Kimball star with SCD Type 2 is simpler there. Many teams use both: a vault to join the sources, and stars to read the data.
Interview question
Would you use Data Vault here? Name the pressures first: audit, many changing sources, parallel loads. Then name the cost: more joins, so you need marts, PIT and bridge tables.
12 · Recap
Recap
⏱ 5 min · Topic 12 of 14
A hub stores business keys. A link stores relationships, many-to-many by design. A satellite stores details and history, and it only gets new rows.
Hash keys come from the business key, so loaders need no lookup. Hash diffs let a loader skip rows that did not change.
A same-as link joins two keys for one thing. The matching rules come from MDM.
PIT and bridge tables make reads fast, and marts are what people query.
Check yourself
Why does a hash key let a link load at the same time as a hub?
Dev has no country. What goes wrong if you hash without COALESCE?
The CRM sends A-301, and the shop sends 1. Where does the vault say they match?
Name two cases where you would not use Data Vault.
13 · Practice
Practice lab
⏱ 3 min · Topic 13 of 14
Each exercise opens a canvas for an entity-relationship diagram (ERD). You draw the tables, and the review checks them against sample rows.