MongoDB'de sonuçları sıralamak için sort(), sayısını sınırlamak için limit(), baştan belirli sayıda belgeyi atlamak için skip() kullanılır. Üçü de find()'ın döndürdüğü cursor üzerinde çalışır ve birlikte kullanıldığında klasik sayfalamayı oluşturur: find().sort({ _id: 1 }).skip(20).limit(10) üçüncü sayfanın on belgesini getirir. Toplam sayfa sayısı için de countDocuments() devreye girer.
Basit görünse de bu üçlünün tutarlı sıralama, bellek sınırları ve büyük ofsetlerdeki yavaşlama gibi önemli ayrıntıları vardır. Bu yazıda cursor kavramından başlayıp skip tabanlı ve aralık (range) tabanlı sayfalamayı karşılaştırıyor, sayma yöntemlerinin farkını açıklıyoruz. Sorgu filtreleri için find sorguları, serinin tamamı için MongoDB öğrenme rehberi yazısına bakabilirsiniz.
Cursor nedir ve neden önemlidir?

find() ve aggregate() sonuçları doğrudan bir dizi olarak değil, bir cursor olarak döndürür. Cursor, sorgu sonuçlarını işaret eden ve onları partiler (batch) halinde getiren bir yapıdır. Sunucu ilk partiyi doldurana kadar çalışır, sonra durur; bu duraklatılmış sorgu bir cursor kimliğiyle tanımlanır. Daha fazla belge varsa istemci getMore komutuyla sonraki partiyi ister. Belgelere göre find() ve aggregate() için varsayılan ilk parti boyutu 101 belgedir; sonraki getMore çağrılarında belge sayısı sınırı yoktur, yalnızca 16 MiB ileti boyutu sınırı geçerlidir.
mongosh'ta bir cursor'ı let ile bir değişkene atarsanız otomatik olarak yinelenmez. Değişkene atamadan sorguyu doğrudan çalıştırırsanız mongosh ilk sonuçları ekrana basar; varsayılan olarak her yinelemede 20 belge gösterir ve bu değer config.set("displayBatchSize", ...) ile değiştirilebilir. Cursor üzerinde next(), hasNext(), forEach() ve toArray() yöntemleriyle gezinebilirsiniz.
let c = db.urunler.find({ kategori: "kitap" })
c.hasNext() // sırada belge var mı (true / false)
c.next() // sıradaki belgeyi döndürür ve ilerler
while (c.hasNext()) {
printjson(c.next())
}
// Tüm sonuçları diziye almak (küçük sonuç kümeleri için)
db.urunler.find({ kategori: "kitap" }).toArray()Boşta kalan cursor'lar sonsuza kadar açık kalmaz. cursorTimeoutMillis parametresi oturum dışındaki boşta cursor'lar için zaman aşımını belirler ve varsayılanı 10 dakikadır; her yeni parti döndürüldüğünde süre uzar. Oturum içindeki cursor'lar ise istemci cursor'ı tükettiğinde, cursor elle kapatıldığında, oturum sonlandırıldığında veya oturum zaman aşımına uğradığında kapanır. Oturum zaman aşımının varsayılanı 30 dakikadır. Sayfalama açısından önemli sonuç şudur: kullanıcı "sonraki sayfa" butonuna basana kadar bir cursor'ı açık tutmaya çalışmak yerine her sayfa için yeni bir sorgu çalıştırmak gerekir.
sort(): sıralama düzeni
sort() bir belge alır; her alan için 1 artan, -1 azalan sıralama anlamına gelir. Birden fazla alan verdiğinizde sıralama soldan sağa uygulanır: önce ilk alana göre, ilk alanı eşit olan belgeler ikinci alana göre sıralanır. sort() cursor'dan herhangi bir belge alınmadan önce uygulanmalıdır.
// Fiyata göre azalan, eşit fiyatlarda ada göre artan
db.urunler.find().sort({ fiyat: -1, ad: 1 })
// Tutarlı sıralama için benzersiz bir alan ekleyin
db.urunler.find().sort({ kategori: 1, _id: 1 })Belgelerde altı çizilen en önemli nokta sıralama tutarlılığıdır. MongoDB belgeleri koleksiyonda belirli bir sırada saklamaz ve $sort kararlı (stable) bir sıralama değildir. Tekrarlanan değerler içeren bir alana göre sıraladığınızda, aynı değere sahip belgeler her çalıştırmada farklı sırada gelebilir. Sayfalamada bu, bir belgenin iki sayfada birden görünmesine veya hiç görünmemesine yol açar. Çözüm basittir: sıralamaya benzersiz değerler içeren en az bir alan ekleyin; en kolayı _id alanıdır. Sıralanan alan iki belgede de yoksa iki belge sıralama açısından eşit sayılır ve yine herhangi bir sırada dönebilir.
Sıralamayla ilgili diğer kurallar şunlardır: en fazla 32 anahtara göre sıralama yapılabilir ve aynı alanı sıralama belgesinde iki kez yazmak hataya yol açar. Farklı BSON tiplerindeki değerler karşılaştırılırken sabit bir tip sırası kullanılır; örneğin null sayılardan, sayılar metinlerden, metinler nesnelerden önce gelir. Tip sırasının tamamı için BSON veri tipleri yazısına bakın.
Dizi alanlarına göre sıralama da özel bir kurala tabidir: artan sıralamada dizinin en küçük elemanı, azalan sıralamada en büyük elemanı sıralama anahtarı olur. Sorgu filtresi bu seçimi etkilemez; yani { etiketler: { $gt: "m" } } ile filtrelediğiniz belgeler, artan sıralamada filtreye uymayan en küçük etikete göre de sıralanabilir.
İndeks ve bellek: sıralamanın gizli maliyeti
MongoDB sıralamayı bir indeks yardımıyla yapamıyorsa belgeleri bellekte sıralamak zorundadır. İndeks olmadan sıralama yapan sorgular, sonuç döndürmeden önce ilgili belgelerin hepsini belleğe yüklemek zorundadır. İndeks kullanan sıralamalar genellikle bellekte sıralamadan daha iyi performans gösterir. Bir sorgunun bellekte sıralama yapıp yapmadığını görmek için sorguya explain() ekleyip planda SORT aşaması olup olmadığına bakabilirsiniz; ayrıntılar explain ve sorgu planı yazısında.
sort() ile limit() birlikte kullanıldığında ve sıralama indeksten gelemiyorsa MongoDB "top-k" adı verilen bir algoritma kullanır: o ana kadar görülen ilk k sonucu tamponda tutar. Bu k sonucun bellek kullanımı 100 megabaytı aşarsa MongoDB otomatik olarak diske geçici dosya yazar; sorguda allowDiskUse(false) belirtilmişse sorgu bunun yerine başarısız olur. Bu yüzden sayfalamada kullanılan sıralama alanları için uygun bir indeks oluşturmak, hem hız hem bellek açısından en etkili adımdır. Bileşik indekslerde alan sırasının sıralamaya etkisi compound index ve ESR kuralı yazısında anlatılıyor.
limit(): sonuç sayısını sınırlamak
limit(n) cursor'ın en fazla kaç belge döndüreceğini belirler ve SQL'deki LIMIT ifadesine karşılık gelir. Gereğinden fazla sonuç döndürmeyi önleyerek performansı artırır. Davranışın bazı kenarları belgelerde açıkça tanımlanmıştır:
limit(0)hiç sınır koymamakla aynıdır; tüm sonuçlar döner.- Negatif bir değer pozitif değere benzer, ancak cursor tek bir parti döndürdükten sonra kapanır. Sınırlanmış sonuç tek partiye sığmazsa, istediğinizden daha az belge alırsınız.
- Değer sayısal olmalıdır; -231'den küçük ve 231'den büyük değerler için davranış tanımsızdır.
limit()desort()gibi cursor'dan belge alınmadan önce uygulanmalıdır.
limit()'i sort() ile birlikte kullanırken de aynı tutarlılık kuralı geçerlidir: sıralamada benzersiz bir alan yoksa, özellikle koleksiyona yoğun yazma yapılıyorsa, "ilk 10" sonuç her çalıştırmada değişebilir.
skip() ile sayfalama
skip(n) sonuç kümesinin başından n belgeyi atlar. skip() ve limit()'i hangi sırayla zincirlediğiniz sonucu değiştirmez: sunucu her zaman önce sıralamaya göre atlama işlemini, sonra sınırlamayı uygular. Yani .skip(20).limit(10) ile .limit(10).skip(20) aynı belgeleri getirir.
function sayfaGetir(sayfaNo, sayfaBoyu) {
return db.urunler.find({ kategori: "kitap" })
.sort({ _id: 1 })
.skip(sayfaNo > 0 ? (sayfaNo - 1) * sayfaBoyu : 0)
.limit(sayfaBoyu)
.toArray()
}
sayfaGetir(1, 10) // 1-10. belgeler
sayfaGetir(3, 10) // 21-30. belgelerBu yöntem anlaşılırdır ve "5. sayfaya git" gibi rastgele sayfa erişimine izin verir. Ama belgelerin açıkça belirttiği bir maliyeti vardır: skip() sonuç döndürmeye başlamadan önce sunucunun sonuç kümesinin başından itibaren taramasını gerektirir. Ofset büyüdükçe skip() yavaşlar. Birinci sayfa hızlı gelirken 10.000. sayfa için sunucu önceki bütün belgeleri geçmek zorundadır.
Aralık sorgularıyla (keyset) sayfalama

Büyük koleksiyonlarda daha iyi bir yol, son görülen değeri hatırlayıp bir sonraki sayfayı o değerden itibaren istemektir. Aralık sorguları indeksleri kullanarak istenmeyen belgeleri taramaktan kaçınabilir; bu yüzden ofset büyüdükçe skip()'e göre genellikle daha iyi performans verir. Belgelerin önerdiği yöntem şöyledir:
- Zaman içinde tutarlı bir yönde değişen ve benzersiz indeksi olan bir alan seçin;
_idbunun için uygundur. - Artan sıralama için
$gt, azalan sıralama için$ltile başlangıç değerinden sonraki belgeleri sorgulayın ve aynı yöndesort()uygulayın. - Sayfadaki son belgenin alan değerini saklayın ve sonraki sorguda başlangıç değeri olarak kullanın.
function sonrakiSayfa(sonId, sayfaBoyu) {
let yeniSon = null
db.urunler.find({ _id: { $gt: sonId } })
.sort({ _id: 1 })
.limit(sayfaBoyu)
.forEach(u => {
print(u.ad)
yeniSon = u._id
})
return yeniSon // null ise başka sayfa yok
}
// MinKey en küçük olası anahtardır; ilk sayfadan başlar
let imlec = MinKey
while (imlec !== null) {
imlec = sonrakiSayfa(imlec, 10)
}Azalan sırada (en yeniler önce) sayfalamak için aynı fonksiyonu $lt ve sort({ _id: -1 }) ile yazar, başlangıç değeri olarak MaxKey kullanırsınız. Burada küçük ama önemli bir uyarı vardır: ObjectId değerleri zamanla artar ama kesin olarak monoton değildir. Yalnızca bir saniyelik zaman çözünürlüğü içerdikleri için aynı saniyede üretilen ObjectId'lerin sırası garanti edilmez; ayrıca istemciler tarafından üretildikleri için farklı makinelerin saatleri farklı olabilir. Bu, sayfalamayı bozmaz (her belge yine bir kez görülür), ama "_id sırası tam olarak ekleme sırasıdır" varsayımını yapmamalısınız.
| Ölçüt | skip + limit | Aralık sorgusu (keyset) |
|---|---|---|
| Rastgele sayfaya atlama | Mümkün | Doğrudan mümkün değil |
| Büyük ofset performansı | Ofset büyüdükçe yavaşlar | İndeks kullanır, büyük ofsette genellikle daha iyi |
| Gerekli indeks | Sıralama alanı | Benzersiz, sıralanabilir alan |
| Eşzamanlı eklemeler | Kayma ve tekrar olabilir | Son değerden devam ettiği için daha dayanıklı |
| Uygun kullanım | Küçük ve orta veri, yönetim panelleri | Sonsuz kaydırma, API sayfalama, büyük veri |
Toplam sayıyı bulmak: countDocuments ve estimatedDocumentCount
Sayfa numaralarını göstermek için toplam belge sayısına ihtiyaç duyarsınız. MongoDB burada iki yöntem sunar ve ikisi farklı şeyleri ölçer. countDocuments(filtre, seçenekler) filtreye uyan belgelerin kesin sayısını döndürür. Bunu meta veriye bakarak değil, bir aggregation çalıştırarak yapar; belgelere göre yöntem aslında $match ve ardından { $group: { _id: null, n: { $sum: 1 } } } aşamalarını sarmalar ve yalnızca n değerini döndürür. Bu sayede temiz olmayan bir kapanıştan sonra veya sharded kümede sahipsiz (orphaned) belgeler varken bile doğru sonuç verir. Boş veya var olmayan bir koleksiyonda 0 döndürür.
// Filtreye uyan kesin sayı
const toplam = db.urunler.countDocuments({ kategori: "kitap" })
const sayfaSayisi = Math.ceil(toplam / 10)
// Seçenekler: limit, skip, hint, maxTimeMS
db.urunler.countDocuments({ kategori: "kitap" }, { limit: 1000, maxTimeMS: 2000 })
// Boş filtre tam tarama yapar; _id indeksini kullandırmak için:
db.urunler.countDocuments({}, { hint: "_id_" })
db.urunler.countDocuments({ _id: { $gte: MinKey } })
// Koleksiyonun tamamı için meta veriden hızlı tahmin (filtre almaz)
db.urunler.estimatedDocumentCount()countDocuments() filtresinde bazı operatörler kullanılamaz: $where yerine $expr, $near yerine $center ile $geoWithin, $nearSphere yerine $centerSphere ile $geoWithin kullanılmalıdır. Boş filtreyle çağrıldığında tam koleksiyon taraması yapar; belgeler bu durumda _id indeksini kullanmak için bir hint vermeyi veya { _id: { $gte: MinKey } } gibi tüm belgeleri yakalayan bir filtre yazmayı önerir.
estimatedDocumentCount() ise filtre almaz ve koleksiyonun meta verisinden sayı döndürür. Çok hızlıdır ama iki sınırlaması vardır: temiz olmayan bir kapanıştan sonra sayı yanlış olabilir ve sharded kümede sahipsiz belgeleri doğru şekilde ayıklamaz. View'larda meta veri olmadığından sayı, view tanımındaki pipeline çalıştırılarak hesaplanır ve hızlı tahmin avantajı kaybolur. Eski count() yöntemi ise kullanımdan kaldırılmıştır; sürücüler de bu iki yöntemi önerir.
Aggregation içinde sayfalama
Sayfalamayı bir aggregation pipeline içinde yapıyorsanız aynı mantık $sort, $skip ve $limit aşamalarıyla kurulur. Belgelerdeki optimizasyon kurallarına göre $sort, $skip ve $limit art arda geldiğinde iyileştirici limiti sıralamaya katar ve limit değerini skip kadar artırır; böylece sıralama yalnızca gereken sayıda belgeyi bellekte tutar. Pipeline kavramı ve diğer optimizasyonlar için aggregation pipeline yazısına bakın.
db.urunler.aggregate([
{ $match: { kategori: "kitap" } },
{ $sort: { fiyat: -1, _id: 1 } },
{ $skip: 20 },
{ $limit: 10 }
])Sık yapılan hatalar

- Benzersiz alan olmadan sıralamak: Aynı fiyattaki ürünler sayfalar arasında yer değiştirir. Sıralamanın sonuna her zaman
_idekleyin. - Hiç sıralama vermeden sayfalamak: Sıralama belirtilmeyen sorgular belgeleri belirsiz bir sırada döndürür; doğal sıraya (natural order) güvenilmemelidir.
- Derin sayfalar için skip kullanmak: Büyük ofsetlerde sunucu atlanan bütün belgeleri tarar. Sonsuz kaydırma gibi senaryolarda aralık sorgusuna geçin.
- Sıralama alanında indeks olmaması: Bellekte sıralama 100 MB sınırına ve MongoDB 9.0'daki işlem başına bellek sınırına takılabilir.
- Toplam için cursor'ı diziye çevirmek:
find().toArray().lengthbütün belgeleri istemciye taşır. Sayı içincountDocuments()kullanın. - Yöntemleri belge okunduktan sonra eklemek:
sort(),limit()veskip()cursor'dan belge alınmadan önce uygulanmalıdır.
Sık Sorulan Sorular
skip ve limit hangi sırayla yazılmalı?
Fark etmez. Sunucu zincirleme sırasından bağımsız olarak önce sıralamaya göre atlar, sonra sınırlar. Okunabilirlik için genellikle sort().skip().limit() sırası tercih edilir.
Sayfalama sırasında yeni belge eklenirse ne olur?
Skip tabanlı sayfalamada araya giren eklemeler ve silmeler ofseti kaydırır; bir belgeyi iki kez görebilir veya atlayabilirsiniz. Aralık tabanlı sayfalama son görülen değerden devam ettiği için bu etkiye daha dayanıklıdır.
countDocuments ile estimatedDocumentCount arasında hangisini seçmeliyim?
Filtre varsa veya kesin sayı gerekiyorsa countDocuments(). Koleksiyonun tamamı için yaklaşık ve hızlı bir sayı yeterliyse estimatedDocumentCount(). İkincisinin temiz olmayan kapanış sonrasında ve sharded kümelerde sapabileceğini unutmayın.
limit(0) neden bütün belgeleri döndürüyor?
Çünkü belgelere göre limit(0) sınır koymamaya eşdeğerdir. Kullanıcıdan gelen sayfa boyutu sıfır olabiliyorsa, sorguya geçmeden önce en az 1 ve makul bir üst sınırla doğrulayın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: MongoDB öğrenme rehberi · find sorguları · explain ve sorgu planı · compound index ve ESR kuralı · aggregation pipeline · BSON veri tipleri