CategoryResourceRepost/极客时间专栏/MySQL 必知必会/实践篇/05 | 主键:如何正确设置主键?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

437 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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