MySQL字段类型选错导致凌晨两点被叫醒:一个INT(1)和TINYINT的血的教训

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 119 浏览 0 回复

上周凌晨两点,手机狂震,监控报警用户积分显示异常。爬起来一查,数据库里积分字段存的值全变成了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)。凌晨两点的教训,值。

评论0
回复 · 0
还没有回复
微信客服 微信客服