SQL JOIN 命令速查表 - SQL 连接查询常用命令大全

面向要关联查询、却常把结果行数统计错的开发者。最常见的坑是:连接一对一多(一对多)的表时,父表的每一行会被子表的匹配行放大一次,结果行数虚增,除非去重。读完能按"未匹配的行是否要保留"决定用 INNER 还是 LEFT,理解一对多连接的行重复陷阱,用 LEFT JOIN ... WHERE ... IS NULL 表达"没有匹配"的场景,并在相关子查询里用 EXISTS 替代 JOIN。

数据库·共 41 条命令·最后更新 2026-07-21
sqljoin数据库查询

典型使用场景

面向每天写报表、排查数据对不上、给已有查询提速的工程师。JOIN 的难点从来不是语法,而是两件事:语义上,LEFT JOIN 后再在 WHERE 里过滤右表条件会把外连接降级成内连接;性能上,多表连接时驱动表选错会导致行数指数级放大。本表覆盖 INNER / LEFT / RIGHT / FULL / CROSS JOIN 的实际写法与等价形式、自连接与多层连接、JOIN 与聚合(COUNT、SUM、AVG、GROUP BY、HAVING)、JOIN 性能优化(EXPLAIN、索引、STRAIGHT_JOIN、FORCE INDEX)、USING 与 NATURAL JOIN 的陷阱,以及子查询与 EXISTS。适合在 review 一条新报表 SQL 时逐条对照「这个 JOIN 语义对不对、有没有索引」。

INNER JOIN 5

SELECT * FROM users INNER JOIN orders ON users.id = orders.user_id
只返回两表匹配的行
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id
简写 JOIN(默认 INNER)
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
ON 子句多个条件,共同决定匹配
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.total > 100
JOIN 后用 WHERE 过滤整体结果
SELECT u.name, o.total, p.amount FROM users u JOIN orders o ON u.id = o.user_id JOIN payments p ON o.id = p.order_id
多表 INNER JOIN 链式关联

LEFT / RIGHT JOIN 6

SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id
返回左表所有行,右表无匹配则为 NULL
SELECT * FROM users RIGHT JOIN orders ON users.id = orders.user_id
返回右表所有行,左表无匹配则为 NULL
SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id WHERE orders.id IS NULL
查找左表独有(无订单的用户)
SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
ON 条件放在 LEFT JOIN 不影响左表行数
SELECT * FROM orders o RIGHT JOIN users u ON u.id = o.user_id WHERE o.id IS NULL
RIGHT JOIN 版反连接(查找无订单用户)
SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id
LEFT JOIN + 聚合,统计所有用户含零订单

FULL / CROSS JOIN 5

SELECT * FROM users FULL OUTER JOIN orders ON users.id = orders.user_id
返回两表所有行,无匹配则为 NULL
SELECT * FROM users u FULL JOIN orders o ON u.id = o.user_id WHERE u.id IS NULL OR o.user_id IS NULL
查找两表独有(对称差)
SELECT * FROM table1 CROSS JOIN table2
笛卡尔积,每行与另一表所有行组合
SELECT * FROM table1, table2
隐式 CROSS JOIN(逗号分隔)
SELECT * FROM sizes CROSS JOIN colors
生成所有规格组合(如尺码 × 颜色)

自连接与多表 5

SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id
自连接(员工-经理关系)
SELECT a.name, b.name AS parent FROM categories a JOIN categories b ON a.parent_id = b.id
自连接(分类-父分类)
SELECT * FROM a JOIN b ON a.id = b.a_id JOIN c ON b.id = c.b_id
多表三表连接
SELECT t1.name AS grandchild, t3.name AS grandparent FROM tree t1 JOIN tree t2 ON t1.parent_id = t2.id JOIN tree t3 ON t2.parent_id = t3.id
多级自连接(祖孙三代)
SELECT u.name, o.total, p.amount FROM users u JOIN orders o ON u.id = o.user_id JOIN payments p ON o.id = p.order_id
三表关联(用户-订单-支付)

JOIN 与聚合 5

SELECT u.name, COUNT(o.id) AS order_count FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.id
每个用户的订单数
SELECT u.name, SUM(o.total) AS total_spent FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.id HAVING SUM(o.total) > 1000
HAVING 过滤聚合结果(消费超 1000)
SELECT c.name, AVG(o.total) AS avg_order FROM customers c JOIN orders o ON c.id = o.customer_id GROUP BY c.id
分组求平均值
SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id HAVING COUNT(o.id) = 0
LEFT JOIN + HAVING 查找零订单用户
SELECT DATE(o.created_at) AS d, COUNT(*) FROM orders o JOIN users u ON u.id = o.user_id GROUP BY DATE(o.created_at)
按日期分组统计

JOIN 性能优化 6

EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id
查看执行计划
EXPLAIN ANALYZE SELECT * FROM users u JOIN orders o ON u.id = o.user_id
实际执行并显示耗时(PostgreSQL / MySQL 8.0+)
CREATE INDEX idx_orders_user_id ON orders(user_id)
为外键建索引加速 JOIN
SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id
只查需要的列,避免 SELECT * 回表
SELECT * FROM users u STRAIGHT_JOIN orders o ON u.id = o.user_id
MySQL 强制左表为驱动表按指定顺序连接
SELECT * FROM users u JOIN orders o FORCE INDEX(idx_user_id) ON u.id = o.user_id
MySQL 强制使用指定索引

USING 与自然连接 4

SELECT * FROM users JOIN orders USING (user_id)
USING 简写(两表关联列同名)
SELECT * FROM users u JOIN orders o USING (user_id, tenant_id)
USING 多列简写,结果合并同名列
SELECT * FROM users NATURAL JOIN orders
自然连接(自动按所有同名列匹配,慎用)
SELECT * FROM users u JOIN orders o ON u.id = o.user_id AND u.tenant_id = o.tenant_id
多列关联(可用 USING 简化)

子查询与 EXISTS 5

SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
查找有订单的用户
SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
查找无订单的用户
SELECT * FROM orders o WHERE o.user_id IN (SELECT id FROM users WHERE status = 'active')
IN 子查询过滤
SELECT u.name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt FROM users u
相关子查询(返回标量)
SELECT * FROM orders WHERE user_id = (SELECT MAX(user_id) FROM orders)
子查询返回单值

参数矩阵

