写了好几年SQL,回头看最容易被低估的一条语句就是 CREATE TABLE。很多人觉得建表嘛,写个 create table user (id int, name varchar(50)) 就完事了,结果过两个月要加字段、要改长度、要处理重复数据、要优化慢查询,才意识到当初那张表建得有多草率。这篇文章我打算先把 CREATE TABLE 的核心语法完整拆一遍,再用一个实际的表结构案例从头走到尾,顺便把最容易踩的坑和日常维护中用得上的设计习惯一并说清楚。不管你是刚学 SQL 的新手,还是写了不少代码但一直没系统梳理过建表逻辑的开发者,这篇都值得花十分钟看完。
1. 建表前最该想清楚的三件事
1.1 你到底需要存储什么
CREATE TABLE 语法本身并不复杂,真正决定一张表好坏的,往往是在写第一条 SQL 之前你有没有想明白业务需求。我见过太多人拿起键盘就写,写完发现字段类型选错、长度不够、唯一性没约束,然后反复 ALTER TABLE,留下一堆历史包袱。
建表前先在纸上列出业务对象的核心属性。比如你要建一张用户表,用户有哪些必须记录的信息?手机号、邮箱、昵称、注册时间、状态,这些是高频查询字段。用户收货地址是要单独一张表还是一并塞进用户表?如果一个人有多个收货地址,塞进用户表就是在给未来挖坑。把这些基础问题理清楚,再动手写 CREATE TABLE,后面会省非常多的事。
另外要想清楚字段的"生命周期"。注册时间、最后登录时间这些字段几乎一定会被查询和统计;一些临时标记位可能上线两周就废弃了。有没有必要把所有字段都放在主表里?那些大字段(比如用户头像 URL 之外的详细资料 JSON)是不是应该拆出去,避免主表行宽过大影响查询性能?这都属于建表前值得花十分钟想明白的问题。
1.2 选对字段类型比想象中更重要
字段类型的选择是 CREATE TABLE 里最影响长期使用体验的部分。很多新手倾向于全部用 varchar,理由是"不管什么都能存"。但这种图省事的做法会在后续几乎每个环节都让你难受:排序是按字典序排的,数值比较会出问题,统计函数用不上,索引也可能失效。
以最常用的几种类型为例:
| 场景 | 推荐类型 | 不推荐的做法 |
|---|---|---|
| 整数 ID | INT / BIGINT | 用 VARCHAR 存 ID,浪费空间且排序错乱(10 会排在 9 前面) |
| 小数金额 | DECIMAL(10,2) | 用 FLOAT/DOUBLE,会有精度误差 |
| 手机号 | VARCHAR(20) | 用 INT,会溢出且丢失前导零 |
| 日期时间 | DATETIME / TIMESTAMP | 用 VARCHAR,无法使用日期函数且排序错乱 |
| 状态值 | TINYINT / ENUM | 用 VARCHAR(255),浪费空间且容易写错 |
| 长文本 | TEXT / MEDIUMTEXT | 用 VARCHAR(5000),超出长度直接报错或截断 |
具体到 MySQL、SQL Server、Oracle、PostgreSQL 这些主流数据库,类型名称略有差异(比如自增字段 MySQL 是 AUTO_INCREMENT,SQL Server 是 IDENTITY(1,1),PostgreSQL 是 SERIAL 或 GENERATED AS IDENTITY),但"选择最贴合数据真实形态的类型"这个原则是共通的。类型定得准,后面写 WHERE 条件、JOIN 关联、建索引都会顺畅很多。
1.3 命名规范在团队协作里等于半条命
说个很现实的场景:你接手一个老项目,打开数据库发现表名有 user、User、t_user、sys_user 四种风格,字段有 userId、user_id、USERID 三种写法。每次写 SQL 都要去确认列名,加个字段还要全库搜一遍有没有别的表用相同含义不同名字的列。
建表时统一命名规范是个低成本高收益的习惯。我个人常用的规范是:表名全小写下划线分隔(user_account、order_detail),字段名全小写下划线分隔(user_id、created_at),主键统一叫 id,外键叫业务含义加 _id(user_id、order_id),时间字段统一叫 created_at、updated_at。SQL 关键字一律大写,增加可读性。这套规范在 MySQL 里格外重要,因为 Linux 下表名区分大小写,Windows 下不区分,同一个库在开发环境和生产环境可能因为大小写问题直接报 table not found。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. CREATE TABLE 核心语法逐段拆解
2.1 一份可以直接套用的完整语法模板
CREATE TABLE 的完整语法在不同数据库里细节不一,但主干是通用的。以最典型的 MySQL 为例:
sql复制CREATE TABLE [IF NOT EXISTS] 表名 (
字段名1 数据类型 [列级约束] [默认值] [注释],
字段名2 数据类型 [列级约束] [默认值] [注释],
...,
[表级约束]
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='表注释';
每个部分的作用:
IF NOT EXISTS:如果同名表已存在则不创建,避免脚本重复执行时直接报错。- 字段定义:字段名 + 数据类型 + 可选的约束(NOT NULL、UNIQUE、PRIMARY KEY、DEFAULT、AUTO_INCREMENT、COMMENT)。
- 表级约束:PRIMARY KEY、UNIQUE KEY、FOREIGN KEY、KEY(索引)可以写在所有字段之后,方便一眼看清整张表的约束结构。
ENGINE、DEFAULT CHARSET:MySQL 专属设置。InnoDB 是支持事务和外键的存储引擎;utf8mb4 是完整的 UTF-8 编码(MySQL 的 utf8 实际上只支持最多 3 字节字符,emoji 根本存不进去)。COMMENT:给表和字段加注释。第一次可能觉得麻烦,但半年后你自己回来看表结构,会发现这些注释比文档还有用。
2.2 一句一句看一个最小示例
拿最简单的用户表举例:
sql复制CREATE TABLE IF NOT EXISTS user_account (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
username VARCHAR(50) NOT NULL COMMENT '用户名',
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
phone VARCHAR(20) DEFAULT NULL COMMENT '手机号',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username),
KEY idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户账号表';
重点看这几个细节:
INT UNSIGNED:主键用无符号整数,可以翻倍可用正数范围,如果业务量没到几十亿没必要硬上 BIGINT,但也好过存负数把空间浪费掉。NOT NULL:username 不允许为空,这是唯一性约束能生效的前提。允许 NULL 的字段上建唯一索引,多行 NULL 并不会冲突(MySQL 中 NULL 不等于 NULL)。DEFAULT CURRENT_TIMESTAMP:创建时间不用你在代码里手动填,数据库自动写入当前时间。ON UPDATE CURRENT_TIMESTAMP更省事,只要这一行数据被更新,数据库自动刷新更新时间。这是非常实用的小特性。UNIQUE KEY uk_username (username):给用户名加唯一索引,避免注册时并发写入重复账号。你可能会问,那查询的时候怎么办?唯一索引本身也是索引,按用户名精确查询时可以直接走这个索引,速度和效果比普通索引更好。KEY idx_email (email):邮箱做普通索引,支持按邮箱查询。如果你现阶段不知道哪些字段会被频繁查询,保守起见只给必要的字段建索引,索引不是越多越好,写多读少的表索引多了反而拖慢写入速度。
2.3 为什么 IF NOT EXISTS 和注释是容易被忽略的救命配置
新手写脚本经常忽略 IF NOT EXISTS。假设你有一份初始化 SQL,要在测试环境重复执行三遍。第一次成功,第二次直接报 Table already exists,你还得先把表删了再重来。如果用 IF NOT EXISTS,重复执行只会跳过,不会报错。这个特性在写迁移脚本、部署脚本、教学示例时都很好用。
注释同理。很多程序员觉得注释是给别人看的,自己写的表不需要。但现实是,过三个月你自己打开数据库看这张表,大概率也忘了 status 这个字段的 0 和 1 到底哪个是正常状态。给每个字段写一行 COMMENT,等于把文档塞进了表结构本身。查字段含义时一条 SHOW FULL COLUMNS FROM user_account; 就能看到全部注释,比翻设计文档快得多。
3. 主键、唯一约束、外键和检查约束,到底该怎么用
3.1 主键选择的自增陷阱与 UUID 之争
主键是 CREATE TABLE 里最需要花心思设计的部分。最常见的是单列自增主键 id INT AUTO_INCREMENT PRIMARY KEY,好处是写入顺序和主键大小基本一致,InnoDB 的聚集索引按主键顺序组织,插入效率高,索引占用空间小。
但自增主键有几个实际场景会踩坑。第一,数据迁移或导入历史数据时,如果硬编码 id 值,可能把自增计数搞乱,后续插入产生主键冲突。第二,分库分表场景下,多个节点的自增主键会重复,这时候需要分布式 ID 方案(雪花算法等),表的主键字段就不能简单依赖数据库自增。第三,同步数据到数据仓库或做多环境数据合并时,自增主键往往没有业务含义,关联定位麻烦。
UUID 或雪花 ID 作为主键的痛点在于,随机字符串做主键会导致 B+ 树频繁页分裂,写入性能下降明显。如果非要用非自增主键,推荐雪花 ID 这类"趋势递增"的方案,而不是完全随机的 UUID。如果就是小项目、单库单表,自增主键是最省心也最高效的选择,别为了"高级"而自找麻烦。
3.2 唯一约束和普通索引,功能看着像但本质不同
UNIQUE KEY 和 KEY 都能加速查询,但 UNIQUE KEY 多了一重"值不允许重复"的约束。这是数据库层面的最后一道防线,比你在应用代码里先 SELECT 再 INSERT 可靠得多。
说个具体场景:用户注册时先查用户名是否存在,不存在就插入。两个请求同时到达,应用层都查不到,然后同时插入,就产生重复数据了。如果你只建了普通索引,这个问题数据库管不了;如果建了唯一索引,第二个 INSERT 会直接报 Duplicate entry,你捕获这个异常,提示用户"用户名已被注册"就好。唯一约束就是那个在并发现场帮你兜底的人。
需要注意,唯一约束对 NULL 的处理各数据库不同。MySQL 中多个 NULL 不视为重复,所以在允许为空的邮箱字段上建唯一索引,不能拦截多条没有邮箱的记录。如果业务上"邮箱要么不填,填了就不允许重复",这个行为正好符合预期;如果希望空值也不允许出现第二次,那还是应该用空字符串而不是 NULL,或者在应用层做控制。
3.3 外键到底建不建,这是一道经典的工程选择题
外键(FOREIGN KEY)用于保证两张表之间的引用完整性。子表插入记录时,外键列的值必须存在于父表主键中;父表删除记录时,如果子表还在引用,会被拦截(RESTRICT)或级联处理(CASCADE)。
sql复制CREATE TABLE order_detail (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
order_id INT UNSIGNED NOT NULL,
product_id INT UNSIGNED NOT NULL,
quantity INT NOT NULL DEFAULT 1,
PRIMARY KEY (id),
KEY idx_order_id (order_id),
CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES order_main(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
我的建议是:小项目、内部系统、团队不大时,外键可以建,省心且有保障;大型互联网高并发系统,很多团队会选择不用外键,把引用完整性交给应用层。 原因是外键约束会在每次插入、更新、删除时触发对父表的检查,在高频写入场景下增加开销和锁竞争,而且分库分表后外键根本没法跨库生效。但如果你做的是一个管理系统、后台应用、或者订单金额敏感的财务模块,我倾向于保留外键,它能在数据库层面阻止很多脏数据的产生。到底怎么选,取决于你对自己项目的判断,没有标准答案。
3.4 CHECK 约束:小约束有大用处
不少开发者对 CHECK 约束的认知停留在"数据库支持但用不上"。实际上它在控制字段取值范围时很好用。比如状态字段只允许 0 和 1,库存字段不允许负数,年龄不允许超过 120。
sql复制CREATE TABLE product (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (id),
CONSTRAINT chk_price CHECK (price >= 0),
CONSTRAINT chk_stock CHECK (stock >= 0),
CONSTRAINT chk_status CHECK (status IN (0, 1))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意不同数据库对 CHECK 约束的执行力度不同。MySQL 8.0.16 之前,CHECK 解析后会被忽略,不会真正生效;8.0.16 之后才开始强制执行。如果你用的老版本 MySQL,不要指望 CHECK 帮你拦截非法值,老老实实靠应用层判断。PostgreSQL、SQL Server 对 CHECK 的支持则一直比较完整。
4. 一个完整案例:从零开始建用户订单表
4.1 订单表设计的取舍思路
看完了单独的知识点,我们来做一个完整案例。需求是:一个简单的电商系统,需要存储用户、订单、订单明细。先分析关系:用户和订单是一对多,订单和订单明细是一对多。于是设计三张表:user_account、order_main、order_detail。
order_main 的字段设计我故意做几个选择,方便说明思考过程:
sql复制CREATE TABLE order_main (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID',
order_no VARCHAR(32) NOT NULL COMMENT '业务订单号',
user_id INT UNSIGNED NOT NULL COMMENT '下单用户ID',
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额',
pay_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '实付金额',
pay_status TINYINT NOT NULL DEFAULT 0 COMMENT '支付状态:0未支付,1已支付,2已退款',
receiver_name VARCHAR(50) NOT NULL COMMENT '收货人姓名',
receiver_phone VARCHAR(20) NOT NULL COMMENT '收货人手机号',
receiver_address VARCHAR(255) NOT NULL COMMENT '收货地址',
remark VARCHAR(255) DEFAULT NULL COMMENT '买家备注',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
paid_at DATETIME DEFAULT NULL COMMENT '支付时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单主表';
注意这里把 order_no 设计成唯一索引的字段。为什么不直接用订单号做主键?自增 BIGINT 做主键,页分裂少、索引小;业务订单号可能包含日期和随机码,长度较长,做主键会占用大量索引空间。普通业务场景推荐"自增主键 + 独立业务订单号 + 唯一约束"的组合,兼顾写入性能和业务查询需求。
4.2 外键、索引和字段长度的平衡
如果按第 3 节说的"内部系统可以建外键",这时可以在 order_detail 上给 order_id 建外键。但我们要注意,如果订单量几年后增长到千万行级别,外键可能成为写入瓶颈。更常见的做法是:保留 order_id 上的普通索引用于关联查询,通过应用层逻辑保证"这个订单明细确实属于这个订单",这也是大型系统的普遍选择。中小项目追求可靠,大项目追求性能和灵活,这个平衡自己拿捏。
字段长度上,DECIMAL(12,2) 可以支撑到十亿级别的金额,对绝大多数业务足够了。VARCHAR(32) 的订单号,如果业务规则里包含了日期再加随机数,一般也不会超长,但要提前留好余量。收货地址 VARCHAR(255) 看似够用,遇到特别长的地址(比如某些带详细门牌号的乡镇地址)可能会截断,如果你遇到过,会知道地址被截断是多让人头疼的事。可以留 500。
4.3 时间字段正确用法与常见误区
时间字段我单独拿出来说,因为这是 CREATE TABLE 里最常见的错误聚集地。很多人的习惯是弄一个 VARCHAR 存"2024-01-01 12:00:00"这种字符串,然后查询时各种 STR_TO_DATE、DATE_FORMAT 来回转换,既麻烦又容易出错。正确做法是直接用 DATETIME 或者 TIMESTAMP 类型。
DATETIME 和 TIMESTAMP 的区别:DATETIME 范围大(1000-9999 年),和时区无关;TIMESTAMP 范围相对小(1970-2038 年,32 位系统限制),存储在 UTC 时间,查询时按会话时区转换。现代系统强烈推荐 DATETIME,否则服务器时区一变,所有历史时间点全部错乱。
设计订单表时,我额外加了 paid_at DATETIME DEFAULT NULL。为什么是 NULL?因为订单创建时还没支付。这个字段只有在支付成功时才写入时间。有些开发者喜欢用"0000-00-00 00:00:00"表示"没有支付时间",这会带来两个问题:一是 MySQL 5.7 以上默认 SQL 模式不允许零日期值,二是逻辑判断时要多处理一个无效值。直接 NULL 最干净,查询未支付订单就是 WHERE paid_at IS NULL,语义清晰。
4.4 字符集和排序规则为什么不能随便选
字符集选错,最常见的后果是中文乱码、emoji 存不进去、中文排序结果怪异。MySQL 老版本默认 latin1,后来默认 utf8,但 MySQL 的 utf8 最多支持 3 字节,emoji 这种 4 字节字符存进去会报错。所以新项目一律使用 utf8mb4,这才是真正的完整 UTF-8 支持。
排序规则 COLLATE 影响的是字符比较和排序。utf8mb4_unicode_ci 按 Unicode 标准排序,对多语言支持更好;utf8mb4_general_ci 比较更快但排序规则略粗糙。日常中文项目用 utf8mb4_unicode_ci 没有明显劣势。需要注意 ci 表示 case-insensitive,也就是英文大小写不敏感。如果业务要求"用户名不区分大小写",这个默认行为刚刚好。表级、库级、字段级的字符集可以分别设置,最省心的策略是建库时设好默认值,建表时继承,不要每个表单独指定。
5. 实操中最常遇到的报错和排查思路
5.1 键值重复、保留字冲突、表已存在的处理
建表时最常见的报错我整理一下,基本覆盖了大部分现场:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| Table 'xxx' already exists | 表已存在 | 使用 IF NOT EXISTS,或先 DROP TABLE 再重建(注意数据备份) |
| Duplicate entry 'xx' for key 'uk_xx' | 插入了违反唯一约束的数据 | 检查业务逻辑或已有数据,清理重复数据后再约束 |
| Can't create table (errno: 150) | 外键关联失败 | 检查父表字段是否有索引、类型是否一致、字符集是否一致 |
| Data too long for column 'xx' | 字段长度不够 | ALTER TABLE 修改字段长度 |
| Unknown column 'xx' in 'field list' | 列名写错 | 查看表结构确认字段名 |
| You have an error in your SQL syntax | SQL 语法错误 | 检查字段定义逗号是否多余或缺失、保留字是否加反引号 |
有个非常隐蔽的问题是使用了保留字。比如字段名叫 order、key、group、user、condition,这些词在 SQL 里有特殊含义,直接用就会语法报错。处理方式有两种:要么换一个字段名(我建议优先),要么在 MySQL 中用反引号包裹保留字 `order`,在 SQL Server 中用方括号 [order],在 PostgreSQL 中用双引号 "order"。我的建议是不要和数据库保留字硬刚,命名时直接避开,团队协作时别人也不需要时刻记着哪些字段要加引号。
5.2 字符集不匹配导致关联查询巨慢
这是一个非常典型的"能跑但很慢"的坑。两张表的关联字段,一张是 utf8mb4_unicode_ci,另一张是 utf8mb4_general_ci,或者一张是 utf8 一张是 utf8mb4。JOIN 时数据库为了做关联,会在内存里把字符集转换,导致索引失效,全表扫描。数据量小的时候没感觉,数据量大了查询直接慢几个数量级。
排查思路是:先查表的字符集,SHOW TABLE STATUS LIKE 'xxx';,再查列的字符集,SHOW FULL COLUMNS FROM xxx;。确保 JOIN 字段两边完全一致。修复方式是在建表时就统一字符集,已经不一致的,用 ALTER TABLE 把两张表的字符集改成一样的,再重建相关索引。
5.3 NULL 引发的三个隐藏坑
NULL 处理是建表最容易低估的地方。第一个坑:字段定义为 NOT NULL,但插入时没给值,报错。第二个坑:字段允许 NULL,但你的 WHERE 写成了 WHERE name <> 'xxx',NULL 的行会被过滤掉,因为 NULL 不参与常规比较运算。第三个坑:使用了唯一索引,但该字段允许 NULL,多条 NULL 记录不会被拦截。
我的经验是:能 NOT NULL 就 NOT NULL,确实可能为空的字段就允许 NULL,但查询时要有意识地处理 NULL。 比如统计时用 IFNULL(column, 0) 或 COALESCE(column, 0),查缺失数据用 IS NULL,条件过滤时用 WHERE column IS NOT NULL。很多人刚接触 SQL 时会在 NULL 上栽跟头,建表时提前想好"这个字段是不是真的需要有 NULL",能少写很多坑人的查询。
5.4 修改表结构会不会锁表
说了这么多 CREATE TABLE,实际上日常维护中不可避免会用到 ALTER TABLE。要考虑的是,在数据量大的表上执行 ALTER TABLE 可能长时间锁表,导致业务写入阻塞。MySQL 8.0 之前,很多 ALTER 操作是 COPY 或 INPLACE 算法,大表上执行需要非常谨慎。8.0 之后,部分操作支持 INSTANT 算法,比如直接追加字段到表尾。
所以,建表时把字段设计得完整一点,本质上是帮未来的自己减少 ALTER TABLE 的次数。如果确实需要变更,建议在业务低峰期操作,或使用 pt-online-schema-change 这类工具,在在线模式下完成表结构变更,减少锁表时间。
6. 建表时多做一步,查询性能省心一整年
6.1 索引设计前移
很多人是表建完了,跑了一段时间,发现某个 SELECT 特别慢,才用 EXPLAIN 看到全表扫描,然后回来补索引。补索引本身没错,但如果在建表时就把高频查询场景想清楚,很多慢查询根本不会出现。
判断要不要建索引,可以问自己几个问题。这个字段会不会出现在 WHERE 条件里?会不会被 ORDER BY 或 GROUP BY 用到?会不会作为 JOIN 的关联字段?如果答案是肯定的,就值得建索引。如果一个字段几乎只会被 SELECT 出来展示,而不会参与过滤,那建索引的意义就不大。索引毕竟占空间、拖慢写入,不是越多越好。
复合索引的字段顺序也要注意。KEY idx_user_created (user_id, created_at) 能同时支撑"按用户查订单"和"按用户查订单并按时间排序"两个场景;如果你建的是 KEY idx_created_user (created_at, user_id),那按用户查订单就没办法走索引了。这就是为什么建表时把最常见的查询模式想清楚,比事后补索引高效得多。
6.2 冗余字段和反范式设计的克制使用
在订单表里直接存 receiver_name、receiver_phone、receiver_address,这些信息其实可以从用户表或地址表关联获得,为什么还要冗余一份?因为订单是交易快照。用户改了收货地址后,历史订单依然需要显示当时的下单地址。这种情况下冗余是合理的、必要的。
问题在于:不是所有冗余都必要。如果两张表 A 和 B 都存在一个"用户名称"字段,而两者本来就通过 user_id 关联,那这个冗余字段如果不同步更新,就会产生数据不一致。建表时问自己:这张表有独立生命周期吗?这个字段在业务上是约束条件还是纯展示?如果是纯展示且变更频繁,优先通过关联查询获取,而不是直接复制一份。这也就是常说的"适度反范式",要反得有理有据。
6.3 COMMENT 写得好,等于给未来的自己留了地图
我在前文反复提到 COMMENT,这里再啰嗦一次。现在很多团队用 Navicat、DBeaver 这类图形工具管理数据库,鼠标悬停在字段上就能看到注释。设计良好的表结构,注释应该能回答这样几个问题:这个字段是干什么的?取值范围是什么?有没有特殊约定?比如 status TINYINT COMMENT '状态:0待支付,1已支付,2已退款',比裸一个 status 清晰一百倍。
如果你在建表时给每个字段都写清楚注释,将来无论是同事接手、出报表还是查数据,都能省大量沟通时间。这点投入放到整个项目周期里,收益非常可观。
7. 几个常见的陷阱,能避一个是一个
聊到这里,CREATE TABLE 的主干内容基本都覆盖到了。最后再集中说几个我在实际项目里反反复复看到的坑,希望你建表时不重蹈覆辙。
一是自增主键的类型选太小。建表时用 INT,业务量涨到 21 亿行左右就会溢出(INT UNSIGNED 上限 42 亿)。如果你判断一个表有长期增长的可能,直接用 BIGINT 更省心,省的以后 ALTER 一个大表。二是把电话号码存成 INT。11 位手机号超出 INT 上限,还会丢失前导零。三是字段名和业务含义对不上,比如 create_time 存的是更新时间。四是把 boolean 用 VARCHAR(100) 存,浪费空间且没有约束力。五是不设置默认值,导致每次插入都必须显式给值,漏了就报错。
如果你能把这些点在建表时全部规避掉,你的 SQL 功力已经比相当一部分开发者扎实了。CREATE TABLE 看起来是一段简单的 DDL,但它是一张表未来所有查询、统计、维护的基础。花十分钟把这张表设计好,后面省下的可能是几十个小时的头痛排查时间。
