Menu
Playground'da Dene

SQLite Prepared Statement: prepare, bind, step, finalize

SQLite'ta prepared statement nedir, neden kullanılır ve her sürücünün arka planda sardığı prepare/bind/step/finalize döngüsü nasıl işler?

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

Prepared Statement Aslında Nedir?

SQLite'a bir SQL metni verdiğinizde, tek bir satır bile hareket etmeden önce ciddi bir iş yükü çıkar ortaya: metni tokenlara ayırması, parse etmesi, tablo ve sütunların var olduğunu doğrulaması, nasıl çalıştıracağını planlaması ve bu planı SQLite'ın sanal makinesi için bytecode'a derlemesi gerekir. Sorgu ancak bütün bunlar bittikten sonra fiilen çalışır.

İşte prepared statement, "bytecode'a derlendi" aşamasında durup bu sonucu elinizde tuttuğunuzda elde ettiğiniz şeydir. Derlenmiş program, gerçek değerlerin sonradan doldurulacağı yuvalar — yani placeholder'lar — içerir. Aynı derlenmiş programı farklı değerlerle defalarca çalıştırabilirsiniz; üstelik dışarıdan, güvenilmeyen kaynaklardan gelen verilerle bile güvenle çalıştırabilirsiniz.

Şöyle düşünün: birine her yemek pişirdiğinde tarifi yüksek sesle okutmakla, tarifi bir kez öğretip o gün sadece malzemeleri söylemek arasındaki fark gibi.

Yaşam Döngüsü: prepare, bind, step, finalize

Hangi dilde olursa olsun her SQLite sürücüsü aynı dört C-API çağrısını sarmalar. C yazmayacak olsanız bile bu isimleri bilmek işinize yarar; çünkü hata mesajları ve dökümanlar bu terminolojiyi kullanır:

  1. sqlite3_prepare_v2 — bir SQL metnini statement handle'ına derler.
  2. sqlite3_bind_* — placeholder değerlerini doldurur (her tip için ayrı bir fonksiyon vardır).
  3. sqlite3_step — programı çalıştırır. SELECT için satırları gezmek üzere tekrar tekrar çağırırsınız. INSERT/UPDATE/DELETE içinse tek çağrı işi bitirir.
  4. sqlite3_finalize — işiniz bittiğinde derlenmiş programı serbest bırakır.

Adımlar arasında sqlite3_reset, biten bir statement'ı başa sarar; böylece yeniden prepare etmeden tekrar bind edip yeniden çalıştırabilirsiniz.

SQL İçindeki Placeholder'lar

SQL metninin içinde her değer noktasını, değeri doğrudan araya sıkıştırmak yerine bir placeholder ile işaretlersiniz. SQLite bunun birkaç farklı biçimini destekler:

-- Anonim, konumsal:
INSERT INTO users (name, email) VALUES (?, ?);

-- Numaralı:
INSERT INTO users (name, email) VALUES (?1, ?2);

-- İsimli:
INSERT INTO users (name, email) VALUES (:name, :email);
INSERT INTO users (name, email) VALUES (@name, @email);
INSERT INTO users (name, email) VALUES ($name, $email);

Sürücü seviyesindeki kodlarda en çok ? kullanılır. Birden fazla parametre olduğunda ya da aynı değer birkaç yerde geçtiğinde, isimli yer tutucular (:name) çok daha okunaklı durur. Projede hangisini seçtiyseniz, o stile sadık kalın.

Asla yapmamanız gereken şey ise sorguyu string birleştirerek (concatenation) oluşturmak:

-- BUNU YAPMAYIN:
"INSERT INTO users (name) VALUES ('" + user_input + "')"

Bu yol doğrudan SQL injection'a çıkar; üstelik birazdan okuyacağınız bytecode yeniden kullanımı avantajını da sıfırlar.

SQL üzerinde uygulamalı bir örnek

Mekanizmayı herhangi bir host dil olmadan görmek için, SQLite'ın size sunduğu yalnızca SQL özelliklerini kullanarak prepare/bind/step adımlarının karşılığını yapalım. Bir tablo oluşturup, literal değerle doldurulan parametre tarzı bir yer tutucu kullanarak satır ekleyelim:

