🤟致敬读者
- 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉
📘博主相关
- 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息
文章目录
- MySQL复杂SQL(多表联查/子查询)详细讲解
- 第一部分:多表联查 (JOIN Operations)
- 1. 连接的类型 (JOIN Types)
- a. INNER JOIN (内连接 / 等值连接)
- b. LEFT [OUTER] JOIN (左外连接)
- c. RIGHT [OUTER] JOIN (右外连接)
- d. FULL [OUTER] JOIN (全外连接)
- e. CROSS JOIN (交叉连接 / 笛卡尔积)
- 2. 多表连接 (Joining More Than Two Tables)
- 3. 自连接 (Self Join)
- 4. 自然连接 (NATURAL JOIN) 和 USING 子句
- 第二部分:子查询 (Subqueries)
- 1. 子查询的位置 (Where Subqueries Can Be Used)
- 2. 子查询的主要类型
- a. 标量子查询 (Scalar Subquery)
- b. 列子查询 (Column Subquery)
- c. 行子查询 (Row Subquery)
- d. 表子查询 / 派生表 (Table Subquery / Derived Table)
- 3. 相关子查询 vs. 非相关子查询
- 4. EXISTS 和 NOT EXISTS
- 关键注意事项与最佳实践
- 总结
📃文章前言
- 🔷文章均为学习工作中整理的笔记。
- 🔶如有错误请指正,共同学习进步。
MySQL复杂SQL(多表联查/子查询)详细讲解
MySQL 中复杂 SQL 的核心部分:多表联查和子查询。这是数据库操作中处理关联数据的强大工具。
核心目标: 从多个相互关联的表中组合和提取所需的数据。
第一部分:多表联查 (JOIN Operations)
当你的数据模型设计良好(遵循规范化原则)时,数据会分散在多个表中,通过主键-外键关系连接。JOIN 操作就是用来基于这些关系将多个表中的行组合起来。
1. 连接的类型 (JOIN Types)
a. INNER JOIN (内连接 / 等值连接)
- 作用: 返回两个表中连接字段值相等的所有行组合。如果某行在其中一个表中没有匹配的行,则不会出现在结果中。
- 语法:
SELECT 列名列表 FROM 表1 [INNER] JOIN 表2 ON 表1.关联字段 = 表2.关联字段 [WHERE 条件]; -- INNER 关键字通常可省略
- 示例: 查询所有有订单的客户信息(假设
customers
表有customer_id
,orders
表有customer_id
外键)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
。 - 语法:
SELECT 列名列表 FROM 表1 LEFT [OUTER] JOIN 表2 ON 表1.关联字段 = 表2.关联字段 [WHERE 条件]; -- OUTER 关键字通常可省略
- 示例: 查询所有客户及其订单(包括没有下过单的客户)
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
。 - 语法:
SELECT 列名列表 FROM 表1 RIGHT [OUTER] JOIN 表2 ON 表1.关联字段 = 表2.关联字段 [WHERE 条件]; -- OUTER 关键字通常可省略
- 示例: 查询所有订单及其对应的客户信息(包括那些可能关联到无效客户的订单 - 这种情况在良好设计的数据模型中较少见,但语法支持)
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 模拟):
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; -- 排除掉左连接中已包含的匹配行
- 示例: 查询所有客户和所有订单(包括没有订单的客户和没有对应客户的订单)
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行数
。通常不是你想要的结果,除非明确需要所有组合。 - 语法:
SELECT 列名列表 FROM 表1 CROSS JOIN 表2; -- 或者使用隐式连接(不推荐): SELECT 列名列表 FROM 表1, 表2;
- 示例: 生成所有产品和所有尺寸的组合
SELECT p.product_name, s.size_name FROM products p CROSS JOIN sizes s;
2. 多表连接 (Joining More Than Two Tables)
- 可以连续使用多个
JOIN
子句连接多个表。 - 语法:
SELECT ... FROM 表1 JOIN 表2 ON 条件 JOIN 表3 ON 条件 -- 条件可以是表2和表3的关系,或者表1和表3的关系(较少见) ... [WHERE ...];
- 示例: 查询订单的详细信息(客户名、订单日期、产品名、数量)
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) 来区分同一个表的两个“实例”。
- 示例: 查询员工及其经理的名字
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: 自动连接所有同名列。强烈不推荐使用! 因为它依赖于列名匹配,不明确且容易出错。
SELECT ... FROM table1 NATURAL JOIN table2; -- 避免使用
- USING 子句: 当连接的两个表具有完全相同名称的关联字段时,可以用
USING
简化ON
。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
语句的VALUES
或SET
部分
2. 子查询的主要类型
a. 标量子查询 (Scalar Subquery)
- 特点: 返回单个值(一行一列)。
- 用途: 可以出现在任何期望单个值的地方(如
SELECT
列表、WHERE
条件中的比较运算符右侧)。 - 示例: 查询价格高于平均价格的产品
SELECT product_name, price FROM products WHERE price > (SELECT AVG(price) FROM products);
- 示例: 在
SELECT
列表中使用(为每行计算一个相关值)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
运算符一起用在WHERE
或HAVING
子句中。 - 示例 (IN): 查询至少订购过一次’Coffee’产品的客户
SELECT customer_id, name FROM customers WHERE customer_id IN (SELECT DISTINCT o.customer_idFROM orders oJOIN order_details od ON o.order_id = od.order_idJOIN products p ON od.product_id = p.product_idWHERE p.product_name = 'Coffee' );
- 示例 (ANY/SOME): 查询价格大于任何电子产品价格的非电子产品 (
> ANY
等价于> (SELECT MIN(price) FROM ... WHERE category='Electronics')
)SELECT product_name, price FROM products WHERE category <> 'Electronics'AND price > ANY (SELECT priceFROM productsWHERE category = 'Electronics');
- 示例 (ALL): 查询价格大于所有电子产品价格的非电子产品 (
> ALL
等价于> (SELECT MAX(price) FROM ... WHERE category='Electronics')
)SELECT product_name, price FROM products WHERE category <> 'Electronics'AND price > ALL (SELECT priceFROM productsWHERE category = 'Electronics');
c. 行子查询 (Row Subquery)
- 特点: 返回单行多列。
- 用途: 与行比较运算符一起使用(较少见)。
- 示例: 查找与特定员工(ID=123)在同一个部门和同一个职位级别的员工
SELECT employee_id, name, department, job_level FROM employees WHERE (department, job_level) = (SELECT department, job_levelFROM employeesWHERE employee_id = 123 ) AND employee_id <> 123; -- 排除自己
d. 表子查询 / 派生表 (Table Subquery / Derived Table)
- 特点: 返回一个结果集(多行多列)。
- 用途: 必须出现在
FROM
子句中,并且必须有别名。 - 作用: 简化复杂查询,创建临时中间结果集。
- 示例: 计算每个类别的平均价格,并找出高于其类别平均价格的产品
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_priceFROM productsGROUP 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
中使用相关子查询)SELECT c.customer_id, c.name FROM customers c WHERE EXISTS (SELECT 1FROM orders oWHERE o.customer_id = c.customer_id -- 关联条件GROUP BY o.customer_idHAVING 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
示例,但可能更高效)SELECT customer_id, name FROM customers c WHERE EXISTS (SELECT 1FROM orders oWHERE o.customer_id = c.customer_id -- 关联条件 );
- 示例 (NOT EXISTS): 查询从未下过订单的客户
SELECT customer_id, name FROM customers c WHERE NOT EXISTS (SELECT 1FROM orders oWHERE o.customer_id = c.customer_id -- 关联条件 );
关键注意事项与最佳实践
- 明确连接条件 (ON Clause): 总是显式地写出连接条件 (
ON
或USING
)。避免隐式连接(逗号分隔表名)和NATURAL JOIN
,它们容易出错且不清晰。 - 表别名 (Aliases): 在多表查询和子查询中,使用简短、有意义的表别名 (
FROM table AS alias
或FROM table alias
)。这能极大提高可读性和避免列名歧义。 - 性能考虑:
- 索引是关键! 确保连接字段 (
ON
子句中的列) 和WHERE
子句中频繁过滤的列上有索引。 - 理解
INNER JOIN
通常比OUTER JOIN
快。 - 相关子查询可能导致性能问题(Nested Loops)。如果可能,尝试将其重写为
JOIN
。 - 大型
IN
子查询可能效率低下,考虑用JOIN
或EXISTS
替代。 - 派生表(
FROM
中的子查询)可能会阻止某些优化。有时可以用WITH
(Common Table Expression - CTE) 在 MySQL 8.0+ 中更清晰地表达。
- 索引是关键! 确保连接字段 (
- NULL 值处理: 在连接条件 (
ON
) 或WHERE
子句中使用涉及可能为NULL
的列进行比较时(如col1 = col2
),如果col1
或col2
为NULL
,该行通常不会匹配(因为NULL = NULL
是UNKNOWN
/NULL
)。如果需要匹配NULL
,需使用IS NULL
显式处理。 - 可读性和维护性:
- 合理缩进和格式化复杂的 SQL。
- 分解非常复杂的查询。使用 CTE (
WITH
子句,MySQL 8.0+) 或临时视图(如果支持)将查询步骤模块化。 - 注释解释复杂的逻辑。
- 测试: 逐步构建复杂查询。先从一个简单的部分开始,验证结果,然后逐步添加
JOIN
或子查询。使用LIMIT
测试大数据集查询的性能。 - 选择 JOIN 还是子查询? 没有绝对答案。通常:
- 需要组合多个表的数据显示时,
JOIN
更自然。 - 用于过滤或计算聚合值的条件检查时,子查询(尤其是
EXISTS
/NOT EXISTS
)可能更直观或更高效。 - 分析执行计划 (
EXPLAIN
) 是确定哪种方式性能更好的最终手段。
- 需要组合多个表的数据显示时,
总结
掌握多表联查 (INNER JOIN
, LEFT JOIN
, RIGHT JOIN
, FULL JOIN
) 和子查询(标量、列、行、表子查询、相关/非相关、EXISTS
/NOT EXISTS
)是进行复杂数据库查询的基础。理解它们的工作原理、适用场景以及性能影响至关重要。通过实践、关注索引、编写清晰的 SQL 并利用 EXPLAIN
分析,你将能够高效地从关联的数据库表中提取所需的信息。记住,清晰性和性能往往是相辅相成的。
📜文末寄语
- 🟠关注我,获取更多内容。
- 🟡技术动态、实战教程、问题解决方案等内容持续更新中。
- 🟢《全栈知识库》技术交流和分享社区,集结全栈各领域开发者,期待你的加入。
- 🔵加入开发者的《专属社群》,分享交流,技术之路不再孤独,一起变强。
- 🟣点击下方名片获取更多内容🍭🍭🍭👇