Chinaunix首页 | 论坛 | 博客
  • 博客访问: 25501
  • 博文数量: 13
  • 博客积分: 464
  • 博客等级: 下士
  • 技术积分: 140
  • 用 户 组: 普通用户
  • 注册时间: 2010-09-19 21:58
文章分类

全部博文(13)

分类: NOSQL

2017-05-05 16:41:25

CRUD即是对文档的创建(create)、读取(read)、更新(update)和删除(delete)操作

【创建操作】

写入操作是增加新文档到集合,如果集合当前尚不存在,写入操作将创建该集合

MongoDB提供以下方法写入数据到集合:
- db.collection.insertOne()
- db.collection.insertMany()

在MongoDB里,写入操作的目标是单个集合,所有写操作在集合层面都是原子性的

db.users.insertOne(    <-- 集合
  {            <-- 文档
    name: "sue",    <-- 键值对
    age: 26,
    status: "pending"
  }
)

【读操作】

读操作是从集合中检索文档,MongoDB提供 db.collection.find() 方法从集合中读取文档

db.users.find(            <-- 集合
  { age: { $gt: 18 } },        <-- 查询条件
  { name: 1, address: 1 }    <-- 返回字段
).limit(5)            

【更新操作】

更新操作是修改集合中已存在的文档,MongoDB提供以下方法更新集合中的文档:

- db.collection.updateOne()
- db.collection.updateMany()
- db.collection.replaceOne()

在MongoDB里,更新操作的目标是单一集合,所有写操作在单个文档层面都是原子性的

db.users.updateMany(            <-- 集合
  { age: { $lt: 18 } },            <-- 过滤条件
  { $set: { status: "reject" } }    <-- 被更新字段
)

【删除操作】

删除操作是移除集合中的文档,MongoDB提供以下方法从集合中删除文档:

- db.collection.deleteOne()
- db.collection.deleteMany()

在MongoDB里,删除操作的标的为单个集合,所有删除操作在单个文档层面都是原子性的

db.users.deleteMany(    <-- 集合
  { status: "reject" }    <-- 删除条件
)


== 写入文档 ==

写入文档时,如果集合当前不存在,会自动创建该集合

【写入单个文档】

db.collection.insertOne()方法用于写入单个文档到集合里
(python版)pymongo.collection.Collection.insert_one()方法用于写入单个文档到集合里

下例,写入新文档到inventory集合,即使新文档里未指定_id字段,MongoDB也会增加_id字段到新文档,其值是一个ObjectId

> db.inventory.insertOne( { item: "canvas", qty: 100, tags: ["cotton"], size: { h: 28, w: 35.5, uom: "cm" } } )

insertOne()返回一个文档,其中包含该新写入文档的_id字段值
(Python版)insert_one()返回一个pymongo.results.InsertOneResult对象,其中inserted_id字段包含新写入文档的_id值

使用以下查询命令读取刚写入的文档

> db.inventory.find( { item: "canvas" } )
(Python版)cursor = db.inventory.find({"item": "canvas"})

【写入多个文档】

db.collection.insertMany()方法用于向集合写入多个文档,传递一个文档数组作为该方法的参数
(Python版)pymongo.collection.Collection.insert_many()方法用于向集合写入多个文档,传递一个文档列表作为该方法的参数

下例写入三个新文档到inventory集合,即使新文档无_id字段,MongoDB在写入它们时也会自动为每个文档都加上_id字段,其值为ObjectId

> db.inventory.insertMany([
    { item: "journal", qty: 25, tags: ["blank", "red"], size: { h: 14, w: 21, uom: "cm" } },
    { item: "mat", qty: 85, tags: ["gray"], size: { h: 27.9, w: 35.5, uom: "cm" } },
    { item: "mousepad", qty: 25, tags: ["gel", "blue"], size: { h: 19, w: 22.85, uom: "cm" } }
  ])

insertMany()返回一个文档,其包含所有新写入文档的_id字段值
(Python版)insert_many()返回一个pymongo.results.InsertManyResult对象,其中inserted_ids字段包含所有新写入文档的_id值列表

使用以下查询命令读取写入的文档

> db.inventory.find( {} )
(Python版)cursor = db.inventory.find({})

【写入行为】

> 集合创建
  如果集合当前不存在,写入操作将创建该集合

> _id字段
  在MongoDB里,每个存储在集合里的文档都必须有一个名为_id的字段,其具有唯一性担任主键,如果一个新写入的文档缺省_id字段,MongoDB会自动生成该字段,其值是一个ObjectId,由带upsert:true选项的更新操作写入的文档也是如此

> 原子性
  MongoDB里所有写入操作在单个文档层面都是原子性的

> 写入确认
  通过写入确认,可以为写入操作指定从MongoDB返回的请求确认等级


== 写入方法 ==

MongoDB提供以下写入文档到集合的方法:

序号    方法                描述
  1.    db.collection.insertOne()    写入单个文档到集合
  2.    db.collection.insertMany()    一次写入多个文档到集合,以文档数组作为参数
  3.    db.collection.insert()        写入单个或多个文档到集合


== 查询文档 ==

以下给出的例子均为在mongo shell里使用db.collection.find()方法的查询操作,先写入以下数据
(Python版)以下给出的例子均为使用驱动PyMongo中的pymongo.collection.Collection.find()方法,先写入以下数据

> db.inventory.insertMany([
     { item: "journal", qty: 25, size: { h: 14, w: 21, uom: "cm" }, status: "A" },
     { item: "notebook", qty: 50, size: { h: 8.5, w: 11, uom: "in" }, status: "A" },
     { 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" }
  ]);

【选取集合里的所有文档】

选取集合里的所有文档,传递一个空文档{}作为查询条件给find方法,如下:

> db.inventory.find( {} )
(Python版)cursor = db.inventory.find({})

该操作对应以下SQL语句:

> SELECT * FROM inventory;

【指定等式条件】

在查询条件里使用:表达式指定等式查询条件

{ : , ... }

下例,从inventory集合中选取所有status字段值为"D"的文档:

> db.inventory.find( { status: "D" } )
(Python版)cursor = db.inventory.find({"status": "D"})

该操作的对应SQL语句为:

> SELECT * FROM inventory WHERE status = "D";

【指定查询操作符】

在查询过滤文档中使用查询操作符来指定查询条件,如下

{ : { : }, ... }

下例从inventory集合中取出所有status为"A"和"D"的文档

> db.inventory.find( { status: { $in: [ "A", "D" ] } } )
(Python版)cursor = db.inventory.find({"status": {"$in": ["A", "D"]}})

虽然也可以用$or操作符编写该表达式,但在同一字段的多个等式查询上使用$in操作符更优于$or

该操作的对应SQL语句为:

> SELECT * FROM inventory WHERE status in ("A", "D");

【指定AND条件】

一个复合查询能在集合文档的多个字段上指定查询条件,隐式地,当给出多个条件子句时,会以逻辑AND相连接,因此只会选取出集合中同时匹配所有条件的文档

下例取出inventory集合中所有status为"A"且qty小于($lt)30的文档:

> db.inventory.find( { status: "A", qty: { $lt: 30 } } )
(Python版)cursor = db.inventory.find({"status": "A", "qty": {"$lt": 30}})

该操作对应以下SQL语句:

> SELECT * FROM inventory WHERE status = "A" AND qty < 30;

【指定OR条件】

使用$or操作符指定一个复合查询条件,以逻辑OR关联所有条件子句,因此会选取出集合中匹配任一条件的文档

下例取出inventory集合中所有status为"A"或qty小于($lt)30的文档:

> db.inventory.find( { $or: [ { status: "A" }, { qty: { $lt: 30 } } ] } )
(Python版)cursor = db.inventory.find({"$or": [{"status": "A"}, {"qty": {"$lt": 30}}]})

该操作对应以下SQL语句:

> SELECT * FROM inventory WHERE status = "A" OR qty < 30;

【同时指定AND和OR条件】

下例中,复合查询文档选取集合中所有status为"A"且qty小于30或item以字符p开头的文档

> db.inventory.find( { status: "A", $or: [ { qty: { $lt: 30 } }, { item: /^p/ } ] } )
(Python版)cursor = db.inventory.find({"status": "A", "$or": [{"qty": {"$lt": 30}}, {"item": {"$regex": "^p"}}]})

该操作对应以下SQL语句:

> SELECT * FROM inventory WHERE status = "A" AND ( qty < 30 OR item LIKE "p%");

另外,MongoDB也支持正则表达式$regex来执行字符串模式匹配

【查询行为】

> 游标

db.collection.find()方法返回一个游标,用心获取匹配查询条件的文档

【其他方法】

以下方法也可以从集合中读取文档:

- db.collection.findOne()方法,执行读取操作并返回一个匹配查询条件的文档
- 在聚合操作里,$match管道阶段也是对集合进行查询


== 查询内嵌文档 ==

【匹配一个内嵌文档】

在内嵌文档上指定一个等式条件,使用查询过滤文档{ : },其中是要匹配的文档

例如,下例查询选取所有size值含有子文档{ h: 14, w: 21, uom: "cm" }的文档:

> db.inventory.find( { size: { h: 14, w: 21, uom: "cm" } } )
(Python版)
from bson.son import SON
cursor = db.inventory.find({"size": SON([("h", 14), ("w", 21), ("uom", "cm")])})

等值匹配是必须给出完整子文档的,不能少字段,也不能打乱字段顺序,例如,以下查询将匹配不到任何inventory集合里的文档:

> db.inventory.find( { size: { h: 14, w: 21 } } )
> db.inventory.find( { size: { w: 21, h: 14, uom: "cm" } } )

第一条查询少了uom字段,第二条查询打乱了h和w字段的顺序

【在内嵌字段上查询】

使用点标记("field.nestedField")可以在一个内嵌文档的字段上指定一个查询条件

> 在内嵌字段上指定等值匹配
  下例选取所有字段size的内嵌字段uom为"in"的文档:

  db.inventory.find( { "size.uom": "in" } )
  (Python版)cursor = db.inventory.find({"size.uom": "in"})

> 使用查询操作符指定匹配条件
  下例选取所有字段size的内嵌字段h小于15的文档:

  db.inventory.find( { "size.h": { $lt: 15 } } )
  (Python版)cursor = db.inventory.find({"size.h": {"$lt": 15}})

> 指定AND条件
  下例查询选取所有内嵌字段h小于15、内嵌字段uom为"in",并且status为"D"的文档:

  db.inventory.find( { "size.h": { $lt: 15 }, "size.uom": "in", status: "D" } )
  (Python版)cursor = db.inventory.find({"size.h": {"$lt": 15}, "size.uom": "in", "status": "D"})


== 数组查询 ==

写入以下数据:

> db.inventory.insertMany([
    { item: "journal", qty: 25, tags: ["blank", "red"], dim_cm: [ 14, 21 ] },
    { item: "notebook", qty: 50, tags: ["red", "blank"], dim_cm: [ 14, 21 ] },
    { item: "paper", qty: 100, tags: ["red", "blank", "plain"], dim_cm: [ 14, 21 ] },
    { item: "planner", qty: 75, tags: ["blank", "red"], dim_cm: [ 22.85, 30 ] },
    { item: "postcard", qty: 45, tags: ["blue"], dim_cm: [ 10, 15.25 ] }
  ]);

【匹配数组】

在数组上指定等值条件,使用查询文档{ : },其中是用于匹配的完整数组,且元素顺序也须相同

下例查询出所有字段tags值为数组["red", "blank"]的文档:

> db.inventory.find( { tags: ["red", "blank"] } )
(Python版)cursor = db.inventory.find({"tags": ["red", "blank"]})

如果只是要查询出数组中包含"red"和"blank",而不关心元素顺序和数组中是否还包含其他元素的话,使用$all操作符:

> db.inventory.find( { tags: { $all: ["red", "blank"] } } )
(Python版)cursor = db.inventory.find({"tags": {"$all": ["red", "blank"]}})

【查询数组中的元素】

使用过滤器{ : }查询数组字段中是否包含至少一个指定值,其中是要匹配的元素值

下例查询出所有tags数组包含元素"red"的文档:

> db.inventory.find( { tags: "red" } )
(Python版)cursor = db.inventory.find({"tags": "red"})

使用{ : { : , ... } }在数组字段中的元素上指定查询过滤条件

下例查询出所有dim_cm数组包含值大于25的元素的文档:

> db.inventory.find( { dim_cm: { $gt: 25 } } )
(Python版)cursor = db.inventory.find({"dim_cm": {"$gt": 25}})

【为数组元素指定多个条件】

当在数组元素上指定复合条件时,可以是数组中单个元素满足这些条件,也可以是数组中多个元素的组合满足这些条件

> 以在数组元素上的复合过滤条件查询数组
  下例查询出数组dim_cm中包含值大于15且小于20的元素或一个元素大于15另一个元素小于20的文档:

  db.inventory.find( { dim_cm: { $gt: 15, $lt: 20 } } )
  (Python版)cursor = db.inventory.find({"dim_cm": {"$gt": 15, "$lt": 20}})

> 一个数组元素满足多个要求
  使用$elemMatch操作符在数组的元素上指定多个要求,查询出数组中有元素满足所有指定要求的文档

  下例查询出所有在dim_cm数组中存在即大于22又小于30的元素的文档:

  db.inventory.find( { dim_cm: { $elemMatch: { $gt: 22, $lt: 30 } } } )
  (Python版)cursor = db.inventory.find({"dim_cm": {"$elemMatch": {"$gt": 22, "$lt": 30}}})

> 查询数组中指定位置的元素
  使用点标记为数组中指定位置的元素设置查询条件,元素标号从0开始

  下例查询出所有数组dim_cm的第2个元素值大于25的文档:

  db.inventory.find( { "dim_cm.1": { $gt: 25 } } )
  (Python版)cursor = db.inventory.find({"dim_cm.1": {"$gt": 25}})

> 以数组长度作为查询条件

  使用$size操作符以数组元素个数查询文档,下例选取所有数组tags有3个元素的文档:

  db.inventory.find( { "tags": { $size: 3 } } )
  (Python版)cursor = db.inventory.find({"tags": {"$size": 3}})


== 查询内嵌文档数组 ==

写入以下数据:

> db.inventory.insertMany( [
    { item:"journal", instock: [ { warehouse:"A", qty:5 }, { warehouse:"C", qty:15 } ] },
    { item:"notebook", instock: [ { warehouse:"C", qty:5 } ] },
    { item:"paper", instock: [ { warehouse:"A", qty:60 }, { warehouse:"B", qty:15 } ] },
    { item:"planner", instock: [ { warehouse:"A", qty:40 }, { warehouse:"B", qty:5 } ] },
    { item:"postcard", instock: [ { warehouse:"B", qty:15 }, { warehouse:"C", qty:35 } ] }
  ]);

【查询数组中的内嵌文档】

下例选择所有数组instock中有元素匹配指定文档的文档:

> db.inventory.find( { "instock": { warehouse: "A", qty: 5 } } )
(Python版)
from bson.son import SON
cursor = db.inventory.find({"instock": SON([("warehouse", "A"), ("qty", 5)])})

等值匹配要给全所有的字段,且字段顺序也须相同,下例将查不出任何文档

> db.inventory.find( { "instock": { qty: 5, warehouse: "A" } } )
(Python版)cursor = db.inventory.find({"instock": SON([("qty", 5), ("warehouse", "A")])})

【在文档数组的字段上指定查询条件】

> 使用数组标号查询一个内嵌文档的字段
  使用点标记为数组中指定位置的文档里的字段指定查询条件,下例选项所有instock数组中第1个文档的qty字段值小于20的文档:

  db.inventory.find( { 'instock.0.qty': { $lte: 20 } } )
  (Python版)cursor = db.inventory.find({'instock.0.qty': {"$lte": 20}})

> 在文档数组的内嵌字段上指定一个查询条件
  如果不知道内嵌文档在数组中的位置,可使用点标号连接数组名和内嵌文档的字段名进行查询

  下例选取所有instock数组中存在文档的qty字段值小于等于20的文档:

  db.inventory.find( { 'instock.qty': { $lte: 20 } } )
  (Python版)cursor = db.inventory.find({'instock.qty': {"$lte": 20}})

【为文档数组指定多个查询条件】

当在多个文档数组的内嵌字段上指定查询条件时,既可以是数组中某个文档满足所有条件,也可以是数组中多个文档组合起来满足所有条件

> 单个内嵌文档满足多个查询条件
  使用$elemMatch操作符在文档数组上指定多个查询要求,选取出单个内嵌文档可以满足所有查询要求的文档

  下例选取出所有instock数组中存在内嵌文档的qty等于5且warehouse为"A"的文档:

  db.inventory.find( { "instock": { $elemMatch: { qty: 5, warehouse: "A" } } } )
  (Python版)cursor = db.inventory.find({"instock": {"$elemMatch": {"qty": 5, "warehouse": "A"}}})

  下例查询出所有instock数组中存在内嵌文档的qty大于10且小于20的文档:

  db.inventory.find( { "instock": { $elemMatch: { qty: { $gt: 10, $lte: 20 } } } } )
  (Python版)cursor = db.inventory.find({"instock": {"$elemMatch": {"qty": {"$gt": 10, "$lte": 20}}}})

> 多个内嵌文档组合满足多个查询条件
  如果不使用$elemMatch操作符指定文档数组上的复合查询条件,则查询出那些内嵌文档组合起来满足查询条件的文档

  下例查询出instock数组中既内嵌文档的qty字段大于10又有内嵌文档的qty小于等于20的文档:

  db.inventory.find( { "instock.qty": { $gt: 10,  $lte: 20 } } )
  (Python版)cursor = db.inventory.find({"instock.qty": {"$gt": 10, "$lte": 20}})

  下例查询出instock数组中既内嵌文档的qty值为5又有内嵌文档的warehouse为"A"的文档:

  db.inventory.find( { "instock.qty": 5, "instock.warehouse": "A" } )
  (Python版)cursor = db.inventory.find({"instock.qty": 5, "instock.warehouse": "A"})


== 指定返回的字段 ==

MongoDB的查询默认返回匹配文档的所有字段,可以指定要返回的字段,以减小MongoDB传递到应用的数据量

写入以下数据:

> db.inventory.insertMany( [
    { item: "journal", status: "A", size: { h: 14, w: 21, uom: "cm" }, instock: [ { warehouse: "A", qty: 5 } ] },
    { item: "notebook", status: "A",  size: { h: 8.5, w: 11, uom: "in" }, instock: [ { warehouse: "C", qty: 5 } ] },
    { item: "paper", status: "D", size: { h: 8.5, w: 11, uom: "in" }, instock: [ { warehouse: "A", qty: 60 } ] },
    { item: "planner", status: "D", size: { h: 22.85, w: 30, uom: "cm" }, instock: [ { warehouse: "A", qty: 40 } ] },
    { item: "postcard", status: "A", size: { h: 10, w: 15.25, uom: "cm" }, instock: [ { warehouse: "B", qty: 15 }, { warehouse: "C", qty: 35 } ] }
  ]);

【返回匹配文档的所有字段】

如果不指定返回哪些字段,find()方法会返回匹配文档的所有字段

下例返回所有status为"A"的文档的全部字段:

> db.inventory.find( { status: "A" } )
(Python版)cursor = db.inventory.find({"status": "A"})

该操作对应SQL语句为:

> SELECT * from inventory WHERE status = "A";

【只返回指定字段和_id字段】

可通过 : 1 来明确指定哪些字段被返回,下例返回的字段只有item、status和默认的_id字段:

> db.inventory.find( { status: "A" }, { item: 1, status: 1 } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1})

【排除_id字段】

也可通过 : 0 来明确指定哪些字段不被返回,下例将不再返回默认的_id字段:

> db.inventory.find( { status: "A" }, { item: 1, status: 1, _id: 0 } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "_id": 0})

【返回除指定字段外的所有字段】

下例返回除status和instock字段以外的所有字段:

> db.inventory.find( { status: "A" }, { status: 0, instock: 0 } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"status": 0, "instock": 0})

【返回内嵌文档的指定字段】

使用点标记引用内嵌文档的字段,将其设置为1,可指定要返回的内嵌文档字段

下例返回字段_id、item、status和size文档中的uom字段,返回的uom字段保持嵌入在size文档内

> db.inventory.find( { status: "A" }, { item: 1, status: 1, "size.uom": 1 } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "size.uom": 1})

【排除指定的内嵌文档字段】

使用点标记引用内嵌文档的字段,将其设置为0,可排除指定的内嵌文档字段

下例排除size文档中的uom字段,其他字段全都返回:

> db.inventory.find( { status: "A" }, { "size.uom": 0 } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"size.uom": 0})

【返回文档数组里的字段】

使用点标记指定文档数组里的字段

下例返回字段_id、item、status和instock数组的内嵌文档的qty字段:

> db.inventory.find( { status: "A" }, { item: 1, status: 1, "instock.qty": 1 } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "instock.qty": 1})

【指定返回的数组元素】

MongoDB提供$elemMatch、$slice和$操作符限定返回的数组中包含哪些元素

下例使用$slice操作符只返回instock数组中最后一个元素:

db.inventory.find( { status: "A" }, { name: 1, status: 1, instock: { $slice: -1 } } )
(Python版)cursor = db.inventory.find({"status": "A"}, {"item": 1, "status": 1, "instock": {"$slice": -1}})

$elemMatch、$slice和$是指定哪些数组元素被返回的唯一方法,而不能使用数组下标,例如{ "instock.0": 1 }是不能用于指定返回instock数组的第1个元素的


== 查询Null或缺失的字段 ==

MongoDB中不同的查询操作符对null值的处理不同

写入以下数据:

> db.inventory.insertMany([{ _id: 1, item: null }, { _id: 2 }])
(Python版)
PyMongo驱动里,使用None代替null
db.inventory.insert_many([{"_id": 1, "item": None}, {"_id": 2}])

