SQL Server, PostgreSQL ve SQLite'ta eşzamanlılığa dayanıklı upsert
Upsert'in eşzamanlılık altında nasıl bozulduğu ve nasıl güvenli yazılacağı: SQL Server'da UPDLOCK ve SERIALIZABLE ya da MERGE, PostgreSQL'de ON CONFLICT, MERGE ve RETURNING, SQLite'ta yerleşik UPSERT ve INSERT OR REPLACE'in neden upsert olmadığı.
Upsert diye bilinen "varsa güncelle, yoksa ekle" mantığı her yerde karşına çıkar: bir ürün kataloğunu senkronize etmek, bir cihazın en son ne zaman görüldüğünü kaydetmek, bir sayacı güncel tutmak. Bu mantık genelde bir varlık kontrolü ve ardından gelen bir UPDATE ya da INSERT olarak başlar. Tek kullanıcılı her testten geçer, sonra eşzamanlılık altında, iki oturum aynı yeni anahtarı aynı anda işlediğinde bozulur.
Bu motorların her birinde artık iş için doğru bir araç var, ama her aracın da kendine özgü bir tuzağı var: SQL Server'da MERGE, PostgreSQL'de ON CONFLICT ile MERGE arasındaki seçim, SQLite'ta ise INSERT OR REPLACE. Bu yazıda naif sürümün neden bozulduğunu, her motor için eşzamanlılığa dayanıklı kalıbı ve production'da sorun çıkaran ayrıntıları anlatıyorum.
Önce kontrol, sonra işlem: naif sürümdeki race condition
Naif sürüm şöyle görünür:
IF EXISTS (SELECT 1 FROM dbo.Stock WHERE Sku = @Sku)
UPDATE dbo.Stock SET Qty = @Qty WHERE Sku = @Sku;
ELSE
INSERT dbo.Stock (Sku, Qty) VALUES (@Sku, @Qty);
İki oturum bunu yeni bir SKU için aynı anda çalıştırsın. İkisi de satırı bulamaz, ikisi de INSERT'e gider ve biri primary key ihlaliyle düşer. Unique constraint yoksa duplicate satır oluşur, ki bu daha kötü. Kodu bir transaction'a sarmak varsayılan READ COMMITTED isolation level'da işe yaramaz, çünkü varlık kontrolünün aldığı shared lock'lar hemen bırakılır. Kontrol ile işlem atomik olmak zorunda.
Araçların gelişimi
Uzun süre iyi bir yerleşik çözüm yoktu. SQL Server'da seçenekler yukarıdaki race'e açık kalıp, duplicate key hatasını yakalayan bir retry döngüsü ya da kendi eşzamanlılık uyarılarıyla gelen MERGE'dü. PostgreSQL 9.5'ten önce seçenekler PL/pgSQL döngüleri ya da writable CTE'lerdi. SQLite'ta ise genelde INSERT OR REPLACE'e başvurulurdu. Oysa bu komut çoğu kişinin sandığından farklı bir şey yapar. Bugün her motorun daha iyi bir seçeneği var.
SQL Server
UPDLOCK + SERIALIZABLE
Deneyimli pek çok SQL Server uzmanının önerdiği kalıp şöyle:
SET XACT_ABORT ON;
BEGIN TRANSACTION;
UPDATE dbo.Stock WITH (UPDLOCK, SERIALIZABLE)
SET Qty = @Qty
WHERE Sku = @Sku;
IF @@ROWCOUNT = 0
INSERT dbo.Stock (Sku, Qty) VALUES (@Sku, @Qty);
COMMIT TRANSACTION;
SERIALIZABLE, satır yokken bile bir key-range lock alır. Böylece ikinci oturum o boşluğa araya girip insert yapamaz. UPDLOCK iki oturumun deadlock'a girmek yerine sıraya girmesini sağlar. Hint olmadan ikisi de shared range lock alır, sonra ikisi de onu dönüştürmeye çalışır ve deadlock oluşur. Satır varsa maliyet tek bir seek, yoksa bir seek artı bir insert. Sku üzerinde unique index şart. Hem doğruluk için, hem de range lock'un bütün tabloyu değil dar bir anahtar aralığını kapsaması için.
SQL Server 2025'te optimized locking açıkken bu hint'lere hâlâ uyuluyor, ama onları taşıyan statement'lar lock after qualification'dan yararlanamıyor. Burada bu kabul edilebilir bir ödünleşim, çünkü hint'lerin varlık sebebi zaten doğruluk.
MERGE
MERGE dbo.Stock WITH (HOLDLOCK) AS t
USING (VALUES (@Sku, @Qty)) AS s (Sku, Qty)
ON t.Sku = s.Sku
WHEN MATCHED THEN
UPDATE SET Qty = s.Qty
WHEN NOT MATCHED THEN
INSERT (Sku, Qty) VALUES (s.Sku, s.Qty);
HOLDLOCK olmadan MERGE'de de IF EXISTS'teki race aynen var. Microsoft'un MERGE dokümantasyonu bu senaryoda HOLDLOCK'un unique key ihlallerini önlediğini söylüyor. Aynı sayfa, ölçek büyüdükçe MERGE'ün "might introduce complicated concurrency issues" diyerek karmaşık eşzamanlılık sorunlarına yol açabileceği konusunda uyarıyor ve yoğun eşzamanlılıkta ayrı statement'ların daha iyi performans verebileceğini belirtiyor. Buna yıllar içinde ayrıntılı biçimde belgelenmiş, toplulukça raporlanmış uzun bir bug geçmişini de ekleyince, önerim MERGE'ü sık çalışan tek satırlık upsert'ler için değil, dikkatle test edilmiş toplu senkronizasyon işleri için kullanmak.
PostgreSQL
INSERT ... ON CONFLICT (9.5+)
INSERT INTO stock (sku, qty)
VALUES ('A-100', 5)
ON CONFLICT (sku) DO UPDATE
SET qty = EXCLUDED.qty,
updated_at = now()
WHERE stock.qty IS DISTINCT FROM EXCLUDED.qty;
Dokümantasyon bu konuda net: ON CONFLICT DO UPDATE, yoğun eşzamanlılık altında bile atomik bir insert ya da update sonucunu garanti ediyor. EXCLUDED, eklemeye çalıştığın satırı temsil ediyor. Opsiyonel WHERE, hiçbir şey değişmediğinde update'i etkisiz hâle getiriyor ve böylece gereksiz yere yeni bir satır versiyonu yazılmıyor. Conflict target bir unique index ya da constraint ile eşleşmeli. Yalnızca "yoksa ekle" istiyorsan DO NOTHING kullan.
MERGE (15+)
MERGE INTO stock AS t
USING (VALUES ('A-100', 5)) AS s (sku, qty)
ON t.sku = s.sku
WHEN MATCHED THEN
UPDATE SET qty = s.qty
WHEN NOT MATCHED THEN
INSERT (sku, qty) VALUES (s.sku, s.qty)
RETURNING merge_action(), t.sku, t.qty;
MERGE PostgreSQL 15 ile geldi. PostgreSQL 17, merge_action() ile birlikte RETURNING desteğini ve WHEN NOT MATCHED BY SOURCE'u ekledi. Böylece silmeler dahil tam bir senkronizasyonu tek statement'ta yapmak mümkün oldu. Ama PostgreSQL dokümantasyonu, eşzamanlı insert'ler söz konusu olduğunda seni INSERT ... ON CONFLICT'e yönlendiriyor. MERGE normal isolation kurallarına tabi, dolayısıyla aynı yeni anahtarı ekleyen iki oturum yine unique ihlaline düşebilir. Benim kuralım şu: eşzamanlı upsert için ON CONFLICT, toplu mutabakat için MERGE.
PostgreSQL 18: RETURNING içinde OLD ve NEW
Bir satırın eklendiğini mi yoksa güncellendiğini mi ayırt etmek için eskiden iç yapıya dayanan xmax = 0 hilesi kullanılırdı. PostgreSQL 18 bunu açık hâle getiriyor:
INSERT INTO stock (sku, qty)
VALUES ('A-100', 5)
ON CONFLICT (sku) DO UPDATE SET qty = EXCLUDED.qty
RETURNING old.qty AS old_qty,
new.qty AS new_qty,
(old.sku IS NULL) AS inserted;
Düz bir insert'te bütün old değerleri NULL. ON CONFLICT DO UPDATE'in update yolunda ise önceki satırı tutuyor.
SQLite
INSERT OR REPLACE: önce silme, sonra ekleme
REPLACE bir conflict resolution stratejisi. Bir unique ya da primary key constraint ihlal edildiğinde SQLite mevcut satırı siler ve yenisini ekler. Bunun sonuçları:
- Belirtmediğin kolonlar default değerlerine ya da NULL'a döner. Eski değerler kaybolur.
- Çakışma
INTEGER PRIMARY KEYdışında bir unique kolondaysa, yeni satır farklı bir rowid alabilir. - Delete trigger'ları yalnızca
recursive_triggersaçıksa tetiklenir. Silinen satır için update hook çağrılmaz. - O satıra referans veren foreign key'ler bir delete ve ardından bir insert görür. Cascade tanımlarının yakınında bunun olmasını istemezsin. Kendi şeman üzerinde test et.
Yerleşik UPSERT (3.24.0+)
INSERT INTO stock (sku, qty)
VALUES ('A-100', 5)
ON CONFLICT (sku) DO UPDATE
SET qty = excluded.qty,
updated_at = CURRENT_TIMESTAMP
WHERE excluded.qty <> stock.qty;
UPSERT, SQLite 3.24.0 (2018-06-04) ile geldi. 3.35.0'dan (2021-03-12) beri birden fazla ON CONFLICT clause'u art arda yazabiliyorsun ve sonuncusu conflict target olmadan DO UPDATE olabiliyor. DO UPDATE içinde yalın bir kolon adı mevcut değeri, excluded. ise eklemeye çalıştığın değeri ifade ediyor.
Değerler bir SELECT'ten geldiğinde bir parser tuhaflığı var. ON bir join koşulu olarak okunabiliyor, bu yüzden dokümantasyon WHERE true bile olsa her zaman bir WHERE eklemeyi öneriyor:
INSERT INTO stock (sku, qty)
SELECT sku, qty FROM staging WHERE true
ON CONFLICT (sku) DO UPDATE SET qty = excluded.qty;
UPSERT virtual table'larda çalışmıyor ve yalnızca uniqueness çakışmalarını ele alıyor. NOT NULL, CHECK ve foreign key ihlalleri hâlâ hata veriyor.
Tuzaklar
- Unique constraint olmaması. Bu yazıdaki her kalıp buna dayanıyor. O olmadan duplicate'leri durduracak hiçbir şey yok.
- Aynı batch'te tekrar eden anahtarlar. PostgreSQL,
ON CONFLICT DO UPDATE'in aynı statement'ta aynı satırı iki kez etkilemesine izin vermiyor.MERGEise cardinality violation hatası veriyor. Girdiyi önce tekilleştir. - Sequence boşlukları. PostgreSQL'de default
nextval()çakışma tespit edilmeden önce çalışıyor, dolayısıyla upsert'ler ID tüketiyor. Zararsız, ama insanların gözüne çarpıyor. - Anlamadan retry yapmak. Duplicate key hatasını yakalayıp tekrar deneyen bir döngü işe yarayabilir, ama fazladan round trip ekler ve tasarım sorunlarını gizler.
Özet ve kontrol listesi
PostgreSQL ve SQLite'ta doğru varsayılan INSERT ... ON CONFLICT ... DO UPDATE. PostgreSQL'de bu komutun eşzamanlılık altında atomik olduğu dokümante edilmiş durumda. SQL Server'da tek satırlık upsert için güvenli kalıp, tek bir transaction içinde UPDATE WITH (UPDLOCK, SERIALIZABLE) ve ardından koşullu bir INSERT. MERGE'ü test edilmiş toplu işlere sakla, INSERT OR REPLACE'i de kimliği önemli olan satırlardan uzak tut.
- İş anahtarına unique constraint koy.
- PostgreSQL ve SQLite'ta gelen değerler için
EXCLUDED/excludedileINSERT ... ON CONFLICT (key) DO UPDATEkullan. - SQL Server'da hepsini tek transaction içinde yap:
UPDATE WITH (UPDLOCK, SERIALIZABLE), hiçbir satır güncellenmediyseINSERT. MERGE'ü test edilmiş toplu senkronizasyonlarda kullan, SQL Server'da her zamanHOLDLOCKile.- SQLite'ta "güncelle" demek istiyorsan asla
INSERT OR REPLACEkullanma. - Tek oturumla değil, aynı anda çalışan iki oturumla test et.
- ---
- baslik: "SQL Server, PostgreSQL ve SQLite'ta eşzamanlılığa dayanıklı upsert"
- ozet: "Upsert'in eşzamanlılık altında nasıl bozulduğu ve nasıl güvenli yazılacağı: SQL Server'da UPDLOCK ve SERIALIZABLE ya da MERGE, PostgreSQL'de ON CONFLICT, MERGE ve RETURNING, SQLite'ta yerleşik UPSERT ve INSERT OR REPLACE'in neden upsert olmadığı."
- tarih: 2026-10-12
- etiketler:
- - sql
- - sql-server
- - postgresql
- - sqlite
- kategoriler:
- - veritabani
- ---
- Upsert diye bilinen "varsa güncelle, yoksa ekle" mantığı her yerde karşına çıkar: bir ürün kataloğunu senkronize etmek, bir cihazın en son ne zaman görüldüğünü kaydetmek, bir sayacı güncel tutmak. Bu mantık genelde bir varlık kontrolü ve ardından gelen bir `UPDATE` ya da `INSERT` olarak başlar. Tek kullanıcılı her testten geçer, sonra eşzamanlılık altında, iki oturum aynı yeni anahtarı aynı anda işlediğinde bozulur.
- Bu motorların her birinde artık iş için doğru bir araç var, ama her aracın da kendine özgü bir tuzağı var: SQL Server'da `MERGE`, PostgreSQL'de `ON CONFLICT` ile `MERGE` arasındaki seçim, SQLite'ta ise `INSERT OR REPLACE`. Bu yazıda naif sürümün neden bozulduğunu, her motor için eşzamanlılığa dayanıklı kalıbı ve production'da sorun çıkaran ayrıntıları anlatıyorum.
- ## Önce kontrol, sonra işlem: naif sürümdeki race condition
- Naif sürüm şöyle görünür:
- ```sql
- IF EXISTS (SELECT 1 FROM dbo.Stock WHERE Sku = @Sku)
- UPDATE dbo.Stock SET Qty = @Qty WHERE Sku = @Sku;
- ELSE
- INSERT dbo.Stock (Sku, Qty) VALUES (@Sku, @Qty);
- ```
- İki oturum bunu yeni bir SKU için aynı anda çalıştırsın. İkisi de satırı bulamaz, ikisi de `INSERT`'e gider ve biri primary key ihlaliyle düşer. Unique constraint yoksa duplicate satır oluşur, ki bu daha kötü. Kodu bir transaction'a sarmak varsayılan `READ COMMITTED` isolation level'da işe yaramaz, çünkü varlık kontrolünün aldığı shared lock'lar hemen bırakılır. Kontrol ile işlem atomik olmak zorunda.
- ## Araçların gelişimi
- Uzun süre iyi bir yerleşik çözüm yoktu. SQL Server'da seçenekler yukarıdaki race'e açık kalıp, duplicate key hatasını yakalayan bir retry döngüsü ya da kendi eşzamanlılık uyarılarıyla gelen `MERGE`'dü. PostgreSQL 9.5'ten önce seçenekler PL/pgSQL döngüleri ya da writable CTE'lerdi. SQLite'ta ise genelde `INSERT OR REPLACE`'e başvurulurdu. Oysa bu komut çoğu kişinin sandığından farklı bir şey yapar. Bugün her motorun daha iyi bir seçeneği var.
- ## SQL Server
- ### UPDLOCK + SERIALIZABLE
- Deneyimli pek çok SQL Server uzmanının önerdiği kalıp şöyle:
- ```sql
- SET XACT_ABORT ON;
- BEGIN TRANSACTION;
- UPDATE dbo.Stock WITH (UPDLOCK, SERIALIZABLE)
- SET Qty = @Qty
- WHERE Sku = @Sku;
- IF @@ROWCOUNT = 0
- INSERT dbo.Stock (Sku, Qty) VALUES (@Sku, @Qty);
- COMMIT TRANSACTION;
- ```
- `SERIALIZABLE`, satır yokken bile bir key-range lock alır. Böylece ikinci oturum o boşluğa araya girip insert yapamaz. `UPDLOCK` iki oturumun deadlock'a girmek yerine sıraya girmesini sağlar. Hint olmadan ikisi de shared range lock alır, sonra ikisi de onu dönüştürmeye çalışır ve deadlock oluşur. Satır varsa maliyet tek bir seek, yoksa bir seek artı bir insert. `Sku` üzerinde unique index şart. Hem doğruluk için, hem de range lock'un bütün tabloyu değil dar bir anahtar aralığını kapsaması için.
- SQL Server 2025'te optimized locking açıkken bu hint'lere hâlâ uyuluyor, ama onları taşıyan statement'lar lock after qualification'dan yararlanamıyor. Burada bu kabul edilebilir bir ödünleşim, çünkü hint'lerin varlık sebebi zaten doğruluk.
- ### MERGE
- ```sql
- MERGE dbo.Stock WITH (HOLDLOCK) AS t
- USING (VALUES (@Sku, @Qty)) AS s (Sku, Qty)
- ON t.Sku = s.Sku
- WHEN MATCHED THEN
- UPDATE SET Qty = s.Qty
- WHEN NOT MATCHED THEN
- INSERT (Sku, Qty) VALUES (s.Sku, s.Qty);
- ```
- `HOLDLOCK` olmadan `MERGE`'de de `IF EXISTS`'teki race aynen var. Microsoft'un MERGE dokümantasyonu bu senaryoda `HOLDLOCK`'un unique key ihlallerini önlediğini söylüyor. Aynı sayfa, ölçek büyüdükçe `MERGE`'ün "might introduce complicated concurrency issues" diyerek karmaşık eşzamanlılık sorunlarına yol açabileceği konusunda uyarıyor ve yoğun eşzamanlılıkta ayrı statement'ların daha iyi performans verebileceğini belirtiyor. Buna yıllar içinde ayrıntılı biçimde belgelenmiş, toplulukça raporlanmış uzun bir bug geçmişini de ekleyince, önerim `MERGE`'ü sık çalışan tek satırlık upsert'ler için değil, dikkatle test edilmiş toplu senkronizasyon işleri için kullanmak.
- ## PostgreSQL
- ### INSERT ... ON CONFLICT (9.5+)
- ```sql
- INSERT INTO stock (sku, qty)
- VALUES ('A-100', 5)
- ON CONFLICT (sku) DO UPDATE
- SET qty = EXCLUDED.qty,
- updated_at = now()
- WHERE stock.qty IS DISTINCT FROM EXCLUDED.qty;
- ```
- Dokümantasyon bu konuda net: `ON CONFLICT DO UPDATE`, yoğun eşzamanlılık altında bile atomik bir insert ya da update sonucunu garanti ediyor. `EXCLUDED`, eklemeye çalıştığın satırı temsil ediyor. Opsiyonel `WHERE`, hiçbir şey değişmediğinde update'i etkisiz hâle getiriyor ve böylece gereksiz yere yeni bir satır versiyonu yazılmıyor. Conflict target bir unique index ya da constraint ile eşleşmeli. Yalnızca "yoksa ekle" istiyorsan `DO NOTHING` kullan.
- ### MERGE (15+)
- ```sql
- MERGE INTO stock AS t
- USING (VALUES ('A-100', 5)) AS s (sku, qty)
- ON t.sku = s.sku
- WHEN MATCHED THEN
- UPDATE SET qty = s.qty
- WHEN NOT MATCHED THEN
- INSERT (sku, qty) VALUES (s.sku, s.qty)
- RETURNING merge_action(), t.sku, t.qty;
- ```
- `MERGE` PostgreSQL 15 ile geldi. PostgreSQL 17, `merge_action()` ile birlikte `RETURNING` desteğini ve `WHEN NOT MATCHED BY SOURCE`'u ekledi. Böylece silmeler dahil tam bir senkronizasyonu tek statement'ta yapmak mümkün oldu. Ama PostgreSQL dokümantasyonu, eşzamanlı insert'ler söz konusu olduğunda seni `INSERT ... ON CONFLICT`'e yönlendiriyor. `MERGE` normal isolation kurallarına tabi, dolayısıyla aynı yeni anahtarı ekleyen iki oturum yine unique ihlaline düşebilir. Benim kuralım şu: eşzamanlı upsert için `ON CONFLICT`, toplu mutabakat için `MERGE`.
- ### PostgreSQL 18: RETURNING içinde OLD ve NEW
- Bir satırın eklendiğini mi yoksa güncellendiğini mi ayırt etmek için eskiden iç yapıya dayanan `xmax = 0` hilesi kullanılırdı. PostgreSQL 18 bunu açık hâle getiriyor:
- ```sql
- INSERT INTO stock (sku, qty)
- VALUES ('A-100', 5)
- ON CONFLICT (sku) DO UPDATE SET qty = EXCLUDED.qty
- RETURNING old.qty AS old_qty,
- new.qty AS new_qty,
- (old.sku IS NULL) AS inserted;
- ```
- Düz bir insert'te bütün `old` değerleri NULL. `ON CONFLICT DO UPDATE`'in update yolunda ise önceki satırı tutuyor.
- ## SQLite
- ### INSERT OR REPLACE: önce silme, sonra ekleme
- `REPLACE` bir conflict resolution stratejisi. Bir unique ya da primary key constraint ihlal edildiğinde SQLite mevcut satırı **siler** ve yenisini ekler. Bunun sonuçları:
- - Belirtmediğin kolonlar default değerlerine ya da NULL'a döner. Eski değerler kaybolur.
- - Çakışma `INTEGER PRIMARY KEY` dışında bir unique kolondaysa, yeni satır farklı bir rowid alabilir.
- - Delete trigger'ları yalnızca `recursive_triggers` açıksa tetiklenir. Silinen satır için update hook çağrılmaz.
- - O satıra referans veren foreign key'ler bir delete ve ardından bir insert görür. Cascade tanımlarının yakınında bunun olmasını istemezsin. Kendi şeman üzerinde test et.
- ### Yerleşik UPSERT (3.24.0+)
- ```sql
- INSERT INTO stock (sku, qty)
- VALUES ('A-100', 5)
- ON CONFLICT (sku) DO UPDATE
- SET qty = excluded.qty,
- updated_at = CURRENT_TIMESTAMP
- WHERE excluded.qty <> stock.qty;
- ```
- UPSERT, SQLite 3.24.0 (2018-06-04) ile geldi. 3.35.0'dan (2021-03-12) beri birden fazla `ON CONFLICT` clause'u art arda yazabiliyorsun ve sonuncusu conflict target olmadan `DO UPDATE` olabiliyor. `DO UPDATE` içinde yalın bir kolon adı mevcut değeri, `excluded.` ise eklemeye çalıştığın değeri ifade ediyor.
- Değerler bir `SELECT`'ten geldiğinde bir parser tuhaflığı var. `ON` bir join koşulu olarak okunabiliyor, bu yüzden dokümantasyon `WHERE true` bile olsa her zaman bir `WHERE` eklemeyi öneriyor:
- ```sql
- INSERT INTO stock (sku, qty)
- SELECT sku, qty FROM staging WHERE true
- ON CONFLICT (sku) DO UPDATE SET qty = excluded.qty;
- ```
- UPSERT virtual table'larda çalışmıyor ve yalnızca uniqueness çakışmalarını ele alıyor. `NOT NULL`, `CHECK` ve foreign key ihlalleri hâlâ hata veriyor.
- ## Tuzaklar
- - **Unique constraint olmaması.** Bu yazıdaki her kalıp buna dayanıyor. O olmadan duplicate'leri durduracak hiçbir şey yok.
- - **Aynı batch'te tekrar eden anahtarlar.** PostgreSQL, `ON CONFLICT DO UPDATE`'in aynı statement'ta aynı satırı iki kez etkilemesine izin vermiyor. `MERGE` ise cardinality violation hatası veriyor. Girdiyi önce tekilleştir.
- - **Sequence boşlukları.** PostgreSQL'de default `nextval()` çakışma tespit edilmeden önce çalışıyor, dolayısıyla upsert'ler ID tüketiyor. Zararsız, ama insanların gözüne çarpıyor.
- - **Anlamadan retry yapmak.** Duplicate key hatasını yakalayıp tekrar deneyen bir döngü işe yarayabilir, ama fazladan round trip ekler ve tasarım sorunlarını gizler.
- ## Özet ve kontrol listesi
- PostgreSQL ve SQLite'ta doğru varsayılan `INSERT ... ON CONFLICT ... DO UPDATE`. PostgreSQL'de bu komutun eşzamanlılık altında atomik olduğu dokümante edilmiş durumda. SQL Server'da tek satırlık upsert için güvenli kalıp, tek bir transaction içinde `UPDATE WITH (UPDLOCK, SERIALIZABLE)` ve ardından koşullu bir `INSERT`. `MERGE`'ü test edilmiş toplu işlere sakla, `INSERT OR REPLACE`'i de kimliği önemli olan satırlardan uzak tut.
- 1. İş anahtarına unique constraint koy.
- 2. PostgreSQL ve SQLite'ta gelen değerler için `EXCLUDED`/`excluded` ile `INSERT ... ON CONFLICT (key) DO UPDATE` kullan.
- 3. SQL Server'da hepsini tek transaction içinde yap: `UPDATE WITH (UPDLOCK, SERIALIZABLE)`, hiçbir satır güncellenmediyse `INSERT`.
- 4. `MERGE`'ü test edilmiş toplu senkronizasyonlarda kullan, SQL Server'da her zaman `HOLDLOCK` ile.
- 5. SQLite'ta "güncelle" demek istiyorsan asla `INSERT OR REPLACE` kullanma.
- 6. Tek oturumla değil, aynı anda çalışan iki oturumla test et.