MySQL 数据类型核心指南:选型、实战与避坑
MySQL 表设计里有一个很容易被低估的问题:字段类型选错,短期通常不会报错。
VARCHAR 能存数字,BIGINT 能存绝大多数 ID,DOUBLE 也能存金额,时间甚至可以直接塞进字符串。业务照样能跑,所以很多问题会被拖到数据量上来以后才暴露。
真正的代价往往出现在后面:索引变大、比较发生隐式转换、金额出现精度误差、时间范围不够、JOIN 用不上索引,甚至只是因为一个毫无必要的 BIGINT,让整张表的二级索引持续膨胀。
所以选数据类型不能只问一句:
这个类型能不能存下当前的数据?
更应该问:
它是否准确表达业务含义,并且用合理的空间支持未来几年的数据规模和查询方式?
下面按 MySQL 8.x 的常见业务场景来拆。
一、选数据类型时,真正要看这 5 件事
建字段之前,建议按这个顺序考虑:
- 数据本身是什么:数字、金额、字符串、时间还是结构化数据;
- 最大值和最小值可能到哪里;
- 是否要求精确计算;
- 这个字段以后会不会建索引、排序、JOIN;
- 未来数据量增长以后,当前类型是否还够用。
比如订单金额:
price DOUBLE
看起来完全能存。
但金额需要精确计算,应该优先考虑:
price DECIMAL(12, 2)
再比如手机号:
mobile BIGINT
虽然中国大陆手机号看起来像数字,但手机号实际上不是用来计算的数字,它是一个标识符。
更合理的是:
mobile VARCHAR(20)
以后碰到 +86、海外号码、前导 0,都不需要修改表结构。
这就是数据类型设计里最重要的一条判断标准:
按业务语义选择类型,而不是按数据长得像什么来选择。
二、整数类型:不要看到数字就上 BIGINT
MySQL 常见整数类型如下:
| 类型 | 字节数 | SIGNED 范围 | UNSIGNED 最大值 |
|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 255 |
| SMALLINT | 2 | -32768 ~ 32767 | 65535 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 16777215 |
| INT | 4 | 约 -21.47 亿 ~ 21.47 亿 | 约 42.94 亿 |
| BIGINT | 8 | 约 ±922 亿亿 | 约 1844 亿亿 |
实际开发里最常用的是:
TINYINT
INT
BIGINT
状态字段优先考虑 TINYINT
例如订单状态:
0 待支付
1 已支付
2 已发货
3 已完成
4 已取消
没必要写成:
status INT NOT NULL
更合理的是:
status TINYINT UNSIGNED NOT NULL
整个业务只有几个状态,1 个字节已经绰绰有余。
类似的字段包括:
用户状态
审核状态
支付状态
删除标记
渠道类型
性别编码
业务类型
都可以优先考虑 TINYINT 或 SMALLINT。
主键到底用 INT 还是 BIGINT?
很多项目建表时已经形成肌肉记忆:
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT
这当然省心,但并不是免费的。
INT 占 4 字节,BIGINT 占 8 字节。
单看主键多 4 字节似乎没什么,但 InnoDB 的二级索引叶子节点会保存主键值。
假设一张表有:
1 亿行数据
5 个二级索引
那么主键从 4 字节变成 8 字节,增加的并不只是主键索引本身。
二级索引也会跟着变胖。
这才是 BIGINT 真正的成本。
如果确定一张普通业务表生命周期内只有几百万甚至几千万条记录:
INT UNSIGNED
已经可以支持大约 42.9 亿个正整数。
但订单流水、日志、行为事件、消息记录这类持续高速增长的数据,直接使用:
BIGINT
通常更省后续迁移成本。
不要机械追求“小类型”,也不要无脑全部 BIGINT。
先估数据规模。
三、SIGNED 和 UNSIGNED:省范围没问题,乱用才麻烦
比如用户 ID 不可能出现负数,于是很多表设计成:
user_id BIGINT UNSIGNED
从范围利用率来看没问题。
真正容易踩坑的是不同表定义不一致。
用户表:
id BIGINT UNSIGNED
订单表:
user_id BIGINT
一个是 UNSIGNED,一个是 SIGNED。
这种差异会给关联、外键定义、ORM 映射和数据迁移制造额外麻烦。
ID 类型最好从项目一开始就统一。
例如统一规定:
所有用户 ID:BIGINT UNSIGNED
所有订单 ID:BIGINT UNSIGNED
状态码:TINYINT UNSIGNED
普通计数器:INT UNSIGNED
比单独讨论某一个字段应该用不用 UNSIGNED 更重要。
四、BOOLEAN 并不是真正独立的布尔类型
MySQL 里:
BOOLEAN
实际上是:
TINYINT(1)
的同义词。
所以:
is_deleted BOOLEAN NOT NULL DEFAULT FALSE
本质上仍然是整数。
很多项目更愿意明确写:
is_deleted TINYINT UNSIGNED NOT NULL DEFAULT 0
约定:
0 = 否
1 = 是
这样数据库层面的含义反而更加直观。
另外不要把:
TINYINT(1)
里面的 1 理解成“只能保存一位数字”。
它不是范围限制。
TINYINT 能存什么值,由类型本身以及是否 UNSIGNED 决定。
五、金额字段:DECIMAL 和 DOUBLE 根本不是一回事
这是业务系统里最值得单独拿出来讲的类型问题。
FLOAT / DOUBLE 是近似值
浮点数使用二进制表示小数。
某些十进制小数不能被二进制浮点数精确表示,所以在连续计算以后可能出现类似:
99.9999999997
100.0000000003
科学计算、坐标、比例、测量值等允许误差的业务可以使用:
FLOAT
DOUBLE
但金额通常不应该。
例如:
amount DECIMAL(12, 2)
12 表示总有效数字位数,2 表示小数位数。
因此可以保存:
9999999999.99
常见金额字段
例如:
price DECIMAL(12, 2)
amount DECIMAL(12, 2)
balance DECIMAL(14, 2)
金额范围特别大的系统,可以根据实际业务扩大:
DECIMAL(18, 2)
如果涉及汇率、佣金率或者数字资产,可能需要更高的小数精度:
exchange_rate DECIMAL(18, 8)
甚至:
amount DECIMAL(36, 18)
关键不是统一用 DECIMAL(18,2),而是先确定:
整数最多多少位?
小数到底需要几位?
再确定 M 和 D。
金额存“分”也是一种方案
还有一种常见设计:
amount BIGINT
数据库里保存单位为“分”的整数。
比如:
¥12.34
保存:
1234
这种方案也可以完全避免浮点误差,而且整数计算非常直接。
但它有一个额外成本:所有调用方必须永远记住单位。
一旦有系统把“分”当成“元”,问题会非常严重。
所以普通电商、支付、财务业务里,使用:
DECIMAL
通常可读性更高。
六、CHAR、VARCHAR、TEXT,到底怎么选?
字符串是 MySQL 建表时最容易被滥用的一类类型。
最经典的设计大概是:
name VARCHAR(255)
mobile VARCHAR(255)
email VARCHAR(255)
order_no VARCHAR(255)
status VARCHAR(255)
能跑。
但这基本等于没有认真设计字段。
CHAR:长度基本固定
适合长度固定或者高度稳定的数据。
例如某些固定编码:
country_code CHAR(2)
如果业务中确实存在固定长度编号,也可以使用 CHAR。
但不要因为身份证号“现在是固定长度”,就机械地认为所有证件字段都应该使用 CHAR。如果系统未来需要支持护照、港澳证件或者其他国家证件,业务语义已经变了。
VARCHAR:绝大多数普通字符串字段的主力
例如:
username VARCHAR(64)
nickname VARCHAR(64)
mobile VARCHAR(20)
email VARCHAR(128)
order_no VARCHAR(32)
title VARCHAR(200)
这里尤其容易出现一个误解:
VARCHAR(100)
这里的 100 对非二进制字符串来说主要表示最多保存多少个字符,不是简单理解成 100 个字节。
使用 utf8mb4 时,一个字符最多可能需要多个字节。
因此:
VARCHAR(1000)
理论占用空间和:
VARCHAR(50)
显然不是一个级别。
这会进一步影响:
行长度
索引长度
内存排序
临时表
网络传输
所以不要习惯性全部写 VARCHAR(255)。
字段长度本身就是数据约束的一部分。
七、TEXT 不是“大号 VARCHAR”
文本比较长时可以考虑:
TEXT
MEDIUMTEXT
LONGTEXT
常见容量等级大致是:
TEXT 约 64 KB
MEDIUMTEXT 约 16 MB
LONGTEXT 约 4 GB
比如:
文章正文
商品详情
小说章节
Markdown 内容
长备注
富文本 HTML
适合使用 TEXT 系列。
例如:
content MEDIUMTEXT NOT NULL
但不要看到文本就直接 TEXT。
如果字段只是:
昵称
标题
订单备注
标签名
地址
长度明显可控,优先使用 VARCHAR。
原因不只是存储空间。
很多业务后面还会:
建立索引
排序
去重
分组
进行条件查询
可控长度的 VARCHAR 更容易处理。
八、不要把“数字字符串”设计成数字
下面这些数据看起来都是数字:
手机号
身份证号
银行卡号
邮政编码
商品编码
快递单号
企业编码
但它们并不是数学意义上的数字。
判断方法非常简单:
这个字段以后会不会进行加减乘除?
如果不会,大概率应该把它看成标识符。
例如邮政编码:
00100
如果存成:
INT
最终可能变成:
100
前导 0 丢了。
手机号同理。
所以:
mobile VARCHAR(20)
通常比:
mobile BIGINT
更合理。
九、DATETIME 和 TIMESTAMP,别只看名字
常见时间类型主要有:
DATE
DATETIME
TIMESTAMP
TIME
普通业务最常用的是前三种。
DATE
只需要日期:
2026-08-25
使用:
birthday DATE
不要为了统一方便,把生日写成:
DATETIME
然后永远保存:
2026-08-25 00:00:00
没有必要。
DATETIME
保存完整日期和时间:
2026-08-25 14:30:25
可以:
created_at DATETIME
如果需要毫秒:
created_at DATETIME(3)
例如:
2026-08-25 14:30:25.123
TIMESTAMP 最大的区别是时区转换
TIMESTAMP 与 MySQL 会话的 time_zone 有关系。
写入和读取过程中会进行 UTC 与当前会话时区之间的转换。
DATETIME 更像是把:
2026-08-25 14:30:25
这组日期时间值直接保存下来。
因此对于:
预约时间
会议时间
营业时间
计划执行时间
这类带有明确业务时间语义的数据,使用 DATETIME 往往更容易控制。
如果整个系统明确统一使用 UTC,并理解 MySQL 的时区行为,TIMESTAMP 也很好用。
问题不在于哪个绝对更好。
问题在于很多项目用了 TIMESTAMP,却根本没有统一服务器、数据库连接和应用层的时区配置。
这种情况下,时区 bug 通常比节省几个字节难查得多。
另外,传统 TIMESTAMP 还有著名的 2038 年时间范围问题。如果系统需要存储很远的未来日期,不要默认使用它。
十、时间不要轻易存 VARCHAR
这种设计最好避免:
created_at VARCHAR(32)
然后保存:
2026-08-25 12:30:22
因为接下来所有时间操作都会变麻烦:
WHERE created_at >= ?
时间函数:
DATE(created_at)
日期计算:
DATE_ADD(...)
月份统计:
YEAR(created_at)
MONTH(created_at)
本来数据库原生能处理的问题,现在全部变成字符串处理。
除非原始业务数据就是一个不规则时间字符串,否则数据库中的标准业务时间应该优先使用原生时间类型。
十一、时间戳要不要直接存 BIGINT?
有些系统喜欢保存 Unix Timestamp:
created_at BIGINT
例如:
1787630400000
优点是跨语言传输简单。
但数据库可读性会明显下降。
本来:
SELECT created_at
FROM orders;
直接可以看到:
2026-08-25 12:00:00
现在看到的是:
1787630400000
SQL 查询、运维排查、报表统计也都需要额外转换。
如果没有非常明确的架构理由,普通关系型业务表直接使用:
DATETIME
通常更舒服。
十二、ENUM:看起来优雅,业务变动以后未必优雅
例如:
status ENUM(
'PENDING',
'PAID',
'SHIPPED',
'FINISHED'
)
代码看起来很直观。
对于长期固定、几乎不发生变化的枚举集合,ENUM 并不是不能用。
但业务状态经常变化时,我更倾向:
status TINYINT UNSIGNED NOT NULL
应用代码维护:
0 = PENDING
1 = PAID
2 = SHIPPED
3 = FINISHED
4 = CANCELLED
原因很简单。
业务增加状态是应用发布里非常常见的一件事,不应该动不动就把它升级成数据库表结构变更。
如果希望数据库层面也具有可读性,可以再使用状态字典表,但不要为了“数据库里看起来舒服”牺牲业务迭代能力。
十三、JSON 很好用,但别拿它逃避表设计
MySQL 的 JSON 类型非常适合存储扩展属性。
例如不同订单来源存在不同的附加参数:
{
"campaignId": 10086,
"device": "ios",
"channel": "douyin"
}
可以定义:
extra JSON
这比建:
extra1
extra2
extra3
extra4
干净得多。
但 JSON 有一个非常明显的边界。
假设后来业务天天查询:
channel = douyin
还需要:
按 channel 分组
按 channel 建索引
按 channel 做 JOIN
那么 channel 已经不是“扩展属性”了。
它已经升级成核心业务字段。
应该考虑直接拆出来:
channel VARCHAR(32)
JSON 最适合:
字段稀疏
结构变化快
很少作为核心过滤条件
不参与主要 JOIN
如果把用户 ID、状态、金额、创建时间全部塞进 JSON,确实少写了几个字段。
代价是把关系型数据库最擅长的部分一起扔掉了。
十四、IP 地址不要再用 VARCHAR(15)
IPv4:
192.168.1.1
最长 15 个字符,所以老系统经常:
ip VARCHAR(15)
然后 IPv6 出现:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
直接存不下。
简单方案可以:
ip VARCHAR(45)
如果 IP 查询、存储量比较大,更推荐:
ip VARBINARY(16)
MySQL 提供了:
INET6_ATON()
INET6_NTOA()
例如:
INSERT INTO access_log(ip)
VALUES (INET6_ATON('192.168.1.1'));
查询:
SELECT INET6_NTOA(ip)
FROM access_log;
IPv4 和 IPv6 都可以统一处理,而且二进制存储更加紧凑。
十五、UUID 用 CHAR(36) 能跑,但代价不小
标准 UUID:
550e8400-e29b-41d4-a716-446655440000
直接存:
CHAR(36)
当然最直观。
但如果一张大表用 UUID 做大量索引,36 字节的字符串并不便宜。
可以转换成:
BINARY(16)
将 128 bit UUID 用 16 字节保存。
但还有一个更重要的问题:
随机 UUID 并不适合直接作为高写入量 InnoDB 表的聚簇主键。
InnoDB 数据按照主键组织。
大量完全随机的主键插入会让写入位置分散,更容易带来页分裂、缓存局部性下降等问题。
所以在高写入业务里,比“UUID 用 CHAR 还是 BINARY”更应该先考虑:
为什么要让随机 UUID 当聚簇主键?
一种常见方案是:
内部主键:BIGINT
外部业务 ID:UUID / 雪花 ID / 其他全局 ID
内部存储效率和外部全局唯一性分开处理。
十六、VARCHAR 长度越大,真的就只是“上限大一点”吗?
不是。
很多人会觉得:
VARCHAR(50)
和:
VARCHAR(5000)
只要实际上都只存 10 个字符,占用空间就差不多,所以干脆都定义得大一点。
这个思路只看到了磁盘实际字符串长度。
没有看到数据库其他执行过程。
过大的字段声明可能影响:
索引设计
排序
内存估算
临时表
行大小限制
ORM 映射
数据质量约束
而且长度限制本身就是一种非常有价值的数据校验。
用户名如果业务最多允许 32 个字符,就应该接近业务限制:
username VARCHAR(32)
而不是:
username VARCHAR(5000)
然后指望永远没有脏数据进来。
十七、字符集和排序规则也属于“类型设计”
下面两个字段虽然都是:
VARCHAR(32)
行为却不一定完全相同:
字符集不同
Collation 不同
会影响:
一个字符最多占多少字节
字符串比较
大小写敏感
排序方式
唯一索引判断
现在新项目通常优先使用:
utf8mb4
而不是老的 MySQL:
utf8
因为 MySQL 传统的 utf8 实际最多使用 3 字节,并不能覆盖完整 Unicode。
例如部分 Emoji 就无法正常保存。
因此如果业务存在:
用户昵称
评论
聊天
文章
社交内容
Emoji
优先:
utf8mb4
基本已经是默认选择。
十八、隐式类型转换,是选错字段类型之后最隐蔽的坑
假设表里:
mobile VARCHAR(20)
并且建立了索引:
KEY idx_mobile (mobile)
正确查询:
SELECT *
FROM user
WHERE mobile = '13800138000';
如果代码错误地把手机号当数字:
SELECT *
FROM user
WHERE mobile = 13800138000;
此时数据库可能需要进行类型转换。
字段本身参与转换后,原本期待的索引访问路径就可能发生变化。
类似问题经常出现在:
VARCHAR 和 BIGINT 比较
SIGNED 和 UNSIGNED 混用
不同字符集字段 JOIN
日期字段与字符串比较
所以字段类型统一并不只是“代码规范”。
它直接关系到 SQL 执行路径。
例如两张表通过:
user_id
关联,就应该尽量保证双方定义完全一致:
BIGINT UNSIGNED
不要一个:
BIGINT
另一个:
VARCHAR(32)
然后期待数据库替应用层收拾残局。
十九、一张订单表应该怎么设计?
把前面的原则放进一个实际表结构里。
下面以 MySQL 8.x 的订单表为例:
CREATE TABLE `orders` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '内部主键',
`order_no` VARCHAR(32) CHARACTER SET ascii COLLATE ascii_bin
NOT NULL COMMENT '订单号',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`product_name` VARCHAR(128)
NOT NULL COMMENT '商品名称',
`amount` DECIMAL(12, 2)
NOT NULL COMMENT '订单金额',
`status` TINYINT UNSIGNED
NOT NULL DEFAULT 0
COMMENT '状态:0待支付 1已支付 2已发货 3已完成 4已取消',
`source_ip` VARBINARY(16)
DEFAULT NULL COMMENT '来源IP',
`remark` VARCHAR(500)
DEFAULT NULL COMMENT '订单备注',
`extra` JSON
DEFAULT NULL COMMENT '扩展信息',
`paid_at` DATETIME(3)
DEFAULT NULL COMMENT '支付时间',
`created_at` DATETIME(3)
NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
COMMENT '创建时间',
`updated_at` DATETIME(3)
NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3)
COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_created_at` (`user_id`, `created_at`),
KEY `idx_status_created_at` (`status`, `created_at`),
CONSTRAINT `chk_orders_amount`
CHECK (`amount` >= 0)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COMMENT='订单表';
这里每一个类型都有明确理由。
id:BIGINT UNSIGNED
订单属于持续高速增长数据,而且通常会长期保留。
这里使用:
BIGINT UNSIGNED
是用更大的索引成本换取更长的数据生命周期。
如果只是几百万条数据的小型配置表,就没有必要照抄。
order_no:VARCHAR(32) + ASCII
订单号通常只包含:
数字
大小写英文字母
特殊分隔符
不需要 utf8mb4。
所以可以单独指定:
CHARACTER SET ascii
并且订单号一般要求精确匹配:
COLLATE ascii_bin
避免某些不必要的大小写比较语义。
amount:DECIMAL
订单金额要求精确:
DECIMAL(12, 2)
不用 DOUBLE。
status:TINYINT
只有几个状态:
TINYINT UNSIGNED
已经足够。
source_ip:VARBINARY(16)
同时兼容:
IPv4
IPv6
比直接给一个很长的字符串更紧凑。
extra:JSON
适合保存不同渠道产生的非核心扩展字段。
但如果以后某个 JSON 属性变成高频查询条件,就应该考虑拆成普通列。
created_at:DATETIME(3)
保留毫秒精度:
2026-08-25 15:32:16.328
对订单、支付、日志等系统通常已经足够。
二十、索引越多,类型选择的成本越明显
假设订单表只有一个主键:
BIGINT
你可能觉得比 INT 多 4 字节,没什么。
但订单表通常还有:
KEY idx_user_created_at (user_id, created_at)
KEY idx_status_created_at (status, created_at)
KEY idx_shop_created_at (shop_id, created_at)
KEY idx_pay_created_at (pay_type, created_at)
InnoDB 二级索引叶子节点会保存对应记录的主键。
于是主键宽度会不断复制到二级索引中。
同样的问题也存在于:
VARCHAR 主键
UUID 主键
超长联合索引
所以建索引的时候,不应该只看:
WHERE 后面有没有这个字段
还应该看:
字段到底有多宽?
主键到底有多宽?
整个索引一条记录需要多少字节?
数据达到亿级以后,几个字节的差别就不再是“几个字节”。
二十一、几个很典型的错误设计
错误 1:所有数字都 BIGINT
status BIGINT
age BIGINT
gender BIGINT
type BIGINT
不是不能运行,只是没有意义。
更合理的可能是:
status TINYINT UNSIGNED
age TINYINT UNSIGNED
gender TINYINT UNSIGNED
type SMALLINT UNSIGNED
数据库表越大,这种差距越明显。
错误 2:所有字符串 VARCHAR(255)
mobile VARCHAR(255)
username VARCHAR(255)
status VARCHAR(255)
order_no VARCHAR(255)
这通常不是“预留扩展空间”,而是没有做数据建模。
错误 3:金额使用 DOUBLE
amount DOUBLE
价格、余额、账单、财务金额优先:
DECIMAL
不要拿浮点误差考验对账系统。
错误 4:手机号使用 BIGINT
mobile BIGINT
手机号是标识符:
mobile VARCHAR(20)
错误 5:时间全部 VARCHAR
created_at VARCHAR(32)
会把本来数据库原生支持的:
范围查询
日期计算
排序
时间函数
全部搞复杂。
错误 6:所有扩展字段全部 JSON
一开始确实开发快。
半年以后:
JSON 查询
JSON 聚合
JSON 排序
JSON 条件组合
开始铺满业务 SQL。
到了这个阶段,这些数据往往早就应该变成普通字段了。
错误 7:为了“以后可能用到”,字段无限放大
title VARCHAR(5000)
实际上标题最多:
100 个字符
那就应该根据业务限制设计。
“可能以后用到”是数据库设计里非常危险的一句话。
真到了业务边界发生数量级变化的时候,通常要改的也不会只有这一个字段。
二十二、建表时可以直接套用的选型思路
普通业务系统可以从下面这套规则开始。
| 业务数据 | 常见选择 |
|---|---|
| 是否、开关、少量状态 | TINYINT |
| 普通整数 | INT |
| 高增长主键、流水 ID | BIGINT |
| 金额 | DECIMAL |
| 比例、科学计算 | FLOAT / DOUBLE |
| 手机号 | VARCHAR |
| 邮箱 | VARCHAR |
| 用户名、昵称 | VARCHAR |
| 固定长度代码 | CHAR |
| 普通标题、备注 | VARCHAR |
| 长文章、富文本 | TEXT / MEDIUMTEXT |
| 生日 | DATE |
| 普通业务时间 | DATETIME |
| 需要时区转换的时间 | TIMESTAMP |
| IPv4 + IPv6 | VARBINARY(16) |
| UUID | BINARY(16) 或 CHAR(36) |
| 稀疏扩展属性 | JSON |
| 高频查询属性 | 普通独立字段 |
这张表只能作为起点,不能代替业务分析。
比如:
BIGINT 是否合适
VARCHAR 到底多长
DECIMAL 保留多少位
DATETIME 是否需要毫秒
JSON 是否该拆列
最后都应该由真实的数据规模和查询方式决定。
二十三、真正值得记住的不是类型表,而是判断方式
MySQL 数据类型并不难记。
难的是在建表时愿意多想一步。
看到:
100
不要马上想到 INT,先判断它是不是编号。
看到:
12.99
不要马上想到 DOUBLE,先判断它是不是金额。
看到:
2026-08-25 15:30:00
不要只问 DATETIME 和 TIMESTAMP 哪个更流行,先把时区语义搞清楚。
看到 ID,也不要无条件:
BIGINT
先估一下这张表到底会有多少数据,以及它会有多少个二级索引。
好的字段类型通常有三个特征:
语义准确、范围够用、存储不过度。
如果还要加第四个,就是:
让未来写 SQL 的人少骂两句。
这比背下所有 MySQL 数据类型的最大长度有用得多。