KoderKoder.ai
PrezziEnterpriseIstruzionePer gli investitori
AccediInizia ora

Prodotto

PrezziEnterprisePer gli investitori

Risorse

ContattaciAssistenzaIstruzioneBlog

Note legali

Informativa sulla privacyTermini di utilizzoSicurezzaNorme di utilizzoSegnala un abuso

Social

LinkedInTwitter
Koder.ai
Lingua

© 2026 Koder.ai. Tutti i diritti riservati.

Home›Blog›Pianificazione dello schema Postgres: approccio passo passo
18 dic 2025·7 min

Pianificazione dello schema Postgres: approccio passo passo

La pianificazione dello schema Postgres ti aiuta a definire entità, vincoli, indici e migrazioni prima della generazione del codice, riducendo le riscritture successive.

Pianificazione dello schema Postgres: approccio passo passo

Perché pianificare lo schema Postgres prima di generare codice

Se costruisci endpoint e modelli prima che la forma del database sia chiara, di solito finisci per riscrivere le stesse funzionalità due volte. L’app funziona per una demo, poi arrivano dati reali e casi limite e tutto comincia a sembrare fragile.

La maggior parte delle riscritture deriva da tre problemi prevedibili:

  • Le entità non sono chiare (nomi o raggruppamenti errati).
  • Le regole non sono applicate (mancano i vincoli).
  • Problemi di performance emergono tardi (gli indici vengono aggiunti dopo che gli utenti si lamentano).

Ognuno di questi costringe a cambiamenti che si propagano nel codice, nei test e nelle app client.

Pianificare il tuo schema Postgres significa decidere prima il contratto dei dati, poi generare codice che lo rispetti. In pratica, questo significa scrivere entità, relazioni e le poche query importanti, quindi scegliere vincoli, indici e una strategia di migrazione prima che qualsiasi strumento generi tabelle e CRUD.

Questo è ancora più importante quando usi una piattaforma di generazione veloce come Koder.ai, dove puoi creare molto codice rapidamente. La generazione rapida è ottima, ma è molto più affidabile quando lo schema è definito. I modelli e gli endpoint generati richiederanno meno modifiche dopo.

Ecco cosa succede tipicamente quando salti la pianificazione:

  • Una tabella “User” diventa silenziosamente un mix di users, admins e teams.
  • Appaiono record duplicati perché nulla impone l’unicità.
  • Le cancellazioni rompono la cronologia perché il comportamento delle referenze non è stato deciso.
  • Una pagina di elenco popolare diventa lenta perché non è stato pianificato l’indice giusto.

Un buon piano di schema è semplice: una descrizione in linguaggio comune delle tue entità, una bozza di tabelle e colonne, i vincoli e gli indici principali, e una strategia di migrazione che ti permetta di cambiare le cose in sicurezza man mano che il prodotto cresce.

Inizia dai bisogni dei dati, non dalle tabelle

La pianificazione dello schema funziona meglio quando parti da ciò che l’app deve ricordare e da cosa le persone devono poter fare con quei dati. Scrivi l’obiettivo in 2–3 frasi in linguaggio semplice. Se non riesci a spiegarlo in modo chiaro, probabilmente creerai tabelle extra di cui non hai bisogno.

Poi concentrati sulle azioni che creano o modificano i dati. Queste azioni sono la vera fonte delle righe e rivelano cosa deve essere validato. Pensa in verbi, non in nomi.

Per esempio, un’app di prenotazioni potrebbe dover creare una prenotazione, riprogrammarla, cancellarla, rimborsarla e inviare messaggi al cliente. Questi verbi suggeriscono rapidamente cosa deve essere memorizzato (fasce orarie, cambi di stato, importi) prima ancora di dare un nome alla tabella.

Annota anche i percorsi di lettura, perché le letture influenzano la struttura e l’indicizzazione. Elenca le schermate o i report che le persone useranno e come vogliono filtrare/ordinare i dati: “Le mie prenotazioni” ordinate per data e filtrate per stato, ricerca admin per nome cliente o riferimento prenotazione, ricavo giornaliero per sede, e una vista di audit su chi ha cambiato cosa e quando.

Infine, nota i requisiti non funzionali che influenzano le scelte di schema, come cronologia di audit, soft deletes, separazione multi-tenant o regole di privacy (es. limitare chi può vedere i dettagli di contatto).

