你有没有遇到过这种情况:接手一个跑了半年的系统,发现“用户年龄”这一列有的人存了整数,有的人存了“25岁”,还有人存了 NULL;某个订单金额明明很接近,算起总账却差出几分钱;一个理论上最多几千行的配置表,查询慢得像在爬山。这些毛病十有八九不是业务代码写错了,而是建表时数据类型和表约束没规划好。MySQL 作为一种关系型数据库,它的能力边界很大程度从字段类型和约束就开始决定了,但我见过太多项目把类型当摆设、把约束当注释,最后数据质量崩塌、改造成本翻倍。
这篇文章我不想写成一个 API 手册式的翻页文档,而是想用做项目时会真正遇到的角度,把 MySQL 的数值类型、字符串类型、日期时间类型,再加上主键、外键、唯一约束这些“表约束”逐个拆开讲。适合刚学完基础 SQL、准备自己设计表结构的人,也适合已经写了一段时间业务、却总被线上数据问题折磨的开发。你会看到很多我在真实业务里的取舍,包括那些踩过之后才明白的“为什么”。
1. 为什么说数据类型选错比业务逻辑写错更麻烦
1.1 数据类型的本质不是“字段格式”,而是底层存储协议
不少初学者会把字段类型理解成一种“格式”,觉得 varchar(20) 就是“最多放 20 个字的文本格式”,int 就是“数字格式”。这个理解方向没有错,但没有触及关键。
在 MySQL 里,一个表最终会落到磁盘的存储引擎上。以最常用的 InnoDB 为例,数据是按行记录方式存储,每个字段是按类型编码写入页文件的。也就是说,类型决定的不只是“能不能填”,而是“这一列在磁盘上占几个字节、按什么规则比较大小、建索引时如何排序、查询引擎能不能直接走索引”。我把类型比作物流仓库里的货位规划:货位设计得合理,叉车来回次数少,仓库吞吐量高;货位设计得随意,货物越多,场内越乱,出库效率越低。
MySQL 官方文档里常见类型可以粗略分几组:整数类型(TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT)、浮点与定点数类型(FLOAT、DOUBLE、DECIMAL)、字符串类型(CHAR、VARCHAR、TEXT 系列)、二进制类型(BINARY、VARBINARY、BLOB 系列)、日期时间类型(DATE、TIME、DATETIME、TIMESTAMP、YEAR)、以及 JSON、ENUM、SET 这类特殊类型。这些类型在存储空间、取值范围、比较规则上完全不同。
值得强调的一点是:一旦业务上线,数据源源不断写入,再想修改字段类型通常意味着大表 DDL,甚至要重建表。MySQL 8.0 虽支持了 INPLACE 的部分改列操作,但依然存在锁表窗口和回滚成本。所以建表时花几分钟想清楚每一列到底是什么,远比后面加班补数据要划算。
1.2 字段设计混乱带来的连锁反应
我把实际项目中因为类型选错导致的麻烦列一个典型的链路,你感受一下:
- 手机号字段如果用了 BIGINT,那么以 0 开头的手机号会被截断;
- 如果手机号字段用了 varchar(11),却有不少人存了 NULL,那查询“WHERE phone IS NULL”时,业务代码常会忘记单独判断;
- 身份标识字段如果不区分状态类型,后续统计时不得不用一堆模糊 LIKE 去匹配;
- 金额字段如果用 FLOAT,则累加运算会出现精度漂移,导致对账不平;
- 一个本该用 TINYINT 表示的状态位,却设计成 VARCHAR(255),索引体积膨胀好几倍,扫描行数多出几个量级。
这些问题的可怕之处在于,它们大多不会在建表当天爆发,而是等到数据量上来、报表开始统计、业务系统开始对接时,才集中暴露。那时你已经无法通过“修改应用代码”解决,只能做数据订正或拆表迁移。做过一次你就会明白,代价小则几个小时,大则跨周。
所以我把类型与约束的设计称作“数据库的架构决策”,它不是写 CRUD 时顺手加个字段那么简单的事。
1.3 一张表看清“错误设计”与“正确设计”的差距
下面是一个“用户账号表”的简化例子,左边是我经常在接手项目里看到的反面设计,右边是更合理的方案:
| 字段语义 | 容易出现的错误设计 | 问题 | 更合理的设计 |
|---|---|---|---|
| 用户 ID | VARCHAR(100) 允许 NULL | 无法高效自增,索引浪费 | BIGINT UNSIGNED AUTO_INCREMENT |
| 用户状态 | VARCHAR(20) 存“正常/禁用/待审核” | 中英文混杂,难维护 | TINYINT 或 ENUM,配合注释 |
| 手机号 | BIGINT | 前导 0 丢失 | CHAR(11) 或 VARCHAR(20) |
| 注册时间 | VARCHAR(19) | 无法直接比较大小,不能走索引范围 | DATETIME |
| 积分余额 | DOUBLE | 金额精度丢失 | DECIMAL(10,2) |
| 性别 | VARCHAR(10),有写“男”也有写“male” | 数据不一致 | TINYINT + CHECK,或 ENUM |
| 登录 IP | VARCHAR(45) | IPv6 放不下还需要改 | VARCHAR(45) 或 VARBINARY(16) |
上表对应的每一个改动,都意味着业务代码中要同步处理。字段类型不是文档里写一次就结束的“注释”,它是所有 SQL 语句、ORM 实体、接口返回结构的数据地基。地基打偏了,楼层越高风险越大。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数值类型实战:从 TINYINT 到 DECIMAL 的取舍逻辑
2.1 整数类型:先把 int(5) 这个误区解决掉
很多人在建表时写过这样的语句:
sql复制CREATE TABLE user (
id INT(5) NOT NULL AUTO_INCREMENT,
...
);
这里最常见的误解是:INT(5) 表示“最多只能存 5 位数”。这个说法是错的。
INT 类型在 MySQL 中固定占用 4 个字节,无论括号里写 5 还是 11,它能存储的范围都是 -2147483648 到 2147483647(无符号为 0 到 4294967295)。括号里的数字是“显示宽度”,它只在设置了 ZEROFILL 属性时会影响展示方式,例如 INT(5) ZEROFILL 会把 42 显示为 00042,而不会限制你存入 123456。在 MySQL 8.0 里,显示宽度已被标记为废弃,将来可能会移除。所以在实际项目里我建议你干脆不写这个括号参数,别给自己和别人留误解空间。
各整数类型真正的差异在于存储范围和字节数:
| 类型 | 占用字节 | 有符号范围 | 无符号范围 | 典型用途 |
|---|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 0 ~ 255 | 状态码、年龄、枚举值 |
| SMALLINT | 2 | -32768 ~ 32767 | 0 ~ 65535 | 小范围计数器 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 0 ~ 16777215 | 中等计数,如某些统计值 |
| INT | 4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 | 最常见的常规编号 |
| BIGINT | 8 |
