mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
327
极客时间专栏/MySQL 必知必会/实践篇/01 | 存储:一个完整的数据存储过程是怎样的?.md
Normal file
327
极客时间专栏/MySQL 必知必会/实践篇/01 | 存储:一个完整的数据存储过程是怎样的?.md
Normal 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> 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> 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> 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/)学习下。
|
||||
|
||||
## 思考题
|
||||
|
||||
我想请你思考一下,假设用户现在要销售商品,你能不能帮它设计一个销售表,把销售信息(商品名称、价格、数量、金额等)都保存起来?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。
|
||||
295
极客时间专栏/MySQL 必知必会/实践篇/02 | 字段:这么多字段类型,该怎么定义?.md
Normal file
295
极客时间专栏/MySQL 必知必会/实践篇/02 | 字段:这么多字段类型,该怎么定义?.md
Normal 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、INT(INTEGER)和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> SELECT *
|
||||
-> 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> SELECT SUM(price)
|
||||
-> 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用DECIMAL(M,D)的方式表示高精度小数。其中,M表示整数部分加小数部分,一共有多少位,M<=65。D表示小数部分位数,D<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种,它们的区别就是最大长度不同。
|
||||
|
||||
- TINYTEXT:255字符(这里假设字符是ASCII码,一个字符占用一个字节,下同)。
|
||||
- TEXT: 65535字符。
|
||||
- MEDIUMTEXT:16777215字符。
|
||||
- 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:59~23: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),你可以对照着看下。
|
||||
|
||||
## 思考题
|
||||
|
||||
假设用户需要一个表来记录会员信息,会员信息包括会员卡编号、会员名称、会员电话、积分值。如果要你为这些字段定义数据类型,你会如何选择呢?为什么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。
|
||||
491
极客时间专栏/MySQL 必知必会/实践篇/03 | 表:怎么创建和修改表?.md
Normal file
491
极客时间专栏/MySQL 必知必会/实践篇/03 | 表:怎么创建和修改表?.md
Normal 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 <表名>
|
||||
{
|
||||
字段名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> 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> 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> 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> ALTER TABLE demo.importheadhist
|
||||
-> ADD confirmer INT; -- 添加一个字段confirmer,类型INT
|
||||
Query OK, 0 rows affected (0.04 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
mysql> ALTER TABLE demo.importheadhist
|
||||
-> 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> 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> ALTER TABLE demo.importheadhist
|
||||
-> CHANGE quantity importquantity DOUBLE;
|
||||
Query OK, 0 rows affected (0.15 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
运行这个SQL语句,查看表的结构,我们会得到下面的结果:
|
||||
|
||||
```
|
||||
mysql> 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> 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> 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”改成不能重复,并且不能为空。
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
554
极客时间专栏/MySQL 必知必会/实践篇/04 | 增删改查:如何操作表中的数据?.md
Normal file
554
极客时间专栏/MySQL 必知必会/实践篇/04 | 增删改查:如何操作表中的数据?.md
Normal 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> 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> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> 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> 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> SELECT *
|
||||
-> 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 > 1;
|
||||
|
||||
```
|
||||
|
||||
好了,下面我们来学习一下修改数据。
|
||||
|
||||
## 修改数据
|
||||
|
||||
我们来看一下MySQL的数据修改语法:
|
||||
|
||||
```
|
||||
UPDATE 表名
|
||||
SET 字段名=值
|
||||
WHERE 条件
|
||||
|
||||
```
|
||||
|
||||
语法很简单,需要注意的一点是,**不要修改主键字段的值**。因为主键是数据记录的唯一标识,如果修改了主键的值,就有可能会破坏数据的完整性。
|
||||
|
||||
下面我们来看一个通过主键查询商品信息的例子,看看修改主键的值,会产生什么样的结果。
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> FROM demo.goodsmaster
|
||||
-> WHERE itemnumber = 3;
|
||||
+------------+---------+-----------+---------------+------+-------+
|
||||
| itemnumber | barcode | goodsname | specification | unit | price |
|
||||
+------------+---------+-----------+---------------+------+-------+
|
||||
| 3 | 0003 | 尺子 | 三角型 | 把 | 5.00 |
|
||||
+------------+---------+-----------+---------------+------+-------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
我们能查询到一条商品编号是3的数据记录:条码是“0003”,名称是“尺子”,价格是5元。
|
||||
|
||||
如果我们修改了主键的值,就可能会改变刚才的查询结果:
|
||||
|
||||
```
|
||||
mysql> UPDATE demo.goodsmaster
|
||||
-> SET itemnumber=2
|
||||
-> WHERE itemnumber = 3;
|
||||
Query OK, 1 row affected (0.02 sec)
|
||||
Rows matched: 1 Changed: 1 Warnings: 0
|
||||
|
||||
mysql> SELECT *
|
||||
-> FROM demo.goodsmaster
|
||||
-> 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> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> FROM demo.goodsmaster
|
||||
-> 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> SELECT *
|
||||
-> FROM demo.goodsmaster
|
||||
-> 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中,起始位置的起点是0,1表示从第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> SELECT *
|
||||
-> FROM demo.goodsmaster;
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| itemnumber | barcode | goodsname | specification | unit | salesprice |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
|
||||
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
|
||||
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
3 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
门店B的商品信息表是“demo.goodsmaster1”:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> FROM demo.goodsmaster;
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| itemnumber | barcode | goodsname | specification | unit | salesprice |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| 1 | 0001 | 教科书 | 16开 | 本 | 89.00 |
|
||||
| 2 | 0002 | 笔 | 10支装 | 包 | 5.00 |
|
||||
| 3 | 0003 | 橡皮 | NULL | 个 | 3.00 |
|
||||
| 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”的值不连续的情况。请你想一想,如何插入数据,才能防止这种情况的发生呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。
|
||||
436
极客时间专栏/MySQL 必知必会/实践篇/05 | 主键:如何正确设置主键?.md
Normal file
436
极客时间专栏/MySQL 必知必会/实践篇/05 | 主键:如何正确设置主键?.md
Normal 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> CREATE TABLE demo.membermaster
|
||||
-> (
|
||||
-> cardno CHAR(8) PRIMARY KEY, -- 会员卡号为主键
|
||||
-> membername TEXT,
|
||||
-> memberphone TEXT,
|
||||
-> memberpid TEXT,
|
||||
-> memberaddress TEXT,
|
||||
-> sex TEXT,
|
||||
-> birthday DATETIME
|
||||
-> );
|
||||
Query OK, 0 rows affected (0.06 sec)
|
||||
|
||||
```
|
||||
|
||||
我们来查询一下表的结构,确认下主键是否创建成功了:
|
||||
|
||||
```
|
||||
mysql> 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> INSERT INTO demo.membermaster
|
||||
-> (
|
||||
-> cardno,
|
||||
-> membername,
|
||||
-> memberphone,
|
||||
-> memberpid,
|
||||
-> memberaddress,
|
||||
-> sex,
|
||||
-> birthday
|
||||
-> )
|
||||
-> VALUES
|
||||
-> (
|
||||
-> '10000001',
|
||||
-> '张三',
|
||||
-> '13812345678',
|
||||
-> '110123200001017890',
|
||||
-> '北京',
|
||||
-> '男',
|
||||
-> '2000-01-01'
|
||||
-> );
|
||||
Query OK, 1 row affected (0.01 sec)
|
||||
|
||||
mysql> INSERT INTO demo.membermaster
|
||||
-> (
|
||||
-> cardno,
|
||||
-> membername,
|
||||
-> memberphone,
|
||||
-> memberpid,
|
||||
-> memberaddress,
|
||||
-> sex,
|
||||
-> birthday
|
||||
-> )
|
||||
-> VALUES
|
||||
-> (
|
||||
-> '10000002',
|
||||
-> '李四',
|
||||
-> '13512345678',
|
||||
-> '123123199001012356',
|
||||
-> '上海',
|
||||
-> '女',
|
||||
-> '1990-01-01'
|
||||
-> );
|
||||
Query OK, 1 row affected (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
插入成功后,我们来看一下表的内容:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> 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> CREATE table demo.trans
|
||||
-> (
|
||||
-> transactionno INT,
|
||||
-> itemnumber INT, -- 为了引用商品信息
|
||||
-> quantity DECIMAL(10,3),
|
||||
-> price DECIMAL(10,2),
|
||||
-> salesvalue DECIMAL(10,2),
|
||||
-> cardno CHAR(8), -- 为了引用会员信息
|
||||
-> transdate DATETIME
|
||||
-> );
|
||||
Query OK, 0 rows affected (0.10 sec)
|
||||
|
||||
```
|
||||
|
||||
创建好表以后,我们来插入一条销售流水:
|
||||
|
||||
```
|
||||
mysql> INSERT INTO demo.trans
|
||||
-> (
|
||||
-> transactionno,
|
||||
-> itemnumber,
|
||||
-> quantity,
|
||||
-> price,
|
||||
-> salesvalue,
|
||||
-> cardno,
|
||||
-> transdate
|
||||
-> )
|
||||
-> VALUES
|
||||
-> (
|
||||
-> 1,
|
||||
-> 1,
|
||||
-> 1,
|
||||
-> 89,
|
||||
-> 89,
|
||||
-> '10000001',
|
||||
-> '2020-12-01'
|
||||
-> );
|
||||
Query OK, 1 row affected (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
接着,我们查询一下2020年12月01日的会员销售记录:
|
||||
|
||||
```
|
||||
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
|
||||
-> FROM demo.trans AS a
|
||||
-> JOIN demo.membermaster AS b
|
||||
-> JOIN demo.goodsmaster AS c
|
||||
-> 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> UPDATE demo.membermaster
|
||||
-> SET membername = '王五',
|
||||
-> memberphone = '13698765432',
|
||||
-> memberpid = '475145197001012356',
|
||||
-> memberaddress='天津',
|
||||
-> sex='女',
|
||||
-> birthday = '1970-01-01'
|
||||
-> WHERE cardno = '10000001';
|
||||
Query OK, 1 row affected (0.02 sec)
|
||||
Rows matched: 1 Changed: 1 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
会员记录改好了,我们再次运行之前的会员消费流水查询:
|
||||
|
||||
```
|
||||
mysql> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
|
||||
-> FROM demo.trans AS a
|
||||
-> JOIN demo.membermaster AS b
|
||||
-> JOIN demo.goodsmaster AS c
|
||||
-> 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> ALTER TABLE demo.membermaster
|
||||
-> DROP PRIMARY KEY;
|
||||
Query OK, 2 rows affected (0.12 sec)
|
||||
Records: 2 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
第二步,修改会员信息表,添加字段“id”为主键,并且给它定义自增约束:
|
||||
|
||||
```
|
||||
mysql> ALTER TABLE demo.membermaster
|
||||
-> ADD id INT PRIMARY KEY AUTO_INCREMENT;
|
||||
Query OK, 0 rows affected (0.12 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
第三步,修改销售流水表,添加新的字段memberid,对应会员信息表中的主键:
|
||||
|
||||
```
|
||||
mysql> ALTER TABLE demo.trans
|
||||
-> ADD memberid INT;
|
||||
Query OK, 0 rows affected (0.04 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
第四步,我们更新一下销售流水表,给新添加的字段“memberid”赋值,让它指向对应的会员信息:
|
||||
|
||||
```
|
||||
mysql> UPDATE demo.trans AS a,demo.membermaster AS b
|
||||
-> SET a.memberid=b.id
|
||||
-> WHERE a.transactionno > 0
|
||||
--> 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> 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> 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> INSERT INTO demo.membermaster
|
||||
-> (
|
||||
-> cardno,
|
||||
-> membername,
|
||||
-> memberphone,
|
||||
-> memberpid,
|
||||
-> memberaddress,
|
||||
-> sex,
|
||||
-> birthday
|
||||
-> )
|
||||
-> VALUES
|
||||
-> (
|
||||
-> '10000001',
|
||||
-> '王五',
|
||||
-> '13698765432',
|
||||
-> '475145197001012356',
|
||||
-> '天津',
|
||||
-> '女',
|
||||
-> '1970-01-01'
|
||||
-> );
|
||||
Query OK, 1 row affected (0.02 sec)
|
||||
|
||||
```
|
||||
|
||||
下面我们看看现在的会员信息:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> 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> SELECT b.membername,c.goodsname,a.quantity,a.salesvalue,a.transdate
|
||||
-> FROM demo.trans AS a
|
||||
-> JOIN demo.membermaster AS b
|
||||
-> JOIN demo.goodsmaster AS c
|
||||
-> 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%,该怎么实现呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
426
极客时间专栏/MySQL 必知必会/实践篇/06 | 外键和连接:如何做关联查询?.md
Normal file
426
极客时间专栏/MySQL 必知必会/实践篇/06 | 外键和连接:如何做关联查询?.md
Normal 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 <外键约束名称>] FOREIGN KEY 字段名
|
||||
REFERENCES <主表名> 字段名
|
||||
|
||||
```
|
||||
|
||||
你可以在创建表的时候定义外键约束:
|
||||
|
||||
```
|
||||
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> SELECT
|
||||
-> constraint_name, -- 表示外键约束名称
|
||||
-> table_name, -- 表示外键约束所属数据表的名称
|
||||
-> column_name, -- 表示外键约束的字段名称
|
||||
-> referenced_table_name, -- 表示外键约束所参照的数据表名称
|
||||
-> referenced_column_name -- 表示外键约束所参照的字段名称
|
||||
-> FROM
|
||||
-> information_schema.KEY_COLUMN_USAGE
|
||||
-> WHERE
|
||||
-> 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> SELECT
|
||||
-> a.transactionno,
|
||||
-> a.itemnumber,
|
||||
-> a.quantity,
|
||||
-> a.price,
|
||||
-> a.transdate,
|
||||
-> b.membername
|
||||
-> FROM
|
||||
-> demo.trans AS a
|
||||
-> JOIN
|
||||
-> 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> SELECT
|
||||
-> a.transactionno,
|
||||
-> a.itemnumber,
|
||||
-> a.quantity,
|
||||
-> a.price,
|
||||
-> a.transdate,
|
||||
-> b.membername
|
||||
-> FROM
|
||||
-> demo.trans AS a
|
||||
-> LEFT JOIN -- 左连接
|
||||
-> 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> 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);
|
||||
+---------------+------------+----------+-------+---------------------+------------+
|
||||
| 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> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> 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的同学,很容易忽略在关联表中定义外键约束的重要性,从而导致数据缺失,影响系统的可靠性。我建议你尽量养成在关联表中定义外键约束的习惯。不过,如果你的业务场景因为高并发等原因,无法承担外键约束的成本,也可以不定义外键约束,但是一定要在应用层面实现外键约束的逻辑功能,这样才能确保系统的正确可靠。
|
||||
|
||||
## 思考题
|
||||
|
||||
如果你的业务场景因高并发等原因,不能使用外键约束,在这种情况下,你怎么在应用层面确保数据的一致性呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
503
极客时间专栏/MySQL 必知必会/实践篇/07 | 条件语句:WHERE 与 HAVING有什么不同?.md
Normal file
503
极客时间专栏/MySQL 必知必会/实践篇/07 | 条件语句:WHERE 与 HAVING有什么不同?.md
Normal 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> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> 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> SELECT DISTINCT b.goodsname
|
||||
-> FROM demo.transactiondetails AS a
|
||||
-> JOIN demo.goodsmaster AS b
|
||||
-> ON (a.itemnumber=b.itemnumber)
|
||||
-> WHERE a.salesvalue > 50;
|
||||
+-----------+
|
||||
| goodsname |
|
||||
+-----------+
|
||||
| 书 |
|
||||
+-----------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
第二步,用HAVING关键字进行查询:
|
||||
|
||||
```
|
||||
mysql> SELECT b.goodsname
|
||||
-> FROM demo.transactiondetails AS a
|
||||
-> JOIN demo.goodsmaster AS b
|
||||
-> ON (a.itemnumber=b.itemnumber)
|
||||
-> GROUP BY b.goodsname
|
||||
-> HAVING max(a.salesvalue)>50;
|
||||
+-----------+
|
||||
| goodsname |
|
||||
+-----------+
|
||||
| 书 |
|
||||
+-----------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
可以发现,两次查询的结果是一样的。那么,这两种查询到底有什么区别,哪个更好呢?要弄明白这个问题,我们要先学习下WHERE和HAVING的执行过程。
|
||||
|
||||
## WHERE
|
||||
|
||||
我们先来分析一下刚才使用WHERE条件的查询语句,来看看MySQL是如何执行这个查询的。
|
||||
|
||||
首先,MySQL从数据表demo.transactiondetails中抽取满足条件“a.salesvalue>50”的记录:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> FROM demo.transactiondetails AS a
|
||||
-> WHERE a.salesvalue > 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> SELECT
|
||||
-> a.*, b.goodsname
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> demo.goodsmaster b ON (a.itemnumber = b.itemnumber)
|
||||
-> WHERE
|
||||
-> a.salesvalue > 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> SELECT
|
||||
-> b.goodsname
|
||||
-> FROM
|
||||
-> demo.transactiondetails AS a
|
||||
-> JOIN
|
||||
-> demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
|
||||
-> WHERE
|
||||
-> a.salesvalue > 50;
|
||||
+-----------+
|
||||
| goodsname |
|
||||
+-----------+
|
||||
| 书 |
|
||||
| 书 |
|
||||
+-----------+
|
||||
2 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,为了消除重复的语句,这里我们需要用到一个关键字:DISTINCT,它的作用是返回唯一不同的值。比如,DISTINCT 字段1,就表示返回所有字段1的不同的值。
|
||||
|
||||
下面我们尝试一下加上DISTINCT关键字的查询:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> DISTINCT(b.goodsname) -- 返回唯一不同的值
|
||||
-> FROM
|
||||
-> demo.transactiondetails AS a
|
||||
-> JOIN
|
||||
-> demo.goodsmaster AS b ON (a.itemnumber = b.itemnumber)
|
||||
-> WHERE
|
||||
-> a.salesvalue > 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> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> 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> SELECT *
|
||||
-> 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> SELECT
|
||||
-> a.transdate, -- 交易时间
|
||||
-> c.operatorname,-- 操作员
|
||||
-> d.goodsname, -- 商品名称
|
||||
-> b.quantity, -- 销售数量
|
||||
-> b.price, -- 价格
|
||||
-> b.salesvalue -- 销售金额
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.operator AS c ON (a.operatorid = c.operatorid)
|
||||
-> JOIN
|
||||
-> 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> SELECT
|
||||
-> a.transdate,
|
||||
-> SUM(b.quantity), -- 统计分组的总计销售数量
|
||||
-> SUM(b.salesvalue) -- 统计分组的总计销售金额
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> 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> SELECT
|
||||
-> a.transdate,
|
||||
-> c.operatorname,
|
||||
-> SUM(b.quantity), -- 数量求和
|
||||
-> SUM(b.salesvalue)-- 金额求和
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.operator AS C ON (a.operatorid = c.operatorid)
|
||||
-> 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> SELECT b.goodsname
|
||||
-> FROM demo.transactiondetails AS a
|
||||
-> JOIN demo.goodsmaster AS b
|
||||
-> ON (a.itemnumber=b.itemnumber)
|
||||
-> GROUP BY b.goodsname
|
||||
-> HAVING max(a.salesvalue)>50;
|
||||
+-----------+
|
||||
| goodsname |
|
||||
+-----------+
|
||||
| 书 |
|
||||
+-----------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
这种查询方式在MySQL里面是分四步实现的。
|
||||
|
||||
第一步,把流水明细表和商品信息表通过公共字段“itemnumber”连接起来,从2个表中获取数据:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> a.*, b.*
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> 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”的最大值>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> SELECT
|
||||
-> a.transdate, c.operatorname
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.operator AS c ON (a.operatorid = c.operatorid)
|
||||
-> GROUP BY a.transdate,c.operatorname
|
||||
-> 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> SELECT
|
||||
-> a.transdate,
|
||||
-> c.operatorname,
|
||||
-> d.goodsname,
|
||||
-> b.quantity,
|
||||
-> b.price,
|
||||
-> b.salesvalue
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.operator AS c ON (a.operatorid = c.operatorid)
|
||||
-> JOIN
|
||||
-> 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> SELECT
|
||||
-> a.transdate,
|
||||
-> c.operatorname,
|
||||
-> SUM(b.quantity), -- 销售数量求和
|
||||
-> SUM(b.salesvalue)-- 销售金额求和
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.operator AS c ON (a.operatorid = c.operatorid)
|
||||
-> GROUP BY a.transdate , operatorname -- 按照日期、收银员分组
|
||||
-> HAVING a.transdate IN ('2020-12-10' , '2020-12-11')
|
||||
-> AND SUM(b.salesvalue) > 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> SELECT
|
||||
-> a.transdate,
|
||||
-> c.operatorname,
|
||||
-> SUM(b.quantity),
|
||||
-> SUM(b.salesvalue)
|
||||
-> FROM
|
||||
-> demo.transactionhead AS a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails AS b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.operator AS c ON (a.operatorid = c.operatorid)
|
||||
-> WHERE a.transdate in ('2020-12-12','2020-12-11') -- 先按日期筛选
|
||||
-> GROUP BY a.transdate , operatorname
|
||||
-> HAVING SUM(b.salesvalue)>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后面的条件,必须是包含分组中的计算函数的条件,你觉得对吗?为什么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。
|
||||
345
极客时间专栏/MySQL 必知必会/实践篇/08 | 聚合函数:怎么高效地进行分组统计?.md
Normal file
345
极客时间专栏/MySQL 必知必会/实践篇/08 | 聚合函数:怎么高效地进行分组统计?.md
Normal 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> SELECT
|
||||
-> LEFT(b.transdate, 10), -- 从关联表获取交易时间,并且通过LEFT函数,获取交易时间字符串的左边10个字符,得到年月日的数据
|
||||
-> c.goodsname, -- 从关联表获取商品名称
|
||||
-> SUM(a.quantity), -- 数量求和
|
||||
-> SUM(a.salesvalue) -- 金额求和
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.goodsmaster c ON (a.itemnumber = c.itemnumber)
|
||||
-> GROUP BY LEFT(b.transdate, 10) , c.goodsname -- 分组
|
||||
-> ORDER BY LEFT(b.transdate, 10) , c.goodsname; -- 排序
|
||||
+-----------------------+-----------+-----------------+-------------------+
|
||||
| LEFT(b.transdate, 10) | goodsname | SUM(a.quantity) | SUM(a.salesvalue) |
|
||||
+-----------------------+-----------+-----------------+-------------------+
|
||||
| 2020-12-01 | 书 | 2.000 | 178.00 |
|
||||
| 2020-12-01 | 笔 | 5.000 | 25.00 |
|
||||
| 2020-12-02 | 书 | 4.000 | 356.00 |
|
||||
| 2020-12-02 | 笔 | 16.000 | 80.00 |
|
||||
+-----------------------+-----------+-----------------+-------------------+
|
||||
4 rows in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
可以看到,我们引入了2个关键字:LEFT 和 ORDER BY,你可能对它们不熟悉,我来具体解释下。
|
||||
|
||||
**LEFT(str,n)**:表示返回字符串str最左边的n个字符。我们这里的LEFT(a.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> SELECT
|
||||
-> SUM(quantity), -- 总计数量
|
||||
-> SUM(salesvalue)-- 总计金额
|
||||
-> FROM
|
||||
-> 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> SELECT
|
||||
-> LEFT(a.transdate, 10),
|
||||
-> c.goodsname,
|
||||
-> AVG(b.quantity), -- 平均数量
|
||||
-> AVG(b.salesvalue) -- 平均金额
|
||||
-> FROM
|
||||
-> demo.transactionhead a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
|
||||
-> GROUP BY LEFT(a.transdate,10),c.goodsname
|
||||
-> ORDER BY LEFT(a.transdate,10),c.goodsname;
|
||||
+-----------------------+-----------+-----------------+-------------------+
|
||||
| LEFT(a.transdate, 10) | goodsname | AVG(b.quantity) | AVG(b.salesvalue) |
|
||||
+-----------------------+-----------+-----------------+-------------------+
|
||||
| 2020-12-01 | 书 | 2.0000000 | 178.000000 |
|
||||
| 2020-12-01 | 笔 | 5.0000000 | 25.000000 |
|
||||
| 2020-12-02 | 书 | 2.0000000 | 178.000000 |
|
||||
| 2020-12-02 | 笔 | 8.0000000 | 40.000000 |
|
||||
+-----------------------+-----------+-----------------+-------------------+
|
||||
4 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
2.MAX()和MIN()
|
||||
|
||||
MAX()表示获取指定字段在分组中的最大值,MIN()表示获取指定字段在分组中的最小值。它们的实现原理差不多,下面我就重点讲一下MAX(),知道了它的用法,MIN()也就很好理解了。
|
||||
|
||||
我们还是来看具体的例子。假如用户要求计算每天里的一次销售的最大数量和最大金额,就可以用下面的代码,得到我们需要的结果:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> LEFT(a.transdate, 10),
|
||||
-> MAX(b.quantity), -- 数量最大值
|
||||
-> MAX(b.salesvalue) -- 金额最大值
|
||||
-> FROM
|
||||
-> demo.transactionhead a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
|
||||
-> GROUP BY LEFT(a.transdate,10)
|
||||
-> ORDER BY LEFT(a.transdate,10);
|
||||
+-----------------------+-----------------+-------------------+
|
||||
| LEFT(a.transdate, 10) | MAX(b.quantity) | MAX(b.salesvalue) |
|
||||
+-----------------------+-----------------+-------------------+
|
||||
| 2020-12-01 | 5.000 | 178.00 |
|
||||
| 2020-12-02 | 10.000 | 267.00 |
|
||||
+-----------------------+-----------------+-------------------+
|
||||
2 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
代码很简单,你一看就明白了。但是,这里有个问题你要注意:千万不要以为MAX(b.quantity)和MAX(b.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> SELECT *
|
||||
-> FROM demo.transactiondetails;
|
||||
+---------------+------------+----------+-------+------------+
|
||||
| transactionid | itemnumber | quantity | price | salesvalue |
|
||||
+---------------+------------+----------+-------+------------+
|
||||
| 1 | 1 | 2.000 | 89.00 | 178.00 |
|
||||
| 1 | 2 | 5.000 | 5.00 | 25.00 |
|
||||
| 2 | 1 | 3.000 | 89.00 | 267.00 |
|
||||
| 2 | 2 | 6.000 | 5.00 | 30.00 |
|
||||
| 3 | 1 | 1.000 | 89.00 | 89.00 |
|
||||
| 3 | 2 | 10.000 | 5.00 | 50.00 |
|
||||
+---------------+------------+----------+-------+------------+
|
||||
6 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
如果我们一屏可以显示30行,需要多少页才能显示完这个表的全部数据呢?
|
||||
|
||||
```
|
||||
mysql> SELECT COUNT(*)
|
||||
-> FROM demo.transactiondetails;
|
||||
+----------+
|
||||
| COUNT(*) |
|
||||
+----------+
|
||||
| 6 |
|
||||
+----------+
|
||||
1 row in set (0.03 sec)
|
||||
|
||||
```
|
||||
|
||||
我们这里只有6条数据,一屏就可以显示了,所以一共1页。
|
||||
|
||||
那么,如果超市经营者想知道,每天、每种商品都有几次销售,我们就需要按天、按商品名称,进行分组查询:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> LEFT(a.transdate, 10), c.goodsname, COUNT(*) -- 统计销售次数
|
||||
-> FROM
|
||||
-> demo.transactionhead a
|
||||
-> JOIN
|
||||
-> demo.transactiondetails b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.goodsmaster c ON (b.itemnumber = c.itemnumber)
|
||||
-> GROUP BY LEFT(a.transdate, 10) , c.goodsname
|
||||
-> ORDER BY LEFT(a.transdate, 10) , c.goodsname;
|
||||
+-----------------------+-----------+----------+
|
||||
| LEFT(a.transdate, 10) | goodsname | COUNT(*) |
|
||||
+-----------------------+-----------+----------+
|
||||
| 2020-12-01 | 书 | 1 |
|
||||
| 2020-12-01 | 笔 | 1 |
|
||||
| 2020-12-02 | 书 | 2 |
|
||||
| 2020-12-02 | 笔 | 2 |
|
||||
+-----------------------+-----------+----------+
|
||||
4 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
运行这段代码,我们就得到了每天、每种商品有几次销售的全部结果。
|
||||
|
||||
2.COUNT(字段)
|
||||
|
||||
COUNT(字段)用来统计分组内这个字段的值出现了多少次。如果字段值是空,就不统计。
|
||||
|
||||
为了说明它们的区别,我举个小例子。假设我们有这样的一个商品信息表,里面包括了商品编号、条码、名称、规格、单位和售价的信息。
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> FROM demo.goodsmaster;
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| itemnumber | barcode | goodsname | specification | unit | salesprice |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
|
||||
| 2 | 0002 | 笔 | NULL | 支 | 5.00 |
|
||||
| 3 | 0002 | 笔 | NULL | 支 | 10.00 |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
3 rows in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
如果我们要统计字段“goodsname”出现了多少次,就要用到函数COUNT(goodsname),结果是3次:
|
||||
|
||||
```
|
||||
mysql> SELECT COUNT(goodsname) -- 统计商品名称字段
|
||||
-> FROM demo.goodsmaster;
|
||||
+------------------+
|
||||
| COUNT(goodsname) |
|
||||
+------------------+
|
||||
| 3 |
|
||||
+------------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
如果我们统计字段“specification”,用COUNT(specification),结果是1次:
|
||||
|
||||
```
|
||||
mysql> SELECT COUNT(specification) -- 统计规格字段
|
||||
-> FROM demo.goodsmaster;
|
||||
+----------------------+
|
||||
| COUNT(specification) |
|
||||
+----------------------+
|
||||
| 1 |
|
||||
+----------------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
你可能会问,为啥计数字段“goodsname”的结果是3,计数字段“specification”却只有1呢?其实,这里的原因就是,3条记录里面的字段“goodsname”没有空值,因此被统计了3次;而字段“specification”有2个空值,因此只统计了1次。
|
||||
|
||||
理解了这一点,你就可以利用计数函数对某个字段计数时,不统计空值的特点,对表中字段的非空值进行计数了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我们学习了聚合函数SUM()、AVG()、MAX()、MIN()和COUNT()。我们在对分组数据进行统计的时候,可以用这些函数来对分组数据求和、求平均值、最大值、最小值,以及统计分组内的记录数,或者分组内字段的值不为空的次数。
|
||||
|
||||
这些函数,为我们对数据库中的数据进行统计和计算提供了方便。因为计算直接在数据库中执行,比在应用层面完成相同的工作,效率高很多。
|
||||
|
||||
最后,我还想多说一句,不知道你注意到没有,这节课我还提到了LEFT和ORDER BY。其实,聚合函数可以和其他关键字、函数一起使用,这样会拓展它的使用场景,让原本复杂的计算变简单。所以,我建议你不仅要认真学习这节课的聚合函数,还要掌握MySQL的各种关键字的功能和用法,并且根据实际工作的需要,尝试把它们组合在一起使用,这样就能利用好数据库的强大功能,更好地满足用户的需求。
|
||||
|
||||
## 思考题
|
||||
|
||||
如果用户想要查询一下,在商品信息表中,到底是哪种商品的商品名称有重复,分别重复了几次,该如何查询呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享你的朋友或同事,我们下节课见。
|
||||
343
极客时间专栏/MySQL 必知必会/实践篇/09 | 时间函数:时间类数据,MySQL是怎么处理的?.md
Normal file
343
极客时间专栏/MySQL 必知必会/实践篇/09 | 时间函数:时间类数据,MySQL是怎么处理的?.md
Normal 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()函数。**EXTRACT(type FROM date)表示从日期时间数据“date”中抽取“type”指定的部分**。
|
||||
|
||||
有了这个函数,我们就可以获取到交易时间的小时部分,从而完成一天中每小时的销售数量和销售金额的查询:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> EXTRACT(HOUR FROM b.transdate) AS 时段,
|
||||
-> SUM(a.quantity) AS 数量,
|
||||
-> SUM(a.salesvalue) AS 金额
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
|
||||
-> GROUP BY EXTRACT(HOUR FROM b.transdate)
|
||||
-> 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()来达到同样的效果。**HOUR(time)表示从日期时间“time”中,获取小时部分信息**。
|
||||
|
||||
需要注意的是,EXTRACT()函数中的“HOUR”表示要获取时间的类型,而HOUR()是一个函数,HOUR(time)可以单独使用,表示返回time的小时部分信息。
|
||||
|
||||
我们可以通过在代码中,把EXTRACT函数改成HOUR函数,来实现相同的功能,如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> HOUR(b.transdate) AS 时段, -- 改为使用HOUR函数
|
||||
-> SUM(a.quantity) AS 数量,
|
||||
-> SUM(a.salesvalue) AS 金额
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
|
||||
-> GROUP BY HOUR(b.transdate) -- 改写为HOUR函数
|
||||
-> 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也提供了支持的函数。
|
||||
|
||||
- YEAR(date):获取date中的年。
|
||||
- MONTH(date):获取date中的月。
|
||||
- DAY(date):获取date中的日。
|
||||
- HOUR(date):获取date中的小时。
|
||||
- MINUTE(date):获取date中的分。
|
||||
- SECOND(date):获取date中的秒。
|
||||
|
||||
这些函数的使用方法和提取小时信息的方法一样,我就不多说了,你只要知道这些函数的含义就可以了,下面我再讲一讲计算日期时间的函数。
|
||||
|
||||
## 计算日期时间的函数
|
||||
|
||||
我先来介绍2个常用的MySQL的日期时间计算函数。
|
||||
|
||||
- DATE_ADD(date, INTERVAL 表达式 type):表示计算从时间点“date”开始,向前或者向后一段时间间隔的时间。“表达式”的值为时间间隔数,正数表示向后,负数表示向前,“type”表示时间间隔的单位(比如年、月、日等)。
|
||||
- LAST_DAY(date):表示获取日期时间“date”所在月份的最后一天的日期。
|
||||
|
||||
这两个函数怎么用呢?接下来,我还是借助咱们项目的实际需求,来给你讲解下。假设今天是2020年12月10日,超市经营者提出,他们需要计算这个月单品销售金额的统计,以及与去年同期相比的增长率。
|
||||
|
||||
这里的关键点是需要获取2019年12月的销售数据。因此,计算2019年12月的起始和截止时间点,就是查询的关键。这个时候,就要用到计算日期时间函数了。
|
||||
|
||||
下面我重点讲解一下如何通过2个计算日期时间函数,来计算2019年12月的起始时间和截止时间。
|
||||
|
||||
我们先来尝试获取2019年12月份的起始时间。
|
||||
|
||||
第一步,用DATE_ADD函数,获取到2020年12月10日上一年的日期:2019年12月10日。
|
||||
|
||||
```
|
||||
mysql> 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> 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> 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> 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> 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”,也就是年月日的格式。
|
||||
- DAYOFWEEK(date):获取日期“date”是周几。1表示周日,2表示周一,以此类推,直到7表示周六。
|
||||
|
||||
假设今天是2021年02月06日,通过下面的代码,我们就可以查到今天商品的全部折后价格了:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> CURDATE() AS 日期,
|
||||
-> CASE DAYOFWEEK(CURDATE()) - 1 WHEN 0 THEN 7 ELSE DAYOFWEEK(CURDATE()) - 1 END AS 周几,
|
||||
-> a.goodsname AS 商品名称,
|
||||
-> a.salesprice AS 价格,
|
||||
-> IFNULL(b.discountrate,1) AS 折扣率,
|
||||
-> a.salesprice * IFNULL(b.discountrate, 1) AS 折后价格
|
||||
-> FROM
|
||||
-> demo.goodsmaster a
|
||||
-> LEFT JOIN
|
||||
-> demo.discountrule b ON (a.itemnumber = b.itemnumber
|
||||
-> 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> SELECT DATE_FORMAT("2020-12-01 13:25:50","%T");
|
||||
+-----------------------------------------+
|
||||
| DATE_FORMAT("2020-12-01 13:25:50","%T") |
|
||||
+-----------------------------------------+
|
||||
| 13:25:50 |
|
||||
+-----------------------------------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
李四习惯按照上下午的方式来查看时间,同样,他可以使用DATE_FORMAT()函数,通过指定格式“%r”来实现:
|
||||
|
||||
```
|
||||
mysql> SELECT DATE_FORMAT("2020-12-01 13:25:50","%r");
|
||||
+-----------------------------------------+
|
||||
| DATE_FORMAT("2020-12-01 13:25:50","%r") |
|
||||
+-----------------------------------------+
|
||||
| 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),你可以随时查看一下。
|
||||
|
||||
另外一个重要的时间函数是DATEDIFF(date1,date2),表示日期“date1”与日期“date2”之间差几天。假如你要计算某段时间的每天交易金额的平均值,只需要把起始日期和截止日期传给这个函数,就可以得到中间隔了几天。再用总计金额除以这个天数,就可以算出来了:
|
||||
|
||||
```
|
||||
mysql> SELECT DATEDIFF("2021-02-01","2020-12-01");
|
||||
+-------------------------------------+
|
||||
| DATEDIFF("2021-02-01","2020-12-01") |
|
||||
+-------------------------------------+
|
||||
| 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服务器获取时间。由于总部的服务器的配置和运维状况一般要好于门店,所以系统时间出现误差的可能性也较小。如果采用云服务器,系统时间的可靠性会更高。
|
||||
|
||||
## 思考题
|
||||
|
||||
假如用户想查一下今天是星期几(不能用数值,要用英文显示),你可以写一个简单的查询语句吗?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
365
极客时间专栏/MySQL 必知必会/实践篇/10 | 如何进行数学计算、字符串处理和条件判断?.md
Normal file
365
极客时间专栏/MySQL 必知必会/实践篇/10 | 如何进行数学计算、字符串处理和条件判断?.md
Normal 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(表达式,V1,V2)这样的条件判断函数;
|
||||
- ……
|
||||
|
||||
这些函数对我们管理数据库、提高数据处理的效率有很大的帮助。接下来,我就带你在解决实际问题的过程中,帮你掌握使用这些函数的方法。
|
||||
|
||||
## 数学函数
|
||||
|
||||
我们先来学习下数学函数,它主要用来处理数值数据,常用的主要有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> SELECT
|
||||
-> c.membername AS '会员', -- 从会员表获取会员名称
|
||||
-> b.transactionno AS '单号',-- 从销售单头表获取单号
|
||||
-> b.transdate AS '交易时间', -- 从销售单头表获取交易时间
|
||||
-> d.goodsname AS '商品名称', -- 从商品信息表获取商品名称
|
||||
-> a.salesvalue AS '交易金额'
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.membermaster c ON (b.memberid = c.memberid)
|
||||
-> JOIN
|
||||
-> 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)
|
||||
|
||||
```
|
||||
|
||||
接着,我们用FLOOR(a.salesvalue),对销售金额向下取整,获取会员积分值,代码如下:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> c.membername AS '会员',
|
||||
-> b.transactionno AS '单号',
|
||||
-> b.transdate AS '交易时间',
|
||||
-> d.goodsname AS '商品名称',
|
||||
-> a.salesvalue AS '交易金额',
|
||||
-> FLOOR(a.salesvalue) AS '积分' -- 使用FLOOR函数向下取整
|
||||
-> FROM
|
||||
-> demo.transactiondetails a
|
||||
-> JOIN
|
||||
-> demo.transactionhead b ON (a.transactionid = b.transactionid)
|
||||
-> JOIN
|
||||
-> demo.membermaster c ON (b.memberid = c.memberid)
|
||||
-> JOIN
|
||||
-> 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()的使用方法。
|
||||
|
||||
超市经营者提出,收银的时候,应收金额可以被设定四舍五入到哪一位。比如,可以设定四舍五入到元、到角,或者到分。
|
||||
|
||||
按照指定的位数,对小数进行四舍五入计算,这样的场景就要用到ROUND(X,D)了。它的作用是通过四舍五入,对数值X保留D位小数。
|
||||
|
||||
根据超市经营者的要求,我们把函数ROUND(X,D)中的保留小数的位数D设置成0、1和2。
|
||||
|
||||
如果要精确到分,我们可以设置保留2位小数:
|
||||
|
||||
```
|
||||
mysql> SELECT ROUND(salesvalue,2) -- D设置成2,表示保留2位小数,也就是精确到分
|
||||
-> FROM demo.transactiondetails
|
||||
-> WHERE transactionid=1 AND itemnumber=1;
|
||||
+---------------------+
|
||||
| ROUND(salesvalue,2) |
|
||||
+---------------------+
|
||||
| 176.22 |
|
||||
+---------------------+
|
||||
1rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
如果要精确到角,可以设置保留1位小数:
|
||||
|
||||
```
|
||||
mysql> SELECT ROUND(salesvalue,1) -- D设置成1,表示保留1位小数,也就是精确到角
|
||||
-> FROM demo.transactiondetails
|
||||
-> WHERE transactionid=1 AND itemnumber=1;
|
||||
+---------------------+
|
||||
| ROUND(salesvalue,1) |
|
||||
+---------------------+
|
||||
| 176.2 |
|
||||
+---------------------+
|
||||
1 rows in set (0.00 sec
|
||||
|
||||
```
|
||||
|
||||
如果要精确到元,可以设置保留0位小数:
|
||||
|
||||
```
|
||||
mysql> SELECT ROUND(salesvalue,0)-- D设置成0,表示保留0位小数,也就是精确到元
|
||||
-> FROM demo.transactiondetails
|
||||
-> WHERE transactionid=1 AND itemnumber=1;
|
||||
+---------------------+
|
||||
| ROUND(salesvalue,0) |
|
||||
+---------------------+
|
||||
| 176 |
|
||||
+---------------------+
|
||||
1 rows in set (0.00 se
|
||||
|
||||
```
|
||||
|
||||
除了刚刚我们所学习的函数,MySQL还支持绝对值函数ABS()和求余函数MOD(),ABS(X)表示获取X的绝对值;MOD(X,Y)表示获取X被Y除后的余数。
|
||||
|
||||
这些函数使用起来都比较简单,你重点掌握它们的含义就可以了,下面我再带你学习下字符串函数。
|
||||
|
||||
## 字符串函数
|
||||
|
||||
除了数学计算,我们还经常会遇到需要对字符串进行处理的场景,比如我们想要在金额前面加一个“¥”的符号,就会用到字符串拼接函数;再比如,我们需要把一组数字以字符串的形式在网上传输,就要用到类型转换函数。
|
||||
|
||||
常用的字符串函数有4个。
|
||||
|
||||
- CONCAT(s1,s2,...):表示把字符串s1、s2……拼接起来,组成一个字符串。
|
||||
- CAST(表达式 AS CHAR):表示将表达式的值转换成字符串。
|
||||
- CHAR_LENGTH(字符串):表示获取字符串的长度。
|
||||
- SPACE(n):表示获取一个由n个空格组成的字符串。
|
||||
|
||||
接下来我还是借助超市项目中的实际应用场景,来说明一下怎么使用这些字符串函数。
|
||||
|
||||
顾客交了钱,完成交易之后,系统必须要打出一张小票。打印小票时,对格式有很多要求。比如说,一张小票纸,57毫米宽,大概可以打32个字符,也就是16个汉字。用户要求一条流水打2行,第一行是商品信息,第二行要包括数量、价格、折扣和金额4种信息。那么,怎么才能清晰地在小票上打印出这些信息,并且打印得整齐漂亮呢?这就涉及对字符串的处理了。
|
||||
|
||||
首先,我们来看一下如何打印第一行的商品信息。商品信息包括:商品名称和商品规格,而且商品规格要包含在括号里面。这样就必须把商品名称和商品规格拼接起来,变成一个字符串。
|
||||
|
||||
这时,我们就可以用合并字符串函数CONCAT(),如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> CONCAT(goodsname, '(', specification, ')') AS 商品信息 -- 这里把商品名称、括号和规格拼接起来
|
||||
-> FROM
|
||||
-> demo.goodsmaster
|
||||
-> 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> SELECT
|
||||
-> CAST(quantity AS CHAR) -- 把decimal类型转换成字符串
|
||||
-> FROM
|
||||
-> demo.transactiondetails
|
||||
-> WHERE
|
||||
-> transactionid = 1 AND itemnumber =1;
|
||||
+---------------------+
|
||||
| CAST(price AS CHAR) |
|
||||
+---------------------+
|
||||
| 2.000 |
|
||||
+---------------------+
|
||||
1 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
第二步,计算字符串的长度,这里我们要用到CHAR_LENGTH()函数。
|
||||
|
||||
需要注意的是,虽然每个汉字打印的时候占2个字符长度,但是这个函数获取的是汉字的个数。因此,如果字符串中有汉字,函数获取的字符串长度跟实际打印的长度是不一样的,需要用空格来补齐。
|
||||
|
||||
我们可以通过下面的查询,获取数量字段转换成字符串后的字符串长度:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> CHAR_LENGTH(CAST(quantity AS CHAR)) AS 长度
|
||||
-> FROM
|
||||
-> demo.transactiondetails
|
||||
-> WHERE
|
||||
-> transactionid = 1 AND itemnumber =1;
|
||||
+---------------------+
|
||||
| 长度 |
|
||||
+---------------------+
|
||||
| 5 |
|
||||
+---------------------+
|
||||
1 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
第三步,用空格补齐7位长度。这时,我们要用到SPACE()函数。
|
||||
|
||||
因为我们采用左对齐的方式打印(左对齐表示字符串从左边开始,右边空余的位置用空格补齐),所以就需要先拼接字符串,再在字符串的后面补齐空格:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> CONCAT(CAST(quantity AS CHAR),
|
||||
-> SPACE(7 - CHAR_LENGTH(CAST(quantity AS CHAR)))) AS 数量
|
||||
-> FROM
|
||||
-> demo.transactiondetails
|
||||
-> WHERE
|
||||
-> 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,顾客会觉得奇怪,也不知道买了什么商品。我们希望,如果规格是空值,就不用加规格了。怎么实现呢?这就要用到条件判断函数了。
|
||||
|
||||
条件判断函数的主要作用,就是根据特定的条件返回不同的值,常用的有两种。
|
||||
|
||||
- IFNULL(V1,V2):表示如果V1的值不为空值,则返回V1,否则返回V2。
|
||||
- IF(表达式,V1,V2):如果表达式为真(TRUE),则返回V1,否则返回V2。
|
||||
|
||||
我们希望规格是空的商品,拼接商品信息字符串的时候,规格不要是空。这个问题,可以通过 IFNULL(specification, '')函数来解决。具体点说就是,对字段“specification”是否为空进行判断,如果为空,就返回空字符串,否则就返回商品规格specification的值。代码如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> goodsname,
|
||||
-> specification,
|
||||
-> CONCAT(goodsname,'(', IFNULL(specification, ''),')') AS 拼接 -- 用条件判断函数,如果规格是空,则括号中是空字符串
|
||||
-> FROM
|
||||
-> demo.goodsmaster;
|
||||
+-----------+---------------+----------+
|
||||
| goodsname | specification | 拼接 |
|
||||
+-----------+---------------+----------+
|
||||
| 书 | 16开 | 书(16开) |
|
||||
| 笔 | NULL | 笔() |
|
||||
+-----------+---------------+----------+
|
||||
2 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果是,如果规格为空,商品信息就变成了“商品信息()”,好像还不错。但是也存在一点问题:商品名称后面的那个空括号“()”会让客人觉得奇怪,能不能去掉呢?
|
||||
|
||||
如果用IFNULL(V1,V2)函数,就不容易做到,但是没关系,我们可以尝试用另一个条件判断函数IF(表达式,V1,V2)来解决。这里表达式是ISNULL(specification),这个函数用来判断字段"specificaiton"是否为空,V1是返回商品名称,V2是返回商品名称拼接规格。代码如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> goodsname,
|
||||
-> specification,
|
||||
-> -- 这里做判断,如果是空值,返回商品名称,否则就拼接规格
|
||||
-> IF(ISNULL(specification),
|
||||
-> goodsname,
|
||||
-> CONCAT(goodsname, '(', specification, ')')) AS 拼接
|
||||
-> FROM
|
||||
-> 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="">
|
||||
|
||||
这些函数看起来很容易掌握,但是有很多坑。比如说,ROUND(X)是对X小数部分四舍五入,那么在“五入”的时候,返回的值是不是一定比X大呢?其实不一定,因为当X为负数时,五入的值会更小。你可以看看下面的代码:
|
||||
|
||||
```
|
||||
mysql> SELECT ROUND(-1.5);
|
||||
+-------------+
|
||||
| ROUND(-1.5) |
|
||||
+-------------+
|
||||
| -2 |
|
||||
+-------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
所以,我建议你在学习的时候,**多考虑边界条件的场景,实际测试一下**。就像这个问题,对于ROUND(X,0),并没有指定X是正数,那如果是负数,会怎样呢?你去测试一下,就明白了。
|
||||
|
||||
## 思考题
|
||||
|
||||
这节课,我介绍了如何用FLOOR()函数来计算会员积分,那么,如果不用FLOOR(),有没有其他办法来实现会员积分的计算呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。
|
||||
446
极客时间专栏/MySQL 必知必会/实践篇/11 | 索引:怎么提高查询的速度?.md
Normal file
446
极客时间专栏/MySQL 必知必会/实践篇/11 | 索引:怎么提高查询的速度?.md
Normal 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> 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> SELECT
|
||||
-> quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> transdate > '2020-12-12'
|
||||
-> AND transdate < '2020-12-13'
|
||||
-> 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> 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> SELECT
|
||||
-> quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> transdate > '2020-12-12'
|
||||
-> AND transdate < '2020-12-13'
|
||||
-> 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> EXPLAIN SELECT
|
||||
-> quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> transdate > '2020-12-12'
|
||||
-> AND transdate < '2020-12-13'
|
||||
-> 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> CREATE INDEX index_trans_itemnumber ON demo.trans (itemnumber);
|
||||
Query OK, 0 rows affected (43.88 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
然后看看效果:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> transdate > '2020-12-12' -- 对交易时间的筛选,可以在transdate的索引中定位
|
||||
-> AND transdate < '2020-12-13'
|
||||
-> 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> EXPLAIN SELECT
|
||||
-> quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> transdate > '2020-12-12'
|
||||
-> AND transdate < '2020-12-13'
|
||||
-> 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> CREATE INDEX index_trans_branchnumber ON demo.trans (branchnumber);
|
||||
Query OK, 0 rows affected (41.49 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
mysql> CREATE INDEX index_trans_cashiernumber ON demo.trans (cashiernumber);
|
||||
Query OK, 0 rows affected (41.95 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
有了门店编号和收款机编号的索引,现在我们就尝试一下以门店编号、收款机编号和商品编号为查询条件,来验证一下索引是不是起了作用。
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> itemnumber,quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> branchnumber = 11 AND cashiernumber = 1 -- 门店编号和收款机号为筛选条件
|
||||
-> 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> EXPLAIN SELECT
|
||||
-> itemnumber,quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> branchnumber = 11 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_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> 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> SELECT
|
||||
-> itemnumber,quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> branchnumber = 11 AND cashiernumber = 1
|
||||
-> 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> EXPLAIN SELECT
|
||||
-> itemnumber,quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE -- 同时筛选门店编号、收款机号和商品编号
|
||||
-> branchnumber = 11 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_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 > 10 AND cashiernumber = 1 AND itemnumber = 100”这个条件,只能用到组合索引中branchnumber>10的部分,后面的索引就都用不上了。我们来看看MySQL的运行计划:
|
||||
|
||||
```
|
||||
mysql> EXPLAIN SELECT
|
||||
-> itemnumber,quantity,price,transdate
|
||||
-> FROM
|
||||
-> demo.trans
|
||||
-> WHERE
|
||||
-> branchnumber > 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个方面,一个存储空间的开销,还有一个是数据操作上的开销。
|
||||
|
||||
- 存储空间的开销,是指索引需要单独占用存储空间。
|
||||
- 数据操作上的开销,是指一旦数据表有变动,无论是插入一条新数据,还是删除一条旧的数据,甚至是修改数据,如果涉及索引字段,都需要对索引本身进行修改,以确保索引能够指向正确的记录。
|
||||
|
||||
因此,索引也不是越多越好,创建索引有存储开销和操作开销,需要综合考虑。
|
||||
|
||||
# 思考题
|
||||
|
||||
假如我有一个单品销售统计表,包括门店编号、销售日期(年月日)、商品编号、销售数量、销售金额、成本、毛利,而用户经常需要对销售情况进行查询,你会对这个表建什么样的索引呢?为什么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。
|
||||
239
极客时间专栏/MySQL 必知必会/实践篇/12 | 事务:怎么确保关联操作正确执行?.md
Normal file
239
极客时间专栏/MySQL 必知必会/实践篇/12 | 事务:怎么确保关联操作正确执行?.md
Normal 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> START TRANSACTION; -- 开始事务
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
mysql> INSERT INTO demo.mytrans VALUES (1,1,5); -- 插入流水
|
||||
Query OK, 1 row affected (0.00 sec)
|
||||
mysql> 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> COMMIT; -- 提交事务
|
||||
Query OK, 0 rows affected (0.06 sec)
|
||||
|
||||
```
|
||||
|
||||
然后我们查询一下结果:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.mytrans; -- 流水插入成功了
|
||||
+---------+------------+----------+
|
||||
| transid | itemnumber | quantity |
|
||||
+---------+------------+----------+
|
||||
| 1 | 1 | 5.000 |
|
||||
+---------+------------+----------+
|
||||
1 row in set (0.00 sec)
|
||||
mysql> SELECT * FROM demo.inventory; -- 库存消减成功了
|
||||
+------------+-------------+
|
||||
| itemnumber | invquantity |
|
||||
+------------+-------------+
|
||||
| 1 | 5.000 |
|
||||
+------------+-------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
这样,通过把2个相关操作放到事务里面,我们就实现了一个事务操作。
|
||||
|
||||
这里有一个坑,我要提醒你一下。**事务并不会自动帮你处理SQL语句执行中的错误**,如果你对事务中的某一步数据操作发生的错误不做处理,继续提交的话,仍然会导致数据不一致。
|
||||
|
||||
为了方便你理解,我举个小例子。
|
||||
|
||||
假如我们的插入一条销售流水的语句少了一个字段,执行的时候出现错误了,如果我们不对这个错误做回滚处理,继续执行后面的操作,最后提交事务,结果就会出现没有流水但库存消减了的情况:
|
||||
|
||||
```
|
||||
mysql> START TRANSACTION;
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
mysql> INSERT INTO demo.mytrans VALUES (1,5); -- 这个插入语句出错了
|
||||
ERROR 1136 (21S01): Column count doesn't match value count at row 1
|
||||
|
||||
mysql> 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> COMMIT;
|
||||
Query OK, 0 rows affected (0.03 sec) -- 事务提交成功了
|
||||
|
||||
```
|
||||
|
||||
我们查一下表的内容:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.mytrans; -- 流水没有插入成功
|
||||
Empty set (0.16 sec)
|
||||
mysql> SELECT * FROM demo.inventory; -- 库存消减成功了
|
||||
+------------+-------------+
|
||||
| itemnumber | invquantity |
|
||||
+------------+-------------+
|
||||
| 1 | 5.000 |
|
||||
+------------+-------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,流水插入失败了,但是库存更新成功了,这时候没有销售流水,但是库存却被消减了。
|
||||
|
||||
这就是因为没有正确使用事务导致的数据不完整问题。那么,如何使用事务,才能避免这种由于事务中的某一步或者几步操作出现错误,而导致数据不完整的情况发生呢?这就要用到事务中错误处理和回滚了:
|
||||
|
||||
- 如果发现事务中的某个操作发生错误,要及时使用回滚;
|
||||
- 只有事务中的所有操作都可以正常执行,才进行提交。
|
||||
|
||||
那这里的关键就是判断操作是不是发生了错误。我们可以通过MySQL的函数ROW_COUNT()的返回,来判断一个DML操作是否失败,-1表示操作失败,否则就表示影响的记录数。
|
||||
|
||||
```
|
||||
mysql> INSERT INTO demo.mytrans VALUES (1,5);
|
||||
ERROR 1136 (21S01): Column count doesn't match value count at row 1
|
||||
mysql> SELECT ROW_COUNT();
|
||||
+-------------+
|
||||
| ROW_COUNT() |
|
||||
+-------------+
|
||||
| -1 |
|
||||
+-------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
另外一个经常会用到事务的地方是存储过程。由于存储过程中包含很多相互关联的数据操作,所以会大量使用事务。我们可以在MySQL的存储过程中,通过获取SQL错误,来决定事务是提交还是回滚:
|
||||
|
||||
```
|
||||
mysql> DELIMITER // -- 修改分隔符为 //
|
||||
mysql> CREATE PROCEDURE demo.mytest() -- 创建存储过程
|
||||
-> BEGIN -- 开始程序体
|
||||
-> DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK; -- 定义SQL操作发生错误是自动回滚
|
||||
-> START TRANSACTION; -- 开始事务
|
||||
-> INSERT INTO demo.mytrans VALUES (1,5);
|
||||
-> UPDATE demo.inventory SET invquantity = invquantity - 5;
|
||||
-> COMMIT; -- 提交事务
|
||||
-> END
|
||||
-> // -- 完成创建存储过程
|
||||
Query OK, 0 rows affected (0.05 sec)
|
||||
|
||||
mysql> DELIMITER ; -- 恢复分隔符为;
|
||||
mysql> CALL demo.mytest(); -- 调用存储过程
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
mysql> SELECT * FROM demo.mytrans; -- 销售流水没有插入
|
||||
Empty set (0.00 sec)
|
||||
mysql> 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中,并不是所有的操作都可以回滚。比如创建数据库、创建数据表、删除数据库、删除数据表等,这些操作是不可以回滚的,所以,你在操作的时候要特别小心,特别是在删除数据库、数据表时,最好先做备份,防止误操作。
|
||||
|
||||
## 思考题
|
||||
|
||||
学完了这节课以后,如果现在有人对你说,事务就是确保事务中的数据操作,要么全部正确执行,要么全部失败,你觉得这句话对吗?为什么?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
267
极客时间专栏/MySQL 必知必会/实践篇/13 | 临时表:复杂查询,如何保存中间结果?.md
Normal file
267
极客时间专栏/MySQL 必知必会/实践篇/13 | 临时表:复杂查询,如何保存中间结果?.md
Normal 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> CREATE TEMPORARY TABLE demo.mysales
|
||||
-> SELECT -- 用查询的结果直接生成临时表
|
||||
-> itemnumber,
|
||||
-> SUM(quantity) AS QUANTITY,
|
||||
-> SUM(salesvalue) AS salesvalue
|
||||
-> FROM
|
||||
-> demo.transactiondetails
|
||||
-> GROUP BY itemnumber
|
||||
-> ORDER BY itemnumber;
|
||||
Query OK, 2 rows affected (0.01 sec)
|
||||
Records: 2 Duplicates: 0 Warnings: 0
|
||||
|
||||
mysql> SELECT * FROM demo.mysales;
|
||||
+------------+----------+------------+
|
||||
| itemnumber | QUANTITY | salesvalue |
|
||||
+------------+----------+------------+
|
||||
| 1 | 5.000 | 411.18 |
|
||||
| 2 | 5.000 | 24.75 |
|
||||
+------------+----------+------------+
|
||||
2 rows in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,这里我是直接用查询结果来创建的临时表。因为创建临时表就是为了存放某个查询的中间结果。直接用查询语句创建临时表比较快捷,而且连接结束后临时表就会被自动删除,不需要过多考虑表的结构设计问题(比如冗余、效率等)。
|
||||
|
||||
到这里,我们就有了一个存储单品销售统计的临时表。接下来,我们计算一下2020年12月的进货信息。
|
||||
|
||||
我们的进货数据包括进货单头表(importhead)和进货单明细表(importdetails)。
|
||||
|
||||
进货单头表包括进货单编号、供货商编号、仓库编号、操作员编号和验收日期:
|
||||
|
||||
<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> CREATE TEMPORARY TABLE demo.myimport
|
||||
-> SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.importvalue) AS importvalue
|
||||
-> FROM demo.importhead a JOIN demo.importdetails b
|
||||
-> ON (a.listnumber=b.listnumber)
|
||||
-> GROUP BY b.itemnumber;
|
||||
Query OK, 3 rows affected (0.01 sec)
|
||||
Records: 3 Duplicates: 0 Warnings: 0
|
||||
|
||||
mysql> SELECT * FROM demo.myimport;
|
||||
+------------+----------+-------------+
|
||||
| itemnumber | quantity | importvalue |
|
||||
+------------+----------+-------------+
|
||||
| 1 | 5.000 | 290.00 |
|
||||
| 2 | 5.000 | 15.00 |
|
||||
| 3 | 8.000 | 40.00 |
|
||||
+------------+----------+-------------+
|
||||
3 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
这样,我们又得到了一个临时表demo.myimport,里面保存了我们需要的进货数据。
|
||||
|
||||
接着,我们来查询单品返厂数据,并且保存到临时表。
|
||||
|
||||
我们的返厂数据表有2个,分别是返厂单头表(returnhead)和返厂单明细表(returndetails)。
|
||||
|
||||
返厂单头表包括返厂单编号、供货商编号、仓库编号、操作员编号和验收日期:
|
||||
|
||||
<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> CREATE TEMPORARY TABLE demo.myreturn
|
||||
-> SELECT b.itemnumber,SUM(b.quantity) AS quantity,SUM(b.returnvalue) AS returnvalue
|
||||
-> FROM demo.returnhead a JOIN demo.returndetails b
|
||||
-> ON (a.listnumber=b.listnumber)
|
||||
-> GROUP BY b.itemnumber;
|
||||
Query OK, 3 rows affected (0.01 sec)
|
||||
Records: 3 Duplicates: 0 Warnings: 0
|
||||
|
||||
mysql> SELECT * FROM demo.myreturn;
|
||||
+------------+----------+-------------+
|
||||
| itemnumber | quantity | returnvalue |
|
||||
+------------+----------+-------------+
|
||||
| 1 | 2.000 | 115.00 |
|
||||
| 2 | 1.000 | 3.00 |
|
||||
| 3 | 1.000 | 5.00 |
|
||||
+------------+----------+-------------+
|
||||
3 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
这样,我们就获得了单品的返厂信息。
|
||||
|
||||
有了前面计算出来的数据,现在,我们就可以把单品的销售信息、进货信息和返厂信息汇总到一起了。
|
||||
|
||||
如果你跟着实际操作的话,你可能会有这样一个问题:我们现在有3个临时表,分别存储单品的销售信息、进货信息和返厂信息。那么,能不能把这3个表相互关联起来,把这些信息都汇总到对应的单品呢?
|
||||
|
||||
答案是不行,不管是用内连接、还是用外连接,都不可以。因为无论是销售信息、进货信息,还是返厂信息,都存在商品信息缺失的情况。换句话说,就是在指定时间段内,某些商品可能没有销售,某些商品可能没有进货,某些商品可能没有返厂。如果仅仅通过这3个表之间的连接进行查询,我们可能会丢失某些数据。
|
||||
|
||||
为了解决这个问题,我们可以引入商品信息表。因为商品信息表包含所有的商品,因此,把商品信息表放在左边,与其他的表进行左连接,就可以确保所有的商品都包含在结果集中。凡是不存在的数值,都设置为0,然后再筛选一下,把销售、进货、返厂都是0的商品去掉,这样就能得到我们最终希望的查询结果:2020年12月的商品销售数量、进货数量和返厂数量。
|
||||
|
||||
代码如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT
|
||||
-> a.itemnumber,
|
||||
-> a.goodsname,
|
||||
-> ifnull(b.quantity,0) as salesquantity, -- 如果没有销售记录,销售数量设置为0
|
||||
-> ifnull(c.quantity,0) as importquantity, -- 如果没有进货,进货数量设为0
|
||||
-> ifnull(d.quantity,0) as returnquantity -- 如果没有返厂,返厂数量设为0
|
||||
-> FROM
|
||||
-> demo.goodsmaster a -- 商品信息表放在左边进行左连接,确保所有的商品都包含在结果集中
|
||||
-> LEFT JOIN demo.mysales b
|
||||
-> ON (a.itemnumber=b.itemnumber)
|
||||
-> LEFT JOIN demo.myimport c
|
||||
-> ON (a.itemnumber=c.itemnumber)
|
||||
-> LEFT JOIN demo.myreturn d
|
||||
-> ON (a.itemnumber=d.itemnumber)
|
||||
-> HAVING salesquantity>0 OR importquantity>0 OR returnquantity>0; -- 在结果集中剔除没有销售,没有进货,也没有返厂的商品
|
||||
+------------+-----------+---------------+----------------+----------------+
|
||||
| itemnumber | goodsname | salesquantity | importquantity | returnquantity |
|
||||
+------------+-----------+---------------+----------------+----------------+
|
||||
| 1 | 书 | 5.000 | 5.000 | 2.000 |
|
||||
| 2 | 笔 | 5.000 | 5.000 | 1.000 |
|
||||
| 3 | 橡皮 | 0.000 | 8.000 | 1.000 |
|
||||
+------------+-----------+---------------+----------------+----------------+
|
||||
3 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
总之,通过临时表,我们就可以把一个复杂的问题拆分成很多个前后关联的步骤,把中间的运行结果存储起来,用于之后的查询。这样一来,就把面向集合的SQL查询变成了面向过程的编程模式,大大降低了难度。
|
||||
|
||||
## 内存临时表和磁盘临时表
|
||||
|
||||
由于采用的存储方式不同,临时表也可分为内存临时表和磁盘临时表,它们有着各自的优缺点,下面我来解释下。
|
||||
|
||||
关于内存临时表,有一点你要注意的是,你可以通过指定引擎类型(比如ENGINE=MEMORY),来告诉MySQL临时表存储在内存中。
|
||||
|
||||
好了,现在我们先来创建一个内存中的临时表:
|
||||
|
||||
```
|
||||
mysql> CREATE TEMPORARY TABLE demo.mytrans
|
||||
-> (
|
||||
-> itemnumber int,
|
||||
-> groupnumber int,
|
||||
-> branchnumber int
|
||||
-> ) ENGINE = MEMORY; (临时表数据存在内存中)
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
接下来,我们在磁盘上创建一个同样结构的临时表。在磁盘上创建临时表时,只要我们不指定存储引擎,MySQL会默认存储引擎是InnoDB,并且把表存放在磁盘上。
|
||||
|
||||
```
|
||||
mysql> CREATE TEMPORARY TABLE demo.mytransdisk
|
||||
-> (
|
||||
-> itemnumber int,
|
||||
-> groupnumber int,
|
||||
-> branchnumber int
|
||||
-> );
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
现在,我们向刚刚的两张表里都插入同样数量的记录,然后再分别做一个查询:
|
||||
|
||||
```
|
||||
mysql> SELECT COUNT(*) FROM demo.mytrans;
|
||||
+----------+
|
||||
| count(*) |
|
||||
+----------+
|
||||
| 4355 |
|
||||
+----------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
mysql> SELECT COUNT(*) FROM demo.mytransdisk;
|
||||
+----------+
|
||||
| count(*) |
|
||||
+----------+
|
||||
| 4355 |
|
||||
+----------+
|
||||
1 row in set (0.21 sec)
|
||||
|
||||
```
|
||||
|
||||
可以看到,区别是比较明显的。对于同一条查询,内存中的临时表执行时间不到10毫秒,而磁盘上的表却用掉了210毫秒。显然,内存中的临时表查询速度更快。
|
||||
|
||||
不过,内存中的临时表也有缺陷。因为数据完全在内存中,所以,一旦断电,数据就消失了,无法找回。不过临时表只保存中间结果,所以还是可以用的。
|
||||
|
||||
我画了一张图,汇总了内存临时表和磁盘临时表的优缺点:
|
||||
|
||||
<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="">
|
||||
|
||||
假设有多个门店,每个门店有多台收款机,每台收款机销售多种商品,请问如何查询每个门店、每台收款机的销售金额占所属门店的销售金额的比率呢?
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
Reference in New Issue
Block a user