Menu

SQLite UPSERT: ON CONFLICT DO UPDATE ve DO NOTHING

SQLite'ta UPSERT nasıl çalışır? ON CONFLICT cümlesi, excluded sözde tablosu, DO NOTHING ile DO UPDATE farkı ve INSERT OR REPLACE'e neden tercih edilmeli.

Bu sayfada çalıştırılabilir editörler var — düzenle, çalıştır ve sonucu anında gör.

Ekle, Zaten Varsa Güncelle

Çok sık karşılaştığımız bir ihtiyaç var: bir satır eklemek istiyoruz ama aynı anahtara sahip bir satır zaten varsa, eklemek yerine onu güncellemek istiyoruz. UPSERT olmasaydı önce bir SELECT yazıp ardından duruma göre INSERT veya UPDATE çalıştırmamız gerekirdi — iki ayrı sorgu, üstüne bir de aralarında yarış durumu (race condition) riski.

SQLite'taki UPSERT bunu tek bir ifadeyle halleder:

İlk çalıştırdığında satır eklenir. Aynı sku ile farklı bir fiyat verip tekrar çalıştırdığında ise mevcut satır yerinde güncellenir. Ne yinelenen kayıt olur, ne de hata.

ON CONFLICT'in Anatomisi

Genel kalıp şöyle:

INSERT INTO table (...) VALUES (...)
ON CONFLICT(conflict_target) DO UPDATE SET col = expr, ...
WHERE condition;

Üç parça önemli:

  • conflict_targetUNIQUE veya PRIMARY KEY kısıtı olan ve çakışma bekleyeceğiniz sütun ya da sütunlar. SQLite bu bilgiyle hangi indeksi izleyeceğine karar veriyor.
  • DO UPDATE SET ... — çakışma olduğunda mevcut satırda neyi güncelleyeceğinizi belirtir. (Ya da sessizce atlamak için DO NOTHING yazarsınız.)
  • Opsiyonel WHERE — güncellemenin gerçekten çalışması için sağlanması gereken ek koşul.

Çakışma hedefi (conflict target), gerçek bir unique kısıtı ile eşleşmek zorunda. Örneğin price sütunu unique değilse ON CONFLICT(price) çalışmaz — SQLite'ın çakışmayı tespit edebileceği bir dayanağı kalmaz.

DO NOTHING: Yoksa Ekle, Varsa Atla

Daha basit olan varyant. Veri seed'lerken ya da olay kaydı tutarken tekrar edenleri sessizce görmezden gelmek istediğinizde işe yarar:

İkinci INSERT aynı event_id ile çakıştığı için normal şartlarda UNIQUE constraint failed hatası fırlatırdı. Ama DO NOTHING sayesinde SQLite bu satırı sessizce atlıyor: hata yok, etkilenen satır da yok.

İşte tam da bu yüzden insanlar genelde "idempotent insert" denen senaryoda INSERT OR IGNORE kullanır. UPSERT'ün DO NOTHING varyantı da aynı işi görür; üstelik WHERE ve RETURNING ifadeleriyle çok daha uyumlu çalışır.

excluded Sözde Tablosu

Bir çakışma tetiklendiğinde elinizde aynı anda iki satır olur: tabloda zaten var olan eski satır ve eklemeye çalıştığınız yeni satır. SQLite, ikisine de ayrı ayrı erişmenin bir yolunu sunar.

  • Çıplak sütun adları (price, name) mevcut satırı ifade eder.
  • excluded.column ise reddedilen gelen satırı ifade eder.

quantity = quantity + excluded.quantity ifadesini "mevcut miktar artı yeni gelen miktar" şeklinde okuyabilirsiniz. İki ekleme işleminden sonra A-100 ürününün miktarı 8 olur. Bu kalıp — yani var olan satırın üzerine birikim yapmak — UPSERT'in en kullanışlı numaralarından biridir.

WHERE ile koşullu UPSERT

Sondaki WHERE, belirli bir koşul sağlanmadığında güncellemeyi atlamanızı sağlar. Bu koşul mevcut satır üzerinde çalışır ve gelen satıra ulaşmak için excluded.* referansını kullanabilir:

Yeni satır daha eski bir updated_at taşıdığı için WHERE koşulu false döner ve güncelleme atlanır. Mevcut satır, daha yeni olan fiyatını korur. Tarihleri yer değiştirirseniz güncelleme çalışır. Bu, "yalnızca daha güncel veriyle üzerine yaz" yaklaşımının klasik kullanım şeklidir.

SQLite ile Çoklu Satır Upsert

VALUES ifadesi birden fazla satır taşıyabilir ve ON CONFLICT her bir satıra bağımsız olarak uygulanır:

A-100 çakışıyor ve güncelleniyor. A-200 ile A-300 ise yeni kayıtlar olduğu için ekleniyor. Tek bir sorguyla hem ekleme hem güncelleme işi bitiyor. Dış bir kaynaktan toplu kayıt senkronlamak için bundan temizi zor bulunur.

