Salta al contenuto
Note per Studenti Organizzazione fisica dei dati e accesso programmatico alle basi di dati

Organizzazione fisica dei dati e accesso programmatico alle basi di dati

In questa pagina 7

Questa nota chiude la parte di interrogazioni e copre tre argomenti del programma ufficiale: l'architettura di un DBMS, la gestione delle interrogazioni e la progettazione fisica, l'accesso programmatico con un'introduzione alle applicazioni web. Le slide del corso trattano in dettaglio solo indici, transazioni e trigger (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 →); il resto di questa nota è scritto a partire dal programma, in forma concisa, con esempi provati in SQLite e Python. Prerequisiti: Basi di dati e DBMS - architettura ANSI-SPARC e indipendenza dei datiUna base di dati (database, DB) è una collezione integrata e persistente di dati correlati che rappresenta un frammento di mondo reale (mini-mondo); un DBMS è il software generico che permette di definirla (schema, nel catalogo come metadati), costruirla e manipolarla (interrogazioni e aggiornamenti). Rispetto a una raccolta di file il DBMS riduce ridondanza e incoerenza, condivide i dati tra applicazioni con controllo della concorrenza (isolamento, atomicità), garantisce durabilità, sicurezza (autenticazione e autorizzazione) ed efficienza. L'architettura ANSI/SPARC ha tre livelli: schema esterno (una vista per applicazione), logico (rappresentazione integrata, indipendente dalla memorizzazione), interno (strutture fisiche); da qui l'indipendenza fisica (cambia l'interno senza toccare logico ed esterno) e logica (cambia il logico senza toccare le applicazioni). Attori: sviluppatori del DBMS, DBA, progettisti di basi di dati e di applicazioni, utenti parametrici e occasionali.Basi di dati e DBMS - architettura ANSI-SPARC e indipendenza dei dati → (livelli e indipendenza dei dati), Algebra relazionale - operatori di base e joinLe operazioni sul modello relazionale sono di aggiornamento (inserimento, modifica, cancellazione, che possono violare i vincoli di integrità: si rifiuta, si corregge, si propaga con cascade o si imposta NULL/default) e di interrogazione (funzioni che da un'istanza del database producono una relazione). L'algebra relazionale è il linguaggio procedurale delle interrogazioni: gli operatori primitivi sono unione, differenza, prodotto, rinomina ρ, selezione σ e proiezione π; derivati sono intersezione, theta-join, equi-join, join naturale. Unione, intersezione e differenza richiedono relazioni compatibili all'unione (stessi attributi e domini). La selezione è una decomposizione orizzontale, la proiezione verticale (commutano solo se gli attributi della selezione sono nella proiezione). Il theta-join è σ_φ(R1×R2), il join naturale equivale a un equi-join sugli attributi omonimi seguito da una proiezione; la sua cardinalità è tra 0 e |R1||R2|, ≤|R1| se gli attributi comuni sono chiave di R2, =|R1| se sono anche chiave esterna di R1 con vincolo di integrità referenziale. Le tuple senza corrispondente sono "dangling". I valori NULL richiedono condizioni esplicite IS NULL / IS NOT NULL, perché σ_{A>40} ∪ σ_{A≤40} non restituisce le tuple con A nullo.Algebra relazionale - operatori di base e join → e 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 →.

Architettura interna di un DBMS

I livelli ANSI/SPARC descrivono che cosa vede l'utente (schema esterno, logico, interno). Dentro il DBMS, ciò che accade tra una query SQL e i dati sul disco si divide in componenti:

applicazione / client SQL
        |  connessione (autenticazione, sessione)
        v
  parser            analisi sintattica e semantica (usa il catalogo) -> albero della query
        v
  ottimizzatore     riscrittura algebrica + scelta del piano (usa le statistiche) -> piano
        v
  esecutore         esegue gli operatori del piano
        v
  metodi di accesso  scansione di heap/file ordinato, indici B-tree e hash
        v
  buffer manager    tiene in memoria i blocchi usati di recente
        v
  gestore dei file  blocchi (pagine) su disco

Accanto stanno il gestore delle transazioni (lock per l'isolamento, log con scrittura anticipata per atomicità e durabilità, ripristino dopo un guasto: 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 →) e il catalogo, un insieme di tabelle che descrive tabelle, colonne, indici e statistiche (numero di righe, numero di valori distinti, istogrammi).

Memoria e costo

I dati sono su disco, organizzati in blocchi (pagine) da alcuni KB; il disco si legge un blocco alla volta (Memoria di massa e scheduling del discoLa memoria di massa è fatta di dischi rigidi (HDD: piatti, tracce, cilindri, settori) e memorie non volatili NAND (SSD); tempo di accesso HDD = seek + latenza rotazionale (mezzo giro, 30/RPM secondi), tempo di I/O = accesso + trasferimento + overhead del controller; i blocchi sono indirizzati con un LBA lineare; per l'HDD si minimizza il seek con FCFS, SSTF (rischio di starvation), SCAN (ascensore) e C-SCAN (più uniforme), con le varianti LOOK e C-LOOK e il deadline di Linux; per le NVM contano FTL, garbage collection, overprovisioning e wear leveling; il sistema operativo formatta (fisico e logico), partiziona, gestisce il boot e lo spazio di swap; parità ed ECC rilevano e correggono errori; il RAID usa striping, mirroring e parità (livelli 0, 1, 4, 5, 6, 10) per più velocità e affidabilità, con MTTDL = 1/PDL.Memoria di massa e scheduling del disco →). Un accesso al disco costa ordini di grandezza più di un'operazione in memoria, quindi il costo di un piano si misura in numero di accessi a blocchi (I/O). Il buffer manager tiene in memoria centrale un insieme di blocchi: se un blocco richiesto c'è già (hit) non si accede al disco, altrimenti se ne sceglie uno da sostituire con una politica come LRU (concetto analogo alla sostituzione delle pagine in Memoria virtuale - paginazione su richiesta e sostituzione delle pagineLa memoria virtuale separa la memoria logica da quella fisica: basta che alcune pagine siano in RAM; con la paginazione su richiesta (demand paging) una pagina si carica solo al primo riferimento, e un riferimento a una pagina non residente (bit di validità = i) genera un page fault gestito dal sistema operativo in sei passi; EAT = (1 - p) * accesso + p * servizio del fault, con p minuscolo; copy-on-write rende economica la fork; se non ci sono frame liberi si sceglie una vittima con FIFO (anomalia di Belady), OPT (ottimo, irrealizzabile), LRU (algoritmo a stack), second chance (bit di riferimento, orologio) o la versione con bit di modifica; i frame si assegnano in modo uguale o proporzionale, con sostituzione globale o locale; se un processo non ha abbastanza frame va in thrashing; il modello del working set e la frequenza dei page fault lo evitano; considerazioni su prepaging, dimensione della pagina, TLB reach e struttura dei programmi; memoria del kernel con buddy system e slab.Memoria virtuale - paginazione su richiesta e sostituzione delle pagine →).

Un record sta in un blocco; un blocco contiene più record. Con nRn_R record e un fattore di blocco ff (record per blocco) la tabella occupa bR=⌈nR/f⌉b_R=\lceil n_R/f\rceil blocchi. L'organizzazione dei file (heap, ordinato, hash, B-tree) e degli indici è descritta in 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 →; riassunto dei costi di una ricerca per uguaglianza: heap bR/2b_R/2 (in media) o bRb_R (se non è unica), ordinato log⁡2bR\log_2 b_R, hash circa 1, B-tree log⁡FbR\log_F b_R, indice primario log⁡2m+1\log_2 m+1.

Gestione delle interrogazioni

SQL è dichiarativo: il DBMS deve scegliere come eseguire la query. Il processo:

  1. Parsing: controlla la sintassi e che tabelle e colonne esistano, e produce una rappresentazione algebrica;
  2. Riscrittura (ottimizzazione logica): applica equivalenze algebriche per ottenere un'espressione più efficiente;
  3. Scelta del piano (ottimizzazione fisica): per ogni operatore sceglie un algoritmo e un metodo di accesso, stimando il costo con le statistiche, e tiene il piano di costo minimo;
  4. Esecuzione.

Equivalenze e riscrittura

Sono le stesse equivalenze dell'algebra (Algebra relazionale - operatori di base e joinLe operazioni sul modello relazionale sono di aggiornamento (inserimento, modifica, cancellazione, che possono violare i vincoli di integrità: si rifiuta, si corregge, si propaga con cascade o si imposta NULL/default) e di interrogazione (funzioni che da un'istanza del database producono una relazione). L'algebra relazionale è il linguaggio procedurale delle interrogazioni: gli operatori primitivi sono unione, differenza, prodotto, rinomina ρ, selezione σ e proiezione π; derivati sono intersezione, theta-join, equi-join, join naturale. Unione, intersezione e differenza richiedono relazioni compatibili all'unione (stessi attributi e domini). La selezione è una decomposizione orizzontale, la proiezione verticale (commutano solo se gli attributi della selezione sono nella proiezione). Il theta-join è σ_φ(R1×R2), il join naturale equivale a un equi-join sugli attributi omonimi seguito da una proiezione; la sua cardinalità è tra 0 e |R1||R2|, ≤|R1| se gli attributi comuni sono chiave di R2, =|R1| se sono anche chiave esterna di R1 con vincolo di integrità referenziale. Le tuple senza corrispondente sono "dangling". I valori NULL richiedono condizioni esplicite IS NULL / IS NOT NULL, perché σ_{A>40} ∪ σ_{A≤40} non restituisce le tuple con A nullo.Algebra relazionale - operatori di base e join →), usate come euristiche:

  • selezioni il prima possibile: σφ1∧φ2(R)=σφ1(σφ2(R))\sigma_{\varphi_1\wedge\varphi_2}(R)=\sigma_{\varphi_1}(\sigma_{\varphi_2}(R)), e se φ\varphi riguarda solo gli attributi di RR, σφ(R⋈S)=σφ(R)⋈S\sigma_\varphi(R\bowtie S)=\sigma_\varphi(R)\bowtie S (si riducono le dimensioni prima del join);
  • proiezioni il prima possibile, conservando gli attributi che servono dopo (per i join e per il risultato);
  • prodotto + selezione = join: σR.A=S.B(R×S)=R⋈A=BS\sigma_{R.A=S.B}(R\times S)=R\bowtie_{A=B}S, che si può calcolare senza generare il prodotto;
  • join commutativo e associativo: l'ordine dei join conta molto: conviene cominciare dai join più selettivi (risultato più piccolo).

Esempio. "Titoli dei riferimenti di McGraw-Hill": πtitle(Reference⋈publisher=idσname=′McGraw-Hill′(Publisher))\pi_{\text{title}}\big(\text{Reference}\bowtie_{\text{publisher}=\text{id}}\sigma_{\text{name}='\text{McGraw-Hill}'}(\text{Publisher})\big) filtra prima i 4 editori a uno e poi fa il join; l'espressione equivalente πtitle(σname=′McGraw-Hill′(Reference⋈Publisher))\pi_{\text{title}}\big(\sigma_{\text{name}='\text{McGraw-Hill}'}(\text{Reference}\bowtie\text{Publisher})\big) fa prima il join su tutta la tabella. Il risultato è lo stesso, il costo no: un ottimizzatore ben fatto li riconduce alla stessa forma.

Algoritmi

Selezione σA=v(R)\sigma_{A=v}(R):

  • scansione completa (sequential scan): legge tutti i bRb_R blocchi; l'unica scelta senza indici o quando molte righe soddisfano la condizione;
  • uso di un indice sull'attributo: pochi accessi se la condizione è selettiva (frazione piccola delle righe).

Join R⋈A=BSR\bowtie_{A=B}S (con RR operando esterno; bR,bSb_R,b_S blocchi, nRn_R record, MM pagine di buffer):

Algoritmo Idea Costo (accessi a blocchi)
Nested loop (per tupla) per ogni tupla di RR scandisce SS bR+nR⋅bSb_R+n_R\cdot b_S
Block nested loop per ogni gruppo di M−2M-2 blocchi di RR scandisce SS bR+⌈bR/(M−2)⌉⋅bSb_R+\lceil b_R/(M-2)\rceil\cdot b_S
Index nested loop per ogni tupla di RR cerca in SS con un indice su BB bR+nR⋅cb_R+n_R\cdot c (cc = costo di una ricerca per indice)
Sort-merge ordina entrambe su AA/BB e le fonde ordinamento + bR+bSb_R+b_S (se già ordinate, solo bR+bSb_R+b_S)
Hash join partiziona entrambe con la stessa funzione hash, poi unisce partizione per partizione circa 3(bR+bS)3(b_R+b_S)

Esempio numerico. bR=1000b_R=1000, bS=100b_S=100, nR=10 000n_R=10\,000, M=12M=12.

  • nested loop per tupla: 1000+10 000⋅100=1 001 0001000+10\,000\cdot100=1\,001\,000;
  • block nested loop con RR esterna: 1000+⌈1000/10⌉⋅100=1000+10 000=11 0001000+\lceil1000/10\rceil\cdot100=1000+10\,000=11\,000;
  • block nested loop con SS esterna (la più piccola all'esterno): 100+⌈100/10⌉⋅1000=10 100100+\lceil100/10\rceil\cdot1000=10\,100;
  • hash join: 3⋅(1000+100)=33003\cdot(1000+100)=3300.

Si vede quanto pesa la scelta: dal primo all'ultimo ci sono tre ordini di grandezza. Per questo l'ottimizzatore ha bisogno delle statistiche del catalogo.

Vedere il piano. EXPLAIN (PostgreSQL: EXPLAIN [ANALYZE]; SQLite: EXPLAIN QUERY PLAN) mostra la scelta. Su una query a tre tabelle del database bibliografico SQLite dà ad esempio SCAN w USING COVERING INDEX sqlite_autoindex_Write_1, poi SEARCH a USING INDEX sqlite_autoindex_Author_1 (id=?) e SEARCH r USING INDEX sqlite_autoindex_Reference_1 (id=?): un index nested loop (una scansione e due ricerche per chiave primaria, create implicitamente da PRIMARY KEY). È lo strumento per controllare che un indice sia davvero usato.

Progettazione fisica

Ultima fase del progetto di una base di dati, dopo la progettazione concettuale e logica (Ristrutturazione dello schema ER e traduzione nel modello relazionaleLa progettazione logica ha due passi. 1) Ristrutturazione dello schema ER, per togliere i costrutti che il modello relazionale non esprime: analisi delle ridondanze (attributi derivati e cicli di relazioni, decisa con volume dei dati e carico, tabelle di frequenza e accessi), eliminazione degli attributi multivalore (nuova entità), composti (appiattiti o nuova entità), delle IS-A (relazione con partecipazione (1,1) dalla sottoclasse) e delle generalizzazioni (opzione 1 relazioni, 2 accorpamento nel padre con attributo discriminante, 3 accorpamento nei figli, solo se completa e disgiunta), scelta degli identificatori principali (niente cicli di identificazione esterna), riscrittura dei vincoli esterni, partizionamento e accorpamento di entità. 2) Traduzione quasi automatica nel modello relazionale: ogni entità diventa una relazione con la chiave primaria data dall'identificatore principale (anche esterno); una relazione molti-a-molti diventa una relazione con le chiavi delle entità; una uno-a-molti con partecipazione obbligatoria si fonde con l'entità dal lato max 1 con una chiave esterna; con partecipazione opzionale si fonde (NULL) o si crea una relazione a parte; le uno-a-uno si fondono sul lato con partecipazione obbligatoria; le ternarie seguono la stessa logica con le chiavi di tutte le entità.Ristrutturazione dello schema ER e traduzione nel modello relazionale →). Partendo dallo schema logico e dal carico di lavoro (le operazioni, la loro frequenza e la priorità, già stimate nella ristrutturazione dello schema ER) si decide:

  1. organizzazione di ogni tabella (heap, ordinata, hash, B-tree);
  2. indici: candidati naturali sono le colonne usate in WHERE con uguaglianza o intervallo, nei join (chiavi esterne), in GROUP BY e ORDER BY; per ogni indice si confronta il guadagno sulle letture con il costo sulle scritture e lo spazio;
  3. eventuale denormalizzazione (attributi derivati, tabelle unite, ridondanza controllata): velocizza le letture ma riporta le anomalie e il lavoro di mantenere coerente la ridondanza (Dipendenze funzionali e normalizzazione - BCNF e terza forma normaleLa qualità di uno schema relazionale si misura con quattro linee guida informali: semantica chiara degli attributi (una relazione per un'entità o associazione), niente ridondanza che causi anomalie di inserimento, cancellazione e modifica, pochi NULL sistematici, nessuna tupla spuria dopo un join (si unisce solo su chiavi primarie ed esterne). La normalizzazione verifica queste proprietà con le dipendenze funzionali X -> Y (se due tuple coincidono su X coincidono su Y), usando le regole di Armstrong (riflessiva, aumento, transitiva) e la chiusura X+ per trovare chiavi. 1NF: attributi atomici (si tolgono multivalore e relazioni annidate). 2NF: ogni attributo non primo dipende completamente da ogni chiave (niente dipendenze parziali). 3NF: nessun attributo non primo dipende transitivamente da una chiave; equivalentemente per ogni X -> A, X è superchiave oppure A è primo. BCNF: per ogni X -> A non banale X è superchiave. Si ottiene scomponendo senza perdere informazione (join senza perdita se l'intersezione determina una delle parti); la 3NF si ottiene sempre mantenendo le dipendenze, la BCNF a volte le perde. Un buon schema ER tradotto è già normalizzato.Dipendenze funzionali e normalizzazione - BCNF e terza forma normale →);
  4. altre scelte del DBMS: partizionamento delle tabelle grandi, viste materializzate per aggregazioni costose, parametri della memoria.

Si verifica con EXPLAIN sulle query vere. Un indice inutilizzato è solo un costo; il carico cambia nel tempo, quindi la progettazione fisica si rivede.

Accesso programmatico

Le applicazioni (un sito, un'app, un programma di calcolo) non lavorano dalla console SQL: devono interrogare e modificare i dati dal codice. SQL da solo non basta: non ha interfaccia utente, né cicli e strutture generali di un linguaggio ospite. Il problema di fondo è l'impedance mismatch (disallineamento): SQL lavora su insiemi di righe di tipi propri (NUMERIC, DATE, NULL), il linguaggio ospite su variabili e oggetti, riga per riga.

Approcci

Approccio Come funziona Esempi
SQL incorporato (embedded) le istruzioni SQL sono scritte nel codice ospite (EXEC SQL ...) e un precompilatore le traduce in chiamate; si usano variabili ospiti e cursori SQL embedded in C, SQLJ
Interfaccia a livello di chiamata (call-level interface) l'applicazione chiama funzioni di una libreria passando l'SQL come stringa, e legge i risultati ODBC, JDBC, libpq di PostgreSQL, DB-API di Python (sqlite3, psycopg)
ORM (object-relational mapping) tabelle mappate su classi, righe su oggetti; la libreria genera l'SQL SQLAlchemy, Hibernate
Routine memorizzate la logica sta nel DBMS (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 →) e l'applicazione la chiama (CALL) PL/pgSQL

Lo schema è sempre lo stesso: connessione →\to cursore →\to execute (con parametri) →\to fetch delle righe →\to commit o rollback →\to chiusura.

python
import sqlite3                                   # DB-API: con PostgreSQL sarebbe psycopg
conn = sqlite3.connect("riferimenti.db")         # 1. connessione
cur = conn.cursor()                              # 2. cursore
cur.execute("SELECT id, title, year FROM Reference WHERE publisher = ? ORDER BY year",
            ("PrenticeHall",))                   # 3. execute con parametro (? = segnaposto)
for ref_id, title, year in cur.fetchall():       # 4. fetch (fetchone / fetchmany / fetchall)
    print(ref_id, title, year)
conn.close()
  • cursor.description dà i nomi delle colonne; fetchone() restituisce una riga alla volta (utile per risultati grandi), fetchall() tutte. NULL diventa None, NUMERIC diventa Decimal, DATE diventa datetime.date.
  • Il segnaposto cambia con il driver: ? per sqlite3 e JDBC, %s per psycopg, :nome per i parametri con nome.
  • Si chiudono cursore e connessione; in un server si usa un pool di connessioni, perché aprirne una per richiesta è costoso.

SQL injection e query parametrizzate

L'errore più grave è costruire l'SQL concatenando l'input dell'utente:

python
q = f"SELECT username FROM Utente WHERE username = '{u}' AND password = '{p}'"   # VIETATO

Con u = "alice" e p = "x' OR '1'='1" la stringa diventa ... WHERE username = 'alice' AND password = 'x' OR '1'='1', e per la precedenza di AND su OR la condizione è sempre vera: la query restituisce tutti gli utenti (provato: alice e bob). Con u = "' OR 1=1 --" il commento -- taglia il resto. Il danno può arrivare alla cancellazione di tabelle, se il driver accetta più istruzioni in una stringa.

La difesa è la query parametrizzata (prepared statement): SQL e dati viaggiano separati; il driver non interpreta mai i valori come SQL.

python
cur.execute("SELECT username FROM Utente WHERE username = ? AND password = ?", (u, p))

Con lo stesso input x' OR '1'='1 la query parametrizzata restituisce zero righe (provato); con le credenziali giuste restituisce alice. Vantaggi: sicurezza, e il piano di esecuzione si può riusare. I nomi di tabelle e colonne non si possono parametrizzare: se dipendono dall'utente si controllano contro un elenco fisso. Le password non si salvano in chiaro (come nel database del laboratorio, che è solo didattico) ma con un hash lento e salato.

Transazioni dal codice

Per default il driver apre una transazione alla prima istruzione di modifica e non la conferma: serve commit(); in caso di errore, rollback(). In Python:

python
try:
    with conn:                                   # commit alla fine, rollback se c'è un'eccezione
        conn.execute("UPDATE Conto SET saldo = saldo + 150 WHERE id = 2")
        conn.execute("UPDATE Conto SET saldo = saldo - 150 WHERE id = 1")   # viola CHECK (saldo >= 0)
except sqlite3.IntegrityError as e:
    print("annullata:", e)                       # i saldi tornano (100, 50): atomicità

(Provato: dopo l'errore i saldi sono invariati; con importi leciti il commit li porta a (70,80)(70,80) dopo uno spostamento di 30.) Le transazioni di una richiesta web devono essere brevi e non aspettare l'utente.

Altri problemi

  • N+1 query: leggere una lista (1 query) e poi, per ogni riga, una query di dettaglio (NN query): meglio un join o una query unica con IN.
  • Portare al client solo le righe e le colonne che servono: filtrare e aggregare nel DBMS, non nell'applicazione.
  • Gestione degli errori (vincoli violati, connessione persa) e chiusura delle risorse.
  • Il codice SQL va tenuto in un punto solo (livello di accesso ai dati), non sparso nella logica.

Applicazioni web

Un'applicazione web a dati è tipicamente a tre livelli (three-tier):

  1. presentazione: il browser (HTML, CSS, JavaScript);
  2. logica applicativa: il server web e l'applicazione (Python, Java, PHP, JavaScript...);
  3. dati: il DBMS.

Il browser parla con il server in HTTP, un protocollo senza stato: ogni richiesta è indipendente (HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding →, Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →). Il server genera la risposta in modo dinamico: legge i parametri della richiesta (query string o corpo di un modulo), esegue query parametrizzate sul DBMS e costruisce HTML oppure JSON (CGI e applicazioni web dinamicheLa Common Gateway Interface (CGI, RFC 3875) e' l'interfaccia fra un server web e un programma esterno che genera la risposta: per ogni richiesta il server fa fork ed exec del programma, gli passa i dati con le meta-variabili d'ambiente (REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, CONTENT_TYPE, SCRIPT_NAME, PATH_INFO, SERVER_*, REMOTE_ADDR, HTTP_* per gli header) e con lo standard input (corpo della POST, CONTENT_LENGTH byte); il programma scrive sullo standard output header CGI (Content-Type obbligatorio, Status, Location), una riga vuota e il corpo, e il server li trasforma in una risposta HTTP completa; i punti delicati sono pipe e dup2, la chiusura delle estremita' inutilizzate, il timeout, i limiti di dimensione e la sicurezza (injection, path traversal, header Proxy, shellshock); il costo di un processo per richiesta ha portato a FastCGI, ai moduli del server e ai server applicativi.CGI e applicazioni web dinamiche →, Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →). Lo stato dell'utente (chi è, il carrello) si ricostruisce con sessioni e cookie.

Esempio minimo (WSGI, libreria standard di Python): un servizio che restituisce in JSON i riferimenti di un editore, GET /?publisher=PrenticeHall.

python
import json
from urllib.parse import parse_qs

def app(environ, start_response):
    params = parse_qs(environ.get("QUERY_STRING", ""))
    publisher = params.get("publisher", [None])[0]
    if publisher is None:
        start_response("400 Bad Request", [("Content-Type", "application/json")])
        return [json.dumps({"errore": "manca publisher"}).encode()]
    rows = conn.execute("SELECT id, title, year FROM Reference WHERE publisher = ? ORDER BY year",
                        (publisher,)).fetchall()                    # query parametrizzata
    body = json.dumps([{"id": i, "title": t, "year": y} for i, t, y in rows], ensure_ascii=False)
    start_response("200 OK", [("Content-Type", "application/json; charset=utf-8")])
    return [body.encode()]

Provato chiamando app direttamente: con publisher=PrenticeHall risponde 200 OK e tre riferimenti (anni 1996, 1999, 2001); senza parametro 400; con un input d'attacco (x' OR '1'='1) 200 con la lista vuota, perché il valore è trattato come testo.

Punti di attenzione per un'applicazione vera: SQL injection (query parametrizzate), XSS (si fa l'escape dell'input prima di scriverlo in HTML), autenticazione e autorizzazione (utenti del DBMS con privilegi minimi: la web app non deve connettersi come amministratore), pool di connessioni, transazioni brevi, indici sulle colonne dei filtri più frequenti.

Errori tipici

  • Concatenare input nelle stringhe SQL invece di usare parametri.
  • Dimenticare commit() (le modifiche non sono salvate) o lasciare transazioni aperte.
  • Fare nell'applicazione ciò che farebbe meglio il DBMS (filtrare e aggregare lato client), o il contrario (logica pesante nelle routine).
  • Una query per riga (N+1N+1).
  • Scegliere gli indici "a caso", senza EXPLAIN e senza guardare il costo sulle scritture.
  • Dimenticare che l'ordine dei join o la stima delle statistiche (non aggiornate) cambiano di molto il costo.
  • Confondere i livelli ANSI/SPARC (che cosa vede l'utente) con i componenti del DBMS (come lavora dentro).

Domande d'esame

1. Elencare le fasi di elaborazione di una query SQL in un DBMS e dire che cosa decide l'ottimizzatore. Traccia. Parsing, riscrittura algebrica (selezioni e proiezioni in basso, join al posto di prodotto + selezione), scelta del piano (algoritmo e metodo di accesso per ogni operatore, con le statistiche del catalogo), esecuzione. L'ottimizzatore sceglie ordine dei join, uso degli indici, algoritmo di join minimizzando gli accessi a blocchi.

2. Con bR=1000b_R=1000, bS=100b_S=100 e 12 pagine di buffer, confrontare il costo di un block nested loop e di un hash join. Traccia. Block nested loop con SS esterna: 100+⌈100/10⌉⋅1000=10 100100+\lceil100/10\rceil\cdot1000=10\,100; hash join: 3(1000+100)=33003(1000+100)=3300.

3. Che cos'è una SQL injection e come ci si difende? Traccia. Costruendo l'SQL con l'input dell'utente si permette di cambiare la struttura della query (x' OR '1'='1 rende vera la condizione). Si usano query parametrizzate, che separano SQL e dati; si limitano i privilegi dell'utente del DBMS.

Versione ripasso

Architettura del DBMS: client, poi parser (usa il catalogo), ottimizzatore (usa le statistiche), esecutore, metodi di accesso (heap, ordinato, hash, B-tree, indici), buffer manager, blocchi su disco. Accanto: gestore delle transazioni (lock, log) e catalogo. Non confondere con i livelli ANSI/SPARC (Basi di dati e DBMS - architettura ANSI-SPARC e indipendenza dei datiUna base di dati (database, DB) è una collezione integrata e persistente di dati correlati che rappresenta un frammento di mondo reale (mini-mondo); un DBMS è il software generico che permette di definirla (schema, nel catalogo come metadati), costruirla e manipolarla (interrogazioni e aggiornamenti). Rispetto a una raccolta di file il DBMS riduce ridondanza e incoerenza, condivide i dati tra applicazioni con controllo della concorrenza (isolamento, atomicità), garantisce durabilità, sicurezza (autenticazione e autorizzazione) ed efficienza. L'architettura ANSI/SPARC ha tre livelli: schema esterno (una vista per applicazione), logico (rappresentazione integrata, indipendente dalla memorizzazione), interno (strutture fisiche); da qui l'indipendenza fisica (cambia l'interno senza toccare logico ed esterno) e logica (cambia il logico senza toccare le applicazioni). Attori: sviluppatori del DBMS, DBA, progettisti di basi di dati e di applicazioni, utenti parametrici e occasionali.Basi di dati e DBMS - architettura ANSI-SPARC e indipendenza dei dati →).

Costo: si misura in accessi a blocchi (Memoria di massa e scheduling del discoLa memoria di massa è fatta di dischi rigidi (HDD: piatti, tracce, cilindri, settori) e memorie non volatili NAND (SSD); tempo di accesso HDD = seek + latenza rotazionale (mezzo giro, 30/RPM secondi), tempo di I/O = accesso + trasferimento + overhead del controller; i blocchi sono indirizzati con un LBA lineare; per l'HDD si minimizza il seek con FCFS, SSTF (rischio di starvation), SCAN (ascensore) e C-SCAN (più uniforme), con le varianti LOOK e C-LOOK e il deadline di Linux; per le NVM contano FTL, garbage collection, overprovisioning e wear leveling; il sistema operativo formatta (fisico e logico), partiziona, gestisce il boot e lo spazio di swap; parità ed ECC rilevano e correggono errori; il RAID usa striping, mirroring e parità (livelli 0, 1, 4, 5, 6, 10) per più velocità e affidabilità, con MTTDL = 1/PDL.Memoria di massa e scheduling del disco →). Con nRn_R record e ff record per blocco, bR=⌈nR/f⌉b_R=\lceil n_R/f\rceil. Ricerca per uguaglianza: heap bR/2b_R/2 in media, ordinato log⁡2bR\log_2 b_R, hash circa 1, B-tree log⁡FbR\log_F b_R.

Gestione delle interrogazioni: parsing, riscrittura algebrica, scelta del piano (algoritmo e metodo di accesso per ogni operatore, costo stimato con le statistiche), esecuzione.

  • Selezioni e proiezioni il più in basso possibile; σR.A=S.B(R×S)=R⋈A=BS\sigma_{R.A=S.B}(R\times S)=R\bowtie_{A=B}S.
  • Join commutativo e associativo: conta l'ordine. Si parte dai join più selettivi.
  • Esempio: πtitle(Reference⋈publisher=idσname=′McGraw-Hill′(Publisher))\pi_{\text{title}}\big(\text{Reference}\bowtie_{\text{publisher}=\text{id}}\sigma_{\text{name}='\text{McGraw-Hill}'}(\text{Publisher})\big) filtra prima e dà lo stesso risultato dell'espressione con il join fatto per primo, ma costa molto meno.

Selezione: scansione completa (bRb_R accessi) oppure indice, conveniente solo se la condizione è selettiva.

Join R⋈A=BSR\bowtie_{A=B}S, con RR esterna, MM pagine di buffer:

Algoritmo Costo (accessi)
Nested loop bR+nR⋅bSb_R+n_R\cdot b_S
Block nested loop bR+⌈bR/(M−2)⌉⋅bSb_R+\lceil b_R/(M-2)\rceil\cdot b_S
Index nested loop bR+nR⋅cb_R+n_R\cdot c (cc = costo di una ricerca per indice)
Sort-merge ordinamento + bR+bS+\ b_R+b_S
Hash join circa 3(bR+bS)3(b_R+b_S)

Esempio con bR=1000b_R=1000, bS=100b_S=100, nR=10 000n_R=10\,000, M=12M=12: nested loop 1 001 0001\,001\,000; block nested loop con RR esterna 11 00011\,000; con SS esterna (la più piccola) 10 10010\,100; hash join 33003300. Tre ordini di grandezza di differenza: per scegliere serve il catalogo.

EXPLAIN: PostgreSQL EXPLAIN [ANALYZE], SQLite EXPLAIN QUERY PLAN. Su tre tabelle del database bibliografico mostra SCAN seguita da due SEARCH ... USING INDEX su chiave primaria: un index nested loop.

Progettazione fisica: da schema logico e carico di lavoro (frequenza e priorità delle operazioni) si decide:

  1. organizzazione di ogni tabella (heap, ordinata, hash, B-tree);
  2. indici su colonne di WHERE (uguaglianza o intervallo), chiavi esterne nei join, GROUP BY, ORDER BY; si confronta il guadagno in lettura con il costo in scrittura e lo spazio;
  3. eventuale denormalizzazione: veloce in lettura, ma riporta le anomalie (Dipendenze funzionali e normalizzazione - BCNF e terza forma normaleLa qualità di uno schema relazionale si misura con quattro linee guida informali: semantica chiara degli attributi (una relazione per un'entità o associazione), niente ridondanza che causi anomalie di inserimento, cancellazione e modifica, pochi NULL sistematici, nessuna tupla spuria dopo un join (si unisce solo su chiavi primarie ed esterne). La normalizzazione verifica queste proprietà con le dipendenze funzionali X -> Y (se due tuple coincidono su X coincidono su Y), usando le regole di Armstrong (riflessiva, aumento, transitiva) e la chiusura X+ per trovare chiavi. 1NF: attributi atomici (si tolgono multivalore e relazioni annidate). 2NF: ogni attributo non primo dipende completamente da ogni chiave (niente dipendenze parziali). 3NF: nessun attributo non primo dipende transitivamente da una chiave; equivalentemente per ogni X -> A, X è superchiave oppure A è primo. BCNF: per ogni X -> A non banale X è superchiave. Si ottiene scomponendo senza perdere informazione (join senza perdita se l'intersezione determina una delle parti); la 3NF si ottiene sempre mantenendo le dipendenze, la BCNF a volte le perde. Un buon schema ER tradotto è già normalizzato.Dipendenze funzionali e normalizzazione - BCNF e terza forma normale →);
  4. partizioni, viste materializzate, parametri di memoria.

Si verifica con EXPLAIN. Un indice inutilizzato è solo un costo.

Accesso programmatico: impedance mismatch tra insiemi di righe SQL e variabili del linguaggio ospite. Approcci: SQL incorporato (precompilatore, cursori), call-level interface (ODBC, JDBC, DB-API di Python), ORM, routine memorizzate (CALL).

Flusso: connessione, cursore, execute con parametri, fetch, commit o rollback, chiusura. Segnaposto: ? in sqlite3 e JDBC, %s in psycopg.

python
cur.execute("SELECT id, title, year FROM Reference WHERE publisher = ? ORDER BY year", ("PrenticeHall",))
for ref_id, title, year in cur.fetchall():
    print(ref_id, title, year)

SQL injection: costruendo la stringa con f"... WHERE username = '{u}' AND password = '{p}'" e p = "x' OR '1'='1" la condizione diventa sempre vera e torna tutti gli utenti. Difesa: query parametrizzate (SQL e dati separati), con execute(..., (u, p)) la stessa password dà zero righe. Tabelle e colonne non si parametrizzano: si controllano contro un elenco fisso. Password salvate con hash lento e salato, privilegi minimi per l'utente del DBMS.

Transazioni dal codice: il driver non conferma da solo; serve commit(), in caso di errore rollback(). Con with conn: si ha commit alla fine e rollback se c'è un'eccezione. Esempio: il trasferimento che viola CHECK (saldo >= 0) lascia i saldi invariati (atomicità). Transazioni brevi, che non aspettano l'utente.

Altri problemi: N+1N+1 query (meglio un join o una query con IN); filtrare e aggregare nel DBMS e non nel client; gestione degli errori; pool di connessioni; SQL in un solo livello di accesso ai dati.

Applicazioni web: tre livelli (browser, server applicativo, DBMS). HTTP è senza stato: sessioni e cookie ricostruiscono lo stato. Il server legge i parametri, esegue query parametrizzate e risponde con HTML o JSON (CGI e applicazioni web dinamicheLa Common Gateway Interface (CGI, RFC 3875) e' l'interfaccia fra un server web e un programma esterno che genera la risposta: per ogni richiesta il server fa fork ed exec del programma, gli passa i dati con le meta-variabili d'ambiente (REQUEST_METHOD, QUERY_STRING, CONTENT_LENGTH, CONTENT_TYPE, SCRIPT_NAME, PATH_INFO, SERVER_*, REMOTE_ADDR, HTTP_* per gli header) e con lo standard input (corpo della POST, CONTENT_LENGTH byte); il programma scrive sullo standard output header CGI (Content-Type obbligatorio, Status, Location), una riga vuota e il corpo, e il server li trasforma in una risposta HTTP completa; i punti delicati sono pipe e dup2, la chiusura delle estremita' inutilizzate, il timeout, i limiti di dimensione e la sicurezza (injection, path traversal, header Proxy, shellshock); il costo di un processo per richiesta ha portato a FastCGI, ai moduli del server e ai server applicativi.CGI e applicazioni web dinamiche →, Web service, XML, JSON, SOAP e RESTUn web service e' un servizio software accessibile via rete da altri programmi, con messaggi in formato standard su HTTP; i formati di dati sono XML (marcatori annidati, attributi, namespace, validazione con XSD) e JSON (RFC 8259: oggetti, array, stringhe, numeri, true, false, null; piu' compatto e diretto per i linguaggi di programmazione); SOAP e' un protocollo di messaggi XML (Envelope con Header facoltativo e Body, Fault per gli errori) inviati di solito con una POST HTTP e descritti da un file WSDL; REST e' uno stile architetturale (Fielding) in cui il servizio espone risorse identificate da URI e le manipola con i metodi HTTP: GET legge, POST crea, PUT sostituisce, PATCH modifica, DELETE elimina, con codici di stato significativi (200, 201 con Location, 204, 400, 404, 409), senza stato sul server, con cache e interfaccia uniforme.Web service, XML, JSON, SOAP e REST →). Attenzione a injection, XSS (escape dell'input in HTML) e autenticazione.

Errori tipici

  • Concatenare l'input nelle stringhe SQL invece di usare parametri.
  • Dimenticare commit() o lasciare transazioni aperte.
  • Una query per riga (N+1N+1).
  • Indici scelti a caso, senza EXPLAIN e senza costo in scrittura.
  • Confondere i livelli ANSI/SPARC con i componenti del DBMS.

Domande d'esame (tracce)

  • Fasi di una query: parsing, riscrittura, scelta del piano, esecuzione. L'ottimizzatore decide ordine dei join, indici e algoritmo di join, minimizzando gli accessi a blocchi.
  • bR=1000b_R=1000, bS=100b_S=100, 12 buffer: block nested loop con SS esterna =10 100=10\,100; hash join =3300=3300.

Prerequisiti: Algebra relazionale - operatori di base e joinLe operazioni sul modello relazionale sono di aggiornamento (inserimento, modifica, cancellazione, che possono violare i vincoli di integrità: si rifiuta, si corregge, si propaga con cascade o si imposta NULL/default) e di interrogazione (funzioni che da un'istanza del database producono una relazione). L'algebra relazionale è il linguaggio procedurale delle interrogazioni: gli operatori primitivi sono unione, differenza, prodotto, rinomina ρ, selezione σ e proiezione π; derivati sono intersezione, theta-join, equi-join, join naturale. Unione, intersezione e differenza richiedono relazioni compatibili all'unione (stessi attributi e domini). La selezione è una decomposizione orizzontale, la proiezione verticale (commutano solo se gli attributi della selezione sono nella proiezione). Il theta-join è σ_φ(R1×R2), il join naturale equivale a un equi-join sugli attributi omonimi seguito da una proiezione; la sua cardinalità è tra 0 e |R1||R2|, ≤|R1| se gli attributi comuni sono chiave di R2, =|R1| se sono anche chiave esterna di R1 con vincolo di integrità referenziale. Le tuple senza corrispondente sono "dangling". I valori NULL richiedono condizioni esplicite IS NULL / IS NOT NULL, perché σ_{A>40} ∪ σ_{A≤40} non restituisce le tuple con A nullo.Algebra relazionale - operatori di base e join →, 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 →. Seguito: 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 →.

Teoria collegata