.. tags:: reference, python-api, duckdb
The DuckDB adapter provides fast, embedded analytical database execution for benchmarks.
DuckDB is included by default with BenchBox, providing:
- No additional configuration required - Works without additional setup
- Columnar query engine - Optimized for analytical queries
- In-memory or persistent - Flexible storage options
- ANSI SQL support - Comprehensive analytical SQL features
Common use cases:
- Development and testing
- CI/CD pipelines
- Small to medium datasets (< 100GB)
- Local benchmarking without cloud infrastructure
Basic usage:
from benchbox.tpch import TPCH
from benchbox.platforms.duckdb import DuckDBAdapter
# In-memory database (default)
adapter = DuckDBAdapter()
# Or persistent database
adapter = DuckDBAdapter(database_path="benchmark.duckdb")
# Run benchmark
benchmark = TPCH(scale_factor=0.1)
results = benchmark.run_with_platform(adapter).. autoclass:: benchbox.platforms.duckdb.DuckDBAdapter :members: :undoc-members: :show-inheritance:
DuckDBAdapter(
database_path: Optional[str] = ":memory:",
memory_limit: Optional[str] = None,
thread_limit: Optional[int] = None,
temp_directory: Optional[str] = None,
enable_profiling: bool = False,
read_only: bool = False,
config: Optional[Dict[str, Any]] = None
)Parameters:
- database_path (str, optional): Database file path or ":memory:" for in-memory. Default: ":memory:"
- memory_limit (str, optional): Maximum memory usage (e.g., "4GB", "512MB"). Default: No limit
- thread_limit (int, optional): Maximum number of threads. Default: All available cores
- temp_directory (str, optional): Directory for temporary files. Default: System temp
- enable_profiling (bool): Enable query profiling. Default: False
- read_only (bool): Open database in read-only mode. Default: False
- config (dict, optional): Additional DuckDB configuration options
Suitable for small datasets and rapid iteration:
from benchbox.platforms.duckdb import DuckDBAdapter
# Default in-memory configuration
adapter = DuckDBAdapter()
# With memory limit
adapter = DuckDBAdapter(memory_limit="2GB")
# With thread control
adapter = DuckDBAdapter(
memory_limit="4GB",
thread_limit=4
)For reusable benchmark data:
# Create persistent database
adapter = DuckDBAdapter(database_path="./benchmarks/tpch.duckdb")
# Run benchmark (data persists)
benchmark = TPCH(scale_factor=1.0)
results = benchmark.run_with_platform(adapter)
# Later: reuse the same database
adapter2 = DuckDBAdapter(database_path="./benchmarks/tpch.duckdb")
results2 = benchmark.run_with_platform(adapter2)Configure for optimal performance:
adapter = DuckDBAdapter(
database_path="benchmark.duckdb",
memory_limit="16GB", # Set appropriate for your system
thread_limit=8, # Match your CPU cores
temp_directory="/fast/ssd/temp", # Use fast storage
config={
"default_order": "DESC",
"preserve_insertion_order": False,
"enable_object_cache": True
}
)Enable query profiling for analysis:
adapter = DuckDBAdapter(
enable_profiling=True,
config={
"enable_profiling": "json",
"profiling_output": "./profiles"
}
)
# Run benchmark
results = benchmark.run_with_platform(adapter)
# Profile information saved to ./profiles/The adapter handles data loading automatically, but you can customize the process:
import duckdb
from benchbox.platforms.duckdb import DuckDBAdapter
# Create adapter with custom connection
adapter = DuckDBAdapter(database_path="benchmark.duckdb")
# Access underlying DuckDB connection
conn = adapter.connection
# Custom bulk load from Parquet
conn.execute("""
CREATE TABLE lineitem AS
SELECT * FROM read_parquet('data/lineitem/*.parquet')
""")# DuckDB automatically detects CSV format
conn.execute("""
CREATE TABLE customer AS
SELECT * FROM read_csv('data/customer.tbl',
delim='|',
header=false,
columns={
'c_custkey': 'INTEGER',
'c_name': 'VARCHAR',
'c_address': 'VARCHAR',
'c_nationkey': 'INTEGER',
'c_phone': 'VARCHAR',
'c_acctbal': 'DECIMAL(15,2)',
'c_mktsegment': 'VARCHAR',
'c_comment': 'VARCHAR'
})
""")from benchbox.platforms.duckdb import DuckDBAdapter
adapter = DuckDBAdapter()
# Execute arbitrary SQL
result = adapter.connection.execute("SELECT COUNT(*) FROM lineitem")
row_count = result.fetchone()[0]
# Execute with parameters
query = "SELECT * FROM orders WHERE o_orderdate > ?"
result = adapter.connection.execute(query, ["1995-01-01"])# Get query plan
explain_result = adapter.connection.execute(
"EXPLAIN SELECT * FROM lineitem WHERE l_shipdate > '1995-01-01'"
)
print(explain_result.fetchall())
# Analyze query with profiling
adapter.connection.execute("PRAGMA enable_profiling")
result = adapter.connection.execute("SELECT COUNT(*) FROM lineitem")
profiling_info = adapter.connection.execute("PRAGMA profiling_output").fetchall()# DuckDB automatically parallelizes queries
adapter = DuckDBAdapter(
memory_limit="16GB",
thread_limit=8 # Use 8 threads for parallel execution
)
# Complex aggregation will use all threads
results = benchmark.run_with_platform(adapter)import duckdb
adapter = DuckDBAdapter()
conn = adapter.connection
# Load DuckDB extensions
conn.execute("INSTALL httpfs")
conn.execute("LOAD httpfs")
# Now can read from S3
conn.execute("""
CREATE TABLE data AS
SELECT * FROM read_parquet('s3://bucket/data/*.parquet')
""")# DuckDB supports advanced window functions
query = """
SELECT
l_orderkey,
l_partkey,
l_extendedprice,
ROW_NUMBER() OVER (PARTITION BY l_orderkey ORDER BY l_extendedprice DESC) as rn
FROM lineitem
WHERE l_shipdate > '1995-01-01'
"""
result = adapter.connection.execute(query)Set memory limits to prevent OOM errors:
adapter = DuckDBAdapter(memory_limit="8GB")
Use persistent databases for large datasets:
adapter = DuckDBAdapter(database_path="large_dataset.duckdb")
Monitor memory usage during execution:
import psutil process = psutil.Process() print(f"Memory usage: {process.memory_info().rss / 1024 / 1024:.0f} MB")
Match thread count to CPU cores:
import os adapter = DuckDBAdapter(thread_limit=os.cpu_count())
Use appropriate data types in schema:
# Prefer HUGEINT over VARCHAR for large integers # Use DATE/TIMESTAMP instead of VARCHAR for dates
Create indexes for filtered columns:
conn.execute("CREATE INDEX idx_shipdate ON lineitem(l_shipdate)")
Verify row counts after loading:
expected_rows = 6_000_000 # SF=1 TPC-H actual_rows = conn.execute("SELECT COUNT(*) FROM lineitem").fetchone()[0] assert actual_rows == expected_rows, f"Expected {expected_rows}, got {actual_rows}"
Check data types:
schema = conn.execute("PRAGMA table_info('lineitem')").fetchall() for column in schema: print(f"{column[1]}: {column[2]}")
Problem: Query fails with out of memory error
Solution:
# Set explicit memory limit
adapter = DuckDBAdapter(memory_limit="4GB")
# Or use persistent database with disk spilling
adapter = DuckDBAdapter(
database_path="benchmark.duckdb",
memory_limit="4GB",
temp_directory="/large/disk/temp"
)Problem: Queries execute slowly
Solutions:
# 1. Increase thread count
adapter = DuckDBAdapter(thread_limit=8)
# 2. Use persistent database to avoid repeated loads
adapter = DuckDBAdapter(database_path="cached.duckdb")
# 3. Enable profiling to identify bottlenecks
adapter = DuckDBAdapter(enable_profiling=True)Problem: "Database is locked" error
Solution:
# Use separate database files for concurrent access
adapter1 = DuckDBAdapter(database_path="benchmark1.duckdb")
adapter2 = DuckDBAdapter(database_path="benchmark2.duckdb")
# Or use in-memory for read-only workloads
adapter = DuckDBAdapter(database_path=":memory:")- :doc:`/platforms/platform-selection-guide` - Choosing DuckDB vs other platforms
- :doc:`/platforms/quick-reference` - Quick setup for all platforms
- :doc:`/platforms/comparison-matrix` - Feature comparison
- :doc:`/benchmarks/tpc-h` - TPC-H on DuckDB
- :doc:`/benchmarks/tpc-ds` - TPC-DS on DuckDB
- :doc:`/benchmarks/clickbench` - ClickBench on DuckDB
- :doc:`../base` - Base benchmark interface
- :doc:`index` - Python API overview
- :doc:`/usage/api-reference` - High-level API guide
- DuckDB Documentation - Official DuckDB docs
- DuckDB Performance Guide - Performance tuning
- DuckDB Extensions - Available extensions