玩转Mongo计算

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
简介:

  MongoDB属于 NoSql 中的基于分布式文件存储的文档型数据库,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似 json 的 bson 格式,因此可以存储比较复杂的数据类型。Mongo 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,但是写起来并不简单。若能集算器 SPL 语言结合,处理起来就相对容易多了。

   现在我们针对 MongoDB 在计算方面的问题进行讨论分析,通过集算器 SPL 语言加以改进,方便用户使用 MongoDB。现从如下情况加以说明:

  1. 单表内嵌数组结构的统计............................................... 1
  2. 单表内嵌文档求和......................................................... 3
  3. 分段分组结构................................................................ 5
  4. 同构表合并................................................................... 6
  5. 关联嵌套结构情况 1...................................................... 8
  6. 关联嵌套结构情况 2..................................................... 10
  7. 关联嵌套结构情况 3..................................................... 11
  8. 多字段分组统计........................................................... 14
  9. 两表关联查询............................................................... 16
  10. 多表关联查询............................................................. 17
  11. 指定数组查找............................................................. 19
  12. 关联表中的数组查找................................................... 20
  13. 单表内嵌数组结构的统计
    对嵌套数组结构中的数据统计处理。查询考试科目的平均分及每个学生的总成绩情况。

测试数据:
1
脚本:
db.student.aggregate( [
  {$unwind : "\$scroe"},
{$group: {
  "_id":   {"lesson":"$scroe.lesson"} ,
  "qty":{"$avg":   "$scroe.mark"}
  }
}
] ) db.student.aggregate( [
  {$unwind : "\$scroe"},
{$group: {
  "_id": {"name"   :"$name"} ,
  "qty":{"$sum" :   "$scroe.mark"}
  }
}
 ] )
由于各科分数 scroe 是按课目、成绩记录的数组结构,统计前需要将它拆解,将每科成绩与学生对应,然后再实现分组计算。这需要熟悉 unwind 与 group 组合的应用。

SPL 脚本:
2

按课目统计的总分数
3
脚本说明:
      A1:连接 mongo 数据库。
      A2:获取 student 表中的数据。
      A3:将 scroe 数据合并成序表,再按课程分组,计算平均分。
      A4:统计每个学生的成绩后返回列名为 NAME、TOTAL 的序表。new 函数表示生成新序表。
      A5:关闭数据库连接。
 
这个比较常用嵌套结构统计的例子许多人遭遇过、需要先拆解,主要是熟悉 mongodb 对嵌套数据结构的处理。

  1. 单表内嵌文档求和
    对内嵌文档中的数据求和处理, 下面要统计每条记录的 income,output 的数量和。

测试数据:
4
Mongodb脚本:

var fields = [  "income", "output"];
db.computer.aggregate([ 
   { 
      $project:{ 
         "values":{ 
            $filter:{ 
               input:{ 
                    "$objectToArray":"\$$ROOT"
               },
               cond:{ 
                  $in:[ 
                     "$\$this.k",
                     fields
                  ]
               }
            }
         }
      }
   },
   { 
      $unwind:"\$values"
   },
   { 
      $project:{ 
         key:"$values.k",
         values:{ 
            "$sum":{ 
               "$let":{ 
                  "vars":{ 
                     "item":{ 
                        "$objectToArray":"\$values.v"
                     }
                  },
                    "in":"$\$item.v"
               }
            }
         }
      }
   },
   {$sort: {"_id":-1}},
   { "$group": {
    "_id": "$_id",
    'income':{"$first":   "$values"},
    "output":{"$last":   "$values"}
    }},
]);
filter将income,output 部分信息存放到数组中,用 unwind 拆解成记录,再累计各项值求和,按 _id 分组合并数据。

SPL 脚本:
5
脚本说明:
      A1:连接数据库
      A2:获取 computer 表中的数据
      A3:将 income、output 字段中的数据分别转换成序列求和,再与 ID 组合生成新序表
      A4:关闭数据库连接。

获取子记录的字段值,然后求和,相对于 mongo 脚本简化了不少。这个内嵌文档与内嵌数组在组织结构上有点类似,不小心容易混淆,注意与上例中的 scroe 数组结构比较,写出的脚本有所不同。

  1. 分段分组结构
    统计各段内的记录数量。下面按销售量分段,统计各段内的数据量,数据如下:

6
分段方法:0-3000;3000-5000;5000-7500;7500-10000;10000 以上。

期望结果:
7
Mongo 脚本
var a_count=0;
var b_count=0;
var c_count=0;
var d_count=0;
var e_count=0;
db.sales.find({
   
}).forEach(
    function(myDoc) {
        if (myDoc.SALES <3000)   {
            a_count += 1;
        }
        else if (myDoc.SALES <5000)   {
            b_count += 1;
        }
        else if (myDoc.SALES   <7500) {
            c_count += 1;
        }
        else if (myDoc.SALES   <10000) {
            d_count += 1;
        }
        else {
            e_count += 1;
        }       
    }
    );
   
print("a_count="+a_count)
print("b_count="+b_count)
print("c_count="+c_count)
print("d_count="+d_count)
print("e_count="+e_count)
这个需求按条件分段分组,mongodb 没有提供对应的 api,实现起来有点繁琐,上面的程序是其中实现的一个例子参考,当然也可以写成其它实现形式。下面看看集算器脚本的实现。

SPL 脚本:
8
脚本说明:
      A1:定义 SALES 分组区间。
      A2:连接 mongodb 数据库。
      A3:获取 sales 表中的数据。
      A4:根据 SALES 区间分组统计员工数。其中函数 pseg()表示返回成员在序列中的区段序号,int() 表示转换成整数。
      A5:关闭数据库连接。
pseg 的使用让 SPL 脚本精简了不少。

  1. 同构表合并
    具有相同结构的多表数据合并。下面将两个员工表数据合并。

Emp1:
9
10
Mongo 脚本:
db.emp1.aggregate([
  {  "&dollar;limit": 1},
  {   "&dollar;facet": {
      "collection1": [
        {"&dollar;limit": 1},
        { "&dollar;lookup": {
          "from": "emp1",
          "pipeline": [{"&dollar;match": {} }],
          "as": "collection1"
        }}
      ],
      "collection2": [
        {"&dollar;limit": 1},
        { "&dollar;lookup": {
          "from": "emp2",
          "pipeline": [{"&dollar;match": {} }],
          "as": "collection2"
        }}
    ]
  }},
  {   "&dollar;project": {
      "data": {
     "&dollar;concatArrays": [
          {"&dollar;arrayElemAt": ["\$collection1.collection1", 0]   },
          {"&dollar;arrayElemAt": ["\$collection2.collection2", 0]   },
        ]
    }
  }},
  {  "&dollar;unwind": "\$data"},
  {  "&dollar;replaceRoot": { "newRoot": "\$data"} }
])
通过 facet 将两表数据先存入各自的数组中,然后 concatArrays 将数组合并,unwind 拆解子记录后,并将它呈现在最外层。SPL 脚本实现则没有那么多“花样”。

SPL 脚本:
12
脚本说明:
      A1:连接 mongodb 数据库。
      A2:获取 emp1 表中的数据。
      A3:获取 emp2 表中的数据。
      A4:合并两表数据。
      A5:关闭数据库连接。

熟悉 sql 语句的 mongo 初学者面对数据合并的 mongo 脚本,估计首次遇到时有点“懵”,SPL 脚本就显得自然易懂了。

  1. 关联嵌套结构情况 1
    两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在内嵌文档中。表 childsgroup 字段 childs 是嵌套数组结构,需要合并的信息 name 在其下。

history:
11
表History中的child_id与表childsgroup中的childs.id关联,希望得到下面结果:
{
    “_id” : ObjectId(“5bab2ae8ab2f1bdb4f434bc3”),
    “id” : “001”,
    “history” : “today worked”,
    “child_id” : “ch001”,
    “childInfo” :
    {
         “name” : “a”
    }
   ………………
}

Mongo 脚本
db.history.aggregate([
    {&dollar;lookup: {
        from:   "childsgroup",
        let: {child_id:   "&dollar;child_id"},
        pipeline: [
            {&dollar;match: {   &dollar;expr: { &dollar;in: [ "\$&dollar;child_id", "\$childs.id"] } } },
            {&dollar;unwind:   "&dollar;childs"},
            {&dollar;match: {   &dollar;expr: { &dollar;eq: [ "\$childs.id", "&dollar;\$child_id"] } } },
            {&dollar;replaceRoot: {   newRoot: "&dollar;childs.info"} }
            ],
            as:   "childInfo"
        }},
  {"&dollar;unwind": "\$childInfo"}
])
这个脚本用了几个函数lookup、pipeline、match、unwind、replaceRoot处理,一般 mongodb 用户不容易写出这样复杂脚本;那我们再看看 spl 脚本的实现:

SPL 脚本:

14
脚本说明:
      A1:连接 mongodb 数据库。
      A2:获取 history 表 中的数据。
      A3:获取 childsgroup 表 中的数据。
      A4:将 childsgroup 中的 childs 数据提取出来合并成序表。
      A5:表 history 中的 child_id 与表 childs 中的 id 关联查询,追加 name 字段, 返回序表。
      A6:关闭数据库连接。
相对 mongodb 脚本写法,SPL 脚本的难度降低了不少,省去了熟悉有关 mongo 函数的用法,如何去组合处理数据等,节约了不少时间。

  1. 关联嵌套结构情况 2
    两个关联表,表 A 与表 B 中的内嵌文档信息关联, 将信息合并到内嵌文档中。表 txtPost 字段 comment 是嵌套数组结构,需要把 comment_content 合并到其下。

15
Mongo 脚本
db.getCollection("txtPost").aggregate([
  {  "&dollar;unwind": "\$comment"},
  {   "&dollar;lookup": {
      "from": "txtComment",
      "localField": "comment.comment_no",
      "foreignField": "comment_no",
      "as": "comment.comment_content"
  }},
  {  "&dollar;unwind": "\$comment.comment_content"},
  {  "&dollar;addFields": { "comment.comment_content":   "&dollar;comment.comment_content.comment_content"}},
  {   "&dollar;group": {
      "_id": "&dollar;_id",
      'post_no':{"&dollar;first": "\$post_no"},
      "comment": {"&dollar;push": "\$comment"}
      }},
 
  ]).pretty()

表txtPost 按 comment 拆解成记录,然后与表 txtComment 关联查询,将其结果放到数组中,再将数组拆解成记录,将comment_content 值移到 comment 下,最后分组合并。

SPL 脚本:
16
脚本说明:
      A1:连接 mongodb 数据库。
      A2:获取 txtPost 表 中的数据。
      A3:获取 txtComment 表 中的数据。
      A4:将序表 A2 下的 comment 与 post_no 组合成序表,其中 post_no 改名为 pno。
      A5:序表 A4 通过 comment_no 与序表 A3 关联,追加字段 comment_content,将其改名为 Content。
      A6:按 pno 分组返回序表,~ 表示当前记录。
      A7:关闭数据库连接。

  1. 关联嵌套结构情况 3
    两个关联表,表 A 与表 B 中的内嵌文档信息关联, 且返回的信息在记录上。表 collection2 字段 product 是嵌套数组结构,返回的信息是 isCompleted 等字段。

测试数据:
collection1:
{
   _id: '5bc2e44a106342152cd83e97',
   description:
    {
      status: 'Good',
      machine: 'X'
     },
   order: 'A',
   lot: '1'
   };
  
collection2:
{
   _id: '5bc2e44a106342152cd83e80',
   isCompleted: false,
   serialNo: '1',
   batchNo: '2',
   product: [ // note the subdocuments here
        {order: 'A', lot: '1'},
        {order: 'A', lot: '2'}
    ]
}

期待结果
{
   _id: 5bc2e44a106342152cd83e97,
   description:
       {
         status: 'Good',
         machine: 'X',
       },
   order: 'A',
   lot: '1' ,
   isCompleted: false,
   serialNo: '1',
   batchNo: '2'
}

Mongo 脚本
db.collection1.aggregate([{
       &dollar;lookup:   {
              from:   "collection2",
              let:   {order: "&dollar;order", lot: "\$lot"},
              pipeline:   [{
                     &dollar;match:   {
                     &dollar;expr:{  &dollar;in: [ { order: "&dollar;\$order", lot: "&dollar;\$lot"},   "&dollar;product"] }
                     }  
                     }],  
                     as:   "isCompleted"
                     }  
              },   {
                     &dollar;addFields:   {
                     "isCompleted":   {&dollar;arrayElemAt: [ "\$isCompleted", 0] }
                     }  
              },   {
                     &dollar;addFields:   { // add the required fields to the top level structure
                     "isCompleted":   "&dollar;isCompleted.isCompleted",
                       "serialNo":   "&dollar;isCompleted.serialNo",
                     "batchNo":   "&dollar;isCompleted.batchNo"
              }  
}])
lookup 两表关联查询,首个 addFields获取isCompleted数组的第一个记录,后一个addFields 转换成所需要的几个字段信息

SPL 脚本:
17
脚本说明:
      A1:连接 mongodb 数据库。
      A2:获取 collection1 表 中的数据。
      A3:获取 collection2 表 中的数据。
      A4:根据条件 order, lot 从序表 A2 中查询记录,然后追加序表 A3 中的字段 serialNo, batchNo,返回合并后的序表。
      A5:关闭数据库连接。