【等值过滤】

{ item : null }查询匹配item值为null和没有item字段的文档

下例会将之前写入的二个文档都返回:

> db.inventory.find( { item: null } )
(Python版)cursor = db.inventory.find({"item": None})

【类型检查】

{ item : { $type: 10 } }查询只匹配item字段存在,且其值为null的文档

下例返回之前写入的第1个文档:

> db.inventory.find( { item : { $type: 10 } } )
(Python版)cursor = db.inventory.find({"item": {"$type": 10}})

【存在检查】

{ item : { $exists: false } }查询只匹配没有item字段的文档

下例返回之前写入的第2个文档:

> db.inventory.find( { item : { $exists: false } } )
(Python版)cursor = db.inventory.find({"item": {"$exists": False}})


== 在mongo shell里迭代游标 ==

db.collection.find()方法返回的是一个游标,迭代该游标即可访问文档,在mongo shell里,如果没有使用var关键字为返回的游标分配一个变量的话,该游标会自动迭代20次输出结果集中最先的20个文档,可使用DBQuery.shellBatchSize参数改变自动迭代的次数

下述的例子介绍手动迭代游标访问文档和使用迭代器序列的方法

【手动迭代游标】

在mongo shell里,当使用var关键字将find()方法返回的游标分配给变量时,游标不再自动迭代

可以在mongo shell里使用游标方法next()来访问文档,如下例:

> var myCursor = db.users.find( { type: 2 } );
> while (myCursor.hasNext()) {
    print(tojson(myCursor.next()));
  }

可使用printjson()方法来代替print(tojson())以输出文档

此外,还可使用游标方法forEach()来迭代游标访问文档,例如:

> var myCursor =  db.users.find( { type: 2 } );
> myCursor.forEach(printjson);

【迭代器序列】

在mongo shell里,可使用toArray()方法来迭代游标,并将文档返回到一个数组里,随后即可通过数组下标访问文档,如下:

> var myCursor = db.inventory.find( { type: 2 } );
> var documentArray = myCursor.toArray();
> var myDocument = documentArray[3];

toArray()方法会将游标中的所有文档载入内存,即排空该游标

【游标行为】

> 关闭非活动游标
  默认的,mongod会自动关闭已经排空的游标,或非活动状态达到10分钟的游标,要在mongo shell里覆盖该行为,可使用cursor.noCursorTimeout()方法:

  var myCursor = db.users.find().noCursorTimeout();

  使用noCursorTimeout()方法后,mongod将不再会因空闲超时而自动关闭游标myCursor,必须使用cursor.close()方法手动关闭,或排空该游标中的文档

> 游标隔离性
  当游标在返回文档时,其他操作可能与该查询同时进行,由于MMAPv1存储引擎,会使文档发生变化的写操作可能导致游标将该文档返回多次,要避免该情况,须使用快照模式(snapshot mode)

> 游标批次
  MongoDB分批返回查询结果,每一批的数据总量不会超过最大BSON文档大小,即find()、aggregate()、listIndexes和listCollections操作每批返回最多16MB数据,可使用batchSize()调整成一个更小的数据量限制,但不能调大

  find()和aggregate()操作默认有一个初始批次大小,101个文档,随后对结果集游标的getMore操作不再有默认批次大小,只是被限制在16MB的数据量以内

  对于用不上索引的排序操作,mongod必须读取所有文档到内存中,以在执行排序操作后返回结果集游标

  当迭代游标,到达当前返回批次的末尾时,如果还有更多文档,cursor.next()方法将执行getMore操作取出下一批结果集,想要知道当前批次中还剩余多少文档,可使用objsLeftInBatch()方法,例如:

  var myCursor = db.inventory.find();
  var myFirstDocument = myCursor.hasNext() ? myCursor.next() : null;
  myCursor.objsLeftInBatch();


== 更新文档 ==

写入以下数据:

> 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" }
  ]);

【更新集合中的文档】

要更新一个文档,MongoDB提供了update操作符,例如$set用于修改指定字段的值

某些更新操作,诸如$set,如果字段不存在,则会自动创建

> 更新单个文档
  下例使用db.collection.updateOne()方法更新inventory集合中匹配item为"paper"的第一个文档:

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

  其中:
  - 使用$set操作符将size.uom字段更新为"cm",status字段更新为"P"
  - 使用$currentDate操作符更新lastModified字段的值为当前日期,如果lastModified字段不存在,$currentDate将创建该字段

> 更新多个文档
  下例使用db.collection.updateMany()方法在inventory集合上更新所有qty小于50的文档:

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

> 替换单个文档
  db.collection.replaceOne()方法用于替换除_id字段外的整个文档,以整个新文档做为第2个参数传递给该方法

  当替换一个文档时,新文档必须由:键值对组成,且可以有与原文档不同的字段,新文档中不需要给出_id字段,因为_id值是不可变的,如果给出了_id字段,其值必须与原文档相同

  下例替换inventory集合中匹配item为"paper"的第1个文档:

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

【更新行为】

> 原子性
  MongoDB里所有的写操作在单个文档层面都是原子性的

> _id字段
  一旦赋值,将不能更改_id字段的值,也不能用不同的_id字段值来替换已有文档

> 文档大小
  当更新或替换操作增大了文档大小,并超过了为该文档分配的空间时,mongod会重新存储该文档

> 字段顺序
  MongoDB保持字段在文档写入时的顺序,但_id永远是文档的首字段

> 更新插入操作
  如果updateOne()、updateMany()和replaceOne()方法使用了 upsert:true 选项,且没有文档匹配给定的过滤条件,则会创建出一个新文件并将其插入到集合中,如果有匹配到文档,则更新或替换该文档

> 写入确认
  可以为更新或替换操作指定写入确认级别


== 更新方法 ==

MongoDB提供以下几种更新集合中文档的方法:

db.collection.updateOne()     更新单个文件,如果有多个文档与过滤条件匹配,也只更新第一个文档
db.collection.updateMany()    更新所有匹配过滤条件的文档
db.collection.replaceOne()    替换单个文件,如果有多个文档与过滤条件匹配,也只替换第一个文档
db.collection.update()        默认更新或替换第一个匹配过滤条件的文档,如要更新所有匹配文档,需使用multi选项


== 删除文档 ==

【删除所有文档】

要从一个集合中删除所有文档,传递一个空过滤文档{}给db.collection.deleteMany()方法

下例从inventory集合删除所有文档:

> db.inventory.deleteMany({})
(Python版)db.inventory.delete_many({})

【删除匹配条件的文档】

可为删除操作指定过滤条件,以删除特定的文档

要删除所有匹配过滤条件的文档,将一个过滤文件作为参数传递给deleteMany()方法

下例从inventory集合中删除所有status为"A"的文档:

> db.inventory.deleteMany({ status : "A" })
(Python版)db.inventory.delete_many({"status": "A"})

【只删除匹配条件的一个文档】

使用db.collection.deleteOne()方法,可按给定过滤条件只删除单个文档,即使有多个文档匹配过滤条件

下例删除inventory集合中第一个status为"D"的文档:

> db.inventory.deleteOne( { status: "D" } )
(Python版)db.inventory.delete_one({"status": "D"})

【删除行为】

> 索引
  即使删除集合中的所有文档,也不会把索引移除掉

> 原子性
  MongoDB的所有写操作在文档层面都是原子性的

> 写入确认
  可以为删除操作指定写入确认级别


== 删除方法 ==

MongoDB提供以下几种从集合中删除文档的方法:

db.collection.deleteOne()     删除单个文档,如果有多个文档匹配过滤条件,则只删除第一个文档
db.collection.deleteMany()    删除所有匹配过滤条件的文档
db.collection.remove()        同上


== 批量写操作 ==

【概述】

MongoDB为客户端提供执行批量写操作的能力,批量写入操作只对单个集合有效,MongoDB允许应用程序决定批量写操作的确认级别

db.collection.bulkWrite()方法提供执行批量写入、更新和删除操作的能力,MongoDB也支持通过db.collection.insertMany()方法完成批量写入

【有序和无序操作】

批量写操作可以是有序的,也可以是无序的

对一个有序列表的操作,MongoDB会串行执行这些操作,当执行过程中有错误发生时,MongoDB会中止执行后续操作,并立刻返回

对一个无序列表的操作,MongoDB会并行执行这些操作,当执行过程中有错误发生时,MongoDB会继续执行后续操作

有序列表的操作速度要慢于无序列表,因为每个操作都必须等待前一个操作完成才能执行

默认地,bulkWrite()执行有序操作,当指定 ordered:false 选项时,才为无序操作

【bulkWrite()方法】

bulkWrite()支持以下写入操作:insertOne、updateOne、updateMany、replaceOne、deleteOne和deleteMany

每个写入操作作为一个数组中的文档传递给bulkWrite(),例如,执行以下多个写入操作:

try {
   db.characters.bulkWrite(
      [
         { insertOne : { "document" : { "_id" : 4, "char" : "Dithras", "class" : "barbarian", "lvl" : 4 } } },
         { insertOne : { "document" : { "_id" : 5, "char" : "Taeln", "class" : "fighter", "lvl" : 3 } } },
         { updateOne : { "filter" : { "char" : "Eldon" }, "update" : { $set : { "status" : "Critical Injury" } } } },
         { deleteOne : { "filter" : { "char" : "Brisbane"} } },
         { replaceOne : { "filter" : { "char" : "Meldane" }, "replacement" : { "char" : "Tanys", "class" : "oracle", "lvl" : 4 } } }
      ]
   );
}
catch (e) {
   print(e);
}

该操作返回以下内容:

{
   "acknowledged" : true,
   "deletedCount" : 1,
   "insertedCount" : 2,
   "matchedCount" : 2,
   "upsertedCount" : 0,
   "insertedIds" : {
      "0" : 4,
      "1" : 5
   },
   "upsertedIds" : {
   }
}

【在分片集合上的批量写入策略】

大批量写入操作,包括初始数据写入或日常数据导入,对分片集群的性能有较大影响,需考虑以下策略:

> 预分裂集合
  如果分片集合是空的,那么集合只有一个初始块(chunk),其保存在单个分片上,MongoDB必须花费时间去接受数据、产生分裂和分布分裂块以达到有效的分片布局,为避免这些性能开销,可以预先分裂集合

> 对mongos运用无序写操作
  要提升分片集群上的写操作性能,使用带 ordered:false 选项的bulkWrite()方法,mongos会尝试并行发送写操作到多个分片上,对于空集合要先对其预分裂

> 避免单调抑制
  如果分片键随数据写入而单调递增,那么所有写入的数据都会汇聚在最新的块(chunk)上,使得总是在扩大单个分片,如此,集群的写入性能将被限制在单个分片的写入性能之下

  如果写入量大于单个分片的处理能力,或是无法避免单调递增的分片键,可以考虑以下调整方案:
  - 反转自增的分片键值,例如将123变成321,将124变成421了
  - 由应用程序计算出分片键的值,打破单调递增的规律


== SQL与MongoDB映射图 ==

【术语和概念】

下表列出一些SQL和MongoDB的术语和概念之间的对应关系

SQL        MongoDB
-----------    ---------------------------
database    database
table        collection
row        document
column        field
index        index
table joins    $lookup, embedded documents
primary key    _id
group by    aggregation

【举例说明】

下表列出多条SQL语句和对应的MongoDB语句,表中的例子假设以下条件:

- SQL语句表名为people
- MongoDB语句集合名为people,且包含如下文档:
  {
    _id: ObjectId("509a8fb2f3f4948bd2f983a0"),
    user_id: "abc123",
    age: 55,
    status: 'A'
  }

> 创建和调整结构

SQL:
  CREATE TABLE people ( id MEDIUMINT NOT NULL AUTO_INCREMENT, user_id Varchar(30), age Number, status char(1), PRIMARY KEY (id) )
MongoDB:
  集合people会在第一次执行insertOne()或insertMany()操作时自动创建,当然也可手动创建
  db.createCollection("people")
  db.people.insertOne( { user_id: "abc123", age: 55, status: "A" } )
  主键为_id,如未给出,也会自动增加

SQL:
  ALTER TABLE people ADD join_date DATETIME
MongoDB:
  集合对文档结构并无硬性限制,不用在集合层更改结构
  然而,在文档层,updateMany()操作可以用$set操作符为已存在的文档增加字段
  db.people.updateMany( { }, { $set: { join_date: new Date() } } )

SQL:
  ALTER TABLE people DROP COLUMN join_date
MongoDB:
  在文档层,updateMany()操作可以用$unset操作符为已存在的文档删除字段
  db.people.updateMany( { }, { $unset: { "join_date": "" } } )

SQL:    CREATE INDEX idx_user_id_asc ON people(user_id)
MongoDB:db.people.createIndex( { user_id: 1 } )

SQL:    CREATE INDEX idx_user_id_asc_age_desc ON people(user_id, age DESC)
MongoDB:db.people.createIndex( { user_id: 1, age: -1 } )

SQL:    DROP TABLE people
MongoDB:db.people.drop()

> 写入数据

SQL:    INSERT INTO people(user_id, age, status) VALUES ("bcd001", 45, "A")
MongoDB:db.people.insertOne({ user_id: "bcd001", age: 45, status: "A" })

> 更新数据

SQL:    UPDATE people SET status = "C" WHERE age > 25
MongoDB:db.people.updateMany( { age: { $gt: 25 } }, { $set: { status: "C" } } )

SQL:    UPDATE people SET age = age + 3 WHERE status = "A"
MongoDB:db.people.updateMany( { status: "A" } , { $inc: { age: 3 } } )

> 删除数据

SQL:    DELETE FROM people WHERE status = "D"
MongoDB:db.people.deleteMany( { status: "D" } )

SQL:    DELETE FROM people
MongoDB:db.people.deleteMany({})

> 查询数据

SQL:    SELECT * FROM people
MongoDB:db.people.find()

SQL:    SELECT id, user_id, status FROM people
MongoDB:db.people.find({ }, { user_id: 1, status: 1 })

SQL:    SELECT user_id, status FROM people
MongoDB:db.people.find({ }, { user_id: 1, status: 1, _id: 0 })

SQL:    SELECT * FROM people WHERE status = "A"
MongoDB:db.people.find({ status: "A" })

SQL:    SELECT user_id, status FROM people WHERE status = "A"
MongoDB:db.people.find({ status: "A" }, { user_id: 1, status: 1, _id: 0 })

SQL:    SELECT * FROM people WHERE status != "A"
MongoDB:db.people.find({ status: { $ne: "A" } })

SQL:    SELECT * FROM people WHERE status = "A" AND age = 50
MongoDB:db.people.find({ status: "A", age: 50 })

SQL:    SELECT * FROM people WHERE status = "A" OR age = 50
MongoDB:db.people.find({ $or: [ { status: "A" }, { age: 50 } ] })

SQL:    SELECT * FROM people WHERE age > 25
MongoDB:db.people.find({ age: { $gt: 25 } })

SQL:    SELECT * FROM people WHERE age < 25
MongoDB:db.people.find({ age: { $lt: 25 } })

SQL:    SELECT * FROM people WHERE age > 25 AND age <= 50
MongoDB:db.people.find({ age: { $gt: 25, $lte: 50 } })

SQL:    SELECT * FROM people WHERE user_id like "%bc%"
MongoDB:db.people.find( { user_id: /bc/ } )
         db.people.find( { user_id: { $regex: /bc/ } } )

SQL:    SELECT * FROM people WHERE user_id like "bc%"
MongoDB:db.people.find( { user_id: /^bc/ } )
         db.people.find( { user_id: { $regex: /^bc/ } } )

SQL:    SELECT * FROM people WHERE status = "A" ORDER BY user_id ASC
MongoDB:db.people.find( { status: "A" } ).sort( { user_id: 1 } )

SQL:    SELECT * FROM people WHERE status = "A" ORDER BY user_id DESC
MongoDB:db.people.find( { status: "A" } ).sort( { user_id: -1 } )

SQL:    SELECT COUNT(*) FROM people
MongoDB:db.people.count()
         db.people.find().count()

SQL:    SELECT COUNT(user_id) FROM people
MongoDB:db.people.count( { user_id: { $exists: true } } )
         db.people.find( { user_id: { $exists: true } } ).count()

SQL:    SELECT COUNT(*) FROM people WHERE age > 30
MongoDB:db.people.count( { age: { $gt: 30 } } )
         db.people.find( { age: { $gt: 30 } } ).count()

SQL:    SELECT DISTINCT(status) FROM people
MongoDB:db.people.distinct( "status" )

SQL:    SELECT * FROM people LIMIT 1
MongoDB:db.people.findOne()
         db.people.find().limit(1)

SQL:    SELECT * FROM people LIMIT 5 SKIP 10
MongoDB:db.people.find().limit(5).skip(10)

SQL:    EXPLAIN SELECT * FROM people WHERE status = "A"
MongoDB:db.people.find( { status: "A" } ).explain()


== 读取关注 ==

用于复制集和分片的查询选项readConcern,决定如何返回查询的数据

【关注级别】

下表列出可用的关注级别:

> "local"
  默认级别,查询返回当前实例上最新的数据,不能保证数据已被写入到复制集的多数成员里,某些数据可能会在发生切换后被回滚掉

> "majority"
  查询返回已经得到复制集中多数成员写入确认的最新数据,要使用该级别,必须做到:
  - 使用--enableMajorityReadConcern选项启动mongod实例,或在配置文件中增加 replication.enableMajorityReadConcern = true 参数
  - 复制集使用WiredTiger存储引擎,并且启用了 protocolVersion: 1 选项

> "linearizable"
  在"majority"的基础上还要求写入确认在读取操作发起之前

  在使用了 writeConcernMajorityJournalDefault = true 参数的复制集上,该级别返回的数据决不会被回滚,当 writeConcernMajorityJournalDefault = false 时,MongoDB将不会等待 w: "majority" 的写入确认,因此,"majority"写入操作可能在有复制集成员丢失的情况下被回滚

  只能在primary上执行该级别的读取操作,且只适用于读取操作指定的查询过滤条件能唯一标识某个文档的情况下

  总是配合使用maxTimeMS选项,以确保在复制集中多数数据承载成员不可用时,读取操作不会被无限期的阻塞,而是以报错返回

  例如:db.restaurants.find( { _id: 5 } ).readConcern("linearizable").maxTimeMS(10000)

  该级别即可用于MMAPv1,也可用于WiredTiger存储引擎

【存储引擎支持】

级别        WiredTiger    MMAPv1
"local"        Yes        Yes
"majority"    Yes        No
"linearizable"    Yes        Yes

serverStatus命令返回的storageEngine.supportsCommittedReads字段,表示当前存储引擎是否支持"majority"级别

> db.serverStatus().storageEngine
{ "name" : "wiredTiger", "supportsCommittedReads" : true }

【readConcern选项】

使用readConcern选项指定读取关注级别:

readConcern: { level: <"majority"|"local"|"linearizable"> }

以下操作可使用该选项:

- find()
- aggregate()
- distinct()
- count()
- parallelCollectionScan()
- geoNear()
- geoSearch()

在mongo shell中为db.collection.find()指定读取关注,需使用cursor.readConcern()方法

【注意事项】

> 读取自己写入的数据
  对于在Primary上使用了"majority"写入确认的写操作,使用"majority"或"linearizable"级别是肯定能读取到自己写入的数据的

> 实时执行顺序
  联合"majority"写入确认,"linearizable"级别能多线程读写某个文档,犹如实时的单线程执行这些读写操作,就是说,这些读写操作在调度上被视为串型的

> 性能比较
  不同于"majority","linearizable"级别确保读取数据的Secondary成员是回复了Primary上"majority"写入确认的成员,因此,"linearizable"级别的读取可能明显地慢于"majority"和"local"级别的读取

  在某些情况下,复制集中的二个成员都会短暂地认为自己是Primary,但多数情况下,只有其中一个能完成"majority"写入确认的写操作,能完成写操作的是当前的Primary,而另一个是尚未承认降级的前Primary,例如网络有延迟,当这种情况发生时,连接到前Primary的客户端可能读取到陈旧的数据,而写入的数据则会被回滚掉


== 写入确认 ==

写入确认是指对单台mongod或复制集或分片集群的写操作的确认级别,在分片集群上,mongos实例会传递写入确认级别到所有分片上

从MongoDB 2.6开始,新的写操作协议整合了写操作和写入确认,不再需要调用getLastError命令

【写入确认规范】

格式:
  { w: , j: , wtimeout: }

其中:
- w选项要求写操作传播到多少台mongod实例上,或传播到指定标签的mongod实例上
- j选项指明在将写操作记录到journal日志里时是否也记录写入级别(w:
- wtimeout选项限定写操作的用时,单位毫秒,防止被无限期的阻塞

> w选项
  有以下值可用:

  -
    要求写操作传播到多少台mongod实例上,例如:
    w:1 默认级别,写操作传播到单台mongod或复制集的primary上
    w:0 不要求写入确认,可能返回套接字异常或网络错误给应用程序
    如果同时指定了 w:0 和 j:true,等同于指定了 w:1
    大于1只能用于复制集,要求写操作传播到指定数量的成员上,包括Primary

  - "majority"
    在MongoDB 3.0之前,是要求写操作传播到复制集的多数节点上,包括Primary
    而从MongoDB 3.0开始,是要求写操作传播到复制集的多数投票节点上,包括Primary
    在w:"majority"级别写操作返回后,客户端可以使用"majority"关注级别读取这些数据

  -
    要求写操作传播到复制集中有指定标签的成员上

> j选项
  如果指定j:true,写入确认级别(即w: )会连同写操作一起被记录到journal日志里,并且在写入完成后立即刷新Journal日志,j:true本身并不能保证写操作不会因复制集的Primary切换而被回滚
  如果mongod未启用Journal功能,则指定j:true的写操作会以报错返回,当复制集使用 protocolVersion:1 选项,且启用了Journal功能,则w:"majority"级别可能自带j:true效果,具体行为由复制集的writeConcernMajorityJournalDefault参数决定

> wtimeout选项
  该选项只适用于w大于1的情况,在达到指定毫秒后以错误返回,即使要求的写入确认级别最终能够成功,当这些写操作返回时,MongoDB不能撤销完成的数据修改操作
  如果不指定wtimeout选项,并且未达到写入确认要求的成员数,该写操作将被无限期阻塞,指定wtimeout为0,等同于无wtimeout选项

【确认行为】

w选项和j选项决定mongod实例什么时候确认写操作

> 单机
  一台单机mongod确认写操作的时机,有可能是在内存中完成了写操作之后,也可能是在记录写操作到磁盘journal日志后,下表列出单台mongod的确认时机与w选项和j选项的对应关系:

            w:1               w:"majority"
  j:true    On-disk journal   On-disk journal
  j:false   In memory         In memory
  未指定j    In memory         On-disk journal(如果启用journal的话)

> 复制集
  复制集上的写入确认时机,既可能是在指定个数的成员在内存中完成了写操作之后,也可能是这些成员将写操作记录到磁盘journal日志之后,在发出写入确认的成员个数由 w: 指定,下表列出确认时机与w选项和j选项的对应关系:

            w:        w:"majority"   
  j:true    On-disk journal   On-disk journal
  j:false   In memory         In memory
  未指定j    In memory         由writeConcernMajorityJournalDefault决定,其为true,则On-disk journal,其为false,则In memory

  当writeConcernMajorityJournalDefault设置为false时,MongoDB并不会在发出写入确认之前等待 w:"majority" 级别的写操作落地,因此,"majority"级别的写操作可能在复制集成员损失时被回滚掉,即在In memory的情况下


== MongoDB CRUD概念 ==

该章节分为三部分:

> 原子性、一致性和分布式操作
  - 原子性和事务
  - 读取隔离、一致性和近因效应
  - 分布式查询
  - 分布式写操作
  - 执行二阶段提交
  - 通过findAndModify的线性读取

> 查询计划、性能和分析
  - 查询计划
  - 查询优化
  - 分析查询性能

> 其他
  - tail游标


== 原子性和事务 ==

在MongoDB里,写操作在单个文档层面是原子性的,即使操作修改了单个文档内的多个内嵌文档

当单个写操作修改多个文档时,每个文档的修改是原子性的,但整个操作不是,其他操作可能交叉进来,然而,可以使用$isolated操作符隔离每个作用于多个文档的写操作

【$isolated操作符】

使用$isolated操作符,作用于多个文档的写操作一旦开始修改文档就会阻止其他交叉操作,并且保证在没有完成操作或报错返回之前其他客户端是看不到这些修改的

一个隔离写操作不能提供“all-or-nothing”原子保障,就是说,如果写操作过程中发生了错误,是无法回滚该错误发生之前已经完成的修改的

$isolated操作符的原理是写操作获得集合上的排他锁,即使是WiredTiger这样的文档级锁存储引擎,$isolated操作符会使WiredTiger在写操作执行期间是单线程的

$isolated不能用于分片集群

【类事务语义】

由于单个文档可以包含多个内嵌文档,那么单文档原子性就有非常多的实际使用场景,对于必须作为单个事务处理的多个写操作,可以由应用程序实现二阶段提交

然而,二阶段提交只能提供类事务语义,使用其虽能确保数据一致性,但在二阶段提交或回滚期间,应用程序有可能返回“半成品”数据

【并发控制】

并发控制允许多个应用程序并发执行操作,而不会引起数据不一致或冲突

一个方法是在某个只有唯一值的字段上创建唯一索引,这可以阻止写入或更新操作产生重复数据,在多个字段上创建唯一索引可以强制这些字段值的组合唯一性

另一个方法是在写操作的查询条件中指定字段的预计当前值,二阶段提交模式提供一个可变量,写操作的查询条件即可以是预计数据,也可以是应用程序标识(application identifier)


== 读取隔离、一致性和近因效应 ==

【隔离保证】

> 未提交读
  在MongoDB里,客户端能看到写操作落地前的数据:
  - 不考虑写入确认级别,其他客户端使用"local"读取关注级别能看到还未回复写入确认给发起客户端的写操作的结果集
  - 客户端使用"local"读取关注级别可以读取到尚能被回滚的数据

  未提交读是默认地隔离级别,而且单机mongod实例、复制集和分片集群都可适用

> 未提交读和单文档原子性
  写操作对于单文档是原子性的,例如在更新文档中的多个字段时,约不会读取到被更新了部分字段的文档

  在单机mongod实例上,对某个文档的一系列读写操作是串型的,而在复制集上,只有在无回滚的情况下,对单个文档的读写操作才是串型

  然而,尽管不会读取到部分更新的文档,但未提交读意味着能读取到未落地的数据

> 未提交读和多文档写
  当单个写操作修改多个文档,每个文档的修改都是原子性的,但整个操作是非原子性的,并且可能被其他操作交叉,然而,可以用$isolated操作符隔离作用于多个文档的单个写操作

  没有隔离的多文档写操作,MongoDB呈现以下行为:
  1. 非时间点读操作,假设一个读操作在时间t1开始读取文档,一个写操作在随后的时间t2更新了还未读取的文档中的一个,读操作在之后的时间t3读取到的将是更新后的文档,因此读取的不是数据的时间点快照
  2. 非串型操作,没有机制保证先发起的读或写操作就一定会先于后发起的读或写操作完成处理
  3. 在读操作执行期间,可能会因文档被更新,而丢失了原先匹配查询条件的文档

> 游标快照
  MongoDB游标在某些情况下会多次返回同一个文档,因为当游标返回文档时,其他操作可能会与其交叉,某些更新操作会导致文档被移动,例如MMAPv1引擎下文档变大了,或修改了文档的某个索引字段值,而查询正好通过该索引读取数据,这都会导致游标多次返回同一文档

  一种方法是使用cursor.snapshot()方法保证查询对每个文档只返回一次,但是snapshot()不能保证返回的文档一定是查询发起时的模样,也不能提供对交叉执行的写入和删除操作的隔离,且snapshot()方法不能与sort()和hint()方法一起使用,也不能用于分片集合

  另一种方法是在集合上创建单字段或多字段组合的唯一索引,并使用hint()方法明确地强制查询使用该唯一索引

【单序写入】

MongoDB为单台mongod实例、复制集和分片集群提供单序写入保证

假设一个应用程序要执行一系列操作,其中包括写操作W1和排在后面的写操作W2,MongoDB保证W1操作先于W2执行

【实时顺序】

在Primary上的读和写操作,"linearizable"级别的读操作和"majority"级别的写操作能够在单个文档上多线程执行读和写,犹如单线程实时执行这些操作,因此这些读写操作的调度可认为是线性化的


== 分布式读操作 ==

【复制集上的读操作】

默认地,读取操作在复制集的primary上执行,然而,客户端能指定读取偏好将读操作定位到其他成员上,例如,客户端能配置读取偏好为从secondary或从最近的成员上读取,还有其他益处:

- 避免访问异地数据中心的网络延迟
- 将读取分布到多台secondary上以提升性能
- 在primary选举期间仍可读取
- 用于备份操作

但在secondary上读到的数据可能已不是primary上的最新数据了,读取偏好会将读操作分配到不同的secondary上,这也可能导致非单序读取

可以基于每次连接或每次操作配置读取偏好

【分片集群上的读操作】

分片集群能够实现在一个由多台mongod实例组成的集群里分区存储数据,且对应用程序几乎是透明的,只需将操作发送给任一一个关联到分片集群的mongos实例即可

分片集群上的读操作在直接定向到指定分片时是非常高效的,分片集合上的查询应该包含该集合的分片键,这样mongos就能根据存储在配置库中集群元数据将查询路由到指定分片上

如果查询不包含分片键,由于无法进行路由操作,mongos必须将查询传递到集群中的所有分片上,各自完成执行后再汇集结果数据,这种“分散聚集”查询的性能是极差的


== 分布式写操作 ==

【复制集上的写操作】

在复制集中,所有的写操作都是在primary上,primary执行写操作并将它们记录到oplog里,oplog是一个可循环重用的集合,secondary持续不断地同步oplog,并且异步执行里面的写操作

【分片集群上的写操作】

对于分片集群里的分片集合,mongos将应用程序发起的写操作传递给负责该数据集的分片上,原理是,mongos参照从配置库中得到的集群元数据,将写操作路由到对应的分片

MongoDB是基于分片键的值,按范围划分集合数据为多个分区,每个分区称为数据块(chunk),再将这些数据块分布到所有的分片上,这样能提升集群上的写操作性能

对单个文档的更新操作必须以分片键或_id字段为查询条件,更新多个文档时如果使用分片键将更高效,否则要广播到所有的分片上

如果分片键的值随每次数据写入而递增或递减,所有写入的数据都会集中在某个分片上,这使得整个集群的处理能力被限制在单个分片上


== 执行二阶段提交 ==

【摘要】

下文提供多文档更新和使用二阶段提交方法写数据到多个文档的“多文档事务”的样例,并扩展该处理以提供“类回滚”功能

【背景】

MongoDB中单个文档上的操作总是原子性的,然而,涉及多个文档的操作是非原子的,由于MongoDB的文档还是较为复杂的,特别是包含多个内嵌文档时,所以在很多实际使用场景中支持单文档的原子性是非常有必要的

尽管单文档原子操作很有用,但仍有需要多文档事务的情况,当执行一个由一系列操作组成的事务时,某些问题就会出现,例如:

- 原子性,在一个事务里,只要有一个操作失败,那么之前的操作都必须回滚掉,即"all or nothing"中的"nothing"
- 一致性,如果有重大故障(如网络或硬件)中断了事务,数据库必须能恢复并保证数据的一致性

对于需要多文档事务的情况,可以在应用程序中实现二阶段提交以支持类似多文档更新的操作,使用二阶段提交可以确保数据的一致性,当出现错误时,能够恢复到事务执行前的状态,在事务执行过程中,文档呈现为待定数据和状态

由于MongoDB只提供单文档操作的原子性,二阶段提交只能提供“类事务”语义,在二阶段提交或回滚期间,应用程序有可能返回的是中间过程的“半成品”数据

【模型】

> 概述
  假设要从账户A转钱到账户B,对于关系型数据库,在一个多语句事务中对A扣钱对B加钱即可,而在MongoDB里,需要模仿一个二阶段提交才能达到相同结果

  之后的例子使用以下二个集合:
  - 集合accounts,存储账户数据
  - 集合transactions,存储转账事务的数据

> 初始化账户数据
  在集合accounts中写入账户A和账户B的数据:

  db.accounts.insert( [ { _id: "A", balance: 1000, pendingTransactions: [] }, { _id: "B", balance: 1000, pendingTransactions: [] } ] )

  返回一个包含写入状态的BulkWriteResult()对象,如果写入成功,nInserted值应该为2

> 初始化转账记录
  每笔待执行的转账操作都写入transactions集合,文档包含以下字段:

  - source和destination字段,其值引用accounts集合中的_id字段
  - value字段,指定从source转账到destination的金额
  - state字段,用于反应事务的当前状态,有initial、pending、applied、done、canceling和canceled六种值
  - lastModified字段,用于表示该记录的最新修改时间

  初始化事务,写入transactions集合,从账户A转账100元到账户B,事务状态为"initial",最新修改时间为当前时间:

  db.transactions.insert( { _id: 1, source: "A", destination: "B", value: 100, state: "initial", lastModified: new Date() } )

  返回一个含有写入状态的WriteResult()对象,如果写入成功,nInserted值应该为1

> 使用二阶段提交执行事务

  1. 获得要启动的事务
     从transactions集合中得到状态为initial的事务,当前只有一个

     var t = db.transactions.findOne( { state: "initial" } ).limit(1)

  2. 更新事务状态为pending
     将该事务状态从"initial"更改为"pending",并用$currentDate操作符更新lastModified字段为当前时间

     db.transactions.update( { _id: t._id, state: "initial" }, { $set: { state: "pending" }, $currentDate: { lastModified: true } } )

     返回一个含有更新状态的WriteResult()对象,如果更新成功,nMatched和nModified值都应该为1

     在更新语句里,查询文档{ _id: t._id, state: "initial" }用于保证被更新的是之前取出的事务,且没有其他程序在处理该事务,如果nMatched和nModified值为0,回退到第一步取剩余的initial事务

  3. 调整两个账户余额
     使用update()方法调整两个账户余额,同时更新balance和pendingTransactions字段,在更新条件中包含 pendingTransactions: { $ne: t._id } 是为了避免因某些特殊原因而重复执行同一个事务

     更新账户A,在balance字段上减去指定金额(t.value),并在pendingTransactions字段里增加当前事务ID(t._id)

     db.accounts.update( { _id: t.source, pendingTransactions: { $ne: t._id } }, { $inc: { balance: -t.value }, $push: { pendingTransactions: t._id } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值均为1

     更新账户B,在balance字段上增加指定金额(t.value),并在pendingTransactions字段里增加当前事务ID(t._id)

     db.accounts.update( { _id: t.destination, pendingTransactions: { $ne: t._id } }, { $inc: { balance: t.value }, $push: { pendingTransactions: t._id } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值均为1

  4. 更新事务状态为applied
     使用以下update()语句更新事务状态为"applied",lastModified字段为最新时间

     db.transactions.update( { _id: t._id, state: "pending" }, { $set: { state: "applied" }, $currentDate: { lastModified: true } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值都为1

  5. 更新两个账户的pendingTransactions字段
     从两个账户的pendingTransactions字段中移除已经执行的事务ID

     db.accounts.update( { _id: t.source, pendingTransactions: t._id }, { $pull: { pendingTransactions: t._id } } )
     db.accounts.update( { _id: t.destination, pendingTransactions: t._id }, { $pull: { pendingTransactions: t._id } } )

     如果更新成功,上述两条update()语句返回的WriteResult()对象的nMatched和nModified的值都应该为1

  6. 更新事务状态为done
     对于完成的事务,将事务的state设置为"done",同时更新lastModified字段:

     db.transactions.update( { _id: t._id, state: "applied" }, { $set: { state: "done" }, $currentDate: { lastModified: true } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值都为1

【事务失败的恢复】

事务处理最重要的部分是,当事务不能成功完成时,从各种失败场景恢复的可能性

> 恢复操作
  二阶段提交模型允许应用程序反向执行事务,以恢复到事务执行前的一致性状态,达到一致性状态所需的时间取决于应用程序需要多长时间恢复一个事务

  下例恢复过程中,使用lastModified字段作为未完成事务是否需要恢复的指标,当"pending"或"applied"状态的事务没能在30分钟内有所更新,即判定这些事务需要被恢复

> pending状态的事务
  从transactions集合取回lastModified早于30分钟前,且state为"pending"的事务:

  var dateThreshold = new Date();
  dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
  var t = db.transactions.findOne( { state: "pending", lastModified: { $lt: dateThreshold } } );

  从上述的第3步“调整两个账户余额”开始反向执行

> applied状态的事务
  从transactions集合取回lastModified早于30分钟前,且state为"applied"的事务:

  var dateThreshold = new Date();
  dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
  var t = db.transactions.findOne( { state: "applied", lastModified: { $lt: dateThreshold } } );

  从上述的第5步“更新两个账户的pendingTransactions字段”开始反向执行

> 回滚操作
  某些情况下,你可能需要回滚或撤销一个事务,例如在事务执行过程中,应用程序发起了“取消”操作,或某个账号被冻结

  已是"applied"状态的事务不需要回滚,而是继续完成后续步骤,然后创建一上新事务,执行与当前事务相反的操作即可

  当前是"pending"状态的事务,使用以下步骤进行回滚:

  1. 更新事务状态为canceling
     将事务状态从"pending"更改为"canceling":

     db.transactions.update( { _id: t._id, state: "pending" }, { $set: { state: "canceling" }, $currentDate: { lastModified: true } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值都为1

  2. 撤消两个账号上的事务操作
     如果事务已经执行,就要反向执行一次以撤消该事务,在更新条件中包含 pendingTransactions: t._id 是为了只更新已经执行了事务的账户

     更新账户B,从其balance减去事务的value值,并从pendingTransactions中移除事务ID:

     db.accounts.update( { _id: t.destination, pendingTransactions: t._id }, { $inc: { balance: -t.value }, $pull: { pendingTransactions: t._id } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值都为1,如果账户B之前未执行过该事务的相关操作,就不会有匹配更新条件的文档,则nMatched和nModified的值都为0

     更新账户A,对其balance加上事务的value值,并从pendingTransactions中移除事务ID:

     db.accounts.update( { _id: t.source, pendingTransactions: t._id }, { $inc: { balance: t.value}, $pull: { pendingTransactions: t._id } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值都为1,如果账户A之前未执行过该事务的相关操作,就不会有匹配更新条件的文档,则nMatched和nModified的值都为0

  3. 更新事务状态为canceled
     将事务状态从"canceling"更新为"cancelled",以完成回滚

     db.transactions.update( { _id: t._id, state: "canceling" }, { $set: { state: "cancelled" }, $currentDate: { lastModified: true } } )

     如果更新成功,返回的WriteResult()对象的nMatched和nModified的值均为1

【多个事务处理程序】

即使有多个应用程序并发的创建和执行事务操作,也不应该导致数据不一致或冲突,在之前的处理过程中,更新或读取事务数据时,都在查询条件中包含了state字段,以防止多个应用程序重复执行同一个事务

例如,App1和App2读取到了同一个处于"initial"状态的事务,App1先于App2开始执行事务,当App2试图执行第2步“更新事务状态为pending”时,包含 state: "initial" 限制的更新条件将匹配不到任何文档,即nMatched和nModified为0,此时App2应该返回到第一步重新获取"initial"状态的事务

当多个应用程序运行时,关键的是在任何时刻都要保证一个事务只被一个应用程序获得,例如,在查询或更新条件中包含预期的state值,还可以在事务文档中创建一个标识,记录哪个应用程序获得了该事务,使用findAndModify()方法可一步完成事务的修改和取回

t = db.transactions.findAndModify( {
      query: { state: "initial", application: { $exists: false } },
      update: { $set: { state: "pending", application: "App1" }, $currentDate: { lastModified: true } },
      new: true
    } )

之后更新事务数据的操作都包含state和application字段,以保证只有匹配该标识的应用程序才可以操作该事务

如果App1在执行事务的过程中发生失败,也应该由App1执行恢复过程,例如获得待恢复的"pending"事务:

var dateThreshold = new Date();
dateThreshold.setMinutes(dateThreshold.getMinutes() - 30);
db.transactions.find( { application: "App1", state: "pending", lastModified: { $lt: dateThreshold } } )

【在生产环境使用二阶段提交】

上述介绍的二阶段提交模型较为简单,假设总是能执行回滚操作,且账户余额充足,生产环境的实现要复杂得多了

对于所有事务,确保使用了合适的写入确认等级


== linearizable级别的findAndModify()方法 ==

【概述】

当从复制集读取时,可能是陈旧的(尚未执行完读取操作发起前的所有写操作)或未持久化的(写操作未被多数成员确认处于尚可回滚的状态),这会因使用的读取关注级别的不同而不同

从MongoDB 3.4开始,引入了"linearizable"读取关注级别,其能保证返回的数据是最新的且是持久化的,该级别只能用于查询过滤条件可以唯一确定某个文档的情况

【linearizable级别的findAndModify()方法】

使用 db.collection.findAndModify() 读取数据是最新的且不会被回滚的,使用带写入确认的findAndModify()方法修改一个文档中“隐藏”的字段,如下:

- db.collection.findAndModify() 的查询条件可以使用到唯一索引定位数据
- findAndModify() 必须真实的修改了文档,不仅匹配到文档,而且还修改了文档
- findAndModify() 必须使用 { w: "majority" } 写入级别

"linearizable"级别的开销要比"majority"大很多,因为会导致写延迟,该技术应该只用在决不能容忍陈旧数据的场景

【环境准备】

初始化products集合:

db.products.insert( [
  { _id: 1, sku: "xyz123", description: "hats", available: [ { quantity: 25, size: "S" }, { quantity: 50, size: "M" } ], _dummy_field: 0 },
  { _id: 2, sku: "abc123", description: "socks", available: [ { quantity: 10, size: "L" } ], _dummy_field: 0 },
  { _id: 3, sku: "ijk123", description: "t-shirts", available: [ { quantity: 30, size: "M" }, { quantity: 5, size: "L" } ], _dummy_field: 0 }
] )

集合中的文档包含名为_dummy_field的隐藏字段,该字段值会被 db.collection.findAndModify() 递增,如果该字段不存在,findAndModify()操作会增加该字段到文档,该字段的目的是确保findAndModify()操作对文档有所修改

【操作步骤】

1. 创建唯一索引
   在 db.collection.findAndModify() 操作的查询条件的字段上创建唯一索引

   该例使用sku字段作为查询条件,所以在该字段上创建唯一索引:

   db.products.createIndex( { sku: 1 }, { unique: true } )

2. 使用findAndModify()方法读取已提交数据
   使用 db.collection.findAndModify() 方法更新并获得文档,需要使用 { w: "majority" } 写入确认,且必须使用唯一索引字段作为查询匹配条件

   以下findAndModify()操作在唯一索引字段sku上指定了匹配条件,并递增了匹配到的文档的_dummy_field字段值,在指定 w: "majority" 时还包含了 wtimeout: 5000,防止写操作由于不能传播到多数投票成员而被无限堵塞

   var updatedDocument = db.products.findAndModify( {
     query: { sku: "abc123" },
     update: { $inc: { _dummy_field: 1 } },
     new: true,
     writeConcern: { w: "majority", wtimeout: 5000 }
   } );

   由于该 findAndModify() 操作只是更新文档中一个无关紧要的隐藏字段,所以可以安全的重复执行

   在某些网络故障的情况下,只有两个节点的复制集可能短暂地都认为自己是Primary,但是只有其中一个能完成 { w: "majority" } 写入确认级别的写操作,该节点就是真正的当前Primary,而另一个节点是尚未公认的“旧”Primary,此时,连接在旧Primary上的客户端,即使使用primary读取关注级别,也可能读取到陈旧的数据,而在旧Primary上的最新写操作,最终也会被回滚掉

   同样,本例中带"majority"的findAndModify()操作也只有连接到正确的Primary上才能操作成功


== 查询计划 ==

MongoDB查询优化器审核查询,并为有可用索引的查询选择最有效的查询计划,在每次执行查询时都使用该查询计划

查询优化器只缓存那些可用查询模型超过一个的查询计划,对于每个查询,查询计划器按照适合的查询模型搜索查询计划缓存,如果没有匹配的查询计划,计划器会生成多个候选计划,并在试用期对它们进行评估,最终选择出效果最好的计划创建模型缓存,之后就会直接使用该查询模型了

如果有匹配的查询计划,查询计划器基于模型创建查询计划,并且通过replanning机制评估它的执行情况,该机制根据计划执行的pass或fail情况,决定是保留还是驱逐该查询模型,如果驱逐,查询计划器会使用上述方法选出一个新的查询计划并缓存

可以使用explain()方法查看查询计划的统计信息,该信息可以帮助创建合适的索引,从MongoDB 2.6开始,explain()操作不再读写查询计划缓存

【计划缓存刷新】

在mongod关闭或重启时,计划缓存并不会持久化,类似移除索引或集合的操作会刷新计划缓存

从MongoDB 2.6开始,提供一系列方法用于查看和修改查询计划缓存,PlanCache.clear()方法可以清除整个计划缓存,也可使用PlanCache.clearPlansByQuery()方法清除指定计划缓存

【索引过滤器】

从2.6版本开始,索引过滤器决定哪个索引被优化器用于查询模型评估

一个查询模型是由查询、排序和聚合投影组成的,如果对于某个查询模型存在索引过滤器,那么优化器只考虑过滤器中指定的那些索引,当某个查询模型存在索引过滤器时,MongoDB会忽略hint()方法,要确认MongoDB是否为查询模型应用了索引过滤器,检查explain()方法的indexFilterSet字段即可

索引过滤器只影响优化器评估的索引,即使给予了查询模型,优化器可能仍会选择集合扫描作为最终的计划

索引过滤器只存在于mongod运行期间,当关闭mongod时并不会被持久化,MongoDB也提供相关命令来手动移除过滤器

由于索引过滤器会覆盖优化器的预设行为,例如忽略hint()方法,所以索引过滤器要谨慎使用


== 查询优化 ==

索引因能减少查询操作需要处理的数据量而得以提升读操作的性能

【创建一个索引以支持读操作】

如果应用程序使用某个或某些字段查询集合,那么该字段上的索引或这些字段上的组合索引能避免要扫描整个集合来寻找和返回查询结果集

例如,一个应用程序使用type字段查询inventory集合:

> db.inventory.find( { type: } );

为提升该查询性能,为inventory集合增加一个type字段上的升序或降序索引,在mongo shell里,可以使用 db.collection.createIndex() 方法创建索引

> db.inventory.createIndex( { type: 1 } )

【查询选择性】

查询选择性是指查询排除或过滤集合中文档的优劣程序,并能决定查询是否能有效地使用索引或是否使用索引

选择性越高,匹配到的文档范围就越小,例如,唯一字段_id上的等值匹配有非常高的选择性,它总是只匹配一个文档

选择性越低,匹配到的文档个数就越多,过小的选择性查询不能有效使用索引,甚至是用不上索引,例如,非等值操作符$nin和$ne是选择性非常差的,因为它们通常匹配索引的绝大部分,因此,在一些情况下,使用索引的$nin或$ne查询的性能并不比扫描整个集合的效果好

【覆盖查询】

覆盖查询是指使用索引即可满足查询过滤和字段返回,不需要在集合上检索任何文档,一个覆盖查询的索引必须满足以下二个要求:

- 查询条件里的字段都是索引的一个部分
- 需要返回的字段也在该索引里都有

例如,inventory集合在type和item字段上有以下索引:

db.inventory.createIndex( { type: 1, item: 1 } )

该索引可以覆盖如下操作,在type和item字段上查询,只返回item字段:

db.inventory.find( { type: "food", item:/^c/ }, { item: 1, _id: 0 } )

必须显式排除_id字段,因为该字段会默认返回,而它又不在索引中

> 性能
  由于索引包含所有查询和返回需要的字段,MongoDB即可用索引匹配查询条件,又能通过索引返回结果集

  覆盖查询要比需要检索索引外文档的查询快很多,因为索引键通常少于文档包含的字段,且索引通常位于RAM中,或顺序存储在磁盘上

> 限制
  - 索引字段的限制
    以下情况,索引将不能覆盖查询:
    - 索引中有字段是数组类型,如果一个索引字段是数组类型,该索引即为多键索引,其不支持覆盖查询
    - 查询条件或要求返回的字段中有内嵌文档

  - 分片集合的限制
    如果索引不包含分片键,则该索引不能覆盖分片集合的查询,但只使用_id索引的查询除外,即只用_id查询并且只返回_id字段,这是分片集合上的特殊覆盖查询

> explain
  要确认查询是否为覆盖查询,查看explain()方法的输出是否有一个IXSCAN段而不是FETCH段,并且executionStats.totalDocsExamined字段值为0

== 优化查询性能 ==

【为查询创建索引】

为查询创建普通索引或组合索引,扫描索引要比扫描整个集合快很多,因为通常索引的字段比文档少,且索引数据有序存储

例如,有一个存储博客贴子的posts集合,经常有匹配author_name字段的查询,要优化这类查询在author_name字段上创建索引即可:

db.posts.createIndex( { author_name : 1 } )

索引也能提升排序查询的性能,例如,经常有按timestamp字段排序的查询,可在timestamp字段上创建索引以进行优化:

db.posts.createIndex( { timestamp : 1 } )

该索引可以同时满足timestamp字段上正序和倒序两种排序查询,因为MongoDB对单键索引即可正序读取也可倒序读取

【限制查询结果数量以减少网络需求】

MongoDB游标以分组方式返回结果集文档,如果已知要获得的文档个数,可以使用limit()方法减少网络开销

如下,是典型的排序查询,如果只需要最先的10个贴子,可使用以下命令:

db.posts.find().sort( { timestamp : -1 } ).limit(10)

【只返回需要的字段】

当只需要返回文档中的部分字段时,你能指定这些只返回的字段,这样做性能要好于返回整个文档:

例如,只需要posts集合中的timestamp、title、author和abstract字段,可以发起以下命令:

db.posts.find( {}, { timestamp : 1 , title : 1 , author : 1 , abstract : 1} ).sort( { timestamp : -1 } )

【使用$hint指定索引】

多数情况下,查询优化器会选择最佳索引,然而可以使用hint()方法强制MongoDB选择某个索引

【使用递增操作符执行服务器端操作】

使用MongoDB的$inc操作符来递增或递减文档中字段的值,该操作是在服务器端,而不是在客户端获取文档后做简单的修改,再将整个文档写回服务端,$inc操作符还能避免竞争情况,简化有多个应用程序同时修改同一文档的处理过程


== 写操作性能 ==

【索引】

集合上的每一个索引都给写操作带来了一定量的开销,集合上的每一个写入和删除操作,MongoDB都要对该集合里每一个索引的相关文档键做写或删操作,一个更新操作会更新集合里那些包含更新字段的索引

对于使用MMAPv1存储引擎的mongod实例,更新操作可能导致一个文档的生长量超过它的已分配空间,当出现这种情况时,MMAPv1引擎移动该文档到一个新的物理位置,这也要更新集合上的每个索引去指向该文档的新位置,所以,移动操作是开销很大的

通常,索引对读操作性能提升的益处要大于写操作性能损失的弊端,但是,也要尽可能的优化写操作性能,当创建新索引时要谨慎,并确认已有索引都是查询需要用的

【文档生长和MMAPv1存储引擎】

一些更新操作会增长文档的尺寸,例如为文档增加了新字段

对于MMAPv1存储引擎,如果一个更新操作导致文档超过当前已分配的记录大小,MongoDB会将新文档重定位到有连续空间的能足够容纳下它的磁盘位置,需要重定位的更新操作要比普通更新操作花更多的时间,尤其当集合中还有包含更新字段的索引时,此时,MongoDB还必须更新所有这些索引,同理,每一个删除操作,则需要处理集合上的所有索引,对写吞吐量有较大影响

从MongoDB 3.0开始,MMAPv1存储引擎默认使用“2幂大小分配法”为文档的生长预留空间,“2幂大小分配法”确保MongoDB分配的文档空间都是2的幂数倍,这使得MongoDB能有效的重用已删除文档的空闲空间,以减少“回收再分配”的次数

【存储性能】

> 硬件
  存储系统的能力对MongoDB写入性能有重要的物理限制,与驱动器有关的独特因素影响着写入性能,包括随机访问模式、磁盘缓存、磁盘预读和RAID卡配置

  固态硬盘(SSD)在随机读写能力上,要比机械硬盘(HDD)提升100倍甚至更多

> 日志(Journal)
  为了能在发生宕机时保证数据不丢,MongoDB使用落地到磁盘的预写日志,先将数据写入内存,再间隔的刷新到Journal文件,如果MongoDB在把新数据刷新到数据文件之前发生了崩溃或重大错误,可以使用journal日志直接对数据文件执行丢失的写操作

  由于Journal日志提供的持久性保证会给写操作带来额外的性能开销,需要考虑以下因素:
  - 如果Journal日志和数据文件存放在同一磁盘上,两者可能会争抢有限的IO处理能力,将它们分布在不同的磁盘上,可以提升整体写操作性能
  - 如果应用程序指定的写入确认级别包含j选项,mongod会缩短Journal日志写入的时间间隔,这会使写入负载变大
  - Journal日志刷新间隔可使用commitIntervalMs参数配置,减少该间隔时间会增加Journal刷新次数,导致MongoDB的写入性能受限,增大该间隔时间可减少Journal刷新次数,但会增大MongoDB崩溃时Journal未记录的写操作数量


== explain结果说明 ==

MongoDB提供explain()方法来返回查询计划和执行统计信息

explain结果中有多种阶段,关键字如下:
- COLLSCAN 意为 扫描集合
- IXSCAN 意为 扫描索引
- FETCH 意为 取回文档
- SHARD_MERGE 意为 从分片融合结果集

【explain输出】

以下列出一些explain操作返回的关键字段

> queryPlanner
  该段包含被查询优化器选中的计划的详细信息,其中:
  - explain.queryPlanner.namespace
    查询执行的命名空间 .
  - explain.queryPlanner.indexFilterSet
    是否基于查询模型应用了索引过滤器
  - explain.queryPlanner.winningPlan
    该文档包含被选中查询计划的详细内容
  - explain.queryPlanner.winningPlan.stage
    该stage的名称,每个stage都只包含各自的信息,当有子stage时,父stage的内容将极为简单
  - explain.queryPlanner.winningPlan.inputStage
    当stage只有一个子stage时,该字段出现,通常是只有一个查询条件,详细描述查询计划,例如,IXSCAN包含索引边界以及索引扫描的其他数据
  - explain.queryPlanner.winningPlan.inputStages
    子stage数组,当有多个子stage时该字段出现,通常是有多个查询条件,详细描述各个查询计划
  - explain.queryPlanner.rejectedPlans
    被查询优化器否定的候选计划数组,如果无其他可选计划,该数组为空

> executionStats
  该段包含被选中计划的详细执行信息,只在executionStats或allPlansExecution模式下运行explain()才有该段
  - explain.executionStats
    包含用选中的计划执行完查询后得到的统计信息
  - explain.executionStats.nReturned
    匹配查询条件的文档个数,对应旧版本的n字段
  - explain.executionStats.executionTimeMillis
    查询选取计划和执行完成的总用时,单位毫秒,对应旧版本的millis字段
  - explain.executionStats.totalKeysExamined
    索引扫描次数,对应旧版本的nscanned字段
  - explain.executionStats.totalDocsExamined
    文档扫描次数,对应旧版本的nscannedObjects字段
  - explain.executionStats.executionStages
    执行情况的详细信息,根据情况下有inputStage或inputStages段
  - explain.executionStats.executionStages.works
    查询执行的工作被拆分成多少个单元,每个单元可由以下动作组成:扫描索引、从集合获取文档、在文档上应用投影,或记录一小段内部日志
  - explain.executionStats.executionStages.advanced
    返回的中间结果集数量
  - explain.executionStats.executionStages.needTime
    不返回中间结果集的工作周期数,例如,索引扫描阶段可能要花费一个工作周期寻找索引中的新位置,而不是返回索引值,所以该工作周期应该算在needTime里,而不是advanced里
  - explain.executionStats.executionStages.needYield
    查询系统上锁的存储层请求次数
  - explain.executionStats.executionStages.isEOF
    执行是否达到流结束,值为true或1,即达到,值为false或0,表示可能仍有结果返回,例如,假设一个带有LIMIT段的IXSCAN型查询,如果返回的结果多于LIMIT的要求,则LIMIT段的isEOF值为1,但上层的IXSCAN段的isEOF值会为0
  - explain.executionStats.executionStages.inputStage.keysExamined
    对于要扫描索引的查询执行段(如IXSCAN),该字段表示在索引扫描过程中被处理的in-bounds和out-of-bounds索引键总数,如果只扫描单个范围连续的索引键,那么只有in-bounds处理量,如果要扫描多个连续段,则会有out-of-bounds处理量,即从上一个范围结尾到下一个范围开始的索引键
    如下例,字段x上有索引,x值从1到100包含了100个文档,执行以下查询:
    db.keys.find( { x : { $in : [ 3, 4, 50, 74, 75, 90 ] } } ).explain( "executionStats" )
    该查询将扫描索引键3和4,当扫描到5时,发现该键不在查询条件范围内(即out-of-bounds),于是开始跳过,直到索引键50,执行到查询完成,扫描了3、4、5、50、51、74、75、76、90和91共十个索引键,所以keysExamined值为10,其中5、51、76和91是范围外的
  - explain.executionStats.executionStages.inputStage.docsExamined
    在查询执行阶段,被扫描的文档个数
  - explain.executionStats.allPlansExecution
    包含在查询计划选取阶段对所有候选计划的部分执行信息,该字段只在explain为allPlansExecution详细模式下才出现

> serverInfo
  返回该MongoDB实例的host、port和version等

【分片集合】

对于分片集合,explain返回核心查询计划,并在shards字段中包含每个被访问分片的服务器信息

- explain.queryPlanner.winningPlan.shards
  包含每个被访问分片的queryPlanner和serverInfo的文档数组
- explain.executionStats.executionStages.shards
  包含每个被访问分片的executionStats信息的文档数组

【兼容性变化】

从MongoDB 3.0开始,explain结果的格式和字段与之前版本有所不同,主要不同点如下:

> 集合扫描和索引使用
  如果查询计划选择扫描集合,则explain结果包含一个COLLSCAN阶段
  如果查询计划选择使用索引,则explain结果包含一个IXSCAN阶段,该阶段包含诸如索引键范式、执行发展方向和索引界限

  而在之前版本的MongoDB里,cursor.explain()返回的cursor字段包含如下值:
  - BasicCursor 为扫描集合
  - BtreeCursor [] 为使用索引

> 覆盖查询
  当一个索引覆盖整个查询,MongoDB能用同一个索引匹配查询条件和返回结果集,不需要从集合中检索文档
  当为覆盖查询时,explain结果有IXSCAN阶段,但没有FETCH阶段,且executionStats.totalDocsExamined值为0

  在之前版本的MongoDB里,cursor.explain()返回indexOnly字段来表示覆盖查询

> 索引交集
  对于索引交集,会有AND_SORTED或AND_HASH阶段,并在inputStages数组中列出这些索引
  在之前的版本中,cursor.explain()返回clauses数组列出这些索引

> 排序阶段
  如果MongoDB能使用索引扫描得到要求的排序顺序,结果集将不包含SORT阶段,否则,explain结果会有该段
  在之前的版本中,cursor.explain()返回scanAndOrder字段指示MongoDB是否能通过索引来完成排序


== 分析查询性能 ==

explain("executionStats")方法返回关于该查询性能的统计信息,这在评估查询是否使用和如何使用索引很有帮助

【评估查询性能】

假设集合inventory有_id、item、type和quantity字段和10条数据

> 无索引查询
  以下查询返回quantity值大等于100小等于200的文档,使用explain("executionStats")方法查看执行计划:

  db.inventory.find({quantity:{$gte:100,$lte:200}}).explain("executionStats")

  返回如下:

{
   "queryPlanner" : {
         "plannerVersion" : 1,
         ...
         "winningPlan" : {
            "stage" : "COLLSCAN",  -- 该值表示执行方式为集合扫描
            ...
         }
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 3,             -- 该值表示查询返回了3个文档
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 0,
      "totalDocsExamined" : 10,    -- 该值表示扫描了10个文档(集合里的全部文档)以找到匹配要求的3个文档
      "executionStages" : {
         "stage" : "COLLSCAN",
         ...
      },
      ...
   },
   ...
}

> 有索引查询
  在quantity字段上创建索引:

  db.inventory.createIndex( { quantity: 1 } )

  再次执行上面的explain命令,得到结果如下:

{
   "queryPlanner" : {
         "plannerVersion" : 1,
         ...
         "winningPlan" : {
               "stage" : "FETCH",
               "inputStage" : {
                  "stage" : "IXSCAN",  -- 该值表示使用到索引
                  "keyPattern" : {
                     "quantity" : 1
                  },
                  ...
               }
         },
         "rejectedPlans" : [ ]
   },
   "executionStats" : {
         "executionSuccess" : true,
         "nReturned" : 3,            -- 该值表示查询返回了3个文档
         "executionTimeMillis" : 0,
         "totalKeysExamined" : 3,    -- 该值表示扫描了3个索引键
         "totalDocsExamined" : 3,    -- 该值表示扫描了3个文档
         "executionStages" : {
            ...
         },
         ...
   },
   ...
}

  当使用上索引时,查询扫描了3个索引键,得到3个匹配的文档并返回,如果没有索引,查询必须扫描整个集合

【索引间比较性能】

使用hint()方法可指定查询使用指定的索引,再结合explain()方法,可对比不同索引下的查询性能

创建以下两个复合索引,第一个索引quantity字段在前type字段在后,第二个索引两字段次序相反:

db.inventory.createIndex( { quantity: 1, type: 1 } )
db.inventory.createIndex( { type: 1, quantity: 1 } )

用第一个索引执行查询:

db.inventory.find({ quantity:{ $gte:100, $lte:300 }, type:"food" }).hint({ quantity:1, type:1 }).explain("executionStats")

返回如下:

{
   "queryPlanner" : {
      ...
      "winningPlan" : {
         "stage" : "FETCH",
         "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
               "quantity" : 1,
               "type" : 1
            },
            ...
            }
         }
      },
      "rejectedPlans" : [ ]
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 2,            -- 返回了2个文档
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 5,    -- 扫描5个索引键
      "totalDocsExamined" : 2,    -- 匹配到2个文档
      "executionStages" : {
      ...
      }
   },
   ...
}

以第二个索引执行查询:

db.inventory.find({ quantity:{ $gte:100, $lte:300 }, type:"food" }).hint({ type:1, quantity:1 }).explain("executionStats")

返回如下:

{
   "queryPlanner" : {
      ...
      "winningPlan" : {
         "stage" : "FETCH",
         "inputStage" : {
            "stage" : "IXSCAN",
            "keyPattern" : {
               "type" : 1,
               "quantity" : 1
            },
            ...
         }
      },
      "rejectedPlans" : [ ]
   },
   "executionStats" : {
      "executionSuccess" : true,
      "nReturned" : 2,            -- 返回了2个文档
      "executionTimeMillis" : 0,
      "totalKeysExamined" : 2,    -- 扫描2个索引键
      "totalDocsExamined" : 2,    -- 匹配到2个文档
      "executionStages" : {
         ...
      }
   },
   ...
}

可见,对于查询条件{ quantity:{ $gte:100, $lte:300 }, type:"food" },索引{ type:1, quantity:1 }比{ quantity:1, type:1 }更有效率


== 跟踪游标 ==

默认地,当客户端取完游标里所有数据后,MongoDB会自动关闭该游标,然而,对于覆盖(capped)集合可以使用跟踪(tailable)游标,即使客户端取完游标里现有的数据,该游标仍能保持打开状态,跟踪游标的理念类似于Linux中带-f选项的tail命令,当有新文档写入覆盖集合后,跟踪游标会继续返回它们

由于覆盖集合上的跟踪游标不使用索引,所以有着很高的吞吐能力,例如,MongoDB的复制功能就是在主机的oplog上使用跟随游标

注意以下行为:
- 跟踪游标不能使用索引,且按数据写入顺序返回文档,即不能指定排序字段
- 因为不能使用索引,所以查询的初始扫描会很费时费力,但之后获得最新文档是很轻松的

跟踪游标可能会卡死,原因有二:
- 查询一开始就无文档匹配
- 游标返回了位于集合末尾的文档,但有程序删除了该文档(即用于定位的文档被删除了)


阅读(1171) | 评论(0) | 转发(0) |
0

上一篇:Mongo Shell

下一篇:没有了

给主人留下些什么吧!~~