Skip to content

Commit 478938c

Browse files
authored
merge yard-export: memo alternatives priced and timed
bench: yard_export — memo alternatives priced and timed
2 parents 78ae3bb + e5a8856 commit 478938c

7 files changed

Lines changed: 3698 additions & 343 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ target_link_libraries(sql_optimizer_cost_model PRIVATE sql_engine)
6464
add_executable(sql_bench benchmarks/sql_bench.cpp)
6565
target_link_libraries(sql_bench PRIVATE sql_engine)
6666

67+
add_executable(yard_export benchmarks/yard_export.cpp)
68+
target_link_libraries(yard_export PRIVATE sql_engine)
69+
6770
if(SQL_ENGINE_BUILD_FUZZER)
6871
add_executable(sql_parser_planner_fuzz fuzz/parser_planner_fuzz.cpp)
6972
target_link_libraries(sql_parser_planner_fuzz PRIVATE sql_engine)

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ cmake -S . -B build && cmake --build build && ctest --test-dir build --output-on
1919
Linux CI runs the full test suite with address and undefined-behavior sanitizers.
2020
Continuous parser and planner fuzzing replays known queries before a timed fuzzing run.
2121

22+
`yard_export` writes `docs/yard/yard-routes.json`: every memo alternative for four workings, priced by the cost model and timed through vectorized execution. See the "Yard Routes" section of `docs/benchmarks.md`.
23+
2224
## Phase map
2325

2426
1. Parser, binder, logical algebra, and golden-query oracle.

‎benchmarks/bench_common.hpp‎

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
#pragma once
2+
3+
#include "execution/interpreter.hpp"
4+
#include "execution/vectorized.hpp"
5+
#include "optimizer/memo.hpp"
6+
#include "optimizer/rewrite.hpp"
7+
#include "sql/ast.hpp"
8+
#include "sql/binder.hpp"
9+
10+
#include <algorithm>
11+
#include <chrono>
12+
#include <cstdint>
13+
#include <iomanip>
14+
#include <sstream>
15+
#include <stdexcept>
16+
#include <string>
17+
#include <utility>
18+
#include <vector>
19+
20+
namespace bench {
21+
22+
constexpr std::size_t kFactRows = 200'000;
23+
constexpr std::size_t kJoinLeftRows = 100'000;
24+
constexpr std::size_t kJoinRightRows = 256;
25+
constexpr std::size_t kEndToEndLeftRows = 120'000;
26+
constexpr std::size_t kEndToEndRightRows = 256;
27+
constexpr std::size_t kClassRows = 128;
28+
constexpr std::size_t kStringRows = 120'000;
29+
constexpr std::size_t kRepetitions = 5;
30+
31+
inline std::uint64_t benchmark_sink = 0;
32+
33+
struct SplitMix64 {
34+
std::uint64_t state;
35+
36+
explicit SplitMix64(std::uint64_t seed) : state(seed) {}
37+
38+
std::uint64_t next() {
39+
std::uint64_t z = (state += 0x9e3779b97f4a7c15ULL);
40+
z = (z ^ (z >> 30U)) * 0xbf58476d1ce4e5b9ULL;
41+
z = (z ^ (z >> 27U)) * 0x94d049bb133111ebULL;
42+
return z ^ (z >> 31U);
43+
}
44+
};
45+
46+
struct ResultSignature {
47+
std::size_t row_count{0};
48+
std::size_t column_count{0};
49+
std::uint64_t checksum{0};
50+
};
51+
52+
struct Timings {
53+
double min_ms{0.0};
54+
double median_ms{0.0};
55+
};
56+
57+
inline void mix_byte(std::uint64_t& hash, std::uint8_t byte) {
58+
hash ^= byte;
59+
hash *= 1099511628211ULL;
60+
}
61+
62+
inline void mix_u64(std::uint64_t& hash, std::uint64_t value) {
63+
for (std::size_t i = 0; i < 8; ++i) {
64+
mix_byte(hash, static_cast<std::uint8_t>((value >> (i * 8U)) & 0xffU));
65+
}
66+
}
67+
68+
inline void mix_string(std::uint64_t& hash, const std::string& value) {
69+
mix_u64(hash, value.size());
70+
for (unsigned char ch : value) {
71+
mix_byte(hash, ch);
72+
}
73+
}
74+
75+
inline ResultSignature signature_for(const storage::ColumnarBatch& batch) {
76+
std::uint64_t hash = 1469598103934665603ULL;
77+
mix_u64(hash, batch.row_count());
78+
mix_u64(hash, batch.column_names().size());
79+
for (const auto& name : batch.column_names()) {
80+
mix_string(hash, name);
81+
}
82+
for (std::size_t row = 0; row < batch.row_count(); ++row) {
83+
for (const auto& name : batch.column_names()) {
84+
if (batch.column_type(name) == catalog::ColumnType::Int64) {
85+
const auto& column = batch.column(name);
86+
mix_byte(hash, column.is_null(row) ? 0 : 1);
87+
if (!column.is_null(row)) {
88+
mix_u64(hash, static_cast<std::uint64_t>(column.at(row)));
89+
}
90+
} else {
91+
const auto& column = batch.string_column(name);
92+
mix_byte(hash, column.is_null(row) ? 0 : 1);
93+
if (!column.is_null(row)) {
94+
mix_string(hash, column.at(row));
95+
}
96+
}
97+
}
98+
}
99+
return ResultSignature{batch.row_count(), batch.column_names().size(), hash};
100+
}
101+
102+
inline bool operator==(const ResultSignature& left, const ResultSignature& right) {
103+
return left.row_count == right.row_count && left.column_count == right.column_count &&
104+
left.checksum == right.checksum;
105+
}
106+
107+
inline std::string hex_checksum(std::uint64_t value) {
108+
std::ostringstream out;
109+
out << "0x" << std::hex << std::setw(16) << std::setfill('0') << value;
110+
return out.str();
111+
}
112+
113+
inline void add_column(storage::ColumnarBatch& batch,
114+
std::string name,
115+
std::vector<std::int64_t> values) {
116+
storage::Int64Column column;
117+
for (auto value : values) {
118+
column.append(value);
119+
}
120+
batch.add_column(std::move(name), std::move(column));
121+
}
122+
123+
inline void add_string_column(storage::ColumnarBatch& batch,
124+
std::string name,
125+
std::vector<std::string> values) {
126+
storage::StringColumn column;
127+
for (auto& value : values) {
128+
column.append(std::move(value));
129+
}
130+
batch.add_column(std::move(name), std::move(column));
131+
}
132+
133+
inline storage::ColumnarBatch make_fact_table() {
134+
SplitMix64 rng(0x5eed000000000001ULL);
135+
std::vector<std::int64_t> a;
136+
std::vector<std::int64_t> bucket;
137+
std::vector<std::int64_t> decile;
138+
std::vector<std::int64_t> half;
139+
std::vector<std::int64_t> group_few;
140+
std::vector<std::int64_t> group_many;
141+
std::vector<std::int64_t> sort_key;
142+
std::vector<std::int64_t> value;
143+
a.reserve(kFactRows);
144+
bucket.reserve(kFactRows);
145+
decile.reserve(kFactRows);
146+
half.reserve(kFactRows);
147+
group_few.reserve(kFactRows);
148+
group_many.reserve(kFactRows);
149+
sort_key.reserve(kFactRows);
150+
value.reserve(kFactRows);
151+
152+
for (std::size_t row = 0; row < kFactRows; ++row) {
153+
a.push_back(static_cast<std::int64_t>(row));
154+
bucket.push_back(static_cast<std::int64_t>(row % 100));
155+
decile.push_back(static_cast<std::int64_t>(row % 10));
156+
half.push_back(static_cast<std::int64_t>(row % 2));
157+
group_few.push_back(static_cast<std::int64_t>(row % 8));
158+
group_many.push_back(static_cast<std::int64_t>(row % 50'000));
159+
sort_key.push_back(static_cast<std::int64_t>(rng.next() % kFactRows));
160+
value.push_back(static_cast<std::int64_t>(rng.next() % 1'000));
161+
}
162+
163+
storage::ColumnarBatch batch;
164+
add_column(batch, "a", std::move(a));
165+
add_column(batch, "bucket", std::move(bucket));
166+
add_column(batch, "decile", std::move(decile));
167+
add_column(batch, "half", std::move(half));
168+
add_column(batch, "group_few", std::move(group_few));
169+
add_column(batch, "group_many", std::move(group_many));
170+
add_column(batch, "sort_key", std::move(sort_key));
171+
add_column(batch, "value", std::move(value));
172+
return batch;
173+
}
174+
175+
inline storage::ColumnarBatch make_join_left_table() {
176+
std::vector<std::int64_t> k1;
177+
std::vector<std::int64_t> k2;
178+
std::vector<std::int64_t> payload;
179+
k1.reserve(kJoinLeftRows);
180+
k2.reserve(kJoinLeftRows);
181+
payload.reserve(kJoinLeftRows);
182+
for (std::size_t row = 0; row < kJoinLeftRows; ++row) {
183+
k1.push_back(static_cast<std::int64_t>(row % 16));
184+
k2.push_back(static_cast<std::int64_t>((row / 16) % 16));
185+
payload.push_back(static_cast<std::int64_t>((row * 17) % 1'000'003));
186+
}
187+
188+
storage::ColumnarBatch batch;
189+
add_column(batch, "k1", std::move(k1));
190+
add_column(batch, "k2", std::move(k2));
191+
add_column(batch, "payload", std::move(payload));
192+
return batch;
193+
}
194+
195+
inline storage::ColumnarBatch make_join_right_table() {
196+
std::vector<std::int64_t> k1;
197+
std::vector<std::int64_t> k2;
198+
std::vector<std::int64_t> payload;
199+
k1.reserve(kJoinRightRows);
200+
k2.reserve(kJoinRightRows);
201+
payload.reserve(kJoinRightRows);
202+
for (std::size_t row = 0; row < kJoinRightRows; ++row) {
203+
k1.push_back(static_cast<std::int64_t>(row % 16));
204+
k2.push_back(static_cast<std::int64_t>(row / 16));
205+
payload.push_back(static_cast<std::int64_t>(10'000 + row));
206+
}
207+
208+
storage::ColumnarBatch batch;
209+
add_column(batch, "k1", std::move(k1));
210+
add_column(batch, "k2", std::move(k2));
211+
add_column(batch, "payload", std::move(payload));
212+
return batch;
213+
}
214+
215+
inline storage::ColumnarBatch make_e2e_left_table() {
216+
std::vector<std::int64_t> k;
217+
std::vector<std::int64_t> group_id;
218+
std::vector<std::int64_t> filter_key;
219+
k.reserve(kEndToEndLeftRows);
220+
group_id.reserve(kEndToEndLeftRows);
221+
filter_key.reserve(kEndToEndLeftRows);
222+
for (std::size_t row = 0; row < kEndToEndLeftRows; ++row) {
223+
k.push_back(static_cast<std::int64_t>(row % kEndToEndRightRows));
224+
group_id.push_back(static_cast<std::int64_t>((row / 3) % 128));
225+
filter_key.push_back(static_cast<std::int64_t>(row % 100));
226+
}
227+
228+
storage::ColumnarBatch batch;
229+
add_column(batch, "k", std::move(k));
230+
add_column(batch, "group_id", std::move(group_id));
231+
add_column(batch, "filter_key", std::move(filter_key));
232+
return batch;
233+
}
234+
235+
inline storage::ColumnarBatch make_e2e_right_table() {
236+
std::vector<std::int64_t> k;
237+
std::vector<std::int64_t> measure;
238+
k.reserve(kEndToEndRightRows);
239+
measure.reserve(kEndToEndRightRows);
240+
for (std::size_t row = 0; row < kEndToEndRightRows; ++row) {
241+
k.push_back(static_cast<std::int64_t>(row));
242+
measure.push_back(static_cast<std::int64_t>(1 + (row % 97)));
243+
}
244+
245+
storage::ColumnarBatch batch;
246+
add_column(batch, "k", std::move(k));
247+
add_column(batch, "measure", std::move(measure));
248+
return batch;
249+
}
250+
251+
inline storage::ColumnarBatch make_e2e_class_table() {
252+
std::vector<std::int64_t> group_id;
253+
std::vector<std::int64_t> tier;
254+
group_id.reserve(kClassRows);
255+
tier.reserve(kClassRows);
256+
for (std::size_t row = 0; row < kClassRows; ++row) {
257+
group_id.push_back(static_cast<std::int64_t>(row));
258+
tier.push_back(static_cast<std::int64_t>(row % 4));
259+
}
260+
storage::ColumnarBatch batch;
261+
add_column(batch, "group_id", std::move(group_id));
262+
add_column(batch, "tier", std::move(tier));
263+
return batch;
264+
}
265+
266+
inline storage::ColumnarBatch make_string_fact_table() {
267+
static const std::vector<std::string> key_pool{
268+
"",
269+
"alpha",
270+
"beta",
271+
"gamma",
272+
"delta",
273+
"key000",
274+
"key001",
275+
"key002",
276+
"key003",
277+
"key004",
278+
"key005",
279+
"key006",
280+
"key007",
281+
"key008",
282+
"key009",
283+
"omega",
284+
};
285+
286+
std::vector<std::string> k;
287+
std::vector<std::string> label;
288+
k.reserve(kStringRows);
289+
label.reserve(kStringRows);
290+
for (std::size_t row = 0; row < kStringRows; ++row) {
291+
k.push_back(key_pool[row % key_pool.size()]);
292+
label.push_back("label" + std::to_string((row * 17) % 4096));
293+
}
294+
295+
storage::ColumnarBatch batch;
296+
add_string_column(batch, "k", std::move(k));
297+
add_string_column(batch, "label", std::move(label));
298+
return batch;
299+
}
300+
301+
inline execution::Catalog make_catalog() {
302+
execution::Catalog catalog;
303+
catalog.add_table("fact", make_fact_table());
304+
catalog.add_table("join_left", make_join_left_table());
305+
catalog.add_table("join_right", make_join_right_table());
306+
catalog.add_table("e2e_left", make_e2e_left_table());
307+
catalog.add_table("e2e_right", make_e2e_right_table());
308+
catalog.add_table("e2e_class", make_e2e_class_table());
309+
catalog.add_table("string_fact", make_string_fact_table());
310+
return catalog;
311+
}
312+
313+
inline plan::LogicalPlan bind_query(const execution::Catalog& catalog, const std::string& sql) {
314+
return sql::bind_select(sql::parse_select(sql), catalog);
315+
}
316+
317+
inline plan::LogicalPlan bind_decorrelated_semi_query(const execution::Catalog& catalog,
318+
const std::string& sql) {
319+
const auto logical = bind_query(catalog, sql);
320+
optimizer::Memo memo;
321+
const auto root = memo.insert(logical);
322+
const auto explored = optimizer::explore_memo_to_fixpoint(memo, optimizer::default_memo_rules());
323+
if (!explored.reached_fixpoint) {
324+
throw std::logic_error("benchmark semi-join memo exploration did not reach fixpoint");
325+
}
326+
const auto alternatives =
327+
memo.extract_alternatives(root, optimizer::AlternativeExtractionOptions{128, 1024});
328+
for (const auto& alternative : alternatives.plans) {
329+
if (plan::to_string(alternative).find("SemiJoin[") != std::string::npos) {
330+
return alternative;
331+
}
332+
}
333+
throw std::logic_error("benchmark IN query did not produce a SemiJoin alternative");
334+
}
335+
336+
inline plan::LogicalPlan bind_best_null_aware_anti_query(const execution::Catalog& catalog,
337+
const std::string& sql) {
338+
const auto logical = bind_query(catalog, sql);
339+
optimizer::Memo memo;
340+
const auto root = memo.insert(logical);
341+
const auto explored = optimizer::explore_memo_to_fixpoint(memo, optimizer::default_memo_rules());
342+
if (!explored.reached_fixpoint) {
343+
throw std::logic_error("benchmark NULL-aware anti memo exploration did not reach fixpoint");
344+
}
345+
const auto best = memo.extract_best(root, catalog);
346+
if (plan::to_string(best).find("NullAwareAntiJoin[") == std::string::npos) {
347+
throw std::logic_error("benchmark NOT IN query did not choose its NullAwareAnti alternative");
348+
}
349+
return best;
350+
}
351+
352+
using ExecuteFn = storage::ColumnarBatch (*)(const plan::LogicalPlan&, const execution::Catalog&);
353+
354+
inline Timings measure(const plan::LogicalPlan& plan,
355+
const execution::Catalog& catalog,
356+
const std::string& engine_name,
357+
ExecuteFn execute) {
358+
std::vector<double> durations;
359+
durations.reserve(kRepetitions);
360+
for (std::size_t repetition = 0; repetition < kRepetitions; ++repetition) {
361+
const auto start = std::chrono::steady_clock::now();
362+
auto result = execute(plan, catalog);
363+
const auto end = std::chrono::steady_clock::now();
364+
const auto signature = signature_for(result);
365+
benchmark_sink ^= signature.checksum + signature.row_count + signature.column_count + repetition;
366+
durations.push_back(std::chrono::duration<double, std::milli>(end - start).count());
367+
}
368+
369+
if (durations.size() != kRepetitions) {
370+
throw std::logic_error(engine_name + " timing loop did not run all repetitions");
371+
}
372+
std::sort(durations.begin(), durations.end());
373+
return Timings{durations.front(), durations.at(durations.size() / 2)};
374+
}
375+
376+
} // namespace bench

0 commit comments

Comments
 (0)