bigywater.tech
MongoDB · 35

hint:强制索引什么时候反而更慢

hint 什么时候会让查询更慢?这篇给同一条查询逐个强制每个索引,量耗时和检查量,再看 hint 的写法、报错和对计划缓存的影响。

0.2 对 29 ms
优化器自己选 user_1 与 hint 到 status_1 的耗时(同一条查询)
563 倍
按 user 的查询被迫走 status_1 时变慢的倍数
0 个
带 hint 的查询之后计划缓存里的条目数

优化器的选择

实测 · 本机执行输出
  hint (优化器自己选)                0.2 ms
  hint status_1                 29 ms
  hint ts_1                    4.0 ms
  hint user_1                  0.2 ms
  hint status_1_ts_1           1.2 ms
  hint $natural: 1(全表)          46 ms
  test> 不 hint 时优化器的选择
  {
    plan: 'FETCH > IXSCAN(user_1)',
    nReturned: 0,
    keysExamined: 30,
    docsExamined: 30,
    ms: 0
  }

三个条件各有索引时,优化器自己选了最快的 user_1(30 个键、0.2 ms);强制其它索引都更慢:status_1 要检查 75,000 个键。

检查量

实测 · 本机执行输出
  test> hint("status_1")
  {
    plan: 'FETCH > IXSCAN(status_1)',
    nReturned: 0,
    keysExamined: 75000,
    docsExamined: 75000,
    ms: 32
  }
  test> hint("ts_1")
  {
    plan: 'FETCH > IXSCAN(ts_1)',
    nReturned: 0,
    keysExamined: 10000,
    docsExamined: 10000,
    ms: 4
  }
  test> hint("user_1")
  {
    plan: 'FETCH > IXSCAN(user_1)',
    nReturned: 0,
    keysExamined: 30,
    docsExamined: 30,
    ms: 0
  }
  test> hint("status_1_ts_1")
  {
    plan: 'FETCH > IXSCAN(status_1_ts_1)',
    nReturned: 0,
    keysExamined: 2500,
    docsExamined: 2500,
    ms: 1
  }

耗时几乎正比于检查的键数:30、2500、10,000、75,000 个键分别对应 0、1、4、32 ms。

写法与报错

实测 · 本机执行输出
  test> hint({status: 1, ts: 1})    // 键模式
  {
    plan: 'FETCH > IXSCAN(status_1_ts_1)',
    nReturned: 0,
    keysExamined: 2500,
    docsExamined: 2500,
    ms: 1
  }
  test> hint("nope_1")    // 不存在的索引
  {
    code: 2,
    message: 'error enumerating plans for query: ns=test.tTree: $and\n' +
      '    status $eq "A"\n' +
      '    user $eq 1234\n' +
      '    ts $gte 290000\n' +
      'Sort: {}\n' +
      'Proj: {}\n' +
      ' planner returned error :: caused by :: hint provided does not correspond to an existing index'
  }
  test> hint({status: -1})    // 存在 status_1,但键模式方向不同
  {
    code: 2,
    message: 'error enumerating plans for query: ns=test.tTree: $and\n' +
      '    status $eq "A"\n' +
      '    user $eq 1234\n' +
      '    ts $gte 290000\n' +
      'Sort: {}\n' +
      'Proj: {}\n' +
      ' planner returned error :: caused by :: hint provided does not correspond to an existing index'
  }
  test> hint({$natural: -1})    // 反向全表
  {
    plan: 'COLLSCAN',
    nReturned: 0,
    keysExamined: 0,
    docsExamined: 300000,
    ms: 52
  }

可以用索引名或键模式;不存在的索引、方向不同的键模式报 code 2;{$natural: 1} / -1 强制正向或反向全表扫描。

不进缓存

实测 · 本机执行输出
  test> 连续 3 次带 hint("ts_1") 的查询之后缓存条目数
  0
  test> 连续 3 次不带 hint 的同形状查询之后缓存条目数
  1

带 hint 的查询不建计划缓存条目,每次都按 hint 执行。

用错的代价

实测 · 本机执行输出
  find({user: 777}):不 hint 0.2 ms;hint status_1 114 ms(563 倍);hint $natural 43 ms(213 倍)
  test> db.t.find({user: 777}).hint("status_1")
  {
    plan: 'FETCH > IXSCAN(status_1)',
    nReturned: 30,
    keysExamined: 300000,
    docsExamined: 300000,
    ms: 125
  }

本来 0.2 ms 的按 user 查询被迫用 status_1,要检查 300,000 个键,慢 563 倍。

其它命令

实测 · 本机执行输出
  test> aggregate([{$match: {user: 777}}], {hint: "status_1"})
  {
    plan: 'FETCH > IXSCAN(status_1)',
    nReturned: 30,
    keysExamined: 300000,
    docsExamined: 300000,
    ms: 125
  }
  test> db.t.countDocuments({user: 777}, {hint: "user_1"})
  30
  test> update 带 hint: "status_1" 的 explain
  { stage: 'UPDATE', docsExamined: 300000, keysExamined: 300000 }

aggregate、countDocuments、update / delete 都支持 hint。

分步演示

hint 强制索引
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。30 万个文档,查询结果为空,重点比较检查量与耗时;耗时是 pymongo 9 次中位数(慢的 3~5 次)
    • 没有验证:hint 与分片集群、与 sort 的组合;hint 的索引被 hideIndex 之后的行为;索引过滤器(indexFilters)与 hint 的优先级;MongoDB 的 Query Settings(8.0 引入的集群级 hint)的作用;hint 到部分索引时的结果完整性(见部分索引一篇)
    京ICP备2026067502号-1京公网安备11010502063487号