bigywater.tech
MongoDB · 23

视图:视图就是存下来的 pipeline

视图是物化的吗?能走索引吗?这篇建了两个视图,看 listCollections 和 system.views 里存了什么,用 explain 对比条件能不能前移到 $group 之前,再试各种写入和限制。

150 对 3000
对分组视图加过滤条件时,底层检查的文档数:条件在分组键上(走索引)对在聚合结果上(全表)
20 层
视图套视图的最大深度
只是 pipeline
system.views 里只存 viewOn 和 pipeline,没有数据

视图是什么

实测 · 本机执行输出
  test> db.createView("paid_orders", "orders", [{$match: {status: "paid"}}, {$project: {secret: 0}}])
  { ok: 1 }
  test> db.getCollectionInfos({name: "paid_orders"})
  [
    {
      name: 'paid_orders',
      type: 'view',
      options: {
        viewOn: 'orders',
        pipeline: [
          { '$match': { status: 'paid' } },
          { '$project': { secret: 0 } }
        ]
      },
      info: { readOnly: true }
    }
  ]
  test> db.system.views.find()
  [
    {
      _id: 'test.paid_orders',
      viewOn: 'orders',
      pipeline: [ { '$match': { status: 'paid' } }, { '$project': { secret: 0 } } ]
    }
  ]
  test> db.getCollectionInfos().map(c => c.name + ":" + c.type)
  [ 'system.views:collection', 'orders:collection', 'paid_orders:view' ]

视图在 system.views 里只存 viewOn 和 pipeline,类型是 view,只读,不含任何数据。

不物化

实测 · 本机执行输出
  test> db.paid_orders.countDocuments({})
  1000
  test> db.orders.updateMany({_id: {$lt: 6}}, {$set: {status: "paid"}}).modifiedCount
  4
  test> db.paid_orders.countDocuments({})
  1004
  test> db.paid_orders.findOne({_id: 3})
  { _id: 3, user: 'u3', status: 'paid', amount: 13 }

每次查询都重新执行 pipeline,所以底层数据一改,视图立刻跟着变。

索引与前移

实测 · 本机执行输出
  test> db.createView("by_user", "orders", [{$group: {_id: "$user", total: {$sum: "$amount"}}}])
  { ok: 1 }
  视图 paid_orders = orders 上 [{$match: {status: "paid"}}, {$project: {secret: 0}}]:
  test> db.paid_orders.find({user: "u3"}).explain("executionStats")
  {
    plan: 'PROJECTION_SIMPLE > FETCH > IXSCAN(user_1)',
    optimizedPipeline: true,
    totalKeysExamined: 150,
    totalDocsExamined: 150,
    nReturned: 51
  }
  对照:直接在 orders 上查同样的条件
  test> db.orders.find({status: "paid", user: "u3"}).explain("executionStats")
  {
    plan: 'FETCH > IXSCAN(user_1)',
    optimizedPipeline: undefined,
    totalKeysExamined: 150,
    totalDocsExamined: 150,
    nReturned: 51
  }
  视图 by_user = orders 上 [{$group: {_id: "$user", total: {$sum: "$amount"}}}]:
  test> db.by_user.explain("executionStats").aggregate([{$match: {_id: "u3"}}])    // 条件在分组键 _id 上
  {
    plan: 'GROUP > FETCH > IXSCAN(user_1)',
    optimizedPipeline: true,
    totalKeysExamined: 150,
    totalDocsExamined: 150,
    nReturned: 1
  }
  test> db.by_user.explain("executionStats").aggregate([{$match: {total: {$gt: 1000}}}])    // 条件在聚合结果 total 上
  {
    plan: 'MATCH > GROUP > COLLSCAN',
    optimizedPipeline: true,
    totalKeysExamined: 0,
    totalDocsExamined: 3000,
    nReturned: 20
  }

视图自己不能建索引,查询用底层集合的索引。优化器会把对视图的过滤条件尽量前移:在分组键上的条件前移到 $group 之前,走底层索引;在聚合结果字段上的条件前移不了,只能全表扫描。

限制

实测 · 本机执行输出
  test> db.paid_orders.insertOne({a: 1})
  Namespace test.paid_orders is a view, not a collection
  test> db.paid_orders.createIndex({user: 1})
  Namespace test.paid_orders is a view, not a collection
  test> db.paid_orders.updateOne({_id: 3}, {$set: {a: 1}})
  Namespace test.paid_orders is a view, not a collection
  test> db.paid_orders.find().sort({$natural: 1}).toArray().length
  1004
  test> db.paid_orders.renameCollection("x")
  cannot rename view: test.paid_orders

对视图的写入、建索引、更新、rename 都会报错;$natural 排序可以。

深度

实测 · 本机执行输出
  连续把 lv(i-1) 作为 lv(i) 的 viewOn 创建视图:成功到 lv20,下一个报错: View depth limit exceeded; maximum depth is 20, full error: {'ok': 0.0, 'errmsg': 'View depth limit exceeded; maximum depth is 20', 'code': 

视图套视图最多 20 层。

底层集合与修改

实测 · 本机执行输出
  test> db.paid_orders.find().toArray().length
  0
  test> db.getCollectionInfos({name: "paid_orders"})[0].options.viewOn
  orders
  test> db.v2.countDocuments({})
  3
  test> db.runCommand({collMod: "v2", viewOn: "src2", pipeline: [{$match: {k: {$gte: 3}}}]})
  { ok: 1 }
  test> db.v2.countDocuments({})
  7

底层集合被删除后视图仍在,查询返回空;collMod 可以直接修改视图的 pipeline。

分步演示

视图就是存下来的 pipeline
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。计划用 explain("executionStats") 取回,helper 函数把 inputStage 链拼成一行
    • 没有验证:视图上的 collation 与 $lookup / $unionWith 的行为;视图与 $text、$geoNear 的限制;视图查询的权限(基于角色的访问控制);分片集合上的视图;物化视图($merge / $out)的刷新策略;视图 pipeline 里使用 $lookup 时的索引选择
    京ICP备2026067502号-1京公网安备11010502063487号