MySQL 运维提效与 8.0 新特性实战:最快复制表、分区表、instant DDL 与 EXPLAIN ANALYZE
系列第 9 篇 · 对标《MySQL 实战 45 讲》第 41/42/43 讲 + 8.0 增量延伸
实验环境:云服务器 Ubuntu 24.04 / 8C14G / MySQL 8.0.46,b2 库 t 表 10 万行
引言:给你 10 分钟,把这张表复制到另一个库
运维同学大概率都遇到过这种需求:“帮我把 b2 库的 t 表,原样复制一份到 b2copy 库,数据不能丢,越快越好”。
听起来像一句简单的CREATE TABLE ... SELECT,但真到了 10 万行、100 万行甚至上亿行的量级,复制的姿势直接决定了你今晚几点下班。是mysqldump一把梭?是SELECT ... INTO OUTFILE加LOAD DATA?还是干脆把.ibd物理文件拷走?
本篇不灌鸡汤,全部基于我在真实云服务器(Ubuntu 24.04 / 8C14G / MySQL 8.0.46)上跑出来的实机回显。前半段复盘 45 讲里最贴近日常的三个话题——最快复制表(41)、grant 之后要不要 flush privileges(42)、要不要使用分区表(43),后半段带你把 MySQL 8.0 的几个"让 DBA 失业"的新特性真正跑一遍:invisible index 灰度下线索引、instant DDL 秒加列、EXPLAIN ANALYZE 真实执行剖析、直方图、窗口函数。
所有命令的输出均为实机回显,宁缺毋假。
实验环境
开始之前,先交代清楚实验底座,避免有人拿着结论在自己的 5.5 版本上跑不出来。
| 项目 | 配置 |
|---|---|
| 操作系统 | Ubuntu 24.04 (x86_64) |
| 规格 | 8 核 / 14G 内存 |
| 实验机 | 云服务器(公网 IP124.70.***.***) |
| MySQL 版本 | 8.0.46 |
| 目标库 / 表 | b2 库,t 表,10 万行 |
| 账号 | root(本地执行);一次性实验账号 ua(密码 ExpPass_2026) |
先 sanity 一把,确认表结构和数据量。t 表结构如下,后面所有实验都建立在这张表上:
$ mysql -uroot b2 -e 'select count(*) from t; show create table t\G' 2>&1 count(*) 100000 *************************** 1. row *************************** Table: t Create Table: CREATE TABLE `t` ( `id` int NOT NULL, `a` int DEFAULT NULL, `b` int DEFAULT NULL, `c` varchar(20) DEFAULT 'x', PRIMARY KEY (`id`), KEY `a` (`a`), KEY `ab` (`a`,`b`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci10 万行,主键id,两个二级索引a和联合索引ab(a,b)。这就是我们的"实验白鼠"。
第一部分:最快复制表,三种姿势到底差多少
姿势一:mysqldump 逻辑导出 + 导入
最经典、最通用、跨版本最稳的姿势。导出:
$ time mysqldump -uroot b2 t --single-transaction --set-gtid-purged=OFF > /tmp/t_dump.sql 2>/dev/null; ls -l --block-size=K /tmp/t_dump.sql real 0m0.042s user 0m0.015s sys 0m0.006s -rw-r--r-- 1 root root 2314K Jul 29 12:58 /tmp/t_dump.sql10 万行导出只用了real 0.042s,生成的 SQL 文件2314K(约 2.3MB)。--single-transaction保证了一致性快照,不会锁表;--set-gtid-purged=OFF是我们单机实验,避免 GTID 相关噪声。
导入到新库 b2copy:
$ mysql -uroot -e 'create database if not exists b2copy'; time mysql -uroot b2copy < /tmp/t_dump.sql real 0m1.206s user 0m0.017s sys 0m0.004s导入耗时1.206s。注意 mysqldump 默认会带上建表、加索引、插数据的全流程,索引是在数据导入后再建的,所以导入比导出慢一个数量级是正常的。
姿势二:SELECT INTO OUTFILE + LOAD DATA INFILE
如果你只需要"裸数据"搬运,CSV 文本路线更快。先确认安全目录限制——这是 8.0 的硬约束,绕不开:
$ mysql -uroot b2 -e 'show variables like "secure_file_priv";' 2>&1 Variable_name Value secure_file_priv /var/lib/mysql-files/secure_file_priv被设置为/var/lib/mysql-files/,意味着INTO OUTFILE/LOAD DATA INFILE只能读写这个目录,这是 8.0 的安全默认值,切忌用 root 家目录或/tmp去赌权限。
导出 CSV:
$ time mysql -uroot b2 -e "select * from t into outfile '/var/lib/mysql-files/t.csv'" 2>&1; ls -l --block-size=K /var/lib/mysql-files/t.csv real 0m0.036s user 0m0.004s sys 0m0.001s -rw-r----- 1 mysql mysql 1921K Jul 29 12:58 /var/lib/mysql-files/t.csv导出real 0.036s,比 mysqldump 还快一点,CSV 文件1921K(比 SQL 文件小,因为没有INSERT语句开销)。
导入前先建好同构表(OUTFILE 不含 DDL):
$ mysql -uroot b2 -e 'drop table if exists t_load; create table t_load like t;' 2>&1导入:
$ time mysql -uroot b2 -e "load data infile '/var/lib/mysql-files/t.csv' into table t_load" 2>&1; mysql -uroot b2 -e 'select count(*) from t_load' real 0m0.892s user 0m0.002s sys 0m0.003s count(*) 100000LOAD DATA导入real 0.892s,比 dump 的 1.206s 快约 26%,且导入后校验count(*)=100000,数据一条不少。文本协议少了解析 SQL 的开销,这是它更快的根本原因。
姿势三:物理拷贝(思路,本篇不展开)
还有第三条路:直接拷.ibd文件 +DISCARD/IMPORT TABLESPACE。它最快,但限制也最多(表空间自包含、版本/页大小一致、需要FLUSH TABLES ... FOR EXPORT拿.cfg元数据)。日常跨库复制,逻辑路线足矣;物理拷贝更适合"同版本整机迁移"这种场景,本篇焦点在前面两种。
三种姿势真实耗时对比
| 姿势 | 导出耗时(real) | 产物大小 | 导入耗时(real) | 含 DDL | 跨版本 |
|---|---|---|---|---|---|
| mysqldump | 0.042s | 2314K SQL | 1.206s | 是(自带) | 友好 |
| OUTFILE+LOAD | 0.036s | 1921K CSV | 0.892s | 否(需手动建表) | 需注意字符集 |
| 物理 .ibd 拷贝 | 极快(文件级) | 原表.ibd | 需 IMPORT | 否 | 严格一致 |
结论:10 万行这个量级下,OUTFILE+LOAD 综合最快;如果追求省心、要连带索引和表结构一次带走,mysqldump 依然是最稳的默认值。
第二部分:grant 之后,到底要不要 flush privileges
“我刚GRANT完,是不是得FLUSH PRIVILEGES让它生效?”——这是面试和群里最高频的迷思之一。45 讲第 42 讲把这件事讲透了,我们直接上实机验证。
先建一个一次性实验账号ua,只给 b2 库的 SELECT 权限:
$ mysql -uroot mysql -e 'create user if not exists "ua"@"%" identified by "ExpPass_2026"; grant select on b2.* to "ua"@"%"; show grants for "ua"@"%";' 2>&1 Grants for ua@% GRANT USAGE ON *.* TO `ua`@`%` GRANT SELECT ON `b2`.* TO `ua`@`%`权限已确认:ua只对b2.*有 SELECT。
关键验证:grant 之后,立刻用 ua 新开一个连接去查,要不要 flush?直接试:
$ mysql -uua -pExpPass_2026 b2 -e 'select count(*) from t' 2>&1 | grep -v Warning count(*) 100000新连接立即生效,查到了 100000 行,全程没有执行任何FLUSH PRIVILEGES。
原因很简单:5.7 之后(8.0 更是如此),GRANT/REVOKE/CREATE USER这些账户管理语句,本身就会同时更新内存中的权限缓存和mysql 系统库(如mysql.user、mysql.db)。既然内存已经是最新的,新连接建立时直接读内存就行,根本不需要你手动 flush。
那FLUSH PRIVILEGES什么时候才需要?只有当你绕过 SQL 语句、直接手改mysql系统库表(比如UPDATE mysql.user SET ...)时,内存和磁盘不一致了,才需要用FLUSH PRIVILEGES把磁盘"刷"回内存。正常用GRANT语句,永远不需要。
再来验证 REVOKE 的即时性,把权限收掉:
$ mysql -uroot mysql -e 'revoke select on b2.* from "ua"@"%";' 2>&1然后 ua 再查一次:
$ mysql -uua -pExpPass_2026 b2 -e 'select count(*) from t' 2>&1 | grep -v 'Using a password' ERROR 1044 (42000): Access denied for user 'ua'@'%' to database 'b2'REVOKE 同样即时生效,新连接直接被拒,抛出真实的ERROR 1044 (42000): Access denied。这条回显很关键——它证明权限回收也不是"等下次登录",而是立刻对新建连接生效。
一句话记住:用
GRANT语句管权限,别手贱加FLUSH PRIVILEGES;只有手改系统表才需要 flush。
第三部分:到底要不要上分区表
分区表是"听起来很美、用起来要命"的典型。45 讲第 43 讲的态度很明确:除非你的场景真的命中分区的核心价值(快速 drop 历史数据、分区裁剪缩小扫描面),否则普通表 + 好索引更省心。我们用实机数据说话。
建一张按年份分区的表,4 个分区:p2024 / p2025 / p2026 / pmax:
$ mysql -uroot b2 -e 'drop table if exists tpart; create table tpart(ftime datetime not null, c int, primary key(ftime,c)) partition by range(year(ftime)) (partition p2024 values less than (2025), partition p2025 values less than (2026), partition p2026 values less than (2027), partition pmax values less than maxvalue); insert into tpart values("2024-06-01",1),("2025-03-15",2),("2026-01-10",3),("2026-07-01",4);' 2>&1插入 4 行,分别落进不同年份分区。先看物理文件——每个分区是独立的 .ibd 文件:
$ ls /var/lib/mysql/b2/ | grep tpart tpart#p#p2024.ibd tpart#p#p2025.ibd tpart#p#p2026.ibd tpart#p#pmax.ibd4 个分区,4 个tpart#p#pXXXX.ibd,各自独立。这就是分区表"可按分区管理物理文件"的底层基础。
分区裁剪:查询只扫命中分区
按分区键ftime精确查询:
$ mysql -uroot b2 -e 'explain select * from tpart where ftime="2026-01-10";' 2>&1 id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE tpart p2026 ref PRIMARY PRIMARY 5 const 1 100.00 Using index注意partitions列:只列出了p2026。优化器做了"分区裁剪"(partition pruning),直接跳过另外 3 个分区,扫描面缩小到 1/4,rows 估算为 1。
没有分区键:照样全扫
如果 WHERE 条件里没有分区键ftime,只用了c:
$ mysql -uroot b2 -e 'explain select * from tpart where c=3;' 2>&1 id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE tpart p2024,p2025,p2026,pmax index PRIMARY PRIMARY 9 NULL 4 25.00 Using where; Using indexpartitions列变成了p2024,p2025,p2026,pmax——四个分区全扫。分区在这里完全没帮上忙,反而因为每个分区都要走一遍而增加了管理成本。
drop partition:历史数据秒删
分区表最硬核的卖点,就是删"某个时间块"的数据时不必逐行DELETE(产生大量 undo/redo、锁表久),而是直接把整个分区"物理丢弃":
$ mysql -uroot b2 -e 'alter table tpart drop partition p2024; select count(*) from tpart;' 2>&1 count(*) 3DROP PARTITION p2024后,count(*)从 4 直接变3——2024-06-01那一行随分区整体消失,命令几乎是秒级返回(无需逐行删除)。这对按时间保留日志、订单等"冷数据定期清理"的场景,价值巨大。
上不上分区?给出判断标准
- 该上:超大数据量 + 明确按时间/范围清理 + 查询常带分区键(命中裁剪)。例如日志表、历史订单表。
- 别上:小表、查询基本不带分区键、或者你只是想"显得专业"。分区表有坑——分区数过多影响优化器、某些 DDL 在分区表上行为不同、MDL 锁在分区级操作上也会放大。普通表 + 合理索引,往往更香。
本篇提示:分区裁剪是否成立,完全取决于你的 WHERE 是否带上分区键。没有分区键的查询,分区表反而更慢。
第四部分:8.0 让 DBA"失业"的新特性实战
如果说前三部分是 45 讲的老话题,那 8.0 的新特性就是 DBA 效率的真实跃迁。下面每一个,我都跑了真实回显。
4.1 invisible index:灰度下线索引的正确姿势
线上要下线一个"疑似没人用"的索引,最怕什么?直接DROP INDEX,结果某个隐藏的慢查询瞬间全表扫、CPU 打满、告警炸锅。8.0 的invisible index就是为这个场景生的:把索引设为对优化器"不可见",但物理上还在,随时能恢复。
先看看正常状态下,where a=5000走哪个索引:
$ mysql -uroot b2 -e 'explain select * from t where a=5000;' 2>&1 id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t NULL ref a,ab a 5 const 1 100.00 NULL正常:possible_keys是a,ab,最终key=a,走单列索引a,干净利落。
现在把索引a设为 invisible:
$ mysql -uroot b2 -e 'alter table t alter index a invisible; show index from t where Key_name="a";' 2>&1 Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_type Comment Index_comment Visible Expression t 1 a 1 a A 64897 NULL NULL YES BTREE NO NULLshow index的Visible列变成了NO,索引确实被标记为不可见,但数据文件里它还在。
再看执行计划:
$ mysql -uroot b2 -e 'explain select * from t where a=5000;' 2>&1 id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t NULL ref ab ab 5 const 1 100.00 NULL注意变化:possible_keys里不再出现a(优化器已经"看不见"它了),最终key落到了联合索引ab上——因为ab(a,b)的最左列也是a,仍能服务a=5000,所以查询没退化为全表扫,只是换了个索引。这正是 invisible index 的安全之处:即便你下线错了,业务 SQL 往往还能靠其他索引兜底,而不是瞬间崩。
确认没问题后,恢复 visible:
$ mysql -uroot b2 -e 'alter table t alter index a visible; explain select * from t where a=5000;' 2>&1 id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE t NULL ref a,ab a 5 const 1 100.00 NULLkey立刻回到a。灰度下线索引的标准流程:先ALTER ... INVISIBLE观察一阵(慢查询、QPS 有无退化),稳妥后再DROP INDEX;一旦有闪失,ALTER ... VISIBLE秒级回滚。
4.2 instant DDL:10 万行加列只要 0.04 秒
传统ALTER TABLE ... ADD COLUMN在大表上是噩梦——要 rebuild 整张表,锁、拷贝、重建索引,动辄小时级。8.0 的instant DDL改写了这个叙事:加列只改数据字典的"行版本"元数据,不碰既有数据页。
实测,10 万行的 t 表加一列:
$ mysql -uroot b2 -e 'set profiling=1; alter table t add column tag varchar(32) default "n/a", algorithm=instant; show profiles;' 2>&1 Query_ID Duration Query 1 0.04012975 alter table t add column tag varchar(32) default 'n/a', algorithm=instantDuration = 0.04012975 秒。注意显式写了ALGORITHM=INSTANT(8.0 对加列默认就会优先 instant,但显式声明更稳,也方便在被迫降级时报错而非偷偷变成 inplace)。新增列有默认值'n/a',旧行的默认值通过"行版本"在读取时即时补全,不回填数据页——这就是它能秒完的底层原理。
4.3 一个真实的踩坑:想看 row version,结果列名报错
讲到 instant DDL 的"行版本",很多文章会让你去information_schema.innodb_tables查total_row_versions。我也试了,结果翻车——在本机 8.0.46 上:
$ mysql -uroot b2 -e 'select table_name, total_row_versions from information_schema.innodb_tables where name like "%b2/t%" limit 5;' 2>&1 ERROR 1054 (42S22) at line 1: Unknown column 'table_name' in 'field list' [exit=1]exit=1,直接报ERROR 1054 Unknown column 'table_name'。这个坑要如实讲:8.0.46 的information_schema.innodb_tables里,并没有table_name这个列(该视图的表名列实际叫name;行版本相关字段在不同小版本间的命名/可用性也有差异)。踩到这种"照着网上文章抄命令却跑不通"的情况,第一反应应当是DESC information_schema.innodb_tables;看真实列名,而不是怀疑自己。这也是本篇"宁缺毋假"的原则——实机报错就是报错,如实呈现。
正确姿势:先
DESC确认列存在再查;想观察 instant 加列痕迹,更靠谱的是看information_schema.innodb_tables里本版本真实存在的列(如instant_cols),或SHOW CREATE TABLE观察元数据变化。
4.4 EXPLAIN ANALYZE:让优化器不再"估算着骗你"
传统EXPLAIN给的是估算:rows、cost 都是优化器猜的。8.0 的EXPLAIN ANALYZE直接真正执行这条语句,把"实际耗时、实际行数、循环次数"打出来。先来个单表范围扫描:
$ mysql -uroot b2 -e 'explain analyze select * from t where a between 10000 and 20000\G' 2>&1 *************************** 1. row *************************** EXPLAIN: -> Index range scan on t using ab over (10000 <= a <= 20000), with index condition: (t.a between 10000 and 20000) (cost=7585 rows=16856) (actual time=1.19..18 rows=10038 loops=1)解读这一行:
Index range scan on t using ab:实际走的是联合索引ab的范围扫描(覆盖a between 10000 and 20000)。cost=7585 rows=16856:这是优化器的估算(猜 16856 行)。(actual time=1.19..18 rows=10038 loops=1):这是真跑出来的——实际只扫了10038 行,耗时 1.19ms 启动、到 18ms 完成,循环 1 次。
估算 16856 vs 实际 10038,差距近 40%。这就是 EXPLAIN ANALYZE 的价值:它能揪出"优化器估算严重偏离实际"的慢查询根因,而不是让你盲调索引。
再看一个两表 JOIN 的完整执行树:
$ mysql -uroot b2 -e 'explain analyze select count(*) from t t1 straight_join t t2 on t1.a=t2.id where t1.id<1000\G' 2>&1 *************************** 1. row *************************** EXPLAIN: -> Aggregate: count(0) (cost=650 rows=1) (actual time=1.43..1.43 rows=1 loops=1) -> Nested loop inner join (cost=550 rows=999) (actual time=0.0676..1.39 rows=999 loops=1) -> Filter: ((t1.id < 1000) and (t1.a is not null)) (cost=200 rows=999) (actual time=0.0599..0.268 rows=999 loops=1) -> Index range scan on t1 using PRIMARY over (id < 1000) (cost=200 rows=999) (actual time=0.0583..0.192 rows=999 loops=1) -> Single-row covering index lookup on t2 using PRIMARY (id=t1.a) (cost=0.25 rows=1) (actual time=972e-6..995e-6 rows=1 loops=999)逐行拆解这棵执行树(从下往上读,和真正执行顺序一致):
- 最内层:
Index range scan on t1 using PRIMARY over (id < 1000),用主键扫 t1 的id<1000,实际999 行,耗时 0.0583…0.192ms。 - Filter:在 t1 上再过滤
(id<1000) and (a is not null),实际 999 行,0.0599…0.268ms。 - Nested loop inner join:对 t1 的 999 行,逐行去 t2 上做
Single-row covering index lookup using PRIMARY (id=t1.a)——这是个覆盖索引回表(因为只需要主键),每次约 972e-6…995e-6 秒(也就是 ~1 微秒),循环了999 次(loops=999,对应外层 999 行),实际产出 999 行。整个 join 实际耗时 0.0676…1.39ms。 - 最外层 Aggregate count(0):汇总出 1 行,1.43ms。
一眼就能看出瓶颈在哪:join 里那个loops=999的嵌套循环,每行 ~1 微秒,合计约 1ms——非常高效,因为走的是主键覆盖索引。EXPLAIN ANALYZE 把"循环 999 次"这种传统 EXPLAIN 看不到的细节直接摊开,调优时再也不用猜。
4.5 直方图:让优化器不再"瞎猜"数据分布
优化器估算 rows 准不准,取决于它对列上数据分布的了解。普通索引只记录"有多少不同值",不知道"哪些值集中、哪些值稀疏"。8.0 的**直方图(histogram)**补上了这块拼图。
给b列建一个 32 桶的等宽/等高直方图:
$ mysql -uroot b2 -e 'analyze table t update histogram on b with 32 buckets;' 2>&1 Table Op Msg_type Msg_text b2.t histogram status Histogram statistics created for column 'b'.再去information_schema.column_statistics看它真实记了什么:
$ mysql -uroot b2 -e 'select schema_name, table_name, column_name, json_extract(histogram,"$.""histogram-type""") as htype, json_extract(histogram,"$.""number-of-buckets-specified""") as buckets from information_schema.column_statistics;' 2>&1 SCHEMA_NAME TABLE_NAME COLUMN_NAME htype buckets b2 t b "equi-height" 32实机记录显示:列b的直方图类型是equi-height(等高直方图),桶数32。等高直方图的每个桶装差不多数量的行,对偏态分布(少数极值撑满、大部分集中)描述得比等宽更准。有了它,优化器在面对WHERE b = 某个高频值时,能更精准地估算出"会命中几行",从而选对索引、选对 join 顺序。
直方图适合非索引列的过滤条件估算;如果列上已经有索引,优先靠索引统计。两者是互补关系。
4.6 窗口函数:一行 SQL 干掉一堆自连接
过去做"排名、分组TopN、四分位"这类分析,得写嵌套子查询甚至自连接,又慢又难读。8.0 的窗口函数一句话搞定。
先来row_number()倒序排名 +ntile(4)四分位,取 id<=20 的前 12 行看效果:
$ mysql -uroot b2 -e 'with ranked as (select id, a, row_number() over (order by a desc) as rn, ntile(4) over (order by a) as quartile from t where id<=20) select * from ranked limit 12;' 2>&1 id a rn quartile 19 5900 20 1 1 12340 19 1 11 16174 18 1 5 20151 17 1 6 27626 16 1 17 27906 15 2 18 29319 14 2 13 32497 13 2 15 36618 12 2 12 40951 11 2 7 43531 10 3 20 48234 9 3解读:
rn是row_number() over (order by a desc)——按a从大到小排的名次。看id=19, a=5900排第20(倒数第一,因为 a 最小),id=20, a=48234排第 9,逻辑完全自洽。quartile是ntile(4) over (order by a)——把按a升序的行切成 4 等份。前 5 行(a 最小的那批)都是 quartile=1,a 升到中段变成 2、再变 3,完美体现"四分位分组"。
再做一个真正的"四分位统计"——每段多少行、a 的 min/max:
$ mysql -uroot b2 -e 'select quartile, count(*), min(a), max(a) from (select ntile(4) over (order by a) quartile, a from t where id<=10000) x group by quartile;' 2>&1 quartile count(*) min(a) max(a) 1 2500 7 25225 2 2500 25235 50230 3 2500 50236 75163 4 2500 75177 999864 个四分位,每段正好 2500 行(共 10000 行),a的范围从[7,25225]平滑递进到[75177,99986]。以前要算出这种"按数值均匀切四段、每段极值"的分布,得写好几层自连接;现在一个ntile(4)加一层聚合,干净利落。
踩坑记录(实机真实翻车,如实记录)
- secure_file_priv 拦路:
SELECT ... INTO OUTFILE不是随便写路径就能跑的。本机secure_file_priv=/var/lib/mysql-files/,写别的目录直接被拒。生产上要么改这个变量(需重启),要么老老实实用指定目录。 - invisible 后 possible_keys 消失:把
a设为 invisible 后,a从possible_keys中彻底消失,查询退而走ab。这是预期行为,但如果你只盯着key列没注意possible_keys变化,容易误判"索引还在用"。 - information_schema.innodb_tables 列名版本差异:照抄的
total_row_versions/table_name查询在本机 8.0.46 直接ERROR 1054退出码 1。教训:不同小版本的information_schema视图列名会变,先DESC再查,别盲信博客。 - 分区表无分区键必全扫:
WHERE c=3不带ftime,四分区全扫,分区表毫无优势。设计分区时务必保证高频查询带分区键。 - LOOP=999 的嵌套循环:EXPLAIN ANALYZE 暴露 join 内层循环 999 次,虽本次每次仅 ~1μs 不慢,但若内层不是主键覆盖 lookup 而是全表扫,999 次放大就是灾难——用它提前发现隐患。
面试高频问答
Q:grant 之后必须 flush privileges 吗?
A:不需要。GRANT/REVOKE/CREATE USER会同时更新内存权限缓存和mysql系统库,新连接直接读内存即生效。只有手改mysql系统表时才需要FLUSH PRIVILEGES把磁盘刷回内存。Q:最快复制一张表的推荐做法?
A:追求省心用mysqldump(自带 DDL+数据,跨版本友好);追求速度用SELECT INTO OUTFILE+LOAD DATA INFILE(本机 10 万行:导出 0.036s/1921K,导入 0.892s,比 dump 的 1.206s 快)。注意secure_file_priv目录限制。同版本整机迁移可上物理.ibd拷贝。Q:分区表一定能提升性能吗?
A:不一定。只有在查询带分区键、命中分区裁剪(如本例ftime只扫 p2026),或需要DROP PARTITION秒删历史数据时才划算。无分区键的查询(如WHERE c=3)会全分区扫描,反而更慢,且有分区数过多、DDL 行为差异、MDL 放大等坑。Q:8.0 的 instant DDL 加列为什么这么快?
A:只修改数据字典里的"行版本"元数据,旧行默认值在读取时即时补全,不回填既有数据页。本机 10 万行加列Duration=0.04012975s。注意需ALGORITHM=INSTANT(8.0 对加列默认也优先 instant),且不是所有 DDL 都支持 instant。Q:invisible index 和直接 drop index 有什么区别?
A:invisible 只让优化器"看不见"该索引,物理上仍在,可随时ALTER ... VISIBLE秒级恢复。适合灰度下线索引:先 invisible 观察,确认无退化再 drop;万一误伤能立刻回滚。直接 drop 没有后悔药。Q:EXPLAIN 和 EXPLAIN ANALYZE 有什么本质区别?
A:EXPLAIN 只给估算(cost/rows 是优化器猜的);EXPLAIN ANALYZE 会真正执行,输出actual time / rows / loops真实值。本例a between 10000 and 20000估算 16856 行、实际仅 10038 行,差距近 40%,靠 ANALYZE 才能暴露这种估算偏差。Q:直方图和索引在优化器里是什么关系?
A:互补。索引记录"不同值数量",直方图记录"列上数据分布(等高/等宽,多少桶)",帮助优化器对非索引列的过滤条件更准地估算行数。本例给b列建了 32 桶等高直方图,类型为equi-height。
实战落地清单:按场景对号入座
前面把原理和实机数据都摊开了,最后用一张"场景对照表"把本篇所有能力收敛成可执行的动作,方便你直接抄进日常运维 SOP。
复制/迁移类
- 单机跨库搬数、且要连带索引结构一次带走:首选
mysqldump(本机 10 万行导出 0.042s、恢复 1.206s),它自带 DDL,最省心。 - 大数据量纯搬运、可接受手动建表:用
OUTFILE+LOAD DATA(导出 0.036s、导入 0.892s),文本协议比 SQL 解析快约四分之一。切记路径必须落在secure_file_priv指定的/var/lib/mysql-files/。 - 同版本整机迁移、停写窗口允许:再考虑物理
.ibd拷贝,速度上限最高但约束最严。
权限类
- 日常授权一律走
GRANT/REVOKE语句,不要画蛇添足FLUSH PRIVILEGES;只有手改mysql系统表导致内存磁盘不一致时才需要 flush。新连接即时生效、即时回收,本机已用ERROR 1044验证。
分区类
- 命中"按时间清理 + 查询带分区键"才上分区;否则普通表加好索引更优。上分区后务必用
EXPLAIN的partitions列确认裁剪是否生效(本例带ftime只扫 p2026,带c扫全部分区),并善用DROP PARTITION秒删冷数据。
8.0 新特性类
- 怀疑某索引无用:先
ALTER ... INVISIBLE灰度观察,确认无退化再DROP,误伤可VISIBLE秒回。 - 大表加列/加字段:显式
ALGORITHM=INSTANT,10 万行实测 0.04 秒,避免 inplace 全表 rebuild。 - 慢查询调优:用
EXPLAIN ANALYZE看真实actual time/rows/loops,别再只信估算值。 - 非索引列过滤条件估算偏差大:建直方图(本例 32 桶等高)补数据分布。
- 排名 / 分组 TopN / 分位统计:优先窗口函数
row_number()、ntile(),替代又慢又难维护的自连接。
把这张清单贴在工位上,本篇就算没白写。
总结:45 讲之后,MySQL 还在进化
从"最快复制一张表"的 0.036s 文本搬运,到 grant 即时生效的权限模型,再到分区表"裁剪与全扫一线之隔"的清醒认知——这些是 45 讲打下的底子,解决的是"把事做对"。而 MySQL 8.0 这一波新特性,解决的是"把事做快、做稳":
- invisible index让索引下线从"赌博"变成"灰度实验";
- instant DDL把加列从小时级压到 0.04 秒;
- EXPLAIN ANALYZE把优化器从"黑盒估算"拉到"白盒实测";
- 直方图补上数据分布这块拼图;
- 窗口函数一行顶过去一堆自连接。
技术的价值,不在新名词多炫,而在真实业务里少一次半夜告警、少一次锁表事故、少写一段又臭又长的 SQL。本篇的全部数字,都来自真实云服务器的实机回显——可以放心地拿去压测、去面试、去说服你的组长"这功能咱们能上"。
本文实验均在真实云服务器完成,输出为实机回显。