where and in sql query

where and in sql query

Erano le tre del mattino di un martedì di novembre quando il database di produzione di un mio cliente, una piattaforma e-commerce con cinquemila ordini l'ora, ha smesso di rispondere. Il colpevole non era un attacco hacker né un bug nel codice di frontend, ma una singola Where And In SQL Query inserita in un report generato automaticamente che cercava di filtrare diecimila ID prodotto contemporaneamente. Mentre il server andava in timeout e il fatturato crollava di cinquemila euro ogni dieci minuti di inattività, il team cercava disperatamente di capire perché un comando apparentemente innocuo avesse saturato la memoria della macchina. Ho visto questa scena ripetersi in aziende di ogni dimensione, dai piccoli uffici tecnici alle multinazionali, dove lo sviluppatore di turno pensa che passare una lista infinita di valori a un operatore di confronto sia una soluzione rapida, senza rendersi conto che sta piazzando una mina sotto le fondamenta dell'infrastruttura.

L'illusione della semplicità con la Where And In SQL Query

Il primo errore che ho visto commettere migliaia di volte riguarda la gestione dei volumi. Molti programmatori alle prime armi pensano che inserire centinaia o migliaia di parametri dentro le parentesi tonde sia un modo efficiente per filtrare i dati. Non lo è. La verità è che i motori di database, che si tratti di PostgreSQL, SQL Server o Oracle, devono analizzare ogni singolo valore che passi loro. Se la lista diventa troppo lunga, il tempo speso dal parser per leggere la stringa del comando supera spesso il tempo necessario per l'estrazione fisica dei dati dal disco.

Ho lavorato su un sistema di logistica dove il software inviava liste di cinquemila codici di spedizione in un colpo solo. Il risultato? Piani di esecuzione totalmente sballati. Il database smetteva di usare gli indici perché riteneva più "economico" scansionare l'intera tabella piuttosto che cercare cinquemila valori sparsi. Quando la lista supera i cento o duecento elementi, stai già giocando col fuoco. Ogni volta che aggiungi un ID a quell'elenco, aumenti il rischio che il database scelga la strada più lenta per darti il risultato. Invece di risolvere il problema, stai creando un collo di bottiglia che si manifesterà solo quando il carico di lavoro sarà al massimo, ovvero nel peggior momento possibile.

Il limite invisibile della memoria

C'è un aspetto tecnico che quasi nessuno considera finché il server non va in kernel panic. Ogni parametro che passi occupa spazio nella memoria dedicata al parsing delle sessioni. Se hai cento utenti che eseguono contemporaneamente una ricerca basata su liste enormi, stai mangiando gigabyte di RAM solo per gestire il testo del comando inviato. Non si tratta solo di velocità, ma di stabilità dell'intero ecosistema. In contesti europei, dove spesso ci basiamo su infrastrutture cloud con risorse misurate per risparmiare sui costi fissi, questo spreco si traduce direttamente in bollette più salate a fine mese o in down improvvisi perché le istanze non riescono a scalare abbastanza velocemente.

Ignorare il potere delle tabelle temporanee

Un malinteso diffuso è che creare una tabella temporanea sia "troppo pesante" rispetto a un semplice filtro diretto. Questa è una sciocchezza che costa ore di debug. Quando hai bisogno di filtrare per un set di dati che proviene da un'altra sorgente, la strategia di iniettare i valori direttamente nel comando è la via più pigra e pericolosa.

Ho visto un caso reale in un'azienda finanziaria milanese dove cercavano di incrociare i codici fiscali dei clienti morosi con la tabella delle transazioni dell'ultimo anno. Usavano una logica che scaricava i codici in memoria applicativa e poi li schiaffava dentro il comando di filtro. La query impiegava dodici minuti per girare. Abbiamo cambiato approccio: caricare quei codici fiscali in una tabella temporanea indicizzata e fare una join. Il tempo di esecuzione è sceso a tre secondi. Tre secondi contro dodici minuti. Non c'è competenza che tenga davanti a una differenza del genere; si tratta solo di capire come lavora il motore sotto il cofano.

Le tabelle temporanee permettono al database di creare statistiche. Se il motore sa quanti record sta cercando di filtrare perché li vede fisicamente in una struttura interna, può decidere con precisione chirurgica se usare un Index Seek o un Merge Join. Senza queste informazioni, il database sta solo tirando a indovinare, e di solito indovina male.

Il mito dell'indipendenza dagli indici

Molti credono che avere un indice sulla colonna filtrata basti a dormire sonni tranquilli. Mi dispiace deluderti, ma non funziona così. Se la selettività della tua ricerca è bassa — ovvero se stai chiedendo al database di restituire il 20% o il 30% dei dati totali della tabella — il database ignorerà l'indice.

Perché l'indice viene ignorato

Immagina di avere una libreria con diecimila libri ordinati per autore. Se ti chiedo di trovarne tre, userai l'indice. Se ti chiedo di trovarne tremila sparsi a caso, farai prima a scorrere tutti gli scaffali dall'inizio alla fine piuttosto che saltare avanti e indietro tremila volte. Il database ragiona allo stesso modo. Quando usi liste di valori molto ampie, superi la soglia di convenienza dell'indice. In quel momento, l'indice non solo è inutile, ma diventa un peso morto perché il database deve comunque caricarlo in memoria per capire che non gli serve.

Scrivere codice che non scala col tempo

Un errore fatale che ho riscontrato in diverse startup è scrivere query che funzionano perfettamente in fase di test ma esplodono in produzione dopo sei mesi. In ambiente di sviluppo hai cento righe di dati e tutto sembra istantaneo. In produzione ne hai dieci milioni.

Pensiamo a come cambia l'approccio man mano che il business cresce. All'inizio, un filtro diretto sembra la scelta più logica. Poi i dati aumentano e inizi a notare dei rallentamenti. Invece di cambiare logica, molti programmatori cercano di ottimizzare il server aggiungendo CPU o RAM. È come cercare di spegnere un incendio versandoci sopra banconote: costoso e inutile. La soluzione non è mai nell'hardware quando il problema risiede nella struttura logica della chiamata.

Confronto tra approccio ingenuo e approccio professionale

Vediamo come si trasforma un'operazione comune nel mondo reale. Prendi un sistema che deve aggiornare lo stato di mille spedizioni diverse.

L'approccio sbagliato, che ho visto documentato in decine di post-mortem, consiste nel generare un'unica, mostruosa riga di comando che elenca tutti i mille ID tra parentesi. Questo costringe il database a un lavoro di parsing immane, blocca le tabelle per un tempo imprevedibile e spesso fallisce se il driver di connessione ha un limite alla lunghezza del testo inviabile. Se un solo ID tra quei mille è scritto male o causa un errore di tipo, l'intera operazione fallisce e devi ricominciare da capo, spesso senza sapere a che punto si sia interrotto il processo.

L'approccio corretto prevede l'inserimento dei mille ID in una tabella di appoggio dedicata, preferibilmente tramite un'operazione di bulk insert che è ottimizzata per la velocità di scrittura. Una volta che i dati sono lì, esegui una singola operazione di aggiornamento legando la tabella delle spedizioni con quella di appoggio tramite una join sulla chiave primaria. In questo modo, il database tratta l'operazione come un blocco atomico e strutturato. Se qualcosa va storto, il log delle transazioni è molto più pulito e facile da gestire. Inoltre, questa strategia permette di riutilizzare i dati per controlli incrociati o report di errore senza dover inviare nuovamente le informazioni dalla tua applicazione al server.

Gestire i tipi di dati con superficialità

Se la colonna nel database è un intero ma la tua applicazione invia i valori come stringhe tra virgolette, hai appena ucciso le prestazioni. Questo accade perché il database deve eseguire una conversione implicita per ogni singola riga della tabella. Se hai un milione di righe, il server farà un milione di conversioni di tipo prima ancora di iniziare a confrontare i valori.

Ho visto sistemi rallentati del 400% solo perché un framework ORM (Object-Relational Mapping) decideva autonomamente di trattare tutti i parametri di una Where And In SQL Query come stringhe Unicode (NVARCHAR) su una colonna che invece era di tipo ASCII (VARCHAR). Il database non poteva usare l'indice perché l'indice era costruito su stringhe semplici, mentre la ricerca chiedeva stringhe complesse. Per risolvere, il database ha dovuto convertire l'intero indice al volo per ogni ricerca. Un disastro che si risolve con una sola riga di configurazione nel codice, se solo sai dove guardare.

La trappola del limite dei parametri

Ogni database ha un limite fisico al numero di parametri che può gestire. SQL Server, per esempio, si ferma a 2100. Sembra un numero alto, ma se stai costruendo un report complesso o una dashboard per un ufficio acquisti che gestisce decine di migliaia di referenze, quel limite lo colpisci in una settimana.

Molti programmatori, una volta scoperto il limite, iniziano a "spezzettare" la query in blocchi da mille. Mandano dieci query invece di una. Questo è un altro errore marchiano. Invece di un problema grosso, ne hai creati dieci medi che intasano la coda di esecuzione. Inoltre, perdi la coerenza dei dati: tra la prima e la decima query, i dati nel database potrebbero essere cambiati, lasciandoti con un risultato finale incoerente che non corrisponde a nessuna realtà temporale precisa. È un incubo per la riconciliazione dei dati finanziari.

Realtà dei fatti e passi concreti

Non esiste una bacchetta magica per far funzionare bene filtri complessi su basi dati enormi. La verità è che se ti trovi a dover passare costantemente lunghe liste di valori al database, c'è quasi certamente un errore fondamentale nel modo in cui stai gestendo il flusso dei dati tra i tuoi servizi. Le informazioni di cui hai bisogno per filtrare dovrebbero, idealmente, trovarsi già dentro il database sotto forma di altre tabelle o viste.

L'uso massiccio di liste esterne indica che la tua applicazione sta cercando di fare il lavoro del database. Fermati. Non vincerai mai contro trent'anni di ingegneria dei motori relazionali. Se vuoi davvero scrivere codice che non faccia saltare in aria il server a metà del mese, devi accettare questi tre punti:

  1. Il database non è una pattumiera dove lanciare stringhe chilometriche sperando che le pulisca. La pulizia e la strutturazione dei dati prima dell'esecuzione sono responsabilità tua.
  2. La velocità non si ottiene con i trucchi sintattici, ma rispettando la struttura fisica dei dati. Se non capisci come funziona un indice B-Tree, non dovresti toccare query che impattano su tabelle vitali.
  3. Le prestazioni costano in termini di design. Scrivere la soluzione pigra richiede cinque minuti oggi ma costerà dieci notti insonni tra un anno. Investi il tempo ora per mappare correttamente le relazioni.

Il successo in questo campo non deriva dalla conoscenza di ogni singola funzione esotica del linguaggio, ma dalla capacità di prevedere come il tuo codice si comporterà sotto pressione. Non fidarti mai dei test su piccoli volumi. Se non hai testato la tua logica con almeno il doppio del carico previsto per il prossimo anno, non hai testato nulla. Sii onesto con te stesso e con i limiti della tua infrastruttura: a volte la soluzione più corretta è dire di no a una funzionalità che richiede filtri insostenibili, piuttosto che implementarla male e vederla fallire miseramente davanti al cliente.

GS

Gabriele Serra

Gabriele Serra segue i temi più discussi del momento con spirito critico e attenzione all'impatto sociale delle notizie.