L'expérience de développement avec D1 est vraiment bonne : un SQLite local qui crée automatiquement, des requêtes qui répondent en quelques millisecondes, pas de serveur à configurer, pas de chaîne de connexion à gérer. Cette absence de friction dans le développement tend à créer l’illusion que la banque se comportera de la même manière dans la production. Ce ne sera pas le cas. Le plafond de 2 Go par banque, les sous-requêtes qui s'additionnent, le coût des lignes écrites par UPDATE et l'application de clés étrangères qui nécessitent un opt-in manuel par session sont les quatre limites qui apparaissent en production et que le quickstart ne mentionne jamais.
Le plafond de 2 Go que personne ne prévoit
D1 impose une limite de 2 Go par base de données. Ce nombre est fixe : il n'est pas possible de l'augmenter pour une banque spécifique en achetant de la capacité supplémentaire. Le forfait payant autorise jusqu'à 10 banques D1, ce qui signifie un total théorique de 20 Go répartis entre des instances distinctes.
Pour une simple application CRUD avec un volume de données modeste, 2 Go suffisent pour des années. Pour les applications comportant des journaux, un historique des événements, des téléchargements de données utilisateur ou des tableaux qui augmentent avec l'utilisation, la limite apparaît plus tôt que prévu. Le problème n'est pas d'atteindre 2 Go en soi : c'est le moment où vous réalisez qu'il va arriver, avec des données en production et sans stratégie de partitionnement définie.
La solution la plus simple pour les applications dont la croissance est prévisible consiste à partitionner les données par domaine dès le début : une base de données pour les données transactionnelles actives, une autre pour l'historique, une autre pour les journaux. Une application SaaS peut partitionner par plages de locataires : locataires 1 à 1 000 dans la banque A, 1 001 à 2 000 dans la banque B. Le travailleur décide à quelle banque accéder en fonction de l'ID du locataire, sans que l'utilisateur ne s'en aperçoive. Cette architecture doit être réfléchie avant l'arrivée des premières données, car refactoriser le partitionnement avec une base de production proche de la limite est une opération délicate.
Le coût caché des sous-requêtes
Chaque requête exécutée en D1 compte comme une sous-requête dans le budget du Worker. La limite pour les Workers est de 1 000 sous-requêtes par appel. Cela semble vaste jusqu'à ce que vous définissiez ce que fait une seule requête HTTP : authentifier le jeton (1 requête), charger l'utilisateur (1 requête), vérifier les autorisations (1 requête), récupérer la liste des ressources (1 requête), etc. Dix requêtes sur un point de terminaison sont courantes.
Le problème N+1 transforme rapidement ce nombre. Un point de terminaison qui répertorie 50 commandes, puis récupère les éléments de chaque commande exécute individuellement 1 + 50 = 51 requêtes. Combinée avec 5 lectures KV pour le cache et 3 accès R2 pour les métadonnées, cette requête unique utilise 59 sous-requêtes. Toujours dans la limite, mais avec une petite marge pour les paramètres plus complexes.
db.batch() résout N+1 sans changer la structure des données : vous regroupez plusieurs requêtes en un seul appel, et elles s'exécutent toutes dans une seule sous-requête. Le résultat revient sous forme de tableau avec un élément par requête. Pour le modèle commandes et articles, db.batch() avec les requêtes construites dynamiquement réduit 51 sous-requêtes à 2 — une requête pour les commandes, une avec IN pour tous les articles à la fois.
Amplification d'écriture : ce que signifie réellement les lignes écrites à 1 $/million
Le modèle de facturation D1 pour les écritures s'applique par ligne affectée et non par opération. Un UPDATE qui modifie 5 000 lignes coûte 5 000 écritures, qu'il s'agisse ou non d'un seul appel à la banque. À 1 $ par million de lignes écrites, cette MISE À JOUR coûte 0,005 $ par exécution.
Ce nombre semble faible, mais les opérations par lots ont un effet cumulatif. Une tâche quotidienne qui met à jour l'état de 100 000 enregistrements dans le cadre d'un traitement nocturne coûte 0,10 $ par exécution, soit 3 $ par mois pour cette seule tâche. Multiplié par plusieurs tâches similaires, le coût des écritures peut facilement dépasser celui des lectures.
Le niveau gratuit a une limite de 100 000 lignes écrites par jour. Une seule opération de mise à jour par lots peut consommer la totalité de cette limite. Cela signifie que le niveau gratuit n'est pas compatible avec les pipelines de traitement qui effectuent des mises à jour groupées : pour toute charge de travail avec des écritures groupées, le forfait payant est la seule solution.
Modèles qui réduisent les coûts d'écriture : ajout uniquement au lieu de mise à jour (insertion d'un nouvel enregistrement d'état au lieu de mettre à jour l'existant), compression périodique au lieu de mises à jour continues et traitement par lots plus importants moins fréquemment au lieu de mises à jour granulaires et fréquentes.
FOREIGN KEYS et PRAGMA : l'astuce par session
SQLite n'applique pas les clés étrangères par défaut. Ce comportement est hérité par D1 sans modification. Si vous définissez FOREIGN KEY (user_id) REFERENCES users(id) dans le schéma et insérez une ligne avec un user_id qui n'existe pas dans la table users, D1 accepte l'INSERT sans erreur — sauf si vous avez exécuté PRAGMA foreign_keys = ON dans cette session.
Le détail critique est "dans cette session". PRAGMA ne persiste pas entre les connexions. Chaque invocation de Worker nécessitant l'application d'une clé étrangère doit exécuter PRAGMA en première opération. Si votre code initialise la base de données via un assistant, ajoutez-y PRAGMA - et documentez-le, car cela finira par échapper à l'attention d'un membre de l'équipe.
Le coût de ne pas le faire est silencieux : vous accumulez des enregistrements orphelins sans aucune erreur dans le journal. Découvrir le problème signifie rechercher manuellement les références rompues, et le résoudre signifie décider s'il faut supprimer les enregistrements invalides ou créer les enregistrements parents manquants. Si le volume de données corrompues est important, la correction devient une délicate migration en production.
Un assistant de démarrage qui exécute toujours PRAGMA avant toute autre opération est l'investissement le plus simple possible contre ce problème. La requête PRAGMA foreign_keys = ON prend moins d'une milliseconde. Le temps de débogage des données invalides en production est considérablement plus long.
A lire aussi
- Objets durables en production : à quoi ressemblera la facture et les limites qui surprennent
- Cloudflare Workers en production : ce qui change après hello world
- KV en production : les schémas qui marchent et ceux qui trompent au début
- Workers : limites de CPU et de mémoire — que la documentation n'explique pas bien
- Cloudflare D1 : La base de données SQLite à la périphérie — et pourquoi « edge » ne signifie pas ce qu'il paraît -Requêtes lentes en D1 : comment diagnostiquer et optimiser
