Skip to content

TPC-H Benchmark

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

Overview

TPC-H tests data warehouse performance using 22 analytical queries.

The queries simulate business intelligence workloads and test complex SQL operations on large datasets.

Key Features

  • 22 analytical queries
  • 8 table wholesale supplier schema
  • Parameterized queries for reproducible tests
  • Multiple scale factors (SF 1 = ~1GB)
  • OLAP operation coverage
  • SQL dialect support
  • Stream processing for throughput tests

Schema Description

TPC-H uses 8 tables modeling a wholesale supplier business:

Core Tables

TablePurposeApproximate Rows (SF 1)
REGIONGeographic regions5
NATIONCountries within regions25
SUPPLIERSupplier information10,000
CUSTOMERCustomer information150,000
PARTParts catalog200,000
PARTSUPPPart-supplier relationships800,000
ORDERSOrder header information1,500,000
LINEITEMOrder line items (fact table)6,000,000

Schema Relationships

erDiagram
    REGION ||--o{ NATION : contains
    NATION ||--o{ CUSTOMER : located_in
    NATION ||--o{ SUPPLIER : located_in
    CUSTOMER ||--o{ ORDERS : places
    SUPPLIER ||--o{ PARTSUPP : supplies
    PART ||--o{ PARTSUPP : supplied_as
    ORDERS ||--o{ LINEITEM : contains
    PART ||--o{ LINEITEM : ordered_as
    SUPPLIER ||--o{ LINEITEM : supplied_by

Key Columns

LINEITEM (Fact Table):

  • L_ORDERKEY, L_LINENUMBER (Composite Primary Key)
  • L_PARTKEY, L_SUPPKEY (Foreign Keys)
  • L_QUANTITY, L_EXTENDEDPRICE, L_DISCOUNT, L_TAX (Measures)
  • L_SHIPDATE, L_COMMITDATE, L_RECEIPTDATE (Temporal)

ORDERS (Order Header):

  • O_ORDERKEY (Primary Key)
  • O_CUSTKEY (Foreign Key to Customer)
  • O_ORDERDATE, O_TOTALPRICE, O_ORDERSTATUS

Query Characteristics

The 22 TPC-H queries test different aspects of query processing. Each query below links to its query template — a representative SQL and DataFrame rendering plus how to extract the exact statement for your platform and scale.

Query Categories

QueryTypeKey Features
Q1Pricing Summary ReportAggregation, GROUP BY, no joins
Q2Minimum Cost SupplierNested subqueries, correlated queries
Q3Shipping Priority3-table join, ORDER BY, LIMIT
Q4Order Priority CheckingEXISTS subquery, temporal predicates
Q5Local Supplier Volume6-table join, regional aggregation
Q6Forecasting RevenueSimple filter, aggregation
Q7Volume ShippingNation-to-nation shipping analysis
Q8National Market ShareComplex aggregation, CASE expressions
Q9Product Type ProfitMulti-year profit analysis
Q10Returned Item ReportingCustomer analysis with joins
Q11Important StockSubquery with aggregation
Q12Shipping ModesMail type analysis
Q13Customer DistributionOuter join, customer classification
Q14Promotion EffectRevenue analysis with CASE
Q15Top SupplierVIEW creation, supplier ranking
Q16Parts/Supplier RelationshipComplex filtering, NOT IN
Q17Small-Quantity-Order RevenueSubquery with aggregation
Q18Large Volume CustomerCustomer ranking by volume
Q19Discounted RevenueComplex OR conditions
Q20Potential Part PromotionMulti-level subqueries
Q21Suppliers Who Kept OrdersMultiple EXISTS/NOT EXISTS
Q22Global Sales OpportunityGeographic analysis

Complexity Patterns

  • Simple Queries (Q1, Q6): Single table aggregation
  • Medium Queries (Q3, Q5, Q10): Multi-table joins with aggregation
  • Complex Queries (Q2, Q15, Q20, Q21): Nested subqueries, correlated queries
  • Analytical Queries (Q7, Q8, Q9): Time-series and trend analysis

Official TPC-H Metrics

BenchBox exports two TPC-H metrics:

MetricMeaning
Power@SizeSingle-stream geometric-mean throughput over the 22 queries (power test)
Throughput@SizeMulti-stream throughput (queries executed per hour) over the wall-clock duration of the throughput test

BenchBox does not export QphH@Size. The specification defines Power@Size and the throughput test with the refresh functions RF1 and RF2, and the refresh stream runs concurrently with the query streams. BenchBox runs the maintenance phase after the throughput test and does not include RF1/RF2 in the power or throughput measurement, so a composite from these two values would overstate the specification metric. Adding the refresh functions to both tests is tracked as follow-up work.

Refresh Functions (RF1 / RF2)

The maintenance phase is implemented in benchbox/core/tpch/maintenance_test.py (TPCHMaintenanceTest):

  • RF1 - Insert new sales: generates new orders and lineitem rows with foreign-key validation before insertion.
  • RF2 - Delete old sales: deletes matching orders/lineitems to keep the database in steady state.

maintenance_pairs, rf1_interval, and rf2_interval on TPCHMaintenanceTestConfig control how many RF1/RF2 pairs run and at what cadence, per the TPC-H spec.

DataFrame Coverage

All 22 TPC-H queries have DataFrame equivalents in benchbox/core/tpch/dataframe_queries.py (one qN_expression_impl function per query, plus native-pandas variants for several queries). The DataFrame and SQL paths are validated against the same result set.

Usage Examples

Basic Query Generation

from benchbox import TPCH
tpch = TPCH(scale_factor=1.0, output_dir="tpch_data")
data_files = tpch.generate_data()
queries = tpch.get_queries()
print(f"Generated {len(queries)} queries")
query_1 = tpch.get_query(1, seed=42)
print(query_1)

Data Generation with Custom Scale

tpch_small = TPCH(scale_factor=0.1, output_dir="tpch_small")
data_files = tpch_small.generate_data()
for file_path in data_files:
table_name = file_path.stem
print(f"{table_name}: {file_path}")

SQL Dialect Translation

Queries are automatically translated to the requested dialect via sqlglot:

query_postgres = tpch.get_query(1, dialect="postgres")
query_duckdb = tpch.get_query(1, dialect="duckdb")
query_mysql = tpch.get_query(1, dialect="mysql")

DuckDB Integration Example

import duckdb
from benchbox import TPCH
tpch = TPCH(scale_factor=0.01, output_dir="tpch_tiny")
data_files = tpch.generate_data()
conn = duckdb.connect("tpch.duckdb")
schema_sql = tpch.get_create_tables_sql()
conn.execute(schema_sql)
for table_name in tpch.get_available_tables():
file_path = tpch.tables[table_name.upper()]
table_name_lower = table_name.lower()
conn.execute(f"""
INSERT INTO {table_name_upper}
SELECT * FROM read_csv('{file_path}',
delim='|',
header=false)
""")
for query_id in range(1, 23):
query_sql = tpch.get_query(query_id, dialect="duckdb")
result = conn.execute(query_sql).fetchall()
print(f"Q{query_id}: {len(result)} rows")

Stream Processing

stream_files = tpch.generate_streams(
num_streams=4,
rng_seed=42,
streams_output_dir="streams"
)
for i, stream_info in enumerate(tpch.get_all_streams_info()):
print(f"Stream {i}: {stream_info['query_count']} queries")
print(f" Permutation: {stream_info['permutation']}")
print(f" Output file: {stream_info['output_file']}")

DuckDB concurrency note: The TPC-H throughput phase shares a single DuckDB connection across streams and issues each stream’s queries through a per-stream cursor (_make_stream_cursor in benchbox/platforms/base/execution.py). DuckDB’s Python client is thread-safe only at the cursor level, not the connection level - see the DuckDB multi-thread guide. If you run BenchBox throughput tests from your own harness against DuckDB, follow the same pattern: one connection, one cursor per thread.

GIL / result-materialization note: for embedded engines (DuckDB, SQLite, DataFusion, …) that release the GIL during native query execution but not while converting result rows into Python objects, fetching a query’s full result set (cursor.fetchall()) re-acquires the GIL for the duration of that conversion. Every concurrent stream that materializes a result at the same time serializes on this step, so larger result sets and higher stream counts inflate execution_time_seconds (and therefore depress Throughput@Size) independently of the engine’s actual query-execution concurrency - an artifact of Python result handling, not of the platform under test. BenchBox’s throughput drivers (benchbox/core/tpch/throughput_test.py, benchbox/core/tpcds/throughput_test.py) avoid forcing this materialization to compute result_count: they read the platform adapter’s already-computed row count directly (benchbox.platforms.base.connection_wrappers.count_query_rows) instead of calling fetchall() a second time. If you build a custom harness that calls fetchall() per stream to inspect or log results, be aware that under this GIL constraint doing so measures Python object materialization throughput as much as it measures the database engine.

Query Characteristics

Query Complexity Categories

Simple Queries (low complexity):

  • Q1, Q6: Single-table aggregation with simple predicates
  • Q14: Simple join with CASE expression

Moderate Queries (medium complexity):

  • Q3, Q5, Q7, Q10, Q12: Multi-table joins with moderate complexity
  • Q4, Q13: EXISTS/outer join patterns

Complex Queries (high complexity):

  • Q2, Q15, Q17, Q20, Q21: Complex nested subqueries
  • Q8, Q9: Multi-year analytical queries
  • Q16, Q18, Q19, Q22: Complex filtering and ranking

Note: Actual execution times vary significantly based on platform, hardware, configuration, and data characteristics. Run benchmarks to measure performance for your specific environment.

Resource Utilization

Query TypeCPU UsageMemory UsageI/O Pattern
AggregationHighMediumSequential
Multi-joinMediumHighRandom
SubqueryHighHighMixed
AnalyticalMediumMediumSequential

Configuration Options

Scale Factor Guidelines

Scale FactorData SizeUse Case
0.01~10 MBDevelopment, unit testing
0.1~100 MBIntegration testing
1~1 GBStandard benchmark
10~10 GBPerformance testing
100+~100+ GBProduction simulation

Advanced Configuration

verbose=True enables detailed logging and parallel=4 runs data generation in parallel. In get_query(), seed makes parameters reproducible, scale_factor overrides the scale factor, and dialect sets the target dialect.

tpch = TPCH(
scale_factor=1.0,
output_dir="tpch_data",
verbose=True,
parallel=4
)
query = tpch.get_query(
query_id=1,
seed=42,
scale_factor=1.0,
dialect="duckdb"
)

Integration Examples

Databricks Integration

from benchbox import TPCH
tpch = TPCH(scale_factor=10, output_dir="/dbfs/tpch_sf10")
data_files = tpch.generate_data()
for table_name in tpch.get_available_tables():
spark.sql(f"""
CREATE TABLE IF NOT EXISTS {table_name}
USING DELTA
LOCATION '/dbfs/tpch_sf10/{table_name.lower()}.tbl'
OPTIONS (
'path' '/dbfs/tpch_sf10/{table_name.lower()}.tbl',
'delimiter' '|',
'header' 'false'
)
""")
for query_id in range(1, 23):
query_sql = tpch.get_query(query_id, dialect="spark")
df = spark.sql(query_sql)
df.show()

Performance Testing Framework

import time
from statistics import mean, median
def benchmark_queries(tpch, connection, query_list=None):
if query_list is None:
query_list = range(1, 23)
results = {}
for query_id in query_list:
times = []
for run in range(3):
query_sql = tpch.get_query(query_id, dialect="duckdb")
start_time = time.time()
result = connection.execute(query_sql).fetchall()
execution_time = time.time() - start_time
times.append(execution_time)
results[query_id] = {
'times': times,
'mean': mean(times),
'median': median(times),
'rows': len(result)
}
return results
results = benchmark_queries(tpch, conn)
for query_id, stats in results.items():
print(f"Q{query_id}: {stats['median']:.2f}s ({stats['rows']} rows)")

Best Practices

Data Generation

  1. Use appropriate scale factors for your testing needs
  2. Generate data once and reuse for multiple test runs
  3. Parallel generation for large scale factors (SF ≥ 10)
  4. Validate data integrity after generation

Query Execution

  1. Warm up queries before timing measurements
  2. Use consistent seeds for reproducible parameter generation
  3. Test incremental complexity (start with simple queries)
  4. Monitor resource usage during execution

Performance Testing

  1. Run multiple iterations to account for variance
  2. Clear caches between runs for cold performance
  3. Use representative scale factors for your use case
  4. Profile both power and throughput metrics

Common Issues and Solutions

Data Generation Issues

Issue: Out of memory during generation

Solution: use a smaller scale factor or increase the number of parallel processes.

tpch = TPCH(scale_factor=1.0, parallel=8)

Issue: Slow data generation

Solution: check disk I/O and use SSD storage.

tpch = TPCH(output_dir="/fast/ssd/path")

Query Execution Issues

Issue: Query timeout on large scale factors

Solution: start with a smaller scale factor and optimize queries.

tpch = TPCH(scale_factor=0.1)
query = tpch.get_query(21, dialect="duckdb")

Issue: Parameter substitution errors

Solution: use explicit seeds for reproducible parameters.

query = tpch.get_query(1, seed=42)

See Also

Understanding BenchBox

Practical Guides

Official TPC-H Resources

External Resources