MongoDB权威指南(4)- Indexing

Note:mongoDB的索引的工作方式和关系数据库中的索引几乎是一样的。

1.索引简介

假设我们要按单个key查询,如下:

> db.people.find({"username" : "mark"})

对单个的key进行查询的时候,我们可以在这个key上建立索引来提高查询速度。使用ensureIndex方法建立索引如下:

> db.people.ensureIndex({"username" : 1})

一个索引只需创建一次,重复创建相同的索引没有任何效果。

一个key上建立的索引会使对这个key的查询速度提高,除此之外就没有效果了,即使是查询包含这个key,如:

> db.people.find({"date" : date1}).sort({"date" : 1, "username" : 1})

这个查询里,服务器必须遍历整个collction来找到日期符合的记录,这个过程叫做table scan(全表扫描),一般情况下你都会尽量避免

table scan,因为它对大型的collection运行非常缓慢。作为一条经验规则,你需要给它创建一个索引,包含了查询中用到的所有key的一个索引。

> db.ensureIndex({"date" : 1, "username" : 1})

传递给ensureIndex方法的document参数和sort方法的参数是一样的,它是一组key/value对,值可能是1或-1,代表索引进行的方向。

如果索引里只有一个key,方向就无所谓了,如果索引里有多个key,那么你就得考虑索引的方向问题。假设我们有下边的一些用户:

{ "_id" : ..., "username" : "smith", "age" : 48, "user_id" : 0 }
{
"_id" : ..., "username" : "smith", "age" : 30, "user_id" : 1 }
{
"_id" : ..., "username" : "john", "age" : 36, "user_id" : 2 }
{
"_id" : ..., "username" : "john", "age" : 18, "user_id" : 3 }
{
"_id" : ..., "username" : "joe", "age" : 36, "user_id" : 4 }
{
"_id" : ..., "username" : "john", "age" : 7, "user_id" : 5 }
{
"_id" : ..., "username" : "simon", "age" : 3, "user_id" : 6 }
{
"_id" : ..., "username" : "joe", "age" : 27, "user_id" : 7 }
{
"_id" : ..., "username" : "jacob", "age" : 17, "user_id" : 8 }
{
"_id" : ..., "username" : "sally", "age" : 52, "user_id" : 9 }
{
"_id" : ..., "username" : "simon", "age" : 59, "user_id" : 10 }

如果我们建立索引{"username" : 1, "age" : -1},mongoDB就会按下边的样子组织用户:

{ "_id" : ..., "username" : "jacob", "age" : 17, "user_id" : 8 }
{
"_id" : ..., "username" : "joe", "age" : 36, "user_id" : 4 }
{
"_id" : ..., "username" : "joe", "age" : 27, "user_id" : 7 }
{
"_id" : ..., "username" : "john", "age" : 36, "user_id" : 2 }
{
"_id" : ..., "username" : "john", "age" : 18, "user_id" : 3 }
{
"_id" : ..., "username" : "john", "age" : 7, "user_id" : 5 }
{
"_id" : ..., "username" : "sally", "age" : 52, "user_id" : 9 }
{
"_id" : ..., "username" : "simon", "age" : 59, "user_id" : 10 }
{
"_id" : ..., "username" : "simon", "age" : 3, "user_id" : 6 }
{
"_id" : ..., "username" : "smith", "age" : 48, "user_id" : 0 }
{
"_id" : ..., "username" : "smith", "age" : 30, "user_id" : 1 }

首先按名字的升序排列,名字相同的组里按降序排列。这索引会优化按{"username" : 1, "age" :-1}的排序操作,而对{"username" : 1, "age" : 1}

的排序效果就没那么好了,如果我们想优化{"username" : 1, "age" : 1},那就应该按{"username" : 1, "age" : 1}来建立索引,让年龄也升序排列。

对username和age建立的索引同时也会是对username的查询速度提高,通常,如果索引有N个key组成,对其中前边部分的查询速度也会提高。

