Bases de datos columnares y series temporales
En resumen: Una base de datos columnar guarda cada columna por separado para que los informes sobre millones de filas lean rápido, y una base de series temporales guarda mediciones con marca de tiempo, como las de un sensor.
Problema
El informe quiere dos columnas y lee siete
La tabla sales de la lección anterior tiene siete columnas: pedido, día, ciudad, categoría, producto, cantidad e importe. El informe de ingresos por mes quiere dos de ellas, day y amount.
Una base de datos corriente guarda los datos por filas: todo sobre una venta está junto en el disco. Para leer dos columnas, lee las filas enteras y tira las otras cinco. Con mil millones de ventas son cientos de gigabytes, de los cuales hace falta poco más de una cuarta parte.
La idea
Cada columna por su cuenta
Una base de datos columnar (ClickHouse, BigQuery, Snowflake, DuckDB) guarda cada columna en un archivo aparte. El informe de ingresos abre solo los archivos de day y amount. Y una columna tiene juntos valores del mismo tipo (solo fechas, solo ciudades), así que se comprime mucho mejor que una fila donde están mezclados.
La compresión más sencilla es un tramo: en vez de «Sofía, Sofía, Sofía…» escribes «Sofía × 50». La columna city entera, ordenada por ciudad, son siete tramos:
Solo funciona si los valores iguales están juntos. Aquí tienes cuántos tramos tiene la columna cuando las filas están en el orden de los pedidos, y cuántos cuando están ordenadas por ciudad:
De 128 valores quedan 39 tramos si las filas no están ordenadas, y 7 si lo están. Por eso en ClickHouse cada tabla se crea con ORDER BY. El orden de las columnas en él es el orden en el disco.
Más precisamente, para curiosos
LAG(city, 1, '-') da la ciudad de la fila anterior (y '-' para la primera fila), y cada fila en la que cambia la ciudad empieza un tramo nuevo. IS NOT en lugar de <> es porque NULL <> NULL no es verdadero, mientras que NULL IS NOT NULL es un «no» claro. Las bases de datos columnares reales usan también otros tipos de compresión: un diccionario (las ciudades pasan a ser los números 1–7), diferencias entre números vecinos y, encima, un algoritmo general (LZ4, ZSTD). Una compresión de 10 veces no es rara.
La idea
Cuándo columnar y cuándo no
Una base de datos columnar es excelente para informes: pocas columnas, muchas filas, sumas y grupos. Es mala para la caja. Un pedido nuevo significa una escritura en siete archivos separados, y un UPDATE de una fila suele significar reescribir todo un bloque de la columna. Por eso a las bases de datos columnares les gusta escribir en lotes grandes (miles de filas a la vez) y cambian pocas veces. Es la división entre OLTP y OLAP de la lección anterior, solo que en el disco.
Problema
La nevera de lácteos
La nevera con los lácteos tiene un sensor que registra la temperatura cada 15 minutos. Por encima de 8 °C el queso y la leche se estropean pronto, y la tienda tiene que saber cuándo pasó. Estos datos son una serie temporal: valores ordenados por tiempo, que solo se añaden y casi nunca se cambian. Dos días de mediciones:
Dos días de 96 mediciones deberían ser 192, pero son 184. Faltan ocho, y vas a ver dónde.
La idea
Reducir a horas
Nadie mira 35 000 puntos en un gráfico de un año. Las mediciones se agrupan por hora (o por día) con unos pocos números para cada una: la media, el valor más bajo y el más alto. Eso es downsampling (reducir la resolución). La hora son los primeros 13 caracteres del tiempo:
La media de las 14:00 es 8,9 °C, y la más alta es 9,7 °C. La puerta estuvo abierta.
Pruébalo
Cuándo suena la alarma
Muestra las horas en las que al menos una medición está por encima de 8 °C (las columnas hour y max_temp, la temperatura más alta de la hora), en el orden de la hora.
Solución
SELECT substr(at, 1, 13) || ':00' AS hour, MAX(temp) AS max_temp
FROM fridge
GROUP BY hour
HAVING MAX(temp) > 8
ORDER BY hour;GROUP BY la hora y HAVING MAX(temp) > 8. Con la media (AVG) no aparecería una hora en la que la temperatura fue de 9 °C durante 15 minutos y de 4 °C el resto del tiempo. Para los productos estropeados importa la más alta, no la media.
La idea
Una media móvil
Los sensores a veces dan un único valor erróneo, y la nevera la abre cada cliente durante segundos. Por eso la alarma suele mirar no una medición, sino la media de las últimas cuatro (la última hora). Se mueve con cada fila nueva: una función de ventana con ROWS BETWEEN 3 PRECEDING AND CURRENT ROW:
La media supera los 8 °C solo a las 14:45, cuando la puerta lleva una hora abierta. Una medición alta al azar la subiría en torno a 1 °C, pero no por encima de 8.
La función de ventana está en la consulta interior, y el período, fuera. WHERE se ejecuta antes que la ventana: si la condición del período está en la misma consulta, la media de las 13:30 no ve las 12:45, las 13:00 y las 13:15 y sale de una sola medición.
Pruébalo
El hueco
El sensor mide cada 15 minutos. Encuentra dónde perdió la conexión: la última medición antes del hueco y la primera después (las columnas last_before y first_after).
Solución
SELECT before AS last_before, at AS first_after
FROM (SELECT at, LAG(at) OVER (ORDER BY at) AS before FROM fridge) AS t
WHERE ROUND((julianday(at) - julianday(before)) * 24 * 60) > 15;LAG(at) da la medición anterior, y julianday convierte el tiempo en días. La diferencia por 24 × 60 está en minutos, y todo lo que pase de 15 es un hueco. ROUND hace falta porque los días son números fraccionarios y 15 minutos salen como 15.000000xxx. Sin la multiplicación la diferencia está en días (0.01 para 15 minutos) y nunca pasa de 15.
La idea
Un período de retención
Las mediciones en bruto crecen en 35 000 filas al año por sensor, y la tienda tiene cientos. Lo habitual es una tabla por hora, que se guarda mucho tiempo, y mediciones en bruto que se guardan poco (aquí, un día). La tabla por horas se hace como en el almacén de datos, con CREATE TABLE … AS SELECT:
Las horas 03:00 y 04:00 tampoco están en ella: no hay ni una sola medición de esas horas. El gráfico tiene que mostrar un espacio vacío, no unir las 02:00 y las 05:00 con una línea recta como si todo estuviera bien.
Ejercicio
Limpiar y un informe
1. La tabla por horas ya está hecha.
Borra las mediciones en bruto anteriores al 25 de octubre de 2026. La tabla fridge_hourly se queda entera.
Solución
DELETE FROM fridge WHERE at < '2026-10-25';DELETE FROM fridge WHERE at < '2026-10-25'. El texto '2026-10-25 00:00' es mayor que '2026-10-25', así que la medición de medianoche se queda. Con <= '2026-10-25 00:00' se borraría también la primera medición del día que conservas. La comprobación mira también fridge_hourly: las horas siguen siendo 46.
2. Un informe del día.
Para cada día: la temperatura más baja, la más alta y la media (redondeada a 0.01) y el número de mediciones (las columnas day, min_temp, max_temp, avg_temp, readings), en el orden del día.
Solución
SELECT substr(at, 1, 10) AS day, MIN(temp) AS min_temp, MAX(temp) AS max_temp, ROUND(AVG(temp), 2) AS avg_temp, COUNT(*) AS readings
FROM fridge
GROUP BY day
ORDER BY day;substr(at, 1, 10) es el día. COUNT(*) muestra que el 25 tiene 88 mediciones, no 96. Un informe sin la cuenta ocultaría que el sensor estuvo callado dos horas.
La idea
Bases de datos para series temporales
También hay bases de datos aparte para series temporales: TimescaleDB (una extensión de PostgreSQL que divide la tabla por tiempo por sí sola y reduce a horas por sí sola), InfluxDB y Prometheus (para métricas de servidores). Todas hacen lo mismo que en la lección: ordenar por tiempo, comprimir las columnas, mantener tablas reducidas y borrar los datos en bruto pasado un período. Las columnares ClickHouse y DuckDB también se las arreglan bien con ellas.
Más precisamente, para curiosos
En TimescaleDB una tabla por horas que se actualiza sola es CREATE MATERIALIZED VIEW … WITH (timescaledb.continuous) AS SELECT time_bucket('1 hour', at) …, y el período de retención es SELECT add_retention_policy('fridge', INTERVAL '1 day'). SQLite no tiene nada parecido: la tabla por horas y el borrado los ejecuta el programa según un horario.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: CREATE TABLE: crear una tabla · Claves naturales y desnormalización · Índices · OLTP y OLAP
- Continúa en: Cómo elegir una base de datos
- Ejercicios sobre el tema: Tipos de bases de datos