Skip to content

Read Primitives Benchmark

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

Query templates: every Read Primitives query

Overview

The Read Primitives benchmark provides focused testing of fundamental database operations without the complexity of business logic. It offers systematic testing of specific database capabilities through 157 primitive SQL queries organized into 31 operation categories.

The benchmark uses the familiar TPC-H schema for data generation while focusing on isolated testing of individual database features. This approach enables developers to identify performance regressions, validate database engine optimizations, and perform focused hardware and configuration comparisons.

Origins and Attribution

The Read Primitives benchmark combines queries from multiple established sources with BenchBox extensions.

Primary Sources

1. Apache Impala targeted-perf Workload

The majority of queries derive from Apache Impala’s targeted-perf workload, a comprehensive suite of performance-focused SQL queries designed to test fundamental database operations in isolation.

The targeted-perf workload provides systematic testing of:

  • Aggregation operations (simple, grouped, materialized)
  • Broadcast and shuffle joins
  • Filter predicates (selective, non-selective, conjunct ordering)
  • Exchange operations (broadcast, shuffle)
  • Sorting and ordering operations
  • String operations and pattern matching

2. Optimizer Sniff Tests

The 13 optimizer_* queries are based on Justin Jaffray’s optimizer sniff test concepts, which provide focused tests for common query optimizer patterns.

These queries test optimizer capabilities including:

  • Subquery decorrelation (EXISTS to semijoin)
  • Redundant operation elimination (DISTINCT elimination)
  • Common subexpression elimination
  • Predicate pushdown through joins
  • Join reordering based on cardinality
  • Limit pushdown optimization
  • Scalar subquery flattening
  • Constant folding and expression simplification
  • Column pruning (projection pushdown)
  • Subquery transformation (IN to EXISTS)
  • Set operation optimization
  • Runtime filter generation

See Optimizer Tests Documentation for detailed information.

BenchBox Extensions

BenchBox extends these foundations with modern SQL capabilities:

  • Window functions (ROW_NUMBER, RANK, LAG, LEAD, aggregate windows)
  • OLAP operations (CUBE, ROLLUP, GROUPING SETS)
  • Statistical functions (PERCENTILE_CONT, VARIANCE, STDDEV, CORR)
  • JSON operations (extraction, aggregation, path queries)
  • Full-text search (MATCH AGAINST with Boolean mode)
  • Time series analysis (trend calculation, month-over-month growth)
  • QUALIFY clause (filtering on window function results)
  • MIN_BY/MAX_BY aggregate functions

These extensions reflect modern SQL:2016+ features and real-world analytical workload patterns.

Attribution

  • Impala-derived queries: Copyright © Apache Software Foundation, Apache License 2.0
  • Optimizer test queries: Based on concepts by Justin Jaffray
  • BenchBox extensions and implementation: Copyright 2026 Joe Harris / BenchBox Project, MIT License

Key Features

  • 157 comprehensive primitive queries covering fundamental database operations
  • 31 operation categories for systematic testing (aggregation, joins, filters, window functions, etc.)
  • Query categorization system with metadata-driven organization
  • Optimized data generation leveraging existing TPC-H infrastructure
  • Smart performance testing with resource-aware optimization capabilities
  • Enhanced developer experience with intelligent query discovery
  • Extensible architecture supporting custom primitive operations
  • Multiple scale factors for different testing scenarios (micro to xlarge)

Value Proposition

The Read Primitives benchmark provides:

  • Isolated testing of specific database operations without business logic complexity
  • Performance regression detection for individual database capabilities
  • Development validation for database engine optimization features
  • Focused benchmarking for hardware and configuration comparison
  • Unit testing capabilities for database-specific features
  • Optimizer sniff tests for query planner validation

Query Categories

The 157 primitive queries are organized into 31 categories, each targeting specific database operations:

Core Operation Categories

CategoryPurposeExample Operations
aggregationBasic aggregation functionsSUM, COUNT, AVG, MIN, MAX
broadcast_joinSmall-to-large table joinsDimension to fact table joins
filterPredicate evaluationWHERE clauses with various selectivity
group_byGrouping operationsSingle and multi-column grouping
joinGeneral join operationsInner, outer, cross joins
limitResult set limitingTOP-N queries
order_bySorting operationsSingle and multi-column sorting
subqueryNested queriesCorrelated and uncorrelated subqueries
unionSet operationsUNION, UNION ALL
window_functionWindow/analytical functionsROW_NUMBER, RANK, LAG, LEAD

Advanced Operation Categories

