TL;DR — En Cassandra no se diseñan tablas, se diseñan consultas. En este artículo construyo una app real en la nube que guarda series de tiempo de estaciones meteorológicas en Apache Cassandra (DataStax Astra DB): particiones acotadas por (estación, día), filas ordenadas por hora, TTL de 30 días y contadores por día. Cada git push se prueba contra un Cassandra real y se despliega solo con GitHub Actions.

🔗 Repositorio: https://github.com/keylalopezz/cassandra-astra-app
🌐 App publicada: https://pulso-tacna.onrender.com
🎥 Video (menos de 5 min): el enlace se agrega al publicar el video
✍️ Artículo complementario (infraestructura y pipeline): Adrian Salas en Dev.to

Pulso Tacna: panel de la app conectada a Astra DB

1. El problema: muchas escrituras, pocas consultas, siempre por tiempo

Una red de estaciones meteorológicas tiene un patrón de datos muy claro:

  • Escribe mucho: cada estación envía una lectura cada pocos minutos, todos los días.
  • Lee siempre de la misma forma: "la última lectura de esta estación" y "las lecturas de las últimas 24 horas".
  • Los datos viejos pierden valor: después de un mes ya no se consultan.

Ese es exactamente el caso para el que nació Apache Cassandra: una base de datos NoSQL orientada a columnas (wide-column store), distribuida y sin un nodo maestro, pensada para escrituras masivas y lecturas por clave.

Para esta actividad no podíamos usar Redis, MongoDB, Neo4j ni Prometheus. Elegí Cassandra porque representa un modelo NoSQL distinto a los que vimos en el curso: ni clave-valor, ni documentos, ni grafos.

2. Arquitectura en una imagen

Navegador ──► Render (ASP.NET Core 10) ──CQL + mTLS──► Astra DB (Apache Cassandra, AWS us-east-2) ▲ git push ──► GitHub Actions: pruebas con Cassandra 5.0 real ─► imagen ghcr.io ─► deploy ─► verificación 
Pieza Tecnología
Base de datos Apache Cassandra en DataStax Astra DB serverless (plan gratuito)
API y panel ASP.NET Core 10 + DataStax C# Driver + Chart.js
Hosting Render (Web Service con la imagen Docker)
Automatización GitHub Actions + Terraform

La app se llama Pulso Tacna: cinco estaciones, desde el Cercado de Tacna (562 m) hasta Candarave (3415 m), registran temperatura y humedad.

3. Primero las consultas, después las tablas

En una base relacional se normaliza y después se piensa en las consultas. En Cassandra es al revés: cada consulta importante tiene su propia tabla, y la clave primaria se diseña para que la consulta lea una sola partición.

Consulta de la app Tabla Cómo se resuelve
Última lectura de una estación lecturas_por_estacion_dia WHERE estacion_id = ? AND dia = ? LIMIT 1
Serie de las últimas 24 h lecturas_por_estacion_dia partición de hoy + partición de ayer
Lecturas de hoy por estación conteo_lecturas_dia un counter
Datos de cada estación estaciones clave simple

4. La clave primaria lo es todo

CREATE TABLE lecturas_por_estacion_dia ( estacion_id text, dia date, ts timestamp, temperatura double, humedad double, PRIMARY KEY ((estacion_id, dia), ts) ) WITH CLUSTERING ORDER BY (ts DESC); 

La clave tiene dos partes:

  • Clave de partición (estacion_id, dia): decide en qué nodo vive el dato. Todas las lecturas de una estación en un mismo día quedan juntas.
  • Columna de agrupamiento ts DESC: ordena las filas dentro de la partición, de la más reciente a la más antigua.

¿Por qué no usar solo estacion_id como partición? Porque esa partición crecería para siempre. Cassandra recomienda particiones acotadas; agregar el día (técnica de bucketing) hace que cada estación genere una partición nueva cada día, de tamaño predecible.

Y como las filas ya están ordenadas por hora descendente, "la última lectura" no necesita ORDER BY ni índices:

SELECT ts, temperatura, humedad FROM lecturas_por_estacion_dia WHERE estacion_id = 'TAC-CANDARAVE' AND dia = '2026-11-15' LIMIT 1; 

Así se ve la partición de Candarave consultada directamente en la consola CQL de Astra DB, ya ordenada:

Consulta de una partición en la consola CQL de Astra DB

⚠️ Un detalle de zona horaria: el "día" de la partición se calcula en hora de Perú (UTC-5), no en UTC. A las 22:00 de Tacna ya es el día siguiente en UTC, y sin ese cuidado la lectura caería en la partición equivocada. Esto está cubierto por una prueba unitaria.

5. TTL: los datos se borran solos

Las lecturas solo interesan 30 días. En lugar de programar un proceso de limpieza, cada escritura lleva su propio tiempo de vida:

INSERT INTO lecturas_por_estacion_dia (estacion_id, dia, ts, temperatura, humedad) VALUES (?, ?, ?, ?, ?) USING TTL 2592000; -- 30 días 

Cuando el TTL vence, Cassandra marca la fila como eliminada y la compacta más tarde. Cero jobs de mantenimiento.

6. Contadores: porque COUNT(*) es caro

En Cassandra, contar las filas de una tabla obliga a recorrer todo el clúster. La solución es mantener un counter que se incrementa en cada escritura:

CREATE TABLE conteo_lecturas_dia ( dia date, estacion_id text, total counter, PRIMARY KEY ((dia), estacion_id) ); UPDATE conteo_lecturas_dia SET total = total + 1 WHERE dia = ? AND estacion_id = ?; 

Con una sola lectura de la partición del día se obtienen los totales de todas las estaciones. Las tablas con contadores solo pueden tener columnas counter fuera de la clave; por eso viven separadas de las lecturas.

Modelo de datos visible en el panel de la app

7. El código con el driver de DataStax

La conexión a Astra DB usa el Secure Connect Bundle (un zip con el host y los certificados mTLS). En local o en CI, la misma clase se conecta a un contenedor de Cassandra:

var builder = Cluster.Builder() .WithQueryOptions(new QueryOptions().SetConsistencyLevel(ConsistencyLevel.LocalQuorum)); builder = esAstra ? builder.WithCloudSecureConnectionBundle(bundlePath).WithCredentials(clientId, secret) : builder.AddContactPoints(host).WithPort(9042); var session = await builder.Build().ConnectAsync(); 

Todas las consultas son sentencias preparadas: Cassandra las analiza una vez y luego solo recibe los valores.

_insertLectura = await _session.PrepareAsync(CqlInsert); _contarLectura = await _session.PrepareAsync(CqlContador); 

8. Escribir en paralelo: de 27 segundos a 0,7

El botón "Historial 24 h" genera 48 rondas (una cada 30 minutos) para las 5 estaciones: 240 lecturas, o sea 480 sentencias. La primera versión las enviaba una por una y tardaba unos 27 segundos, porque cada ida y vuelta a Astra DB cuesta unos 50 ms.

Cassandra está hecha para escrituras concurrentes, así que la segunda versión las envía en paralelo (hasta 32 a la vez) y además manda el INSERT y el contador al mismo tiempo:

await Task.WhenAll( _session.ExecuteAsync(_insertLectura.Bind(n.EstacionId, dia, ts, n.Temperatura, n.Humedad)), _session.ExecuteAsync(_contarLectura.Bind(dia, n.EstacionId))); 

Resultado medido en producción: 240 lecturas en 706 ms.

Historial de 24 horas de Candarave y la consola CQL del panel

La curva diaria se ve tal como la modela el simulador: máximo a media tarde y mínimo de madrugada, con la temperatura de Candarave muy por debajo de la costa por su altitud.

9. Probar contra un Cassandra de verdad

Las pruebas no usan mocks: en GitHub Actions se levanta un contenedor cassandra:5.0 y las pruebas de integración escriben y leen de verdad.

services: cassandra: image: cassandra:5.0 ports: [ "9042:9042" ] options: >- --health-cmd "cqlsh -e 'describe keyspaces'" --health-interval 10s --health-retries 20 

Las 14 pruebas verifican, entre otras cosas, que el "día" de la partición respete la hora de Perú, que el contador suba después de cada escritura, que una lectura fuera de rango devuelva 400 y que el historial cubra al menos 23 horas. Si una sola falla, no se publica nada.

Pipeline: pruebas, imagen y despliegue

El detalle de la infraestructura (Terraform, Astra DB, Render) y del pipeline completo está en el artículo de mi compañero Adrian.

10. Lo que aprendí

  1. Las consultas mandan. Antes de escribir un CREATE TABLE hay que tener la lista de consultas. Si mañana necesitara "todas las estaciones con temperatura mayor a 30 °C", crearía otra tabla para esa consulta, no un índice ni un ALLOW FILTERING.
  2. Particiones acotadas. Agregar el día a la clave de partición evita particiones gigantes y mantiene el rendimiento estable con el paso del tiempo.
  3. TTL y contadores resuelven tareas que en SQL requieren procesos aparte.
  4. Concurrencia. Con Cassandra conviene enviar muchas escrituras pequeñas en paralelo: pasé de 27 s a 0,7 s sin cambiar el modelo.
  5. Serverless tiene letra pequeña. La base gratuita de Astra DB se hiberna si nadie la usa; por eso un workflow programado envía lecturas cada hora y la mantiene activa.

Autores: Keyla Noelia Lopez Ojeda y Adrian Fabricio Salas Acuña — Base de Datos II, Escuela Profesional de Ingeniería de Sistemas, Universidad Privada de Tacna.

🔗 Repositorio: https://github.com/keylalopezz/cassandra-astra-app · 🌐 App: https://pulso-tacna.onrender.com