Perché il database del tuo bot gestisce tutto in segreto
Il primo bot “di successo” che ho lanciato nel 2018 è andato in tilt non perché il modello fosse cattivo, o il codice lento, ma perché il database era un disastro.
Gli utenti ricevevano risposte destinate ad altri. Le sessioni “dimenticavano” chi eri. Le analisi erano inutili.
Tutto perché ho trattato il DB come un pensiero secondario.
Se stai costruendo bot per utenti reali, il database è la spina dorsale. Non il modello. Non il gateway. Il modello dei dati.
Nel 2026 avremo LLM più belli e framework più brillanti, ma lo stesso vecchio problema:
- “Dove salvo lo stato della conversazione?”
- “Come tengo separate le sessioni?”
- “Cosa registro senza far crescere a dismisura lo storage?”
Permettimi di spiegarti come progetto database per bot che realmente funzionano in produzione, non solo in demo.
Entità chiave: utenti, sessioni, messaggi
Ogni bot che spedisco inizia con tre tabelle. I nomi cambiano, le idee no:
- users — chi ti sta parlando
- sessions — un intervallo di conversazione correlata
- messages — ciò che è stato detto, in ordine
users
Tratto gli utenti come “chi questa piattaforma pensa che tu sia”, non “il concetto filosofico di persona”.
users - id (pk) - external_id (unico, es., slack_user_id, telefono) - platform (slack | web | whatsapp | ecc.) - created_at - deleted_at (nullable)
Un utente di Slack e un utente di WhatsApp potrebbero essere la stessa persona, ma non sovraingegnerò l’“identità globale” fino a quando non ne avrò realmente bisogno.
Se cerchi di risolvere perfettamente l’identità dal primo giorno, ti blocchi.
sessions
Una sessione è “un filo di conversazione coerente.” Per un bot di supporto, potrebbe iniziare quando qualcuno fa una domanda e terminare dopo 30 minuti di silenzio o quando un agente chiude il ticket.
sessions - id (pk) - user_id (fk users.id) - platform (ancora, slack/web/ecc.) - status (attivo | chiuso) - started_at - ended_at (nullable) - metadata (jsonb) -- es., topic, channel_id, ticket_id
Memorizzo sempre metadata come JSON per i dati specifici della piattaforma che non voglio si mescolino nello schema principale.
Esempio: Slack channel_id, web browser_session_id, qualunque altra cosa.
messages
I messaggi sono l’unica fonte di verità per “ciò che è realmente accaduto.”
messages - id (pk) - session_id (fk sessions.id) - sender_type (user | bot | system) - sender_id (nullable, fk users.id per utente, forse bot_id in seguito) - external_message_id (nullable, es., slack_ts) - content (text) - content_type (text | json | rich) - created_at - raw_payload (jsonb, nullable)
Due motivi per cui questa tabella è importante:
- Debugging: quando qualcuno dice “il bot è impazzito,” hai bisogno di una trascrizione esatta.
- Formazione: avrai bisogno di conversazioni reali per ottimizzazioni o aggiustamenti dei prompt.
Per un bot che gestisce ~50k messaggi/giorno (uno che gestiamo in Postgres 15 su AWS RDS),
questa struttura ha funzionato bene con un’adeguata indicizzazione su session_id e created_at.
Interrogare gli ultimi 50 messaggi di una sessione è ancora economico.
Dove mettere lo “stato” del bot affinché non ti crei problemi
Il più grande errore di design che vedo: riempire di stato ovunque. Redis. JWT. Colonne JSON casuali. Nascosti in stringhe di prompt.
Così nessuno sa quale stato sia quello reale.
Stato a breve termine: memorizzalo nella cache
Cose di cui hai bisogno solo durante l’interazione corrente, e che puoi ricostruire da zero, devono andare in uno storage veloce:
- Redis,
KeyDB, o store in memoria per chiavi effimere - Scadenza in minuti, non in giorni
Esempi di chiavi che utilizzo effettivamente:
session_state:{session_id} -> json, ttl=30m
rate_limit:{user_id} -> counters, ttl=1h
otp:{phone} -> code, ttl=5m
Se Redis muore, il tuo bot potrebbe dimenticare dove si trovava in un modulo. Fastidioso, ma non catastrofico.
Questo è il tipo di cosa giusta da mettere lì.
Stato persistente: mettilo nel database
Qualsiasi cosa di cui ti pentiresti di perdere deve andare in un database reale:
- Progresso di onboarding
- Flag di funzionalità per utente
- Flussi di lavoro a lungo termine (“domanda di prestito #1234 passo 3/7”)
Di solito mantengo questo in una tabella separata invece di ingrossare sessions:
conversation_state - id (pk) - session_id (fk sessions.id, unico) - state_name (text) -- es., "verify_email", "collect_address" - data (jsonb) -- stato strutturato arbitrario - updated_at
Sì, JSON. No, non per tutto. Ma per lo stato del flusso del bot, il JSON va bene. Cambia spesso e sei l’unico a consumarlo.
Non nascondere lo stato nei JWT
Memorizzare la logica della sessione nelle dichiarazioni JWT è allettante. È anche il modo in cui ottieni bug in cui più client non concordano sulla realtà.
Tengo i JWT per l’autenticazione, e basta. Lo stato vive nel DB o nella cache.
Logging, analisi e non annegare nei dati
I bot generano un’assurda quantità di dati. Se registri ogni token per ogni richiesta e lo conservi per sempre, la tua fattura dell’infrastruttura ti ricorderà mensilmente.
Dividi “log di runtime” e “dati analitici”
Li trattiamo come due preoccupazioni diverse:
- Log di runtime: per il debugging. Breve conservazione (7–30 giorni).
Invia a qualcosa come Loki, Elasticsearch o CloudWatch. - Dati analitici: strutturati, interrogabili, a lungo termine. Vivono in SQL o in un warehouse.
Per l’analisi mi piace avere uno schema o un database separato:
conversation_metrics - id (pk) - session_id - user_id - messages_user_count - messages_bot_count - started_at - ended_at - first_response_ms - resolved (bool)
Calcoli questo tramite un lavoro che analizza messages ogni notte o tramite streaming.
In un bot cliente abbiamo attivato l’aggregazione notturna a marzo 2025 e abbiamo ridotto a metà le domande di supporto “cosa sta succedendo?” perché potevamo effettivamente vedere i punti di abbandono.
Log dei prompt e dei modelli
Se il tuo bot utilizza LLM, tieni un registro di ciò che hai inviato e di ciò che hai ricevuto, con un riferimento al messaggio/sessione.
llm_calls - id (pk) - session_id - message_id (fk messages.id, nullable) - provider (openai | anthropic | local) - model - prompt_tokens - completion_tokens - cost_usd - request_payload (jsonb) -- redatto - response_payload (jsonb) -- redatto - created_at
Due note importanti:
- Redatti segreti e PII prima di memorizzare i log.
- Imposta una policy di conservazione. Non hai bisogno di tre anni di prompt grezzi per il 99% dei bot.
SQL vs NoSQL vs vector stores (e come li mescolo)
Versione breve: mi affido a Postgres. Aggiungerò altri store quando il dolore è reale, non ipotetico.
Relazionale (Postgres, MySQL)
Perfetto per:
- Utenti, sessioni, messaggi, stato
- Reporting, join, migrazioni
- Consistenza forte per “chi ha detto cosa e quando”
Postgres con jsonb offre abbastanza flessibilità per evitare di creare cinque diversi database troppo presto.
Cache (Redis)
Utilizza questo per:
- Limiti di velocità
- Stato della sessione effimera
- Tabelle di ricerca piccole e frequenti (flag di configurazione, toggles di funzionalità)
Vector store
Se il tuo bot fa generazione aumentata dal recupero (RAG), avrai bisogno di embeddings e ricerca di similarità.
In pratica vedo tre schemi:
- Postgres + pgvector
- Servizi dedicati come Pinecone, Weaviate, Qdrant
- Opzioni specifiche per il cloud come Aurora + pgvector, AlloyDB, ecc.
Tendo a usare pgvector fino a quando:
- Embeddings > ~5–10 milioni di righe o
- I requisiti di latenza delle query diventano ristretti (< 50ms) con traffico intenso
Schizzo dello schema:
documents - id (pk) - source (kb | ticket | faq) - source_id - content - metadata (jsonb) - created_at document_embeddings - id (pk) - document_id (fk documents.id) - embedding vector(1536) - created_at
Tieni separato il “cosa” (documenti) dal “come lo cerchiamo” (embeddings).
Cambierai modello; non vuoi che questo si mescoli con il tuo contenuto principale.
FAQ: domande sulla struttura che continuo a ricevere
Q: Dovrei memorizzare messaggi completi nel DB o solo riassunti?
Memorizza messaggi completi, almeno per 30–90 giorni. I riassunti sono ottimi per la ricerca e l’analisi, ma quando qualcosa si rompe, vuoi il testo grezzo.
Se sei preoccupato per lo storage, imposta una policy di conservazione e archivia vecchi messaggi in uno storage più economico.
Q: Come gestisco bot multi-tenant (molti clienti)?
Aggiungi un tenant_id ovunque sia importante: users, sessions, messages, conversation_state.
Indicizzalo. Ogni query che tocca i dati utente deve filtrare per tenant_id.
Se hai bisogno di isolamento più rigoroso in seguito, puoi separare i tenant tra schemi o database, ma inizia con una colonna tenant solida.
Q: Dove memorizzo file (immagini, PDF) inviati dagli utenti?
Non nel database. Memorizzali in S3/GCS/Blob storage e conserva i riferimenti:
attachments - id (pk) - message_id (fk messages.id) - file_url - file_type - size_bytes - created_at
Se hai bisogno di eseguire OCR o embedding su di essi, memorizza quell’output in tabelle separate collegate tramite attachment_id.
🕒 Published: