“创建数据表”谁都会,但“创建一张靠谱的数据表”就没那么简单了。我近几年接手过不少跑了好几年的老项目,从数据库里能看到各种让人血压升高的操作:几百号人共用一个库,表却没有主键;金额字段用的是FLOAT,对账对到崩溃;该加非空约束的字段全部默认NULL,业务代码里满屏都是空值判断。这些问题的源头几乎都是同一步——建表。尤其是不少朋友会把《MySQL创建数据表及其约束条件》当成“可选”内容来学,觉得反正SQL能跑就行,约束条件写不写无所谓。
这篇文章我就围绕MySQL建表和约束条件这组核心主题,把建表前要想清楚的事、CREATE TABLE语法细节、六大约束的具体用法、一份完整的订单表设计实操,以及我在实际排查中踩过的各种报错坑,一次性讲透。适合正在学数据库的初学者,也适合那些建表全靠感觉、后期被数据质量追着跑的开发同行。
1. 建表前的设计思路:为什么大多数人会在这里踩坑
1.1 先想清楚这张表到底干什么的
很多新手拿到需求就直接写SQL,写完发现字段不够用、类型不对、关系理不清,又得删表重建。这不是SQL语法的问题,而是设计意识的问题。
建表之前,先回答三个问题:这张表保存什么业务对象?这个对象有哪些核心属性?它和其他表是什么关系?
举个例子,做一个用户表,你要先明确“用户”是谁——是注册账号的人,还是后台管理员,还是只有手机号没注册的访客?属性上,用户名、密码、邮箱、手机号、注册时间这些是必须的,性别、年龄、头像这类是可有可无的,而余额、积分这种其实属于账号资产,很多人会一股脑塞进用户表,等字段多到一张横屏截图都放不下的时候才想起来拆表。
关系上更要命。用户表和订单表是一对多关系,所以订单表里要放一个user_id作为外键列。如果连这层关系都没想明白,后面做关联查询的时候只能靠字符串拼接去匹配,数据一乱神仙难救。
我的习惯是:先在纸上画个简单的字段清单,每个字段标上“必填/选填”“是否重复出现”“是否是金额类数值”,画完再动手写SQL。这个习惯帮我省掉的返工时间,比任何工具都值钱。
1.2 字段类型选错的代价比你想的大
字段类型是建表里最“翻车”的高发区。常见三类问题:
第一类是数值类型乱选。价格、余额、折扣这种对精度敏感的数据,有人图省事用FLOAT或DOUBLE,结果0.1+0.2不等于0.3的经典问题直接出现在账目上。金额正确姿势是DECIMAL,比如DECIMAL(10,2),整数部分8位小数部分2位,存储精确无误差。数量、年龄、状态码这类整数,用INT或TINYINT就行,主键或可能超大的计数用BIGINT。
第二类是字符串类型不分场合。用户名、手机号、订单号这类定长或接近定长的短串,用VARCHAR(32)或VARCHAR(50)完全够;但有人把所有字段都定义成VARCHAR(255),甚至把一个简单状态标记也塞进VARCHAR(255),白白浪费存储空间还拖慢索引效率。更麻烦的是超过255的文本,有人还继续用VARCHAR,要知道VARCHAR超过255之后实际存储和TEXT差异不大,而且TEXT类型不能有默认值,设计上会束手束脚。
第三类是时间类型混用。DATETIME和TIMESTAMP经常有人分不清。DATETIME存的时间范围大(1000年到9999年),跟时区无关;TIMESTAMP范围到2038年,跟数据库时区有关。平时业务推荐DATETIME,除非你明确需要存UTC时间并做时区换算。
选类型的原则很简单:能用整型不用字符串,能用定长不用变长,能用DECIMAL不用浮点。短一点、准一点、稳一点。
1.3 表名、字段名命名与公共字段约定
命名规范看着是小事,维护的时候才追悔莫及。真实的例子:某项目表名有的叫user、有的叫t_user、有的叫用户表,字段名有user_name、有username、有usr_nm,团队协作起来,光对齐字段就要翻半天文档。
几点约定可以作为参考:
- 表名单数形式,全小写,下划线分隔,比如user、order_item,不要叫users、orders或者Orders。
- 字段名全小写,下划线分隔,不要用驼峰。数据库层面统一大小写敏感设置很重要,Linux下MySQL默认区分大小写,Windows上不区分,如果你在两种环境间切换,驼峰表名分分钟报错。
- 主键字段统一叫id或xxx_id,比如user_id、order_id。
- 时间字段建议带时间后缀,如create_time、update_time,而且建议用DATETIME类型。
另外,几乎每张业务表都应该具备的公共字段:主键id、创建时间create_time、更新时间update_time,逻辑删除字段deleted(用TINYINT,0未删1已删)看团队习惯加上。这些字段放进去以后,查询、排查、数据订正都会顺很多。
2. CREATE TABLE基础语法与字段定义细节
2.1 一条建表语句的完整结构
MySQL建表主命令是CREATE TABLE,完整结构包括库名、表名、字段定义、约束声明、表选项。先看一个最典型的骨架:
CREATE TABLE [IF NOT EXISTS] `表名` ( `字段1` 类型(长度) [约束] [注释], `字段2` 类型(长度) [约束] [注释], ..., [表级约束], [索引定义] ) ENGINE=存储引擎 DEFAULT CHARSET=字符集 COMMENT='表注释';这里有个高频问题:IF NOT EXISTS到底用不用?我建议在自动化脚本和迁移脚本里加上,防止重复执行报错。但如果是手工建表,还是让它正常报错比较好——如果你心里想的表名已经存在,那应该先思考是不是搞错了业务对象,而不是默默跳过。
库名和表名可以写库名.表名,比如mydb.user,这样即使当前USE的不是目标库也能建对位置。很多新手恰恰是因为忘了USE,把所有表都建到了默认库里,后面查半天才发现表在别的地方。
MySQL其实允许创建临时表,语法是CREATE TEMPORARY TABLE,会话结束自动删除。这在复杂存储过程中做中间结果集很好用,但要注意它只在当前会话可见,别拿它当正式表用。
2.2 字段定义的核心选项:类型、长度、属性、注释
字段定义是建表的重头戏,一个标准字段由这几部分组成:
`字段名` 数据类型 [NOT NULL] [DEFAULT 值] [AUTO_INCREMENT] [COMMENT '注释']数据类型和长度,我平时常用一套保守选型:
| 场景 | 推荐类型 | 说明 |
|---|---|---|
| 主键或大计数 | BIGINT UNSIGNED | 自增用,U是unsigned缩写 |
| 普通整数 | INT / TINYINT UNSIGNED | 状态机、年龄、数量 |
| 短文本 | VARCHAR(20~50) | 用户名、手机号、订单号 |
| 中等文本 | VARCHAR(255) | 邮箱、地址、备注 |
| 金额、价格 | DECIMAL(10,2) | 绝不使用FLOAT/DOUBLE |
| 日期时间 | DATETIME | 创建/更新时间 |
| 枚举类状态 | TINYINT | 比ENUM更灵活 |
| 开关标记 | TINYINT(1) | 0/1语义,用BIT也容易踩坑 |
AUTO_INCREMENT自增,只对整数主键常用。一张表只能有一个自增字段,而且它必须被定义为键。如果不小心定义了多个,MySQL会直接报错。自增的起始值可以用AUTO_INCREMENT=n来指定,但一般只在数据迁移后需要保持原ID时才用。
COMMENT注释,强烈建议每个字段都写。别觉得麻烦,数据库里字段一多,没人记得当初那个mark是用来干嘛的。写注释不需要文采,写清楚这个字段存什么、有哪些约定的值即可。比如状态 0待支付 1已支付 2已发货 3已完成 4已取消,看到注释就能理解,比翻代码快多了。
NULL与NOT NULL也是字段定义的关键选择。注意,NULL和空字符串是两码事。NULL代表“值不存在”,''代表“空字符串”。如果你做唯一约束,更要小心NULL,后面我会专门说。
2.3 表级选项:存储引擎、字符集、排序规则
建表语句结尾的ENGINE、CHARSET、COLLATE是表级选项,很多人忽略,结果表建完报错才知道它们的重要性。
存储引擎,MySQL默认且推荐InnoDB。它支持事务、行级锁、外键约束,是唯一能把数据安全和一致性做好的引擎。MyISAM虽然读快但没有事务和外键,崩溃恢复还容易丢数据,除非做全文索引的特殊场景,否则别用。建表的时候直接写ENGINE=InnoDB,明确无误。
字符集必须统一utf8mb4。注意MySQL的utf8其实是utf8mb3,它只支持基本多语言平面,存不了emoji和某些生僻汉字。utf8mb4才是真正的完整UTF-8。我见过不少老库用utf8,结果用户昵称里一个emoji直接写不进去,报错让人莫名其妙。排序规则COLLATE常用utf8mb4_unicode_ci或utf8mb4_0900_ai_ci,对中文和英文排序、比较都够用。
提示:字符集如果建表时没定,就默认继承数据库的;如果数据库也没定,就继承服务器配置的。最稳妥的做法是建库、建表、建连接都明确utf8mb4,三层统一,避免任何乱码隐患。
3. 六大约束详解:不只是“可选”
3.1 主键约束(PRIMARY KEY):数据表的身份证
主键是每行记录的唯一标识,它在物理层面决定数据怎么组织。InnoDB本身就是索引组织表,主键索引就是聚簇索引,数据行按照主键顺序物理存储。所以选主键直接影响插入性能和数据分布。
主键有三个硬性要求:非空、唯一、最多一个。建议全部使用自增整数主键,也就是BIGINT UNSIGNED AUTO_INCREMENT。自增主键的好处是写入时顺序递增,新数据直接插到索引尾部,不需要频繁移动数据页,页分裂少,性能最稳。
有人问:业务上明明有身份证号、手机号、邮箱这种自然唯一值,能不能直接当主键?我的建议是尽量不要。身份证号涉及隐私、手机号可能注销复用、邮箱可能修改,任何值都可能变,主键一旦变了,各种外键引用都要跟着改,而且字符串主键的索引空间和比较开销都比整数大得多。让主键脱离业务,永远做一个无意义的序列号,是最省心的。
主键写法有两种:
-- 列级约束:直接写在字段后 `user_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, -- 表级约束:写在所有字段后面 PRIMARY KEY (`user_id`)表级约束的优势是支持复合主键,比如PRIMARY KEY (order_id, user_id)。不过复合主键如果不是特别必要,建议还是用单列自增主键,复合主键会让索引体积变大,业务逻辑也更容易混乱。
3.2 非空约束与默认值约束:最容易被忽视的保障
NOT NULL和DEFAULT通常会放在一起理解。我见过很多表,字段既不设NOT NULL也不设DEFAULT,插入的时候不传这个字段,它就成了NULL,代码里不得不写大量判空逻辑。
NOT NULL表达的是“这个属性必须存在”。用户名、邮箱、订单号、创建时间这些业务上不能缺失的字段,都该加NOT NULL。加了之后,漏传该字段的INSERT会直接失败,这在开发期是好事——宁可让它早点崩,也不要让脏数据悄悄入库。
DEFAULT表达的是“如果调用方没传就用这个值”。这两者是配合关系:状态字段设置TINYINT NOT NULL DEFAULT 1,那么插入时少传状态列,它自动变成1;不设默认值又加了NOT NULL,如果代码少传一个字段,整条语句就直接报错1048。
时间字段的默认值有个特殊语法,MySQL 5.6.5之后支持:
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'CURRENT_TIMESTAMP就是当前时间,ON UPDATE表示该行任何字段被UPDATE时自动刷新这个时间。这是我建表必写的两个字段,省去了应用层手动维护时间的麻烦。注意ON UPDATE只对DATETIME/TIMESTAMP有效,别往INT时间戳上乱加,会直接报语法错误。
3.3 唯一约束(UNIQUE):杜绝重复数据的防线
业务上有一些字段,逻辑上不该重复,比如用户名、邮箱、订单号。数据库层的唯一约束是防重复的最后一道闸门。很多团队光靠应用层先查一遍再插入,并发一高,两个请求同时查到“不存在”,然后同时插入,重复数据就产生了。唯一约束是解决这个问题的正解。
唯一约束创建方式:
-- 建表时直接定义 `username` VARCHAR(50) NOT NULL UNIQUE, -- 或表级定义,推荐这种,因为可以指定约束名 UNIQUE KEY `uk_username` (`username`)我给唯一索引命名习惯是uk_字段名,主键索引名直接用PRIMARY,普通索引用idx_字段名。规范命名在后续查索引、删索引时会少很多麻烦事。
和唯一约束纠缠不清的是NULL。MySQL里,唯一索引不会限制NULL重复,因为NULL代表“未知”,数据库认为两个未知不相等。所以username VARCHAR(50) UNIQUE,如果允许NULL,就可以插入多条username为NULL的记录。想要“必填且唯一”,就必须NOT NULL + UNIQUE一起上。
另一个容易掉坑的地方是复合唯一。比如订单表里,同一个用户对同一个商品下过一次单就不能再下,这可能用到UNIQUE KEYuk_user_product(user_id,product_id)。注意复合唯一的顺序要看查询条件,索引最左前缀原则决定了你查询时如果只带上product_id,这个复合唯一索引可能帮不上忙。
3.4 检查约束(CHECK):MySQL 8.0.16之后才真正生效
很多人学MySQL时会发现,官方文档和很多教程都写了CHECK约束,但实际执行却发现怎么写都不生效。这个坑的根源在于历史版本:MySQL 8.0.16之前,CHECK约束只做语法解析,不实际校验数据,也就是说你写了等于没写;8.0.16及以后版本才真正支持并强制校验。
所以如果你想用CHECK约束,先确认版本:
SELECT VERSION();8.0.16以上才能踏实用。典型写法:
`age` TINYINT UNSIGNED NOT NULL CHECK (`age` BETWEEN 0 AND 120), `status` TINYINT NOT NULL CHECK (`status` IN (0, 1, 2))有人问,既然业务代码也能校验,为什么还要用CHECK?答案还是那句话:数据库是最后一道防线。应用层可能有多个入口——后台管理系统写一套校验、App接口写一套校验、运营临时刷数据直接写SQL,只要数据库层不做约束,总有一个入口能混进脏数据。
但也不要过度依赖CHECK。复杂业务规则的校验(比如订单金额要等于明细金额之和)放在数据库里会让触发器或存储过程非常臃肿,该交给应用层还是交给应用层。CHECK适合的是简单、固定、不会频繁变化的规则。
3.5 外键约束(FOREIGN KEY):表与表之间的羁绊
外键约束是用来维护两张表之间引用完整性的。订单表里的user_id必须存在于用户表,如果用户不存在,订单就不该存在。没有外键时,这是一句逻辑口号;有了外键,数据库会帮你强制执行。
外键定义语法:
CONSTRAINT `fk_orders_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON UPDATE CASCADE ON DELETE RESTRICT细节比较多,逐一说:
外键字段和引用字段类型必须完全一致。INT引用BIGINT会报错,VARCHAR(20)引用VARCHAR(50)虽然不报错但不推荐,尽量保持长度一致。两边还都需要索引,MySQL会在外键列上自动建索引,但引用列必须已有索引(主键当然满足)。
ON UPDATE和ON DELETE的行为,常用的就四个选项:
| 动作 | 含义 | 使用场景 |
|---|---|---|
| RESTRICT | 有子记录时,父记录禁止删除/更新(默认) | 订单关联用户,默认推荐 |
| CASCADE | 父记录删除/更新,子记录同步删除/更新 | 文章和评论,评论跟随文章删 |
| SET NULL | 父记录变更,子记录外键置NULL | 需要子记录保留,但不指向任何父记录 |
| NO ACTION | 和RESTRICT行为类似,MySQL中基本等同 | 很少用 |
这里必须提醒一个现实争议:生产环境到底要不要物理外键?我见过一些大厂规范直接禁止物理外键,理由是外键约束在分库分表时基本没法用,而且高并发写入时外键检查会带来额外开销和锁竞争。单库单表、数据量可控的项目,物理外键能实打实保数据一致性,建议用;到了需要分库分表或超高并发阶段,再退化成应用层维护的逻辑外键。
注意:外键约束名在同一库中不能重复,而且删表时要先删子表(有外键的表),再删父表,否则会报错。反之建表时要先建父表再建子表。
4. 完整实操:从零设计并创建用户表和订单表
4.1 需求拆解与建表决策
理论讲再多,不如完整走一遍。我来设计一个简化版的用户-订单系统,两张表:用户表users和订单表orders。
用户表需求拆解:用户有唯一用户名、唯一邮箱,可以有手机号和备注,账户有一个余额(金额精度敏感),状态有正常和禁用。订单表需求拆解:订单属于某个用户,订单号全表唯一,订单有总额(金额精度敏感),状态有未支付、已支付、已发货、已完成、已取消。用户删不删?电商业务,用户已下单,删除会导致订单无法追溯,所以外键删除限制为RESTRICT;用户ID一般不会更新,但为了语义完整,UPDATE设CASCADE。
这个案例里有几个典型的建表决策:
- 主键都用BIGINT UNSIGNED AUTO_INCREMENT,理由是数据量可以很大且无业务含义。
- 金额用DECIMAL(10,2),10位总长度中8位给整数部分,满足绝大多数日常金额需求。
- 状态用TINYINT,因为语义稳定且后续可能扩展,比ENUM更灵活。
- 时间字段用DATETIME + DEFAULT CURRENT_TIMESTAMP,让数据库接管时间维护。
- 所有字段有中文注释,避免半年后自己都不认识这张表。
4.2 完整SQL语句与执行过程
先建父表users:
CREATE TABLE IF NOT EXISTS `users` ( `user_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '用户名', `email` VARCHAR(100) NOT NULL COMMENT '邮箱', `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额', `gender` TINYINT NOT NULL DEFAULT 0 COMMENT '性别 0未知 1男 2女', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态 1正常 0禁用', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`user_id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_email` (`email`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';再建子表orders:
CREATE TABLE IF NOT EXISTS `orders` ( `order_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', `user_id` BIGINT UNSIGNED NOT NULL COMMENT '下单用户ID', `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `total_amount` DECIMAL(12,2) NOT NULL COMMENT '订单总金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态 0待支付 1已支付 2已发货 3已完成 4已取消', `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`order_id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), CONSTRAINT `fk_orders_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';如果你的库是utf8mb4,一般不需要再写COLLATE,默认继承数据库排序规则。这里显式写上是希望表的行为可预期。两张表建好后,用SHOW CREATE TABLE检查一下定义,比SHOW TABLES更能核对细节。
4.3 写几条测试数据验证约束效果
建表只是开始,验证约束是否按预期工作更有价值。先插入正常数据:
INSERT INTO `users` (`username`, `email`, `phone`) VALUES ('zhangsan', 'zs@example.com', '13800000000'); INSERT INTO `users` (`username`, `email`, `phone`) VALUES ('lisi', 'lisi@example.com', '13900000000'); INSERT INTO `orders` (`user_id`, `order_no`, `total_amount`) VALUES (1, 'NO20250101001', 199.90); INSERT INTO `orders` (`user_id`, `order_no`, `total_amount`) VALUES (1, 'NO20250101002', 89.00); INSERT INTO `orders` (`user_id`, `order_no`, `total_amount`) VALUES (2, 'NO20250101003', 4599.00);再故意写几条会失败的,看数据库怎么拦:
-- 用户名非空约束,预期报错 1048 Column 'username' cannot be null INSERT INTO `users` (`username`, `email`) VALUES (NULL, 'nullname@example.com'); -- 用户名唯一约束,预期报错 1062 Duplicate entry 'zhangsan' for key 'users.uk_username' INSERT INTO `users` (`username`, `email`) VALUES ('zhangsan', 'duplicate@example.com'); -- 外键约束,user_id=999用户在父表不存在,预期报错 1452 INSERT INTO `orders` (`user_id`, `order_no`, `total_amount`) VALUES (999, 'NO20250101004', 10.00); -- 订单号唯一约束,预期报错 1062 INSERT INTO `orders` (`user_id`, `order_no`, `total_amount`) VALUES (2, 'NO20250101001', 10.00); -- 外键删除限制,删除已下单用户,预期报错 1451 DELETE FROM `users` WHERE `user_id` = 1;这些预期报错看着烦,但它们恰恰是约束在干活。没有约束的表,这些脏数据全都会悄悄落库,等业务跑起来才发现关联对不上。数据库报错比线上事故好处理一百倍。
5. 常见报错与排查思路实录
5.1 高频错误代码速查
建表和约束相关的报错信息,几乎每天都有人踩,我整理了一份速查:
| 错误代码 | 报错关键词 | 原因 | 处理思路 |
|---|---|---|---|
| 1046 | No database selected | 没有选中数据库 | USE 库名;或建表写库名.表名 |
| 1048 | Column cannot be null | 违反非空约束 | 补字段值,或检查DEFAULT设置 |
| 1062 | Duplicate entry | 唯一约束/主键冲突 | 查已有数据,清理重复或改业务逻辑 |
| 1064 | SQL syntax error | 语法错误 | 复制语句逐段检查,重点看引号、逗号 |
| 1075 | Incorrect table definition | 自增字段未定义为主键 | AUTO_INCREMENT字段必须设为主键或唯一键 |
| 1215 | Cannot add foreign key constraint | 外键添加失败 | 检查类型一致、索引、存储引擎、字符集 |
| 1451 | Cannot delete or update a parent row | 违反外键删除/更新限制 | 先处理子表数据,或调整外键动作 |
| 1452 | Cannot add or update a child row | 外键值在父表不存在 | 检查关联ID是否有效 |
| 1826 | Duplicate foreign key constraint name | 外键名重复 | 换一个约束名 |
| 3780 | Referencing column and referenced column mismatch | 外键类型不匹配 | 两列类型、长度、字符集对齐 |
这个表我建议直接存一份。遇到报错,先看错误码,别一行行读英文原文(虽然原文也有用),错误码能帮你秒级定位方向。
5.2 修改约束的正确姿势:先确认业务影响
表建好后往往还要改约束,这是DBA日常,也是高风险操作。几个高频操作:
给已有表加唯一约束:
ALTER TABLE `users` ADD UNIQUE KEY `uk_email` (`email`);这一步执行前务必确认现有数据没有重复email,否则会直接失败。排查重复可以:
SELECT `email`, COUNT(*) FROM `users` GROUP BY `email` HAVING COUNT(*) > 1;删除主键:
ALTER TABLE `users` DROP PRIMARY KEY;这操作通常伴随重建索引和表数据重组。生产环境别直接执行,先做好备份,并在低峰期操作。如果主键是自增字段,要先修改字段定义去掉AUTO_INCREMENT,否则删主键也会报错。
删除外键约束:
ALTER TABLE `orders` DROP FOREIGN KEY `fk_orders_user`;注意这里用的是FOREIGN KEY而不是KEY,很多人写DROP KEY fk_orders_user,结果报错找不到。删除普通索引用DROP INDEX,删除外键用DROP FOREIGN KEY,两种语法不要混淆。
5.3 几个不太有人提的实操技巧
经验这种东西,写在文档里的少,踩出来才发现“原来如此”。分享几个我自己的习惯:
第一,建表脚本一定要用版本管理工具管理起来。很多人建完表就不管了,等测试环境、生产环境各建一遍,发现两边结构不一致时欲哭无泪。把建表SQL放进代码仓库,每次结构变更都走review流程,才能保证多环境完全同步。
第二,凡是涉及ALTER TABLE的操作,先跑一次EXPLAIN或者至少确认表数据量。大数据量表加索引、加约束都会锁表或耗时很久,可能导致线上服务中断。数据量大时,可以考虑用在线DDL工具来降低影响。
第三,约束不是给数据库自己看的,是给团队里所有人看的。新同学看一张带完善约束和注释的表,很快能理解业务规则;如果表里什么约束都没有,光靠代码猜,效率极低。
第四,养成定期检查约束是否被触发的习惯。比如每天看看错误日志里有没有频繁的1062重复键冲突,如果有,说明应用层防重逻辑有漏洞,数据库拦截了但业务体验不好,值得去优化代码。
6. 写在经验之后的一点个人建议
我自己在建表这件事上吃的亏不少。早期也是图省事,能不加索引就不加,能不加约束就不约束,结果数据量上来之后,全表扫描慢到接口超时,重复数据、脏数据一堆接一堆,每次做数据清洗都像在扫雷。后来改了习惯,每张表从建表第一天就把主键、唯一键、非空、默认值、外键这些约束一次性定好,后面反而省心得多。
最后分享一个小技巧:把约束当成数据库的“自动测试用例”。每次有SQL违反约束,都是数据库在给你报一个Bug,无论是代码漏传字段,还是业务规则没考虑全,及时去修,比等数据被污染后再补救要划算得多。如果你想系统提升建表能力,可以把前文这张用户订单表多扩展几个场景——比如加一张商品表、一张订单明细表,把复合唯一、外键级联删除都实践一遍。表结构稳了,业务跑起来才会真正轻松。