数据库的核心是数据,而数据的家是表。前面你已经学会"建库、切库"这些让数据住进来的江湖手艺,但真正的战场在表这一层——数据的增删改查,全都发生在表里。今天这一篇,我们就围绕着表做文章:怎么创建一张结构合理的表,怎么给它加上各种约束,建完之后想改结构又怎么办,以及最后一拍脑门要删表时,你该有多谨慎。

这一篇是 DDL 的主场。DDL 是 Data Definition Language 的缩写,翻译过来是"数据定义语言",就是用来"定义、修改、干掉"表结构的那一类 SQL 语句——建表 CREATE、改表 ALTER、删表 DROP 全都归它管。它和后面要学的 DML(Data Manipulation Language,数据操作语言,负责 INSERT/UPDATE/DELETE 这种动数据本身的操作)分工明确:DDL 管"盖房子改户型",DML 管"往屋里摆家具换家具"。

你应该有的知识准备

在读之前,希望你至少对下面这些名字不陌生(大多是建库那篇留下的),我这里只做一句话温习:

  • 库(database)与表(table):库是一栋大楼,表是大楼里的一个个房间。数据住在房间里。
  • 字符集(character set)与校验规则(collate):字符集决定"一个字用什么二进制编码",校验规则决定"这些字怎么比大小、排序、去重"。两者都有库级默认值。
  • 存储引擎(storage engine):决定表在磁盘上用什么方式存文件,最常用的是 InnoDB 和 MyISAM。
  • MySQL 客户端:你现在敲 mysql -u root -p 进去的那个黑窗口,就是我们要跑所有 SQL 的地方。

如果你哪一条还蒙着,别急,今天要讲的建表会把它们串起来重新过一遍。

表中两个绕不开的词:字段和约束

先把这个最基础、后面反复出现的词字段钉清楚。字段(field)就是表的"列(column)",你可以把它想成一张 Excel 表格的"某一列表头"。一张表由若干字段组成,每个字段有几个要素:字段名(这一列叫什么)、数据类型(这一列装什么类型的值、装多大)、可选的属性(能不能为空、有没有默认值、是不是主键等)。

再解释约束(constraint)。约束就是数据库给某一列或某几列"定下的规矩",强制数据必须符合某些条件,凡是"越界"的数据直接不让进表。常见的约束有:

  • 主键约束:这一列(或几列)的值必须唯一且不能为空,用来唯一标识一行。
  • 非空约束:这一列的每一行都必须有值。
  • 默认值约束:这一列你不给值的时候,自动填入指定的默认值。
  • 唯一约束:这一列的值在全表必须唯一,但可以为空。
  • 检查约束:给这一列的值划一个合法范围,不满足就拒绝(MySQL 8.0.16+ 才真正强制执行)。

约束不是可有可无的装饰。一个没有约束的表,就像一栋没有"承重墙"的房子——回头你会发现数据脏到你根本没法信任它。

创建表:CREATE TABLE 语法逐行拆

创建表的完整语法长这样:

CREATE TABLE 表名 (
    字段名1 数据类型 [字段属性...],
    字段名2 数据类型 [字段属性...],
    字段名3 数据类型 [字段属性...]
) character set 字符集 collate 校验规则 engine 存储引擎;

逐行给你拆开:

-- 命令头:CREATE TABLE 后面跟"要创建的表名"
CREATE TABLE users (
    -- 第一个字段,字段名叫 id,类型是整型 int
    id int,
    -- 第二个字段,name 是变长字符串,最长 20 个字符,comment 是给字段加注释
    name varchar(20) comment '用户名',
    -- 第三个字段,password 是定长 32 字符,存 md5 值正好 32 位
    password char(32) comment '密码是32位的md5值',
    -- 第四个字段,birthday 是日期类型,只存年月日
    birthday date comment '生日'
) character set utf8 engine MyISAM;

几个点为你讲透:

第一,字段定义里出现的是什么。 每一个字段是"字段名 数据类型"开头,后面可以跟一堆括号里的附加属性,比如 comment '注释'、not null、default 值、primary key、auto_increment 等等。"字段属性"决定了这一列除了类型之外的行为约束,是可选的,但会在约束那一节变得很重要。

第二,字符集和校验规则到底怎么定。 建表语法的结尾,character set utf8 里的 utf8 在 MySQL 里其实是"utf8mb3",只支持最多 3 字节的 UTF-8 字符。真正能存 4 字节表情符号(emoji)的是 utf8mb4。如果你是 8.0 版本,utf8mb4 已经是默认值。这里要记住一个优先级规则:如果你在建表时没指定字符集,就用所在数据库的字符集;如果数据库也没定,就逐级往上套到服务器(server)级别的默认值。 collate 校验规则同理——不指定就沿用库的。所以"建库时把字符集定成 utf8mb4",能让底下所有没显式指定的表都自动继承,省心很多。

第三,一条 CREATE TABLE 末尾的分号不能丢。 SQL 语句要以分号结束,这是客户端解析一句完整命令的记号,忘了它命令就不会执行。

第四,关于 engine 的默认值。 如果不写 engine MyISAM 或者 engine InnoDB,MySQL 会用默认存储引擎(8.0 的默认是 InnoDB)。所以这句 engine 大部分时候可以省,但你得明白它有这个选项,因为它直接决定了表在磁盘上生成哪几个文件。

现在动手在我这个大课堂上实操一把。假设我们要创建一张学生成绩表,字段有学号、姓名、成绩,我建议你把这个案例跟着敲一遍:

-- 先切到我们之前建好的某个库里,比如 student
use student;
-- 建一张成绩表:id 是主键(唯一辨识每行),name 非空,score 默认 0
CREATE TABLE score (
    id int primary key,
    name varchar(50) not null comment '学生姓名',
    score double default 0 comment '默认成绩为 0'
) engine InnoDB;

跑完之后怎么确认成功了?下一节马上教你看表结构。

建表案例:MyISAM 与 InnoDB 在你磁盘上留下的痕迹

知识点里专门给了个"备注":建一个引擎是 InnoDB 的库,去观察存储目录。这句话背后藏着一个非常直观的真相——不同的存储引擎,创建表时产生的文件不一样。

先看数据目录在哪。通常是 MySQL 安装目录下的 data 文件夹,Linux 上常见于 /var/lib/mysql,Windows 上常见于你的安装根目录下的 mysql\data。如果你的库名是 student,那里面就应该能看到一个叫 student 的子文件夹,一个库对应一个目录。

每个表在这个库里都会留下文件。MyISAM 引擎的表会给表生成三个文件,我们拿前面的 users 表举例,它用的是 engine MyISAM:

  • users.frm:表结构文件。字段名、字段类型、约束等信息都写在这里。MySQL 8.0 之前所有引擎都有 .frm(8.0 里 InnoDB 不再生成它,把表结构挪进了数据字典)。
  • users.MYD:表数据文件。MYD 是 MY Data(MyISAM 数据)的缩写,表里每一行记录都存在这。
  • users.MYI:表索引文件。MYI 是 MY Index(MyISAM 索引)的缩写,你在表上建的索引都存在这。

而 InnoDB 引擎的表通常只有一个文件(也可能是分开的 .ibd 数据文件 + 共享表空间文件),它会把数据和索引一并管理,不区分 MYD/MYI。这就是为什么知识点让你"创建一个 engine 是 InnoDB 的数据库,观察存储目录"——你去对照着看,MyISAM 三件套和 InnoDB 的文件,数一数就一目了然了。

这里其实是你理解引擎差异的第一步:MyISAM 把"结构、数据、索引"拆成三块小文件,InnoDB 喜欢把数据按聚簇的方式统一管理。至于两者在锁、事务上的天壤之别,那是后面的章节,今天先在"文件层面"和它混个脸熟。记住一句话:除非你明确知道要老项目的兼容性,否则新表一律用 InnoDB。

先认识数据类型,才知道建表时选什么

建表语法里最核心的自由度,全在"给每个字段选哪种数据类型"。类型选错了,轻则浪费空间,重则丢精度、溢出、甚至存不进去。我把最常用的几类给你捋一遍,建表时基本够用:

类型族代表类型干什么用的建议
整数int、bigint、tinyint存年龄、学号、数量、表 id数量可能很大的用 bigint,0/1 开关用 tinyint
小数decimal(p,s)、double、float存金额、成绩、价格金额用 decimal 保精度,普通的浮点近似值用 double
字符串varchar(n)、char(n)、text存姓名、密码、长文本长度会变的用 varchar,定长的用 char
日期时间date、datetime、timestamp、year存生日、创建时间只存年月日用 date,要精确到秒用 datetime
布尔tinyint(1)存是/否MySQL 没有真正的 bool,用 tinyint(1) 代替

几个高频细节:

  • varchar(n) 里的 n 是"字符数"不是"字节数"。varchar(20) 最多存 20 个字符(一个中文、一个英文、一个 emoji 都算 1 个字符),实际占多少字节由字符集决定。
  • char 和 varchar 的区别:char 定长——不管存多少都占满声明的空间,速度略快但浪费;varchar 变长——用多少占多少,省空间。固定长度的场景(比如 md5 永远 32 位字符)用 char,姓名这种长度可变的用 varchar。
  • 超过 varchar(65535) 用 text 系列,但要注意 text 不能有默认值,使用上也有一系列限制,能不用尽量不用。

这个表格你先有个直觉,等系统学"数据类型"那一篇再逐类展开。

约束:主键、非空、默认、唯一、检查

现在我们正儿八经地进入约束部分。前面列了五种约束,这里挨个讲明白它们怎么用、边界在哪。

主键约束:一张表只有一个主键,且值唯一非空

主键(primary key)是整张表最特殊的列。它的作用用一句话讲:在表中唯一指向某一行的"身份证号"。生活中身份证号保证人人不同,主键保证表中每一行的主键值都不一样,而且必须是"活人拥有"的——也就是不能为 null。

主键有两个必须背下来的硬规则:

  1. 一张表只能有一个主键。 这个"一个"指的是"主键"这个东西一个,但主键可以是"复合主键"——由多列一起拼成主键。复合主键的意思是"这多列的组合值在全表唯一"。比如 (stu_id, course_id) 作为主键,允许同一个学生出现多行、同一门课出现多行,但"同一学生 + 同一门课"的组合只能有一行。
  2. 主键列的值必须唯一且非空。 唯一是主键的本分,非空是因为空值无法用来做唯一辨识。

写进建表语句的方式有几种,效果一样:

CREATE TABLE t1 (
    id int primary key,          -- 写法一:数据类型后面直接跟 primary key
    name varchar(20)
);
CREATE TABLE t2 (
    id int,
    name varchar(20),
    primary key (id)             -- 写法二:表级声明,把主键列写进括号,复合主键用逗号分隔
);

复合主键这样建:

CREATE TABLE sc (
    stu_id int,
    cou_id int,
    score int,
    primary key (stu_id, cou_id)  -- 复合主键:(学号,课号) 的组合必须唯一
);

主键列在 desc 里会显示 Key 那一列为 PRI,后面讲 desc 时你会看到。

这里有个新手极易踩的坑要说透:主键一旦设了,往表里插入相同主键值就会报错。看这个例子:

-- 先建表,id 是主键
CREATE TABLE p (
    id int primary key,
    v int
);
-- 插入一条 id=1
INSERT INTO p VALUES (1, 100);
-- 再插入一条 id 也是 1,会报 "Duplicate entry '1' for key 'p.PRIMARY'"
INSERT INTO p VALUES (1, 200);

第二条 INSERT 会撞墙。这个"主键冲突"是最经典也最让人头大的报错,理解了"主键必须唯一"你就知道为啥它拦你——因为主键就是要保证"一行一个身份",重复了身份就乱了。

非空约束:NOT NULL 这一列必须有值

非空约束(not null)只干一件事:这一列的每一行都不允许是 null。有些业务字段是"没它就活不下去"的,比如用户表的注册邮箱、订单表的金额,这种就应该给它 not null,从根上堵死"空值混进来"的可能。

CREATE TABLE user2 (
    id int primary key,
    phone varchar(11) not null   -- 手机号这一列不能为空
);

如果试着插入一个 phone 为 null 的行,MySQL 会拒绝并报错(8.0 以下会默认把空串塞进去再警告,行为版本相关,8.0 之后严格模式默认开启,直接报错)。

默认值约束:DEFAULT 这一列不给值就自动填

默认值约束(default)给列一个"兜底值":插入时你这一列没给值,数据库就自动用指定默认值填上。

CREATE TABLE order1 (
    id int primary key,
    status varchar(20) default 'pending'  -- 不填状态时默认为 pending
);
-- 只插 id,status 没给,会自动变成 'pending'
INSERT INTO order1 (id) VALUES (1);
SELECT * FROM order1;
-- 结果:id=1, status=pending

需要注意边界:default 只在"你这列没给值"时兜底;如果你诚实地给了 null,那这列就会是 null,不会再用默认值(那是 default 干预不到的)。另外,text/blob 类型不能有默认值,这是它们的限制。

唯一约束:UNIQUE 这一列全表两两不重复,但可以为空

唯一约束(unique)保证这一列(或这几列组合)的值在全表不重复,但它和主键的最大差别是:唯一约束允许 null,而且允许多个 null。因为 MySQL 认为"多个 null 之间没法比,也就谈不上重复",所以 null 可以出现多次。

CREATE TABLE u (
    id int primary key,
    email varchar(100) unique   -- email 全表唯一,但允许为 null
);
-- 第一次插入正常
INSERT INTO u (id, email) VALUES (1, 'a@x.com');
-- 再来一个相同 email,报 Duplicate entry
INSERT INTO u (id, email) VALUES (2, 'a@x.com');
-- 但 email 为 null 可以插多个
INSERT INTO u (id, email) VALUES (3, NULL);
INSERT INTO u (id, email) VALUES (4, NULL);

在 desc 里,唯一索引那列 Key 会显示 UNI。

检查约束:CHECK 给值划范围,8.0.16+ 才真正生效

检查约束(check constraint)用于给这一列的值画一条"合法线",让越线的数据进不了表。比如年龄必须是正数、状态只能是那几个枚举值。语法是把条件写在 check(条件) 里:

CREATE TABLE stu (
    id int primary key,
    age int check (age >= 0 and age <= 150),  -- 年龄必须在 0 到 150 之间
    gender varchar(10) check (gender in ('男','女'))
);

要注意一个坑:MySQL 8.0.16 之前的版本对 check 是"睁一只眼闭一只眼"——你写了它不强制,只是检查一下语法存下来,插入超范围数据照样成功。 8.0.16 之后才算真正强制执行。所以如果你用的是旧版本,别指望 check 帮你拦数据,得靠应用层或触发器去兜。我用它之前会先在文档里确认自己 MySQL 的版本。

关于约束,还有两个知识点(约束名、给约束起名)我们放到后面"查看表结构"和"坑"里讲,因为它们和 desc、ALTER 强相关。

自增字段:AUTO_INCREMENT

和主键搭配最频繁的一个属性是"自增"。auto_increment 的作用是:这一列你根本不用手动给值,MySQL 自动帮你从 1 开始递增(默认步长 1)填上。它给"主键"这份工作省了天大的事——你不需要自己去算"下一条 id 该是几",数据库全包了。

CREATE TABLE t_auto (
    id int primary key auto_increment,  -- id 自增主键
    name varchar(20)
);
-- 只传 name,id 自动分配
INSERT INTO t_auto (name) VALUES ('张三'), ('李四');
SELECT * FROM t_auto;

结果你会发现 id 自动填了 1、2。注意几点:

  • auto_increment 通常只加在主键/唯一键上,而且一般必须是整数类型。
  • 它不会复用已删除的 id。你删掉 id=1 那行,再插一条,新的 id 不会捡回 1,而是从当前自增基础上继续往上。这是"自增只增不减"的特性。
  • 可以手动指定起点:auto_increment=1000 写在建表语句结尾圆括号后面,让 id 从 1000 开始。

查看表结构:DESC 与 SHOW CREATE TABLE

建好表、改完表,你怎么知道表现在长什么样?MySQL 给了你两个视角,一个看"表格总览",一个看"完整 SQL 原文"。

DESC 看表格总览

desc 是 describe 的缩写,作用是查看表结构——把表的每个字段按"行"列出来,告诉你每列的类型、能不能为空、是不是主键/唯一键、默认值、以及额外属性。命令语法超简单:

DESC 表名;

我们用前面那张 users 表演示(假设它已经建好):

mysql> desc users;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | int          | YES  |     | NULL    |       |
| name     | varchar(20)  | YES  |     | NULL    |       |
| password | char(32)     | YES  |     | NULL    |       |
| birthday | date         | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+

这张结果表的每一列分别代表:

  • Field:字段名,也就是列名。
  • Type:字段类型,比如 int、varchar(20)。
  • Null:这一列允不允许为 null。YES 表示允许空,NO 表示非空约束在起作用。
  • Key:这一列是什么键。空白表示普通列;PRI 表示主键;UNI 表示唯一键;MUL 表示该列是一个非唯一索引的一部分或是允许重复值的外键列。
  • Default:这一列的默认值,没设默认就是 NULL。
  • Extra:额外属性,比如自增列这里会显示 auto_increment。

看 desc 是判断"我这张表设计得对不对"最快的方式。凡是主键列,Key 必须是 PRI;凡是 not null 列,Null 必须是 NO;凡是你设了默认值的列,Default 应该能看到那个值。

SHOW CREATE TABLE 看完整建表原文

desc 给你一个"一眼总览",但如果你想知道这张表当初到底是怎么被完整定义的(包括字符集、引擎、注释、约束、自增起点这些 desc 不显示的东西),要用另一个命令:

SHOW CREATE TABLE 表名\G

末尾那个 \G 是让结果竖着打出来、别挤成一坨更好读。输出会把你换行、加引号后的完整建表语句原样复现出来(很多写法 MySQL 会帮你规范化,比如补齐默认引擎、字符集),这是排查"这张表到底按什么建的"最权威的依据。

修改表:ALTER TABLE 的一揽子操作

项目需求天天变,表结构也不可能一成不变。加个字段、删个字段、把某列长度改大、把某列类型换掉……这些统统叫"修改表结构",用的是 ALTER TABLE 命令。

要建立的第一层认知:ALTER TABLE 是"在已有表上动刀"的总入口,后面跟的追加动作五花八门。 我用 CD 机打比方,ALTER TABLE 表名 相当于把"表"这张 CD 放进机器,接着你想干嘛再递命令。下面把最常见的几种一次讲全。

先讲前提:这一节我们在一张已有数据的 users 表上演示。为了贴近真实,先给 users 插两条记录:

-- 往 users 表插入两行数据:id、name、password、birthday
INSERT INTO users VALUES (1,'a','b','1982-01-04'), (2,'b','c','1984-01-04');
SELECT * FROM users;

插完你自己 SELECT 看一眼,记住这两条数据的样子,因为我们接下来要在它头上来回改结构,并且要时刻盯着"改字段对已有数据有没有影响"。

添加字段:ADD

给表加一个字段,语法:

ALTER TABLE 表名 ADD 字段名 数据类型 [字段属性...] [AFTER 字段名];

[AFTER 字段名] 是控制的:新加的字段插到哪个已有字段的后面。不写的话,新字段默认加在表的最后。演示一下给 users 加一个"图片路径"字段 assets,并且要它跟在 birthday 后面:

-- 加一个 assets 字段,类型 varchar(100),存图片路径,紧跟 birthday 列之后
mysql> alter table users add assets varchar(100) comment '图片路径' after birthday;

现在 desc 看表结构,你会发现多了 assets 列,并且它排在了 birthday 后面:

mysql> desc users;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | int          | YES  |     | NULL    |       |
| name     | varchar(20)  | YES  |     | NULL    |       |
| password | char(32)     | YES  |     | NULL    |       |
| birthday | date         | YES  |     | NULL    |       |
| assets   | varchar(100) | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+

然后再 SELECT * FROM users;,会看到原来的两行数据还在,只是新增的 assets 列对每行都填了 NULL——换句话说,插入新字段后,对原来表中的数据没有影响,旧数据不会丢也不会串行,只是新列暂时都空着。这是 ADD 很重要的一个性质:添列是"加抽屉",不动里面的旧东西。

修改字段:MODIFY

modify 用来改一个字段的类型、长度、属性(但不能改字段名,改名要用后面说的 change)。语法:

ALTER TABLE 表名 MODIFY 字段名 新数据类型 [新属性...];

这里有个大坑必须先打预防针:MODIFY 是"整体重定义"这一列——你写什么,新类型就是什么,我前面强调过。所以修改时,如果这一列原来有 not null、default 这类约束,而你在新定义里没带,那这些约束会被改没。这就是为什么文档里反复提醒"modify 列要带上完整定义"。

演示把 users 的 name 从 varchar(20) 改成 varchar(60)(长度变大):

-- 把 name 的长度从 20 改成 60,只改这一个定义
mysql> alter table users modify name varchar(60);

再看 desc,name 的类型变成了 varchar(60),而原数据还在:

mysql> desc users;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | int          | YES  |     | NULL    |       |
| name     | varchar(60)  | YES  |     | NULL    |       |
| password | char(32)     | YES  |     | NULL    |       |
| birthday | date         | YES  |     | NULL    |       +
| assets   | varchar(100) | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+

长度只能从 20 扩大到 60 吗?反过来能不能缩小?能,但有边界:如果你把长度缩得比表里已有的最长数据还短,MySQL 要么直接把超长数据截断(默认行为,丢数据!),要么在严格模式下直接拒绝。 所以缩列要非常小心,先确认这列里真没有更长值再说。

modify 顺便还能改默认值和其它属性,比如给已有的列补一个默认值:

-- 把 assets 字段再加一个默认值 'default.png'(连带把类型再原样写一遍)
ALTER TABLE users MODIFY assets varchar(100) default 'default.png';

看到了吧,想改一个属性,得把整列的类型都重新抄一遍——这就是"整体重定义"的含义。

删除字段:DROP COLUMN

删字段去掉一列,语法:

ALTER TABLE 表名 DROP 字段名;
-- 把 password 列删掉
mysql> alter table users drop password;

删完再看 desc,password 没了:

mysql> desc users;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | int          | YES  |     | NULL    |       |
| name     | varchar(60)  | YES  |     | NULL    |       |
| birthday | date         | YES  |     | NULL    |       |
| assets   | varchar(100) | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+

这里必须把 DROP 字段的高危性讲到最重:删除字段会连同这一列所有的数据一起删掉,而且是直接消失、没有后悔药的。 前面的密码列 password 里存的 32 位密码串('b' 那两行)已经随字段一起被抹掉了。这不是"隐藏"是"销毁"。所以动 DROP 之前,务必先 SELECT 看这一列还有没有你要留的数据,有就先用 SELECT ... WHERE 或者导出工具备份出来,再决定删不删。

ADD/MODIFY/DROP 三种命令一起用逗号分隔

ALTER TABLE 一个很实用的特性是:可以在同一条语句里,用逗号连接多个动作一次执行。语法:

ALTER TABLE 表名
    ADD 字段1 类型1,
    MODIFY 字段2 新类型2,
    DROP 字段3;

这样一次往返就把几件事办了。但要注意:同一条 ALTER 里连续的动作有先后依赖,比如你这条要 DROP 的列又在后面 MODIFY,容易逻辑打架,写的时候按顺序想清楚。

修改表名与列名:RENAME 与 CHANGE

前面 ADD、MODIFY、DROP 改的都是表的内容,还有两类操作是"改名"性质的:改表名、改列名。

改表名:RENAME

把整张表换名,语法:

ALTER TABLE 旧表名 RENAME TO 新表名;

