Negli anni ‘80 e ‘90, quando ancora i DVD non erano stati inventati, nelle case della maggior parte delle famiglie faceva da padrone il videoregistratore. Per la prima volta, questo incredibile oggetto elettronico permetteva di registrare e rivedere in un secondo momento qualunque programma televisivo (il segnale televisivo al tempo era analogico e non esistevano i programmi criptati), memorizzava i video all’interno di grosse scatolone, che per alcuni decenni hanno riempito scaffali e librerie degli appartamenti di tutto il mondo: le famose videocassette.
Quando si iniziava ad avere all’incirca tre o quattro mobili pieni, sorgeva il problema di sapere quante videocassette si aveva in casa, quanti e quali film vi erano memorizzati all’interno di ognuna, quale era il genere maggiormente presente, di quale attore si aveva tutti i film, la serie completa delle trilogie preferite, ma soprattutto in quale ripiano di quale scaffale si poteva trovare una specifica puntata del cartone animato o del telefilm preferito (ancora il termine “serie” non veniva usato).
Per le persone meno fortunate che a casa non avevano ancora un PC, (allora non c’era un PC in tutte le case come adesso) non rimaneva che aprire il quaderno dedicato e scrivere su carta, creare delle tabelle con all’interno tante colonne, che permettevano almeno di poter rispondere a queste domande a colpo d’occhio. Se si riusciva a far stare una cinquantina di titoli in ogni pagina, scrivendo molto piccolo grazie all’occhio allenato, si riusciva subito a trovare la riga della tabella, che gli permetteva di sapere dove allungare la mano nell’array di videocassette della libreria per estrarre quella giusta.
Inoltre, con i nomi degli attori principali ben ordinati, nelle varie colonne su qualche centinaio di titoli, il cervello di quelli maggiormente fanatici della propria collezione era in grado di contare in pochi istanti, quanti film avevano per ogni attore e se di un certo attore avevano tutti i film. Quando, nei primi anni ‘90, nelle case iniziarono a entrare i primi PC IBM 286, i più fortunati che avevano Lotus, uno dei più importanti antenati dei moderni fogli di calcolo, riuscivano a conservare queste informazioni all’interno di tabelle elettroniche, che gli permettevano di fare queste stesse interrogazioni, su quantità di dati molto più grandi in un tempo infinitamente più piccolo.
Il primo contatto con le infinite possibilità offerte dalla digitalizzazione delle informazioni, all’interno di strutture ben definite come le tabelle, lo si aveva infine quando ci si recava nel luogo di culto per eccellenza delle videocassette: la videoteca. Sebbene l’operatore avesse a che fare con stanze piene di videocassette, quando impostava la domanda in modo corretto nel terminale, dove tutte le informazioni erano state precedentemente caricate, all’interno di primitive basi di dati, era in grado di rispondere subito e con precisione alle richieste dei clienti che cercavano film di tutti i tipi, permettendo loro di salvare il sabato sera.
Come è possibile interrogare una base di dati per estrarne le informazioni che ci servono?
Se i dati sono strutturati in modo opportuno, è sufficiente conoscere il linguaggio giusto, che dipende dal particolare sistema informatico utilizzato per gestire la base di dati; il linguaggio SQL, utilizzato nei server MySQL, è forse il più importante di questi linguaggi, e a partire da esso sono stati definiti diversi linguaggi derivati (pgSQL – utilizzato da postgreSQL, Transact-SQL – utilizzato da Microsoft SQL Server).
Il linguaggio SQL si basa su un insieme relativamente piccolo di istruzioni, che combinate tra loro in modo opportuno, mostrano appieno la potenza di questo linguaggio. Le prime due istruzioni con cui si fa tipicamente conoscenza, quando si inizia a studiare il SQL, sono le istruzioni SELECT e FROM, utilizzate per selezionare (SELECT) i campi desiderati dalla (FROM) tabella di interesse; ognuna di queste istruzioni quindi deve essere seguita dalle informazioni necessarie per comporre l’interrogazione completa da sottomettere al server. Ad esempio, se si vuole accedere ai campi nome e cognome di una tabella che si chiama anagrafica, la sintassi corretta sarà semplicemente:
SELECT nome, cognome FROM anagrafica;
Il punto e virgola è obbligatorio alla fine di ogni istruzione. Esistono dei caratteri speciali che hanno dei ruoli particolari nella sintassi SQL; ad esempio: il carattere * permette di selezionare in modo compatto tutte le colonne di una tabella, senza neanche porsi il problema di quali sono i loro nomi:
SELECT * FROM anagrafica;
La selezione delle colonne prende anche il nome di filtro verticale mentre la selezione sulle righe prende anche il nome di filtro orizzontale, ed è uno dei primi punti dove si evidenzia la potenza del linguaggio. Per selezionare, ad esempio, tutte le righe relative a delle persone che sono nate a Cagliari dopo il 1990 si deve usare la terza istruzione fondamentale del linguaggio SQL, l’istruzione WHERE, in combinazione con gli operatori logici AND/OR/NOT che permettono di realizzare le infinite combinazioni tra asserzioni del linguaggio naturale:
SELECT * FROM anagrafica WHERE luogo_di_nascita =='Cagliari' and data_di_nascita >= '01-01-1991';
La query di sopra suppone ovviamente che il formato del dato utilizzato per il campo data_di_nascita (è bene che i nomi dei campi non abbiano degli spazi) sia il tipo date, altrimenti otterremmo dei risultati diversi da quelli attesi. Se tutto si limitasse a selezionare righe e colonne, forse non sarebbe tanto interessante studiare questo linguaggio, ma la realtà è che con esso possiamo sia applicare degli operatori di aggregazione e raggruppamento che ci permettono di estendere di molto le possibilità di interrogazione della base di dati. Ad esempio: se volessimo sapere quante sono le persone della tabella anagrafica che corrispondono alla selezione di sopra, sarebbe sufficiente applicare l’operatore di aggregazione COUNT() al campo indicato nella selezione, in questo caso il carattere speciale *:
SELECT COUNT (*) FROM anagrafica WHERE luogo_di_nascita =='Cagliari' and data_di_nascita >= '01-01-1991';
Il risultato in questo caso non sarà più una selezione di righe della tabella ma un valore numerico di sintesi dei dati. Se poi volessimo fare questo conteggio raggruppando i risultati per classe di età rispetto alla data attuale, in modo da sapere quanti dei nati dopo il 1990 attualmente hanno 1 anno, 2 anni, 3 anni, etc.., sarebbe sufficiente aggiungere l’operatore di raggruppamento GROUP BY, indicando quindi il campo eta (i caratteri con gli accenti non possono essere usati nei nomi dei campi) come il campo rispetto al quale effettuare il raggruppamento, dopo la clausola WHERE:
SELECT eta, COUNT (*) FROM anagrafica WHERE luogo_di_nascita == 'Cagliari' and data_di_nascita >= '01-01-1991' GROUP BY eta;
Si noti che l’aggiunta del GROUP BY alla fine ha richiesto anche l’aggiunta dello stesso campo di raggruppamento eta tra i campi gestiti dall’operatore SELECT, perché nella risposta alla query, è necessario avere l’informazione rispetto alla quale viene applicata l’operazione di aggregazione COUNT() per sapere quante persone tra quelle selezionate hanno un anno, quante persone hanno due anni, quante persone hanno tre anni e così via.
Un altro fattore importante, per descrivere la potenza del linguaggio è la possibilità di unire le informazioni memorizzate in tabelle differenti, per rispondere a domande sempre più complesse che riguardano più entità. I database SQL infatti si basano sul concetto di database relazionale, per cui le diverse entità che descrivono la base di dati, sono memorizzate in tabelle diverse, che sono messe in relazione tra loro dalla presenza di almeno una colonna in comune, grazie alla quale il gestore del database è in grado di ricostruire informazioni più complesse di quelle memorizzate nelle singole tabelle.
Ad esempio: se la nostra base di dati contenesse anche la tabella descrittiva, della situazione economica delle persone presenti nella tabella anagrafica potremmo sapere quali sono stati i guadagni associati a ogni persona interconnettendo le righe della tabella anagrafica con quelle della tabella pagamenti, se nella colonna pagamenti è presente un campo che permette di risalire in modo univoco alle persone presenti nella tabella anagrafica, come può essere, un campo che contiene il codice fiscale, senza questo campo di collegamento infatti sarebbe impossibile ricostruire in modo corretto le informazioni che sono state separate in diverse tabelle. Dal momento che in questi casi si opera su più di una tabella, è possibile risolvere eventuali ambiguità tra nomi di campi identici che si trovano su più tabelle, utilizzando la notazione con il punto, grazie alla quale ogni campo è individuato in modo univoco, indicando prima il nome della tabella a cui appartiene unito al nome del campo stesso, tramite il carattere del punto. L’istruzione che permette di effettuare queste ricostruzioni tra righe di diverse tabelle è l’istruzione JOIN che necessita di avere l’informazione del campo di collegamento tra le tabelle:
SELECT anagrafica.nome, anagrafica.cognome, pagamenti.primo_trimestre FROM anagrafica JOIN pagamenti ON anagrafica.codice_fiscale = pagamenti.codice_fiscale;
Le operazioni di JOIN sono di diverso tipo (INNER, OUTER, LEFT, RIGHT) e permettono di realizzare tutte le tipiche operazioni tra insiemi, per poter quindi ottenere solo l’insieme intersezione tra le tabelle, oppure l’intersezione più i soli elementi dell’una o dell’altra tabella, ma anche la disgiunzione tra le tabelle, cioè gli elementi non comuni tra le tabelle, in modo da ampliare ulteriormente la capacità di interrogazione della base di dati.
Alcune interrogazioni richiedono di estrarre alcuni valori esistenti nella base di dati per poi usarli in una condizione di confronto; per fare questo si utilizzano contemporaneamente due interrogazioni facendo in modo che una sia all’interno (query interna) della clausola WHERE dell’altra (query esterna); questo fa sì che venga prima processata la query interna e che il suo risultato sia utilizzato come una nuova tabella temporanea per la query esterna. Questo tipo di interrogazioni vengono dette nidificate. Se volessi trovare le persone che hanno l’età superiore a quella dei residenti a Roma dovrei usare la seguente query nidificata:
SELECT * FROM anagrafica WHERE eta > ( SELECT MAX (eta) FROM anagrafica WHERE residenza = 'Roma' );
nella quale il gestore del database esegue prima la select interna trovando il valore massimo di eta delle persone residenti a Roma, ad esempio 25, per sostituirlo quindi dietro le quinte alla query esterna semplificandola in questo modo:
SELECT * FROM anagrafica WHERE eta > (25);
Se si pensa al fatto che il livello di nidificazione, quindi il numero di sotto-query, può essere grande a piacere, questo espande a dismisura la quantità di interrogazioni che possiamo sottomettere al database.
Quanto visto in questo articolo è solo una piccola, ma importante parte del mondo SQL. Si potrebbe estendere l’argomento, citando le query correlate, nelle quali ogni esecuzione di una sotto-query è correlata al valore di uno o più campi delle righe candidate nella query principale, i vincoli di chiave primaria ed esterna, che permettono rispettivamente di identificare univocamente le righe delle tabelle dove sono definite e di garantire l’integrità referenziale tra due o più tabelle (cioè il fatto che se le righe di due colonne di due tabelle distinte, sono collegate tra loro, allora esiste sempre l’elemento di una tabella puntato da un elemento dell’altra tabella), ma forse già adesso puoi dire con una certa confidenza che hai almeno intuito di che cosa si parla quando si ha a che fare con il linguaggio SQL.