PostgreSQL'de veri değiştirmenin üç temel komutu vardır: INSERT yeni satır ekler, UPDATE mevcut satırların sütunlarını değiştirir, DELETE satırları siler. Üçü de isteğe bağlı bir RETURNING yan tümcesi alır; bu sayede değişen satırları ayrı bir SELECT çalıştırmadan geri okuyabilirsiniz. PostgreSQL 18 ile RETURNING içinde old ve new takma adlarıyla bir satırın hem eski hem yeni değerini aynı anda döndürmek mümkün hale geldi.
Bu yazıda çok satırlı INSERT, INSERT ... SELECT, başka bir tabloya dayanarak UPDATE ... FROM ve DELETE ... USING, sütun listeli atama, komut etiketlerinin anlamı ve RETURNING'in bütün biçimleri resmî kılavuza dayanarak ve PostgreSQL 18.1 üzerinde çalıştırılmış örneklerle anlatılıyor. "Varsa güncelle, yoksa ekle" senaryosu, yani ON CONFLICT ve MERGE, ayrı bir konu olarak UPSERT ve MERGE yazısında ele alınıyor.
Örnek tablo

Örneklerde kimliği otomatik üretilen bir müşteri tablosu kullanıyoruz. GENERATED ALWAYS AS IDENTITY sütunlarının ayrıntısı ve serial ile farkı identity ve sequence yazısının konusu; burada yalnızca id değerini veritabanının ürettiğini bilmeniz yeterli.
CREATE TABLE musteri (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ad text NOT NULL,
il text NOT NULL,
puan integer NOT NULL DEFAULT 0,
durum text NOT NULL DEFAULT 'aktif'
);INSERT: tek ve çok satırlı ekleme
Kılavuzun temel kuralı şudur: veri her zaman tam satır olarak eklenir. Bazı sütunların değerini bilmiyorsanız bile bir satır oluşur; yazmadığınız sütunlar tanımlı varsayılan değerleriyle, varsayılan yoksa NULL ile doldurulur. Sütun listesini yazmadan INSERT INTO musteri VALUES (...) biçimini de kullanabilirsiniz, ancak o zaman değerleri tablodaki sütun sırasıyla vermeniz gerekir. Kılavuz, sütun adlarını her zaman açıkça yazmayı birçok kullanıcının iyi bir alışkanlık saydığını belirtir; tabloya ileride sütun eklendiğinde ya da sıra değiştiğinde sorgunuz bozulmaz.
Tek komutla birden fazla satır eklemek için VALUES listesine virgülle ayrılmış birden çok parantez yazarsınız. Aşağıdaki komut üç müşteri ekliyor ve RETURNING ile veritabanının ürettiği kimlikleri ve varsayılan değerleri geri alıyor:
INSERT INTO musteri (ad, il) VALUES
('Ayşe Yılmaz', 'İzmir'),
('Mehmet Kaya', 'Ankara'),
('Elif Demir', 'İstanbul')
RETURNING id, ad, puan, durum; id | ad | puan | durum
----+-------------+------+-------
1 | Ayşe Yılmaz | 0 | aktif
2 | Mehmet Kaya | 0 | aktif
3 | Elif Demir | 0 | aktif
(3 rows)
INSERT 0 3Son satırdaki INSERT 0 3 komut etiketidir. İkinci sayı eklenen (ya da ON CONFLICT DO UPDATE kullanıldıysa güncellenen) satır sayısıdır. Baştaki 0, eskiden WITH OIDS tablolarında eklenen satırın OID değerini gösteren alandır; kılavuza göre artık bu tür tablolar oluşturulamadığı için her zaman 0 yazılır.
Bir sütuna varsayılan değerini açıkça vermek için değer yerine DEFAULT yazabilirsiniz; bütün sütunları varsayılanla doldurmak için de INSERT INTO tablo DEFAULT VALUES biçimi vardır. Identity sütununda DEFAULT, ilgili sequence'ten yeni bir değer üretilmesi anlamına gelir. Değerin tipi sütunla uyuşmuyorsa PostgreSQL otomatik tip dönüşümünü dener; dönüşüm mümkün değilse komut hata verir.
INSERT ... SELECT: sorgu sonucunu eklemek
VALUES yerine bir SELECT sorgusu yazarsanız, sorgunun döndürdüğü bütün satırlar eklenir; sorgu hiç satır döndürmezse hiçbir şey eklenmez ve bu bir hata değildir. Böylece sorgu dilinin bütün gücünü (süzme, birleştirme, hesaplama) eklenecek satırları üretmek için kullanabilirsiniz. Örnekte bir aday tablosundan yalnızca fuarda tanışılan kişileri 10 başlangıç puanıyla müşteri yapıyoruz:
CREATE TABLE aday (ad text, il text, kaynak text);
INSERT INTO aday VALUES ('Can Öztürk', 'Bursa', 'fuar'), ('Zeynep Arslan', 'İzmir', 'web'), ('Deniz Şahin', 'Ankara', 'fuar');
INSERT INTO musteri (ad, il, puan)
SELECT ad, il, 10 FROM aday WHERE kaynak = 'fuar'
RETURNING id, ad, il, puan; id | ad | il | puan
----+-------------+--------+------
4 | Can Öztürk | Bursa | 10
5 | Deniz Şahin | Ankara | 10
(2 rows)
INSERT 0 2Kılavuz RETURNING'in özellikle INSERT ... SELECT ile çok kullanışlı olduğunu vurgular: hangi satırların eklendiğini ve onlara hangi kimliklerin verildiğini ayrıca sorgulamadan öğrenirsiniz. Sorgudaki sütun sayısı hedef sütun listesiyle eşleşmelidir; değerler soldan sağa sırayla eşlenir, ad benzerliğine bakılmaz.
UPDATE: satırları güncellemek

UPDATE için üç bilgi gerekir: hangi tablo ve sütun, yeni değer ne ve hangi satırlar. SQL satırlar için genel bir benzersiz kimlik sağlamadığından hangi satırın güncelleneceğini koşulla tarif edersiniz; tek bir satırı güvenle hedeflemenin yolu birincil anahtarı kullanmaktır. Koşula uyan satır yoksa bu hata sayılmaz, komut UPDATE 0 döndürür. WHERE yan tümcesini hiç yazmazsanız tablodaki bütün satırlar güncellenir.
Yeni değer sabit olmak zorunda değildir; satırın mevcut değerlerine dayanan herhangi bir ifade olabilir. SET içindeki eşittir işareti atamadır, WHERE içindeki ise karşılaştırmadır. PostgreSQL 18'de RETURNING listesinde old.sutun ve new.sutun yazarak değişikliğin öncesini ve sonrasını birlikte alabilirsiniz:
UPDATE musteri SET puan = puan + 50
WHERE il = 'Ankara'
RETURNING ad, old.puan AS eski_puan, new.puan AS yeni_puan; ad | eski_puan | yeni_puan
-------------+-----------+-----------
Mehmet Kaya | 0 | 50
Deniz Şahin | 10 | 60
(2 rows)
UPDATE 2UPDATE komutunun etiketindeki sayı güncellenen satır sayısıdır ve kılavuza göre değeri değişmemiş olsa bile eşleşen satırları kapsar. Bir BEFORE UPDATE tetikleyicisi bazı güncellemeleri engellediyse sayı eşleşen satır sayısından az olabilir.
Birden fazla sütunu aynı anda değiştirmek için SET a = 1, b = 2 yazabilir ya da sütun listesi biçimini kullanabilirsiniz: SET (a, b) = (1, 2). Bu biçimin sağ tarafı bir satır kurucusu veya tek satır döndüren bir alt sorgu olabilir. Hedef sütunu tablo adıyla nitelemek ise geçersizdir; PostgreSQL bunu açık bir ipucuyla reddeder:
UPDATE musteri SET musteri.puan = 1 WHERE id = 1;ERROR: column "musteri" of relation "musteri" does not exist
LINE 1: UPDATE musteri SET musteri.puan = 1 WHERE id = 1;
^
HINT: SET target columns cannot be qualified with the relation name.UPDATE ... FROM ve alt sorgu ile güncelleme
Başka bir tablodaki bilgiye göre güncelleme yapmanın iki yolu vardır: alt sorgu kullanmak ya da FROM yan tümcesine ek tablolar yazmak. FROM kullandığınızda hedef tablo, listelenen tablolarla birleştirilir ve birleştirmenin her çıktı satırı hedef tablo için bir güncelleme işlemi olur. Örnekte il bazında tanımlı bir bonus tablosuna göre puan ekliyoruz:
CREATE TABLE il_bonus (il text PRIMARY KEY, bonus integer);
INSERT INTO il_bonus VALUES ('İzmir', 20), ('Bursa', 5);
UPDATE musteri m SET puan = m.puan + b.bonus
FROM il_bonus b
WHERE b.il = m.il
RETURNING m.ad, m.il, b.bonus, m.puan; ad | il | bonus | puan
-------------+-------+-------+------
Ayşe Yılmaz | İzmir | 20 | 20
Can Öztürk | Bursa | 5 | 15
(2 rows)
UPDATE 2İki önemli kural var. Birincisi, hedef tabloyu FROM listesinde tekrar yazmayın; PostgreSQL bunu kendi kendine birleştirme (self join) olarak yorumlar. Kılavuz, bazı başka veritabanlarının hedef tablonun FROM içinde yeniden yazılmasını beklediğini ve uygulama taşırken buna dikkat edilmesi gerektiğini özellikle not eder. İkincisi, birleştirme her hedef satır için en fazla bir satır üretmelidir. Bir hedef satır diğer tablodaki birden fazla satırla eşleşirse bunlardan yalnızca biri kullanılır ve hangisinin seçileceği öngörülemez.
Alt sorgu yöntemi bu belirsizliğe karşı daha güvenlidir, ama başka bir farkı da vardır. Sütun alt sorgudan atanıyorsa ve alt sorgu hiç satır döndürmezse sütuna NULL yazılır. FROM yönteminde ise eşleşmeyen satır hiç güncellenmez. Bonus tablosunda Ankara ve İstanbul için kayıt olmadığından aşağıdaki komut puan sütununa NULL yazmaya çalışır ve NOT NULL kısıtı yüzünden bütün komut başarısız olur:
UPDATE musteri SET puan = (SELECT bonus FROM il_bonus b WHERE b.il = musteri.il)
WHERE id IN (2, 3)
RETURNING id, ad, il, puan;ERROR: null value in column "puan" of relation "musteri" violates not-null constraint
DETAIL: Failing row contains (2, Mehmet Kaya, Ankara, null, aktif).Kısıt olmasaydı komut sessizce bu iki müşterinin puanını silmiş olurdu. Kılavuz bu farkı şöyle özetler: alt sorgu biçimi birden fazla eşleşme olduğunda hata vererek sizi uyarır, birleştirme biçimi ise eşleşme olmadığında satıra dokunmaz. Hangisini seçeceğiniz, eşleşmeyen satırlara ne olmasını istediğinize bağlıdır; alt sorgu kullanıyorsanız coalesce ile bir yedek değer vermek ya da WHERE EXISTS ile yalnızca eşleşen satırları hedeflemek iyi bir önlemdir.
RETURNING için takma ad: WITH (OLD AS o, NEW AS n)
Tabloda old ya da new adlı bir sütun varsa ya da daha kısa yazmak istiyorsanız, RETURNING WITH (OLD AS o, NEW AS n) ile bu özel satırlara başka adlar verebilirsiniz. Takma ad verildiğinde asıl adlar gizlenir ve yalnızca takma adlarla başvurulabilir. Aynı örnekte sütun listesi atamasını da görüyorsunuz:
UPDATE musteri SET (puan, durum) = (0, 'pasif')
WHERE id = 3
RETURNING WITH (OLD AS o, NEW AS n) n.ad, o.durum AS once, n.durum AS sonra; ad | once | sonra
------------+-------+-------
Elif Demir | aktif | pasif
(1 row)
UPDATE 1DELETE ve DELETE ... USING
Ekleme gibi silme de yalnızca tam satırlar üzerinde yapılır; hangi satırların silineceğini yine bir koşulla belirtirsiniz. Kılavuz WHERE yazılmayan DELETE FROM tablo komutunun bütün satırları sileceğini ayrıca hatırlatır. Tablonun tamamını boşaltmak istiyorsanız TRUNCATE daha hızlı bir yoldur; bu komut CREATE TABLE ve ALTER TABLE yazısında anlatılıyor.
Başka bir tablodaki bilgiye göre silmek için PostgreSQL'e özgü USING yan tümcesi kullanılır; bu, UPDATE'teki FROM'un karşılığıdır. Standart yol ise WHERE il IN (SELECT ...) biçiminde bir alt sorgudur. Kılavuz, birleştirme biçiminin bazı durumlarda daha kolay yazıldığını veya daha hızlı çalıştığını belirtir. DELETE içinde old silinen satırı gösterir, new değerleri ise basit bir silmede her zaman NULL olur:
CREATE TABLE kara_liste (il text);
INSERT INTO kara_liste VALUES ('Bursa');
DELETE FROM musteri m
USING kara_liste k
WHERE m.il = k.il
RETURNING m.id, m.ad, old.puan AS eski, new.puan AS yeni; id | ad | eski | yeni
----+------------+------+------
4 | Can Öztürk | 15 |
(1 row)
DELETE 1Burada bir ayrıntıya dikkat edin: DELETE komutunda nitelenmemiş bir sütun adı ya da * eski değerleri, UPDATE ve INSERT komutunda ise yeni değerleri döndürür. RETURNING m.ad yazdığınızda silme işleminde silinen satırın adını alırsınız. Bütün işlemlerden sonra tablonun durumu şöyle:
SELECT id, ad, il, puan, durum FROM musteri ORDER BY id; id | ad | il | puan | durum
----+-------------+----------+------+-------
1 | Ayşe Yılmaz | İzmir | 20 | aktif
2 | Mehmet Kaya | Ankara | 50 | aktif
3 | Elif Demir | İstanbul | 0 | pasif
5 | Deniz Şahin | Ankara | 60 | aktif
(4 rows)Koşula uyan satır bulunmadığında da komutlar hata vermez; yalnızca etiket sıfır olur. Uygulamanızda "satır gerçekten güncellendi mi" sorusunu bu sayıyla ya da RETURNING çıktısının boş olup olmadığıyla anlayabilirsiniz:
UPDATE musteri SET puan = 999 WHERE il = 'Trabzon';
DELETE FROM musteri WHERE durum = 'pasif';UPDATE 0
DELETE 1RETURNING ayrıntıları

RETURNING listesinin içeriği bir SELECT seçim listesiyle aynı kurallara uyar: hedef tablonun sütunları, onlardan hesaplanan ifadeler ve UPDATE ... FROM ile DELETE ... USING durumunda birleştirilen tabloların sütunları yazılabilir. Komut türüne göre varsayılan olarak hangi değerin döndüğü aşağıdaki tabloda özetlenmiştir:
| Komut | Nitelenmemiş sütun | old.sutun | new.sutun |
|---|---|---|---|
| INSERT | Eklenen satır | NULL (ON CONFLICT DO UPDATE ile çakışan satırda dolu) | Eklenen satır |
| UPDATE | Güncellenmiş satır | Güncelleme öncesi değer | Güncelleme sonrası değer |
| DELETE | Silinen satır | Silinen satır | NULL |
| MERGE | INSERT/UPDATE için yeni, DELETE için eski değer | Önceki değer | Sonraki değer |
Hedef tabloda tetikleyiciler varsa RETURNING, satırın tetikleyiciler tarafından değiştirilmiş son halini döndürür; kılavuz, tetikleyicinin hesapladığı sütunları okumanın da bu yan tümcenin yaygın kullanımlarından biri olduğunu belirtir. RETURNING kullanmak için listede geçen sütunlarda SELECT yetkisi gerekir. Tetikleyicilerin kendisi trigger yazısında anlatılıyor.
RETURNING'in bir başka güçlü kullanımı, veri değiştiren komutları WITH içine koyup çıktısını başka bir komuta aktarmaktır; örneğin silinen satırları aynı sorguda bir arşiv tablosuna eklemek. Bu kalıp WITH ve CTE yazısında ayrıntılı işleniyor.
Büyük güncelleme ve silmeler
UPDATE ve DELETE komutlarının LIMIT yan tümcesi yoktur. Kılavuz, çok sayıda satırı etkileyen bir güncellemenin tablo şişmesine, replika gecikmesine ve kilit çekişmesine yol açabileceğini, bu durumda işi küçük parçalar halinde yapmanın ve parçalar arasında VACUUM çalıştırmanın mantıklı olabileceğini söyler. Önerdiği yöntem, bir CTE içinde sınırlı sayıda satırın sistem sütunu ctid değerini kilitleyerek seçmek ve hedef tabloyu bununla birleştirmektir:
WITH parti AS (
SELECT m.ctid FROM musteri AS m
WHERE m.durum = 'pasif'
ORDER BY m.id
FOR UPDATE
LIMIT 5000
)
DELETE FROM musteri AS dm
USING parti AS p
WHERE dm.ctid = p.ctid;Komut, silinecek satır kalmayana kadar tekrar çalıştırılır. Kılavuz, ctid kullanımının yalnızca sorgu tekrar tekrar çalıştırıldığı için güvenli olduğunu, çünkü satır güncellendiğinde ctid değerinin değişebileceğini vurgular. Şişme ve VACUUM mekanizması VACUUM ve autovacuum yazısında, kilitler kilit ve deadlock yazısında ele alınıyor.
Sık yapılan hatalar
- WHERE unutmak:
UPDATEveDELETEbütün tabloyu etkiler. Riskli değişiklikleri bir transaction içinde yapıp sonucu kontrol ettikten sonra onaylamak iyi bir alışkanlıktır (bkz. transaction yazısı). - UPDATE ... FROM ile birden fazla eşleşme: Hedef satır kaynak tablodaki iki satırla eşleşirse hangisinin kullanılacağı belirsizdir. Kaynak tarafta birleştirme anahtarının benzersiz olduğundan emin olun.
- Alt sorgunun boş dönmesi: Alt sorgudan atanan sütun eşleşme yoksa
NULLolur. Ya kısıtla yakalanır ya da veri sessizce bozulur. - Hedef sütunu nitelemek:
SET tablo.sutun = ...geçersizdir; yalnızca sütun adını yazın. - Sütun listesi olmadan INSERT: Tablo yapısı değiştiğinde değerler yanlış sütunlara gidebilir.
- RETURNING yerine ikinci sorgu: Eklenen satırın kimliğini "en büyük id" sorgusuyla bulmak eşzamanlı ortamda yanlış satırı getirebilir;
RETURNING idkullanın.
Kullanıcıdan gelen değerleri bu komutlara metin birleştirerek değil, parametre olarak geçirin; uygulama tarafındaki doğru yöntem SQL injection önleme yazısında ve PHP için PDO bağlantısı yazısında anlatılıyor.
Sık Sorulan Sorular
Eklenen satırın otomatik kimliğini nasıl alırım?
INSERT ... RETURNING id yazın. Kılavuz bu kullanımı özellikle önerir: kimlik veritabanında üretildiği için istemcinin ayrı bir sorguyla onu bulmaya çalışması hem gereksiz hem de eşzamanlı eklemelerde güvenilmezdir.
PostgreSQL 18'de RETURNING ile ilgili ne değişti?
Sürüm notlarına göre PostgreSQL 18 öncesinde RETURNING, INSERT ve UPDATE için yalnızca yeni değerleri, DELETE için eski değerleri döndürebiliyordu. 18 ile INSERT, UPDATE, DELETE ve MERGE komutlarında old ve new özel takma adları geldi ve bu adlar WITH (OLD AS ..., NEW AS ...) ile değiştirilebiliyor. Bütün yenilikler için PostgreSQL 18 yenilikleri yazısına bakın.
UPDATE FROM mu, alt sorgu mu kullanmalıyım?
Kaynak tabloda her hedef satır için tek eşleşme garanti ise FROM çoğu zaman daha okunaklı ve hızlıdır. Birden fazla eşleşmenin hata olarak yakalanmasını istiyorsanız ya da eşleşmeyen satırın belirli bir değer almasını istiyorsanız alt sorgu daha uygundur; kılavuz alt sorgu yaklaşımını daha güvenli olarak niteler.
DELETE ile TRUNCATE arasındaki fark nedir?
DELETE koşula göre satır siler, RETURNING ve USING destekler ve etkilenen satır sayısını raporlar. TRUNCATE ise tablonun bütün satırlarını kaldırmak için tasarlanmış daha hızlı bir mekanizmadır; koşul almaz.
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 · UPSERT: ON CONFLICT ve MERGE · SELECT, WHERE, ORDER BY, LIMIT · WITH ve recursive CTE · Identity, serial ve sequence · BEGIN, COMMIT, ROLLBACK