mongodb

发布时间 2023-12-11 09:27:38作者: primaryC

mongodb内部培训

目录

mongodbV4.4文档:

https://www.mongodb.com/docs/v4.4/

对于一些基础的安装、crud操作和查询,可以通过官方的文档和教程进行学习,本文内容只涉及开发人员知识。不涉及运维、部署等内容。

简介

MongoDB是一种开源的文档型数据库管理系统,它使用文档来存储数据,文档是一种类似于JSON的数据结构,可以包含键值对、数组和嵌套文档。这种灵活的数据模型使得MongoDB适用于各种类型的数据。

基础概念

  1. 数据库(Database):数据库是MongoDB中的顶层容器,用于存储相关数据的集合。每个数据库都有自己的权限和安全性设置,并且可以在同一台服务器上同时存在多个数据库。在MongoDB中,可以使用命令use <database_name>来切换到指定的数据库。
  2. 集合(Collection):集合是数据库中的一个逻辑分组,类似于关系型数据库中的表。它是一组相关文档的容器,每个文档都可以有不同的结构。集合中的文档可以根据需要动态地添加或删除字段,没有固定的模式要求。在MongoDB中,可以使用命令db.<collection_name>来访问指定的集合。
  3. 文档(Document):文档是MongoDB中的基本数据单元,类似于关系型数据库中的行。它是一个键值对的集合,可以包含各种类型的数据,如字符串、数字、日期、数组、嵌套文档等。文档使用类似于JSON的格式表示,具有灵活的结构。在MongoDB中,文档以BSON(Binary JSON)格式存储,这种格式可以有效地表示复杂的数据结构

和关系型数据库的对应关系

适用场景

  1. 非结构化的数据结构

    我觉得这是相比于mysql等关系型数据库最主要的区别,也是选用mongodb的主要原因~

    MongoDB的文档型数据模型非常灵活,可以根据需要动态地添加或删除字段,没有固定的模式要求。这使得MongoDB适用于需要频繁变化的数据结构或者需要存储半结构化数据的场景。

  2. 大数据量和高性能要求

    在mongodb中常用的数据都缓存在内存里,这样大部分操作只需要读内存,自然很快。此外,文档模式设计一般会是的你所需要的数据都相对集中在一起(这也应该是我们选则monogodb存储数据的最佳范式,即内联文档),减少了关系性数据库需要从各个地方去把数据找过来(然后Join)所耗费的随机读时间。

  3. 复杂的查询需求

    MongoDB提供了强大的查询语言和丰富的查询功能,包括查询操作符、聚合管道和地理空间查询等。这使得在MongoDB中执行复杂的查询变得更加简单和高效。

  4. 天生适用于云原生和微服务架构

    ...

数据建模

mongodb中数据模型设计的最佳实践

mongodb中支持两种数据模型:

嵌入式数据模型

MongoDB可以将相关数据嵌入到单个结构或文档中。这些模式通常被称为“非规范化”模型,如下图:

嵌入式模型应该是我们使用mongodb优先考虑的数据结构,通过使用这种结构,我们可以使用更少的查询和更新语句来完成操作。

同时我们也能减少使用事务操作。

标准化数据模型

和关系型数据库一致,如图:

在mongodb中,通常在以下几种情况下,才考虑这种数据结构:

  1. 嵌入的数据量过多,且包含了很多热门业务不需要的数据,这反而回降低读取操作的速度。

    好比我们的model_origin表。质量、进度、造价都有各自的业务属性。各自主流业务也只关心自己的数据,所以拆开反增加访问速度,和降低技术负债。

  2. 更复杂的多对多关系

    多对多的关系中,使用传统的关系型数据架构能够有更清晰的业务逻辑,数据的一致性保持也更简单,所以多对多不建议使用嵌入式方式。

N对N关系

  • 一对一: 看业务,大多数情况使用嵌入式
  • 一对多: 看业务,大多数使用折中方案, 部分嵌入,部分走引用。
  • 多对多: 关系型

事务

在monogodb中,写操作在单个文档级别上是天生原子的,即使该操作修改了单个文档中的多个嵌入文档(这也是为什么建议优先使用嵌入的原因)。

当单个写入操作修改多个文档,每个文档的修改都是原子的,但操作作为一个整体不是原子的。此时需要引入分布式事务处理。

MongoDB支持多文档事务:

  • 在版本4.0中,MongoDB支持副本集上的多文档事务。
  • 在4.2版本中,MongoDB引入了分布式事务,增加了对分片集群上多文档事务的支持,并结合了对副本集上多文档事务的现有支持。此后mongodb中 【分布式事务】和【多文档事务】是一个意思。
  • ...

