Clickhouse基础知识

Clickhouse基础知识

本文参考以下博文:

https://clickhouse.tech/docs/zh/ 【clickhouse中文社区文档】

https://www.jianshu.com/p/5f7809b1965e

https://blog.csdn.net/jmx_bigdata/article/details/108568278

https://blog.csdn.net/jmx_bigdata/article/details/108719569

一.Clickhouse简介

Clickhouse 是一个开源的面向联机分析处理(OLAP, On-Line Analytical Processing)的列式存储数据库管理系统。

优点缺点
写入快、查询快不支持事务
SQL 支持不适合典型的 K/V 存储
简单方便,不依赖 Hadoop 技术栈不适合 Blob/Document 存储
支持线性扩展不支持完整的 Update/Delete 操作
深度列存储非跨平台
向量化查询执行并非查询资源控制不好处理
数据压缩不支持二级索引
并行和分布式查询
实时数据更新

1. ClickHouse的大数据处理架构优点

ClickHouse的大数据处理架构典型的大数据处理架构
链路简单数据处理流程复杂,链路多
数据准实时入库Spark/Flink占用资源多
清单数据随用随查基于原始数据的即席查询延迟大
基于原始数据的统计分析效率高清单数据查询成本高
硬件投资少硬件投资多
维护简单维护成本高

2. ClickHouse与Hbase、Elasticsearch对比

资源占用

  • Hbase依赖hadoop,占用资源一般。
  • Elasticsearch基于内存,占用资源多。
  • ClickHouse占用资源少,约为Elasticsearch的5%-10%。

清单查询

  • Hbase适用于rowkey返回少量数据的查询,非rowkey字段需全表扫描。
  • Elasticsearch支持索引字段查询,速度快。
  • ClickHouse支持针对所有字段的清单查询,速度快。建议使用主键索引查询,节约资源。

数据分析

  • Hbase不适合用于分析的数据源。
  • Elasticsearch分析功能有限,不能多表关联。
  • ClickHouse适合OLAP分析,速度快。

查询语法

  • Hbase专用API,默认不支持SQL。

  • Elasticsearch专用DSL,有限的SQL支持。

  • ClickHouse支持复杂SQL。

读写阻塞

  • Hbase高并发读写容易产生region热点。
  • Elasticsearch高并发读写容易产生热点。
  • ClickHouse读写相互不影响。

可维护性

  • Hbase维护复杂,元数据损坏可能导致数据不可用。
  • Elasticsearch维护一般。
  • ClickHouse易于扩展,数据容易恢复。

硬件成本

  • Hbase依赖Hadoop生态。

  • ElasticsearchCPU密集型, 单节点内存不超过64GB,超出浪费。

  • ClickHouseCPU密集型,能充分利用主机资源。

集群架构

  • Hbase和Elasticsearch为主从架构。
  • ClickHouse为多主架构,完全自主实现高可用。

二.单机版安装

1. 系统检查

# 查看 Linux 版本
[root@ck01 ~]# cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)

# 检查系统是否支持clickhouse安装
[root@ck01 ~]# grep -q sse4_2 /proc/cpuinfo && echo “SSE 4.2 supported” || echo “SSE 4.2 not supported”
“SSE 4.2 supported”

若显示为SSE4.2suported 则可以继续安装。

2. 下载与安装

第三方机构 Altinity 提供了完整的 rpm 包,支持在 Centos 下安装。
网址: https://packagecloud.io/Altinity/clickhouse
以下 4 个 rpm 包即可:

mkdir -p /baicdt/softwares/ck
cd /baicdt/softwares/ck/

wget --content-disposition https://packagecloud.io/Altinity/clickhouse/packages/el/7/clickhouse-server-common-20.6.6.7-1.el7.x86_64.rpm/download.rpm

wget --content-disposition https://packagecloud.io/Altinity/clickhouse/packages/el/7/clickhouse-server-20.6.6.7-1.el7.x86_64.rpm/download.rpm

wget --content-disposition https://packagecloud.io/Altinity/clickhouse/packages/el/7/clickhouse-common-static-20.6.6.7-1.el7.x86_64.rpm/download.rpm

wget --content-disposition https://packagecloud.io/Altinity/clickhouse/packages/el/7/clickhouse-client-20.6.6.7-1.el7.x86_64.rpm/download.rpm

#安装
rpm -ivh *.rpm
  1. 安装完后默认的配置文件是/etc/clickhouse-server/config.xml,其中默认的数据目录、临时目录、日志目录为:
    • /var/lib/clickhouse 默认数据目录
    • /var/lib/clickhouse/tmp/ 默认临时目录
    • /var/log/clickhouse-server 默认日志目录
  2. 服务端启动脚本:/etc/init.d/clickhouse-server

3. 启动与验证

# 启动 clickhouse-server
systemctl start clickhouse-server

# 查看 clickhouse-server 状态
systemctl status clickhouse-server

# 开机启动 clickhouse-server 状态
systemctl enable clickhouse-server

# 进入clickhouse客户端: 命令行clickhouse-client –h host –u –p
clickhouse-client

三.集群安装

IP主机名
192.168.52.210ck01.hadoop.com
192.168.52.220ck02.hadoop.com
192.168.52.230ck03.hadoop.com

    ClickHouse集群安装非常简单,首先重复上面步骤,分别在其他机器上安装ClickHouse,然后再分别配置一下/etc/clickhouse-server/config.xml/etc/metrika.xml两个文件即可。注意:ClickHouse集群的依赖于Zookeeper,所以要保证先安装好Zookeeper集群。本文演示三个节点的ClickHouse集群安装,具体步骤如下:

1. 三台服务器上进行单机的部署

2. 创建metrika.xml文件