CategoryPurposeExample Operations
case_whenConditional logicCASE expressions
castType conversionsData type casting
coalesceNULL handlingCOALESCE, NULLIF
common_table_expressionCTEsWITH clauses
cross_joinCartesian productsCross join patterns
date_functionsTemporal operationsDate arithmetic, extraction
distinctDeduplicationSELECT DISTINCT
existsExistence checksEXISTS/NOT EXISTS subqueries
in_listSet membershipIN clause with lists
likePattern matchingLIKE operations
null_handlingNULL operationsIS NULL, IS NOT NULL

Specialized Categories

CategoryPurposeExample Operations
optimizerOptimizer stress testsEquivalent query variations
partition_byPartitioned analyticsWindow functions with PARTITION BY
self_joinTable self-joinsHierarchical queries
string_functionsString operationsCONCAT, SUBSTRING, LENGTH
type_conversionAdvanced castingComplex type transformations

Schema Description

The Read Primitives benchmark reuses the TPC-H schema for data generation, providing familiar and well-understood data patterns. This allows developers to focus on query performance without learning a new schema.

TPC-H Tables Used

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

Data Generation

The benchmark leverages the existing TPC-H data generation infrastructure:

  • No additional data files required beyond TPC-H
  • Consistent data patterns across all primitive queries
  • Referential integrity maintained automatically
  • Multiple scale factors supported for different testing needs

Usage Examples

Basic Query Execution

from benchbox import ReadPrimitives
primitives = ReadPrimitives(scale_factor=0.01, output_dir="primitives_data")
data_files = primitives.generate_data()
queries = primitives.get_queries()
print(f"Available: {len(queries)} primitive queries")
agg_query = primitives.get_query("aggregation_sum_basic")
print(agg_query)

Category-Based Testing

aggregation_queries = primitives.get_queries_by_category("aggregation")
join_queries = primitives.get_queries_by_category("join")
window_queries = primitives.get_queries_by_category("window_function")
print(f"Aggregation tests: {len(aggregation_queries)}")
print(f"Join tests: {len(join_queries)}")
print(f"Window function tests: {len(window_queries)}")
for query_id, query_sql in aggregation_queries.items():
result = conn.execute(query_sql).fetchall()
print(f"{query_id}: {len(result)} rows")

DuckDB Integration Example

import duckdb
from benchbox import ReadPrimitives
import time
primitives = ReadPrimitives(scale_factor=0.01, output_dir="primitives_tiny")
data_files = primitives.generate_data()
conn = duckdb.connect("primitives.duckdb")
schema_sql = primitives.get_create_tables_sql()
conn.execute(schema_sql)
for table_name in primitives.get_available_tables():
file_path = primitives.tables[table_name.upper()]
conn.execute(f"""
INSERT INTO {table_name}
SELECT * FROM read_csv('{file_path}',
delim='|',
header=false)
""")
categories = ["aggregation", "filter", "join", "group_by", "window_function"]
results = {}
for category in categories:
print(f"\nTesting {category} primitives...")
category_queries = primitives.get_queries_by_category(category)
for query_id, query_sql in category_queries.items():
start_time = time.time()
result = conn.execute(query_sql).fetchall()
execution_time = time.time() - start_time
results[query_id] = {
'category': category,
'time': execution_time,
'rows': len(result)
}
print(f" {query_id}: {execution_time:.3f}s ({len(result)} rows)")
conn.close()

Regression Testing Framework

import time
from statistics import mean, median
from typing import Dict, List
class PrimitivesRegressionTester:
def __init__(self, primitives: ReadPrimitives, connection):
self.primitives = primitives
self.connection = connection
def run_regression_suite(self, categories: List[str] = None) -> Dict:
if categories is None:
categories = ["aggregation", "filter", "join", "group_by"]
results = {}
for category in categories:
print(f"Running {category} regression tests...")
category_queries = self.primitives.get_queries_by_category(category)
for query_id, query_sql in category_queries.items():
times = []
for _ in range(3):
start_time = time.time()
result = self.connection.execute(query_sql).fetchall()
execution_time = time.time() - start_time
times.append(execution_time)
results[query_id] = {
'category': category,
'avg_time': mean(times),
'median_time': median(times),
'min_time': min(times),
'times': times,
'rows': len(result)
}
return results
def compare_with_baseline(self, current_results: Dict, baseline_results: Dict,
threshold: float = 0.1) -> List[Dict]:
regressions = []
for query_id, current in current_results.items():
if query_id not in baseline_results:
continue
baseline = baseline_results[query_id]
time_increase = (current['median_time'] - baseline['median_time']) / baseline['median_time']
if time_increase > threshold:
regressions.append({
'query_id': query_id,
'category': current['category'],
'baseline_time': baseline['median_time'],
'current_time': current['median_time'],
'increase_pct': time_increase * 100
})
return regressions
tester = PrimitivesRegressionTester(primitives, conn)
current_results = tester.run_regression_suite()
regressions = tester.compare_with_baseline(current_results, baseline_results)
if regressions:
print("\n⚠️ Performance Regressions Detected:")
for reg in regressions:
print(f" {reg['query_id']}: {reg['increase_pct']:.1f}% slower")
else:
print("\n✅ No regressions detected")