在大多数情况下,多文档事务比单文档写入带来更大的性能成本,并且多文档事务的可用性不应取代有效的模式设计。对于许多场景,反规范化的数据模型(嵌入文档和数组)将继续是最适合您的数据和用例的。也就是说,对于许多场景,适当地对数据建模将最大限度地减少对多文档事务的需求。

生产经验

下面列一些实际使用中遇到的关于事务失败的才坑点,详细内容,参看官方文档

  1. 运行时间的限制

    默认情况下,事务的运行时间必须小于一分钟。您可以对 mongod 实例使用 transactionLifetimeLimitSeconds 修改此限制。对于分片集群,必须为所有分片副本集成员修改该参数。超过此限制的事务将被视为已过期,并将由定期清理进程中止。

    //设置事务超时为30s
    db.adminCommand( { setParameter: 1, transactionLifetimeLimitSeconds: 30 } )
    //也可以添加启动参数
    mongod --setParameter "transactionLifetimeLimitSeconds=30"
    
    
  2. Oplog 操作日志的大小限制

    ?先解释什么是oplog:

    oplog(操作日志)是MongoDB中的一种特殊的集合,用于记录数据库的操作。它是一个有序的、循环的、固定大小的集合,用于支持数据复制和故障恢复。

    每当在MongoDB中进行写操作(如插入、更新或删除文档)时,这些操作都会被记录到oplog中。oplog中的每个条目都包含了一个完整的操作,包括操作类型、操作的命名空间(数据库和集合名称)、操作的具体内容(如插入的文档、更新的字段和值等)以及操作的时间戳。

    oplog的主要作用是支持数据复制和故障恢复。当MongoDB部署为复制集时,主节点将写操作记录到自己的oplog中,并将这些操作异步地传播给其他副本集成员。这样,其他副本集成员可以通过读取主节点的oplog来复制主节点上的写操作,从而保持数据的一致性。

    此外,oplog还用于故障恢复。当主节点发生故障时,副本集中的其他成员可以使用自己的oplog来重放主节点上的写操作,以恢复数据的一致性,并选举新的主节点。

    需要注意的是,oplog是一个固定大小的集合,一旦达到其限制,最旧的条目将被删除以腾出空间给新的操作。因此,如果写操作过于频繁或oplog的大小设置不合理,可能会导致较旧的操作被丢失。

    ps. 可以通过设置oplog的大小,来减少mongodb的磁盘占用。
    MongoDB创建尽可能多的oplog条目来封装事务中的所有写操作,而不是为事务中的所有写操作创建单个条目。这消除了由单个oplog条目对其所有写入操作强加的16MB总大小限制。尽管删除了总大小限制,但每个oplog条目仍必须在BSON文档大小限制16MB之内。
    上面这句话人话就是,事务中的crud操作,mongodb尽自己最大努力来拆分成不同的oplog条目了。但是可能还是存在几个操作在一个条目里,而每一个oplog条目不能超过BSON文档大小限制16MB。

  3. WiredTiger Cache的限制

    ?WiredTiger :MongoDB的一种存储引擎,它是MongoDB 3.2版本及以后版本的默认存储引擎。WiredTiger以其高性能、高度可扩展性和优化的数据压缩功能而闻名,支持事务。ps 还有MMAPv1和In-Memory存储引擎。
    如果在mongodb-server中有一个未提交的事务超过 WiredTiger cache size 的5%,该事务也会中止并返回一个写冲突错误。
    WiredTiger cache size:顾名思义,存储引擎的缓存大小。详情点击查看

    从MongoDB 3.4开始,默认的WiredTiger内部缓存大小是以下两者中较大的一个:(内存大小-1GB )*0.5,或256 MB。

一般查询符

  1. 使用比较操作符进行高级查询:

    • 等于(e)、不等于(ne)、大于(gt)、小于(lt)、大于等于(gte)、小于等于(lte)等操作符的使用。
    • 使用范围查询(in、nin)来匹配多个值。
    • 使用逻辑操作符(and、or、$not)来组合多个查询条件。
  2. 使用数组操作符进行高级查询:

    • 使用$elemMatch操作符来匹配数组中满足特定条件的元素。
    • 使用$size操作符来匹配数组长度。
    • 使用$all操作符来匹配包含指定元素的数组。
  3. 正则表达式查询:

    • 使用正则表达式来进行模糊匹配。
    • 使用$regex操作符来进行正则表达式查询。
  4. 指定查询返回内容

    findPublisher = collection.find(eq("status", "A")).projection(include("item", "status"));
    
  5. 文本搜索:

    MongoDB的文本搜索功能支持对字符串内容执行文本搜索的查询操作。为了执行文本搜索,MongoDB使用文本索引$text 操作符。

    业务中用的不多

