讀古今文學網 > MongoDB實戰 > 9.3 分片集群的查詢與索引 >

9.3 分片集群的查詢與索引

從應用程序的角度來看,查詢分片集群和查詢單個mongod沒什麼區別。這兩種情況下,查詢接口和迭代結果集的過程是一樣的。但在外表之下,兩者還是有區別的,有必要瞭解一下其中的細節。

9.3.1 分片查詢類型

假設正在查詢一個分片集群,為了返回一個恰當的查詢響應,mongos要與多少個分片進行交互?稍微思考一下,就能發現這與分片鍵是否出現在查詢選擇器裡有關。還記得嗎?配置服務器(就是mongos)維護了一份分片範圍的映射關係,就是我們在本章早些時候看到的塊。如果查詢包含分片鍵,那麼mongos通過塊數據能很快定位哪個分片包含查詢的結果集。這稱為針對性查詢(targeted query)。

但是,如果分片鍵不是查詢的一部分,那麼查詢計劃器就不得不訪問所有分片來完成查詢。這稱為全局查詢或分散/聚集查詢(scatter/gather query)。圖9-3對這兩種查詢做了描述。

圖9-3 針對副本集的針對性查詢與全局查詢

針對任意指定的分片集群查詢,explain命令能顯示其詳細查詢路徑。讓我們先來看一個針對性查詢,此處要查詢位於集合第一個塊裡的文檔。

> selector = {username: \"Abbott\",
  \"_id\" : ObjectId(\"4e8a1372238d3bece8000012\")}
> db.spreadsheets.find(selector).explain
{
  \"shards\" : {
    \"shard-b/arete:30100,arete:30101\" : [
      {
         \"cursor\" : \"BtreeCursor username_1__id_1\",
         \"nscanned\" : 1,
         \"n\":1,
         \"millis\" : 0,
         \"indexBounds\" : {
           \"username\" : [
             [
                \"Abbott\",
                \"Abbott\"
             ]
         ],
         \"_id\" : [
           [
             ObjectId(\"4d6d57f61d41c851ee000092\"),
             ObjectId(\"4d6d57f61d41c851ee000092\")
           ]
         ]
       }
     }
   ]
 },
 \"n\" : 1,
 \"nscanned\" : 1,
 \"millisTotal\" : 0,
 \"numQueries\" : 1,
 \"numShards\" : 1
}
  

explain的結果清晰地說明查詢命中了一個分片——分片B,返回了一個文檔。1查詢計劃器很聰明地使用了分片鍵前綴的子集來路由查詢。也就是說你也可以單獨根據用戶名進行查詢:

1. 注意,簡單起見,在這個執行計劃以及接下來的執行計劃裡,我省略了很多字段。

> db.spreadsheets.find({username: \"Abbott\"}).explain
{
  \"shards\" : {
    \"shard-b/arete:30100,arete:30101\" : [
      {
        \"cursor\" : \"BtreeCursor username_1__id_1\",
        \"nscanned\" : 801,
        \"n\" : 801,
      }
    ]
  },
  \"n\" : 801,
  \"nscanned\" : 801,
  \"numShards\" : 1
}
  

該查詢總共返回了801個用戶文檔,但仍然只訪問了一個分片。

那麼全局查詢又會怎麼樣呢?也可以方便地使用explain命令。下面就是一個根據filename字段進行查詢的例子,其中既沒有用到索引,也沒有用到分片鍵:

> db.spreadsheets.find({filename: \"sheet-1\"}).explain
{
  \"shards\" : {
    \"shard-a/arete:30000,arete:30002,arete:30001\" : [
      {
        \"cursor\" : \"BasicCursor\",
        \"nscanned\" : 102446,
        \"n\" : 117,
        \"millis\" : 85,
      }
    ],
    \"shard-b/arete:30100,arete:30101\" : [
      {
        \"cursor\" : \"BasicCursor\",
        \"nscanned\" : 77754,
        \"nscannedObjects\" : 77754,
        \"millis\" : 65,
      }
    ]
  },
  \"n\" : 2900,
  \"nscanned\" : 180200,
  \"millisTotal\" : 150,
  \"numQueries\" : 2,
  \"numShards\" : 2
}
  

如你所想,該全局查詢在兩個分片上都進行了表掃瞄。如果該查詢與你的應用程序有關,你一定想在filename字段上增加一個索引。無論哪種情況,它都會搜索整個集群以返回完整結果。

