拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MySQL主键约束与唯一约束:区别、用法与实操案例

MySQL 和约束这两个词对新手来说往往听着很玄乎。很多刚学 MySQL 的朋友建表时随手写几行字段就完事等到数据乱掉、重复记录满天飞或者 Join 查不到数据时才回头补课才发现当初没建约束是给自己挖了个大坑。我自己最初也是这样被重复数据教做人之后才老老实实把约束吃透。这篇要讲的就是 MySQL 里最常用的两个约束主键约束和唯一约束。搞懂它们你的表结构基本就稳了一大半后续踩坑的概率也会直线下降。提示如果你连 MySQL 环境都还没装好建议先把数据库装好、能用命令行或客户端连上再对照本文动手敲。约束是建表时定义的规则纸上谈兵远不如亲手试一遍。1. 约束到底在约束什么1.1 约束的本质是给数据立规矩MySQL 里的约束本质上就是数据库在写入数据前帮你做的一道检查。你规定这个字段不能为空、不能重复、必须是某个范围数据库就会在每次 INSERT 或 UPDATE 时自动校验不符合规矩的操作直接拒绝执行。打个比方约束就好比单位门口的闸机只有刷了门禁卡的人才能进没卡或者卡过期的人进不去。没有这道闸机谁都能往你表里塞数据脏数据、重复数据、空数据一股脑全进来后面你清洗数据时能累到怀疑人生。MySQL 中常见的约束一共五类NOT NULL非空、UNIQUE唯一、PRIMARY KEY主键、DEFAULT默认值、CHECK检查再加上外键 FOREIGN KEY大致就齐了。六类里面主键约束和唯一约束是出场频率最高、也最容易让新手犯迷糊的两个。因为这两个都跟“唯一性”有关一个表里既可以有主键也可以有唯一约束那它们到底有什么区别这就是本文要解决的核心问题。1.2 为什么主键和唯一约束被称为“最常用”原因很简单。任何一个正经业务表都会有一列或者几列用来区分“这是哪条记录”。比如用户表里的用户ID订单表里的订单号商品表里的商品编码。这些字段天然要求“不能重复”否则数据就乱了套。唯一约束解决的是“业务上不允许重复”的场景比如身份证号、手机号、邮箱这些在业务逻辑上必须是唯一值。主键约束则更严格一些它不仅是唯一值还承担着“记录身份标识”的职责是整张表的数据定位基准也是其他表通过外键关联你的参照物。换句话说唯一约束是“你的身份证号不能和别人重复”主键是“你是你不是别人”。这俩字段在使用频率上碾压其他约束所以新手入门先吃透这两个就够用了。2. 主键约束数据表的身份证2.1 主键到底是什么规则主键约束在 MySQL 里同时具备三个属性值非空、值唯一、一个表只能有一个主键。前两个好理解最后一个属性其实很讲究。一个表只能有一个主键这个“一个”指的是主键只能定义一组但这一组里可以包含多个列这就是“联合主键”。大部分新手接触到的单列主键通常是配着 AUTO_INCREMENT 自增整数来用的。这样做的目的是让数据库自动帮你分配一个递增序号你完全不用关心主键值应该是多少。反正每插入一条记录MySQL 就自动给它编号。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100), PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表语句应该是 MySQL 新手最早学会的模板之一。id 列定义成 INT加了 NOT NULL 和 AUTO_INCREMENT最后在表级用 PRIMARY KEY (id) 把主键落在 id 列上。以后每插一条记录id 自动往上加从 1、2、3……一直排下去。2.2 主键为什么必须存在主键的意义不只在“保证唯一”。更深层的作用是InnoDB 存储引擎里数据实际上是以主键为逻辑顺序组织的。InnoDB 的索引结构是聚簇索引Clustered Index整张表的行数据直接挂在主键索引的叶子节点上。你没有主键InnoDB 就会在后台偷偷找一列不重复的字段当主键找不到就生成一个隐藏列当主键。所以哪怕你建表时不写 PRIMARY KEYInnoDB 也会给你弄一个隐式主键。与其让数据库猜不如自己明明白白定义一个。主键一旦建立MySQL 会自动为主键列创建一个唯一索引查询时走索引扫描效率远高于全表扫。这里有个很容易被忽略的细节主键列的长度直接影响到普通索引的大小。因为 InnoDB 的二级索引叶子节点存储的是主键值主键越长其他索引的存储空间就越大。所以推荐用自增整数做主键而不是很长的字符串更不能用那种超长的业务单号。这也是很多公司建表规范里明确写的主键尽量用 INT 或 BIGINT 自增。主键太长的后果短期内看不出来等数据量大到千万级索引体积的差异会非常明显。2.3 新手最容易犯的两个主键错误第一个错误是好心给主键加上了业务含义。比如用手机号做主键当时觉得反正是唯一的还省事。结果后来产品要支持用户改绑手机号你发现想改主键值牵一发动全身所有关联表都要跟着改。如果你是业务字段的主键一旦业务规则调整你的表结构就得跟着重构。第二个错误是使用 UUID 或其他随机字符串做主键。UUID 不用自增插入时值完全是随机的。InnoDB 聚簇索引喜欢顺序插入随机值会导致页分裂、频繁移动行记录出现大量碎片。表现在系统上就是插入变慢、磁盘占用增多。不是说 UUID 绝不能用但在单库单表的主键场景下自增整数几乎是所有教科书和公司规范的一致答案。注意主键是一个表的身份标识它不承担业务判断。哪怕用户名可以唯一标识一个人也不建议把 username 当主键真正的主键应该是那个永不变更、自增递增的 id。3. 唯一约束主键的“同胞兄弟”3.1 唯一约束解决的场景唯一约束的关键词是 UNIQUE KEY。它和主键最大的相同点是字段值不能重复。不同点是唯一约束允许 NULL唯一约束可以有多个创建时也会自动生成索引但构建的索引是普通唯一索引不是聚簇索引。举例来说用户表里主键是 id但 username 或 email 通常也要加唯一约束。因为在业务上两个用户的用户名或邮箱不能相同。这属于业务规则不属于身份标识用唯一约束再合适不过。CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, email VARCHAR(100), PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这样写之后当你想插一个已存在的 username 时MySQL 会直接报错Duplicate entry xxx for key user.uk_username。这个报错信息对新手很常见看到时第一反应不应该是“为什么插不进去”而应该是“哪个唯一约束拦住了我”。3.2 唯一约束与主键的四个区别新手混乱的根源往往在于没把这两个约束放在同一个维度对比。我把核心区别拆成一张表。对比维度主键约束唯一约束数量限制一个表只能有一个主键一个表可以有多个唯一约束NULL 值不允许 NULL允许 NULL且多个 NULL 不算重复索引类型聚簇索引InnoDB普通唯一索引业务定位记录的身份标识业务上的防重复规则这里面最反直觉的是 NULL 值那一条唯一约束允许多行 NULL。也就是说如果 email 列加了唯一约束表里可以存在多条 email 为 NULL 的记录。很多新手以为唯一约束会让 NULL 也变成唯一结果发现 NULL 可以插入多条一脸懵。这是数据库标准行为NULL 表示“未知”数据库认为多个未知不相等所以互不冲突。另外唯一约束可以和主键同时存在也可以建在多个列上形成联合唯一。比如订单明细表里同一张订单不能出现两次同一个商品那就用 (order_id, product_id) 做联合唯一约束。这类组合用主键做会很别扭但用唯一约束做很自然。3.3 唯一约束的隐藏价值顺带给你一个索引建立唯一约束的同时MySQL 会自动为这些列创建唯一索引。这就意味着你在写 WHERE 条件时如果命中了这些唯一字段查询就能直接走索引。举个例子用户登录靠 username 查库username 上有唯一索引的话这条查询就会非常快。相当于你给字段加了防重复规则还免费得到一个索引。相比之下如果你不建唯一约束光靠应用层去查一遍再插入既慢又容易出并发问题不如数据库层面一刀切。但有利就有弊。每个唯一索引都会增加写入成本因为每次插入时数据库都要检查唯一性。所以不要给表里每个字段都加上唯一约束只在业务上真正需要唯一的字段上加。否则索引膨胀插入效率下降得不偿失。注意唯一约束和唯一索引叫法不同本质上是同一个东西。你在建表语句里写 UNIQUE KEY (col)或者单独用 CREATE UNIQUE INDEX 创建效果是一样的。区别只是一个是建表时用一个是已存在的表上补加。4. 实操演练建表时把约束用对4.1 一个完整的用户表建表案例讲了这么多不如直接上手敲一遍。这里我拿一个稍微真实一点的场景用户注册表。需求是用户ID自增、用户名不能重复、邮箱不能重复但允许为空、状态字段有默认值。表结构可以这样建。CREATE TABLE t_user ( id INT NOT NULL AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username), UNIQUE KEY uk_email (email) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这段语句里主键落在 id 上username 和 email 各有一条唯一约束。注意 email 的 DEFAULT NULL就是前面说的“唯一约束允许多条 NULL”的经典场景。注册时可以允许用户不填邮箱提交时数据库不会拦。现在试着插入几条数据验证约束效果-- 正常插入 INSERT INTO t_user (username, email) VALUES (zhangsan, zsexample.com); -- 用户名重复会报错 INSERT INTO t_user (username, email) VALUES (zhangsan, otherexample.com); -- 邮箱重复也会报错 INSERT INTO t_user (username, email) VALUES (lisi, zsexample.com); -- 邮箱为空可以插多次不会报错 INSERT INTO t_user (username, email) VALUES (wangwu, NULL); INSERT INTO t_user (username, email) VALUES (zhaoliu, NULL);自己敲一遍这几条你对约束的理解会比看十篇文章都深。我特别推荐你去故意试错一下插重复值看数据库到底报什么错。把错误提示看熟了以后线上遇到类似说辞的报错一眼就能定位。4.2 表已建好怎么补救约束如果表已经建好了当初没加约束不用推倒重建。可以用 ALTER TABLE 语句补加。这是工作中非常常见的操作。-- 给已有表添加主键 ALTER TABLE t_user ADD PRIMARY KEY (id); -- 添加唯一约束 ALTER TABLE t_user ADD UNIQUE KEY uk_username (username); -- 删除唯一约束 ALTER TABLE t_user DROP INDEX uk_username; -- 删除主键 ALTER TABLE t_user DROP PRIMARY KEY;这里有个操作细节给已有字段添加唯一约束之前一定要先确认现有数据是否满足唯一性。如果有两条记录 username 是相同的ALTER 直接执行会失败。正确的姿势是先用 GROUP BY 查一遍重复项清理干净后再加约束。-- 找出重复的 username SELECT username, COUNT(*) FROM t_user GROUP BY username HAVING COUNT(*) 1;我见过不少人在生产环境直接执行加唯一约束结果数据库报错Duplicate entry。这不是数据库的问题是存量脏数据的问题。所以补约束前先做数据清理这是流程问题不是语法问题。4.3 约束加完之后业务层别忘了处理报错数据库约束能拦住脏数据但它的报错信息对前端用户并不友好。你在插入重复用户名时MySQL 会给一个生硬的 Duplicate entry 错误。如果你的程序不做处理用户看到的就是“500 内部错误”。正确的做法是在代码里捕获这个数据库异常转成业务提示。以 Java 为例插入时捕捉 DuplicateKeyException返回“用户名已被注册”如果是 MySQL 原生错误码通常关注 1062 这个值它代表唯一约束冲突。约束是后端最后一道防线但不是用户体验的全部。数据库帮你拦住了坏数据应用层再把拦截结果翻译成人话这套搭配才算完整。很多新手只注重数据库能不能建出来忽略了对外表现的细节这一点容易被忽视。5. 新手踩坑实录这些问题我也犯过5.1 典型问题速查表我把新手在约束上常踩的坑汇总一下你在自查时可以直接对应。问题报错或现象解决方案主键重复插入Duplicate entry 1 for key PRIMARY检查插入逻辑确认 id 值来源唯一约束冲突Duplicate entry xxx for key uk_username检查业务字段重复提示用户更换给已有脏数据表加唯一约束ALTER 执行失败提示 Duplicate先清理重复数据再加约束主键用业务字段业务修改时牵一发动全身设计阶段用自增 id 做主键忘记加索引直接查查询全表扫描速度慢考虑在 WHERE 高频字段建索引唯一约束字段一会 NULL 一会儿有值多次 NULL 插入成功以为出 bug理解 NULL 语义需求上用空字符串替代5.2 场景一重复提交导致唯一约束爆掉做活动页面时用户可能双击提交按钮前端没做防抖后端也没做幂等。同一个 username 被提交两次第二次插入时唯一约束就炸了。这个场景非常典型。处理办法有两层。第一层前端按钮置灰防止双击。第二层后端捕获重复键异常返回友好提示。数据库的唯一约束不是帮你防误触的但它能保证即便出现误触也不会真的写入两条脏数据。所以唯一约束存在的意义不光是“业务上不允许重复”还是“数据异常时的兜底保险”。5.3 场景二NULL 导致唯一约束没生效另一个常见的困惑是email 加了唯一约束为什么还能插两条 NULL前面解释过这是 MySQL 的标准行为多个 NULL 之间不彼此冲突。如果你的业务要求“要么都填邮箱要么都为空但不能一部分有值一部分为空”这种逻辑已经超出唯一约束的能力范围了放在应用层处理或者用空字符串代替 NULL 再配合唯一约束使用。用空字符串代替 NULL 是一种工程技巧。把 email 字段设置成 NOT NULL DEFAULT 然后在唯一约束下空字符串只能存一条第二条会报错。这种设计适合“每个用户必须有邮箱但允许为空字符串占位”的场景。但要注意它改变了字段语义NULL 表示不存在空字符串表示没有填写两个含义要想清楚再选。5.4 场景三规范化命名约束否则后期调试想骂人约束命名是个容易被忽略的点。MySQL 默认会给约束自动命名比如唯一约束默认名和被约束字段名一致删起来还算好认。但经常有多个唯一约束落在相似字段上比如 uk_name_phone你看到名字就知道是 name phone 的联合唯一约束。如果你什么都不写全靠默认后期维护时想 DROP 一个约束得先去 information_schema 里翻表非常痛苦。我自己的习惯是统一前缀主键约束一般就叫 PRIMARY不用管唯一约束用 uk_ 开头后面跟字段名多个字段用下划线连接普通索引用 idx_ 开头外键用 fk_ 开头。这套规则在团队里推行之后维护表结构的人都会轻松很多。5.5 排查约束问题的一些实用命令实际排查时如果你不确定表上有哪些约束、哪些索引用 SHOW 命令能看到全貌。这是我在工作中使用频率相当高的排查手段。-- 查看表结构能看到约束和索引 SHOW CREATE TABLE t_user; -- 查看已有索引 SHOW INDEX FROM t_user;SHOW CREATE TABLE 的结果里你会看到 PRIMARY KEY 和 UNIQUE KEY 等字样非常直观。当一个 SQL 执行很慢时我也会先用 SHOW INDEX 看看查询条件里的字段有没有索引。很多时候慢查询的解决办法是补一个索引而不是去优化那几条 SQL 逻辑。提示约束和索引是两套概念但紧密关联。主键约束和唯一约束会自动创建索引这一点既是福利也是成本。你在设计字段时一定要想清楚一个字段是否真的需要唯一性因为它的代价是每次写入时多一次唯一性检查。6. 最后再分享一点个人体会MySQL 的约束看着简单实际用起来学问不少。我自己刚入门时也干过用手机号做主键、给每个字段都加唯一约束的“蠢事”被数据量和业务变更狠狠教育过。后来逐步养成一套规矩先想清楚每个字段的业务含义再看它需不需要唯一性、允不允许为空最后才是动手建表。如果你正在学 MySQL我建议你把主键约束和唯一约束吃透之后再往下学索引、事务、存储过程因为它们本质上是后续所有数据库设计的基础。约束不牢后面的优化再多也填不上数据结构设计时的坑。这篇文章能帮你减少的正是那些我当年踩过之后才明白的入门坑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门