首页 » MongoDB实战 » MongoDB实战全文在线阅读

《MongoDB实战》2.2 创建索引并查询

关灯直达底部

创建索引来提升查询性能是很常见的做法。很幸运,你能轻松地在Shell中创建MongoDB的索引。如果没接触过数据库索引,本节内容会让你理解对它们的需求;如果有过索引的使用经验,你会发现创建索引然后使用explain方法根据索引来剖析查询有多么方便。

2.2.1 创建一个大集合

只有集合中的文档达到一定的数量之后,索引示例才有意义。因此,向numbers集合中添加200 000个简单文档。因为MongoDB Shell也是一个JavaScript解释器,所以实现这一功能的代码很简单:

for(i=0; i<200000; i++) {  db.numbers.save({num: i});}  

这些文档数量不少,因此如果插入花了不少时间也不用感到惊讶。执行返回后,可以运行两条查询来验证文档全部存在:

> db.numbers.count200000> db.numbers.find{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac830a/"), /"num/" : 0 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac830b/"), /"num/" : 1 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac830c/"), /"num/" : 2 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac830d/"), /"num/" : 3 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac830e/"), /"num/" : 4 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac830f/"), /"num/" : 5 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8310/"), /"num/" : 6 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8311/"), /"num/" : 7 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8312/"), /"num/" : 8 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8313/"), /"num/" : 9 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8314/"), /"num/" : 10 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8315/"), /"num/" : 11 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8316/"), /"num/" : 12 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8317/"), /"num/" : 13 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8318/"), /"num/" : 14 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8319/"), /"num/" : 15 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831a/"), /"num/" : 16 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831b/"), /"num/" : 17 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831c/"), /"num/" : 18 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831d/"), /"num/" : 19 }has more  

count命令说明插入了200 000个文档,随后的查询显示了前20个结果,你可以用it命令显示更多查询结果:

>it{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831e/"), /"num/" : 20 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831f/"), /"num/" : 21 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8320/"), /"num/" : 22 }...  

it命令会告诉Shell返回下一个结果集。1

1. 你也许想知道背后究竟发生了什么。所有的查询都会创建一个游标,可以迭代结果集。这个过程是隐藏在Shell的使用过程中的,因此目前还没有必要详细说明。如果你迫不及待地想深入了解游标及其特性,可以阅读第3章和第4章。

手头有了数量可观的文档之后,我们试着运行一些查询。就你目前对MongoDB查询引擎的了解,一个简单的匹配num属性的查询很好理解:

> db.numbers.find({num: 500}){ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac84fe/"), /"num/" : 500 }  

但更值得一提的是,你还可以使用特殊的$gt$lt操作符(最早见于第1章,分别表示大于和小于)来执行范围查询。下面的语句用来查询num值大于199 995的所有文档:

 > db.numbers.find( {num: {/"$gt/": 199995 }} ){ /"_id/" : ObjectId(/"4bfbf1dedba1aa7c30afcade/"), /"num/" : 199996 }{ /"_id/" : ObjectId(/"4bfbf1dedba1aa7c30afcadf/"), /"num/" : 199997 }{ /"_id/" : ObjectId(/"4bfbf1dedba1aa7c30afcae0/"), /"num/" : 199998 }{ /"_id/" : ObjectId(/"4bfbf1dedba1aa7c30afcae1/"), /"num/" : 199999  

还可以结合使用这两个操作符指定上界和下界:

 > db.numbers.find( {num: {/"$gt/": 20, /"$lt/": 25 }} ){ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac831f/"), /"num/" : 21 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8320/"), /"num/" : 22 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8321/"), /"num/" : 23 }{ /"_id/" : ObjectId(/"4bfbf132dba1aa7c30ac8322/"), /"num/" : 24  

可以看到,使用简单的JSON文档,可以像在SQL中一样声明复杂的范围查询。MongoDB查询语言由大量特殊关键字组成,$gt$lt只是其中的两个,在后续的章节中你还会看到更多查询的例子。

当然,这样的查询如果效率不高,那么几乎一点儿价值都没有。下一节中我们将探索MongoDB的索引特性,开始思考查询效率。

2.2.2 索引与explain

如果你使用过关系型数据库,想必对SQL的EXPLAIN并不陌生。EXPLAIN用来描述查询路径,通过判断查询使用了哪个索引来帮助开发者诊断慢查询。MongoDB也有提供相同服务的“EXPLAIN”。为了了解它是如何工作的,先在运行过的查询上试一下:

 > db.numbers.find( {num: {/"$gt/": 199995 }} ).explain 

返回结果如代码清单2-1所示。

代码清单2-1 无索引查询的典型explain输出

 {  /"cursor/" : /"BasicCursor/",  /"nscanned/" : 200000,  /"nscannedObjects/" : 200000,  /"n/" : 4,  /"millis/" : 171,  /"nYields/" : 0,  /"nChunkSkips/" : 0,  /"isMultiKey/" : false,  /"indexOnly/" : false,  /"indexBounds/":{} }  

查看explain的输出,你会惊讶地发现,查询引擎为了返回4个结果(n)扫描了整个集合,即全部200 000个文档(nscanned)。BasicCursor游标类型说明该查询在返回结果集时没有使用索引。扫描文档和返回文档数量之间巨大的差异说明这是一个低效查询。在现实当中,集合与文档本身可能会更大,处理查询所需的时间将大大超过此处的171 ms。

这个集合需要索引。你可以通过ensureIndex方法为num键创建一个索引。请输入下列索引创建代码:

 > db.numbers.ensureIndex({num: 1})  

与查询和更新等其他MongoDB操作一样,你为ensureIndex方法传入了一个文档,定义索引的键。这里,文档{num:1}说明为numbers集合中所有文档的num键构建一个升序索引。

可以调用getIndexes方法来验证索引是否已经创建好了:

> db.numbers.getIndexes[  {   /"name/" : /"_id_/",   /"ns/" : /"tutorial.numbers/",   /"key/" : {     /"_id/" : 1   }  },  {    /"_id/" : ObjectId(/"4bfc646b2f95a56b5581efd3/"),    /"ns/" : /"tutorial.numbers/",    /"key/" : {    /"num/" : 1  },  /"name/" : /"num_1/"  }]  

该集合现在有两个索引了,第一个是为每个集合自动创建的标准_id索引,第二个是刚才在num上创建的索引。

如果现在再来运行explain方法,在查询的响应时间上会有巨大的差异,如代码清单2-2所示。

代码清单2-2 有索引查询的explain输出

 > db.numbers.find({num: {/"$gt/": 199995 }}).explain{   /"cursor/" : /"BtreeCursor num_1/",  /"indexBounds/" : [    [      {        /"num/" : 199995      },      {        /"num/" : 1.7976931348623157e+308      }    ]   ],   /"nscanned/" : 5,   /"nscannedObjects/" : 4,   /"n/" : 4,   /"millis/" : 0 }  

现在查询利用了num上的索引,只扫描了5个文档,将查询时间从171 ms降到了1 ms以下。

如果这个例子激起了你的兴趣,请不要错过专门介绍索引和查询优化的第7章。接下来让我们看看基本的管理命令,它们可以用来获取MongoDB实例的信息。你还将了解到一些技术,它们与如何在Shell里获取帮助相关,这有助于掌握众多Shell命令。