# 每台服务器都要创建
vim /etc/metrika.xml
<yandex>
	<!-- 该标签与config.xml的<remote_servers incl="clickhouse_remote_servers" >保持一致--> 
	<clickhouse_remote_servers>
		<!-- 自定义的标签名,也就是集群名称,可以修改 -->
		<perftest_3shards_1replicas>
			<!-- 配置三个分片,每个分片对应一台机器-->
			<!-- 数据分片1  -->
			<shard>
				<!-- 定义数据分片的数据权重。比如有两个分片,第一个权重为9,第二个权重为10,
				则该批次的行将会有9/19的数据被发送到第一个分片,10/19的数据被发送到第二分片。-->
				<weight>1</weight>
				<!-- 是否启用内部复制。true 代表写入数据时选择第一个健康的副本进行写入,其余
				副本以该表本身进行复制,保证复制表的一致性。false(默认) 代表将数据直接写入所
				有副本,因为没有检查复制表的一致性,而且随着时间的推移,它们将包含略微不同的
				数据。 -->
				<internal_replication>true</internal_replication>
				<!-- 指定分片数据副本,可为每个服务器指定参数主机、端口和可选的用户、密码、安全、压缩等 -->
				<replica>
					<!-- 数据分片远程服务地址(支持IPv6 )。如果指定了域名(domain),
					服务器在启动时发出DNS请求并且只要服务器在运行就能一直保存结果。如
					果DNS请求失败,服务器不会启动。如果更改DNS记录,请重新启动服务器。 -->
					<host>192.168.52.210</host>
					<!-- TCP端口(对应config.xml的"tcp_port",通常设置为9000)。不要将其与http_port混淆。 -->
					<port>9000</port>
				</replica>
			</shard>
			<!-- 数据分片2  -->
			<shard>
				<replica>
					<internal_replication>true</internal_replication>
					<host>192.168.52.220</host>
					<port>9000</port>
				</replica>
			</shard>
			<!-- 数据分片3  -->
			<shard>
				<internal_replication>true</internal_replication>
				<replica>
					<host>192.168.52.230</host>
					<port>9000</port>
				</replica>
			</shard>
		</perftest_3shards_1replicas>
	</clickhouse_remote_servers>

	<!-- zookeeper相关配置: 该标签与config.xml的 <zookeeper incl="zookeeper-servers" optional="true" /> 保持一致 -->
	<zookeeper-servers>
		<node index="1">
			<host>192.168.52.210</host>
			<port>2181</port>
		</node>
		<node index="2">
			<host>192.168.52.220</host>
			<port>2181</port>
		</node>
		<node index="3">
			<host>192.168.52.230</host>
			<port>2181</port>
		</node>
	</zookeeper-servers>

	<!-- 分片和副本标识: shard标签配置分片编号; <replica>配置分片副本主机名(需要修改对应主机上的配置)-->
	<!-- 宏定义,子标签有:
	         1、{layer} - ClickHouse集群的昵称,用于区分不同集群之间的数据。
			 2、{shard} - 分片编号或符号引用。
			 3、{replica} - 副本的名称(唯一),通常与主机名匹配macros为可选定义。
		配置文件中定义了在创建表时每台服务器就可以使用相同的建表DDL。否则要ReplicatedMergeTree指定zk路径和replica值。 -->
	<macros>
		<shard>01</shard>
		<replica>192.168.52.210</replica>
	</macros>
	
	<!-- 监听网络,::/0代表监听所有ip -->
	<networks>
		<ip>::/0</ip>
	</networks>
	
	<!-- 数据压缩算法:
			 ClickHouse检查{min_part_size}和{min_part_size_ratio},并处理与这些条件匹配的{case}块。
			 如果<case>不匹配,ClickHouse应用lz4压缩算法。{method}指压缩方法,可接受值:lz4或zstd(实验值)。
	-->
	<clickhouse_compression>
		<case>
			<min_part_size>10000000000</min_part_size>
			<min_part_size_ratio>0.01</min_part_size_ratio>
			<method>lz4</method>
		</case>
	</clickhouse_compression>

</yandex>

配置说明来自 https://www.jianshu.com/p/5f7809b1965e

ck02服务器macros标签属性

	<macros>
		<shard>02</shard>
		<replica>192.168.52.220</replica>
	</macros>

ck03服务器macros标签属性

	<macros>
		<shard>03</shard>
		<replica>192.168.52.230</replica>
	</macros>

3. 修改config.xml配置

/etc/clickhouse-server/config.xml中放开远程主机监听,每台服务器都要修改

    <listen_host>::1</listen_host>
    <listen_host>0.0.0.0</listen_host>

遇到问题:

登录时若报异常Code: 210. DB::NetException: Connection refused (localhost:9000, ::1)
解决:需要开放<listen_host>::1</listen_host>。另外zookeeper集群没有正常启动也会造成建表时报此类错误。

4. 重启与验证

# 每台服务器重启clickhouse-server
systemctl restart clickhouse-server
# 查看 clickhouse-server 状态
systemctl status clickhouse-server
# 启动客户端,-m参数支持多行输入
clickhouse-client -m

可以查询系统表验证集群配置是否已被加载:

ck01.hadoop.com :) select cluster,shard_num,replica_num,host_name,port,user from system.clusters;
结果如下:
┌─cluster───────────────────────────┬─shard_num─┬─replica_num─┬─host_name──────┬─port─┬─user────┐
│ perftest_3shards_1replicas        │         11192.168.52.2109000default │
│ perftest_3shards_1replicas        │         21192.168.52.2209000default │
│ perftest_3shards_1replicas        │         31192.168.52.2309000default │
│ test_cluster_two_shards           │         11127.0.0.19000default │
│ test_cluster_two_shards           │         21127.0.0.29000default │
│ test_cluster_two_shards_localhost │         11 │ localhost      │ 9000default │
│ test_cluster_two_shards_localhost │         21 │ localhost      │ 9000default │
│ test_shard_localhost              │         11 │ localhost      │ 9000default │
│ test_shard_localhost_secure       │         11 │ localhost      │ 9440default │
│ test_unavailable_shard            │         11 │ localhost      │ 9000default │
│ test_unavailable_shard            │         21 │ localhost      │    1default │
└───────────────────────────────────┴───────────┴─────────────┴────────────────┴──────┴─────────┘

查看集群的分片信息(宏变量):分别在各自机器上执行下面命令:

ck01.hadoop.com :) select * from system.macros;
┌─macro───┬─substitution───┐
│ replica │ 192.168.52.210 │
│ shard   │ 01             │
└─────────┴────────────────┘

5. 分布式DDL入门操作

    在分布式集群中,我们通常需要先创建本地表(又叫:分片表、复制表)再创建分布式表。因为分布式表只是作为一个查询引擎,本身不存储任何数据,查询时将sql发送到所有集群分片,然后进行进行处理和聚合后将结果返回给客户端。创建什么样的表,需要根据实际的使用场景决定在创建表指定什么样的表引擎

(1)创建本地表了

CREATE TABLE IF NOT EXISTS user_local 
(
    id Int32,
    name String
)ENGINE = MergeTree()
ORDER BY id
PARTITION BY id
PRIMARY KEY id;

我们先在一台机器上,对user_local表进行插入数据,然后再查询user_cluster

(2)创建分布式表

    默认情况下,CREATE、DROP、ALTER、RENAME操作仅仅在当前执行该命令的server上生效。在集群环境下,可以使用ON CLUSTER语法,这样就可以在整个集群发挥作用。

比如创建一张分布式表:

CREATE TABLE IF NOT EXISTS user_cluster ON CLUSTER perftest_3shards_1replicas
(
    id Int32,
    name String
)ENGINE = Distributed(perftest_3shards_1replicas, default, user_local,id);

表引擎的定义规则:

Distributed(cluster_name, database_name, table_name, [sharding_key])

  • cluster_name:集群名称,与集群配置中的自定义名称相对应。
  • database_name:数据库名称
  • table_name:本地表名称
  • sharding_key:用于分片的key值,在数据写入的过程中,分布式表会依据分片key的规则,将数据分布到各个节点的本地表。可选。

注意:

    创建分布式表是读时检查的机制,对创建分布式表和本地表的先后顺序没有强制要求

    同样值得注意的是,在上面的语句中使用了ON CLUSTER分布式DDL,这意味着在集群的每个分片节点上,都会创建一张Distributed表,这样便可以从其中任意一端发起对所有分片的读、写请求。

(3)测试:插入|查询

-- 插入数据
ck03.hadoop.com :) INSERT INTO user_local VALUES(1,'tom'),(2,'jack');

-- 任意节点查询user_cluster表,可查询到插入的数据
ck03.hadoop.com :) select * from user_cluster;
┌─id─┬─name─┐
│  1 │ tom  │
└────┴──────┘
┌─id─┬─name─┐
│  2 │ jack │
└────┴──────┘

