详解 ClickHouse 的表引擎

发布于:2024-06-21 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、简介

  • 表引擎决定了如何存储表的数据。包括:
    • 数据的存储方式和位置,写到哪里以及从哪里读取数据
    • 支持哪些查询以及如何支持
    • 并发数据访问
    • 索引的使用(如果存在)
    • 是否可以执行多线程请求
    • 数据复制参数
  • 表引擎的使用方式:必须显式在创建表时定义该表使用的引擎,以及引擎使用的相关参数。特别注意:表引擎的名称大小写敏感

二、TinyLog

​ Log Farmily 的表引擎之一,以列文件的形式保存在磁盘上,不支持索引,没有并发控制。一般保存少量数据的小表,生产环境上作用有限。可以用于平时练习测试用。

create table t_tinylog 
( 
    id String, 
    name String
) 
engine=TinyLog;

三、Memory

​ 内存引擎,数据以未压缩的原始形式直接保存在内存当中,服务器重启数据就会消失。读写操作不会相互阻塞,不支持索引。简单查询下有非常非常高的性能表现(超过 10G/s)。一般用到它的地方不多,除了用来测试,就是在需要非常高的性能,同时数据量又不太大(上限大概 1 亿行)的场景。

四、MergeTree

重点

​ MergeTree Farmily 的表引擎之一,ClickHouse 中最强大的表引擎系列,支持索引和分区,地位相当于 innodb 之于 Mysql。基于 MergeTree 还衍生出了很多非常有特色的引擎(*MergeTree)

