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,327 @@
<audio id="audio" title="01 | 存储:一个完整的数据存储过程是怎样的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/42/72210e06a140aa00b4d20fb60dd45542.mp3"></audio>
你好我是朱晓峰。今天我想跟你聊一聊MySQL是怎么存储数据的。
存储数据是处理数据的第一步。在咱们的超市项目中,每天都要处理大量的商品,比如说进货、卖货、盘点库存,商品的种类很多,而且数量也比较大。只有正确地把数据存储起来,我们才能进行有效的处理和分析,进而对经营情况进行科学的评估,超市负责人在做决策时,就能够拿到数据支持。否则,只能是一团乱麻,没有头绪,也无从着手。
那么,怎样才能把用户各种经营相关的、纷繁复杂的数据,有序和高效地存储起来呢?
在MySQL中一个完整的**数据存储过程总共有4步分别是创建数据库、确认字段、创建数据表、插入数据。**
<img src="https://static001.geekbang.org/resource/image/8b/b1/8b8b594631a175e3016686da88d569b1.jpg" alt="">
接下来我就给你详细讲解一下这个过程的每一步帮你掌握MySQL的数据存储机制。
先提醒你一句,这节课最后有一个视频,我在视频里演示了今天讲到的所有操作。我建议你学完文字以后,跟着视频实操一下。
好了,话不多说,我们现在开始。
## 创建MySQL数据库
**数据存储的第一步,就是创建数据库**
你可能会问,为啥我们要先创建一个数据库,而不是直接创建数据表呢?
这是个很好的问题。其实啊这是因为从系统架构的层次上看MySQL数据库系统从大到小依次是数据库服务器、数据库、数据表、数据表的行与列。
安装程序已经帮我们安装了MySQL数据库服务器所以我们必须从创建数据库开始。
**数据库是MySQL里面最大的存储单元**。数据表、数据表里的数据,以及我们以后会学到的表与表之间的关系,还有在它们的基础上衍生出来的各种工具,都存储在数据库里面。**没有数据库,数据表就没有载体,也就无法存储数据。**
下面我就来给你具体介绍下怎么在我们安装的MySQL服务器里面创建、删除和查看数据库。
### 1.如何创建数据库?
创建数据库我们已经在上节课介绍过了你可以在Workbench的工作区通过下面的SQL语句创建数据库“demo”
```
CREATE DATABASE demo
```
### 2.如何查看数据库?
下面我们来看一下,如何查看数据库。
在Workbench的导航栏我们可以看到数据库服务器里的所有数据库如下图所示
<img src="https://static001.geekbang.org/resource/image/da/9e/da02212629a3084da3ee3d67d982fa9e.png" alt="">
你也可以在Workbench右边的工作区通过查询语句查看所有的数据库
```
mysql&gt; SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| demo |
| information_schema |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)
```
看到这儿你是不是觉得很奇怪为什么Workbench导航栏里面的数据库只有两个我们创建的数据库“demo”和安装完MySQL就有的数据库“sys”
换句话说为什么有的数据库我们可以在Workbench里面看到有的数据库却必须通过查询语句才可以看到呢要弄明白这个问题你必须要知道这些数据库都是干什么的。
- “demo”是我们通过SQL语句创建的数据库是我们用来存储用户数据的也是我们使用的主要数据库。
- “information_schema”是MySQL系统自带的数据库主要保存MySQL数据库服务器的系统信息比如数据库的名称、数据表的名称、字段名称、存取权限、数据文件所在的文件夹和系统使用的文件夹等等。
- “performance_schema”是MySQL系统自带的数据库可以用来监控MySQL的各类性能指标。
- “sys”数据库是MySQL系统自带的数据库主要作用是以一种更容易被理解的方式展示MySQL数据库服务器的各类性能指标帮助系统管理员和开发人员监控MySQL的技术性能。
- “mysql”数据库保存了MySQL数据库服务器运行时需要的系统信息比如数据文件夹、当前使用的字符集、约束检查信息等等。
如果你是DBA或者是MySQL数据库程序员想深入了解MySQL数据库系统的相关信息可以看下[官方文档](https://dev.mysql.com/doc/refman/8.0/en/system-schema.html)。
话说回来为什么Workbench里面我们只能看到“demo”和“sys”这2个数据库呢其实啊这是因为Workbench是图形化的管理工具主要面向开发人员“demo”和“sys”这2个数据库已经够用了。如果有特殊需求比如需要监控MySQL数据库各项性能指标、直接操作MySQL数据库系统文件等可以由DBA通过SQL语句查看其它的系统数据库。
## 确认字段
**数据存储流程的第二步是确认表的字段**
创建好数据库之后我们选择要导入的Excel数据文件MySQL会让我们确认新表中有哪些列以及它们的数据类型。这些列就是MySQL数据表的字段。
MySQL数据表由行与列组成一行就是一条数据记录每一条数据记录都被分成许多列一列就叫一个字段。每个字段都需要定义数据类型这个数据类型叫做字段类型。
<img src="https://static001.geekbang.org/resource/image/d1/b0/d17b6e27ce7e64a7c9e7c9c6938c50b0.png" alt="">
这样一来每一条数据记录的每一个片段就按照字段的定义被严格地管理起来了从而使数据有序而且可靠。MySQL支持多种字段类型字段的定义会影响数据的取值范围、精度以及系统的可靠性下节课我会重点给你讲一讲字段的定义。这里你只要选择系统默认的字段类型就可以了。
## 创建数据表
**数据存储流程的第三步,是创建数据表。**
当我们确认好了表的字段点击下一步Workbench就帮助我们创建了一张表。
MySQL中的数据表是什么呢**你可以把它看成用来存储数据的最主要工具**。数据表对存储在里面的数据进行组织和管理,使数据变得有序,并且能够实现高效查询和处理。
虽然Workbench帮助我们创建了一个表但大多数情况下我们是不会先准备一个Excel文件再通过Workbench的数据导入来创建表的这样太麻烦了。**我们可以通过SQL语句自己来创建表。**
具体咋做呢?我来介绍一下。
首先在Workbench的工作区输入以下SQL语句
```
CREATE TABLE demo.test
(
barcode text,
goodsname text,
price int
);
```
执行这个SQL语句之后就能创建出一个与导入的Excel表一样的MySQL数据表了。
这里有2点需要你格外注意一下。
- **创建表的时候,最好指明数据库**。否则如果你没有选中数据库Workbench会提示错误要是你当前选中的数据库不对还可能把表创建到错误的数据库中。
- **不要在最后一个字段的后面加逗号“,”,这也是初学者容易犯的错误**。
下面我们就来聊一聊,查看数据表的结构、查看数据库中的表的方法。
### 1.如何查看表的结构?
创建好了表,再来看看如何查看表的结构。
我们运行下面的SQL语句
```
DESCRIBE demo.test;
```
运行结果如下:
```
mysql&gt; DESCRIBE demo.test;
+-----------+------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------+------+-----+---------+-------+
| barcode | text | YES | | NULL | |
| goodsname | text | YES | | NULL | |
| price | int | YES | | NULL | |
+-----------+------+------+-----+---------+-------+
3 rows in set (0.00 sec)
```
我来解释下这些信息都代表什么意思。
- Field表示字段名称。
- Type表示字段类型这里barcode、goodsname是文本型的price是整数类型的。
- Null表示这个字段是否允许是空值NULL。这里你一定要注意在MySQL里面空值不等于空字符串。一个空字符串的长度是0而一个空值的长度是空。而且在MySQL里面空值是占用空间的。
- Key我们暂时把它叫做键。
- Default表示默认值。我们导入的表的所有的字段都允许是空默认值都是NULL。
- Extra表示附加信息。
关于字段类型和Key后面我会具体讲解这里你只需要知道它们的含义就可以了。
### 2.如何查看数据库中的表?
创建完成后,怎么查看表有没有真的被创建出来呢?
我们可以通过Workbench的导航栏点击数据库下面的“Tables”找到这个数据库中的所有数据表。另外我们也可以在工作区通过SQL语句查询某个数据库中的数据表。
先用USE语句选择数据库
```
USE demo;
```
运行后进入demo数据库然后用SHOW语句查询这个数据库中所有的表
```
SHOW TABLES;
```
运行后结果如下:
```
mysql&gt; SHOW TABLES;
+----------------+
| Tables_in_demo |
+----------------+
| test |
+----------------+
1 row in set (0.00 sec)
```
这样我们就可以看到数据库“demo”里面只有一个数据表“test”。
### 3.如何设置主键?
讲到了数据表,我就一定要给你讲一讲主键。**因为主键可以确保数据的唯一性,而且能够减少数据错误**。
举个简单的小例子。主键就像是咱们的身份证号码,它是唯一的。每个身份证号码只对应唯一的一个人。同样,每一个人只有唯一的身份证号码。
MySQL中数据表的主键是表中的一个字段或者几个字段的组合。它主要有3个特征
- 必须唯一,不能重复;
- 不能是空;
- 必须可以唯一标识数据表中的记录。
一个MySQL数据表中只能有一个主键。虽然MySQL也允许创建没有主键的表但是**我建议你一定要给表定义主键,并且养成习惯。因为主键可以帮助你减少错误数据,并且提高查询的速度**(后面我会专门用一节课的时间介绍“怎么用好主键”)。
我来举个例子,假设我们有这样一张表:
<img src="https://static001.geekbang.org/resource/image/ca/0e/ca3b0c3f2e7b1de8065a5efb427bc70e.jpg" alt="">
我们给它起个名字叫“goodsmaster”意思是商品表。说到这儿你可能注意到了我的表名、字段名都用的是英文。其实MySQL也允许数据表名称、字段名称用中文但我还是**建议你用英文原因有2个一是书写方便二是不容易出错**。如果用汉字,涉及到编码问题,就会容易出现错误。
那么,在这个表里,哪个字段是主键呢?
商品名称“goodsname”行不行呢不行原因是重名的商品太多了比如“笔”大家都可以生产一种叫“笔”的商品各种各样的笔不同规格的比如长的、短的不同材料的比如墨水的、铅芯的……由于可能重复商品名称和数据记录之间不能形成一一对应的关系所以“goodsname”不能作为主键。同样价格“price”重复的可能性很大也不能做主键。
商品条码“barcode”能不能成为这个数据表的主键呢
好像可以。商品的条码都是由中国物品编码中心统一编制的,一种商品对应一个条码,一个条码对应一种商品。这不就是一一对应的关系吗?
在实际操作中,有例外的情况。比较典型的就是用户的门店里面有很多自己生产或者加工的商品。比如,馒头、面条等自产食品,散装的糕点、糖果等称重商品,等等。为了管理方便,门店往往会自己给它们设置条码。这样,很容易产生重复、重用的现象。
<img src="https://static001.geekbang.org/resource/image/06/38/061b762a72bb2999ac3ae3da54e54638.jpg" alt="">
这么说商品条码“barcode”也有重复的可能也不能用做主键。
那么,如果数据表中所有的字段都有重复的可能,我们怎么设置主键呢?答案是**我们可以自己添加一个不会重复的字段来做主键**。
比如在上面的例子中我们就可以添加一个字段字段类型是整数我们给它取个名字叫商品编号“itemnumber”。而且我们可以每次增加一条新数据的时候让这个字段的值自动加1这样就永远不会重复了如下表所示
<img src="https://static001.geekbang.org/resource/image/84/4c/849157b071fcc6d8c7d28c9a1aaf1b4c.jpg" alt="">
我们添加字段商品编号“itemnumber”为主键这样我们的商品表“goodsmaster”就有了主键。
在Workbench中我们可以通过修改表结构来增加一个主键字段
<img src="https://static001.geekbang.org/resource/image/fe/a7/febd6a85383f2b7de02b59eb4fe3f6a7.png" alt="">
你也可以通过一条SQL语句修改表的结构来增加一个主键字段
```
ALTER TABLE demo.test
ADD COLUMN itemnumber int PRIMARY KEY AUTO_INCREMENT;
```
我简单解释下这些关键字的含义。
- ALTER TABLE表示修改表
- ADD COLUMN表示增加一列
- PRIMARY KEY表示这一列是主键
- AUTO_INCREMENT表示每增加一条记录这个值自动增加。一会儿讲到添加数据的时候我还会详细介绍一下它。
## 插入数据
**数据存储流程的第四步,也是最后一步,是把数据插入到表当中去。**
Workbench的数据导入功能可以帮助我们把Excel的数据导入到表里面那么我们自己怎么向数据表中插入一条数据呢我们可以借助SQL语句。
```
INSERT INTO demo.test
(barcode,goodsname,price)
VALUES ('0001','本',3);
```
这里的INSERT INTO表示向demo.test中插入数据后面是要插入数据的字段名VALUES表示对应的值。
在添加数据的时候有2点需要你格外注意一下。
1. 要插入数据的字段名也可以不写,但是我建议你不要怕麻烦,**一定要每次都写**。这样做的好处是可读性好,不易出错,而且容易修改。否则,如果你记不住表的字段,就只能去查表的结构,才能知道值所对应的字段了。
1. 由于字段itemnumber定义了AUTO_INCREMENT所以我们插入一条记录的时候不给它赋值系统也会自动给它赋值。而且每次赋值都会在上次的赋值基础上自动增加1。你也可以在插入一条记录的时候给itemnumber 赋值由于它是主键新的值必须与已有记录的itemnumber值不同否则系统会提示错误。
最后,我还专门录制了一段视频,把今天讲到的实际操作演示了一遍,你可以跟着视频再演练下。
<video poster="https://media001.geekbang.org/4b8954c165a84537b7cdbd5f54d23755/snapshots/322a5fe009b34275989e4f0682bebd58-00005.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/7e27d07d27d407ebcc195a0e78395f55/4d6e2c6f-17810d406a4-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src=" https://media001.geekbang.org/f3b7a5474307482d8387645e348dccf9/4342ca9da931447eae4723d1901e9913-d1b210472a78fe4f269073cc7af8891a-sd.m3u8" type="application/x-mpegURL"></video>
## 总结
今天,我们学习了数据存储的完整过程,包括创建数据库、创建数据表、确认字段和插入数据。建议你跟着文字和视频实际操作一下,熟练掌握存储数据的方法。
在进行具体操作的时候我们会用到8种SQL语句我再给你汇总下。
```
-- 创建数据库
CREATE DATABASE demo
-- 删除数据库
DROP DATABASE demo
-- 查看数据库
SHOW DATABASES;
-- 创建数据表:
CREATE TABLE demo.test
(
barcode text,
goodsname text,
price int
);
-- 查看表结构
DESCRIBE demo.test;
-- 查看所有表
SHOW TABLES;
-- 添加主键
ALTER TABLE demo.test
ADD COLUMN itemnumber int PRIMARY KEY AUTO_INCREMENT;
-- 向表中添加数据
INSERT INTO demo.test
(barcode,goodsname,price)
VALUES ('0001','本',3);
```
最后我还想再给你讲一讲MySQL中SQL语句的书写规范。
MySQL以分号来识别一条SQL语句结束所以**你写的每一条SQL语句的最后都必须有一个分号否则MySQL会认为这条语句没有完成提示语法错误**。
所以我建议你写在SQL语句时遵循统一的样式以增加可读性减少错误。如果你不是很清楚具体的规范可以点击这个[链接](https://www.sqlstyle.guide/zh/)学习下。
## 思考题
我想请你思考一下,假设用户现在要销售商品,你能不能帮它设计一个销售表,把销售信息(商品名称、价格、数量、金额等)都保存起来?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,295 @@
<audio id="audio" title="02 | 字段:这么多字段类型,该怎么定义?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/67/7c/673b3e489c777yy4b193b61fedeb727c.mp3"></audio>
你好,我是朱晓峰。
MySQL中有很多字段类型比如整数、文本、浮点数等等。如果类型定义合理就能节省存储空间提升数据查询和处理的速度相反如果数据类型定义不合理就有可能会导致数据超出取值范围引发系统报错甚至可能会出现计算错误的情况进而影响到整个系统。
之前我们就遇到过这样一个问题在销售流水表中需要定义商品销售的数量。由于有称重商品不能用整数我们想当然地用了浮点数为了确保精度我们还用了DOUBLE类型。结果却造成了在没有找零的情况下客人无法结账的重大错误。经过排查我们才发现原来DOUBLE类型是不精准的不能使用。
你看,准确地定义字段类型,不但关系到数据存储的效率,而且会影响整个信息系统的可靠性。所以,我们必须要掌握不同字段的类型,包括它们的适用场景、定义方法,这节课,我们就聊一聊这个问题。
首先我要说的是MySQL中最简单的数据类型整数类型。
## 整数类型
整数类型一共有5种包括TINYINT、SMALLINT、MEDIUMINT、INTINTEGER和BIGINT它们的区别如下表所示
<img src="https://static001.geekbang.org/resource/image/dd/68/dd11099e29ae339f605a222b5b194368.jpg" alt="">
这么多整数类型,咱们该怎么选择呢?
其实,在评估用哪种整数类型的时候,你**需要考虑存储空间和可靠性的平衡问题**:一方面,用占用字节数少的整数类型可以节省存储空间;另一方面,要是为了节省存储空间,使用的整数类型取值范围太小,一旦遇到超出取值范围的情况,就可能引起系统错误,影响可靠性。
举个例子在我们的项目中商品编号采用的数据类型是INT。
我们之所以没有采用占用字节更少的SMALLINT类型整数原因就在于客户门店中流通的商品种类较多而且每天都有旧商品下架新商品上架这样不断迭代日积月累。如果使用SMALLINT类型虽然占用字节数比INT类型的整数少但是却不能保证数据不会超出范围65535。相反使用INT就能确保有足够大的取值范围不用担心数据超出范围影响可靠性的问题。
你要注意的是,在实际工作中,系统故障产生的成本远远超过增加几个字段存储空间所产生的成本。因此,我建议你首先**确保数据不会超过取值范围**,在这个前提之下,再去考虑如何节省存储空间。
接下来,我再给你介绍下浮点数类型和定点数类型。
## 浮点数类型和定点数类型
浮点数和定点数类型的特点是可以处理小数,你可以把整数看成小数的一个特例。因此,浮点数和定点数的使用场景,就比整数大多了。
我们先来了解下MySQL支持的浮点数类型分别是FLOAT、DOUBLE、REAL。
- FLOAT表示单精度浮点数
- DOUBLE表示双精度浮点数
- REAL默认就是DOUBLE。如果你把SQL模式设定为启用“REAL_AS_FLOAT”那么MySQL就认为REAL是FLOAT。如果要启用“REAL_AS_FLOAT”就可以通过以下SQL语句实现
```
SET sql_mode = “REAL_AS_FLOAT”;
```
FLOAT和DOUBLE这两种数据类型的区别是啥呢其实就是FLOAT占用字节数少取值范围小DOUBLE占用字节数多取值范围也大。
<img src="https://static001.geekbang.org/resource/image/13/64/13d20b6f3a8a8d7ed4274d9b7a251c64.jpg" alt="">
看到这儿,你有没有发现一个问题:为什么浮点数类型的无符号数取值范围,只相当于有符号数取值范围的一半,也就是只相当于有符号数取值范围大于等于零的部分呢?
其实这里的原因是MySQL是按照这个格式存储浮点数的符号S、尾数M和阶码E。因此无论有没有符号MySQL的浮点数都会存储表示符号的部分。因此所谓的无符号数取值范围其实就是有符号数取值范围大于等于零的部分。
不过,我要提醒你的是,**浮点数类型有个缺陷,就是不精准**。因此在一些对精确度要求较高的项目中千万不要使用浮点数不然会导致结果错误甚至是造成不可挽回的损失。下面我来重点解释一下为什么MySQL的浮点数不够精准。
为了方便你理解,我来借助一个实际的例子演示下。
我们先创建一个表,如下所示:
```
CREATE TABLE demo.goodsmaster
(
barcode TEXT,
goodsname TEXT,
price DOUBLE,
itemnumber INT PRIMARY KEY AUTO_INCREMENT
);
```
运行这个语句我们就创建了一个表其中的字段“price”就是浮点数类型。我们再通过下面的SQL语句给这个表插入几条数据
```
-- 第一条
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0001',
'书',
0.47
);
-- 第二条
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0002',
'笔',
0.44
);
-- 第三条
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0002',
'胶水',
0.19
);
```
现在,我们运行一个查询语句,看看现在表里数据的情况:
```
SELECT * from demo.goodsmaster;
```
这个时候,我们可以得到下面的结果:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+---------+-----------+-------+------------+
| barcode | goodsname | price | itemnumber |
+---------+-----------+-------+------------+
| 0001 | 书 | 0.47 | 1 |
| 0002 | 笔 | 0.44 | 2 |
| 0002 | 胶水 | 0.19 | 3 |
+---------+-----------+-------+------------+
3 rows in set (0.00 sec)
```
然后我们用下面的SQL语句把这3个价格加在一起看看得到了什么
```
SELECT SUM(price)
FROM demo.goodsmaster;
```
这里我们又用到一个关键字SUM这是MySQL中的求和函数是MySQL聚合函数的一种你只要知道这个函数表示计算字段值的和就可以了。
我们期待的运行结果是0.47 + 0.44 + 0.19 = 1.1,可是,我们得到的是:
```
mysql&gt; SELECT SUM(price)
-&gt; FROM demo.goodsmaster;
+--------------------+
| SUM(price) |
+--------------------+
| 1.0999999999999999 |
+--------------------+
```
查询结果是1.0999999999999999。看到了吗?虽然误差很小,但确实有误差。
你也可以尝试把数据类型改成FLOAT然后运行求和查询得到的是1.0999999940395355。显然,误差更大了。
虽然1.10和1.0999999999999999好像差不多,但是我们有时候需要以通过数值对比为条件进行查询,一旦出现误差,就查不出需要的结果了:
```
SELECT *
FROM demo.goodsmaster
WHERE SUM(price)=1.1
```
那么,为什么会存在这样的误差呢?问题还是**出在MySQL对浮点类型数据的存储方式上**。
MySQL用4个字节存储FLOAT类型数据用8个字节来存储DOUBLE类型数据。无论哪个都是采用二进制的方式来进行存储的。比如9.625用二进制来表达就是1001.101或者表达成1.001101×2^3。看到了吗如果尾数不是0或5比如9.624),你就无法用一个二进制数来精确表达。怎么办呢?就只好在取值允许的范围内进行近似(四舍五入)。
现在你一定明白了为什么数据类型是DOUBLE的时候我们得到的结果误差更小一些而数据类型是FLOAT的时候误差会更大一下。原因就是DOUBLE有8位字节精度更高。
说到这里,我想你已经彻底理解了浮点数据类型不精准的原因了。
那么MySQL有没有精准的数据类型呢当然有这就是**定点数类型DECIMAL**。
就像浮点数类型的存储方式决定了它不可能精准一样DECIMAL的存储方式决定了它一定是精准的。
浮点数类型是把十进制数转换成二进制数存储DECIMAL则不同它是把十进制数的整数部分和小数部分拆开分别转换成十六进制数进行存储。这样所有的数值就都可以精准表达了不会存在因为无法表达而损失精度的问题。
MySQL用DECIMALM,D的方式表示高精度小数。其中M表示整数部分加小数部分一共有多少位M&lt;=65。D表示小数部分位数D&lt;M。
下面我们就用刚才的表demo.goodsmaster验证一下。
首先我们运行下面的语句把字段“price”的数据类型修改为DECIMAL(5,2)
```
ALTER TABLE demo.goodsmaster
MODIFY COLUMN price DECIMAL(5,2);
```
然后,我们再一次运行求和语句:
```
SELECT SUM(price)
FROM demo.goodsmaster;
```
这次我们得到了完美的结果1.10。
由于DECIMAL数据类型的精准性在我们的项目中除了极少数比如商品编号用到整数类型外其他的数值都用的是DECIMAL原因就是这个项目所处的零售行业要求精准一分钱也不能差。
当然,在一些对精度要求不高的场景下,比起占用同样的字节长度的定点数,浮点数表达的数值范围可以更大一些。
简单小结下浮点数和定点数的特点:浮点类型取值范围大,但是不精准,适用于需要取值范围大,又可以容忍微小误差的科学计算场景(比如计算化学、分子建模、流体动力学等);定点数类型取值范围相对小,但是精准,没有误差,适合于对精度要求极高的场景(比如涉及金额计算的场景)。
## 文本类型
在实际的项目中我们还经常遇到一种数据就是字符串数据。比如刚刚那个简单的表demo.goodsmaster中有两个字段“barcode”和“goodsname”。它们当中存储的条码、商品名称都是字符串数据。这两个字段的数据类型我们都选择了TEXT类型。
TEXT类型是MySQL支持的文本类型的一种。此外MySQL还支持CHAR、VARCHAR、ENUM和SET等文本类型。我们来看看它们的区别。
- CHAR(M)固定长度字符串。CHAR(M)类型必须预先定义字符串长度。如果太短,数据可能会超出范围;如果太长,又浪费存储空间。
- VARCHAR(M) 可变长度字符串。VARCHAR(M)也需要预先知道字符串的最大长度,不过只要不超过这个最大长度,具体存储的时候,是按照实际字符串长度存储的。
- TEXT字符串。系统自动按照实际长度存储不需要预先定义长度。
- ENUM 枚举类型,取值必须是预先设定的一组字符串值范围之内的一个,必须要知道字符串所有可能的取值。
- SET是一个字符串对象取值必须是在预先设定的字符串值范围之内的0个或多个也必须知道字符串所有可能的取值。
对于ENUM类型和SET类型来说你必须知道所有可能的取值所以只能用在某些特定场合比如某个参数设定的取值范围只有几个固定值的场景。
因为不需要预先知道字符串的长度系统会按照实际的数据长度进行存储所以TEXT类型最为灵活方便所以下面我们重点学习一下它。
TEXT类型也有4种它们的区别就是最大长度不同。
- TINYTEXT255字符这里假设字符是ASCII码一个字符占用一个字节下同
- TEXT 65535字符。
- MEDIUMTEXT16777215字符。
- LONGTEXT 4294967295字符相当于4GB
不过需要注意的是TEXT也有一个问题**由于实际存储的长度不确定MySQL不允许TEXT类型的字段做主键。遇到这种情况你只能采用CHAR(M)或者VARCHAR(M)。**
所以我建议你在你的项目中只要不是主键字段就可以按照数据可能的最大长度选择这几种TEXT类型中的的一种作为存储字符串的数据类型。
## 日期与时间类型
除了刚刚说的这3种类型还有一类也是经常用到的那就是日期与时间类型。
日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。原因是客户需要知道数据的时间标签,从而进行数据查询、统计和处理。
**用得最多的日期时间类型就是DATETIME**。虽然MySQL也支持YEAR、TIME时间、DATE日期以及TIMESTAMP类型但是**我建议你在实际项目中尽量用DATETIME类型**。因为这个数据类型包括了完整的日期和时间信息使用起来比较方便。毕竟如果日期时间信息分散在好几个字段就会很不容易记而且查询的时候SQL语句也会更加复杂。
这里我也给你列出了MySQL支持的其他日期时间类型的一些参数
<img src="https://static001.geekbang.org/resource/image/5d/d5/5dde7b30c14147bd88eacff77e5892d5.jpg" alt="">
可以看到,不同数据类型表示的时间内容不同、取值范围不同,而且占用的字节数也不一样,你要根据实际需要灵活选取。
不过我也给你一条小建议为了确保数据的完整性和系统的稳定性优先考虑使用DATETIME类型。因为虽然DATETIME类型占用的存储空间最多但是它表达的时间最为完整取值范围也最大。
另外这里还有个问题为什么时间类型TIME的取值范围不是-23:59:5923:59:59呢原因是MySQL设计的TIME类型不光表示一天之内的时间而且可以用来表示一个时间间隔这个时间间隔可以超过24小时。
时间类型的应用场景还是比较广的后面我会单独用一节课来讲在数据库中处理时间的问题。这节课你一定要知道MySQL支持哪几种时间类型它们的区别是什么这样在学后面的内容时才能游刃有余。
## 总结
今天,我给你介绍了几个常用的字段数据类型,包括整数类型、浮点数类型、定点数类型、文本类型和日期时间类型。同时,我们还清楚了为什么整数类型用得少,浮点数为什么不精准,以及常用的日期时间类型。
另外我们还学习了几个新的SQL语句。尤其是第2条我们在项目中会经常用到你一定要重点牢记。
```
-- 修改字段类型语句
ALTER TABLE demo.goodsmaster
MODIFY COLUMN price DOUBLE;
-- 计算字段合计函数:
SELECT SUM(price)
FROM demo.goodsmaster;
```
最后我还想再给你分享1个小技巧。在定义数据类型时如果确定是整数就用INT如果是小数一定用定点数类型DECIMAL如果是字符串只要不是主键就用TEXT如果是日期与时间就用DATETIME。
- 整数INT。
- 小数DECIMAL。
- 字符串TEXT。
- 日期与时间DATETIME。
这样做的好处是,首先确保你的系统不会因为数据类型定义出错。
不过凡事都是有两面的可靠性好并不意味着高效。比如TEXT虽然使用方便但是效率不如CHAR(M)和VARCHAR(M)。如果你有进一步优化的需求,我再给你分享一份[文档](https://dev.mysql.com/doc/refman/8.0/en/data-types.html),你可以对照着看下。
## 思考题
假设用户需要一个表来记录会员信息,会员信息包括会员卡编号、会员名称、会员电话、积分值。如果要你为这些字段定义数据类型,你会如何选择呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,491 @@
<audio id="audio" title="03 | 表:怎么创建和修改表?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/4f/7d74a01588b6a686fa489e7b7d1c144f.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊怎么创建和修改数据表。
创建和修改数据表,是数据存储过程中的重要一环。我们不仅需要把表创建出来,还需要正确地设置限定条件,这样才能确保数据的一致性和完整性。同时,表中的数据会随着业务需求的变化而变化,添加和修改相应的字段也是常见的操作。这节课,我们就来学习下具体的方法。
在我们的超市项目里客户经常需要进货这就需要在MySQL数据库里面创建一个表来管理进货相关的数据。我们先看看这个表里有什么内容。
假设这个表叫做进货单头表importhead如下图所示
<img src="https://static001.geekbang.org/resource/image/83/ff/83c593cb7679ed5f99c29d937712c8ff.jpg" alt="">
这里的1、2、3表示门店的3种进货方式分别是配送中心配送、门店采买和供货商直供。
其中“1配送中心配送”是标准进货方式。因为超市是连锁经营为了确保商品质量和品类一致超过9成的门店进货是通过配送中心进行配送的。因此我们希望这个字段的值能够默认是1这样一来除非有特别的指定否则门店进货单的进货方式就自动设置成“1”了。
现在客户需要一个类似的表来存储进货数据而且进货方式还有3个可能的取值范围需要设置默认值那么应该怎么创建这个表呢另外创建好表以后又该怎么进行修改呢
## 如何创建数据表?
首先我们要知道MySQL创建表的语法结构
```
CREATE TABLE &lt;表名&gt;
{
字段名1 数据类型 [字段级别约束] [默认值]
字段名2 数据类型 [字段级别约束] [默认值]
......
[表级别约束]
};
```
在这里,我们通过定义表名、表中的字段、表的属性等,把一张表创建出来。
你可能注意到了在MySQL创建表的语法结构里面有一个词叫做“约束”。**“约束”限定了表中数据应该满足的条件**。MySQL会根据这些限定条件对表的操作进行监控阻止破坏约束条件的操作执行并提示错误从而确保表中数据的唯一性、合法性和完整性。这是创建表时不可缺少的一部分。
下面我来带你创建刚刚提到的进货单表。需要注意的是,这里我们需要定义默认值,也就是要定义默认值约束,除此之外,还有很多种约束,一会儿我再细讲。
我们先来看基本的数据表创建流程,创建代码如下:
```
CREATE TABLE demo.importhead
(
listnumber INT,
supplierid INT,
stocknumber INT,
--我们在字段importype定义为INT类型的后面按照MySQL创建表的语法加了默认值1。
importtype INT DEFAULT 1,
quantity DECIMAL(10,3),
importvalue DECIMAL(10,2),
recorder INT,
recordingdate DATETIME
);
```
运行这个SQL语句表demo.importhead就按照我们的要求被创建出来了。
在创建表的时候字段名称要避开MySQL的[系统关键字](https://dev.mysql.com/doc/refman/8.0/en/keywords.html#keywords-removed-in-current-series)原因是MySQL系统保留的关键字都有特定的含义如果作为字段名称出现在SQL语句中MySQL会把这个字段名称理解为系统关键字从而导致SQL语句无法正常运行。比如刚刚我们把进货金额设置为“importvalue”而不是“value”就是因为“value”是MySQL的系统关键字。
好了现在我们尝试往刚刚创建的表里插入一条记录来验证一下对字段“importtype”定义的默认值约束是否起了作用。
```
INSERT INTO demo.importhead
(
listnumber,
supplierid,
stocknumber,
-- 这里我们没有插入字段importtype的值
quantity,
importvalue,
recorder,
recordingdate
)
VALUES
(
3456,
1,
1,
10,
100,
1,
'2020-12-10'
);
```
插入完成后,我们来查询一下表的内容:
```
SELECT *
FROM demo.importhead
```
运行结果如下:
```
mysql&gt; select * from demo.importhead;
+------------+------------+-------------+------------+----------+-------------+----------+---------------------+
| listnumber | supplierid | stocknumber | importtype | quantity | importvalue | recorder | recordingdate |
+------------+------------+-------------+------------+----------+-------------+----------+---------------------+
| 1234 | 1 | 1 | 1 | 10.000 | 100.00 | 1 | 2020-12-10 00:00:00 |
| 2345 | 1 | 1 | 2 | 20.000 | 2000.00 | 1 | 2020-12-10 00:00:00 |
| 3456 | 1 | 1 | 1 | 20.000 | 2000.00 | 1 | 2020-12-10 00:00:00 |
+------------+------------+-------------+------------+----------+-------------+----------+---------------------+
3 rows in set (0.00 sec)
```
你会发现字段importtype的值已经是1了。这样通过在创建表的时候设置默认值我们就实现了将字段的默认值定义为1的目的。
到这里,表就被创建出来了。
### 都有哪些约束?
刚刚这种给字段设置默认值的做法,就是默认约束。设置了默认约束,插入数据的时候,如果不明确给字段赋值,那么系统会把设置的默认值自动赋值给字段。
除了默认约束,还有主键约束、外键约束、非空约束、唯一性约束和自增约束。
我们在[上节课](https://time.geekbang.org/column/article/350470)里学的主键,其实就是主键约束,我就不多说了。外键约束涉及表与表之间的关联,以及确保表的数据一致性的问题,内容比较多,后面我在讲“关联表”的时候,再给你具体解释。
现在,我来重点给你介绍一下非空约束、唯一性约束和自增约束。
**1.非空约束**
非空约束表示字段值不能为空,如果创建表的时候,指明某个字段非空,那么添加数据的时候,这个字段必须有值,否则系统就会提示错误。
**2.唯一性约束**
唯一性约束表示这个字段的值不能重复,否则系统会提示错误。跟主键约束相比,唯一性约束要更加弱一些。
在一个表中我们可以指定多个字段满足唯一性约束而主键约束则只能有一个这也是MySQL系统决定的。另外**满足主键约束的字段,自动满足非空约束,但是满足唯一性约束的字段,则可以是空值**。
为了方便你理解我来举个例子。比如我们有个商品信息表goodsmaster如下所示
<img src="https://static001.geekbang.org/resource/image/5f/93/5f73f86c51f7b259fef9d52eaf5da893.jpg" alt="">
barcode代表条码goodsname代表名称。为了防止条码重复我们可以定义字段“barcode”满足唯一性约束。这样一来条码就不能重复但是可以为空而且只能有一条记录条码为空。
同样道理为了防止名称重复我们也可以定义字段“goodsname”满足唯一性约束。但是由于无论名称和条码都可能重用或者可能为空都不适合做主键。因此对这个表来说可以添加一个满足唯一性要求的新字段来做主键。
**3.自增约束**
自增约束可以让MySQL自动给字段赋值且保证不会重复非常有用只是不容易用好。所以我借助一个例子来给你具体讲一讲。
假如我们有这样一个商品信息表:
<img src="https://static001.geekbang.org/resource/image/52/c2/524bfc84ab3555283dd981a14f2a06c2.jpg" alt="">
从这个表中我们可以看到barcode、goodsname和price都不能确保唯一性所以没有任何一个字段可以做主键因此我们可以自己添加一个字段itemnumber并且每次添加一条数据的时候要给值增加1。怎么实现呢我们就可以通过定义自增约束的方式让系统自动帮我们赋值从而满足唯一性这样就可以做主键了。
<img src="https://static001.geekbang.org/resource/image/e0/b1/e004230d1b626e17c3822c678cb1e5b1.jpg" alt="">
这里有2个问题需要你注意一下。
第一在数据表中只有整数类型的字段包括TINYINT、SMALLINT、MEDIUMINT、INT和BIGINT才可以定义自增约束。自增约束的字段每增加一条数据值自动增加1。
第二你可以给自增约束的字段赋值这个时候MySQL会重置自增约束字段的自增基数下次添加数据的时候自动以自增约束字段的最大值加1为新的字段值。
举个例子我们通过Workbench把数据表demo.goodsmaster中的字段itemnumber定义为满足自增约束如下图所示
<img src="https://static001.geekbang.org/resource/image/4b/22/4bb1264e88ee558e51061e0956d13f22.png" alt="">
然后,我们插入一条测试记录:
```
INSERT INTO demo.goodsmaster
(
itemnumber,
barcode,
goodsname,
specification,
unit,
price
)
VALUES
(
-- 指定商品编号为100
100,
'0003',
'测试1',
'',
'个',
10
);
```
运行这条语句之后,查看表的内容,我们得到:
```
mysql&gt; select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+----+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 地图 | NULL | 张 | 9.90 |
| 3 | 0003 | 笔 | 10支 | 包 | 3.00 |
| 100 | 0003 | 测试1 | | 个 | 10.00 |
+------------+---------+-----------+---------------+------+-------+
4 rows in set (0.02 sec)
```
这个时候字段“itemnumber”的值不连续最大值是我们刚刚插入的100。
接着,我们再插入一条数据:
```
INSERT INTO demo.goodsmaster
(
-- 不指定自增字段itemnumber的值
barcode,
goodsname,
specification,
unit,
supplierid,
price
)
VALUES
(
'0004',
'测试2',
'',
'个',
1,
20
);
```
运行这条语句之后,我们再查看表的内容:
```
mysql&gt; select * from demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 地图 | NULL | 张 | 9.90 |
| 3 | 0003 | 笔 | 10支 | 包 | 3.00 |
| 100 | 0003 | 测试1 | | 个 | 10.00 |
| 101 | 0004 | 测试2 | | 个 | 20.00 |
+------------+---------+-----------+---------------+------+-------+
5 rows in set (0.00 sec)
```
可以看到系统自动给自增字段“itemnumber”在最大值的基础之上加了1赋值为101。
好了,到这里,我们就学会了创建表和定义约束的方法。约束要根据业务需要定义在相应的字段上,这样才能保证数据是准确的,你一定要注意它的使用方法。
## 如何修改表?
创建完表以后,我们经常还需要修改表,下面我们就来学习下修改表的方法。
在咱们的超市项目中当我们创建新表的时候会出现这样的情况我们前面创建的进货单表是用来存储进货数据的。但是我们还要创建一个进货单历史表importheadhist用来存储验收过的进货数据。这个表的结构跟进货单表类似只是多了2个字段分别是验收人confirmer和验收时间confirmdate。针对这种情况我们很容易想到可以通过复制表结构然后在这个基础上通过修改表结构来创建新的表。具体怎么实现呢接下来我就给你讲解一下。
首先,我们要把原来的表结构复制一下,代码如下:
```
CREATE TABLE demo.importheadhist
LIKE demo.importhead;
```
运行这个语句之后一个跟demo.importhead有相同表结构的空表demo.importheadhist就被创建出来了。
这个新创建出来的表,还不是我们需要的表,我们需要对这个表进行修改,通过添加字段和修改字段,来获得我们需要的“进货单历史表”。
### 添加字段
你可能会想到我们可以通过Workbench用可视化操作来修改表的结构如下图所示
<img src="https://static001.geekbang.org/resource/image/53/3f/537d01d3c33f8c9f804056e7795d1e3f.png" alt="">
这样当然没问题,但是我想给你讲一个更方便灵活的方式:**用SQL语句来修改表的结构**。
现在我要给这个新的表增加2个字段confirmer和confirmdate就可以用下面的代码
```
mysql&gt; ALTER TABLE demo.importheadhist
-&gt; ADD confirmer INT; -- 添加一个字段confirmer类型INT
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; ALTER TABLE demo.importheadhist
-&gt; ADD confirmdate DATETIME; -- 添加一个字段confirmdate类型是DATETIME
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
运行这个SQL语句查看表的结构你会发现在字段的最后多了两个字段
- “confirmer”数据类型是INT
- “confirmdate"类型是DATETIME。
我们来查看一下表结构:
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| quantity | decimal(10,3) | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.02 sec)
```
这样通过简单增加2个字段我们就获得了进货单历史表。
### 修改字段
除了添加字段我们可能还要修改字段比如我们要把字段名称“quantity”改成“importquantity”并且把字段类型改为DOUBLE该怎么操作呢
我们可以通过修改表结构语句ALTER TABLE来修改字段
```
mysql&gt; ALTER TABLE demo.importheadhist
-&gt; CHANGE quantity importquantity DOUBLE;
Query OK, 0 rows affected (0.15 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
运行这个SQL语句查看表的结构我们会得到下面的结果
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| importquantity | double | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.02 sec)
```
可以看到,字段名称和字段类型全部都改过来了。
如果你不想改变字段名称只想改变字段类型例如把字段“importquantity”类型改成DECIMAL(10,3),你可以这样写:
```
ALTER TABLE demo.importheadhist
MODIFY importquantity DECIMAL(10,3);
```
运行SQL语句查看表结构你会发现已经改过来了。
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| importquantity | decimal(10,3) | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.02 sec)
```
我们还可以通过SQL语句**向表中添加一个字段**,我们甚至可以指定添加字段在表中的位置。
比如说在字段supplierid之后添加一个字段suppliername数据类型是TEXT。我们可以这样写SQL语句
```
ALTER TABLE demo.importheadhist
ADD suppliername TEXT AFTER supplierid;
```
运行SQL语句查看表结构
```
mysql&gt; DESCRIBE demo.importheadhist;
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| listnumber | int | NO | PRI | NULL | |
| supplierid | int | NO | | NULL | |
| suppliername | text | YES | | NULL | |
| stocknumber | int | NO | | NULL | |
| importtype | int | YES | | 1 | |
| importquantity | decimal(10,3) | YES | | NULL | |
| importvalue | decimal(10,2) | YES | | NULL | |
| recorder | int | YES | | NULL | |
| recordingdate | datetime | YES | | NULL | |
| confirmer | int | YES | | NULL | |
| confirmdate | datetime | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
11 rows in set (0.02 sec)
```
到这里,我们就完成了修改字段在表中位置的操作。
## 总结
这节课,我们学习了创建和修改数据表的具体方法。在讲创建表时,我讲到了一个重要的概念,就是约束,包括默认约束、非空约束、唯一性约束和自增约束等。
- 默认值约束:就是给字段设置一个默认值。
- 非空约束:就是声明字段不能为空值。
- 唯一性约束:就是声明字段不能重复。
- 自增约束就是声明字段值能够自动加1且不会重复。
在修改表时,我们可以通过修改已经存在的表创建新表,也可以通过添加字段、修改字段的方式来修改数据表。
最后我给你汇总了一些常用的创建表的SQL语句你一定要牢记。
```
CREATE TABLE
(
字段名 字段类型 PRIMARY KEY
);
CREATE TABLE
(
字段名 字段类型 NOT NULL
);
CREATE TABLE
(
字段名 字段类型 UNIQUE
);
CREATE TABLE
(
字段名 字段类型 DEFAULT 值
);
-- 这里要注意自增类型的条件,字段类型必须是整数类型。
CREATE TABLE
(
字段名 字段类型 AUTO_INCREMENT
);
-- 在一个已经存在的表基础上,创建一个新表
CREATE demo.importheadhist LIKE demo.importhead;
-- 修改表的相关语句
ALTER TABLE 表名 CHANGE 旧字段名 新字段名 数据类型;
ALTER TABLE 表名 ADD COLUMN 字段名 字段类型 FIRST|AFTER 字段名;
ALTER TABLE 表名 MODIFY 字段名 字段类型 FIRST|AFTER 字段名;
```
对于初学者来说掌握了今天的内容就足够对数据表进行操作了。不过MySQL支持的数据表操作不只这些我来举几个简单的小例子你可以了解一下有个印象。
比如,你可以在表一级指定表的存储引擎:
```
ALTER TABLE 表名 ENGINE=INNODB;
```
你还可以通过指定关键字AUTO_EXTENDSIZE来指定存储文件自增空间的大小从而提高存储空间的利用率。
在MySQL 8.0.23之后的版本中你甚至还可以通过INVISIBLE关键字使字段不可见但却可以使用。
如果你想了解更多有关数据表的操作,我也给你提供两份资料:[MySQL创建表文档](https://dev.mysql.com/doc/refman/8.0/en/create-table.html)和[MySQL修改表文档](https://dev.mysql.com/doc/refman/8.0/en/alter-table.html)。这些都是MySQL的官方文档相信会对你有所帮助。
## 思考题
请你写一个SQL语句将表demo.goodsmaster中的字段“salesprice”改成不能重复并且不能为空。
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,554 @@
<audio id="audio" title="04 | 增删改查:如何操作表中的数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/19/f5/199d6yyyy3f3da59fb1ccb6394ab8cf5.mp3"></audio>
你好,我是朱晓峰。今天,我们来聊一聊如何操作数据表里的数据。
在咱们的超市项目中,我们给用户设计好了一个数据表 demo.goodsmaster定义好了里面的字段以及各种约束如下所示
```
mysql&gt; DESCRIBE demo.goodsmaster;
+---------------+--------------+------+-----+---------+--+
| Field | Type | Null | Key | Default |Extra |
+---------------+------------+------+-----+---------+------------+
| itemnumber | int | NO | PRI | NULL |auto_increment |
| barcode | text | NO | | NULL | |
| goodsname | text | NO | | NULL | |
| specification | text | YES | | NULL | |
| unit | text | YES | | NULL | |
| price | decimal(10,2)| NO | | NULL | |
+---------------+------------+------+-----+---------+----------------+
6 rows in set (0.02 sec)
```
现在,我们需要使用这个表来存取数据。那么,如何对表里面的数据进行操作呢?接下来,我就给你讲讲操作表中的数据的方法,也就是常说的“增删改查”。
## 添加数据
我们先来看一下添加数据的语法结构:
```
INSERT INTO 表名 [(字段名 [,字段名] ...)] VALUES (值的列表);
```
这里方括号“[]”表示里面的内容可选也就是说根据MySQL的语法要求写不写都可以。
添加数据分为两种情况:插入数据记录和插入查询结果。下面我分别来介绍下。
### 插入数据记录
其实MySQL支持的数据插入操作十分灵活。你既可以通过给表里面所有的字段赋值完整地插入一条数据记录也可以在插入记录的时候只给部分字段赋值。
这是什么意思呢?我借助一个例子来给你讲解下。
比如我们有一个叫demo.goodsmaster的表包括了itemnumber、barcode、goodsname、specification、unit和price共6个字段。我想要插入一条数据记录其中包含了所有字段的值就可以这样操作
```
INSERT INTO demo.goodsmaster
(
itemnumber,
barcode,
goodsname,
specification,
unit,
price
)
VALUES
(
4,
'0003',
'尺子',
'三角型',
'把',
5
);
```
运行这个SQL语句然后对数据表进行查询可以得到下面的结果
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
+------------+---------+-----------+---------------+------+-------+
1 row in set (0.01 sec)
```
如果我想插入一条记录,但是只给部分字段赋值,可不可以呢?比如说,客户有个商品,需要马上上线销售,目前只知道条码、名称和价格,其它的信息先不录入,等之后再补,可以实现吗?
我们来尝试一下只给3个字段赋值看看实际操作的结果
```
INSERT INTO demo.goodsmaster
(
-- 这里只给3个字段赋值itemnumber、specification、unit不赋值
barcode,
goodsname,
price
)
VALUES
(
'0004',
'测试',
10
);
```
运行这条SQL语句我们来查询表的内容就会发现显然是可以的。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
+------------+---------+-----------+---------------+------+-------+
2 rows in set (0.00 sec)
```
我们之所以能够在插入一条数据记录的时候,只给部分字段赋值,原因就在于我们对字段的定义方式。
那字段是怎么定义的呢?我们来查看一下表的结构,看看各个字段的定义:
```
mysql&gt; DESCRIBE demo.goodsmaster;
+---------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------------+------+-----+---------+----------------+
| itemnumber | int | NO | PRI | NULL | auto_increment |
| barcode | text | NO | | NULL | |
| goodsname | text | NO | | NULL | |
| specification | text | YES | | NULL | |
| unit | text | YES | | NULL | |
| price | decimal(10,2) | NO | | NULL | |
+---------------+---------------+------+-----+---------+----------------+
6 rows in set (0.01 sec)
```
可以看到我们在插入数据时没有明确赋值的3个字段都有着各自的特点。“specification”和“unit”都可以是空值而itemnumber则定义了自增约束。
我们在插入一条数据记录的时候必须要考虑字段约束的3种情况。
第一种情况是如果字段允许为空而我们没有给它赋值那么MySQL会自动给它们赋予空值。在刚刚的代码中“specification”“unit”字段都允许为空因此如果数据插入语句没有指定这几个字段的值MySQL会自动插入空值。
第二种情况是如果字段是主键就不能为空这个时候MySQL会按照我们添加的约束进行处理。比如字段“itemnumber”是主键不能为空而我们定义了自增约束所以MySQL自动在之前的最大值基础上加了1。因此“itemnumber”也有了自己该有的值。
第三种情况是,如果有一个字段定义不能为空,又不是主键,当你插入一条数据记录的时候,就必须给这个记录赋值。
如果我们的操作违反了字段的约束限制,会出现什么情况呢?
比如说我们尝试把表demo.goodsmaster的字段“speicification”改为不能为空
```
ALTER TABLE demo.goodsmaster
MODIFY specification TEXT NOT NULL;
```
运行这个SQL语句系统会提示错误原因就是我们刚才部分插入了一条数据记录没有给字段“specification”赋值这跟我们给字段“specification”添加非空约束的操作冲突了。
因此我们要把字段“speicification”的值为空的数据记录删除然后再修改字段约束
```
DELETE
FROM demo.goodsmaster
WHERE itemnumber=5;
```
删除数据记录之后再运行上面的语句给字段“specification”添加非空约束就成功了。
现在我们来验证一下非空约束。我们尝试部分插入一条数据记录不给字段“specification”赋值
```
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0004',
'测试',
10
);
```
运行这个SQL语句MySQL报告错误提示字段“specification”没有默认值。也就是说这个字段不能为空如果插入数据时不给它赋值就必须给它一个默认值。
现在,你一定清楚了,**部分插入一条数据记录是可以的但前提是没有赋值的字段一定要让MySQL知道如何处理比如可以为空、有默认值或者是自增约束字段等等否则MySQL会提示错误的**。
好了到这里我们就学会了给MySQL的数据表插入一条数据记录。但是在实际工作中一次只插入一条数据有时候会不够用。
我们的项目就有这样的场景门店每天的销售流水都很多日积月累流水表变得越来越大。如果就让它这么不断地增长数据量甚至会达到数亿条占据的存储空间达到几十个G。虽然MySQL可以处理这样比较大的数据表但是每次操作的响应时间就会延长这会导致系统的整体效率下降。
刚开始的时候我们开发了一个日结处理当天算清所有账目。其中一个步骤就是把当天流水表的数据全部转到历史流水表当中去。现在我们就可以用上数据插入语句了。具体有2步
1. 从流水表中取出一条数据;
1. 把这条数据插入到历史流水表中。
然后不断重复这两个步骤,一直到把今天流水表的数据全部插入到历史流水表当中去。
你肯定会说,这种做法的效率也太低了吧?有没有更好的办法呢?
当然是有的。这个时候我们就要用到MySQL的另一种数据插入操作了把查询结果插入到数据表中。
### 插入查询结果
MySQL支持把查询的结果插入到数据表中我们可以指定字段甚至是数值插入到数据表中。语法结构如下
```
INSERT INTO 表名 (字段名)
SELECT 字段名或值
FROM 表名
WHERE 条件
```
举个例子在我们的超市信息系统的MySQL数据库中历史流水表设计与流水表非常类似。不同的是历史流水表增加了一些字段来标识历史流水的状态比如日结时间字段是用来记录日结操作是什么时候进行的。
用INSERT语句实现起来也很简单
```
INSERT INTO 历史流水表 (日结时间字段,其他字段)
SELECT 获取当前时间函数,其他字段
FROM 流水表
```
好了,添加数据的操作就讲完了,现在你知道了,我们给一张数据表插入一条数据记录的时候,可以给所有的字段赋值,也可以给部分字段赋值。这取决于字段的定义。如果字段不能为空并且没有默认值,就必须赋值。另外,我们还可以通过把一个查询结果插入数据表中的方式,提高添加数据的效率。
接下来,我们再来看看如何删除数据。
## 删除数据
数据删除的语法很简单,如下所示:
```
DELETE FROM 表名
WHERE 条件
```
如果我们现在想把刚才用过的表demo.goodsmaster里的内容清理一下删除全部数据可以通过下面的SQL语句来实现
```
DELETE FROM demo.goodsmaster;
```
因为我们的查询主要是在MySQL的图形化管理工具中进行现在我们在Workbench中运行上面的SQL语句。这个时候Workbench会提示错误。
这是因为Workbench自动处于安全模式它要求对数据的删除或修改操作中必须包含WHERE条件。而且这个WHERE条件中必须用到主键约束或者唯一性约束的字段。MySQL的这种安全性设置主要就是为了防止删除或者修改数据时出现误操作导致删除或修改了不相关的数据。
因此我们要习惯在删除数据的时候添加条件语句WHERE防止误操作。
假设我们的数据表demo.goodsmaster中有如下数据记录
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
+------------+---------+-----------+---------------+------+-------+
2 rows in set (0.00 sec)
```
如果我们要删除所有数据,可以这样写:
```
DELETE FROM demo.goodsmaster
WHERE itemnumber &gt; 1;
```
好了,下面我们来学习一下修改数据。
## 修改数据
我们来看一下MySQL的数据修改语法
```
UPDATE 表名
SET 字段名=值
WHERE 条件
```
语法很简单,需要注意的一点是,**不要修改主键字段的值**。因为主键是数据记录的唯一标识,如果修改了主键的值,就有可能会破坏数据的完整性。
下面我们来看一个通过主键查询商品信息的例子,看看修改主键的值,会产生什么样的结果。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; WHERE itemnumber = 3;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 3 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
+------------+---------+-----------+---------------+------+-------+
1 row in set (0.00 sec)
```
我们能查询到一条商品编号是3的数据记录条码是“0003”名称是“尺子”价格是5元。
如果我们修改了主键的值,就可能会改变刚才的查询结果:
```
mysql&gt; UPDATE demo.goodsmaster
-&gt; SET itemnumber=2
-&gt; WHERE itemnumber = 3;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; WHERE itemnumber = 3;
Empty set (0.00 sec)
```
可以看到查询结果是空因为商品编号是3的数据记录已经不存在了。
当然,如果你必须要修改主键的值,那有可能就是主键设置得不合理。关于主键的设置问题,我会在下节课给你详细介绍。这里你只需要知道,不要随便修改表的主键值,就可以了。
## 查询数据
经过了前面的操作之后,现在,我们知道了把数据存入数据表里,以及删除和修改表里数据的方法。那么如何知道数据操作的结果呢?这就要用到数据查询了。
我们先来看下查询语句的语法结构:
```
SELECT *|字段列表
FROM 数据源
WHERE 条件
GROUP BY 字段
HAVING 条件
ORDER BY 字段
LIMIT 起始点,行数
```
在这些字段中SELECT、WHERE、GROUP BY和HAVING比较好理解你只要知道它们的含义就可以了。
- SELECT是查询关键字表示我们要做一个查询。“*”是一个通配符,表示我们要查询表中所有的字段。你也可以把要查询的字段罗列出来,这样,查询的结果可以只显示你想要查询的字段内容。
- WHERE表示查询条件。你可以把你要查询的数据所要满足的条件放在WHERE关键字之后。
- GROUP BY作用是告诉MySQL查询结果要如何分组经常与MySQL的聚合函数一起使用。
- HAVING用于筛选查询结果跟WHERE类似。
FROM、ORDER BY和LIMIT相对来说比较复杂需要注意的地方比较多我来具体给你解释一下。
### FROM
FROM关键字表示查询的数据源。我们现在只学习了单个数据表你可以把你要查询的数据表名直接写在FROM关键字之后。以后我们在学到关联表的时候你就会知道FROM关键字后面还可以跟着更复杂的数据表联接。
这里需要你要注意的是,数据源也不一定是表,也可以是一个查询的结果。比如下面的查询:
<img src="https://static001.geekbang.org/resource/image/c8/b0/c88513447b9yydbb6f983603ffba19b0.png" alt="">
这里你要注意的是红色框里的部分叫做派生表derived table或者子查询subquery意思是我们把一个查询结果数据集当做一个虚拟的数据表来看待。MySQL规定必须要用AS关键字给这个派生表起一个别名。在这张图中我给这个派生表起了个名字叫做“a”。
### ORDER BY
ORDER BY的作用是告诉MySQL查询结果如何排序。**ASC表示升序DESC表示降序**。
我来举个简单的小例子带你看看ORDER BY是怎么使用的这里我们仍然假设字段“specification”和“unit”允许为空。我们向表demo.goodsmaster中插入2条数据
```
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0003',
'尺子1',
15
);
INSERT INTO demo.goodsmaster
(
barcode,
goodsname,
price
)
VALUES
(
'0004',
'测试1',
20
);
```
如果我们不控制查询结果的顺序,就会得到这样的结果:
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
| 6 | 0003 | 尺子1 | NULL | NULL | 15.00 |
| 7 | 0004 | 测试1 | NULL | NULL | 20.00 |
+------------+---------+-----------+---------------+------+-------+
4 rows in set (0.00 sec)
```
如果我们使用ORDER BY对查询结果进行控制结果就不一样了
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; ORDER BY barcode ASC,price DESC;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 6 | 0003 | 尺子1 | NULL | NULL | 15.00 |
| 4 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
| 7 | 0004 | 测试1 | NULL | NULL | 20.00 |
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
+------------+---------+-----------+---------------+------+-------+
4 rows in set (0.00 sec)
```
可以看到查询结果会先按照字段barcode的升序排序相同barcode里面的字段按照price的降序排序。
### LIMIT
LIMIT的作用是告诉MySQL只显示部分查询的结果。比如现在我们的数据表demo.goodsmaster中有4条数据我们只想要显示第2、3条数据就可以用LIMIT关键字来实现
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster
-&gt; LIMIT 1,2;
+------------+---------+-----------+---------------+------+-------+
| itemnumber | barcode | goodsname | specification | unit | price |
+------------+---------+-----------+---------------+------+-------+
| 5 | 0004 | 测试 | NULL | NULL | 10.00 |
| 6 | 0003 | 尺子1 | NULL | NULL | 15.00 |
+------------+---------+-----------+---------------+------+-------+
2 rows in set (0.00 sec)
```
这里的“LIMIT 1,2”中“1”表示起始位置MySQL中起始位置的起点是01表示从第2条记录开始“2”表示2条数据。因此“LIMIT 1,2”就表示从第2条数据开始显示2条数据也就是显示了第2、3条数据。
## 总结
今天我们学习了添加、删除、修改和查询数据的方法这些都是我们经常遇到的操作你一定要好好学习。下面的这些SQL语句是我总结的数据操作语法你一定要重点掌握。
```
INSERT INTO 表名 [(字段名 [,字段名] ...)] VALUES (值的列表);
INSERT INTO 表名 (字段名)
SELECT 字段名或值
FROM 表名
WHERE 条件
DELETE FROM 表名
WHERE 条件
UPDATE 表名
SET 字段名=值
WHERE 条件
SELECT *|字段列表
FROM 数据源
WHERE 条件
GROUP BY 字段
HAVING 条件
ORDER BY 字段
LIMIT 起始点,行数
```
最后我再补充一点。如果我们把查询的结果插入到表中时导致主键约束或者唯一性约束被破坏了就可以用“ON DUPLICATE”关键字进行处理。这个关键字的作用是告诉MySQL如果遇到重复的数据该如何处理。
我来给你举个例子。
假设用户有2个各自独立的门店分别有自己的系统。现在需要引入连锁经营的模式把2个店用一套系统统一管理。那么首先遇到的问题就是需要进行数据整合。下面我们就以商品信息表为例来说明如何通过使用“ON DUPLICATE”关键字把两个门店的商品信息数据整合到一起。
假设门店A的商品信息表是“demo.goodsmaster”代码如下
```
mysql&gt; SELECT *
-&gt; 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)
```
门店B的商品信息表是“demo.goodsmaster1”
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster1;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 教科书 | NULL | NULL | 89.00 |
| 4 | 0004 | 馒头 | | | 1.50 |
+------------+---------+-----------+---------------+------+------------+
2 rows in set (0.00 sec)
```
假设我们要把门店B的商品数据插入到门店A的商品表中去如果有重复的商品编号就用门店B的条码替换门店A的条码用门店B的商品名称替换门店A的商品名称如果没有重复的编号就直接把门店B的商品数据插入到门店A的商品表中。这个操作就可以用下面的SQL语句实现
```
INSERT INTO demo.goodsmaster
SELECT *
FROM demo.goodsmaster1 as a
ON DUPLICATE KEY UPDATE barcode = a.barcode,goodsname=a.goodsname;
-- 运行结果如下
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 教科书 | 16开 | 本 | 89.00 |
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
| 4 | 0004 | 馒头 | | | 1.50 |
+------------+---------+-----------+---------------+------+------------+
4 rows in set (0.00 sec)
```
最后我再跟你分享3份资料分别是[MySQL数据插入](https://dev.mysql.com/doc/refman/8.0/en/insert.html)、[MySQL数据更新](https://dev.mysql.com/doc/refman/8.0/en/update.html)和[MySQL数据查询](https://dev.mysql.com/doc/refman/8.0/en/select.html),如果你在工作中遇到了更加复杂的操作需求,就可以参考一下。
## 思考题
我想请你思考一个问题商品表demo.goodsmaster中字段“itemnumber”是主键而且满足自增约束如果我删除了一条记录再次插入数据的时候就会出现字段“itemnumber”的值不连续的情况。请你想一想如何插入数据才能防止这种情况的发生呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,436 @@
<audio id="audio" title="05 | 主键:如何正确设置主键?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/ff/7c72bfee0a63yy0fdac7199217a257ff.mp3"></audio>
你好我是朱晓峰今天我们来聊一聊如何用好MySQL的主键。
前面在讲存储的时候,我提到过主键,它可以唯一标识表中的某一条记录,对数据表来说非常重要。当我们需要查询和引用表中的一条记录的时候,最好的办法就是通过主键。只有合理地设置主键,才能确保我们准确、快速地找到所需要的数据记录。今天我就借助咱们的超市项目的实际需求,来给你讲一讲怎么正确设置主键。
在我们的项目中客户要进行会员营销相应的我们就需要处理会员信息。会员信息表demo.membermaster的设计大体如下
<img src="https://static001.geekbang.org/resource/image/87/c0/87977152197dbaa92d6a86cc9911c1c0.jpg" alt="">
为了能够唯一地标识一个会员的信息,我们需要为会员信息表设置一个主键。那么,怎么为这个表设置主键,才能达到我们理想的目标呢?
今天,我就带你在解决这个实际问题的过程中,学习下三种设置主键的思路:**业务字段做主键**、**自增字段做主键**和**手动赋值字段做主键**。
## 业务字段做主键
针对这个需求,我们最容易想到的,是选择表中已有的字段,也就是跟业务相关的字段做主键。那么,在这个表里,哪个字段比较合适呢?我们来分析一下。
会员卡号cardno看起来比较合适因为会员卡号不能为空而且有唯一性可以用来标识一条会员记录。我们来尝试一下用会员卡号做主键。
我们可以用下面的代码在创建表的时候设置字段cardno为主键
```
mysql&gt; CREATE TABLE demo.membermaster
-&gt; (
-&gt; cardno CHAR(8) PRIMARY KEY, -- 会员卡号为主键
-&gt; membername TEXT,
-&gt; memberphone TEXT,
-&gt; memberpid TEXT,
-&gt; memberaddress TEXT,
-&gt; sex TEXT,
-&gt; birthday DATETIME
-&gt; );
Query OK, 0 rows affected (0.06 sec)
```
我们来查询一下表的结构,确认下主键是否创建成功了:
```
mysql&gt; DESCRIBE demo.membermaster;
+---------------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+-------+
| cardno | char(8) | NO | PRI | NULL | |
| membername | text | YES | | NULL | |
| memberphone | text | YES | | NULL | |
| memberpid | text | YES | | NULL | |
| memberaddress | text | YES | | NULL | |
| sex | text | YES | | NULL | |
| birthday | datetime | YES | | NULL | |
+---------------+----------+------+-----+---------+-------+
7 rows in set (0.02 sec)
```
可以看到字段cardno在表示键值的key这一列的值是“PRI”意思是PRIMARY KEY这就表示它已经被设置成主键了。这里需要注意的一点是除了字段cardno所有的字段都允许为空。这是因为这些信息有可能当时不知道要稍后补齐。
会员卡号做主键有没有什么问题呢我们插入2条数据来验证下
```
mysql&gt; INSERT INTO demo.membermaster
-&gt; (
-&gt; cardno,
-&gt; membername,
-&gt; memberphone,
-&gt; memberpid,
-&gt; memberaddress,
-&gt; sex,
-&gt; birthday
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; '10000001',
-&gt; '张三',
-&gt; '13812345678',
-&gt; '110123200001017890',
-&gt; '北京',
-&gt; '男',
-&gt; '2000-01-01'
-&gt; );
Query OK, 1 row affected (0.01 sec)
mysql&gt; INSERT INTO demo.membermaster
-&gt; (
-&gt; cardno,
-&gt; membername,
-&gt; memberphone,
-&gt; memberpid,
-&gt; memberaddress,
-&gt; sex,
-&gt; birthday
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; '10000002',
-&gt; '李四',
-&gt; '13512345678',
-&gt; '123123199001012356',
-&gt; '上海',
-&gt; '女',
-&gt; '1990-01-01'
-&gt; );
Query OK, 1 row affected (0.01 sec)
```
插入成功后,我们来看一下表的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.membermaster;
+----------+------------+-------------+--------------------+---------------+------+---------------------+
| cardno | membername | memberphone | memberpid | memberaddress | sex | birthday |
+----------+------------+-------------+--------------------+---------------+------+---------------------+
| 10000001 | 张三 | 13812345678 | 110123200001017890 | 北京 | 男 | 2000-01-01 00:00:00 |
| 10000002 | 李四 | 13512345678 | 123123199001012356 | 上海 | 女 | 1990-01-01 00:00:00 |
+----------+------------+-------------+--------------------+---------------+------+---------------------+
2 rows in set (0.00 sec)
```
我们发现不同的会员卡号对应不同的会员字段“cardno”唯一地标识某一个会员。如果都是这样会员卡号与会员一一对应系统是可以正常运行的。
但是实际情况是上线不到一周就发生了“cardno”无法唯一识别某一个会员的问题。原来会员卡号存在重复使用的情况。
这也很好理解比如张三因为工作变动搬离了原来的地址不再到商家的门店消费了退还了会员卡于是张三就不再是这个商家门店的会员了。但是商家不想让这个会员卡空着就把卡号是“10000001”的会员卡发给了王五。
从系统设计的角度看这个变化只是修改了会员信息表中的卡号是“10000001”这个会员信息并不会影响到数据一致性。也就是说修改会员卡号是“10000001”的会员信息系统的各个模块都会获取到修改后的会员信息不会出现“有的模块获取到修改之前的会员信息有的模块获取到修改后的会员信息而导致系统内部数据不一致”的情况。因此从信息系统层面上看是没问题的。但是从使用系统的业务层面来看就有很大的问题了会对商家造成影响。
下面,我们就来看看这种修改,是如何影响到商家的。
比如我们有一个销售流水表记录了所有的销售流水明细。2020年12月01日张三在门店购买了一本书消费了89元。那么系统中就有了张三买书的流水记录如下所示
<img src="https://static001.geekbang.org/resource/image/86/a4/864b283a81320351ccdeaf24be558aa4.jpg" alt="">
我们可以用下面的代码创建销售流水表。因为需要引用会员信息和商品信息,所以表中要包括商品编号字段和会员卡号字段。
```
mysql&gt; CREATE table demo.trans
-&gt; (
-&gt; transactionno INT,
-&gt; itemnumber INT, -- 为了引用商品信息
-&gt; quantity DECIMAL(10,3),
-&gt; price DECIMAL(10,2),
-&gt; salesvalue DECIMAL(10,2),
-&gt; cardno CHAR(8), -- 为了引用会员信息
-&gt; transdate DATETIME
-&gt; );
Query OK, 0 rows affected (0.10 sec)
```
创建好表以后,我们来插入一条销售流水:
```
mysql&gt; INSERT INTO demo.trans
-&gt; (
-&gt; transactionno,
-&gt; itemnumber,
-&gt; quantity,
-&gt; price,
-&gt; salesvalue,
-&gt; cardno,
-&gt; transdate
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; 1,
-&gt; 1,
-&gt; 1,
-&gt; 89,
-&gt; 89,
-&gt; '10000001',
-&gt; '2020-12-01'
-&gt; );
Query OK, 1 row affected (0.01 sec)
```
接着我们查询一下2020年12月01日的会员销售记录
```
mysql&gt; SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.membermaster AS b
-&gt; JOIN demo.goodsmaster AS c
-&gt; ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.00 sec)
```
我们得到的查询结果是张三在2020年12月01日买了一本书花了89元。
需要注意的是这里我用到了JOIN也就是表的关联目的是为了引用其他表的信息包括会员信息表demo.membermaster和商品信息表demo.goodsmaster。有关关联表查询的具体细节我会在下节课讲到这里你只要知道通过关联查询可以从会员信息表中获取会员信息从商品信息表中获取商品信息就可以了。
下面我们假设会员卡“10000001”又发给了王五我们需要更改会员信息表
```
mysql&gt; UPDATE demo.membermaster
-&gt; SET membername = '王五',
-&gt; memberphone = '13698765432',
-&gt; memberpid = '475145197001012356',
-&gt; memberaddress='天津',
-&gt; sex='女',
-&gt; birthday = '1970-01-01'
-&gt; WHERE cardno = '10000001';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
会员记录改好了,我们再次运行之前的会员消费流水查询:
```
mysql&gt; SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.membermaster AS b
-&gt; JOIN demo.goodsmaster AS c
-&gt; ON (a.cardno = b.cardno AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 王五 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)
```
这次得到的结果是王五在2020年12月01日买了一本书消费89元。
很明显这个结果把张三的消费行为放到王五身上去了肯定是不对的。这里的原因就是我们把会员卡号是“10000001”的会员信息改了而会员卡号是主键会员消费查询通过会员卡号关联到会员信息得到了完全错误的结果。
现在你知道了吧,千万不能把会员卡号当做主键。
那么,会员电话可以做主键吗?不行的。在实际操作中,手机号也存在被运营商收回,重新发给别人用的情况。
那身份证号行不行呢?好像可以。因为身份证决不会重复,身份证号与一个人存在一一对应的关系。可问题是,身份证号属于个人隐私,顾客不一定愿意给你。对门店来说,顾客就是上帝,要是强制要求会员必须登记身份证号,会把很多客人赶跑的。其实,客户电话也有这个问题,这也是我们在设计会员信息表的时候,允许身份证号和电话都为空的原因。
这样看来,任何一个现有的字段都不适合做主键。
所以,我建议你**尽量不要用业务字段,也就是跟业务有关的字段做主键**。毕竟,作为项目设计的技术人员,我们谁也无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。
既然业务字段不可以,那我们再来试试自增字段。
## 使用自增字段做主键
我们来给会员信息表添加一个字段比如叫id给这个字段定义自增约束这样我们就有了一个具备唯一性的而且不为空的字段来做主键了。
接下来,我们就来修改一下会员信息表的结构,添加一个自增字段做主键。
第一步,修改会员信息表,删除表的主键约束,这样,原来的主键字段,就不再是主键了。不过需要注意的是,删除主键约束,并不会删除字段。
```
mysql&gt; ALTER TABLE demo.membermaster
-&gt; DROP PRIMARY KEY;
Query OK, 2 rows affected (0.12 sec)
Records: 2 Duplicates: 0 Warnings: 0
```
第二步修改会员信息表添加字段“id”为主键并且给它定义自增约束
```
mysql&gt; ALTER TABLE demo.membermaster
-&gt; ADD id INT PRIMARY KEY AUTO_INCREMENT;
Query OK, 0 rows affected (0.12 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第三步修改销售流水表添加新的字段memberid对应会员信息表中的主键
```
mysql&gt; ALTER TABLE demo.trans
-&gt; ADD memberid INT;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
第四步我们更新一下销售流水表给新添加的字段“memberid”赋值让它指向对应的会员信息
```
mysql&gt; UPDATE demo.trans AS a,demo.membermaster AS b
-&gt; SET a.memberid=b.id
-&gt; WHERE a.transactionno &gt; 0
--&gt; AND a.cardno = b.cardno; -- 这样操作可以不用删除trans的内容在实际工作中更适合
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
```
这个更新语句包含了2个关联的表看上去有点复杂。其实你完全可以通过删除表demo.trans、重建表再插入一条数据的操作来达到同样的目的但是我不建议你这么做。
在实际操作中你不一定能删掉demo.trans这个表因为这个表里面可能已经有了很多重要的数据。所以你一定要认真学习一下我给你介绍的这个更新数据的方法这种复杂一点的更新语句在实战中更有用。
好了到这里我们就完成了数据表的重新设计让我们看一下新的数据表demo.membermaster和demo.trans的结构
```
mysql&gt; DESCRIBE demo.membermaster;
+---------------+----------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+----------------+
| cardno | char(8) | NO | | NULL | |
| membername | text | YES | | NULL | |
| memberphone | text | YES | | NULL | |
| memberpid | text | YES | | NULL | |
| memberaddress | text | YES | | NULL | |
| sex | text | YES | | NULL | |
| birthday | datetime | YES | | NULL | |
| id | int | NO | PRI | NULL | auto_increment |
+---------------+----------+------+-----+---------+----------------+
8 rows in set (0.02 sec)
mysql&gt; DESCRIBE demo.trans;
+---------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+---------------+------+-----+---------+-------+
| transactionno | int | NO | PRI | NULL | |
| itemnumber | int | YES | | NULL | |
| quantity | decimal(10,3) | YES | | NULL | |
| price | decimal(10,2) | YES | | NULL | |
| salesvalue | decimal(10,2) | YES | | NULL | |
| cardno | char(8) | YES | | NULL | |
| transdate | datetime | YES | | NULL | |
| memberid | int | YES | | NULL | |
+---------------+---------------+------+-----+---------+-------+
8 rows in set (0.00 sec)
```
现在如果我们再次面对卡号重用的情况该如何应对呢这里我们假设回到修改会员卡10000001为王五之前的状态
如果张三的会员卡“10000001”不再使用发给了王五我们就在会员信息表里面增加一条记录
```
mysql&gt; INSERT INTO demo.membermaster
-&gt; (
-&gt; cardno,
-&gt; membername,
-&gt; memberphone,
-&gt; memberpid,
-&gt; memberaddress,
-&gt; sex,
-&gt; birthday
-&gt; )
-&gt; VALUES
-&gt; (
-&gt; '10000001',
-&gt; '王五',
-&gt; '13698765432',
-&gt; '475145197001012356',
-&gt; '天津',
-&gt; '女',
-&gt; '1970-01-01'
-&gt; );
Query OK, 1 row affected (0.02 sec)
```
下面我们看看现在的会员信息:
```
mysql&gt; SELECT *
-&gt; FROM demo.membermaster;
+----------+------------+-------------+--------------------+---------------+------+---------------------+----+
| cardno | membername | memberphone | memberpid | memberaddress | sex | birthday | id |
+----------+------------+-------------+--------------------+---------------+------+---------------------+----+
| 10000001 | 张三 | 13812345678 | 110123200001017890 | 北京 | 男 | 2000-01-01 00:00:00 | 1 |
| 10000002 | 李四 | 13512345678 | 123123199001012356 | 上海 | 女 | 1990-01-01 00:00:00 | 2 |
| 10000001 | 王五 | 13698765432 | 475145197001012356 | 天津 | 女 | 1970-01-01 00:00:00 | 3 |
+----------+------------+-------------+--------------------+---------------+------+---------------------+----+
3 rows in set (0.00 sec)
```
由于字段“cardno”不再是主键可以允许重复因此我们可以在保留会员“张三”信息的同时添加使用同一会员卡号的“王五”的信息。
现在再来查会员消费,就不会出问题了:
```
mysql&gt; SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
-&gt; FROM demo.trans AS a
-&gt; JOIN demo.membermaster AS b
-&gt; JOIN demo.goodsmaster AS c
-&gt; ON (a.memberid = b.id AND a.itemnumber=c.itemnumber);
+------------+-----------+----------+------------+---------------------+
| membername | goodsname | quantity | salesvalue | transdate |
+------------+-----------+----------+------------+---------------------+
| 张三 | 书 | 1.000 | 89.00 | 2020-12-01 00:00:00 |
+------------+-----------+----------+------------+---------------------+
1 row in set (0.01 sec)
```
可以看到结果是2020年12月01日张三买了一本书消费89元是正确的。
如果是一个小项目只有一个MySQL数据库服务器用添加自增字段作为主键的办法是可以的。不过这并不意味着在任何情况下你都可以这么做。
举个例子用户要求把增加新会员的工作放到门店进行因为发展新会员的工作一般是在门店进行的毕竟人们一般都是在购物的同时申请会员。解决的办法是门店的信息系统添加新增会员的功能把新的会员信息先存放到本地MySQL数据库中再上传到总部进行汇总。
可是问题来了如果会员信息表的主键是自增的那么各个门店新加的会员就会出现“id”冲突的可能。
比如A店的MySQL数据库中的demo.membermaster中字段“id”的值是100这个时候新增了一个会员“id”是101。同时B店的字段“id”值也是100要加一个新会员“id”也是101毕竟B店的MySQL数据库与A店相互独立。等A店与B店都把新的会员上传到总部之后就会出现两个“id”是101但却是不同会员的情况这该如何处理呢
## 手动赋值字段做主键
为了解决这个问题我们想了一个办法取消字段“id”的自增属性改成信息系统在添加会员的时候对“id”进行赋值。
具体的操作是这样的在总部MySQL数据库中有一个管理信息表里面的信息包括成本核算策略支付方式等还有总部的系统参数我们可以在这个表中添加一个字段专门用来记录当前会员编号的最大值。
门店在添加会员的时候先到总部MySQL数据库中获取这个最大值在这个基础上加1然后用这个值作为新会员的“id”同时更新总部MySQL数据库管理信息表中的当前会员编号的最大值。
这样一来各个门店添加会员的时候都对同一个总部MySQL数据库中的数据表字段进行操作就解决了各门店添加会员时会员编号冲突的问题同时也避免了使用业务字段导致数据错误的问题。
## 总结
今天,我给你介绍了设置数据表主键的三种方式:数据表的业务字段做主键、添加自增字段做主键,以及添加手动赋值字段做主键。
- 用业务字段做主键,看起来很简单,但是我们应该尽量避免这样做。因为我们无法预测未来会不会因为业务需要,而出现业务字段重复或者重用的情况。
- 自增字段做主键,对于单机系统来说是没问题的。但是,如果有多台服务器,各自都可以录入数据,那就不一定适用了。因为如果每台机器各自产生的数据需要合并,就可能会出现主键重复的问题。
- 我们可以采用手动赋值的办法,通过一定的逻辑,确保字段值在全系统的唯一性,这样就可以规避主键重复的问题了。
刚开始使用MySQL时很多人都很容易犯的错误是喜欢用业务字段做主键想当然地认为了解业务需求但实际情况往往出乎意料而更改主键设置的成本非常高。所以如果你的系统比较复杂尽量给表加一个字段做主键采用手动赋值的办法虽然系统开发的时候麻烦一点却可以避免后面出大问题。
## 思考题
在刚刚的例子中如果我想把销售流水表demo.trans中所有单位是“包”的商品的价格改成原来价格的80%,该怎么实现呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,426 @@
<audio id="audio" title="06 | 外键和连接:如何做关联查询?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/99/0cbabcfb798777a941e3095790yybe99.mp3"></audio>
你好,我是朱晓峰。今天我来和你聊一聊关联查询的问题。
在实际的数据库应用开发过程中我们经常需要把2个或2个以上的表进行关联以获取需要的数据。这是因为为了提高存取效率我们会把不同业务模块的信息分别存放在不同的表里面。但是从业务层面上看我们需要完整全面的信息为经营决策提供数据支撑。
就拿咱们的超市项目来说,数据库里面的销售流水表一般只保存销售必需的信息(比如商品编号、数量、价格、金额和会员卡号等)。但是,在呈现给超市经营者的统计报表里面,只包括这些信息是不够的,比如商品编号、会员卡号,这些数字经营者就看不懂。因此,必须要从商品信息表提取出商品信息,从会员表中提取出会员的相关信息,这样才能形成一个完整的报表。**这种把分散在多个不同的表里的数据查询出来的操作,就是多表查询**。
不过,这种查询可不简单,我们需要建立起多个表之间的关联,然后才能去查询,同时还需要规避关联表查询中的常见错误。具体怎么做呢?我来借助实际的项目给你讲一讲。
在我们项目的进货模块有这样2个数据表分别是进货单头表importhead和进货单明细表importdetails我们每天都要对这两个表进行增删改查的操作。
进货单头表记录的是整个进货单的总体信息:
<img src="https://static001.geekbang.org/resource/image/43/11/43d3b085096efc3a6111a89bbe0e7911.jpg" alt="">
进货单明细表记录了每次进货的商品明细信息。一条进货单头数据记录,对应多条进货商品的明细数据,也就是所谓的一对多的关系。具体信息如下表所示:
<img src="https://static001.geekbang.org/resource/image/36/44/369981d9a37a38fb65c0981a0544bc44.jpg" alt="">
现在我们需要查询一次进货的所有相关数据包括进货单的总体信息和进货商品的明细这样一来我们就需要把2个表关联起来那么该怎么操作呢
在MySQL中为了把2个表关联起来会用到2个重要的功能外键FOREIGN KEY和连接JOIN。外键需要在创建表的阶段就定义连接可以通过相同意义的字段把2个表连接起来用在查询阶段。
接下来,我就先和你聊聊外键。
## 如何创建外键?
我先来解释一下什么是外键。
假设我们有2个表分别是表A和表B它们通过一个公共字段“id”发生关联关系我们把这个关联关系叫做R。如果“id”在表A中是主键那么表A就是这个关系R中的主表。相应的表B就是这个关系中的从表表B中的“id”就是表B用来引用表A中数据的叫外键。所以**外键就是从表中用来引用主表中数据的那个公共字段**。
为了方便你理解,我画了一张图来展示:
<img src="https://static001.geekbang.org/resource/image/68/ae/68836a01eb1d667dea93ceda8e5714ae.jpg" alt="">
如图所示在关联关系R中公众字段字段A是表A的主键所以表A是主表表B是从表。表B中的公共字段字段A是外键。
在MySQL中外键是通过外键约束来定义的。外键约束就是约束的一种它必须在从表中定义包括指明哪个是外键字段以及外键字段所引用的主表中的主键字段是什么。MySQL系统会根据外键约束的定义监控对主表中数据的删除操作。如果发现要删除的主表记录正在被从表中某条记录的外键字段所引用MySQL就会提示错误从而确保了关联数据不会缺失。
外键约束可以在创建表的时候定义,也可以通过修改表来定义。我们先来看看外键约束定义的语法结构:
```
[CONSTRAINT &lt;外键约束名称&gt;] FOREIGN KEY 字段名
REFERENCES &lt;主表名&gt; 字段名
```
你可以在创建表的时候定义外键约束:
```
CREATE TABLE 从表名
(
字段名 类型,
...
-- 定义外键约束,指出外键字段和参照的主表字段
CONSTRAINT 外键约束名
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名)
)
```
当然,你也可以通过修改表来定义外键约束:
```
ALTER TABLE 从表名 ADD CONSTRAINT 约束名 FOREIGN KEY 字段名 REFERENCES 主表名 (字段名);
```
一般情况下,表与表的关联都是提前设计好了的,因此,会在创建表的时候就把外键约束定义好。不过,如果需要修改表的设计(比如添加新的字段,增加新的关联关系),但没有预先定义外键约束,那么,就要用修改表的方式来补充定义。
下面,我就来讲一讲怎么创建外键约束。
先创建主表demo.importhead
```
CREATE TABLE demo.importhead (
listnumber INT PRIMARY KEY,
supplierid INT,
stocknumber INT,
importtype INT,
importquantity DECIMAL(10 , 3 ),
importvalue DECIMAL(10 , 2 ),
recorder INT,
recordingdate DATETIME
);
```
然后创建从表demo.importdetails并且给它定义外键约束
```
CREATE TABLE demo.importdetails
(
listnumber INT,
itemnumber INT,
quantity DECIMAL(10,3),
importprice DECIMAL(10,2),
importvalue DECIMAL(10,2),
-- 定义外键约束,指出外键字段和参照的主表字段
CONSTRAINT fk_importdetails_importhead
FOREIGN KEY (listnumber) REFERENCES importhead (listnumber)
);
```
运行这个SQL语句我们就在创建表的同时定义了一个名字叫“fk_importdetails_importhead”的外键约束。同时我们声明这个外键约束的字段“listnumber”引用的是表importhead里面的字段“listnumber”。
外键约束创建好之后我们可以通过Workbench来查看外键约束是不是创建成功了
<img src="https://static001.geekbang.org/resource/image/8f/9b/8f7154a32943699113a308b62ea5979b.png" alt="">
当然我们也可以通过SQL语句来查看这里我们要用到MySQL自带的、用于存储系统信息的数据库information_schema。我们可以查看外键约束的相关信息
```
mysql&gt; SELECT
-&gt; constraint_name, -- 表示外键约束名称
-&gt; table_name, -- 表示外键约束所属数据表的名称
-&gt; column_name, -- 表示外键约束的字段名称
-&gt; referenced_table_name, -- 表示外键约束所参照的数据表名称
-&gt; referenced_column_name -- 表示外键约束所参照的字段名称
-&gt; FROM
-&gt; information_schema.KEY_COLUMN_USAGE
-&gt; WHERE
-&gt; constraint_name = 'fk_importdetails_importhead';
+-----------------------------+---------------+-------------+-----------------------+------------------------+
| CONSTRAINT_NAME | TABLE_NAME | COLUMN_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
+-----------------------------+---------------+-------------+-----------------------+------------------------+
| fk_importdetails_importhead | importdetails | listnumber | importhead | listnumber |
+-----------------------------+---------------+-------------+-----------------------+------------------------+
1 row in set (0.05 sec)
```
通过查询我们可以看到外键约束所在的表是“importdetails”外键字段是“listnumber”参照的主表是“importhead”参照的主表字段是“listnumber”。这样通过定义外键约束我们已经建立起了2个表之间的关联关系。
关联关系建立起来之后,如何才能获取我们需要的数据呢?这时,我们就需要用到连接查询了。
## 连接
在MySQL中有2种类型的连接分别是内连接INNER JOIN和外连接OUTER JOIN
- 内连接表示查询结果只返回符合连接条件的记录,这种连接方式比较常用;
- 外连接则不同,表示查询结果返回某一个表中的所有记录,以及另一个表中满足连接条件的记录。
下面我先来讲一下内连接。
在MySQL里面关键字JOIN、INNER JOIN、CROSS JOIN的含义是一样的都表示内连接。我们可以通过JOIN把两个表关联起来来查询两个表中的数据。
我借助一个小例子,来帮助你理解。
咱们的项目中有会员销售的需求,所以,我们的流水表中的数据记录,既包括非会员的普通销售,又包括会员销售。它们的区别是,会员销售的数据记录包括会员编号,而在非会员销售的数据记录中,会员编号为空。
来看一下项目中的销售表demo.trans)。实际的销售表比较复杂为了方便你理解我把表进行了简化并且假设业务字段cardno是会员信息表的主键。简化以后的结构如下所示
<img src="https://static001.geekbang.org/resource/image/97/73/97b818520dc0de32c5c8e17181746673.jpg" alt="">
再看下简化后的会员信息表demo.membermaster
<img src="https://static001.geekbang.org/resource/image/02/c7/023f62732eede3ee7c14cebd689770c7.jpg" alt="">
这两个表之间存在关联关系表demo.trans中的字段“cardno”是这个关联关系中的外键。
我们可以通过内连接,查询所有会员销售的流水记录:
```
mysql&gt; SELECT
-&gt; a.transactionno,
-&gt; a.itemnumber,
-&gt; a.quantity,
-&gt; a.price,
-&gt; a.transdate,
-&gt; b.membername
-&gt; FROM
-&gt; demo.trans AS a
-&gt; JOIN
-&gt; demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate | membername |
+---------------+------------+----------+-------+---------------------+------------+
| 1 | 1 | 1.000 | 89.00 | 2020-12-01 00:00:00 | 张三 |
+---------------+------------+----------+-------+---------------------+------------+
1 row in set (0.00 sec)
```
可以看到我们通过公共字段“cardno”把两个表关联到了一起查询出了会员消费的数据。
在这里关键字JOIN与关键字ON配对使用意思是查询满足关联条件“demo.trans表中cardno的值与demo.membermaster表中的cardno值相等”的两个表中的所有记录。
知道了内连接,我们再来学习下外连接。跟内连接只返回符合连接条件的记录不同的是,外连接还可以返回表中的所有记录,它包括两类,分别是左连接和右连接。
- 左连接一般简写成LEFT JOIN返回左边表中的所有记录以及右表中符合连接条件的记录。
- 右连接一般简写成RIGHT JOIN返回右边表中的所有记录以及左表中符合连接条件的记录。
当我们需要查询全部流水信息的时候,就会用到外连接,代码如下:
```
SELECT
a.transactionno,
a.itemnumber,
a.quantity,
a.price,
a.transdate,
b.membername
FROM demo.trans AS a
LEFT JOIN demo.membermaster AS b -- LEFT JOIN以demo.transaction为主
ON (a.cardno = b.cardno);
```
可以看到我用到了LEFT JOIN意思是以表demo.trans中的数据记录为主这个表中的数据记录要全部出现在结果集中同时给出符合连接条件a.cardno=b.cardno)的表demo.membermaster中的字段membername的值。
我们也可以使用RIGHT JOIN实现同样的效果代码如下
```
SELECT
a.transactionno,
a.itemnumber,
a.quantity,
a.price,
a.transdate,
b.membername
FROM
demo.membermaster AS b
RIGHT JOIN
demo.trans AS a ON (a.cardno = b.cardno); -- RIGHT JOIN顺序颠倒了还是以demo.trans为主
```
其实,这里就是把顺序颠倒了一下,意思是一样的。运行之后,我们都能得到下面的结果:
```
mysql&gt; SELECT
-&gt; a.transactionno,
-&gt; a.itemnumber,
-&gt; a.quantity,
-&gt; a.price,
-&gt; a.transdate,
-&gt; b.membername
-&gt; FROM
-&gt; demo.trans AS a
-&gt; LEFT JOIN -- 左连接
-&gt; demo.membermaster AS b ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate | membername |
+---------------+------------+----------+-------+---------------------+------------+
| 1 | 1 | 1.000 | 89.00 | 2020-12-01 00:00:00 | 张三 |
| 2 | 2 | 1.000 | 12.00 | 2020-12-02 00:00:00 | NULL |
+---------------+------------+----------+-------+---------------------+------------+
2 rows in set (0.00 sec)
mysql&gt; SELECT
-&gt; a.transactionno,
-&gt; a.itemnumber,
-&gt; a.quantity,
-&gt; a.price,
-&gt; a.transdate,
-&gt; b.membername
-&gt; FROM
-&gt; demo.membermaster AS b
-&gt; RIGHT JOIN -- 右连接
-&gt; demo.trans AS a
-&gt; ON (a.cardno = b.cardno);
+---------------+------------+----------+-------+---------------------+------------+
| transactionno | itemnumber | quantity | price | transdate | membername |
+---------------+------------+----------+-------+---------------------+------------+
| 1 | 1 | 1.000 | 89.00 | 2020-12-01 00:00:00 | 张三 |
| 2 | 2 | 1.000 | 12.00 | 2020-12-02 00:00:00 | NULL |
+---------------+------------+----------+-------+---------------------+------------+
2 rows in set (0.00 sec)
```
通过关联查询,销售流水数据里就补齐了会员的名称,我们也就获取到了需要的数据。
## 关联查询的误区
有了连接我们就可以进行2个表的关联查询了。你可能会有疑问关联查询必须在外键约束的基础上才可以吗
其实在MySQL中外键约束不是关联查询的必要条件。很多人往往在设计表的时候觉得只要连接查询就可以搞定一切了外键约束太麻烦没有必要。如果你这么想就进入了一个误区。
下面我就以超市进货的例子,来实际说明一下,为什么这种思路不对。
假设一次进货数据是这样的供货商编号是1进货仓库编号是1。我们进货的商品编号是1234进货数量是1进货价格是10进货金额是10。
我先插入单头数据:
```
INSERT INTO demo.importhead
(
listnumber,
supplierid,
stocknumber
)
VALUES
(
1234,
1,
1
);
```
运行成功后,查看一下表的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.importhead;
+------------+------------+-------------+------------+----------+-------------+-------------+
| listnumber | supplierid | stocknumber | importtype | quantity | importprice | importvalue |
+------------+------------+-------------+------------+----------+-------------+-------------+
| 1234 | 1 | 1 | 1 | NULL | NULL | NULL |
+------------+------------+-------------+------------+----------+-------------+-------------+
1 row in set (0.00 sec)
```
可以看到我们有了一个进货单头单号是1234供货商是1号供货商进货仓库是1号仓库。
接着,我们向进货单明细表中插入进货明细数据:
```
INSERT INTO demo.importdetails
(
listnumber,
itemnumber,
quantity,
importprice,
importvalue
)
VALUES
(
1234,
1,
1,
10,
10
);
```
运行成功,查看一下表的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.importdetails;
+------------+------------+----------+-------------+-------------+
| listnumber | itemnumber | quantity | importprice | importvalue |
+------------+------------+----------+-------------+-------------+
| 1234 | 1 | 1.000 | 10.00 | 10.00 |
+------------+------------+----------+-------------+-------------+
1 row in set (0.00 sec)
```
这样我们就有了1234号进货单的明细数据进货商品是1号商品进货数量是1个进货价格是10元进货金额是10元。
这个时候,如果我删除进货单头表的数据,就会出现只有明细、没有单头的数据缺失情况。我们来看看会发生什么:
```
DELETE FROM demo.importhead
WHERE listnumbere = 1234;
```
运行这条语句MySQL会提示错误因为数据删除违反了外键约束。看到了吗MySQL阻止了数据不一致的情况出现。
不知道你有没有注意我插入数据的顺序为什么我要先插入进货单头表的数据再插入进货单明细表的数据呢其实这是因为如果我先插入数据到从表也就是进货单明细表会导致MySQL找不到参照的主表信息会提示错误因为添加数据违反了外键约束。
你可能会不以为然,觉得按照信息系统的操作逻辑,生成一张进货单的时候,一定是先生成单头,再插入明细。同样,删除一张进货单的时候,一定是先删除明细,再删除单头。
要是你这么想可能就会“中招”了。原因很简单既然我们把进货数据拆成了2个表这就决定了无论是数据添加还是数据删除都不能通过一条SQL语句实现。实际工作中什么突发情况都是有可能发生的。你认为一定会完成的操作完全有可能只执行了一部分。
我们曾经就遇到过这么一个问题:用户月底盘点的时候,盘点单无法生成,系统提示“有未处理的进货单”。经过排查,发现是进货单数据发生了数据缺失,明细数据还在,对应的单头数据却被删除了。我们反复排查之后,才发现是缺少了防止数据缺失的机制。最后通过定义外键约束,解决了这个问题。
所以虽然你不用外键约束也可以进行关联查询但是有了它MySQL系统才会保护你的数据避免出现误删的情况从而提高系统整体的可靠性。
现在来回答另外一个问题为什么在MySQL里没有外键约束也可以进行关联查询呢原因是外键约束是有成本的需要消耗系统资源。对于大并发的SQL操作有可能会不适合。比如大型网站的中央数据库可能会因为外键约束的系统开销而变得非常慢。所以MySQL允许你不使用系统自带的外键约束在应用层面完成检查数据一致性的逻辑。也就是说即使你不用外键约束也要想办法通过应用层面的附加逻辑来实现外键约束的功能确保数据的一致性。
## 总结
这节课,我给你介绍了如何进行多表查询,我们重点学习了外键和连接。
外键约束可以帮助我们确定从表中的外键字段与主表中的主键字段之间的引用关系还可以确保从表中数据所引用的主表数据不会被删除从而保证了2个表中数据的一致性。
连接可以帮助我们对2个相关的表进行连接查询从2个表中获取需要的信息。左连接表示连接以左边的表为主结果集中要包括左边表中的所有记录右连接表示连接以右边的表为主结果集中要包括右边表中的所有记录。
我汇总了常用的SQL语句你一定要重点掌握。
```
-- 定义外键约束:
CREATE TABLE 从表名
(
字段 字段类型
....
CONSTRAINT 外键约束名称
FOREIGN KEY (字段名) REFERENCES 主表名 (字段名称)
);
ALTER TABLE 从表名 ADD CONSTRAINT 约束名 FOREIGN KEY 字段名 REFERENCES 主表名 (字段名);
-- 连接查询
SELECT 字段名
FROM 表名 AS a
JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
SELECT 字段名
FROM 表名 AS a
LEFT JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
SELECT 字段名
FROM 表名 AS a
RIGHT JOIN 表名 AS b
ON (a.字段名称=b.字段名称);
```
刚开始学习MySQL的同学很容易忽略在关联表中定义外键约束的重要性从而导致数据缺失影响系统的可靠性。我建议你尽量养成在关联表中定义外键约束的习惯。不过如果你的业务场景因为高并发等原因无法承担外键约束的成本也可以不定义外键约束但是一定要在应用层面实现外键约束的逻辑功能这样才能确保系统的正确可靠。
## 思考题
如果你的业务场景因高并发等原因,不能使用外键约束,在这种情况下,你怎么在应用层面确保数据的一致性呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,503 @@
<audio id="audio" title="07 | 条件语句WHERE 与 HAVING有什么不同?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/6f/b7c2af698e89790c1500c1a9f964856f.mp3"></audio>
你好,我是朱晓峰。
我们在进行查询的时候经常需要按条件对查询结果进行筛选这就要用到条件语句WHERE和HAVING了。
WHERE是直接对表中的字段进行限定来筛选结果HAVING则需要跟分组关键字GROUP BY一起使用通过对分组字段或分组计算函数进行限定来筛选结果。虽然它们都是对查询进行限定却有着各自的特点和适用场景。很多时候我们会遇到2个都可以用的情况。一旦用错就很容易出现执行效率低下、查询结果错误甚至是查询无法运行的情况。
下面我就借助项目实施过程中的实际需求给你讲讲WHERE和HAVING分别是如何对查询结果进行筛选的以及它们各自的优缺点来帮助你正确地使用它们使你的查询不仅能够得到正确的结果还能占用更少的资源并且速度更快。
## 一个实际查询需求
超市的经营者提出要查单笔销售金额超过50元的商品。我们来分析一下这个需求需要查询出一个商品记录集限定条件是单笔销售金额超过50元。这个时候我们就需要用到WHERE和HAVING了。
这个问题的条件很明确,查询的结果也只有“商品”一个字段,好像很容易实现。
假设我们有一个这样的商品信息表demo.goodsmaster里面有2种商品书和笔。
```
mysql&gt; SELECT *
-&gt; FROM demo.goodsmaster;
+------------+---------+-----------+---------------+------+------------+
| itemnumber | barcode | goodsname | specification | unit | salesprice |
+------------+---------+-----------+---------------+------+------------+
| 1 | 0001 | 书 | | 本 | 89.00 |
| 2 | 0002 | 笔 | | 支 | 5.00 |
+------------+---------+-----------+---------------+------+------------+
2 rows in set (0.00 sec)
```
同时我们还有一个商品销售明细表demo.transactiondetails里面有4条销售记录
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | 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 |
+---------------+------------+----------+-------+------------+
4 rows in set (0.01 sec)
```
接下来我们分别用WHERE和HAVING进行查询看看它们各自是如何查询的是否能够得到正确的结果。
第一步用WHERE关键字进行查询
```
mysql&gt; SELECT DISTINCT b.goodsname
-&gt; FROM demo.transactiondetails AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; WHERE a.salesvalue &gt; 50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
第二步用HAVING关键字进行查询
```
mysql&gt; SELECT b.goodsname
-&gt; FROM demo.transactiondetails AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; GROUP BY b.goodsname
-&gt; HAVING max(a.salesvalue)&gt;50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
可以发现两次查询的结果是一样的。那么这两种查询到底有什么区别哪个更好呢要弄明白这个问题我们要先学习下WHERE和HAVING的执行过程。
## WHERE
我们先来分析一下刚才使用WHERE条件的查询语句来看看MySQL是如何执行这个查询的。
首先MySQL从数据表demo.transactiondetails中抽取满足条件“a.salesvalue&gt;50”的记录
```
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails AS a
-&gt; WHERE a.salesvalue &gt; 50;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | salesvalue |
+---------------+------------+----------+-------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 |
+---------------+------------+----------+-------+------------+
2 rows in set (0.00 sec)
```
为了获取到销售信息所对应的商品名称我们需要通过公共字段“itemnumber”与数据表demo.goodsmaster进行关联从demo.goodsmaster中获取商品名称
```
mysql&gt; SELECT
-&gt; a.*, b.goodsname
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.goodsmaster b ON (a.itemnumber = b.itemnumber)
-&gt; WHERE
-&gt; a.salesvalue &gt; 50;
+---------------+------------+----------+-------+------------+-----------+
| transactionid | itemnumber | quantity | price | salesvalue | goodsname |
+---------------+------------+----------+-------+------------+-----------+
| 1 | 1 | 1.000 | 89.00 | 89.00 | 书 |
| 2 | 1 | 2.000 | 89.00 | 178.00 | 书 |
+---------------+------------+----------+-------+------------+-----------+
2 rows in set (0.00 sec)
```
这个时候,如果查询商品名称,就会出现两个重复的记录:
```
mysql&gt; SELECT
-&gt; b.goodsname
-&gt; FROM
-&gt; demo.transactiondetails AS a
-&gt; JOIN
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; WHERE
-&gt; a.salesvalue &gt; 50;
+-----------+
| goodsname |
+-----------+
| 书 |
| 书 |
+-----------+
2 rows in set (0.00 sec)
```
需要注意的是为了消除重复的语句这里我们需要用到一个关键字DISTINCT它的作用是返回唯一不同的值。比如DISTINCT 字段1就表示返回所有字段1的不同的值。
下面我们尝试一下加上DISTINCT关键字的查询
```
mysql&gt; SELECT
-&gt; DISTINCT(b.goodsname) -- 返回唯一不同的值
-&gt; FROM
-&gt; demo.transactiondetails AS a
-&gt; JOIN
-&gt; demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
-&gt; WHERE
-&gt; a.salesvalue &gt; 50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
这样我们就得到了需要的结果单笔销售金额超过50元的商品就是“书”。
总之WHERE关键字的特点是直接用表的字段对数据集进行筛选。如果需要通过关联查询从其他的表获取需要的信息那么执行的时候也是先通过WHERE条件进行筛选用筛选后的比较小的数据集进行连接。这样一来连接过程中占用的资源比较少执行效率也比较高。
## HAVING
讲完了WHERE我们再说说HAVING是如何执行的。不过在这之前我要先给你介绍一下GROUP BY因为HAVING不能单独使用必须要跟GROUP BY一起使用。
我们可以把GROUP BY理解成对数据进行分组方便我们对组内的数据进行统计计算。
下面我举个小例子具体讲一讲GROUP BY如何使用以及如何在分组里面进行统计计算。
假设现在有一组销售数据,我们需要从里面查询每天、每个收银员的销售数量和销售金额。我们通过下面的代码,来查看一下数据的内容:
```
mysql&gt; SELECT *
-&gt; FROM demo.transactionhead;
+---------------+------------------+------------+---------------------+
| transactionid | transactionno | operatorid | transdate |
+---------------+------------------+------------+---------------------+
| 1 | 0120201201000001 | 1 | 2020-12-10 00:00:00 |
| 2 | 0120201202000001 | 2 | 2020-12-11 00:00:00 |
| 3 | 0120201202000002 | 2 | 2020-12-12 00:00:00 |
+---------------+------------------+------------+---------------------+
3 rows in set (0.00 sec)
mysql&gt; SELECT *
-&gt; FROM demo.transactiondetails;
+---------------+------------+----------+-------+------------+
| transactionid | itemnumber | quantity | price | 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 |
+---------------+------------+----------+-------+------------+
4 rows in set (0.01 sec)
mysql&gt; SELECT *
-&gt; FROM demo.operator;
+------------+----------+--------+--------------+-------------+---------+--------------------+--------+
| operatorid | branchid | workno | operatorname | phone | address | pid | duty |
+------------+----------+--------+--------------+-------------+---------+--------------------+--------+
| 1 | 1 | 001 | 张静 | 18612345678 | 北京 | 110392197501012332 | 店长 |
| 2 | 1 | 002 | 李强 | 13312345678 | 北京 | 110222199501012332 | 收银员 |
+------------+----------+--------+--------------+-------------+---------+--------------------+--------+
2 rows in set (0.01 sec)
mysql&gt; SELECT
-&gt; a.transdate, -- 交易时间
-&gt; c.operatorname,-- 操作员
-&gt; d.goodsname, -- 商品名称
-&gt; b.quantity, -- 销售数量
-&gt; b.price, -- 价格
-&gt; b.salesvalue -- 销售金额
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; JOIN
-&gt; demo.goodsmaster AS d ON (b.itemnumber = d.itemnumber);
+---------------------+--------------+-----------+----------+-------+------------+
| transdate | operatorname | goodsname | quantity | price | salesvalue |
+---------------------+--------------+-----------+----------+-------+------------+
| 2020-12-10 00:00:00 | 张静 | 书 | 1.000 | 89.00 | 89.00 |
| 2020-12-10 00:00:00 | 张静 | 笔 | 2.000 | 5.00 | 10.00 |
| 2020-12-11 00:00:00 | 李强 | 书 | 2.000 | 89.00 | 178.00 |
| 2020-12-12 00:00:00 | 李强 | 笔 | 10.000 | 5.00 | 50.00 |
+---------------------+--------------+-----------+----------+-------+------------+
4 rows in set (0.00 sec)
```
如果我想看看每天的销售数量和销售金额可以按照一个字段“transdate”对数据进行分组和统计
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; SUM(b.quantity), -- 统计分组的总计销售数量
-&gt; SUM(b.salesvalue) -- 统计分组的总计销售金额
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; GROUP BY a.transdate;
+---------------------+-----------------+-------------------+
| transdate | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+-----------------+-------------------+
| 2020-12-10 00:00:00 | 3.000 | 99.00 |
| 2020-12-11 00:00:00 | 2.000 | 178.00 |
| 2020-12-12 00:00:00 | 10.000 | 50.00 |
+---------------------+-----------------+-------------------+
3 rows in set (0.00 sec)
```
如果我想看每天、每个收银员的销售数量和销售金额就可以按2个字段进行分组和统计分别是“transdate”和“operatorname”
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; SUM(b.quantity), -- 数量求和
-&gt; SUM(b.salesvalue)-- 金额求和
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS C ON (a.operatorid = c.operatorid)
-&gt; GROUP BY a.transdate , c.operatorname; -- 按照交易日期和操作员分组
+---------------------+--------------+-----------------+-------------------+
| transdate | operatorname | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+--------------+-----------------+-------------------+
| 2020-12-10 00:00:00 | 张静 | 3.000 | 99.00 |
| 2020-12-11 00:00:00 | 李强 | 2.000 | 178.00 |
| 2020-12-12 00:00:00 | 李强 | 10.000 | 50.00 |
+---------------------+--------------+-----------------+-------------------+
3 rows in set (0.00 sec)
```
可以看到,通过对销售数据按照交易日期和收银员进行分组,再对组内数据进行求和统计,就实现了对每天、每个收银员的销售数量和销售金额的查询。
好了知道了GROUP BY的使用方法我们就来学习下HAVING。
回到开头的超市经营者的需求查询单笔销售金额超过50元的商品。现在我们来使用HAVING来实现代码如下
```
mysql&gt; SELECT b.goodsname
-&gt; FROM demo.transactiondetails AS a
-&gt; JOIN demo.goodsmaster AS b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; GROUP BY b.goodsname
-&gt; HAVING max(a.salesvalue)&gt;50;
+-----------+
| goodsname |
+-----------+
| 书 |
+-----------+
1 row in set (0.00 sec)
```
这种查询方式在MySQL里面是分四步实现的。
第一步把流水明细表和商品信息表通过公共字段“itemnumber”连接起来从2个表中获取数据
```
mysql&gt; SELECT
-&gt; a.*, b.*
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.goodsmaster b ON (a.itemnumber = b.itemnumber);
+---------------+------------+----------+-------+------------+------------+---------+-----------+---------------+------+------------+
| transactionid | itemnumber | quantity | price | salesvalue | itemnumber | barcode | goodsname | specification | unit | salesprice |
+---------------+------------+----------+-------+------------+------------+---------+-----------+---------------+------+------------+
| 1 | 1 | 1.000 | 89.00 | 89.00 | 1 | 0001 | 书 | NULL | 本 | 89.00 |
| 1 | 2 | 2.000 | 5.00 | 10.00 | 2 | 0002 | 笔 | NULL | 支 | 5.00 |
| 2 | 1 | 2.000 | 89.00 | 178.00 | 1 | 0001 | 书 | NULL | 本 | 89.00 |
| 3 | 2 | 10.000 | 5.00 | 50.00 | 2 | 0002 | 笔 | NULL | 支 | 5.00 |
+---------------+------------+----------+-------+------------+------------+---------+-----------+---------------+------+------------+
4 rows in set (0.00 sec)
```
查询的结果有点复杂,为了方便你理解,我对结果进行了分类,并加了注释,如下图所示:
<img src="https://static001.geekbang.org/resource/image/5a/33/5a65e30972010a72576d4008fb0b9333.jpg" alt="">
第二步,把结果集按照商品名称分组,分组的示意图如下所示:
组1
<img src="https://static001.geekbang.org/resource/image/23/10/239766d1849b25d03049be4f21c95510.jpg" alt="">
组2
<img src="https://static001.geekbang.org/resource/image/2d/ca/2d4b2fec2c3b84a25928f21353727eca.jpeg" alt="">
第三步对分组后的数据集进行筛选把组中字段“salesvalue”的最大值&gt;50的组筛选出来。筛选后的结果集如下所示
<img src="https://static001.geekbang.org/resource/image/96/a6/96bdae61f5924a9118071294cab86ba6.jpeg" alt="">
第四步返回商品名称。这时我们就得到了需要的结果单笔销售金额超过50元的商品就是“书”。
现在我们来简单小结下使用HAVING的查询过程。首先我们要把所有的信息都准备好包括从关联表中获取需要的信息对数据集进行分组形成一个包含所有需要的信息的数据集合。接着再通过HAVING条件的筛选得到需要的数据。
## 怎么正确地使用WHERE和HAVING
现在你已经知道了WHERE和HAVING的具体使用方法。那么在查询时我们怎样才能正确地使用它们呢
首先你要知道它们的2个典型区别。
第一个区别是,**如果需要通过连接从关联表中获取需要的数据WHERE是先筛选后连接而HAVING是先连接后筛选**。
这一点就决定了在关联查询中WHERE比HAVING更高效。因为WHERE可以先筛选用一个筛选后的较小数据集和关联表进行连接这样占用的资源比较少执行效率也就比较高。HAVING则需要先把结果集准备好也就是用未被筛选的数据集进行关联然后对这个大的数据集进行筛选这样占用的资源就比较多执行效率也较低。
第二个区别是WHERE可以直接使用表中的字段作为筛选条件但不能使用分组中的计算函数作为筛选条件HAVING必须要与GROUP BY配合使用可以把分组计算的函数和分组字段作为筛选条件。
这决定了,**在需要对数据进行分组统计的时候HAVING可以完成WHERE不能完成的任务**。这是因为在查询语法结构中WHERE在GROUP BY之前所以无法对分组结果进行筛选。HAVING在GROUP BY之后可以使用分组字段和分组中的计算函数对分组的结果集进行筛选这个功能是WHERE无法完成的。
这么说你可能不太好理解我来举个小例子。假如超市经营者提出要查询一下是哪个收银员、在哪天卖了2单商品。这种必须先分组才能筛选的查询用WHERE语句实现就比较难我们可能要分好几步通过把中间结果存储起来才能搞定。但是用HAVING则很轻松代码如下
```
mysql&gt; SELECT
-&gt; a.transdate, c.operatorname
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; GROUP BY a.transdate,c.operatorname
-&gt; HAVING count(*)=2; -- 销售了2单
+---------------------+--------------+
| transdate | operatorname |
+---------------------+--------------+
| 2020-12-10 00:00:00 | 张静 |
+---------------------+--------------+
1 row in set (0.01 sec)
```
我汇总了WHERE和HAVING各自的优缺点如下图所示
<img src="https://static001.geekbang.org/resource/image/24/50/2423421554df9a7dfd15495beb850150.jpg" alt="">
不过需要注意的是WHERE和HAVING也不是互相排斥的我们可以在一个查询里面同时使用WHERE和HAVING。
举个例子假设现在我们有一组销售数据包括交易时间、收银员、商品名称、销售数量、价格和销售金额等信息超市的经营者要查询“2020-12-10”和“2020-12-11”这两天收银金额超过100元的销售日期、收银员名称、销售数量和销售金额。
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; d.goodsname,
-&gt; b.quantity,
-&gt; b.price,
-&gt; b.salesvalue
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; JOIN
-&gt; demo.goodsmaster as d on (b.itemnumber=d.itemnumber);
+---------------------+--------------+-----------+----------+-------+------------+
| transdate | operatorname | goodsname | quantity | price | salesvalue |
+---------------------+--------------+-----------+----------+-------+------------+
| 2020-12-10 00:00:00 | 张静 | 书 | 1.000 | 89.00 | 89.00 |
| 2020-12-10 00:00:00 | 张静 | 笔 | 2.000 | 5.00 | 10.00 |
| 2020-12-11 00:00:00 | 李强 | 书 | 2.000 | 89.00 | 178.00 |
| 2020-12-12 00:00:00 | 李强 | 笔 | 10.000 | 5.00 | 50.00 |
+---------------------+--------------+-----------+----------+-------+------------+
4 rows in set (0.00 sec)
```
我们来分析一下这个需求由于是要按照销售日期和收银员进行统计所以必须按照销售日期和收银员进行分组因此我们可以通过使用GROUP BY和HAVING进行查询
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; SUM(b.quantity), -- 销售数量求和
-&gt; SUM(b.salesvalue)-- 销售金额求和
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; GROUP BY a.transdate , operatorname -- 按照日期、收银员分组
-&gt; HAVING a.transdate IN ('2020-12-10' , '2020-12-11')
-&gt; AND SUM(b.salesvalue) &gt; 100; -- 最后筛选数据
+---------------------+--------------+-----------------+-------------------+
| transdate | operatorname | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+--------------+-----------------+-------------------+
| 2020-12-11 00:00:00 | 李强 | 2.000 | 178.00 |
+---------------------+--------------+-----------------+-------------------+
1 row in set (0.00 sec)
```
如果你仔细看HAVING后面的筛选条件就会发现条件a.transdate IN ('2020-12-10' , '2020-12-11')其实可以用WHERE来限定。我们把查询改一下试试
```
mysql&gt; SELECT
-&gt; a.transdate,
-&gt; c.operatorname,
-&gt; SUM(b.quantity),
-&gt; SUM(b.salesvalue)
-&gt; FROM
-&gt; demo.transactionhead AS a
-&gt; JOIN
-&gt; demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.operator AS c ON (a.operatorid = c.operatorid)
-&gt; WHERE a.transdate in ('2020-12-12','2020-12-11') -- 先按日期筛选
-&gt; GROUP BY a.transdate , operatorname
-&gt; HAVING SUM(b.salesvalue)&gt;100; -- 后按金额筛选
+---------------------+--------------+-----------------+-------------------+
| transdate | operatorname | SUM(b.quantity) | SUM(b.salesvalue) |
+---------------------+--------------+-----------------+-------------------+
| 2020-12-11 00:00:00 | 李强 | 2.000 | 178.00 |
+---------------------+--------------+-----------------+-------------------+
1 row in set (0.00 sec)
```
很显然我们同样得到了需要的结果。这是因为我们把条件拆分开包含分组统计函数的条件用HAVING普通条件用WHERE。这样我们就既利用了WHERE条件的高效快速又发挥了HAVING可以使用包含分组统计函数的查询条件的优点。当数据量特别大的时候运行效率会有很大的差别。
## 总结
今天我给你介绍了条件语句WHERE和HAVING在MySQL中的执行原理。WHERE可以先按照条件对数据进行筛选然后进行数据连接所以效率更高。HAVING可以在分组之后通过使用分组中的计算函数实现WHERE难以完成的数据筛选。
了解了WHERE和HAVING各自的特点我们就可以在查询中充分利用它们的优势更高效地实现我们的查询目标。
最后我想提醒你的是很多人刚开始学习MySQL的时候不太喜欢用HAVING一提到条件语句就想当然地用WHERE。其实HAVING是非常有用的特别是在做一些复杂的统计查询的时候经常要用到分组这个时候HAVING就派上用场了。
当然你也可以不用HAVING而是把查询分成几步把中间结果存起来再用WHERE筛选或者干脆把这部分筛选功能放在应用层面用代码来实现。但是这样做的效率很低而且会增加工作量加大维护成本。所以学会使用HAVING对你完成复杂的查询任务非常有帮助。
## 思考题
有这样一种说法HAVING后面的条件必须是包含分组中的计算函数的条件你觉得对吗为什么
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,345 @@
<audio id="audio" title="08 | 聚合函数:怎么高效地进行分组统计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/c2/3df24cf968e41eed5dcd06c9e1e9eec2.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊聚合函数。
MySQL中有5种聚合函数较为常用分别是求和函数SUM()、求平均函数AVG()、最大值函数MAX()、最小值函数MIN()和计数函数COUNT()。接下来,我就结合超市项目的真实需求,来带你掌握聚合函数的用法,帮你实现高效的分组统计。
咱们的项目需求是这样的超市经营者提出他们需要统计某个门店每天、每个单品的销售情况包括销售数量和销售金额等。这里涉及3个数据表具体信息如下所示
销售明细表demo.transactiondetails)
<img src="https://static001.geekbang.org/resource/image/ba/4e/ba86b64760c96caf85872f362790534e.jpeg" alt="">
销售单头表demo.transactionhead)
<img src="https://static001.geekbang.org/resource/image/32/50/3242decd05814f16479f2e6edb5fd050.jpeg" alt="">
商品信息表demo.goodsmaster
<img src="https://static001.geekbang.org/resource/image/d7/1a/d72f0fb930280cb611d8f31aed98bf1a.jpeg" alt="">
要统计销售就要用到数据求和那么我们就先来学习下求和函数SUM()。
## SUM
SUM函数可以返回指定字段值的和。我们可以用它来获得用户某个门店每天每种商品的销售总计数据
```
mysql&gt; SELECT
-&gt; LEFT(b.transdate, 10), -- 从关联表获取交易时间并且通过LEFT函数获取交易时间字符串的左边10个字符得到年月日的数据
-&gt; c.goodsname, -- 从关联表获取商品名称
-&gt; SUM(a.quantity), -- 数量求和
-&gt; SUM(a.salesvalue) -- 金额求和
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (a.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(b.transdate, 10) , c.goodsname -- 分组
-&gt; 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**:表示按照指定的字段排序。超市经营者指定按照日期和单品统计,那么,统计的结果按照交易日期和商品名称的顺序排序,会更加清晰。
知道了2个关键字之后刚刚的查询就容易理解了。接下来我们就再拆解一下看看这个查询是如何执行的。我用图表来直观地演示一下各个步骤。
第一步完成3个表的连接由于字段比较多为了你理解我省略了一些在这一步不重要的字段
<img src="https://static001.geekbang.org/resource/image/95/a7/953cd3d7199a36bf070e1a481a852da7.jpeg" alt="">
第二步对结果集按照交易时间和商品名称进行分组我们可以分成下面4组。
第一组:
<img src="https://static001.geekbang.org/resource/image/3a/96/3a9ee51c76802f3dd204c4b680548096.jpeg" alt="">
第二组
<img src="https://static001.geekbang.org/resource/image/1c/e5/1c99979d20f62a22265bd479365b91e5.jpeg" alt="">
第三组
<img src="https://static001.geekbang.org/resource/image/61/85/61c37518b1a8dac6d33e6e85bdc53385.jpeg" alt="">
第四组
<img src="https://static001.geekbang.org/resource/image/52/96/52b8bebfb9bd9ed4e866ceb2a9cef796.jpeg" alt="">
第三步,对各组的销售数量和销售金额进行统计,并且按照交易日期和商品名称排序。这样就得到了我们需要的结果,如下所示:
```
+-----------------------+-----------+-----------------+-------------------+
| 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)
```
如果用户需要知道全部商品销售的总计数量和总计金额我们也可以把数据集的整体看作一个分组进行计算。这样就不需要分组关键字GROUP BY以及排序关键字ORDER BY了。你甚至不需要从关联表中获取数据也就不需要连接了。就像下面这样
```
mysql&gt; SELECT
-&gt; SUM(quantity), -- 总计数量
-&gt; SUM(salesvalue)-- 总计金额
-&gt; FROM
-&gt; demo.transactiondetails;
+---------------+-----------------+
| SUM(quantity) | SUM(salesvalue) |
+---------------+-----------------+
| 27.000 | 639.00 |
+---------------+-----------------+
1 row in set (0.05 sec)
```
到这里呢求和函数SUM()的使用方法我就讲完了。需要提醒你的是,求和函数获取的是分组中的合计数据,所以你要对分组的结果有准确的把握,否则就很容易搞错。这也就是说,你要知道是按什么字段进行分组的。如果是按多个字段分组,你要知道字段之间有什么样的层次关系;如果是按照以字段作为变量的某个函数进行分组的,你要知道这个函数的返回值是什么,返回值又是如何影响分组的等。
## AVG、MAX和MIN
接下来我们来计算一下分组中数据的平均值、最大值和最小值。这个时候就要用到AVG()、MAX()和MIN()了。
1.AVG
首先我们来学习下计算平均值的函数AVG。它的作用是通过计算分组内指定字段值的和以及分组内的记录数算出分组内指定字段的平均值。
举个例子如果用户需要计算每天、每种商品平均一次卖出多少个、多少钱这个时候我们就可以用到AVG函数了如下所示
```
mysql&gt; SELECT
-&gt; LEFT(a.transdate, 10),
-&gt; c.goodsname,
-&gt; AVG(b.quantity), -- 平均数量
-&gt; AVG(b.salesvalue) -- 平均金额
-&gt; FROM
-&gt; demo.transactionhead a
-&gt; JOIN
-&gt; demo.transactiondetails b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(a.transdate,10),c.goodsname
-&gt; 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)
```
2.MAX和MIN
MAX()表示获取指定字段在分组中的最大值MIN()表示获取指定字段在分组中的最小值。它们的实现原理差不多下面我就重点讲一下MAX()知道了它的用法MIN()也就很好理解了。
我们还是来看具体的例子。假如用户要求计算每天里的一次销售的最大数量和最大金额,就可以用下面的代码,得到我们需要的结果:
```
mysql&gt; SELECT
-&gt; LEFT(a.transdate, 10),
-&gt; MAX(b.quantity), -- 数量最大值
-&gt; MAX(b.salesvalue) -- 金额最大值
-&gt; FROM
-&gt; demo.transactionhead a
-&gt; JOIN
-&gt; demo.transactiondetails b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(a.transdate,10)
-&gt; 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是分别计算的。下面我们就来分析一下刚刚的查询。
查询中用到3个相互关联的表销售流水明细表、销售流水单头表和商品信息表。这3个表连接完成之后MySQL进行了分组。我用图示的办法给你展示出来
第一组
<img src="https://static001.geekbang.org/resource/image/3b/8a/3ba226e73b81a02a294ab83c7yy0d68a.jpeg" alt="">
第二组
<img src="https://static001.geekbang.org/resource/image/6e/2d/6e50732ff2c59199abbea6381d083a2d.jpeg" alt="">
在第一组中最大数量出现在第2条记录是5最大金额出现在第1条记录是178。同样道理在第二组中最大数量出现在第4条记录是10最大金额则出现在第1条记录是267。
所以MAX字段这个函数返回分组集中最大的那个值。如果你要查询 MAX字段1和MAX字段2而它们是相互独立、分别计算的你千万不要想当然地认为结果在同一条记录上。那样的话你就掉坑里了。
## COUNT
通过**COUNT**,我们可以了解数据集的大小,这对系统优化十分重要。
举个小例子,在项目实施的过程中,我们遇到了这么一个问题:由于用户的销售数据很多,而且每天都在增长,因此,在做销售查询的时候,经常会遇到卡顿的问题。这是因为,查询的数据量太大了,导致系统不得不花很多时间来处理数据,并给数据集分配资源,比如内存什么的。
怎么解决卡顿的问题呢?我们想到了一个分页的策略。
所谓的分页策略,其实就是,不把查询的结果一次性全部返回给客户端,而是根据用户电脑屏幕的大小,计算一屏可以显示的记录数,每次只返回用户电脑屏幕可以显示的数据集。接着,再通过翻页、跳转等功能按钮,实现查询目标的精准锁定。这样一来,每次查询的数据量较少,也就大大提高了系统响应速度。
这个策略能够实现的一个关键,就是要**计算出符合条件的记录一共有多少条**,之后才能计算出一共有几页、能不能翻页或跳转。
要计算记录数就要用到COUNT()函数了。这个函数有两种情况。
- COUNT*):统计一共有多少条记录;
- COUNT字段统计有多少个不为空的字段值。
1.COUNT(*)
如果COUNT*与GROUP BY一起使用就表示统计分组内有多少条数据。它也可以单独使用这就相当于数据集全体是一个分组统计全部数据集的记录数。
我举个小例子,假设我有个销售流水明细表如下:
```
mysql&gt; SELECT *
-&gt; 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行需要多少页才能显示完这个表的全部数据呢
```
mysql&gt; SELECT COUNT(*)
-&gt; FROM demo.transactiondetails;
+----------+
| COUNT(*) |
+----------+
| 6 |
+----------+
1 row in set (0.03 sec)
```
我们这里只有6条数据一屏就可以显示了所以一共1页。
那么,如果超市经营者想知道,每天、每种商品都有几次销售,我们就需要按天、按商品名称,进行分组查询:
```
mysql&gt; SELECT
-&gt; LEFT(a.transdate, 10), c.goodsname, COUNT(*) -- 统计销售次数
-&gt; FROM
-&gt; demo.transactionhead a
-&gt; JOIN
-&gt; demo.transactiondetails b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
-&gt; GROUP BY LEFT(a.transdate, 10) , c.goodsname
-&gt; 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)
```
运行这段代码,我们就得到了每天、每种商品有几次销售的全部结果。
2.COUNT字段
COUNT字段用来统计分组内这个字段的值出现了多少次。如果字段值是空就不统计。
为了说明它们的区别,我举个小例子。假设我们有这样的一个商品信息表,里面包括了商品编号、条码、名称、规格、单位和售价的信息。
```
mysql&gt; SELECT *
-&gt; 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次
```
mysql&gt; SELECT COUNT(goodsname) -- 统计商品名称字段
-&gt; FROM demo.goodsmaster;
+------------------+
| COUNT(goodsname) |
+------------------+
| 3 |
+------------------+
1 row in set (0.00 sec)
```
如果我们统计字段“specification”用COUNT(specification)结果是1次
```
mysql&gt; SELECT COUNT(specification) -- 统计规格字段
-&gt; FROM demo.goodsmaster;
+----------------------+
| COUNT(specification) |
+----------------------+
| 1 |
+----------------------+
1 row in set (0.00 sec)
```
你可能会问为啥计数字段“goodsname”的结果是3计数字段“specification”却只有1呢其实这里的原因就是3条记录里面的字段“goodsname”没有空值因此被统计了3次而字段“specification”有2个空值因此只统计了1次。
理解了这一点,你就可以利用计数函数对某个字段计数时,不统计空值的特点,对表中字段的非空值进行计数了。
## 总结
今天我们学习了聚合函数SUM、AVG、MAX、MIN和COUNT。我们在对分组数据进行统计的时候可以用这些函数来对分组数据求和、求平均值、最大值、最小值以及统计分组内的记录数或者分组内字段的值不为空的次数。
这些函数,为我们对数据库中的数据进行统计和计算提供了方便。因为计算直接在数据库中执行,比在应用层面完成相同的工作,效率高很多。
最后我还想多说一句不知道你注意到没有这节课我还提到了LEFT和ORDER BY。其实聚合函数可以和其他关键字、函数一起使用这样会拓展它的使用场景让原本复杂的计算变简单。所以我建议你不仅要认真学习这节课的聚合函数还要掌握MySQL的各种关键字的功能和用法并且根据实际工作的需要尝试把它们组合在一起使用这样就能利用好数据库的强大功能更好地满足用户的需求。
## 思考题
如果用户想要查询一下,在商品信息表中,到底是哪种商品的商品名称有重复,分别重复了几次,该如何查询呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,343 @@
<audio id="audio" title="09 | 时间函数时间类数据MySQL是怎么处理的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c3/98/c38dd356368fdd7ecec6b5579f447298.mp3"></audio>
你好我是朱晓峰。今天咱们来聊一聊MySQL的时间函数。
顾名思义,时间函数就是用来处理时间的函数。时间,几乎可以说是各类项目中都会存在的数据,项目需求不同,我们需要的时间函数也不一样,比如:
- 如果我们要统计一天之中不同时间段的销售情况就要获取时间值中的小时值这就会用到函数HOUR()
- 要计算与去年同期相比的增长率这就要计算去年同期的日期时间会用到函数DATE_ADD()
- 要计算今天是周几、有没有优惠活动这就要用到函数DAYOFWEEK()了;
- ……
这么多不同类型的时间函数,该怎么选择呢?这节课,我就结合不同的项目需求,来讲一讲不同的时间函数的使用方法,帮助你轻松地处理各类时间数据。
## 获取日期时间数据中部分信息的函数
我先举个小例子。超市的经营者提出,他们希望通过实际的销售数据,了解到一天当中什么时间段卖得好,什么时间段卖得不好,这样他们就可以根据不同时间的销售情况,合理安排商品陈列和人员促销,以实现收益最大化。
要达到这个目标,我们就需要统计一天中每小时的销售数量和销售金额。
这里涉及3组数据分别是销售单头表demo.transactionhead)、销售单明细表 (demo.transactiondetails)和商品信息表demo.goodsmaster为了便于你理解表的结构和表里的记录都是经过简化的
销售单头表包含了销售单的整体信息,包括流水单号、交易时间、收款机编号、会员编号和收银员编号等。
<img src="https://static001.geekbang.org/resource/image/92/ca/925490737131d75b38fe6af861c645ca.jpeg" alt="">
销售单明细表中保存的是交易明细数据,包括商品编号、销售数量、价格、销售金额等。
<img src="https://static001.geekbang.org/resource/image/fd/0b/fdfbf05544e36251b2784259fb3ca00b.jpeg" alt="">
商品信息表主要包括商品编号、条码、商品名称、规格、单位和售价。
<img src="https://static001.geekbang.org/resource/image/44/01/44f29d06fc689edd79e3fe81a39e2d01.jpeg" alt="">
需要注意的是,销售单明细表通过流水编号与销售单头表关联,其中流水编号是外键。通过流水编号,销售单明细表引用销售单头表里的交易时间、会员编号等信息,同时,通过商品编号与商品信息表关联,引用商品信息表里的商品名称等信息。
首先,我们来分析一下“统计一天中每小时的销售数量和销售金额”的这个需求。
要统计一天中每小时的销售情况实际上就是要把销售数据按照小时进行分组统计。那么解决问题的关键就是把交易时间的小时部分提取出来。这就要用到MySQL的日期时间处理函数EXTRACT和HOUR了。
为了获取小时的值我们要用到EXTRACT()函数。**EXTRACTtype FROM date表示从日期时间数据“date”中抽取“type”指定的部分**。
有了这个函数,我们就可以获取到交易时间的小时部分,从而完成一天中每小时的销售数量和销售金额的查询:
```
mysql&gt; SELECT
-&gt; EXTRACT(HOUR FROM b.transdate) AS 时段,
-&gt; SUM(a.quantity) AS 数量,
-&gt; SUM(a.salesvalue) AS 金额
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; GROUP BY EXTRACT(HOUR FROM b.transdate)
-&gt; ORDER BY EXTRACT(HOUR FROM b.transdate);
+------+--------+--------+
| 时段 | 数量 | 金额 |
+------+--------+--------+
| 9 | 16.000 | 500.00 |
| 10 | 11.000 | 139.00 |
| 11 | 10.000 | 30.00 |
| 12 | 40.000 | 200.00 |
| 13 | 5.000 | 445.00 |
| 15 | 6.000 | 30.00 |
| 17 | 1.000 | 3.00 |
| 18 | 2.000 | 178.00 |
| 19 | 2.000 | 6.00 |
+------+--------+--------+
9 rows in set (0.00 sec)
```
查询的过程是这样的:
1. 从交易时间中抽取小时信息EXTRACT(HOUR FROM b.transdate)
1. 按交易的小时信息分组;
1. 按分组统计销售数量和销售金额的和;
1. 按交易的小时信息排序。
这里我是用“HOUR”提取时间类型DATETIME中的小时信息同样道理你可以用“YEAR”获取年度信息用“MONTH”获取月份信息用“DAY”获取日的信息。如果你需要获取其他时间部分的信息可以参考下[时间单位](https://dev.mysql.com/doc/refman/8.0/en/expressions.html#temporal-intervals)。
这个查询我们也可以通过使用日期时间函数HOUR()来达到同样的效果。**HOURtime表示从日期时间“time”中获取小时部分信息**。
需要注意的是EXTRACT()函数中的“HOUR”表示要获取时间的类型而HOUR()是一个函数HOUR(time)可以单独使用表示返回time的小时部分信息。
我们可以通过在代码中把EXTRACT函数改成HOUR函数来实现相同的功能如下所示
```
mysql&gt; SELECT
-&gt; HOUR(b.transdate) AS 时段, -- 改为使用HOUR函数
-&gt; SUM(a.quantity) AS 数量,
-&gt; SUM(a.salesvalue) AS 金额
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; GROUP BY HOUR(b.transdate) -- 改写为HOUR函数
-&gt; ORDER BY HOUR(b.transdate);-- 改写为HOUR函数
+------+--------+--------+
| 时段 | 数量 | 金额 |
+------+--------+--------+
| 9 | 16.000 | 500.00 |
| 10 | 11.000 | 139.00 |
| 11 | 10.000 | 30.00 |
| 12 | 40.000 | 200.00 |
| 13 | 5.000 | 445.00 |
| 15 | 6.000 | 30.00 |
| 17 | 1.000 | 3.00 |
| 18 | 2.000 | 178.00 |
| 19 | 2.000 | 6.00 |
+------+--------+--------+
9 rows in set (0.00 sec)
```
除了获取小时信息我们往往还会遇到要统计年度信息、月度信息等情况MySQL也提供了支持的函数。
- YEARdate获取date中的年。
- MONTHdate获取date中的月。
- DAYdate获取date中的日。
- HOURdate获取date中的小时。
- MINUTEdate获取date中的分。
- SECONDdate获取date中的秒。
这些函数的使用方法和提取小时信息的方法一样,我就不多说了,你只要知道这些函数的含义就可以了,下面我再讲一讲计算日期时间的函数。
## 计算日期时间的函数
我先来介绍2个常用的MySQL的日期时间计算函数。
- DATE_ADDdate, INTERVAL 表达式 type表示计算从时间点“date”开始向前或者向后一段时间间隔的时间。“表达式”的值为时间间隔数正数表示向后负数表示向前“type”表示时间间隔的单位比如年、月、日等
- LAST_DAYdate表示获取日期时间“date”所在月份的最后一天的日期。
这两个函数怎么用呢接下来我还是借助咱们项目的实际需求来给你讲解下。假设今天是2020年12月10日超市经营者提出他们需要计算这个月单品销售金额的统计以及与去年同期相比的增长率。
这里的关键点是需要获取2019年12月的销售数据。因此计算2019年12月的起始和截止时间点就是查询的关键。这个时候就要用到计算日期时间函数了。
下面我重点讲解一下如何通过2个计算日期时间函数来计算2019年12月的起始时间和截止时间。
我们先来尝试获取2019年12月份的起始时间。
第一步用DATE_ADD函数获取到2020年12月10日上一年的日期2019年12月10日。
```
mysql&gt; SELECT DATE_ADD('2020-12-10', INTERVAL - 1 YEAR);
+-------------------------------------------+
| DATE_ADD('2020-12-10', INTERVAL - 1 YEAR) |
+-------------------------------------------+
| 2019-12-10 |
+-------------------------------------------+
1 row in set (0.00 sec)
```
第二步获取2019年12月10日这个时间节点开始上个月的日期这样做的目的是方便获取月份的起始时间
```
mysql&gt; SELECT DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH);
+------------------------------------------------------------------------+
| DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH) |
+------------------------------------------------------------------------+
| 2019-11-10 |
+------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
第三步获取2019年11月10日这个时间点月份的最后一天继续接近我们的目标2019年12月01日。
```
mysql&gt; SELECT LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH));
+----------------------------------------------------------------------------------+
| LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)) |
+----------------------------------------------------------------------------------+
| 2019-11-30 |
+----------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
到这里我们获得了2019年11月30日这个日期。你是不是觉得我们已经达到目的了呢要是这样的话你就错了。因为2019年11月30日可能会有销售的。如果用这个日期作为统计销售额的起始日期你就多算了这一天的销售。怎么办呢我们还要进行下一步。
第四步计算2019年11月30日后一天的日期
```
mysql&gt; SELECT DATE_ADD(LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)),INTERVAL 1 DAY);
+-----------------------------------------------------------------------------------------------------------+
| DATE_ADD(LAST_DAY(DATE_ADD(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR),INTERVAL - 1 MONTH)),INTERVAL 1 DAY) |
+-----------------------------------------------------------------------------------------------------------+
| 2019-12-01 |
+-----------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
你看我们终于获得了正确的起始日期2019年12月01日。
同样,我们可以用下面的方法,获得截止日期:
```
mysql&gt; SELECT DATE_ADD(LAST_DAY(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR)),INTERVAL 1 DAY);
+------------------------------------------------------------------------------+
| DATE_ADD(LAST_DAY(DATE_ADD('2020-12-10', INTERVAL - 1 YEAR)),INTERVAL 1 DAY) |
+------------------------------------------------------------------------------+
| 2020-01-01 |
+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
```
简单小结下我们可以用DATE_ADD()来计算从某个时间点开始过去或者未来一个时间间隔的时间通过LAST_DAY()函数,获得某个时间节点当月的最后一天的日期。借助它们,我们就可以获取从某个时间节点出发的指定月份的起始日期和截止日期。
除了DATE_ADD()ADDDATE()、DATE_SUB()和SUBDATE()也能达到同样的效果。
- ADDDATE()跟DATE_ADD()用法一致;
- DATE_SUB()SUBDATE()与DATE_ADD()用法类似,方向相反,执行日期的减操作。
## 其他日期时间函数
学习了刚刚的时间函数,我们已经可以应对大部分有关时间的场景了。但是这还不够,有的时候,我们还需要其他的日期时间信息,比如:
- 今天是几月几号,星期几;
- 两个时间点之间隔了几天;
- 把时间按照一定的格式显示出来;
- ……
这时就要用到其他日期时间函数了主要包括CURDATE()、DAYOFWEEK()、DATE_FORMAT和DATEDIFF()。
我来借助一个例子,具体解释下这些函数怎么用。
超市经营者为了吸引顾客经常要进行一些促销活动。具体来讲就是以周为单位按照周中不同的日期进行促销比如周一如何打折、周二如何打折、周末如何打折等。那么如何计算当天的价格呢我们来看下单品促销信息demo.discountrule
<img src="https://static001.geekbang.org/resource/image/cd/cf/cdc62927a60978e2128616fab3697fcf.jpeg" alt="">
这个表中的信息表示单品打折的时间和折扣率:
- 编号是1的商品周一、周三和周五打折折扣率分别是9折、75折和88折
- 编号是2的商品周二、周四和周六打折折扣率分别是5折、65折和8折。
- 周日所有商品打5折。
如果我们想要查到具体的价格我们首先要知道当前的日期以及今天是星期几。这就要用到2个MySQL的时间函数CURDATE和DAYOFWEEK
- CURDATE获取当前的日期。日期格式为“YYYY-MM-DD”也就是年月日的格式。
- DAYOFWEEKdate获取日期“date”是周几。1表示周日2表示周一以此类推直到7表示周六。
假设今天是2021年02月06日通过下面的代码我们就可以查到今天商品的全部折后价格了
```
mysql&gt; SELECT
-&gt; CURDATE() AS 日期,
-&gt; CASE DAYOFWEEK(CURDATE()) - 1 WHEN 0 THEN 7 ELSE DAYOFWEEK(CURDATE()) - 1 END AS 周几,
-&gt; a.goodsname AS 商品名称,
-&gt; a.salesprice AS 价格,
-&gt; IFNULL(b.discountrate,1) AS 折扣率,
-&gt; a.salesprice * IFNULL(b.discountrate, 1) AS 折后价格
-&gt; FROM
-&gt; demo.goodsmaster a
-&gt; LEFT JOIN
-&gt; demo.discountrule b ON (a.itemnumber = b.itemnumber
-&gt; AND CASE DAYOFWEEK(CURDATE()) - 1 WHEN 0 THEN 7 ELSE DAYOFWEEK(CURDATE()) - 1 END = b.weekday);
+------------+------+----------+-------+--------+----------+
| 日期 | 周几 | 商品名称 | 价格 | 折扣率 | 折后价格 |
+------------+------+----------+-------+--------+----------+
| 2021-02-06 | 6 | 书 | 89.00 | 1.00 | 89.0000 |
| 2021-02-06 | 6 | 笔 | 5.00 | 0.80 | 4.0000 |
| 2021-02-06 | 6 | 橡皮 | 3.00 | 1.00 | 3.0000 |
+------------+------+----------+-------+--------+----------+
3 rows in set (0.00 sec)
```
这个查询我们用到了CURDATE函数来获取当前日期也用到了DAYOFWEEK函数来获取当前是周几的信息。由于DAYOFWEEK()函数以周日为1开始计周一是2……周六是7而数据表中是从周一为1开始计算为了对齐我用到了条件判断函数CASE我来解释下这个函数。
MySQL中CASE函数的语法如下
```
CASE 表达式 WHEN 值1 THEN 表达式1 [ WHEN 值2 THEN 表达式2] ELSE 表达式m END
```
在我们这个查询中“表达式”有7种可能的值。通过CASE函数我们可以根据DAYOFWEEK()函数返回的值对每个返回值进行处理从而跟促销信息表中的字段weekday对应。
除了获取特定的日期,咱们还经常需要把日期按照一定的格式显示出来,这就要用到日期时间格式化的函数**DATE_FORMAT()它表示将日期时间“date”按照指定格式显示**。
举个小例子张三希望用24小时制来查看时间那么他就可以通过使用DATE_FORMAT()函数,指定格式“%T”来实现
```
mysql&gt; SELECT DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%T&quot;);
+-----------------------------------------+
| DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%T&quot;) |
+-----------------------------------------+
| 13:25:50 |
+-----------------------------------------+
1 row in set (0.00 sec)
```
李四习惯按照上下午的方式来查看时间同样他可以使用DATE_FORMAT()函数,通过指定格式“%r”来实现
```
mysql&gt; SELECT DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%r&quot;);
+-----------------------------------------+
| DATE_FORMAT(&quot;2020-12-01 13:25:50&quot;,&quot;%r&quot;) |
+-----------------------------------------+
| 01:25:50 PM |
+-----------------------------------------+
1 row in set (0.00 sec
```
格式的详细内容非常丰富,我就不一一介绍了,我给你分享一个[链接](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format),你可以随时查看一下。
另外一个重要的时间函数是DATEDIFFdate1,date2表示日期“date1”与日期“date2”之间差几天。假如你要计算某段时间的每天交易金额的平均值只需要把起始日期和截止日期传给这个函数就可以得到中间隔了几天。再用总计金额除以这个天数就可以算出来了
```
mysql&gt; SELECT DATEDIFF(&quot;2021-02-01&quot;,&quot;2020-12-01&quot;);
+-------------------------------------+
| DATEDIFF(&quot;2021-02-01&quot;,&quot;2020-12-01&quot;) |
+-------------------------------------+
| 62 |
+-------------------------------------+
1 row in set (0.00 sec)
```
## 总结
今天我们学习了MySQL的时间处理函数包括获取日期时间类型数据中部分信息的函数、计算日期时间的函数和获取特定日期的函数我用图片来帮你汇总了下。
<img src="https://static001.geekbang.org/resource/image/6f/49/6f03f8e150e324101b20acf0c61be649.jpg" alt="">
最后我还想多说一句MySQL中获取的时间其实就是MySQL服务器计算机的系统时间。如果你的系统有一定规模需要在多台计算机上运行就要注意时间校准的问题。比如我们的信息系统受门店经营环境和操作人员的素质所限有时会遇到误操作、停电等故障而导致的计算机系统时间失准问题。这对整个信息系统的可靠性影响非常大。
针对这个问题有2种解决办法。
第一种方法是可以利用Windows系统自带的网络同步的方式来校准系统时间。
另一种办法就是门店统一从总部MySQL服务器获取时间。由于总部的服务器的配置和运维状况一般要好于门店所以系统时间出现误差的可能性也较小。如果采用云服务器系统时间的可靠性会更高。
## 思考题
假如用户想查一下今天是星期几(不能用数值,要用英文显示),你可以写一个简单的查询语句吗?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,365 @@
<audio id="audio" title="10 | 如何进行数学计算、字符串处理和条件判断?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/af/a3b33304a92fc8e444335a190fdc7aaf.mp3"></audio>
你好,我是朱晓峰。
MySQL提供了很多功能强大而且使用起来非常方便的函数包括数学函数、字符串处理函数和条件判断函数等。
在很多场景中 ,我们都会用到这些函数,比如说,在超市项目的实际开发过程中,会有这样的需求:
- 会员积分的规则是一元积一分不满一元不积分这就要用到向下取整的数学函数FLOOR()
- 在打印小票的时候收银纸的宽度是固定的怎么才能让打印的结果清晰而整齐呢这个时候就要用到CONCAT()等字符串处理函数;
- 不同数据的处理方式不同怎么选择正确的处理方式呢这就会用到IF(表达式V1V2)这样的条件判断函数;
- ……
这些函数对我们管理数据库、提高数据处理的效率有很大的帮助。接下来,我就带你在解决实际问题的过程中,帮你掌握使用这些函数的方法。
## 数学函数
我们先来学习下数学函数它主要用来处理数值数据常用的主要有3类分别是取整函数ROUND()、CEIL()、FLOOR()绝对值函数ABS()和求余函数MOD()。
知道了这些函数,我们来看看超市经营者的具体需求。他们提出,为了提升销量,要进行会员营销,主要是给会员积分,并以积分数量为基础,给会员一定的优惠。
积分的规则也很简单,就是消费一元积一分,不满一元不积分,那我们就需要对销售金额的数值进行取整。
这里主要用到四个表,分别是销售单明细表、销售单头表、商品信息表和会员信息表。为了方便你理解,我对表结构和数据进行了简化。
销售单明细表:
<img src="https://static001.geekbang.org/resource/image/54/93/543b4ce8c0c8b1f3bb7028c911213f93.jpeg" alt="">
销售单头表:
<img src="https://static001.geekbang.org/resource/image/a4/33/a4df12d3469aaf2f770fbfa8fb842c33.jpeg" alt="">
商品信息表:
<img src="https://static001.geekbang.org/resource/image/26/03/262121b96d4ce48e310cdff37d536203.jpeg" alt="">
会员信息表:
<img src="https://static001.geekbang.org/resource/image/10/8e/105d12853e4b09aefd96f2423613648e.jpeg" alt="">
这个场景下可以用到MySQL数学函数中的取整函数主要有3种。
- 向上取整CEIL(X)和CEILING(X)返回大于等于X的最小INT型整数。
- 向下取整FLOOR(X)返回小于等于X的最大INT型整数。
- 舍入函数ROUND(X,D)X表示要处理的数D表示保留的小数位数处理的方式是四舍五入。ROUND(X)表示保留0位小数。
现在积分的规则是一元积一分不满一元不积分显然是向下取整那就可以用FLOOR函数。
首先,我们要通过关联查询,获得会员消费的相关信息:
```
mysql&gt; SELECT
-&gt; c.membername AS '会员', -- 从会员表获取会员名称
-&gt; b.transactionno AS '单号',-- 从销售单头表获取单号
-&gt; b.transdate AS '交易时间', -- 从销售单头表获取交易时间
-&gt; d.goodsname AS '商品名称', -- 从商品信息表获取商品名称
-&gt; a.salesvalue AS '交易金额'
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.membermaster c ON (b.memberid = c.memberid)
-&gt; JOIN
-&gt; demo.goodsmaster d ON (a.itemnumber = d.itemnumber);
+------+------------------+---------------------+----------+----------+
| 会员 | 单号 | 交易时间 | 商品名称 | 交易金额 |
+------+------------------+---------------------+----------+----------+
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 书 | 176.22 |
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 笔 | 24.75 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 书 | 234.96 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 笔 | 26.40 |
+------+------------------+---------------------+----------+----------+
4 rows in set (0.01 sec)
```
接着我们用FLOORa.salesvalue对销售金额向下取整获取会员积分值代码如下
```
mysql&gt; SELECT
-&gt; c.membername AS '会员',
-&gt; b.transactionno AS '单号',
-&gt; b.transdate AS '交易时间',
-&gt; d.goodsname AS '商品名称',
-&gt; a.salesvalue AS '交易金额',
-&gt; FLOOR(a.salesvalue) AS '积分' -- 使用FLOOR函数向下取整
-&gt; FROM
-&gt; demo.transactiondetails a
-&gt; JOIN
-&gt; demo.transactionhead b ON (a.transactionid = b.transactionid)
-&gt; JOIN
-&gt; demo.membermaster c ON (b.memberid = c.memberid)
-&gt; JOIN
-&gt; demo.goodsmaster d ON (a.itemnumber = d.itemnumber);
+------+------------------+---------------------+----------+----------+------+
| 会员 | 单号 | 交易时间 | 商品名称 | 交易金额 | 积分 |
+------+------------------+---------------------+----------+----------+------+
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 书 | 176.22 | 176 |
| 张三 | 0120201201000001 | 2020-12-01 14:25:56 | 笔 | 24.75 | 24 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 书 | 234.96 | 234 |
| 李四 | 0120201202000001 | 2020-12-02 10:50:50 | 笔 | 26.40 | 26 |
+------+------------------+---------------------+----------+----------+------+
4 rows in set (0.01 sec)
```
你看通过FLOOR(),我们轻松地获得了超市经营者需要的积分数据。
类似的如果用户的积分规则改为“不满一元积一分”其实就是对金额数值向上取整这个时候我们就可以用CEIL()函数。操作方法和前面是一样的,我就不具体解释了。
最后我再来讲一讲舍入函数ROUND的使用方法。
超市经营者提出,收银的时候,应收金额可以被设定四舍五入到哪一位。比如,可以设定四舍五入到元、到角,或者到分。
按照指定的位数对小数进行四舍五入计算这样的场景就要用到ROUNDX,D了。它的作用是通过四舍五入对数值X保留D位小数。
根据超市经营者的要求我们把函数ROUND(X,D)中的保留小数的位数D设置成0、1和2。
如果要精确到分我们可以设置保留2位小数
```
mysql&gt; SELECT ROUND(salesvalue,2) -- D设置成2表示保留2位小数也就是精确到分
-&gt; FROM demo.transactiondetails
-&gt; WHERE transactionid=1 AND itemnumber=1;
+---------------------+
| ROUND(salesvalue,2) |
+---------------------+
| 176.22 |
+---------------------+
1rows in set (0.00 sec)
```
如果要精确到角可以设置保留1位小数
```
mysql&gt; SELECT ROUND(salesvalue,1) -- D设置成1表示保留1位小数也就是精确到角
-&gt; FROM demo.transactiondetails
-&gt; WHERE transactionid=1 AND itemnumber=1;
+---------------------+
| ROUND(salesvalue,1) |
+---------------------+
| 176.2 |
+---------------------+
1 rows in set (0.00 sec
```
如果要精确到元可以设置保留0位小数
```
mysql&gt; SELECT ROUND(salesvalue,0)-- D设置成0表示保留0位小数也就是精确到元
-&gt; FROM demo.transactiondetails
-&gt; WHERE transactionid=1 AND itemnumber=1;
+---------------------+
| ROUND(salesvalue,0) |
+---------------------+
| 176 |
+---------------------+
1 rows in set (0.00 se
```
除了刚刚我们所学习的函数MySQL还支持绝对值函数ABS和求余函数MODABSX表示获取X的绝对值MODXY表示获取X被Y除后的余数。
这些函数使用起来都比较简单,你重点掌握它们的含义就可以了,下面我再带你学习下字符串函数。
## 字符串函数
除了数学计算,我们还经常会遇到需要对字符串进行处理的场景,比如我们想要在金额前面加一个“¥”的符号,就会用到字符串拼接函数;再比如,我们需要把一组数字以字符串的形式在网上传输,就要用到类型转换函数。
常用的字符串函数有4个。
- CONCATs1,s2,...表示把字符串s1、s2……拼接起来组成一个字符串。
- CAST表达式 AS CHAR表示将表达式的值转换成字符串。
- CHAR_LENGTH字符串表示获取字符串的长度。
- SPACEn表示获取一个由n个空格组成的字符串。
接下来我还是借助超市项目中的实际应用场景,来说明一下怎么使用这些字符串函数。
顾客交了钱完成交易之后系统必须要打出一张小票。打印小票时对格式有很多要求。比如说一张小票纸57毫米宽大概可以打32个字符也就是16个汉字。用户要求一条流水打2行第一行是商品信息第二行要包括数量、价格、折扣和金额4种信息。那么怎么才能清晰地在小票上打印出这些信息并且打印得整齐漂亮呢这就涉及对字符串的处理了。
首先,我们来看一下如何打印第一行的商品信息。商品信息包括:商品名称和商品规格,而且商品规格要包含在括号里面。这样就必须把商品名称和商品规格拼接起来,变成一个字符串。
这时我们就可以用合并字符串函数CONCAT如下所示
```
mysql&gt; SELECT
-&gt; CONCAT(goodsname, '(', specification, ')') AS 商品信息 -- 这里把商品名称、括号和规格拼接起来
-&gt; FROM
-&gt; demo.goodsmaster
-&gt; WHERE itemnumber = 1;
+----------+
| 商品信息 |
+----------+
| 书(16开) |
+----------+
1 row in set (0.00 sec)
```
这样我们就得到了商品编号是1的商品它的商品信息是“书16开”。
第二步我们来看一下如何打印第二行。第二行包括数量、价格、折扣和金额一共是4种信息。
因为一行最多是32个字符我们给数量分配7个字符价格分配7个字符折扣分配6个字符金额分配9个字符加上中间3个空格正好是32个字符。
为啥这么分配呢?我简单解释下。
- 数量7个字符就是小数点前面给3位小数点后面给3位外加小数点1位最大999.999,基本满足零售的需求了。
- 同样道理价格给7位意思是小数点前面4位小数点后面2位外加小数点这样最大可以表示9999.99。
- 折扣6位小数点后面2位小数点前面2位加上小数点和“%”,这样是够用的。
- 金额9位最大可以显示到999999.99,也够用了。
分配好了各部分信息的字符串大小,我再讲一下格式处理,因为数据的取值每次都会不同,如果直接打印,会参差不齐。这里我以数量为例,来具体说明一下。因为数量比较有代表性,而且比较简单,不像金额或者折扣率那样,有时还要根据用户的需求,加上“¥”或者“%”。
第一步把数量转换成字符串。这里我们需要用到把数值转换成字符串的CAST函数如下所示
```
mysql&gt; SELECT
-&gt; CAST(quantity AS CHAR) -- 把decimal类型转换成字符串
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; WHERE
-&gt; transactionid = 1 AND itemnumber =1;
+---------------------+
| CAST(price AS CHAR) |
+---------------------+
| 2.000 |
+---------------------+
1 rows in set (0.00 sec)
```
第二步计算字符串的长度这里我们要用到CHAR_LENGTH函数。
需要注意的是虽然每个汉字打印的时候占2个字符长度但是这个函数获取的是汉字的个数。因此如果字符串中有汉字函数获取的字符串长度跟实际打印的长度是不一样的需要用空格来补齐。
我们可以通过下面的查询,获取数量字段转换成字符串后的字符串长度:
```
mysql&gt; SELECT
-&gt; CHAR_LENGTH(CAST(quantity AS CHAR)) AS 长度
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; WHERE
-&gt; transactionid = 1 AND itemnumber =1;
+---------------------+
| 长度 |
+---------------------+
| 5 |
+---------------------+
1 rows in set (0.00 sec)
```
第三步用空格补齐7位长度。这时我们要用到SPACE函数。
因为我们采用左对齐的方式打印(左对齐表示字符串从左边开始,右边空余的位置用空格补齐),所以就需要先拼接字符串,再在字符串的后面补齐空格:
```
mysql&gt; SELECT
-&gt; CONCAT(CAST(quantity AS CHAR),
-&gt; SPACE(7 - CHAR_LENGTH(CAST(quantity AS CHAR)))) AS 数量
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; WHERE
-&gt; transactionid = 1 AND itemnumber = 1;
+----------+
| 数量 |
+----------+
| 2.000 |
+----------+
1 row in set (0.00 sec)
```
除此以外MySQL还支持SUBSTR、MID、TRIM、LTRIM、RTRIM。我画了一张图来展示它们的含义你可以了解一下。
<img src="https://static001.geekbang.org/resource/image/86/d9/86f0f4ebe3055db5c112784d86aa07d9.jpg" alt="">
一般来说关于字符串函数你掌握这些就足够了。不过MySQL支持的字符串函数还有很多如果你在实际工作中遇到了更复杂的情况可以参考MySQL官方的[文档](https://dev.mysql.com/doc/refman/8.0/en/string-functions.html)。
## 条件判断函数
我们刚才在对商品信息字符串进行拼接的时候会有一种例外的情况那就是当规格为空的时候商品信息会变成“NULL”。这个结果显然不是我们想要的因为名称变成NULL顾客会觉得奇怪也不知道买了什么商品。我们希望如果规格是空值就不用加规格了。怎么实现呢这就要用到条件判断函数了。
条件判断函数的主要作用,就是根据特定的条件返回不同的值,常用的有两种。
- IFNULLV1V2表示如果V1的值不为空值则返回V1否则返回V2。
- IF表达式V1V2如果表达式为真TRUE则返回V1否则返回V2。
我们希望规格是空的商品,拼接商品信息字符串的时候,规格不要是空。这个问题,可以通过 IFNULL(specification, '')函数来解决。具体点说就是对字段“specification”是否为空进行判断如果为空就返回空字符串否则就返回商品规格specification的值。代码如下所示
```
mysql&gt; SELECT
-&gt; goodsname,
-&gt; specification,
-&gt; CONCAT(goodsname,'(', IFNULL(specification, ''),')') AS 拼接 -- 用条件判断函数,如果规格是空,则括号中是空字符串
-&gt; FROM
-&gt; demo.goodsmaster;
+-----------+---------------+----------+
| goodsname | specification | 拼接 |
+-----------+---------------+----------+
| 书 | 16开 | 书(16开) |
| 笔 | NULL | 笔() |
+-----------+---------------+----------+
2 rows in set (0.00 sec)
```
结果是,如果规格为空,商品信息就变成了“商品信息()”,好像还不错。但是也存在一点问题:商品名称后面的那个空括号“()”会让客人觉得奇怪,能不能去掉呢?
如果用IFNULLV1V2函数就不容易做到但是没关系我们可以尝试用另一个条件判断函数IF表达式V1V2来解决。这里表达式是ISNULL(specification),这个函数用来判断字段"specificaiton"是否为空V1是返回商品名称V2是返回商品名称拼接规格。代码如下所示
```
mysql&gt; SELECT
-&gt; goodsname,
-&gt; specification,
-&gt; -- 这里做判断,如果是空值,返回商品名称,否则就拼接规格
-&gt; IF(ISNULL(specification),
-&gt; goodsname,
-&gt; CONCAT(goodsname, '(', specification, ')')) AS 拼接
-&gt; FROM
-&gt; demo.goodsmaster;
+-----------+---------------+----------+
| goodsname | specification | 拼接 |
+-----------+---------------+----------+
| 书 | 16开 | 书(16开) |
| 笔 | NULL | 笔 |
+-----------+---------------+----------+
2 rows in set (0.02 sec)
```
这个结果就是,如果规格为空,商品信息就是商品名称;如果规格不为空,商品信息是商品名称拼接商品规格,这就达到了我们的目的。
## 总结
今天,我们学习了用于提升数据处理效率的数学函数、字符串函数和条件判断函数。
<img src="https://static001.geekbang.org/resource/image/06/f7/06f0cb9251af48e626b81016630f9ff7.jpg" alt="">
这些函数看起来很容易掌握但是有很多坑。比如说ROUNDX是对X小数部分四舍五入那么在“五入”的时候返回的值是不是一定比X大呢其实不一定因为当X为负数时五入的值会更小。你可以看看下面的代码
```
mysql&gt; SELECT ROUND(-1.5);
+-------------+
| ROUND(-1.5) |
+-------------+
| -2 |
+-------------+
1 row in set (0.00 sec)
```
所以,我建议你在学习的时候,**多考虑边界条件的场景,实际测试一下**。就像这个问题对于ROUND(X,0)并没有指定X是正数那如果是负数会怎样呢你去测试一下就明白了。
## 思考题
这节课我介绍了如何用FLOOR函数来计算会员积分那么如果不用FLOOR有没有其他办法来实现会员积分的计算呢
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,446 @@
<audio id="audio" title="11 | 索引:怎么提高查询的速度?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d0/d8/d0b32336271fe5a52410da5a19ee7fd8.mp3"></audio>
你好,我是朱晓峰。
在我们的超市信息系统刚刚开始运营的时候因为数据量很少每一次的查询都能很快拿到结果。但是系统运转时间长了以后数据量不断地累积变得越来越庞大很多查询的速度就变得特别慢。这个时候我们就采用了MySQL 提供的高效访问数据的方法—— 索引有效地解决了这个问题甚至之前的一个需要8秒钟才能完成的查询现在只用0.3秒就搞定了速度提升了20多倍。
那么,索引到底是啥呢?该怎么使用呢?这节课,我们就来聊一聊。
# 索引是什么?
如果你去过图书馆,应该会知道图书馆的检索系统。图书馆为图书准备了检索目录,包括书名、书号、对应的位置信息,包括在哪个区、哪个书架、哪一层。我们可以通过书名或书号,快速获知书的位置,拿到需要的书。
MySQL中的索引就相当于图书馆的检索目录它是帮助MySQL系统快速检索数据的一种存储结构。我们可以在索引中按照查询条件检索索引字段的值然后快速定位数据记录的位置这样就不需要遍历整个数据表了。而且数据表中的字段越多表中数据记录越多速度提升越是明显。
我来举个例子进一步解释下索引的作用。这里要用到销售流水表demo.trans表结构如下
```
mysql&gt; describe demo.trans;
+---------------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+----------+------+-----+---------+-------+
| itemnumber | int | YES | MUL | NULL | |
| quantity | text | YES | | NULL | |
| price | text | YES | | NULL | |
| transdate | datetime | YES | MUL | NULL | |
| actualvalue | text | YES | | NULL | |
| barcode | text | YES | | NULL | |
| cashiernumber | int | YES | MUL | NULL | |
| branchnumber | int | YES | MUL | NULL | |
| transuniqueid | text | YES | | NULL | |
+---------------+----------+------+-----+---------+-------+
9 rows in set (0.02 sec)
```
某个门店的销售流水表有400万条数据现在我要查看一下商品编号是100的商品在2020-12-12这一天的销售情况查询代码如下
```
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (8.08 sec)
```
可以看到结果总共有2条记录可是却花了8秒钟非常慢。同时这里我没有做表的关联这只是单表的查询而且只是一个门店几个月的数据而已。而总部是把所有门店的数据都汇总到一起查询速度更慢这样的查询效率我们肯定是不能接受的。
怎么解决这个问题呢?这时,我们就可以给数据表添加索引。
# 单字段索引
MySQL支持单字段索引和组合索引而单字段索引比较常用我们先来学习下创建单字段索引的方法。
## 如何创建单字段索引?
创建单字段索引一般有3种方式
1. 你可以通过CREATE语句直接给已经存在的表创建索引这种方式比较简单我就不多解释了
1. 可以在创建表的同时创建索引;
1. 可以通过修改表来创建索引。
直接给数据表创建索引的语法如下:
```
CREATE INDEX 索引名 ON TABLE 表名 (字段);
```
创建表的同时创建索引的语法如下所示:
```
CREATE TABLE 表名
(
字段 数据类型,
….
{ INDEX | KEY } 索引名(字段)
)
```
修改表时创建索引的语法如下所示:
```
ALTER TABLE 表名 ADD { INDEX | KEY } 索引名 (字段);
```
这里有个小问题要提醒你一下给表设定主键约束或者唯一性约束的时候MySQL会自动创建主键索引或唯一性索引。这也是我建议你在创建表的时候一定要定义主键的原因之一。
举个小例子我们可以给表demo.trans创建索引如下
```
mysql&gt; CREATE INDEX index_trans ON demo.trans (transdate(10));
Query OK, 0 rows affected (1 min 8.71 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (0.30 sec)
```
可以看到加了索引之后这一次我们只用了0.3秒比没有索引的时候快了20多倍。这么大的差距说明索引对提高查询的速度确实很有帮助。那么索引是如何做到这一点的呢下面我们来学习下单字段索引的作用原理。
## 单字段索引的作用原理
要知道索引是怎么起作用的我们需要借助MySQL中的EXPLAIN 这个关键字。
EXPLAIN关键字能够查看SQL语句的执行细节包括表的加载顺序表是如何连接的以及索引使用情况等。
```
mysql&gt; EXPLAIN SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100;
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
| 1 | SIMPLE | trans | NULL | range | index_trans | index_trans | 6 | NULL | 5411 | 10.00 | Using index condition; Using where; Using MRR |
+----+-------------+-------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------------------------------+
1 row in set, 1 warning (0.00 sec)
```
我来解释下代码里的关键内容。
- type=range表示使用索引查询特定范围的数据记录。
- rows=5411表示需要读取的记录数。
- possible_keys=index_trans表示可以选择的索引是index_trans。
- key=index_trans表示实际选择的索引是index_trans。
- extra=Using index condition;Using where;Using MRR这里面的信息对SQL语句的执行细节做了进一步的解释包含了3层含义第一个是执行时使用了索引第二个是执行时通过WHERE条件进行了筛选第三个是使用了顺序磁盘读取的策略。
通过这个小例子我们可以发现有了索引之后MySQL在执行SQL语句的时候多了一种优化的手段。也就是说在查询的时候可以先通过查询索引快速定位然后再找到对应的数据进行读取这样就大大提高了查询的速度。
## 如何选择索引字段?
在刚刚的查询中我们是选择transdate交易时间字段来当索引字段你可能会问为啥不选别的字段呢这是因为交易时间是查询条件。MySQL可以按照交易时间的限定“2020年12月12日”在索引中而不是数据表中寻找满足条件的索引记录再通过索引记录中的指针来定位数据表中的数据。这样索引就能发挥作用了。
不过你有没有想过itemnumber字段也是查询条件能不能用itemnumber来创建一个索引呢我们来试一试
```
mysql&gt; CREATE INDEX index_trans_itemnumber ON demo.trans (itemnumber);
Query OK, 0 rows affected (43.88 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
然后看看效果:
```
mysql&gt; SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12' -- 对交易时间的筛选可以在transdate的索引中定位
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100; -- 对商品编号的筛选可以在itemnumber的索引中定位
+----------+--------+---------------------+
| quantity | price | transdate |
+----------+--------+---------------------+
| 1.000 | 220.00 | 2020-12-12 19:45:36 |
| 1.000 | 220.00 | 2020-12-12 08:56:37 |
+----------+--------+---------------------+
2 rows in set (0.38 sec)
```
我们发现用itemnumber创建索引之后查询速度跟之前差不多基本在同一个数量级。
这是为啥呢我们来看看MySQL的运行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; transdate &gt; '2020-12-12'
-&gt; AND transdate &lt; '2020-12-13'
-&gt; AND itemnumber = 100; -- 对itemnumber 进行限定
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans,index_trans_itemnumber | index_trans_itemnumber | 5 | const | 1192 | 0.14 | Using where |
+----+-------------+-------------+------------+------+------------------------------------------------+------------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
```
我们发现“possible_keys= index_trans,index_trans_itemnumber ”就是说MySQL认为可以选择的索引确实有2个一个是用transdate字段创建的索引index_trans另一个是用itemnumber字段创建的索引index_trans_itemnumber。
key= index_trans_itemnumber 说明MySQL实际选择使用的索引是itemnumber字段创建的索引index_trans_itemnumber。而rows=1192就表示实际读取的数据记录数只有1192个比用transdate创建的索引index_trans的实际读取记录数要少这就是MySQL选择使用itemnumber索引的原因。
**所以,我建议你在选择索引字段的时候,要选择那些经常被用做筛选条件的字段**。这样才能发挥索引的作用,提升检索的效率。
# 组合索引
在实际工作中有时会遇到比较复杂的数据表这种表包括的字段比较多经常需要通过不同的字段筛选数据特别是数据表中包含多个层级信息。比如我们的销售流水表就包含了门店信息、收款机信息和商品信息这3个层级信息。门店对应多个门店里的收款机每个收款机对应多个从这台收款机销售出去的商品。我们经常要把这些层次信息作为筛选条件来进行查询。这个时候单字段的索引往往不容易发挥出索引的最大功效可以使用组合索引。
现在先看看单字段索引的效果我们分别用branchnumber和cashiernumber来创建索引
```
mysql&gt; CREATE INDEX index_trans_branchnumber ON demo.trans (branchnumber);
Query OK, 0 rows affected (41.49 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql&gt; CREATE INDEX index_trans_cashiernumber ON demo.trans (cashiernumber);
Query OK, 0 rows affected (41.95 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
有了门店编号和收款机编号的索引,现在我们就尝试一下以门店编号、收款机编号和商品编号为查询条件,来验证一下索引是不是起了作用。
```
mysql&gt; SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1 -- 门店编号和收款机号为筛选条件
-&gt; AND itemnumber = 100; -- 商品编号为筛选条件
+------------+----------+--------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+--------+---------------------+
| 100 | 1.000 | 220.00 | 2020-07-11 09:18:35 |
| 100 | 1.000 | 220.00 | 2020-09-06 21:21:58 |
| 100 | 1.000 | 220.00 | 2020-11-10 15:00:11 |
| 100 | 1.000 | 220.00 | 2020-12-25 14:28:06 |
| 100 | 1.000 | 220.00 | 2021-01-09 20:21:44 |
| 100 | 1.000 | 220.00 | 2021-02-08 10:45:05 |
+------------+----------+--------+---------------------+
6 rows in set (0.31 sec)
```
结果有6条记录查询时间是0.31秒,跟只创建商品编号索引差不多。下面我们就来查看一下执行计划,看看新建的索引有没有起作用。
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber | index_trans_itemnumber | 5 | const | 1192 | 20.50 | Using where |
+----+-------------+-------+------------+------+---------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)
```
MySQL有3个索引可以用分别是用branchnumber创建的index_trans_branchnumber、用cashiernumber创建的index_trans_cashiernumber和用itemnumber创建的index_trans_itemnumber。
最后MySQL还是选择了index_trans_itemnumber实际筛选的记录数是1192花费了0.31秒。
为什么MySQL会这样选呢这是因为优化器现在有3种索引可以用分别是商品编号索引、门店编号索引和收款机号索引。优化器发现商品编号索引实际搜索的记录数最少所以最后就选择了这种索引。
所以,**如果有多个索引而这些索引的字段同时作为筛选字段出现在查询中的时候MySQL会选择使用最优的索引来执行查询操作**。
能不能让这几个筛选字段同时发挥作用呢这就用到组合索引了。组合索引就是包含多个字段的索引。MySQL最多支持由16个字段组成的组合索引。
## 如何创建组合索引?
创建组合索引的语法结构与创建单字段索引相同,不同的是相比单字段索引,组合索引使用了多个字段。
直接给数据表创建索引的语法如下:
```
CREATE INDEX 索引名 ON TABLE 表名 (字段1字段2...);
```
创建表的同时创建索引:
```
CREATE TABLE 表名
(
字段 数据类型,
….
{ INDEX | KEY } 索引名(字段1字段2...)
)
```
修改表时创建索引:
```
ALTER TABLE 表名 ADD { INDEX | KEY } 索引名 (字段1字段2...);
```
现在,针对刚刚的查询场景,我们就可以通过创建组合索引,发挥多个字段的筛选作用。
具体做法是我们给销售流水表创建一个由3个字段branchnumber、cashiernumber、itemnumber组成的组合索引如下所示
```
mysql&gt; CREATE INDEX Index_branchnumber_cashiernumber_itemnumber ON demo.trans (branchnumber,cashiernumber,itemnumber);
Query OK, 0 rows affected (59.26 sec)
Records: 0 Duplicates: 0 Warnings: 0
```
有了组合索引,刚刚的查询速度就更快了:
```
mysql&gt; SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+------------+----------+--------+---------------------+
| itemnumber | quantity | price | transdate |
+------------+----------+--------+---------------------+
| 100 | 1.000 | 220.00 | 2020-07-11 09:18:35 |
| 100 | 1.000 | 220.00 | 2020-09-06 21:21:58 |
| 100 | 1.000 | 220.00 | 2020-11-10 15:00:11 |
| 100 | 1.000 | 220.00 | 2020-12-25 14:28:06 |
| 100 | 1.000 | 220.00 | 2021-01-09 20:21:44 |
| 100 | 1.000 | 220.00 | 2021-02-08 10:45:05 |
+------------+----------+--------+---------------------+
6 rows in set (0.00 sec)
```
几乎是瞬间就完成了不超过10毫秒。我们看看MySQL的执行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE -- 同时筛选门店编号、收款机号和商品编号
-&gt; branchnumber = 11 AND cashiernumber = 1
-&gt; AND itemnumber = 100;
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber,index_branchnumber_cashiernumber_itemnumber | index_branchnumber_cashiernumber_itemnumber | 15 | const,const,const | 6 | 100.00 | NULL |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+---------------------------------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)
```
这个查询MySQL可以用到的索引有4个
- index_trans_itemnumber
- index_trans_branchnumber
- index_trans_cashiernumber
- 我们刚才用branchnumber、cashiernumber和itemnumber创建的组合索引Index_branchnumber_cashiernumber_itemnumber。
MySQL选择了组合索引筛选后读取的记录只有6条。组合索引被充分利用筛选更加精准所以非常快。
## 组合索引的原理
下面我就来讲讲组合索引的工作原理。
**组合索引的多个字段是有序的,遵循左对齐的原则**。比如我们创建的组合索引排序的方式是branchnumber、cashiernumber和itemnumber。因此筛选的条件也要遵循从左向右的原则如果中断那么断点后面的条件就没有办法利用索引了。
比如说我们刚才的条件branchnumber = 11 AND cashiernumber = 1 AND itemnumber = 100包含了从左到右的所有字段所以可以最大限度使用全部组合索引。
假如把条件换成“cashiernumber = 1 AND itemnumber = 100”由于我们的组合索引是按照branchnumber、cashiernumber和itemnumber的顺序建立的最左边的字段branchnumber没有包含到条件当中中断了所以这个条件完全不能使用组合索引。
类似的如果筛选的是一个范围如果没有办法无法精确定位也相当于中断。比如“branchnumber &gt; 10 AND cashiernumber = 1 AND itemnumber = 100”这个条件只能用到组合索引中branchnumber&gt;10的部分后面的索引就都用不上了。我们来看看MySQL的运行计划
```
mysql&gt; EXPLAIN SELECT
-&gt; itemnumber,quantity,price,transdate
-&gt; FROM
-&gt; demo.trans
-&gt; WHERE
-&gt; branchnumber &gt; 10 AND cashiernumber = 1 AND itemnumber = 100;
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | trans | NULL | ref | index_trans_itemnumber,index_trans_branchnumber,index_trans_cashiernumber,index_branchnumber_cashiernumber_itemnumber | index_trans_itemnumber | 5 | const | 1192 | 20.50 | Using where |
+----+-------------+-------+------------+------+-----------------------------------------------------------------------------------------------------------------------+------------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.02 sec)
```
果然MySQL没有选择组合索引而是选择了用itemnumber创建的普通索引index_trans_itemnumber。因为**如果只用组合索引的一部分,效果没有单字段索引那么好**。
# 总结
这节课,我们学习了什么是索引、如何创建和使用索引。索引可以非常显著地提高数据查询的速度,数据表里包含的数据越多,效果越显著。我们应该选择经常被用做筛选条件的字段来创建索引,这样才能通过索引缩小实际读取数据表中数据的范围,发挥出索引的优势。如果有多个筛选的字段,而且经常一起出现,也可以用多个字段来创建组合索引。
如果你要删除索引,就可以用:
```
DROP INDEX 索引名 ON 表名;
```
当然, 有的索引不能用这种方法删除,比如主键索引,你就必须通过修改表来删除索引。语法如下:
```
ALTER TABLE 表名 DROP PRIMARY KEY
```
最后我来跟你说说索引的成本。索引能够提升查询的效率但是建索引也是有成本的主要有2个方面一个存储空间的开销还有一个是数据操作上的开销。
- 存储空间的开销,是指索引需要单独占用存储空间。
- 数据操作上的开销,是指一旦数据表有变动,无论是插入一条新数据,还是删除一条旧的数据,甚至是修改数据,如果涉及索引字段,都需要对索引本身进行修改,以确保索引能够指向正确的记录。
因此,索引也不是越多越好,创建索引有存储开销和操作开销,需要综合考虑。
# 思考题
假如我有一个单品销售统计表,包括门店编号、销售日期(年月日)、商品编号、销售数量、销售金额、成本、毛利,而用户经常需要对销售情况进行查询,你会对这个表建什么样的索引呢?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,239 @@
<audio id="audio" title="12 | 事务:怎么确保关联操作正确执行?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a6/53/a6329fcd8cd566a1a2d772c9468f7753.mp3"></audio>
你好,我是朱晓峰。
我们经常会遇到这样的场景几个相互关联的数据操作必须是全部执行或者全部不执行不可以出现部分执行的情况。比如说你从微信账号里提现100元到银行卡上这个动作就包括了相互关联的2个步骤首先是微信账号减100元然后是银行卡账号加100元这里假设没有手续费。假如因为某种异常这2个操作只执行了一个另外一个没有执行就会出现你的钱少了100元或者你的钱多了100元的情况这肯定是不能接受的。
如何才能确保多个关联操作全部执行呢?这时就要用到事务了。接下来我就重点讲一讲什么是事务,以及如何正确使用事务。
## 什么是事务?
事务是MySQL的一项功能它可以使一组数据操作也叫DML操作是英文Data Manipulation Language的缩写包括SELECT、INSERT、UPDATE和DELETE要么全部执行要么全部不执行不会因为某种异常情况比如硬件故障、停电、网络中断等出现只执行一部分操作的情况。
事务的语法结构如下所示:
```
START TRANSACTION 或者 BEGIN (开始事务)
一组DML语句
COMMIT提交事务
ROLLBACK事务回滚
```
我解释一下这几个关键字。
- **START TRANSACTION和BEGIN**表示开始事务意思是通知MySQL后面的DML操作都是当前事务的一部分。
- **COMMIT**:表示提交事务,意思是执行当前事务的全部操作,让数据更改永久有效。
- **ROLLBACK**:表示回滚当前事务的操作,取消对数据的更改。
事务有4个主要特征分别是原子性atomicity、一致性consistency、持久性durability和隔离性isolation
- 原子性:表示事务中的操作要么全部执行,要么全部不执行,像一个整体,不能从中间打断。
- 一致性:表示数据的完整性不会因为事务的执行而受到破坏。
- 隔离性:表示多个事务同时执行的时候,不互相干扰。不同的隔离级别,相互独立的程度不同。
- 持久性:表示事务对数据的修改是永久有效的,不会因为系统故障而失效。
持久性非常好理解,我就不多说了,接下来我重点讲一讲事务的原子性、一致性和隔离性,这是确保关联操作正确执行的关键。
## 如何确保操作的原子性和数据的一致性?
我借助一个超市的收银员帮顾客结账的简单场景来讲解。在系统中,结算的动作主要就是销售流水的产生和库存的消减。这里会涉及销售流水表和库存表,如下所示:
销售流水表demo.mytrans
<img src="https://static001.geekbang.org/resource/image/bd/1e/bd6a537byy788646d1202167245c1c1e.jpeg" alt="">
库存表demo.inventory
<img src="https://static001.geekbang.org/resource/image/f1/98/f17cfe65a02f4a5a54e4a49e63a35998.jpeg" alt="">
现在假设门店销售了5个商品编号是1的商品这个动作实际上包括了2个相互关联的数据库操作
1. 向流水表中插入一条“1号商品卖了5个”的销售流水
1. 把库存表中的1号商品的库存减5。
这里包含了2个DML操作为了避免意外事件导致的一个操作执行了而另一个没有执行的情况我把它们放到一个事务里面利用事务中数据操作的原子性来确保数据的一致性。
```
mysql&gt; START TRANSACTION; -- 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql&gt; INSERT INTO demo.mytrans VALUES (1,1,5); -- 插入流水
Query OK, 1 row affected (0.00 sec)
mysql&gt; UPDATE demo.inventory SET invquantity = invquantity - 5 WHERE itemnumber = 1; -- 更新库存
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql&gt; COMMIT; -- 提交事务
Query OK, 0 rows affected (0.06 sec)
```
然后我们查询一下结果:
```
mysql&gt; SELECT * FROM demo.mytrans; -- 流水插入成功了
+---------+------------+----------+
| transid | itemnumber | quantity |
+---------+------------+----------+
| 1 | 1 | 5.000 |
+---------+------------+----------+
1 row in set (0.00 sec)
mysql&gt; SELECT * FROM demo.inventory; -- 库存消减成功了
+------------+-------------+
| itemnumber | invquantity |
+------------+-------------+
| 1 | 5.000 |
+------------+-------------+
1 row in set (0.00 sec)
```
这样通过把2个相关操作放到事务里面我们就实现了一个事务操作。
这里有一个坑,我要提醒你一下。**事务并不会自动帮你处理SQL语句执行中的错误**,如果你对事务中的某一步数据操作发生的错误不做处理,继续提交的话,仍然会导致数据不一致。
为了方便你理解,我举个小例子。
假如我们的插入一条销售流水的语句少了一个字段,执行的时候出现错误了,如果我们不对这个错误做回滚处理,继续执行后面的操作,最后提交事务,结果就会出现没有流水但库存消减了的情况:
```
mysql&gt; START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql&gt; INSERT INTO demo.mytrans VALUES (1,5); -- 这个插入语句出错了
ERROR 1136 (21S01): Column count doesn't match value count at row 1
mysql&gt; UPDATE demo.inventory SET invquantity = invquantity - 5 WHERE itemnumber = 1;
Query OK, 1 row affected (0.00 sec) -- 后面的更新语句仍然执行成功了
Rows matched: 1 Changed: 1 Warnings: 0
mysql&gt; COMMIT;
Query OK, 0 rows affected (0.03 sec) -- 事务提交成功了
```
我们查一下表的内容:
```
mysql&gt; SELECT * FROM demo.mytrans; -- 流水没有插入成功
Empty set (0.16 sec)
mysql&gt; SELECT * FROM demo.inventory; -- 库存消减成功了
+------------+-------------+
| itemnumber | invquantity |
+------------+-------------+
| 1 | 5.000 |
+------------+-------------+
1 row in set (0.00 sec)
```
结果显示,流水插入失败了,但是库存更新成功了,这时候没有销售流水,但是库存却被消减了。
这就是因为没有正确使用事务导致的数据不完整问题。那么,如何使用事务,才能避免这种由于事务中的某一步或者几步操作出现错误,而导致数据不完整的情况发生呢?这就要用到事务中错误处理和回滚了:
- 如果发现事务中的某个操作发生错误,要及时使用回滚;
- 只有事务中的所有操作都可以正常执行,才进行提交。
那这里的关键就是判断操作是不是发生了错误。我们可以通过MySQL的函数ROW_COUNT()的返回来判断一个DML操作是否失败-1表示操作失败否则就表示影响的记录数。
```
mysql&gt; INSERT INTO demo.mytrans VALUES (1,5);
ERROR 1136 (21S01): Column count doesn't match value count at row 1
mysql&gt; SELECT ROW_COUNT();
+-------------+
| ROW_COUNT() |
+-------------+
| -1 |
+-------------+
1 row in set (0.00 sec)
```
另外一个经常会用到事务的地方是存储过程。由于存储过程中包含很多相互关联的数据操作所以会大量使用事务。我们可以在MySQL的存储过程中通过获取SQL错误来决定事务是提交还是回滚
```
mysql&gt; DELIMITER // -- 修改分隔符为 //
mysql&gt; CREATE PROCEDURE demo.mytest() -- 创建存储过程
-&gt; BEGIN -- 开始程序体
-&gt; DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK; -- 定义SQL操作发生错误是自动回滚
-&gt; START TRANSACTION; -- 开始事务
-&gt; INSERT INTO demo.mytrans VALUES (1,5);
-&gt; UPDATE demo.inventory SET invquantity = invquantity - 5;
-&gt; COMMIT; -- 提交事务
-&gt; END
-&gt; // -- 完成创建存储过程
Query OK, 0 rows affected (0.05 sec)
mysql&gt; DELIMITER ; -- 恢复分隔符为;
mysql&gt; CALL demo.mytest(); -- 调用存储过程
Query OK, 0 rows affected (0.00 sec)
mysql&gt; SELECT * FROM demo.mytrans; -- 销售流水没有插入
Empty set (0.00 sec)
mysql&gt; SELECT * FROM demo.inventory; -- 库存也没有消减,说明事务回滚了
+------------+-------------+
| itemnumber | invquantity |
+------------+-------------+
| 1 | 10.000 |
+------------+-------------+
1 row in set (0.00 sec)
```
这里我们要先通过“DELIMITER //”语句把MySQL语句的结束标识改为“//”(默认语句的结束标识是“;”。这样做的目的是告诉MySQL一直到“//”才是语句的结束否则MySQL会在遇到第一个“;”的时候认为语句已经结束,并且执行。这样就会报错,自然也就没办法创建存储过程了。
创建结束以后,我们还要录入“//”告诉MySQL存储过程创建完成了并且通过“DELIMITER ;”,再把语句结束标识改回到“;”。
关于存储过程我会在后面的课程里给你详细介绍。这里你只需要知道在这个存储过程中我使用了“DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;”这个语句来监控SQL语句的执行结果一旦发发生错误就自动回滚并退出。通过这个机制我们就实现了对事务中的SQL操作进行监控如果发现事务中的任何SQL操作发生错误就自动回滚。
总之,**我们要把重要的关联操作放在事务中,确保操作的原子性,并且对失败的操作进行回滚处理**。只有这样,才能真正发挥事务的作用,保证关联操作全部成功或全部失败,最终确保数据的一致性。
## 如何用好事务的隔离性?
接下来,我们再学习下如何用好事务的隔离性。
超市经营者提出门店要支持网上会员销售现在我们假设会员张三是储值会员他的会员卡里有100元。张三用会员卡到门店消费100元他爱人用他的会员卡在网上消费100元。
张三在门店消费结算的时候开启了一个事务A包括这样3个操作
1. 读取卡内金额为100
1. 更新卡内金额为0
1. 插入一条销售流水。
张三的爱人在网上购物开启了一个事务B也来读取卡内金额。如果B读取卡内金额的操作发生在A更新卡内金额之后并且在插入销售流水之前那么B读出的金额应该是多少呢如果B读出0元那么A有可能由于后面的操作失败而回滚。因此B可能会读到一条错误信息而导致本来可以成功的交易失败。有什么办法可以解决这个问题呢
这个时候就会用到MySQL的另外一种机制“锁”。MySQL可以把A中被修改过而且还没有提交的数据锁住让B处于等待状态一直到A提交完成或者失败回滚再释放锁允许B读取这个数据。这样就可以防止因为A回滚而导致B读取错误的可能了。
MySQL中的锁有很多种功能也十分强大。咱们这门课里不要求你掌握锁你只要知道MySQL可以用锁来控制事务对数据的操作就可以了。
通过对锁的使用,可以实现事务之间的相互隔离。**锁的使用方式不同,隔离的程度也不同**。
MySQL支持4种事务隔离等级。
1. READ UNCOMMITTED可以读取事务中还未提交的被更改的数据。
1. READ COMMITTED只能读取事务中已经提交的被更改的数据。
1. REPEATABLE READ表示一个事务中对一个数据读取的值永远跟第一次读取的值一致不受其他事务中数据操作的影响。这也是MySQL的默认选项。
1. SERIALIZABLE表示任何一个事务一旦对某一个数据进行了任何操作那么一直到这个事务结束MySQL都会把这个数据锁住禁止其他事务对这个数据进行任何操作。
一般来讲使用MySQL默认的隔离等级REPEATABLE READ就已经够了。不过也不排除需要对一些关键的数据操作使用最高的隔离等级SERIALIZABLE。
举个例子,在我们的超市项目中,就对每天的日结操作设置了最高的隔离等级。因为日结要进行大量的核心数据计算,包括成本、毛利、毛利率、周转率,等等,并把结果保存起来,作为各类查询、报表系统、决策支持模块的基础,绝对不能出现数据错误。
当然,**计算完成之后,你也不要忘记把隔离等级恢复到系统默认的状态**,否则,会对日常的系统营运效率产生比较大的影响。
事务的隔离性对并发操作非常有用。当许多用户同时操作数据库的时候,隔离性可以确保各个连接之间互相不影响。这里我要提醒你的是,正确设置事务的隔离等级很重要。
一方面,**对于一些核心的数据更改操作,你可能需要较高的隔离等级**,比如涉及金额的修改;另一方面,**你要考虑资源的消耗,不能使系统整体的效率受到太大的影响**。所以,要根据具体的应用场景,正确地使用事务。
## 总结
事务可以确保事务中的一系列操作全部被执行不会被打断或者全部不被执行等待再次执行。事务中的操作具有原子性、一致性、永久性和隔离性的特征。但是这并不意味着被事务包裹起来的一系列DML数据操作就一定会全部成功或者全部失败。你需要对操作是否成功的结果进行判断并通知MySQL针对不同情况分别完成事务提交或者回滚操作才能最终确保事务中的操作全部成功或全部失败。
MySQL支持4种不同的事务隔离等级等级越高消耗的系统资源也越多你要根据实际情况进行设定。
在MySQL中并不是所有的操作都可以回滚。比如创建数据库、创建数据表、删除数据库、删除数据表等这些操作是不可以回滚的所以你在操作的时候要特别小心特别是在删除数据库、数据表时最好先做备份防止误操作。
## 思考题
学完了这节课以后,如果现在有人对你说,事务就是确保事务中的数据操作,要么全部正确执行,要么全部失败,你觉得这句话对吗?为什么?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。

View File

@@ -0,0 +1,267 @@
<audio id="audio" title="13 | 临时表:复杂查询,如何保存中间结果?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a4/63/a45504b0c686ff7962783841892f3263.mp3"></audio>
你好,我是朱晓峰。今天,我来和你聊一聊临时表。
当我们遇到一些复杂查询的时候,经常无法一步到位,或者是一步到位会导致查询语句太过复杂,开发和维护的成本过高。这个时候,就可以使用临时表。
下面,我就结合实际的项目来讲解一下,怎么拆解一个复杂的查询,通过临时表来保存中间结果,从而把一个复杂查询变得简单而且容易实现。
## 临时表是什么?
临时表是一种特殊的表,用来存储查询的中间结果,并且会随着当前连接的结束而自动删除。**MySQL中有2种临时表分别是内部临时表和外部临时表**
- 内部临时表主要用于性能优化,由系统自动产生,我们无法看到;
- 外部临时表通过SQL语句创建我们可以使用。
因为我们不能使用内部临时表,所以我就不多讲了。今天,我来重点讲一讲我们可以创建和使用的外部临时表。
首先,你要知道临时表的创建语法结构:
```
CREATE TEMPORARY TABLE 表名
(
字段名 字段类型,
...
);
```
跟普通表相比临时表有3个不同的特征
1. 临时表的创建语法需要用到关键字TEMPORARY
1. 临时表创建完成之后,只有当前连接可见,其他连接是看不到的,具有连接隔离性;
1. 临时表在当前连接结束之后,会被自动删除。
因为临时表有连接隔离性不同连接创建相同名称的临时表也不会产生冲突适合并发程序的运行。而且连接结束之后临时表会自动删除也不用担心大量无用的中间数据会残留在数据库中。因此我们就可以利用这些特点用临时表来存储SQL查询的中间结果。
## 如何用临时表简化复杂查询?
刚刚提到,临时表可以简化复杂查询,具体是怎么实现的呢?我来介绍一下。
举个例子超市经营者想要查询2020年12月的一些特定商品销售数量、进货数量、返厂数量那么我们就要先把销售、进货、返厂这3个模块分开计算用临时表来存储中间计算的结果最后合并在一起形成超市经营者想要的结果集。
首先我们统计一下在2020年12月的商品销售数据。
假设我们的销售流水表mysales如下所示
<img src="https://static001.geekbang.org/resource/image/ay/f5/ayy269bb7dd210a1f1716ebd2947e9f5.jpeg" alt="">
我们可以用下面的SQL语句查询出每个单品的销售数量和销售金额并存入临时表
```
mysql&gt; CREATE TEMPORARY TABLE demo.mysales
-&gt; SELECT -- 用查询的结果直接生成临时表
-&gt; itemnumber,
-&gt; SUM(quantity) AS QUANTITY,
-&gt; SUM(salesvalue) AS salesvalue
-&gt; FROM
-&gt; demo.transactiondetails
-&gt; GROUP BY itemnumber
-&gt; ORDER BY itemnumber;
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql&gt; 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
进货单头表包括进货单编号、供货商编号、仓库编号、操作员编号和验收日期:
<img src="https://static001.geekbang.org/resource/image/0a/80/0acf5b6ee4f154414fefd543b33f7180.jpeg" alt="">
进货单明细表包括进货单编号、商品编号、进货数量、进货价格和进货金额:
<img src="https://static001.geekbang.org/resource/image/36/36/368d0f558d497be064d264baaf99e636.jpeg" alt="">
我们用下面的SQL语句计算进货数据并且保存在临时表里面
```
mysql&gt; CREATE TEMPORARY TABLE demo.myimport
-&gt; SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.importvalue) AS importvalue
-&gt; FROM demo.importhead a JOIN demo.importdetails b
-&gt; ON (a.listnumber=b.listnumber)
-&gt; GROUP BY b.itemnumber;
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql&gt; 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
返厂单头表包括返厂单编号、供货商编号、仓库编号、操作员编号和验收日期:
<img src="https://static001.geekbang.org/resource/image/20/3e/206c5ba8bdcb13c55c78a1ec1f4aab3e.jpeg" alt="">
返厂单明细表包括返厂单编号、商品编号、返厂数量、返厂价格和返厂金额:
<img src="https://static001.geekbang.org/resource/image/a9/4b/a9bda457d8f15e8f87fc5d0f4e2ee24b.jpeg" alt="">
我们可以使用下面的SQL语句计算返厂信息并且保存到临时表中。
```
mysql&gt; CREATE TEMPORARY TABLE demo.myreturn
-&gt; SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.returnvalue) AS returnvalue
-&gt; FROM demo.returnhead a JOIN demo.returndetails b
-&gt; ON (a.listnumber=b.listnumber)
-&gt; GROUP BY b.itemnumber;
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql&gt; 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月的商品销售数量、进货数量和返厂数量。
代码如下所示:
```
mysql&gt; SELECT
-&gt; a.itemnumber,
-&gt; a.goodsname,
-&gt; ifnull(b.quantity,0) as salesquantity, -- 如果没有销售记录销售数量设置为0
-&gt; ifnull(c.quantity,0) as importquantity, -- 如果没有进货进货数量设为0
-&gt; ifnull(d.quantity,0) as returnquantity -- 如果没有返厂返厂数量设为0
-&gt; FROM
-&gt; demo.goodsmaster a -- 商品信息表放在左边进行左连接,确保所有的商品都包含在结果集中
-&gt; LEFT JOIN demo.mysales b
-&gt; ON (a.itemnumber=b.itemnumber)
-&gt; LEFT JOIN demo.myimport c
-&gt; ON (a.itemnumber=c.itemnumber)
-&gt; LEFT JOIN demo.myreturn d
-&gt; ON (a.itemnumber=d.itemnumber)
-&gt; HAVING salesquantity&gt;0 OR importquantity&gt;0 OR returnquantity&gt;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临时表存储在内存中。
好了,现在我们先来创建一个内存中的临时表:
```
mysql&gt; CREATE TEMPORARY TABLE demo.mytrans
-&gt; (
-&gt; itemnumber int,
-&gt; groupnumber int,
-&gt; branchnumber int
-&gt; ) ENGINE = MEMORY; (临时表数据存在内存中)
Query OK, 0 rows affected (0.00 sec)
```
接下来我们在磁盘上创建一个同样结构的临时表。在磁盘上创建临时表时只要我们不指定存储引擎MySQL会默认存储引擎是InnoDB并且把表存放在磁盘上。
```
mysql&gt; CREATE TEMPORARY TABLE demo.mytransdisk
-&gt; (
-&gt; itemnumber int,
-&gt; groupnumber int,
-&gt; branchnumber int
-&gt; );
Query OK, 0 rows affected (0.00 sec)
```
现在,我们向刚刚的两张表里都插入同样数量的记录,然后再分别做一个查询:
```
mysql&gt; SELECT COUNT(*) FROM demo.mytrans;
+----------+
| count(*) |
+----------+
| 4355 |
+----------+
1 row in set (0.00 sec)
mysql&gt; SELECT COUNT(*) FROM demo.mytransdisk;
+----------+
| count(*) |
+----------+
| 4355 |
+----------+
1 row in set (0.21 sec)
```
可以看到区别是比较明显的。对于同一条查询内存中的临时表执行时间不到10毫秒而磁盘上的表却用掉了210毫秒。显然内存中的临时表查询速度更快。
不过,内存中的临时表也有缺陷。因为数据完全在内存中,所以,一旦断电,数据就消失了,无法找回。不过临时表只保存中间结果,所以还是可以用的。
我画了一张图,汇总了内存临时表和磁盘临时表的优缺点:
<img src="https://static001.geekbang.org/resource/image/c5/bf/c5f3d549f5f0fd72e74ec9c5441467bf.jpeg" alt="">
## 总结
这节课,我们学习了临时表的概念,以及使用临时表来存储中间结果以拆分复杂查询的方法。临时表可以存储在磁盘中,也可以通过指定引擎的办法存储在内存中,以加快存取速度。
其实临时表有很多好处除了可以帮助我们把复杂的SQL查询拆分成多个简单的SQL查询而且因为临时表是连接隔离的不同的连接可以使用相同的临时表名称相互之间不会受到影响。除此之外临时表会在连接结束的时候自动删除不会占用磁盘空间。
当然,临时表也有不足,比如会挤占空间。我建议你,**在使用临时表的时候,要从简化查询和挤占资源两个方面综合考虑,既不能过度加重系统的负担,同时又能够通过存储中间结果,最大限度地简化查询**。
## 思考题
我们有这样的一个销售流水表:
<img src="https://static001.geekbang.org/resource/image/a9/b6/a970618a3807cyyeeaf28ac57fa034b6.jpeg" alt="">
假设有多个门店,每个门店有多台收款机,每台收款机销售多种商品,请问如何查询每个门店、每台收款机的销售金额占所属门店的销售金额的比率呢?
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。