-- 其他服务器查询分布式表
ck02.hadoop.com :) select * from user_cluster;
┌─id─┬─name─┐
│  2 │ jack │
└────┴──────┘
┌─id─┬─name─┐
│  1 │ tom  │
└────┴──────┘

然后,我们再向user_cluster中插入一些数据,观察user_local表数据变化,可以发现数据被分散存储到了其他节点上了。

-- 向user_cluster插入数据
ck03.hadoop.com :) INSERT INTO user_cluster VALUES(3,'lilei'),(4,'lihua'); 
-- 个服务器查看user_local数据,会发现新插入的数据分散到了其他节点
ck03.hadoop.com :) select * from user_local;
┌─id─┬─name─┐
│  1 │ tom  │
└────┴──────┘
┌─id─┬─name─┐
│  2 │ jack │
└────┴──────┘

四.Clickhouse表引擎

1. 表引擎的作用

  • 决定表存储在哪里以及以何种方式存储
  • 支持哪些查询以及如何支持
  • 并发数据访问
  • 索引的使用
  • 是否可以执行多线程请求
  • 数据复制参数

2. 表引擎分类

引擎分类引擎名称
MergeTree系列MergeTree 、ReplacingMergeTree 、SummingMergeTree 、 AggregatingMergeTree CollapsingMergeTree 、 VersionedCollapsingMergeTree 、GraphiteMergeTree
Log系列TinyLog 、StripeLog 、Log
Integration EnginesKafka 、MySQL、ODBC 、JDBC、HDFS
Special EnginesDistributed 、MaterializedView、 Dictionary 、Merge 、File、Null 、Set 、Join 、 URL View、Memory 、 Buffer

3. Log系列表引擎

应用场景

    Log系列表引擎功能相对简单,主要用于快速写入小表(1百万行左右的表),然后全部读出的场景。即一次写入多次查询。

Log系列表引擎的特点
  • 共性特点

    1. 数据存储在磁盘上
    2. 当写数据时,将数据追加到文件的末尾
    3. 不支持并发读写,当向表中写入数据时,针对这张表的查询会被阻塞,直至写入动作结束
    4. 不支持索引
    5. 不支持原子写:如果某些操作(异常的服务器关闭)中断了写操作,则可能会获得带有损坏数据的表
    6. 不支持ALTER操作(这些操作会修改表设置或数据,比如delete、update等等)
  • 区别

    1. TinyLog是Log系列引擎中功能简单、性能较低的引擎。它的存储结构由数据文件和元数据两部分组成。其中,数据文件是按列独立存储的,也就是说每一个列字段都对应一个文件。除此之外,TinyLog不支持并发数据读取。
    2. StripLog支持并发读取数据文件,当读取数据时,ClickHouse会使用多线程进行读取,每个线程处理一个单独的数据块。另外,StripLog将所有列数据存储在同一个文件中,减少了文件的使用数量。
    3. Log支持并发读取数据文件,当读取数据时,ClickHouse会使用多线程进行读取,每个线程处理一个单独的数据块。Log引擎会将每个列数据单独存储在一个独立文件中。
TinyLog表引擎使用

    适用于一次写入,多次读取的场景对于处理小批数据的中间表可以使用该引擎。值得注意的是,使用大量的小表存储数据,性能会很低。

-- 建表
CREATE TABLE emp_tinylog (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
  )ENGINE=TinyLog();

-- 插入数据
INSERT INTO emp_tinylog 
VALUES (1,'tom','上海',25,'技术部',20000),(2,'jack','上海',26,'人事部',10000);
INSERT INTO emp_tinylog
VALUES (3,'bob','北京',33,'财务部',50000),(4,'tony','杭州',28,'销售事部',50000);

-- 查询数据
ck01.hadoop.com :) SELECT * FROM emp_tinylog
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部   │ 20000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部   │ 10000.00 │
│      3 │ bob  │ 北京       │  33 │ 财务部   │ 50000.00 │
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘

进入默认数据存储目录,查看底层数据存储形式,可以看出:TinyLog引擎表每一列都对应的文件

[root@ck01 ~]# cd /var/lib/clickhouse/data/default/emp_tinylog
[root@ck01 emp_tinylog]# ll -ah
total 28K
drwxr-x--- 2 clickhouse clickhouse 131 Sep 24 10:56 .
drwxr-x--- 5 clickhouse clickhouse  63 Sep 24 10:56 ..
-rw-r----- 1 clickhouse clickhouse  56 Sep 24 10:56 age.bin
-rw-r----- 1 clickhouse clickhouse  97 Sep 24 10:56 depart.bin
-rw-r----- 1 clickhouse clickhouse  60 Sep 24 10:56 emp_id.bin
-rw-r----- 1 clickhouse clickhouse  70 Sep 24 10:56 name.bin
-rw-r----- 1 clickhouse clickhouse  68 Sep 24 10:56 salary.bin
-rw-r----- 1 clickhouse clickhouse 185 Sep 24 10:56 sizes.json
-rw-r----- 1 clickhouse clickhouse  80 Sep 24 10:56 work_place.bin
  • 注:sizes.json文件内使用JSON格式记录了每个.bin文件内对应的数据大小的信息

当我们执行ALTER操作时会报错,说明该表引擎不支持ALTER操作

-- 以下操作会报错:
-- DB::Exception: Mutations are not supported by storage TinyLog.
ALTER TABLE emp_tinylog DELETE WHERE emp_id = 5;
ALTER TABLE emp_tinylog UPDATE age = 30 WHERE emp_id = 4;
StripLog表引擎使用

    相比TinyLog而言,StripeLog拥有更高的查询性能(拥有.mrk标记文件,支持并行查询),同时其使用了更少的文件描述符(所有数据使用同一个文件保存)。

-- 建表
CREATE TABLE emp_stripelog (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
  )ENGINE=StripeLog;
-- 插入数据  
INSERT INTO emp_stripelog
VALUES (1,'tom','上海',25,'技术部',20000),(2,'jack','上海',26,'人事部',10000);
INSERT INTO emp_stripelog 
VALUES (3,'bob','北京',33,'财务部',50000),(4,'tony','杭州',28,'销售事部',50000);

-- 查询数据,由于是分两次插入数据,所以查询时会有两个数据块
ck01.hadoop.com :) select * from emp_stripelog;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      3 │ bob  │ 北京       │  33 │ 财务部   │ 50000.00 │
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘

进入默认数据存储目录,查看底层数据存储形式

[root@ck01 emp_tinylog]# cd /var/lib/clickhouse/data/default/emp_stripelog
[root@ck01 emp_stripelog]# ll -h
total 12K
-rw-r----- 1 clickhouse clickhouse 673 Sep 24 11:05 data.bin
-rw-r----- 1 clickhouse clickhouse 281 Sep 24 11:05 index.mrk
-rw-r----- 1 clickhouse clickhouse  69 Sep 24 11:05 sizes.json

可以看出StripeLog表引擎对应的存储结构包括三个文件:

  • data.bin:数据文件,所有的列字段使用同一个文件保存,它们的数据都会被写入data.bin
  • index.mrk:数据标记,保存了数据在data.bin文件中的位置信息(每个插入数据块对应列的offset),利用数据标记能够使用多个线程,以并行的方式读取data.bin内的压缩数据块,从而提升数据查询的性能。
  • sizes.json:元数据文件,记录了data.binindex.mrk大小的信息