实现从数据记录中的内嵌结构中筛选,将符合条件的数据合并成新序表。

  1. 多字段分组统计
    统计分类项下的总数及各子项数。下面统计按 addr 分类 book 数及其下不同的 book 数。

18
Mongo 脚本
db.books.aggregate([
    {   "&dollar;group": {
      "_id": {
      "addr": "&dollar;addr",
      "book": "&dollar;book"
      },
      "bookCount": {"&dollar;sum": 1}
    }},
    {   "&dollar;group": {
     "_id": "&dollar;_id.addr",
      "books": {
      "&dollar;push": {
      "book": "&dollar;_id.book",
      "count": "&dollar;bookCount"
      },
      },
     
"count": {"&dollar;sum": "\$bookCount"}
    }},
    {  "&dollar;sort": { "count": -1} },
    {   "&dollar;project": {
      
"books": {"&dollar;slice": [ "\$books", 2] },
          "count": 1
    }}
]).pretty()
先按 addr,book 分组统计 book 数,再按 addr 分组统计 book 数,调整显示顺序

SPL脚本:

19
脚本说明:
      A1:连接 mongodb 数据库。
      A2:获取books表中的数据。
      A3:按 addr,book 分组统计 book 数,
      A4:再按 addr 分组统计 book 数。
      A5:将 A4 中的 Total 按 addr 关联后合并到序表中。
      A6:关闭数据库连接。

  1. 两表关联查询
    从关联表中选择所需要的字段组合成新表。

20
Mongo 脚本
db.c1.aggregate([
    {   "&dollar;lookup": {
      "from": "c2",
          "localField": "user1",
          "foreignField": "user1",
          "as": "collection2_doc"
      }},
    {  "&dollar;unwind": "\$collection2_doc"},
    {   "&dollar;redact": {
          "&dollar;cond": [
              {"&dollar;eq": [ "\$user2",   "&dollar;collection2_doc.user2"] },
              "&dollar;\$KEEP",
              "&dollar;\$PRUNE"
          ]
      }},
    {   "&dollar;project": {
          "user1": 1,
          "user2": 1,
          "income": "&dollar;income",
          "output": "&dollar;collection2_doc. output"
      }}
      ]).pretty()
lookup 两表进行关联查询,redact 对记录根据条件进行遍历处理,project 选择要显示的字段。

SPL脚本:
21
脚本说明:
      A1:连接 mongodb 数据库。
      A2:获取c1表中的数据。
      A3:获取c2表中的数据。
      A4:两表按字段 user1,user2 关联,追加序表 A3 中的 output 字段,返回序表。
      A5:关闭数据库连接。

通过 join 把两个关联表不同的字段合并成新表。

  1. 多表关联查询
    多于两个表的关联查询,结合成一张大表。

22
合并后的结果:
{
    "_id" : ObjectId("5901a4c63541b7d5d3293766"),
    "firstName" : "shubham",
    "lastName" : "verma",
 
   "address" : {
        "address" : "Gurgaon"
    },
    "social" : {
        "fbURLs" : "http://www.facebook.com",
        "twitterURLs" : "http://www.twitter.com"
    }
}

Mongo 脚本
db.doc1.aggregate([
    {&dollar;match:   { _id: ObjectId("5901a4c63541b7d5d3293766") } },
    {
          &dollar;lookup:
        {
              from: "doc2",
              localField: "_id",
              foreignField: "userId",
              as: "address"
        }
    },
    {
          &dollar;unwind: "\$address"
    },
    {
          &dollar;project: {
              "address._id": 0,
              "address.userId": 0,
              "address.mob": 0
        }
    },
    {
          &dollar;lookup:
        {
              from: "doc3",
              localField: "_id",
              foreignField: "userId",
              as: "social"
        }
    },
    {
          &dollar;unwind: "\$social"
    },
 
  {  
    &dollar;project:   {     
             "social._id": 0,     
             "social.userId": 0
       }
 }
]).pretty();
由于 Mongodb 数据结构原因,写法也多样化,展示也各不相同。

SPL 脚本:
23
此脚本与上面例子类似,只是多了一个关联表,每次 join 就新增加字段,最后叠加构成一张大表。.

SPL 脚本的简洁性、统一性就非常明显。

  1. 指定数组查找
    从指定的数组中查找符合条件的记录。所给的数组为:["Chemical", "Biology", "Math"]。

24
Mongodb 脚本
var field = ["Chemical",   "Biology", "Math"]
db.student.aggregate([
  {   "&dollar;project": {
      "name":1,
      "lessons": {
        "&dollar;filter": {
          "input": "&dollar;lesson",
          "cond": {
            "&dollar;in": [
              "&dollar;\$this",
              field
            ]
          }
        }
        },
      }},
    {  "&dollar;project":   {"name":1,"lessons":1,"sizeOflesson":   {"&dollar;size": "&dollar;lessons"} }},
    {  &dollar;match: { "sizeOflesson":{ \$gt: 0}}}
])
查询选修课包含["Chemical", "Biology", "Math"]的同学。

SPL 脚本:
25
脚本说明:
      A1:定义查询条件科目数组。
      A2:连接 mongodb 数据库。
      A3:获取 student 表中的数据。
      A4:查询存在数组中的科目记录。
      A5:生成字段为 name, lesson 的新序表,其中符合条件的值存放在字段 lesson 中
      A6:关闭数据库连接。

集算器对给定数组中查询记录的实现更简明易懂。

  1. 关联表中的数组查找
    从关联表记录数据组中查找符合条件的记录, 用给定的字段组合成新表。

测试数据:
26
Mongo 脚本
db.users.aggregate([
  { "&dollar;lookup": {
    "from" :   "workouts",
    "localField" :   "workouts",
    "foreignField" :   "_id",
    "as" :   "workoutDocumentsArray"
  }},
  {&dollar;project: {   _id:0,workouts:0} } ,
  {"&dollar;unwind":   "&dollar;workoutDocumentsArray"},;
  {"&dollar;replaceRoot": {   "newRoot":  { &dollar;mergeObjects:   [ "&dollar;\$ROOT", "&dollar;workoutDocumentsArray"] } } },
  {$project: {   workoutDocumentsArray: 0} }
  ]).pretty()
把关联表 users,workouts 查询结果放到数组中,再将数组拆解,提升子记录的位置,去掉不需要的字段。

SPL 脚本:

27
脚本说明:
       A1:连接 mongodb 数据库。
       A2:获取 users 表中的数据。
       A3:获取 workouts 表中的数据。
       A4:查询序表 A3 的 _id 值存在于序表 A2 中 workouts 数组的记录, 并追加 name 字段, 返回合并的序表。
       A5:关闭数据库连接。
由于需要获取序列的交集不为空为条件,故将 _id 转换成序列。

  Mongo 存储的数据结构相对关联数据库更复杂、更灵活,其提供的查询语言也非常强、能适应不同的情况,需要了解函数也不少,函数之间的结合更是变化无穷,因此要掌握并熟悉应用它并非易事。集算器的离散性、易用性恰好能弥补 Mongo 这方面的不足,它降低了 mongo 学习成本及使用 mongo 操作的复杂度、难度,让 mongo 的功能得到更充分的展现,同时也希望 mongo 越来越受到广大爱好者的青睐。

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
6月前
|
NoSQL MongoDB 数据库
【MongoDB】MongoDB中执行查询
【4月更文挑战第3天】【MongoDB】MongoDB中执行查询
|
存储 NoSQL Shell
【mongo 系列】mongodb 学习六,索引浅析
索引是一种用来方便查询数据的 数据结构
105 0
|
存储 NoSQL MongoDB
【mongo 系列】mongodb 学习十,MongoDB 分片集群
之前说到了主从集群,关于主从集群的搭建以及细节后面会再次分享,这次我们先初步来看看 分片集群
148 0
|
存储 JSON 分布式计算
MongoDB:19-MongoDB-Map Reduce
MongoDB:19-MongoDB-Map Reduce
140 0
MongoDB:19-MongoDB-Map Reduce
|
存储 NoSQL 数据库
MongoDB系列二(介绍).
一、特点     学习一个东西,至少首先得知道它能做什么?适合做什么?有什么优缺点吧?     传统关系型数据库,遵循三大范式。即原子性、唯一性、每列与主键直接关联性。但是后来人们慢慢发现,不要把这些数据分散到多个表、节点或实体中,将这些信息收集到一个非规范化(也就是文档)的结构中会更有意义。
1380 0
|
NoSQL
云Mongodb Sharding如何在指定的Shard上执行Profile等命令
阿里云mongodb sharding集群处于安全的考虑用户所有的请求都是都是通过mongos来完成,而不能直连其中节点。而mongos官方目前支持的命令有限,这种情况下,用户想在某一个节点上执行一些命令,例如开启某个shard的primary节点的profile,或者某个shard的primary节点空间比较紧张执行compact命令释放空间,都无法支持;为此云mongo团队开发了一个新的命令runCommandOnShard来满足这部分需求。
3375 0