欢迎拜访我的个人博客。

Clickhouse 中最强大的表引擎当属 MergeTree (兼并树)引擎及该系列(*MergeTree)中的其他引擎。

MergeTree 系列的引擎被设计用于刺进极很多的数据到一张表傍边。数据能够以数据片段的形式一个接着一个的快速写入,数据片段在后台按照一定的规矩进行兼并。比较在刺进时不断修正(重写)已存储的数据,这种战略会高效很多。

主要特点:

  • 存储的数据按主键排序。

这使得您能够创立一个小型的稀疏索引来加速数据检索。

  • 假如指定了 分区键 的话,能够运用分区。

在相同数据集和相同成果集的情况下 ClickHouse 中某些带分区的操作会比一般操作更快。查询中指定了分区键时 ClickHouse 会主动截取分区数据。这也有效增加了查询功能。

  • 支撑数据副本。

ReplicatedMergeTree 系列的表提供了数据副本功能。

  • 支撑数据采样。

需求的话,您能够给表设置一个采样方法。

!!! note “注意” 兼并 引擎并不归于 *MergeTree 系列。

建表

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
    ...
    INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
    INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MergeTree()
ORDER BY expr
[PARTITION BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[TTL expr [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
[SETTINGS name=value, ...]

子句

  • ENGINE – 引擎名和参数。 ENGINE = MergeTree(). MergeTree 引擎没有参数。

  • ORDER BY — 排序键。

可所以一组列的元组或恣意的表达式。 例如: ORDER BY (CounterID, EventDate) 。

假如没有运用 PRIMARY KEY 显式指定的主键,ClickHouse 会运用排序键作为主键。

假如不需求排序,能够运用 ORDER BY tuple(). 参阅 挑选主键

  • PARTITION BY — 分区键 ,可选项。

大多数情况下,不需求分运用区键。即便需求运用,也不需求运用比月更细粒度的分区键。分区不会加速查询(这与 ORDER BY 表达式不同)。永久也别运用过细粒度的分区键。不要运用客户端指定分区标识符或分区字段称号来对数据进行分区(而是将分区字段标识或称号作为 ORDER BY 表达式的榜首列来指定分区)。

要按月分区,能够运用表达式 toYYYYMM(date_column) ,这儿的 date_column 是一个 Date 类型的列。分区名的格局会是 “YYYYMM” 。

  • PRIMARY KEY – 假如要 挑选与排序键不同的主键,在这儿指定,可选项。

默许情况下主键跟排序键(由 ORDER BY 子句指定)相同。 因而,大部分情况下不需求再专门指定一个 PRIMARY KEY 子句。

  • SAMPLE BY – 用于抽样的表达式,可选项。

假如要用抽样表达式,主键中有必要包括这个表达式。例如: SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID)) 。

  • TTL – 指定行存储的持续时刻并界说数据片段在硬盘和卷上的移动逻辑的规矩列表,可选项。

表达式中有必要存在至少一个 Date 或 DateTime 类型的列,比方:

TTL date + INTERVAl 1 DAY

规矩的类型 DELETE|TO DISK ‘xxx’|TO VOLUME ‘xxx’指定了当满意条件(抵达指定时刻)时所要履行的动作:移除过期的行,仍是将数据片段(假如数据片段中的一切行都满意表达式的话)移动到指定的磁盘(TO DISK ‘xxx’) 或 卷(TO VOLUME ‘xxx’)。默许的规矩是移除(DELETE)。能够在列表中指定多个规矩,但最多只能有一个DELETE的规矩。

  • SETTINGS — 操控 MergeTree 行为的额定参数,可选项:

    index_granularity — 索引粒度。索引中相邻的『符号』间的数据行数。默许值8192 。参阅数据存储。
    index_granularity_bytes — 索引粒度,以字节为单位,默许值: 10Mb。假如想要仅按数据行数约束索引粒度, 请设置为0(不建议)。
    min_index_granularity_bytes – 答应的最小数据粒度,默许值:1024b。该选项用于避免误操作,增加了一个非常低索引粒度的表。参阅数据存储
    enable_mixed_granularity_parts — 是否启用经过 index_granularity_bytes 操控索引粒度的巨细。在19.11版别之前, 只要 index_granularity 装备能够用于约束索引粒度的巨细。当从具有很大的行(几十上百兆字节)的表中查询数据时候,index_granularity_bytes 装备能够提高ClickHouse的功能。假如您的表里有很大的行,能够敞开这项装备来提高SELECT 查询的功能。
    use_minimalistic_part_header_in_zookeeper — ZooKeeper中数据片段存储方法 。假如use_minimalistic_part_header_in_zookeeper=1 ,ZooKeeper 会存储更少的数据。更多信息参阅[服务装备参数](Server Settings | ClickHouse Documentation)这章中的 设置描绘 。
    min_merge_bytes_to_use_direct_io — 运用直接 I/O 来操作磁盘的兼并操作时要求的最小数据量。兼并数据片段时,ClickHouse 会计算要被兼并的一切数据的总存储空间。假如巨细超越了 min_merge_bytes_to_use_direct_io 设置的字节数,则 ClickHouse 将运用直接 I/O 接口(O_DIRECT 选项)对磁盘读写。假如设置 min_merge_bytes_to_use_direct_io = 0 ,则会禁用直接 I/O。默许值:10 * 1024 * 1024 * 1024 字节。

    merge_with_ttl_timeout — TTL兼并频率的最小距离时刻,单位:秒。默许值: 86400 (1 天)。
    write_final_mark — 是否启用在数据片段尾部写入最终索引符号。默许值: 1(不要关闭)。
    merge_max_block_size — 在块中进行兼并操作时的最大行数约束。默许值:8192
    storage_policy — 存储战略。 拜见 运用具有多个块的设备进行数据存储.
    min_bytes_for_wide_part,min_rows_for_wide_part 在数据片段中能够运用Wide格局进行存储的最小字节数/行数。您能够不设置、只设置一个,或全都设置。参阅:数据存储
    max_parts_in_total – 一切分区中最大块的数量(意义不明)
    max_compress_block_size – 在数据紧缩写入表前,未紧缩数据块的最大巨细。您能够在大局设置中设置该值(拜见max_compress_block_size)。建表时指定该值会覆盖大局设置。
    min_compress_block_size – 在数据紧缩写入表前,未紧缩数据块的最小巨细。您能够在大局设置中设置该值(拜见min_compress_block_size)。建表时指定该值会覆盖大局设置。
    max_partitions_to_read – 一次查询中可拜访的分区最大数。您能够在大局设置中设置该值(拜见max_partitions_to_read)。

示例装备

ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate, intHash32(UserID)) SAMPLE BY intHash32(UserID) SETTINGS index_granularity=8192

