從應用程序的角度來看,查詢分片集群和查詢單個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
。通常這都很簡單,但是在運行分片集群時,有幾點關於索引的內容應該牢記於心,下面我會逐個進行說明。
每個分片都維護了自己的索引。這點應該是顯而易見的,當你在分片集合上聲明索引時,每個分片都會為它那部分集合構建獨立的索引。例如,在上一節裡,你通過
mongos
發起了db.spreasheets.ensureIndex
命令,每一個分片都單獨處理了索引創建命令。由此可以得出一個結論,每個分片上的分片集合都應該擁有相同的索引。如果不是這樣的話,查詢性能會很不穩定。
分片集合只允許在
_id
字段和分片鍵上添加唯一性索引。其他地方不行,因為這需要在分片間進行通信,實施起來很複雜,而且相信這麼做速度也很慢,沒有實現的價值。
一旦理解了如何進行查詢的路由選擇,以及索引是如何工作的,你應該就能針對分片集群寫出漂亮的查詢和索引了。第7章裡幾乎所有關於索引和查詢優化的建議都能用得上,此外,在必要的時候,你還可以使用強大的explain
工具。