UUID vs Auto-Increment: Quale Scegliere per il Database?

THEJORD Team5 min di lettura
databaseuuidarchitecturebackend

UUID vs Auto-Increment: confronto completo per database. Vantaggi, svantaggi, performance e criteri per scegliere l'identificatore giusto per il tuo caso.

UUID vs Auto-Increment: Quale Scegliere per il Database?

Il Dilemma delle Primary Key

Scegliere tra UUID e auto-increment per le chiavi primarie è una decisione architetturale importante che influenza performance, scalabilità e sicurezza dell'applicazione. Non esiste una risposta universale: ogni approccio ha vantaggi specifici per casi d'uso diversi. Questa guida analizza entrambe le opzioni per aiutarti a scegliere consapevolmente.

Auto-Increment (Sequential ID)

Come Funziona

-- MySQL/MariaDB
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);

-- PostgreSQL
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);
-- oppure con IDENTITY (SQL standard)
CREATE TABLE users (
    id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);

-- Inserimento
INSERT INTO users (email) VALUES ('mario@example.com');
-- id = 1
INSERT INTO users (email) VALUES ('luigi@example.com');
-- id = 2

Vantaggi

  • Dimensione ridotta: INT = 4 byte, BIGINT = 8 byte
  • Performance indice: Inserimenti sequenziali ottimali per B-tree
  • Leggibilità: ID 42 è più facile da ricordare di un UUID
  • Debug semplice: Ordine cronologico implicito
  • Join veloci: Meno byte da confrontare

Svantaggi

  • Prevedibilità: /users/1, /users/2... enumerable
  • Single point of failure: Il database genera l'ID
  • Merge difficile: Conflitti tra database/shard
  • Limite: INT max = 2 miliardi (BIGINT = 9 quintilioni)
  • Esposizione dati: Rivela numero di record

UUID (Universally Unique Identifier)

Come Funziona

-- PostgreSQL (supporto nativo)
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL
);

-- MySQL 8.0+
CREATE TABLE users (
    id BINARY(16) PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);
-- Oppure come stringa (meno efficiente)
CREATE TABLE users (
    id CHAR(36) PRIMARY KEY,
    email VARCHAR(255) NOT NULL
);

-- Inserimento
INSERT INTO users (id, email)
VALUES (UUID(), 'mario@example.com');
-- id = '550e8400-e29b-41d4-a716-446655440000'

Versioni UUID

# UUIDv1: Timestamp + MAC address
# Pro: Ordinabile per tempo
# Contro: Espone MAC address, privacy
# Esempio: 6ba7b810-9dad-11d1-80b4-00c04fd430c8

# UUIDv4: Completamente random
# Pro: Sicuro, imprevedibile
# Contro: Frammentazione indice
# Esempio: f47ac10b-58cc-4372-a567-0e02b2c3d479

# UUIDv7: Timestamp + random (draft 2024)
# Pro: Ordinabile + sicuro, best of both worlds
# Contro: Nuovo, supporto limitato
# Esempio: 018e5f9c-7c00-7000-8000-000000000001

Vantaggi

  • Unicità globale: Generabile ovunque senza coordinazione
  • Sicurezza: Non enumerable, non rivela conteggio
  • Distribuito: Perfetto per microservizi e sharding
  • Merge semplice: No conflitti tra database
  • Client-side: Generabile prima dell'insert

Svantaggi

  • Dimensione: 16 byte (vs 4-8 per INT)
  • Performance: Random causa frammentazione B-tree
  • Leggibilità: Difficili da ricordare/comunicare
  • URL lunghi: /users/550e8400-e29b-41d4-a716-446655440000

Confronto Performance

Benchmark Insert

# Test: 1 milione di insert

# Auto-increment INT
Tempo: 45 secondi
Dimensione indice: 12 MB

# UUID v4 (BINARY 16)
Tempo: 120 secondi
Dimensione indice: 48 MB

# UUID v4 (CHAR 36)
Tempo: 180 secondi
Dimensione indice: 108 MB

# UUID v7 (BINARY 16)
Tempo: 55 secondi  # Molto meglio!
Dimensione indice: 48 MB

Perché UUIDv4 è Lento

# B-tree index con inserimenti random

# Auto-increment: inserisce sempre alla fine
[1] [2] [3] [4] [5] [6] [7] [8]
                            ↑ nuovo

# UUIDv4: inserisce in posizione random
[abc...] [def...] [ghi...] [jkl...]
              ↑ nuovo qui! (causa page split)

# Page split = riscrittura pagine = lento

# Soluzione: UUIDv7 (timestamp prefix)
# Inserisce quasi sequenzialmente come auto-increment