例如,我们建立了索引{"a" : 1, "b" : 1, "c" : 1, ..., "z" : 1},那么效果上相当于我们也有了{"a" : 1}, {"a" : 1, "b" : 1}, {"a" : 1, "b" : 1, "c" :1}等等。

mongoDB的查询优化器会调整查询条件之间的顺序以利用索引,比如说你要查询{"x" : "foo", "y" : "bar"},而你的索引是{"y" : 1, "x" :1},优化器会自行调整。

索引的不利之处是给插入、更新、删除操作增添了一些负担。

在某些情况下,使用索引也许还不如不用索引。通常,如果查询返回collection里一半甚至更多的记录,那么相比为几乎每个document查找索引及其值,直接使用

全表扫描还更快些。

索引度量? (Scaling Index)

假设我们有个collection存储用户的状态消息,我们想按用户查询每个用户的最新状态,根据我们学到的知识,我们可能会这样建立索引:

> db.status.ensureIndex({user : 1, date : -1})

这个索引会使对user和date的查询速度提高,但实际上并不是最好的选择。按照这个索引,我们的数据可能是下边这个样子:

User 123 on March 13, 2010
User
123 on March 12, 2010
User
123 on March 11, 2010
User
123 on March 5, 2010
User
123 on March 4, 2010
User
124 on March 12, 2010
User
124 on March 11, 2010
...

如果只是这个数据规模,这样子看起来还是不错的,如果程序里有百万千万的用户,每个用户每天都会产生几十条状态更新呢?

如果每个用户的状态消息的索引记录都占用了磁盘空间一页的大小,那么每次进行最新状态查询时,数据都不得不加载另外一个页面进内存。

要是我们使用{date : -1, user : 1}做索引,那么数据库就可以将最近几天的索引保持在内存里,会有更少的页面对换,查询最新状态

也会更快。

对嵌入document的key建立索引

> db.blog.ensureIndex({"comments.date" : 1})

对嵌入的document建立索引和对顶级document建立索引没有差别,两者在组合索引里也可以组合使用。

为排序建立索引

如果对一个未建立索引的key调用sort方法,mongoDB需要取出所有的数据,放入内存然后排序,所以这个大小是有限制的,

如果collection太大,mongoDB就会返回一个错误。建立索引可以避免这个问题,使你可以对任意数量的数据进行排序而不会耗尽内存。

2.唯一索引 

唯一索引保证对于指定的key,collection里每个document中其值都是唯一的。如,要保证用户名都不重复:

> db.people.ensureIndex({"username" : 1}, {"unique" : true})

Note:如果key不存在,索引就会将其值存储为null,如果要再插入一个不含此key的document,插入就会失败,因为已经有了一个

值为null的document。

删除重复

对已有的collection建立唯一索引时,里边也许已经有了重复的值,这会导致索引建立失败,如果你想删掉具有重复值的document,

可以使用dropDups选项,遇到的第一个document被保留,其他的都被删除掉了。

> db.people.ensureIndex({"username" : 1}, {"unique" : true, "dropDups" : true})

组合唯一索引

组合唯一索引里的单个key的值可以是重复的,但是所有key的组合必须是唯一的。

3.使用explain和hint

> db.foo.find().explain()

explain方法返回一个document而不是游标本身,这个document包含了用到的索引、统计信息等。

举个例子,对一个无索引的collection执行一个最简单的查询({}),返回64个document,那么explain的输出为

> db.people.find().explain()
{
  "cursor" : "BasicCursor",
  "indexBounds" : [ ],
  "nscanned" : 64,
  "nscannedObjects" : 64,
  "n" : 64,
  "millis" : 0,
  "allPlans" : [
  {
    "cursor" : "BasicCursor",
    "indexBounds" : [ ]
  }
  ]

}
  • "cursor" : "BasicCursor"
    意思是查询没有使用索引
  • "nscanned" : 64
    数据库扫描过的document数量
  • "n" : 64
    返回的结果集的document数量
  • "millis" : 0
    数据库执行查询消耗的毫秒数

现在我们看个稍微复杂点的例子,假设我们在age键上建立了索引,我们要查询年龄为20多岁的用户。

> db.c.find({age : {$gt : 20, $lt : 30}}).explain()
{
  "cursor" : "BtreeCursor age_1",
  "indexBounds" : [
    [{
"age" : 20},{"age" : 30}]
  ],
  "nscanned" : 14,
  "nscannedObjects" : 12,
  "n" : 12,
  "millis" : 1,
  "allPlans" : [
    {
    "cursor" : "BtreeCursor age_1",
    "indexBounds" : [
      [{
"age" : 20},{"age" : 30}]
    ]
  }
  ]
}
  • "cursor" : "BtreeCursor age_1"
    这次不是
    BasicCursor了,索引是存储在B-Tree的数据结构里,这个查询使用了索引,它是使用了B-Tree类型的游标。
    age_1是索引的名字,有了这个名字我们就可以查询system.indexes collection,获取关于此索引的更多信息。
    > db.system.indexes.find({"ns" : "test.c", "name" : "age_1"})
    {
      "_id" : ObjectId("4c0d211478b4eaaf7fb28565"),
      "ns" : "test.c",
      "key" : {
        "age" : 1
      },
      "name" : "age_1"
    }
  • "allPlans" : [ ... ]
    列出了此查询可用的所有的计划。如果我们有多个索引和更加复杂的查询,"allPlans"就会包含所有可能的计划。

让我们看个更复杂点的查询例子,假设我们有一个索引{"username" : 1, "age" : 1}和一个索引{"age" : 1, "username" : 1},那么当我们

查询username和age的时候会发生什么事?实际上这样要依赖于查询。

> db.c.find({age : {$gt : 10}, username : "sally"}).explain()
{
  "cursor" : "BtreeCursor username_1_age_1",
  "indexBounds" : [
    [
      {
        "username" : "sally",
        "age" : 10
      },
      {
        "username" : "sally",
        "age" : 1.7976931348623157e+308
      }
    ]
  ],
  "nscanned" : 13,
  "nscannedObjects" : 13,
  "n" : 13,
  "millis" : 5,
  "allPlans" : [
    {
      "cursor" : "BtreeCursor username_1_age_1",
      "indexBounds" : [
        [
          {
            "username" : "sally",
            "age" : 10
          },
          {
            "username" : "sally",
            "age" : 1.7976931348623157e+308
          }
        ]
      ]
    }
  ],
  "oldPlan" : {
    "cursor" : "BtreeCursor username_1_age_1",
    "indexBounds" : [
      [
        {
          "username" : "sally",
          "age" : 10
        },
        {
          "username" : "sally",
          "age" : 1.7976931348623157e+308
        }
      ]
    ]
  }
}

由于当我们查询的是一个确定的username值和一个age范围值,所以数据库使用的是{"username" : 1, "age" : 1}这个索引,

反过来,如果我们查询的是一个确定的年龄和名字范围,那么数据库就会使用另外的那个索引

> db.c.find({"age" : 14, "username" : /.*/}).explain()
{
  "cursor" : "BtreeCursor age_1_username_1 multi",
  "indexBounds" : [
    [
      {
        "age" : 14,
        "username" : ""
      },
      {
        "age" : 14,
        "username" : {
        }
      }
    ],
    [
      {
        "age" : 14,
        "username" : /.*/
      },
      {
        "age" : 14,
        "username" : /.*/
      }
    ]
  ],
  "nscanned" : 2,
  "nscannedObjects" : 2,
  "n" : 2,
  "millis" : 2,
  "allPlans" : [
    {
      "cursor" : "BtreeCursor age_1_username_1 multi",
      "indexBounds" : [
        [
          {
            "age" : 14,
            "username" : ""
          },
          {
            "age" : 14,
            "username" : {
            }
          }
        ],
        [
          {
            "age" : 14,
            "username" : /.*/
          },
          {
            "age" : 14,
            "username" : /.*/
          }
        ]
      ]
    }
  ]
}