Se prevedi di generare codice dopo, queste note diventano ottimi prompt. Definiscono cosa è richiesto, cosa può cambiare e cosa deve essere ricercabile. Se usi Koder.ai, scrivere tutto questo prima di generare rende Planning Mode molto più efficace perché la piattaforma lavora su requisiti reali invece che su ipotesi.

Definisci entità e relazioni in linguaggio semplice

Prima di toccare le tabelle, scrivi una descrizione in termini semplici di ciò che la tua app memorizza. Inizia elencando i nomi che ripeti: user, project, message, invoice, subscription, file, comment. Ogni sostantivo è una possibile entità.

Poi aggiungi una frase per entità che risponda: cos’è e perché esiste? Per esempio: “Un Project è uno spazio di lavoro che un utente crea per raggruppare lavoro e invitare altri.” Questo evita tabelle vaghe come data, items o misc.

La proprietà è la decisione successiva e influisce quasi su ogni query. Per ogni entità, decidi:

  • chi la crea,
  • chi può vederla,
  • chi può modificarla,
  • cosa succede quando il proprietario viene eliminato (mantenere, trasferire o cancellare).

Ora decidi come identificherai i record. Gli UUID sono ottimi quando le righe possono essere create da molti posti (web, mobile, job) o quando non vuoi ID prevedibili. I bigint sono più piccoli e veloci. Se ti serve un identificatore leggibile, tienilo separato (es. un breve project_code unico all’interno di un account) invece di usarlo come chiave primaria.

Infine, scrivi le relazioni a parole prima di diagrammare: un utente ha molti progetti, un progetto ha molti messaggi, e gli utenti possono appartenere a molti progetti. Segna ogni collegamento come obbligatorio o opzionale, per esempio “un messaggio deve appartenere a un progetto” vs “una fattura può appartenere a un progetto”. Queste frasi diventeranno la fonte di verità per la generazione del codice in seguito.

Trasforma le entità in tabelle e colonne

Una volta che le entità sono chiare in linguaggio naturale, converti ciascuna in una tabella con colonne che rappresentano fatti reali da memorizzare.

Inizia con nomi e tipi su cui puoi essere coerente. Scegli pattern consistenti: nomi di colonna in snake_case, lo stesso tipo per la stessa idea e chiavi primarie prevedibili. Per i timestamp, preferisci timestamptz così i fusi orari non ti sorprendono più avanti. Per il denaro, usa numeric(12,2) (o salva i centesimi come intero) invece dei float.

Per i campi di stato, usa o un enum di Postgres o una colonna text con un CHECK constraint in modo che i valori consentiti siano controllati.

Decidi cosa è obbligatorio vs opzionale traducendo le regole in NOT NULL. Se un valore deve esistere perché la riga abbia senso, rendilo obbligatorio. Se è davvero sconosciuto o non applicabile, permetti i null.

Un set pratico di colonne di default da pianificare:

  • id (uuid o bigint, scegli un approccio e mantienilo coerente)
  • created_at e updated_at
  • deleted_at solo se hai davvero bisogno di soft deletes e restore
  • created_by quando servono tracce chiare di chi ha fatto cosa

Le relazioni molti-a-molti dovrebbero quasi sempre diventare tabelle di join. Per esempio, se più utenti possono collaborare su un app, crea app_members con app_id e user_id, poi applica un vincolo di unicità sulla coppia per evitare duplicati.

Pensa alla cronologia fin da subito. Se sai che ti servirà versioning, pianifica una tabella immutabile come app_snapshots, dove ogni riga è una versione salvata collegata a apps tramite app_id e timbrata con created_at.

Aggiungi vincoli che proteggono i tuoi dati

Iterate with migrations
Regenerate models and endpoints after each migration instead of patching by hand.
Try Koderai

I vincoli sono le protezioni del tuo schema. Decidi quali regole devono essere vere a prescindere da quale servizio, script o tool admin tocchi il database.

Inizia con identità e relazioni. Ogni tabella ha bisogno di una primary key, e ogni campo “belongs to” dovrebbe essere una vera foreign key, non solo un intero che speri corrisponda.

Poi aggiungi unicità dove i duplicati causerebbero danni reali, come due account con la stessa email o due line item con lo stesso (order_id, product_id).

Vincoli ad alto valore da pianificare presto:

  • Primary keys: scegli uno stile coerente (UUID o bigint) così le join restano prevedibili.
  • Foreign keys: rendi esplicita la relazione e previeni righe orfane.
  • Unique constraints: usali per l’identità di business (email, username) e per regole “solo uno di questi”.
  • Check constraints: regole economiche come amount >= 0, status IN ('draft','paid','canceled') o rating BETWEEN 1 AND 5.
  • Not null: rendi obbligatori i campi che in realtà servono, non solo quelli “di solito compilati”.

Il comportamento di cascade è dove la pianificazione ti salva dopo. Chiediti cosa si aspetta la gente. Se un cliente viene eliminato, i suoi ordini di solito non dovrebbero sparire: questo punta a RESTRICT/NO ACTION e a mantenere la cronologia. Per dati dipendenti come i line item di un ordine, il CASCADE può avere senso perché gli item non hanno significato senza il genitore.

Quando poi genererai modelli e endpoint, questi vincoli diventeranno requisiti chiari: quali errori gestire, quali campi sono obbligatori e quali casi limite sono impossibili per progetto.

Pianifica gli indici a partire dalle query reali

Gli indici devono rispondere a una domanda: cosa deve essere veloce per gli utenti reali.

Inizia dalle schermate e dalle chiamate API che prevedi di rilasciare subito. Una pagina di elenco che filtra per stato e ordina per nuovo ha esigenze diverse di una pagina di dettaglio che carica record correlati.

Scrivi 5–10 pattern di query in linguaggio naturale prima di scegliere un indice. Per esempio: “Mostra le mie fatture per gli ultimi 30 giorni, filtra per pagate/non pagate, ordina per created_at”, o “Apri un progetto e lista i suoi task per due_date.” Questo mantiene le scelte di indice radicate nell’uso reale.

Un primo insieme utile di indici spesso include le colonne foreign key usate per join, colonne usate come filtro comune (status, user_id, created_at) e uno o due indici composti per query multi-filtro stabili, come (account_id, created_at) quando filtri sempre per account_id e poi ordini per tempo.

L’ordine degli indici composti conta. Metti per primo la colonna su cui filtri più spesso (e che è più selettiva). Se filtri per tenant_id a ogni richiesta, spesso va in testa a molti indici.

Evita di indicizzare tutto “giusto per sicurezza”. Ogni indice aggiunge lavoro a INSERT e UPDATE e questo può danneggiare più di una query rara leggermente più lenta.

Pianifica la ricerca testuale separatamente. Se ti basta un semplice “contiene”, ILIKE può essere sufficiente all’inizio. Se la ricerca è centrale, valuta il full-text (tsvector) presto così non dovrai riprogettare dopo.

Decidi la strategia di migrazione prima di generare codice

Uno schema non è “finito” quando crei le prime tabelle. Cambia ogni volta che aggiungi una funzionalità, correggi un errore o impari di più sui dati. Se decidi la strategia di migrazione fin dall’inizio, eviti riscritture dolorose dopo la generazione del codice.

Tieni una regola semplice: cambia il database a piccoli passi, una funzionalità alla volta. Ogni migrazione dovrebbe essere facile da revisionare e sicura da eseguire in ogni ambiente.

Come gestire i cambiamenti breaking in sicurezza

La maggior parte dei problemi proviene dal rinominare o rimuovere colonne, o dal cambiare tipi. Invece di fare tutto in un colpo solo, pianifica un percorso sicuro:

  • Aggiungi prima nuove colonne (nullable se serve), poi popola i dati.
  • Se l’app deve restare operativa durante il cambiamento, usa dual-write per un breve periodo (scrivi sia nel campo vecchio che in quello nuovo).
  • Passa le letture al nuovo campo, poi pulisci il vecchio in una migrazione successiva.

Questo richiede più passaggi, ma nella pratica è più veloce perché riduce outage e patch d’emergenza.

I dati seed fanno parte delle migrazioni. Decidi quali tabelle di riferimento sono “sempre lì” (ruoli, stati, paesi, tipi di piano) e rendile prevedibili. Metti insert e update per queste tabelle in migrazioni dedicate così ogni sviluppatore e ogni deploy ottiene gli stessi risultati.

Regole di coerenza tra locale, dev e prod

Stabilisci le aspettative presto:

  • Stesse migrazioni, stesso ordine, ovunque.
  • Niente hotfix manuali in produzione senza una migrazione corrispondente.
  • Ogni migrazione ha una storia forward chiara e un piano di rollback realistico.

I rollback non sono sempre una perfetta “down migration”. A volte il miglior rollback è il ripristino da backup. Se usi Koder.ai, vale anche la pena decidere quando affidarsi a snapshot e rollback per recuperi rapidi, specialmente prima di cambi rischiosi.

Un esempio semplice che puoi copiare e adattare

Turn the checklist into code
Turn your checklist into a build plan and ship a stable first version sooner.
Start Free

Immagina una piccola SaaS dove le persone si uniscono in team, creano progetti e tracciano task.

Inizia elencando le entità e solo i campi necessari per il giorno uno:

  • users: id, email, full_name, created_at
  • teams: id, org_id, name, created_at
  • team_members: team_id, user_id, role, joined_at
  • projects: id, team_id, name, status, created_at
  • tasks: id, project_id, assignee_user_id (nullable), title, state, due_date (nullable), created_at

Le relazioni sono semplici: un team ha molti progetti, un progetto ha molti task, e gli utenti si uniscono ai team tramite team_members. I task appartengono a un progetto e possono essere assegnati a un utente.

Ora aggiungi alcuni vincoli che prevengono bug che trovi di solito troppo tardi:

  • Rendi users.email unico (case-insensitive se usi citext).
  • Rendi i nomi dei team unici all’interno della stessa org: UNIQUE (org_id, name) su teams.
  • Previeni membership duplicate: UNIQUE (team_id, user_id) su team_members.

Gli indici dovrebbero corrispondere alle schermate reali. Per esempio, se la lista dei task filtra per project e state e ordina per newest, pianifica un indice come tasks (project_id, state, created_at DESC). Se “My tasks” è una vista chiave, un indice come tasks (assignee_user_id, state, due_date) può aiutare.

Per le migrazioni, mantieni il primo insieme sicuro e semplice: crea tabelle, chiavi primarie, foreign key e i vincoli unici core. Un buon cambiamento successivo è qualcosa da aggiungere dopo che l’uso lo dimostra, come introdurre soft delete (deleted_at) sui task e adattare gli indici “active tasks” per ignorare le righe cancellate.

Errori comuni e come evitarli

La maggior parte delle riscritture avviene perché il primo schema manca di regole e dettagli sull’uso reale. Un buon pass di pianificazione non serve a creare diagrammi perfetti, ma a individuare le trappole presto.

Un errore comune è mantenere regole importanti solo nel codice applicativo. Se un valore deve essere unico, presente o entro un range, il database dovrebbe farlo rispettare. Altrimenti uno script, un nuovo endpoint o un import manuale possono aggirare la logica.

Un altro errore frequente è considerare gli indici un problema successivo. Aggiungerli dopo il lancio spesso diventa un lavoro di indovinare e puoi finire per indicizzare la cosa sbagliata mentre la query lenta reale è una join o un filtro su uno status.

Le tabelle molti-a-molti sono anche fonte di bug silenziosi. Se la tabella di join non evita duplicati, puoi memorizzare la stessa relazione due volte e passare ore a capire “perché questo utente ha due ruoli?”.

È anche facile creare tabelle prima e poi rendersi conto di aver bisogno di log di audit, soft deletes o cronologia eventi. Quegli aggiustamenti si ripercuotono su endpoint e report.

Infine, le colonne JSON sono allettanti per dati “flessibili”, ma tolgono controlli e rendono l’indicizzazione più difficile. JSON va bene per payload davvero variabili, non per campi core di business.

Prima di generare codice, esegui questa lista di controllo rapida:

  • Sposta le regole chiave nei vincoli: NOT NULL, CHECK, UNIQUE e foreign key.
  • Scrivi 3–5 query reali e indicizza per quelle, non per “magari dopo”.
  • Aggiungi un UNIQUE composto sulle tabelle di join (es. user_id + role_id).
  • Decidi presto se ti serve cronologia di audit e modellala come tabella separata.
  • Tieni JSON per attributi rari/ opzionali e promuovi i campi importanti a colonne.

Checklist veloce prima di generare modelli e endpoint