Null

在mongodb中对于一个文档字段,空的定义有两种。 一种是Null,即该文档有这个属性,但是为Null。(和java中的对象属性null差不多)。还有一种是该集合中有这个属性,但是集合中的这个文档没有这个属性。此时属性即为缺失字段。

MongoDB中不同的查询运算符对 null 值的处理方式不同。

举例说明:好比现在在inventory集合中插入以下两条数据。其中一条数据item为null,一条缺失属性item

collection.insertMany(asList(
        Document.parse("{'_id': 1, 'item': null}"),
        Document.parse("{'_id': 2}")
));
  1. eq查询

    FindIterable<Document> findIterable = collection.find(eq("item", null));
    

    查询将返回集合中的两个文档。

  2. type查询

    这里type("item", BsonType.NULL) 查询仅匹配包含值为 null 的 item 字段的文档;即item 字段的值是BSON类型 Null:

    findIterable = collection.find(type("item", BsonType.NULL));
    
    
  3. exists查询

    findIterable = collection.find(exists("item", false));
    

    查询只返回不包含 item 字段的文档。

大数据量查询

对于大数据量查询,可采取以下优化手段来提高查询性能:

  1. 索引优化:创建合适的索引是提高查询性能的关键。

  2. 分片、分表、拆表:

    1. 分片是将表的数据分散存储在多个节点上,每个节点负责一部分数据。这种类似于拓展了底层硬件的存储能力,不涉及业务结构上的改动。
    2. 分表: 水平拆表—>可以按照用户ID、地理位置或其他业务规则将数据分散到不同的数据库中。 如我们的model_origin表
    3. 拆表:垂直拆表—>将一个大表按照某种规则或条件拆分成多个较小的表。每个拆分后的表通常包含相同的字段结构,但存储不同的数据子集。例如,可以按照时间范围、地理位置或其他业务规则将数据分散到不同的表中。表的分解可以提高查询性能,减少单个表的数据量,从而减轻数据库的负载。
      对于业务上的分表、拆表都会带来业务代码逻辑上的改动,有的拆分反而会丢掉了mongodb作为非结构化数据库带来的优点,增加了代码负债(cost表,经历过拆分,又重构为一张表的过程)。
      延深: 分表的同时有必要分库么,分库带来了啥优点、缺点~
  3. 分批处理

    参考com.bdip.mongodb.util.MongoPageUtil

    分批处理可以通过分页、游标等方式。

    • 游标

      使用游标可以分批获取大量数据,而不是一次性获取全部数据。这对于处理大型数据集或网络传输较慢的情况非常有用。通过逐批获取数据,可以减少内存消耗和超时风险。

      ?注意:对于包含排序操作但不包含索引的查询,服务器必须在返回任何结果之前加载内存中的所有文档以执行排序。所以索引很重要~

                      //1. 定义一个mongodb 查询迭代器
                      FindIterable<ModelOrigin> oldModelOriginIterable = mongoOption.option(oldModelNamePayload, ModelDBTypeEnum.origin, ModelOrigin.class,
                              collection -> collection.find(in(eidCol, eidSet)));
                      //2. 定义自己的实体处理逻辑
                      Consumer<List<ModelOrigin>> consumer = perModelOriginList -> {
                          List<String> oldEidList = perModelOriginList.stream().map(ModelOrigin::getEid).collect(Collectors.toList());
                          Map<String, ModelOrigin> eidEntityIdMap = new HashMap<>();
                          log.info("开始晨曦cost属性复制");
                          mongoOption.option(newModelNamePayload, ModelDBTypeEnum.origin, ModelOrigin.class, coll -> coll.find(in(eidCol, oldEidList)))
                                  .forEach(newOrigin -> eidEntityIdMap.put(newOrigin.getEid(), newOrigin));
                          Set<String> newEidSet = eidEntityIdMap.keySet();
                          List<BsCost> newBsCostList = IterUtil.toList(mongoOption.option(bsId, cloud_cost_bs_cost, BsCost.class, collection -> collection.find(
                                  buildBsCostFilters(oldModelNamePayload, ne(colName(BsCost::getDataBelong), DataBelong.MODEL_MAPPING), in(eidCol, newEidSet))
                          )).map(item -> {
                              item.setEntityId(eidEntityIdMap.get(item.getEid()).getEntityId());
                              item.setVname(eidEntityIdMap.get(item.getEid()).getVname());
                              item.setModelVersion(newModelVersion);
                              item.setId(null);
                              return item;
                          }));
                          mongoOption.option(bsId, cloud_cost_bs_cost, BsCost.class, collection -> collection.insertMany(newBsCostList));
                          if (isActivateInheritance) {
                              log.info("开始数据继承..");
                              inheritCostProcess(new ArrayList<>(newEidSet), bsId, oldModelNamePayload, newModelNamePayload);
                              //查询删除的
                              List<String> delEids = (List<String>) CollUtil.subtract(oldEidList, new ArrayList<>(newEidSet));
                              deleteCostProcess(delEids, bsId, oldModelNamePayload);
                          }
                      };
                      //3. 调用游标工具方法
                      MongoPageUtil.pageProcess(oldModelOriginIterable, consumer, modelMappingProperties.getComponentSearchPageSize());
      
          /**
           * mongodb的游标分页处理函数
           *
           * @param dataIterable     mongo数据迭代器
           * @param pageDataConsumer 分页数据处理函数
           * @param pageSize         分页大小
           * @param <T>              数据类型
           * @author liangtao
           * @date 2022/5/12
           **/
          public static <T> void pageProcess(MongoIterable<T> dataIterable, Consumer<List<T>> pageDataConsumer, int pageSize) {
              List<T> dataCacheList = new ArrayList<>(pageSize);
              //这里的batchSize 就是告诉mongo server一次传输的数据量大小 .cursor就是启用游标,返回一个游标对象
              try (MongoCursor<T> dataCursor = dataIterable.batchSize(pageSize).cursor()) {
                  while (dataCursor.hasNext()) {
                      dataCacheList.add(dataCursor.next());
                      if (dataCacheList.size() >= pageSize) {
                          pageDataConsumer.accept(dataCacheList);
                          dataCacheList.clear();
                      }
                  }
                  if (!dataCacheList.isEmpty()) {
                      pageDataConsumer.accept(dataCacheList);
                  }
              }
          }
      
    • 分页处理

      涉及内部业务的建议使用游标,涉及返回给前端分页的,使用分页处理。

              //1. 查询总数的逻辑
              Bson[] countAggregate = new Bson[]{
                      match(
                              and(in(CostConstants.ID, boxSelectionComponentIds), queryBson, in("last3.projectCode", searchVo.getProjectCodes()))
                      ),
                      project(meteringCostVoProject)
              };
              //2. 查询总数 
              Integer count = mongoOption.option(bsRootId, cloud_cost_bs_cost, coll -> MongoPageUtil.getSearchCount(coll, countAggregate));
              //3. 分页大小,ps这里如果count=0要设置为1,不然mongodb查询报错
              int limit = searchVo.getLimit() == -1 ? Math.max(1, count) : searchVo.getLimit();
              //4. 查询page
              List<MeteringCostVo> dataList = mongoOption.option(bsRootId, cloud_cost_bs_cost, MeteringCostVo.class,
                      coll -> MongoPageUtil.page(coll, limit, searchVo.getPage(), countAggregate));
      
          
          /**
           * mongodb查询总数
           *
           * @param collection 操作符
           * @param pipeline   aggregate管道
           * @return int
           * @author liangtao
           * @date 2022/9/6
           **/
          public static int getSearchCount(MongoCollection<Document> collection, Bson... pipeline) {
              List<Bson> pipelineList = new ArrayList<>();
              if (pipeline != null && pipeline.length > 0) {
                  pipelineList.addAll(Arrays.asList(pipeline));
              }
              pipelineList.add(Aggregates.count());
              return getSearchCount(collection.aggregate(pipelineList));
          }
          
          /**
           * mongodb查询总数
           *
           * @param dataIterable 数据iterable
           * @return int
           * @author liangtao
           * @date 2022/9/6
           **/
          public static int getSearchCount(MongoIterable<Document> dataIterable) {
              MongoCursor<Document> iterator = dataIterable.iterator();
              return iterator.hasNext() ? iterator.next().getInteger("count") : 0;
          }
          
          /**
           * mongodb分页查询
           * @author liangtao
           * @date 2022/9/6
           * @param collection 操作
           * @param pageSize 每页大小
           * @param currentPage 当前页
           * @param pipeline aggregate管道
           * @return java.util.List<R>
           **/
          public static <R> List<R> page(MongoCollection<R> collection, int pageSize, int currentPage, Bson... pipeline) {
              List<Bson> pipelineList = new ArrayList<>();
              if (pipeline != null && pipeline.length > 0) {
                  pipelineList.addAll(Arrays.asList(pipeline));
              }
              pipelineList.add(skip(pageSize * (currentPage - 1)));
              pipelineList.add(limit(pageSize));
              List<R> result=new ArrayList<>(pageSize);
              return collection.aggregate(pipelineList).into(result);
          }
      

