SQLite'ta UPSERT, bir INSERT ifadesinin sonuna eklenen ON CONFLICT(sütun) DO UPDATE SET ... veya ON CONFLICT DO NOTHING yan tümcesidir. Ekleme bir UNIQUE ya da PRIMARY KEY kısıtını ihlal edecekse satır eklenmez; bunun yerine mevcut satır güncellenir veya hiçbir şey yapılmaz. Eklenmek istenen değerlere excluded.sütun biçiminde erişilir. Bu söz dizimi SQLite'a 3.24.0 sürümünde PostgreSQL'in söz dizimi örnek alınarak eklendi; birden fazla ON CONFLICT yan tümcesi ve hedefsiz DO UPDATE ise 3.35.0 sürümüyle geldi.
Bu yazıda UPSERT'in ayrıntılı kurallarını, SQLite'a özgü eski "ON CONFLICT" çatışma çözüm algoritmalarıyla farkını, REPLACE komutunun neden UPSERT yerine kullanılmaması gerektiğini ve beş çatışma algoritmasının davranışını çalıştırılmış örneklerle anlatıyoruz. Temel INSERT, UPDATE ve RETURNING kullanımı için SQLite INSERT, UPDATE, DELETE, serinin tamamı için SQLite öğrenme rehberi yazılarına bakabilirsiniz.
Aynı kelimeler, iki farklı özellik

SQLite'ta "ON CONFLICT" ifadesi iki ayrı özellikte geçer ve belgeler bunların karıştırılmaması konusunda açıkça uyarır. Biri SQLite'ın 3.0.0 sürümünden önce de bulunan çatışma çözüm yan tümcesidir; diğeri 3.24.0 ile eklenen UPSERT'tir.
| Özellik | Yazılışı | Ne yapar |
|---|---|---|
| Çatışma çözüm algoritması | INSERT OR IGNORE, UPDATE OR REPLACE, CREATE TABLE içinde UNIQUE ON CONFLICT ... | Kısıt ihlalinde ifadenin nasıl davranacağını seçer: ROLLBACK, ABORT, FAIL, IGNORE veya REPLACE |
| UPSERT | INSERT ... ON CONFLICT(hedef) DO UPDATE / DO NOTHING | Benzersizlik çatışmasında eklemeyi güncellemeye veya hiçbir şey yapmamaya çevirir |
INSERT ve UPDATE içinde çatışma algoritması "ON CONFLICT" yerine "OR" ile yazılır; INSERT OR IGNORE okunuşu bu yüzden doğaldır. UPSERT ise INSERT ifadesinin VALUES veya SELECT kısmından sonra gelir.
DO UPDATE ile sayaç tutmak
UPSERT'in en bilinen kullanımı "yoksa ekle, varsa artır" düzenidir. Bir kelime tablosunda her kelimenin kaç kez görüldüğünü sayalım:
CREATE TABLE kelime(kelime TEXT PRIMARY KEY, adet INTEGER NOT NULL DEFAULT 1);
INSERT INTO kelime(kelime) VALUES ('sqlite')
ON CONFLICT(kelime) DO UPDATE SET adet = adet + 1;
INSERT INTO kelime(kelime) VALUES ('sqlite')
ON CONFLICT(kelime) DO UPDATE SET adet = adet + 1;
-- Çok satırlı eklemede karar her satır için ayrı verilir
INSERT INTO kelime(kelime) VALUES ('index'), ('sqlite'), ('index')
ON CONFLICT(kelime) DO UPDATE SET adet = adet + 1;
SELECT * FROM kelime ORDER BY kelime;
-- çıktı:
-- index|2
-- sqlite|3DO UPDATE içinde yazılan çıplak sütun adları, satırın ekleme denemesinden önceki değerini gösterir; bu yüzden adet + 1 mevcut sayacı artırır. Aynı ifadeyi kelime.adet + 1 biçiminde tablo adıyla da yazabilirsiniz; belgelere göre PostgreSQL ikinci biçimi zorunlu tutar, SQLite ikisini de kabul eder. Çok satırlı eklemede karar her satır için ayrı verilir: aynı ifade içinde ikinci kez gelen index satırı, ilk satırın eklediği kayıtla çakıştı ve onun sayacını artırdı. DO UPDATE yalnızca çatışmaya yol açan tek satır üzerinde çalışır; güncellemeyi o satırla sınırlamak için ayrıca WHERE yazmanız gerekmez.
excluded, koşullu güncelleme ve RETURNING
Eklenmek istenen yeni değerlere excluded. önekiyle ulaşılır. DO UPDATE sonundaki isteğe bağlı WHERE ise güncellemeyi koşula bağlar; koşul yanlışsa DO UPDATE hiçbir şey yapmayan bir işleme dönüşür. Aşağıdaki örnek yalnızca daha yeni tarihli fiyatları kabul ediyor:
CREATE TABLE fiyat(urun TEXT PRIMARY KEY, tutar INTEGER, tarih TEXT);
INSERT INTO fiyat VALUES ('kalem', 15, '2026-10-01');
-- Daha yeni tarihli fiyat gelirse güncelle
INSERT INTO fiyat VALUES ('kalem', 18, '2026-11-01')
ON CONFLICT(urun) DO UPDATE SET tutar = excluded.tutar, tarih = excluded.tarih
WHERE excluded.tarih > fiyat.tarih
RETURNING urun, tutar, tarih;
-- çıktı: kalem|18|2026-11-01
-- Daha eski tarihli fiyat gelirse hiçbir şey yapma
INSERT INTO fiyat VALUES ('kalem', 12, '2026-09-01')
ON CONFLICT(urun) DO UPDATE SET tutar = excluded.tutar, tarih = excluded.tarih
WHERE excluded.tarih > fiyat.tarih
RETURNING urun, tutar, tarih;
-- (çıktı yok: satır değişmedi)
SELECT * FROM fiyat;
-- çıktı: kalem|18|2026-11-01RETURNING yan tümcesi, ifadenin gerçekten eklediği veya güncellediği satırları döndürür. İkinci denemede WHERE koşulu yanlış olduğu için satır değişmedi ve RETURNING hiçbir satır döndürmedi. Bu özellik, uygulamanın "kayıt güncellendi mi" sorusunu ek bir SELECT yapmadan yanıtlamasını sağlar. RETURNING'in diğer kuralları için DML yazısına bakın.
WHERE içinde mevcut satıra fiyat.tarih gibi tablo adıyla başvurmak okunabilirliği artırır, çünkü aynı koşulda excluded.tarih de geçmektedir. INSERT ifadesine AS takma_ad verirseniz bu takma ad da UPSERT'in WHERE ve SET kısımlarında kullanılabilir.
Çatışma hedefi: hangi kısıt yakalanır

ON CONFLICT ile DO arasındaki parantezli kısım "çatışma hedefi" adını alır ve UPSERT'i tetikleyecek benzersizlik kısıtını belirtir. Hedef, tablodaki bir PRIMARY KEY, UNIQUE kısıtı veya benzersiz indeksle tam olarak eşleşmelidir; eşleşmezse ifade hiç çalışmaz:
INSERT INTO fiyat VALUES ('kalem', 20, '2026-12-01') ON CONFLICT(tutar) DO NOTHING;
-- hata: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint3.35.0 sürümünden beri bir INSERT birden fazla ON CONFLICT yan tümcesi taşıyabilir. Yan tümceler yazıldıkları sırayla denetlenir ve her satır için yalnızca eşleşen ilk yan tümce çalışır. Hedef yalnızca son yan tümcede yazılmayabilir; hedefsiz son yan tümce, önceki yan tümcelerin yakalamadığı her benzersizlik çatışmasında devreye girer:
CREATE TABLE uye(
id INTEGER PRIMARY KEY, eposta TEXT UNIQUE, kullanici TEXT UNIQUE, ad TEXT
);
INSERT INTO uye VALUES (1, 'a@ornek.com', 'ayse', 'Ayşe'), (2, 'm@ornek.com', 'mehmet', 'Mehmet');
-- eposta çakışırsa adı güncelle, başka bir benzersizlik çakışırsa hiçbir şey yapma
INSERT INTO uye VALUES (3, 'a@ornek.com', 'ayse2', 'Ayşe Yılmaz')
ON CONFLICT(eposta) DO UPDATE SET ad = excluded.ad
ON CONFLICT DO NOTHING;
INSERT INTO uye VALUES (4, 'yeni@ornek.com', 'mehmet', 'Başka Mehmet')
ON CONFLICT(eposta) DO UPDATE SET ad = excluded.ad
ON CONFLICT DO NOTHING;
SELECT * FROM uye ORDER BY id;
-- çıktı:
-- 1|a@ornek.com|ayse|Ayşe Yılmaz
-- 2|m@ornek.com|mehmet|Mehmetİlk eklemede e-posta çakıştı ve ad güncellendi. İkinci eklemede e-posta yeniydi ama kullanıcı adı çakıştı; bu çatışmayı ilk yan tümce yakalamadığı için hedefsiz DO NOTHING çalıştı ve satır eklenmedi.
UPSERT yalnızca benzersizlik çatışmalarını yakalar
UPSERT; NOT NULL, CHECK veya yabancı anahtar ihlallerine ve tetikleyicilerle uygulanan kurallara karışmaz. Bu, ON CONFLICT DO NOTHING ile INSERT OR IGNORE arasındaki önemli bir farkı doğurur:
CREATE TABLE kisi(id INTEGER PRIMARY KEY, eposta TEXT UNIQUE, ad TEXT NOT NULL);
INSERT INTO kisi(eposta, ad) VALUES ('a@ornek.com', 'Ayşe');
INSERT INTO kisi(eposta, ad) VALUES ('b@ornek.com', NULL) ON CONFLICT DO NOTHING;
-- hata: NOT NULL constraint failed: kisi.ad
INSERT OR IGNORE INTO kisi(eposta, ad) VALUES ('b@ornek.com', NULL);
SELECT count(*) FROM kisi;
-- çıktı: 1 (hata yok, satır sessizce atlandı)DO NOTHING, NOT NULL ihlalini yakalamadığı için hata döndü. INSERT OR IGNORE ise NOT NULL ihlalini de yok saydı ve satırı hiçbir uyarı vermeden atladı. "Varsa dokunma" niyetiyle INSERT OR IGNORE kullanan kodlar, bu yüzden geçersiz verinin sessizce kaybolmasına yol açabilir. Yalnızca yinelenen kayıtları atlamak istiyorsanız DO NOTHING daha dar ve daha güvenli bir seçimdir. NULL değerlerin benzersizlik kısıtlarındaki davranışı için SQLite NULL davranışı yazısına bakın.
DO UPDATE her zaman ABORT ile çalışır
DO UPDATE kısmındaki güncelleme, çatışma çözüm algoritması olarak her zaman ABORT kullanır; ifade "DO UPDATE OR ABORT" yazılmış gibi davranır. Güncelleme başka bir kısıtı ihlal ederse bütün INSERT ifadesi geri alınır ve durur. Bu durum, INSERT ifadesi ya da onu çalıştıran tetikleyici başka bir algoritma belirtse bile değişmez:
-- uye tablosu U4 sonrasındaki hâlinde
INSERT INTO uye VALUES (5, 'a@ornek.com', 'x', 'X')
ON CONFLICT(eposta) DO UPDATE SET kullanici = 'mehmet';
-- hata: UNIQUE constraint failed: uye.kullanici
-- Çok satırlı eklemede tek bir DO UPDATE hatası bütün ifadeyi geri alır
INSERT INTO uye VALUES (6, 'z@ornek.com', 'zeynep', 'Zeynep'), (7, 'a@ornek.com', 'q', 'Q')
ON CONFLICT(eposta) DO UPDATE SET kullanici = 'mehmet';
-- hata: UNIQUE constraint failed: uye.kullanici
SELECT count(*) FROM uye;
-- çıktı: 2 (Zeynep de eklenmedi)İkinci ifadede Zeynep satırının kendisinde hiçbir sorun yoktu; buna rağmen aynı ifadedeki başka bir satırın DO UPDATE hatası onu da geri aldı. Toplu içe aktarmalarda UPSERT kullanıyorsanız, güncellenen sütunlardaki benzersizlik kısıtlarını önceden düşünün.
INSERT ... SELECT ile UPSERT
Değerler bir SELECT ifadesinden geliyorsa ayrıştırıcı, ON kelimesinin UPSERT'i mi yoksa bir JOIN koşulunu mu başlattığını ayırt edemeyebilir. Belgelerin önerdiği çözüm, SELECT'e her zaman bir WHERE eklemektir; gerekirse WHERE true yeterlidir:
CREATE TABLE kaynak(eposta TEXT, ad TEXT);
INSERT INTO kaynak VALUES ('m@ornek.com', 'Mehmet Kaya');
INSERT INTO uye(eposta, ad) SELECT eposta, ad FROM kaynak
ON CONFLICT(eposta) DO UPDATE SET ad = excluded.ad;
-- hata: near "DO": syntax error
INSERT INTO uye(eposta, ad) SELECT eposta, ad FROM kaynak WHERE true
ON CONFLICT(eposta) DO UPDATE SET ad = excluded.ad;
SELECT id, ad FROM uye WHERE eposta = 'm@ornek.com';
-- çıktı: 2|Mehmet KayaKısmi benzersiz indeksle UPSERT
Çatışma hedefi bir kısmi benzersiz indeksi gösterecekse, hedefin içine indeksin WHERE koşulunu da yazmanız gerekir. Aşağıdaki tabloda her kullanıcının yalnızca bir aktif görevi olabilir:
CREATE TABLE gorev(id INTEGER PRIMARY KEY, kullanici INTEGER, baslik TEXT, aktif INTEGER);
CREATE UNIQUE INDEX tek_aktif ON gorev(kullanici) WHERE aktif = 1;
INSERT INTO gorev VALUES (1, 7, 'Rapor', 1), (2, 7, 'Eski iş', 0);
INSERT INTO gorev(kullanici, baslik, aktif) VALUES (7, 'Sunum', 1)
ON CONFLICT(kullanici) DO UPDATE SET baslik = excluded.baslik;
-- hata: ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
INSERT INTO gorev(kullanici, baslik, aktif) VALUES (7, 'Sunum', 1)
ON CONFLICT(kullanici) WHERE aktif = 1 DO UPDATE SET baslik = excluded.baslik;
SELECT * FROM gorev;
-- çıktı:
-- 1|7|Sunum|1
-- 2|7|Eski iş|0Koşulsuz hedef hiçbir kısıtla eşleşmedi, çünkü kullanici sütunu tek başına benzersiz değil; yalnızca aktif = 1 olan satırlar arasında benzersiz. Hedefe WHERE aktif = 1 eklendiğinde eşleşme sağlandı. Kısmi indekslerin ayrıntıları için SQLite partial index yazısına bakın.
REPLACE neden UPSERT değildir
REPLACE INTO, INSERT OR REPLACE için başka veritabanlarıyla uyumluluk amacıyla sunulan bir takma addır. Adı benzer olsa da davranışı UPSERT'ten çok farklıdır: REPLACE algoritması çatışmaya yol açan mevcut satırı siler, ardından yeni satırı ekler. Yeni satırda belirtmediğiniz sütunlar eski değerlerini korumaz, varsayılan değerlerini alır:
CREATE TABLE uye(id INTEGER PRIMARY KEY, eposta TEXT UNIQUE, ad TEXT NOT NULL, puan INTEGER DEFAULT 0);
INSERT INTO uye(eposta, ad, puan) VALUES ('a@ornek.com', 'Ayşe', 50);
REPLACE INTO uye(eposta, ad) VALUES ('a@ornek.com', 'Ayşe Y.');
SELECT id, eposta, ad, puan FROM uye;
-- çıktı: 2|a@ornek.com|Ayşe Y.|0 (id 1'den 2'ye değişti, puan sıfırlandı)Kullanıcının kimliği değişti ve puanı kayboldu. Bu tablodaki id değerine başka bir tablodan başvuruluyorsa ilişki de kopar. Aynı işi UPSERT ile yapmak satırı yerinde güncellerdi. REPLACE'in belgelerde not edilen başka istisnaları da vardır: sildiği satırlar için silme tetikleyicileri yalnızca recursive triggers açıkken çalışır, C arayüzündeki güncelleme kancası (update hook) bu silmeler için çağrılmaz ve değişiklik sayacı artırılmaz. Belgeler bu davranışların gelecekte değişebileceğini de belirtir.
CREATE TABLE ayar(anahtar TEXT PRIMARY KEY, deger TEXT);
CREATE TABLE silinen(anahtar TEXT);
CREATE TRIGGER ayar_sil AFTER DELETE ON ayar BEGIN INSERT INTO silinen VALUES (OLD.anahtar); END;
INSERT INTO ayar VALUES ('tema', 'acik');
REPLACE INTO ayar VALUES ('tema', 'koyu');
SELECT count(*) FROM silinen;
-- çıktı: 0
PRAGMA recursive_triggers = ON;
REPLACE INTO ayar VALUES ('tema', 'mavi');
SELECT * FROM silinen;
-- çıktı: temaSilme tetikleyicisine dayalı bir denetim günlüğünüz varsa REPLACE kullanımı bu günlükte boşluk bırakabilir. Tetikleyicilerin ayrıntıları için SQLite trigger yazısına bakın.
Beş çatışma çözüm algoritması

UPSERT dışındaki kısıt ihlallerinde ne olacağını çatışma çözüm algoritması belirler. Varsayılan ABORT'tur ve SQL standardının öngördüğü davranış da budur.
| Algoritma | İhlal olduğunda | Önceki değişiklikler |
|---|---|---|
| ROLLBACK | İfade SQLITE_CONSTRAINT ile durur ve işlemin tamamı geri alınır | İşlemdeki tüm değişiklikler kaybolur; işlem yoksa ABORT gibi davranır |
| ABORT | İfade SQLITE_CONSTRAINT ile durur | Bu ifadenin değişiklikleri geri alınır, işlemdeki önceki ifadeler korunur |
| FAIL | İfade SQLITE_CONSTRAINT ile durur | Bu ifadenin hata anına kadar yaptıkları korunur |
| IGNORE | Sorunlu satır atlanır, ifade devam eder, hata dönmez | Korunur; yabancı anahtar ihlalinde ABORT gibi davranır |
| REPLACE | UNIQUE veya PRIMARY KEY ihlalinde eski satır silinir ve işlem sürer | Korunur; CHECK ve yabancı anahtar ihlalinde ABORT gibi davranır |
ABORT, FAIL ve IGNORE arasındaki fark çok satırlı bir UPDATE'te açıkça görülür:
CREATE TABLE t(id INTEGER PRIMARY KEY, k INTEGER UNIQUE);
INSERT INTO t VALUES (1, 1), (2, 5), (3, 3), (4, 4);
UPDATE OR ABORT t SET k = k + 1; -- 3. satırda k=4 olur, 4. satırla çakışır
-- hata: UNIQUE constraint failed: t.k
SELECT group_concat(k) FROM t;
-- çıktı: 1,5,3,4 (hiçbir değişiklik kalmadı)
UPDATE OR FAIL t SET k = k + 1;
-- hata: UNIQUE constraint failed: t.k
SELECT group_concat(k) FROM t;
-- çıktı: 2,6,3,4 (çakışmadan önceki iki satır değişti)
UPDATE OR IGNORE t SET k = 7;
SELECT group_concat(k) FROM t;
-- çıktı: 7,6,3,4 (yalnızca ilk satır güncellendi, diğerleri atlandı)Bu örnekteki FAIL sonucu, satırların hangi sırayla işlendiğine bağlıdır ve bu sıraya güvenen bir kod yazmamalısınız. FAIL kullandığınızda ifadenin yarıda kalmış olabileceğini ve sonucun işlenme sırasına bağlı olabileceğini kabul etmiş olursunuz. Belgelere göre FAIL yalnızca benzersizlik, NOT NULL ve CHECK kısıtlarında çalışır; yabancı anahtar ihlali ABORT'a yol açar.
Algoritma tablo tanımında da bir kısıta bağlanabilir. ON CONFLICT yan tümcesi UNIQUE, NOT NULL ve PRIMARY KEY kısıtlarında kullanılabilir, CHECK ve FOREIGN KEY kısıtlarında kullanılamaz. INSERT veya UPDATE içindeki OR yan tümcesi, tablo tanımındaki algoritmanın önüne geçer:
CREATE TABLE etiket(ad TEXT UNIQUE ON CONFLICT IGNORE);
INSERT INTO etiket VALUES ('sql'), ('sqlite'), ('sql');
SELECT count(*) FROM etiket;
-- çıktı: 2
INSERT OR ABORT INTO etiket VALUES ('sql');
-- hata: UNIQUE constraint failed: etiket.adREPLACE algoritmasının NOT NULL ihlalindeki davranışı da farklıdır: NULL değerin yerine sütunun varsayılan değeri konur, varsayılan değer yoksa ABORT uygulanır. Kısıt tanımları için SQLite CREATE TABLE, hata mesajlarının anlamı için SQLite hata kodları yazılarına bakın.
Sık yapılan hatalar
- "Yoksa ekle, varsa güncelle" için REPLACE kullanıp satır kimliğini ve belirtilmeyen sütunları kaybetmek.
- Yinelenen kayıtları atlamak için INSERT OR IGNORE kullanıp NOT NULL ve CHECK ihlallerini de sessizce yutmak.
- Çatışma hedefini benzersiz olmayan bir sütuna yazmak; ifade "does not match any PRIMARY KEY or UNIQUE constraint" hatası verir.
- Kısmi benzersiz indekse karşılık gelen hedefte indeksin WHERE koşulunu unutmak.
- INSERT ... SELECT ile UPSERT yazarken SELECT'e WHERE eklemeyi unutmak.
- DO UPDATE içinde
excluded.önekini unutup mevcut değeri kendisine yazmak. - UPSERT'i sanal tablolarda kullanmaya çalışmak; belgelere göre sanal tablolarda çalışmaz.
Sık Sorulan Sorular
UPSERT hangi SQLite sürümünden itibaren kullanılabilir?
Tek ON CONFLICT yan tümceli UPSERT 3.24.0 sürümünden, birden fazla yan tümce ve hedefsiz DO UPDATE 3.35.0 sürümünden itibaren kullanılabilir. Uygulamanızın kullandığı kütüphanenin sürümünü sqlite_version() ile kontrol edin.
DO UPDATE içinde WHERE yazmazsam başka satırlar da güncellenir mi?
Hayır. DO UPDATE yalnızca çatışmayı yaratan satıra uygulanır. DO UPDATE sonundaki WHERE, satır seçmek için değil, güncellemeyi koşula bağlamak için vardır.
ON CONFLICT DO NOTHING ile INSERT OR IGNORE aynı şey mi?
Hayır. DO NOTHING yalnızca benzersizlik çatışmalarını yakalar. INSERT OR IGNORE ise benzersizlik ve NOT NULL ihlallerini de hata vermeden atlar. Denememizde CHECK (x > 0) kısıtlı bir tabloya INSERT OR IGNORE ... VALUES (1), (-1), (2) yazdığımızda -1 satırı da sessizce atlandı ve yalnızca 1 ile 2 eklendi. Yabancı anahtar ihlallerinde ise IGNORE, ABORT gibi davranır.
Hangi satırın eklendiğini, hangisinin güncellendiğini nasıl ayırt ederim?
RETURNING ile dönen satırlar etkilenen kayıtları gösterir. Ekleme ile güncellemeyi ayırmak için tabloda bir oluşturma zamanı veya sürüm sütunu tutup bunu RETURNING ile birlikte döndürebilirsiniz.
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: SQLite öğrenme rehberi · SQLite INSERT, UPDATE, DELETE · SQLite CREATE TABLE · SQLite trigger · SQLite partial index