Own the generated source
Keep full control by exporting source code after you generate your app.
Export Code

Fermati e assicurati che il piano sia sufficientemente completo da generare codice senza correre dietro alle sorprese. L’obiettivo non è la perfezione. È intercettare i vuoti che causano riscritture: relazioni mancanti, regole poco chiare e indici che non corrispondono all’uso reale.

Usa questo come controllo pre-volo rapido:

  • Per ogni entità, scrivi una frase su chi la possiede (user, org, system) e cosa significa “deleted” (hard delete, soft delete, archived).
  • Per ogni tabella, conferma tipo delle colonne, obbligatorietà, valori di default e come sono impostati i timestamp (dall’app o dal DB).
  • Scrivi le regole che non devono mai rompersi: primary key, foreign key, regole di unicità e qualche semplice check (come amount >= 0 o stati consentiti).
  • Scegli le top 3–5 schermate o chiamate API e lista i filtri esatti e l’ordinamento che richiedono. Verifica che gli indici corrispondano.
  • Schizza le prime migrazioni: schema iniziale, quali dati devono esistere al giorno uno (seed rows) e una modifica che prevedi di fare presto (aggiungere uno stato, separare un campo nome, introdurre org).

Un rapido test di sanità: immagina che un collega si unisca domani. Potrebbe costruire i primi endpoint senza chiedere “questo può essere null?” o “cosa succede alla delete?” ogni ora?

Passi successivi: dal piano al codice con meno riscritture

Quando il piano è leggibile e i flussi principali hanno senso su carta, trasformalo in qualcosa eseguibile: uno schema reale più migrazioni.

Inizia con una migrazione iniziale che crea tabelle, tipi (se usi enum) e i vincoli indispensabili. Mantieni la prima versione piccola ma corretta. Carica qualche dato seed e esegui le query che l’app userà davvero. Se un flusso è scomodo, correggi lo schema mentre la storia delle migrazioni è ancora corta.

Genera modelli e endpoint solo dopo che puoi testare alcune azioni end-to-end con lo schema in piedi (create, update, list, delete, più un’azione di business reale). La generazione del codice è più veloce quando tabelle, chiavi e naming sono abbastanza stabili da non dover rinominare tutto il giorno dopo.

Un loop pratico che mantiene le riscritture basse:

  • Aggiorna il piano quando impari qualcosa dai test.
  • Aggiungi una nuova migrazione (evita di modificare quelle vecchie dopo che altri le hanno usate).
  • Rigenera modelli e endpoint se lo schema cambia.
  • Riesegui gli stessi flussi e query per confermare che nulla si rompe.
  • Aggiungi campi e indici opzionali solo dopo che i percorsi core sono solidi.

Decidi presto cosa validare nel database vs a livello API. Metti regole permanenti nel database (foreign key, unique, check). Mantieni regole morbide nell’API (feature flag, limiti temporanei e logica cross-table complessa che cambia spesso).

Se usi Koder.ai, un approccio sensato è mettersi d’accordo su entità e migrazioni in Planning Mode prima, poi generare il backend Go + PostgreSQL. Quando una modifica va storta, snapshot e rollback possono aiutarti a tornare rapidamente a una versione funzionante mentre rivedi il piano dello schema.

Domande frequenti

Perché dovrei pianificare lo schema Postgres prima di generare modelli e endpoint?

Pianifica prima lo schema. Fissa un contratto dati stabile (tabelle, chiavi, vincoli) così i modelli e gli endpoint generati non richiederanno continui rinomini e riscritture.

In pratica: scrivi le entità, le relazioni e le query principali, poi conferma vincoli, indici e migrazioni prima di generare il codice.

Qual è il modo più rapido per iniziare la pianificazione dello schema senza bloccarsi sui diagrammi?

Scrivi 2–3 frasi che descrivono cosa l’app deve ricordare e cosa gli utenti devono poter fare.

Poi elenca:

  • Le azioni che creano/modificano i dati (verbi)
  • Le schermate/report che gli utenti useranno (read paths)
  • I bisogni non funzionali come audit history, soft deletes, multi-tenancy e privacy

Questo ti dà abbastanza chiarezza per progettare le tabelle senza sovraccaricare lo schema.

Come capisco quali sono le mie entità principali?

Inizia elencando i sostantivi che ripeti spesso (user, project, invoice, task). Per ognuno scrivi una frase: cos’è e perché esiste.

Se non riesci a descriverlo chiaramente, finirai probabilmente con tabelle vaghe come items o misc e te ne pentirai dopo.

Dovrei usare UUID o bigint come ID in Postgres?

Scegli una strategia ID coerente per tutto lo schema.

  • UUID: ottimo quando le righe possono essere create da molti posti (web/mobile/job) o non vuoi ID prevedibili
  • bigint: più piccoli e un po’ più veloci, semplici quando tutto è creato dal server

Se ti serve un identificatore leggibile, aggiungi una colonna unica separata (es. project_code) invece di usarla come chiave primaria.

Come scegliere il comportamento di delete per le foreign key (CASCADE vs RESTRICT)?

Decidilo per ogni relazione in base alle aspettative degli utenti e alla conservazione dei dati.

Default comuni:

  • Mantieni la cronologia: usa RESTRICT/NO ACTION quando cancellare il genitore eliminerebbe record importanti (cliente → ordini)
  • Cascade sicuro: usa CASCADE quando le righe figlie non hanno senso senza il genitore (ordine → line item)

Prendi questa decisione presto perché influisce sul comportamento delle API e sui casi limite.

Quali vincoli dovrei aggiungere dal giorno uno?

Metti le regole permanenti nel database così tutti i writer (API, script, import, tool admin) sono forzati a rispettarle.

Priorità del giorno zero:

Come scelgo gli indici senza sovra-indicizzare?

Parti dai pattern di query reali, non dai “magari dopo”.

Scrivi 5–10 query in linguaggio naturale (filtri + ordinamento), poi aggiungi indici per quelle:

  • Colonne FK usate per join
  • Filtri comuni come status, user_id, created_at
Qual è il modo corretto di modellare le relazioni molti-a-molti?

Crea una tabella di join con due foreign key e un vincolo UNIQUE composto.

Esempio:

  • team_members(team_id, user_id, role, joined_at)
  • Aggiungi UNIQUE (team_id, user_id) per evitare duplicati

Questo previene bug silenziosi come “perché questo utente appare due volte?” e mantiene le query pulite.

Quali tipi e pattern di colonna sono buone impostazioni predefinite in Postgres?

Di default:

  • timestamptz per i timestamp (meno sorprese sui fusi orari)
  • numeric(12,2) o centesimi interi per denaro (evita float)
  • Valori di stato imposti con enum Postgres o CHECK constraints

Mantieni i tipi coerenti tra le tabelle (stesso tipo per lo stesso concetto) così join e validazioni restano prevedibili.

Quale strategia di migrazione aiuta a evitare di rompere il codice generato in seguito?

Usa migrazioni piccole e revisionabili e evita cambiamenti distruttivi in un solo passaggio.

Percorso sicuro:

  • Aggiungi nuove colonne prima (nullable se serve)
  • Backfilla i dati
  • Eventualmente scrivi su entrambi i campi per un breve periodo (dual-write)
  • Passa la lettura al nuovo campo
  • Rimuovi il vecchio campo più tardi

Decidi anche come gestire seed/reference data in modo che ogni ambiente sia coerente.

Indice
Perché pianificare lo schema Postgres prima di generare codiceInizia dai bisogni dei dati, non dalle tabelleDefinisci entità e relazioni in linguaggio sempliceTrasforma le entità in tabelle e colonneAggiungi vincoli che proteggono i tuoi datiPianifica gli indici a partire dalle query realiDecidi la strategia di migrazione prima di generare codiceUn esempio semplice che puoi copiare e adattareErrori comuni e come evitarliChecklist veloce prima di generare modelli e endpointPassi successivi: dal piano al codice con meno riscrittureDomande frequenti
Condividi
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo
  • PRIMARY KEY su ogni tabella
  • FOREIGN KEY per ogni colonna “belongs to”
  • UNIQUE dove i duplicati causano problemi reali (email, (team_id, user_id) nelle join)
  • CHECK per regole semplici (importi non negativi, stati consentiti)
  • NOT NULL per campi necessari al senso della riga
  • Alcuni indici composti che rispecchiano pattern stabili (es. (account_id, created_at))
  • Evita di indicizzare tutto; ogni indice rallenta INSERT e UPDATE.