注意:

    StripeLog引擎将所有数据都存储在了一个文件中,对于每次的INSERT操作,ClickHouse会将数据块追加到表文件的末尾,StripeLog引擎同样不支持ALTER UPDATEALTER DELETE 操作。

Log表引擎使用

    Log引擎表适用于临时数据,一次性写入、测试场景。Log引擎结合了TinyLog表引擎和StripeLog表引擎的长处,是Log系列引擎中性能最高的表引擎。

-- 建表
CREATE TABLE emp_log (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
  )ENGINE=Log;

-- 插入数据
INSERT INTO emp_log VALUES (1,'tom','上海',25,'技术部',20000),(2,'jack','上海',26,'人事部',10000);
INSERT INTO emp_log VALUES (3,'bob','北京',33,'财务部',50000),(4,'tony','杭州',28,'销售事部',50000);

-- 查询数据,由于是分两次插入数据,所以查询时会有两个数据块
SELECT * FROM emp_log;

进入默认数据存储目录,查看底层数据存储形式

[root@ck01 emp_log]# ll -h
total 32K
-rw-r----- 1 clickhouse clickhouse  56 Sep 24 11:12 age.bin
-rw-r----- 1 clickhouse clickhouse  97 Sep 24 11:12 depart.bin
-rw-r----- 1 clickhouse clickhouse  60 Sep 24 11:12 emp_id.bin
-rw-r----- 1 clickhouse clickhouse  96 Sep 24 11:12 __marks.mrk
-rw-r----- 1 clickhouse clickhouse  70 Sep 24 11:12 name.bin
-rw-r----- 1 clickhouse clickhouse  68 Sep 24 11:12 salary.bin
-rw-r----- 1 clickhouse clickhouse 215 Sep 24 11:12 sizes.json
-rw-r----- 1 clickhouse clickhouse  80 Sep 24 11:12 work_place.bin

Log引擎的存储结构包含三部分:

  • .bin:数据文件,数据文件按列单独存储
  • __marks.mrk:数据标记,统一保存了数据在各个.bin文件中的位置信息。利用数据标记能够使用多个线程,以并行的方式读取。.bin内的压缩数据块,从而提升数据查询的性能。
  • sizes.json:记录了.bin__marks.mrk大小的信息

**注意:**Log表引擎会将每一列都存在一个文件中,对于每一次的INSERT操作,都会对应一个数据块

4. MergeTree系列引擎

    在所有的表引擎中,最为核心的当属MergeTree系列表引擎,这些表引擎拥有最为强大的性能和最广泛的使用场合。对于非MergeTree系列的其他引擎而言,主要用于特殊用途,场景相对有限。而MergeTree系列表引擎是官方主推的存储引擎,支持几乎所有ClickHouse核心功能。

MergeTree表引擎(重要)

    更详细的讲解参阅: https://clickhouse.tech/docs/zh/engines/table-engines/mergetree-family/mergetree/

    MergeTree在写入一批数据时,数据总会以数据片段的形式写入磁盘,且数据片段不可修改。为了避免片段过多,ClickHouse会通过后台线程,定期合并这些数据片段,属于相同分区的数据片段会被合成一个新的片段。这种数据片段往复合并的特点,也正是合并树名称的由来。

MergeTree作为家族系列最基础的表引擎,主要有以下特点:

  • 存储的数据按照主键排序:允许创建稀疏索引,从而加快数据查询速度

  • 支持分区,可以通过PRIMARY KEY语句指定分区字段。

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

  • 支持数据副本

  • 支持数据采样

建表语法

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(). 这种情况下,ClickHouse会按照插入的顺序存储数据。
  • PARTITION BY– 分区键 。可选

    要按月分区,可以使用表达式 toYYYYMM(date_column) ,这里的 date_column 是一个 Date 类型的列。分区名的格式会是 "YYYYMM"

  • PRIMARY KEY – 主键。如果排序字段与主键不一致,可以单独指定主键字段。否则默认主键是排序字段。可选

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

    如果指定了该字段,那么主键中也必须包含该字段。比如SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID))

  • TTL–指定行存储的持续时间并定义数据片段在硬盘和卷上的移动逻辑的规则列表。**可选。**当时间到达时,如果是列字段级别的TTL,则会删除这一列的数据;如果是表级别的TTL,则会删除整张表的数据。可选

  • SETTINGS — 控制 MergeTree 行为的额外参数。可选

建表示例

-- 建表
DROP TABLE IF EXISTS emp_mergetree;
CREATE TABLE emp_mergetree (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
)ENGINE=MergeTree()
ORDER BY emp_id
PARTITION BY work_place;

-- 插入数据 每个INSERT插入语句都会根据分区字段生成新的分区
INSERT INTO emp_mergetree 
VALUES (1,'tom','上海',25,'技术部',20000),(2,'jack','上海',26,'人事部',10000);
INSERT INTO emp_mergetree 
VALUES (3,'bob','北京',33,'财务部',50000),(4,'tony','杭州',28,'销售事部',50000); 

-- 查询数据
-- 按work_place进行分区,分了三个区
SELECT * FROM emp_mergetree;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      3 │ bob  │ 北京       │  33 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘

查看一下数据存储格式,也可以看出,存在三个分区文件夹,每一个分区文件夹内存储了对应分区的数据

[root@ck01 ~]# cd /var/lib/clickhouse/data/default/emp_mergetree
[root@ck01 emp_mergetree]# ll -h
total 16K
drwxr-x--- 2 clickhouse clickhouse 4.0K Sep 24 14:03 1c89a3ba9fe5fd53379716a776c5ac34_3_3_0
drwxr-x--- 2 clickhouse clickhouse 4.0K Sep 24 14:03 40d45822dbd7fa81583d715338929da9_1_1_0
drwxr-x--- 2 clickhouse clickhouse 4.0K Sep 24 14:03 a6155dcc1997eda1a348cd98b17a93e9_2_2_0
drwxr-x--- 2 clickhouse clickhouse    6 Sep 24 14:02 detached
-rw-r----- 1 clickhouse clickhouse    1 Sep 24 14:02 format_version.txt

