MySQL 多表查询

核心目标: 学习如何使用 JOINUNION、自连接和子查询等技术,从一个或多个表中检索和组合复杂的数据。

前提:

  1. 熟悉单表 SELECT 查询 (SELECT, FROM, WHERE, ORDER BY, LIMIT 等)。
  2. 理解表间关联(主键-外键)。

1. 表连接 (JOIN)

作用:将来自两个或多个表中的行,基于它们之间的相关列组合起来。

(1.1) 内连接 (INNER JOINJOIN)

作用:返回两个(或多个)表中 满足连接条件 的行。不匹配的行被排除。最常用。
语法:

select column_list
from table1
inner join table2 on join_condition;

示例:

-- 查询学生及其专业名称 (只显示有匹配专业的学生)
select s.student_name, m.major_name
from students s
inner join majors m on s.major_id = m.major_id;
(1.2) 外连接 (OUTER JOIN)
  • 左连接 (LEFT JOINLEFT OUTER JOIN)
    作用:返回 左表的所有行,以及右表中匹配的行。右表无匹配时,其列值为 NULL
    语法:
select column_list
from table1 -- 左表
left join table2 on join_condition;

示例:

-- 查询所有学生及其专业 (没有专业的学生也会显示)
select s.student_name, m.major_name
from students s
left join majors m on s.major_id = m.major_id;
  • 右连接 (RIGHT JOINRIGHT OUTER JOIN)
    作用:返回 右表的所有行,以及左表中匹配的行。左表无匹配时,其列值为 NULL
    语法:
select column_list
from table1
right join table2 on join_condition; -- 右表

示例:

-- 查询所有专业及其学生 (没有学生的专业也会显示)
select s.student_name, m.major_name
from students s
right join majors m on s.major_id = m.major_id;
  • (了解) 全外连接 (FULL OUTER JOIN)
    作用:返回左表和右表中 所有 的行。无匹配的对应列为 NULL
    语法:MySQL 不直接支持。通过 LEFT JOIN UNION RIGHT JOIN 模拟。
    模拟语法:
select column_list from table1 left join table2 on join_condition
union
select column_list from table1 right join table2 on join_condition;
(1.3) (了解) 交叉连接 (CROSS JOIN)

作用:返回两表的笛卡尔积(所有可能的行组合)。谨慎使用。
语法:

select column_list from table1 cross join table2;
(1.4) 使用 USING 子句

当连接的两个表中用于匹配的列名称完全相同时,可用 USING(common_column_name) 简化 ON
语法:

select ... from table1 join table2 using(common_column_name);

2. 自连接 (Self Join)

作用:将一个表连接到它自身,用于查询表内行与行之间的关系(如员工与经理)。必须使用表别名。
语法:

select alias1.column, alias2.column
from table_name alias1
join table_name alias2 on alias1.related_column = alias2.related_column;

示例 (查询员工及其经理):

select
e.emp_name as employee_name,
m.emp_name as manager_name
from
employees e
left join
employees m on e.manager_id = m.emp_id;

3. 联合查询 (UNION / UNION ALL)

作用:合并两个或多个 SELECT 语句的结果集。
语法:

select column_list1 from table1 ...
union | union all
select column_list2 from table2 ...
[order by ...]
[limit ...]

规则:

  • 所有 SELECT 语句列数相同。
  • 对应列数据类型兼容。
  • 列顺序相同。
  • UNION 去重,UNION ALL 不去重。

示例:

-- 合并活跃客户和潜在客户邮箱 (去重)
select email from active_customers
union
select email from potential_customers;

-- 合并不同年份销售记录 (不去重)
select product_id, sale_date, amount from sales_2023
union all
select product_id, sale_date, amount from sales_2024;

4. 子查询 (Subqueries)

作用:嵌套在其他 SQL 语句中的查询。根据返回结果的结构,可分为以下几类:

(4.1) 标量子查询 (Scalar Subquery)

定义:返回 单个值(即一行一列)的子查询。
用途:可以用在任何需要单个值的地方,如 SELECT 列表、WHERE 子句的比较条件、SET 子句等。

示例:

-- WHERE 子句:查询工资高于平均工资的员工
select emp_name, salary
from employees
where salary > (select avg(salary) from employees); -- 子查询返回平均工资(单个值)

-- SELECT 列表:查询每个学生姓名及其选修的课程总数
select
s.student_name,
(select count(*) from enrollments e where e.student_id = s.student_id) as courses_taken -- 子查询计算每个学生的选课数(单个值)
from students s;

-- SET 子句 (在 UPDATE 中): 将所有员工工资设为平均工资 (危险操作示例)
-- update employees set salary = (select avg(salary) from employees);
(4.2) 列子查询 (Column Subquery)

定义:返回 单列多行 的子查询。
用途:主要用于 WHERE 子句中与 IN, NOT IN, ANY, SOME, ALL 等操作符结合使用。

示例:

-- 使用 IN:查询选修了 'CS101' 或 'MA201' 课程的学生姓名
select student_name
from students
where student_id in (select student_id from enrollments where course_id in ('CS101', 'MA201')); -- 子查询返回选了这两门课的学生 ID 列表

-- 使用 NOT IN:查询没有下过订单的客户
select customer_name
from customers
where customer_id not in (select distinct customer_id from orders); -- 子查询返回下过单的客户 ID 列表

-- 使用 ANY/SOME:查询工资高于 IT 部门(dept_id=2)任意一个员工工资的其他部门员工
-- (注意: 不包括 IT 部门自身员工)
select emp_name, salary
from employees
where salary > any (select salary from employees where dept_id = 2)
and dept_id != 2; -- 或 dept_id is null