参数作用示例
INNER JOIN ... ON只保留两侧都匹配的行;不匹配的行直接消失SELECT * FROM users INNER JOIN orders ON users.id = orders.user_id
JOIN u ON ...(表别名)JOIN 是 INNER JOIN 的缩写;写别名让多表连接时的列引用不易混淆SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id
LEFT JOIN ... ON保留左表全部行,右表无匹配时补 NULL,是「左表为主」的默认选择SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id
RIGHT JOIN ... ON保留右表全部行;实际工程里通常改写成 LEFT JOIN 更易读SELECT * FROM users RIGHT JOIN orders ON users.id = orders.user_id
... WHERE o.id IS NULL反连接写法:找出左表中没有右表记录的实体(无订单用户、未分类商品)SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id WHERE orders.id IS NULL
LEFT JOIN ... ON u.id = o.user_id AND o.status = 'paid'把状态条件放进 ON 里,才不会把 LEFT JOIN 降级成 INNER JOINSELECT u.name, o.total FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
FULL OUTER JOIN两侧都保留;MySQL 不支持,需要用 LEFT JOIN 与 RIGHT JOIN 加 UNION 模拟SELECT * FROM users FULL OUTER JOIN orders ON users.id = orders.user_id
CROSS JOIN / 逗号连接笛卡尔积,行数是两表行数相乘;只在「所有组合都合法」的小表上用SELECT * FROM sizes CROSS JOIN colors
自连接 e LEFT JOIN employees m同一张表连接自身,是层级关系(员工-经理、分类-父分类)的标准写法SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id
多表链式 JOIN三张以上表要逐段写清 ON 条件,漏一段就会产生笛卡尔积SELECT u.name, o.total, p.amount FROM users u JOIN orders o ON u.id = o.user_id JOIN payments p ON o.id = p.order_id
COUNT(o.id) AS order_count统计时数的是具体列而不是 *,这样无匹配的 LEFT JOIN 行才会正确计为 0SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id
GROUP BY ... HAVING SUM(o.total) > 1000HAVING 过滤聚合结果,WHERE 过滤聚合前的行;两者顺序不能互换SELECT u.name, SUM(o.total) AS total_spent FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.id HAVING SUM(o.total) > 1000
SELECT DATE(o.created_at) AS d, COUNT(*)按函数结果分组做时间序列统计;注意部分数据库要求函数与列同组SELECT DATE(o.created_at) AS d, COUNT(*) FROM orders o JOIN users u ON u.id = o.user_id GROUP BY DATE(o.created_at)
EXPLAIN SELECT ...查看执行计划里的驱动表、连接类型与索引命中情况,是排 JOIN 慢查询的第一步EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id
EXPLAIN ANALYZE SELECT ...真正执行并给出每步实际行数与耗时,能暴露「估算行数与实际差几个数量级」的统计信息问题EXPLAIN ANALYZE SELECT * FROM users u JOIN orders o ON u.id = o.user_id
CREATE INDEX idx_orders_user_id ON orders(user_id)为连接键建索引;没有索引的连接在大表上必然全表扫描CREATE INDEX idx_orders_user_id ON orders(user_id)
STRAIGHT_JOIN强制按 FROM 的书写顺序连接,作为优化器选错驱动表时的临时手段SELECT * FROM users u STRAIGHT_JOIN orders o ON u.id = o.user_id
FORCE INDEX (idx_user_id)强制指定某表用哪个索引,比 STRAIGHT_JOIN 更精确,但会把索引固化在查询里SELECT * FROM users u JOIN orders o FORCE INDEX(idx_user_id) ON u.id = o.user_id
JOIN ... USING (user_id)按同名列连接,且该列在结果里只出现一次;适合规范一致的宽表SELECT * FROM users JOIN orders USING (user_id)
NATURAL JOIN隐式按所有同名列连接;表一改列名语义就静默改变,极不推荐SELECT * FROM users NATURAL JOIN orders
ON u.id = o.user_id AND u.tenant_id = o.tenant_id多租户场景必须把租户列写进 ON,否则会跨租户串数据SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id AND u.tenant_id = o.tenant_id
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)相关子查询的存在性判断,找到即停;比 IN 更能正确处理含 NULL 的列SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
WHERE NOT EXISTS (...)反连接的存在性判断,通常比 LEFT JOIN + IS NULL 执行计划更干净SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)
user_id IN (SELECT id FROM users WHERE status = 'active')半连接写法;子查询结果含 NULL 时 NOT IN 会整体返回空集SELECT * FROM orders o WHERE o.user_id IN (SELECT id FROM users WHERE status = 'active')
SELECT (SELECT COUNT(*) ...) AS cnt标量子查询作投影,对每一行执行一次;行数多时明显变慢,应改用 JOIN + GROUP BYSELECT u.name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt FROM users u
WHERE user_id = (SELECT MAX(user_id) FROM orders)取极值行的经典写法,比排序取第一行更直观SELECT * FROM orders WHERE user_id = (SELECT MAX(user_id) FROM orders)

易错点与避坑指南

现象本该显示「无订单的用户」,结果这些用户整行都没了。

原因写的是 LEFT JOIN 却把右表条件放在了 WHERE:WHERE orders.status = 'paid' 在补 NULL 后不成立,等于把 LEFT JOIN 降级成 INNER JOIN。

