总字符数: 19.91K
代码: 10.67K, 文本: 5.08K
预计阅读时间: 1.14 小时
多表查询
我们之前在讲解SQL语句的时候,讲解了DQL语句,也就是数据查询语句,但是之前讲解的查询都是单表查询,而本章节我们要学习的则是多表查询操作,主要从以下几个方面进行讲解.
多表关系
项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系,基本上分为三种:
- 一对多(多对一)
- 多对多
- 一对一
一对多
案例: 部门 与 员工的关系
关系: 一个部门对应多个员工,一个员工对应一个部门
实现: 在多的一方建立外键,指向一的一方的主键
多对多
案例: 学生与课程的关系
关系: 一个学生可以选修多门课程,一门课程也可以供多个学生选择
实现: 建立第三张中间表,中间表至少包含两个外键,分别关联两方主键
对应的SQL脚本:
1 | CREATE TABLE student ( |
一对一
案例: 用户 与 用户详情的关系
关系: 一对一关系,多用于单表拆分,将一张表的基础字段放在一张表中,其他详情字段放在另
一张表中,以提升操作效率
实现: 在任意一方加入外键,关联另外一方的主键,并且设置外键为唯一的(UNIQUE)
对应的SQL脚本:
1 | CREATE TABLE tb_user ( |
多表查询概述
数据准备
- 删除之前 emp, dept表的测试数据
- 执行如下脚本,创建emp表与dept表并插入测试数据
1 | -- 创建dept表,并插入数据 |
概述
多表查询就是指从多张表中查询数据.
原来查询单表数据,执行的SQL形式为:select * from emp;
那么我们要执行多表查询,就只需要使用逗号分隔多张表即可,如: select * from emp , dept ;
具体的执行结果如下:
此时,我们看到查询结果中包含了大量的结果集,总共102条记录,而这其实就是员工表emp所有的记录 (17) 与 部门表dept所有记录(6) 的所有组合情况,这种现象称之为笛卡尔积.接下来,就来简单介绍下笛卡尔积.
笛卡尔积: 笛卡尔乘积是指在数学中,两个集合A集合 和 B集合的所有组合情况
而在多表查询中,我们是需要消除无效的笛卡尔积的,只保留两张表关联部分的数据.
在SQL语句中,如何来去除无效的笛卡尔积呢? 我们可以给多表查询加上连接查询的条件即可.select * from emp , dept where emp.dept_id = dept.id;
而由于id为17的员工,没有dept_id字段值,所以在多表查询时,根据连接查询的条件并没有查询到.
分类
连接类型 | 描述 | 特点 |
---|---|---|
内连接 | 只查询两个表中匹配的数据. | 结果集中仅包含两表中都有的记录. |
左外连接 | 查询左表所有数据以及两表交集部分数据. | 结果集包含左表所有记录,如果右表中有匹配,则包含右表数据,否则右表部分为NULL. |
右外连接 | 查询右表所有数据以及两表交集部分数据. | 结果集包含右表所有记录,如果左表中有匹配,则包含左表数据,否则左表部分为NULL. |
全外连接 | 查询两表所有数据,不仅仅是交集部分. | 结果集包括左表和右表的所有记录,匹配的行将一起显示,不匹配的行则与NULL填充. |
自连接 | 当前表与自身进行连接查询,必须使用表别名以区分同一表的不同实例. | 常用于查询具有层次或树状结构的数据. |
内连接
内连接查询的目的是返回两张表中匹配关联条件的交集部分的数据.
语法结构
内连接的语法有隐式和显式两种不同的形式:
隐式内连接
使用逗号分隔表名,然后在WHERE
子句中指定条件.
1 | SELECT 字段列表 |
显式内连接
使用INNER JOIN
关键字连接两个表,并在ON
子句中指定连接条件.
1 | SELECT 字段列表 |
案例
设有两个表结构emp
和dept
,员工表emp
和部门表dept
通过dept_id
关联.
A. 隐式内连接实现查询员工姓名及其关联的部门名称
1 | -- 不使用别名 |
B. 显式内连接实现查询员工姓名及其关联的部门名称
1 | -- 使用INNER JOIN关键字和表别名 |
表的别名使用
表别名可以在FROM
子句中使用AS
关键字定义,也可以直接跟在表名之后.
1 | -- 使用AS关键字定义别名 |
注意事项
- 别名一旦定义,在当前查询语句中必须使用别名来引用表内的字段.
- 原始表名在定义别名后在当前查询中不再使用.
外连接
外连接包括左外连接和右外连接,用于不仅查询表之间的匹配数据,还包括左表或右表中的所有数据.
语法结构
1. 左外连接
使用LEFT [OUTER] JOIN
关键字,通常用于查询左表(表1)的全部数据,以及两表交集的数据.
1 | SELECT 字段列表 |
2. 右外连接
使用RIGHT [OUTER] JOIN
关键字,通常用于查询右表(表2)的全部数据,以及两表交集的数据.
1 | SELECT 字段列表 |
案例
A. 左外连接
查询emp
表的所有数据,以及与之对应的部门信息.
假设有两个表emp
和dept
,员工表emp
通过dept_id
与部门表dept
关联.
1 | -- 使用LEFT OUTER JOIN关键字 |
B. 右外连接
查询dept
表的所有数据,以及与之对应的员工信息.
1 | -- 使用RIGHT OUTER JOIN关键字 |
注意事项
- 左外连接和右外连接在逻辑上是可以互相转换的,只需要调整连接查询时SQL中表的先后位置.
- 在实际开发中,左外连接比右外连接使用得更为频繁.
自连接
自连接查询
自连接指的是一张表与其自身进行连接查询.自连接可以是内连接也可以是外连接.
语法结构
自连接至少需要为同一张表设定两个不同的别名,以区分连接中的两个不同的表角色.
1 | SELECT 字段列表 |
案例
A. 查询员工及其所属领导的名字
在emp
表中通过managerid
关联自己,以查询员工及其对应的领导名字.
1 | SELECT a.name AS '员工', b.name AS '领导' |
B. 查询所有员工及其领导的名字,包括没有领导的员工
使用左外连接以确保即使员工没有领导也会被查询出来.
1 | SELECT a.name AS '员工', b.name AS '领导' |
注意事项
- 在进行自连接时,别名的使用是必须的,以便清楚地区分条件和字段所属的表的不同实例.
- 不使用别名将导致查询语句在引用字段时产生歧义.
联合查询
UNION
查询用于合并两个或多个SELECT
语句的结果集,返回一个新的结果集.
语法结构
使用UNION
或UNION ALL
将多个查询合并,需确保每个查询中的字段数目和数据类型都一致.
1 | SELECT 字段列表 FROM 表A ... |
特点
UNION
在合并结果集时会自动去除重复的记录.UNION ALL
直接合并结果集,包含所有重复记录.
案例
A. 查询薪资低于5000或年龄大于50岁的员工
可以使用OR
逻辑运算符进行查询,也可以使用UNION
或UNION ALL
来实现.
1 | -- 使用OR逻辑运算符 |
注意事项
- 在使用
UNION
时,每个查询必须有相同数量的列,列的顺序和类型必须兼容. - 如果希望包含重复行,应使用
UNION ALL
. - 如果想要结果集排序,只能在最后一个
SELECT
语句后使用ORDER BY
子句.
union all查询出来的结果,仅仅进行简单的合并,并未去重
1 | select * from emp where salary < 5000 union sselect * from emp where age > 50; |
union 联合查询,会对查询出来的结果进行去重处理
如果多条查询语句查询出来的结果,字段数量不一致,在进行union/union all联合查询时,将会报错子查询
概念
SQL语句中嵌套SELECT语句,称为嵌套查询,又称子查询.
1 | SELECT * FROM t1 WHERE column1 = ( SELECT column1 FROM t2 ); |
子查询外部的语句可以是INSERT / UPDATE / DELETE / SELECT
的任何一个.
分类
根据子查询结果不同,分为:
- 标量子查询(子查询结果为单个值)
- 列子查询(子查询结果为一列)
- 行子查询(子查询结果为一行)
- 表子查询(子查询结果为多行多列)
根据子查询位置,分为:
- WHERE之后
- FROM之后
- SELECT之后
标量子查询
标量子查询返回单个值的子查询,常与比较操作符结合使用.
常用操作符
- 等于
=
- 不等于
<>
- 大于
>
- 大于等于
>=
- 小于
<
- 小于等于
<=
案例
A. 查询”销售部”的所有员工信息
可以通过子查询找到”销售部”的部门ID,然后使用该ID查询员工信息.
1 | -- 查询"销售部"的部门ID |
B. 查询在”方东白”入职之后的员工信息
先找出”方东白”的入职日期,然后查询所有在此日期之后入职的员工.
1 | -- 查询"方东白"的入职日期 |
注意事项
- 子查询在括号内执行,并返回单个值,供外部查询使用.
- 确保子查询返回的是单个值,否则会导致错误.
- 子查询可以用在
SELECT
、FROM
、WHERE
等子句中.
列子查询
子查询返回的结果是一列(可以是多行),这种子查询称为列子查询.
常用的操作符:IN 、NOT IN 、 ANY 、SOME 、 ALL
A. 查询”销售部”和”市场部”的所有员工信息
要查询两个部门的员工信息,可以先找到这两个部门的ID,然后在员工表中查找.
1 | -- 查询"销售部"和"市场部"的部门ID |
B. 查询比财务部所有人工资都高的员工信息
寻找所有财务部员工的工资,然后比较其他员工的工资是否都高于财务部的.
1 | -- 查询财务部的部门ID |
C. 查询比研发部其中任意一人工资高的员工信息
找出研发部所有人的工资,然后查询工资高于研发部任一员工工资的员工.
1 | -- 查询研发部所有人的工资 |
注意事项
- 在使用
IN
子句时,子查询可以返回一个或多个值. - 使用
ALL
关键字时,外层查询的结果必须大于子查询返回的所有值. - 使用
ANY
(或SOME
)时,只需外层查询的结果大于子查询返回值中的任意一个即可. - 确保子查询与外部查询在逻辑上正确关联,以避免意外的查询结果.
行子查询
子查询返回的结果是一行(可以是多列),这种子查询称为行子查询.
常用的操作符:= 、<> 、IN 、NOT IN
A. 查询与 “张无忌” 的薪资及直属领导相同的员工信息 ;
1 | -- 这个需求同样可以拆解为两步进行: |
表子查询
子查询返回的结果是多行多列,这种子查询称为表子查询.
常用的操作符:IN
A. 查询与 “鹿杖客” , “宋远桥” 的职位和薪资相同的员工信息
1 | -- 分解为两步执行: |
B. 查询入职日期是 “2006-01-01” 之后的员工信息 , 及其部门信息分解为两步执行:
1 | -- 入职日期是 "2006-01-01" 之后的员工信息 |
多表查询案例
1 | -- 创建薪资等级表salgrade |
在这个案例中,我们主要运用上面所讲解的多表查询的语法,完成以下的12个需求即可,而这里主要涉及到的表就三张:
- emp员工表
- dept部门表
- salgrade薪资等级表 .
查询员工的姓名、年龄、职位、部门信息(隐式内连接)表: emp,dept 连接条件: emp.dept_id = dept.id
1 | select e.name , e.age , e.job , d.name from emp e , dept d where e.dept_id = d.id; |
查询年龄小于30岁的员工的姓名、年龄、职位、部门信息(显式内连接) 表: emp,dept 连接条件: emp.dept_id = dept.id
1 | select e.name, e.age, e.job, d.name from emp e inner join dept d on e.dept_id = d.id where e.age < 30; |
查询拥有员工的部门ID、部门名称表: emp,dept 连接条件: emp.dept_id = dept.id
1 | select distinct d.id , d.name from emp e , dept d where e.dept_id = d.id; |
查询所有年龄大于40岁的员工, 及其归属的部门名称; 如果员工没有分配部门, 也需要展示出来(外连接) 表: emp,dept 连接条件: emp.dept_id = dept.id
1 | select e.*, d.name from emp e left join dept d on e.dept_id = d.id where e.age > 40 ; |
查询所有员工的工资等级 表: emp , salgrade 连接条件 : emp.salary >= salgrade.losal and emp.salary <= salgrade.hisal
1 | -- 方式一 |
查询 "研发部" 所有员工的信息及工资等级表: emp , salgrade , dept 连接条件 : emp.salary between salgrade.losal and salgrade.hisal , emp.dept_id = dept.id 查询条件 : dept.name = '研发部'
1 | select e.* , s.grade from emp e , dept d , salgrade s where e.dept_id = d.id and ( |
查询 "研发部" 员工的平均工资 表: emp,dept 连接条件: emp.dept_id = dept.id
1 | select avg(e.salary) from emp e, dept d where e.dept_id = d.id and d.name = '研发部'; |
查询工资比 "灭绝" 高的员工信息
1 | -- 查询 "灭绝" 的薪资 |
查询比平均薪资高的员工信息
1 | -- 查询员工的平均薪资 |
查询低于本部门平均工资的员工信息
1 | -- 查询指定部门平均薪资 |
查询所有的部门信息, 并统计部门的员工人数
1 | select d.id, d.name , ( select count(*) from emp e where e.dept_id = d.id ) '人数' from dept d; |
1 | select s.name , s.no , c.name from student s , student_course sc , course c where |
以上需求的实现方式可能会很多, SQL写法也有很多,只要能满足我们的需求,查询出符合条件的记录即可
事务
事务简介
事务 是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系
统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败.
就比如: 张三给李四转账1000块钱,张三银行账户的钱减少1000,而李四银行账户的钱要增加1000. 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败.
正常情况: 转账这个操作, 需要分为以下这么三步来完成 , 三步完成之后, 张三减少1000, 而李四增加1000, 转账成功 :
异常情况: 转账这个操作, 也是分为以下这么三步来完成 , 在执行第三步是报错了, 这样就导致张三减少1000块钱, 而李四的金额没变, 这样就造成了数据的不一致, 就出现问题了.
为了解决上述的问题,就需要通过数据的事务来完成,我们只需要在业务逻辑执行之前开启事务,执行 完毕后提交事务.如果执行过程中报错,则回滚事务,把数据恢复到事务开始之前的状态
注意: 默认MySQL的事务是自动提交的,也就是说,当执行完一条DML语句时,MySQL会立即隐式的提交事务.
事务操作
1 | -- 删除已存在的account表 |
未控制事务
测试正常情况
1 | -- 1. 查询张三余额 |
测试完毕之后检查数据的状态, 可以看到数据操作前后是一致的.
测试异常情况
1 | -- 1. 查询张三余额 |
我们把数据都恢复到2000, 然后再次一次性执行上述的SQL语句(出错了…. 这句话不符合SQL语法,执行就会报错),检查最终的数据情况, 发现数据在操作前后不一致了.
控制事务一
查看/设置事务提交方式
1 | SELECT @@autocommit ; |
提交事务
1 | COMMIT; |
回滚事务
1 | ROLLBACK; |
注意:上述的这种方式,我们是修改了事务的自动提交行为, 把默认的自动提交修改为了手动提交, 此时我们执行的DML语句都不会提交, 需要手动的执行commit进行提交.
控制事务二
开启事务
1 | START TRANSACTION 或 BEGIN ; |
提交事务
1 | COMMIT; |
回滚事务
1 | ROLLBACK; |
转账案例
1 | -- 开启事务 |
事务四大特性
- 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败.
- 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态.
- 隔离性(Isolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行.
- 持久性(Durability):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的.
上述就是事务的四大特性,简称ACID.
并发事务问题
- 赃读:一个事务读到另外一个事务还没有提交的数据.比如B读取到了A未提交的数据
- 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读.事务A两次读取同一条记录,但是读取到的数据却是不一样的.
- 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了 “幻影”.
事务隔离级别
为了解决并发事务所引发的问题,在数据库中引入了事务隔离级别.主要有以下几种:
查看事务隔离级别
1
SELECT @@TRANSACTION_ISOLATION;
设置事务隔离级别
1
2SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED |
READ COMMITTED | REPEATABLE READ | SERIALIZABLE }注意:事务隔离级别越高,数据越安全,但是性能越低.