ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MySQL 建表时,INT、VARCHAR、DATETIME、DECIMAL 到底怎么选?

2026/9/28 21:23:45 拓冰建站 浏览量
MySQL 建表时,INT、VARCHAR、DATETIME、DECIMAL 到底怎么选? 0.1 0.2 ≠ 0.3。你在 MySQL 里用 FLOAT 存金额对账永远差这一分钱。这篇把建表时最常纠结的四种类型——整数 INT、文本 VARCHAR、时间 DATETIME、金额 DECIMAL——一次讲清看完直接照着建表。先把选型原则钉死所有类型选择背后就四条原则后面每一节都是它们的展开最小存储能放下业务范围的最小类型。年龄不用 BIGINT状态不用 INT。精准匹配是什么类型就用什么类型。数值用数值、文本用字符串、日期用日期别全塞 VARCHAR。精度优先金额、汇率必须定点数浮点绝对不行。兼容拓展自增主键预见到后期数据量直接 BIGINT别等溢出再改表。知道了这四条我们从一张订单表的第一个字段开始选。记住选型不是背类型表是对着业务字段一个个问这一列存什么。主键 IDINT 还是 BIGINTINT(11) 又是什么订单表第一个字段必然是order_id。选 INT 还是 BIGINTINT 是 32 位有符号整数固定 4 字节有符号范围 -21 亿 ~ 21 亿加UNSIGNED翻一倍到 0 ~ 42 亿。中小体量业务 42 亿够你写到公司倒闭但分布式、雪花 ID、海量流水直接上 BIGINT8 字节上限 9.2×10^18。业务里 99% 的整数字段都是非负的——用户 ID、订单数量、浏览量、年龄、排序号这种字段统一加UNSIGNED。一举两得容量翻倍负数脏数据根本写不进去。只有真会出现负数的场景差值、温度、盈亏统计才保留有符号。整数查询快不是玄学——固定 4 字节长度数据库比较、排序、走索引都是按定长算不用像 VARCHAR 那样先读长度前缀再定位。这也是能整型就别字符串的底层原因。这里有个几乎人人踩过的坑INT 后面的括号数字不是存储位数。要点INT(11)、INT(5)、INT(20)占用空间和取值范围完全一样——都是 4 字节。括号里的数字叫显示宽度Display Width只在配ZEROFILL时才生效用于左侧补零。前端本来就会管展示格式数据库这层零填充现在没人用。CREATETABLEt(aINT(5)ZEROFILL,bINT(11));INSERTINTOtVALUES(12,12);SELECTa,bFROMt;-- 预期输出: a00012, b12看到区别没a被补成 5 位b没有。但两个字段占的空间一模一样。⚠️坑我见过有人为了省空间把主键从 INT(11) 改成 INT(5)上线后发现查询结果多了前导零前端把00012当字符串拼了。这事改起来简单但暴露了他根本不知道显示宽度是干嘛的。整型家族怎么挑对照下表类型字节有符号范围无符号范围典型场景TINYINT1-128 ~ 1270 ~ 255状态、性别、评分SMALLINT2-3.2 万 ~ 3.2 万0 ~ 6.5 万年份、小计数INT4±21 亿0 ~ 42 亿用户 ID、订单 IDBIGINT8±9.2×10^180 ~ 1.8×10^19雪花 ID、海量主键主键选完状态字段别再用 INT。订单状态就 0/1/2 三个值用TINYINT UNSIGNED只占 1 字节比 INT 省 3 倍。大表上这个差距肉眼可见。顺便把状态字段的默认值也定了别用INT DEFAULT NULL。两个问题——INT 浪费空间NULL 会让WHERE status ! 1这种查询直接漏掉 NULL 行。统一TINYINT UNSIGNED NOT NULL DEFAULT 0业务里没有未赋值这一说。记住主键预见数据量状态用 TINYINTINT(M) 的 M 别当回事。手机号字段INT 能存为什么不能用下一个字段是phone。11 位数字看着像整数很多人第一反应是 INT。这是个生产事故级别的错误。手机号看着像数字用 INT 存会有三个问题前导 0 直接丢——手机号本身没有前导 0但卡号、身份证、工号有。0012345存进 INT 变成12345对不上。超范围溢出——身份证 18 位早就爆 INT 的 42 亿上限存进去就是错的。失去字符串语义——手机号不做加减乘除你要的是它长什么样不是值多大。⚠️坑我刚工作时见过一张用户表用 BIGINT 存身份证号数据迁移才发现有几个人的号开头是 0导出来全少一位。这种脏数据事后根本查不回来。所以短文本、编号、手机号、邮箱统一用VARCHAR。VARCHAR(M) 的 M 是字符数不是字节数——这是第二个新手坑。一个汉字在 utf8mb4 下最多占 4 字节但VARCHAR(50)就是按 50 个字符算不管你存的是汉字还是字母。长度按够用且最小挑昵称、用户名、手机号VARCHAR(32)或VARCHAR(50)地址、简介、标题VARCHAR(128)或VARCHAR(255)文章正文、商品详情别用 VARCHAR换 TEXT为什么因为 MySQL 单行有 65535 字节的硬上限所有列共享。你把一堆 VARCHAR(2000) 堆进去建表直接报错。长文本交给 TEXT别硬塞。还有个细节VARCHAR 的长度前缀占 1 字节还是 2 字节分界正好在 255。定义 ≤255 时长度前缀 1 字节255 时 2 字节——这也是为什么很多人习惯把 VARCHAR 卡到 255不是迷信是有底层原因的。但别为了这 1 字节硬把短文本设成 255按需选就好。另外密码字段也是 VARCHAR——存的是加密后的哈希串比如 bcrypt 输出 60 位VARCHAR(60)就够别去存明文。要点CHAR和VARCHAR的区别——CHAR 固定长度、尾补空格适合长度完全固定的字段比如定长编码VARCHAR 按需存储90% 场景用它。手机号虽然长度固定 11 位但前导 0 不参与运算这两点还是选 VARCHAR。VARCHAR 还有两个硬规矩字符集统一utf8mb4不然 emoji 存不进去字段默认NOT NULL DEFAULT 别让字符串字段为 NULL——NULL 会让索引效率下降WHERE name ! a这种条件还会漏行。新手最省事的写法是所有字段都用 VARCHAR——图省事其实是灾难。一旦金额列是 VARCHARORDER BY amount DESC就变成字典序排9排在10前面SUM(amount)直接报错或按字符串拼WHERE age 18也走不了数值比较。类型混用索引和计算全部失效。VARCHAR 建索引还有个细节字段太长时别整列建索引指定前缀长度。比如标题列VARCHAR(255)索引只取前 50 个字符INDEX title(title(50))。整列建索引会让索引文件暴涨B 树层级变深查询反而慢。记住编号类字段哪怕全是数字只要带前导 0、不运算、长度超 INT就用 VARCHAR。时间字段DATETIME 还是 TIMESTAMP订单表第三个关键字段是create_time。教科书里教过两个DATETIME 和 TIMESTAMP选哪个直接给结论新项目一律 DATETIME别碰 TIMESTAMP。先说版本前提MySQL 5.6 及以上把 DATETIME 改成了二进制压缩存储固定 5 字节更早版本是字符串存又慢又占地方。现在没人用 5.6 以前的版本了这条可以直接跳过但你要是接手老项目先SELECT version()看一眼。对比项DATETIMETIMESTAMP空间5 字节4 字节范围1000 ~ 9999 年1970 ~ 2038 年时区无关存绝对值依赖时区改时区就乱自动更新可控默认强制那省下来的 1 字节换一个 2038 年就溢出的炸弹值不值⚠️坑2038 问题不是吓唬人。TIMESTAMP 底层是 32 位时间戳到 2038-01-19 03:14:07 UTC 就翻转。现在写的代码几年后可能就在这个坑上炸。而且 TIMESTAMP 会随数据库时区自动转换——服务器从东八区迁到新加坡老数据看起来全变了。DATETIME 存的是绝对值不管服务器时区怎么改2026-09-27 22:30:00就是这个数。实际建表时create_time和update_time是标配create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT创建时间,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT更新时间create_time插入时自动填当前时间以后不改update_time在行被 UPDATE 时自动刷新。不用业务代码操心。精度上普通业务DATETIME(0)精确到秒就够秒杀、日志、高频排序场景上DATETIME(3)精确到毫秒。两个延伸只存日期不存时间比如生日用DATE只存时长比如视频时长用TIME别全用 DATETIME 浪费。❌错误用VARCHAR存时间字符串或者用INT存时间戳。前者没法用DATEDIFF、DATE_FORMAT这些函数范围索引也走不了后者人眼看不出是几点排查问题全靠转。还有一条容易被忽略的服务器时区、MySQL 时区、应用时区三层必须统一设成Asia/Shanghai。不然你在本地看到的时间和线上差 8 小时排查问题时先怀疑数据错了绕一大圈才发现是时区没对齐。记住新项目时间字段用 DATETIMETIMESTAMP 的 2038 炸弹和时区坑别去接。字段为什么 FLOAT 永远对不平账回到开头那个反常识——0.1 0.2 到底等多少在 MySQL 里实测一下SELECT0.10.2;-- 预期输出: 0.30000000000000004SELECTCAST(0.1ASDECIMAL(10,2))CAST(0.2ASDECIMAL(10,2));-- 预期输出: 0.30FLOAT 和 DOUBLE 是二进制浮点数底层用二进制近似存小数0.1 在二进制里是无限循环存进去就已经不是 0.1 了。这不是 bug是 IEEE 754 的设计。FLOAT 大约 6-7 位有效数字DOUBLE 大约 15-16 位——别被DOUBLE 更准骗了它只是误差更小累加十笔百笔之后照样飘。✅验证你把 0.1 存 FLOAT、0.2 存 FLOAT、加起来再 SUM十笔交易下来对账差几分钱是常态。财务找过来的时候你跟他解释这是二进制浮点——没用。DECIMAL 是十进制定点数按十进制字符串存0.1 就是 0.10.2 就是 0.2加起来严格 0.3。语法DECIMAL(M, D)M 是总位数D 是小数点后位数。常用配置订单金额、余额、商品价格DECIMAL(10,2)最大 99999999.99大额交易、企业账务DECIMAL(16,2)汇率、税率、利率DECIMAL(8,4)留 4 位小数⚠️坑别把 M 设得过大。DECIMAL(65,30)听着安全实际占的存储是按 M 算的每多一位都在浪费。DECIMAL 上限是 M65、D30但没人真用到那么大。按业务最大值选单价封顶 9999 元的业务DECIMAL(10,2)完全够。金额字段的默认值是0.00不是NULL也不是整数0——后者会让小数位丢精度。温度、身高、体重这种不要求精确的数用 DOUBLE 是对的但把它用在金额上就是事故。还有一层兜底数据库存 DECIMAL 不代表业务代码可以躺平。入库前用BigDecimal.setScale(2, RoundingMode.HALF_UP)截一下不然传进来一个0.123456数据库按 D2 静默截断成0.12业务层完全无感知对账单又差一分。记住资金字段一律 DECIMAL(10,2)FLOAT/DOUBLE 留给科学计算别碰钱。一张标准订单表长什么样把前面四个选择拼起来一张能直接上线的订单表大致是这样CREATETABLEorders(order_idBIGINTUNSIGNEDNOTNULLAUTO_INCREMENT,user_idBIGINTUNSIGNEDNOTNULL,statusTINYINTUNSIGNEDNOTNULLDEFAULT0,phoneVARCHAR(18)NOTNULLDEFAULT,amountDECIMAL(10,2)NOTNULLDEFAULT0.00,create_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,update_timeDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(order_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4;对照一下前面讲的每一条主键order_id用 BIGINT 预见未来状态status用 TINYINT 省空间手机号phone用 VARCHAR 容纳前导 0金额amount用 DECIMAL 保精度create_time/update_time双字段 DATETIME 自动维护全表NOT NULL 默认值没有 NULL 字段。这就是一套能跑的最小规范。结尾速查与常见坑业务字段选型对照业务字段推荐类型标准写法用户 ID、订单 IDBIGINTBIGINT UNSIGNED NOT NULL AUTO_INCREMENT状态、是否删除TINYINTTINYINT UNSIGNED NOT NULL DEFAULT 0昵称、地址VARCHARVARCHAR(50) NOT NULL DEFAULT 手机号、身份证VARCHARVARCHAR(18) NOT NULL DEFAULT 创建/更新时间DATETIMEDATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP订单金额DECIMALDECIMAL(10,2) NOT NULL DEFAULT 0.00汇率、税率DECIMALDECIMAL(8,4) NOT NULL DEFAULT 0.0000生日DATEDATE NOT NULL DEFAULT 1970-01-01常见坑清单按踩中频率排序全表用 VARCHAR——数值没法排序、没法运算、索引效率掉。金额用 FLOAT/DOUBLE——对账永远差几分钱。TIMESTAMP 用在新项目——2038 溢出 时区错乱。手机号/身份证用 INT——前导 0 丢失、超长溢出。纠结 INT(11) 还是 INT(5)——显示宽度而已别浪费时间。字段默认 NULL——WHERE ! x漏行、索引效率下降。VARCHAR 一律 255——短文本浪费空间长文本又截断。用字符串存时间——时间函数全废范围查询慢。下一步你可以做的找一张你最近建的表跑一句SHOW CREATE TABLE 表名\G把字段类型拉出来对照上面这张表逐字段过一遍。我赌至少有一个字段要么用大了、要么用错了类型。改完再跑一次EXPLAIN看看索引有没有变化。参考链接MySQL 官方文档 - 数据类型MySQL 官方文档 - Fixed-Point Types (Exact Value)MySQL 官方文档 - The DATE, DATETIME, and TIMESTAMP Types