Gerçek bir uygulamada bu değerleri inline yazmazsın; INSERT sorgusunu ?, ? yer tutucularıyla bir kez prepare eder, sonra her kullanıcı için isim ve e-posta çiftini bind edip step çağırırsınız. Üretilen bytecode her çağrıda aynıdır; yalnızca bağlanan değerler değişir.

Aynı statement'ı yeniden kullanmak (performans kazancı)

Aşağıdaki kalıp, kullandığınız driver'ın yazmana izin verdiği yapı. Bu sözde koddur — her dil biraz farklı yazar — ama iskelet her yerde aynıdır:

-- bir kez hazırlanır:
INSERT INTO users (name, email) VALUES (?, ?);

-- ardından, bir döngüde:
--   bind(1, name)
--   bind(2, email)
--   step()
--   reset()

Hazırlama (prepare) adımı SQL'i bir kez ayrıştırıp derler. Sonraki her iterasyon yalnızca bytecode'u çalıştırır ve değerleri slotlara kopyalar. Toplu insert işlemlerinde (mesela 100.000 satır import etmeyi düşün) bu yaklaşım, 100.000 ayrı sorguyu tek tek ayrıştırıp çalıştırmaktan çok daha hızlıdır — özellikle hepsini tek bir transaction içine alırsanız, çoğu zaman bir mertebe (10x) fark eder.

Sık yapılan bir hata: insanlar döngü kuruyor ve prepare çağrısını döngünün içinde yapıyor. Bu, tüm kazancı çöpe atmak demek. Prepare'i döngünün dışında yap; bind ve step döngünün içinde kalsın.

Neden Güvenli Yol Bu?

Bağlanan (bound) parametreler, SQL'in içine yerleştirilen string'ler değildir. Tipli slotlar üzerinden — integer slotu, text slotu, blob slotu — bytecode programına aktarılan değerlerdir. SQLite bunları asla yeniden SQL olarak ayrıştırmaz, yani hiçbir değer sorgunun yapısını değiştiremez. İşte bu yüzden parametreli sorgular SQL injection'a karşı doğal bir kalkan oluşturur.

Karşılaştıralım:

-- Güvensiz. Eğer user_input şu ise:  '); DROP TABLE users;--
-- sorgu yıkıcı bir hâle gelir.
"SELECT * FROM users WHERE name = '" + user_input + "'"

-- Güvenli. user_input bir TEXT değer olarak bağlanır ve içeriği
-- ne olursa olsun yalnızca bir dize olarak karşılaştırılır.
SELECT * FROM users WHERE name = ?;

İkinci form, user_input değişkeni '); DROP TABLE users;-- gibi tehlikeli bir değer içerse bile güvenlidir. SQLite, adı tam olarak bu (epey tuhaf) metne eşit bir kullanıcı arar, böyle birini bulamaz ve sıfır satır döner. Sorgunun yapısı değerden etkilenip değişemez.

SQL injection konusuna ileride ayrı bir yazıda daha detaylı gireceğiz; ama şimdilik aklınızda kalması gereken şu: prepared statement, SQL injection'a karşı bir savunma değil, asıl savunmadır.

Satır Döndüren Sorgular

SELECT sorgularında step her seferinde tek bir satır döner. Sürücü genelde "bitti" yanıtı gelene kadar bir döngü içinde ilerler:

Uygulama kodunda sürücü, bu SELECT sorgusunu 2.00 yerine ? koyarak prepare eder, eşik değerini bind eder ve bir döngü içinde step çağırarak her çağrıda bir satır okur. Son satırdan sonra step işlemin tamamlandığını bildirir; sürücü de ya statement'ı reset ederek yeni bir eşikle tekrar çalıştırır ya da finalize ile kapatır.

Finalize Etmeyi Sakın Unutmayın

Prepared statement, SQLite içinde küçük bir bellek tahsisidir. Bunları sızdırmak hem belleği yer hem de — daha önemlisi — veritabanı üzerinde dahili bir kilit tutarak diğer yazıcıları bloklayabilir. Her sürücü bu temizliği otomatik yapmanın bir yolunu sunar: Python'da context manager'lar, C#'ta using blokları, C++'ta RAII. Bunları kullanmanızı öneririm:

  • Python'un sqlite3 modülü, cursor garbage collector tarafından toplandığında finalize işlemini yapar; ama açıkça cursor.close() çağırmak çok daha temiz.
  • better-sqlite3 (Node), Statement nesnesi garbage-collect edildiğinde finalize eder; uzun ömürlü prepared statement'lar sorun değildir.
  • Saf C kullanıyorsanız sqlite3_finalize çağrısını kendiniz yapmanız gerekir. Unutmak gerçek bir bug'dır.

Pratik kural şu: prepare ettiyseniz, bir yerde mutlaka finalize edilmesi gerekir.

Belki Kendiniz Hiç Çağırmayacaksınız

sqlite3_prepare_v2 fonksiyonunu doğrudan çağırmanız çok nadir olur. Üst seviye sürücüler connection.execute("SELECT ... WHERE id = ?", (42,)) çağrısını sizin için prepare/bind/step/finalize adımlarına dönüştürür. Yine de bu yaşam döngüsünü bilmenin faydası var:

  • "statement is busy" veya "cannot operate on a finalized statement" gibi hatalar gördüğünüzde arka planda neler döndüğünü anlarsınız.
  • Sıkı bir döngü içinde insert yaparken uzun ömürlü prepared statement'ları cache'lemeniz gerektiğini bilirsiniz.
  • String birleştirme cazip görünse bile, refleks olarak parametreli sorgu yazarsınız.

ORM'ler ve query builder'lar bu işi bir adım daha öteye taşır. SQL'i kendileri kurar, prepared statement'ları yönetir ve size tipli sonuçlar verir. Ama altta yatan hep aynı dört çağrıdır.

Sırada: Parametre Bind Etme

Yer tutuculardan şimdiye kadar soyut bir şekilde bahsettik. Bir sonraki bölümde işin bind tarafına detaylıca bakacağız — pozisyonel ve isimli parametreler, tip yönetimi, NULL ve sorgulara gerçek uygulama verisi geçirmeye başladığınızda karşınıza çıkan ufak tefek tuzaklar.

Sıkça Sorulan Sorular

SQLite'ta prepared statement nedir?

Prepared statement, parse edilmiş, derlenmiş ve yeniden kullanılabilir bir bytecode programına dönüştürülmüş bir SQL sorgusudur — ancak değerlerin geleceği yerlerde placeholder (? ya da :name) bırakılır. Değerleri ayrı bir adımda, çalıştırma sırasında bind edersin. SQLite bu mekanizmayı sqlite3_prepare_v2, sqlite3_bind_*, sqlite3_step ve sqlite3_finalize fonksiyonlarıyla sunar.

SQLite'ta neden prepared statement kullanmalıyım?

İki temel sebep var: güvenlik ve hız. Bind edilen parametreler hiçbir zaman SQL söz dizimiyle karıştırılmaz, dolayısıyla SQL injection imkansız hale gelir. Bir de aynı sorguyu defalarca çalıştırıyorsan — örneğin 10.000 satır insert ediyorsan — bir kez prepare edip her seferinde sadece bind etmek, parser'ı her döngüde devre dışı bırakır ve ölçülebilir bir performans kazancı sağlar.

Prepared statement ile normal sorgu arasındaki fark nedir?

Düz bir sqlite3_exec çağrısı SQL'i tek seferde parse edip çalıştırır ve değerler metin olarak SQL'in içine gömülür. Prepared statement ise derleme ile çalıştırmayı birbirinden ayırır: SQL'i bir kez prepare edersin, tipli değerleri placeholder'lara bind edersin, sonuçlar arasında step ile ilerlersin ve aynı sorguyu tekrar çalıştırmak için reset edersin. Python'un sqlite3 modülü, better-sqlite3 vb. tüm üst seviye sürücüler aslında arka planda prepared statement kullanır.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA