MySQL复杂SQL(多表联查/子查询)详细讲解

🤟致敬读者

  • 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉

📘博主相关


文章目录

  • MySQL复杂SQL(多表联查/子查询)详细讲解
    • 第一部分:多表联查 (JOIN Operations)
      • [1. 连接的类型 (JOIN Types)](#1. 连接的类型 (JOIN Types))
        • [a. INNER JOIN (内连接 / 等值连接)](#a. INNER JOIN (内连接 / 等值连接))
        • [b. LEFT [OUTER] JOIN (左外连接)](#b. LEFT [OUTER] JOIN (左外连接))
        • [c. RIGHT [OUTER] JOIN (右外连接)](#c. RIGHT [OUTER] JOIN (右外连接))
        • [d. FULL [OUTER] JOIN (全外连接)](#d. FULL [OUTER] JOIN (全外连接))
        • [e. CROSS JOIN (交叉连接 / 笛卡尔积)](#e. CROSS JOIN (交叉连接 / 笛卡尔积))
      • [2. 多表连接 (Joining More Than Two Tables)](#2. 多表连接 (Joining More Than Two Tables))
      • [3. 自连接 (Self Join)](#3. 自连接 (Self Join))
      • [4. 自然连接 (NATURAL JOIN) 和 USING 子句](#4. 自然连接 (NATURAL JOIN) 和 USING 子句)
    • 第二部分:子查询 (Subqueries)
      • [1. 子查询的位置 (Where Subqueries Can Be Used)](#1. 子查询的位置 (Where Subqueries Can Be Used))
      • [2. 子查询的主要类型](#2. 子查询的主要类型)
        • [a. 标量子查询 (Scalar Subquery)](#a. 标量子查询 (Scalar Subquery))
        • [b. 列子查询 (Column Subquery)](#b. 列子查询 (Column Subquery))
        • [c. 行子查询 (Row Subquery)](#c. 行子查询 (Row Subquery))
        • [d. 表子查询 / 派生表 (Table Subquery / Derived Table)](#d. 表子查询 / 派生表 (Table Subquery / Derived Table))
      • [3. 相关子查询 vs. 非相关子查询](#3. 相关子查询 vs. 非相关子查询)
      • [4. EXISTS 和 NOT EXISTS](#4. EXISTS 和 NOT EXISTS)
    • 关键注意事项与最佳实践
    • 总结

📃文章前言

  • 🔷文章均为学习工作中整理的笔记。
  • 🔶如有错误请指正,共同学习进步。

MySQL复杂SQL(多表联查/子查询)详细讲解

MySQL 中复杂 SQL 的核心部分:多表联查子查询。这是数据库操作中处理关联数据的强大工具。

核心目标: 从多个相互关联的表中组合和提取所需的数据。

第一部分:多表联查 (JOIN Operations)

当你的数据模型设计良好(遵循规范化原则)时,数据会分散在多个表中,通过主键-外键关系连接。JOIN 操作就是用来基于这些关系将多个表中的行组合起来。

1. 连接的类型 (JOIN Types)

a. INNER JOIN (内连接 / 等值连接)
  • 作用: 返回两个表中连接字段值相等的所有行组合。如果某行在其中一个表中没有匹配的行,则不会出现在结果中。

  • 语法:

    sql 复制代码
    SELECT 列名列表
    FROM 表1
    [INNER] JOIN 表2 ON 表1.关联字段 = 表2.关联字段
    [WHERE 条件];
    -- INNER 关键字通常可省略
  • 示例: 查询所有有订单的客户信息(假设 customers 表有 customer_idorders 表有 customer_id 外键)

    sql 复制代码
    SELECT c.customer_id, c.name, o.order_id, o.order_date
    FROM customers c
    INNER JOIN orders o ON c.customer_id = o.customer_id;
    -- 结果只包含那些在customers表中有记录且在orders表中至少有一个订单的客户。
  • 图示: 两个集合的交集部分。

b. LEFT [OUTER] JOIN (左外连接)
  • 作用: 返回左表 (表1) 的所有行,即使在右表 (表2) 中没有匹配的行。对于左表中存在而右表中没有匹配的行,右表相关的列将显示为 NULL

  • 语法:

    sql 复制代码
    SELECT 列名列表
    FROM 表1
    LEFT [OUTER] JOIN 表2 ON 表1.关联字段 = 表2.关联字段
    [WHERE 条件];
    -- OUTER 关键字通常可省略
  • 示例: 查询所有客户及其订单(包括没有下过单的客户)

    sql 复制代码
    SELECT c.customer_id, c.name, o.order_id, o.order_date
    FROM customers c
    LEFT JOIN orders o ON c.customer_id = o.customer_id;
    -- 结果包含所有客户。对于没有订单的客户,o.order_id 和 o.order_date 会是 NULL。
  • 图示: 整个左集合 + 与右集合的交集部分。右集合独有的部分被舍弃。

c. RIGHT [OUTER] JOIN (右外连接)
  • 作用:LEFT JOIN 相反。返回右表 (表2) 的所有行,即使在左表 (表1) 中没有匹配的行。对于右表中存在而左表中没有匹配的行,左表相关的列将显示为 NULL

  • 语法:

    sql 复制代码
    SELECT 列名列表
    FROM 表1
    RIGHT [OUTER] JOIN 表2 ON 表1.关联字段 = 表2.关联字段
    [WHERE 条件];
    -- OUTER 关键字通常可省略
  • 示例: 查询所有订单及其对应的客户信息(包括那些可能关联到无效客户的订单 - 这种情况在良好设计的数据模型中较少见,但语法支持)

    sql 复制代码
    SELECT c.customer_id, c.name, o.order_id, o.order_date
    FROM customers c
    RIGHT JOIN orders o ON c.customer_id = o.customer_id;
    -- 结果包含所有订单。如果某个订单的 customer_id 在 customers 表中找不到,则 c.customer_id 和 c.name 会是 NULL。
  • 图示: 整个右集合 + 与左集合的交集部分。左集合独有的部分被舍弃。

  • 注意: RIGHT JOIN 在实际应用中不如 LEFT JOIN 常见,因为通常可以通过调整表顺序使用 LEFT JOIN 达到相同目的。

d. FULL [OUTER] JOIN (全外连接)
  • 作用: 返回左表和右表中的所有行。当某行在另一个表中没有匹配行时,则另一个表相关的列将显示为 NULL。如果两个表中有匹配的行,则进行连接。

  • 语法 (MySQL 不支持直接的 FULL OUTER JOIN,需用 UNION 模拟):

    sql 复制代码
    SELECT 列名列表
    FROM 表1
    LEFT JOIN 表2 ON 表1.关联字段 = 表2.关联字段
    UNION [ALL] -- 通常用 UNION 去重,如果确定不会有重复或需要保留重复则用 UNION ALL
    SELECT 列名列表
    FROM 表1
    RIGHT JOIN 表2 ON 表1.关联字段 = 表2.关联字段
    WHERE 表1.关联字段 IS NULL; -- 排除掉左连接中已包含的匹配行
  • 示例: 查询所有客户和所有订单(包括没有订单的客户和没有对应客户的订单)

    sql 复制代码
    SELECT c.customer_id, c.name, o.order_id, o.order_date
    FROM customers c
    LEFT JOIN orders o ON c.customer_id = o.customer_id
    UNION
    SELECT c.customer_id, c.name, o.order_id, o.order_date
    FROM customers c
    RIGHT JOIN orders o ON c.customer_id = o.customer_id
    WHERE c.customer_id IS NULL; -- 只取右连接中左表为NULL的部分(即orders有而customers没有的行)
  • 图示: 左集合 + 右集合的并集。

e. CROSS JOIN (交叉连接 / 笛卡尔积)
  • 作用: 返回两个表中所有可能的行组合。结果集的行数是 表1行数 * 表2行数通常不是你想要的结果,除非明确需要所有组合。

  • 语法:

    sql 复制代码
    SELECT 列名列表
    FROM 表1
    CROSS JOIN 表2;
    -- 或者使用隐式连接(不推荐):
    SELECT 列名列表
    FROM 表1, 表2;
  • 示例: 生成所有产品和所有尺寸的组合

    sql 复制代码
    SELECT p.product_name, s.size_name
    FROM products p
    CROSS JOIN sizes s;

2. 多表连接 (Joining More Than Two Tables)

  • 可以连续使用多个 JOIN 子句连接多个表。

  • 语法:

    sql 复制代码
    SELECT ...
    FROM 表1
    JOIN 表2 ON 条件
    JOIN 表3 ON 条件 -- 条件可以是表2和表3的关系,或者表1和表3的关系(较少见)
    ...
    [WHERE ...];
  • 示例: 查询订单的详细信息(客户名、订单日期、产品名、数量)

    sql 复制代码
    SELECT c.name, o.order_date, p.product_name, od.quantity
    FROM orders o
    JOIN customers c ON o.customer_id = c.customer_id
    JOIN order_details od ON o.order_id = od.order_id
    JOIN products p ON od.product_id = p.product_id;

3. 自连接 (Self Join)

  • 作用: 将表与其自身连接。常用于表示层次结构(如员工-经理关系、类别-父类别)。

  • 技巧: 需要使用表别名 (Alias) 来区分同一个表的两个"实例"。

  • 示例: 查询员工及其经理的名字

    sql 复制代码
    SELECT e1.employee_name AS Employee, e2.employee_name AS Manager
    FROM employees e1
    LEFT JOIN employees e2 ON e1.manager_id = e2.employee_id;
    -- 使用 LEFT JOIN 是因为顶级经理没有上级(manager_id 为 NULL)

4. 自然连接 (NATURAL JOIN) 和 USING 子句

  • NATURAL JOIN: 自动连接所有同名列。强烈不推荐使用! 因为它依赖于列名匹配,不明确且容易出错。

    sql 复制代码
    SELECT ... FROM table1 NATURAL JOIN table2; -- 避免使用
  • USING 子句: 当连接的两个表具有完全相同名称 的关联字段时,可以用 USING 简化 ON

    sql 复制代码
    SELECT c.customer_id, c.name, o.order_id, o.order_date
    FROM customers c
    JOIN orders o USING (customer_id); -- 等价于 ON c.customer_id = o.customer_id

第二部分:子查询 (Subqueries)

子查询是指嵌套在另一个 SQL 查询(主查询)内部的查询。子查询的结果被外部查询使用。

1. 子查询的位置 (Where Subqueries Can Be Used)

  • SELECT 子句(标量子查询)
  • FROM 子句(派生表/内联视图)
  • WHERE 子句(最常用)
  • HAVING 子句
  • INSERT / UPDATE / DELETE 语句的 VALUESSET 部分

2. 子查询的主要类型

a. 标量子查询 (Scalar Subquery)
  • 特点: 返回单个值(一行一列)。

  • 用途: 可以出现在任何期望单个值的地方(如 SELECT 列表、WHERE 条件中的比较运算符右侧)。

  • 示例: 查询价格高于平均价格的产品

    sql 复制代码
    SELECT product_name, price
    FROM products
    WHERE price > (SELECT AVG(price) FROM products);
  • 示例:SELECT 列表中使用(为每行计算一个相关值)

    sql 复制代码
    SELECT order_id, order_date,
           (SELECT COUNT(*) FROM order_details od WHERE od.order_id = o.order_id) AS item_count
    FROM orders o;
b. 列子查询 (Column Subquery)
  • 特点: 返回单列多行

  • 用途: 常与 IN, ANY/SOME, ALL 运算符一起用在 WHEREHAVING 子句中。

  • 示例 (IN): 查询至少订购过一次'Coffee'产品的客户

    sql 复制代码
    SELECT customer_id, name
    FROM customers
    WHERE customer_id IN (
        SELECT DISTINCT o.customer_id
        FROM orders o
        JOIN order_details od ON o.order_id = od.order_id
        JOIN products p ON od.product_id = p.product_id
        WHERE p.product_name = 'Coffee'
    );
  • 示例 (ANY/SOME): 查询价格大于任何 电子产品价格的非电子产品 (> ANY 等价于 > (SELECT MIN(price) FROM ... WHERE category='Electronics'))

    sql 复制代码
    SELECT product_name, price
    FROM products
    WHERE category <> 'Electronics'
      AND price > ANY (
          SELECT price
          FROM products
          WHERE category = 'Electronics'
      );
  • 示例 (ALL): 查询价格大于所有 电子产品价格的非电子产品 (> ALL 等价于 > (SELECT MAX(price) FROM ... WHERE category='Electronics'))

    sql 复制代码
    SELECT product_name, price
    FROM products
    WHERE category <> 'Electronics'
      AND price > ALL (
          SELECT price
          FROM products
          WHERE category = 'Electronics'
      );
c. 行子查询 (Row Subquery)
  • 特点: 返回单行多列

  • 用途: 与行比较运算符一起使用(较少见)。

  • 示例: 查找与特定员工(ID=123)在同一个部门和同一个职位级别的员工

    sql 复制代码
    SELECT employee_id, name, department, job_level
    FROM employees
    WHERE (department, job_level) = (
        SELECT department, job_level
        FROM employees
        WHERE employee_id = 123
    )
    AND employee_id <> 123; -- 排除自己
d. 表子查询 / 派生表 (Table Subquery / Derived Table)
  • 特点: 返回一个结果集(多行多列)

  • 用途: 必须出现在 FROM 子句中,并且必须有别名。

  • 作用: 简化复杂查询,创建临时中间结果集。

  • 示例: 计算每个类别的平均价格,并找出高于其类别平均价格的产品

    sql 复制代码
    SELECT p.product_id, p.product_name, p.category, p.price, cat_avg.avg_price
    FROM products p
    JOIN (
        SELECT category, AVG(price) AS avg_price
        FROM products
        GROUP BY category
    ) cat_avg ON p.category = cat_avg.category
    WHERE p.price > cat_avg.avg_price;

3. 相关子查询 vs. 非相关子查询

  • 非相关子查询 (Uncorrelated Subquery):
    • 子查询可以独立运行,不依赖于外部查询。
    • 执行过程:先执行子查询得到结果集,然后外部查询使用这个结果集。
    • 上面大部分示例都是非相关的。
  • 相关子查询 (Correlated Subquery):
    • 子查询不能独立运行,它引用了外部查询中的列。

    • 执行过程:外部查询取出一行,传递给子查询;子查询基于外部行中的值执行;外部查询根据子查询返回的结果判断是否保留该行;重复此过程处理外部查询的每一行。

    • 效率提示: 相关子查询通常比非相关子查询或 JOIN 慢,因为它需要对外部查询的每一行都执行一次子查询。优化时需谨慎。

    • 示例: 查询那些订单总额超过 1000 的客户 (在 WHERE 中使用相关子查询)

      sql 复制代码
      SELECT c.customer_id, c.name
      FROM customers c
      WHERE EXISTS (
          SELECT 1
          FROM orders o
          WHERE o.customer_id = c.customer_id -- 关联条件
          GROUP BY o.customer_id
          HAVING SUM(o.total_amount) > 1000
      );
      -- 或者更高效的方式可能是使用 JOIN + GROUP BY + HAVING
    • 示例:SELECT 列表中使用相关子查询 (如之前的 item_count 例子)

4. EXISTS 和 NOT EXISTS

  • 专门用于相关子查询(但也可以用于非相关)。

  • EXISTS (subquery): 如果子查询返回至少一行 ,则结果为 TRUE

  • NOT EXISTS (subquery): 如果子查询返回零行 ,则结果为 TRUE

  • 非常高效,因为只要子查询找到一行匹配,EXISTS 就立即返回 TRUE,不需要处理所有结果。

  • 示例 (EXISTS): 查询至少下过一个订单的客户 (等价于前面的 IN 示例,但可能更高效)

    sql 复制代码
    SELECT customer_id, name
    FROM customers c
    WHERE EXISTS (
        SELECT 1
        FROM orders o
        WHERE o.customer_id = c.customer_id -- 关联条件
    );
  • 示例 (NOT EXISTS): 查询从未下过订单的客户

    sql 复制代码
    SELECT customer_id, name
    FROM customers c
    WHERE NOT EXISTS (
        SELECT 1
        FROM orders o
        WHERE o.customer_id = c.customer_id -- 关联条件
    );

关键注意事项与最佳实践

  1. 明确连接条件 (ON Clause): 总是显式地写出连接条件 (ONUSING)。避免隐式连接(逗号分隔表名)和 NATURAL JOIN,它们容易出错且不清晰。
  2. 表别名 (Aliases): 在多表查询和子查询中,使用简短、有意义的表别名 (FROM table AS aliasFROM table alias)。这能极大提高可读性和避免列名歧义。
  3. 性能考虑:
    • 索引是关键! 确保连接字段 (ON 子句中的列) 和 WHERE 子句中频繁过滤的列上有索引。
    • 理解 INNER JOIN 通常比 OUTER JOIN 快。
    • 相关子查询可能导致性能问题(Nested Loops)。如果可能,尝试将其重写为 JOIN
    • 大型 IN 子查询可能效率低下,考虑用 JOINEXISTS 替代。
    • 派生表(FROM 中的子查询)可能会阻止某些优化。有时可以用 WITH (Common Table Expression - CTE) 在 MySQL 8.0+ 中更清晰地表达。
  4. NULL 值处理: 在连接条件 (ON) 或 WHERE 子句中使用涉及可能为 NULL 的列进行比较时(如 col1 = col2),如果 col1col2NULL,该行通常不会 匹配(因为 NULL = NULLUNKNOWN/NULL)。如果需要匹配 NULL,需使用 IS NULL 显式处理。
  5. 可读性和维护性:
    • 合理缩进和格式化复杂的 SQL。
    • 分解非常复杂的查询。使用 CTE (WITH 子句,MySQL 8.0+) 或临时视图(如果支持)将查询步骤模块化。
    • 注释解释复杂的逻辑。
  6. 测试: 逐步构建复杂查询。先从一个简单的部分开始,验证结果,然后逐步添加 JOIN 或子查询。使用 LIMIT 测试大数据集查询的性能。
  7. 选择 JOIN 还是子查询? 没有绝对答案。通常:
    • 需要组合多个表的数据显示时,JOIN 更自然。
    • 用于过滤或计算聚合值的条件检查时,子查询(尤其是 EXISTS/NOT EXISTS)可能更直观或更高效。
    • 分析执行计划 (EXPLAIN) 是确定哪种方式性能更好的最终手段。

总结

掌握多表联查 (INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN) 和子查询(标量、列、行、表子查询、相关/非相关、EXISTS/NOT EXISTS)是进行复杂数据库查询的基础。理解它们的工作原理、适用场景以及性能影响至关重要。通过实践、关注索引、编写清晰的 SQL 并利用 EXPLAIN 分析,你将能够高效地从关联的数据库表中提取所需的信息。记住,清晰性和性能往往是相辅相成的。


📜文末寄语

  • 🟠关注我,获取更多内容。
  • 🟡技术动态、实战教程、问题解决方案等内容持续更新中。
  • 🟢《全栈知识库》技术交流和分享社区,集结全栈各领域开发者,期待你的加入。
  • 🔵加入开发者的《专属社群》,分享交流,技术之路不再孤独,一起变强。
  • 🟣点击下方名片获取更多内容🍭🍭🍭👇

相关推荐
在未来等你3 分钟前
SQL进阶之旅 Day 21:临时表与内存表应用
sql·mysql·postgresql·database·temporary-table·memory-table·sql-optimization
zhuiQiuMX1 小时前
分享今天做的力扣SQL题
sql·算法·leetcode
小Tomkk2 小时前
阿里云 RDS mysql 5.7 怎么 添加白名单 并链接数据库
数据库·mysql·阿里云
老纪的技术唠嗑局3 小时前
重剑无锋,大巧不工 —— OceanBase 中的 Nest Loop Join 使用技巧分享
数据库·sql
冰橙子id4 小时前
centos7编译安装LNMP架构
mysql·nginx·架构·centos·php
玛奇玛丶4 小时前
面试官:千万级订单表新增字段怎么弄?
后端·mysql
nanzhuhe5 小时前
sql中group by使用场景
数据库·sql·数据挖掘
天天摸鱼的java工程师5 小时前
从被测试小姐姐追着怼到运维小哥点赞:我在项目管理系统的 MySQL 优化实战
java·后端·mysql
消失在人海中5 小时前
oracle sql 语句 优化方法
数据库·sql·oracle