主键、自增和约束
建表不是只写字段类型,还要告诉数据库哪些数据是合法的。
这些规则就叫约束。
例如:
- 用户名不能为空。
- 邮箱不能重复。
- 年龄不能超过正常范围。
- 文章必须属于一个已经存在的用户。
这些都应该尽量交给数据库做最后兜底。
一、先看一个完整例子
先看一张用户表:
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
age TINYINT UNSIGNED NULL,
status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT uk_users_username UNIQUE (username),
CONSTRAINT uk_users_email UNIQUE (email),
CONSTRAINT chk_users_age CHECK (age IS NULL OR age <= 120)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这张表里同时用到了:
后面会把这些语法逐个拆开讲。
二、约束的两种写法
MySQL 里约束常见有两种写法。
1. 字段级约束
字段级约束直接写在字段后面。
基本格式:
例如:
username VARCHAR(50) NOT NULL
意思是:
再例如:
status ENUM('active', 'disabled') NOT NULL DEFAULT 'active'
意思是:
status 只能是 active 或 disabled。
status 不能为空。
- 插入数据时如果不传
status,默认就是 active。
2. 表级约束
表级约束写在所有字段定义的后面,和字段定义一样用逗号分隔。
基本格式:
CONSTRAINT 约束名 约束类型 (字段名)
例如:
CONSTRAINT uk_users_email UNIQUE (email)
拆开看:
所以这句 SQL 的意思是:
给 users 表的 email 字段加一个唯一约束,约束名叫 uk_users_email
为什么要起约束名?
- 报错时更容易知道是哪条规则失败。
- 后面修改表结构时,可以通过名字删除或调整约束。
- 团队协作时,看到名字就知道约束用途。
常见命名方式:
注意:NOT NULL 和 DEFAULT 通常直接写在字段后面,不用 CONSTRAINT 起名字。
三、建表语句里逗号怎么写
很多初学者第一次写 CREATE TABLE 时,容易被逗号卡住。
基本结构是:
CREATE TABLE 表名 (
字段定义1,
字段定义2,
字段定义3,
表级约束1,
表级约束2
);
规则很简单:
- 每一行字段定义后面通常要加逗号。
- 每一行表级约束后面通常也要加逗号。
- 最后一行后面不要加逗号。
例如:
CREATE TABLE demo (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_demo_email UNIQUE (email)
);
这里最后一行 CONSTRAINT uk_demo_email UNIQUE (email) 后面没有逗号。
四、主键 PRIMARY KEY
主键用来唯一标识一行数据。
一张业务表通常都应该有主键。
例如用户表:
这里的 id 就是主键。
主键有两个特点:
1. 常见写法
字段后面直接写:
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY
也可以把主键单独写在表级约束位置:
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
完整例子:
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
);
这两种写法都可以。
教程里更推荐后面这种写法,因为后面学习联合主键、唯一约束、外键约束时,结构更统一。
2. 为什么需要主键
假设没有主键:
这两行数据完全一样。
如果要修改其中一行,很难准确告诉数据库要改哪一行。
有了主键以后:
UPDATE users
SET username = 'tom01'
WHERE id = 1;
数据库就能明确知道只修改 id = 1 这一行。
3. 联合主键
主键也可以由多个字段一起组成。
例如学生选课表:
CREATE TABLE student_courses (
student_id BIGINT UNSIGNED NOT NULL,
course_id BIGINT UNSIGNED NOT NULL,
selected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id)
);
这表示:
不过普通业务表更常见的做法还是使用一个自增 id 做主键。
五、自增 AUTO_INCREMENT
AUTO_INCREMENT 表示自动递增。
它常和主键一起使用:
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
插入数据时不用手动传 id:
INSERT INTO users (username, email, password_hash)
VALUES ('tom', 'tom@example.com', 'hash_value');
MySQL 会自动生成下一个 ID。
1. 自增字段的要求
AUTO_INCREMENT 有几个常见要求:
业务表常用:
BIGINT UNSIGNED NOT NULL AUTO_INCREMENT
原因是:
BIGINT 范围大。
UNSIGNED 不允许负数。
- 主键 ID 本来就不需要负数。
2. 自增 ID 不保证连续
不要以为自增 ID 一定是连续的。
例如插入失败、事务回滚、删除数据,都可能导致 ID 中间出现空洞。
这是正常现象。
所以自增 ID 适合做内部主键,不适合直接当作严格连续的订单编号。
如果业务要求订单号连续或有特殊规则,应该单独设计订单号字段。
3. 设置自增起始值
可以设置下一次自增从哪个值开始:
ALTER TABLE users AUTO_INCREMENT = 10001;
这个语句表示:
users 表下一次自动生成的 id 从 10001 附近开始
注意:如果表里已经有更大的 ID,MySQL 不会让自增值倒退。
六、非空约束 NOT NULL
NOT NULL 表示字段不能为空。
语法:
例如:
username VARCHAR(50) NOT NULL
这表示插入用户时,username 必须有值。
如果这样插入:
INSERT INTO users (email, password_hash)
VALUES ('no-name@example.com', 'hash_value');
会失败,因为没有给 username 赋值。
允许为空时可以写 NULL:
NULL 表示未知、没有填写、不适用。
建议:
虽然 MySQL 默认字段通常允许 NULL,但建议你在建表时明确写 NOT NULL 或 NULL,这样表结构更清楚。
七、唯一约束 UNIQUE
UNIQUE 表示字段值不能重复。
邮箱、手机号、用户名、订单号这类字段经常需要唯一约束。
1. 字段级写法
email VARCHAR(100) NOT NULL UNIQUE
这表示:
2. 表级写法
CONSTRAINT uk_users_email UNIQUE (email)
这是更推荐的写法。
因为它给约束起了名字:uk_users_email。
完整例子:
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_users_email UNIQUE (email)
);
如果已经有一条数据:
INSERT INTO users (email)
VALUES ('tom@example.com');
再插入同样的邮箱:
INSERT INTO users (email)
VALUES ('tom@example.com');
就会失败。
3. 联合唯一约束
唯一约束也可以作用在多个字段上。
例如第三方登录账号表:
CREATE TABLE user_oauth_accounts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
platform VARCHAR(30) NOT NULL,
open_id VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_oauth_platform_openid UNIQUE (platform, open_id)
);
这表示:
platform 和 open_id 的组合不能重复
也就是说:
4. UNIQUE 和 NULL 的关系
MySQL 里,唯一约束允许出现多个 NULL。
例如:
如果没有写 NOT NULL,多行数据的 phone 都可以是 NULL。
所以真正要求唯一的业务字段,通常要这样写:
phone VARCHAR(20) NOT NULL,
CONSTRAINT uk_users_phone UNIQUE (phone)
八、默认值 DEFAULT
DEFAULT 表示插入数据时如果没有提供值,就使用默认值。
语法:
常见写法:
status ENUM('active', 'disabled') NOT NULL DEFAULT 'active'
插入时不传 status:
INSERT INTO users (username, email, password_hash)
VALUES ('jack', 'jack@example.com', 'hash_value');
查询时 status 会是:
数字默认值:
view_count INT UNSIGNED NOT NULL DEFAULT 0
时间默认值:
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
更新时间常见写法:
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
含义是:
- 插入数据时,
updated_at 默认是当前时间。
- 更新这一行数据时,
updated_at 自动变成当前时间。
注意:DEFAULT 通常是字段定义的一部分,不用 CONSTRAINT 起名字。
九、检查约束 CHECK
CHECK 用来限制字段值必须满足某个条件。
MySQL 8.0.16 以后才真正执行 CHECK 约束。
基本语法:
CONSTRAINT 约束名 CHECK (条件表达式)
例如限制年龄:
CONSTRAINT chk_users_age CHECK (age IS NULL OR age <= 120)
拆开看:
为什么写成 age IS NULL OR age <= 120?
因为前面字段定义里年龄允许为空:
age TINYINT UNSIGNED NULL
所以检查规则要表达:
age 可以为空;如果不为空,就必须小于等于 120
完整例子:
CREATE TABLE check_demo (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
age TINYINT UNSIGNED NULL,
PRIMARY KEY (id),
CONSTRAINT chk_age_range CHECK (age IS NULL OR age <= 120)
);
插入合法数据:
INSERT INTO check_demo (age)
VALUES (18);
插入非法数据:
INSERT INTO check_demo (age)
VALUES (200);
会失败,因为 200 不满足 age <= 120。
再看一个价格例子:
CREATE TABLE products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT chk_products_price CHECK (price >= 0)
);
这里限制商品价格不能小于 0。
实际项目里不要只依赖 CHECK:
- 后端参数校验负责给用户友好提示。
- 数据库约束负责兜底,防止绕过后端直接写入脏数据。
十、外键约束 FOREIGN KEY
外键用来保证两张表之间的关联是有效的。
例如文章属于某个用户:
posts.user_id 关联 users.id
也就是说,文章表里的 user_id 必须在用户表 users.id 中真实存在。
1. 先创建被引用的表
先有用户表:
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_users_email UNIQUE (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
再创建文章表:
CREATE TABLE posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 外键语法拆解
外键的完整格式通常是:
CONSTRAINT 外键名
FOREIGN KEY (当前表字段)
REFERENCES 被引用表(被引用字段)
ON DELETE 删除行为
ON UPDATE 更新行为
例如:
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
ON UPDATE CASCADE
拆开看:
3. 外键字段类型必须一致
外键字段和被引用字段类型要一致。
正确:
users.id BIGINT UNSIGNED
posts.user_id BIGINT UNSIGNED
错误:
users.id BIGINT UNSIGNED
posts.user_id BIGINT
少了 UNSIGNED 也可能导致外键创建失败。
4. 插入不存在的关联数据会失败
如果 users 表里没有 id = 999 的用户:
INSERT INTO posts (user_id, title, content)
VALUES (999, '错误文章', '这个用户不存在');
这条 SQL 会失败。
因为外键要求:
posts.user_id 必须能在 users.id 中找到
十一、外键的删除和更新行为
外键可以指定父表数据删除或更新时,子表如何处理。
常见行为:
1. RESTRICT
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
ON UPDATE CASCADE
含义:
这是比较保守、安全的做法。
2. CASCADE
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE CASCADE
含义:
这个行为要谨慎使用。
如果误删用户,文章也会一起被删。
3. SET NULL
user_id BIGINT UNSIGNED NULL,
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE SET NULL
含义:
删除用户时,文章不删除,只是把 posts.user_id 改成 NULL
注意:使用 ON DELETE SET NULL 时,外键字段必须允许 NULL。
如果字段写的是:
user_id BIGINT UNSIGNED NOT NULL
就不能使用 ON DELETE SET NULL。
十二、修改已有表的约束
实际项目中,有时表已经创建好了,后面才需要补约束。
这时使用 ALTER TABLE。
1. 添加唯一约束
ALTER TABLE users
ADD CONSTRAINT uk_users_email UNIQUE (email);
意思是:
给 users.email 添加一个名叫 uk_users_email 的唯一约束
添加之前要确认表里没有重复邮箱,否则会添加失败。
2. 添加检查约束
ALTER TABLE users
ADD CONSTRAINT chk_users_age CHECK (age IS NULL OR age <= 120);
3. 添加外键约束
ALTER TABLE posts
ADD CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
ON UPDATE CASCADE;
添加之前要确认:
posts.user_id 的类型和 users.id 一致。
users.id 已经是主键或唯一键。
posts 里已有的 user_id 都能在 users.id 里找到。
- 两张表都使用支持外键的存储引擎,通常是
InnoDB。
4. 修改字段为 NOT NULL
NOT NULL 是字段定义的一部分,通常用 MODIFY 修改字段。
ALTER TABLE users
MODIFY username VARCHAR(50) NOT NULL;
注意:修改前要确认 username 里没有 NULL,否则会失败。
5. 删除约束
删除唯一约束:
ALTER TABLE users
DROP INDEX uk_users_email;
删除检查约束:
ALTER TABLE users
DROP CHECK chk_users_age;
删除外键约束:
ALTER TABLE posts
DROP FOREIGN KEY fk_posts_user;
主键一般不要随便删除。
如果确实要删除主键,需要先确认表结构和自增字段会不会受影响:
ALTER TABLE users
DROP PRIMARY KEY;
十三、完整用户表和文章表示例
用户表:
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
age TINYINT UNSIGNED NULL,
status ENUM('active', 'disabled') NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT uk_users_username UNIQUE (username),
CONSTRAINT uk_users_email UNIQUE (email),
CONSTRAINT chk_users_age CHECK (age IS NULL OR age <= 120)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
文章表:
CREATE TABLE posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
status ENUM('draft', 'published', 'archived') NOT NULL DEFAULT 'draft',
view_count INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
ON UPDATE CASCADE,
CONSTRAINT chk_posts_view_count CHECK (view_count >= 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这两张表体现了几个常见规则:
十四、常见错误
1. UNIQUE 字段忘记写 NOT NULL
email VARCHAR(100) UNIQUE
这表示邮箱不能重复,但可以有多个 NULL。
更常见的业务写法应该是:
email VARCHAR(100) NOT NULL,
CONSTRAINT uk_users_email UNIQUE (email)
2. 外键字段类型不一致
id BIGINT UNSIGNED
user_id BIGINT
一个有 UNSIGNED,一个没有,这就不是完全一致。
应该改成:
id BIGINT UNSIGNED
user_id BIGINT UNSIGNED
3. ON DELETE SET NULL 搭配了 NOT NULL
错误搭配:
user_id BIGINT UNSIGNED NOT NULL,
CONSTRAINT fk_posts_user
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE SET NULL
SET NULL 要把字段设置成 NULL,但字段又写了 NOT NULL,这两个规则互相冲突。
4. 旧版本 MySQL 以为 CHECK 生效
MySQL 8.0.16 之前,CHECK 语法可能能写,但不会真正执行。
如果项目依赖 CHECK,要确认数据库版本。
5. 把自增 ID 当作业务编号
自增 ID 不保证连续,也不适合直接暴露成订单编号。
订单号、流水号这类字段应该单独设计。
十五、约束设计原则
记住一句话: