Mert Özen Her Satırda Daha İleri
Veritabanı SQL Temelleri — #15

INSERT, UPDATE, DELETE ve MERGE: OUTPUT, Upsert ve WHERE'siz UPDATE'e Karşı Güvenli Çalışma

Mert Özen 8 Kas 2026 14 dk 4 görüntülenme
INSERT, UPDATE, DELETE ve MERGE: OUTPUT, Upsert ve WHERE'siz UPDATE'e Karşı Güvenli Çalışma

T-SQL'in UPDATE ... FROM'u bir hedef row'a birden çok kaynak row eşleşince hata vermez ve hangi değeri yazacağı belirsizdir; MERGE ise 8672 hatasıyla durur. Çok satırlı INSERT'i, OUTPUT ile yeni id'leri, standart correlated UPDATE'i, DELETE ile TRUNCATE'in log farkını, MERGE tuzaklarını, açık upsert'i ve @@ROWCOUNT kontrollü güvenli çalışmayı gerçek çıktılarla anlatıyoruz.

Sürüm notu: Bu yazıdaki SQL (Structured Query Language) örnekleri SQL Server 2025 (17.0, CU10 (Cumulative Update 10)) üzerinde serinin Bookstore veritabanıyla çalıştırılmıştır.

Önce Tahmin Et

Yayınevinden gelen yeni fiyat listesi price_updates tablosuna yüklendi. Listede bir hata var: Dune için iki row girilmiş, biri yayınevinin fiyatı (420,00), diğeri bir kampanya fiyatı (380,00). Hazırlık ve mevcut fiyatlar:

DROP TABLE IF EXISTS dbo.price_updates;
CREATE TABLE dbo.price_updates (
    isbn      CHAR(13)      NOT NULL,
    new_price DECIMAL(10,2) NOT NULL,
    source    VARCHAR(20)   NOT NULL
);
INSERT INTO dbo.price_updates (isbn, new_price, source)
SELECT isbn, 420.00, 'publisher' FROM dbo.books WHERE book_id = 26
UNION ALL
SELECT isbn, 380.00, 'campaign'  FROM dbo.books WHERE book_id = 26
UNION ALL
SELECT isbn, 560.00, 'publisher' FROM dbo.books WHERE book_id = 27;
SELECT book_id, title, list_price FROM dbo.books WHERE book_id IN (26, 27);
book_id title        list_price
------- ------------ ----------
     26 Dune             390.50
     27 Dune Messiah     530.50

Şimdi T-SQL'in (Transact-SQL) join'li UPDATE syntax'ı ile fiyatları güncelliyoruz. Dune'un iki eşleşmesi var. Sence ne olur: hata mı, iki güncelleme mi, yoksa başka bir şey mi?

SET NOCOUNT OFF;
UPDATE b
SET b.list_price = p.new_price
FROM dbo.books AS b
JOIN dbo.price_updates AS p ON p.isbn = b.isbn;
SELECT book_id, title, list_price FROM dbo.books WHERE book_id IN (26, 27);
(2 rows affected)
book_id title        list_price
------- ------------ ----------
     26 Dune             420.00
     27 Dune Messiah     560.00

Hata yok, uyarı yok, "2 rows affected". Dune 420,00 oldu; kampanya fiyatı sessizce yok sayıldı. Hangi row'un kazanacağını belirleyen bir kural da yok: Microsoft Learn bu durumda sonucun belirsiz (undefined) olduğunu açıkça yazar. Yarın plan değişirse aynı script 380,00 yazabilir. Aynı işi MERGE ile denersen SQL Server seni durdurur:

MERGE dbo.books AS b
USING dbo.price_updates AS p
   ON p.isbn = b.isbn
WHEN MATCHED THEN
    UPDATE SET b.list_price = p.new_price;
Msg 8672, Level 16, State 1, Server b8be943222fa, Line 1
The MERGE statement attempted to UPDATE or DELETE the same row more than once. This happens when a target row matches more than one source row. A MERGE statement cannot UPDATE/DELETE the same row of the target table multiple times. Refine the ON clause to ensure a target row matches at most one source row, or use the GROUP BY clause to group the source rows.

Bu yazı veriyi değiştiren statement'ları ele alıyor: INSERT, UPDATE, DELETE, TRUNCATE ve MERGE. Her birinde iki soru soracağız: standart ne diyor, T-SQL ne ekliyor ve hangi noktada seni sessizce yanıltabilir?

INSERT: Çok Satırlı VALUES ve INSERT ... SELECT

INSERT'in iki temel biçimi var. Birincisi değerleri doğrudan vermek; tek statement'ta birden çok row yazılabilir (SQL Server'da tek VALUES listesi en fazla 1000 row alır):

SET NOCOUNT OFF;
INSERT INTO dbo.authors (first_name, last_name, birth_year)
VALUES (N'Italo', N'Calvino', 1923),
       (N'Jorge Luis', N'Borges', 1899);
(2 rows affected)

Column listesini her zaman yaz. INSERT INTO dbo.authors VALUES (...) tablonun column sırasına bağlıdır; birisi tabloya yeni bir column eklediği gün kırılır, daha kötüsü tipler uyuyorsa yanlış column'a yazar. author_id IDENTITY olduğu için listede yok; değeri SQL Server üretir.

İkinci biçim bir sorgunun sonucunu eklemek. Stoğu yeniden sipariş seviyesinin altına düşen kitaplar için tedarik talebi oluşturuyoruz (reorder_requests bu yazı için kendi veritabanımda oluşturduğum bir tablo):

DROP TABLE IF EXISTS dbo.reorder_requests;
CREATE TABLE dbo.reorder_requests (
    request_id   INT IDENTITY(1,1) PRIMARY KEY,
    book_id      INT NOT NULL,
    quantity     INT NOT NULL,
    requested_at DATETIME2(0) NOT NULL DEFAULT (SYSUTCDATETIME())
);
SET NOCOUNT OFF;
INSERT INTO dbo.reorder_requests (book_id, quantity)
SELECT s.book_id, s.reorder_level * 2 - s.quantity_on_hand
FROM dbo.stock_items AS s
WHERE s.quantity_on_hand < s.reorder_level;
(15 rows affected)

Set-based düşünmenin yazmadaki karşılığı bu: 15 kitap için 15 ayrı INSERT göndermek yerine tek statement. Uygulama kodunda bir döngü içinde INSERT görüyorsan çoğu zaman tek bir INSERT ... SELECT ile değiştirilebilir.

OUTPUT ile Yeni Id'leri Almak

IDENTITY ile üretilen id'leri geri almanın eski yolu SCOPE_IDENTITY(); ama o yalnız son üretilen değeri döndürür. Çok satırlı bir insert'te bütün id'leri almak için OUTPUT clause'u kullanılır. inserted sanal tablosu eklenen row'ları tutar:

DECLARE @new_authors TABLE (author_id INT, last_name NVARCHAR(100));
INSERT INTO dbo.authors (first_name, last_name, birth_year)
OUTPUT inserted.author_id, inserted.last_name INTO @new_authors (author_id, last_name)
VALUES (N'Kazuo', N'Ishiguro', 1954),
       (N'Chimamanda Ngozi', N'Adichie', 1977);
SELECT author_id, last_name FROM @new_authors ORDER BY author_id;
author_id last_name
--------- ---------
       72 Ishiguro
       73 Adichie

OUTPUT ... INTO sonucu bir table variable'a yazar; böylece id'leri sonraki statement'ta kullanabilirsin. Gerçek hayattaki en tipik örnek bir siparişi kalemleriyle birlikte kaydetmek:

SET XACT_ABORT ON;
BEGIN TRANSACTION;
DECLARE @order TABLE (order_id INT);
INSERT INTO dbo.orders (customer_id, ordered_at, status)
OUTPUT inserted.order_id INTO @order (order_id)
VALUES (1, '2026-06-30 10:15:00', 'Pending');
INSERT INTO dbo.order_items (order_id, book_id, quantity, unit_price)
SELECT o.order_id, b.book_id, 1, b.list_price
FROM @order AS o
CROSS JOIN dbo.books AS b
WHERE b.book_id IN (39, 40);
COMMIT;
SELECT oi.order_id, oi.book_id, oi.quantity, oi.unit_price
FROM dbo.order_items AS oi
WHERE oi.order_id = (SELECT order_id FROM @order);
order_id book_id quantity unit_price
-------- ------- -------- ----------
     380      39        1     230.90
     380      40        1     600.00

Sipariş id'sini OUTPUT ile aldık ve kalemleri INSERT ... SELECT ile tek seferde ekledik. İki insert bir transaction içinde; SET XACT_ABORT ON sayesinde ikincisi hata verirse birincisi de geri alınır ve kalemsiz bir sipariş kalmaz. Dapper serisinin Execute ile INSERT, UPDATE, DELETE yazısında yeni id'yi uygulama tarafından okumayı görmüştük.

Diğer lehçelerde aynı işi RETURNING görür: PostgreSQL'de, SQLite 3.35'ten beri ve MariaDB'de INSERT ... RETURNING id. RETURNING ISO (International Organization for Standardization) standardının parçası değildir; MySQL'de ise yoktur ve LAST_INSERT_ID()'ye kalırsın. Lehçe farklarını bir sonraki yazıda topluca karşılaştıracağız.

UPDATE ... FROM ve Standart Correlated UPDATE

Standardın UPDATE'i tek bir tabloyu günceller; başka bir tabloya bakmak için subquery kullanırsın. T-SQL ise UPDATE ... FROM ile join'li güncellemeye izin verir. Algorithms kategorisindeki kitaplara %10 zam, iki şekilde:

UPDATE b
SET b.list_price = b.list_price * 1.10
OUTPUT inserted.book_id, deleted.list_price AS old_price, inserted.list_price AS new_price
FROM dbo.books AS b
JOIN dbo.categories AS c ON c.category_id = b.category_id
WHERE c.name = N'Algorithms';
book_id old_price new_price
------- --------- ---------
     65   1450.00   1595.00
     66   1150.00   1265.00
UPDATE dbo.books
SET list_price = list_price * 1.10
OUTPUT inserted.book_id, deleted.list_price AS old_price, inserted.list_price AS new_price
WHERE category_id IN (SELECT category_id FROM dbo.categories WHERE name = N'Algorithms');
book_id old_price new_price
------- --------- ---------
     65   1595.00   1754.50
     66   1265.00   1391.50

İkisi de aynı iki row'u güncelledi (ikincisi birincinin üzerine bir zam daha yaptı, çıktıdaki eski fiyatlar bu yüzden farklı). OUTPUT ile deleted (eski değer) ve inserted (yeni değer) sanal tablolarını birlikte görmek, bir güncellemenin ne yaptığını doğrulamanın en ucuz yolu.

Açılıştaki belirsizlik yalnız UPDATE ... FROM'a özgü. Standart biçimde yeni değer bir subquery'den gelir ve subquery birden çok row döndürürse serinin 11. yazısındaki 512 hatasını alırsın. Tedarik taleplerini stoğa ekleyen correlated UPDATE:

UPDATE dbo.stock_items
SET quantity_on_hand = quantity_on_hand + (SELECT SUM(r.quantity)
                                           FROM dbo.reorder_requests AS r
                                           WHERE r.book_id = stock_items.book_id)
OUTPUT inserted.book_id, deleted.quantity_on_hand AS before_qty, inserted.quantity_on_hand AS after_qty
WHERE EXISTS (SELECT 1 FROM dbo.reorder_requests AS r WHERE r.book_id = stock_items.book_id)
  AND book_id <= 20;
book_id before_qty after_qty
------- ---------- ---------
     16          4        10
     18          0        20
     19          6        20
     20          0        20

Burada SUM kullandığım için bir kitabın birden çok talebi olsa bile tek değer döner; tasarım gereği belirli. Çıktıyı kısa tutmak için ilk 20 kitapla sınırladım. Kural: UPDATE ... FROM yazıyorsan, join'in hedef tablodaki her row için en fazla bir eşleşme üreteceğinden emin ol; emin değilsen önce kaynağı GROUP BY ile tekilleştir. PostgreSQL de standart dışı bir UPDATE ... FROM sunar ve aynı belirsizliği taşır; MySQL ise UPDATE books AS b JOIN ... SET ... biçimini kullanır.

DELETE mi, TRUNCATE mı?

İkisi de row siler ama çok farklı araçlardır. Farkı görmek için order_items'ın bir kopyasını, ayrı bir IDENTITY column'u ile oluşturuyorum:

DROP TABLE IF EXISTS dbo.order_items_archive;
SELECT CAST(order_item_id AS INT) AS order_item_id, order_id, book_id, quantity, unit_price
INTO dbo.order_items_archive
FROM dbo.order_items;
ALTER TABLE dbo.order_items_archive ADD archive_id INT IDENTITY(1,1);

Şimdi aynı 682 row'u bir kez DELETE, bir kez TRUNCATE ile, transaction içinde silip transaction log'a ne yazıldığını sys.dm_tran_database_transactions ile okuyorum ve her seferinde geri alıyorum:

BEGIN TRANSACTION;
DELETE FROM dbo.order_items_archive;
SELECT dt.database_transaction_log_record_count AS log_records,
       dt.database_transaction_log_bytes_used   AS log_bytes
FROM sys.dm_tran_database_transactions AS dt
JOIN sys.dm_tran_current_transaction AS ct ON ct.transaction_id = dt.transaction_id
WHERE dt.database_id = DB_ID();
ROLLBACK;
log_records log_bytes
----------- ---------
        773     95128
BEGIN TRANSACTION;
TRUNCATE TABLE dbo.order_items_archive;
SELECT dt.database_transaction_log_record_count AS log_records,
       dt.database_transaction_log_bytes_used   AS log_bytes
FROM sys.dm_tran_database_transactions AS dt
JOIN sys.dm_tran_current_transaction AS ct ON ct.transaction_id = dt.transaction_id
WHERE dt.database_id = DB_ID();
ROLLBACK;
SELECT COUNT(*) AS rows_after_rollback FROM dbo.order_items_archive;
log_records log_bytes
----------- ---------
         15      1280
rows_after_rollback
-------------------
                682

DELETE her row'u tek tek log'ladı: 773 log kaydı, yaklaşık 95 KB. TRUNCATE ise row'ları değil, data sayfalarının serbest bırakılmasını log'ladı: 15 kayıt. Rollback sonrası 682 row geri geldi; yaygın inanışın aksine SQL Server'da TRUNCATE transaction içinde geri alınabilir. İki fark daha:

DELETE FROM dbo.order_items_archive;
INSERT INTO dbo.order_items_archive (order_item_id, order_id, book_id, quantity, unit_price) VALUES (1, 1, 1, 1, 1);
SELECT archive_id AS after_delete FROM dbo.order_items_archive;
TRUNCATE TABLE dbo.order_items_archive;
INSERT INTO dbo.order_items_archive (order_item_id, order_id, book_id, quantity, unit_price) VALUES (1, 1, 1, 1, 1);
SELECT archive_id AS after_truncate FROM dbo.order_items_archive;
after_delete
------------
         683
after_truncate
--------------
             1
TRUNCATE TABLE dbo.orders;
Msg 4712, Level 16, State 1, Server b8be943222fa, Line 1
Cannot truncate table 'dbo.orders' because it is being referenced by a FOREIGN KEY constraint.
ÖzellikDELETETRUNCATE TABLE
WHEREVarYok, bütün tablo
LogHer rowSayfa serbest bırakma
IDENTITYKaldığı yerden devam ederBaşlangıç değerine döner
Foreign key ile referans verilen tabloSilinebilir (constraint'ler kontrol edilir)Hata 4712
TriggerDELETE trigger'ları çalışırÇalışmaz
Gereken yetkiDELETEALTER

TRUNCATE TABLE standarda SQL:2008 ile girdi. Staging tablolarını boşaltmak için doğru araç; iş verisini silmek için neredeyse hiçbir zaman değil.

MERGE ve Tuzakları

MERGE, standarda SQL:2003 ile giren ve bir kaynağı hedefle eşleştirip eşleşenleri güncelleyen, eşleşmeyenleri ekleyen (ya da silen) tek statement. Depoya gelen sevkiyatı stoğa işlemek tipik bir kullanım; bazı kitapların stok row'u var, bazılarının yok:

DROP TABLE IF EXISTS dbo.stock_delivery;
CREATE TABLE dbo.stock_delivery (book_id INT NOT NULL PRIMARY KEY, quantity INT NOT NULL);
INSERT INTO dbo.stock_delivery VALUES (1, 5), (2, 3), (70, 10), (71, 4);
MERGE dbo.stock_items AS s
USING dbo.stock_delivery AS d
   ON d.book_id = s.book_id
WHEN MATCHED THEN
    UPDATE SET s.quantity_on_hand = s.quantity_on_hand + d.quantity,
               s.updated_at = SYSUTCDATETIME()
WHEN NOT MATCHED BY TARGET THEN
    INSERT (book_id, quantity_on_hand) VALUES (d.book_id, d.quantity)
OUTPUT $action AS action, inserted.book_id, deleted.quantity_on_hand AS before_qty, inserted.quantity_on_hand AS after_qty;
action book_id before_qty after_qty
------ ------- ---------- ---------
UPDATE       1         47        52
UPDATE       2         45        48
INSERT      70       NULL        10
INSERT      71       NULL         4

OUTPUT $action her row için yapılan işlemi gösteriyor: iki güncelleme, iki ekleme. Kâğıt üzerinde güzel. Pratikte ise MERGE'ün bilinen tuzakları var:

Kaynakta aynı hedef row'a eşleşen birden çok row varsa hata 8672 alırsın (açılıştaki örnek). Bu aslında iyi bir şey, ama kaynağı önceden tekilleştirmen gerekir. MERGE noktalı virgülle bitmek zorunda. Unutursan hata 10713:

MERGE dbo.stock_items AS s
USING dbo.stock_delivery AS d
   ON d.book_id = s.book_id
WHEN MATCHED THEN
    UPDATE SET s.quantity_on_hand = s.quantity_on_hand + 0
PRINT 'done';
Msg 10713, Level 15, State 1, Server b8be943222fa, Line 6
A MERGE statement must be terminated by a semi-colon (;).

Concurrency. Microsoft Learn'ün MERGE sayfası, aynı unique key'i hem ekleyip hem güncelleyen senaryolarda HOLDLOCK belirtilmesinin unique key ihlallerini önleyeceğini ve yoğun eşzamanlı yük altında ayrı INSERT/UPDATE mantığının daha iyi performans gösterebileceğini yazar. HOLDLOCK olmadan iki eşzamanlı MERGE aynı anda "row yok" görüp ikisi birden insert etmeye çalışabilir. Uzun yıllar boyunca MERGE ile ilgili çok sayıda bug raporu yayımlandı (filtered index, indexed view ve trigger'larla etkileşim). Bunların çoğu düzeltildi, ama birçok ekip MERGE'ü yalnız ETL (Extract, Transform, Load) işlerinde, tek oturumda çalışan batch'lerde kullanmayı tercih ediyor.

Açık Upsert: Önce UPDATE, Sonra INSERT

Uygulamadan gelen tek row'luk upsert için ben şu açık pattern'i tercih ediyorum:

DECLARE @book_id INT = 72, @qty INT = 6;
SET XACT_ABORT ON;
BEGIN TRANSACTION;
UPDATE dbo.stock_items WITH (UPDLOCK, SERIALIZABLE)
SET quantity_on_hand = quantity_on_hand + @qty, updated_at = SYSUTCDATETIME()
WHERE book_id = @book_id;
IF @@ROWCOUNT = 0
    INSERT INTO dbo.stock_items (book_id, quantity_on_hand) VALUES (@book_id, @qty);
COMMIT;
SELECT book_id, quantity_on_hand FROM dbo.stock_items WHERE book_id = @book_id;
book_id quantity_on_hand
------- ----------------
     72                6

The Elements of Style'ın (72) stok row'u yoktu; UPDATE 0 row etkiledi, INSERT çalıştı. UPDLOCK, SERIALIZABLE hint'leri UPDATE'in baktığı key aralığını transaction sonuna kadar kilitler; böylece iki eşzamanlı istek aynı anda "yok" deyip iki kez insert edemez. Kilitler, isolation level'lar ve bu hint'lerin tam olarak ne yaptığı SQL Server Derinlemesine serisinin konusu; burada bilmen gereken, upsert'in tek statement değil, korunması gereken iki adım olduğu.

WHERE'siz UPDATE'e Karşı Güvenli Çalışma

Production veritabanında herkesin en az bir kez yaşadığı an: WHERE satırını seçmeden statement'ı çalıştırmak. Algorithms kitaplarına %5 zam yapacaktık, WHERE unutuldu. Koruyucu script bunu yakalıyor:

SET XACT_ABORT ON;
BEGIN TRANSACTION;
DECLARE @expected INT = (SELECT COUNT(*) FROM dbo.books WHERE category_id = 12);
UPDATE dbo.books
SET list_price = list_price * 1.05;
DECLARE @affected INT = @@ROWCOUNT;
IF @affected <> @expected
BEGIN
    ROLLBACK;
    PRINT CONCAT('Rolled back: expected ', @expected, ' rows, affected ', @affected, '.');
END
ELSE
    COMMIT;
SELECT COUNT(*) AS books_priced_over_1500 FROM dbo.books WHERE list_price > 1500;
Rolled back: expected 2 rows, affected 77.
books_priced_over_1500
----------------------
                     1

Pattern şu: beklediğin row sayısını önce aynı filtreyle say, değişikliği transaction içinde yap, @@ROWCOUNT'u beklentiyle karşılaştır, uyuşmuyorsa ROLLBACK. 77 kitabın fiyatı bir anlığına değişti ama commit edilmedi; son SELECT yalnız önceki örneklerde zamlanan tek kitabı gösteriyor. Bu pattern'e birkaç alışkanlık daha ekle:

  • Elle yazdığın her UPDATE/DELETE'i önce aynı FROM ve WHERE ile SELECT olarak çalıştır, sonra ilk satırı değiştir.
  • SET XACT_ABORT ON kullan; hata olduğunda transaction açık kalmasın.
  • SSMS (SQL Server Management Studio) ya da Visual Studio Code'da (VS Code) çalışırken transaction'ı açık bırakıp öğle yemeğine çıkma; tuttuğun kilitler başkalarını bekletir.
  • Uygulama kodundan gelen değişikliklerde değerleri her zaman parameter olarak gönder. Web Güvenliği serisinin SQL Injection yazısı ve Dapper serisinin parameter'lar yazısı bunun neden pazarlık konusu olmadığını anlatıyor.

Üretimde Dikkat

Büyük bir DELETE ya da UPDATE'i tek statement'ta yapmak transaction log'u şişirir ve uzun süre kilit tutar. Milyonlarca row'u silerken işi DELETE TOP (5000) ... WHERE ... gibi küçük parçalara bölüp döngüde çalıştırmak yaygın yöntemdir; her parça kendi transaction'ında commit edilir. Bölmenin neden 5000 civarında yapıldığı (lock escalation) SQL Server Derinlemesine serisinde.

OUTPUT clause'unun bir kısıtı var: hedef tabloda etkin bir trigger varsa OUTPUT'u INTO olmadan kullanamazsın. Trigger'lı tablolarda OUTPUT ... INTO @table yaz.

MERGE ya da UPDATE ... FROM kullanan ETL script'lerinde kaynağın tekilliğini bir UNIQUE constraint ya da script başında bir kontrol sorgusuyla garanti et. Açılıştaki gibi sessiz belirsizlikler aylar sonra "bu fiyat nereden geldi" sorusu olarak döner.

Sık Düşülen Tuzaklar

UPDATE ... FROM'da hedef row'a birden çok kaynak row eşleşmesi. Hata vermez, hangi değerin yazılacağı belirsizdir.

INSERT'te column listesini yazmamak.

Çok satırlı insert sonrası id'leri SCOPE_IDENTITY() ile almaya çalışmak. Yalnız sonuncuyu verir; OUTPUT kullan.

TRUNCATE'i iş verisi için DELETE'in hızlı hâli sanmak. IDENTITY sıfırlanır, trigger çalışmaz, foreign key ile referans verilen tabloda hiç çalışmaz.

MERGE'ü eşzamanlı upsert için HOLDLOCK'suz kullanmak.

Elle yazılan UPDATE/DELETE'i transaction ve row sayısı kontrolü olmadan production'da çalıştırmak.

Küçük Bir Deneme

★☆☆ Hiç sipariş vermemiş müşteriler (37–40) için customers tablosundan silme sorgusunu önce SELECT olarak yaz, sonra transaction içinde DELETE olarak çalıştır, OUTPUT deleted.email ile silinenleri gör ve ROLLBACK yap.

★★☆ Açılıştaki price_updates tablosunu, her ISBN (International Standard Book Number) için source = 'campaign' row'u varsa onu, yoksa publisher row'unu seçecek şekilde tekilleştiren bir CTE (Common Table Expression) yaz ve UPDATE ... FROM'u bu CTE'ye karşı çalıştır. Aynı işi MERGE ile de yaz; artık 8672 hatası gelmemeli.

★★★ İki ayrı sorgu penceresinde aynı anda çalışacak bir upsert senaryosu kur: birinci pencerede BEGIN TRAN ile açık upsert'in UPDATE adımını çalıştır ve commit etme, ikinci pencerede aynı book_id için aynı script'i çalıştır. UPDLOCK, SERIALIZABLE hint'leriyle ve hint'ler olmadan ne olduğunu karşılaştır.

Bu yazıyla birlikte sorgu dilinin çekirdeğini tamamladık: okuma, gruplama, birleştirme, window function'lar ve yazma. Bir sonraki yazıda aynı sorguların SQL Server, PostgreSQL, MySQL ve SQLite'ta nasıl farklılaştığını, sayfalamadan upsert'e, identifier quoting'den boolean tipine kadar yan yana koyuyoruz.