# 进入一个分区目录查看
[root@ck01 1c89a3ba9fe5fd53379716a776c5ac34_3_3_0]# ll -h
total 76K
-rw-r----- 1 clickhouse clickhouse   27 Sep 24 14:03 age.bin
-rw-r----- 1 clickhouse clickhouse   48 Sep 24 14:03 age.mrk2
-rw-r----- 1 clickhouse clickhouse  591 Sep 24 14:03 checksums.txt
-rw-r----- 1 clickhouse clickhouse  138 Sep 24 14:03 columns.txt
-rw-r----- 1 clickhouse clickhouse    1 Sep 24 14:03 count.txt
-rw-r----- 1 clickhouse clickhouse   39 Sep 24 14:03 depart.bin
-rw-r----- 1 clickhouse clickhouse   48 Sep 24 14:03 depart.mrk2
-rw-r----- 1 clickhouse clickhouse   28 Sep 24 14:03 emp_id.bin
-rw-r----- 1 clickhouse clickhouse   48 Sep 24 14:03 emp_id.mrk2
-rw-r----- 1 clickhouse clickhouse   14 Sep 24 14:03 minmax_work_place.idx
-rw-r----- 1 clickhouse clickhouse   31 Sep 24 14:03 name.bin
-rw-r----- 1 clickhouse clickhouse   48 Sep 24 14:03 name.mrk2
-rw-r----- 1 clickhouse clickhouse    7 Sep 24 14:03 partition.dat
-rw-r----- 1 clickhouse clickhouse    4 Sep 24 14:03 primary.idx
-rw-r----- 1 clickhouse clickhouse   30 Sep 24 14:03 salary.bin
-rw-r----- 1 clickhouse clickhouse   48 Sep 24 14:03 salary.mrk2
-rw-r----- 1 clickhouse clickhouse   33 Sep 24 14:03 work_place.bin
-rw-r----- 1 clickhouse clickhouse   48 Sep 24 14:03 work_place.mrk2
  • checksums.txt:校验文件,使用二进制格式存储。它保存了余下各类文件(primary. idx、count.txt等)的size大小及size的哈希值,用于快速校验文件的完整性和正确性。

  • columns.txt:列信息文件,使用明文格式存储。用于保存此数据分区下的列字段信息。cat columns.txt

  • count.txt:计数文件,使用明文格式存储。用于记录当前数据分区目录下数据的总行数

  • primary.idx:一级索引文件,使用二进制格式存储。用于存放稀疏索引,一张MergeTree表只能声明一次一级索引,即通过ORDER BY或者PRIMARY KEY指定字段。借助稀疏索引,在数据查询的时能够排除主键条件范围之外的数据文件,从而有效减少数据扫描范围,加速查询速度。

  • 列.bin:数据文件,使用压缩格式存储,默认为LZ4压缩格式,用于存储某一列的数据。由于MergeTree采用列式存储,所以每一个列字段都拥有独立的.bin数据文件,并以列字段名称命名。

  • 列.mrk2:列字段标记文件,使用二进制格式存储。标记文件中保存了.bin文件中数据的偏移量信息

  • partition.dat与minmax_[Column].idx:如果指定了分区键,则会额外生成partition.dat与minmax索引文件,它们均使用二进制格式存储。partition.dat用于保存当前分区下分区表达式最终生成的值,即分区字段值;而minmax索引用于记录当前分区下分区字段对应原始数据的最小和最大值。比如当使用EventTime字段对应的原始数据为2020-09-17、2020-09-30,分区表达式为PARTITION BY toYYYYMM(EventTime),即按月分区。partition.dat中保存的值将会是2019-09,而minmax索引中保存的值将会是2020-09-17 2020-09-30。

注意:多次插入数据,会生成多个分区文件

-- 新插入两条数据  虽然北京分区已经存在,但新插入时,还是生成了新的新的分区,这也说明了每个INSERT插入语句都会根据分区字段生成新的分区
INSERT INTO emp_mergetree
VALUES (5,'robin','北京',35,'财务部',50000),(6,'lilei','北京',38,'销售事部',50000);

-- 查询结果
ck01.hadoop.com :) SELECT * FROM emp_mergetree;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      3 │ bob  │ 北京       │  33 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name──┬─work_place─┬─age─┬─depart───┬───salary─┐
│      5 │ robin │ 北京       │  35 │ 财务部   │ 50000.00 │
│      6 │ lilei │ 北京       │  38 │ 销售事部 │ 50000.00 │
└────────┴───────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
# 查看一下数据存储格式,也可以看出,又新生成了一个分区文件夹 a6155dcc1997eda1a348cd98b17a93e9_4_4_0
[root@ck01 emp_mergetree]# ll /var/lib/clickhouse/data/default/emp_mergetree
total 20
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:03 1c89a3ba9fe5fd53379716a776c5ac34_3_3_0
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:03 40d45822dbd7fa81583d715338929da9_1_1_0
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:03 a6155dcc1997eda1a348cd98b17a93e9_2_2_0
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:19 a6155dcc1997eda1a348cd98b17a93e9_4_4_0
drwxr-x--- 2 clickhouse clickhouse    6 Sep 24 14:02 detached
-rw-r----- 1 clickhouse clickhouse    1 Sep 24 14:02 format_version.txt

    新插入的数据新生成了一个数据块,并没有与原来的分区数据在一起,我们可以执行optimize命令,执行合并操作

-- 仅对'北京'分区内的数据执行合并操作, 合并操作也会根据合并的分区数在数据存储目录中生成相应分区数的分区文件夹
OPTIMIZE TABLE emp_mergetree PARTITION '北京';

-- 再次执行查询
ck01.hadoop.com :) SELECT * FROM emp_mergetree;             
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name──┬─work_place─┬─age─┬─depart───┬───salary─┐
│      3 │ bob   │ 北京       │  33 │ 财务部   │ 50000.00 │
│      5 │ robin │ 北京       │  35 │ 财务部   │ 50000.00 │
│      6 │ lilei │ 北京       │  38 │ 销售事部 │ 50000.00 │
└────────┴───────┴────────────┴─────┴──────────┴──────────┘
# 查看一下数据存储格式,可以看出,又新生成了一个分区文件夹,原来的分区文件夹不变。 a6155dcc1997eda1a348cd98b17a93e9_2_4_1
[root@ck01 emp_mergetree]# ll /var/lib/clickhouse/data/default/emp_mergetree
total 24
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:03 1c89a3ba9fe5fd53379716a776c5ac34_3_3_0
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:03 40d45822dbd7fa81583d715338929da9_1_1_0
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:03 a6155dcc1997eda1a348cd98b17a93e9_2_2_0
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:24 a6155dcc1997eda1a348cd98b17a93e9_2_4_1
drwxr-x--- 2 clickhouse clickhouse 4096 Sep 24 14:19 a6155dcc1997eda1a348cd98b17a93e9_4_4_0
drwxr-x--- 2 clickhouse clickhouse    6 Sep 24 14:02 detached
-rw-r----- 1 clickhouse clickhouse    1 Sep 24 14:02 format_version.txt

在MergeTree中主键并不用于去重,而是用于索引,加快查询速度

-- 插入一条相同主键的数据,会发现该条数据可以插入
INSERT INTO emp_mergetree VALUES (1,'sam','杭州',35,'财务部',50000);

-- 查询发现emp_id=1的数据查到了两条,由此可知,并不会对主键进行去重
ck01.hadoop.com :) SELECT * FROM emp_mergetree WHERE emp_id = 1;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ sam  │ 杭州       │  35 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘

ReplacingMergeTree表引擎

    上文最后提到MergeTree表引擎无法对相同主键的数据进行去重,ClickHouse提供了ReplacingMergeTree引擎,它能够合并分区时删除相同的数据分区内重复数据

建表语法

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = ReplacingMergeTree([ver])
[PARTITION BY expr]
[ORDER BY expr]
[PRIMARY KEY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]
  • ver – 列的版本。**可选。**类型为 UInt*, DateDateTime

    在数据合并的时候,ReplacingMergeTree 从所有具有相同排序键的行中选择一行留下:

    • 如果 ver 列未指定,保留最后一条。
    • 如果 ver 列已指定,保留 ver 值最大的版本

建表示例:验证合并分区时,只有相同的数据分区内重复的数据才会被去重

-- 建表  ORDER BY (emp_id,name)
DROP TABLE IF EXISTS emp_replacingmergetree;
CREATE TABLE emp_replacingmergetree (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
)ENGINE=ReplacingMergeTree()
ORDER BY (emp_id,name)
PRIMARY KEY emp_id
PARTITION BY work_place;

 -- 插入数据   数据存储目分区数:4
