01 · Punto de partida
De la cola local a la historia compartida
En el tema 33 el dispositivo guardaba registros, esperaba confirmaciones y reintentaba con una identidad estable. Ahora seguimos el recorrido en el servidor. La base debe reconocer esos reintentos y conservar una historia que se pueda consultar por dispositivo y tiempo.
La nube no es un formato de archivo ni una clase de base de datos: es dónde se ejecuta y administra una parte del sistema. Un servidor propio o un servicio gestionado pueden almacenar las mismas series, con distintas responsabilidades de operación. El diseño del dato sigue siendo necesario en ambos casos.
Con HTTP, una API recibe el lote. Con MQTT, un consumidor se suscribe y lo procesa. En ambos casos hay una frontera que importa: recibir un mensaje no equivale a confirmar su almacenamiento. El servicio devuelve el resultado después de la transacción requerida, con la identidad del registro al que se refiere.
Usamos SQLite en memoria para que el ejemplo sea reproducible sin cuentas ni servidores externos. Ejecuta SQL real y muestra transacciones; no representa una base desplegada ni demuestra su capacidad, disponibilidad o durabilidad ante cortes.
02 · Organizar la historia
Qué es una serie temporal
Una serie temporal es una secuencia de observaciones asociadas al tiempo. «Temperatura de la cámara 07» es una serie; la temperatura de la cámara 08 es otra. Cada observación conserva valor, unidad, calidad y contexto suficiente para interpretarla.
| Categoría | Ejemplo | Cómo se usa |
|---|---|---|
| Identidad del registro | camara07 / arranque 42 / secuencia 6 | Reconocer un reintento |
| Identidad de la serie | camara07 / temperatura | Filtrar la historia de una magnitud |
| Tiempo y valor | 00:05 UTC, 9,2 °C | Ubicar la observación en el proceso |
| Calidad | Válida o adquisición fallida | Decidir qué se puede promediar |
| Metadatos del equipo | Sitio, modelo y firmware | Interpretar y administrar la flota |
La clave de serie no tiene por qué ser la clave del registro. Un dispositivo puede producir miles de lecturas de la misma magnitud; cada una necesita identidad, pero todas pertenecen a la misma serie. Un tiempo tampoco identifica por sí solo una lectura: dos sensores pueden medir en el mismo milisegundo.
Lecturas, eventos y estado actual
Las lecturas describen cómo evolucionó una magnitud. Los eventos describen transiciones, como «temperatura alta activada». El estado actual responde otra pregunta: «¿cuál es la última situación conocida?». Conviene modelarlos por separado, aunque compartan identidad de dispositivo y referencias de tiempo.
Una tabla con una sola fila por dispositivo sirve para consultar su estado actual, pero sobrescribir esa fila en cada mensaje pierde la historia. Una tabla histórica no resuelve automáticamente la consulta del estado actual si los datos llegan desordenados: actualizar «último valor» con cada recepción puede hacer retroceder el panel.
Magnitudes y dimensiones
Podés tener tablas por tipo de lectura o una tabla más general con magnitud y unidad normalizadas. En este tema hay una sola magnitud, temperatura en décimas de °C, así que la columna valor_dc fija la unidad. No guardamos a veces grados Celsius y a veces Fahrenheit bajo el mismo nombre.
Las dimensiones que definen series deben tener un propósito de consulta. Si un motor indexa combinaciones de etiquetas, agregar un identificador único de mensaje como etiqueta puede multiplicar el número de series. La identidad de entrega pertenece al diseño de deduplicación; no tiene por qué convertirse en una nueva serie por punto.
03 · Dos relojes, dos preguntas
Medición, recepción y reloj
Un equipo que recupera la red puede entregar a las 10:20 una lectura tomada a las 10:05. Si la guardás con la hora de llegada como si fuera la de medición, el gráfico moverá el incidente quince minutos. Conservá ambos tiempos:
Tiempo de medición
Cuándo se adquirió el dato en el dispositivo. Ordena la evolución física y permite agrupar por el período en que ocurrió.
Tiempo de recepción
Cuándo el servidor lo recibió. Permite estudiar retraso, tráfico e interrupciones del transporte.
En el ejemplo, medido_ms y recibido_ms son enteros con milisegundos desde el origen Unix. Se interpretan como instantes UTC; la zona horaria se aplica al mostrar fechas. El origen de la traza es 2026-01-01T00:00:00Z, equivalente a 1767225600000 ms.
No confundas milisegundos desde el arranque con milisegundos Unix. El registro local del tema 33 llevaba un contador de tiempo relativo. Para situarlo en una fecha hace falta una relación conocida entre ese contador y un reloj sincronizado, junto con la identidad de arranque y la calidad de esa relación. Si no existe, no se inventa una fecha de adquisición.
Cuando el reloj del dispositivo no es confiable
Podés conservar la marca relativa, la fecha de recepción y una calidad de tiempo que indique «sin sincronizar». Una reconstrucción posterior debe quedar identificada como estimación. Cambiar silenciosamente la fecha de una fila ya aceptada también cambiaría ventanas, retención y resultados históricos.
La resta recibido − medido aproxima el retraso total solo cuando los relojes son comparables. Un resultado negativo puede indicar un reloj adelantado; uno enorme, una cola atrasada o una fecha equivocada. Validá límites y registrá anomalías sin corregir todos los tiempos a la hora del servidor.
Las consultas de este tema usan intervalos [desde, hasta): incluyen el inicio y excluyen el final. Así, un punto exactamente a las 00:05 pertenece a la ventana de 00:05–00:10 y no se cuenta también en la anterior.
04 · Un contrato persistente
Un esquema para las lecturas
Este esquema es SQL para SQLite. La clave primaria implementa la identidad del tema anterior; el índice responde a la consulta habitual «lecturas de un dispositivo entre dos instantes». En una base como PostgreSQL, las marcas Unix en milisegundos necesitan un tipo de 64 bits, como BIGINT, o un tipo temporal adecuado: no se copia sin revisar el significado de cada tipo.
CREATE TABLE lecturas (
dispositivo TEXT NOT NULL,
arranque INTEGER NOT NULL,
secuencia INTEGER NOT NULL,
medido_ms INTEGER NOT NULL,
recibido_ms INTEGER NOT NULL,
valor_dc INTEGER,
calidad INTEGER NOT NULL CHECK (calidad IN (0, 1)),
PRIMARY KEY (dispositivo, arranque, secuencia),
CHECK (
(calidad = 0 AND valor_dc IS NULL) OR
(calidad = 1 AND valor_dc IS NOT NULL
AND typeof(valor_dc) = 'integer' AND valor_dc BETWEEN -400 AND 1250)
)
);
CREATE INDEX idx_lecturas_dispositivo_tiempo
ON lecturas(dispositivo, medido_ms);La adquisición fallida se almacena con calidad cero y valor NULL. No se reemplaza por 0 °C, porque sería una temperatura válida y alteraría mínimos y promedios. La ausencia de una fila, en cambio, significa que no tenemos ese registro: no demuestra que hubo una adquisición fallida.
La validación del servicio verifica además tipos, identificadores, unidades y límites temporales antes de escribir. Las restricciones de la base son una segunda defensa. SQLite tiene reglas de tipado propias; el ejemplo no pretende que la palabra INTEGER valide por sí sola todo lo recibido. Ver la documentación de CREATE TABLE de SQLite.
Separar identidad de autorización
Que una carga JSON diga "dispositivo": "camara07" no demuestra que tenga permiso para escribir como ese equipo. El servicio identifica al emisor a partir de credenciales y comprueba su pertenencia al sitio o cliente. En una plataforma con varios clientes, esa separación debe estar presente en claves, consultas y permisos.
El programa didáctico admite solo camara07 y no implementa una API autenticada. Un dispositivo no debería llevar las credenciales administrativas de la base ni conectarse a ella directamente para ejecutar SQL arbitrario. La API de ingesta valida el contrato y limita lo que puede hacer.
05 · El otro extremo del reintento
Recibir sin duplicar ni sobrescribir
Una ingesta es idempotente cuando repetir el mismo registro no agrega otro efecto lógico. El transporte puede repetir; la base conserva una sola lectura con esa identidad. La restricción única debe estar en la base: preguntar «¿existe?» y luego insertar sin una restricción permite que dos receptores simultáneos creen duplicados.
INSERT INTO lecturas
(dispositivo, arranque, secuencia, medido_ms, recibido_ms, valor_dc, calidad)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(dispositivo, arranque, secuencia) DO NOTHING;En SQLite, ON CONFLICT puede actuar sobre una restricción única, como describe su documentación de UPSERT. Eso solo resuelve la colisión: después hay que comprobar que el contenido original coincide. El mismo identificador con otra temperatura no es un reintento legítimo que deba ignorarse sin diagnóstico.
| Situación | Resultado | Qué hacer |
|---|---|---|
| Identidad nueva y dato válido | Insertado y confirmado | El dispositivo puede avanzar su cursor |
| Misma identidad y mismo contenido | Duplicado reconocido | Confirmar otra vez sin agregar una fila |
| Misma identidad y contenido diferente | Conflicto | No sobrescribir; registrar y resolver la causa |
| Datos que no cumplen el contrato | Rechazado | Respuesta explícita; evitar reintentos infinitos del mismo error |
| No se pudo confirmar la transacción | Sin confirmación de éxito | Reintentar con la misma identidad |
La primera fecha de recepción se mantiene para el registro original; un reintento no la reemplaza. Si necesitás auditar cada intento, se registra en otro lugar. Así, estudiar el retraso inicial no queda alterado por el momento del último reenvío.
La transacción marca la frontera del ACK
El servicio valida, inserta o reconoce el duplicado, confirma la transacción y recién entonces devuelve el ACK de aplicación. Si se corta la respuesta después de confirmar, el siguiente intento se reconoce como duplicado. Si la transacción falla, no se anuncia que el registro quedó guardado.
Si también hay que actualizar un estado actual en la misma base, puede hacerse dentro de esa transacción, con una comparación que impida reemplazarlo por una medición más antigua. Si hay que disparar un efecto externo, como enviar un aviso, la transacción de la base no lo vuelve atómico por sí sola: puede guardarse una tarea pendiente y procesarla con su propia identidad de reintento.
Un ACK de un lote debe especificar qué registros quedaron aceptados. Confirmar solo «hasta la secuencia 100» supone que no hay huecos anteriores. Un resultado individual o un rango contiguo explícito evita que el dispositivo borre lecturas que el servidor todavía no tiene.
06 · Preguntas concretas
Consultas e índices
Una base útil responde preguntas definidas. En este curso necesitamos, al menos, la historia de una cámara en un intervalo, su última lectura válida y estadísticas por ventanas. Los parámetros se pasan separados del SQL, sin concatenar texto recibido desde dispositivos o formularios.
Historia por dispositivo e intervalo
SELECT medido_ms, valor_dc, calidad
FROM lecturas
WHERE dispositivo = ? AND medido_ms >= ? AND medido_ms < ?
ORDER BY medido_ms, arranque, secuencia
LIMIT ?;El índice (dispositivo, medido_ms) acompaña la igualdad por dispositivo y el rango de tiempo. El índice único de identidad resuelve otra necesidad; no ordena toda la historia por fecha. Los índices aceleran ciertos accesos a cambio de espacio y trabajo de escritura. La documentación del planificador de SQLite explica cómo se aprovechan los índices de varias columnas.
Revisá el plan con EXPLAIN QUERY PLAN y con un volumen representativo. Crear un índice por cada columna sin observar las consultas no es un método de diseño. Para paginar grandes historias, usá un cursor que incluya el tiempo y un desempate estable; el tiempo solo puede repetirse.
Última lectura válida
SELECT medido_ms, valor_dc
FROM lecturas
WHERE dispositivo = ? AND calidad = 1
ORDER BY medido_ms DESC, arranque DESC, secuencia DESC
LIMIT 1;Última por medición no es última por recepción. En nuestra traza, la medición más reciente es 4,0 °C en el minuto 11. La lectura de 6,5 °C del minuto 2 llega en el 12. Ordenar por recepción la pondría como «última», aunque describe un estado anterior.
La última válida tampoco demuestra que el dato sea fresco. Si después hubo fallas de adquisición, el panel debe mostrar la antigüedad y la calidad reciente. No alcanza con devolver una temperatura histórica sin su marca de tiempo ni señalar cuánto hace que no hay una medición válida.
Cuántos datos faltan
Una consulta sin filas no debe inventar ceros. Se puede comparar la cantidad observada con la esperada para un período conocido, pero esa expectativa depende de la estrategia: con banda muerta o muestreo adaptativo, del tema 31, no se espera una fila por minuto. Los resúmenes deben conservar contadores de adquisición si se quiere medir cobertura física.
07 · Menos detalle, preguntas más rápidas
Resumir sin esconder los picos
Agrupar datos por ventanas reduce lo que hay que leer para mostrar semanas o meses. Una ventana de cinco minutos puede conservar cantidad total de filas, cantidad válida, suma, mínimo y máximo. La media se obtiene de suma / validas; la división no incluye lecturas inválidas.
-- Ejemplo SQLite: ventana de 300000 ms, alineada al origen Unix.
SELECT (medido_ms / 300000) * 300000 AS inicio_ms,
COUNT(*) AS total,
COUNT(valor_dc) AS validas,
SUM(valor_dc) AS suma_dc,
MIN(valor_dc) AS min_dc,
MAX(valor_dc) AS max_dc
FROM lecturas
WHERE dispositivo = ? AND medido_ms >= ? AND medido_ms < ?
GROUP BY inicio_ms
ORDER BY inicio_ms;Para los enteros positivos de este ejemplo, la división entera de SQLite calcula el bloque. En otros dialectos se usa la función temporal o expresión equivalente. COUNT(*) cuenta filas; COUNT(valor_dc) cuenta valores no nulos. Las funciones agregadas de SQLite documentan ese tratamiento de NULL, suma y cantidad.
Las cinco mediciones de los minutos 0 a 4 son 4,0; 4,1; 6,5; 4,2 y 9,0 °C. Su media es 5,56 °C y su máximo 9,0 °C. Mostrar solo la media ocultaría que hubo una lectura alta. El siguiente bloque tiene cinco filas, pero solo cuatro válidas; su media es 5,325 °C y su máximo 9,2 °C.
Guardar suma y cantidad permite volver a resumir
La media de los tres bloques de nuestra traza no se obtiene promediando sus medias sin ponderar: tienen cinco, cuatro y dos lecturas válidas. Guardar suma y cantidad permite combinarlos correctamente. Para los doce registros, con once valores válidos, la suma es 572 décimas y la media global es 5,2 °C.
Mínimo combinado = mínimo de los mínimos
Máximo combinado = máximo de los máximos
Una media de muestras tampoco es automáticamente una media ponderada por tiempo. Si el dispositivo informa más seguido cuando la temperatura sube, esos instantes aportan más filas. Para calcular cuánto tiempo estuvo sobre un umbral hace falta definir cómo se interpreta la señal entre mediciones y qué ocurre en los huecos.
El máximo no conserva la forma completa de la señal ni la duración del incidente. Si esa información es necesaria, se conserva más detalle o se almacenan también eventos. Un resumen es otra forma de perder información a cambio de ahorrar recursos, como vimos en el procesamiento local.
08 · Qué se conserva y hasta cuándo
Retención y datos tardíos
Una política puede conservar lecturas crudas durante 30 días, resúmenes de cinco minutos durante un año y eventos durante otro plazo. Son ejemplos de niveles de detalle, no períodos universales. La elección depende de las consultas, los compromisos del producto y el costo medido.
- Definir cortes: qué tiempo gobierna cada retención y qué retraso se admite.
- Construir o corregir resúmenes: incluir los registros que llegaron dentro del plazo permitido.
- Confirmar resultados: cantidades, cobertura e integridad del trabajo terminado.
- Eliminar lo que venció: después de disponer de lo que debía conservarse.
La retención de lecturas puede seguir el tiempo de medición, mientras que los registros de recepción siguen el de llegada. Una lectura de hace tres días recibida hoy puede estar ya fuera del horizonte crudo. El servicio necesita una respuesta definida: aceptarla en otro nivel, actualizar un resumen o rechazarla como fuera de plazo. No debe entrar y salir sin que se sepa si pudo contribuir a la historia.
Una ventana cerrada todavía puede recibir datos
La ventana [00:00, 00:05) terminó a las 00:05, pero la lectura de las 00:02 llega a las 00:12. «Terminó el intervalo» y «ya no espero datos» son conceptos distintos. Se puede dar un período de gracia y publicar resúmenes provisionales hasta que venza.
Al incorporar un dato tardío, recalculá y reemplazá el resumen de esa ventana desde la fuente completa o aplicá un incremento que esté ligado de manera transaccional a una identidad nueva. Volver a sumar toda la ventana sobre el resultado anterior duplicaría cantidades. El ejemplo usa reemplazo del resultado recalculado, y verifica que ejecutarlo dos veces no cambia los totales.
Si ya borraste parte de los crudos, no podés recalcular la ventana completa únicamente con lo que quedó. Conservá la fuente durante el plazo de corrección o diseñá otra estrategia. El laboratorio recibe toda la traza antes de resumir y purgar; no simula llegadas nuevas después de ese corte.
La identidad también necesita un plazo de conservación
Si la clave de deduplicación solo existe en la fila cruda y eliminás esa fila, un reintento antiguo podría volver a parecer nuevo. La política de retención debe considerar cuánto tiempo el dispositivo puede reintentar. Se pueden conservar recibos de identidad más tiempo que los crudos, o definir una admisión con fecha límite y respuesta explícita para lo vencido.
Conservar resúmenes no conserva automáticamente esas identidades. Si el servicio recibe un registro ya resumido, no puede decidir si es nuevo mirando solo mínimo, máximo y cantidad. La retención de crudos, la de resúmenes y la de recibos de entrega cumplen funciones diferentes.
Evitar consultas que cuentan dos veces
Al combinar historia resumida con detalle reciente, usá intervalos sin solapamiento. Por ejemplo, resúmenes anteriores a las 10:00 y crudos desde las 10:00. Sumar el resumen 09:55–10:00 y además los crudos de 09:58–10:00 cuenta parte de la ventana dos veces.
En tablas grandes, particionar por períodos puede simplificar la gestión de datos antiguos. No es lo mismo que un índice: el particionado divide físicamente una tabla lógica. PostgreSQL documenta tanto el descarte de particiones como las restricciones de unicidad asociadas al diseño en su guía de particionado. Una identidad única global no debe darse por supuesta al repartir datos por fecha.
09 · Volumen y herramientas
Capacidad y elección del almacenamiento
La cantidad de dispositivos sola no define el tamaño del problema. Importan cuántas magnitudes guarda cada uno, con qué período, cuántos días de detalle se conservan y qué consultas hay que responder:
Filas retenidas ≈ filas por día × días
Espacio lógico ≈ filas retenidas × bytes lógicos por fila
Con 100 dispositivos, una magnitud y una lectura cada 60 s, se generan 144.000 filas al día. En 30 días son 4.320.000 filas. Si el presupuesto lógico es de 64 bytes por fila, resultan 276.480.000 bytes: aproximadamente 263,7 MiB. Ese tamaño es un supuesto del cálculo, no una medición del archivo SQLite ni del registro binario de 24 bytes del tema anterior.
Si para una estimación reservás un factor 3 por tablas, índices y sobrecarga, y mantenés dos copias completas de ese almacenamiento, la cuenta da unos 1,545 GiB. Faltan las copias de seguridad, los registros de transacciones y el margen operativo. El factor real se mide con datos y consultas representativos; no existe un multiplicador universal.
| Familia | Qué facilita | Qué comprobar |
|---|---|---|
| Base relacional | Restricciones, transacciones y relaciones con equipos y clientes | Índices, concurrencia y mantenimiento con el volumen esperado |
| Motor o extensión de series temporales | Operaciones y organización orientadas a tiempo | Identidad de puntos, datos tardíos, etiquetas y políticas de retención |
| Archivos en almacenamiento de objetos | Archivo histórico y procesamiento por lotes | Catálogo, formato, compresión y costo de recuperar lo necesario |
Una base relacional con tiempos e índices ya puede representar series temporales. Un motor especializado puede ofrecer herramientas que reduzcan trabajo operativo, pero no elimina las decisiones sobre duplicados, unidades o relojes. Guardar un JSON gigantesco por dispositivo tampoco sustituye índices ni consultas estructuradas.
SQLite permite practicar sin un proceso de servidor separado, como explica la referencia de sqlite3 de Python. Llevar el diseño a un servicio concurrente implica evaluar otro entorno de ejecución, conexión, permisos, respaldo y configuración de persistencia. El ejemplo no establece cuántos dispositivos soportaría un despliegue.
Los costos incluyen ingestión, almacenamiento, lecturas, transferencia y operación. Antes de comparar proveedores, medí el patrón de uso: cuántas filas llegan, cuánto reintento hay, qué horizontes se consultan y cuántos puntos necesita realmente el panel. El tema siguiente profundiza qué telemetría conviene producir.
10 · SQL real, sin infraestructura externa
Servidor de datos reproducible en Python
El programa crea una base SQLite en memoria, ingresa trece entregas que representan doce registros, reconoce el reintento y detecta un conflicto de contenido. Luego consulta el último valor, genera resúmenes de cinco minutos y conserva solo los crudos de los últimos cinco minutos de la traza.
El origen T0 es medianoche UTC y la observación se cierra en el minuto 15. El registro del minuto 2 llega en el 12; el del minuto 5 se repite en el 13; el minuto 8 tiene calidad inválida. Para simplificar, el reloj de medición ya está sincronizado y la autenticación real queda fuera del ejemplo.
Python en el navegador. La primera ejecución descarga Pyodide y el módulo opcional sqlite3. La base es temporal y se vuelve a crear al ejecutar. También podés copiar el bloque a un archivo Python 3 y ejecutarlo localmente.
import sqlite3
MINUTO = 60000
T0 = 1767225600000
AHORA = T0 + 15 * MINUTO
db = sqlite3.connect(":memory:", isolation_level=None)
db.executescript("""
CREATE TABLE lecturas (
dispositivo TEXT NOT NULL, arranque INTEGER NOT NULL,
secuencia INTEGER NOT NULL, medido_ms INTEGER NOT NULL,
recibido_ms INTEGER NOT NULL, valor_dc INTEGER,
calidad INTEGER NOT NULL CHECK(calidad IN (0, 1)),
PRIMARY KEY(dispositivo, arranque, secuencia),
CHECK((calidad = 0 AND valor_dc IS NULL) OR
(calidad = 1 AND valor_dc IS NOT NULL
AND typeof(valor_dc) = 'integer' AND valor_dc BETWEEN -400 AND 1250))
);
CREATE INDEX idx_lecturas_dispositivo_tiempo
ON lecturas(dispositivo, medido_ms);
CREATE TABLE resumenes (
dispositivo TEXT NOT NULL, ventana_ms INTEGER NOT NULL,
inicio_ms INTEGER NOT NULL, total INTEGER NOT NULL,
validas INTEGER NOT NULL, suma_dc INTEGER NOT NULL,
min_dc INTEGER, max_dc INTEGER,
PRIMARY KEY(dispositivo, ventana_ms, inicio_ms)
);
""")
def recibir(fila):
dispositivo, boot, seq, medido, recibido, valor, calidad = fila
if dispositivo != "camara07":
raise ValueError("Dispositivo fuera del ejemplo")
if any(type(v) is not int for v in (boot, seq, medido, recibido, calidad)):
raise ValueError("Tipos inválidos")
if not (boot >= 0 and seq > 0 and T0 <= medido < AHORA
and T0 <= recibido < AHORA and calidad in (0, 1)):
raise ValueError("Identidad, tiempo o calidad inválidos")
if ((calidad == 0 and valor is not None) or
(calidad == 1 and (type(valor) is not int or not -400 <= valor <= 1250))):
raise ValueError("Valor incompatible con calidad")
db.execute("BEGIN IMMEDIATE")
try:
cur = db.execute("""
INSERT INTO lecturas VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(dispositivo, arranque, secuencia) DO NOTHING
""", fila)
nueva = cur.rowcount == 1
if not nueva:
anterior = db.execute("""
SELECT medido_ms, valor_dc, calidad FROM lecturas
WHERE dispositivo = ? AND arranque = ? AND secuencia = ?
""", (dispositivo, boot, seq)).fetchone()
if anterior != (medido, valor, calidad):
raise ValueError("Misma identidad con contenido diferente")
db.execute("COMMIT")
except Exception:
if db.in_transaction:
db.execute("ROLLBACK")
raise
# Aquí recién se puede emitir el ACK de aplicación con esa identidad.
return "nueva" if nueva else "duplicada"
valores = [40, 41, 65, 42, 90, 92, 40, 41, None, 40, 41, 40]
orden = [0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 2, 5]
resultados = []
for posicion, i in enumerate(orden):
minuto_rx = i if posicion < 11 else posicion + 1
fila = ("camara07", 42, i + 1, T0 + i * MINUTO,
T0 + minuto_rx * MINUTO + 500, valores[i], int(valores[i] is not None))
resultados.append(recibir(fila))
assert resultados.count("nueva") == 12 and resultados.count("duplicada") == 1
print("Entregas: 13; filas: 12; duplicados reconocidos: 1")
# El mismo ID con otro contenido se rechaza, no se sobrescribe.
try:
recibir(("camara07", 42, 6, T0 + 5 * MINUTO, T0 + 14 * MINUTO, 99, 1))
except ValueError:
pass
else:
raise AssertionError("No se detectó el conflicto")
assert db.execute("SELECT valor_dc FROM lecturas WHERE secuencia=6").fetchone() == (92,)
ultima = db.execute("""
SELECT medido_ms, valor_dc FROM lecturas
WHERE dispositivo=? AND calidad=1
ORDER BY medido_ms DESC, arranque DESC, secuencia DESC LIMIT 1
""", ("camara07",)).fetchone()
por_llegada = db.execute("""
SELECT medido_ms, valor_dc FROM lecturas
WHERE dispositivo=? AND calidad=1 ORDER BY recibido_ms DESC LIMIT 1
""", ("camara07",)).fetchone()
assert ultima == (T0 + 11 * MINUTO, 40)
assert por_llegada == (T0 + 2 * MINUTO, 65)
print("Última por medición: 4,0 °C; por primera recepción: 6,5 °C")
def resumir():
ventana = 5 * MINUTO
db.execute("BEGIN IMMEDIATE")
try:
db.execute("""
INSERT INTO resumenes
SELECT dispositivo, ?, (medido_ms / ?) * ? AS inicio,
COUNT(*), COUNT(valor_dc), COALESCE(SUM(valor_dc), 0),
MIN(valor_dc), MAX(valor_dc)
FROM lecturas WHERE medido_ms >= ? AND medido_ms < ?
GROUP BY dispositivo, inicio
ON CONFLICT(dispositivo, ventana_ms, inicio_ms) DO UPDATE SET
total=excluded.total, validas=excluded.validas,
suma_dc=excluded.suma_dc, min_dc=excluded.min_dc, max_dc=excluded.max_dc
""", (ventana, ventana, ventana, T0, AHORA))
db.execute("COMMIT")
except Exception:
if db.in_transaction:
db.execute("ROLLBACK")
raise
resumir()
resumir() # Recalcular reemplaza el resultado; no suma otra vez.
grupos = db.execute("""
SELECT inicio_ms, total, validas, suma_dc, min_dc, max_dc
FROM resumenes ORDER BY inicio_ms
""").fetchall()
assert grupos == [(T0, 5, 5, 278, 40, 90),
(T0 + 5 * MINUTO, 5, 4, 213, 40, 92),
(T0 + 10 * MINUTO, 2, 2, 81, 40, 41)]
print("Ventana total válidas mínimo media máximo")
for inicio, n, validas, suma, minimo, maximo in grupos:
minuto = (inicio - T0) // MINUTO
print(f"[{minuto:2},{minuto+5:2}) {n:5} {validas:7}"
f" {minimo/10:7.1f} {suma/validas/10:7.3f} {maximo/10:8.1f}")
assert sum(g[3] for g in grupos) / sum(g[2] for g in grupos) / 10 == 5.2
# Comprobar el límite superior exclusivo: minuto 5 va en la segunda ventana.
assert db.execute("""
SELECT COUNT(*) FROM lecturas WHERE medido_ms >= ? AND medido_ms < ?
""", (T0, T0 + 5 * MINUTO)).fetchone() == (5,)
# Instantánea cerrada: después de esta purga no hacemos nuevas ingestas.
db.execute("BEGIN IMMEDIATE")
db.execute("DELETE FROM lecturas WHERE medido_ms < ?", (AHORA - 5 * MINUTO,))
db.execute("COMMIT")
assert db.execute("SELECT COUNT(*) FROM lecturas").fetchone() == (2,)
assert db.execute("SELECT COUNT(*) FROM resumenes").fetchone() == (3,)
print("Retención: quedan 2 crudos y 3 resúmenes; media histórica: 5,2 °C")
print("9 comprobaciones superadas.")
db.close()
Resultado inicial: trece entregas producen doce filas, una de ellas inválida. Se reconoce un duplicado y se rechaza un conflicto. Los resúmenes de cinco minutos tienen 5, 5 y 2 filas; 5, 4 y 2 valores válidos; y máximos de 9,0; 9,2 y 4,1 °C. Después de la purga quedan dos crudos y tres resúmenes. Las nueve comprobaciones terminan sin errores.
Qué demuestra y qué falta para desplegar
El código prueba unicidad, conflicto, orden temporal, agregación y retención en una instantánea. BEGIN IMMEDIATE serializa este trabajo de escritura en SQLite; no es una prueba de concurrencia distribuida. COMMIT cierra la transacción, pero una base en memoria no conserva nada al terminar el proceso.
Antes de operar un servicio hacen falta API y autenticación, configuración de persistencia, recibos de deduplicación con su propio plazo, políticas para fechas fuera de rango y datos tardíos, monitoreo y recuperación. Este programa no recibe nuevos datos después de borrar crudos; recalcular sus resúmenes desde la tabla ya recortada perdería parte de la historia.
Los parámetros ? se enlazan con valores separados. No reemplaces esa práctica por interpolación de cadenas para construir consultas. El cargador del curso incorpora los módulos opcionales usados por los ejemplos, siguiendo el mecanismo de carga de paquetes de Pyodide.
11 · Exploración interactiva
Qué historia conserva la nube
El laboratorio usa las mismas trece entregas. Elegí si se reconoce el reintento, qué eje temporal se utiliza, cuánto dura cada resumen y cuántos minutos de crudos se conservan. El cierre de la instantánea siempre es T0 + 15 minutos. Los resúmenes se calculan antes de eliminar crudos.
Una serie con un dato tardío y un reintento
Blanco: puntos crudos válidos conservados. Celeste: mínimo y máximo por ventana. Naranja: media de esa ventana. Las lecturas inválidas no se dibujan como cero. Una ventana sin valores válidos queda sin marca de temperatura.
Preparando vista.
Resúmenes de la historia incorporada
| Ventana | Total | Válidas | Mínimo | Media | Máximo |
|---|
Registros crudos que todavía quedan
| Secuencia | Medido (min) | Recibido (min) | Valor |
|---|
Recorrido guiado
- Con los valores iniciales hay 13 entregas, 12 filas y 3 resúmenes. La última medición válida es 4,0 °C y la media histórica es 5,2 °C. El reintento no agrega una fila.
- Revisá [5, 10): tiene cinco filas y cuatro valores válidos. La media es 5,325 °C; el minuto 8 no aporta un cero ni se incluye en el divisor.
- Cambiá el eje a recepción. La última válida pasa a 6,5 °C: es el dato medido en el minuto 2 que llegó en el 12. Las ventanas agrupan llegadas, no la evolución física. La media global sigue siendo 5,2 °C porque no cambió el conjunto.
- Volvé a medición y desmarcá la deduplicación. Se incorporan 13 filas y la media histórica sube a aproximadamente 5,533 °C: la lectura de 9,2 °C cuenta dos veces. En [5, 10) hay seis filas, cinco válidas y media de 6,1 °C.
- Restaurá deduplicación y elegí 15 minutos de resumen. Hay un bloque con doce filas, once válidas, media de 5,2 °C y máximo de 9,2 °C. El máximo conserva el pico que la media oculta.
- Con ventanas de cinco minutos, conservá solo los últimos cinco minutos de crudos. Quedan dos filas: minutos 10 y 11. Los tres resúmenes siguen disponibles porque se produjeron antes de purgar.
- Elegí ventanas de un minuto. La del minuto 8 contiene una fila y cero valores válidos. Su temperatura queda «Sin dato». Un bloque vacío o inválido no representa 0 °C.
El simulador reconstruye la instantánea completa cada vez que calculás; cambiar retención no realiza una eliminación irreversible. La ingesta real del programa Python siempre deduplica: la opción de desactivarla en el laboratorio sirve para observar el sesgo. El gráfico no mezcla los resúmenes con los crudos en una suma; los presenta como dos niveles de detalle de la misma historia.
Desafío: ¿el tiempo de recepción siempre es incorrecto?
No. Es el eje correcto para estudiar carga de ingesta, interrupciones o retraso de llegada. Para representar cuándo ocurrió un cambio de temperatura se necesita el de medición, con su calidad de reloj. Son preguntas diferentes que requieren conservar ambos tiempos.
12 · Sostener el sistema
Operación y método de trabajo
Una base que responde hoy no está necesariamente preparada para seguir operando. Hay que observar tanto el flujo de entrada como el de consultas y mantenimiento. Las pruebas de este tema validan resultados; el rendimiento y la recuperación se miden en el entorno que vas a usar.
| Medida | Qué puede mostrar |
|---|---|
| Registros recibidos, aceptados, repetidos y rechazados | Reintentos, contrato incompatible o fallas de identificación |
| Distribución del retraso de recepción | Colas atrasadas, relojes incorrectos o una red intermitente |
| Tiempo de confirmar una ingesta | Trabajo de escritura, bloqueos o saturación del almacenamiento |
| Duración y filas examinadas de las consultas | Índices insuficientes o paneles que piden demasiado detalle |
| Espacio y atraso de resúmenes o retención | Crecimiento por tareas detenidas o mantenimiento incompleto |
| Antigüedad de la última restauración probada | Si la copia de seguridad tiene una recuperación verificada |
Replicar no sustituye hacer copias de seguridad: una eliminación equivocada puede propagarse a las réplicas. Definí cuántos datos se puede perder y cuánto puede tardar una recuperación, y probá restaurar una copia en un entorno separado. También la configuración de durabilidad de la base forma parte de lo que promete el ACK.
- Escribí las consultas del producto: último dato, historia, eventos y agregados por sitio o dispositivo.
- Fijá el contrato: identidad, tiempo, unidades, calidad y versiones.
- Implementá la ingesta idempotente: transacción, conflicto de contenido y ACK inequívoco.
- Diseñá índices con esas consultas: medí su beneficio y el costo de mantenerlos.
- Definí retención y tardanza juntas: fuente necesaria para corregir y plazo de deduplicación.
- Dimensioná con datos representativos: almacenamiento, tráfico, consultas y tareas de mantenimiento.
- Probá repetición, desorden y purga: contrastá resultados antes y después de resumir.
- Verificá permisos y recuperación: cada cliente consulta lo suyo y las copias se pueden restaurar.
Confusiones frecuentes
«El broker MQTT ya es mi base histórica»
El broker distribuye mensajes y puede conservar estado de sesiones o publicaciones retenidas según su configuración. Eso no define un historial consultable de lecturas con intervalos, calidad y retención. Un consumidor debe llevar los datos al almacenamiento previsto.
«La hora del servidor es suficiente para graficar»
Sirve para ordenar recepciones. Un dispositivo que vacía una cola entrega datos viejos junto con nuevos; reemplazar la fecha de medición por la de llegada mueve los incidentes en el gráfico.
«Si existe la identidad, ignoro cualquier contenido»
El mismo registro repetido debe tener el mismo contenido físico. Una colisión con otra temperatura puede indicar un contador reiniciado mal o un error de firmware. Se rechaza y diagnostica en lugar de sobrescribir silenciosamente.
«La media de las medias es la media global»
Solo si las cantidades de valores que representan son iguales, o si se ponderan correctamente. Guardar suma y cantidad permite combinar ventanas de tamaños o coberturas diferentes.
«Ya tengo el resumen, puedo olvidar cómo deduplicaba»
El resumen no enumera las identidades que lo componen. Si borrás el único registro de identidad y luego llega un reintento, podés contar de nuevo. La conservación de recibos o una política de admisión debe cubrir ese caso.
«Más índices siempre hacen más rápida la base»
Cada índice ocupa espacio y cuesta trabajo al escribir. Algunos no ayudan a las consultas reales. Se eligen con planes y mediciones, no por cantidad.
13 · Práctica guiada
Predecí antes de comprobar
1. Volumen de una flota
Doscientos dispositivos envían tres magnitudes cada minuto. Si se guarda una fila por magnitud, ¿cuántas filas hay por día y en treinta días?
Ver solución
200 × 3 × 1440 = 864.000 filas al día. En treinta días: 25.920.000 filas. Esto no incluye reintentos físicos, índices ni resúmenes; los reintentos reconocidos no deberían agregar filas lógicas.
2. Qué significa «último»
Una lectura de las 10:02 llega a las 10:20, después de haber recibido otra medida a las 10:19. ¿Cuál representa la medición más reciente?
Ver solución
La de las 10:19, suponiendo relojes de medición válidos. La de las 10:02 es la última llegada, pero describe una situación anterior. Conservá ambos tiempos para responder las dos preguntas.
3. Un límite entre ventanas
Hay un punto medido exactamente a las 00:05. ¿Se cuenta en [00:00, 00:05), en [00:05, 00:10) o en las dos?
Ver solución
Solo en [00:05, 00:10). Incluir el inicio y excluir el final evita el doble conteo entre intervalos consecutivos.
4. Calidad y promedio
Una ventana contiene 4,0 °C, 8,0 °C y una adquisición fallida. ¿Cuál es su media y cuántas filas y valores válidos tiene?
Ver solución
Tiene tres filas, dos valores válidos y media de 6,0 °C. La falla no es un cero: se registra con calidad inválida y valor nulo. Mínimo 4,0 °C; máximo 8,0 °C.
5. Combinar resúmenes
Un bloque tiene dos valores válidos y media de 4 °C; otro tiene ocho y media de 9 °C. ¿Cuál es la media de los diez valores?
Ver solución
(2 × 4 + 8 × 9) / 10 = 8 °C. Promediar 4 y 9 sin ponderar daría 6,5 °C, que no representa los diez valores.
6. Borrar la identidad demasiado pronto
Se conservan crudos dos días, pero un dispositivo puede reenviar su cola durante una semana. La clave de deduplicación solo vive en la fila cruda. ¿Qué caso falta cubrir?
Ver solución
Un reintento de más de dos días puede volver a parecer nuevo después de borrar su fila. Hay que conservar recibos de identidad durante el horizonte de reintento o definir y comunicar una admisión que resuelva lo vencido, sin volver a sumar la misma lectura.
7. Un registro tardío cambia el resumen
Una ventana tenía suma 200 décimas y cuatro valores válidos. Llega una lectura nueva de 100 décimas. ¿Qué cambia? ¿Qué no debe pasar si se reejecuta el trabajo?
Ver solución
La suma pasa a 300 y la cantidad a 5; la media es 60 décimas, es decir, 6,0 °C. El trabajo repetido debe reemplazar ese resultado o reconocer que el incremento ya se aplicó; no volver a sumar el mismo registro.
8. Resúmenes y crudos solapados
Un informe suma un resumen que cubre 09:00–10:00 y además los crudos de 09:30–10:30. ¿Qué error introduce y cómo se evita?
Ver solución
Cuenta dos veces 09:30–10:00. Se usan rangos disjuntos, por ejemplo resúmenes hasta las 10:00 y crudos desde las 10:00. Si se necesita un corte dentro de una ventana, hace falta detalle suficiente para dividirla correctamente.
14 · Síntesis
Ideas para recordar
- La nube conserva una historia consultable; el servicio de ingesta define qué significa aceptar y confirmar un registro.
- Identidad de registro, identidad de serie y tiempo cumplen funciones distintas. La clave única permite reconocer reintentos sin duplicar efectos.
- Medición y recepción responden preguntas diferentes. Un dato tardío no debe hacer retroceder el estado actual del proceso.
- Calidad inválida no equivale a cero. Cantidad total y cantidad válida deben distinguirse al resumir.
- Las consultas por dispositivo e intervalo necesitan índices apropiados y límites de resultados; no se pide toda la historia para cada panel.
- Suma, cantidad válida, mínimo y máximo permiten combinar ventanas sin esconder extremos ni promediar mal las medias.
- Retención, corrección de datos tardíos y conservación de identidades se diseñan juntas. Borrar crudos elimina la fuente para ciertos recálculos.
- En la traza, trece entregas producen doce registros, once válidos y media de 5,2 °C. Un reintento contado como nuevo subiría esa media a aproximadamente 5,533 °C.
¿Listo para el tema 35?
Antes de avanzar, elegí una magnitud de tu proyecto y definí su identidad, unidad, calidad y referencias de tiempo. Escribí una consulta histórica y otra de último dato, y fijá cuánto conservarás crudos, resúmenes y recibos de entrega.
En el próximo tema, Telemetría, integramos estas decisiones: qué conviene reportar, con qué frecuencia, en qué formato y con qué costo de datos para que el sistema produzca la información que realmente necesita.