PostgreSQL'de veri okumanın temel aracı SELECT komutudur: SELECT ile hangi sütunları istediğinizi, FROM ile hangi tablodan okuyacağınızı, WHERE ile hangi satırların kalacağını, ORDER BY ile sıralamayı, LIMIT / OFFSET veya standart FETCH FIRST ile de kaç satır döneceğini belirlersiniz. Sorgunun yazılış sırası ile mantıksal işlenme sırası aynı değildir; bu fark, takma adların nerede kullanılabildiğini ve DISTINCT ON gibi yapıların nasıl davrandığını doğrudan belirler.

Bu yazıda tek tablolu sorguların bütün parçalarını resmî PostgreSQL 18 kılavuzuna dayanarak ve her örneği PostgreSQL 18.1 üzerinde çalıştırarak anlatıyoruz: seçim listesi ve sütun etiketleri, WHERE içindeki üç değerli mantık ve NULL tuzakları, NULLS FIRST / NULLS LAST, sayfalama, WITH TIES, DISTINCT ON ve VALUES listeleri. Birden çok tabloyu birleştirmek için JOIN rehberine, gruplama için GROUP BY yazısına bakabilirsiniz; SQL'e tamamen yeniyseniz önce SQL nedir yazısı iyi bir başlangıçtır.

Örnek tablo

Bütün örnekler aşağıdaki küçük ürün tablosu üzerinde çalışır. Bilerek bir ürünün kategorisini, bir başkasının stok bilgisini boş (NULL) bıraktık; çünkü sorgu davranışındaki sürprizlerin çoğu NULL değerlerden çıkar.

sql
CREATE TABLE urun (
    id       integer PRIMARY KEY,
    ad       text NOT NULL,
    kategori text,
    fiyat    numeric(10,2) NOT NULL,
    stok     integer
);
INSERT INTO urun VALUES
    (1, 'Defter A4',      'Kırtasiye', 45.00, 120),
    (2, 'Tükenmez Kalem', 'Kırtasiye', 12.50, 300),
    (3, 'Çay 1 kg',       'Gıda',     210.00,  40),
    (4, 'Türk Kahvesi',   'Gıda',      95.00, NULL),
    (5, 'USB Bellek',     'Elektronik',180.00, 25),
    (6, 'Kulaklık',       'Elektronik',450.00, 10),
    (7, 'Hediye Kutusu',  NULL,        60.00, 75),
    (8, 'Fosforlu Kalem', 'Kırtasiye', 12.50, 0);

SELECT sorgusunun mantıksal işlenme sırası

SELECT sorgusunun mantıksal işlenme sırası: WITH listesindeki sorgular hesaplanır (bkz. CTE yazısı)., FROM listesindeki bütün öğeler hesaplanır; birden fazla öğe varsa çapraz...

Kılavuzun SELECT başvuru sayfası, bir sorgunun kavramsal olarak hangi adımlarla işlendiğini sıralar. Bu sırayı bilmek, "neden bu takma adı burada kullanamıyorum" sorusunun cevabıdır:

  • WITH listesindeki sorgular hesaplanır (bkz. CTE yazısı).
  • FROM listesindeki bütün öğeler hesaplanır; birden fazla öğe varsa çapraz birleştirilir.
  • WHERE koşulunu sağlamayan satırlar atılır.
  • GROUP BY ve toplama fonksiyonları uygulanır, ardından HAVING grupları süzer.
  • Seçim listesindeki ifadeler hesaplanarak çıktı satırları oluşur.
  • DISTINCT veya DISTINCT ON tekrarları ayıklar.
  • UNION, INTERSECT, EXCEPT birden fazla sorgunun sonucunu birleştirir.
  • ORDER BY sıralar, LIMIT / OFFSET / FETCH sonucun bir dilimini alır.
  • FOR UPDATE gibi kilit yan tümceleri seçilen satırları kilitler (ayrıntı kilit yazısında).

