Ders 14 / 21
Toplu İşlemler
Yazma yolunun ölçeklenmesi: aynı iki bin satırın dört yoldan yazılması ve sürelerin karşılaştırılması, işlem sınırının belirleyici payı, parti büyüklüğünün deyim sayısı ve sorgu uzunluğuyla ilişkisi, toplu güncellemede deyim sayısının ölçülmesi.
İçindekiler
Önceki iki ders okuma yolunu ölçtü: kaç sorgu çalışıyor ve her sorgu ne kadar veri taşıyor. Yazma yolunun kendi ölçeği vardır.
Ödünç verilerinin başka bir sistemden aktarıldığını düşünelim: iki bin kayıt yazılacak. Bu iş tek tek de yapılabilir, tek bir işlemde de, tek bir deyimde de. Üç yol da aynı satırları üretir. Bu ders aralarındaki farkı ölçer ve farkın nereden geldiğini gösterir.
Dört Yol
Ölçüm her yol için veritabanını sıfırdan kurar; böylece hiçbir yol öncekinin ısıttığı önbellekten yararlanmaz.
// toplu-ekleme.mjs — ayni 2000 satir dort yoldan yazilir, sureler olculur import { DatabaseSync } from "node:sqlite"; import { rmSync } from "node:fs"; const ADET = 2000; const veri = Array.from({ length: ADET }, (_, i) => [ (i % 200) + 1, (i % 60) + 1, `2025-${String((i % 12) + 1).padStart(2, "0")}-01`, ]); function yeniVeritabani() { rmSync("aktarim.db", { force: true }); const db = new DatabaseSync("aktarim.db"); db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL)`); return db; } function olc(ad, yaz) { const db = yeniVeritabani(); const t = performance.now(); yaz(db); const sure = performance.now() - t; const n = db.prepare("SELECT count(*) AS n FROM odunc").get().n; db.close(); console.log(`${ad.padEnd(38)} satir=${n} sure=${sure.toFixed(0).padStart(6)} ms ` + `satir_basi=${(sure * 1000 / n).toFixed(1).padStart(6)} us`); return sure; } const a = olc("her satir ayri islem", (db) => { for (const s of veri) { db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(...s); } }); const b = olc("tek islem, her satir hazirlanir", (db) => { db.exec("BEGIN"); for (const s of veri) { db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)").run(...s); } db.exec("COMMIT"); }); const c = olc("tek islem, deyim yeniden kullanilir", (db) => { db.exec("BEGIN"); const ekle = db.prepare("INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES (?,?,?)"); for (const s of veri) ekle.run(...s); db.exec("COMMIT"); }); const d = olc("tek islem, 500'luk cok satirli deyim", (db) => { db.exec("BEGIN"); for (let i = 0; i < veri.length; i += 500) { const parti = veri.slice(i, i + 500); const yerler = parti.map(() => "(?,?,?)").join(","); db.prepare(`INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES ${yerler}`) .run(...parti.flat()); } db.exec("COMMIT"); }); console.log(`\noranlar (ilk yaklasima gore): ${(a/b).toFixed(1)}x ${(a/c).toFixed(1)}x ${(a/d).toFixed(1)}x`);
node toplu-ekleme.mjs
her satir ayri islem satir=2000 sure= 376 ms satir_basi= 188.2 us tek islem, her satir hazirlanir satir=2000 sure= 3 ms satir_basi= 1.5 us tek islem, deyim yeniden kullanilir satir=2000 sure= 1 ms satir_basi= 0.4 us tek islem, 500'luk cok satirli deyim satir=2000 sure= 1 ms satir_basi= 0.7 us oranlar (ilk yaklasima gore): 127.9x 470.4x 276.9x
Süreler donanıma ve dosya sistemine bağlıdır; oranlar yapıyı gösterir.
En büyük sıçrama ilk iki satır arasında. Aradaki tek fark işlem sınırıdır. İlk yaklaşımda
her INSERT kendi işlemini açıp kapatır; kapanış her seferinde kalıcılık gerektirir, yani
verinin diske indiğinin garanti edilmesi beklenir. İki bin kesinleştirme, iki bin
dayanıklılık noktası demektir. İkinci yaklaşımda tek bir kesinleştirme var.
Toplu yazmada birinci karar deyim biçimi değil, işlem sınırıdır. SQL Temelleri kursunda
toplu ekleme, çok satırlı INSERT deyimi olarak tanıtılmıştı; ölçüm, asıl kazancın orada
olmadığını gösteriyor.
İkinci ve üçüncü satır arasındaki fark deyim hazırlamadan geliyor. Her satır için deyimi yeniden hazırlamak, aynı metni iki bin kez ayrıştırmak demektir. Hazır deyimi bir kez kurup yeniden kullanmak bu işi bir kereye indiriyor.
Dördüncü satır, yerel ölçümde beklenenden yavaş. Çok satırlı deyim her parti için farklı uzunlukta bir SQL metni üretir, dolayısıyla her parti yeniden ayrıştırılır; yerel bir veritabanında bu, hazır deyimi yeniden kullanmaktan pahalıdır. Çok satırlı deyimin kazancı başka yerdedir: deyim sayısını düşürür, dolayısıyla ağ üzerindeki gidiş-dönüş sayısını düşürür. Yerel ölçüm bu kazancı göstermez.
Parti Büyüklüğü
Çok satırlı deyimde seçilecek tek sayı parti büyüklüğüdür.
// parti-boyu.mjs — cok satirli deyimde parti buyuklugunun etkisi import { DatabaseSync } from "node:sqlite"; import { rmSync } from "node:fs"; const ADET = 2000; const veri = Array.from({ length: ADET }, (_, i) => [ (i % 200) + 1, (i % 60) + 1, `2025-${String((i % 12) + 1).padStart(2, "0")}-01`, ]); for (const parti of [1, 10, 50, 200, 1000]) { rmSync("aktarim.db", { force: true }); const db = new DatabaseSync("aktarim.db"); db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL)`); const t = performance.now(); db.exec("BEGIN"); let deyim = 0, enUzun = 0; for (let i = 0; i < veri.length; i += parti) { const dilim = veri.slice(i, i + parti); const sql = `INSERT INTO odunc (kitap_id, uye_id, alis_tarihi) VALUES ${ dilim.map(() => "(?,?,?)").join(",")}`; enUzun = Math.max(enUzun, sql.length); db.prepare(sql).run(...dilim.flat()); deyim += 1; } db.exec("COMMIT"); const sure = performance.now() - t; console.log(`parti=${String(parti).padStart(4)} deyim=${String(deyim).padStart(4)} ` + `en_uzun_sql=${String(enUzun).padStart(6)} karakter sure=${sure.toFixed(1).padStart(6)} ms`); db.close(); }
node parti-boyu.mjs
parti= 1 deyim=2000 en_uzun_sql= 64 karakter sure= 4.1 ms parti= 10 deyim= 200 en_uzun_sql= 136 karakter sure= 1.4 ms parti= 50 deyim= 40 en_uzun_sql= 456 karakter sure= 1.1 ms parti= 200 deyim= 10 en_uzun_sql= 1656 karakter sure= 1.0 ms parti=1000 deyim= 2 en_uzun_sql= 8056 karakter sure= 0.9 ms
Kazancın büyük kısmı ilk adımlarda alınıyor: birden ona geçmek süreyi üçte bire indiriyor, elliden bine geçmek neredeyse hiçbir şey kazandırmıyor. Buna karşılık sorgu metni doğrusal büyüyor ve bin satırlık partide sekiz kilobayta yaklaşıyor.
Parti büyüklüğünü sınırlayan üç etken vardır. Bağlı değişken sayısının motor tarafından konan bir üst sınırı bulunur; parti başına sütun sayısıyla çarpılan değişken sayısı bu sınırı aşamaz. Uzun sorgu metni ayrıştırma maliyetini ve bellek kullanımını artırır. Ve büyük parti, hata durumunda geri alınacak işin de büyümesi demektir.
Bu yüzden parti, “olabildiğince büyük” değil, kazancın düzleştiği noktada seçilir. Ölçümde bu nokta elli ile iki yüz arasındadır; başka bir şemada başka bir yerde olur, ama eğrinin biçimi aynıdır.
Toplu Güncelleme
Güncellemede her satıra farklı bir değer yazmak gerekir; bu, tek bir UPDATE deyimiyle
yapılabilir ama nasıl yapılacağı seçim ister.
// toplu-guncelleme.mjs — 1000 satira ayri ayri deger yazmanin uc yolu import { DatabaseSync } from "node:sqlite"; import { rmSync } from "node:fs"; const ADET = 1000; const guncellemeler = Array.from({ length: ADET }, (_, i) => [ i + 1, `2025-${String((i % 12) + 1).padStart(2, "0")}-15`, ]); function hazirla() { rmSync("aktarim.db", { force: true }); const db = new DatabaseSync("aktarim.db"); db.exec(`CREATE TABLE odunc (odunc_id INTEGER PRIMARY KEY, kitap_id INTEGER NOT NULL, uye_id INTEGER NOT NULL, alis_tarihi TEXT NOT NULL, iade_tarihi TEXT)`); db.exec("BEGIN"); const ekle = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,NULL)"); for (let i = 1; i <= ADET; i++) ekle.run(i, (i % 200) + 1, (i % 60) + 1, "2025-01-01"); db.exec("COMMIT"); return db; } function olc(ad, guncelle) { const db = hazirla(); const sayac = { deyim: 0 }; const t = performance.now(); guncelle(db, sayac); const sure = performance.now() - t; const n = db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NOT NULL").get().n; const ornek = db.prepare("SELECT iade_tarihi FROM odunc WHERE odunc_id = 7").get().iade_tarihi; db.close(); console.log(`${ad.padEnd(30)} guncellenen=${n} ornek(7)=${ornek} ` + `deyim=${String(sayac.deyim).padStart(4)} sure=${sure.toFixed(1).padStart(6)} ms`); return sure; } const a = olc("tek tek guncelleme", (db, sayac) => { db.exec("BEGIN"); const g = db.prepare("UPDATE odunc SET iade_tarihi = ? WHERE odunc_id = ?"); for (const [id, tarih] of guncellemeler) { g.run(tarih, id); sayac.deyim += 1; } db.exec("COMMIT"); }); const b = olc("deger listesinden guncelleme", (db, sayac) => { db.exec("BEGIN"); const yerler = guncellemeler.map(() => "(?,?)").join(","); db.prepare(`UPDATE odunc SET iade_tarihi = v.tarih FROM (SELECT column1 AS id, column2 AS tarih FROM (VALUES ${yerler})) AS v WHERE odunc.odunc_id = v.id`).run(...guncellemeler.flat()); sayac.deyim += 1; db.exec("COMMIT"); }); const c = olc("hazirlik tablosu uzerinden", (db, sayac) => { db.exec("BEGIN"); db.exec("CREATE TEMP TABLE hazirlik (id INTEGER PRIMARY KEY, tarih TEXT NOT NULL)"); const ekle = db.prepare("INSERT INTO hazirlik VALUES (?,?)"); for (const [id, tarih] of guncellemeler) { ekle.run(id, tarih); sayac.deyim += 1; } db.exec(`UPDATE odunc SET iade_tarihi = (SELECT tarih FROM hazirlik WHERE id = odunc.odunc_id) WHERE odunc_id IN (SELECT id FROM hazirlik)`); sayac.deyim += 1; db.exec("DROP TABLE hazirlik"); db.exec("COMMIT"); }); console.log(`\noranlar (tek tek guncellemeye gore): ${(a/b).toFixed(2)}x ${(a/c).toFixed(2)}x`);
node toplu-guncelleme.mjs
tek tek guncelleme guncellenen=1000 ornek(7)=2025-07-15 deyim=1000 sure= 0.9 ms deger listesinden guncelleme guncellenen=1000 ornek(7)=2025-07-15 deyim= 1 sure= 1.0 ms hazirlik tablosu uzerinden guncellenen=1000 ornek(7)=2025-07-15 deyim=1001 sure= 1.0 ms oranlar (tek tek guncellemeye gore): 0.91x 0.98x
Üç yolun süresi yerel ölçümde aynı. Bu, olumsuz bir sonuç değil, bir sonuçtur: aynı işlem sınırının içinde kaldıkları sürece deyim sayısı yerel veritabanında maliyeti belirlemiyor. Ayrım deyim sütununda: bin, bir ve bin bir. Veritabanı ağın ötesindeyse bu sayı doğrudan gidiş-dönüş sayısıdır ve o zaman aradaki fark bine katlanır.
Üçüncü yolun deyim sayısı en yüksek olduğu hâlde ayrı bir değeri vardır. Hazırlık tablosu verilerin önce ayrı bir bağıntıya yüklenmesini, orada doğrulanmasını ve ancak sonra hedefe uygulanmasını sağlar. Hedef bağıntıya tek bir deyimle dokunulur; hatalı satır hedefe hiç ulaşmaz. Hazırlık tablosunun yüklenmesi de kendi içinde bir toplu ekleme işidir ve bu dersin ilk ölçümündeki kurallara uyar.
Özet
- Aynı iki bin satır dört yoldan yazıldı: her satır ayrı işlemde 376 ms, tek işlemde 3 ms, hazır deyim yeniden kullanılarak 1 ms sürdü.
- En büyük kazanç işlem sınırından geldi; iki bin kesinleştirme yerine bir kesinleştirme yapmak süreyi yüz yirmi kattan fazla düşürdü.
- Parti büyüklüğünde kazanç erken düzleşiyor: 1’den 10’a geçmek süreyi üçte bire indirdi, 50’den 1000’e geçmek ölçülebilir bir katkı vermedi; sorgu metni ise doğrusal büyüdü.
- Toplu güncellemede üç yolun yerel süresi eşit çıktı; ayrım deyim sayısındadır (1000, 1, 1001) ve ağ üzerinden bu sayı gidiş-dönüş sayısına dönüşür.
- Hazırlık tablosu, veriyi hedefe uygulamadan önce doğrulama olanağı verir ve hedefe tek deyimle dokunur.
Sonraki Adım
Buraya kadar ölçülen her şey uygulamanın kendi yaptığı işti: kaç sorgu çalıştı, kaç bayt taşındı, kaç deyim gönderildi. Bir sorgu bu ölçütlerin hepsini geçip yine de yavaş olabilir. Tek bir sorgu, tek bir işlemde, yalnız gereken sütunlarla çalışıp saniyeler sürebilir; çünkü veritabanı o sorguyu nasıl yürüteceğine kendi karar verir. Sonraki ders o kararı uygulamanın içinden okur, yavaş sorguyu kütüğe düşürür ve planın neden değiştiğini gösterir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.