MongoDB'de $group aşaması, aynı grup anahtarına sahip belgeleri tek bir belgede birleştirir ve her grup için toplam, ortalama, sayı gibi değerleri accumulator operatörleriyle hesaplar. Grup anahtarı _id alanında belirtilir: { $group: { _id: "$kategori", toplam: { $sum: "$tutar" } } } her kategori için tek bir belge ve o kategorinin toplam tutarını üretir. Bu, SQL'deki GROUP BY ifadesinin karşılığıdır.

Bu yazıda $group'un söz dizimini, en çok kullanılan accumulator'ları ($sum, $avg, $min, $max, $first, $last, $push, $addToSet, $count), eksik ve sayısal olmayan değerlerin nasıl ele alındığını, $count ve $sortByCount aşamalarını ve bellek sınırlarını ele alıyoruz. Pipeline'ın genel mantığı için önce aggregation pipeline yazısını okumanızı öneririz. Serinin tamamı MongoDB öğrenme rehberinde.

$group söz dizimi

$group söz dizimi
javascript
{
  $group: {
    _id: <ifade>,                          // grup anahtarı (zorunlu)
    <alan1>: { <accumulator1>: <ifade1> },
    ...
  }
}

_id zorunludur ve grup anahtarını belirler. Çoğunlukla bir alan yoludur ("$kategori"), ama birden fazla alandan oluşan bir belge veya bir ifadenin sonucu da olabilir. _id değeri null ya da başka bir sabit verilirse, $group bütün giriş belgeleri için tek bir belge döndürür; bu, koleksiyonun tamamı üzerinden toplam almanın yoludur. Çıktı belgelerinde _id alanı o grubun anahtarını taşır. Diğer alanlar isteğe bağlıdır ve accumulator operatörleriyle hesaplanır.

Akılda tutulması gereken iki temel kural vardır. Birincisi, $group çıktı belgelerini sıralamaz; belirli bir sırada görmek istiyorsanız arkasına bir $sort ekleyin. İkincisi, $group engelleyici (blocking) bir aşamadır: işleme başlamadan önce bütün girdinin gelmesini bekler. Bu, paralel işlemeyi azaltır ve büyük veri kümelerinde ciddi miktarda bellek kullanabilir.

Örnek veri ve temel gruplama

javascript
db.satislar.insertMany([
  { _id: 1, kategori: "kirtasiye",  urun: "kalem",    adet: 10, fiyat: 5,   tarih: ISODate("2026-01-05") },
  { _id: 2, kategori: "kirtasiye",  urun: "defter",   adet: 4,  fiyat: 20,  tarih: ISODate("2026-02-10") },
  { _id: 3, kategori: "elektronik", urun: "kulaklik", adet: 1,  fiyat: 300, tarih: ISODate("2026-02-11") },
  { _id: 4, kategori: "kirtasiye",  urun: "kalem",    adet: 6,  fiyat: 5,   tarih: ISODate("2026-03-01") },
  { _id: 5, kategori: "elektronik", urun: "kablo",    adet: 3,  fiyat: 40,  tarih: ISODate("2026-03-02") }
])

db.satislar.aggregate([
  { $group: {
      _id: "$kategori",
      ciro: { $sum: { $multiply: ["$adet", "$fiyat"] } },
      toplamAdet: { $sum: "$adet" },
      ortalamaFiyat: { $avg: "$fiyat" },
      satisSayisi: { $count: {} }
  } },
  { $sort: { ciro: -1 } }
])

Bu pipeline iki grup üretir. "elektronik" grubu için ciro 300 + 120 = 420, toplam adet 4, ortalama fiyat (300 + 40) / 2 = 170 ve satış sayısı 2 olur. "kirtasiye" grubu için ciro 50 + 80 + 30 = 160, toplam adet 20, ortalama fiyat (5 + 20 + 5) / 3 = 10 ve satış sayısı 3 olur. Sondaki $sort olmasaydı iki grubun hangi sırada geleceği garanti edilmezdi. Burada $sum içine bir ifade yazarak her belgenin satır tutarını hesaplayıp topladığımıza dikkat edin; accumulator'lar yalnızca alan yolu değil her geçerli ifadeyi kabul eder.

Accumulator operatörleri

Accumulator operatörleri: $sum: tipler ve eksik alanlar, $avg, $min ve $max, $first ve $last: sıralama şart, $push ve $addToSet
AccumulatorDöndürdüğüNot
$sumSayısal değerlerin toplamıSayısal olmayanları yok sayar; { $sum: 1 } sayma için
$avgSayısal değerlerin ortalamasıHiç sayısal değer yoksa null
$min / $maxEn küçük / en büyük değernull ve eksik değerleri yok sayar
$first / $lastGruptaki ilk / son belgenin ifade değeriAnlamlı olması için önce $sort gerekir
$pushDeğerlerin dizisiTekrarları korur, sıra giriş sırasıdır
$addToSetBenzersiz değerlerin dizisiEleman sırası tanımsızdır
$countGruptaki belge sayısıParametre almaz, { $sum: 1 } ile eşdeğer
$topN / $bottomNSıralamaya göre ilk / son n değersortBy ile kendi sıralamasını taşır
$firstN / $lastNGruptaki ilk / son n elemanBelge sırası tanımlıysa anlamlıdır
$median / $percentileMedyan / yüzdelik değerBelgelerde 7.0 ile eklendiği belirtilir
$mergeObjectsBelgeleri birleştirerek tek belgeGömülü belgeleri toplamak için

Listede ayrıca $stdDevPop, $stdDevSamp, $top, $bottom, $maxN, $minN ve kullanıcı tanımlı fonksiyon çalıştıran $accumulator gibi operatörler de bulunur. Aşağıda en sık kullanılanların uç durumlardaki davranışına bakıyoruz.

$sum: tipler ve eksik alanlar

$sum, sayısal ve sayısal olmayan değerlerin karıştığı bir alanda sayısal olmayanları yok sayar ve yalnızca sayıları toplar. Alan hiçbir belgede yoksa sonuç 0 olur. Farklı sayı tipleri karıştığında küçük tip büyük olana yükseltilir; tiplerin küçükten büyüğe sırası integer, long, double, decimal şeklindedir ve sonuç tipi de genellikle büyük olan giriş tipidir. Taşma durumunda integer sonuç long'a, long sonuç double'a yükseltilir; double veya decimal'da taşma ise pozitif veya negatif sonsuz olarak gösterilir ve tip yükseltmesi yapılmaz. Sayı tiplerinin ayrıntıları için BSON veri tipleri yazısına bakın.

$avg, $min ve $max

$avg eksik değerler dahil sayısal olmayan bütün değerleri yok sayar; hiçbir değer sayısal değilse null döndürür. İşlenen değer bir diziyse $avg onu sayısal olmayan bir değer olarak görür. $min ve $max ise null olan veya alanı eksik belgeleri yok sayar ve yalnızca dolu değerlere bakar; bütün belgelerde değer null veya eksikse sonuç null olur. $group içinde ifade bir diziye çözümlenirse $min diziyi gezmez, diziyi bir bütün olarak karşılaştırır.

$first ve $last: sıralama şart

$first ve $last, gruptaki ilk veya son belgenin ifade değerini döndürür. "İlk" ve "son" kavramları belgelerin gruba hangi sırayla girdiğine bağlıdır; bu yüzden anlamlı bir sonuç için $group'dan önce bir $sort aşaması eklenmelidir. İşlenen alan eksikse $first null döndürür; önceki aşamadan hiç belge gelmezse $group hiçbir şey döndürmez. Grup anahtarı alanı eksik olan belgeler ise _id: null grubunda toplanır.

