Skip to content

Commit f4ed680

Browse files
bench(berlinmod): single canonical query file; SRID-consistent Points load
Replace the 17 per-query DuckDB-specific SQL files with a single berlinmod_r_queries.sql derived directly from the engine-agnostic berlinmod_r_queries_portable.sql (MobilityDB-BerlinMOD). The queries contain no engine-specific casts or prefilters; the identical SQL runs on PostgreSQL/MobilityDB, DuckDB/MobilityDuck, and Spark/MobilityDuck without modification. The run_queries.py runner parses the file by -- QN: markers and injects .mode csv/.output per query; explain mode wraps each query with EXPLAIN ANALYZE in-process, eliminating the need for separate explain files. Points.Geom is constructed via ST_Transform to EPSG:3857, consistent with the trips coordinate system. Expected row counts (BerlinMOD scalefactor 0.005, Brussels): Q1:72 Q2:1 Q3:6 Q4:80 Q5:100 Q6:0 Q7:26 Q8:75 Q9:94 Q10:21 Q11:0 Q12:0 Q13:278 Q14:1 Q15:118 Q16:6 Q17:1
1 parent e5d0464 commit f4ed680

37 files changed

Lines changed: 360 additions & 792 deletions

benchmark/BerlinMOD/run_queries.py

Lines changed: 124 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
#!/usr/bin/env python3
2-
"""
3-
Query runner for BerlinMOD benchmark
4-
"""
2+
"""Query runner for BerlinMOD benchmark."""
53

64
import subprocess
75
import time
@@ -10,176 +8,163 @@
108
import os
119
import argparse
1210
import re
13-
from typing import Dict, Tuple
11+
from typing import Dict, Optional, Tuple
1412

13+
QUERIES_FILE = "./sql/berlinmod_r_queries.sql"
1514
QUERIES_NUM = 17
1615

16+
17+
def _parse_queries(queries_file: str) -> Dict[int, str]:
18+
"""Split berlinmod_r_queries.sql into {query_num: sql} by -- QN: markers."""
19+
with open(queries_file, encoding="utf-8") as f:
20+
content = f.read()
21+
parts = re.split(r"(?=^-- Q\d+:)", content, flags=re.MULTILINE)
22+
queries: Dict[int, str] = {}
23+
for part in parts:
24+
m = re.match(r"-- Q(\d+):", part.strip())
25+
if m:
26+
n = int(m.group(1))
27+
sql = re.sub(r"^-- Q\d+:.*\n", "", part.strip(), count=1).strip()
28+
queries[n] = sql
29+
return queries
30+
31+
1732
class QueryRunner:
18-
def __init__(self, duckdb_path: str = "../../build/release/duckdb",
19-
benchmark: str = "default",
20-
queries_path: str = "./sql/queries",
21-
explain_path: str = "./sql/explain",
22-
output_path: str = "./results/output"):
33+
def __init__(
34+
self,
35+
duckdb_path: str = "../../build/release/duckdb",
36+
benchmark: str = "default",
37+
queries_file: str = QUERIES_FILE,
38+
output_path: str = "./results/output",
39+
):
2340
self.duckdb_path = duckdb_path
2441
self.benchmark = benchmark
25-
self.queries_path = queries_path
26-
self.explain_path = explain_path
42+
self.queries_file = queries_file
2743
self.output_path = output_path
2844
self.queries_num = QUERIES_NUM
45+
self._queries: Optional[Dict[int, str]] = None
46+
47+
def _get_queries(self) -> Dict[int, str]:
48+
if self._queries is None:
49+
self._queries = _parse_queries(self.queries_file)
50+
return self._queries
51+
52+
def _run_sql(self, sql: str) -> subprocess.CompletedProcess:
53+
return subprocess.run(
54+
[self.duckdb_path, f"./databases/{self.benchmark}.db"],
55+
input=sql,
56+
capture_output=True,
57+
text=True,
58+
)
59+
60+
def run_query(self, query_num: int) -> Tuple[float, int]:
61+
queries = self._get_queries()
62+
if query_num not in queries:
63+
print(f"\tError: Q{query_num} not found in {self.queries_file}")
64+
return -1, -1
65+
66+
print(f"\nRunning Q{query_num}")
67+
output_file = f"{self.output_path}/{self.benchmark}/query_{query_num}.csv"
68+
full_sql = f".mode csv\n.output {output_file}\n{queries[query_num]}"
2969

