1. 当索引不再"保鲜"会发生什么?
上周三,我收到某电商平台的紧急求助:他们的订单表在清理了200万历史数据后,商品搜索响应时间从50ms暴增到2秒。通过EXPLAIN
查看执行计划,发现原本应该走索引的查询变成了全表扫描。这就像超市里明明有货架标签,收银员却要逐个货架找商品——问题就出在那些"过期"的索引上。
2. 索引为何会"失忆"?
2.1 存储引擎的"记忆方式"
以MyISAM为例,执行以下批量删除:
DELETE FROM orders WHERE create_time < '2023-01-01';
此时.MYD数据文件会立即缩小,但.MYI索引文件却像被橡皮擦随意涂抹——留下大量空洞。InnoDB虽然支持事务回滚,但它的MVCC机制会让已删除记录在purge线程清理前继续占用索引空间。
2.2 查看索引健康度的体检报告
-- 查看索引碎片率
SELECT
TABLE_NAME,
INDEX_NAME,
ROUND(DATA_FREE / (1024 * 1024), 2) AS '碎片空间(MB)',
ROUND((DATA_FREE / (DATA_LENGTH + INDEX_LENGTH)) * 100, 2) AS '碎片率%'
FROM
INFORMATION_SCHEMA.TABLES
WHERE
TABLE_SCHEMA = 'your_db'
AND DATA_FREE > 0;
当碎片率超过30%时,查询优化器可能认为扫描全表比使用索引更高效。
3. 给索引做"大扫除"的三种姿势
3.1 官方推荐套餐:OPTIMIZE TABLE
OPTIMIZE TABLE orders;
这相当于给数据库做了一次深度SPA:
- 创建临时表复制数据
- 重建所有索引
- 统计信息更新
- 原表替换
但要注意MyISAM会锁表,而InnoDB在5.6+版本支持ONLINE DDL。
3.2 精准打击方案:ALTER TABLE
ALTER TABLE orders ENGINE=InnoDB; -- 重建表
ALTER TABLE orders DROP INDEX idx_name, ADD INDEX idx_name (name); -- 重建单个索引
适合业务高峰期的折中方案,比如只重建最关键的索引。
3.3 自动化维护脚本
创建维护存储过程:
DELIMITER $$
CREATE PROCEDURE auto_rebuild_index()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE tname VARCHAR(64);
DECLARE cur CURSOR FOR
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'your_db'
AND ENGINE = 'InnoDB';
OPEN cur;
read_loop: LOOP
FETCH cur INTO tname;
IF done THEN
LEAVE read_loop;
END IF;
SET @sql = CONCAT('ALTER TABLE ', tname, ' ENGINE=InnoDB');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE cur;
END$$
DELIMITER ;
通过事件调度器每月执行一次。
4. C#开发者的运维工具箱
使用MySqlConnector类库实现智能维护:
using MySql.Data.MySqlClient;
public class IndexMaintainer
{
// 连接配置
private const string connStr = "server=localhost;user=root;database=order_db;port=3306;password=123456";
// 异步重建索引
public async Task RebuildIndexAsync(string tableName)
{
using var conn = new MySqlConnection(connStr);
await conn.OpenAsync();
// 获取索引列表
var indexCmd = new MySqlCommand(
$"SHOW INDEX FROM {tableName}", conn);
using var reader = await indexCmd.ExecuteReaderAsync();
var indexes = new List<string>();
while (await reader.ReadAsync())
{
string indexName = reader.GetString(2);
if (!indexes.Contains(indexName))
indexes.Add(indexName);
}
await reader.CloseAsync();
// 逐个重建
foreach (var index in indexes)
{
var rebuildCmd = new MySqlCommand(
$"ALTER TABLE {tableName} DROP INDEX `{index}`, ADD INDEX `{index}` (...)", conn);
await rebuildCmd.ExecuteNonQueryAsync();
Console.WriteLine($"已重建索引 {index}");
}
}
}
注意:实际使用需要根据表结构动态生成列定义。
5. 不同场景下的选择恐惧症解药
5.1 电商大促期间
- 选择:ALTER TABLE ... ALGORITHM=INPLACE
- 优势:在线操作,不阻塞写入
- 代价:执行时间可能较长
5.2 金融系统月末结算
- 选择:pt-online-schema-change工具
- 优势:完全不停机
- 代价:需要额外磁盘空间
5.3 物联网历史数据归档
- 选择:分区表+TRUNCATE PARTITION
- 优势:毫秒级删除
- 副作用:需要提前规划分区策略
6. 这些坑你千万别踩
锁表惊魂:某物流系统在双11前夜执行OPTIMIZE TABLE,导致订单积压3小时
- 对策:使用SHOW PROCESSLIST监控阻塞情况
磁盘空间刺客:重建500GB的表需要额外500GB空间
- 检查:df -h确认磁盘剩余空间
主从延迟黑洞:在主库执行ALTER导致从库延迟10小时
- 方案:先在从库测试,使用MASTER_DELAY参数控制
统计信息陷阱:某次重建后查询更慢,发现是基数估算错误
- 修复:ANALYZE TABLE重新收集统计信息
7. 性能参数调优秘籍
在my.cnf中增加这些配置:
[mysqld]
innodb_online_alter_log_max_size=256M
innodb_sort_buffer_size=64M
tmp_table_size=64M
定期检查:
mysql> SELECT * FROM sys.schema_unused_indexes;
mysql> SELECT * FROM information_schema.INNODB_TRX WHERE TIME > 60;
8. 总结:让索引永葆青春的秘诀
就像汽车需要定期保养,数据库索引也需要周期性维护。通过本文的三种重建方案,配合智能化的监控策略,可以有效解决数据删除后的索引失效问题。记住这些关键点:
- 碎片率超过30%必须处理
- 业务高峰期优先使用在线DDL
- 结合分区表设计减少维护压力
- 每次维护后验证执行计划
最后送大家一个自查清单: ✅ 每周检查碎片率 ✅ 大删除操作后立即重建索引 ✅ 使用Percona Toolkit监控索引健康度 ✅ 重要表配置自动维护任务
当你的索引始终"保鲜",查询性能就会像刚开封的薯片一样保持酥脆!