INSERT INTO emp_replacingmergetree VALUES (1,'tom','上海',25,'技术部',20000);
INSERT INTO emp_replacingmergetree VALUES (2,'jack','上海',26,'人事部',10000);
INSERT INTO emp_replacingmergetree VALUES (3,'bob','北京',33,'财务部',50000);
INSERT INTO emp_replacingmergetree VALUES (4,'tony','杭州',28,'销售事部',50000); 

-- 查询
select * from emp_replacingmergetree;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      3 │ bob  │ 北京       │  33 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘

-- 再次向该表插入具有相同主键的数据  数据存储目分区数:6
-- (1,'tom','北京',26,'技术部',10000) -- emp_id和name相同,满足orderby字段重复,但区分不同,不会被去重
-- (1,'sam','上海',25,'技术部',20000)  -- 满足分区相同,主键相同,但orderby字段不同,不会被去重
-- (1,'tom','上海',25,'技术部',50000)  -- emp_id、name、分区都相同,满足分区相同,orderby字段相同,会被去重
INSERT INTO emp_replacingmergetree VALUES (1,'tom','北京',26,'技术部',10000),(1,'sam','上海',25,'技术部',20000),(1,'tom','上海',25,'技术部',50000);

-- 查询数据
select * from emp_replacingmergetree;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ sam  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      3 │ bob  │ 北京       │  33 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 北京       │  26 │ 技术部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘

-- 执行合并操作,数据存储目分区数:9
optimize table emp_replacingmergetree final;

-- 再次查询,相同主键的数据,保留最近插入的数据,旧的数据被清除
select * from emp_replacingmergetree;    
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ sam  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 50000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 北京       │  26 │ 技术部 │ 10000.00 │
│      3 │ bob  │ 北京       │  33 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
SummingMergeTree表引擎

    该引擎继承了MergeTree引擎,推荐将该引擎和 MergeTree 一起使用。例如,将完整的数据存储在MergeTree 表中,并且使用 SummingMergeTree 来存储聚合数据。这种方法可以避免因为使用不正确的主键组合方式而丢失数据。

    如果用户只需要查询数据的汇总结果,不关心明细数据,并且数据的汇总条件是预先明确的,即GROUP BY的分组字段是确定的,可以使用该表引擎

建表语法

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = SummingMergeTree([columns]) -- 指定合并时汇总字段
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

建表示例

-- 建表  SummingMergeTree(salary)当合并时,相同分区如果有排序字段重复的数据会被去重,重复数据的salary会被求和,作为去重后的salary字段的值
DROP TABLE IF EXISTS emp_summingmergetree;
CREATE TABLE emp_summingmergetree (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
)ENGINE=SummingMergeTree(salary)
ORDER BY (emp_id,name)
PRIMARY KEY emp_id
PARTITION BY work_place;

 -- 插入数据   数据存储目分区数:3
INSERT INTO emp_summingmergetree
VALUES (1,'tom','上海',25,'技术部',20000),(2,'jack','上海',26,'人事部',10000);
INSERT INTO emp_summingmergetree
VALUES (3,'bob','北京',33,'财务部',50000),(4,'tony','杭州',28,'销售事部',50000); 

-- 再次插入数据   数据存储目分区数:5
-- (1,'tom','上海',25,'信息部',10000) 同一分区内排序字段重复,会被去重且合并salary,保留最新的记录(即此条记录),但salary字段会与重复字段合并salary的值应为30000
-- (1,'jerry','上海',25,'信息部',10000) 同一分区内,主键字段重复但排序字段不重复,不会被去重
-- (1,'tom','北京',26,'人事部',10000) 不同分区内,排序字段重复,不会被去重
INSERT INTO emp_summingmergetree VALUES 
(1,'tom','上海',25,'信息部',10000),
(1,'jerry','上海',25,'信息部',10000),
(1,'tom','北京',26,'人事部',10000);

-- 执行合并操作   数据存储目分区数:5
optimize table emp_summingmergetree final;

-- 查询
select * from emp_summingmergetree;       
┌─emp_id─┬─name──┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ jerry │ 上海       │  25 │ 信息部 │ 10000.00-- 同一分区排序字段不同不会被去重1 │ tom   │ 上海       │  25 │ 技术部 │ 30000.00-- salary值为300002 │ jack  │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴───────┴────────────┴─────┴────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart───┬───salary─┐
│      4 │ tony │ 杭州       │  28 │ 销售事部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴──────────┴──────────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 北京       │  26 │ 人事部 │ 10000.00-- 分区不同,未被去重3 │ bob  │ 北京       │  33 │ 财务部 │ 50000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘
Aggregatingmergetree表引擎

该表引擎继承自MergeTree,可以使用 AggregatingMergeTree 表来做增量数据统计聚合。如果要按一组规则来合并减少行数,则使用 AggregatingMergeTree 是合适的。AggregatingMergeTree是通过预先定义的聚合函数计算数据并通过二进制的格式存入表内。

与SummingMergeTree的区别在于:SummingMergeTree对非主键列进行sum聚合,而AggregatingMergeTree则可以指定各种聚合函数。

建表示例

AggregatingMergeTree通常作为物化视图的表引擎,与普通MergeTree搭配使用。

-- 创建一个MereTree引擎的明细表,用于存储全量的明细数据,对外提供实时查询
DROP TABLE IF EXISTS tbl_emp_mergetree;
CREATE TABLE tbl_emp_mergetree (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
)ENGINE=MergeTree()
ORDER BY (emp_id,name)
PARTITION BY work_place;
  
-- 创建一张物化视图,使用AggregatingMergeTree表引擎
DROP VIEW IF EXISTS view_emp_agg;
CREATE MATERIALIZED VIEW view_emp_agg
ENGINE = AggregatingMergeTree()
PARTITION BY emp_id
ORDER BY (emp_id,name)
AS SELECT
     emp_id,
     name,
     sumState(salary) AS salary
FROM tbl_emp_mergetree
GROUP BY emp_id,name;

-- 向基础明细表emp_mergetree_base插入数据
INSERT INTO tbl_emp_mergetree
VALUES (1,'tom','上海',25,'技术部',20000),(1,'tom','上海',26,'人事部',10000);

-- 查询物化视图
SELECT emp_id, name, sumMerge(salary) 
FROM view_emp_agg
GROUP BY emp_id,name;
-- 结果
┌─emp_id─┬─name─┬─sumMerge(salary)─┐
│      1 │ tom  │         30000.00 │
└────────┴──────┴──────────────────┘
CollapsingMergeTree表引擎

    CollapsingMergeTree【collapsing 译:折叠、崩塌】就是一种==通过以增代删的思路,支持行级数据修改和删除的表引擎。==它通过定义一个sign标记位字段,记录数据行的状态。如果sign标记为1,则表示这是一行有效的数据;如果sign标记为-1,则表示这行数据需要被删除。CollapsingMergeTree 会异步的删除(折叠)这些除了特定列 Sign1-1 的值以外,其余所有字段的值都相等的成对的行。没有成对的行会被保留。更多的细节请看本文的折叠部分。

    每次需要新增数据时,写入一行sign标记为1的数据;需要删除数据时,则写入一行sign标记为-1的数据。

建表语法

-- sign是一个Int8类型的字段
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = CollapsingMergeTree(sign)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

