ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL 8建库改库删库实战:字符集、权限与生命周期管理要点

MySQL 8建库改库删库实战:字符集、权限与生命周期管理要点

接手一个新项目,第一件事往往是建库;项目下线,最后一件事往往是删库。这话听起来像废话,但真做起来比很多人想得要麻烦得多。MySQL 8的CREATE DATABASE、ALTER DATABASE、DROP DATABASE三条语句,语法上看起来和5.7差不多,可一旦把字符集、排序规则、权限模型这些底层逻辑变化叠加进去,同一套写法在不同版本里的表现可能完全不同。这篇文章就围绕这三条语句展开,把我实际建库、改库、删库过程中踩过的坑和验证过的结论都梳理一遍,适合刚接触MySQL 8的开发者,也适合正在做存量库版本升级的运维同学参考。

1. MySQL 8在数据库管理语句上到底改了什么

1.1 从5.7到8.0,容易被忽视的变化清单

先泼一盆冷水:如果你只用5.7的思维来看8.0的数据库管理语句,大概率会在两个地方翻车——一个是字符集的默认行为,另一个是权限校验的严格程度。

先看最容易感知到的变化。MySQL 8.0的CREATE DATABASE、ALTER DATABASE、DROP DATABASE三句话,从语法关键字上看和5.7几乎没有区别:

CREATE DATABASE [IF NOT EXISTS] db_name [create_option] ... ALTER DATABASE db_name [alter_option] ... DROP DATABASE [IF EXISTS] db_name

差别藏在细节里。MySQL 5.7时代,默认字符集是latin1,排序规则是latin1_swedish_ci;到了MySQL 8.0,默认值直接改成了utf8mb4和utf8mb4_0900_ai_ci。也就是说,你如果不显式指定字符集,8.0建出来的库默认就是utf8mb4,这本身是好事,但很多从5.7迁移过来的旧脚本里可能写了DEFAULT CHARACTER SET latin1,那就需要逐条确认。

另一个容易被忽略的点是权限。MySQL 8.0对SHOW DATABASES的权限处理更严格了,某些账号即使能看到库名,也可能因为缺少对应库的权限而在实际连接时报Access denied。我遇到过一种情况:业务账号有全局的CREATE权限但没有某个具体库的任何权限,建库语句能执行,但建完以后连USE都进不去,排查半天才发现是权限粒度的问题。

还有个更隐蔽的点:CREATE DATABASE IF NOT EXISTS在没有IF NOT EXISTS的情况下、目标库已存在时会直接报错,这个行为一直如此;但加上IF NOT EXISTS以后,会产生一个warning而不是错误,这个warning可以通过SHOW WARNINGS查看。在自动化脚本里,如果没处理warning,排查问题时容易漏掉关键信息。

1.2 为什么字符集默认值变了,影响到底有多大

很多人觉得字符集默认值变了就变了,反正utf8mb4比latin1更通用。但问题在于,默认字符集改变不是单纯“换个编码”,它会顺着数据库、表、列一路向下传递,最终影响存储空间、索引长度、排序行为,甚至影响主从同步的判定。

举个例子,老项目有一张业务表,列定义里没有显式写字符集,完全依赖建库时的默认值。5.7时代建库默认latin1,表也继承成latin1;如果升级MySQL后直接把这套旧库的建表语句拿去新环境执行,而新环境默认是utf8mb4,那么同样一个VARCHAR(100)列,存储同样的中文字符,占用字节数完全不同。更麻烦的是,如果两张表字符集不一致,做JOIN时可能直接抛Illegal mix of collations错误。

所以我的习惯是:在所有建库语句里显式写明CHARACTER SET和COLLATE,哪怕是打算用默认值也要写出来。这看起来多写几个单词,实际上是在给未来减少麻烦——至少任何人看建库语句,都能一眼知道你当时是怎么想的。

2. CREATE DATABASE:一条语句背后的完整决策链

2.1 语法全解与每一个可选参数的用途

CREATE DATABASE在8.0里的完整语法长这样:

CREATE DATABASE [IF NOT EXISTS] db_name [CHARACTER SET charset_name] [COLLATE collation_name] [DEFAULT ENCRYPTION='Y' | 'N']

最后这个DEFAULT ENCRYPTION是8.0.13以后才有的,它控制这个库以后新建表时是否默认开启页级加密(InnoDB表空间加密)。如果你的数据有合规要求,或者整个实例都要求加密,建议在库级别就把它定下来。这个参数只在初始化时生效,不代表后面不能改,库已经存在之后也可以用ALTER DATABASE调整,但提前定好总比事后再补强。