一些查詢要求並行獲取整個結果集。例如,假設想根據修改時間對電子錶格進行排序。這要求在mongos路由進程裡合併結果。沒有索引,這樣的查詢會非常低效,並且會屢遭禁止。因此,在下面這個查詢最近創建文檔的例子裡,你會先創建必要的索引:

> db.spreadsheets.ensureIndex({updated_at: 1})
> db.spreadsheets.find({}).sort({updated_at: 1}).explain
{
  \"shards\" : {
    \"shard-a/arete:30000,arete:30002\" : [
      {
        \"cursor\" : \"BtreeCursor updated_at_1\",
        \"nscanned\" : 102446,
        \"n\" : 102446,
        \"millis\" : 191,
      }
    ],
    \"shard-b/arete:30100,arete:30101\" : [
      {
        \"cursor\" : \"BtreeCursor updated_at_1\",
        \"nscanned\" : 77754,
        \"n\" : 77754,
        \"millis\" : 130,
      }
    ]
  },
  \"n\" : 180200,
  \"nscanned\" : 180200,
  \"millisTotal\" : 321,
  \"numQueries\" : 2,
  \"numShards\" : 2
}
  

正如預期的那樣,游標掃瞄了每個分片的updated_at索引,以此返回最近更新的文檔。

更有可能出現的查詢是返回某個用戶最新修改的文檔。同樣,你要創建必要的索引,隨後發起查詢:

> db.spreadsheets.ensureIndex({username: 1, updated_at: -1})
> db.spreadsheets.find({username: \"Wallace\"}).sort(
  {updated_at: -1}).explain
{
  \"clusteredType\" : \"ParallelSort\",
  \"shards\" : {
    \"shard-1-test-rs/arete:30100,arete:30101\" : [
      {
        \"cursor\" : \"BtreeCursor username_1_updated_at_-1\",
        \"nscanned\" : 801,
        \"n\" : 801,
        \"millis\" : 1,
      }
    ]
  },
  \"n\" : 801,
  \"nscanned\" : 801,
  \"numQueries\" : 1,
  \"numShards\" : 1
}
  

關於這個執行計劃,有幾個需要注意的地方。首先,該查詢指向了單個分片。因為你指定了分片鍵,所以查詢路由器可以找出哪個分片包含了相關的塊。隨後你就會發現排序並不需要訪問所有的分片;當排序查詢中包含分片鍵,所要查詢的分片數量通常都能有所減少。本例中,只需訪問一個分片,也能想像類似的查詢,即需要訪問幾個分片,所訪問的分片數量少於分片總數。

第二個需要注意的地方是分片使用了{username: 1, updated_at: -1}索引來執行查詢。這說明了一個很重要的內容,即分片集群是如何處理查詢的。通過分片鍵將查詢路由給指定分片,一旦到了某個分片上,由分片自行決定使用哪個索引來執行該查詢。在為應用程序設計查詢和索引時,請牢記這一點。

9.3.2 索引

你剛看了一些例子,其中演示了索引查詢是如何在分片集群裡工作的。有時,如果不確定某個查詢是怎麼解析的,可以試試explain。通常這都很簡單,但是在運行分片集群時,有幾點關於索引的內容應該牢記於心,下面我會逐個進行說明。

  1. 每個分片都維護了自己的索引。這點應該是顯而易見的,當你在分片集合上聲明索引時,每個分片都會為它那部分集合構建獨立的索引。例如,在上一節裡,你通過mongos發起了db.spreasheets.ensureIndex命令,每一個分片都單獨處理了索引創建命令。

  2. 由此可以得出一個結論,每個分片上的分片集合都應該擁有相同的索引。如果不是這樣的話,查詢性能會很不穩定。

  3. 分片集合只允許在_id字段和分片鍵上添加唯一性索引。其他地方不行,因為這需要在分片間進行通信,實施起來很複雜,而且相信這麼做速度也很慢,沒有實現的價值。

一旦理解了如何進行查詢的路由選擇,以及索引是如何工作的,你應該就能針對分片集群寫出漂亮的查詢和索引了。第7章裡幾乎所有關於索引和查詢優化的建議都能用得上,此外,在必要的時候,你還可以使用強大的explain工具。