Bu listeden çıkan pratik sonuç şudur: WHERE, seçim listesinden önce çalışır. Bu yüzden seçim listesinde verdiğiniz bir takma adı WHERE içinde kullanamazsınız; ifadeyi orada yeniden yazmanız gerekir. ORDER BY ise sonra çalıştığı için çıktı sütununun adını tek başına kabul eder.

Seçim listesi ve sütun etiketleri

En basit seçim listesi * işaretidir ve tablo ifadesinin ürettiği bütün sütunları döndürür. Bunun yerine virgülle ayrılmış ifadeler yazabilirsiniz: sütun adları, hesaplamalar, fonksiyon çağrıları, hatta hiçbir sütuna dayanmayan sabitler. Her ifade, sonuç tablosuna sanki yeni bir sanal sütun ekler ve her sonuç satırı için bir kez hesaplanır.

sql
SELECT ad, fiyat, fiyat * 1.20 AS kdv_dahil
FROM urun
WHERE kategori = 'Kırtasiye';
text
       ad       | fiyat | kdv_dahil
----------------+-------+-----------
 Defter A4      | 45.00 |   54.0000
 Tükenmez Kalem | 12.50 |   15.0000
 Fosforlu Kalem | 12.50 |   15.0000
(3 rows)

AS kdv_dahil, hesaplanan sütuna bir etiket verir. Etiket vermezseniz PostgreSQL bir ad seçer: basit sütun başvurusunda sütunun kendi adı, fonksiyon çağrısında fonksiyonun adı kullanılır; daha karmaşık ifadelerde ise ?column? gibi genel bir ad görürsünüz. Kılavuz, AS sözcüğünü atlamanın mümkün olduğunu ama etiket bir PostgreSQL anahtar sözcüğüyle çakışırsa hata alacağınızı belirtir; ileride eklenecek anahtar sözcüklere karşı da güvende kalmak için her zaman AS yazmanızı ya da etiketi çift tırnak içine almanızı önerir.

Birden fazla tabloyla çalışırken aynı adlı sütunları tablo adıyla nitelemeniz gerekir (urun.ad); belirli bir tablonun bütün sütunları için urun.* yazılabilir. * kullandığınızda sütunlara AS ile yeni ad veremezsiniz. Uygulama kodunda * yerine sütunları açıkça yazmak, tabloya sonradan sütun eklendiğinde istemcinin beklemediği veriyi almasını önler.

PostgreSQL, FROM olmadan da SELECT çalıştırmanıza izin verir; bu, basit bir hesaplamayı denemenin en kısa yoludur. Kılavuz bunu standart dışı bir kolaylık olarak not eder:

sql
SELECT 2 + 2;
text
 ?column?
----------
        4
(1 row)

WHERE: satırları süzmek ve üç değerli mantık

WHERE: satırları süzmek ve üç değerli mantık

WHERE içine boolean sonuç veren herhangi bir ifade yazabilirsiniz. Önemli ayrıntı şudur: bir satır yalnızca koşul doğru (true) çıktığında kalır; koşul yanlış ya da NULL (bilinmiyor) çıkarsa satır atılır. SQL üç değerli mantık kullanır ve sıradan karşılaştırma işleçleri, girdilerden biri NULL olduğunda doğru ya da yanlış değil, NULL döndürür. Kılavuzdaki örnekle 7 = NULL da 7 <> NULL da NULL sonuç verir.

Bu yüzden "kategorisi boş olan ürünler" sorgusunu eşittir işaretiyle yazmak hiçbir satır döndürmez. Doğru yol IS NULL yüklemidir:

sql
SELECT ad FROM urun WHERE kategori = NULL;   -- hiçbir satır gelmez
SELECT ad FROM urun WHERE kategori IS NULL;
text
 ad
----
(0 rows)

      ad
---------------
 Hediye Kutusu
(1 row)

Aynı mantık kategori <> 'Gıda' gibi koşullarda da geçerlidir: kategorisi NULL olan Hediye Kutusu bu sorgunun sonucunda yer almaz, çünkü karşılaştırmanın sonucu bilinmiyor olur. NULL değerini sıradan bir değer gibi karşılaştırmak istediğinizde IS DISTINCT FROM ve IS NOT DISTINCT FROM yüklemlerini kullanın; bunlar iki taraf da NULL olduğunda eşit, yalnızca biri NULL olduğunda farklı sonucunu verir.

Aralık ve liste testleri için BETWEEN ve IN sık kullanılır. BETWEEN uç değerleri dahil eder; yani a BETWEEN x AND y, a >= x AND a <= y ile aynıdır. Uçların sırası ters yazılırsa (BETWEEN 200 AND 50) sonuç boş olur; buna karşı BETWEEN SYMMETRIC uçları kendisi sıralar.

sql
SELECT ad, fiyat FROM urun
WHERE fiyat BETWEEN 50 AND 200 AND kategori IN ('Gıda', 'Elektronik');
text
      ad      | fiyat
--------------+--------
 Türk Kahvesi |  95.00
 USB Bellek   | 180.00
(2 rows)

Takma ad meselesine dönersek: WHERE seçim listesinden önce işlendiği için aşağıdaki sorgu hata verir. Gerçek hata çıktısı şöyledir:

sql
SELECT ad, fiyat * 1.20 AS kdv_dahil FROM urun WHERE kdv_dahil > 100;
text
ERROR:  column "kdv_dahil" does not exist
LINE 1: ...ECT ad, fiyat * 1.20 AS kdv_dahil FROM urun WHERE kdv_dahil ...
                                                             ^

Çözüm, ifadeyi WHERE fiyat * 1.20 > 100 biçiminde tekrar yazmak ya da sorguyu bir alt sorguya veya CTE'ye sarıp dış sorguda takma adı kullanmaktır.

ORDER BY, ASC/DESC ve NULLS FIRST / LAST

ORDER BY yazmazsanız satırlar "belirsiz" bir sırayla gelir. Kılavuz bu sıranın tarama ve birleştirme planına, verinin diskteki yerleşimine bağlı olduğunu ve buna güvenilmemesi gerektiğini açıkça söyler. Belirli bir sıra istiyorsanız her zaman ORDER BY yazmalısınız.

Varsayılan yön ASC (artan) yöndür. NULL değerler, sanki bütün dolu değerlerden büyükmüş gibi sıralanır; bu yüzden ASC sıralamada en sona, DESC sıralamada en başa düşer. Stoğa göre azalan sıralamada stok bilgisi boş olan Türk Kahvesi'nin listenin tepesine çıkması çoğu zaman istenen şey değildir:

sql
SELECT ad, stok FROM urun ORDER BY stok DESC;
SELECT ad, stok FROM urun ORDER BY stok DESC NULLS LAST;
text
       ad       | stok
----------------+------
 Türk Kahvesi   |
 Tükenmez Kalem |  300
 Defter A4      |  120
 Hediye Kutusu  |   75
 Çay 1 kg       |   40
 USB Bellek     |   25
 Kulaklık       |   10
 Fosforlu Kalem |    0
(8 rows)

       ad       | stok
----------------+------
 Tükenmez Kalem |  300
 Defter A4      |  120
 Hediye Kutusu  |   75
 Çay 1 kg       |   40
 USB Bellek     |   25
 Kulaklık       |   10
 Fosforlu Kalem |    0
 Türk Kahvesi   |
(8 rows)

Birden fazla sıralama ifadesi yazdığınızda, sonraki ifadeler yalnızca öncekilere göre eşit kalan satırları sıralar. Yön ve NULLS seçenekleri her ifadeye ayrı uygulanır: ORDER BY x, y DESC yazmak ORDER BY x ASC, y DESC anlamına gelir, x de azalan sıralanmaz. Bu, en sık yapılan okuma hatalarından biridir.

ORDER BY içinde seçim listesindeki bir sütunun etiketini ya da sıra numarasını (ORDER BY 2) kullanabilirsiniz; seçim listesinde olmayan bir sütuna göre sıralamak da serbesttir. Ancak çıktı etiketi tek başına yazılmalıdır; onu bir ifadenin içinde kullanamazsınız. Kılavuz bu kısıtlamanın belirsizliği azaltmak için konduğunu belirtir:

sql
SELECT ad, fiyat * 1.20 AS kdv_dahil FROM urun ORDER BY kdv_dahil + 1;
text
ERROR:  column "kdv_dahil" does not exist
LINE 1: ... ad, fiyat * 1.20 AS kdv_dahil FROM urun ORDER BY kdv_dahil ...
                                                             ^

Metin sütunları, sütuna uygulanan karşılaştırma kuralına (collation) göre sıralanır ve bu kural ORDER BY ad COLLATE "tr-TR-x-icu" gibi bir ifadeyle sorgu içinde değiştirilebilir. Türkçe harflerin sıralanması ayrı bir konu olduğu için collation ve Türkçe sıralama yazısında ayrıntılı ele alınıyor. Bu yazıdaki örnekler C.UTF-8 yerel ayarlı bir kümede çalıştırıldı.

LIMIT, OFFSET ve FETCH FIRST ile sayfalama

LIMIT n en fazla n satır döndürür; OFFSET m ise satırları döndürmeye başlamadan önce m satırı atlar. İkisi birlikte yazıldığında önce OFFSET kadar satır atlanır, sonra LIMIT sayılır. LIMIT ALL ya da NULL değerli bir LIMIT sınırsız anlamına gelir; OFFSET 0 veya NULL değerli OFFSET hiç yazılmamış gibi davranır.

sql
SELECT id, ad FROM urun ORDER BY id LIMIT 3 OFFSET 3;
text
 id |      ad
----+--------------
  4 | Türk Kahvesi
  5 | USB Bellek
  6 | Kulaklık
(3 rows)

Kılavuzun bu konudaki en önemli uyarısı şudur: LIMIT kullanırken sonucu benzersiz bir sıraya sokan bir ORDER BY şarttır. Sorgu planlayıcısı LIMIT ve OFFSET değerlerini plan seçerken hesaba katar; farklı sayfalar için farklı planlar, dolayısıyla farklı satır sıraları çıkabilir. Benzersiz bir sıra yoksa aynı satır iki sayfada birden görünebilir ya da hiç görünmeyebilir. Hatta aynı LIMIT sorgusu iki kez çalıştırıldığında farklı alt kümeler döndürebilir. Kılavuz bunun bir hata değil, SQL'in sıra garantisi vermemesinin doğal sonucu olduğunu vurgular.

Ayrıca OFFSET ile atlanan satırlar sunucu içinde yine de hesaplanmak zorundadır; büyük bir OFFSET değeri bu nedenle verimsiz olabilir. Derin sayfalarda "son görülen anahtardan sonrasını getir" mantığıyla çalışan anahtar tabanlı sayfalama, SQL sorgu optimizasyonu yazısında anlatılıyor.

SQL standardındaki FETCH FIRST ve WITH TIES

LIMIT ve OFFSET PostgreSQL'e özgü sözdizimidir (MySQL de kullanır). SQL:2008 standardı aynı işi OFFSET başlangıç ROWS FETCH FIRST n ROWS ONLY biçimiyle yapar ve PostgreSQL bunu da destekler. ROW / ROWS ve FIRST / NEXT sözcükleri anlamı değiştirmeyen dolgu sözcüklerdir; sayı yazılmazsa varsayılan 1'dir. Standardın sunduğu gerçek ek yetenek WITH TIES seçeneğidir: son sıradaki satırla ORDER BY açısından eşit olan bütün satırları da sonuca ekler. Bu seçenek için ORDER BY zorunludur.

sql
SELECT ad, fiyat FROM urun ORDER BY fiyat FETCH FIRST 1 ROW WITH TIES;
SELECT ad, fiyat FROM urun ORDER BY fiyat FETCH FIRST 1 ROW ONLY;
text
       ad       | fiyat
----------------+-------
 Fosforlu Kalem | 12.50
 Tükenmez Kalem | 12.50
(2 rows)

       ad       | fiyat
----------------+-------
 Tükenmez Kalem | 12.50
(1 row)

İkinci sorgunun hangi kalemi döndüreceği belirsizdir: iki ürün de 12.50 fiyatlıdır ve ORDER BY aralarında seçim yapmaz. Bizim çalıştırmamızda Tükenmez Kalem geldi, ama buna güvenemezsiniz. "En ucuz ürün hangisi" sorusuna eşitlikleri de gösteren dürüst cevap WITH TIES sürümüdür; tek bir satır istiyorsanız ORDER BY fiyat, id gibi ikinci bir anahtarla sırayı benzersiz yapın.

DISTINCT ve DISTINCT ON

SELECT DISTINCT, seçim listesi işlendikten sonra tamamen aynı olan satırları teke indirir. İki satır en az bir sütunda farklıysa farklı sayılır ve bu karşılaştırmada NULL değerler birbirine eşit kabul edilir; bu yüzden aşağıdaki sonuçta boş kategori tek bir satır olarak görünür:

sql
SELECT DISTINCT kategori FROM urun ORDER BY kategori;
text
  kategori
------------
 Elektronik
 Gıda
 Kırtasiye

(4 rows)

DISTINCT ON (ifade, ...) ise PostgreSQL'e özgü bir uzantıdır: verilen ifadelere göre eşit olan her satır kümesinden yalnızca ilk satırı tutar. "İlk" satırın hangisi olacağı, ORDER BY ile belirlenmezse öngörülemez. DISTINCT ON işlemi sıralamadan sonra yapıldığı için, her gruptan istediğiniz satırın başa gelmesini ORDER BY ile sağlarsınız. Her kategorinin en pahalı ürününü bulmak için:

sql
SELECT DISTINCT ON (kategori) kategori, ad, fiyat
FROM urun
ORDER BY kategori, fiyat DESC;
text
  kategori  |      ad       | fiyat
------------+---------------+--------
 Elektronik | Kulaklık      | 450.00
 Gıda       | Çay 1 kg      | 210.00
 Kırtasiye  | Defter A4     |  45.00
            | Hediye Kutusu |  60.00
(4 rows)

Kuralın katı bir tarafı vardır: DISTINCT ON ifadeleri, ORDER BY listesinin en soldaki ifadeleriyle eşleşmelidir. Önce kategoriye göre sıralamayı unutursanız PostgreSQL sorguyu reddeder:

sql
SELECT DISTINCT ON (kategori) kategori, ad FROM urun ORDER BY fiyat;
text
ERROR:  SELECT DISTINCT ON expressions must match initial ORDER BY expressions
LINE 1: SELECT DISTINCT ON (kategori) kategori, ad FROM urun ORDER B...
                            ^

Kılavuz DISTINCT ON'un standart olmadığını ve sonucu belirsiz kalabildiği için bazen kötü üslup sayıldığını da not eder. Aynı "her gruptan bir satır" işi standart SQL ile row_number() pencere fonksiyonu ve bir alt sorguyla yapılabilir; bu yöntem pencere fonksiyonları yazısında anlatılıyor. Eşitlik durumunda hangi satırın kalacağını belirlemek için ORDER BY listesine benzersiz bir sütun eklemeyi unutmayın.

VALUES ile sabit tablo üretmek

VALUES, diske tablo yazmadan sorgu içinde kullanılabilen bir "sabit tablo" üretir. Her parantezli liste bir satırdır; bütün listelerde eleman sayısı aynı olmalı ve karşılık gelen değerlerin tipleri uyumlu olmalıdır. PostgreSQL sütunlara varsayılan olarak column1, column2 adlarını verir; kılavuz bu adların standartta tanımlı olmadığını, bu yüzden takma ad listesiyle kendi adlarınızı vermenizin daha iyi olduğunu söyler:

sql
SELECT * FROM (VALUES (34, 'İstanbul'), (6, 'Ankara'), (35, 'İzmir')) AS il (plaka, ad)
ORDER BY plaka;
text
 plaka |    ad
-------+----------
     6 | Ankara
    34 | İstanbul
    35 | İzmir
(3 rows)

VALUES sözdizimsel olarak bir SELECT ile eşdeğer sayılır; bu yüzden UNION içinde kullanılabilir, ardına ORDER BY ve LIMIT eklenebilir. En yaygın kullanımı INSERT komutunun veri kaynağı olmaktır (bkz. INSERT, UPDATE, DELETE yazısı); ikinci yaygın kullanımı ise küçük bir eşleme tablosunu sorguya alt sorgu olarak katmaktır. Benzer bir kısayol olarak TABLE urun komutu da SELECT * FROM urun ile aynı işi yapar, ama WHERE veya toplama kabul etmez.

Sık yapılan hatalar

Sık yapılan hatalar: WHERE sutun = NULL, ORDER BY olmadan LIMIT, WHERE içinde çıktı takma adı, ORDER BY x, y DESC ile ikisinin de azalan sıralanacağını sanmak
HataNe olurDoğrusu
WHERE sutun = NULLHiç satır dönmez, çünkü sonuç NULL olurWHERE sutun IS NULL
ORDER BY olmadan LIMITHer çalıştırmada farklı alt küme gelebilirBenzersiz bir ORDER BY ekleyin
WHERE içinde çıktı takma adıcolumn does not exist hatasıİfadeyi tekrar yazın veya alt sorgu kullanın
ORDER BY x, y DESC ile ikisinin de azalan sıralanacağını sanmakx artan sıralanırORDER BY x DESC, y DESC
DISTINCT ON ile uyumsuz ORDER BYDISTINCT ON expressions must match hatasıORDER BY listesine önce DISTINCT ON ifadelerini yazın
Büyük OFFSET ile derin sayfalamaAtlanan satırlar yine de hesaplanırAnahtar tabanlı sayfalama

Bir de performans notu: sorgunuz yavaşsa sorunun tam olarak nerede olduğunu tahmin etmek yerine planı okuyun. EXPLAIN çıktısının nasıl yorumlanacağı EXPLAIN ANALYZE yazısında, hangi sütunlara indeks koymanın anlamlı olduğu indeks türleri yazısında ele alınıyor.

Sık Sorulan Sorular

LIMIT ile FETCH FIRST arasında fark var mı?

Sonuç açısından aynı işi yaparlar; LIMIT PostgreSQL ve MySQL'in kullandığı sözdizimidir, FETCH FIRST ise SQL standardıdır. Fark, FETCH FIRST ... WITH TIES seçeneğidir: son satırla eşit sıralanan satırları da getirir ve bunun LIMIT karşılığı yoktur.

NULL değerler sıralamada nereye düşer?

PostgreSQL NULL değerleri bütün dolu değerlerden büyük kabul eder: artan sıralamada sonda, azalan sıralamada başta görünürler. Bunu her sıralama ifadesi için NULLS FIRST ya da NULLS LAST yazarak değiştirebilirsiniz.

DISTINCT ile GROUP BY aynı şey mi?

Toplama fonksiyonu kullanmayan bir GROUP BY, kılavuzun da belirttiği gibi pratikte bir sütunun farklı değerlerini hesaplar ve DISTINCT ile aynı sonucu verir. Ancak sayım, toplam, ortalama gibi grup başına hesaplamalar gerekiyorsa GROUP BY kullanmanız gerekir.

Seçim listesindeki takma adı neden WHERE içinde kullanamıyorum?

Çünkü WHERE, seçim listesi hesaplanmadan önce işlenir; o aşamada takma ad henüz yoktur. Takma ad ORDER BY ve GROUP BY içinde tek başına kullanılabilir, WHERE ve HAVING içinde ise ifadeyi açık yazmanız gerekir.

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 · JOIN türleri ve LATERAL · GROUP BY ve aggregate fonksiyonlar · Window function rehberi · INSERT, UPDATE, DELETE ve RETURNING · SQL sorgu optimizasyonu