İçeriğe geç
academia.sh

Ders 13 / 21

Aşırı Veri Çekme

Aktarılan verinin bayt cinsinden ölçülmesi: bütün sütunları çekmekle gereken sütunları çekmenin farkı, satır sınırlamasının katkısı, toplamanın uygulamada mı veritabanında mı yapılacağı ve yıldız seçiminin sütun çakışmasıyla sessizce veri düşürmesi.

İçindekiler

Önceki ders sorgu sayısını düşürdü. Toplu getirme beş yüz kayıt için 565, birleştirme 500 satır döndürdü ve satır sayısı doğru ölçütmüş gibi göründü.

Satır sayısı aktarılan verinin ölçüsü değildir. Bir satır beş sütun da taşıyabilir, yirmi sütun da; sütunlardan biri kısa bir tarih de olabilir, bin karakterlik bir özet de. Bu ders ölçüyü bayta çevirir ve sınırlamanın iki düzeyini — hangi sütunlar ve kaç satır — ayrı ayrı ölçer.

Ölçüm Verisi

Ölçümün anlamlı olması için bağıntıların gerçekçi genişlikte olması gerekir: kitapta özet ve kapak metni, üyede adres ve notlar.

// uret.mjs — ozet metni tasiyan kitap bagintisi ile olcum verisi
import { DatabaseSync } from "node:sqlite";
import { rmSync } from "node:fs";
rmSync("kutuphane.db", { force: true });

const db = new DatabaseSync("kutuphane.db");
db.exec(`
CREATE TABLE uye (uye_id INTEGER PRIMARY KEY, ad TEXT NOT NULL, soyad TEXT NOT NULL,
                  eposta TEXT, adres TEXT, notlar TEXT);
CREATE TABLE kitap (kitap_id INTEGER PRIMARY KEY, baslik TEXT NOT NULL, yazar TEXT NOT NULL,
                    isbn TEXT NOT NULL, ozet TEXT NOT NULL, kapak_metni TEXT NOT NULL);
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);`);