30-
def run_sql(self, filename: str) -> Tuple[float, int]:
31-
success = False
32-
print(f"\nRunning {filename}")
3370
start_time = time.time()
34-
sql_path = os.path.join(self.queries_path, filename)
35-
if not os.path.exists(sql_path):
36-
print(f"\tError: Query file not found: {sql_path}")
37-
return -1, -1
38-
39-
with open(sql_path, "r") as f:
40-
sql = f.read()
41-
42-
sql = sql.replace(".output results/output/query", f".output results/output/{self.benchmark}/query")
43-
44-
while not success:
45-
result = subprocess.run(
46-
[self.duckdb_path, f"./databases/{self.benchmark}.db"],
47-
input=sql,
48-
capture_output=True,
49-
text=True
50-
)
71+
while True:
72+
result = self._run_sql(full_sql)
5173
if result.returncode == 0:
52-
success = True
53-
else:
54-
print(f"\tError running query: {result.stderr}")
55-
print("\tTrying again...")
56-
time.sleep(1)
57-
start_time = time.time()
58-
59-
end_time = time.time()
60-
elapsed = (end_time - start_time) * 1000 # milliseconds
74+
break
75+
print(f"\tError: {result.stderr}")
76+
print("\tRetrying...")
77+
time.sleep(1)
78+
start_time = time.time()
6179

80+
elapsed = (time.time() - start_time) * 1000
6281
print(f"\tDone in {elapsed:.2f}ms")
6382

64-
line_count = self.run_validation(filename)
83+
line_count = self._count_output_rows(output_file)
6584
print(f"\tOutput row count: {line_count}")
66-
6785
return elapsed, line_count
6886

69-
def run_validation(self, filename: str) -> int:
70-
output_file = f"{self.output_path}/{self.benchmark}/{filename.replace('.sql', '.csv')}"
87+
def _count_output_rows(self, output_file: str) -> int:
7188
if not os.path.exists(output_file):
72-
print(f"\tError: Output file not found: {output_file}")
89+
print(f"\tError: output file not found: {output_file}")
7390
return -1
74-
with open(output_file, "r") as f:
75-
line_count = sum(1 for _ in f)
76-
if line_count > 0:
77-
line_count -= 1
78-
return line_count
91+
with open(output_file, encoding="utf-8") as f:
92+
count = sum(1 for _ in f)
93+
return max(0, count - 1)
7994

