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])
(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.
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.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ırak | Python'a bırak |
|---|---|---|
| Şehre göre toplam ciro | GROUP BY sehir, SUM(tutar) | Sonucu tabloya/JSON'a dök |
| Son 30 günün siparişleri | WHERE tarih >= ? | Tarih aralığını hesapla, parametre olarak ver |
| Şüpheli işlem kuralı (çok adımlı) | Ham satırları getir | Kuralı fonksiyonla uygula, test et |
| CSV'den yükleme | INSERT (executemany) | CSV'yi oku, temizle, türleri çevir |
| Müşteriye e-posta | Alı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ş: 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])
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())
?, ? 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 yarar | Not |
|---|---|---|
cur.lastrowid | Son eklenen satırın id'si | executemany sonrası güvenilmez |
cur.rowcount | UPDATE/DELETE'in etkilediği satır | SELECT'te -1 |
fetchone / fetchmany / fetchall | Tek, 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 UPDATE | UPSERT | Çakışma hedefi UNIQUE/PK olmalı |
PRAGMA foreign_keys = ON | Yabancı anahtar denetimi | Her bağlantıda tekrar |
PRAGMA journal_mode = WAL | Okuyucular yazarı beklemez | Dosya veritabanında |
create_function | Python fonksiyonunu SQL'de kullan | deterministic=True indekslenebilir |
con.backup(hedef) | Canlı veritabanını kopyala | Dosyayı 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ç.
: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ş: 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.
| Fonksiyon | Ne verir | Tipik 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ğeri | Aydan aya değişim, siparişler arası gün |
SUM(x) OVER (ORDER BY ...) | Kümülatif toplam | Yıl başından bugüne ciro, Pareto |
AVG(x) OVER (... ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) | Hareketli ortalama | 3 aylık trend |
SUM(x) OVER () | Tüm tablonun toplamı her satırda | Pay (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())
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..sql dosyasında durur; Python betiği dosyayı okuyup con.execute(sql, parametreler) ile çalıştırır ve sonucu testle doğrular.Ü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.