MySQL 约束与索引完全指南

前言

MySQL 的约束和索引是面试、日常开发中最基础也最容易踩坑的知识点。很多人在建表时只写列级约束,分不清 UNIQUE 约束和唯一索引的区别,不了解 CHECK 在 5.7 下形同虚设,更不知道建表阶段就能直接创建普通索引。

这篇文章会把 六种约束的列级/表级写法、CHECK 的版本坑、外键生产禁用原因、索引的建表创建方式、以及面试高频考点 一次性讲清楚。每个结论都经过实际验证,拿来即用。

前置重要修正 & 说明

  1. NOT NULLDEFAULT 仅支持列级,不存在表级写法

  2. PRIMARY KEY / UNIQUE / CHECK / FOREIGN KEY 支持列级、表级两种写法

  3. 标准SQL条件使用 AND,禁止使用 &&&& 是MySQL方言,不通用)

  4. MySQL 5.7 语法上允许写CHECK,但是完全不生效;8.0版本才真正实现CHECK约束

  5. ⚠️ 区分两个语法:

CONSTRAINT 名称 UNIQUE(字段) → 标准SQL【唯一约束】

UNIQUE INDEX 索引名(字段) → MySQL扩展【唯一索引】;底层存储结构一致,但DDL删除语法不同


一、约束的两种基础模板

MySQL 建表时,约束可以跟在字段后面(列级),也可以在所有字段定义完后统一声明(表级)。

复制代码
-- 【列级约束】约束紧跟字段定义,只作用于当前字段
CREATE TABLE demo(
    字段名 数据类型 [约束],
    ...
);

-- 【表级约束】所有字段定义完成后单独声明,支持多字段联合约束,可自定义约束名称
CREATE TABLE demo(
    字段名 数据类型,
    ...,
    [CONSTRAINT 约束名] 约束类型 (字段列表)
);

为什么要学表级写法?

  • 联合主键、联合唯一、联合外键只能通过表级实现;
  • 自定义约束名,后续 ALTER TABLE 增删约束时更清晰,避免系统生成乱码名。

二、支持列级 & 表级写法的约束

1. PRIMARY KEY 主键约束

作用:非空 + 唯一 ,唯一标识一行。一张表最多只能有一个主键。主键约束内置两条强制规则: ① 字段不能为 NULL② 字段不能重复 数据库引擎执行写入时,会主动校验唯一性,重复数据拒绝入库。

① 列级写法(单列主键常用)
复制代码
CREATE TABLE tb_user(
    id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
    name VARCHAR(20)
);
② 表级写法(推荐,支持联合主键)
复制代码
CREATE TABLE tb_user(
    id INT AUTO_INCREMENT COMMENT '主键ID',
    name VARCHAR(20),
    PRIMARY KEY (id)
);

-- ✅ 联合主键:只能使用表级写法
CREATE TABLE tb_order_item(
    order_id INT COMMENT '订单编号',
    product_id INT COMMENT '商品编号',
    PRIMARY KEY (order_id, product_id)
);

💡 主键自动生成聚簇索引(InnoDB),值不能为NULL,不能重复。MySQL 规定:主键约束的系统固定名称永远是 PRIMARY,自己命名是无效的! 哪怕你写 CONSTRAINT pk_tbuser_id PRIMARY KEY(id),去查约束名称依然是 PRIMARY,自定义名称直接被数据库忽略。


2. UNIQUE 唯一约束

作用:字段值不能重复,但允许多个 NULL(NULL和任何值都不相等)。

① 列级写法
复制代码
CREATE TABLE tb_user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    phone VARCHAR(11) UNIQUE COMMENT '手机号,唯一不可重复'
);
② 表级写法(联合唯一、自定义约束名)
复制代码
CREATE TABLE tb_user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    phone VARCHAR(11),
    -- 标准SQL唯一约束
    CONSTRAINT uk_tbuser_phone UNIQUE (phone)
);

-- 联合唯一
CONSTRAINT uk_phone_name UNIQUE (phone, name)

⚠️ 区分:

CONSTRAINT uk_xxx UNIQUE(字段) ------ 标准唯一约束,删除用 DROP CONSTRAINT uk_xxx

