Capitolo 3
Il modello relazionale
I database sono il fulcro dei sistemi informativi, sono il cuore delle applicazioni. La struttura
di un database, denominata modello di dati (schema o anche progettazione di database),
specifica un database. Uno dei modelli più importanti utilizzati per i moderni database è il
modello relazionale. Benché non sia l'unico modello di dati, è probabilmente il più importante.
Il modello relazionale viene usato principalmente per i database transazionali, dove i dati di
un'azienda vengono prima archiviati, in contrapposizione ai database warehouse che servono
come archivio dei dati storici. Rispetto agli altri modelli di dati di oggi, il modello relazionale è
particolarmente utile per i dati transazionali perché l'integrità dei dati può essere dichiarata e
applicata dal modello. L'integrità dei dati è la conformità dei dati alle regole aziendali. Se i dati
sono sbagliati la prima volta che vengono inseriti in azienda, l'impatto sull'intera azienda sarà
negativo. Ad esempio, i sistemi analitici non aiuterebbero a migliorare le operazioni a causa
del comune concetto garbage in - garbage out. Un altro vantaggio del modello relazionale è
che viene definito matematicamente. Quindi, durante la modellazione non sei guidato solo
dalle procedure consigliate, puoi valutare anche la tua struttura e decidere fermamente se la
tal cosa va bene o meno.
I sistemi di gestione dei database relazionali (RDBMS), incluso Microsoft SQL Server, archiviano
i dati in formato relazionale. Benché l'implementazione fisica sia diversa da rivenditore a
rivenditore, il modello relazionale fornisce una percezione dei dati utente coerente per tutti
i RDBMS. In questo capitolo saranno introdotti i concetti principali del modello relazionale.
Questa conoscenza ti aiuterà a capire i successivi capitoli in cui saranno illustrate query
avanzate.
Introduzione al modello relazionale
Il modello relazionale è stato creato negli anni '60 da Edgar F. Codd, che lavorava all'IBM. È
una concettualizzazione semplice ma definita in modo rigoroso del modo in cui gli utenti
ricevono e utilizzano i dati. Tratta i tre aspetti principali dell'elaborazione dei dati nel seguente
modo, secondo quanto scritto in An Introduction to Database Systems, 8th edition di C. J. Date
(Addison-Wesley, 2003):
■
Strutturale
I dati vengono ricevuti dall'utente solo sotto forma di tabelle.
■
Manipolativo Gli utenti manipolano i dati con un insieme infinito di operatori
relazionali. Gli operatori rappresentano l'algebra relazionale.
■
Integrità Le tabelle devono soddisfare vincoli di integrità definiti.
L'aspetto strutturale viene espresso anche dall'Information Principle, che afferma che tutte le
informazioni di un database relazionale vengono espresse in un (e solo un) modo come valori
espliciti in colonne all'interno delle righe di una tabella.
Nel modello relazionale una tabella viene definita relazione e una riga viene definita tupla.
Nella prossima sezione verranno illustrate più nel dettaglio sia le relazioni sia le tuple.
83
84
Microsoft Transact SQL Query
Relazioni, tuple e tipi
Una relazione è un oggetto matematico che rappresenta ciò che gli utilizzatori dei database
chiamano una tabella. Gli elementi di una determinata relazione, come le righe di una
tabella, rappresentano le istanze di qualche entità reale, come persone, luoghi, cose o eventi.
La relazione rappresenta l'insieme di questi elementi, che sono tuple dal punto di vista
matematico. Una tupla è un insieme dei relativi attributi, ognuno dei quali viene rappresentato
da tre cose: il nome dell'attributo, il tipo di attributo e il valore dell'attributo.
Nota Il modello relazionale utilizza nozioni più generali di relazione e tupla rispetto a quelli
introdotti nel capitolo 2. Nel capitolo 2 hai imparato a conoscere le tuple ordinate, che avevano
parti posizionali bene definite: prima, seconda e così via. Anche qui le tuple hanno parti ben
definite, ma sono non ordinate, inoltre sono definite dai nomi di attributo invece che dalle posizioni
ordinali. Nel capitolo 2 una relazione era un insieme di coppie ordinate di un prodotto cartesiano.
Qui una relazione è un insieme di tuple non ordinate che hanno la stessa intestazione. Le nozioni
usate nel modello relazionale sono più astratte, e renderle precise dal punto di vista matematico
non è molto intuitivo.
L'insieme dei nomi e dei tipi di attributo di una tupla, presi insieme, vengono chiamati
intestazione. Puoi pensare all'intestazione di una tupla come a un modulo che può essere
riempito; il modulo ha nomi di attributi con spazi vuoti per i valori che vanno inseriti. Una tupla
è la copia riempita di un modulo di intestazione. Le proprietà della tupla includono:
■
Ogni attributo di una tupla contiene esattamente un valore del tipo appropriato per
ognuno dei relativi nomi di attributo. Pensando di nuovo a una tupla come a una
copia riempita di un modulo di intestazione, c'è esattamente un valore in ogni spazio
vuoto (e del tipo appropriato per quel particolare attributo).
■
Gli attributi non hanno un ordine (come gli elementi di un set). Di conseguenza ogni
attributo deve avere un nome diverso, perché non puoi fare riferimento a un attributo
usando la relativa posizione in una tupla. In termini di moduli, il modo in cui i nomi di
attributo vengono disposti nel modulo di intestazione è irrilevante; hanno importanza
solo i nomi degli attributi e, di conseguenza, tali nomi devono essere diversi.
■
Un subset di una tupla è una tupla (con meno attributi). Usando di nuovo l'analogia
del modulo, una sezione di un modulo, vista da sola, è comunque un modulo, ma può
avere meno voci.
Benché sia possibile definire gli operatori dall'algebra relazionale sulle tuple, non puoi
manipolare singole tuple in un database relazionale. Le operazioni vengono eseguite solo
sui set di tuple, ossia sulle relazioni. Le tuple non solo costituiscono le relazioni, ma aiutano
anche a definirle. Una relazione è composta da un set di tuple con la stessa intestazione;
l'intestazione di tali tuple può essere chiamata intestazione della relazione e viceversa. Allo
stesso modo, è possibile pensare che le relazioni abbiano degli attributi. Le relazioni con
intestazioni diverse sono tipi di relazioni diversi. Nel modello relazionale i tipi di dati degli
attributi, al contrario dei tipi di intestazioni delle relazioni, a volte vengono definiti domini per
evitare confusione con la parola tipo.
Capitolo 3
Il modello relazionale
85
Come i contenuti di una tabella di database possono cambiare, una relazione dovrebbe
essere in grado di contenere diversi set di tuple in momenti di versi. Le relazioni del modello
relazionale di fatto sono variabili, a volte chiamate variabili relazionali, e il valore di una
variabile relazionale di un qualunque tipo è un set di tuple di tale tipo. Non verrà sempre fatta
una distinzione tra relazioni e variabili relazionali dello stesso tipo, seguendo la pratica comune
di altre aree della matematica. Ad esempio, spesso verrà scritto "n è un numero intero", mentre
bisognerebbe scrivere correttamente "n è una variabile intera". Il fatto che una relazione sia un
set di tuple comporta le seguenti importanti conseguenze:
■
Come per le tuple, gli attributi di una relazione non hanno un ordine.
■
Ogni attributo di una relazione ha un tipo di dati fortemente definito. Ogni tupla di
una relazione contiene esattamente un valore di questo tipo per ogni attributo.
■
La proiezione di una relazione è una relazione, in cui la proiezione è un'operazione che
seleziona un subset specifico di attributi da una relazione (e da tutte le relative tuple).
La proiezione è uno degli operatori più importanti nell'algebra relazionale.
■
Una relazione non ha tuple duplicate. È una conseguenza del fatto che una relazione è
un set e i set contengono elementi distinti. Dal momento che le tuple di una relazione
sono distinte, possono essere distinte da alcuni o da tutti i relativi valori di attributo.
Un subset minimo di attributi sufficiente a distinguere le tuple per ogni valore della
variabile relazionale viene definito chiave.
■
L'ordine delle tuple non è importante. Ancora una volta, questo deriva dalla teoria dei
set, in cui gli elementi di un set non sono ordinati. Ciò significa che in una relazione,
termini come prima, successiva, precedente, ultima tupla non vengono definiti.
Il termine tipo è stato usato più volte, presumendo tacitamente che si capisca di quale tipo
si tratti. Di seguito viene riportata una definizione formale di tipo: un tipo, chiamato anche
tipo di dati o dominio, è un set finito di valori, ad esempio un set finito di valori interi. Benché
in matematica i set universali (ad esempio i valori interi) possano essere infiniti, in un sistema
informatico incontri sempre un limite. Quindi, un set di valori possibili di un tipo è finito.
Ogni valore ha esattamente un tipo più specifico. Con "più specifico" si intende la possibilità
di ereditarietà del tipo (benché l'ereditarietà del tipo non sia ancora implementata in SQL
Server). Ad esempio, il valore 3 può essere considerato un numero reale, un intero o un
numero naturale; un numero naturale è il tipo più specifico per tale numero. In breve, puoi
tranquillamente dire che ogni valore in un database relazionale ha un solo tipo.
Un tipo è composto da:
■
Un nome
■
Uno o più rappresentazioni possibili denominate:
❏
Una è archiviata fisicamente.
❏
Almeno una viene dichiarata agli utenti.
■
Un insieme di operatori possibili sui valori del tipo
■
Vincoli del tipo
86
Microsoft Transact SQL Query
Ogni variabile e ogni attributo ha un tipo esplicito, ogni operatore restituisce un risultato di
qualche tipo esplicito, ogni parametro di ogni operatore ha un tipo esplicito e ogni espressione
è implicitamente di qualche tipo. L'archiviazione fisica non viene esposta agli utenti; dipende
dal sistema. Un tipo vincola i valori possibili in modi diversi: con vincoli espliciti e con operatori
definiti. Ad esempio, per il tipo integer puoi definire gli operatori di addizione, sottrazione e
moltiplicazione. L'operatore di divisione non viene definito come integer per tutte le coppie
di integer perché il risultato può non rientrare nel dominio integer. I numeri naturali sono
gli integer con un vincolo; il numero deve essere positivo (o, secondo alcuni autori, non
negativo). Gli operatori e i vincoli sono caratterizzati da interfoliazione. Nota che l'operatore di
sottrazione non viene definito all'interno dei numeri naturali, anche se lo era per gli integer.
Perché un tipo sia utile, deve implementare almeno due operatori: un operatore mutatore,
che consente l'aggiornamento di variabili e attributi del tipo, e un operatore di selezione, che
consente di recuperare i valori del tipo. Gli altri operatori possono essere definiti dal creatore
del tipo nel modo appropriato all'uso che si intende farne. Nota che il tipo può avere più
presentazioni e quindi può avere più operatori di selezione. Ad esempio, un punto in un piano
può essere rappresentato nei sistemi cartesiani o con coordinate polari.
Un concetto importante è se un tipo è scalare o non scalare. Un tipo non scalare ha un insieme
di componenti visibili all'utente e direttamente accessibili; un tipo scalare no. I tipi scalari
vengono definiti anche atomici o incapsulati. Questa descrizione è in qualche modo vaga. È
chiaro quando un punto nel piano delle coordinate è scalare? Sia le presentazioni cartesiane
che polari hanno componenti visibili all'utente. Tuttavia, se operi solo sui punti interi e mai
sulle singole coordinate, un singolo punto non è divisibile e quindi è scalare. E il tipo car?
Sicuramente ha componenti visibili all'utente, ma normalmente lo consideri come indivisibile
e quindi scalare. Si proverà ora a dare una definizione precisa. Un valore è scalare se lo utilizzi
solo con operatori definiti per il relativo tipo. Gli operatori possono recuperare o aggiornare
una singola coordinata di un punto, ma quando questi operatori vengono definiti sui punti (al
contrario dei numeri), un punto resta comunque scalare.
Un insieme di punti archiviati in una stringa è non scalare se devi operare con i punti recuperati
dalla stringa. Se usi questo insieme come stringa e operi su di esso solo con gli operatori di
stringa, allora questo valore è scalare. Ma quando un insieme di punti definisce un poligono?
Se definito in modo esplicito un tipo poligono, si tratta di un tipo scalare. Se operi con i punti
che definiscono gli angoli del poligono tramite operazioni definite nel tipo poligono, i valori
di questo tipo sono scalari. Nota che questo si riflette sul mondo reale. A volte tratti un valore
come scalare di un qualche tipo e a volte come insieme di componenti in cui ogni componente
ha il proprio tipo. Ad esempio, guidi una macchina come se fosse un valore scalare. Quando
porti la macchina da un meccanico, tuttavia, il meccanico potrebbe trattarla come insieme non
scalare di componenti.
Nelle relazioni sono consentiti solo gli attributi scalari (o atomici). Ciò non significa che i punti
su un piano non possono essere valori attributo di una relazione; tuttavia, i valori dell'attributo
devono essere archiviati usando il tipo più specifico per i punti, in altre parole, il tipo di punto
e non una stringa di coordinate. Qual è il tipo più specifico di un valore? Dipende dall'uso che
si intende farne. Se stai sviluppando un'applicazione per le risorse umane, l'immagine di un
dipendente può probabilmente essere trattata come valore scalare di qualche tipo binario, e
Capitolo 3
Il modello relazionale
87
modelleresti una relazione Subjects usando un singolo attributo Picture. Se stai sviluppando
un'applicazione di riconoscimento facciale e devi analizzare l'immagine usando alcuni grafici
di vettore, modellerai una relazione Subjects usando una relazione Pictures associata che
abbia i propri e più dettagliati attributi (o, in alternativa, una relazione Subjects con attributi
dettagliati di un'immagine invece che un singolo attributo Picture).
Ad ogni modo, il modello relazionale non è limitato all'utilizzo di pochi tipi specifici; supporta
tutti i tipi possibili. Alcuni dei tipi più comuni vengono forniti da un RDBMS e vengono definiti
sistema. Inoltre, un RDBMS dovrebbe consentire di estendere il set di tipi sistema con tipi
definiti dall’utente. SQL Server consente di creare tipi definiti dall’utente nella versione 2005 e
successive.
Il significato di relazioni
Come già detto, ogni relazione rappresenta delle entità del mondo reale, ad esempio persone,
luoghi, cose o eventi. Un'entità è una cosa che può essere identificata in modo distinto ed è
di interesse aziendale. Il termine classe di entità può essere usato al posto di entità per un tipo
di cosa (ad esempio "persona") in contrapposizione a un esempio o una rappresentazione
specifica (ad esempio "Steve Kass", che rappresenta una persona specifica). Ogni
rappresentazione di un'entità può essere identificata in modo univoco, fatto che consente di
usare una relazione per rappresentare un'entità.
Ogni rappresentazione di un'entità svolge un ruolo importante nell'applicazione o nel
sistema in cui è rappresentata. Questo è il concetto di astrazione: in un database sono
disponibili solo classi di entità (e attributi di tali entità) che hanno un motivo per essere lì. Ogni
rappresentazione di un'entità può essere descritta tramite uno o più attributi. Le relazioni
sono associazioni tra entità. Una relazione è un subset dei prodotti incrociati dei set dell'entità
coinvolti nella relazione. Gli attributi forniscono delle informazioni sulle entità che sono di
interesse per l'applicazione.
Il paragrafo precedente definisce il significato di relazioni in termini di entità e dei rapporti tra
loro, come definito da Peter Chen nel suo scritto “The Entity-Relationship Model—Toward a
Unified View of Data", a cui fanno riferimento la maggior parte dei libri sulla modellazione dei
dati. L'approccio entità-relazione è quello più usato anche nella modellazione del database
relazionale (ricerca di entità, relazioni e relativi attributi). Tuttavia esiste un altro approccio
per capire cosa significa relazioni. In questo secondo approccio, considerato più naturale, le
relazioni vengono descritte in termini di proposizioni e predicati.
Nel capitolo 2 hai appreso la definizione di proposizioni e predicati. Cosa ha a che fare
questa definizione con una relazione? Nel linguaggio normale vengono fatte affermazioni
sulle entità di interesse tramite dichiarazioni del fatto, o, in logica, tramite le proposizioni. Di
seguito viene illustrato un esempio di proposizione: Il dipendente con l'ID numero 17 si chiama
Fernando, lavora nel reparto D1 ed è stato assunto il 19 luglio 2003. Le forme generalizzate
delle proposizioni sono i predicati. Di seguito viene illustrato un esempio di predicato: Il
dipendente con l'ID numero (Dip#) si chiama (Nome), lavora nel reparto (Rep#) ed è stato
assunto il (DataAssunzione). I quattro termini nelle parentesi sono segnaposto o parametri che
corrispondono ai quattro valori della proposizione precedente.
88
Microsoft Transact SQL Query
Quando sostituisci i parametri con valori specifici, il predicato si riduce a una singola
proposizione. Di seguito sono riportati i valori per i parametri che riducono il predicato
sopracitato nella proposizione precedente:
(17; Fernando; D1; 19 luglio 2003)
Puoi notare che i parametri formano una tupla. Nota che le tuple in una relazione
rappresentano di fatto le proposizioni. Come le tuple rappresentano le proposizioni, le
intestazioni della relazione rappresentano i predicati per tali proposizioni. Questo approccio
è molto vicino al linguaggio normale. Descrivi semplicemente un problema aziendale, trova i
predicati e scrivili, in questo modo ottieni il modello di dati. Naturalmente hai bisogno di uno
strumento che converta i predicati in relazioni. Questo approccio alla modellazione simile al
linguaggio normale viene definito modellazione ruolo-oggetto, ed è descritto in Information
Modeling and Relational Databases, 2nd edition di Terry Halpin e Tony Morgan (Morgan
Kaufmann, 2008) (informazioni in lingua inglese).
Ma questo non è un libro sulla modellazione. Quello che devi capire è il significato delle
relazioni. Puoi considerarle dei contenitori di entità del mondo reale o dei predicati e
proposizioni del linguaggio normale. Nota che per i predicati citati fino ad ora non esistono
vincoli sui valori di tupla che li rendono proposizioni, tranne il fatto che devono essere valori
dei tipi attributo. I vincoli verranno illustrati a breve; per ora verrà fornita una definizione
generica e informale del tipo di regola che viene applicata con i vincoli: una proposizione che
restituisce False per il predicato della relazione (intestazione) non può mai fare parte della
relazione.
Viste (e altre relazioni virtuali)
Le viste sono una parte importante del database relazionale. Un'altra parte importante delle
query in un'applicazione sono le relazioni temporanee (o set di righe nella terminologia di SQL
Server). Una vista è una relazione virtuale; di fatto è una query archiviata che viene valutata in
fase di esecuzione quando necessario. Un utente di database, uno sviluppatore di applicazioni
o un'applicazione non dovrebbe essere in grado di distinguere una vista da una tabella.
Questo principio è importante (principio di interscambiabilità) e afferma che non dovrebbe
esserci distinzione tra relazioni effettive (a volte chiamate di base) e relazioni virtuali. Questo
principio fornisce indipendenza logica dei dati in un database relazionale. Tale indipendenza
può aiutarti molto con due problemi: crescita e ristrutturazione. Se la tabella di un database
diventa troppo grande e causa scarse prestazioni, puoi suddividerla manualmente in più
tabelle nuove per poi riunirle in una vista il cui nome è il nome originale della tabella. Le nuove
tabelle possono anche trovarsi in database separati o in server separati. Se devi ristrutturare
una tabella e non puoi modificare un'applicazione che la utilizza, puoi creare una vista che
restituisce la struttura originale all'applicazione. Un'applicazione utilizza la vista senza sapere
che si tratta di una relazione virtuale. Tuttavia, le viste non possono fornire un'indipendenza
dei dati totale. Se non puoi nascondere a un'applicazione tutte le modifiche alla struttura di
una tabella con una vista, devi modificare anche l'applicazione. Ad esempio, potresti dover
aggiungere un attributo che deve essere inserito manualmente dagli utenti finali.
Capitolo 3
Il modello relazionale
89
Questo concetto di interscambiabilità può essere ulteriormente esteso alle espressioni di
tabella, query che restituiscono relazioni all'interno di query esterne. Probabilmente conosci
già le tabelle derivate e le espressioni di tabella comuni; apprenderai come utilizzarle in modo
efficace nel capitolo 6.
Convenzioni dei nomi
Le convenzioni dei nomi aiutano a creare strutture più intuitive e a scrivere codice più chiaro.
La scelta della convenzione non è importante quanto la scelta di una convenzione e il relativo
utilizzo in modo coerente. La scelta della convenzione è solo una questione di storia, gusto,
limiti del sistema e così via. I progettisti dei database tendono ad appassionarsi molto alle
convenzioni dei nomi.
Per il significato delle relazioni si consiglia l'approccio predicato-proposizioni. Ad esempio,
viene ripetuta la proposizione già citata: "Il dipendente con l'ID numero 17 si chiama
Fernando, lavora nel reparto D1 ed è stato assunto il 19 luglio 2003". Si consiglia di poter
sempre ricreare i predicati e le proposizioni. Una tupla che rappresenta questa proposizione
viene scritta in una relazione con solo valori, ad esempio (17, Fernando, D1, 19-07-2003). È
semplice ricreare questa proposizione se il relativo predicato, ossia la struttura di tabella, ha
dei nomi significativi per la tabella stessa e per le colonne, ad esempio Employees(EmployeeId,
EmployeeName, DepartmentId, HireDate). Tuttavia, se la tabella e le colonne avessero i
nomi Tabella1(colonna1, colonna2, colonna3, colonna4) dovresti essere in grado di leggere il
database. In questo modo è più semplice stabilire se il database risolve in modo appropriato
il problema aziendale e se i dati sono conformi alle regole aziendali. Inoltre è più semplice
per un nuovo programmatore familiarizzare con la struttura del database, e l'attività di
interscambio dei dati con altri sistemi viene resa molto semplice.
L'unica convenzione dei nomi che non viene consigliata per un database relazionale è quella
denominata notazione ungherese, in cui si usano i prefissi per denotare i tipi di oggetto.
La notazione ungherese usa nomi come tblEmployees per una tabella di dipendenti e
vwCustomerOrders per una vista di ordini dei clienti; tali nomi contraddicono il principio di
interscambiabilità che rappresenta il principio più importante del modello relazionale.
Il modello relazionale: riepilogo rapido
Il modello relazionale è indipendente dallo sfondo, ossia non dipende da un presupposto
specifico. Questo fatto verrà sottolineato più volte. Per prima cosa, il modello relazionale non
dipende dal tipo. Non esistono tipi "relazionali" prescritti o tipi "oltre il relazionale". Il modello
relazionale consente tutti i tipi. Infatti, è completamente valido per definire una relazione
con un singolo attributo di un tipo abbastanza complesso; si tratterebbe di una relazione
fortemente tipizzata. Tuttavia, i tipi forniti dal sistema sono in genere più semplici da utilizzare
perché gli sviluppatori del database conoscono già come usarli e, naturalmente, non devono
svilupparli da zero.
90
Microsoft Transact SQL Query
Per riepilogare, il modello relazionale è composto da:
■
Una raccolta infinita di tipi scalari
■
Un modo per definire i tipi; in altre parole un generatore di tipi
■
Un modo per definire i tipi di relazioni; in altre parole un generatore di tipi di relazioni
■
Un modo per generare variabili relazionali e assegnargli valori (set)
■
Algebra relazionale: una raccolta infinita di operatori relazionali
Le tabelle rappresentano le relazioni, e tutte le informazioni di un database relazionale
vengono archiviate nelle tabelle. Una relazione rappresenta un'entità del mondo reale. Inoltre,
le tuple di una relazione rappresentano le proposizioni, mentre un'intestazione di relazione
rappresenta un predicato.
Il modello relazionale non dipende dalle convezioni dei nomi, perché come già detto non
dipende dello sfondo. Ciò significa che dipende da te usare una convenzione dei nomi
abbastanza descrittiva da rendere possibile ricreare i predicati e le proposizioni del database.
Algebra relazionale e calcoli relazionali
Per manipolare le relazioni (le variabili relazionali) hai bisogno di alcuni operatori. Le relazioni
e gli operatori sulle relazioni formano ciò che viene chiamato algebra relazionale. La raccolta
di operatori relazionali è infinita, ma alcuni operatori sono considerati di base. Benché gli
operatori di base siano in qualche modo intuitivi, verranno illustrati per completezza.
Operatori di base
Come per i tipi semplici, sono necessari almeno due operatori di tipo relazionale: uno per
archiviare un set di tuple in una variabile relazionale e uno per recuperare il valore della
variabile. Ciò corrisponde alle nozioni familiari di assegnazione e valutazione. L'operatore
relazionale di selezione (corrispondente alla valutazione) restituisce una tabella da una
variabile relazionale, mentre l'operatore relazionale di assegnazione assegna un valore di
tabella a una variabile relazionale.
Ovviamente è necessario anche un insieme di operatori booleani di base per relazioni e tuple:
■
= (uguale)
■
≠ (non uguale)
■
⊆ (subset di)
■
⊇ (superset di)
■
∈ (elemento di)
■
=∅ (è vuoto)
Capitolo 3
Il modello relazionale
91
I primi quattro operatori elencati accettano due relazioni come parametri. Il quinto verifica
se una tupla è un membro della relazione, in altre parole accetta una tupla come parametro
di sinistra e una relazione come parametro di destra. Infine, l'ultimo operatore dell'elenco
accetta un'unica relazione come parametro e verifica se è vuota. Se lo desideri puoi definire
gli operatori di addizione per convenienza, ad esempio subset esatto di (ossia subset di e non
uguale a) e superset esatto di (superset di e non uguale a). Si desidera citare un altro operatore
specifico che aiuta molto con la presentazione tabulare di una relazione, l'operatore Order By
<attributo_1,attributo_2,. . .,attributo_n>.
L'operatore Order By non restituisce un risultato non ordinato; quindi non restituisce un set o
una relazione, che sono non ordinati. Puoi considerare il valore restituito dell'operatore Order
By come una tabella ordinata. L'ordinamento tuttavia non è predefinito per relazioni e tuple;
quindi il supporto dell'operatore Order By per una particolare relazione richiede che almeno
un attributo della relazione supporti l'ordinamento e i seguenti operatori:
■
> (maggiore di)
■
≥ (maggiore o uguale a)
■
≤ (minore o uguale a)
■
< (minore di)
La tabella restituita dall'operatore Order By viene ordinata in base ai valori di uno o più
attributi, che devono essere tutti del tipo di dati che supporta gli operatori elencati.
Algebra relazionale
L'algebra relazionale è una raccolta di operatori che accettano relazioni come parametri di
input e restituiscono relazioni. Il fatto che il risultato di ogni operazione relazionale sia una
relazione viene definito come proprietà di chiusura relazionale dell'algebra relazionale. Codd
originariamente ha definito otto operatori relazionali, quattro basati sugli operatori sui set
tradizionali e quattro su speciali operatori relazionali. Questi otto sono Restrict, Project,
Product, Union, Intersect, Minus, Join e Divide.
L'algebra relazionale non è chiusa; puoi definire ulteriori operatori, purché rispettino la
proprietà di chiusura relazionale. Saranno introdotti una serie di operatori utili oltre agli otto
originali di Codd. Naturalmente, dal momento che la raccolta di operatori relazionali è infinita,
l'elenco non sarà completo. Sono stati deliberatamente scelti gli operatori considerati più utili
e che vengono usati nel linguaggio Transact-SQL più avanti nel libro.
Gli otto operatori originali di Codd
L'operatore Restrict filtra le tuple di una relazione. Il risultato di questo operatore è una
relazione con un numero inferiore (o uguale) di tuple rispetto alla relazione originale. Il tipo
principale di relazione restituito è lo stesso del tipo principale della relazione originale. La
restrizione si basa su un'espressione booleana (denominata espressione di restrizione) che
confronta i valori degli attributi con valori letterali, variabili, altri attributi o espressioni.
92
Microsoft Transact SQL Query
La relazione di output dell'operatore Restrict contiene esattamente le tuple della relazione
originale per cui l'espressione di restrizione ha restituito True.
L'operatore Restrict filtra una relazione orizzontalmente, a differenza dell'operatore Project
che filtra una relazione verticalmente. L'operatore Project è molto più semplice: oltre a una
relazione, questo operatore accetta come input un elenco di attributi necessari per la relazione
risultante. Nota che la proiezione corretta dovrebbe includere solo tuple univoche, altrimenti il
risultato non sarebbe una relazione.
Tuttavia, RDMBS non applica questa regola perché è più pratico consentire un multiset (o un
contenitore) come risultato, per inviarlo direttamente a un'applicazione client o per archiviarlo
temporaneamente.
Nella figura 3-1 vengono illustrati graficamente gli operatori Restrict e Project. Immagina che
il rettangolo di destra che mostra l'operatore Project rappresenti la relazione Employees, con
gli attributi ID, Name, HireDate, DepartmentID e BirthDate. L'operatore Project restituisce una
relazione con ID, HireDate e DepartmentID come relativi attributi, che vengono indicati nella
figura dal colore più scuro.
Restrict
Project
FIGURA 3-1 Operatori Restrict e Project
L'operatore Product si basa sul prodotto cartesiano della matematica. Hai già appreso dal
capitolo 2 che il prodotto cartesiano di due set è un set di coppie ordinate (x,y), dove x deriva
dal primo set e y dal secondo. Tuttavia, nel modello relazionale, le tuple non sono ordinate e
l'operatore Product deve rispettare la proprietà di chiusura relazionale e restituire un set di
tuple non ordinate, non un set di coppie ordinate.
Quindi, nell'algebra relazionale, l'operatore Product viene generalizzato. Invece di restituire
coppie ordinate (x,y) di tuple (dove x è una tupla del primo input di Product e y una tupla
del secondo), l'operatore Product restituisce tuple che sono l'unione delle due tuple originali.
Unione in questo caso viene usato nel senso della teoria dei set; significa che la tupla finale ha
come relativi attributi l'unione degli attributi delle due tuple originali.
Unione significa naturalmente unione distinta, e quindi se un attributo compare in entrambe
le relazioni di input, viene mantenuta solo un'occorrenza nell'output dell'operatore Product.
Capitolo 3
Il modello relazionale
93
Cosa succede se le due relazioni originali includono un attributo con lo stesso nome e desideri
mantenere entrambi? Chiaramente, l'operatore Product non è completo; è necessario un
operatore che consenta la ridenominazione di un attributo. Tale operatore non fa parte
dell'algebra originale di Codd, quindi verrà introdotto dopo questa sezione che riguarda
invece gli otto operatori originali.
L'operatore relazionale Union si basa sull'operatore di set Union. Tuttavia, l'operatore
relazionale Union differisce nuovamente dalla controparte matematica per la proprietà di
chiusura relazionale dell'algebra relazionale.
Dal momento che il risultato deve essere una relazione e una relazione può avere tuple solo
di un tipo di intestazione, l'unione relazionale deve essere limitata a relazioni di input dello
stesso tipo o proiettare implicitamente ogni relazione di input negli attributi che sono comuni
a entrambe le relazioni di input.
Nella figura 3-2 vengono illustrati gli operatori Product e Union. Per l'operatore Union, una
proiezione di ognuna delle due relazioni viene usata per limitare l'unione solo agli attributi che
le relazioni hanno in comune. Il risultato di Union ha lo stesso tipo di intestazione per entrambi
gli input (o le relative proiezioni negli attributi comuni) e contiene tuple distinte.
Product
Union
a
x
a x
b
y
a y
c
b x
b y
c x
c y
FIGURA 3-2 Operatori Product e Union
L'operatore relazionale Intersect si basa, analogamente all'operatore relazionale Union,
sull'operatore sui set Intersect, e come Union ha la restrizione che gli operandi (relazioni) siano
dello stesso tipo o che venga preapplicata una proiezione implicita agli operandi. Il risultato
è un set di tuple distinte che compaiono in entrambe le relazioni di input (o nelle relative
proiezioni sugli attributi comuni).
Un altro operatore relazionale, Minus (o Difference), si basa sull'operatore equivalente della
teoria dei set, di nuovo con una proiezione nota per fare in modo che gli operandi abbiano lo
stesso tipo. Il risultato dell'operatore relazionale Minus è una relazione che include solo tuple
94
Microsoft Transact SQL Query
dell'operando di sinistra che non compaiono nell'operando di destra. Nella figura 3-3 vengono
illustrati gli operatori Intersect e Minus.
Intersect
Minus
FIGURA 3-3 Operatori Intersect e Minus
Esistono molte varietà dell'operatore relazionale Join, tuttavia la più importante è l'operatore
Natural Join, illustrato nella figura 3-4. Natural Join ha bisogno di due relazioni con almeno
un attributo in comune; il risultato è una relazione con tuple per cui gli attributi in comune
hanno valori uguali. Questi attributi in comune provengono solo da una delle relazioni unite in
join e dall'unione degli altri attributi di entrambe le relazioni. Union viene usato nuovamente
nel senso della teoria dei set, quindi un'unione di attributi distinti di relazioni originali. Come
l'operatore Product, l'operatore Join sarebbe molto più utile con un operatore che consentisse
la ridenominazione di un attributo. Come affermato, nella figura 3-4 viene illustrato l'operatore
Natural Join. Immagina che la relazione di input di sinistra sia la relazione Employees con
gli attributi numero ID dipendente e numero Department ID e che la relazione di input di
destra sia la relazione Departments con gli attributi numero Department ID e Department
Name. L'operatore Natural Join utilizza l'attributo comune numero Department ID per
trovare corrispondenza tra i dipendenti e i relativi reparti in base all'uguaglianza del numero
Department ID. Nota che nella relazione risultante il numero Department ID compare solo
una volta. Inoltre, il risultato contiene solo tuple che derivano da una corrispondenza basata
sui numeri Department ID di entrambe le relazioni di input. Infine, nota anche che un singolo
reparto (y2 nella figura 3-4) ha corrispondenza con più di un dipendente.
Non tutti i join sono naturali, e non tutti i join sono basati sull'operatore di uguaglianza.
I join generali (join che non usano necessariamente l'operatore di uguaglianza come
condizione di corrispondenza per le tuple) vengono definiti Θ (theta) join. Se l'operatore per
la corrispondenza delle tuple è l'operatore di uguaglianza, allora il join viene definito equi-join.
Un join naturale è semplicemente un caso speciale di equi-join.
Capitolo 3
Il modello relazionale
95
Natural Join
x1
y1
y1
z1
x1
y1
z1
x2
y2
y2
z2
x2
y2
z2
x3
y3
y3
z3
x3
y3
z3
y4
z4
FIGURA 3-4 Operatore Natural Join
Probabilmente l'operatore relazionale meno compreso è Divide. Una relazione divisore viene
utilizzata per dividere una relazione dividendo e generare una relazione quoziente. La relazione
quoziente è composta dai valori di una colonna della tabella dividendo per cui la seconda
colonna contiene tutti i valori nel divisore.
Benché questo sia un capitolo teorico, verrà utilizzato del codice per spiegare l'operatore
Divide e un problema che potresti incontrare se esegui la divisione con un set vuoto, quindi
un problema di divisione per zero. Verrà utilizzato un esempio che hai visto nel capitolo 2 e
che vedrai ancora nel capitolo 6. Il problema, che fa riferimento al database InsideTSQç2008,
chiede di restituire tutti i clienti per cui ogni dipendente degli USA ha gestito almeno un
ordine. In questo caso, dividi il set di tutti gli ordini per il set di tutti i dipendenti USA, per cui
ti aspetti di ottenere il set di clienti corrispondenti. T-SQL non ha un operatore Divide. Per
illustrarlo, il problema verrà riscritto come compare nel capitolo 6:
Restituire i clienti
per cui non puoi trovare
alcun dipendente
degli USA
per cui non puoi trovare
alcun ordine
inserito per il cliente
e dal dipendente
La query per questo problema è piuttosto intuitiva:
USE InsideTSQL2008;
SELECT custid FROM Sales.Customers AS C
WHERE NOT EXISTS
(SELECT * FROM HR.Employees AS E
WHERE country = N'USA'
AND NOT EXISTS
(SELECT * FROM Sales.Orders AS O
WHERE O.custid = C.custid
AND O.empid = E.empid));
Questa query restituisce 23 righe, quindi significa che ci sono 23 clienti per cui ogni
dipendente USA ha gestito almeno un ordine. La domanda verrà ora posta con un paese
diverso: quanti clienti ci sono per cui ogni dipendente di Israele abbia gestito almeno un
ordine?
96
Microsoft Transact SQL Query
Di seguito viene riportata la stessa query con un parametro modificato:
SELECT custid FROM Sales.Customers AS C
WHERE NOT EXISTS
(SELECT * FROM HR.Employees AS E
WHERE country = N'IL'
AND NOT EXISTS
(SELECT * FROM Sales.Orders AS O
WHERE O.custid = C.custid
AND O.empid = E.empid));
Questa query restituisce 91 righe, che rappresentano tutti i clienti. Questo non può
essere il risultato atteso, dal momento che non esistono dipendenti di Israele nella tabella
HR.Employees. Questo è il modo in cui l'operatore Divide è stato definito originariamente.
Dal momento che la tabella HR.Employees non ha dipendenti di Israele, la condizione che un
cliente sia stato servito da tutti i dipendenti di Israele è vera per ogni cliente. In altre parole,
ogni cliente è stato servito da ogni dipendente di Israele. Tuttavia, è vero anche qualcos'altro:
ogni cliente non è stato servito dai dipendenti di Israele. Nota che non c'è una verità migliore
in questo caso; quella che scegli dipende dal problema che stai risolvendo. In questo caso
si verifica qualcosa di simile al paradosso di Russell (citato nel capitolo 2)? Non proprio. Il
problema è che non è stata considerata la possibilità di non avere dipendenti di Israele. Se
la domanda originale "clienti . . . per cui . . . almeno un ordine" fosse intesa a significare che di
fatto ci fossero degli ordini, è possibile dare una risposta semplicemente aggiungendo una
condizione al predicato che richiede di restituire i clienti serviti da tutti i dipendenti di Israele,
se esiste almeno un dipendente di Israele:
Restituire clienti
per cui non puoi trovare
alcun dipendente
di Israele
per cui non puoi trovare
alcun ordine
inserito dal cliente
e dal dipendente
se esiste almeno un dipendente di Israele
La query ora è uguale alla seguente:
SELECT custid FROM Sales.Customers AS C
WHERE
NOT EXISTS
(SELECT * FROM HR.Employees AS E
WHERE country = N'IL'
AND NOT EXISTS
(SELECT * FROM Sales.Orders AS O
WHERE O.custid = C.custid
AND O.empid = E.empid))
AND EXISTS
(SELECT * FROM HR.Employees AS E
WHERE country = N'IL');
Capitolo 3
Il modello relazionale
97
Questa query restituisce zero righe, come ti saresti aspettato quando hai originariamente fatto
la domanda. La formula per l'operatore Divide include tre relazioni:
a Divide By b Per c,
dove a è il dividendo, b il divisore e c è la relazione mediatore. La relazione a ha gli attributi
A e la relazione b gli attributi B. L'operatore Divide restituisce una relazione che include tutte
le tuple del divisore così che una tupla {A, B} compaia nella relazione mediatore per tutte le
tuple della relazione divisore. Negli esempi mostrati, il dividendo è la relazione Customers, il
divisore è la relazione che include dipendenti di un paese specifico (USA o Israele negli esempi)
e il mediatore è la relazione Orders. Tuttavia, per evitare il problema della divisione per zero
è stata usata una quarta relazione temporanea (SELECT * FROM HR.Employees AS E WHERE
country = N’IL’). Puoi esprimere il predicato che richiede di restituire i clienti serviti da tutti
i dipendenti degli USA se esiste almeno un dipendente degli USA anche in un altro modo,
ossia cercando clienti diversi (rappresentati con custid) dagli ordini serviti dai dipendenti USA
avendo il numero di diversi dipendenti USA che hanno servito un cliente uguale al numero
totale di dipendenti degli USA (troverai questo esempio anche nel capitolo 6):
SELECT custid
FROM Sales.Orders
WHERE empid IN
(SELECT empid FROM HR.Employees
WHERE country = N'USA')
GROUP BY custid
HAVING COUNT(DISTINCT empid) =
(SELECT COUNT(*) FROM HR.Employees
WHERE country = N'USA');
Questa query restituisce il risultato per la seconda versione della divisione sia per i dipendenti
USA sia di Israele e inoltre è molto più corta. Per concludere con gli otto operatori originali di
algebra relazionale, nella figura 3-5 viene illustrato graficamente l'operatore Divide esteso (con
la relazione mediatore).
Divide
a
a x
x
b
a y
z
c
a z
a
b x
b z
FIGURA 3-5 Operatore Divide esteso
98
Microsoft Transact SQL Query
Altri operatori di algebra relazionale
Come già detto, l'algebra relazionale ha una serie infinita di operatori, ma qui ci si focalizzerà
su quelli più utili.
È già stato detto quanto sia utile l'operatore Rename. Senza, tutti gli operatori che accettano
più di una relazione come parametri sarebbero molto limitati. L'operatore Rename assegna un
alias a un attributo o a una relazione in una query. Nota che è molto pratico avere alias per le
relazioni oltre che per gli attributi, perché una singola query può fare riferimento alla stessa
relazione più di una volta.
Un linguaggio che supporta l'algebra relazionale viene detto completo dal punto di vista
relazionale; tuttavia, ciò non significa che sia anche completo dal punto di vista del calcolo. Non
è stato ancora introdotto un operatore che restituisce un attributo calcolato nella relazione
risultante. L'operatore Extend è l'operatore che aggiunge un'espressione denominata (che
restituisce un valore scalare) alla relazione risultante. Nota che questa espressione non è
limitata ai calcoli tra attributi solo di una singola tupla; l'espressione può lavorare anche su più
tuple se aggrega più valori di input a un singolo valore di output. Nella figura 3-6 vengono
illustrati gli operatori Rename ed Extend, con attributi con alias e aggiunti di colore più scuro.
Rename
Extend
A
B
A
C
x1
y1
x1
y1
x1
y2
x1
y2
x2
y1
x2
y1
FIGURA 3-6 Operatori Rename ed Extend
L'operatore Extend esegue calcoli orizzontali, o nel senso della tupla. È necessario un operatore
per i calcoli verticali, o nel senso dell'attributo. L'operatore che esegue i calcoli verticali è
Summarize (illustrato nella figura 3-7), che combina una proiezione sugli attributi su cui viene
fatto un calcolo verticale con un'estensione della relazione risultante per includere i calcoli di
aggregazione.
I semi join sono join che restituiscono tuple di una relazione in base all'esistenza di tuple
correlate in un'altra relazione. L'operatore di sinistra Semijoin (illustrato nella figura 3-8)
restituisce tuple della relazione di sinistra, mentre l'operatore Semijoin di destra restituisce le
tuple della relazione di destra.
Capitolo 3
Il modello relazionale
99
Summarize
x1
y1
z1
x1
f (z1,z2)
x1
y2
z2
x2
f (z3,z4)
x2
y1
z3
x2
y2
z4
FIGURA 3-7 Operatore Summarize
Semijoin
x1
y1
y1
z1
x1
y1
x2
y2
y2
z2
x2
y2
x3
y3
y3
z3
x3
y3
x4
y4
FIGURA 3-8 Operatore Semijoin (di sinistra)
La teoria dei grafi è una delle teorie più utili in matematica. È stata sviluppata da Leonhard
Euler mentre studiava un famoso problema storico di matematica denominato i sette ponti di
Königsberg. Di seguito viene riportata una breve descrizione del problema tratta da Wikipedia:
La città di Königsberg, (ora Kaliningrad) è percorsa dal fiume Pregel e da suoi
affluenti e presenta due estese isole che sono connesse tra di loro e con le due aree
principali della città da sette ponti.
La questione è se sia possibile con una passeggiata seguire un percorso che
attraversa ogni ponte una e una volta sola.
Nella teoria dei grafi un grafo è un insieme di elementi (denominati nodi o vertici) e
connessioni (denominate spigoli) tra coppie di elementi. I nodi sono elementi statici astratti,
mentre gli spigoli possono rappresentare associazioni o relazioni tra i nodi. Un sistema
stradale, ad esempio, può essere rappresentato con un grafo: le città sono i nodi, mentre le
strade sono gli spigoli. Gli alberi e le gerarchie sono casi speciali si grafi. In una relazione in
genere si modella un grafo con il modello elenco adiacenza. In questo modello si considerano
gli spigoli del grafo come spigoli diretti da un vertice all'altro, e tali spigoli vengono
rappresentati come tuple. I nodi connessi tramite uno spigolo (che può essere visto come
adiacente in virtù dello spigolo che li connette) sono rappresentati dagli attributi della tupla
spigolo. Vengono rappresentati solo i nodi con una connessione. Il problema con il modello
elenco adiacenza si presenta quando devi eseguire una query. Ad esempio, se devi trovare
tutti i percorsi possibili dalla città A alla città B nel sistema stradale, la query deve includere
qualche tipo di ciclo (il ciclo può essere nascosto in un'espressione di tabella ricorsiva comune,
ma resta sempre un ciclo). Per rendere queste query più veloci e semplici, si può usare un
nuovo operatore relazionale, TClose. Questo operatore unario restituisce la chiusura transitiva
100
Microsoft Transact SQL Query
della relazione originale. Il risultato è una relazione con lo stesso tipo principale della relazione
originale, ma include tuple di tutte le coppie di nodi con percorsi non interrotti tra loro.
L'esecuzione di query su una relazione risultante di questo tipo è molto più semplice. Nel
capitolo 12 apprenderai altre informazioni sui grafi, gli alberi, le gerarchie e anche su come
calcolare la chiusura transitiva di un grafo. Per ora osserva semplicemente la rappresentazione
grafica dell'operatore nella figura 3-9.
TClose
a
b
c
d
a c
a c
b c
b c
b d
b d
c e
c e
c f
c f
e f
e f
e
f
a e
b e
a f
b f
FIGURA 3-9 Operatore TClose (di sinistra)
Per completezza saranno illustrati altri due operatori ben conosciuti che hanno a che fare
con le relazioni con i dati temporali: Unpack e Pack. Anche se questo libro non tratta di
problemi temporali, molti libri lo fanno, ad esempio Inside Microsoft SQL Server 2008: T-SQL
Programming di Itzik Ben-gan e altri. (Microsoft Press, 2009).
Immagina che ogni tupla in una relazione abbia un attributo che rappresenta l'intervallo di
tempo per cui è valida una tupla. Pensa di avere un tipo intervallo di tempo nella raccolta dei
tipi, definito dal sistema o definito dall'utente. Una tupla con un simile intervallo di validità può
essere simile al seguente:
{A, d4:d6}
Senza definire esplicitamente l'intestazione di questa tupla, presumi che la proposizione qui
dica che il fornitore A sia sotto contratto (un fornitore valido) per il periodo che va dal punto
nel tempo d4 al punto nel tempo d6 e che i punti nel tempo siano distinti: d1, d2, d3 e così via,
come i giorni del calendario, ad esempio. Puoi avere anche altre tuple per lo stesso fornitore,
ad esempio:
{A, d5:d7}
{A, d8:d8}
Capitolo 3
Il modello relazionale
101
Qui le tre tuple per il fornitore A hanno intervalli di validità che si sovrappongono e sono
adiacenti. Come puoi trovare il numero di punti nel tempo distinti in cui il fornitore A era
sotto contratto? Come puoi combinare le tuple con intervalli adiacenti e sovrapposti in una
singola tupla che rappresenta che il fornitore A era sotto contratto in modo continuato per un
intervallo più lungo senza interruzioni?
Bisogna definire un operatore Unpack come operatore relazionale unario che restituisce una
relazione con tutti i punti nel tempo distinti e validi proiettati su un set di operatori di input,
nel modo in cui l'operatore Summarize proietta sugli attributi di input. Tuttavia, Unpack
fa l'opposto di Summarize in termini di tuple restituite; la relazione restituita viene estesa
per includere tuple per tutti i punti nel tempo validi e distinti. Nel caso delle proposizioni
dell'esempio, il solo attributo di input per cui i punti nel tempo possono essere decompressi è
il fornitore. L'operatore Pack esegue l'opposto: restituisce una relazione con attributi di input
per cui gli intervalli sono compressi e gli intervalli sono un'unione di tutti gli intervalli delle
tuple di origine per gli stessi attributi di input che si sovrappongono o sono adiacenti. Nota
che unione in questo caso non è l'operatore relazionale Union; è un'unione di intervalli, definita
solo per gli intervalli che si sovrappongono o sono adiacenti. Nella figura 3-10 vengono
illustrati graficamente gli operatori Unpack e Pack.
Unpack
Pack
a
d4 : d6
a
d4 : d4
a
d4 : d6
a
d4 : d8
a
d5 : d7
a
d5 : d5
a
d5 : d7
b
d2 : d4
a
d8 : d8
a
d6 : d6
a
d8 : d8
b
d7 : d7
b
d2 : d4
a
d7 : d7
b
d2 : d4
b
d7 : d7
a
d8 : d8
b
d7 : d7
b
d2 : d2
b
d3 : d3
b
d4 : d4
b
d7 : d7
FIGURA 3-10 Operatori Unpack e Pack
Operatori di algebra relazionale primitivi
Forse avrai già notato che molti degli operatori relazionali definiti fino ad ora possono essere
espressi con altri operatori relazionali. Infatti, la maggior parte degli operatori citati fino ad ora
sono solo collegamenti che rendono le espressioni relazionali più semplici e brevi.
102
Microsoft Transact SQL Query
Infatti, persino gli otto operatori originali di Codd non sono tutti primitivi; alcuni possono
essere espressi con altri. Il componente Query Optimizer RDBMS può utilizzare questo aspetto
nell'ottimizzazione di una query; può riscrivere una query al relativo equivalente logico usando
operatori diversi, che possono essere implementati con operatori fisici più veloci rispetto agli
operatori relazionali in un RDBMS specifico. Ad esempio, potresti notare che a volte SQL Server
utilizza l'operatore fisico Merge Join quando usi l'operatore logico (relazionale) Union.
Nota inoltre che gli operatori relazionali che si basano su operatori sui set sono diversi dagli
operatori sui set originali.
Calcolo relazionale
L'algebra relazionale fornisce una raccolta infinita di operatori relazionali che vengono utilizzati
per creare la relazione desiderata che risulta da una query; servono per stabilire un sistema per
ottenere la relazione risultante. L'algebra relazionale è prescrittiva. Il calcolo relazionale è un
modo alternativo per ottenere la relazione risultante desiderata da un sistema. Con il calcolo
relazionale descrivi la relazione risultante. Quindi il calcolo relazionale è descrittivo.
Come puoi descrivere la relazione risultante di cui hai bisogno? Ancora una volta usando i
predicati. Descrivi la relazione risultante con un predicato soggetto a vincoli. Ad esempio,
quando è stato descritto l'operatore relazionale Divide, è stato usato il calcolo relazionale per
introdurre il problema: restituire tutti i clienti per cui ogni dipendente degli USA abbia gestito
almeno un ordine. La descrizione più dettagliata è la seguente:
Restituire i clienti
per cui non puoi trovare
alcun dipendente
degli USA
per cui non puoi trovare
alcun ordine
inserito per il cliente
e dal dipendente
Esistono due tipi di calcolo relazionale: calcolo di tuple e calcolo di dominio. Nel calcolo di tuple
specifichi il risultato di una query descrivendo le condizioni di appartenenza alla tupla per la
relazione risultante. Nel calcolo di dominio specifichi la relazione risultante limitando i domini
di attributi. Benché esista una netta differenza matematica tra calcolo di tuple e calcolo di
dominio, per lo scopo di questo libro tale differenza può essere considerata una sfumatura. La
differenza era importante in passato perché diversi linguaggi, seri competitori di SQL, si sono
evoluti basandosi sul calcolo di tuple e dominio. Per il calcolo di tuple è stato sviluppato QUEL
(Query Language), mentre il calcolo di dominio era supportato da QBE (Query by Example).
Per spiegare la differenza tra l'algebra relazionale e il calcolo relazionale, è opportuno usare
un esempio. Immagina due relazioni: Customers con attributi CustomerId, CustomerName e
City, e Orders con attributi OrderId, CustomerId e OrderDate. La query che stai risolvendo è
"Ottieni i valori di attributo CustomerId e CustomerName dei diversi clienti di Parigi che hanno
inserito almeno un ordine". Una formula algebrica e prescrittiva della query potrebbe essere la
seguente:
Capitolo 3
Il modello relazionale
103
1. Unire in join Customers e Orders tramite CustomerId.
2. Limitare il risultato delle tuple alla città di Parigi.
3. Riepilogare il risultato tramite CustomerId e CustomerName per ottenere i clienti
distinti.
4. Proiettare il risultato tramite CustomerId e CustomerName.
Una formula di calcolo descrittiva della query potrebbe essere la seguente:
Restituire CustomerId e CustomerName per i clienti di Parigi per cui esiste qualche ordine.
La descrizione del risultato della query è molto simile nel calcolo di tuple e di dominio. In
entrambi i casi è inclusa una descrizione dell'intestazione risultante (denominata anche prototupla) e una descrizione dei vincoli in termini di predicato che usa un'espressione quantificata.
Nell'esempio, CustomerId e CustomerName definiscono la proto-tupla, l'intestazione della
relazione risultante. Il predicato nell'esempio usa l'istruzione quantificata dal punto di
vista dell'esistenza "clienti di Parigi per cui esiste qualche ordine". La parola esiste indica la
quantificazione. Crei il predicato combinando espressioni logiche usando gli operatori logici
standard ¬ (Not), ∧ (And) e ∨ (Or). Inoltre, le espressioni quantificate sono necessarie per il
calcolo relazionale. Quindi il quantificatore di esistenza ∃ (Exists) e il quantificatore universale
∀ (For all) sono una parte indispensabile del calcolo relazionale.
SQL consente di esprimere il risultato desiderato di una query quasi in un linguaggio umano.
Supporta sia gli operatori logici che i quantificatori. Itzik ha affermato più volte in questo libro
che alcuni problemi sono più semplici da risolvere quando vengono rifrasati con un predicato
diverso o vengono analizzati usando la logica inversa. Ora puoi notare che questo spesso
significa che di fatto usi il calcolo relazionale.
Il calcolo relazionale e l'algebra relazionale sono equivalenti; hanno entrambi la stessa
espressività. Quindi dipende da te scegliere il modo migliore per esprimere la relazione
risultante desiderata; esprimere una query (usando l'algebra relazionale o il calcolo relazionale)
e capire il significato di una relazione (entità o predicato e proposizioni) sono simili.
Supporto di T-SQL
È stato detto che SQL non è l'unico linguaggio usato per la manipolazione delle relazioni.
In effetti il modello relazionale non dipende dal linguaggio; questo è un altro aspetto di
indipendenza di base del modello relazionale. SQL è solo uno dei linguaggi possibili. Tuttavia,
c'è uno standard ANSI esistente per SQL. E anche se non è perfetto, SQL è il linguaggio più
usato comunemente per la manipolazione delle relazioni. Transact-SQL (T-SQL) è il dialetto
dello standard SQL di SQL Server.
T-SQL supporta la maggior parte degli operatori dell'algebra relazionale. Puoi manipolare le
relazioni con istruzioni DML (Data Manipulation Language), quindi SELECT, INSERT, UPDATE,
DELETE e MERGE. L'operatore Product viene espresso con CROSS JOIN. L'operatore Restrict
viene supportato nelle clausole WHERE e HAVING e implicitamente nella clausola ON di
104
Microsoft Transact SQL Query
un'operazione JOIN se il join non è un CROSS JOIN, in quanto gli altri join filtrano il risultato
di un CROSS JOIN. L'operatore Project viene supportato nella parte SELECT di una query, in
cui elenchi gli attributi in modo esplicito. Gli operatori relazionali Union, Intersect e Minus
hanno le relative controparti negli operatori T-SQL UNION, INTERSECT ed EXCEPT. Tutti i tipi
di operatori Join, theta join, equi-join, semi-join e natural join, sono supportati dall'operatore
JOIN. L'operatore Rename viene espresso in T-SQL con la clausola AS, che può comparire
nell'elenco SELECT di una query per la ridenominazione degli attributi e nella parte FROM di
una query per la ridenominazione delle relazioni. L'operatore Extend viene espresso nell'elenco
SELECT, che può includere espressioni di calcolo denominate oltre agli attributi originali.
L'operatore Summarize diventa in T-SQL la clausola GROUP BY. Gli operatori relazionali Divide,
TClose, Unpack e Pack non hanno operatori T-SQL direttamente equivalenti.
Il calcolo relazionale viene supportato tramite la parte SELECT di una query, in cui descrivi la
proto-tupla, e nelle clausole WHERE e HAVING, in cui vincoli la relazione risultante con un
predicato. Naturalmente, T-SQL supporta tutti gli operatori logici standard: ¬ (Not), ∧ (And)
e ∨ (Or) e sia il quantificatore di esistenza ∃ (Exists) sia il quantificatore universale ∀ (For all)
nelle espressioni che vincolano la relazione risultante.
Dopo tutte queste informazioni si può dire che T-SQL è completo dal punto di vista
relazionale.
Integrità dei dati
È già stato detto che l'integrità dei dati è fondamentale per un database relazionale. Di fatto,
le regole di integrità dei dati sono una parte importante di un database relazionale. Un RDBMS
deve applicare le regole. Rendendo le regole parte del database, informi il sistema di quali
regole si tratta. Con i vincoli dichiarativi, il modo in cui vengono applicate dipende dal sistema;
con il codice procedurale devi definire il modo in cui implementarle. In entrambi i casi i vincoli
vengono espressi in termini di predicati.
Le intestazioni delle relazioni, tabella fisica e definizioni di vista che includono definizioni del
tipo di attributo, insieme ai vincoli dichiarativi e procedurali, costituiscono lo schema di un
database. Ora è possibile riepilogare esattamente cos'è uno schema di database. Uno schema
di database rappresenta predicati soggetti a vincoli che descrivono uno scenario aziendale.
Puoi ottenere i predicati soggetti a vincoli dalle intestazioni delle relazioni e dai vincoli definiti
nel database. Un predicato di database può essere definito come un'aggregazione di tutti i
predicati di relazione e di vincolo. Le regole di integrità dei dati possono essere espresse con
una singola regola: nel database non deve mai esserci un valore che viola il relativo predicato
soggetto a vincoli.
I vincoli possono essere classificati in vincoli base che definiscono l'integrità di entità, di
dominio e referenziale e le regole aziendali. Le regole di integrità di base si possono esprimere
con vincoli dichiarativi. La maggior parte delle regole aziendali hanno bisogno di codice di
programmazione in SQL Server. Le regole aziendali possono essere di qualunque tipo, ad
esempio regole di cardinalità o di frequenza (quante tuple possono esistere in una relazione in
un qualunque momento), regole di derivazione dei dati (come calcolare lo stato degli eventi),
Capitolo 3
Il modello relazionale
105
regole di subset (una relazione può avere un subset di tuple solo di un'altra relazione), regole
di inclusione (un periodo in cui un fornitore ha fornito un prodotto deve essere incluso in un
periodo in cui il fornitore aveva un contratto valido), regole di processo (quale evento deve
verificarsi prima) e altre ancora. Dipende dal creatore del database e dell'applicazione decidere
dove implementare le regole. Si consiglia di avere almeno i vincoli dichiarativi nel database
relazionale. Dopo tutto, se non li usi, perché usare un RDBMS?
I vincoli possono essere classificati anche in altri modi. Ad esempio, possono essere classificati
in base al tipo di oggetto che vincolano: vincoli di tipo, attributo, relazione e database.
Possono inoltre essere classificati come immediati o posticipati, in base a quando vengono
applicati: immediatamente o alla fine della transazione corrente. Nota che in base alla regola
che "in un database non deve mai esserci un valore che viola il relativo predicato soggetto a
vincoli", all'interno di un database relazionale dovrebbero funzionare solo i vincoli immediati.
Ciò significa che i vincoli devono essere applicati al limite di un'unica istruzione DML, non
alla fine di una transazione o anche dopo. In un RDBMS una singola istruzione DML viene
trattata come un'operazione atomica anche se modifica più righe; quindi, durante l'esecuzione
dell'istruzione ottieni righe che violano alcuni vincoli, ma mai dopo che è terminata l'istruzione.
Nota che i vincoli immediati non possono garantire che un database rifletta sempre uno stato
valido degli affari nell'ambiente reale. Ad esempio, anche se il trasferimento di denaro da un
account a un altro viene inteso come operazione atomica, include due aggiornamenti in un
database. Entrambi gli aggiornamenti devono terminare correttamente, altrimenti non ne
viene eseguito nessuno. Quindi, è chiaramente necessario qualche altro mezzo per rendere
i database sempre coerenti con il mondo reale. Ciò può essere ottenuto con le transazioni.
Una transazione è un'unità logica di lavoro che estende una nozione di atomicità a livello di
istruzione. Benché le transazioni svolgano un ruolo importante in un RDBMS, non verranno
illustrate nel dettaglio; per ulteriori informazioni in merito, leggi Inside Microsoft SQL Server
2008: T-SQL Programming.
SQL standard di ANSI consente i vincoli posticipati. SQL Server non li implementa. Tuttavia,
possono essere implementati nel codice procedurale per verifiche e ricerche avanzate di dati
errati. La correttezza è un termine più rigido di coerenza; un RDBMS può applicare la coerenza
dei dati ma non la correttezza. La coerenza significa che i dati rispettano le regole aziendali
dichiarate e note al sistema; la correttezza viene definita al di fuori del sistema dagli utenti del
sistema stesso.
Vincoli dichiarativi
Dal momento che i vincoli dichiarativi rappresentano il modo più importante per
implementare le regole aziendali in un database relazionale, verranno illustrati più nel
dettaglio rispetto agli altri vincoli.
Integrità dell'entità
Le tabelle di un database sono rappresentazioni fisiche delle relazioni, mentre le righe di una
tabella rappresentano le tuple; le relazioni sono composte da tuple univoche.
Questo è l'integrità dell'entità, identificazione univoca delle righe di una tabella.
106
Microsoft Transact SQL Query
Devi avere una combinazione di colonne (che rappresentano fisicamente gli attributi) che
identificano in modo univoco una riga. Il set di colonne minimo che identifica in modo univoco
ogni riga viene chiamato chiave. Ogni tabella può avere più combinazioni di colonne univoche,
in altre parole più chiavi candidate. Sei tu che devi selezionarne una come riferimento principale
per ogni riga e chiamarla chiave primaria. SQL Server ha due vincoli per l'integrità dell'entità: il
vincolo Unique per le chiavi candidate e il vincolo Primary Key per le chiavi primarie. Puoi avere
più vincoli Unique e un vincolo Primary Key per tabella.
Come sai ogni tabella deve avere una chiave. Sai inoltre che SQL Server non applica questa
regola: puoi creare una tabella senza un vincolo Primary Key o Unique. Il motivo è puramente
pratico. Immagina di dover importare i dati da un file di testo. Se avessi una chiave definita,
dovresti pulire i dati nel file di testo prima dell'importazione. Pulire i file di testo è meno
pratico che pulire i dati in una tabella SQL Server. Nonostante ciò, nell'ambiente di produzione,
tutte le tabelle devono avere una chiave definita per ogni tabella.
Ogni chiave ha due proprietà richieste e due desiderate (D. Sarka, 2008). Univocità e
applicabilità sono richieste; stabilità e minimalità sono desiderate. Univocità significa che la
chiave identifica ogni tupla in modo univoco. Applicabilità significa che la chiave deve essere
applicabile per tutte le tuple della relazione, deve essere nota e non deve essere composta
da attributi che non sono significativi per alcune tuple (consulta la sezione "Generalizzazione
e specializzazione" più avanti in questo capitolo). Stabilità significa che la chiave non deve
cambiare, se possibile. Minimalità significa che la chiave deve essere composta dal minor
numero possibile di colonne e dal minor numero possibile di byte. Nonostante ciò, a causa
dei problemi fisici, dovresti cercare chiavi con tutte e quattro le proprietà. Per verificare i
cambiamenti di un entità nel tempo, ad esempio negli scenari di data warehouse, la stabilità
diventa una proprietà necessaria. Mentre le chiavi minime forniscono prestazioni migliori.
Esiste un antico dibattito sulle chiavi e su quale siano le migliori: naturali o surrogate. Una
chiave naturale è un subset degli attributi che definiscono un'entità. Una chiave surrogata è
una chiave creata dal progettista e aggiunta agli attributi di un'entità; in genere è un numero
sequenziale. Non è possibile fare una netta distinzione tra chiavi naturali e surrogate. Un
Social Security ID (SSID) è una chiave naturale o surrogata? Qualcuno potrebbe aggiungerla
agli attributi dell'entità Person. Per dare una definizione di chiave naturale si potrebbe dire
che una chiave è naturale se l'attributo che rappresenta viene usato per l'identificazione
indipendentemente dal database. Se hai qualcosa di univoco, applicabile, stabile e breve nella
tabella, usalo. In caso contrario, aggiungi un numero sequenziale per il riferimento principale,
così da avere tutte le proprietà richieste e desiderate per la chiave primaria..
Se una chiave è applicabile, i relativi valori devono essere noti. SQL Server impone questa
regola proibendo alle colonne che consentono valori NULL la partecipazione nei vincoli
Primary Key; tuttavia, consente le colonne con valori NULL nei vincoli Unique. Più avanti in
questo capitolo verranno nuovamente ripresti i valori NUll, che rappresentano il segnaposto
per qualcosa di sconosciuto. Per ora si consiglia semplicemente di non usare colonne che
ammettono valori NULL per le chiavi.
Capitolo 3
Il modello relazionale
107
Integrità referenziale
Una chiave esterna è un set di colonne i cui valori corrispondono a qualche chiave di un'altra
tabella, in altre parole, una copia di una chiave di un'altra relazione. Le chiavi esterne denotano
le associazioni tra le relazioni, rappresentano il collante che tiene insieme le relazioni in un
database. L'applicazione della regole delle chiavi esterne può essere espressa brevemente:
In un database non devono mai esserci chiavi esterne senza corrispondenza. Le chiavi esterne
mantengono i riferimenti tra le relazioni, in altre parole applicano l'integrità referenziale.
La regola delle chiavi esterne può essere mantenuta durante le operazioni di aggiornamento
ed eliminazione in diversi modi. In SQL Server esistono quattro possibilità per applicare la
regole delle chiavi esterne, e ogni possibilità è composta da due coppie di regole. Una coppia
di regole riguarda la tabella principale (padre), l'altra coppia riguarda la tabella secondaria
(figlio). La coppia di regole per la tabella figlio è immutabile; le regole sono sempre le stesse
per tutte e quattro le possibilità di implementazione di una chiave esterna:
■
Non puoi inserire una riga nella tabella figlio se non ha una riga correlata nella tabella
padre.
■
Non puoi aggiornare le colonne della chiave esterna nella tabella figlio in un modo
che le lascerebbe senza una riga correlata nella tabella padre.
Le due regole per la tabella padre sono diverse per ognuna delle quattro possibili
implementazioni. Le quattro possibilità standard e l'implementazione delle due regole per la
tabella padre sono:
■
■
■
■
Implementazione di No action
❏
Non puoi eliminare una riga nella tabella padre se ha righe correlate nella
tabella figlio.
❏
Non puoi aggiornare le colonne chiave nella tabella padre se hanno righe
correlate nella tabella figlio che diventerebbero orfane.
Implementazione di Cascade
❏
Se elimini una riga nella tabella padre, devi eliminare tutte le righe correlate
nella tabella figlio.
❏
Se aggiorni la chiave primaria nella tabella padre, devi aggiornare le chiave
esterna in tutte le tabelle figlio correlate con lo stesso nuovo valore.
Implementazione di Set Null
❏
Se elimini una riga nella tabella padre, devi impostare su sconosciute (NULL)
tutte le chiavi esterne delle righe correlate nella tabella figlio.
❏
Se aggiorni una chiave primaria nella tabella padre, devi impostare su
sconosciute (NULL) tutte le chiavi esterne delle righe correlate nella tabella figlio.
Implementazione di Set Default
❏
Se elimini una riga nella tabella padre, devi impostare a un valore predefinito
tutte le chiavi esterne delle righe correlate nella tabella figlio.
108
Microsoft Transact SQL Query
❏
Se aggiorni una chiave primaria nella tabella padre, devi impostare a un valore
predefinito tutte le chiavi esterne delle righe correlate nella tabella figlio.
In breve, qualunque cosa tu faccia, non lasciare mai orfane le righe nella tabella figlio.
Normalmente usi l'implementazione No Action. L'implementazione Cascade la useresti per le
eliminazioni solo nel caso in cui volessi implementare una forte relazione tra la tabella padre e
la tabella figlio. In una relazione simile le righe della tabella figlio non avrebbero senso senza le
righe padre. Un esempio classico sono gli elementi ordini e riga ordine: gli elementi riga ordine
non possono esistere senza un ordine. Se elimini un ordine, devi eliminare anche tutti i relativi
elementi riga. Non si consiglia l'uso di Cascade per gli aggiornamenti. Cascade updates indica
che la chiave nella tabella padre non è stabile, e la stabilità è una delle proprietà desiderate per
una chiave. Le regole Set Null e Set Default sono utili per mantenere la cronologia della tabella
figlio; ad esempio, un ordine con un cliente sconosciuto fornisce informazioni su qualcosa che
è stato ordinato e quando è stato ordinato, ma non da chi. Nonostante ciò, oggi la cronologia
viene normalmente mantenuta in un data warehouse, quindi solitamente non sono necessarie
queste regole.
Un vincolo di chiave esterna deve fare riferimento a una chiave in una tabella padre. La tabella
padre può essere uguale alla tabella figlio; una chiave esterna può fare riferimento alla tabella
stessa. In questo modo puoi rappresentare grafi, alberi e gerarchie usando il modello elenco di
adiacenza.
Integrità del dominio
L'integrità del dominio limita il dominio dei possibili valori di un attributo. Naturalmente il tipo
di attributo già vincola i possibili valori di un attributo. Un altro modo standard per limitare il
dominio di un attributo in un database relazionale è il vincolo check.
Un vincolo check è un'espressione logica che restituisce true, false o unknown, è un altro
predicato. Un RDBMS lo applica ogni volta che una tupla viene inserita o aggiornata. I
valori di attributo della tupla sostituiscono i parametri del predicato, rendendo il predicato
una proposizione. Una tupla viene rifiutata se la proposizione restituisce false. La sintassi di
un'espressione vincolo check è simile alla sintassi delle espressioni in una clausola WHERE.
I vincoli check possono essere semplici come il controllo di un intervallo di valori. Tuttavia,
cosa puoi fare quando non conosci in anticipo l'intervallo consentito, quando devi mantenere i
valori nell'intervallo consentito in modo dinamico? Cosa fai quando l'elenco dei valori possibili
è molto lungo o addirittura infinito? Un'espressione vincolo check sarebbe composta da un
elenco enorme di valori collegati con operatori logici OR, inoltre dovresti modificare il vincolo
ogni volta che l'elenco dei valori possibili cambia. In tal caso, è più semplice utilizzare le tabelle
di ricerca. Colleghi l'attributo o gli attributi che stai vincolando a una tabella di ricerca con una
chiave esterna. Quindi, i vincoli di chiave esterna possono servire anche come meccanismi di
integrità del dominio.
Tutti i vincoli citati, chiavi, chiavi esterne e vincoli check, svolgono un ruolo importante
nell'ottimizzazione delle query. Forniscono informazioni a un RDBMS, e questo aiuta a
trovare un piano di esecuzione ottimale. Le chiavi forniscono informazioni che stai cercando
per un singolo valore; tale valore è univoco. Quindi la ricerca è molto ristretta, e il sistema
Capitolo 3
Il modello relazionale
109
può usare una ricerca indicizzata. Le chiavi esterne forniscono informazioni sull'esistenza di
una riga padre, aiutando a trovare l'algoritmo di join più efficace. I vincoli check forniscono
informazioni sull'intervallo, quindi (ad esempio) la ricerca di un valore che si trova all'esterno
dell'intervallo restituisce zero righe, e il sistema non deve neanche leggere i dati per restituire
il set di risultati corretto. Apprenderai ulteriori informazioni sull'ottimizzazione delle query nel
capitolo 4.
Altri strumenti per l'applicazione dell'integrità
Come è già stato detto, i vincoli espliciti non sono i soli strumenti per applicare l'integrità
dei dati. Anche i tipi di dati sono dei vincoli, che usano vincoli definiti dal tipo e set di
operazioni consentite. Un attributo è un vincolo con il relativo tipo di dati. Puoi inoltre
definire se la colonna di una tabella consente valori NULL. Infine, anche le definizioni delle
tabelle rappresentano dei vincoli: se non hai un punto in cui inserire un valore, non puoi
inserirlo. Questo aspetto verrà illustrato meglio nella sezione di questo capitolo relativa alla
normalizzazione.
Non puoi implementare tutte le regole aziendali usando strumenti dichiarativi. Alcuni vincoli
sono troppo complessi, mentre altri superano il limite del database. Una chiave esterna, ad
esempio, è limitata all'associazione delle tabelle solo nello stesso database. Alcuni vincoli
devono essere implementati in modo programmatico. Puoi inserire codice vincolante in
un'applicazione client, in un livello intermedio, nel livello di accesso ai dati, nelle stored
procedure di un database o in un qualunque altro punto in cui esiste codice. Tuttavia, se
desideri che il RDBMS applichi automaticamente vincoli complessi, devi usare i trigger.
I trigger sono speciali stored procedure eseguite, o avviate, automaticamente da un RDBMS.
Puoi usare trigger DML (Data Modification Language) per applicare le regole di modifica
dei dati e trigger DDL (Data Definition Language) per applicare le regole di modifica dello
schema. I trigger possono essere attivati primo o dopo l'istruzione che sta modificando lo
stato del database. SQL Server 2008 supporta due tipi di trigger DML, INSTEAD OF e AFTER,
e solo un tipo di trigger DDL, AFTER. I trigger INSTEAD OF di fatto non sono trigger BEFORE
dello standard ANSI, vengono attivati prima dell'istruzione, ma intercettano anche l'istruzione,
quindi puoi poi fare quello che desideri nel corpo del trigger. Se desideri che l'istruzione venga
eseguita, devi scriverlo esplicitamente nel corpo del trigger.
In teoria, devi sempre essere in grado di usare una vista invece di una relazione di base.
Tuttavia, non tutte le viste sono aggiornabili. Ad esempio, una vista può riepilogare alcuni
attributi di una tabella base; un RDBMS non sa come distribuire un valore da una singola riga
di una vista a più righe base. I trigger INSTEAD OF sono pensati specificatamente per rendere
aggiornabili le viste.
SQL Server 2008 offre anche un tipo di sistema XML incorporato. Il tipo XML applica alcune
regole di integrità: consente solo XML corretti. Inoltre puoi convalidare valori XML con uno
schema predefinito di una raccolta di schemi creata all'interno del database SQL Server.
Descrizioni dettagliate sui trigger e le convalide XML vanno oltre lo scopo del libro; per
ulteriori informazioni leggi Inside Microsoft SQL Server 2008: T-SQL Programming.
110
Microsoft Transact SQL Query
Puoi inoltre usare alcuni elementi di un database che non applicano di fatto l'integrità dei dati,
ma aiutano gli utenti a inserire valori corretti. Le impostazioni predefinite possono aiutare a
inserire un valore quando non è elencato esplicitamente nell'istruzione SELECT. SQL Server
2008 ha inoltre un tipo Timestamp; SQL Server inserisce e aggiorna i valori di questo tipo
automaticamente, inoltre garantisce che i valori nelle colonne di questo tipo siano univoche
nel database. La proprietà IDENTITY di una colonna può aiutare a inserire numeri sequenziali.
Una cosa importante da sapere è l'ordine in cui il sistema applica i vincoli. Probabilmente avrai
notato che si è passati da una discussione relativa all'implementazione generale (e teorica)
all'implementazione specifica di SQL Server 2008. I dettagli dei vincoli sono specifici del
sistema, quindi sembra più appropriato parlare del sistema illustrato in questo libro, Microsoft
SQL Server 2008. Quindi, l'ordine di esecuzione in SQL Server è il seguente:
1. Viene verificato lo schema (se un aggiornamento è valido per lo schema di tabella).
2. Vengono controllati i tipi di dati.
3. Vengono attivati i trigger INSTEAD OF invece dell'istruzione effettiva.
4. Vengono applicati i vincoli Default.
5. Viene verificato il supporto dei valori NULL.
6. Vengono verificati i vincoli Primary Key e Unique.
7. Vengono applicati i vincoli Foreign Key e Check.
8. Viene eseguita l'istruzione.
9. Vengono attivati i trigger AFTER.
Questo ordine ti dice che i vincoli dichiarativi vengono applicati prima dell'istruzione effettiva,
inoltre impediscono aggiornamenti errati, mentre i trigger AFTER vengono attivati dopo
l'istruzione, e devi eseguire il rollback di una modifica errata scoperta dal trigger AFTER
dell'istruzione. Ciò significa che l'uso di vincoli dichiarativi è più efficace dell'uso dei trigger
AFTER, quindi devi optare per l'uso di tali vincoli ogni volta che è possibile. Non dimenticare
un altro vantaggio nell'uso dei vincoli dichiarativi: possono essere utili nell'ottimizzazione delle
query.
Il Buono, il Cattivo e. . . lo sconosciuto!
L'ultima questione che verrà trattata in merito all'integrità dei dati è se sia necessario
consentire i valori NULL nel database. In un mondo ideale il database dovrebbe solo
rappresentare proposizioni vere; se qualcosa è NULL e non sai che significa NULL, non puoi
dire che sia vera. Quindi, da un certo punto di vista, non dovresti consentire i valori NULL.
Tuttavia, nel mondo reale manca sempre qualche informazione, almeno temporaneamente.
Inoltre, puoi sperimentare il paradosso di Russell, illustrato nel capitolo 2. Oltre alla descrizione
teorica, si desidera fornire un esempio trovato in Fermat’s Last Theorem di Simon Singh
(HarperPerennial, 2005) (informazioni in lingua inglese), che mostra il paradosso di Russell
nella vita reale. Si tratta del problema del libraio meticoloso.
Capitolo 3
Il modello relazionale
111
Questo libraio ha due tipi di cataloghi (di qualunque argomento); alcuni elencano se stessi
nei riferimenti, altri no. Il libraio vuole creare due nuovi cataloghi: uno che elenca tutti i
cataloghi che elencano se stessi e uno che elenca tutti i cataloghi che non elencano se stessi. Il
problema si presenta con il secondo catalogo: dovrebbe elencarsi? Se si elenca, per definizione
non dovrebbe essere elencato. Se non si elenca, per definizione dovrebbe essere elencato.
Immagina di dover inserire questi due cataloghi in un database, e in una tabella che descrive
i cataloghi hai un attributo rappresentato da una bandiera che mostra se il catalogo elenca se
stesso. Cosa inseriresti in questo attributo per il catalogo che elenca tutti i cataloghi che non
elencano se stessi? NULL potrebbe andare bene, in quando mostra che non puoi avere nulla di
significativo in questo caso.
Naturalmente, nella vita reale incontrerai informazioni mancanti per le mille ragioni diverse
dal paradosso di Russell. Nonostante ciò, devi trovare un modo per risolvere il problema delle
informazioni mancanti. Lo standard ANSI prescrive e SQL Server implementa i valori NULL per
denotare i valori mancanti. Nota che NULL non è un valore, è solo un indicatore. NULL non ha
il privilegio di essere uguale a se stesso. Alcuni autori (Date, Pascal) proibiscono severamente
i valori NULL, altri li consentono esplicitamente (Codd), mentre altri (Halpin) non li discutono,
mostrano solo come modellarli e usarli. Cosa fare?
Se i valori NULL non sono consentiti, devi implementare dei valori speciali per contrassegnare
le informazioni mancanti. Il vantaggio di questo approccio è che puoi usare gli operatori
booleani standard nelle query, quindi non saranno necessari operatori speciali che gestiscono i
valori NULL. Lo svantaggio è che non esiste un valore singolo, standard e speciale, accettato in
tutto il mondo. Inoltre un singolo valore speciale non sarebbe sufficiente; di fatto ne sarebbe
necessario uno per ogni tipo di dati. Usare valori NULL significa usare uno standard che sia già
accettato; tuttavia significa anche introdurre la logica a tre valori, dove not true non è uguale a
false. La logica a tre valori rende le query più complesse.
Dopo aver considerato i pro e i contro, la conclusione è che i valori NULL esistono per un
motivo, inoltre vengono implementati da tutti i maggiori RDBMS, quindi si preferisce utilizzarli
invece di inventare valori speciali. Nel corso del libro verranno fornite ulteriori informazioni
sulla scrittura di query efficienti con logica a tre valori. Nonostante ciò, è possibile evitare
alcuni valori NULL, ad esempio quelli che esistono perché un attributo non è applicabile per
una particolare tupla di una relazione. In questo caso è un problema di progettazione. Un
buono schema vincola, o esclude, i valori NULL che rappresentano "non applicabile". È quindi
giunto il momento di definire uno schema buono.
Normalizzazione e altri argomenti di progettazione
Per prima cosa bisogna chiarire che questo non è un libro sulla modellazione; si tratta di
una guida pratica con qualche capitolo introduttivo che spiega la teoria che sta dietro la
pratica. La teoria aiuta a capire perché in SQL Server vengono implementate alcune cose in
un determinato modo. Questo libro aiuta a capire meglio cosa fai quando crei e mantieni un
database relazionale, oltre ad aiutarti a trovare modi diversi per esprimere le query, trovare
query più ottimizzate e così via. Quindi non verrà spiegato come eseguire la modellazione;
verrà illustrato cosa devi ottenere con i modelli.
112
Microsoft Transact SQL Query
Sul mercato si trovano molti libri sulla modellazione, che non hanno bisogno di pubblicità. Ne
verranno citati alcuni solo per rendere più completo il capitolo. Uno è Information Modeling
and Relational Databases, 2nd edition di Terry Halpin e Tony Morgan (Morgan Kauffman, 2008)
(informazioni in lingua inglese), considerato la bibbia della modellazione ORM (object-role
modelling). Per quanto riguarda l'approccio più popolare, l'approccio ER, vale la pena citare
Data Modeling Essentials, 3rd edition di Graeme Simsion e Graham Witt (Morgan Kauffman,
2004) (informazioni in lingua inglese), in cui si possono trovare molte informazioni sul processo
di modellazione e informazioni sulle regole aziendali. Infine, se sei uno sviluppatore e usi già la
metodologia UML (Unified Modeling Language), una buona risorsa potrebbe essere Database
Design for Smarties: Using UML for Data Modeling di Robert J. Muller (Morgan Kauffman,
1999) (informazioni in lingua inglese).
Quello che si deve ottenere per creare un buon modello relazionale vien descritto
matematicamente con normalizzazione e specializzazione. Dal momento che la
normalizzazione è più complessa, verrà illustrata più nel dettaglio, anche se entrambi gli
aspetti sono importanti per una buona progettazione. Prima di iniziare con la normalizzazione
però, è opportuno ripetere un concetto molto semplice ma molto importante sulla buona
progettazione: un database relazionale è ben progettato se puoi ricostruire i predicati (e le
proposizioni) usati per descrivere il problema aziendale.
Formati normali per dipendenze funzionali
Le tabelle sono normalizzate quando rappresentano proposizioni relative a entità di un
tipo, in altre parole quando rappresentano un singolo set. Ciò significa che le entità non si
sovrappongono alle tabelle e che le tabelle sono ortogonali o normali in termini matematici.
Quando una tabella soddisfa un determinato insieme di condizioni prescritte, si dice che sia
in una forma normale. Un database è normalizzato quando tutte le tabelle sono normalizzate.
Puoi creare modelli di database completamente normalizzati con l'approccio ORM o ER.
La normalizzazione è un processo di riprogettazione per frazionare le entità. Il processo
include la decomposizione, ma non la decomposizione che porta alla perdita di informazioni.
Dopo il processo di normalizzazione, tutte le informazioni originali devono poter essere
ottenute con query che includono operatori relazionali come Join e altri. La normalizzazione
si ottiene applicando una sequenza di regole per creare ciò che viene chiamato forme
normali. L'obiettivo è eliminare la ridondanza e l'incompletezza. Nota che la seconda viene
spesso tralasciata, tuttavia la normalizzazione elimina l'incompletezza oltre alla ridondanza.
Vengono definite molte forme normali. Le più importanti sono la prima, seconda terza,
Boyce-Codd, quarta e quinta. Se un database ha la quinta forma normale, si dice che sia
completamente normalizzato. Se un database non è completamente normalizzato, possono
verificarsi anomalie nella manipolazione dei dati. Verranno illustrate le prime quattro forme
normali, che riguardano le dipendenze funzionali. Una variabile dipendente è dipendente
dal punto di vista funzionale da una indipendente quando esiste esattamente un valore della
variabile dipendente per ogni valore della variabile indipendente. Ciò significa che se conosci
il valore della variabile indipendente, conosci anche il valore della variabile dipendente. In una
relazione, gli attributi non chiave sono dipendenti dal punto di vista funzionale dalle chiavi;
se conosci il valore chiave, puoi trovare il valore dell'attributo non chiave. Questo è ciò che
significa dipendenza funzionale in una relazione.
Capitolo 3
Il modello relazionale
113
Prima forma normale
Immagina uno scenario reale con clienti che ordinano prodotti. Clienti, ordini e prodotti sono
entità scoperte quando hai avuto la descrizione dello scenario aziendale. Inizialmente esegui la
modellazione di tutto in un'unica tabella denominata Orders. Nella tabella 3-1 viene illustrata
la tabella immaginaria Orders. Le colonne che fanno parte della chiave sono ombreggiate (in
questo esempio solo OrderId).
Una tabella prima di 1NF
TABELLA 3-1
OrderId
CustomerId
CustomerName
OrderDate
Items
1
1
Company ABC
2008-10-22
Ap Apples q=5, Ch Cherries q=10
2
1
Company ABC
2008-10-24
Ba Bananas q=12
3
2
Company ABC
2008-09-15
Ap Apples q=3, Ba Bananas q=3
Questo progetto, ovviamente, è problematico. Alcune possibili anomalie nella manipolazione
dei dati sono:
■
Inserimento
❏
■
Aggiornamento
❏
■
Se l'elemento Ba viene rinominato, come esegui l'aggiornamento? Puoi perdere
facilmente qualche riga che dovresti aggiornare. E ciò a causa della ridondanza.
Eliminazione
❏
■
Come inserisci un cliente senza un ordine? (ad ogni modo, puoi notare
l'incompletezza del problema?)
Se l'ordine 3 viene eliminato, i dati del cliente 2 saranno persi. Questo è un
problema di incompletezza.
Selezione
❏
Come calcoli la quantità totale di banane? È il problema della colonna non
scalare. La colonna Items è una raccolta.
La prima forma normale (1NF) dice che una tabella rientra nella prima forma normale se tutte
le colonne sono atomiche. Non sono consentite colonne multivalore. Nota che la definizione di
1NF afferma semplicemente che una tabella deve rappresentare una relazione.
La decomposizione deve iniziare con la colonna Items. Ha bisogno di una singola riga per
elemento in un ordine, e ogni parte atomica dei dati di un singolo elemento (ProductId,
ProductName, Quantity) deve ottenere la propria colonna relativa. Tuttavia, dopo la
decomposizione, ottieni più righe per un singolo ordine. OrderId da solo non può più essere
la chiave. La nuova chiave è composta dalle colonne OrderId e ProductId. Se consenti più
prodotti su un singolo ordine, ad esempio ogni volta con uno sconto diverso, non potrai usare
ProductId come parte della chiave. Probabilmente aggiungerai l'attributo ItemId e lo userai
come parte della nuova chiave. Una tabella decomposta in 1NF viene illustrata nella tabella
3-2.
114
Microsoft Transact SQL Query
TABELLA 3-2
Una tabella in 1NF
OrderId CustomerId CustomerName OrderDate
Product
ItemID ProductId Quantity Name
1
1
Company ABC
2008-10-22
1
Ap
5
Apples
1
1
Company ABC
2008-10-22
2
Ch
10
Cherries
2
1
Company ABC
2008-10-24
1
Ba
12
Bananas
3
2
XYZ
2008-09-15
1
Ap
3
Apples
3
2
XYZ
2008-09-15
2
Ba
3
Bananas
Prima di iniziare con 2NF, è opportuno sottolineare un equivoco comune con 1NF. Spesso si
legge di ripetere il gruppo di colonne. Considera, ad esempio, la tabella Employees illustrata
nella figura 3-11.
Employees
PK
EmployeeId
EmployeeName
Child1Name
Child2Name
Child3Name
FIGURA 3-11 Tabella Employees
Probabilmente noterai qualcosa di strano. Presenta un gruppo di colonne che si ripetono con
un nome simile, ChildXName. Child1Name è il nome del figlio maggiore, Child2Name è il nome
del secondo più vecchio e Child3Name è il nome del terzo più vecchio (non considerando
i gemelli). Naturalmente la domanda è cosa succede se un dipendente ha più di tre figli.
Probabilmente creeresti una nuova tabella. Potresti pensare che stai normalizzando la tabella
Employees.
Sai che il modello relazionale non dipende dai nomi. Rinomina quindi la tabella e tutte le
colonne per ottenere la tabella illustrata nella figura 3-12.
Orders
PK
OrdersId
CustomerID
OrderDate
DueDate
ShipDate
FIGURA 3-12 Tabella Orders (la tabella Employees rinominata)
Probabilmente ti sentirai più a tuo agio con questo progetto e questa tabella ti sembrerà
perfettamente normalizzata. Anche la tabella Employees rientrava in 1NF, ma il problema è che
viene creato un vincolo in entrambe le tabelle. Il primo vincolo afferma che ci sono dipendenti
con tre figli (o al massimo tre se le colonne consentono valori NULL); il secondi vincolo afferma
Capitolo 3
Il modello relazionale
115
che un ordine ha tre date. Naturalmente il primo vincolo non ha senso nel mondo reale, quindi
il primo progetto non andava bene in ogni caso. Tuttavia era normalizzato. Ricorda che puoi
vincolare il modello di dati stesso con il progetto di tabella. Spesso un gruppo di colonne che
si ripete con nomi simili rappresenta davvero una raccolta nascosta; tuttavia, non decomporre
tali gruppi automaticamente. Verifica prima le regole aziendali, i predicati soggetti a vincoli.
Seconda forma normale
Dopo aver ottenuto 1NF, come visto nella tabella 3-2, esistono ancora molte anomalie di
aggiornamento:
■
Inserimento
Come inserisci un cliente senza un ordine? (Incompletezza)
❏
■
Aggiornamento
Se un cliente cambia la data di ordine, come puoi eseguire l'aggiornamento?
(Ridondanza)
❏
■
Eliminazione
Se l'ordine 3 viene eliminato, i dati del cliente 2 saranno persi. (Incompletezza)
❏
Per ottenere la seconda forma normale (2NF), una tabella deve essere nella 1NF (noterai la
progressione lineare) e ogni colonna non chiave deve essere dipendente dal punto di vista
funzionale dall'intera chiave. Ciò significa che nessuna colonna non chiave può dipendere
solo da una parte della chiave. Nella tabella 3-2 hai bisogno solo di OrderId per ottenere
CustomerId e OrderDate; non hai bisogno di ItemId, che fa ugualmente parte della chiave. Per
le forme normali diverse da 1NF, la decomposizione significa la creazione di nuove tabelle,
non solo nuove righe come in 1NF. Per ottenere 2NF devi decomporre la tabella in due tabelle,
come illustrato nelle tabelle 3-3 e 3-4.
Tabella Orders in 2NF
TABELLA 3-3
OrderId
CustomerId
CustomerName
OrderDate
1
1
Company ABC
2008-10-22
2
1
Company ABC
2008-10-24
3
2
XYZ
2008-09-15
TABELLA 3-4
Tabella OrderDetails in 2NF
OrderId
ItemId
ProductId
Quantity
ProductName
1
1
Ap
5
Apples
1
2
Ch
10
Cherries
2
1
Ba
12
Bananas
3
1
Ap
3
Apples
3
2
Ba
3
Bananas
116
Microsoft Transact SQL Query
Esegui la divisione in modo da lasciare solo gli attributi che dipendono da OrderId nella tabella
Orders, inoltre introduci una nuova tabella, OrderDetails, con altri attributi. 2NF riguarda la
relazione tra le colonne che fanno parte di una chiave e altre colonne che non ne fanno parte.
Per ottenere la decomposizione senza perdite, devi poter riunire in join le due tabelle per
generare la tabella originale. Quindi hai bisogno di qualche valore comune in entrambe le
tabelle. Naturalmente questa è la colonna OrderId della tabella Orders, che come già sai è la
colonna chiave esterna nella tabella OrderDetails.
Terza forma normale
Con 2NF è stata risolta l'anomalia dell'aggiornamento della data di ordine dovuta alla
ridondanza. Tuttavia restano ancora molti problemi:
■
Inserimento
Come inserisci un cliente senza un ordine? (Incompletezza)
❏
■
Aggiornamento
Se un cliente o un prodotto viene rinominato, come esegui l'aggiornamento?
(Ridondanza)
❏
■
Eliminazione
Se l'ordine 3 viene eliminato, i dati del cliente 2 saranno persi. (Incompletezza)
❏
Per ottenere la terza forma normale (3NF), una tabella deve essere nella 2NF e ogni colonna
non chiave non deve essere dipendente dal punto di vista transitivo da ogni chiave. In altre
parole, le colonne non chiave devono essere indipendenti l'una dall'altra. Ad esempio, nella
tabella 3-3 da OrderId puoi trovare CustomerId e da CustomerId puoi trovare transitivamente
il valore CustomerName. Prova a trovare un problema simile nella tabella 3-4 (naturalmente
ProductId e ProductName non sono dipendenti l'una dall'altra).
Per ottenere 3NF devi creare nuove tabelle per le dipendenze tra le colonne non chiave, come
illustrato dalla tabella 3-5 alla tabella 3-8.
Tabella Customers in 3NF
TABELLA 3-5
CustomerId
CustomerName
1
Company ABC
2
XYZ
TABELLA 3-6
Tabella Orders in 3NF
OrderId
CustomerId
OrderDate
1
1
2008-10-22
2
1
2008-10-24
3
2
2008-09-15
Capitolo 3
Il modello relazionale
117
Tabella OrderDetails in 3NF
TABELLA 3-7
OrderId
ItemId
ProductId
Quantity
1
1
Ap
5
1
2
Ch
10
2
1
Ba
12
3
1
Ap
3
3
2
Ba
3
TABELLA 3-8
Tabella Products in 3NF
ProductId
ProductName
Ap
Apples
Ch
Cherries
Ba
Bananas
Quando ottieni 3NF in genere elimini tutte le anomalie di manipolazione dei dati. In genere
quando normalizzi fino a 3NF, il risultato soddisfa anche BCNF, 4NF e 5NF. Le forme normali
più elevate di violazioni sono rare. Per rendere questa panoramica completa, tuttavia,
verranno illustrate le forme normali più elevate e verranno forniti suggerimenti pratici su come
riconoscere la possibilità di violarle.
Forma normale di Boyce-Codd
La prima domanda che potresti farti è perché la NF successiva non viene chiamata 4NF. Il fatto
è che Codd di fatto a voluto sostituire la 3FN con quella che viene ora conosciuta come forma
normale di Boyce-Code (BCFN). Dal momento che è più rigida di 3NF, 3NF non è stata tolta, di
conseguenza esiste una certa incoerenza nella numerazione.
Verrà ora illustrato come violare BCNF. Immagina per un momento di avere la tabella Orders
senza la colonna OrderId e con un unico ordine per cliente al giorno consentito. Inoltre, ogni
ordine ha un tempo di spedizione standard, quindi OrderDate fornisce l'atteso DueDate. Nella
tabella 3-9 viene illustrato questo esempio. Per chiarire la dipendenza, DueDate è sempre un
giorno dopo OrderDate.
TABELLA 3-9
Tabella immaginaria Orders
CustomerId
OrderDate
DueDate
OtherOrderColumns
1
2008-10-22
2008-10-23
...
1
2008-10-24
2008-10-25
...
2
2008-09-15
2008-09-16
...
Questa tabella ha due chiavi candidate composte: {CustomerId, OrderDate} e {CustomerId,
DueDate}. Le chiavi candidate si sovrappongono sulla colonna CustomerId (ombreggiata con
un colore più scuro per dimostrare che viene usata due volte). È in 3NF perché tutte le colonne
118
Microsoft Transact SQL Query
non chiave dipendono intransitivamente da ogni chiave. Tuttavia, è possibile una anomalia di
manipolazione dei dati specifica:
■
Aggiornamento
❏
Se un cliente cambia OrderDate, non devi dimenticare di aggiornare anche
DueDate. (Ridondanza)
Puoi violare BCNF solo nel raro caso in cui una tabella ha più di una chiave candidata
composta e le chiavi candidate si sovrappongono. Sarebbe possibile decomporre la tabella
3-9 in due nuove tabelle basate sulle due chiavi candidate, per brevità nella notazione breve,
mostrando solo le intestazioni di tabella:
OrdersOrderDate {CustomerId, OrderDate, OtherOrderColumns}
OrdersDueDate {CustomerId, DueDate, OtherOrderColumns}
Tuttavia, il buon senso ti dice che questa decomposizione non è quello che desideri nel
tuo modello. Inoltre, esiste una ridondanza nascosta tra le due nuove tabelle; altre colonne
non chiave si ripetono. Non è possibile risolvere questo problema solo con le regole di
normalizzazione (sai già che il buon senso può aiutarti). Questo problema verrà ripreso più
avanti con una soluzione formale, quando verrà descritto il principio della progettazione
ortogonale.
La BCNF non è ancora stata definita. BCNF afferma che ogni determinante deve essere una
chiave. La parte indipendente di una dipendenza funzionale viene chiamata determinante.
Un attributo chiave deve essere un determinante, ossia non deve essere determinato. Nella
tabella 3-9 OrderDate determinava DueDate e viceversa, inoltre entrambi sono attributi
chiave (precisamente parte di una chiave). In altre parole, per ottenere BCNF non devi avere
dipendenze funzionali tra gli attributi chiave.
Puoi ottenere BCNF senza decomposizione usando il buon senso. Nelle tabelle 3-10 e 3-11
vengono illustrate le due possibilità per ottenere BCNF nella tabella 3-9.
TABELLA 3-10
Tabella Orders in BCNF: Prima soluzione
CustomerId
OrderDate
StandardShippingTimeDays
OtherOrderColumns
1
2008-10-22
1
...
1
2008-10-24
1
...
2
2008-09-15
1
...
TABELLA 3-11
Tabella Orders in BCNF: Seconda soluzione
OrderId
CustomerId
OrderDate
DueDate
OtherOrderColumns
1
1
2008-10-22
2008-10-23
...
2
1
2008-10-24
2008-10-25
...
3
2
2008-09-15
2008-09-16
...
Nota che la soluzione illustrata nella tabella 3-11 non definisce più le coppie (CustomerId,
OrderDate) e (CustomerId, Duedate) come chiavi. Quindi non è realmente una soluzione se le
Capitolo 3
Il modello relazionale
119
due coppie continuano a determinare gli ordini. Tuttavia è stata introdotta perché è più simile
agli scenari reali; un cliente può inserire più di un ordine al giorno.
Forme normali più elevate
Le forme normali più elevate, la quarta e la quinta, non riguardano le dipendenze funzionali,
bensì le dipendenze multivalori e di join. Verranno ora illustrate.
Quarta forma normale
Come già detto, le violazioni della quarta e quinta forma normale sono molto rare, e in genere
possono essere evitate con il buon senso. Per iniziare, le violazioni possono verificarsi solo in
una tabella composta da colonne che insieme compongono una chiave, senza colonna non
chiave e con almeno tre colonne chiave. Gli esempi seguenti di violazioni della 4NF e 5NF, così
come le soluzioni, si basano su esempi di Practical Issues in Database Management di Fabian
Pascal (Addison-Wesley, 2000) (informazioni in lingua inglese).
Si inizierà con il descrivere un esempio di problema aziendale. Un'azienda fittizia lavora su
dei progetti. I dipendenti vengono assegnati a tali progetti. Ogni dipendente ha una serie di
competenze. Se un dipendente viene assegnato a un progetto, tale dipendente esegue tutte
le attività che può eseguire. Nella tabella 3-12 viene illustrato questo esempio. Benché non
venga mostrato qui, immagina che ci siano tabelle Employees, Projects e Activities separate nel
database.
TABELLA 3-12
Tabella Employee-Projects-Activities
Employee
Project
Activity
1
Proj 111
ABC
1
Proj 111
DEF
1
Proj 222
ABC
1
Proj 222
DEF
2
Proj 111
ABC
2
Proj 111
XYZ
Noterai la ridondanza. Sono possibili le seguenti anomalie nella manipolazione dei dati:
■
Inserimento
❏
■
Aggiornamento
❏
■
Come assegni un dipendente a un progetto se il dipendente non ha ancora
competenze? (Incompletezza)
Se un dipendente viene riassegnato da un progetto a un altro, come gestisci
l'aggiornamento di tutte le righe necessarie? (Ridondanza)
Eliminazione
❏
Se elimini tutte le assegnazioni di progetto di un dipendente, le informazioni
relative alle relative competenze andranno perse. (Incompletezza)
120
Microsoft Transact SQL Query
Le informazioni su progetti e attività si ripetono per ogni dipendente. È possibile evitare
questo problema se si consentono colonne multivalori, come illustrato nella tabella 3-13.
TABELLA 3-13
Tabella Employee-Projects-Activities con colonne multivalori
Employee
Project
Activity
1
Proj 111
ABC
Proj 222
DEF
Proj 111
ABC
2
XYZ
Questa situazione indica che esiste qualcosa denominato dipendenza multivalore tra
dipendenti, progetti e attività. Le dipendenze multivalori sono una generalizzazione delle
dipendenze funzionali. La quarta forma normale (4NF) afferma che non devono esserci
dipendenze multivalori non triviali che non sono dipendenze funzionali. Per ottenere tutto ciò,
devi decomporre la tabella 3-12, come illustrato nelle tabelle 3-14 e 3-15.
TABELLA 3-14
Tabella Employee-Projects
Employee
Project
1
Proj 111
1
Proj 222
2
Proj 111
TABELLA 3-15
Tabella Employee-Activities
Employee
Activity
1
ABC
1
DEF
2
ABC
2
XYZ
Quinta forma normale
Ora verrà leggermente modificata la descrizione del problema aziendale. Se un dipendente
viene assegnato a un progetto, tale dipendente non deve eseguire tutte le attività per cui ha le
competenze da eseguire in questo progetto; infatti, in un progetto alcune delle attività per cui
i dipendenti hanno le competenze potrebbero non essere necessarie. Tuttavia se un progetto
include un'attività, un dipendente viene assegnato al progetto e tale dipendente assegnato
esegue la sopraccitata attività, il dipendente deve eseguire tale attività sul progetto. Nella
tabella 3-16 viene illustrato un esempio.
Capitolo 3
TABELLA 3-16
Il modello relazionale
121
Tabella Employee-Projects-Activities
Employee
Project
Activity
1
Proj 111
ABC
1
Proj 111
DEF
1
Proj 222
ABC
2
Proj 111
ABC
2
Proj 111
XYZ
Senza decomposizione, le possibili anomalie nella manipolazione dei dati sono simili alle
anomalie citate nella sezione della 4NF. Dopo la decomposizione in due tabelle, come hai visto
nelle tabelle 3-14 e 3-15, provi a unire in join le tabelle decomposte per riottenere la tabella
originale 3-16. Quello che accade è che ottieni un'ulteriore tupla non corretta:
{1, Proj 222, DEF}
Con la decomposizione della tabella 3-16 in due tabelle che sono di fatto proiezioni della
tabella originale, hai ottenuto una riga non corretta se hai unito in join le due nuove tabelle.
Il problema sta nel fatto che la tabella originale violava il cosiddetto vincolo di dipendenza
join. Una relazione soddisfa la relazione join se ogni valore lecito della relazione è uguale al
join delle relative proiezioni. Le dipendenze join sono una generalizzazione delle dipendenze
multivalori. Per risolvere il problema devi eseguire una decomposizione in tre tabelle. Oltre
alle tabelle Employess-Projects ed Employees-Activities, devi avere anche una tabella ProjectsActivities, come illustrato nella tabella 3-17.
TABELLA 3-17
Tabella Projects-Activities
Project
Activity
Proj 111
ABC
Proj 111
DEF
Proj 222
ABC
Proj 111
XYZ
Se non esiste una violazione della dipendenza join, una tabella è in 5NF. Una definizione di
forma normale afferma che ogni dipendenza join non triviale nella tabella è implicata dalle
chiavi della tabella.
Infine, è opportuno ritornare sul buon senso citato precedentemente. Cosa succede se un
progetto include un'attività, un dipendente viene assegnato a un progetto e tale dipendente
assegnato esegue la sopraccitata attività, ma il dipendente non deve eseguire tale attività sul
progetto? A questo punto sono necessarie quattro tabelle, che è il progetto probabilmente
creato inizialmente. È necessaria una tabella Employees-Projects che mostra quali dipendenti
sono assegnati a quale progetto; la tabella Employees-Activities che mostra quali attività
possono essere eseguite dai dipendenti, la tabella Projects-Activities che mostra quali attività
sono necessarie in quale progetto, e infine la tabella Employees-Projects-Activities che mostra
quale attività viene eseguita da quale dipendente su quale progetto.
122
Microsoft Transact SQL Query
Altre forme normali
Prima di introdurre le forme normali quarta e quinta è opportuno citare brevemente la
forma normale chiave dominio (DKNF). In DKNF tutti i vincoli derivano dai domini (tipi) e
dalle chiavi (chiavi candidate e chiavi esterne). Una tabella in DKNF è libera di violare regole
di entità, referenziali e di dominio, come descritto precedentemente. È nella quinta forma
normale e quindi completamente normalizzata. Tuttavia, DKNF è una forma normale più
teorica che pratica. Per ottenerla, devi creare moltissimi tipi diversi. Questa è una missione
quasi impossibile, specialmente se i tipi devono essere accettati ampiamente e i vincoli di tipo
devono essere concordati. Inoltre, gli utenti dei tipi (il database e gli altri sviluppatori) devono
imparare molte cose solo per iniziare a usare i tipi.
C. J. Date ha proposto una sesta forma normale, una forma che risolve possibili anomalie
di dati temporali. Tuttavia per risolvere problemi di dati temporali, è necessario introdurre
il tipo di dati Interval, implementato negli operatori Pack e Unpack, e risolvere anche altri
problemi. Leggi Inside Microsoft SQL Server 2008: T-SQL Programming per una discussione più
approfondita sui dati temporali e le soluzioni proposte per i problemi temporali.
Denormalizzazione
Dovresti sempre provare a ottenere almeno 3NF quando progetti un database. Tuttavia,
a volte devi rigirare il processo e, dopo aver normalizzato un database, devi iniziare a
denormalizzarlo. I due motivi principali per la denormalizzazione sono prestazioni e
cronologia, come illustrato in Designing Database Solutions by Using Microsoft SQL Server
2005 di Dejan Sarka, Andy Leonard, Javier Loria e Adolfo Wiernik (Microsoft Press, 2007)
(informazioni in lingua inglese).
Una domanda aziendale classica è qual è la quantità attuale di prodotto in magazzino. Puoi
calcolare le quantità in magazzino sommando le forniture e sottraendo le consegne. Gli stati
e i livelli possono essere sempre calcolati dagli eventi. Tuttavia, questa domanda può essere
molto frequente. Quindi ha senso aggregare gli eventi ai livelli e agli stati e mantenere tali
aggregati con ogni nuovo evento. Inoltre, puoi velocizzare i join replicando una chiave esterna
dalla prima tabella figlio alla seconda. Le query di questo tipo possono includere meno
tabelle da unire in join. In entrambi i casi hai eseguito la denormalizzazione per migliorare le
prestazioni.
Immagina un altro esempio. Un'applicazione di fatturazione usa un progetto di database
completamente normalizzato. L'indirizzo di un cliente viene archiviato solo nella tabella
Customers. Se un cliente si sposta, aggiorni tale indirizzo cliente con quello nuovo. Supponi
che dopo l'aggiornamento, il cliente chiede di ristampare una vecchia fattura. Ora hai un
problema perché non hai salvato il vecchio indirizzo. Puoi risolvere il problema mantenendo
una copia dell'indirizzo del cliente con la data della fattura nella tabella Invoices (bisogna dire
che questo potrebbe non essere trattato come denormalizzazione; probabilmente hai appena
perso quell'attributo InvoiceAddress mentre analizzavi il problema aziendale). Nella figura 3-13
viene illustrato il database Invoices completamente normalizzato.
Capitolo 3
Employees
PK
Customers
EmployeeId
PK
EmployeeName
CustomerName
Address
CityId
FK1 EmployeeId
CustomerId
ProductName
Price
Warehouses
PK
ProductId
PK
Invoiceld
FK1 Customerld
InvoiceDate
InvoiceDetails
PK,FK1
PK
InvoiceId
ItemId
FK2
ProductId
Quantity
Discount
WarehouseId
WarehouseId
FK3
WarehouseName
123
Invoices
Products
PK
Il modello relazionale
FIGURA 3-13 Versione normalizzata del database Invoices
Puoi eseguire la denormalizzazione in più punti. Ad esempio, puoi trasferire la colonna
EmployeeId alla tabella Invoices per evitare un join con la tabella Customers quando analizzi le
fatture solo tramite i dipendenti. Puoi includere le colonne CustomerName e CustomerAddress
nella tabella Invoices per mantenere la cronologia. Puoi mantenere gli aggregati, ad esempio
livello di magazzino per warehouse (in una tabella separata), livello totale di magazzino per
prodotto, vendite anno per data per cliente e altro ancora. La figura 3-14 illustra una versione
denormalizzata del database Invoices.
Employees
PK
EmployeeId
PK
EmployeeName
CustomerName
CustomerAddress
CityId
FK1 EmployeeId
YTDSales
ProductsInWarehouses
PK,FK1
PK,FK2
Customers
CustomerId
WarehouseId
ProductId
Invoices
PK
Invoiceld
FK1 Customerld
InvoiceDate
EmployeeId
CustomerName
CustomerAddress
Products
QualityInStock
PK
Warehouses
PK
ProductId
ProductName
Price
TotalInStock
InvoiceDetails
PK,FK1
PK
InvoiceId
ItemId
FK2
ProductId
Quantity
Discount
WarehouseId
WarehouseId
WarehouseName
FK3
FIGURA 3-14 Versione denormalizzata del database Invoices
Ricorda di eseguire la denormalizzazione in modo molto deliberato. Dopo la
denormalizzazione, introduci possibili anomalie di aggiornamento nel database. Devi
mantenere i dati ridondanti nelle transazioni definite dall'utente. Se inserisci un nuovo
124
Microsoft Transact SQL Query
evento, ad esempio, fai attenzione ad aggiornare il livello o lo stato derivato dagli eventi nella
stessa transazione. I trigger sono particolarmente utili per mantenere i dati denormalizzati.
Con i trigger, che fanno automaticamente parte di una transazione, trasferisci il carico della
manutenzione dei dati denormalizzati sul tuo RDBMS.
Generalizzazione e specializzazione
Torna al problema dei valori NULL. Ricorda che puoi avere valori NULL quando un attributo
non è applicabile per alcune tuple. Puoi eliminare la necessità di usare i valori NULL in questo
modo tramite la specializzazione introducendo i sottotipi. Il problema può essere anche
aggirato; ricorda la decomposizione per risolvere la violazione BCNF precedentemente in
questo capitolo:
OrdersOrderDate {CustomerId, OrderDate, OtherOrderColumns}
OrdersDueDate {CustomerId, DueDate, OtherOrderColumns}
Queste due relazioni hanno molti attributi in comune e questo è un tipo di ridondanza. Puoi
risolvere tale ridondanza tramite la generalizzazione introducendo i sopratipi.
Due entità sono di tipi distinti, o primitivi, se non hanno attributi in comune. Alcune relazioni
possono avere sia attributi in comune sia attributi distinti. Se hanno un identificatore in
comune (ossia uno schema di identificazione primario in comune o una chiave primaria in
comune) si può parlare di relazione sopratipo/sottotipo speciale. I sopratipi e i sottotipi sono
utili per rappresentare diversi livelli di generalizzazione o specializzazione. Nella descrizione
di un problema aziendale, il verbo è (o esplicitamente è un tipo di) porta a una relazione
sopratipo/sottotipo. Ad esempio, un cliente è un partner, così come un fornitore. Ovviamente, i
clienti e i fornitori hanno qualcosa in comune.
Nell'esempio precedente, i partner sono un sopratipo dei clienti e dei fornitori. Se inizi con
i sottotipi e trovi un sopratipo, stai usando un approccio dal basso verso l'alto. L'approccio
dall'alto verso il basso è il contrario. Quando esegui la generalizzazione o la specializzazione,
si verifica lo stesso problema: dove fermarsi? È possibile rispondere facilmente alla domanda
con l'approccio dall'alto verso il basso. Ferma la specializzazione (in altre parole, ferma
l'introduzione) dei sottotipi quando non ci sono altri attributi interessanti per un altro livello
di sottotipi. La tecnica opposta è più problematica; dopo tutto puoi finire con solo poche
entità, ad esempio soggetti, oggetti ed eventi. Una possibile condizione di arresto è quando
raggiungi gli oggetti astratti, o gli oggetti che non esistono nel mondo reale. Gli oggetti
astratti non fanno parte di un database relazionale. Tuttavia, a volte è pratico introdurre
un sopratipo solo per condividere uno schema di identificazione comune anche tra entità
disgiunte. Per esperienza si consiglia un approccio pratico che funzioni bene per se stessi:
fermati quando hai un problema nel denominare il sopratipo (quando raggiungi nomi
come cosa). Se non puoi denominarlo immediatamente, probabilmente stai provando a
generalizzare entità disgiunte.
Di seguito vengono illustrati altri consigli pratici per la generalizzazione e la specializzazione.
Se hai una tabella con alcuni valori noti e molti valori NULL in alcune colonne, probabilmente
è opportuno eseguire la specializzazione. Controlla se i valori NULL rappresentano valori
Capitolo 3
Il modello relazionale
125
sconosciuti o attributi che non sono applicabili alle righe in cui compaiono. Puoi eliminare i
valori NULL per gli attributi che non sono applicabili se introduci i sottotipi. Per l'approccio
dal basso verso l'alto, le tabelle che hanno molte colonne con nomi simili o addirittura uguali
probabilmente necessitano di una tabella sopratipo. Nota che sei nuovamente dipendente da
una buona convenzione di denominazione.
Nella figura 3-15 vengono illustrate le entità che devono essere generalizzate.
CustomersOriginal
PK
CustomerId
CompanyName
Address
DiscountCode
SuppliersOriginal
PK
SupplierId
CompanyName
Address
URL
FIGURA 3-15 Prima della generalizzazione
È necessario citare un problema notevole con la generalizzazione. Cosa succede se il sistema
con la struttura della figura 3-15 è già in ambiente di produzione con una notevole quantità di
dati già inseriti? In tal caso la generalizzazione non è così semplice. Non solo devi introdurre
un modello generalizzato come quello illustrato nella figura 3-16, ma anche fare attenzione ai
dati. Devi unire e togliere i duplicati di clienti e fornitori nel caso in cui un cliente fosse anche
un fornitore.
Partners
Customers
PK PartnerId
PK,FK1 PartnerId
PK,FK1 PartnerId
DiscountCode
Suppliers
CompanyName
Address
URL
FIGURA 3-16 Dopo la generalizzazione
Hai ricevuto molti consigli pratici su come trovare sopratipi e sottotipi. È stata inoltre citata
una regola formale relativa al momento in cui arrestare la specializzazione: quando non hai
più attributi da aggiungere a un sottotipo. Tuttavia, per rendere coerente questo argomento
con il resto delle teorie espresse nel capitolo, è necessaria una definizione formale di quando
arrestare la generalizzazione.
Principio della progettazione ortogonale
Trovi i sopratipi più generali quando nessuna delle due relazioni viene definita in modo tale
che possano rappresentare gli stessi fatti. Una definizione più formale afferma che il database
deve essere conforme al principio della progettazione ortogonale, come affermato in An
Introduction to Database Systems, 8th edition di C. J. Date (Addison-Wesley, 2003) (informazioni
in lingua inglese):
A e B devono essere variabili relazionali distinte. Non devono esistere
decomposizioni senza perdite di A e B in A1, A2, . . ., Am e B1, B2, . . ., Bn
(rispettivamente) così che alcune proiezioni di Ai nel set A1, A2, . . ., Am e alcune
proiezioni di Bj nel set B1, B2, . . ., Bn abbiano significati sovrapposti.
126
Microsoft Transact SQL Query
Ora è opportuno aggiungere qualche parola di spiegazione. Il termine ortogonale significa
che le relazioni devono avere significati reciprocamente indipendenti, e questo è esattamente
quello che si desidera per i tipi primitivi. Potresti notare che il principio è solo buon senso
formalizzato. Mentre la normalizzazione riduce la ridondanza all'interno delle relazioni, la
generalizzazione (o la progettazione ortogonale) riduce la ridondanza tra le relazioni. Infine,
la specializzazione riduce la necessità di usare il valore NULL per un attributo che non è
applicabile. Nota inoltre che il principio della progettazione ortogonale evita anche inutili
decomposizioni orizzontali in base alle restrizioni di non sovrapposizione della relazione
originale, in quanto otterresti di nuovo delle proiezioni delle nuove relazioni decomposte
con significati sovrapposti. L'implicazione della progettazione ortogonale è che anche se le
relazioni A e B hanno lo stesso tipo di intestazione, deve essere mantenuto quanto segue:
A Union B
: è un'unione disgiunta
A Intersect B
: è vuoto
A Minus B
: è uguale ad A
Puoi usare queste equazioni per verificare se hai relazioni senza significati sovrapposti.
Conclusioni
Questo capitolo ha introdotto il modello relazionale. Sono stati spiegati termini base come
tipo, tupla, relazione e attributo. Il significato di una relazione dovrebbe ora essere chiaro, e
dovresti riconoscere che puoi considerare una relazione come un'entità aziendale o capirla
come un predicato con proposizioni. Puoi inoltre usare questo doppio approccio quando
esegui la manipolazione delle relazioni; puoi essere prescrittivo, usando l'algebra relazionale,
o descrittivo, usando il calcolo relazionale. Sono stati sottolineati l'importanza dell'integrità
dei dati e il significato di mantenerla. Sono inoltre stati illustrati in modo esaustivo i vincoli.
È stato poi affrontato il problema dei valori NULL. Il capitolo si è concluso con un insieme
di regole formali e principi per ottenere una buona progettazione, incluse normalizzazione
e progettazione ortogonale. È stato sottolineato più volte come una buona convenzione di
denominazione sia fondamentale per una buona progettazione. Tutte le nozioni teoriche
illustrate in questo capitolo e nel capitolo 2 dovrebbero aiutarti a capire le query avanzate che
incontrerai nei prossimi capitoli.