import argparse
import gc
import os
import random
import sys
import tempfile
import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'apexbase', 'python'))
try:
from apexbase import ApexClient
HAS_APEX = True
except ImportError:
HAS_APEX = False
print("ERROR: ApexBase not found")
import pandas as pd
try:
import duckdb
HAS_DUCKDB = True
except ImportError:
HAS_DUCKDB = False
try:
import sqlite3
HAS_SQLITE = True
except ImportError:
HAS_SQLITE = False
@dataclass
class BenchConfig:
rows: int = 1_000_000 iterations: int = 10 threads: int = 8 warmup: int = 2 batch_size: int = 25_000
config = BenchConfig()
CITIES = ["Beijing", "Shanghai", "Guangzhou", "Shenzhen", "Hangzhou"]
CATEGORIES = ["Electronics", "Clothing", "Food", "Sports", "Books"]
def generate_data(n: int) -> List[Dict]:
rng = random.Random(42)
data = []
for i in range(n):
data.append({
'_id': i + 1,
'user_id': rng.randint(1, 50000),
'category': rng.choice(CATEGORIES),
'city': rng.choice(CITIES),
'price': round(rng.uniform(10.0, 1000.0), 2),
'quantity': rng.randint(1, 100),
'is_active': rng.choice([True, False]),
'score': rng.uniform(0.0, 100.0)
})
return data
def data_to_columns(data: List[Dict]) -> Dict:
if not data:
return {}
columns = {}
for key in data[0].keys():
columns[key] = [record[key] for record in data]
return columns
QUERIES_SIMPLE = [
"SELECT COUNT(*) FROM test_table",
"SELECT SUM(price) FROM test_table",
"SELECT AVG(score) FROM test_table",
]
QUERIES_MEDIUM = [
"SELECT city, COUNT(*) FROM test_table GROUP BY city",
"SELECT category, AVG(price) FROM test_table GROUP BY category",
"SELECT city, category, SUM(quantity) FROM test_table GROUP BY city, category",
]
QUERIES_COMPLEX = [
"SELECT city, COUNT(*) as cnt, AVG(price) as avg_price FROM test_table GROUP BY city ORDER BY cnt DESC",
"SELECT category, COUNT(*) as total, AVG(score) as avg_score FROM test_table WHERE price > 100 GROUP BY category",
"SELECT * FROM test_table WHERE price > 500 ORDER BY price DESC LIMIT 100",
]
class BaseBenchmark:
def __init__(self, tmpdir: str, data: List[Dict]):
self.tmpdir = tmpdir
self.data = data
self.n = len(data)
def setup(self):
raise NotImplementedError
def execute(self, query: str) -> float:
raise NotImplementedError
def close(self):
pass
def name(self) -> str:
return self.__class__.__name__
class ApexBaseBench(BaseBenchmark):
def __init__(self, tmpdir: str, data: List[Dict]):
super().__init__(tmpdir, data)
self.client = None
def setup(self):
self.client = ApexClient(self.tmpdir)
self.client.create_table('test_table')
batch_size = config.batch_size
for i in range(0, len(self.data), batch_size):
batch = self.data[i:i+batch_size]
columns = data_to_columns(batch)
self.client.store(columns)
for _ in range(config.warmup):
self.client.execute("SELECT COUNT(*) FROM test_table").to_pandas()
def execute(self, query: str) -> float:
t0 = time.perf_counter()
result = self.client.execute(query)
result.to_pandas() return time.perf_counter() - t0
def close(self):
if self.client:
self.client.close()
class DuckDBBench(BaseBenchmark):
def __init__(self, tmpdir: str, data: List[Dict]):
super().__init__(tmpdir, data)
self.conn = None
def setup(self):
db_path = os.path.join(self.tmpdir, "duckdb.db")
self.conn = duckdb.connect(db_path)
self.conn.execute("""
CREATE TABLE test_table (
_id INTEGER,
user_id INTEGER,
category VARCHAR,
city VARCHAR,
price DOUBLE,
quantity INTEGER,
is_active BOOLEAN,
score DOUBLE
)
""")
df = pd.DataFrame(self.data)
self.conn.execute("INSERT INTO test_table SELECT * FROM df")
for _ in range(config.warmup):
self.conn.execute("SELECT COUNT(*) FROM test_table").fetchall()
def execute(self, query: str) -> float:
t0 = time.perf_counter()
result = self.conn.execute(query).fetchall()
return time.perf_counter() - t0
def close(self):
if self.conn:
self.conn.close()
class SQLiteBench(BaseBenchmark):
def __init__(self, tmpdir: str, data: List[Dict]):
super().__init__(tmpdir, data)
self.conn = None
def setup(self):
db_path = os.path.join(self.tmpdir, "sqlite.db")
self.conn = sqlite3.connect(db_path)
self.conn.execute("PRAGMA journal_mode=WAL")
self.conn.execute("PRAGMA synchronous=NORMAL")
self.conn.execute("""
CREATE TABLE test_table (
_id INTEGER PRIMARY KEY,
user_id INTEGER,
category TEXT,
city TEXT,
price REAL,
quantity INTEGER,
is_active INTEGER,
score REAL
)
""")
for record in self.data:
self.conn.execute("""
INSERT INTO test_table VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (record['_id'], record['user_id'], record['category'],
record['city'], record['price'], record['quantity'],
1 if record['is_active'] else 0, record['score']))
self.conn.commit()
for _ in range(config.warmup):
self.conn.execute("SELECT COUNT(*) FROM test_table").fetchall()
def execute(self, query: str) -> float:
query = query.replace('is_active = true', 'is_active = 1')
query = query.replace('is_active = false', 'is_active = 0')
t0 = time.perf_counter()
result = self.conn.execute(query).fetchall()
return time.perf_counter() - t0
def close(self):
if self.conn:
self.conn.close()
def run_single_query_throughput(bench: BaseBenchmark, queries: List[str]) -> Dict:
print(f"\n {bench.name()}: 单线程顺序查询吞吐量测试")
for q in queries[:1]:
bench.execute(q)
total_queries = len(queries) * config.iterations
gc.collect()
t0 = time.perf_counter()
for _ in range(config.iterations):
for query in queries:
bench.execute(query)
elapsed = time.perf_counter() - t0
qps = total_queries / elapsed
print(f" 总查询数: {total_queries}, 耗时: {elapsed:.3f}s, Q/s: {qps:.1f}")
return {
'total_queries': total_queries,
'elapsed_sec': elapsed,
'qps': qps
}
def run_concurrent_throughput(bench_class, tmpdir: str, data: List[Dict],
queries: List[str], num_threads: int) -> Dict:
print(f"\n 并发查询吞吐量测试 ({num_threads} 线程)")
results_lock = threading.Lock()
total_executed = [0]
total_time = [0.0]
errors = [0]
def worker(thread_id: int):
try:
thread_dir = os.path.join(tmpdir, f'thread_{thread_id}')
os.makedirs(thread_dir, exist_ok=True)
if bench_class == ApexBaseBench:
worker_bench = ApexBaseBench(thread_dir, data)
elif bench_class == DuckDBBench:
worker_bench = DuckDBBench(thread_dir, data)
elif bench_class == SQLiteBench:
worker_bench = SQLiteBench(thread_dir, data)
else:
return
worker_bench.setup()
queries_per_thread = config.iterations * len(queries)
thread_start = time.perf_counter()
for _ in range(config.iterations):
for query in queries:
exec_time = worker_bench.execute(query)
with results_lock:
total_executed[0] += 1
total_time[0] += exec_time
thread_elapsed = time.perf_counter() - thread_start
worker_bench.close()
return {
'thread_id': thread_id,
'queries': total_executed[0],
'elapsed': thread_elapsed
}
except Exception as e:
with results_lock:
errors[0] += 1
return {'thread_id': thread_id, 'error': str(e)}
warmup_bench = bench_class(tmpdir, data)
warmup_bench.setup()
for q in queries[:1]:
warmup_bench.execute(q)
warmup_bench.close()
gc.collect()
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [executor.submit(worker, i) for i in range(num_threads)]
worker_results = [f.result() for f in as_completed(futures)]
wall_clock_time = max([r.get('elapsed', 0) for r in worker_results if 'elapsed' in r])
if wall_clock_time > 0:
total_queries = num_threads * config.iterations * len(queries)
concurrent_qps = total_queries / wall_clock_time
else:
concurrent_qps = 0
print(f" 总查询数: {total_queries}, 墙钟时间: {wall_clock_time:.3f}s, 并发Q/s: {concurrent_qps:.1f}")
return {
'threads': num_threads,
'total_queries': total_queries,
'wall_clock_sec': wall_clock_time,
'concurrent_qps': concurrent_qps,
'errors': errors[0]
}
def run_concurrent_sequential(bench_class, tmpdir: str, data: List[Dict],
queries: List[str], num_threads: int) -> Dict:
print(f"\n 并发顺序测试 ({num_threads} 线程, 每线程 {config.iterations} 轮)")
results = []
def worker(thread_id: int):
try:
thread_dir = os.path.join(tmpdir, f'thread_{thread_id}')
os.makedirs(thread_dir, exist_ok=True)
if bench_class == ApexBaseBench:
bench = ApexBaseBench(thread_dir, data)
elif bench_class == DuckDBBench:
bench = DuckDBBench(thread_dir, data)
elif bench_class == SQLiteBench:
bench = SQLiteBench(thread_dir, data)
else:
return None
bench.setup()
query_times = []
for _ in range(config.iterations):
for query in queries:
t0 = time.perf_counter()
bench.execute(query)
query_times.append(time.perf_counter() - t0)
bench.close()
return {
'thread_id': thread_id,
'queries': len(query_times) * config.iterations,
'times': query_times
}
except Exception as e:
return {'thread_id': thread_id, 'error': str(e)}
gc.collect()
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [executor.submit(worker, i) for i in range(num_threads)]
worker_results = [f.result() for f in as_completed(futures)]
wall_clock = time.perf_counter() - t0
total_queries = sum([r.get('queries', 0) for r in worker_results if 'queries' in r])
all_times = []
for r in worker_results:
if 'times' in r:
all_times.extend(r['times'])
avg_latency = sum(all_times) / len(all_times) if all_times else 0
concurrent_qps = total_queries / wall_clock if wall_clock > 0 else 0
print(f" 总查询数: {total_queries}")
print(f" 墙钟时间: {wall_clock:.3f}s")
print(f" 平均延迟: {avg_latency*1000:.2f}ms")
print(f" 并发Q/s: {concurrent_qps:.1f}")
return {
'threads': num_threads,
'total_queries': total_queries,
'wall_clock_sec': wall_clock,
'avg_latency_ms': avg_latency * 1000,
'concurrent_qps': concurrent_qps
}
def main():
parser = argparse.ArgumentParser(description='ApexBase Q/s Benchmark')
parser.add_argument('--rows', type=int, default=config.rows,
help=f'数据行数 (default: {config.rows})')
parser.add_argument('--iterations', type=int, default=config.iterations,
help=f'查询迭代次数 (default: {config.iterations})')
parser.add_argument('--threads', type=int, default=config.threads,
help=f'并发线程数 (default: {config.threads})')
parser.add_argument('--databases', nargs='+', default=['apex', 'duckdb', 'sqlite'],
help='要测试的数据库')
args = parser.parse_args()
config.rows = args.rows
config.iterations = args.iterations
config.threads = args.threads
print("=" * 60)
print("ApexBase 查询吞吐量 Benchmark")
print("=" * 60)
print(f"数据行数: {config.rows:,}")
print(f"查询迭代: {config.iterations}")
print(f"并发线程: {config.threads}")
print("\n生成测试数据...")
data = generate_data(config.rows)
print(f" 生成 {len(data):,} 条记录")
all_queries = QUERIES_SIMPLE + QUERIES_MEDIUM + QUERIES_COMPLEX
results = {}
print("\n" + "=" * 60)
print("1. 单线程顺序查询吞吐量测试")
print("=" * 60)
for db_name in args.databases:
tmpdir = tempfile.mkdtemp()
try:
if db_name == 'apex' and HAS_APEX:
bench = ApexBaseBench(tmpdir, data)
elif db_name == 'duckdb' and HAS_DUCKDB:
bench = DuckDBBench(tmpdir, data)
elif db_name == 'sqlite' and HAS_SQLITE:
bench = SQLiteBench(tmpdir, data)
else:
continue
print(f"\n>>> {db_name.upper()}")
bench.setup()
r_simple = run_single_query_throughput(bench, QUERIES_SIMPLE)
r_medium = run_single_query_throughput(bench, QUERIES_MEDIUM)
r_complex = run_single_query_throughput(bench, QUERIES_COMPLEX)
r_all = run_single_query_throughput(bench, all_queries)
results[f'{db_name}_single'] = {
'simple': r_simple,
'medium': r_medium,
'complex': r_complex,
'all': r_all
}
bench.close()
finally:
import shutil
shutil.rmtree(tmpdir, ignore_errors=True)
print("\n" + "=" * 60)
print("2. 并发查询吞吐量测试")
print("=" * 60)
for db_name in args.databases:
tmpdir = tempfile.mkdtemp()
try:
if db_name == 'apex' and HAS_APEX:
bench_class = ApexBaseBench
elif db_name == 'duckdb' and HAS_DUCKDB:
bench_class = DuckDBBench
elif db_name == 'sqlite' and HAS_SQLITE:
bench_class = SQLiteBench
else:
continue
print(f"\n>>> {db_name.upper()}")
thread_counts = [1, 2, 4, 8]
for num_threads in thread_counts:
if num_threads > config.threads:
break
r = run_concurrent_sequential(bench_class, tmpdir, data,
QUERIES_SIMPLE, num_threads)
if f'{db_name}_concurrent' not in results:
results[f'{db_name}_concurrent'] = {}
results[f'{db_name}_concurrent'][num_threads] = r
finally:
import shutil
shutil.rmtree(tmpdir, ignore_errors=True)
print("\n" + "=" * 60)
print("汇总结果")
print("=" * 60)
print("\n--- 单线程顺序 Q/s ---")
for db in ['apex', 'duckdb', 'sqlite']:
key = f'{db}_single'
if key in results:
r = results[key]['all']
print(f"{db:10s}: {r['qps']:>8.1f} Q/s (总查询: {r['total_queries']}, 耗时: {r['elapsed_sec']:.3f}s)")
print("\n--- 并发 Q/s (随线程数变化) ---")
for db in ['apex', 'duckdb', 'sqlite']:
key = f'{db}_concurrent'
if key in results:
print(f"\n{db.upper()}:")
for threads, r in sorted(results[key].items()):
print(f" {threads} 线程: {r['concurrent_qps']:>8.1f} Q/s (延迟: {r['avg_latency_ms']:.2f}ms)")
if __name__ == '__main__':
main()