@@ -357,7 +357,13 @@ class DistributedPlanner {
357357 if (keys.empty ()) return all_shards;
358358
359359 std::vector<size_t > target_indices;
360- if (keys.size () > 1 ) {
360+ if (keys.size () > 1 &&
361+ shards_.routing_strategy (table->table_name ) == RoutingStrategy::RANGE ) {
362+ sql_parser::StringRef first{keys[0 ].c_str (),
363+ static_cast <uint32_t >(keys[0 ].size ())};
364+ extract_shard_targets (where_expr, first, table->table_name ,
365+ all_shards.size (), target_indices);
366+ } else if (keys.size () > 1 ) {
361367 extract_composite_targets (where_expr, keys, table->table_name , target_indices);
362368 } else {
363369 sql_parser::StringRef shard_key{keys[0 ].c_str (),
@@ -1203,6 +1209,157 @@ class DistributedPlanner {
12031209 return current ? current : join_node;
12041210 }
12051211
1212+ bool column_on_table (const sql_parser::AstNode* node, const TableInfo* table,
1213+ const TableInfo* other) const {
1214+ if (!node || !table) return false ;
1215+ if (node->type == sql_parser::NodeType::NODE_QUALIFIED_NAME ) {
1216+ const sql_parser::AstNode* t = node->first_child ;
1217+ if (!t) return false ;
1218+ sql_parser::StringRef tn = t->value ();
1219+ if (table->table_name .equals_ci (tn.ptr , tn.len )) return true ;
1220+ if (table->alias .ptr && table->alias .equals_ci (tn.ptr , tn.len )) return true ;
1221+ return false ;
1222+ }
1223+ if (node->type == sql_parser::NodeType::NODE_COLUMN_REF ||
1224+ node->type == sql_parser::NodeType::NODE_IDENTIFIER ) {
1225+ if (!catalog_.get_column (table, node->value ())) return false ;
1226+ if (other && catalog_.get_column (other, node->value ())) return false ;
1227+ return true ;
1228+ }
1229+ return false ;
1230+ }
1231+
1232+ const sql_parser::AstNode* probe_key_in_join (const sql_parser::AstNode* cond,
1233+ const TableInfo* probe,
1234+ const TableInfo* build) const {
1235+ if (!cond || !probe || !build) return nullptr ;
1236+ const auto & keys = shards_.get_shard_keys (probe->table_name );
1237+ if (keys.size () != 1 ) return nullptr ;
1238+ sql_parser::StringRef sk{keys[0 ].c_str (), static_cast <uint32_t >(keys[0 ].size ())};
1239+ std::vector<std::pair<const sql_parser::AstNode*, const sql_parser::AstNode*>> eqs;
1240+ collect_eq_pairs (cond, eqs);
1241+ for (const auto & eq : eqs) {
1242+ if (is_shard_key_ref (eq.first , sk) && column_on_table (eq.second , build, probe))
1243+ return eq.first ;
1244+ if (is_shard_key_ref (eq.second , sk) && column_on_table (eq.first , build, probe))
1245+ return eq.second ;
1246+ }
1247+ return nullptr ;
1248+ }
1249+
1250+ sql_parser::AstNode* make_in_list_on_column (const sql_parser::AstNode* col,
1251+ const std::vector<Value>& values) {
1252+ if (!col || values.empty ()) return nullptr ;
1253+ sql_parser::AstNode* stub = sql_parser::make_node (
1254+ arena_, sql_parser::NodeType::NODE_IN_LIST ,
1255+ sql_parser::StringRef{nullptr , 0 });
1256+ sql_parser::AstNode* col_copy = sql_parser::make_node (
1257+ arena_, col->type , col->value (), col->flags );
1258+ col_copy->first_child = col->first_child ;
1259+ stub->add_child (col_copy);
1260+ return build_in_list_from_values (stub, values);
1261+ }
1262+
1263+ sql_parser::AstNode* and_preds (const sql_parser::AstNode* a,
1264+ const sql_parser::AstNode* b) {
1265+ if (!a) return const_cast <sql_parser::AstNode*>(b);
1266+ if (!b) return const_cast <sql_parser::AstNode*>(a);
1267+ sql_parser::AstNode* n = sql_parser::make_node (
1268+ arena_, sql_parser::NodeType::NODE_BINARY_OP ,
1269+ sql_parser::StringRef{" AND" , 3 });
1270+ n->add_child (const_cast <sql_parser::AstNode*>(a));
1271+ n->add_child (const_cast <sql_parser::AstNode*>(b));
1272+ return n;
1273+ }
1274+
1275+ std::vector<Value> collect_build_join_keys (const TableInfo* build,
1276+ const sql_parser::AstNode* where_expr,
1277+ const sql_parser::AstNode* join_eq_other) {
1278+ std::vector<Value> out;
1279+ if (!build || !join_eq_other || !remote_executor_) return out;
1280+ const sql_parser::AstNode* proj[1 ] = {join_eq_other};
1281+ const auto & shards = shards_.get_shards (build->table_name );
1282+ if (shards.empty ()) return out;
1283+ std::vector<ShardInfo> targets = shards;
1284+ if (shards_.is_sharded (build->table_name ) && shards.size () > 1 )
1285+ return out;
1286+ sql_parser::StringRef sql = qb_.build_select (
1287+ build, where_expr, proj, 1 , nullptr , 0 ,
1288+ nullptr , nullptr , 0 , -1 , true );
1289+ ResultSet rs = remote_executor_->execute (shards[0 ].backend_name .c_str (), sql);
1290+ for (const auto & row : rs.rows ) {
1291+ if (row.column_count > 0 && value_is_routable (row.get (0 )))
1292+ out.push_back (copy_value_arena (row.get (0 )));
1293+ }
1294+ return out;
1295+ }
1296+
1297+ const sql_parser::AstNode* other_eq_side (const sql_parser::AstNode* cond,
1298+ const sql_parser::AstNode* probe_key) const {
1299+ std::vector<std::pair<const sql_parser::AstNode*, const sql_parser::AstNode*>> eqs;
1300+ collect_eq_pairs (cond, eqs);
1301+ for (const auto & eq : eqs) {
1302+ if (eq.first == probe_key) return eq.second ;
1303+ if (eq.second == probe_key) return eq.first ;
1304+ }
1305+ return nullptr ;
1306+ }
1307+
1308+ PlanNode* try_semijoin_prune (PlanNode* join_node,
1309+ const TableInfo* left_table,
1310+ const TableInfo* right_table) {
1311+ if (!join_node || !remote_executor_ || !join_node->join .condition )
1312+ return nullptr ;
1313+ if (!left_table || !right_table) return nullptr ;
1314+
1315+ bool ls = shards_.is_sharded (left_table->table_name );
1316+ bool rs = shards_.is_sharded (right_table->table_name );
1317+ if (ls == rs) return nullptr ;
1318+
1319+ const TableInfo* probe = ls ? left_table : right_table;
1320+ const TableInfo* build = ls ? right_table : left_table;
1321+ bool probe_is_left = ls;
1322+ const sql_parser::AstNode* probe_key =
1323+ probe_key_in_join (join_node->join .condition , probe, build);
1324+ if (!probe_key) return nullptr ;
1325+ const sql_parser::AstNode* build_col =
1326+ other_eq_side (join_node->join .condition , probe_key);
1327+ if (!build_col) return nullptr ;
1328+
1329+ ScanContext bctx = extract_scan_context (
1330+ probe_is_left ? join_node->right : join_node->left );
1331+ std::vector<Value> keys = collect_build_join_keys (build, bctx.where_expr , build_col);
1332+ if (keys.empty ()) return nullptr ;
1333+
1334+ sql_parser::AstNode* in_list = make_in_list_on_column (probe_key, keys);
1335+ if (!in_list) return nullptr ;
1336+
1337+ ScanContext pctx = extract_scan_context (
1338+ probe_is_left ? join_node->left : join_node->right );
1339+ if (!pctx.scan ) return nullptr ;
1340+ const sql_parser::AstNode* probe_where = and_preds (pctx.where_expr , in_list);
1341+ PlanNode* probe_dist = distribute_scan (pctx.scan , probe_where,
1342+ nullptr , nullptr , nullptr , false );
1343+
1344+ PlanNode* build_dist = nullptr ;
1345+ if (bctx.scan && !shards_.is_sharded (build->table_name )) {
1346+ sql_parser::StringRef sql = qb_.build_select (
1347+ build, bctx.where_expr , nullptr , 0 , nullptr , 0 ,
1348+ nullptr , nullptr , 0 , -1 , false );
1349+ build_dist = make_remote_scan (
1350+ shards_.get_backend (build->table_name ), sql, build);
1351+ } else {
1352+ build_dist = distribute_node (probe_is_left ? join_node->right : join_node->left );
1353+ }
1354+ if (!probe_dist || !build_dist) return nullptr ;
1355+
1356+ PlanNode* result = make_plan_node (arena_, PlanNodeType::JOIN );
1357+ result->join = join_node->join ;
1358+ result->left = probe_is_left ? probe_dist : build_dist;
1359+ result->right = probe_is_left ? build_dist : probe_dist;
1360+ return result;
1361+ }
1362+
12061363 PlanNode* distribute_join (PlanNode* join_node) {
12071364 const TableInfo* left_table = find_table (join_node->left );
12081365 const TableInfo* right_table = find_table (join_node->right );
@@ -1217,6 +1374,9 @@ class DistributedPlanner {
12171374 return distribute_colocated_join (join_node, left_table, right_table);
12181375 }
12191376
1377+ if (PlanNode* sj = try_semijoin_prune (join_node, left_table, right_table))
1378+ return sj;
1379+
12201380 PlanNode* left_dist = nullptr ;
12211381 PlanNode* right_dist = nullptr ;
12221382
0 commit comments