The framework runs each query three times for stable measurements. compare_with_baseline flags a regression when a query is slower than the baseline by more than threshold (0.1 means 10%).

Performance Profiling

from typing import Dict
import time
def profile_primitive_categories(primitives: ReadPrimitives, connection) -> Dict:
categories = primitives.get_all_categories()
profile_results = {}
for category in categories:
category_queries = primitives.get_queries_by_category(category)
times = []
for query_id, query_sql in category_queries.items():
start_time = time.time()
result = connection.execute(query_sql).fetchall()
execution_time = time.time() - start_time
times.append(execution_time)
if times:
profile_results[category] = {
'query_count': len(category_queries),
'total_time': sum(times),
'avg_time': sum(times) / len(times),
'min_time': min(times),
'max_time': max(times)
}
sorted_categories = sorted(
profile_results.items(),
key=lambda x: x[1]['total_time'],
reverse=True
)
print("\nPerformance Profile by Category:")
print(f"{'Category':<30} {'Queries':<10} {'Total Time':<15} {'Avg Time':<15}")
print("-" * 70)
for category, stats in sorted_categories:
print(f"{category:<30} {stats['query_count']:<10} "
f"{stats['total_time']:<15.3f} {stats['avg_time']:<15.3f}")
return dict(sorted_categories)
profile = profile_primitive_categories(primitives, conn)

The categories are sorted by total time to identify bottlenecks.

Performance Characteristics

Query Execution Patterns

Fast Primitives (< 10ms on SF 0.01):

  • Simple filters: Single-column predicates
  • Basic aggregations: COUNT, SUM without grouping
  • Limit queries: TOP-N without sorting

Medium Primitives (10-100ms on SF 0.01):

  • Join operations: 2-3 table joins
  • Group by operations: Single-column grouping
  • Window functions: Basic ROW_NUMBER, RANK

Slower Primitives (> 100ms on SF 0.01):

  • Complex joins: Multi-table joins with filters
  • Multi-column grouping: High cardinality groups
  • Advanced window functions: LAG, LEAD with partitioning
  • String operations: LIKE, regex patterns

Scale Factor Guidelines

Scale FactorData SizeQuery TimesUse Case
0.001 (micro)~1 MBMillisecondsUnit tests
0.01 (small)~10 MB10-100msCI/CD pipelines
0.1 (medium)~100 MB100ms-1sRegression testing
1.0 (large)~1 GB1-10sPerformance validation
10.0 (xlarge)~10 GB10s-60sStress testing

Configuration Options

Basic Configuration

primitives = ReadPrimitives(scale_factor=0.001, output_dir="primitives_dev")
primitives = ReadPrimitives(scale_factor=0.01, output_dir="primitives_ci")
primitives = ReadPrimitives(scale_factor=1.0, output_dir="primitives_prod")

The three settings are for development (fast iteration), CI/CD (balanced performance), and production validation (realistic scale).

Advanced Configuration

primitives = ReadPrimitives(
scale_factor=0.1,
output_dir="primitives_data",
verbose=True,
parallel=4,
cache_data=True
)
query_postgres = primitives.get_query("aggregation_sum_basic", dialect="postgres")
query_duckdb = primitives.get_query("aggregation_sum_basic", dialect="duckdb")
query_mysql = primitives.get_query("aggregation_sum_basic", dialect="mysql")

verbose=True enables detailed logging, parallel=4 generates data in parallel, and cache_data=True caches the generated data. The get_query calls translate the query to each target dialect.

Best Practices

Data Generation

  1. Reuse TPC-H data - Generate once, use for all primitive queries
  2. Use appropriate scale factors - Start small for development
  3. Cache generated data - Avoid regeneration overhead
  4. Validate data integrity - Ensure referential integrity

Query Execution

  1. Category-based testing - Focus on relevant primitive categories
  2. Multiple iterations - Run queries multiple times for stable timings
  3. Clear caches - Between runs for cold performance testing
  4. Monitor resources - Track CPU, memory, I/O usage

Regression Testing

  1. Establish baselines - Record initial performance metrics
  2. Automated comparison - Use regression testing frameworks
  3. Threshold-based alerts - Define acceptable performance variance
  4. Category-level analysis - Identify performance patterns by category

