← Zurück zum Blog
·8 Min. Lesezeit·

Fünf SQL-Muster, die eine Python-Schleife ersetzen — und warum sie gewinnen

Das teuerste Python, das ich review, ist for order in orders nach einem SELECT *. Letzte Zeile pro Gruppe, Monat zu Monat, gefilterte Zähler und Upserts sind Queries. Wie ich sie in Postgres schreibe, statt die Tabelle zu laden.

SQLPostgreSQLPythonPerformanceEngineering

Das teuerste Python, das ich review, ist nicht der untypisierte FastAPI-Handler. Es ist das for order in orders, das existiert, weil niemand eine Fensterfunktion geschrieben hat. Die Schleife wirkt ehrlich. So wird aus einem Report, der vier Sekunden brauchte, vierzig — und montags nach einem guten Verkaufswochenende ein Timeout.

Ich habe schon geschrieben, wie ich einen Datenbanktyp wähle. Hier die nächste Schicht: das SQL, das ich wirklich schreibe, wenn die Produktfrage „letzte Bestellung pro Kunde“, „Umsatz diesen Monat gegen letzten“ oder „Buchung einfügen, wenn der Slot frei ist“ lautet. Postgres antwortet in einem Roundtrip. Python auch — nachdem es die Tabelle geladen hat. Der Begleitartikel: wie diese Query Python verlassen soll, ohne den Heap zurückzuholen.

1. Die Schleife, die die Tabelle lädt

Ein typisches Skript: SELECT * FROM orders, dann gruppieren im Dict, dann das neueste created_at pro customer_id. Bei 800 Zeilen fühlt es sich gut an. Bei 800.000 ist es ein Support-Ticket. Die Datenbank hat Index, Sortierung und Speicher dafür schon. Das haben Sie bezahlt. Python als zweite Query-Engine — so erscheint die Rechnung in Latenz, nicht auf einer geplanten Invoice.

  • Ist das Ergebnis kleiner als die Quelle und die Regel hat einen Namen — last, sum, rank, exists — ist es eine Query.
  • Ist die Regel ein Business-Aufsatz — Erstattungen mit menschlicher Ausnahme, ein Policy-Absatz — darf sie in Python bleiben.

2. Letzte Zeile pro Gruppe — DISTINCT ON und ROW_NUMBER

Aktuelles Abo pro Workspace. Letzter Rechnungsstatus pro Bestellung. Der Fahrer mit dem letzten Ping. Das ist die erste Schleife, die ich lösche. In Postgres schreibe ich DISTINCT ON (workspace_id) und ORDER BY workspace_id, created_at DESC. Der portable Cousin ist ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at DESC), dann rn = 1 behalten.

DISTINCT ON ist kürzer, und ich nutze es, wenn Postgres mir gehört. ROW_NUMBER schreibe ich, wenn dieselbe Query einen Umzug überleben muss, oder wenn ich Rang 1 und 2 brauche — den vorigen Plan, den Zweitplatzierten. Holen Sie nicht jedes Abo und reduzieren Sie in Python. Bei Gleichstand liegen Sie falsch, und Sie laden Spalten, die Sie nie zeichnen.

3. Laufende Summen und „seit letztem Monat“ — SUM OVER und LAG

Monat zu Monat ist kein Join einer Tabelle auf sich selbst in einer Schleife. LAG(revenue) OVER (PARTITION BY workspace_id ORDER BY month) legt den Vormonat in dieselbe Zeile. SUM(amount) OVER (PARTITION BY customer_id ORDER BY created_at) ist ein laufender Saldo. Fensterfunktionen behalten die Zeilenkörnung und sehen trotzdem die Nachbarschaft. GROUP BY fällt zusammen. Eine Python-Schleife baut die Nachbarschaft im Dict und vergisst Zeitzonen.

Brauchen Sie die Detailzeile und die Gruppensumme, schlägt SUM(amount) OVER (PARTITION BY order_id) eine zweite Query und schlägt N+1 aus dem ORM. Der ganze Trick: die Körnung behalten, die Sie zeichnen, die Nachbarschaft in der Engine rechnen.

4. Bedingte Zähler — FILTER statt if

COUNT(*) FILTER (WHERE status = paid) neben COUNT(*) FILTER (WHERE status = refunded) ist ein Scan. Die Python-Version ist ein Durchlauf mit zwei Zählern — bis jemand einen dritten Status in einer zweiten Schleife ergänzt, oder der Export wächst und Sie zweimal scannen. FILTER ist lesbar. CASE in SUM läuft auf Engines ohne FILTER. In Postgres will ich FILTER, weil die Absicht auf der Seite steht: dieser Zähler hat ein Prädikat.

5. Upsert statt erst lesen, dann einfügen

Das Rennen: lesen „ist diese E-Mail frei“, dann einfügen. Zwei Tabs, zwei Worker, ein Unique, den Sie in Produktion entdecken. INSERT ... ON CONFLICT (email) DO UPDATE — oder DO NOTHING — ist die Transaktion, die die Datenbank schon kann. Dieselbe Familie: UPDATE ... WHERE version = $version für einen optimistic Lock. Check-then-write in Python ist eine schlechtere Datenbank von Hand.

  • Buchungsslots, Idempotenz-Keys, „Kunde anlegen wenn fehlend“ — das sind Upserts, keine If-Blöcke.
  • Ein Unique-Index ist Teil des Produkts. Ohne ihn ist ON CONFLICT Theater.

6. Wie ich prüfe, dass ich keine langsamere Query erfunden habe

EXPLAIN (ANALYZE, BUFFERS) auf dem langsamen Pfad. Sehen Sie einen Sequential Scan auf einer erwachsenen Tabelle, brauchten Sie einen Index zu Partition und Order — (workspace_id, created_at DESC) für DISTINCT ON. Eine Fensterfunktion ist nicht gratis. Sie ist trotzdem billiger, als den Heap an die App zu schicken. Sortiert der Plan zwei Millionen Zeilen für ein Dashboard mit zwanzig, setzen Sie eine Datumsgrenze. SQL, das eine Schleife ersetzt, kann innen in der Engine immer noch eine schlechte Schleife sein.

Fazit: das Resultset benennen, die Query schreiben

Python ist ein guter Ort für HTTP, Policy und den Satz, den der Nutzer sieht. Ein schlechter Ort, um GROUP BY neu zu erfinden. Fünf Formen — last-per-group, Lag, laufende Summe, gefilterte Aggregate, Upsert — und die meisten „cleveren“ Skripte, die ich review, werden eine View oder eine Query in einer Repository-Funktion. Der andere Artikel ist das Python, das diese Arbeit nicht rückgängig macht.

Sprechen wir über Ihr Projekt

Ich bin Senior-Webentwicklerin mit Schwerpunkt React und Next.js - verfügbar für Freelance-Projekte weltweit.