1. 简单使用

  • 创建一个 clickhouse 表并指定表引擎

    create table t_order_mt
    (
    	id UInt32,
        sku_id String,
        total_amount Decimal(16,2),
        create_time Datetime
    )
    engine=MergeTree
    partition by toYYYYMMDD(create_time)
    primary key (id)
    order by (id, sku_id);
    
  • 插入数据

    insert into t_order_mt values
    (101,'sku_001',1000.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 11:00:00'),
    (102,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',12000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',600.00,'2020-06-02 12:00:00');
    

2. 表引擎参数

MergeTree 有很多参数(绝大多数用默认值即可),但是有三个参数是更加重要的

2.1 partition by

分区参数,可选,不指定则表只有一个分区,目录名称为 all

  • 语法:partition by (column)

  • 分区的目的主要是降低扫描的范围,优化查询速度

  • 分区表是以分目录的形式存储数据

    #表在磁盘的目录结构 
    t_order_mt #表目录
    	20200601_1_1_0 
    	20200602_2_2_0
    	detached #卸载目录
    	format_version.txt #版本文件
    
    #分区目录名组成:分区值_最小分区块编号_最大分区块编号_合并层级
    	#分区值:分区值由 partition by 指定的分区字段决定
    		#未指定分区字段:分区值为 all
    		#整型的分区字段:以该字段整型值的字符串形式作为分区值
    		#日期型或可转成日期的分区字段:以该字段值的字符串形式作为分区值
    		#String、Float等类型的分区字段:通过128位的Hash算法取字段的Hash值作为分区值
    	#最小分区块编号:自增,从1开始向上递增,每产生一个分区目录就向上递增一个数字
    	#最大分区块编号:新创建的分区的最大分区块编号=最小分区块编号
    	#合并层级:被合并的次数
    
    #分区目录文件:
    20200601_1_1_0
    	checksums.txt #校验文件,用于检验各个文件的正确性。存放各个文件的size和hash值
    	columns.txt #表的列信息
    	count.txt #表的数据条数
    	data.bin  #数据文件
    	data.mrk3 #标记文件,记录列的偏移量,在idx文件和bin文件之间起到桥梁作用。如果以mrk2开头表示该表启用了自适应索引间隔
    	default_compression_codec.txt #默认的压缩方式
    	minmax_create_time.idx #分区键的最大最小值
    	partition.dat #分区文件
    	primary.idx #主键索引文件,用于加快查询效率
    
  • 分区后,面对涉及跨分区的查询统计,ClickHouse 会以分区为单位并行处理

  • 任何一个批次的数据写入都会产生一个临时分区,不会纳入任何一个已有的分区。写入后的某个时刻(大概 10-15 分钟后),ClickHouse 会自动执行合并操作(等不及也可以手动通过 optimize 执行),把临时分区的数据,合并到已有分区中

    --第一次插入数据生成的分区目录为:
    20200601_1_1_0
    20200602_2_2_0
    
    --再次执行数据插入
    insert into t_order_mt values
    (101,'sku_001',1000.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 11:00:00'),
    (102,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',12000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',600.00,'2020-06-02 12:00:00');
    
    --此时的表分区目录为:
    20200601_1_1_0
    20200601_3_3_0
    20200602_2_2_0
    20200602_4_4_0
    
    --手动执行合并
    optimize table t_order_mt final;
    --optimize table t_order_mt partition '20200601' final; --合并指定分区
    
    --表分区目录变为:
    20200601_1_1_0  --过期数据,后续会被清理
    20200601_1_3_1  --合并后的目录
    20200601_3_3_0
    20200602_2_2_0
    20200602_2_4_1
    20200602_4_4_0
    
2.2 primary key

主键参数,可选

  • 语法:primary key (column1[,column2...])
  • clickhouse 的主键不同于其他数据库,它没有设置唯一约束,因此主键列可以存在相同的值
  • clickhouse 的主键提供了数据的一级索引,采用稀疏索引进行保存,在查询时通过对主键进行某种形式的二分查找,能够定位到对应的 index granularity(索引粒度),避免了全表扫描。
  • clickhouse 中的 MergeTree 默认的索引粒度是 8192。官方不建议修改这个值,除非该列存在大量重复值,比如在一个分区中几万行才有一个不同数据。
  • 稀疏索引的好处就是可以用很少的索引数据,定位更多的数据,代价就是只能定位到索引粒度的第一行,然后再进行扫描。
2.3 order by

排序参数,必须设置

  • 语法:order by (column1[,column2...])
  • order by 设定了分区内的数据按照哪些字段顺序进行有序保存,分区间还是无序的
  • 主键必须是 order by 字段的前缀字段,如 order by 字段是 (id,sku_id),那么主键必须是 id 或者 (id,sku_id)
2.4 二级索引

二级索引能够为非主键字段的查询发挥作用

  • 在 clickhouse v20.1.2.4 版本中二级索引还是实验性的,使用前需要设置,之后的版本则默认开启了

    set allow_experimental_data_skipping_indices=1;
    
  • 语法:

    INDEX index_name column_name TYPE [minmax] GRANULARITY num
    
    --INDEX:指定二级索引的名称和对应的表字段
    --TYPE:指定二级索引的类型,一般为 minmax
    --GRANULARITY:设定二级索引对于一级索引粒度的粒度,即 num 个一级索引的分块组成一个二级索引的分块
    
  • 使用方法:

    --创建带二级索引的表
    create table t_order_mt2
    (
    	id UInt32,
    	sku_id String,
    	total_amount Decimal(16,2),
    	create_time Datetime,
    	INDEX a total_amount TYPE minmax GRANULARITY 5
    ) 
    engine=MergeTree
    partition by toYYYYMMDD(create_time)
    primary key (id)
    order by (id, sku_id);
    
    
    --插入数据
    insert into t_order_mt2 values
    (101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
    (102,'sku_002',2000.00,'2020-06-01 11:00:00'),
    (102,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',12000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',600.00,'2020-06-02 12:00:00');
    
  • 测试二级索引效果

    clickhouse-client --send_logs_level=trace <<< 'select * from t_order_mt2 where total_amount > toDecimal32(900., 2)';
    
2.5 数据TTL

TTL 即 Time To Live,MergeTree 提供了可以管理数据表或者列的生命周期的功能。

  • 列级别 TTL

    --语法:
    column_name1 column_type TTL column_name2 + interval num [SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|QUARTER|YEAR]
    
    --column_name2:不能是主键字段,且字段类型必须为 Date 或者 Datetime 类型,推荐使用分区的日期字段
    
    --1. 建表时设置
    create table t_order_mt3
    (
     	id UInt32,
     	sku_id String,
     	total_amount Decimal(16,2) TTL create_time + interval 10 SECOND,
     	create_time Datetime
    ) 
    engine =MergeTree
    partition by toYYYYMMDD(create_time)
    primary key (id)
    order by (id, sku_id);
     
    --2. 在已有表上设置
    alter table t_order_mt2 modify column total_amount Decimal(16,2) TTL create_time + interval 10 SECOND;
    
    
    --插入数据
    insert into t_order_mt3 values
    (106,'sku_001',1000.00,'2023-09-10 15:02:30'),
    (107,'sku_002',2000.00,'2023-09-10 15:02:40'),
    (110,'sku_003',600.00,'2023-09-10 15:02:50');
    
    --手动合并,查看效果,TTL到期后,指定的字段数据变为该类型默认值 0.00
    optimize table t_order_mt3 final;
    select * from t_order_mt3;
    
  • 表级别 TTL

    --语法:
    TTL column_name + interval num [SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|QUARTER|YEAR]
    [DELETE|TO DISK 'path'|TO VOLUME 'path']
    [WHERE conditions]
    [GROUP BY key_expr [SET v1 = aggr_func(v1),...]]
    
    --column_name:不能是主键字段,且字段类型必须为 Date 或者 Datetime 类型,推荐使用分区的日期字段
    --可以指定过期数据是删除(默认)还是移动到磁盘等
    
    --1. 建表时设置
    create table t_order_mt4
    (
     	id UInt32,
     	sku_id String,
     	total_amount Decimal(16,2),
     	create_time Datetime
    ) 
    engine =MergeTree
    partition by toYYYYMMDD(create_time)
    primary key (id)
    order by (id, sku_id)
    TTL create_time + interval 10 SECOND;
     
    --2. 在已有表上设置
    alter table t_order_mt3 modify TTL create_time + interval 10 SECOND;
    
    
    --插入数据
    insert into t_order_mt4 values
    (106,'sku_001',1000.00,'2023-09-10 15:12:30'),
    (107,'sku_002',2000.00,'2023-09-10 15:12:40'),
    (110,'sku_003',600.00,'2023-09-10 15:12:50');
    
    --手动合并,查看效果,TTL到期后,对应数据行记录被删除
    optimize table t_order_mt4 final;
    select * from t_order_mt4;
    

五、ReplacingMergeTree

​ ReplacingMergeTree 是 MergeTree 引擎的一个变种,它的存储特性完全继承 MergeTree,只是多了一个去重的功能。 但 ReplacingMergeTree 去重能力有限, ReplacingMergeTree 适用于在后台清除重复的数据以节省空间,但是它不保证表中没有重复的数据出现。

1. 功能说明

  • ReplacingMergeTree 是通过 order by 参数指定的字段作为唯一约束进行去重的
  • 去重只能在同一分区的内部进行,不能执行跨分区的去重
  • 去重的时机:a) 在 clickhouse 新版本中同一批次的数据插入时会进行去重;b) 分区进行自动或手动合并操作时会进行去重
  • 去重后数据的保留:a) 当指定了版本字段时,会将版本字段值最大的那条数据保留,若版本字段最大值数据有多条则保留顺序为最后插入的那条数据;b) 没有指定版本字段时则保留顺序为最后插入的那条数据

2. 简单使用

  • 创建一张表并指定引擎为 ReplacingMergeTree

    create table t_order_rmt
    (
    	id UInt32,
    	sku_id String,
    	total_amount Decimal(16,2) ,
    	create_time Datetime
    ) 
    engine=ReplacingMergeTree(create_time)
    partition by toYYYYMMDD(create_time)
    primary key (id)
    order by (id, sku_id);
    
    --(create_time)为指定版本字段,一般建议为日期类型字段,可以确定重复数据的保留规则
    
    
  • 向表中插入数据

    insert into t_order_rmt values
    (101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
    (102,'sku_002',2000.00,'2020-06-01 11:00:00'),
    (102,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',12000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',600.00,'2020-06-02 12:00:00');
    
  • 查询数据

    select * from t_order_rmt;
    
    --在新版本中同一批次数据插入时会进行数据去重,所以总共只有 4 条数据
    --在老版本中则不会进行去重,所以会查询出 6 条数据
    
  • 再次向表中插入数据并查询

    insert into t_order_rmt values
    (101,'sku_001',1000.00,'2020-06-01 12:00:00') ,
    (102,'sku_002',2000.00,'2020-06-01 11:00:00'),
    (102,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',12000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',600.00,'2020-06-02 12:00:00');
    
    select * from t_order_rmt;
    
    --此时分区未合并,会查询出 8 条数据
    
    --执行分区合并
    optimize table t_order_rmt final;
    
    --再次进行查询,重复数据已经去除,查询出 4 条数据
    

六、SummingMergeTree

​ SummingMergeTree 是 MergeTree 引擎的一个变种,它扩展的功能是能够对分区内的同一维度的数据进行预聚合操作,从而减少存储空间的耗费和降低查询时聚合的开销。

1. 功能说明

  • SummingMergeTree 是通过 order by 参数指定的字段作为聚合维度
  • 聚合数据列:a) 可以通过 SummingMergeTree(column1,…) 指定进行聚合的字段,可以指定多个,但所有字段必须是数值类型;b) 未指定聚合字段则会对表中所有非维度的数值类型字段进行预聚合操作
  • 聚合后各字段的保留:维度字段不变,数值字段为汇总后的值,其他字段保留最早插入时的那个值
  • 预聚合只能在同一分区内进行,不能执行跨分区的预聚合
  • 预聚合的时机:a) 在 clickhouse 新版本中同一批次的数据插入时会进行预聚合;b) 分区进行自动或手动合并操作时会进行预聚合
  • 注意:在进行查询获取汇总值时还是需要使用 sum() 聚合函数,因为表中可能会包含一些还没来得及预聚合的临时明细

2. 简单使用

  • 创建一张表并指定引擎为 SummingMergeTree

    create table t_order_smt
    (
    	id UInt32,
    	sku_id String,
    	total_amount Decimal(16,2) ,
    	create_time Datetime
    ) 
    engine=SummingMergeTree(total_amount)
    partition by toYYYYMMDD(create_time)
    primary key (id)
    order by (id,sku_id );
    
    --(total_amount)是指定要进行汇总的字段,可以指定多个
    
  • 插入数据并查询

    insert into t_order_smt values
    (101,'sku_001',1000.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 11:00:00'),
    (102,'sku_004',2500.00,'2020-06-01 12:00:00'),
    (102,'sku_002',2000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',12000.00,'2020-06-01 13:00:00'),
    (102,'sku_002',600.00,'2020-06-02 12:00:00');
    
    select * from t_order_smt;
     
    --新版本同一批次插入时会直接进行汇总,所以查询 20200601 分区内 102,sku_002 维度的 total_amount 字段的值为汇总值 16000.00,create_time 字段值为最早插入的值 2020-06-01 11:00:00
    
  • 再次插入数据并手动合并分区

    insert into t_order_smt values (101,'sku_001',2000.00,'2020-06-01 13:00:00');
    
    select * from t_order_smt; --分区未合并,101,'sku_001' 维度的数据有两条
    
    optimize table t_order_smt final;
    
    select * from t_order_smt;
    
    --分区合并,同一维度数据进行汇总,101,'sku_001' 维度的 total_amount 值为 3000.00,create_time 字段值为 2020-06-01 12:00:00