Principi di design di database per applicazioni scalabili e performanti.
Un database ben progettato e' la fondamenta di qualsiasi applicazione scalabile. Errori nel design dello schema si pagano caro nel tempo: query lente, dati inconsistenti, migration dolorose. In questa guida vediamo i principi fondamentali che ogni sviluppatore dovrebbe padroneggiare.
La normalizzazione organizza i dati per ridurre la ridondanza e migliorare l'integrita'. Le prime tre forme normali coprono il 99% dei casi pratici.
Prima Forma Normale (1NF): ogni colonna deve contenere valori atomici, non liste.
-- Sbagliato: telefoni come lista CSV
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255),
phones VARCHAR(500) -- "333-111, 347-222" <- WRONG
);
-- Corretto: tabella separata per i telefoni
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255)
);
CREATE TABLE user_phones (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
phone VARCHAR(20) NOT NULL,
type VARCHAR(10) DEFAULT 'mobile'
);
Terza Forma Normale (3NF): ogni attributo non-chiave deve dipendere solo dalla chiave primaria.
-- Sbagliato: city e country dipendono da zip_code, non da order_id
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER,
zip_code VARCHAR(10),
city VARCHAR(100), -- dipende da zip_code
country VARCHAR(50) -- dipende da zip_code
);
-- Corretto: normalizzazione degli indirizzi
CREATE TABLE addresses (
id SERIAL PRIMARY KEY,
zip_code VARCHAR(10),
city VARCHAR(100),
country VARCHAR(50)
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
address_id INTEGER REFERENCES addresses(id)
);
Un indice mancante puo' trasformare una query da millisecondi a minuti su tabelle grandi.
-- Indice su colonne usate frequentemente in WHERE
CREATE INDEX idx_posts_status_published_at
ON posts(status, published_at DESC)
WHERE status = 'published';
-- Indice per full-text search
CREATE INDEX idx_posts_content_fts
ON posts USING gin(to_tsvector('italian', title || ' ' || content));
-- Indice parziale (solo subset delle righe)
CREATE INDEX idx_orders_pending
ON orders(created_at)
WHERE status = 'pending';
-- Indice composito per query frequenti
CREATE INDEX idx_post_tags
ON post_tag(post_id, tag_id);
Regola pratica: aggiungi un indice quando:
WHERE, JOIN ON, ORDER BYI vincoli a livello di database sono piu' affidabili di qualsiasi validazione applicativa:
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft', 'published', 'archived')),
category_id INTEGER NOT NULL REFERENCES categories(id),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
Scegliere il tipo corretto impatta performance e spazio su disco:
-- Usa TIMESTAMPTZ (con timezone) per date e ore
created_at TIMESTAMPTZ DEFAULT now()
-- Usa NUMERIC per denaro, mai FLOAT (problemi di arrotondamento)
price NUMERIC(10, 2)
-- Usa UUID per ID pubblici (sicurezza + distribuzione)
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
-- Usa BOOLEAN, non TINYINT o VARCHAR
is_active BOOLEAN DEFAULT true
-- Usa JSONB (non JSON) su PostgreSQL per dati semi-strutturati
metadata JSONB DEFAULT '{}'
Cancella logicamente invece di fisicamente per preservare la storia:
ALTER TABLE posts ADD COLUMN deleted_at TIMESTAMPTZ;
-- Query che esclude i record eliminati
SELECT * FROM posts WHERE deleted_at IS NULL;
-- Indice parziale per efficienza
CREATE INDEX idx_posts_active ON posts(published_at DESC)
WHERE deleted_at IS NULL;
Le migration sbagliate causano downtime. Segui queste regole:
ALTER TABLE ADD COLUMN x INT DEFAULT 0CREATE INDEX CONCURRENTLYIl database design non si impara in un giorno, ma questi principi ti eviteranno gli errori piu' costosi. Normalizza la struttura, aggiungi indici dove servono, usa vincoli per garantire l'integrita' e scegli tipi di dato precisi.
Investire in un buon schema fin dall'inizio e' molto piu' economico che dover ristrutturare un database con milioni di righe in produzione.