SQLite'ta veri INSERT ile eklenir, UPDATE ile değiştirilir, DELETE ile silinir. INSERT üç biçimde yazılır: VALUES ile bir veya birden çok satır, SELECT ile başka bir sorgunun sonucu ya da DEFAULT VALUES ile tamamen varsayılan değerlerden oluşan tek satır. UPDATE ve DELETE, WHERE koşulu yazılmazsa tablodaki bütün satırları etkiler. Üç komutun sonuna eklenen RETURNING yan tümcesi, değiştirilen satırları ayrı bir sorguya gerek kalmadan geri döndürür.

Bu yazıda her komutu SQLite 3.50.6 üzerinde çalıştırılmış örneklerle ele alıyor; çok satırlı ekleme, INSERT SELECT, satır değeri atamaları, başka tablolara dayalı UPDATE FROM, RETURNING'in sınırları ve LIMIT'li DELETE gibi derlemeye bağlı ayrıntıları açıklıyoruz. Tablo tanımları ve kısıtlar için CREATE TABLE ve kısıtlamalar, serinin tamamı için SQLite öğrenme rehberi sayfasına bakabilirsiniz.

INSERT INTO ... VALUES

INSERT INTO ... VALUES

En yaygın biçim, sütun listesini ve değerleri birlikte vermektir. Sütun listesi yazılırsa her değer grubunda tam o kadar değer bulunmalıdır; listede olmayan sütunlar tablo tanımındaki varsayılan değeri, varsayılan yoksa NULL alır. Sütun listesi yazılmazsa değer sayısı tablodaki sütun sayısına eşit olmalı ve değerler soldan sağa sırayla yerleştirilir. Tek bir INSERT içinde virgülle ayrılmış birden çok değer grubu verebilirsiniz:

sql
CREATE TABLE uye(
  id      INTEGER PRIMARY KEY,
  eposta  TEXT NOT NULL UNIQUE,
  ad      TEXT NOT NULL,
  puan    INTEGER NOT NULL DEFAULT 0,
  kayit   TEXT DEFAULT '2026-11-07'
);
INSERT INTO uye(eposta, ad) VALUES ('ayse@ornek.com', 'Ayşe');
INSERT INTO uye(eposta, ad, puan) VALUES
  ('mehmet@ornek.com', 'Mehmet', 40),
  ('zeynep@ornek.com', 'Zeynep', 75);
SELECT changes(), last_insert_rowid();
-- çıktı: 2|3

INSERT INTO uye(eposta, ad) VALUES ('can@ornek.com', 'Can') RETURNING id, puan, kayit;
-- çıktı: 4|0|2026-11-07

INSERT INTO uye VALUES (10, 'elif@ornek.com', 'Elif', 5, NULL);
INSERT INTO uye(eposta, ad) VALUES ('deniz@ornek.com', 'Deniz') RETURNING id;
-- çıktı: 11

changes() son tamamlanan INSERT, UPDATE veya DELETE ifadesinin değiştirdiği satır sayısını, last_insert_rowid() ise bu bağlantıda son eklenen satırın rowid değerini döndürür. Çok satırlı INSERT sonrasında ikisi de son ifadeye aittir: iki satır eklendi ve sonuncusunun kimliği 3 oldu. RETURNING ile SQLite'ın sizin yerinize doldurduğu değerleri, yani otomatik kimliği ve varsayılanları anında görürsünüz.

Son iki satır INTEGER PRIMARY KEY'in davranışını gösteriyor: kimliği elle 10 olarak verdiğimizde sonraki otomatik kimlik 11 oldu. Otomatik kimliklerin nasıl seçildiği ve AUTOINCREMENT'ın bu davranışı nasıl değiştirdiği PRIMARY KEY ve AUTOINCREMENT yazısında anlatılıyor. Uygulama kodundan veri eklerken değerleri SQL metnine birleştirmek yerine her zaman parametre kullanın; bunun nedenleri SQL injection önleme yazısında, dil bağlayıcılarındaki karşılıkları ise Python sqlite3 ve Node.js node:sqlite yazılarında.

INSERT INTO ... SELECT ve DEFAULT VALUES

İkinci biçimde VALUES yerine bir SELECT yazılır ve sorgunun döndürdüğü her satır için tabloya bir satır eklenir. Sorgu birleşik (UNION gibi) olabilir, ORDER BY ve LIMIT içerebilir. Sütun sayıları tutmazsa ifade hiç çalışmadan reddedilir. Aşağıdaki örneklerde önce şu başlangıç verisini kullanıyoruz:

sql
CREATE TABLE uye(id INTEGER PRIMARY KEY, eposta TEXT NOT NULL UNIQUE, ad TEXT NOT NULL,
                 puan INTEGER NOT NULL DEFAULT 0, sehir TEXT);
INSERT INTO uye(eposta, ad, puan, sehir) VALUES
  ('ayse@ornek.com', 'Ayşe', 10, 'İzmir'),
  ('mehmet@ornek.com', 'Mehmet', 40, 'Ankara'),
  ('zeynep@ornek.com', 'Zeynep', 75, 'İzmir');
sql
CREATE TABLE arsiv(eposta TEXT, ad TEXT, tasindi TEXT);
INSERT INTO arsiv(eposta, ad, tasindi)
  SELECT eposta, ad, '2026-11-07' FROM uye WHERE sehir = 'İzmir';
SELECT changes();
-- çıktı: 2
SELECT * FROM arsiv ORDER BY eposta;
-- çıktı:
-- ayse@ornek.com|Ayşe|2026-11-07
-- zeynep@ornek.com|Zeynep|2026-11-07

INSERT INTO arsiv VALUES ('x@ornek.com', 'X');
-- hata: table arsiv has 3 columns but 2 values were supplied
INSERT INTO arsiv(eposta) VALUES ('y@ornek.com', 'Y');
-- hata: 2 values for 1 columns

Belgeler iki ayrıntıya dikkat çeker. Birincisi, INSERT SELECT ifadesine UPSERT yan tümcesi ekleyecekseniz sorguya en azından WHERE true yazmalısınız; aksi hâlde ayrıştırıcı ON kelimesinin bir birleştirme koşuluna mı yoksa UPSERT'e mi ait olduğunu ayırt edemez. İkincisi, INSERT INTO hedef SELECT * FROM kaynak biçimi, iki tablonun disk biçimi aynıysa satırları çözüp yeniden kurmadan doğrudan kopyalayan "xfer" iyileştirmesini kullanabilir. Bu iyileştirme yalnızca tek kaynak tablolu, WHERE ve WITH içermeyen yalın biçimde devreye girer.

Üçüncü biçim olan INSERT INTO tablo DEFAULT VALUES, her sütunu varsayılan değeriyle dolduran tek bir satır ekler. Bu biçim UPSERT ile birlikte kullanılamaz ve tetikleyici gövdelerinde yer alamaz. Örneğini CREATE TABLE yazısındaki DEFAULT bölümünde görebilirsiniz.

Çakışmalarda davranış: INSERT OR ...

INSERT kelimesinin yerine INSERT OR ABORT, OR FAIL, OR IGNORE, OR REPLACE veya OR ROLLBACK yazarak yalnızca o ifade için bir çakışma çözüm algoritması seçebilirsiniz. MySQL uyumluluğu için tek başına REPLACE kelimesi de INSERT OR REPLACE anlamına gelir. Varsayılan ABORT, kısıt bozulunca ifadenin bütün değişikliklerini geri alır:

sql
INSERT OR IGNORE INTO uye(eposta, ad)
  VALUES ('ayse@ornek.com', 'Ayşe 2'), ('can@ornek.com', 'Can');
SELECT changes();
-- çıktı: 1
INSERT INTO uye(eposta, ad) VALUES ('ayse@ornek.com', 'Ayşe 3');
-- hata: UNIQUE constraint failed: uye.eposta

IGNORE, benzersizlik kısıtını bozan satırı sessizce atladı ve diğer satırı ekledi; bu yüzden changes() 1 döndü. REPLACE ise çakışan eski satırı silip yenisini ekler; silinen satırın diğer sütunları da kaybolur ve belgelere göre bu silme işlemi, özyinelemeli tetikleyiciler açık değilse silme tetikleyicilerini çalıştırmaz. Var olan satırı silmeden yalnızca bazı sütunlarını güncellemek istiyorsanız doğru araç UPSERT'tür; UPSERT ve ON CONFLICT yazısında REPLACE ile farkını ayrıntılı anlatıyoruz.

UPDATE: SET, satır değerleri ve WHERE

UPDATE: SET, satır değerleri ve WHERE