const metin = (n, tohum) => `${tohum} `.repeat(n).slice(0, n * 6);
db.exec("BEGIN");
const uyeEkle = db.prepare("INSERT INTO uye VALUES (?,?,?,?,?,?)");
for (let i = 1; i <= 60; i++) {
  uyeEkle.run(i, `Uye${i}`, `Soyad${i}`, `uye${i}@ornek.test`,
              metin(20, `Sokak${i}`), metin(30, "not"));
}
const kitapEkle = db.prepare("INSERT INTO kitap VALUES (?,?,?,?,?,?)");
for (let i = 1; i <= 200; i++) {
  kitapEkle.run(i, `Kitap ${i}`, `Yazar ${(i % 40) + 1}`,
                `978-0-00-${String(100000 + i).slice(0, 6)}`, metin(120, "ozet"), metin(60, "kapak"));
}
const oduncEkle = db.prepare("INSERT INTO odunc VALUES (?,?,?,?,?)");
for (let i = 1; i <= 2000; i++) {
  const gun = String((i % 28) + 1).padStart(2, "0");
  const ay = String((i % 12) + 1).padStart(2, "0");
  oduncEkle.run(i, (i % 200) + 1, (i % 60) + 1, `2025-${ay}-${gun}`, i % 4 === 0 ? null : "2025-12-31");
}
db.exec("COMMIT");
console.log("kitap:", db.prepare("SELECT count(*) AS n FROM kitap").get().n,
            " ortalama ozet uzunlugu:", db.prepare("SELECT avg(length(ozet)) AS n FROM kitap").get().n,
            " acik odunc:", db.prepare("SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n);
node uret.mjs
kitap: 200  ortalama ozet uzunlugu: 600  acik odunc: 500

İki Düzeyde Sınırlama

Görev yine aynı: açık ödünç listesi. Ekranda gösterilen alanlar ödünç kimliği, alış tarihi, kitap başlığı ve üyenin adı. Üç sorgu bu listeyi üç farklı genişlikte çeker.

// alan-olcumu.mjs — ayni listeyi farkli sutun kumeleriyle ceker, aktarilan baytlari olcer
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const olc = (ad, sql) => {
  const t = performance.now();
  const satirlar = db.prepare(sql).all();
  const sure = performance.now() - t;
  const bayt = Buffer.byteLength(JSON.stringify(satirlar));
  console.log(`${ad.padEnd(26)} satir=${String(satirlar.length).padStart(4)}  ` +
    `bayt=${String(bayt).padStart(7)}  satir_basi=${String(Math.round(bayt / satirlar.length)).padStart(5)}  ` +
    `sure=${sure.toFixed(1)} ms`);
  return bayt;
};

const hepsi = olc("butun sutunlar", `
  SELECT * FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id
                        JOIN uye u ON u.uye_id = o.uye_id
  WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id`);

const gerekli = olc("yalniz gereken sutunlar", `
  SELECT o.odunc_id, o.alis_tarihi, k.baslik, u.ad, u.soyad
  FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id
  WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id`);

const sayfa = olc("gereken sutun + 20 satir", `
  SELECT o.odunc_id, o.alis_tarihi, k.baslik, u.ad, u.soyad
  FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id JOIN uye u ON u.uye_id = o.uye_id
  WHERE o.iade_tarihi IS NULL ORDER BY o.odunc_id LIMIT 20`);

console.log(`\nsutun secimi orani: ${(hepsi / gerekli).toFixed(1)}x`);
console.log(`sutun + satir orani: ${(hepsi / sayfa).toFixed(1)}x`);
node alan-olcumu.mjs
butun sutunlar             satir= 500  bayt= 731162  satir_basi= 1462  sure=1.5 ms
yalniz gereken sutunlar    satir= 500  bayt=  47744  satir_basi=   95  sure=0.3 ms
gereken sutun + 20 satir   satir=  20  bayt=   1867  satir_basi=   93  sure=0.0 ms

sutun secimi orani: 15.3x
sutun + satir orani: 391.6x

Satır sayısı ilk iki ölçümde aynı: beş yüz. Aktarılan veri on beş kat farklı. Fark tamamen sütun seçiminden geliyor; satır başına 1462 bayt yerine 95 bayt aktarılıyor. Ekranda gösterilmeyen özet, kapak metni, adres ve notlar sütunları taşınan verinin on dörtte on üçünü oluşturuyor.

Satır sınırlaması ikinci bir çarpan ekliyor. Sayfa başına yirmi satır gösteren bir arayüz için beş yüz satırın tamamını çekmek gereksizdir; iki sınırlama birlikte oranı 391 kata çıkarıyor.

Süreler burada yanıltıcıdır ve yanıltıcı olduğu için bırakıldı: veritabanı süreç içinde olduğu için 731 kilobayt bir buçuk milisaniyede “aktarıldı”. Ağ üzerinden aynı veri, bant genişliği ve gecikmeyle çarpılarak taşınır. Ölçüt yine süre değil, bayt olarak konur.

Toplamayı Nereye Koymalı

Aşırı veri çekmenin en keskin biçimi, uygulamanın veritabanının işini üstlenmesidir. “Kaç açık ödünç var” sorusu iki yoldan yanıtlanabilir.

// toplama-nerede.mjs — sayimi uygulamada mi veritabaninda mi yapmali
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

let t = performance.now();
const satirlar = db.prepare("SELECT * FROM odunc WHERE iade_tarihi IS NULL").all();
const uygulamada = satirlar.length;
const uygulamaSure = performance.now() - t;
const uygulamaBayt = Buffer.byteLength(JSON.stringify(satirlar));

t = performance.now();
const veritabaninda = db.prepare(
  "SELECT count(*) AS n FROM odunc WHERE iade_tarihi IS NULL").get().n;
const vtSure = performance.now() - t;
const vtBayt = Buffer.byteLength(JSON.stringify({ n: veritabaninda }));

console.log(`uygulamada sayma  : sonuc=${uygulamada}  bayt=${uygulamaBayt}  sure=${uygulamaSure.toFixed(2)} ms`);
console.log(`veritabaninda sayma: sonuc=${veritabaninda}  bayt=${vtBayt}  sure=${vtSure.toFixed(2)} ms`);
console.log(`bayt orani: ${Math.round(uygulamaBayt / vtBayt)}x`);
node toplama-nerede.mjs
uygulamada sayma  : sonuc=500  bayt=44845  sure=0.31 ms
veritabaninda sayma: sonuc=500  bayt=9  sure=0.05 ms
bayt orani: 4983x

Aynı sayı, beş bin kat farklı veriyle. Uygulamada sayma yöntemi kayıt sayısıyla büyür; veritabanında sayma sabit dokuz bayt döndürür. Toplama, süzme ve sıralama gibi işler verinin bulunduğu yerde yapılır; uygulamaya yalnız sonuç taşınır. Bu ilke gruplama ve uç değer sorguları için de aynen geçerlidir.

Yıldız Seçiminin İkinci Bedeli

Bütün sütunları istemenin maliyeti yalnız bayt değildir. Birleştirmede aynı adı taşıyan sütunlar çakışır ve sonuç nesnesinde biri ötekini örter.

// yildiz-tuzagi.mjs — birlestirmede yildiz secimi ayni adli sutunlari sessizce birlestirir
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("kutuphane.db");

const BIRLESTIRME = `FROM odunc o JOIN kitap k ON k.kitap_id = o.kitap_id
                                  JOIN uye u ON u.uye_id = o.uye_id
                     WHERE o.odunc_id = 4`;

console.log("uc bagintinin sutun toplami: 5 + 6 + 6 = 17");
console.log("yildizla donen alan sayisi :", Object.keys(db.prepare(`SELECT * ${BIRLESTIRME}`).get()).length);

// Sema degisir: kitap bagintisina da 'notlar' sutunu eklenir.
db.exec("ALTER TABLE kitap ADD COLUMN notlar TEXT");
db.exec("UPDATE kitap SET notlar = 'cildi yipranmis'");

const yildiz = db.prepare(`SELECT * ${BIRLESTIRME}`).get();
const acik = db.prepare(
  `SELECT k.notlar AS kitap_notu, u.notlar AS uye_notu ${BIRLESTIRME}`).get();

console.log("sema degisiminden sonra yildizla donen alan sayisi:", Object.keys(yildiz).length);
console.log("  yildiz.notlar   :", JSON.stringify(yildiz.notlar.slice(0, 20)));
console.log("  acik.kitap_notu :", JSON.stringify(acik.kitap_notu));
console.log("  acik.uye_notu   :", JSON.stringify(acik.uye_notu.slice(0, 20)));
node uret.mjs
node yildiz-tuzagi.mjs
kitap: 200  ortalama ozet uzunlugu: 600  acik odunc: 500
uc bagintinin sutun toplami: 5 + 6 + 6 = 17
yildizla donen alan sayisi : 15
sema degisiminden sonra yildizla donen alan sayisi: 15
  yildiz.notlar   : "not not not not not "
  acik.kitap_notu : "cildi yipranmis"
  acik.uye_notu   : "not not not not not "

İlk satırda on yedi sütun istendi, on beş alan geldi: kitap_id ve uye_id adları iki bağıntıda da bulunduğu için birleşti. Şemaya yeni bir sütun eklendikten sonra istenen sütun sayısı on sekize çıktı, dönen alan sayısı yine on beş kaldı. notlar alanı üyenin notunu taşıyor; kitabın notu sessizce düştü. Açık seçimle sorulduğunda iki değer de yerinde.

Buradaki tehlike hata vermemesidir. Sorgu başarılı, alan dolu, değer geçerli — ama yanlış bağıntıdan geliyor. Şema göçleri dersinde eklenen bir sütun, yıldız seçimi kullanan bir kod yolunu bu şekilde bozabilir ve göç sırasında hiçbir uyarı üretmez.

Ne Kadar Sınırlamalı

Sınırlamanın ters yönü de vardır. Gereğinden az sütun çeken bir sorgu, eksik kalan alan için ikinci bir gidiş-dönüş gerektirir; önceki dersin N+1 sorunu bu yoldan geri gelebilir. Bu yüzden ölçüt “en az sütun” değil, gösterilen alanların tam kümesidir.

Pratikte üç kural işe yarar. Liste görünümleri yalnız listede görünen alanları çeker; ayrıntı görünümü ayrı bir sorgu ile çağrılır. Uzun metin ve ikili içerik taşıyan sütunlar liste sorgularına hiç girmez. Sayfalama, sonuç kümesinin sınırını sorguya yazar; uygulamada diliminin alınması aktarımı azaltmaz, çünkü veri zaten taşınmıştır.

Bu kuralların uygulanabilmesi, sorgunun hangi alanları döndürdüğünün kod okunarak görülebilmesine bağlıdır. Yıldız seçimi bu görünürlüğü ortadan kaldırır: sorguya bakan biri hangi alanların taşındığını göremez, şema değiştiğinde taşınan veri sessizce büyür.

Özet

  • Aynı beş yüz satırlık liste, bütün sütunlarla 731162 bayt, yalnız gereken sütunlarla 47744 bayt aktardı; satır sayısı aynıyken fark 15,3 kat.
  • Satır sınırlaması ikinci bir çarpan ekledi; iki sınırlama birlikte oranı 391 kata çıkardı.
  • Sayımı uygulamada yapmak 44845 bayt, veritabanında yapmak 9 bayt taşıdı; toplama, süzme ve sıralama verinin bulunduğu yerde yapılır.
  • Yıldız seçimi birleştirmede aynı adlı sütunları birleştirdi; şemaya eklenen bir sütunun değeri sessizce düştü ve hiçbir hata üretilmedi.
  • Ölçüt “en az sütun” değil, gösterilen alanların tam kümesidir; eksik seçim ikinci bir gidiş-dönüş üretir.

Sonraki Adım

Bu iki ders okuma yolunu ele aldı: 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 tek tek mi yazılacak, tek bir işlemde mi, yoksa tek bir deyimde mi? Üç yol da aynı satırları üretir; süreleri arasında büyüklük mertebesi farkı vardır. Sonraki ders bu farkı ölçer, farkın nereden geldiğini gösterir ve parti büyüklüğünün nasıl seçildiğini ele alır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat