Using TimescaleDB with Hasura GraphQL
- Create a Digital Ocean droplet and install docker and docker-compose
- Run Hasura with TimescaleDB using docker-compose
- Learn what hypertables are
- Run GraphQL queries on hypertables
- Run GraphQL subscriptions and get real-time data


$ ssh -i <path-to-the-private-key> root@<public-ip-of-droplet>
$ apt-get update
$ apt-get install docker.io docker-compose
# verify docker is working
$ docker info
$ docker run -d -p 5432:5432 timescale/timescaledb:latest-pg10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
version: '2' | |
services: | |
timescale: | |
image: timescale/timescaledb:latest-pg10 | |
restart: always | |
environment: | |
POSTGRES_PASSWORD: postgrespassword | |
volumes: | |
- db_data:/var/lib/postgresql/data | |
graphql-engine: | |
image: hasura/graphql-engine:v1.0.0-alpha20 | |
ports: | |
- "8080:8080" | |
depends_on: | |
- "timescale" | |
restart: always | |
environment: | |
HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@timescale:5432/postgres | |
HASURA_GRAPHQL_ACCESS_KEY: mylongsecretkey | |
command: | |
- graphql-engine | |
- serve | |
- --enable-console | |
volumes: | |
db_data: |
$ docker-compose up -d
$ docker ps
CONTAINER ID IMAGE ... CREATED STATUS PORTS ...
097f58433a2b hasura/graphql-engine ... 1m ago Up 1m 8080->8080/tcp ...
bccb3af615fd timescale/timescaledb ... 1m ago Up 1m 5432/tcp ...
$ docker ps
# exec into the container, with psql
# once we are connected to the db, we get a psql prompt
# create the timescale extension
$ docker exec -it <container-id> psql -U postgrespostgres-# postgres-#
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
-- We start by creating a regular SQL table
CREATE TABLE conditions (
time TIMESTAMPTZ NOT NULL,
location TEXT NOT NULL,
temperature DOUBLE PRECISION NULL,
-- in celsius humidity DOUBLE PRECISION NULL
-- in percentage
);

-- This creates a hypertable that is partitioned by time
-- using the values in the `time` column.
SELECT create_hypertable('conditions', 'time');

-- This creates a hypertable that is partitioned by time
-- using the values in the `time` column.
INSERT INTO conditions(time, location, temperature, humidity) VALUES (NOW(), 'office', 27.24, 48.15);
INSERT INTO conditions(time, location, temperature, humidity) VALUES (NOW(), 'home', 32.21, 77.23);
SELECT time_bucket('15 minutes', time) AS fifteen_min,
location, COUNT(*), MAX(temperature) AS max_temp, MAX(humidity) AS max_hum FROM conditions
WHERE time > NOW() - interval '3 hours'
GROUP BY fifteen_min, location
ORDER BY fifteen_min DESC, max_temp DESC;
CREATE VIEW last_fifteen_mins_stats AS (
SELECT time_bucket('15 minutes', time) AS fifteen_min, location,
COUNT(*), MAX(temperature) AS max_temp, MAX(humidity) AS max_hum
FROM conditions
WHERE time > NOW() - interval '3 hours'
GROUP BY fifteen_min, location
ORDER BY fifteen_min DESC, max_temp DESC
);

query {
last_fifteen_mins_stats {
location,
fifteen_min,
max_temp,
max_hum
}
}
{
"data": {
"last_fifteen_mins_stats": [
{"max_temp": 32.21, "location": "home", "max_hum": 77.23,
"fifteen_min": "2018-08-22T11:15:00+00:00"},
{"max_temp": 27.24, "location": "office", "max_hum": 77.23, "fifteen_min": "2018-08-22T11:15:00+00:00"}
]
}
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
from datetime import datetime | |
import time | |
import json | |
import requests | |
query = """ | |
mutation { | |
insert_conditions (objects: [ | |
{ | |
time: "%s", | |
location: "%s", | |
temperature: %.2f, | |
humidity: %.2f | |
} | |
]) { | |
affected_rows | |
} | |
} | |
""" | |
url = 'http://<your-graphql-engine-endpoint>/v1alpha1/graphql' | |
headers = { | |
'x-hasura-access-key': '<your-access-key>' | |
} | |
location_choices = ['loc1', 'loc2', 'loc3', 'loc4', 'home', 'office'] | |
def mk_query(cur_time, loc, temp, humid): | |
q = query % (cur_time, loc, temp, humid) | |
print(q) | |
r = requests.post(url, data=json.dumps({'query': q}), headers=headers) | |
print(r.text) | |
while True: | |
temp = random.uniform(18.2, 39.89) | |
humid = random.uniform(40.34, 92.87) | |
loc = random.choice(location_choices) | |
cur_time = datetime.now().isoformat() | |
mk_query(cur_time, loc, temp, humid) | |
time.sleep(0.5) |
$ python3 timescale_ingest.py
subscription {
last_fifteen_mins_stats {
location,
fifteen_min,
max_temp,
max_hum
}
}

Related reading