Sviluppo Software

Database design: Principi fondamentali

Principi di design di database per applicazioni scalabili e performanti.

Database Design: Principi Fondamentali

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.

Normalizzazione: Eliminare la Ridondanza

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)
);

Indici: la Chiave delle Performance

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:

  • Una colonna appare spesso in WHERE, JOIN ON, ORDER BY
  • La tabella ha piu' di 10.000 righe
  • La query impiega piu' di qualche millisecondo

Vincoli per l'Integrità dei Dati

I 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()
);

Tipi di Dati Giusti

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 '{}'

Soft Delete

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;

Migration Sicure in Produzione

Le migration sbagliate causano downtime. Segui queste regole:

  1. Aggiungi colonne con defaultALTER TABLE ADD COLUMN x INT DEFAULT 0
  2. Non rinominare colonne direttamente — Aggiungi la nuova, migra i dati, rimuovi la vecchia
  3. Indici con CONCURRENTLY — Non blocca le scritture: CREATE INDEX CONCURRENTLY
  4. Transactions nelle migration — Se una parte fallisce, tutto fa rollback
  5. Testa la migration su un dump produzione prima di applicarla

Conclusioni

Il 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.

Tag

Condividi

Articoli correlati