ClickHouse: Redis as source for dictionaries
If two technologies make my heart bit louder, those are ClickHouse
and Redis
. Two very different databases, born to solve very different problems that, since ClickHouse 20.8, can work together.
Today we’ll talk about ClickHouse’s new external dictionary source for Redis
. Although documented piece by piece, I haven’t found a concrete example that uses it, so I have created one little demo to play with it.
TL;DR
Clone the demo in https://github.com/pachico/demo-clickhouse-redis-dictionaries and follow instruction or keep reading for some notes.
Hypothetical use case: currency exchange rate
So, in which cases could we need these two databases connected together?
We don’t use ClickHouse for transactional operations but analytical queries. This is huge amounts of immutable data (facts) being aggregated for all sorts of insights. This brings two issues:
- data in ClickHouse is immutable: we deal with facts, with things that already happened, that do not require to be edited (although you could, but that’s an entirely different topic)
- many times, you don’t have the dynamic data required to complete your reports
Let’s dig into these two issues by imagining we have an application that stores a constant stream of price quotes
(or biddings, or anything else that stores prices) from all over the world, although our company is in Europe and all our insights must be calculated in euros
.
ClickHouse receives this data and stores it in the quotes_entry
table that has this layout:
product_id | timestamp | quote | currency |
---|---|---|---|
1001 | 2025-07-07 12:00:01 | 129.5 | USD |
1002 | 2025-07-07 12:00:02 | 132.3 | GBP |
I can’t do aggregations since quotes are in different currencies.
Luckily, we have currency exchange rates stored in a Redis
instance, updated every 6 hours.
I guess that by now you know what I want to do.
What we can do to solve our problem is to create a materialized view that, once each row is inserted, will fetch from a dictionary connected to Redis the latest exchange rates, apply the conversion and store the results in a new table quote
where all quotes are also converted to Euro.
How we can do it
This is our tables and dictionary definitions:
CREATE DATABASE IF NOT EXISTS quote;
use quote;
-- This is the DDL definition for the dictionary
-- that will fetch exchange rates from Redis
CREATE DICTIONARY IF NOT EXISTS exchange_rate
(
currency String,
rate Float32
)
PRIMARY KEY currency
SOURCE(REDIS(
host 'redis'
port 6379
storage_type 'simple'
db_index 0
))
LAYOUT(COMPLEX_KEY_HASHED())
LIFETIME(3600);
-- This table stores the data as is when reaches our system
CREATE TABLE IF NOT EXISTS quote_entry
(
product_id UInt32,
timestamp DateTime,
quote Float32,
currency LowCardinality(String)
)
ENGINE = Null;
-- This is the table where all quotes are converted to Euro
CREATE TABLE IF NOT EXISTS quote
(
product_id UInt32,
timestamp DateTime,
quote Float32,
currency LowCardinality(String),
quote_eur Float32
)
ENGINE = Memory;
-- This is the materialized view that does the magic
CREATE MATERIALIZED VIEW IF NOT EXISTS quote_view
TO quote AS
SELECT
quote_entry.product_id AS product_id,
quote_entry.timestamp AS timestamp,
quote_entry.quote AS quote,
quote_entry.currency AS currency,
round((quote_entry.quote * exchange_rate.rate), 2) AS quote_eur
FROM quote_entry LEFT JOIN exchange_rate
ON quote_entry.currency = exchange_rate.currency;
Once the data is inserted into the table quote_entry
the materialized view will join the inserted row with the dictionary, which has in memory the latest exchange rates, applies the conversion and saves the converted quote into the table quote
.
Have a look at the dictionary DDL:
...
LAYOUT(COMPLEX_KEY_HASHED())
LIFETIME(3600);
Make sure you read the documentation about the LAYOUT
that best suits your case. If our primary key was an integer, most likely we would have chosen another one.
And the LIFETIME
setting means that ClickHouse will keep the values of the dictionary in memory during that amount of seconds. Make sure you choose the one that matches your business requirements.
Also note that the table quote_entry
has ENGINE = Null;
which means that it really doesn’t store any data. It’s just there to trigger the materialized view.
Let’s now add some exchange rates to Redis so that the dictionary can fetch them. In Redis, do
mset USD 0.82 GBP 1.11
and now let’s see if ClickHouse can fetch this data
SELECT *
FROM quote.exchange_rate
┌─currency─┬─rate─┐
│ GBP │ 1.11 │
│ USD │ 0.82 │
└──────────┴──────┘
Now let’s insert a few thousand random entries with in quote_entry
INSERT INTO quote.quote_entry SELECT
transform(number % 2, [0, 1], [1001, 1002], 0) AS product_id,
now() AS timestamp,
round(cbrt(rand()), 2) AS quote,
transform(number % 2, [0, 1], ['USD', 'GBP'], '') AS currency
FROM numbers(1, 10000);
and check that the materialized view did its job:
SELECT *
FROM quote.quote
LIMIT 10
┌─product_id─┬───────────timestamp─┬───quote─┬─currency─┬─quote_eur─┐
│ 1002 │ 2020-12-06 19:37:46 │ 1027.06 │ GBP │ 1140.04 │
│ 1001 │ 2020-12-06 19:37:46 │ 1401.09 │ USD │ 1148.89 │
│ 1002 │ 2020-12-06 19:37:46 │ 723.13 │ GBP │ 802.67 │
│ 1001 │ 2020-12-06 19:37:46 │ 681.06 │ USD │ 558.47 │
│ 1002 │ 2020-12-06 19:37:46 │ 888.61 │ GBP │ 986.36 │
│ 1001 │ 2020-12-06 19:37:46 │ 1478.06 │ USD │ 1212.01 │
│ 1002 │ 2020-12-06 19:37:46 │ 1210.04 │ GBP │ 1343.14 │
│ 1001 │ 2020-12-06 19:37:46 │ 1171.39 │ USD │ 960.54 │
│ 1002 │ 2020-12-06 19:37:46 │ 851.8 │ GBP │ 945.5 │
│ 1001 │ 2020-12-06 19:37:46 │ 1185.65 │ USD │ 972.23 │
└────────────┴─────────────────────┴─────────┴──────────┴───────────┘
Everything seems to work as expected and now we can do all our aggregations entirely in euros.
Closing notes
I created a scripted demo that will spawn this scenario using docker-compose
so you can play with it.
Find it at https://github.com/pachico/demo-clickhouse-redis-dictionaries
Hope you find it useful.