PostgreSQL'de GROUP BY, belirttiğiniz sütunlarda aynı değere sahip satırları tek bir grup satırına indirir; count, sum, avg, min, max, string_agg, array_agg gibi toplama (aggregate) fonksiyonları da her grup için tek bir değer hesaplar. WHERE gruplamadan önce satırları, HAVING ise gruplamadan sonra grupları süzer. FILTER yan tümcesi tek bir toplama fonksiyonunun girdisini daraltır; GROUPING SETS, ROLLUP ve CUBE ise ara toplamları ve genel toplamı aynı sorguda üretir.
Bu yazıda bu yapıların hepsini resmî PostgreSQL 18 kılavuzuna dayanarak ve PostgreSQL 18.1 üzerinde çalıştırılmış örneklerle anlatıyoruz: NULL değerlerin toplama fonksiyonlarındaki davranışı, "must appear in the GROUP BY clause" hatası ve işlevsel bağımlılık, sıralı toplama, boş kümelerde sum sonucunun neden NULL olduğu ve GROUPING() fonksiyonu. Satırları teke indirmeden grup bazında hesap yapmak istiyorsanız window function yazısı daha uygun olabilir.
Örnek tablo

İl ve kategori bazında satışları tutan küçük bir tablo kullanıyoruz. Bir satırın tutarı bilinmiyor (NULL), bir satır da iade edilmiş durumda. Sonuçlardaki metin sıralaması C.UTF-8 yerel ayarına göredir; bu yüzden "Çay" gibi Türkçe harfle başlayan değerler ASCII harflerden sonra gelir. Türkçe sıralama için collation yazısına bakın.
CREATE TABLE satis (
id integer PRIMARY KEY,
il text NOT NULL,
kategori text NOT NULL,
urun text NOT NULL,
adet integer NOT NULL,
tutar numeric(10,2),
iade boolean NOT NULL DEFAULT false
);
INSERT INTO satis VALUES
(1, 'Ankara', 'Kırtasiye', 'Defter', 10, 450.00, false),
(2, 'Ankara', 'Gıda', 'Çay', 3, 630.00, false),
(3, 'İstanbul', 'Kırtasiye', 'Kalem', 40, 500.00, false),
(4, 'İstanbul', 'Kırtasiye', 'Defter', 5, 225.00, true),
(5, 'İstanbul', 'Gıda', 'Kahve', 2, 190.00, false),
(6, 'İzmir', 'Gıda', 'Çay', 1, 210.00, false),
(7, 'İzmir', 'Kırtasiye', 'Kalem', 12, 150.00, false),
(8, 'İzmir', 'Gıda', 'Kahve', 4, NULL, false);Temel toplama fonksiyonları ve NULL
Toplama fonksiyonu, birden çok girdi satırından tek bir sonuç hesaplar. GROUP BY olmadan kullanıldığında bütün tablo tek bir grup sayılır:
SELECT count(*) AS satir, count(tutar) AS tutarli, count(DISTINCT urun) AS farkli_urun,
sum(tutar) AS toplam, avg(tutar) AS ortalama, min(tutar), max(tutar)
FROM satis; satir | tutarli | farkli_urun | toplam | ortalama | min | max
-------+---------+-------------+---------+----------------------+--------+--------
8 | 7 | 4 | 2355.00 | 336.4285714285714286 | 150.00 | 630.00
(1 row)Bu tek satır, toplama fonksiyonlarının en önemli kuralını gösteriyor: yerleşik toplama fonksiyonlarının çoğu NULL girdileri yok sayar. count(*) bütün satırları (8), count(tutar) ise yalnızca tutarı dolu satırları (7) sayar; count(DISTINCT urun) farklı ve NULL olmayan değerlerin sayısını verir. Ortalama da 8 değil 7 satır üzerinden hesaplanmıştır: tutarı bilinmeyen satır sıfır sayılmaz, hesaba hiç girmez. Kılavuzdaki tabloya göre integer ve numeric girdilerde avg sonucu numeric tipindedir, bu yüzden uzun bir ondalık görüyorsunuz; gösterim için round(avg(tutar), 2) kullanabilirsiniz.
GROUP BY ile gruplama
GROUP BY, listelenen sütunların hepsinde aynı değere sahip satırları bir grup satırında birleştirir; sütunların listede hangi sırayla yazıldığı önemli değildir. Toplama fonksiyonları her grup için ayrı hesaplanır:
SELECT il, count(*) AS satis_sayisi, sum(tutar) AS ciro
FROM satis
GROUP BY il
ORDER BY il; il | satis_sayisi | ciro
----------+--------------+---------
Ankara | 2 | 1080.00
İstanbul | 3 | 915.00
İzmir | 3 | 360.00
(3 rows)Gruplanmış bir sorguda seçim listesinde yalnızca gruplama sütunları ve toplama ifadeleri yer alabilir. Kılavuzun açıkladığı neden basittir: bir gruptaki satırların urun değerleri farklıysa grup satırı için tek bir değer seçilemez. Bu kuralı çiğnediğinizde PostgreSQL'in en tanıdık hata mesajlarından birini alırsınız:
SELECT il, urun, sum(tutar) FROM satis GROUP BY il;ERROR: column "satis.urun" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT il, urun, sum(tutar) FROM satis GROUP BY il;
^Çözüm ya urun sütununu da GROUP BY listesine eklemek (böylece il ve ürün çiftleri gruplanır) ya da onu bir toplama fonksiyonunun içine almaktır, örneğin string_agg(urun, ', '). PostgreSQL ayrıca standart SQL'in ötesine geçerek seçim listesindeki çıktı sütunlarının adına ya da sıra numarasına göre gruplamaya izin verir (GROUP BY 1) ve basit sütun adları yerine ifadelerle gruplamayı da kabul eder. Adı hem girdi sütunu hem çıktı sütunu olarak yorumlanabilecek bir belirsizlik varsa GROUP BY girdi sütununu seçer; ORDER BY ise tersine çıktı sütununu seçer.
İşlevsel bağımlılık: birincil anahtara göre gruplama
Kural bir istisnayla yumuşar. Gruplanan sütunlar bir tablonun birincil anahtarını içeriyorsa, o tablonun diğer sütunları birincil anahtara işlevsel olarak bağımlıdır ve GROUP BY içinde yazılmadan seçilebilir; çünkü her anahtar değeri için yalnızca bir ad, bir il vardır. Müşteri başına sipariş özetinde yalnızca m.id ile gruplamak yeterlidir:
CREATE TABLE musteri (id integer PRIMARY KEY, ad text, il text);
CREATE TABLE siparis (id integer PRIMARY KEY, musteri_id integer REFERENCES musteri, tutar numeric);
INSERT INTO musteri VALUES (1, 'Ayşe', 'İzmir'), (2, 'Mehmet', 'Ankara'), (3, 'Elif', 'Bursa');
INSERT INTO siparis VALUES (10, 1, 120), (11, 1, 80), (12, 2, 300);
SELECT m.id, m.ad, m.il, count(s.id) AS siparis, coalesce(sum(s.tutar), 0) AS toplam
FROM musteri m
LEFT JOIN siparis s ON s.musteri_id = m.id
GROUP BY m.id
ORDER BY m.id; id | ad | il | siparis | toplam
----+--------+--------+---------+--------
1 | Ayşe | İzmir | 2 | 200
2 | Mehmet | Ankara | 1 | 300
3 | Elif | Bursa | 0 | 0
(3 rows)Bu örnekte iki ayrıntı daha önemli. Siparişi olmayan Elif için count(*) değil count(s.id) kullandık: LEFT JOIN Elif için sağ tarafı NULL olan bir satır üretir, count(*) bu satırı 1 olarak sayardı. sum ise satır olmadığında NULL döndürdüğü için coalesce ile 0'a çevrildi. Kılavuzun uyumluluk notuna göre PostgreSQL işlevsel bağımlılığı yalnızca tablonun birincil anahtarı GROUP BY listesindeyken tanır. PostgreSQL 18 sürüm notları ise planlayıcının, bir benzersiz indeksin bütün sütunları gruplamada yer aldığında aynı tablonun diğer gruplama sütunlarını gereksiz sayıp düşürebildiğini belirtir; bu bir plan iyileştirmesidir, hangi sorguların yazılabileceğini değiştirmez.
WHERE ve HAVING farkı

Kılavuz temel farkı şöyle koyar: WHERE, gruplar ve toplamlar hesaplanmadan önce girdi satırlarını seçer, yani hangi satırların toplamaya gireceğini belirler. HAVING ise gruplar ve toplamlar hesaplandıktan sonra grup satırlarını seçer. İadeleri dışarıda bırakıp cirosu 700'ü aşan illeri bulmak için iki yan tümceyi birlikte kullanırız:
SELECT il, sum(tutar) AS ciro
FROM satis
WHERE NOT iade
GROUP BY il
HAVING sum(tutar) > 700
ORDER BY ciro DESC; il | ciro
--------+---------
Ankara | 1080.00
(1 row)İstanbul'un toplam cirosu 915 idi, ama iade edilen 225 liralık satış WHERE ile çıkarılınca 690'a düştü ve HAVING eşiğini geçemedi. Bu sıranın sonucu olarak WHERE içinde toplama fonksiyonu kullanılamaz; toplamaya hangi satırların gireceğine bir toplamayla karar vermek anlamsızdır:
SELECT il FROM satis WHERE sum(tutar) > 700 GROUP BY il;ERROR: aggregate functions are not allowed in WHERE
LINE 1: SELECT il FROM satis WHERE sum(tutar) > 700 GROUP BY il;
^Seçim listesindeki takma adı HAVING içinde kullanmak da mümkün değildir; kılavuz, çıktı sütun adlarının ORDER BY ve GROUP BY içinde kullanılabildiğini ama WHERE ve HAVING içinde ifadenin açıkça yazılması gerektiğini belirtir:
SELECT il, sum(tutar) AS ciro FROM satis GROUP BY il HAVING ciro > 700;ERROR: column "ciro" does not exist
LINE 1: ... sum(tutar) AS ciro FROM satis GROUP BY il HAVING ciro > 700...
^HAVING tek başına da bir sorguyu gruplanmış hale getirir: GROUP BY olmasa bile bütün satırlar tek grup sayılır ve koşul doğruysa bir, değilse sıfır satır döner.
string_agg, array_agg ve toplama içinde ORDER BY
Bazı toplama fonksiyonlarının sonucu girdi sırasına bağlıdır: array_agg, string_agg, json_agg bunlardandır. Girdi sırası varsayılan olarak belirsizdir; kılavuz sırayı, fonksiyonun son argümanından sonra yazılan bir ORDER BY ile belirlemenizi önerir. DISTINCT ile birlikte kullanıldığında ORDER BY ifadeleri yalnızca DISTINCT listesindeki sütunlara başvurabilir:
SELECT il,
string_agg(DISTINCT urun, ', ' ORDER BY urun) AS urunler,
array_agg(adet ORDER BY adet DESC) AS adetler
FROM satis
GROUP BY il
ORDER BY il; il | urunler | adetler
----------+----------------------+----------
Ankara | Defter, Çay | {10,3}
İstanbul | Defter, Kahve, Kalem | {40,5,2}
İzmir | Kahve, Kalem, Çay | {12,4,1}
(3 rows)array_agg, NULL girdileri de diziye ekler; string_agg ise NULL değerleri atlar. Dizilerle çalışmanın ayrıntıları PostgreSQL dizi tipi yazısında, JSON toplama fonksiyonları JSONB yazısında ele alınıyor.
FILTER: tek bir toplamayı koşullu yapmak
FILTER (WHERE ...), yalnızca bağlı olduğu toplama fonksiyonunun girdisinden satır çıkarır; aynı sorgudaki diğer toplamalar bütün satırları görmeye devam eder. Böylece tek bir geçişte "toplam satış", "iade sayısı" ve "gıda cirosu" gibi farklı koşullu sayıları yan yana alabilirsiniz:
SELECT il,
count(*) AS toplam,
count(*) FILTER (WHERE iade) AS iade_sayisi,
sum(tutar) FILTER (WHERE kategori = 'Gıda') AS gida_cirosu
FROM satis
GROUP BY il
ORDER BY il; il | toplam | iade_sayisi | gida_cirosu
----------+--------+-------------+-------------
Ankara | 2 | 0 | 630.00
İstanbul | 3 | 1 | 190.00
İzmir | 3 | 0 | 210.00
(3 rows)İzmir'in gıda cirosu 210: tutarı NULL olan kahve satırı filtreden geçse bile sum tarafından yok sayıldı. FILTER olmadan aynı sonucu sum(CASE WHEN ... THEN tutar END) ile de alabilirsiniz, ama FILTER niyeti daha açık ifade eder. Kılavuz ayrıca bütün toplama fonksiyonlarının HAVING ve seçim listesindeki skaler ifadelerden önce hesaplandığını hatırlatır; yani bir CASE ifadesiyle bir toplamanın hesaplanmasını atlatamazsınız.
Boş kümede sum NULL döndürür
Kılavuzun açıkça uyardığı bir ayrıntı: count dışındaki toplama fonksiyonları hiç satır seçilmediğinde NULL döndürür. Yani satırı olmayan bir küme için sum sıfır değil NULL, array_agg de boş dizi değil NULL verir. Bursa'da hiç satış yok:
SELECT sum(tutar) AS toplam, count(*) AS adet, coalesce(sum(tutar), 0) AS guvenli
FROM satis WHERE il = 'Bursa'; toplam | adet | guvenli
--------+------+---------
| 0 | 0
(1 row)Raporda boş hücre yerine 0 görmek istiyorsanız kılavuzun önerdiği gibi coalesce kullanın. Bir başka not: GROUP BY yokken bu sorgu satır bulunmasa bile tek bir satır döndürür; GROUP BY il ekleseydiniz hiç satır dönmezdi, çünkü oluşacak grup yoktur.
GROUPING SETS, ROLLUP ve CUBE
Gruplama kümeleri, aynı veriyi birden fazla gruplama ölçütüne göre ayrı ayrı toplayıp sonuçları tek bir tabloda verir. Kılavuza göre etkisi, her gruplama kümesi için ayrı bir GROUP BY sorgusu yazıp bunları UNION ALL ile birleştirmeye eşdeğerdir. Bir kümede yer almayan gruplama sütunları, o kümeden gelen satırlarda NULL olarak görünür. Boş küme () bütün satırların genel toplamını üretir:
SELECT kategori, il, sum(adet)
FROM satis
GROUP BY GROUPING SETS ((kategori), (il), ())
ORDER BY kategori, il; kategori | il | sum
-----------+----------+-----
Gıda | | 10
Kırtasiye | | 67
| Ankara | 13
| İstanbul | 47
| İzmir | 17
| | 77
(6 rows)ROLLUP (a, b), listenin bütün öneklerini üretir: (a, b), (a) ve (). Bu, il içinde kategori, il ara toplamı ve genel toplam gibi hiyerarşik raporlar için idealdir. Gerçek bir NULL değer ile "bu sütun bu satırda toplanmadı" anlamındaki NULL değeri ayırt etmek için GROUPING() fonksiyonu kullanılır. Fonksiyon bir bit maskesi döndürür: en sağdaki argüman en düşük bite karşılık gelir, ilgili ifade o satırın gruplama kümesinde yoksa bit 1 olur:
SELECT il, kategori, sum(tutar) AS ciro, GROUPING(il, kategori) AS g
FROM satis
GROUP BY ROLLUP (il, kategori)
ORDER BY il, kategori; il | kategori | ciro | g
----------+-----------+---------+---
Ankara | Gıda | 630.00 | 0
Ankara | Kırtasiye | 450.00 | 0
Ankara | | 1080.00 | 1
İstanbul | Gıda | 190.00 | 0
İstanbul | Kırtasiye | 725.00 | 0
İstanbul | | 915.00 | 1
İzmir | Gıda | 210.00 | 0
İzmir | Kırtasiye | 150.00 | 0
İzmir | | 360.00 | 1
| | 2355.00 | 3
(10 rows)g = 1 satırları il ara toplamlarıdır (kategori gruplanmadı), g = 3 ise hem il hem kategori gruplanmadığı için genel toplamdır. CUBE (a, b) ise bütün alt kümeleri, yani (a, b), (a), (b) ve () kümelerini üretir; böylece kategori bazındaki ara toplamlar da gelir:
SELECT il, kategori, sum(adet) AS adet
FROM satis
GROUP BY CUBE (il, kategori)
ORDER BY il NULLS LAST, kategori NULLS LAST; il | kategori | adet
----------+-----------+------
Ankara | Gıda | 3
Ankara | Kırtasiye | 10
Ankara | | 13
İstanbul | Gıda | 2
İstanbul | Kırtasiye | 45
İstanbul | | 47
İzmir | Gıda | 5
İzmir | Kırtasiye | 12
İzmir | | 17
| Gıda | 10
| Kırtasiye | 67
| | 77
(12 rows)Birden fazla gruplama öğesi aynı GROUP BY içinde yazılırsa son küme listesi bunların kartezyen çarpımı olur ve tekrarlı kümeler oluşabilir; kılavuz bunları GROUP BY DISTINCT ile ayıklayabileceğinizi gösterir. Bu, SELECT DISTINCT ile aynı şey değildir: tekrar eden kümeler kaldırılır, ama gruplanmamış bir sütundaki gerçek NULL değer yine gruplama NULL değerinden ayırt edilemeyebilir. GROUP BY içinde (a, b) yazımı satır kurucusu değil ifade listesi olarak yorumlanır; gerçekten satır kurucusu gerekiyorsa ROW(a, b) yazın.
Sıralı küme fonksiyonları: medyan ve mod
Bazı toplamalar ancak sıralı bir girdi üzerinde anlamlıdır; bunlara sıralı küme (ordered-set) fonksiyonları denir ve sıralama WITHIN GROUP (ORDER BY ...) ile verilir. percentile_cont(0.5) medyanı, gerekirse komşu değerler arasında ara değer bularak hesaplar; mode() en sık değeri döndürür:
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY adet) AS medyan,
mode() WITHIN GROUP (ORDER BY urun) AS en_sik_urun
FROM satis; medyan | en_sik_urun
--------+-------------
4.5 | Defter
(1 row)Sekiz adet değerinin ortadaki ikisi 4 ve 5 olduğu için medyan 4.5 çıktı. Mod sonucu ise dikkatle okunmalı: dört ürünün her biri ikişer kez satıldı. Kılavuz, eşit sıklıkta birden çok değer varsa mode() fonksiyonunun keyfi olarak ilkini seçtiğini belirtir; buradaki "Defter" bu yüzden tek doğru cevap değildir.
Son olarak PostgreSQL 18 sürüm notlarına göre min() ve max() artık diziler ve bileşik tipler üzerinde de çalışıyor. Diziler eleman eleman karşılaştırıldığı için max(ARRAY[adet, id]) önce adede, eşitlikte id değerine bakar:
SELECT max(ARRAY[adet, id]) AS en_buyuk_dizi FROM satis; en_buyuk_dizi
---------------
{40,3}
(1 row)Sık yapılan hatalar

| Hata | Sonuç | Çözüm |
|---|---|---|
| Gruplanmamış sütunu seçmek | must appear in the GROUP BY clause hatası | GROUP BY listesine ekleyin veya toplama içine alın |
| WHERE içinde sum, count | aggregate functions are not allowed in WHERE | Koşulu HAVING içine taşıyın |
| LEFT JOIN sonrası count(*) | Eşleşmeyen satır 1 sayılır | count(sag_tablo.id) kullanın |
| Boş kümede sum beklemek | NULL döner | coalesce(sum(x), 0) |
| avg ile NULL satırların sıfır sayılacağını sanmak | NULL satırlar hesaba girmez | Gerekirse avg(coalesce(x, 0)) |
| string_agg sırasına güvenmek | Sıra belirsizdir | Fonksiyon içine ORDER BY yazın |
Bire çok birleştirmeden sonra toplam almak da sık bir tuzaktır: sipariş satırlarıyla birleştirilmiş bir müşteri tablosunda müşteriye ait sabit bir değeri toplamak onu sipariş sayısı kadar katlar. Böyle durumlarda önce alt sorguda ya da CTE içinde toplayıp sonra birleştirin. Birleştirme türleri için JOIN yazısına bakın. Kılavuz ayrıca bütün tablo üzerinde count(*) çalıştırmanın tablo boyutuyla orantılı iş gerektirdiğini, çünkü tablonun ya da bütün satırları kapsayan bir indeksin taranması gerektiğini not eder.
Sık Sorulan Sorular
count(*) ile count(sutun) farkı nedir?
count(*) girdi satırlarının sayısını verir. count(sutun) yalnızca o sütunun NULL olmadığı satırları sayar, count(DISTINCT sutun) ise farklı ve NULL olmayan değerleri sayar.
HAVING içinde takma ad kullanabilir miyim?
Hayır. HAVING içinde toplama ifadesini yeniden yazmanız gerekir: HAVING sum(tutar) > 700. Takma ad ORDER BY içinde kullanılabilir.
GROUP BY ile DISTINCT arasında fark var mı?
Toplama fonksiyonu olmayan bir GROUP BY, kılavuzun da belirttiği gibi bir sütunun farklı değerlerini hesaplar ve DISTINCT ile aynı sonucu verir. Grup başına sayı, toplam veya liste gerekiyorsa GROUP BY kullanılır.
ROLLUP sonucundaki NULL satırlarını nasıl etiketlerim?
GROUPING(sutun) değeri 1 olan satırlar o sütunun toplandığı ara toplam satırlarıdır. CASE WHEN GROUPING(il) = 1 THEN 'Toplam' ELSE il END gibi bir ifadeyle bu satırlara okunur bir etiket verebilirsiniz.
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: PostgreSQL öğrenme rehberi · SELECT, WHERE, ORDER BY, LIMIT · JOIN türleri ve LATERAL · Window function rehberi · WITH ve CTE · Dizi tipi ve array_agg