讀古今文學網 > MongoDB實戰 > 2.2 創建索引並查詢 >

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.count
200000

> 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命令。