1. 概述
MySQL中的多表查询是指在一个查询中同时使用两个或多个表,通过某种关系将它们连接起来,以检索所需的数据。多表查询在数据库操作中非常常见,尤其是在处理复杂的数据关系时。
简单讲就是:多表查询是指从多个表中检索数据。当你需要从多个表中获取信息并将它们组合在一起时,可以使用多表查询。这通常通过使用JOIN子句来实现。
例如,假设我们有两个表:一个是“学生”表,包含学生的信息;另一个是“课程”表,包含课程的信息。如果我们想找出哪些学生选修了特定的课程,我们可以使用多表查询来连接这两个表,并检索相关的信息。
2. 多表关系
在项目开发中,特别是在设计数据库时,我们需要根据业务需求和业务模块之间的关系来构建表结构。因为不同的业务模块之间是有关联的,所以这些表结构之间也会有各种关联。基本上,这些关联可以分为三种:
- 一对一关系(One-to-One):这意味着一个表中的记录与另一个表中的记录直接对应。例如,每个人都有一个独特的身份证号,所以“用户”表和“身份证信息”表之间可能存在一对一关系。
- 一对多关系(One-to-Many):一个表中的记录可以与另一个表中的多个记录相关联。例如,一个班级有多个学生,所以“班级”表与“学生”表之间存在一对多关系。
- 多对多关系(Many-to-Many):一个表中的记录可以与另一个表中的多个记录相关联,反之亦然。例如,一个学生可以选择多门课程,而一门课程也可以有多个学生选择。因此,“学生”表和“课程”表之间存在多对多关系。
2.1. 一对一
-- 创建用户表
CREATE TABLE Users (
UserID INT PRIMARY KEY,
Username VARCHAR(50),
-- 其他用户相关字段...
);
-- 创建身份证信息表
CREATE TABLE IDInfo (
IDInfoID INT PRIMARY KEY,
UserID INT,
IDNumber VARCHAR(18),
-- 其他身份证信息相关字段...
FOREIGN KEY (UserID) REFERENCES Users(UserID)
);
- Users 表和 IDInfo 表之间是一对一的关系,因为每个人都有一个唯一的身份证号。
- UserID 是 Users 表的主键,而在 IDInfo 表中,UserID 是外键,它引用了 Users 表中的 UserID。这意味着每个记录在 IDInfo 表中都必须有一个与之对应的 UserID 在 Users 表中。
- 如果某个用户没有身份证信息,则可以在 Users 表中创建记录,但在 IDInfo 表中不创建记录。或者反过来,如果某人的身份证信息是保密的或未收集,则可以在 IDInfo 表中创建记录,但在 Users 表中不创建记录。但两者之间不能同时没有记录,因为它们之间是一对一的关系。
2.2. 一对多
-- 创建班级表
CREATE TABLE Classes (
ClassID INT PRIMARY KEY,
ClassName VARCHAR(50),
-- 其他班级相关字段...
);
-- 创建学生表
CREATE TABLE Students (
StudentID INT PRIMARY KEY,
StudentName VARCHAR(50),
ClassID INT,
FOREIGN KEY (ClassID) REFERENCES Classes(ClassID)
-- 其他学生相关字段...
);
Classes 表和 Students 表之间存在一对多关系。一个班级可以有多个学生,但每个学生只属于一个班级。这通过在 Students 表中添加一个外键 ClassID 来实现,该外键引用 Classes 表的 ClassID 主键。通过这种方式,确保每个学生都与一个班级相关联,但每个班级可以有多个学生。
2.3. 多对多
-- 创建学生表
CREATE TABLE Students (
StudentID INT PRIMARY KEY,
StudentName VARCHAR(50),
-- 其他学生相关字段...
);
-- 创建课程表
CREATE TABLE Courses (
CourseID INT PRIMARY KEY,
CourseName VARCHAR(50),
-- 其他课程相关字段...
);
-- 创建学生选课表,这是一个典型的“多对多”关系表,也称为“关联表”或“联接表”
CREATE TABLE StudentCourses (
StudentID INT,
CourseID INT,
FOREIGN KEY (StudentID) REFERENCES Students(StudentID),
FOREIGN KEY (CourseID) REFERENCES Courses(CourseID)
);
Students 表和 Courses 表之间存在多对多关系。一个学生可以选择多门课程,而一门课程也可以有多个学生选择。这种关系通过一个关联表 StudentCourses 来实现,该表记录了哪些学生选择了哪些课程。在这个关联表中,StudentID 和 CourseID 都是外键,分别引用 Students 表和 Courses 表的主键。通过这种方式,确保每个学生和课程之间的多对多关系得以实现。
3. 多表查询
3.1. 数据准备
-- 创建dept表,并插入数据
create table dept(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '部门名称'
)comment '部门表';
INSERT INTO dept (id, name) VALUES (1, '研发部'), (2, '市场部'),(3, '财务部'), (4,'销售部'), (5, '总经办'), (6, '人事部');
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
-- 创建emp表,并插入数据
create table emp(
id int auto_increment comment 'ID' primary key,
name varchar(50) not null comment '姓名',
age int comment '年龄',
job varchar(20) comment '职位',
salary int comment '薪资',
entrydate date comment '入职时间',
managerid int comment '直属领导ID',
dept_id int comment '部门ID'
)comment '员工表';
-- 添加外键
alter table emp add constraint fk_emp_dept_id foreign key (dept_id) references dept(id);
INSERT INTO emp (id, name, age, job,salary, entrydate, managerid, dept_id)
VALUES
(1, '金庸', 66, '总裁',20000, '2000-01-01', null,5),
(2, '张无忌', 20, '项目经理',12500, '2005-12-05', 1,1),
(3, '杨逍', 33, '开发', 8400,'2000-11-03', 2,1),
(4, '韦一笑', 48, '开发',11000, '2002-02-05', 2,1),
(5, '常遇春', 43, '开发',10500, '2004-09-07', 3,1),
(6, '小昭', 19, '程序员鼓励师',6600, '2004-10-12', 2,1),
(7, '灭绝', 60, '财务总监',8500, '2002-09-12', 1,3),
(8, '周芷若', 19, '会计',48000, '2006-06-02', 7,3),
(9, '丁敏君', 23, '出纳',5250, '2009-05-13', 7,3),
(10, '赵敏', 20, '市场部总监',12500, '2004-10-12', 1,2),
(11, '鹿杖客', 56, '职员',3750, '2006-10-03', 10,2),
(12, '鹤笔翁', 19, '职员',3750, '2007-05-09', 10,2),
(13, '方东白', 19, '职员',5500, '2009-02-12', 10,2),
(14, '张三丰', 88, '销售总监',14000, '2004-10-12', 1,4),
(15, '俞莲舟', 38, '销售',4600, '2004-10-12', 14,4),
(16, '宋远桥', 40, '销售',4600, '2004-10-12', 14,4),
(17, '陈友谅', 42, null,2000, '2011-10-12', 1,null);
3.2. 多表查询
原来查询单表数据,执行的SQL形式为:select * from emp;
那么我们要执行多表查询,就只需要使用逗号分隔多张表即可如:
select * from emp, dept;
此时,我们看到查询结果中包含了大量的结果集,总共102条记录,而这其实就是员工表emp所有的记录(17) 与 部门表dept所有记录(6) 的所有组合情况,这种现象称之为笛卡尔积。
笛卡尔积:
简单讲,当我们提到两个表的笛卡尔积时,我们是指这两个表中的每一行与另一个表中的每一行进行组合的所有可能情况。
例如,假设你有两个表A和B。
- 表A有a行
- 表B有b行
那么,A和B的笛卡尔积将有a×b行。这是因为对于表A中的每一行,你都可以与表B中的每一行进行组合。
具体到您给出的例子中:
- 员工表emp有17行
- 部门表dept有6行
所以,emp和dept的笛卡尔积将有17×6 = 102行。这就是为什么查询结果中包含了102条记录,即使这并不是我们通常想要的结果。
为了避免这种笛卡尔积,我们通常需要指定两个表之间的关联条件。
比如这样:
select * from emp, dept where emp.dept.id == dept.id;
这里为什么没有17号员工——他的部门id是null值,不符合where条件
3.3. 多表查询分类
- 内连接:相当于查询A、B交集部分数据(C部分)
- 外连接:
-
- 左外连接:查询左表所有数据,以及两张表交集部分数据(A + C 部分)
- 右外连接:查询右表所有数据,以及两张表交集部分数据(B + C 部分)
- 自连接:当前表与自身的连接查询,自连接必须使用表别名
3.4. 内连接
内连接查询的是两张表交集部分的数据。(C部分)
内连接的语法分为两种: 隐式内连接、显式内连接。
隐式内连接:使用逗号(,)分隔表名,并在WHERE子句中指定连接条件。
SELECT 字段列表
FROM 表1, 表2
WHERE 表1.字段 = 表2.字段;
显式内连接:使用 INNER JOIN关键字明确指出连接操作,并在ON子句中指定连接条件。
(其中inner可以省略)
SELECT 字段列表
FROM 表1
[INNER] JOIN 表2
ON 表1.字段 = 衡2.字段;
3.4.1. 隐/显式内连接
隐式内连接和显式内连接都是在SQL中实现内连接的方式,它们的主要区别在于语法格式和可读性、性能方面:
可读性:
- 隐式内连接:由于连接条件与WHERE子句中的其他过滤条件混合在一起,对于复杂查询或多个表的连接,可读性较差,不易于理解和维护。
- 显式内连接:通过明确的JOIN关键字和ON子句分离连接条件和过滤条件,使得查询语句结构更清晰,可读性更好。
性能:
- 隐式内连接:在执行时,数据库系统首先会做笛卡尔积(即两个表的所有行组合),然后再根据WHERE子句中的条件进行筛选,这在大数据量下可能会导致性能问题。
- 显式内连接:数据库系统通常能够更有效地处理显式内连接,因为它可以直接根据ON子句中的连接条件进行匹配,避免了不必要的笛卡尔积操作。在大数据量下,显式内连接通常会有更好的性能。
因此,虽然两种内连接都能达到相同的数据查询结果,但显式内连接在可读性和性能上通常更优。
现代的SQL编程实践中,更推荐使用显式内连接,因为它的语法更标准、清晰,且在处理大数据时更高效。
示例:
- 查询每一个员工的姓名 , 及关联的部门的名称 (隐式内连接实现)
select emp.name, dept.name
from emp, dept
where emp.dept_id = dept.id ;
-- 为每一张表起别名,简化SQL编写
select e.name, d.name
from emp e, dept d
where e.dept_id = d.id;
- 查询每一个员工的姓名 , 及关联的部门的名称 (显式内连接实现)
select e.name, d.name from emp e inner join dept d on e.dept_id = d.id;
-- 为每一张表起别名,简化SQL编写
select e.name, d.name from emp e join dept d on e.dept_id = d.id;
3.5. 外连接
3.5.1. 左/右外连接
外连接分为左外连接和右外连接
- 左外连接:查询左表所有数据,以及两张表交集部分数据(A + C 部分)
- 右外连接:查询右表所有数据,以及两张表交集部分数据(B + C 部分)
左外连接:
左外连接会返回第一个(左)表中的所有记录,即使在第二个(右)表中没有与之匹配的记录。
(其中OUTER可以省略)
SELECT 字段列表
FROM 表1
LEFT [OUTER] JOIN 表2
ON 表1.字段 = 表2.字段;
右外连接:
右外连接会返回第二个(右)表中的所有记录,即使在第一个(左)表中没有与之匹配的记录。
SELECT 字段列表
FROM 表1
RIGHT [OUTER] JOIN 表2
ON 表1.字段 = 表2.字段;
示例:
- 查询emp表的所有数据, 和对应的部门信息(左外连接)
emp是全部需要的,然后是对应的部门表中的信息,也就是交集的部分,左外连接
select e.*, d.name from emp e left outer join dept d on e.dept_id = d.id;
select e.*, d.name from emp e left join dept d on e.dept_id = d.id;
- 查询dept表的所有数据, 和对应的员工信息(右外连接)
select d.*, e.* from emp e right outer join dept d on e.dept_id = d.id;
select d.*, e.* from emp e right join dept d on e.dept_id = d.id;
在SQL查询中,左外连接(LEFT OUTER JOIN)和右外连接(RIGHT OUTER JOIN)在某些情况下可以相互替代,以达到相同的数据查询结果。这主要是通过调整在连接查询时两个表的顺序以及相应的连接条件来实现的。在日常开发使用时,更偏向于左外连接。
3.6. 自连接
3.6.1. 自连接查询
自连接查询是一种特殊的SQL查询技术,它用于在同一个表中连接相同的表,通常是为了比较表中的不同
行。这种查询方式允许你根据表中的某些关联字段来关联表的自身,从而获取具有特定关系的数据。
就是自己连接自己,也就是把一张表连接查询多次。
对于自连接查询,可以是内连接查询,也可以是外连接查询。
- 内连接的自连接查询:
SELECT 字段列表
FROM 表A 别名A
JOIN 表A 别名B
ON 别名A.关联字段 = 别名B.关联字段;
表A
被自己连接了两次,分别使用了别名别名A
和别名B
。通过在ON
子句中指定连接条件(通常是表中的一个或多个关联字段),你可以比较表中的不同行。
自连接可以用于多种场景,例如:
- 层次结构数据查询:在具有父子关系的层次结构数据中,自连接可以帮助你查找父节点和子节点之间的关系。
- 比较同一表中不同行的关系:当你需要比较一个表中的行与该表中的其他行的关系时,自连接非常有用。
- 自连接不仅可以是内连接,也可以是外连接,包括左外连接、右外连接或全外连接。这取决于你的具体需求和你要解决的问题。
左外连接的自连接查询:
SELECT 字段列表
FROM 表A 别名A
LEFT JOIN 表A 别名B
ON 别名A.关联字段 = 别名B.关联字段;
在这个左外连接的自连接查询中,结果集将包含所有别名A
的记录,即使在别名B
中找不到匹配的记录,这些记录对应的别名B
字段值将被填充为NULL。同样,你可以根据需要将左外连接替换为右外连接或全外连接。
在自连接查询中,必须要为表起别名,要不然我们不清楚所指定的条件、返回的字段,到底是哪一张表的字段。
示例:
查询员工及其所属领导的名字(内连接的自连接)
-- 把a当做员工表,b当做直属领导表
select a.name '员工', b.name '直属领导'
from emp a , emp b
where a.managerid = b.id;
因为金庸是老板,没有直属领导,不符合条件,所以不会在查询结果内
查询所有员工 emp 及其领导的名字 emp , 如果员工没有领导, 也需要查询出来(外连接的自连接)
-- 查出a员工表中的所有信息,老板也是公司的员工
select a.name '员工', b.name '领导'
from emp a left join emp b
on a.managerid = b.id;
3.6.2. 联合查询
联合查询(UNION)在SQL中是一种将两个或更多SELECT语句的结果集合并为一个结果集的查询方式。
SELECT 字段列表 FROM 表A ...
UNION [ALL]
SELECT 字段列表 FROM 表B ....;
- 这些SELECT语句必须返回相同数量和类型的列。
- union all 会将全部的数据直接合并在一起,union 会对合并之后的数据去重。
示例:
将薪资低于 5000 的员工 , 和 年龄大于 50 岁的员工全部查询出来
当前对于这个需求,我们可以直接使用多条件查询,使用逻辑运算符 or 连接即可
也可以通过union/union all来联合查询
select * from emp where salary < 5000
union all
select * from emp where age > 50;
带关键字union all,会将全部的数据直接合并在一起,可能导致重复数据存在
带关键字union all,会对合并之后的数据去重
select * from emp where salary < 5000
union
select * from emp where age > 50;
如果多个查询语句执行后的结果,其包含的字段数量不相同,在尝试使用union或union all进行联合查询时,会引发错误。
这是因为union和union all要求参与联合的查询结果必须具有相同数量和类型的字段。只有当每个查询的结果列数、列的数据类型以及列的顺序都相同时,才能进行有效的联合查询。否则,数据库系统无法正确地将这些结果合并在一起,从而导致错误发生。
select * from emp where salary < 5000
union
select name from emp where age > 50;
3.7. 子查询(嵌套查询)
3.7.1. 概述
- 概念
嵌套查询或子查询就是在一条SQL查询语句中,放入另一条或多条SQL查询语句。就像一个查询里面套着另一个查询。先执行里面的“小”查询,得到结果后,再用这个结果去影响外面的“大”查询。这种方式可以帮助我们处理更复杂的数据查询需求,比如查找满足特定条件的记录或者比较不同数据之间的信息。
SELECT 字段列表 FROM 表名 WHERE 字段 = ( SELECT column1 FROM t2 );
- 分类
根据子查询结果的不同,我们可以将子查询分类为:
- 标量子查询:这种类型的子查询返回单个值。通常用于比较或者在 WHERE 子句中作为条件使用。
- 列子查询:这种类型的子查询返回一列数据。通常用于与外部查询的列进行比较或者连接操作。
- 行子查询:这种类型的子查询返回一行数据。通常用于查找与子查询结果完全匹配的记录。
- 表子查询:这种类型的子查询返回多行多列的数据,相当于一个小型的表格。通常用于作为外部查询的数据源或者进行集合操作。
根据子查询在SQL语句中的位置,我们可以将子查询分类为:
- WHERE之后的子查询:这种类型的子查询放在 WHERE 子句中,用于定义筛选条件。外部查询会根据子查询的结果来决定哪些记录应该被选择。
- FROM之后的子查询:这种类型的子查询放在 FROM 子句中,生成一个临时的表格或者视图,作为外部查询的数据源。
- SELECT之后的子查询:这种类型的子查询放在 SELECT 子句中,通常用于计算或者聚合操作。子查询的结果会直接作为外部查询的输出字段。
3.7.2. 标量子查询
标量子查询是指子查询的结果是一个单独的值,如一个数字、字符串或日期。这种类型的子查询通常在比较操作中使用,作为主查询的一个条件。
常用的操作符:= <> > >= < <=
- 使用等于(=)操作符:
SELECT * FROM Employees
WHERE Salary = (SELECT MAX(Salary) FROM Employees);
这个查询会返回薪资最高员工的所有信息。
- 使用不等于(<>)操作符:
SELECT * FROM Products
WHERE Price <> (SELECT AVG(Price) FROM Products);
这个查询会返回价格不等于平均价格的所有产品。
- 使用大于(>)、小于(<)、大于等于(>=)、小于等于(<=)操作符:
SELECT * FROM Orders
WHERE OrderDate > (SELECT MIN(OrderDate) FROM Orders WHERE Status = 'Completed');
这个查询会返回完成状态且订单日期大于最早完成订单日期的所有订单。
示例:
查询 "销售部" 的所有员工信息
完成这个需求时,我们可以将需求分解为两步
首先,要先查出销售部的id为多少(这时就不能用上帝视角去看——我知道销售部的id是多少)
select id from dept where name = '销售部';
在根据销售部的id在查询销售部的所有员工id
select * from emp where dept_id = 4;
使用子查询的话,就可以把这两个查询语句结合起来
select * from emp where dept_id = (select id from dept where name = '销售部');
3.7.3. 列子查询
列子查询是指子查询的结果是一列或多行一列的数据。
这种类型的子查询通常用于在主查询中进行集合比较或者条件筛选。
常用的操作符:IN 、NOT IN 、 ANY 、SOME 、 ALL
操作符 | 描述 |
IN | 在指定的集合范围之内,多选一 |
NOT IN | 不在指定的集合范围之内 |
ANY | 子查询返回列表中,有任意一个满足即可 |
SOME | 与ANY等同,使用SOME的地方都可以使用ANY |
ALL | 子查询返回列表的所有值都必须满足 |
使用 IN 操作符
SELECT * FROM Employees
WHERE DepartmentID IN (SELECT DepartmentID FROM Departments WHERE Location = 'New York');
这个查询会返回所有在纽约部门工作的员工信息
使用 NOT IN 操作符:
SELECT * FROM Products
WHERE ProductID NOT IN (SELECT ProductID FROM Orders);
这个查询会返回没有被订购过的产品信息。
使用 ANY 或 SOME 操作符(两者在大多数情况下效果相同):
SELECT * FROM Employees
WHERE Salary > ANY (SELECT Salary FROM Employees WHERE DepartmentID = 1);
这个查询会返回薪资高于部门1任意员工的员工信息。
使用 ALL 操作符:
SELECT * FROM Employees
WHERE Salary < ALL (SELECT Salary FROM Employees WHERE DepartmentID = 1);
这个查询会返回薪资低于部门1所有员工的员工信息。
示例:
查询 "销售部" 和 "市场部" 的所有员工信息
先查询 "销售部" 和 "市场部" 的部门ID
select id
from dept
where name = '销售部' or name = '市场部';
根据部门ID, 查询员工信息
select *
from emp
where dept_id in (select id from dept where name = '销售部' orname = '市场部');
3.7.4. 行子查询
行子查询是指子查询的结果是一行或多列一行的数据。这种类型的子查询通常用于在主查询中查找与子查
询结果完全匹配的记录。
常用的操作符:= 、<> 、IN 、NOT IN
使用等于(=)操作符:
SELECT * FROM Employees
WHERE (EmployeeID, DepartmentID) = (SELECT EmployeeID, DepartmentID FROM Departments WHERE Location = 'New York');
这个查询会返回在纽约部门工作的员工信息,要求员工ID和部门ID都与子查询结果完全匹配。
使用不等于(<>)操作符:
SELECT * FROM Orders
WHERE (CustomerID, OrderDate) <> (SELECT CustomerID, MAX(OrderDate) FROM Orders WHERE Status = 'Completed');
这个查询会返回除最新完成订单的客户及其订单日期外的所有订单信息。
使用 IN 操作符:
SELECT * FROM Products
WHERE (ProductID, ProductName) IN (SELECT ProductID, ProductName FROM SupplierProducts WHERE SupplierID = 1);
这个查询会返回供应商ID为1提供的所有产品信息。
使用 NOT IN 操作符:
SELECT * FROM Customers
WHERE (CustomerID, CompanyName) NOT IN (SELECT CustomerID, CompanyName FROM Invoices);
这个查询会返回未开具过发票的客户信息。
示例:
查询与 "张无忌" 的薪资及直属领导相同的员工信息
先查询 "张无忌" 的薪资及直属领导
select salary, managerid
from emp
where name = '张无忌';
再查询与 "张无忌" 的薪资及直属领导相同的员工信息
select *
from emp
where (salary,managerid) = (select salary, managerid from emp where name = '张无忌');
3.7.5. 表子查询
表子查询是指子查询的结果是多行多列的数据,相当于一个小型的表格。这种类型的子查询通常用于在主查询中作为数据源或者进行集合操作。
使用 IN 操作符:
SELECT * FROM Employees
WHERE (EmployeeID, DepartmentID) IN (SELECT EmployeeID, DepartmentID FROM Departments WHERE Location = 'New York');
这个查询会返回在纽约部门工作的员工信息,子查询返回的是所有纽约部门的员工ID和部门ID的组合。
示例:
查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
先查询与 "鹿杖客" , "宋远桥" 的职位和薪资
select job, salary from emp where name = '鹿杖客' or name = '宋远桥';
再查询与 "鹿杖客" , "宋远桥" 的职位和薪资相同的员工信息
select *
from emp where (job,salary)
in ( select job, salary from emp where name ='鹿杖客' or name = '宋远桥' );