QuestDB es una base de datos de series temporales en formato columnar, pensada para ingerir millones de filas por segundo y consultarlas con SQL. Acepta datos por el protocolo de línea de InfluxDB (ILP) y por el protocolo de PostgreSQL, así que puedes usar clientes y herramientas que ya conoces. En este tutorial desplegarás QuestDB en Ubuntu 24.04 con Docker Compose, crearás una tabla para datos de sensores con particionado y deduplicación, enviarás datos por ILP y los consultarás desde la consola web y desde psql.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS con al menos 2 GB de RAM y disco SSD, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo que pertenezca al grupo docker.
  • Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker.

Paso 1: Ajustar los límites del kernel

QuestDB mapea en memoria muchos archivos de columnas y particiones. Con los valores por defecto de Linux puede quedarse sin mapas de memoria cuando la base de datos crece, así que la documentación de QuestDB recomienda subir vm.max_map_count:

echo 'vm.max_map_count = 1048576' | sudo tee /etc/sysctl.d/60-questdb.conf
sudo sysctl --system

Comprueba el valor:

sysctl vm.max_map_count
vm.max_map_count = 1048576

El límite de archivos abiertos se ajusta para el contenedor en el archivo de Compose del paso siguiente.

Paso 2: Crear el proyecto de Docker Compose

Crea un directorio para el despliegue:

mkdir -p ~/questdb
cd ~/questdb

QuestDB expone por el protocolo de PostgreSQL el usuario admin con contraseña quest por defecto. Genera una contraseña propia y guárdala en un archivo .env que Compose leerá automáticamente:

echo "QDB_PG_PASSWORD=$(openssl rand -hex 24)" > .env
chmod 600 .env

Crea el archivo de Compose:

nano compose.yaml
services:
  questdb:
    image: questdb/questdb:10.0.1
    container_name: questdb
    restart: unless-stopped
    ports:
      - "127.0.0.1:9000:9000"
      - "127.0.0.1:8812:8812"
    environment:
      QDB_PG_USER: admin
      QDB_PG_PASSWORD: ${QDB_PG_PASSWORD}
      QDB_TELEMETRY_ENABLED: "false"
    volumes:
      - questdb_data:/var/lib/questdb
    ulimits:
      nofile:
        soft: 1048576
        hard: 1048576

volumes:
  questdb_data:

Algunos detalles de esta configuración:

  • Consulta la última versión en la página de releases de QuestDB y fija la etiqueta de la imagen, en lugar de latest, para controlar cuándo actualizas.
  • El puerto 9000 sirve la consola web, la API REST y la ingesta ILP por HTTP. El 8812 es el protocolo de PostgreSQL.
  • Ambos puertos se publican solo en 127.0.0.1. La consola web y el endpoint de ingesta de QuestDB open source no piden autenticación, así que no deben quedar abiertos a Internet.
  • Cualquier opción de server.conf se puede pasar como variable de entorno con el prefijo QDB_, en mayúsculas y con _ en lugar de puntos: pg.password se convierte en QDB_PG_PASSWORD.

Paso 3: Arrancar QuestDB

Descarga la imagen y arranca el contenedor en segundo plano:

docker compose up -d

Comprueba que está en marcha:

docker compose ps
NAME      IMAGE                    COMMAND                  SERVICE   CREATED          STATUS          PORTS
questdb   questdb/questdb:10.0.1   "/docker-entrypoint.…"   questdb   12 seconds ago   Up 11 seconds   127.0.0.1:8812->8812/tcp, 127.0.0.1:9000->9000/tcp

Lanza una consulta de prueba por la API REST. El endpoint /exec recibe SQL en el parámetro query y devuelve JSON:

curl -G http://127.0.0.1:9000/exec --data-urlencode "query=SELECT now()"
{"query":"SELECT now()","columns":[{"name":"now","type":"TIMESTAMP"}],"timestamp":-1,"dataset":[["2026-09-25T10:31:07.412593Z"]],"count":1}