UPSERT ile INSERT OR REPLACE arasındaki fark

INSERT OR REPLACE ilk bakışta aynı işi yapıyormuş gibi görünür. Ama yapmaz.

notes uçtu gitti. INSERT OR REPLACE aslında 1 numaralı satırı tamamen sildi ve yerine yepyeni bir satır ekledi — yani listelemediğiniz her kolon ya NULL'a ya da kendi varsayılan değerine geri döndü. Üstüne üstlük DELETE trigger'larını da tetikler ve ON DELETE foreign key'leri üzerinden cascade çalıştırır.

UPSERT ise satırı olduğu gibi korur:

notes sütunu hâlâ yerinde duruyor. Yalnızca SET içinde belirttiğiniz kolonlar güncellendi. Varsayılan olarak UPSERT kullanın; INSERT OR REPLACE'i ise gerçekten "sil ve yeniden ekle" davranışı istediğinizde tercih edin.

Birden Fazla Çakışma Hedefi

Bir satır birden fazla kısıt üzerinde çakışabilecekse, ON CONFLICT ifadelerini ardışık olarak zincirleyebilirsiniz:

Hangi kısıt önce tetiklenirse o kazanır ve o dalın DO UPDATE ifadesi çalışır. Pratikte çoğu tablonun tek bir bariz çakışma hedefi vardır — birincil anahtar ya da tek bir unique sütun — ve nadiren birden fazla cümleciğe ihtiyaç duyarsınız.

Sık Yapılan Hatalar

İnsanların sık tökezlediği birkaç nokta:

  • Eşleşen unique indeks yoksa UPSERT da yok. ON CONFLICT(col) ifadesi, col sütununun PRIMARY KEY olmasını ya da UNIQUE kısıtı taşımasını ister. Aksi halde SQLite "no such constraint" hatası fırlatır.
  • Çakışma yoksa DO UPDATE çalışmaz. Bu, insert'e alternatif bir davranıştır; yanına eklenen ekstra bir adım değil. Bir anahtar ilk kez görüldüğünde yalnızca insert çalışır.
  • excluded salt-okunurdur. Ondan okuyabilirsiniz ama ona yazamazsın. SET ifadesinin hedefi her zaman mevcut satırdır.
  • Otomatik üretilen INTEGER PRIMARY KEY rowid'leri. id'yi sen vermezsen, her insert yeni bir id alır — çakışacak bir şey kalmaz. UPSERT yalnızca çakışan sütunun değeri çağıran tarafından belirgin biçimde verildiğinde anlamlıdır.

Sırada: RETURNING

UPSERT, hangi satırların eklendiğini, hangilerinin güncellendiğini ya da nihai değerlerinin ne olduğunu sana söylemez. Bunun için RETURNING cümleciği var — etkilenen satırları aynı sorgunun içinde geri döndürür, ardından bir SELECT çekmene gerek kalmaz. Sırada o var.

Sıkça Sorulan Sorular

SQLite'ta UPSERT nedir?

UPSERT, normalde bir UNIQUE veya PRIMARY KEY kısıtını ihlal edecek olan bir INSERT işleminin otomatik olarak UPDATE'e (ya da hiçbir şey yapmamaya) dönüşmesidir. Yazımı şöyle: INSERT ... ON CONFLICT(sütun) DO UPDATE SET ... veya DO NOTHING. SQLite bu özelliği 3.24.0 sürümünden (2018) itibaren destekliyor.

SQLite UPSERT'te excluded tablosu nedir?

excluded, eklemeye çalıştığınız satırı tutan özel bir sözde tablodur. DO UPDATE SET ... içinde mevcut satıra sütun adıyla, reddedilen yeni satıra ise excluded.sütun şeklinde erişirsiniz. Yani SET price = excluded.price demek, "fiyatı yeni INSERT'in getirdiği değerle değiştir" demektir.

INSERT OR REPLACE ile UPSERT arasındaki fark nedir?

INSERT OR REPLACE çakışan satırı siler ve yerine yenisini ekler — bu da DELETE tetikleyicilerini çalıştırır, ON DELETE CASCADE'li yabancı anahtarları bozar ve belirtmediğiniz tüm sütunları varsayılan değerlerine döndürür. UPSERT ise mevcut satırı yerinde günceller; sadece SET içinde adını verdiğiniz sütunlar değişir. Gerçekten sil-ve-yeniden-ekle istemiyorsanız UPSERT'ü tercih edin.

SQLite'ta tek seferde birden çok satır upsert edilebilir mi?

Evet. INSERT INTO t(...) VALUES (...), (...), (...) ON CONFLICT(sütun) DO UPDATE SET ... sorunsuz çalışır. Her satır çakışma hedefine karşı tek tek kontrol edilir; DO UPDATE içindeki excluded satırı da çakışmayı tetikleyen o anki gelen satırı temsil eder.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA