← Torna al blog
·8 min di lettura·

Cinque pattern SQL che sostituiscono un loop Python — e perché vincono

Il Python più caro che rivedo è for order in orders dopo un SELECT *. Ultima riga per gruppo, mese su mese, conteggi filtrati e upsert sono query. Come le scrivo in Postgres invece di scaricare la tabella.

SQLPostgreSQLPythonPerformanceIngegneria

Il Python più caro che rivedo non è l’handler FastAPI senza tipi. È il for order in orders che esiste perché nessuno ha scritto una funzione finestra. Il loop sembra onesto. È anche così che un report di quattro secondi diventa quaranta, poi un timeout il lunedì dopo un buon weekend di vendite.

Ho già scritto come scelgo un tipo di database. Questo è lo strato dopo: l’SQL che scrivo davvero quando la domanda di prodotto è «ultimo ordine per cliente», «ricavi di questo mese contro il precedente», o «inserisci la prenotazione se lo slot è libero». Postgres risponde in un andata e ritorno. Python anche — dopo aver scaricato la tabella. L’articolo gemello: come quella query deve uscire da Python senza riportare l’heap.

1. Il loop che scarica la tabella

Uno script tipico: SELECT * FROM orders, poi raggruppare in un dict, poi il created_at più recente per customer_id. Su 800 righe sembra a posto. Su 800.000 è un ticket di supporto. Il database ha già indice, ordinamento e memoria per questo. Lo avete pagato. Usare Python come secondo motore di query è come il costo esce in latenza, non su una fattura prevista.

  • Se il risultato è più piccolo della fonte e la regola ha un nome — last, sum, rank, exists — è una query.
  • Se la regola è un saggio di business — rimborsi con eccezione umana, un paragrafo di policy — può restare in Python.

2. Ultima riga per gruppo — DISTINCT ON e ROW_NUMBER

Abbonamento corrente per workspace. Ultimo stato fattura per ordine. L’autista che ha pingato per ultimo. È il primo loop che cancello. In Postgres scrivo DISTINCT ON (workspace_id) e ORDER BY workspace_id, created_at DESC. Il cugino portabile è ROW_NUMBER() OVER (PARTITION BY workspace_id ORDER BY created_at DESC), poi tenere rn = 1.

DISTINCT ON è più corto e lo uso quando Postgres è mio. ROW_NUMBER lo scrivo quando la stessa query deve sopravvivere a un trasloco, o quando servono rango 1 e 2 — il piano precedente, il secondo. Non scaricate ogni abbonamento per ridurre in Python. Sbaglierete i pareggi e caricherete colonne che non disegnate mai.

3. Totali progressivi e «dal mese scorso» — SUM OVER e LAG

Mese su mese non è un join della tabella su se stessa in un loop. LAG(revenue) OVER (PARTITION BY workspace_id ORDER BY month) mette il mese scorso sulla stessa riga. SUM(amount) OVER (PARTITION BY customer_id ORDER BY created_at) è un saldo progressivo. Le funzioni finestra tengono il grano della riga e vedono comunque il vicinato. GROUP BY comprime. Un loop Python ricostruisce quel vicinato in un dict e dimentica i fusi.

Se vi servono la riga di dettaglio e il totale del gruppo, SUM(amount) OVER (PARTITION BY order_id) batte una seconda query e batte l’N+1 dell’ORM. Tutto il trucco: tenere il grano che disegnate, calcolare il vicinato nel motore.

4. Conteggi condizionali — FILTER invece di if

COUNT(*) FILTER (WHERE status = paid) accanto a COUNT(*) FILTER (WHERE status = refunded) è una scansione. La versione Python è un passaggio e due contatori — finché qualcuno non aggiunge un terzo stato in un secondo loop, o l’export cresce e scansionate due volte. FILTER si legge. Un CASE dentro SUM gira sui motori senza FILTER. In Postgres preferisco FILTER perché l’intento sta sulla pagina: questo conteggio ha un predicato.

5. Upsert invece di leggi-poi-inserisci

La corsa: leggere «questa email è libera», poi inserire. Due schede, due worker, un unique che scoprite in produzione. INSERT ... ON CONFLICT (email) DO UPDATE — o DO NOTHING — è la transazione che il database sa già eseguire. Stessa famiglia: UPDATE ... WHERE version = $version per un lock ottimistico. Check-then-write in Python è un database peggiore scritto a mano.

  • Slot di prenotazione, chiavi di idempotenza, «crea il cliente se manca» — sono upsert, non blocchi if.
  • Un indice unique è parte del prodotto. Senza, ON CONFLICT è teatro.

6. Come controllo di non aver inventato una query più lenta

EXPLAIN (ANALYZE, BUFFERS) sul percorso lento. Se vedete una sequential scan su una tabella cresciuta, vi serviva un indice allineato a partizione e ordine — (workspace_id, created_at DESC) per DISTINCT ON. Una funzione finestra non è gratis. Resta più economica che spedire l’heap all’app. Se il piano ordina due milioni di righe per una dashboard che ne mostra venti, aggiungete un vincolo di data. L’SQL che sostituisce un loop può ancora essere un loop cattivo dentro il motore.

Conclusione: nominate il result set, scrivete la query

Python è un buon posto per HTTP, le policy e la frase che vede l’utente. Un posto povero per riscoprire GROUP BY. Cinque forme — last-per-group, lag, somma progressiva, aggregati filtrati, upsert — e la maggior parte degli script «furbi» che rivedo diventano una vista o una sola query in una funzione di repository. L’altro articolo è il Python che rifiuta di disfare quel lavoro.

Parliamo del tuo progetto?

Sono un’ingegnera web senior, specializzata in React e Next.js - disponibile per progetti freelance in tutto il mondo.

Dove sono

Kyiv, Ucraina

Telegram

Contattami

WhatsApp

Contattami