Si no responde, revisa los registros con docker compose logs --tail 50 questdb.

Paso 4: Abrir la consola web por un túnel SSH

Como el puerto 9000 solo escucha en localhost, accede a la consola web desde tu ordenador mediante un túnel SSH. Ejecuta esto en tu máquina local, no en el servidor:

ssh -L 9000:127.0.0.1:9000 your_user@your_server_ip

Mientras la sesión esté abierta, abre http://localhost:9000 en el navegador. Verás el editor SQL de QuestDB con el listado de tablas a la izquierda. Puedes ejecutar en él todas las consultas SQL de esta guía.

Paso 5: Crear una tabla de series temporales

En QuestDB cada tabla de series temporales tiene una columna de marca de tiempo designada, que determina el orden físico de los datos y el particionado. Crea una tabla para lecturas de sensores:

curl -G http://127.0.0.1:9000/exec --data-urlencode "query=
CREATE TABLE sensores (
  ts TIMESTAMP,
  sensor SYMBOL,
  ubicacion SYMBOL,
  temperatura DOUBLE,
  humedad DOUBLE
) TIMESTAMP(ts) PARTITION BY DAY WAL
DEDUP UPSERT KEYS(ts, sensor);"
{"ddl":"OK"}

Qué hace cada parte:

  • SYMBOL es un tipo para cadenas repetitivas (identificadores, ubicaciones). Se guarda como un diccionario de enteros, lo que ahorra espacio y acelera los filtros.
  • TIMESTAMP(ts) marca ts como marca de tiempo designada.
  • PARTITION BY DAY guarda cada día en un directorio aparte, así que las consultas por rango de fechas solo leen las particiones necesarias y borrar datos antiguos es inmediato.
  • WAL activa el registro de escritura previa, necesario para la deduplicación y para aceptar escrituras concurrentes.
  • DEDUP UPSERT KEYS(ts, sensor) hace que una fila con el mismo ts y sensor que otra ya existente la sustituya en lugar de duplicarla. Es útil cuando un dispositivo reenvía datos tras un corte.

Paso 6: Ingerir datos con ILP sobre HTTP

El protocolo de línea de InfluxDB (ILP) es la forma más rápida de escribir en QuestDB. Cada línea tiene el formato tabla,simbolo=valor campo=valor marca_de_tiempo. Envía dos lecturas al endpoint /write; al omitir la marca de tiempo, QuestDB usa la hora del servidor:

curl -i -X POST http://127.0.0.1:9000/write --data-binary $'sensores,sensor=s1,ubicacion=almacen temperatura=21.4,humedad=48.2\nsensores,sensor=s2,ubicacion=oficina temperatura=23.1,humedad=41.0\n'
HTTP/1.1 204 No Content

Un 204 significa que QuestDB aceptó las filas. En las tablas WAL los datos se aplican de forma asíncrona y aparecen en las consultas en unos milisegundos. En producción no escribas ILP a mano: usa los clientes oficiales de QuestDB (Python, Java, Go, Rust, Node.js, C/C++, .NET), que agrupan filas, reintentan y gestionan los errores, o Telegraf con su salida influxdb_v2 apuntando a http://127.0.0.1:9000.

Comprueba ahora la deduplicación. Envía dos veces la misma lectura del sensor s9 con una marca de tiempo explícita en nanosegundos, cambiando solo la temperatura:

curl -s -X POST http://127.0.0.1:9000/write --data-binary 'sensores,sensor=s9,ubicacion=lab temperatura=20.0,humedad=50.0 1790000000000000000'
curl -s -X POST http://127.0.0.1:9000/write --data-binary 'sensores,sensor=s9,ubicacion=lab temperatura=20.5,humedad=50.0 1790000000000000000'
curl -G http://127.0.0.1:9000/exec --data-urlencode "query=SELECT ts, temperatura FROM sensores WHERE sensor = 's9'"
{"query":"SELECT ts, temperatura FROM sensores WHERE sensor = 's9'","columns":[{"name":"ts","type":"TIMESTAMP"},{"name":"temperatura","type":"DOUBLE"}],"timestamp":0,"dataset":[["2026-09-21T14:13:20.000000Z",20.5]],"count":1}

Solo hay una fila, con el último valor recibido.

Paso 7: Consultar series temporales con SQL

QuestDB añade a SQL varias extensiones para series temporales. Ejecuta estas consultas en la consola web o con curl como en los pasos anteriores.

SAMPLE BY agrega por intervalos de tiempo. Esta consulta calcula la media de temperatura y la humedad máxima por sensor en intervalos de un minuto durante la última hora:

SELECT ts, sensor, avg(temperatura) AS temp_media, max(humedad) AS humedad_max
FROM sensores
WHERE ts > dateadd('h', -1, now())
SAMPLE BY 1m;

LATEST ON devuelve la última lectura de cada sensor, algo habitual en paneles de estado:

SELECT * FROM sensores
LATEST ON ts PARTITION BY sensor;

Para filtrar por intervalo de fechas, el operador IN con una cadena de tiempo solo lee las particiones afectadas:

SELECT count() FROM sensores WHERE ts IN '2026-09';

Paso 8: Conectarte por el protocolo de PostgreSQL

Cualquier cliente de PostgreSQL puede consultar QuestDB por el puerto 8812, lo que permite usar Grafana con su fuente de datos de PostgreSQL, librerías como psycopg o herramientas de BI. Instala el cliente psql:

sudo apt install postgresql-client

Conéctate con el usuario admin a la base de datos qdb, usando como contraseña el valor de QDB_PG_PASSWORD del archivo .env:

psql -h 127.0.0.1 -p 8812 -U admin -d qdb
qdb=> SELECT sensor, temperatura FROM sensores LATEST ON ts PARTITION BY sensor;
 sensor | temperatura
--------+-------------
 s1     |        21.4
 s2     |        23.1
 s9     |        20.5
(3 rows)

Si la conexión se rechaza con invalid username/password, comprueba que el contenedor recibió la variable con docker compose exec questdb env | grep QDB_PG.

Paso 9: Limitar la retención de datos

Las series temporales crecen sin parar, así que conviene decidir cuánto tiempo guardarlas. Con particiones diarias puedes configurar un TTL para que QuestDB borre automáticamente las particiones más antiguas que el periodo indicado:

ALTER TABLE sensores SET TTL 30 DAYS;

También puedes borrar particiones a mano, por ejemplo antes de un cambio de política:

ALTER TABLE sensores DROP PARTITION WHERE ts < dateadd('d', -30, now());

Borrar una partición elimina su directorio completo, así que es instantáneo aunque contenga millones de filas.

Solución de problemas

  • El contenedor se reinicia con errores de mmap o too many open files: revisa que vm.max_map_count está aplicado en el host (paso 1) y que el bloque ulimits está en compose.yaml.
  • curl: (7) Failed to connect to 127.0.0.1 port 9000: QuestDB todavía está arrancando o se ha detenido. Consulta docker compose logs questdb.
  • Los datos enviados por ILP no aparecen: la respuesta de /write incluye el error si una línea es inválida (tipo de columna distinto al existente, por ejemplo). Envía con curl -i y lee el cuerpo de la respuesta.
  • La consola web no carga por el túnel: el túnel debe seguir abierto y el puerto local 9000 libre en tu ordenador. Prueba con otro puerto local: ssh -L 19000:127.0.0.1:9000 ... y abre http://localhost:19000.

Conclusión

QuestDB está funcionando en Ubuntu 24.04 con Docker Compose, accesible solo desde localhost, con una tabla particionada que deduplica lecturas, ingesta por ILP y consultas por REST, consola web y psql. Como siguientes pasos, conecta Grafana con la fuente de datos de PostgreSQL o el plugin oficial de QuestDB, envía métricas reales con uno de los clientes ILP y planifica copias de seguridad del volumen questdb_data.