Come le idee fondamentali di Jeffrey Ullman alimentano i database moderni: algebra relazionale, regole di ottimizzazione, join e pianificazione in stile compilatore che aiutano i sistemi a scalare.

Molte persone che scrivono SQL, costruiscono dashboard o ottimizzano una query lenta hanno beneficiato del lavoro di Jeffrey Ullman, anche se non hanno mai sentito il suo nome. Ullman è un informatico e docente il cui lavoro di ricerca e i suoi libri di testo hanno aiutato a definire come i database descrivono i dati, ragionano sulle query e le eseguono in modo efficiente.
Quando un motore di database trasforma il tuo SQL in qualcosa che può eseguire velocemente, si appoggia a idee che devono essere precise e adattabili. Ullman ha contribuito a formalizzare il significato delle query (così il sistema può riscriverle in sicurezza) e a collegare il pensiero sui database con quello sui compilatori (così una query può essere parsata, ottimizzata e tradotta in passi eseguibili).
Quell'influenza è silenziosa perché non appare come un pulsante nel tuo strumento BI o come una funzione visibile nella console cloud. Si vede invece in:
JOINQuesto post usa le idee centrali di Ullman come guida agli internals dei database che contano nella pratica: come l'algebra relazionale sta sotto SQL, come le riscritture di query preservano il significato, perché gli ottimizzatori basati sui costi fanno le scelte che fanno e come gli algoritmi di join spesso decidono se un job finisce in secondi o in ore.
Inseriremo anche qualche concetto in stile compilatore — parsing, riscrittura e pianificazione — perché i motori di database si comportano più come compilatori sofisticati di quanto molti immaginino.
Una promessa rapida: terremo la discussione accurata ma eviteremo dimostrazioni pesanti di matematica. L'obiettivo è fornire modelli mentali che puoi applicare al lavoro la prossima volta che compaiono problemi di prestazioni, scalabilità o comportamenti di query confusi.
Se hai mai scritto una query SQL e ti sei aspettato che "significasse" una sola cosa, stai usando idee che Jeffrey Ullman ha aiutato a popolarizzare e formalizzare: un modello chiaro per i dati, più modi precisi per descrivere cosa chiede una query.
Alla base, il modello relazionale tratta i dati come tabelle (relazioni). Ogni tabella ha righe (tuple) e colonne (attributi). Ora sembra ovvio, ma la parte importante è la disciplina che crea:
Questa cornice rende possibile ragionare su correttezza e prestazioni senza giri di parole. Quando sai cosa rappresenta una tabella e come le righe sono identificate, puoi prevedere cosa faranno i join, cosa significano i duplicati e perché certi filtri cambiano i risultati.
L'insegnamento di Ullman usa spesso l'algebra relazionale come una specie di calcolatrice per le query: un piccolo insieme di operazioni (select, project, join, union, difference) che puoi combinare per esprimere ciò che desideri.
Perché è utile lavorando con SQL: i database traducono SQL in una forma algebrica e poi la riscrivono in un'altra forma equivalente. Due query che sembrano diverse possono essere algebricamente uguali — ed è così che gli ottimizzatori possono riordinare i join, spostare filtri in basso o rimuovere lavoro ridondante mantenendo intatto il significato.
SQL è in gran parte “cosa”, ma i motori spesso ottimizzano usando l'algebra come “come”.
I dialetti SQL variano (Postgres vs. Snowflake vs. MySQL), ma i fondamentali no. Capire chiavi, relazioni e equivalenza algebrica ti aiuta a capire quando una query è logicamente errata, quando è solo lenta e quali cambiamenti preservano il significato attraverso piattaforme diverse.
L'algebra relazionale è la "matematica sotto" SQL: un piccolo set di operatori che descrivono il risultato che desideri. Il lavoro di Jeffrey Ullman ha reso questa visione operatoriale netta e insegnabile — ed è ancora il modello mentale che la maggior parte degli ottimizzatori usa.
Una query di database può essere espressa come una pipeline di pochi mattoncini:
WHERE in SQL)SELECT col1, col2)JOIN ... ON ...)UNION)EXCEPT in molti dialetti SQL)Poiché l'insieme è piccolo, diventa più semplice ragionare sulla correttezza: se due espressioni algebriche sono equivalenti, restituiscono la stessa tabella per ogni stato valido del database.
Prendi una query familiare:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Concettualmente, questo è:
iniziare con un join di customers e orders: customers ⋈ orders
select solo gli ordini superiori a 100: σ(o.total > 100)(...)
project la colonna che vuoi: π(c.name)(...)
Non è la notazione interna esatta usata da ogni motore, ma è l'idea giusta: SQL diventa un albero di operatori.
Molti alberi diversi possono significare lo stesso risultato. Per esempio, i filtri spesso possono essere spostati prima (applicare σ prima di un grande join), e le proiezioni possono eliminare colonne inutilizzate prima (applicare π presto).
Quelle regole di equivalenza permettono al database di riscrivere la tua query in un piano più economico senza cambiare il significato. Una volta che vedi le query come algebra, l’“ottimizzazione” smette di essere magia e diventa una rimodellazione sicura guidata da regole.
Quando scrivi SQL, il database non lo esegue "così com'è". Traduce la tua istruzione in un piano di query: una rappresentazione strutturata del lavoro da fare.
Un buon modello mentale è un albero di operatori. Le foglie leggono tabelle o indici; i nodi interni trasformano e combinano righe. Gli operatori comuni includono scan, filter (selezione), project (scegli colonne), join, group/aggregate e sort.
I database tipicamente separano la pianificazione in due livelli:
L'influenza di Ullman emerge nell'enfasi sulle trasformazioni che preservano il significato: riorganizza il piano logico in molti modi senza cambiare la risposta, poi scegli una strategia fisica efficiente.
Prima di scegliere l'approccio finale di esecuzione, gli ottimizzatori applicano regole algebriche di "pulizia". Queste riscritture non cambiano i risultati; riducono il lavoro inutile.
Esempi comuni:
Supponiamo che tu voglia gli ordini di utenti in un paese:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Un'interpretazione ingenua potrebbe unire tutti gli users con tutti gli orders e poi filtrare il Canada. Una riscrittura che preserva il significato spinge il filtro più in basso così il join tocca meno righe:
country = 'CA'order_id e totalIn termini di piano, l'ottimizzatore prova a trasformare:
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
in qualcosa più simile a:
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Stesso risultato. Meno lavoro.
Queste riscritture sono facili da non notare perché non le scrivi mai esplicitamente — eppure sono una delle ragioni principali per cui lo stesso SQL può essere veloce su un database e lento su un altro.
Quando esegui una query SQL, il database considera più modi validi per ottenere la stessa risposta, poi sceglie quello che si aspetta sia il meno costoso. Quel processo decisionale si chiama ottimizzazione basata sui costi — ed è uno dei punti più pratici in cui la teoria in stile Ullman si manifesta nelle prestazioni di tutti i giorni.
Un modello di costi è un sistema di punteggio che l'ottimizzatore usa per confrontare piani alternativi. La maggior parte dei motori stima il costo usando poche risorse chiave:
Il modello non deve essere perfetto; deve essere direzionalmente corretto abbastanza spesso da scegliere piani buoni.
Prima di poter valutare i piani, l'ottimizzatore si pone una domanda a ogni passo: quante righe produrrà questo? Questa è la stima della cardinalità.
Se filtri WHERE country = 'CA', il motore stima quale frazione della tabella corrisponde. Se unisci customers con orders, stima quante coppie corrisponderanno sulla chiave di join. Queste stime di conteggio delle righe determinano se preferisce un index scan a una scansione completa, un hash join a un nested loop, o se un ordinamento sarà piccolo o enorme.
Le ipotesi dell'ottimizzatore si basano su statistiche: conteggi, distribuzioni di valori, tassi di null e talvolta correlazioni fra colonne.
Quando le statistiche sono obsolete o mancanti, il motore può sbagliare le stime di ordini di grandezza. Un piano che sembra economico sulla carta può diventare costoso nella realtà — sintomi classici sono rallentamenti improvvisi dopo la crescita dei dati, cambi di piano “casuali” o join che improvvisamente spillano su disco.
Stime migliori spesso richiedono più lavoro: statistiche più dettagliate, campionamento o esplorare più piani candidati. Ma anche la pianificazione ha un costo, specialmente per query complesse.
Quindi gli ottimizzatori bilanciano due obiettivi:
Capire quel compromesso ti aiuta a interpretare l'output di EXPLAIN: l'ottimizzatore non cerca di essere brillante — cerca di essere predicibilmente corretto con informazioni limitate.
Il lavoro di Ullman ha aiutato a diffondere un'idea semplice ma potente: SQL non viene tanto “eseguito” quanto tradotto in un piano di esecuzione. Dove ciò è più evidente è nei join. Due query che restituiscono le stesse righe possono avere tempi di esecuzione molto diversi a seconda dell'algoritmo di join scelto dal motore — e dell'ordine in cui unisce le tabelle.
Nested loop join è concettualmente semplice: per ogni riga a sinistra, trova le righe corrispondenti a destra. Può essere veloce quando il lato sinistro è piccolo e il lato destro ha un indice utile.
Hash join costruisce una tabella hash da un input (di solito il più piccolo) e la usa per sondare l'altro. Brilla per input grandi e non ordinati con condizioni di uguaglianza (es. A.id = B.id), ma richiede memoria; lo spill su disco può annullare il vantaggio.
Merge join scorre due input in ordine ordinato. È ideale quando entrambi i lati sono già ordinati (o ordinabili a basso costo), come quando gli indici forniscono righe in ordine di chiave di join.
Con tre o più tabelle, il numero di possibili ordini di join esplode. Unire prima due tabelle grandi può creare un enorme risultato intermedio che rallenta tutto il resto. Un ordine migliore spesso inizia dal filtro più selettivo (poche righe) e si estende verso l'esterno, mantenendo piccoli gli intermedi.
Gli indici non solo velocizzano le ricerche: rendono certe strategie di join praticabili. Un indice sulla chiave di join può trasformare un nested loop costoso in una rapida "seek per riga". Viceversa, indici mancanti o inutilizzabili possono spingere il motore verso hash join o grandi ordinamenti per i merge join.
I database non si limitano a “eseguire SQL”. Lo compilano. L'influenza di Ullman copre sia la teoria dei database sia il pensiero sui compilatori, e quel collegamento spiega perché i motori di query si comportano come catene di strumenti di linguaggi: traducono, riscrivono e ottimizzano prima di fare qualsiasi lavoro.
Quando invii una query, il primo passo somiglia al front end di un compilatore. Il motore tokenizza parole chiave e identificatori, verifica la grammatica e costruisce un parse tree (spesso semplificato in un abstract syntax tree). Qui si catturano errori basilari: virgole mancanti, nomi di colonna ambigui, regole di grouping non valide.
Un modello mentale utile: SQL è un linguaggio di programmazione il cui “programma” descrive relazioni di dati invece di cicli.
I compilatori convertono la sintassi in una rappresentazione intermedia (IR). I database fanno qualcosa di simile: traducono la sintassi SQL in operatori logici come:
GROUP BY)Quella forma logica è più vicina all'algebra relazionale del testo SQL, il che facilita il ragionamento su significato ed equivalenza.
Le ottimizzazioni dei compilatori mantengono identici i risultati del programma riducendone il costo di esecuzione. Gli ottimizzatori dei database fanno lo stesso, usando sistemi di regole come:
Questa è la versione database della “eliminazione del codice morto”: non le stesse tecniche, ma la stessa filosofia — preservare la semantica, ridurre il costo.
Se la tua query è lenta, non fissare solo l'SQL. Guarda il piano di query come faresti con l'output di un compilatore. Un piano ti dice cosa il motore ha effettivamente scelto: ordine dei join, uso degli indici e dove si spende il tempo.
Pratica utile: impara a leggere l'output di EXPLAIN come un "assembly" delle prestazioni. Trasforma il tuning da congettura a debugging basato su prove. Per approfondire l'abitudine pratica, vedi /blog/practical-query-optimization-habits.
Una buona performance delle query spesso parte ancora prima che tu scriva SQL. La teoria del design dello schema di Ullman (in particolare la normalizzazione) riguarda strutturare i dati così il database può mantenerli corretti, prevedibili ed efficienti man mano che crescono.
La normalizzazione mira a:
Questi benefici di correttezza si traducono in vantaggi di prestazioni: meno campi duplicati, indici più piccoli e aggiornamenti meno costosi.
Non serve memorizzare dimostrazioni per usare le idee:
Denormalizzare può avere senso quando:
La chiave è denormalizzare deliberatamente, con un processo per mantenere sincroniche le duplicazioni.
Il design dello schema condiziona ciò che l'ottimizzatore può fare. Chiavi e foreign key chiare abilitano migliori strategie di join, riscritture più sicure e stime di cardinalità più accurate. Nel frattempo, duplicazioni eccessive possono gonfiare indici e rallentare le scritture, e colonne multivalore bloccano predicati efficienti. Con la crescita dei dati, queste decisioni di modellazione iniziali spesso contano più dell'ottimizzazione fine di una singola query.
Quando un sistema "scala", raramente è solo aggiungere macchine più grandi. Spesso la parte difficile è che lo stesso significato di query deve essere preservato mentre il motore sceglie una strategia fisica molto diversa per mantenere i tempi di esecuzione prevedibili. L'enfasi di Ullman sulle equivalenze formali è proprio ciò che permette quei cambi di strategia senza modificare i risultati.
A taglie piccole, molti piani "funzionano". A scala, la differenza tra scansionare una tabella, usare un indice o usare un risultato precomputato può essere tra secondi e ore. La parte teorica conta perché l'ottimizzatore ha bisogno di un insieme sicuro di regole di riscrittura (es. spingere filtri, riordinare join) che non alterino la risposta — anche se cambiano radicalmente il lavoro svolto.
Il partizionamento (per data, cliente, regione, ecc.) trasforma una tabella logica in molti pezzi fisici. Questo influenza la pianificazione:
Il testo SQL può restare identico, ma il miglior piano dipende da dove vivono le righe.
Le materialized view sono essenzialmente "sotto-espressioni salvate". Se il motore può provare che la tua query corrisponde (o può essere riscritta per corrispondere) a un risultato memorizzato, può sostituire lavoro costoso — come join e aggregazioni ripetute — con una semplice ricerca. Questo è pensiero di algebra relazionale in pratica: riconoscere l'equivalenza e riutilizzare.
La cache può velocizzare letture ripetute, ma non salverà una query che deve scansionare troppi dati, rimescolare enormi intermedi o calcolare un join gigantesco. Quando emergono problemi di scala, la soluzione è spesso: ridurre la quantità di dati toccata (layout/partizionamento), ridurre il calcolo ripetuto (materialized views) o cambiare il piano — non solo "aggiungere cache".
L'influenza di Ullman si vede in una mentalità semplice: tratta una query lenta come una dichiarazione di intento che il database è libero di riscrivere, poi verifica cosa ha effettivamente deciso di fare. Non serve essere un teorico per trarne vantaggio — serve solo una routine ripetibile.
Inizia dalle parti che solitamente dominano i tempi di esecuzione:
Se fai una sola cosa, individua il primo operatore dove il conteggio delle righe esplode. Quello è di solito la causa principale.
Sono facili da scrivere e sorprendentemente costosi:
WHERE LOWER(email) = ... può impedire l'uso dell'indice (usa una colonna normalizzata o un indice funzionale se supportato).L'algebra relazionale incoraggia due mosse pratiche:
WHERE prima dei join quando possibile per ridurre gli input.Una buona ipotesi suona così: “Questo join è costoso perché stiamo unendo troppe righe; se filtriamo orders agli ultimi 30 giorni prima, l'input del join diminuisce.”
Usa una regola decisionale semplice:
EXPLAIN mostra lavoro evitabile (join inutili, filtri tardivi, predicati non sargable).L'obiettivo non è "SQL furbo" ma risultati intermedi prevedibili e ridotti — proprio il tipo di miglioramento che le idee di Ullman rendono più facili da individuare.
Questi concetti non sono solo per i DBA. Se stai mettendo in produzione un'applicazione, stai prendendo decisioni di database e pianificazione delle query anche senza saperlo: forma dello schema, scelte di chiavi, pattern di query e layer di accesso ai dati influenzano ciò che l'ottimizzatore può fare.
Se usi un flusso di lavoro a sviluppo accelerato (per esempio, generare un'app React + Go + PostgreSQL da un'interfaccia chat in Koder.ai), i modelli mentali in stile Ullman sono una rete di sicurezza pratica: puoi rivedere lo schema generato per chiavi e relazioni pulite, ispezionare le query su cui l'app si basa e convalidare le prestazioni con EXPLAIN prima che i problemi raggiungano la produzione. Più velocemente iteri sul ciclo “intento della query → piano → correzione”, più valore ottieni dallo sviluppo accelerato.
Non serve "studiare la teoria" come hobby separato. Il modo più rapido per beneficiare dei fondamenti in stile Ullman è imparare quanto basta per leggere i piani di query con sicurezza — e poi esercitarsi sul proprio database.
Cerca questi libri e argomenti di lezione (nessuna affiliazione — solo punti di partenza ampiamente citati):
Inizia in piccolo e mantieni ogni passo legato a qualcosa che puoi osservare:
Scegli 2–3 query reali e iterale:
IN in EXISTS, spingi i predicati prima, rimuovi colonne non necessarie e confronta i risultati.Usa un linguaggio chiaro e basato sui piani:
Questo è il vantaggio pratico delle fondamenta di Ullman: ottieni un vocabolario condiviso per spiegare le prestazioni — senza indovinare.
Jeffrey Ullman ha contribuito a formalizzare come i database rappresentano il significato delle query e come possono trasformarle in equivalenti più veloci in modo sicuro. Queste basi si vedono ogni volta che un motore riscrive una query, riorganizza i join o sceglie un piano d'esecuzione diverso garantendo lo stesso insieme di risultati.
L'algebra relazionale è un piccolo insieme di operatori (select, project, join, union, difference) che descrivono con precisione il risultato di una query. I motori traducono spesso SQL in una struttura simile a un albero di operatori in modo da poter applicare regole di equivalenza (come spostare i filtri più in basso) prima di scegliere una strategia di esecuzione.
Perché l'ottimizzazione si basa sulla prova che una query riscritta restituisce gli stessi risultati. Le regole di equivalenza permettono all'ottimizzatore di fare cose come:
WHERE prima di un joinQueste modifiche possono ridurre drasticamente il lavoro senza cambiare il significato.
Un piano logico descrive cosa va calcolato (filtri, join, aggregazioni) indipendentemente dai dettagli di storage. Un piano fisico decide come eseguirlo (index scan vs full scan, hash join vs nested loop, parallelismo, strategie di ordinamento). La maggior parte delle differenze di prestazioni deriva dalle scelte fisiche, rese possibili dalle riscritture logiche.
L'ottimizzazione basata sui costi valuta più piani validi e sceglie quello con il costo stimato più basso. I costi sono tipicamente guidati da fattori pratici come righe processate, I/O, CPU e memoria (incluso il rischio che hash o sort debbano essere spillati su disco).
La cardinalità stimata è la stima del numero di righe che produrrà un dato step. Queste stime guidano l'ordine dei join, il tipo di join e la scelta fra index scan e full scan. Quando le stime sbagliano (spesso per statistiche vecchie o mancanti), possono verificarsi rallentamenti improvvisi, grandi spill su disco o cambi di piano inattesi.
Concentrati su alcuni segnali ad alto valore:
Tratta il piano come un output compilato: mostra cosa il motore ha effettivamente deciso di fare.
La normalizzazione riduce fatti duplicati e anomalie di aggiornamento, il che spesso si traduce in tabelle e indici più piccoli e join più affidabili. La denormalizzazione può essere appropriata per analytics o pattern di lettura intensiva, ma deve essere deliberata (regole chiare di refresh e ridondanza nota) per non compromettere la correttezza nel tempo.
La scala spesso richiede di cambiare la strategia fisica mantenendo identico il significato della query. Strumenti comuni:
La cache aiuta letture ripetute, ma non risolve una query che deve toccare troppi dati o produrre intermedi giganteschi.