处置把右表条件移进 ON 子句里;若确实是只要已支付订单,就明确改用 INNER JOIN。

现象统计订单数时「没有订单的用户」被算成了 1。

原因聚合用了 COUNT(*) 或 COUNT(o.*),无匹配时补出来的一行 NULL 也被计入。

处置改成 COUNT(o.id) 这类指定可空列的写法,无匹配时 COUNT 忽略 NULL 并返回 0。

现象行数暴涨,多表连接后结果比预期大几个数量级。

原因三表链式连接时某一段 ON 条件写漏或写错,导致中间产生笛卡尔积。

处置逐段验证:先只 join 两张表看行数,再加第三张;EXPLAIN 里确认每段的驱动表与 rows 估算。

现象JOIN 突然变慢,数据量没涨但耗时翻倍。

原因连接键缺索引,或统计信息陈旧导致优化器选错驱动表。

处置EXPLAIN 看是否全表扫描,给连接键建索引;必要时用 EXPLAIN ANALYZE 对比估算与实际行数。

现象同一份 SQL 在不同数据库(MySQL / PostgreSQL)行为不一致。

原因FULL OUTER JOIN 与 USING 支持程度不同,字符串拼接与 GROUP BY 规则(是否要求所有非聚合列都在组内)也不一致。

处置按目标数据库写等价实现;跨库报表尽量收敛到 JOIN + 显式列名 + 严格 GROUP BY。

现象NATURAL JOIN 后结果少了一列,语义突然变了。

原因NATURAL JOIN 隐式按所有同名列连接,任一侧新增同名列都会静默改变连接条件。

处置改用显式 JOIN ... ON,把连接列写死,避免 schema 变更带来隐性 bug。

现象多租户库里查出了别的租户的数据。

原因连接条件只写了主键相等,忘了 tenant_id;或者过滤租户的 WHERE 只加在了一侧。

处置把 tenant_id 写进 ON 子句,并在两侧都用 tenant_id 过滤,必要时对租户列建复合索引。

现象WHERE user_id NOT IN (SELECT ...) 返回空结果。

原因子查询结果里含 NULL,NOT IN 遇到 NULL 会整体判定为未知,从而一行都不返回。

处置改用 NOT EXISTS,或先在子查询里过滤掉 NULL。

排障路径

  1. 1看执行计划与连接类型

    EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id

    重点看每个表的 type、key 与 rows。

  2. 2对比估算与实际行数

    EXPLAIN ANALYZE SELECT * FROM users u JOIN orders o ON u.id = o.user_id

    两者差几个数量级说明统计信息需要更新。

  3. 3确认连接键是否命中索引

    grep -n "CREATE INDEX" schema.sql

    索引缺失是最常见的 JOIN 性能根因。

  4. 4验证多租户过滤是否成对出现

    grep -n "tenant_id" queries.sql

    每个涉及多租户表的查询都应两侧过滤。

  5. 5检查是否误用 NATURAL JOIN

    grep -n "NATURAL JOIN" queries.sql

    生产查询里出现它基本都需要改成显式 ON。

  6. 6排查是否把右表条件写进了 WHERE

    grep -n "LEFT JOIN" -A 3 queries.sql

    紧随其后的 WHERE 若引用右表列,就是降级成内连接的信号。

命令示例

反连接:找出一个订单都没有的用户

SELECT u.name, o.total FROM users u LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' WHERE o.id IS NULL

条件写在 ON 而不是 WHERE,是这条查询成立的关键:LEFT JOIN 的 ON 条件只影响右表能否匹配上,不会把左表的行过滤掉。所以零订单用户仍然留在结果里,再靠 WHERE o.id IS NULL 挑出来。如果把 o.status 写进 WHERE,所有未支付的用户都会被误判成「无订单」。

零订单用户:LEFT JOIN + 聚合 + HAVING

SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id HAVING COUNT(o.id) = 0

