MariaDB ve MySQL’de ON DUPLICATE KEY UPDATE ile Upsert İşlemi
Veritabanı yönetiminde en sık karşılaşılan senaryolardan biri şudur: “Bu kayıt varsa güncelle, yoksa ekle.” Bu işlem için ayrı ayrı SELECT, INSERT ve UPDATE sorguları yazmak hem zahmetli hem de race condition gibi sorunlara açık bir yaklaşımdır. MySQL ve MariaDB’nin sunduğu ON DUPLICATE KEY UPDATE sözdizimi tam olarak bu sorunu tek bir atomik işlemle çözer. Upsert (update + insert) olarak da bilinen bu yöntem, üretim ortamlarında sayaçlar, önbellekler, istatistik tabloları ve senkronizasyon senaryolarında hayat kurtarır.
ON DUPLICATE KEY UPDATE Nedir?
ON DUPLICATE KEY UPDATE, bir INSERT sorgusuna eklenen ve şunu söyleyen bir uzantıdır: “Eğer eklemeye çalıştığın kayıt birincil anahtar (PRIMARY KEY) veya benzersiz indeks (UNIQUE INDEX) ihlali yaratıyorsa, INSERT yapmak yerine şu UPDATE işlemini uygula.”
Bu yaklaşımın en büyük avantajı atomikliktir. Uygulama katmanında önce SELECT, sonra koşula göre INSERT veya UPDATE yapan mantık, iki farklı istek arasında başka bir process’in araya girmesiyle tutarsız sonuçlar üretebilir. ON DUPLICATE KEY UPDATE bu riski tamamen ortadan kaldırır.
Temel sözdizimi şu şekildedir:
INSERT INTO tablo_adi (kolon1, kolon2, kolon3)
VALUES (deger1, deger2, deger3)
ON DUPLICATE KEY UPDATE
kolon2 = VALUES(kolon2),
kolon3 = VALUES(kolon3);
Burada VALUES(kolon_adi) ifadesi, INSERT edilmeye çalışılan ama çakışma yüzünden kullanılamayan değeri ifade eder. Yani “INSERT’te ne gönderdiysem onu kullan” demektir.
Temel Bir Örnek ile Başlayalım
Diyelim ki bir web sitesinin ziyaretçi sayacını tutan basit bir tablo var:
CREATE TABLE sayfa_istatistikleri (
sayfa_id INT NOT NULL,
tarih DATE NOT NULL,
gorunum INT DEFAULT 0,
tekil_ziyaret INT DEFAULT 0,
PRIMARY KEY (sayfa_id, tarih),
INDEX idx_tarih (tarih)
) ENGINE=InnoDB;
Her sayfa görüntülendiğinde bu tabloya kayıt atmak istiyoruz. Eğer o gün için kayıt yoksa yeni ekle, varsa sayacı artır:
INSERT INTO sayfa_istatistikleri (sayfa_id, tarih, gorunum, tekil_ziyaret)
VALUES (42, CURDATE(), 1, 1)
ON DUPLICATE KEY UPDATE
gorunum = gorunum + 1,
tekil_ziyaret = tekil_ziyaret + 1;
Bu sorgu her çalıştığında şu işi yapar:
- Eğer bugün için
sayfa_id = 42kaydı yoksa yeni satır ekler, her iki sayaç 1 olur. - Eğer kayıt varsa sadece iki sayacı birer artırır, hiçbir şey eklemez.
Uygulama katmanında 3-4 satır mantık yerine tek bir SQL satırı. Güzel, değil mi?
VALUES() Fonksiyonu vs Doğrudan Değer
Upsert yazarken iki farklı yaklaşım göreceksiniz. Farkı anlamak önemli:
-- Yaklaşım 1: VALUES() fonksiyonu ile (önerilen)
INSERT INTO urunler (urun_kodu, stok_miktari, son_guncelleme)
VALUES ('SKU-1234', 50, NOW())
ON DUPLICATE KEY UPDATE
stok_miktari = VALUES(stok_miktari),
son_guncelleme = VALUES(son_guncelleme);
-- Yaklaşım 2: Doğrudan değer yazarak
INSERT INTO urunler (urun_kodu, stok_miktari, son_guncelleme)
VALUES ('SKU-1234', 50, NOW())
ON DUPLICATE KEY UPDATE
stok_miktari = 50,
son_guncelleme = NOW();
Yaklaşım 1 daha temiz ve esnek çünkü değeri tek yerde tanımlıyorsunuz. Çok satırlı INSERT işlemlerinde VALUES() fonksiyonu hangi satırın değerini kullanacağını otomatik çözüyor. Yaklaşım 2’de NOW() iki kez çağrılacağından teorik olarak farklı bir zaman damgası üretebilir, bu tür edge case’lerden kaçınmak için her zaman VALUES() tercih edin.
Not: MariaDB 10.3.3+ ve MySQL 8.0.19+ sürümlerinde VALUES() fonksiyonu deprecated sayılıyor ve bunun yerine alias kullanımı öneriliyor:
-- Modern sözdizimi (MariaDB 10.3.3+ / MySQL 8.0.19+)
INSERT INTO urunler (urun_kodu, stok_miktari, son_guncelleme)
VALUES ('SKU-1234', 50, NOW()) AS yeni
ON DUPLICATE KEY UPDATE
stok_miktari = yeni.stok_miktari,
son_guncelleme = yeni.son_guncelleme;
Çok Satırlı Upsert İşlemleri
ON DUPLICATE KEY UPDATE‘in gerçek gücü çok satırlı INSERT işlemlerinde ortaya çıkar. Örneğin bir dış sistemden toplu ürün güncellemesi alıyorsunuz:
INSERT INTO urunler (urun_kodu, urun_adi, fiyat, kategori_id)
VALUES
('SKU-001', 'Laptop Pro 15', 12500.00, 5),
('SKU-002', 'Kablosuz Fare', 450.00, 8),
('SKU-003', 'USB-C Hub', 320.00, 8),
('SKU-004', 'Mekanik Klavye', 1200.00, 8),
('SKU-005', 'Webcam HD', 750.00, 9)
ON DUPLICATE KEY UPDATE
urun_adi = VALUES(urun_adi),
fiyat = VALUES(fiyat),
kategori_id = VALUES(kategori_id);
Bu tek sorgu 5 satırı işler. Her biri için ayrı ayrı “var mı yok mu” kontrolü yapılmaz, veritabanı motoru bunu kendi içinde halleder. 5000 satırlık bir CSV import işlemi için bu performans farkı çok ciddi boyutlara ulaşır.
Etkilenen Satır Sayısı ve ROW_COUNT()
Bu noktada bir detayı paylaşmak isterim çünkü başlangıçta kafamı çok karıştırmıştı. ON DUPLICATE KEY UPDATE işleminin ardından ROW_COUNT() fonksiyonu veya bağlantı kütüphanesinin döndürdüğü “etkilenen satır sayısı” beklenenden farklı çıkabilir:
- 1 döndürürse: Yeni satır eklendi (INSERT yapıldı).
- 2 döndürürse: Mevcut satır güncellendi (UPDATE yapıldı).
- 0 döndürürse: Satır zaten vardı ama UPDATE sonrasında hiçbir değer değişmedi.
INSERT INTO ayarlar (anahtar, deger)
VALUES ('max_baglanti', '100')
ON DUPLICATE KEY UPDATE
deger = VALUES(deger);
SELECT ROW_COUNT() AS etkilenen_satir;
-- İlk çalıştırmada: 1 (INSERT)
-- İkinci çalıştırmada (aynı değerle): 0 (hiçbir şey değişmedi)
-- Farklı değerle: 2 (UPDATE)
Uygulama katmanında bu değeri kontrol ediyorsanız 0 ve 2’nin her ikisinin de “kayıt zaten vardı” anlamına geldiğini unutmayın.
Gerçek Dünya Senaryosu: Kullanıcı Oturum Takibi
Bir SaaS uygulamasında kullanıcıların son oturum bilgilerini tuttuğunuzu düşünün. Her login işleminde bu tabloyu güncellemek istiyorsunuz:
CREATE TABLE kullanici_oturum (
kullanici_id BIGINT NOT NULL,
son_giris DATETIME NOT NULL,
giris_ip VARCHAR(45) NOT NULL,
toplam_giris INT DEFAULT 1,
son_user_agent VARCHAR(512),
PRIMARY KEY (kullanici_id)
) ENGINE=InnoDB;
-- Her login işleminde çağrılan sorgu
INSERT INTO kullanici_oturum
(kullanici_id, son_giris, giris_ip, toplam_giris, son_user_agent)
VALUES
(1001, NOW(), '85.103.45.67', 1, 'Mozilla/5.0 (Windows NT 10.0)...')
ON DUPLICATE KEY UPDATE
son_giris = VALUES(son_giris),
giris_ip = VALUES(giris_ip),
toplam_giris = toplam_giris + 1,
son_user_agent = VALUES(son_user_agent);
Dikkat edin: toplam_giris için VALUES() kullanmıyoruz çünkü INSERT’te gelen değer 1, ama güncelleme sırasında mevcut değere 1 eklemek istiyoruz. Bu esneklik ON DUPLICATE KEY UPDATE‘i gerçekten güçlü kılan şeydir: UPDATE kısmında INSERT değerini de mevcut değeri de kullanabilirsiniz.
Gerçek Dünya Senaryosu: Ürün Envanter Senkronizasyonu
Bir ERP sisteminden periyodik olarak envanter verisi çektiğinizi ve bunu MariaDB’nizde tuttuğunuzu düşünün. Her senkronizasyonda hem yeni ürünler eklemeli hem de mevcut ürünleri güncellemeliyiz:
CREATE TABLE envanter (
sku VARCHAR(50) NOT NULL,
depo_kodu VARCHAR(10) NOT NULL,
miktar DECIMAL(10,2) DEFAULT 0,
rezerv_miktar DECIMAL(10,2) DEFAULT 0,
son_sync TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sync_kaynak VARCHAR(50),
PRIMARY KEY (sku, depo_kodu),
INDEX idx_son_sync (son_sync)
) ENGINE=InnoDB;
-- ERP'den gelen toplu veri senkronizasyonu
INSERT INTO envanter (sku, depo_kodu, miktar, rezerv_miktar, son_sync, sync_kaynak)
VALUES
('P-10045', 'DEP-IST', 150.00, 20.00, NOW(), 'ERP-SYNC-v2'),
('P-10045', 'DEP-ANK', 45.00, 5.00, NOW(), 'ERP-SYNC-v2'),
('P-20871', 'DEP-IST', 0.00, 0.00, NOW(), 'ERP-SYNC-v2'),
('P-33412', 'DEP-IZM', 230.00, 15.00, NOW(), 'ERP-SYNC-v2')
ON DUPLICATE KEY UPDATE
miktar = VALUES(miktar),
rezerv_miktar = VALUES(rezerv_miktar),
son_sync = VALUES(son_sync),
sync_kaynak = VALUES(sync_kaynak);
Bu senaryo için önemli bir optimizasyon notu: Eğer binlerce satır senkronize ediyorsanız sorguyu transaction içine alın:
START TRANSACTION;
INSERT INTO envanter (sku, depo_kodu, miktar, rezerv_miktar, son_sync, sync_kaynak)
VALUES
('P-10045', 'DEP-IST', 150.00, 20.00, NOW(), 'ERP-SYNC-v2'),
('P-10045', 'DEP-ANK', 45.00, 5.00, NOW(), 'ERP-SYNC-v2')
-- ... binlerce satır
ON DUPLICATE KEY UPDATE
miktar = VALUES(miktar),
rezerv_miktar = VALUES(rezerv_miktar),
son_sync = VALUES(son_sync),
sync_kaynak = VALUES(sync_kaynak);
COMMIT;
AUTO_INCREMENT ve Dikkat Edilmesi Gereken Bir Tuzak
ON DUPLICATE KEY UPDATE ile çalışırken AUTO_INCREMENT sütunlarında bir davranışa dikkat etmek gerekir. Her çakışmada, yani UPDATE yapıldığında bile, AUTO_INCREMENT sayacı artabilir. Bu InnoDB’nin çalışma şeklinden kaynaklanır.
CREATE TABLE log_ozet (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
log_tipi VARCHAR(50) UNIQUE NOT NULL,
adet INT DEFAULT 0,
son_tarih DATETIME
) ENGINE=InnoDB;
-- Bu sorgu her çalıştığında AUTO_INCREMENT artabilir
-- Update yapılsa bile id sayacı boşluklar bırakabilir
INSERT INTO log_ozet (log_tipi, adet, son_tarih)
VALUES ('ERROR', 1, NOW())
ON DUPLICATE KEY UPDATE
adet = adet + 1,
son_tarih = NOW();
Yüksek frekanslı upsert işlemlerinde AUTO_INCREMENT değerlerinde boşluklar oluşur. Bu genellikle sorun değildir çünkü AUTO_INCREMENT zaten boşluksuz ardışık değer garantisi vermez. Ama id değerlerinin her zaman ardışık olmasını bekleyen bir sistemle entegre çalışıyorsanız bunu göz önünde bulundurun.
REPLACE INTO ile Karşılaştırma
MariaDB ve MySQL’de upsert için bir alternatif daha var: REPLACE INTO. Ancak ikisi arasındaki fark kritiktir:
REPLACE INTO şunu yapar: Çakışma varsa önce mevcut kaydı siler, sonra yeni kaydı ekler. Bu DELETE + INSERT anlamına gelir.
ON DUPLICATE KEY UPDATE ise sadece UPDATE yapar.
Bu farkın pratikte ne anlama geldiğini şöyle açıklayabilirim:
REPLACE INTOkullandığınızda, tablodaupdated_atgibi bir sütununuz varsa ve bunu UPDATE kısmında belirtmediyseniz, o sütun varsayılan değerine sıfırlanır. Çünkü kayıt silindi ve yeniden oluşturuldu.- Foreign key constraint’leriniz varsa
REPLACE INTOciddi sorunlara yol açabilir çünkü önce silme işlemi yapılır. ON DUPLICATE KEY UPDATEile yalnızca belirttiğiniz kolonları güncelliyorsunuz, diğerleri dokunulmadan kalıyor.
Genel tavsiye: Neredeyse her zaman ON DUPLICATE KEY UPDATE tercih edin. REPLACE INTO çok spesifik senaryolarda faydalı olabilir ama varsayılan seçeneğiniz olmamalı.
Koşullu Güncelleme: Her Zaman Üzerine Yazma
Bazen güncelleme yaparken “sadece daha yeni veri varsa güncelle” gibi bir mantık uygulamak istersiniz. Bunu IF veya GREATEST/LEAST fonksiyonlarıyla yapabilirsiniz:
-- Sadece yeni fiyat eskisinden farklıysa güncelle,
-- ayrıca fiyat geçmişini de kaydet
INSERT INTO fiyat_takip (urun_id, fiyat, fiyat_tarihi, guncelleme_sayisi)
VALUES (501, 299.99, NOW(), 1)
ON DUPLICATE KEY UPDATE
onceki_fiyat = fiyat,
fiyat = IF(VALUES(fiyat) != fiyat, VALUES(fiyat), fiyat),
fiyat_tarihi = IF(VALUES(fiyat) != fiyat, VALUES(fiyat_tarihi), fiyat_tarihi),
guncelleme_sayisi = guncelleme_sayisi + 1;
Ya da timestamp karşılaştırmasıyla sadece daha yeni veriyi kabul eden bir yaklaşım:
-- Sadece gelen veri daha güncel ise güncelle
INSERT INTO sensor_verileri (sensor_id, olcum_degeri, olcum_zamani)
VALUES (88, 23.7, '2024-01-15 14:30:00')
ON DUPLICATE KEY UPDATE
olcum_degeri = IF(
VALUES(olcum_zamani) > olcum_zamani,
VALUES(olcum_degeri),
olcum_degeri
),
olcum_zamani = GREATEST(VALUES(olcum_zamani), olcum_zamani);
Bu yaklaşım özellikle IoT ve telemetri senaryolarında, gecikmiş paketlerin eski veriyle yeni veriyi ezmesini önlemek için kullanışlıdır.
Performans İpuçları
Yüksek trafikli sistemlerde ON DUPLICATE KEY UPDATE kullanırken şunlara dikkat edin:
- Batch büyüklüğü: Tek bir INSERT INTO … VALUES (…), (…), (…) ile gönderilen satır sayısını makul tutun. 1000-5000 satır arası genellikle iyi bir denge noktasıdır. Çok büyük batch’ler lock süresini uzatır.
- İndeks seçimi: Sözdiziminin çalışması için tabloda mutlaka PRIMARY KEY veya UNIQUE INDEX olmalıdır. Birden fazla UNIQUE INDEX varsa her biri çakışma kontrolü için taranır, bu performansı etkiler. Gereksiz unique constraint’lerden kaçının.
- InnoDB buffer pool: Upsert yoğun tablolarınızın sık erişilen kısımlarının buffer pool’da kalmasını sağlayın.
innodb_buffer_pool_sizeayarını buna göre boyutlandırın.
- INSERT IGNORE alternatifi: Sadece “yoksa ekle, varsa hiçbir şey yapma” ihtiyacınız varsa
ON DUPLICATE KEY UPDATEyerineINSERT IGNOREkullanın. Daha hafif bir işlemdir çünkü UPDATE kısmı yoktur.
-- Sadece yoksa ekle, varsa dokunma
INSERT IGNORE INTO varsayilan_ayarlar (anahtar, deger, aciklama)
VALUES
('tema', 'koyu', 'Arayuz temasi'),
('dil', 'tr', 'Sistem dili'),
('zaman_dilimi','Europe/Istanbul', 'Zaman dilimi');
Stored Procedure ile Upsert Sarmalama
Karmaşık upsert mantığını stored procedure içine almak, uygulama katmanını temiz tutar ve güvenlik açısından da faydalıdır:
DELIMITER //
CREATE PROCEDURE musteri_upsert(
IN p_musteri_no VARCHAR(20),
IN p_ad_soyad VARCHAR(100),
IN p_email VARCHAR(255),
IN p_telefon VARCHAR(20),
IN p_guncelleme_ip VARCHAR(45)
)
BEGIN
INSERT INTO musteriler
(musteri_no, ad_soyad, email, telefon, kayit_tarihi, son_guncelleme_ip)
VALUES
(p_musteri_no, p_ad_soyad, p_email, p_telefon, NOW(), p_guncelleme_ip)
ON DUPLICATE KEY UPDATE
ad_soyad = VALUES(ad_soyad),
email = VALUES(email),
telefon = VALUES(telefon),
son_guncelleme_tarihi = NOW(),
son_guncelleme_ip = VALUES(son_guncelleme_ip),
guncelleme_sayisi = guncelleme_sayisi + 1;
-- Uygulama katmanına işlem tipini bildir
SELECT ROW_COUNT() AS etkilenen_satir,
CASE ROW_COUNT()
WHEN 1 THEN 'INSERT'
WHEN 2 THEN 'UPDATE'
ELSE 'NO_CHANGE'
END AS islem_tipi;
END //
DELIMITER ;
-- Kullanımı
CALL musteri_upsert(
'MUS-2024-00145',
'Ahmet Yilmaz',
'[email protected]',
'+905551234567',
'192.168.1.50'
);
Sık Yapılan Hatalar
UNIQUE INDEX olmadan kullanmaya çalışmak: ON DUPLICATE KEY UPDATE sadece PRIMARY KEY veya UNIQUE INDEX ihlali durumunda tetiklenir. Normal indeksler veya hiç kısıtlama yoksa sorgu her zaman INSERT yapar.
UPDATE kısmında VALUES(id) kullanmak: PRIMARY KEY kolonunu ON DUPLICATE KEY UPDATE bloğunda güncellemeye çalışmak hata üretebilir veya beklenmedik sonuçlar doğurabilir. PRIMARY KEY’i asla UPDATE kısmına dahil etmeyin.
Trigger çakışmaları: Tabloda INSERT ve UPDATE trigger’ları varsa davranışı test edin. INSERT trigger’ı her zaman tetiklenmez; eğer UPDATE yapılırsa sadece BEFORE INSERT ve AFTER INSERT tetiklenir, UPDATE trigger’ları tetiklenmez. Bu bazen audit log gibi sistemlerde boşluklar yaratır.
Sonuç
ON DUPLICATE KEY UPDATE, sysadmin ve backend geliştirici olarak en sık başvurduğum SQL özelliklerinden biri. Uygulama katmanında “önce sorgula, sonra ekle ya da güncelle” mantığı yerine tek bir atomik işlemle bu ihtiyacı çözmek hem kodu temiz tutar hem de gereksiz veritabanı roundtrip’lerini ortadan kaldırır.
Pratik olarak şunu önerebilirim: İstatistik tabloları, önbellek tablolar, senkronizasyon işlemleri ve kullanıcı tercih yönetimi gibi senaryolarda bu sözdizimini refleks olarak kullanın. Sadece dikkat etmeniz gereken iki şey var: tablonuzda mutlaka PRIMARY KEY veya UNIQUE INDEX olsun, ve AUTO_INCREMENT boşluklarından rahatsız olmayın.
MariaDB 10.5+ ve MySQL 8.0+ kullanıyorsanız VALUES() fonksiyonu yerine alias sözdizimini tercih edin; bu hem daha okunaklı hem de gelecekte deprecation sorunuyla karşılaşmazsınız. Eski sürümlerle çalışıyorsanız klasik VALUES() fonksiyonu tamamen güvenli ve stabil şekilde çalışmaya devam ediyor.