建表示例

-- 建表  CollapsingMergeTree同样是以ORDER BY排序键作为判断数据唯一性的依据
DROP TABLE IF EXISTS emp_collapsingmergetree;
CREATE TABLE emp_collapsingmergetree (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资',
  sign Int8
)ENGINE=CollapsingMergeTree(sign)
ORDER BY (emp_id,name)
PARTITION BY work_place;

-- 插入新增数据,sign=1表示正常数据
INSERT INTO emp_collapsingmergetree 
VALUES (1,'tom','上海',25,'技术部',20000,1);

-- 更新上述的数据
-- 首先插入一条与原来相同的数据(ORDER BY字段一致),并将sign置为-1
INSERT INTO emp_collapsingmergetree 
VALUES (1,'tom','上海',25,'技术部',20000,-1);

-- 再插入更新之后的数据
INSERT INTO emp_collapsingmergetree 
VALUES (1,'tom','上海',25,'技术部',30000,1);

-- 查看一下结果
cdh04 :) select * from emp_collapsingmergetree ;

SELECT *
FROM emp_collapsingmergetree

┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┬─sign─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 30000.001 │
└────────┴──────┴────────────┴─────┴────────┴──────────┴──────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┬─sign─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00-1 │
└────────┴──────┴────────────┴─────┴────────┴──────────┴──────┘
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┬─sign─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.001 │
└────────┴──────┴────────────┴─────┴────────┴──────────┴──────┘

-- 执行分区合并操作
optimize table emp_collapsingmergetree;
-- 再次查询,sign=1与sign=-1的数据相互抵消了,即被删除
select * from emp_collapsingmergetree ;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┬─sign─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 30000.001 │
└────────┴──────┴────────────┴─────┴────────┴──────────┴──────┘

注意:

1、分区数据折叠不是实时的,需要后台进行Compaction操作,用户也可以使用手动合并命令,但是效率会很低,一般不推荐在生产环境中使用。当进行汇总数据操作时,可以通过改变查询方式,来过滤掉被删除的数据。

SELECT 
    emp_id, 
    name, 
    sum(salary * sign)
FROM emp_collapsingmergetree
GROUP BY 
    emp_id, 
    name
HAVING sum(sign) > 0;

2、CollapsingMergeTree对于写入数据的顺序有着严格要求,否则导致无法正常折叠。

    如果数据的写入程序是单线程执行的,则能够较好地控制写入顺序;如果需要处理的数据量很大,数据的写入程序通常是多线程执行的,那么此时就不能保障数据的写入顺序了。在这种情况下,CollapsingMergeTree的工作机制就会出现问题。但是可以通过VersionedCollapsingMergeTree的表引擎得到解决

VersionedCollapsingMergeTree表引擎

    上面提到CollapsingMergeTree表引擎对于数据写入乱序的情况下,不能够实现数据折叠的效果。VersionedCollapsingMergeTree表引擎的作用与CollapsingMergeTree完全相同,它们的不同之处在于,VersionedCollapsingMergeTree对数据的写入顺序没有要求,在同一个分区内,任意顺序的数据都能够完成折叠操作。VersionedCollapsingMergeTree使用version列来实现乱序情况下的数据折叠。

建表语法

-- 该引擎除了需要指定一个sign标识之外,还需要指定一个UInt8类型的version版本号。
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = VersionedCollapsingMergeTree(sign, version)
[PARTITION BY expr]
[ORDER BY expr]
[SAMPLE BY expr]
[SETTINGS name=value, ...]

建表示例

DROP TABLE IF EXISTS emp_collapsingmergetree;
CREATE TABLE emp_versioned (
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资',
  sign Int8,
  version Int8
)ENGINE=VersionedCollapsingMergeTree(sign, version)
ORDER BY (emp_id,name)
PARTITION BY work_place;
  
-- 先插入需要被删除的数据,即sign=-1的数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,-1,1);
-- 再插入sign=1的数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',20000,1,1);
-- 在插入一个新版本数据
INSERT INTO emp_versioned VALUES (1,'tom','上海',25,'技术部',30000,1,2);

-- 手动合并
optimize table emp_versioned;

-- 再次查询
select * from emp_versioned;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┬─sign─┬─version─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 30000.0012 │
└────────┴──────┴────────────┴─────┴────────┴──────────┴──────┴─────────┘

上述案例虽然插入的数据是乱序的,依然能够实现折叠的效果,因为在定义version字段之后,VersionedCollapsingMergeTree会自动将version作为排序条件并增加到ORDER BY的末端,就上述的例子而言,最终的排序字段为ORDER BY emp_id,name,version desc。

GraphiteMergeTree表引擎

    该引擎用来对 Graphite数据进行’瘦身’及汇总。对于想使用CH来存储Graphite数据的开发者来说可能有用。

    如果不需要对Graphite数据做汇总,那么可以使用任意的CH表引擎;但若需要,那就采用 GraphiteMergeTree 引擎。它能减少存储空间,同时能提高Graphite数据的查询效率。

5. 外部集成表引擎

    ClickHouse提供了许多与外部系统集成的方法,包括一些表引擎。这些表引擎与其他类型的表引擎类似,可以用于将外部数据导入到ClickHouse中,或者在ClickHouse中直接操作外部数据源。

    例如直接读取HDFS的文件或者MySQL数据库的表。这些表引擎只负责元数据管理和数据查询,而它们自身通常并不负责数据的写入,数据文件直接由外部系统提供。目前ClickHouse提供了下面的外部集成表引擎:

HDFS

语法:ENGINE = HDFS(URI, format)

  • URI:HDFS文件路径
  • format:文件格式,比如CSV、JSON、TSV等

使用示例

-- 建表
DROP TABLE IF EXISTS hdfs_engine_table;
CREATE TABLE hdfs_engine_table(
  emp_id UInt16 COMMENT '员工id',
  name String COMMENT '员工姓名',
  work_place String COMMENT '工作地点',
  age UInt8 COMMENT '员工年龄',
  depart String COMMENT '部门',
  salary Decimal32(2) COMMENT '工资'
) ENGINE=HDFS('hdfs://192.168.52.100:8020/clickhouse/hdfs_engine_table.csv', 'CSV');

-- 写入数据
INSERT INTO hdfs_engine_table 
VALUES (1,'tom','上海',25,'技术部',20000),(2,'jack','上海',26,'人事部',10000);
-- 再次插入就会失败,报文件已存在。一般是csv文件已经在hdfs中存在了,我们直接建表直接去读
INSERT INTO hdfs_engine_table 
VALUES (3,'bob','北京',33,'财务部',50000),(4,'tony','杭州',28,'销售事部',50000); 

-- 查询数据
select * from hdfs_engine_table;
┌─emp_id─┬─name─┬─work_place─┬─age─┬─depart─┬───salary─┐
│      1 │ tom  │ 上海       │  25 │ 技术部 │ 20000.00 │
│      2 │ jack │ 上海       │  26 │ 人事部 │ 10000.00 │
└────────┴──────┴────────────┴─────┴────────┴──────────┘

    这种方式与使用Hive类似,我们直接可以将HDFS对应的文件映射成ClickHouse中的一张表,这样就可以使用SQL操作HDFS上的文件了。**注意:**ClickHouse并不能够删除HDFS上的数据,当我们在ClickHouse客户端中删除了对应的表,只是删除了表结构,HDFS上的文件并没有被删除,这一点跟Hive的外部表十分相似。

MySQL

    MySQL引擎用于将远程的MySQL服务器中的表映射到ClickHouse中,并允许您对表进行INSERTSELECT查询,以方便您在ClickHouse与MySQL之间进行数据交换。

建表语法

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],
    ...
) ENGINE = MySQL('host:port', 'database', 'table', 'user', 'password'[, replace_query, 'on_duplicate_clause']);

建表示例

-- 连接MySQL中clickhouse数据库的test表
CREATE TABLE mysql_engine_table(
    id Int32,
    name String
) ENGINE = MySQL(
 '192.168.52.100:3306',
 'clickhouse',
 'test', 
 'root', 
 '123456');
 
-- 查询数据
SELECT * FROM mysql_engine_table;
┌─id─┬─name─────┐
│  1 │ zhangsan │
│  2 │ lisi     │
│  3 │ wangwu   │
└────┴──────────┘

-- 插入数据,会将数据插入MySQL对应的表中
-- 在MySQL中查询数据时,也会发现新增了一条数据
INSERT INTO mysql_engine_table VALUES(4,'robin');

数据类型映射关系

MySQLClickHouse
UNSIGNED TINYINTUInt8
TINYINTInt8
UNSIGNED SMALLINTUInt16
SMALLINTInt16
UNSIGNED INT, UNSIGNED MEDIUMINTUInt32
INT, MEDIUMINTInt32
UNSIGNED BIGINTUInt64
BIGINTInt64
FLOATFloat32
DOUBLEFloat64
DATEDate
DATETIME, TIMESTAMPDateTime
BINARYFixedString

其他的MySQL数据类型将全部都转换为字符串。同时以上的所有类型都支持可为空

Kafka

建表语法

CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
    name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
    name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
    ...
) ENGINE = Kafka()
SETTINGS
    kafka_broker_list = 'host:port',
    kafka_topic_list = 'topic1,topic2,...',
    kafka_group_name = 'group_name',
    kafka_format = 'data_format'[,]
    [kafka_row_delimiter = 'delimiter_symbol',]
    [kafka_schema = '',]
    [kafka_num_consumers = N,]
    [kafka_max_block_size = 0,]
    [kafka_skip_broken_messages = N,]
    [kafka_commit_every_batch = 0,]
    [kafka_thread_per_consumer = 0]

必要参数:

  • kafka_broker_list – 以逗号分隔的 brokers 列表 (localhost:9092)。
  • kafka_topic_list – topic 列表 (my_topic)。
  • kafka_group_name – Kafka 消费组名称 (group1)。如果不希望消息在集群中重复,请在每个分片中使用相同的组名。
  • kafka_format – 消息体格式。使用与 SQL 部分的 FORMAT 函数相同表示方法,例如 JSONEachRow。了解详细信息,请参考 Formats 部分。

可选参数:

  • kafka_row_delimiter - 每个消息体(记录)之间的分隔符。
  • kafka_schema – 如果解析格式需要一个 schema 时,此参数必填。例如,普罗托船长 需要 schema 文件路径以及根对象 schema.capnp:Message 的名字。
  • kafka_num_consumers – 单个表的消费者数量。默认值是:1,如果一个消费者的吞吐量不足,则指定更多的消费者。消费者的总数不应该超过 topic 中分区的数量,因为每个分区只能分配一个消费者。

建表示例

在kafka中创建ck_topic主题,并向该主题写入数据

# 启动kafka
cd /baicdt/servers/kafka_2.11-1.0.0/
nohup bin/kafka-server-start.sh config/server.properties 2>&1 &

# 查看Kafka当中已存在的主题
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--list \
--zookeeper 192.168.52.100:2181

#创建一个新的topic,名称为ck_topic
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--create \
--zookeeper 192.168.52.100:2181 \
--replication-factor 1 \
--partitions 3 \
--topic ck_topic


# 模拟生产者生产数据  {"id":1,"name":"zhangsan"}   {"id":2,"name":"lisi"}
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-console-producer.sh --broker-list 192.168.52.100:9092 --topic ck_topic

# 模拟消费者进行消费数据
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-console-consumer.sh \
--from-beginning \
--topic ck_topic  \
--zookeeper 192.168.52.100:2181

# 删除topic
/baicdt/servers/kafka_2.11-1.0.0/bin/kafka-topics.sh \
--zookeeper 192.168.52.100:2181 \
--delete \
--topic ck_topic

在clickhouse中建kafka_table表消费数据

-- 建表 kafka_format = 'JSONEachRow' 会自动解析字符串{"id":1,"name":"zhangsan"}
-- 如果 kafka_format = 'CSV' 则用来解析 1,"zhangsan" 这种格式的字符串
DROP TABLE IF EXISTS kafka_table;
CREATE TABLE kafka_table (
    id Int32,
    name String
  ) ENGINE = Kafka()
    SETTINGS
    kafka_broker_list = '192.168.52.100:9092',
    kafka_topic_list = 'ck_topic',
    kafka_group_name = 'custom_group_01',
    kafka_format = 'JSONEachRow';

-- 查询,数据查询到一次后再次查询不到了,即无法重复消费
select * from kafka_table;

**注意:**当我们一旦查询完毕之后,ClickHouse会删除表内的数据,其实Kafka表引擎只是一个数据管道,我们可以通过物化视图的方式访问Kafka中的数据。

如何更合理的使用Kafka表引擎呢?

  • 首先创建一张Kafka表引擎的表,用于从Kafka中读取数据
  • 然后再创建一张普通表引擎的表,比如MergeTree,面向终端用户使用
  • 最后创建物化视图,用于将Kafka引擎表实时同步到终端用户所使用的表中
--  创建Kafka引擎表
 CREATE TABLE kafka_table_consumer (
    id UInt64,
    name String
  ) ENGINE = Kafka()
    SETTINGS
    kafka_broker_list = '192.168.52.100:9092',
    kafka_topic_list = 'ck_topic',
    kafka_group_name = 'custom_group_02',
    kafka_format = 'JSONEachRow';

-- 创建一张终端用户使用的表
CREATE TABLE kafka_table_mergetree (
  id UInt64 ,
  name String
)ENGINE=MergeTree()
ORDER BY id;
  
-- 创建物化视图,同步数据
CREATE MATERIALIZED VIEW view_consumer TO kafka_table_mergetree
    AS SELECT id,name FROM kafka_table_consumer;
    
-- 查询,多次查询,已经被查询的数据依然会被输出
select * from kafka_table_mergetree;

6. 其他表引擎

Distributed表引擎

    Distributed表引擎是分布式表的代名词,它自身不存储任何数据,数据都分散存储在某一个分片上,能够自动路由数据至集群中的各个节点,所以Distributed表引擎需要和其他数据表引擎一起协同工作。

    所以,一张分布式表底层会对应多个本地分片数据表,由具体的分片表存储数据,分布式表与分片表是一对多的关系。

    建表语法和使用示例请参照 第三章第5节。

Memory表引擎

    Memory表引擎直接将数据保存在内存中,数据既不会被压缩也不会被格式转换。当ClickHouse服务重启的时候,Memory表内的数据会全部丢失。一般在测试时使用。

 CREATE TABLE table_memory (
    id UInt64,
    name String
  ) ENGINE = Memory();
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值