Common Issues and Solutions

Data Generation Issues

Issue: Data generation too slow

primitives = ReadPrimitives(scale_factor=0.01, parallel=8)

Solution: use a smaller scale factor or parallel generation.

Issue: Out of disk space

primitives = ReadPrimitives(scale_factor=0.001)

Solution: use the micro scale factor (about 1MB) or clean up old data.

Query Execution Issues

Issue: Queries timeout on large scale

primitives = ReadPrimitives(scale_factor=0.01)

Solution: start with a smaller scale factor for fast execution.

Issue: Inconsistent performance measurements

times = []
for _ in range(5):
start = time.time()
result = conn.execute(query_sql).fetchall()
times.append(time.time() - start)
median_time = sorted(times)[len(times) // 2]

Solution: run multiple iterations and use the median.

DataFrame Support

The Read Primitives benchmark provides full DataFrame support for both expression-family and pandas-family platforms, enabling native DataFrame API benchmarking without SQL.

Platform Support Matrix

PlatformFamilyStatusImplementation Count
PolarsExpressionFull support149 queries
PySparkExpressionFull support149 queries
DataFusionExpressionFull support149 queries
PandasPandasFull support149 queries
DaskPandasFull support149 queries
cuDFPandasFull support149 queries

Note: Only 3 queries are skipped for DataFrame platforms (correlated subqueries that have no DataFrame equivalent): optimizer_exists_to_semijoin, optimizer_in_to_exists, optimizer_scalar_subquery_flattening.

DataFrame Execution

Run Read Primitives on DataFrame platforms using the CLI:

Terminal window
benchbox run --platform polars-df --benchmark read-primitives --scale 0.01
benchbox run --platform pandas-df --benchmark read-primitives --scale 0.01
benchbox run --platform pyspark-df --benchmark read-primitives --scale 0.01

The three commands run on Polars, Pandas, and PySpark DataFrames, in that order.

Programmatic DataFrame Usage

from benchbox import ReadPrimitives
from benchbox.core.read_primitives.dataframe_queries import (
aggregation_distinct_expression_impl,
aggregation_distinct_pandas_impl,
)
from benchbox.platforms import get_dataframe_adapter
primitives = ReadPrimitives(scale_factor=0.01)
primitives.generate_data()
polars_adapter = get_dataframe_adapter("polars-df", working_dir=str(primitives.output_dir))
ctx = polars_adapter.create_context()
polars_adapter.load_tables_from_data_source(ctx, primitives.output_dir)
result = aggregation_distinct_expression_impl(ctx)
print(result.collect())
pandas_adapter = get_dataframe_adapter("pandas-df", working_dir=str(primitives.output_dir))
pandas_ctx = pandas_adapter.create_context()
pandas_adapter.load_tables_from_data_source(pandas_ctx, primitives.output_dir)
result = aggregation_distinct_pandas_impl(pandas_ctx)
print(result)

create_context() returns an empty context, and working_dir does not register files. Load the generated tables into the context with load_tables_from_data_source. The first half runs an expression-family query on Polars, and the second half does the same for a Pandas-family query.

Expression Family Example

Expression-family queries use ctx.col() and ctx.lit() for declarative, lazy evaluation:

def aggregation_groupby_large_expression_impl(ctx):
lineitem = ctx.get_table("lineitem")
col = ctx.col
result = lineitem.group_by("l_orderkey", "l_partkey", "l_suppkey").agg(
col("l_quantity").sum().alias("total_qty"),
col("l_extendedprice").mean().alias("avg_price"),
)
return result

Pandas Family Example

Pandas-family queries use string column access and boolean indexing:

def filter_selective_pandas_impl(ctx):
lineitem = ctx.get_table("lineitem")
result = lineitem[lineitem["l_returnflag"] == "R"]
return result

The filter uses Pandas-style boolean indexing.

DataFrame Query Categories

All 26 SQL query categories have DataFrame implementations:

CategoryExpression FamilyPandas FamilyNotes
aggregation✅✅COUNT, SUM, AVG, COUNT DISTINCT
filter✅✅WHERE clauses, predicate pushdown
groupby✅✅GROUP BY, ROLLUP, CUBE
orderby✅✅Sorting, TOP-N
window✅✅ROW_NUMBER, RANK, LAG/LEAD
qualify✅✅Window filtering (via two-step pattern)
broadcast✅✅Small table broadcast joins
string✅✅LIKE, CONCAT, SUBSTRING
optimizer⚠️⚠️3 correlated subqueries skipped

See Also

Understanding BenchBox

Practical Guides