SQL: guida completa dai fondamenti alle query avanzate
SQL (Structured Query Language) è il linguaggio standard per gestire e manipolare database relazionali. È uno dei linguaggi più utilizzati in ambito informatico e rappresenta una competenza fondamentale per sviluppatori, analisti di dati e amministratori di database.
In questo articolo
- Universale: Funziona con tutti i principali DBMS (MySQL, PostgreSQL, SQL Server, Oracle)
- Potente: Permette di gestire enormi quantità di dati in modo efficiente
- Richiesto: Competenza essenziale in molti ruoli IT
- Standardizzato: Sintassi simile tra diversi sistemi di database
Indice della guida
- Concetti fondamentali
- Installazione e configurazione
- Creazione database e tabelle
- Operazioni CRUD
- Query di selezione
- Join e relazioni
- Funzioni aggregate
- Query avanzate
- Stored procedure e funzioni
- Ottimizzazione e prestazioni
- Buone pratiche
Concetti fondamentali
Cosa sono i database relazionali?
Un database relazionale organizza i dati in tabelle collegate tra loro tramite relazioni. Ogni tabella contiene:
- Righe (Record): Singole entità di dati
- Colonne (Campi): Attributi che descrivono le entità
- Chiave Primaria: Identifica univocamente ogni riga
- Chiave Esterna: Collega tabelle diverse
Terminologia di base
1
2
3
4
5
6
7
|
-- Esempio di tabella
CREATE TABLE users (
id INT PRIMARY KEY, -- Chiave primaria
name VARCHAR(100) NOT NULL, -- Campo obbligatorio
email VARCHAR(100) UNIQUE, -- Campo univoco
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
|
Tipi di dati comuni
| Tipo |
Descrizione |
Esempio |
INT |
Numero intero |
42 |
VARCHAR(n) |
Stringa variabile |
'Gianluca' |
TEXT |
Testo lungo |
'Descrizione molto lunga...' |
DATE |
Data |
'2024-11-16' |
TIMESTAMP |
Data e ora |
'2024-11-16 14:30:00' |
DECIMAL(p,s) |
Numero decimale |
99.99 |
BOOLEAN |
Vero/Falso |
TRUE |
Installazione e configurazione
MySQL (consigliato per principianti)
Windows/macOS:
- Scaricare da Sito ufficiale MySQL
- Installare MySQL Server e MySQL Workbench
- Configurare password per l’utente root
Linux (Ubuntu/Debian):
1
2
3
4
5
6
7
8
|
sudo apt update
sudo apt install mysql-server mysql-client
# Configurazione sicurezza
sudo mysql_secure_installation
# Accesso a MySQL
sudo mysql -u root -p
|
PostgreSQL (Alternativa per ambito aziendale)
1
2
3
4
5
|
# Ubuntu/Debian
sudo apt install postgresql postgresql-contrib
# Accesso
sudo -u postgres psql
|
SQLite (per test locale)
1
2
3
4
5
|
# Installazione
sudo apt install sqlite3
# Creare/aprire database
sqlite3 test.db
|
Strumenti GUI consigliati
- MySQL Workbench: GUI ufficiale per MySQL
- phpMyAdmin: Web-based per MySQL
- pgAdmin: GUI per PostgreSQL
- DBeaver: Multi-database tool gratuito
Creazione database e tabelle
Creare un database
1
2
3
4
5
6
7
8
|
-- Creare nuovo database
CREATE DATABASE ecommerce;
-- Selezionare database da usare
USE ecommerce;
-- Vedere tutti i database
SHOW DATABASES;
|
Creare tabelle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
-- Tabella utenti
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
birth_date DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Tabella prodotti
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INT DEFAULT 0,
category_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id)
);
-- Tabella categorie
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT
);
-- Tabella ordini
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
total_amount DECIMAL(10, 2) NOT NULL,
status ENUM('pending', 'confirmed', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
|
Modificare tabelle esistenti
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
-- Aggiungere colonna
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- Modificare colonna
ALTER TABLE products MODIFY COLUMN price DECIMAL(12, 2);
-- Eliminare colonna
ALTER TABLE users DROP COLUMN phone;
-- Aggiungere indice
CREATE INDEX idx_users_email ON users(email);
-- Eliminare tabella
DROP TABLE IF EXISTS old_table;
|
Operazioni CRUD
CREATE - Inserire Dati
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-- Inserimento singolo
INSERT INTO categories (name, description)
VALUES ('Electronics', 'Electronic devices and accessories');
-- Inserimento multiplo
INSERT INTO users (username, email, password_hash, first_name, last_name)
VALUES
('mario.rossi', 'mario@email.com', 'hash123', 'Mario', 'Rossi'),
('anna.verdi', 'anna@email.com', 'hash456', 'Anna', 'Verdi'),
('luca.bianchi', 'luca@email.com', 'hash789', 'Luca', 'Bianchi');
-- Inserimento con subquery
INSERT INTO products (name, price, category_id)
SELECT 'iPhone 15', 999.99, id
FROM categories
WHERE name = 'Electronics';
|
READ - Leggere Dati
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- Selezionare tutti i campi
SELECT * FROM users;
-- Selezionare campi specifici
SELECT username, email, first_name FROM users;
-- Con condizioni
SELECT * FROM products WHERE price > 100;
-- Ordinamento
SELECT * FROM products ORDER BY price DESC;
-- Limitare risultati
SELECT * FROM products LIMIT 10;
-- Paginazione
SELECT * FROM products LIMIT 10 OFFSET 20;
|
UPDATE - Aggiornare Dati
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
-- Aggiornare singolo record
UPDATE users
SET first_name = 'Giuseppe', last_name = 'Verdi'
WHERE id = 1;
-- Aggiornare con condizioni
UPDATE products
SET stock_quantity = stock_quantity - 1
WHERE id = 5 AND stock_quantity > 0;
-- Aggiornare con subquery
UPDATE products
SET category_id = (SELECT id FROM categories WHERE name = 'Books')
WHERE name LIKE '%Manual%';
|
DELETE - Eliminare Dati
1
2
3
4
5
6
7
8
9
10
11
|
-- Eliminare record specifico
DELETE FROM users WHERE id = 10;
-- Eliminare con condizioni
DELETE FROM products WHERE stock_quantity = 0;
-- Eliminare tutti i record (ATTENZIONE!)
DELETE FROM temp_table;
-- Truncate (piu veloce per svuotare tabella)
TRUNCATE TABLE temp_table;
|
Query di selezione
Operatori di Confronto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-- Operatori numerici
SELECT * FROM products WHERE price >= 100;
SELECT * FROM products WHERE stock_quantity BETWEEN 10 AND 50;
SELECT * FROM products WHERE price IN (99.99, 199.99, 299.99);
-- Operatori testuali
SELECT * FROM users WHERE first_name LIKE 'M%'; -- Inizia con M
SELECT * FROM users WHERE email LIKE '%@gmail.com'; -- Finisce con @gmail.com
SELECT * FROM users WHERE first_name LIKE '_ario'; -- Esattamente 5 caratteri, finisce con ario
-- Operatori logici
SELECT * FROM products
WHERE price > 100 AND stock_quantity > 0;
SELECT * FROM products
WHERE category_id = 1 OR category_id = 2;
SELECT * FROM users
WHERE NOT (first_name = 'Mario' OR last_name = 'Rossi');
|
Ordinamento e Raggruppamento
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-- Ordinamento
SELECT * FROM products ORDER BY price ASC, name DESC;
-- Raggrupare dati
SELECT category_id, COUNT(*) as product_count
FROM products
GROUP BY category_id;
-- Filtrare gruppi
SELECT category_id, AVG(price) as avg_price
FROM products
GROUP BY category_id
HAVING AVG(price) > 100;
|
Funzioni per stringhe
1
2
3
4
5
6
7
8
9
10
11
12
|
-- Concatenazione
SELECT CONCAT(first_name, ' ', last_name) as full_name FROM users;
-- Conversione maiuscole/minuscole
SELECT UPPER(name), LOWER(email) FROM users;
-- Lunghezza stringa
SELECT name, LENGTH(name) as name_length FROM products;
-- Substring
SELECT LEFT(name, 10) as short_name FROM products;
SELECT SUBSTRING(email, 1, LOCATE('@', email) - 1) as username FROM users;
|
Funzioni per date
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-- Data corrente
SELECT NOW(), CURDATE(), CURTIME();
-- Estrarre parti della data
SELECT
YEAR(created_at) as year,
MONTH(created_at) as month,
DAY(created_at) as day
FROM users;
-- Calcoli con date
SELECT
name,
created_at,
DATEDIFF(NOW(), created_at) as days_ago
FROM products;
-- Formattazione date
SELECT DATE_FORMAT(created_at, '%d/%m/%Y %H:%i') as formatted_date FROM users;
|
Ordinare per data salvata come stringa
Se hai una colonna stringa con formato yyyymmdd:hhmmss (es. 20240131:235959), ordina convertendo la stringa in data/ora.
MySQL
1
2
3
|
SELECT *
FROM logs
ORDER BY STR_TO_DATE(data_str, '%Y%m%d:%H%i%s') DESC;
|
PostgreSQL
1
2
3
|
SELECT *
FROM logs
ORDER BY to_timestamp(data_str, 'YYYYMMDD:HH24MISS') DESC;
|
SQLite
1
2
3
4
5
6
7
8
9
10
|
SELECT *
FROM logs
ORDER BY (
substr(data_str, 1, 4) || '-' ||
substr(data_str, 5, 2) || '-' ||
substr(data_str, 7, 2) || ' ' ||
substr(data_str, 10, 2) || ':' ||
substr(data_str, 12, 2) || ':' ||
substr(data_str, 14, 2)
) DESC;
|
Come funziona la conversione
L’idea e trasformare la stringa in un valore data/ora ordinabile:
- MySQL:
STR_TO_DATE interpreta la stringa secondo il pattern e restituisce un DATETIME.
- PostgreSQL:
to_timestamp fa la stessa cosa, producendo un timestamp ordinabile.
- SQLite: non ha una funzione equivalente, quindi si ricompone la stringa nel formato
YYYY-MM-DD HH:MM:SS, che SQLite interpreta correttamente per l’ordinamento.
Consiglio: se possibile, salva le date in colonne DATE/DATETIME per evitare conversioni in query.
Join e relazioni
Tipi di Join
INNER JOIN (Intersezione)
1
2
3
4
5
6
7
|
-- Prodotti con nome categoria
SELECT
p.name as product_name,
p.price,
c.name as category_name
FROM products p
INNER JOIN categories c ON p.category_id = c.id;
|
LEFT JOIN (Tutti i record della tabella sinistra)
1
2
3
4
5
6
|
-- Tutti i prodotti, anche senza categoria
SELECT
p.name as product_name,
COALESCE(c.name, 'No Category') as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id;
|
RIGHT JOIN (Tutti i record della tabella destra)
1
2
3
4
5
6
7
|
-- Tutte le categorie, anche senza prodotti
SELECT
c.name as category_name,
COUNT(p.id) as product_count
FROM products p
RIGHT JOIN categories c ON p.category_id = c.id
GROUP BY c.id, c.name;
|
FULL OUTER JOIN (Tutti i record di entrambe le tabelle)
1
2
3
4
5
6
|
-- MySQL non supporta FULL OUTER JOIN nativamente, si usa UNION
SELECT p.name as product_name, c.name as category_name
FROM products p LEFT JOIN categories c ON p.category_id = c.id
UNION
SELECT p.name as product_name, c.name as category_name
FROM products p RIGHT JOIN categories c ON p.category_id = c.id;
|
Join Multiple
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- Query complessa con piu join
SELECT
u.username,
o.id as order_id,
o.total_amount,
o.order_date,
oi.quantity,
p.name as product_name,
p.price
FROM users u
INNER JOIN orders o ON u.id = o.user_id
INNER JOIN order_items oi ON o.id = oi.order_id
INNER JOIN products p ON oi.product_id = p.id
WHERE o.status = 'confirmed'
ORDER BY o.order_date DESC;
|
Self Join
1
2
3
4
5
6
|
-- Tabella employees con manager_id che referenzia id nella stessa tabella
SELECT
e.name as employee_name,
m.name as manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
|
Funzioni aggregate
Funzioni base
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
-- Contare record
SELECT COUNT(*) as total_users FROM users;
SELECT COUNT(DISTINCT category_id) as unique_categories FROM products;
-- Somme e medie
SELECT
SUM(price * stock_quantity) as inventory_value,
AVG(price) as average_price,
MIN(price) as cheapest_product,
MAX(price) as most_expensive_product
FROM products;
-- Con raggruppamento
SELECT
category_id,
COUNT(*) as product_count,
AVG(price) as avg_price,
SUM(stock_quantity) as total_stock
FROM products
GROUP BY category_id;
|
Funzioni avanzate
1
2
3
4
5
6
7
8
9
10
11
12
|
-- Deviazione standard e varianza
SELECT
STDDEV(price) as price_std_dev,
VARIANCE(price) as price_variance
FROM products;
-- Concatenazione gruppi
SELECT
category_id,
GROUP_CONCAT(name ORDER BY price DESC SEPARATOR ', ') as products
FROM products
GROUP BY category_id;
|
Window functions (funzioni finestra)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
-- Ranking
SELECT
name,
price,
RANK() OVER (ORDER BY price DESC) as price_rank,
ROW_NUMBER() OVER (ORDER BY price DESC) as row_num
FROM products;
-- Partizioni
SELECT
name,
category_id,
price,
AVG(price) OVER (PARTITION BY category_id) as category_avg_price,
price - AVG(price) OVER (PARTITION BY category_id) as price_vs_avg
FROM products;
-- Running totals
SELECT
order_date,
total_amount,
SUM(total_amount) OVER (ORDER BY order_date) as running_total
FROM orders
ORDER BY order_date;
|
Query avanzate
Subquery
Subquery scalari
1
2
3
4
5
6
7
8
9
|
-- Prodotti sopra la media
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- Utenti con ordini
SELECT username
FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);
|
Subquery correlate
1
2
3
4
5
6
7
8
|
-- Prodotti piu costosi di ogni categoria
SELECT p1.name, p1.price, p1.category_id
FROM products p1
WHERE p1.price = (
SELECT MAX(p2.price)
FROM products p2
WHERE p2.category_id = p1.category_id
);
|
Common table expressions (CTE)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
-- CTE Semplice
WITH expensive_products AS (
SELECT * FROM products WHERE price > 500
)
SELECT c.name, COUNT(ep.id) as expensive_count
FROM categories c
LEFT JOIN expensive_products ep ON c.id = ep.category_id
GROUP BY c.id, c.name;
-- CTE Ricorsiva (es. struttura gerarchica)
WITH RECURSIVE category_tree AS (
-- Caso base: categorie radice
SELECT id, name, parent_id, 0 as level
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- Ricorsione: categorie figlie
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY level, name;
|
CASE statements
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- Categorizzazione con CASE
SELECT
name,
price,
CASE
WHEN price < 50 THEN 'Budget'
WHEN price BETWEEN 50 AND 200 THEN 'Mid-range'
WHEN price > 200 THEN 'Premium'
ELSE 'Unknown'
END as price_category,
CASE
WHEN stock_quantity = 0 THEN 'Out of Stock'
WHEN stock_quantity < 10 THEN 'Low Stock'
ELSE 'In Stock'
END as stock_status
FROM products;
|
UNION e INTERSECT
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-- Combinare risultati
SELECT 'user' as type, username as name FROM users
UNION ALL
SELECT 'category' as type, name FROM categories
ORDER BY type, name;
-- Intersezione (MySQL simulation)
SELECT DISTINCT p1.name
FROM products p1
INNER JOIN (
SELECT name FROM products WHERE category_id = 1
) p2 ON p1.name = p2.name
WHERE p1.category_id = 2;
|
Stored procedure e funzioni
Stored procedure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
DELIMITER //
-- Procedura per aggiungere utente
CREATE PROCEDURE AddUser(
IN p_username VARCHAR(50),
IN p_email VARCHAR(100),
IN p_password_hash VARCHAR(255),
IN p_first_name VARCHAR(50),
IN p_last_name VARCHAR(50)
)
BEGIN
DECLARE user_exists INT DEFAULT 0;
-- Controllo se utente esiste già
SELECT COUNT(*) INTO user_exists
FROM users
WHERE username = p_username OR email = p_email;
IF user_exists > 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'User already exists';
ELSE
INSERT INTO users (username, email, password_hash, first_name, last_name)
VALUES (p_username, p_email, p_password_hash, p_first_name, p_last_name);
SELECT 'User created successfully' as result;
END IF;
END //
DELIMITER ;
-- Chiamare la procedura
CALL AddUser('new_user', 'new@email.com', 'hash123', 'New', 'User');
|
Funzioni
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
DELIMITER //
-- Funzione per calcolare sconto
CREATE FUNCTION CalculateDiscount(price DECIMAL(10,2), discount_pct INT)
RETURNS DECIMAL(10,2)
READS SQL DATA
DETERMINISTIC
BEGIN
RETURN price * (100 - discount_pct) / 100;
END //
-- Funzione per contare ordini utente
CREATE FUNCTION GetUserOrderCount(user_id INT)
RETURNS INT
READS SQL DATA
BEGIN
DECLARE order_count INT;
SELECT COUNT(*) INTO order_count FROM orders WHERE user_id = user_id;
RETURN order_count;
END //
DELIMITER ;
-- Utilizzo delle funzioni
SELECT
name,
price,
CalculateDiscount(price, 10) as discounted_price
FROM products;
SELECT
username,
GetUserOrderCount(id) as total_orders
FROM users;
|
Trigger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
-- Trigger per aggiornare timestamp
DELIMITER //
CREATE TRIGGER update_product_timestamp
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
SET NEW.updated_at = CURRENT_TIMESTAMP;
END //
-- Trigger per log delle modifiche
CREATE TRIGGER product_audit_log
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
INSERT INTO audit_log (table_name, operation, old_values, new_values, timestamp)
VALUES (
'products',
'UPDATE',
CONCAT('price:', OLD.price, ',stock:', OLD.stock_quantity),
CONCAT('price:', NEW.price, ',stock:', NEW.stock_quantity),
NOW()
);
END //
DELIMITER ;
|
Ottimizzazione e prestazioni
Indici
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- Indice semplice
CREATE INDEX idx_products_name ON products(name);
-- Indice composto
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);
-- Indice univoco
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Indice parziale (dove supportato)
CREATE INDEX idx_active_users ON users(username) WHERE status = 'active';
-- Vedere gli indici
SHOW INDEX FROM products;
-- Eliminare indice
DROP INDEX idx_products_name ON products;
|
Analisi delle query
1
2
3
4
5
6
7
8
|
-- EXPLAIN per analizzare piano di esecuzione
EXPLAIN SELECT p.name, c.name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.price > 100;
-- EXPLAIN ANALYZE (PostgreSQL/MySQL 8.0+)
EXPLAIN ANALYZE SELECT * FROM products WHERE name LIKE 'iPhone%';
|
Ottimizzazioni Comuni
1. Evitare SELECT *
1
2
3
4
5
|
-- Inefficiente
SELECT * FROM products WHERE price > 100;
-- Efficiente
SELECT id, name, price FROM products WHERE price > 100;
|
2. Usare LIMIT quando possibile
1
2
|
-- Limitare risultati quando non serve tutto
SELECT name, price FROM products ORDER BY price DESC LIMIT 10;
|
3. Ottimizzare JOIN
1
2
3
4
5
6
7
8
9
10
|
-- Inefficiente - WHERE dopo JOIN
SELECT p.name, c.name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.price > 100;
-- Piu efficiente - filtrare prima del JOIN
SELECT p.name, c.name
FROM (SELECT * FROM products WHERE price > 100) p
JOIN categories c ON p.category_id = c.id;
|
4. Usare EXISTS invece di IN con subquery
1
2
3
4
5
6
7
|
-- Puo essere lento con grandi dataset
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders);
-- Spesso piu efficiente
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
|
Configurazioni prestazioni
1
2
3
4
5
6
7
8
9
10
11
|
-- Vedere configurazioni correnti
SHOW VARIABLES LIKE '%buffer%';
SHOW VARIABLES LIKE '%cache%';
-- Statistiche prestazioni
SHOW STATUS LIKE 'Slow_queries';
SHOW STATUS LIKE 'Questions';
-- Abilitare slow query log
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 2; -- Query piu lente di 2 secondi
|
Buone pratiche
Convenzioni di naming
1
2
3
4
5
6
7
8
9
10
11
12
|
-- Nomi chiari e consistenti
CREATE TABLE user_profiles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
profile_picture_url VARCHAR(255),
bio TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_profiles_user_id (user_id),
FOREIGN KEY fk_user_profiles_user_id (user_id)
REFERENCES users(id) ON DELETE CASCADE
);
|
Sicurezza
1. Prevenire SQL Injection
1
2
3
4
5
6
7
|
-- Vulnerabile a SQL injection
query = "SELECT * FROM users WHERE username = '" + username + "'"
-- Usare prepared statements
PREPARE stmt FROM 'SELECT * FROM users WHERE username = ?';
SET @username = 'mario.rossi';
EXECUTE stmt USING @username;
|
2. Principio del Minimo Privilegio
1
2
3
4
5
6
7
8
9
|
-- Creare utente con privilegi limitati
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'secure_password';
-- Dare solo i privilegi necessari
GRANT SELECT, INSERT, UPDATE ON ecommerce.users TO 'app_user'@'localhost';
GRANT SELECT ON ecommerce.products TO 'app_user'@'localhost';
-- Non dare mai privilegi globali se non necessario
-- GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'localhost'; -- Evitare
|
Gestione transazioni
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-- Transazione esplicita
START TRANSACTION;
INSERT INTO orders (user_id, total_amount) VALUES (1, 99.99);
SET @order_id = LAST_INSERT_ID();
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (@order_id, 5, 2, 49.99);
UPDATE products SET stock_quantity = stock_quantity - 2 WHERE id = 5;
-- Se tutto è andato bene
COMMIT;
-- In caso di errore
-- ROLLBACK;
|
Backup e manutenzione
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- Backup database
mysqldump -u root -p ecommerce > backup_ecommerce.sql
-- Ripristino
mysql -u root -p ecommerce < backup_ecommerce.sql
-- Ottimizzare tabelle
OPTIMIZE TABLE products;
-- Analizzare tabelle per statistiche
ANALYZE TABLE products;
-- Controllare e riparare tabelle
CHECK TABLE products;
REPAIR TABLE products;
|
Design del database
1. Normalizzazione
1
2
3
4
5
6
7
8
9
10
11
|
-- Denormalizzato (duplicazione dati)
CREATE TABLE orders_bad (
id INT PRIMARY KEY,
customer_name VARCHAR(100),
customer_email VARCHAR(100),
product_name VARCHAR(100),
product_price DECIMAL(10,2)
);
-- Normalizzato (terza forma normale)
-- Tabelle separate con relazioni tramite chiavi esterne
|
2. Convenzioni per Chiavi Esterne
1
2
3
4
5
6
7
8
9
10
|
CREATE TABLE order_items (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT
);
|
Test delle query
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-- Creare dati di test
INSERT INTO users (username, email, password_hash, first_name, last_name)
SELECT
CONCAT('user', i) as username,
CONCAT('user', i, '@test.com') as email,
'test_hash' as password_hash,
CONCAT('User', i) as first_name,
'Test' as last_name
FROM (
SELECT a.N + b.N * 10 + c.N * 100 + 1 as i
FROM
(SELECT 0 as N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a,
(SELECT 0 as N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b,
(SELECT 0 as N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) c
) numbers
LIMIT 1000;
|
Conclusioni e prossimi passi
Risorse per continuare
Risorse Gratuite
- SQLBolt: Tutorial interattivi SQL
- W3Schools SQL: Riferimento e esempi
- PostgreSQL Tutorial: Guida completa PostgreSQL
- MySQL Documentation: Documentazione ufficiale
- SQL Practice: HackerRank, LeetCode per esercizi
Dataset per pratica
- Sakila: Database di esempio MySQL (DVD rental)
- Northwind: Database classico Microsoft
- Chinook: Database per musica digitale
- World: Database con dati geografici
Comunita e forum
- Stack Overflow: Tag SQL per domande specifiche
- Reddit r/SQL: Discussioni e consigli
- DBA Stack Exchange: Per amministratori database
- MySQL/PostgreSQL Forums: Comunità ufficiali