1. 数据类型,为什么值得花一整篇来聊
先抛个问题:你建表的时候,是怎么决定某个字段用INT还是VARCHAR的?如果答案是“凭感觉”,那你大概率在某个深夜被Data too long for column或者Out of range value这种报错折磨过。我见过太多新手,甚至一些工作两三年的开发,能把CRUD写得飞起,但一说到数据类型就露怯——CHAR和VARCHAR到底差在哪?DECIMAL和FLOAT存钱哪个靠谱?DATETIME和TIMESTAMP为什么总有人选错?
这篇文章就围绕 MySQL 里最常见的那几类数据类型,把它掰开揉碎了讲清楚。不仅告诉你每个类型是干什么的,更重要的是告诉你为什么要这么选,以及在真实项目里踩过的坑。不管你是在配 MySQL 8.0 环境、准备面试,还是正在做数据库设计,这篇都能帮你省下不少试错时间。
我对数据类型的理解是:它是表结构的“地基”,地基没打对,上层建筑再华丽也没用。选错类型,轻则浪费存储空间,重则数据溢出、精度丢失、索引失效,甚至导致整个业务逻辑出错。下面直接从最核心的数值类型开始。
2. 数值类型:别把 INT 当万能钥匙
2.1 整数类型:TINYINT 到 BIGINT,到底该怎么选
MySQL 的整数类型一共有五个:TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT。它们之间的区别,说白了就是“能装多大的数”。我直接把它们的字节数和取值范围列出来,你感受一下:
| 类型 | 字节数 | 有符号范围 | 无符号范围 |
|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 0 ~ 255 |
| SMALLINT | 2 | -32768 ~ 32767 | 0 ~ 65535 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 0 ~ 16777215 |
| INT | 4 | -2147483648 ~ 2147483647 | 0 ~ 4294967295 |
| BIGINT | 8 | -2^63 ~ 2^63-1 | 0 ~ 2^64-1 |
很多人看到这个表,第一反应是“那我全用 BIGINT 不就行了,省得以后不够用”。这种想法在个人项目里也许没大问题,但在生产环境就是灾难。为什么?因为 MySQL 的内存临时表、排序操作、索引存储都会跟字段类型的大小直接挂钩。举个例子:一张一千万行的表,主键如果从INT换成BIGINT,光主键索引占用的空间就会多出大约 40MB 到 80MB(取决于页大小和填充率),再加上二级索引里冗余的主键,这个数字还会翻倍。
我自己的经验是:状态位、布尔值、年龄这种小范围数据用TINYINT;订单数量、计数类字段用INT;只有确实可能超过 21 亿的数值,比如大型平台的总流水、用户总量这种,才考虑BIGINT。至于SMALLINT和MEDIUMINT,用的场景比较窄,但遇到正好在范围内的字段,比如积分数、短编码,用它们能比INT省一半甚至更多的空间。
还有一个必须提到的点:ZEROFILL 和显示宽度。老版本的 MySQL 允许你写INT(11)这种带显示宽度的语法,新版本已经废弃显示宽度了。INT(11)里的 11 从来就不限制存储范围,只影响某些客户端展示时的补零效果,而且从 MySQL 8.0.17 开始,显示宽度已经被明确移除。所以看到有人还在那儿纠结INT(10)还是INT(11),直接把官网文档甩给他就完事了。
2.2 浮点与定点:FLOAT、DOUBLE、DECIMAL 的相爱相杀
如果说整数类型是“选择题”,那浮点类型就是“送命题”。因为太多人在这上面吃过亏,尤其是涉及金额计算的场景。
先说FLOAT和DOUBLE。这两个是真正的浮点数,底层用二进制近似存储。这意味着什么呢?意味着你存进去的 0.1,实际在机器里是一个无限接近 0.1 但不等于 0.1 的二进制小数。你查出来展示的时候看着没问题,但一旦做累加、比较,各种诡异的事情就出来了。经典的例子:0.1 + 0.2 在浮点数运算里不等于 0.3。这不是 MySQL 的 bug,是所有使用 IEEE 754 标准的语言的通病。
DECIMAL则是定点数,以字符串形式存储十进制数值,不存在二进制浮点误差。所以涉及钱的字段,比如单价、金额、余额,一律用DECIMAL,这个没有什么好商量的。DECIMAL(10, 2)的意思是总位数 10 位,其中小数部分 2 位,也就是最大能存 99999999.99。
我知道有人会抬杠:“那我先用 DOUBLE,存的时候注意一下不就行了?”不行。你以为你注意了,但聚合函数一跑,SUM、AVG 的结果照样是 DOUBLE 类型,误差会在不知不觉间累积。我接手过一个老项目,订单金额用 FLOAT 存,运营后台按天汇总交易额,月底对账的时候差了十几块钱,查了一天最后定位到是浮点精度问题。从那以后我立的规矩就是:钱相关的字段,DEFINITELY DECIMAL,绝不妥协。
还有个实操细节:DECIMAL在 MySQL 5.6 之前的版本,存储引擎层为了对齐会做些额外处理,但在现代版本里,DECIMAL的存储和计算性能已经优化得很好了,不需要因为担心性能而退回去用DOUBLE。真要追求极致性能且能接受精度损失的场景,比如统计报表里的趋势图、大屏展示的非关键指标,用DOUBLE也说得过去,但要清楚边界在哪里。
3. 字符串类型:CHAR、VARCHAR 和 TEXT 的抉择
3.1 CHAR 与 VARCHAR:不只是长度固定与否的区别
这两个是日常建表用到最多的字符串类型。教科书上说:CHAR是定长,VARCHAR是变长。这个说法没错,但只说对了一半。它们真正的差别在于存储机制和性能特征。
CHAR(N)一旦定义,就固定占用 N 个字符的空间。哪怕你只存了一个字符,它照样占满 N 个字符的存储(尾部空格会被移除)。好处是存储结构固定,读取的时候不需要额外记录长度信息,在某些场景下访问速度略快,因为 MySQL 知道每条记录的这个字段从哪里开始到哪里结束。
VARCHAR(N)则是“实际用多少,基本占多少”,但需要额外的 1 到 2 个字节来记录实际长度(取决于最大长度是否超过 255 字节)。这个长度前缀在读取时需要多一步解析,所以理论上比CHAR慢一丢丢。但这只是理论。在 InnoDB 存储引擎下,数据页的读写开销远比这点解析开销大,所以为了性能去刻意用CHAR的场景其实非常少。
我的建议是:短且长度基本固定的编码,用CHAR。比如性别(如果用字符串存的话)、国家代码、MD5 哈希值(32 位十六进制)、UUID 的去掉横杠的 32 位形式。长度不确定的,一律VARCHAR,比如用户名、邮箱、地址。有一个容易忽略的点:VARCHAR(5)和VARCHAR(200)在存储相同内容时,数据占用的空间是一样的,但 MySQL 在排序或创建临时表时,会按字段定义的最大长度来分配内存。所以把VARCHAR定义得过大,不是“有备无患”,而是“隐性浪费”。我见过有人把所有字符串都写成VARCHAR(255),结果一张表有二十多个这种字段,每次排序内存临时表的开销直接翻几倍。字段长度够用就行,不要无脑贪大。
3.2 TEXT 家族的陷阱与替代方案
TEXT家族包括TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT,区分标准就是最大长度。但我要说的是它们几个“坑爹”的特性。
第一,TEXT类型不能有默认值。这在 MySQL 8.0 里依然如此(8.0.13 之后可以给BLOB、TEXT设置表达式默认值,但直接给一个静态字符串默认值还是不行)。这就导致你在建表时如果用NOT NULL DEFAULT '',直接报错。解决思路是:要么允许 NULL,要么在应用层处理空值。
第二,TEXT类型的字段不能直接完整地参与索引。你只能给它加前缀索引,比如INDEX idx_content (content(20)),只对前 20 个字符建索引。这就意味着如果你用TEXT字段做 WHERE 条件或者 ORDER BY,性能会非常难看。我之前的习惯是:所有需要排序、去重、作为查询条件的字段,哪怕很长,也优先考虑用VARCHAR承载(比如限制在 500 或 1000 字符以内)。只有真正的大文本内容,比如文章正文、日志详情、JSON 响应快照,才用TEXT家族。
第三,TEXT存储在 InnoDB 里,如果长度较大,会被放到溢出页(off-page),即不在主索引的叶子节点中直接存放,而是用一个 20 字节的指针指向外部存储页。这会导致一次额外的 IO。所以涉及大文本的查询,尽量只查需要的字段,别无脑SELECT *,否则每个文本字段都可能是潜在的性能杀手。
3.3 字符集与排序规则:乱码的根源在这
聊字符串类型,就绕不开字符集。MySQL 的字符集控制着字符如何编码成字节,排序规则(collation)控制着字符如何比较排序。这俩选错,直接后果就是中文乱码、排序顺序诡异、WHERE name = 'a'查不到数据。
主流的字符集就是utf8mb4和utf8mb4_unicode_ci。这里要特意强调一下,MySQL 里那个叫utf8的字符集,其实是残缺的,最大只支持 3 字节编码,存不了 emoji 表情和一些生僻中文。真正的完整 UTF-8 在 MySQL 里叫utf8mb4。从 MySQL 8.0 开始,utf8mb4已经是默认字符集,但如果你在维护老项目,建表时一定确认一下表级别的字符集。
关于排序规则,utf8mb4_unicode_ci和utf8mb4_general_ci是常见的两个选择。general_ci的实现比较粗糙,排序速度稍快但准确性差;unicode_ci基于 Unicode 标准,排序更精确,现代版本中两者性能差距已经微乎其微。我在新项目里统一用utf8mb4_unicode_ci。需要注意ci后缀表示大小写不敏感(case insensitive),如果业务需要区分大小写,要用utf8mb4_bin。
一个很容易踩的坑是:连接层的字符集。有些人建表和连接字符集不一致,插入中文后看到的是乱码或者问号。MySQL 8.0 的默认连接字符集已经是utf8mb4,但如果你用老版本客户端,或者连接串里没写characterEncoding=utf8(最保险是写utf8mb4),那照样乱。我排查乱码问题的顺序永远是:先看表字符集,再看连接参数,最后看字段的排序规则。
4. 日期与时间类型:DATETIME、TIMESTAMP 与那些时区坑
4.1 DATETIME 与 TIMESTAMP 的核心差异
日期时间类型在面试里几乎是必考题,但很多人答不到点子上。DATETIME和TIMESTAMP的区别,记住以下核心几点就够了。
第一,存储空间和时间范围。DATETIME从 MySQL 5.6 版本开始占 5 个字节(原先是 8 字节),范围是1000-01-01 00:00:00到9999-12-31 23:59:59。TIMESTAMP占 4 字节,范围只有1970-01-01 00:00:01UTC 到2038-01-19 03:14:07UTC。是的,2038 问题对 32 位时间戳是真实存在的,不过 MySQL 8.0 的TIMESTAMP底层已经用 64 位存储了,范围扩展到跟DATETIME差不多,但语义上仍然受传统时区表现影响。
第二,时区处理方式。TIMESTAMP存储的是 UTC 时间戳,查询时按当前会话的时区转换展示。DATETIME则是一个“墙钟时间”,你存什么就是什么,跟时区无关。这个差异实际影响很大:如果你的应用有海外用户,需要按用户时区展示本地时间,用TIMESTAMP配合数据库时区设置会有天然优势;如果只是国内业务,大家统一用北京时间,那DATETIME更简单直接,不会因为数据库时区设置被篡改而出现展示错乱。
第三,自动初始化和更新。TIMESTAMP老版本天然支持DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,DATETIME要到 5.6.5 之后才支持。现在两者都可以用这些语法了,所以这方面不再构成选型差异。
我的实战建议:业务时间字段,除非有明确的跨时区需求,否则用DATETIME。系统记录字段,比如created_at、updated_at,用DATETIME也行,但注意统一。有一个我踩过的坑:开发环境数据库时区是 UTC,线上是 CST(中国标准时间),用TIMESTAMP存时间,结果开发的同事写入一条记录后发现差 8 个小时,排查了半天最后发现是两边时区不一致,同步到一个时区后问题就消失了。
4.2 DATE、TIME 与 YEAR:小巧的专项类型
除了两个大头,MySQL 还提供了DATE(只有日期,3 字节)、TIME(只有时间或持续时间,3 字节)、YEAR(1 字节)这种专项类型。很多人在建表时习惯“凡是日期全用 DATETIME”,实际上如果业务只需要精确到天,比如生日、节假日表,用DATE比DATETIME省 2 个字节,而且语义更清晰。
TIME类型有一个容易被误解的地方:它的范围不只是 00:00:00 到 23:59:59,而是-838:59:59到838:59:59。它可以表示持续时间,比如某个任务的耗时是 36 小时 15 分,用TIME是可以存的。这个特性偶尔会派上用场。
关于YEAR,我用得很少,一般存年份直接用SMALLINT更灵活。但你要是在老项目里见到YEAR(2),注意那已经是上古语法了,MySQL 8.0 里只能定义YEAR(4)。
4.3 时间字段的默认值规范
建表时时间字段的默认值有几种用法,我直接给出我常用的模板:
CREATE TABLE `order_info` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` VARCHAR(32) NOT NULL COMMENT '订单号', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待支付,1已支付,2已取消', `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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';ON UPDATE CURRENT_TIMESTAMP这个语法非常实用:任何 UPDATE 操作自动刷新这个字段,不需要在业务代码里手动维护。但有个注意点:ONLY_FULL_GROUP_BY 模式下,如果某条 UPDATE 语句并没有真正修改数据,updated_at可能不会变动(取决于 MySQL 版本和参数),这个问题在 MySQL 8.0 里依然会存在,所以如果你的业务依赖时间戳判断“是否被更新过”,别把它当作唯一依据。
5. 特殊类型:ENUM、SET、JSON、BLOB 与二进制
5.1 ENUM 和 SET:少数场景下的优雅方案
ENUM是枚举类型,相当于限定字段只能取某几个值之一。比如状态字段用ENUM('pending', 'paid', 'cancelled')。优点是可读性强,存储上每个值用 1 到 2 个字节,比较紧凑。缺点是扩展性差:加一个枚举值需要执行ALTER TABLE,在数据量大时会锁表;而且ENUM底层是按照索引数值存储的,如果你存了'0'这种字符串进去,会有各种边界问题。
SET则是多个值的组合,适合“多选”场景,比如一个人拥有哪些角色。它内部用位图表示,最多 64 个不同成员。但在实际开发中,我更倾向用单独的子表或者 JSON 数组来处理多选关系,因为SET的查询需要用到FIND_IN_SET这种函数,没法走索引,数据量大时是性能隐患。
ENUM在面试中经常被问到底层怎么存,但我对它的态度是:能用 TINYINT + 代码映射状态码,就不要用 ENUM。原因很简单:ENUM的排序是按内部索引值而不是字符串值,这会给日常查询带来认知负担。不过让我给个例外:小型配置表、字典表里的取值固定不变的存量系统,用ENUM可读性确实好,迁移成本也低,不算罪过。
5.2 JSON 类型:方便,但别滥用
MySQL 5.7 引入了原生JSON类型,8.0 又加了 JSON 索引和大量 JSON 函数,可以说 JSON 在 MySQL 里已经是一等公民了。它有几个关键机制:
- JSON 文档在插入时会做二进制解析(用
json_binary编码),所以每次都重新解析文本,查询时可以通过$.path语法来提取字段。 JSON类型不能在创建索引时直接索引整个文档,但 MySQL 8.0 支持通过“生成的列(generated column)”把某个 JSON 字段提取出来,并在该生成列上建索引。- JSON 字段会额外存储文档的键信息,所以占用的空间比纯文本略大。
我自己的经验是:JSON 类型适合存“结构灵活但不需要频繁检索”的数据,比如第三方接口返回的原始报文、用户扩展属性。如果某个 JSON 里的字段要作为查询条件,那请务必将这个字段映射到正常的列上,或者使用生成列加索引。别把 MySQL 当文档数据库用,上了生产环境你会被慢查询教做人的。这是经验之谈。
5.3 二进制类型:BINARY、VARBINARY、BLOB
二进制类型在业务开发里出场率不高,但也不陌生。比如存图片、文件、加密后的数据,或者一些精确的二进制串比较。BINARY(N)和VARBINARY(N)对应字符类型的CHAR和VARCHAR,只不过存的是字节而不是字符。BLOB家族对应TEXT家族,同样不能有默认值,同样可以通过前缀索引来加速。
这里我啰嗦一句:如果是图片、视频这类文件,强烈不建议直接存数据库。文件放对象存储,数据库只存 URL,这是行业标准做法。数据库里放文件,既占空间又拖慢备份恢复,还让表变得臃肿。BLOB的合理使用场景是:小型的、需要事务一致性的二进制数据,比如加密密钥、签名证书,或者不方便用文件系统管理的临时数据。
6. 属性与设计:从建表到走索引的那些事
6.1 UNSIGNED、NOT NULL 与 AUTO_INCREMENT
整数类型有一个属性UNSIGNED(无符号),表示字段不存储负数。很多人主键喜欢用BIGINT UNSIGNED,这样正数范围从 2^63 扩大到 2^64,对主键来说省一点“负数空间”没用,但对自增主键来说,能推迟溢出。溢出这个问题不是耸人听闻:INT自增主键最大 21 亿多,一些大表真的会撞到天花板。一旦主键溢出自增报错Out of range value for column,运维会想杀人。
NOT NULL应该成为你的默认习惯。我见过一个表,二十个字段,只有主键是NOT NULL,其余全是可空,导致业务代码里到处都是if (row.getX() != null)。这一是增加了心智负担,二是 MySQL 对 NULL 的索引效率略低(每个 NULL 都需要单独的特殊标记,三值逻辑也让查询容易出 bug)。能用空字符串、0、空数组表达“无”,就尽量不要用 NULL。唯一的例外是:业务语义上确实有“未知”和“无”的区别时,比如某个可选填的扩展字段,还没填是 NULL,确定没有是空串。
AUTO_INCREMENT是 MySQL 的自增方式。注意它有三个保证:必须定义在索引列上,必须是整数类型,只能有一个。InnoDB 的自增实现里有一个著名的innodb_autoinc_lock_mode参数,默认值为 1(连续模式),在批量插入时锁表,可保证自增连续性。MySQL 8.0 默认是 2,性能更好但自增值可能跳跃。如果你因为数据迁移或者删除记录发现自增主键跳号了,不用慌,这是 InnoDB 的机制决定的,应用层面不应该依赖自增主键的连续性来生成业务单据号。
6.2 数据类型与索引的匹配逻辑
索引能不能生效,跟数据类型高度相关。最典型的问题是:你在VARCHAR字段上存数字,查询时传了一个数字类型,MySQL 会做隐式类型转换,导致索引失效。我之前查过一条慢日志,WHERE phone = 13800138000,phone 是VARCHAR(20),查询时传入的是整数,MySQL 会把字段做转换,结果就是这个查询扫了全表几十万行。改成WHERE phone = '13800138000'之后,瞬间命中索引。这种问题在代码写 ORM 查询时特别容易踩,因为很多语言会自动帮你做类型转换。
另一个匹配问题是排序规则。如果两个字段要 JOIN,它们的数据类型、字符集、排序规则必须一致,否则 MySQL 无法使用 join buffer 的快速匹配方式,日积月累也会拖垮性能。
如果某个字段是VARCHAR但业务上只存数字,比如订单号、银行卡号,建议还是保持VARCHAR,不要为了“看起来正规”改成BIGINT。因为这类号码经常带前导零或者超长,一旦转成整数,数据就坏掉了。类型选择要服务真实业务,而不是追求“看起来对”。
6.3 如何正确修改字段类型
线上表改字段类型不是小事。先说简单场景:VARCHAR(50)改成VARCHAR(100),属于扩展长度,InnoDB 在多数情况下可以通过在线 DDL 快速完成(ALGORITHM=INPLACE)。但如果是从INT改成BIGINT,或者VARCHAR改成TEXT,可能就需要重建表,过程会伴随锁或额外空间占用。MySQL 8.0 的在线 DDL 已经比老版本强很多,但依然要注意以下几点:
- 大表(几千万行)执行
ALTER TABLE,最好先在低峰期操作,并预估好磁盘空间。 - 修改前备份,出了问题能回滚。最保险的流程是:先在测试库跑一遍同构表评估耗时,再设计上线窗口。
- 变更前看
performance_schema里的锁等待情况,避免在业务高峰期造成阻塞。
我经历过一次线上ALTER TABLE导致主从延迟十几分钟的事故,从那以后凡是涉及大表结构变更,一律走完善的上线流程,绝不“顺手一改”。
7. 实战速查:核心建议汇总
7.1 一种建表参考风格
结合前面所有内容,我总结一份我平时建表的偏好,给你做个参考:
| 场景 | 推荐类型 | 备注 |
|---|---|---|
| 主键(单表) | BIGINT UNSIGNED | 自增,预留空间 |
| 分布式 ID(雪花) | BIGINT | 不能自增,应用层生成 |
| 状态、开关、枚举值 | TINYINT | 配合注释或代码映射 |
| 数量、计数 | INT UNSIGNED | 不会为负 |
| 金额 | DECIMAL(10,2) 或更精细 | 按需扩大小数位 |
| 折扣率、比例 | DECIMAL(5,4) | 精度优先 |
| 用户名、邮箱、地址 | VARCHAR(32~255) | 按实际业务定长 |
| 手机号 | VARCHAR(20) | 别用 INT,前导 0 会丢 |
| 哈希值、MD5 | CHAR(32) | 长度固定 |
| 生日、日期 | DATE | 精确到天即可 |
| 创建时间、更新时间 | DATETIME | 配合 CURRENT_TIMESTAMP |
| 大段文本 | TEXT 或 MEDIUMTEXT | 不参与索引和排序 |
| 扩展信息 | JSON | 不涉及检索或配合生成列 |
这个表不是金科玉律,但起码能帮你避免 80% 的常见选型错误。
7.2 面试高频点:一网打尽
既然热搜里有“MySQL 面试题”这个关键词,我就顺手把数据类型这块的面试高频考点列一下,你要去面试的话重点看这几条:
CHAR和VARCHAR的最大长度限制。VARCHAR的最大长度受行大小限制(65535 字节),还要减去其他字段占用的空间,以及编码字节数。实际用utf8mb4时,单字段最大约 16000 多个字符,但我不建议你真去卡这个极限值。DATETIME和TIMESTAMP的区别(上文已展开)。DECIMAL和FLOAT/DOUBLE的区别。INT(11)这种写法到底是什么意思——能答出“显示宽度已废弃,不影响存储范围”绝对加分。- 字符集
utf8和utf8mb4的区别。 TEXT能否有默认值,索引怎么建。- 隐式类型转换为什么会导致索引失效。
这些点如果你都能对答如流,数据类型这个模块基本就过关了。
8. 常见问题速查:我踩过的坑,你就别踩了
这里把实战中最常遇到的数据类型相关问题和排查方法整理成一张表,建议收藏:
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 插入中文变问号或乱码 | 表/连接字符集不是 utf8mb4 | 检查SHOW CREATE TABLE和连接串参数 |
Out of range value for column | 数值超出字段范围 | 改用更大的类型,比如 INT 改 BIGINT |
Data too long for column | 字符串超长 | 检查字段类型长度,调整 VARCHAR(N) |
| 金额对账总是差几分 | 用了 FLOAT 或 DOUBLE | 改成 DECIMAL,重新迁移数据 |
| datetime 查询结果差 8 小时 | 数据库时区和应用时区不一致 | 统一时区,或改用 DATETIME 存墙钟时间 |
WHERE phone = 138...不走索引 | 隐式类型转换 | 查询条件里带引号,传字符串 |
| 排序结果和预期不一致 | ENUM 按索引值排 | 加数字排序字段,或避免 ENUM |
| JSON 字段查得慢 | 未建生成列索引 | 对高频查询字段增加 GENERATED COLUMN 并建索引 |
| 自增主键跳号 | innodb_autoinc_lock_mode=2 等 | 接受跳号,不依赖主键连续性 |
| 无法给 TEXT 字段设置默认值 | 类型特性限制 | 允许 NULL 或改 VARCHAR |
9. 写在最后的一个建议
数据类型的选择,表面上是一行CREATE TABLE的书写方式,实际考验的是你对业务的理解深度。存储空间是成本,查询性能是收益,数据准确性是底线,这三者的平衡就是设计功力的体现。我见过用VARCHAR(1000)存用户昵称的“豪放派”,也见过用DECIMAL(30,10)存一个 0 到 1 之间概率值的“保守派”,各有各的道理,但也有各自的代价。
我个人在实际操作中最深的体会是:不要沉迷于“能不能用”的讨论,而要追问“该不该用”。一个字段类型选对了,可能什么都感觉不到;选错了,问题会在几个月甚至一年后突然爆发,到时候出现在你面前的是一个深夜的报警电话,和一条让你头疼欲裂的慢查询。数据库设计阶段多花点时间想清楚,比上线之后熬夜修 bug 要划算太多了。
最后分享一个可用的小技巧:如果你拿到一个旧库的表结构,不确定某个字段的类型到底合理不合理,可以先跑一条SHOW CREATE TABLE,再结合information_schema.COLUMNS里的DATA_TYPE批量列出来,对疑似有问题的字段做采样分析,按字段长度分布、最小值、最大值判断是否越界、是否精度丢失。这类脚本我每次接手老项目都会写一遍,比肉眼翻建表语句高效得多。数据库设计这块,细节决定成败,希望你少踩坑。