在这个比如中,咱们设置了按月进行分区。

一起咱们设置了一个按用户 ID 哈希的抽样表达式。这使得您能够对该表中每个 CounterIDEventDate 的数据伪随机分布。假如您在查询时指定了 SAMPLE 子句。 ClickHouse会回来关于用户子集的一个均匀的伪随机数据采样。

数据存储

表由按主键排序的数据片段(DATA PART)组成。

当数据被刺进到表中时,会创立多个数据片段并按主键的字典序排序。例如,主键是 (CounterID, Date) 时,片段中数据首先按 CounterID 排序,具有相同 CounterID 的部分按 Date 排序。

不同分区的数据会被分红不同的片段,ClickHouse 在后台兼并数据片段以便更高效存储。不同分区的数据片段不会进行兼并。兼并机制并不确保具有相同主键的行全都兼并到同一个数据片段中。

数据片段能够以 Wide 或 Compact 格局存储。在 Wide 格局下,每一列都会在文件体系中存储为单独的文件,在 Compact 格局下一切列都存储在一个文件中。Compact 格局能够提高刺进量少刺进频率频频时的功能。

数据存储格局由 min_bytes_for_wide_part 和 min_rows_for_wide_part 表引擎参数操控。假如数据片段中的字节数或行数少于相应的设置值,数据片段会以 Compact 格局存储,否则会以 Wide 格局存储。

每个数据片段被逻辑的分割成颗粒(granules)。颗粒是 ClickHouse 中进行数据查询时的最小不可分割数据集。ClickHouse 不会对行或值进行拆分,所以每个颗粒总是包括整数个行。每个颗粒的榜首行经过该行的主键值进行符号, ClickHouse 会为每个数据片段创立一个索引文件来存储这些符号。关于每列,无论它是否包括在主键傍边,ClickHouse 都会存储类似符号。这些符号让您能够在列文件中直接找到数据。

