Skip to content

TPC-H Data Vault Benchmark

CLI name: datavault - use benchbox run --benchmark datavault

Overview

The TPC-H Data Vault benchmark tests database performance with Data Vault 2.0 modeling patterns. It transforms TPC-H’s 8 normalized tables into 21 Data Vault tables and adapts all 22 TPC-H queries to work with Hub-Link-Satellite joins.

Data Vault 2.0 is an enterprise data warehouse modeling methodology that separates business keys (Hubs), relationships (Links), and descriptive attributes (Satellites). This design enables incremental loading, full auditability, and historical tracking.

Key Features

  • 21 Data Vault tables - 7 Hubs, 6 Links, 8 Satellites
  • 22 adapted queries - TPC-H queries rewritten for Hub-Link-Satellite patterns
  • MD5 hash keys - Surrogate keys using 32-character hash values
  • Audit columns - LOAD_DTS, LOAD_END_DTS, RECORD_SOURCE on all tables
  • HASHDIFF tracking - Change detection for satellite records
  • TPC-H source data - Leverages existing TPC-H data generation infrastructure
  • SQL dialect support - DDL translation via SQLGlot

Schema Description

Table Types

TypeCountPurpose
Hubs7Business keys with hash surrogates
Links6Relationships between Hubs
Satellites8Descriptive attributes with temporal tracking

Hub Tables (Business Entities)

TableBusiness KeySource
hub_regionr_regionkeyREGION
hub_nationn_nationkeyNATION
hub_customerc_custkeyCUSTOMER
hub_suppliers_suppkeySUPPLIER
hub_partp_partkeyPART
hub_ordero_orderkeyORDERS
hub_lineiteml_orderkey + l_linenumberLINEITEM
TableConnects
link_nation_regionNation → Region
link_customer_nationCustomer → Nation
link_supplier_nationSupplier → Nation
link_part_supplierPart ↔ Supplier
link_order_customerOrder → Customer
link_lineitemLineItem → Order, Part, Supplier

Satellite Tables (Attributes)

TableParentKey Attributes
sat_regionhub_regionr_name, r_comment
sat_nationhub_nationn_name, n_comment
sat_customerhub_customerc_name, c_address, c_acctbal, c_mktsegment
sat_supplierhub_suppliers_name, s_address, s_acctbal
sat_parthub_partp_name, p_mfgr, p_brand, p_type, p_size
sat_partsupplink_part_supplierps_availqty, ps_supplycost
sat_orderhub_ordero_orderstatus, o_totalprice, o_orderdate
sat_lineitemlink_lineiteml_quantity, l_extendedprice, l_discount, dates

Schema Relationships