这里必须用 COUNT(o.id) 而不是 COUNT(*):无订单时 o.id 全是 NULL,COUNT(o.id) 得 0,COUNT(*) 因为左表那行仍存在会得 1,判断永远不成立。区分「有没有」和「有多少」时,永远数右表的非空列。

自连接把扁平表还原成层级

SELECT t1.name AS grandchild, t3.name AS grandparent FROM tree t1 JOIN tree t2 ON t1.parent_id = t2.id JOIN tree t3 ON t2.parent_id = t3.id

同一张 tree 表用三个别名连续自连接三次,就能沿 parent_id 上溯两代。结果里出现的 grandparent 为 NULL 并不代表写错,而是根节点的父节点本来就不存在——需要时补一个 LEFT JOIN 并对 t3.name 做 IS NULL 判断来区分根节点和断链数据。

建外键索引前后对比执行计划

EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id CREATE INDEX idx_orders_user_id ON orders(user_id) EXPLAIN ANALYZE SELECT * FROM users u JOIN orders o ON u.id = o.user_id

第一次 EXPLAIN 如果看到 Nested Loop 加上很长的 rows 估算,说明 orders.user_id 没走索引,数据库要对 users 的每一行去全表扫一次 orders——orders 一大就是灾难。建索引后再 EXPLAIN ANALYZE 拿到真实耗时,确认扫描行数确实降下来了再收工。

输出

Nested Loop  (cost=0.00..45231.44 rows=1500)

用 EXISTS 代替 IN,避免大子查询反复求值

SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) SELECT * FROM orders o WHERE o.user_id IN (SELECT id FROM users WHERE status = 'active')

两者的语义差别很关键:NOT EXISTS / EXISTS 遇到第一条匹配行就短路,IN 通常需要把整个子查询结果物化成集合。用户表几十万行时,IN 子查询的中间结果可能比主查询还大,改成 EXISTS 后执行计划通常会从物化(Materialize)变成半连接(Semi Join)。

常见坑与注意事项

  • JOIN 漏写 ON(或写成 ON 1=1、ON a.id=b.id 里手误写成同表同列)会产生笛卡尔积:两张各 5 万行的表会返回 25 亿行,磁盘写满、连接池被打满,线上常常表现为「接口变慢」而不是「SQL 报错」,极难定位。写完 JOIN 先数一下 ON 里的条件数是否等于前 N-1 张表的连接键数。
  • 内连接时 NULL 永远不参与比较:ON a.user_id = o.user_id 遇到 NULL 会被判为不匹配,不是匹配。任何带 NULL 的关联列(未填的外部 ID、历史遗留字段)都会静默丢行。关联列可能为 NULL 时必须用 NULLIF 包一层或改用 IS NOT DISTINCT FROM 语义,并在条件里补 o.user_id IS NOT NULL。
  • SELECT DISTINCT 只是把重复行压掉,不解决重复的来源。用 LEFT JOIN 接明细表(orders、items)时行数本来就会膨胀,加 DISTINCT 会把金额、数量一起丢掉——三个订单总额 100 的用户去重后只剩一行 100。正确做法是用 GROUP BY 按主键聚合,而不是加 DISTINCT 掩盖。
  • 把右表过滤条件写进 LEFT JOIN 的 WHERE 会让它退化成 INNER JOIN:本该保留的无订单用户被 WHERE o.total > 100 全部滤掉,最后 LEFT JOIN 白写。要保留左表全部行,右表条件必须放在 ON 里。
  • SELECT * 出现在 JOIN 里时,两表同名列(比如都叫 id、created_at)会在结果集里出现两遍,业务代码按下标取值很容易取到另一张表的字段;更糟的是加了 count(o.id) 后语义仍然正确,导致这个 bug 长期潜伏。写查询时只 SELECT 用到的列,并显式给列加别名。
  • 多表 JOIN 忘了建索引会触发全表扫描,EXPLAIN 里的 rows 估算能提前暴露。生产上给 users JOIN orders ON u.id = o.user_id 加索引前,先确认 orders.user_id 上是否已有可用索引,重复建索引只会拖慢写入。

