Re:Mysql数据库基础篇(五):拒绝脏数据!一文吃透MySQL表的约束(主键、唯一键、外键与实战案例)


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Mysql
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录

  • 概要&序論
  • 一、MySQL表的约束基础与空属性
    • [1.1 空属性的基本概念](#1.1 空属性的基本概念)
    • [1.2 案例:班级表的业务约束](#1.2 案例:班级表的业务约束)
  • 二、默认值约束
  • 三、列描述
  • 四、zerofill属性
    • [4.1 zerofill的实际效果](#4.1 zerofill的实际效果)
    • [4.2 底层存储的本质](#4.2 底层存储的本质)
  • 五、主键约束
    • [5.1 主键的追加与删除](#5.1 主键的追加与删除)
    • [5.2 复合主键](#5.2 复合主键)
  • 六、自增长与索引
    • [6.1 自增长的特点](#6.1 自增长的特点)
    • [6.2 获取最新自增ID](#6.2 获取最新自增ID)
    • [6.3 什么是索引](#6.3 什么是索引)
  • 七、唯一键约束
    • [7.1 唯一键与主键的区别](#7.1 唯一键与主键的区别)
    • [7.2 唯一键的实战案例](#7.2 唯一键的实战案例)
  • 八、外键约束
    • [8.1 外键的概念与语法](#8.1 外键的概念与语法)
    • [8.2 外键的实战案例](#8.2 外键的实战案例)
    • [8.3 如何理解外键约束](#8.3 如何理解外键约束)
  • 九、电商数据库表设计
    • [9.1 商品表设计](#9.1 商品表设计)
    • [9.2 客户表设计](#9.2 客户表设计)
    • [9.3 购买表设计](#9.3 购买表设计)

概要&序論

  Hello大家好,我是此方,上一篇我们讲解了Mysql中表里面的各种字段类型,那么有了类型,我们还需要各种约束来让表更加安全,本文开始讲解MYSQL表的约束相关内容。

一、MySQL表的约束基础与空属性

  在数据库设计中,真正约束字段的是数据类型。但是数据类型的约束往往很单一,我们需要一些额外的约束来更好地保证数据的合法性,从业务逻辑角度保证数据的正确性。比如有一个字段是email,业务要求它是唯一的,这就不能仅仅依赖数据类型了。

  表的约束有很多种,这里主要介绍如下几个:null/not null,default,comment,zerofill,primary key,auto_increment,unique key。我们首先来看看空属性。

1.1 空属性的基本概念

  空属性有两个值:null(默认的)和not null(不为空)。数据库默认字段基本都是字段为空,但是实际开发时,尽可能保证字段不为空,因为数据为空没办法参与运算。

  我们可以直接在MySQL中验证这一点,空值参与任何运算结果依然是空值。

sql 复制代码
mysql> select null;
+------+
| NULL |
+------+
| NULL |
+------+
1 row in set (0.00 sec)

mysql> select 1+null;
+--------+
| 1+null |
+--------+
|   NULL |
+--------+
1 row in set (0.00 sec)

1.2 案例:班级表的业务约束

  假设我们要创建一个班级表,包含班级名和班级所在的教室。站在正常的业务逻辑中,如果班级没有名字,你就不知道你在哪个班级;如果教室名字可以为空,你就不知道在哪上课。

  所以我们在设计数据库表的时候,一定要在表中进行限制,不满足上面条件的数据就不能插入到表中。这就是"约束"的实际应用。

sql 复制代码
mysql> create table myclass(
    -> class_name varchar(20) not null,
    -> class_room varchar(10) not null);
Query OK, 0 rows affected (0.02 sec)

mysql> desc myclass;
+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| class_name | varchar(20) | NO   |     | NULL    |       |
| class_room | varchar(10) | NO   |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+

  此时如果我们在插入数据时,没有给教室数据赋值,由于没有默认值且设置了not null,插入就会直接失败。

sql 复制代码
// 插入数据时,没有给教室数据插入失败:
mysql> insert into myclass(class_name) values('class1');
ERROR 1364 (HY000): Field 'class_room' doesn't have a default value

二、默认值约束

  在实际业务中,某一种数据会经常性地出现某个具体的值。我们可以在一开始建表的时候就指定好默认值(default),在需要真实数据的时候,用户可以选择性地使用默认值,如果不需要,就不传这个字段。

  我们来看一个具体的例子,创建一个包含姓名、年龄和性别的表,其中年龄默认值为0,性别默认值为男。

sql 复制代码
mysql> create table tt10 (
    -> name varchar(20) not null,
    -> age tinyint unsigned default 0,
    -> sex char(2) default '男'
    -> );
Query OK, 0 rows affected (0.00 sec)

mysql> desc tt10;
+-------+---------------------+------+-----+---------+-------+
| Field | Type                | Null | Key | Default | Extra |
+-------+---------------------+------+-----+---------+-------+
| name  | varchar(20)         | NO   |     | NULL    |       |
| age   | tinyint(3) unsigned | YES  |     | 0       |       |
| sex   | char(2)             | YES  |     | 男      |       |
+-------+---------------------+------+-----+---------+-------+

  通过 desc 查看表结构,我们可以看到 age 和 sex 字段的 Default 列已经有了对应的值。默认值的生效规则是:数据在插入的时候,如果不给该字段赋值,就会自动使用默认值。

  我们在插入数据时,只给 name 字段赋值,看看效果。

sql 复制代码
mysql> insert into tt10(name) values('zhangsan');
Query OK, 1 row affected (0.00 sec)

mysql> select * from tt10;
+----------+-----+-----+
| name     | age | sex |
+----------+-----+-----+
| zhangsan |   0 | 男  |
+----------+-----+-----+

  可以看到,虽然只插入了姓名,但年龄和性别都被自动填上了默认值。这里需要特别注意:只有设置了default的列,才可以在插入值的时候,对列进行省略。如果没有设置default且允许为空,省略时会被置为NULL;如果是not null且没有default,省略就会直接报错。

三、列描述

  列描述(comment)没有实际含义,它专门用来描述字段,会根据表创建语句保存,用来给程序员或DBA来进行了解。这在团队协作和后期维护中非常有用。

  我们来看具体的建表案例,为各个字段加上注释说明。

sql 复制代码
mysql> create table tt12 (
    -> name varchar(20) not null comment '姓名',
    -> age tinyint unsigned default 0 comment '年龄',
    -> sex char(2) default '男' comment '性别'
    -> );

  这里有一个小注意点:not null和default一般不需要同时出现,因为default本身有默认值,不会为空。

  如果我们使用 desc 命令查看表结构,是看不到注释信息的。要想看到这些注释,需要查看表的创建语句。

sql 复制代码
mysql> show create table tt12\G
*************************** 1. row ***************************
       Table: tt12
Create Table: CREATE TABLE `tt12` (
  `name` varchar(20) NOT NULL COMMENT '姓名',
  `age` tinyint(3) unsigned DEFAULT '0' COMMENT '年龄',
  `sex` char(2) DEFAULT '男' COMMENT '性别'
) ENGINE=MyISAM DEFAULT CHARSET=gbk
1 row in set (0.00 sec)

  通过 show create table 不仅能看到详细的注释,还能看到表的引擎和字符集信息。

四、zerofill属性

  刚开始学习数据库时,很多人对数字类型后面的长度很迷茫。我们可以通过 show 看看建表语句,比如 int(10) 这个代表什么意思呢?整型不是4字节吗?这个10又代表什么呢?

  其实,如果没有 zerofill 这个属性,括号内的数字是毫无意义的。我们来看一个具体的例子,创建一个包含两个 int(10) 字段的表。

sql 复制代码
mysql> show create table tt3\G
*************************** 1. row ***************************
       Table: tt3
Create Table: CREATE TABLE `tt3` (
  `a` int(10) unsigned DEFAULT NULL,
  `b` int(10) unsigned DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=gbk
1 row in set (0.00 sec)

  当我们插入数据并查询时,显示的就是数据本身。

sql 复制代码
mysql> insert into tt3 values(1,2);
Query OK, 1 row affected (0.00 sec)
mysql> select * from tt3;
+------+------+
| a    | b    |
+------+------+
|    1 |    2 |
+------+------+

4.1 zerofill的实际效果

  但是对列添加了 zerofill 属性后,显示的结果就有所不同了。我们修改 tt3 表 a 列的属性。

sql 复制代码
mysql> alter table tt3 change a a int(5) unsigned zerofill;
mysql> show create table tt3\G
*************************** 1. row ***************************
       Table: tt3
Create Table: CREATE TABLE `tt3` (
  `a` int(5) unsigned zerofill DEFAULT NULL, -- 具有了zerofill
  `b` int(10) unsigned DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=gbk
1 row in set (0.00 sec)

  对 a 列添加了 zerofill 属性后,再进行查找,返回如下结果。

sql 复制代码
mysql> select * from tt3;
+-------+------+
| a     | b    |
+-------+------+
| 00001 |    2 |
+-------+------+

  这次可以看到 a 的值由原来的 1 变成了 00001,这就是 zerofill 属性的作用。如果宽度小于设定的宽度(这里设置的是5),系统会自动填充 0。

4.2 底层存储的本质

  需要特别注意的是,这只是最后显示的结果,在 MySQL 中实际存储的还是 1。为什么是这样呢?我们可以用 hex 函数来证明。

sql 复制代码
mysql> select a, hex(a) from tt3;
+-------+--------+
| a     | hex(a) |
+-------+--------+
| 00001 | 1      |
+-------+--------+

  可以看出数据库内部存储的还是 1,00001 只是设置了 zerofill 属性后的一种格式化输出而已。

五、主键约束

  主键(primary key)用来唯一地约束该字段里面的数据,不能重复,不能为空,一张表中最多只能有一个主键;主键所在的列通常是整数类型。我们可以在创建表的时候直接在字段上指定主键。

sql 复制代码
mysql> create table tt13 (
    -> id int unsigned primary key comment '学号不能为空',
    -> name varchar(20) not null);
Query OK, 0 rows affected (0.00 sec)

mysql> desc tt13;
+-------+---------------------+------+-----+---------+-------+
| Field | Type                | Null | Key | Default | Extra |
+-------+---------------------+------+-----+---------+-------+
| id    | int(10) unsigned    | NO   | PRI | NULL    |       |
| name  | varchar(20)         | NO   |     | NULL    |       |
+-------+---------------------+------+-----+---------+-------+

  在 desc 的结果中,Key 栏中的 PRI 就表示该字段是主键。主键的约束非常严格,主键对应的字段中不能重复,一旦重复,操作就会失败。

sql 复制代码
mysql> insert into tt13 values(1, 'aaa');
Query OK, 1 row affected (0.00 sec)

mysql> insert into tt13 values(1, 'aaa');
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'

5.1 主键的追加与删除

  当表创建好以后但是没有主键的时候,我们可以再次追加主键。

sql 复制代码
alter table 表名 add primary key(字段列表)

  同样地,我们也可以删除已经存在的主键。

sql 复制代码
mysql> alter table tt13 drop primary key;
mysql> desc tt13;
+-------+---------------------+------+-----+---------+-------+
| Field | Type                | Null | Key | Default | Extra |
+-------+---------------------+------+-----+---------+-------+
| id    | int(10) unsigned    | NO   |     | NULL    |       |
| name  | varchar(20)         | NO   |     | NULL    |       |
+-------+---------------------+------+-----+---------+-------+

5.2 复合主键

  在实际业务中,有时候单一的一个字段无法唯一标识一条记录。在创建表的时候,在所有字段之后,我们可以使用 primary key(主键字段列表) 来创建主键,如果有多个字段作为主键,这就叫复合主键。

sql 复制代码
mysql> create table tt14(
    -> id int unsigned,
    -> course char(10) comment '课程代码',
    -> score tinyint unsigned default 60 comment '成绩',
    -> primary key(id, course) -- id和course为复合主键
    -> );
Query OK, 0 rows affected (0.01 sec)

mysql> desc tt14;
+--------+---------------------+------+-----+---------+-------+
| Field  | Type                | Null | Key | Default | Extra |
+--------+---------------------+------+-----+---------+-------+
| id     | int(10) unsigned    | NO   | PRI | 0       |       |
| course | char(10)            | NO   | PRI |         |       |
| score  | tinyint(3) unsigned | YES  |     | 60      |       |
+--------+---------------------+------+-----+---------+-------+

  可以看到,id 和 course 这两列合成为了主键。在插入数据时,只有这两个字段的组合完全一致时,才会触发主键冲突。

sql 复制代码
mysql> insert into tt14 (id,course) values(1, '123');
Query OK, 1 row affected (0.02 sec)

mysql> insert into tt14 (id,course) values(1, '123');
ERROR 1062 (23000): Duplicate entry '1-123' for key 'PRIMARY' -- 主键冲突

六、自增长与索引

  在了解了主键之后,我们经常会遇到一种需求:希望每次插入新记录时,某个字段能够自动生成一个唯一的递增值。这就是自增长(auto_increment)的用武之地。当对应的字段不给值时,会自动地被系统触发,系统会从当前字段中已经有的最大值+1操作,得到一个新的不同的值。通常和主键搭配使用,作为逻辑主键。

6.1 自增长的特点

  在使用自增长之前,我们需要了解它的三个核心特点。

  1. 任何一个字段要做自增长,前提是本身是一个索引(key一栏有值)。
  2. 自增长字段必须是整数。
  3. 一张表最多只能有一个自增长。

  我们来看一个具体的案例,创建一个带有自增长主键的表。

sql 复制代码
mysql> create table tt21(
    -> id int unsigned primary key auto_increment,
    -> name varchar(10) not null default ''
    -> );

mysql> insert into tt21(name) values('a');
mysql> insert into tt21(name) values('b');

mysql> select * from tt21;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
+----+------+

  可以看到,我们在插入数据时完全没有理会 id 字段,但系统自动帮我们填上了 1 和 2。

6.2 获取最新自增ID

  在插入数据后,如果我们后续的业务逻辑需要用到刚刚生成的自增ID,可以使用 last_insert_id() 函数来获取上次插入的 AUTO_INCREMENT 的值。需要注意的是,如果是批量插入,获取的是第一个值。

sql 复制代码
mysql> select last_insert_id();
+------------------+
| last_insert_id() |
+------------------+
|                1 |
+------------------+

6.3 什么是索引

  在上面自增长的特点中提到了索引,那么到底什么是索引呢?在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。

  索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。索引提供指向存储在表的指定列中的数据值的指针,然后根据您指定的排序顺序对这些指针排序。数据库使用索引以找到特定值,然后顺指针找到包含该值的行。这样可以使对应于表的SQL语句执行得更快,可快速访问数据库表中的特定信息。

七、唯一键约束

  一张表中往往有很多字段需要唯一性,数据不能重复,但是一张表中只能有一个主键。为了解决表中有多个字段需要唯一性约束的问题,MySQL引入了唯一键(unique key)。

  唯一键的本质和主键差不多,但有一个显著的区别:唯一键允许为空,而且可以多个为空,空字段不做唯一性比较。

7.1 唯一键与主键的区别

  我们可以简单理解成,主键更多的是标识唯一性的,而唯一键更多的是保证在业务上,不要和别的信息出现重复。乍一听好像没啥区别,我们举一个场景例子来帮助大家理解。

  比如在公司,我们需要一个员工管理系统,系统中有一个员工表,表中有两列信息:一个身份证号码,一个是员工工号。我们可以选择身份证号码作为主键。而在设计员工工号的时候,也需要一种约束:所有的员工工号都不能重复。具体指的是在公司的业务上不能重复,此时就可以将员工工号设计成为唯一键。

  一般而言,我们建议将主键设计成为和当前业务无关的字段,这样当业务调整的时候,可以尽量不对主键做过大的调整。

7.2 唯一键的实战案例

  我们来看一个具体的代码案例,创建一个学生表,将学号设置为唯一键,并允许它为空。

sql 复制代码
mysql> create table student (
    -> id char(10) unique comment '学号,不能重复,但可以为空',
    -> name varchar(10)
    -> );
Query OK, 0 rows affected (0.01 sec)

mysql> insert into student(id, name) values('01', 'aaa');
Query OK, 1 row affected (0.00 sec)

  当我们尝试插入相同的学号时,唯一约束会拦截操作。

sql 复制代码
mysql> insert into student(id, name) values('01', 'bbb'); -- 唯一约束不能重复
ERROR 1062 (23000): Duplicate entry '01' for key 'id'

  但是,因为唯一键允许为空,我们可以插入多个 id 为 NULL 的记录。

sql 复制代码
mysql> insert into student(id, name) values(null, 'bbb'); -- 但可以为空
Query OK, 1 row affected (0.00 sec)

mysql> select * from student;
+------+------+
| id   | name |
+------+------+
| 01   | aaa  |
| NULL | bbb  |
+------+------+

八、外键约束

  前面我们了解了主键和唯一键,它们都是针对单表内部数据的约束。而在实际开发中,不同表之间往往存在关联关系。外键(foreign key)就是用于定义主表和从表之间关系的约束。

8.1 外键的概念与语法

  外键约束主要定义在从表上,主表则必须是有主键约束或unique约束。当定义外键后,要求从表的外键列数据必须在主表的主键列存在,或者为null。它的基本语法如下。

sql 复制代码
foreign key (字段名) references 主表(列)

8.2 外键的实战案例

  假设我们有一个班级表(myclass)和一个学生表(stu)。学生表中的班级id必须对应班级表中真实存在的班级,我们可以通过外键来建立这种联系。

  首先,我们需要先创建主表。

sql 复制代码
create table myclass (
    id int primary key,
    name varchar(30) not null comment '班级名'
);

  接着,我们再创建从表,并在其中指定外键。

sql 复制代码
create table stu (
    id int primary key,
    name varchar(30) not null comment '学生名',
    class_id int,
    foreign key (class_id) references myclass(id)
);

  表建好后,我们插入一些正常的数据。

sql 复制代码
mysql> insert into myclass values(10, 'C++大牛班'),(20, 'java大神班');
Query OK, 2 rows affected (0.03 sec)

mysql> insert into stu values(100, '张三', 10), (101, '李四', 20);
Query OK, 2 rows affected (0.01 sec)

  此时如果我们插入一个班级号为30的学生,因为主表中没有这个班级,外键约束会直接拦截这个操作,插入不成功。

sql 复制代码
mysql> insert into stu values(102, 'wangwu', 30);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails

  但如果我们插入班级id为null的数据,是允许的。比如来了一个学生,目前还没有分配班级。

sql 复制代码
mysql> insert into stu values(102, 'wangwu', null);
Query OK, 1 row affected (0.00 sec)

8.3 如何理解外键约束

  首先我们承认,这个世界是数据很多都是相关性的。理论上,上面的例子,我们不创建外键约束,就正常建立学生表以及班级表,该有的字段我们都有。此时在实际使用的时候,可能会出现什么问题?

  有没有可能插入的学生信息中有具体的班级,但是该班级却没有在班级表中?比如学校只开了比特100班、比特101班,但是在学生里面竟然有比特102班的学生(这个班目前并不存在),这很明显是有问题的。

  因为此时两张表在业务上是有相关性的,但是在业务上没有建立约束关系,那么就可能会出现数据不一致的问题。

  解决方案就是通过外键完成的。建立外键的本质其实就是把相关性交给 MySQL 去审核了,提前告诉 MySQL 表之间的约束关系,那么当用户插入不符合业务逻辑的数据的时候,MySQL 不允许你插入。

九、电商数据库表设计

  为了巩固前面所学的各种约束,我们来看一个完整的综合案例。假设有一个商店的数据,需要记录客户及购物情况,这个数据库由以下三个表组成:商品表(goods)、客户表(customer)和购买表(purchase)。

  针对这三个表,我们有以下几个明确的设计要求。

  1. 每个表都需要设计主外键。
  2. 客户的姓名不能为空值。
  3. 客户的邮箱不能重复。
  4. 客户的性别只能限定为男或女。

  首先,我们需要创建并选择对应的数据库。

sql 复制代码
-- 创建数据库
create database if not exists bit32mall default character set utf8;

-- 选择数据库
use bit32mall;

9.1 商品表设计

  商品表(goods)需要记录商品编号、名称、单价、类别和供应商。我们将商品编号设为主键并自增,名称和供应商要求非空。

sql 复制代码
create table if not exists goods
(
    goods_id int primary key auto_increment comment '商品编号',
    goods_name varchar(32) not null comment '商品名称',
    unitprice int not null default 0 comment '单价,单位分',
    category varchar(12) comment '商品分类',
    provider varchar(64) not null comment '供应商名称'
);

9.2 客户表设计

  客户表(customer)需要记录客户号、姓名、地址、邮箱、性别和身份证。根据要求,姓名不能为空;邮箱和身份证由于业务上不能重复,我们将其设置为唯一键(unique key);性别使用枚举类型限定为非空。

sql 复制代码
create table if not exists customer
(
    customer_id int primary key auto_increment comment '客户编号',
    name varchar(32) not null comment '客户姓名',
    address varchar(256) comment '客户地址',
    email varchar(64) unique key comment '电子邮箱',
    sex enum('男','女') not null comment '性别',
    card_id char(18) unique key comment '身份证'
);

9.3 购买表设计

  购买表(purchase)用于记录订单,包含订单号、客户编号、商品编号和购买数量。订单号作为主键自增,同时我们需要建立外键,将客户编号关联到客户表,将商品编号关联到商品表,保证数据的关联性和完整性。

sql 复制代码
create table if not exists purchase
(
    order_id int primary key auto_increment comment '订单号',
    customer_id int comment '客户编号',
    goods_id int comment '商品编号',
    nums int default 0 comment '购买数量',
    foreign key (customer_id) references customer(customer_id),
    foreign key (goods_id) references goods(goods_id)
);

好的本文内容到此结束,我是此方,我们下期再见。

相关推荐
想做小南娘,发现自己是女生喵1 小时前
LAN Device Monitor 第二周学习文档
java·服务器·数据库
byte轻骑兵1 小时前
【TextIn xParse + WorkBuddy】把8小时的标书审查压进1小时:招标要求逐条核验与废标风险预警工作台
大数据·数据库·aigc·textin·ai数据层基础设施
翔云1234561 小时前
Gaea(小米MySQL Proxy)整体架构
运维·数据库·mysql
jaysee-sjc1 小时前
【苍穹外卖】Day10:定时任务与 WebSocket 实时提醒实战
java·开发语言·javascript·数据库·vue.js·websocket·网络协议
牧瀬クリスだ2 小时前
Redis五大数据类型全解析:从入门到实战
java·数据库·redis·后端·缓存
瑞克-桑切斯2 小时前
PostgreSQL 14.4 源码部署 & 运维手册(CentOS7)
数据库·学习·postgresql
上海红同电子2 小时前
红同电子线束工厂100问|绝缘护套采购,材质PVC、TPE、PP该如何选型?
数据库·经验分享·职场和发展·产品运营·制造·材质
泡海椒2 小时前
JQuick-SQL “数据源 SPI”辨析:应用适配器与 Function SPI 的正确边界
数据库·驱动开发·sql
无毁的湖光-Al2 小时前
高可用之路-观测篇-那些年遇到过的指标幻觉
linux·数据库·tcp