INDICE LINGUAGGIO SQL
1.01 - Database Relazionali
1.02 - Creare il database
1.03 - Popolare il database
1.04 - SELECT, interrogare il database
1.05 - Ricerca, filtraggio e ordinamento
1.06 - GROUP BY e funzioni di aggregazione
1.07 - UPDATE: modificare i dati
1.08 - DELETE, eliminare i record dalle tabelle
1.09 - Subquery
1.10 - Le transazioni
1.11 - Trigger
1.12 - Funzioni e procedure
1.13 - Modificare la struttura del database
1.14 - Utilizzo multiutente di un database
Dal sito HTML.it Guida linguaggio SQL
Chi ha necessità di utilizzare database sul proprio server non può prescindere dalla conoscenza di SQL (acronimo che
sta per Structured Query Language) il linguaggio per la creazione di database relazionali
Ritorna all'indice
1.01 - Database Relazionali
La struttura fondamentale del modello relazionale e’ appunto la relazione, cioe’ una tabella bidimensionale costituita
da righe (tuple) e colonne (attributi). Le relazioni rappresentano le entità che si ritiene essere interessanti nel database
Ogni istanza dell’entità trovera posto in una riga (tupla) della relazione, mentre gli attributi della relazione rappresente-
ranno le proprietà dell’entità.
Una relazione è la definizione della struttura della tabella, cioè il suo nome e l'elenco delle colonne (attributi) che la
compongono.
Ogni colonna (attributo) di una relazione è caratterizzato da un nome e da un dominio.
Il dominio indica quali valori possono essere assunti da una colonna della relazione (ad es. per il campo SESSO saranno
F, M). Una chiave esterna (foreign key) è una combinazione di attributi di una relazione che sono chiave primaria per
un’altra relazione.
Caratteristica fondamentale dei valori presenti in una chiave esterna è che, a meno che non siano null, devono corrispon-
dere a valori esistenti nella chiave primaria della relazione a cui si riferiscono.
Questa proprieta’ va sotto il nome di integrità referenziale (referential integrity)
Tutte le manipolazioni possibili sulle relazioni sono ottenibili grazie alla combinazione di cinque soli operatori:
REStrICT, PROJECT, TIMES, UNION e MINUS (operatori dell'algebra relazionale)
Per comodita’ sono stati anche definiti tre operatori addizionali che comunque possono essere ottenuti applicando i soli
cinque operatori fondamentali:
JOIN, INTERSECT e DIVIDE
Gli operatori relazionali ricevono come argomento una relazione o un insieme di relazioni
e restituiscono una singola relazione come risultato.
Vediamo brevemente questi otto operatori:
REStrICT: restituisce una relazione contenente un sottoinsieme delle tuple della relazione a cui
viene applicato. Gli attributi rimangono gli stessi.
PROJECT: restituisce una relazione con un sottoinsieme degli attributi della relazione a cui viene
applicato. Le tuple della relazione risultato vengono composte dalle tuple della relazione originale in modo che continuino
ad essere un insieme in senso matematico.
TIME: viene applicato a due relazioni ed effettua il prodotto cartesiano delle tuple.
Ogni tupla della prima relazione viene concatenata con ogni tupla della seconda.
JOIN: vengono concatenate le tuple di due relazioni in base al valore di un insieme dei loro attibuti.
UNION: applicando questo operatore a due relazioni compatibili, se ne ottiene una contenente le
tuple di entrambe le relazioni. Due relazioni sono compatibili se hanno lo stesso numero di attributi e gli attributi corrispon-
denti nelle due relazioni hanno lo stesso dominio.
MINUS: applicato a due relazioni compatibili, ne restituisce una terza contenente le tuple
che si trovano solo nella prima relazione.
INTERSECT: applicato a due relazioni compatibili, restituisce una relazione contenente le
tuple che esistono in entrambe le relazioni.
DIVIDE: applicato a due relazioni che abbiano degli attributi comuni, ne restituisce una
terza contenente tutte le tuple della prima relazione che possono essere fatte corrispondere a tutti i valori della seconda
relazione.
Nelle seguenti tabelle, a titolo di esempio, sono raffigurati i risultati dell’applicazione di alcuni operatori relazionali alle
relazioni Persone e Figli. Come nomi per le relazioni risultato si sono utilizzate le espressioni che le producono.
Persone
Figli
REStrICT (Persone)
sesso=’M’
PROJECT sesso (Persone)
JOIN (Persone, Figli)
Persone(numero_persona)=Figli(numero_persona)
I database relazionali compiono tutte le operazioni sulle tabelle utilizzando l’algebra relazionale, anche se normalmente
non permettono all’utente di utilizzarla.
L’utente interagisce con il database attraverso un’interfaccia differente, il linguaggio SQL, un linguaggio dichiarativo
che permette di descrivere insiemi di dati. Le istruzioni SQL vengono scomposte dal DBMS in una serie di operazioni
relazionali.
Ritorna all'indice
1.02 - Creare il database
Un database in un sistema relazionale e’ composto da un’insieme di tabelle, che corrispondono alle relazioni del modello
relazionale. Nella terminologia usata nell’SQL non si fa accenno alle relazioni, cosi’ come non viene usato il termine attributo,
ma viene usata la parola colonna, e non si parla di tupla, ma di riga.
Nel seguito verranno usate indifferentemente le due terminologie, quindi tabella varra’ per relazione, colonna per attributo, riga
per tupla, e viceversa.
In pratica la creazione del database consiste nella creazione delle tabelle che lo compongono.
In realta’ prima di poter procedere alla creazione delle tabelle normalmente occorre creare in effetti il database, il che di solito
significa definire uno spazio dei nomi separato per ogni insieme di tabelle.
In questo modo per un DBMS e’ possibile gestire piu’ database indipendenti contemporaneamente, senza che ci siano dei conflitti
con i nomi che vengono utilizzati in ciascuno di essi. Il sistema previsto dallo standard per creare degli spazi dei nomi separati
consiste nell’utilizzo dell’istruzione SQL CREATE SCHEMA. Di solito tale sistema non viene utilizzato (o almeno non con gli
scopi ed il significato previsti dallo standard), ma ogni DBMS prevede una procedura proprietaria per creare un database.
Normalmente viene esteso il linguaggio SQL introducendo un’istruzione non prevista nello standard: CREATE DATABASE.
La sintassi utilizzata da PostgreSQL, ma anche dai piu’ diffusi DBMS, e’ la seguente:
CREATE DATABASE nome_database
Una volta creato il database e’ possibile creare le tabelle che lo compogono. L’istruzione SQL preposta a questo scopo e’:
CREATE table nome_tabella (
nome_colonna tipo_colonna [ clausola_default ] [ vincoli_di_colonna ]
[ , nome_colonna tipo_colonna [ clausola_default ] [ vincoli_di_colonna ] … ]
[ , [ vincolo_di tabella] … ] )
Ad esempio se in Microsoft Access creo una nuova query e scrico il sequente codice:
CREATE table TAB1 (
COL1 CHAR,
COL2 INTEGER
)
avremo:
nome_colonna: e’ il nome della colonna che compone la tabella. Sarebbe meglio non esagerare con la lunghezza degli
identificatori di colonna, dal momento che l’SQL Entry Level prevede nomi non piu’ lunghi di 18 caratteri. Si consulti
comunque la documentazione dello specifico database. I nomi devono iniziare con un carattere alfabetico.
tipo_colonna: e’ l’indicazione del tipo di dato che la colonna potra’ contenere. I principali tipi previsti dallo standard
SQL sono:
CHARACTER(n): una stringa a lunghezza fissa di esattamente n caratteri. CHARACTER puo’ essere abbreviato
con CHAR
CHARACTER VARYING(n): una stringa a lunghezza variabile di al massimo n caratteri. CHARACTER VARYING
puo’ essere abbreviato con VARCHAR o CHAR VARYING.
INTEGER: un numero intero con segno. Puo’ essere abbreviato con INT. La precisione, cioe’ la grandezza del
numero intero che puo’ essere memorizzato in una colonna di questo tipo, dipende dall’implementazione del
particolare DBMS.
SMALLINT: un numero intero con segno con precisione non superiore a INTEGER.
FLOAT(p): un numero a virgola mobile, con precisione p. Il valore massimo di p dipende dall’implementazione
del DBMS. E’ possibile usare FLOAT senza indicazione della precisione, utilizzando quindi la precisione di default,
anch’essa dipendente dall’implementazione. REAL e DOUBLE PRECISION sono dei sinonimi per un FLOAT con
una particolare precisione. Anche in questo caso le precisioni dipendono dall’implementazione, con il vincolo che la
precisione del primo non sia superiore a quella del secondo.
DECIMAL(p,q): un numero a virgola fissa di almeno p cifre e segno, con q cifre dopo la virgola. DEC e’ un
abbreviazione per DECIMAL. DECIMAL(p) e’ un abbreviazione per DECIMAL(p,0).
Il valore massimo di p dipende dall’implementazione.
INTERVAL: un periodo di tempo (anni, mesi, giorni, ore, minuti, secondi e frazioni di secondo).
DATE, TIME e TIMESTAMP: un preciso istante temporale. DATE permette di indicare l’anno, il mese e il giorno.
Con TIME si possono specificare l’ora, i minuti e i secondi. TIMESTAMP e’ la combinazione dei due precedenti.
I secondi sono un numero con la virgola, permettendo cosi’ di specificare anche frazioni di secondo.
clausola_default: indica il valore di default che assumera’ la colonna se non gliene viene assegnato uno esplicitamente nel
momento della creazione della riga. La sintassi da utilizzare e’ la seguente:
DEFAULT { valore | NULL }
dove, valore e’ un valore valido per il tipo con cui la colonna e’ stata definita.
vincoli_di_colonna: sono vincoli di integrita’ che vengono applicati al singolo attributo. Sono:
NOT NULL, che indica che la colonna non puo’ assumere il valore NULL.
PRIMARY KEY, che indica che la colonna e’ la chiave primaria della tabella.
una definizione di riferimento, con cui si indica che la colonna e’ una chiave esterna verso la tabella e i campi
indicati nella definizione. La sintasi e’ la seguente:
REFERENCES nome_tabella [ ( colonna1 [ , colonna2 ... ] ) ]
[ ON DELETE { CASCADE | SET DEFAULT | SET NULL } ]
[ ON UPDATE { CASCADE | SET DEFAULT | SET NULL } ]
Le clausole ON DELETE e ON UPDATE indicano quale azione deve essere compiuta nel caso in cui una riga (tupla)
nella tabella referenziata venga eliminata o aggiornata. Infatti in tali casi nella colonna referenziante (che e’ quella
che si sta definendo) potrebbero esserci dei valori inconsistenti. Le azioni possono essere:
CASCADE: eliminare la tupla contenente la colonna referenziante (nel caso di ON DELETE) o aggiornare anche
la colonna referenziante (nel caso di ON UPDATE).
SET DEFAULT: assegnare alla colonna referenziante il suo valore di default.
SET NULL: assegnare alla colonna referenziante il valore NULL.
un controllo di valore, con il quale si permette o meno l’assegnazione di un valore alla colonna, in base al risultato di
un espressione. La sintassi da usare e’:
CHECK (espressione_condizionale)
dove espressione_condizionale e’ un’espressione che restituisce vero o falso.
Ad esempio, se stiamo definendo la colonna COLONNA1, definendo il seguente controllo:
CHECK ( COLONNA1 < 1000 )
in tale colonna potranno essere inseriti solo valori inferiori a 1000.
vincoli_di_colonna: sono vincoli di integrita’ che possono riferirsi a piu’ colonne della tabella. Sono:
la definizione della chiave primaria:
PRIMARY KEY ( colonna1 [ , colonna2 ... ] )
Si noti che in questo caso, a differenza della definizione della chiave primaria come vincolo di colonna, essa puo’ essere
formata da piu’ di un attributo.
le definizioni delle chiavi esterne:
FOREIGN KEY ( colonna1 [ , colonna2 ... ] ) definizione_di_riferimento
La definizione_di_riferimento ha la stessa sintassi e significato di quella che puo’ comparire come vincolo di colonna.
un controllo di valore, con la stessa sintassi e significato di quello che puo’
essere usato come vincolo di colonna.
Per chiarire meglio l’utilizzo dell’istruzione CREATE table, esaminiamo alcuni comandi che implementano il database
bibliografico di esempio.
CREATE table Publication (
ID INTEGER PRIMARY KEY,
type CHAR(18) NOT NULL
);
In Access avremo:
La precedente istruzione crea la tabella Publication, formata dalle due colonne ID di tipo INTEGER, e type di tipo CHAR(18).
ID e’ la chiave primaria della relazione. Sull’attributo type e’ posto un vincolo di non nullita’.
CREATE table Book (
ID INTEGER PRIMARY KEY REFERENCES Publication(ID),
title VARCHAR(160) NOT NULL,
publisher INTEGER NOT NULL REFERENCES Publication(ID),
volume VARCHAR(16),
series VARCHAR(160),
edition VARCHAR(16),
pub_month CHAR(3),
pub_year INTEGER NOT NULL,
nota VARCHAR(255)
);
In Access avremo la creazione della nuova tabelle e della relazione tra book e Pubblication:
Creiamo anche:
CREATE table Person (
ID INTEGER PRIMARY KEY,
Names CHAR(18) NOT NULL,
Surname CHAR(18) NOT NULL,
Address CHAR(32),
CAP INTEGER,
City CHAR(18),
Country CHAR(2)
);
Crea la relazione Book, formata da nove attributi. La chiave primaria e’ l’attributo ID, che e’ anche una chiave esterna
verso la relazione Publication. Sulle colonne (attributi) title, publisher e pub_year sono posti dei vincoli di non nullità.
Inoltre l’attributo publisher e’ una chiave esterna verso la tabella Publisher.
CREATE table Author (
publicationID INTEGER REFERENCES Publication(ID),
personID INTEGER REFERENCES Person(ID),
PRIMARY KEY (publicationID, personID)
);
Crea la relazione Author, composta da due attributi: publicationID e personID. La chiave primaria in questo caso è
formata dalla combinazione dei due attributi, come indicato dal vincolo di tabella PRIMARY KEY. PublicationID è
una chiave esterna verso la relazione Publication, mentre personID lo e’ verso la relazione Person.
Ritorna all'indice
1.03 - Popolare il database
INSERT, inserire una riga in una tabella
L’istruzione SQL che effettua l’inserimento di una nuova riga in una tabella è INSERT. La sintassi con cui essa viene usata
piu’ comunemente è:
INSERT INTO nome_tabella [ ( elenco_campi ) ] VALUES ( elenco_valori )
nome_tabella: è il nome della tabella in cui deve essere inserita la nuova riga;
elenco_campi: è l’elenco dei nomi dei campi a cui deve essere assegnato un valore, separati fra loro da una virgola.
I campi non compresi nell’elenco assumeranno il loro valore di default o NULL se non hanno un valore di default.
È un errore non inserire nell’elenco un campo che non abbia un valore di default e non possa assumere il valore NULL.
Se l’elenco non viene specificato dovranno essere specificati i valori di tutti i campi della tabella.
elenco_valori: è l’elenco dei valori che saranno assegnati ai campi della tabella, nell’ordine e numero specificati
dall’elenco_campi o in quello della definizione della tabella (se elenco_campi non viene specificato).
I valori possono essere un’espressione scalare del tipo appropriato per il campo o le keyword DEFAULT o NULL, se il campo
prevede un valore di default o ammette il valore NULL.
Il precedente esempio di inserimento viene quindi eseguito tramite le seguenti istruzioni SQL (una alla volta in ACCESS):
INSERT INTO Person VALUES ( 1, 'Agosti', 'Maristella' );
INSERT INTO Person VALUES ( 2, 'Benfante', 'Lucio' );
INSERT INTO Person VALUES ( 3, 'Melucci', 'Massimo' );
INSERT INTO Person VALUES ( 4, 'Spaccapietra', 'S.' );
INSERT INTO Person VALUES ( 5, 'Maryansky', 'F.' );
INSERT INTO Institution ( ID, name, city, country )
VALUES ( 1, '7th IFIP 2.6 Working Conference on Database Semantics (DS-7)',
'Leysin', 'Switzerland' );
INSERT INTO Publication VALUES ( 1, 'Proceedings' );
INSERT INTO Publication VALUES ( 2, 'InProceedings' );
INSERT INTO Proceedings ( ID, title, organization, pub_month, pub_year )
VALUES ( 1, 'Searching for Semantics: Data Mining, Reverse Engineering',
1, 'Oct', 1997 );
INSERT INTO InProceedings ( ID, proceedingsID, title, pages )
VALUES ( 2, 1,
'OFAHIR: "On-the-Fly" Automatic Authoring of Hypertexts for Information Retrieval',
'129-154' );
INSERT INTO Author VALUES ( 2, 1 );
INSERT INTO Author VALUES ( 2, 2 );
INSERT INTO Author VALUES ( 2, 3 );
INSERT INTO Editor VALUES ( 1, 4 );
INSERT INTO Editor VALUES ( 1, 5 );
Un’altra forma abbastanza utilizzata dell’istruzione INSERT segue la seguente sintassi:
INSERT INTO nome_tabella [ ( elenco_campi ) ]
istruzione_select
L’unica differenza con la precedente sintassi consiste nella sostituzione della clausola VALUES con un istruzione SELECT.
SELECT permette di estrarre dalle tabelle del database dei dati che vengono organizzati in una nuova relazione.
La precedente istruzione INSERT permette quindi di inserire nella tabella e nei campi specificati dati provenienti da altre tabelle.
Ovviamente, affinché l’istruzione venga eseguita con successo, i dati prodotti dall’istruzione SELECT dovranno essere compatibili
con i vincoli ed i domini dei campi della tabella in cui si sta effettuando l’inserimento.
Ritorna all'indice
1.04 - SELECT, interrogare il database
La SELECT è l'istruzioni che ci permettono di estrarre i dati che ci interessano.
La sintassi completa è la seguente:
SELECT [DISTINCT ] lista_elementi_selezione
FROM lista_riferimenti_tabella
[ WHERE espressione_condizionale ]
[ GROUP BY lista_colonne ]
[ HAVING espressione_condizionale ]
[ ORDER BY lista_colonne ]
Le uniche clausole obbligatorie sono SELECT e FROM.
Quest’ultima, detta clausola di selezione, stabilisce da quale tabella estrarre i dati, mentre la prima è detta
clausola di proiezione e stabilisce quali colonne devono essere riportate nel risultato finale.
La clausola WHERE definisce invece un filtro sulle righe che verranno analizzate, mentre ORDER BY indica l’ordinamento da
applicare al risultato finale. Se WHERE non viene specificata, non viene applicato alcun filtro sulle righe, che quindi vengono
esaminate tutte. Se invece non viene specificato alcun ordinamento, le righe vengono restituite senza un ordine definito,
generalmente così come vengono trovate in tabella.
Per quanto riguarda selezione e la proiezione, di cui ci occupiamo in questa lezione, vediamo subito un esempio:
SELECT Person.surname FROM Person
Questa query estrae dalla tabella Person tutti gli elementi, restituendo solamente la colonna surname.
Si possono indicare più colonne separandole con una virgola:
SELECT Person.surname, Person.names FROM Person
La sintassi completa prevede di specificare la colonna con la tabella di appartenenza;
quest’ultima, inoltre, si può omettere se non ci sono ambiguità:
SELECT surname, names FROM Person
Si può utilizzare l’asterisco (*) per indicare che si vogliono analizzare tutte le colonne di una tabella. Ad esempio:
SELECT Person.* FROM Person
che, se non ci sono ambiguità, equivale alla forma più comunemente utilizzata:
SELECT * FROM Person
È possibile anche specificare dei nomi alternativi (alias) per le colonne restituite come risultato, usando la parola chiave AS:
SELECT surname, names AS Allias_Names FROM Person
Il risultato produrrà due colonne, la prima denominata surname, e la seconda Allias_Names (in virtù dell’alias definito).
L’uso degli alias è particolarmente comodo quando si usano le espressioni: infatti, è possibile utilizzare espressioni SQL nella
clausola di proiezione per effettuare calcoli o elaborazioni sui valori presenti nei campi.
Ad esempio, la seguente interrogazione restituisce il cognome e l’iniziale del nome di ogni persona:
SELECT surname, MID(names, 1, 1) AS Iniziale FROM Person
PS: SUBSTRING in Access è sostituita da MID
Le possibili espressioni utilizzabili sono moltissime e dipendono in gran parte dal database utilizzato, quindi spesso le stesse
funzioni hanno nomi o sintassi diverse in MySQL, Oracle o SQL Server. Purtroppo ciò limita la portabilità delle espressioni tra i
vari database. Per i dettagli è quindi necessario ricorrere alla documentazione ufficiale dei singoli database.
Ci sono funzioni per lavorare con le date, con i numeri, con le stringhe di testo o per gestire i valori NULL.
Ad esempio, la funzione COALESCE restituisce il primo valore non nullo tra le espressioni indicate:
SELECT COALESCE(surname, given_names, 'Senza nome') AS name FROM Person
Esistono anche funzioni condizionali, come ad esempio CASE, che restituisce un valore in base al soddisfacimento di un certo predicato.
Per esempio, la seguente query calcola l’iniziale solo quando (CASE WHEN) il nome della persona è più lungo (LENGTH) di cinque caratteri:
SELECT CASE WHEN LENGTH(given_names) > 5
THEN CONCAT(SUBSTRING(given_names, 1, 1), '.')
ELSE given_names
END AS Abbrev
FROM Person
Si possono concatenare più espressioni CASE WHEN, esattamente come un’espressione else if del C o degli altri linguaggi imperativi.
Nel prossimo capitolo vedremo una particolare classe di funzioni, dette di aggregazione, che possono essere usate per eseguire operazioni
quali conteggi, somme, medie.
DISTINCT, eliminare i duplicati
La prima query che abbiamo visto restituirebbe righe duplicate nel caso in cui nella tabella fossero presenti persone con lo stesso cognome.
Per evitare ciò occorre specificare l’opzione DISTINCT:
SELECT DISTINCT surname FROM Person
Questa opzione fa sì che nel risultato non ci siano righe uguali tra loro, ed è utile soprattutto quando si lavora con i join.
Un join (congiunzione) è una funzione che stabilisce come combinare righe di due tabelle diverse per ottenere una tabella unica.
Si distinguono generalmente tre tipi di join, che vediamo di seguito.
Inner join
Questa operazione, che in SQL è implementata dalla clausola INNER JOIN o semplicemente JOIN, combina solo le righe delle due tabelle che
soddisfano un certo predicato di confronto, come in un’operazione di intersezione.
Si possono effettuare confronti tra date o numeri; ad esempio per avere tutti i libri usciti dopo la data di pubblicazione di ogni articolo:
SELECT Book.title, Article.title
FROM Book
JOIN Article on Article.pub_year > Book.pub_year
Comunque nella maggior parte dei casi questo predicato è un’uguaglianza tra ID:
SELECT surname, given_names, title
FROM Person
JOIN Author on person.ID = Author.personID
JOIN Book on Book.ID = Author.publicationID
Questa query restituisce i nominativi e i titoli dei libri pubblicati.
Outer join
La query precedente restituirà solo i libri e le persone per cui esiste un collegamento.
Quindi non restituirà né le persone che non hanno scritto libri né i libri senza autore.
A seconda se vogliamo anche questi avremo rispettivamente un LEFT OUTER o un RIGHT OUTER JOIN. Ad esempio:
SELECT surname, given_names, title
FROM Person
LEFT JOIN Author on person.ID = Author.personID
LEFT JOIN Book on Book.ID = Author.publicationID
Come si vede la parola chiave OUTER è opzionale.
In questo caso, per le persone che non hanno pubblicato un libro, la colonna title avrà valore NULL.
MySQL non supporta nativamente il FULL OUTER JOIN, che permette di fare un join destro e sinistro simultaneamente, per ottenere,
nel nostro caso, oltre agli autori e ai libri anche i libri pubblicati anonimamente e le persone che non hanno scritto libri:
SELECT surname, given_names, title
FROM Person
FULL JOIN Author on person.ID = Author.personID
FULL JOIN Book on Book.ID = Author.publicationID
Unioni
L’operazione sistemistica di unione si può fare in SQL utilizzando la parola chiave UNION.
Ad esempio, per effettuare qualcosa di simile ad un FULL JOINcon MySQL si può scrivere:
SELECT surname, given_names, title FROM Person
LEFT JOIN Author on person.ID = Author.personID
LEFT JOIN Book on Book.ID = Author.publicationID
UNION
SELECT surname, given_names, title FROM Person
RIGHT JOIN Author on person.ID = Author.personID
RIGHT JOIN Book on Book.ID = Author.publicationID
Nell’uso di UNION bisogna fare in modo che le colonne delle varie SELECT da concatenare abbiano lo stesso numero e siano dello
stesso tipo altrimenti si avrà un errore.
Proprio come l’operazione sistemistica di unione, questa ignora le righe duplicate. Quindi, come per DISTINCT, è necessario prestare
attenzione nell’utilizzo perché se le righe restituite sono in grande quantità, l’eliminazione dei duplicati può impegnare molto tempo
il server. Per evitare la ricerca dei duplicati su deve usare l’istruzione UNION ALL:
SELECT surname, given_names, title FROM Person
JOIN Author on person.ID = Author.personID
JOIN Book on Book.ID = Author.publicationID
UNION ALL
SELECT surname, given_names, title FROM Person
JOIN Author on person.ID = Author.personID
JOIN Article on Article.ID = Author.publicationID
Ritorna all'indice
1.05 - Ricerca, filtraggio e ordinamento
Continuiamo con l’istruzione SELECT e stavolta vediamo le clausole WHERE e ORDER BY.
Filtraggio
Già nella lezione precedente, quando abbiamo parlato di inner join, abbiamo visto un particolare tipo di filtraggio, l’intersezione tra insiemi.
Ora vediamo invece come, con la clausola WHERE, possiamo filtrare le nostre righe in modo molto preciso sulla base dei valori che assumono i
campi.
In generale, la sintassi di questa clausola è un espressione che può essere o vera o falsa per una certa riga del nostro insieme di dati.
L’espressione più semplice ovviamente è l’eguaglianza:
SELECT *
FROM Person
WHERE surname = 'Knuth'
In questo caso verranno restituite le persone aventi un certo cognome. Usando l’operatore AND possiamo combinare due espressioni per richiedere
di restituire le righe che soddisfano entrambe le condizioni:
SELECT * FROM Person
WHERE surname = 'Knuth' AND given_names = 'Donald'
In questo caso, nel nostro database di esempio, non avremo alcun risultato perché l’operatore di uguaglianza (=) viene soddisfatto solo se i
valori sono esattamente uguali, mentre ‘Donald E.‘ è diverso da ‘Donald‘. Con l’operatore LIKE, utilizzabile solamente con i dati di tipo testuale
come VARCHAR, possiamo invece cercare i valori che corrispondono ad un certo pattern. Si possono utilizzare due caratteri jolly:
- il carattere % verrà soddisfatto da qualsiasi sequenza di caratteri trovata;
- il carattere _ verrà soddisfatto da qualsiasi carattere (singolo) trovato.
Stavolta, quindi, questa interrogazione restituirà esattamente il record di Donald E. Knuth:
SELECT * FROM Person
WHERE surname = 'Knuth' AND given_names LIKE 'Donald%'
Se non viene specificato alcun carattere jolly, LIKE si comporta esattamente come un’uguaglianza semplice.
Si noti che MySQL supporta anche la parola chiave REGEXP, che effettua un confronto tramite espressioni regolar, molto più potenti della
ricerca tramite LIKE. Oracle supporta la funzione REGEXP_LIKE. Ad esempio, con la seguente interrogazione MySQL si ottengono i libri che
nel titolo contengono prima il testo “Object” poi qualsiasi carattere, quindi il testo “Oriented” e infine un testo che può essere o
"Databases" o "Concepts":
SELECT * FROM Book
WHERE title REGEXP 'Object.*Oriented.*(Databases|Concepts)'
In Oracle la sintassi è leggermente diversa:
REGEXP_LIKE(title, 'Object.*Oriented.*(Databases|Concepts)')
Per dettagli sulle espressioni regolari supportate dai database, rimandiamo alle guide ufficiali dei rispettivi database.
Oltre all’operatore AND, le espressioni si possono combinare con l’operatore OR, soddisfatto quando almeno una delle espressioni è vera:
SELECT * FROM Book WHERE pub_year = 1983 OR pub_year = 1993 OR pub_year = 1980
Questo tipo di filtri si può scrivere in maniera più compatta e più comoda con l’operatore IN:
SELECT * FROM Book WHERE pub_year IN (1983, 1993, 1980)
L’operato IN permette di indicare un elenco di possibili valori. Non solo: è possibile specificare anche query secondarie:
SELECT * FROM Book WHERE pub_year IN (SELECT pub_year FROM Article)
Nel combinare espressioni AND e OR, dobbiamo ricordare che AND ha la precedenza nella valutazione. Quindi (come per la maggior parte dei
linguaggi) dobbiamo usare le parentesi per modificare l’ordine degli operatori, altrimenti, ad esempio, questa interrogazione potrebbe dare
risultati inaspettati:
SELECT * FROM Book
WHERE (pub_year = 1983 OR pub_year = 1993) AND publisher = 1
Infatti, eseguendola senza le parentesi, otterremmo anche i libri pubblicati nel 1983, indipendentemente dall’editore, oltre ai libri
pubblicati nel 1993 dall’editore avente ID uguale a 1.
Un altro operatore logico (stavolta unario) importante è il NOT, utilizzato per negare una condizione:
SELECT * FROM Book WHERE NOT(volume = 1)
Contrariamente a quanto si possa pensare, questa query non restituirà né i volumi diversi dal primo, ma neanche i libri aventi il
campo volume impostato a NULL. Questo perché in SQL il valore NULL è speciale nel senso che le espressioni che contengono valori nulli
vengono valutate come NULL, quindi richiedono i seguenti operatori speciali:
- IS NULL
- IS NOT NULL
Possiamo quindi riscrivere la query dell’esempio così:
SELECT * FROM Book WHERE volume IS NULL OR NOT(volume = 1)
Oppure possiamo utilizzare la funzione COALESCE che abbiamo visto nella lezione precedente:
SELECT * FROM Book WHERE NOT(COALESCE(volume, 0) = 1)
Naturalmente, si può testare anche la disuguaglianza, con gli appositi operatori:
disuguaglianza semplice: < >
minore < e minore o uguale: < =
maggiore > e maggiore o uguale: > =
Nel caso dei valori testuali, come CHAR e VARCHAR, l’ordinamento è quello alfabetico in base alla codifica scelta per il database.
L’operatore BETWEEN è comodo per cercare valori in un intervallo, ad esempio di date, ma può essere usato anche con gli altri tipi.
L’intervallo si intende chiuso (con gli estremi); quindi con la seguente interrogazione avremo anche i libri pubblicati nel 1980 e nel 1993:
SELECT title, pub_year
FROM Book
WHERE pub_year BETWEEN 1980 AND 1993
Ordinamento
Per ordinare i risultati di una query si usa la clausola ORDER BY, seguita dalle espressioni da valutare per effettuare l’ordinamento.
Ad esempio:
SELECT title, series, pub_year FROM Book
ORDER BY title
Questa query restituisce i libri pubblicati in ordine di titolo; se li volessimo ordinati prima per anno di pubblicazione e poi per titolo,
basterebbe modificare la clausola in questo modo:
SELECT title, series, pub_year FROM Book
ORDER BY pub_year DESC, title
Abbiamo semplicemente separato i due campi da una virgola. Con la parola chiave DESC abbiamo specificato che vogliamo un ordinamento
discendente, ossia mettendo prima i più recenti. La parola chiave ASC, che indica l’ordinamento ascendente, è opzionale perché corrisponde
ll’ordinamento di default.
Il comportamento di ORDER BY con i valori nulli dipende dal tipo di database utilizzato. Ad esempio MySQL e SQL Server li mettono sempre
in testa, mentre Oracle li mette in coda. Molti database, come Oracle e PostgreSQL hanno le parola chiave NULLS FIRST e NULLS LAST per
indicare come trattare i valori nulli; la seguente query, in Oracle, restituisce prima i libri ordinati per collana, mettendo in testa i libri
che non appartengono ad una collana:
SELECT title, series FROM Book ORDER BY series NULLS FIRST
Considerato che nella clausola di ordinamento possiamo usare tutte le funzioni che abbiamo già visto (non quelle di aggregazione che vedremo
nella prossima lezione), un modo portabile per ottenere lo stesso effetto è di utilizzare la funzione COALESCE. Infatti, anche con MySQL
possiamo ottenere lo stesso effetto scrivendo:
SELECT title, series FROM Book ORDER BY COALESCE(series, '0')
In questo modo i valori nulli verranno considerato come una stringa contenente il solo zero.
Ritorna all'indice
1.06 - GROUP BY e funzioni di aggregazione
Con la parola chiave DISTINCT, nella lezione 10, abbiamo visto la forma più semplice di aggregazione: quella che distingue le righe uguali
riportandole quindi una sola volta. L’esempio ivi riportato è, infatti, riscrivibile utilizzando la clausola GROUP BY; in altre parole,
le seguenti due istruzioni sono equivalenti:
SELECT DISTINCT surname FROM Person
SELECT surname FROM Person GROUP BY surname
Se le eseguiamo nel nostro database di esempio, otteniamo dunque gli stessi risultati. La clausola GROUP BY serve a specificare quali sono i
campi sui cui effettuare i raggruppamenti: il motore di query, per ogni riga esaminerà tali campi e la classificherà nel gruppo corrispondente.
Si possono specificare calcoli da effettuare per ogni gruppo. Ad esempio la query seguente restituisce, per ogni gruppo (surname), il numero di
occorrenze trovate, cioè quante persone hanno quel cognome:
SELECT surname, COUNT(*)
FROM Person
GROUP BY surname
In questo caso il motore raggruppa le righe in base al cognome, e per ogni gruppo effettua il conteggio di quanti elementi sono presenti nel
gruppo.
Da quanto detto, possiamo desumere una naturale limitazione di SQL: se viene specificata una clausola GROUP BY, allora nella clausola SELECT
deve esserci:
- o un campo specificato nella clausola GROUP BY;
- oppure una funzione di aggregazione.
Questo perché quando il motore aggrega le righe deve sapere come comportarsi per ogni campo da restituire.
Funzioni di aggregazione
Le funzioni di aggregazione sono particolari funzioni che operano su più righe. In generale, non tutti i database supportano le stesse
funzioni, per cui è bene riferirsi alle guide ufficiali. Ad esempio MySql fornisce la funzione GROUP_CONCAT, che non è invece supportata su
Oracle.
Le funzioni più comuni sono:
- COUNT per effettuare conteggi nel gruppo;
- SUM per le somme;
- MAX e MIN;
- AVG per calcolare la media.
La funzione COUNT
Questa funzione può essere invocata in tre modi. Il più semplice COUNT(*) l’abbiamo già visto: effettua il conteggio di tutte le righe presenti
nel gruppo, indipendentemente dai valori assunti. Può essere usato anche senza GROUP BY, per calcolare le righe totali presenti in una tabella:
SELECT COUNT(*) FROM Person
Se tra le parentesi specifichiamo un’espressione, verranno contate solo le righe che hanno quell’espressione non nulla. Ad esempio la seguente
query raggruppa i libri per anno di pubblicazione, contando quanti sono e quanti sono stati suddivisi per volume.
SELECT pub_year, COUNT(*), COUNT(volume)
FROM Book
GROUP BY pub_year
Se prima dell’espressione indichiamo la parola chiave DISTINCT verranno conteggiate solo le espressioni non nulle e distinte. In questo caso,
in cui abbiamo due tabelle in join, avremo come risultato il numero di redattori per ogni libro. Avremo vedremo uno 0 nel caso in cui il libro
non ha specificato nessun redattore nel database.
SELECT Book.title, COUNT(DISTINCT personID)
FROM Book
LEFT JOIN Editor ON book.ID = Editor.publicationID
GROUP BY Book.id, Book.title
La funzione SUM
Questa funzione somma i valori dei campi trovati nel gruppo. I valori nulli vengono ignorati, contrariamente a quanto farebbe una somma
semplice.
SELECT Person.surname, SUM(Book.pages)
FROM Book
JOIN Editor ON book.ID = Editor.publicationID
JOIN Person ON Editor.personID = Person.ID
GROUP BY Person.ID, Person.surname
Questa interrogazione restituisce il numero di pagine totale editate da ogni redattore.
Le funzioni MIN, MAX e AVG
Con queste funzioni si possono ottenere i valori massimo e minimo di una colonna, in base al criterio di ordinamento predefinito
(ad esempio per il testo verrà usato un ordinamento alfabetico). I valori nulli vengono ignorati.
Naturalmente più funzioni possono essere usate nella stessa query. La seguente restituisce, per ogni anno di pubblicazione, il primo titolo
in ordine alfabetico, il numero di libri pubblicati, la media del numero di pagine e il numero di pagine del libro più grande:
SELECT Book.pub_year, MIN(title), COUNT(*), AVG(pages), MAX(pages)
FROM Book
GROUP BY pub_year
Filtraggio sul raggruppamento
A differenza di WHERE, che agisce a livello di singola riga, la parola chiave HAVING permette di effettuare un filtraggio sul ragguppamento.
Questa clausola si inserisce subito dopo la GROUP BY.
Il criterio di filtraggio può contenere qualsiasi funzione di raggruppamento. Supponiamo di volere vedere in quali abbiamo pubblicato almeno
100 libri:
SELECT Book.pub_year, COUNT(*), AVG(pages)
FROM Book
GROUP BY pub_year HAVING COUNT(*) > 100
Analisi dei dati con ROLLUP e CUBE
Le direttive ROLLUP e CUBE, restituiscono nel risultato ulteriori righe per rappresentare aggregazioni trasversali, ad esempio per calcolare
subtotali. Vediamo un esempio concreto:
SELECT Book.pub_year, publisher, COUNT(*), SUM(pages)
FROM Book
GROUP BY pub_year, publisher WITH ROLLUP
Se eseguissimo questa query senza WITH ROLLUP otterremmo, per ogni anno e per ogni casa editrice, il numero di libri pubblicati e il totale
delle pagine. Ma se volessimo vedere anche l’aggregazione per anno indipendentemente dall’editore dovremmo effettuare un’altra query.
Utilizzando invece il rollup, possiamo vedere anche tali raggruppamenti trasversali, come vediamo dalle ultime tre righe restituite dalla query:
Come si vede, sono stati fatti anche i raggruppamenti per anno, indipendentemente dall’editore e i totali generali (ultima riga).
Non sono stati fatti invece i raggruppamenti per editore indipendentemente dall’anno. Per avere tutte le possibili combinazioni di raggruppamenti
si utilizza la direttiva WITH CUBE, per ottenere anche righe fatte così:
Ritorna all'indice
1.07 - UPDATE: modificare i dati
Dopo aver visto le modalità per inserire dati nel database e per interrogarlo, passiamo in rassegna le istruzioni per modificarne i dati.
Il comando UPDATE
Il comando UPDATE serve a modificare dati già inseriti nelle tabelle del database. La sua sintassi è la seguente:
UPDATE nome_tabella
SET nomeCampo1 = [,nomeCampoX = ]*
[ WHERE predicato ]
Il significato del comando è quello di modificare i campi indicati nella clausola SET delle righe della tabella nome_tabella che soddisfano il
predicato indicato nella clausola WHERE. Su quest’ultima non ci dilunghiamo perché per essa valgono le stesse considerazioni fatte per il comando
SELECT. Diciamo soltanto che se viene omessa, l’aggiornamento avverrà su tutte le righe della tabella. Di conseguenza bisogna porre particolare
attenzione al momento del lancio di questo comando.
L’utilizzo tipo è la modifica di un singolo record, utilizzando l’id:
UPDATE Person SET given_names = 'Stefano' WHERE ID = 4
Possiamo fare modifiche massive, ad esempio mettendo a 1 il valore del campo volume di tutti i record dove tale campo assume il valore nullo:
UPDATE Book SET volume = 1, note='1 solo volume' WHERE volume IS NULL
In questo esempio vediamo anche come si possono applicare più assegnazioni (sul campo note) nello stesso comando.
L’assegnamento può anche usare altri campi del record da modificare. Ad esempio qui impostiamo in ogni record il campo note alla concatenazione
di un testo con il titolo del libro:
UPDATE Book SET note=CONCAT('titolo: ', title)
L’espressione scalare utilizzata per aggiornare una colonna può anche essere il risultato di una query scalare, cioè una interrogazione che
restituisce una sola riga e una sola colonna:
UPDATE Book SET note= 'editore: ' + (SELECT name FROM Publisher WHERE Publisher.ID = Book.publisher)
In questo caso abbiamo aggiornato il campo note inserendo il nome dell’editore del libro.
Aggiungiamo infine che il comando è atomico, ossia o avviene l’aggiornamento di tutte le righe della tabella, oppure, se si verifica anche un
solo errore su una di esse (ad esempio un valore troppo lungo, oppure un vincolo violato), il comando non verrà eseguito su nessuna di esse
(quindi o successo completo o tutto rimane invariato).
Ritorna all'indice
1.08 - DELETE, eliminare i record dalle tabelle
L’istruzione DELETE permette di eliminare i record di una tabella che soddisfano un certo predicato:
DELETE FROM nome_tabella
[ WHERE predicato ]
Anche in questo caso non ci dilunghiamo sulla clausola WHERE, per la quale vale la considerazione fatta sopra: se viene omessa, l’eliminazione
avverrà su tutte le righe della tabella. Quindi occorre prestare particolare attenzione con la specificazione della clausola, anche per verificare
che si specifichino solo i record che si vogliono eliminare.
Se sussistono delle chiavi esterne sui record che vengono specificati, e se nei vincoli non sono state specificate azioni in caso di cancellazione
con la clausola ON DELETE (come visto nella lezione sulla creazione del database), allora l’annullamento fallirà. Ad esempio questa query fallirà
a causa del riferimento del record nella tabella Book:
DELETE FROM Publisher WHERE ID = 1
Se invece, ad esempio, fosse stata specificata la clausola ON DELETE CASCADE nella definizione della tabella Book, anche i libri che avessero
riferimento a questo editore sarebbero stati cancellati nella stessa transazione.
Ritorna all'indice
1.09 - Subquery
Nelle precedenti lezioni abbiamo visto come eseguire le principali operazione su un database relazionale tramite SQL, descrivendo il funzionamento
e la sintassi delle istruzioni INSERT, SELECT, UPDATE e DELETE.
Alcune situazioni più complesse, inoltre, possono essere gestite sfruttando le cosiddette subquery, cioè inserendo l’istruzione SELECT all’interno
di query che ne contengono già una, o che contengono altre istruzioni comele precedenti.
Nella lezione sull’istruzione UPDATE, abbiamo già visto un esempio di subquery, che riportiamo qui di seguito:
UPDATE Book SET note= 'editore: ' + (SELECT name FROM Publisher WHERE Publisher.ID = Book.publisher)
In questo caso, all’interno di una query di modifica dei dati, abbiamo avuto bisogno di selezionare una serie di record sfruttando un’intera query di
selezione.
Di seguito approfondiremo più nel dettaglio come e quando ha senso l’uso di una subquery, con alcuni esempi pratici di utilizzo.
Operazioni su subquery
Per capire in che modo utilizzare una subquery, è necessario innanzitutto soffermarsi sui possibili valori di ritorno di una query di selezione
(ovvero di una query che utilizza l’istruzione SELECT). Lo faremo sfruttando alcuni esempi, ognuno dei quali utilizza un diverso tipo di subquery,
e che ci porterà a discutere alcuni costrutti interessanti del linguaggio SQL.
La parola chiave IN
Iniziamo con la query seguente:
SELECT *
FROM Clienti
WHERE id IN (SELECT idCliente FROM Incassi WHERE importo > 1000)
La subquery interna ritorna un insieme di id di clienti, e più precisamente quelli per i quali il campo importo supera il valore 1000.
Se osserviamo ora l’intera query, noteremo la parola chiave IN: dal momento che è inserito all’interno di una clausola WHERE, questo costrutto ci
permette di selezionare tutte e sole le righe della tabella Clienti tali che il campo id sia presente tra i risultati della subquery.
Utilizzando NOT IN otterremmo, ovviamente, l’esatto contrario: selezioneremmo tutti quei clienti il cui id NON è presente tra i risultati della subquery.
Le parole chiave SOME, ANY ed ALL
Altre possibilità offerte da SQL sono rappresentate dalle parole chiave SOME, ANY ed ALL. Immaginiamo di volere selezionare una riga della tabella
Incassi solo se la colonna importo è maggiore di almeno uno dei valori ritornati da una subquery. Possiamo implementare questa logica con la parola
chiave SOME (nonchè con la parola chiave ANY, che è in tutto e per tutto equivalente):
SELECT *
FROM Incassi
WHERE importo > SOME (SELECT costo FROM Commesse WHERE tipologia = 'commessaSemplice')
Modificare il funzionamento di un operatore di confronto come > ci permette di verificare se un valore di un campo (in questo caso importo) è maggiore di
almeno uno degli elementi di un insieme di valori (ovvero il risultato della subquery).
Modifichiamo ora la precedente query utilizzando la parola chiave ALL al posto di SOME:
SELECT *
FROM Incassi
WHERE importo > ALL (SELECT costo FROM Commesse WHERE tipologia = 'commessaSemplice')
Con ALL ci assicureremo che il valore di importo sia maggiore di tutti i valori contenuti nell’insieme dei risultati ritornato dalla subquery.
EXISTS e NOT EXISTS
In stretta correlazione con gli operatori IN e NOT IN ci sono anche EXISTS e NOT EXISTS: essi permettono, infatti, di verificare se una subquery ritorna
una o più righe, ovvero se essa produce un risultato vuoto. La parola chiave EXISTS, in particolare, ritornerà un valore booleano pari a TRUE se e solo se
la subquery successiva seleziona almeno una riga. Vediamo un esempio:
SELECT DISTINCT idCitta
FROM Citta
WHERE EXISTS (SELECT * FROM Citta_SquadreCalcio WHERE Citta.idCitta = Citta_SquadreCalcio.idCitta)
La query, come si evince, ritorna gli id delle città per le quali esiste almeno una squadra di calcio all’interno della tabella Citta_SquadreCalcio.
Mentre l’uso di EXISTS è facilmente comprensibile, ciò che può risultare meno intuitivo è il modo in cui la subquery è correlata con la query principale: all’interno della clausola WHERE, infatti, abbiamo utilizzato il campo idCitta della tabella Citta. Ciò implica che la subquery dovrà essere eseguita con un parametro diverso per ogni riga estratta dalla query esterna. Il risvolto pratico è un significato degrado delle prestazioni: in questi casi è sempre meglio preferire soluzioni alternative, considerato anche che spesso le subquery correlate possono essere trasformate in operazioni di JOIN o simili. Lo stesso risultato della query precedente, ad esempio, si poteva ottenere come segue:
SELECT DISTINCT idCitta
FROM Citta, Citta_SquadreCalcio
WHERE Citta.idCitta = Citta_SquadreCalcio.idCitta
Subquery e funzioni di aggregazione
Un altro possibile uso delle subquery è esemplificabile come segue:
SELECT *
FROM Studenti
WHERE voto > (SELECT AVG(voto) FROM Studenti)
In questo caso, e a differenza di quanto visto finora, la subquery non ritorna una serie di risultati, bensì esattamente un valore (in questo caso la
media dei voti di un insieme di studenti). È quindi chiaro in che modo è possibile utilizzare una subquery che sfrutta una funzione di aggregazione
(in questo caso AVG) all’interno di una clausola WHERE.
Subquery all’interno di FROM
Per concludere questa lezione, osserviamo che tutte le subquery viste finora sono state utilizzate, con gli opportuni operatori, all’interno di una clausola WHERE, per lo più in qualità di operandi su operazioni di confronto.
È altresì possibile prevedere l’uso delle subquery all’interno di una clausola FROM, indipendentemente dal fatto che essa faccia parte di una query di selezione (SELECT) piuttosto che di eliminazione (DELETE), inserimento (INSERT) o aggiornamento (UPDATE). Consideriamo l’esempio che segue:
UPDATE Tabella1
SET colonna1 = T.colonna1
FROM (SELECT * FROM Tabella2) AS SubQuery INNER JOIN Tabella1 ON Tabella1.colonnaX = SubQuery.colonnaY
In questa operazione di UPDATE abbiamo effettuato un INNER JOIN tra la Tabella1 ed il risultato di una subquery: il contenuto ritornato dalla subquery è
utilizzato come una vera e propria tabella, all’interno della clausola FROM. L’unica accortezza che diventa necessaria in questi casi è la necessità di
utilizzare la parola chiave AS per dare un nome temporaneo al risultato della subquery (e poterne quindi richiamare i campi all’interno delle clausole
WHERE o per effettuare un JOIN).
Ritorna all'indice
1.10 - Le transazioni
Le singole istruzioni INSERT, UPDATE e DELETE sono atomiche nel senso che o hanno successo totale (su tutte le righe coinvolte) o falliscono totalmente
senza alcun effetto sul database.
È possibile combinare più istruzioni in una singola transazione atomica. Ad esempio, in Oracle o in MySQL:
START TRANSACTION;
DELETE FROM Book WHERE Publisher = 1;
DELETE FROM Publisher WHERE ID = 1
COMMIT;
Con questo script viene eliminato l’editore con ID pari a 1, ma prima vengono eliminati i libri pubblicati da esso. Racchiudendo il blocco tra
START TRANSACTION e COMMIT TRANSACTION, si fa in modo di rendere tutto il blocco atomico: o avvengono con successo entrambe
le istruzioni oppure in caso di errore tutto resta invariato. Con l’istruzione ROLLBACK, invece, si forza il fallimento della transazione,
lasciando il database allo stato consistente.
Livello di isolamento delle transazioni
A proposito delle transazioni, soprattutto se esse sono lunghe, è importante sapere quale livello di isolamento stiamo usando.
Il livello di isolamento stabilisce come transazioni contemporanee si comportano rispetto ai dati. Ogni RDBMS ha un livello di isolamento di default e
generalmente si può stabilire un livello differente per sessione o per transazione. Lo standard ANSI/ISO stabilisce quattro livelli di isolamento,
generalmente implementati da tutti i RDBMS:
1. Serializable. È il massimo livello di isolamento: ogni transazione, dall’inizio alla fine, non vede le modifiche fatte ai dati acceduti.
Il vantaggio è che la transazione può lavorare sul database assumendo di essere la sola transazione in corso sul database. Un modo di realizzare questo
livello è l’approccio ottimistico: ogni transazione lavora in isolamento, poi, se accadono problemi di concorrenza, la transazione che tenta di agire su
un dato modificato da altre transazioni fallirà con un errore e con conseguente rollback. Lo svantaggio è che ci possono essere molte scritture fallite
se ci sono tante transazioni che interessano gli stessi dati.
2. Repeatable Read. Con questo livello si fa in modo che i dati letti durante la transazione in corso non possono essere modificati da altre transazioni
per tutta la durata della transazione in corso. I vari RDBMS gestiscono questo livello utilizzando i lock in lettura sulle righe lette durante la transazione.
L’unico problema che può succedere con questo livello consiste nel verificarsi delle cosiddette letture fantasma: se rieseguo la stessa query durante la
transazione, potrei trovare righe in più di quelle che ho letto in precedenza, ma mai in meno o modificate. Lo svantaggio di questo livello è una penalizzazione
delle prestazioni se ci sono molte transazioni concorrenti che agiscono sulle stesse tabelle. Questo è il livello di default per MySQL.
3. Read Committed. Utilizzando questo livello, invece, si evitano i lock in lettura sulle tabelle che sono molto onerosi dal punto di vista prestazionale.
Lo svantaggio è che, oltre al fenomeno delle letture fantasma, si verifica anche quello delle letture non ripetibili: in pratica, rieseguendo due volte la
stessa SELECT nel corso di una transazione, potrei ottenere dati diversi se altre transazioni sono terminate nel tempo intercorso tra le due letture.
Questo è il livello di default per Oracle e per Microsoft Sql Server.
4. Read Uncommitted. Questo è il livello più basso, in pratica nessun isolamento. Con questo livello si possono avere letture sporche: nella transazione
corrente si possono leggere dati che qualche altra transazione sta scrivendo in quel momento senza aver ancora fatto COMMIT, quindi può capitare di leggere
chiavi violate, dati inconsistenti, eccetera.
Ovviamente non esiste il livello migliore di isolamento, generalmente il livello di default è valido nella maggior parte dei contesti.
Gli RDBMS supportano gli altri livelli per gestire casi particolari di utilizzo in concorrenza.
Ritorna all'indice
1.11 - Trigger
La maggior parte dei database relazionali permette di estendere le funzionalità che abbiamo visto fin qui per aggiungere comportamenti o addirittura nuovi
tipi di dati. In questa lezione approfondiremo proprio questi aspetti.
Trigger
Un trigger è un’azione che deve essere intrapresa quando succede un certo tipo di evento nel database.
La sintassi di creazione di un trigger è definita nello standard ISO, sebbene essa non venga rispettata da tutti i database.
Ad esempio Microsoft SQL Server ha una sintassi abbastanza personalizzata di CREATE TRIGGER, al contrario di MySQL che invece rispetta abbastanza
lo standard. Consideriamo la seguente istruzione:
CREATE TRIGGER ins_month_if_year
BEFORE INSERT ON Book
FOR EACH ROW SET NEW.pub_month = COALESCE(NEW.pub_month, 'JAN');
Ciò comporterà, ad ogni inserimento nella tabella Book, l’impostazione automatica del valore JAN tutte le volte in cui non viene specificato un mese di
pubblicazione.
Diamo un’occhiata alla sintassi: dopo la clausola CREATE TRIGGER viene specificato il nome del trigger da creare. Invece, con BEFORE INSERT ON
indichiamo che vogliamo eseguire un’azione prima dell’inserimento nella tabella Book. Quando viene specificata la parola chiave BEFORE non si dovrebbero
fare modifiche al database, piuttosto si dovrebbe intervenire sui dati che stanno per essere inseriti, altrimenti si rischia di perdere un po’ la visione
di quello che sta succedendo.
Si noti che non tutti i database supportano l’uso della parola chiave BEFORE: su Microsoft Sql Server, ad esempio, si può implementare un comportamento
analogo con il trigger di alternativa all’esecuzione standard (INSTEAD OF).
Con la clausola FOR EACH ROW specifichiamo che l’istruzione che segue sarà eseguita per ogni riga da inserire. Infatti, è possibile inserire più righe
con una sola istruzione INSERT, come abbiamo visto nella lezione 9.
Ovviamente avremmo potuto ottenere lo stesso effetto impostando un valore di default, ma con TRIGGER la forzatura avverrà solamente in fase di inserimento
e, per come abbiamo realizzato il trigger, anche per le INSERT che tentassero di impostare esplicitamente il valore NULL nel campo:
INSERT INTO Book(ID, title, publisher, pub_year, pub_month) values(2, 'year', 1, 2015, NULL);
Ovviamente si può introdurre un trigger analogo anche in aggiornamento, sostituendo BEFORE INSERT ON con BEFORE UPDATE ON.
I trigger di tipo BEFORE sono comodi quando ci si vuole assicurare una certa coerenza tra diversi dati di una stessa riga.
Esempi sono i casi in cui una riga contiene un campo che deve essere la somma di altri due, oppure che non può assumere certi valori in certe condizioni,
che non possono essere espresse con un semplice vincolo di colonna (CHECK).
È possibile fare in modo che il trigger venga eseguito dopo una certa operazione, ad esempio per inizializzare tabelle di relazione, per allineare dati
o ricalcolare dati aggregati. Possiamo anche usare i trigger per loggare le modifiche: nell’esempio che segue, salveremo con un log le modifiche fatte
alla tabella Person, salvandole nella nuova tabella LogPerson.
CREATE TABLE LogPerson(IdPerson int NOT NULL,surname varchar(160) NOT NULL,ts TIMESTAMP);
CREATE TRIGGER log_updates AFTER UPDATE ON Person
FOR EACH ROW INSERT INTO LogPerson(ts, IdPerson, surname)
VALUES(CURRENT_TIMESTAMP, NEW.id, NEW.surname);
Oracle: id auto-incrementali
I trigger sono molto utili in Oracle per ottenere una funzionalità presente in altri database come MySQL (AUTO_INCREMENT) e SQL Server (IDENTITY):
i valori auto-incrementali.
Mentre su MySQL è possibile creare un campo numerico che si incrementa ad ogni inserimento tramite la parola chiave AUTO_INCREMENT, su Oracle la
medesima funzionalità si può ottenere con una sequenza, utilizzandola come descritto nella Guida Oracle ma dentro un trigger:
CREATE SEQUENCE PersonSeq;
CREATE OR REPLACE TRIGGER Person_inc
BEFORE INSERT ON Person
FOR EACH ROW
BEGIN
SELECT PersonSeq.NEXTVAL INTO :NEW.id FROM dual;
END;
Questo trigger, all’inserimento di una persona, calcola il valore successivo della sequenza (NEXTVAL) e lo inserisce nel campo id della riga da inserire.
Ritorna all'indice
1.12 - Funzioni e procedure
Oltre ai trigger, un altro modo per estendere le funzionalità del linguaggio SQL “tradizionale” è la definizione di funzioni e procedure. In questa lezione vedremo come fare.
Funzioni
Nella lezione sul comando SELECT abbiamo visto come si possono usare le funzioni nelle interrogazioni per operare con valori numerici, testuali o booleani.
Quasi tutti i database SQL permettono di estendere l’insieme delle funzioni utilizzabili creandone di nuove.
Ad esempio, per il nostro database, possiamo creare una funzione per verificare se il codice ISBN è corretto.
In Oracle possiamo usare la sintassi seguente, che verifica se la cifra di controllo del codice è valida, restituendo Y in caso affermativo,
o altrimenti il codice di controllo calcolato:
CREATE OR REPLACE FUNCTION CHECK_ISBN ( ISBN IN VARCHAR2 )
RETURN VARCHAR2 IS
ACC INTEGER := 0;
CURR_CHAR VARCHAR2(2);
BEGIN
FOR I IN 1..9 LOOP
CURR_CHAR := SUBSTR(ISBN,I,1);
ACC := ACC + (i) * TO_NUMBER(CURR_CHAR);
END LOOP;
ACC := MOD(ACC, 11);
IF ACC = 10 THEN CURR_CHAR := 'X'; ELSE CURR_CHAR := TO_CHAR(ACC); END IF;
IF CURR_CHAR = SUBSTR(ISBN,LENGTH(ISBN),1) THEN RETURN 'Y'; ELSE RETURN CURR_CHAR; END IF;
END CHECK_ISBN;
Il lavoro viene svolto tra le istruzioni BEGIN ed END: qui viene eseguito un ciclo sui caratteri presenti nel codice e viene calcolata la cifra di controllo.
Nella penultima riga si verifica se l’ultimo carattere del codice ISBN corrisponde al valore calcolato. Questo blocco di codice è stato scritto in PL/SQL,
l’estensione di SQL realizzata per Oracle proprio per la programmabilità: per altri database la sintassi sarebbe stata diversa.
Una volta creata, si può invocare la funzione in una normale istruzione interrogazione:
SELECT title, CHECK_ISBN(isbncode) FROM Books
Procedure
Una procedura, a differenza di una funzione, non può essere usata in una semplice istruzione SELECT, ma può essere invocata con una sintassi ad hoc, ed è usata,
generalmente, per incapsulare del codice che ha qualche effetto collaterale, ad esempio inserire, modificare o eliminare righe dal database.
Per mostrare un esempio, la seguente procedura, scritta per SQL Server (quindi in T-SQL), inserisce simultaneamente oltre al libro, anche il suo autore ed il suo
editore, se non esistono nel databas; altrimenti li collega:
CREATE PROCEDURE insert_book
@title varchar(200),
@author_surname varchar(32),
@author_given_names varchar(32),
@publisher varchar(64)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @idPerson AS INT;
DECLARE @idPublisher AS INT;
DECLARE @idBook AS INT;
SELECT @idPerson = ID From Person
WHERE surname = @author_surname AND given_names = @author_given_names;
IF @@ROWCOUNT = 0 BEGIN
SELECT @idPerson = MAX(ID) + 1 FROM Person;
INSERT INTO Person(ID, surname, given_names)
VALUES(@idPerson, @author_surname, @author_given_names);
END
SELECT @idPublisher = ID From Publisher WHERE name = @publisher;
IF @@ROWCOUNT > 0 BEGIN
SELECT @idPublisher = COALESCE(MAX(ID)+1, 1) FROM Publisher;
INSERT INTO Publisher(ID, name) VALUES(@idPublisher, @publisher);
END
SELECT @idBook = COALESCE(MAX(ID)+1, 1) FROM Publication;
INSERT INTO Publication(ID, type) VALUES(@idBook, 'Book');
INSERT INTO Book(ID, title, publisher, pub_year)
VALUES (@idBook, @title, @idPublisher, YEAR(getdate()));
INSERT INTO Author(personID, publicationID)
VALUES (@idPerson, @idBook);
END
Tipi definiti dall’utente
Oracle (ma anche PostgreSQL) permette di creare tipi definiti dall’utente che incapsulano dati e procedure proprio come nella programmazione ad oggetti.
Si tratta di una funzionalità molto potente perché, oltre a dare maggior chiarezza al database, evita il moltiplicarsi di campi simili in diverse tabelle,
e stesse logiche in varie procedure.
Il seguente esempio in PL/SQL mostra la definizione di un oggetto contenente coordinate di localizzazione e una funzione per calcolare la distanza
(in questo caso per semplicità il calcolo viene effettuato come se si trattasse di coordinate piane):
CREATE OR REPLACE TYPE geoloc AS OBJECT (
latit NUMBER,
longt NUMBER,
MEMBER FUNCTION distanza(altra geoloc) RETURN NUMBER
);
/
CREATE TYPE BODY geoloc IS
MEMBER FUNCTION distanza(altra geoloc) RETURN NUMBER IS
BEGIN
RETURN SQRT(POWER(altra.latit-latit,2)+POWER(altra.longt-longt,2));
END;
END;
Si può notare che con la parola chiave MEMBER si dichiara solo la firma della funzione. Il corpo delle funzioni, invece, va definito a parte all’interno
di un blocco CREATE TYPE BODY. Una volta definito, il tipo si può usare nella creazione delle tabelle, e le sue funzioni possono essere usate nelle interrogazioni,
come in questi esempi:
CREATE TABLE Indirizzi(ID INT, indirizzo VARCHAR2(500), posizione geoloc);
INSERT INTO Indirizzi(ID, indirizzo, posizione)
VALUES(1, 'Lungomare Vanvitelli', geoloc(43.6251419,13.5049253);
SELECT ind.indirizzo FROM Indirizzi ind
WHERE ind.posizione.distanza(geoloc(43.6224239,13.5071353)) < 100;
Ritorna all'indice
1.13 - Modificare la struttura del database
Nel corso della precedente lezione si e’ visto come modificare i dati gia’ presenti nel database.
A volte pero’ non e’ sufficiente modificare i dati, ma occorre aggiornare la struttura stessa del database per far si’ che possano essere rappresentate nuove informazioni.
Dal momento che la struttura del detabase e’ data sostanzialmente dall’insieme delle tabelle che lo compongono, il suo aggiornamento corrisponde all’eliminazione di tabelle
o alla modifica delle loro caratteristiche.
Per eliminare una tabella da un database il comando SQL da utilizzare e’ DROP table:
DROP table nome_tabella { REStrICT | CASCADE }
nome_tabella è il nome della tabella che deve essere eliminata.
Se si specifica la clausola CASCADE vengono automaticamente eliminati i vincoli di integrita’ e le viste (view) in cui la tabella e’ coinvolta. Viceversa, se si specifica
la clausola REStrICT ed esistono dei vincoli di integrita’ o delle viste che si riferiscono alla tabella, l’operazione fallisce.
Ad esempio, si siano definite le seguenti due tabelle:
CREATE table Prova1 (
Id INTEGER PRIMARY KEY,
Nome VARCHAR(50)) CREATE table Prova2 (
Id INTEGER PRIMARY KEY,
Nome VARCHAR(50),
toProva1 INTEGER REFERENCES Prova1(Id))
Volendo eliminare la tabella Prova1, l’istruzione:
DROP table Prova1 REStrICT
fallirà dal momento che esiste un vincolo di integrità che lega una chiave esterna della tabella Prova2 con la tabella Prova1.
Invece l'istruzione:
DROP table Prova1 CASCADE
verrà eseguita con successo e produrrà anche l’eliminazione del vincolo di integrità referenziale presente sulla tabella Prova2.
Nel caso in cui si voglia modificare una tabella esistente nel database, l’istruzione da utilizzare è ALTER table.
Dal momento che la sintassi di questa istruzione è piuttosto complessa, essa verrà spiegata scomponendola in base alle funzionalità che si
vogliono ottenere:
Aggiunta di una nuova colonna nella tabella
ALTER table nome_tabella ADD [ COLUMN ] definizione_colonna
nome_tabella è il nome della tabella che si vuole modificare.
La definizione della colonna segue la stessa sintassi vista nella lezione "Creare il database" nella spiegazione dell’istruzione CREATE table.
Dovranno quindi essere specificati il nome della colonna, il suo tipo ed eventualmente il suo valore di default e i vincoli imposti sulla colonna.
La parola chiave COLUMN può essere omessa (qui e in tutti i casi successivi).
Eliminazione di una colonna nella tabella
ALTER table nome_tabella
DROP [ COLUMN ] nome_colonna { REStrICT | CASCADE }
nome_colonna è il nome della colonna che si vuole eliminare.
Le clausole RESStrIC e CASCADE si comportano esattamente come nell’istruzione DROP table vista precedentemente.
L’istruzione fallirà, oltre che nei casi già visti per REStrICT, se si tenta di eliminare una colonna che è l’unica colonna di una tabella.
Modifica del valore di default di una colonna
ALTER table nome_tabella
ALTER [ COLUMN ] nome_colonna { SET clausola_default | DROP DEFAULT }
La sintassi ed il significato della clausola che definisce il nuovo valore di default sono identici a quelli della clausola_default che si usa nel
comando CREATE table.
Eliminazione di un vincolo della tabella
ALTER table nome_tabella
DROP CONStrAINT nome_vincolo { REStrICT | CASCADE }
Elimina il vincolo identificato dal nome specificato.
L’operazione fallisce se è stata specificata la clausola REStrICT ed esistono altri vincoli che dipendono da quello che si intende eliminare.
Specificando la clausola CASCADE l’operazione verrà sempre completata con successo, cancellando inoltre i vincoli dipendenti da quello eliminato.
Spesso si vuole eliminare un vincolo a cui non è stato assegnato un nome esplicitamente, facendo precedere la definizione del vincolo dalla clausola
opzionale [CONStrAINT nome_vincolo].
In tal caso, dal momento che il DBMS avrà comunque assegnato un nome al vincolo per poterlo identificare, occorrerà interrogare le tabelle di sistema
del DBMS ed ottenere da esse il suo nome. La particolare interrogazione richiesta dipende dallo specifico DBMS utilizzato.
Aggiunta di un vincolo alla tabella
ALTER table nome_colonna
ADD vincolo_di_tabella
La sintassi da utilizzare per la definizione del vincolo è la stessa utilizzata nel comando CREATE table per i vincoli di tabella.
Ritorna all'indice
1.14 - Utilizzo multiutente di un database
Fino ad ora abbiamo esaminato le caratteristiche del linguaggio SQL che riguardano la definizione e la manipolazione dei dati presenti in un database,
senza preoccuparci del fatto che normalmente l’accesso a tali dati avviene in maniera concorrente da parte di piu’ utenti contemporaneamente.
I meccanismi a sostegno di tale metodo di accesso riguardano principalmente la sicurezza dei dati, la gestione delle transazioni e la possibilita’ di
definire delle viste sulle tabelle del database.
1. Sicurezza
L’esecuzione di un operazione sui dati del database da parte di un utente e’ subordinata al possesso da parte dell’utente dei necessari privilegi per la
particolare operazione eseguita sullo specifico insieme di dati.
In generale i privilegi vengono attribuiti nella seguente maniera:
- Un utente che crea una tabella o qualunque altro oggetto del database ne e’ il proprietario e gli vengono automaticamente garantiti tutti i privilegi
applicabili a tale oggetto, con la possibilita’ di impostare anche ad altri utenti tali privilegi (privilegio di concessione).
- Un utente che abbia un privilegio ed abbia in oltre su di esso il privilegio di concessione puo’ assegnare tale privilegio ad un altro utente e passare
ad esso anche il privilegio di concessione.
- I privilegi sono concessi da chi ne abbia il permesso (cioe’ dal proprietario dell’oggetto e da chi abbia il privilegio di concessione) mediante il
comando GRANT e revocati mediante il comando REVOKE.
La sintassi del comando GRANT e’ la seguente:
GRANT elenco_privilegi ON oggetto TO elenco_utenti [ WITH GRANT OPTION ]
Esso assegna all’utente i privilegi presenti nell’elenco_privilegi sull’oggetto specificato.
I privilegi assegnabili sono i seguenti (con le relative sintassi):
USAGE
Privilegio per usare uno specifico dominio o altro oggetto del database.
SELECT
Privilegio per accedere a tutte le colonne di una tabella o di una vista.
INSERT [ (nome_colonna) ]
Se viene specificata l’opzione nome_colonna, e’ il privilegio per inserire valori nella colonna indicata di una tabella o di una vista.
Senza il nome_colonna e’ il privilegio per inserire valori in tutte le colonne, comprese quelle che saranno aggiunte in seguito.
UPDATE [ (nome_colonna) ]
Se viene specificata l’opzione nome_colonna, e’ il privilegio per aggiornare il valore nella colonna indicata di una tabella o di una vista.
In caso contrario permette di aggiornare il valore di tutte le colonne, comprese quelle che saranno aggiunte in seguito.
DELETE
Privilegio per eliminare righe da una tabella o da una vista.
REFERENCES [ (nome_colonna) ]
Se viene specificata l’opzione nome_colonna, e’ il privilegio di riferirsi alla colonna indicata di una tabella o di una vista nella definizione di
un vincolo di integrita’. Senza l’opzione concede tale privilegio per tutte le colonne, comprese quelle aggiunte in seguito.
L’oggetto a cui il privilegio si riferisce e’ generalmente una tabella o una vista. La sintassi per la sua specificazione e’ in tal caso:
[table] nome_tabella
Nel caso di altri oggetti segue la sintassi:
tipo_oggetto nome_oggetto
dove tipo_oggetto puo’ essere DOMAIN, CHARACTER SET, COLLATION o trANSLATION.
Nel caso di oggetti diversi da tabelle o viste, l’unico privilegio applicabile e’ quello di USAGE.
L’elenco_utenti e’ un elenco di identificativi di utenti o gruppi di utenti.
Puo’ anche essere utilizzata la parola chiave PUBLIC, che indica tutti gli utenti e i gruppi conosciuti nel sistema.
Se e’ presente l’opzione [ WITH GRANT OPTION ], viene assegnato inoltre il privilegio di concessione, che permette agli utenti di trasferire ulteriormente
i privilegi loro assegnati.
Ad esempio:
GRANT SELECT, INSERT, UPDATE(nome) ON persona TO benfante WITH GRANT OPTION
assegna all’utente benfante i privilegi di SELECT e INSERT su tutte le colonne della tabella persona e quello di UPDATE sulla sola colonna
nome di tale tabella.
Gli viene inoltre garantito il privilegio di assegnare tali permessi ad altri utenti.
Per togliere i privilegi agli utenti si usa invece REVOKE:
REVOKE [ GRANT OPTION FOR ] elenco_privilegi ON oggetto FROM elenco_utenti { REStrIC | CASCADE }
elenco_privilegi, oggetto ed elenco_utenti hanno le stesso significato delle corrispondenti opzioni di GRANT.
L’opzione GRANT OPTION FOR revoca il privilegio di concessione.
Se viene specificata la clausola REStrICT, il comando REVOKE puo’ fallire nel caso in cui l’utente a cui vengono revocati i privilegi
li abbia ulteriormente concessi ad altri utenti.
Se e’ presente invece la clausola CASCADE, l’istruzione verra’ sempre completata con successo e verranno revocati i privilegi anche di quegli utenti e
di tutti gli utenti a cui essi li hanno concessi (…e cosi’ via, finche’ non ci saranno piu’ permessi “abbandonati”, cioe’ concessi senza che chi li ha concessi
ne sia ancora in possesso). Verranno inoltre distrutti gli oggetti del database costruiti grazie a tali permessi.
2. Gestione delle transazioni
Le transazioni SQL sono insiemi di istruzioni che devono essere trattati come delle unita’ atomiche, cioe’ non scomponibili nelle singole istruzioni da
cui sono formate.
Grazie a tale atomicita’ le transazioni permettono di eseguire operazioni complesse sul database mantenendone l’integrita’. Infatti una transazione viene
eseguita con successo se e solo se tutte le operazioni che la compongono terminano con successo.
In caso contrario, cioe’ se una delle operazioni fallisce o se la transazione viene esplicitamente annullata, tutte le operazioni precedenti vengono
annullate anch’esse.
Le operazioni di una transazione non hanno alcun effetto sul database fino a quando la transazione non viene completata con successo.
Dal momento che ad un database possono accedere piu’ utenti contemporanamente, in ogni istante potremmo avere piu’ transazioni che manipolano il
database in maniera concorrente.
Lo standard SQL prevede che normalmente le transazioni debbano essere eseguite nel “livello di isolamento serializzabile” (isolation level SERIALIZABLE),
cioe’ in una modalita’ di esecuzione che garantisca la “serializzabilita'” delle transazioni. Il fatto che le transazioni siano serializzabili significa
che il loro effetto complessivo sul database e’ quello che si otterrebbe se esse venissero eseguite in maniera non concorrente l’una di seguito all’altra.
Nel linguaggio SQL standard non esiste un’istruzione che faccia iniziare esplicitamente una transazione. Le istruzioni vengono divise in due classi:
quelle che possono iniziare una transazione e quelle che non la fanno iniziare.
Nel momento in cui si cerca di eseguire un’istruzione della prima classe, se non e’ gia’ in corso una transazione, ne viene cominciata una.
La transazione continua fino a quando una delle istruzioni fallisce, causando l’annullamento dell’intera transazione, o se vengono eseguite le istruzioni
COMMIT WORK o ROLLBACK WORK.
L’istruzione COMMIT WORK termina la transazione confermandola, rendendo quindi definitivi gli effetti delle sue istruzioni sul database.
L’istruzione ROLLBACK WORK invece la termina annullandola.
Spesso i DBMS che si trovano in commercio implementano la gestione delle transazioni in maniera differente da quanto previsto dallo standard (almeno nelle
loro impostazioni di default).
In tal caso, di solito e’ previsto un comando che inizia esplicitamente una transazione (BEGIN trANSACTION, START WORK, o altro). Se una transazione non e’
stata iniziata esplicitamente, le singole istruzioni ne compongono una ciascuna.
Per capire meglio quali potrebbero essere le conseguenze della manipolazione concorrente dei dati di un database senza l’utilizzo delle transazioni,
vediamone un’esempio. Supponiamo di avere un database con il quale gestiamo gli ordini dei prodotti che vendiamo. In particolare, quando un cliente ci
sottopone una richiesta per un prodotto, ne verifichiamo la disponibilita’ e nel caso in cui possiamo soddisfare l’ordine, sottraiamo alla quantita’ in
giacenza la quantita’ che ci e’ stata richiesta. traducendo tutto cio’ in SQL, otteniamo la quantita’ in giacenza con l’istruzione (istruzione A):
SELECT giacenza FROM prodotti
WHERE prodottoID=1453
L’aggiornamento della giacenza, una volta verificata la disponibilita’, e’ ottenuta dalla seguente istruzione (istruzione B):
UPDATE prodotti
SET giacenza=giacenza-1
WHERE prodottoID=1453
Se due utenti cercano di eseguire questa operazione, senza che le due istruzioni che la compongono siano state raggruppate in una transazione, potrebbe
accadere che le istruzioni vengano eseguite nell’ordine e con i risultati seguenti :
- Istruzione A eseguita dall’utente 1: viene restituita una giacenza del prodotto pari a 1, quindi l’ordine verra’ approvato.
- Istruzione A eseguita dall’utente 2: come prima la giacenza e’ 1 e anche in questo caso l’ordine verra’ approvato.
- Istruzione B eseguita dall’utente 1: a questo punto nel database la giacenza per il prodotto vale 0.
- Istruzione B eseguita dall’utente 2: ora la giacenza vale -1, che e’ ovviamente un valore errato.
Come si vede il risultato finale e’ che uno dei due clienti non potra’ ricevere (almeno non subito) la merce, dato che non ne avevamo in giacenza una quantita’
sufficiente per entrambi i clienti.
Se le due istruzioni fossero state inserite in una transazione, il problema non sarebbe sorto, dato che la transazione del secondo utente non avrebbe
potuto leggere il valore della giacenza fino a quando non fosse stata completata la transazione del primo utente.
A quel punto, la giacenza avrebbe avuto valore 0 e l’ordine non sarebbe stato erratamente approvato.
3. Viste
Fino ad ora le uniche tabelle con cui abbiamo avuto a che fare sono state quelle definite con il comando CREATE table.
Il linguaggio SQL mette anche a disposizione la possibilita’ di definire delle tabelle “virtuali”, le viste, calcolate a partire da altre tabelle.
Esse sono virtuali nel senso che non occupano spazio su disco, ma sono il risultato di interrogazioni su altre tabelle e quindi sempre allineate
con i valori contenuti in tali tabelle.
L’istruzione SQL per definire una vista e’ la seguente:
CREATE VIEW nome_vista [ ( elenco_nomi_colonne ) ]
AS espressione_tabella
Essa crea una vista chiamata nome_vista definita dall’espressione_tabella.
Tipicamente espressione_tabella e’ un’instruzione select che produrra’ la tabella che interessa. l’elenco_nomi_colonne puo’ essere usata per assegnare
dei nomi alle colonne della vista.
Cio’ e’ utile nel caso in cui le colonne derivanti dall’espressione tabella siano il risultato di un calcolo (ad esempio COUNT(nome_colonna)) e non
abbiano quindi un nome esplicito.
Una volta creata, una vista puo’ essere utilizzata come una normale tabella. Le uniche limitazioni riguardano le operazioni che modificano i dati in
essa contenuti. Infatti, non tutte le viste sono aggiornabili.
Le regole che discriminano fra una vista aggiornabile e una non aggiornabile sono piuttosto complesse e non e’ questa la sede per descriverle
(si vedano i libri in bibliografia, in particolare quello di C.J. Date).
Qui ci limiteremo a cercare di capire, mediante un esempio, perche’ questo accade.
Proviamo ad utilizzare la seguente vista sul nostro database bibliografico:
CREATE VIEW book_publisher89
AS SELECT B.title, P.name
FROM Book B, Publisher P
WHERE B.publisher = P.ID
AND B.pub_year=1989
Essa ci permette di eseguire la query che la definisce semplicemente utilizzando l’istruzione:
SELECT * FROM book_publisher89
Possiamo anche impostare ulteriori condizioni (o fare in modo che il risultato sia ordinato secondo una particolare colonna della vista, ecc…):
SELECT title FROM book_publisher89
WHERE name = “ACM Press”
Quest’ultima interrogazione ci fornisce l’elenco dei titoli dei libri pubblicati dall’ACM Press nel 1989.
Come si vede per quanto riguarda operazioni di interrogazione una vista si comprta come una normale tabella.
Le differenze sorgono quando si cerca di applicare ad una vista delle operazioni di aggiornamento.
Ad esempio, se cerchiamo di eseguire la seguente istruzione:
INSERT INTO book_publisher89
VALUES( “Nuovo libro”, “publisher”)
Il DBMS non riuscira’ ad eseguirla, restituendo un errore tipo “No INSERT permission”.
Il motivo e’ che non e’ in grado di creare le righe corrispondendi al nostro nuovo record nelle due tabelle “reali” da cui la vista e’ originata
(i problemi sono vari: deve creare solo una righa nella tabella Book e collegarla ad una particolare riga nella tabella Publisher, o creare una riga
in entrambe le tabelle; come decidere quali valori assegnare alle chiavi primarie degli eventuali nuovi record; quali valori assegnare agli altri campi
delle due tabelle; ecc…)
Grazie alle viste (e all’assegnazione oculata dei permessi agli utenti) e’ possibile fare in modo che utenti diversi abbiano una percezione della
struttura del database anche molto diversa da quella che ha realmente e impedire che particolari categorie di utenti possano accedere ad informazioni
che non competono loro.
Ad esempio, supponiamo di avere una tabella in cui sono memorizzati i dati anagrafici dei dipendenti di una ditta e l’ammontare del loro stipendio
mensile.
Ovviamente si vorrebbe impedire la consultazione dei dati relativi agli stipendi a tutti gli utenti, eccetto a quelli che si devono occupare
dell’erogazione/amministrazione degli stessi.
Un sistema per fare cio’ e’ quello di definire una vista contenente solo le colonne dei dati anagrafici. In questo modo tutti gli utenti autorizzati ad
accedere ai dati anagrafici, ma non a quelli degli stipendi, lo potranno fare solo attraverso tale vista. Ulteriori partizionamenti potrebbero essere
fatti in senso orizzontale, creando ad esempio un vista che contiene solo le informazioni sui dirigenti ed una che contiene i dati degli altri dipendenti.
Inoltre, le viste spesso contribuiscono a facilitare quella indipendenza fra applicazioni e struttura dei dati, che rende i database degli strumenti tanto utili.
Infatti se ad un certo punto fosse necessario cambiare la struttura del database (scomponendo, ad esempio, una tabella in due tabelle per motivi di efficienza),
non sarebbe necessario modificare tutte le applicazioni adattandole alla nuova struttura, ma sarebbe sufficiente creare delle opportune view, in modo che dal
punto di vista delle applicazioni niente sia stato cambiato.
|