ClickHouse-之MergeTree引擎分析

ClickHouse-之MergeTree引擎分析

0 前言

clickhouse引擎介绍https://clickhouse.tech/docs/en/engines/table-engines/#mergetree

clickhouse索引什么时候会被使用到https://clickhouse.tech/docs/en/engines/table-engines/mergetree-family/mergetree/#functions-support

想了解更多请参考https://clickhouse.tech/docs/en/engines/table-engines/mergetree-family/mergetree/#mergetree

Hbase的引擎分为2种,表引擎、数据库引擎

  • TableEngine
  • DatabaseEngine

1 Table Engine

TableEngine确定了表的类型,同时决定了是否支持以下属性

  • 数据存储

    • 如何存储
    • 存储在哪
    • 从哪儿读,往哪儿写
  • 支持什么样的查询,如何查询

  • 是否支持数据的并发访问

    • r+r 同时读
    • r+w 同时读与写
    • w+w 同时写
  • 是否支持索引

  • 是否支持多线程的请求(请求并发数)

  • 数据副本的参数

2 Table Engine之 【MergeTree 引擎家族】

MergeTree引擎非常优秀,这类家族的引擎是最通用的也是功能最强大的,这类引擎共享一个属性,支持快速插入数据,并交给后续的数据处理进程处理,同时MergeTree引擎支持副本、分区、二级跨越索引以及一些其它引擎不支持的特性。

  • MergeTree

  • ReplacingMergeTree

  • SummingMergeTree

  • AggregatingMergeTree

  • CollapsingMergeTree

  • VersionedCollapsingMergeTree

  • GraphiteMergeTree

2.1 MergeTree

该引擎能快速插入大量的数据到该引擎的表中,数据是一部分一部分的被写入,这比持续将数据写入存储介质中效率要快好多。

2.1.0 MergeTree引擎主要特性如下

1、数据被写入后会按照primary key自动排序,主要是用台merge的时候用到了归并排序算法。

2、支持partition key的指定,因为clickhouse可以自动以partition为单位对数据进行切分,同样的数据集,分区相比不分区性能更高效。

3、支持数据备份(ReplacingMergeTree)。

4、支持海量数据取样查询。

2.1.1 怎么创建一个MergeTree的表

CREATE TABLE [IF NOT EXISTS] [DB.]table_name [On CLUSTER cluster](
	name1 Type1 [DEFAULT|MATEROALIZED|ALIAS expr ] [TTL expr1],
  ...
  INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1 --GRANULARITY代表索引间隔= value1 * index_granularity,假如1级索引间隔为8096,value=2,那么二级索引的索引间隔就是2*8096行
)ENGINE = MERGETREE()
ORDER BY expr
[PARTITION BY expr]  --分区键
[PRIMARY KEY expr]  --主键
[SMAPLE BY expr]   --取样键
[TTL date + INTERVAL 1 DAY [DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'], ...]
[SETTING name=value,...] --设置参数

2.1.2 MergeTree引擎的数据存储

  • 当数据被插入到一个表中的时候,这个时候数据集已经生成(有点类似于Kafka中的不同的logSegment.log文件),每个数据集part都会按照primary key的16进制形式进行排序;

  • 不同partition的数据被划分到不同的part中,在后台Clickhouse会将同个分区的的parts进行merge操作以便进行更高效的存储,但是这个merge策略不能保证拥有相同primary key的rows存储在相同的part中;

  • 数据part能以紧凑的形式存储,也可以按照宽松的形式进行存储

    • 宽松存储将每个column的数据单独存储在文件系统的一个文件中
    • 紧凑存储将表中的所有column都存储在一个文件中
    • 紧凑存储能被使用在有少量且频繁的插入操作的表
  • 数据存储格式的参数,通过setting指定

    • min_bytes_for_wide_part
    • min_rows_for_wide_part
    • 如果单个数据part的大小或者行数<以上参数值,那么就按照Wide,否则按照Compact紧凑形式存储。
  • 每个数据集part从逻辑上被分成很多个颗粒granules,这个granules是clickhouse可以读取的最小的的数据单位,clickhouse不会将rows和values进行切分,所有每个granules通常包含整数个rows,第一行row会被改行的primary key标记,对于每个column,不管包含在主键与否,clickhouse都会为其存储一份主键标记,这个primary key mark能让我们直接在每个column的存储文件中查找数据。

  • granules的大小由参数严格控制,通过setting指定

    • index_granularity
      • granules的rows会落在[1,index_granularity]区间内,取决于rows的整体大小
    • index_granularity_bytes
      • granules的大小可以超过以上参数的值,但是前提条件这个颗粒只有单个row,且这个row的大小就超过该参数值。

2.1.3 主键和索引在查询中的使用

如果在创建表的时候指定了以下主键

CREATE TABLE .....PRIMARY KEY(COUNTERID,DATE)

那么排序和索引的情况会与下图中描述的一致。

在这里插入图片描述

假如此时的查询语句为:

-- 此时查询处理器将会将范围限定在marks range[0,3]&[6,8]的范围内
SELECT * FROM TABLE_NAME WHERE CounterId in('a','h');
-- 此时查询处理器将会将范围限定在marks range[1,3]&[7,8]的范围内
SELECT * FROM TABLE_NAME WHERE CounterId in('a','h') AND Date = 3;
--如果只用这个,那么会将查询范围限定在[1,10]范围内
SELECT * FROM TABLE_NAME WHERE Date = 3;

简而言之,使用索引往往比全表扫描要快太多!!~

由于索引是基于granules,且只有每个granules第一行会标记primary key,所以Clickhouse的索引属于稀疏索引,这样每次在查询数据的时候可能会查询出额外的多余数据,这个额外的数据行数最大可达到index_granularity * 2

稀疏索引占用内存很少,它可以让我们工作在很大的数据集上,因为这些索引会Clickhouse加载到内存中。

primary key也可以为Nullable类型,只要打开allow_nullable_key参数,我们还可以在排序的时候将Null值排列在列表的最前或者最后。

--查询案例
SELECT * FROM t_null_nan ORDER BY y NULLS FIRST | LAST ;
┌─x─┬─s────┐
│ 1 │ ᴺᵁᴸᴸ │
│ 2 │ ᴺᵁᴸᴸ │
│ 3 │ ABC  │
│ 4123a │
│ 5 │ abc  │
│ 6 │ bca  │
│ 7 │ BCA  │
└───┴──────┘

2.1.4 主键(primary key)的性能优化

主键中column的数量没有明确的限制,我们可以根据不同的数据结构,我们可以动态在primary key中添加或减少column的数量,从而进行性能调优。

  • 提升索引的性能表现
    • 如果初始primary key 是(a,b),此时我们查询需要用到c,此时将c添加到primary key(a,b,c),能缩小range范围;
    • 如果查询范围很大,也可以将c加进primary key
  • 提升压缩比
    • 因为clickhouse按照primary key进行排序,将c加到索引中,使数据更加有连贯性,压缩性能更好
  • CollapsingMergeTreeSummingMergeTree引擎中合并数据部分时提供其他逻辑。
    • 在这种情况下指定除了primary key之外的排序键是很有意义的

NOTE:一个很长的主键会影响插入的速度和内存的消耗,但是在primary key中添加额外的列不影响查询SELECT。

REMEMBER:如果你不想让数据按照主键进行排序,你可以在创建表的时候不创建主键,指定ORDER BY tuple(),这样clickhouse能保证单个线程的插入有序,如果你想按照插入的数据全排序,你可以将setting max_insert_thread = 1

2.1.5 选择一个与sorting key不相同的primary key

我们可以将primary key设置成与sorting key不同的样式.

NOTE:当我们使用SummingMergeTree and AggregatingMergeTree table engines的时候,可以将primary key 设置成 sorting key的一个tuple前缀,会很有帮助,具体设置如下

primary_key(a,b,c)
sorting key(a,b,c,d,e...)

-- 当使用这任何引擎的时候,所有的列会被分成2个类别,dimensions and measures,通常GROUP BY measures,而聚合使用dimensions,如下SQL

SELECT 
...
WHERE A =GROUP BY B --这里B是measures,A是dimensions

因为这2种引擎会将相同sorting key的所有rows进行聚合,这样就很自然的向其(sorting key)添加所有dimensions,最终的结果是sorting key由一长串的columns列表组成,而且这个列表得在有新的维度添加进来的时候,经常性的进行更新。

ALTER sorting key是一个轻量化的操作因为在一个表中新增一个列的时候,同时它也会被添加到sorting key,已存在的数据parts不需要改变,因为他们的sorting key是新的sorting key的前缀,而新增的列是没有插入数据的,所以不需要对数据进行重新排序(因为排序前后的效果是一样的)。

2.1.6 在查询中使用index和partition

在SELECT查询中,clickhouse会先解析是否用到了index,一个索引是否被用到,那就要看其是否被用在WHERE / PREWHERE上,或者使用在IN、LIKE,或者在index columns上使用了函数,或者说用到了这些index的逻辑关系。

查看以下引擎的几种查询情况

CREATE TABLE table(
 ......
)ENGINE MergeTree() 
PARTITION BY toYYYYMM(EventDate) --
ORDER BY (CounterID, EventDate) 
SETTINGS index_granularity=8192

--以下3种都会用到索引
--clickhouse会首先通过主键索引排除不相关的数据,通过partition_key排除不相关的partition
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'))

--以下情况用不到索引
SELECT count() FROM table WHERE CounterID = 34 OR URL LIKE '%upyachka%'

如果需要检查ClickHouse是否会在查询的时候用到索引,可以在创建表的时候指定以下参数。

  • force_index_by_date

  • force_primary_key

2.1.7 skiping data indexes使用

这种跳跃式的索引是二级索引,通常是在创建表的column栏目进行指定。这种索引在MergeTree家族中才能指定。

CREATE TABLE (
  a UInt8,
	INDEX index_name expr TYPE type(...) GRANULARITY granularity_value --
)ENGINE=MergeTree();

NOTE:数据跳跃的指标将会基于数据块聚合一些信息,这些指标由granularity_value组成,这实际上就是一个granules的数据行数,这些聚合信息将会被用在SELECT语句中,用来过滤掉WHERE条件不满足的大量数据块,较少扫描的基数,提高查询性能。

二级索引使用样例

CREATE TABLE table_name
(
    u64 UInt64,
    i32 Int32,
    s String,
    ...
    INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3, 
    INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4
) ENGINE = MergeTree()
...

SELECT count() FROM table WHERE s < 'z'
SELECT count() FROM table WHERE u64 * i32 == 10 AND u64 * length(s) >= 1234

在设置二级索引的时候,我们需要指定一些指标,用来过滤不相关的数据集。

  • minmax
    • 存储表达式的极端值,如果对象是tuple,那么就存储器中所有元素的极端值,然后与primary key一样排除文件系统上不相关的数据块。
  • set(max_rows)
    • 存储指定表达式的唯一值,max_rows=0说明没有限制,在每个数据块上使用该值校验是否与where表达式匹配。
  • ngrambf_v1(n, size_of_bloom_filter_in_bytes,number_of_hash_function,random_seed)
    • works only with string类型
    • 存储一个布隆过滤器,包含所有block的所有语法
    • 可以被使用在equals\like\in的表达式优化
      • n : 表示式的大小
      • size_of_bloom_filter_in_bytes: 布隆过滤器的大小,256或者512,它会被压缩
      • number_of_hash_function :布隆过滤器中使用的函数个数
      • random_seed:布隆过滤器函数的随机因子
  • tokenbf_v1(size_of_bloom_filter_in_bytes,number_of_hash_functions,random_seed)
    • 同上,但是存储tokens而不是ngrams
    • tokens是通过非数字类型分割的序列化数据。
  • Bloom_filter([false_positive])
    • 支持老多类型了
    • false_positive默认=0.025,这是从过滤器接受正负的概率,范围(0,1)

2.1.8 mergetree的table并发访问

  • merge tree引擎支持并行同时查询同一条数据 r+r
  • merge tree支持同时读写 r+w
    • 采用MVC(mutil version control),只会查query时间当前版本的数据,插入操作不影响读操作。

2.1.9 TTL的支持

  • MergeTree支持Table、Column的TTL。
  • Table的TTL可以用于任意的表
    • TTL的表达式必须使用Date或者DateTime格式
--TABLE 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';
  • Column的TTL可以针对所有个人创建的Column
--Column TTL具体使用方式
CREATE TABLE example_table
(
    d DateTime,
    a Int TTL d + INTERVAL 1 MONTH,
    b Int TTL d + INTERVAL 1 MONTH,
    c String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(d)
ORDER BY d;
-----修改
ALTER TABLE example_table 
MODIFY COLUMN c String TTL d + INTERVAL 1 DAY; 
  • TTL的过期标记数据将会在Clickhouse对parts进行merge的时候被清除,有点类似于Hbase中的delete的墓碑,在compaction的时候被清除。

2.1.10 MergeTree 分块设备存储冷、热数据

mergetree引擎支持将数据存储在多个块设备上,当你知道一个表的数据少量数据被经常访问而大量的数据很少访问,这就是冷热数据,所以尽量将热数据存放在SSD或者内存中,其它的冷数据存放在HDD,如果你公司不缺钱,那么这个就无关紧要了,这个块设备相关的术语如下。

  • Disk - 挂载在文件系统上的块设备
  • Default Disk - 在<path>标签中指定的数据存储路径
  • Volumn - 有序磁盘组,由1到N歌Disk组成
  • Storage policy - 设置Volumns中数据相互移动的规则策略

Storage policy与Disk相关的配置可以参数可以参考以下链接

Disk、Volumn、Storage policy的策略应该包含在\<storage_configuration>标签中,可以在config.xml或者config.d的xml文件中配置。

<!----------------------------------------挂载磁盘分配---------------------------------------------------->
<storage_configuration>
    <disks>
        <disk_name_1> <!-- disk name ,所有的disk name必须不同,这是存储磁盘目录的分辨标识-->
            <path>/mnt/fast_ssd/clickhouse/</path> <!---->
        </disk_name_1>
        <disk_name_2>
            <path>/mnt/hdd1/clickhouse/</path>
            <keep_free_space_bytes>10485760</keep_free_space_bytes>
        </disk_name_2>
        <disk_name_3>
            <path>/mnt/hdd2/clickhouse/</path>
            <keep_free_space_bytes>10485760</keep_free_space_bytes>
        </disk_name_3>

        ...
    </disks>

    ...
</storage_configuration>
<!-------------------------------------storage policy and volumns ---------------------------------------->
<storage_configuration>
    ...
    <policies>
        <policy_name_1>
            <volumes>
                <volume_name_1>
                    <disk>disk_name_from_disks_configuration</disk>
                    <max_data_part_size_bytes>1073741824</max_data_part_size_bytes>
                </volume_name_1>
                <volume_name_2>
                    <!-- configuration -->
                </volume_name_2>
                <!-- more volumes -->
            </volumes>
            <move_factor>0.2</move_factor>
        </policy_name_1>
        <policy_name_2>
            <!-- configuration -->
        </policy_name_2>

        <!-- more policies -->
    </policies>
    ...
</storage_configuration>

<!---具体案例--->
<storage_configuration>
    ...
    <policies>
        <hdd_in_order> <!-- policy name -->
            <volumes>
                <single> <!-- volume name,这里冷数据的移动采用round robin的轮询策略 -->
                    <disk>disk1</disk>
                    <disk>disk2</disk>
                </single>
            </volumes>
        </hdd_in_order>
				<!--将冷数据从SSD到HDD的配置-->
        <moving_from_ssd_to_hdd>
            <volumes>
                <hot>
                    <disk>fast_ssd</disk> <!--存储热数据的磁盘-->
                    <max_data_part_size_bytes>1073741824</max_data_part_size_bytes> <!--1G,当part数据达到1G直接进入coldHDD-->
                </hot>
                <cold>
                    <disk>disk1</disk>
                </cold>
            </volumes>
            <move_factor>0.2</move_factor>  <!--一旦fast_ssd的磁盘占用达到80%,那么经过后台线程池的线程将数据往disk1中转移-->

        </moving_from_ssd_to_hdd>
				
        <small_jbod_with_external_no_merges>
            <volumes>
                <main>
                    <disk>jbod1</disk>
                </main>
                <external>
                    <disk>external</disk>
                    <prefer_not_to_merge>true</prefer_not_to_merge>
                </external>
            </volumes>
        </small_jbod_with_external_no_merges>
    </policies>
    ...
</storage_configuration>

创建表的时候使用存储策略

CREATE TABLE table_with_non_default_policy (
    EventDate Date,
    OrderID UInt64,
    BannerID UInt64,
    SearchPhrase String
) ENGINE = MergeTree
ORDER BY (OrderID, BannerID)
PARTITION BY toYYYYMM(EventDate)
SETTINGS storage_policy = 'moving_from_ssd_to_hdd' --指定配置文件中的策略标签,一旦表创建之后,存储策略不能被改变,默认是存在一个Volumn的多<path>目录中

--如果我们想要指定后台移动数据的线程个数,我们可以通过`background_move_pool_size`参数进行指定,默认=8

2.1.11 数据进入磁盘的方式

  • INSERT语句
  • 后台Merge数据或者MUTATIONS(就是定义语言,如ALTER TABLE …update|delete)
  • 从其它副本下载数据
  • ALTER TABLE …FREEZE PARTITION的时候

除了ALTERFREEZE PARTITION,其它的方式中,每个part都会存储在一个Volumn的一个Disk中,存储策略为:

  1. The first volume (in the order of definition) that has enough disk space for storing a part (unreserved_space > current_part_size) and allows for storing parts of a given size (max_data_part_size_bytes > current_part_size) is chosen.

    按定义顺序,第一个满足以上2个条件的Volumn将被选择存储该Part,然后给该Part找到格式的Disk。

  2. Within this volume, that disk is chosen that follows the one, which was used for storing the previous chunk of data, and that has free space more than the part size (unreserved_space - keep_free_space_bytes > current_part_size).

    在选出的这个Volumn中,将会选择第一个存储之前的数据的Disk后面一个满足以上条件的Disk存储这个Part

用户可以强制将数据从一个Volumn移动到其它的Volumn,只需要使用 ALTER TABLE … MOVE PART|PARTITION … TO VOLUME|DISK …查询语句,这个查询语句会将后台移动操作的所有限制考虑进来,它自己会初始化一个动作,这个动作不需要等后台操作完成,如果移动的target空间不足,或者任意条件不满足,User都会获取到一个Error,大家可以试一试。

NOTE:数据的移动不影响副本,可以针对一个表的不同副本采用不同的存储策略。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值