MySQL 5.7 到 8.4:给 InnoDB 旧表的增量升级清单
很多存量项目还保留着 MySQL 5.7 时期的表设计:规则写在服务代码里,字段间关系依赖约定,给索引做调整时只能直接上线观察。迁移到 MySQL 8 并不意味着要重写全部 SQL;更有价值的做法,是先挑那些能减少错误、降低维护成本、又可以逐表落地的能力。
本文只讨论以 InnoDB 为主、目标版本为 MySQL 8.x(8.4 可作为 LTS 落点)的应用库。它不是一份“所有新特性”清单,也不把升级说成没有代价的性能优化。每次 DDL、认证方式和执行计划变化,都应该先在与生产接近的数据和流量条件下验证。
CHECK:把字段关系放回数据库,但先校正版本认知
下面这段订单约束很适合写入表定义:数量必须大于零,剩余数量不能为负,也不能超过原始数量。
CREATE TABLE orders ( |
这不是 MySQL 8.4 才有的能力。MySQL 8.0.16 起才真正实现并执行 CHECK 约束;更早版本曾允许有限的 CHECK 语法,但只解析、不执行。也就是说,如果一套应用仍会连到 5.7,不能因为 DDL 中“写了 CHECK”就以为数据一定安全。官方发布说明明确记录了这一变化。MySQL 8.0.16 release notes
对于开发者,它的收益不是少写一行 if,而是让不同写入入口遵守同一底线:后台任务、管理脚本、批量导入、修复 SQL 和未来新增服务,都无法轻易绕过“剩余数量不能大于原始数量”这种领域事实。
CHECK 的两个容易踩坑的边界
NULL不是FALSE。CHECK条件结果为TRUE或UNKNOWN时都能通过;因此qty、rest_qty是否允许为空,要另外用NOT NULL表达。不能指望CHECK (qty > 0)自动拒绝NULL。- 它不是触发器,也不能查询其他表。 约束表达式不能用子查询、变量、存储函数或非确定性函数;不能拿
NOW()解决“过期时间必须晚于当前时间”这类动态规则。跨表余额、库存并发扣减仍需事务、行锁和业务逻辑协作。
这些限制和约束在写入时的行为可参考 MySQL 8.4 CHECK 文档。
给已经运行的旧表加约束:先查脏数据,再改变规则
直接执行 DDL 不是第一步。先把历史数据中会违反新规则的记录列出来,并决定是修复、归档还是显式允许例外:
SELECT id, qty, rest_qty, status |
确认没有不应保留的异常后,再增加约束:
ALTER TABLE orders |
最后用两条故意失败的写入验证数据库真的在兜底;示例中的数值只用于验证,不要在生产订单表上随意插入:
-- 应报 CHECK constraint 违反错误 |
写入失败时,应用实际会看到什么
对上面的 rest_qty > qty 示例,MySQL 8.0.16+ 的典型返回是:
ERROR 3819 (HY000): Check constraint 'ck_orders_rest_qty' is violated |
服务端不应只把它记录成“数据库异常”。这是一个可以预期的业务拒绝:调用方应该把它转换为参数或状态不合法的结果,并保留约束名、订单主键和请求追踪号;不要通过捕获异常后重试同一条错误写入来掩盖问题。
还要特别区分普通写入和 IGNORE 写入:INSERT IGNORE、UPDATE IGNORE、LOAD DATA ... IGNORE 遇到约束失败时,可能以警告形式跳过出错行,而不是让整个语句报错。批量任务必须检查受影响行数和 SHOW WARNINGS,否则“SQL 执行成功”并不代表数据全部入库。
INSERT IGNORE INTO orders (qty, rest_qty, status) |
下面这些并非都是 MySQL 8 的新能力,但它们往往会在启用新约束、收紧类型或提高并发后更早暴露。异常码适合用来做应用层分类;错误文案会随版本、驱动和语言封装略有差异。
| 场景 | 常见 MySQL 错误 | 应用侧第一反应 |
|---|---|---|
CHECK 约束失败 |
3819,Check constraint ... is violated |
返回可理解的字段 / 状态错误,不盲目重试 |
| 唯一键冲突 | 1062,Duplicate entry |
判断是否为幂等请求;不要把所有重复都当成成功 |
| 外键写入失败 | 1452,Cannot add or update a child row |
检查父记录、写入顺序与事务边界 |
| 非法 JSON | 3140,Invalid JSON text |
拒绝或规范化输入;不要把坏 JSON 悄悄转成字符串 |
| 死锁 | 1213,Deadlock found ... |
对幂等短事务做有限次数、带退避的重试 |
| 锁等待超时 | 1205,Lock wait timeout exceeded |
排查长事务 / 热点行;不要无限重试 |
| 连接耗尽 | 1040,Too many connections |
限流、检查连接池泄漏和慢请求,不是只调大上限 |
1213 和 1205 都不是“数据库坏了”的同义词。对交易类写入,更常见的根因是多个事务以不同顺序锁同一批记录、事务中混入远程调用、或一条范围更新意外扩大了锁定范围。统一锁顺序、缩短事务、让更新条件命中唯一 / 明确索引,通常比把超时时间调得更长有效。
ALTER TABLE 是否真正做到低阻塞,取决于 MySQL 小版本、表结构、约束操作和当前长事务;不要因为它看起来只是“加一个规则”就假设无锁。上线前应在预发实测耗时、观察元数据锁,并为失败或长时间等待准备停止窗口。写操作高峰期尤其应先清理长事务,再安排变更。
从 5.7 开始就值得用的增量能力
JSON 加生成列:保留灵活输入,但别放弃索引和约束
MySQL 5.7 提供原生 JSON 类型和生成列。对扩展字段频繁变化、但又需要按其中少数键查询的业务,这比把 JSON 全部拆散或完全交给应用过滤更平衡。
ALTER TABLE orders |
好处是查询可以走普通索引,JSON 仍能保留非核心扩展字段。代价也很明确:生成列的表达式一旦变成高频更新路径,就会增加写入成本;JSON 里适合索引、参与关联或作为金额 / 状态判断的字段,最终仍应有清晰的类型和约束,不能把 JSON 当成没有治理的“万能字段”。
8.0 带来的开发效率,不只是性能
CTE 和窗口函数:让统计 SQL 少一层临时表和应用循环
以前做“每个用户最近一笔订单”或“按日累计”时,常见写法是嵌套子查询、临时表,甚至把分组后逻辑搬到应用里。8.0 的窗口函数让这类查询更接近业务语言:
WITH ranked_orders AS ( |
优势是可读、可审核,也更容易在同一条 SQL 内完成排名、去重、环比和累计计算。注意它不保证一定更快:窗口函数可能需要排序或临时表,仍要使用真实条件下的 EXPLAIN ANALYZE 验证,而不是只因为 SQL 更漂亮就替换线上关键路径。
EXPLAIN ANALYZE |
EXPLAIN ANALYZE 会实际执行语句来给出执行时间与行数,不能把它当成零成本的“查看计划”按钮;生产大查询应限流、限时或在只读副本 / 预发执行。
函数索引:让表达式查询不必全表扫
MySQL 8.0.13 起可为表达式建立函数索引。例如业务邮箱不区分大小写、又不方便立刻改写全部历史字段时:
CREATE INDEX idx_users_lower_email |
它适合已经稳定、重复出现的表达式;不适合把每个偶发查询都做成索引。索引会占空间、拖慢写入,表达式也需要与查询文本匹配才可能被优化器使用。
不可见索引:先验证“删掉会不会慢”,再真正删
存量库经常有不知道谁在用的索引。直接删除后回滚成本高;不可见索引可以先让优化器默认不再选择它:
ALTER TABLE orders |
这非常适合索引瘦身前的验证,但不是自动化删除工具。需要覆盖关键 SQL、慢查询和报表任务,并注意会话级或全局设置可能允许优化器使用不可见索引。详见 Invisible Indexes。
直方图与执行计划:帮助优化器看见非均匀数据
状态列、地区或业务类型等低基数字段,实际分布往往很不均匀。直方图能帮助优化器估算过滤后的行数:
ANALYZE TABLE orders |
它不是索引,也不会替代组合索引设计;它改变的是估算信息。上线前后都应比对关键 SQL 的实际耗时和扫描行数,因为更“聪明”的估算也可能让某个边缘查询换到不合适的计划。
InnoDB DDL:更容易上线,不等于可以跳过变更管理
MySQL 8 的原子 DDL、InnoDB 在线 DDL 和部分 Instant DDL,减少了过去“建到一半失败,留下半张表”的风险,也让某些新增列 / 索引操作更适合渐进上线。真正对开发有价值的不是记住所有 ALGORITHM,而是把 DDL 当成一项可观测变更:
-- 明确表达希望减少读写阻塞;如果当前操作不支持,MySQL 会拒绝而非静默降级。 |
不同版本和操作对 INSTANT / INPLACE / COPY 的支持范围不同。对涉及大表、索引重建、列位置调整、生成列或约束的 DDL,不应复制上面的参数后直接执行;先查当前版本官方矩阵、在预发复现,并观察复制延迟、长事务和元数据锁。参考 InnoDB Online DDL。
重启、连接数与并发:不要把“默认值”当成容量结论
以前不少人会把“重启后自增 ID 可能变化”当作 InnoDB 的固定规律。它对较早版本的经验有参考价值:计数器曾主要存在内存里,重启后需要重新初始化。但在现代 MySQL 8 的 InnoDB 中,当前最大 AUTO_INCREMENT 计数会写入 redo log 并在 checkpoint 保存到数据字典,正常重启后会恢复这个值。
这不等于 ID 从此连续、也不等于 ID 能承担业务顺序:回滚、失败写入、批量预分配和异常退出都可能留下间隔;异常退出前尚未落盘的已分配值也不能保证绝不复用。因此订单、成交和账务都不应以“自增 ID 必然连续”作为对账或排序依据。自增列更适合作为内部代理键,业务时间与业务流水号应单独记录。
可以先在每个实例上看真实配置,而不是背一套网上参数:
SHOW VARIABLES |
| 项目 | MySQL 8.4 默认值 | 容易被误解的地方 |
|---|---|---|
max_connections |
151 |
是同时客户端连接上限,不是 QPS、TPS 或安全并发数;上调会增加每连接内存和排队压力 |
innodb_thread_concurrency |
0 |
0 表示不人为限制并发进入 InnoDB 的线程,不是“数据库只有 0 个并发” |
transaction_isolation |
REPEATABLE-READ |
默认可避免同一事务内一致读的变化,但锁定范围与查询条件、索引密切相关 |
autocommit |
ON |
单条写入自动提交;多条相关写入必须显式放进短事务 |
官方同样明确:现代多核机器通常不需要先限制 innodb_thread_concurrency;是否限流应该以压测和监控为依据。InnoDB 并发说明 系统变量说明
高频交易写入:隔离级别不是一个全局性能开关
InnoDB 默认是 REPEATABLE READ。对于余额、订单状态或持仓这类需要同一事务内多次读取保持一致视图,或需要防止范围条件被插入新记录影响的流程,应该先保留这个默认值,并让查询走唯一键或合适的范围索引。
对已经验证不依赖范围防幻读、主要做按唯一键的条件更新的高冲突短事务,可以评估在写入会话中使用 READ COMMITTED。它让每次一致读读取更新的快照,并减少一般更新 / 锁定读的间隙锁;代价是幻读可能出现,不能拿它处理需要“先查范围、再假设范围不变”的逻辑。
-- 只在已经完成并发语义验证的写入会话中使用;不要直接改全局默认值。 |
这里的关键不是 READ COMMITTED 本身,而是 UPDATE 把状态和余量判断放在同一条、命中主键的原子语句中:affected_rows = 1 才表示扣减成功。若需要同时写订单、余额、冻结记录和账本,应把这些写入放入一个短事务,并对 1213 做有上限的幂等重试。SERIALIZABLE 不应作为“交易更安全”的默认替代,它通常会显著增加锁竞争;READ UNCOMMITTED 也不适合作为账务和订单数据的隔离级别。关于 READ COMMITTED 的间隙锁边界见 InnoDB 隔离级别文档。
字段类型:新增能力的正确用法比“用了新类型”更重要
MySQL 8 并没有一个通用的原生 UUID 字段类型;INVISIBLE 也是列属性,不是隐藏敏感数据的安全机制。更实用的是根据数据语义选型:
| 数据 | 建议定义 | 适合的场景与限制 |
|---|---|---|
| 金额、价格、费率 | DECIMAL(24, 8) 等按业务精度确定的定点数 |
不用 FLOAT / DOUBLE 保存需精确对账的金额;精度和币种最小单位要先约定 |
| 高频事件时间 | DATETIME(6) |
保存微秒级事件时间;通常统一按 UTC 写入。它不是新增类型,但对撮合、成交、审计排序很实用 |
| 外部 UUID | BINARY(16) |
用 UUID_TO_BIN() 写入、BIN_TO_UUID() 读取,比 36 字符文本更紧凑;不要把 UUID 是否递增当作业务时间顺序 |
| 扩展属性 | JSON + 已索引的生成列 |
JSON 适合低频变化的附加信息;筛选、关联、金额和状态仍应落为具名列 |
| 大文本 / 二进制 | TEXT / BLOB |
大字段不宜被无差别读出;要配合对象存储、长度限制和访问路径设计 |
暂不在 SELECT * 展示的兼容列 |
INVISIBLE 列属性 |
可辅助渐进迁移,但仍会参与主键 / 唯一键比较;不能用于隐藏密码、密钥或权限数据 |
例如使用二进制 UUID 与微秒时间记录外部请求:
CREATE TABLE order_requests ( |
JSON、BLOB、TEXT 和 GEOMETRY 的默认值有额外语法要求:需要写成表达式,即使值本身是字面量。数据类型默认值说明
经典连接编码问题:库是 utf8mb4,为什么仍会乱码或比较报错
这是存量 MySQL 项目最容易被低估的一类问题:数据库、表和列都可能是 utf8mb4,但客户端连接建立时声明的字符集不同,字符串在传输或比较时仍会被错误转换。更进一步,即使字符集同为 utf8mb4,不同的 collation(排序规则) 也可能改变等值比较、唯一性判断和排序结果。
MySQL 至少有四层需要分别确认:服务端默认、数据库默认、表 / 列定义,以及当前会话连接。不要只看建库语句就下结论。
-- 1. 当前连接实际协商的字符集与排序规则 |
连接协商不一致时,典型现象不是只有“中文显示成乱码”。也可能出现表情无法写入、字符串比较结果意外、唯一索引判断与产品预期不符,或拼接 / JOIN 时出现:
ERROR 1267 (HY000): Illegal mix of collations ... for operation '=' |
应用应在每个新建连接由驱动 / 连接池统一声明 utf8mb4,不要依赖开发机或服务器默认值。若需要临时验证,会话中可以执行:
SET NAMES utf8mb4; |
这条语句会同时设置 character_set_client、character_set_connection、character_set_results,并按该字符集的默认规则设置 collation_connection。若业务需要明确比较规则,也可以在连接建立时指定:
SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci; |
但不要把这当成修复历史乱码的方法:如果旧连接曾用错误编码把字节写入,后续再 SET NAMES utf8mb4 只会让新连接正确,无法自动还原旧数据。修复前应先抽样比对原始字节、应用输入和备份,再选择针对性的转换方案;直接对全表 CONVERT TO CHARACTER SET 可能改变索引长度、排序语义和唯一键冲突结果。
对已经全部升级到 MySQL 8 的新系统,统一使用 utf8mb4 是稳妥基线;utf8 是 utf8mb3 的已弃用别名,不应再作为新库约定。在 5.7 与 8.0 / 8.4 混合复制或灰度期间,不能贸然把列改为只有新版本才识别的 collation;应先确认全部副本、驱动和导入导出工具的兼容性,再安排字符集迁移。 MySQL 字符集总览 SET NAMES 语义
为什么 8.4 仍需要谨慎:新能力也会放大旧问题
8.4 是 MySQL 的 LTS 选择,但“LTS”不等于旧应用天然兼容。升级前至少把下面几项列入检查表:
| 检查项 | 为什么需要看 | 可采取的动作 |
|---|---|---|
| 存量数据质量 | 新 CHECK 会让过去被放行的坏数据在新写入时显形 |
先跑预检 SQL,定义修复和例外规则 |
| 驱动与认证 | 老驱动、语言运行时和旧认证插件可能无法适配默认认证方式 | 在升级前做真实连接回归,不要靠生产报错排查 |
| SQL mode、字符集与保留字 | 更严格的规则会暴露历史 SQL 的隐式依赖 | 用应用回归和慢查询回放验证 |
| 执行计划 | 统计信息、直方图和优化器改进可能让同一 SQL 改走另一条路径 | 对核心 SQL 留基线,升级后用 EXPLAIN ANALYZE 复核 |
| DDL 与复制 | 大表变更、长事务、不同版本副本会让短操作变成长故障 | 先备份、演练回滚、分批变更,并维护版本兼容顺序 |
特别是约束上线,不应该变成“应用把异常吞掉”。对于 INSERT IGNORE、UPDATE IGNORE 或批处理,违反 CHECK 的行可能以警告和跳过形式处理;调用方必须能感知写入数量与异常数量,不能只看 SQL 没有抛错就视为成功。
公开事故复盘:43 秒故障为什么会拖成 24 小时
GitHub 2018 MySQL 集群网络分区是一个很适合作为边界案例的公开事故:一次计划性网络维护造成两个数据中心之间约 43 秒的网络分区,随后引发复制拓扑异常、跨地域写入延迟和恢复决策困难,最终形成约 24 小时 11 分钟的服务降级。GitHub 的事后报告强调,当两个方向都有尚未汇合的写入时,为了保护数据完整性,团队选择向前恢复而不是草率回切。
这个案例不是“用了 MySQL 就会出事故”,也不能拿来类比单机实例。它说明的是:高频写入系统的风险不只在 SQL 本身,还在复制拓扑、故障切换、重试风暴、写入保护和恢复演练。对一般团队而言,可落地的教训是:
- 故障时先限制额外变更和自动写入,避免扩大分叉;
- 让订单写入具备幂等键,重试不会生成第二笔业务动作;
- 监控复制延迟、死锁、连接池等待、长事务和错误码比例,而不是只看 CPU;
- 定期演练“主库不可写、部分副本落后、消息重复投递”的恢复路径。
完整过程可查看 GitHub October 21 post-incident analysis。
一个务实的升级顺序
如果目标是改善旧项目维护,而不是做一次高风险的大迁移,我倾向按下面顺序推进:
- 先盘点版本与连接依赖。 明确所有实例、复制链路、驱动版本和认证方式,准备可恢复备份。
- 挑一张边界清楚的小表试点。 先用
CHECK、生成列或不可见索引治理一个具体问题,保留前后对比和回滚办法。 - 把执行计划验证写进变更流程。 新索引、直方图、窗口函数改写都应有真实数据验证,不只看语法可执行。
- 最后再扩大 DDL 范围。 任何看起来能在线完成的变更,都要把锁等待、复制延迟和长事务纳入发布窗口。
MySQL 8 的价值,不在于让表定义变得更炫,而在于让数据规则、查询意图和变更风险更接近真实业务。CHECK 很适合从订单数量、余额范围、状态组合这类基础规则开始;它能减少“某个入口忘记校验”的问题,但不能替代事务设计、监控、测试和对异常数据的处置策略。
参考资料
- MySQL 8.0.16 Release Notes:CHECK 约束由解析改为实现
- MySQL 8.4 Reference Manual:CHECK Constraints
- MySQL 8.4 Reference Manual:Window Functions
- MySQL 8.4 Reference Manual:WITH / Common Table Expressions
- MySQL 8.4 Reference Manual:InnoDB Online DDL
- MySQL 8.4 Reference Manual:AUTO_INCREMENT Handling in InnoDB
- MySQL 8.4 Reference Manual:Transaction Isolation Levels
- MySQL 8.4 Reference Manual:Character Sets, Collations, Unicode
- MySQL 8.4 Reference Manual:SET NAMES
- GitHub:October 21 post-incident analysis




