首页/数据/SQL 速查/IN 子查询与半连接

IN 子查询与半连接

IN 子查询表达“存在于另一张表”:只要找到匹配就返回该行,不放大行数;优化器通常把它改写为半连接。

SQL 速查数据更新于 2026-10-13

IN 子查询与半连接

IN 子查询表达「左表的行在子查询结果中存在」,语义上属于半连接:每行最多输出一次,不会像普通 JOIN 那样按匹配条数放大结果。优化器通常把它改写成半连接来执行——MySQL 8 提供 FirstMatch、LooseScan、Materialization 等多种策略,PostgreSQL 常用哈希半连接,所以「子查询一定比 JOIN 慢」是过时的说法。判断依据始终是执行计划,而不是直觉。

基本信息

语义存在即保留该行,每行至多出现一次
MySQL 8 策略半连接(FirstMatch / LooseScan / Table Pullout 等)或物化后连接
PostgreSQL通常改写为哈希半连接或嵌套循环半连接
语法限制IN 子查询只能返回一列,多列会直接报错
与 JOIN 的区别JOIN 输出两侧列并按匹配行数展开,IN 只做筛选

典型使用场景

  • 白名单筛选:只保留存在于名单表中的用户或订单
  • 只筛不取:需要的列全在主表,不需要副表的任何字段
  • 替代 EXISTS 的等价写法:语义相同,写法更短
  • 分页列表的主过滤条件:先确定候选主键,再取明细
  • 数据校验:检查主表里有没有「明细表完全缺失」的孤儿行

三种等价写法

三种等价写法
SELECT u.id, u.name
FROM users u
WHERE u.id IN (SELECT o.user_id FROM orders o WHERE o.amount > 100);

# 等价写法:EXISTS 更直观地表达「存在」,大子查询下也常被优化得更好
SELECT u.id, u.name
FROM users u
WHERE EXISTS (SELECT 1 FROM orders o
              WHERE o.user_id = u.id AND o.amount > 100);

# 需要副表字段时才用 JOIN,并注意行数放大
SELECT DISTINCT u.id, u.name
FROM users u JOIN orders o ON o.user_id = u.id
WHERE o.amount > 100;

常见报错

常见报错
# 错误:子查询返回多列
SELECT * FROM users WHERE id IN (SELECT id, name FROM vip_users);
# ERROR 1241: Operand should contain 1 column(s)

# 修正:只取需要的单列,或改用 EXISTS 相关子查询
SELECT * FROM users WHERE id IN (SELECT id FROM vip_users);

几种写法对比

写法行数是否放大说明
IN 子查询不放大MySQL 半连接或物化;PG 哈希半连接
EXISTS不放大半连接,找到首行即可短路
JOIN会放大需要输出副表列,必要时配合 DISTINCT
NOT IN不放大子查询含 NULL 时结果直接为空,需格外小心
MySQL 的半连接能力受 optimizer_switch 中的 semijoin / materialization 开关影响;出现性能异常时可以对比开关前后的执行计划,但不要在线上长期关闭它们。
改用 NOT IN 时要警惕 NULL:只要子查询结果里出现一个 NULL,整个 NOT IN 就会返回空集,筛选条件形同失效。

易错点

  • 子查询返回多列,直接报错 1241,IN 只接受单列
  • 大子查询配小外层时优化器选择物化,性能劣化却没意识到可以对比 EXISTS 的计划
  • 把 IN 换成 JOIN 后忘记 DISTINCT,结果出现重复行
  • 用 NOT IN 做排除筛选,子查询可能返回 NULL 时结果直接为空

数据来源与参考

  • MySQL 8.0 Reference Manual · Optimizing Subqueries with Semijoin
  • PostgreSQL Documentation · IN
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