This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,505 @@
<audio id="audio" title="14 | 视图:如何简化查询?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/a6/7a31d6f3b98195b015ca698da7b135a6.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊视图。
视图是一种虚拟表,我们可以把一段查询语句作为视图存储在数据库中,在需要的时候,可以把视图看做一个表,对里面的数据进行查询。
举个小例子,在学校的信息系统里面,为了减少冗余数据,学生档案(包括姓名、年龄等)和考试成绩(包括考试时间、科目、分数等)是分别存放在不同的数据表里面的,但是,我们经常需要查询学生的考试成绩(包括学生姓名、科目、分数)。这个时候,我们就可以把查询学生考试成绩的这个关联查询,用视图的形式保存起来。这样一来,我们不仅可以从视图中直接查询学生考试成绩,让查询变得简单,而且,视图没有实际存储数据,还避免了数据存储过程中可能产生的冗余,提高了存储的效率。
今天,我就结合超市的项目,来具体讲解一下怎么创建和操作视图,来帮助你提高查询效率。
## 视图的创建及其好处
首先,我们来学习下创建视图的方法,以及使用视图的一些好处。
创建视图的语法结构:
```
CREATE [OR REPLACE]
VIEW 视图名称 [(字段列表)]
AS 查询语句
```
现在假设我们要查询一下商品的每日销售明细这就要从销售流水表demo.trans和商品信息表demo.goodsmaster中获取到销售数据和对应的商品信息数据。
销售流水表包含流水单号、商品编号、销售数量、销售金额和交易时间等信息:
<img src="https://static001.geekbang.org/resource/image/65/56/65168f6e248fcc848e8b9968a712c956.jpeg" alt="">
商品信息表包含商品编号、条码、名称和售价等信息:
<img src="https://static001.geekbang.org/resource/image/73/4b/73836142bbfd6cc0f69de418acb7444b.jpeg" alt="">
在不使用视图的情况下,我们可以通过对销售流水表和商品信息表进行关联查询,得到每天商品销售统计的结果,包括销售日期、商品名称、每天销售数量的合计和每天销售金额的合计,如下所示:
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; b.goodsname,
-&gt; SUM(a.quantity) AS quantity, -- 统计销售数量
-&gt; SUM(a.salesvalue) AS salesvalue -- 统计销售金额
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN -- 连接查询
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; GROUP BY a.transdate , a.itemnumber;
+---------------------+------------+-----------+----------+------------+
| transdate | itemnumber | goodsname | quantity | salesvalue |
+---------------------+------------+-----------+----------+------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 89.00 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 5.00 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 20.00 |
+---------------------+------------+-----------+----------+------------+
3 rows in set (0.00 sec)
```
在实际项目中,我们发现,每日商品销售查询使用的频次很高,而且经常需要以这个查询的结果为基础,进行更进一步的统计。
举个例子超市经营者要查一下“每天商品的销售数量和当天库存数量的对比”如果用一个SQL语句查询就会比较复杂。历史库存表demo.inventoryhist如下所示
<img src="https://static001.geekbang.org/resource/image/ac/bc/ac3f59ba59e460bb4962391b20a8c1bc.jpeg" alt="">
接下来我们的查询步骤会使用到子查询和派生表,很容易理解,你知道含义就行了。
- 子查询:就是嵌套在另一个查询中的查询。
- 派生表:如果我们在查询中把子查询的结果作为一个表来使用,这个表就是派生表。
这个查询的具体步骤是:
1. 通过子查询获得单品销售统计的查询结果;
1. 把第一步中的查询结果作为一个派生表,跟历史库存表进行连接,查询获得包括销售日期、商品名称、销售数量和历史库存数量在内的最终结果。
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; a.goodsname,
-&gt; a.quantity, -- 获取单品销售数量
-&gt; b.invquantity -- 获取历史库存数量
-&gt; FROM
-&gt; (SELECT -- 子查询,统计单品销售
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; b.goodsname,
-&gt; SUM(a.quantity) AS quantity,
-&gt; SUM(a.salesvalue) AS salesvalue
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; GROUP BY a.transdate , a.itemnumber
-&gt; ) AS a -- 派生表,与历史库存进行连接
-&gt; LEFT JOIN
-&gt; demo.inventoryhist AS b
-&gt; ON (a.transdate = b.invdate
-&gt; AND a.itemnumber = b.itemnumber);
+---------------------+------------+-----------+----------+-------------+
| transdate | itemnumber | goodsname | quantity | invquantity |
+---------------------+------------+-----------+----------+-------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 100.000 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 99.000 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 200.000 |
+---------------------+------------+-----------+----------+-------------+
3 rows in set (0.00 sec)
```
可以看到,这个查询语句是比较复杂的,可读性和可维护性都比较差。那该怎么办呢?其实,针对这种情况,我们就可以使用视图。
我们可以把商品的每日销售统计查询做成一个视图,存储在数据库里,代码如下所示:
```
mysql&gt; CREATE VIEW demo.trans_goodsmaster AS -- 创建视图
-&gt; SELECT
-&gt; a.transdate,
-&gt; a.itemnumber,
-&gt; b.goodsname, -- 从商品信息表中获取名称
-&gt; SUM(a.quantity) AS quantity, -- 统计销售数量
-&gt; SUM(a.salesvalue) AS salesvalue -- 统计销售金额
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber) -- 与商品信息表关联
-&gt; GROUP BY a.transdate , a.itemnumber; -- 按照销售日期和商品编号分组
Query OK, 0 rows affected (0.01 sec)
```
这样一来,我们每次需要查询每日商品销售数据的时候,就可以直接查询视图,不需要再写一个复杂的关联查询语句了。
我们来试试用一个查询语句直接从视图中进行查询:
```
mysql&gt; SELECT * -- 直接查询
-&gt; FROM demo.trans_goodsmaster; -- 视图
+---------------------+------------+-----------+----------+------------+
| transdate | itemnumber | goodsname | quantity | salesvalue |
+---------------------+------------+-----------+----------+------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 89.00 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 5.00 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 20.00 |
+---------------------+------------+-----------+----------+------------+
3 rows in set (0.01 sec)
```
结果显示,这两种查询方式得到的结果是一样的。
如果我们要进一步查询“每日单品销售的数量与当日的库存数量的对比”,就可以把刚刚定义的视图作为一个数据表来使用。我们把它跟历史库存表连接起来,来获取销售数量和历史库存数量。就像下面的代码这样,查询就简单多了:
```
mysql&gt; SELECT
-&gt; a.transdate, -- 从视图中获取销售日期
-&gt; a.itemnumber, -- 从视图中获取商品编号
-&gt; a.goodsname, -- 从视图中获取商品名称
-&gt; a.quantity, -- 从视图中获取销售数量
-&gt; b.invquantity -- 从历史库存表中获取历史库存数量
-&gt; FROM
-&gt; demo.trans_goodsmaster AS a -- 视图
-&gt; LEFT JOIN
-&gt; demo.inventoryhist AS b ON (a.transdate = b.invdate
-&gt; AND a.itemnumber = b.itemnumber); -- 直接连接库存历史表
+---------------------+------------+-----------+----------+-------------+
| transdate | itemnumber | goodsname | quantity | invquantity |
+---------------------+------------+-----------+----------+-------------+
| 2020-12-01 00:00:00 | 1 | 本 | 1.000 | 100.000 |
| 2020-12-01 00:00:00 | 2 | 笔 | 1.000 | 99.000 |
| 2020-12-02 00:00:00 | 3 | 胶水 | 2.000 | 200.000 |
+---------------------+------------+-----------+----------+-------------+
3 rows in set (0.00 sec)
```
结果显示,这里的查询结果和我们刚刚使用派生表的查询结果是一样的。但是,**使用视图的查询语句明显简单多了,可读性更好,也更容易维护**。
## 如何操作视图和视图中的数据?
创建完了视图,我们还经常需要对视图进行一些操作,比如修改、查看和删除视图。同时,我们可能还需要修改视图中的数据。具体咋操作呢?我来介绍下。
### 如何操作视图?
修改、查看、删除视图的操作比较简单,你只要掌握具体的语法就行了。
修改视图的语法如下所示:
```
ALTER VIEW 视图名
AS 查询语句;
```
查看视图的语法是:
```
查看视图:
DESCRIBE 视图名;
```
删除视图要使用DROP关键词具体方法如下
```
删除视图:
DROP VIEW 视图名;
```
好了,到这里,对视图的操作我就介绍完了,下面我再讲讲怎么操作视图中的数据。
### 如何操作视图中的数据?
刚刚说过,视图本身是一个虚拟表,所以,对视图中的数据进行插入、修改和删除操作,实际都是通过对实际数据表的操作来实现的。
**1.在视图中插入数据**
我借用刚刚的视图demo.view_goodsmaster来给你解释下。假设商品信息表中的规格字段specification被删除了当我们尝试用INSERT INTO 语句向视图中插入一条记录的时候,就会提示错误了:
```
mysql&gt; INSERT INTO demo.view_goodsmaster
-&gt; (itemnumber,barcode,goodsname,salesprice)
-&gt; VALUES
-&gt; (5,'0005','测试',100);
ERROR 1471 (HY000): The target table view_goodsmaster of the INSERT is not insertable-into
```
这是因为,**只有视图中的字段跟实际数据表中的字段完全一样MySQL才允许通过视图插入数据**。刚刚的视图中包含了实际数据表所没有的字段“specification”所以在插入数据时系统就会提示错误。
为了解决这个问题,我们来修改一下视图,让它只包含实际数据表中有的字段,也就是商品编号、条码、名称和售价。代码如下:
```
mysql&gt; ALTER VIEW demo.view_goodsmaster
-&gt; AS
-&gt; SELECT itemnumber,barcode,goodsname,salesprice -- 只包含实际表中存在的字段
-&gt; FROM demo.goodsmaster
-&gt; WHERE salesprice &gt; 50;
Query OK, 0 rows affected (0.01 sec)
```
对视图进行修改之后,我们重新尝试向视图中插入一条记录:
```
mysql&gt; INSERT INTO demo.view_goodsmaster
-&gt; (itemnumber,barcode,goodsname,salesprice)
-&gt; VALUES
-&gt; (5,'0005','测试',100);
Query OK, 1 row affected (0.02 sec)
```
结果显示,插入成功了。
现在我们来查看一下视图中的数据:
```
mysql&gt; SELECT *
-&gt; FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 |
| 5 | 0005 | 测试 | 100.00 | -- 通过视图插入的数据
+------------+---------+-----------+------------+
2 rows in set (0.01 sec)
```
结果显示表中确实包含了我们插入的商品编号是5的商品信息。
现在,视图中已经包括了刚才插入的数据,那么,实际数据表中的数据情况又是怎样的呢?我们再来看一下:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 |
| 2 | 0002 | 笔 | 5.00 |
| 3 | 0003 | 胶水 | 10.00 |
| 5 | 0005 | 测试 | 100.00 | -- 通过视图插入的数据
+------------+---------+-----------+------------+
4 rows in set (0.00 sec)
```
可以看到实际数据表demo.goodsmaster中也已经包含通过视图插入的商品编号是5的商品数据了。
**2.删除视图中的数据**
我们可以通过DELETE语句删除视图中的数据
```
mysql&gt; DELETE FROM demo.view_goodsmaster -- 直接在视图中删除数据
-&gt; WHERE itemnumber = 5;
Query OK, 1 row affected (0.02 sec)
```
现在我们来查看视图和实际数据表的内容会发现商品编号是5的商品都已经被删除了。
```
mysql&gt; SELECT *
-&gt; FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 | -- 视图中已经没有商品编号是5的商品了
+------------+---------+-----------+------------+
1 row in set (0.00 sec)
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 89.00 |
| 2 | 0002 | 笔 | 5.00 |
| 3 | 0003 | 胶水 | 10.00 | -- 实际表中也已经没有商品编号是5的商品了
+------------+---------+-----------+------------+
3 rows in set (0.00 sec)
```
**3.修改视图中的数据**
我们可以通过UPDATE语句对视图中的数据进行修改
```
mysql&gt; UPDATE demo.view_goodsmaster -- 更新视图中的数据
-&gt; SET salesprice = 100
-&gt; WHERE itemnumber = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
结果显示,更新成功了。现在我们来查看一下视图和实际数据表,代码如下所示:
```
mysql&gt; SELECT *
-&gt; FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 100.00 | -- 视图中的售价改过了
+------------+---------+-----------+------------+
1 row in set (0.01 sec)
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 本 | 100.00 | -- 实际数据表中的售价也改过了
| 2 | 0002 | 笔 | 5.00 |
| 3 | 0003 | 胶水 | 10.00 |
+------------+---------+-----------+------------+
3 rows in set (0.00 sec)
```
可以发现,视图和原来的数据表都已经改过来了。
需要注意的是,**我不建议你对视图的数据进行更新操作**因为MySQL允许用比较复杂的SQL查询语句来创建视图比如SQL查询语句中使用了分组和聚合函数或者是UION和DISTINCT关键字所以要通过对这个结果集的更新来更新实际数据表有可能不被允许因为MySQL没办法精确定位实际数据表中的记录。就比如刚刚讲到的那个“每日销售统计查询”视图就没办法更改因为创建视图的SQL语句是一个包含了分组函数GROUP BY的查询。
## 视图有哪些优缺点?
到这里,视图的操作我就讲完了,现在我们把视线拔高一点,来看看视图都有哪些优缺点。只有全面掌握视图的特点,我们才能充分享受它的高效,避免踩坑。
首先,我来介绍下视图的优点。
第一,因为我们可以把视图看成一张表来进行查询,所以在使用视图的时候,我们不用考虑视图本身是如何获取数据的,里面有什么逻辑,包括了多少个表,有哪些关联操作,而是可以直接使用。这样一来,实际上就把查询模块化了,查询变得更加简单,提高了开发和维护的效率。所以,你可以把那些经常会用到的查询和复杂查询的子查询定义成视图,存储到数据库中,这样可以为你以后的使用提供方便。
第二,视图跟实际数据表不一样,它存储的是查询语句。所以,在使用的时候,我们要通过定义视图的查询语句来获取结果集。而视图本身不存储数据,不占用数据存储的资源。
第三,视图具有隔离性。视图相当于在用户和实际的数据表之间加了一层虚拟表。也就是说,**用户不需要查询数据表,可以直接通过视图获取数据表中的信息**。这样既提高了数据表的安全性,同时也通过视图把用户实际需要的信息汇总在了一起,查询起来很轻松。
<img src="https://static001.geekbang.org/resource/image/a2/45/a2ca36c520ed6a6fee5f788d5dcb2845.jpg" alt="">
第四,**视图的数据结构相对独立,即便实际数据表的结构发生变化,我们也可以通过修改定义视图的查询语句,让查询结果集里的字段保持不变**。这样一来,针对视图的查询就不受实际数据表结构变化的影响了。
这一点不容易理解,我举个小例子来说明一下。
假设我们有一个实际的数据表demo.goodsmaster包括商品编号、条码、名称、规格和售价等信息
<img src="https://static001.geekbang.org/resource/image/63/8f/63f46f64a5007278b97235a00633d58f.jpeg" alt="">
在这个表的基础上我们建一个视图查询所有价格超过50元的商品
```
mysql&gt; CREATE VIEW demo.view_goodsmaster AS
-&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; WHERE salesprice &gt; 50;
Query OK, 0 rows affected (0.03 sec)
```
接着,我们在这个视图的基础上做一个查询,来验证一下视图的内容:
```
mysql&gt; SELECT barcode,goodsname,specification
-&gt; FROM demo.view_goodsmaster;
+---------+-----------+---------------+
| barcode | goodsname | specification |
+---------+-----------+---------------+
| 0001 | 本 | 16开 |
+---------+-----------+---------------+
1 row in set (0.00 sec)
```
结果显示我们得到了商品信息表中售价大于50元的商品16开
假设现在我们需要把数据表demo.goodsmaster中的字段“specification”删掉就可以用下面的代码
```
mysql&gt; ALTER TABLE demo.goodsmaster DROP COLUMN specification;
Query OK, 0 rows affected (0.13 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
这样一来,因为少了一个字段,而我们的语句又是直接查询数据表的,代码就会提示错误:
```
mysql&gt; SELECT barcode,goodsname,specification
-&gt; FROM demo.goodsmaster;
ERROR 1054 (42S22): Unknown column 'specification' in 'field list'
```
你看代码提示字段“specification”不存在。
但是,如果查询的是视图,就可以通过修改视图来规避这个问题。我们可以用下面的代码把刚才的视图修改一下:
```
mysql&gt; ALTER VIEW demo.view_goodsmaster
-&gt; AS
-&gt; SELECT
-&gt; itemnumber,
-&gt; barcode,
-&gt; goodsname,
-&gt; '' as specification, -- 由于字段不存在插入一个长度是0的空字符串作为这个字段的值
-&gt; salesprice
-&gt; FROM demo.goodsmaster
-&gt; WHERE salesprice &gt; 50;
Query OK, 0 rows affected (0.02 sec)
```
你看虽然实际数据表中已经没有字段“specification”了但是视图中却保留了这个字段而且字段值始终是空字符串。所以我们不用修改原有视图的查询语句它也会正常运行。下面的代码查询的结果中就包括了实际数据表没有的字段“specification”。
```
mysql&gt; SELECT barcode,goodsname,specification
-&gt; FROM demo.view_goodsmaster;
+---------+-----------+---------------+
| barcode | goodsname | specification |
+---------+-----------+---------------+
| 0001 | 本 | |
+---------+-----------+---------------+
1 row in set (0.00 sec)
```
结果显示,运行成功了。这个视图查询,就没有受到实际数据表中删除字段的影响。
看到这儿,你可能会说,视图有这么多好处,那我以后都用视图可以吗?其实不是的,视图也有自身的不足。
如果我们在实际数据表的基础上创建了视图,那么,**如果实际数据表的结构变更了,我们就需要及时对相关的视图进行相应的维护**。特别是当视图是由视图生成的时候维护会变得比较复杂。因为创建视图的SQL查询可能会对字段重命名也可能包含复杂的逻辑这些都会增加维护的成本。
所以,在创建视图的时候,你要结合实际项目需求,综合考虑视图的优点和不足,这样才能正确使用视图,使系统整体达到最优。
为了方便你掌握,我用一张图来汇总下视图的优缺点:
<img src="https://static001.geekbang.org/resource/image/d4/12/d4cb502799c8328d22774907d7b97212.jpeg" alt="">
## 总结
今天,我给你介绍了简化查询的工具:视图,我们学习了创建视图、操作视图和视图中的数据的方法以及视图的优缺点。你要重点掌握操作的语法结构。
最后,我还是想提醒你一下,虽然可以更新视图数据,但总的来说,视图作为虚拟表,主要用于方便查询。我不建议你更新视图的数据,因为对视图数据的更改,都是通过对实际数据表里数据的操作来完成的,而且有很多限制条件。
视图虽然有很多优点。但是在创建视图、简化查询的同时,也要考虑到视图太多而导致的数据库维护成本的问题。
视图不是越多越好,特别是嵌套的视图(就是在视图的基础上创建视图),我不建议你使用,因为逻辑复杂,可读性不好,容易变成系统的潜在隐患。
## 思考题
假设某公园售票系统包括门票信息表和类别信息表,这两个表之间通过类别编号相关联。
门票信息表包含门票编号、名称、类别编号和剩余数量等信息。
<img src="https://static001.geekbang.org/resource/image/e1/6a/e1d07bcc301ec2929ff50d601b3c5a6a.jpeg" alt="">
类别信息表包含类别编号、开门时间和闭馆时间。
<img src="https://static001.geekbang.org/resource/image/8f/42/8f21b2f64c2bb51ab7719f27d40ed942.jpeg" alt="">
请编写一个视图视图返回的结果集包括当前时间可以卖的门票名称和剩余数量说明开门前30分钟开始售票结束前30分钟停止售票
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,328 @@
<audio id="audio" title="15 | 存储过程:如何提高程序的性能和安全性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/b2/7db3784962df3021f43c8c37d58dbfb2.mp3"></audio>
你好我是朱晓峰。今天呢我们来聊一聊MySQL的存储过程。
在我们的超市项目中,每天营业结束后,超市经营者都要计算当日的销量,核算成本和毛利等营业数据,这也就意味着每天都要做重复的数据统计工作。其实,这种数据量大,而且计算过程复杂的场景,就非常适合使用存储过程。
简单来说呢存储过程就是把一系列SQL语句预先存储在MySQL服务器上需要执行的时候客户端只需要向服务器端发出调用存储过程的命令服务器端就可以把预先存储好的这一系列SQL语句全部执行。
这样一来不仅执行效率非常高而且客户端不需要把所有的SQL语句通过网络发给服务器减少了SQL语句暴露在网上的风险也提高了数据查询的安全性。
今天,我就借助真实的超市项目,给你介绍一下如何创建和使用存储过程,帮助你提升查询的效率,并且让你开发的应用更加简洁安全。
## 如何创建存储过程?
在创建存储过程的时候我们需要用到关键字CREATE PROCEDURE。具体的语法结构如下
```
CREATE PROCEDURE 存储过程名 [ IN | OUT | INOUT] 参数名称 类型)程序体
```
接下来,我以超市的日结计算为例,给你讲一讲怎么创建存储过程。当然,为了方便你理解,我对计算的过程进行了简化。
假设在日结计算中,我们需要统计每天的单品销售,包括销售数量、销售金额、成本、毛利、毛利率等。同时,我们还要把计算出来的结果存入单品统计表中。
这个计算需要用到几个数据表,我分别来展示下这些表的基本信息。
销售单明细表demo.transactiondetails中包括了每笔销售中的商品编号、销售数量、销售价格和销售金额。
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+------------+------------+
| transactionid | itemnumber | quantity | salesprice | salesvalue |
+---------------+------------+----------+------------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 |
| 1 | 2 | 2.000 | 5.00 | 10.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 |
| 3 | 3 | 3.000 | 15.00 | 45.00 |
+---------------+------------+----------+------------+------------+
5 rows in set (0.00 sec)
```
销售单头表demo.transactionhead中包括流水单号、收款机编号、会员编号、操作员编号、交易时间。
```
mysql&gt; SELECT *
-&gt; FROM demo.transactionhead;
+---------------+------------------+-----------+----------+------------+---------------------+
| transactionid | transactionno | cashierid | memberid | operatorid | transdate |
+---------------+------------------+-----------+----------+------------+---------------------+
| 1 | 0120201201000001 | 1 | 1 | 1 | 2020-12-01 00:00:00 |
| 2 | 0120201201000002 | 1 | NULL | 1 | 2020-12-01 00:00:00 |
| 3 | 0120201202000001 | 1 | NULL | 1 | 2020-12-02 00:00:00 |
+---------------+------------------+-----------+----------+------------+---------------------+
3 rows in set (0.00 sec)
```
商品信息表demo.goodsmaster中包括商品编号、商品条码、商品名称、规格、单位、售价和平均进价。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | NULL | 本 | 89.00 | 33.50 |
| 2 | 0002 | 笔 | NULL | 支 | 5.00 | 3.50 |
| 3 | 0003 | 胶水 | NULL | 瓶 | 15.00 | 11.00 |
+------------+---------+-----------+---------------+------+------------+----------------+
3 rows in set (0.00 sec)
```
存储过程会用刚刚的三个表中的数据进行计算,并且把计算的结果存储到下面的这个单品统计表中。
```
mysql&gt; DESCRIBE demo.dailystatistics;
+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| itemnumber | int | YES | MUL | NULL | |
| quantity | decimal(10,3) | YES | | NULL | |
| actualvalue | decimal(10,2) | YES | | NULL | |
| cost | decimal(10,2) | YES | | NULL | |
| profit | decimal(10,2) | YES | | NULL | |
| profitratio | decimal(10,4) | YES | | NULL | |
| salesdate | datetime | YES | MUL | NULL | |
+-------------+---------------+------+-----+---------+----------------+
8 rows in set (0.01 sec)
```
我们现在就来创建一个存储过程,完成单品销售统计的计算。我来讲一讲具体的思路。
第一步我们把SQL语句的分隔符改为“//”。因为存储过程中包含很多SQL语句如果不修改分隔符的话MySQL会在读到第一个SQL语句的分隔符“;”的时候,认为语句结束并且执行,这样就会导致错误。
第二步我们来创建存储过程把要处理的日期作为一个参数传入关于参数下面我会具体讲述。同时用BEGIN和END关键字把存储过程中的SQL语句包裹起来形成存储过程的程序体。
第三步在程序体中先定义2个数据类型为DATETIME的变量用来记录要计算数据的起始时间和截止时间。
第四步,删除保存结果数据的单品统计表中相同时间段的数据,目的是防止数据重复。
第五步,计算起始时间和截止时间内单品的销售数量合计、销售金额合计、成本合计、毛利和毛利率,并且把结果存储到单品统计表中。
这五个步骤,我们就可以用下面的代码来实现。
```
mysql&gt; DELIMITER // -- 设置分割符为//
-&gt; CREATE PROCEDURE demo.dailyoperation(transdate TEXT)
-&gt; BEGIN -- 开始程序体
-&gt; DECLARE startdate,enddate DATETIME; -- 定义变量
-&gt; SET startdate = date_format(transdate,'%Y-%m-%d'); -- 给起始时间赋值
-&gt; SET enddate = date_add(startdate,INTERVAL 1 DAY); -- 截止时间赋值为1天以后
-&gt; -- 删除原有数据
-&gt; DELETE FROM demo.dailystatistics
-&gt; WHERE
-&gt; salesdate = startdate;
-&gt; -- 插入新计算的数据
-&gt; INSERT into dailystatistics
-&gt; (
-&gt; salesdate,
-&gt; itemnumber,
-&gt; quantity,
-&gt; actualvalue,
-&gt; cost,
-&gt; profit,
-&gt; profitratio
-&gt; )
-&gt; SELECT
-&gt; LEFT(b.transdate,10),
-&gt; a.itemnumber,
-&gt; SUM(a.quantity), -- 数量总计
-&gt; SUM(a.salesvalue), -- 金额总计
-&gt; SUM(a.quantity*c.avgimportprice), -- 计算成本
-&gt; SUM(a.salesvalue-a.quantity*c.avgimportprice), -- 计算毛利
-&gt; CASE sum(a.salesvalue) WHEN 0 THEN 0
-&gt; ELSE round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue),4) END -- 计算毛利率
-&gt; FROM
-&gt; demo.transactiondetails AS a
-&gt; JOIN
-&gt; demo.transactionhead AS b
-&gt; ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c
-&gt; ON (a.itemnumber=c.itemnumber)
-&gt; WHERE
-&gt; b.transdate&gt;startdate AND b.transdate&lt;enddate
-&gt; GROUP BY
-&gt; LEFT(b.transdate,10),a.itemnumber
-&gt; ORDER BY
-&gt; LEFT(b.transdate,10),a.itemnumber;
-&gt; END
-&gt; // -- 语句结束,执行语句
Query OK, 0 rows affected (0.01 sec)
-&gt; DELIMITER ; -- 恢复分隔符为;
```
这样,我们的存储过程就创建成功了。
在这个存储过程中,我们用到了存储过程的参数定义和程序体,这些具体是什么意思呢?我们来学习下。
### 存储过程的参数定义
存储过程可以有参数,也可以没有参数。一般来说,当我们通过客户端或者应用程序调用存储过程的时候,如果需要与存储过程进行数据交互,比如,存储过程需要根据输入的数值为基础进行某种数据处理和计算,或者需要把某个计算结果返回给调用它的客户端或者应用程序,就需要设置参数。否则,就不用设置参数。
参数有3种分别是IN、OUT和INOUT。
- IN表示输入的参数存储过程只是读取这个参数的值。如果没有定义参数种类默认就是IN表示输入参数。
- OUT表示输出的参数存储过程在执行的过程中把某个计算结果值赋给这个参数执行完成之后调用这个存储过程的客户端或者应用程序就可以读取这个参数返回的值了。
- INOUT表示这个参数既可以作为输入参数又可以作为输出参数使用。
**除了定义参数种类,还要对参数的数据类型进行定义**。在这个存储过程中我定义了一个参数transdate 的数据类型是TEXT。这个参数的用处是告诉存储过程我要处理的是哪一天的数据。我没有指定参数种类是IN、OUT或者INOUT这是因为在MySQL中**如果不指定参数的种类默认就是IN表示输入参数**。
知道了参数下面我具体讲解一下这个存储过程的程序体。存储过程的具体操作步骤都包含在程序体里面我们来分析一下程序体中SQL操作的内容就可以知道存储过程到底在做什么。
### 存储过程的程序体
程序体中包含的是存储过程需要执行的SQL语句一般通过关键字BEGIN表示SQL语句的开始通过END表示SQL语句的结束。
在程序体的开始部分我定义了2个变量分别是startdate和enddate。它们都是DATETIME类型作用是根据输入参数transdate计算出需要筛选的数据的时间区间。
后面的代码分3步完成起始时间和截止时间的计算并且分别赋值给变量startdate和enddate。
第一步使用DATE_FROMAT函数把输入的参数按照YYYY年MM月DD日的格式转换成了日期时间类型数据比如输入参数是“2020-12-01”那么转换成的日期时间值是“2020-12-01 00:00:00”表示2020年12月01日00点00分00秒。
第二步把第一步中计算出的值作为起始时间赋值给变量startdate。
第三步把第一步中计算出的值通过DATE_ADD函数计算出1天以后的时间赋值给变量enddate。
这样,我就获得了需要计算的销售时段。计算出了起始时间和截止时间之后,我们先删除需要计算日期的单品统计数据,以防止数据重复。接着,我们重新计算单品的销售统计,并且把计算的结果插入到单品统计表。
在计算单品销售统计的时候也分为3步
1. 按照“成本=销售数量×平均价格”的方式计算成本;
1. 按照“毛利=销售金额 - 成本”的方式计算毛利;
1. 按照“毛利率 = 毛利 ÷销售金额”的方式计算毛利率。
需要注意的是这里我使用CASE函数来解决销售金额为0时计算毛利的问题。这是为了防止计算出现被0除而报错的情况。不要以为销售金额就一定大于0在实际项目运行的过程中会出现因为优惠而导致实际销售金额为0的情况。我建议你在实际工作中把这些极端情况都考虑在内提前进行防范这样你的代码才能稳定可靠。
存储过程通过开始时定义的分隔符“//”结束MySQL执行这段SQL语句就创建出了一个存储过程demo.dailyoperation.最后,你不要忘了把分隔符改回到“;”。
创建完之后,怎么知道我们创建的存储过程是否成功了呢?下面我介绍一下查看存储过程的方法。
## 如何查看存储过程?
我们可以通过SHOW CREATE PROCEDURE 存储过程名称,来查看刚刚创建的存储过程:
```
mysql&gt; SHOW CREATE PROCEDURE demo.dailyoperation \G
*************************** 1. row ***************************
Procedure: dailyoperation -- 存储过程名
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
Create Procedure: CREATE DEFINER=`root`@`localhost` PROCEDURE `dailyoperation`(transdate TEXT) -- 存储过程名
BEGIN -- 开始程序体
DECLARE startdate,enddate DATETIME;
SET startdate = date_format(transdate,'%Y-%m-%d');
SET enddate = date_add(startdate,INTERVAL 1 DAY);
DELETE FROM demo.dailystatistics -- 删除重复
WHERE
salesdate = startdate;
INSERT into dailystatistics -- 将计算结果插入每日统计表
(
salesdate,
itemnumber,
quantity,
actualvalue,
cost,
profit,
profitratio
)
SELECT
LEFT(b.transdate,10),
a.itemnumber,
SUM(a.quantity),
SUM(a.salesvalue),
SUM(a.quantity*c.avgimportprice),
SUM(a.salesvalue-a.quantity*c.avgimportprice),
CASE sum(a.salesvalue) WHEN 0 THEN 0
ELSE round(sum(a.salesvalue-a.quantity*c.avgimportprice)/sum(a.salesvalue),4) END
FROM
demo.transactiondetails AS a
JOIN
demo.transactionhead AS b
ON (a.transactionid = b.transactionid)
JOIN
demo.goodsmaster c
ON (a.itemnumber=c.itemnumber)
WHERE
b.transdate&gt;startdate AND b.transdate&lt;enddate
GROUP BY
LEFT(b.transdate,10),a.itemnumber
ORDER BY
LEFT(b.transdate,10),a.itemnumber;
END -- 结束程序体
character_set_client: gbk -- 采用的字符集gbk
collation_connection: gbk_chinese_ci -- 连接校对采用的字符集
Database Collation: utf8mb4_0900_ai_ci -- 数据校对字符集
1 row in set (0.00 sec)
```
## 如何调用存储过程?
下面我们来尝试调用一下这个存储过程并且给它传递一个参数“2020-12-01”也就是计算2020年12月01日的单品统计数据
```
mysql&gt; CALL demo.dailyoperation('2020-12-01');
Query OK, 2 rows affected (0.03 sec)
```
存储过程执行结果提示“Query OK”表示执行成功了。“2 rows affected”表示执行的结果影响了2条数据记录。
我们用SELECT语句来查看一下单品统计表看看有没有把单品统计的结果存入单品统计表中。
```
mysql&gt; SELECT * -- 查询单品统计表中的数据
-&gt; FROM demo.dailystatistics;
+----+------------+----------+-------------+--------+--------+-------------+---------------------+
| id | itemnumber | quantity | actualvalue | cost | profit | profitratio | salesdate |
+----+------------+----------+-------------+--------+--------+-------------+---------------------+
| 13 | 1 | 3.000 | 267.00 | 100.50 | 166.50 | 0.6236 | 2020-12-01 00:00:00 | -- 编号是1的商品单品统计结果
| 14 | 2 | 2.000 | 10.00 | 7.00 | 3.00 | 0.3000 | 2020-12-01 00:00:00 | -- 编号是2的商品单品统计的结果
+----+------------+----------+-------------+--------+--------+-------------+---------------------+
2 rows in set (0.00 sec)
```
看到了吗我们已经能够在单品统计表中查询到2020年12月01日的单品统计结果了。这也就意味着我们的存储过程被执行了它计算出了我们需要的单品统计结果并且把统计结果存入了单品统计表中。
## 如何修改和删除存储过程?
如果你需要修改存储过程的内容我建议你在Workbench中操作。这是因为你可以在里面直接修改存储过程而如果用SQL命令来修改存储过程就必须删除存储过程再重新创建相比之下在Workbench中修改比较简单。
具体的做法是在左边的导航栏找到数据库demo展开之后找到存储过程stored procedure然后找到我们刚刚创建的dailyoperation点击右边的设计按钮就可以在右边的工作区进行修改了。
<img src="https://static001.geekbang.org/resource/image/98/bb/98058c5a4b4f2517ef83d22895dd96bb.png" alt="">
修改完成之后点击工作区右下方的按钮“Apply”保存修改。
在MySQL中存储过程不像普通的编程语言比如VC++、Java等那样有专门的集成开发环境。因此你可以通过SELECT语句把程序执行的中间结果查询出来来调试一个SQL语句的正确性。调试成功之后把SELECT语句后移到下一个SQL语句之后再调试下一个SQL语句。这样逐步推进就可以完成对存储过程中所有操作的调试了。当然你也可以把存储过程中的SQL语句复制出来逐段单独调试。
删除存储过程很简单,你知道具体的语法就行了:
```
DROP PROCEDURE 存储过程名称;
```
## 总结
这节课,我们学习了创建、查看、修改和删除存储过程的具体方法。
存储过程的优点就是执行效率高,而且更加安全,不过,它也有着自身的缺点,那就是开发和调试的成本比较高,而且不太容易维护。
在存储过程开发的过程中虽然也有一些第三方工具可以对存储过程进行调试但要收费。我建议你通过SELECT语句输出变量值的办法进行调试虽然有点麻烦但是成本低而且简单可靠。如果你的存储过程需要随产品一起分发可以考虑把脚本放在安装程序中在产品安装的过程中创建需要的存储过程。
## 思考题
请写一个简单的存储过程要求是定义2个参数一个输入参数a数据类型是INT另一个输出参数是b类型是INT。程序体完成的操作是b = a + 1。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,306 @@
<audio id="audio" title="16 | 游标:对于数据集中的记录,该怎么逐条处理?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/da/fd/da01c9fbaa9dbd0c468c9089e7fdf5fd.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊游标。
咱们前面学习的MySQL数据操作语句都是针对结果集合的。也就是说每次处理的对象都是一个数据集合。如果需要逐一处理结果集中的记录就会非常困难。
虽然我们也可以通过筛选条件WHERE和HAVING或者是限定返回记录的关键字LIMIT返回一条记录但是却无法在结果集中像指针一样向前定位一条记录、向后定位一条记录或者是随意定位到某一条记录并对记录的数据进行处理。
这个时候,就可以用到游标。所谓的游标,也就是能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。
这么说可能有点抽象我举一个生活中的例子你一看就明白了。比如你想去某个城市旅游现在需要订酒店。你打开预订酒店的App设置好价格区间后进行搜索得到了一个酒店列表。接下来你可能要逐条查看列表中每个酒店的客户评价最后选择一个口碑不错的酒店。这个逐条搜索并对选中的数据进行操作的过程就相当于游标对数据记录进行操作的过程。
今天我就来给你讲一讲游标的使用方法,同时还会通过一个案例串讲,帮助你更好地使用游标,让你能够轻松地处理数据集中的记录。
# 游标的使用步骤
游标只能在存储程序内使用,存储程序包括存储过程和存储函数。关于存储过程,我们上节课刚刚学过,这里我简单介绍一下存储函数。创建存储函数的语法是:
```
CREATE FUNCTION 函数名称 参数RETURNS 数据类型 程序体
```
存储函数与存储过程很像,但有几个不同点:
1. 存储函数必须返回一个值或者数据表,存储过程可以不返回。
1. 存储过程可以通过CALL语句调用存储函数不可以。
1. 存储函数可以放在查询语句中使用,存储过程不行。
1. 存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。
这节课,我们主要学习下游标在存储过程中的使用方法,因为游标在存储过程中更常用。游标在存储函数中的使用方法和在存储过程中的使用方法是一样的。
在使用游标的时候主要有4个步骤。
第一步,定义游标。语法结构如下:
```
DECLARE 游标名 CURSOR FOR 查询语句
```
这里就是声明一个游标,它可以操作的数据集是“查询语句”返回的结果集。
第二步,打开游标。语法结构如下:
```
OPEN 游标名称;
```
打开游标之后,系统会为游标准备好查询的结果集,为后面游标的逐条读取结果集中的记录做准备。
第三步,从游标的数据结果集中读取数据。语法结构是这样的:
```
FETCH 游标名 INTO 变量列表;
```
这里的意思是通过游标,把当前游标指向的结果集中那一条记录的数据,赋值给列表中的变量。
需要注意的是,**游标的查询结果集中的字段数必须跟INTO后面的变量数一致**否则在存储过程执行的时候MySQL会提示错误。
第四步,关闭游标。语法结构如下:
```
CLOSE 游标名;
```
**用完游标之后,你一定要记住及时关闭游标**。因为游标会占用系统资源,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放游标占用的系统资源。
知道了基本步骤,下面我就结合超市项目的实际案例,带你实战一下。
# 案例串讲
在超市项目的进货模块中,有一项功能是对进货单数据进行验收。其实就是在对进货单的数据确认无误后,对进货单的数据进行处理,包括增加进货商品的库存,并修改商品的平均进价。下面我用实际数据来演示一下这个操作流程。
这里我们要用到进货单头表demo.importheadl、进货单明细表demo.importdetails、库存表demo.inventory和商品信息表demo.goodsmaster
进货单头表:
<img src="https://static001.geekbang.org/resource/image/aa/97/aa133989325a1125ff118bce9993ec97.jpeg" alt="">
进货单明细表:
<img src="https://static001.geekbang.org/resource/image/a5/b3/a5b6af4f27a24e003bced20e044336b3.jpeg" alt="">
库存表:
<img src="https://static001.geekbang.org/resource/image/1e/d3/1e1c6d83d75094376bf21e6f4c0572d3.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/cd/8e/cd9832d7cac00446b937462518bf018e.jpeg" alt="">
要验收进货单,我们就需要对每一个进货商品进行两个操作:
1. 在现有库存数量的基础上,加上本次进货的数量;
1. 根据本次进货的价格、数量,现有商品的平均进价和库存,计算新的平均进价:(本次进货价格 * 本次进货数量+现有商品平均进价 * 现有商品库存)/(本次进货数量+现有库存数量)。
针对这个操作,如果只用我们在[第4讲](https://time.geekbang.org/column/article/351225)里学习的SQL语句完成起来就比较困难。
因为我们需要通过应用程序来控制操作流程做成一个循环操作每次只查询一种商品的数据记录并进行处理一直到把进货单中的数据全部处理完。这样一来应用必须发送很多的SQL指令到服务器跟服务器的交互多不仅代码复杂而且也不够安全。
这个时候,如果使用游标,就很容易了。因为所有的操作都可以在服务器端完成,应用程序只需要发送一个命令调用存储过程就可以了。现在,我们就来看看如何用游标来解决这个问题。
我用代码创建了一个存储过程demo.mytest。当然你也完全可以在Workbench中创建存储过程非常简单我就不多说了。创建存储过程的代码如下
```
mysql&gt; DELIMITER //
mysql&gt; CREATE PROCEDURE demo.mytest(mylistnumber INT)
-&gt; BEGIN
-&gt; DECLARE mystockid INT;
-&gt; DECLARE myitemnumber INT;
-&gt; DECLARE myquantity DECIMAL(10,3);
-&gt; DECLARE myprice DECIMAL(10,2);
-&gt; DECLARE done INT DEFAULT FALSE; -- 用来控制循环结束
-&gt; DECLARE cursor_importdata CURSOR FOR -- 定义游标
-&gt; SELECT b.stockid,a.itemnumber,a.quantity,a.importprice
-&gt; FROM demo.importdetails AS a
-&gt; JOIN demo.importhead AS b
-&gt; ON (a.listnumber=b.listnumber)
-&gt; WHERE a.listnumber = mylistnumber;
-&gt; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; -- 条件处理语句
-&gt;
-&gt; OPEN cursor_importdata; -- 打开游标
-&gt; FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice; -- 读入第一条记录
-&gt; REPEAT
-&gt; -- 更新进价
-&gt; UPDATE demo.goodsmaster AS a,demo.inventory AS b
-&gt; SET a.avgimportprice = (a.avgimportprice*b.invquantity+myprice*myquantity)/(b.invquantity+myquantity)
-&gt; WHERE a.itemnumber=b.itemnumber AND b.stockid=mystockid AND a.itemnumber=myitemnumber;
-&gt; -- 更新库存
-&gt; UPDATE demo.inventory
-&gt; SET invquantity = invquantity + myquantity
-&gt; WHERE stockid = mystockid AND itemnumber=myitemnumber;
-&gt; -- 获取下一条记录
-&gt; FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice;
-&gt; UNTIL done END REPEAT;
-&gt; CLOSE cursor_importdata;
-&gt; END
-&gt; //
Query OK, 0 rows affected (0.02 sec)
-&gt; DELIMITER ;
```
这段代码比较长核心操作有6步我来给你详细解释下。
1. 把MySQL的分隔符改成“//”。
1. 开始程序体之后我定义了4个变量分别是mystockid、myitemnumber、myquantity和myprice这几个变量的作用是存储游标中读取的仓库编号、商品编号、进货数量和进货价格数据。
1. 定义游标。这里我指定了游标的名称以及游标可以处理的数据集mylistnumber指定的进货单的全部进货商品明细数据
1. 定义条件处理语句“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”。
1. 打开游标,读入第一条记录,然后开始执行数据操作。
1. 关闭游标,结束程序。
可以看到,在这个存储过程中,我用到了条件处理语句,它的作用是告诉系统,在存储程序遇到问题的时候,应该如何处理。
## 条件处理语句
条件处理语句的语法结构:
```
DECLARE 处理方式 HANDLER FOR 问题 操作;
```
下面我结合刚刚的“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”,来解释一下条件处理语句是如何工作的。
1. **语法结构中的“问题”是指SQL操作中遇到了什么问题**。比如这里的问题是“NOT FOUND”意思就是游标走到结果集的最后没有记录了。也就是说数据集中的所有记录都已经处理完了。
1. 执行的操作是“SET done=TRUE”done是我定义的用来标识数据集中的数据是否已经处理完成的一个标记。done=TRUE意思是数据处理完成了。
1. **处理方式有2种选择分别是“CONTINUE”和“EXIT”**,表示遇到问题,执行了语法结构中的“操作”之后,是选择继续运行程序,还是选择退出,结束程序。
所以,这个条件处理语句的意思就是:当游标读到结果集的最后,没有记录了,设置操作完成标识为真,然后继续运行程序。
在存储过程的第5步为了逐一处理每一条记录我还使用了流程控制语句。
解决复杂问题不可能通过一个SQL语句完成我们需要执行多个SQL操作。流程控制语句的作用就是控制存储过程中SQL语句的执行顺序是我们完成复杂操作必不可少的一部分。下面我就给你具体讲解一下。
## 流程控制语句
MySQL的流程控制语句也只能用于存储程序。主要有3类。
1. 跳转语句ITERATE和LEAVE语句。
1. 循环语句LOOP、WHILE和REPEAT语句。
1. 条件判断语句IF语句和CASE语句。
接下来我依次讲解一下跳转语句、循环语句和条件判断语句。
### 跳转语句
1. ITERATE语句只能用在循环语句内表示重新开始循环。
1. LEAVE语句可以用在循环语句内或者以BEGIN和END包裹起来的程序体内表示跳出循环或者跳出程序体的操作。
### 循环语句
LOOP语句的语法结构是
```
标签LOOP
操作
END LOOP 标签;
```
关于这个语句需要注意的是LOOP循环不能自己结束需要用跳转语句ITERATE或者LEAVE来进行控制。
WHILE语句的语法结构
```
WHILE 条件 DO
操作
END WHILE;
```
WHILE循环通过判断条件是否为真来决定是否继续执行循环中的操作你要注意一点**WHILE循环是先判断条件再执行循环体中的操作**。
REPEAT语句的语法结构
```
REPEAT
操作
UNTIL 条件 END REPEAT
```
REPEAT循环也是通过判断条件是否为真来决定是否继续执行循环内的操作的与WHILE不同的是**REPEAT循环是先执行操作后判断条件**。
最后我来讲讲条件判断语句IF语句和CASE语句。
### 条件判断语句
IF语句的语法结构是
```
IF 表达式1 THEN 操作1
[ELSEIF 表达式2 THEN 操作2]……
[ELSE 操作N]
END IF
```
这里“[]”中的内容是可选的。**IF语句的特点是不同的表达式对应不同的操作**。
CASE语句的语法结构是
```
CASE 表达式
WHEN 值1 THEN 操作1
[WHEN 值2 THEN 操作2]……
[ELSE 操作N]
END CASE;
```
这里“[]”中的内容是可选的。CASE语句的特点是表达式不同的值对应不同的操作。
到这里,我们处理进货单验收的存储过程就创建好了。现在,让我们来运行一下这个存储过程,看看能不能得到我们想要的结果:
```
mysql&gt; CALL demo.mytest(1234); -- 调用存储过程验收单号是1234的进货单
Query OK, 0 rows affected (11.68 sec) -- 执行成功了
mysql&gt; select * from demo.inventory; -- 查看库存,已经改过来了
+---------+------------+-------------+
| stockid | itemnumber | invquantity |
+---------+------------+-------------+
| 1 | 1 | 15.000 |
| 1 | 2 | 23.000 |
+---------+------------+-------------+
2 rows in set (0.00 sec)
mysql&gt; select * from demo.goodsmaster; -- 查看商品信息表,平均进价也改过来了
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+----------------+
2 rows in set (0.00 sec)
```
很显然,库存和平均价格都被正确地计算出来了。
最后,有个小问题要提醒你注意:**如果一个操作要用到另外一个操作的结果,那我们一定不能搞错操作的顺序**。比如,在刚刚的例子中,我是先计算平均价格,后消减库存数量,这就是因为,计算平均价格的时候会用到库存数量,如果先消减库存数量,平均价格的计算就会不准。
# 总结
这节课我们学习了游标的使用方法包括在存储过程中使用游标的4个步骤分别是定义游标、打开游标、读取游标数据和关闭游标。除此之外我还介绍了经常与游标结合使用的流程控制语句包括循环语句LOOP、WHILE和REPEAT条件判断语句IF和CASE还有跳转语句LEAVE和ITERATE。
游标是MySQL的一个重要的功能为逐条读取结果集中的数据提供了完美的解决方案。跟在应用层面实现相同的功能相比游标可以在存储程序中使用效率高程序也更加简洁。但是游标会消耗系统资源所以我建议你养成用完之后就关闭的习惯这样才能提高系统的整体效率。
# 思考题
假设我有一个数据表demo.test具体信息如下所示
<img src="https://static001.geekbang.org/resource/image/a6/44/a62ccd9ea5e88cfa793dc837a1168f44.jpeg" alt="">
你能自己写一个简单的存储过程,用游标来逐一处理一个数据表中的数据吗?
要求编号为偶数的记录myquant=myquant+1编号是奇数的记录myquant=myquant+2。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,431 @@
<audio id="audio" title="17 | 触发器:如何让数据修改自动触发关联操作,确保数据一致性?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/97/81/97d0bcc17f014yy02b4e5416ff27e981.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊触发器。
在实际开发中我们经常会遇到这样的情况有2个或者多个相互关联的表如商品信息和库存信息分别存放在2个不同的数据表中我们在添加一条新商品记录的时候为了保证数据的完整性必须同时在库存表中添加一条库存记录。
这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用事务包裹起来,确保这两个操作成为一个原子操作,要么全部执行,要么全部不执行。要是遇到特殊情况,可能还需要对数据进行手动维护,这样就很容易忘记其中的一步,导致数据缺失。
这个时候,其实咱们可以使用触发器。你可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。这样一来,就不用担心因为忘记添加库存数据而导致的数据缺失了。
听上去好像很不错,那触发器到底怎么使用呢?接下来,我就重点给你聊聊。我会先给你讲解创建、查看和删除触发器的具体操作,然后借助一个案例带你实战一下。
## 如何操作触发器?
首先,咱们来学习下触发器的基本操作。
### 创建触发器
创建触发器的语法结构是:
```
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
```
在创建时,你一定要注意触发器的三个要素。
- 表名:表示触发器监控的对象。
- INSERT|UPDATE|DELETE表示触发的事件。INSERT表示插入记录时触发UPDATE表示更新记录时触发DELETE表示删除记录时触发。
- BEFORE|AFTER表示触发的时间。BEFORE表示在事件之前触发AFTER表示在事件之后触发。
只有把这三个要素定义好,才能正确使用触发器。
创建好触发器之后,咱们还要知道触发器是不是创建成功了。怎么查看呢?我来介绍下。
### 查看触发器
查看触发器的语句是:
```
SHOW TRIGGERS\G;
```
### 删除触发器
删除触发器很简单,你只要知道语法结构就可以了:
```
DROP TRIGGER 触发器名称;
```
知道了触发器的操作方法,接下来咱们就借助超市项目的实际案例,在真实的场景中实战一下,毕竟,实战是掌握操作的最好方法。
## 案例讲解
超市项目实际实施过程中,客户经常要查询储值余额变动的明细,但是,查询会员消费流水时,存在数据汇总不及时、查询速度比较慢的问题。这时,我们就想到用触发器,及时把会员储值金额的变化信息记录到一个专门的表中。
我先用咱们熟悉的SQL语句来实现记录储值金额变动的操作后面再带你使用触发器来操作。通过两种操作的对比你就能更好地理解在什么情况下触发器能够比普通的SQL语句更加简洁高效从而帮助你用好触发器这个工具提高开发的能力。
下面我就借助具体数据来详细说明一下。这里我们需要用到会员信息表demo.membermaster和会员储值历史表demo.deposithist
会员信息表:
<img src="https://static001.geekbang.org/resource/image/26/34/26166082e21c2cbd38e4c21d03e76734.jpeg" alt="">
会员储值历史表:
<img src="https://static001.geekbang.org/resource/image/f0/c5/f0b59f65801543af6c52b208c71925c5.jpeg" alt="">
假如在2020年12月20日这一天会员编号是2的会员李四到超市的某家连锁店购买了一条烟消费了150元。现在我们用之前学过的SQL语句把这个会员储值余额的变动情况记录到会员储值历史表中。
第一步查询出编号是2的会员卡的储值金额是多少。我们可以用下面的代码来实现
```
mysql&gt; SELECT memberdeposit
-&gt; FROM demo.membermaster
-&gt; WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 200.00 |
+---------------+
1 row in set (0.00 sec)
```
第二步我们把会员编号是2的会员的储值金额减去150。
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET memberdeposit = memberdeposit - 150
-&gt; WHERE memberid = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
第三步读出会员编号是2的会员当前的储值金额。
```
mysql&gt; SELECT memberdeposit
-&gt; FROM demo.membermaster
-&gt; WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 50.00 |
+---------------+
1 row in set (0.00 sec)
```
第四步,把会员编号和前面查询中获得的储值起始金额、储值余额和储值金额变化值,写入会员储值历史表。
```
mysql&gt; INSERT INTO demo.deposithist
-&gt; (
-&gt; memberid,
-&gt; transdate,
-&gt; oldvalue,
-&gt; newvalue,
-&gt; changedvalue
-&gt; )
-&gt; SELECT 2,NOW(),200,50,-150;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
```
这样,我们就完成了记录会员储值金额变动的操作。现在,我们来查询一下记录的结果:
```
mysql&gt; SELECT *
-&gt; FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
+----+----------+---------------------+----------+----------+--------------+
1 row in set (0.00 sec)
```
结果显示会员编号是2的会员卡储值金额在2020-12-20 10:37:51时有变动变动前是200元变动后是50元减少了150元。
你看这个记录会员储值金额变动的操作非常复杂我们用了4步才完成。而且为了确保数据的一致性我们还要用事务把这几个关联的操作包裹起来这样一来消耗的资源就比较多。
那如果用触发器来实现,效果会怎样呢?我们来实操一下。
首先我们创建一个数据表demo.membermaster的触发器。每当更新表中的数据时先触发触发器如果发现会员储值金额有变化就把会员编号信息、更新的时间、更新前的储值金额、更新后的储值金额和变动金额写入会员储值历史表。然后再执行会员信息表的更新操作。
创建触发器的代码如下所示:
```
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE -- 在更新前触发
ON demo.membermaster
FOR EACH ROW -- 表示每更新一条记录,触发一次
BEGIN -- 开始程序体
IF (new.memberdeposit &lt;&gt; old.memberdeposit) -- 如果储值金额有变化
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit, -- 更新前的储值金额
NEW.memberdeposit, -- 更新后的储值金额
NEW.memberdeposit-OLD.memberdeposit; -- 储值金额变化值
END IF;
END
//
DELIMITER ;
```
创建完成之后,我们查看一下触发器,看看是不是真的创建成功了:
```
mysql&gt; SHOW TRIGGERS \G;
*************************** 1. row ***************************
Trigger: upd_membermaster -- 触发器名称
Event: UPDATE -- 触发的事件
Table: membermaster -- 表名称
Statement: BEGIN -- 被触发时要执行的程序体
IF (new.memberdeposit &lt;&gt; old.memberdeposit) -- 储值金额变动时插入历史储值表
THEN
INSERT INTO demo.deposithist
(
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
NEW.memberid,
NOW(),
OLD.memberdeposit,
NEW.memberdeposit,
NEW.memberdeposit-OLD.memberdeposit;
END IF;
END
Timing: BEFORE
Created: 2021-04-03 15:02:48.18
sql_mode: STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
Definer: root@localhost
character_set_client: utf8mb4
collation_connection: utf8mb4_0900_ai_ci
Database Collation: utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
```
从代码中,我们可以知道触发器的具体信息:
- Trigger表示触发器名称这里是upd_membermaster
- Event表示触发事件这里是UPDATE表示更新触发
- Table表示定义触发器的数据表这里是membermaster
- Statement表示触发时要执行的程序体。
看到这些信息,我们就可以确认,触发器创建成功了。
创建成功以后我们尝试更新一下会员编号是1的会员的储值金额这里假设把会员1的储值金额增加10元。简单说明一下会员也可以把钱存到会员卡里需要的时候进行消费对应的就是储值金额的增加。
我们用下面的代码来完成这个操作:
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET memberdeposit = memberdeposit + 10
-&gt; WHERE memberid = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
现在,我们来查看一下会员信息表和会员储值历史表:
```
mysql&gt; SELECT *
-&gt; FROM demo.membermaster;
+----------+----------+------------+---------+-------------+---------------+
| memberid | cardno | membername | address | phone | memberdeposit |
+----------+----------+------------+---------+-------------+---------------+
| 1 | 10000001 | 张三 | 北京 | 13812345678 | 110.00 |
| 2 | 10000002 | 李四 | 天津 | 18512345678 | 50.00 |
+----------+----------+------------+---------+-------------+---------------+
2 rows in set (0.00 sec)
mysql&gt; SELECT *
-&gt; FROM demo.deposithist;
+----+----------+---------------------+----------+----------+--------------+
| id | memberid | transdate | oldvalue | newvalue | changedvalue |
+----+----------+---------------------+----------+----------+--------------+
| 1 | 2 | 2020-12-20 10:37:51 | 200.00 | 50.00 | -150.00 |
| 2 | 1 | 2020-12-20 11:32:09 | 100.00 | 110.00 | 10.00 |
+----+----------+---------------------+----------+----------+--------------+
2 rows in set (0.01 sec)
```
结果显示,触发器正确地记录了修改会员储值金额的操作。
如果你一直跟着我进行操作到这里你可能会有疑问在会员历史储值表中记录修改会员储值金额的操作和实际修改会员信息表是2个操作有没有可能一个成功一个失败比如说记录修改会员储值金额的操作失败了但是会员的储值金额被修改了。
其实在MySQL中如果触发器中的操作失败了那么触发这个触发器的数据操作也会失败不会出现一个成功、一个失败的情况。
我还是借助一个例子来解释下。假设我们创建了一个触发器这个触发器的程序体中的SQL语句有误比如多了一个字段。在这种情况下触发器执行会失败因此数据更新的操作也不会执行。我们用下面的代码来验证一下
```
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE
ON demo.membermaster
FOR EACH ROW
BEGIN
IF (new.memberdeposit &lt;&gt; old.memberdeposit)
THEN
INSERT INTO demo.deposithist
(
aa, -- 不存在的字段
memberid,
transdate,
oldvalue,
newvalue,
changedvalue
)
SELECT
1, -- 给不存在的字段赋值
NEW.memberid,
NOW(),
OLD.memberdeposit,
NEW.memberdeposit,
NEW.memberdeposit-OLD.memberdeposit;
END IF;
END
//
DELIMITER ;
```
现在假设我们要把会员编号是2的会员卡的储值金额更新为20
```
mysql&gt; update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
系统提示因为字段“aa”不存在导致触发器执行失败。现在我们来看看会员储值金额有没有被修改
```
mysql&gt; select * from demo.membermaster;
+----------+----------+------------+---------+-------------+---------------+
| memberid | cardno | membername | address | phone | memberdeposit |
+----------+----------+------------+---------+-------------+---------------+
| 1 | 10000001 | 张三 | 北京 | 13812345678 | 110.00 |
| 2 | 10000002 | 李四 | 天津 | 18512345678 | 50.00 |
+----------+----------+------------+---------+-------------+---------------+
2 rows in set (0.00 sec)
```
结果显示会员储值金额不变。这个时候为了让应用程序知道触发器是否执行成功我们可以通过ROW_COUNT()函数来发现错误:
```
mysql&gt; select row_count();
+-------------+
| row_count() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
结果是-1说明我们可以通过ROW_COUNT()函数捕获到错误。
我们回顾一下对于记录会员储值金额变化的操作可以通过应用层发出SQL语句指令或者用一个存储过程来实现。无论哪种方式都要通过好几步相互关联的操作而且要做成一个事务处理过程复杂消耗的资源也较多。如果用触发器效率就会提高很多消耗的资源也少。同时还可以起到事务的类似功能保证关联操作全部完成或全部失败。
## 触发器的优缺点
通过刚刚的案例,你应该感受到触发器的高效了。现在,咱们把视角拔高一下,来看看触发器具体都有什么优缺点。毕竟,知己知彼,才能百战不殆。只有非常清楚它的优缺点,你才能充分发挥它的作用。
我先来说说触发器的优点。
**首先,触发器可以确保数据的完整性**。这是怎么体现的呢?我来举个小例子。
假设我们用进货单头表demo.importhead来保存进货单的总体信息包括进货单编号、供货商编号、仓库编号、总计进货数量、总计进货金额和验收日期。
<img src="https://static001.geekbang.org/resource/image/5b/e8/5b36cf11405b0159a1388ee23639aee8.jpeg" alt="">
用进货单明细表demo.importdetails来保存进货商品的明细包括进货单编号、商品编号、进货数量、进货价格和进货金额。
<img src="https://static001.geekbang.org/resource/image/b8/0e/b8e3fc8e001da9af2c85c22be5e6780e.jpeg" alt="">
每当我们录入、删除和修改一条进货单明细数据的时候,进货单明细表里的数据就会发生变动。这个时候,在进货单头表中的总计数量和总计金额就必须重新计算,否则,进货单头表中的总计数量和总计金额就不等于进货单明细表中数量合计和金额合计了,这就是数据不一致。
为了解决这个问题我们就可以使用触发器规定每当进货单明细表有数据插入、修改和删除的操作时自动触发2步操作
1. 重新计算进货单明细表中的数量合计和金额合计;
1. 用第一步中计算出来的值更新进货单头表中的合计数量与合计金额。
这样一来,进货单头表中的合计数量与合计金额的值,就始终与进货单明细表中计算出来的合计数量与合计金额的值相同,数据就是一致的,不会互相矛盾。
**其次,触发器可以帮助我们记录操作日志。**
利用触发器,可以具体记录什么时间发生了什么。我们前面的记录修改会员储值金额的触发器,就是一个很好的例子。这对我们还原操作执行时的具体场景,更好地定位问题原因很有帮助。
**另外,触发器还可以用在操作数据前,对数据进行合法性检查。**
举个小例子。超市进货的时候,需要库管录入进货价格。但是,人为操作很容易犯错误,比如说在录入数量的时候,把条形码扫进去了;录入金额的时候,看串了行,录入的价格远超售价,导致账面上的巨亏……这些都可以通过触发器,在实际插入或者更新操作之前,对相应的数据进行检查,及时提示错误,防止错误数据进入系统。
说了这么多触发器的优点,那是不是所有事件可以驱动的操作,都应该用触发器呢?要是你这么想,就掉坑里了。
下面我来说说触发器的缺点。
**触发器最大的一个问题就是可读性差。**
因为触发器存储在数据库中,并且由事件驱动,这就意味着触发器有可能不受应用层的控制。这对系统维护是非常有挑战的。
这是啥意思呢?我举个例子,你一看就明白了。
还是拿我们创建触发器时讲到的修改会员储值操作的那个触发器为例。如果触发器中的操作出了问题,会导致会员储值金额更新失败。我用下面的代码演示一下:
```
mysql&gt; update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
结果显示系统提示错误字段“aa”不存在。
这是因为触发器中的数据插入操作多了一个字段系统提示错误。可是如果你不了解这个触发器很可能会认为是更新语句本身的问题或者是会员信息表的结构出了问题。说不定你还会给会员信息表添加一个叫“aa”的字段试图解决这个问题结果只能是白费力。
另外,相关数据的变更,特别是数据表结构的变更,都可能会导致触发器出错,进而影响数据操作的正常运行。这些都会由于触发器本身的隐蔽性,影响到应用中错误原因排查的效率。
## 总结
今天这节课,我给你介绍了如何操作触发器。为了方便你学习,我汇总了相关的语法结构:
```
创建触发器的语法结构是
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
查看触发器的语句是:
SHOW TRIGGERS\G;
删除触发器的语法结构是:
DROP TRIGGER 触发器名称;
```
除此之外,我们还学习了触发器的优缺点。它的优点是可以确保数据一致性、记录操作日志和检查数据合法性。不过,它也存在可读性差,会增加系统维护的成本的缺点。在使用触发器的时候,你一定要综合考量。
最后,我还想再给你提一个小建议:**维护一个完整的数据库设计文档**。因为运维人员可能会经常变动,如果有一个完整的数据库设计文档,就可以帮助新人快速了解触发器的设计思路,从而减少错误,降低系统维护的成本。
## 思考题
我在课程中提到,每当在进货单明细表中插入或修改数据的时候,都要更新进货单头表中的总计数量和总计金额,这个问题可以用触发器来解决。你能不能创建一个触发器,要求是当操作人员更新进货单明细表中相关数据的时候,自动触发对进货单头表中相关数据的更改,确保数据的一致性?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,316 @@
<audio id="audio" title="18 | 权限管理:如何控制数据库访问,消除安全隐患?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d4/33/d4c1ce6debd0c28a2272429250a58433.mp3"></audio>
你好,我是朱晓峰,今天,我来和你聊一聊权限管理。
我们在开发应用的时候,经常会遇到一种需求,就是要根据用户的不同,对数据进行横向和纵向的分组。
所谓横向的分组,就是指用户可以接触到的数据的范围,比如可以看到哪些表的数据;所谓纵向的分组,就是指用户对接触到的数据能访问到什么程度,比如能看、能改,甚至是删除。
我们把具有相同数据访问范围和程度的用户归成不同的类别,这种类别就叫角色。通过角色,管理用户对数据库访问的范围和程度就更加方便了。这也就是对用户的数据访问权限的管理。
**恰当的权限设定,可以确保数据的安全性,这是至关重要的**
那么,怎么进行权限管理呢?这节课,我就结合超市项目的实际案例,给你讲一下权限管理的具体操作,包括怎么操作角色和用户,怎么通过角色给用户授权,怎么直接给用户授权,从而帮助你管理好用户的权限,提升数据库的安全性。
下面我就来先讲讲角色。我们可以通过角色对相同权限的用户进行分组管理,这样可以使权限管理更加简单高效。
## 角色的作用
角色是在MySQL 8.0中引入的新功能,相当于一个权限的集合。引入角色的目的是方便管理拥有相同权限的用户。
下面我举个超市项目中的例子,来具体说明一下如何通过角色管理用户权限。
超市项目中有库管、营运和财务等不同的模块它们各自对应不同的数据表。比如库存模块中的盘点表demo.invcount、营运模块中的商品信息表demo.goodsmaster还有财务模块中的应付账款表demo.settlement。下面是这些表的具体信息。
盘点表:
<img src="https://static001.geekbang.org/resource/image/81/28/812f141d6f1f7fb85a2c797a3c666528.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/00/dd/00151b4b837c70fc5d7516b3307cf8dd.jpeg" alt="">
应付账款表:
<img src="https://static001.geekbang.org/resource/image/81/c6/81c3b5cd0937fca32b0a54847170cec6.jpeg" alt="">
在超市项目中,员工的职责不同,包括库管、营运和财务等,不同的职责有不同的数据访问权限。比如:
- 张三是库管,他就可以查询商品信息表,对盘点表有增删改查的权限,但无权访问应付账款表;
- 李四是营运,他就拥有对商品信息表有增删改查的权限,而对库存表和应付账款表,只有查看的权限;
- 王五是财务,他就有对应付账款表有增删改查的权限,对商品信息表和库存表,只有查看的权限。
所以,我们需要为每一个职责创建一个对应的角色,为每个员工创建一个对应的数据库用户。然后通过给角色赋予相关的权限,再把角色赋予用户,实现对超市员工访问数据权限的管理,从而保证数据的安全性。
这样说有点抽象,下面我们具体操作一下角色和用户。
## 如何操作角色?
首先,我们要创建一个角色,为后面的授权做好准备。
### 如何创建角色?
MySQL中的角色名称由角色名称加主机名称组成。创建角色的语法结构如下
```
CREATE ROLE 角色名;
```
假设我们现在需要创建一个经理的角色,就可以用下面的代码:
```
mysql&gt; CREATE ROLE 'manager'@'localhost';
Query OK, 0 rows affected (0.06 sec)
```
这里的意思是创建一个角色角色名称是“manager”角色可以登录的主机是“localhost”意思是只能从数据库服务器运行的这台计算机登录这个账号。你也可以不写主机名直接创建角色“manager”
```
mysql&gt; CREATE ROLE 'manager';
Query OK, 0 rows affected (0.01 sec)
```
如果不写主机名MySQL默认是通配符“%”,意思是这个账号可以从任何一台主机上登录数据库。
同样道理,如果我们要创建库管的角色,就可以用下面的代码:
```
mysql&gt; CREATE ROLE 'stocker';
Query OK, 0 rows affected (0.02 sec)
```
**创建角色之后,默认这个角色是没有任何权限的,我们需要给角色授权**
### 怎么给角色赋予权限?
给角色授权的语法结构是:
```
GRANT 权限 ON 表名 TO 角色名;
```
假设我们现在想给经理角色授予商品信息表、盘点表和应付账款表的只读权限,就可以用下面的代码来实现:
```
mysql&gt; GRANT SELECT ON demo.settlement TO 'manager';
Query OK, 0 rows affected (0.03 sec)
mysql&gt; GRANT SELECT ON demo.goodsmaster TO 'manager';
Query OK, 0 rows affected (0.01 sec)
mysql&gt; GRANT SELECT ON demo.invcount TO 'manager';
Query OK, 0 rows affected (0.01 sec)
```
如果我们需要赋予库管角色盘点表的增删改查权限、商品信息表的只读权限,对应付账款表没有权限,就可以这样:
```
mysql&gt; GRANT SELECT,INSERT,DELETE,UPDATE ON demo.invcount TO 'stocker';
Query OK, 0 rows affected (0.02 sec)
mysql&gt; GRANT SELECT ON demo.goodsmaster TO 'stocker';
Query OK, 0 rows affected (0.02 sec)
```
### 查看角色权限
赋予角色权限之后我们可以通过SHOW GRANTS语句来查看权限是否创建成功了
```
mysql&gt; SHOW GRANTS FOR 'manager';
+-------------------------------------------------------+
| Grants for manager@% |
+-------------------------------------------------------+
| GRANT USAGE ON *.* TO `manager`@`%` |
| GRANT SELECT ON `demo`.`goodsmaster` TO `manager`@`%` |
| GRANT SELECT ON `demo`.`invcount` TO `manager`@`%` |
| GRANT SELECT ON `demo`.`settlement` TO `manager`@`%` |
+-------------------------------------------------------+
4 rows in set (0.00 sec)
```
只要你创建了一个角色系统就会自动给你一个“USAGE”权限意思是连接登录数据库的权限。代码的最后三行代表了我们给角色“manager”赋予的权限也就是对商品信息表、盘点表和应付账款表的只读权限。
再来看看库管角色的权限:
```
mysql&gt; SHOW GRANTS FOR 'stocker';
+----------------------------------------------------------------------------+
| Grants for stocker@% |
+----------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `stocker`@`%` |
| GRANT SELECT ON `demo`.`goodsmaster` TO `stocker`@`%` |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `demo`.`invcount` TO `stocker`@`%` |
+----------------------------------------------------------------------------+
3 rows in set (0.00 sec)
```
结果显示,库管角色拥有商品信息表的只读权限和盘点表的增删改查权限。
当我们需要对业务重新整合的时候,可能就需要对之前创建的角色进行清理,删除一些不会再使用的角色。
### 删除角色
删除角色的操作很简单,你只要掌握语法结构就行了。
```
DROP ROLE 角色名称;
```
这个操作十分简单,我就不多说了。
到这里,关于角色的操作,我就介绍完了,下面我们来看看关于用户的操作。
## 如何操作用户?
我们可以把数据库中的角色看作是一个权限集。角色本身不能操作数据,这个任务还是要靠用户来完成。在操作用户前,我们先要创建一个用户。
### 创建用户
创建用户的语法结构是这样的:
```
CREATE USER 用户名 [IDENTIFIED BY 密码];
```
“[ ]”表示可选,也就是说,可以指定用户登录时需要密码验证,也可以不指定密码验证,这样用户可以直接登录。不过,不指定密码的方式不安全,不推荐使用。
举个例子假设我们要给张三创建一个用户用户名是“zhangsan”密码是“mysql”可以通过下面的代码来实现
```
mysql&gt; CREATE USER 'zhangsan' IDENTIFIED BY 'mysql';
Query OK, 0 rows affected (0.02 sec)
```
这样,张三的用户就创建成功了。
### 给用户授权
给用户授权的方式有2种分别是通过把角色赋予用户给用户授权和直接给用户授权。
通过把角色赋予用户给用户授权的语法结构如下:
```
GRANT 角色名称 TO 用户名称;
```
举个小例子,我们想要给张三赋予库管的角色,可以通过下面的代码实现:
```
mysql&gt; GRANT 'stocker' TO 'zhangsan';
Query OK, 0 rows affected (0.01 sec)
```
我们也可以直接给用户授权,语法结构如下:
```
GRANT 权限 ON 表名 TO 用户名;
```
这种方式简单直接,我就不多说了。下面我们来查看一下这个用户的权限有哪些。
### 查看用户权限
查看用户权限的语法结构是:
```
SHOW GRANTS FOR 用户名;
```
我们可以通过下面的代码来查看张三的权限:
```
mysql&gt; SHOW GRANTS FOR 'zhangsan';
+---------------------------------------+
| Grants for zhangsan@% |
+---------------------------------------+
| GRANT USAGE ON *.* TO `zhangsan`@`%` |
| GRANT `stocker`@`%` TO `zhangsan`@`%` |
+---------------------------------------+
2 rows in set (0.00 sec)
```
结果显示,张三拥有库管角色的权限。
说到这里,我必须要提醒你一个常见的坑。
如果现在你用张三的这个用户去登录你会发现这个账号是没有任何权限的。你是不是觉得很奇怪我不是把角色“stocker”赋予用户“zhangsan”了吗那用户“zhangsan”应该有角色“stocker”的权限啊。其实这是因为**MySQL中创建了角色之后默认都是没有被激活的**,也就是不能用,必须要用下面的语句激活:
```
SET global activate_all_roles_on_login=ON;
```
这条SQL语句的意思是对所有角色永久激活。运行这条语句之后用户“zhangsan”才真正拥有了角色“stocker”的所有权限。
下面我们就用张三的账号登录,确认一下他有没有相应的权限:
```
H:\&gt;mysql -u zhangsan -p
Enter password: *****
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 24
Server version: 8.0.23 MySQL Community Server - GPL
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql&gt; select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+----------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgimportprice |
+------------+---------+-----------+---------------+------+------------+----------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 31.00 |
| 2 | 0002 | 笔 | NULL | 包 | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+----------------+
2 rows in set (0.02 sec)
```
结果显示,我们可以正常登录,并且可以查询商品信息表的内容。
### 删除用户
当用户不再使用的时候,我们也可以删除用户。操作起来很简单,你只要知道语法结构就行了:
```
DROP USER 用户名;
```
## 总结
今天这节课,我们学习了权限管理的方法,包括如何操作角色和用户,如何通过角色给用户授权,如何直接给用户授权等。
角色是权限的集合。你可以直接给用户授予访问数据库的权限,也可以通过把角色授予用户,从而把角色对数据库的访问权限全部授予给用户。而用户是数据库的使用者,我们可以通过给用户授予访问数据库中资源的权限,来控制使用者对数据库的访问,消除安全隐患。
需要注意的是,角色在刚刚创建出来的时候,默认是没有激活的,需要手动激活,才可以使用。如果你把角色赋予了用户,那么用户就拥有了角色的全部权限。但是,如果你删除了角色,那么用户也就失去了通过这个角色所获得的所有权限。
我知道有一些程序员喜欢使用Root超级用户来访问数据库完全把权限控制放在应用层面实现。这样当然也是可以的。不过我建议你尽量使用数据库自己的角色和用户机制来控制访问权限不要轻易用Root账号。因为Root账号密码放在代码里面不安全一旦泄露数据库就会完全失去保护。而且MySQL的权限控制功能十分完善应该尽量利用可以提高效率而且安全可靠。
## 思考题
在今天的课里我举了一个例子提到超市运营中的一个职责“财务”。财务可以对商品信息表、盘点表有只读的权限对应付账款表有增删改查的权限。请你设计一个角色财务“accountant”具备这些权限。给会计“李四”创建一个用户账号“lisi”使李四通过财务的角色获得对应付账款表增删改查的权限和对商品信息表、盘点表有只读的权限。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,251 @@
<audio id="audio" title="19 | 日志(上):系统出现问题,如何及时发现?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/84/940866d2267e4859005225614ae40184.mp3"></audio>
你好,我是朱晓峰。
我们曾经开发过一个数据库应用系统,但是却突然遭遇了数据库宕机。在这种情况下,定位宕机的原因就非常关键,毕竟,知道了问题,才能确定解决方案。
这时我们就想到了查看数据库的错误日志因为日志中记录了数据库运行中的诊断信息包括了错误、警告和注释等信息。从日志中我们发现原来某个连接中的SQL操作发生了死循环导致内存不足被系统强行终止了。知道了原因处理起来也就比较轻松了系统很快就恢复了运行。
除了发现错误,日志在数据复制、数据恢复、操作审计,以及确保数据的永久性和一致性等方面,都有着不可替代的作用,对提升你的数据库应用的开发能力至关重要。
今天,我就结合超市项目的实际案例,给你讲解一下怎么通过查看系统日志,来了解数据库中实际发生了什么,从而快速定位原因。
MySQL的日志种类非常多包括通用查询日志、慢查询日志、错误日志、二进制日志、中继日志、重做日志和回滚日志内容比较多而且都很重要所以我们来花两节课的时间学习一下。
这节课,我会先具体讲一讲通用查询日志、慢查询日志和错误日志。
## 通用查询日志
**通用查询日志记录了所有用户的连接开始时间和截止时间以及发给MySQL数据库服务器的所有SQL指令**。当我们的数据发生异常时,开启通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。
举个小例子在超市项目实施的过程中我们曾遇到过这样一件事超市经营者月底查账的时候发现超市的1号门店在12月1日销售了5件化妆品但是当天对应的历史库存并没有减少。化妆品的金额都比较大库存不对的话会在报表查询中产生巨额差异触发到报警机制对超市经营者的决策产生影响。超市经营者找到我们对系统的可靠性提出质疑。
我们对系统进行了仔细检查,没有发现数据问题。可是商品确实卖出去了,当天的历史库存也确实没有消减。这个时候,我们想到了检查通用查询日志,看看当天到底发生了什么。
查看之后我们就复原了当天的情况12月1日下午门店的收银台销售了5件化妆品但是由于网络故障流水没有及时上传到总部。12月1日晚上11:59总部的历史库存被保存下来但是因为没有收到门店的流水所以没有消减库存。12月2日上午门店的网络恢复了流水得以上传总部这个时候对应化妆品的库存才被消减掉。
这样,我们就确定了故障的原因,也就是超市的网络问题,而系统本身是没有问题的。
你看,通用查询日志可以帮助我们了解操作发生的具体时间和操作的细节,对找出异常发生的原因极其关键。
下面我来具体介绍一下控制通用查询日志的系统变量。通过这些变量,你会清楚怎么控制通用查询日志的开启和关闭,以及保存日志的文件是哪个。
```
mysql&gt; SHOW VARIABLES LIKE '%general%';
+------------------+---------------+
| Variable_name | Value |
+------------------+---------------+
| general_log | OFF | -- 通用查询日志处于关闭状态
| general_log_file | GJTECH-PC.log | -- 通用查询日志文件的名称是GJTECH-PC.log
+------------------+---------------+
2 rows in set, 1 warning (0.00 sec)
```
在这个查询的结果中有2点需要我们注意一下。
1. 系统变量general_log的值是OFF表示通用查询日志处于关闭状态。在MySQL中这个参数的默认值是关闭的。因为一旦开启记录通用查询日志MySQL会记录所有的连接起止和相关的SQL操作这样会消耗系统资源并且占用磁盘空间。我们可以通过手动修改变量的值在需要的时候开启日志。
1. 通用查询日志文件的名称是GJTECH-PC.log。这样我们就知道在哪里可以查看通用查询日志的内容了。
下面我们来看看如何开启通用查询日志把所有连接的起止和连接的SQL操作都记录下来。这个操作可以帮助我们追踪SQL操作故障的原因。
### 开启通用查询日志
我们可以通过设置系统变量的值来开启通用查询日志并且指定通用查询日志的文件夹和文件名为“H:\mytest.log”。这个操作如下
```
mysql&gt; SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.00 sec)
mysql&gt; SET @@global.general_log_file = 'H:\mytest.log';
Query OK, 0 rows affected (0.02 sec)
```
为了确认我们的设定是否已经生效,我们再来查询一下通用查询日志的状态:
```
mysql&gt; SHOW VARIABLES LIKE '%general%';
+------------------+--------------+
| Variable_name | Value |
+------------------+--------------+
| general_log | ON | -- 通用查询日志开启
| general_log_file | H:mytest.log | -- 日志名称也改过了
+------------------+--------------+
2 rows in set, 1 warning (0.00 sec)
```
结果显示通用查询日志已经开启文件是“H:\mytest.log”这就意味着我们的操作成功了。
### 查看通用查询日志
通用查询日志都是文本型数据,可以用记事本打开。下面我们就用记事本打开我电脑上的通用查询日志,实际看一看通用查询日志的内容,包括都有哪些连接,什么时候登录了数据库,都做了哪些操作等信息。
```
2021-04-05T06:39:53.621980Z 28 Connect zhangsan@localhost on using SSL/TLS -- 账号zhangsan从本地登录
2021-04-05T06:39:53.622085Z 28 Connect Access denied for user 'zhangsan'@'localhost' (using password: NO) -- 没有使用密码,连接被拒绝了
2021-04-05T06:40:02.522303Z 29 Connect zhangsan@localhost on using SSL/TLS
2021-04-05T06:40:02.522913Z 29 Query select @@version_comment limit 1
2021-04-05T06:40:14.211511Z 29 Query SELECT *
FROM demo.invcount -- 查询数据表demo.invcount内容
2021-04-05T06:40:37.647625Z 29 Query UPDATE demo.invcount
SET plquant = - 5 -- 更新数据表demo.invcount
WHERE itemnumber = 1
2021-04-05T06:41:15.047067Z 29 Query SELECT *
FROM demo.goodsmaster -- 查询数据表demo.goodsmaster
```
在通用查询日志里面我们可以清楚地看到账号“zhangsan”是什么时间登录的服务器登录之后做了什么SQL操作针对的是哪个数据表等信息。
### 删除通用查询日志
当用户对数据库的操作比较频繁时,通用查询日志文件会不断变大。为了节省磁盘空间,我们可以移除旧的日志文件,创建新的日志文件,来对通用查询日志文件进行维护。
第一步,关闭通用查询日志:
```
mysql&gt; SET GLOBAL general_log = 'OFF'; -- 关闭通用查询日志
Query OK, 0 rows affected (0.01 sec)
mysql&gt; SHOW VARIABLES LIKE '%general_log%'; -- 查看通用查询日志状态
+------------------+--------------+
| Variable_name | Value |
+------------------+--------------+
| general_log | OFF |
| general_log_file | H:mytest.log |
+------------------+--------------+
2 rows in set, 1 warning (0.00 sec)
```
第二步把通用查询日志文件“H:\mytest.log”移至备份文件夹空出磁盘H的空间。
第三步,开启通用查询日志:
```
mysql&gt; SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.01 sec)
mysql&gt; SHOW VARIABLES LIKE '%general_log%';
+------------------+--------------+
| Variable_name | Value |
+------------------+--------------+
| general_log | ON |
| general_log_file | H:mytest.log |
+------------------+--------------+
2 rows in set, 1 warning (0.00 sec)
```
这个时候你会发现MySQL已经给我们准备好了一个新的通用查询日志文件“H:\mytest.log”并且记录了我们第一个查询的语句“SHOW VARIABLES LIKE '%general_log%';”。
文件内容如下:
```
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe, Version: 8.0.23 (MySQL Community Server - GPL). started with:
TCP Port: 3306, Named Pipe: MySQL
Time Id Command Argument
2021-04-05T07:02:03.007394Z 30 Query SHOW VARIABLES LIKE '%general_log%'
```
总之,开启了通用查询日志之后,如果遇到用户对数据产生质疑的情况,我们就可以通过查看通用查询日志,还原当时的场景,快速定位并解决问题。
## 慢查询日志
**慢查询日志用来记录执行时间超过指定时长的查询**。它的主要作用是帮助我们发现那些执行时间特别长的SQL查询并且有针对性地进行优化从而提高系统的整体效率。当我们的数据库服务器发生阻塞、运行变慢的时候检查一下慢查询日志找到那些慢查询对解决问题很有帮助。
慢查询日志是由MySQL的配置文件进行控制的。下面我先简单介绍一下MySQL的配置文件。
在MySQL的安装目录中C:\ProgramData\MySQL\MySQL Server 8.0我们可以找到MySQL的配置文件“my.ini”。这个文件是一个文本格式的文件可以直接用记事本打开来阅读。
我们来看看配置文件中关于慢查询日志变量的相关设定:
```
slow-query-log=1 -- 表示开启慢查询日志,系统将会对慢查询进行记录。
slow_query_log_file=&quot;GJTECH-PC-slow.log&quot; -- 表示慢查询日志的名称是&quot;GJTECH-PC-slow.log&quot;。这里没有指定文件夹,默认就是数据目录:&quot;C:\ProgramData\MySQL\MySQL Server 8.0\Data&quot;。
long_query_time=10 -- 表示慢查询的标准是查询执行时间超过10秒
```
除了刚刚的这些变量控制慢查询日志的还有一个系统变量min_examined_row_limit。这个变量的意思是查询扫描过的最少记录数。这个变量和查询执行时间共同组成了判别一个查询是否是慢查询的条件。如果查询扫描过的记录数大于等于这个变量的值并且查询执行时间超过long_query_time的值那么这个查询就被记录到慢查询日志中反之则不被记录到慢查询日志中。
如果要查看当前这个系统变量的值,我们就可以用下面的代码:
```
mysql&gt; show variables like 'min%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| min_examined_row_limit | 0 |
+------------------------+-------+
1 row in set, 1 warning (0.00 sec)
```
这个值默认是0。与long_query_time=10合在一起表示只要查询的执行时间超过10秒钟哪怕一个记录也没有扫描过都要被记录到慢查询日志中。你也可以根据需要通过修改“my.ini”文件来修改查询时长或者通过SET指令用SQL语句修改“min_examined_row_limit”的值。
只是你要注意如果修改了MySQL的配置文件“my.ini”就需要重启服务器这样才能使修改生效。
来看一个例子之前我运行的一个慢查询被记录到了慢查询日志中。这个例子记录了一个运行时间超过10秒的慢查询的发生时间、连接所属的用户、执行的时长、锁表的时长和扫描过的记录数等相关信息。
```
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe, Version: 8.0.23 (MySQL Community Server - GPL). started with:
TCP Port: 3306, Named Pipe: MySQL
Time Id Command Argument
# Time: 2021-03-25T07:20:33.412260Z -- 执行开始时间
# User@Host: root[root] @ localhost [::1] Id: 13 -- 用户
# Query_time: 10.166435 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0 -- 执行时长、锁表时长、检查的记录数
use demo;
SET timestamp=1616656823;
```
通过这个慢查询日志的记录,我们就可以发现是哪个查询消耗了大量的系统资源,是哪个连接里面的查询,具体什么时间开始的。有了这些信息,我们就可以对慢查询进行分析,决定优化的方式,避免出现同样的问题。
好了,到这里,通用查询日志和慢查询日志我就讲完了,咱们最后再来学习一种重要的日志:错误日志。
## 错误日志
**错误日志记录了MySQL服务器启动、停止运行的时间以及系统启动、运行和停止过程中的诊断信息包括错误、警告和提示**等。当我们的数据库服务器发生系统故障时,错误日志是发现问题、解决故障的首选。
错误日志默认是开启的。我们可以在MySQL的配置文件“my.ini”中配置它
```
# Error Logging.
log-error=&quot;GJTECH-PC.err&quot;
```
这段代码指定了错误日志的文件名。如果没有指定文件夹默认就是数据目录“C:\ProgramData\MySQL\MySQL Server 8.0\Data”。
下面我们查看一下错误日志的内容:
```
2021-02-28T08:07:07.228880Z 0 [System] [MY-010116] [Server] C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe (mysqld 8.0.23) starting as process 7652
2021-02-28T08:07:07.270982Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
2021-02-28T08:07:08.116433Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
```
可以看到错误日志文件中记录了服务器启动的时间以及存储引擎InnoDB启动和停止的时间等。
## 总结
今天这节课,我们学习了通用查询日志、慢查询日志和错误日志。
- 通用查询日志:可以记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。
- 慢查询日志:可以记录运行时间和检查记录数超过指定值的查询,方便我们对查询进行优化。
- 错误日志:它记录了服务器启动、运行和停止过程中的诊断信息,方便我们了解服务器的状态,从而对服务器进行维护。
最后,我还是想提醒你一句,**千万不要小看日志**。很多看似奇怪的问题,答案往往就藏在日志里。很多情况下,只有通过查看日志才能发现问题的原因,真正解决问题。所以,一定要学会查看日志,养成检查日志的习惯。
## 思考题
请你思考一下怎么设置开启慢查询日志并且把慢查询日志的指定时长设置为5秒最少扫描记录数为1
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,472 @@
<audio id="audio" title="20 | 日志(下):系统故障,如何恢复数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/28/921d3fba4fc361633647b8e679eac928.mp3"></audio>
你好,我是朱晓峰。
上节课咱们学习了通用查询日志、慢查询日志和错误日志它们可以帮助我们快速定位系统问题。但实际上日志也可以帮助我们找回由于误操作而丢失的数据比如二进制日志binary log、中继日志relay log、回滚日志undo log和重做日志redo log
这节课我们就来学习下这4种日志。
## 二进制日志
**二进制日志主要记录数据库的更新事件**,比如创建数据表、更新表中的数据、数据更新所花费的时长等信息。通过这些信息,我们可以再现数据更新操作的全过程。而且,由于日志的延续性和时效性,我们还可以利用日志,完成无损失的数据恢复和主从服务器之间的数据同步。
可以说,二进制日志是进行数据恢复和数据复制的利器。所以,接下来我就结合一个实际案例,重点给你讲一讲怎么操作它。
### 如何操作二进制日志?
操作二进制日志,主要包括查看、刷新二进制日志,用二进制日志恢复数据,以及删除二进制日志。
**1.查看二进制日志**
查看二进制日志主要有3种情况分别是查看当前正在写入的二进制日志、查看所有的二进制日志和查看二进制日志中的所有数据更新事件。
查看当前正在写入的二进制日志的SQL语句是
```
SHOW MASTER STATUS;
```
我们可以通过这条语句,查看当前正在写入的二进制日志的名称和当前写入的位置:
```
mysql&gt; SHOW MASTER STATUS;
+----------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------------+----------+--------------+------------------+-------------------+
| GJTECH-PC-bin.000011 | 2207 | | | |
+----------------------+----------+--------------+------------------+-------------------+ -- 当前正在写入的二进制日志是“GJTECH-PC-bin.000011”当前的位置是2207。
1 row in set (0.00 sec)
```
查看所有的二进制日志的SQL语句是
```
SHOW BINARY LOGS;
```
查看二进制日志中所有数据更新事件的SQL语句是
```
SHOW BINLOG EVENTS IN 二进制文件名;
```
**2.刷新二进制日志**
刷新二进制日志的SQL语句是
```
FLUSH BINARY LOGS;
```
这条语句的意思是关闭服务器正在写入的二进制日志文件并重新打开一个新文件文件名的后缀在现有的基础上加1。
**3.用二进制日志恢复数据**
我们可以用mysqlbinlog工具进行数据恢复
```
mysqlbinlog start-positon=xxx end-position=yyy 二进制文件名 | mysql -u 用户 -p
```
这条命令的意思是执行二进制日志中从位置xxx开始到yyy截止的所有数据更新操作。这里的截止位置也可以不写意思是从位置xxx开始执行二进制文件中的所有数据更新操作。
**4.删除二进制日志**
如果我们已经把日志文件保存到了安全的地方就可以通过下面的SQL语句删除所有二进制日志文件以释放磁盘空间
```
mysql&gt; RESET MASTER;
Query OK, 0 rows affected (0.20 sec)
mysql&gt; SHOW BINARY LOGS;
+----------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+----------------------+-----------+-----------+
| GJTECH-PC-bin.000001 | 156 | No |
+----------------------+-----------+-----------+
1 row in set (0.00 sec)
```
结果显示所有二进制日志文件都被删除了MySQL从头准备了一个“.000001”为后缀的新的二进制日志文件。
我们也可以通过SQL语句删除比指定二进制日志文件编号小的所有二进制日志文件
```
mysql&gt; PURGE MASTER LOGS TO 'GJTECH-PC-bin.000005';
Query OK, 0 rows affected (0.02 sec)
```
好了,知道了二进制日志的操作方法,下面我们借助一个案例,来实操一下。我们来看看怎么通过二进制日志恢复数据,避免因故障或异常等导致数据损失。
### 案例讲解
假设数据库demo中有一个商品信息表demo.goodsmaster我先对数据库demo做了一个全量备份。所谓的全量备份就是指对数据库中存储的全部数据进行备份。备份完成之后我又在商品信息表中插入了新数据。
这个时候数据库demo出现异常数据全部丢失。现在咱们需要把所有的数据包括备份前的数据和备份之后新插入的数据都恢复回来。我来介绍下具体的操作步骤。
商品信息表的信息如下所示:
<img src="https://static001.geekbang.org/resource/image/98/83/980b731a6ab550894479b1f3382a9683.jpeg" alt="">
可以看到表中有一条记录编号是1的商品名称是“书”售价是89元。
**第一步,做数据库备份。**
你可以用MySQL的数据备份工具mysqldump来备份数据。这个工具的语法结构如下所示
```
mysqldump -u 用户 -p 密码 数据库 &gt; 备份文件
```
在这个场景中我们可以使用mysqldump工具把数据库demo中的全部信息备份到文件“mybackup.sql”中来完成对数据库demo的全量备份
```
H:\&gt;mysqldump -u root -p demo &gt; mybackup.sql
Enter password: *****
```
这个命令的意思是把数据库demo中的全部数据备份到文件mybackup.sql中。
**第二步用“FLUSH BINARY LOGS;”语句刷新一下日志。**
```
mysql&gt; FLUSH BINARY LOGS;
Query OK, 0 rows affected (0.06 sec)
```
这步操作的目的是:产生一个新的二进制日志文件,使这个文件只保存数据备份之后的数据更新事件,这样可以方便我们查看文件的内容。
**第三步,给商品信息表插入一条新的数据记录“笔”。**
```
mysql&gt; INSERT INTO demo.goodsmaster
-&gt; (
-&gt; itemnumber,
-&gt; barcode,
-&gt; goodsname,
-&gt; salesprice
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; 2,
-&gt; '0002',
-&gt; '笔',
-&gt; 3
-&gt; );
Query OK, 1 row affected (0.03 sec)
```
这样我们就增加了一个新的商品“笔”。
现在,我们来查看一下数据表里的内容:
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 书 | 89.00 |
| 2 | 0002 | 笔 | 3.00 |
+------------+---------+-----------+------------+
2 rows in set (0.00 sec)
```
结果显示我们有了2条记录分别是“书”和“笔”。
假设这个时候,系统突然宕机,数据库无法启动,为了使系统恢复正常,我们重启机器,重新创建数据库,并且需要进行数据恢复。
**第四步,准备从备份文件恢复数据。**
每当服务器启动、刷新二进制日志或者二进制日志大小超过系统变量 max_binlog_size指定的值时系统就会生成一个新的二进制日志文件。
我们先查看一下机器上的二进制日志文件,以确定哪个文件是我们正在使用的。
```
mysql&gt; SHOW BINARY LOGS;
+----------------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+----------------------+-----------+-----------+
| GJTECH-PC-bin.000005 | 179 | No |
| GJTECH-PC-bin.000006 | 113316452 | No |
| GJTECH-PC-bin.000007 | 12125 | No |
| GJTECH-PC-bin.000008 | 1544 | No |
| GJTECH-PC-bin.000009 | 207 | No |
| GJTECH-PC-bin.000010 | 1758 | No |
| GJTECH-PC-bin.000011 | 2207 | No |
| GJTECH-PC-bin.000012 | 462 | No |
+----------------------+-----------+-----------+
12 rows in set (0.01 sec)
```
结果显示最新的也就是记录了数据插入操作的二进制日志文件是“GJTECH-PC-bin.000012”,这就是接下来我们要用的日志文件。
接着,我们来刷新一下二进制日志文件,这样做的目的是防止后面数据恢复的事件全都被写入这个二进制日志文件,妨碍我们理解文件的内容。
```
mysql&gt; FLUSH BINARY LOGS;
Query OK, 0 rows affected (0.08 sec)
```
现在,我们查看一下当前正在写入的二进制文件和位置,确认一下系统是否创建了新的二进制日志文件:
```
mysql&gt; SHOW MASTER STATUS;
+----------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+----------------------+----------+--------------+------------------+-------------------+
| GJTECH-PC-bin.000013 | 156 | | | |
+----------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)
```
结果显示当前在使用的二进制日志文件是“GJTECH-PC-bin.000013”,这样保存增量数据的二进制日志文件就不会受到后面操作的影响了。
下面我来删除并重建一个数据库demo给你演示一下如何使用二进制日志恢复数据。
```
mysql&gt; DROP DATABASE demo;
Query OK, 1 row affected (0.07 sec)
mysql&gt; CREATE DATABASE demo;
Query OK, 1 row affected (0.01 sec)
```
通过上面的操作就有了一个空数据库demo。接下来就可以恢复数据了。
**第五步,从备份恢复数据。**
可以通过mysql命令来恢复数据语法结构如下
```
mysql -u 用户 -p 密码 数据库名称 &lt; 备份文件
```
现在我用刚才的备份文件,通过下面的指令来恢复数据:
```
H:\&gt;mysql -u root -p demo&lt;mybackup.sql
Enter password: *****
```
这个指令的意思是在数据库demo中执行备份文件“mybackup.sql”中的所有SQL操作这样就可以把demo中的数据恢复到备份时的状态了。
我们,来看一下现在商品信息表中的数据:
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 书 | 89.00 |
+------------+---------+-----------+------------+
1 row in set (0.00 sec)
```
结果显示,只有一条数据记录“书”,那么,怎么才能把我们备份之后插入的那条数据“笔”给找回呢?这个时候,就要进入下一步,使用二进制日志了。
**第六步从保存增量信息的二进制日志文件“GJTECH-PC-bin.000012”中恢复增量数据。**
<img src="https://static001.geekbang.org/resource/image/c1/64/c1d720d4405e479ba71eca27d5a15e64.png" alt="">
通过查看二进制日志中的事件你会发现写入第二条记录的时候MySQL使用了一个事务操作起始位置是306截止位置是462。这样就可以用mysqlbinlog工具进行数据恢复了。日志名称是“C:\ProgramData\MySQL\MySQL Server 8.0\Data\GJTECH-PC-bin.000012”读取日志的起始位置是306。
```
H:\&gt;mysqlbinlog --start-position=306 &quot;C:\ProgramData\MySQL\MySQL Server 8.0\Data\GJTECH-PC-bin.000012&quot; | mysql -u root -p
Enter password: *****
```
现在我们查看一下商品信息表,确认一下备份之后插入的商品数据记录是不是恢复回来了。
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | 书 | 89.00 |
| 2 | 0002 | 笔 | 3.00 |
+------------+---------+-----------+------------+
2 rows in set (0.00 sec)
```
结果显示,备份之后插入的记录“笔”也找回来了。
到这里,二进制日志恢复数据的功能我就介绍完了。需要提醒你注意的是,在实际工作中,用二进制日志文件找回数据时经常会遇到问题,主要就是不容易找准起始位置或者截止位置。找早了,会导致数据冲突、重复;找晚了,又会丢失数据。所以,我建议你在数据备份结束之后,把当前的二进制日志位置记录下来,存放在一个安全的地方,这关系到数据恢复的完整性,一定不要怕麻烦。
二进制日志还有一个重要的功能,就是在主从服务器的架构中,把主服务器的操作复制到从服务器。而这个操作要借助中继日志一起完成。
## 中继日志
**中继日志只在主从服务器架构的从服务器上存在**。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步。
搭建好主从服务器之后中继日志默认会保存在从服务器的数据目录C:\ProgramData\MySQL\MySQL Server 8.0\Data文件名的格式是从服务器名-relay-bin.序号。
中继日志还有一个索引文件:从服务器名-relay-bin.index用来定位当前正在使用的中继日志。
中继日志与二进制日志的格式相同可以用mysqlbinlog工具进行查看。下面是中继日志的一个片段
```
SET TIMESTAMP=1618558728/*!*/;
BEGIN
/*!*/;
# at 950
#210416 15:38:48 server id 1 end_log_pos 832 CRC32 0xcc16d651 Table_map: `demo`.`test` mapped to number 91
# at 1000
#210416 15:38:48 server id 1 end_log_pos 872 CRC32 0x07e4047c Delete_rows: table id 91 flags: STMT_END_F -- server id 1 是主服务器,意思是主服务器删了一行数据
BINLOG '
CD95YBMBAAAAMgAAAEADAAAAAFsAAAAAAAEABGRlbW8ABHRlc3QAAQMAAQEBAFHWFsw=
CD95YCABAAAAKAAAAGgDAAAAAFsAAAAAAAEAAgAB/wABAAAAfATkBw==
'/*!*/;
# at 1040
```
这一段的意思是主服务器“server id 1”对表demo.test进行了2步操作
1. 定位到表demo.test编号是91的记录日志位置是832
1. 删除编号是91的记录日志位置是872。
关于中继日志,有一个很容易踩到的坑。如果从服务器宕机,有的时候为了系统恢复,你要重装操作系统,这样就可能会导致你的服务器名称与之前不同。而中继日志的名称里面是包含从服务器名的。因此,在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是文件名不对了。解决的方法也很简单,只要把从服务器的名称改回之前的名称就可以了。
下面我再介绍一下回滚日志。
## 回滚日志
**回滚日志的作用是进行事务回滚**
当事务执行的时候,回滚日志中记录了事务中每次数据更新前的状态。当事务需要回滚的时候,可以通过读取回滚日志,恢复到指定的位置。另一方面,回滚日志也可以让其他的事务读取到这个事务对数据更改之前的值,从而确保了其他事务可以不受这个事务修改数据的影响。
回滚日志的设置是啥样的呢?我们来学习下相关变量值,包括文件大小、所在的文件夹、是否加密、是否自动截断回收以及是否有独立的表空间等。这些都是我们了解事务回滚的机制的关键。
```
mysql&gt; SHOW VARIABLES LIKE '%innodb_max_undo_log_size%';
+--------------------------+------------+
| Variable_name | Value |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
+--------------------------+------------+
1 row in set, 1 warning (0.00 sec)
```
变量“innodb_max_undo_log_size”的意思是单个回滚日志最大可占用1G字节存储空间。
下面几个变量定义了回滚日志所在的文件夹、是否加密、是否自动截断回收空间和是否有独立的表空间等。
```
mysql&gt; SHOW VARIABLES LIKE '%innodb_undo%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_undo_directory | .\ | -- 表示回滚日志的存储目录是数据目录。
| innodb_undo_log_encrypt | OFF | -- 表示回滚日志不加密。
| innodb_undo_log_truncate | ON | -- 表示回滚日志是否自动截断回收,前提是设置了独立表空间。
| innodb_undo_tablespaces | 2 | -- 表示回滚日志有自己的独立表空间而不是在共享表空间ibdata文件中。
+--------------------------+-------+
4 rows in set, 1 warning (0.00 sec)
```
这里的结果显示了这4个变量的默认值。下面我来分别解释一下。
- innodb_undo_directory=.\ 表示回滚日志的存储目录是数据目录数据目录的位置可以通过查询变量“datadir”来查看。
- innodb_undo_log_encrypt = OFF表示回滚日志不加密。
- innodb_undo_log_truncate = ON表示回滚日志是否自动截断回收这个变量有效的前提是设置了独立表空间。
- innodb_undo_tablespaces = 2表示回滚日志有自己的独立表空间而不是在共享表空间ibdata文件中。
下面的截图显示了回滚日志的存储目录,以及在文件夹中的名称等信息:
<img src="https://static001.geekbang.org/resource/image/90/38/90ebac7eed19314e6fc5928b92a56b38.png" alt="">
最后我来介绍一下MySQL的重做日志。
## 重做日志
重做日志是存储在磁盘上的一种日志文件主要有2个作用。
1. 在系统遇到故障的恢复过程中,可以修复被未完成的事务修改的数据。
1. MySQL为了提高数据存取的效率减少磁盘操作的频率对数据的更新操作不会立即写到磁盘上而是把数据更新先保存在内存中积累到一定程度再集中进行磁盘读写操作。这样就存在一个问题一旦出现宕机或者停电等异常情况内存中保存的数据更新操作可能会丢失。这个时候就可以通过读取重做日志中记录的数据更新操作把没来得及写到磁盘上的数据更新写到磁盘上确保数据的完整性。
我们可以通过系统变量的值,了解重做日志所在的文件夹和文件的数量。这些是我们进一步了解系统运行机制的必要条件,有助于我们开发出高效的数据库应用。
```
mysql&gt; SHOW VARIABLES LIKE '%innodb_log_files_in_group%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
1 row in set, 1 warning (0.00 sec)
```
结果显示变量innodb_log_files_in_group值是2表示有2个重做日志文件。
需要注意的是变量innodb_log_files_in_group值的取值范围是1~4这四个文件分别用于记录不同的操作.
1. 用户创建表的插入操作;
1. 用户创建表的更新和删除操作;
1. 临时表的插入操作;
1. 临时表的更新和删除操作。
那么为什么在我的电脑上变量innodb_log_files_in_group值是2呢其实这是因为我只执行了对用户创建表的插入操作和更新删除操作所以只用到了2个文件。如果我还执行了临时表的插入和更新删除的操作那么这个变量的值就会变成4也就是会有4个重做日志文件了。
## 总结
这节课,我们学习了二进制日志、中继日志、回滚日志和重做日志。
1. 二进制日志:主要用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。
1. 中继日志:就是主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。
1. 回滚日志:用来存储事务中数据更新前的状态,以便回滚和保持其他事务的数据一致性。
1. 重做日志:是为了确保数值持久性、防止数据更新丢失的一种日志。
在这几种日志中,你一定要格外注意二进制日志的用法。有了它,我们就可以通过数据库的全量备份和二进制日志中保存的增量信息,完成数据库的无损失恢复。不过,我要提醒你的是,如果你遇到数据量大、数据库和数据表很多(比如分库分表的应用)的场景,用二进制日志进行数据恢复,是很有挑战性的,因为起止位置不容易管理。
在这种情况下,一个有效的解决办法是配置主从数据库服务器,甚至是一主多从的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。
## 思考题
下面是一段二进制日志中事件的内容:
```
mysql&gt; SHOW BINLOG EVENTS IN 'GJTECH-PC-bin.000013';
+----------------------+------+----------------+-----------+-------------+--------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+----------------------+------+----------------+-----------+-------------+--------------------------------------+
| GJTECH-PC-bin.000013 | 556 | Query | 1 | 627 | BEGIN |
| GJTECH-PC-bin.000013 | 627 | Table_map | 1 | 696 | table_id: 114 (demo.goodsmaster) |
| GJTECH-PC-bin.000013 | 696 | Delete_rows | 1 | 773 | table_id: 114 flags: STMT_END_F |
| GJTECH-PC-bin.000013 | 773 | Xid | 1 | 804 | COMMIT /* xid=253 */ |
| GJTECH-PC-bin.000013 | 804 | Anonymous_Gtid | 1 | 894 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| GJTECH-PC-bin.000013 | 894 | Query | 1 | 969 | BEGIN |
| GJTECH-PC-bin.000013 | 969 | Table_map | 1 | 1038 | table_id: 114 (demo.goodsmaster) |
| GJTECH-PC-bin.000013 | 1038 | Write_rows | 1 | 1094 | table_id: 114 flags: STMT_END_F |
| GJTECH-PC-bin.000013 | 1094 | Xid | 1 | 1125 | COMMIT /* xid=259 */ |
```
观察一下其中包括了哪几个SQL数据操作呢为了从二进制日志中恢复这些操作我们应该如何设置起始位置和截止位置呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,466 @@
<audio id="audio" title="21 | 数据备份:异常情况下,如何确保数据安全?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/38/aa398d679f7139c51800898e1886a238.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊数据备份。
数据备份,对咱们技术人员来说十分重要。当成千上万的用户,每天使用我们开发的应用做着他们的日常工作的时候,数据的安全性就不光是你一个人的事了。要是有一天,突然发生了某种意想不到的情况,导致数据库服务器上的数据全部丢失,所有使用这个应用的人都会受到严重影响。
所以,我们必须“未雨绸缪”,及时把数据备份到安全的地方。这样,当突发的异常来临时,我们就能把数据及时恢复回来,就不会造成太大损失。
MySQL的数据备份有2种一种是物理备份通过把数据文件复制出来达到备份的目的另外一种是逻辑备份通过把描述数据库结构和内容的信息保存起来达到备份的目的。逻辑备份这种方式是免费的广泛得到使用而物理备份的方式需要收费用得比较少。所以这节课我重点和你聊聊逻辑备份。
我还会给你介绍一下MySQL中的数据备份工具mysqldump、数据恢复的命令行客户端工具mysql以及数据表中数据导出到文件和从文件导入的SQL语句帮助你提高你所开发的应用中的数据安全性。
## 如何进行数据备份?
首先我们来学习下用于数据备份的工具mysqldump。它总共有三种模式
1. 备份数据库中的表;
1. 备份整个数据库;
1. 备份整个数据库服务器。
接下来我就来介绍下这3种备份的具体方法。
### 如何备份数据库中的表?
mysqldump备份数据库中的表的语法结构是
```
mysqldump -h 服务器 -u 用户 -p 密码 数据库名称 [表名称 … ] &gt; 备份文件名称
```
我简单解释一下这里的核心内容。
- “-h”后面跟的服务器名称如果省略默认是本机“localhost”。
- “-u”后面跟的是用户名。
- “-p”后面跟的是密码如果省略执行的时候系统会提示录入密码。
我举个小例子,带你看看怎么使用这个工具。
假设数据库demo中有2个表分别是商品信息表demo.goodsmaster和会员表demo.membermaster
商品信息表:
<img src="https://static001.geekbang.org/resource/image/9c/45/9cd746385988ee32d8813ffbb12ed645.jpeg" alt="">
会员表:
<img src="https://static001.geekbang.org/resource/image/bd/87/bd0e205b0893773944275861ae9b6e87.jpeg" alt="">
现在我需要把数据库demo备份到文件中就可以用下面的代码实现
```
H:\&gt;mysqldump -u root -p demo goodsmaster membermaster &gt; test.sql
Enter password: *****
```
这个指令的意思就是备份本机数据库服务器上demo数据库中的商品信息表和会员信息表的所有信息。
**备份文件是以文本格式保存的**,我们可以用记事本打开,看一下备份的内容:
```
-- MySQL dump 10.13 Distrib 8.0.23, for Win64 (x86_64)
--
-- Host: localhost Database: demo -- 表示从本地进行备份数据库是demo
-- ------------------------------------------------------
-- Server version 8.0.23
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Table structure for table `goodsmaster` -- 商品信息表的结构
--
DROP TABLE IF EXISTS `goodsmaster`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `goodsmaster` (
`itemnumber` int NOT NULL,
`barcode` text,
`goodsname` text,
`specification` text,
`unit` text,
`salesprice` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`itemnumber`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `goodsmaster` -- 商品信息表的内容
--
LOCK TABLES `goodsmaster` WRITE;
/*!40000 ALTER TABLE `goodsmaster` DISABLE KEYS */;
INSERT INTO `goodsmaster` VALUES (1,'0001','书','16开','本',89.00),(2,'0002','笔','10支装','包',5.00),(3,'0003','橡皮',NULL,'个',3.00);
/*!40000 ALTER TABLE `goodsmaster` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `membermaster` -- 会员表的结构
--
DROP TABLE IF EXISTS `membermaster`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `membermaster` (
`id` int NOT NULL AUTO_INCREMENT,
`cardno` char(8) NOT NULL,
`membername` text,
`memberphone` text,
`memberpid` text,
`memberaddress` text,
`sex` text,
`birthday` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `membermaster` -- 会员表的内容
--
LOCK TABLES `membermaster` WRITE;
/*!40000 ALTER TABLE `membermaster` DISABLE KEYS */;
INSERT INTO `membermaster` VALUES ('10000001','张三','13812345678','110123200001017890','北京','男','2000-01-01 00:00:00',1),('10000002','李四','13512345678','123123199001012356','上海','女','1990-01-01 00:00:00',2);
/*!40000 ALTER TABLE `membermaster` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2021-04-11 10:43:04
```
从这个文件中我们可以看到它相当于一个SQL执行脚本里面包括了创建商品信息表和会员表的SQL语句以及把表里的数据插入到这两个表中的SQL语句。这样一来商品信息表和会员信息表的结构信息和全部数据信息就都备份出来了。
下面我来介绍一下备份整个数据库的方法。
### 如何备份数据库?
mysqldump备份数据库的语法结构是
```
mysqldump -h 服务器 -u 用户 -p 密码 --databases 数据库名称 … &gt; 备份文件名
```
举个小例子假设我现在需要对本机的数据库服务器中的2个数据库demo和demo1进行备份就可以用下面的指令
```
H:\&gt;mysqldump -u root -p --databases demo demo1 &gt; test1.sql
Enter password: *****
```
现在,我们来查看一下备份文件的内容:
```
-- MySQL dump 10.13 Distrib 8.0.23, for Win64 (x86_64)
--
-- Host: localhost Database: demo
-- ------------------------------------------------------
-- Server version 8.0.23
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Current Database: `demo` -- 备份数据库demo
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `demo` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `demo`; -- 备份数据库中的表
--
-- Table structure for table `dailystatistics`
--
DROP TABLE IF EXISTS `dailystatistics`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `dailystatistics` (
`id` int NOT NULL AUTO_INCREMENT,
`itemnumber` int DEFAULT NULL,
`quantity` decimal(10,3) DEFAULT NULL,
`actualvalue` decimal(10,2) DEFAULT NULL,
`cost` decimal(10,2) DEFAULT NULL,
`profit` decimal(10,2) DEFAULT NULL,
`profitratio` decimal(10,4) DEFAULT NULL,
`salesdate` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_dailystatistic_salesdate` (`salesdate`),
KEY `index_dailystatistic_itemnumber` (`itemnumber`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `dailystatistics`
--
LOCK TABLES `dailystatistics` WRITE;
/*!40000 ALTER TABLE `dailystatistics` DISABLE KEYS */;
INSERT INTO `dailystatistics` VALUES (15,1,3.000,267.00,100.50,166.50,0.6236,'2020-12-01 00:00:00'),(16,2,2.000,10.00,7.00,3.00,0.3000,'2020-12-01 00:00:00');
/*!40000 ALTER TABLE `dailystatistics` ENABLE KEYS */;
UNLOCK TABLES;
-- 这里省略了其他表的备份语句
--
-- Current Database: `demo1` -- 备份数据库demo1
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `demo1` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `demo1`;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2021-04-11 11:02:09
```
可以看到这个文件里面包含了创建数据库demo和demo1的SQL语句以及创建数据库中所有表、插入所有表中原有数据的SQL语句。
### 如何备份整个数据库服务器?
mysqldump备份整个数据库服务器的语法结构是
```
mysqldump -h 服务器 -u 用户 -p 密码 --all-databases &gt; 备份文件名
```
举个小例子假设我要把本机上整个MySQL服务器备份下来可以用下面的代码
```
H:\&gt;mysqldump -u root -p --all-databases &gt; test2.sql
Enter password: *****
```
这个指令表示备份本机上运行的MySQL数据库服务器的全部内容包含系统数据库和用户创建的数据库中的库结构信息、表结构信息和表里的数据。这种备份方式会把系统数据库也全部备份出来而且消耗的资源也比较多一般来说没有必要我就不展开细说了。
备份文件有了,如何用它进行数据恢复呢?下面我就来给你介绍下具体的方法。
## 如何进行数据恢复?
mysqldump的备份文件包含了创建数据库、数据表以及插入数据表里原有数据的SQL语句我们可以直接运行这些SQL语句来进行数据恢复。
数据恢复的方法主要有2种
- 使用“mysql”命令行客户端工具进行数据恢复
- 使用“SOURCE”语句进行数据恢复。
使用“mysql”命令行客户端工具进行数据恢复的命令如下
```
H:\&gt;mysql -u root -p demo &lt; test.sql
Enter password: *****
```
我来简单介绍下这个数据恢复命令。
- mysql是一个命令行客户端工具可以与MySQL服务器之间进行连接执行SQL语句。
- “-u”后面跟的是用户。
- “-p”后面跟的是密码。
在这个命令里面我指定了数据库因为备份文件test.sql里面只有数据表的备份信息需要指定恢复到哪个数据库中。如果使用的备份文件备份的是数据库的信息比如test1.sql或者是整个MySQL数据库服务器的信息比如test2.sql则不需要指定数据库。
第二种数据恢复的方法是使用“SOURCE”语句恢复数据语法结构如下
```
SOURCE 备份文件名
```
举个小例子,刚才我们对商品信息表和会员信息表进行了备份,现在想用备份的文件进行恢复,就可以用下面的语句:
```
mysql&gt; USE demo;
Database changed
mysql&gt; SOURCE H:\\test.sql
Query OK, 0 rows affected (0.00 sec)
```
注意这里需要先用“USE”语句把当前的数据库变更为demo这样商品信息表和会员表才能恢复到正确的数据库里面。否则可能会恢复错误。
除此之外你还可以通过这种方式用整个数据库的备份文件把数据库恢复回来甚至是用整个数据库服务器的备份文件恢复整个MySQL服务器。
到这里我们就掌握了备份和恢复整个数据库服务器、数据库和数据库中的表的方法。不过有的时候我们只关心表里的数据本身希望能够把表里的数据按照一定的格式保存下来。这个时候mysqldump就不够用了。所以接下来我再给你介绍下MySQL数据导出和导入的方法。
## 如何导出和导入表里的数据?
先来学习下怎么把一个表的数据按照一定的格式,导出成一个文件。
### SELECT语句导出数据
使用“SELECT … INTO OUTFILE”语句导出数据表的语法结构是
```
SELECT 字段列表 INTO OUTFILE 文件名称
FIELDS TERMINATED BY 字符
LINES TERMINATED BY 字符
FROM 表名;
```
我来解释下这段代码。
- INTO OUTFILE 文件名称,表示查询的结果保存到文件名称指定的文件中;
- FIELDS TERMINATED BY 字符,表示列之间的分隔符是“字符”;
- LINES TERMINATED BY 字符,表示行之间的分隔符是“字符”。
举个小例子假设我们要把商品信息表导出到文件H:\goodsmaster.txt中该如何实现呢按照我刚刚介绍的语法结构来尝试一下
```
mysql&gt; SELECT * INTO OUTFILE 'H:\goodsmaster.txt'
-&gt; FIELDS TERMINATED BY ','
-&gt; LINES TERMINATED BY '\n'
-&gt; FROM demo.goodsmaster;
ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
```
结果,系统提示错误。其实,这是因为**服务器的“secure-file-priv”参数选项不允许把文件写入到H:\goodsmaster.txt中**。那怎么解决这个问题呢?
这个时候我们可以通过MySQL的配置文件my.ini来查看一下“secure-file-priv”参数的设定并且按照这个参数设定的要求准备导入文件。
打开C:\ProgramData\MySQL\MySQL Server 8.0\my.ini找到“secure-file-priv”参数设定如下所示
```
# Secure File Priv.
secure-file-priv=&quot;C:/ProgramData/MySQL/MySQL Server 8.0/Uploads&quot;
```
这个意思是说只能把数据导出到“C:/ProgramData/MySQL/MySQL Server 8.0/Uploads”这个文件夹中所以如果我们把数据导出到H:\goodsmaster.txt中就违反了系统参数的设定导致发生错误。
现在我们来修改一下数据导出的SQL语句把导出文件的路径改到系统要求的文件目录看看结果如何
```
mysql&gt; SELECT * INTO OUTFILE 'C:/ProgramData/MySQL/MySQL Server 8.0/Uploads/goodsmaster.txt'
-&gt; FIELDS TERMINATED BY ','
-&gt; LINES TERMINATED BY '\n'
-&gt; FROM demo.goodsmaster;
Query OK, 3 rows affected (0.00 sec)
```
结果显示,执行成功了。下面我们来看一下结果文件的内容:
```
1,0001,书,16开,本,89.00
2,0002,笔,10支装,包,5.00
3,0003,橡皮,\N,个,3.00
```
很显然,这很符合我们希望的导出格式:行与行之间用回车“\n”分隔列与列之间用逗号“,”分隔。
到这里,我们就知道怎么把数据表中的数据按照一定的格式导出到文件了。那在实际工作中,我们还经常需要把一定格式的数据从文件中导入到数据表中。
“LOAD DATA”语句就是MySQL提供的一种快速数据读入的方法在实际工作中常用于大量数据的导入效率极高。
### 使用“LOAD DATA”语句导入数据
“LOAD DATA”是与“SELECT … INTO OUTFILE”相对应的数据导入语句。语句结构是
```
LOAD DATA INFILE 文件名
INTO TABLE 表名
FIELDS TERMINATED BY 字符
LINES TERMINATED BY 字符;
```
我举个小例子来演示一下“LOAD DATA”语句是如何工作的。
还是以我们刚才导出的那个文件goodsmaster.txt为例现在我们把这个文件内的数据导入到商品信息表demo.goodsmaster中去。
为了演示方便我会先把demo.goodsmaster中的数据先删除然后使用“LOAD DATA”语句把刚才的导出文件goodsmaster.txt的内容导入进来再与删除之前的数据进行对比来验证“LOAD DATA”语句的执行效果。
首先,我们把商品信息表中的数据删除:
```
mysql&gt; DELETE FROM demo.goodsmaster
-&gt; WHERE itemnumber&gt;0;
Query OK, 3 rows affected (0.03 sec)
```
然后我们尝试把文件goodsmaster.txt中的数据导入进来
```
mysql&gt; LOAD DATA INFILE 'C:/ProgramData/MySQL/MySQL Server 8.0/Uploads/goodsmaster.txt'
-&gt; INTO TABLE demo.goodsmaster
-&gt; FIELDS TERMINATED BY ','
-&gt; LINES TERMINATED BY '\n';
Query OK, 3 rows affected (0.02 sec)
Records: 3 Deleted: 0 Skipped: 0 Warnings: 0
```
结果显示,导入成功了。我们再查看一下数据表中的内容:
```
mysql&gt; SELECT * FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
+------------+---------+-----------+---------------+------+------------+
3 rows in set (0.00 sec)
```
结果显示与我们删除之前的数据完全一致。这说明“LOAD DATA”语句成功导入了数据文件goodsmaster.txt中的数据。
## 总结
今天我们重点学习了数据备份包括数据备份的工具mysqldump以及用命令行客户端工具“mysql”和SQL语句“SOURCE”进行数据恢复的方法。同时我还给你介绍了用于导出数据表中数据的语句“SELECT … INTO OUTFILE”和导入的语句“LOAD DATA”。这些都是你在备份数据时必不可少的对确保数据的安全性至关重要。
最后提醒你一点“LOAD DATA”是很好用的工具因为它的数据导入速度是非常惊人的。一个400万条数据的文件用“LOAD DATA”语句只需要几分钟就可以完成而其他的方法比如使用Workbench来导入数据就需要花费好几个小时。
## 思考题
这节课我介绍了数据导出语句“SELECT … INTO OUTFILE”并且在第一次数据导出时遇到了一个系统参数“secure-file-priv”设定的目录与导出文件目录不一致从而导致导出失败的问题。最后我通过修改导出文件的文件夹解决了这个问题。
我想请你思考一下如果还是想把导出文件保存到H:\目录下,有没有办法实现呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。