Upload
others
View
10
Download
0
Embed Size (px)
Citation preview
Pagina 1
A. Veneziani – SQL funzionalità “avanzate”
Consideriamo per i test e gli esempi delle query e delle regole presentate a seguire useremo il DB
“Acquisti”, definito dallo schema:
E contenente i dati:
mysql> SELECT * FROM Utenti;
+-----------+--------+---------+--------+--------+
| Id_Utente | Nome | Cognome | Login | Passwd |
+-----------+--------+---------+--------+--------+
| 1 | Mario | Rossini | rossi | red |
| 2 | Carlo | Verdi | verdi | green |
| 3 | Gianni | Gialli | gialli | yellow |
+-----------+--------+---------+--------+--------+
mysql> SELECT * FROM Accessi;
+------------+---------------------+-----------+
| Id_Accesso | Momento | Id_Utente |
+------------+---------------------+-----------+
| 1 | 2018-03-26 22:15:42 | 1 |
| 2 | 2018-03-26 22:16:07 | 1 |
| 3 | 2018-03-26 22:18:20 | 1 |
| 4 | 2018-03-26 22:19:13 | 1 |
| 5 | 2018-03-26 22:20:55 | 1 |
| 6 | 2018-03-26 22:22:04 | 1 |
| 7 | 2018-03-26 22:23:07 | 1 |
| 8 | 2018-03-26 22:27:28 | 2 |
| 9 | 2018-03-26 22:38:50 | 2 |
| 10 | 2018-03-26 22:39:10 | 2 |
| 11 | 2018-03-26 22:41:09 | 2 |
| 12 | 2018-03-26 22:44:07 | 2 |
| 13 | 2018-03-26 22:44:39 | 2 |
+------------+---------------------+-----------+
Pagina 2
mysql> SELECT * FROM Acquisti;
+------------+-------------+----------+
| Id_Accesso | Id_Prodotto | Quantita |
+------------+-------------+----------+
| 1 | 2 | 1 |
| 2 | 1 | 2 |
| 7 | 2 | 2 |
| 7 | 4 | 2 |
| 8 | 3 | 2 |
| 9 | 3 | 2 |
| 10 | 2 | 1 |
| 11 | 3 | 2 |
| 12 | 3 | 2 |
| 12 | 4 | 3 |
| 13 | 3 | 2 |
| 13 | 4 | 3 |
+------------+-------------+----------+
mysql> SELECT * FROM prodotti;
+-------------+---------------------+--------+----------+
| Id_Prodotto | NomeProdotto | Prezzo | Quantita |
+-------------+---------------------+--------+----------+
| 1 | Guanti | 3.00 | 13 |
| 2 | Scopa | 5.50 | 8 |
| 3 | 6 bicchieri | 2.50 | 16 |
| 4 | Straccio microfibre | 3.00 | 12 |
+-------------+---------------------+--------+----------+
Funzioni di aggregazione
Le funzioni di aggregazione hanno questo nome perchè appunto aggregano i dati presenti su più record in
un dato complessivo. Tali funzioni risolvono diverse problematiche:
COUNT(...) – conteggio del numero di record risultanti da una certa query
MAX(...) e MIN(...) – individuazione del valore massimo o minimo presente in una certa serie di dati
in un certo campo
AVG(...) – individuazione del valore medio dedotto in base ad una certa serie di dati in un certo
campo
SUM(....) - somma dei valori di un certo campo
Il modo più semplice per utilizzare tali funzioni di aggregazione è quello di farle operare su un solo gruppo
di elementi.
Ad esempio se volessimo il numero di accessi effettuati nel sistema del DB “Acquisti”1: SELECT COUNT(*) As NumeroAccessi FROM Accessi +---------------+
| NumeroAccessi |
+---------------+
| 13 |
+---------------+
Ovviamente tale conteggio si riferisce ad un solo gruppo. Un problema da esso derivato potrebbe essere
trovare gli accessi dopo le 22:40 del giorno 26/3/2018: SELECT COUNT(*) As NumeroAccessi FROM Accessi WHERE Momento > '2018-03-26 22:40' +---------------+
1 Se la tabella Accessi fosse vuota il numero di accessi contato sarebbe 0.
Pagina 3
| NumeroAccessi |
+---------------+
| 3 |
+---------------+
Il conteggio 3, deriva dai dati che soddisfano la condizione posta, visibili con:
SELECT * FROM Accessi WHERE Momento > '2018-03-26 22:40' +------------+---------------------+-----------+ | Id_Accesso | Momento | Id_Utente | +------------+---------------------+-----------+ | 11 | 2018-03-26 22:41:09 | 2 | | 12 | 2018-03-26 22:44:07 | 2 | | 13 | 2018-03-26 22:44:39 | 2 | +------------+---------------------+-----------+
E così con lo stesso significato potremo aggiungere ulteriori condizioni e contare quante tuple soddisfino le
condizioni poste.
Nel caso alcun record soddisfi la condizione posta, allora il conteggio prodotto dalla COUNT sarà 0:
SELECT COUNT(*) As NumeroAccessi FROM Accessi WHERE Momento > '2018-03-26 23:00' +---------------+
| NumeroAccessi |
+---------------+
| 0 |
+---------------+
Con logica analoga possiamo utilizzare le altre funzioni per ricerche e calcoli su un solo insieme di dati.
Da notare che la funzione COUNT può essere applicata anche su una singola colonna di una tabella. In quel
caso il conteggio conta i valori della colonna che sono diversi da NULL, e non conta invece i NULL presenti
nella colonna stessa.
Ad esempio poniamoci il problema di determinare a quando risalga l’accesso più recente2: SELECT MAX(Momento) FROM Accessi
+---------------------+
| PiuRecente |
+---------------------+
| 2018-03-26 22:44:39 |
+---------------------+
In questo caso il problema di quale sia l’accesso più recente, può essere risolto tramite la query con
subquery: SELECT * FROM Accessi WHERE Momento = (SELECT MAX(Momento) As PiuRecente FROM Accessi) +------------+---------------------+-----------+
| Id_Accesso | Momento | Id_Utente |
+------------+---------------------+-----------+
| 13 | 2018-03-26 22:44:39 | 2 |
+------------+---------------------+-----------+
Il principio di questa query si basa sull’uguagliare il valore del campo Momento (di accesso) al valore
massimo individuato nella colonna Momento, individuato con la opportuna subquery.
E’ possibile uguagliare (o comunque confrontare) i due valori essendo entrambi di tipo datetime.
Un altro problema che può coinvolgere funzioni di aggregazione è il calcolo del valore complessivo dei
prodotti a magazzino. Sulla tabella prodotti potremo allora effettuare la query: SELECT SUM(Quantita * Prezzo) As ValoreTotale FROM Prodotti +--------------+
| ValoreTotale |
2 Se la tabella Accessi fosse vuota il valore ritornato dalla funzione MAX(...) (ed analogamente dalla funzione MIN) sarebbe NULL.
Pagina 4
+--------------+
| 165.50 |
+--------------+
Ovviamente anche su questo calcolo potrebbero essere fatte delle varianti che selezionino il valore
complessivo solo di alcuni prodotti (ad esempio di tutti quelli con prezzo maggiore di 3.00 euro)3: SELECT SUM(Quantita * Prezzo) As ValoreTotale FROM Prodotti WHERE Prezzo > 3.00 +--------------+
| ValoreTotale |
+--------------+
| 89.50 |
+--------------+
Un altra problematica banale che può coinvolgere una funzione di aggregazione applicata su un solo gruppo
di dati è determinare la media del prezzo dei prodotti4: SELECT AVG(Prezzo) As PrezzoMedio FROM Prodotti
+-------------+
| PrezzoMedio |
+-------------+
| 3.625000 |
+-------------+
Un possibile problema derivabile da quello che ci siamo posti sopra è quali prodotti siano sopra il prezzo
medio: SELECT * FROM Prodotti WHERE Prezzo > (SELECT AVG(Prezzo) As PrezzoMedio FROM Prodotti) +-------------+--------------+--------+----------+
| Id_Prodotto | NomeProdotto | Prezzo | Quantita |
+-------------+--------------+--------+----------+
| 2 | Scopa | 5.50 | 8 |
+-------------+--------------+--------+----------+
Anche questo problema è risolubile, come si vede, tramite una query con subquery.
Si ricordi che nel caso particolare di operazioni su una tabella senza record (vuota), la funzione COUNT
rende 0, le funzioni MIN, MAX, SUM e AVG rendono il valore NULL.
Nel caso sulla colonna sulla quale è applicata una di queste funzioni ci siano valori NULL (ovviamente
perchè il campo stesso ammette tale valore), il valore NULL, viene ignorato, ossia i calcoli si svolgono come
se tale valore non esistesse. Ciò inclusa la media (AVG) per la quale l’elemento NULL non viene
conteggiato, ossia viene ugualmente ignorato.
Funzioni di aggregazione operanti su più gruppi
In molti casi le funzioni di aggregazione non aggregano dati in base ad un unico raggruppamento (come
negli esempi precedenti), ma in base a più raggruppamenti. Si deve in tal caso definire un criterio di
raggruppamento tramite la specifica clausola GROUP BY....
Un esempio di una applicazione del GROUP BY potrebbe essere contare gli accessi effettuati da ogni utente.
E’ ovvio che il problema impone di effettuare il conteggio dei dati derivati dal join tenendo conto dell’ id
dell’utente o altra sua caratteristica (univoca).
Il problema è risolto da questa query: SELECT Nome, Cognome, COUNT(*) As NumeroAccessi FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente
3 Se la tabella Prodotti fosse vuota, la somma SUM(...) applicata alla formula indicata renderebbe NULL. 4 Se la tabella Prodotti fosse vuota il valore della media (AVG) sarà NULL.
Pagina 5
GROUP BY Accessi.Id_Utente +-------+---------+---------------+ | Nome | Cognome | NumeroAccessi | +-------+---------+---------------+ | Mario | Rossini | 7 | | Carlo | Verdi | 6 | +-------+---------+---------------+
Come si vede tramite un opportuno uso della clausola GROUP BY è stato possibile contare gli accessi
effettuati da ogni utente.
In realtà è possibile inserire condizioni che condizionino i dati sottoposti a conteggio, ad esempio considero
solo gli accessi precedenti a 26/3/2018 22:40, e quindi nel nostro caso il risultato diviene:
SELECT Nome, Cognome, COUNT(*) As NumeroAccessi FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Momento < '2018-03-26 22:40' GROUP BY Accessi.Id_Utente +-------+---------+---------------+
| Nome | Cognome | NumeroAccessi |
+-------+---------+---------------+
| Mario | Rossini | 7 |
| Carlo | Verdi | 3 |
+-------+---------+---------------+
Ossia gli accessi dell’utente Verdi, data la nuova condizione da rispettare sono ora in numero minore.
Tuttavia è possibile inserire non solo condizioni sui dati non aggregati (come quella precedente), ma anche
condizioni su dati a loro volta aggregati. Ciò si può fare con la clausola HAVING che seguirà la GROUP BY:
SELECT Nome, Cognome, COUNT(*) As NumeroAccessi FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Momento < '2018-03-26 22:40' GROUP BY Accessi.Id_Utente HAVING NumeroAccessi > 5; +-------+---------+---------------+ | Nome | Cognome | NumeroAccessi | +-------+---------+---------------+ | Mario | Rossini | 7 | +-------+---------+---------------+
In questo caso risultano solo gli accessi di Rossini, in quanto la clausola HAVING (riguardante un dato
anch’esso oggetto di aggregazione, in questo caso il conteggio del numero di accessi) impone che siano
rilevati solo quelli con conteggio maggiore di 5 (quindi il precedente record con conteggio 3 è escluso dalla
tabella).
Ovviamente è possibile impostare condizioni relative alle clausole HAVING anche su dati che non sono
quelli oggetto dell’aggregazione effettuata nella query stessa. Ad esempio ci si ponga il semplice problema
di contare i prodotti il cui prezzo sia superiore alla media Ad esempio pensiamo se si volessero sapere il
numero di prodotti diversi acquistati da ogni utente.
La relativa query è in questo caso piuttosto complessa: SELECT Nome, Cognome, Id_Prodotto, Quantita FROM Utenti, Accessi, Acquisti WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Accessi.Id_Accesso = Acquisti.Id_Accesso
Rappresenta quante operazioni di acquisto sono state effettuate (e su quale prodotto): +-------+---------+-------------+----------+
| Nome | Cognome | Id_Prodotto | Quantita |
+-------+---------+-------------+----------+
| Mario | Rossini | 2 | 1 |
Pagina 6
| Mario | Rossini | 1 | 2 |
| Mario | Rossini | 2 | 2 |
| Mario | Rossini | 4 | 2 |
| Carlo | Verdi | 3 | 2 |
| Carlo | Verdi | 3 | 2 |
| Carlo | Verdi | 2 | 1 |
| Carlo | Verdi | 3 | 2 |
| Carlo | Verdi | 3 | 2 |
| Carlo | Verdi | 4 | 3 |
| Carlo | Verdi | 3 | 2 |
| Carlo | Verdi | 4 | 3 |
+-------+---------+-------------+----------+
In realtà i prodotti acquistati sono però ripetuti in vari casi e quindi il conteggio deve essere fatto
distinguendo i prodotti con id uguale:
SELECT Nome, Cognome, COUNT(DISTINCT Id_Prodotto) As NumeroProdotti FROM Utenti, Accessi, Acquisti WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Accessi.Id_Accesso = Acquisti.Id_Accesso GROUP BY Utenti.Id_Utente +-------+---------+----------------+
| Nome | Cognome | NumeroProdotti |
+-------+---------+----------------+
| Mario | Rossini | 3 |
| Carlo | Verdi | 3 |
+-------+---------+----------------+
E la query sopra rende il risultato corretto del numero di prodotti diversi acquistati (senza ripetizioni) da
ciascun utente.
Immaginiamo ora di rendere ancor più complessa la query considerando di questi solo i raggruppamenti di
prodotti che hanno quantità media acquistata per utente maggiore di 2.
In realtà possiamo oltre al numero di prodotti acquistati calcolare la quantità media acquistata nelle
operazioni di acquisto per ogni utente. La seguente query mostra anche questo dato (aggregato):
SELECT Nome, Cognome, COUNT(DISTINCT Id_Prodotto) As NumeroProdotti, AVG(Quantita) FROM Utenti, Accessi, Acquisti WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Accessi.Id_Accesso = Acquisti.Id_Accesso GROUP BY Utenti.Id_Utente +-------+---------+----------------+---------------+ | Nome | Cognome | NumeroProdotti | AVG(Quantita) | +-------+---------+----------------+---------------+ | Mario | Rossini | 3 | 1.7500 | | Carlo | Verdi | 3 | 2.1250 | +-------+---------+----------------+---------------+
Una ulteriore condizione potrebbe coinvolgere questo dato aggregato, imponendo appunto che la quantità
media acquistata per utente maggiore di 2. Questo problema sarà risolto dalla query: SELECT Nome, Cognome, COUNT(DISTINCT Id_Prodotto) As NumeroProdotti, AVG(Quantita) FROM Utenti, Accessi, Acquisti WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Accessi.Id_Accesso = Acquisti.Id_Accesso GROUP BY Utenti.Id_Utente HAVING AVG(Quantita) > 2 +-------+---------+----------------+---------------+
| Nome | Cognome | NumeroProdotti | AVG(Quantita) |
+-------+---------+----------------+---------------+
| Carlo | Verdi | 3 | 2.1250 |
Pagina 7
+-------+---------+----------------+---------------+
Anche in questo caso la condizione essendo su un dato aggregato deve essere inserita nella apposita
clausola HAVING (e non nella parte WHERE).
Un altro problema di aggregazione effettuato su gruppi, potrebbe essere quello di trovare la spesa
effettuata per ogni accesso: SELECT Accessi.*, SUM(Acquisti.Quantita * Prezzo) As SpesaPerAccesso FROM Accessi, Acquisti, Prodotti WHERE Accessi.Id_Accesso = Acquisti.Id_Accesso AND Acquisti.Id_Prodotto = Prodotti.Id_Prodotto GROUP BY Accessi.Id_Accesso; +------------+---------------------+-----------+-----------------+ | Id_Accesso | Momento | Id_Utente | SpesaPerAccesso | +------------+---------------------+-----------+-----------------+ | 1 | 2018-03-26 22:15:42 | 1 | 5.50 | | 2 | 2018-03-26 22:16:07 | 1 | 7.00 | | 7 | 2018-03-26 22:23:07 | 1 | 17.00 | | 8 | 2018-03-26 22:27:28 | 2 | 5.00 | | 9 | 2018-03-26 22:38:50 | 2 | 5.00 | | 10 | 2018-03-26 22:39:10 | 2 | 5.50 | | 11 | 2018-03-26 22:41:09 | 2 | 5.00 | | 12 | 2018-03-26 22:44:07 | 2 | 14.00 | | 13 | 2018-03-26 22:44:39 | 2 | 14.00 | +------------+---------------------+-----------+-----------------+
E se si volesse sapere per queste spese per ogni accesso quali di esse sono superiori ai 10 euro, si dovrebbe
far di nuovo ricorso alla clausola aggiuntiva HAVING: SELECT Accessi.*, SUM(Acquisti.Quantita * Prezzo) As SpesaPerAccesso FROM Accessi, Acquisti, Prodotti WHERE Accessi.Id_Accesso = Acquisti.Id_Accesso AND Acquisti.Id_Prodotto = Prodotti.Id_Prodotto GROUP BY Accessi.Id_Accesso HAVING SpesaPerAccesso > 10; +------------+---------------------+-----------+-----------------+ | Id_Accesso | Momento | Id_Utente | SpesaPerAccesso | +------------+---------------------+-----------+-----------------+ | 7 | 2018-03-26 22:23:07 | 1 | 17.00 | | 12 | 2018-03-26 22:44:07 | 2 | 14.00 | | 13 | 2018-03-26 22:44:39 | 2 | 14.00 | +------------+---------------------+-----------+-----------------+
Una ulteriore applicazione di funzioni di aggregazione potrebbe essere numero di pezzi venduti per ciascun
prodotto:
La query mette in evidenza le quantità vendute: SELECT NomeProdotto, Acquisti.Quantita FROM Acquisti, Prodotti WHERE Acquisti.Id_Prodotto = Prodotti.Id_Prodotto +---------------------+----------+
| NomeProdotto | Quantita |
+---------------------+----------+
| Guanti | 2 |
| Scopa | 1 |
| Scopa | 2 |
| Scopa | 1 |
| 6 bicchieri | 2 |
| 6 bicchieri | 2 |
| 6 bicchieri | 2 |
| 6 bicchieri | 2 |
| 6 bicchieri | 2 |
Pagina 8
| Straccio microfibre | 2 |
| Straccio microfibre | 3 |
| Straccio microfibre | 3 |
+---------------------+----------+
Da questa si può subito dedurre la query che ci permette invece di calcolare le quantità vendute raggruppate per prodotto:
SELECT NomeProdotto, SUM(Acquisti.Quantita) As PezziVenduti FROM Acquisti, Prodotti WHERE Acquisti.Id_Prodotto = Prodotti.Id_Prodotto GROUP BY Prodotti.Id_Prodotto; +---------------------+--------------+
| NomeProdotto | PezziVenduti |
+---------------------+--------------+
| Guanti | 2 |
| Scopa | 4 |
| 6 bicchieri | 10 |
| Straccio microfibre | 8 |
+---------------------+--------------+
Inoltre c’è da tener conto che è possibile effettuare raggruppamenti multipli.
Ad esempio si supponga di voler sapere quanti acquisti (operazioni di acquisto) sono stati effettuati con un
certa quantita di prodotto. Inquesto caso raggrupperemo per Quantita: # numero di acquisti raggruppati per quantita
SELECT Quantita, COUNT(*) As NumeroAcquisti FROM Acquisti GROUP BY Quantita
Che avrà il risultato: +----------+----------------+
| Quantita | NumeroAcquisti |
+----------+----------------+
| 1 | 2 |
| 2 | 8 |
| 3 | 2 |
+----------+----------------+
Ossia leggiamo che sono stati fatti 2 operazioni di acquisto con quantita di prodotto acquistata pari a 1, 8
con quantità pari a 2, e 2 operazioni di acquisto con quantita pari a 3. Il criterio con cui sono stati
raggruppati questi dati è appunto la quantita di prodotto acquistato in ogni operazione di acquisto.
Pensiamo ora di voler raggruppare sia per la quantita acquistata, sia per l’id del prodotto oggetto di
acquisto. In questo caso i criteri di raggruppamento non sono più uno solo, ma due. In pratica in questo
caso il gruppo relativo agli acquisti di una quantita 1, si scomporrà in vari sottogruppi di acquisti con
quantita 1, coinvolgenti id prodotto diversi.
Risulterà la query:
# numero di acquisti raggruppati per quantita ed id del prodotto # (raggruppa per quantita, ma separatamente per ogni prodotto) SELECT Quantita, Id_Prodotto, COUNT(*) As NumeroAcquisti FROM Acquisti GROUP BY Quantita, Id_Prodotto +----------+-------------+----------------+
| Quantita | Id_Prodotto | NumeroAcquisti |
+----------+-------------+----------------+
| 1 | 2 | 2 |
| 2 | 1 | 1 |
| 2 | 2 | 1 |
| 2 | 3 | 5 |
| 2 | 4 | 1 |
Pagina 9
| 3 | 4 | 2 |
+----------+-------------+----------------+
Che indica i dati precedenti ma raggruppati in modo diverso, dato il sottoraggruppamento per id del
prodotto. I prodotti acquistati in quantita 1 riguardano il solo prodotto2, e sono numericamente 2.
I prodotti acquistati in quantita 2 (per ogni operazione di acquisto) sono ora scorporati in diverse sottovoci,
date dagli id dei veri prodotti coinvolti in tali acquisti (prodotti di id 1, 2, 3, 4), con numero di acquisti
diversi in numero a seconda dei casi. Infine i prodotti venduti con quantita 3 sono sempre uno solo (id = 4),
e il numero di acquisti di questo tipo è 2. E’ bene notare che la somma di tutti i numeri di acquisti è anche
in questo caso 12, come nel caso precedente, ma i raggruppamenti multipli hanno portato ad avere una
differente suddivisione nel numero di acquisti.
Ordinamenti
I dati risultato di una query possono anche essere ordinati, qualunque operazione sia stata effettuata
precedentemente. Bisogna scegliere ovviamente un criterio di ordinamento, legato ad una o più colonne
coinvolte nella query.
Un semplice esempio può essere ordinare i nostri prodotti in ordine alfabetico: SELECT * FROM Prodotti ORDER BY NomeProdotto; +-------------+---------------------+--------+----------+
| Id_Prodotto | NomeProdotto | Prezzo | Quantita |
+-------------+---------------------+--------+----------+
| 3 | 6 bicchieri | 2.50 | 16 |
| 1 | Guanti | 3.50 | 13 |
| 2 | Scopa | 5.50 | 8 |
| 4 | Straccio microfibre | 3.00 | 12 |
+-------------+---------------------+--------+----------+
La clausola ASC è quella di default ed infatti il nostro ordinamento è alfabetico ascendente. Per ordinare in
modo inverso basta indicare DESC alla fine della riga ORDER BY; ad esempio nell’ esempio precedente: SELECT * FROM Prodotti ORDER BY NomeProdotto DESC;
Nella tabella Prodotti vi sono diverse colonne che possono essere oggetto di ordinamento. Supponiamo di voler affiancare al primo ordinamento un secondo e supponiamo di modificare il dato di prezzo 3.50 in 3.00. La query:
SELECT * FROM Prodotti ORDER BY Prezzo, NomeProdotto
Ordina in base al nome del prezzo e come secondo criterio di ordinamento in base al nome del prodotto. Quindi se il prezzo risulta uguale a quel punto il nome del prodotto sarà un ulteriore criterio di ordinamento su cui basarsi.
+-------------+---------------------+--------+----------+ | Id_Prodotto | NomeProdotto | Prezzo | Quantita | +-------------+---------------------+--------+----------+ | 3 | 6 bicchieri | 2.50 | 16 | | 1 | Guanti | 3.00 | 13 | | 4 | Straccio microfibre | 3.00 | 12 | | 2 | Scopa | 5.50 | 8 | +-------------+---------------------+--------+----------+
Se si inverte il criterio si inverte il risultato: SELECT * FROM Prodotti ORDER BY Prezzo ASC , NomeProdotto DESC
Pagina 10
+-------------+---------------------+--------+----------+ | Id_Prodotto | NomeProdotto | Prezzo | Quantita | +-------------+---------------------+--------+----------+ | 3 | 6 bicchieri | 2.50 | 16 | | 4 | Straccio microfibre | 3.00 | 12 | | 1 | Guanti | 3.00 | 13 | | 2 | Scopa | 5.50 | 8 | +-------------+---------------------+--------+----------+
Ovviamente si possono sottoporre ad ordinamento anche dati aggregati, ad esempio ordinare i prodotti in
base ai pezzi venduti per ciscuno di essi; riprendendo una query precedente: SELECT NomeProdotto, SUM(Acquisti.Quantita) As PezziVenduti FROM Acquisti, Prodotti WHERE Acquisti.Id_Prodotto = Prodotti.Id_Prodotto GROUP BY Prodotti.Id_Prodotto ORDER BY PezziVenduti DESC; +---------------------+--------------+
| NomeProdotto | PezziVenduti |
+---------------------+--------------+
| 6 bicchieri | 10 |
| Straccio microfibre | 8 |
| Scopa | 4 |
| Guanti | 2 |
+---------------------+--------------+
La clausola DISTINCT
Supponiamo di porci il (semplice) problema di capire quali quantità siano state acquistate nelle varie
operazioni di acquisto effettuate. Il dato è deducibile in assoluto dalla colonna Quantita della tabella
Acquisti, ma ovviamente è disaggregato. Una soluzione possibile è effettuare un raggruppamento, senza
l’uso, in questo caso non necessario, di funzioni di aggregazione: SELECT Quantita FROM Acquisti GROUP BY Quantita +----------+
| Quantita |
+----------+
| 1 |
| 2 |
| 3 |
+----------+
Abbiamo infatti ottenuto le quantita oggetto di acquisti di singoli prodotti.
In alternativa basterebbe fare una comune selezione sulla colonna, forzando i valori risultanti ad essere
distinti. Ciò può essere fatto con la clausola DISTINCT, posta a seguire al comando SELECT:
SELECT DISTINCT Quantita FROM Acquisti +----------+
| Quantita |
+----------+
| 1 |
| 2 |
| 3 |
+----------+
E come si vede il risultato è lo stesso.
Pagina 11
Un altro problema che potrebbe chiamare in causa la clausola DISTINCT è la questione: individuare quali
utenti abbiano effettuato accessi. In assoluto gli utente che abbiano effettuato accessi sono elencati
tramite la query: SELECT Utenti.* FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente
Ma tale query ha il grosso difetto di elencarli, in generale, in modo ripetuto. +-----------+-------+---------+-------+--------+
| Id_Utente | Nome | Cognome | Login | Passwd |
+-----------+-------+---------+-------+--------+
| 1 | Mario | Rossini | rossi | red |
| 1 | Mario | Rossini | rossi | red |
| 1 | Mario | Rossini | rossi | red |
| 1 | Mario | Rossini | rossi | red |
| 1 | Mario | Rossini | rossi | red |
| 1 | Mario | Rossini | rossi | red |
| 1 | Mario | Rossini | rossi | red |
| 2 | Carlo | Verdi | verdi | green |
| 2 | Carlo | Verdi | verdi | green |
| 2 | Carlo | Verdi | verdi | green |
| 2 | Carlo | Verdi | verdi | green |
| 2 | Carlo | Verdi | verdi | green |
| 2 | Carlo | Verdi | verdi | green |
+-----------+-------+---------+-------+--------+
Per avere dati maggiormente utilizzabili e chiari, è opportuno a questo punto eliminare i duplicati dei dati
ricavati. Ciò è possibile con la clausola DISTINCT:
SELECT DISTINCT Utenti.* FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente +-----------+-------+---------+-------+--------+
| Id_Utente | Nome | Cognome | Login | Passwd |
+-----------+-------+---------+-------+--------+
| 1 | Mario | Rossini | rossi | red |
| 2 | Carlo | Verdi | verdi | green |
+-----------+-------+---------+-------+--------+
Come si vede ora i dati duplicati sono del tutto spariti e restano ripetuti una sola volta gli utenti che hanno
acceduto.
Poniamoci adesso il problema di contare quanti utenti hanno acceduto al sistema di acquisto.
Una possibilità data la query precedente che già da dei dati utili è utilizzarla a sua volta come tabella da cui
dedurre la risposta alla richiesta di cui sopra:
SELECT COUNT(*) As NumeroUtenti FROM (SELECT DISTINCT Utenti.* FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente) As T1;
In realtà qui la sorgente dei dati dalla quale vengono estratti è a sua volta una query, vista come tabella (T1). Un altro modo per dare risposta sarebbe stato effettuare il classico inner join e andare a contare i valori distinti degli utenti, ossia degli id degli utenti:
SELECT COUNT(DISTINCT Utenti.Id_Utente) As NumeroUtenti FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente
Pagina 12
In questo caso, essendo gli utenti presenti nel risultato della query di base ripetuti più volte, non basta però una COUNT generica che conterebbe le ripetizioni. La query:
SELECT COUNT(*) As NumeroUtenti FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente
Non risponde alla domanda posta e dà un risultato errato: +--------------+ | NumeroUtenti | +--------------+ | 13 | +--------------+
Viceversa la stessa query corretta con il conteggio su una sola colonna (la chiave primaria di Utenti nel
nostro caso) e la clausola di distinguere tra valori identici, ci permette di giungere al risultato: SELECT COUNT(DISTINCT Utenti.Id_Utente) As NumeroUtenti FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente +--------------+
| NumeroUtenti |
+--------------+
| 2 |
+--------------+
UNION e operazioni insiemistiche
Anche in SQL esistono degli operatori per effettuare operazioni su insiemi. Ovviamente questi insiemi
sono costituiti da insiemi di tuple o record, tutti di tipo simile, ossia con lo stesso numero di colonne e di
tipi analoghi.
Consideriamo l’insieme degli utenti che non hanno mai acceduto al sistema a cui siano uniti gli utenti che
hanno acquistato il prodotto 3. Le due query che risolvono i singoli problemi proposti sono SELECT * # tutti gli utenti FROM Utenti WHERE Id_Utente NOT IN # ... che non sono fra quelli che ... (SELECT Utenti.Id_Utente # utenti che hanno acceduto al sistema FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente)
La query proposta qui sopra si basa su una sottrazione tra l’insieme di tutti gli utenti e su questo individuare quelli che non sono nel secondo insieme (subquery), ossia coloro che hanno acceduto al sistema. E’ ovvio che si ottengono gli utenti che non hanno acceduto al sistema.
+-----------+-------+---------+-------+--------+ | Id_Utente | Nome | Cognome | Login | Passwd | +-----------+-------+---------+-------+--------+ | 3 | Carlo | Verdi | verdi | green | +-----------+-------+---------+-------+--------+
Per il secondo problema si analizza cosa hanno acquistato gli utenti e si impone di selezionare solo quelli
che hanno acquistato il prodotto di id 3: SELECT Utenti.* FROM Utenti, Accessi, Acquisti WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Accessi.Id_Accesso = Acquisti.Id_Accesso AND Acquisti.Id_Prodotto = 3;
Da notare che questa query può produrre risultati duplicati. La combinazione dei due gruppi di utenti può avvenire tramite una UNION:
SELECT * # tutti gli utenti
Pagina 13
FROM Utenti WHERE Id_Utente NOT IN # ... che non sono fra quelli che ... (SELECT Utenti.Id_Utente # utenti che hanno acceduto al sistema FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente) UNION SELECT Utenti.* FROM Utenti, Accessi, Acquisti WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Accessi.Id_Accesso = Acquisti.Id_Accesso AND Acquisti.Id_Prodotto = 3;
che rende tra l’altro un risultato privo di duplicati in quanti dal punto di vista insiemistico essi non possono
esistere: +-----------+-------+---------+-------+--------+
| Id_Utente | Nome | Cognome | Login | Passwd |
+-----------+-------+---------+-------+--------+
| 3 | Carlo | Verdi | verdi | green |
| 2 | Carlo | Verdi | verdi | green |
+-----------+-------+---------+-------+--------+
In pratica la sintassi della UNION si scrive inserendo una indicazione UNION tra due query. Le due query i
cui dati dovranno essere uniti, dovranno produrre:
Stesso numero di colonne
Tipi di colonne identici
Ordine delle colonne uguale
Sotto queste condizioni la UNION tra i due insiemi di tuple sarà possibile.
Una variante della UNION è la UNION ALL, la quale effettua la fusione tra insiemi di tuple eventualmente
inserendo eventuali duplicati delle truple stesse, rendendo quindi il risultato non privo di duplicati e quindi
non un insieme in sesno stretto.
Ad esempio supponiamo di voler unire in un unico risultato gli accessi effettuati dopo le 22:40 del
26/3/2018 e quelli effettuati dall’utente 2: SELECT Accessi.* FROM Accessi WHERE Momento > '2018-03-26 22:40' UNION SELECT Accessi.* FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Utenti.Id_Utente = 2
Il set di queste due query si sovrappone, e quello della prima è contenuto del tutto in quello della seconda: +------------+---------------------+-----------+
| Id_Accesso | Momento | Id_Utente |
+------------+---------------------+-----------+
| 11 | 2018-03-26 22:41:09 | 2 |
| 12 | 2018-03-26 22:44:07 | 2 |
| 13 | 2018-03-26 22:44:39 | 2 |
| 8 | 2018-03-26 22:27:28 | 2 |
| 9 | 2018-03-26 22:38:50 | 2 |
| 10 | 2018-03-26 22:39:10 | 2 |
+------------+---------------------+-----------+
a questo punto l’unione dei due insiemi coincide con quello della seconda query.
Pagina 14
Esiste però un altra variante della UNION detta UNION ALL. Nel caso però venga usata tale variante si avrà
l’aggiunta comunque nell’insieme dei risultati dei record della prima, assieme ai record della seconda
query, pur creando duplicati: +------------+---------------------+-----------+
| Id_Accesso | Momento | Id_Utente |
+------------+---------------------+-----------+
| 11 | 2018-03-26 22:41:09 | 2 |
| 12 | 2018-03-26 22:44:07 | 2 |
| 13 | 2018-03-26 22:44:39 | 2 |
| 8 | 2018-03-26 22:27:28 | 2 |
| 9 | 2018-03-26 22:38:50 | 2 |
| 10 | 2018-03-26 22:39:10 | 2 |
| 11 | 2018-03-26 22:41:09 | 2 |
| 12 | 2018-03-26 22:44:07 | 2 |
| 13 | 2018-03-26 22:44:39 | 2 |
+------------+---------------------+-----------+
In realtà l‘applicazione della UNION ALL, al contrario della UNION comporta che il risultato possa contenere
tuple duplicate.
Consideriamo ora una operazione di intersezione di due insiemi di tuple. Il comando per metterla in atto è
INTERSECT, non implementato nella sintassi SQL di MySQL. Volendo individuare quali accessi siano in
comune tra quelli dell’utente 2 e quelli effettuati dopo il 26/3/2018 22:40, potremo effettuare la seguente
operazione equivalente che implementa una intersezione tra i due insiemi di tuple:
SELECT Accessi.* FROM Accessi WHERE Momento > '2018-03-26 22:40' AND Id_Accesso IN (SELECT Accessi.Id_Accesso FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Utenti.Id_Utente = 2)
Che produce appunto l’intersezione dei due insiemi: +------------+---------------------+-----------+
| Id_Accesso | Momento | Id_Utente |
+------------+---------------------+-----------+
| 11 | 2018-03-26 22:41:09 | 2 |
| 12 | 2018-03-26 22:44:07 | 2 |
| 13 | 2018-03-26 22:44:39 | 2 |
+------------+---------------------+-----------+
Lo stesso risultato lo otteniamo ovviamente invertendo la query con la sua subquery: SELECT Accessi.* FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente AND Utenti.Id_Utente = 2 AND Id_Accesso IN (SELECT Id_Accesso FROM Accessi WHERE Momento > '2018-03-26 22:40')
Vediamo ora un esempio di differenza tra insiemi sempre effettuata con l’insieme di dati precedente. Supponiamo quindi di voler trovare tutti gli accessi dell’ utente 2 (insieme più ampio), esclusi quelli che siano avvenuti dopo il 26/3/2018 dalle 22:40. Ovviamente questo problema può essere visto come un problema di differenza tra insiemi. Gli elementi
dei due insiemi sono comuni e sono i singoli accessi. SELECT Accessi.* FROM Utenti, Accessi WHERE Utenti.Id_Utente = Accessi.Id_Utente AND
Pagina 15
Utenti.Id_Utente = 2 AND Id_Accesso NOT IN (SELECT Id_Accesso FROM Accessi WHERE Momento > '2018-03-26 22:40')
L’operatore NOT IN permette di effettuare l’operazione di differenza prendendo l’insieme della query principale (nel ns. caso gli accessi dell’utente di id 2) e escludendo da tale insieme tutti gli elementi (gli accessi) che abbiano id presente nell’ insieme degli id prodotti dalla seconda query (ossia gli accessi avvenuti dopo il 26/3/2018 22:40), ossia quegli accessi dell’utente 2 il cui id non è nell’insieme degli id degli accessi avenuti dopo il 26/3/2018 22:40. Questo realizza quindi una operazione di differenza tra insiemi. La differenza tra insiemi in SQL ha un suo comando specifico (MINUS), il quale però purtroppo non è implementato nell’ SQL di MySQL. Come si è visto quindi anche in questi casi tornano utili due operatori spesso utilizzabili:
IN determina se un certo elemento (del primo insieme) sia presente nel secondo (prodotto dalla subquery)
NOT IN determina se un certo elemento (del primo insieme) non sia presente nel secondo (prodotto dalla subquery)
Alcuni problemi risolvibili con gli operatori IN e NOT IN
Alcuni problemi su insiemi necessitano dell’uso degli operatori su insiemi IN e NOT IN.
Ad esempio poniamoci il problema di voler conoscere il/i prodotto/i che siano stati acquistati solo
dall’utente di id 2. Per risolvere questo problema possiamo individuare tutti i prodotti acquistati da utenti
diversi da 2, dopodichè andremo a trovare il complemento di questo insieme, che escludendo tutti i
prodotti acquistati da utenti diversi da 2, ci darà i prodotti acquistati dall’utente 2 e solo da esso (utenti con
altri id sono esclusi).
Per verificare questa query consideriamo il quadro di insieme dato dalla query: # utente e prodotto acquistato SELECT utenti.Id_Utente, prodotti.Id_Prodotto FROM utenti, accessi, acquisti, prodotti WHERE utenti.Id_Utente = accessi.Id_Utente AND accessi.Id_Accesso = acquisti.Id_Accesso AND acquisti.Id_Prodotto = prodotti.Id_Prodotto; +-----------+-------------+
| Id_Utente | Id_Prodotto |
+-----------+-------------+
| 1 | 1 |
| 1 | 2 |
| 1 | 2 |
| 1 | 4 |
| 2 | 2 |
| 2 | 3 |
| 2 | 3 |
| 2 | 3 |
| 2 | 3 |
| 2 | 3 |
| 2 | 4 |
| 2 | 4 |
+-----------+-------------+
Come si può osservare già ad occhio il prodotto 3 viene acquistato solo dall’utente 2, ma non da 1.
(Osserviamo anche che analoga cosa avviene al prodotto 1, con l’utente di id 1).
A questo punto troviamo tutti i prodotti che sono acquistati da utenti diversi da 2:
SELECT prodotti.Id_Prodotto FROM utenti, accessi, acquisti, prodotti WHERE
Pagina 16
utenti.Id_Utente = accessi.Id_Utente AND accessi.Id_Accesso = acquisti.Id_Accesso AND acquisti.Id_Prodotto = prodotti.Id_Prodotto AND Utenti.Id_Utente <> 2; +-------------+
| Id_Prodotto |
+-------------+
| 1 |
| 2 |
| 2 |
| 4 |
+-------------+
Nel risultato ottenuto manca il prodotto 3, che giustamente viene acquistato solo da 2 e quindi non viene acquistato da qualunque utente che non sia 2 (Utenti.Id_Utente <> 2). A questo punto per individuare il prodotto in questione basta prendere tutti i prodotti e togliere da essi i prodotti che hanno acquistato gli utenti diversi da 2. Restano i prodotti acquistati da 2 soltanto:
# prodotti che siano stati acquistati solo dall'utente 2 SELECT * # tutti i prodotti FROM Prodotti WHERE Id_Prodotto NOT IN # prodotti che sono stati acquistati da utenti diversi da 2 (SELECT prodotti.Id_Prodotto FROM utenti, accessi, acquisti, prodotti WHERE utenti.Id_Utente = accessi.Id_Utente AND accessi.Id_Accesso = acquisti.Id_Accesso AND acquisti.Id_Prodotto = prodotti.Id_Prodotto AND Utenti.Id_Utente <> 2);
che rende: +-------------+--------------+--------+----------+ | Id_Prodotto | NomeProdotto | Prezzo | Quantita | +-------------+--------------+--------+----------+ | 3 | 6 bicchieri | 2.50 | 16 | +-------------+--------------+--------+----------+
Il solo prodotto 3. In modo simmetrico chidendo quali siano i prodotti acquistati dal solo utente 1 con query del tutto analoga (cambia solo l’ultima parte in Utenti.Id_Utente <> 1) si ottiene correttamente:
+-------------+--------------+--------+----------+ | Id_Prodotto | NomeProdotto | Prezzo | Quantita | +-------------+--------------+--------+----------+ | 1 | Guanti | 3.00 | 13 | +-------------+--------------+--------+----------+