写SQL的人要是没被数据类型坑过都不好意思说自己做过后端。前阵子接手一个跑了快三年的订单系统打开orders表的一瞬间差点背过气去。价格字段price用的是varchar(20)数量字段quantity用的是varchar(10)连下单时间created_at都是varchar(19)。数据量刚到百万级的时候统计报表要把字符串转成数值再求和排序结果经常鸡同鸭讲磁盘占用比正常设计高出百分之三十以上。这不是个例太多从业务转过来或者纯靠ORM建表的同学建表时直接在编辑器里选varchar打天下等出了问题再回来补课代价早就翻倍了。这篇文章就把MySQL数据类型的家底摊开讲一遍重点放在你怎么选、为什么这么选以及那些文档里不会明说的坑。MySQL的数据类型体系按存储大类来分就这么几块数值型、字符串型、日期时间型、JSON和二进制。每一类里又有细分参数不同行为就完全不同。下面一个个过我会把字节数、取值范围、底层存储逻辑和实际业务场景里的选择建议揉在一起讲。1. 数值类型int(11)到底是什么意思decimal才是金额的归宿1.1 整数家族的字节数与取值范围MySQL的整数类型有五个tinyint、smallint、mediumint、int、bigint。它们的区别说白了就是字节数不同决定了能装多大范围的数。一个字节8位有符号类型用一位做符号位所以取值范围就是-2^(8n-1)到2^(8n-1)-1n是字节数。整理成表类型字节数有符号范围无符号范围TINYINT1-128 ~ 1270 ~ 255SMALLINT2-32768 ~ 327670 ~ 65535MEDIUMINT3-8388608 ~ 83886070 ~ 16777215INT4-2147483648 ~ 21474836470 ~ 4294967295BIGINT8-9223372036854775808 ~ 92233720368547758070 ~ 18446744073709551615在你设计表结构时先别管功能细节第一件事就是想清楚这个字段到底要装多大的数能小则小。比如一个状态字段用0和1表示tinyint就够了城市编码、年龄、枚举编号smallint或者mediumint都行。一个字段省几个字节看似不起眼但建了索引之后百万行数据就是几MB到几十MB的差距对InnoDB的B树来说页大小固定16KB字段越短每页能容纳的索引键就越多查询时IO次数就越少。这是一个被很多人忽略的性能杠杆。1.2 深入理解int(11)的显示宽度一个经典的认知误区很多人在建表时看到int(11)就以为这是限制最大长度以为存不下超过11位的数字。这是最经典的误区。int(11)里的11只是显示宽度指的是在使用zerofill属性时位数不足11位会在前面补0它不影响存储和取值范围。你就算写成int(1)照样能存下2147483647。从MySQL 8.0开始显示宽度这个特性已经被废弃了int就是int不再需要指定宽度。所以新项目建表时别写int(11)这种老古董写法了直接int就好。如果你真的遇到过zerofill补零的需求比如编号要显示成00123这种格式那另说但那是展示层的逻辑建议放到查询时格式化而不是折腾底层表结构。再说说unsigned和zerofill。unsigned是无符号如果确认字段永远不可能是负数比如年龄、数量、主键可以加上让正数上限直接翻倍。但在MySQL 8.0.17之后zerofill也被标记为废弃而且zerofill会隐式地给字段加上unsigned属性。我的建议是别用zerofill补零用LPAD函数在SQL里做就行比如LPAD(id, 6, 0)。为了一点点显示上的便利去污染底层存储设计不值当。1.3 浮点与定点float和double是近似值decimal才是精确的金额、税率、余额这类的数据闭着眼睛用decimal这是铁律。float和double走的是二进制浮点数存储天生就是近似值。经典例子select 0.1 0.2float算出来是0.30000000000000004。银行系统里差一分钱都是事故所以金额必须用decimal。decimal是定点数按十进制存储不存在浮点误差。定义方式decimal(M, D)M是总位数D是小数位数。比如decimal(10, 2)表示总长10位其中小数占2位整数部分最多8位。要注意M的最大值是65D最大是30超出这个组合就报错了。float和double也不是没用。科学计算、统计数据、地理坐标这种对精度不敏感、但对计算速度和存储空间敏感的场景用float或者double挺合适。比如经纬度double(9, 6)就挺常见坐标本来就是采集精度有限的数据没必要用decimal较真。1.4 自增主键到底选int还是bigint主键这里单独拎出来说因为踩坑的人实在多。int的上限是21亿多看着挺多但在高并发写入、日志表、流水表里三五年就能撞线。撞线之后主键再插入就会报duplicate entry表直接写不进去。所以我的建议是单表数据量按业务预期能控制在千万级别以内且增长缓慢用int就行。用户表、订单表、流水表、消息表这类高频写入的表老老实实上bigint。分库分表场景下主键通常走分布式ID方案雪花算法之类那必须是bigint因为雪花ID本身就是64位的长整数。int和bigint差4个字节建了主键索引后对每行数据都有影响。但为了4个字节去承担业务停摆的风险我觉得不划算。主键这个位置我倾向于直接bigint起步除非你能拍着胸脯保证十年内绝对不超过int上限。2. 字符串类型char和varchar不是长度差那么简单2.1 char的定长逻辑与varchar的变长逻辑char和varchar长的很像底层思路完全不同。char是定长你定义char(10)不管你存1个字符还是9个字符MySQL都会在磁盘上占10个字符的空间存的少了后面用空格补齐查询时再把空格去掉。varchar是变长定义varchar(10)你存几个字符就占几个字符的空间另外还要额外花1到2个字节去记录这个字符串的实际长度。这么一对比似乎varchar完胜char什么场景还要用char有。比如MD5摘要固定32位UUID去掉横线32位手机号在某些国家是固定位数这些长度恒定不变的字段用char就没有长度记录的开销性能上略优也符合语义。另外需要注意char在比较时会去掉尾部的空格这既是特性也是坑。如果你的业务数据本身尾部就可能有空格用char存储查出来发现空格没了别意外这就是定长类型的设计。还有一个细节varchar会保留尾部的空格而char不会。举个例子往char(5)里存a a加4个空格读出来是a往varchar(5)里存同样的内容读出来还是a 。在这个基础上做唯一约束或者等值匹配结果可能和你想的不一样这一点设计字段时要有数。2.2 varchar(N)里的N到底是指字符还是字节最长能定义多大varchar(N)里的N指的是字符数不是字节数。这个字符数在不同字符集下对应不同字节数。比如utf8mb4字符集一个汉字占4个字节因为utf8mb4要兼容emoji一个英文字母占1个字节。那varchar最长能定义多大有两个约束需要同时满足单行的存储上限是65535字节这是MySQL行格式层面的硬限制。varchar本身需要额外1到2个字节记录长度超过255字节的字段需要用2个字节。所以如果你用utf8mb4字符集理论上varchar的最大字符数是65535除以4再减去那2个长度字节算下来约16383个字符。但这个只是理论上限实际建表时还要考虑整行的字段总和不能超过65535字节如果有多个varchar字段或者有text字段单个字段的可用空间会被压缩。你也不需要把字段定义到这种极端程度一个字段超过一两千字符就该考虑text系列了。2.3 varchar(255)真的是黄金标准吗网上很多老教程推荐varchar(255)这跟一个历史原因有关在MySQL 5.6之前utf8字符集下varchar(255)刚好对应767字节255*3765而索引的单个列长度限制是767字节所以varchar(255)可以在前缀索引下直接建全文索引。但MySQL 5.6之后的InnoDB已经把索引长度限制提高到了3072字节这个限制早已不是问题。现在建varchar(255)的问题在于很多ORM框架和代码习惯会认为255是“够用就好”的默认值导致大量字段实际只需要几十个字符却统一占用了255个字符的长度描述空间。虽然varchar是变长存储定义长度大小不影响磁盘占用但会影响内存排序时的临时表大小以及某些执行计划里对字段宽度的预估进而影响优化器选择索引。实际经验是varchar长度按业务真实上限再加一点余量比如昵称定64标题定128地址定255就足够了不要无脑255。2.4 text家族与它们的天坑当字符串长度超过几千甚至几万字符时就该考虑text系列了。MySQL的text家族分四个档位类型最大存储字节说明TINYTEXT255字节用1字节记录长度TEXT65535字节约64KB用2字节记录长度MEDIUMTEXT16777215字节约16MB用3字节记录长度LONGTEXT4294967295字节约4GB用4字节记录长度text类型最常见的一个坑是不能有默认值。你在建表语句里写content text default abcMySQL会直接报错。如果业务上需要“无内容时表现为空字符串”你用ORM做插入时手动给一个空字符串就不会触发这个问题。另外text字段不能像varchar那样直接在完整列上建普通索引必须指定前缀长度比如INDEX idx_content(content(100))这意味着text做等值匹配和排序的代价比较高。所以能用varchar解决的别轻易上text。文章正文、富文本内容、JSON快照这类确实会很大的数据再用text而且要尽量避免这类字段参与where等值匹配或者order by排序。还有一个重要的点InnoDB行格式下过长字段和行溢出处理有关。当一行数据过大比如total字段长度超过页的一半时InnoDB会把可变长字段放到溢出页即额外开辟的存储页原页面上只保留20字节左右的指针。这个机制导致的结果是行里有没有大字段会影响整张表在扫描时的IO行为。一个表如果塞了三四个text字段即使你只是select id、name扫描时也可能因为行溢出产生额外的随机IO。设计表结构时把大字段拆到单独的表里用主键关联属于常规优化操作。3. 日期时间类型datetime和timestamp的千年老坑3.1 date、time、datetime、timestamp怎么区分先把四兄弟的用途说清楚DATE只存日期范围是1000-01-01到9999-12-31占3字节。TIME只存时间范围是-838:59:59到838:59:59占3字节。注意它的范围是838小时不只是24小时这是为了支持“经过的时间”这种场景。DATETIME存日期加时间范围是1000-01-01 00:00:00到9999-12-31 23:59:59占8字节不随时区变化。TIMESTAMP也存日期加时间但范围是1970-01-01 00:00:01到2038-01-19 03:14:07占4字节会随数据库会话的时区自动转换。3.2 2038年问题真的存在timestamp用4字节存储存的是从1970年1月1日0点开始的秒数。4字节有符号整数上限是2147483647换算成日期就是2038-01-19 03:14:07。这个时刻过后再用timestamp存时间就会溢出。很多老系统的日期字段还在用timestamp等到了2038年就要出乱子这和千年虫问题本质一样。我的建议是新表一律用datetime彻底避开这个期限问题。datetime占8字节确实比timestamp的4字节大但现代服务器的磁盘和内存已经不是几十年前的标准了能买到的安全边际更值钱。如果你确实需要timestamp自动更新特性别急datetime在MySQL 5.6.5之后也已经支持自动初始化和自动更新两条腿一样长。3.3 时区处理timestamp存的是UTCdatetime存的是字面值这一节是很多后端和DBA争吵的焦点。timestamp在存储时会把当前会话时区的时间转换成UTC时间存进去查询时再转回当前会话时区。也就是说如果数据库的time_zone设置变了同一个timestamp值查出来显示的时间就变了。datetime不做这种转换你插入什么就是什么显示的也是那个字面值和时区无关。所以在多时区的业务系统里timestamp反而有它的价值只要每个客户端连接的会话时区设置正确读写时间都能自动转换不需要应用层手动处理。但这也引入了隐患——如果某个客户端的时区设置错了或者被修改了就会导致时间错乱。datetime的做法是把时区转换完全交给应用层数据库只当一个透明存储。我的习惯是应用层统一用UTC8的日期时间字符串数据库用datetime存储查询出来直接就是业务要的格式简单直接排查问题也容易。做全球化多时区业务并且团队有能力治理好各端的时区配置才考虑timestamp。3.4 自动填充创建时间和更新时间在建表时设置记录的创建时间和更新时间常用的写法就是default current_timestamp和on update current_timestamp。比如CREATE TABLE user_operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, action VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这样insert时created_at和updated_at都会自动取当前时间update时只有updated_at会自动更新created_at保持不变。需要注意ON UPDATE的触发条件是这一行真的发生了UPDATE操作而且数据有变化。如果你执行一个UPDATE语句即使set的值和原值完全一样某些情况下updated_at也可能被置为当前时间这个行为在不同的数据库版本和sql_mode下略有差异依赖这个特性的话最好用实际SQL验证一遍。3.5 存时间用int时间戳是不是好方案不少老系统喜欢用int存Unix时间戳当时是省了空间但代价是可读性几乎为零。你直接查数据库看到一行1677753600完全不知道这是什么时间必须脑海里换算或者用FROM_UNIXTIME函数转。而且int的上限问题同样存在——2038年之后int时间戳和timestamp一起死。就算改用bigint存毫秒级时间戳也只是从“存数”变成了“存二进制数”徒增理解成本。除非你是在做高性能、高压缩比的基础设施否则我不建议用整数时间戳作为业务表的常规日期字段。4. enum和set省空间的枚举陷阱与多选方案4.1 enum的本质是整数不是字符串enum类型在磁盘上存的是整数下标不是字符串本身。定义enum(待支付,已支付,已取消)实际存储时三个状态分别对应1、2、3。这个设计的优点是极省空间只占1到2个字节比varchar存中文状态名几个字节到十几个字节要划算得多。但代价也很明显。第一排序不是按字符串的字母序而是按定义时的顺序。你定义enum(待支付,已支付,已取消)order by出来的结果是待支付在最前面已支付次之已取消最后这往往不是你想的业务排序。第二往枚举里加一个新值需要执行ALTER TABLE修改列定义在大表上这是高成本的DDL操作Online DDL虽然能减少锁表时间但依然有风险。第三enum的值和数字之间有个混淆陷阱往enum字段里插入数字2如果插入值是字符串2转成对应枚举值如果直接插入整数2则是取第二个枚举值。这个区别在参数写法和框架预编译时容易被忽略非常容易埋雷。4.2 什么时候该用enum说实话业务表里我对enum持谨慎态度。枚举值高度稳定、数量少、且业务上线后几乎不会增加比如订单状态如果只有三四个固定值并且产品明确说以后不会加状态那用enum可以。但只要状态列表有可能扩张就用varchar或者tinyint加注释代替。更推荐的做法是用tinyint存状态码通过代码里的枚举类或者常量表来维护对应关系既稳定又可控以后加状态不用动表结构。4.3 set类型适合什么场景set和enum是亲戚enum是单选set是多选。比如用户标签“会员、VIP、白名单”一个人可以有多个用set(会员,VIP,白名单)就挺合适。底层同样按位存每个选项占1个bit最多可以定义64个选项。查询某个标签包含与否用FIND_IN_SET或者LIKE。set的问题和enum一样加选项也要做DDL且涉及位运算的概念不熟悉的人查数据时容易写出全表扫描的SQL。我的建议是标签类、权限类的小规模多选可以用set但一旦选项个数可能变多就老老实实用关联表或者逗号分隔字符串维护成本低得多。5. JSON类型和二进制类型新功能好用但得知道分寸5.1 JSON类型不只是一个带校验的字符串从MySQL 5.7开始有了原生的JSON类型很多人觉得它就是text换了个名字其实差别大了。JSON列在插入时会做合法性校验非法的JSON直接报错text类型可不会管这些。还有一点JSON在存储时会做二进制编码的优化MySQL把JSON文档转成一种内部二进制格式读取时不需要重复解析可以快速访问文档中的某一个字段。在JSON里取某个字段的值最常用的就是JSON_EXTRACT函数或者更简洁的-操作符。在MySQL 8.0里还能用-直接去掉双引号拿到字符串。比如SELECT user_name, profile-$.age AS age FROM user_info WHERE profile-$.city 上海;这里有个大坑需要注意JSON字段上想加速查询不能像普通字段那样直接建索引。MySQL的做法是支持生成列也就是从JSON里抽出一个字段存成虚拟列然后在这个虚拟列上建索引。生成列的语法如下ALTER TABLE user_info ADD COLUMN city VARCHAR(50) GENERATED ALWAYS AS (profile-$.city) STORED, ADD INDEX idx_city (city);这种设计把JSON的动态扩展和查询性能兼顾了是JSON类型相对text的真正优势。但要提醒一句JSON类型也不是万能胶。复杂查询、多条件过滤、需要跨JSON字段做关联的场景性能远不如传统的关系模型。能用普通字段表达的不要塞进JSON里。合理的JSON使用场景是字段结构不稳定、还在快速迭代中的扩展属性比如第三方接口返回的原始包、配置项、用户自定义设置这种“不常查但需要存下来”的数据。5.2 binary/varbinary和blob家族binary和varbinary是和char/varchar对应的二进制类型适合存字节字符串比如MD5、SHA256这类散列值。注意binary在比较时是按字节比且会用0x00补齐到定义长度也就是说binary(32)存了一个17字节的散列值剩下的用0x00填满。用binary存长度固定的散列值可以但存长度可变的二进制数据就用varbinary。blob家族tinyblob、blob、mediumblob、longblob专门存大的二进制数据比如图片、附件、音视频文件。但这里我要泼一盆冷水业务上文件类数据不要直接塞进数据库。原因有几个层面数据库会膨胀得很快备份恢复的时间成倍增长。二进制大对象在读写时占用大量内存和网络带宽严重拖慢正常的小查询。InnoDB对大对象同样有行溢出机制文件大了会走溢出页查询计划会变复杂。常规做法是文件放对象存储比如云上的OSS/S3/COS之类或者自建的MinIO数据库只存文件的URL或者对象存储的key。数据库存一个varchar(255)的路径香得很。除非你的文件全部加起来小于几百MB而且完全没有扩容的可能那才考虑longblob。6. 建表实战一张订单表把所有选择串起来6.1 从零设计一张真实的订单表下面这张表聚合了上面提到的绝大多数决策点我加了注释看完就能直接用CREATE TABLE trade_order ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 订单ID雪花算法或自增, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号唯一, user_id BIGINT NOT NULL COMMENT 用户ID高频查询字段, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 订单总金额单位元, discount_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 优惠金额, pay_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 实付金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待支付 1已支付 2已发货 3已完成 4已取消 5售后中, pay_time DATETIME DEFAULT NULL COMMENT 支付时间允许为空, consignee_name VARCHAR(64) NOT NULL COMMENT 收货人姓名, consignee_phone VARCHAR(20) NOT NULL COMMENT 收货人手机号, address_detail VARCHAR(255) NOT NULL COMMENT 详细地址, buyer_remark VARCHAR(500) DEFAULT NULL COMMENT 买家备注, extra_info JSON DEFAULT NULL COMMENT 扩展信息如渠道来源、发票信息, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_status (status), KEY idx_created_at (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT交易订单表;这张表里几个决策点过一遍订单号用varchar(32)存业务号而不是做成主键。业务号虽然唯一但主键迁移、修改、关联时会出现各种连锁问题自增bigint主键加上独立唯一键更稳妥。金额相关全部用decimal(12,2)整数部分10位足够覆盖绝大多数订单金额。状态用tinyint配合注释维护枚举对应关系不直接存中文避免排序和比较的坑。买家备注用varchar(500)没上text因为这个长度在业界就是极限再长真该text但备注算业务上可控字段500字符够写。extra_info用JSON这个表在设计初期可能还要接各种渠道来源的数据直接建一堆字段不现实JSON先兜底等核心字段探查清楚后再拆成正式列。6.2 null和not null的取舍原则建表时每个字段都要想清楚能不能为空。这个选择的影响比想象的大索引对NULL值的处理更复杂。一个可空列上建索引查询时用IS NULL或者IS NOT NULL走索引的效果和不带NULL的列不一样某些情况下优化器会放弃索引。聚合函数和比较运算里NULL表示“未知”参与运算的结果大概率是NULL。比如SUM(amount)如果某几行是NULLSUM的结果也是NULL而不是把NULL当0。所以统计类的字段必须NOT NULL加DEFAULT 0。很多ORM框架和代码封装对NULL的映射比空字符串麻烦处理不好就是全盘空指针。原则就一条业务含义上一定会有值的字段NOT NULL加默认值确实允许无值的字段用NULL表达“没有”而不是用空字符串或0硬塞。比如支付时间在创建订单时确实还没有支付动作用NULL表达“尚未支付”就比用1970-01-01或者0000-00-00 00:00:00这种魔法值强得多。6.3 字符集与排序规则的坑建表语句最后面的DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci这行千万不能省。字符集不统一会导致两表关联时转换开销甚至出现索引失效。utf8mb4是必须的因为它才能完整支持emoji和绝大多数语言的字符。而utf8mb4_0900_ai_ci是MySQL 8.0的默认排序规则属于更快、更准确的一组。但如果你用的是MySQL 5.7没有这个排序规则需要回退到utf8mb4_general_ci或者utf8mb4_unicode_ci。排序规则影响的是字符串的比较和排序结果。utf8mb4_0900_ai_ci是大小写不敏感、口音不敏感适合英文和中文的通用场景。如果业务要求大小写敏感比如用户名登录校验就要用utf8mb4_bin或者在建表时单独给这个字段指定排序规则。这里容易踩的一个坑是表连接时两端字符集或者排序规则不一致MySQL会先把一边做隐式转换再比较索引就用不上了。保证所有表都用同一套字符集和排序规则是最省心的做法。6.4 大字段和热点字段分离记一次慢查询排查上面那套理论我在一个实际项目里体验最深。有个消息通知表本来设计是标题和正文都放主表里其中正文用mediumtext。表只有三十万行但是每行正文平均两三千字符整张表占了近2GB。这时候问题来了用户打开消息列表页只显示标题和未读状态SQL是select id, title, is_read from message order by created_at desc limit 20。结果每次执行都要150到300毫秒慢得莫名其妙。用explain看执行计划发现MySQL走了主键索引但每一行都要去读溢出页里的正文因为主表行数据里只存了正文的指针除非你明确select大字段InnoDB也要访问溢出页来确认数据的可见性。三十万行的扫描每次都触发大量随机IO所以慢。后来做的操作很简单把正文拆到独立的message_content表主表只留id, title, is_read, created_at。拆完之后同样的SQL从200毫秒降到5毫秒。这背后就是我在text那一节说的行溢出机制和字段类型选择直接相关。再一个经验是有时候你在explain里看到typeindex以为用上了索引实际上可能是在扫全索引性能照样崩。优化表结构时除了看得见的字段长度、类型还要关注每个字段在InnoDB页里的实际行为特别是大字段对整个表扫描的影响。7. 最后的决策清单建表前问自己五个问题每新建一张表我建议按这五个问题过一遍比自己凭经验拍脑袋要靠谱得多这个字段会参与到where等值匹配、排序、join关联中吗如果是必须选能用上索引的类型字符串要选varchar并控制长度数值要选合适大小的整数类型。这个字段的值可能的最大值和最小值是多少确定后选择最小的能包住的整数或decimal类型为性能和存储兜底。这个字段的值是固定选项吗选项会不会变如果几乎不变可以考虑enum/tinyint加注释否则老老实实varchar。这个字段会不会存超过几千字符的内容如果会就把大字段拆到从表不要和热点字段挤在一张表里。这个字段跟时间有关吗一律datetime别用int时间戳别用timestamp做长期业务表。我在实际维护数据库时还养成了一个习惯每次建表后用SHOW CREATE TABLE把建表语句导出来存一份到项目的文档目录后面做数据库评审、迁移、对比版本时直接看这个快照比任何记忆都可靠。数据类型的合理选择不是一次性的工作而是在系统迭代中持续review像重构代码一样去重构表结构。最后分享一个我正在用的检查小手段定期在测试库跑一次SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA 你的库名按DATA_LENGTH倒序把靠前的表拎出来用explain抽查几条核心查询。数据膨胀总是悄悄地发生等用户开始抱怨接口变慢的时候就已经晚了。选对数据类型是成本最低的优化手段这句话怎么强调都不为过。