MySQL字段类型选错导致凌晨两点被叫醒:一个INT(1)和TINYINT的血的教训
上周凌晨两点,手机狂震,监控报警用户积分显示异常。爬起来一查,数据库里积分字段存的值全变成了127。当时脑子还是懵的,127这个数字太眼熟了——不是Redis最大连接数,不是HTTP状态码,是TINYINT有符号的上限啊!
回溯代码,三个月前接了个签到积分的需求,当时图省事直接复制了旧表的字段定义:
【错误写法】
```sql
CREATE TABLE user_points (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
points INT(1) DEFAULT 0 COMMENT '用户积分', -- 坑在这里!
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
```
看到没?INT(1)。我当时脑子里想的是"积分最多也就几百,写个1省空间",结果完全搞混了显示宽度和存储范围。
MySQL的INT(1)和INT(11)在存储上是一模一样的,都是4字节、有符号范围-2147483648到2147483647。那个括号里的数字只是ZEROFILL时的显示宽度,跟存多少数据半毛钱关系没有。而我真正想要的小整数类型,应该是TINYINT或者SMALLINT。
更坑的是,这表是跑了一段时间后才发现问题。因为INT(1)能正常存大数字,前期测试完全没暴露。直到后来有个运营活动,用户连续签到7天给500积分,加上原有积分直接过127——但等等,INT不是能存几十亿吗?
问题出在ORM层。我们用的那个版本有个"智能"优化:检测到字段定义是INT(1),自动映射成PHP的tinyint类型处理,返回时强转了。数据库里明明存着500,PHP拿到手变成127,写回去又覆盖掉原值。这锅MySQL不背,但根子是我瞎写字段类型埋的雷。
【正确写法】
```sql
-- 如果积分范围确定在0-255(比如每日签到1分,上限255天)
points TINYINT UNSIGNED DEFAULT 0 COMMENT '用户积分',
-- 如果可能到几万(比如积分商城兑换)
points SMALLINT UNSIGNED DEFAULT 0 COMMENT '用户积分',
-- 如果可能到几百万且需要省空间(一般没必要)
points MEDIUMINT UNSIGNED DEFAULT 0 COMMENT '用户积分',
-- 默认无脑选,除非你有明确的存储压力
points INT UNSIGNED DEFAULT 0 COMMENT '用户积分'
```
修复那天我顺手整理了团队规范,核心就一条:不要用INT(n)的n来表达业务预期范围。需要限制前端显示位数,那是应用层的事;需要省存储,老老实实选对的类型。
附个速查表,我打印贴显示器边上了:
| 类型 | 有符号范围 | 无符号范围 | 字节 |
|------|-----------|-----------|------|
| TINYINT | -128~127 | 0~255 | 1 |
| SMALLINT | -32768~32767 | 0~65535 | 2 |
| MEDIUMINT | -8388608~8388607 | 0~16777215 | 3 |
| INT | -21亿~21亿 | 0~43亿 | 4 |
| BIGINT | 极大 | 极大 | 8 |
现在建表我必过一遍这个表,哪怕只是存个0/1的状态字段,也明确写TINYINT UNSIGNED而不是INT(1)。凌晨两点的教训,值。

