D1 es la base de datos SQLite de Cloudflare, disponible como binding en Workers. Tienes una base de datos SQL real — sintaxis SQLite completa, transacciones, índices — accesible desde el edge sin overhead de conexión TCP.
Para un contador de vistas de un blog, es perfecto. Para un sistema financiero, querrás entender los tradeoffs primero.
El Problema de las Migraciones
D1 no tiene un runner de migraciones integrado. No hay equivalente a db migrate up, ni versionado de esquema, ni rollback. Si necesitas que la tabla exista, la creas tú mismo.
El patrón que funciona: CREATE TABLE IF NOT EXISTS en cada petición relevante.
javascriptasync function initDb(env) {
try {
await env.DB.prepare(
"CREATE TABLE IF NOT EXISTS post_views (slug TEXT PRIMARY KEY, views INTEGER DEFAULT 0)"
).run();
} catch(e) {}
}
Esto parece un desperdicio, pero D1 hace que CREATE TABLE IF NOT EXISTS sea casi un no-op cuando la tabla ya existe. La primera petición crea la tabla; cada petición posterior verifica y sigue en microsegundos. Sin paso de despliegue, sin archivo de migración que mantener.
await vs ctx.waitUntil: Cambia Lo Que el Usuario Ve
Este es el no-obvio.
Cuando un usuario visita un post, quiero incrementar el contador de vistas y mostrar el conteo actualizado en la misma página. El enfoque naive es disparar el incremento con ctx.waitUntil (no bloqueante, corre después de enviar la respuesta) y leer el conteo por separado. El problema: leerías el valor pre-incremento.
javascript// Incorrecto: lee el conteo antes de que el incremento termine
ctx.waitUntil(db.prepare("INSERT OR REPLACE ...").run());
const views = await db.prepare("SELECT views ...").first();
// views es el conteo viejo
javascript// Correcto: espera el incremento, luego lee
await db.prepare("INSERT INTO post_views (slug, views) VALUES (?, 1) ON CONFLICT(slug) DO UPDATE SET views = views + 1").bind(slug).run();
const r = await db.prepare("SELECT views FROM post_views WHERE slug = ?").bind(slug).first();
return r ? r.views : 1;
// views es el conteo actualizado
Esperar cuesta unos 5-10ms. Para un contador de vistas, eso es aceptable. El usuario ve 1 VISTA en su primera visita, no 0 VISTAS con una actualización retardada.
El Patrón Upsert
INSERT OR REPLACE y ON CONFLICT DO UPDATE de SQLite son tus aliados. Para un contador de vistas, quieres: insertar con views=1 si el slug no existe, o incrementar si ya existe. Una sola consulta, atómica.
sqlINSERT INTO post_views (slug, views) VALUES (?, 1)
ON CONFLICT(slug) DO UPDATE SET views = views + 1
Esto es más seguro que SELECT + INSERT/UPDATE condicional, que requeriría una transacción para ser libre de race conditions.
D1 No Es Para Alto Throughput de Escritura
D1 tiene una arquitectura de escritor único — como el propio SQLite, las escrituras están serializadas. Para un blog con lectores ocasionales, esto es irrelevante. Si tienes cientos de peticiones de escritura concurrentes por segundo, encontrarás límites.
El throughput de lectura está bien. D1 usa réplicas de lectura por regiones para consultas SELECT, así que las lecturas escalan horizontalmente sin ninguna configuración.
El límite práctico para este caso de uso: los incrementos del contador de vistas son infrecuentes en comparación con las lecturas. Un post con 10,000 vistas por día tiene un promedio de 7 escrituras por minuto — SQLite lo maneja sin pensarlo.
Desarrollo Local
Los bindings de D1 funcionan en wrangler dev con un archivo SQLite local. El esquema es independiente del de producción, así que necesitas ejecutar tu CREATE TABLE IF NOT EXISTS manualmente o dejar que la primera petición lo haga.
bashwrangler d1 execute dwsa-db --local --command \
"CREATE TABLE IF NOT EXISTS post_views (slug TEXT PRIMARY KEY, views INTEGER DEFAULT 0)"
O simplemente visita una página de post en local dev — initDb corre en la primera petición y crea la tabla automáticamente. Mismo comportamiento que en producción, sin paso extra.
Qué Funciona Bien
D1 es genuinamente bueno para:
- Contadores y agregados simples
- Preferencias o configuraciones de usuario (clave-valor pero consultable)
- Cualquier cosa donde normalmente usarías un archivo SQLite
No es un reemplazo para Postgres o MySQL en sistemas que necesitan claves foráneas aplicadas al nivel de base de datos, joins complejos en datasets grandes, o replicación en streaming. Para un blog, es la herramienta correcta.