-- 使用 ALL:查询工资高于 IT 部门所有员工工资的员工
select emp_name, salary
from employees
where salary > all (select salary from employees where dept_id = 2);
(4.3) 行子查询 (Row Subquery)

定义:返回 单行多列 的子查询。
用途:主要用于 WHERE 子句中进行行级别的比较。需要使用括号将多个列括起来进行比较。

示例:

-- 查询与 ID 为 101 的员工 部门和经理都相同的其他员工
select emp_name
from employees
where (dept_id, manager_id) = (select dept_id, manager_id from employees where emp_id = 101)
and emp_id != 101; -- 排除员工 101 自己

-- 查找选修了与学生 'Alice' 完全相同的一组课程的学生 (这比较复杂,通常用 JOIN 或 EXISTS 更优,仅作行子查询示例)
-- (假设能查询出 Alice 选的所有课作为一个 "行" - 实际场景复杂)
-- select ... where (col1, col2, ...) = (select col1, col2, ... from ... where ...)

注意:行子查询的直接比较在某些场景下可能效率不高或不易理解,常有替代方案。

(4.4) 表子查询 (Table Subquery / Derived Table)

定义:返回 多行多列(即一个结果集)的子查询。
用途:主要用在 FROM 子句中,将子查询的结果当作一个临时的表(必须为其指定别名)来进一步查询。也可用在 JOIN 子句中。

示例:

-- FROM 子句:查询每个专业的平均 GPA,然后找出平均 GPA 大于 3.5 的专业
select major_avg.major_name, major_avg.avg_gpa
from (
select m.major_name, avg(s.gpa) as avg_gpa -- 子查询计算每个专业的平均 GPA
from students s
join majors m on s.major_id = m.major_id
group by m.major_name
) as major_avg -- 必须为派生表指定别名
where major_avg.avg_gpa > 3.5;

-- JOIN 子句:查询每个部门中工资最高的员工信息
select e.emp_name, e.salary, d.dept_name
from employees e
inner join (
select dept_id, max(salary) as max_salary -- 子查询找出每个部门的最高工资
from employees
where dept_id is not null
group by dept_id
) as dept_max_salary
on e.dept_id = dept_max_salary.dept_id and e.salary = dept_max_salary.max_salary -- 连接条件:部门ID相同且工资等于该部门最高工资
inner join departments d on e.dept_id = d.dept_id;
(4.5) EXISTS / NOT EXISTS 结合子查询

EXISTS 操作符用于检查子查询是否返回任何行。如果子查询返回至少一行,EXISTS 返回 TRUE;否则返回 FALSE。NOT EXISTS 相反。这通常用于替代 IN 进行存在性检查,效率可能更高。子查询通常是相关子查询(依赖外部查询的值)。

示例:

-- EXISTS:查询至少选修了一门课程的学生
select s.student_name
from students s
where exists (select 1 from enrollments e where e.student_id = s.student_id limit 1); -- 检查是否存在该学生的选课记录

-- NOT EXISTS:查询没有员工的部门
select d.dept_name
from departments d
where not exists (select 1 from employees e where e.dept_id = d.dept_id limit 1); -- 检查是否不存在该部门的员工记录

练习题 (Practice Exercises - Advanced Queries with Answers)

继续使用 employeesdepartments 表。

employees 表:

emp_id emp_name dept_id salary manager_id
101 Alice 1 60000 102
102 Bob 2 75000 NULL
103 Charlie 1 65000 101
104 David NULL 50000 102
105 Eve 2 80000 102

departments 表:

dept_id dept_name location
1 HR New York
2 IT London
3 Finance New York
  1. (Self Join) 查询所有员工及其直接经理的姓名。如果员工没有经理,也要显示该员工。
    答案:

    select e.emp_name as employee_name, m.emp_name as manager_name
    from employees e
    left join employees m on e.manager_id = m.emp_id;
    
  2. (Column Subquery with IN) 查询所有在 ‘New York’ 地点工作的员工的姓名和工资。
    答案:

    select emp_name, salary
    from employees
    where dept_id in (select dept_id from departments where location = 'New York');
    
  3. (Scalar Subquery) 查询工资最高的员工的姓名和工资。
    答案:

    select emp_name, salary
    from employees
    where salary = (select max(salary) from employees);
    
  4. (Scalar Subquery in SELECT) 查询每个部门的名称以及该部门的员工人数。
    答案:

    select
    d.dept_name,
    (select count(*) from employees e where e.dept_id = d.dept_id) as employee_count
    from departments d;
    
  5. (Table Subquery / Derived Table) 先计算每个部门的平均工资,然后找出平均工资超过 70000 的部门名称及其平均工资。
    答案:

    select dept_avg.dept_name, dept_avg.avg_salary
    from (
    select d.dept_name, avg(e.salary) as avg_salary
    from employees e
    join departments d on e.dept_id = d.dept_id
    group by d.dept_name
    ) as dept_avg
    where dept_avg.avg_salary > 70000;
    
  6. (UNION) 假设有一个 retired_employees 表结构类似 employees,合并当前员工和退休员工的姓名列表(去除重复)。
    答案: (假设 retired_employees 表存在)

    select emp_name from employees
    union
    select emp_name from retired_employees;
    
  7. (EXISTS) 查询哪些部门至少有一名员工。
    答案:

    select d.dept_name
    from departments d
    where exists (select 1 from employees e where e.dept_id = d.dept_id limit 1);
    
  8. (Column Subquery with ALL) 查询工资比 HR 部门(dept_id=1)所有员工都高的员工姓名和工资。
    答案:

    select emp_name, salary
    from employees
    where salary > all (select salary from employees where dept_id = 1);
    

你可能感兴趣的:(SQL教程,mysql,数据库,sql,oracle)