結論
先上結論,在不建立索引的情況下,除使用 _id
之外的字段排序,使用 find
進行分頁查詢 skip
和limit
的查詢結果能正常返回順序,不會受到其他影響。而使用聚合方式 aggregate
查詢,其返回結果是不確定的,解決方式是爲這個字段建立索引或者增加能確定順序的排序條件。
原因
因爲在這種查詢中,提供的排序條件是不充分的。在進行聚合查詢時,數據庫已經按照要求按 {count: 1}
完成了排序,但是因爲它們的值都一樣,不管誰放在前面誰放在後面其實都沒有違反要求,因爲查詢要求只是 count
的升序而已。從數據庫的角度來講,既然沒有額外的要求,那當然是以最高效的方式返回結果,也就是不管 count
以外的順序,因爲這樣最省資源。
那麼什麼是最高效?這裏涉及一些數據庫底層的知識。在單機上,如果沒有索引支持,數據庫會嘗試遍歷所有數據,然後做一個內存排序來返回結果,從節省資源的角度,顯然這個排序只排到滿足了 count
升序爲止,其他字段可以說是先到先得。這就造成在count相同時,其他順序是隨機的。它們可能受到:
- 自己在磁盤上的順序影響,因爲這會影響到數據庫先遍歷到哪條記錄。並且要注意,每次更新數據時它們在磁盤上的順序是會變化的;
- 在分片集羣環境中,結果還受到哪個片先返回數據的影響。分片環境中的排序是先在各個片排好序,再進行一次合併排序;
- 理論上還和數據庫使用的排序算法相關;
總之,在 aggregate
查詢中,如果不指定,數據庫不保證返回結果是確定的。
至於解決方案也已經很明確了,指定一個可以完全確定順序的排序條件,比如:
{$sort: {count: -1, _id: 1}}
但是需要知道的是,這樣會讓數據庫付出額外的操作來保證第二個排序條件的正確性,在實際使用場景中要根據實際情況判斷這是不是真的有意義。
另外的方式是給這個字段建立索引,那麼查詢出來的結果就會按照索引中的順序返回。
測試過程:
生成20條測試數據
for (let i = 0 ; i < 10 ; i++){
db.getCollection('Test').insert({name:String(i), count:NumberInt(0)})
}
結果如下:
db.getCollection('Test').find({})
find
查詢分頁的結果
db.getCollection('Test').find({}).sort({count:1}).limit(3)
db.getCollection('Test').find({}).sort({count:1}).limit(5)
可以看到返回的結果是確定的,不會因爲 skip
和limit
而受影響。
aggregate
查詢分頁的結果
db.getCollection('Test').aggregate([
{$sort: {count:1}},
{$limit:3}
])
db.getCollection('Test').aggregate([
{$sort: {count:1}},
{$limit:5}
])
db.getCollection('Test').aggregate([
{$sort: {count:1}},
{$limit:10}
])
db.getCollection('Test').aggregate([
{$sort: {count:1}},
{$skip: 1},
{$limit:5}
])
在這裏我們可以看到,返回結果是不確定的,會隨着 skip
和limit
而受影響。
增加一個排序字段,再次進行查詢,能看到結果符合預期,不會受到影響。
db.getCollection('Test').aggregate([
{$sort: {count:1,_id:1}},
{$limit:3}
])
創建字段索引
這時候我們給這個創建索引,看看情況怎樣
db.getCollection('Test').createIndex({"count":1})
再次進行查詢,看看結果如何
db.getCollection('Test').aggregate([
{$sort: {count:1}},
{$limit:10}
])
此時已經發現,返回結果是跟 find
查詢一致,並且不會受到 skip
和limit
的影響。