这里要特别提醒一个细节:CHARACTER SET和COLLATE是可以只写一个的。如果你只写CHARACTER SET utf8mb4,MySQL会自动选择该字符集对应的默认排序规则;如果你只写COLLATE utf8mb4_bin,MySQL会自动匹配该排序规则对应的字符集。但“自动匹配”不等于“一定是你想要的那个”,尤其是服务端字符集比较复杂时,宁可两个都写全。

还有一点:数据库名不是随便起的。MySQL 8.0中有一些保留字,直接作为库名会报语法错误,必须用反引号括起来才行。比如CREATE DATABASE order会报错,你得写成:

CREATE DATABASE `order` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

我建议所有自动化脚本里都统一加反引号,这样即使未来遇见保留字也不会炸。

2.2 字符集与排序规则选型:从utf8mb4到utf8mb4_0900_ai_ci

这是建库语句里最重要的决策点,值得单独说。MySQL 8.0默认的utf8mb4_0900_ai_ci是基于Unicode 9.0的排序规则,它和我们熟悉的utf8mb4_general_ci、utf8mb4_unicode_ci都不是一回事。

它们之间的区别,用最简单的话说:

排序规则特点适用场景
utf8mb4_general_ci老的默认规则,排序比较粗糙,速度快老项目、对排序精度要求不高的场景
utf8mb4_unicode_ci基于Unicode标准,比general_ci更准确通用业务库
utf8mb4_0900_ai_ciMySQL 8.0默认,基于Unicode 9.0,支持更完整的语言排序和重音不敏感新项目、8.0环境下的首选

0900_ai_ci里的ai表示accent insensitive(重音不敏感),ci表示case insensitive(大小写不敏感)。如果你的业务对大小写和重音敏感,可以使用utf8mb4_0900_as_cs(as = accent sensitive,cs = case sensitive)。

选择顺序上,我一般遵从三条原则:

  • 新库、新项目,MySQL 8.0环境直接用默认的utf8mb4_0900_ai_ci,别折腾。
  • 需要和5.7旧实例做数据同步或逻辑备份恢复,选utf8mb4_general_ci或utf8mb4_unicode_ci,因为老的排序规则在两端行为更一致。
  • 唯一性约束、分区、主从版本混杂的实例,排序规则最好统一,否则同步链路容易报Illegal mix of collations。

如果你在建库时贪省事不写排序规则,MySQL就按默认来,一旦库建好了,后面再想改排序规则,连表带字段都得动一遍,工作量翻好几倍。这是我在实际项目中交过的学费。

2.3 实际建库时容易被忽略的三个细节

先说第一个:CREATE DATABASE执行成功后,对应的磁盘目录是什么时候创建的。在InnoDB和默认配置下,库名会映射到数据目录下的一个子目录,但MySQL 8.0引入数据字典后,库的存在性和文件系统目录不是强绑定的。某些异常情况下,你能在数据字典里看到这个库,实际目录却不存在,连接时报Can't create database之类的错误。遇到这种问题,不要急着重建,优先检查datadir对应目录是否存在、权限是否正确。

第二个细节是IF NOT EXISTS的“副作用”。我在自动化交付脚本里经常这样写:

mysql -e "CREATE DATABASE IF NOT EXISTS app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"

如果库已经存在,但字符集不是utf8mb4,这个语句不会报错,也做不了任何修改。脚本看起来“执行成功”了,实际上库的字符集还是旧的。所以自动化脚本里,建库之后最好加一个校验步骤,查一下information_schema.SCHEMATA里的DEFAULT_CHARACTER_SET_NAME,确认和预期一致再进入下一步。

第三个细节是关于大小写敏感。lower_case_table_names参数在MySQL 8.0里只能在初始化时设置,不能动态修改。它影响的不仅是表名,也影响库名的存储和比较。如果你的实例配置了这个参数,建库时库名最好全部用小写,否则后续在Linux平台和Windows平台之间做迁移时,路径匹配可能出问题。这种问题在开发环境不出现,一到跨平台演练时就暴露。

3. ALTER DATABASE:能改什么、不能改什么,边界要清楚

3.1 修改字符集和排序规则的完整操作

ALTER DATABASE最常见的用途就一个:修改数据库的默认字符集和排序规则。语法是:

ALTER DATABASE db_name [CHARACTER SET charset_name] [COLLATE collation_name] [DEFAULT ENCRYPTION='Y' | 'N']

举个例子,有一个旧库当时用utf8mb4_general_ci建的,新业务需要重音不敏感的比较,就可以执行:

ALTER DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

