ClickHouseにおけるテーブルパーティションとは何ですか?
パーティションは、テーブルの data parts を MergeTreeエンジンファミリー において、時間範囲、カテゴリ、またはその他のキー属性などの特定の基準に一致する、概念的に意味のある論理的ユニットとして整理する方法です。これらの論理的ユニットにより、データの管理、クエリ、最適化が容易になります。
PARTITION BY
テーブルを最初に定義する際に、PARTITION BY句を使用することでパーティショニングを有効にできます。この句には、任意のカラムに関するSQL式を含めることができ、その結果が行が属するパーティションを定義します。
これを示すために、What are table parts の例のテーブルに、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);
このテーブルをクエリすることができます。
ディスク上の構造
行のセットがテーブルに挿入されるたびに、全ての挿入された行を含む1つのデータパートを作成する代わりに、ClickHouseは挿入された行の中でそれぞれユニークなパーティションキー値に対して新しいデータパートを作成します:
ClickHouseサーバーは、上記の図に示した4行の挿入の行をまずtoStartOfMonth(date)
のパーティションキー値によって分割します。
次に、特定された各パーティションに対して、行は通常通りに処理されます(① ソート、② カラムへの分割、③ 圧縮、④ ディスクへの書き込み)。
パーティショニングが有効になっている場合、ClickHouseは自動的に各データパートに対してMinMaxインデックスを作成することに注意してください。これは、パーティションキー式で使用される各テーブルカラムの最小値と最大値を含むファイルです。
パーティションごとのマージ
パーティショニングが有効な場合、ClickHouseはパーティション内でのみデータパーツをマージし、パーティション間ではマージしません。これは、上記の例のテーブルに対して示しています:
上の図に示すように、異なるパーティションに属するパーツは決してマージされません。高いカーディナリティのパーティションキーを選択すると、数千のパーティションに分散されたパーツは、事前に設定された制限を超えて、忌まわしい Too many ^^parts^^
エラーを引き起こします。この問題に対処するのは簡単です: カーディナリティが1000〜10000未満 の妥当なパーティションキーを選択してください。
パーティションの監視
私たちの例のテーブルにおける全ての既存のユニークパーティションのリストを取得するには、仮想カラム _partition_value
を使用することでクエリできます:
SELECT DISTINCT _partition_value AS partition
FROM uk.uk_price_paid_simple_partitioned
ORDER BY partition ASC;
あるいは、ClickHouseはすべてのテーブルのすべての部分とパーティションをsystem.partsシステムテーブルで追跡しており、以下のクエリは、上記の例のテーブルのすべてのパーティションのリストと、これらのパーティションごとの現在のアクティブパーツの数および行の合計を返します:
SELECT
partition,
count() AS parts,
sum(rows) AS rows
FROM system.parts
WHERE (database = 'uk') AND (`table` = 'uk_price_paid_simple_partitioned') AND active
GROUP BY partition
ORDER BY partition ASC;
テーブルパーティションは何に使用されるか?
データ管理
ClickHouseにおいて、パーティショニングは主にデータ管理機能です。パーティション式に基づいて論理的にデータを整理することで、各パーティションを独立して管理できます。たとえば、上の例のテーブルのパーティショニングスキームは、TTLルールを使用して、古いデータを自動的に削除することにより、メインテーブルに最後の12か月のデータだけが保持されるシナリオを可能にします(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条件を満たす全体のパーティション(table partsのセット)は削除され、クリーンアップ操作がより効率的に行われ、パーツを書き直す必要がないのです。
同様に、古いデータを削除するのではなく、よりコスト効果の高いストレージ階層に自動的かつ効率的に移動することができます:
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';
クエリ最適化
パーティションはクエリパフォーマンスに役立ちますが、これはアクセスパターンに大きく依存します。クエリがわずか数パーティション(理想的には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';
このクエリは、上記の例のテーブルに対して実行され、Londonで2020年12月に販売されたすべてのプロパティの最高価格を計算します。このクエリは、テーブルのパーティションキーとして使用されるカラム(date
)と、テーブルの主キーとして使用されるカラム(town
)の両方でフィルタリングを行います(date
は主キーの一部ではありません)。
ClickHouseは、このクエリを無関係なデータを評価しないために一連のプルーニング技術を適用することによって処理します:
① パーティションプルーニング: MinMaxインデックス は、テーブルのパーティションキーで使用されるカラムに対するクエリのフィルターに合致しない論理的にパートを無視するために使用されます。
② グラニュールプルーニング: ステップ①の後の残りのデータパーツに対して、そのプライマリインデックスが使用され、テーブルの主キーで使用されるカラムに対するクエリのフィルターに合致しない論理的に無関係なすべてのグラニュール(行のブロック)を無視します。
クリックハウスは、上記の例のクエリの物理クエリ実行プランを調査することによって、これらのデータプルーニングステップを観察できます:
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 │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
上記の出力は以下を示します:
① パーティションプルーニング: EXPLAIN出力の行7から18は、ClickHouseが最初にdate
フィールドのMinMaxインデックスを使用して、3257の既存のグラニュールの中で、クエリのdate
フィルターに一致する行を含む436のアクティブなデータパーツの中から11を特定したことを示しています。
② グラニュールプルーニング: EXPLAIN出力の行19から24では、ClickHouseが次にステップ①で特定されたデータパートのプライマリインデックス(town
フィールドに対して作成された)を使用して、クエリのtown
フィルターに一致する行を含むグラニュールの数を11から1に減らしたことを示しています。このことは、クエリ実行のために上に印刷したClickHouseクライアントの出力にも反映されています:
... 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がスキャンおよび処理するデータのボリュームが増えることがよくあります。
これを示すために、What are table partsの例のテーブル(パーティショニングが有効でない)と、上記の現在の例のテーブル(パーティショニングが有効)で同じクエリを実行します。両方のテーブルは同じデータと行数を含んでいます:
SELECT
table,
sum(rows) AS rows
FROM system.parts
WHERE (database = 'uk') AND (table IN ['uk_price_paid_simple', 'uk_price_paid_simple_partitioned']) AND active
GROUP BY table;
しかし、パーティションが有効なテーブルは、アクティブなデータパーツがより多いです。これは、前述のようにClickHouseがデータパーツをマージしないためです。
SELECT
table,
count() AS parts
FROM system.parts
WHERE (database = 'uk') AND (table IN ['uk_price_paid_simple', 'uk_price_paid_simple_partitioned']) AND active
GROUP BY table;
上記に示すように、パーティションされたテーブルuk_price_paid_simple_partitioned
は600以上のパーティションを持ち、したがって600,306個のアクティブなデータパーツが存在します。一方、非パーティションされたテーブルuk_price_paid_simple
は、すべての初期データがバックグラウンドでのマージによって1つのアクティブパートにマージされることができました。
私たちの例のクエリに対してチェックした場合、パーティショニングフィルターなしでパーティションテーブル上で実行した物理クエリ実行プランにおいて、出力の行19および20でClickHouseは3257の既存のグラニュール(行のブロック)の中から671を特定し、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が、クエリのフィルタに一致する行を含む可能性がある、テーブルの単一のアクティブデータパート内の3083の既存の行のブロックの241を特定したことを示しています:
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のブロック(約550万行)を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.
一方、非パーティションテーブルでのクエリ実行 では248のブロック(約200万行)を12ミリ秒でスキャンおよび処理します:
SELECT MAX(price) AS highest_price
FROM uk.uk_price_paid_simple
WHERE town = 'LONDON';
┌─highest_price─┐
│ 594300000 │ -- 594.30 million
└───────────────┘
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.