提示

  • LEFT JOIN 的 WHERE 条件如果放在 ON 里,只影响右表匹配;放在 WHERE 里会过滤整个结果集。
  • CROSS JOIN 会产生笛卡尔积,数据量大时慎用。
  • 用表别名(u、o)可以让多表查询更简洁易读。
  • EXISTS 子查询在某些场景下比 JOIN 性能更好,特别是只需要判断存在性时。
  • JOIN 的连接列建议建索引,否则会触发全表扫描导致性能下降。
  • USING(a, b) 比 ON x.a = y.a AND x.b = y.b 更简洁,且结果集会合并同名列。

常见问题

INNER JOIN 和 LEFT JOIN 到底怎么选?判断标准是什么?

只看你是否需要保留「右表没有匹配」的那批左表行。要两边都有对应记录才输出用 INNER:SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id(JOIN 默认就是 INNER)。要保留所有用户、没订单的人也要看到,就换 LEFT JOIN,未匹配时右表字段为 NULL。最容易踩的坑是把过滤条件写错位置:写成 LEFT JOIN ... WHERE o.status = 'paid' 会把没订单的用户过滤掉,因为 NULL 比较不为真;正确做法是把状态条件写进 ON:ON u.id = o.user_id AND o.status = 'paid',这样只影响匹配而不减少左表行数。

JOIN 之后行数比预期多了,是什么导致的?

几乎都是「一对多」被放大。users JOIN orders 时,一个用户有 3 笔订单就产生 3 行;再 JOIN payments,一个订单有多条支付记录,行数继续翻倍。改法有几种:只 SELECT 真正需要的列而不是 SELECT *,用表别名和聚合把每行收敛成一行:SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id;或者在 ON 上加聚合前先限定范围;只看存在性时根本不用 JOIN,改成 WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id),某些场景下性能反而更好。

怎么查出「没有订单的用户」这类反连接场景?

LEFT JOIN 加 IS NULL 判定:SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id WHERE orders.id IS NULL。匹配上的行右表 id 非空被 WHERE 排除,剩下的正是完全没有订单的用户。用 HAVING 也能做等价的事:SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id HAVING COUNT(o.id) = 0;或者完全不用 JOIN,SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)。要查两表各自独有的记录,则用 FULL OUTER JOIN 加 WHERE u.id IS NULL OR o.user_id IS NULL 取对称差。

多表关联时 USING、NATURAL JOIN 和 ON 怎么选?

ON 最通用也最清晰,表名不同名时必须用它:ON u.id = o.user_id AND u.tenant_id = o.tenant_id。两表关联列同名时用 USING 更短:SELECT * FROM users JOIN orders USING (user_id),多列可以写 USING (user_id, tenant_id),而且结果集里同名列会合并成一个字段,省掉 u. 和 o. 的区分。NATURAL JOIN 会自动按所有同名列匹配,列一改就静默出错,表中明确标注慎用,只在临时查询里用。

JOIN 查询突然变慢,先查什么?

先看执行计划:EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id 确认连接方式,EXPLAIN ANALYZE(PostgreSQL / MySQL 8.0+)能实际执行并显示耗时。最常见的原因是连接列没索引,用 CREATE INDEX idx_orders_user_id ON orders(user_id) 补上,避免全表扫描。其次把 SELECT * 换成只查需要的列,减少回表。MySQL 下优化器选错驱动表时,可以用 STRAIGHT_JOIN 强制按写的顺序连接,或 FORCE INDEX(idx_user_id) 指定索引;注意 COUNT(*)、SUM、SUM 这类聚合后要过滤结果,走的是 HAVING 而不是 WHERE。

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。

由 巧匠 维护

公开更新于 2026年7月21日,内容持续校对官方文档。

联系我们

命令或描述有误?提交反馈、商务合作或产品建议都可发送邮件给我们。

联系我们