?查询也好、更新也罢都建议进行合理大小的批量处理。(mongodb一般都是限制16Mb一次)
就经验而言,索引> 分批处理> 分片、分表、拆表

索引

  • 索引是MongoDB中用于提高查询性能的重要工具。它们可以帮助数据库快速定位和访问数据,从而加快查询速度。
  • MongoDB中的索引与其他数据库系统中的索引类似。MongoDB在集合级别定义索引,并支持MongoDB集合中文档的任何字段或子字段的索引。
  • 创建集合时会在_id字段上创建一个唯一的索引。 _id 索引防止客户端插入两个具有相同 _id 字段值的文档。

创建索引

  • 行创建索引时,如果没有这个集合会自动创建集合。
  • 很多业务情况是在没有这个集合的情况下执行插入操作后创建的集合。此时应该在何时创建索引呢?
    1. 每次插入时通过db.collection.getIndexes()检查有无索引,没有则创建
    2. 服务启动脚本→去检查有无这个业务表,没有时则创建表+索引。(这里又分为两种情况,动态集合→根据业务生成的集合、静态集合→事先定好的集合)
collection.createIndex(Indexes.descending("name"));

索引名称

索引的默认名称是索引键和索引中每个键的方向的级联(即1或-1)使用下划线作为分隔符。例如,在 { item : 1, quantity: -1 } 上创建的索引具有名称 item_1_quantity_-1

可以创建具有自定义名称的索引,可以创建比默认名称更容易理解的索引。例如下面创建了一个查询模型的索引

db.xxxx.createIndex(
  { modelId: 1, modelVersion: -1 } ,
  { name: "query for model" }
)

可以使用 db.collection.getIndexes() 方法查看索引名称。索引一旦创建,就无法重命名。相反,您必须删除并使用新名称重新创建索引。

索引类型

MongoDB支持多种类型的索引,详细内容请点击查看。包括:

单字段索引

最简单的索引类型,基于单个字段创建。可以根据字段的值快速定位和访问文档。

复合索引

基于多个字段创建的索引。可以同时根据多个字段的值进行查询和排序。

复合索引中列出的字段的顺序具有重要意义。例如,如果复合索引由 { userid: 1, score: -1 } 组成,则索引首先按 userid 排序,然后在每个 userid 值内按 score 排序。

多键索引

MongoDB使用多键索引来索引存储在数组中的内容。如果索引一个包含数组值的字段,MongoDB会为数组的每个元素创建单独的索引条目。这些多键索引允许查询通过匹配数组的一个或多个元素来选择包含数组的文档。如果索引字段包含数组值,MongoDB会自动判断是否创建多键索引;不需要显式指定多键类型。

地理空间索引

文本搜索索引

散列索引

索引的属性

唯一索引

唯一索引确保索引字段不存储重复值;即强制索引字段的唯一性。默认情况下,MongoDB在创建集合时会在_id字段上创建一个唯一的索引。

部分索引

部分索引只对集合中满足指定筛选表达式的文档进行索引。通过对集合中的文档子集进行索引,部分索引具有较低的存储需求,并降低了索引创建和维护的性能成本。

可以理解为条件索引,只对满足指定条件的子集建立索引

例如,下面的操作创建一个复合索引,该索引仅索引具有大于5的 rating 字段的文档

db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)

如果使用索引导致结果集不完整,MongoDB将不会使用部分索引进行查询或排序操作。

db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )  //会使用索引
db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } ) //不会使用索引
db.restaurants.find( { cuisine: "Italian" } ) //不会使用

稀疏索引

稀疏索引仅包含具有索引字段的文档的条目,即使索引字段包含空值。索引将跳过缺少索引字段的任何文档。

db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } ) //此时建立的索引不包括 不包含 xmpp_id 字段的文档。

优先考虑使用部分索引。稀疏索引其实只是部分索引的一个子集(筛选表达式为 字段不为空)

Hidden Indexes

。。。

查询性能评估

请结合gpt食用

  1. 执行(Explain)

    cursor.explain("executionStats")db.collection.explain("executionStats") 方法提供有关查询性能的统计信息。

    https://www.mongodb.com/docs/v4.4/reference/explain-results/

  2. 索引统计信息

    可以使用db.collection.stats()方法来获取集合的索引统计信息。索引统计信息包括索引的大小、索引的使用情况等。通过分析索引统计信息,可以了解索引的大小和效率,返回结果中的indexSizes字段会显示索引的大小,indexDetails字段会显示每个索引的详细信息。

Aggregation查询

Aggregation(聚合)操作可以以管道的形式分阶段处理、关联多个文档,并返回最终的计算结果。

  • 每个阶段对输入文档执行一个操作。例如,阶段可以过滤文档、对文档进行分组和计算值。
  • 从一个阶段输出的文档被输入到下一个阶段。ps.除了 $out$merge$geoNear$changeStream
  • 聚合管道可以返回文档组的结果。例如,返回总计、平均值、最大值和最小值。
  • 聚合管道不会修改集合中的文档,对于管道中的修改操作,试试修改当前流水线中的数据,除非管道包含 $merge$out 阶段。

管道优化

聚合流水线操作具有优化阶段,mongodb的优化器一般情况会自动尝试重塑流水线以提高性能,但是有时候我们还是需要主动遵循一些查询范式,来优化查询(存在优化器没有自动优化的情况),下面列出几个常用的优化范式

  1. project优化

    聚合管道可以确定它是否仅需要文档中的字段的子集来获得结果。如果是这样,管道将只使用那些必需的字段,从而减少通过管道的数据量。

  1. 管道顺序优化

    如果聚合流水线包含多个投影或 $match 阶段,MongoDB对每个 $match 阶段执行此优化,将每个 $match 过滤器移动到过滤器不依赖的所有投影阶段之前。

  2. sort优化

    $sort 后面跟着 $match 时, $match 移动到 $sort 之前,以最小化要排序的对象数量。即 sort应该尽可能的放在最后

  3. skip优化

    当序列中 $project 后面跟着 $skip 时, $skip 移动到 $project 之前。

  4. 阶段合并优化

    此阶段,目前测试 mongodb的优化器一般都能自己进行合并优化。

    对于能合并的阶段,因尽可能的合并成一个阶段进行处理,避免创建大量中间文档。

    • 多个连续的match、skip,limit等操作应该合并成一个命令。
    • lookup和unwind操作
      {
        $lookup: {
          from: "otherCollection",
          as: "resultingArray",
          localField: "x",
          foreignField: "y"
        }
      },
      { $unwind: "$resultingArray"}
      
      优化器可以将 $unwind 阶段合并为 $lookup 阶段。如果使用 explain 选项运行聚合,则 explain 输出将显示合并的阶段:
      {
        $lookup: {
          from: "otherCollection",
          as: "resultingArray",
          localField: "x",
          foreignField: "y",
          unwinding: { preserveNullAndEmptyArrays: false }
        }
      }
      

高频命令

lookup

同一数据库中的未分片集合[1]执行左外联接,以从“联接”集合中筛选文档以进行处理。对于每个输入文档, $lookup 阶段添加一个新的数组(一般使用unwind展开)字段,其元素是来自“联接”集合的匹配文档。该 $lookup 阶段将这些重新塑造的文档传递到下一阶段

  • 常用用法

    左链接一般使用单个指定的字段进行等值查询,一般使用下面的lookup语法

    • 语法
      {
         $lookup:
           {
             from: <collection to join>,
             localField: <field from the input documents>,
             foreignField: <field from the documents of the "from" collection>,
             as: <output array field>
           }
      }
      
      属性 说明
      from 指定同一数据库中要执行联接的集合。
      localField 指定当前集合的等值查询字段
      foreignField from集合的等值查询字段
      as 指定要添加到输入文档的新数组字段的名称。新数组字段包含集合中的匹配文档 from 。如果输入文档中已存在指定的名称,则会覆盖现有字段。

) | |

  • 复杂连接查询

    为了在两个集合之间执行不相关的子查询、或者允许多个相等匹配之外的其他连接条件,该 $lookup 阶段具有以下语法:

    {
       $lookup:
         {
           from: <collection to join>,
           let: { <var_1>: <expression>, …, <var_n>: <expression> },
           pipeline: [ <pipeline to execute on the collection to join> ],
           as: <output array field>
         }
    }
    
    属性 说明
    from 指定同一数据库中要执行联接的集合。
    let 1. 指定要在下面的pipeline阶段中使用的变量。使用变量表达式访问文档输入阶段中的字段。 2. 若要引用流水线阶段中的变量,请使用 $$<variable>语法。 3. 在pipeline中使用$match需要用$expr来包裹起来, $expr 允许在语法内部使用聚合表达式。 4. 如果不使用$expr包裹,$match可以引用最外层的文档字段,但是let中定义的字段就无法引用到了。 5. 索引问题: expr包裹后的查询,只有等值查询才能使用到from中的集合索引,别的查询都无法使用索引。 6. 对于下面的pipeline的非$match阶段,不要使用$expr操作符。
    pipeline 指定要在联接(from)集合上运行的管道。若要返回所有文档,请指定空管道 [] 。 pipeline无法直接访问输入文档字段。而应首先使用let定义输入文档字段的变量,然后引用阶变量
    as 指定要添加到输入文档的新数组字段的名称。新数组字段包含集合中的匹配文档 from 。如果输入文档中已存在指定的名称,则会覆盖现有字段。

