Ana içeriğe geç
Kesit Python
Orta · Veritabanı (giriş)

sqlite3 ile Python

Bellek içi SQLite veritabanı kurar, parametreli sorguyla SQL enjeksiyonundan korunur, executemany ile toplu yükler, Row factory ile satırları sözlük gibi okur ve transaction mantığını kavrarsın.

Uygulamada aç, 28 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden SQLite?

SQLite, tek bir dosyada (ya da bellekte) yaşayan, sunucu gerektirmeyen bir ilişkisel veritabanıdır (relational database). Python ile birlikte gelen sqlite3 modülü sayesinde kurulum yapmadan SQL yazabilirsin. Anadolu Market'te bir analist için tipik kullanım şu: binlerce satırlık sipariş CSV'sini bir SQLite tablosuna yükle, SQL ile grupla ve filtrele, sonucu Python ile raporla. Masaüstü uygulamaları, mobil uygulamalar ve tarayıcılar da yerel verisini SQLite'ta tutar; yani öğrendiğin şey oyuncak değil, dünyanın en yaygın veritabanı motoru.

Kesit SQL'de SQL'in kendisini öğrendin. Bu derste sorulan soru farklı: Python, SQL ile nasıl konuşur? Üç kavram yeter: bağlantı (connection), imleç (cursor) ve transaction.

Bağlantı, tablo, ekleme, sorgu

sqlite3.connect(":memory:") yalnızca RAM'de yaşayan bir veritabanı açar; program bitince kaybolur. Derste ve testte idealdir. Dosya istersen connect("market.db") yazarsın. execute bir SQL cümlesi çalıştırır, fetchall sonuç satırlarını demet listesi olarak getirir.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("""
    CREATE TABLE siparis (
        id INTEGER PRIMARY KEY,
        musteri TEXT NOT NULL,
        sehir TEXT NOT NULL,
        tutar REAL NOT NULL
    )