TO 是可省的,即 RENAME 新表名 也行。演示把 users 改名为 employee:

-- 把 users 表改名为 employee,TO 可省
mysql> alter table users rename to employee;

改完这步,原来的表名 users 就用不了了,SELECT * FROM employee 才能看到数据。改名只改"名字"这个标签,表结构、表数据原封不动,可在任何地方用。

改列名:CHANGE

modify 不能改列名,要改列名得用 change。它的语法比 modify 多一个"新列名":

ALTER TABLE 表名 CHANGE 旧列名 新列名 新数据类型 [新属性...];

关键点又来了(跟 modify 一个道理):CHANGE 也必须给出新列的完整定义,包括类型、约束一起写全。 台词语法里明说"新字段需要完整定义",指的就是这个。演示把 employee 的 name 列改名为 xingming:

-- 把 name 列改名为 xingming,类型和约束必须完整重写一遍
mysql> alter table employee change name xingming varchar(60);

desc employee 一看,name 真的变成了 xingming:

mysql> desc employee;
+----------+--------------+------+-----+---------+-------+
| Field    | Type         | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id       | int          | YES  |     | NULL    |       |
| xingming | varchar(60)  | YES  |     | NULL    |       |
| birthday | date         | YES  |     | NULL    |       |
| assets   | varchar(100) | YES  |     | NULL    |       |
+----------+--------------+------+-----+---------+-------+

数据呢?还健在——SELECT * FROM employee 能看到原来的两行,只是列头从 name 换成了 xingming。改名只是换个标签,数据不搬家。

最后把 modify 和 change 一对儿记牢:

  • MODIFY:改类型/长度/属性,不能改列名。
  • CHANGE:能改列名,但要带上完整的新定义(改列名之外,顺带也能改类型)。

约束的增删改:在已存在的表上加主键/唯一/默认

前面建表时给列加了约束,但现实里经常是"表都建好了、数据也进了一堆,才发现这列该加个约束"。这时就得用 ALTER TABLE 来补。给你看几个最常用的对约束的增删:

-- 给已有表的 id 列追加主键
ALTER TABLE t ADD PRIMARY KEY (id);
 
-- 给 email 列追加唯一约束
ALTER TABLE t ADD UNIQUE (email);
 
-- 给 age 列追加默认值
ALTER TABLE t ALTER age SET DEFAULT 18;
 
-- 删掉默认值
ALTER TABLE t ALTER age DROP DEFAULT;

这些语句就是讲透一点就够了:加约束本质上是在给"已经存在的列"补规矩,但前提是表里的数据得先符合这条规矩。 比如表里有两行 email 已经重复了,你再 ADD UNIQUE (email) 就会因为现有数据违规而报错。所以加约束之前,得先保证老数据是干净的。

删除表:DROP 高危操作

删表是整个章节最需要"手放刹车"的一步。语法:

DROP [TEMPORARY] TABLE [IF EXISTS] 表名1 [, 表名2] ...;

各部分含义:

  • DROP TABLE:删除表的主体命令。
  • TEMPORARY:可选,表示只删"临时表"。
  • IF EXISTS:可选,加了它,如果表不存在也不会报错,直接忽略;不加,表不存在时报错。
  • 表名1[, 表名2]:可以一次删多张,用逗号分隔。

演示删掉一张表 t1:

-- 把 t1 表整个删掉(表结构 + 表数据一起没)
drop table t1;

DROP TABLE 的威力和 DROP 字段一脉相承但更大的多:它会把整张表的结构、数据、以及表上的索引、约束"连锅端"全部删除,且不可撤销。 建表时辛辛苦苦的 CREATE、填进去的数据、加的索引,一秒钟全没了。所以在生产环境,DROP TABLE(以及 DROP DATABASE)是最高危的操作之一。任何这种"删库删表"级别的东西,我的建议是:先在测试库、先在低峰期、先备份、先 --if-exists 兜底、再确认表名拼写无误,最后才执行。

MySQL 8.0 还提供了一道"后悔药"叫 DROP TABLE ... ; 配合 RESTRICT 其实帮不上忙——真正的后悔药是"回收站机制"(MySQL 8.0.20+ 某些版本内置的 mysql.purged_* 或第三方工具实现),但大部分环境没有。所以最可靠的防线永远是:删之前备份。

反引号:看似不起眼却总出事的符号

最后补一个所有建表/改表语句里随时会碰到、又最容易被忽略的符号:**反引号 ` **(键盘左上角 Esc 键下方那个键,~ 波浪号那个键位,按一下打的不是单引号 ')。

反引号的作用是"把表名、库名、字段名包起来,当作普通标识符处理",这样哪怕你的表名或列名碰巧跟 MySQL 的保留字重名(比如你非得起个叫 order、desc、select 的字段/表),也能正常使用。看两个例子:

-- 反引号包住"可能和保留字冲突"的列名/表名,避免语法错误
CREATE TABLE `t` (
    `select` int,      -- 列名叫 select(保留字),用反引号包住才不报错
    `order` varchar(20)
);
-- 查的时候也要用反引号包住
SELECT `select` FROM `t`;

现实里更常见的用法是:MySQL 自带的图形工具(像 Navicat)自动生成的 SQL,通篇都是反引号——那是它在给你做"保险",把每个标识符都罩起来。反引号不是必须的,你名字起得规矩(不以数字开头、不碰保留字),完全可以不写;但一旦你的字段名或表名将要踩到保留字,反引号就是唯一救星。记住"反引号包标识符、单引号包字符串"这个区分,才不会在这两个看起来很像的引号上翻车。

建表规范与那些坑:一篇小抄

课程主体讲完了,我把这一路最容易踩、最值得记的规范攒成一张"实操小抄",方便你建表、改表时翻。

建表就争取一次设计好,少做后补。 ALTER 虽然救场,但每一句 ALTER 在大表上都可能是速度灾难(改类型往往要重建整张表、锁表,线上动辄卡住)。设计阶段多花五分钟,胜过上线后补十句 ALTER。

字字符集明确,别裸奔。 建表最好显式 character set utf8mb4,或至少确认库级继承下来的是 utf8mb4,否则遇到 emoji 就原形毕露。

引擎新表无脑 InnoDB。 除非有特殊兼容需求,别碰 MyISAM(不支持事务、行锁,崩溃恢复差,MySQL 8.0 后 MyISAM 连表结构文件都没了基本是历史遗留)。

主键是表的命根子。 尽量给每张表一个自增主键 id int auto_increment primary key,别设计"没有主键"的表。

约束宁可多给不可少。 能在库里有约束兜底,就别只靠程序里 if 判断——数据库说"不"比程序说"不"更可靠。

modify/change 要带完整定义。 改一列时要重抄整列的类型和想保留的约束,否则会被"定义覆盖"吞掉。

缩列先看数据,加约束先查脏数据。 这两类操作都要求现有数据配合,先 SELECT 确认是安全的黄色信号。

DROP 类操作给足敬畏。 字段、表、库,删之前备份,删之后没有撤销。

保留字与反引号。 名字起规矩省心,真撞了保留字就用反引号。

每次改完用 DESC / SHOW CREATE TABLE 复验。 改没改对,眼睛看结构最直观,别盲目信任命令执行成功的提示。

思考题与详解

每篇留几道题,自行验证掌握程度。答案写在题目下方,先自己动脑再对照。

思考题 1:为什么一张表只能有一个主键,却能有很多个唯一约束?

答案与详解: 主键和唯一约束都保证"唯一",但主键的定位是"整张表的身份标识",一套数据只能有一个"身份证制度",所以只能有一个主键;而唯一约束只是"保证这一列不重复"的辅助规矩,业务上可能同时需要"邮箱唯一、手机号唯一、昵称唯一",这几个互不冲突,可以并存,所以可以有多个唯一约束。除此之外主键还隐含非空(不能为 null),而唯一约束允许且允许多个 null。这就是"一个主键 + 多个唯一"能并存的原因。

思考题 2:用 ALTER TABLE t MODIFY age int; 想把 age 列改成 int,如果 age 原本是 int not null default 18,改完之后会发生什么?

答案与详解: 会发生"约束丢失"。因为 MODIFY 是"整体重定义",你这条新定义里只写了 int,没写 not null 也没写 default 18,所以改完之后 age 变成了"允许为空、无默认值"的普通 int 列,原约束被覆盖没了。这正是"MODIFY 要写完整定义"的醒世案例。如果你只想改类型而保留约束,就得把想留下的约束一并写上,例如 ALTER TABLE t MODIFY age int not null default 18;。

思考题 3:某表 t 的 id 是自增主键,现在已插入 id=1、2、3 三行,然后删掉了 id=3 的行,再插入新一行,新行的 id 是多少?

答案与详解: 是 4。"自增"只增不减,即使 3 被删了,自增计数器的当前值也已经走到 4,所以下一条插入会拿 4,而不是"捡回"被删的 3。这是自增的设计:省去"去找最大 id 再 +1"的复杂度,也不做空闲 id 复用。如果你非要手动控制,可以显式传 id 强制指定,或在建表时用 auto_increment=1000 设起点,但默认的自增行为就是"一路取新的最大,自增不回收"。

思考题 4:为什么给已有数据的表 ADD UNIQUE (email) 之前,最好先查一遍数据?

答案与详解: 因为唯一约束要求"这一列现有数据在全表两两不重复",如果表里已经存在两个相同 email 的行,ADD UNIQUE(email) 一旦试图为这一列建唯一索引,就会因为现有数据违规而报错(比如 Duplicate entry)。所以加这种约束前要先用 SELECT 或 GROUP BY ... HAVING count(*)>1 查一下有没有重复。同样,ADD PRIMARY KEY 之前也要确认目标列没有重复、没有 null。一句话:给老表加约束,等于让老数据"补考通过"才能加,不然就加不上去。

思考题 5:DROP TABLE 和 ALTER TABLE ... DROP 字段 都会造成不可恢复的删除吗?两者在人为风险上有什么区别?

答案与详解: 两者的共同点是"删除不可撤销",都是高危操作。区别在影响范围:ALTER TABLE ... DROP 字段 只删这一列的数据和列本身,表的其它列、其它行的数据都还在,损失是"局部"的;DROP TABLE 则把整张表的结构、全部数据、索引、约束一起端掉,损失是"整盘"的,重建也没有了原数据。因此 DROP TABLE(以及 DROP DATABASE)风险级别更高、更要求先备份。使用习惯上,凡是 DROP 都要先备份、先 SELECT 复核、先 IF EXISTS 兜底,再动手。

思考题 6:为什么在 MySQL 8.0.16 之前,CHECK 约束"写了也白写"?

答案与详解: 在 8.0.16 之前,MySQL 对 CHECK 的处理是"语法上接受、存储下来、但实际上不强制执行"——你插入超范围的数据,数据库根本不拦你,CHECK 只是个"摆设"。从 8.0.16 起,MySQL 才开始真正校验并强制 CHECK 约束。所以如果你在使用旧版本,不要指望 CHECK 帮你挡脏数据,得靠应用层校验或触发器兜底;如果是 8.0.16 及以上,则可以用它作为层面的防线。写CHECK 前先 SELECT VERSION(); 确认自己版本,是稳妥做法。

思考题 7:MODIFY 和 CHANGE 都能改列,区别到底在哪?给 name varchar(50) 这列"只改长度到 80",用哪个、怎么写?

答案与详解: 区别就一句:MODIFY 不能改列名,CHANGE 能改列名(但必须给出新列的完整定义)。"只改 name 的长度到 80"这种情况列名没变,用哪个都行,但既然不改名,用 MODIFY 更直观: ALTER TABLE t MODIFY name varchar(80); 如果你还要顺带把列名从 name 改成 full_name,那就只能用 CHANGE,并且要写完整定义: ALTER TABLE t CHANGE name full_name varchar(80); 提醒一点:上面两例都只写了类型,如果原列带了 not null 等约束且你想保留,就记得在定义里补充。

思考题 8:为什么大量使用字符串且有中英文混合的场景,更推荐 utf8mb4 而不是 utf8?

答案与详解: MySQL 里的 utf8(老别名)实际是 utf8mb3,每个字符最多 3 字节,能覆盖绝大多数常用汉字和拉丁字符,但存不下 emoji 这类需要 4 字节的字符;一旦用户昵称里带了表情,要么插入报错、要么被拆成乱码。utf8mb4 是"最大 4 字节"的完整 UTF-8,既能存汉字也能存 emoji 和各类增补平面字符,兼容性更广,也因此成为 MySQL 8.0 的默认字符集。选 utf8mb4 基本不会有"字符集不够用"的烦恼,代价只是极小的存储开销,对于当今服务器磁盘来说可以忽略。

思考题 9:desc 结果里,为什么主键列的 Null 是 NO、Key 是 PRI,而唯一约束列 Null 可以是 YES?

答案与详解: 因为主键的语义就是"既唯一又非空",建主键时 MySQL 隐式给这列加上非空保证,所以 Null 一定显示 NO,Key 显示 PRI。而唯一约束只保证"不重复",并不要求非空,所以唯一约束列允许为 null,Null 列可能显示 YES(同时也因为唯一性,Key 显示 UNI)。你只需要记住:主键 = 唯一 + 非空,唯一约束 = 唯一(可空),这样看 desc 时就能自洽地解释每个字段。


到这里,关于"表的一生"你已经走完了:从 CREATE TABLE 把它生下来,用 DESC 和 SHOW CREATE TABLE 随时体检它,用 ALTER TABLE 的 ADD/MODIFY/CHANGE/DROP 陪它改头换面,最后用 DROP TABLE 送它走。你如今也知道了一张合格的表该长什么样——有显式的字符集、有自增主键、该上约束的地方绝不让数据裸奔、改字段永远带上完整定义。

建表绝不是"写一句 CREATE 就完事"的儿戏,它承载着整张表的命运:结构设计得好,后续十年增删改查都轻松;设计得糙,上线没多久就得靠一堆 ALTER 打补丁。而这一篇里埋的地基——字段、数据类型、约束、自增、desc——恰恰是接下来"往表里插数据、查数据、改数据、删数据"(DML)那几课的前置弹药。下一篇文章,我们就正式把数据搬进这张精心布置的桌子,从 INSERT 第一行记录说起。