javascript
// Her kategorinin en son satışı
db.satislar.aggregate([
  { $sort: { kategori: 1, tarih: -1 } },
  { $group: {
      _id: "$kategori",
      sonUrun: { $first: "$urun" },
      sonTarih: { $first: "$tarih" }
  } }
])

Bu sorgu "kirtasiye" için 1 Mart tarihli kalem satışını, "elektronik" için 2 Mart tarihli kablo satışını döndürür. Belgelere göre pipeline aynı alana göre sıralayıp grupluyorsa ve $group yalnızca $first veya $last kullanıyorsa, gruplanan alanda sıralamayla uyumlu bir indeks oluşturmak bazı durumlarda $group'un her grubun ilk veya son belgesini indeks üzerinden hızla bulmasını sağlar. Bu örnek için { kategori: 1, tarih: -1 } indeksi uygun adaydır.

Tarih sırasına göre "en son n kayıt" gibi daha esnek ihtiyaçlar için $topN kullanılabilir. n, sortBy ve output alanlarını alır; sıralamayı kendi içinde taşıdığı için önceden $sort gerektirmez. sortBy pipeline çıktısının sırasını etkilemez.

javascript
// Her kategoride en yüksek tutarlı 2 satış
db.satislar.aggregate([
  { $group: {
      _id: "$kategori",
      enIyiler: { $topN: {
          n: 2,
          sortBy: { fiyat: -1 },
          output: { urun: "$urun", fiyat: "$fiyat" }
      } }
  } }
])

$push ve $addToSet

$push her gruptaki değerleri bir diziye toplar ve tekrarları korur; dizideki sıra, belgelerin pipeline'a giriş sırasıdır. Belirli bir sıra istiyorsanız $group'dan önce $sort ekleyin. $addToSet ise yalnızca benzersiz değerleri tutar ve dizinin eleman sırası tanımsızdır. Değer bir diziyse $addToSet onu tek bir eleman olarak ekler. Değer bir belgeyse, bir belgenin kopya sayılması için var olan belgeyle aynı alanlara ve değerlere aynı sırada sahip olması gerekir.

javascript
db.satislar.aggregate([
  { $group: {
      _id: "$kategori",
      tumUrunler: { $push: "$urun" },        // tekrarlar dahil
      farkliUrunler: { $addToSet: "$urun" }  // benzersiz, sırası tanımsız
  } }
])

"kirtasiye" grubu için tumUrunler kalem, defter, kalem olmak üzere üç eleman içerirken farkliUrunler yalnızca kalem ve defter değerlerini içerir.

Birden fazla alana göre ve null ile gruplama

Grup anahtarı olarak bir belge verirseniz her benzersiz alan kombinasyonu ayrı bir grup olur. Bu, örneğin kategori ve ay bazında rapor almanın yoludur. Anahtarın içinde $year veya $month gibi ifadeler de kullanılabilir. _id: null ise bütün belgeleri tek grupta toplar.

javascript
// Kategori ve ay bazında ciro
db.satislar.aggregate([
  { $group: {
      _id: { kategori: "$kategori", ay: { $month: "$tarih" } },
      ciro: { $sum: { $multiply: ["$adet", "$fiyat"] } }
  } },
  { $sort: { "_id.kategori": 1, "_id.ay": 1 } }
])

// Tüm koleksiyon için tek satır özet
db.satislar.aggregate([
  { $group: { _id: null, toplamCiro: { $sum: { $multiply: ["$adet", "$fiyat"] } }, kayit: { $sum: 1 } } }
])

İkinci sorgu tek bir belge döndürür: toplam ciro 580 ve kayıt sayısı 5. Belgeler bu kullanımın SQL'deki SELECT SUM(...), COUNT(*) FROM tablo sorgusuna denk geldiğini gösterir. Bileşik anahtarlarla çalışırken sonraki aşamalarda alanlara "_id.kategori" gibi noktalı yolla erişirsiniz.

Gruplamadan sonra filtrelemek: HAVING karşılığı

SQL'de gruplanmış sonuçları HAVING ile filtrelersiniz. MongoDB'de bunun karşılığı, $group'dan sonra gelen bir $match aşamasıdır. Bu noktada filtre, giriş belgelerindeki alanlara değil, $group'un ürettiği alanlara uygulanır. $group'dan önce gelen $match ise SQL'deki WHERE gibidir ve gruplanacak belge sayısını azalttığı için performans açısından her zaman tercih edilmelidir.

javascript
db.satislar.aggregate([
  { $match: { tarih: { $gte: ISODate("2026-02-01") } } },   // WHERE
  { $group: { _id: "$kategori", ciro: { $sum: { $multiply: ["$adet", "$fiyat"] } } } },
  { $match: { ciro: { $gte: 200 } } }                       // HAVING
])

Bu sorgu 1 Şubat ve sonrasındaki satışları alır (2, 3, 4 ve 5 numaralı belgeler), kategoriye göre gruplar ve cirosu 200 ve üzerinde olan grubu bırakır. "kirtasiye" grubunun bu dönemdeki cirosu 80 + 30 = 110 olduğu için elenir, yalnızca 420 ciroyla "elektronik" kalır. Daha fazla SQL karşılığı için MongoDB ve SQL karşılaştırma yazısına bakın.

$count ve $sortByCount aşamaları

Sadece belge saymak istiyorsanız $count aşaması pratik bir kısayoldur. Bir metin alır; bu metin sayının yazılacağı çıktı alanının adıdır. Ad boş olamaz, $ ile başlayamaz ve nokta karakteri içeremez. Belgelere göre $count, _id: null ve { $sum: 1 } ile yapılan bir $group ile ardından _id'yi gizleyen bir $project dizisine eşdeğerdir. Önemli bir uç durum: giriş kümesi boşsa $count hiçbir sonuç döndürmez; sıfır içeren bir belge beklemeyin. Sonucun tipi, değeri tutabilen en küçük tiptir: sırasıyla Int32, Long ve Double.

Aşama olan $count, accumulator olan $count ile karıştırılmamalıdır. Accumulator biçimi { $count: {} } şeklinde yazılır, parametre almaz, $group, $bucket ve $bucketAuto içinde kullanılır ve { $sum: 1 } ile işlevsel olarak aynıdır.

$sortByCount ise gelen belgeleri bir ifadenin değerine göre gruplar, her gruptaki belge sayısını hesaplar ve sonucu sayıya göre azalan sıralar. Her çıktı belgesinde iki alan bulunur: grubun değeri olan _id ve count. Belgelere göre { $group: { _id: ifade, count: { $sum: 1 } } } ve ardından { $sort: { count: -1 } } dizisine eşdeğerdir. İfade olarak alan yolu veya bir belgeye çözümlenen bir ifade verilebilir, ancak doğrudan yazılmış bir belge (document literal) verilemez.

javascript
// Teslim edilen sipariş sayısı
db.siparisler.aggregate([
  { $match: { durum: "teslim" } },
  { $count: "teslimSayisi" }
])

// En çok satış yapılan kategoriler, sayıya göre azalan
db.satislar.aggregate([ { $sortByCount: "$kategori" } ])

Örnek veride $sortByCount önce 3 sayısıyla "kirtasiye", sonra 2 sayısıyla "elektronik" grubunu döndürür. Dizideki her etiketi ayrı saymak istiyorsanız önce $unwind ile diziyi açmanız gerekir; $unwind $lookup ve $unwind yazısında anlatılıyor. Filtreye uyan belgeleri aggregation yazmadan saymak için countDocuments de aynı $group ve $sum mantığını kullanır.

Bellek sınırları

$group aşaması 100 megabayttan fazla RAM gerektirirse MongoDB geçici dosyalara yazar; ancak allowDiskUse seçeneği false ise $group hata döndürür. Bu kural aşamanın kendi durumu için geçerlidir. $push ve $addToSet gibi accumulator'lar ise tek bir biriktirilmiş nesneyi bellek eşiğinin ötesine büyütebilir ve bunlar diske yazamaz. Biriktirilen nesne sınırı aşarsa allowDiskUse true olsa bile işlem hata verir. $push için bu sınırı internalQueryMaxPushBytes, $addToSet için internalQueryMaxAddToSetBytes sunucu parametresi belirler; varsayılan değerleri 104857600 bayt (100 megabayt) olup çalışma anında setParameter komutuyla artırılabilir.

Pratikte bu, "her müşterinin bütün siparişlerini bir diziye topla" gibi sınırsız büyüyebilecek gruplamaların riskli olduğu anlamına gelir. Diziyi $topN veya $firstN ile sınırlamak ya da yalnızca gereken alanları toplamak daha güvenlidir. Ayrıca grup sonuçlarındaki her belgenin 16 MiB BSON sınırını aşmaması gerekir; belgelerdeki $$ROOT ile bütün belgeleri gruplama örneği de bu sınırı hatırlatır.

javascript
// MongoDB 9.0'da hata verir: accumulator alan adı boş
db.satislar.aggregate([
  { $group: { _id: null, "": { $avg: "$fiyat" } } }
])

Sık yapılan hatalar

Sık yapılan hatalar: Grup çıktısının sıralı geleceğini varsaymak, $first ve $last'ı sıralamasız kullanmak, Alan yolunda dolar işaretini unutmak, Filtreyi gruplamadan sonra yapmak
  • Grup çıktısının sıralı geleceğini varsaymak: $group çıktıyı sıralamaz; arkasına $sort ekleyin.
  • $first ve $last'ı sıralamasız kullanmak: Hangi belgenin "ilk" olacağı belirsiz kalır.
  • Alan yolunda dolar işaretini unutmak: { $sum: "adet" } bir metin olduğu için sayısal değildir ve yok sayılır; doğrusu "$adet".
  • Filtreyi gruplamadan sonra yapmak: Grup anahtarıyla ilgili olmayan koşulları $group'dan önceki $match'e taşıyın.
  • Boş girdide $count'tan 0 beklemek: Giriş boşsa $count sonuç döndürmez; uygulamada sonucun olmamasını sıfır olarak yorumlayın.
  • Sınırsız $push: 100 MB sınırına takılır ve diske yazamaz.

Sık Sorulan Sorular

$group ile distinct() arasındaki fark nedir?

{ $group: { _id: "$alan" } } bir alanın farklı değerlerini döndürür ve belgelerde bu biçimdeki sorguların DISTINCT_SCAN planıyla çalışabileceği belirtilir. $group, distinct'ten farklı olarak her değer için ek hesaplamalar yapmanıza da izin verir.

Sayma için $sum: 1 mi, $count: {} mı?

İkisi $group içinde işlevsel olarak aynıdır. $count: {} niyeti daha açık anlatır, $sum: 1 ise belgelerdeki countDocuments(), $count ve $sortByCount eşdeğerliklerinde de kullanılan yazımdır.

Gruplanan alan bazı belgelerde yoksa ne olur?

O belgeler _id: null değerine sahip tek bir grupta toplanır. Bunu istemiyorsanız gruplamadan önce { alan: { $exists: true } } koşuluyla bir $match ekleyin.

$group indeks kullanır mı?

Önündeki $match ve $sort aşamaları indeks kullanabilir. $group'un kendisi yalnızca belirli durumlarda, örneğin yalnızca $first veya $last kullanılıp aynı alana göre sıralanıp gruplandığında ya da farklı değer aramalarında DISTINCT_SCAN ile indeksten yararlanır.

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 · aggregation pipeline · $lookup ve $unwind · $addFields ve $project · SQL karşılaştırma · MongoDB 9.0 yenilikleri