最近整理一篇MySQL 8建表的实操笔记,起因是接手一个后端服务,发现库里不少表是早期随手建的:订单金额用FLOAT,用户状态用VARCHAR存中文,还有几张大表连主键都没有。排查问题时我把MySQL 8的CREATE TABLE完整过了一遍,从表名、临时表、表克隆和复制、列数据类型到列属性,发现这个版本相比5.7变化确实不少。比如默认字符集改成了utf8mb4,CHECK约束从8.0.16开始真正强制执行,自增ID持久化不再随重启回退,DATETIME也支持了表达式默认值。这篇文章面向正在用MySQL 8做开发的同学,也适合DBA把建表规范落进评审清单,内容偏实战,看完可以直接照着写表结构。
1. 建表前的三个决定:表名、引擎和字符集
很多人建表上来就写CREATE TABLE,其实表名、引擎、字符集这三个决定应该在写第一行SQL之前就想清楚,因为它们一旦上线就不好改,后面每次迁移、扩容都会牵扯到。
1.1 表名不是随便取的,大小写敏感是个隐形雷
表名最长64个字符,这个限制一般用不到,真正容易踩坑的是大小写敏感问题。MySQL在Linux下默认区分大小写,在Windows和macOS下默认不区分,这就导致一个很经典的现场:开发在本地建了一张UserAccount表,代码里写user_account,本地跑得好好的,部署到Linux服务器就报“table doesn't exist”。我见过不止一个项目因为这个在凌晨上线时回滚。
解决办法很简单,团队里约定统一小写加下划线,user_account这种风格,并且把这条写进代码规范。另一个需要注意的点是避免保留字,比如order、group、desc这些词,建表时不会报错,但写SQL时到处要加反引号,麻烦且容易漏。如果真的碰上了,要么改名,要么用反引号包起来,但从维护角度我强烈建议改名。
表名最好带业务前缀,比如订单域用ord_开头,用户域用user_开头,商品域用goods_开头。这样在库表数量超过几百张时,按前缀就能快速定位,权限管理也能按域区分,否则光靠记忆找表非常痛苦。
1.2 存储引擎:业务表认准InnoDB,临时表可以例外
MySQL 8的默认存储引擎是InnoDB,这意味着不写ENGINE关键字时就是InnoDB。InnoDB提供事务、行级锁、外键、崩溃恢复这些能力,绝大多数业务表都该用它。
有些老项目还在用MyISAM,理由是查询快、占用小,但MyISAM不支持事务,崩溃后恢复也麻烦,并发写入时表级锁会卡住整个表。MySQL 8里虽然还能用MyISAM,但官方已经在边缘化了,新业务表没必要给自己埋这个坑。
不过临时表是个例外,临时表经常用MEMORY引擎,把数据放在内存里,存取速度极快,适合存中间计算结果。但MEMORY表有两个限制要注意:一是表大小受max_heap_table_size限制,默认16MB,数据量大了会报“The table is full”;二是VARCHAR和BLOB这些可变长度字段在内存里会按定长存储,比较浪费空间。数据量超过阈值时,临时表还是回到InnoDB更稳妥。
1.3 字符集:MySQL 8默认utf8mb4,但建表时最好显式声明
MySQL 8把默认字符集从latin1改成了utf8mb4,默认排序规则是utf8mb4_0900_ai_ci。utf8mb4是utf8的超集,支持emoji和生僻字,后端存用户昵称、评论内容都不怕乱码。
实际建表时我建议还是显式声明一遍:
CREATE TABLE user_account ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;这么写不是为了显得专业,而是防止实例级参数不一致。你本地是utf8mb4,测试环境可能被人改成了gbk,如果建表语句没写,建出来的表就随环境走了。显式写清楚之后,无论在哪执行,表结构都一样,迁移时少很多麻烦。
另外要注意utf8mb4比utf8多一个“_0900_ai_ci”的排序规则选择,ai表示口音不敏感,ci表示大小写不敏感,这是默认的。如果业务需要区分大小写,可以改成utf8mb4_bin,但要在列级别指定,后面细讲。
2. 临时表:会话内的隔离工作台
临时表我单独拿出来讲,是因为很多开发同学虽然知道语法,但没搞懂它的生命周期,用起来出了不少问题。临时表在MySQL里是一个很实用的工具,尤其适合处理复杂查询的中间结果。
2.1 临时表是什么,它和普通表有什么本质区别
临时表用TEMPORARY关键字创建,它只在当前会话内可见,当前会话断开连接后,临时表会自动删除。这个特性意味着同一时间线上可能有几十个会话各自创建同名临时表,互不干扰。
它和普通表的本质区别在于生命周期和作用域。普通表是持久化对象,删除前一直存在,所有连接都能看到;临时表是会话私有的,其他连接连名字都看不到,也没法访问。这个特性特别适合做批处理:你从几张表里汇总出中间数据,存入临时表,再继续做复杂计算,算完会话结束,一切自动清理,不用操心删表。
但要小心同名遮蔽问题。如果你在当前会话里创建了一张和普通表同名的临时表,那么在这个会话内,所有SQL操作都会优先访问临时表,普通表被“遮蔽”了。正常情况你感觉不到,一旦忘了删除临时表,就会遇到“改了数据却不生效”的诡异问题。排查方法是用SHOW CREATE TABLE查看,如果看到的建表语句里带TEMPORARY关键字,说明当前有临时表。
2.2 临时表的创建语法和内存引擎使用
创建临时表和普通表几乎一样,只是多了TEMPORARY关键字:
CREATE TEMPORARY TABLE tmp_user_order_summary ( user_id BIGINT NOT NULL, total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, order_count INT NOT NULL DEFAULT 0, PRIMARY KEY (user_id) ) ENGINE=MEMORY;这个表把每个用户的订单汇总结果放在内存里,后续查询直接读它,不需要反复JOIN原始大表。ENGINE=MEMORY是临时表常用的优化手段,但要注意前文说的max_heap_table_size限制,如果预估数据量超过阈值,就别用MEMORY了,让临时表落到磁盘,用默认的InnoDB引擎。
临时表还支持用LIKE克隆结构:
CREATE TEMPORARY TABLE tmp_user_ref LIKE user_account;这个语句只会复制表结构,不会带数据,如果你需要用到user_account的结构但不想动原表数据,可以用它快速建一张空壳。
实际使用中还有一个连接池的坑。应用连数据库通常走连接池,连接是复用的,理论上会话结束后临时表该删,但如果你在代码里没有显式删除临时表,连接归还到池子时临时表可能还在,下次同一个连接执行相同逻辑时,CREATE TEMPORARY TABLE就会报“Table already exists”。建议在每次创建前先执行DROP TEMPORARY TABLE IF EXISTS清理一遍,养成习惯。
3. LIKE和AS SELECT:表克隆与复制的两把刀
表克隆和复制是日常运维和开发的高频需求,比如给大表建备份表、把生产表复制到测试库、按条件抽取数据生成报表表。MySQL 8提供了两种核心方式:CREATE TABLE ... LIKE和CREATE TABLE ... AS SELECT,两者差异很大,选错了会意外丢索引或者丢数据。
3.1 CREATE TABLE ... LIKE:结构完整,但数据不带
CREATE TABLE user_account_bak LIKE user_account;这条语句会把源表的列定义、索引、生成列、CHECK约束都复制过来,相当于做了一份完整结构拷贝。它有几个重要特性:
- 不复制数据,所以执行完是一张空表。
- 不复制外键约束,虽然列和索引都在,但外键关系需要手动添加。
- 自增列不保留当前自增值,从1重新开始。
LIKE在MySQL 8里还支持括号写法:CREATE TABLE user_account_bak (LIKE user_account),效果一样,看个人习惯。如果你只需要一张和源表结构一样、但数据清空的表,用LIKE最合适。
我经常用它做两件事:一是给大表做归档,先LIKE建一张空表,再按条件把历史数据搬进去;二是做表结构变更演练,先在备份表上试一试ALTER TABLE,确认没问题再操作线上表。
3.2 CREATE TABLE ... AS SELECT:数据直接复制,但结构不一定是你想要的
CREATE TABLE user_account_copy AS SELECT * FROM user_account WHERE status = 1;CTAS会把查询结果直接落成一张新表,数据一步到位,看着很方便。但它有几个隐藏的坑,不注意就会踩:
- 新表不会继承索引、主键、外键,哪怕源表有完好的索引,新表也是一张没有任何索引的“裸表”,数据量大时查询会全表扫描。
- 类型会被推断,不一定是你期望的类型。比如SELECT一个VARCHAR(20)的字符串,新表可能推断成VARCHAR(255),白白浪费存储;如果SELECT的是表达式,比如SUM(amount),新表的列名会变成表达式文本,使用时非常别扭。
- 默认值不会保留,源表的DEFAULT CURRENT_TIMESTAMP这些属性,CTAS后都丢了。
解决类型问题的办法是显式指定列类型:
CREATE TABLE tmp_salary_stat AS SELECT user_id, CAST(SUM(amount) AS DECIMAL(12,2)) AS total_salary FROM salary_log GROUP BY user_id;解决索引问题的办法是建表后手动补索引。但最推荐的完整复制方案其实是下面这个组合。
3.3 完整复制一张表的推荐组合
要真正复制一张表,包括结构、数据、索引、外键,一步到位的方法不存在,至少不能靠单条CREATE TABLE实现。我推荐的组合是两步走:
CREATE TABLE user_account_dev LIKE user_account; INSERT INTO user_account_dev SELECT * FROM user_account; -- 如需外键,执行后手动ALTER TABLE添加第一步用LIKE拿到完整结构,第二步用INSERT SELECT把数据灌进去,两步执行完基本就是一张和源表一致的复制表。如果数据量很大,INSERT SELECT可以分批执行,比如按id范围分多次插入,避免一次锁住源表太长时间。
三张表复制方式对比:
| 方式 | 复制结构 | 复制数据 | 复制索引 | 复制自增值 | 复制外键 |
|---|---|---|---|---|---|
| CREATE TABLE ... LIKE | 是 | 否 | 是 | 否 | 否 |
| CREATE TABLE ... AS SELECT | 部分推断 | 是 | 否 | 否 | 否 |
| LIKE + INSERT SELECT | 是 | 是 | 是 | 否 | 否 |
从表格可以看出,没有一种方式能完美复制外键,所以凡是涉及表复制的操作,最后都要检查外键约束,这是最容易被忽略的一步。
4. 列数据类型选型:从整数到日期时间的实用对照
列数据类型选型是整个建表过程里最考验经验的环节。选错类型不像语法错误那样立刻爆出来,而是会在数据量大了之后慢慢出问题:查询变慢、存储膨胀、精度丢失、排序异常。这一节我把常用的数据类型过一遍,按数值、字符串、日期时间三个维度讲清楚选型逻辑。
4.1 数值类型:整数、小数和金额到底该怎么选
整数类型按存储字节数和取值范围区分:
| 类型 | 字节数 | 有符号范围 | 常见用途 |
|---|---|---|---|
| TINYINT | 1 | -128 ~ 127 | 状态码、开关 |
| SMALLINT | 2 | -32768 ~ 32767 | 中范围数量 |
| MEDIUMINT | 3 | -8388608 ~ 8388607 | 中范围ID |
| INT | 4 | -2147483648 ~ 2147483647 | 常规ID、计数 |
| BIGINT | 8 | -9223372036854775808 ~ 9223372036854775807 | 大表主键、时间戳 |
主键建议用BIGINT UNSIGNED,尤其是有分库分表预期的场景,单表数据量过亿很常见,INT到21亿就接近上限了,到时再改造主键类型代价极大。状态码、性别、是否删除这些字段用TINYINT就够,一个字节省不了多少空间,但语义清晰。
小数类型必须重点说。金额、汇率、重量这些对精度敏感的数据,一律用DECIMAL,不要用FLOAT或DOUBLE。FLOAT和DOUBLE是浮点数,存在精度误差,比如0.1在二进制里无法精确表示,累加之后误差会越来越大。订单金额用FLOAT存,初期看不出问题,月底对账时差个几块钱就有的折腾了。
DECIMAL(10,2)表示最多10位数字、小数部分2位,整数部分最多8位,也就是最大99999999.99,这个精度对绝大多数业务的金额字段都够用。DECIMAL本质是字符串存储,计算时由MySQL做定点运算,不会丢精度,代价是比浮点数慢一点点,但为了数据准确完全值得。
还有一个细节,MySQL 8.0.17以后废弃了整数显示宽度,也就是以前写的INT(11)、TINYINT(4)这种写法不再有实际作用,虽然不报错但纯属多余。直接写INT、TINYINT就好,别给自己加戏。
4.2 字符串类型:CHAR和VARCHAR的取舍,以及TEXT的定位
字符串类型最常用的是CHAR和VARCHAR,两者区别在于存储方式:CHAR(N)是定长,声明多长就占用多长,不足部分用空格填充;VARCHAR(N)是变长,实际存多长就占用多长,另外加1到2字节记录长度。
CHAR适合存长度基本固定的数据,比如MD5、手机号、状态码,定长存储避免了碎片,访问速度也稳定。VARCHAR适合存长度变化大的数据,比如用户名、邮箱、地址,变长存储能显著节省空间。
注意VARCHAR声明的是字符数不是字节数。在utf8mb4字符集下,一个汉字占4字节,VARCHAR(255)最多能存255个汉字,占用最大1020字节。InnoDB的索引键最大是3072字节,所以VARCHAR(255)做索引没问题,但如果声明VARCHAR(768)再建索引,就超了。长文本字段如果需要索引,要么改成前缀索引,要么考虑用生成列。
CHAR和VARCHAR还有一个差异:CHAR在存储时会去掉尾部空格,VARCHAR会保留。这意味着如果你往CHAR(10)里存“abc”,取出后是“abc”,尾部空格被清了;但如果业务数据本身依赖尾部空格,就不要用CHAR。
TEXT和BLOB要慎用。TEXT是变长文本类型,最大64KB,适合存文章、评论这类长内容。但它有几个限制:不能直接建普通索引(只能前缀索引)、排序和GROUP BY时会有临时表落盘的风险、刚插入时性能正常但更新时因为行变长会产生碎片。能用VARCHAR解决的业务尽量用VARCHAR,TEXT留给自己真实的长文本场景。
4.3 日期时间类型:DATETIME和TIMESTAMP,选错会出大问题
MySQL 8提供了DATE、DATETIME、TIMESTAMP三种常用日期类型。DATE只存年月日,DATETIME存年月日时分秒,范围从1000年到9999年,TIMESTAMP也是年月日时分秒,但范围只有1970年到2038年。
这个差异非常关键。TIMESTAMP的范围上限是2038年1月19日,很多老系统在2038年临近时会开始出现时间溢出。如果你开发的是长期维护的业务系统,比如金融、政务、保险,日期字段一律用DATETIME,不要用TIMESTAMP。
TIMESTAMP还有一个特性,它会跟随数据库时区自动转换。存入时从会话时区转成UTC,取出时再转回会话时区。如果应用服务器和数据库服务器时区不一致,或者迁移云主机时没注意时区,时间数据就会出现偏差。DATETIME则不做任何转换,存入什么就是什么,不受时区影响,这也是我推荐DATETIME的原因。
设置默认值时,MySQL 8推荐写法:
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPcreated_at在插入时自动取当前时间,updated_at在更新时自动刷新。这个组合几乎是建表标配,大部分业务表都该有这两个字段。MySQL 8.0.13开始,DATETIME还支持用括号表达式做默认值,比如DEFAULT (DATE_ADD(NOW(), INTERVAL 1 DAY)),灵活性更高,但日常用得不多。
5. 列属性:默认值、约束和自增的细节坑
列属性决定了一列的约束行为,包括是否允许为空、默认值是什么、是否自动递增、是否有CHECK约束。这些属性和数据类型配合起来,构成了完整的表规则。很多建表问题不是语法错误,而是属性设置不合理,导致应用层要额外写很多判断代码。
5.1 主键和AUTO_INCREMENT:自增列在MySQL 8里的行为变化
InnoDB是索引组织表,数据按主键顺序组织存放。有显式主键时,表结构清晰;没有主键时,InnoDB会挑第一个非空唯一索引作为主键,实在没有就生成一个6字节的隐藏主键。隐藏主键对用户不可见,但会占用额外空间,而且没有任何业务意义。
我见过一张没有主键的日志表,数据量几千万,偶尔做DELETE和UPDATE性能极差,因为没有稳定主键,条件扫描效率很低。强烈建议每张表都显式定义主键。
自增列用AUTO_INCREMENT关键字,有几个硬性规则:一个表只能有一个自增列;自增列必须是索引;自增列必须是数值类型。常见写法:
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', PRIMARY KEY (id)MySQL 8对自增的一个重大改进是自增值持久化。在MySQL 5.7及以前,自增计数保存在内存里,重启后会根据当前最大ID重新计算,如果删过最大ID的数据,重启后自增可能从被删的ID继续,造成主键复用。MySQL 8把自增值写入了重做日志,重启后不会回退,这个坑算是彻底解决了。
事务回滚时自增ID不会回退,所以自增ID跳号是正常现象。不要试图让ID完全连续,那是和数据库设计原理较劲。
5.2 NOT NULL和DEFAULT:能非空就非空,默认值要合理
建议建表时把能NOT NULL的字段都写成NOT NULL,并给一个合理的默认值。原因有三:一是不允许NULL可以提高查询效率,索引里排除NULL的判断更简单;二是避免应用层到处判空;三是不为NULL的语义更明确,字段要么有值,要么有默认值,不会有模棱两可的状态。
比如用户昵称,如果允许NULL,代码里就要判断nickname != null,否则报空指针。但如果你设置DEFAULT '',取出来就是个空字符串,展示逻辑可以统一处理。
默认值本身也有语法细节。MySQL 8.0.13以后,默认值支持表达式,除了CURRENT_TIMESTAMP这类关键字,还能用括号表达式,比如DEFAULT (UUID())或者DEFAULT (DATE_ADD(NOW(), INTERVAL 1 DAY))。注意表达式要加括号,不加括号的写法会报语法错误。
这里要专门提醒:MySQL 8的默认sql_mode包含STRICT_TRANS_TABLES,也就是严格模式。在严格模式下,插入非法日期、超长字符串、不符合默认值要求的数据都会直接报错,而不是像5.7以前那样只发警告然后截断。很多老项目迁移到MySQL 8后突然插入失败,就是这个原因。排查时先查一下sql_mode,再检查应用程序插入的数据是否规范。
5.3 CHECK约束:8.0.16之后才真正执行,生成列也很实用
很多从MySQL 5.6时代过来的开发都有个印象,CHECK约束写了也白写,因为MySQL解析后直接忽略。这个情况在MySQL 8.0.16之后彻底变了,CHECK约束开始强制执行。
CREATE TABLE user_profile ( user_id BIGINT NOT NULL, age TINYINT UNSIGNED NOT NULL, email VARCHAR(100), CONSTRAINT chk_age CHECK (age BETWEEN 0 AND 150), CONSTRAINT chk_email_format CHECK (email IS NULL OR email LIKE '%@%') );这样的约束写在表结构里,应用层即使漏了校验,数据库也会兜底拦截非法数据。不过要注意,CHECK约束的表达式要求是确定性函数,不能用NOW()这种随执行时间变化的函数,也不能用存储过程或自定义函数。
生成列是另一个实用属性,它不需要应用层计算,由MySQL根据其他列自动生成:
price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, total_amount DECIMAL(10,2) GENERATED ALWAYS AS (price * quantity) STORED生成列分VIRTUAL和STORED两种。VIRTUAL不占实际存储,每次查询时现算,适合计算量大但不频繁访问的字段;STORED会真实落盘,适合经常用来查询过滤的字段,比如订单总价、优惠后价格之类。如果这个生成列还要建索引,那就选STORED,因为虚拟列建索引需要MySQL支持二级索引,虽然8.0支持,但性能上还是STORED更直观。
5.4 列级字符集与排序规则:混合字符集是乱码和慢查询的根源
列级别可以单独指定字符集,列没指定时继承表的默认字符集,表没指定时继承实例默认字符集。这个继承链平时没问题,但一旦某列被显式指定成不同字符集,就会埋雷。
比如一张表默认utf8mb4,有个历史遗留列是latin1,数据本身是英文和数字,看着没问题。但如果你把这一列和其他utf8mb4列做JOIN或字符串比较,MySQL会对其中一个做字符集转换,转换不仅带来性能开销,还可能因为编码差异导致匹配不上,或者在大数据量下索引失效。
所以建表时尽量统一字符集,整个库、所有表、所有列都使用utf8mb4,不给自己留例外。如果真有兼容老数据的场景,在查询时显式用CONVERT函数转换,而不是在表结构里混合字符集。
排序规则决定字符串比较怎么排。utf8mb4_0900_ai_ci是默认的,大小写不敏感、口音不敏感,日常业务足够。如果业务需要精确区分大小写,比如存储验证码、序列号,可以指定utf8mb4_bin,它会按二进制精确比较,速度也更快。如果只是部分字段需要区分大小写,就在列级别指定,不影响全表。
6. 建表现场排错:我在生产环境踩过的5个坑
建表SQL看着简单,但真正上线时踩过的坑往往不在语法本身,而在环境差异和数据行为。这一节整理几个我实际遇到过的问题,当作避坑实录分享出来,能帮助读者省不少排查时间。
6.1 表名大小写导致“表不存在”
问题现象:应用日志报ERROR 1146 Table doesn't exist,但用客户端工具看表明明在。排查发现本地开发和线上环境的lower_case_table_names参数不一致。
这个参数在MySQL 8里默认是0,即区分大小写,但Windows和macOS上默认是1,即不区分。如果你在本地建了一张UserAccount,代码里查user_account,本地环境正常,部署到Linux后直接报表不存在。
关键点:lower_case_table_names在初始化实例后修改非常麻烦,需要重启并且可能导致表名识别混乱。最省心的方案是统一规范,建表全部小写下划线,从根上规避这个问题。跨环境部署前也建议用SHOW VARIABLES LIKE 'lower_case_table_names'检查一遍。
6.2 临时表和普通表同名,数据被默默改到临时表
问题现象:某应用晚上跑批,跑完发现部分数据没记录。排查半天发现代码里创建了临时表tmp_user,而线上刚好也有一张普通表叫tmp_user。在批处理会话里创建的临时表遮蔽了同名的普通表,所有INSERT和UPDATE都写进了临时表,会话结束临时表删除,数据直接消失。
这个坑的隐蔽性在于SHOW TABLES看不到临时表,只有SHOW CREATE TABLE能看到TEMPORARY关键字。排查时如果怀疑当前连接有临时表,执行SELECT CONNECTION_ID()确认会话,再查information_schema.INNODB_TEMP_TABLE_INFO。日常规范里应该规定临时表命名带tmp_前缀并加上随机数后缀,或者统一建在单独的临时库中,避免和业务表混淆。
6.3 CTAS复制表后索引和默认值全丢,查询慢成爬
问题现象:给一张上亿的表做数据归档,用CREATE TABLE archive_data AS SELECT * FROM source_table生成历史数据表,结果查询历史数据时慢到无法接受。
原因是CTAS复制出来的表没有任何索引,主键也没了,默认值也丢了,相当于一张“裸表”。对比LIKE方式,CTAS本质上更像“把查询结果导出成表”,而不是“复制表结构”。如果业务需求是保留完整结构和索引,一定要用LIKE加INSERT SELECT的组合。如果用CTAS是图省事,那后续至少要把索引和主键手动补上。
我自己的习惯是:任何表复制操作执行完之后,至少检查三个东西——主键、常用查询条件的索引、外键。一张表上线后没有主键等于给自己埋定时炸弹。
6.4 时间列默认值报语法错误
问题现象:建表语句里写created_at DATETIME NOT NULL DEFAULT NOW(),执行报语法错误。MySQL 8对默认值表达式的语法有要求,CURRENT_TIMESTAMP可以直接写,但其他函数写默认值时需要用括号,比如DEFAULT (NOW())。
更常见的写法是用DEFAULT CURRENT_TIMESTAMP,这个从5.6开始就支持,也是官方推荐。另一个容易踩的坑是DATETIME列没有加NOT NULL就直接配DEFAULT CURRENT_TIMESTAMP,严格模式下行为会变得很微妙,因为NULL值得默认值可能在插入时被覆盖,也可能不生效,不如直接写成NOT NULL DEFAULT CURRENT_TIMESTAMP,语义干净。
6.5 严格模式下插入失败
问题现象:老应用在MySQL 5.7上跑得好好的,迁移到MySQL 8后,某些INSERT语句开始报错,错误信息明确指到某个字段超长或日期非法。
原因很可能是MySQL 8的默认sql_mode包含了STRICT_TRANS_TABLES,对数据做严格校验。5.7有些情况下只是warning,8.0直接error。遇到这种问题,先在数据库执行SELECT @@sql_mode,看是否有STRICT_TRANS_TABLES。处理方式有三种:修正应用程序插入的数据、调整字段长度、确认业务确实需要宽松行为后再调整会话级sql_mode。但我不建议为了兼容老代码全局关闭严格模式,那会让脏数据悄悄混进来,后面清洗成本更大。
加上一个快速排查表,方便日常对照:
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| CREATE TABLE报语法错误 | 表名或列名用了保留字 | 用反引号包住或改名 |
| 插入数据出现乱码 | 表、列字符集不一致 | 统一utf8mb4 |
| 自增ID跳号 | 事务回滚、插入冲突 | 正常现象,不必处理 |
| CTAS生成的表查询很慢 | 没有索引和主键 | 改用LIKE+INSERT SELECT或补索引 |
| 时间默认值报错 | 默认值表达式没加括号 | 统一用CURRENT_TIMESTAMP |
最后聊一点我个人的体会。建表这件事,看起来是所有SQL操作里最简单的,但它决定了后面所有SQL的性能和稳定性。与其上线后不断ALTER TABLE打补丁,不如在最开始就把引擎、字符集、数据类型、约束一次想清楚。我给自己定了一份建表检查清单:表名统一小写下划线带业务前缀;显式写InnoDB和utf8mb4;一定有主键,用BIGINT UNSIGNED自增;能NOT NULL的字段都加默认值;金额用DECIMAL,时间用DATETIME;业务规则能用CHECK表达就写进表结构。有了这份清单,新表几乎不用返工。如果你最近也在折腾MySQL 8的建表,希望能从这篇笔记里找到一些参考,少踩几个我已经替你踩过的坑。