如果你发现数据库使用的不是你想用的索引,那么你可以使用hint强制数据库使用你指定的索引。

> db.c.find({"age" : 14, "username" : /.*/}).hint({"username" : 1, "age" : 1})

指定索引通常是没有必要的,mongoDB有自己的查询优化器,会很聪明地选择使用哪个索引,你只需要关心的是优化器有可用的索引以备选择。

4.索引管理

每个database都有个叫system.indexes的collection,它里边存储了索引的元数据信息,这个collection是保留的,不能进行插入或删除,

只能通过ensureIndex和dropIndexes命令来操作里边的document。system.indexes里包含了每个索引的详细信息,另外还有个叫

system.namespaces的collection列出了索引的名字。查看这collection可以看到,每个collection至少有两条记录,一个是collection本身,

另外的是collection里的每个索引。

建立索引是个耗时耗资源的操作,如果collection的数据量很大,你可以指定background选项来在后台进行工作。

> db.people.ensureIndex({"username" : 1}, {"background" : true})

如果没有使用background选项的话,database就会阻塞所有的请求,知道索引建立完成。

如果你不在需要某个索引,你可以用dropIndexes命令移除它,你可能得先在system.indexes里找到索引的名字,因为各种驱动自动生成

的索引名字各不一样。

> db.runCommand({"dropIndexes" : "foo", "index" : "alphabet"})

使用*删除collection的所有的索引

> db.runCommand({"dropIndexes" : "foo", "index" : "*"})

5.地理空间索引

在ensureIndex方法中使用"2d"做参数而不是1或者-1,建立空间索引

> db.map.ensureIndex({"gps" : "2d"})

gps这个key的值必须是某种成对的值,一个包含两个元素的数组,或者一个有两个key的嵌入的document,下边的例子都是可以的

{ "gps" : [ 0, 100 ] }
{
"gps" : { "x" : -30, "y" : 30 } }
{
"gps" : { "latitude" : -180, "longitude" : 180 } }

嵌入的document里边key的名字是任意的,它们的值缺省是从-180到180,方便使用经纬度,如果你要使用其他的单位,可以指定

最大值和最小值

> db.star.trek.ensureIndex({"light-years" : "2d"}, {"min" : -1000, "max" : 1000})

地理空间索引可以通过两种方式来使用,一是普通的find查询,另外是作为数据库命令。

 > db.map.find({"gps" : {"$near" : [40, -73]}}).limit(10)

和下边的使用geoNear命令进行的查询等价

> db.runCommand({geoNear : "map", near : [40, -73], num : 10});

mongoDB还允许你使用一个shape来查找document,为了查找shape里所有的点,我们可以使用"$within"条件操作符,使用"$box"

定义一个矩形

> db.map.find({"gps" : {"$within" : {"$box" : [[10, 20], [15, 30]]}}})

"$box"的值是含两个元素的数组,第一个是左边的Y值小的顶点,第二个是右边的Y值大的顶点。(大概就是这个意思,因为一般地理坐标系统

中,Y轴是向上的,而我们的屏幕坐标中,原点在左上角,Y轴是向下的,数据库里仅仅是数据)

同样,你也可以找到一个圆里边的所有点,$center的第一个元素是圆心,第二个元素是半径

> db.map.find({"gps" : {"$within" : {"$center" : [[12, 25], 5]}}})

组合空间索引

> db.ensureIndex({"location" : "2d", "desc" : 1})

如果你要查询最近的咖啡馆

> db.map.find({"location" : {"$near" : [-70, 30]}, "desc" : "coffeeshop"}).limit(1)
{
  "_id" : ObjectId("4c0d1348928a815a720a0000"),
  "name" : "Mud",
  "location" : [x, y],
  "desc" : ["coffee", "coffeeshop", "muffins", "espresso"]
}

posted on 2011-07-08 10:43  chouyuu  阅读(2340)  评论(0编辑  收藏  举报