原创

MySQL 不可见索引试运行:删除前验证读路径与约束边界

删掉一个索引只需一条 DDL,确认它没有用却不能只看一条 SQL。订单查询不用它,夜间对账可能还用;平时查询没变化,带 FORCE INDEX 的旧报表可能直接报错。

MySQL 8.4 的不可见索引适合解决这个验证问题:保留索引结构和写入维护,让默认优化器暂时忽略它。观察读路径的变化以后,再决定是否真正删除。它不能替你测出“删除以后写入能快多少”,因为隐藏期间索引仍在更新。

先准备一个能看出访问路径的实验

下面的命令面向 MySQL 8.4、InnoDB,使用独立实验库。不要直接把实验表名换成业务表名执行。数据规模用于观察访问路径,不构成性能测试。

CREATE DATABASE IF NOT EXISTS invisible_index_lab
    CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE invisible_index_lab;

CREATE TABLE purchase_order (
    id BIGINT NOT NULL,
    tenant_id INT NOT NULL,
    buyer_id BIGINT NOT NULL,
    request_no CHAR(12) NOT NULL,
    created_at DATETIME(6) NOT NULL,
    amount DECIMAL(12,2) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uk_request (request_no),
    KEY idx_buyer_recent
        (tenant_id, buyer_id, created_at DESC, id DESC)
) ENGINE=InnoDB;

SET SESSION cte_max_recursion_depth = 11000;
INSERT INTO purchase_order
    (id, tenant_id, buyer_id, request_no, created_at, amount)
WITH RECURSIVE seq(n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM seq WHERE n < 10000
)
SELECT n, MOD(n, 10), MOD(n, 1000),
       CONCAT('R', LPAD(n, 11, '0')),
       TIMESTAMP('2026-01-01 00:00:00') + INTERVAL n SECOND,
       10.00 + MOD(n, 50)
FROM seq;

ANALYZE TABLE purchase_order;

这张表刻意没有给每个查询条件单独建索引。后面要观察的是某一个联合索引消失以后,查询如何退让,而不是让另一个等价索引把变化遮住。

EXPLAIN FORMAT=TREE
SELECT id, created_at, amount
FROM purchase_order
WHERE tenant_id = 3 AND buyer_id = 123
ORDER BY created_at DESC, id DESC
LIMIT 5;

记录执行计划中的访问方式、索引名称和排序节点。这里有满足等值前缀及排序要求的候选索引,通常会形成窄范围读取。具体估计行数以自己的实例输出为准,不要把一张示例执行计划的数字当验收标准。

隐藏以后,默认查询和试验查询可以分开

ALTER TABLE purchase_order
    ALTER INDEX idx_buyer_recent INVISIBLE;

SELECT INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME, IS_VISIBLE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
  AND TABLE_NAME = 'purchase_order'
ORDER BY INDEX_NAME, SEQ_IN_INDEX;

IS_VISIBLE 变为 NO,索引并没有删除。重新执行刚才的普通 EXPLAIN,比较读路径是否改变;没有其他合适索引时,可能出现表扫描和额外排序。

然后让一条试验查询临时考虑不可见索引:

EXPLAIN FORMAT=TREE
SELECT /*+ SET_VAR(optimizer_switch = 'use_invisible_indexes=on') */
       id, created_at, amount
FROM purchase_order
WHERE tenant_id = 3 AND buyer_id = 123
ORDER BY created_at DESC, id DESC
LIMIT 5;

这个提示只改变当前语句的优化器选项,不会把索引重新显示给其他连接,也不是强制使用这个索引。提示开启的是“可以考虑”;最终选哪个访问路径,仍由优化器决定。若结果没有变化,先运行 SHOW WARNINGS 检查提示是否被接受,再看表规模、统计信息和候选路径的代价。

也可以用会话设置做多条 SQL 的对照,不过连接池里更容易遗留状态。单语句提示适合这种局部试验:试验结束,不需要记得把借来的连接恢复原状。

主动制造一次失败:旧的索引提示会暴露依赖

保持索引不可见,并明确关闭当前会话对不可见索引的考虑:

SET SESSION optimizer_switch = 'use_invisible_indexes=off';

-- 预期失败:默认不可使用已经隐藏的索引名称。
SELECT id
FROM purchase_order FORCE INDEX (idx_buyer_recent)
WHERE tenant_id = 3 AND buyer_id = 123;

这类失败与普通查询变慢不同。普通查询还有机会退回其他路径,显式指定索引名的代码可能在解析优化阶段就报错。清理索引之前,应搜索应用 SQL、报表脚本和运维任务中的索引提示。只查看慢查询榜单,很容易漏掉每周才跑一次的任务。

恢复索引可见性不需要重建整棵索引:

ALTER TABLE purchase_order
    ALTER INDEX idx_buyer_recent VISIBLE;

这让回退比“删掉以后再建回来”轻得多,但它仍然是 DDL,需要取得相应元数据锁。长事务占着表时,操作也可能等待。不要把“元数据变更较轻”理解成“任何时候都不会影响请求”。

不可见的唯一索引仍然拦截重复值

索引是否参与查询,和它是否维护唯一性,是两件事。继续在实验库里验证:

ALTER TABLE purchase_order ALTER INDEX uk_request INVISIBLE;

-- 预期失败:复制 id=1 的 request_no,违反唯一约束。
INSERT INTO purchase_order
    (id, tenant_id, buyer_id, request_no, created_at, amount)
SELECT 10001, tenant_id, buyer_id, request_no, created_at, amount
FROM purchase_order
WHERE id = 1;

ALTER TABLE purchase_order ALTER INDEX uk_request VISIBLE;

这一点会直接影响变更评估。把唯一索引隐藏,不会模拟删除唯一约束后的写入行为;把一个大索引隐藏,也不会让它立即释放磁盘、减少每次写入的索引维护工作。

主键不能隐藏。没有显式主键的表,还要留意充当隐式主键的非空唯一索引,不能看到 UNIQUE 就认定一定能变为不可见。处理真实表之前,先保存 SHOW CREATE TABLE,把存储结构和约束角色看清楚。

怎样把实验转成一次可回退的索引清理

真正有价值的观察单位是业务查询集合,而不是某次调用。隐藏前保存代表性 SQL、执行计划和基线;隐藏后覆盖高峰、低峰及周期任务,按查询摘要对比扫描行数、耗时分布和错误。观察期间如果发生大幅数据增长或发布新版本,前后样本就不再是同一组条件,需要重新解释差异。

确认没有读路径依赖后,才进入删除阶段。删除会改变写入维护成本,也会释放相应索引占用;这部分收益要在删除后观察,不能从隐藏阶段凭空推算。保留重建 DDL、执行窗口和回退容量预算同样必要:索引越大,重新创建越不能靠临时决定。

实验结束可以删除整个独立库:

DROP DATABASE invisible_index_lab;

不可见索引最有用的地方,是让“这个索引应该没用了”变成一个能观察、能迅速撤回的假设。验证范围没覆盖到的业务,依然属于未知,不能靠索引名看起来重复就替它下结论。

观察到“没变化”,还需要排除两个盲区

索引使用统计通常有重置和采样边界。数据库重启以后只观察了半天,不能证明上个月跑过的结算任务不依赖这个索引;从副本上看到它没被使用,也不能直接推出主库没有对应读取。统计表适合找候选,业务调用范围仍要自己补齐。

隐藏后还应该保留一组恢复演练:确认恢复可见的语句、执行账户及数据库都正确,记录索引当前状态。一次变更包含多个索引时,逐个保存状态,不要凭“原来应该都可见”批量恢复。新建时就不可见的试验索引,可能根本不该公开给全部请求。

如果观察期间数据库整体负载上升,要把索引影响与环境变化分开。只比较两个时间点的平均耗时,会混进缓存冷热、数据量、请求参数分布和下游压力。更可靠的做法是保留同一类查询的行数分布、调用量和慢参数样本,必要时在隔离副本上重放代表性读取。不能证明差异来自索引,就先维持可回退状态。

删除决策也应有明确截止条件。例如完整覆盖业务周期后仍无依赖,且已核对其他索引能够承担约束与读取,才执行删除。这个周期由任务频率决定,不是技术文章替所有系统规定的固定天数。

正文到此结束
评论插件初始化中...
Loading...