<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%，该怎么实现呢？

欢迎在留言区写下你的思考和答案，我们一起交流讨论。如果你觉得今天的内容对你有所帮助，欢迎你把它分享给你的朋友或同事，我们下节课见。
