BASI DATI PDF
Document Details
Uploaded by ConsistentCosecant
Tags
Summary
This document provides a detailed explanation of the fundamental concepts related to data management, information systems, data models, and database management systems (DBMS). It explores the differences between information systems and computer systems, the importance of data representation and encoding within computer systems, and the structure and function of database systems.
Full Transcript
BASI DATI: **Sistemi Informativi e Sistemi Informatici** Nello svolgimento di ogni attività, la **disponibilità di informazioni** e la **capacità di gestirle efficacemente** sono essenziali, sia a livello individuale sia nelle organizzazioni di ogni dimensione. Ogni organizzazione, infatti, dispon...
BASI DATI: **Sistemi Informativi e Sistemi Informatici** Nello svolgimento di ogni attività, la **disponibilità di informazioni** e la **capacità di gestirle efficacemente** sono essenziali, sia a livello individuale sia nelle organizzazioni di ogni dimensione. Ogni organizzazione, infatti, dispone di un **sistema informativo** che organizza e gestisce le informazioni necessarie per perseguire i propri scopi. È importante notare che l'esistenza di un sistema informativo è solo **in parte legata alla sua automatizzazione**: i sistemi informativi esistono da molto prima dell'era dei calcolatori elettronici (ad esempio, archivi bancari o anagrafici). Quando parliamo della parte automatizzata di un sistema informativo, usiamo il termine **sistema informatico**. Negli ultimi decenni, la diffusione capillare dell'informatica ha fatto sì che molti sistemi informativi siano anche sistemi informatici, ma non bisogna confondere i due concetti. Nelle attività umane più semplici, le informazioni possono essere gestite anche senza rappresentazioni complesse, utilizzando **lingua scritta, numeri o disegni**. In altri casi, le informazioni non vengono nemmeno formalizzate ma restano memorizzate a livello mentale. Con la crescita della complessità delle attività, è diventato necessario **organizzare e codificare** meglio le informazioni. Nei **sistemi informatici**, la **rappresentazione e codifica** delle informazioni diventa cruciale. Le informazioni vengono tradotte in **dati**, che devono essere interpretati per fornire significato. **I dati da soli non hanno alcun significato**; è solo attraverso la loro interpretazione e correlazione che essi diventano informazioni. Le informazioni, a loro volta, arricchiscono la nostra conoscenza del mondo. A livello pratico, possiamo considerare i **dati** come **simboli** (numeri, stringhe, ecc.) che richiedono **elaborazione** per acquisire significato, diventando così **informazioni** utili. Un esempio semplice chiarisce questa distinzione: la stringa \"Ferrari\" e il numero \"8\", scritti su un foglio di carta, sono **dati**. Da soli, non significano nulla. Tuttavia, nel contesto di un ristorante durante una notte di Capodanno, questi dati possono rappresentare un'ordinazione di una bottiglia di spumante della marca Ferrari, da addebitare alla camera numero 8. Questo esempio dimostra come, una volta aggiunte informazioni contestuali, i dati si trasformino in **informazioni**. Introducendo il concetto di **base di dati**, possiamo definire questa come una **collezione di dati** utilizzata per rappresentare le informazioni necessarie per un sistema informativo. Nei sistemi informativi complessi, una **base di dati** diventa uno strumento fondamentale per gestire grandi quantità di dati in modo efficiente. Inoltre, i dati tendono a essere molto più stabili nel tempo rispetto alle procedure che operano su di essi. Per esempio, i **dati bancari** sono rimasti invariati per decenni, mentre le procedure che li gestiscono cambiano più frequentemente. Quando una procedura viene sostituita, la nuova \"eredita\" i **dati** della vecchia, spesso con trasformazioni. Questa stabilità dei **dati** porta alla conclusione che essi rappresentano una **risorsa fondamentale** per l\'organizzazione: un vero e proprio patrimonio da sfruttare e proteggere. **Basi di Dati: Un Pilastro dell\'Informazione** Fin dagli albori dell\'informatica, l\'attenzione verso la **gestione dei dati** ha sempre rappresentato un aspetto centrale. Tuttavia, solo a partire dalla fine degli anni Sessanta sono stati sviluppati **sistemi software specifici per la gestione dei dati**, e, ancora oggi, alcune applicazioni non ne fanno uso. In assenza di un **software dedicato**, la gestione dei dati è stata tradizionalmente affidata a **linguaggi di programmazione** come C e Fortran o, in tempi più recenti, a **linguaggi a oggetti** come C++ e Java. Esistono ancora applicazioni scritte in COBOL, un linguaggio degli anni Sessanta ormai superato. L\'approccio **convenzionale** alla gestione dei dati si basa su **archivi o file** per memorizzare i dati in modo persistente su memoria di massa. Sebbene un file permetta di memorizzare e cercare dati, esso offre solo **semplici meccanismi di accesso** e condivisione. In questo modello, ogni programma gestisce i propri **file \"privati\"**, il che comporta **duplicazioni** dei dati e **possibili incoerenze** quando dati replicati non vengono aggiornati in modo sincrono. Un esempio concreto di questa problematica si può osservare nel contesto universitario, dove diverse entità gestiscono informazioni sui docenti. L\'**ufficio del personale** mantiene i dati relativi alla carriera accademica, le **presidenze delle facoltà** gestiscono gli incarichi di insegnamento, e l\'**ufficio stipendi** utilizza tali informazioni per calcolare le retribuzioni. Se ognuno di questi uffici gestisse le proprie informazioni separatamente, si creerebbero **incoerenze** tra le varie copie dei dati. Le **basi di dati** sono state sviluppate per **risolvere questi problemi**, permettendo la gestione integrata delle informazioni e riducendo **ridondanza** e **incoerenze**. Un **sistema di gestione di basi di dati** (DBMS) è un **software** progettato per gestire **collezioni di dati** di grandi dimensioni, condivise tra più utenti, e garantire che siano **persistenti**, **affidabili** e **private**. Le principali caratteristiche di un **DBMS** sono: - **Grandi dimensioni**: Le basi di dati possono raggiungere dimensioni enormi, misurate in centinaia o migliaia di terabyte. I DBMS devono essere in grado di gestire queste collezioni di dati, che spesso superano la capacità della memoria centrale disponibile. - **Condivisione dei dati**: I dati devono essere **accessibili** a più utenti o applicazioni in modo sicuro, riducendo la ridondanza e prevenendo **inconsistenze**. I DBMS includono un **controllo di concorrenza** per garantire che le operazioni simultanee non causino conflitti. - **Persistenza**: I dati rimangono **disponibili** anche al termine dell\'esecuzione di un programma. A differenza dei dati temporanei gestiti in memoria centrale, i dati in un DBMS sono **permanenti**. - **Affidabilità**: I DBMS offrono meccanismi di **salvataggio e ripristino** per proteggere i dati da errori hardware o software, garantendo che i dati cruciali, come quelli finanziari, siano **preservati nel tempo**. - **Privatezza**: I DBMS regolano l\'accesso ai dati attraverso **meccanismi di autorizzazione**, permettendo solo ad utenti autorizzati di eseguire determinate operazioni sui dati. - **Efficienza ed efficacia**: Un DBMS deve essere **efficiente** nell\'utilizzo delle risorse di sistema e **efficace** nel rendere produttive le attività degli utenti. Sebbene sia possibile gestire dati **persistenti** e **di grandi dimensioni** utilizzando file semplici, i **DBMS offrono funzionalità superiori**, come l\'accesso condiviso e la gestione centralizzata. Ad esempio, in un\'università, un sistema ideale prevedrebbe una **singola base di dati** condivisa tra tutti gli uffici per gestire le informazioni sui docenti, i corsi, le facoltà e gli aspetti amministrativi. Tuttavia, a volte è necessario mantenere basi di dati separate, come nel caso delle informazioni riservate sui docenti rispetto a quelle pubbliche sui corsi, che possono essere accessibili tramite il sito web dell\'ateneo. **Modelli dei dati** Un **modello dei dati** è un insieme di concetti utilizzati per **organizzare** i dati di interesse e descriverne la **struttura** in modo che possa essere compresa e gestita da un elaboratore. Ogni modello dei dati fornisce dei **meccanismi di strutturazione**, che sono analoghi ai costruttori di tipo dei linguaggi di programmazione. Questi meccanismi permettono di definire **nuovi tipi** basati su tipi **predefiniti** (elementari) e costruttori di tipo. Ad esempio, nel linguaggio di programmazione **C**, è possibile definire nuovi tipi attraverso costruttori come **struct**, **union**, **enum**, e **pointer** (\*). Il **modello relazionale**, introdotto formalmente agli inizi degli anni Settanta e diffusosi durante il decennio successivo, rappresenta il modello dei dati più utilizzato e viene considerato il modello di **riferimento** nel campo della gestione dei dati. Il **modello relazionale dei dati** consente di definire tipi attraverso il **costruttore relazione**, che organizza i dati in **insiemi di record a struttura fissa**. Una relazione è comunemente rappresentata come una **tabella**: le **righe** rappresentano specifici record, mentre le **colonne** corrispondono ai campi di ciascun record. È importante notare che l\'**ordine** delle righe e delle colonne in una tabella non ha rilevanza nel contesto del modello relazionale. **Schemi e istanze** Nelle **basi di dati**, si distingue tra una parte **sostanzialmente invariante nel tempo**, detta **schema della base di dati**, e una parte **variabile nel tempo**, chiamata **istanza o stato della base di dati**. Lo **schema** rappresenta le **caratteristiche strutturali dei dati**, mentre l\'**istanza** si riferisce ai **valori effettivi** che tali dati assumono in un dato momento. Ad esempio, nella relazione **DOCENZA**, lo schema è costituito dalla struttura fissa che include due **attributi**: **Corso** e **NomeDocente**. Lo schema della relazione si può rappresentare così:\ **DOCENZA(Corso, NomeDocente)**.\ Le **righe** della tabella, invece, sono soggette a cambiamenti nel tempo, riflettendo l\'**offerta corrente di corsi** e i **docenti associati**. Ad esempio, in un dato momento, l\'istanza potrebbe essere: Basi di dati -- Rossi Reti -- Neri Linguaggi -- Verdi In termini tecnici, lo **schema** rappresenta la componente **intensionale** della base di dati, mentre l\'**istanza** rappresenta la componente **estensionale**. **Architettura a tre livelli delle basi di dati** La nozione di modello e di schema può essere ulteriormente articolata attraverso una proposta di **architettura a tre livelli** per i DBMS. I tre livelli sono: - **Schema esterno**: descrive una porzione della base di dati, specifica per l\'utente o un gruppo di utenti. Lo schema esterno può avere un'organizzazione diversa rispetto a quella dello schema logico. In effetti, è possibile avere diversi schemi esterni per uno stesso schema logico. - **Schema logico**: rappresenta una descrizione **complessiva** della base di dati secondo il **modello logico** scelto (ad esempio, relazionale o a oggetti). - **Schema interno**: descrive la rappresentazione **fisica** dei dati, come vengono **memorizzati** su disco, per esempio tramite file sequenziali o hash. In pratica, nei sistemi moderni, il **livello esterno** può essere gestito tramite l\'uso di **viste** (**views**), ovvero relazioni derivate da altre tabelle. Queste permettono di adattare l'**accesso ai dati** in base alle esigenze degli utenti. Ad esempio, uno studente di **Ingegneria Elettronica** potrebbe vedere solo i corsi del suo manifesto, tramite una **vista** sulla base di dati più ampia, come mostrato nella relazione **ELETTRONICA**. Inoltre, i meccanismi di **autorizzazione** consentono di regolare l'**accesso degli utenti** alle informazioni della base di dati, garantendo così sia la sicurezza che la **privatezza** dei dati sensibili. **Modelli logici nei sistemi di basi dati** Il **modello relazionale** si fonda su due concetti principali: **relazione** e **tabella**. Questi concetti, pur avendo origini diverse, sono strettamente connessi. La **relazione** proviene dalla **teoria degli insiemi** nella matematica, mentre la **tabella** è un concetto intuitivo e di uso comune. Questa combinazione ha contribuito al successo del modello relazionale, in quanto offre sia una base formale rigorosa sia una rappresentazione comprensibile e utilizzabile facilmente dagli utenti finali. **Indipendenza dei dati** Il modello relazionale è stato progettato per garantire l\'**indipendenza dei dati**, separando il **livello fisico** da quello **logico**. Gli utenti e i programmatori operano esclusivamente al livello logico, senza la necessità di conoscere come i dati vengono effettivamente memorizzati fisicamente. Questa separazione rende il modello versatile e adatto per molte applicazioni, poiché eventuali modifiche alla struttura fisica dei dati non richiedono cambiamenti nelle applicazioni che li utilizzano. In confronto, i modelli di basi di dati **precedenti** (come quello **reticolare** e quello **gerarchico**) incorporavano riferimenti espliciti alle strutture fisiche attraverso puntatori e l'ordine fisico dei dati. Questo vincolo rendeva più difficile la gestione e l\'evoluzione delle basi di dati. **Tre accezioni del termine \"relazione\"** Il termine **relazione**, nell'ambito delle basi di dati, ha tre diverse interpretazioni: - **Relazione matematica**: deriva dalla teoria degli insiemi e si riferisce a una collezione di n-tuples o coppie ordinate di elementi. - **Relazione nel modello relazionale**: utilizza la nozione matematica come base, ma presenta alcune differenze, come vedremo più avanti. Una tabella rappresenta una relazione in questo contesto. - **Relazione nel modello Entità-Relazione (Entity-Relationship)**: questo concetto, tradotto dall\'inglese \"relationship\", è usato per rappresentare i **legami tra entità** del mondo reale nel modello concettuale. **Relazioni e Tabelle** Il **prodotto cartesiano** è un concetto fondamentale nella matematica, in particolare nella teoria degli insiemi, ed è alla base del **modello relazionale** nelle basi di dati. Dati due insiemi [*D*~1~]{.math.inline} e [*D*~2~]{.math.inline}, il loro prodotto cartesiano, indicato come [*D*~1~ × *D*~2~]{.math.inline}, è l\'insieme di tutte le **coppie ordinate** [(*u*~1~, *u*~2~)]{.math.inline}, dove [*u*~1~ ]{.math.inline}appartiene a [*D*~1~]{.math.inline} e [*u*~2~]{.math.inline} appartiene a [*D*~2~]{.math.inline}. **Esempio di prodotto cartesiano:** Se consideriamo gli insiemi: \ [*A* = {1, 2, 4}]{.math.display}\ \ [*B* = {*a*, *b*}]{.math.display}\ Il prodotto cartesiano [*A* × *B*]{.math.inline} è l\'insieme di tutte le possibili coppie [(*a*, *b*)]{.math.inline} in cui il primo elemento proviene da A e il secondo da B, cioè: \ [*A* × *B* = {(1, *a*), (1, *b*), (2, *a*), (2, *b*), (4, *a*), (4, *b*)}]{.math.display}\ Questo esempio genera sei coppie ordinate, poiché A contiene 3 elementi e B ne contiene 2, quindi [3 × 2 = 6]{.math.inline}. **Relazione matematica:** Una **relazione matematica** sugli insiemi [*D*~1~]{.math.inline} e [*D*~2~]{.math.inline} è semplicemente un **sottoinsieme del prodotto cartesiano** [*D*~1~ × *D*~2~]{.math.inline}. Ad esempio, una relazione su A e B potrebbe essere: \ [{(1, *a*), (1, *b*), (4, *b*)}]{.math.display}\ Questa relazione contiene solo alcune coppie del prodotto cartesiano, quindi è un sottoinsieme. **Rappresentazione tabellare:** Le relazioni matematiche possono essere rappresentate in forma di **tabelle**, dove: - Le colonne della tabella corrispondono ai domini [*D*~1~]{.math.inline}, [*D*~2~]{.math.inline}, ecc. - Le righe corrispondono alle n-uple o coppie ordinate della relazione. Nell\'esempio precedente, il prodotto cartesiano [*A* × *B*]{.math.inline} e la relazione specifica possono essere rappresentati come tabelle con righe e colonne. **Generalizzazione a n insiemi:** Il concetto di prodotto cartesiano può essere generalizzato a più di due insiemi. Dati n insiemi [*D*~1~, *D*~2~, ..., *D*~*n*~]{.math.inline}, il loro prodotto cartesiano è indicato come [*D*~1~ × *D*~2~ × ... × *D*~*n*~]{.math.inline} ed è costituito dalle **n-uple** [(*v*~1~, *v*~2~, ..., *v*~*n*~),]{.math.inline} dove ciascun [*v*~*i*~]{.math.inline} appartiene a [*D*~*i*~]{.math.inline}. Una **relazione matematica** su questi insiemi è un sottoinsieme del prodotto cartesiano. Il numero n delle componenti di una n-upla (e quindi del prodotto cartesiano) è chiamato **grado** della relazione. Il numero di n-uple in una relazione è la sua **cardinalità**. **Applicazione pratica nelle basi di dati:** Le relazioni matematiche trovano una rappresentazione pratica nel modello relazionale delle basi di dati. Ad esempio, una tabella che registra i risultati di partite di calcio può essere rappresentata come una relazione. Se i domini sono: Squadra1:Stringa Squadra2:Stringa GolSquadra1:Intero GolSquadra2:Intero La relazione risulterebbe essere un sottoinsieme del prodotto cartesiano di questi domini: \ [Stringa × Stringa × Intero × Intero]{.math.display}\ Ogni riga della tabella rappresenta una partita e i rispettivi risultati. In sintesi, il concetto di **prodotto cartesiano** e di **relazione** formano la base del **modello relazionale** utilizzato nelle basi di dati, dove ogni tabella rappresenta una relazione matematica che può essere rappresentata e manipolata in modo chiaro e intuitivo. **Relazioni con attributi** Le osservazioni sulle relazioni e le loro rappresentazioni tabellari nel contesto delle basi di dati evidenziano diversi aspetti importanti. Ogni **n-upla** in una relazione stabilisce un legame tra i suoi elementi. Ad esempio, una n-upla [(*u*~1~, *u*~2~, ..., *u*~*n*~)]{.math.inline} collega i valori dei domini [*D*~1~, *D*~2~, ..., *D*~*n*~]{.math.inline} in un certo ordine. Nella relazione che rappresenta i risultati di partite di calcio, una riga come [\$(\"\\text{Juventus}\",\"\\text{Lazio}\",3,1)\$]{.math.inline} stabilisce un collegamento tra i nomi delle squadre e i rispettivi punteggi. È importante notare che una relazione è un insieme di n-uple, e come tale presenta alcune proprietà. Innanzitutto, non esiste alcun ordinamento tra le n-uple; l\'ordine degli elementi in un insieme non è rilevante. Anche se in una rappresentazione tabellare le righe sono visualizzate in un certo ordine, questo è \"accidentale\". Cambiando l\'ordine delle righe, la relazione rappresentata rimane invariata. Inoltre, le n-uple di una relazione devono essere distinte; non possono esserci n-uple duplicate, poiché gli elementi di un insieme devono essere unici. Pertanto, ogni riga di una tabella deve essere unica. Allo stesso tempo, **l\'ordinamento interno** delle n-uple è cruciale. Sebbene l\'ordine delle righe in una relazione sia irrilevante, all\'interno di ciascuna n-upla l\'ordine dei valori è importante. Ogni valore è associato a un dominio specifico e la posizione del valore è fondamentale per interpretarlo correttamente. Se, ad esempio, si scambiasse la posizione dei domini che rappresentano i gol segnati dalle squadre, il significato della relazione cambierebbe drasticamente. Il fatto che i valori siano interpretati in base alla loro posizione rende il concetto di relazione matematica meno flessibile quando si tratta di gestire dati complessi. In molti contesti informatici, si preferisce l\'uso di notazioni non posizionali, come nei record, dove i campi sono identificati da nomi (attributi), piuttosto che da posizioni. Questa preferenza deriva dal fatto che i nomi sono più esplicativi e permettono una gestione più semplice e comprensibile dei dati. Quando usiamo le relazioni per organizzare i dati nelle basi di dati, possiamo pensare a ciascuna relazione come a un insieme di record omogenei, definiti sugli stessi campi (o attributi). Ciò rende la struttura della relazione più vicina a quella di un record piuttosto che a una semplice n-upla posizionale. Una relazione che registra i risultati delle partite potrebbe avere attributi come **SquadraDiCasa**, **SquadraOspitata**, **RetiCasa**, e **RetiOspitata**. Associando a ciascun dominio un nome simbolico (attributo), si può migliorare la chiarezza della relazione. Invece di riferirsi ai domini in base alla loro posizione, possiamo riferirci agli attributi. Per esempio, nella tabella delle partite, gli attributi possono essere usati come intestazioni delle colonne, rendendo la relazione più leggibile e interpretabile. Una volta introdotti gli attributi, l\'ordine delle colonne (e degli attributi stessi) diventa irrilevante. Non è più necessario parlare di primo dominio, secondo dominio e così via; è sufficiente fare riferimento agli attributi. Questo consente una maggiore flessibilità nella rappresentazione dei dati. Inoltre, è possibile utilizzare una notazione utile per riferirsi agli attributi, come [*t*\[*A*\]]{.math.inline}, dove t è una tupla e A è un attributo. Questa notazione ci permette di accedere al valore della tupla per un dato attributo. Infine, possiamo formalizzare il concetto di tupla: una tupla su un insieme di attributi X è una funzione t che associa a ciascun attributo [*A* ∈ *X*]{.math.inline} un valore appartenente al dominio associato a A. Con questa nuova definizione, una relazione non è più vista come un insieme di n-uple posizionali, ma come un insieme di tuple che associano i valori ai nomi degli attributi. Questa definizione elimina la necessità di interpretare i dati in base alla loro posizione, rendendo la gestione dei dati più flessibile e intuitiva. **Relazione e basi di dati** Come già osservato, una relazione può essere utilizzata per organizzare dati rilevanti nell\'ambito di un\'applicazione di interesse. Tuttavia, di solito non è sufficiente una singola relazione; una base di dati è generalmente costituita da più relazioni, le cui tuple contengono valori comuni, necessari per stabilire corrispondenze. Per esemplificare, consideriamo una base di dati in cui: - **La prima relazione** contiene informazioni relative a un insieme di studenti, con numero di matricola, cognome, nome e data di nascita. - **La seconda relazione** raccoglie informazioni relative agli esami, specificando il numero di matricola dello studente, il codice del corso e il voto. Questa relazione fa riferimento ai dati contenuti nelle altre due: agli studenti, attraverso i numeri di matricola, e ai corsi, tramite i relativi codici. - **La terza relazione** include informazioni su alcuni corsi, comprendenti codice, titolo e docente. La base di dati presenta una delle caratteristiche fondamentali del modello relazionale, spesso descritta come \"basata su valori\": i riferimenti tra dati in relazioni diverse sono rappresentati mediante valori dei domini che compaiono nelle tuple. Gli altri modelli logici, come quello reticolare e gerarchico (sviluppati prima del modello relazionale ma ancora in uso), stabiliscono le corrispondenze in modo esplicito attraverso puntatori e sono perciò detti modelli \"basati su record e puntatori\". Sebbene non approfondiremo questi modelli, è utile evidenziare la loro caratteristica principale: i dati sono collegati attraverso puntatori. Rispetto a un modello basato su record e puntatori, il modello relazionale presenta diversi vantaggi: Richiede di rappresentare solo ciò che è rilevante dal punto di vista dell\'applicazione (e quindi dell\'utente); i puntatori sono un elemento aggiuntivo, legato a aspetti realizzativi. Nei modelli con puntatori, il programmatore si riferisce a dati che non sono significativi per l\'applicazione. La rappresentazione logica dei dati, costituita esclusivamente dai valori, non fa alcun riferimento alla rappresentazione fisica, che può cambiare nel tempo. Il modello relazionale consente quindi di ottenere l\'indipendenza fisica dei dati; poiché tutte le informazioni sono contenute nei valori, è relativamente semplice trasferire i dati da un contesto a un altro (ad esempio, trasferire una base di dati da un computer a un altro). In presenza di puntatori, l\'operazione risulta più complessa, poiché i puntatori hanno un significato locale al singolo sistema, il che non sempre è immediato da esportare. È interessante notare che anche in una base di dati relazionale, a livello fisico, i dati possono essere rappresentati attraverso l\'uso di puntatori. Tuttavia, nei modelli relazionali, i puntatori non sono visibili a livello logico. Inoltre, nei sistemi di basi di dati a oggetti, che rappresentano una delle direzioni evolutive delle basi di dati, vengono introdotti identificatori di oggetto, i quali, sebbene a un livello di astrazione più alto, presentano alcune caratteristiche dei puntatori. Possiamo ora riassumere le definizioni relative al modello relazionale, distinguendo il livello degli schemi da quello delle istanze: **Schema di relazione**: costituito da un simbolo R (nome della relazione) e da un insieme di (nomi di) attributi [*X* = {*A*~1~, *A*~2~, ..., *A*~*n*~}]{.math.inline}, di solito indicato come [*R*(*X*)]{.math.inline}. A ciascun attributo è associato un dominio, come visto in precedenza. **Schema di base di dati**: un insieme di schemi di relazione con nomi diversi: \ [*R* = {*R*~1~(*X*~1~), *R*~2~(*X*~2~), ..., *R*~*n*~(*X*~*n*~)}]{.math.display}\ I nomi di relazione servono principalmente a distinguere le varie relazioni all\'interno della base di dati. **Istanza di relazione**: un insieme r di tuple su X. \ [*R* = {*R*~1~(*X*~1~), *R*~2~(*X*~2~), ..., *R*~*n*~(*X*~*n*~)}]{.math.display}\ dove ogni [*r*~*i*~]{.math.inline} è una relazione sullo schema [*R*~*i*~(*X*~*i*~)]{.math.inline}. Per chiarire, lo schema della base di dati può essere definito come segue (con opportune definizioni per i domini): \ [*R* = {STUDENTI(Matricola, Cognome, Nome), ESAMI(Voto, Corso), CORSI(Codice, Titolo, Docente)}]{.math.display}\ Per approfondire ulteriormente i concetti fondamentali del modello relazionale, esaminiamo un paio di esempi. In primo luogo, notiamo che secondo la definizione sono ammissibili relazioni su un solo attributo. Ciò può avere senso, in particolare, in basi di dati composte da più relazioni, in cui la relazione su singolo attributo contiene valori che appaiono come valori di un attributo di un\'altra relazione. Ad esempio, in una base di dati che include la relazione STUDENTI, si può utilizzare un\'altra relazione sul solo attributo Matricola per indicare gli studenti lavoratori (attraverso i rispettivi numeri di matricola, che devono apparire nella relazione STUDENTI). **Informazione incompleta e valori nulli** La struttura del modello relazionale, come discusso, è molto semplice e potente, ma presenta anche una certa rigidità. Le informazioni devono essere rappresentate tramite tuple di dati omogenee, e ogni relazione può contenere solo tuple conformi al suo schema. Questo può risultare problematico quando i dati disponibili non corrispondono esattamente al formato previsto. Ad esempio, in una relazione con lo schema: **PERSONE(Cognome, Nome, Indirizzo, Telefono)** il valore dell\'attributo Telefono potrebbe non essere disponibile per tutte le tuple. Utilizzare un valore del dominio per rappresentare l\'assenza di informazioni, come ad esempio lo zero per i numeri di telefono, può generare confusione e non è una pratica corretta. Ciò richiede che esista un valore mai utilizzato per indicare l\'assenza, e non sempre è possibile trovare un valore del dominio adeguato. Ad esempio, in un attributo per le date di nascita, non ci sono valori non utilizzati in un tipo Data correttamente definito. Inoltre, l\'uso di valori del dominio può mascherare la distinzione tra valori \"veri\" e valori fittizi, rendendo necessario che i programmi accedano alla base di dati con attenzione, distinguendo correttamente i valori. Per rappresentare la non disponibilità di valori in modo più chiaro, si introduce il concetto di valore nullo. Un valore nullo denota l\'assenza di informazione ed è un valore speciale distinto dai valori del dominio. Nelle rappresentazioni tabellari, il valore nullo viene indicato con il simbolo NULL. Consideriamo i seguenti esempi di valori nulli: **Città A**: ha un valore nullo per l\'indirizzo della prefettura, indicando che il valore è sconosciuto. **Città B**: non ha una prefettura, quindi l\'attributo relativo è inesistente; il valore nullo rappresenta l\'inesistenza del valore. **Città C**: è una nuova provincia e non si conosce né se esista né quale sia l\'indirizzo della prefettura; il valore nullo indica l\'incertezza riguardo all\'esistenza dell\'informazione. Nei sistemi di basi di dati relazionali, i valori nulli sono gestiti in modo semplice, senza ipotesi particolari, collocandoli nella categoria di valori \"senza informazione\". Riflettendo ulteriormente sui valori nulli, consideriamo una base di dati con lo stesso schema delle PERSONE. Un valore nullo per la data di nascita potrebbe essere accettabile, mentre un valore nullo per il numero di matricola o per il codice di un corso comporterebbe problematiche maggiori, poiché questi valori sono essenziali per stabilire correlazioni tra tuple di relazioni diverse. La presenza di valori nulli in una relazione, come nella relazione degli esami, può rendere inutilizzabili le informazioni. Inoltre, un numero eccessivo di valori nulli in una relazione può sollevare dubbi sull\'identità delle tuple stesse. È quindi cruciale controllare la presenza di valori nulli nelle relazioni, specificando che sono ammessi solo su alcuni attributi e non su altri. Alla fine, vedremo che è possibile definire criteri per individuare quali attributi non dovrebbero contenere valori nulli. **Vincoli di Integrità** In una base di dati, è fondamentale evitare situazioni problematiche come quelle descritte in precedenza. A tal fine, è stato introdotto il concetto di **vincolo di integrità**, che rappresenta una proprietà che deve essere soddisfatta dalle istanze per **garantire informazioni corrette per l\'applicazione**. Ogni vincolo può essere considerato come un predicato che associa a ogni istanza un valore di vero o falso. Se il predicato assume il valore vero, si afferma che l\'istanza soddisfa il vincolo. In generale, a uno schema di base di dati è associato un insieme di vincoli e si considerano corrette (o lecite, o ammissibili) le istanze che soddisfano tutti i vincoli. Per ciascuno dei casi precedentemente discussi, potrebbe essere introdotto un vincolo per vietare la situazione indesiderata. I vincoli possono essere classificati in base agli elementi della base di dati che ne sono coinvolti. Distinguiamo due categorie, di cui la prima presenta alcuni casi particolari: **Vincoli intrarelazionali**: questi vincoli sono definiti rispetto a singole relazioni della base di dati. I primi tre casi precedentemente discussi corrispondono a vincoli intrarelazionali. Talvolta, il coinvolgimento riguarda le tuple (o addirittura i valori) separatamente l\'una dall\'altra: - **Vincolo di tupla**: è un vincolo che può essere valutato su ciascuna tupla indipendentemente dalle altre. I vincoli relativi ai primi due casi rientrano in questa categoria. - **Vincolo su valori o vincolo di dominio**: è un caso ancora più specifico, in cui si impone una restrizione sul dominio di un attributo. Ad esempio, nel caso in cui sono ammessi solo valori dell\'attributo Voto compresi fra 18 e 30. **Vincoli interrelazionali**: questi vincoli coinvolgono più relazioni. Ad esempio, nella situazione indesiderata del quarto caso, è possibile vietare la situazione richiedendo che un numero di matricola compaia nella relazione ESAMI solo se è presente nella relazione STUDENTI. **Vincoli di tupla** Come abbiamo discusso, i vincoli di tupla esprimono condizioni sui valori di ciascuna tupla, indipendentemente dalle altre. Una sintassi utile per esprimere vincoli di questo tipo consente di definire espressioni booleane, che utilizzano connettivi logici come AND, OR e NOT, con atomi che confrontano (con operatori di uguaglianza, disuguaglianza e ordinamento) valori di attributo o espressioni aritmetiche su valori di attributo. I vincoli violati identificati possono essere descritti come segue: \ [(Voto \> 18) AND (Voto \ 1; **Operatori Insiemistici in SQL** In SQL, gli operatori insiemistici consentono di combinare i risultati di due o più query trattandoli come se fossero insiemi matematici. Le principali operazioni insiemistiche sono **UNION**, **INTERSECT** ed **EXCEPT**. Ognuno di questi operatori ha caratteristiche uniche che li rendono utili in diverse situazioni. **UNION** L\'operatore **UNION** è utilizzato per unire i risultati di due o più query, producendo un insieme di righe distinte. Questo significa che, se ci sono righe duplicate tra le query, queste verranno eliminate nel risultato finale. Un aspetto importante da considerare quando si utilizza **UNION** è che le colonne delle query devono avere lo stesso numero e tipo di dati compatibili. Ad esempio, se una query seleziona nomi e cognomi, anche l\'altra deve avere colonne corrispondenti. Immagina di avere due tabelle, **STUDENTI\_BASIDATI** e **STUDENTI\_ANALISI1**, entrambe contenenti dati sugli studenti. Utilizzando **UNION**, puoi ottenere un elenco unico di nomi e cognomi degli studenti presenti in entrambe le tabelle, senza duplicati. Ad esempio: SELECT Nome, Cognome FROM STUDENTI\_BASIDATI UNION SELECT Nome, Cognome FROM STUDENTI\_ANALISI1; Questo comando restituirebbe un insieme di studenti, assicurandosi che ogni nome e cognome appaiano una sola volta. **INTERSECT** L\'operatore **INTERSECT** è utile quando desideri identificare le righe comuni tra due query. Questa operazione restituisce solo le righe che si trovano in entrambe le tabelle, consentendo di vedere le sovrapposizioni tra i due set di dati. Proseguendo con l\'esempio delle tabelle degli studenti, se desideri trovare solo gli studenti presenti in entrambe le tabelle, puoi scrivere: SELECT Nome, Cognome FROM STUDENTI\_BASIDATI INTERSECT SELECT Nome, Cognome FROM STUDENTI\_ANALISI1; In questo caso, il risultato includerebbe solo gli studenti che sono elencati in entrambe le tabelle, il che può essere particolarmente utile per analisi comparative o per verificare l\'appartenenza a gruppi specifici. **EXCEPT** L\'operatore **EXCEPT** serve a trovare le righe presenti nella prima query ma non nella seconda. Questo è utile per isolare le differenze tra i due set di risultati. Tornando all\'esempio delle tabelle, se vuoi ottenere un elenco di studenti che si trovano in **STUDENTI\_BASIDATI** ma non in **STUDENTI\_ANALISI1**, puoi utilizzare: SELECT Nome, Cognome FROM STUDENTI\_BASIDATI EXCEPT SELECT Nome, Cognome FROM STUDENTI\_ANALISI1; In questo modo, otterrai solo gli studenti che non sono inclusi nella seconda tabella, fornendo un insight utile per analisi e reportistica. Quando si utilizzano gli operatori insiemistici, è importante prestare attenzione alle performance, soprattutto con dataset di grandi dimensioni. Le operazioni insiemistiche possono richiedere tempo, poiché il database deve confrontare i dati per trovare duplicati o sovrapposizioni. È quindi consigliabile testare le singole query per assicurarsi che funzionino come previsto prima di combinarle. Inoltre, è possibile ordinare i risultati finali utilizzando la clausola **ORDER BY**, ma questa deve essere applicata solo all\'ultima query della catena di operazioni. Infine, se si desidera mantenere anche i duplicati, è possibile utilizzare **UNION ALL**, che non rimuove le righe duplicate e può offrire migliori performance. **Query Nidificate** Le query nidificate, note anche come sottointerrogazioni o subquery, sono una caratteristica potente di SQL che consente di utilizzare il risultato di una query all\'interno di un\'altra. Questa funzionalità permette di eseguire interrogazioni più complesse e dettagliate, facilitando l\'analisi e la manipolazione dei dati. **Struttura delle Query Nidificate** In una query nidificata, la sottointerrogazione viene generalmente inserita nella clausola **WHERE** della query principale. La condizione di confronto può essere effettuata utilizzando vari operatori, inclusi quelli di confronto standard e alcuni operatori speciali che estendono le capacità di confronto. **Operatori di Confronto** **ALL**: Quando si utilizza **ALL**, la condizione specificata deve essere verificata per tutti gli elementi restituiti dalla sottointerrogazione. Ad esempio, se vuoi trovare tutti i dipendenti il cui stipendio è maggiore di quello di tutti i manager, potresti scrivere: SELECT Nome FROM DIPENDENTI WHERE Stipendio \> ALL (SELECT Stipendio FROM MANAGER); In questo caso, il nome del dipendente verrà restituito solo se il suo stipendio è superiore a quello di tutti i manager. **ANY**: Utilizzando **ANY**, la condizione deve essere verificata per almeno uno degli elementi restituiti dalla sottointerrogazione. Ad esempio: SELECT Nome FROM DIPENDENTI WHERE Stipendio \< ANY (SELECT Stipendio FROM MANAGER); Qui, il nome del dipendente sarà restituito se il suo stipendio è inferiore a quello di almeno un manager. **IN e NOT IN**: Questi operatori funzionano in modo simile a **=ANY** e **≠ALL**. Usando **IN**, puoi verificare se un valore è presente nel risultato della sottointerrogazione, mentre **NOT IN** verifica se un valore non è presente. SELECT Nome FROM DIPENDENTI WHERE Stipendio IN (SELECT Stipendio FROM MANAGER); Questo esempio restituirà i nomi dei dipendenti il cui stipendio è esattamente uguale a quello di almeno un manager. **EXISTS e NOT EXISTS**: Questi operatori sono utilizzati per verificare se una sottointerrogazione restituisce un insieme di risultati non vuoto. SELECT Nome FROM DIPENDENTI D WHERE EXISTS (SELECT 1 FROM MANAGER M WHERE D.Stipendio = M.Stipendio); In questo caso, il nome del dipendente verrà restituito se esiste almeno un manager con lo stesso stipendio. **Struttura delle Query Nidificate** Una query nidificata può contenere altre sottointerrogazioni, creando una gerarchia di interrogazioni. Questo consente di costruire interrogazioni molto complesse. Ad esempio, potresti voler trovare tutti i dipendenti il cui stipendio è superiore alla media degli stipendi dei manager: SELECT Nome FROM DIPENDENTI WHERE Stipendio \> (SELECT AVG(Stipendio) FROM MANAGER); In questo caso, la sottointerrogazione calcola prima la media degli stipendi dei manager e poi confronta il risultato con il salario di ciascun dipendente. **Considerazioni sull\'Uso delle Query Nidificate** Le query nidificate sono estremamente utili per l\'analisi dei dati, ma possono anche influenzare le performance, soprattutto quando si lavora con dataset molto grandi. È consigliabile testare le sottointerrogazioni separatamente per assicurarsi che funzionino come previsto prima di integrarle in query più complesse. Inoltre, l\'uso eccessivo di sottointerrogazioni può rendere le query più difficili da leggere e mantenere. Pertanto, è importante bilanciare la complessità delle query con la loro chiarezza e comprensibilità. **Esempio 1: Trovare i nomi e i cognomi delle persone che partecipano al progetto con codice \'EUI05\'** Questa query cerca i dipendenti che partecipano al progetto con un codice specifico. Si utilizza una sottointerrogazione per cercare le matricole dei dipendenti che partecipano al progetto \'EUI05\' e poi, con la query esterna, si recuperano i loro nomi e cognomi. SELECT A.Nome, A.Cognome FROM ANAGRAFICA A WHERE A.Matricola IN ( SELECT T.Mat FROM TEAM T WHERE T.Prog = \'EUI05\' ); **Esempio 2: Trovare i nomi e i cognomi delle persone che partecipano al progetto "Sistemi Informatici e Calcolo Parallelo"** In questo caso, si utilizza una doppia sottointerrogazione: la prima per ottenere il codice del progetto tramite il nome, e la seconda per trovare le matricole dei partecipanti al progetto. SELECT A.Nome, A.Cognome FROM ANAGRAFICA A WHERE A.Matricola IN ( SELECT T.Mat FROM TEAM T WHERE T.Prog IN ( SELECT P.Codice FROM PROGETTI P WHERE P.Nome = \'Sistemi Informatici e Calcolo Parallelo\' ) ); **Esempio 3: Trovare i nomi e i cognomi delle persone che partecipano al progetto con codice \'EUI05\' (usando EXISTS)** In questo esempio, si utilizza l\'operatore EXISTS per verificare l\'esistenza di almeno una tupla nella sottointerrogazione. SELECT A.Nome, A.Cognome FROM ANAGRAFICA A WHERE EXISTS ( SELECT \* FROM TEAM T WHERE T.Prog = \'EUI05\' AND T.Mat = A.Matricola ); **Esempio 4: Trovare il nome dei progetti i cui fondi sono maggiori del progetto \"IDEA\"** In questa query si cerca il nome di tutti i progetti che hanno fondi maggiori rispetto al progetto \"IDEA\", utilizzando l\'operatore ANY. SELECT P1.Codice FROM PROGETTI P1 WHERE P1.Fondi \> ANY ( SELECT P2.Fondi FROM PROGETTI P2 WHERE P2.Nome = \'IDEA\' ); **Esempio 5: Trovare il codice dei progetti con maggiori fondi (usando ALL)** Questa query utilizza l\'operatore ALL per trovare il progetto con i fondi più alti confrontandolo con tutti gli altri progetti. SELECT P1.Codice FROM PROGETTI P1 WHERE P1.Fondi \>= ALL ( SELECT P2.Fondi FROM PROGETTI P2 ); **Le Viste** Le **viste** in SQL rappresentano un concetto potente e flessibile per creare tabelle virtuali che consentono di presentare i dati in modi personalizzati senza duplicare le informazioni fisicamente. Ecco una spiegazione più articolata del concetto: **Che cos\'è una vista?** Una vista è una **tabella virtuale** derivata da una o più tabelle esistenti tramite una query SQL. Tuttavia, a differenza delle tabelle tradizionali, le viste non memorizzano i dati in sé; piuttosto, memorizzano una definizione della query che può essere utilizzata per accedere ai dati originali. Questo fa sì che le viste non occupino spazio aggiuntivo nel database, ma agiscano come uno strato di astrazione sopra le tabelle esistenti. **Caratteristiche principali delle viste** **Virtualità**: Una vista non memorizza fisicamente i dati; le righe e le colonne che mostra sono il risultato di una query eseguita al momento della richiesta. Questo rende le viste efficienti in termini di spazio. **Interfaccia di accesso**: Le viste sono un\'utile interfaccia per gli utenti o le applicazioni che necessitano di accedere frequentemente a determinati set di dati, ma che potrebbero non aver bisogno di vedere l\'intera tabella. Ad esempio, una vista può essere creata per mostrare solo determinati attributi di una tabella o solo le righe che soddisfano determinate condizioni. **Limitazioni sugli aggiornamenti**: Una conseguenza del fatto che le viste sono tabelle virtuali è che non sempre possono essere aggiornate direttamente. In altre parole, esistono delle restrizioni nel modificare i dati attraverso una vista, soprattutto se la query che la definisce è complessa (ad esempio, se coinvolge più tabelle o funzioni aggregate). **Esempio di sintassi per la creazione di una vista** La creazione di una vista in SQL avviene tramite la seguente sintassi: CREATE VIEW nome\_vista AS SELECT \... FROM \... WHERE \... **Esempio pratico** Supponiamo di avere una tabella ANAGRAFICA che contiene informazioni sui dipendenti, e di voler creare una vista per mostrare solo i dipendenti di un particolare dipartimento, in questo caso il dipartimento \"DIS\". La sintassi sarà la seguente: CREATE VIEW IMPIEGATI\_DIS AS SELECT \* FROM ANAGRAFICA WHERE NomeDip = \'DIS\'; In questo esempio: **IMPIEGATI\_DIS** è il nome della vista. La query seleziona tutte le righe dalla tabella ANAGRAFICA dove il campo NomeDip (che indica il dipartimento) è uguale a \'DIS\'. **Operazioni set-oriented** Le **operazioni set-oriented** in SQL permettono di applicare comandi di inserimento, aggiornamento o cancellazione a **insiemi di tuple** piuttosto che a singole righe, rendendo l\'interazione con la base di dati più efficiente. Queste operazioni si basano su comandi che operano su interi set di dati, grazie alla potenza delle query SELECT che identificano le tuple su cui intervenire. Vediamo nel dettaglio come funzionano. **Inserimento set-oriented (INSERT)** L\'operazione INSERT può essere estesa per aggiungere **insiemi di dati** in una tabella, utilizzando il risultato di una query SELECT. Invece di inserire i dati riga per riga, è possibile inserire in blocco le righe che soddisfano determinate condizioni. Esempio: INSERT INTO IMPIEGATI\_DIS ( SELECT \* FROM ANAGRAFICA WHERE NomeDip = \'DIS\'); Questo comando copia tutte le righe della tabella ANAGRAFICA in cui il campo NomeDip è uguale a \'DIS\', inserendole nella tabella IMPIEGATI\_DIS. Si sfrutta la potenza del SELECT per specificare il set di dati che devono essere inseriti. **Cancellazione set-oriented (DELETE)** Similmente, l\'operazione di cancellazione può essere applicata a **più righe contemporaneamente**, utilizzando condizioni che determinano quali tuple devono essere eliminate. Esempio: DELETE FROM ANAGRAFICA WHERE NomeDip = \'DIS\'; Questo comando cancella tutte le righe della tabella ANAGRAFICA in cui il campo NomeDip ha il valore \'DIS\'. In questo modo, si eliminano tutte le informazioni relative ai dipendenti del dipartimento \'DIS\' in un\'unica operazione. **Aggiornamento set-oriented (UPDATE)** L\'operazione UPDATE può essere utilizzata per modificare **più tuple** contemporaneamente. Anche in questo caso, si utilizza una condizione che determina quali righe devono essere aggiornate. Esempio: UPDATE ANAGRAFICA SET Cognome = \'\' WHERE NomeDip = \'DIS\'; Questo comando aggiorna il campo Cognome a un valore vuoto per tutte le righe della tabella ANAGRAFICA dove il campo NomeDip è \'DIS\'. L\'operazione UPDATE viene eseguita su tutte le righe che soddisfano la condizione specificata. **Normalizzazione e Forme Normali** La **normalizzazione** è il processo di riorganizzazione delle relazioni in una base di dati per ridurre la **ridondanza** e migliorare l\'integrità dei dati. L'obiettivo principale della normalizzazione è quello di eliminare anomalie di aggiornamento, cancellazione e inserimento, ottenendo schemi di relazione che soddisfino determinati requisiti, chiamati **forme normali**. Una **forma normale** rappresenta un insieme di condizioni che un determinato schema deve rispettare per evitare problemi strutturali. Il raggiungimento di una forma normale migliora la qualità dello schema stesso, riducendo duplicazioni e migliorando la coerenza dei dati. **Problemi negli Schemi Non Normalizzati** Uno schema non normalizzato può presentare **ridondanza** di informazioni, cioè la ripetizione di dati senza aggiungere nuove informazioni utili. Questo genera **anomalie** quando si cerca di aggiornare, eliminare o inserire dati, perché ogni modifica può comportare l'intervento su più tuple, riducendo l\'efficienza e aumentando la possibilità di errore. **Esempio di Schema Non Normalizzato** Consideriamo la relazione **Progettazioni (Impiegato, Stipendio, Progetto, Bilancio, Funzione)**, dove: Ogni **Impiegato** ha un solo **Stipendio**, indipendentemente dai progetti a cui partecipa. Ogni **Progetto** ha un **Bilancio** unico. Se un impiegato partecipa a tre progetti, lo stipendio sarà ripetuto tre volte, causando ridondanza. Inoltre, se lo stipendio dell'impiegato cambia, dovrà essere aggiornato in tutte le tuple in cui compare, generando **anomalia di aggiornamento**. **Le Anomalie** Le anomalie sono problemi derivanti dall\'utilizzo di un\'unica relazione per rappresentare informazioni eterogenee. Quando concetti diversi vengono fusi in una singola relazione, si creano problemi di ridondanza e anomalie che rendono difficoltose le operazioni di manipolazione dei dati. Le principali **anomalie** sono: - **Anomalia di aggiornamento**: ogni volta che si modifica un valore ripetuto (ad esempio, lo stipendio), bisogna aggiornare tutte le tuple che lo contengono. - **Anomalia di cancellazione**: eliminando un progetto da cui dipendono altre informazioni (ad esempio, l\'impiegato), si rischia di perdere anche dati ancora validi (ad esempio, lo stipendio dell\'impiegato). - **Anomalia di inserimento**: non è possibile inserire un impiegato che non partecipa a nessun progetto, perché mancherebbe una parte delle informazioni necessarie per completare la tupla. **Esempio di Anomalia di Cancellazione** Supponiamo che un impiegato partecipi a un solo progetto. Se quel progetto viene eliminato, anche tutte le informazioni sull'impiegato verranno cancellate, nonostante potrebbe essere ancora un dipendente dell'azienda. **Dipendenze Funzionali (DF)** Per eliminare le anomalie, è necessario identificare le relazioni logiche tra gli attributi di una relazione, note come **dipendenze funzionali (DF)**. Una dipendenza funzionale è una relazione tra due insiemi di attributi di una relazione, in cui un attributo (o un insieme di attributi) determina univocamente un altro attributo. Se Y e Z sono due sottoinsiemi di attributi di una relazione, una dipendenza funzionale Y→Z indica che il valore di Z è determinato univocamente dal valore di Y. Formalmente: Y→Z si legge: \"Y determina Z\" o \"Z dipende funzionalmente da Y\". **Definizione Formale di Dipendenza Funzionale** Consideriamo: Una relazione r su uno schema di relazione R(X) e due sottoinsiemi Y e Z di X. Esiste una dipendenza funzionale da Y a Z, indicata con Y→Z, se, per ogni coppia di tuple [*t*~1~]{.math.inline} e [*t*~2~]{.math.inline} in r, che hanno lo stesso valore in Y, risulta che [*t*~1~]{.math.inline} e [*t*~2~]{.math.inline} hanno anche lo stesso valore in Z: [∀*t*~1~, *t*~2~ ∈ *r* : *t*~1~\[*Y*\] = *t*~2~\[*Y*\] ⇒ *t*~1~\[*Z*\] = *t*~2~\[*Z*\]]{.math.inline} Questa condizione stabilisce che se due tuple concordano nei valori di Y, devono concordare anche nei valori di Z. **Esempi di Dipendenze Funzionali** Ogni impiegato ha un solo stipendio: \ [Impiegato → Stipendio]{.math.display}\ Anche se l\'impiegato partecipa a più progetti, il suo stipendio rimane unico. Ogni progetto ha un bilancio: \ [Progetto → Bilancio]{.math.display}\ Il bilancio di un progetto dipende solo dal progetto e non dagli impiegati. Ogni impiegato ha una funzione specifica in ogni progetto: \ [Impiegato, Progetto → Funzione]{.math.display}\ Anche se un impiegato può svolgere più ruoli in progetti diversi, in un progetto specifico avrà una sola funzione. **Dipendenze Funzionali Banali e Non Banali** Le **dipendenze funzionali banali** sono quelle che si verificano per definizione e non forniscono nuove informazioni utili sulla struttura della relazione. Per esempio, Impiegato,Progetto→Progetto è una dipendenza banale, poiché \"Progetto\" è già parte dell'insieme a sinistra dell'implicazione. Una dipendenza funzionale Y→Z è **non banale** se nessuno degli attributi in Z è già presente in Y. Ad esempio: \ [Impiegato → Stipendio]{.math.display}\ è una dipendenza non banale perché lo stipendio dipende interamente dall\'impiegato e non è parte dell\'insieme Y. **Chiavi e Attributi** Una **chiave** in uno schema di relazione è un insieme di attributi che identifica univocamente una tupla. Gli attributi che partecipano a una chiave si chiamano **attributi primi**, mentre quelli che non partecipano a nessuna chiave sono detti **attributi non primi**. Se K è una chiave per una relazione R(X), allora ogni attributo non primo dipende funzionalmente da K. Questo significa che possiamo esprimere una dipendenza funzionale completa come: \ [*K* → *Z*]{.math.display}\ dove Z è un insieme di attributi non primi e [*X* ⊇ *K* ∪ *Z*]{.math.inline}. **Esempio di Chiave e Attributi** In una relazione **Impiegati (Impiegato, Stipendio)**, la chiave è Impiegato, e quindi ogni altro attributo della relazione, come Stipendio, dipende da esso: \ [Impiegato → Stipendio]{.math.display}\ **Dipendenza Funzionale Completa** Una dipendenza funzionale Y→Z è detta **completa** se nessun sottoinsieme proprio di Y determina Z. In altre parole, l\'intero insieme di attributi Y è necessario per determinare univocamente Z. **Esempio di Dipendenza Funzionale Completa** Nella relazione **Partecipazioni (Impiegato, Progetto, Funzione)**, l'attributo Funzione dipende sia da Impiegato che da Progetto. Quindi, la dipendenza Impiegato,Progetto→Funzione è completa. Se considerassimo solo uno di questi attributi, non saremmo in grado di determinare la funzione di un impiegato senza conoscere anche il progetto specifico. **Forme Normali** Le forme normali rappresentano un insieme di regole o criteri progettuali, mirati a ridurre la ridondanza e prevenire le anomalie nei database relazionali. Tali regole definiscono come organizzare i dati in modo efficiente e consistente all\'interno delle relazioni, o tabelle, di un database, e guidano il processo di normalizzazione, che è il processo attraverso cui si trasforma uno schema di dati in modo da soddisfare determinate condizioni per evitare problemi. **Prima forma normale (1NF)** La **Prima Forma Normale (1NF)** rappresenta la base del modello relazionale, *che richiede che ogni attributo di una relazione contenga un valore atomico*, ossia indivisibile. In altre parole, **non sono ammessi attributi composti** o multivalore all\'interno delle tabelle. Ciò significa che una tabella deve essere strutturata in modo tale che ogni cella contenga un solo dato, e ogni riga abbia un valore unico per un determinato attributo. L\'applicazione della 1NF ha l\'obiettivo di rendere le tabelle più facili da gestire ed elaborare. Se una tabella contiene colonne con insiemi di valori, può risultare difficile da gestire nelle query, e potrebbero emergere problemi come la ridondanza o le difficoltà nella manipolazione dei dati. **Seconda forma normale (2NF)** Una relazione è in **Seconda Forma Normale (2NF)** se soddisfa due condizioni: è in 1NF e ogni attributo non primo dipende completamente da tutta la chiave primaria. In altre parole, gli attributi non chiave non devono dipendere da una parte sola della chiave primaria (dipendenze parziali), ma devono essere totalmente determinati dalla chiave. Per comprendere meglio, supponiamo di avere una relazione che descrive gli impiegati e i progetti a cui lavorano, dove la chiave primaria è composta da \"Impiegato\" e \"Progetto\". Se esiste un attributo come \"Stipendio\", che dipende solo dall\'impiegato e non dal progetto, si creerebbe una dipendenza parziale. Questa condizione violerebbe la 2NF perché lo stipendio dipende solo da una parte della chiave e non dalla combinazione \"Impiegato-Progetto\". La decomposizione di tale relazione è una delle soluzioni per riportare la tabella in 2NF, separando le informazioni dipendenti esclusivamente dall\'impiegato in una nuova tabella. **Decomposizione e decomposizione senza perdita** Quando una relazione non soddisfa la 2NF o una forma normale più elevata, è possibile decomporre la relazione in più tabelle che soddisfano i requisiti della normalizzazione. Tuttavia, è fondamentale che la decomposizione sia eseguita senza perdita di informazioni, garantendo che il join naturale tra le nuove tabelle ricostruisca la relazione originale. La decomposizione senza perdita è ottenuta quando il join naturale delle proiezioni della relazione originale sulle nuove tabelle restituisce esattamente l\'istanza originale. Per garantire questo, è necessario che gli attributi comuni tra le tabelle risultanti contengano una superchiave di almeno una delle tabelle. In altre parole, gli attributi condivisi tra le tabelle decomposte devono permettere di ricostruire le tuple originarie senza creare dati spuri. **Conservazione delle dipendenze** Un altro aspetto cruciale nella decomposizione è la **conservazione delle dipendenze funzionali**, ossia la capacità di preservare i vincoli esistenti tra gli attributi nel processo di decomposizione. Se una decomposizione separa gli attributi di una dipendenza funzionale, può diventare difficile verificare il soddisfacimento di tale dipendenza senza dover ricostruire più tabelle. Ciò può compromettere l\'integrità dei dati e rendere più complessa la gestione del database. **Terza forma normale (3NF)** Una relazione è in **Terza Forma Normale (3NF)** se è in 2NF e non contiene dipendenze transitive tra attributi non primi e la chiave primaria. Una dipendenza transitiva si verifica quando un attributo non primo è determinato da un altro attributo non primo, che a sua volta dipende dalla chiave. Per esempio, se abbiamo una relazione con gli attributi \"Impiegato\", \"Progetto\" e \"Sede\", e se \"Sede\" dipende da \"Progetto\", e \"Progetto\" dipende da \"Impiegato\", allora \"Sede\" dipende transitivamente da \"Impiegato\". Questa condizione viola la 3NF, poiché l\'attributo \"Sede\" non dovrebbe dipendere da un attributo che non è parte della chiave primaria. La 3NF si concentra dunque sull\'eliminazione delle dipendenze transitive per evitare anomalie come ridondanze o difficoltà negli aggiornamenti. Per raggiungere questa forma normale, è spesso necessario eseguire ulteriori decomposizioni delle relazioni. **Forma normale di Boyce-Codd (BCNF)** La **Forma Normale di Boyce-Codd (BCNF)** è una versione più rigorosa della 3NF. Una relazione è in BCNF se per ogni dipendenza funzionale non banale, l\'insieme di attributi che determina gli altri attributi (determinante) deve essere una superchiave della relazione. In altre parole, una relazione non può avere dipendenze funzionali in cui l\'insieme di attributi determinante non sia una superchiave. Sebbene una relazione in 3NF soddisfi la maggior parte dei requisiti per evitare anomalie, esistono casi in cui alcune dipendenze funzionali violano comunque il principio della superchiave, che la BCNF risolve. Tuttavia, passare alla BCNF non garantisce sempre la conservazione delle dipendenze funzionali, quindi il processo deve essere bilanciato attentamente. **Comportamento attivo delle basi di dati e trigger** Tradizionalmente, i DBMS erano considerati passivi: eseguivano solo le istruzioni transazionali inviate dagli utenti, come inserimenti, cancellazioni o aggiornamenti. Tuttavia, il comportamento attivo, introdotto con i trigger, consente ai DBMS di reagire autonomamente a determinati eventi. Questa caratteristica si basa sul paradigma E-C-A (evento-condizione-azione), che permette alla base di dati di rispondere automaticamente a una situazione specifica quando si verifica un evento particolare. I trigger, dunque, non sono altro che regole definite che si attivano a fronte di eventi transazionali predefiniti. Ogni trigger segue il paradigma E-C-A, suddiviso in tre componenti fondamentali: - **Evento**: rappresenta il momento di attivazione del trigger, ossia una modifica dello stato dei dati, come un\'operazione di INSERT, DELETE o UPDATE. - **Condizione**: una condizione logica che specifica se il trigger debba effettivamente essere eseguito quando si verifica l\'evento. - **Azione**: rappresenta una sequenza di comandi SQL o una procedura memorizzata che viene eseguita se la condizione è soddisfatta. Grazie ai trigger, una parte delle logiche applicative diventa \"condivisa\" all\'interno della base di dati, garantendo l\'indipendenza della conoscenza: le regole e la logica reattiva vengono estratte dal codice dell'applicazione e centralizzate nella gestione della base di dati stessa. Ciò rende la manutenzione delle regole aziendali più agevole e, allo stesso tempo, aumenta la consistenza dei dati, poiché tutti i sistemi che accedono alla base di dati condividono lo stesso insieme di regole. Eventuali **condizioni** e **azioni**, cioè le istruzioni che il trigger deve eseguire se la condizione è verificata. **Modo di esecuzione dei trigger: BEFORE e AFTER** I trigger possono essere configurati per essere eseguiti prima (BEFORE) o dopo (AFTER) l\'operazione specificata: - **BEFORE**: esegue il trigger prima che l\'evento venga effettivamente applicato sulla base di dati. Questa modalità è utile quando è necessario controllare o validare le modifiche prima che vengano registrate, garantendo integrità e consistenza dei dati in fase di modifica. - **AFTER**: esegue il trigger subito dopo che l\'operazione è stata completata. Questo è il comportamento più comune e viene utilizzato in moltissime applicazioni, come la gestione dei log o l\'aggiornamento di record collegati in altre tabelle. **Granularità dei trigger: statement-level e row-level** La granularità di un trigger definisce se l\'attivazione debba avvenire a livello di singola istruzione (statement-level) o di singola riga (row-level): - **Statement-level**: il trigger viene eseguito una sola volta per ciascuna istruzione SQL, indipendentemente dal numero di righe coinvolte. Questo approccio è coerente con l\'approccio set-oriented dei comandi SQL, che si applicano a insiemi di dati. - **Row-level**: il trigger viene eseguito una volta per ogni riga modificata dall\'istruzione SQL. È utile per operazioni che richiedono la verifica o la modifica di singole righe, ma può risultare meno efficiente se l\'operazione coinvolge un elevato numero di tuple. **Clausola REFERENCING e variabili di transizione** Per i trigger row-level, si possono utilizzare le variabili di transizione OLD e NEW, che rappresentano i valori della riga prima e dopo la modifica. Per i trigger statement-level, sono invece disponibili OLD TABLE e NEW TABLE, ossia due tabelle di transizione che contengono le tuple modificate prima e dopo l\'operazione: - **OLD** e **NEW** sono utilizzabili solo nei trigger row-level e permettono di riferirsi ai valori di una singola tupla. - **OLD TABLE** e **NEW TABLE** sono disponibili per i trigger statement-level e consentono di accedere a tutte le tuple coinvolte nell'operazione. **ODBC: Interoperabilità e Accesso ai Dati Multi-Piattaforma** L'ODBC (Open Database Connectivity) è una tecnologia progettata per risolvere un problema molto concreto: permettere a un\'applicazione di interagire con database diversi senza dover scrivere codice specifico per ciascun sistema. Prima della sua introduzione, infatti, chiunque sviluppasse software capace di accedere ai dati si trovava a riscrivere parte del codice ogni volta che cambiava il database sottostante. Con ODBC, Microsoft ha creato uno standard comune, una sorta di "ponte" per l'accesso ai dati. La struttura di ODBC è ideata per mantenere una separazione tra applicazione e database, utilizzando una serie di driver specifici per ciascun sistema di gestione dei dati (come MySQL, Oracle, PostgreSQL, ecc.). Questi driver fanno da **traduttori**, traducendo le richieste dell'applicazione in comandi che il database può comprendere. Grazie a questa architettura, lo sviluppatore può scrivere codice una volta sola e, cambiando solo il driver, far funzionare la propria applicazione con database diversi, senza dover apportare modifiche significative. Un'altra caratteristica interessante di ODBC è la **possibilità di accedere ai dati da diversi linguaggi di programmazione**. Non essendo legato a un linguaggio specifico, ODBC supporta linguaggi come C, C++, Python, e molti altri, rendendolo una scelta ideale per le aziende che utilizzano un mix di tecnologie. Tuttavia, questa versatilità ha un **costo in termini di prestazioni**: poiché la connessione passa attraverso vari livelli di traduzione, le applicazioni ODBC possono risultare meno veloci rispetto a quelle che utilizzano protocolli nativi, soprattutto in contesti ad alte prestazioni. Inoltre, ogni database richiede un driver specifico, il che può complicare la gestione dei progetti. Infatti, l'ODBC funziona tramite un sistema di DSN (Data Source Name), che contiene tutte le informazioni di connessione necessarie. Il DSN deve essere configurato per ogni database, introducendo un ulteriore livello di complessità nella gestione delle connessioni, soprattutto quando si ha a che fare con diversi database in un unico sistema. **JDBC: Un'interfaccia Nativa e Ottimizzata per Java** JDBC (Java Database Connectivity) è un termine chiave in ambito informatico, specialmente per lo sviluppo di applicazioni in Java che richiedono accesso a database. Si tratta di una libreria Java fondamentale, che si articola in varie API progettate per facilitare la connessione e l'interazione con una molteplicità di database, indipendentemente dal tipo di sistema di gestione del database (DBMS) impiegato. JDBC consente a un'applicazione Java di inviare ![](media/image16.png)istruzioni SQL, di ricevere e interpretare i risultati, e di gestire la comunicazione con il database tramite un'interfaccia unificata. La tecnologia JDBC è composta da diverse implementazioni che variano a seconda delle necessità di accesso e delle prestazioni richieste. Una delle implementazioni più diffuse, soprattutto negli ambienti Windows, è la "bridge JDBC -- ODBC", una soluzione che rappresenta un \"ponte\" tra l'applicazione Java e il database tramite il driver ODBC (Open Database Connectivity). Questa bridge consente l'accesso a un ampio numero di database purché il driver ODBC sia disponibile e configurato correttamente sul server. In pratica, il driver JDBC -- ODBC permette al codice Java di comunicare con il driver ODBC, che a sua volta si interfaccia con il database desiderato. Sebbene sia diffusa, questa soluzione potrebbe risultare meno performante rispetto ad altre implementazioni di JDBC, data la doppia traduzione delle istruzioni. **Funzionamento del Driver Manager e delle API JDBC** L\'applicazione Java utilizza l\'API JDBC per dialogare con il **JDBC Driver Manager**, un elemento centrale che coordina la gestione delle connessioni e la distribuzione delle richieste SQL. Quando un\'applicazione invia una query, il Driver Manager sceglie il driver appropriato e invia la richiesta al DBMS tramite un'API specifica del driver. Questa API, nota come **JDBC Driver API**, si occupa di convertire i comandi e di comunicare con il DBMS secondo il protocollo necessario. Questo approccio rende l\'accesso ai dati più standardizzato e indipendente, semplificando notevolmente lo sviluppo. Nel contesto Windows, il driver più utilizzato per gestire la connessione a database attraverso i driver ODBC è proprio il bridge JDBC -- ODBC, che permette a un'applicazione Java di interfacciarsi con i driver ODBC disponibili, ampliando notevolmente la compatibilità con diversi DBMS. Tuttavia, è utile notare che, nonostante sia una soluzione versatile, il bridge JDBC -- ODBC è meno efficiente rispetto a un driver puro Java, in quanto introduce un ulteriore livello di traduzione e dipende da un componente esterno. **Componenti Principali dell\'Architettura JDBC** L\'architettura JDBC si struttura attorno a due componenti fondamentali: il **Driver Manager** e i **driver specifici JDBC**. Questi ultimi sono definiti in base al DBMS specifico con cui l\'applicazione deve interfacciarsi. Ad esempio, esistono driver JDBC progettati appositamente per MySQL, altri per Oracle e così via. Il ruolo del Driver Manager è essenziale poiché rappresenta il \"layer di astrazione\", che consente alle applicazioni Java di comunicare con i database mediante un set di API standardizzato (JDBC API). In pratica, il Driver Manager funge da regista, coordinando le connessioni e caricando i driver necessari in base al database utilizzato. In questo modo, un'applicazione può essere implementata con un approccio modulare, in cui la logica del codice rimane indipendente dal database specifico. **Tecnologia dei DBMS** Un sistema di gestione di basi di dati (DBMS) include una serie di moduli specializzati che operano insieme per garantire l'efficienza, l'affidabilità e la sicurezza delle operazioni sui dati. La struttura di un DBMS è complessa e orientata a ottimizzare ogni aspetto della gestione dei dati, dalla semplice memorizzazione fino alla gestione della concorrenza e del recupero in caso di guasti. Vediamo in dettaglio come funziona ciascun modulo e come questi contribuiscono al corretto funzionamento del sistema. 1. **Gestione delle Query** 2. **Gestore dei Metodi di Accesso e dei File** 3. **Gestore del Buffer di Memoria** 4. **Gestore dello Spazio su Disco** 5. **Gestore della Concorrenza** 6. **Gestore dell'Affidabilità** 7. **Gestore dell'Integrità** 8. **Gestore degli Accessi** **Memorizzazione dei Dati** Per gestire in modo persistente le informazioni di una base di dati, il DBMS memorizza i dati su **dispositivi di memoria di massa**, come dischi o, meno comunemente, nastri. La memoria di massa è fondamentale per mantenere i dati anche quando il sistema è spento, permettendo un accesso rapido alle informazioni necessarie. Quando una query richiede l'elaborazione di un'informazione, i dati vengono trasferiti dalla memoria di massa alla memoria centrale per poter essere utilizzati nel calcolo. Dopo l'elaborazione, i risultati possono essere riscritti su disco per garantire la persistenza delle modifiche. **File di Record** La struttura dati tipica per la memorizzazione delle informazioni in un database è il **file di record**. Ogni file è formato da una sequenza di record, dove **ogni record rappresenta una singola unità di informazione** composta da uno o più campi. Questi campi possono includere vari tipi di dati e ognuno di essi è identificato da un identificatore univoco che permette di individuare rapidamente l'indirizzo fisico della pagina su cui risiede. **Organizzazione dei File in un DBMS** Nel contesto di un sistema di gestione di basi di dati (DBMS), l\'organizzazione dei file è fondamentale per ottimizzare le operazioni di memorizzazione e recupero dei dati. I file possono essere organizzati in vari modi a seconda delle esigenze specifiche del sistema e delle operazioni che devono essere supportate. Le principali tipologie di file utilizzate in un DBMS sono i **file non ordinati (heap)**, i **file ordinati (sequenziali)** e i **file ad accesso calcolato (hash)**. Ciascuna di queste strutture presenta vantaggi e svantaggi, che influenzano il modo in cui i dati vengono memorizzati, recuperati e aggiornati. **File HEAP** ![](media/image18.png)I file heap sono una delle strutture più semplici e vengono utilizzati per memorizzare i record senza un ordine specifico. In un file heap, i record vengono inseriti in maniera casuale. Ogni nuovo record viene posizionato alla fine del file, e se lo spazio disponibile in una pagina non è sufficiente, viene creata una nuova pagina e aggiunta alla fine del file. Questa struttura consente operazioni di inserimento molto rapide, ma l\'accesso ai dati non è altrettanto efficiente. Poiché i record non sono ordinati, l\'unico metodo per recuperare i dati è la ricerca lineare, che richiede la scansione dell\'intero file. Per quanto riguarda la cancellazione, quando un record viene eliminato, esso viene semplicemente \"marcato\" come cancellato, ma lo spazio che occupava non viene effettivamente liberato. Questo porta, con il tempo, a un deterioramento delle prestazioni del file, soprattutto in caso di frequenti cancellazioni, poiché si generano aree vuote che necessitano di essere recuperate tramite una riorganizzazione del file. **File ORDINATI** I file ordinati, al contrario, memorizzano i record in ordine crescente o decrescente, solitamente su uno o più campi di ciascun record, come ad esempio una chiave primaria. L\'ordinamento consente operazioni di ricerca più efficienti, in particolare mediante l\'uso della ricerca binaria, che riduce significativamente il numero di operazioni necessarie per trovare un record. Tuttavia, le operazioni di inserimento e cancellazione diventano più complesse: ogni volta che si inserisce un nuovo record, è necessario trovare la posizione giusta in cui inserirlo e, se non c\'è spazio sufficiente in una pagina, si crea una nuova pagina o si spostano i record esistenti. La cancellazione richiede anch\'essa una riorganizzazione del file per rimuovere lo spazio libero lasciato dai record eliminati. Inoltre, per mantenere il file ordinato, è necessario eseguire periodiche riorganizzazioni, come il **merge-sort**, per evitare che il file si degradi nel tempo. Nonostante la complessità delle operazioni di aggiornamento, i file ordinati sono molto efficienti per operazioni di ricerca, poiché l\'ordinamento permette di ridurre drasticamente il numero di confronti necessari per recuperare un record. **File HASH** I file hash utilizzano una funzione matematica, chiamata **funzione di hash**, per determinare la posizione in cui un record deve essere memorizzato nel file. La funzione di hash prende uno o più campi del record (ad esempio una chiave) e restituisce un valore che rappresenta l\'indirizzo fisico del record nel file. Questo approccio consente un accesso molto rapido ai dati, poiché la posizione del record è determinata direttamente senza bisogno di eseguire ricerche lineari o binarie. Tuttavia, la funzione di hash non può garantire che ogni record abbia una posizione unica, poiché il numero di valori di hash possibili può essere maggiore rispetto al numero di posizioni disponibili nel file. Questo fenomeno, chiamato **collisione**, si verifica quando due o più record hanno lo stesso valore di hash e quindi finiscono nella stessa posizione del file. Per gestire le collisioni, vengono utilizzati **bucket**, che sono zone di memoria in cui vengono memorizzati più record con lo stesso valore di hash. Se un bucket è pieno, i record successivi vengono inseriti in una **zona di overflow**, che può essere gestita in modo libero o tramite una lista concatenata. Nonostante le collisioni, i file hash offrono un accesso molto veloce ai dati, ma è essenziale che la funzione di hash sia ben progettata per evitare una distribuzione inefficiente dei record. ![](media/image20.png)**Collisioni nei File di HASH** Quando si utilizza una **funzione di hash** per determinare la posizione di un record in un file, l\'idea è quella di calcolare un valore numerico (l\'hash) che corrisponde a un indirizzo specifico del record nel file. Tuttavia, non è possibile garantire che ogni record abbia un indirizzo univoco, poiché il numero di possibili valori che una funzione di hash può generare è molto più ampio rispetto al numero di posizioni disponibili nel file. Questo porta inevitabilmente alla possibilità di **collisioni**, ossia situazioni in cui due o più record hanno lo stesso valore di hash e quindi sono indirizzati alla stessa posizione nel file. **Cos\'è una Collisione?** Una collisione si verifica quando due o più record, anche se distinti, vengono mappati dalla funzione di hash sulla stessa posizione del file. Poiché ogni posizione nel file è associata a un unico **bucket** (una zona di memoria che contiene uno o più record), più record che hanno lo stesso valore di hash finiscono nello stesso bucket. **Gestione delle Collisioni** Quando si verifica una collisione e il bucket in questione è già pieno, è necessario adottare una strategia per gestire l\'inserimento dei record. Esistono due principali tecniche di gestione delle collisioni: 1. **Area di Overflow**: Quando un bucket non è in grado di contenere ulteriori record, si può creare un\'area di overflow. Questo è uno spazio aggiuntivo in cui i record che causano collisioni vengono inseriti. Ci sono due modalità comuni per l\'uso dell\'area di overflow: - **Inserimento libero**: I record vengono posizionati nell\'area di overflow in modo sequenziale, seguendo l\'ordine di arrivo. Questo approccio non richiede una struttura complessa, ma potrebbe rallentare l\'accesso ai dati se l\'area di overflow diventa molto grande. - **Lista concatenata**: Ogni bucket può essere associato a una lista concatenata che contiene i record con lo stesso valore di hash. In questo modo, quando si verifica una collisione, il record viene semplicemente aggiunto alla lista collegata al bucket. Questa soluzione consente di mantenere un\'organizzazione più ordinata e facilmente navigabile, ma comporta un ulteriore livello di indirezione (bisogna navigare nella lista concatenata per trovare il record desiderato). 2. **Riorganizzazione dei Bucket (Open Addressing)**: Un\'altra tecnica per risolvere le collisioni è l\'**open addressing**, che consiste nell\'assegnare al record un\'altra posizione nel file se la posizione calcolata è già occupata. In questo caso, quando si verifica una collisione, la funzione di hash può cercare una nuova posizione disponibile nel file, spostandosi in modo sequenziale o utilizzando altre strategie come il **linear probing** (scansione lineare delle posizioni) o il **quadratic probing** (scansione quadratica). **Indici di Accesso** Un **indice** è una struttura dati progettata per ottimizzare l\'accesso ai record di un database, migliorando l\'efficienza nelle operazioni di ricerca e recupero delle informazioni. Pur non essendo strettamente necessari per il funzionamento di un DBMS, gli indici sono strumenti fondamentali per velocizzare le query, riducendo i tempi di ricerca nei dati. In sostanza, un indice permette di localizzare rapidamente un record all\'interno di un file senza dover eseguire una scansione completa del contenuto, migliorando notevolmente la performance del sistema. Gli indici sono legati a uno o più **campi di ricerca**, che costituiscono la **chiave di ricerca**. Questa chiave può essere un singolo campo o una combinazione di campi della tabella, e viene utilizzata per determinare rapidamente la posizione del record all\'interno del file di dati. Ogni indice di solito associa alla chiave di ricerca un **identificatore di record (RID)**, che indica la posizione fisica del record nel file. In alcuni casi, un indice può essere progettato per memorizzare una **lista di identificatori** se diversi record condividono la stessa chiave di ricerca, permettendo così di gestire situazioni in cui la chiave non è univoca. **Tipi di Indici** Gli indici possono essere classificati in diverse tipologie, ciascuna con caratteristiche e vantaggi specifici. Il **indice primario** è uno dei più comuni e viene costruito su un file **sequenziale ordinato** in base alla **chiave primaria** di una relazione. Poiché la chiave primaria deve essere univoca, l\'indice primario permette un accesso rapido e diretto ai record del file. Un file può avere solo un **indice primario**, in quanto una chiave primaria è univoca per ogni record. Accanto agli indici primari, troviamo gli **indici secondari**, che vengono creati su una **chiave non primaria**. Gli indici secondari sono utili per ottimizzare le ricerche su colonne che non sono chiavi primarie ma che sono comunque frequentemente interrogate. A differenza degli indici primari, un file può avere **più indici secondari**, ciascuno dedicato a una colonna diversa che si desidera indicizzare. Esiste anche il **indice di clustering**, che è costruito su un campo che non è una chiave primaria, ma che raggruppa i record in base ai valori di quel campo. In altre parole, un indice di clustering raggruppa i record che hanno lo stesso valore in un campo e li memorizza fisicamente vicini nel file di dati. Un file può avere **un solo indice di clustering**, poiché la sua funzione è quella di determinare l\'ordine fisico dei record nel file. **Indici Sparsi e Indici Densi** Oltre ai diversi tipi di indice, è possibile fare una distinzione tra **indici sparsi** e **indici densi**. Un **indice sparso** è un tipo di indice che contiene un record per solo alcuni valori della chiave di ricerca. In altre parole, non tutti i possibili valori della chiave sono indicizzati, ma solo quelli più significativi o utilizzati frequentemente nelle query. Questo tipo di indice è utile quando non è necessario avere una corrispondenza completa tra ogni record e la sua chiave di ricerca. Un **indice denso**, invece, è un indice che contiene un record per ogni possibile valore della chiave di ricerca, assicurando che ogni singolo record del file dati sia rappresentato nell\'indice. Sebbene più completo, questo tipo di indice può richiedere più spazio di memoria e una gestione più complessa, ma offre una precisione maggiore nelle operazioni di ricerca. **Indexed Sequential Files** ![](media/image22.png)Un **file sequenziale indicizzato** è un tipo di file ordinato che utilizza un indice primario per migliorare l\'accesso ai dati. Questa struttura è stata sviluppata inizialmente con il metodo **ISAM** (Indexed Sequential Access Method) da IBM e successivamente evoluta in **VSAM** (Virtual Sequential Access Method) per un maggiore adattamento alle tecnologie di storage moderne. L\'indice primario consente di accedere rapidamente ai record memorizzati nel file ordinato, riducendo notevolmente i tempi di ricerca rispetto a una ricerca sequenziale. Il file sequenziale indicizzato è solitamente composto da tre principali sezioni: l\'**area di memorizzazione primaria**, dove i dati vengono memorizzati in modo ordinato; un **indice primario**, che contiene le chiavi di ricerca e i puntatori ai record nel file sequenziale; e un\'**area di overflow**, che gestisce l\'aggiunta di nuovi record quando lo spazio dell\'area primaria è esaurito. Grazie a questa struttura, la lettura e la scrittura dei dati possono essere effettuate in modo più efficiente. **Indice Multilivello** Un indice **multilivello** è utilizzato per organizzare file con un numero elevato di pagine, suddividendo il file in indici più piccoli. Ogni livello dell\'indice contiene voci che puntano a blocchi di dati o ad altri indici di livello inferiore. In pratica, per accedere ai dati, è necessario attraversare diversi livelli di indici, simili a un indice degli indici. Questo approccio migliora la velocità di accesso ai dati, riducendo il numero di passaggi necessari per localizzare un record. **Indici B-Tree e B^+^-Tree** Gli **indici B-Tree** e **B^+^-Tree** sono strutture dati fondamentali per la gestione efficiente dei dati in un database. Entrambe le strutture sono progettate per consentire operazioni rapide di ricerca, inserimento e cancellazione su file di grandi dimensioni, garantendo un accesso bilanciato ai dati. Questi indici sono particolarmente utili per migliorare le prestazioni dei database, poiché riducono significativamente il numero di operazioni di I/O richieste per trovare un record o per fare operazioni su intervalli di dati. **Il B-Tree** ![](media/image24.png)Il **B-Tree** è una struttura ad albero auto-bilanciata, in cui ogni nodo interno ha più di un figlio e contiene un numero variabile di chiavi. Le chiavi nei nodi sono sempre ordinate, e ogni chiave agisce come una \"guida\" per indirizzare la ricerca ai nodi figli. La principale caratteristica del B-Tree è che tutti i nodi foglia sono alla stessa profondità, garantendo così un accesso uniforme ai dati. Ogni nodo di un B-Tree contiene sia chiavi che puntatori ai sottolivelli (sottoalberi), il che permette di navigare rapidamente verso i record ricercati. Il numero di chiavi che ogni nodo può contenere dipende dall\'ordine dell\'albero, e i nodi interni devono contenere almeno metà del numero massimo di chiavi consentito. La struttura è progettata per ridurre al minimo il numero di accessi ai dischi, poiché ogni livello dell\'albero contiene più chiavi e quindi consente di ridurre il numero di livelli complessivi. Le operazioni di **ricerca** in un B-Tree sono particolarmente efficienti grazie alla capacità di restringere progressivamente la ricerca a sottolivelli più piccoli, riducendo il numero di operazioni necessarie per trovare un dato. Anche le operazioni di **inserimento** e **cancellazione** sono abbastanza efficienti, ma possono comportare una ristrutturazione dell\'albero, come la **divisione** dei nodi in caso di overflow o la **fusione** dei nodi in caso di sottoutilizzo. **Il B^+^-Tree** Il **B^+^-Tree** è una variante del B-Tree che ottimizza ulteriormente l\'accesso ai dati. La differenza principale tra B-Tree e B^+^-Tree è che, mentre nel B-Tree i nodi interni e foglia contengono sia chiavi che dati, nel B^+^-Tree solo i nodi foglia contengono i dati veri e propri. I nodi interni, invece, contengono solo le chiavi per guidare la ricerca, ma non memorizzano i dati associati. Questa separazione tra chiavi e dati rende il B^+^-Tree più efficiente nelle operazioni di ricerca, poiché i nodi interni contengono solo chiavi di indice che riducono il carico di lavoro per la navigazione. In un B^+^-Tree, i nodi foglia sono anche organizzati in una **lista concatenata**, che permette una scansione sequenziale molto efficiente dei dati, un\'operazione particolarmente utile quando si lavora con intervalli di valori o con **query di intervallo**. Un'altra caratteristica distintiva del B^+^-Tree è che, poiché i nodi foglia sono concatenati, è più facile eseguire operazioni sequenziali come la lettura di tutti i record in ordine. Questo rende il B^+^-Tree ideale per i database che devono gestire ricerche di intervallo o operazioni di scansione sequenziale, poiché consente di accedere ai dati in modo molto rapido. **Differenze tra B-Tree e B^+^-Tree** La principale differenza tra i due alberi sta nell'organizzazione dei dati: nel B-Tree, sia i nodi interni che i nodi foglia contengono dati e chiavi, mentre nel B^+^-Tree i nodi interni contengono solo chiavi e i dati sono concentrati nei nodi foglia. Questo porta a diverse implicazioni per l\'efficienza. Nel B^+^-Tree, la separazione tra chiavi e dati semplifica la struttura e permette una gestione più efficiente della memoria e dei dati, poiché i nodi interni contengono solo informazioni di navigazione. Inoltre, i nodi foglia del B^+^-Tree sono collegati tra loro, facilitando operazioni come la scansione di un intervallo di dati, che è più complessa in un B-Tree. Questo significa che il B^+^-Tree è più adatto a scenari in cui le operazioni di intervallo sono frequenti, mentre il B-Tree può essere preferito in contesti dove non si prevede un uso intensivo di ricerche su intervalli. Un'altra differenza importante è la gestione delle **operazioni di inserimento e cancellazione**. Poiché nel B^+^-Tree i dati sono contenuti solo nei nodi foglia, ogni modifica (inserimento o cancellazione) coinvolge solo i nodi foglia, semplificando la gestione rispetto a un B-Tree, dove anche i nodi interni possono essere aggiornati. **Indici per Data Werehousing** Gli indici di Bitmap e gli indici di Join sono tecniche di ottimizzazione ampiamente utilizzate nei database, specialmente in contesti di data warehousing e analisi dati, per migliorare le prestazioni di query complesse. Entrambi permettono di velocizzare l'accesso ai dati, riducendo il tempo di risposta per le query e alleggerendo il carico sul sistema. Approfondiamo come funzionano e in quali casi trovano impiego. **Indici di Bitmap** Gli indici di Bitmap sono particolarmente efficaci quando vengono applicati a campi con un numero limitato di valori distinti, detti attributi a **bassa cardinalità**. Esempi tipici sono attributi come il genere (maschio o femmina), lo stato civile (single, sposato, divorziato) o variabili booleane (vero/falso). L'indice di Bitmap costruisce, per ogni valore dell'attributo, un **vettore di bit** che rappresenta la presenza o l\'assenza del valore in ciascuna riga della tabella. Nel dettaglio, per ogni valore dell'attributo, si crea una sequenza di bit lunga quanto il numero delle righe della tabella. Ogni bit corrisponde a una riga: il bit sarà **1** se la riga contiene quel valore specifico dell'attributo, **0** altrimenti. **Indici di Join** Gli indici di Join sono pensati per ottimizzare le query che richiedono di unire dati provenienti da due o più tabelle. Un join è un'operazione fondamentale nei database relazionali, che permette di combinare dati da più tabelle basandosi su colonne correlate, ma, su grandi quantità di dati, può diventare molto oneroso in termini di prestazioni. Per risolvere questo problema, un indice di join pre-calcola l'unione tra due o più tabelle, salvando l'operazione di join e il risultato in una struttura di indice dedicata. In questo modo, quando si esegue una query che richiede il join tra le stesse tabelle, il database può accedere direttamente all\'indice pre-calcolato anziché dover rifare l\'intero processo di join. **Il Buffer Manager** La gestione della memoria in un sistema di gestione di basi di dati (DBMS) è fondamentale per garantire che le operazioni di lettura e scrittura sui dati siano eseguite nel modo più efficiente possibile. In questo contesto, il **Buffer Manager** (gestore del buffer) gioca un ruolo cruciale, poiché è responsabile del trasferimento di pagine di dati tra la memoria secondaria (disco) e la memoria centrale (RAM). Vediamo come funziona in dettaglio e quali politiche adotta per garantire efficienza e integrità dei dati. **Buffer e Buffer Manager: Definizioni e Ruoli** - **Buffer**: il buffer è un'area della memoria centrale, gestita dal DBMS, che serve come spazio temporaneo dove vengono memorizzate le pagine dei dati. È condiviso tra le varie transazioni e organizzato in unità chiamate **pagine**, che di solito hanno dimensioni pari o multipli dei blocchi di memoria secondaria (di solito tra 1 KB e 100 KB). L'obiettivo principale del buffer è ridurre il numero di accessi alla memoria secondaria, che sono costosi in termini di tempo rispetto all'accesso alla memoria centrale. - **Buffer Manager**: il buffer manager è il modulo del DBMS che gestisce il buffer e si occupa di trasferire le pagine tra il disco e la RAM. La sua funzione è di assicurare che le pagine necessarie alle transazioni siano disponibili in memoria centrale e di ridurre al minimo gli accessi alla memoria secondaria. **Funzionamento del Buffer Manager** Il funzionamento del Buffer Manager si articola in diversi passaggi che ottimizzano l'utilizzo del buffer e la gestione della memoria. Quando un livello superiore del DBMS richiede una pagina specifica (per una lettura o una scrittura), il Buffer Manager deve: 1. **Controllare la presenza della pagina nel buffer**: se la pagina richiesta è già presente nel buffer, viene direttamente utilizzata senza bisogno di accedere alla memoria secondaria. 2. **Caricare una pagina dal disco**: se la pagina richiesta non è presente nel buffer, il Buffer Manager deve leggere la pagina dalla memoria secondaria e trasferirla nel buffer. Tuttavia, se il buffer è già pieno, il gestore deve liberare spazio, adottando una strategia specifica per decidere quale pagina eliminare per fare posto alla nuova. **Strategie di Gestione della Memoria: Politiche FIFO e LRU** Il Buffer Manager può adottare diverse politiche per decidere quali pagine rimuovere dal buffer quando questo è pieno. Le principali politiche sono: - **FIFO (First In, First Out)**: con la politica FIFO, la prima pagina caricata nel buffer è anche la prima a essere rimossa. È una politica semplice, ma non sempre ottimale, poiché non tiene conto dell'utilizzo recente della pagina. - **LRU (Least Recently Used)**: questa politica cerca di mantenere in memoria le pagine utilizzate di recente, eliminando invece quelle che non sono state accedute da più tempo. LRU è più efficiente rispetto a FIFO in molti casi, poiché le pagine utilizzate di recente hanno una maggiore probabilità di essere richieste nuovamente. **Funzionamento Dettagliato del Buffer Manager** Per garantire un corretto funzionamento, il Buffer Manager gestisce variabili di stato come **count** e **dirty** per ciascuna pagina, e segue una serie di passaggi: - **Contatore di utilizzo (count)**: ogni pagina ha un contatore che traccia il numero di transazioni che stanno usando quella pagina. Questo contatore aumenta ogni volta che una transazione accede alla pagina e diminuisce quando una transazione la rilascia. - **Stato "sporco" (dirty)**: il flag **dirty** indica se la pagina è stata modificata mentre si trova nel buffer. Se la pagina è stata modificata (cioè ha dirty = 1), deve essere scritta sul disco prima di essere rimossa dal buffer per garantire la persistenza delle modifiche. Il processo di gestione delle pagine avviene nel modo seguente: 1. **Richiesta di pagina**: quando il Buffer Manager riceve una richiesta di lettura o scrittura per una pagina, verifica se la pagina è già presente nel buffer. 2. **Incremento di count e aggiornamento di dirty**: se la pagina è già presente, il valore di **count** viene incrementato di 1. Se la pagina viene modificata, il flag **dirty** viene posto a 1. 3. **Sostituzione di una pagina**: se la pagina richiesta non è nel buffer e non ci sono pagine libere, il Buffer Manager seleziona una pagina da sostituire usando una politica come FIFO o LRU. - Se la pagina selezionata per la sostituzione è **dirty**, il Buffer Manager la scrive su disco per assicurarsi che tutte le modifiche siano salvate. - Se non ci sono pagine libere, può adottare strategie come **steal** o **no-steal**: 4. **Caricamento della nuova pagina**: una volta liberato lo spazio, la pagina richiesta viene caricata nel buffer. Il valore **count** è impostato a 1, e **dirty** viene inizialmente posto a 0. **Query Processor** Il **Query Processor** (gestore delle query) è una componente centrale di un sistema di gestione di basi di dati (DBMS), responsabile di prendere in carico le query SQL, verificarne la correttezza, ottimizzarle e produrre un piano di esecuzione che consenta al database di rispondere alle richieste nel modo più efficiente possibile. Il Query Processor si avvale di varie tecniche di ottimizzazione, sia algebriche che basate sui costi, per minimizzare il tempo di risposta e l\'uso delle risorse. **Processo di Ottimizzazione delle Query** Il processo di ottimizzazione delle query comprende diverse fasi, ciascuna delle quali è mirata a migliorare l\'efficienza dell\'esecuzione: ![](media/image26.png)**Analisi della Query**: In questa fase, la query SQL viene sottoposta a controlli di tipo lessicale, sintattico e semantico. Questi controlli sono necessari per verificare che la query sia ben formata e valida rispetto al modello del database. Le informazioni relative allo schema del database sono memorizzate nel catalogo del DBMS, che viene consultato in questa fase per accertare la correttezza della query: - **Analisi lessicale**: esamina i token della query, ossia le parole chiave e i simboli, per assicurarsi che la sintassi sia corretta. - **Analisi sintattica**: verifica che la struttura della query sia conforme alle regole grammaticali del linguaggio SQL. - **Analisi semantica**: verifica che gli attributi e le tabelle citati nella query esistano e che le operazioni richieste siano valide rispetto ai tipi di dati e alle relazioni definite nel database. Alla fine di questa fase, la query viene tradotta in una **forma algebrica** usando l'algebra relazionale, ovvero una rappresentazione che consente di manipolare la query attraverso trasformazioni equivalenti. **Ottimizzazione Algebrica**: La query in forma algebrica viene ulteriormente trasformata applicando regole di equivalenza dell\'algebra relazionale. Lo scopo è quello di ottenere una versione della query che sia logicamente equivalente a quella originale ma più efficiente da eseguire. Questa fase si basa sul concetto di **equivalenza algebrica**, per cui due espressioni sono equivalenti se producono lo stesso risultato indipendentemente dall\'istanza attuale del database. Esempi di ottimizzazioni algebriche includono: - **Riordinamento delle operazioni**: ad esempio, applicare prima le selezioni più restrittive per ridurre il numero di tuple da processare in operazioni successive. - **Pushing di selezioni e proiezioni**: spostare le operazioni di selezione e proiezione il più vicino possibile alle operazioni di scansione delle tabelle, riducendo il numero di righe e colonne da elaborare nelle fasi successive. - **Eliminazione di ridondanze**: semplificare o eliminare parti della query che risultano ridondanti o superflue. **Concetto di Equivalenza Algebrica** Alla base dell'ottimizzazione algebrica vi è il concetto di **equivalenza algebrica** dell\'algebra relazionale. Due espressioni dell'algebra relazionale sono dette equivalenti se restituiscono lo stesso risultato per qualsiasi stato attuale della base di dati. Questo permette di trasformare la query iniziale in una forma equivalente più efficiente, senza alterare il risultato finale **Ottimizzazione Basata sui Costi**: Una volta ottenuta una forma algebrica ottimizzata, il Query Processor utilizza informazioni dettagliate sulle caratteristiche del database (ad esempio, il numero di righe nelle tabelle, la presenza di indici e le distribuzioni di valori degli attributi) per stimare i costi associati a varie strategie di esecuzione della query. Questa stima dei costi permette di determinare il **piano di esecuzione** più efficiente per eseguire la query. Al termine di questa fase, viene scelto un **piano di esecuzione finale**, che è una sequenza di operazioni di basso livello, come scansioni di tabelle, accesso agli indici e join, progettata per minimizzare il costo stimato. **Piani di Query e Ottimizzazione delle Query** La progettazione dei piani di esecuzione delle query è una componente fondamentale del funzionamento di un Database Management System (DBMS). Un piano di query rappresenta una sequenza di operazioni di basso livello, ottimizzate per ottenere il risultato desiderato con il minor costo computazionale possibile. Durante l'elaborazione di una query, il DBMS utilizza informazioni statistiche sulla struttura e sui dati presenti nella base di dati per stimare le dimensioni dei risultati intermedi e scegliere l'ordine di esecuzione ottimale delle operazioni. Le operazioni più comuni includono scansioni (che analizzano l'intero contenuto di una tabella), accessi diretti (che utilizzano indici per recuperare rapidamente specifici record), ordinamenti e operazioni di join (che combinano dati provenienti da più tabelle). L\'obiettivo dell\'ottimizzazione è ridurre al minimo il tempo di esecuzione della query e l\'uso delle risorse del sistema. Un piano di query può essere determinato in due modalità principali: - **Compile & Store:** In questa modalità, il piano di esecuzione viene generato una sola volta e memorizzato nel catalogo del DBMS. Questo approccio è particolarmente utile per query che devono essere eseguite frequentemente e non subiscono variazioni, poiché consente di evitare l'overhead di calcolare il piano ogni volta. - **Compile & Go:** In questo caso, il piano di esecuzione viene determinato dinamicamente ogni volta che la query vi