ClickHouse Optimization: Partitioning, Indexes, and Materialized Views

Slow queries and high disk load when working with ClickHouse are a familiar problem that slows down business growth. We are a team of engineers specializing in ClickHouse optimization: we configure partitioning, implement skip indexes and materialized views to speed up query execution. We deliver turnkey projects—from schema audit to ongoing support—ensuring stable operation of your analytics.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

ClickHouse Optimization: Partitioning, Indexes, and Materialized Views

A SELECT query on a 2-trillion-row table takes 40 seconds. Disk I/O at 100%, the server is humming. Sounds familiar? Usually, this means the primary key is poorly chosen, partitioning doesn't match the query pattern, or data is queried without leveraging skip indexes. We are a team of engineers with 10+ years of experience running ClickHouse in production. Our approach delivers measurable results: typical query execution time savings of 5–10x, and disk load reduction up to 70%. Contact us for a schema audit—we will assess the optimization potential within 1 day.

Why Partitioning Is Not a Silver Bullet

A common mistake: creating a table with PARTITION BY toYYYYMM(event_date) and expecting miracles. Partitioning in ClickHouse is primarily a data management tool (TTL, dropping old partitions), not a way to speed up queries. A query filtering on a specific month does skip irrelevant partitions, but if the date filter is loose or absent—ClickHouse reads everything.

Real story: a client partitioned an events table by toYYYYMMDD(ts)—365 partitions per year. This caused thousands of tiny parts to be created on every nightly insert, the MergeTree couldn't keep up with merges, and queries started opening hundreds of file descriptors. The system log was full of Too many parts (300). Merges are processing significantly slower than inserts. Rolling back to monthly partitioning plus tuning merge_with_ttl_timeout fixed the issue in an hour.

Optimal partition granularity: a partition should contain data for a period convenient for dropping. Use daily partitions only if daily data volume exceeds a few gigabytes; otherwise stick with monthly.

How to Choose the Right ORDER BY?

In ClickHouse, ORDER BY defines the physical data sort order on disk and serves as the primary key (if PRIMARY KEY is not explicitly specified). The default granularity is 8192 rows. Each granule is described in a sparse index, and ClickHouse decides which granules to read based on this index.

If the first element of ORDER BY is user_id UUID, but all queries filter on event_type and ts, the index is useless—ClickHouse reads all granules.

The rule for key selection: first put columns with low cardinality that appear most frequently in WHERE, then high-cardinality columns. For example:

CREATE TABLE events (
    event_type LowCardinality(String),
    project_id UInt32,
    ts DateTime,
    user_id UUID,
    payload String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (event_type, project_id, ts)
SETTINGS index_granularity = 8192;

A query like WHERE event_type = 'click' AND project_id = 42 AND ts >= now() - INTERVAL 1 YEAR now reads only the needed granule range.

How Do Skip Indexes Work?

minmax, set, bloom_filter, tokenbf_v1 are the four main types of skip indexes. They do not replace the primary key but complement it for columns not in ORDER BY.

bloom_filter is indispensable for UUID or hash lookups:

ALTER TABLE events ADD INDEX idx_user_id user_id TYPE bloom_filter(0.01) GRANULARITY 4; 

After materialization (MATERIALIZE INDEX), a query like WHERE user_id = '...' skips granules that definitely lack that UUID. With a 1% false positive rate and 1000 granules, only 10-50 are actually read instead of all.

tokenbf_v1 works on string tokens—useful for substring searches in logs without LIKE '%pattern%'.

Index Type When to Use Approximate Cardinality
minmax Ranges (date, temperature) Low–Medium
set Exact values (status, category) Up to tens of thousands
bloom_filter UUID, hash lookups Any
tokenbf_v1 Substring search in text Any

What Are Materialized Views and When to Use Them?

If the same aggregating query runs frequently—move the aggregation to insert time. MVs in ClickHouse are triggers on INSERT that route data to a target aggregate table.

CREATE MATERIALIZED VIEW events_hourly_mv TO events_hourly AS
SELECT
    toStartOfHour(ts) AS hour,
    event_type,
    project_id,
    count() AS cnt,
    uniqHLL12(user_id) AS uniq_users
FROM events
GROUP BY
    hour,
    event_type,
    project_id;

The target table uses AggregatingMergeTree with AggregateFunction:

CREATE TABLE events_hourly (
    hour DateTime,
    event_type LowCardinality(String),
    project_id UInt32,
    cnt AggregateFunction(count),
    uniq_users AggregateFunction(uniqHLL12, UUID)
) ENGINE = AggregatingMergeTree()
ORDER BY (hour, event_type, project_id);

Queries on aggregated data run 10-100 times faster than on raw data. But note: MVs only update on new inserts. Historical data must be added manually via INSERT INTO ... SELECT.

How to Profile Queries?

Before making changes, check system.query_log:

SELECT query_id, query, read_rows, read_bytes, result_rows, elapsed, memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query_start_time >= now() - INTERVAL 1 HOUR
ORDER BY elapsed DESC
LIMIT 20;

read_bytes / result_bytes gives the over-read ratio. If you read 100 GB for a 1 MB result—the index is not working or missing.

system.parts shows partition state and number of parts. If count(*) > 300 for one partition—MergeTree can't keep up with merges; check background_pool_size and max_bytes_to_merge_at_max_space_in_pool.

How We Optimize ClickHouse

We start with an audit: collect the top-20 heavy queries from system.query_log, review table schemas, and profile using EXPLAIN indexes=1, actions=1. This reveals 80% of the issues.

Then we design: propose changes to ORDER BY/PARTITION BY, a list of skip indexes, and MVs. We agree with your team—changing the sort key requires table recreation with data migration.

Implementation: create new tables, set up MVs, migrate data in batches via INSERT INTO new_table SELECT ... FROM old_table WHERE partition_key = X. Parallel performance tests compare elapsed before and after on identical queries.

After migration, we provide documentation: which index for which queries, how to monitor system.parts, when to run OPTIMIZE TABLE ... FINAL.

Case Study: E-commerce Store with 5 Billion Events Per Day

Queries on raw events in real-time were lagging dashboards. We added a bloom_filter skip index on user_id, configured monthly partitioning, and created an MV for hourly aggregates. Typical query execution time dropped from 30 seconds to 0.3 seconds, and cluster load decreased by 3x.

Estimated Timelines and Scope

Task Scale Data Volume Timeline
Optimize 1-2 tables up to 1 TB 1-2 days
Full schema audit + MVs 1-10 TB 2-3 days
Migration with table recreation 10+ TB 3-5 days

Pricing is determined individually after an infrastructure audit and data schema analysis. Typical infrastructure cost savings from reduced load can reach 40% on cloud instances.

What's Included

  • Schema analysis and top-20 heavy query profiling (EXPLAIN, system.query_log)
  • Design of partitioning, primary key, and skip indexes
  • Creation and configuration of materialized views
  • Data migration with minimal downtime (rolling migration)
  • Operations documentation: monitoring recommendations, typical queries, runbook
  • Team training: how to work with the new schema, how to add new fields

Our guarantees: all changes are tested on a data copy, results are measured in numbers. We don't just change the schema—we prove acceleration on your queries. Leave a request for an audit—we'll contact you within a day.