L'esperienza di sviluppo con D1 è davvero buona: un SQLite locale che crea automaticamente, query che rispondono in pochi millisecondi, nessun server da configurare, nessuna stringa di connessione da gestire. Questo attrito zero nello sviluppo tende a creare l’illusione che la banca si comporti allo stesso modo nella produzione. Non lo farà. Il tetto di 2 GB per banca, le sottorichieste che si sommano, il costo delle righe scritte da UPDATE e l'applicazione di chiavi esterne che richiedono l'attivazione manuale per sessione sono i quattro limiti che compaiono nella produzione e che Quickstart non menziona mai.
Il tetto massimo di 2 GB che nessuno prevede
D1 impone un limite di 2 GB per database. Questo numero è fisso: non è possibile aumentarlo per una banca specifica acquistando capacità aggiuntiva. Il piano a pagamento consente fino a 10 banche D1, il che significa un totale teorico di 20 GB distribuiti tra istanze separate.
Per una semplice applicazione CRUD con un volume di dati modesto, 2 GB sono sufficienti per anni. Per le applicazioni con log, cronologia eventi, caricamenti di dati utente o tabelle che crescono con l'utilizzo, il limite viene visualizzato prima del previsto. Il problema non è raggiungere i 2 GB in sé: è il momento in cui realizzi che arriveranno, con i dati in produzione e senza una strategia di partizionamento definita.
Il percorso più pulito per le applicazioni con crescita prevedibile è partizionare i dati per dominio fin dall'inizio: un database per i dati transazionali attivi, un altro per la cronologia, un altro per i log. Un'applicazione SaaS può essere suddivisa in base a intervalli di tenant: tenant da 1 a 1000 nella banca A, da 1001 a 2000 nella banca B. Il lavoratore decide a quale banca accedere in base all'ID tenant, senza che l'utente se ne accorga. Questa architettura deve essere pensata prima che arrivino i primi dati, perché refactoring del partizionamento con un database di produzione vicino al limite è un'operazione delicata.
Il costo nascosto delle sottorichieste
Ogni query eseguita in D1 conta come una sottorichiesta nel budget del lavoratore. Il limite per i Worker è di 1000 sottorichieste per invocazione. Sembra ampio finché non si delinea cosa fa una singola richiesta HTTP: autenticare il token (1 query), caricare l'utente (1 query), controllare le autorizzazioni (1 query), recuperare l'elenco delle risorse (1 query) e così via. Sono comuni dieci query su un endpoint.
Il problema N+1 trasforma rapidamente questo numero. Un endpoint che elenca 50 ordini e quindi recupera gli articoli da ciascun ordine esegue individualmente 1 + 50 = 51 query. Combinata con 5 letture KV per la cache e 3 accessi R2 per i metadati, questa singola richiesta utilizza 59 sottorichieste. Ancora entro il limite, ma con un piccolo margine per endpoint più complessi.
db.batch() risolve N+1 senza modificare la struttura dei dati: raggruppi più query in un'unica chiamata e tutte vengono eseguite in un'unica sottorichiesta. Il risultato ritorna come un array con un elemento per query. Per il modello di ordini e articoli, db.batch() con le query costruite dinamicamente riduce 51 sottorichieste a 2: una query per gli ordini, una con IN per tutti gli articoli contemporaneamente.
Amplificazione della scrittura: cosa significa realmente 1 dollaro per milione di righe scritte
Il modello di fatturazione D1 per le scritture è per riga interessata, non per operazione. Un UPDATE che modifica 5.000 righe costa 5.000 scritture, indipendentemente dal fatto che si tratti di una singola chiamata alla banca. Con un costo di 1 dollaro per milione di righe scritte, questo AGGIORNAMENTO costa 0,005 dollari per esecuzione.
Questo numero sembra piccolo, ma le operazioni batch hanno un effetto cumulativo. Un lavoro giornaliero che aggiorna lo stato di 100.000 record come parte dell'elaborazione notturna costa $ 0,10 per esecuzione, $ 3 al mese solo per quel lavoro. Moltiplicato per numerosi lavori simili, il costo delle scritture può facilmente superare il costo delle letture.
Il livello gratuito ha un limite di 100mila righe scritte al giorno. Una singola operazione di aggiornamento batch può consumare l'intero limite. Ciò significa che il livello gratuito non è compatibile con le pipeline di elaborazione che eseguono aggiornamenti in blocco: per qualsiasi carico di lavoro con scritture in blocco, il piano a pagamento è l'unica strada da percorrere.
Modelli che riducono i costi di scrittura: sola aggiunta invece di aggiornamento (inserimento di un nuovo record di stato invece di aggiornare quello esistente), compressione periodica invece di aggiornamenti continui ed elaborazione in batch più grandi meno frequentemente invece di aggiornamenti granulari e frequenti.
CHIAVI STRANIERE e PRAGMA: il trucco per sessione
SQLite non applica chiavi esterne per impostazione predefinita. Questo comportamento viene ereditato da D1 senza modifiche. Se definisci FOREIGN KEY (user_id) REFERENCES users(id) nello schema e inserisci una riga con user_id che non esiste nella tabella users, D1 accetta l'INSERT senza errori, a meno che tu non abbia eseguito PRAGMA foreign_keys = ON in quella sessione.
Il dettaglio critico è "in quella sessione". PRAGMA non persiste tra le connessioni. Ogni invocazione di Worker che necessita dell'applicazione di una chiave esterna deve eseguire PRAGMA come prima operazione. Se il tuo codice inizializza il database tramite un helper, aggiungi PRAGMA lì e documentalo, perché alla fine sfuggirà all'attenzione di qualcuno nel team.
Il costo di non farlo è silenzioso: si accumulano record orfani senza errori nel registro. Scoprire il problema significa cercare manualmente i riferimenti interrotti e risolverlo significa decidere se eliminare i record non validi o creare i record principali mancanti. Se il volume dei dati corrotti è elevato, la correzione diventa una delicata migrazione verso la produzione.
Un assistente all'avvio che esegue sempre PRAGMA prima di qualsiasi altra operazione è l'investimento più semplice possibile contro questo problema. La query PRAGMA foreign_keys = ON richiede meno di un millisecondo. Il tempo per eseguire il debug dei dati non validi in produzione è notevolmente più lungo.
Leggi anche
- Oggetti durevoli in produzione: come sarà la bolletta e i limiti che sorprendono
- Operatori Cloudflare in produzione: cosa cambia dopo hello world
- KV nella produzione: gli schemi che funzionano e quelli che ingannano all'inizio
- Lavoratori: limiti di CPU e memoria - che la documentazione non spiega bene
- Cloudflare D1: Il database SQLite all'edge — e perché "edge" non significa ciò che sembra
- Query lente in D1: come diagnosticare e ottimizzare
