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.
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ı

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:
WITHlistesindeki sorgular hesaplanır (bkz. CTE yazısı).FROMlistesindeki bütün öğeler hesaplanır; birden fazla öğe varsa çapraz birleştirilir.WHEREkoşulunu sağlamayan satırlar atılır.GROUP BYve toplama fonksiyonları uygulanır, ardındanHAVINGgrupları süzer.- Seçim listesindeki ifadeler hesaplanarak çıktı satırları oluşur.
DISTINCTveyaDISTINCT ONtekrarları ayıklar.UNION,INTERSECT,EXCEPTbirden fazla sorgunun sonucunu birleştirir.ORDER BYsıralar,LIMIT/OFFSET/FETCHsonucun bir dilimini alır.FOR UPDATEgibi 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.
SELECT ad, fiyat, fiyat * 1.20 AS kdv_dahil
FROM urun
WHERE kategori = 'Kırtasiye'; 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:
SELECT 2 + 2; ?column?
----------
4
(1 row)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:
SELECT ad FROM urun WHERE kategori = NULL; -- hiçbir satır gelmez
SELECT ad FROM urun WHERE kategori IS NULL; 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.
SELECT ad, fiyat FROM urun
WHERE fiyat BETWEEN 50 AND 200 AND kategori IN ('Gıda', 'Elektronik'); 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:
SELECT ad, fiyat * 1.20 AS kdv_dahil FROM urun WHERE kdv_dahil > 100;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:
SELECT ad, stok FROM urun ORDER BY stok DESC;
SELECT ad, stok FROM urun ORDER BY stok DESC NULLS LAST; 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:
SELECT ad, fiyat * 1.20 AS kdv_dahil FROM urun ORDER BY kdv_dahil + 1;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.
SELECT id, ad FROM urun ORDER BY id LIMIT 3 OFFSET 3; 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.
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; 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:
SELECT DISTINCT kategori FROM urun ORDER BY kategori; 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:
SELECT DISTINCT ON (kategori) kategori, ad, fiyat
FROM urun
ORDER BY kategori, fiyat DESC; 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:
SELECT DISTINCT ON (kategori) kategori, ad FROM urun ORDER BY fiyat;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:
SELECT * FROM (VALUES (34, 'İstanbul'), (6, 'Ankara'), (35, 'İzmir')) AS il (plaka, ad)
ORDER BY plaka; 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

| Hata | Ne olur | Doğrusu |
|---|---|---|
| WHERE sutun = NULL | Hiç satır dönmez, çünkü sonuç NULL olur | WHERE sutun IS NULL |
| ORDER BY olmadan LIMIT | Her çalıştırmada farklı alt küme gelebilir | Benzersiz 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ı sanmak | x artan sıralanır | ORDER BY x DESC, y DESC |
| DISTINCT ON ile uyumsuz ORDER BY | DISTINCT ON expressions must match hatası | ORDER BY listesine önce DISTINCT ON ifadelerini yazın |
| Büyük OFFSET ile derin sayfalama | Atlanan satırlar yine de hesaplanır | Anahtar 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