准备数据
# 创建部门表
CREATE TABLE dept(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20)
)
INSERT INTO dept (NAME) VALUES ('开发部'),('市场部'),('财务部');
# 创建员工表
CREATE TABLE emp (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
gender CHAR(1), -- 性别
salary DOUBLE, -- 工资
join_date DATE, -- 入职日期
dept_id INT,
FOREIGN KEY (dept_id) REFERENCES dept(id) -- 外键,关联部门表(部门表的主键)
)
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('孙悟空','男
',7200,'2013-02-24',1);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('猪八戒','男
',3600,'2010-12-02',2);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('唐僧','男',9000,'2008-08-08',2);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('白骨精','女',5000,'2015-10-07',3);
INSERT INTO emp(NAME,gender,salary,join_date,dept_id) VALUES('蜘蛛精','女',4500,'2011-03-14',1);
多表查询的作用:
如果我们要查询一个人的名字和他所在的部门,就需要使用多表查询。
如果用一条SQL语句查询多个表,因为查询结果在多张表中。每张表取1列或者多列。
什么是笛卡尔积?
-- 需求:查询所有的员工和所有的部门
SELECT * FROM emp,dept;
我们发现结果:
但是并不是每条结果都是正确的,上述查询语句,就出现了错误的结果,只有当员工表中的dep_id=部门的id的数据才是有用的。因此需要过滤条件过滤掉没用的数据。·
SELECT * FROM emp,dept WHERE emp.`dept_id` = dept.`id`;
-- 查询员工和部门的名字
SELECT emp.`name`, dept.`name` FROM emp,dept WHERE emp.`dept_id` = dept.`id`;
用左边表的记录去匹配右边表的记录,如果条件符合就显示。例如:从表.外键。
隐式内连接:看不到JOIN关键字,条件使用WHERE指定。
SELECT 字段名 FROM 左表,右表 WHERE 条件
SELECT * FROM emp,dept WHERE emp.`dept_id`=dept.`id`;
显式内连接:使用INNER JOIN.....ON 语句,可以省略INNER
SELECT 字段名 FROM 左表 [INNER] JOIN 右表 ON 条件
查询唐僧的信息,显示员工id,姓名,性别,工资和所在的部门名称,我们发现需要联合2张表同时才能查询出需要的数据,使用内连接。
(1)确定查询那些表:
select * from emp inner join dept;
(2)确定连接条件,员工表.dep_id=部门表.id 的数据才是有效的。
SELECT * FROM emp e INNER JOIN dept d ON e.`dept_id`=d.`id`;
(3)确定查询条件,查询的是唐僧的信息,显示员工的id、姓名,性别,工资和所在的部门名称
SELECT e.`id`,e.`name`,e.`gender`,e.`salary`,d.`name` FROM
emp e INNER JOIN dept d ON e.`dept_id`=d.`id` WHERE e.`name`='唐僧';
(4) 我们发现写表名有点长,可以给表取别名,显示的字段名也使用别名.
SELECT e.`id` 编号,e.`name` 姓名,e.`gender` 性别,e.`salary` 工资,d.`name` 部门名字
FROM emp e INNER JOIN dept d ON e.`dept_id` = d.`id` WHERE e.`name`='唐僧';
左外连接:使用LEFT OUTER JOIN...ON ,OUTER可以省略。
SELECT 字段名 FROM 左表 LEFT [OUTER] JOIN 右表 ON 条件
用左边表的记录去匹配右边表的记录,如果条件符合则显示,否则,显示null
即:在内连接的基础上保证左表的数据完全显示。
-- 在部门表中增加一个销售部
INSERT INTO dept (NAME) VALUES('销售部');
SELECT * FROM dept;
-- 使用内连接查询
SELECT * FROM emp e INNER JOIN dept d ON d.`id`=e.`dept_id`;
-- 使用左连接
SELECT * FROM dept d LEFT JOIN emp e ON d.`id`=e.`dept_id`;
右外连接:使用RIGHT OUTER JOIN ON ,OUTER 可以省略。
SELECT * FROM 左表 RIGHT [OUTER] JOIN 右表 on 条件
用右边表的记录去匹配左边表的记录,如果符合条件,则显示,否则,显示NULL。
即:在内连接的基础上保证右表的数据全部显示。
-- 在员工表中增加一个员工
INSERT INTO emp VALUES (NULL, '沙僧','男',6666,'2013-12-05',NULL);
SELECT * FROM emp;
-- 使用内连接查询
SELECT * FROM dept INNER JOIN emp ON dept.`id` = emp.`dept_id`;
-- 使用右外连接查询
SELECT * FROM dept d RIGHT JOIN emp e ON d.`id`=e.`dept_id`;
-- 需求:查询开发部中有哪些员工
SELECT * FROM emp;
-- 通过两条语句查询
SELECT id FROM dept WHERE NAME='开发部';
SELECT * FROM emp WHERE dept_id=1;
-- 使用子查询
SELECT * FROM emp WHERE dept_id=(SELECT id FROM dept WHERE NAME='开发部');
子查询的概念:
(1)一个查询的结果作为另一个查询的条件。
(2)有查询的嵌套。内部查询成为子查询。
(3)子查询需要使用括号。
(1)子查询的结果是单行单列。
(2)子查询的结果是多行单列
(3)子查询的结果是多行多列。
子查询的结果只要是单行单列,肯定在WHERE后面作为条件,父查询使用比较运算符,如:> 、<、<>、=等。
例如:
-- 查询谁的工作最高
-- 1)查询工资最高是多少
SELECT MAX(salary) FROM emp;
-- 2)根据最高工资查询到对应的人
SELECT * FROM emp WHERE salary=(SELECT MAX(salary) FROM emp);
-- 查询工资小于平均工资的员工有哪些?
-- 1)查询平均工资是多少
SELECT AVG(salary) FROM emp;
-- 2)查询工资小于平均工资的人
SELECT * FROM emp WHERE salary<(SELECT AVG(salary) FROM emp);
子查询的结果如果是单行多列,结果类似于一个数组,父类使用IN运算符。
SELECT 查询字段 FROM 表 where 字段 IN(子查询);
-- 查询工资大于5000的员工,来自于哪些部门的名字
-- 1)先查询工资大于500的员工所在的部门
SELECT dept_id FROM emp WHERE salary>5000;
-- 2) 再查询在这些部门id中部门的名字
SELECT NAME FROM dept WHERE id IN (SELECT dept_id FROM emp WHERE salary>5000)
-- 查询开发部与财务部所有的员工信息
-- 1) 先查询开发部和财务部的id
SELECT id FROM dept WHERE NAME IN('开发部','财务部');
-- 2)查询这个部门id中有哪些员工
SELECT * FROM emp WHERE dept_id IN (SELECT id FROM dept WHERE NAME IN('开发部','财务部'));
子查询的结果只要是多列,肯定在FROM后面作为表。
SELECT 查询字段 FROM (子查询) 表别名 where 条件;
子查询作为表要为其起别名,否则这张表则无法访问表中的字段。
-- 查询出2011年以后入职的员工信息,包括部门名称
-- 在员工表中查询2011-1-1以后入职的员工
SELECT * FROM emp WHERE join_date>'2011-1-1';
-- 查询所有的部门信息,与上面的虚拟表中的信息组合,找出所有部门id等于的dept_id
SELECT * FROM dept d,(SELECT * FROM emp WHERE join_date>'2011-1-1') e WHERE d.`id`=e.dept_id;
也可以使用表连接来查询
SELECT * FROM emp INNER JOIN dept ON emp.`dept_id`=dept.`id` WHERE join_date>'2011-1-1';
SELECT * FROM emp INNER JOIN dept ON emp.`dept_id`=dept.`id` AND join_date>='2011-1-1';
什么是事务:在实际的开发过程中,一个业务操作如:转账,往往是要多次访问数据库才能完成的。转账是一个用户扣钱,另一个用户加钱。如果其中有一条SQL语句出现异常,这条SQL就可能执行失败。
事务执行是一个整体,所有的SQL语句必须执行成功,如果其中有一条SQL语句出现了异常,那么所有的SQL语句都要回滚,整个业务执行失败。
例如转账业务:
-- 创建数据表
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
balance DOUBLE
);
INSERT INTO account (NAME,balance) VALUES('张三',1000),('李四',1000);
模拟张三给李四转500元钱,一个转账的业务操作最少要执行下面的2条语句:
张三账号-500,
李四账号+500
-- 张三账号-500
UPDATE account SET balance=balance-500 WHERE NAME='张三';
-- 李四账号+500
UPDATE account SET balance=balance+500 WHERE NAME='李四';
假设当张三账号上-500元,服务器崩溃了。李四的账号并没有+500元,数据就出现问题了。我们需要保证其中一条SQL语句出现问题,整个转账就算失败。只有两条SQL都成功了转账才算成功。这个时候就需要用到事务。
MySQL中可以有两种方式进行事务的操作:
(1)手动提交事务。
(2)自动提交事务
手动提交事务的SQL语句。
功能 | SQL语句 |
开启事务 | start transaction; |
提交事务 | commit; |
回滚事务 | rollback; |
手动提交事务使用的过程:
(1)事务执行成功:开启事务 —>执行多条SQL语句--->成功提交事务
(2)事务执行失败:开启事务---->执行多条SQL语句--->事务的回滚
事务提交演示:
模拟张三给李四转500(成功),目前数据库如下:
1.使用DOS控制台进入MySQL
2.执行以下SQL语句:开启事务--->张三账户-500 —> 李四账户+500
3.使用SQLYog查看数据库;发现数据并没有改变
4.在控制台执行commit 提交事务;
5.使用SQLYog查看数据库;发现数据改变
事务回滚演示:
模拟张三给李四转500钱(失败),目前数据库如下:
1.在控制台执行以下SQL语句:1.开启事务, 2.张三账号-500
2.使用SQLYog查看数据库:发现数据并没有改变
3.在控制台执行rollback回滚事务:
4. 使用SQLYog查看数据库:发现数据没有改变
如果事务中SQL语句没有问题,commit提交事务,会对数据库数据的数据进行改变。 如果事务中SQL语句有问题,rollback回滚事务,会回退到开启事务时的状态。
MySQL默认每一条DML(增删改)语句都是一条单独的事务,每条语句都会自动开启一个事务,语句执行完毕后,自动提交事务,MySQL默认开始自动提交事务。
自动提交事务演示;
1.将金额重置为1000
2.更新其中某一个账户
3.使用SQLYog查看数据库:发现数据已经改变
取消自动提交:
查看MySQL是否开启自动提交事务:
@@ 表示全局变量,1 表示开始,0表示关闭。
取消自动提交事务:
执行更新语句,使用SQLYog查看数据库,发现数据并没有改变。
在控制台执行commit提交任务。
事务开启后,所有的操作都会临时保存到事务日志中,事务日志只有在得到commit命令后才会同步到数据表中,其他任何情况都会清空事务日志(rollback,断开连接)。
事务原理图:
事务的步骤:
1.客户端连接数据库服务器,创建连接时创建此用户临时日志文件
2.开始事务后,所有的操作都会写入到临时日志文件中。
3.所有的查询操作从表中查询,但是会经过日志文件加工后才返回
4.如果事务提交则将日志文件中的数据写到表中,否则清空日志文件。
在某些成功的操作完成之后,后续的操作有可能成功有可能失败,但是不管成功还是失败,前面操作都已经成功,可以在当前成功的位置设置一个回滚点。可以供后续失败操作返回到该位置,而不是返回所有操作,这个点称之为回滚点。
回滚点操作语句:
回滚点的操作语句 | 语句 |
设置回滚点 | savepoint 名字 |
回到回滚点 | rollback 名字 |
回滚点的具体操作:
1.将数据还原到1000
2.开启事务
3.让张三账号减3次钱,每次10块
4.设置回滚点:savepoint three_times;
5.让张三账号减4次钱,每次10块
6.回到回滚点:rollback to three_times;
7.分析执行过程
总结:设置回滚点可以让我们在失败的时候回到回滚点,而不是回到事务开启的时候。
(1)事务的四大特征ACID
事务特性 | 含义 |
原子性(Atomicity) | 每个事务都是一个整体,不可再拆分,事务中所有的SQL语句要么都执行成功,要么都失败。 |
一致性(Consistency) | 事务在执行前数据库的状态与执行后数据库的状态保持一致。如:转账前2个人的总金额是2000,转账后2个人总金额也是2000 |
隔离性(Isolation) | 事务与事务之间不应该相互影响,执行时保持隔离的状态。 |
持久性(Durability) | 一旦事务执行成功,对数据库的修改是持久的。就算关机,也是保存下来的。 |
(2) 事务的隔离级别
事务在操作时的理想状态,所有的事务之间保持隔离,互不影响。因为并发操作。多个用户同时访问一个数据库,可能引发并发访问的问题。
并发访问 的问题 | 含义 |
脏读 | 一个事务读取到了另一个事务中尚未提交的数据 |
不可重复读 | 一个事务中两次读取的数据内容不一致,要求的是一个事务中多次读取时数据是一致的,这是事务update时引发的问题 |
幻读 | 一个事务中两次读取的数据的数量不一致,要求在一个事务多次读取的数据的数量是一致的,这是insert或delete时引发的问题 |
(3)MySQL数据库有四种隔离级别
上面的级别最低,下面的级别最高。“是“表示会出现这种问题,”否“表示不会出现这种问题。
级别 | 名字 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 数据库默认隔离级别 |
1 | 读未提交 | read uncommitted | 是 | 是 | 是 | |
2 | 读已提交 | read committed | 否 | 是 | 是 | Oracle和SQL Server |
3 | 可重复读 | repeatable read | 否 | 否 | 是 | MySQL |
4 | 串行化 | serializable | 否 | 否 | 否 |
隔离级别越高,性能越差,安全性越高。
(4)MySQL事务隔离级别相关的命令
查询全局事务隔离级别
查询隔离级别 SELECT @@tx_isolation;
设置事务隔离级别,需要退出MySQL再登录才能看到隔离级别的变化。
设置隔离级别 set global transaction isolation level 级别字符串;
(5)脏读的演示
将数据进行恢复: UPDATE account SET balance=1000;
1.打开A窗口登录MySQL,设置全局的隔离级别为最低
mysql -uroot -proot
set global transaction isolation level read uncommitted;
2.打开B窗口,AB窗口都开启事务
use day23;
start transaction;
3.A窗口更新2个人的账户,未提交
UPDATE account set balance =balance-500 where id=1;
UPDATE account set balance =balance+500 where id=2;
4.B窗口查询账户
select * from account;
5.A窗口回滚
rollback;
6.B窗口查询账户,钱没了
脏读非常危险,,比如张三向李四购买商品,张三开启事务,向李四账号转入500块,然后打电话给李四说钱已经转了。李四一查询钱到账了,发货给张三。张三收到货后回滚事务,李四的再查看钱没了。
解决脏读问题:将全局的隔离级别进行提升。
将数据恢复:
UPDATE account set balance =1000;
1.在A窗口设置全局的隔离级别为read committed
set global transaction isolation level read committed;
B窗口退出MySQL,B窗口再进入MySQL
AB 同时开启事务:
2. A更新2个人的账户,为提交
UPDATE account set balance=balance-500 where id=1;
UPDATE account set balance=balance+500 where id=2;
3. B窗口查询账户
A窗口commit提交事务
4 B窗口查看账户
结论:read committed 的方式可以避免脏读发生。
(6)不可重复读的演示。
将数据进行恢复:
UPDATE account SET balance = 1000;
1.开启A窗口
set global transaction isolation level read committed;
2. 开启B窗口,在B窗口开启事务
start transaction;
select * from account;
3.在A窗口开启事务,并更新数据
start transaction;
update account set balance=balance+500 where id=1;
commit;
4.B窗口查询
select * from account;
两次查询输出的结果不同,到底哪次是对的?不知道以哪次为准。 很多人认为这种情况就对了,无须困惑,当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和发短信给客户,结果在一个事务中针对不同的输出目的地进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
解决不可重复读的问题:将全局的隔离级别进行提升为repeatable read
将数据恢复:
UPDATE account SET balance = 1000;
1.A窗口设置隔离级别为:repeatable read
set global transaction isolation level repeatable read;
2. B窗口退出MySQL,B窗口再进入MySQL
start transaction;
select * from account;
3.A窗口更新数据
start transaction;
update account set balance=balance+500 where id=1;
commit;
4.B窗口查询
select * from account;
结论:同一个事务中为了保证多次查询数据一致,必须使用repeatable read隔离级别。
(7)幻读的演示:
在MySQL中无法看到幻读的效果。
但是我们可以把事务的隔离级别设置到最高,以挡住幻读的发生,将数据进行恢复;
UPDATE account SET balance = 1000;
1.开启A窗口
set global transaction isolation level serializable; -- 设置隔离级别为最高
2.A窗口退出MySQL。A窗口重新登录MySQL
start transaction;
select count(*) from account;
3.再开启B窗口,登录MySQL
4.在B窗口中开启事务,添加一条记录
start transaction; -- 开启事务
insert into account (name,balance) values ('LaoWang', 500);
5.在A窗口中commit提交事务,B窗口中insert语句会在A窗口事务提交后立马运行
6.在A窗口中接着查询,发现数据不变
select count(*) from account;
7.B窗口中commit提交当前事务
8.A窗口就能看到最新的数据
结论:使用serializable隔离级别,一个事务没有执行完,其他事务的SQL执行不了,可以挡住幻读
DDL :create / alter / drop
DML:insert/update/delete
DQL:select/show
DCL:grant/revoke
我们现在默认使用的都是root用户,超级管理员,拥有全部的权限。但是,一个公司里面的数据库服务器上面可能同时运行着很多个项目的数据库。所以,我们应该可以根据不同的项目建立不同的用户,分配不同的权限来管理和维护数据库。
注:mysqld是MySQL的主程序,服务器端。mysql是MySQL的命令行工具,客户端.
创建用户的语法:
CREATE USER '用户名'@'主机名' IDENTIFIED BY '密码';
关键词说明:
关键字 | 说明 |
'用户名' | 将创建的用户名 |
'主机名' | 指定该用户在哪个主机上可以登陆,如果是本地用户可用localhost,如果想让该用户可以从任意远程主机登陆,可以使用通配符% |
'密码' | 该用户的登陆密码,密码可以为空,如果为空则该用户可以不需要密码登陆服务器 |
具体操作:
创建user1用户,只能在localhost这个服务器登录mysql服务器,密码为123
create user 'user1'@'localhost' identified by '123';
创建user2用户可以在任何电脑上登录mysql服务器,密码为123
create user 'user2'@'%' identified by '123';
注:创建的用户名都在mysql数据库中的user表中可以查看到,密码经过了加密。
用户被创建之后吗,没有什么权限,需要给用户授权。
给用户授权的语法:
GRANT 权限1,权限2...on 数据库名.表名 TO '用户名'@'主机名';
关键字说明:
关键字 | 说明 |
GRANT…ON…TO | 授权关键字 |
权限 | 授予用户的权限,如CREATE、ALTER、SELECT、INSERT、UPDATE等。如果要授予所有的权限则使用ALL |
数据库名.表名 | 该用户可以操作哪个数据库的哪些表。如果要授予该用户对所有数据库和表的相应操作权限则可用*表示,如*.* |
'用户名'@'主机名' | 给哪个用户授权,注:有2对单引号 |
具体操作:
给user1用户分配对test这个数据库操作的权限:创建表,修改表,插入记录,更新记录,查询
grant create,alter,insert,update,select on test.* to 'user1'@'localhost';
注:用户名和主机名要与上面创建的相同,要加单引号。
给user2用户分配所有权限,对所有数据库的所有表
grant all on *.* to 'user2'@'%';
语法:
REVOKE 权限1, 权限2... ON 数据库.表名 revoke all on test.* from 'user1'@'localhost'; '用户名'@'主机名';
关键字说明:
关键字 | 说明 |
REVOKE…ON…FROM | 撤销授权的关键字 |
权限 | 用户的权限,如CREATE、ALTER、SELECT、INSERT、UPDATE等,所有的权限则使用ALL |
数据库名.表名 | 对哪些数据库的哪些表,如果要取消该用户对所有数据库和表的操作权限则可用*表示,如*.* |
'用户名'@'主机名' | 给哪个用户撤销 |
具体操作:
撤销user1用户对test数据库所有表的操作的权限。
revoke all on test.* from 'user1'@'localhost';
注:用户名和主机名要与创建时相同,各自要加上单引号
语法:
SHOW GRANTS FOR '用户名'@'主机名';
具体操作:
查看user1用户权限:
注:usage是指连接(登陆)权限,建立一个用户,就会自动授予其usage权限(默认授予)。
语法:
DROP USER '用户名'@'主机名';
具体操作:
drop user 'user2'@'%';
语法:
mysqladmin -uroot -p password 新密码
注:需要在未登录MySQL的情况下操作,新密码不需要要加引号。
具体操作:
1.将root管理员的新密码改成123456
2.要求输入旧密码
3.使用新密码登录
语法:
set password for '用户名'@'主机名' = password('新密码');
注:需要在登录MySQL的情况下操作,新密码需要加上引号。
具体实现:
1.将'user1'@'localhost'的密码改成'666666'
2.使用新密码登录,老密码登录不了