深入学习mongdb(一) mongdb增删改查-张柏沛IT博客

正文内容

深入学习mongdb(一) mongdb增删改查

栏目:数据库 系列:深入学习mongdb 发布时间:2021-03-13 18:04 浏览量:63

更多增删改查操作可以参考官方文档
https://docs.mongoing.com/

 

插入数据


单条插入

db.foo.insert({"bar" : "baz"})

需要注意的是,如果插入的数据中设置了_id字段,那么客户端或者服务端就不会再为这条数据添加一个_id,而是使用用户设置的_id。


批量插入

db.foo.insert([{"_id" : 0}, {"_id" : 1}, {"_id" : 2}])

不能在单次请求中将多个文档批量插入到多个集合中,只能将多个文档插入到一个集合中。

MongoDB批量插入能接受的最大消息长度是48 MB,单个文档最大大小是16MB。所以在一次批量插入中能插入的文档是有限制的。如果试图插入48 MB以上的数据,多数驱动程序会将这个批量插入请求拆分为多个48 MB的批量插入请求。

如果在执行批量插入的过程中有一个文档插入失败(例如_id重复),那么在这个文档之前的所有文档都会成功插入到集合中,而这个文档以及之后的所有文档全部插入失败。在批量插入中遇到错误时,如果希望batchInsert 忽略错误并且继续执行后续插入,可以使用continueOnError 选项。Shell并不支持这个选项,但是所有驱动程序都支持。

除了insert方法可以进行单条或批量插入,还有insertOne和insertMany()方法。

执行插入操作时,如果该集合当前不存在,则插入操作将创建该集合。

MongoDB中的所有写操作都是单个文档级别(行级别)的原子操作。

 

删除数据
删除所有文档

db.foo.remove({})

 

按条件删除

db.mailing.list.remove({"opt-out" : true})      // 删除mailing.list集合中所有"opt-out" 为true 的人

删除数据是永久性的,不能撤销,也不能恢复。

 

删除集合

db.foo.drop()

 

更新数据

更新

db.collection.updateOne(<filter>, <update>, <options>)
db.collection.updateMany(<filter>, <update>, <options>)
db.collection.replaceOne(<filter>, <update>, <options>)

mongodb提供了以$开头的更新操作符(update operator)来辅助更新操作,如 $set,$currentDate,$gt,$lt,$incr等。

例子如下,先插入以下数据

