Forme Normali e Basi di Dati (PDF)
Document Details
Uploaded by ConsistentCosecant
Tags
Summary
This document discusses normal forms in database design. It explains the concepts of first, second, third normal form, and Boyce-Codd normal form, along with decomposition techniques. It also covers the use of triggers for active database behavior and database connectivity technologies like ODBC and JDBC. The document provides a structured overview of the topics.
Full Transcript
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 da...
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 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 Ogni query viene interpretata e processata da un modulo chiamato Gestore delle Query. Questo modulo analizza la query e, se necessario, la "ottimizza" selezionando il piano di esecuzione più efficiente per portarla a termine. L’ottimizzazione consiste nella scelta delle operazioni di basso livello da eseguire (come scansioni di tabelle, accessi diretti, ordinamenti e join) in base al costo computazionale e di accesso ai dati. L’analisi di una query richiede quindi un piano dettagliato in termini di operatori di basso livello. Per tradurre una query in questi operatori, il DBMS utilizza metodi specifici di accesso ai dati, con il supporto di strutture come indici e tabelle di gestione dei file. 2. Gestore dei Metodi di Accesso e dei File Per garantire l'accesso ai dati in modo efficiente, il Gestore dei Metodi di Accesso e dei File coordina le operazioni che riguardano l’accesso e la gestione delle informazioni sui file della base di dati. Da un punto di vista logico, ogni file può essere visto come una sequenza di record o una collezione di pagine di record. 3. Gestore del Buffer di Memoria Il Gestore del Buffer di Memoria determina quando e come trasferire le pagine di record tra la memoria centrale e la memoria di massa. Questo meccanismo è essenziale per garantire che solo le informazioni necessarie siano mantenute in memoria, riducendo al minimo i tempi di accesso a disco e ottimizzando l’uso della memoria centrale. 4. Gestore dello Spazio su Disco Le operazioni di lettura, scrittura, allocazione e rilasciamento delle pagine su disco sono controllate dal Gestore dello Spazio su Disco. Questo modulo fornisce un controllo accurato sull’uso del disco, assicurando che lo spazio sia utilizzato in modo efficiente e che le pagine vengano allocate o liberate correttamente. 5. Gestore della Concorrenza In un DBMS, molteplici utenti possono eseguire transazioni contemporaneamente. Il Gestore della Concorrenza è responsabile di assicurare che queste operazioni avvengano in modo corretto e coerente, mantenendo le proprietà ACID (Atomicità, Consistenza, Isolamento, Durabilità). Questo modulo garantisce che le transazioni non interferiscano tra loro, evitando conflitti e inconsistenze nei dati. 6. Gestore dell’Affidabilità Il Gestore dell’Affidabilità interviene in caso di guasti del sistema, ad esempio a causa di interruzioni di corrente o malfunzionamenti hardware. Questo modulo registra in tempo reale tutte le modifiche effettuate ai dati (attraverso un log delle transazioni) per poter ripristinare lo stato del sistema in caso di errore. In caso di un guasto, il log permette di ricostruire la base di dati fino a un punto di stato consistente. 7. Gestore dell’Integrità La consistenza dei dati è assicurata dal Gestore dell’Integrità, che controlla il rispetto dei vincoli di integrità definiti all’interno del database. Questo modulo entra in azione ogni volta che vengono eseguite operazioni di modifica sui dati per garantire che nessun vincolo venga violato, mantenendo la qualità e la coerenza delle informazioni memorizzate. 8. Gestore degli Accessi Il Gestore degli Accessi si occupa della sicurezza dei dati, assicurando che solo utenti e applicazioni autorizzati possano accedere alle informazioni della base di dati. Questo modulo controlla i privilegi degli utenti e verifica che le operazioni effettuate siano compatibili con i livelli di accesso assegnati, prevenendo accessi non autorizzati. 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 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. 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: o 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. o 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 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 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. In questo caso, viene aggiornato un contatore, detto count, che traccia quante transazioni stanno utilizzando quella pagina. Se la pagina viene modificata, viene segnato come dirty (sporco), indicandone la modifica in memoria centrale e la necessità di un eventuale aggiornamento sul disco. 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. o Se la pagina selezionata per la sostituzione è dirty, il Buffer Manager la scrive su disco per assicurarsi che tutte le modifiche siano salvate. o Se non ci sono pagine libere, può adottare strategie come steal o no-steal: Steal: il Buffer Manager può prelevare (steal) una pagina ancora in uso da un’altra transazione, salvandola su disco se è sporca. No-steal: il Buffer Manager evita di rimuovere pagine ancora in uso e cerca di aspettare che una pagina venga rilasciata. 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: 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: o Analisi lessicale: esamina i token della query, ossia le parole chiave e i simboli, per assicurarsi che la sintassi sia corretta. o Analisi sintattica: verifica che la struttura della query sia conforme alle regole grammaticali del linguaggio SQL. o 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: o Riordinamento delle operazioni: ad esempio, applicare prima le selezioni più restrittive per ridurre il numero di tuple da processare in operazioni successive. o 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. o 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 viene eseguita. Questo approccio è più adatto a query occasionali o con parametri variabili, dove i cambiamenti nei dati potrebbero influenzare la scelta del piano ottimale. Gestione delle Transazioni nei DBMS Un aspetto cruciale nella gestione di un DBMS è garantire che le operazioni simultanee effettuate da più utenti o programmi applicativi non compromettano la consistenza e l'integrità della base di dati. Questo compito è reso complesso dal numero elevato di operazioni concorrenti che un sistema può trovarsi a gestire in un ambiente multi- utente. Definizione di Transazione Una transazione rappresenta un'unità logica di elaborazione all'interno di un DBMS. Essa consiste in una sequenza di operazioni, quali letture e scritture sulla base di dati, che vengono eseguite come un blocco unico. Può corrispondere all'esecuzione di un intero programma, a una sua parte o a un singolo comando SQL, come un'istruzione INSERT, UPDATE o DELETE. L’aspetto fondamentale di una transazione è la sua indivisibilità. Questo implica che le modifiche effettuate durante una transazione sono atomiche: devono essere tutte applicate alla base di dati oppure nessuna. Una transazione, inoltre, porta la base di dati da uno stato consistente a un altro stato consistente, nel rispetto dei vincoli definiti sul modello dei dati. Stati di una Transazione: Commit e Abort Al termine della sua esecuzione, una transazione può concludersi in due modi: Commit: Se la transazione si completa con successo, tutte le modifiche effettuate sono confermate e registrate permanentemente nella base di dati. A questo punto, il nuovo stato della base di dati diventa visibile agli altri utenti e applicazioni. Abort: Se durante l’esecuzione si verifica un errore (ad esempio, un vincolo di integrità non viene rispettato, si verifica un crash del sistema o si decide di annullare la transazione), tutte le modifiche effettuate vengono annullate. Questo processo è noto come rollback e garantisce che la base di dati venga riportata al suo stato iniziale, evitando di lasciare effetti parziali o inconsistenti. Proprietà ACID delle Transazioni Un DBMS garantisce il corretto funzionamento delle transazioni mediante il rispetto delle proprietà ACID, che rappresentano i requisiti fondamentali per la gestione dei dati in un ambiente multi-utente: 1. Atomicità: La proprietà di atomicità assicura che una transazione venga eseguita interamente o per nulla. Se una parte della transazione fallisce, tutte le modifiche parziali vengono annullate. Questo comportamento protegge la base di dati da stati intermedi incoerenti. 2. Consistenza: Una transazione deve sempre portare la base di dati da uno stato consistente a un altro. Ciò significa che i vincoli definiti sul modello dei dati (come chiavi primarie, vincoli di unicità o di integrità referenziale) devono essere rispettati durante l’esecuzione della transazione. 3. Isolamento: In un ambiente con transazioni concorrenti, l’isolamento garantisce che le transazioni vengano eseguite in modo indipendente. Gli effetti parziali di una transazione in corso non devono essere visibili ad altre transazioni fino al momento del commit. Questo evita problemi come letture inconsistenti o "dirty reads". 4. Durabilità (Persistenza): Una volta che una transazione viene confermata tramite commit, i suoi effetti devono essere permanenti, anche in caso di guasti del sistema o interruzioni improvvise. Per garantire questa proprietà, il DBMS utilizza tecniche come il logging e i checkpoint. Gestione della Concorrenza e dell’Affidabilità Per supportare un accesso simultaneo alla base di dati da parte di più utenti o applicazioni, il DBMS include moduli dedicati alla gestione della concorrenza e dell’affidabilità. Gestore della Concorrenza: Questo modulo si occupa di garantire che le transazioni concorrenti non interferiscano tra loro, preservando l’integrità e la consistenza della base di dati. Tecniche comuni includono il locking (blocco delle risorse) e il timestamping, che assicurano un’esecuzione controllata delle transazioni. Gestore dell’Affidabilità: Questo modulo protegge la base di dati da eventi imprevedibili, come crash del sistema o interruzioni di corrente. Attraverso tecniche come il logging (registro delle operazioni) e il ripristino (recovery), il sistema è in grado di annullare le transazioni incomplete e ripristinare la base di dati a uno stato consistente. Diagramma degli Stati di una Transazione Il diagramma degli stati di una transazione rappresenta i diversi stadi che una transazione può attraversare durante la sua esecuzione. Ogni stato corrisponde a una specifica condizione della transazione, dal momento in cui viene avviata fino a quando termina con successo (commit) o viene annullata (abort). Di seguito, viene fornita una descrizione dettagliata degli stati e del flusso tra di essi: Stati di una Transazione 1. Active (Attivo): La transazione è stata avviata ed è in esecuzione. Durante questo stato, vengono eseguite le istruzioni della transazione, come letture, scritture e altre operazioni sulla base di dati. Questo è lo stato iniziale di ogni transazione. Una transazione può lasciare lo stato Active nei seguenti casi: o Passa allo stato Partially Committed dopo aver completato tutte le istruzioni con successo. o Entra nello stato Failed in caso di errore (ad esempio, violazione di vincoli di integrità o crash del sistema). 2. Partially Committed (Parzialmente Committed): La transazione si trova in questo stato quando ha completato con successo l'esecuzione dell'ultima istruzione prevista. Tuttavia, non è ancora completamente Committed perché i dati aggiornati potrebbero non essere stati ancora scritti su memoria secondaria (persistenza). In questa fase, la transazione è ancora vulnerabile: o Se si verifica un errore (ad esempio, un crash di sistema), passa allo stato Failed. o Se invece tutti gli aggiornamenti vengono salvati correttamente su disco, passa allo stato Committed. 3. Committed (Confermato): La transazione ha completato con successo tutte le operazioni e tutti i suoi effetti sono stati registrati permanentemente nella base di dati. Questo stato rappresenta la conclusione positiva della transazione. 4. Failed (Fallito): Una transazione entra nello stato Failed quando non può essere completata con successo. Questo può accadere per vari motivi, come la violazione di un vincolo di integrità o un'interruzione improvvisa (crash). Una transazione nello stato Failed è destinata a essere abortita. 5. Aborted (Annullato): La transazione è stata annullata e tutte le modifiche apportate sono state annullate (rollback). Lo stato della base di dati è stato riportato a quello precedente all'inizio della transazione. In alcuni casi, è possibile riavviare la transazione dopo un abort, tentando una nuova esecuzione. Gestione della Concorrenza nel DBMS La gestione della concorrenza è un aspetto cruciale nei DBMS, in particolare nei sistemi OLTP (Online Transaction Processing), dove decine o centinaia di transazioni vengono generate ogni secondo. L'obiettivo principale è consentire l'accesso simultaneo ai dati condivisi, garantendo al contempo la consistenza e l'integrità dei dati. Problemi legati agli accessi concorrenti Quando più transazioni accedono contemporaneamente agli stessi dati, possono verificarsi anomalie come: 1. Dirty Read: Una transazione legge dati modificati da un’altra transazione che non ha ancora effettuato il commit. 2. Non-Repeatable Read: Una transazione legge lo stesso dato due volte, ma ottiene risultati diversi perché un'altra transazione ha effettuato un aggiornamento nel frattempo. 3. Phantom Read: Una transazione rileva l'inserimento o l'eliminazione di nuovi record da parte di un'altra transazione durante la sua esecuzione. Serializzazione: la soluzione ideale, ma impraticabile Una soluzione banale a questi problemi sarebbe eseguire le transazioni in modo strettamente sequenziale (serializzazione). Tuttavia, questa strategia è impraticabile nei sistemi moderni, poiché ridurrebbe drasticamente il throughput e aumenterebbe i tempi di risposta. Nei sistemi OLTP, il DBMS deve essere in grado di eseguire operazioni in concorrenza massimizzando le transazioni per secondo (TPS). Soluzioni al Problema della Concorrenza nei DBMS Nei moderni sistemi di gestione delle basi di dati (DBMS), l'accesso concorrente ai dati è una caratteristica fondamentale. Tuttavia, questa concorrenza può portare a problemi di integrità e consistenza, soprattutto quando più transazioni accedono simultaneamente agli stessi dati. L'obiettivo primario del DBMS è garantire che tali accessi simultanei non compromettano la coerenza del sistema, permettendo nel contempo un'elevata efficienza operativa. Questo è particolarmente critico nei sistemi OLTP, dove il volume di transazioni al secondo (TPS) è molto elevato e il tempo di risposta deve rimanere basso per garantire prestazioni ottimali. Un approccio teorico, e al tempo stesso semplice, per risolvere i conflitti potrebbe consistere nell'eseguire tutte le transazioni in modo strettamente sequenziale. In altre parole, ogni transazione verrebbe completata interamente prima che un'altra abbia inizio. Tuttavia, questa soluzione non è praticabile, poiché comporterebbe un drastico rallentamento del sistema, rendendolo inadatto alle necessità operative delle applicazioni moderne. Di conseguenza, il DBMS adotta strategie più sofisticate per alternare e sovrapporre l'esecuzione di transazioni (esecuzione interleaved) senza sacrificare l'affidabilità dei dati. Per raggiungere questo equilibrio tra concorrenza ed efficienza, il DBMS implementa specifiche tecniche di controllo della concorrenza. Queste tecniche si concentrano sull'evitare situazioni problematiche, come la possibilità che una transazione legga dati parzialmente aggiornati (dirty read) o che il risultato finale sia influenzato da una sovrapposizione non gestita correttamente. Locking (Gestione dei Blocchi) Uno dei metodi principali utilizzati dai DBMS per prevenire interferenze tra transazioni è la gestione dei blocchi, o locking. Questo approccio si basa sull'idea di assegnare un controllo temporaneo su determinati dati a una transazione, impedendo ad altre di accedere o modificare quegli stessi dati fino a quando il controllo non viene rilasciato. Ad esempio, supponiamo che una transazione A stia aggiornando un record specifico. Durante questa operazione, un lock esclusivo (exclusive lock) viene posto sul record, garantendo che nessun'altra transazione possa né leggere né modificare quel dato fino al completamento dell'operazione. In modo simile, una transazione che deve solo leggere i dati può acquisire un lock condiviso (shared lock), consentendo ad altre transazioni di leggere gli stessi dati ma impedendo aggiornamenti simultanei. Questa strategia è particolarmente efficace, ma non è priva di rischi. Problemi come il deadlock, ovvero una situazione in cui due o più transazioni restano bloccate in attesa di risorse che non possono essere rilasciate, devono essere gestiti con attenzione tramite algoritmi di rilevamento e risoluzione. Timestamp Ordering Un altro approccio per garantire l'integrità dei dati è basato sull'ordinamento temporale delle transazioni. Ogni transazione riceve un timestamp univoco al momento della sua creazione, che determina l'ordine in cui le operazioni devono essere eseguite. Questo metodo assicura che le transazioni più "vecchie" abbiano priorità su quelle più recenti, evitando così situazioni in cui le transazioni più nuove sovrascrivano modifiche ancora in corso. Ad esempio, immaginiamo due transazioni, T1 e T2, che tentano di accedere a un dato condiviso. Se T1 ha un timestamp precedente rispetto a T2, al primo tentativo di accesso T2 sarà messa in attesa fino al completamento di T1. Questo sistema elimina il rischio di anomalie legate alla sovrapposizione delle operazioni, anche se potrebbe comportare un aumento del tempo di attesa in situazioni di carico elevato. Multiversion Concurrency Control (MVCC) Un approccio più avanzato e flessibile per gestire la concorrenza è rappresentato dal controllo della concorrenza multiversione, noto come MVCC. Questo metodo consente di mantenere più versioni dello stesso dato, permettendo alle transazioni di accedere a versioni diverse a seconda del loro stato. Ad esempio, una transazione in lettura può accedere a una versione "vecchia" di un dato, mentre una transazione in scrittura lavora su una versione aggiornata. Questo approccio elimina completamente i blocchi di lettura, aumentando significativamente le prestazioni in scenari con elevato numero di operazioni di lettura. Tuttavia, la complessità nella gestione delle versioni può comportare un maggiore utilizzo di memoria e tempi più lunghi per operazioni di garbage collection. Two-Phase Locking (2PL) Un'altra tecnica ampiamente utilizzata è la pianificazione a due fasi, o Two-Phase Locking (2PL). In questo approccio, ogni transazione attraversa due fasi distinte: 1. Fase di acquisizione dei lock: Durante questa fase, la transazione acquisisce tutti i lock necessari per completare le sue operazioni. 2. Fase di rilascio dei lock: Una volta che la transazione ha acquisito tutti i lock e completato le operazioni, i lock vengono rilasciati. Questo modello garantisce che una transazione non possa interferire con un'altra che si trova in una fase diversa, ma richiede un'attenta implementazione per evitare situazioni di stallo o inutili ritardi. Accesso Concorrente nei Sistemi di Basi di Dati Un sistema di basi di dati deve garantire accesso concorrente ai dati condivisi preservandone integrità e consistenza. L’accesso simultaneo, tuttavia, può introdurre anomalie che compromettono i dati, soprattutto quando transazioni si sovrappongono parzialmente. Principali Anomalie nei Sistemi di Basi di Dati Le anomalie più comuni che si verificano in un contesto di accesso concorrente sono: 1. Perdita di Aggiornamento (Lost Update): Quando due transazioni aggiornano lo stesso dato senza sincronizzazione, una sovrascrive l'altra, annullandone di fatto l’operazione. Questo avviene se una transazione non completa il proprio ciclo prima che l’altra intervenga. 2. Lettura Sporca (Dirty Read): Una transazione legge un valore modificato da un’altra che non ha ancora confermato le proprie operazioni tramite commit. Se la transazione che ha effettuato l’aggiornamento iniziale viene poi annullata (rollback), il dato letto risulta incoerente. 3. Aggiornamento Fantasma (Ghost Update): Si verifica quando una transazione aggiorna più oggetti correlati, ma un’altra transazione sovrapposta non rileva correttamente tutti gli aggiornamenti. Per esempio, una transazione potrebbe ignorare vincoli d’integrità tra dati correlati, rendendo alcuni aggiornamenti "invisibili". Soluzioni Tradizionali e Limiti dell’Accesso Serializzato Un approccio banale per evitare queste anomalie è eseguire le transazioni in modo serializzato, una alla volta, garantendo che nessuna interferisca con le altre. Questo metodo, però, è impraticabile nei sistemi OLTP (Online Transaction Processing), dove centinaia di transazioni al secondo richiedono tempi di risposta rapidi. L’accesso seriale, infatti, ridurrebbe drasticamente il throughput, penalizzando l’efficienza complessiva del sistema. Controllo della Concorrenza (CdC): Teoria Il controllo della concorrenza è un elemento essenziale nella gestione delle basi di dati moderne. Quando più transazioni accedono simultaneamente agli stessi dati, è necessario un sistema che garantisca la coerenza e l’integrità delle informazioni, evitando conflitti o anomalie che potrebbero compromettere l'affidabilità del sistema. In questa prospettiva, ogni transazione viene vista come una sequenza ordinata di operazioni di lettura e scrittura su oggetti della base di dati. Per esempio, si possono considerare due transazioni come segue: T1: legge un oggetto, lo aggiorna, poi legge e aggiorna altri oggetti (es. r1(X), w1(X), r1(Y), r1(Z), w1(Z)). T2: legge vari oggetti e aggiorna uno di essi (es. r2(X), r2(Y), r2(Z), w2(Y)). Nella pratica, spesso vengono omessi dettagli come i comandi di inizio transazione (begin transaction), conferma (commit) o annullamento (rollback), poiché l'interesse si concentra principalmente sulle interazioni tra le operazioni e sugli effetti complessivi sui dati. Concetti Chiave e Definizioni Per gestire correttamente la concorrenza tra transazioni, è utile definire alcuni concetti fondamentali: Schedule: uno schedule è una sequenza di operazioni di lettura e scrittura generate da un insieme di transazioni concorrenti. Deve rispettare l’ordine temporale delle operazioni all’interno di ciascuna transazione. Scheduler: lo scheduler è il componente del sistema che accetta, rifiuta o riordina le operazioni richieste dalle transazioni, al fine di preservare la consistenza dei dati. Un aspetto cruciale è rappresentato dal commit, il punto in cui una transazione raggiunge uno stato consistente. Quando una transazione viene annullata tramite abort, tutte le sue operazioni vengono rimosse dallo schedule. Tipi di Schedule Gli schedule possono essere classificati in base al loro comportamento: Schedule seriale: in questo caso, tutte le operazioni di una transazione vengono completate prima che inizi la successiva. Sebbene sia il metodo più semplice e sicuro, risulta inefficiente in termini di prestazioni. Schedule serializzabile: rappresenta uno schedule non seriale che garantisce tuttavia lo stesso risultato di uno schedule seriale. Questo approccio consente di bilanciare concorrenza e consistenza. Obiettivi del Controllo della Concorrenza Il controllo della concorrenza ha l’obiettivo di assicurare che l’esecuzione concorrente delle transazioni produca risultati consistenti e corretti. Per raggiungere questo scopo, è necessario, creare schedule serializzabili, evitando conflitti tra transazioni e implementare tecniche efficienti per verificare la serializzabilità, minimizzando i costi computazionali. I sistemi di gestione delle basi di dati (DBMS) moderni adottano meccanismi che garantiscono automaticamente la serializzabilità. Questi si dividono principalmente in due categorie: Metodi Basati sui Lock I metodi basati sui lock si fondano sull’idea di proteggere ogni oggetto del database con un meccanismo di blocco. Ogni volta che una transazione vuole accedere a un oggetto, deve prima acquisire un lock appropriato. Esistono due tipi principali di lock: 1. Read Lock (blocco di lettura): Questo tipo di lock consente a una transazione di leggere un oggetto. Più transazioni possono acquisire contemporaneamente un read lock sullo stesso oggetto, perché la lettura non altera lo stato dell’oggetto. 2. Write Lock (blocco di scrittura): Questo tipo di lock è esclusivo e permette di modificare un oggetto. Solo una transazione per volta può acquisire un write lock su un determinato oggetto, impedendo così che altre transazioni possano leggerlo o scriverlo contemporaneamente. Un aspetto importante è che, se una transazione intende prima leggere e poi scrivere un oggetto, può richiedere inizialmente un read lock e poi effettuare un’escalation al write lock. Questo approccio riduce il livello di contesa sugli oggetti del database, poiché il lock esclusivo viene acquisito solo quando strettamente necessario. Una volta terminata l’operazione, la transazione deve rilasciare il lock acquisito con un’operazione di unlock, restituendo così l’oggetto al suo stato libero e permettendo ad altre transazioni di accedervi. Ruolo del Lock Manager La gestione dei lock è affidata a una componente del DBMS chiamata lock manager, che riceve le richieste di blocco dalle transazioni e decide se concederle o rifiutarle, basandosi su una tabella dei conflitti. In questa tabella, si definisce se una richiesta di lock può essere accettata, considerando lo stato attuale dell’oggetto (libero, bloccato in lettura o bloccato in scrittura). Two-Phase Locking (2PL) e delle sue Varianti Il protocollo Two-Phase Locking (2PL) è uno dei metodi più utilizzati nei database per garantire la serializzabilità delle transazioni. La serializzabilità è una proprietà fondamentale per assicurarsi che le transazioni concorrenti, eseguite contemporaneamente, non compromettano l'integrità dei dati. Il 2PL viene applicato per evitare la cosiddetta incoerenza serializzabile, in cui l'ordine delle operazioni tra transazioni non rispetta un ordine seriale (un ordine che sarebbe stato eseguito se le transazioni fossero eseguite una dopo l’altra, senza sovrapposizioni). Il principio base del 2PL si fonda sul controllo dell’accesso agli oggetti del database tramite lock (blocco), in modo che le transazioni non possano interferire tra di loro. Esso prevede due fasi principali nell’esecuzione di una transazione: una fase crescente e una fase decrescente. Fase Crescente del 2PL La fase crescente è la fase in cui una transazione acquisisce tutti i lock necessari per compiere le operazioni di lettura e scrittura sugli oggetti del database. Acquisizione dei lock: Una transazione può acquisire lock in lettura (read lock) o in scrittura (write lock), ma non può rilasciare alcun lock durante questa fase. Il suo scopo principale in questa fase è quello di raccogliere tutti i lock necessari per garantire che nessun’altra transazione possa interferire con la sua esecuzione. Obiettivo: Durante questa fase, la transazione si prepara a completare tutte le sue operazioni, proteggendo le risorse (oggetti del database) da accessi concorrenti che potrebbero causare incoerenza. Questo impedisce che due transazioni possano scrivere nello stesso oggetto nello stesso momento, o che una transazione legga un dato che sta per essere modificato da un’altra transazione. Fase Decrescente del 2PL La fase decrescente si verifica una volta che la transazione ha acquisito tutti i lock necessari e ha completato le operazioni di lettura e scrittura sugli oggetti. Ora, la transazione può iniziare a rilasciare i lock. Rilascio dei lock: La transazione inizia a rilasciare i lock che aveva acquisito nella fase crescente. Tuttavia, un aspetto cruciale del protocollo è che, una volta che una transazione inizia a rilasciare un lock, non può più acquisirne di nuovi. Obiettivo: La fase decrescente segna la fine delle operazioni di scrittura o lettura su oggetti protetti da lock. Quando la transazione ha rilasciato tutti i lock, è terminata e può essere committata (finalizzata), assicurando che tutte le modifiche siano permanenti, o abbandonata (abort), nel caso in cui si verifichi un errore. Il vincolo chiave del 2PL è che una volta che una transazione rilascia un lock, non può acquisirne di nuovi. Questo impedisce che una transazione possa continuare a modificare un oggetto mentre sta già rilasciando altri lock, evitando così conflitti tra transazioni che potrebbero portare a uno stato inconsistente dei dati. Strict 2PL Il Strict 2PL è una variante del 2PL che aggiunge una restrizione importante per migliorare ulteriormente la coerenza dei dati. In particolare, nel Strict 2PL, i lock acquisiti da una transazione non vengono rilasciati fino al commit o all’abort della transazione. Questo approccio ha un impatto significativo sul controllo delle anomalie e sull'affidabilità generale del sistema. Rilascio dei lock post-commit: In Strict 2PL, i lock vengono mantenuti fino al commit (finalizzazione) della transazione o all’abort (annullamento). Ciò significa che le modifiche fatte dalla transazione non sono visibili ad altre transazioni finché la transazione non è completamente conclusa. Questa restrizione previene fenomeni come le letture sporche (dirty reads), in cui una transazione legge dati che potrebbero essere annullati (abortiti) da un’altra transazione. Affidabilità maggiore: Con Strict 2PL, le transazioni sono più sicure poiché non possono mai leggere dati che potrebbero essere modificati o annullati da altre transazioni. Questo aumenta la consistenza e l’affidabilità complessiva del sistema, riducendo la possibilità di ottenere uno stato del database errato o incoerente. Problemi del 2PL: Deadlock Uno dei problemi principali associati all'uso dei lock è il deadlock. Un deadlock si verifica quando due o più transazioni sono in uno stato di stallo, ciascuna in attesa che un’altra transazione rilasci un lock su un oggetto. Ad esempio: 1. La Transazione A ha un lock di scrittura su oggetto X e sta aspettando un lock di lettura su oggetto Y. 2. La Transazione B ha un lock di scrittura su oggetto Y e sta aspettando un lock di lettura su oggetto X. In questo scenario, entrambe le transazioni sono bloccate, poiché ognuna sta aspettando un lock che l’altra non può rilasciare finché non completa la sua operazione. Questo porta a una situazione di deadlock, dove nessuna delle transazioni può proseguire. Metodi Basati sul Timestamp I metodi basati sul timestamp offrono un approccio alternativo ai lock per garantire la serializzabilità delle transazioni, utilizzando l’ordine temporale come criterio principale per gestire l’accesso concorrente ai dati. L’idea fondamentale è che ogni transazione riceve un timestamp univoco al momento del suo inizio, e l’ordine di esecuzione delle operazioni è determinato da questo valore temporale. Questo consente di evitare la contesa esplicita sui dati e i relativi problemi associati, come i deadlock. Principio di Funzionamento Ogni transazione 𝑇𝑖 riceve un timestamp univoco 𝑇𝑆(𝑇𝑖 ) all’avvio. Questo timestamp rappresenta il momento in cui la transazione è iniziata e stabilisce la sua priorità rispetto alle altre. All’interno del database, per ogni oggetto X, vengono mantenuti due valori temporali associati al suo stato: TSR(X): il più recente timestamp della transazione che ha letto l’oggetto X; TSW(X): il più recente timestamp della transazione che ha scritto sull’oggetto X. Questi valori vengono aggiornati dinamicamente in base alle operazioni eseguite dalle transazioni. Il controllo di concorrenza si basa sul confronto tra il timestamp della transazione e questi valori temporali per determinare se l’operazione è consentita. Regole di Conflitto Per garantire la serializzabilità, il metodo stabilisce delle regole per l'accesso agli oggetti, distinguendo tra le operazioni di lettura e scrittura. 1. Operazione di Lettura: Se il timestamp della transazione è minore di TSW(X), significa che la transazione sta tentando di leggere un valore sovrascritto da una transazione successiva. Questo renderebbe impossibile mantenere un ordine seriale coerente. Di conseguenza, l'operazione viene rifiutata e 𝑇𝑖 deve essere abortita e riavviata con un nuovo timestamp. Se il timestamp della transazione è maggiore o uguale a TSW(X), l'operazione viene accettata. In questo caso, il valore viene aggiornato al timestamp, indicando che l’oggetto è stato letto da una transazione con quel timestamp. 2. Operazione di Scrittura: Se il timestamp della transazione è minore di TSR(X), significa che 𝑇𝑖 sta tentando di sovrascrivere un valore che è stato già letto da una transazione successiva. Per evitare anomalie, l’operazione viene rifiutata e la transazione deve essere abortita e riavviata. Se il timestamp della transazione è minore di TSW(X), significa che si sta tentando di scrivere su un oggetto che è già stato modificato da una transazione successiva. Anche in questo caso, l’operazione viene rifiutata e la transazione abortisce. Se nessuna delle condizioni precedenti si verifica, l’operazione è accettata. Il valore del timestamp in scrittura viene aggiornato a 𝑇𝑆(𝑇𝑖 ), e l’oggetto può essere modificato. Controllo di Affidabilità nei Sistemi di Gestione di Basi di Dati (DBMS) Il controllo di affidabilità è un aspetto cruciale per garantire che un sistema di database mantenga l'integrità dei dati anche in caso di guasti hardware o software. L'obiettivo del controllo di affidabilità è quello di ripristinare lo stato corretto del sistema dopo un malfunzionamento, assicurando che le proprietà ACID (Atomicità, Coerenza, Isolamento, Durabilità) siano rispettate. In particolare, il controllo di affidabilità garantisce che le transazioni siano atomiche (tutto o nulla), e che i dati siano persistenti anche in presenza di guasti. Se un guasto si verifica durante l'esecuzione di una transazione, il sistema deve essere in grado di ripristinare la base di dati nel suo stato precedente, come se il guasto non fosse mai accaduto. Memorie nei DBMS La gestione della memoria è fondamentale nel contesto del controllo di affidabilità. Le memorie coinvolte sono divise in tre principali tipologie: 1. Memoria centrale (RAM): Questa è la memoria volatile, usata per la gestione immediata dei dati durante l'esecuzione delle transazioni. È molto veloce ma non persistente, il che significa che i dati possono andare persi in caso di guasto. 2. Memoria di massa (dischi rigidi o SSD): Questo storage è persistente e offre una capacità di archiviazione molto maggiore rispetto alla memoria centrale. Tuttavia, la velocità di accesso ai dati è più lenta rispetto alla memoria centrale. Gestore dell'Affidabilità Il Gestore dell’Affidabilità è il componente del DBMS responsabile della gestione della persistenza dei dati, garantendo che tutte le modifiche delle transazioni completate siano memorizzate in modo permanente. Questo gestore è particolarmente importante per garantire che i dati rimangano coerenti dopo un guasto e che tutte le transazioni vengano ripristinate correttamente. La principale difficoltà è che le operazioni di scrittura non sono sempre atomiche. In altre parole, se una transazione ha effettuato delle modifiche in memoria centrale ma non ha ancora scritto i dati sulla memoria di massa al momento di un guasto, quei cambiamenti potrebbero essere persi. Concetti di Recovery Il recupero (recovery) si basa su vari concetti chiave che aiutano a riportare il sistema nel suo stato consistente dopo un guasto. I principali concetti di recovery sono i seguenti: File di Log: Il log è un file fondamentale per il recovery, poiché registra tutte le operazioni delle transazioni in ordine cronologico. È una sorta di "diario di bordo" che permette di ricostruire lo stato corretto del database dopo un guasto. Il log contiene dettagli come l'identificativo della transazione, l'operazione effettuata (es. lettura, scrittura), e l’oggetto del database coinvolto, oltre ai valori prima e dopo la modifica. Checkpoint: Il checkpoint è un'operazione che viene effettuata per ottimizzare le operazioni di recovery. Durante un checkpoint, il sistema "congela" lo stato delle transazioni e trasferisce tutte le modifiche in memoria di massa, registrando le transazioni attive e sincronizzando il contenuto della base di dati con il log. Questo riduce la quantità di lavoro necessario durante il recovery, poiché il sistema sa esattamente quali transazioni erano in corso e quali sono già state completate. Dump (Backup): Il dump è una copia del contenuto del database che viene archiviata su memoria stabile, di solito durante periodi di inattività del sistema. Questo backup è necessario per ricostruire i dati in caso di guasti gravi, come danni irreparabili alla memoria secondaria. Dettagli sul Log di Sistema Il log di sistema è il cuore del processo di recovery, e contiene diversi tipi di record. Ogni transazione che interagisce con il database è registrata nel log con i seguenti dettagli: ID della transazione (T): Identificativo univoco della transazione. Timestamp (TS): Il momento in cui l'operazione è stata eseguita. Operazione (Op): Tipo di operazione effettuata dalla transazione (begin, update, delete, insert, commit, abort). Oggetto (O): L'oggetto del database che è stato modificato. Before-image (𝑩𝑰 ): Il valore dell'oggetto prima della modifica da parte della transazione. After-image (𝑨𝑰 ): Il valore dell'oggetto dopo la modifica da parte della transazione. Inoltre, il log include: Record di Dump (DP): Indica il momento in cui è stato effettuato l'ultimo backup e fornisce dettagli sui file coinvolti. Record di Checkpoint (CK): Contiene un elenco delle transazioni attive al momento del checkpoint, facilitando il processo di recovery. Operazioni di Ripristino (Recovery) Il processo di ripristino (recovery) ha come obiettivo riportare il database in uno stato consistente dopo un guasto, utilizzando le informazioni contenute nel log. Undo e Redo Le due operazioni principali per il recovery sono: Undo (disfare): Se un guasto si verifica prima che una transazione completi il suo commit, tutte le sue modifiche devono essere annullate (undo). Ciò significa che, attraverso il log, il sistema ripristina i dati allo stato precedente alla transazione. Redo (rifare): Se un guasto si verifica dopo che una transazione ha effettuato il commit ma prima che i dati siano scritti sulla memoria di massa, tutte le modifiche devono essere ripetute (redo). Il sistema ripristina i dati, applicando nuovamente le modifiche registrate nel log, per assicurarne la persistenza. Ripristino dopo un Guasto 1. Se il guasto avviene prima del commit: Il sistema deve annullare tutte le modifiche delle transazioni che non sono state completate. Queste modifiche vengono ripristinate allo stato precedente all'inizio della transazione (operazione di undo). 2. Se il guasto avviene dopo il commit: Il sistema deve rifare tutte le operazioni che sono state committate ma non ancora scritte in memoria di massa (operazione di redo). Operazioni di Checkpoint Un checkpoint aiuta a ridurre il lavoro necessario durante il ripristino. Quando un checkpoint viene effettuato, il sistema assicura che tutte le transazioni che hanno effettuato il commit abbiano avuto i loro dati scritti sulla memoria di massa. Inoltre, registra le transazioni ancora attive al momento del checkpoint. In caso di guasto, il sistema può ripartire dal checkpoint più recente, limitando il numero di operazioni di undo e redo. Le operazioni di checkpoint sono quindi fondamentali per garantire che, in caso di guasto, il sistema possa riprendersi rapidamente senza dover analizzare l’intero log. Basi di Dati Direzionali Sistemi Informativi Direzionali: Struttura e Funzioni I sistemi informativi direzionali rappresentano una componente fondamentale nella gestione strategica di un'azienda moderna. Essi si pongono come supporto tecnologico e informativo alle decisioni aziendali, agevolando la direzione nel definire obiettivi, strategie e azioni da intraprendere per garantirne il successo e la sostenibilità a lungo termine. La complessità crescente del mercato e la necessità di adattamento alle variazioni economiche e tecnologiche richiedono una gestione informata e attenta dei processi, che è resa possibile da un sistema informativo adeguato e ben integrato nei livelli aziendali. Livelli di Gestione: Direzione e Operatività In un’azienda, le informazioni gestionali possono essere categorizzate e gestite su più livelli. Il livello direzionale è il punto di riferimento per la pianificazione a lungo termine e la definizione degli obiettivi aziendali. Questo livello comprende i dirigenti e i quadri che hanno il compito di analizzare dati complessi, interpretare le tendenze di mercato, anticipare le esigenze dei clienti e pianificare strategie per il futuro dell’azienda. Queste informazioni devono essere basate su dati attendibili e analisi precise, che permettano di valutare le opportunità e i rischi. Al livello operativo, invece, l’attenzione è focalizzata sulle attività quotidiane che garantiscono la produzione dei beni o la fornitura dei servizi dell’azienda. Gli operatori a questo livello svolgono compiti specifici e spesso ripetitivi, seguendo le linee guida stabilite dai dirigenti, con un focus principale sull’efficienza e sulla qualità del prodotto finale. Le attività operative generano un flusso continuo di dati riguardanti l’andamento produttivo, le vendite, le scorte di magazzino, i tempi di lavorazione e la qualità, che vengono poi inviati al livello direzionale sotto forma di reportistica. Tipologia delle Informazioni Direzionali I sistemi informativi direzionali operano elaborando informazioni ad alto livello, ovvero fortemente aggregate, che vengono sintetizzate per fornire ai dirigenti una visione chiara e facilmente interpretabile dello stato dell’azienda. Queste informazioni, raccolte e organizzate da vari sottosistemi operativi e gestionali, vengono trasformate in dati significativi chiamati “indicatori prestazionali”. Gli indicatori prestazionali forniscono una rappresentazione quantitativa dei risultati raggiunti, permettendo alla direzione di valutare sia il successo delle strategie intraprese sia l'andamento complessivo dell’azienda. La Natura Sintetica delle Informazioni Direzionali Un sistema informativo direzionale deve selezionare e aggregare i dati più rilevanti per consentire ai dirigenti di visualizzare in pochi parametri gli aspetti cruciali della gestione aziendale. A differenza delle informazioni operative, che possono comprendere una vasta gamma di dettagli puntuali (es. transazioni giornaliere, livelli di produzione, e tempi di ciclo), le informazioni direzionali devono ridurre questi dettagli a sintesi utili alla valutazione globale delle prestazioni. Ad esempio, invece di mostrare il numero esatto di prodotti venduti per ciascun punto vendita, il sistema può sintetizzare tali dati in metriche aggregate come il volume di vendite mensile, il ricavo totale per area geografica o la percentuale di crescita rispetto all’anno precedente. Il Paradigma “Indicatori – Misure – Fonti” nei Sistemi Informativi Direzionali Il paradigma "Indicatori – Misure – Fonti" è un modello strutturale utilizzato per organizzare le informazioni necessarie alla direzione aziendale in modo chiaro e funzionale. Questo approccio si fonda su tre elementi principali: gli indicatori, che rappresentano le metriche chiave per valutare le performance aziendali; le misure, che quantificano questi indicatori in valori specifici; e le fonti, ossia i dati e i sistemi che generano le informazioni necessarie. 1. Indicatori Gli indicatori sono variabili strategiche selezionate per monitorare l'andamento aziendale rispetto agli obiettivi stabiliti. Essi rappresentano i parametri essenziali che la direzione deve monitorare per comprendere se l'azienda sta progredendo nella direzione desiderata e se le strategie intraprese sono efficaci. 2. Misure Le misure sono i valori numerici attribuiti agli indicatori e rappresentano la quantificazione specifica degli obiettivi di performance. Se l'indicatore è il parametro da monitorare, la misura è il dato numerico che quantifica questo parametro. 3. Fonti Le fonti rappresentano i dati grezzi e i sistemi informativi dai quali vengono estratti gli indicatori e le misure. Possono includere sistemi di gestione delle risorse aziendali (ERP), database, reportistica, sondaggi di soddisfazione, e persino dati raccolti tramite strumenti IoT nei casi in cui si monitorano attività operative automatizzate. Le fonti sono suddivisibili in: Fonti interne: Dati generati direttamente dalle operazioni aziendali, come i report di vendita, i dati di produzione e il feedback raccolto dai clienti tramite sistemi di CRM. Fonti esterne: Dati provenienti dall'esterno dell'azienda, come le analisi di mercato, i benchmark di settore, i dati economici globali e altre informazioni rilevanti per le decisioni strategiche. Dimensioni (Variabili) dell’Analisi nelle Informazioni Direzionali Per una gestione efficace e informata, l’analisi delle informazioni direzionali in un’azienda deve considerare più dimensioni, ognuna delle quali offre una prospettiva specifica sulle prestazioni e sul raggiungimento degli obiettivi. Le dimensioni principali dell’analisi direzionale includono il tempo, il prodotto, i processi, la responsabilità e il cliente. Queste dimensioni costituiscono le variabili chiave attraverso cui è possibile ottenere una visione completa e dettagliata dell’andamento aziendale. 1. Dimensione Tempo La dimensione temporale consente di valutare l’andamento dell’azienda lungo un arco temporale specifico, come giorni, mesi, trimestri o anni. Questa dimensione è fondamentale perché permette alla direzione di monitorare l'evoluzione delle performance e di identificare trend o cicli. Ad esempio, analizzando i ricavi e le vendite su base trimestrale, la direzione può osservare come le strategie adottate in un dato periodo abbiano influito sui risultati complessivi e pianificare interventi in risposta a variazioni stagionali o cambiamenti di mercato. 2. Dimensione Prodotto La dimensione prodotto è cruciale per valutare costi e ricavi associati a ciascun prodotto o servizio offerto. Si tratta di un’analisi finalizzata non solo a monitorare la redditività di ciascun prodotto, ma anche a comprendere quali linee di prodotti contribuiscono maggiormente al fatturato e quali, eventualmente, devono essere rinnovate o potenziate. In termini contabili, questa dimensione si basa su indicatori di tipo monetario (ad esempio, margini di profitto, costi unitari, incidenza sul totale delle vendite) che misurano il valore economico generato da ciascun prodotto. 3. Dimensione Processi La dimensione processi si concentra sull’efficienza e sull’efficacia delle attività interne. È volta a monitorare le performance operative, valutando, ad esempio, la tempestività nella produzione e nella consegna, l’efficienza dei cicli produttivi e il rispetto dei tempi di progetto. I parametri di questa dimensione includono misure di qualità, tempi di ciclo, e tassi di errore, che consentono alla direzione di identificare i punti di forza e le aree di miglioramento nei processi aziendali. Analizzando i processi, è possibile apportare modifiche che migliorino la produttività e riducano gli sprechi. 4. Dimensione Responsabilità La dimensione responsabilità è fondamentale per valutare le prestazioni dei singoli dirigenti e dei rispettivi centri di responsabilità, che possono essere dipartimenti, unità di business o specifici team all’interno dell’azienda. Ogni centro di responsabilità è associato a una serie di indici di performance che riflettono il contributo specifico alle performance aziendali, come l’efficienza del dipartimento, la gestione del budget, e il raggiungimento degli obiettivi. Questa dimensione permette di assegnare in maniera chiara le responsabilità dei risultati ottenuti e di incentivare il miglioramento continuo. 5. Dimensione Cliente La dimensione cliente è fondamentale per comprendere la redditività e il volume di affari generati dai diversi segmenti di clientela. Questa dimensione include l’analisi della soddisfazione del cliente, la fidelizzazione, e la redditività di ciascun cliente o gruppo di clienti. Esaminare questa dimensione permette alla direzione di capire quali segmenti sono più profittevoli, quali necessitano di strategie di fidelizzazione, e dove possono essere implementati miglioramenti nei servizi. Inoltre, questa dimensione aiuta a valutare l’efficacia delle strategie di marketing e le risposte della clientela ai prodotti e ai servizi offerti. Architettura dei Sistemi Informativi Direzionali (SID) L'architettura dei Sistemi Informativi Direzionali (SID) si basa su una struttura suddivisa in due principali sottosistemi: front-end e back-end, supportati da una base dati direzionale comunemente nota come Data Warehouse. Questa architettura consente di raccogliere, organizzare, e presentare in modo efficace le informazioni necessarie per le decisioni strategiche aziendali. 1. Data Warehouse: La Base Dati Direzionale Il Data Warehouse è il cuore del Sistema Informativo Direzionale, una base dati direzionale in cui vengono raccolte e centralizzate le informazioni provenienti da diverse fonti aziendali. Queste informazioni vengono estratte dai sistemi operativi, riorganizzate e integrate per fornire una visione globale e aggiornata delle operazioni aziendali, utile per supportare le decisioni della direzione. Il Data Warehouse permette di mantenere una storicità dei dati, accumulando informazioni storiche utili per analisi di lungo termine e trend storici. Orientato al Soggetto Il DWH è progettato per essere orientato ai soggetti dell’elaborazione, il che significa che i dati sono organizzati attorno a entità specifiche rilevanti per l’analisi direzionale, come ad esempio clienti, prodotti, o regioni geografiche. Questo approccio si differenzia dalle tradizionali applicazioni di database, in cui i dati sono orientati principalmente ai processi o alle funzioni operative. Essere orientato al soggetto implica che le informazioni sono aggregate e strutturate secondo le dimensioni di interesse strategico, facilitando la comprensione del contesto e l’individuazione di tendenze e anomalie nei settori chiave. Integrato Un elemento fondamentale del DWH è la sua natura integrata. Le informazioni provengono da diverse basi dati aziendali (spesso eterogenee) e quindi necessitano di essere armonizzate per consentire analisi coerenti. Per raggiungere questa coerenza, il DWH unifica i dati tramite vari meccanismi di integrazione, tra cui: Misure consistenti delle variabili: Ogni dato viene riportato a un’unità di misura e formato standard, eliminando le discrepanze tra le varie fonti. Attributi fisici uniformi: Gli attributi dei dati (come le date o i codici prodotto) seguono convenzioni standard per semplificare la loro interpretazione. Strutture di codifica: Vengono unificate le codifiche, come quelle relative ai prodotti o ai clienti, per evitare ambiguità nell’analisi. Convenzioni sui nomi: I nomi delle variabili vengono standardizzati per facilitare la comprensione e l’utilizzo da parte degli utenti. Questa integrazione garantisce che tutte le informazioni siano consistenti e pronte per essere utilizzate nelle analisi, indipendentemente dalla fonte di origine, consentendo così una visione aziendale coerente. Tempo-Variante A differenza de