You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2266 lines
130 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Mysql 技能树 - 用 MySQL 解决实际问题
## 0. 目录设计
- 实践
- 创建数据库、数据表、对表中的数据进行增删改查操作、使用函数、表与表之间的关联操作
- 进阶
- 程序存储在服务器上、利用突发事件来调用程序、在不改变存储结构的前提下创建虚拟表以方便查询等
- 优化
- 数据库的设计规范,还会带你创建数据模型,帮助你来理清设计思路
- 案例
- 你从 0 到 1 设计一个连锁超市的信息系统数据库
## 1. 前言
- 鸡汤理论
- 在工作中,最重要的绝对不是你的知识储备量,而是你**解决实际问题的能力**
- **正确的学习方法,远比你投入的时间更重要**
- 快速应用在实战项目: 项目的实际需求 --> 解决问题所需的知识点 --> 用好这些知识的实战经验
- 配置一个自己顺手的数据库操作环境
-
## 1. 实践
### 1.8 聚合函数:怎么高效地进行分组统计?
- MySQL 中有 5 种聚合函数较为常用,分别是求和函数 SUM()、求平均函数 AVG()、最大值函数 MAX()、最小值函数 MIN() 和计数函数 COUNT()
- 项目需求是这样的:超市经营者提出,他们需要统计某个门店,每天、每个单品的销售情况,包括销售数量和销售金额等。这里涉及 3 个数据表,具体信息如下所示:
- 销售明细表demo.transactiondetails)
- ![聚合函数销售明细表](pic/聚合函数销售明细表.png)
- 销售单头表demo.transactionhead)
- ![销售单头表](pic/聚合函数销售单头表.png)
- 商品信息表demo.goodsmaster
- ![聚合函数商品信息表](pic/聚合函数商品信息表.png)
- SUM
- SUM函数可以返回指定字段值的和。我们可以用它来获得用户某个门店每天每种商品的销售总计数据
```sql
mysql> SELECT
-> LEFT(b.transdate, 10), -- 从关联表获取交易时间并且通过LEFT函数获取交易时
-> c.goodsname, -- 从关联表获取商品名称
-> SUM(a.quantity), -- 数量求和
-> SUM(a.salesvalue) -- 金额求和
-> FROM
-> demo.transactiondetails a
-> JOIN
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
-> JOIN
-> demo.goodsmaster c ON (a.itemnumber = c.itemnumber)
-> GROUP BY LEFT(b.transdate, 10) , c.goodsname -- 分组
-> ORDER BY LEFT(b.transdate, 10) , c.goodsname; -- 排序
+-----------------------+-----------+-----------------+-------------------+
| LEFT(b.transdate, 10) | goodsname | SUM(a.quantity) | SUM(a.salesvalue) |
+-----------------------+-----------+-----------------+-------------------+
| 2020-12-01 | | 2.000 | 178.00 |
| 2020-12-01 | | 5.000 | 25.00 |
| 2020-12-02 | | 4.000 | 356.00 |
| 2020-12-02 | | 16.000 | 80.00 |
+-----------------------+-----------+-----------------+-------------------+
4 rows in set (0.01 sec)
```
- 我们引入了 2 个关键字LEFT 和 ORDER BY
- LEFT(strn)**表示返回字符串 str 最左边的 n 个字符**。我们这里的 LEFTa.transdate,10表示返回交易时间字符串最左边的 10 个字符。在 MySQL 中,
DATETIME 类型的默认格式是YYYY-MM-DD也就是说年份 4 个字符,之后是“-”,然后是月份 2 个字符,之后又是“-”,然后是日 2 个字符,所以完整的年月日
是 10 个字符。用户要求按照日期统计,所以,**我们需要从日期时间数据中,把年月日的部分截取出来**。
- ORDER BY**表示按照指定的字段排序**。超市经营者指定按照日期和单品统计,那么,统计的结果按照交易日期和商品名称的顺序排序,会更加清晰。
- 上述步骤是分为3步
- 第一步,完成 3 个表的连接
- 第二步,对结果集按照交易时间和商品名称进行分组
- 第三步,对各组的销售数量和销售金额进行统计,并且按照交易日期和商品名称排序
- **如果用户需要知道全部商品销售的总计数量和总计金额,我们也可以把数据集的整体看作一个分组,进行计算**。这样就不需要分组关键字 GROUP BY以及排序关键字 ORDER BY
了。你甚至不需要从关联表中获取数据,也就不需要连接了。就像下面这样:
```sql
mysql> SELECT
-> SUM(quantity), -- 总计数量
-> SUM(salesvalue)-- 总计金额
-> FROM
-> demo.transactiondetails;
+---------------+-----------------+
| SUM(quantity) | SUM(salesvalue) |
+---------------+-----------------+
| 27.000 | 639.00 |
+---------------+-----------------+
1 row in set (0.05 sec)
```
- **AVG、MAX和 MIN**
- 举个例子,如果用户需要计算每天、每种商品,平均一次卖出多少个、多少钱,这个时候,我们就可以用到 AVG函数了
```sql
mysql> SELECT
-> LEFT(a.transdate, 10),
-> c.goodsname,
-> AVG(b.quantity), -- 平均数量
-> AVG(b.salesvalue) -- 平均金额
-> FROM
-> demo.transactionhead a
-> JOIN
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
-> JOIN
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-> GROUP BY LEFT(a.transdate,10),c.goodsname
-> ORDER BY LEFT(a.transdate,10),c.goodsname;
+-----------------------+-----------+-----------------+-------------------+
| LEFT(a.transdate, 10) | goodsname | AVG(b.quantity) | AVG(b.salesvalue) |
+-----------------------+-----------+-----------------+-------------------+
| 2020-12-01 | | 2.0000000 | 178.000000 |
| 2020-12-01 | | 5.0000000 | 25.000000 |
| 2020-12-02 | | 2.0000000 | 178.000000 |
| 2020-12-02 | | 8.0000000 | 40.000000 |
+-----------------------+-----------+-----------------+-------------------+
4 rows in set (0.00 sec)
```
- **MAX和 MIN**
- MAX() 表示获取指定字段在分组中的最大值MIN() 表示获取指定字段在分组中的最小值。它们的实现原理差不多,下面我就重点讲一下 MAX()知道了它的用法MIN() 也就
很好理解了。
- 假如用户要求计算每天里的一次销售的最大数量和最大金额,就可以用下面的代码,得到我们需要的结果:
```sql
mysql> SELECT
-> LEFT(a.transdate, 10),
-> MAX(b.quantity), -- 数量最大值
-> MAX(b.salesvalue) -- 金额最大值
-> FROM
-> demo.transactionhead a
-> JOIN
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
-> JOIN
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-> GROUP BY LEFT(a.transdate,10)
-> ORDER BY LEFT(a.transdate,10);
+-----------------------+-----------------+-------------------+
| LEFT(a.transdate, 10) | MAX(b.quantity) | MAX(b.salesvalue) |
+-----------------------+-----------------+-------------------+
| 2020-12-01 | 5.000 | 178.00 |
| 2020-12-02 | 10.000 | 267.00 |
+-----------------------+-----------------+-------------------+
2 rows in set (0.00 sec)
```
- 千万不要以为 MAXb.quantity和 MAXb.salesvalue算出的结果一定是同一条记录的数据。实际上**MySQL 是分别计算的**。下面我们就来分析一下刚刚的查询。
- MAX字段这个函数返回分组集中最大的那个值。如果你要查询 MAX字段1和 MAX字段 2而它们是相互独立、分别计算的你千万不要想当然地认为结果
在同一条记录上。
- **COUNT**
- 怎么解决卡顿的问题呢?我们想到了一个分页的策略。
- 所谓的分页策略,其实就是,不把查询的结果一次性全部返回给客户端,而是根据用户电脑屏幕的大小,计算一屏可以显示的记录数,每次只返回用户电脑屏幕可以显示的数据
集。接着,再通过翻页、跳转等功能按钮,实现查询目标的精准锁定。这样一来,每次查询的数据量较少,也就大大提高了系统响应速度。
- 这个策略能够实现的一个关键,就是要**计算出符合条件的记录一共有多少条**,之后才能计算出一共有几页、能不能翻页或跳转。
- 要计算记录数,就要用到 COUNT() 函数了。这个函数有两种情况。
- COUNT*):统计一共有多少条记录;
- COUNT字段统计有多少个不为空的字段值。
- 如果 COUNT*)与 GROUP BY 一起使用,就表示统计分组内有多少条数据。它也可以单独使用,这就相当于数据集全体是一个分组,统计全部数据集的记录数。
- 假设我有个销售流水明细表如下:
```sql
mysql> SELECT *
-> FROM demo.transactiondetails;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | salesvalue |
+---------------+------------+----------+-------+------------+
| 1 | 1 | 2.000 | 89.00 | 178.00 |
| 1 | 2 | 5.000 | 5.00 | 25.00 |
| 2 | 1 | 3.000 | 89.00 | 267.00 |
| 2 | 2 | 6.000 | 5.00 | 30.00 |
| 3 | 1 | 1.000 | 89.00 | 89.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 |
+---------------+------------+----------+-------+------------+
6 rows in set (0.00 sec)
```
- 如果我们一屏可以显示 30 行,需要多少页才能显示完这个表的全部数据呢?
```sql
mysql> SELECT COUNT(*)
-> FROM demo.transactiondetails;
+----------+
| COUNT(*) |
+----------+
| 6 |
+----------+
1 row in set (0.03 sec)
```
- 我们这里只有 6 条数据,一屏就可以显示了,所以一共 1 页。
- 那么,如果超市经营者想知道,每天、每种商品都有几次销售,我们就需要按天、按商品名称,进行分组查询:
```sql
mysql> SELECT
-> LEFT(a.transdate, 10), c.goodsname, COUNT(*) -- 统计销售次数
-> FROM
-> demo.transactionhead a
-> JOIN
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
-> JOIN
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-> GROUP BY LEFT(a.transdate, 10) , c.goodsname
-> ORDER BY LEFT(a.transdate, 10) , c.goodsname;
+-----------------------+-----------+----------+
| LEFT(a.transdate, 10) | goodsname | COUNT(*) |
+-----------------------+-----------+----------+
| 2020-12-01 | | 1 |
| 2020-12-01 | | 1 |
| 2020-12-02 | | 2 |
| 2020-12-02 | | 2 |
+-----------------------+-----------+----------+
4 rows in set (0.00 sec)
```
- 运行这段代码,我们就得到了每天、每种商品有几次销售的全部结果。
- COUNT字段
- COUNT字段用来统计分组内这个字段的值出现了多少次。如果字段值是空就不统计。
- 为了说明它们的区别,我举个小例子。假设我们有这样的一个商品信息表,里面包括了商品编号、条码、名称、规格、单位和售价的信息。
```sql
mysql> SELECT *
-> FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | | 16 | | 89.00 |
| 2 | 0002 | | NULL | | 5.00 |
| 3 | 0002 | | NULL | | 10.00 |
+------------+---------+-----------+---------------+------+------------+
3 rows in set (0.01 sec)
```
- 如果我们要统计字段“goodsname”出现了多少次就要用到函数 COUNTgoodsname结果是 3 次:
```sql
mysql> SELECT COUNT(goodsname) -- 统计商品名称字段
-> FROM demo.goodsmaster;
+------------------+
| COUNT(goodsname) |
+------------------+
| 3 |
+------------------+
1 row in set (0.00 sec)
```
- 如果我们统计字段“specification”用 COUNT(specification),结果是 1 次:
```sql
mysql> SELECT COUNT(specification) -- 统计规格字段
-> FROM demo.goodsmaster;
+----------------------+
| COUNT(specification) |
+----------------------+
| 1 |
+----------------------+
1 row in set (0.00 sec)
```
- 你可能会问为啥计数字段“goodsname”的结果是 3计数字段“specification”却只有 1 呢其实这里的原因就是3 条记录里面的字段“goodsname”没有空值因此被
统计了 3 次而字段“specification”有 2 个空值,因此只统计了 1 次。
- 理解了这一点,**你就可以利用计数函数对某个字段计数时,不统计空值的特点,对表中字段的非空值进行计数了**。
### 1.13 临时表:复杂查询,如何保存中间结果?
- 拆解一个复杂的查询,通过临时表来保存中间结果,从而把一个复杂查询变得简单而且容易实现。
- 临时表是什么?
- 临时表是一种特殊的表,用来存储查询的中间结果,并且会**随着当前连接的结束而自动删除**。MySQL 中有 2 种临时表,分别是**内部临时表和外部临时表**
- 内部临时表主要用于性能优化,由系统自动产生,我们无法看到;
- 外部临时表通过 SQL 语句创建,我们可以使用。
- 外部临时表
- 临时表的创建语法结构:
```sql
CREATE TEMPORARY TABLE 表名(
字段名 字段类型,
...
);
```
- 跟普通表相比,临时表有 3 个不同的特征:
- 临时表的创建语法需要用到**关键字 TEMPORARY**
- 临时表创建完成之后,**只有当前连接可见,其他连接是看不到的,具有连接隔离性**
- 临时表在**当前连接结束之后,会被自动删除**。
- 因为临时表有连接隔离性,不同连接创建相同名称的临时表也不会产生冲突,**适合并发程序的运行**。而且,连接结束之后,临时表会自动删除,也不用担心大量无用的中间数据会
残留在数据库中。因此,我们就可以利用这些特点,**用临时表来存储 SQL 查询的中间结果**。
- 如何用临时表简化复杂查询?
- 举个例子,超市经营者想要查询 2020 年 12 月的一些特定商品销售数量、进货数量、返厂数量,那么,我们就要先把销售、进货、返厂这 3 个模块分开计算,用临时表来存储中间
计算的结果,最后合并在一起,形成超市经营者想要的结果集。
- 假设我们的销售流水表mysales如下所示
- ![销售流水表](pic/销售流水表.png)
- 用下面的 SQL 语句,查询出每个单品的销售数量和销售金额,并存入临时表:
```sql
mysql> CREATE TEMPORARY TABLE demo.mysales
-> SELECT -- 用查询的结果直接生成临时表
-> itemnumber,
-> SUM(quantity) AS QUANTITY,
-> SUM(salesvalue) AS salesvalue
-> FROM
-> demo.transactiondetails
-> GROUP BY itemnumber
-> ORDER BY itemnumber;
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM demo.mysales;
+------------+----------+------------+
| itemnumber | QUANTITY | salesvalue |
+------------+----------+------------+
| 1 | 5.000 | 411.18 |
| 2 | 5.000 | 24.75 |
+------------+----------+------------+
2 rows in set (0.01 sec)
```
- 这里我是直接用**查询结果来创建的临时表**。因为创建临时表就是为了存放某个查询的中间结果。直接用查询语句创建临时表比较快捷,而且连接结束后临时表就会
被自动删除,不需要过多考虑表的结构设计问题(比如冗余、效率等)。
- 接下来,我们计算一下 2020 年 12 月的进货信息
- 我们的进货数据包括进货单头表importhead和进货单明细表importdetails
- ![临时表进货子表](pic/临时表进货子表.png)
- 用下面的 SQL 语句计算进货数据,并且保存在临时表里面:
```sql
mysql> CREATE TEMPORARY TABLE demo.myimport
-> SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.importvalue) AS importvalue
-> FROM demo.importhead a JOIN demo.importdetails b
-> ON (a.listnumber=b.listnumber)
-> GROUP BY b.itemnumber;
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM demo.myimport;
+------------+----------+-------------+
| itemnumber | quantity | importvalue |
+------------+----------+-------------+
| 1 | 5.000 | 290.00 |
| 2 | 5.000 | 15.00 |
| 3 | 8.000 | 40.00 |
+------------+----------+-------------+
3 rows in set (0.00 sec)
```
- 这样,我们又得到了一个临时表 demo.myimport,里面保存了我们需要的进货数据。
- 接着,我们来查询单品返厂数据,并且保存到临时表。
- 我们的返厂数据表有 2 个,分别是返厂单头表(returnhead)和返厂单明细表(returndetails)。
- 返厂单头表包括返厂单编号、供货商编号、仓库编号、操作员编号和验收日期:
- ![临时表返厂单表头](pic/临时表返厂单表头.png)
- 返厂单明细表包括返厂单编号、商品编号、返厂数量、返厂价格和返厂金额:
- ![临时表返厂单明细表](pic/临时表返厂单明细表.png)
- 我们可以使用下面的 SQL 语句计算返厂信息,并且保存到临时表中。
```sql
mysql> CREATE TEMPORARY TABLE demo.myreturn
-> SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.returnvalue) AS retur
-> FROM demo.returnhead a JOIN demo.returndetails b
-> ON (a.listnumber=b.listnumber)
-> GROUP BY b.itemnumber;
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> SELECT * FROM demo.myreturn;
+------------+----------+-------------+
| itemnumber | quantity | returnvalue |
+------------+----------+-------------+
| 1 | 2.000 | 115.00 |
| 2 | 1.000 | 3.00 |
| 3 | 1.000 | 5.00 |
+------------+----------+-------------+
3 rows in set (0.00 sec)
```
- 这样,我们就获得了单品的返厂信息。
- 有了前面计算出来的数据,现在,我们就可以把单品的销售信息、进货信息和返厂信息汇总到一起了。
- 如果你跟着实际操作的话,你可能会有这样一个问题:我们现在有 3 个临时表,分别存储单品的销售信息、进货信息和返厂信息。那么,能不能把这 3 个表相互关联起来,把这些信息都汇总到对应的单品呢?
- **答案是不行,不管是用内连接、还是用外连接,都不可以**。因为无论是销售信息、进货信息,还是返厂信息,都存在商品信息缺失的情况。换句话说,就是在指定时间段内,某些
商品可能没有销售,某些商品可能没有进货,某些商品可能没有返厂。**如果仅仅通过这 3 个表之间的连接进行查询,我们可能会丢失某些数据**。
- 为了解决这个问题,我们可以**引入商品信息表。因为商品信息表包含所有的商品**,因此,把商品信息表放在左边,与其他的表进行左连接,就可以确保所有的商品都包含在结果集
中。凡是不存在的数值,都设置为 0然后再筛选一下把销售、进货、返厂都是 0 的商品去掉这样就能得到我们最终希望的查询结果2020 年 12 月的商品销售数量、进货数
量和返厂数量。
- 代码如下所示:
```sql
mysql> SELECT
-> a.itemnumber,
-> a.goodsname,
-> ifnull(b.quantity,0) as salesquantity, -- 如果没有销售记录销售数量设置为0
-> ifnull(c.quantity,0) as importquantity, -- 如果没有进货进货数量设为0
-> ifnull(d.quantity,0) as returnquantity -- 如果没有返厂返厂数量设为0
-> FROM
-> demo.goodsmaster a -- 商品信息表放在左边进行左连接,确保所有的商品都包
-> LEFT JOIN demo.mysales b
-> ON (a.itemnumber=b.itemnumber)
-> LEFT JOIN demo.myimport c
-> ON (a.itemnumber=c.itemnumber)
-> LEFT JOIN demo.myreturn d
-> ON (a.itemnumber=d.itemnumber)
-> HAVING salesquantity>0 OR importquantity>0 OR returnquantity>0; -- 在结果集中
+------------+-----------+---------------+----------------+----------------+
| itemnumber | goodsname | salesquantity | importquantity | returnquantity |
+------------+-----------+---------------+----------------+----------------+
| 1 | | 5.000 | 5.000 | 2.000 |
| 2 | | 5.000 | 5.000 | 1.000 |
| 3 | 橡皮 | 0.000 | 8.000 | 1.000 |
+------------+-----------+---------------+----------------+----------------+
3 rows in set (0.00 sec)
```
- 总之,通过临时表,我们就可以把一个复杂的问题拆分成很多个前后关联的步骤,把中间的运行结果存储起来,用于之后的查询。这样一来,**就把面向集合的 SQL 查询变成了面向
过程的编程模式,大大降低了难度**。
- 内存临时表和磁盘临时表
- 由于采用的存储方式不同,临时表也可分为内存临时表和磁盘临时表,它们有着各自的优缺点,下面我来解释下。
- **关于内存临时表,有一点你要注意的是,你可以通过指定引擎类型(比如 ENGINE=MEMORY来告诉 MySQL 临时表存储在内存中**。
- 创建一个内存中的临时表:
```sql
mysql> CREATE TEMPORARY TABLE demo.mytrans
-> (
-> itemnumber int,
-> groupnumber int,
-> branchnumber int
-> ) ENGINE = MEMORY; (临时表数据存在内存中)
Query OK, 0 rows affected (0.00 sec)
```
- 接下来,我们在磁盘上创建一个同样结构的临时表。**在磁盘上创建临时表时只要我们不指定存储引擎MySQL 会默认存储引擎是 InnoDB并且把表存放在磁盘上**。
```sql
mysql> CREATE TEMPORARY TABLE demo.mytransdisk
-> (
-> itemnumber int,
-> groupnumber int,
-> branchnumber int
-> );
Query OK, 0 rows affected (0.00 sec)
```
- 现在,我们向刚刚的两张表里都插入同样数量的记录,然后再分别做一个查询:
```sql
mysql> SELECT COUNT(*) FROM demo.mytrans;
+----------+
| count(*) |
+----------+
| 4355 |
+----------+
1 row in set (0.00 sec)
mysql> SELECT COUNT(*) FROM demo.mytransdisk;
+----------+
| count(*) |
+----------+
| 4355 |
+----------+
1 row in set (0.21 sec)
```
- 可以看到,区别是比较明显的。对于同一条查询,内存中的临时表执行时间不到 10 毫秒,而磁盘上的表却用掉了 210 毫秒。显然,内存中的临时表查询速度更快。
- 不过,内存中的临时表也有缺陷。因为数据完全在内存中,所以,一旦断电,数据就消失了,无法找回。**不过临时表只保存中间结果,所以还是可以用的**。
- 内存临时表和磁盘临时表的优缺点:
- ![内存临时表和磁盘临时表的优缺点](pic/内存临时表和磁盘临时表的优缺点.png)
- 总结:
- 我们学习了临时表的概念,以及使用临时表来存储中间结果以拆分复杂查询的方法。临时表可以存储在磁盘中,也可以通过指定引擎的办法存储在内存中,以加快存取速度。
- 其实,临时表有很多好处,除了可以帮助我们把复杂的 SQL 查询拆分成多个简单的 SQL查询而且因为临时表是连接隔离的不同的连接可以使用相同的临时表名称相互之
间不会受到影响。除此之外,临时表会在连接结束的时候自动删除,不会占用磁盘空间。
- 当然,临时表也有不足,比如会挤占空间。在使用临时表的时候,要从简化查询和挤占资源两个方面综合考虑,既不能过度加重系统的负担,同时又能够通过存储中间结果,最大限度地简化查询。
## 2. 进阶
### 2.1 视图:如何简化查询?
- 视图是一种虚拟表,我们可以把一段查询语句作为视图存储在数据库中,在需要的时候,可以把视图看做一个表,对里面的数据进行查询。
- 让查询变得简单,而且,**视图没有实际存储数据,还避免了数据存储过程中可能产生的冗余,提高了存储的效率**。
- 视图的创建及其好处
- 创建视图的语法结构:
```sql
CREATE [OR REPLACE]
VIEW 视图名称 [(字段列表)]
AS 查询语句
```
- 现在假设我们要查询一下商品的每日销售明细这就要从销售流水表demo.trans和 商品信息表demo.goodsmaster中获取到销售数据和对应的商品信息数据。
- 销售流水表包含流水单号、商品编号、销售数量、销售金额和交易时间等信息:
- ![销售流水表](pic/销售流水表.png)
- 商品信息表包含商品编号、条码、名称和售价等信息:
- ![商品信息表](pic/视图商品信息表.png)
- 在不使用视图的情况下,我们可以通过对销售流水表和商品信息表进行关联查询,得到每天商品销售统计的结果,包括销售日期、商品名称、每天销售数量的合计和每天销售金额
的合计,如下所示:
```sql
mysql> SELECT
-> a.transdate,
-> a.itemnumber,
-> b.goodsname,
-> SUM(a.quantity) AS quantity, -- 统计销售数量
-> SUM(a.salesvalue) AS salesvalue -- 统计销售金额
-> FROM
-> demo.trans AS a
-> LEFT JOIN -- 连接查询
-> demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-> 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如下所示
- ![视图历史库存表](pic/视图历史库存表.png)
- 接下来我们的查询步骤会使用到子查询和派生表,很容易理解,你知道含义就行了。
- 子查询:就是嵌套在另一个查询中的查询。
- 派生表:如果我们在查询中把子查询的结果作为一个表来使用,这个表就是派生表。
- 这个查询的具体步骤是:
- 通过子查询获得单品销售统计的查询结果;
- 把第一步中的查询结果作为一个派生表,跟历史库存表进行连接,查询获得包括销售日期、商品名称、销售数量和历史库存数量在内的最终结果。
```sql
mysql> SELECT
-> a.transdate,
-> a.itemnumber,
-> a.goodsname,
-> a.quantity, -- 获取单品销售数量
-> b.invquantity -- 获取历史库存数量
-> FROM
-> (SELECT -- 子查询,统计单品销售
-> a.transdate,
-> a.itemnumber,
-> b.goodsname,
-> SUM(a.quantity) AS quantity,
-> SUM(a.salesvalue) AS salesvalue
-> FROM
-> demo.trans AS a
-> LEFT JOIN demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-> GROUP BY a.transdate , a.itemnumber
-> ) AS a -- 派生表,与历史库存进行连接
-> LEFT JOIN
-> demo.inventoryhist AS b
-> ON (a.transdate = b.invdate
-> 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)
```
- 可以看到,这个查询语句是比较复杂的,可读性和可维护性都比较差。那该怎么办呢?其实,针对这种情况,我们就可以使用视图。
- 我们可以把商品的每日销售统计查询做成一个视图,存储在数据库里,代码如下所示:
```sql
mysql> CREATE VIEW demo.trans_goodsmaster AS -- 创建视图
-> SELECT
-> a.transdate,
-> a.itemnumber,
-> b.goodsname, -- 从商品信息表中获取名称
-> SUM(a.quantity) AS quantity, -- 统计销售数量
-> SUM(a.salesvalue) AS salesvalue -- 统计销售金额
-> FROM
-> demo.trans AS a
-> LEFT JOIN
-> demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber) -- 与商品信息表关联
-> GROUP BY a.transdate , a.itemnumber; -- 按照销售日期和商品编号分组
Query OK, 0 rows affected (0.01 sec)
```
- 这样一来,我们每次需要查询每日商品销售数据的时候,就可以直接查询视图,不需要再写一个复杂的关联查询语句了。
- 我们来试试用一个查询语句直接从视图中进行查询:
```sql
mysql> SELECT * -- 直接查询
-> 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)
```
- 结果显示,这两种查询方式得到的结果是一样的。
- 如果我们要进一步查询“每日单品销售的数量与当日的库存数量的对比”,就可以把刚刚定义的视图作为一个数据表来使用。我们把它跟历史库存表连接起来,来获取销售数量和
历史库存数量。
```sql
mysql> SELECT
-> a.transdate, -- 从视图中获取销售日期
-> a.itemnumber, -- 从视图中获取商品编号
-> a.goodsname, -- 从视图中获取商品名称
-> a.quantity, -- 从视图中获取销售数量
-> b.invquantity -- 从历史库存表中获取历史库存数量
-> FROM
-> demo.trans_goodsmaster AS a -- 视图
-> LEFT JOIN
-> demo.inventoryhist AS b ON (a.transdate = b.invdate
-> 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)
```
- 结果显示,这里的查询结果和我们刚刚使用派生表的查询结果是一样的。但是,使用视图的查询语句明显简单多了,可读性更好,也更容易维护。
- 如何操作视图和视图中的数据?
- 创建完了视图,我们还经常需要对视图进行一些操作,比如修改、查看和删除视图。
- 如何操作视图?
- 修改、查看、删除视图的操作比较简单,你只要掌握具体的语法就行了。
- 修改视图的语法如下所示:
```sql
ALTER VIEW 视图名
AS 查询语句;
```
- 查看视图的语法是:
```sql
查看视图:
DESCRIBE 视图名;
```
- 删除视图要使用 DROP 关键词,具体方法如下:
```sql
删除视图:
DROP VIEW 视图名;
```
- 如何操作视图中的数据?
- 视图本身是一个虚拟表,所以,对视图中的数据进行插入、修改和删除操作,实际都是通过对实际数据表的操作来实现的。
- 在视图中插入数据
- 借用刚刚的视图 demo.view_goodsmaster 来给你解释下。假设商品信息表中的规格字段specification被删除了当我们尝试用 INSERT INTO 语句向视图中插入一条记录
的时候,就会提示错误了:
```sql
mysql> INSERT INTO demo.view_goodsmaster
-> (itemnumber,barcode,goodsname,salesprice)
-> VALUES
-> (5,'0005','测试',100);
ERROR 1471 (HY000): The target table view_goodsmaster of the INSERT is not ins ...
```
- 这是因为,**只有视图中的字段跟实际数据表中的字段完全一样MySQL 才允许通过视图插入数据**。刚刚的视图中包含了实际数据表所没有的字段“specification”所以在插入
数据时,系统就会提示错误。
- 为了解决这个问题,我们来修改一下视图,让它只包含实际数据表中有的字段,也就是商品编号、条码、名称和售价。代码如下:
```sql
mysql> ALTER VIEW demo.view_goodsmaster
-> AS
-> SELECT itemnumber,barcode,goodsname,salesprice -- 只包含实际表中存在的字段
-> FROM demo.goodsmaster
-> WHERE salesprice > 50;
Query OK, 0 rows affected (0.01 sec)
```
- 对视图进行修改之后,我们重新尝试向视图中插入一条记录:
```sql
mysql> INSERT INTO demo.view_goodsmaster
-> (itemnumber,barcode,goodsname,salesprice)
-> VALUES
-> (5,'0005','测试',100);
Query OK, 1 row affected (0.02 sec)
```
- 结果显示,插入成功了。
- 实际数据表中的数据情况
```sql
mysql> SELECT *
-> 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 的商品数据了。
- **删除视图中的数据**
- 我们可以通过 DELETE 语句,删除视图中的数据:
```sql
mysql> DELETE FROM demo.view_goodsmaster -- 直接在视图中删除数据
-> WHERE itemnumber = 5;
Query OK, 1 row affected (0.02 sec)
```
- 现在我们来查看视图和实际数据表的内容,会发现商品编号是 5 的商品都已经被删除了。
```sql
mysql> SELECT *
-> FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | | 89.00 | -- 视图中已经没有商品编号是5的商品
+------------+---------+-----------+------------+
1 row in set (0.00 sec)
mysql> SELECT *
-> 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)
```
- 修改视图中的数据
- 我们可以通过 UPDATE 语句对视图中的数据进行修改:
```sql
mysql> UPDATE demo.view_goodsmaster -- 更新视图中的数据
-> SET salesprice = 100
-> WHERE itemnumber = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
- 结果显示,更新成功了。现在我们来查看一下视图和实际数据表,代码如下所示:
```sql
mysql> SELECT *
-> FROM demo.view_goodsmaster;
+------------+---------+-----------+------------+
| itemnumber | barcode | goodsname | salesprice |
+------------+---------+-----------+------------+
| 1 | 0001 | | 100.00 | -- 视图中的售价改过了
+------------+---------+-----------+------------+
1 row in set (0.01 sec)
mysql> SELECT *
-> 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的查询。
- 视图有哪些优缺点?
- 到这里,视图的操作我就讲完了,现在我们把视线拔高一点,来看看视图都有哪些优缺点。只有全面掌握视图的特点,我们才能充分享受它的高效,避免踩坑。
- 视图的优点。
- 第一,因为我们可以把视图看成一张表来进行查询,所以在使用视图的时候,我们不用考虑视图本身是如何获取数据的,里面有什么逻辑,包括了多少个表,有哪些关联操作,而
是可以直接使用这样一来,实际上就把查询模块化了,查询变得更加简单,提高了开发和维护的效率。所以,你可以把那些经常会用到的查询和复杂查询的子查询定义成视图,
存储到数据库中,这样可以为你以后的使用提供方便。
- 第二,视图跟实际数据表不一样,它存储的是查询语句。所以,在使用的时候,我们要通过定义视图的查询语句来获取结果集。而视图本身不存储数据,不占用数据存储的资源。
- 第三,视图具有隔离性。视图相当于在用户和实际的数据表之间加了一层虚拟表。也就是说,**用户不需要查询数据表,可以直接通过视图获取数据表中的信息**。这样既提高了数据
表的安全性,同时也通过视图把用户实际需要的信息汇总在了一起,查询起来很轻松。
- 第四,**视图的数据结构相对独立,即便实际数据表的结构发生变化,我们也可以通过修改定义视图的查询语句,让查询结果集里的字段保持不变**。这样一来,针对视图的查询就不
受实际数据表结构变化的影响了。
- 假设我们有一个实际的数据表demo.goodsmaster包括商品编号、条码、名称、规格和售价等信息
- ![视图实际的数据表](pic/视图实际的数据表.png)
- 在这个表的基础上,我们建一个视图,查询所有价格超过 50 元的商品:
```sql
mysql> CREATE VIEW demo.view_goodsmaster AS
-> SELECT *
-> FROM demo.goodsmaster
-> WHERE salesprice > 50;
Query OK, 0 rows affected (0.03 sec)
```
- 接着,我们在这个视图的基础上做一个查询,来验证一下视图的内容:
```sql
mysql> SELECT barcode,goodsname,specification
-> FROM demo.view_goodsmaster;
+---------+-----------+---------------+
| barcode | goodsname | specification |
+---------+-----------+---------------+
| 0001 | | 16 |
+---------+-----------+---------------+
1 row in set (0.00 sec)
```
- 结果显示,我们得到了商品信息表中售价大于 50 元的商品16 开)。
- 假设现在我们需要把数据表 demo.goodsmaster 中的字段“specification”删掉就可以用下面的代码
```sql
mysql> ALTER TABLE demo.goodsmaster DROP COLUMN specification;
Query OK, 0 rows affected (0.13 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
- 这样一来,因为少了一个字段,而我们的语句又是直接查询数据表的,代码就会提示错误:
```sql
mysql> SELECT barcode,goodsname,specification
-> FROM demo.goodsmaster;
ERROR 1054 (42S22): Unknown column 'specification' in 'field list'
```
- 你看代码提示字段“specification”不存在。
- 但是,如果查询的是视图,就可以通过修改视图来规避这个问题。我们可以用下面的代码把刚才的视图修改一下:
```sql
mysql> ALTER VIEW demo.view_goodsmaster
-> AS
-> SELECT
-> itemnumber,
-> barcode,
-> goodsname,
-> '' as specification, -- 由于字段不存在插入一个长度是0的空字符串作为这个字段的值
-> salesprice
-> FROM demo.goodsmaster
-> WHERE salesprice > 50;
Query OK, 0 rows affected (0.02 sec)
```
- 你看虽然实际数据表中已经没有字段“specification”了但是视图中却保留了这个字段而且字段值始终是空字符串。所以我们不用修改原有视图的查询语句它也会正常
运行。下面的代码查询的结果中就包括了实际数据表没有的字段“specification”。
```sql
mysql> SELECT barcode,goodsname,specification
-> FROM demo.view_goodsmaster;
+---------+-----------+---------------+
| barcode | goodsname | specification |
+---------+-----------+---------------+
| 0001 | | |
+---------+-----------+---------------+
1 row in set (0 00 sec)
```
- 结果显示,运行成功了。这个视图查询,就没有受到实际数据表中删除字段的影响。
- 视图有这么多好处,那我以后都用视图可以吗?其实不是的,视图也有自身的不足。
- 如果我们在实际数据表的基础上创建了视图,那么,**如果实际数据表的结构变更了,我们就需要及时对相关的视图进行相应的维护**。特别是当视图是由视图生成的时候,维护会变
得比较复杂。因为创建视图的 SQL 查询可能会对字段重命名,也可能包含复杂的逻辑,这些都会增加维护的成本。
- 一张图来汇总下视图的优缺点:
- ![视图的优缺点](pic/视图的优缺点.png)
- 虽然可以更新视图数据,但总的来说,视图作为虚拟表,主要用于方便查询。我不建议你更新视图的数据,因为对视图数据的更改,都是通过对实际
数据表里数据的操作来完成的,而且有很多限制条件。
- 视图虽然有很多优点。但是在创建视图、简化查询的同时,也要考虑到视图太多而导致的数据库维护成本的问题。
- 视图不是越多越好,特别是嵌套的视图(就是在视图的基础上创建视图),我不建议你使用,因为逻辑复杂,可读性不好,容易变成系统的潜在隐患。
### 2.2 存储过程:如何提高程序的性能和安全性?
- 在我们的超市项目中,每天营业结束后,超市经营者都要计算当日的销量,核算成本和毛利等营业数据,这也就意味着每天都要做重复的数据统计工作。其实,这种数据量大,而
且计算过程复杂的场景,就非常适合使用存储过程。
- 简单来说呢,存储过程就是把一系列 SQL 语句预先存储在 MySQL 服务器上,需要执行的时候,客户端只需要向服务器端发出调用存储过程的命令,服务器端就可以把预先存储好
的这一系列 SQL 语句全部执行。
- 这样一来,不仅执行效率非常高,而且客户端不需要把所有的 SQL 语句通过网络发给服务器,减少了 SQL 语句暴露在网上的风险,也提高了数据查询的安全性。
- 如何创建存储过程?
- 在创建存储过程的时候,我们需要用到关键字 CREATE PROCEDURE
```sql
CREATE PROCEDURE 存储过程名 [ IN | OUT | INOUT] 参数名称 类型)程序体
```
- 假设在日结计算中,我们需要统计每天的单品销售,包括销售数量、销售金额、成本、毛利、毛利率等。同时,我们还要把计算出来的结果存入单品统计表中。
- 销售单明细表demo.transactiondetails中包括了每笔销售中的商品编号、销售数量、销售价格和销售金额。
```sql
mysql> SELECT *
-> 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中包括流水单号、收款机编号、会员编号、操作员编号、交易时间。
```sql
mysql> SELECT *
-> FROM demo.transactionhead;
+---------------+------------------+-----------+----------+------------+------
| transactionid | transactionno | cashierid | memberid | operatorid | transdat
+---------------+------------------+-----------+----------+------------+------
| 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中包括商品编号、商品条码、商品名称、规格、单位、售价和平均进价。
```sql
mysql> SELECT *
-> FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+------
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgim
+------------+---------+-----------+---------------+------+------------+------
| 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)
```
- 存储过程会用刚刚的三个表中的数据进行计算,并且把计算的结果存储到下面的这个单品统计表中。
```sql
mysql> 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 的变量**,用来记录要计算数据的起始时间和截止时间。
- 第四步,**删除保存结果数据的单品统计表中相同时间段的数据**,目的是防止数据重复。
- 第五步,**计算起始时间和截止时间内单品的销售数量合计、销售金额合计、成本合计、毛利和毛利率,并且把结果存储到单品统计表中**。
- 这五个步骤,我们就可以用下面的代码来实现。
```sql
mysql> DELIMITER // -- 设置分割符为//
-> CREATE PROCEDURE demo.dailyoperation(transdate TEXT)
-> BEGIN -- 开始程序体
-> DECLARE startdate,enddate DATETIME; -- 定义变量
-> SET startdate = date_format(transdate,'%Y-%m-%d'); -- 给起始时间赋值
-> SET enddate = date_add(startdate,INTERVAL 1 DAY); -- 截止时间赋值为1天以后
-> -- 删除原有数据
-> 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),
-> 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>startdate AND b.transdate<enddate
-> GROUP BY
-> LEFT(b.transdate,10),a.itemnumber
-> ORDER BY
-> LEFT(b.transdate,10),a.itemnumber;
-> END
-> // -- 语句结束,执行语句
Query OK, 0 rows affected (0.01 sec)
-> 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 步:
- 按照“成本 = 销售数量×平均价格”的方式计算成本;
- 按照“毛利 = 销售金额 - 成本”的方式计算毛利;
- 按照“毛利率 = 毛利 ÷销售金额”的方式计算毛利率。
- 需要注意的是,这里我**使用 CASE 函数来解决销售金额为 0 时计算毛利的问题**。这是为了防止计算出现被 0 除而报错的情况。不要以为销售金额就一定大于 0在实际项目运行的
过程中,会出现因为优惠而导致实际销售金额为 0 的情况。我建议你在实际工作中,把这些极端情况都考虑在内,提前进行防范,这样你的代码才能稳定可靠。
- 存储过程通过开始时定义的分隔符“//”结束MySQL 执行这段 SQL 语句就创建出了一个存储过程demo.dailyoperation. **最后,你不要忘了把分隔符改回到“;”**。
- 怎么知道我们创建的存储过程是否成功了呢?下面我介绍一下查看存储过程的方法。
- 如何查看存储过程?
- 我们可以通过 SHOW CREATE PROCEDURE 存储过程名称,来查看刚刚创建的存储过程:
```sql
mysql> 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`
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)
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>startdate AND b.transdate<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 日的单品统计数据:
```sql
mysql> CALL demo.dailyoperation('2020-12-01');
Query OK, 2 rows affected (0.03 sec)
```
- 存储过程执行结果提示“Query OK”表示执行成功了。“2 rows affected”表示执行的结果影响了 2 条数据记录。
- 我们用 SELECT 语句来查看一下单品统计表,看看有没有把单品统计的结果存入单品统计表中。
```sql
mysql> SELECT * -- 查询单品统计表中的数据
-> FROM demo.dailystatistics;
+----+------------+----------+-------------+--------+--------+-------------+--
| id | itemnumber | quantity | actualvalue | cost | profit | profitratio | sal
+----+------------+----------+-------------+--------+--------+-------------+--
| 13 | 1 | 3.000 | 267.00 | 100.50 | 166.50 | 0.6236 | 2020-12-01 00:00:00 |
| 14 | 2 | 2.000 | 10.00 | 7.00 | 3.00 | 0.3000 | 2020-12-01 00:00:00 |
+----+------------+----------+-------------+--------+--------+-------------+--
2 rows in set (0.00 sec)
```
- 看到了吗?我们已经能够在单品统计表中,查询到 2020 年 12 月 01 日的单品统计结果了。这也就意味着我们的存储过程被执行了,它计算出了我们需要的单品统计结果,并且
把统计结果存入了单品统计表中。
- 如何修改和删除存储过程?
- 如果你需要修改存储过程的内容,我建议你在 IDE 中操作。这是因为你可以在里面直接修改存储过程,而如果用 SQL 命令来修改存储过程,就必须删除存储过程再重新创
建,相比之下,在 IDE 中修改比较简单。
- 在 MySQL 中,存储过程不像普通的编程语言(比如 VC++、Java 等)那样有专门的集成 开发环境。因此,你可以通过 SELECT 语句,把程序执行的中间结果查询出来,来调试一
个 SQL 语句的正确性。调试成功之后,把 SELECT 语句后移到下一个 SQL 语句之后,再调试下一个 SQL 语句。这样逐步推进,就可以完成对存储过程中所有操作的调试了。当
然,你也可以把存储过程中的 SQL 语句复制出来,逐段单独调试。
- 删除存储过程很简单,你知道具体的语法就行了:
```sql
DROP PROCEDURE 存储过程名称;
```
### 2.3 游标:对于数据集中的记录,该怎么逐条处理?
- 所谓的游标,也就是能够对结果集中的每一条记录进行定位,并对指向的记录中的数据进行操作的数据结构。
- 游标的使用步骤
- 游标只能在存储程序内使用,存储程序包括存储过程和存储函数。关于存储过程,我们上节课刚刚学过,这里我简单介绍一下存储函数。创建存储函数的语法是:
```sql
CREATE FUNCTION 函数名称 (参数)RETURNS 数据类型 程序体
```
- 存储函数与存储过程很像,但有几个不同点:
- 存储函数必须返回一个值或者数据表,存储过程可以不返回。
- 存储过程可以通过 CALL 语句调用,存储函数不可以。
- 存储函数可以放在查询语句中使用,存储过程不行。
- 存储过程的功能更加强大,包括能够执行对表的操作(比如创建表,删除表等)和事务操作,这些功能是存储函数不具备的。
- 游标在存储函数中的使用方法和在存储过程中的使用方法是一样的。
- 在使用游标的时候,主要有 4 个步骤。
- 第一步,定义游标。语法结构如下:
```sql
DECLARE 游标名CURSOR FOR 查询语句
```
- 这里就是声明一个游标,它可以操作的数据集是“查询语句”返回的结果集。
- 第二步,打开游标。语法结构如下:
```sql
OPEN 游标名称;
```
- 打开游标之后,系统会为游标准备好查询的结果集,为后面游标的逐条读取结果集中的记录做准备。
- 第三步,从游标的数据结果集中读取数据。语法结构是这样的:
```sql
FETCH 游标名 INTO 变量列表;
```
- 这里的意思是通过游标,把当前游标指向的结果集中那一条记录的数据,赋值给列表中的变量。
- 需要注意的是,**游标的查询结果集中的字段数,必须跟 INTO 后面的变量数一致**否则在存储过程执行的时候MySQL 会提示错误。
- 第四步,关闭游标。语法结构如下:
```sql
CLOSE 游标名;
```
- **用完游标之后,你一定要记住及时关闭游标。因为游标会占用系统资源**,如果不及时关闭,游标会一直保持到存储过程结束,影响系统运行的效率。而关闭游标的操作,会释放
游标占用的系统资源。
- 知道了基本步骤,下面我就结合超市项目的实际案例,带你实战一下。
- 案例串讲
- 在超市项目的进货模块中,有一项功能是对进货单数据进行验收。其实就是在对进货单的数据确认无误后,对进货单的数据进行处理,包括增加进货商品的库存,并修改商品的平
均进价。下面我用实际数据来演示一下这个操作流程。
- 这里我们要用到进货单头表demo.importheadl、进货单明细表demo.importdetails、库存表demo.inventory和商品信息表
demo.goodsmaster
- 进货单头表:
- ![游标进货单头表](pic/游标进货单头表.png)
- 进货单明细表:
- ![游标进货单明细表](pic/游标进货单明细表.png)
- 库存表:
- ![游标库存表](pic/游标库存表.png)
- 商品信息表:
- ![游标商品信息表](pic/游标商品信息表.png)
- 要验收进货单,我们就需要对每一个进货商品进行两个操作:
- 在现有库存数量的基础上,加上本次进货的数量;
- 根据本次进货的价格、数量,现有商品的平均进价和库存,计算新的平均进价:(本次进货价格 * 本次进货数量 + 现有商品平均进价 * 现有商品库存)/(本次进货数量 + 现
有库存数量)
- 因为我们需要通过应用程序来控制操作流程,做成一个循环操作,每次只查询一种商品的数据记录并进行处理,一直到把进货单中的数据全部处理完。这样一来,应用必须发送很
多的 SQL 指令到服务器,跟服务器的交互多,不仅代码复杂,而且也不够安全。
- 这个时候,如果使用游标,就很容易了。因为所有的操作都可以在服务器端完成,应用程序只需要发送一个命令调用存储过程就可以了。现在,我们就来看看如何用游标来解决这
个问题。
- 我用代码创建了一个存储过程 demo.mytest。创建存储过程的代码如下
```sql
mysql> DELIMITER //
mysql> CREATE PROCEDURE demo.mytest(mylistnumber INT)
-> BEGIN
-> DECLARE mystockid INT;
-> DECLARE myitemnumber INT;
-> DECLARE myquantity DECIMAL(10,3);
-> DECLARE myprice DECIMAL(10,2);
-> DECLARE done INT DEFAULT FALSE; -- 用来控制循环结束
-> DECLARE cursor_importdata CURSOR FOR -- 定义游标
-> SELECT b.stockid,a.itemnumber,a.quantity,a.importprice
-> FROM demo.importdetails AS a
-> JOIN demo.importhead AS b
-> ON (a.listnumber=b.listnumber)
-> WHERE a.listnumber = mylistnumber;
-> DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; -- 条件处理语句
->
-> OPEN cursor_importdata; -- 打开游标
-> FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice; --
-> REPEAT
-> -- 更新进价
-> UPDATE demo.goodsmaster AS a,demo.inventory AS b
-> SET a.avgimportprice = (a.avgimportprice*b.invquantity+myprice*myquantity)/
-> WHERE a.itemnumber=b.itemnumber AND b.stockid=mystockid AND a.itemnumber=my
-> -- 更新库存
-> UPDATE demo.inventory
-> SET invquantity = invquantity + myquantity
-> WHERE stockid = mystockid AND itemnumber=myitemnumber;
-> -- 获取下一条记录
-> FETCH cursor_importdata INTO mystockid,myitemnumber,myquantity,myprice;
-> UNTIL done END REPEAT;
-> CLOSE cursor_importdata;
-> END
-> //
Query OK, 0 rows affected (0.02 sec)
-> DELIMITER ;
```
- 这段代码比较长,核心操作有 6 步
- 把 MySQL 的分隔符改成“//”
- 开始程序体之后,我定义了 4 个变量,分别是 mystockid、myitemnumber、myquantity 和 myprice这几个变量的作用是存储游标中读取的仓库编号、商品编
号、进货数量和进货价格数据。
- 定义游标。这里我指定了游标的名称以及游标可以处理的数据集mylistnumber 指定的进货单的全部进货商品明细数据)。
- 定义条件处理语句“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”。
- 打开游标,读入第一条记录,然后开始执行数据操作。
- 关闭游标,结束程序。
- 可以看到,在这个存储过程中,我用到了条件处理语句,它的作用是告诉系统,在存储程序遇到问题的时候,应该如何处理。
- 条件处理语句
- 条件处理语句的语法结构:
```sql
DECLARE 处理方式 HANDLER FOR 问题 操作;
```
- 下面我结合刚刚的“DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;”,来解释一下条件处理语句是如何工作的。
- **语法结构中的“问题”是指 SQL 操作中遇到了什么问题**。比如这里的问题是“NOT FOUND”意思就是游标走到结果集的最后没有记录了。也就是说数据集中的所
有记录都已经处理完了。
- 执行的操作是“SET done=TRUE”done 是我定义的用来标识数据集中的数据是否已经处理完成的一个标记。done=TRUE意思是数据处理完成了。
- **处理方式有 2 种选择分别是“CONTINUE”和“EXIT”**,表示遇到问题,执行了语法结构中的“操作”之后,是选择继续运行程序,还是选择退出,结束程序。
- 所以,这个条件处理语句的意思就是:当游标读到结果集的最后,没有记录了,设置操作完成标识为真,然后继续运行程序。
- 在存储过程的第 5 步,为了逐一处理每一条记录,我还使用了流程控制语句。
- 解决复杂问题不可能通过一个 SQL 语句完成,我们需要执行多个 SQL 操作。流程控制语句的作用就是控制存储过程中 SQL 语句的执行顺序,是我们完成复杂操作必不可少的一部
分。下面我就给你具体讲解一下。
- 流程控制语句
- MySQL 的流程控制语句也只能用于存储程序。主要有 3 类。
- 跳转语句ITERATE 和 LEAVE 语句。
- 循环语句LOOP、WHILE 和 REPEAT 语句。
- 条件判断语句IF 语句和 CASE 语句。
- 跳转语句
- ITERATE 语句:只能用在循环语句内,表示重新开始循环。
- LEAVE 语句:可以用在循环语句内,或者以 BEGIN 和 END 包裹起来的程序体内,跳出循环或者跳出程序体的操作。
- 循环语句
- LOOP 语句的语法结构是:
```sql
标签:LOOP
操作
END LOOP 标签;
```
- 关于这个语句需要注意的是LOOP 循环不能自己结束,需要用跳转语句 ITERATE 或者LEAVE 来进行控制。
- WHILE 语句的语法结构:
```sql
WHILE 条件 DO
操作
END WHILE;
```
- WHILE 循环通过判断条件是否为真来决定是否继续执行循环中的操作,你要注意一点,**WHILE 循环是先判断条件,再执行循环体中的操作**。
- REPEAT 语句的语法结构:
```sql
REPEAT
操作
UNTIL 条件 END REPEAT
```
- REPEAT 循环也是通过判断条件是否为真来决定是否继续执行循环内的操作的,与 WHILE 不同的是,**REPEAT 循环是先执行操作,后判断条件**。
- 最后我来讲讲条件判断语句IF 语句和 CASE 语句。
- 条件判断语句
- IF 语句的语法结构是:
```sql
IF 表达式1 THEN 操作1
[ELSEIF 表达式2 THEN 操作2]……
[ELSE 操作N]
END IF
```
- 这里“[]”中的内容是可选的。IF 语句的特点是,不同的表达式对应不同的操作。
- CASE 语句的语法结构是:
```sql
CASE 表达式
WHEN 1 THEN 操作1
[WHEN 2 THEN 操作2]……
[ELSE 操作N]
END CASE;
```
- 这里“[]”中的内容是可选的。CASE 语句的特点是,表达式不同的值对应不同的操作。
- 到这里,我们处理进货单验收的存储过程就创建好了。现在,让我们来运行一下这个存储过程,看看能不能得到我们想要的结果:
```sql
mysql> CALL demo.mytest(1234); -- 调用存储过程验收单号是1234的进货单
Query OK, 0 rows affected (11.68 sec) -- 执行成功了
mysql> select * from demo.inventory; -- 查看库存,已经改过来了
+---------+------------+-------------+
| stockid | itemnumber | invquantity |
+---------+------------+-------------+
| 1 | 1 | 15.000 |
| 1 | 2 | 23.000 |
+---------+------------+-------------+
2 rows in set (0.00 sec)
mysql> select * from demo.goodsmaster; -- 查看商品信息表,平均进价也改过来了
+------------+---------+-----------+---------------+------+------------+------
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgim
+------------+---------+-----------+---------------+------+------------+------
| 1 | 0001 | | 16 | | 89.00 | 31.00 |
| 2 | 0002 | | NULL | | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+------
2 rows in set (0.00 sec)
```
- 很显然,库存和平均价格都被正确地计算出来了。
- 有个小问题要提醒你注意:**如果一个操作要用到另外一个操作的结果,那我们一定不能搞错操作的顺序**。
### 2.4 触发器:如何让数据修改自动触发关联操作,确保数据一致性?
- 在实际开发中,我们经常会遇到这样的情况:有 2 个或者多个相互关联的表,如商品信息和库存信息分别存放在 2 个不同的数据表中,我们在添加一条新商品记录的时候,为了保
证数据的完整性,必须同时在库存表中添加一条库存记录。
- 这样一来,我们就必须把这两个关联的操作步骤写到程序里面,而且要用事务包裹起来,确保这两个操作成为一个原子操作,要么全部执行,要么全部不执行。要是遇到特殊情
况,可能还需要对数据进行手动维护,这样就很容易忘记其中的一步,导致数据缺失。
- 这个时候,其实咱们可以使用触发器。你可以创建一个触发器,让商品信息数据的插入操作自动触发库存数据的插入操作。
- 如何操作触发器?
- 创建触发器
- 创建触发器的语法结构是:
```sql
CREATE TRIGGER 触发器名称 {BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON 表名 FOR EACH ROW 表达式;
```
- 在创建时,你一定要注意触发器的三个要素。
- 表名:表示触发器监控的对象。
- INSERT|UPDATE|DELETE表示触发的事件。INSERT 表示插入记录时触发UPDATE 表示更新记录时触发DELETE 表示删除记录时触发。
- BEFORE|AFTER表示触发的时间。BEFORE 表示在事件之前触发AFTER 表示在事件之后触发。
- 咱们还要知道触发器是不是创建成功了。怎么查看呢?
- 查看触发器
- 查看触发器的语句是:
```sql
SHOW TRIGGERS\G;
```
- 删除触发器
- 删除触发器很简单,你只要知道语法结构就可以了:
```sql
DROP TRIGGER 触发器名称;
```
- 案例
- 超市项目实际实施过程中,客户经常要查询储值余额变动的明细,但是,查询会员消费流水时,存在数据汇总不及时、查询速度比较慢的问题。这时,我们就想到用触发器,及时
把会员储值金额的变化信息记录到一个专门的表中。
- 我先用咱们熟悉的 SQL 语句来实现记录储值金额变动的操作,后面再带你使用触发器来操作。通过两种操作的对比,你就能更好地理解,在什么情况下,触发器能够比普通的 SQL
语句更加简洁高效,从而帮助你用好触发器这个工具,提高开发的能力。
- 下面我就借助具体数据来详细说明一下。这里我们需要用到会员信息表demo.membermaster和会员储值历史表demo.deposithist
- 会员信息表:
- ![触发器会员信息表](pic/触发器会员信息表.png)
- 会员储值历史表:
- ![会员储值历史表](pic/会员储值历史表.png)
- 假如在 2020 年 12 月 20 日这一天,会员编号是 2 的会员李四,到超市的某家连锁店购买了一条烟,消费了 150 元。现在,我们用之前学过的 SQL 语句,把这个会员储值余额的
变动情况记录到会员储值历史表中。
- 第一步,查询出编号是 2 的会员卡的储值金额是多少。我们可以用下面的代码来实现:
```sql
mysql> SELECT memberdeposit
-> FROM demo.membermaster
-> WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 200.00 |
+---------------+
1 row in set (0.00 sec)
```
- 第二步,我们把会员编号是 2 的会员的储值金额减去 150。
```sql
mysql> UPDATE demo.membermaster
-> SET memberdeposit = memberdeposit - 150
-> WHERE memberid = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
- 第三步,读出会员编号是 2 的会员当前的储值金额。
```sql
mysql> SELECT memberdeposit
-> FROM demo.membermaster
-> WHERE memberid = 2;
+---------------+
| memberdeposit |
+---------------+
| 50.00 |
+---------------+
1 row in set (0.00 sec)
```
- 第四步,把会员编号和前面查询中获得的储值起始金额、储值余额和储值金额变化值,写入会员储值历史表。
```sql
mysql> INSERT INTO demo.deposithist
-> (
-> memberid,
-> transdate,
-> oldvalue,
-> newvalue,
-> changedvalue
-> )
-> SELECT 2,NOW(),200,50,-150;
Query OK, 1 row affected (0.02 sec)
Records: 1 Duplicates: 0 Warnings: 0
```
- 这样,我们就完成了记录会员储值金额变动的操作。现在,我们来查询一下记录的结果:
```sql
mysql> SELECT *
-> 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 的触发器。每当更新表中的数据时,先触发触发器,如果发现会员储值金额有变化,就把会员编号信息、更新的时间、更新前
的储值金额、更新后的储值金额和变动金额,写入会员储值历史表。然后,再执行会员信息表的更新操作。
- 创建触发器的代码如下所示:
```sql
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE -- 在更新前触发
ON demo.membermaster
FOR EACH ROW -- 表示每更新一条记录,触发一次
BEGIN -- 开始程序体
IF (new.memberdeposit <> 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 ;
```
- 创建完成之后,我们查看一下触发器,看看是不是真的创建成功了:
```sql
mysql> SHOW TRIGGERS \G;
*************************** 1. row ***************************
Trigger: upd_membermaster -- 触发器名称
Event: UPDATE -- 触发的事件
Table: membermaster -- 表名称
Statement: BEGIN -- 被触发时要执行的程序体
IF (new.memberdeposit <> 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 元。简单说明一下,会员也可以把钱存到会员卡里,需要的时候进行消
费,对应的就是储值金额的增加。
- 我们用下面的代码来完成这个操作:
```sql
mysql> UPDATE demo.membermaster
-> SET memberdeposit = memberdeposit + 10
-> WHERE memberid = 1;
Query OK, 1 row affected (0.03 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
- 现在,我们来查看一下会员信息表和会员储值历史表:
```sql
mysql> 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)
mysql> SELECT *
-> 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 语句有误,比如多了一个字段。在这种情况下,触发器执行会失败,因此,数据更新
的操作也不会执行。我们用下面的代码来验证一下:
```sql
DELIMITER //
CREATE TRIGGER demo.upd_membermaster BEFORE UPDATE
ON demo.membermaster
FOR EACH ROW
BEGIN
IF (new.memberdeposit <> 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
```sql
mysql> update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
- 系统提示因为字段“aa”不存在导致触发器执行失败。现在我们来看看会员储值金额有没有被修改
```sql
mysql> 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() 函数来发现错误:
```sql
mysql> select row_count();
+-------------+
| row_count() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
- 结果是 -1说明我们可以通过 ROW_COUNT() 函数捕获到错误。
- 对于记录会员储值金额变化的操作,可以通过应用层发出 SQL 语句指令,或者用一个存储过程来实现。无论哪种方式,都要通过好几步相互关联的操作,而且要做
成一个事务,处理过程复杂,消耗的资源也较多。如果用触发器,效率就会提高很多,消耗的资源也少。同时,还可以起到事务的类似功能,保证关联操作全部完成或全部失败。
- 触发器的优缺点
- 通过刚刚的案例,你应该感受到触发器的高效了。现在,咱们把视角拔高一下,来看看触发器具体都有什么优缺点。毕竟,知己知彼,才能百战不殆。只有非常清楚它的优缺点,
你才能充分发挥它的作用。
- 我先来说说触发器的优点。
- **首先,触发器可以确保数据的完整性**。
- 每当我们录入、删除和修改一条进货单明细数据的时候,进货单明细表里的数据就会发生变动。这个时候,在进货单头表中的总计数量和总计金额就必须重新计算,否则,进货单
头表中的总计数量和总计金额就不等于进货单明细表中数量合计和金额合计了,这就是数据不一致。
- 为了解决这个问题,我们就可以使用触发器,规定每当进货单明细表有数据插入、修改和删除的操作时,自动触发 2 步操作:
- 重新计算进货单明细表中的数量合计和金额合计;
- 用第一步中计算出来的值更新进货单头表中的合计数量与合计金额。
- 这样一来,进货单头表中的合计数量与合计金额的值,就始终与进货单明细表中计算出来的合计数量与合计金额的值相同,数据就是一致的,不会互相矛盾。
- **其次,触发器可以帮助我们记录操作日志**。
- 利用触发器,可以具体记录什么时间发生了什么。我们前面的记录修改会员储值金额的触发器,就是一个很好的例子。这对我们还原操作执行时的具体场景,更好地定位问题原因
很有帮助。
- **另外,触发器还可以用在操作数据前,对数据进行合法性检查。**
- 举个小例子。超市进货的时候,需要库管录入进货价格。但是,人为操作很容易犯错误,比如说在录入数量的时候,把条形码扫进去了;录入金额的时候,看串了行,录入的价格
远超售价,导致账面上的巨亏……这些都可以通过触发器,在实际插入或者更新操作之前,对相应的数据进行检查,及时提示错误,防止错误数据进入系统。
- 下面我来说说触发器的缺点。
- **触发器最大的一个问题就是可读性差**。
- 因为触发器存储在数据库中,并且由事件驱动,这就意味着触发器有可能不受应用层的控制。这对系统维护是非常有挑战的。
- 还是拿我们创建触发器时讲到的修改会员储值操作的那个触发器为例。如果触发器中的操作出了问题,会导致会员储值金额更新失败。我用下面的代码演示一下:
```sql
mysql> update demo.membermaster set memberdeposit=20 where memberid = 2;
ERROR 1054 (42S22): Unknown column 'aa' in 'field list'
```
- 结果显示系统提示错误字段“aa”不存在。
- 这是因为,触发器中的数据插入操作多了一个字段,系统提示错误。可是,如果你不了解这个触发器,很可能会认为是更新语句本身的问题,或者是会员信息表的结构出了问题。
说不定你还会给会员信息表添加一个叫“aa”的字段试图解决这个问题结果只能是白费力。
- 另外,相关数据的变更,特别是数据表结构的变更,都可能会导致触发器出错,进而影响数据操作的正常运行。这些都会由于触发器本身的隐蔽性,影响到应用中错误原因排查的
效率。
- 小建议:**维护一个完整的数据库设计文档**。因为运维人员可能会经常变动,如果有一个完整的数据库设计文档,就可以帮助新人快速了解触发器的设计
思路,从而减少错误,降低系统维护的成本。
### 2.5 权限管理:如何控制数据库访问,消除安全隐患?
- 经常会遇到一种需求,就是要根据用户的不同,对数据进行横向和纵向的分组。
- 所谓横向的分组,就是指用户可以接触到的数据的范围,比如可以看到哪些表的数据;所谓纵向的分组,就是指用户对接触到的数据能访问到什么程度,比如能看、能改,甚至是删除。
- 我们把具有相同数据访问范围和程度的用户归成不同的类别,这种类别就叫角色。通过角色,管理用户对数据库访问的范围和程度就更加方便了。这也就是对用户的数据访问权限的管理。
- 恰当的权限设定,可以确保数据的安全性,这是至关重要的。
- 权限管理: 包括怎么操作角色和用户,怎么通过角色给用户授权,怎么直接给用户授权,从而帮助你管理好用户的权限,提升数据库的安全性。
- 先讲讲角色。我们可以通过角色对相同权限的用户进行分组管理,这样可以使权限管理更加简单高效。
- 角色的作用
- 角色是在 MySQL 8.0 中引入的新功能,相当于一个权限的集合。引入角色的目的是方便管理拥有相同权限的用户。
- 超市项目中有库管、营运和财务等不同的模块它们各自对应不同的数据表。比如库存模块中的盘点表demo.invcount、营运模块中的商品信息表demo.goodsmaster
还有财务模块中的应付账款表demo.settlement
- 盘点表:
- ![权限管理盘点表](pic/权限管理盘点表.png)
- 商品信息表:
- ![权限商品信息表](pic/权限商品信息表.png)
- 应付账款表:
- ![权限应付账款表](pic/权限应付账款表.png)
- 在超市项目中,员工的职责不同,包括库管、营运和财务等,不同的职责有不同的数据访问权限。比如:
- 张三是库管,他就可以查询商品信息表,对盘点表有增删改查的权限,但无权访问应付账款表;
- 李四是营运,他就拥有对商品信息表有增删改查的权限,而对库存表和应付账款表,只有查看的权限;
- 王五是财务,他就有对应付账款表有增删改查的权限,对商品信息表和库存表,只有查看的权限。
- 所以,我们需要为每一个职责创建一个对应的角色,为每个员工创建一个对应的数据库用户。然后通过给角色赋予相关的权限,再把角色赋予用户,实现对超市员工访问数据权限
的管理,从而保证数据的安全性。
- 如何操作角色?
- 如何创建角色?
- MySQL 中的角色名称由角色名称加主机名称组成。创建角色的语法结构如下:
```sql
CREATE ROLE 角色名;
```
- 假设我们现在需要创建一个经理的角色,就可以用下面的代码:
```sql
mysql> CREATE ROLE 'manager'@'localhost';
Query OK, 0 rows affected (0.06 sec)
```
- 这里的意思是创建一个角色角色名称是“manager”角色可以登录的主机是“localhost”意思是只能从数据库服务器运行的这台计算机登录这个账号。你也可以
不写主机名直接创建角色“manager”
```sql
mysql> CREATE ROLE 'manager';
Query OK, 0 rows affected (0.01 sec)
```
- 如果不写主机名MySQL 默认是通配符“%”,意思是这个账号可以从任何一台主机上登录数据库。
- 同样道理,如果我们要创建库管的角色,就可以用下面的代码:
```sql
mysql> CREATE ROLE 'stocker';
Query OK, 0 rows affected (0.02 sec)
```
- **创建角色之后,默认这个角色是没有任何权限的,我们需要给角色授权。**
- 怎么给角色赋予权限?
- 给角色授权的语法结构是:
```sql
GRANT 权限 ON 表名 TO 角色名;
```
- 假设我们现在想给经理角色授予商品信息表、盘点表和应付账款表的只读权限,就可以用下面的代码来实现:
```sql
mysql> GRANT SELECT ON demo.settlement TO 'manager';
Query OK, 0 rows affected (0.03 sec)
mysql> GRANT SELECT ON demo.goodsmaster TO 'manager';
Query OK, 0 rows affected (0.01 sec)
mysql> GRANT SELECT ON demo.invcount TO 'manager';
Query OK, 0 rows affected (0.01 sec)
```
- 如果我们需要赋予库管角色盘点表的增删改查权限、商品信息表的只读权限,对应付账款表没有权限,就可以这样:
```sql
mysql> GRANT SELECT,INSERT,DELETE,UPDATE ON demo.invcount TO 'stocker';
Query OK, 0 rows affected (0.02 sec)
mysql> GRANT SELECT ON demo.goodsmaster TO 'stocker';
Query OK, 0 rows affected (0.02 sec)
```
- 查看角色权限
- 赋予角色权限之后,我们可以通过 SHOW GRANTS 语句,来查看权限是否创建成功了:
```sql
mysql> 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”赋予的权限也就是对商品信息
表、盘点表和应付账款表的只读权限。
- 再来看看库管角色的权限:
```sql
mysql> 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)
```
- 结果显示,库管角色拥有商品信息表的只读权限和盘点表的增删改查权限。
- 当我们需要对业务重新整合的时候,可能就需要对之前创建的角色进行清理,删除一些不会再使用的角色。
- 删除角色
- 删除角色的操作很简单,你只要掌握语法结构就行了。
```sql
DROP ROLE 角色名称;
```
- 如何操作用户?
- 我们可以把数据库中的角色看作是一个权限集。角色本身不能操作数据,这个任务还是要靠用户来完成。在操作用户前,我们先要创建一个用户。
- 创建用户
- 创建用户的语法结构是这样的:
```sql
CREATE USER 用户名 [IDENTIFIED BY 密码];
```
- “[ ]”表示可选,也就是说,可以指定用户登录时需要密码验证,也可以不指定密码验证,这样用户可以直接登录。不过,不指定密码的方式不安全,不推荐使用。
- 举个例子假设我们要给张三创建一个用户用户名是“zhangsan”密码是“mysql”可以通过下面的代码来实现
```sql
mysql> CREATE USER 'zhangsan' IDENTIFIED BY 'mysql';
Query OK, 0 rows affected (0.02 sec)
```
- 这样,张三的用户就创建成功了。
- 给用户授权
- 给用户授权的方式有 2 种,分别是通过把角色赋予用户给用户授权,和直接给用户授权。
- 通过把角色赋予用户给用户授权的语法结构如下:
```sql
GRANT 角色名称 TO 用户名称;
```
- 举个小例子,我们想要给张三赋予库管的角色,可以通过下面的代码实现:
```sql
mysql> GRANT 'stocker' TO 'zhangsan';
Query OK, 0 rows affected (0.01 sec)
```
- 我们也可以直接给用户授权,语法结构如下:
```sql
GRANT 权限 ON 表名 TO 用户名;
```
- 这种方式简单直接,我就不多说了。下面我们来查看一下这个用户的权限有哪些。
- 查看用户权限
- 查看用户权限的语法结构是:
```sql
SHOW GRANTS FOR 用户名;
```
- 我们可以通过下面的代码来查看张三的权限:
```sql
mysql> 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 中创建了角色之后,默认都是没有被激活的,也就是不能用,必须要用下面的语句激活:
```sql
SET global activate_all_roles_on_login=ON;
```
- 这条 SQL 语句的意思是对所有角色永久激活。运行这条语句之后用户“zhangsan”才真正拥有了角色“stocker”的所有权限。
- 下面我们就用张三的账号登录,确认一下他有没有相应的权限:
```sql
H:\>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> select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+------
| itemnumber | barcode | goodsname | specification | unit | salesprice | avgim
+------------+---------+-----------+---------------+------+------------+------
| 1 | 0001 | | 16 | | 89.00 | 31.00 |
| 2 | 0002 | | NULL | | 5.00 | 2.87 |
+------------+---------+-----------+---------------+------+------------+------
2 rows in set (0.02 sec)
```
- 结果显示,我们可以正常登录,并且可以查询商品信息表的内容。
- 删除用户
- 当用户不再使用的时候,我们也可以删除用户。操作起来很简单,你只要知道语法结构就行了:
```sql
DROP USER 用户名;
```
- 总结
- 角色是权限的集合。你可以直接给用户授予访问数据库的权限,也可以通过把角色授予用户,从而把角色对数据库的访问权限全部授予给用户。而用户是数据库的使用者,我们可
以通过给用户授予访问数据库中资源的权限,来控制使用者对数据库的访问,消除安全隐患。
- 需要注意的是,角色在刚刚创建出来的时候,默认是没有激活的,需要手动激活,才可以使用。如果你把角色赋予了用户,那么用户就拥有了角色的全部权限。但是,如果你删除
了角色,那么用户也就失去了通过这个角色所获得的所有权限。
- 有一些程序员喜欢使用 Root 超级用户来访问数据库,完全把权限控制放在应用层面实现。这样当然也是可以的。不过我建议你,尽量使用数据库自己的角色和用户机制
来控制访问权限,不要轻易用 Root 账号。因为 Root 账号密码放在代码里面不安全一旦泄露数据库就会完全失去保护。而且MySQL 的权限控制功能十分完善,应该尽量利
用,可以提高效率,而且安全可靠。
### 2.5 日志(上):系统出现问题,如何及时发现?
- 除了发现错误,日志在数据复制、数据恢复、操作审计,以及确保数据的永久性和一致性等方面,都有着不可替代的作用,对提升你的数据库应用的开发能力至关重要。
- MySQL 的日志种类非常多,包括通用查询日志、慢查询日志、错误日志、二进制日志、中继日志、重做日志和回滚日志
- 通用查询日志
- **通用查询日志记录了所有用户的连接开始时间和截止时间,以及发给 MySQL 数据库服务器的所有 SQL 指令**。当我们的数据发生异常时,开启通用查询日志,还原操作时的具体场
景,可以帮助我们准确定位问题。
- 举个小例子,在超市项目实施的过程中,我们曾遇到过这样一件事:超市经营者月底查账的时候发现,超市的 1 号门店在 12 月 1 日销售了 5 件化妆品,但是当天对应的历史库存
并没有减少。化妆品的金额都比较大,库存不对的话,会在报表查询中产生巨额差异,触发到报警机制,对超市经营者的决策产生影响。超市经营者找到我们,对系统的可靠性提
出质疑。
- 我们对系统进行了仔细检查,没有发现数据问题。可是商品确实卖出去了,当天的历史库存也确实没有消减。这个时候,我们想到了检查通用查询日志,看看当天到底发生了什
么。
- 查看之后我们就复原了当天的情况12 月 1 日下午,门店的收银台销售了 5 件化妆品但是由于网络故障流水没有及时上传到总部。12 月 1 日晚上 11:59总部的历史库存被
保存下来但是因为没有收到门店的流水所以没有消减库存。12 月 2 日上午,门店的网络恢复了,流水得以上传总部,这个时候,对应化妆品的库存才被消减掉。
- 这样,我们就确定了故障的原因,也就是超市的网络问题,而系统本身是没有问题的。
- 下面我来具体介绍一下控制通用查询日志的系统变量。通过这些变量,你会清楚怎么控制通用查询日志的开启和关闭,以及保存日志的文件是哪个。
```sql
mysql> 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 点需要我们注意一下。
- **系统变量 general_log 的值是 OFF表示通用查询日志处于关闭状态**。在 MySQL 中这个参数的默认值是关闭的。因为一旦开启记录通用查询日志MySQL 会记录所有的
连接起止和相关的 SQL 操作,**这样会消耗系统资源并且占用磁盘空间。我们可以通过手动修改变量的值,在需要的时候开启日志**。
- 通用查询日志文件的**名称是 GJTECH-PC.log**。这样我们就知道在哪里可以查看通用查询日志的内容了。
- 下面我们来看看如何开启通用查询日志,把所有连接的起止和连接的 SQL 操作都记录下来。这个操作可以帮助我们追踪 SQL 操作故障的原因。
- 开启通用查询日志
- 我们可以通过设置系统变量的值来开启通用查询日志并且指定通用查询日志的文件夹和文件名为“H:\mytest.log”。这个操作如下
```sql
mysql> SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.00 sec)
mysql> SET @@global.general_log_file = 'H:\mytest.log';
Query OK, 0 rows affected (0.02 sec)
```
- 为了确认我们的设定是否已经生效,我们再来查询一下通用查询日志的状态:
```sql
mysql> 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”这就意味着我们的操作成功了。
- 查看通用查询日志
- 通用查询日志都是文本型数据,可以用记事本打开。下面我们就用记事本打开我电脑上的通用查询日志,实际看一看通用查询日志的内容,包括都有哪些连接,什么时候登录了数
据库,都做了哪些操作等信息。
```composer log
2021-04-05T06:39:53.621980Z 28 Connect zhangsan@localhost on using SSL/TLS -
2021-04-05T06:39:53.622085Z 28 Connect Access denied for user 'zhangsan'@'loca
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 操作,针对的是哪个数据表等信息**。
- 删除通用查询日志
- 当用户对数据库的操作比较频繁时,通用查询日志文件会不断变大。为了节省磁盘空间,我们可以移除旧的日志文件,创建新的日志文件,来对通用查询日志文件进行维护。
- 第一步,关闭通用查询日志:
```sql
mysql> SET GLOBAL general_log = 'OFF'; -- 关闭通用查询日志
Query OK, 0 rows affected (0.01 sec)
mysql> 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 的空间。
- 第三步,开启通用查询日志:
```sql
mysql> SET GLOBAL general_log = 'ON';
Query OK, 0 rows affected (0.01 sec)
mysql> 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%';”。
- 文件内容如下:
```composer log
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe, Version: 8.0.23 (MySQL
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”
- 我们来看看配置文件中关于慢查询日志变量的相关设定:
```sql
slow-query-log=1 -- 表示开启慢查询日志,系统将会对慢查询进行记录。
slow_query_log_file="GJTECH-PC-slow.log" -- 表示慢查询日志的名称是"GJTECH-PC-slow
long_query_time=10 -- 表示慢查询的标准是查询执行时间超过10秒
```
- 除了刚刚的这些变量控制慢查询日志的还有一个系统变量min_examined_row_limit。这个变量的意思是查询扫描过的最少记录数。这个变量和查询执行时间共同组成了判
别一个查询是否是慢查询的条件。如果查询扫描过的记录数大于等于这个变量的值,并且查询执行时间超过 long_query_time 的值,那么,这个查询就被记录到慢查询日志中;反
之,则不被记录到慢查询日志中。
- 如果要查看当前这个系统变量的值,我们就可以用下面的代码:
```sql
mysql> 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 秒的慢查询的发生时间、连接所属的用户、执行的时长、锁表的时长和
扫描过的记录数等相关信息。
```composer log
C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe, Version: 8.0.23 (MySQL
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”中配置它
```sql
# Error Logging.
log-error="GJTECH-PC.err"
```
- 这段代码指定了错误日志的文件名。如果没有指定文件夹默认就是数据目录“C:\ProgramData\MySQL\MySQL Server 8.0\Data”。
- 下面我们查看一下错误日志的内容:
```sql
2021-02-28T08:07:07.228880Z 0 [System] [MY-010116] [Server] C:\Program Files\M
2021-02-28T08:07:07.270982Z 1 [System] [MY-013576] [InnoDB] InnoDB initializat
2021-02-28T08:07:08.116433Z 1 [System] [MY-013577] [InnoDB] InnoDB initializat
```
- 可以看到,错误日志文件中记录了服务器启动的时间,以及存储引擎 InnoDB 启动和停止的时间等。
- 总结
- **通用查询日志:可以记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都
有很大的帮助。**
- **慢查询日志:可以记录运行时间和检查记录数超过指定值的查询,方便我们对查询进行优化。**
- **错误日志:它记录了服务器启动、运行和停止过程中的诊断信息,方便我们了解服务器的状态,从而对服务器进行维护。**
- 我还是想提醒你一句,千万不要小看日志。**很多看似奇怪的问题,答案往往就藏在日志里**。很多情况下,只有通过查看日志才能发现问题的原因,真正解决问题。所以,一
定要学会查看日志,**养成检查日志的习惯**。
### 2.6 日志(下):系统故障,如何恢复数据?
### 2.7 数据备份:异常情况下,如何确保数据安全?
## 3. 优化
### 3.1 范式:如何消除冗余和高效存取?
- 现象,什么样的表需要重新使用范式进行设计
- ![冗余供货表](pic/冗余供货表.png)
- 表里重复的数据非常多:比如第一行和第二行的数据,同样是 3478 号单据,供货商编号、供货商名称和仓库,这 **3 个字段的信息完全相同**。可是这 2 条数据的**后
半部分又不相同**,因此,并不能认为它们是冗余数据而删除。
- 坏味道:
- 在我们的工作场景中,这种由于数据表结构设计不合理,而导致的数据重复的现象并不少见,往往是系统虽然能够运行,承载能力却很差,稍微有点流量,就会出现**内存不足、
CUP 使用率飙升的情况,甚至会导致整个项目失败**。
- **第一范式**
- 这张进货单表重新设计的第一步,就是要把所有的列,也就是字段,都确认一遍,确保**每个字段只包含一种数据**。如果各种数据都混合在一起,就无法通过后面的拆解,把重复的数据去掉。
- 其实,这就是第一范式所要求的:**所有的字段都是基本数据字段,不可进一步拆分**。
- 在我们的这张表里“property”这一字段可以继续拆分。其他字段已经都是基本数据字段不能再拆了。
- 经过优化,我们**把“property”这一字段拆分成“specification规格”和“unit单位”**,这 2 个字段如下:
- ![第一范式拆分供货表](pic/第一范式拆分供货表.png)
- 这样处理之后,字段多了一个,但是每一个字段都成了**不可拆分的最小信息单元**,我们就可以在这个表的基础之上,着手进行进一步的优化了。这就要用到数据表设计的第二范式.
- **第二范式**
- 通过观察,我们可以发现,这个表的前 2 条记录的前 4 个字段完全一样。那可不可以通过拆分,把它们变成一条记录呢?当然是可以的,而且为了优化,必须要进行拆分。
- 第二范式就告诉了我们拆分的原则:**在满足第一范式的基础上,还要满足数据表里的每一条数据记录,都是可唯一标识的。而且所有字段,都必须完全依赖主键,不能只依赖主键的一部分。**
- 根据这个要求,我们可以对表进行重新设计。
- 重新设计的第一步就是要确定这个表的主键。通过观察发现字段“listnumber”+“barcode”可以唯一标识每一条记录可以作为主键。确定好了主键
以后,我们判断一下,哪些字段完全依赖主键,哪些字段只依赖于主键的一部分。同时,把只依赖于主键一部分的字段拆分出去,形成新的数据表。
- 首先进货单明细表里面的“goodsname” “specification” “unit” 这些信息是商品的属性只依赖于“barcode”不完全依赖主键可以拆分出去。我们把这 3 个字段加上
它们所依赖的字段“barcode”拆分形成一个新的数据表“商品信息表”。
- 这样一来,原来的数据表就被拆分成了两个表。
- 商品信息表:
- ![第二范式拆分后的商品信息表](pic/第二范式拆分后的商品信息表.png)
- 进货单表:
- ![第二范式拆分后的进货单表](pic/第二范式拆分后的进货单表.png)
- 同样道理字段“supplierid”“suppliername”“stock”只依赖于“listnumber”不完全依赖于主键所以我们可以把“supplierid”“suppliername”“stock”这 3
个字段拆出去再加上它们依赖的字段“listnumber”就形成了一个新的表“进货单头表”。剩下的字段会组成新的表我们叫它“进货单明细表”。
- 这样一来,原来的数据表就拆分成了 3 个表。
- 进货单头表:
- ![第二范式拆分下的进货单头表](pic/第二范式拆分下的进货单头表.png)
- 进货单明细表:
- ![第二范式拆分下的进货单明细表](pic/第二范式拆分下的进货单明细表.png)
- 商品信息表:
- ![第二范式拆分下的商品信息表](pic/第二范式拆分下的商品信息表.png)
- 我们再来分析一下拆分后的 3 个表,保证这 3 个表都满足第二范式的要求。
- 在“商品信息表”中字段“barcode”是有可能存在重复的比如用户门店可能有散装称重商品和自产商品会存在条码共用的情况。所以所有的字段都不能唯一标识表里
的记录。这个时候我们必须给这个表加上一个主键比如说是自增字段“itemnumber”。
- 现在我们就可以把进货单明细表里面的字段“barcode”都替换成字段“itemnumber”这就得到了新的进货单明细表和商品信息表。
- 进货单明细表(新)
- ![进货单明细表(新)](pic/进货单明细表(新).png)
- 商品信息表(新)
- ![商品信息表(新)](pic/商品信息表(新).png)
- 这样一来,我们拆分后的 3 个数据表中的数据都不存在重复,可以唯一标识。而且,表中的其他字段,都完全依赖于表的主键,不存在部分依赖的情况。所以,拆分后的 3 个数据
表就全部满足了第二范式的要求。
- **第三范式**
- 我们的进货单头表还有数据冗余的可能。因为“suppliername”依赖“supplierid”。那么这个时候就可以按照第三范式的原则进行拆分了。
- 第三范式要求数据表**在满足第二范式的基础上,不能包含那些可以由非主键字段派生出来的字段,或者说,不能存在依赖于非主键字段的字段**。
- 在刚刚的进货单头表中字段“suppliername”依赖于非主键字段“supplierid”。因此这个表不满足第三范式的要求。
- 那接下来,我们就进一步拆分下进货单头表,把它拆解成供货商表和进货单头表。
- 供货商表:
- ![供货商表](pic/供货商表.png)
- 进货单头表:
- ![进货单头表](pic/进货单头表.png)
- 这样一来,供货商表和进货单头表中的所有字段,都完全依赖于主键,不存在任何一个字段依赖于非主键字段的情况了。所以,这 2 个表就都满足第三范式的要求了。
- 但是在进货单明细表中quantity * importprice = importvalue“importprice”“quantity”和“importvalue”这 3 个字段,可以通过
任意两个计算出第三个来,这就存在冗余字段。如果严格按照第三范式的要求,现在我们应该进行进一步优化。优化的办法是删除其中一个字段,只保留另外 2 个,这样就没有冗
余数据了。
- 真的可以这样做吗?要回答这个问题,我们就要先了解下实际工作中的业务优先原则。
- 业务优先的原则
- 所谓的业务优先原则,就是指一切以业务需求为主,技术服务于业务。**完全按照理论的设计不一定就是最优,还要根据实际情况来决定**。这里我们就来分析一下不同选择的利与弊。
- 对于 quantity * importprice = importvalue看起来“importvalue”似乎是冗余字段但并不会导致数据不一致。可是如果我们把这个字段取消是会影响业务的。
- 因为有的时候供货商会经常进行一些促销活动按金额促销那他们拿来的进货单只有金额没有价格。而“importprice”反而是通过“importvalue”÷“quantity”计算出
来的。因此如果不保留“importvalue”字段只有“importprice”和“quantity”的话经过四舍五入会产生较大的误差。这样日积月累最终会导致查询结果出现较大偏
差,影响系统的可靠性。
- 我借助一个例子来说明下为什么会有偏差。
- 假设进货金额是 25.5 元,数量是 34那么进货价格就等于 25.5÷34=0.74 元,但是如果用这个计算出来的进货价格来计算进货金额,那么,进货金额就等于 0.74×34=25.16
元,其中相差了 25.5-25.16=0.34 元。代码如下所示:
- “importvalue”=25.5元“quantity”=34“importprice”=25.5÷34=0.74
- “importprice”=0.74元“quantity”=34“importvalue”=0.74*34=25.16
- 误差 = 25.5 - 25.16 = 0.34
- 现在你知道了在我们这个场景下“importvalue”是必须要保留的。
- 那么换一种思路如果我们保留“quantity”和“importvalue”取消“importprice”这样不是既能节省存储空间又不会影响精确度吗
- 其实不是的。“importprice”是系统的核心指标涉及成本核算。几乎所有的财务、营运和决策支持模块都要涉及到成本问题如果取消“importprice”这个字段那么系统的
运算成本、开发和运维成本,都会大大提高,得不偿失。
- 所以本着业务优先的原则在不影响系统可靠性的前提下可以容忍一定程度的数据冗余保留“importvalue”“importprice”和“quantity"。
- 因此,最后的结果是,我们可以把进货单表拆分成下面的 4 个表:
- 供货商表:
- ![供货商表](pic/供货商表.png)
- 进货单头表:
- ![进货单头表](pic/进货单头表.png)
- 进货单明细表:
- ![进货单明细表(新)](pic/进货单明细表(新).png)
- 商品信息表:
- ![商品信息表(新)](pic/商品信息表(新).png)
- 这样一来,我们就避免了冗余数据,而且还能够满足业务的需求,这样的数据表设计,才是合格的设计。
- 一般来说MySQL 的数据库设计满足第三范式,就足够了。不过,第三范式,并不是终极范式,还有 **BCNF 范式(也叫 BC 范式)、第四范式和第五范式**
### 3.2 ER模型如何理清数据库设计思路
- 接上面对表按范式进行拆解
- 但是,当我们按照这样的方式拆分一连串数据表时,却发现越拆越多,而且支离破碎。事实上,**局部最优的表,不仅有可能存在进一步拆分的情况,还有可能会出现数据缺失**。
- 毕竟,数据库设计是牵一发而动全身的。那有没有什么办法提前看到数据库的全貌呢?
- ER 模型就是一个这样的工具。ER 模型也叫作实体关系模型,是用来描述现实生活中客观存在的事物、事物的属性,以及事物之间关系的一种数据模型。
- 在开发基于数据库的信息系统的**设计阶段**,通常使用 ER 模型**来描述信息需求和信息特性**,帮助我们理清业务逻辑,从而设计出优秀的数据库。
- ER 模型包括哪些要素?
- 在 ER 模型里面,有三个要素,分别是**实体、属性和关系**。
- 实体。在 ER 模型中,用**矩形来表示**。实体分为两类,分别是**强实体和弱实体**。强实体是指不依赖于其他实体的实体;弱实体是指对另一个实体有很强
的依赖关系的实体。
- 属性,则是指**实体的特性**。比如超市的地址、联系电话、员工数等。在 ER 模型中用**椭圆形来表示**。
- 关系,则是指**实体之间的联系**。比如超市把商品卖给顾客,就是一种超市与顾客之间的联系。在 ER 模型中用**菱形来表示**。
- 需要注意的是,有的时候,**实体和属性不容易区分**
- 该如何区分实体和属性呢?
- 一个原则:我们要从系统整体的角度出发去看,**可以独立存在的是实体,不可再分的是属性**。也就是说,属性不需要进一步描述,不能包含其他属性。
- 在 ER 模型的 3 个要素中,关系又可以分为 3 种类型,分别是 **1 对 1、1 对多和多对多**
- 1 对 1**指实体之间的关系是一一对应的**,比如个人与身份证信息之间的关系就是 1 对1 的关系。一个人只能有一个身份证信息,一个身份证信息也只属于一个人。
- 1 对多:**指一边的实体通过关系,可以对应多个另外一边的实体**。相反,另外一边的实体通过这个关系,则只能对应唯一的一边的实体。比如超市与超市里的收款机之间的从
属关系,超市可以拥有多台收款机,但是每一条收款机只能从属于一个超市。
- 多对多:**指关系两边的实体都可以通过关系对应多个对方的实体**。比如在进货模块中,供货商与超市之间的关系就是多对多的关系,一个供货商可以给多个超市供货,一个超
市也可以从多个供货商那里采购商品。
- 超市业务创建 ER 模型
- ![超市业务创建ER模型](pic/超市业务创建ER模型.png)
- 在这个图中,供货商和超市之间的供货关系,两边的数字都不是 1表示多对多的关系。
- 同样,超市和顾客之间的零售关系,也是多对多的关系。
- 这个 ER 模型,包括了 3 个实体之间的 2 种关系:
- 超市从供货商那里采购商品;
- 超市把商品卖给顾客。
- 有了这个 ER 模型,我们就可以从整体上理解超市的业务了。但是,这里没有包含属性,这样就无法体现实体和关系的具体特征。现在,我们需要把属性加上,用椭圆来表示,这样
我们得到的 ER 模型就更加完整了。
- ER 模型的细化
- 进货模块
- 实体及属性
- 供货商:名称、地址、电话、联系人。
- 商品:条码、名称、规格、单位、价格。
- 门店:编号、地址、电话、联系人。
- 仓库:编号、名称。
- 员工:工号、姓名、住址、电话、身份证号、职位。
- 实体关系
- 其中,供货商、商品和门店是强实体,因为它们不需要依赖其他任何实体。
- 而仓库和员工是弱实体,因为它们虽然都可以独立存在,但是它们都依赖门店这个实体,因此都是弱实体。
- ER 模型如下:
- ![ER模型](pic/ER模型.png)
- 这里我是用粗框矩形表示弱实体,用粗框菱形,表示弱实体与它依赖的强实体之间的关系。
- 零售模块
- 零售业务包括普通零售和会员零售两种模式。普通零售包含的实体,包括门店、商品和收银款台;会员零售包含的实体,包括门店、商品、会员和收银款台。
- 实体及属性
- 商品:条码、名称、规格、单位、价格。
- 会员:卡号、发卡门店、名称、电话、身份证、地址、积分、储值。
- 门店:编号、地址、电话、联系人。
- 收银款台:编号、名称。
- 实体关系
- 其中,商品和门店不依赖于任何其他实体,所以是强实体;
- 会员和收银款台都依赖于门店,所以是弱实体。
- 零售模块的 ER 模型了:
- ![零售ER模型](pic/零售ER模型.png)
- 完整的 ER 模型:
- ![完整ER模型](pic/完整ER模型.png)
- 如何把 ER 模型图转换成数据表?
- 通过绘制 ER 模型,我们已经理清了业务逻辑,现在,我们就要进行非常重要的一步了:把绘制好的 ER 模型,转换成具体的数据表。
- 我来介绍下转换的原则。
- 一个实体通常转换成一个数据表;
- 一个多对多的关系,通常也转换成一个数据表;
- 一个 1 对 1或者 1 对多的关系,往往通过表的外键来表达,而不是设计一个新的数据表;
- 属性转换成表的字段。
### 3.3 查询有点慢,语句该如何写?
- 查询分析语句
- 虽然 MySQL 的查询分析语句并不能直接优化查询,但是却可以帮助你了解 SQL 语句的执行计划,有助于你分析查询效率低下的原因,进而有针对性地进行优化。查询分析语句的
语法结构是:
- { EXPLAIN | DESCRIBE | DESC }查询语句;
```shell
mysql> SELECT itemnumber,quantity,price,transdate
-> FROM demo.trans
-> WHERE itemnumber=1
-> AND transdate>'2020-06-18 09:00:00'
-> AND transdate<'2020-06-18 12:00:00';
+------------+----------+-------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+-------+---------------------+
| 1 | 0.276 | 70.00 | 2020-06-18 11:04:00 |
| 1 | 1.404 | 70.00 | 2020-06-18 11:10:57 |
| 1 | 0.554 | 70.00 | 2020-06-18 11:18:12 |
| 1 | 0.431 | 70.00 | 2020-06-18 11:27:39 |
| 1 | 0.446 | 70.00 | 2020-06-18 11:42:08 |
| 1 | 0.510 | 70.00 | 2020-06-18 11:56:43 |
+------------+----------+-------+---------------------+
6 rows in set (6.54 sec)
```
- 结果显示,有 6 条记录符合条件。这个简单的查询一共花去了 6.54 秒,这个速度显然太慢了。
- 现在,我们用下面的语句分析一下这个查询的具体细节:
```shell
mysql> EXPLAIN SELECT itemnumber,quantity,price,transdate -- 分析查询执行情况
-> FROM demo.trans
-> WHERE itemnumber=1 -- 通过商品编号筛选
-> AND transdate>'2020-06-18 09:00:00' -- 通过交易时间筛选
-> AND transdate<'2020-06-18 12:00:00';
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key |key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ALL | NULL | NULL | NULL | NULL | 4157166 | 1.11 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
```
- id是一个查询序列号。
- table表示与查询结果相关的表的名称。
- partition表示查询访问的分区。
- key表示优化器最终决定使用的索引是什么。
- key_len表示优化器选择的索引字段按字节计算的长度。如果没有使用索引这个值就是空。
- ref表示哪个字段或者常量被用来与索引字段比对以读取表中的记录。如果这个值是“func”就表示用函数的值与索引字段进行比对。
- rows表示为了得到查询结果必须扫描多少行记录。
- filtered表示查询筛选出的记录占全部表记录数的百分比。
- possible_key表示 MySQL 可以通过哪些索引找到查询的结果记录。如果这里的值是空,就说明没有合适的索引可用。你可以通过查看 WHERE 条件语句中使用的字段,来
决定是否可以通过创建索引提高查询的效率
- Extra表示 MySQL 执行查询中的附加信息。你可以点击这个链接查询详细信息。
- type表示表是如何连接的。至于具体的内容你可以参考下查询分析语句输出内容说明。
- 除了刚刚这些字段,还有 1 个比较重要,那就是 select_type。
- SIMPLE表示简单查询不包含子查询和联合查询。
- PRIMARY表示是最外层的查询。
- UNION表示联合查询中的第二个或者之后的查询。
- DEPENDENTUNION表示联合查询中的第二个或者之后的查询而且这个查询受外查询的影响。
- 关于这个 DEPENDENTUNION 取值,
```shell
mysql> SELECT *
-> FROM demo.goodsmaster a
-> WHERE itemnumber in
-> (
-> SELECTb.itemnumber
-> FROM demo.goodsmaster b
-> WHERE b.goodsname = '书'
-> UNION
-> SELECTc.itemnumber
-> FROM demo.goodsmaster c
-> WHERE c.goodsname = '笔'
-> );
...
2 rows in set (0.00 sec)
```
- MySQL 在执行的时候,会把这个语句进行优化,重新写成下面的语句:
```sql
SELECT *
FROM demo.goodsmaster a
WHERE EXISTS
(
SELECT b.id
FROM demo.goodsmaster b
WHERE b.goodsname = '书' ANDa.itemnumber=b.itemnumber
UNION
SELECT c.id
FROM demo.goodsmaster c
WHERE c.goodsname = '笔' AND a.itemnumber=c.itemnumber
);
```
- 在这里,子查询中的联合查询是:
```sql
SELECT c.id
FROM demo.goodsmaster c
WHERE c.goodsname = '笔' AND a.itemnumber=c.itemnumber
```
- 这个查询就用到了与外部查询相关的条件 a.itemnumber=c.itemnumber因此查询类别就变成了“UNION DEPENDENT”。
- 分析一下刚刚的查询语句。
- 这个查询是一个简单查询,涉及的表是 demo.trans没有分区连接类型是扫描全表没有索引一共要扫描的记录数是 4157166。因此查询速度慢的主要原因是没
有索引,导致必须要对全表进行扫描才能完成查询。所以,针对这个问题,可以通过创建索引的办法,来提高查询的速度。
- 下面,我们用条件语句中的筛选字段 itemnumber 和 transdate 分别创建索引:
```shell
mysql> CREATE INDEX itemnumber_trans ON demo.trans(itemnumber);
Query OK, 0 rows affected (59.86 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> CREATE INDEX transdate_trans ON demo.trans(transdate);
Query OK, 0 rows affected (56.75 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
- 2 种查询优化的方法
- 怎么在包含关键字“LIKE”和“OR”的条件语句中利用索引提高查询效率。
- 使用关键字“LIKE”
- “LIKE”经常被用在查询的限定条件中通过通配符“%”来筛选符合条件的记录。比如
- WHERE字段 LIKE aa%表示筛选出所有以“aa”开始的记录
- WHERE字段 LIKE %aa%表示所有字段中包含“aa”的记录。
- 这里你要注意的是,**通配符在前面的筛选条件是不能用索引的**。也就是说WHERE字段LIKE%aa和WHERE字段 LIKE %aa%’都不能使用索引,但是通配符在后面的筛选条
件,就可以使用索引。
- 使用关键字“OR”
- 关键字“OR”表示“或”的关系“WHERE 表达式 1 OR 表达式 2”就表示表达式 1 或者表达式 2 中只要有一个成立,整个 WHERE 条件就是成立的。
- 需要注意的是,**只有当条件语句中只有关键字“OR”并且“OR”前后的表达式中的字段都建有索引的时候查询才能用到索引**。
- 我刚才已经用字段条码给商品流水表创建了一个索引现在我再用商品编号“itemnumber”创建一个索引
```shell
mysql> CREATE INDEX trans_itemnumber ON demo.trans(itemnumber);
Query OK, 0 rows affected (20.24 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
- 我们先看一下关键字“OR”前后的表达式中的字段都创建了索引的情况
```shell
mysql> EXPLAIN SELECT * FROM demo.trans
-> WHERE barcode LIKE '6953150%'
-> OR itemnumber = 1;
+----+-------------+-------+------------+-------------+-----------------------
| id | select_type | table | partitions | type | possible_keys | key | key_len
+----+-------------+-------+------------+-------------+-----------------------
| 1 | SIMPLE | trans | NULL | index_merge | trans_barcode,trans_itemnumber | t
+----+-------------+-------+------------+-------------+-----------------------
1 row in set, 1 warning (0.01 sec)
```
- 我们先看一下关键字“OR”前后的表达式中的字段都创建了索引的情况
```shell
mysql> EXPLAIN SELECT * FROM demo.trans
-> WHERE barcode LIKE '6953150%'
-> OR itemnumber = 1;
+----+-------------+-------+------------+-------------+-----------------------
| id | select_type | table | partitions | type | possible_keys | key | key_len
+----+-------------+-------+------------+-------------+-----------------------
| 1 | SIMPLE | trans | NULL | index_merge | trans_barcode,trans_itemnumber | t
+----+-------------+-------+------------+-------------+-----------------------
1 row in set, 1 warning (0.01 sec)
```
- 说明优化器选择了合并索引的方式。因此这个关键字“OR”前后的表达式中的字段都创建了索引的查询是可以用到索引的。
### 3.4 表太大了,如何设计才能提高性能?