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 := X

All’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 ∪ S

Aggiungiamo 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.

  1. 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).
  2. 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 r

La 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 è cambiato

Ciclo 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
end

Per 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)

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:

RiABCD
R1 (AB)aAaBb1Cb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

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]).
RiABCD
R1 (AB)aAaBb1Cb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

Confronto tra R2 e R3:

  • Su A: b2A b3A quindi A → B qui non si applica (R2[A] R3[A]).
RiABCD
R1 (AB)aAaBb1Cb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

Confronto tra R1 e R3:

  • Su A: aA b3A quindi A → B qui non si applica (R1[A] R3[A]).
RiABCD
R1 (AB)aAaBb1Cb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

B → C :

Confronto tra R1 e R2:

  • Su B: aB = aB.
  • Su C: b1C aC quindi uniformo C mettendo aC in R1.
RiABCD
R1 (AB)aAaBaCb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

Confronto tra R2 e R3:

  • Su B: aB b3B quindi B → C qui non si applica (R2[B] R3[B]).
RiABCD
R1 (AB)aAaBaCb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

Confronto tra R1 e R3:

  • Su B: aB b3B quindi B → C qui non si applica (R1[B] R3[B]).
RiABCD
R1 (AB)aAaBaCb1D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

C → D :

Confronto tra R1 e R2:

  • Su C: aC = aC.
  • Su D: b1D b2D quindi uniformo C mettendo b2D in R1.
RiABCD
R1 (AB)aAaBaCb2D
R2 (BC)b2AaBaCb2D
R3 (CD)b3Ab3BaCaD

Confronto tra R2 e R3:

  • Su C: aC = aC.
  • Su D: b1D aD quindi uniformo C mettendo aC in R2.
RiABCD
R1 (AB)aAaBaCb1D
R2 (BC)b2AaBaCaD
R3 (CD)b3Ab3BaCaD

Confronto tra R1 e R3:

  • Su C: aC = aC.
  • Su D: b1D aD quindi uniformo C mettendo aC in R1.
RiABCD
R1 (AB)aAaBaCaD
R2 (BC)b2AaBaCaD
R3 (CD)b3Ab3BaCaD

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è tamρ(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 tamρ(rf) e rf = mρ(rf) allora possiamo concludere che tarf, 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 ∪ A

Scansiona 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 - S

Rimuove gli attributi orfani da R.

ρ := ρ ∪ S

Aggiunge 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: ρ := ρ ∪ R

Se 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:

else

Se 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 
end

S è 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: ρ := ρ ∪ R

Esiste 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)
end

Per 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:


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 ⊆ Ge 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 ji - 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.