Skip to content

Commit 7ade921

Browse files
committed
feat: parallel scatter, composite keys, LIST prune, plan-cache redistribute
Flatten UNION ALL remotes so N shards open together. Route composite HASH keys, fail unknown tables, and move rows on shard-key UPDATE. LIST misses fail closed and BETWEEN/IN prune; composite colocated joins push when every key part is equated. Cache the logical plan and re-distribute on each hit.
1 parent 9e264da commit 7ade921

11 files changed

Lines changed: 1198 additions & 213 deletions

include/sql_engine/distributed_planner.h

Lines changed: 515 additions & 123 deletions
Large diffs are not rendered by default.

include/sql_engine/operators/set_op_op.h

Lines changed: 51 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,86 +17,82 @@ class SetOpOperator : public Operator {
1717
public:
1818
SetOpOperator(Operator* left, Operator* right, uint8_t op, bool all,
1919
bool parallel_open = false, ThreadPool* pool = nullptr)
20-
: left_(left), right_(right), op_(op), all_(all),
20+
: op_(op), all_(all), parallel_open_(parallel_open), pool_(pool) {
21+
children_.push_back(left);
22+
children_.push_back(right);
23+
}
24+
25+
explicit SetOpOperator(std::vector<Operator*> children,
26+
bool parallel_open = false, ThreadPool* pool = nullptr)
27+
: children_(std::move(children)), op_(SET_OP_UNION), all_(true),
2128
parallel_open_(parallel_open), pool_(pool) {}
2229

2330
void open() override {
24-
if (parallel_open_ && pool_) {
25-
// Thread-pool parallel open: ~1-2us dispatch vs ~200us for std::async
26-
auto fl = pool_->submit([this]{ left_->open(); });
27-
auto fr = pool_->submit([this]{ right_->open(); });
28-
fl.get();
29-
fr.get();
30-
} else if (parallel_open_) {
31-
// Fallback: std::async when no pool available
32-
auto fl = std::async(std::launch::async, [this]{ left_->open(); });
33-
auto fr = std::async(std::launch::async, [this]{ right_->open(); });
34-
fl.get();
35-
fr.get();
31+
if (children_.empty()) return;
32+
if (parallel_open_ && children_.size() > 1) {
33+
std::vector<std::future<void>> futures;
34+
futures.reserve(children_.size());
35+
for (size_t i = 0; i < children_.size(); ++i) {
36+
auto launcher = [this, i]{ children_[i]->open(); };
37+
if (pool_) {
38+
futures.push_back(pool_->submit(std::move(launcher)));
39+
} else {
40+
futures.push_back(std::async(std::launch::async, std::move(launcher)));
41+
}
42+
}
43+
for (auto& f : futures) f.get();
3644
} else {
37-
left_->open();
38-
right_->open();
45+
for (auto* c : children_) c->open();
3946
}
40-
reading_left_ = true;
47+
child_idx_ = 0;
4148
seen_.clear();
4249
expected_col_count_ = -1;
4350

44-
if (op_ == SET_OP_INTERSECT || op_ == SET_OP_EXCEPT) {
45-
// Materialize right side into a set
51+
if ((op_ == SET_OP_INTERSECT || op_ == SET_OP_EXCEPT) && children_.size() >= 2) {
4652
right_set_.clear();
4753
Row r{};
48-
while (right_->next(r)) {
54+
while (children_[1]->next(r)) {
4955
check_col_count(r);
5056
check_operator_row_limit(right_set_.size(), kDefaultMaxOperatorRows, "SetOpOperator");
5157
right_set_.insert(row_key(r));
5258
}
53-
right_->close();
59+
children_[1]->close();
60+
right_closed_ = true;
5461
}
5562
}
5663

5764
bool next(Row& out) override {
65+
if (children_.empty()) return false;
66+
5867
if (op_ == SET_OP_UNION && !all_) {
59-
// UNION (deduplicated)
60-
while (true) {
61-
bool got = false;
62-
if (reading_left_) {
63-
got = left_->next(out);
64-
if (!got) { reading_left_ = false; }
65-
}
66-
if (!reading_left_) {
67-
got = right_->next(out);
68-
if (!got) return false;
68+
while (child_idx_ < children_.size()) {
69+
if (!children_[child_idx_]->next(out)) {
70+
++child_idx_;
71+
continue;
6972
}
70-
if (got) {
71-
check_col_count(out);
72-
std::string key = row_key(out);
73-
if (seen_.find(key) == seen_.end()) {
74-
check_operator_row_limit(seen_.size(), kDefaultMaxOperatorRows, "SetOpOperator");
75-
}
76-
if (seen_.insert(key).second) return true;
73+
check_col_count(out);
74+
std::string key = row_key(out);
75+
if (seen_.find(key) == seen_.end()) {
76+
check_operator_row_limit(seen_.size(), kDefaultMaxOperatorRows, "SetOpOperator");
7777
}
78+
if (seen_.insert(key).second) return true;
7879
}
80+
return false;
7981
}
8082

8183
if (op_ == SET_OP_UNION && all_) {
82-
// UNION ALL: yield left then right
83-
if (reading_left_) {
84-
if (left_->next(out)) {
84+
while (child_idx_ < children_.size()) {
85+
if (children_[child_idx_]->next(out)) {
8586
check_col_count(out);
8687
return true;
8788
}
88-
reading_left_ = false;
89-
}
90-
if (right_->next(out)) {
91-
check_col_count(out);
92-
return true;
89+
++child_idx_;
9390
}
9491
return false;
9592
}
9693

9794
if (op_ == SET_OP_INTERSECT) {
98-
// Yield left rows that also appear in right
99-
while (left_->next(out)) {
95+
while (children_[0]->next(out)) {
10096
check_col_count(out);
10197
std::string key = row_key(out);
10298
if (right_set_.count(key)) {
@@ -108,8 +104,7 @@ class SetOpOperator : public Operator {
108104
}
109105

110106
if (op_ == SET_OP_EXCEPT) {
111-
// Yield left rows that don't appear in right
112-
while (left_->next(out)) {
107+
while (children_[0]->next(out)) {
113108
check_col_count(out);
114109
std::string key = row_key(out);
115110
if (!right_set_.count(key)) {
@@ -124,20 +119,22 @@ class SetOpOperator : public Operator {
124119
}
125120

126121
void close() override {
127-
left_->close();
128-
right_->close();
122+
for (size_t i = 0; i < children_.size(); ++i) {
123+
if (right_closed_ && i == 1) continue;
124+
children_[i]->close();
125+
}
129126
seen_.clear();
130127
right_set_.clear();
131128
}
132129

133130
private:
134-
Operator* left_;
135-
Operator* right_;
131+
std::vector<Operator*> children_;
136132
uint8_t op_;
137133
bool all_;
138134
bool parallel_open_;
139135
ThreadPool* pool_ = nullptr;
140-
bool reading_left_ = true;
136+
size_t child_idx_ = 0;
137+
bool right_closed_ = false;
141138
std::unordered_set<std::string> seen_;
142139
std::unordered_set<std::string> right_set_;
143140
// Column count established by the first row we see. Used to detect

include/sql_engine/plan_executor.h

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1171,11 +1171,29 @@ class PlanExecutor {
11711171
}
11721172

11731173
Operator* build_set_op(PlanNode* node) {
1174+
std::vector<PlanNode*> remote_leaves;
1175+
if (node->set_op.op == SET_OP_UNION && node->set_op.all &&
1176+
collect_union_all_remotes(node, remote_leaves) &&
1177+
remote_leaves.size() > 2) {
1178+
std::vector<Operator*> children;
1179+
children.reserve(remote_leaves.size());
1180+
for (PlanNode* leaf : remote_leaves) {
1181+
Operator* child = build_remote_scan(leaf);
1182+
if (!child) return nullptr;
1183+
children.push_back(child);
1184+
}
1185+
bool parallel = parallel_open_enabled_ && children.size() > 1;
1186+
auto op = std::make_unique<SetOpOperator>(
1187+
std::move(children), parallel, parallel ? pool_ : nullptr);
1188+
Operator* ptr = op.get();
1189+
operators_.push_back(std::move(op));
1190+
return ptr;
1191+
}
1192+
11741193
Operator* left = build_operator(node->left);
11751194
Operator* right = build_operator(node->right);
11761195
if (!left || !right) return nullptr;
11771196

1178-
// Enable parallel open when both children are remote scans and executor is thread-safe
11791197
bool parallel = parallel_open_enabled_ &&
11801198
(node->left && node->left->type == PlanNodeType::REMOTE_SCAN &&
11811199
node->right && node->right->type == PlanNodeType::REMOTE_SCAN);
@@ -1187,6 +1205,21 @@ class PlanExecutor {
11871205
return ptr;
11881206
}
11891207

1208+
static bool collect_union_all_remotes(const PlanNode* node,
1209+
std::vector<PlanNode*>& out) {
1210+
if (!node) return false;
1211+
if (node->type == PlanNodeType::REMOTE_SCAN) {
1212+
out.push_back(const_cast<PlanNode*>(node));
1213+
return true;
1214+
}
1215+
if (node->type == PlanNodeType::SET_OP &&
1216+
node->set_op.op == SET_OP_UNION && node->set_op.all) {
1217+
return collect_union_all_remotes(node->left, out) &&
1218+
collect_union_all_remotes(node->right, out);
1219+
}
1220+
return false;
1221+
}
1222+
11901223
Operator* build_remote_scan(PlanNode* node) {
11911224
if (!remote_executor_) return nullptr;
11921225
sql_parser::StringRef sql{node->remote_scan.remote_sql,

include/sql_engine/session.h

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,16 @@ class Session {
126126
std::string sql_key(sql, len);
127127
auto cache_it = plan_cache_.find(sql_key);
128128
if (cache_it != plan_cache_.end()) {
129-
// Cache hit: move this entry to the front of the LRU list.
130129
plan_cache_order_.splice(plan_cache_order_.begin(),
131130
plan_cache_order_,
132131
cache_it->second);
133132
exec_arena_.reset();
134133
auto& entry = *cache_it->second;
134+
PlanNode* plan = maybe_distribute(entry.plan, exec_arena_);
135+
if (!plan) return {};
135136
PlanExecutor<D> executor(functions_, catalog_, exec_arena_);
136137
wire_executor(executor);
137-
return executor.execute(entry.plan);
138+
return executor.execute(plan);
138139
}
139140

140141
// Cache miss: full parse -> plan -> optimize -> distribute pipeline
@@ -164,25 +165,21 @@ class Session {
164165

165166
plan = optimizer_.optimize(plan, cached_parser->arena());
166167

167-
// Distribute across shards if shard map is configured
168+
ResultSet rs;
168169
if (shard_map_ && remote_executor_) {
169-
DistributedPlanner<D> dplanner(*shard_map_, catalog_, cached_parser->arena(), remote_executor_, &functions_);
170-
plan = dplanner.distribute(plan);
170+
exec_arena_.reset();
171+
PlanNode* dist = maybe_distribute(plan, exec_arena_);
172+
if (!dist) return {};
173+
PlanExecutor<D> executor(functions_, catalog_, exec_arena_);
174+
wire_executor(executor);
175+
rs = executor.execute(dist);
176+
} else {
177+
PlanExecutor<D> executor(functions_, catalog_, cached_parser->arena());
178+
wire_executor(executor);
179+
rs = executor.execute(plan);
171180
}
172181

173-
// Execute first using the parser's arena. preprocess_aggregates (called
174-
// inside execute()) may allocate into the arena to modify the plan in-place.
175-
// Those allocations must persist in the parser arena (not exec_arena_) so
176-
// the cached plan remains valid across calls.
177-
PlanExecutor<D> executor(functions_, catalog_, cached_parser->arena());
178-
wire_executor(executor);
179-
ResultSet rs = executor.execute(plan);
180-
181-
// Cache the plan, enforcing the LRU bound. The parser arena is kept
182-
// alive via unique_ptr stored in the CachedPlan so all plan/AST
183-
// string pointers remain valid for subsequent cache hits.
184182
insert_into_plan_cache(std::move(sql_key), std::move(cached_parser), plan);
185-
186183
return rs;
187184
}
188185

@@ -378,6 +375,15 @@ class Session {
378375
std::unordered_map<std::string, CacheIter> plan_cache_;
379376
size_t plan_cache_max_size_ = 1024;
380377

378+
PlanNode* maybe_distribute(PlanNode* plan, sql_parser::Arena& arena) {
379+
if (!plan || !shard_map_ || !remote_executor_) return plan;
380+
DistributedPlanner<D> dplanner(*shard_map_, catalog_, arena,
381+
remote_executor_, &functions_);
382+
PlanNode* dist = dplanner.distribute(plan);
383+
if (dplanner.last_error()) return nullptr;
384+
return dist;
385+
}
386+
381387
void insert_into_plan_cache(std::string key,
382388
std::unique_ptr<sql_parser::Parser<D>> parser,
383389
PlanNode* plan) {

0 commit comments

Comments
 (0)