""")
con.execute("INSERT INTO siparis (musteri, sehir, tutar) VALUES (?, ?, ?)", ("Ayşe", "Ankara", 1250.0))
con.execute("INSERT INTO siparis (musteri, sehir, tutar) VALUES (?, ?, ?)", ("Mehmet", "İzmir", 480.5))
con.commit()

satirlar = con.execute("SELECT id, musteri, sehir, tutar FROM siparis ORDER BY id").fetchall()
print(satirlar)
print("ilk satır türü:", type(satirlar[0]).__name__)
toplam = con.execute("SELECT SUM(tutar) FROM siparis").fetchone()[0]
print("toplam:", toplam)
con.close()

Dikkat edilecek nokta: fetchall demet (tuple) listesi döner, sütun adları yoktur. fetchone tek satır (ya da None) getirir; tek değerli sonuçlarda [0] ile içini alırsın. con.execute, arka planda bir imleç oluşturup çalıştırmanın kısa yoludur; cur = con.cursor() ile açık imleç de kullanabilirsin.

Parametreli sorgu ve SQL enjeksiyonu

Yukarıdaki ? işaretleri yer tutucudur (placeholder). Değerleri SQL metnine f-string ile gömmek yerine ayrı bir demetle verirsin. Bunun iki nedeni var: sqlite3 tırnak ve özel karakterleri doğru kaçırır, ve kullanıcıdan gelen metin SQL olarak yorumlanmaz. Aşağıdaki örnek, kötü niyetli bir girdinin f-string'li sorguyu nasıl ele geçirdiğini, parametreli sorgunun ise aynı metni sadece veri olarak gördüğünü gösterir.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE musteri (ad TEXT, sehir TEXT)")
con.executemany("INSERT INTO musteri VALUES (?, ?)", [("Ayşe", "Ankara"), ("Can", "Bursa"), ("Elif", "Ankara")])

girdi = "Ankara' OR '1'='1"   # arama kutusuna yazılan kötü niyetli metin

# YANLIŞ: metin SQL'in parçası olur, koşul her zaman doğru çıkar
kotu = con.execute(f"SELECT ad FROM musteri WHERE sehir = '{girdi}'").fetchall()
print("f-string ile:", [r[0] for r in kotu])

# DOĞRU: metin yalnızca bir değerdir, hiçbir şehirle eşleşmez
iyi = con.execute("SELECT ad FROM musteri WHERE sehir = ?", (girdi,)).fetchall()
print("parametre ile:", [r[0] for r in iyi])

# İsimli parametre: okunur ve sıraya bağlı değil
isimli = con.execute("SELECT ad FROM musteri WHERE sehir = :s ORDER BY ad", {"s": "Ankara"}).fetchall()
print("isimli parametre:", [r[0] for r in isimli])
Tek değerli parametre demet olmalı(girdi,) sondaki virgüle dikkat. (girdi) yazarsan parantez bir işlem önceliği parantezi olur, demet değil; sqlite3 metni karakter karakter parametre sanıp hata verir.

executemany, aynı sorguyu bir dizi parametreyle tekrar tekrar çalıştırır; CSV'den yüzlerce satır yüklerken döngüde tek tek execute çağırmaktan hem kısa hem hızlıdır.

Row factory: satırı sözlük gibi okumak

r[2] yazıp "üçüncü sütun hangisiydi" diye düşünmek hata kaynağıdır. con.row_factory = sqlite3.Row dedikten sonra her satır hem indeksle hem sütun adıyla okunur; dict(satir) ile gerçek sözlüğe çevrilir ve JSON'a gitmeye hazır olur.

import json
import sqlite3

con = sqlite3.connect(":memory:")
con.row_factory = sqlite3.Row
con.execute("CREATE TABLE siparis (id INTEGER PRIMARY KEY, sehir TEXT, tutar REAL)")
con.executemany("INSERT INTO siparis (sehir, tutar) VALUES (?, ?)", [
    ("Ankara", 1250.0), ("İzmir", 480.5), ("Ankara", 300.0), ("Bursa", 920.0), ("İzmir", 150.0)
])

for row in con.execute("SELECT sehir, COUNT(*) AS adet, SUM(tutar) AS toplam FROM siparis GROUP BY sehir ORDER BY toplam DESC"):
    print(row["sehir"], row["adet"], row["toplam"])

ilk = con.execute("SELECT * FROM siparis WHERE id = ?", (1,)).fetchone()
print("sütunlar:", ilk.keys())
print("sözlük:", dict(ilk))
print(json.dumps([dict(r) for r in con.execute("SELECT id, sehir FROM siparis WHERE sehir = 'İzmir'")], ensure_ascii=False))

GROUP BY, SUM ve ORDER BY işini SQL yaptı; Python yalnızca sonucu dolaştı. Bu iş bölümü dersin sonundaki "köprü" bölümünün özeti.

Transaction: commit, rollback, bağlam yöneticisi

Ilgaz Bank'ta bir havale iki UPDATE'tir: gönderenden düş, alıcıya ekle. İlki olup ikincisi olmazsa para buharlaşır. Transaction (işlem bütünlüğü) tam bunu çözer: bir grup değişiklik ya hep birlikte kalıcı olur (commit) ya da hep birlikte geri alınır (rollback). sqlite3'te bağlantı nesnesi with ile kullanılınca blok hatasız biterse otomatik commit, istisna çıkarsa otomatik rollback yapar. Dikkat: with con: bağlantıyı kapatmaz, yalnızca transaction'ı yönetir.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE hesap (no TEXT PRIMARY KEY, bakiye REAL NOT NULL CHECK (bakiye >= 0))")
con.executemany("INSERT INTO hesap VALUES (?, ?)", [("TR01", 1000.0), ("TR02", 200.0)])
con.commit()

def havale(gonderen: str, alici: str, tutar: float) -> bool:
    try:
        with con:
            con.execute("UPDATE hesap SET bakiye = bakiye - ? WHERE no = ?", (tutar, gonderen))
            con.execute("UPDATE hesap SET bakiye = bakiye + ? WHERE no = ?", (tutar, alici))
        return True
    except sqlite3.IntegrityError as e:
        print("geri alındı:", e)
        return False

def bakiyeler():
    return dict(con.execute("SELECT no, bakiye FROM hesap ORDER BY no").fetchall())

print("başlangıç:", bakiyeler())
print("300 havale:", havale("TR01", "TR02", 300.0), bakiyeler())
print("5000 havale:", havale("TR01", "TR02", 5000.0), bakiyeler())

İkinci havalede ilk UPDATE bakiyeyi eksiye düşürmeye çalışınca CHECK kısıtı IntegrityError fırlattı; with con bloğu bu istisnayı görüp her şeyi geri aldı. Bakiyeler ilk havaleden sonraki halinde kaldı. Kısıtı veritabanına yazmak, Python tarafındaki if kontrolünü unutsan bile veriyi korur.

Bağlantıyı kapatmayı unutmaDosya tabanlı veritabanında işin bitince con.close() çağır; en temizi bağlantıyı contextlib.closing(sqlite3.connect(...)) ile açmak ya da sınıfına __enter__/__exit__ yazmaktır. Bellek içi veritabanında kapatma, veriyi de siler.
Tür eşlemesiSQLite'ın INTEGER, REAL, TEXT, BLOB türleri sırasıyla int, float, str, bytes olarak gelir. Tarih tipi yoktur: tarihleri ISO metni ("2024-03-15") olarak sakla, karşılaştırma yine doğru çalışır. NULL ise Python'da None olur.

Kesit SQL'e köprü: SQL ne yapar, Python ne yapar?

İki araç rakip değil, iş arkadaşı. Küme işlemleri (filtre, gruplama, birleştirme, sıralama) veritabanı motorunun uzmanlık alanıdır; milyon satırda Python döngüsünden kat kat hızlıdır ve veriyi belleğe taşımaz. Python ise SQL'in beceremediği yerde devreye girer: karmaşık iş kuralları, dış sistemlerle konuşma, rapor biçimleme, test.

İşSQL'e bırakPython'a bırak
Şehre göre toplam ciroGROUP BY sehir, SUM(tutar)Sonucu tabloya/JSON'a dök
Son 30 günün siparişleriWHERE tarih >= ?Tarih aralığını hesapla, parametre olarak ver
Şüpheli işlem kuralı (çok adımlı)Ham satırları getirKuralı fonksiyonla uygula, test et
CSV'den yüklemeINSERT (executemany)CSV'yi oku, temizle, türleri çevir
Müşteriye e-postaAlıcı listesini seçŞablon doldur, gönder

Pratik kural: "Veriyi azaltan iş SQL'de, veriyi anlamlandıran iş Python'da." Bir SELECT'in döndürdüğü satır sayısı ne kadar azsa, Python tarafı o kadar hızlı ve basit olur.

Derinleş

Derinleş: sqlite3 ile gerçek uygulama kalıpları

Tablo kurmayı, parametreli sorguyu ve transaction'ı öğrendin. Gerçek bir stok ya da müşteri uygulamasında ise şu sorular gelir: "Eklediğim satırın id'si ne?", "Ürün varsa stoğu artır, yoksa ekle", "Kaç satır güncellendi?", "Sipariş silinmiş müşteriye bağlı kalmasın", "Liste uzunluğu belli olmayan IN sorgusu nasıl yazılır?", "Sorgu neden yavaş?". Bu bölüm bunları yanıtlar. SQL dilinin kendisi (JOIN, GROUP BY, pencere fonksiyonları) için Kesit SQL'e; ORM için sqlalchemy-orm, DataFrame'e okumak için pandas-giris dersine bakabilirsin.

İmlecin verdiği bilgiler: lastrowid, rowcount, fetchmany

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE musteri (id INTEGER PRIMARY KEY, ad TEXT, sehir TEXT)")
cur = con.execute("INSERT INTO musteri (ad, sehir) VALUES (?, ?)", ("Ayşe", "İzmir"))
print("yeni id:", cur.lastrowid)
con.executemany("INSERT INTO musteri (ad, sehir) VALUES (?, ?)", [("Can", "Van"), ("Ece", "Van"), ("Ali", "Bursa")])

cur = con.execute("UPDATE musteri SET sehir = ? WHERE sehir = ?", ("Van Merkez", "Van"))
print("güncellenen satır:", cur.rowcount)

cur = con.execute("SELECT id, ad FROM musteri ORDER BY id")
while parca := cur.fetchmany(2):      # büyük sonuçları parça parça çek
    print("parça:", parca)
print("sütunlar:", [k[0] for k in con.execute("SELECT * FROM musteri").description])

UPSERT: varsa güncelle, yoksa ekle

Depodan gelen stok dosyasında bazı ürünler yeni, bazıları mevcut. Önce SELECT ile bakıp sonra INSERT ya da UPDATE yazmak hem yavaş hem de iki işlem arasında yarış durumuna (race condition) açıktır. SQLite 3.24+ INSERT ... ON CONFLICT ... DO UPDATE sözdizimini destekler; excluded eklenmeye çalışılan satırı temsil eder.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE stok (sku TEXT PRIMARY KEY, ad TEXT, adet INTEGER NOT NULL)")
con.execute("INSERT INTO stok VALUES ('LP-1', 'Laptop', 5)")

gelen = [("LP-1", "Laptop", 3), ("MS-7", "Mouse", 40)]
with con:
    con.executemany("""
        INSERT INTO stok (sku, ad, adet) VALUES (?, ?, ?)
        ON CONFLICT(sku) DO UPDATE SET adet = adet + excluded.adet
    """, gelen)
print(con.execute("SELECT * FROM stok ORDER BY sku").fetchall())

# Yalnızca yoksa ekle, varsa dokunma:
con.execute("INSERT OR IGNORE INTO stok VALUES ('MS-7', 'Mouse', 999)")
print(con.execute("SELECT adet FROM stok WHERE sku = 'MS-7'").fetchone()[0])
INSERT OR REPLACE tuzağıOR REPLACE çakışan satırı silip yeniden ekler: belirtmediğin sütunlar varsayılana döner ve yabancı anahtarla bağlı satırlar etkilenebilir. Güncelleme niyetin varsa ON CONFLICT DO UPDATE kullan.

Yabancı anahtar, kısıt ve IntegrityError

SQLite geriye uyumluluk için yabancı anahtar (foreign key) denetimini varsayılan olarak kapalı tutar. Her bağlantıda PRAGMA foreign_keys = ON çalıştırmazsan var olmayan müşteriye sipariş yazabilirsin. Kısıt ihlallerinin hepsi sqlite3.IntegrityError olarak gelir; yakalayıp anlamlı mesaja çevir.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("PRAGMA foreign_keys = ON")
con.executescript("""
    CREATE TABLE musteri (id INTEGER PRIMARY KEY, eposta TEXT UNIQUE NOT NULL);
    CREATE TABLE siparis (
        id INTEGER PRIMARY KEY,
        musteri_id INTEGER NOT NULL REFERENCES musteri(id) ON DELETE CASCADE,
        tutar REAL CHECK (tutar > 0)
    );
    INSERT INTO musteri (eposta) VALUES ('ayse@ornek.com');
    INSERT INTO siparis (musteri_id, tutar) VALUES (1, 120);
