一次MySQL大表索引删除之旅:从卡死到表损坏再到迁移

就在昨天,我遇到了一个堪称教科书级别的数据库问题。我需要在一个拥有 6000万行数据 的大表上删除一个索引,本以为一个简单的 ALTER TABLE 命令就能搞定,没想到却由此开启了一段惊心动魄、长达数小时的排查与修复之旅。

整个过程涉及到了 DDL卡死、表引擎的致命缺陷、表损坏与修复、索引统计信息异常 等一系列问题。我将整个过程复盘记录下来,希望能给未来的自己提个醒,也希望能帮助每一位可能遇到类似困境的同学。

故事的主角: 一张名为 wa_xinghao 的表,数据量 6000万+。

第一幕:风平浪静下的暗流------ALTER TABLE 为何卡住了?

一切始于这条命令:

sql 复制代码
ALTER TABLE `wa_xinghao` DROP INDEX `product_id-s1`;

执行后,终端就卡住了,没有任何反应。与此同时,我发现应用中所有与这张表相关的查询都变得极慢,甚至超时。我的第一反应是:锁住了!

我立刻打开新的数据库连接,执行 SHOW FULL PROCESSLIST; 进行诊断。

通常,ALTER 卡住最常见的原因是 Waiting for table metadata lock,即有其他长事务或查询占着表的元数据锁。但这次,我看到的 State 却是:

copy to tmp table

这个状态让我心头一凉。它意味着 MySQL 正在执行最原始、最耗时的表重建操作:创建一个带新结构(没有了那个索引)的临时表,然后把 6000 万行数据一行一行地拷贝过去。在此期间,原表会被长时间锁定。对于6000万的数据量,这无疑是一场灾难。

第二幕:深挖根源------为什么是 COPY 而不是 INPLACE

我使用的是 MySQL 5.7,它早就支持 DROP INDEX 的 Online DDL (INPLACE 算法),不应该退化成 COPY 才对。为了验证,我尝试强制指定算法:

sql 复制代码
ALTER TABLE `wa_xinghao` DROP INDEX `product_id-s1`, ALGORITHM=INPLACE, LOCK=NONE;

结果,MySQL 毫不留情地给了我一个错误: ERROR 1845 (0A000): ALGORITHM=INPLACE is not supported for this operation. Try ALGORITHM=COPY.

这下问题明确了:由于某种原因,MySQL 认为对这张表无法执行 INPLACE 操作。我立刻执行 SHOW CREATE TABLE wa_xinghao; 查看表结构,真相大白:

sql 复制代码
) ENGINE=MyISAM ...

根源找到了!是 MyISAM 存储引擎,我一直想当然的认为是 InnoDB 引擎,因为该数据库一眼看下去全都是 InnoDB 引擎!

MyISAM 是一个古老的、非事务性的引擎,它存在诸多致命缺陷,其中之一就是对 DDL 操作的支持非常薄弱 。它几乎没有"在线"操作的概念,大部分 ALTER 操作都会导致锁表和表重建。

第三幕:一波未平一波又起------表损坏与漫长的修复

在定位到问题后,我无法忍受长时间的锁表,于是做了一个现在看来非常危险的决定:我 KILL 掉了那个正在 copy to tmp tableALTER 进程。

短暂的平静后,新的噩梦来了。当我尝试对这张表做任何操作(哪怕是 SELECT)时,都收到了一个新错误:

ERROR 1194 (HY000): Table 'wa_xinghao' is marked as crashed and should be repaired

是的,由于强行中断了对底层文件的修改,MyISAM 表"不负众望"地损坏了。这是 MyISAM 引擎非崩溃安全的典型表现。

唯一的出路就是修复它:

sql 复制代码
REPAIR TABLE `wa_xinghao`;

然后,我又进入了新一轮的漫长等待。通过 SHOW PROCESSLIST;,我看到状态变成了:

Repair by sorting

这个状态表示 MySQL 正在通过排序的方式重建索引文件 (.MYI)。对于 6000 万行数据和多个索引,这是一个极其消耗 CPU 和 I/O 的过程。我能做的只有耐心等待,并祈祷服务器不要被拖垮。这一次,我绝对不敢再 KILL 它了

第四幕:雨过天晴后的一个"小彩蛋"------诡异的索引基数

在经历了数小时的煎熬后,表终于修复成功了!

在检查表结构时,我无意中发现了一个有趣的现象:

如上图所示,snsn2 这两个索引的基数(Cardinality) 竟然比主键 PRIMARY 的基数还要大!这在逻辑上是不可能的,因为主键是绝对唯一的,其基数应该等于总行数。

原因揭秘:

  1. 基数是估算值:MySQL 为了性能,是通过随机抽样来估算基数的,本身就存在误差。
  2. 统计信息已过时:最主要的原因是,在经历了"崩溃-修复"这个混乱的过程后,表的统计信息没有被及时更新,导致数据陈旧且不准确。

解决方案很简单,手动强制更新统计信息:

sql 复制代码
ANALYZE TABLE `wa_xinghao`;

执行完毕后,基数恢复正常。

最终章:浴火重生------拥抱 InnoDB

这次惊心动魄的经历,让我下定决心彻底告别 MyISAM。我的最终目标是:在删除多余索引的同时,将表引擎转换为 InnoDB

直接执行 ALTER TABLE ... ENGINE=InnoDB 同样会触发锁表的表重建,不可取。最佳实践有两种:

方案一:在线操作的王者 pt-online-schema-change

Percona Toolkit 中的这个工具是处理大表 DDL 的行业标准。它通过创建"幽灵表"+触发器同步增量数据的方式,可以在不锁表、对业务影响极小的情况下完成表结构的变更。

bash 复制代码
# 一条命令,同时完成"删索引"和"转引擎"两大任务
pt-online-schema-change \
  --alter "DROP INDEX `product_id-s1`, ENGINE=InnoDB" \
  h=your_host,D=your_database,t=wa_xinghao,u=your_user,p=your_password \
  --execute

方案二:手动离线操作(需要停机窗口)

如果业务允许短暂的停机维护,可以采用"先迁移数据,后建索引"的思路,速度极快。

  1. 创建无索引的 InnoDB 新表 wa_xinghao_new
  2. 快速导出数据到文件SELECT ... INTO OUTFILE ... FROM wa_xinghao;
  3. 快速加载数据到新表LOAD DATA INFILE ... INTO TABLE wa_xinghao_new;
  4. 在新表上在线创建所需索引ALTER TABLE wa_xinghao_new ADD INDEX ...;
  5. 在停机窗口内,原子重命名RENAME TABLE wa_xinghao TO wa_xinghao_old, wa_xinghao_new TO wa_xinghao;

反思

这次经历虽然坎坷,但收获巨大。我总结了以下几点核心教训:

  1. 引擎是根本 :在对大表做任何操作前,先看 ENGINE!不同的引擎,行为天差地别。
  2. 告别 MyISAM :对于任何有写入、更新和高可用性要求的业务,请立即、马上将 MyISAM 迁移到 InnoDBMyISAM 的非崩溃安全和表级锁就是埋在系统里的定时炸弹。
  3. 敬畏 KILL 命令 :不要轻易 KILL 一个正在对 MyISAM 表进行写操作的 DDL 进程,否则你将大概率收获一个损坏的表。
  4. 善用专业工具 :对于大表 DDL,pt-online-schema-changegh-ost 不是"可选项",而是"必需品"。
  5. 理解状态信息SHOW PROCESSLIST 中的 State 是排查问题的金钥匙。copy to tmp tableRepair by sorting 都告诉了我们底层正在发生什么。
相关推荐
孟猛20235 分钟前
Unix ODBC和Mysql ODBC
数据库
天天摸鱼的java工程师7 分钟前
在 Spring 框架中,如何自定义一个注解?
后端
代码or搬砖11 分钟前
Spring JDBC配置与讲解
java·数据库·spring
m0_6348654013 分钟前
sa-token:我将代替你,Spring Security
java·后端·spring
IT_102417 分钟前
springboot企业级项目开发之项目测试——集成测试!
spring boot·后端·spring·集成测试
想用offer打牌1 小时前
一站式了解RocketMQ如何实现顺序消息😵
后端·rocketmq
程序视点1 小时前
MySQL数据库操作完全指南:从入门到闭关
数据库·sql·mysql
不吃肉的羊1 小时前
Apache开启gzip压缩
后端
喵手1 小时前
如何高效进行对象拷贝?浅拷贝与深拷贝的陷阱,你知道吗?
java·后端·java ee
喵手1 小时前
这年头,还有谁不会用CollectionUtils类?也太...
java·后端·java ee