OLAP エンジン - ClickHouse の最適化
1. データの整合性の問題
データの整合性をサポートする最高の Mergetree でさえ、結果整合性のみを保証します。
ReplacingMergeTree
- このエンジンは、削除するという点で MergeTree とは異なります
排序键值相同的重复项
。 - データ重複排除は、データ統合中にのみ実行されます。
合并会在后台一个不确定的时间进行
、そのため、前もって計画することはできません。 - OPTIMIZE ステートメントを呼び出して予定外のマージを開始することはできますが、OPTIMIZE ステートメントによってデータの読み取りと書き込みが大量に発生する可能性があるため、これに依存しないでください。
したがって、ReplacingMergeTree はバックグラウンドで重複データをクリアしてスペースを節約するのに適していますが、重複データが表示されないことを保証するものではありません。
ReplacingMergeTree や SummingMergeTree などのテーブル エンジンを使用すると、一時的なデータの不整合が発生する場合があります。一貫性が非常に重要な一部のシナリオでは、通常、次の解決策があります。
1.1 テストテーブルとデータの準備
-- 1、创建表
-- user_id 是数据去重更新的标识;
-- create_time 是版本号字段,每组数据中 create_time 最大的一行表示最新的数据;
-- deleted 是自定的一个标记位,比如 0 代表未删除,1 代表删除数据。
CREATE TABLE test_distinct(
user_id UInt64,
score String,
deleted UInt8 DEFAULT 0,
create_time DateTime DEFAULT toDateTime(0)
) ENGINE= ReplacingMergeTree(create_time)
ORDER BY user_id;
-- 2、写入 100 万 测试数据
INSERT INTO TABLE test_distinct(user_id,score)
WITH(
SELECT ['A','B','C','D','E','F','G']
)AS dict
SELECT number AS user_id, dict[number%7+1] FROM numbers(1000000);
-- 3、修改前 5 万 行数据,修改内容包括 name 字段和 create_time 版本号字段
INSERT INTO TABLE test_distinct(user_id,score,create_time)
WITH(
SELECT ['AA','BB','CC','DD','EE','FF','GG']
)AS dict
SELECT number AS user_id, dict[number%7+1], now() AS create_time FROM
numbers(50000);
-- 4、还未触发分区合并,所以还未去重
SELECT COUNT() FROM test_distinct;
┌─count()─┐
│ 1050000 │
└─────────┘
1.2 FINAL による重複排除
在查询语句后增加 FINAL 修饰符,这样在查询的过程中将会执行 Merge 的特殊逻辑(例如数据去重,预聚合等)。
注: FINAL を追加すると、クエリがシングル スレッドの実行プロセスになり、クエリの速度が非常に遅くなるため、このメソッドは基本的に初期バージョンでは使用されませんでした。
v20.5.2.7-stable バージョンでは、FINAL 查询支持多线程执行
1 つのクエリのスレッド数を max_final_threads パラメータで制御できます。
SELECT COUNT() FROM test_distinct final;
┌─count()─┐
│ 1000000 │
└─────────┘
1.3 手動最適化 (非推奨)
-- 在写入数据后,立刻执行 OPTIMIZE 强制触发新写入分区的合并动作。
OPTIMIZE TABLE test_distinct FINAL;
-- 语法如下
OPTIMIZE TABLE [db.]name [ON CLUSTER cluster] [PARTITION partition |
PARTITION ID 'partition_id'] [FINAL] [DEDUPLICATE [BY expression]]
-- 再次查询,发现已经去重
centos04 :) SELECT COUNT() FROM test_distinct;
┌─count()─┐
│ 1000000 │
└─────────┘
1.4 group by による重複排除
-- 1、利用group by进行去重
CREATE VIEW view_test_distinct AS
SELECT
user_id ,
argMax(score, create_time) AS score, -- 按照 create_time 的最大值取 score 的值
argMax(deleted, create_time) AS deleted, -- 按照 create_time 的最大值取 deleted 的值
max(create_time) AS ctime
FROM test_distinct
GROUP BY user_id -- 对利用group by进行去重
HAVING deleted = 0; -- 筛选未删除的数据
-- 2、再次插入一条数据(重复数据)
INSERT INTO TABLE test_distinct(user_id,score,create_time) VALUES(0,'AAAA',now());
SELECT
*
FROM test_distinct
WHERE user_id = 0;
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│ 0 │ AAAA │ 0 │ 2023-04-06 12:59:03 │
└─────────┴───────┴─────────┴─────────────────────┘
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│ 0 │ AA │ 0 │ 2023-04-06 12:48:26 │
└─────────┴───────┴─────────┴─────────────────────┘
SELECT
*
FROM view_test_distinct
WHERE user_id = 0;
┌─user_id─┬─score─┬─deleted─┬───────────────ctime─┐
│ 0 │ AAAA │ 0 │ 2023-04-06 12:59:03 │
└─────────┴───────┴─────────┴─────────────────────┘
-- 3、再次插入一条标记为删除的数据
INSERT INTO TABLE test_distinct(user_id,score,deleted,create_time)
VALUES(0,'AAAA',1,now());
SELECT
*
FROM test_distinct
WHERE user_id = 0;
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│ 0 │ AAAA │ 0 │ 2023-04-06 12:59:03 │
└─────────┴───────┴─────────┴─────────────────────┘
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│ 0 │ AAAA │ 1 │ 2023-04-06 13:02:22 │
└─────────┴───────┴─────────┴─────────────────────┘
┌─user_id─┬─score─┬─deleted─┬─────────create_time─┐
│ 0 │ AA │ 0 │ 2023-04-06 12:48:26 │
└─────────┴───────┴─────────┴─────────────────────┘
SELECT
*
FROM view_test_distinct
WHERE user_id = 0;
-- 这行数据并没有被真正的删除,而是被过滤掉了。在一些合适的场景下,可以结合 表级别的 TTL 最终将物理数据删除。
次に、実行計画を表示します
-- 语法如下
EXPLAIN [AST | SYNTAX | PLAN | PIPELINE] [setting = value, ...]
SELECT ... [FORMAT ...]
➢ 計画:用于查看执行计划,默认值
。
◼ ヘッダー プランの各ステップのヘッドの説明を出力します。デフォルトはオフで、デフォルト値は 0 です。
◼ 説明 プランの各ステップの説明を出力します。デフォルトで有効になっており、デフォルト値は 1 です。
◼ アクション プランの各ステップの詳細情報を出力します。デフォルトはオフで、デフォルト値は 0 です。
➢ AST: 構文ツリーを表示するために使用されます。
➢ SYNTAX: 構文を最適化するために使用されます。
➢ PIPELINE: PIPELINE プランを表示するために使用します。
◼ ヘッダー デフォルトでは閉じられている計画の各ステップのヘッド指示を印刷します。
◼ グラフは、DOT グラフィック言語を使用してパイプライン ダイアグラムを記述します。パイプライン ダイアグラムはデフォルトで閉じられています。関連するグラフィックを表示するには、graphviz と協力する必要があります。
◼ アクション グラフが有効になっている場合、デフォルトでコンパクト印刷が有効になります。
3. テーブル作成の最適化
3.1 フィールドタイプ
3.1.1 時間フィールドの種類
テーブルを作成するときに、数値または日時で表現できるフィールドに文字列を使用しないでください。
ClickHouseの最下層はDateTimeをtimestampのLong型として格納していますが、DateTimeは関数で変換する必要がなく、実行効率が高く可読性が高いため、Long型の格納は推奨されていません。
create table t_a(
id UInt32,
sku_id String,
total_amount Decimal(16,2) ,
create_time Int32
) engine =ReplacingMergeTree(create_time)
partition by toYYYYMMDD(toDate(create_time)) --需要转换一次,否则报错
primary key (id)
order by (id, sku_id);
3.1.2 null 格納タイプ
Nullable カラムを格納する際に NULL マークを格納するために追加のファイルを作成する必要があり、Nullable カラムはインデックスを作成できないため、Nullable 型はほとんどの場合パフォーマンスを低下させると関係者は指摘しています。
したがって、よほど特別な事情がある場合や、应直接使用字段默认值表示空
業務上意味のない値をご自身で指定する場合を除きます。
3.2 パーティションとインデックス
- 通常、日ごとに分割することを選択します。
- 1 億個のデータは、通常、約 30 のパーティションを選択します。
- インデックス列を指定する必要があります.ClickHouseのインデックス列は、order byで指定されるソート列です. インデックス: order by(a,b,c) は最初に左から右にインデックス付けされ、頻繁にクエリされるフィールドが最初に配置されます。
- カーディナリティが特に大きい場合、インデックス列には適していません。
- カーディナリティが大きい列: この列のデータ レコードの数が近いほど、重複排除後のカーディナリティが大きくなります。
- カーディナリティが大きいとインデックス作成に適していない理由: カーディナリティが大きすぎると、検索を順番にトラバースする必要があり、インデックス作成の意味が失われます。
……
PARTITION BY toYYYYMM(EventDate)
ORDER BY (CounterID, EventDate, intHash32(UserID)) -- UserID基数特别大的不适合做索引列,利用intHash32()解决
……
3.3 テーブルパラメータ
- Index_granularity は、インデックスの粒度を制御するために使用されます。デフォルトは 8192 です。必要でない場合は、調整することはお勧めしません。
- 全量の履歴データをテーブルに保持する必要がない場合は、
建议制定TTL(生存时间值)
期限切れの履歴データを手動で処理する手間を省くことができ、TTL も Alter table を介していつでも変更できます。
3.4 書き込みと削除の最適化
- 単一または小規模なバッチの削除および挿入操作を実行しないようにしてください。これにより、小さなパーティション ファイルが生成され、バックグラウンドのマージ タスクに大きな負荷がかかります。
- 一度に多くのパーティションを書き込んだり、データをすばやく書きすぎたりしないでください。
- データの書き込みが速すぎると、マージ速度が追いつかず、エラーが報告されます
一般建议每秒钟发起2-3次写入操作,每次操作写入2w-5w条数据
。
- データの書き込みが速すぎると、マージ速度が追いつかず、エラーが報告されます
3.5 共通設定項目
3.5.1 CPU 構成
構成 | 説明 |
---|---|
background_pool_size | バックグラウンド スレッド プールのサイズ。マージ スレッドは、このスレッド プールで実行されます。このスレッド プールは、マージ スレッドだけが使用するわけではありません。デフォルト値は 16 です。許可されている場合は、CPU 数の 2 倍に変更することをお勧めします。 (1 つのコアを 2 つのスレッドに仮想化できます) |
background_schedule_pool_size | バックグラウンド タスクを実行するスレッドの数。デフォルトは 128 です。CPUの数 (スレッド数) の 2 倍に変更することをお勧めします。 |
background_distributed_schedule_pool_size | バックグラウンドタスクを実行する分散送信のスレッド数を設定します。デフォルトは 16 です。CPUの数 (スレッド数) の 2 倍に変更することをお勧めします。 |
max_concurrent_queries | 同時処理リクエスト (select、insert などを含む) の最大数、デフォルト値は 100、推奨値は150 (足りない場合は追加) ~ 300、デフォルトの単位は 1 秒あたり 1 です。 |
最大スレッド数 | 1 つのクエリで使用できる CPU の最大数を設定します。デフォルトは CPU コアの数です。 |
3.5.2 メモリ構成
構成 | 説明 |
---|---|
max_memory_usage | users.xml のこのパラメーターは、単語 Query が占有する最大メモリを示します。この値は比較的大きく設定できるため、クラスター クエリの上限を増やすことができます。128G メモリのマシンなど、OS 用に少し予約して、100G に設定します。 |
max_bytes_before_external_group_by | 通常、メモリは max_memory_usage の半分に従って設定され、グループが使用するメモリがしきい値を超えると、実行用のディスクに更新されます。クリックハウスアグリゲーションは、クエリと中間データの作成、中間データのマージ、前のアイテムと合わせて、 2 つのフェーズに分かれているため、50GB が推奨されます。 |
max_bytes_before_external_sort | order by が max_bytes_before_external_sort メモリを使用した場合、(ディスクの並べ替えに基づいて) ディスクを上書きします. この値が設定されていない場合, メモリが不足している場合にエラーがスローされます. この値が設定されている場合, order by正常に完了することができますが、速度はストレージ メモリに比べて遅くなければなりません (実際の測定は非常に遅く、受け入れられません)。 |
max_table_size_to_drop | このパラメータは、config.xml でテーブルまたはパーティションを削除するために使用されます。デフォルトは 50GB です。これは、50GB を超えるパーティション テーブルを削除すると失敗することを意味します。0 に変更することをお勧めします。これにより、パーティション テーブルがどれほど大きくても削除できるようになります。 |
4. クエリの最適化
4.1 単一テーブル クエリの最適化
4.1.1 where の代わりに prewhere
prewhere および where ステートメントは同じ効果があり、データのフィルタリングに使用されます。違いは、 prewhere はエンジンの MergeTree ファミリのテーブルのみをサポートすることです首先会读取指定的列数据,来判断数据过滤,等待数据过滤之后再读取 select 声明的列字段来补全其余属性
。
クエリ列がフィルタ列よりも明らかに多い場合、prewhere を使用するとクエリのパフォーマンスが 10 倍向上し、フィルタリング段階でデータの読み取り方法が自動的に最適化され、io 操作が削減されます。
場合によっては、prewhere ステートメントは where ステートメントよりも処理するデータが少なく、パフォーマンスが高くなります。
4.1.2 列の枝刈りとパーティションの枝刈り
- 列のプルーニング: 実際には、select * を使用して必要なフィールドを除外することを避けるためです。
- パーティションのプルーニング: select * の使用を避ける、where でフィールドによるパーティションを使用する、パーティションを選択する
4.1.3 where と limit を組み合わせた順序
order by クエリは、1,000 万を超えるデータ セットの場合、where 条件および limit ステートメントと一緒に使用する必要があります。
4.1.4 仮想列の構築を避ける
If it is not required, do not build virtual columns on the result set. 仮想列はリソースを消費し、パフォーマンスを浪費します. フロント エンドでそれらを処理するか、テーブルに実際のフィールドを構築して追加のストレージとして使用することを検討できます.
- 仮想列: 次のような、元のテーブルには存在しない計算列:
select
a,
b,
a+b -- 虚拟列,虚拟列非常消耗资源,浪费性能。
from
table
4.1.5 個別の代わりに uniqCombined
- uniqCombined: おおよその重複排除ですが、精度はそれほど低くなく、差は非常に小さいです
パフォーマンスは 10 倍以上向上する可能性があり、uniqCombined の基礎となるレイヤーは同様の HyperLogLog アルゴリズムによって実装されます。
正確性を必要としない 1,000 万を超えるデータに対して正確な重複排除を実行し、おおよその重複排除を使用することはお勧めしません。
-- 反例:
select count(distinct rand()) from hits_v1;
-- 正例:
SELECT uniqCombined(rand()) from datasets.hits_v1
4.2 マルチテーブル関連付け
clickhouse 的JOIN:
- 原則: 適切なテーブルをメモリにロードしてから照合します。
- 使用しない、使用方法:
- フィルタリングできる場合は、最初にフィルタリングします。特に右側のテーブルです。
- 小さなテーブルを右のテーブルに置きます。
- 特殊なシナリオでは、ディクショナリ テーブルの使用を検討できます。
- 置き換えることができる場合は、JOIN の代わりに IN を使用します
-- 建表的时候,想要复制表结构:
create table XXX as select * from XXXX where 1 = 0; -- 条件不成立,数据永远不会写进来
4.2.1 結合の原則
A が B を結合し、すべてのテーブル B をメモリにロードすると、テーブル A のデータがメモリ内のテーブル B と 1 つずつ一致します。
4.2.2 JOIN を IN に置き換える
- マルチテーブル ジョイント クエリの場合、クエリ データが 1 つのテーブルのみからのものである場合、JOIN の代わりに IN 操作を検討できます。
select table_a.* from table_a where table_a.count_id in (select count_id from table_b);
4.2.3 サイズテーブルJOIN
複数のテーブルを結合する場合は、右の小さなテーブルの原則が満たされている必要があります.右のテーブルが関連付けられている場合は、メモリにロードされ、左のテーブルと比較されます.左結合、右結合、または内部結合のいずれかをクリックしますハウス、それは常に右のテーブルを保持しています.各レコードはレコードが存在するかどうかを調べるために左のテーブルに行くので、右のテーブルは小さなテーブルでなければなりません.
4.2.4 述語プッシュダウンへの注意
- 参加する前にフィルタリングしてみてください
ClickHouse は、結合クエリ中に述語プッシュダウン操作を積極的に開始しません, そして、各サブクエリは事前にフィルタリング操作を完了する必要があります. 述語プッシュダウンが実行されるかどうかは、パフォーマンスに大きな影響を与えることに注意してください.
4.2.5 分散テーブルは GLOBAL を使用する
- クエリの増幅: 2 つの分散テーブルが JOIN を実行すると、2 つのテーブルの N ノードが互いにクエリを開始し、N*N 回になります。
2 つの分散テーブルで IN と JOIN の前に GLOBAL キーワードを追加する必要があり、クエリ要求を受け取ったノードで右側のテーブルが 1 回だけクエリされ、他のノードに分散されます。GLOBAL キーワードが追加されていない場合、各ノードは右側のテーブルに対して個別にクエリを開始し、右側のテーブルは分散テーブルであるため、右側のテーブルは合計で N2 回クエリされます (N は分散テーブルのフラグメントです)。 table Quantity)、これはクエリの増幅であり、多くのオーバーヘッドが発生します。
4.2.6 ディクショナリ テーブルの使用
- 自分で作成したテーブルまたは外部ファイルにすることができます。
相関分析が必要な一部のビジネスでは、ディクショナリ テーブルがメモリに常駐するため、ディクショナリ テーブルが大きくなりすぎないように、ジョイン操作用のディクショナリ テーブルとして作成されます。
4.2.7 早期フィルタリング
実行速度を向上させ、メモリ消費を削減するために、論理フィルタリングを追加することでデータ スキャンを減らすことができます。
5. マテリアライズド ビュー
5.1 はじめに
- View: SQLの演算ロジックを保存します。
- マテリアライズド ビュー:
不仅保存SQL的操作逻辑,还保存操作过后的结果,结果根据相应的引擎存到磁盘或内存中
.
ClickHouse の具体化されたビューは、クエリ結果の一種の永続性であり、クエリ効率の向上をもたらします。ユーザーがチェックを入れるとテーブルと変わりません. これはテーブルであり、一张时刻在预计算的表
作成プロセスで特別なエンジンが使用されているように見えます。
「クエリ結果セット」の範囲は非常に広く、基本テーブルの一部のデータの単純なコピー、または複数テーブルの結合後に生成された結果またはそのサブセット、またはオリジナルデータなど したがって、基になるテーブルが変更されてもマテリアライズド ビューは変更されないため、スナップショット (スナップショット) とも呼ばれます。
利点: クエリの速度が速い. マテリアライズド ビューのすべてのルールが記述されている場合、元のデータ クエリよりもはるかに高速であり、すべて事前に計算されているため、行の総数が少なくなります。
デメリット:基本的にストリーミングデータの利用シナリオであり、付加的な技術であるため、重複排除やデコアなどの分析に履歴データを利用したい場合、マテリアライズドビューでの利用はあまり容易ではありません。特定のシナリオでの使用も制限されます。また、テーブルに多くの具体化されたビューがある場合、このテーブルを書き込むときに、大量のマシン リソースが消費されます。たとえば、データ帯域幅がいっぱいになり、ストレージが一度に大幅に増加しました。
5.2 基本構文
作成時に、ビュー データを保持するための非表示のターゲット テーブルが作成されます。TO で指定して、明示的なテーブルに保存することもできます。TO テーブル名が追加されていない場合、テーブル名はデフォルトで .inner. マテリアライズド ビュー名になります。
CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]table_name [ON CLUSTER] [TO[db.]name] [ENGINE = engine] [POPULATE] AS SELECT ...
- [POPULATE]: 追加後、ビューの作成時に履歴データがトラバースされるため、サーバーの負荷が増加します
如果要历史数据,使用INSERT INTO写入数据
。 TO [db].[table]
それなしでマテリアライズド ビューを作成する場合は、ENGINE
データを格納するテーブル エンジンを指定する必要があります。- を使用して
TO [db].[table]
マテリアライズド ビューを作成する場合POPULATE
。 - 具体化されたビューの実装は次のとおりです。指定されたテーブルにデータを挿入すると、データの挿入された部分がクエリ
SELECT
によってSELECT
変換され、結果がビューに挿入されます。 - クエリ ステートメントには、次の句を含めることができます: DISTINCT、GROUP BY、ORDER BY、LIMIT...
5.3 事例の詳細説明
-- 1、创建测试数据
create table test_a_test(
user_id UInt64,
score String,
deleted UInt8 DEFAULT 0,
create_time Date
) ENGINE = MergeTree()
partition by toYYYYMM(create_time)
order by (create_time,intHash32(user_id))
sample by intHash32(user_id)
SETTINGS index_granularity = 8192;
insert into
test_a_test
select
*
FROM
test_a
limit 10000;
-- 2、创建物化视图
create materialized view test_mview
engine = SummingMergeTree
partition by toYYYYMM(create_time)
order by (create_time,intHash32(user_id))
as
SELECT
user_id,
create_time,
count(score),
sum(deleted)
from
test_a_test ta
WHERE
create_time >= toDate(0)
group by user_id,create_time ;
show tables;
>>>结果
.inner_id.5bfba660-812e-49ec-885f-3fa63e16f2f4 -- 默认存储数据的表格
test_a_test
test_mview
-- 3、插入数据
SELECT * from test_mview; -- 第一次查询结果为空
insert into
test_a_test
select
*
FROM
test_a
limit 10;
SELECT * from test_mview; -- 插入后在查询有10条新增数据
select * from `.inner_id.068a0cde-c260-4fee-b902-c7f74cc4f194`; -- 自动创建的表中也有数据
-- 4、导入历史数据(重点!!!)
insert into
test_mview
-- 将物化视图的逻辑再写一遍
SELECT
user_id,
create_time,
count(score),
sum(deleted)
from
test_a_test ta
WHERE
create_time >= toDate(0)
group by user_id,create_time