颗粒的巨细经过表引擎参数 index_granularity 和 index_granularity_bytes 操控。颗粒的行数的在 [1, index_granularity] 规模中,这取决于行的巨细。假如单行的巨细超越了 index_granularity_bytes 设置的值,那么一个颗粒的巨细会超越 index_granularity_bytes。在这种情况下,颗粒的巨细等于该行的巨细。

主键的挑选

主键中列的数量并没有清晰的约束。根据数据结构,您能够在主键包括多些或少些列。这样能够:

  • 改善索引的功能。

  • 假如当时主键是 (a, b) ,在下列情况下增加另一个 c 列会提高功能:

  • 查询会运用 c 列作为条件

  • 很长的数据规模( index_granularity 的数倍)里 (a, b) 都是相同的值,而且这样的情况很遍及。换言之,就是参加另一列后,能够让您的查询略过很长的数据规模。

  • 改善数据紧缩。

    ClickHouse 以主键排序片段数据,所以,数据的一致性越高,紧缩越好。

  • 在CollapsingMergeTree 和 SummingMergeTree 引擎里进行数据兼并时会提供额定的处理逻辑。

    在这种情况下,指定与主键不同的 排序键 也是有意义的。

长的主键会对刺进功能和内存耗费有负面影响,但主键中额定的列并不影响 SELECT 查询的功能。

能够运用 ORDER BY tuple() 语法创立没有主键的表。在这种情况下 ClickHouse 根据数据刺进的次序存储。假如在运用 INSERT … SELECT 时期望坚持数据的排序,请设置 max_insert_threads = 1。

想要根据初始次序进行数据查询,运用 单线程查询

挑选与排序键不同的主键

Clickhouse能够做到指定一个跟排序键不一样的主键,此时排序键用于在数据片段中进行排序,主键用于在索引文件中进行符号的写入。这种情况下,主键表达式元组有必要是排序键表达式元组的前缀(即主键为(a,b),排序列有必要为(a,b,**))。

当运用 SummingMergeTree 和 AggregatingMergeTree 引擎时,这个特性非常有用。通常在运用这类引擎时,表里的列分两种:维度 和 度量 。典型的查询会经过恣意的 GROUP BY 对度量列进行聚兼并经过维度列进行过滤。由于 SummingMergeTree 和 AggregatingMergeTree 会对排序键相同的行进行聚合,所以把一切的维度放进排序键是很天然的做法。但这将导致排序键中包括很多的列,而且排序键会伴随着新增加的维度不断的更新。

在这种情况下合理的做法是,只保存少量的列在主键傍边用于提高扫描效率,将维度列增加到排序键中。

对排序键进行 ALTER 是轻量级的操作,因为当一个新列一起被参加到表里和排序键里时,已存在的数据片段并不需求修正。由于旧的排序键是新排序键的前缀,而且新增加的列中没有数据,因而在表修正时的数据关于新旧的排序键来说都是有序的。

索引和分区在查询中的应用

关于 SELECT 查询,ClickHouse 剖析是否能够运用索引。假如 WHERE/PREWHERE 子句具有下面这些表达式(作为完好WHERE条件的一部分或悉数)则能够运用索引:进行持平/不持平的比较;对主键列或分区列进行IN运算、有固定前缀的LIKE运算(如name like ‘test%’)、函数运算(部分函数适用),还有对上述表达式进行逻辑运算

因而,在索引键的一个或多个区间上快速地履行查询是或许的。下面比如中,指定标签;指定标签和日期规模;指定标签和日期;指定多个标签和日期规模等履行查询,都会非常快。

当引擎装备如下时:

ENGINE MergeTree() PARTITION BY toYYYYMM(EventDate) ORDER BY (CounterID, EventDate) SETTINGS index_granularity=8192

这种情况下,这些查询:

SELECT count() FROM table WHERE EventDate = toDate(now()) AND CounterID = 34
SELECT count() FROM table WHERE EventDate = toDate(now()) AND (CounterID = 34 OR CounterID = 42)
SELECT count() FROM table WHERE ((EventDate >= toDate('2014-01-01') AND EventDate <= toDate('2014-01-31')) OR EventDate = toDate('2014-05-01')) AND CounterID IN (101500, 731962, 160656) AND (CounterID = 101500 OR EventDate != toDate('2014-05-01'))

ClickHouse 会根据主键索引剪掉不符合的数据,根据按月分区的分区键剪掉那些不包括符合数据的分区。

上文的查询显现,即便索引证于复杂表达式,因为读表操作经过优化,所以运用索引不会比完好扫描慢。

下面这个比如中,不会运用索引。

SELECT count() FROM table WHERE CounterID = 34 OR URL LIKE '%upyachka%'

要查看 ClickHouse 履行一个查询时能否运用索引,可设置 force_index_by_date 和 force_primary_key 。

运用按月分区的分区列答应只读取包括适当日期区间的数据块,这种情况下,数据块会包括很多天(最多整月)的数据。在块中,数据按主键排序,主键榜首列或许不包括日期。因而,仅运用日期而没有用主键字段作为条件的查询将会导致需求读取超越这个指定日期以外的数据。

函数支撑

WHERE 子句中的条件能够包括对某列数据进行运算的函数表达式,假如列是索引的一部分,ClickHouse会在履行函数时测验运用索引。不同的函数对索引的支撑是不同的。

set 索引会对一切函数生效,其他索引对函数的生效情况见下表

image-20221221165930093

并发数据拜访

关于表的并发拜访,咱们运用多版别机制。换言之,当一张表一起被读和更新时,数据从当时查询到的一组片段中读取。没有冗长的的锁。刺进不会阻碍读取。

对表的读操作是主动并行的。

表 TTL

表能够设置一个用于移除过期行的表达式,以及多个用于在磁盘或卷上主动转移数据片段的表达式。当表中的行过期时,ClickHouse 会删去一切对应的行。关于数据片段的转移特性,有必要一切的行都满意转移条件。

TTL expr
    [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'][, DELETE|TO DISK 'aaa'|TO VOLUME 'bbb'] ...
    [WHERE conditions]
    [GROUP BY key_expr [SET v1 = aggr_func(v1) [, v2 = aggr_func(v2) ...]] ]

TTL 规矩的类型紧跟在每个 TTL 表达式后边,它会影响满意表达式时(抵达指定时刻时)应当履行的操作:

  • DELETE – 删去过期的行(默许操作);
  • TO DISK ‘aaa’ – 将数据片段移动到磁盘 aaa;
  • TO VOLUME ‘bbb’ – 将数据片段移动到卷 bbb.
  • GROUP BY – 聚合过期的行

运用WHERE从句,您能够指定哪些过期的行会被删去或聚合(不适用于移动)。GROUP BY表达式有必要是表主键的前缀。假如某列不是GROUP BY表达式的一部分,也没有在SET从句显现引证,成果行中相应列的值是随机的(就好像运用了any函数)。

示例:

创立时指定 TTL

CREATE TABLE example_table
(
    d DateTime,
    a Int
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d
TTL d + INTERVAL 1 MONTH [DELETE],
    d + INTERVAL 1 WEEK TO VOLUME 'aaa',
    d + INTERVAL 2 WEEK TO DISK 'bbb';

修正表的 TTL

ALTER TABLE example_table
    MODIFY TTL d + INTERVAL 1 DAY;

创立一张表,设置一个月后数据过期,这些过期的行中日期为星期一的删去:

CREATE TABLE table_with_where
(
    d DateTime,
    a Int
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d
TTL d + INTERVAL 1 MONTH DELETE WHERE toDayOfWeek(d) = 1;

创立一张表,设置过期的列会被聚合。列x包括每组行中的最大值,y为最小值,d为或许恣意值。

CREATE TABLE table_for_aggregation
(
    d DateTime,
    k1 Int,
    k2 Int,
    x Int,
    y Int
)
ENGINE = MergeTree
ORDER BY (k1, k2)
TTL d + INTERVAL 1 MONTH GROUP BY k1, k2 SET x = max(x), y = min(y);

删去数据

ClickHouse 在数据片段兼并时会删去掉过期的数据。

当ClickHouse发现数据过期时, 它将会履行一个计划外的兼并。要操控这类兼并的频率, 您能够设置 merge_with_ttl_timeout。假如该值被设置的太低, 它将引发很多计划外的兼并,这或许会耗费很多资源。

假如在两次兼并的时刻距离中履行 SELECT 查询, 则或许会得到过期的数据。为了避免这种情况,能够在 SELECT 之前运用 OPTIMIZE 。