Pattern Ibridi

ID Interno + UUID Pubblico

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    public_id UUID UNIQUE DEFAULT gen_random_uuid(),
    email VARCHAR(255) NOT NULL
);

-- Join interni veloci con id
SELECT * FROM orders WHERE user_id = 42;

-- API usa public_id
GET /api/users/550e8400-e29b-41d4-a716-446655440000

-- Index su public_id per lookup API
CREATE INDEX idx_users_public_id ON users(public_id);

ULID (Universally Unique Lexicographically Sortable Identifier)

# ULID: alternativa a UUID
# 26 caratteri, Crockford Base32
# Timestamp + random, ordinabile

# Formato: TTTTTTTTTTRRRRRRRRRRRRR
# T = timestamp (ms), R = random

# Esempio
01ARZ3NDEKTSV4RRFFQ69G5FAV

# Vantaggi vs UUID:
# - Più corto (26 vs 36 caratteri)
# - Case-insensitive
# - Ordinabile cronologicamente
# - No caratteri speciali

# JavaScript
import { ulid } from 'ulid';
const id = ulid();  // 01ARZ3NDEKTSV4RRFFQ69G5FAV

Snowflake ID (Twitter)

# 64-bit ID, ordinabile, distribuito
# Formato: timestamp | datacenter | worker | sequence

# Pro:
# - Dimensione INT (8 byte)
# - Ordinabile per tempo
# - Supporta 4096 worker

# Usato da: Twitter, Discord, Instagram

# JavaScript (esempio semplificato)
function snowflake() {
    const epoch = 1288834974657n;
    const timestamp = BigInt(Date.now()) - epoch;
    const workerId = 1n;
    const sequence = 0n;

    return (timestamp << 22n) | (workerId << 12n) | sequence;
}

Quando Usare Cosa

Scegli Auto-Increment quando:

  • Database singolo, non distribuito
  • Performance è priorità massima
  • Applicazioni interne, non pubbliche
  • Volume dati < 2 miliardi record
  • Non esponi ID nelle API pubbliche

Scegli UUID quando:

  • Sistema distribuito o microservizi
  • Merge frequenti tra database
  • ID generati client-side
  • Sicurezza: no enumerazione
  • API pubbliche

Scegli UUIDv7/ULID quando:

  • Vuoi vantaggi UUID + ordinabilità
  • Performance è importante
  • Nuovo progetto senza vincoli legacy

Implementazione

Node.js

// UUIDv4
import { randomUUID } from 'crypto';
const id = randomUUID();

// UUIDv7 (con libreria)
import { uuidv7 } from 'uuidv7';
const id = uuidv7();

// ULID
import { ulid } from 'ulid';
const id = ulid();

PostgreSQL

-- UUIDv4 (built-in)
SELECT gen_random_uuid();

-- UUIDv7 (con estensione pg_uuidv7)
CREATE EXTENSION pg_uuidv7;
SELECT uuid_generate_v7();

-- O con funzione custom
CREATE OR REPLACE FUNCTION uuid_v7()
RETURNS uuid AS $$
    SELECT encode(
        set_byte(
            set_byte(
                overlay(uuid_send(gen_random_uuid())
                    placing substring(int8send(
                        floor(extract(epoch from clock_timestamp()) * 1000)::bigint
                    ) from 3) from 1 for 6),
                6, (get_byte(uuid_send(gen_random_uuid()), 6) & 15) | 112
            ),
            8, (get_byte(uuid_send(gen_random_uuid()), 8) & 63) | 128
        ),
        'hex'
    )::uuid;
$$ LANGUAGE sql VOLATILE;

Considerazioni Finali

La scelta tra UUID e auto-increment impatta anche la strategia di backup e replica. Gli auto-increment possono causare conflitti in scenari di multi-master replication. Gli UUID facilitano la merge di database separati senza remap degli ID. Considera anche l'impatto sui log e debug: gli ID sequenziali sono più facili da comunicare telefonicamente o in ticket di supporto rispetto agli UUID.

Strumenti Correlati

Per lavorare con identificatori:

Conclusione

La scelta dipende dal contesto:

  • Monolith tradizionale: Auto-increment con INT/BIGINT
  • API pubblica: UUID pubblico + ID interno
  • Distribuito: UUIDv7 o ULID
  • Alta scala: Snowflake ID

Nel 2025, UUIDv7 sta diventando lo standard de facto per nuovi progetti: combina unicità globale, ordinabilità e buone performance.

Per altri strumenti utili, esplora i nostri tool online gratuiti. Per approfondimenti, consulta UUID v7 draft RFC.