db.inventory.insertMany( [
   { item: "canvas", qty: 100, size: { h: 28, w: 35.5, uom: "cm" }, status: "A" },
   { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "mat", qty: 85, size: { h: 27.9, w: 35.5, uom: "cm" }, status: "A" },
   { item: "mousepad", qty: 25, size: { h: 19, w: 22.85, uom: "cm" }, status: "P" },
   { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "P" },
   { item: "paper", qty: 100, size: { h: 8.5, w: 11, uom: "in" }, status: "D" },
   { item: "planner", qty: 75, size: { h: 22.85, w: 30, uom: "cm" }, status: "D" },
   { item: "postcard", qty: 45, size: { h: 10, w: 15.25, uom: "cm" }, status: "A" },
   { item: "sketchbook", qty: 80, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
   { item: "sketch pad", qty: 95, size: { h: 22.85, w: 30.5, uom: "cm" }, status: "A" }
] );

 

更新操作1:

db.inventory.updateOne(
    { item: "paper" },
    {
        $set: { "size.uom": "cm", status: "P" }, 
        $currentDate: { lastModified: true }
    }
)

 

$set 运算符将size.uom字段的值更新为“ cm”,将状态字段的值更新为“ P”,而不会影响其他字段。
$currentDate运算符将lastModified字段(字段名可自定义)的值更新为当前日期。 如果lastModified字段不存在,则$currentDate将创建该字段。


更新操作2:更新qty小于50的所有文档

db.inventory.updateMany( 
      { "qty": { $lt: 50 } },
      {  
          $set: { "size.uom": "in", status: "P" }, 
          $currentDate: { lastModified: true }  
      }
  )


更新操作3:如果要替换_id字段以外的文档的全部字段,请使用replaceOne()方法,并将一个全新的文档作为第二个参数传递。

db.inventory.replaceOne(
   { item: "paper" },
   { item: "paper", instock: [ { warehouse: "A", qty: 60 }, { warehouse: "B", qty: 40 } ] }
)

此时新文档会覆盖旧文档所有字段。


PS:更新操作不允许以任何形式修改_id字段。


更新操作4:第三参传入{upsert:true},更新不存在的文档时新增

db.inventory.replaceOne(
   { item: "paper123" },
   { _id:123, item: "paper", instock: [ { warehouse: "A", qty: 60 }, { warehouse: "B", qty: 40 } ] },
   {upsert:true}
)


相比于“先查询是否存在这个文档,如果不存在则新增”,使用upsert的好处是他是一个原子操作,不会引起竞争。

updateOne(), updateMany(), 和 replaceOne()都可以接收第三参(他们接受的形参是一样的)。


更新操作5:更新后返回更新前的文档数据(只能修改匹配到的第一个文档)
 

db.people.findAndModify({
    query: { name: "Andy" },
    sort: { rating: 1 },        // 按rating顺序排序
    update: { $inc: { score: 1 } },     // 这里也可以是remove删除操作
    upsert: true
})


与upsert结合使用的话,如果查询条件没有匹配到文档则会新增一个文档,如果要避免多次 upsert,请确保query查询的字段为唯一索引。

 

更新操作6:save
如果文档不存在,它会自动创建文档;如果文档存在,它就更新这个文档。
它需要传入一个文档。要是这个文档含有"_id" 键,save 会调用update 。否则,会调用insert。

> var x = db.foo.findOne()
> x.num = 42
42
> db.foo.save(x)

update方法也可以实现更新操作,但默认只能更新单条文档(如果要更新多条文档就要传入额外的参数)。


常用的更新修改器

"$set" 用来指定一个字段的值。如果这个字段存在则更新它,不存在则创建它。

"$unset" 可以将指定的键删除,例如
db.users.update({"name" : "joe"},{"$unset" : {"favorite book" : 1}})        // 1不重要。

"$inc" 修改器用来增加或减少已有键的值,或者该键不存在那就创建一个,例如
db.games.update({"game" : "pinball", "user" : "joe"},{"$inc" : {"score" : -50}})

"$push" 会向已有的数组字段的末尾加入一个元素,要是没有就创建一个新的数组,例如
db.blog.posts.update({"title" : "A blog post"},{"$push" : {"comments" :{"name" : "joe", "email" : "joe@example.com","content" : "nice post."}}})

"$each" 子操作符,可以配合"$push" 操作,往一个数组字段添加多个元素,例如
db.stock.ticker.update({"_id" : "GOOG"}, {"$push" : {"hourly" : {"$each" : [562.776, 562.790, 559.123]}}})

如果希望数组的最大长度是固定的,那么可以将"$slice" 和"$push"组合在一起使用,这样就可以保证数组不会超出设定好的最大长度
db.movies.find({"genre" : "horror"},{"$push" : {"top10" : {"$each" : ["Nightmare on Elm Street", "Saw"],"$slice" : -10}}})
这个例子会限制数组只包含最后加入的10个元素。"$slice" 的值必须是负整数。如果数组的元素数量大于10,那么只有最新的10个元素会保留。因此,"$slice" 可以用来在文档中创建一个队列。

"$addToSet" 和"$push"一样可以向数组字段的末尾加入一个元素,但是$addToSet可以避免插入重复值例如:
db.users.update({"_id" : ObjectId("4b2d75476cc613d5ee930164")},{"$addToSet" : {"emails" : "joe@hotmail.com"}})。
$addToSet可以用来在文档中创建一个set集合。
$each也能与$addToSet组合使用。

注意,update配合这些操作符只能修改单条文档,如果希望修改多条文档可以使用updateMany

 

如何能够以文档中的某一个数组字段的子文档的属性作为条件,并且修改数组中的某一个文档的属性。例如,在一个文章表中,我想把评论者为john的评论的所有文章下的评论的作者改为jenkin

db.blog.posts.find()
[
    {
        "_id" : ObjectId("4b329a216cc613d5ee930192"),
        "content" : "...",
        "comments" : [
            {
                "comment" : "good post",
                "author" : "John",
                "votes" : 0
            },
            {
                "comment" : "i thought it was too short",
                "author" : "Claire",
                "votes" : 3
            },
            {
                "comment" : "free watches",
                "author" : "Alice",
                "votes" : -1
            }
        ]
    },
    {
        "_id" : ObjectId("4b329a216cc613d5ee930192"),
        "content" : "...",
        "comments" : [
            {
                "comment" : "good post",
                "author" : "John",
                "votes" : 0
            },
            {
                "comment" : "i thought it was too short",
                "author" : "Claire",
                "votes" : 3
            },
            {
                "comment" : "free watches",
                "author" : "Alice",
                "votes" : -1
            }
        ]
    }
]


可以这样:

db.blog.posts.updateMany({"comments.author" : "John"}, {$set:{"comments.$.author" : "Jenkey"}})

其中$的意思是修改范围为数组里面所有的下标。如果希望指定某个下标,可以用数字{$set:{"comments.2.author" : "Jenkey"}}。


更多的更新操作符可以搜索官方文档。

 


修改器速度
修改一个文档的速度取决于是否会导致文档从磁盘的原有位置移动到磁盘的其他位置。
将文档插入到MongoDB中时,依次插入的文档在磁盘上的位置是相邻的。如果一个文档由于修改操作变大了,原先的位置就放不下这个文档了,这个文档就会被移动到集合中的另一个位置。
$inc 能就地修改,因为不需要改变文档的大小,只需要将键的值修改一下(对文档大小的改变非常小),所以非常快。而数组修改器可能会改变文档的大小(尤其是$push和$addToSet的时候,因此这两个修改器往往可能成为修改文档的性能瓶颈),就会慢一些("$set" 在文档大小不发生变化情况下会很快,如果会改变文档大小,其性能也会有所下降)。

可以在实际操作中看到这种变化。创建一个包含几个文档的集合,对某个位于中间的文档进行修改,使其文档大小变大。然后会发现这个文档被移动到了集合的尾部。

MongoDB不得不移动一个文档时,它会修改集合的填充因子 (padding factor)。填充因子是MongoDB为每个新文档预留的增长空间。可以运行db.coll.stats() 查看填充因子。执行上面的更新之前,"paddingFactor" 字段的值是1:根据实际的文档大小,为每个新文档分配精确的空间,不预留任何增长空间。让其中一个文档增大之后,再次运行这个命令,会发现填充因子增加到了1.5:为每个新文档预留其一半大小的空间作为增长空间。如

果随后的更新导致了更多次的文档移动,填充因子会持续变大(虽然不会像第一次移动时的变化那么大)。如果不再有文档移动,填充因子的值会缓慢降低。而且文档频繁移动导致的填充因子的变大意味着空间碎片的增多,也就是说很多多余的存储空间没有使用(没有存内容),但这些空间却被占用了。
如果有太多不能重用的空白空间,你会经常在日志中看到如下信息:

Thu Apr 5 01:12:28 [conn124727] info DFM::findAll(): extent a:7f18dc00 was empty, skipping ahead


这就是说,执行查询时,MongoDB会在整个范围内进行查找,却找不到任何文档:这只是个空白空间。这个消息提示本身没什么影响,但是它指出你当前拥有太多的碎片,可能需要进行压缩。

其实不仅是修改会引起可能的移动文档,删除也是会引起文档移动。所以尽可能减少这样的操作。

移动文档是非常慢的。MongoDB必须将文档原先所占的空间释放掉,然后将文档写入另一片空间。因此,应该尽量让填充因子的值接近1(大于1就意味着存在空间碎片,当然这样的空间碎片在该文档进行数据扩涨的时候是有好处的,可以避免再一次移动文档,可如果存在太多的空间碎片或太大块的空间碎片就会造成空间浪费)。



写入安全机制

写入安全 (Write Concern)是一种客户端(如php或python的mongodb的模块)设置,用于控制写入的安全级别。
默认情况下,插入、删除和更新都会一直等待数据库响应(写入是否成功),然后才会继续执行。通常,遇到错误时,客户端会抛出一个异常。

两种最基本的写入安全机制是应答式 写入(acknowledged wirte)和非应答式 写入(unacknowledged write)。应答式写入是默认的方式:数据库会给出响应,告诉你写入操作是否成功执行。非应答式写入不返回任何响应,所以无法知道写入是否成功。我理解为如果是应答式写入,那么所有的更新和插入操作都是阻塞的方法,非应答式则是异步非阻塞的方法。

通常来说,应用程序应该使用应答式写入。但是,对于一些不是特别重要的数据(比如日志或者是批量加载数据),你可能不愿意为了自己不关心的数据而等待数据库响应。在这种情况下,可以使用非应答式写入。

非应答式写入不返回数据库错误,但是如果尝试向已经关闭的套接字(socket)执行写入,或者写入套接字时发生了错误,都会引起异常。

使用非应答式写入时,一种经常被忽视的错误是插入无效数据。比如,如果试图插入两个具有相同"_id" 字段的文档。键重复异常是一种非常常见的错误,还有其他很多类似的错误,比如无效的修改器或者是磁盘空间不足等。


 

 

查询数据
使用find 或者findOne 函数和查询文档对数据库执行查询;
使用$ 条件查询实现范围查询、数据集包含查询、不等式查询,以及其他一些查询;

查询将会返回一个数据库游标,游标只会在你需要时才将需要的文档批量返回;
还有很多针对游标执行的元操作,包括忽略一定数量的结果,或者限定返回结果的数量,以及对结果排序。


find查询

MongoDB中使用find 来进行查询。查询就是返回一个集合中文档的子集,子集合的范围从0个文档到整个集合。find 的第一个参数决定了要返回哪些文档,这个参数是一个文档,用于指定查询条件。
空的查询文档(例如{})会匹配集合的全部内容。要是不指定查询文档,默认就是{} 。


指定需要返回的键
有时并不需要将文档中所有键/值对都返回。遇到这种情况,可以通过find (或者findOne )的第二个参数来指定想要的键。这样做既会节省传输的数据量,又能节省客户端解码文档的时间和内存消耗。

例如,如果只对用户集合的"username" 和"email" 键感兴趣,可以使用如下查询返回这些键

db.users.find({}, {"username" : 1, "email" : 1})

 

默认情况下"_id" 这个键总是被返回,即便是没有指定要返回这个键。
也可以用第二个参数来剔除查询结果中的某些键/值对。例如,我们不希望结果中含有"fatal_weakness" 键
db.users.find({}, {"fatal_weakness" : 0})

使用这种方式,也可以把"_id" 键剔除掉:
db.users.find({}, {"username" : 1, "_id" : 0})

查询条件
"$lt" 、"$lte" 、"$gt" 和"$gte" 就是全部的比较操作符,分别对应<、<=、>和>=。例如
db.users.find({"age" : {"$gte" : 18, "$lte" : 30}})
这样就可以查找到"age" 字段大于等于18、小于等于30的所有文档。

这样的范围查询对日期尤为有用

start = new Date("01/01/2007")
db.users.find({"registered" : {"$lt" : start}})


操作符"$ne" 表示“!=”,"$ne" 能用于所有类型的数据。

db.users.find({"username" : {"$ne" : "joe"}})

 

"$in" 可以用来查询一个键的多个值;

db.raffle.find({"ticket_no" : {"$in" : [725, 542, 390]}})

"$nin" 将返回与数组中所有条件都不匹配的文档。

 

"$or" 接受一个包含所有可能条件的数组作为参数,例如:

db.raffle.find({"$or" : [{"ticket_no" : {"$in" : [725, 542, 390]}}, {"winner" : true}]})

翻译成where原语就是 where ticket_no in [725, 542, 390] or winner = true;

有一种比较奇妙的查询条件,查询x字段等于4并且小于等于1的文档。这看似矛盾,但其实也是合理的,例如x字段的值是一个数组[0, 4]。此时查询条件应该这样写:

db.users.find({"$and" : [{"x" : {"$lte" : 1}}, {"x" : 4}]})

 

 

关于null
例如有3个文档

{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

db.c.find({"y" : null})可以按照预期的方式查询"y" 键为null 的文档,null 不仅会匹配某个键的值为null 的文档,而且还会匹配不包含这个键的文档,所以下面的条件会匹配到所有文档:

db.c.find({"z" : null})


原因是所有文档都没有z这个字段。

如果仅想匹配键值为null 的文档,既要检查该键的值是否为null ,还要通过"$exists" 条件判定键值已存在:

db.c.find({"z" : {"$in" : [null], "$exists" : true}})

 

 

正则表达式

执行不区分大小写的匹配

db.users.find({"name" : /joe/i})

 

查询name字段包含abc字符串的文档:

db.users.find({"name" : /.*?abc.*?/i})

 

查询name字段以abc字符串开头的文档:

db.users.find({"name" : /^abc/i})


这种匹配是能够使用到索引的,极为高效。

 

 

查询数组
假如fruit字段是一个数组,那么下面的条件是能够查询到fruit字段包含banana元素的文档

db.food.insert({"fruit" : ["apple", "banana", "peach"]})
db.food.insert({"fruit" : "apple", "fruit" : "banana", "fruit" : "peach"})

 

db.food.find({"fruit" : "banana"})      // 能把上面两条都匹配到

 

假如要找到数组中既有"apple" 又有"banana" 的文档,可以用$all:

db.food.find({fruit : {$all : ["apple", "banana"]}})

 

假如条件为fruit中的所有元素都被包含在一个数组x中,例如fruit是['a', 'b'], x是['a','b','c'],此时可以用$in

 

假如想要做到数组的全匹配,例如我就想匹配到下面的第一条:
db.food.insert({"_id" : 1, "fruit" : ["apple", "banana", "peach"]})
db.food.insert({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]})
db.food.insert({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})

条件就要写为

db.food.find({"fruit" : ["apple", "banana", "peach"]})

以下写法是不会匹配到第一条的:

db.food.find({"fruit" : ["apple", "banana"]})   //少了1个元素
db.food.find({"fruit" : ["banana", "apple", "peach"]})   //顺序不对

如果想匹配到fruit的第3个元素为peach的文档,可以这样

db.food.find({"fruit.2" : "peach"})


"$size"  可以用它查询特定长度的数组

db.food.find({"fruit" : {"$size" : 3}})     // 查fruit的长度为3的文档

 

"$size" 并不能与其他查询条件(比如"$gt" )组合使用,因此我们无法直接查询数组长度的范围,例如查询元素个数大于3的fruit的文档。
此时我们可以通过在文档中添加一个"size" 字段的方式来记录数组的长度来实现长度的范围查询。如:

db.food.update(criteria,{"$push" : {"fruit" : "strawberry"}, "$inc" : {"size" : 1}})    // 而且这还是一个原子操作


但是这种技巧并不能与"$addToSet" 操作符同时使用。因为$addToSet不一定会使得数组增大。


$slice 可以获取数组字段的指定元素子集。例如:

db.blog.posts.findOne(cond, {"comments" : {"$slice" : 10}})     // cond是查询条件,第二参是指定要查的字段comments,这个字段是一个数组,此时可以使用$slice截取数组的元素。 -10则是获取后10个元素。

 

"$slice" 也可以指定偏移值:

db.blog.posts.findOne(criteria, {"comments" : {"$slice" : [23, 10]}})       // 23是偏移量

 

$slice如果出现,那么将返回文档中的所有键。
例如有个文档:

{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "title" : "A blog post",
    "content" : "...",
    "comments" : [
        {
            "name" : "joe",
            "email" : "joe@example.com",
            "content" : "nice post."
        },
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

使用这个:

db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}})

