YAOTU INSIGHTS

MySQL数值类型详解:选型、精度与索引性能避坑指南

MySQL数值类型详解:选型、精度与索引性能避坑指南
做数据库开发的朋友应该都有这种体会MySQL里的数据类型看着简单真正踩坑往往都在数值类型上。今天不聊安装也不聊配置专门把数值类型从取值范围、字节大小、精度差异到索引性能摊开来一次性讲清楚。这篇文章适合两类人看一是刚接触MySQL、正要建表的新手能少走弯路二是写过不少SQL、但没系统梳理过数值类型的老手可以借机查漏补缺。1. 先看清数值类型的全貌再建表1.1 数值类型为什么这么重要很多初学者建表时不重视数据类型随手把所有的“数字”都定义成INT所有的“金额”都定义成DOUBLE结果上线几个月后就会出现各种灵异现象状态值明明是0和1查出来却变成了一串乱码金额加在一起差几分钱自增主键突然报错关联查询越来越慢。这些问题的根源几乎都能追溯到建表时对数值类型的选择上。MySQL的数值类型决定了三件事数据怎么存储、能存多大的数、参与计算时的行为。存储大小直接影响磁盘占用和内存缓存效率取值范围决定数据边界计算行为决定精度和溢出表现。可以说一张表一半的“性格”都是由数值类型塑造的。1.2 四大阵营一次分清MySQL的数值类型可以分成四类整数类型、浮点类型、定点类型和位类型。整数类型包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT浮点类型包括FLOAT和DOUBLE定点类型是DECIMALNUMERIC是它的别名位类型是BIT。类型存储字节有符号范围无符号范围常见用途TINYINT1-128 ~ 1270 ~ 255状态码、年龄、开关值SMALLINT2-32768 ~ 327670 ~ 65535小型计数、端口号MEDIUMINT3-8388608 ~ 83886070 ~ 16777215中等计数使用较少INT4-2147483648 ~ 21474836470 ~ 4294967295常规整数主键、编号BIGINT8-2^63 ~ 2^63-10 ~ 2^64-1高并发主键、雪花IDFLOAT4约 ±3.40282e38同上科学计算、精度要求不高的浮点DOUBLE8约 ±1.79769e308同上通用浮点精度约15位有效数字DECIMAL动态最大65位总长度最大65位总长度金额、精确小数注意FLOAT和DOUBLE的“无符号”其实不常见它们允许负数和正数存储范围一样只是精度不同。DECIMAL的存储字节不固定取决于定义的精度和小数位所以表格里用“动态”标注。1.3 选型逻辑范围第一存储第二性能第三我建表时有一个固定的思考顺序先确定这个字段的数据范围再考虑用哪种类型存储最节省最后才想性能。很多人反过来直接选最大的类型觉得“反正存储便宜”。实际上数据范围定错了后面会产生两个问题要么溢出报错要么浪费空间拖累索引。举一个简单的类比数值类型就像停车场里的车位。TINYINT是摩托车位INT是普通轿车位BIGINT是货车位。你把一辆自行车放到货车位没问题但一个车位只能停一辆车货车位数量有限时大家都停货车位整个停车场能停的车就少了。InnoDB的索引页也一样主键类型越大一个数据页能存放的行数就越少索引层级越深查询IO也就越多。所以如果字段的值永远不超过255就用TINYINT不会超过65535就用SMALLINT常规编号用INT明确知道会非常巨大才用BIGINT。主键和业务主编码可以适当放宽一档因为一旦表上线后再改类型代价非常大。2. 整数类型从TINYINT到BIGINT的实操细节2.1 每个整数类型适合干什么整数类型是所有数值类型中最常用的也是大家最容易用错的。TINYINT适合布尔标志、状态码、枚举值。比如用户状态0禁用1启用订单状态10待支付20已支付这类值范围小且固定用TINYINT就够了。SMALLINT适合端口号、比较小的计数器比如一个小时内登录次数、库存数量不足65535的商品。MEDIUMINT比较尴尬能存1677万比INT小一半但实际很多场景要么TINYINT/SMALLINT扛得住要么直接用INT所以我很少用它。INT是最常见的整数类型常规业务表的主键、用户ID、商品ID都用它。INT有符号最大21.47亿无符号最大42.94亿对于绝大多数中小型系统来说完全够用。BIGINT则是为大数据准备的分布式系统喜欢用雪花ID做全局主键雪花ID的长度完全超过INT范围必须用BIGINT。如果你预估业务会高速增长或者将来要导入海量历史数据主键用BIGINT是稳妥选择。2.2 UNSIGNED不是银弹但用好了能翻倍UNSIGNED表示无符号数只允许非负整数。它的好处是在同样的字节数下上限翻倍。比如TINYINT无符号可以到255INT无符号可以到42.94亿。对于主键如果你确认不会出现负ID用UNSIGNED INT是很多团队的做法。不过UNSIGNED有一个非常隐蔽的坑无符号数相减如果结果为负数不是报错而是产生一个巨大的正向溢出值。比如执行SELECT CAST(1 AS UNSIGNED) - CAST(2 AS UNSIGNED);在MySQL中并不会直接返回-1而是返回18446744073709551615。因为无符号整数在数据库中发生了回绕这在聚合运算、存储过程计算中很容易引发查不出原因的脏数据。所以我的建议是业务上明确“只能是正数”的字段才考虑UNSIGNED否则老老实实用有符号类型。如果两个表做关联一个字段设为UNSIGNED另一个没设外键约束和JOIN也可能因为类型不一致而报错或走不上索引。这一点在字段类型检查时务必保持一致。2.3 显示宽度、ZEROFILL和AUTO_INCREMENT的真相以前经常看到这样的建表语句INT(11)、INT(4)很多新手误以为括号里的数字限制存储长度其实那是显示宽度。MySQL的老版本中显示宽度配合ZEROFILL可以让数字在查询结果里补零比如INT(4) ZEROFILL会把1显示成0001。但这只是格式化并不改变真实存储值而且从MySQL 8.0.17开始整数类型的显示宽度已经被废弃以后不要再用。ZEROFILL也有副作用它会让该列自动变成UNSIGNED这就可能导致你原本想存负数结果插入-1直接报错或者被截断。如果你确实需要对数字补零我建议放在应用层处理比如用LPAD函数LPAD(id, 4, 0)既不影响存储也不会有默认无符号的隐藏陷阱。AUTO_INCREMENT则必须配合整数类型使用而且通常和主键绑定。需要注意的是自增列必须是有符号或无符号的整数类型不能用FLOAT、DOUBLE、DECIMAL。InnoDB的自增值会持久化MySQL 8.0中重启后不会回退但老版本在重启后可能因为内存中最大值回溯导致复用ID。升级到8.0之后这个老问题基本消失了。2.4 主键到底选INT还是BIGINT这是我在无数团队聊天里被问到过的问题。如果业务单表数据量在一亿以内用INT UNSIGNED做主键最坏情况接近43亿其实已经覆盖绝大多数业务。如果你们有分库分表计划、需要生成全局唯一ID、或者主键来自第三方系统并且长度巨大那就直接用BIGINT不要犹豫。有一个很容易忽视的点是二级索引的放大效应。InnoDB的二级索引叶子节点会存储主键值如果主键从INT升级到BIGINT每个二级索引行会多出4字节。一张表如果五六个二级索引这个空间开销会被放大五六倍写入和查询都会更吃力。所以在满足数据范围的前提下主键越短越好。我自己经历过一个超大表把主键从BIGINT改回INT当然是通过新表数据迁移索引体积明显下降某些范围查询快了20%以上。3. 小数精度与DECIMAL的实战选择3.1 FLOAT/DOUBLE为什么会“算不准”浮点数的精度问题几乎所有编程语言和数据库都存在因为计算机使用二进制近似表示十进制小数。比如一个简单的加法在MySQL执行SELECT 0.1 0.2;结果很可能是0.30000000000000004而不是0.3。这种误差在单条记录里看不出来但一旦对大量浮点数据做SUM、AVG误差就会累积最后的对账差异会让你怀疑人生。FLOAT占4字节DOUBLE占8字节。DOUBLE精度更高有效数字约15位FLOAT约7位。但它们都是“近似值”不是精确值。如果只是做温度传感器采集、GPS经纬度、科学计算里的中间量FLOAT/DOUBLE没问题。但如果牵扯到钱、库存数量、需要绝对一致的对账数字就绝不能依赖浮点。3.2 DECIMAL定点数的正确打开方式DECIMAL是定点数本质上是将数字以字符串形式按位存储计算时由MySQL高精度库处理所以它能精确表达十进制小数。定义语法是DECIMAL(M, D)M是总位数D是小数位数。例如DECIMAL(10, 2)表示总长度10位其中小数部分2位整数部分最多8位最大值为99999999.99。DECIMAL的存储大小不是固定的MySQL 8.0中每9位数字占4字节剩余位数按分配表计算。比如DECIMAL(18, 2)整数部分16位、小数2位整数16位由9位7位组成9位占4字节7位占4字节小数2位占1字节总共9字节。你不需要精确记住公式只要知道它比INT和BIGINT占的空间大得多因此不该滥用。使用DECIMAL还有一个细节它的小数运算结果也是精确的但在某些版本中DECIMAL之间比较时如果小数位不同会发生隐式转换。我建议在所有涉及金额的字段上统一小数位数比如统一为2位避免因为3位小数和2位小数比较导致意外结果。3.3 业务场景下的金额、比率、坐标选型金额字段我强烈建议用DECIMAL。比如订单金额、账户余额、优惠金额都用DECIMAL(10, 2)甚至DECIMAL(12, 2)。如果你的业务只面向几个国家货币单位最大也就千万级10位整数绰绰有余。如果担心将来膨胀可以一开始就设计为DECIMAL(12, 2)。在超大流量的互联网公司有些团队会把金额乘以100存储为BIGINT的“分”这样计算全部是整数速度最快而且没有精度问题。这是一种很好的策略适合开发规范严格、不用SQL直接对金额做复杂运算的场景。比率、百分比、评分这类字段比如好评率、完成率如果允许一定误差用DOUBLE或FLOAT都可以。但如果你要保证统计结果完全精确比如胜率、汇率也建议用DECIMAL。坐标和地理位置一般用DOUBLE或者DECIMAL(10, 7)存储经纬度。经纬度需要小数位很长DOUBLE的精度基本够用但要注意联合索引中的空间计算。我在实际项目中就踩过金额精度的坑。一个支付对账表金额字段用的是DOUBLE运营反馈某天账单总额总是差几毛钱。查到最后发现是由于多个DOUBLE字段相乘和累加后产生了一分钱以内的误差报表四舍五入后差异被放大。后来把所有金额字段统一改成DECIMAL误差才彻底消失。4. BOOL、BIT这些“边缘”数值类型4.1 BOOLEAN其实是TINYINT(1)MySQL并没有真正的布尔类型。TINYINT(1)可以被用作布尔值TRUE和FALSE只是1和0的别名。你可以写CREATE TABLE t (flag BOOLEAN);实际上它被解析成TINYINT(1)。这意味着你可以在flag里插入2、-1、100等数字MySQL不会阻止你。如果你希望这个字段只能存0或1需要加CHECK约束例如CHECK (flag IN (0, 1))MySQL 8.0.16之后才真正强制执行CHECK约束。这个特性导致很多ORM框架在处理布尔字段时有点尴尬。比如用Python的pandas读出来的可能是一串0和1用JavaScript判断时可能遇到字符串“0”的坑。我的习惯是如果业务方只接受两种状态字段用TINYINT(1) NOT NULL DEFAULT 0应用层再严格校验。千万别建一个没有任何约束的BOOL字段然后又指望数据库帮你挡脏数据。4.2 BIT类型的使用场景与转换麻烦BIT(M)用于存储二进制位M范围是1到64。它更适合存储位图、权限标记这类紧凑数据比如一个字段的8个bit分别表示8种权限开关比用8个TINYINT省很多空间。但它也有明显的麻烦SELECT输出时BIT值默认会以二进制字符显示看起来是一堆乱码必须用BIN()、OCT()、HEX()这样的函数转换才能得到人类可读的结果。在JDBC或Python驱动中BIT类型常常被映射成bytes或二进制字符串比较麻烦。所以除非你的场景确实需要按位操作否则普通业务表不要轻易用BIT。如果你只是想存“是/否”用TINYINT比BIT直观得多也更容易被各种ORM支持。4.3 什么时候用BIT什么时候用TINYINT我的经验是单开关用TINYINT位集合用SET或BIGINT做位运算BIT只用在特殊协议解析、权限位图等需要贴近底层数据结构的场景。不要为了“省空间”去用BIT因为省下的那点空间远没有你为了转换格式所付出的代码成本高。5. 数值类型如何影响索引与性能5.1 存储字节与B树索引的放大效应InnoDB的索引结构是B树聚簇索引的叶子节点直接存储整行数据二级索引的叶子节点存储索引列值和主键值。主键的字节长度会影响所有二级索引的大小这就是前面说的放大效应。假设有1000万行数据主键从INT4字节变成BIGINT8字节每个二级索引叶子节点多4字节一个表如果5个二级索引相当于每个索引多40MB以上的空间实际可能更严重。更关键的是B树的层数和数据量、页大小有关。主键越短单个数据页能容纳的索引项越多树的高度就越低查询需要访问的磁盘页越少。MySQL的InnoDB默认页大小16KBINT主键可以让一个二级索引页放下更多键值整体IO次数明显减少。所以建表时对主键字节数较真不是强迫症而是实打实的性能优化。5.2 隐式类型转换索引失效的最大陷阱这是数值类型最常见的性能杀手。比如order_no是VARCHAR类型你写WHERE order_no 10001MySQL会把字符串列转换成数字再比较导致该列上的索引失效。反过来如果字段是INT类型条件里写的是字符串MySQL会把字符串转成数字通常索引还能用但依然有隐式转换的额外成本。最稳妥的做法是字段和参数的类型保持一致。两个表关联时更容易踩这个坑。一个表主键是BIGINT另一个表外键是INT虽然数值上能匹配但MySQL可能需要为其中一列做转换无法高效利用索引。更常见的是字符串类型的业务ID和数值类型关联导致大表全表扫描。所以建表时同一个业务含义的字段在主表和从表里必须定义成完全相同的类型包括是否有UNSIGNED。5.3 排序、分组与数值类型的关系数值类型排序天然就是按大小排列不需要字符集和排序规则处理这比VARCHAR排序要快。但分组统计时如果分组字段基数很高并且列上没有索引MySQL可能需要生成临时表。选择合适的数据类型能降低临时表的内存占用提升分组效率。另外注意数值的负号处理。有符号整数在排序时会自然处理负数在前无符号则不允许负数。如果你设计的字段需要有正有负比如积分变化、余额变动一定不要用UNSIGNED否则数据会溢出或者报错。这些小细节往往在功能测试时不会被发现等到报表统计时才冒出来。6. 常见问题与避坑实录6.1 手机号、身份证这类数字为什么不用数值类型经常有新手把手机号字段定义成BIGINT或DOUBLE然后发现显示成13800138000变成了1.3800138e10或者前面的0丢了。手机号虽然是数字字符但它没有数学意义不需要计算和比较大小应该用VARCHAR(20)。身份证号长度18位虽然BIGINT能容纳但身份证号里有可能是X结尾而且不能参与算术运算当然也用VARCHAR。这个原则可以扩大到所有“看起来像数字但不是数值”的字段订单号、编号、银行卡号都别存成数值类型。数值类型和字符串类型的选择标准不是“里面是不是数字”而是“会不会参与数值计算”。如果只用来等值匹配和展示字符串更安全。6.2 数值溢出在严格模式下怎么表现MySQL的严格模式由sql_mode控制比如STRICT_TRANS_TABLES。在8.0默认是开启的插入超出范围的数值会直接报错Out of range value for column。如果你把模式调整成非严格模式MySQL会把数值截断到边界值并给出警告但数据照样写入。这就会造成表中出现不符合业务预期的数据。我建议生产环境保持严格模式不要为了“省事”关闭它。宁可在测试阶段暴露问题也不要让脏数据悄悄进入业务表。同时代码里也要做好参数校验因为数据库报错返回给用户的时间成本更高。6.3 类型转换函数CAST/CONVERT怎么用当你确实需要临时转换类型时用CAST或CONVERT。基本语法是CAST(expr AS type)type可以是SIGNED、UNSIGNED、DECIMAL(M,D)、CHAR等。例如CAST(12.6 AS DECIMAL(10,2))会得到12.60。CONVERT和CAST功能类似区别在于CONVERT还支持字符集转换比如CONVERT(name USING utf8mb4)。转换有一个经典陷阱字符串转数字时MySQL会做“最好努力”解析。123abc转换成123abc转换成0不会报错。如果你依赖这种隐式行为很可能会在数据校验时漏掉脏数据。所以在应用层做严格的输入校验比数据库转换更可靠。6.4 MySQL 8.0中数值类型的变化MySQL 8.0对数值类型有一些重要调整。整数类型的显示宽度已经废弃现在定义INT(11)会有警告但不影响功能。FLOAT(M,D)、DOUBLE(M,D)这种带精度的浮点定义语法也进入弃用阶段官方建议使用标准FLOAT、DOUBLE或DECIMAL。这意味着旧系统的建表语句在升级后可能会产生大量警告虽然还能执行但新开发不要再沿用旧写法。另外MySQL 8.0默认字符集变为utf8mb4和数值类型没有直接关系但涉及将数值转换为字符串时要注意字符集的一致性。8.0的DECIMAL底层实现和旧版本相比没有本质变化但性能上针对高精度运算做了优化。6.5 建表时的避坑清单最后分享一份我多年积累下来的避坑清单你可以直接截图保存自增主键不要用TINYINT或SMALLINT至少用INT UNSIGNED高并发或分布式场景用BIGINT。状态字段用TINYINT不要用VARCHAR更不要用DOUBLE。外键和关联字段类型必须完全一致包括UNSIGNED。金额字段统一DECIMAL别用FLOAT/DOUBLE。布尔字段用TINYINT(1)加CHECK约束才能限制为0/1。手机号、卡号、单号等不参与计算的数字用VARCHAR。不要用ZEROFILL来格式化显示宽度已废弃。所有字段都要考虑是否有负值再决定是否加UNSIGNED。优先使用严格模式别让数据库默默截断溢出数值。说实话数值类型是我刚开始写SQL时最不重视的东西后来在金额对账和索引优化上连续栽了跟头才回头把这一课补上。现在每建一张表我都会先问三个问题这个数最大能到多少需不需要小数将来会不会增长到突破边界。想清楚这三件事再决定用哪种类型就能避开上面大部分坑。希望这篇踏实的整理能让你在数据库开发路上少走一些弯路。