bigywater.tech
MongoDB · 44

隐藏索引:不删索引也能评估删掉的影响

不确定能不能删一个索引?隐藏它!这篇验证隐藏索引的行为:查询计划、访问计数、写入维护、唯一与 TTL 约束、hint 以及限制,说明它为什么是“删除索引前的安全开关”。

0.2 → 28 ms
同一条查询在索引可见与被隐藏时的耗时
200002
隐藏期间插入后每个索引仍然被维护的条目数
0 个
隐藏的 TTL 索引上 4 秒后剩余的过期文档数(TTL 照常工作)

隐藏与可见

实测 · 本机执行输出
  test> db.t.find({a: 1234})    // a_1 可见
  {
    plan: 'FETCH > IXSCAN(a_1)',
    nReturned: 40,
    keysExamined: 40,
    docsExamined: 40,
    ms: 4
  }
  test> db.t.hideIndex("a_1")
  { hidden_old: false, hidden_new: true, ok: 1 }
  test> db.t.find({a: 1234})    // a_1 已隐藏
  {
    plan: 'COLLSCAN',
    nReturned: 40,
    keysExamined: 0,
    docsExamined: 200000,
    ms: 37
  }
  test> db.t.unhideIndex("a_1")
  { hidden_old: true, hidden_new: false, ok: 1 }
  test> db.t.find({a: 1234})    // 取消隐藏
  {
    plan: 'FETCH > IXSCAN(a_1)',
    nReturned: 40,
    keysExamined: 40,
    docsExamined: 40,
    ms: 0
  }
  耗时(9 次中位数):可见 0.2 ms;隐藏 28 ms;取消隐藏 0.3 ms

隐藏后优化器不再考虑这个索引,同一条查询从 IXSCAN 变成 COLLSCAN(0.2 ms 变 28 ms);取消隐藏立刻恢复,不需要重建。

可观测

实测 · 本机执行输出
  test> db.t.hideIndex("a_1"); db.t.getIndexes().map(i => i.name + (i.hidden ? " (hidden)" : ""))
  [ '_id_', 'a_1 (hidden)', 'u_1', 'b_1' ]
  test> $indexStats(隐藏中)
  [ { name: 'a_1', ops: Long('0') } ]
  test> 又查了两次 a 之后的 $indexStats(隐藏中,ops 应该没变)
  [ { name: 'a_1', ops: Long('0') } ]
  test> 取消隐藏后查一次 a
  [ { name: 'a_1', ops: Long('1') } ]

getIndexes 里有 hidden: true;$indexStats 的访问计数在隐藏期间不增长,可以用来评估“如果删掉它会怎样”。

仍被维护

实测 · 本机执行输出
  test> db.t.hideIndex("a_1"); db.t.insertMany([{_id: 9000001, a: 999999, b: 1, u: 9000001}, {_id: 9000002, a: 999999, b: 1, u: 9000002}]).insertedCount
  validate 的索引条目数(隐藏中): {'_id_': 200002, 'a_1': 200002, 'b_1': 200002, 'u_1': 200002}
  test> 取消隐藏后 db.t.find({a: 999999})
  2
  test> explain
  {
    plan: 'FETCH > IXSCAN(a_1)',
    nReturned: 2,
    keysExamined: 2,
    docsExamined: 2,
    ms: 0
  }

隐藏的索引照常随写入更新:隐藏期间插入的文档,取消隐藏后马上可以用索引查到。

hint

实测 · 本机执行输出
  test> 隐藏后 hint("a_1")
  {
    code: 2,
    message: ':: caused by :: hint provided does not correspond to an existing index'
  }
  test> 隐藏后 hint({a: 1})
  {
    code: 2,
    message: ':: caused by :: hint provided does not correspond to an existing index'
  }

隐藏的索引对 hint 也相当于不存在,报 code 2。

约束照常

实测 · 本机执行输出
  test> 隐藏唯一索引 u_1 后插入重复的 u
  E11000 duplicate key error collection: test.t index: u_1 dup key: { u: 5 }
  test> db.ttl.hideIndex("c_ttl")
  { hidden_old: false, hidden_new: true, ok: 1 }
  隐藏的 TTL 索引 c_ttl,插入 10 个已过期的文档,4 秒后剩余: 0 个

隐藏的唯一索引仍然拒绝重复,隐藏的 TTL 索引仍然过期文档;隐藏只影响优化器。

限制

实测 · 本机执行输出
  test> try { db.t.hideIndex("_id_") } catch (e) { e.message }
  can't hide _id index
  test> hideIndex("nope")
  cannot find index nope for ns test.t
  test> [隐藏前缓存条目数, hideIndex 之后的缓存条目数]
  [ 1, 0 ]

不能隐藏 _id 索引;隐藏与取消隐藏都会清空该集合的计划缓存。

分步演示

隐藏索引
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。20 万个文档;耗时是 pymongo 的 9 次中位数;TTL 监控间隔调到 1 秒
    • 没有验证:隐藏索引对内存/磁盘空间的节省(它并不节省);副本集上 hideIndex 的传播;隐藏部分索引或多键索引;隐藏索引后长时间观察线上流量的流程;分片集合上的 hideIndex
    京ICP备2026067502号-1京公网安备11010502063487号