unwind

从输入文档中解构数组字段,为每个元素输出一个文档。一般是跟在lookup后面使用

  • 常用用法

    可以将数组字段路径传递给 $unwind 。使用此语法时, $unwind 如果字段值为null、缺少或空数组,则不输出文档。

    指定字段路径时,请在字段名称前加上美元符号 $ ,并用引号括起来。

    { $unwind: <field path> }
    
  • 复杂的解构选项使用

    {
      $unwind:
        {
          path: <field path>, //数组字段的字段路径。若要指定字段路径,请在字段名称前加上美元符号 $ 并用引号括起来。
          includeArrayIndex: <string>,//可选。用于保存元素数组索引的新字段的名称。名称不能以$ 开头。
          preserveNullAndEmptyArrays: <boolean> //true: 如果path为空、缺失或是一个空数组,则 $unwind 输出文档; false: path 为null、missing或空数组, $unwind 则不输出文档。
        }
    }
    

group

$group阶段根据 group key将文档分组,输出是每个唯一group key的文档。

$group 不对其输出文档进行排序

  • group key

    通常是一个字段或一组字段,也可以是表达式的结果。使用$group 中的_id来设置。在group的输出阶段,group key被设置为文档的_id

    输出文档还可以包含使用累加器表达式设置其他字段。

  • 语法

    {
      $group:
        {
          _id: <expression>, // 指定Group key ,如果未null,则会将文档全部聚合成一个文档
          <field1>: { <accumulator1> : <expression1> }, //可选。使用累加器运算符计算。
    
          ...
        }
     }
    

累加器表达式

名称 描述
$accumulator 返回用户定义的累加器函数的结果。
$addToSet 对于每个分组,返回一个包含唯一表达式值的数组。数组元素的顺序是不确定的。
$avg 返回数值的平均值。忽略非数值。
$first 对于每个分组,返回第一个文档的值。仅在文档有定义的顺序时才有意义。
$last 对于每个分组,返回最后一个文档的值。仅在文档有定义的顺序时才有意义。
$max 对于每个分组,返回表达式的最大值。
$mergeObjects 返回通过合并每个分组的输入文档创建的文档。
$min 对于每个分组,返回表达式的最小值。
$push 对于每个分组,返回一个包含表达式值的数组。
$stdDevPop 返回输入值的总体标准偏差。
$stdDevSamp 返回输入值的样本标准偏差。
$sum 返回数值的总和。忽略非数值。

addFields

向文档中添加新字段。 $addFields 输出包含输入文档中所有现有字段和新添加字段的文档。

从版本4.2开始,MongoDB添加了一个新的聚合管道阶段 $set,等价于addFields
如果新字段的名称与现有字段名称相同(包括 _id ),则 $addFields 使用指定表达式的值覆盖该字段的现有值。

{ $addFields: { <newField>: <expression>, ... } }

project

将包含请求字段的文档传递到管道中的下一阶段。指定的字段可以是输入文档中的现有字段或新计算的字段。

类型转换

$convert

将值转换为指定的类型。

{
   $convert:
      {
         input: <expression>, //参数可以是任何有效的表达式
         to: <type expression>, //参数可以是解析的表达式,见下表
         onError: <expression>,  // Optional. 在转换过程中遇到错误(包括不支持的类型转换)时返回的值。参数可以是任何有效的表达式。如果未指定,则操作在遇到错误时抛出错误并停止。
         onNull: <expression>    // Optional.为空或缺少时要返回的值 input 。参数可以是任何有效的表达式。如果未指定,则 $convert 返回null input 。
      }
}

详细的转换逻辑,请参阅:https://www.mongodb.com/docs/v4.4/reference/operator/aggregation/convert/#std-label-convert-to-double

字符串标识符 数值标识符
"double" 1
"string" 2
"objectId" 7
"bool" 8
"date" 9
"int" 16
"long" 18
"decimal" 19

toXXX

除了使用convert之外,MongoDB还提供以下聚合运算符作为默认“onError”和“onNull”行为可接受时的简写:

不常用

$facet

在同一组输入文档的单个阶段中处理多个聚合管道。每个子管道在输出文档中具有其自己的字段,其中其结果被存储为文档数组$facet 阶段允许您创建多方面的聚合,这些聚合在单个聚合阶段内跨多个维度或方面表征数据。多面聚合提供多个过滤器和分类,以指导数据浏览和分析。输入文档仅传递到 $facet 阶段一次。

$facet 在同一组输入文档上启用各种聚合,而无需多次检索输入文档。每个子管道 $facet 都传递完全相同的输入文档集。这些子管道彼此完全独立,并且每个子管道输出的文档数组存储在输出文档中的单独字段中。

说人话就是,将输入数据流分成多个独立的子流,并对每个子流应用不同的聚合操作。可以同时执行多个聚合操作,每个操作都在独立的子流上进行。每个子流的结果将作为一个对象数组返回。

{ $facet:
   {
      // 为每个指定的管道指定输出字段名称outputField1。
      <outputField1>: [ <stage1>, <stage2>, ... ],
      <outputField2>: [ <stage1>, <stage2>, ... ],
      ...

   }
}
 //定义一组facet子流集合
 List<Facet> listFacet = new ArrayList<>();
 //根据每一个查询条件,构建一个子流
 listCondition.forEach(m->{
                //子流自己的管道聚合
                List<Bson> listEachFacet = new ArrayList<>();
                
                //进行管道填充...
                if(finalListNoUseDbids.size() > 0){
                    if(finalExistdbids.size() > 0){
                        List<Long> list = removeAllNew(finalExistdbids, finalListNoUseDbids);
                        listEachFacet.add(match(Filters.in("entityId",list)));
                    } else {
                        listEachFacet.add(match(Filters.nin("entityId", finalListNoUseDbids)));
                    }

                } else {
                    if(finalExistdbids.size() > 0){
                        listEachFacet.add(match(Filters.in("entityId",finalExistdbids)));
                    }
                }
                listEachFacet.add(unwind("$properties"));
                Document pushdatause = new Document("name", "$properties.name").append("value", new Document("$toString","$properties.value"));
                listEachFacet.add(group("$entityId", Accumulators.push("properties",pushdatause)));
                listEachFacet.add(project(Projections.fields(
                        Projections.fields(new Document("entityId","$_id")),
                        Projections.include("properties")
                )));
                listEachFacet.add(unwind("$properties"));

                List<Bson> listMatch = new ArrayList<>();

                //...省略逻辑代码
                //添加到
                listFacet.add(new Facet("data"+num,listEachFacet));
            });
            AggregateIterable<Document> iter2 = mongoOption.modelOption(modelId,version,ModelDBTypeEnum.origin,
                    collection -> {
                        AggregateIterable<Document> aggregateIterable = collection.aggregate(
                                asList(
                                        //这里使用facet返回
                                        facet(listFacet)
                                )).allowDiskUse(true);
                        return aggregateIterable;
                    }
            );
{
"data0":[...],
"data1":[...],
"data2":[...],
...
}

$graphLookup

递归查询使用,如查询某条数据的子、父数据。

$replaceRoot

用指定的文档替换输入文档。该操作将替换输入文档中的所有现有字段,包括 _id 字段。您可以将现有嵌入文档提升到顶层,也可以创建新文档进行提升

$regexFind

在聚合表达式中提供正则表达式(regex)模式匹配功能。如果找到匹配项,则返回包含第一个匹配项信息的文档。

#regexFindAll

在聚合表达式中提供正则表达式(regex)模式匹配功能。该运算符返回一个文档数组,其中包含每个匹配项的信息。如果未找到匹配项,则返回空数组。

$regexMatch

正则匹配

$reduce

$filter

数组筛选,根据指定的条件选择要返回的数组的子集。返回一个数组,其中只包含与条件匹配的元素。返回的元素按原始顺序排列。用于project中

$cmp

属性比较,支持内联集合的逐个属性比较。返回-1(前<后),0,1

插入and更新

...

单一数据的处理

findOneAndUpdate

上述操作符包括:

  • 加号(+):将两个数相加。
  • 减号(-):将两个数相减。
  • 乘号( *):将两个数相乘。
  • 除号(/):将一个数除以另一个数。
  • 等于号(=):将两边的值相等。

在表达式

$setIntersection

$setOnInsert

多数据处理

bulkWrite操作

/


  1. 无法左外连接分片集合,但是分片集合可以通过使用lookup左外连接未分片集合 ↩︎