依旧会返回title和content字段,除非是这样才不会返回title和content字段:

db.blog.posts..find({},{comments:{$slice:-1}, content:0, title:0})


如果数组中的元素是一个对象,我们还能根据数组中对象的属性来查询,例如

p.find({"comments.name":"bob"})     // 查询评论的作者有bob的文档


返回:

{
        "_id" : ObjectId("4b2d75476cc613d5ee930164"),
        "title" : "A blog post",
        "content" : "...",
        "comments" : [
                {
                        "name" : "joe",
                        "email" : "joe@example.com",
                        "content" : "nice post."
                },
                {
                        "name" : "bob",
                        "email" : "bob@example.com",
                        "content" : "good post."
                }
        ]
}

db.blog.posts.find({"comments.name" : "bob"}, {"comments.$" : 1})    // 查询评论的作者有bob的文档,并且我希望comments中只显示bob的所有评论,不要joe的评论


返回:

{ "_id" : ObjectId("4b2d75476cc613d5ee930164"), "comments" : [ { "name" : "bob", "email" : "bob@example.com", "content" : "good post." } ] }

不过使用comments.$只显示1条bob的评论。即使写为"comments.$":100也只显示1条bob的评论。

 

数组和范围查询的相互作用
假如某个文档的"x" 字段是一个数组,如果"x" 键的某一个元素与查询条件的任意一条语句相匹配,那么这个文档也会被返回
例如有以下4个文档:
{"x" : 5}
{"x" : 15}
{"x" : 25}
{"x" : [5, 25]}

db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}) 
可以匹配到
{"x" : 15}
{"x" : [5, 25]}     // 因为25与查询条件中的第一个语句(大于10)相匹配,5与查询条件中的第二个语句(小于20)相匹配。所以能匹配到。

如果希望范围查询能在数组字段也生效,可以用"$elemMatch" ,但是"$elemMatch" 只能用于数组元素和内嵌文档:

db.test.find({"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}})    // 查不到任何结果

 

如果当前查询的字段上创建过索引,可以使用min() 和max() 将查询条件遍历的索引范围限制为"$gt" 和"$lt" 的值:

db.test.find({"x" : {"$gt" : 10, "$lt" : 20}).min({"x" : 10}).max({"x" : 20})   // {"x" : 15}

只有当前查询的字段上建立过索引时,才可以使用min() 和max() 

 

 

 

查询内嵌文档
内嵌文档就是一个字段的的值也是一个或多个文档(json对象或数组中包着多个json对象)
例如:

{
    "name" : {
        "first" : "Joe",
        "last" : "Schmoe"
     },
     "age" : 45
} 

查寻姓为Joe,名Schmoe的人可以这样:

po.find({"name.first":"Joe", "name.last":"Schmoe"})
//或者 
po.find({"name" : {"first" : "Joe", "last" : "Schmoe"}})

 

但是以下查询是没有结果的,原因是要查询一个完整的子文档,子文档必须精确匹配:

po.find({"name" : {"last" : "Schmoe","first" : "Joe"}})     //顺序不对
po.find({"name" : {"last" : "Schmoe"}})         // 没有完整匹配
po.find({"name" : {"first" : "Joe"}})

 

我们可以使用点表示法查询内嵌文档的键来进行部分属性的查询,如:

po.find({"name.first":"Joe"})

不过需要注意的是,插入数据的时候内嵌文档的key不能包含点,比如不要以url作为内嵌文档的key。


内嵌文档的多条件匹配
看看这个例子:

{
    "content" : "...",
    "comments" : [
        {
            "author" : "joe",
            "score" : 3,
            "comment" : "nice post"
        },
        {
            "author" : "mary",
            "score" : 6,
            "comment" : "terrible post"
        }
    ]
}

 

查询5分以上的评论:

cond = { "comments.score" : { "$gt" : 5 } }        // 可以查到
p.find(cond)

如果我有两个及以上的条件:查询5分以上且评论者为joe的文档

cond = { "comments.score":{$gt:5} ,"comments.author":"joe"}     // 按逻辑是不应该查到数据的,但是他却查到了,因为内嵌文档的查询条件只要有1个条件满足了就会返回相应文档。这里满足了第一个条件。

 

此时同样可以使用$elemMatch来整合多个条件:

cond = { "comments":{$elemMatch:{"author":"joe", "score":{"$gt":5}}}}       // 此时就查不到了,说明这个条件是正确的。


$where查询
$where查询可以做一些更复杂的条件匹配,它的值是一个函数
"$where" 语句最常见的应用就是比较文档中的两个键的值是否相等。例如

db.foo.insert({"apple" : 1, "banana" : 6, "peach" : 3}) 
db.foo.insert({"apple" : 8, "spinach" : 4, "watermelon" : 4})

 db.foo.find({"$where" : function () {
 for (var current in this) {
     for (var other in this) {
         if (current != other && this[current] == this[other]) {
             return true;
         }
     }
 }
 return false;
 }});

为安全起见,应该严格限制或者消除"$where" 语句的使用。应该禁止终端用户使用任意的"$where" 语句。
不是非常必要时,一定要避免使用"$where" 查询,因为它们在速度上要比常规查询慢很多。每个文档都要从BSON转换成JavaScript对象,然后通过"$where" 表达式来运行。而且"$where" 语句不能使用索引,所以只在走投无路时才考虑"$where" 这种用法。

先使用常规查询进行过滤,然后再使用"$where" 语句,这样组合使用可以降低性能损失。如果可能的话,使用"$where"语句前应该先使用索引进行过滤,"$where" 只用于对结果进行进一步过滤。

 

 


游标
find方法会返回一个游标对象(cursor)。游标通常能够对最终结果进行有效的控制。可以限制结果的数量,略过部分结果,根据任意键按任意顺序的组合对结果进行各种排序,或者是执行其他一些强大的操作。

调用find 时,shell并不立即查询数据库,而是等待真正开始要求获得结果时才发送查询(如cursor.next()或者cursor.hasNext(),而且也不是每次一执行cursor.next()都会去拿一次,而是先拿一定数量或大小如4M的文档。假如用户要拿100万条数据,可能第一次执行cursor的next时会先从数据库查10万条交给cursor,调用next的时候先从内存中的10万条,等cursor中的数据用完了才会再连接数据库再查询10万条,直到100万条数据都查完)。几乎游标对象的每个方法都返回游标本身,这样就可以按任意顺序组成方法链。

var cursor = db.foo.find().sort({"x" : 1}).limit(1).skip(10);
// cursor.hasNext()
cursor.forEach(function(x) {
    print(x.name);
});

 

limit、skip和sort方法
最常用的查询选项就是限制返回结果的数量、忽略一定数量的结果以及排序。

db.c.find().skip(3)     // 该操作会略过前三个匹配的文档,然后返回余下的文档。如果集合里面能匹配的文档少于3个,则不会返回任何文档。

sort 接受一个对象作为参数,这个对象是一组键/值对,键对应文档的键名,值代表排序的方向。排序方向可以是1(升序)或者-1(降序)。
db.c.find().sort({username : 1, age : -1})

这3个方法可以组合使用。这对于分页非常有用。skip()相当于是偏移量,然而skip过多的条数会导致性能问题(就像mysql分页那样),应该避免使用skip(),优化方式可以参考mysql的分页,例如使用覆盖索引或记录上一页的最后一个id查询比这个id大的n条数据。

 

其他可能用到skip的场景以及优化技巧:
1.随机选取1条文档
错误的方式:先计算文档总数,然后选择一个从0到文档数量之间的随机数,利用find 做一次查询,略过这个随机数那么多的文档

var total = db.foo.count()
var random = Math.floor(Math.random()*total)
db.foo.find().skip(random).limit(1)     // 一旦random很大,就会很慢

正确的方式,可以在插入文档的时候加一个字段(如rand_num)用来表示随机数,然后在程序中随机生成一个random值并获取大于这个random值的第一个文档即可,如下:

db.people.insert({"name" : "joe", "random" : Math.random()})
db.people.insert({"name" : "john", "random" : Math.random()})
db.people.insert({"name" : "jim", "random" : Math.random()})

var random = Math.random()
result = db.foo.findOne({"random" : {"$gt" : random}})

偶尔也会遇到产生的随机数比集合中所有随机值都大的情况,这时就没有结果返回了。遇到这种情况,那就获取小于这个random值的第一个文档即可。


$min, $max和$maxscan 
$maxscan指定本次查询中扫描文档数量的上限如
db.foo.find(criteria)._addSpecial("$maxscan", 20)
如果不希望查询耗时太多,也不确定集合中到底有多少文档需要扫描,那么可以使用这个选项。这种方式的一个坏处是,某些你希望得到的文档没有扫描到。

$min, $max只能用在索引的字段上
"$min" 强制指定一次索引扫描的下边界,这在复杂查询中非常有用,通常应该使用"$gt" 代替"$min"。
$max同理。


查询快照

数据处理通常的做法就是先把数据从MongoDB中取出来,然后做一些变换,最后再存回去:
cursor = db.foo.find({/*条件*/});     // 不会真的去查数据库

while (cursor.hasNext()) {
    var doc = cursor.next();    // 这里才会真的去查
    doc = process(doc);
    db.foo.save(doc);
}

考虑一个问题,如果我们对一个doc文档修改后会改变他的大小,那么这个doc存回集合的时候位置变到后面,导致我们往后拿数据的时候会重复查询到它。
应对这个问题的方法就是对查询进行快照 (snapshot)。如果使用了这个选项,查询就在"_id"索引上遍历执行,这样可以保证每个文档只被返回一次。我们只需要改为:
db.foo.find({/*条件*/}).snapshot()
这样的话,在游标cursor内每一次next()都不会查到重复数据。

不过新版本的mongodb好像已经没有这个方法了,但是我们依旧需要注意这个问题。


游标生命周期
看待游标有两种角度:客户端的游标以及客户端游标表示的数据库游标。前面讨论的都是客户端的游标,接下来简要看看服务器端发生了什么。

在服务器端,游标消耗内存和其他资源。游标遍历尽了结果以后,或者客户端发来消息要求终止,数据库将会释放这个游标资源。所以要尽量保证尽快释放游标(在合理的前提下)。

游标完成所有时,它会清除自身。另外,如果游标在客户端被释放或销毁时,驱动程序会向服务器发送一条特别的消息,让其销毁服务端的游标。最后,即便用户没有迭代完所有结果,如果一个游标在10分钟内没有使用的话,数据库游标也会自动销毁。这样的话,如果客户端崩溃或者出错,MongoDB就不需要维护这上千个被打开却不再使用的游标。这种“超时销毁”的行为是我们希望的。

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 深入学习mongdb(一) mongdb增删改查

热门推荐
推荐新闻