DuckDB ile CSV ve Parquet dosyalarını yüklemeden sorgulamak: küçük ekip için analitik katman
DuckDB ile CSV ve Parquet dosyalarını veritabanına yüklemeden SQL ile sorgulama: glob okuma, bellek sınırı, Parquet'e dönüştürme, SQLite ile iş bölümü, rapor hattı.
DuckDB, tek bir Python paketi olarak kurulan ve klasördeki CSV ya da Parquet dosyalarını önce bir veritabanına yüklemeden doğrudan SQL ile sorgulayan gömülü bir analitik motordur. Küçük ekiplerde veri çoğu zaman bir sunucuda değil, bir klasörde yaşar: muhasebeden gelen aylık dışa aktarımlar, bir API'den çekilmiş günlük JSON dökümleri, ERP'den alınmış satış tabloları. Bu dosyalar üzerinde soru sormak için genelde ya Excel açılır ya da pandas ile her seferinde aynı okuma ve birleştirme kodu yeniden yazılır.
Sorun, bu iki yolun da ölçeklenmemesidir. Excel birkaç yüz bin satırda yavaşlar, pandas ise dosyanın tamamını belleğe almak ister ve RAM'den büyük bir dosyada süreç çöker. Bir veri ambarı kurmak ise tek kişilik ya da üç kişilik bir ekip için hem maliyet hem bakım yükü demektir. DuckDB bu iki uç arasındaki boşluğu doldurur: sunucu yoktur, kurulum tek komuttur, sorgu dili standart SQL'dir ve dosyaları olduğu yerde okur.
Bu yazıda DuckDB'yi bir analitik katman olarak nasıl konumlandırdığımı anlatıyorum: hangi durumlarda seçilir, bir klasör dolusu dosya tek sorguyla nasıl okunur, bellek sınırı nasıl yönetilir, işlemsel kayıt tutan SQLite ile iş bölümü nasıl yapılır ve bütün bunlar bir günlük rapor hattına nasıl bağlanır. Örnekler Python 3.9 ve üzeri ile, yalnız duckdb paketini gerektirir.
DuckDB nedir ve ne zaman seçilir
DuckDB, sütun tabanlı depolama ve vektörleştirilmiş yürütme kullanan, süreç içi çalışan bir analitik veritabanıdır. SQLite nasıl bir işlemsel veritabanını tek dosyaya ve tek kütüphaneye sığdırıyorsa, DuckDB de aynı yaklaşımı toplama, gruplama ve birleştirme ağırlıklı analitik sorgular için uygular. Ayrı bir sunucu süreci, kullanıcı yönetimi ya da ağ yapılandırması yoktur; Python'dan import duckdb ile kullanılır.
Seçim ölçütü, iş yükünün biçimidir. Çok sayıda küçük yazma ve tekil satır okuma varsa, yani uygulama kaydı tutuluyorsa, SQLite ya da PostgreSQL doğru araçtır. Az sayıda büyük okuma, milyonlarca satır üzerinde gruplama ve birkaç tabloyu birleştirme varsa DuckDB öne çıkar. İki iş yükü aynı projede varsa ikisi birlikte kullanılır; bunu aşağıda ayrıca ele alıyorum.
DuckDB'nin küçük ekip için üç somut avantajı var:
- Kurulum ve bakım yükü yok: paket güncellemesi dışında yönetilecek bir şey bulunmaz.
- Dosya biçimleri yerleşik: CSV, Parquet ve JSON ek kütüphane olmadan okunur ve yazılır.
- Bellek disiplini: sorgu belleğe sığmazsa geçici dosyaya taşar, süreç çökmez.
CSV ve Parquet dosyalarını doğrudan sorgulamak
Doğrudan sorgulama, bir dosya yolunu ya da joker desenini SQL'de tablo adı yerine yazmak demektir. DuckDB read_csv ve read_parquet fonksiyonlarıyla dosyayı açar, sütun adlarını ve tiplerini ilk satırlardan çıkarır, eşleşen bütün dosyaları tek bir sanal tablo gibi sunar. Aylık dışa aktarımların bulunduğu bir klasörü birleştirmek için döngü ya da concat gerekmez.
Aşağıdaki örnek, veri/satis/ altındaki bütün CSV dosyalarını tek sorguda okuyup ürün bazında aylık ciro çıkarır. filename seçeneği her satırın hangi dosyadan geldiğini de sütun olarak ekler; bu, bozuk bir dışa aktarımı bulmak için en kısa yoldur.
import duckdb
con = duckdb.connect() # bellek içi bağlantı; dosya adı verilirse kalıcı olur
sorgu = """
SELECT
date_trunc('month', tarih) AS ay,
urun_kodu,
SUM(adet * birim_fiyat) AS ciro,
COUNT(*) AS satir
FROM read_csv('veri/satis/*.csv',
header = true,
delim = ';',
decimal_separator = ',',
dateformat = '%d.%m.%Y',
filename = true)
GROUP BY 1, 2
ORDER BY 1, ciro DESC
"""
for satir in con.execute(sorgu).fetchall():
print(satir)
# pandas kuruluysa aynı sonuç DataFrame olarak alınabilir:
df = con.execute(sorgu).df()
print(df.head())
Türkçe dışa aktarımlarda üç ayar neredeyse her zaman gerekir: noktalı virgül ayırıcı, virgüllü ondalık ve gün.ay.yıl tarih biçimi. Bunlar belirtilmezse DuckDB tahmin etmeye çalışır ve tahmin yanlışsa fiyat sütunu metin olarak gelir. Tip çıkarımını DESCRIBE SELECT * FROM read_csv(...) ile sorgu yazmadan önce kontrol etmek, sonradan hata aramaktan ucuzdur.
Parquet dosyalarında bu sorunlar yoktur, çünkü biçim sütun tiplerini kendi içinde taşır. Bu yüzden ham CSV'yi bir kez Parquet'e çevirip sonraki bütün sorguları Parquet üzerinde çalıştırmak hem hız hem tutarlılık kazandırır. Dönüşüm tek bir COPY ifadesidir ve bir sonraki bölümde bellek yönetimiyle birlikte gösteriyorum.
Bellek sınırı ve RAM'den büyük dosyalar
Bellek sınırı, DuckDB'nin bir sorgu için kullanabileceği en yüksek RAM miktarını belirleyen ve aşıldığında ara sonuçları diske taşıran ayardır. Varsayılan değer makinenin toplam belleğine göre hesaplanır; aynı sunucuda başka servisler de çalışıyorsa bu değeri açıkça düşürmek gerekir. Sınır düşük tutulduğunda sorgu yavaşlar ama tamamlanır; sınır yokken süreç işletim sistemi tarafından sonlandırılabilir.
Aşağıdaki örnek iki şeyi birden yapıyor: bağlantıyı bellek ve iş parçacığı sınırıyla açıyor, ardından büyük bir CSV'yi okuyup yıl ve aya göre bölümlenmiş Parquet klasörüne yazıyor. Yazma işlemi akış halinde ilerlediği için kaynak dosyanın RAM'e sığması gerekmez.
import duckdb
from pathlib import Path
HAM = Path("veri/ham/islemler.csv")
HEDEF = Path("veri/parquet/islemler")
HEDEF.mkdir(parents=True, exist_ok=True)
con = duckdb.connect()
con.execute("SET memory_limit = '2GB'")
con.execute("SET threads = 2")
con.execute("SET temp_directory = 'veri/tmp'")
con.execute(f"""
COPY (
SELECT
*,
year(tarih) AS yil,
month(tarih) AS ay
FROM read_csv('{HAM.as_posix()}', header = true, delim = ';',
dateformat = '%d.%m.%Y', decimal_separator = ',')
)
TO '{HEDEF.as_posix()}'
(FORMAT PARQUET, PARTITION_BY (yil, ay), OVERWRITE_OR_IGNORE true)
""")
# Bölümlenmiş klasör tek tablo gibi okunur; WHERE koşulu yalnız ilgili bölümleri açar
toplam = con.execute(f"""
SELECT yil, ay, COUNT(*) AS satir, SUM(tutar) AS tutar
FROM read_parquet('{HEDEF.as_posix()}/*/*/*.parquet', hive_partitioning = true)
WHERE yil = 2025
GROUP BY yil, ay ORDER BY yil, ay
""").fetchall()
print(toplam)
Bölümleme (partition) burada bir hız hilesinden fazlasıdır. Sorguda WHERE yil = 2025 yazıldığında DuckDB yalnız o klasörün altındaki dosyaları açar, diğerlerine dokunmaz. Aylık rapor için geçen ay klasörünü okumak, iki yıllık tek bir dosyayı taramaktan her koşulda daha az kaynak ister. Bölüm anahtarı olarak rapor sorgularında en çok filtrelenen sütun seçilir; satış verisinde bu genellikle tarih, destek verisinde ise ekip ya da kanal olur.
SQLite ile iş bölümü: işlemsel kayıt ve analitik sorgu
İş bölümü, günlük işlemleri SQLite'ta tutup analitik sorguları DuckDB ile çalıştırmak anlamına gelir. SQLite satır satır yazma, kilit ve işlem (transaction) güvencesi konusunda güçlüdür; DuckDB ise milyonlarca satırı toplamada. Aynı veriyi iki yerde tutmak yerine SQLite kaynak olarak kalır, DuckDB onu okur ya da düzenli aralıklarla Parquet anlık görüntüsü alır.
Doğrudan okuma için DuckDB'nin sqlite eklentisi vardır; INSTALL sqlite; LOAD sqlite; komutlarından sonra ATTACH 'uygulama.db' AS app (TYPE sqlite) ile SQLite tabloları DuckDB sorgularında kullanılabilir. Eklenti ilk kullanımda internetten indirildiği için kapalı ağdaki sunucularda önceden paketlenmesi gerekir. Daha basit ve bağımlılıksız yol, SQLite'tan gece bir Parquet anlık görüntüsü almaktır:
import sqlite3
import duckdb
from datetime import date
KAYNAK = "uygulama.db"
HEDEF = f"veri/parquet/siparis_{date.today():%Y%m%d}.parquet"
# 1) SQLite'tan okunacak satırları standart kütüphaneyle çek
src = sqlite3.connect(KAYNAK)
src.row_factory = sqlite3.Row
satirlar = [dict(r) for r in src.execute(
"SELECT id, musteri_id, tarih, tutar, durum FROM siparis WHERE tarih >= date('now', '-90 day')"
)]
src.close()
# 2) Listeyi DuckDB'ye tablo olarak ver, Parquet'e yaz
con = duckdb.connect()
con.execute("CREATE TABLE siparis AS SELECT unnest($satirlar, recursive := true)", {"satirlar": satirlar})
con.execute(f"COPY siparis TO '{HEDEF}' (FORMAT PARQUET)")
print(len(satirlar), "satır yazıldı:", HEDEF)
Bu desen, uygulama veritabanını rapor sorgularının yükünden korur ve raporun hangi anın verisiyle üretildiğini dosya adında belgeler. SQLite'ı küçük boru hatlarının omurgası olarak nasıl kurduğumu SQLite ile küçük ama sağlam veri boru hatları yazısında anlattım; buradaki anlık görüntü adımı o hattın doğal devamıdır.
Günlük rapor hattına bağlamak
Rapor hattı, DuckDB sorgusunu bir SQL dosyasından okuyup sonucu CSV ya da Parquet olarak yazan ve zamanlayıcıdan çağrılan küçük bir komut satırı aracıdır. SQL'in Python kodundan ayrı bir dosyada durması, raporu değiştirmek isteyen kişinin kod okumadan yalnız sorguyu düzenlemesine izin verir. Çıktı dosyası sabit bir yola yazılır; şablon ve PDF üretimi bu dosyayı girdi olarak alır.
Aşağıdaki araç üç parametre alır: sorgu dosyası, çıktı yolu ve isteğe bağlı rapor tarihi. Tarih verilmezse dünü kullanır, böylece gece çalışan görev her zaman tamamlanmış bir günü raporlar. Sorgu dosyasında $tarih adlı bağlı parametre bulunur ve dışarıdan gelen değer metin birleştirmeyle değil parametre olarak geçirilir.
import argparse
import sys
from datetime import date, timedelta
from pathlib import Path
import duckdb
def main() -> int:
ap = argparse.ArgumentParser(description="DuckDB rapor hattı")
ap.add_argument("sorgu", type=Path, help="SQL dosyası; $tarih parametresi kullanabilir")
ap.add_argument("cikti", type=Path, help="Sonuç dosyası (.csv ya da .parquet)")
ap.add_argument("--tarih", type=date.fromisoformat, default=date.today() - timedelta(days=1))
ap.add_argument("--bellek", default="1GB")
a = ap.parse_args()
con = duckdb.connect()
con.execute("SET memory_limit = $b", {"b": a.bellek})
try:
sql = a.sorgu.read_text(encoding="utf-8")
con.execute("CREATE TEMP TABLE sonuc AS " + sql, {"tarih": a.tarih})
bicim = "PARQUET" if a.cikti.suffix == ".parquet" else "CSV, HEADER true, DELIMITER ';'"
a.cikti.parent.mkdir(parents=True, exist_ok=True)
con.execute(f"COPY sonuc TO '{a.cikti.as_posix()}' (FORMAT {bicim})")
satir = con.execute("SELECT COUNT(*) FROM sonuc").fetchone()[0]
except (duckdb.Error, OSError) as e:
print(f"rapor üretilemedi: {e}", file=sys.stderr)
return 1
print(f"{satir} satır yazıldı: {a.cikti} (tarih={a.tarih})")
return 0
if __name__ == "__main__":
sys.exit(main())
Çıkış kodu burada önemlidir: sorgu hata verdiğinde araç sıfır dışı kodla biter ve zamanlayıcı ya da üst hat bunu görür. Üretilen CSV ya da Parquet dosyası, şablondan otomatik rapor üretimi yazısındaki HTML ve PDF adımına doğrudan girdi olur; iki aracı aynı crontab satırında art arda bağlamak yeterlidir.
Sonuç: ilk adım
DuckDB, küçük ekibin elindeki dosya yığınını sunucu kurmadan sorgulanabilir bir analitik katmana çevirir. Ham CSV'ler bir kez bölümlenmiş Parquet'e dönüştürülür, bellek sınırı açıkça konur, işlemsel veri SQLite'ta kalıp gece anlık görüntüsü alınır ve raporlar SQL dosyalarından üretilen bir araçla zamanlayıcıya bağlanır. Her parça tek başına küçüktür; birlikte, veri ambarı olmadan veri ambarı disiplini sağlar.
İlk adım olarak en sık açtığınız dışa aktarım klasörünü seçin, DESCRIBE SELECT * FROM read_csv('klasor/*.csv') ile tip çıkarımını kontrol edin ve o klasörü tek bir COPY ile Parquet'e çevirin. Bir haftalık raporu bu Parquet üzerinde SQL ile yeniden ürettiğinizde, pandas'ta kaç satır kod sildiğinizi görmek yöntemin değerini kendiliğinden anlatır. Kendi veri yığınınız için hangi katmanın uygun olduğunu birlikte değerlendirmek isterseniz iletişim sayfasından yazabilirsiniz.