Ders 24 / 25
Toplu Veri Yükleme
Yüksek hacimli içe ve dışa aktarımın maliyeti: işlem sınırının yükleme hızına etkisi, dizinlerin yükleme sırasında mı sonrasında mı kurulacağı, hazırlık tablosuyla bozuk satırların ayıklanması ve dışa aktarımın biçimi.
İçindekiler
İzleme, sistemin olağan yükü altında ne yaptığını gösterir. Bazı işler ise olağan yükün dışındadır ve sistemi bilerek zorlar: bir arşivin içeri aktarılması, başka bir kurumdan gelen üye listesinin yüklenmesi, yıllık ödünç kayıtlarının dışarı çıkarılması. Bu işler milyonlarca satırı tek seferde taşır.
Toplu yüklemenin sıradan yazmadan ayrıldığı nokta şudur: sıradan yazmada bir satırın maliyeti önemsizdir, toplu yüklemede satır başına düşen her maliyet satır sayısıyla çarpılır. Bu ders o çarpanların nereden geldiğini ve nasıl kaldırıldığını ele alıyor.
İşlem Sınırının Bedeli
Her kesinleştirme, motorun değişikliği kalıcı hâle getirmesini gerektirir: günlük diske yazılır ve yazmanın gerçekten tamamlandığı doğrulanır. Bu adım kursun Motor Mimarisi konusunda ele alınmıştı ve orada bir satırın güvencesi olarak sunulmuştu. Toplu yüklemede aynı adım, satır sayısı kadar tekrarlanan bir maliyete dönüşür.
Aşağıdaki blok aynı otuz bin satırı beş farklı parti büyüklüğüyle yüklüyor. Mutlak süreler makineye, diske ve dosya sistemine bağlıdır; anlamlı olan satırlar arasındaki orandır.
rm -f yukleme.db yukleme.db-journal node - <<'EOF' const { DatabaseSync } = require("node:sqlite"); const fs = require("node:fs"); const N = 30000; const SEMA = `CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL)`; const olc = (ad, parti) => { fs.rmSync("yukleme.db", { force: true }); fs.rmSync("yukleme.db-journal", { force: true }); const db = new DatabaseSync("yukleme.db"); db.exec("PRAGMA synchronous=FULL"); db.exec(SEMA); const ekle = db.prepare( "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)"); const t = process.hrtime.bigint(); for (let i = 0; i < N; i += parti) { if (parti > 1) db.exec("BEGIN"); for (let j = i; j < Math.min(i + parti, N); j++) ekle.run(j % 400 + 1, j % 900 + 1, j % 3 + 1, "2025-06-01"); if (parti > 1) db.exec("COMMIT"); } const ms = Number(process.hrtime.bigint() - t) / 1e6; db.close(); console.log(ad.padEnd(26) + " | " + String(Math.ceil(N / parti)).padStart(6) + " | " + (ms.toFixed(0) + " ms").padStart(9) + " | " + Math.round(N / (ms / 1000)).toLocaleString("tr-TR").padStart(11)); }; console.log("yol | islem | sure | satir/saniye"); console.log("---------------------------|--------|-----------|------------"); olc("satir basina bir islem", 1); olc("100 satirlik partiler", 100); olc("1.000 satirlik partiler", 1000); olc("10.000 satirlik partiler", 10000); olc("tek islem", N); EOF
yol | islem | sure | satir/saniye ---------------------------|--------|-----------|------------ satir basina bir islem | 30000 | 5864 ms | 5.116 100 satirlik partiler | 300 | 66 ms | 454.359 1.000 satirlik partiler | 30 | 14 ms | 2.162.182 10.000 satirlik partiler | 3 | 10 ms | 3.094.565 tek islem | 1 | 9 ms | 3.373.219
Satır başına bir işlem ile tek işlem arasındaki fark altı yüz kattan fazla. Yüklenen veri aynı, yazılan satırlar aynı, yapılan iş aynı — fark yalnız kaç kez kesinleştirildiğidir.
Eğrinin biçimi de önemlidir. Kazancın büyük bölümü ilk adımda, yüzlük partilere geçişte elde ediliyor; binden sonrası küçük iyileşme veriyor. Bu, parti büyüklüğünün olabildiğince büyük seçilmesi gerektiği anlamına gelmez. Tek bir dev işlemin üç sakıncası vardır: başarısız olduğunda baştan başlanır, geri alma bilgisi işlem süresince birikir ve uzun süren yazma işlemi kursun Motor Mimarisi konusunda ele alınan ölü satır temizliğini geciktirir. Yüz bin ile bir milyon satır arasındaki bir parti büyüklüğü, ölçülen kazancın neredeyse tamamını verirken bu üç sakıncayı da sınırlar.
Parti sınırı aynı zamanda bir ilerleme noktasıdır. Her partinin sonunda kaç satırın yüklendiği bilinir; yükleme kesildiğinde kalınan yerden sürdürülebilir. Bunun için gelen verinin sırasının belirlenimci olması ve yüklenen son anahtarın kaydedilmesi yeter.
Dizinlerin Yükleme Sırasındaki Maliyeti
İkinci çarpan dizinlerdir. Tabloya eklenen her satır, her dizine de bir giriş ekler ve bu girişin ağacın doğru yerine yerleştirilmesi gerekir. Satırlar dizin anahtarına göre sıralı gelmediğinde, her ekleme ağacın rastgele bir yerine dokunur.
Alternatif, dizinleri yükleme bittikten sonra kurmaktır: motor o zaman bütün anahtarları bir kerede sıralar ve ağacı aşağıdan yukarı, sırayla oluşturur.
rm -f dizinli.db dizinli.db-journal node - <<'EOF' const { DatabaseSync } = require("node:sqlite"); const fs = require("node:fs"); const N = 200000; const SEMA = `CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL, sube_id INT NOT NULL, alis TEXT NOT NULL)`; const DIZIN = ["CREATE INDEX odunc_uye ON odunc(uye_id)", "CREATE INDEX odunc_kitap ON odunc(kitap_id)", "CREATE INDEX odunc_sube_alis ON odunc(sube_id, alis)"]; const sure = (is) => { const t = process.hrtime.bigint(); is(); return Number(process.hrtime.bigint() - t) / 1e6; }; const olc = (ad, oncedenDizin) => { fs.rmSync("dizinli.db", { force: true }); fs.rmSync("dizinli.db-journal", { force: true }); const db = new DatabaseSync("dizinli.db"); db.exec(SEMA); if (oncedenDizin) for (const d of DIZIN) db.exec(d); const ekle = db.prepare( "INSERT INTO odunc(kitap_id,uye_id,sube_id,alis) VALUES(?,?,?,?)"); const yukleme = sure(() => { db.exec("BEGIN"); for (let i = 0; i < N; i++) ekle.run(i % 400 + 1, i % 900 + 1, i % 3 + 1, "2025-06-01"); db.exec("COMMIT"); }); const dizinKurma = oncedenDizin ? 0 : sure(() => { for (const d of DIZIN) db.exec(d); }); db.close(); console.log(ad.padEnd(24) + " | " + (yukleme.toFixed(0) + " ms").padStart(9) + " | " + (dizinKurma ? dizinKurma.toFixed(0) + " ms" : "-").padStart(9) + " | " + ((yukleme + dizinKurma).toFixed(0) + " ms").padStart(9) + " | " + (Math.round(fs.statSync("dizinli.db").size / 1024) + " KB").padStart(9)); }; console.log(N.toLocaleString("tr-TR") + " satir, uc dizin"); console.log(); console.log("yol | yukleme | dizin | toplam | dosya"); console.log("-------------------------|-----------|-----------|-----------|----------"); olc("dizinler onceden var", true); olc("dizinler sonradan", false); EOF
200.000 satir, uc dizin yol | yukleme | dizin | toplam | dosya -------------------------|-----------|-----------|-----------|---------- dizinler onceden var | 551 ms | - | 551 ms | 14128 KB dizinler sonradan | 60 ms | 84 ms | 144 ms | 13352 KB
Yükleme süresi dokuz kat düştü; dizin kurma süresi eklendiğinde bile toplam yaklaşık dörtte bire indi. Dosya da küçüldü: toplu kurulan bir dizin sayfalarını sıkı doldurur, satır satır büyütülen dizin ise sayfa bölünmeleri nedeniyle boşluk bırakır. Bu boşluk, dizinlerin bakımı konusunda — bu kursun Dizinler ve Bölümleme konusunda — şişme başlığı altında ele alınan durumdur.
Yöntemin bedeli, dizinlerin bulunmadığı süre boyunca tablonun okuma başarımının düşmesidir. İlk yükleme yapılan boş bir tabloda bu bedel yoktur. Var olan ve kullanılan bir tabloya büyük bir aktarım yapılıyorsa dizinleri düşürmek seçenek değildir; o durumda parti büyüklüğü ayarlanır ve yükleme yoğun olmayan bir saate alınır.
Aynı gerekçe kısıtlar için de geçerlidir. Yabancı anahtar denetimi her satırda bir arama yapar; benzersizlik kısıtı her satırda bir denetim gerektirir. Motorların çoğu kısıtların işlem sonuna ertelenmesine izin verir; ertelenen denetim satır başına değil, bir kerede yapılır.
Hazırlık Tablosu ve Bozuk Satırlar
Dışarıdan gelen veri temiz gelmez. Tek bir bozuk satır, doğrudan hedef tabloya yazan bir yüklemeyi işlemin ortasında durdurur ve o ana kadar yapılan iş geri alınır. Yirmi bin satırın bin üç yüzüncüsünde duran bir yükleme, hem zaman kaybettirir hem de sorunun tamamını göstermez — sıradaki bozuk satırlar henüz görülmemiştir.
Yerleşik çözüm bir hazırlık tablosudur (staging table): gelen dosya, her sütunu metin olan ve hiçbir kısıt taşımayan bir tabloya olduğu gibi alınır. Doğrulama, veri veritabanına girdikten sonra sorguyla yapılır. Geçerli satırlar hedefe taşınır, geçersizler nedeniyle birlikte ayrılır.
rm -f aktarim.db odunc_gelen.csv temiz.csv # Disaridan gelen dosya: 20.000 satirlik bir arsiv aktarimi, icinde bozuk satirlar var. node - <<'EOF' > odunc_gelen.csv console.log("id,kitap_id,uye_id,sube_id,alis"); for (let i = 1; i <= 20000; i++) { if (i === 77) { console.log("yetmisyedi,12,34,1,2025-05-04"); continue; } // sayi degil if (i === 512) { console.log("512,12,34,1,04.05.2025"); continue; } // tarih bicimi if (i === 900) { console.log("900,12,99999,1,2025-05-04"); continue; } // olmayan uye if (i === 1301) { console.log("1301,12,34,1"); continue; } // eksik alan console.log([i, (i % 400) + 1, (i % 900) + 1, (i % 3) + 1, "2025-05-04"].join(",")); } EOF sqlite3 aktarim.db <<'SQL' CREATE TABLE uye(id INTEGER PRIMARY KEY, ad TEXT NOT NULL); WITH RECURSIVE s(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM s WHERE n<900) INSERT INTO uye(id,ad) SELECT n,'Uye-'||n FROM s; CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT NOT NULL, uye_id INT NOT NULL REFERENCES uye(id), sube_id INT NOT NULL, alis TEXT NOT NULL); -- Hazirlik tablosu: her sutun metin, hicbir kisit yok. CREATE TABLE hazirlik(id TEXT, kitap_id TEXT, uye_id TEXT, sube_id TEXT, alis TEXT); CREATE TABLE hatali(satir TEXT, neden TEXT); SQL sqlite3 aktarim.db ".import --csv --skip 1 odunc_gelen.csv hazirlik" echo "hazirlik tablosuna alinan satir: $(sqlite3 aktarim.db 'SELECT COUNT(*) FROM hazirlik;')" sqlite3 -box -header aktarim.db <<'SQL' INSERT INTO hatali(satir, neden) SELECT id || ',' || IFNULL(kitap_id,'') || ',' || IFNULL(uye_id,'') || ',' || IFNULL(sube_id,'') || ',' || IFNULL(alis,''), CASE WHEN id GLOB '*[^0-9]*' OR uye_id GLOB '*[^0-9]*' THEN 'sayisal olmayan alan' WHEN alis IS NULL OR alis NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' THEN 'tarih bicimi' WHEN NOT EXISTS (SELECT 1 FROM uye WHERE uye.id = CAST(hazirlik.uye_id AS INTEGER)) THEN 'olmayan uye' END FROM hazirlik WHERE id GLOB '*[^0-9]*' OR uye_id GLOB '*[^0-9]*' OR alis IS NULL OR alis NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' OR NOT EXISTS (SELECT 1 FROM uye WHERE uye.id = CAST(hazirlik.uye_id AS INTEGER)); INSERT INTO odunc(id,kitap_id,uye_id,sube_id,alis) SELECT CAST(id AS INTEGER), CAST(kitap_id AS INTEGER), CAST(uye_id AS INTEGER), CAST(sube_id AS INTEGER), alis FROM hazirlik WHERE id NOT GLOB '*[^0-9]*' AND uye_id NOT GLOB '*[^0-9]*' AND alis GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' AND EXISTS (SELECT 1 FROM uye WHERE uye.id = CAST(hazirlik.uye_id AS INTEGER)); SELECT (SELECT COUNT(*) FROM hazirlik) AS gelen, (SELECT COUNT(*) FROM odunc) AS alinan, (SELECT COUNT(*) FROM hatali) AS ayrilan; SELECT neden, COUNT(*) AS satir FROM hatali GROUP BY neden ORDER BY satir DESC; SQL # Disari aktarim: ayni bicimde bir dosya uretilir. sqlite3 aktarim.db <<'SQL' .mode csv .headers on .output temiz.csv SELECT id, kitap_id, uye_id, sube_id, alis FROM odunc WHERE sube_id=1; SQL echo "disari yazilan satir: $(($(wc -l < temiz.csv) - 1))" head -2 temiz.csv
odunc_gelen.csv:1302: expected 5 columns but found 4 - filling the rest with NULL hazirlik tablosuna alinan satir: 20000 ┌───────┬────────┬─────────┐ │ gelen │ alinan │ ayrilan │ ├───────┼────────┼─────────┤ │ 20000 │ 19996 │ 4 │ └───────┴────────┴─────────┘ ┌──────────────────────┬───────┐ │ neden │ satir │ ├──────────────────────┼───────┤ │ tarih bicimi │ 2 │ │ sayisal olmayan alan │ 1 │ │ olmayan uye │ 1 │ └──────────────────────┴───────┘ disari yazilan satir: 6665 id,kitap_id,uye_id,sube_id,alis 3,4,4,1,2025-05-04
Yükleme durmadı. Yirmi bin satırın tamamı veritabanına girdi, on dokuz bin dokuz yüz doksan altısı hedefe taşındı ve dört satır nedeniyle birlikte ayrıldı. Ayrılan satırların listesi düzeltilip yeniden yüklenebilir; bu, tek bir hatalı satır yüzünden bütün aktarımın tekrarlanmasından çok daha ucuzdur.
Çıktının ilk satırı ayrıca öğreticidir. Eksik alanlı satır, içe aktarım aracı tarafından bir uyarıyla karşılandı ve eksik sütun boş değerle dolduruldu — yani sessizce reddedilmedi, sessizce kabul edildi. Doğrulama katmanı olmasaydı bu satır tarih alanı boş olarak hedefe girerdi. İçe aktarma araçlarının hatalı biçimi nasıl karşıladığı motora ve araca göre değişir; hangi davranışın geçerli olduğu varsayılmaz, sınanır.
Dışa aktarım aynı sözleşmenin ters yönüdür: sütun sırası, ayırıcı, başlık satırı ve tarih biçimi karşı taraf için belirlenir. Aktarımların iki yönü de aynı biçim tanımına bağlıysa, dışarı verilen dosya kendi hazırlık tablosuna geri yüklenerek sınanabilir.
Yükleme Sırasındaki Diğer Kararlar
Üç ayar daha yükleme süresini belirler ve üçü de aynı ödünleşimi taşır: dayanıklılığın geçici olarak azaltılması.
Günlük ayarı. Motorların bir bölümü, toplu yükleme sırasında günlüğe daha az yazan bir kip sunar. Kazanç büyüktür; karşılığında yükleme sırasında bir çökme olursa tablo kurtarılamaz duruma gelebilir ve yükleme baştan yapılır. Boş bir tabloya ilk yükleme yapılırken kabul edilebilir, var olan veriye eklerken değil.
Eşitleme ayarı. Kesinleştirmede diske yazmanın tamamlanmasını beklemeyen bir ayar, küçük partilerde büyük fark yaratır. Yükleme bittikten sonra eski değere döndürülmesi gerekir; bu adımın unutulması, sistemin sonraki aylarını sessizce risk altında bırakır.
Kopyalama komutu. Motorların çoğu, satır satır ekleme yerine akış hâlinde veri alan bir komut sağlar. Yukarıdaki içe aktarım bunun bir örneğidir. Kazancı, deyim ayrıştırma ve tekil kayıt işleme yükünün ortadan kalkmasıdır.
Son olarak yüklemenin ardından istatistiklerin yenilenmesi gerekir. Planlayıcı, tabloda kaç satır olduğunu ve değerlerin nasıl dağıldığını kendi topladığı istatistiklerden bilir; İleri SQL kursunda ele alınan bu bilgi, toplu yüklemeden sonra gerçekle bağını yitirir. Yenilenmediğinde planlayıcı boş sandığı bir tabloyu tarar ve yükleme sonrası sorgular beklenmedik biçimde yavaşlar.
Özet
- Kesinleştirme sayısı toplu yüklemenin birinci maliyetidir; satır başına bir işlem ile partili yükleme arasında ölçümde altı yüz katı aşan fark çıktı.
- Kazancın büyük bölümü ilk birkaç yüz satırlık partide elde edilir; çok büyük partiler yeniden başlama maliyetini ve geri alma bilgisini büyütür.
- Dizinler yükleme sırasında satır başına bakım gerektirir; sonradan toplu kurulduğunda hem süre düşer hem de dizin daha az yer kaplar.
- Hazırlık tablosu, gelen veriyi kısıtsız biçimde alıp doğrulamayı sorguya taşır; bozuk satırlar yüklemeyi durdurmadan nedeniyle birlikte ayrılır.
- İçe aktarma araçları hatalı biçimli satırları sessizce kabul edebilir; davranış varsayılmaz, sınanır.
- Yükleme sonrası istatistiklerin yenilenmesi gerekir; yenilenmediğinde planlayıcı eski bilgiyle çalışır.
Sonraki Adım
Bu konudaki dersler bir veritabanının yaşamını sürdürmesi için gereken her işi kurdu: yetkilendirme, yedekleme, kurtarma, çoğaltma, erişilebilirlik, bağlantı yönetimi, izleme ve toplu aktarım. Geriye bir tanesi kaldı ve hepsini birden ilgilendirir. Motorun sürümü zamanla değişir; güvenlik düzeltmeleri, hata gidermeleri ve desteğin sona ermesi yükseltmeyi zorunlu kılar. Yükseltme, çalışan bir sistemin altındaki katmanı değiştirmektir: kesinti penceresi, uyumluluk denetimi, geri dönüş planı ve uygulamanın iki sürümle birden çalışabilmesi gerekir. Son ders bunu ele alıyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.