Что такое партиции таблиц в ClickHouse?
Партиции объединяют части данных таблицы из семейства движков MergeTree в упорядоченные логические единицы. Это способ организации данных в соответствии с определёнными критериями, такими как временные диапазоны, категории или другие ключевые атрибуты. Такие логические единицы упрощают управление данными, выполнение запросов и оптимизацию.
Партиционирование можно включить при первоначальном определении таблицы с помощью секции PARTITION BY. Эта секция может содержать SQL-выражение над любыми столбцами, результат которого определяет, к какой партиции принадлежит строка.
Чтобы проиллюстрировать это, мы дополним пример таблицы Что такое части таблицы, добавив секцию PARTITION BY toStartOfMonth(date), которая распределяет части таблицы по месяцам продажи недвижимости:
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
ORDER BY (town, street)
PARTITION BY toStartOfMonth(date);
Вы можете запросить данные из этой таблицы в нашей Песочнице ClickHouse.
Каждый раз, когда в таблицу вставляется набор строк, ClickHouse вместо создания одной-единственной части данных, содержащей все вставленные строки (как описано здесь), создает как минимум одну новую часть данных для каждого уникального значения ключа партиционирования среди вставленных строк (не менее одной):
Сначала сервер ClickHouse разделяет строки из примера вставки с 4 строками, схематично показанного на диаграмме выше, по значению ключа партиционирования toStartOfMonth(date).
Затем для каждой выявленной партиции строки обрабатываются как обычно в несколько последовательных шагов (① Сортировка, ② Разбиение на столбцы, ③ Сжатие, ④ Запись на диск).
Обратите внимание: при включенном партиционировании ClickHouse автоматически создает индексы MinMax для каждой части данных. Это просто файлы для каждого столбца таблицы, используемого в выражении ключа партиционирования, которые содержат минимальное и максимальное значения этого столбца в пределах части данных.
При включенном партиционировании ClickHouse выполняет слияния частей данных только в пределах одной партиции, но не между разными партициями. Покажем это на примере таблицы выше:
Как показано на диаграмме выше, части, принадлежащие разным партициям, никогда не сливаются. Если выбран ключ партиционирования с высокой мощностью, то части, распределенные по тысячам партиций, никогда не будут кандидатами на слияние, что приведет к превышению заранее заданных ограничений и вызовет пресловутую ошибку Too many parts. Решение этой проблемы простое: выберите разумный ключ партиционирования с мощностью менее 1000..10000.
Вы можете выполнить запрос, чтобы получить список всех существующих уникальных партиций нашей тестовой таблицы, используя виртуальный столбец _partition_value:
Кроме того, ClickHouse отслеживает все части и партиции всех таблиц в системной таблице system.parts, а следующий запрос возвращает для приведённой выше тестовой таблицы список всех партиций, а также текущее количество активных частей и суммарное число строк в этих частях для каждой партиции:
Для чего нужны партиции таблиц?
В ClickHouse партиционирование в первую очередь используется для управления данными. Благодаря логической организации данных на основе выражения партиционирования каждой партицией можно управлять независимо. Например, схема партиционирования в приведённой выше таблице позволяет реализовать сценарии, в которых в основной таблице хранятся только данные за последние 12 месяцев, а более старые данные автоматически удаляются с помощью правила TTL (см. добавленную последнюю строку DDL-оператора):
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH DELETE;
Поскольку таблица разбита на партиции по toStartOfMonth(date), целые партиции (наборы частей таблицы), подпадающие под условие TTL, будут удаляться, что повышает эффективность очистки без переписывания частей.
Аналогично, вместо удаления старых данных их можно автоматически и эффективно перемещать на более экономичный уровень хранилища:
CREATE TABLE uk.uk_price_paid_simple_partitioned
(
date Date,
town LowCardinality(String),
street LowCardinality(String),
price UInt32
)
ENGINE = MergeTree
PARTITION BY toStartOfMonth(date)
ORDER BY (town, street)
TTL date + INTERVAL 12 MONTH TO VOLUME 'slow_but_cheap';
Партиции могут повысить производительность запросов, но это сильно зависит от характера доступа к данным. Если запросы обращаются только к нескольким партициям (в идеале — к одной), производительность может улучшиться. На практике это обычно полезно только тогда, когда ключ партиционирования не входит в первичный ключ, а фильтрация выполняется именно по нему, как показано в примере запроса ниже.
Этот запрос выполняется для таблицы из примера выше и вычисляет максимальную цену среди всех проданных объектов недвижимости в Лондоне за декабрь 2020 года, используя фильтрацию как по столбцу (date), который входит в ключ партиционирования таблицы, так и по столбцу (town), который входит в первичный ключ таблицы (при этом date не является частью первичного ключа).
ClickHouse обрабатывает этот запрос, применяя последовательность методов отсеивания данных, чтобы не обрабатывать нерелевантные данные:
① Отсечение партиций: MinMax indexes используются, чтобы игнорировать целые партиции (наборы частей), которые заведомо не могут соответствовать фильтру запроса по столбцам, используемым в ключе партиционирования таблицы.
② Отсечение гранул: Для оставшихся частей данных после шага ① используется их первичный индекс, чтобы игнорировать все гранулы (блоки строк), которые заведомо не могут соответствовать фильтру запроса по столбцам, используемым в первичном ключе таблицы.
Мы можем увидеть эти шаги отсеивания данных, изучив физический план выполнения для нашего примера запроса выше с помощью оператора EXPLAIN :
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE date >= '2020-12-01'
AND date <= '2020-12-31'
AND town = 'LONDON';
┌─explain──────────────────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple_partitioned) │
6. │ Indexes: │
7. │ MinMax │
8. │ Keys: │
9. │ date │
10. │ Condition: and((date in (-Inf, 18627]), (date in [18597, +Inf))) │
11. │ Parts: 1/436 │
12. │ Granules: 11/3257 │
13. │ Partition │
14. │ Keys: │
15. │ toStartOfMonth(date) │
16. │ Condition: and((toStartOfMonth(date) in (-Inf, 18597]), (toStartOfMonth(date) in [18597, +Inf))) │
17. │ Parts: 1/1 │
18. │ Granules: 11/11 │
19. │ PrimaryKey │
20. │ Keys: │
21. │ town │
22. │ Condition: (town in ['LONDON', 'LONDON']) │
23. │ Parts: 1/1 │
24. │ Granules: 1/11 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Вывод выше показывает:
① Отсечение партиций: строки 7–18 приведённого выше вывода EXPLAIN показывают, что ClickHouse сначала использует индекс MinMax поля date, чтобы выявить 11 из 3257 существующих гранул (блоков строк), хранящихся в 1 из 436 существующих активных частей данных, которые содержат строки, соответствующие фильтру date в запросе.
② Отсечение гранул: строки 19–24 приведённого выше вывода EXPLAIN показывают, что затем ClickHouse использует первичный индекс (созданный по полю town) части данных, определённой на шаге ①, чтобы ещё сильнее сократить число гранул (содержащих строки, которые потенциально также соответствуют фильтру town в запросе) — с 11 до 1. Это также отражено в выводе ClickHouse-client, который мы привели выше для выполненного запроса:
... Elapsed: 0.006 sec. Processed 8.19 thousand rows, 57.34 KB (1.36 million rows/s., 9.49 MB/s.)
Peak memory usage: 2.73 MiB.
Это означает, что ClickHouse просканировал и обработал 1 гранулу (блок из 8192 строк) за 6 миллисекунд, чтобы вычислить результат запроса.
Партиционирование — это прежде всего средство управления данными
Имейте в виду, что запросы по всем партициям обычно выполняются медленнее, чем тот же запрос к таблице без партиционирования.
При партиционировании данные обычно распределяются по большему числу частей данных, из-за чего ClickHouse часто приходится сканировать и обрабатывать больший объем данных.
Это можно продемонстрировать, выполнив один и тот же запрос как для таблицы из примера Что такое части таблицы (без партиционирования), так и для нашей текущей таблицы из примера выше (с партиционированием). Обе таблицы содержат одни и те же данные и одинаковое количество строк:
Однако в таблице с партиционированием больше активных частей данных, потому что, как упоминалось выше, ClickHouse выполняет слияние части данных только внутри партиций, но не между ними:
Как показано выше, партиционированная таблица uk_price_paid_simple_partitioned имеет более 600 партиций и, следовательно, 600 306 активных частей данных. В то же время в таблице без партиционирования uk_price_paid_simple все исходные части данных могли быть объединены фоновыми слияниями в одну активную часть.
Если посмотреть физический план выполнения нашего запроса из примера выше без фильтра по партиции для партиционированной таблицы с помощью оператора EXPLAIN, то в строках 19 и 20 приведенного ниже вывода видно, что ClickHouse определил 671 из 3257 существующих гранул (блоков строк), распределенных по 431 из 436 существующих активных частей данных, которые потенциально содержат строки, соответствующие фильтру запроса, и поэтому будут просканированы и обработаны движком запросов:
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE town = 'LONDON';
┌─explain─────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple_partitioned) │
6. │ Indexes: │
7. │ MinMax │
8. │ Condition: true │
9. │ Parts: 436/436 │
10. │ Granules: 3257/3257 │
11. │ Partition │
12. │ Condition: true │
13. │ Parts: 436/436 │
14. │ Granules: 3257/3257 │
15. │ PrimaryKey │
16. │ Keys: │
17. │ town │
18. │ Condition: (town in ['LONDON', 'LONDON']) │
19. │ Parts: 431/436 │
20. │ Granules: 671/3257 │
└─────────────────────────────────────────────────────────────────┘
Физический план выполнения того же примерного запроса для таблицы без партиций показывает в строках 11 и 12 приведённого ниже вывода, что ClickHouse выявил 241 из 3083 существующих блоков строк в единственной активной части данных таблицы, которые потенциально содержат строки, соответствующие фильтру запроса:
EXPLAIN indexes = 1
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─explain───────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Keys: │
9. │ town │
10. │ Condition: (town in ['LONDON', 'LONDON']) │
11. │ Parts: 1/1 │
12. │ Granules: 241/3083 │
└───────────────────────────────────────────────────────┘
При выполнении запроса к партиционированной версии таблицы ClickHouse сканирует и обрабатывает 671 блок строк (~ 5,5 миллиона строк) за 90 миллисекунд:
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple_partitioned
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594.30 million
└───────────────┘
1 row in set. Elapsed: 0.090 sec. Processed 5.48 million rows, 27.95 MB (60.66 million rows/s., 309.51 MB/s.)
Peak memory usage: 163.44 MiB.
В то время как при выполнении запроса к непартиционированной таблице ClickHouse сканирует и обрабатывает 241 блок (~ 2 миллиона строк) за 12 миллисекунд:
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594,30 млн
└───────────────┘
1 row in set. Elapsed: 0.012 sec. Processed 1.97 million rows, 9.87 MB (162.23 million rows/s., 811.17 MB/s.)
Peak memory usage: 62.02 MiB.
Последнее изменение 10 июня 2026 г.