UNIQUE INDEX uk_xxx(字段) ------ MySQL 唯一索引,删除用 DROP INDEX uk_xxx

两者底层结构一样,但DDL语法不同。


3. CHECK 检查约束

作用:限制字段取值范围。

🔴 重大坑 :MySQL 5.7 能解析 CHECK 语法但完全不生效,8.0 才真正支持。

① 列级写法
复制代码
CREATE TABLE tb_user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    age INT CHECK (age > 0 AND age <= 120)   -- 5.7无效,8.0生效
);
② 表级写法(方便命名、后续删除)
复制代码
CREATE TABLE tb_user(
    id INT PRIMARY KEY AUTO_INCREMENT,
    age INT,
    CONSTRAINT ck_tbuser_age CHECK (age > 0 AND age <= 120)
);

4. FOREIGN KEY 外键约束

作用:保证表间参照完整性,仅 InnoDB 引擎支持

⚠️ 生产环境几乎不用外键 :高并发下容易死锁、锁等待,实际项目都在业务层代码控制关联关系。

前置条件:被引用字段必须是主键或唯一索引,且数据类型严格一致。

① 列级写法(不推荐)
复制代码
CREATE TABLE tb_order(
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT FOREIGN KEY REFERENCES tb_user(id)
);
② 表级写法(工程规范写法)
复制代码
CREATE TABLE tb_order(
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT COMMENT '关联用户id',
    CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES tb_user(id)
    ON DELETE SET NULL    -- 主表删除,子表置空
    ON UPDATE CASCADE     -- 主表主键更新,子表同步
);

三、仅支持列级写法的约束(无表级语法)

这两个约束只能紧跟在字段后面,写成表级会直接报错。

1. NOT NULL 非空约束

复制代码
-- ✅ 正确
name VARCHAR(20) NOT NULL;

-- ❌ 不存在这种表级写法
CREATE TABLE tb_user(
    id INT,
    name VARCHAR(20),
    NOT NULL(name)   -- 语法错误
);

2. DEFAULT 默认值约束

复制代码
-- ✅ 正确
status TINYINT DEFAULT 1 COMMENT '状态 1启用 0禁用';

-- ❌ 非法表级写法
CREATE TABLE tb_user(
    status TINYINT,
    DEFAULT(status) 1   -- 语法错误
);

四、约束汇总对照表

约束类型 列级支持 表级支持 支持多字段联合 说明
PRIMARY KEY 一张表最多1个,自动创建聚簇索引
UNIQUE 允许多个NULL,一张表可有多个
CHECK MySQL5.7无效,8.0才生效
FOREIGN KEY 仅InnoDB支持,生产不建议用
NOT NULL - 仅列级
DEFAULT - 仅列级

五、完整可运行的规范建表示例

复制代码
CREATE TABLE tb_user (
    id INT AUTO_INCREMENT COMMENT '主键ID',
    username VARCHAR(20) NOT NULL COMMENT '用户名',
    age INT COMMENT '年龄',
    phone VARCHAR(11) NOT NULL COMMENT '手机号码',
    status TINYINT DEFAULT 1 COMMENT '状态:1正常 0禁用',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    -- 表级约束区域
    PRIMARY KEY (id),
    CONSTRAINT uk_tbuser_phone UNIQUE (phone),
    CONSTRAINT ck_tbuser_age CHECK (age > 0 AND age <= 120)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户信息表';

六、ALTER TABLE 动态增删约束(面试高频)

复制代码
-- 1. 添加唯一约束
ALTER TABLE tb_user ADD CONSTRAINT uk_tbuser_phone UNIQUE (phone);
-- 删除唯一约束
ALTER TABLE tb_user DROP CONSTRAINT uk_tbuser_phone;

-- 2. 添加检查约束
ALTER TABLE tb_user ADD CONSTRAINT ck_tbuser_age CHECK (age > 0 AND age <= 120);
-- 删除检查约束
ALTER TABLE tb_user DROP CONSTRAINT ck_tbuser_age;

-- 3. 添加外键
ALTER TABLE tb_order ADD CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES tb_user(id);
-- 删除外键(注意语法是 DROP FOREIGN KEY)
ALTER TABLE tb_order DROP FOREIGN KEY fk_order_user;

-- 4. 添加非空(本质是修改字段属性)
ALTER TABLE tb_user MODIFY COLUMN username VARCHAR(20) NOT NULL;

-- 5. 修改默认值
ALTER TABLE tb_user ALTER COLUMN status SET DEFAULT 1;

七、建表时直接创建索引(不仅可以在建表后加)

很多人以为索引只能在表创建后用 ALTER TABLE ... ADD INDEX,其实建表时完全可以直接定义所有索引,这也是上线前的最佳实践。

1. 约束自动附带索引

  • 主键约束 → 自动创建主键索引

  • 唯一约束 → 自动创建唯一索引

    CREATE TABLE tb_user (
    id INT AUTO_INCREMENT PRIMARY KEY, -- 自动主键索引
    phone VARCHAR(11) UNIQUE -- 自动唯一索引
    );

2. 表级手动创建普通/复合索引(最常用)

复制代码
CREATE TABLE tb_user (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20) NOT NULL,
    age INT,
    -- 单列普通索引
    INDEX idx_tbuser_age (age),
    -- 复合(联合)索引
    INDEX idx_tbuser_name_age (name, age)
);

KEYINDEX 的同义词,写哪个都可以。

3. 列级索引(不推荐)

复制代码
name VARCHAR(20) NOT NULL INDEX   -- 无法自定义索引名,维护困难

4. 建表后再追加索引

复制代码
ALTER TABLE tb_user ADD INDEX idx_tbuser_age(age);
-- 或
CREATE INDEX idx_tbuser_age ON tb_user(age);

大表加索引会锁表(5.7),生产环境建议使用 pt-online-schema-changegh-ost


八、约束 vs 普通索引的关键区分

  1. 主键/UNIQUE 约束 :目的是保证数据唯一性,底层顺便帮你建了索引。
  2. 普通 INDEX 索引没有任何校验规则,唯一目的就是加速查询。
  3. UNIQUE 约束 ≠ 普通索引,前者多了唯一性检查。
  4. 删除区别:
    • 约束用 DROP CONSTRAINT
    • 索引用 DROP INDEX

九、面试高频总结背诵点

  1. NOT NULLDEFAULT 只能列级;主键、唯一、CHECK、外键两种写法均可。
  2. 联合主键、联合唯一必须用表级写法
  3. MySQL 5.7 的 CHECK 只是摆设,8.0 才真正生效。
  4. 外键仅 InnoDB 支持,生产环境用代码控制关联,不用数据库外键。
  5. CONSTRAINT 约束名 用于给约束命名,方便后续按名称删除;列级约束会由系统自动生成随机名,不便于维护。
  6. 建表时就可以创建索引 ,通过表级 INDEX(字段) 定义普通索引,主键/唯一约束自动生成索引。
  7. 已存在的表用 ALTER TABLE ADD INDEXCREATE INDEX 追加索引,但大表要注意锁表风险。
相关推荐
百万蹄蹄向前冲15 小时前
密码都对Node.js却连不上Linux数据库
linux·mysql
华章酱15 小时前
MySQL幻读是怎么出现的
数据库·mysql·幻读
浅念-16 小时前
Redis基础详解:单线程模型、String与Hash数据结构
服务器·数据库·redis·sql·mysql·nosql数据库·nosql
aramae16 小时前
MySQL内置函数(7)
开发语言·笔记·后端·mysql·其他
这个DBA有点耶17 小时前
InnoDB索引组织表下,复合主键和自增主键的物理存储差异与选型对比
数据库·mysql·架构
李少兄18 小时前
数据库批量清理前置防御:如何严谨校验目标表是否存在
mysql
香吧香18 小时前
Docker Swarm 线上环境 MariaDB XA 悬停事务故障排查
mysql·异常
小白男神18 小时前
MySQL进阶学习三(视图)
后端·mysql
文人sec21 小时前
grant 之后要跟着 flush privileges 吗?要不要使用分区表?
数据库·mysql·adb·数据分析