Esercizio - Interrogazioni sugli ordini di una pizzeria (tema d'esame 1 marzo 2013)
In questa pagina 7
Testo (compito d'esame del 1 marzo 2013, "Gestione ordini pizzeria", dal corso di Basi di Dati, Ing. Informatica UniPD). Una pizzeria a Padova vuole gestire le ordinazioni:
- ci sono dei tavoli identificati da un numero, liberi, occupati o in attesa del conto; ad ogni tavolo c'è un solo cameriere che prende la comanda;
- la comanda ha un numero, una data, il tavolo e il cameriere che l'ha compilata, e la lista delle pietanze ordinate;
- le pietanze si dividono in piatti e bevande, hanno una categoria (antipasti, pizze, pizze speciali, dolci, analcolici, birre, vini...), un numero identificativo, un nome, una descrizione e un prezzo; ogni elemento della comanda ha una quantità;
- ogni piatto è composto da alcuni ingredienti; solo per le pizze si possono fare aggiunte o eliminazioni di ingredienti (una capricciosa senza funghi con doppia mozzarella); ogni ingrediente ha un prezzo, che si conta nell'aggiunta;
- alla fine si chiede il conto, uno scontrino (con lista delle pietanze, prezzi e data di emissione) o una fattura (con un numero identificativo, e un codice fiscale o una partita IVA); non c'è sovrapprezzo per il coperto.
Interrogazioni da scrivere in algebra relazionale e in SQL:
- la somma delle birre e delle pizze ordinate il giorno 2013-03-01;
- la fattura numero 10: tutti i piatti ordinati e il loro prezzo (comprese le aggiunte e le eliminazioni);
- la media dei conti del tavolo numero 5.
La progettazione ER (entità, relazioni, scelta della "pietanza ordinata" come entità debole, il conto come generalizzazione scontrino/fattura) è nell'esercizio di progettazione di questa stessa traccia. Qui si riporta soltanto lo schema relazionale della soluzione del corso e si risolvono le tre query. Teoria: SQL - interrogazioni, join e aggregazioniUn'interrogazione SQL ha almeno SELECT e FROM; le altre clausole (WHERE, GROUP BY, HAVING, ORDER BY) sono opzionali e HAVING richiede GROUP BY. Ordine logico di valutazione: FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY. SELECT corrisponde a π (ma senza DISTINCT non elimina i duplicati), WHERE a σ, gli alias ad AS/ρ, i join a ⋈ (INNER, LEFT, RIGHT, FULL). WHERE seleziona le righe prima del raggruppamento, HAVING seleziona i gruppi dopo. Le funzioni COUNT, SUM, AVG, MIN, MAX ignorano i NULL (COUNT() conta le righe, DISTINCT toglie i duplicati dentro la funzione); con un aggregato o con GROUP BY nella SELECT possono comparire solo colonne di raggruppamento o aggregati. LIKE usa % (qualsiasi sequenza) e _ (un carattere). I confronti con NULL non sono veri: serve IS NULL. Un filtro sulla tabella di destra di un LEFT JOIN va nella clausola ON, non in WHERE, altrimenti il join diventa interno. Con interi COUNT(a)/COUNT(b) è una divisione intera: serve CAST.SQL - interrogazioni, join e aggregazioni →, Algebra relazionale estesa - divisione, aggregazione e outer joinL'algebra relazionale di base lavora una tupla alla volta e non sa fare calcoli sui valori né riassumere più tuple. L'estensione aggiunge: la proiezione generalizzata π_{F1,...,Fn}(R), che calcola espressioni sugli attributi (prezzo per quantità); l'aggregazione ℱ_{SUM, MAX, MIN, AVG, COUNT}(R), che produce una sola tupla; il raggruppamento (A1,...,Am)ℱ_{f(B)}(R), che produce una tupla per gruppo (grado n+m, cardinalità = numero di gruppi). Le funzioni di aggregazione ignorano i NULL, COUNT() conta le tuple, i NULL formano un gruppo a sé. Gli outer join (left, right, full) mantengono con NULL le tuple dangling del join; i NULL nella chiave di join non si accoppiano mai. La divisione R÷S dà gli x tali che (x,y) sta in R per ogni y di S, e si ottiene con π, × e −. Ricette: massimo = aggregazione + join con il massimo, "tutti" = divisione o complemento, "almeno due" = self-join.Algebra relazionale estesa - divisione, aggregazione e outer join →.
Progettazione ER e schema relazionale della stessa traccia: Esercizio - Progettazione ER e schema relazionale della pizzeria (tema d'esame 30 giugno 2025).
Schema relazionale (dalla soluzione del corso)
Le chiavi primarie sono in grassetto; le chiavi esterne con la freccia.
| Relazione | Attributi | Note |
|---|---|---|
TAVOLO |
numero, stato | stato {libero, occupato, attesa conto} |
CAMERIERE |
id | |
COMANDA |
numero, data, tavolo TAVOLO, cameriere CAMERIERE |
un tavolo ha più comande (non una sola), ciascuna con un cameriere |
CONTO |
id, comanda COMANDA (chiave: un conto per comanda), totale, tipo, numero_fattura, piva, cf, data |
generalizzazione scontrino/fattura assorbita nel genitore con l'attributo tipo; numero_fattura e (piva o cf) obbligatori se tipo = fattura (vincolo esterno) |
INGREDIENTE |
nome, prezzo | |
PIETANZA |
numero, nome, descrizione, prezzo, tipo, categoria | piatto/bevanda assorbite nel genitore (tipo, categoria) |
COMPONE |
ingrediente INGREDIENTE, pietanza PIETANZA |
solo per le pizze (vincolo esterno) |
PIETANZA_ORDINATA |
id, pietanza PIETANZA, comanda COMANDA, qta |
entità identificata da id e dalla pietanza (identificazione esterna): due pizze margherita nella stessa comanda sono due istanze con id diversi |
MODIFICA |
id, pietanza, ingrediente INGREDIENTE, aggiunge |
PIETANZA_ORDINATA; aggiunge vero = aggiunta, falso = eliminazione |
Due scelte della soluzione da conoscere: (a) totale è un attributo derivato (somma, per ogni pietanza ordinata, di qta per il prezzo con le aggiunte): lo si tiene perché serve per la terza query; (b) la PIETANZA_ORDINATA è un'entità a sé, con modifiche per ogni istanza, perché una comanda può contenere due pizze uguali con modifiche diverse. Per le query sotto si è aggiunta a CONTO la data (il testo la richiede per lo scontrino; la soluzione la ometteva nella tabella a mano) e aggiunge come booleano.
Dati inventati per provare
TAVOLO: (1, libero), (5, occupato), (7, libero). CAMERIERE: 1, 2. INGREDIENTE: mozzarella 1,5; funghi 1,0; prosciutto 2,0; pomodoro 0,5; olive 0,8. PIETANZA (numero, nome, prezzo, tipo, categoria): (1, Margherita, 6, piatto, pizze), (2, Capricciosa, 8, piatto, pizze), (3, Birra media, 4, bevanda, birre), (4, Tiramisù, 5, piatto, dolci), (5, Bruschetta, 3,5, piatto, antipasti), (6, Cola, 3, bevanda, analcolici).
COMANDA (numero, data, tavolo, cameriere): (1, 2013-03-01, 5, 1), (2, 2013-03-01, 7, 2), (3, 2013-03-02, 5, 1), (4, 2013-03-01, 5, 2).
PIETANZA_ORDINATA (id, pietanza, comanda, qta): (1,1,1,1), (2,1,1,1), (1,3,1,3), (1,4,1,1), (1,2,2,1), (1,6,2,2), (1,5,3,1), (3,1,3,1), (2,2,4,2), (2,3,4,1). MODIFICA (id, pietanza, ingrediente, aggiunge): (1,1,mozzarella,vero), (2,1,pomodoro,falso), (1,2,funghi,falso), (1,2,olive,vero).
CONTO (id, comanda, totale, tipo, numero_fattura): (1, 1, 30,5, fattura, 10), (2, 2, 14,8, scontrino, -), (3, 3, 9,5, scontrino, -), (4, 4, 20, fattura, 11).
Nella comanda 1 c'è una margherita con doppia mozzarella (aggiunta a 1,5) e una senza pomodoro (l'eliminazione non cambia il prezzo), tre birre e un tiramisù: , il totale del conto 1.
Query 1: la somma delle birre e delle pizze ordinate il 2013-03-01
Idea. Occorrono le pietanze ordinate nelle comande di quel giorno (COMANDA PIETANZA_ORDINATA), poi la categoria della pietanza (PIETANZA), poi un filtro sulla categoria e una somma delle quantità.
Algebra. Con per PIETANZA_ORDINATA:
(Nel join tra COM e PB l'attributo numero è quello della pietanza; nel primo join quello della comanda: va rinominato o proiettato via, per esempio prima del secondo join, come fa la soluzione.) Per avere un solo numero (pizze e birre insieme) si toglie la categoria dal raggruppamento: .
SQL.
SELECT p.categoria, SUM(po.qta) AS totale
FROM comanda AS c
INNER JOIN pietanza_ordinata AS po ON c.numero = po.comanda
INNER JOIN pietanza AS p ON po.pietanza = p.numero
WHERE p.categoria IN ('pizze', 'birre') AND c.data = '2013-03-01'
GROUP BY p.categoria;Risultato sui dati: birre 4 (3 nella comanda 1 e 1 nella 4), pizze 5 (2 margherite nella 1, 1 capricciosa nella 2, 2 capricciose nella 4). Con un unico totale (togliendo p.categoria dalla SELECT e il GROUP BY) il risultato è 9.
Commenti.
- La richiesta "la somma delle birre e delle pizze" è ambigua: due numeri (uno per categoria) o uno solo? La soluzione del corso raggruppa per categoria; si può fare una query sola con
GROUP BYe un'altra senza. Meglio dichiarare la scelta. - Si somma
qta, non si conta conCOUNT(*): ogni riga ordinata ha una quantità (3 birre in una riga). - "Pizze speciali" è una categoria diversa da "pizze": è corretto escluderla con
IN ('pizze','birre'), ma se la richiesta le comprendesse servirebbeLIKE 'pizze%'. - Errore nel materiale del corso: nella soluzione a mano il valore
'Bme'al posto di'Birre'nelWHEREè un refuso che non restituirebbe nessuna birra. - Algebra e SQL sono stati confrontati: stesso risultato.
Query 2: la fattura numero 10, con piatti e prezzo
Idea. Dalla fattura (CONTO con numero_fattura = 10) si risale alla comanda, alle pietanze ordinate, e per ogni pietanza si calcola il prezzo unitario = prezzo base + prezzo delle sole aggiunte; l'eliminazione di un ingrediente non cambia il prezzo (il testo conta il prezzo "nell'aggiunta"). L'importo di una riga è qta per il prezzo unitario.
Soluzione del corso.
SELECT * FROM conto AS c
INNER JOIN pietanza_ordinata AS po ON c.pietanza = po.pietanza -- la soluzione scrive così
INNER JOIN modifica AS m ON po.id = m.id AND po.pietanza = m.pietanza
INNER JOIN pietanza AS p ON po.pietanza = p.numero
WHERE fattura = 10;Errori della soluzione del corso. (1)
CONTOnon ha l'attributopietanza: il collegamento conPIETANZA_ORDINATAèc.comanda = po.comanda(SQLite: no such column: c.pietanza). (2) Il join interno conMODIFICAelimina le pietanze senza modifiche: sui dati, la fattura 10 ha 4 righe ordinate, ma solo 2 hanno una modifica, quindi la query ne restituisce 2 (le due margherite) e perde birre e tiramisù. Serve un left outer join. (3) Non si calcola nessun prezzo:SELECT *mostra solo le righe. Il testo chiede "il prezzo (comprese le aggiunte/eliminazioni)".
Soluzione corretta (SQL).
SELECT po.id, p.nome, po.qta, p.prezzo,
COALESCE(SUM(CASE WHEN m.aggiunge THEN i.prezzo END), 0) AS extra,
po.qta * (p.prezzo + COALESCE(SUM(CASE WHEN m.aggiunge THEN i.prezzo END), 0)) AS importo
FROM conto AS c
INNER JOIN pietanza_ordinata AS po ON po.comanda = c.comanda
INNER JOIN pietanza AS p ON p.numero = po.pietanza
LEFT JOIN modifica AS m ON m.id = po.id AND m.pietanza = po.pietanza
LEFT JOIN ingrediente AS i ON i.nome = m.ingrediente
WHERE c.numero_fattura = 10
GROUP BY po.id, po.pietanza, p.nome, po.qta, p.prezzo;Il LEFT JOIN conserva le pietanze senza modifiche (con m e i a NULL); CASE WHEN m.aggiunge THEN i.prezzo END prende il prezzo solo per le aggiunte (le eliminazioni danno NULL e SUM le ignora); COALESCE(..., 0) trasforma il NULL (nessuna aggiunta) in 0. Si raggruppa per l'identificatore della pietanza ordinata, perché il join con MODIFICA moltiplica le righe (una per modifica): senza GROUP BY si avrebbero righe duplicate.
Risultato sulla fattura 10 (comanda 1):
| id | nome | qta | prezzo | extra | importo |
|---|---|---|---|---|---|
| 1 | Margherita | 1 | 6 | 1,5 | 7,5 |
| 2 | Margherita | 1 | 6 | 0 | 6 |
| 1 | Birra media | 3 | 4 | 0 | 12 |
| 1 | Tiramisù | 1 | 5 | 0 | 5 |
La somma degli importi è 30,5, uguale al totale memorizzato nel conto: la verifica che l'attributo derivato è coerente.
Algebra relazionale (corretta). Si evita il NULL con un'unione:
(dove è la relazione con il solo attributo extra e la sola tupla .)
Il primo passo isola le pietanze ordinate della fattura; il secondo i prezzi delle sole aggiunte; il terzo somma per pietanza ordinata e aggiunge per quelle senza aggiunte (la differenza del terzo passo), così il join finale non perde nessuna riga; l'ultimo è una proiezione generalizzata che calcola l'importo. Il risultato coincide con quello SQL (verificato).
Query 3: la media dei conti del tavolo 5
Idea. Il conto non conosce il tavolo: si passa dalla comanda. Poi si media il totale.
Algebra (la soluzione del corso, con una rinomina necessaria: CONTO e COMANDA hanno entrambe data):
SQL.
SELECT AVG(c.totale) AS media_conti
FROM conto AS c INNER JOIN comanda AS co ON c.comanda = co.numero
WHERE co.tavolo = 5;Sui dati: i conti del tavolo 5 sono quelli delle comande 1, 3 e 4, con totali , , : media . Se il tavolo non ha conti la media è NULL.
Senza l'attributo derivato totale, bisogna ricalcolarlo per ogni comanda (somma per pietanza ordinata di qta per prezzo con le aggiunte, come nella query 2) e mediare i totali: stesso 20 (verificato). L'attributo derivato fa risparmiare un join e un raggruppamento, a costo di dover mantenere coerente il totale quando la comanda cambia (un candidato a trigger, vedi SQL avanzato - indici, transazioni e triggerUn file di record può essere heap (inserimento in ordine, ricerca per uguaglianza in media n/2 accessi), ordinato (log2 n), hash (costante, solo uguaglianza) o B-tree (log_F n, anche intervalli). Un indice è una struttura ausiliaria di coppie (chiave K, puntatore P) ordinate, molto più piccola della tabella: l'indice primario è sul campo di ordinamento del file ordinato (una chiave per blocco, non denso, log2(m)+1 accessi), l'indice secondario è su un campo qualsiasi (denso). CREATE [UNIQUE] INDEX, anche su espressioni; PostgreSQL usa tabelle heap con indici secondari B-tree (predefinito) o hash, e PRIMARY KEY e UNIQUE creano un indice implicito. Gli indici velocizzano le letture ma costano nelle scritture e in spazio. Una transazione è una sequenza atomica di operazioni con proprietà ACID (atomicità, consistenza, isolamento, durabilità): START TRANSACTION, COMMIT, SAVEPOINT, ROLLBACK [TO SAVEPOINT]. Un trigger è una regola evento-condizione-azione (ECA): BEFORE/AFTER, ROW/STATEMENT, WHEN, con funzione PL/pgSQL che usa NEW e OLD; serve per vincoli complessi e attributi derivati.SQL avanzato - indici, transazioni e trigger →).
Errori nel materiale del corso (riassunto)
- Query 1: refuso
'Bme'; richiesta ambigua (una somma o una per categoria). - Query 2: join interno con
MODIFICAche elimina le pietanze senza modifiche;ON c.pietanza = ...su una colonna inesistente; nessun calcolo del prezzo. - Query 3: nel join
COMANDACONTOgli attributi omonimi (data) vanno rinominati.
Versione ripasso
Schema (soluzione del corso): TAVOLO(numero, stato), CAMERIERE(id), COMANDA(numero, data, tavolo, cameriere), CONTO(id, comanda, totale, tipo, numero_fattura, piva, cf), INGREDIENTE(nome, prezzo), PIETANZA(numero, nome, descrizione, prezzo, tipo, categoria), COMPONE(ingrediente, pietanza), PIETANZA_ORDINATA(id, pietanza, comanda, qta), MODIFICA(id, pietanza, ingrediente, aggiunge). Nel conto si aggiungono data e aggiunge booleano. Teoria: SQL - interrogazioni, join e aggregazioniUn'interrogazione SQL ha almeno SELECT e FROM; le altre clausole (WHERE, GROUP BY, HAVING, ORDER BY) sono opzionali e HAVING richiede GROUP BY. Ordine logico di valutazione: FROM, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY. SELECT corrisponde a π (ma senza DISTINCT non elimina i duplicati), WHERE a σ, gli alias ad AS/ρ, i join a ⋈ (INNER, LEFT, RIGHT, FULL). WHERE seleziona le righe prima del raggruppamento, HAVING seleziona i gruppi dopo. Le funzioni COUNT, SUM, AVG, MIN, MAX ignorano i NULL (COUNT() conta le righe, DISTINCT toglie i duplicati dentro la funzione); con un aggregato o con GROUP BY nella SELECT possono comparire solo colonne di raggruppamento o aggregati. LIKE usa % (qualsiasi sequenza) e _ (un carattere). I confronti con NULL non sono veri: serve IS NULL. Un filtro sulla tabella di destra di un LEFT JOIN va nella clausola ON, non in WHERE, altrimenti il join diventa interno. Con interi COUNT(a)/COUNT(b) è una divisione intera: serve CAST.SQL - interrogazioni, join e aggregazioni →, Algebra relazionale estesa - divisione, aggregazione e outer joinL'algebra relazionale di base lavora una tupla alla volta e non sa fare calcoli sui valori né riassumere più tuple. L'estensione aggiunge: la proiezione generalizzata π_{F1,...,Fn}(R), che calcola espressioni sugli attributi (prezzo per quantità); l'aggregazione ℱ_{SUM, MAX, MIN, AVG, COUNT}(R), che produce una sola tupla; il raggruppamento (A1,...,Am)ℱ_{f(B)}(R), che produce una tupla per gruppo (grado n+m, cardinalità = numero di gruppi). Le funzioni di aggregazione ignorano i NULL, COUNT() conta le tuple, i NULL formano un gruppo a sé. Gli outer join (left, right, full) mantengono con NULL le tuple dangling del join; i NULL nella chiave di join non si accoppiano mai. La divisione R÷S dà gli x tali che (x,y) sta in R per ogni y di S, e si ottiene con π, × e −. Ricette: massimo = aggregazione + join con il massimo, "tutti" = divisione o complemento, "almeno due" = self-join.Algebra relazionale estesa - divisione, aggregazione e outer join →.
Birre e pizze del 2013-03-01: , poi il join con
PIETANZAfiltrata per categoria e . Si sommaqta, non si conta. Risultato: birre 4 (3 nella comanda 1, 1 nella 4), pizze 5 (2 margherite nella 1, 1 capricciosa nella 2, 2 nella 4); senza distinzione per categoria, 9. Il testo non dice se la somma va fatta per categoria: conviene dichiarare la scelta.- SQL:
WHERE p.categoria IN ('pizze', 'birre') AND c.data = '2013-03-01' GROUP BY p.categoria. - Refuso del corso:
'Bme'al posto di'Birre'.
- SQL:
Fattura 10: piatti e prezzo. Il prezzo unitario è il prezzo base più il prezzo delle sole aggiunte; l'eliminazione non cambia il prezzo. Righe: Margherita con doppia mozzarella , Margherita senza pomodoro , tre birre , tiramisù : totale , uguale al totale del conto.
SELECT po.id, p.nome, po.qta, p.prezzo,
COALESCE(SUM(CASE WHEN m.aggiunge THEN i.prezzo END), 0) AS extra
FROM conto AS c
INNER JOIN pietanza_ordinata AS po ON po.comanda = c.comanda
INNER JOIN pietanza AS p ON p.numero = po.pietanza
LEFT JOIN modifica AS m ON m.id = po.id AND m.pietanza = po.pietanza
LEFT JOIN ingrediente AS i ON i.nome = m.ingrediente
WHERE c.numero_fattura = 10
GROUP BY po.id, po.pietanza, p.nome, po.qta, p.prezzo;- Il
LEFT JOINconMODIFICAconserva le pietanze senza modifiche; ilGROUP BYserve perché il join moltiplica le righe. L'importo è , cioè , , e . - Algebra: è la somma degli extra per , unita a per le pietanze senza aggiunte, così il join finale non perde righe. La formula finale è una proiezione generalizzata che calcola l'importo.
- Errori del corso: join interno con
MODIFICA, che lascia 2 righe su 4;c.pietanzanon esiste inCONTO; prezzo mai calcolato.
- Media dei conti del tavolo 5. I conti delle comande 1, 3 e 4 hanno totali , e : media .
SELECT AVG(c.totale) AS media_conti
FROM conto AS c INNER JOIN comanda AS co ON c.comanda = co.numero
WHERE co.tavolo = 5;- Nel join
datacompare in entrambe le tabelle e va rinominata: in algebra , poi . Un tavolo senza conti dà NULL. - Senza l'attributo derivato
totalesi ricalcola per ogni comanda, come nella query 2.
Errori tipici
- Join interno dove servono le pietanze senza modifica.
COUNT(*)al posto di sommareqta.- Join con
MODIFICAsenzaGROUP BY: righe duplicate. - Media su un tavolo senza conti: NULL.