Hanzo Database
Real-time analytics database
The store behind dashboards, product analytics and observability. Columns live in separate files, so a query reads only the ones it names. Rows are sorted and grouped into parts by time, so a query bounded by a range reads a run of adjacent blocks instead of hunting for them. Rollups are materialized views updated as rows land, which is why the number on the dashboard is the current one rather than last night's.
Events, metrics and traces are one shape
Append-heavy, partitioned by time, queried by range. One engine serves all three.
Vectorised execution
Values from one column sit next to each other in memory in a single representation, so a sum or a filter runs across a block of them with vector instructions instead of once per row — and only the columns the query names come off the disk at all.
Rollups that keep themselves current
Write the aggregate as a materialized view and it is maintained as rows arrive, holding partial aggregate states that merge when you read them. No nightly batch, and no window in which the dashboard is showing yesterday.
High cardinality is not a special case
Per-user, per-device and per-experiment dimensions stay as they are — nothing downsampled, no labels dropped to keep an index small. Repeated values are stored as dictionary references, and the sorting key decides how much of a scan a range can skip.
Data ages out on a rule you wrote
A TTL clause in the table definition moves old partitions to slower storage or removes them outright, and a table's data can sit on object storage while query nodes scale on their own. Retention is part of the schema rather than a cron job somebody has to remember.
Several doors, one engine
It answers over HTTP, on its own native protocol, and on the MySQL and PostgreSQL wire ports — which is how a BI tool or a driver that has never heard of it connects anyway.
Adding a column is a metadata change
The ALTER returns at once and the column is materialised as parts get rewritten in the background, so reads carry on throughout. Partitions are dropped whole, which is why removing a month costs almost nothing.
The schema says how it will be read
-- One part per month, sorted the way it will be filtered,
-- and ninety days of retention written into the table itself.
CREATE TABLE events (
ts DateTime64(3),
user_id UUID,
event LowCardinality(String),
properties String
) ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (event, ts)
TTL toDateTime(ts) + INTERVAL 90 DAY;
-- A rollup maintained as rows land, not a job that runs at 3am.
CREATE MATERIALIZED VIEW dau
ENGINE = AggregatingMergeTree ORDER BY day AS
SELECT toDate(ts) AS day, uniqState(user_id) AS users
FROM events GROUP BY day;
-- Reading it merges the partial states. events is never scanned.
SELECT day, uniqMerge(users) AS dau
FROM dau
WHERE day >= today() - 30
GROUP BY day ORDER BY day;