MySQL 运维提效与 8.0 新特性实战:最快复制表、分区表、instant DDL 与 EXPLAIN ANALYZE
2026/7/29 15:27:52 网站建设 项目流程

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 OUTFILELOAD 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_ci

10 万行,主键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.sql

10 万行导出只用了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(*) 100000

LOAD DATA导入real 0.892s,比 dump 的 1.206s 快约 26%,且导入后校验count(*)=100000,数据一条不少。文本协议少了解析 SQL 的开销,这是它更快的根本原因。

姿势三:物理拷贝(思路,本篇不展开)

还有第三条路:直接拷.ibd文件 +DISCARD/IMPORT TABLESPACE。它最快,但限制也最多(表空间自包含、版本/页大小一致、需要FLUSH TABLES ... FOR EXPORT.cfg元数据)。日常跨库复制,逻辑路线足矣;物理拷贝更适合"同版本整机迁移"这种场景,本篇焦点在前面两种。

三种姿势真实耗时对比

姿势导出耗时(real)产物大小导入耗时(real)含 DDL跨版本
mysqldump0.042s2314K SQL1.206s是(自带)友好
OUTFILE+LOAD0.036s1921K CSV0.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.usermysql.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.ibd

4 个分区,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 index

partitions列变成了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(*) 3

DROP 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_keysa,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 NULL

show indexVisible列变成了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 NULL

key立刻回到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=instant

Duration = 0.04012975 秒。注意显式写了ALGORITHM=INSTANT(8.0 对加列默认就会优先 instant,但显式声明更稳,也方便在被迫降级时报错而非偷偷变成 inplace)。新增列有默认值'n/a',旧行的默认值通过"行版本"在读取时即时补全,不回填数据页——这就是它能秒完的底层原理。

4.3 一个真实的踩坑:想看 row version,结果列名报错

讲到 instant DDL 的"行版本",很多文章会让你去information_schema.innodb_tablestotal_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)

逐行拆解这棵执行树(从下往上读,和真正执行顺序一致):

  1. 最内层Index range scan on t1 using PRIMARY over (id < 1000),用主键扫 t1 的id<1000,实际999 行,耗时 0.0583…0.192ms。
  2. Filter:在 t1 上再过滤(id<1000) and (a is not null),实际 999 行,0.0599…0.268ms。
  3. 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。
  4. 最外层 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

解读:

  • rnrow_number() over (order by a desc)——按a从大到小排的名次。看id=19, a=5900排第20(倒数第一,因为 a 最小),id=20, a=48234排第 9,逻辑完全自洽。
  • quartilentile(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 99986

4 个四分位,每段正好 2500 行(共 10000 行),a的范围从[7,25225]平滑递进到[75177,99986]。以前要算出这种"按数值均匀切四段、每段极值"的分布,得写好几层自连接;现在一个ntile(4)加一层聚合,干净利落。


踩坑记录(实机真实翻车,如实记录)

  1. secure_file_priv 拦路SELECT ... INTO OUTFILE不是随便写路径就能跑的。本机secure_file_priv=/var/lib/mysql-files/,写别的目录直接被拒。生产上要么改这个变量(需重启),要么老老实实用指定目录。
  2. invisible 后 possible_keys 消失:把a设为 invisible 后,apossible_keys中彻底消失,查询退而走ab。这是预期行为,但如果你只盯着key列没注意possible_keys变化,容易误判"索引还在用"。
  3. information_schema.innodb_tables 列名版本差异:照抄的total_row_versions/table_name查询在本机 8.0.46 直接ERROR 1054退出码 1。教训:不同小版本的information_schema视图列名会变,DESC再查,别盲信博客。
  4. 分区表无分区键必全扫WHERE c=3不带ftime,四分区全扫,分区表毫无优势。设计分区时务必保证高频查询带分区键。
  5. LOOP=999 的嵌套循环:EXPLAIN ANALYZE 暴露 join 内层循环 999 次,虽本次每次仅 ~1μs 不慢,但若内层不是主键覆盖 lookup 而是全表扫,999 次放大就是灾难——用它提前发现隐患。

面试高频问答

  1. Q:grant 之后必须 flush privileges 吗?
    A:不需要。GRANT/REVOKE/CREATE USER会同时更新内存权限缓存和mysql系统库,新连接直接读内存即生效。只有手改mysql系统表时才需要FLUSH PRIVILEGES把磁盘刷回内存。

  2. Q:最快复制一张表的推荐做法?
    A:追求省心用mysqldump(自带 DDL+数据,跨版本友好);追求速度用SELECT INTO OUTFILE+LOAD DATA INFILE(本机 10 万行:导出 0.036s/1921K,导入 0.892s,比 dump 的 1.206s 快)。注意secure_file_priv目录限制。同版本整机迁移可上物理.ibd拷贝。

  3. Q:分区表一定能提升性能吗?
    A:不一定。只有在查询带分区键、命中分区裁剪(如本例ftime只扫 p2026),或需要DROP PARTITION秒删历史数据时才划算。无分区键的查询(如WHERE c=3)会全分区扫描,反而更慢,且有分区数过多、DDL 行为差异、MDL 放大等坑。

  4. Q:8.0 的 instant DDL 加列为什么这么快?
    A:只修改数据字典里的"行版本"元数据,旧行默认值在读取时即时补全,不回填既有数据页。本机 10 万行加列Duration=0.04012975s。注意需ALGORITHM=INSTANT(8.0 对加列默认也优先 instant),且不是所有 DDL 都支持 instant。

  5. Q:invisible index 和直接 drop index 有什么区别?
    A:invisible 只让优化器"看不见"该索引,物理上仍在,可随时ALTER ... VISIBLE秒级恢复。适合灰度下线索引:先 invisible 观察,确认无退化再 drop;万一误伤能立刻回滚。直接 drop 没有后悔药。

  6. Q:EXPLAIN 和 EXPLAIN ANALYZE 有什么本质区别?
    A:EXPLAIN 只给估算(cost/rows 是优化器猜的);EXPLAIN ANALYZE 会真正执行,输出actual time / rows / loops真实值。本例a between 10000 and 20000估算 16856 行、实际仅 10038 行,差距近 40%,靠 ANALYZE 才能暴露这种估算偏差。

  7. 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验证。

分区类

  • 命中"按时间清理 + 查询带分区键"才上分区;否则普通表加好索引更优。上分区后务必用EXPLAINpartitions列确认裁剪是否生效(本例带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。本篇的全部数字,都来自真实云服务器的实机回显——可以放心地拿去压测、去面试、去说服你的组长"这功能咱们能上"。

本文实验均在真实云服务器完成,输出为实机回显。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询