这里必须说清楚一个边界:这个语句修改的是“数据库级别的默认值”,它只影响修改之后新建的表和字段,已经存在的表不会跟着变。已经存在的表的字符集,是在建表时从当时的库默认值里继承下来的,等于已经固化到表定义里了。想让存量表也一起变,只能对每张表单独执行ALTER TABLE。

有时候你执行了ALTER DATABASE,然后建了一张不带字符集定义的新表,结果一查发现还是老字符集。先别慌,大概率是建表时你已经显式写了字符集,或者这张表的定义里缓存了旧值。查看一张表当前实际用的字符集,用这个语句最直接:

SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'app_db';

3.2 为什么没有RENAME DATABASE

很多从其他数据库迁移过来的同学会下意识找RENAME DATABASE,MySQL直到8.0都没有提供这个能力。官方不做的原因也好理解:数据库名下挂着一堆表、视图、存储过程,改库名等于把所有对象的元数据路径都改一遍,还要处理视图和存储过程里的限定名引用,事务性和一致性都非常难保证。官方推荐的做法是先建新库,然后把旧库的表结构、数据搬过去,最后删旧库。本质上是“复制-校验-下线”三步走,而不是一条命令改名。

实际迁移的时候,如果实例支持,用RENAME TABLE逐表挪非常方便,因为RENAME TABLE本身处理了表数据和字典信息,比“导出导入”快很多。伪代码如下:

CREATE DATABASE app_db_2025 CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; RENAME TABLE app_db.user TO app_db_2025.user; RENAME TABLE app_db.order TO app_db_2025.order;

如果只是临时把某个库挪到另一个实例,用逻辑备份工具导一遍再导进新库,也是一条路,只是速度慢一点。需要注意的是,视图和存储过程不会跟着RENAME TABLE迁移,需要单独用导出的SQL重建。

3.3 线上改库的注意事项

线上执行ALTER DATABASE比CREATE DATABASE风险高,因为此时库里大概率已经有业务在跑。改库级别的默认字符集虽然不会立刻影响存量表,但会造成一个“新旧定义不一致”的窗口期:旧表沿用老排序规则,新表用新排序规则。两个集合一旦发生关联查询,就可能出现Illegal mix of collations报错。

所以在线上执行之前,我的操作清单通常是:

  • 先确认存量表里有没有字符集和库级默认值不一致的。用information_schema查一遍,做到心里有数。
  • 评估业务对新建表字符集的新旧依赖。如果新老表要频繁联表,建议把存量表的ALTER TABLE也排进变更窗口,一次性解决。
  • 尽量避免业务高峰执行,因为ALTER TABLE重建大表很耗时,而且可能产生锁等待。
  • 修改后观察一段时间,重点看慢查询日志里有没有新增的字符集转换相关告警。

还有一个小坑:ALTER DATABASE改变的不是“数据”,所以它不会写大量的binlog,主从同步不会有压力。但如果你同时改了DEFAULT ENCRYPTION参数,实例里已经开启GTID的主从环境可能会因为元数据更新而产生DDL事件,个别版本下需要确认同步线程没有报错。

4. DROP DATABASE:删库的完整姿势与后悔药

4.1 语法细节与权限模型

DROP DATABASE是三条语句里最危险的,语法却最简单:

DROP DATABASE [IF EXISTS] db_name;

在MySQL 8.0里,删库就是删库,没有RESTRICT或CASCADE这类参数可选,也不会问你“确认吗”。执行成功,库下的所有表、视图、存储过程、触发器全部清除,数据目录里对应的表空间文件也会被删除。这个行为和其他某些数据库的“删除标记”机制完全不同,MySQL走的是真删路线。

权限方面,执行DROP DATABASE至少需要该库的DROP权限。注意,MySQL 8.0中,只有全局DROP权限(DROP ON *.*)或对应库的DROP权限(DROP ON db_name.*)才能执行。一个容易踩的坑是:某些账号看起来有很高的全局权限,但实际是ALL PRIVILEGES ON db_name.*这种库级授权,此时删除其他库就会失败。我遇到过同事在清理临时库时,用了一个只授权了特定库的账号去跑清理脚本,结果脚本对不存在的库报权限不足,排查半天才想起来是账号权限范围不对。

另外,DROP DATABASE会返回影响的行数,这个数字表示删除了多少张表,不是删除了多少行数据。自动化脚本里用这个返回值判断成功与否是可以的,但不要把它理解成“删了几行数据”。

4.2 误删之后的恢复思路

