How to build dashboards on ClickHouse data
Max Musing
Max MusingFounder and CEO of Basedash
· July 29, 2026

Max Musing
Max MusingFounder and CEO of Basedash
· July 29, 2026

To build dashboards on ClickHouse, connect a BI tool over ClickHouse’s HTTP interface (port 8123, or 8443 for TLS) using a dedicated read-only user, query directly against your tables so you keep ClickHouse’s speed instead of extracting data into a slower engine, and read from pre-aggregated materialized views for the heavy rollups. The one rule that trips up most teams: if a table uses ReplacingMergeTree or a CollapsingMergeTree engine, add the FINAL modifier (or query a view that does) or your dashboard will silently return duplicated, pre-merge rows.
This guide is for engineers, analysts, and operators who already run ClickHouse for events, logs, product telemetry, or real-time analytics and want shareable dashboards on top of it. ClickHouse is not a normal transactional database, and treating it like one is where dashboards go wrong. Below: why ClickHouse changes how you connect BI tools, how to set up the connection safely, whether to query raw tables or materialized views, the correctness gotchas that produce wrong numbers, how to keep real-time dashboards from hammering the cluster, and when ClickHouse is the wrong place to build a given dashboard.
GRANT SELECT on only the databases reporting needs. Do not reuse the default account.ReplacingMergeTree and CollapsingMergeTree tables deduplicate only during background merges, so a plain SELECT can return duplicates. Use FINAL at query time for correct results (ClickHouse docs).Most BI advice assumes a row-oriented database like PostgreSQL or MySQL, or a cloud warehouse like Snowflake. ClickHouse is different in ways that matter for dashboards.
It stores data by column, not by row. A query that touches three columns of a 60-column table reads only those three columns off disk, which is why ClickHouse can aggregate billions of rows in under a second. The flip side: SELECT * on a wide table is wasteful, and single-row lookups by a non-key column are slow. Dashboards should aggregate and filter on the sort key, not fetch individual records.
It sorts data on disk by the table’s ORDER BY key and uses a sparse primary index. Filtering on the leading key columns lets ClickHouse skip most of the table. Filtering on a random column forces a wider scan.
It is append-optimized. ClickHouse handles high-volume inserts extremely well but does not do row-level updates and deletes the way a transactional database does. Instead, engines in the MergeTree family reconcile changes in the background. That reconciliation is the source of the correctness gotchas later in this guide.
The practical takeaway: a dashboard that works fine on Postgres can be slow or wrong on ClickHouse if you point the same query patterns at it. Build for aggregation, filter on the sort key, and know which table engine you are reading.
Connecting is straightforward once you know the two interfaces and set up access correctly.
ClickHouse exposes two main interfaces. The HTTP interface listens on port 8123 (8443 for HTTPS), and the native TCP protocol listens on port 9000 (9440 for TLS), per the ClickHouse network ports reference. Most BI and dashboard tools connect over HTTP because it has the widest driver support; some, like Grafana’s official plugin, can use the native protocol for lower overhead.
For ClickHouse Cloud, you connect to the provided HTTPS endpoint on 8443 (or native TLS on 9440) with the credentials from the console. Either way, use an encrypted connection for anything beyond localhost.
Do not connect BI with the default account. Create a user scoped to only what reporting needs. ClickHouse uses SQL-driven role-based access control (ClickHouse access docs):
CREATE USER bi_readonly IDENTIFIED BY 'strong-password-here'
SETTINGS readonly = 1;
GRANT SELECT ON analytics.* TO bi_readonly;
The readonly = 1 profile setting blocks writes and prevents the connection from changing server settings. GRANT SELECT on a single database (here analytics) keeps the BI connection out of everything else. This makes analytics access auditable and revocable without touching ingestion. Grant on specific databases or tables rather than *.*. See the GRANT statement docs for the full syntax.
Heavy dashboard queries and real-time ingestion compete for the same CPU and memory. If you run a ClickHouse cluster with replicas, route reporting to a replica so a dashboard refresh does not slow ingestion. On ClickHouse Cloud, separate compute services can isolate analytics traffic from write traffic. On a single node, keep dashboard queries cheap and filtered so they do not starve inserts.
This is the decision that determines whether your dashboards are fast and correct. ClickHouse gives you three layers to query, and the right choice depends on the query pattern.
| Layer | What it is | Best for | Watch out for |
|---|---|---|---|
| Raw MergeTree table | The base table your data lands in | Ad hoc exploration, filtered detail views, anything you query occasionally | Full-table aggregations get expensive as the table grows |
| Incremental materialized view | An insert trigger that writes a pre-aggregated result into a target table | High-traffic dashboard tiles that always run the same rollup (daily active users, revenue by day) | Only reflects rows inserted after it was created; ignores updates and deletes to the source |
| Projection | An alternate sort order or aggregation stored inside the same table, chosen automatically by the optimizer | Speeding up queries that filter or group differently from the table’s ORDER BY key |
Adds storage and insert cost; less flexible than a separate view |
A ClickHouse materialized view is not the Postgres concept of the same name. It is a trigger that runs your SELECT against each newly inserted block and appends the result to a target table, as the ClickHouse materialized view docs explain. That is what makes real-time pre-aggregation possible at scale, but it also means the view only ever sees new inserts. It does not react to updates or deletes on the source table, and for a materialized view built on a JOIN, only inserts to the left-most table trigger it.
The practical pattern for most dashboards:
If your source data is updated or deleted rather than only appended, prefer a refreshable materialized view or scheduled rollup over an incremental one, because incremental views will drift out of sync. The materialized views versus projections guide covers the tradeoffs.
This is the most important section, because it produces numbers that look plausible but are wrong.
ClickHouse teams often use ReplacingMergeTree (to emulate upserts) or CollapsingMergeTree (to handle updates by canceling and re-inserting rows). Both engines only remove duplicate or canceled rows during background merges, which happen at an unpredictable time. Until a merge runs, a plain SELECT returns the pre-merge state, which means duplicated rows or rows that should have been deleted.
The ClickHouse documentation is explicit about this: deduplication “offers eventual correctness only,” and “queries can, therefore, produce incorrect answers.” To get correct results at query time you add the FINAL modifier (ClickHouse docs):
-- Can over-count: returns un-merged duplicate rows
SELECT count() FROM orders;
-- Correct: FINAL completes the merge logic at query time
SELECT count() FROM orders FINAL;
Why this bites dashboards specifically: most BI tools generate SELECT ... FROM table and have no idea the table is a ReplacingMergeTree. The dashboard renders a clean number that is quietly inflated by un-merged duplicates, and it changes over time as merges run in the background, so the same chart shows different totals on different days for no visible reason.
How to handle it:
SHOW CREATE TABLE). If it is ReplacingMergeTree, CollapsingMergeTree, or VersionedCollapsingMergeTree, you need deduplication at read time.FINAL automatically for these engines, turn that on. Some tools, including Basedash, can detect the engine and add it; many cannot.FINAL (CREATE VIEW orders_final AS SELECT * FROM orders FINAL) and point the dashboard at the view.FINAL has a cost, especially when you are not filtering on primary key columns. Filter on the sort key where you can to keep it cheap, and lean on materialized views for the heaviest aggregations so you are not running FINAL over the whole table on every load.The same care applies to counting distinct values. On high-cardinality columns, ClickHouse’s approximate functions like uniq are far faster than exact counts and are usually fine for a dashboard, but decide deliberately whether a tile needs an exact or approximate distinct count rather than letting the tool pick.
ClickHouse is fast, but a dashboard that a hundred people leave open on auto-refresh can generate a surprising amount of load. A few habits keep it healthy.
ORDER BY columns (usually a time range plus a tenant or category) read a fraction of the table. Queries that filter on a random column scan far more.SELECT *. Select only the columns a chart needs. Because storage is columnar, narrow selects read dramatically less data.EXPLAIN and check how many rows it reads. For a broader approach to diagnosing slow tiles, see our BI performance playbook.ClickHouse is excellent for the analytics it was designed for, and a poor fit for some things dashboards commonly need. Use this as a filter before you build.
A common healthy pattern is to run two systems: ClickHouse for high-volume event and time-series analytics, and a transactional database (often the app’s Postgres) for operational, record-level views. If you are weighing whether you even need ClickHouse yet, our guide on when to add a data warehouse walks through the signals.
Because ClickHouse speaks SQL over HTTP and the native protocol, most modern BI tools can connect. The right one depends on who will use the dashboards.
FINAL gotcha automatically when it detects the relevant engines.For a detailed, honest comparison across integration depth, AI features, and pricing, see our best BI tools for ClickHouse guide. If you also have application data in Postgres, our companion guide on building dashboards on Supabase data covers the row-oriented side of the same problem.
Use this as a practical sequence when you add a BI tool to a ClickHouse deployment.
readonly = 1 and GRANT SELECT on only the databases reporting needs.default account to a BI tool.ReplacingMergeTree or CollapsingMergeTree table so you know where FINAL is required.FINAL explicitly. Turn on automatic FINAL if your tool supports it, or point dashboards at views that include it.Point the tool at ClickHouse’s HTTP interface (port 8123, or 8443 for HTTPS) or the native protocol (9000, or 9440 for TLS), using a dedicated read-only user created with CREATE USER ... SETTINGS readonly = 1 and GRANT SELECT on the databases reporting needs. Most BI tools connect over HTTP. For ClickHouse Cloud, use the HTTPS endpoint and credentials from the console. Always use an encrypted connection outside localhost.
Almost always because the table uses ReplacingMergeTree or CollapsingMergeTree, which only remove duplicate rows during background merges. Until a merge runs, a plain SELECT returns un-merged rows, so counts and sums are inflated and change over time. Add the FINAL modifier at query time, enable your tool’s automatic FINAL for these engines, or query a view that includes FINAL.
Query materialized views for tiles that always run the same aggregation, since the rollup is computed once at insert time. Query raw tables for exploratory and drill-down views where the query shape changes. Remember that ClickHouse incremental materialized views are insert triggers, so they only reflect newly inserted rows and ignore updates and deletes to the source table.
Yes. ClickHouse is built for real-time analytics on large, high-ingest datasets and can aggregate billions of rows in under a second. Keep dashboards efficient by filtering on the table’s sort key, selecting only needed columns, pre-aggregating heavy tiles with materialized views, and setting refresh intervals plus query caching so many concurrent viewers do not overload the cluster.
Avoid it for single-record lookups, frequently updated small dimension tables, tiny datasets, and dashboards that join many normalized tables on every query. Those fit a transactional database like PostgreSQL better. A common pattern is to run ClickHouse for event and time-series analytics and keep operational, record-level views on your application database.
Written by

Founder and CEO of Basedash
Max Musing is the founder and CEO of Basedash, an AI-native business intelligence platform designed to help teams explore analytics and build dashboards without writing SQL. His work focuses on applying large language models to structured data systems, improving query reliability, and building governed analytics workflows for production environments.
Basedash lets you build charts, dashboards, and reports in seconds using all your data.