MySQL数据类型应按需选择最小够用者:整数选TINYINT/INT/BIGINT UNSIGNED,金额必用DECIMAL,字符串据长度选CHAR/VARCHAR/TEXT,时间据时区需求选TIMESTAMP/DATETIME。

直接说结论:MySQL 数据类型不是越多越好,而是按需选最小够用的那个。选大了浪费空间、拖慢索引;选小了 runtime 报 Out of range value 或静默截断;用错类型(比如用 FLOAT 存金额)可能引发资损。
整数类型怎么选:TINYINT 到 BIGINT 的实际分界点
别看文档列了一堆类型,日常 90% 场景只用到三个:
-
TINYINT UNSIGNED:状态字段(如status)、布尔(0/1)、枚举值个数 ≤ 255 的场景。1 字节,存 0–255,别写TINYINT(1)—— 括号里的数字是显示宽度,跟存储无关,还容易误导人。 -
INT UNSIGNED:用户 ID、订单号、计数器等常规主键或正整数字段。4 字节,上限 42 亿,对大多数单库业务足够;如果用自增,注意INT溢出后会从 0 重开始(除非设了auto_increment_increment)。 -
BIGINT UNSIGNED:分布式 ID(Snowflake)、毫秒级时间戳、超大计数(如日活统计)、未来可能分库分表的主键。8 字节,代价是索引体积翻倍、JOIN 和排序更耗内存。
容易踩的坑:
- 用
BIGINT存年龄、状态、开关位 —— 浪费 7 字节/行,百万行就多占近 700MB; - 用有符号
INT存用户 ID 却插入负数 —— 不报错但语义错乱; - 以为
INT(11)比INT(5)能存更多数 —— 完全一样,都是 4 字节,(11)只在ZEROFILL下起作用(不推荐用)。
小数存钱为什么必须用 DECIMAL,而不是 FLOAT/DOUBLE
因为 FLOAT 和 DOUBLE 是 IEEE 754 浮点数,本质是近似存储。哪怕存 0.1 + 0.2,查出来也可能是 0.30000000000000004。
金融类字段(如 price、balance、discount)必须用 DECIMAL(M,D):
-
M是总位数(精度),D是小数位数;例如DECIMAL(10,2)表示最多 10 位数字,其中 2 位小数(范围 -99999999.99 ~ 99999999.99); - 它按字符串方式精确计算,加减乘除不会丢精度;
- 性能略低于
FLOAT,但对账、结算这类场景,精度优先级远高于那点 CPU 时间。
常见错误:
- 建表时写
price FLOAT(10,2)——FLOAT不支持括号里的(M,D)精度声明,MySQL 会忽略并按默认单精度存,结果不可控; - 用
DOUBLE存余额,做 sum() 后和对账系统差几分钱,排查半天才发现是浮点误差累积。
字符串类型:CHAR、VARCHAR、TEXT 怎么不踩内存和索引坑
核心判断逻辑就一条:长度是否固定且 ≤ 255 字符?
- 固定短文本(如国家码
CN、性别M/F、订单状态paid)→ 用CHAR(2)或CHAR(10)。InnoDB 会按定义长度分配空间,定长利于缓存对齐,查询更快。 - 长度明显可变、平均长度 VARCHAR(N)(N 设为预估最大长度)。它只存实际内容 + 1~2 字节长度头,省空间;但要注意:若 N > 5000,某些版本 MySQL 会把该列转成 TEXT 处理,影响索引和排序行为。
- 纯大文本(文章正文、日志、HTML 片段)→ 用
TEXT或MEDIUMTEXT。它们不参与行内存储,而是存指针+外部页,避免单行过大导致 buffer pool 压力;但不能直接建全文索引以外的索引(VARCHAR才能前缀索引)。
致命误区:
- 用
VARCHAR(255)存手机号 —— 实际只需VARCHAR(20)(含国际区号、+、- 等);255 是历史惯性,不是标准; - 给
TEXT字段加INDEX—— MySQL 会报错或静默转为前缀索引(如INDEX(content(255))),但搜全文时大概率失效; - 用
CHAR存昵称(最长 30,但多数 5–10 字)—— 白白浪费 20+ 字节/行,压缩率低,buffer pool 更快被挤满。
时间字段别再用 INT 存时间戳,也别混用 DATETIME 和 TIMESTAMP
Unix 时间戳(秒级或毫秒级整数)看似简单,但会丢掉时区语义、无法用 MySQL 内置时间函数(如 DATE_ADD、TIMESTAMPDIFF)直接处理,后期改需求极痛苦。
正确姿势:
- 记录「事件发生时间」且需跨时区转换(如用户下单时间)→ 用
TIMESTAMP。它存的是 UTC 时间,读写自动转当前 session 时区,适合日志、审计类场景;但注意:它的范围只有1970–2038(32 位限制),2038 年问题真实存在。 - 记录「固定时间点」且无需时区转换(如合同生效日、生日)→ 用
DATETIME。范围大(1000–9999),存的就是字面值,不随时区变;缺点是写入时不自动填充,得显式写NOW()或应用层传值。
高频错误:
- 用
INT存秒级时间戳 → 查询要FROM_UNIXTIME(created_at)才能看,聚合时还得反复转换,索引效率也差; - 把
DATETIME当TIMESTAMP用,结果上线后发现不同地区用户看到的时间不一致,还以为是前端 bug; - 建表时写
created_at DATETIME DEFAULT CURRENT_TIMESTAMP却忘了加ON UPDATE CURRENT_TIMESTAMP(如果需要自动更新)。
真正复杂的地方不在类型列表本身,而在于字段语义是否和类型行为对齐 —— 一个 TINYINT 可以是状态、可以是枚举序号、也可以是布尔,但一旦语义模糊,后期加新状态或迁移就容易崩。


















