La decomposizione di uno schema di relazione è un processo fondamentale nella progettazione di basi di dati. L’obiettivo principale è ottenere schemi più semplici e coerenti, spesso in Terza Forma Normale (3NF), garantendo al tempo stesso due requisiti essenziali:
- Preservare le dipendenze funzionali: tutte le regole di integrità che valevano sullo schema originario devono continuare a valere.
- Assicurare il join senza perdita: ogni istanza legale dello schema originario deve poter essere ricostruita mediante join naturale, senza introdurre tuple spurie o informazioni estranee.
La decomposizione viene tipicamente applicata quando lo schema non è già in 3NF, oppure per motivi di efficienza degli accessi (ridurre la dimensione delle tuple permette di caricarne di più in memoria e separare informazioni usate in contesti diversi migliora le prestazioni).
DEFINIZIONE
Sia R uno schema di relazione. Una decomposizione di R è una famiglia ρ = {R1, R2, …, Rk} di sottoinsiemi di R che ricopre R, ovvero:
Se lo schema R è composto da un certo insieme di attributi, decomporlo significa definire dei sottoschemi che contengono ognuno un sottoinsieme degli attributi di R. I sottoschemi possono avere attributi in comune, e la loro unione deve necessariamente contenere tutti gli attributi di R.
DECOMPOSIZIONI CHE PRESERVANO LE DIPENDENZE
Sia R uno schema di relazione. F un insieme di dipendenze funzionali su R e ρ = {R1, R2, …, Rk} decomposizione di R, diciamo che ρ preserva F se:
dove:
Ogni πRi(F) è un insieme di dipendenze funzionali dato dalla proiezione dell’insieme di dipendenze funzionali F sul sottoschema Ri. Contiene solo le dipendenze di F+ che hanno tutti gli attributi (determinati e determinanti) in Ri.
Verifica:
Verificare se una decomposizione preserva un insieme di dipendenze funzionali F richiede che venga verificata l’equivalenza dei due insiemi di dipendenze funzionali F e G=∪i=1k πRi(F) e quindi che valga la doppia inclusione F+⊆ G+ e G+⊆ F+.
Prima inclusione:
Dimostriamo che G+⊆ F+:
Per come è stato definito G in questo caso sarà sicuramente G ⊆ F+, infatti: G=∪i=1k πRi(F), dove πRi(F) = { X → Y | X → Y ∈ F+ ∧ XY ⊆ Ri } quindi ogni πRi(F) che viene inclusa in G è per definizione una porzione di F+. Inoltre, per il lemma: G ⊆ F+ ⟹ G+⊆ F+, inclusione verificata.
Seconda inclusione:
Dimostriamo che F+⊆ G+:
Dimostrare che F ⊆ G+ significa verificare che per ogni X → Y ∈ F, vale Y ⊆ X+G, infatti: Se Y ⊄ X+G allora X → Y ∉ GA per il lemma e quindi X → Y ∉ G+ per il teorema. Quindi basta verificare che anche una sola dipendenza in F non appartiene alla chiusura di G per poter affermare che l’equivalenza non sussiste. Per farlo si utilizza un semplice algoritmo iterativo:
Algoritmo verifica F ⊆ G+:
begin
success := true
for every X → Y ∈ F do:
begin
calculate Z # Z = chiusura di X rispetto a G
if Y ⊄ Z then: success := false
end
end- Input: due insiemi F e G di dipendenze funzionali su uno schema R e la chiusura X+G dell’insieme di attributi X rispetto a G (Z = X+G).
- Output: la variabile booleana success (se success = true allora F ⊆ G+).
Per il calcolo di X+G si potrebbe ricorrere all’algoritmo del calcolo della chiusura di un insieme di attributi, ma per farlo dovremmo prima calcolare G. Sappiamo però che per definizione: G=∪i=1k πRi(F), dove πRi(F) = { X → Y | X → Y ∈ F+ ∧ XY ⊆ Ri } Dunque per costruire G in modo diretto, dovremmo prima avere F+. Ma il calcolo di F+ è noto per essere esponenziale nel numero di attributi, perché può generare un numero enorme di dipendenze funzionali implicate. Ricorriamo quindi a un algoritmo che ci permette di calcolare X+G a partire da F:
Algoritmo calcolo X+G:
begin
Z := X
S := Ø
for i := 1 to k do: S := S ∪ [(Z ∩ Rᵢ)⁺ ∩ Rᵢ]
while S ⊄ Z do:
begin
Z := Z ∪ S
for i := 1 to k do: S := S ∪ [(Z ∩ Rᵢ)⁺ ∩ Rᵢ]
end
end- Input: uno schema R, un insieme F di dipendenze funzionali su R, una decomposizione ρ = {R1, R2, …, Rk} di R, un sottoinsieme X di R;
- Output: la chiusura X+G di X rispetto a G=∪i=1k πRi(F) (nella variabile Z).
Idea di base:
Invece di calcolare prima F+ e poi costruire G (operazione esponenziale), l’algoritmo simula direttamente la proiezione e l’applicazione delle dipendenze di G usando solo F. Il meccanismo è semplice: per ogni sottoschema Ri, si prende Z ∩ Ri (gli attributi di Z che stanno in Ri), si calcola la loro chiusura rispetto a F, e si aggiungono a Z solo gli attributi risultanti nella chiusura che appartengono a Ri.
Inizializzazione:
Z sarà la variabile che contiene X+G.
Z := XAll’inizio conosciamo solo che X determina se stesso (per riflessività), quindi X ⊆ X+G.
S := ØS è l’insieme temporaneo di attributi che si possono aggiungere a Z ad ogni ciclo (inizialmente vuoto).
Ciclo di scelta iniziale degli attributi:
for i := 1 to k do: S := S ∪ [(Z ∩ Rᵢ)⁺ ∩ Rᵢ]Per ogni sottoschema Ri:
- Prendi gli attributi già presenti in Z che appartengono a Ri : Z ∩ Ri
- Calcola la loro chiusura rispetto a F : (Z ∩ Ri)+F
- Mantieni solo gli attributi della chiusura che stanno in Ri : (Z ∩ Ri)+F ∩ Ri
- Aggiungi a S gli attributi risultanti : S ∪ [(Z ∩ Ri)+F ∩ Ri]
Ciclo di espansione:
while S ⊄ Z do:Se S contiene attributi non ancora in Z, allora possiamo espandere Z.
Z := Z ∪ SAggiungiamo S a Z.
for i := 1 to k do: S := S ∪ [(Z ∩ Rᵢ)⁺ ∩ Rᵢ]Ripetiamo il calcolo di S con il nuovo Z. Questo può produrre ulteriori attributi deducibili, che verranno aggiunti al prossimo ciclo.
Continuiamo finché S non aggiunge più nulla (cioè S ⊆ Z).
Teorema sulla validità dell’algoritmo calcolo X+G:
L’algoritmo calcola correttamente X+G con G=∪i=1k πRi(F).
Dimostrazione:
Indichiamo con Z0 il valore iniziale di Z (Z0 = X) e con Zj ed Sj, con j ≥ 1, i valori di Z ed S dopo la j-esima esecuzione del corpo del ciclo. Facile vedere che, per ogni j:
Ricorda: In Zj ci sono gli attributi aggiunti a Z fino alla j-esima iterazione. Alla fine di ogni iterazione aggiungiamo qualcosa a Z da S, ma non eliminiamo mai nessun attributo in Z.
Sia f tale che Sf ⊆ Zf (cioè Zf = valore di Z al termine dell’algoritmo = X+G), dimostriamo che:
Prima inclusione:
Dimostriamo che Zf ⊆ X+G:
Caso base (j = 0):
Per riflessività, tutti gli attributi in X appartengono a X+G, perché X+G è l’insieme di tutti gli attributi determinati in G a partire da X. Caso base verificato.
Ipotesi induttiva:
Si assume che tutti gli attributi ottenuti con al più j iterazioni dell’algoritmo siano già in X+G.
Passo induttivo (j > 0):
Bisogna mostrare che anche ogni attributo in Zj+1 è contenuto in X+G, dunque che ogni attributo in Zj ∪ Sj è contenuto in X+G (dato che per l’algoritmo: Zj+1 = Zj ∪ Sj).
Se A ∈ Zj allora per ipotesi induttiva A ⊆ X+G, dunque in questo caso il passo è dimostrato.
Se A ∈ Sj allora per definizione di Sj esiste un indice i ≤ k tale che A ∈ [(Zj ∩ Ri)+F ∩ Ri]. Dunque, per definizione di chiusura, se A ∈ Sj allora (Zj ∩ Ri) → A ∈ F+, con (Zj ∩ Ri) ∪ A ⊆ Ri. Ciò significa che A è derivabile da (Zj ∩ Ri) usando dipendenze che coinvolgono solo attributi di Ri. Tali dipendenze appartengono sicuramente alla proiezione πRi(F) = { X → Y | X → Y ∈ F+∧ XY ⊆ Ri }, (in questo caso X = (Zj ∩ Ri) e Y = A) quindi, per definizione, appartengono a G (ricordiamo che G=∪i=1k πRi(F)). Dunque, se (Zj ∩ Ri) → A ∈ G e (Zj ∩ Ri) ⊆ Zj ⊆ X+G allora A ⊆ X+G. Anche in questo caso il passo è dimostrato.
Abbiamo dimostrato che l’algoritmo non introduce attributi “a caso”. Ogni attributo che aggiunge è davvero derivabile da X usando le dipendenze in G, quindi Zf ⊆ X+G.
Seconda inclusione:
Dimostriamo che X+G ⊆ Zf:
L’algoritmo parte da (Z0 = X) e poi aggiunge attributi senza mai rimuoverne. Quindi alla fine sarà sempre valida la condizione:
L’algoritmo si ferma solo quando non riesce più ad aggiungere attributi applicando le dipendenze funzionali in G. Questo significa che, se esistesse una dipendenza Y → A ∈ G con Y ⊆ Zf ma A ∉ Zf, l’algoritmo potrebbe ancora aggiungere A a Zf e quindi non sarebbe terminato. Poiché è terminato (abbiamo raggiunto Zf), una situazione del genere non può esistere e quindi possiamo dire che Zf è già chiuso, cioè:
Una proprietà generale della chiusura di X che nasce dalla monotonia delle chiusure è: se X ⊆ Y allora X+G ⊆ Y+G. Qui sappiamo che X ⊆ Zf, quindi: X+G ⊆ (Zf)+G. Ma dato che (Zf)+G = Zf, allora X+G ⊆ Zf. L’inclusione è dimostrata, infatti, al termine dell’algoritmo, Zf deve contenere tutto ciò che è derivabile da X in G.
DECOMPOSIZIONI CON JOIN SENZA PERDITA
Sia R uno schema di relazione. Una decomposizione ρ = {R1, R2, …, Rk} di R ha un join senza perdita se per ogni istanza legale r di R si ha:
Ogni πRi(r) è un insieme di tuple dato dalla proiezione dell’istanza r sul sottoschema Ri. Contiene solo le tuple di r che hanno tutti gli attributi in Ri.
Teorema sulle decomposizioni con join senza perdita
Sia R uno schema di relazione e ρ = {R1, R2, …, Rk} una decomposizione di R. Per ogni istanza legale r di R, indicato con mρ(r) = πR1(r) ⨝ πR2(r) ⨝ … ⨝ πRk(r) si ha:
- r ⊆ mρ(r)
- πRi(mρ(r)) = πRi(r)
- mρ(mρ(r)) = mρ(r)
mρ(r) è il join delle proiezioni di r sui sottoschemi Ri, cioè l’istanza di R ricostruita a partire dalla decomposizione ρ.
Dimostrazione r ⊆ mρ(r):
Dimostriamo che ogni tupla di r compare anche in mρ(r). Il join delle proiezioni infatti non perde mai tuple originali (può solo aggiungerne di altre).
Sia t una tupla ∈ r. Proiettando r su Ri, la parte di t sugli attributi di Ri compare in πRi(r) (ovvero t[Ri] ∈ πRi(r) per ogni i ∈ {1, …, k}). Nel join naturale mρ(r) delle proiezioni, tutte queste parti compatibili si ricombinano, quindi la tupla t viene ricostruita, ovvero t ∈ mρ(r) (ogni tupla di r è anche in mρ(r)).
Dimostrazione πRi(mρ(r)) = πRi(r):
Dimostriamo che se si prende il join mρ(r) e lo si proietta su Ri, si ottiene esattamente la stessa proiezione che si aveva da r. Bisogna dimostrare due inclusioni.
- Per il punto 1 si ha r ⊆ mρ(r) e, quindi πRi(r) ⊆ πRi(mρ(r)) è dimostrata (proiettando entrambi sui campi Ri, l’inclusione resta valida).
- Dimostriamo ora che anche πRi(mρ(r)) ⊆ πRi(r). Sia t ∈ mρ(r). Per definizione di join naturale, la tupla t è stata ottenuta combinando tuple provenienti dalle proiezioni di r, cioè: t = t1 ∪ t2 ∪ … ∪ tk con t1 ∈ πR1(r), t2 ∈ πR2(r), …, tk ∈ πRk(r) e tutte compatibili sugli attributi in comune. In particolare, la parte della tupla t sugli attributi Ri coincide con la tupla ti, quindi t[Ri] = ti. Poiché ti ∈ πRi(r), segue che: t[Ri] ∈ πRi(r). Questo vale per ogni t ∈ mρ(r), quindi πRi(mρ(r)) ⊆ πRi(r).
Dimostrazione mρ(mρ(r)) = mρ(r):
Dimostriamo che se si prende l’istanza ricostruita mρ(r) e si applica di nuovo la stessa operazione di decomposizione e join, si ottiene lo stesso risultato (idempotenza). Questo è importante perché assicura che la ricostruzione è stabile e non dipende da quante volte si applica l’operazione.
Per definizione mρ(mρ(r)) = πR1(mρ(r)) ⨝ πR2(mρ(r)) ⨝ … ⨝ πRk(mρ(r)). Dal punto 2 già dimostrato sappiamo che πRi(mρ(r)) = πRi(r) per ogni i. Sostituendo: mρ(mρ(r)) = πR1(r) ⨝ πR2(r) ⨝ … ⨝ πRk(r) = mρ(r).
Algoritmo verifica join senza perdita:
begin
build r
repeat
begin
for every X → Y ∈ F do:
begin
if ∃ t₁,t₂ ∈ r tali che t₁[X] = t₂[X] e t₁[Y] ≠ t₂[Y] then:
begin
for every Aⱼ in Y do:
begin
if t₁[Aⱼ] = "aⱼ" then: t₂[Aⱼ] := t₁[Aⱼ]
else: t₁[Aⱼ] := t₂[Aⱼ]
end
end
end
end
until: r ha almeno una riga con tutte "aⱼ" || r non è cambiato
if r ha almeno una riga con tutte "aⱼ" then: ρ ha un lossless join
else: ρ non ha un lossless join
end- Input: uno schema di relazione R, un insieme F di dipendenze funzionali su R, una decomposizione ρ = {R1, R2, …, Rk} di R.
- Output: decide se ρ ha un join senza perdita.
Costruzione di r :
build rLa tabella r ha:
- |R| colonne j (una per ogni attributo di R)
- |ρ| righe i (una per ogni sottoschema Ri)
In ogni cella di r :
- Se l’attributo Aj ∈ Ri, mette il simbolo aj.
- Se l’attributo Aj ∉ Ri, mette un simbolo distinto bij.
N.B. Ogni riga di r rappresenta una proiezione πRi(r) del join, definendo gli attributi di Ri con aj, e gli attributi non in Ri con simboli diversi.
Ciclo di propagazione:
repeat
...
until: r ha almeno una riga con tutte "aⱼ" || r non è cambiatoCiclo che termina se:
- r contiene almeno una riga con tutte aj nei campi, oppure
- r non è nell’ultima iterazione del ciclo (cioè si propagano nuove uguaglianze).
for every X → Y ∈ F do:
begin
if ∃ t₁,t₂ ∈ r tali che t₁[X] = t₂[X] e t₁[Y] ≠ t₂[Y] then:
begin
for every Aⱼ in Y do:
begin
if t₁[Aⱼ] = "aⱼ" then: t₂[Aⱼ] := t₁[Aⱼ]
else: t₁[Aⱼ] := t₂[Aⱼ]
end
end
endPer ogni X → Y ∈ F :
- Se ∃ t1, t2 ∈ r tali che t1[X] = t2[X] e t1[Y] ≠ t2[Y] (definizione di dipendenza funzionale), allora:
- Per ogni attributo Aj nell’insieme Y (determinato):
- Se t1[Aj] = aj allora : t2[Aj] acquisisce il valore di t1[Aj] (ovvero aj)
- Se t1[Aj] ≠ aj (quindi t1[Aj] = bij) allora : t1[Aj] acquisisce il valore di t2[Aj] (che potrebbe essere aj o bij)
- Per ogni attributo Aj nell’insieme Y (determinato):
N.B. In questo modo stiamo anche rendendo l’istanza r un’istanza legale. Infatti alla fine del ciclo avremo che per ogni t1[X] = t2[X] allora t1[Y] = t2[Y].
Verifica finale:
if r ha almeno una riga con tutte "aⱼ" then: ρ ha un lossless join
else: ρ non ha un lossless join- Se esiste una riga in r con tutti aj : significa che la decomposizione è senza perdita di join, perché quella riga rappresenta la tupla originale ricostruita senza ambiguità.
- Se non esiste una riga in r con tutti aj : la decomposizione è con perdita, perché non si riesce a ricostruire le tuple originali senza generare simboli spuri.
Esempio:
R = ABCD F = { A → B, C → D, B → C } ρ = { R1 = AB, R2 = BC, R3 = CD }
Verificare se ρ è una decomposizione con join senza perdita.
Costruzione della tabella r :
Colonne: A, B, C, D Righe: una per ciascun sottoschema R1, R2, R3
Regola di riempimento:
- Se l’attributo Aj ∈ Ri, metto aj
- Altrimenti metto un simbolo distinto bij
Tabella iniziale:
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | b1C | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
Ciclo di propagazione con le FD di F:
Applichiamo ripetutamente: per ogni X → Y ∈ F: Se esistono due righe t1, t2 tali che t1[X] = t2[X] e t1[Y] ≠ t2[Y], allora si uniformano i simboli in Y (preferendo gli aj quando presenti):
A → B :
Confronto tra R1 e R2:
- Su A: aA ≠ b2A quindi A → B qui non si applica (R1[A] ≠ R2[A]).
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | b1C | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
Confronto tra R2 e R3:
- Su A: b2A ≠ b3A quindi A → B qui non si applica (R2[A] ≠ R3[A]).
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | b1C | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
Confronto tra R1 e R3:
- Su A: aA ≠ b3A quindi A → B qui non si applica (R1[A] ≠ R3[A]).
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | b1C | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
B → C :
Confronto tra R1 e R2:
- Su B: aB = aB.
- Su C: b1C ≠ aC quindi uniformo C mettendo aC in R1.
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | aC | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
Confronto tra R2 e R3:
- Su B: aB ≠ b3B quindi B → C qui non si applica (R2[B] ≠ R3[B]).
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | aC | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
Confronto tra R1 e R3:
- Su B: aB ≠ b3B quindi B → C qui non si applica (R1[B] ≠ R3[B]).
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | aC | b1D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
C → D :
Confronto tra R1 e R2:
- Su C: aC = aC.
- Su D: b1D ≠ b2D quindi uniformo C mettendo b2D in R1.
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | aC | b2D |
| R2 (BC) | b2A | aB | aC | b2D |
| R3 (CD) | b3A | b3B | aC | aD |
Confronto tra R2 e R3:
- Su C: aC = aC.
- Su D: b1D ≠ aD quindi uniformo C mettendo aC in R2.
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | aC | b1D |
| R2 (BC) | b2A | aB | aC | aD |
| R3 (CD) | b3A | b3B | aC | aD |
Confronto tra R1 e R3:
- Su C: aC = aC.
- Su D: b1D ≠ aD quindi uniformo C mettendo aC in R1.
| Ri | A | B | C | D |
|---|---|---|---|---|
| R1 (AB) | aA | aB | aC | aD |
| R2 (BC) | b2A | aB | aC | aD |
| R3 (CD) | b3A | b3B | aC | aD |
Condizione di terminazione e controllo:
Il ciclo si ferma quando:
- La tabella non cambia più, oppure
- Appare una riga con tutti simboli aj.
A questo punto, la riga R1 è già tutta in aj, condizione sufficiente per concludere che la decomposizione ρ = { R1 = AB, R2 = BC, R3 = CD } è senza perdita.
Teorema sulla validità dell’algoritmo verifica join senza perdita:
L’algoritmo verifica correttamente se una decomposizione ρ di R ha un join senza perdita.
Dimostrazione:
Occorre dimostrare che: se ρ ha un join senza perdita (mρ(r) = r per ogni r legale), allora, quando l’algoritmo termina, la tabella r ha una tupla con tutte aj.
Costruiamo r0, ovvero l’stanza r nella fase iniziale dell’algoritmo:
- creiamo una tupla ti0 per ogni Ri
- inseriamo aj nelle colonne degli attributi di Ri
- inseriamo simboli distinti bij nelle altre colonne.
Applichiamo il ciclo di propagazione fino ad ottenere rf, ovvero l’stanza r nella fase finale dell’algoritmo.
Per ogni tupla tif nelle colonne di Ri restano sicuramente tutti aj (per la costruzione di r), quindi tif[Ri] = (aj1, aj2, …) . Cioè la proiezione della tupla ti sugli attributi del suo schema Ri contiene solo aj.
Definiamo ora la tupla ta tale che ta[R]= (aj1, aj2, …, ajn) cioè la tupla su tutto R che ha solo aj. Consideriamo poi il join naturale delle proiezioni delle tuple. Il join combina tuple che coincidono sugli attributi in comune:
Osserviamo che, dato che ogni tupla tif ha sicuramente aj nelle colonne del sottoschema Ri corrispondente, sugli attributi comuni tra due sottoschemi Ri1 e Ri2, con Ri1 ∩ Ri2 ≠ Ø (con almeno un attributo in comune su cui fare il join) il valore è sempre aj. Dunque le tuple sono compatibili e il join produce sicuramente la tupla che ha aj ovunque. Quindi:
Inoltre, per definizione, sappiamo che:
Ora tif[Ri] è una delle tuple contenute nella corrispondente proiezione πRi(rf), di conseguenza il join delle sole tif[Ri] produce un sottoinsieme del join di tutte le proiezioni. Quindi:
Cioè ta ∈ mρ(rf).
Abbiamo infine che rf è legale (soddisfa F, perché l’algoritmo ha applicato e soddisfatto tutte le dipendenze) e ρ è lossless. Per definizione di lossless: rf = mρ(rf). Dunque se ta ∈ mρ(rf) e rf = mρ(rf) allora possiamo concludere che ta ∈ rf, cioè in rf esiste una riga con tutti aj (l’algoritmo è corretto).
CALCOLO DELLA DECOMPOSIZIONE
Dato uno schema di relazione R e una copertura minimale F su R è sempre possibile calcolare in tempo polinomiale una decomposizione ρ = {R1, R2, …, Rk} di R tale che:
- per ogni i, i=1, …, k : Ri è in 3NF.
- ρ preserva F.
Per il calcolo ci serviamo di un algoritmo:
Algoritmo calcolo ρ:
begin
S := Ø
for every A ∈ R such that ∄ X → Y ∈ F | A ∈ X ∪ Y do: S := S ∪ A
if S ≠ Ø then:
begin
R := R - S
ρ := ρ ∪ S
end
if ∃ X → Y ∈ F | (X ∪ Y = R) then: ρ := ρ ∪ R
else
begin
for every X → A ∈ F do: ρ := ρ ∪ (X ∪ A)
end
end- Input: uno schema R, una copertura minimale F su R.
- Output: una decomposizione ρ di R che preserva F e che per ogni i, i=1, …, k : Ri è in 3NF.
N.B. La copertura minimale non garantisce che una chiave candidata compaia ancora in qualche dipendenza funzionale. Per questo motivo, se si desidera garantire anche la proprietà di join senza perdita, è necessario verificare che la decomposizione finale ρ contenga almeno un sottoschema che includa una chiave candidata di R. Se ciò non accade, allora si aggiunge esplicitamente una relazione contenente una chiave candidata: ρ := ρ ∪ {K} dove K è una chiave candidata di R. Questo passo non è obbligatorio per la sola preservazione delle dipendenze e per la 3NF, ma diventa necessario se si vuole assicurare che la decomposizione sia anche lossless.
Inizializzazione:
S := ØS è l’insieme temporaneo di attributi orfani, cioè che non sono coinvolti in nessuna DF di F (inizialmente vuoto).
Raccolta degli attributi orfani:
for every A ∈ R such that ∄ X → Y ∈ F | A ∈ X ∪ Y do: S := S ∪ AScansiona ogni attributo A di R e verifica se non appare in nessuna dipendenza di F (né nel determinante X né nel determinato Y). Se l’attributo soddisfa tale condizione (è orfano) lo aggiunge a S.
Separazione degli attributi orfani:
if S ≠ Ø then:Se almeno un attributo di R è orfano:
R := R - SRimuove gli attributi orfani da R.
ρ := ρ ∪ SAggiunge S come nuova relazione (sottoschema di R) nella decomposizione. È una relazione contenente solo attributi orfani.
Controllo di una dipendenza totale su R:
if ∃ X → Y ∈ F | (X ∪ Y = R) then: ρ := ρ ∪ RSe esiste una DF in F il cui insieme di attributi (determinante ∪ determinato) copre esattamente tutti gli attributi attuali di R. Allora aggiunge lo schema R alla decomposizione. Questo copre tutte le DF e rispetta la 3NF perché con copertura minimale e con determinanti appropriati, le violazioni sono evitate o rese ammissibili (attributi primi o dipendenze da chiave).
Calcolo della decomposizione per DF minimali:
elseSe non esiste una DF totale su R:
for every X → A ∈ F do: ρ := ρ ∪ (X ∪ A)Per ogni dipendenza minimale con lato destro singolo A (come richiesto dalla copertura minimale) aggiunge una relazione (sottoschema di R) contenente gli attributi di X insieme all’attributo A (attributi della DF). (N.B.)
Perché funziona:
Preservazione delle DF: ogni dipendenza minimale X → A ha una relazione Ri = XA dove la dipendenza è naturalmente valida, quindi l’insieme F è preservato (o ricostruibile via chiusura).
3NF assicurata: con copertura minimale e relazioni Ri costruite come XA, gli attributi determinati sono o primi nelle rispettive relazioni o dipendono da chiavi candidate del loro schema, rispettando la definizione di 3NF. Gli attributi orfani non creano violazioni perché non ci sono DF che li coinvolgono.
Pulizia semantica: separare gli orfani evita di introdurre attributi che non partecipano a vincoli nel mezzo di relazioni Ri governate da DF, riducendo ambiguità e facilitando la manutenzione.
Teorema sulla validità dell’algoritmo calcolo ρ:
L’algoritmo calcola correttamente ρ = {R1, R2, …, Rk} tale che:
- per ogni i, i=1, …, k : Ri è in 3NF.
- ρ preserva F.
Dimostrazione:
Dimostriamo separatamente le due proprietà:
1. ρ preserva F:
Sia G = ∪i=1k πRi(F), per verificare che ρ preserva F dobbiamo dimostrare che F e G siano due insiemi equivalenti.
Prima inclusione:
Dimostriamo che F+⊆ G+:
Sappiamo che per ogni X → A ∈ F si ha XA ∈ ρ, infatti dall’algoritmo notiamo che XA è uno dei sottoschemi di R:
for every X → A ∈ F do: ρ := ρ ∪ (X ∪ A)Dunque per definizione di proiezione, se XA ⊆ Ri, allora X → A ∈ G, quindi F ⊆ G, e quindi F+ ⊆ G+.
N.B. La chiusura è monotona (può solo crescere o restare uguale all’insieme di partenza): quindi se G contiene tutte le DF di F allora la sua chiusura G+ conterrà sicuramente tutte le DF in F+.
Seconda inclusione:
Dimostriamo che G+⊆ F+:
Sempre per definizione, G è ottenuto proiettando F sui vari sottoschemi, quindi ogni DF in G proviene da F+, dunque G ⊆ F+ e, per il lemma: G+⊆ F+
Conclusione:
Abbiamo dimostrato entrambe le inclusioni, dunque F+ = G+, ρ preserva F.
2. Ogni Ri in ρ è in 3NF:
Analizziamo i diversi casi che si possono presentare:
Caso 1 (S ∈ ρ):
if S ≠ Ø then:
begin
R := R - S
ρ := ρ ∪ S
endS è l’insieme degli attributi orfani. Per definizione, se un attributo non è determinato da nessuno, l’unico modo per “coprirlo” è che faccia parte della chiave. Quindi in ogni relazione S, tutti gli attributi sono primi (appartengono alla chiave). Dunque se S ∈ ρ, ogni attributo in S fa parte della chiave e quindi, banalmente, S è in 3NF.
Caso 2 (R ∈ ρ):
if ∃ X → Y ∈ F | (X ∪ Y = R) then: ρ := ρ ∪ REsiste una dipendenza funzionale X → A ∈ F che coinvolge tutti gli attributi di R. Poiché F è una copertura minimale (condizione necessaria affinché l’algoritmo funzioni) la dipendenza X → A è sicuramente della forma: (R - A) → A e quindi R - A è chiave nello schema R. Prendendo una qualsiasi Y → B ∈ F+ con YB ⊆ R:
- Se B = A, allora per minimalità Y = R - A in quanto non può esistere un sottoinsieme proprio della chiave candidata che determina A. Dunque Y è chiave candidata e la DF rispetta la 3NF.
- Se B ≠ A, allora B ∈ R - A, ovvero B appartiene a una chiave candidata, dunque B è un attributo primo e per questo la DF rispetta la 3NF.
Dunque anche in questo caso ρ è in 3NF.
Caso 3 (XA ∈ ρ):
else
begin
for every X → A ∈ F do: ρ := ρ ∪ (X ∪ A)
endPer ogni DF minimale X → A ∈ F, l’algoritmo costruisce il sottoschema X ∪ A. Poiché F è una copertura minimale, non può esistere un sottoinsieme X’⊆ X tale che X’→ A dunque X è chiave nello schema XA. Prendendo una qualsiasi Y → B ∈ F+ con YB ⊆ XA:
- Se B = A, allora per minimalità Y = X in quanto non può esistere un sottoinsieme proprio della chiave candidata che determina A. Dunque Y è chiave candidata e la DF rispetta la 3NF.
- Se B ≠ A, allora B ∈ X, ovvero B appartiene a una chiave candidata, dunque B è un attributo primo e per questo la DF rispetta la 3NF.
Dunque anche in questo caso ρ è in 3NF.
Teorema sul calcolo di ρ con lossless join:
Dato uno schema di relazione R, una copertura minimale F su R e una decomposizione ρ di R prodotta dall’algoritmo di decomposizione: La decomposizione σ = ρ ∪ W, dove W è una chiave per R, è tale che:
- per ogni i, i=1, …, k : Ri è in 3NF.
- σ preserva F.
- σ ha un join senza perdita.
Dimostrazione:
Dimostriamo separatamente le tre proprietà:
1. σ preserva F:
Sia G’= ∪i=1k πRi(F), per verificare che σ preserva F dobbiamo dimostrare che F e G’ siano due insiemi equivalenti.
Poiché ρ preserva F (per il teorema) anche σ preserva F. Infatti σ = ρ ∪ W quindi stiamo aggiungendo una nuova proiezione πZ(F) all’insieme G : G’= G ∪ πZ(F). Dunque F ⊆ G ⊆ G’e quindi per la monotonia della chiusura vale: F+ ⊆ G+ ⊆ G’+ .
Inoltre, per definizione, G’ è ottenuto proiettando F sui vari sottoschemi, quindi ogni DF in G’ proviene da F+, dunque G’⊆ F+ e, per il lemma: G’+⊆ F+
Abbiamo dimostrato entrambe le inclusioni, dunque F+ = G’+, σ preserva F.
2. Ogni Ri in σ è in 3NF:
Poiché ogni schema di relazione in ρ è in 3NF (per il teorema) e σ = ρ ∪ W, è sufficiente verificare che anche lo schema di relazione Z sia in 3NF per poter affermare che ogni schema di relazione in σ sia in è in 3NF.
Supponiamo per assurdo che W non sia una chiave per lo schema Z (sappiamo che è impossibile in quanto, per riflessività, W → W vale sempre). Allora esiste un sottoinsieme W’ di W che determina tutto lo schema W, ovvero tale che W’→ W ∈ F+. Poiché W è chiave per lo schema R, ovvero W → R ∈ F+, pertanto, per transitività abbiamo W’→ R ∈ F+, ma questo contraddice il fatto che W è chiave per lo schema R (verrebbe violato il principio di minimalità). Pertanto W è chiave per lo schema Z e quindi per ogni dipendenza funzionale X → A ∈ F+ con XA ⊆ W: A è attributo primo (appartiene alla chiave W).
3. σ ha un join senza perdita:
Sappiamo che W è una chiave per R, quindi W+= R. Utilizzando quindi l’algoritmo per il calcolo della chiusura su W possiamo determinare tutti gli attributi di R - W grazie alle dipendenze funzionali minime sullo schema R. L’algoritmo produce una lista ordinata di attributi determinati A1, A2, …, An e, per ciascuno di essi, un determinante Yi ⊆ Zi-1 tale che Yi → Ai ∈ F. Quindi l’algoritmo produrrà Z0 = W e Zi = Zi-1 ∪ Ai.
Poiché F è una copertura minimale, per ogni dipendenza Yi → Ai lo schema YiAi appartiene alla decomposizione ρ, e quindi anche a σ.
Applicando alla decomposizione σ l’algoritmo di verifica del join senza perdita, e utilizzando come insieme di dipendenze funzionali su R, l’insieme F di dipendenze Yi → Ai con cui abbiamo calcolato la chiusura W+, possiamo dimostrare per induzione che σ ha un join senza perdita.
Costruiamo la tabella r con una riga per ogni sottoschema di σ. In particolare:
- La riga di W ha inizialmente aj su tutti gli attributi di W.
- La riga di ciascun sottoschema YiAi ha aj su Yi e, per propagazione, anche su Ai.
Consideriamo le dipendenze Y1 → A1, Y2 → A2, …, Yn → An nell’ordine in cui gli attributi entrano nella chiusura di W. Dimostriamo per induzione che, dopo aver applicato la dipendenza Yi → Ai, la riga di W contiene aj su tutti gli attributi Zi.
N.B. L’ordine delle dipendenze dato dal calcolo di W+ ci garantisce che nella tabella r, al passo i, la riga di W ha già aj su Yi, quindi la DF Yi → Ai è applicabile e possiamo propagare la aj su Ai.
Caso base (i = 1):
Quindi :
- La riga W ha già aj su tutti i suoi attributi e quindi anche su Y1.
- La riga Y1A1 ha aj su Y1 e su A1.
Applicando la regola dell’algoritmo alla DF Y1 → A1 propaghiamo la aj di A1 (sulla riga di Y1) pure sulla riga di W. Risultato: la riga di W ha aj su tutti gli attributi di Z1 = W ∪ A1.
Ipotesi induttiva:
Si assume che per ogni i > 1, nella riga corrispondente a W nella tabella r, ci sia una aj in corrispondenza di ogni attributi Aj con j ≤ i - 1 (lossless join sempre verificato).
Passo induttivo (i > 1):
Supponiamo valida l’ipotesi induttiva. Nella riga corrispondente a W nella tabella r ci sono già tutte le aj sugli attributi di Zi-1. Ora consideriamo l’attributo Ai che entra in chiusura grazie alla dipendenza funzionale minimale Yi → Ai con Yi ⊆ Zi-1:
- La riga W, per ipotesi, ha già aj su tutti gli attributi di Zi-1, e quindi anche su Yi.
- La riga Y1A1 ha aj su Y1 e su A1.
Poiché le due righe concordano con aj su Yi, l’algoritmo di verifica del join senza perdita applica la DF Yi → Ai e propaga la aj su Ai anche nella riga di W.
Dunque La riga di W contiene ora aj su tutti gli attributi di Zi = Zi-1 ∪ A1:
Ripetendo questo ragionamento per ogni i=1, …, n, otteniamo che la riga di W si arricchisce progressivamente di tutte le aj fino a coprire l’intero schema:
Quindi, al termine, la tabella r contiene una riga tutta aj, e per definizione dell’algoritmo la decomposizione σ ha un join senza perdita.