Clickhouse是俄罗斯yandex公司于2016年开源的一个列式数据库管理系统,在OLAP领域像一匹黑马一样,以其超高的性能受到业界的青睐。特性:
不足:
OLTP是Online transaction processing的英文缩写,指在线/联机事务处理,这么说其实还是比抽象的。OLTP典型的应用领域包括银行、证劵等金融行业,电子商务系统等,在此举最经典的银行例子,我们在招商银行APP上查询账户余额、收支信息和转账记录,在ATM机上存钱,取钱,将招行账号的钱转到工行账号上。这些都是典型的OLTP类操作,这些操作都比较简单,主要是对数据库中的数据进行增删改查。操作主体一般是产品的用户。
OLAP是Online analytical processing的英文缩写,指联机分析处理。从字面上我们能看出是做分析类操作。通过分析数据库中的数据来得出一些结论性的东西。比如给老总们看的报表,用于进行市场开拓的用户行为统计,不同维度的汇总分析结果等等。操作主体一般是运营、销售和市场等团队人员而不是用户。
单次OLTP处理的数据量比较小,所涉及的表非常有限,一般仅一两张表。而OLAP是为了从大量的数据中找出某种规律性的东西,经常用到count()、sum()和avg()等聚合方法,用于了解现状并为将来的计划/决策提供数据支撑,所以对多张表的数据进行连接汇总非常普遍。
为了表示跟OLTP的数据库(database)在数据量和复杂度上的不同,一般称OLAP的操作对象为数据仓库(data warehouse),简称数仓。数据库仓库中的数据,往往来源于多个数据库,以及相应的业务日志。
列式存储是指一列中的数据在存储介质中是连续存储的;行式存储是指一行中的数据在存储介质中是连续存储的。简单的说,你可以把列式数据库认为是每一列都是一个表,这个表只有一列,如果只在该列进行条件查询,速度就很快。
行数据库适用于读取出少行,多列的情况;列数据库相反,适用于读取出少数列,多数行的情况。
列数据库可以节省空间,如果某一行的某一列没有数据,那在列存储时,就可以不存储该列的值。
传统的行式数据库将一个个完整的数据行存储在数据页中。这种方式在大数据量查询的时候会出现以下问题:在没有索引的情况下,会把一行全部查出来,查询会使用大量IO;虽然建立索引和物化视图可以可以快速定位列,但是也需要花费大量时间。但是如果处理查询时需要用到大部分的数据列,这种方式在磁盘IO上是比较高效的。
一般来说,OLTP(Online Transaction Processing,联机事务处理)应用适合采用这种方式。
列式数据库是将同一个数据列的各个值存放在一起。插入某个数据行时,该行的各个数据列的值也会存放到不同的地方。因此,列式数据库大大地提高了OLAP大数据量查询的效率。当然,列式数据库不是万能的,每次读取某个数据行时,需要分别从不同的地方读取各个数据列的值,然后合并在一起形成数据行。因此,如果每次查询涉及的数据量较小或者大部分查询都需要整行的数据,列式数据库并不适用。
1.绝大多数请求都是用于读访问的
2.数据需要以大批次(大于1000行)进行更新,而不是单行更新;或者根本没有更新操作
3.数据只是添加到数据库,没有必要修改
4.读取数据时,会从数据库中提取出大量的行,但只用到一小部分列
5.表很“宽”,即表中包含大量的列
6.查询频率相对较低(通常每台服务器每秒查询数百次或更少)
7.对于简单查询,允许大约50毫秒的延迟
8.列的值是比较小的数值和短字符串(例如,每个URL只有60个字节)
9.在处理单个查询时需要高吞吐量(每台服务器每秒高达数十亿行)
10.不需要事务
11.数据一致性要求较低
12.每次查询中只会查询一个大表。除了一个大表,其余都是小表
13.查询结果显著小于数据源。即数据有过滤或聚合。返回结果不超过单个服务器内存大小
Log家族具有最小功能的轻量级引擎。当您需要快速写入许多小表(最多约100w行)并在以后整体读取它们时,该类型的引擎是最有效的。
1、TinyLog引擎
2、StripeLog引擎(数据分块列在一起)
3、Log引擎(数据分块记录偏移量)
MergeTree系列的表引擎支持主键索引、数据分区、数据副本和数据采样这些特性,同时也只有此系列的表引擎支持ALTER相关操作
特点:
create table tb_merge_tree(
id Int8 ,
name String ,
ctime Date
)engine=MergeTree()
partition by name --选填,如果不声明分区键,则ClickHouse会生成一个名为all的分区。
order by id --必填,默认情况下主键(PRIMARY KEY)与排序键相
primary id --选填,主键字段生成一级索引,主键允许存在重复数据
sample by intHash32(id) --选填,用于声明数据以何种标准进行采样
settings index_granularity = 8192, --选填,索引的粒度
index_granularity_bytes = 0, --自适应间隔大小,根据每一批次写入数据的体量大小,动态划分间隔大小。0表示不启动自适应功能。
enable_mixed_granularity_parts = true, --选填,是否开启自适应索引间隔的功能,默认开启
merge_with_ttl_timeout = 10, --选填,数据TTL功能
storage_policy = 1; --选填,多路径存储策略
MergeTree表引擎中的数据是拥有物理存储的,数据会按照分区目录的形式保存到磁盘上。
一张数据表的完整物理结构分为3个层级,依次是数据表目录、分区目录及各分区下具体的数据文件。
CollapsingMergeTree是一种通过以增代删的思路,支持行级数据修改和删除的表引擎。
通过定义一个sign标记位字段,记录数据行的状态。如果sign标记为1,则表示这是一行有效的数据;如果sign标记为-1,则表示这行数据需要被删除。当CollapsingMergeTree分区合并时,同一数据分区内,sign标记为1和-1的一组数据会被抵消删除。
**注意:**CollapsingMergeTree虽然解决了主键相同的数据即时删除的问题,但是状态持续变化且多线程并行写入情况下,状态行与取消行位置可能乱序,导致无法正常折叠。只有保证老的状态行在在取消行的上面, 新的状态行在取消行的下面! 但是多线程无法保证写的顺序!
解决CollapsingMergeTree乱序写入情况下无法正常删除(折叠)问题,在建表语句中新增了一列version,用于在乱序情况下记录状态行与取消行的对应关系
主键(排序)相同,且version相同,sign相反的行,在合并时会被删除。
只需要查询汇总结果,不关心明细数据。并且数据的汇总条件是预先明确的(group by条件明确,不会随意改变)
通过定义AggregateFunction(聚合函数,数据类型) 决定针对哪些列字段计算。
写入时需要使用-state语法,查询时使用-merge语法。
clickhouse可以直接从HDFS中指定的目录下加载数据,自己不存储数据,仅仅读取数据
engine=HDFS('hdfs://linux01:8020/ck/test/*','csv')
-clickhouse支持的文件格式有CSV、TSV、JSON等
engine=MYSQL('localhost:3306','test','test','root','123456')
基本上来说传统关系型数据库(以MySQL为例)的SQL语句,基本支持但是也有不一样的地方
用于创建指定名称的数据库,语法如下:CREATE DATABASE [IF NOT EXISTS] db_name
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster]
(
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2],
...
) ENGINE = engine
// DEFAULT expr – 默认值,用法与SQL类似。
// MATERIALIZED expr – 物化表达式,被该表达式指定的列不能被INSERT,因为它总是被计算出来的,对于INSERT而言,不需要考虑这些列。 另外,在SELECT查询中如果包含星号,此列不会被查询。
// ALIAS expr – 别名。
// 有三种方式创建表:
1)直接创建
:) create table t1(id UInt16,name String) engine=TinyLog
2)创建一个与其他表具有相同结构的表
CREATE TABLE [IF NOT EXISTS] [db.]table_name AS [db2.]name2 [ENGINE = engine]
可以对其指定不同的表引擎声明。如果没有表引擎声明,则创建的表将与db2.name2使用相同的表引擎。
3)使用指定的引擎创建一个与SELECT子句的结果具有相同结构的表,并使用SELECT子句的结果填充它。
CREATE TABLE [IF NOT EXISTS] [db.]table_name ENGINE = engine AS SELECT ...
// 主要用于向表中添加数据,基本格式如下:INSERT INTO [db.]table [(c1, c2, c3)] VALUES (v11, v12, v13), (v21, v22, v23), ...
// 实例:insert into t1 values(1,'zhangsan'),(2,'lisi'),(3,'wangwu')
// 还可以使用select来写入数据,基本格式如下:INSERT INTO [db.]table [(c1, c2, c3)] SELECT ...
// 实例:insert into t2 select * from t3
ClickHouse提供了Delete 和Update的能力,这类操作被称为Mutation查询,它可以看做Alter 的一种。
虽然可以实现修改和删除,但是和一般的OLTP数据库不一样,Mutation语句是一种很“重”的操作,而且不支持事务。
“重”的原因主要是每次修改或者删除都会导致放弃目标数据的原有分区,重建新分区。所以尽量做批量的变更,不要进行频繁小数据的操作。
// 删除操作
alter table t_order_smt delete where sku_id ='sku_001';
// 修改操作
alter table t_order_smt update total_amount=toDecimal32(2000.00,2) where id =102;
// 由于操作比较“重”,所以 Mutation语句分两步执行,同步执行的部分其实只是进行新增数据新增分区和并把旧分区打上逻辑上的失效标记。知道触发分区合并的时候,才会删除旧数据释放磁盘空间。
clickhouse基本上与标准SQL 差别不大。
支持子查询
支持CTE(with 子句)
支持各种JOIN, 但是JOIN操作无法使用缓存,所以即使是两次相同的JOIN语句,Clickhouse也会视为两条新SQL。
不支持窗口函数。
不支持自定义函数。
GROUP BY 操作增加了 with rollup\with cube\with total 用来计算小计和总计。
create table t_order_mt(
id UInt32,
sku_id String,
total_amount Decimal(16,2),
create_time Datetime
) engine = MergeTree
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id,sku_id);
insert into t_order_mt values(101,'sku_001',1000.00,'2020-08-01 12:00:00’);
insert into t_order_mt values(102,'sku_002',2000.00,'2020-08-01 12:00:00');
insert into t_order_mt values(103,'sku_004',2500.00,'2020-08-01 12:00:00');
insert into t_order_mt values(104,'sku_002',2000.00,'2020-08-01 12:00:00');
insert into t_order_mt values(105,'sku_003',600.00,'2020-08-02 12:00:00’);
insert into t_order_mt values(105,'sku_004',600.00,'2020-08-02 12:00:00');
insert into t_order_mt values(106,'sku_001',1000.00,'2020-08-04 12:00:00');
insert into t_order_mt values(107,'sku_002',2000.00,'2020-08-04 12:00:00');
insert into t_order_mt values(108,'sku_004',2500.00,'2020-08-04 12:00:00');
insert into t_order_mt values(109,'sku_002',2000.00,'2020-08-04 12:00:00');
insert into t_order_mt values(110,'sku_003',600.00,'2020-08-01 12:00:00');
// with rollup : 从右至左去掉维度进行小计。
select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with rollup;
// with cube : 从右至左去掉维度进行小计,再从左至右去掉维度进行小计。
select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with cube;
// with totals: 只计算合计。
select id , sku_id,sum(total_amount) from t_order_mt group by id,sku_id with totals;
// ALTER只支持MergeTree系列,Merge和Distributed引擎的表,基本语法:ALTER TABLE [db].name [ON CLUSTER cluster] ADD|DROP|MODIFY COLUMN ...
// 参数解析:
ADD COLUMN – 向表中添加新列
DROP COLUMN – 在表中删除列
MODIFY COLUMN – 更改列的类型
// 案例演示:
新增字段:alter table tableName add column newcolname String after col1
修改字段类型:alter table tableName modify column newcolname String;
删除字段:alter table tableName drop column newcolname;
// 查看表结构
hadoop102 :) desc dis_table;
DESCRIBE TABLE dis_table
┌─name─┬─type───┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─┐
│ id │ UInt16 │ │ │ │ │ │
│ name │ String │ │ │ │ │ │
└──────┴────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────┘
2 rows in set. Elapsed: 0.001 sec.
特殊编码与通用的压缩算法相比,区别在于,通用的LZ4和ZSTD压缩算法是普适行的,不关心数据的分布特点,而特殊编码类型对于特定场景下的数据会有更好的压缩效果。
压缩算法和特殊编码两者可以结合起来一起使用
CREATE TABLE k19_ods.test8 (
`found _time` Uint32,
`recv_timet` UInt32 CODEC ( NONE ),
`recv_time2` UInt32,
`recv_time3` Ulnt32 CODEC ( LZ4 ),
`recv_time4` UInt32 CODEC (LZ4HC ( 9 )),
`recv_time5` UInt32 CODEC (ZSTD ( 9 ),
`recv_time6` Ulnt32 CODEC ( T640 ),
`name0` String CODEC (Delta (, LZ4 ),
`name1` String CODEC ( DoubleDelta0 )),
`name2` String CODEC ( Gorilla ( 0 ),
`name4` String cODEC ( Gorilla ), Lz4 )
) ENGINE = MergeTree () PARTITION BYtoYYYYMMDD ( toDateTime(found_time ))
ORDER BY
found_time
对于LZ4HC和ZSTD选择的压缩level越高,压缩效果越好,但是CPU的使用率也会相应的越高。如果插入的数据量很大,会明显看到较高的CPU使用率。
clickhouse的索引由于其存储引擎的设计,可以做的非常简单。主要有一级索引和标记组成。一级索引实现数据到block的映射,标记实现block到文件偏移量的实现。另外,由于一级索引非常小,1亿条数据只需要1万多行的索引,因此一级索引可以常驻内存,加速查找。
同时,clickhouse还提供了二级索引,不过二级索引比较简单,且不是必须的,对整体性能影响也不大。
首先,clickhouse的一级索引使用了一种叫做稀疏索引的技术,那么何为稀疏索引呢?既然有稀疏索引,是不是相对的也有稠密索引呢?没错,确实有。二者的区别如下:
稠密索引: 每行数据记录都会对应一行索引标记。
稀疏索引: 每隔若干行记录对应一条索引标记。
既然概念清楚了,那么使用稀疏索引带来的好处也是显而易见的,那就是可以大幅减少索引占用的空间。以clcikhouse默认的索引力度8192为例,1亿行数据只需要存储12208行索引。因为占用空间小,clickhouse中一级索引的数据是常驻内存的,所以取用速度极快。
那么稠密索引也有其好处,因为每条记录都有索引,所以查询的时候可以一步到位,当然缺点就是会占用较多的空间,像mysql的主键索引即是使用的稠密索引。
索引粒度对应的是index_granularity这个参数,前一节我们已经说了clickhouse使用的是稀疏索引的方式,那么index_granulariy这个参数就是决定每隔多上行数据生成一条索引记录了,默认是8192,新版本的clickhouse已经提供了自适应粒度大小的特性。
clickhouse会在每个分区目录下生成一个索引文件primary.idx,记录了主键排序后按照索引粒度采样的值,以二进制的方式存储,可以通过od命令进行查看:
[root@slave3 20201216_22_22_0]# od -l -j 0 -N 80 --width=8 primary.idx
0000000 1608087600001562
0000010 1608087966741501
0000020 1608087637579224
0000030 1608088172094313
0000040 1608087674594699
0000050 1608087862362366
0000060 1608087712835091
0000070 1608087875942332
0000100 1608087750685288
0000110 1608088251777291
0000120
因为是稀疏索引,所以显然只靠一级索引文件是无法精确定位到数据的,这时候就需要标记文件登场了。在分区目录下,你可以看到很多后缀为.bin和.mrk2的文件,其中.bin是真实的数据内容,.mrk2就是我们要说的标记文件。因为clickhouse底层是按列进行存储的,因此每一列会对应一个.bin文件和.mrk2文件。
[root@slave3 20201216_22_22_0]# od -l -j 0 -N 240 --width=24 ./city.mrk2
0000000 0 0 8192
0000030 0 32768 8192
0000060 295 0 8192
0000110 295 32768 8192
0000140 590 0 8192
0000170 590 32768 8192
0000220 885 0 8192
0000250 885 32768 8192
0000300 885 64680 0
0000330
一行标记数据使用一个元组表示,元组内包含数据压缩块位置(在.bin文件中数据是切分成若干个数据块压缩存储的),数据块内偏移和索引粒度的大小。
标记文件并不能常驻内存,俄日是使用LRU缓存策略加快其读取速度。
clickhouse是如何利用primary.idx和.mr2文件检索到具体的文件内容的呢?首先索引文件和标记文件在行上是对齐的,从上面索引文件和标记文件的示例可以看出来,二者的行数是一样的,如图:
在查询的时候,会先根据要索引的值或范围,在primary.idx文件中确定一个行号范围(具体确定的过程这里就不详细展开了,基本上就是一个递归交集的判断),然后按照相同的行号范围在每一列的.mrk中查询,得到要查询的值在数据文件.bin的哪一个压缩块,以及将该压缩块解压之后在什么位置,然后将查询到的数据结果返回。
通过partition + 一级索引 + 标记文件,层层缩小数据扫描范围,clickhouse达到了其快速检索的目的。
疑问:上面所说的都是查询条件命中索引的情况,如果没有的话clickhouse是怎么处理的呢?
当然只能是每个partition挨个扫描了,不过因为.bin文件分了若干个小的压缩块,clickhouse利用多线程读取压缩块的方式在一定程度上也可以加速查找过程。
如果数据被放置在page cache中,则一个不太复杂的查询在单个服务器上大约能够以2-10GB/s(未压缩)的速度进行处理(对于简单的查询,速度可以达到30GB/s)。如果数据没有在page cache中的话,那么速度将取决于你的磁盘系统和数据的压缩率。例如,如果一个磁盘允许以400MB/s的速度读取数据,并且数据压缩率是3,则数据的处理速度为1.2GB/s。这意味着,如果你是在提取一个10字节的列,那么它的处理速度大约是1-2亿行每秒。对于分布式处理,处理速度几乎是线性扩展的,但这受限于聚合或排序的结果不是那么大的情况下。
数据被page cache缓存的情况下,它的延迟应该小于50毫秒(最佳情况下应该小于10毫秒)。 否则,延迟取决于数据的查找次数。延迟可以通过以下公式计算得知: 查找时间(10 ms) * 查询的列的数量 * 查询的数据块的数量。
ClickHouse可以在单个服务器上每秒处理数百个查询(在最佳的情况下最多可以处理数千个)。但是由于这不适用于分析型场景。建议每秒最多查询100次。
建议每次写入不少于1000行的批量写入,或每秒不超过一个写入请求。当使用tab-separated格式将一份数据写入到MergeTree表中时,写入速度大约为50到200MB/s。如果您写入的数据每行为1Kb,那么写入的速度为50,000到200,000行每秒。如果您的行更小,那么写入速度将更高。为了提高写入性能,您可以使用多个INSERT进行并行写入,这将带来线性的性能提升。
count: 千万级别,500毫秒,1亿 800毫秒 2亿 900毫秒 3亿 1.1秒
group: 百万级别 200毫米,千万 1秒,1亿 10秒,2亿 20秒,3亿 30秒
join:千万-10万 600 毫秒, 千万 -百万:10秒,千万-千万 150秒
通过insert语句逐条插入的方法性能最差,且对于MergeTree表引擎来说,在大数据量写入的情况下该方法会触发频繁的后台文件合并,甚至会出现“too many parts”的错误。
mysql:
(1)MySQL单条SQL是单线程的,只能跑满一个core
(2)IO方面,MySQL是行存储,MySQL需要大量随机IO
ClickHouse:
(1)ClickHouse相反,有多少CPU,吃多少资源,所以飞快
(2)ClickHouse不支持事务,不存在隔离级别。ClickHouse的定位是分析性数据库,而不是严格的关系型数据库
(3)IO方面,ClickHouse是列存储,后者在count()这类操作天然有优势,ClickHouse基本是顺序IO
ru.yandex.clickhouse
clickhouse-jdbc
0.3.2
com.alibaba
druid
1.1.21
com.baomidou
mybatis-plus-boot-starter
3.4.0
application.yml 配置:SpringBoot默认是不注入 clickhouse属性值的,需要自己绑定。
spring:
datasource:
# 数据源选择
type: com.alibaba.druid.pool.DruidDataSource
# clickhouse配置
click:
driverClassName: ru.yandex.clickhouse.ClickHouseDriver
url: jdbc:clickhouse://127.0.0.1:8123/default
username: default
password:
initialSize: 10
maxActive: 100
minIdle: 10
maxWait: 6000
Bean配置:用了druid监控所以在这里边初始化了,这个 DataSource 也可以在启动类里初始化。
@Configuration
public class DruidConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.click")
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
-- 删除语法
alter table table_name delete WHERE primary_key='10';
-- 修改语法
alter table tb_stattable_name update cloumn1=222 WHERE primary_key = '4';
create table engine mysql
CREATE TABLE [IF NOT EXISTS] [db.]table_name [ON CLUSTER cluster](
name1 [type1] [DEFAULT|MATERIALIZED|ALIAS expr1] [TTL expr1],
name2 [type2] [DEFAULT|MATERIALIZED|ALIAS expr2] [TTL expr2],
...
INDEX index_name1 expr1 TYPE type1(...) GRANULARITY value1,
INDEX index_name2 expr2 TYPE type2(...) GRANULARITY value2
) ENGINE = MySQL('host:port', 'database', 'table', 'user', 'password'[, replace_query, 'on_duplicate_clause']);
注意,实际数据存储在远端mysql数据库中,可以理解成外表。
CREATE TABLE ${表名} ENGINE = MergeTree ORDER BY ${主键名} AS
SELECT * FROM mysql('${MySql的IP:PORT}', 'MySql的数据库名', 'MySql的表名', 'MySql的用户名', 'MySql的密码');
重要: 1: MySQL的必需有主键且不能为空
2: ORDER BY 后的字段大小写要和MySQL的大小写一样
3: 需要注意的是建表的时候需要注意两边的字段类型,特别是MySql的Decimal的类型,ClickHouse建表需要注意更改为 Float64 类型。
字段映射表:
ClickHouse | MySQL | Comment |
---|---|---|
UInt8, UInt16, UInt32, UInt64 | TINYINT UNSIGNED, SMALLINT UNSIGNED, INT UNSIGNED, BIGINT UNSIGNED | |
Int8, Int16, Int32, Int64 | TINYINT SIGNED, SMALLINT SIGNED, INT SIGNED, BIGINT SIGNED | |
Float32, Float64 | FLOAT, DOUBLE | Supports inf, -inf, nan, recommended NOT to use by ClickHouse! |
String | BLOB, TEXT, VARCHAR, VARBINARY | No encoding. Recommended to use UTF-8. In fact behaves like a BLOB. |
FixedString(n) | CHAR, BINARY | \0 padded. Less functions available than String, in fact it behaves like BINARY. |
Date | DATE | UNIX epoch date up to 2038. |
DateTime | DATETIME, TIMESTAMP | UNIX epoch timestamp up to 2038. |
Enum | ENUM | Similar to MySQL ENUM. Behaves like Int8/16. |
Array(type) | n.a. | Array of type. Closest equivalent in MySQL is JSON? Not well supported. |
Tuple() | n.a. | |
Nested() | n.a. | Closest equivalent in MySQL is JSON? |
AggregateFunction() | n.a. | |
Set | n.a. | |
Expression | n.a. |