8095
def run_queries(self) -> Dict:
81-
results = dict()
82-
83-
for query_num in range(1, self.queries_num + 1):
84-
filename = f"query_{query_num}.sql"
85-
elapsed, line_count = self.run_sql(filename)
96+
results: Dict = {}
97+
for n in range(1, self.queries_num + 1):
98+
elapsed, line_count = self.run_query(n)
8699
if elapsed != -1:
87-
results[filename] = {
100+
results[f"query_{n}.sql"] = {
88101
"elapsed": elapsed,
89-
"row_count": line_count
102+
"row_count": line_count,
90103
}
91-
92104
return results
93105

94-
def run_explain_sql(self, filename: str) -> Tuple[float, int]:
95-
output_file = f"{self.output_path}/{self.benchmark}/explain/{filename.replace('.sql', '.txt')}"
96-
97-
success = False
98-
print(f"\nRunning {filename}")
99-
sql_path = os.path.join(self.explain_path, filename)
100-
if not os.path.exists(sql_path):
101-
print(f"\tError: Query file not found: {sql_path}")
102-
return -1, -1
103-
104-
with open(sql_path, "r") as f:
105-
sql = f.read()
106-
107-
sql = sql.replace(".output results/output/explain", f".output results/output/{self.benchmark}/explain")
108-
109-
while not success:
110-
result = subprocess.run(
111-
[self.duckdb_path, f"./databases/{self.benchmark}.db"],
112-
input=sql,
113-
capture_output=True,
114-
text=True
115-
)
116-
if result.returncode == 0:
117-
success = True
118-
else:
119-
print(f"\tError running query: {result.stderr}")
120-
print("\tTrying again...")
121-
time.sleep(1)
122-
start_time = time.time()
123-
124-
with open(output_file, "r") as f:
125-
output = f.readlines()
126-
127-
elapsed = -1.0
128-
for line in output:
129-
match = re.search(r"Total Time:\s*([0-9.]+)s", line)
130-
if match:
131-
try:
132-
elapsed = float(match.group(1)) * 1000 # convert to ms
133-
except ValueError:
134-
elapsed = -1.0
135-
break
136-
137-
print(f"\tDone in {elapsed:.2f}ms")
138-
139-
return elapsed
140-
141106
def run_explain(self) -> Dict:
142-
if not os.path.exists(f"./results/output/{self.benchmark}/explain"):
143-
os.makedirs(f"./results/output/{self.benchmark}/explain")
144-
145-
results = dict()
146-
for query_num in range(1, self.queries_num + 1):
147-
filename = f"query_{query_num}.sql"
148-
elapsed = self.run_explain_sql(filename)
149-
if elapsed != -1:
150-
results[filename] = elapsed
107+
queries = self._get_queries()
108+
explain_dir = f"{self.output_path}/{self.benchmark}/explain"
109+
os.makedirs(explain_dir, exist_ok=True)
110+
111+
results: Dict = {}
112+
for n in range(1, self.queries_num + 1):
113+
if n not in queries:
114+
continue
115+
output_file = f"{explain_dir}/query_{n}.txt"
116+
full_sql = (
117+
f".output {output_file}\n"
118+
f"SET memory_limit = '20GB';\n"
119+
f"EXPLAIN ANALYZE\n{queries[n]}"
120+
)
121+
print(f"\nRunning EXPLAIN Q{n}")
122+
result = self._run_sql(full_sql)
123+
if result.returncode != 0:
124+
print(f"\tError: {result.stderr}")
125+
continue
126+
127+
elapsed = -1.0
128+
if os.path.exists(output_file):
129+
with open(output_file, encoding="utf-8") as f:
130+
for line in f:
131+
m = re.search(r"Total Time:\s*([0-9.]+)s", line)
132+
if m:
133+
try:
134+
elapsed = float(m.group(1)) * 1000
135+
except ValueError:
136+
pass
137+
break
138+
139+
print(f"\tDone in {elapsed:.2f}ms")
140+
results[f"query_{n}.sql"] = elapsed
151141
return results
152142

153-
def main():
154-
parser = argparse.ArgumentParser(description="Data loader for BerlinMOD benchmark")
155-
parser.add_argument("--benchmark", type=str, required=True, help="Name of the benchmark run")
156-
parser.add_argument("--explain", type=int, default=0, choices=[0, 1], help="Run explain queries")
157-
benchmark = parser.parse_args().benchmark
158-
explain = parser.parse_args().explain
159143

160-
if not os.path.exists(f"./results/output/{benchmark}"):
161-
os.makedirs(f"./results/output/{benchmark}")
144+
def main():
145+
parser = argparse.ArgumentParser(description="BerlinMOD query runner for DuckDB")
146+
parser.add_argument("--benchmark", type=str, required=True)
147+
parser.add_argument("--explain", type=int, default=0, choices=[0, 1])
148+
args = parser.parse_args()
162149

163150
duckdb_path = "../../build/release/duckdb"
164151
if not os.path.exists(duckdb_path):
165-
print(f"Error: DuckDB executable not found at {duckdb_path}")
166-
print("Please make sure you're running this from the benchmark directory and DuckDB is built.")
152+
print(f"Error: DuckDB not found at {duckdb_path}")
167153
sys.exit(1)
168-
169-
runner = QueryRunner(duckdb_path, benchmark)
170-
if explain:
171-
results = runner.run_explain()
172-
else:
173-
results = runner.run_queries()
174-
175-
if not os.path.exists(f"./results/stats/{benchmark}"):
176-
os.makedirs(f"./results/stats/{benchmark}")
177-
178-
stats_filename = "run_queries.json" if not explain else "run_explain.json"
179-
with open(f"./results/stats/{benchmark}/{stats_filename}", "w") as f:
154+
155+
output_path = "./results/output"
156+
os.makedirs(f"{output_path}/{args.benchmark}", exist_ok=True)
157+
158+
runner = QueryRunner(duckdb_path, args.benchmark)
159+
results = runner.run_explain() if args.explain else runner.run_queries()
160+
161+
stats_dir = f"./results/stats/{args.benchmark}"
162+
os.makedirs(stats_dir, exist_ok=True)
163+
stats_file = "run_explain.json" if args.explain else "run_queries.json"
164+
with open(f"{stats_dir}/{stats_file}", "w", encoding="utf-8") as f:
180165
json.dump(results, f, indent=4)
181-
182-
print(f"\nResults saved to ./results/stats/{benchmark}/{stats_filename}")
166+
print(f"\nResults saved to {stats_dir}/{stats_file}")
167+
183168

184169
if __name__ == "__main__":
185-
main()
170+
main()

0 commit comments

Comments
 (0)