""")
for sql, p in [("INSERT INTO musteri (eposta) VALUES (?)", ("ayse@ornek.com",)),
               ("INSERT INTO siparis (musteri_id, tutar) VALUES (?, ?)", (99, 10)),
               ("INSERT INTO siparis (musteri_id, tutar) VALUES (?, ?)", (1, -5))]:
    try:
        con.execute(sql, p)
    except sqlite3.IntegrityError as e:
        print("Reddedildi:", e)
con.execute("DELETE FROM musteri WHERE id = 1")
print("kalan sipariş:", con.execute("SELECT COUNT(*) FROM siparis").fetchone()[0])

Dinamik IN listesi, indeks ve sorgu planı

Kullanıcı birden çok şehir seçtiğinde IN (?, ?, ?) yer tutucularını listenin uzunluğu kadar üretirsin; değerleri yine parametre olarak verirsin. Sık filtrelenen sütuna indeks eklemek sorguyu tam tablo taramasından (SCAN) indeks aramasına (SEARCH) çevirir; EXPLAIN QUERY PLAN bunu gösterir.

import sqlite3

con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE siparis (id INTEGER PRIMARY KEY, sehir TEXT, tutar REAL)")
con.executemany("INSERT INTO siparis (sehir, tutar) VALUES (?, ?)",
                [("Ankara", 10), ("Van", 20), ("İzmir", 30), ("Ankara", 5)])

secilen = ["Ankara", "İzmir"]
yer = ", ".join("?" * len(secilen))
sql = f"SELECT sehir, SUM(tutar) FROM siparis WHERE sehir IN ({yer}) GROUP BY sehir ORDER BY sehir"
print(sql)
print(con.execute(sql, secilen).fetchall())

plan = lambda: con.execute("EXPLAIN QUERY PLAN SELECT * FROM siparis WHERE sehir = ?", ("Van",)).fetchall()[0][-1]
print("indeks yok:", plan())
con.execute("CREATE INDEX ix_siparis_sehir ON siparis(sehir)")
print("indeks var:", plan())

# Python fonksiyonunu SQL içinde kullan
con.create_function("kdvli", 1, lambda t: round(t * 1.2, 2), deterministic=True)
print(con.execute("SELECT id, kdvli(tutar) FROM siparis ORDER BY id LIMIT 2").fetchall())
f-string burada neden güvenli?f-string yalnızca ?, ? yer tutucularını üretir; kullanıcı değerleri hâlâ ayrı parametre listesindedir. Kural: SQL metnine yalnızca senin kontrolündeki parçalar (yer tutucu, beyaz listeden seçilmiş sütun adı) girer, kullanıcı verisi asla.
AraçNe işe yararNot
cur.lastrowidSon eklenen satırın id'siexecutemany sonrası güvenilmez
cur.rowcountUPDATE/DELETE'in etkilediği satırSELECT'te -1
fetchone / fetchmany / fetchallTek, parça, tümüBüyük sonuçta fetchmany ya da imleçte döngü
executescriptÇok cümleli SQL (şema)Parametre almaz, önce COMMIT yapar
ON CONFLICT DO UPDATEUPSERTÇakışma hedefi UNIQUE/PK olmalı
PRAGMA foreign_keys = ONYabancı anahtar denetimiHer bağlantıda tekrar
PRAGMA journal_mode = WALOkuyucular yazarı beklemezDosya veritabanında
create_functionPython fonksiyonunu SQL'de kullandeterministic=True indekslenebilir
con.backup(hedef)Canlı veritabanını kopyalaDosyayı elle kopyalamaktan güvenli

Tarihleri saklamak ve dosya veritabanı

SQLite'ın ayrı bir tarih tipi yoktur. Tarihi ISO 8601 metni (2026-03-15, 2026-03-15T14:30:00) olarak sakla: alfabetik sıralama kronolojik sıralamayla aynı olur ve date(), strftime() SQL fonksiyonları çalışır. Python 3.12'de sqlite3'ün varsayılan tarih dönüştürücüleri kullanımdan kaldırıldı (deprecated); dönüşümü kendin açıkça yap. Kendi bilgisayarında gerçek dosyayla çalışırken şu iskeleti kullan:

import sqlite3
from contextlib import closing

with closing(sqlite3.connect("anadolu.db", timeout=10)) as con:
    con.execute("PRAGMA foreign_keys = ON")
    con.execute("PRAGMA journal_mode = WAL")
    with con:                       # transaction: başarıda commit, hatada rollback
        con.execute("INSERT INTO log (zaman, mesaj) VALUES (?, ?)", ("2026-03-15T09:00:00", "başladı"))
    with closing(sqlite3.connect("yedek.db")) as hedef:
        con.backup(hedef)

Komut satırından incelemek için sqlite3 anadolu.db (Windows'ta sqlite.org'dan sqlite-tools indir), içinde .tables, .schema siparis, .mode box komutlarını kullan. Görsel araç istersen "DB Browser for SQLite" ücretsizdir. database is locked hatası alırsan başka bir program (ya da kapanmamış bir bağlantı) yazma kilidini tutuyordur: timeout ver, bağlantıları closing ile kapat, WAL modunu aç.

Gerçek işteSQLite telefonlarda, tarayıcılarda, masaüstü uygulamalarında ve test ortamlarında her yerde. Anadolu Market'in depo el terminalleri çevrimdışı çalışırken taramaları yerel SQLite'a UPSERT ile yazar, bağlantı gelince merkeze aktarır. Ekipler ayrıca birim testlerde üretim PostgreSQL'i yerine :memory: SQLite kullanır; ama dikkat: tip denetimi ve bazı SQL özellikleri farklıdır, kritik sorguları gerçek veritabanında da test et.
Derinleş

Derinleş: pencere fonksiyonları ve CTE ile analitik SQL

Derste GROUP BY ile özet çıkarmayı gördün. GROUP BY satırları daraltır: 910 sipariş, kanal başına 3 satıra iner. Analist sorularının çoğu ise satırları koruyup yanına bir özet eklemeyi ister: "her siparişin, müşterinin kaçıncı siparişi olduğu", "her ayın bir önceki aya göre değişimi", "her şehirde en çok harcayan müşteri". Bunun aracı pencere fonksiyonlarıdır (window functions). Uzun sorguları okunur adımlara bölmek için de CTE (common table expression, WITH ... AS) kullanılır. SQLite 3.25'ten beri ikisini de destekler; tarayıcıdaki ve Python'la gelen sqlite3 bu sürümlerin çok üstündedir. Bu bölümde gerçek Anadolu Market verisini veri.baglan ile yükleyeceğiz. Konunun tam hâli Kesit SQL'in analitik SQL derslerindedir; burada Python tarafından nasıl kullanılacağına odaklanıyoruz.

CTE: sorguyu adımlara bölmek

Sipariş tutarı kalemlerden hesaplanır. Bunu her sorguda alt sorgu olarak tekrar yazmak yerine bir kez sip adlı CTE olarak tanımlarız; sonraki adımlar onu tablo gibi kullanır. Birden çok CTE virgülle zincirlenir ve her biri öncekileri görebilir.

import veri

con = veri.baglan("anadolu_market", tablolar=["orders", "order_items", "customers", "products"])
SQL = """
WITH sip AS (
    SELECT o.order_id, o.customer_id, o.order_date, o.channel,
           SUM(oi.quantity * oi.unit_price * (1 - oi.discount)) AS tutar
    FROM orders o JOIN order_items oi ON oi.order_id = o.order_id
    WHERE o.status = 'delivered'
    GROUP BY o.order_id
),
aylik AS (
    SELECT substr(order_date, 1, 7) AS ay, ROUND(SUM(tutar), 2) AS ciro
    FROM sip WHERE order_date >= '2024-01-01'
    GROUP BY ay
)
SELECT ay, ciro FROM aylik ORDER BY ciro DESC LIMIT 3
"""
for satir in con.execute(SQL):
    print(satir)

Pencere fonksiyonunun anatomisi

fonksiyon() OVER (PARTITION BY ... ORDER BY ... çerçeve). PARTITION BY pencereyi gruplara böler (GROUP BY gibi ama satırları daraltmadan), ORDER BY pencere içindeki sırayı, çerçeve (ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) ise hesaba hangi satırların gireceğini belirler.

FonksiyonNe verirTipik soru
ROW_NUMBER()1, 2, 3... (eşitlikte de farklı)Müşterinin ilk siparişi, her gruptan tek satır
RANK() / DENSE_RANK()Eşitlere aynı sıra; RANK sonra boşluk bırakır (5, 5, 7), DENSE_RANK bırakmaz (5, 5, 6)Satış liderleri tablosu
LAG(x) / LEAD(x)Önceki / sonraki satırın değeriAydan aya değişim, siparişler arası gün
SUM(x) OVER (ORDER BY ...)Kümülatif toplamYıl başından bugüne ciro, Pareto
AVG(x) OVER (... ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)Hareketli ortalama3 aylık trend
SUM(x) OVER ()Tüm tablonun toplamı her satırdaPay (yüzde) hesabı
import veri

con = veri.baglan("anadolu_market", tablolar=["orders", "order_items", "customers", "products"])
SQL = """
WITH sip AS (
    SELECT o.order_id, o.customer_id, o.order_date, o.channel,
           SUM(oi.quantity * oi.unit_price * (1 - oi.discount)) AS tutar
    FROM orders o JOIN order_items oi ON oi.order_id = o.order_id
    WHERE o.status = 'delivered'
    GROUP BY o.order_id
),
aylik AS (
    SELECT substr(order_date, 1, 7) AS ay, SUM(tutar) AS ciro
    FROM sip WHERE order_date >= '2024-01-01' GROUP BY ay
)
SELECT ay, ROUND(ciro, 0) AS ciro,
       ROUND(100.0 * (ciro - LAG(ciro) OVER (ORDER BY ay)) / LAG(ciro) OVER (ORDER BY ay), 1) AS degisim,
       ROUND(AVG(ciro) OVER (ORDER BY ay ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 0) AS ort3
FROM aylik ORDER BY ay
"""
for satir in con.execute(SQL).fetchall()[-4:]:
    print(satir)

Grup içinde ilk N: CTE + ROW_NUMBER

Pencere fonksiyonları WHERE'den sonra hesaplanır; bu yüzden WHERE sira <= 3 aynı SELECT içinde yazılamaz. Kalıp her zaman aynıdır: sırayı bir CTE'de hesapla, dış sorguda filtrele.

import veri

con = veri.baglan("anadolu_market", tablolar=["orders", "order_items", "customers", "products"])
SQL = """
WITH sip AS (
    SELECT o.order_id, o.customer_id, o.order_date, o.channel,
           SUM(oi.quantity * oi.unit_price * (1 - oi.discount)) AS tutar
    FROM orders o JOIN order_items oi ON oi.order_id = o.order_id
    WHERE o.status = 'delivered'
    GROUP BY o.order_id
),
siralama AS (
    SELECT c.city, c.customer_id, ROUND(SUM(s.tutar), 2) AS harcama,
           ROW_NUMBER() OVER (PARTITION BY c.city ORDER BY SUM(s.tutar) DESC) AS sira
    FROM sip s JOIN customers c ON c.customer_id = s.customer_id
    WHERE c.city IS NOT NULL
    GROUP BY c.city, c.customer_id
)
SELECT city, customer_id, harcama FROM siralama WHERE sira = 1
ORDER BY harcama DESC LIMIT 4
"""
print(con.execute(SQL).fetchall())
SQL mi pandas mı?Aynı hesapları pandas'ta groupby().cumcount(), shift(), rolling() ve rank() ile yaparsın. Veri bir veritabanında duruyorsa hesabı SQL'de yapıp yalnızca sonucu Python'a çekmek, milyonlarca satırı ağdan taşımaktan çok daha hızlıdır. Python'un işi burada sorguyu parametreyle çalıştırmak, sonucu test etmek ve raporlamaktır.
Gerçek işteAnalist mülakatlarının SQL bölümü büyük ölçüde bu kalıplardan oluşur: "her departmanda en yüksek maaşlı 3 kişi", "aydan aya büyüme", "ardışık günler", "kümülatif pay". Üretimde ise bu sorgular bir .sql dosyasında durur; Python betiği dosyayı okuyup con.execute(sql, parametreler) ile çalıştırır ve sonucu testle doğrular.
Uzmanlaş

Üretimde sqlite3: kalıplar ve sınırlar

Bir veri katmanı (data layer) yazarken sorguları uygulamanın her yerine dağıtma; bir Depo sınıfında topla. Sınıfın kurucusu bağlantıyı alır, metotları (ekle, sehre_gore_toplam) SQL'i saklar, geri kalan kod SQL görmez. Böylece bir gün PostgreSQL'e geçersen yalnızca bu sınıfı değiştirirsin. Testlerde ise aynı sınıfa ":memory:" bağlantısı verirsin; her test temiz bir veritabanıyla başlar, dosya kirliliği olmaz.

Şema değişiklikleri (migration) için CREATE TABLE IF NOT EXISTS başlangıçtır; büyüyen projede PRAGMA user_version ile şema sürümü tutup sırayla ALTER TABLE betikleri çalıştırırsın. Performans için iki not: toplu eklemede her satırdan sonra commit yapma, executemany + tek commit kullan; sık filtrelenen sütuna CREATE INDEX ekle. SQLite'ın sınırı eşzamanlı yazmadır: aynı anda tek yazar kabul eder. Çok kullanıcılı web uygulaması ya da gigabaytlarca büyüyen veri için PostgreSQL gibi sunucu tabanlı bir motora geçersin; ama Python tarafındaki kalıp (bağlantı, parametreli sorgu, transaction) birebir aynı kalır, çünkü hepsi DB-API 2.0 standardını izler.

Alıştırmalara geç (28)