
MySQL 8.0 Semi-Join 源码揭秘FirstMatch 与 Duplicate Weedout 物理算子实现在关系型数据库查询优化中半连接Semi-Join是优化器用来消除IN (SELECT ...)和EXISTS (SELECT ...)低效相关子查询的最强武器。在传统的全连接Inner Join中如果左表的某一行在右表中匹配到了 5 条记录左表的这一行就会在结果集中重复出现 5 次产生数据扇出Fanout。而 Semi-Join 的代数语义是只要在右表中找到了至少一条匹配记录立即返回左表的这一行且左表在结果集中绝不重复出现。在 MySQL 8.0 的执行引擎中优化器在将子查询扁平化上拉为 Semi-Join 之后提供了5 种底层物理执行策略FirstMatch, MaterializeLookup, MaterializeScan, LooseScan, DuplicateWeedout。其中FirstMatch短路匹配与Duplicate Weedout临时表去重是最为通用且最核心的两大物理执行算子。深入 MySQL 8.0 源码sql/sql_executor.cc与sql/item_subselect.cc看懂这两个算子的内存与指针流转才能真正掌握复杂关联查询的极致调优。// MySQL 8.0 Semi-Join 算子执行伪代码 (sql/sql_executor.cc) // 1. FirstMatch 物理算子核心循环 enum_nested_loop_code sub_select_firstmatch(JOIN *join, QEP_TAB *qep_tab, bool end_of_records) { int error (*qep_tab-read_record)(qep_tab); if (error 0) return NESTED_LOOP_ERROR; if (error 0) return NESTED_LOOP_NO_MORE_ROWS; // 关键点: 只要内表找到第一条匹配行 (First Match Found) if (qep_tab-condition()-val_int()) { // 核心源码动作: 立即跳转回外层驱动表短路跳过内表剩余所有匹配行! // 彻底杜绝了内表重复扫描与结果集扇出膨胀 return evaluate_join_record(join, qep_tab); } return NESTED_LOOP_OK; }算子一FirstMatch短路匹配算子——以极速短路消灭内表扫描FirstMatch 类似于编程语言中的break语句假设我们查询SELECT c.customer_name FROM t_customer c WHERE c.id IN (SELECT o.customer_id FROM t_order o WHERE o.amount 1000);[FirstMatch 物理短路扫描时序图] 外表 t_customer 扫描到 customer_id 1001 │ ▼ [进入内表 t_order 索引树扫描 (idx_customer_amount)] - 扫描第 1 行: (1001, 1200元) ──▶ 【满足条件! 命中!】 ──▶ 【FirstMatch 立即触发短路返回外表行!】 - 内表剩余的第 2 行 (1001, 1500元)、第 3 行 (1001, 2000元) ──▶ 【完全不读! 物理跳过!】执行机制外表读取一行驱动内表 B 树索引扫描。一旦内表找到第 1 行满足条件的记录算子立即中断当前内表的遍历直接向上层返回该客户名称并跳转回外表读取下一个客户适用场景内表在关联字段上建有高效索引。此时 FirstMatch 的单次短路成本极低性能达到巅峰。算子二Duplicate Weedout去重算子——在内存中斩断扇出如果内表没有索引或者优化器决定选择内表作为驱动表Join 顺序被调换为内表驱动外表FirstMatch 无法生效此时 MySQL 8.0 会激活Duplicate Weedout去重算子// Duplicate Weedout 去重哈希表检查逻辑 (sql/sql_executor.cc) bool do_sj_dups_weedout(THD *thd, SJ_TMP_TABLE *sjtbl) { // 获取外表当前行在物理存储引擎中的唯一 RowID (主键句柄) uchar *rowid_buf sjtbl-rowid_buffer; sjtbl-file-position(sjtbl-table-record[0]); memcpy(rowid_buf, sjtbl-table-file-ref, sjtbl-table-file-ref_length); // 将 RowID 插入内存临时去重表 (基于内存哈希表) int error sjtbl-file-ha_write_row(sjtbl-table-record[0]); if (error HA_ERR_FOUND_DUPP_KEY) { // 发现重复 RowID: 说明该外表行在之前已经输出过了无情丢弃 (Weedout)! return true; } // 首次遇到该 RowID: 放行输出 return false; }[Duplicate Weedout 内存哈希去重时序图] 外表 t_customer 与 内表 t_order 正常做常规全连接 (产生 3 行包含 customer_id 1001 的结果) │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Duplicate Weedout 内存哈希过滤器 (SJ_TMP_TABLE): │ │ - 处理第 1 行 (RowID: 1001) ──▶ 插入哈希表成功 ──▶ 【放行输出!】│ │ - 处理第 2 行 (RowID: 1001) ──▶ 命中唯一键冲突 ──▶ 【丢弃!】 │ │ - 处理第 3 行 (RowID: 1001) ──▶ 命中唯一键冲突 ──▶ 【丢弃!】 │ └─────────────────────────────────────────────────────────────┘执行机制允许底层执行引擎像普通 Inner Join 一样产生临时的数据扇出Fanout但在最终将数据发送给客户端之前内核开辟了一块临时的内存哈希表记录外表每一行的物理 RowID。只有第一次出现的 RowID 允许输出后续重复的记录全部在内存中被物理剔除Weedout适用场景外表数据量较小内表无索引或多表复杂交错关联。两大算子的 EXPLAIN 特征与调优在EXPLAIN执行计划中FirstMatch会在内表的Extra字段中显式打印FirstMatch(outer_table)Duplicate Weedout会在外表和内表之间用Start temporary与End temporary将参与去重的表范围包裹起来。-- 生产级优化通过 Optimizer Hints 精准控制 Semi-Join 算子选型 -- 1. 强制走 FirstMatch 短路匹配 SELECT /* SEMIJOIN(FIRSTMATCH) */ customer_name FROM t_customer WHERE id IN (SELECT customer_id FROM t_order); -- 2. 强制走 Duplicate Weedout 内存去重 SELECT /* SEMIJOIN(DUPSWEEDOUT) */ customer_name FROM t_customer WHERE id IN (SELECT customer_id FROM t_order);理解 FirstMatch 的短路机制与 Duplicate Weedout 的内存 RowID 去重原理才能在面对复杂的子查询性能瓶颈时精准利用 Hints 纠正优化器的算子裁决榨干数据库内核的每一滴计算潜能。