美文网首页
让我们学习下MongoDB吧~

让我们学习下MongoDB吧~

作者: 慕小牧 | 来源:发表于2020-01-06 16:42 被阅读0次

    emm...其实也是因为很久没有更新过了,再加上最近刚好系统的学完了下mongoDB,就干脆发到这上面来了。

    数据CRUD

    插入数据

    insertOne

    语法
    db.dbName.insertOne(<document>, {
        writeConcern: <document>
    });
    
    # document 插入的文档内容
    # options 插入文档的操作参数
    ## writeConcern 文档的写入级别 默认就行
    
    示例
    # 插入一条数据
    db.dbName.insertOne(
    {
        id: 1,
        name: "张三"
    })
    
    # 成功后的返回
    { "acknowledged": true, "insertedId": 1}
    
    注意事项

    acknowledged: true 表示mongodb的写入安全级别被启动,由于我们在db.dbName.insertOne命令中没有提供writeConcern文档,这里显示的是mongodb默认的安全写级别启用状态。

    insertedId是当前插入文档的Object_id

    dbName在不存在时会自动创建

    insertMany

    语法
    db.dbName.insertMany([document array], {
        writeConcern: <document>,
        ordered: <boolean>
    });
    
    # [document array] 要插入的文档集合
    # options 插入文档操作的参数
    ## writeConcern: 文档的安全写入级别 默认就行
    ## ordered: 文档的写入顺序 默认为true 按顺序插入
    
    示例
    # 插入多条数据
    db.dbName.insertMany([
        {
            name: "张三",
            age: 24
        },
        {
            name: "李四",
            age: 20
        }
    ])
    
    # 注意 insertMany 插入的是一个数组文档集合
    
    注意事项

    在使用insertMany插入多条文档的时候,在orderedtrue的情况下,如果有其中一条文档出现错误,比如主键重复之类的,那么会导致所有文档无法被插入。反之,如果ordered属性为false的话,只有出错的文档无法被插入。可以使用db.dbName.insertMany([], { ordered: false })来控制是否按顺序插入文档。

    insert

    语法
    db.dbName.insert(<document or array of documents>, {
        writeConcern: <document>,
        ordered: <boolean>
    });
    
    # <document or array of documents> 要插入的文档集合 可单个也可多个
    # options 插入文档操作的参数
    ## writeConcern: 文档的安全写入级别 默认就行
    ## ordered: 文档的写入顺序 默认为true 按顺序插入
    
    示例
    # 插入一条数据
    db.dbName.insert(
        {
            name: "张三"
        }
    )
    
    # 插入多条数据
    db.dbName.insert([
        {
            name: "张三"
        },
        {
            name: "李四"
        }
    ])
    

    save

    saveinsert命令一样,唯一不同的地方在于save无法创建多条数据 。

    区别

    insertOneinsertMany不支持db.dbName.explain()命令。而insert可以。explain可以见explain

    读取数据

    find

    语法
    db.dbName.find(<query>,<projection>);
    
    # query 筛选操作
    # projection 字段投影
    

    其中projection定义了对读取结果的进行的投影

    示例
    # 读取所有数据
    db.dbName.find();
    
    # 对读取文档进行格式化
    db.dbName.find().pretty();
    
    # 读取张三的数据
    db.dbName.find({ name: "张三" });
    
    # 读取年龄为25岁的张三的数据
    db.dbName.find({ name: "张三", age: 25 });
    

    操作符

    比较操作符
    语法
    db.dbName.find({ field: { $<operator>: <value> } })
    
    # field 需要筛选的字段
    # operator 操作符
    # value 查询值
    
    操作符
    • $eq: 匹配字段相等的文档
    • $ne: 匹配字段不相等的文档
    • $gt: 匹配字段值大于查询值的文档
    • $gte: 匹配字段值大于等于查询值的文档
    • $lt: 匹配字段值小于查询值的文档
    • $lte: 匹配字段值小于等于查询值的文档
    # 查找名字为张三的用户
    db.user.find({ name: { $eq: "张三" }});
    
    # 查找名字不是张三的用户
    db.user.find({ name: { $ne: "张三" }});
    
    # 查找年龄大于19岁的用户
    db.user.find({ age: { $gt: 19 }});
    
    # 查找年龄大于或者等于19的用户
    db.user.find({ age: { $gte: 19 }});
    
    # 查找名字为张三,且年龄大于18的用户
    db.user.find({ name: { $eq: "张三" }, age: { $gt: 18 }});
    

    除此之外,还有两个操作符:

    • $in: 匹配字段值在查询值之间的数据
    • $nin: 匹配字段值不在查询值之间的数据

    $in的形式为:{ field: { $in: [<value1>, <value2>, ..., <valueN>] }}

    $nin同上。

    # 查找名字为张三和李四的信息
    db.user.find({ name: { $in: ["张三", "李四"] }});
    
    # 查找名字不等于张三和李四的信息
    db.user.find({ name: { $nin: ["张三", "李四"] }});
    
    逻辑操作符
    操作符
    • $not: 匹配筛选条件不成立的文档
    • $and: 匹配多个筛选条件全部成立的文档
    • $or: 匹配至少一个筛选条件成立的文档
    • $nor: 匹配多个筛选条件全部不成立的文档

    $not的形式为:{ field: $not: { <operator-expression> }}

    # 查找年龄不小于20的用户
    db.user.find({ age: $not: { $lt: 20 }});
    

    $and的形式为: { $and: [<expression1>, <expression2>, ..., <expressionN>] }

    # 查找年龄大于20且用户名不为张三的用户
    db.user.find({ $and: [
        { age: { $gt: 20 }},
        { name: { $ne: "张三" } }
    ]})
    
    # and 简写
    # 字段不同时
    db.user.find({
        age: { $gt: 20 },
        name: { $ne: "张三" }
    })
    
    # 字段相同时
    # 查找年龄大于20且小于25的用户
    db.user.find({
        age: { $gt: 20, $lt: 25 }
    })
    

    $or$nor的形式同$and

    字段操作符
    • $exists: 匹配字段存在的文档
    • $type: 匹配字段类型是指定值的文档

    使用exists,形式为:{ field: { $exists: <boolean> } }

    # 获取账户类型包含银行账户的文档
    db.accouts.find({
        "_id.type": { $exists: true }
    })
    
    # 匹配id.type存在且值为checking的文档
    db.accounts.find({
        "_id.type": {
            $eq: "checking",
            $exists: true
        }
    })
    

    $type的形式有两种:

    • { field: { $type: <BSON type> } }
    • { field: { $type: [<BSON type1>, <BSON type2>, ..., <BSON typeN>] } }
    # 查找age字段类型为null的数据
    db.user.find({
        age: {
            $type: null
        }
    })
    
    # 查找age字段类型为string的数据
    db.user.find({
        age: {
            $type: "string"
        }
    })
    
    # 查找主键_id为ObjectId和number的数据
    db.user.find({
        _id: {
            $type: ["ObjectId", "number"]
        }
    })
    
    数组操作符

    常用的数组操作符有:

    • $all: 匹配数组字段中包含所有查询值的文档
    • $elemMatch: 匹配数组字段中至少存在一个值满足筛选条件的文档
    示例
    # 新建一些信息
    db.user.insert([
        {
            name: "张三",
            age: 20,
            habbies: ["篮球", "足球"]
        },
        {
            name: "李四",
            age: 22,
            habbies: ["唱歌", "跑步", "游泳"]
        }
    ])
    
    # 查找喜欢篮球和足球的用户
    db.user.find({
        habbies: {
            $all: ["篮球", "足球"]
        }
    })
    
    # 查找爱好喜欢唱歌和足球的用户
    db.user.find({
        habbies: {
            $elemMatch: {
                $in: ["唱歌", "足球"]
            }
        }
    })
    
    运算操作符

    运算操作符使用$regex使用正则表达式进行匹配文档数据。

    $regex有两种语法:

    • { field: { : /pattern/, : "<options>" } }
    • { field: { : /pattern/<options> } }
    示例
    # 第二种语法使用较少,通常搭配$in使用
    # 查找名字中以c开头或者j开头的用户
    db.user.find({
        name: {
            $in: [/^c/, /^j/]
        }
    })
    
    # 查找用户名字中包括lie的用户,不区分大小写
    db.user.find({
        name: {
            $regex: /lie/,
            $options: "i"
        }
    })
    

    游标

    使用db.dbName.find()返回的就是一个游标,在不迭代游标的情况下,默认只列出前20个数据文档。

    var cursor = db.user.find(); # 前20条用户数据
    cursor[1]; # 使用游标下标访问数据 
    

    这里定义了一个cursor变量用来保存游标,在游标没有遍历结束的情况下,10分钟后会被自动关闭,或者手动遍历结束,游标也会自动关闭。

    如果想要游标不超时关闭,可以使用noCursorTimeout()来保持游标的持久性。

    比如: var cursor = db.user.find().noCursorTimeout()

    但需要注意的是,如果没有去遍历游标,则需要手动去关闭:cursor.close()

    游标函数
    函数
    • cursor.hasNext() 判断游标中是否还有没有返回的游标
    • cursor.next() 返回下一个还未返回的游标
    • cursor.forEach(<function>) 循环遍历游标数据
    • cursor.limit(<number>) 返回指定数量的游标
    • cursor.skip(<offset>) 跳过指定数量的游标
    • cursor.count(<applySkipLimit>) 计数游标数
    • cursor.sort(<document>) 对游标进行排序
    示例
    # 返回下一个游标
    var cursor = db.user.find();
    while(cursor.hasNext()) {
        printjson(cursor.next())
    }
    
    # 遍历游标
    cursor.forEach(printjson)
    
    # 返回一条数据
    db.user.find({ name: "张三" }).limit(1)
    
    # 跳过前2条数据
    db.user.find({ name: "张三" }).skip(2)
    
    # 统计名为张三的用户
    db.user.find({ name: "张三" }).count() # 返回find的数据数量
    db.user.find({ name: "张三" }).limit(1).count() # 依然返回find的数据数量
    db.user.find({ name: "张三" }).limit(1).count(true) # 返回1
    
    注意事项

    如果limit传入的是0,那么返回的还是未限制的数据条数。

    cursor.count中,applySkipLimit默认为false,也就是说,cursor.count不会考虑cursor.skipcursor.limit的效果。

    在使用db.dbName.find().count(),也就是find不提供筛选条件的时候,count则会从集合的元数据中取得结果。在复杂的分布式结构中,这种做法是不提倡的,因为文档数据可能不准确

    sort中,可以定义一些字段的排序要求来排序整个文档,具体语法为:sort({ field: ordering })。其中ordering的值有1-11表示由小到大,也就是升序,-1表示由大到小,也就是降序排序。

    # 年龄从大到小排列
    db.user.find().sort({ age: -1 })
    
    # 年龄由大到小 姓名按字母排序
    db.user.find().sort({ age: -1, name: 1 })
    

    当有多个sort字段的时候,依次从左往右进行排序。

    注意事项

    find()执行之后,在sortskiplimit三种游标函数中,sort的优先级高于skiplimit,也就是先执行,其次就是skip的优先级高于limit

    # 先sort进行年龄升序排序,然后在sort结果中跳过前4条文档,最后限制输出2条文档。
    db.user.find().sort({ age: 1 }).limit(2).skip(4);
    

    和书写顺序无关。

    文档投影

    find中,有另外一个参数projection可以用来选择性的返回文档中需要返回的字段,其语法为:db.dbName.find({}, { field: inclusion }),其中inclusion的值为1或者0

    1表示返回该字段,0表示不返回。

    # 只返回张三的姓名和年龄
    db.user.find({ name: "张三" }, {
        name: 1,
        age: 1
    })
    
    # 返回的文档不需要_id字段
    db.user.find({}, {
        _id: 0
    })
    

    不可以同时存在10,不然会报错。要么列出所有想显示的字段,要么列出所有不想显示的字段,切勿同时存在包含和不包含的关系, 主键_id除外。

    数组投影

    $slice

    数组投影可以使用$slice关键字进行操作,具体语法为:

    • $slice: number: 返回数组中指定位置的的数据,可为负数,从尾部开始计数
    • $slice: [skip, limit]: 跳过指定条数的数据,开始返回指定数量的数据。
    # 返回张三的第一个兴趣爱好
    db.user.find({ name: "张三" }, {
        habbies: {
            $slice: 1
        }
    })
    
    # 返回张三第一个以外的其他两个兴趣
    db.user.find({ name: "张三" }, {
        habbies: {
            $slice: [1, 2]
        }
    })
    
    $elemMatch

    也可以使用$elemMatch$操作符进行投影操作:

    # 如果兴趣中有篮球或者游泳,则返回该habbies字段
    db.user.find({}, {
        name: 1,
        habbies: {
            $elemMatch: {
                $in: ["篮球", "游泳"]
            }
        }
    })
    

    更新文档

    update

    语法
    db.dbName.update(<query>, <update>, {
        upsert: <boolean>,
        multi: <boolean>
    });
    # query 文档的筛选条件
    # update 文档的更新内容
    # options 文档更新操作的一些参数
    ## upsert 是否文档不存在时创建 默认为false
    ## multi 是否多文档更新,默认false
    
    更新操作符
    • $set: 更新字段
    • $unset: 删除字段
    • $rename: 重命名字段
    • $inc: 加减字段值
    • $mul: 相乘字段值
    • $min: 经过比较后,取最小字段值
    • $max: 经过比较后,取最大字段值
    • $addToSet: 用于数组更新,插入新值
    • $pop: 用于数组删除,只能删除第一个或者最后一个元素
    • $pull: 用于数组删除,删除特定元素
    • $pullAll: 用于数组删除,删除多个特定元素
    • $push: 用于数组添加,添加元素,大体同addToSet,但比之更灵活。
    例子
    # 更新张三的年龄为24岁
    db.user.find({ name: "张三" }, {
        name: "张三",
        age: 24
    })
    
    # 使用更新操作符
    db.user.find({ name: "张三" }, {
        $set: {
            age: 24
        }
    })
    
    # 更新内嵌字段
    db.user.find({ name: "张三" }, {
        $set: {
            "info.age": 24
        }
    })
    
    # 更新数组内的数据
    db.user.find({ name: "张三" }, {
        $set: {
            # 更新张三的第一个兴趣为打游戏
            "habbies.0": "打游戏"
        }
    })
    # PS:如果修改的数组下标下不存在数据,则向数组追加数据,如果数组长度为3,添加的下标为6,跳过的数据则为null
    
    # 删除张三的年龄
    db.user.find({ name: "张三" }, {
        $unset: {
            age: ""
        }
    })
    # PS: unset 删除数组是让对应下标的数据变成null,并不改变原有数组的长度。
    
    # 重命名张三的age为user_age
    db.user.find({ name: "张三" }, {
        $rename: {
            age: "user_age"
        }
    })
    # PS: 如果修改后的字段原本已经存在在数据集合中,那么那个已经存在的则会被删除。
    
    # 让张三的年龄减小1岁
    db.user.find({ name: "张三" }, {
        $inc: {
            age: -1 # +1 表示原有的数值上+1
        }
    })
    
    # 让张三的零花钱少一半
    db.user.find({ name: "张三" }, {
        $mul: {
            money: 0.5 # 会在原有数值上*0.5
        }
    })
    
    # 如果张三的钱<我给的钱,则张三的钱 = 我给的钱
    db.user.find({ name: "张三" }, {
        $max: {
            money: 150 # 张三原有的money为100,小于我给的150,所以张三现在的money为150
        }
    })
    # min 取最小值
    
    # 给张三的兴趣爱好添加一个读书
    db.user.find({ name: "张三" }, {
        $addToSet: {
            habbies: "读书" # 如果存在 则不更新。
        }
    })
    
    # 给张三的兴趣添加读书和打电玩
    db.user.find({ name: "张三" }, {
        $addToSet: {
            habbies: {
                $each: ["读书", "打电玩"] # 添加多个的情况需要使用$each 不然会将整个数组当做一个值插入
            }
        }
    })
    
    # 删除张三的最后一个兴趣
    db.user.find({ name: "张三" }, {
        $pop: {
            habbies: 1 # -1 表示删除第一个值
        }
    })
    
    # 删除兴趣里带有打字的兴趣
    db.user.find({ name: "张三" }, {
        $pull: {
            habbies: {
                $regex: /打/
            }
        }
    })
    
    注意事项

    当设置update的中的multitrue的时候,更新所有筛选到的文档,虽然都是在一个线程中执行,但是线程在执行的过程中是会被挂起的,别的线程也会有机会对他进行修改。

    findAndModify

    TODO。。

    save

    如果save的文档中,包含了主键_id,那么,save调用的其实就是update操作,进行更新操作,且upsert会被设置为true

    删除文档

    remove

    语法
    db.dbName.remove(<query>, {
        justOne: <boolean>
    });
    
    # query 查询条件
    # options 操作参数
    ## justOne 是否只删除一个文档 默认为false
    
    示例
    # 删除年龄为24的用户
    db.user.remove({ age: 24 })
    
    # 删除年龄小于20岁的用户
    db.user.remove({ age: {
        $lt: 20
    }})
    
    # 删除年龄小于20岁的第一个用户
    db.user.remove({ age: {
        $lt: 20
    }}, {
        justOne: true
    })
    
    # 删除所有数据
    db.user.remove({});
    

    drop

    语法
    db.dbName.drop({ writeConcern: <document> });
    
    # writeConcern: 删除操作的安全写级别
    
    示例
    # 删除用户表
    db.user.drop();
    

    数据聚合操作

    aggregate

    语法

    db.dbName.aggregate(<pipeline>, {
        allowDiskUse: <boolean>
    });
    
    # pipeline 定义了操作中使用的聚合管道阶段和聚合操作符
    # options 聚合操作参数
    ## allowDiskUse 允许每个聚合管道操作超出内存上限(100MB)时,将操作数据写入临时文件
    

    表达式

    字段路径表达式
    # 字段路径表达式
    $<field> # 使用$来指示字段路径
    $<field>.<sub-field> # 使用$和.来指示内嵌文档字段路径
    
    # 举例
    $name # 指示姓名字段
    $info.age # 指示用户信息中的年龄字段
    
    系统变量表达式
    # 系统字段表达式
    $$<variable> # 使用$$来指示系统变量
    
    # 举例
    $$CURRENT # 指示管道中当前操作的文档
    $$CURRENT.<field> # 和$<field>是等效的
    
    常量表达式
    # 常量表达式
    $literal: <value> # 指示常量value
    
    # 举例
    $literal: "$name" # 指示常量字符串 "$name"
                                        # 这里的$被当做常量处理,而不是字段路径表达式
    

    聚合管道阶段

    先创建点数据,方便以下例子使用:

    db.user.insert([
        {
            name: { firstName: "Zhang", lastName: "san" },
            age: 22,
            money: 1000
        },
        {
            name: { firstName: "Li", lastName: "si" },
            age: 20,
            money: 1500
        }
    ])
    
    $project

    对输入的文档再次投影,控制文档的格式输出

    # 返回用户的存款和姓氏
    db.user.aggregate([
        {
            $project: {
                _id: 0,
                user: "$name.firstName",
                money: 1
            }
        }
    ])
    # output
    # { user: "Zhang", money: 1000 }
    # { user: "Li", money: 1500 }
    
    # 使用$concat进行字段拼接
    db.user.aggregate([
        {
            $project: {
                _id: 0,
                user: {
                    $concat: ["$name.firstName", " ", "$name.lastName"]
                },
                money: 1
            }
        }
    ])
    # output
    # { user: "Zhang san", money: 1000 }
    # { user: "Li si", money: 1500 }
    
    $match

    对输入的文档进行筛选,和读取文档的筛选语法一样

    # 在管道中获取姓氏中为Zhang的用户
    db.user.aggregate([
        {
            $match: {
                "$name.firstName": "Zhang"
            }
        }
    ])
    # output
    # { _id: ObjectId(xxx), name: { firstName: "Zhang", lastName: "san" }, age: 22, money: 1000 }
    
    # 多条件筛选
    db.user.aggregate([
        {
            $match: {
                $or: [
                    {
                        money: { $gt: 800, $lt: 1200}
                    },
                    {
                        "$name.firstName": "Li"
                    }
                ]
            }
        }
    ])
    # output
    # { _id: ObjectId(xxx), name: { firstName: "Zhang", lastName: "san" }, age: 22, money: 1000 }
    # { _id: ObjectId(xxx), name: { firstName: "Li", lastName: "si" }, age: 20, money: 1500 }
    
    # PS: $match 并不会修改原有的数据格式。
    
    # 配合project使用
    db.user.aggregate([
        {
            $match: {
                "$name.firstName": "Zhang"
            }
        },
        {
            $project: {
                _id: 0,
                user: "$name.firstName",
                money: 1
            }
        }
    ])
    # output
    # { user: "Zhang", money: 1000 }
    
    $limit

    筛选管道内前N篇文档

    # 筛选第一个用户
    db.user.aggregate([
        {
            $limit: 1
        }
    ])
    # output
    # { _id: ObjectId(xxx), name: { firstName: "Zhang", lastName: "san" }, age: 22, money: 1000 }
    
    $skip

    跳过管道内前N篇文档

    # 跳过第一个用户
    db.user.aggregate([
        {
            $skip: 1
        }
    ])
    # output
    # { _id: ObjectId(xxx), name: { firstName: "Li", lastName: "si" }, age: 20, money: 1500 }
    
    $unwind

    展开输入文档中的数组字段

    # 新增字段currency
    db.user.update({
        "name.firstName": "Zhang",
    }, {
        $set: {
            currency: ["CNY", "USD"]
        }
    })
    
    db.user.update({
        "name.firstName": "Li"
    }, {
        $set: {
            currency: "GBP"
        }
    })
    
    # 展开数据中的货币数组
    db.user.aggregate([
        {
            $unwind: {
                path: "$currency" # path 指向需要展开的数组字段
                #includeArrayIndex: "ccyIndex" 展开数组时,显示对应的下标字段,值为对应的索引。如果path指向的非数组,该字段的值则为null
                #preserveNullAndEmptyArrays: true 不过滤那些path指向的字段值为null或者为空数组[]或者不存在的数据。
            }
        }
    ])
    # output
    # { _id: ObjectId(xxx), name: { firstName: "Zhang", lastName: "san" }, age: 22, money: 1000, currency: "CNY" }
    # { _id: ObjectId(xxx), name: { firstName: "Zhang", lastName: "san" }, age: 22, money: 1000, currency: "USD" }
    # { _id: ObjectId(xxx), name: { firstName: "Li", lastName: "si" }, age: 20, money: 1500, currency: "GBP" }
    
    # PS:unwind是将数组拆分成单独的一条数据 由数组->字符串
    # PS:unwind展开的数组字段如果不存在或者为空数组[],或者为null,则unwind会过滤这些数据。如不想过滤,设置preserveNullAndEmptyArrays为true即可。不存在和数组为空的情况下,打印出的数据不会包含指向字段,null的情况则会打印字段并且值为null。
    
    $sort

    对输入的文档进行排序

    # 对年龄进行排序
    db.user.aggregate([
        {
            $sort: {
                $age: 1 # 由小到大排序
            }
        }
    ])
    
    # 先对年龄升序排序,再对收入降序排序
    db.user.aggregate([
        {
            $sort: {
                $age: 1, # 升序
                $money: -1 # 降序
            }
        }
    ])
    
    $lookup

    对输入的文档进行查询操作,可以对非管道数据集进行操作。

    简单查询
    # 基本语法
    $lookup: {
        # 同一数据库中的另一个集合(表)
        from: <collection to join>,
        # 管道中希望用来去查询的字段名
        localField: <field from the input documents>,
        # 查询from集合中的查询字段
        foreignField: <field from the documents of the "from" collection>,
        # 把查询到的结果写入管道中的自定义的字段中 该字段是数组类型
        as: <output array field>
    }
    
    # 增加一个额外的集合(表)forex
    db.forex.insert([
        {
            ccy: "USD",
            rate: 6.91
        },
        {
            ccy: "GBP",
            rate: 8.72
        },
        {
            ccy: "CNY",
            rate: 1.0
        }
    ])
    
    # 查询汇率写入对应的用户表中
    db.user.aggregate([
        {
            $unwind: {
                path: "$currency"
            }
        },
        {
            $lookup: {
                from: "forex",
                localField: "currency",
                foreignField: "ccy",
                as: "forexRate"
            }
        },
        {
            $project: {
                _id: 0,
                user: "$name.firstName",
                currency: 1
            }
        }
    ])
    # output
    # { user: "Zhang", currency: "USD", forexRate: [{ ccy: "USD", rate: 6.91 }] }
    # { user: "Zhang", currency: "CNY", forexRate: [{ ccy: "CNY", rate: 1.0 }] }
    # { user: "Li", currency: "GBP", forexRate: [{ ccy: "GBP", rate: 8.72 }] }
    
    复杂查询
    # 基本语法
    $lookup: {
        # 同一数据库中的其他集合(表)
        from: <collection to join>,
        # 对原有管道的中需要在pipeline中使用的字段进行声明,如没有 可省略
        let: { <var_1>: <expression>, .., <var_n>: <expression> },
        # 对from集合进行聚合处理,该管道中无法使用原有管道的字段变量,如需使用,则需要再let中声明
        pipeline: [<pipeline to execute on the collection to join>],
        # 把查询到的结果写入管道中的自定义的字段中 该字段是数组类型
        as: <output array field>
    }
    
    # 将汇率大于7的写入用户表
    db.user.aggregate([
        {
            $project: {
                _id: 0,
                user: "$name.firstName",
                currency: 1
            }
        },
        {
            $unwind: {
                path: "$currency"
            }
        },
        {
            $lookup: {
                from: "forex",
                pipeline: [
                    {
                        $match: {
                            rate: {
                                $gt: 7
                            }
                        }
                    }
                ],
                as: "forexRate"
            }
        }
    ])
    # output 会发现结果是无差别写入
    # { user: "Zhang", currency: "USD", forexRate: [{ ccy: "GBP", rate: 8.72 }] }
    # { user: "Zhang", currency: "CNY", forexRate: [{ ccy: "GBP", rate: 8.72 }] }
    # { user: "Li", currency: "GBP", forexRate: [{ ccy: "GBP", rate: 8.72 }] }
    
    # 为currency为USD的写入汇率大于7的数据
    db.user.aggregate([
        {
            $project: {
                _id: 0,
                user: "$name.firstName",
                currency: 1
            },
            {
                $unwind: {
                    path: "$currency"
                }
            },
            {
                $lookup: {
                    from "forex",
                    let: { cry: "$currency" },
                    pipeline: [
                        {
                            $match: {
                                # 当使用let声明系统变量的时候,需要使用expr才可以调用到
                                $expr: {
                                    $and: [
                                        {
                                            $eq: ["$$cry", "USD"] # 使用let中声明的系统变量cry
                                        },
                                        {
                                            $gt: ["$rate", 7] # 使用from集合中的字段变量
                                        }
                                    ]
                                }
                            }
                        }
                    ],
                    as: "forexRate"
                }
            }
        }
    ])
    # output
    # { user: "Zhang", currency: "USD", forexRate: [{ ccy: "GBP", rate: 8.72 }] }
    # { user: "Zhang", currency: "CNY", forexRate: [] }
    # { user: "Li", currency: "GBP", forexRate: [] }
    
    $group

    对输入的文档进行分组

    在不使用管道操作符的情况下,可以返回管道文档中某一值的不重复值

    # 语法
    $group: {
        _id: <expression>, # 必须要的参数 用于定义分组规则
        <field1>: { <accumulator1> : <expression1> }, # 使用聚合操作符定义新字段
        ...
    }
    
    # 定义一个交易集合
    db.transactions.insert([
        {
            symbol: "10023",
            qty: 100,
            price: 567.4,
            currency: "CNY"
        },
        {
            symbol: "AMZN",
            qty: 1,
            price: 1377.5,
            currency: "USD"
        },
        {
            symbol: "AAPL",
            qty: 20,
            price: 150.7,
            currency: "USD"
        }
    ])
    
    # 简单示例
    # 针对交易集合中的币种进行分组
    db.transactions.aggregate([
        {
            $group: {
                _id: "$currency"
            }
        }
    ])
    #output
    # { _id: "CNY" }
    # { _id: "USD" }
    
    # 分组统计
    db.transactions.aggregate([
        {
            $group: {
                _id: "$currency",
                totalQty: { $sum: "$qty" },
                totalNotional: { $sum: { $multiply: ["$price", "$qty"] } },
                avgPrice: { $avg: "$price" },
                count: { $sum: 1 },
                maxNotional: { $max: { $multiply: ["$price", "$qty"] } },
                minNotional: { $min: { $multiply: ["$price", "$qty"] } }
            }
        }
    ])
    # output
    # { "_id" : "USD", "totalQty" : 21.0, "totalNotional" : 4391.5, "avgPrice" : 764.1, "count" : 2.0, "maxNotional" : 3014.0, "minNotional" : 1377.5 }
    # { "_id" : "CNY", "totalQty" : 100.0, "totalNotional" : 56740.0, "avgPrice" : 567.4, "count" : 1.0, "maxNotional" : 56740.0, "minNotional" : 56740.0 }
    
    $out

    将管道内的文档输出

    # 输出统计信息
    db.transactions.aggregate([
        {
            $group: {
                _id: "$currency",
                totalQty: { $sum: "$qty" },
                totalNotional: { $sum: { $multiply: ["$price", "$qty"] } },
                avgPrice: { $avg: "$price" },
                count: { $sum: 1 },
                maxNotional: { $max: { $multiply: ["$price", "$qty"] } },
                minNotional: { $min: { $multiply: ["$price", "$qty"] } }
            }
        },
        {
            $out: "output" # 输出结果到新的集合(表)output中。
        }
    ])
    
    # PS:如果该集合(表)已经存在,则会在保留索引的情况下,清空数据,再插入新保存的数据
    

    优化

    • 在有$match的时候,尽量保证$match的执行先于其他管道操作。因为$match阶段,会对管道文档进行筛选,减少管道中的文档数量,数量越少,调整效率越快。
    • $skip$project都存在的情况下,保证$skip的操作优先于$project$skip用于跳过文档,避免对要跳过的文档做完操作后再去skip,也是提升效率的一种。
    • 其实mongodb都会自动保证这些的优先级。

    索引

    索引就是给指定字段进行排序的数据结构,给数据集合(表)创建索引,能够大大提高数据库搜索性能。

    操作

    createIndex

    语法
    db.dbName.createIndex(<keys>, {
        unique: <boolean>,
        sparse: <boolean>,
        expireAfterSeconds: <number>
    });
    
    # keys 索引字段
    # options 创建索引操作的参数
    ## unique 索引的唯一性 默认为false
    ## sparse 索引的稀疏性 只将包含索引字段的文档加入到索引集合中 默认为false
    ## expireAfterSeconds 索引的可生存时间 单位秒
    
    # PS:复合键索引也可以具有稀疏性,只有在缺失复合键所包含的所有字段的情况下,文档才不会加入到索引中
    # PS:复合键索引不具备生存时间特性
    
    示例
    # 创建数据
    db.demoIndex.insert([
        {
            name: "blob",
            balance: 100,
            currency: ["CNY"]
        },
        {
            name: "lucy",
            balance: 200,
            currency: ["AUD", "USD"]
        },
        {
            name: "andy",
            balance: 50,
            currency: ["GBP", "CNY"]
        }
    ])
    
    # 为demoIndex集合创建单键索引
    db.demoIndex.createIndex({ name: 1 });
    
    # 为demoIndex集合创建一个复合键索引
    db.demoIndex.createIndex({ name: 1, balance: -1 })
    
    # 为demoIndex集合创建一个多建索引
    db.demoIndex.createIndex({ currency: 1 })
    
    
    注意
    • 多键索引只能给数组建立,多键索引会给数组的每一个元素创建一个键。
    • 1表示索引字段按升序排列,-1表示索引字段按降序排列

    getIndexes

    获取集合索引信息

    # 获取demoIndex的索引信息
    db.demoIndex.getIndexes();
    

    dropIndex

    删除索引信息

    # 删除demoIndex的索引
    db.demoIndex.dropIndex(<keys | name>);
    
    # keys 索引的字段信息
    # name 索引name 可以通过getIndexes获取到
    

    如果需要更改索引,只能通过删除索引后重新建立。

    explain

    用于查看建立索引后的效果。

    语法
    db.dbName.explain().<method(...)>
    
    # method 用于查看索引效果的方法,包括find()、count()、aggregate()、distinct()、group()、remove()、update()。
    
    示例
    # 使用没有创建索引的字段进行搜索
    db.demoIndex.explain().find({ balance: 100 })
    
    # 主要观察返回信息中的queryPlanner.winningPlan.stage字段
    ## COLLSCAN 效率最低,通常需要循环遍历整个文档
    ## PROJECTION 效率最高
    ## FETCH 命中索引,效率一般
    

    数据模型

    文档结构

    内嵌式文档

    内嵌式文档,一般指在一个文档中,还会存在其他子文档,比如:

    {
        name: "张三",
        info: {
            age: 22,
            habbies: "篮球"
        }
    }
    
    # info为子文档 也就是内嵌文档。
    

    规范式文档

    规范式文档就是,将顶层文档中的一些子文档提取出来存放在一个新的文档中,通过ObjectId进行关联,这样做能够有效的减少一些重复子文档。

    # 文档一
    {
        course: "篮球课",
        user: <ObjectId_1>
    }
    {
        course: "乒乓球课",
        user: <ObjectId_1>
    }
    
    # 文档二
    {
        id: <ObjectId_1>,
        name: "张三",
        age: 22
    }
    
    # 可能会存在一个人选多门课程 将用户的文档提取出来 通过ObjectId和顶层课程文档进行关联。
    

    文档关系

    一对一

    使用内嵌文档的好处

    • 一次查询就可以返回所有需要用的信息
    • 更具有独立性的数据作为顶层文档
    • 补充性的数据作为内嵌文档

    一对多

    使用内嵌文档的好处

    • 一次查询就可以得到所有需要用的信息

    使用内嵌文档的缺点

    • 更新内嵌文档的复杂度增高

    使用规范式文档的好处

    • 减少了重复数据
    • 降低了文档更新的复杂度

    使用规范式文档的缺点

    • 需要多次查询才能得到完整的数据

    数据复制

    • 高可用性
    • 数据安全
    • 分流/分工

    复制集

    在复制集节点中,会存在一个主节点主节点主要负责的是所有数据的写入请求。

    主节点底下会存在若干个副节点副节点会不断的从主节点(或者其他符合条件的副节点)中复制数据,该步骤是异步的。

    主副节点都可以处理读取的请求。

    每个节点之间都会相互发送一个心跳请求,用于检测节点之间的健康情况

    默认情况下,节点之间会每隔2秒发送一次心跳请求,超过10秒无响应的,则表示该节点出现故障

    一个复制集中最多只能存在50个节点

    如果主节点故障了,那么MongoDB则会通过内部的一个选举算法,从副节点中选出一个成为新的主节点。

    示例

    # 假设已经存在三个mongodb数据库 在不同的服务/端口下
    
    # 创建一个拥有三个节点的数据集
    rs.initiate({
        _id: "mytest",
        members: [
            {
                _id: 0,
                host: "mongo1:27017"
            },
            {
                _id: 1,
                host: "mongo2:27018"
            },
            {
                _id: 2,
                host: "mongo3:27019"
            }
        ]
    })
    
    # 查看复制集的状态
    rs.status()
    

    数据库分片

    介绍

    数据库分片,简单说就是将整个数据库的数据分成一个个子集,然后将每个子集存储在分片上,最终这些分片集群合在一起就是这个数据库完整的数据。

    每个数据库分片是能够运行在不同的服务器中的,从而提高数据库的可拓展性。

    分片集群的构成

    • 至少两个分片
    • 配置服务器 用于存储分片元数据和集群配置,哪些数据存于哪些分片信息之类的
    • mongos 分片路由,客户端访问分片路由,再由分片路由去访问配置服务器获取对应分片数据
    • server 用于运行分片路由的应用服务器

    配置服务器

    • 存储各分片数据段列表和数据段范围
    • 存储集群的认证和授权配置
    • 不同的集群不要共用配置服务器

    mongos

    • 客户请求应发给mongos,而不是分片服务器
    • 当查询包含分片片键时,mongos将查询发送到指定分片
    • 否则mongos将查询发送到所有分片,并汇总所有查询结果

    主分片

    • 集群中的每个数据库都会选择一个分片做为主分片
    • 主分片中存储的是不需要分片的集合
    • 创建数据库的时候,数据最少的分片会被选择为主分片

    分片片键

    • 片键值被用来将集合中的文档划分为数据段
    • 片键必须对应一个索引或者索引前缀(单键或者复合键)
    • 可以使用片键值的哈希值来生成哈希片键

    数据库安全

    创建用户

    # 进入admin数据库 该数据库用来保存用户信息
    use admin;
    
    # 在admin数据库中创建用户信息
    db.createUser({
        user: "userAdmin",
        pwd: "passwords",
        roles: ["userAdminAnyDatabase"] # 授权角色 该权限只能管理数据库用户和角色 但是不能操作集合
    })
    

    mongodb默认是没有启动身份认证的,也就是默认用户登录。这种情况下,没办法使用创建用户进行登录,如果想使用身份认证,则启动mongodb的时候需要加上一个参数-auth

    # 启用mongodb的身份认证
    mongod --auth;
    

    启用身份认证之后,就可以使用自定义用户进行登录mongodb

    用户认证

    创建好用户之后,需要进行用户验证:

    # 验证用户
    mongo -u "userAdmin" -p "passwords" --authenticationDatabase "admin"
    
    # --anthenticationDatabase 表示需要验证信息的数据库是哪个,如果用户信息存在默认进入的表中,则不需要该参数
    
    # or 
    use admin;
    db.auth("userAdmin", "passwords");
    

    授权

    权限

    # 权限 = 我在哪儿 + 做什么
    # e.g
    {
        resource: {
            db: "test",
            collection: ""
        },
        actions: ["find", "update"]
    }
    # resource 表示我想操作权限的数据库和集合是哪些,如果collection为空,则为整个db的所有集合
    # actions 表示对该数据库,我能做的操作有哪些,这里表示在test中,该用户能够对集合执行find和update操作
    

    角色

    # 角色 = 一组权限的集合
    # e.g
    read # 读取当前数据库中所有非系统集合
    readWrite # 读写当前数据库中所有非系统集合
    dbAdmin # 管理当前数据库
    userAdmin # 管理当前数据库中的用户和角色
    read/readWrite/dbAdmin/userAdmin + AnyDatabase # 对所有数据库执行操作(只在admin数据库中提供)
    

    示例

    # 创建一个只能读取test数据库的用户
    use admin;
    db.createUser({
        user: "onlyRead",
        pwd: "password",
        roles: [{
            role: "read",
            db: "test" # 如果是在test中创建的用户 可以省略db
        }]
    });
    
    mongo -u "onlyRead" -p "password" --authenticationDatabase "admin";
    # PS: 新创建的用户要生效,一定需要关闭mongodb的进程,重新进入
    
    
    # 创建一个只能读取user集合的用户
    # 没有内建角色符合,所以先创建一个自定义的角色
    use test;
    db.createRole({
        role: "readUser",
        privileges: [{
            resource: {
                db: "test",
                collection: "user"
            },
            actions: ["find"] # 只能执行读取操作
        }],
        roles: [] # 从原有角色继承
    })
    
    # 然后创建角色
    db.createUser({
        user: "onlyReadUser",
        pwd: "password",
        roles: [{
            role: "readUser",
            db: "test"
        }]
    })
    

    数据库常用工具

    数据处理工具

    mongoexport

    将数据导出为json或者csv格式的文件。

    # 语法
    mongoexport --db dbName --collection collectionName --type=csv/json --fields field1,field2,...,fieldn --out outputPath -u userName -p password --authenticationDatabase "admin";
    
    # dbName 数据库名称
    # collectionName 集合名称
    # --type csv或者json
    # --fields 导出的字段名称 type为csv时必须提供 如果有内嵌文档,可以使用field.field方式选择
    # ouputPath 导出的路径
    # userName 执行导出操作的用户名称
    # password 执行导出操作的用户密码
    

    mongoexport还可以使用查询语句进行文档导出:

    # 通过筛选导出
    # 在原来的语法最后加上--query参数
    mongoexport --db dbName --collection collectionName --type=csv/json --fields field1,field2,...,fieldn --out outputPath -u userName -p password --authenticationDatabase "admin" --query '{ field: <expression> }';
    

    除此之外,还支持--limit--skip--sort

    mongoimport

    将json或者csv格式的数据导入到mongodb中。

    # 语法
    mongoimport --db dbName --collection collectionName --type csv/json [--headerline | --fields field1,field2,...,fieldn] --file filePath  [--drop] -u userName -p password --authenticationDatabase "admin" [--upsertFields filed1,filed2,...,filedn] [--stopOnError] [--maintainInsertionOrder]
    
    # dbName 数据库名称
    # collectionName 集合名称
    # --type csv或者json 为json时可以不提供headerline和fields
    # --headerline 告知程序csv的第一行为字段名称而非数据
    # --fileds 和headerline是二选一参数 headerline取第一行字段为字段名,fields则是自定义字段名称
    # filePath 导入文件的路径
    # --drop 是否在导入前先drop集合 可选参数
    # userName 执行导出操作的用户名称
    # password 执行导出操作的用户密码
    # upsertFields 告诉mongodb 导入的时候看字段是否相同,相同的话就更新,不要根据文档主键不同而不断的去新增文档 可选参数
    # --stopOnError 导入出错就停止 可选参数
    # --maintainInsertionOrder 按照文件字段值顺序进行导入 可选参数
    

    数据库监控

    mongostat

    监听数据库的使用情况

    # 语法
    mongostat --host localhost --port 27017 -u userName -p password --authenticationDatabase "admin" [--rowcount times] [times] [-o "filed1,filed2,..,fieldn"]
    
    # localhost 监听的ip
    # port 监听的端口
    # --rowcount times 一共抓取times次监控数据 可选参数
    # times 每隔times秒抓取一次数据 可选参数
    # -o 只想显示的状态名称
    
    # PS:需要用户有clusterMonitor的角色权限
    
    # 状态名称
    ## command 每秒执行的命令书
    ## dirty, used 数据库引擎缓存的使用量百分比
    ## vsize 虚拟内存使用量(MB)
    ## res 常驻内存使用量(MB)
    ## conn 连接数
    

    mongotop

    监听数据库中集合的查询情况

    语法同mongostat

    数据库故障诊断

    查询时间过长

    建立合适的索引,可以使用explain来判断索引的效率。

    响应时间过长

    工作集可能超出RAM的大小,可以通过mongostat来查看数据库的使用情况。

    连接失败

    可能超过了连接数,使用命令db.serverStatus().connections来查看mongodb支持的连接数。

    查看服务器数据库配置文件中的maxIncomingConnections的数值是否被限制。

    查看ulimit配置,主要看open files的数值。使用命令:ulimit -a

    相关文章

      网友评论

          本文标题:让我们学习下MongoDB吧~

          本文链接:https://www.haomeiwen.com/subject/cfmkactx.html