一 clickhouse-简介
ClickHouse是俄罗斯的Yandex于2016年开源的一个用于联机分析(OLAP:Online Analytical Processing)的列式数据库管理系统(DBMS:Database Management System) , 主要用于在线分析处理查询(OLAP),能够使用SQL查询实时生成分析数据报告。 ClickHouse的全称是Click Stream,Data WareHouse,简称ClickHouse
ClickHouse是一个完全的列式分布式数据库管理系统(DBMS),允许在运行时创建表和数据库,加载数据和运行查询,而无需重新配置和重新启动服务器,支持线性扩展,简单方便,高可靠性,容错。它在大数据领域没有走 Hadoop 生态,而是采用 Local attached storage 作为存储,这样整个 IO 可能就没有 Hadoop 那一套的局限。它的系统在生产环境中可以应用到比较大的规模,因为它的线性扩展能力和可靠性保障能够原生支持 shard + replication 这种解决方案。它还提供了一些 SQL 直接接口,有比较丰富的原生 client。
1 优点
-
灵活的MPP架构,支持线性扩展,简单方便,高可靠性
-
多服务器分布式处理数据 ,完备的DBMS系统
-
底层数据列式存储,支持压缩,优化数据存储,优化索引数据 优化底层存储
-
容错跑分快:比Vertica快5倍,比Hive快279倍,比MySQL快800倍,其可处理的数据级别已达到10亿级别
-
功能多:支持数据统计分析各种场景,支持类SQL查询,异地复制部署
海量数据存储,分布式运算,快速闪电的性能,几乎实时的数据分析 ,友好的SQL语法,出色的函数支持
2 缺点
- 不支持事务,不支持真正的删除/更新 (批量)
- 不支持高并发,官方建议qps为100,可以通过修改配置文件增加连接数,但是在服务器足够好的情况下
- 不支持二级索引
- 不擅长多表join *** 大宽表
- 元数据管理需要人为干预 ***
- 尽量做1000条以上批量的写入,避免逐行insert或小批量的insert,update,delete操作
3 应用场景
1.绝大多数请求都是用于读访问的, 要求实时返回结果
2.数据需要以大批次(大于1000行)进行更新,而不是单行更新;或者根本没有更新操作
3.数据只是添加到数据库,没有必要修改
4.读取数据时,会从数据库中提取出大量的行,但只用到一小部分列
5.表很“宽”,即表中包含大量的列
6.查询频率相对较低(通常每台服务器每秒查询数百次或更少)
7.对于简单查询,允许大约50毫秒的延迟
8.列的值是比较小的数值和短字符串(例如,每个URL只有60个字节)
9.在处理单个查询时需要高吞吐量(每台服务器每秒高达数十亿行)
10.不需要事务
11.数据一致性要求较低 [原子性 持久性 一致性 隔离性]
12.每次查询中只会查询一个大表。除了一个大表,其余都是小表
13.查询结果显著小于数据源。即数据有过滤或聚合。返回结果不超过单个服务器内存大小
4 核心概念
1) 数据分片
数据分片是将数据进行横向切分,这是一种在面对海量数据的场 景下,解决存储和查询瓶颈的有效手段,是一种分治思想的体现。 ClickHouse支持分片,而分片则依赖集群。每个集群由1到多个分片组成,而每个分片则对应了ClickHouse的1个服务节点。分片的数量上限 取决于节点数量(1个分片只能对应1个服务节点)。ClickHouse并不像其他分布式系统那样,拥有高度自动化的分片功能。ClickHouse提供了本地表(Local Table)与分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。这种设计类似数据库的分库和分表,十分灵活。例如在业务系统上线的初期,数据体量并不高,此时数据表并不需要多个分片。所以使用单个节点的本地表(单个数据分片)即可满足业务需求,待到业务增长、数据量增大的时候,再通过新增数据分片的方式分流数据,并通过分布式表实现分布式查询。这就好比一辆手动挡赛车,它将所有的选择权都交到了使用者的手中!
2) 列式存储
1)如前所述,分析场景中往往需要读大量行但是少数几个列。在行存模式下,数据按行连续存储,所有列的数据都存储在一个bloCK中,不参与计算的列在IO时也要全部读出,读取操作被严重放大。而列存模式下,只需要读取参与计算的列即可,极大的减低了IO cost,加速了查询。
2)同一列中的数据属于同一类型,压缩效果显著。列存往往有着高达十倍甚至更高的压缩比,节省了大量的存储空间,降低了存储成本。
3)更高的压缩比意味着更小的data size,从磁盘中读取相应数据耗时更短。
4)*的压缩算法选择。不同列的数据具有不同的数据类型,适用的压缩算法也就不尽相同。可以针对不同列类型,选择最合适的压缩算法。
5)高压缩比,意味着同等大小的内存能够存放更多数据,系统cache效果更好。
官方数据显示,通过使用列存,在某些分析场景下,能够获得100倍甚至更高的加速效应。
3) 向量化
ClickHouse不仅将数据按列存储,而且按列进行计算。传统OLTP数据库通常采用按行计算,原因是事务处理中以点查为主,SQL计算量小,实现这些技术的收益不够明显。但是在分析场景下,单个SQL所涉及计算量可能极大,将每行作为一个基本单元进行处理会带来严重的性能损耗:
1)对每一行数据都要调用相应的函数,函数调用开销占比高;
2)存储层按列存储数据,在内存中也按列组织,但是计算层按行处理,无法充分利用CPU cache的预读能力,造成CPU Cache miss严重;
3)按行处理,无法利用高效的SIMD指令;
ClickHouse实现了向量执行引擎(Vectorized execution engine),对内存中的列式数据,一个batch调用一次SIMD指令(而非每一行调用一次),不仅减少了函数调用次数、降低了cache miss,而且可以充分发挥SIMD指令的并行能力,大幅缩短了计算耗时。向量执行引擎,通常能够带来数倍的性能提升。
(SIMD全称Single Instruction Multiple Data,单指令多数据流,能够复制多个操作数,并把它们打包在大型寄存器的一组指令集。以同步方式,在同一时间内执行同一条指令。)
4) 表
上层数据的视图展示概念 ,包括表的基本结构和数据
5) 分区
ClickHouse支持PARTITION BY子句,在建表时可以指定按照任意合法表达式进行数据分区操作,比如通过toYYYYMM()将数据按月进行分区、toMonday()将数据按照周几进行分区、对Enum类型的列直接每种取值作为一个分区等。数据以分区的形式统一管理和维护一批数据!
6) 副本
数据存储副本,在集群模式下实现高可用 , 简单理解就是相同的数据备份,在CK中通过复制集,我们实现保障了数据可靠性外,也通过多副本的方式,增加了CK查询的并发能力。这里一般有2种方式:(1)基于ZooKeeper的表复制方式;(2)基于Cluster的复制方式。由于我们推荐的数据写入方式本地表写入,禁止分布式表写入,所以我们的复制表只考虑ZooKeeper的表复制方案。
7) 引擎 必须指定引擎
不同的引擎决定了表数据的存储特点,位置和表数据的操作行为:
-
决定表存储在哪里以及以何种方式存储
-
支持哪些查询以及如何支持
-
并发数据访问
-
索引的使用
-
是否可以执行多线程请求
-
数据是否存储副本
-
并发操作 insert into tb_x select * from tb_x ;
表引擎决定了数据在文件系统中的存储方式,常用的也是官方推荐的存储引擎是MergeTree系列,如果需要数据副本的话可以使用ReplicatedMergeTree系列,相当于MergeTree的副本版本。读取集群数据需要使用分布式表引擎Distribute。
二 clickhouse-部署
ClickHouse支持运行在主流64位CPU架构(X86、AArch和 PowerPC)的Linux操作系统之上,可以通过源码编译、预编译压缩包、Docker镜像和RPM等多种方法进行安装。
1 单节点部署
sudo yum -y install yum-utils
sudo rpm --import /
sudo yum-config-manager --add-repo /rpm/
sudo yum -y install clickhouse-server clickhouse-client
sudo /etc//clickhouse-server start -- 启动服务
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 启动交互式客户端
[root@ck1 /]# clickhouse-client -m
ClickHouse client version 20.8.3.18.
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 20.8.3 revision 54438.
ck1 :) show databases ;
- 1
- 2
- 3
- 4
- 5
2) CK目录介绍
程序在安装的过程中会自动构建整套目录结构,接下来分别说明它们的作用。
(1) /etc/clickhouse-server:
服务端的配置文件目录,包括全局配置 和用户配置等。
(2)/var/lib/clickhouse:
默认的数据存储目录(通常会修改默认路径配置,将数据保存到大容量磁盘挂载的路径)。
(3)/var/log/clickhouse-server
默认保存日志的目录(通常会修改路径配置,将日志保存到大容量磁盘挂载的路径)。
(4)/usr/bin 默认添加进系统环境变量中
find ./ -name "clickhouse*"
clickhouse:主程序的可执行文件。
clickhouse-client:一个指向ClickHouse可执行文件的软链接,供客户端连接 使用。
clickhouse-server:一个指向ClickHouse可执行文件的软链接,供服务端启动 使用。
clickhouse-compressor:内置提供的压缩工具,可用于数据的正压反解。
- 1
- 2
- 3
- 4
- 5
3) 启动
在启动之前建议首先修改CK的核心参数配置打开配置文件,修改数据保存的地址:
/etc/clickhouse-server
[root@linux03 clickhouse-server]# ll
total 44
-rw-r--r--. 1 root root 33742 Dec 8 20:47
-rw-r--r--. 1 root root 5587 Oct 5 2020
- 1
- 2
- 3
- 4
- 5
- 6
vi
<path>/chbase/data/</path>
<tmp_path>/chbase/data/tmp/</tmp_path>
<user_files_path>/chbase/data/user_files/</user_files_path>
- 1
- 2
- 3
ClickHouse的底层访问接口支持TCP和HTTP两种协议,其中,TCP 协议拥有更好的性能,其默认端口为9000,主要用于集群间的内部通信及CLI客户端;而HTTP协议则拥有更好的兼容性,可以通过REST服务的形式被广泛用于JAVA、Python等编程语言的客户端,其默认端口为8123。通常而言,并不建议用户直接使用底层接口访问ClickHouse,更为推荐的方式是通过CLI和JDBC这些封装接口,因为它们更加简单易用!
- 启动服务
clickhouse-server start
netstat -nltp | grep 9000
tcp6 0 0 :::9000 :::* LISTEN 1354/clickhouse-ser
- 1
- 2
- 3
交互式客户端
clickhouse-client -u default --password -m
- -h
- –port
- -m 交互式客户端中可以执行多行函数
- -q
# clickhouse-client -m
ClickHouse client version 20.8.3.18.
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 20.8.3 revision 54438.
linux03 :)
- 1
- 2
- 3
- 4
- 5
(1)–host/-h:服务端的地址,默认值为localhost。如果修改了 内的listen_host,则需要依靠此参数指定服务端 地址
(2)–port:服务端的TCP端口,默认值为9000。如果要修改内的tcp_port,则需要使用此参数指定。
(3)–user/-u:登录的用户名,默认值为default。如果使用非 default的其他用户名登录,则需要使用此参数指定,例如下 面所示代码。关于自定义用户的介绍将在后面介绍或者关注博客地址。 /qq_37933018?t=1
(4)–password:登录的密码,默认值为空。如果在用户定义中未设置 密码,则不需要填写(例如默认的default用户)。
(5)–database/-d:登录的数据库,默认值为default。
(6)–query/-q:只能在非交互式查询时使用,用于指定SQL语句。
(7)–multiquery/-n:在非交互式执行时,允许一次运行多条SQL语 句,多条语句之间以分号间隔。
(8)–time/-t:在非交互式执行时,会打印每条SQL的执行时间,
- 非交互式客户端
非交互式执行方式一般只执行一次 ,不进入到客户中的非操作方式 ,用户测试,数据导入, 数据导出非常方便 !
clickhouse-client -n -q
clickhouse-client -q -n 'show databases; use test1;' ;
-n 支持同时执行多个SQL语句 ,语句之间使用;号分割
-q 执行SQL语句
- 1
- 2
- 3
- 4
只用这种方式也可以实现数据的导入和数据的导出!后面我们会介绍到!!
- clickhouse-client -h linux01 报错 拒绝连接
vi
<listen_host>::</listen_host>
service clickhouse-server restart -- 重启服务
clickhouse-client -h linux01
- 1
- 2
- 3
- 4
2 集群部署
/clickhouse/rpm/testing/x86_64/
可以将rpm二进制软件安装包 下载到本地 使用 rpm命令安装
在每个节上安装ZK
三 clickhouse基础入门
基本语法演示
-- show databases ;
-- create database if not exists test1 ;
-- use test1 ;
-- select currentDatabase() ;
-- drop database test1 ;
- 1
- 2
- 3
- 4
- 5
1 数据类型
注意在CK中关键字严格区分大小写
建表的时候一般情况下要指定引擎!!!
create table tb_test1(
id Int8 ,
name String
)engine=Memory;
┌─name─┬─type───┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ id │ Int8 │ │ │ │ │ │
│ name │ String │ │ │ │ │ │
└──────┴────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
1.1 数值类型
1) IntX和UIntX
以前我们常用Tinyint、Smallint、Int和Bigint指代整数的不同取值范围。而ClickHouse则直接使用Int8、Int16、Int32和Int64指代4种大小的Int类型,其末尾的数字正好表明了占用字节的大小(8位=1字节),
ClickHouse支持无符号的整数,使用前缀U表示
create table test_int(
id Int8 ,
age UInt8 ,
cdId Int32
)engine=Memory ;
- 1
- 2
- 3
- 4
- 5
2) FloatX
注意: 和我以前的认知是一样的,这种数据类型在数据特别精准的情况下可能出现数据精度问题!
Select 8.0/0 -->inf 正无穷
Select -8.0/0 -->inf 负无穷
Select 0/0 -->nan 非数字
3) Decimal
如果要求更高精度的数值运算,则需要使用定点数。ClickHouse提 供了Decimal32、Decimal64和Decimal128三种精度的定点数。可以通过 两种形式声明定点:简写方式有Decimal32(S)、Decimal64(S)、
Decimal128(S)三种,原生方式为Decimal(P,S),其中:
·P代表精度,决定总位数(整数部分+小数部分),取值范围是1 ~38;·S代表规模,决定小数位数,取值范围是0~P
在使用两个不同精度的定点数进行四则运算的时候,它们的小数点 位数S会发生变化。
在进行加法运算时,S取最大值。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相加后S=4:
在进行减法运算时,S取最大值。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相减后S=4:
在进行乘法运算时,S取最和。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相乘后S=4+2:
在进行除法运算时,S取最大值。例如下面的查询,toDecimal64(2,4)与toDecimal32(2,2)相除后S=4:但是要保证被除数的S大于除数的S,否则会报错
create table test_decimal(
id Int8 ,
sal Decimal(5,2) -- 5 总位数 2 小数位 确定2
)engine=Memory ;
- 1
- 2
- 3
- 4
1.2 字符串类型
符串类型可以细分为String、FixedString和UUID三类。从命名来看仿佛不像是由一款数据库提供的类型,反而更像是一门编程语言的设计,没错CK语法具备编程语言的特征(数据+运算)
1) String
字符串由String定义,长度不限。因此在使用String的时候无须声明大小。它完全代替了传统意义上数据库的Varchar、Text、Clob和Blob等字符类型。String类型不限定字符集,因为它根本就没有这个概念,所以可以将任意编码的字符串存入其中。但是为了程序的规范性和可维护性,在同一套程序中应该遵循使用统一的编码,例如“统一保持UTF-8编码”就是一种很好的约定。所以在对数据操作的时候我们不在需要区关注编码和乱码问题!
2) FixedString
FixedString类型和传统意义上的Char类型有些类似,对于一些字符有明确长度的场合,可以使用固定长度的字符串。定长字符串通过FixedString(N)声明,其中N表示字符串长度。但与Char不同的是,
FixedString使用null字节填充末尾字符,而Char通常使用空格填充。比如在下面的例子中,字符串‘abc’虽然只有3位,但长度却是5,因为末尾有2位空字符填充 !
create table test_str(
name String ,
job FixedString(4) -- 最长4个字符
)engine=Memory ;
- 1
- 2
- 3
- 4
3) UUID
UUID是一种数据库常见的主键类型,在ClickHouse中直接把它作为一种数据类型。UUID共有32位,它的格式为8-4-4-4-12。如果一个UUID类型的字段在写入数据时没有被赋值,则会依照格式使用0填充
CREATE TABLE test_uuid
(
`uid` UUID,
`name` String
)
ENGINE = Log ;
DESCRIBE TABLE test_uuid
┌─name─┬─type───┬
│ uid │ UUID │
│ name │ String │
└──────┴────────┴
insert into test_uuid select generateUUIDv4() , 'zss' ;
insert into test_uuid values (generateUUIDv4() , 'zss') ;
select * from test_uuid ;
┌──────────────────────────────────uid─┬─name─┐
│ 47e39e22-d2d6-46fd-8014-7cd3321f4c7b │ zss │
└──────────────────────────────────────┴──────┘
-------------------------UUID类型的字段默认补位0-----------------------------
insert into test_uuid (name) values('hangge') ;
┌──────────────────────────────────uid─┬─name─┐
│ 47e39e22-d2d6-46fd-8014-7cd3321f4c7b │ zss │
└──────────────────────────────────────┴──────┘
┌──────────────────────────────────uid─┬─name───┐
│ 00000000-0000-0000-0000-000000000000 │ hangge │
└──────────────────────────────────────┴────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
1.3 时间类型
1) Date
Date类型不包含具体的时间信息,只精确到天,支持字符串形式写入:
CREATE TABLE test_date
(
`id` int,
`cd` Date
)
ENGINE = Memory ;
DESCRIBE TABLE test_date ;
┌─name─┬─type──┬
│ id │ Int32 │
│ ct │ Date │
└──────┴───────┴
insert into test_date vlaues(1,'2021-09-11'),(2,now()) ;
select id , ct from test_date ;
┌─id─┬─────────ct─┐
│ 1 │ 2021-09-11 │
│ 2 │ 2021-05-17 │
└────┴────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
2) DateTime
DateTime类型包含时、分、秒信息,精确到秒,支持字符串形式写入:
create table testDataTime(ctime DateTime) engine=Memory ;
insert into testDataTime values('2021-12-27 01:11:12'),(now()) ;
select * from testDataTime ;
- 1
- 2
- 3
3)DateTime64
DateTime64可以记录亚秒,它在DateTime之上增加了精度的设置
-- 建表
CREATE TABLE test_date_time64
(
`ctime` DateTime64
)
ENGINE = Memory ;
-- 建表
CREATE TABLE test_date_time64_2
(
`ctime` DateTime64(2)
)
ENGINE = Memory ;
-- 分别插入数据
insert into test_date_time64 values('2021-11-11 11:11:11'),(now()) ;
insert into test_date_time64_2 values('2021-11-11 11:11:11'),(now()) ;
-- 查询数据
SELECT *
FROM test_date_time64;
┌───────────────────ctime─┐
│ 2021-11-11 11:11:11.000 │
│ 2021-05-17 10:40:51.000 │
└─────────────────────────┘
SELECT
*, toTypeName(ctime)
FROM test_date_time64
┌───────────────────ctime─┬─toTypeName(ctime)─┐
│ 2021-11-11 11:11:11.000 │ DateTime64(3) │
│ 2021-05-17 10:40:51.000 │ DateTime64(3) │
------------------------------------------------
SELECT
*, toTypeName(ctime)
FROM test_date_time64_2
┌──────────────────ctime─┬─toTypeName(ctime)─┐
│ 2021-11-11 11:11:11.00 │ DateTime64(2) │
│ 2021-05-17 10:41:26.00 │ DateTime64(2) │
└────────────────────────┴───────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
1.4 复杂类型
1) Enum
ClickHouse支持枚举类型,这是一种在定义常量时经常会使用的数据类型。ClickHouse提供了Enum8和Enum16两种枚举类型,它们除了取值范围不同之外,别无二致。枚举固定使用(String:Int)Key/Value键值对的形式定义数据,所以Enum8和Enum16分别会对应(String:Int8)和(String:Int16)!
create table test_enum(id Int8 , color Enum('red'=1 , 'green'=2 , 'blue'=3)) engine=Memory ;
insert into test_enum values(1,'red'),(1,'red'),(2,'green');
也可以使用这种方式进行插入数据:
insert into test_enum values(3,3) ;
- 1
- 2
- 3
- 4
在定义枚举集合的时候,有几点需要注意。首先,Key和Value是不允许重复的,要保证唯一性。其次,Key和Value的值都不能为Null,但Key允许是空字符串。在写入枚举数据的时候,只会用到Key字符串部分,
注意: 其实我们可以使用字符串来替代Enum类型来存储数据,那么为什么是要使用枚举类型呢?这是出于性能的考虑。因为虽然枚举定义中的Key属于String类型,但是在后续对枚举的所有操作中(包括排序、分组、去重、过滤等),会使用Int类型的Value值 ,提高处理数据的效率!
- 限制枚举类型字段的值
- 底层存储的是对应的Int类型的数据 , 使用更小的存储空间
- 可以使用String来替代枚举 / 没有值的限定
- 插入数据的时候可以插入指定的字符串 也可以插入对应的int值
2) Array(T)
CK支持数组这种复合数据类型 , 并且数据在操作在今后的数据分析中起到非常便利的效果!数组的定义方式有两种 : array(T) [e1,e2…] , 我们在这里要求数组中的数据类型是一致的!
数组的定义
[1,2,3,4,5]
array('a' , 'b' , 'c')
[1,2,3,'hello'] -- 错误
create table test_array(
id Int8 ,
hobby Array(String)
)engine=Memory ;
insert into test_array values(1,['eat','drink','la']),(2,array('sleep','palyg','sql'));
┌─id─┬─hobby───────────────────┐
│ 1 │ ['eat','drink','la'] │
│ 2 │ ['sleep','palyg','sql'] │
└────┴─────────────────────────┘
select id , hobby , toTypeName(hobby) from test_array ;
┌─id─┬─hobby───────────────────┬─toTypeName(hobby)─┐
│ 1 │ ['eat','drink','la'] │ Array(String) │
│ 2 │ ['sleep','palyg','sql'] │ Array(String) │
└────┴─────────────────────────┴───────────────────┘
select id , hobby[2] , toTypeName(hobby) from test_array ; -- 数组的取值 [index] 1-based
select * , hobby[1] , length(hobby) from test_array ; length(arr) -- 数组的长度
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
3) Tuple
在java中封装一个用户的基本信息 (id,name,age,gender)
需要创建一个POJO/JavaBean类 UserBean , 然后将字段的值set进去 . .属性操作任意的属性
Tuple4(1,zss,23,M) 通过获取指定位置 _2 的值 操作数据
(2,lss,24,F)
元组类型由1~n个元素组成,每个元素之间允许设置不同的数据类型,且彼此之间不要求兼容。元组同样支持类型推断,其推断依据仍然以最小存储代价为原则。与数组类似,元组也可以使用两种方式定义,常规方式tuple(T):元组中可以存储多种数据类型,但是要注意数据类型的顺序
tuple(…)
(…)
col Tuple(Int8 , String …)
(‘’ , ‘’) 对偶元组 entry --> map
select tuple(1,'asb',12.23) as x , toTypeName(x) ;
┌─x───────────────┬─toTypeName(tuple(1, 'asb', 12.23))─┐
│ (1,'asb',12.23) │ Tuple(UInt8, String, Float64) │
└─────────────────┴────────────────────────────────────┘
---简写形式
SELECT
(1, 'asb', 12.23) AS x,
toTypeName(x)
┌─x───────────────┬─toTypeName(tuple(1, 'asb', 12.23))─┐
│ (1,'asb',12.23) │ Tuple(UInt8, String, Float64) │
└─────────────────┴────────────────────────────────────┘
注意:建表的时候使用元组的需要制定元组的数据类型
CREATE TABLE test_tuple (
c1 Tuple(UInt8, String, Float64)
) ENGINE = Memory;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- (1,2,3,‘abc’)
- tuple(1,2,3,‘abc’)
- col Tuple(Int8,Int8,String) – 定义泛型
- tuple(1,‘zss’,12.12)
select tupleElement(c1 , 2) from test_tuple; -- 获取元组指定位置的值
- 1
4) Nested
Nested是一种嵌套表结构。一张数据表,可以定义任意多个嵌套类型字段,但每个字段的嵌套层级只支持一级,即嵌套表内不能继续使用嵌套类型。对于简单场景的层级关系或关联关系,使用嵌套类型也是一种不错的选择。
create table test_nested(
uid Int8 ,
name String ,
props Nested(
pid Int8,
pnames String ,
pvalues String
)
)engine = Memory ;
desc test_nested ;
┌─name──────────┬─type──────────┬
│ uid │ Int8 │
│ name │ String │
│ props.pid │ Array(Int8) │
│ props.pnames │ Array(String) │
│ props.pvalues │ Array(String) │
└───────────────┴───────────────┴
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
嵌套类型本质是一种多维数组的结构。嵌套表中的每个字段都是一个数组,并且行与行之间数组的长度无须对齐。需要注意的是,在同一行数据内每个数组字段的长度必须相等。
insert into test_nested values(1,'hadoop',[1,2,3],['p1','p2','p3'],['v1','v2','v3']);
-- 行和行之间的属性的个数可以不一致 ,但是当前行的Nested类型中的数组个数必须一致
insert into test_nested values(2,'spark',[1,2],['p1','p2'],['v1','v2']);
SELECT *
FROM test_nested
┌─uid─┬─name───┬─props.pid─┬─props.pnames─────┬─props.pvalues────┐
│ 1 │ hadoop │ [1,2,3] │ ['p1','p2','p3'] │ ['v1','v2','v3'] │
└─────┴────────┴───────────┴──────────────────┴──────────────────┘
┌─uid─┬─name──┬─props.pid─┬─props.pnames─┬─props.pvalues─┐
│ 2 │ spark │ [1,2] │ ['p1','p2'] │ ['v1','v2'] │
└─────┴───────┴───────────┴──────────────┴───────────────┘
SELECT
uid,
name,
props.pid,
props.pnames[1]
FROM test_nested;
┌─uid─┬─name───┬─props.pid─┬─arrayElement(props.pnames, 1)─┐
│ 1 │ hadoop │ [1,2,3] │ p1 │
└─────┴────────┴───────────┴───────────────────────────────┘
┌─uid─┬─name──┬─props.pid─┬─arrayElement(props.pnames, 1)─┐
│ 2 │ spark │ [1,2] │ p1 │
└─────┴───────┴───────────┴───────────────────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
create table test_nested(
id Int8 ,
name String ,
scores Nested(
seq UInt8 ,
sx Float64 ,
yy Float64 ,
yw Float64
)
)engine = Memory ;
insert into test_nested values (1,'wbb',[1,2,3],[11,12,13],[14,14,11],[77,79,10]);
insert into test_nested values (2,'taoge',[1,2],[99,10],[14,40],[77,11]);
-- 注意 每行中的数组的个数一致 行和行之间可以不一直被
┌─id─┬─name─┬─scores.seq─┬─scores.sx──┬─scores.yy──┬─scores.yw──┐
│ 1 │ wbb │ [1,2,3] │ [11,12,13] │ [14,14,11] │ [77,79,10] │
└────┴──────┴────────────┴────────────┴────────────┴────────────┘
┌─id─┬─name──┬─scores.seq─┬─scores.sx─┬─scores.yy─┬─scores.yw─┐
│ 2 │ taoge │ [1,2] │ [99,10] │ [14,40] │ [77,11] │
└────┴───────┴────────────┴───────────┴───────────┴───────────┘
SELECT
name,
scores.sx
FROM test_nested;
┌─name─┬─scores.sx──┐
│ wbb │ [11,12,13] │
└──────┴────────────┘
┌─name──┬─scores.sx─┐
│ taoge │ [99,10] │
└───────┴───────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
-
和单纯的多个数组类型的区别是
- 每行数据中的每个属性数组的长度一致
5) Map
clickEvent 用户打开页面 , 点击一个按钮, 触发了点击事件
set allow_experimental_map_type = 1 ; -- 启用Map数据类型
CREATE TABLE test_map (
a Map(String, UInt64)
) ENGINE=Memory;
desc test_map ;
insert into test_map valeus({'lss':21,'zss':22,'ww':23}) ,({'lss2':21,'zss2':22,'ww2':23});
SELECT
*,
mapKeys(a),
mapValues(a),
a['lss'],
length(a)
FROM test_map
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
Map集合就是K->V 映射关系
tuple('zss',23) 元组中的元素只有两个 对偶元组 K->V
Map中内容(tuple2)
cast(v , dataType) 强制类型转换
select cast('21' , 'UInt8')+3 ;
-- 拉链操作
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map;
([1, 2, 3], ['Ready', 'Steady', 'Go'])
SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map , mapKeys(map) as ks , mapValues(map) as vs;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
6) GEO
- Point
SET allow_experimental_geo_types = 1;
CREATE TABLE geo_point (p Point) ENGINE = Memory();
INSERT INTO geo_point VALUES((10, 10));
SELECT p, toTypeName(p) FROM geo_point;
┌─p───────┬─toTypeName(p)─┐
│ (10,10) │ Point │
└─────────┴───────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- Ring
SET allow_experimental_geo_types = 1;
CREATE TABLE geo_ring (r Ring) ENGINE = Memory();
INSERT INTO geo_ring VALUES([(0, 0), (10, 0), (10, 10), (0, 10)]);
SELECT r, toTypeName(r) FROM geo_ring;
┌─r─────────────────────────────┬─toTypeName(r)─┐
│ [(0,0),(10,0),(10,10),(0,10)] │ Ring │
└───────────────────────────────┴───────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- Polygon
SET allow_experimental_geo_types = 1;
CREATE TABLE geo_polygon (pg Polygon) ENGINE = Memory();
INSERT INTO geo_polygon VALUES([[(20, 20), (50, 20), (50, 50), (20, 50)], [(30, 30), (50, 50), (50, 30)]]);
SELECT pg, toTypeName(pg) FROM geo_polygon;
- 1
- 2
- 3
- 4
- MultiPolygon
SET allow_experimental_geo_types = 1;
CREATE TABLE geo_multipolygon (mpg MultiPolygon) ENGINE = Memory();
INSERT INTO geo_multipolygon VALUES([[[(0, 0), (10, 0), (10, 10), (0, 10)]], [[(20, 20), (50, 20), (50, 50), (20, 50)],[(30, 30), (50, 50), (50, 30)]]]);
SELECT mpg, toTypeName(mpg) FROM geo_multipolygon;
- 1
- 2
- 3
- 4
7)IPV4
域名类型分为IPv4和IPv6两类,本质上它们是对整型和字符串的进一步封装。IPv4类型是基于UInt32封装的
(1)出于便捷性的考量,例如IPv4类型支持格式检查,格式错误的IP数据是无法被写入的,例如:
INSERT INTO IP4_TEST VALUES (‘’,‘192.0.0’)
Code: 441. DB::Exception: Invalid IPv4 value.
(2)出于性能的考量,同样以IPv4为例,IPv4使用UInt32存储,相比String更加紧凑,占用的空间更小,查询性能更快。IPv6类型是基于FixedString(16)封装的,它的使用方法与IPv4别无二致, 在使用Domain类型的时候还有一点需要注意,虽然它从表象上看起来与String一样,但Domain类型并不是字符串,所以它不支持隐式的自动类型转换。如果需要返回IP的字符串形式,则需要显式调用 IPv4NumToString或IPv6NumToString函数进行转换。
create table test_domain(
id Int8 ,
ip IPv4
)engine=Memory ;
insert into test_domain values(1,'192.168.133.2') ;
insert into test_domain values(1,'192.168.133') ; 在插入数据的会进行数据的检查所以这行数据会报错
-- Exception on client:
-- Code: 441. DB::Exception: Invalid IPv4 value.
-- Connecting to database doit1 at localhost:9000 as user default.
-- Connected to ClickHouse server version 20.8.3 revision 54438.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
8) Boolean和Nullable
ck中没有Boolean类型 ,使用1和0来代表true和false
Nullable 某种数据类型允许为null , 或者是没有给值的情况下模式是NULL
create table test_null(
id Int8 ,
age Int8
)engine = Memory ;
create table test_null2(
id Int8 ,
age Nullable(Int8)
)engine = Memory ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
2 基本语法
2.1 DDL基础
- 建表 指定引擎
目前只有MergeTree、Merge和Distributed这三类表引擎支持 ALTER修改,所以在进行alter操作的时候注意表的引擎!
CREATE TABLE tb_test1
(
`id` Int8,
`name` String
)
ENGINE = Memory ;
-- 只有 MergeTree支持表结构的修改
-- MergeTree一定指定主键和排序字段 order by 代表两个含义
CREATE TABLE test_alter1
(
`id` Int8,
`name` String
)
ENGINE = MergeTree()
order by id ;
-- 查看建表语句 查看引擎类型参数值
show create table test_alter1 ;
-----------------------------------
CREATE TABLE test_alter1
(
`id` Int8,
`name` String
)
ENGINE = MergeTree()
ORDER BY id
-- 参数设置 索引力度 默认是 8192
SETTINGS index_granularity = 8192;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 修改表结构
-- 查看表结构
desc tb_test1 ;
┌─name─┬─type───┬
│ id │ Int8 │
│ name │ String │
└──────┴────────┴
-- 添加字段
alter table tb_test1 add column age UInt8 ;-- 报错 , 因为修改的表引擎是内存引擎,不支持表结构的修改
-- 创建一张MergeTree引擎的表
CREATE TABLE tb_test2
(
`id` Int8,
`name` String
)
ENGINE = MergeTree()
ORDER BY id ;
┌─name─┬─type───┬
│ id │ Int8 │
│ name │ String │
└──────┴────────┴
-- 添加字段
alter table tb_test2 add column age UInt8 ;
┌─name─┬─type───┬
│ id │ Int8 │
│ name │ String │
│ age │ UInt8 │
└──────┴────────┴
alter table tb_test2 add column gender String after name ;
┌─name───┬─type───┬
│ id │ Int8 │
│ name │ String │
│ gender │ String │
│ age │ UInt8 │
└────────┴────────┴
-- 删除字段
alter table tb_test2 drop column age ;
-- 修改字段的数据类型
alter table tb_test2 modify column gender UInt8 default 0 ;
┌─name───┬─type───┬─default_type─┬─default_expression─┬
│ id │ Int8 │ │ │
│ name │ String │ │ │
│ gender │ UInt8 │ DEFAULT │ 0 │
└────────┴────────┴──────────────┴────────────────────┴
-- 作为一个优秀的程序员,表的字段使用注释一种良好的习惯, 所以建议大家在操作的时候使用注释来描述字段的意义
-- 修改 / 添加字段的注释 内部使用的编码默认是UTF8
alter table tb_test2 comment column name '用户名' ;
┌─name───┬─type───┬─default_type─┬─default_expression─┬─comment─┬
│ id │ Int8 │ │ │ │
│ name │ String │ │ │ 用户名 │
│ gender │ UInt8 │ DEFAULT │ 0 │ │
└────────┴────────┴──────────────┴────────────────────┴─────────┴
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 移动表
在Linux系统中,mv命令的本意是将一个文件从原始位置A移动到目标位置B,但是如果位 置A与位置B相同,则可以变相实现重命名的作用。ClickHouse的RENAME查询就与之有着异曲同工之妙,RENAME语句的完整语法如下所示:
-- 修改表名
rename table tb_test1 to t1 ;
-- 修改多张表名
rename table tb_test2 to t2 , t1 to tt1 ;
-- 移动表到另一数据库中
rename table t2 to test1.t ;
-- 查看数据库下的所有的表
show tables ;
show tables from db_name ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 设置表属性
-- 设置列的默认值
create table tb_test3(
id Int8 ,
name String comment '用户名' ,
role String comment '角色' default 'VIP'
)engine = Log ;
┌─name─┬─type───┬─default_type─┬─default_expression─┬
│ id │ Int8 │ │ │
│ name │ String │ │ │
│ role │ String │ DEFAULT │ 'VIP' │
└──────┴────────┴──────────────┴────────────────────┴
insert into tb_test3 (id , name) values(1,'HANGGE') ;
SELECT *
FROM tb_test3 ;
┌─id─┬─name───┬─role─┐
│ 1 │ HANGGE │ VIP │
└────┴────────┴──────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
2.2 DML基础
1) 插入数据
INSERT语句支持三种语法范式,三种范式各有不同,可以根据写入的需求灵活运用。
第一种方式
使用VALUES格式的常规语法
INSERT INTO [db.]table [(c1, c2, c3…)] VALUES (v11, v12, v13…), (v21, v22, v23…), …
其中,c1、c2、c3是列字段声明,可省略。VALUES后紧跟的是由元组组成的待写入数据,通过下标位 与列字段声明一一对应。数据支持批量声明写入,多行数据之间使用逗号分隔
第二种方式
静态数据: cat user.txt
1,zss,23,BJ,M
2,lss,33,NJ,M
3,ww,21,SH,F
create table test_load1(
id UInt8 ,
name String ,
age UInt8 ,
city String ,
gender String
)engine=Log ;
-- 将数据导入到表中
cat user.txt | clickhouse-client -q 'insert into default.test_load1 format CSV' --password
clickhouse-client -q 'insert into default.test_load1 format CSV' < user.txt
上面的两种方式都可以将数据导入到表中
-- 我们还可以执行数据行属性的分割符
clickhouse-client --format_csv_delimiter='-' -q 'insert into default.test_load1 format CSV' <
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
/docs/en/interfaces/formats/#formats 支持的数据文件格式
1,a,2017-02-05,200
1,a,2017-02-06,300
1,a,2017-02-07,200
1,a,2017-02-08,400
1,a,2017-02-10,600
1,b,2017-02-05,200
1,b,2017-02-06,300
1,b,2017-02-08,200
1,b,2017-02-09,400
1,b,2017-02-10,600
1,c,2017-01-31,200
1,c,2017-02-01,300
1,c,2017-02-02,200
1,c,2017-02-03,400
1,c,2017-02-10,600
1,a,2017-03-01,200
1,a,2017-03-02,300
1,a,2017-03-03,200
1,a,2017-03-04,400
1,a,2017-03-05,600
create table tb_orders(
id Int8 ,
name String ,
cdate Date ,
money Decimal(5,2)
)
engine = MergeTree
primary key id
order by (id , cdate)
partition by name ;
--导入数据
cat | clickhouse-client -q 'insert into tb_orders format CSV' --password
┌─id─┬─name─┬──────cdate─┬──money─┐
│ 1 │ c │ 2017-01-31 │ 200.00 │
│ 1 │ c │ 2017-02-01 │ 300.00 │
│ 1 │ c │ 2017-02-02 │ 200.00 │
│ 1 │ c │ 2017-02-03 │ 400.00 │
│ 1 │ c │ 2017-02-10 │ 600.00 │
└────┴──────┴────────────┴────────┘
┌─id─┬─name─┬──────cdate─┬──money─┐
│ 1 │ a │ 2016-01-01 │ 300.00 │
│ 1 │ a │ 2017-02-05 │ 200.00 │
│ 1 │ a │ 2017-02-06 │ 300.00 │
│ 1 │ a │ 2017-02-07 │ 200.00 │
│ 1 │ a │ 2017-02-08 │ 400.00 │
│ 1 │ a │ 2017-02-10 │ 600.00 │
│ 1 │ a │ 2017-03-01 │ 200.00 │
│ 1 │ a │ 2017-03-02 │ 300.00 │
│ 1 │ a │ 2017-03-03 │ 200.00 │
│ 1 │ a │ 2017-03-04 │ 400.00 │
│ 1 │ a │ 2017-03-05 │ 600.00 │
└────┴──────┴────────────┴────────┘
┌─id─┬─name─┬──────cdate─┬──money─┐
│ 1 │ b │ 2017-02-05 │ 200.00 │
│ 1 │ b │ 2017-02-06 │ 300.00 │
│ 1 │ b │ 2017-02-08 │ 200.00 │
│ 1 │ b │ 2017-02-09 │ 400.00 │
│ 1 │ b │ 2017-02-10 │ 600.00 │
│ 1 │ b │ 2020-01-01 │ 300.00 │
└────┴──────┴────────────┴────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
第三种方式
INSERT INTO [db.]table [(c1, c2, c3…)] SELECT …
虽然VALUES和SELECT子句的形式都支持声明表达式或函数,但是表达式和函数会带来额外的性能开销,从而导致写入性能的下降。所以如果追求极致的写入性能,就应该尽可能避免使用它们。
create table log3 as log2 ;
Insert into log3 select * from log2 ;
create table tb_res engine = Log as select * from tb_ds ;
-- create table tb_name like tb_ds ; -- 不支持
- 1
- 2
- 3
- 4
ClickHouse内部所有的数据操作都是面向Block数据块的,所以INSERT查询最终会将数据转换为Block数据块。也正因如此,INSERT语句在单个数据块的写入过程中是具有原子性的。在默认的情况下,每个数据块最多可以写入1048576行数据(由max_insert_block_size参数控制)。也就是说,如果一条INSERT语句写入的数据少于max_insert_block_size行,那么这批数据的写入是具有原子性的,即要么全部成功,要么全部失败。需要注意的是,只有在ClickHouse服务端处理数据的时候才具有这种原子写入的特性,例如使用JDBC或者HTTP接口时。因为max_insert_block_size参数在使用CLI命令行或 者INSERT SELECT子句写入时是不生效的。
2) 更新删除数据
[一般不会操作] olap 查询多
如果是MergeTree引擎的表
可以删除分区 重新导入
可以根据条件删除数据 根据条件更新数据 alter table delete/ update where
(mutation操作)
可以使用CK中提供的特殊的引擎实现数据的删除和更新操作 CollapsingMergeTree VersionedCollapsingMergeTree
ClickHouse提供了DELETE和UPDATE的能力,这类操作被称为Mutation查询,它可以看作ALTER语句的变种。虽然Mutation能最终实现修改和删除,但不能完全以通常意义上的UPDATE和DELETE来理解,我们必须清醒地认识到它的不同:首先,Mutation语句是一种“很重”的操作,更适用于批量数据的修改和删除;其次,它不支持事务,一旦语句被提交执行,就会立刻对现有数据产生影响,无法回滚;最后, Mutation语句的执行是一个异步的后台过程,语句被提交之后就会立即返回。所以这并不代表具体逻辑已经执行完毕,它的具体执行进度需要通过系统表查询。注意数据的修改和删除操作是使用用MergeTree家族引擎:
只有MergeTree引擎的数据才能修改
删除分区数据 修改整个分区的数据
-- 创建表
create table test_muta(
id UInt8 ,
name String ,
city String
)engine=MergeTree()
partition by city
order by id ;
-- 导入数据
clickhouse-client -q 'insert into test_muta format CSV' < data.csv
-- 删除分区数据
alter table test_muta drop partition 'SH' ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
条件删除数据
alter table test_muta delete where id=3 ; -- 一定加条件
- 1
条件更新数据
ALTER TABLE [db_name.]table_name UPDATE column1 = expr1 [, ...] WHERE filter_expr
ALTER TABLE test_ud
UPDATE name = 'my', job = 'teacher' WHERE id = '2' ;
alter table test_muta update name='李思思' where id=3 ;
- 1
- 2
- 3
- 4
- 5
但是注意的时候一定指定where条否则会报错,这种语法的where条件也可以使用子查询 ;
2.3 分区表操作
目前只有MergeTree系列 的表引擎支持数据分区,分区的基本概念和意义和hive中的意义一样,这里不过多赘述!
区内排序 , 合并 ,去重
create table test_partition1(
id String ,
ctime DateTime
)engine=MergeTree()
partition by toYYYYMM(ctime)
order by (id) ;
-- 查看建表语句
show create table test_partition1;
CREATE TABLE default.test_partition1
(
`id` String,
`ctime` DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(ctime)
ORDER BY id
SETTINGS index_granularity = 8192 -- 索引粒度 稀疏索引
-- 插入数据
insert into test_partition1 values(1,now()) ,(2,'2021-06-11 11:12:13') ;
-- 查看数据
SELECT *
FROM test_partition1 ;
┌─id─┬───────────────ctime─┐
│ 2 │ 2021-06-11 11:12:13 │
└────┴─────────────────────┘
┌─id─┬───────────────ctime─┐
│ 1 │ 2021-05-19 13:38:29 │
└────┴─────────────────────┘
-- 查看表中的分区
ClickHouse内置了许多system系统表,用于查询自身的状态信息。 其中parts系统表专门用于查询数据表的分区信息。
SELECT
name,
table,
partition
FROM system.parts
WHERE table = 'test_partition1' ;
┌─name─────────┬─table───────────┬─partition─┐
│ 202105_1_1_0 │ test_partition1 │ 202105 │
│ 202106_2_2_0 │ test_partition1 │ 202106 │
└──────────────┴─────────────────┴───────────┘
insert into test_partition1 values(1,now()) ,(2,'2021-06-12 11:12:13') ;
┌─name─────────┬─table───────────┬─partition─┐
│ 202105_1_1_0 │ test_partition1 │ 202105 │
│ 202105_3_3_0 │ test_partition1 │ 202105 │
│ 202106_2_2_0 │ test_partition1 │ 202106 │
│ 202106_4_4_0 │ test_partition1 │ 202106 │
└──────────────┴─────────────────┴───────────┘
-- 删除分区
alter table test_partition1 drop partition '202109' ;
删除分区以后 , 分区中的所有的数据全部删除
SELECT
name,
table,
partition
FROM system.parts
WHERE table = 'test_partition1'
┌─name─────────┬─table───────────┬─partition─┐
│ 202106_2_2_0 │ test_partition1 │ 202106 │
│ 202106_4_4_0 │ test_partition1 │ 202106 │
└──────────────┴─────────────────┴───────────┘
SELECT *
FROM test_partition1
┌─id─┬───────────────ctime─┐
│ 2 │ 2021-06-12 11:12:13 │
└────┴─────────────────────┘
┌─id─┬───────────────ctime─┐
│ 2 │ 2021-06-11 11:12:13 │
└────┴─────────────────────┘
-- 复制分区
create table tb_y as tb_x ;
clickHouse支持将A表的分区数据复制到B表,这项特性可以用于快速数据写入、多表间数据同步和备份等场景,它的完整语法如下:
ALTER TABLE B REPLACE PARTITION partition_expr FROM A
ALTER TABLE test_partition2 REPLACE PARTITION '202107' FROM test_partition1 ;
不过需要注意的是,并不是任意数据表之间都能够相互复制,它们还需要满足两个前提 条件:
·两张表需要拥有相同的分区键
·它们的表结构完全相同。
create table test_partition2 as test_partition1 ;
show create table test_partition2 ; -- 查看表2的建表语句
CREATE TABLE default.test_partition2 as test_partition1 ;
│ CREATE TABLE default.test_partition2
(
`id` String,
`ctime` DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(ctime)
ORDER BY id
SETTINGS index_granularity = 8192 │ -- 两张表的结构完全一致
-- 复制一张表的分区到另一张表中
SELECT *
FROM test_partition2
┌─id─┬───────────────ctime─┐
│ 2 │ 2021-06-12 11:12:13 │
└────┴─────────────────────┘
┌─id─┬───────────────ctime─┐
│ 2 │ 2021-06-11 11:12:13 │
└────┴─────────────────────┘
┌─id─┬───────────────ctime─┐
│ 2 │ 2021-06-21 11:12:13 │
└────┴─────────────────────┘
----------------------------
alter table test_partition2 replace partition '202106' from test_partition1
alter table test_muta2 replace partition 'BJ' from test_muta ;
SELECT
name,
table,
partition
FROM system.parts
WHERE table = 'test_partition2'
┌─name─────────┬─table───────────┬─partition─┐
│ 202106_2_2_0 │ test_partition2 │ 202106 │
│ 202106_3_3_0 │ test_partition2 │ 202106 │
│ 202106_4_4_0 │ test_partition2 │ 202106 │
└──────────────┴─────────────────┴───────────┘
-- 重置分区数据
如果数据表某一列的数据有误,需要将其重置为初始值,如果设置了默认值那么就是默认值数据,如果没有设置默认值,系统会给出默认的初始值,此时可以使用下面的语句实现:
ALTER TABLE tb_name CLEAR COLUMN column_name IN PARTITION partition_expr ;
注意: 不能重置主键和分区字段
示例:
alter table test_rep clear column name in partition '202105' ;
alter table test_muta clear column name in partition 'BJ' ;
-- 卸载分区
表分区可以通过DETACH语句卸载,分区被卸载后,它的物理数据并没有删除,而是被转移到了当前数据表目录的detached子目录下。而装载分区则是反向操作,它能够将detached子目录下的某个分区重新装载回去。卸载与装载这一对伴生的操作,常用于分区数据的迁移和备份场景
┌─id─┬─name─┬───────────────ctime─┐
│ 1 │ │ 2021-05-19 13:59:49 │
│ 2 │ │ 2021-05-19 13:59:49 │
└────┴──────┴─────────────────────┘
┌─id─┬─name─┬───────────────ctime─┐
│ 3 │ ww │ 2021-04-11 11:12:13 │
└────┴──────┴─────────────────────┘
alter table test_rep detach partition '202105' ;
alter table test_muta detach partition 'BJ' ;
┌─id─┬─name─┬───────────────ctime─┐
│ 3 │ ww │ 2021-04-11 11:12:13 │
└────┴──────┴─────────────────────┘
-- 装载分区
alter table test_rep attach partition '202105' ;
alter table test_muta attach partition 'BJ' ;
┌─id─┬─name─┬───────────────ctime─┐
│ 1 │ │ 2021-05-19 13:59:49 │
│ 2 │ │ 2021-05-19 13:59:49 │
└────┴──────┴─────────────────────┘
┌─id─┬─name─┬───────────────ctime─┐
│ 3 │ ww │ 2021-04-11 11:12:13 │
└────┴──────┴─────────────────────┘
-- 记住,一旦分区被移动到了detached子目录,就代表它已经脱离了ClickHouse的管理,ClickHouse并不会主动清理这些文件。这些分区文件会一直存在,除非我们主动删除或者使用ATTACH语句重新装载
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 删除分区
- 添加分区
- 复制分区数据
- 卸载分区
- 装载分区
2.4 视图
1) 普通视图
ClickHouse拥有普通和物化两种视图,其中物化视图拥有独立的存储**,而普通视图只是一层简单的查询代理**
CREATE VIEW [IF NOT EXISTS] [db_name.]view_name AS SELECT ...
- 1
普通视图不会存储任何数据,它只是一层单纯的SELECT查询映射,起着简化查询、明晰语义的作用,对查询性能不会有任何增强。
create view test3_view as select id , upper(name) , role from tb_test3 ;
┌─name────────────┐
│ tb_test3 │
│ test3_view │
│ test_partition1 │
│ test_partition2 │
│ test_rep │
│ tt1 │
└─────────────────┘
drop view test3_view ; -- 删除视图
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
2) 物化视图
物化视图支持表引擎,数据保存形式由它的表引擎决定,创建物化视图的完整语法如下所示
create materialized view mv_log engine=Log populate as select * from log ;
物化视图创建好之后,如果源表被写入新数据,那么物化视图也会同步更新。POPULATE修饰符决定了物化视图的初始化策略:如果使用了POPULATE修饰符,那么在创建视图的过程中,会连带将源表中已存在的数据一并导入,如同执行了INTO SELECT 一般;反之,如果不使用POPULATE修饰符,那么物化视图在创建之后是没有数据的,它只会同步在此之后被写入源表的数据。物化视图目前并不支持同步删除,如果在源表中删除了数据,物化视图的数据仍会保留。
create materialized view test3_view engine = Log populate as select * from tb_test3 ;
-- 建表的时候同步数据 , 当数据更新以后 物化视图中的数据会同步更新 , 但是当删除数据以后,物化视图中的数据不会被删除
SELECT *
FROM test3_view ;
┌─id─┬─name───┬─role─┐
│ 1 │ HANGGE │ VIP │
│ 2 │ BENGE │ VIP │
│ 3 │ PINGGE │ VIP │
└────┴────────┴──────┘
-- 向源表中擦混入数据
SELECT *
FROM test3_view
┌─id─┬─name──┬─role─┐
│ 4 │ TAOGE │ VIP │
└────┴───────┴──────┘
┌─id─┬─name───┬─role─┐
│ 1 │ HANGGE │ VIP │
│ 2 │ BENGE │ VIP │
│ 3 │ PINGGE │ VIP │
└────┴────────┴──────┘
-- 删除源表中的数据 , 物化视图中的数据 不会变化 ****
注意: 数据删除语法只适用于MergeTree引擎的表 基本语法如下
ALTER TABLE db_name.table_name DROP PARTITION '20210601'
ALTER TABLE db_name.table_name DELETE WHERE day = '20210618'
ALTER TABLE <table_name> UPDATE col1 = expr1, ... WHERE <filter>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
Show tables ; 其实物化视图就是一种特殊的表
四 引擎详解
表引擎是ClickHouse设计实现中的一大特色 ,数据表拥有何种特性、数据以何 种形式被存储以及如何被加载。ClickHouse拥有非常庞大的表引擎体 系,截至本书完成时,其共拥有合并树、外部存储、内存、文件、接口 和其他6大类20多种表引擎。而在这众多的表引擎中,又属合并树 (MergeTree)表引擎及其家族系列(*MergeTree)最为强大,在生产 环境的绝大部分场景中,都会使用此系列的表引擎。因为只有合并树系 列的表引擎才支持主键索引、数据分区、数据副本和数据采样这些特 性,同时也只有此系列的表引擎支持ALTER相关操作。
合并树家族自身也拥有多种表引擎的变种。其中MergeTree作为家 族中最基础的表引擎,提供了主键索引、数据分区、数据副本和数据采 样等基本能力,而家族中其他的表引擎则在MergeTree的基础之上各有 所长。例如ReplacingMergeTree表引擎具有删除重复数据的特性,而 SummingMergeTree表引擎则会按照排序键自动聚合数据。如果给合并树 系列的表引擎加上Replicated前缀,又会得到一组支持数据副本的表引 擎,例如ReplicatedMergeTree、ReplicatedReplacingMergeTree、 ReplicatedSummingMergeTree等。
表引擎(即表的类型)决定了:
- 数据的存储方式和位置,写到哪里以及从哪里读取数据
- 支持哪些查询以及如何支持。
- 并发数据访问。
- 索引的使用(如果存在)。
- 是否可以执行多线程请求。
- 数据复制参数,是否可以存储数据副本。
- 分布式引擎 实现分布式
… …
1 Log系列引擎
Log家族具有最小功能的[轻量级引擎。当您需要快速写入许多小表(最多约100万行)并在以后整体读取它们时,该类型的引擎是最有效的。
1.1 TinyLog引擎
最简单的表引擎,用于将数据存储在磁盘上。每列都存储在单独的压缩文件中,写入时,数据将附加到文件末尾。该引擎没有并发控制
1、最简单的引擎
2、没有索引,没有标记块
3、写是追加写
4、数据以列字段文件存储
5、不允许同时读写
- 1
- 2
- 3
- 4
- 5
-- 建表
create table test_tinylog(
id UInt8 ,
name String ,
age UInt8
)engine=TinyLog ;
-- 查看表结构
desc test_tinylog ;
-- 查看建表语句
SHOW CREATE TABLE test_tinylog ;
-- 插入数据
insert into test_tinylog values(1,'liubei',45),(2,'guanyu',43),(3,'zhangfei',41) ;
SELECT *
FROM test_tinylog
┌─id─┬─name─────┬─age─┐
│ 1 │ liubei │ 45 │
│ 2 │ guanyu │ 43 │
│ 3 │ zhangfei │ 41 │
└────┴──────────┴─────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
查看数底层存储
[root@doit01 test_tinylog]# pwd
/var/lib/clickhouse/data/default/test_tinylog
-rw-r-----. 1 clickhouse clickhouse 29 May 19 15:29
-rw-r-----. 1 clickhouse clickhouse 29 May 19 15:29
-rw-r-----. 1 clickhouse clickhouse 50 May 19 15:29
-rw-r-----. 1 clickhouse clickhouse 90 May 19 15:29
-- 当再次插入数据以后 , 在每个文件中追加写入的
-rw-r-----. 1 clickhouse clickhouse 58 May 19 15:31
-rw-r-----. 1 clickhouse clickhouse 58 May 19 15:31
-rw-r-----. 1 clickhouse clickhouse 100 May 19 15:31
-rw-r-----. 1 clickhouse clickhouse 91 May 19 15:31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
insert into t select * from t 会将表存储结构损坏 : 删除表目录 删除元数据
1.2 StripeLog引擎
1、存储所有数据
2、 对数据建立索引
3、 数据大小
4、并发读写
- 1
- 2
- 3
- 4
create table test_stripelog(
id UInt8 ,
name String ,
age UInt8
)engine=StripeLog ;
-- 插入数据
insert into test_stripelog values(1,'liubei',45),(2,'guanyu',43),(3,'zhangfei',41) ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
查看底层数据
/var/lib/clickhouse/data/default/test_stripelog
-rw-r-----. 1 clickhouse clickhouse 167 May 19 15:43 存储所有列的数据
-rw-r-----. 1 clickhouse clickhouse 75 May 19 15:43 记录数据的索引信息
-rw-r-----. 1 clickhouse clickhouse 68 May 19 15:43 记录文件内容的大小
- 1
- 2
- 3
- 4
1.3 Log引擎
日志与 TinyLog 的不同之处在于,«标记» 的小文件与列文件存在一起。这些标记写在每个数据块上,并且包含偏移量,这些偏移量指示从哪里开始读取文件以便跳过指定的行数。这使得可以在多个线程中读取表数据。对于并发数据访问,可以同时执行读取操作,而写入操作则阻塞读取和其它写入。Log 引擎不支持索引。同样,如果写入表失败,则该表将被破坏,并且从该表读取将返回错误。Log 引擎适用于临时数据,write-once 表以及测试或演示目的。
1、*.bin存储每个字段的数据
2、 数据块标记
3、支持多线程处理
4、并发读写
- 1
- 2
- 3
- 4
drop table if exists test_log ;
create table test_log(
id UInt8 ,
name String ,
age UInt8
)engine=Log ;
insert into test_log values(1,'liubei',45),(2,'guanyu',43),(3,'zhangfei',41) ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
查看数据
-rw-r-----. 1 clickhouse clickhouse 29 May 19 15:46
-rw-r-----. 1 clickhouse clickhouse 29 May 19 15:46
-rw-r-----. 1 clickhouse clickhouse 48 May 19 15:46 __marks.mrk ***
-rw-r-----. 1 clickhouse clickhouse 50 May 19 15:46
-rw-r-----. 1 clickhouse clickhouse 120 May 19 15:46
- 1
- 2
- 3
- 4
- 5
Log 和 StripeLog 引擎支持:
并发访问数据的锁。
INSERT
请求执行过程中表会被锁定,并且其他的读写数据的请求都会等待直到锁定被解除。如果没有写数据的请求,任意数量的读请求都可以并发执行。
并行读取数据。
在读取数据时,ClickHouse 使用多线程。 每个线程处理不同的数据块。
Log 引擎为表中的每一列使用不同的文件。StripeLog 将所有的数据存储在一个文件中。因此 StripeLog 引擎在操作系统中使用更少的描述符,但是 Log 引擎提供更高的读性能。
TinyLog 引擎是该系列中最简单的引擎并且提供了最少的功能和最低的性能。TingLog 引擎不支持并行读取和并发数据访问,并将每一列存储在不同的文件中。它比其余两种支持并行读取的引擎的读取速度更慢,并且使用了和 Log 引擎同样多的描述符。你可以在简单的低负载的情景下使用它。
2 MergeTree系列引擎
MergeTree系列的表引擎是ClickHouse数据存储功能的核心。它们提供了用于弹性和高性能数据检索的大多数功能:列存储,自定义分区,稀疏的主索引,辅助数据跳过索引等。
基本[MergeTree表引擎可以被认为是单节点ClickHouse实例的默认表引擎,因为它在各种用例中通用且实用。
除了基础表引擎MergeTree之 外,常用的表引擎还有ReplacingMergeTree、SummingMergeTree、 AggregatingMergeTree、CollapsingMergeTree和 VersionedCollapsingMergeTree。每一种合并树的变种,在继承了基 础MergeTree的能力之后,又增加了独有的特性。其名称中的“合并” 二字奠定了所有类型MergeTree的基因,它们的所有特殊逻辑,都是在 触发合并的过程中被激活的。
主要特点:
- 存储按主键排序的数据。
这使您可以创建一个小的稀疏索引,以帮助更快地查找数据。
- 如果指定了[分区键]则可以使用[分区)。
ClickHouse支持的某些分区操作比对相同数据,相同结果的常规操作更有效。ClickHouse还会自动切断在查询中指定了分区键的分区数据。这也提高了查询性能。
- 数据复制支持。
ReplicatedMergeTree表族提供数据复制。有关更多信息.
- 数据采样支持。
如有必要,可以在表中设置数据采样方法。
2.1 MergeTree引擎
MergeTree在写入一批数据时,数据总会以数据片段的形式写入磁盘,且数据片段不可修改。为了避免片段过多,ClickHouse会通过后台线程,定期合并这些数据片段,属于相同分区的数据片段会被合成 一个新的片段。这种数据片段往复合并的特点,也正是合并树名称的由来。
语法
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster1](
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, ...]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
MergeTree表引擎除了常规参数之外,还拥有一些独有的配置选 项。接下来会着重介绍其中几个重要的参数,
(1)PARTITION BY [选填]:分区键,用于指定表数据以何种标 准进行分区。分区键既可以是单个列字段,也可以通过元组的形式使 用多个列字段,同时它也支持使用列表达式。如果不声明分区键,则 ClickHouse会生成一个名为all的分区。合理使用数据分区,可以有效 减少查询时数据文件的扫描范围,更多关于数据分区的细节会在6.2节 介绍。
(2)ORDER BY [必填]:排序键,用于指定在一个数据片段内, 数据以何种标准排序。默认情况下主键(PRIMARY KEY)与排序键相 同。排序键既可以是单个列字段,例如ORDER BY CounterID,也可以 通过元组的形式使用多个列字段,例如ORDER BY(CounterID,EventDate)。当使用多个列字段排序时,以ORDER BY(CounterID,EventDate)为例,在单个数据片段内,数据首先会以 CounterID排序,相同CounterID的数据再按EventDate排序。
(3)PRIMARY KEY [选填]:主键,顾名思义,声明后会依照主键 字段生成一级索引,用于加速表查询。默认情况下,主键与排序键 (ORDER BY)相同,所以通常直接使用ORDER BY代为指定主键,无须刻 意通过PRIMARY KEY声明。所以在一般情况下,在单个数据片段内,数 据与一级索引以相同的规则升序排列。与其他数据库不同,MergeTree 主键允许存在重复数据(ReplacingMergeTree可以去重)。
(4)SAMPLE BY [选填]:抽样表达式,用于声明数据以何种标准 进行采样。如果使用了此配置项,那么在主键的配置中也需要声明同 样的表达式,例如:
省略... ) ENGINE = MergeTree() ORDER BY (CounterID, EventDate, intHash32(UserID) SAMPLE BY intHash32(UserID)
- 1
- 2
- 3
(5)SETTINGS:index_granularity [选填]: index_granularity对于MergeTree而言是一项非常重要的参数,它表 示索引的粒度,默认值为8192。也就是说,MergeTree的索引在默认情 况下,每间隔8192行数据才生成一条索引,其具体声明方式如下所 示:
index1 -----> data1
index2------>data2
index3------>data3
省略... ) ENGINE = MergeTree() 省略... SETTINGS index_granularity = 8192; -- 调大
- 1
- 2
- 3
- 4
8192是一个神奇的数字,在ClickHouse中大量数值参数都有它的 影子,可以被其整除(例如最小压缩块大小 min_compress_block_size:65536)。通常情况下并不需要修改此参 数,但理解它的工作原理有助于我们更好地使用MergeTree。关于索引 详细的工作原理会在后续阐述。
(6)SETTINGS:index_granularity_bytes [选填]:在19.11版本之前,ClickHouse只支持固定大小的索引间隔,由 index_granularity控制,默认为8192。在新版本中,它增加了自适应 间隔大小的特性,即根据每一批次写入数据的体量大小,动态划分间 隔大小。而数据的体量大小,正是由index_granularity_bytes参数控 制的,默认为10M(10×1024×1024),设置为0表示不启动自适应功 能。
(7)SETTINGS:enable_mixed_granularity_parts [选填]:设 置是否开启自适应索引间隔的功能,默认开启。
(8)SETTINGS:merge_with_ttl_timeout [选填]:从19.6版本 开始,MergeTree提供了数据TTL的功能,
(9)SETTINGS:storage_policy [选填]:从19.15版本开始, MergeTree提供了多路径的存储策略,关于这部分的详细介绍,
1) 创建表
drop table if exists tb_merge_tree ;
create table tb_merge_tree(
id Int8 ,
city String ,
ctime Date
)
engine=MergeTree()
order by id
partition by city ;
-- 查看建表语句
│ CREATE TABLE default.tb_merge_tree
(
`id` Int8,
`city` String,
`ctime` Date
)
ENGINE = MergeTree()
PARTITION BY city
ORDER BY id
SETTINGS index_granularity = 8192 │
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
2)导入数据
insert into tb_merge_tree values(1,'BJ',now()) ,(2,'NJ',now()),(3,'DJ',now());
insert into tb_merge_tree values(4,'BJ',now()) ,(5,'NJ',now()),(6,'DJ',now());
insert into tb_merge_tree values(7,'BJ',now()) ,(8,'NJ',now()),(9,'DJ',now());
insert into tb_merge_tree values(10,'BJ',now()) ,(11,'NJ',now()),(12,'DJ',now());
┌─id─┬─city─┬──────ctime─┐
│ 9 │ DJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 2 │ NJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 5 │ NJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 12 │ DJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 8 │ NJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 11 │ NJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 1 │ BJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 3 │ DJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 4 │ BJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 6 │ DJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 7 │ BJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 10 │ BJ │ 2021-05-19 │
└────┴──────┴────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
3)合并数据
optimize table tb_merge_tree final ; 一次性按照分区合并所有的数据
SELECT *
FROM tb_merge_tree
┌─id─┬─city─┬──────ctime─┐
│ 3 │ DJ │ 2021-05-19 │
│ 6 │ DJ │ 2021-05-19 │
│ 9 │ DJ │ 2021-05-19 │
│ 12 │ DJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 2 │ NJ │ 2021-05-19 │
│ 5 │ NJ │ 2021-05-19 │
│ 8 │ NJ │ 2021-05-19 │
│ 11 │ NJ │ 2021-05-19 │
└────┴──────┴────────────┘
┌─id─┬─city─┬──────ctime─┐
│ 1 │ BJ │ 2021-05-19 │
│ 4 │ BJ │ 2021-05-19 │
│ 7 │ BJ │ 2021-05-19 │
│ 10 │ BJ │ 2021-05-19 │
└────┴──────┴────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
CK内部会自动的合并分区的数据, 也会删除多余的文件夹中的数据
4)数据存储原理
# 合并前
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_11_11_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_2_2_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_5_5_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_8_8_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_12_12_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_3_3_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_6_6_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_9_9_0
drwxr-x---. 2 clickhouse clickhouse 6 May 19 16:48 detached
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_10_10_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_1_1_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_4_4_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_7_7_0
-rw-r-----. 1 clickhouse clickhouse 1 May 19 16:48 format_version.txt
# 合并后
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_11_11_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:49 0b1654565b11c57ce8e06fba0d990406_2_11_1
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_2_2_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_5_5_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 0b1654565b11c57ce8e06fba0d990406_8_8_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_12_12_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:49 4ac8f272bc049477e80a3f42338ca531_3_12_1
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_3_3_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_6_6_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 4ac8f272bc049477e80a3f42338ca531_9_9_0
drwxr-x---. 2 clickhouse clickhouse 6 May 19 16:48 detached
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_10_10_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_1_1_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:49 e35d0ca9d946a627c9fc98b8f80391ce_1_10_1
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_4_4_0
drwxr-x---. 2 clickhouse clickhouse 223 May 19 16:48 e35d0ca9d946a627c9fc98b8f80391ce_7_7_0
-rw-r-----. 1 clickhouse clickhouse 1 May 19 16:48 format_version.txt
-- 进入到一个分区目录中查看
-rw-r-----. 1 clickhouse clickhouse 385 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 38 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 48 May 19 17:12 city.mrk2
-rw-r-----. 1 clickhouse clickhouse 74 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 1 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 34 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 48 May 19 17:12 ctime.mrk2
-rw-r-----. 1 clickhouse clickhouse 30 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 48 May 19 17:12 id.mrk2
-rw-r-----. 1 clickhouse clickhouse 6 May 19 17:12 minmax_city.idx
-rw-r-----. 1 clickhouse clickhouse 3 May 19 17:12
-rw-r-----. 1 clickhouse clickhouse 2 May 19 17:12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
(1)partition:分区目录,余下各类数据文件(、 [Column].mrk、[Column].bin等)都是以分区目录的形式被组织存放 的,属于相同分区的数据,最终会被合并到同一个分区目录,而不同分 区的数据,永远不会被合并在一起。
(2):校验文件,使用二进制格式存储。它保存了余下各类文件(、等)的size大小及size的哈希值,用于快速校验文件的完整性和正确性。
(3):列信息文件,使用明文格式存储。用于保存此数据分区下的列字段信息,例如:
(4):计数文件,使用明文格式存储。用于记录当前数 据分区目录下数据的总行数
(5):一级索引文件,使用二进制格式存储。用于存放稀疏索引,一张MergeTree表只能声明一次一级索引(通过ORDER BY 或者PRIMARY KEY)。借助稀疏索引,在数据查询的时能够排除主键条 件范围之外的数据文件,从而有效减少数据扫描范围,加速查询速度。
(6)[Column].bin:数据文件,使用压缩格式存储,默认为LZ4压缩格式,用于存储某一列的数据。由于MergeTree采用列式存储,所以每一个列字段都拥有独立的.bin数据文件,并以列字段名称命名(例如 、等)。
(7)[Column].mrk:列字段标记文件,使用二进制格式存储。标记文件中保存了.bin文件中数据的偏移量信息。标记文件与稀疏索引对齐,又与.bin文件一一对应,所以MergeTree通过标记文件建立了 稀疏索引与.bin数据文件之间的映射关系。即首先通过稀疏索引()找到对应数据的偏移量信息(.mrk),再通过 偏移量直接从.bin文件中读取数据。由于.mrk标记文件与.bin文件一一对应,所以MergeTree中的每个列字段都会拥有与其对应的.mrk标记文件(例如、等)。
(8)[Column].mrk2:如果使用了自适应大小的索引间隔.则标记 文件会以.mrk2命名。它的工作原理和作用与.mrk标记文件相同。
(9)与minmax_[Column].idx:如果使用了分区键,例如PARTITION BY EventTime,则会额外生成与 minmax索引文件,它们均使用二进制格式存储。用于保 存当前分区下分区表达式最终生成的值;而minmax索引用于记录当前分 区下分区字段对应原始数据的最小和最大值。例如EventTime字段对应 的原始数据为2019-05-01、2019-05-05,分区表达式为PARTITION BY toYYYYMM(EventTime)。中保存的值将会是2019-05,而 minmax索引中保存的值将会是2019-05-012019-05-05。
在这些分区索引的作用下,进行数据查询时能够快速跳过不必要的 数据分区目录,从而减少最终需要扫描的数据范围。
(10)skp_idx_[Column].idx与skp_idx_[Column].mrk:如果在建 表语句中声明了二级索引,则会额外生成相应的二级索引与标记文件, 它们同样也使用二进制存储。二级索引在ClickHouse中又称跳数索引, 目前拥有minmax、set、ngrambf_v1和tokenbf_v1四种类型。这些索引 的最终目标与一级稀疏索引相同,都是为了进一步减少所需扫描的数据 范围,以加速整个查询过程。
2.2 ReplacingMergeTree
这个引擎是在 MergeTree 的基础上,添加了“处理重复数据”的功能,该引擎和MergeTree的不同之处在于它会删除具有相同(区内)排序一样的重复项。数据的去重只会在合并的过程中出现。合并会在未知的时间在后台进行(手动合并),所以你无法预先作出计划。有一些数据可能仍未被处理。因此,ReplacingMergeTree 适用于在后台清除重复的数据以节省空间,但是它不保证没有重复的数据出现。
1 无版本参数
根据数据的插入时间 , 后插入的数据保留
drop table if exists test_replacingMergeTree1 ;
create table test_replacingMergeTree1(
oid Int8 ,
ctime DateTime ,
cost Decimal(10,2)
)engine = ReplacingMergeTree()
order by oid
partition by toDate(ctime) ;
-- 天分区 同一天的oid相同的数据会被去重
-- 插入数据
insert into test_replacingMergeTree1 values(3,'2021-01-01 11:11:11',30) ;
insert into test_replacingMergeTree1 values(1,'2021-01-01 11:11:14',40) ;
insert into test_replacingMergeTree1 values(1,'2021-01-01 11:11:11',10);
insert into test_replacingMergeTree1 values(2,'2021-01-01 11:11:11',20) ;
insert into test_replacingMergeTree1 values(1,'2021-01-02 11:11:11',41) ;
-- 优化合并
optimize table test_replacingMergeTree1 final ;
┌─oid─┬───────────────ctime─┬──cost─┐
│ 1 │ 2021-01-02 11:11:11 │ 41.00 │
└─────┴─────────────────────┴───────┘
┌─oid─┬───────────────ctime─┬──cost─┐
│ 1 │ 2021-01-01 11:11:11 │ 10.00 │
│ 2 │ 2021-01-01 11:11:11 │ 20.00 │
│ 3 │ 2021-01-01 11:11:11 │ 30.00 │
└─────┴─────────────────────┴───────┘
由于系统对CK的操作是多线程执行的, 所以不能保证数据插入的顺序 , 就可能出现数据删除错乱的现象
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
-- 主键oid 排序字段两个 验证去重规则是按主键还是排序字段
drop table if exists test_replacingMergeTree2 ;
create table test_replacingMergeTree2(
oid Int8 ,
ctime DateTime ,
cost Decimal(10,2)
)engine = ReplacingMergeTree()
primary key oid
order by (oid ,ctime)
partition by toDate(ctime) ;
insert into test_replacingMergeTree2 values(1,'2021-01-01 11:11:11',10) ;
insert into test_replacingMergeTree2 values(1,'2021-01-01 11:11:11',20) ;
insert into test_replacingMergeTree2 values(1,'2021-01-01 11:11:11',30);
insert into test_replacingMergeTree2 values(1,'2021-01-01 11:11:12',40) ;
insert into test_replacingMergeTree2 values(1,'2021-01-01 11:11:13',50) ;
-- 由此可见 去重并不是根据主键,而知根据区内排序相同的数据会被删除
┌─oid─┬───────────────ctime─┬──cost─┐
│ 1 │ 2021-01-01 11:11:11 │ 30.00 │
│ 1 │ 2021-01-01 11:11:12 │ 40.00 │
│ 1 │ 2021-01-01 11:11:13 │ 50.00 │
└─────┴─────────────────────┴───────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
2 有版本参数
- 版本字段可以是数值
- 版本字段可以是时间
drop table if exists test_replacingMergeTree3 ;
create table test_replacingMergeTree3(
oid Int8 ,
ctime DateTime ,
cost Decimal(10,2)
)engine = ReplacingMergeTree(ctime)
order by oid
partition by toDate(ctime) ;
insert into test_replacingMergeTree3 values(1,'2021-01-01 11:11:11',10) ;
insert into test_replacingMergeTree3 values(1,'2021-01-01 11:11:12',20) ;
insert into test_replacingMergeTree3 values(1,'2021-01-01 11:11:10',30);
insert into test_replacingMergeTree3 values(1,'2021-01-01 11:11:19',40) ;
insert into test_replacingMergeTree3 values(1,'2021-01-01 11:11:13',50) ;
-- 合并数据以后 保留的是时间最近的一条数据
┌─oid─┬───────────────ctime─┬──cost─┐
│ 1 │ 2021-01-01 11:11:19 │ 40.00 │
└─────┴─────────────────────┴───────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
总结:
(1)使用ORDER BY排序键作为判断重复数据的唯一依据。
(2)只有在合并分区的时候才会触发删除重复数据的逻辑。
(3)以数据分区为单位删除重复数据。当分区合并时,同一分区内的重复数据会被删除;不同分区之间的重复数据不会被删除。
(4)在进行数据去重时,因为分区内的数据已经基于ORBER BY进行了排序,所以能够找到那些相邻的重复数据。
(5)数据去重策略有两种:
- 如果没有设置ver版本号,则保留同一组重复数据中的最后一行。
- 如果设置了ver版本号,则保留同一组重复数据中ver字段取值 最大的那一行。
使用这个引擎可以实现数据的更新
2.3 CollapsingMergeTree
CollapsingMergeTree就是一种通过以增代删的思路,支持行级数 据修改和删除的表引擎。它通过定义一个sign标记位字段,记录数据行 的状态。如果sign标记为1,则表示这是一行有效的数据;如果sign标 记为-1,则表示这行数据需要被删除。当CollapsingMergeTree分区合 并时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。 这种1和-1相互抵消的操作,犹如将一张瓦楞纸折叠了一般。这种直观 的比喻,想必也正是折叠合并树(CollapsingMergeTree)名称的由来,
多行的排序相同的状态为1的数据会折叠成一行 , 保留最后一行
两行排序相同的数据, 状态为 1 和 -1 删除这两行数据
ENGINE = CollapsingMergeTree(sign)
drop table if exists tb_cps_merge_tree1 ;
CREATE TABLE tb_cps_merge_tree1
(
user_id UInt64,
name String,
age UInt8,
sign Int8
)
ENGINE = CollapsingMergeTree(sign)
ORDER BY user_id;
-- 插入数据
insert into tb_cps_merge_tree1 values(1,'xiaoluo',23,1),(2,'xiaoyu',24,1),(3,'xiaofeng',25,1) ;
insert into tb_cps_merge_tree1 values(1,'xiaoluo_',23,-1),(2,'xiaoyu_',24,-1),(3,'xiaofeng2',25,1) ;
-- 合并优化
optimize table tb_cps_merge_tree1 ;
-- 实现了数据的删除和已经存在数据的更新
SELECT *
FROM tb_cps_merge_tree1
┌─user_id─┬─name──────┬─age─┬─sign─┐
│ 3 │ xiaofeng2 │ 25 │ 1 │
└─────────┴───────────┴─────┴──────┘
CollapsingMergeTree虽然解决了主键相同的数据即时删除的问题,但是状态持续变化且多线程并行写入情况下,状态行与取消行位置可能乱序,导致无法正常折叠。只有保证老的状态行在在取消行的上面, 新的状态行在取消行的下面! 但是多线程无法保证写的顺序!
drop table if exists tb_cps_merge_tree2 ;
CREATE TABLE tb_cps_merge_tree2
(
user_id UInt64,
name String,
age UInt8,
sign Int8
)
ENGINE = CollapsingMergeTree(sign)
ORDER BY user_id;
insert into tb_cps_merge_tree2 values(1,'xiaoluo_',23,-1),(2,'xiaoyu_',24,-1),(3,'xiaofeng2',25,1) ;
insert into tb_cps_merge_tree2 values(1,'xiaoluo',23,1),(2,'xiaoyu',24,1),(3,'xiaofeng',25,1) ;
-- 合并优化
optimize table tb_cps_merge_tree2 ;
┌─user_id─┬─name─────┬─age─┬─sign─┐
│ 1 │ xiaoluo_ │ 23 │ -1 │
│ 1 │ xiaoluo │ 23 │ 1 │
│ 2 │ xiaoyu_ │ 24 │ -1 │
│ 2 │ xiaoyu │ 24 │ 1 │
│ 3 │ xiaofeng │ 25 │ 1 │
└─────────┴──────────┴─────┴──────┘
假如有相同的排序数据,并且状态都是1,可以实现数据的更新 ,如果我们不能保证折叠的行在状态行的下面 ,数据无法保证可以正常删除
----查询正确的数据
select
tb_cps_merge_tree2.*
from
tb_cps_merge_tree2
join
(
select
user_id ,
sum(sign) as sum_sign
from
tb_cps_merge_tree2
group by user_id
having sum_sign = 1
)t
on tb_cps_merge_tree2.user_id =t.user_id ;
┌─user_id─┬─name─────┬─age─┬─sign─┐
│ 3 │ xiaofeng │ 25 │ 1 │
└─────────┴──────────┴─────┴──────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
2.4 VersionedCollapsingMergeTree
为了解决CollapsingMergeTree乱序写入情况下无法正常折叠(删除)问题,VersionedCollapsingMergeTree表引擎在建表语句中新增了一列Version,用于在乱序情况下记录状态行与取消行的对应关系。主键(排序)相同,且Version相同、Sign相反的行,在Compaction时会被删除。与CollapsingMergeTree类似, 为了获得正确结果,业务层需要改写SQL,将count()、sum(col)分别改写为sum(Sign)、sum(col * Sign)。
drop table if exists tb_vscmt ;
CREATE TABLE tb_vscmt
(
uid UInt64,
name String,
age UInt8,
sign Int8,
version UInt8
)
ENGINE = VersionedCollapsingMergeTree(sign, version)
ORDER BY uid;
INSERT INTO tb_vscmt VALUES (1001, 'ADA', 18, -1, 1);
INSERT INTO tb_vscmt VALUES (1001, 'ADA', 18, 1, 1),(101, 'DAD', 19, 1, 1),(101, 'DAD', 11, 1, 3);
INSERT INTO tb_vscmt VALUES(101, 'DAD', 11, 1, 2) ;
-- 可以保证要删除的数据会被删除, 没有折叠标记的数据会被保留
optimize table tb_vscmt ;
┌─uid─┬─name─┬─age─┬─sign─┬─version─┐
│ 101 │ DAD │ 19 │ 1 │ 1 │
│ 101 │ DAD │ 11 │ 1 │ 2 │
│ 101 │ DAD │ 11 │ 1 │ 3 │
└─────┴──────┴─────┴──────┴─────────┘
版本不一致的数据不会被折叠删除
┌──uid─┬─name─┬─age─┬─sign─┬─version─┐
│ 101 │ DAD │ 19 │ 1 │ 1 │
│ 101 │ DAD │ 11 │ 1 │ 2 │
│ 101 │ DAD │ 11 │ 1 │ 3 │
│ 1001 │ ADA │ 18 │ -1 │ 1 │
│ 1001 │ ADA │ 18 │ 1 │ 2 │
└──────┴──────┴─────┴──────┴─────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
2.5 SummingMergeTree
假设有这样一种查询需求:终端用户只需要查询数据的汇总结果,不关心明细数据,并且数据的汇总条件是预先明确的(GROUP BY 条件明确,且不会随意改变)。
对于这样的查询场景,在ClickHouse中如何解决呢?最直接的方 案就是使用MergeTree存储数据,然后通过GROUP BY聚合查询,并利用 SUM聚合函数汇总结果。这种方案存在两个问题。
- 存在额外的存储开销:终端用户不会查询任何明细数据,只关心汇总结果,所以不应该一直保存所有的明细数据。
- 存在额外的查询开销:终端用户只关心汇总结果,虽然 MergeTree性能强大,但是每次查询都进行实时聚合计算也是一种性能消耗。
SummingMergeTree就是为了应对这类查询场景而生的。顾名思义,它能够在合并分区的时候按照预先定义的条件聚合汇总数据,将同一分组下的多行数据汇总合并成一行,这样既减少了数据行,又降低了后续汇总查询的开销。
提示:
ORDER BY (A、B、C、D)
PRIMARY KEY A
这种强制约束保障了即便在两者定义不同的情况下,主键仍然是排序键的前缀,不会出现索引与数据顺序混乱的问题。
在定义表的主键的时候,我们会考虑主键上的索引快速查找数据
ORDER BY (B、C) PRIMARY KEY A 这种是错误的!
- 1
- 2
- 3
- 4
- 5
drop table summing_table ;
CREATE TABLE summing_table(
id String,
city String,
sal UInt32,
comm Float64,
ctime DateTime
)ENGINE = SummingMergeTree()
PARTITION BY toDate(ctime)
ORDER BY (id, city)
PRIMARY KEY id ;
-- 在合并的时候 ,分区内, 相同排序的行数据的所有的数值字段都会求和(sum)
-- 插入数据
insert into summing_table
values
(1,'shanghai',10,20,'2021-06-12 01:11:12'),
(1,'shanghai',20,30,'2021-06-12 01:11:12'),
(3,'shanghai',10,20,'2021-11-12 01:11:12'),
(3,'Beijing',10,20,'2021-11-12 01:11:12') ;
optimize table summing_table ;
┌─id─┬─city─────┬─sal─┬─comm─┬───────────────ctime─┐
│ 3 │ Beijing │ 10 │ 20 │ 2021-11-12 01:11:12 │
│ 3 │ shanghai │ 10 │ 20 │ 2021-11-12 01:11:12 │
└────┴──────────┴─────┴──────┴─────────────────────┘
┌─id─┬─city─────┬─sal─┬─comm─┬───────────────ctime─┐
│ 1 │ shanghai │ 30 │ 50 │ 2021-06-12 01:11:12 │
└────┴──────────┴─────┴──────┴─────────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
上面的例子中没有指定sum的字段 ,那么表中符合要求的所有的数值字段都会进行求和 ,我们可以在建表的时候指定求和的字段
drop table summing_table2 ;
CREATE TABLE summing_table2(
id String,
city String,
money UInt32,
num UInt32,
ctime DateTime
)ENGINE = SummingMergeTree(money)
PARTITION BY toDate(ctime)
ORDER BY city ;
--每个城市每天的销售总额
insert into summing_table2 values(1,'BJ',100,11,now()),
(2,'BJ',100,11,now()),
(3,'BJ',100,11,now()),
(4,'NJ',100,11,now()),
(5,'NJ',100,11,now()),
(6,'SH',100,11,now()),
(7,'BJ',100,11,'2021-05-18 11:11:11'),
(8,'BJ',100,11,'2021-05-18 11:11:11') ;
SELECT *
FROM summing_table2 ;
┌─id─┬─city─┬─money─┬─num─┬───────────────ctime─┐
│ 1 │ BJ │ 300 │ 11 │ 2021-05-19 21:53:49 │
│ 4 │ NJ │ 200 │ 11 │ 2021-05-19 21:53:49 │
│ 6 │ SH │ 100 │ 11 │ 2021-05-19 21:53:49 │
└────┴──────┴───────┴─────┴─────────────────────┘
┌─id─┬─city─┬─money─┬─num─┬───────────────ctime─┐
│ 7 │ BJ │ 200 │ 11 │ 2021-05-18 11:11:11 │
└────┴──────┴───────┴─────┴─────────────────────┘
SELECT city ,money
FROM summing_table2 ;
┌─city─┬─money─┐
│ BJ │ 300 │
│ NJ │ 200 │
│ SH │ 100 │
└──────┴───────┘
┌─city─┬─money─┐
│ BJ │ 200 │
└──────┴───────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
支持嵌套格式的求和操作
CREATE TABLE summing_table_nested(
id String,
nestMap Nested(
id UInt32,
key UInt32,
val UInt64
),
create_time DateTime
)ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(create_time)
ORDER BY id ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
总结:
(1)用ORBER BY排序键作为聚合数据的条件Key。
(2)只有在合并分区的时候才会触发汇总的逻辑。
(3)以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合Key相同的数据会被合并汇总,而不同分区之间的数据则不会被汇总。
(4)如果在定义引擎时指定了columns汇总列(非主键的数值类 型字段),则SUM汇总这些列字段;如果未指定,则聚合所有非主键的数值类型字段。
(5)在进行数据汇总时,因为分区内的数据已经基于ORBER BY排序,所以能够找到相邻且拥有相同聚合Key的数据。
(6)在汇总数据时,同一分区内,相同聚合Key的多行数据会合并成一行。其中,汇总字段会进行SUM计算;对于那些非汇总字段,则会使用第一行数据的取值。
(7)支持嵌套结构,但列字段名称必须以Map后缀结尾。嵌套类 型中,默认以第一个字段作为聚合Key。除第一个字段以外,任何名称 以Key、Id或Type为后缀结尾的字段,都将和第一个字段一起组成复合 Key。
2.5 AggregatingMergeTree
AggregatingMergeTree就有些许数据立方体的意思,它能够在合并分区的时候,按照预先定义的条件聚合数据。同时,根据预先定义的聚合函数计算数据并通过二进制的格式存入表内。将同一分组下的多行数据聚合成一行,既减少了数据行,又降低了后续聚合查询的开销。可以说,AggregatingMergeTree 是SummingMergeTree的升级版,它们的许多设计思路是一致的,例如同时定义 ORDER BY与PRIMARY KEY的原因和目的。但是在使用方法上,两者存在明显差异,应该说AggregatingMergeTree的定义方式是MergeTree家族中最为特殊的一个。
NGINE = AggregatingMergeTree()
- 1
AggregatingMergeTree没有任何额外的设置参数,在分区合并时,在每个数据分区内,会按照ORDER BY聚合。而使用何种聚合函数,以及针对哪些列字 段计算,则是通过定义AggregateFunction数据类型实现的。在insert和select时,也有独特的写法和要求:写入时需要使用-State语法,查询时使用-Merge语法。
AggregateFunction(arg1 , arg2) ;
参数一 聚合函数
参数二 数据类型
sum_cnt AggregateFunction(sum, Int64) ;
先创建原始表 —插入数据—> 创建预先聚合表 --通过Insert的方式导入数据, 数据会按照指定的聚合函数聚合预先数据!
-- 1)建立明细表
CREATE TABLE detail_table
(id UInt8,
ctime Date,
money UInt64
) ENGINE = MergeTree()
PARTITION BY toDate(ctime)
ORDER BY id;
-- 2)插入明细数据
INSERT INTO detail_table VALUES(1, '2021-08-06', 100);
INSERT INTO detail_table VALUES(1, '2021-08-06', 100);
INSERT INTO detail_table VALUES(1, '2021-08-06', 300); ***
INSERT INTO detail_table VALUES(2, '2021-08-07', 200);
INSERT INTO detail_table VALUES(2, '2021-08-07', 200);
-- 3)建立预先聚合表,
-- 注意:其中UserID一列的类型为:AggregateFunction(uniq, UInt64)
CREATE TABLE agg_table
(id UInt8,
ctime Date,
cnt AggregateFunction(count, UInt64)
) ENGINE = AggregatingMergeTree()
PARTITION BY toDate(ctime)
ORDER BY id;
-- 4) 从明细表中读取数据,插入聚合表。
-- 注意:子查询中使用的聚合函数为 uniqState, 对应于写入语法<agg>-State
INSERT INTO agg_table
select id, ctime, countState(money)
from detail_table
group by id, ctime ;
-- 不能使用普通insert语句向AggregatingMergeTree中插入数据。
-- 本SQL会报错:Cannot convert UInt64 to AggregateFunction(uniq, UInt64)
INSERT INTO agg_table VALUES(1, '2020-08-06', 1);
-- 5) 从聚合表中查询。
-- 注意:select中使用的聚合函数为uniqMerge,对应于查询语法<agg>-Merge
SELECT
id, ctime ,
countMerge(uid) AS state
FROM agg_table
GROUP BY id, ctime;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
使用物化视图同步聚合数据
-- 建立明细表
drop table orders ;
CREATE TABLE orders
(
uid UInt64,
money UInt64,
ctime Date,
Sign Int8
)
ENGINE = MergeTree()
ORDER BY uid;
--插入数据
insert into orders values(1,100,toDate(now()),1) ;
insert into orders values(1,100,toDate(now()),1) ;
insert into orders values(1,100,toDate(now()),1) ;
insert into orders values(2,200,toDate(now()),1) ;
insert into orders values(2,200,toDate(now()),1) ;
insert into orders values(2,200,toDate(now()),1) ;
insert into orders values(2,100,toDate(now()),1) ;
-- 将聚合逻辑创建成物化视图
CREATE MATERIALIZED VIEW orders_agg_view
ENGINE = AggregatingMergeTree()
PARTITION BY toDate(ctime)
ORDER BY uid
populate
as select
uid ,
ctime ,
sumState(money) as mm -- 注意别名
from
orders
group by uid , ctime;
-- 查询物化视图数据
select uid,ctime,sumMerge(mm) from orders_agg_view group by uid, ctime ;
-- 更新明细数据, 物化视图中的数据实时计算更新
insert into orders values(1,100,toDate(now()),1);
┌─uid─┬──────ctime─┬─sumMerge(mm)─┐
│ 2 │ 2021-05-19 │ 400 │
│ 1 │ 2021-05-19 │ 200 │
└─────┴────────────┴──────────────┘
┌─uid─┬──────ctime─┬─sumMerge(mm)─┐
│ 2 │ 2021-05-19 │ 400 │
│ 1 │ 2021-05-19 │ 300 │
└─────┴────────────┴──────────────┘
┌─uid─┬──────ctime─┬─sumMerge(mm)─┐
│ 2 │ 2021-05-19 │ 400 │
│ 1 │ 2021-05-19 │ 400 │
└─────┴────────────┴──────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
总结:
(1)用ORBER BY排序键作为聚合数据的条件Key。
(2)使用AggregateFunction字段类型定义聚合函数的类型以及聚合的字 段。
(3)只有在合并分区的时候才会触发聚合计算的逻辑。
(4)以数据分区为单位来聚合数据。当分区合并时,同一数据分区内聚合 Key相同的数据会被合并计算,而不同分区之间的数据则不会被计算。
(5)在进行数据计算时,因为分区内的数据已经基于ORBER BY排序,所以 能够找到那些相邻且拥有相同聚合Key的数据。
(6)在聚合数据时,同一分区内,相同聚合Key的多行数据会合并成一 行。对于那些非主键、非AggregateFunction类型字段,则会使用第一行数据的 取值。
(7)AggregateFunction类型的字段使用二进制存储,在写入数据时,需 要调用State函数;而在查询数据时,则需要调用相应的Merge函数。其中,* 表示定义时使用的聚合函数。
(8)AggregatingMergeTree通常作为物化视图的表引擎,与普通 MergeTree搭配使用。
该查询尝试使用[MergeTree]系列中的表引擎初始化表的未计划的数据部分合并。[MaterializedView和[Buffer]引擎OPTMIZE也支持。不支持其他表引擎。
当OPTIMIZE与使用[ReplicatedMergeTree]表引擎,ClickHouse创造了合并,并等待所有节点上执行(如果该任务replication_alter_partitions_sync已启用设置)。
· 如果OPTIMIZE由于任何原因未执行合并,则不会通知客户端。要启用通知,请使用[optimize_throw_if_noop]设置。
· 如果指定PARTITION,则仅优化指定的分区。[如何设置分区表达式]。
· 如果指定FINAL,即使所有数据已经在一个部分中,也会执行优化。
· 如果指定DEDUPLICATE,则将对完全相同的行进行重复数据删除(比较所有列),这仅对MergeTree引擎有意义。
3 外部存储引擎
3.1 HDFS引擎
Clickhouse可以直接从HDFS中指定的目录下加载数据 , 自己根本不存储数据, 仅仅是读取数据
ENGINE = HDFS(hdfs_uri,format)
·hdfs_uri表示HDFS的文件存储路径;
·format表示文件格式(指ClickHouse支持的文件格式,常见的有 CSV、TSV和JSON等)。
- 1
- 2
- 3
我们一般期望的是数据有其他方式写入到HDFS系统中, 使用CK的HDFS引擎加载处理分析数据.
这种形式类似Hive的外挂表,由其他系统直接将文件写入HDFS。通过HDFS表引擎的hdfs_uri和format参数分别与HDFS的文件路径、文件格式建立映射。其中,hdfs_uri支持以下几种常见的配置方法:
-
绝对路径:会读取指定路径的单个文件,例如/clickhouse/hdfs_table1。
-
通配符:匹配所有字符,例如路径为/clickhouse/hdfs_table/,则会读取/click-house/hdfs_table路径下的所有文件。
-
?通配符:匹配单个字符,例如路径为/clickhouse/hdfs_table/organization_?.csv,则会读取/clickhouse/hdfs_table路径下与organization_?.csv匹配的文件,其中?代表任意一个合法字符。
-
{M…N}数字区间:匹配指定数字的文件,例如路径为/clickhouse/hdfs_table/organization_{1…3}.csv,则会读取/clickhouse/hdfs_table/路径下的文件organization_1.csv、organization_2.csv和organization_3.csv。
create table test_hdfs1(
id Int8 ,
name String ,
age Int8
)engine=HDFS('hdfs://linux01:8020/ck/test1/*' ,CSV) ;
创建文件,将文件上传到指定的目录下
1.txt
1,zss,21
2,ww,22
2.txt
3,lss,33
4,qaa,32
3.txt
5,as,31
6,ghf,45
--匹配单个字符
create table test_hdfs2(
id Int8 ,
name String ,
age Int8
)engine=HDFS('hdfs://linux01:8020/ck/test1/?.txt' ,CSV) ;
-- 匹配数字之间的文件
create table test_hdfs3(
id Int8 ,
name String ,
age Int8
)engine=HDFS('hdfs://linux01:8020/ck/test1/a_{1..2}.txt' ,CSV) ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
3.2 mysql引擎
MySQL表引擎可以与MySQL数据库中的数据表建立映射,并通过SQL向其发起远程查询, 包括SELECT和INSERT
它的声明方式如下:
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']);
- 1
- 2
- 3
- 4
- 5
- 6
其中各参数的含义分别如下:
·host:port表示MySQL的地址和端口。
·database表示数据库的名称。
·table表示需要映射的表名称。
·user表示MySQL的用户名。
·password表示MySQL的密码。
·replace_query默认为0,对应MySQL的REPLACE INTO语法。如果将它设置为1,则会用REPLACE INTO代替INSERT INTO。
·on_duplicate_clause默认为0,对应MySQL的ON DUPLICATE KEY语法。如果需要使用该设置,则必须将replace_query设置成0。
那么在正式使用MySQL引擎之前首先当前机器要有操作MySQL数据的权限 ,开放MySQL的远程连连接权限操作如下:
1) set global validate_password_policy=0;
2) set global validate_password_length=1; 这个两个设置以后 密码很简单不会报错
3) grant all privileges on *.* to 'root'@'%' identified by 'root' with grant option;
4) flush privileges;
- 1
- 2
- 3
- 4
-- 在mysql中建表
create table tb_x(id int, name varchar(25), age int) ;
insert into tb_x values(1,'zss',23),(2,'lss',33) ;
-- 在clickhouse中建表
CREATE TABLE tb_mysql
(
`id` Int8,
`name` String,
`age` Int8
)
ENGINE = MySQL('doit01:3306', 'test1', 'tb_x', 'root', 'root');
-- 查看数据
-- 插入数据
insert into tb_mysql values(3,'ww',44) ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 支持查询数据
- 支持插入数据
- 不支持删除和更新操作
3.3 File引擎
File表引擎能够直接读取本地文件的数据,通常被作为一种扩充手段来使用。例如:它可以读取由其他系统生成的数据文件,如果外部系统直接修改了文件,则变相达到了数据更新的目的;它可以将 ClickHouse数据导出为本地文件;它还可以用于数据格式转换等场景。除此以外,File表引擎也被应用于clickhouse-local工具
ENGINE = File(format)
- 1
drop table if exists test_file1 ;
create table test_file1(
id String ,
name String ,
age UInt8
)engine=File("CSV") ;
- 1
- 2
- 3
- 4
- 5
- 6
在默认的目录下回生成一个文件夹 , 文件夹中可以写入文件 ,但是文件的名字必须是
insert into test_file1 values('u001','hangge',33) ;
- 1
file表函数
– 去指定的路径下加载本地的数据
select * from file('/ck/','CSV','id Int8 , name String ,gender String,age UInt8') ;
- 1
默认加载的是特定的文件夹,数据一定要在指定的文件夹下才会被加载
修改默认的数据加载的文件夹
vi /etc/clickhouse-server/
/path n下一个
<!-- Directory with user provided files that are accessible by 'file' table function. -->
<user_files_path>/</user_files_path>
- 1
- 2
- 3
- 4
重启服务
service clickhouse-server restart
- 1
3.4 MySQL 数据库引擎
语法
CREATE DATABASE [IF NOT EXISTS] db_name [ON CLUSTER cluster]
ENGINE = MySQL('host:port', ['database' | database], 'user', 'password')
- 1
- 2
create database db_ck_mysql engine=MySQL('doit01:3306','test1','root','root') ;
- 1
常用于数据的合并 , 加载mysql中的数据和ck中的数据合并 , 不做数据的修改和建表
4 内存引擎
接下来将要介绍的几款表引擎,都是面向内存查询的,数据会从内存中被直接访问,所以它们被归纳为内存类型。但这并不意味着内存类表引擎不支持物理存储,事实上,除了Memory表引擎之外,其余的几款表引擎都会将数据写入磁盘,这是为了防止数据丢失,是一种故障恢复手段。而在数据表被加载时,它们会将数据全部加载至内存,以供查询之用。将数据全量放在内存中,对于表引擎来说是一把双刃剑:一方面,这意味着拥有较好的查询性能;而另一方面,如果表内装载的数据量过大,可能会带来极大的内存消耗和负担!
4.1 Memory
Memory表引擎直接将数据保存在内存中,数据既不会被压缩也不会被格式转换,数据在内存中保存的形态与查询时看到的如出一辙。 正因为如此,当ClickHouse服务重启的时候,Memory表内的数据会全部丢失。所以在一些场合,会将Memory作为测试表使用,很多初学者在学习ClickHouse的时候所写的Hello World程序很可能用的就是Memory表。由于不需要磁盘读取、序列化以及反序列等操作,所以Memory表引擎支持并行查询,并且在简单的查询场景中能够达到与MergeTree旗鼓相当的查询性能(一亿行数据量以内)。Memory表的创建方法如下所示:
CREATE TABLE memory_1 (
id UInt64
)ENGINE = Memory() ;
- 1
- 2
- 3
Memory表更为广 泛的应用场景是在ClickHouse的内部,它会作为集群间分发数据的存储载体来使用。例如在分布式IN查询的场合中,会利用Memory临时表保存IN子句的查询结果,并通过网络将它传输到远端节点。
4.2 Set
Set表引擎是拥有物理存储,数据首先会被写至内存,然后被同步到磁盘文件中。所以当服务重启时,它的数据不会丢失,当数据表被重新装载时,文件数据会再次被全量加载至内存。众所周知,在Set 数据结构中,所有元素都是唯一的。Set表引擎具有去重的能力,在数据写入的过程中,重复的数据会被自动忽略。然而Set表引擎的使用场景既特殊又有限,它虽然支持正常的INSERT写入,但并不能直接使用SELECT对其进行查询,Set表引擎只能间接作为IN查询的右侧条件被查询使用
Set表引擎的存储结构由两部分组成,它们分别是:
[num].bin数据文件:保存了所有列字段的数据。其中,num是 一个自增id,从1开始。伴随着每一批数据的写入(每一次INSERT),都会生成一个新的.bin文件,num也会随之加1。
tmp临时目录:数据文件首先会被写到这个目录,当一批数据写入完毕之后,数据文件会被移出此目录。
create table test_set(
id Int8 ,
name String
)engine=Set();
- 1
- 2
- 3
- 4
发现在数据库的目录下是还有对应的目录的,可见数据会被存储到磁盘上的
插如数据 ;
但是这种表不允许我们直接查询
CREATE TABLE x
(
`id` Int8,
`name` String
)
ENGINE =Set
insert into x values(1,'zss'),(4,'ww') ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
正确的查询方法是将Set表引擎作为IN查询的右侧条件
select * from x where (id,name) in test_set ;
CREATE TABLE test_set_source
(
`id` Int8,
`name` String,
`age` Int8
)
ENGINE = Log ;
insert into test_set_source values(1,'lss',21),(2,'ww',33),(3,'zl',11) ;
-- 以set表中的数据为依据 筛选数据
select * from test_set_source where id in test_set;
注意 : in的条件个表的字段一致
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
4.3 Buffer
Buffer表引擎完全使用内存装载数据,不支持文件的持久化存储,所以当服务重启之后,表内的数据会被清空。Buffer表引擎不是为了面向查询场景而设计的,它的作用是充当缓冲区的角色。假设有这样一种场景,我们需要将数据写入目标MergeTree表A,由于写入的并发数很高,这可能会导致MergeTree表A的合并速度慢于写入速度(因为每一次INSERT都会生成一个新的分区目录)。此时,可以引入Buffer表来缓解这类问题,将Buffer表作为数据写入的缓冲区。数据首先被写入Buffer表,当满足预设条件时,Buffer表会自动将数据刷新到目标表
ENGINE = Buffer(database, table, num_layers, min_time, max_time, min_rows, max_rows, min_bytes, max_bytes)
其中,参数可以分成基础参数和条件参数两类,首先说明基础参数的作用:
-
database:目标表的数据库。
-
table:目标表的名称,Buffer表内的数据会自动刷新到目标表。
-
num_layers:可以理解成线程数,Buffer表会按照num_layers的数量开启线程,以并行的方式将数据刷新到目标表,官方建议设为16。
Buffer表并不是实时刷新数据的,只有在阈值条件满足时它才会刷新。阈值条件由三组最小和最大值组成。接下来说明三组极值条件参数的具体含义:
-
min_time和max_time:时间条件的最小和最大值,单位为秒,从第一次向表内写入数据的时候开始计算;
-
min_rows和max_rows:数据行条件的最小和最大值;
-
min_bytes和max_bytes:数据体量条件的最小和最大值,单位为字节。
根据上述条件可知,Buffer表刷新的判断依据有三个,满足其中任意一个,Buffer表就会刷新数据,它们分别是:
-
如果三组条件中所有的最小阈值都已满足,则触发刷新动作;
-
如果三组条件中至少有一个最大阈值条件满足,则触发刷新动作;
还有一点需要注意,上述三组条件在每一个num_layers中都是单独计算的。假设num_layers=16,则Buffer表最多会开启16个线程来响应数据的写入,它们以轮询的方式接收请求,在每个线程内,会独立进行上述条件判断的过程。也就是说,假设一张Buffer表的
max_bytes=100000000(约100 MB),num_layers=16,那么这张Buffer表能够同时处理的最大数据量约是1.6 GB。
create table xx(
id Int64
)engine=Log ;
CREATE TABLE buffer_to_xx AS memory_1
ENGINE = Buffer(default, xx, 16, 10, 100, 10000, 1000000, 10000000, 100000000) ;
- 1
- 2
- 3
- 4
- 5
- 6
INSERT INTO TABLE buffer_to_xx SELECT number FROM numbers(1000000) ;
此时,buffer_to_xx内有数据,而目标表memory_1是没有的,因为目前不论从时间、数据行还是数据大小来判断,没有一个达到了最大阈值。所以在大致100秒之后,数据才会从buffer_to_xx刷新到xx。
可以在ClickHouse的日志中发现相关记录信息:
INSERT INTO TABLE buffer_to_xx SELECT number FROM numbers(1000001) ;
数据直接被插入到表中
创建一个具有与’’相同结构的’merge.hits_buffer’表,并使用Buffer引擎。写入此表时,数据会缓冲在RAM中,然后再写入“ ”表。创建了16个缓冲区。如果经过了100秒,或者已写入一百万行,或者已写入100 MB数据,则将刷新其中每个数据;或者同时经过10秒并写入10,000行和10 MB数据。例如,如果只写了一行,则无论如何,在100秒后它将被刷新。但是,如果已写入许多行,则将更快地刷新数据。
当服务器停止时,使用DROP TABLE或DETACH TABLE,缓冲区数据也将刷新到目标表。
您可以在数据库名称和表名称的单引号中设置空字符串。这表明没有目标表。在这种情况下,当达到数据刷新条件时,只需清除缓冲区。这对于将数据窗口保留在内存中可能很有用。
从缓冲区表读取数据时,将从缓冲区和目标表(如果有的话)中处理数据。
请注意
l 缓冲区表不支持索引。换句话说,缓冲区中的数据已被完全扫描,这对于大型缓冲区而言可能很慢。(对于下级表中的数据,将使用其支持的索引。)
l 如果“缓冲区”表中的列集与从属表中的列集不匹配,则插入两个表中都存在的列子集。
l 如果类型与缓冲区表和从属表中的任一列都不匹配,则会在服务器日志中输入错误消息,并清除缓冲区。
如果刷新缓冲区时从属表不存在,也会发生相同的情况。
l 如果需要对下级表和Buffer表运行ALTER,建议先删除Buffer表,对下级表运行ALTER,然后再次创建Buffer表。
l 如果服务器异常重启,缓冲区中的数据将会丢失。
l FINAL和SAMPLE对于缓冲区表不能正常工作。这些条件将传递到目标表,但不用于处理缓冲区中的数据。如果需要这些功能,建议从目标表读取时仅使用缓冲区表进行写入。
l 将数据添加到缓冲区时,缓冲区之一被锁定。如果同时从表执行读取操作,则会导致延迟。
l 插入到缓冲区表中的数据可能以不同的顺序和不同的块最终出现在从属表中。因此,很难使用Buffer表正确地写入CollapsingMergeTree。为了避免出现问题,可以将“ num_layers”设置为1。
l 如果目标表被复制,则写入缓冲区表时,复制表的某些预期特性会丢失。数据部分的行顺序和大小的随机变化会导致重复数据删除退出工作,这意味着不可能对复制表进行可靠的“仅一次”写入。
l 由于这些缺点,我们仅建议在极少数情况下使用Buffer表。
l 当在一个单位时间内从大量服务器接收到太多INSERT且无法在插入之前对数据进行缓冲的情况下,将使用Buffer表,这意味着INSERT不能足够快地运行。
l 注意,即使一次插入缓冲区表也没有意义。这样只会产生每秒几千行的速度,而插入更大的数据块则每秒会产生一百万行以上的速度(请参阅“性能”一节)。
1) 创建一个目标表
create table tb_user_target(uid Int8 , name String) engine=TinyLog ;
2) 创建一个缓存表
CREATE TABLE tb_user_buffer AS tb_user_target ENGINE = Buffer(doit26, tb_user_target, 16, 10, 100, 10000, 1000000, 10000000, 100000000) ;
CREATE TABLE tb_user_buffer2 AS tb_user_target ENGINE = Buffer(doit26, tb_user_target, 16, 10, 100, 2, 10, 10000000, 100000000) ;
3) 向缓存表中插入数据
insert into tb_user_buffer values(1,’Yang’),(2,'Haha') ,(3,'ADA') ;
4) 等待以后查看目标表中的数据
select * from tb_user ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
五 查询语法
with x as (select from ) ,y as(select from) select from x , y …
5.1 with
ClickHouse支持CTE(Common Table Expression,公共表表达式),以增强查询语句的表达
SELECT pow(2, 2)
┌─pow(2, 2)─┐
│ 4 │
└───────────┘
SELECT pow(pow(2, 2), 2)
┌─pow(pow(2, 2), 2)─┐
│ 16 │
└───────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在改用CTE的形式后,可以极大地提高语句的可读性和可维护性,\
with pow(2,2) as a select pow(a,3) ;
- 1
1) 定义变量
WITH
1 AS start,
10 AS end
SELECT
id + start,
*
FROM tb_mysql
┌─plus(id, start)─┬─id─┬─name─┬─age─┐
│ 2 │ 1 │ zss │ 23 │
│ 3 │ 2 │ lss │ 33 │
│ 4 │ 3 │ ww │ 44 │
│ 2 │ 1 │ zss │ 23 │
│ 3 │ 2 │ lss │ 33 │
│ 2 │ 1 │ zss │ 23 │
│ 3 │ 2 │ lss │ 33 │
└─────────────────┴────┴──────┴─────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
2) 调用函数
SELECT *
FROM tb_partition
┌─id─┬─name─┬────────────birthday─┐
│ 1 │ xl │ 2021-05-20 10:50:46 │
│ 2 │ xy │ 2021-05-20 11:17:47 │
└────┴──────┴─────────────────────┘
┌─id─┬─name─┬────────────birthday─┐
│ 3 │ xf │ 2021-05-19 11:11:12 │
└────┴──────┴───────────---------─┘
WITH toDate(birthday) AS bday
SELECT
id,
name,
bday
FROM tb_partition
┌─id─┬─name─┬───────bday─┐
│ 1 │ xl │ 2021-05-20 │
│ 2 │ xy │ 2021-05-20 │
└────┴──────┴────────────┘
┌─id─┬─name─┬───────bday─┐
│ 3 │ xf │ 2021-05-19 │
└────┴──────┴────────────┘
- 练习
WITH
count(1) AS cnt,
groupArray(cdate) AS list
SELECT
name,
cnt,
list
FROM tb_shop2
GROUP BY name
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
3) 子查询
可以定义子查询 ,但是一定还要注意的是,子查询只能返回一行结果 ,否则会跑出异常
WITH
(
SELECT *
FROM tb_partition
WHERE id = 1
) AS sub
SELECT
*,
sub
FROM tb_partition
┌─id─┬─name─┬────────────birthday─┬─sub────────────────────────────┐
│ 1 │ xl │ 2021-05-20 10:50:46 │ (1,'xl','2021-05-20 10:50:46') │
│ 2 │ xy │ 2021-05-20 11:17:47 │ (1,'xl','2021-05-20 10:50:46') │
└────┴──────┴─────────────────────┴────────────────────────────────┘
┌─id─┬─name─┬────────────birthday─┬─sub────────────────────────────┐
│ 3 │ xf │ 2021-05-19 11:11:12 │ (1,'xl','2021-05-20 10:50:46') │
└────┴──────┴─────────────────────┴────────────────────────────────┘
with (select * from tb_shop2 where name = 'a' and cdate = '2017-03-01') as x select * from tb_shop2 where (name,cdate,money)=x ;
1 子查询的结果必须是一条数据
2 where (id,name,age) = (1,'zss',23)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
5.2 from
SQL是一种面向集合的编程语言 ,from决定了程序从那里读取数据
-
表中查询数据
-
子查询中查询数据
-
表函数中查询数据 select * from numbers(3) ;
file hdfs mysql numbers(10)
表函数
构建表的函数 , 使用场景如下:
SELECT查询的[FROM)子句。
创建表AS 查询。
1 file
file(path, format, structure)
path — The relative path to the file from user_files_path. Path to file support following globs in readonly mode: *, ?, {abc,def} and {N..M} where N, M — numbers, `'abc', 'def' — strings.
format — The format of the file.
structure — Structure of the table. Format 'column1_name column1_type, column2_name column2_type, ...'.
- 1
- 2
- 3
- 4
数据文件必须在指定的目录下 /var/lib/clickhouse/user_files
SELECT *
FROM file('', 'CSV', 'id Int8,name String , age UInt8')
-- 文件夹下任意的文件
SELECT *
FROM file('*', 'CSV', 'id Int8,name String , age UInt8')
- 1
- 2
- 3
- 4
- 5
2 numbers
SELECT *
FROM numbers(10) ;
SELECT *
FROM numbers(2, 10) ;
SELECT *
FROM numbers(10) limit 3 ;
SELECT toDate('2020-01-01') + number AS d
FROM numbers(365)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
3 mysql
CH可以直接从mysql服务中查询数据
mysql('host:port', 'database', 'table', 'user', 'password'[, replace_query, 'on_duplicate_clause']);
SELECT *
FROM mysql('linux01:3306', 'db_doit_ch', 'emp', 'root', 'root')
- 1
- 2
- 3
- 4
4 hdfs
SELECT *FROM hdfs(‘hdfs://hdfs1:9000/test’, ‘TSV’, ‘column1 UInt32, column2 UInt32, column3 UInt32’)LIMIT 2
SELECT *
FROM hdfs(‘hdfs://linux01:8020/’, ‘CSV’, ‘id Int8 ,name String , age Int8’)
5.3 array join
ARRAY JOIN子句允许在数据表的内部,与数组或嵌套类型的字段进行JOIN操作,从而将一行数组展开为多行。类似于hive中的explode炸裂函数的功能!
CREATE TABLE test_arrayjoin
(
`name` String,
`vs` Array(Int8)
)
ENGINE = Memory ;
insert into test_arrayjoin values('xw',[1,2,3]),('xl',[4,5]),('xk',[1]);
-- 将数组中的数据展开
SELECT
*,
s
FROM test_arrayjoin
ARRAY JOIN vs AS s
┌─name─┬─vs──────┬─s─┐
│ xw │ [1,2,3] │ 1 │
│ xw │ [1,2,3] │ 2 │
│ xw │ [1,2,3] │ 3 │
│ xl │ [4,5] │ 4 │
│ xl │ [4,5] │ 5 │
│ xk │ [1] │ 1 │
└──────┴─────────┴───┘
-- arrayMap 高阶函数,对数组中的每个元素进行操作
SELECT
*,
arrayMap(x->x*2 , vs) vs2
FROM test_arrayjoin ;
SELECT
*,
arrayMap(x -> (x * 2), vs) AS vs2
FROM test_arrayjoin
┌─name─┬─vs──────┬─vs2─────┐
│ xw │ [1,2,3] │ [2,4,6] │
│ xl │ [4,5] │ [8,10] │
│ xk │ [1] │ [2] │
└──────┴─────────┴─────────┘
SELECT
*,
arrayMap(x -> (x * 2), vs) AS vs2 ,
vv1 ,
vv2
FROM test_arrayjoin
array join
vs as vv1 ,
vs2 as vv2 ;
┌─name─┬─vs──────┬─vs2─────┬─vv1─┬─vv2─┐
│ xw │ [1,2,3] │ [2,4,6] │ 1 │ 2 │
│ xw │ [1,2,3] │ [2,4,6] │ 2 │ 4 │
│ xw │ [1,2,3] │ [2,4,6] │ 3 │ 6 │
│ xl │ [4,5] │ [8,10] │ 4 │ 8 │
│ xl │ [4,5] │ [8,10] │ 5 │ 10 │
│ xk │ [1] │ [2] │ 1 │ 2 │
└──────┴─────────┴─────────┴─────┴─────┘
select
id ,
h ,
xx
from
tb_array_join
array join
hobby as h ,
arrayEnumerate(hobby) as xx ;
┌─id─┬─h─────┬─xx─┐
│ 1 │ eat │ 1 │
│ 1 │ drink │ 2 │
│ 1 │ sleep │ 3 │
│ 2 │ study │ 1 │
│ 2 │ sport │ 2 │
│ 2 │ read │ 3 │
└────┴───────┴────┘
┌─id─┬─h─────┬─xx─┐
│ 3 │ eat │ 1 │
│ 3 │ drink │ 2 │
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
案例
a,2017-02-05,200
a,2017-02-06,300
a,2017-02-07,200
a,2017-02-08,400
a,2017-02-08,300
a,2017-02-10,600
b,2017-02-05,200
b,2017-02-06,300
b,2017-02-08,200
b,2017-02-09,400
b,2017-02-10,600
c,2017-01-31,200
c,2017-02-01,300
c,2017-02-02,200
c,2017-02-03,400
c,2017-02-10,600
a,2017-03-01,200
a,2017-03-02,300
a,2017-03-03,200
a,2017-03-04,400
a,2017-03-05,600
drop table if exists tb_shop ;
CREATE TABLE tb_shop
(
`name` String,
`cdate` Date,
`cost` Float64
)engine=ReplacingMergeTree(cdate)
order by (name,cdate) ;
-- 导入数据
clickhouse-client -q 'insert into doit23.tb_shop format CSV' < shop.txt ;
┌─name─┬──────cdate─┬─cost─┐
│ a │ 2017-02-05 │ 200 │
│ a │ 2017-02-06 │ 300 │
│ a │ 2017-02-07 │ 200 │
│ a │ 2017-02-08 │ 400 │
│ a │ 2017-02-10 │ 600 │
│ a │ 2017-03-01 │ 200 │
│ a │ 2017-03-02 │ 300 │
│ a │ 2017-03-03 │ 200 │
│ a │ 2017-03-04 │ 400 │
│ a │ 2017-03-05 │ 888 │
│ b │ 2017-02-05 │ 200 │
│ b │ 2017-02-06 │ 300 │
│ b │ 2017-02-08 │ 200 │
│ b │ 2017-02-09 │ 400 │
│ b │ 2017-02-10 │ 600 │
│ c │ 2017-01-31 │ 200 │
│ c │ 2017-02-01 │ 300 │
│ c │ 2017-02-02 │ 200 │
│ c │ 2017-02-03 │ 400 │
│ c │ 2017-02-10 │ 600 │
└──────┴────────────┴──────┘
select
name ,
groupArray(cdate) arr ,
arrayEnumerate(arr) as indexs
from
tb_shop
group by name;
┌─name─┬─arr─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─indexs─────────────────┐
│ b │ ['2017-02-05','2017-02-06','2017-02-08','2017-02-09','2017-02-10'] │ [1,2,3,4,5] │
│ c │ ['2017-01-31','2017-02-01','2017-02-02','2017-02-03','2017-02-10'] │ [1,2,3,4,5] │
│ a │ ['2017-02-05','2017-02-06','2017-02-07','2017-02-08','2017-02-10','2017-03-01','2017-03-02','2017-03-03','2017-03-04','2017-03-05'] │ [1,2,3,4,5,6,7,8,9,10] │
└──────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────┘
select
name ,
dt - num
from
(select
name ,
groupArray(cdate) arr ,
arrayEnumerate(arr) as indexs
from
tb_shop
group by name
)
array join
arr as dt ,
indexs as num ;
┌─name─┬─minus(dt, num)─┐
│ b │ 2017-02-04 │
│ b │ 2017-02-04 │
│ b │ 2017-02-05 │
│ b │ 2017-02-05 │
│ b │ 2017-02-05 │
│ c │ 2017-01-30 │
│ c │ 2017-01-30 │
│ c │ 2017-01-30 │
│ c │ 2017-01-30 │
│ c │ 2017-02-05 │
│ a │ 2017-02-04 │
│ a │ 2017-02-04 │
│ a │ 2017-02-04 │
│ a │ 2017-02-04 │
│ a │ 2017-02-05 │
│ a │ 2017-02-23 │
│ a │ 2017-02-23 │
│ a │ 2017-02-23 │
│ a │ 2017-02-23 │
│ a │ 2017-02-23 │
└──────┴────────────────┘
select
name ,
diff ,
count(1) cnt
from
(select
name ,
(dt - num) as diff
from
(select
name ,
groupArray(cdate) arr ,
arrayEnumerate(arr) as indexs
from
tb_shop
group by name
)
array join
arr as dt ,
indexs as num
)
group by name , diff;
┌─name─┬───────diff─┬─count(1)─┐
│ b │ 2017-02-04 │ 2 │
│ a │ 2017-02-23 │ 5 │
│ c │ 2017-01-30 │ 4 │
│ c │ 2017-02-05 │ 1 │
│ a │ 2017-02-04 │ 4 │
│ b │ 2017-02-05 │ 3 │
│ a │ 2017-02-05 │ 1 │
└──────┴────────────┴──────────┘
select
name ,
diff ,
count(1) cnt
from
(select
name ,
(dt - num) as diff
from
(select
name ,
groupArray(cdate) arr ,
arrayEnumerate(arr) as indexs
from
tb_shop
group by name
)
array join
arr as dt ,
indexs as num
)
group by name , diff
order by cnt desc
limit 1 by name ;
┌─name─┬───────diff─┬─cnt─┐
│ a │ 2017-02-23 │ 5 │
│ c │ 2017-01-30 │ 4 │
│ b │ 2017-02-05 │ 3 │
└──────┴────────────┴─────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
5.4 关联查询
所有标准 SQL JOIN 支持类型:
- INNER JOIN, only matching rows are returned.
- LEFT OUTER JOIN, non-matching rows from left table are returned in addition to matching rows.
- RIGHT OUTER JOIN, non-matching rows from right table are returned in addition to matching rows.
- FULL OUTER JOIN, non-matching rows from both tables are returned in addition to matching rows.
- CROSS JOIN, produces cartesian product of whole tables, “join keys” are not specified.
JOIN子句可以对左右两张表的数据进行连接,这是最常用的查询子句之一。它的语法包含连接精度和连接类型两部分。
连接精度
连接精度决定了JOIN查询在连接数据时所使用的策略,目前支持ALL、ANY和ASOF三种类型。如果不主动声明,则默认是ALL。可以通过join_default_strictness配置参数修改默认的连接精度类型。
对数据是否连接匹配的判断是通过JOIN KEY进行的,目前只支持等式(EQUAL JOIN)。交叉连接(CROSS JOIN)不需要使用JOIN KEY,因为它会产生笛卡儿积。
-- 准备数据
drop table if exists yg ;
create table yg(
id Int8 ,
name String ,
age UInt8 ,
bid Int8
)engine=Log ;
insert into yg values(1,'AA',23,1) ,
(2,'BB',24,2) ,
(3,'VV',27,1) ,
(4,'CC',13,3) ,
(5,'KK',53,3) ,
(6,'MM',33,3) ;
drop table if exists bm ;
create table bm(
bid Int8 ,
name String
)engine=Log ;
insert into bm values(1,'x'),(2,'Y'),(3,'Z');
drop table if exists gz ;
drop table gz ;
create table gz(
id Int8 ,
jb Int64 ,
jj Int64
)engine=Log ;
insert into gz values (1,1000,2000),(1,1000,2000),(2,2000,1233),(3,2000,3000),(4,4000,1000),(5,5000,2000);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
1)all
如果左表内的一行数据,在右表中有多行数据与之连接匹配,则返回右表中全部连接的数据。而判断连接匹配的依据是左表与右表内的数据,基于连接键(JOIN KEY)的取值完全相等(equal),等同于 =。
SELECT *
FROM yg
ALL INNER JOIN gz ON yg.id = gz.id ;
SELECT *
FROM yg AS inser
ALL JOIN gz ON yg.id = gz.id ;
SELECT *
FROM yg AS inser
JOIN gz ON yg.id = gz.id ;
┌─id─┬─name─┬─age─┬─bid─┬─gz.id─┬───jb─┬───jj─┐
│ 1 │ AA │ 23 │ 1 │ 1 │ 1000 │ 2000 │
│ 1 │ AA │ 23 │ 1 │ 1 │ 1000 │ 2000 │
│ 2 │ BB │ 24 │ 2 │ 2 │ 2000 │ 1233 │
│ 3 │ VV │ 27 │ 1 │ 3 │ 2000 │ 3000 │
│ 4 │ CC │ 13 │ 3 │ 4 │ 4000 │ 1000 │
│ 5 │ KK │ 53 │ 3 │ 5 │ 5000 │ 2000 │
└────┴──────┴─────┴─────┴───────┴──────┴──────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
2)any
如果左表内的一行数据,在右表中有多行数据与之连接匹配,则仅返回右表中第一行连接的数据。ANY与ALL判断连接匹配的依据相同。
SELECT *
FROM yg
ANY INNER JOIN gz ON yg.id = gz.id
┌─id─┬─name─┬─age─┬─bid─┬─gz.id─┬───jb─┬───jj─┐
│ 1 │ AA │ 23 │ 1 │ 1 │ 1000 │ 2000 │
│ 2 │ BB │ 24 │ 2 │ 2 │ 2000 │ 1233 │
│ 3 │ VV │ 27 │ 1 │ 3 │ 2000 │ 3000 │
│ 4 │ CC │ 13 │ 3 │ 4 │ 4000 │ 1000 │
│ 5 │ KK │ 53 │ 3 │ 5 │ 5000 │ 2000 │
└────┴──────┴─────┴─────┴───────┴──────┴──────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
3)asof
asof连接键之后追加定义一个模糊连接的匹配条件asof_column。
drop table if exists emp1 ;
create table emp1(
id Int8 ,
name String ,
ctime DateTime
)engine=Log ;
insert into emp1 values(1,'AA','2021-01-03 00:00:00'),
(1,'AA','2021-01-02 00:00:00'),
(2,'CC','2021-01-01 00:00:00'),
(3,'DD','2021-01-01 00:00:00'),
(4,'EE','2021-01-01 00:00:00');
drop table if exists emp2 ;
create table emp2(
id Int8 ,
name String ,
ctime DateTime
)engine=Log ;
insert into emp2 values(1,'aa','2021-01-02 00:00:00'),
(1,'aa','2021-01-02 00:00:00'),
(2,'cc','2021-01-01 00:00:00'),
(3,'dd','2021-01-01 00:00:00');
-- ASOF inner join
SELECT *
FROM emp2
ASOF INNER JOIN emp1 ON (emp1.id = emp2.id) AND (emp1.ctime > emp2.ctime)
┌─id─┬─name─┬───────────────ctime─┬─emp1.id─┬─emp1.name─┬──────────emp1.ctime─┐
│ 1 │ aa │ 2021-01-02 00:00:00 │ 1 │ AA │ 2021-01-03 00:00:00 │
│ 1 │ aa │ 2021-01-02 00:00:00 │ 1 │ AA │ 2021-01-03 00:00:00 │
└────┴──────┴─────────────────────┴─────────┴───────────┴─────────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
5.5 with模型
- with cube
- with rollup
- with totals
drop table is exists tb_with ;
create table tb_with(
id UInt8 ,
vist UInt8,
province String ,
city String ,
area String
)engine=MergeTree()
order by id ;
insert into tb_with values(1,12,'山东','济南','历下') ;
insert into tb_with values(2,12,'山东','济南','历下') ;
insert into tb_with values(3,12,'山东','济南','天桥') ;
insert into tb_with values(4,12,'山东','济南','天桥') ;
insert into tb_with values(5,88,'山东','青岛','黄岛') ;
insert into tb_with values(6,88,'山东','青岛','黄岛') ;
insert into tb_with values(7,12,'山西','太原','小店') ;
insert into tb_with values(8,12,'山西','太原','小店') ;
insert into tb_with values(9,112,'山西','太原','尖草坪') ;
SELECT
province,
city,
area,
sum(vist)
FROM tb_with
GROUP BY
province,
city,
area
WITH CUBE ;
┌─province─┬─city─┬─area───┬─sum(vist)─┐
│ 山东 │ 青岛 │ 黄岛 │ 176 │
│ 山东 │ 济南 │ 天桥 │ 24 │
│ 山东 │ 太原 │ 尖草坪 │ 112 │
│ 山东 │ 济南 │ 历下 │ 24 │
│ 山西 │ 太原 │ 小店 │ 12 │
│ 山东 │ 太原 │ 小店 │ 12 │
└──────────┴──────┴────────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ 山东 │ 青岛 │ │ 176 │
│ 山东 │ 济南 │ │ 48 │
│ 山西 │ 太原 │ │ 12 │
│ 山东 │ 太原 │ │ 124 │
└──────────┴──────┴──────┴───────────┘
┌─province─┬─city─┬─area───┬─sum(vist)─┐
│ 山东 │ │ 历下 │ 24 │
│ 山东 │ │ 小店 │ 12 │
│ 山东 │ │ 天桥 │ 24 │
│ 山西 │ │ 小店 │ 12 │
│ 山东 │ │ 尖草坪 │ 112 │
│ 山东 │ │ 黄岛 │ 176 │
└──────────┴──────┴────────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ 山西 │ │ │ 12 │
│ 山东 │ │ │ 348 │
└──────────┴──────┴──────┴───────────┘
┌─province─┬─city─┬─area───┬─sum(vist)─┐
│ │ 济南 │ 历下 │ 24 │
│ │ 济南 │ 天桥 │ 24 │
│ │ 太原 │ 尖草坪 │ 112 │
│ │ 青岛 │ 黄岛 │ 176 │
│ │ 太原 │ 小店 │ 24 │
└──────────┴──────┴────────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ │ 青岛 │ │ 176 │
│ │ 济南 │ │ 48 │
│ │ 太原 │ │ 136 │
└──────────┴──────┴──────┴───────────┘
┌─province─┬─city─┬─area───┬─sum(vist)─┐
│ │ │ 天桥 │ 24 │
│ │ │ 小店 │ 24 │
│ │ │ 黄岛 │ 176 │
│ │ │ 历下 │ 24 │
│ │ │ 尖草坪 │ 112 │
└──────────┴──────┴────────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ │ │ │ 360 │
└──────────┴──────┴──────┴───────────┘
SELECT
province,
city,
area,
sum(vist)
FROM tb_with
GROUP BY
province,
city,
area
WITH ROLLUP;
┌─province─┬─city─┬─area───┬─sum(vist)─┐
│ 山东 │ 青岛 │ 黄岛 │ 176 │
│ 山东 │ 济南 │ 天桥 │ 24 │
│ 山东 │ 太原 │ 尖草坪 │ 112 │
│ 山东 │ 济南 │ 历下 │ 24 │
│ 山西 │ 太原 │ 小店 │ 12 │
│ 山东 │ 太原 │ 小店 │ 12 │
└──────────┴──────┴────────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ 山东 │ 青岛 │ │ 176 │
│ 山东 │ 济南 │ │ 48 │
│ 山西 │ 太原 │ │ 12 │
│ 山东 │ 太原 │ │ 124 │
└──────────┴──────┴──────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ 山西 │ │ │ 12 │
│ 山东 │ │ │ 348 │
└──────────┴──────┴──────┴───────────┘
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ │ │ │ 360 │
└──────────┴──────┴──────┴───────────┘
SELECT
province,
city,
area,
sum(vist)
FROM tb_with
GROUP BY
province,
city,
area
WITH TOTALS;
┌─province─┬─city─┬─area───┬─sum(vist)─┐
│ 山东 │ 青岛 │ 黄岛 │ 176 │
│ 山东 │ 济南 │ 天桥 │ 24 │
│ 山东 │ 太原 │ 尖草坪 │ 112 │
│ 山东 │ 济南 │ 历下 │ 24 │
│ 山西 │ 太原 │ 小店 │ 12 │
│ 山东 │ 太原 │ 小店 │ 12 │
└──────────┴──────┴────────┴───────────┘
Totals:
┌─province─┬─city─┬─area─┬─sum(vist)─┐
│ │ │ │ 360 │
└──────────┴──────┴──────┴───────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
六 函数
ClickHouse主要提供两类函数—普通函数和聚合函数。普通函数由IFunction接口定义,拥有数十种函数实现,例如FunctionFormatDateTime、FunctionSubstring等。除了一些常见的函数 ( 诸如四则运算、日期转换等 ) 之外,也不乏一些非常实用的函数,例如网址提取函数、IP地址脱敏函数等。普通函数是没有状态的,函数效果作用于每行数据之上。当然,在函数具体执行的过程中,并不会一行一行地运算,而是采用向量化的方式直接作用于一整列数据。聚合函数由IAggregateFunction接口定义,相比无状态的普通函数,聚合函数是有状态的。以COUNT聚合函数为例,其AggregateFunctionCount的状态使用整UInt64记录。聚合函数的状态支持序列化与反序列化,所以能够在分布式节点之间进行传输,以实现增量计算。
6.1 普通函数
6.1.1 类型转换函数
-
toInt8(expr)
— Results in theInt8
data type. -
toInt16(expr)
— Results in theInt16
data type. -
toInt32(expr)
— Results in theInt32
data type. -
toInt64(expr)
— Results in theInt64
data type. -
toInt128(expr)
— Results in theInt128
data type. -
toInt256(expr)
— Results in theInt256
data type.
SELECT toInt64(nan), toInt32(32), toInt16('16'), toInt8(8.8);
┌─────────toInt64(nan)─┬─toInt32(32)─┬─toInt16('16')─┬─toInt8(8.8)─┐
│ -9223372036854775808 │ 32 │ 16 │ 8 │
└──────────────────────┴─────────────┴───────────────┴─────────────┘
- 1
- 2
- 3
- 4
-
toUInt(8|16|32|64|256)OrZero
-
toUInt(8|16|32|64|256)OrNull
-
toFloat(32|64)
-
toFloat(32|64)OrZero
-
toFloat(32|64)OrNull
-
toDate
-
toDateOrZero
-
toDateOrNull
-
toDateTime
-
toDateTimeOrZero
-
toDateTimeOrNull
-
toDecimal(32|64|128|256)
-
toYYYYMM
toString
now() AS now_local,
toString(now(), 'Asia/Yekaterinburg') AS now_yekat;
┌───────────now_local─┬─now_yekat───────────┐
│ 2016-06-15 00:11:21 │ 2016-06-15 02:11:21 │
└─────────────────────┴─────────────────────┘
- 1
- 2
- 3
- 4
- 5
- CAST(x, T)
Arguments
- `x` — Any type.
- `T` — Destination type. String
**Returned value**
- 1
- 2
- 3
- 4
SELECT
'2016-06-15 23:00:00' AS timestamp,
CAST(timestamp AS DateTime) AS datetime,
CAST(timestamp AS Date) AS date,
CAST(timestamp, 'String') AS string,
CAST(timestamp, 'FixedString(22)') AS fixed_string;
┌─timestamp───────────┬────────────datetime─┬───────date─┬─string──────────────┬─fixed_string──────────
│ 2016-06-15 23:00:00 │ 2016-06-15 23:00:00 │ 2016-06-15 │ 2016-06-15 23:00:00 │ 2016-06-15 23:00:00\0\0\0 │
└─────────────────────┴─────────────────────┴────────────┴─────────────────────┴───────────────────────
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
6.1.2 日期函数
SELECT
toDateTime('2016-06-15 23:00:00') AS time,
toDate(time) AS date_local,
toDate(time, 'Asia/Yekaterinburg') AS date_yekat,
toString(time, 'US/Samoa') AS time_samoa
┌────────────────time─┬─date_local─┬─date_yekat─┬─time_samoa──────────┐
│ 2016-06-15 23:00:00 │ 2016-06-15 │ 2016-06-16 │ 2016-06-15 09:00:00 │
└─────────────────────┴────────────┴────────────┴─────────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- toDate
- toYear
- toMonth
- toHour
- toMinute
- toSecond
- toUnixTimestamp
- date_trunc 将时间截断 date_trunc(unit, value[, timezone])
second
minute
hour
day
week
month
quarter
year
SELECT now(), date_trunc('hour', now());
┌───────────────now()─┬─date_trunc('hour', now())─┐
│ 2021-05-21 13:52:42 │ 2021-05-21 13:00:00 │
└─────────────────────┴───────────────────────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- date_add
date_add(unit, value, date)
second
minute
hour
day
week
month
quarter
year
SELECT date_add(YEAR, 3, toDate('2018-01-01'));
date_diff('unit', startdate, enddate, [timezone])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- date_diff
- date_sub
- timestamp_add
- timestamp_sub
- toYYYYMM
- toYYYYMMDD
- toYYYYMMDDhhmmss
- formatDateTime
%C | year divided by 100 and truncated to integer (00-99) | 20 |
---|---|---|
%d | day of the month, zero-padded (01-31) | 02 |
%D | Short MM/DD/YY date, equivalent to %m/%d/%y | 01/02/18 |
%e | day of the month, space-padded ( 1-31) | 2 |
%F | short YYYY-MM-DD date, equivalent to %Y-%m-%d | 2018-01-02 |
%G | four-digit year format for ISO week number, calculated from the week-based year [defined by the ISO 860 standard, normally useful only with %V | 2018 |
%g | two-digit year format, aligned to ISO 8601, abbreviated from four-digit notation | 18 |
%H | hour in 24h format (00-23) | 22 |
%I | hour in 12h format (01-12) | 10 |
%j | day of the year (001-366) | 002 |
%m | month as a decimal number (01-12) | 01 |
%M | minute (00-59) | 33 |
%n | new-line character (‘’) | |
%p | AM or PM designation | PM |
%Q | Quarter (1-4) | 1 |
%R | 24-hour HH:MM time, equivalent to %H:%M | 22:33 |
%S | second (00-59) | 44 |
%t | horizontal-tab character (’) | |
%T | ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S | 22:33:44 |
%u | ISO 8601 weekday as number with Monday as 1 (1-7) | 2 |
%V | ISO 8601 week number (01-53) | 01 |
%w | weekday as a decimal number with Sunday as 0 (0-6) | 2 |
%y | Year, last two digits (00-99) | 18 |
%Y | Year | 2018 |
%% | a % sign | % |
SELECT formatDateTime(now(), '%D')
┌─formatDateTime(now(), '%D')─┐
│ 05/21/21 │
└─────────────────────────────┘
- 1
- 2
- 3
- 4
- 5
-
FROM_UNIXTIME
SELECT FROM_UNIXTIME(423543535) ┌─FROM_UNIXTIME(423543535)─┐ │ 1983-06-04 10:58:55 │ └──────────────────────────┘
- 1
- 2
- 3
- 4
- 5
6.1.3 条件函数
- if(exp1 , exp2,exp3)
- multiIf()
drop table if exists tb_if;
create table if not exists tb_if(
uid Int16,
name String ,
gender String
)engine = TinyLog ;
insert into tb_if values(1,'zss1','M') ;
insert into tb_if values(2,'zss2','M') ;
insert into tb_if values(3,'zss3','F') ;
insert into tb_if values(4,'zss4','O') ;
insert into tb_if values(5,'zss5','F') ;
--------单条件判断---------
SELECT
*,
if(gender = 'M', '男', '女')
FROM tb_if
┌─uid─┬─name─┬─gender─┬─if(equals(gender, 'M'), '男', '女')─┐
│ 1 │ zss1 │ M │ 男 │
│ 2 │ zss2 │ M │ 男 │
│ 3 │ zss3 │ F │ 女 │
│ 4 │ zss4 │ O │ 女 │
│ 5 │ zss5 │ F │ 女 │
-------------------------------------------------------------
多条件判断
SELECT
*,
multiIf(gender = 'M', '男', gender = 'F', '女', '保密') AS sex
FROM tb_if
┌─uid─┬─name─┬─gender─┬─sex──┐
│ 1 │ zss1 │ M │ 男 │
│ 2 │ zss2 │ M │ 男 │
│ 3 │ zss3 │ F │ 女 │
│ 4 │ zss4 │ O │ 保密 │
│ 5 │ zss5 │ F │ 女 │
└─────┴──────┴────────┴──────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
6.1.6 其他
visitParamExtractRaw('{"abc":"\\n\\u0000"}', 'abc') = '"\\n\\u0000"';
visitParamExtractRaw('{"abc":{"def":[1,2,3]}}', 'abc') = '{"def":[1,2,3]}';
select JSONExtract('{"a":"hello","b":"tom","c":12}', 'Tuple(String,String,UInt8)') as kn;
-- 元组函数
select tupleElement((1,2,3,4,66),5);
-- BitMap 函数
-- bitmapBuild
SELECT
bitmapBuild([1, 2, 3, 4, 5]) AS res,
toTypeName(res)
┌─res─┬─toTypeName(bitmapBuild([1, 2, 3, 4, 5]))─┐
│ │ AggregateFunction(groupBitmap, UInt8) │
└─────┴──────────────────────────────────────────┘
-- bitmapToArray
SELECT bitmapToArray(bitmapBuild([1, 2, 3, 4, 5])) AS res;
-- bitmapSubsetInRange
SELECT bitmapToArray(bitmapSubsetInRange(bitmapBuild([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 100, 200, 500]), toUInt32(30), toUInt32(200))) AS res
┌─res───────────────┐
│ [30,31,32,33,100] │
└───────────────────┘
-- bitmapContains
SELECT bitmapContains(bitmapBuild([1, 5, 7, 9]), toUInt32(9)) AS res
┌─res─┐
│ 1 │
└─────┘
-- bitmapHasAny 有任意一个元素
SELECT bitmapHasAny(bitmapBuild([1, 2, 3]), bitmapBuild([3, 4, 5])) AS res
┌─res─┐
│ 1 │
└─────
-- bitmapHasAll 有任意一个元素
-- bitmapMin
-- bitmapMax
-- bitmapAnd 交集
-- bitmapOr 并集
-- bitmapAndnot差集
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
七 分布式
1 在集群的每个节点上安装ck服务
2 <listen_host>::<listen_host>
service clickhouse-server restart
clickhouse-client -h ck01 链接 8123
3 配置zookeeper 正常启动
- 1
- 2
- 3
- 4
- 5
在页面请求http://ck01:8123/play
集群是副本和分片的基础,它将ClickHouse的服务拓扑由单节点延 伸到多个节点,但它并不像Hadoop生态的某些系统那样,要求所有节点组成一个单一的大集群。ClickHouse的集群配置非常灵活,用户既可以将所有节点组成一个单一集群,也可以按照业务的诉求,把节点划分为多个小的集群。在每个小的集群区域之间,它们的节点、分区和副本数量可以各不相同
另一种是从功能作用层面区分,使用副本的主要目的是防止数据丢失,增加数据存储的冗余;而使用分片的主要目的是实现数据的水平切分,
经讲过MergerTree的命名规则。如果在*MergeTree的前面增加Replicated的前缀,则能够组合 成一个新的变种引擎,即Replicated-MergeTree复制表!
只有使用了ReplicatedMergeTree复制表系列引擎,才能应用副本的能力(后面会介绍另一种副本的实现方式)。或者用一种更为直接的方式理解,即使用ReplicatedMergeTree的数据表就是副本。 ReplicatedMergeTree是MergeTree的派生引擎,它在MergeTree的 基础上加入了分布式协同的能力,
在MergeTree中,一个数据分区由开始创建到全部完成,会历经两类存储区域。
(1)内存:数据首先会被写入内存缓冲区。
(2)本地磁盘:数据接着会被写入tmp临时目录分区,待全部完成后再将临时目录重命名为正式分区。
ReplicatedMergeTree在上述基础之上增加了ZooKeeper的部分,它会进一步在ZooKeeper内创建一系列的监听节点,并以此实现多个实例之间的通信。在整个通信过程中,ZooKeeper并不会涉及表数据的传输。
- 依赖ZooKeeper:在执行INSERT和ALTER查询的时候,ReplicatedMergeTree需要借助ZooKeeper的分布式协同能力,以实现多个副本之间的同步。但是在查询副本的时候,并不需要使用 ZooKeeper。关于这方面的更多信息,会在稍后详细介绍。
- 表级别的副本:副本是在表级别定义的,所以每张表的副本配置都可以按照它的实际需求进行个性化定义,包括副本的数量,以及副本在集群内的分布位置等。
- 多主架构(Multi Master):可以在任意一个副本上执行INSERT和ALTER查询,它们的效果是相同的。这些操作会借助ZooKeeper的协同能力被分发至每个副本以本地形式执行。
- Block数据块:在执行INSERT命令写入数据时,会依据 max_insert_block_size的大小(默认1048576行)将数据切分成若干个Block数据块。所以Block数据块是数据写入的基本单元,并且具有 写入的原子性和唯一性。
- 原子性:在数据写入时,一个Block块内的数据要么全部写入成功,要么全部失败。
- 唯一性:在写入一个Block数据块的时候,会按照当前Block数据块的数据顺序、数据行和数据大小等指标,计算Hash信息摘要并记录在案。在此之后,如果某个待写入的Block数据块与先前已被写入的 Block数据块拥有相同的Hash摘要(Block数据块内数据顺序、数据大小和数据行均相同),则该Block数据块会被忽略。这项设计可以预防由异常原因引起的Block数据块重复写入的问题。
7.0 分片概念
通过引入数据副本,虽然能够有效降低数据的丢失风险(多份存储),并提升查询的性能(分摊查询、读写分离),但是仍然有一个问题没有解决,那就是数据表的容量问题。到目前为止,每个副本自
身,仍然保存了数据表的全量数据。所以在业务量十分庞大的场景中,依靠副本并不能解决单表的性能瓶颈。想要从根本上解决这类问题,需要借助另外一种手段,即进一步将数据水平切分,也就是我们将要介绍的数据分片。ClickHouse中的每个服务节点都可称为一个shard(分片)。从理论上来讲,假设有N(N>=1)张数据表A,分布在N个ClickHouse服务节点,而这些数据表彼此之间没有重复数据,那么就可以说数据表A拥有N个分片。然而在工程实践中,如果只有这些分片表,那么整个 Sharding(分片)方案基本是不可用的。对于一个完整的方案来说,还需要考虑数据在写入时,如何被均匀地写至各个shard,以及数据在查询时,如何路由到每个shard,并组合成结果集。所以,ClickHouse
的数据分片需要结合Distributed表引擎一同使用
Distributed表引擎自身不存储任何数据,它能够作为分布式表的一层透明代理,在集群内部自动开展数据的写入、分发、查询、路由等工作
7.1 配置zookeeper
需要在每台CK的节点上配置ZK的位置
ClickHouse使用一组zookeeper标签定义相关配置,默认情况下,在全局配置中定义即可。但是各个副本所使用的Zookeeper 配置通常是相同的,为了便于在多个节点之间复制配置文件,更常见的做法是将这一部分配置抽离出来,独立使用一个文件保存。
首先,在文件中配置
<zookeeper-servers>
<node index="1">
<host>doit01</host>
<port>2181</port>
</node>
<node index="2">
<host>doit02</host>
<port>2181</port>
</node>
<node index="3">
<host>doit03</host>
<port>2181</port>
</node>
</zookeeper-servers>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
incl与配置文件内的节点名称要彼此对应。至此,整个配置过程就完成了。
ClickHouse在它的系统表中,颇为贴心地提供了一张名为zookeeper的代理表。通过这张表,可以使用SQL查询的方式读取远端ZooKeeper内的数据。有一点需要注意,在用于查询的SQL语句中,必须指定path条件,
将配置文件同步到其他集群节点!!
scp linux02:$PWD
scp linux03:$PWD
重启服务
- 1
- 2
- 3
7.2 创建副本表
在创建副本表以前, 首先要启动集群中的zookeeper
首先,由于增加了数据的冗余存储,所以降低了数据丢失的风险;其次,由于副本采用了多主
架构,所以每个副本实例都可以作为数据读、写的入口,这无疑分摊了节点的负载。
在使用单使用副本功能的时候 , 我们对CK集群不需要任何的配置就可以实现数据的多副本存储!只需要在建表的时候指定engine和ZK的位置即可 ;
ENGINE = ReplicatedMergeTree('zk_path', 'replica_name')
-- /clickhouse/tables/{shard}/table_name
-- /clickhouse/tables/ 是约定俗成的路径固定前缀,表示存放数据表的根路径。
- 1
- 2
- 3
- 4
- 5
·{shard}表示分片编号,通常用数值替代,例如01、02、03。一张数据表可以有多个分片,而每个分片都拥有自己的副本。
·table_name表示数据表的名称,为了方便维护,通常与物理表的名字相同(虽然ClickHouse并不强制要求路径中的表名称和物理表名相同);而replica_name的作用是定义在ZooKeeper中创建的副本名称,该名称是区分不同副本实例的唯一标识。一种约定成俗的命名方式是使用所在服务器的域名称。
对于zk_path而言,同一张数据表的同一个分片的不同副本,应该定义相同的路径;而对于replica_name而言,同一张数据表的同一个分片的不同副本,应该定义不同的名称
1) 一个分片 , 多个副本表
-- lixnu01 机器
create table tb_demo1 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo1', 'linux01')
order by id ;
-- lixnu02 机器
create table tb_demo1 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo1', 'linux02')
order by id ;
-- lixnu03 机器
create table tb_demo1 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo1', 'linux03')
order by id ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
查看zookeeper中的内容
[zk: localhost:2181(CONNECTED) 0] ls /
[a, zookeeper, clickhouse, DNS, datanode1, server1, hbase]
[zk: localhost:2181(CONNECTED) 1] ls /clickhouse
[tables, task_queue]
[zk: localhost:2181(CONNECTED) 2] ls /clickhouse/tables
[01]
[zk: localhost:2181(CONNECTED) 3] ls /clickhouse/tables/01
[tb_demo1]
[zk: localhost:2181(CONNECTED) 4] ls /clickhouse/tables/01/tb_demo1
[metadata, temp, mutations, log, leader_election, columns, blocks, nonincrement_block_numbers, replicas, quorum, block_numbers]
[zk: localhost:2181(CONNECTED) 5] ls /clickhouse/tables/01/tb_demo1/replicas
[linux02, linux03, linux01]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
SELECT *
FROM system.zookeeper
WHERE path = '/' ;
- 1
- 2
- 3
在任何一台节点上,插入数据, 在其他节点上都能同步数据
- 1
2) 两个分片 , 一个分片有副本一个分片没有副本
-- lixnu01 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo2', 'linux01')
order by id ;
-- lixnu02 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo2', 'linux02')
order by id ;
-- lixnu03 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo2', 'linux03')
order by id ;
-------------------
-- lixnu01 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo2', 'linux01')
order by id ;
-- lixnu02 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/01/tb_demo2', 'linux02')
order by id ;
-- lixnu03 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/02/tb_demo2', 'linux03')
order by id ;
-- lixnu04 机器
create table tb_demo2 (
id Int8 ,
name String)engine=ReplicatedMergeTree('/clickhouse/tables/02/tb_demo2', 'linux03')
order by id ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
7.3 分布式引擎
Distributed表引擎是分布式表的代名词,它自身不存储任何数据,而是作为数据分片的透明代理,能够自动路由数据至集群中的各个节点,所以Distributed表引擎需要和其他数据表引擎一起协同工作,
一般使用分布式表的目的有两种,
-
一种是表存储多个副本并且有大量的并发操作,我们可以使用分布式表来分摊请求压力解决并发问题
-
一种是表特别大有多个切片组成 ,并且每切片数据也可以存储数据副本
-
本地表:通常以_local为后缀进行命名。本地表是承接数据的载体,可以使用非Distributed的任意表引擎,一张本地表对应了一个数据分片
-
分布式表:通常以_all为后缀进行命名。分布式表只能使用Distributed表引擎,它与本地表形成一对多的映射关系,日后将通过分布式表代理操作多张本地表。
ENGINE = Distributed(cluster, database, table [,sharding_key])
- 1
-
cluster:集群名称,与集群配置中的自定义名称相对应。在对分布式表执行写入和查询的过程中,它会使用集群的配置信息来找到相应的host节点。
-
database和table:分别对应数据库和表的名称,分布式表使用这组配置映射到本地表。
-
sharding_key:分片键,选填参数。在数据写入的过程中,分布式表会依据分片键的规则,将数据分布到各个host节点的本地表。
7.3.1 没有副本
本示例是,使用某个集群 , 创建多分片无副本的表配置了一个集群 cluster1 集群中有三台机器ck1 ck2 ck3,没有副本,如果在这个集群上建表, 表数据会有三个切片 ,没有存储数据副本
<clickhouse_remote_servers>
<cluster1>
<!-- 集群名为cluster1 整个集群中每个表有三个分片,分别在lx01 lx02 lx03上 -->
<shard>
<replica>
<host>linux01</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>linux02</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>linux03</host>
<port>9000</port>
</replica>
</shard>
</cluster1>
<cluster2>
<!-- 集群名为cluster2 一个切片 三个副本 -->
<shard>
<replica>
<host>linux01</host>
<port>9000</port>
</replica>
<replica>
<host>linux02</host>
<port>9000</port>
</replica>
<replica>
<host>linux03</host>
<port>9000</port>
</replica>
</shard>
</cluster2>
<!--集群三 多个分片 保留副本 注意一个主机只使用一次 -->
<cluster3>
<shard>
<replica>
<host>doit01</host>
<port>9000</port>
</replica>
<replica>
<host>doit02</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>doit03</host>
<port>9000</port>
</replica>
<replica>
<host>doit04</host>
<port>9000</port>
</replica>
</shard>
</cluster3>
</clickhouse_remote_servers>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
同步配置文件 到集群中
-- 创建本地表
create table tb_demo3 on cluster cluster1(
id Int8 ,
name String
)engine=MergeTree()
order by id ;
-- 创建分布式表
create table demo3_all on cluster cluster1 engine=Distributed('cluster1','default','tb_demo3',id) as tb_demo3 ;
--向分布式表中插入数据 ,数据会根据插入规则将数据插入到不同的分片中
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
7.3.2 有副本的配置
<!-- 配置集群2 , 集群中的表有两个分片 ,其中分片1 有两个副本 -->
<cluster2>
<shard>
<replica>
<host>linux01</host>
<port>9000</port>
</replica>
<replica>
<host>linux02</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>linux03</host>
<port>9000</port>
</replica>
</shard>
</cluster2>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
-- 创建本地表
create table tb_demo4 on cluster cluster2(
id Int8 ,
name String
)engine=MergeTree()
order by id ;
-- 创建分布式表
create table demo4_all on cluster cluster2 engine=Distributed('cluster2','default','tb_demo4',id) as tb_demo4 ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
第一 :
1) 数据1分片的时候 , 多个副本 可以不使用分布式表
第二:
2)有多个分片的时候使用分布式表 给分片分配数据
多个分片的表
多个副本表
多分片 多副本
一个节点在一个集群中只能使用一次
- 1
- 2
- 3
- 4
7.4 分布式DDL
ClickHouse支持集群模式,一个集群拥有1到多个节点。CREATE、ALTER、DROP、RENMAE及TRUNCATE这些DDL语句,都支持分布式执行。这意味着,如果在集群中任意一个节点上执行DDL语句,那么集群中的 每个节点都会以相同的顺序执行相同的语句。这项特性意义非凡,它就如同批处理命令一样,省去了需要依次去单个节点执行DDL的烦恼。将一条普通的DDL语句转换成分布式执行十分简单,只需加上ON CLUSTER cluster_name声明即可。例如,执行下面的语句后将会对 ch_cluster集群内的所有节点广播这条DDL语句:
-- 建表 on cluster cluster1
create table tb_demo3 on cluster cluster1(
id Int8 ,
name String
)engine=MergeTree()
order by id ;
-- 删除集群中所有的本地表或者是分布式表
drop table if exists tb_demo3 on cluster cluster1;
-- 修改集群中的表结构
alter table t3 on cluster cluster1 add column age Int8 ;
-- 删除字段
-- 删除分区
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
7.5 分布式协同原理
副本协同的核心流程主要有INSERT、MERGE、MUTATION和ALTER四种,分别对应了数据写入、分区合并、数据修改和元数据修改。INSERT和ALTER查询是分布式执行的。借助 ZooKeeper的事件通知机制,多个副本之间会自动进行有效协同,但是它们不会使用ZooKeeper存储任何分区数据。而其他查询并不支持分布式执行,包括SELECT、CREATE、DROP、RENAME和ATTACH。例如,为了创建多个副本,我们需要分别登录每个ClickHouse节点。接下来,会依次介绍上述流程的工作机理。为了便于理解,我先来整体认识一下各个流程的介绍方法。
7.5.1 insert原理
7.5.2 Merge原理
无论MERGE操作从哪个副本发起,其合并计划都会交由主副本来制定,和insert一样
7.5.3 mutation原理
alter table x update name=zss where
alter table x delete where
当对ReplicatedMergeTree执行ALTER DELETE或者ALTER UPDATE操作的时候,即会进入MUTATION部分的逻辑,它的核心流程如图
7.5.4 alter原理
当对ReplicatedMergeTree执行ALTER操作进行元数据修改的时候,即会进入ALTER部
分的逻辑,例如增加、删除表字段等。
八 应用案例
1 用户和权限
在中添加用户配置
<?xml version="1.0"?>
<yandex>
<profiles>
<default>
<max_memory_usage>10000000000</max_memory_usage>
<use_uncompressed_cache>0</use_uncompressed_cache>
<load_balancing>random</load_balancing>
</default>
<readonly>
<readonly>1</readonly>
</readonly>
</profiles>
<users>
<default>
<password></password>
<networks incl="networks" replace="replace">
<ip>::/0</ip>
</networks>
<profile>default</profile>
<quota>default</quota>
</default>
<!-- 定义一个用户 用户名为hangge 密码使用明文 root -->
<hangge>
<password_sha256_hex>f493c8a7a3c37088731336766459cc37e4b094e95b918038726660cc42013fcd</password_sha256_hex>
<networks incl="networks" replace="replace">
<ip>::/0</ip>
</networks>
<profile>default</profile>
<quota>default</quota>
</hangge>
</users>
<quotas>
<default>
<interval>
<duration>3600</duration>
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</yandex>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
明文密码
<password></password> 里面没有配置说明是没有密码
- 1
SHA256加密:
在使用SHA256加密算法的时候,需要通过password_sha256_hex标签定义密码
[root@ck1 ~]# echo -n hangge | openssl dgst -sha256
(stdin)= f493c8a7a3c37088731336766459cc37e4b094e95b918038726660cc42013fcd
- 1
- 2
- 3
<hangge>
<password_sha256_hex>f493c8a7a3c37088731336766459cc37e4b094e95b918038726660cc42013fcd</password_sha256_hex>
<networks incl="networks" replace="replace">
<ip>::/0</ip>
</networks>
<profile>default</profile>
<quota>default</quota>
</hangge>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
double_sha1加密:
在使用double_sha1加密算法的时候,则需要通过 password_double_sha1_hex标签定义密码,
<password_double_sha1_hex>23ae809ddacaf96af0fd78ed04b6a265e05aa257</password_double_sha1_hex>
\# echo -n 123 | openssl dgst -sha1 -binary | openssl dgst -sha1
(stdin)= 23ae809ddacaf96af0fd78ed04b6a265e05aa257
- 1
- 2
- 3
- 4
- 5
用户权限控制’
<hangge>
<password_sha256_hex>60cd41aedc4e47e8883682b416109e7b7e345e15decc63c2c98ecdab5e8e053a</password_sha256_hex>
<networks incl="networks" />
<profile>readonly</profile>
<quota>default</quota>
<allow_databases>
<database>default</database>
</allow_databases>
</hangge>
## 3 JDBC和客户端工具
### 3.1 JDBC
```xml
<!-- /artifact//clickhouse-jdbc -->
<dependency>
<groupId>ru.yandex.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.2.4</version>
</dependency>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
public class Demo1 {
public static void main(String[] args) throws Exception {
Class.forName("");
String url = "jdbc:clickhouse://linux01:8123/default";
String username = "default";
String password = "";
Connection con = DriverManager.getConnection(url, username, password);
Statement stmt = con.createStatement();
ResultSet resultSet = stmt.executeQuery("select * from tb_demo1");
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
System.out.println(id + ":" + name);
}
con.close();
stmt.close();
resultSet.close();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
高可用模式允许设置多个host地址,每次会从可用的地址中随机选择一个进行连接,在高可用模式下,需要通过BalancedClickhouseDataSource对象获取连接
public static void main(String[] args) throws Exception {
// 初始化驱动
Class.forName("");
// url
String url = "jdbc:clickhouse://linux01:8123,linux02:8123,linux03:8123/default";
//设置JDBC参数
ClickHouseProperties clickHouseProperties = new ClickHouseProperties();
clickHouseProperties.setUser("default");
//声明数据源
BalancedClickhouseDataSource balanced = new BalancedClickhouseDataSource(url, clickHouseProperties);
//对每个host进行ping操作, 排除不可用的dead连接
balanced.actualize();
//获得JDBC连接
Connection con = balanced.getConnection();
Statement stmt = con.createStatement();
ResultSet resultSet = stmt.executeQuery("select * from demo3_all");
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
System.out.println(id + ":" + name);
}
con.close();
stmt.close();
resultSet.close();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
3.2 客户端工具DBeaver
/#!/login 提供的一个页面可视化工具
4 用户行为分析
4.1 windowFunnel函数
(参数一) 时间的单位 窗口的大小 时间的单位 (时间 , 事件链条)
uid1 event1 1551398404
uid1 event2 1551398406
uid1 event3 1551398408
uid2 event2 1551398412
uid2 event3 1551398415
uid3 event3 1551398410
uid3 event4 1551398413
————————————————
-- 建表
drop table if exists test_funnel ;
CREATE TABLE test_funnel(
uid String,
eventid String,
eventTime UInt64)
ENGINE = MergeTree
ORDER BY (uid, eventTime) ;
-- 导入数据
insert into test_funnel values
('uid1','event1',1551398404),
('uid1','event2',1551398406),
('uid1','event3',1551398408),
('uid2','event2',1551398412),
('uid2','event3',1551398415),
('uid3','event3',1551398410),
('uid3','event4',1551398413);
-- 查看数据
┌─uid──┬─eventid─┬──eventTime─┐
│ uid1 │ event1 │ 1551398404 │
│ uid1 │ event2 │ 1551398406 │
│ uid1 │ event3 │ 1551398408 │
│ uid2 │ event2 │ 1551398412 │
│ uid2 │ event3 │ 1551398415 │
│ uid3 │ event3 │ 1551398410 │
│ uid3 │ event4 │ 1551398413 │
└──────┴─────────┴────────────┘
select
uid ,
windowFunnel(4)(
toDateTime(eventTime),
eventid='event1' ,
eventid='event2' ,
eventid='event3'
) as funnel
from
test_funnel
group by uid ;
┌─uid──┬─funnel─┐
│ uid3 │ 0 │
│ uid1 │ 3 │
│ uid2 │ 0 │
└──────┴────────┘
select
uid ,
windowFunnel(4)(
toDateTime(eventTime),
eventid='event2' ,
eventid='event3'
) as funnel
from
test_funnel
group by uid ;
┌─uid──┬─funnel─┐
│ uid3 │ 0 │
│ uid1 │ 2 │
│ uid2 │ 2 │
└──────┴────────┘
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
案例
建表 导入数据
clickhouse-client -q 'insert into test1.ods_log format JSONAsString' < event.log
drop table if exists test_log ;
create table test_log engine=MergeTree() order by (id,ts)
as
with visitParamExtractUInt(line,'timeStamp') as ts ,
visitParamExtractString(line ,'account')as account,
visitParamExtractString(line ,'deviceId')as deviceId,
visitParamExtractString(line ,'sessionId')as sessionId,
visitParamExtractString(line ,'ip')as ip,
visitParamExtractString(line ,'eventId')as eventId,
visitParamExtractRaw(line ,'properties')as properties
select
if(account='' , deviceId , account) id ,
account ,
deviceId,
sessionId,
ip,
eventId,
properties,
ts
from
tb_ods_log ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
select
id ,
windowFunnel(100000)(
toDateTime(ts),
eventId='productView' ,
eventId='adClick' ,
eventId='productView' ,
eventId='collect'
) as funnel
from
test_log
group by id ;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
4.2 sequenceCount
sequenceCount满足要求的次数
SELECT id,
sequenceCount('(?1)')
(
FROM_UNIXTIME(ts) ,
eventId='adShow' ,
eventId='productView' ,
eventId='collect' ,
eventId='addCart' )AS cnt
FROM test_log
GROUP BY id having id='0T7136zA3BZI';
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
4.3 sequenceMatch
这个函数都需要指定模式串、时间列和期望的事件序列(最多可指定32个事件)。模式串的语法有以下三种:
(?N):表示时间序列中的第N个事件,从1开始。例如上述SQL中,(?2)即表示event_type = ‘shtKkclick’ AND column_type = ‘homePage’。
(?t op secs):插入两个事件之间,表示它们发生时需要满足的时间条件(单位为秒)。例如上述SQL中,(?1)(?t<=15)(?2)即表示事件1和2发生的时间间隔在15秒以内。
.*:表示任意的非指定事件。
SELECT
id,
sequenceMatch('(?1)(?t<=10)(?2)(?3).*(?4)')(
FROM_UNIXTIME(ts) ,
eventId='adClick' ,
eventId='productView' ,
eventId='collect' ,
eventId='addCart'
) AS is_match
FROM log
GROUP BY id
having id='dGHDHV7WOrpJ';
SELECT
id,
sequenceCount('(?1)(?t<=10)(?2)(?3).*(?4)')(
FROM_UNIXTIME(ts) ,
eventId='adClick' ,
eventId='productView' ,
eventId='collect' ,
eventId='addCart'
) AS cnt
FROM test_log
GROUP BY id ;
having id='dGHDHV7WOrpJ 0T7136zA3BZI';
select id ,eventId from test_log
where id = 'dGHDHV7WOrpJ'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32