讲实话,DROP DATABASE执行后,最靠谱的后悔药只有一种:备份。不管是全量备份还是某次逻辑备份,只要有备份,恢复路径就是清晰的。没有备份的情况下,只能靠binlog做基于时间点的恢复,但DROP DATABASE本身在binlog里是一条事件,恢复的时候通常要恢复到删除前一刻,精确性取决于binlog的保留长度和GTID的连续性。

我自己见过的比较有效的恢复操作,大致是这么几条:

  • 立即把实例设为read_only或临时断开业务写入,防止后续事务继续覆盖数据。
  • 确认binlog存在,然后找一台临时实例把备份恢复到删除前的位置。
  • 用mysqlbinlog解析相关binlog,确认DROP DATABASE事件位置,恢复时跳过这条事件。
  • 恢复之后,核对表数量和关键表的行数,与删除前监控数据交叉验证。

这里有一个很实际的经验:即使有备份,恢复也不是百分百无损失的。比如备份到凌晨,那么凌晨到删除时刻之间的数据,只能通过binlog补齐,而binlog补齐的过程对事务顺序要求很高。所以删库操作我从来都是安排在变更窗口,并且提前验证备份的可恢复性,绝不能只看“备份脚本执行成功”就觉得万事大吉。

4.3 什么时候该用DROP,什么时候别用

不是所有“不要的库”都适合DROP。我的经验是分三种情况处理:

  • 项目彻底下线,数据没有合规留存要求,直接DROP,干净利落。
  • 数据保留30天以上,但应用不再访问,先把库改成只读,整体迁到冷备机再DROP。
  • 只是怀疑某个库没用了,但没有人能确认,别急着删,先加上read_only,挂一个月观察日志,确认无访问后再删。

还有一类常见操作:环境清理时批量删库。我的脚本会先把所有待删库名落到一个文本文件里,逐行执行删除,并在删除前做一次同名库存在性校验。这么做看起来繁琐,但能防止手滑把正在用的库名给匹配进去。

5. 从建库到下线:数据库生命周期的实际管理节奏

5.1 库名命名规范:把规则定在建库之前

回头看,我在项目里关于建库、改库、删库踩过的坑,有一大半都不是SQL语法问题,而是库里没有定好规矩。最典型的例子是:同一个实例上,有人建了pay_service,有人建了PAY_SERVICE,还有人建了payService,三个库实际都是同一套系统在不同阶段遗留的。等到清理的时候,根本分不清哪个是活库。

所以我现在带团队时的做法是:建库之前先定命名规范。库名统一小写字母加下划线,业务域加环境后缀,比如order_service_dev、order_service_prod,严禁在库名里混入版本号和时间戳。这样做的原因是数据库管理语句本身不关心这个名字怎么起,但人肉运维的时候,规范的名字能省下大量确认时间。

另外,库级别也建议标注“负责人信息”,MySQL数据字典里虽然没这个字段,但可以在库描述里用注释体现,或者在配置管理库里记录。我见过一个团队用一张额外的元数据表维护每个库的负责人、业务域、下线时间,配合定时扫描information_schema.SCHEMATA,可以自动发现“无人认领”的库,再进入清理流程。这个方法很土,但非常有效。

5.2 变更窗口与版本兼容性:替换场景下的双库并行策略

数据库迁移、版本升级时,最忌讳的就是“改完马上删旧库”。MySQL 8.0和5.7的字符集、排序规则差异已经在前面提到过,如果直接把生产库RENAME或者DROP,出了兼容性问题都来不及回滚。

稳妥的思路是双库并行:建一个新库,导入旧库的结构和数据,应用层灰度切流量到新库,稳定运行一段时间后再把旧库下线。这个过程里,CREATE DATABASE负责开新路,ALTER DATABASE负责调整字符集参数,DROP DATABASE负责最终清理。三步配合,节奏清清楚楚。

并行期间还要注意一个问题:新库和旧库一旦字符集不同,应用层如果同时读到两边数据做合并,容易出现排序规则冲突。所以并行开始时就要核对两边库级和表级的TABLE_COLLATION是否一致,不一致就必须先统一。我一般会写一个查询脚本,直接输出所有表名和排序规则的比对清单,肉眼扫一遍再决定下一步。

最后再分享一个小习惯:不管建库、改库还是删库,把执行语句写进变更记录,并附上执行前的确认输出。不是为了应付流程,而是万一出问题,你能快速知道某条语句在哪个时间点、由哪个脚本、基于什么判断被执行过。数据库管理语句就那么几条,真正决定成败的关键,往往在语句之外。

我在实际使用中发现,只要把这三条语句的边界、默认行为和字符集逻辑吃透,再配合一套简单的规范,MySQL 8里的数据库生命周期管理比你想象的要轻松得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进