← Bloga dön
·8 dk okuma·

Bir Python döngüsünün yerini alan beş SQL kalıbı — ve neden kazanırlar

İncelediğim en pahalı Python, bir SELECT * sonrası for order in orders. Grupta son satır, aydan aya, süzülmüş sayımlar ve upsert’ler sorgudur. Tabloyu indirmek yerine bunları Postgres’te nasıl yazarım.

SQLPostgreSQLPythonPerformansMühendislik

İncelediğim en pahalı Python, tipsiz FastAPI handler’ı değil. Kimse pencere fonksiyonu yazmadığı için duran for order in orders. Döngü dürüst durur. Dört saniyelik raporun kırk olması, iyi bir satış hafta sonundan sonra pazartesi timeout’a dönmesi de böyle olur.

Bir veritabanı türünü nasıl seçtiğimi zaten yazdım. Burası sonraki katman: ürün sorusu «müşteri başına son sipariş», «bu ayın geliri geçen aya karşı» veya «slot boşsa rezervasyonu yaz» olduğunda gerçekten yazdığım SQL. Postgres bunları bir gidiş-dönüşte yanıtlar. Python da yanıtlar — tabloyu indirdikten sonra. Eş yazı: bu sorgunun yığını geri çekmeden Python’dan nasıl çıkması.

1. Tabloyu indiren döngü

Tipik bir betik: SELECT * FROM orders, sonra bir dict’te grupla, sonra customer_id başına en yeni created_at. 800 satırda iyi durur. 800.000’de destek kaydıdır. Veritabanının bunun için indeksi, sıralaması ve belleği zaten var. Bunun parasını verdiniz. Python’u ikinci bir sorgu motoru yapmak, faturanın planladığınız kesimde değil gecikmede görünmesidir.

  • Sonuç kaynaktan küçükse ve kuralın bir adı varsa — last, sum, rank, exists — bu bir sorgudur.
  • Kural bir iş denemesiyse — insan istisnalı iade, bir politika paragrafı — Python’da kalabilir.

2. Grupta son satır — DISTINCT ON ve ROW_NUMBER

Çalışma alanı başına güncel abonelik. Sipariş başına son fatura durumu. Son ping atan sürücü. Sildiğim ilk döngü budur. Postgres’te DISTINCT ON (workspace_id) ve ORDER BY workspace_id, created_at DESC yazarım. Taşınabilir kuzen: ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at DESC), sonra rn = 1 kalsın.

DISTINCT ON daha kısadır; Postgres bana aitse onu kullanırım. Aynı sorgunun bir taşımaya dayanması gerekiyorsa, veya 1. ve 2. sıra lazımsa — önceki plan, ikinci — ROW_NUMBER yazarım. Tüm abonelikleri çekip Python’da indirmeyin. Beraberlikte yanılır, hiç çizmediğiniz sütunları da yüklersiniz.

3. Kümülatif toplam ve «geçen aydan beri» — SUM OVER ve LAG

Aydan aya, bir döngü içinde tablonun kendisine join’i değildir. LAG(revenue) OVER (PARTITION BY workspace_id ORDER BY month) geçen ayı aynı satıra koyar. SUM(amount) OVER (PARTITION BY customer_id ORDER BY created_at) kümülatif bakiyedir. Pencere fonksiyonları satır tanesini korur ve yine komşuluğu görür. GROUP BY çöker. Bir Python döngüsü o komşuluğu bir dict’te kurar ve saat dilimlerini unutur.

Hem ayrıntı satırı hem grup toplamı lazımsa, SUM(amount) OVER (PARTITION BY order_id) ikinci sorguyu ve ORM’den N+1’i yener. Bütün numara: çizeceğiniz taneyi tutmak, komşuluğu motorda hesaplamak.

4. Koşullu sayımlar — if yerine FILTER

COUNT(*) FILTER (WHERE status = paid) ile yanında COUNT(*) FILTER (WHERE status = refunded) tek taramadır. Python sürümü bir geçiş ve iki sayaçtır — biri ikinci döngüde üçüncü durum ekleyene, veya dışa aktarma büyüyüp iki kez tarayana kadar. FILTER okunur. SUM içindeki CASE, FILTER almamış motorlarda çalışır. Postgres’te FILTER isterim çünkü niyet sayfadadır: bu sayımın bir yüklemi vardır.

5. Önce oku sonra yaz yerine upsert

Yarış: «bu e-posta boş mu» oku, sonra yaz. İki sekme, iki worker, üretimde keşfettiğiniz bir unique. INSERT ... ON CONFLICT (email) DO UPDATE — veya DO NOTHING — veritabanının zaten bildiği işlemdir. Aynı aile: iyimser kilit için UPDATE ... WHERE version = $version. Python’da check-then-write, elle yazılmış daha kötü bir veritabanıdır.

  • Rezervasyon slotları, idempotency anahtarları, «yoksa müşteriyi oluştur» — bunlar if değil, upsert’tir.
  • Benzersiz indeks ürünün parçasıdır. O olmadan ON CONFLICT tiyatrodur.

6. Daha yavaş bir sorgu uydurmadığımı nasıl bakarım

Yavaş yolda EXPLAIN (ANALYZE, BUFFERS). Büyümüş bir tabloda sequential scan görüyorsanız, bölüm ve sıraya uyan bir indeks lazımdı — DISTINCT ON için (workspace_id, created_at DESC). Pencere fonksiyonu bedava değildir. Yığını uygulamaya göndermekten yine ucuzdur. Plan, yirmi satır gösteren bir pano için iki milyon satır sıralıyorsa tarih sınırı ekleyin. Döngünün yerini alan SQL, motorun içinde hâlâ kötü bir döngü olabilir.

Sonuç: sonuç kümesine ad verin, sorguyu yazın

Python; HTTP, politika ve kullanıcının gördüğü cümle için iyi bir yerdir. GROUP BY’ı yeniden keşfetmek için kötü bir yerdir. Beş biçim — grupta son, lag, kümülatif toplam, süzülmüş toplamlar, upsert — ve incelediğim «zeki» betiklerin çoğu bir görünüm veya depo fonksiyonunda tek sorgu olur. Diğer yazı, o işi geri almayı reddeden Python’dur.

Projenizi konuşalım mı?

React ve Next.js konusunda uzman kıdemli bir web geliştiriciyim - dünya çapında freelance projelere açığım.