UPDATE, WHERE koşulunun doğru olduğu her satırda SET listesindeki sütunlara yeni değer atar. Koşul hiçbir satıra uymazsa bu bir hata değildir; ifade sıfır satırı değiştirir. Belgelerdeki önemli kurallar şunlardır: SET ifadeleri güncellenen satırın sütunlarına başvurabilir ve bütün ifadeler atamalar yapılmadan önce hesaplanır; aynı sütun listede birden çok kez geçerse yalnızca en sağdaki atama geçerli olur; 3.15.0 sürümünden beri parantezli bir sütun listesine aynı boyutta bir satır değeri atanabilir.

sql
UPDATE uye SET puan = puan + 5 WHERE sehir = 'İzmir' RETURNING ad, puan;
-- çıktı (satır sırası garanti değildir):
-- Ayşe|15
-- Zeynep|80

UPDATE uye SET puan = puan * 2, sehir = upper(sehir) WHERE id = 2;
SELECT ad, puan, sehir FROM uye WHERE id = 2;
-- çıktı: Mehmet|80|ANKARA

UPDATE uye SET (puan, sehir) = (0, 'Bursa') WHERE ad = 'Zeynep';
SELECT ad, puan, sehir FROM uye WHERE id = 3;
-- çıktı: Zeynep|0|Bursa

UPDATE uye SET puan = 1, puan = 99 WHERE id = 1;
SELECT puan FROM uye WHERE id = 1;
-- çıktı: 99

UPDATE uye SET puan = -1 WHERE id = 999;
SELECT changes();
-- çıktı: 0

İlk ifadede RETURNING, güncellenmiş değerleri döndürdü; UPDATE ve INSERT için RETURNING içindeki sütun adları değişiklik sonrasındaki değeri gösterir. Satırların hangi sırayla döneceği belgelere göre belirsizdir, bu yüzden sıraya güvenen kod yazmayın. upper() fonksiyonunu burada yalnızca ASCII harflerden oluşan bir şehir adında kullandık; Türkçe harflerde standart SQLite'ın bu fonksiyonu harfleri değiştirmez. Ayrıntılar metin fonksiyonları yazısında.

UPDATE FROM: başka tablolara göre güncellemek

3.33.0 sürümünden beri UPDATE ifadesine bir FROM yan tümcesi eklenebilir. Güncellenen hedef tablo, FROM içindeki tablolar veya alt sorgularla birleştirilir; hangi satırların güncelleneceği ve yeni değerlerin ne olacağı bu birleşimden hesaplanır. SQLite'ın uygulaması PostgreSQL ile uyumlu olmayı hedefler. Belgelerdeki satış ve stok örneğinin bir uyarlaması şöyledir:

sql
CREATE TABLE stok(urun_id INTEGER PRIMARY KEY, ad TEXT, adet INTEGER);
CREATE TABLE satis(id INTEGER PRIMARY KEY, urun_id INTEGER, adet INTEGER);
INSERT INTO stok VALUES (1, 'Kalem', 100), (2, 'Defter', 50), (3, 'Silgi', 30);
INSERT INTO satis(urun_id, adet) VALUES (1, 3), (1, 7), (2, 5);

UPDATE stok
   SET adet = adet - gunluk.toplam
  FROM (SELECT urun_id, sum(adet) AS toplam FROM satis GROUP BY urun_id) AS gunluk
 WHERE stok.urun_id = gunluk.urun_id;
SELECT ad, adet FROM stok ORDER BY urun_id;
-- çıktı:
-- Kalem|90
-- Defter|45
-- Silgi|30

Hedef tablo FROM listesine yazılmaz; yalnızca kendisiyle birleştirmek istiyorsanız farklı bir takma adla eklenir. Belgeler SQL Server'ın hedef tabloyu FROM içinde tekrar yazdığını, MySQL'in ise birleştirmeyi UPDATE ile SET arasında tanımladığını belirtir; başka bir veritabanından sorgu taşırken bu farka dikkat edin. Birleşim aynı hedef satır için birden çok sonuç üretirse bunlardan yalnızca biri kullanılır ve hangisinin seçileceği belirsizdir. Örnekte bu yüzden satışları önce bir alt sorguda ürüne göre topladık; böylece her stok satırına tek bir satır eşleşti. RETURNING içinde FROM'daki yardımcı tablolara başvurulamaz.

DELETE ve RETURNING

DELETE, WHERE koşulu doğru olan satırları siler; koşulun yanlış ya da NULL olduğu satırlar kalır. Koşul yazılmazsa tablodaki bütün kayıtlar silinir. DELETE için RETURNING içindeki sütunlar silinmeden önceki değerleri gösterir:

sql
DELETE FROM satis WHERE urun_id = 1 RETURNING id, adet;
-- çıktı:
-- 1|3
-- 2|7
SELECT count(*) FROM satis;
-- çıktı: 1

DELETE FROM stok WHERE adet = NULL;
SELECT changes();
-- çıktı: 0

DELETE FROM stok WHERE adet < 40 RETURNING ad;
-- çıktı: Silgi

DELETE FROM satis;
SELECT changes(), count(*) FROM satis;
-- çıktı: 1|0

DELETE FROM stok ORDER BY adet LIMIT 1;
-- hata: near "ORDER": syntax error

adet = NULL koşulu hiçbir satırı silmedi; çünkü NULL ile yapılan eşitlik karşılaştırmasının sonucu NULL'dur ve satır seçilmez. NULL değerleri seçmek için IS NULL kullanılır; konu NULL davranışı yazısında. WHERE ve RETURNING içermeyen ve tetikleyicisi olmayan bir tablodaki DELETE için SQLite, satırları tek tek ziyaret etmeden tabloyu boşaltan "truncate" iyileştirmesini kullanır; 3.6.5 sürümünden beri bu durumda da changes() doğru sayıyı döndürür.

Son satır yaygın bir beklentiyi düzeltiyor: UPDATE ve DELETE ifadelerinde ORDER BY ve LIMIT, ancak SQLite SQLITE_ENABLE_UPDATE_DELETE_LIMIT derleme seçeneğiyle derlendiyse kullanılabilir. Bu seçenek olmayan derlemelerde, denediğimiz 3.50.6 aracında ve 3.45.3 kütüphanesinde olduğu gibi, ifade söz dizimi hatası verir. Seçenek açık olsa bile tetikleyici gövdelerinde kullanılamaz ve ORDER BY yalnızca hangi satırların sınıra gireceğini belirler. Taşınabilir bir yol, silinecek satırları bir alt sorguyla seçmektir: DELETE FROM stok WHERE urun_id IN (SELECT urun_id FROM stok ORDER BY adet LIMIT 1);

Uygulamalı örnek: eski siparişleri arşive taşımak

Üç komut gerçek işlerde çoğu zaman birlikte kullanılır. Tamamlanmış siparişleri ana tablodan arşiv tablosuna taşımak için önce satırları INSERT SELECT ile kopyalar, sonra aynı satırları DELETE ile silersiniz. İki adım arasında bir hata olursa verinin iki tabloda birden bulunması ya da hiç bulunmaması gerekir; bu yüzden işlemi BEGIN ve COMMIT arasına alıyoruz:

sql
CREATE TABLE siparis(id INTEGER PRIMARY KEY, musteri TEXT, durum TEXT, tarih TEXT);
CREATE TABLE siparis_arsiv(id INTEGER PRIMARY KEY, musteri TEXT, durum TEXT, tarih TEXT);
INSERT INTO siparis(musteri, durum, tarih) VALUES
  ('Ayşe', 'teslim', '2026-09-02'), ('Mehmet', 'yolda', '2026-11-05'),
  ('Zeynep', 'teslim', '2026-10-11'), ('Can', 'iptal', '2026-08-20');

BEGIN;
INSERT INTO siparis_arsiv
  SELECT * FROM siparis WHERE durum IN ('teslim', 'iptal') AND tarih < '2026-11-01';
DELETE FROM siparis WHERE id IN (SELECT id FROM siparis_arsiv);
SELECT changes();
-- çıktı: 3
COMMIT;

SELECT id, musteri, durum FROM siparis;
-- çıktı: 2|Mehmet|yolda
SELECT count(*) FROM siparis_arsiv;
-- çıktı: 3

Burada birkaç tercih bilinçli yapıldı. İki tablonun sütunları aynı sırada olduğu için SELECT * kullanmak güvenli; şemalar farklılaşırsa sütun listesini açıkça yazmalısınız. Silme koşulunu yeniden yazmak yerine arşive gerçekten kopyalanan kimliklere dayandırdık; böylece iki ifade arasında koşul farkı oluşamaz. Tarihleri ISO 8601 metni olarak sakladığımız için tarih < '2026-11-01' karşılaştırması doğru sırayla çalışır. İşlem içinde bir hata olursa ROLLBACK ile iki tablo da başlangıç durumuna döner.

RETURNING'in sınırları

RETURNING'in sınırları: Yalnızca en üst düzeydeki INSERT, UPDATE ve DELETE ifadelerinde kullanılabilir..., Bir alt sorgu gibi kullanılamaz; sonuçlar yalnızca uygulamaya döner...

RETURNING 3.35.0 sürümünden beri vardır ve söz dizimi PostgreSQL örnek alınarak tasarlanmıştır. Belgelerde sayılan sınırlamalar, onu doğru kullanmak için bilinmesi gereken noktalardır:

  • Yalnızca en üst düzeydeki INSERT, UPDATE ve DELETE ifadelerinde kullanılabilir; tetikleyici içinde kullanılamaz.
  • Bir alt sorgu gibi kullanılamaz; sonuçlar yalnızca uygulamaya döner, başka bir tabloya veya sorguya aktarılamaz.
  • Satırlar belirsiz bir sırayla döner ve bu sıra sürümden ya da çalıştırmadan çalıştırmaya değişebilir.
  • Yalnızca ifadenin doğrudan değiştirdiği satırları raporlar; tetikleyicilerin ya da yabancı anahtar eylemlerinin yaptığı değişiklikler ve AFTER tetikleyicilerinin sonradan değiştirdiği değerler görünmez.
  • En üst düzeyde toplama veya pencere fonksiyonu içeremez; bunlar ancak RETURNING içindeki alt sorgularda yer alabilir.
  • Sanal tablolarda DELETE ve UPDATE için kullanılamaz. UPSERT ile birlikte kullanıldığında hem eklenen hem güncellenen satırları raporlar.

Belgeler bir performans ayrıntısını da not eder: bütün değişiklikler ilk adımda yapılır ve RETURNING çıktısı bellekte biriktirilir. Çok sayıda satır veya büyük değerler döndüren bir ifade bu yüzden geçici olarak fazla bellek kullanabilir. Bu, değişikliklerin hemen diske yazıldığı anlamına gelmez; ifade bir işlemin parçasıysa kalıcılık yine COMMIT ile sağlanır.

Sık yapılan hatalar

  • WHERE koşulunu unutmak: UPDATE ve DELETE bütün tabloyu etkiler. Önce aynı koşulla SELECT çalıştırın.
  • NULL'u eşittir ile aramak: WHERE sutun = NULL hiçbir satır seçmez; IS NULL kullanın.
  • Sütun listesi olmadan INSERT yazmak: Tabloya sütun eklendiğinde ifade "has N columns but M values" hatasıyla bozulur. Sütun listesini her zaman yazın.
  • REPLACE'i güncelleme sanmak: REPLACE eski satırı siler; listede olmayan sütunlar varsayılanlara döner.
  • RETURNING sırasına güvenmek: Dönen satırların sırası garanti edilmez.
  • Binlerce satırı ayrı ayrı eklemek: Her ifade kendi işlemi içinde çalışırsa yazma yavaşlar. Toplu eklemeleri tek bir işlemde gruplayın; ayrıntılar performans ayarları yazısında.

Sık Sorulan Sorular

Eklenen satırın kimliğini nasıl öğrenirim?

RETURNING id en açık yoldur. Alternatif olarak aynı bağlantıda last_insert_rowid() fonksiyonunu çağırabilirsiniz; bu değer bağlantıya özeldir.

Kayıt varsa güncelle, yoksa ekle nasıl yapılır?

INSERT ... ON CONFLICT (sutun) DO UPDATE SET ... biçimindeki UPSERT söz dizimiyle. Ayrıntılar SQLite UPSERT yazısında.

Bir tabloyu tamamen boşaltmanın en hızlı yolu nedir?

WHERE olmadan DELETE FROM tablo; yazmak. Tabloda tetikleyici yoksa SQLite truncate iyileştirmesini kullanır. Dosya boyutunun küçülmesi için ayrıca VACUUM gerekir; bkz. VACUUM yazısı.

Kaç satırın değiştiğini nasıl görürüm?

SQL içinde changes(), komut satırında .changes on kullanın. Bağlantı açıldığından beri toplam değişiklik sayısı için total_changes() fonksiyonu vardır.

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 CREATE TABLE · SQLite UPSERT · SQLite transaction · SQLite SELECT sorguları