设计数据库表结构时,主键和外键是绕不开的两个概念。主键用来唯一标识一行记录,外键则负责把两张表关联起来,保证数据的一致性。不少人在写建表语句时,主键随便加,外键直接省略,等到数据量上来出现脏数据才后悔。这篇文章就把MySQL里创建主键和外键的语句写法完整梳理一遍,并附上可以直接运行的例子。

一、主键的几种创建写法
主键(primary key)的本质是一个非空且唯一的索引。在MySQL里声明主键有三种常见位置:直接跟在列定义后面、单独一行表级约束、以及建表之后用alter table追加。
第一种是列内联写法,最简洁,适合单列主键:
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;第二种是表级写法,把PRIMARY KEY单独写一行。当主键由多个列组成(复合主键)时,只能用这种方式:
CREATE TABLE order_items (
order_id INT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
quantity INT NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;第三种是事后补救,表已经建好了才发现没设主键,可以用alter table修改:
ALTER TABLE users ADD PRIMARY KEY (id);
需要注意两点:一张表只能有一个主键,但主键可以包含多列;自增列AUTO_INCREMENT必须是索引的一部分,通常直接让它当主键。另外推荐使用无符号整型(UNSIGNED)配合AUTO_INCREMENT,能支持的id范围直接翻倍。
二、外键约束的完整语法
外键(foreign key)用于建立两张表之间的引用关系。比如订单表里的user_id必须对应users表里真实存在的id,这个约束就靠外键实现。外键同样有列内联和表级两种写法,先看表级写法,这是最规范、可读性最好的方式:
CREATE TABLE orders (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
order_no VARCHAR(32) NOT NULL,
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id)
REFERENCES users (id)
ON DELETE CASCADE
ON UPDATE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这条语句里几个部分的作用分别是:CONSTRAINT后面的fk_orders_user是约束名,方便以后删除或修改;FOREIGN KEY括号里写本表的外键列;REFERENCES后面跟被引用的表和列。ON DELETE和ON UPDATE定义了参照动作,常用的有四种。
- CASCADE:主表记录删除时,子表关联记录跟着删除;主表id更新时,子表外键值同步更新。
- RESTRICT:只要子表还有引用,主表就不允许删除或更新,直接报错,这是默认行为。
- SET NULL:主表记录删除后,子表对应的外键列被置为NULL,要求该列允许为NULL。
- NO ACTION:在MySQL的InnoDB中等同于RESTRICT。
如果建表时忘了加外键,可以用alter table补上,效果完全一样:
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE CASCADE;删除外键的语句也很简单,用约束名即可:
ALTER TABLE orders DROP FOREIGN KEY fk_orders_user;
三、创建外键时的常见报错与避坑要点
实际操作中,外键创建失败的概率远高于主键,原因多半集中在以下几个地方。第一是存储引擎问题,只有InnoDB支持外键,如果建表时写了ENGINE=MyISAM,外键约束会被静默忽略,不报错但不生效,这是最隐蔽的坑。MySQL 5.5之后的默认引擎是InnoDB,一般不用担心,但老项目迁移时要留意。
第二是数据类型不完全一致。外键列和被引用列的类型、长度、符号属性必须严格相同,users表的id是INT UNSIGNED,那么orders表的user_id也必须是INT UNSIGNED,只写INT就会报错。字符集和排序规则不同也可能导致失败。
第三是被引用列必须有索引。主键自带索引所以没问题,但如果引用的是普通列,必须先给它建索引,否则会报错。有趣的是,MySQL会自动为外键列创建索引,但不会为被引用列创建。
第四是已存在脏数据。如果orders表里有一条user_id为999的记录,而users表里没有id为999的用户,添加外键约束时会直接失败。解决办法是先清理数据再建约束:
-- 先查出引用了不存在用户的脏数据
SELECT o.* FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.id IS NULL;
-- 清理后再添加外键
ALTER TABLE orders
ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users (id);最后提一个工程实践层面的建议:互联网高并发场景下,很多团队会选择不建物理外键,只在应用层保证关联关系,因为级联操作会在高峰期锁住多张表,影响性能。但对于后台管理系统、财务类对一致性要求高的系统,物理外键仍然是防止脏数据的最有效手段。是否使用,要根据业务场景权衡,而不是一刀切。
总结一下:主键用PRIMARY KEY声明,单列可内联,复合主键用表级写法;外键用FOREIGN KEY配合REFERENCES,别忘了指定ON DELETE动作。表名、约束名规范命名,类型严格对齐,引擎选InnoDB,掌握这些要点,建表语句基本就不会出问题了。