主键、自增和约束

建表不是只写字段类型,还要告诉数据库哪些数据是合法的。

这些规则就叫约束。

例如:

  • 用户名不能为空。
  • 邮箱不能重复。
  • 年龄不能超过正常范围。
  • 文章必须属于一个已经存在的用户。

这些都应该尽量交给数据库做最后兜底。

一、先看一个完整例子

先看一张用户表:

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;

这张表里同时用到了:

写法含义
PRIMARY KEY (id)id 设置为主键
AUTO_INCREMENT插入数据时自动生成递增 ID
NOT NULL字段不能为空
DEFAULT 'active'没传值时使用默认值
CONSTRAINT uk_users_email UNIQUE (email)email 加唯一约束
CONSTRAINT chk_users_age CHECK (...)age 加检查约束

后面会把这些语法逐个拆开讲。

二、约束的两种写法

MySQL 里约束常见有两种写法。

1. 字段级约束

字段级约束直接写在字段后面。

基本格式:

字段名 数据类型 约束1 约束2 约束3

例如:

username VARCHAR(50) NOT NULL

意思是:

部分含义
username字段名
VARCHAR(50)字段类型
NOT NULL这个字段不能为空

再例如:

status ENUM('active', 'disabled') NOT NULL DEFAULT 'active'

意思是:

  • status 只能是 activedisabled
  • status 不能为空。
  • 插入数据时如果不传 status,默认就是 active

2. 表级约束

表级约束写在所有字段定义的后面,和字段定义一样用逗号分隔。

基本格式:

CONSTRAINT 约束名 约束类型 (字段名)

例如:

CONSTRAINT uk_users_email UNIQUE (email)

拆开看:

部分含义
CONSTRAINT表示这里开始定义一个具名约束
uk_users_email约束名,自己起的名字
UNIQUE约束类型,这里是唯一约束
(email)这个约束作用在哪个字段上

所以这句 SQL 的意思是:

给 users 表的 email 字段加一个唯一约束,约束名叫 uk_users_email

为什么要起约束名?

  • 报错时更容易知道是哪条规则失败。
  • 后面修改表结构时,可以通过名字删除或调整约束。
  • 团队协作时,看到名字就知道约束用途。

常见命名方式:

前缀用途示例
pk_主键约束pk_users
uk_唯一约束uk_users_email
fk_外键约束fk_posts_user
chk_检查约束chk_users_age

注意:NOT NULLDEFAULT 通常直接写在字段后面,不用 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

主键用来唯一标识一行数据。

一张业务表通常都应该有主键。

例如用户表:

idusernameemail
1tomtom@example.com
2jackjack@example.com

这里的 id 就是主键。

主键有两个特点:

  • 不能为 NULL
  • 不能重复。

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 有几个常见要求:

要求说明
必须是整数类型例如 INTBIGINT
通常必须是索引字段最常见就是主键
一张表通常只能有一个自增字段不要给多个字段都加自增

业务表常用:

BIGINT UNSIGNED NOT NULL AUTO_INCREMENT

原因是:

  • BIGINT 范围大。
  • UNSIGNED 不允许负数。
  • 主键 ID 本来就不需要负数。

2. 自增 ID 不保证连续

不要以为自增 ID 一定是连续的。

例如插入失败、事务回滚、删除数据,都可能导致 ID 中间出现空洞。

1, 2, 3, 7, 8

这是正常现象。

所以自增 ID 适合做内部主键,不适合直接当作严格连续的订单编号。

如果业务要求订单号连续或有特殊规则,应该单独设计订单号字段。

3. 设置自增起始值

可以设置下一次自增从哪个值开始:

ALTER TABLE users AUTO_INCREMENT = 10001;

这个语句表示:

users 表下一次自动生成的 id 从 10001 附近开始

注意:如果表里已经有更大的 ID,MySQL 不会让自增值倒退。

六、非空约束 NOT NULL

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

bio TEXT NULL

NULL 表示未知、没有填写、不适用。

建议:

字段建议
用户名、邮箱、密码哈希NOT NULL
文章标题、文章正文NOT NULL
创建时间、更新时间NOT NULL
头像、简介、生日可以 NULL

虽然 MySQL 默认字段通常允许 NULL,但建议你在建表时明确写 NOT NULLNULL,这样表结构更清楚。

七、唯一约束 UNIQUE

UNIQUE 表示字段值不能重复。

邮箱、手机号、用户名、订单号这类字段经常需要唯一约束。

1. 字段级写法

email VARCHAR(100) NOT NULL UNIQUE

这表示:

email 不能为空,并且不能重复

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 的组合不能重复

也就是说:

platformopen_id是否允许
wechatabc允许
githubabc允许
wechatabc不允许,因为组合重复

4. UNIQUE 和 NULL 的关系

MySQL 里,唯一约束允许出现多个 NULL

例如:

phone VARCHAR(20) UNIQUE

如果没有写 NOT NULL,多行数据的 phone 都可以是 NULL

所以真正要求唯一的业务字段,通常要这样写:

phone VARCHAR(20) NOT NULL,
CONSTRAINT uk_users_phone UNIQUE (phone)

八、默认值 DEFAULT

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 会是:

active

数字默认值:

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)

拆开看:

部分含义
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

拆开看:

部分含义
CONSTRAINT fk_posts_user定义一个名叫 fk_posts_user 的约束
FOREIGN KEY (user_id)当前表的 user_id 是外键字段
REFERENCES users(id)它引用 users 表的 id 字段
ON DELETE RESTRICT用户下面还有文章时,不允许删除用户
ON UPDATE CASCADE如果用户 ID 更新,文章表里的 user_id 跟着更新

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 中找到

十一、外键的删除和更新行为

外键可以指定父表数据删除或更新时,子表如何处理。

常见行为:

写法含义
ON DELETE RESTRICT子表还有关联数据时,禁止删除父表数据
ON DELETE CASCADE删除父表数据时,自动删除子表关联数据
ON DELETE SET NULL删除父表数据时,把子表外键字段设置为 NULL
ON UPDATE CASCADE父表主键更新时,子表外键字段跟着更新

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;

这两张表体现了几个常见规则:

规则SQL 写法
用户 ID 自动生成id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT
每张表有主键PRIMARY KEY (id)
用户名不能重复CONSTRAINT uk_users_username UNIQUE (username)
邮箱不能重复CONSTRAINT uk_users_email UNIQUE (email)
年龄范围受限制CONSTRAINT chk_users_age CHECK (...)
文章必须属于有效用户CONSTRAINT fk_posts_user FOREIGN KEY ...
阅读数不能为负数CONSTRAINT chk_posts_view_count CHECK (view_count >= 0)

十四、常见错误

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 不保证连续,也不适合直接暴露成订单编号。

订单号、流水号这类字段应该单独设计。

十五、约束设计原则

原则说明
每张业务表都要有主键更新、删除、关联都需要稳定标识
自增主键只做内部标识不要依赖它连续,也不要把它当业务编号
关键字段尽量 NOT NULL避免出现不完整数据
业务唯一字段加 UNIQUE例如邮箱、手机号、订单号
需要组合唯一时用联合唯一约束例如 (platform, open_id)
外键字段类型必须一致包括 UNSIGNED 也要一致
谨慎使用 ON DELETE CASCADE避免误删父表数据时连带删除大量子表数据
后端校验和数据库约束一起用后端负责提示,数据库负责兜底

记住一句话:

字段类型决定能存什么,约束决定什么数据才算合法。