erDiagram
    hub_region ||--o{ link_nation_region : "hk_region"
    hub_nation ||--o{ link_nation_region : "hk_nation"
    hub_nation ||--o{ link_customer_nation : "hk_nation"
    hub_nation ||--o{ link_supplier_nation : "hk_nation"
    hub_customer ||--o{ link_customer_nation : "hk_customer"
    hub_customer ||--o{ link_order_customer : "hk_customer"
    hub_supplier ||--o{ link_supplier_nation : "hk_supplier"
    hub_supplier ||--o{ link_part_supplier : "hk_supplier"
    hub_part ||--o{ link_part_supplier : "hk_part"
    hub_part ||--o{ link_lineitem : "hk_part"
    hub_order ||--o{ link_order_customer : "hk_order"
    hub_order ||--o{ link_lineitem : "hk_order"
    hub_lineitem ||--o{ link_lineitem : "hk_lineitem"

    hub_region ||--|| sat_region : "hk_region"
    hub_nation ||--|| sat_nation : "hk_nation"
    hub_customer ||--|| sat_customer : "hk_customer"
    hub_supplier ||--|| sat_supplier : "hk_supplier"
    hub_part ||--|| sat_part : "hk_part"
    hub_order ||--|| sat_order : "hk_order"
    link_part_supplier ||--|| sat_partsupp : "hk_part_supplier"
    link_lineitem ||--|| sat_lineitem : "hk_lineitem_link"

Loading Order

Tables must be loaded respecting referential integrity:

  1. Hubs (no dependencies): hub_region → hub_nation → hub_customer → hub_supplier → hub_part → hub_order → hub_lineitem
  2. Links (depend on Hubs): link_nation_region → link_customer_nation → link_supplier_nation → link_part_supplier → link_order_customer → link_lineitem
  3. Satellites (depend on Hubs/Links): sat_region → sat_nation → sat_customer → sat_supplier → sat_part → sat_partsupp → sat_order → sat_lineitem

Query Characteristics

All 22 TPC-H queries are adapted for the Data Vault schema using Hub→Satellite→Link join patterns. Each query filters for current records using load_end_dts IS NULL.

Query Complexity

CategoryQueriesPattern
SimpleQ1, Q6Single link + satellite, aggregation
MediumQ3, Q4, Q10, Q12, Q143-5 table joins
ComplexQ2, Q5, Q7, Q8, Q98+ table chains with subqueries
AdvancedQ15, Q17, Q20, Q21CTEs, correlated subqueries, EXISTS

Example Query (Q1 - Pricing Summary)

SELECT
sl.l_returnflag,
sl.l_linestatus,
SUM(sl.l_quantity) AS sum_qty,
SUM(sl.l_extendedprice) AS sum_base_price,
SUM(sl.l_extendedprice * (1 - sl.l_discount)) AS sum_disc_price,
AVG(sl.l_discount) AS avg_disc,
COUNT(*) AS count_order
FROM link_lineitem ll
JOIN sat_lineitem sl ON ll.hk_lineitem_link = sl.hk_lineitem_link
AND sl.load_end_dts IS NULL
WHERE sl.l_shipdate <= DATE '1998-12-01' - INTERVAL '90' DAY
GROUP BY sl.l_returnflag, sl.l_linestatus
ORDER BY sl.l_returnflag, sl.l_linestatus

Usage Examples

Basic Usage

from benchbox import DataVault
dv = DataVault(scale_factor=1.0, output_dir="datavault_data")
data_files = dv.generate_data()
print(f"Generated {len(data_files)} tables")
queries = dv.get_queries()
query_1 = dv.get_query(1)

generate_data() transforms TPC-H data into Data Vault tables.

DuckDB Integration

import duckdb
from benchbox import DataVault
dv = DataVault(scale_factor=0.1, output_dir="dv_test")
data_files = dv.generate_data()
conn = duckdb.connect("datavault.duckdb")
conn.execute(dv.get_create_tables_sql())
for table_name in dv.get_table_loading_order():
file_path = data_files.get(table_name)
if file_path:
conn.execute(f"""
INSERT INTO {table_name}
SELECT * FROM read_csv('{file_path}', delim='|', header=false)
""")
for query_id in range(1, 23):
result = conn.execute(dv.get_query(query_id)).fetchall()
print(f"Q{query_id}: {len(result)} rows")

The example generates the Data Vault data, creates the schema, loads the tables in loading order, and then runs all 22 queries.

CLI Usage

Terminal window
benchbox run --platform duckdb --benchmark datavault --scale 1.0 --phases generate
benchbox run --platform duckdb --benchmark datavault --scale 1.0 --phases power
benchbox run --dry-run ./preview --platform duckdb --benchmark datavault --scale 0.1

The commands generate data, run the power test, and do a dry run that previews the queries.

Configuration Options

CLI Options (--benchmark-option)

Configure Data Vault via --benchmark-option KEY=VALUE:

OptionDefaultDescription
hash_algorithmmd5Hash algorithm for hub/link keys. Only md5 is accepted by the benchmark runner (SUPPORTED_HASH_ALGORITHMS = ("md5",) in datavault/benchmark.py); the VARCHAR(32) hash-key column constrains the schema to 32-hex-char outputs. The lower-level datavault/etl/hash_functions.py helper also accepts sha1 and (truncated) sha256, but those are only reachable from library code, not the CLI.
record_sourceTPCHRecord source identifier for audit columns
force_regenerate-Force data regeneration (true/false)

Accepts hyphenated aliases (e.g. hash-algorithm, record-source).

Terminal window
benchbox run --platform duckdb --benchmark datavault --scale 1 \
--benchmark-option record_source=MY_SOURCE

This sets a custom record source. The hash algorithm stays md5, because the CLI rejects SHA-256.

Initialization Parameters

DataVault(
scale_factor=1.0,
output_dir="output",
parallel=4,
force_regenerate=False,
hash_algorithm="md5",
record_source="TPCH",
compress_data=False,
compression_type="gzip"
)
  • scale_factor: TPC-H scale factor (1.0 is about 1 GB of source data).
  • output_dir: directory for generated files.
  • parallel: parallel workers for TPC-H generation.
  • force_regenerate: regenerate even if data exists.
  • hash_algorithm: hash algorithm (only md5 is supported).
  • record_source: source identifier for audit columns.
  • compress_data: enable file compression.
  • compression_type: compression type (gzip or zstd).

TPC-H Source File Discovery

The Data Vault transformer (datavault/etl/transformer.py) reads the upstream TPC-H .tbl files and supports three on-disk layouts for each source table, probed in order:

  1. Compressed, sharded - customer.tbl.1.zst, customer.tbl.2.zst, … (also .gz). Preferred when present.
  2. Compressed, single file - customer.tbl.zst or customer.tbl.gz.
  3. Uncompressed, sharded - customer.tbl.1, customer.tbl.2, … Matched when the final dot-separated segment is numeric; returned as a glob pattern so DuckDB reads all shards.
  4. Uncompressed, single file - customer.tbl (fallback).

Shard numbering is a flat integer suffix (no zero padding); any non-numeric suffix is treated as a compression extension rather than a shard index.

Row-Count Validation API

benchbox.core.datavault.validate_row_counts returns a DataVaultValidationReport dataclass that summarises whether every generated Data Vault table matches its expected row count for the given TPC-H scale factor (datavault/validation.py).

from pathlib import Path
from benchbox.core.datavault import validate_row_counts
report = validate_row_counts(
data_dir=Path("datavault_data"),
scale_factor=1.0,
use_manifest=True,
tolerance_pct=1.0,
)
print(report)
print(report.is_valid)
print(report.tables_passed,
report.tables_failed,
report.tables_validated)
report.to_dict()

use_manifest=True prefers _datagen_manifest.json when it is present, and tolerance_pct=1.0 is the default variance allowance. print(report) gives a human-readable summary, and report.is_valid is True when every table is within tolerance. The three counts are populated by __post_init__. to_dict() returns a serializable form for JSON output.

Fields on DataVaultValidationReport:

FieldTypeDescription
scale_factorfloatTPC-H scale factor used for generation
resultslist[ValidationResult]Per-table actual vs expected counts and variance
tables_validatedintTotal tables inspected (derived)
tables_passedintTables within tolerance (derived)
tables_failedintTables outside tolerance (derived)
is_validboolTrue iff tables_failed == 0 (derived)

Lineitem-derived tables use a minimum tolerance of 1% to absorb the natural variance of TPC-H lineitem generation.

Scale Factor Guidelines

Scale FactorSource DataData Vault TablesUse Case
0.01~10 MB~15 MBDevelopment
0.1~100 MB~150 MBIntegration testing
1.0~1 GB~1.5 GBStandard benchmark
10~10 GB~15 GBPerformance testing

Performance Characteristics

Join Patterns

Data Vault queries typically involve more joins than equivalent TPC-H queries due to the Hub-Link-Satellite structure. A simple TPC-H 3-table join may become a 6-8 table join in Data Vault.

Performance Implications:

  • Hash join heavy - Most queries benefit from hash join optimization
  • Index candidates - Hash key columns (hk_*) are primary join keys
  • Filter pushdown - load_end_dts IS NULL filters should be pushed to satellites

Optimization Recommendations

  1. Index hash keys - Create indexes on all hk_* columns
  2. Partition satellites - Consider partitioning large satellites by load_dts
  3. Materialize current views - Pre-filter load_end_dts IS NULL for hot paths
  4. Statistics - Ensure optimizer has accurate cardinality estimates for links

Data Vault 2.0 Concepts

Hash Keys

All surrogate keys use MD5 hashes stored as VARCHAR(32):

  • Hub hash keys: MD5 of business key (e.g., MD5(c_custkey))
  • Link hash keys: MD5 of concatenated hub hash keys
  • HASHDIFF: MD5 of concatenated attribute values for change detection

Audit Columns

Every table includes:

  • load_dts (TIMESTAMP): When the record was loaded
  • record_source (VARCHAR): Source system identifier

Satellites additionally include:

  • load_end_dts (TIMESTAMP, nullable): End date for historical records (NULL = current)
  • hashdiff (VARCHAR(32)): Hash of attributes for change detection

Comparison with TPC-H

AspectTPC-HData Vault
Tables821
Schema Type3NFHub-Link-Satellite
Surrogate KeysNoneMD5 hash keys
Audit TrailNoneFull (load_dts, record_source)
Historical DataNoneSatellite versioning
Join ComplexityLowerHigher
Query Count2222 (adapted)

See Also