ClickHouse 是俄罗斯搜索巨头 Yandex 公司早 2016年 开源的一个极具 " 战斗力 " 的实时数据分析数据库,开发语言为C++,是一个用于联机分析 (OLAP:Online Analytical Processing) 的列式数据库管理系统(DBMS:Database Management System),简称 CK,工作速度比传统方法快100-1000倍,ClickHouse 的性能超过了目前市场上可比的面向列的DBMS。 每秒钟每台服务器每秒处理数亿至十亿多行和数十千兆字节的数据。
随着业务的迅猛增长,Yandex.Metrica目前已经成为世界第三大Web流量分析平台,每天处理超过200亿个跟踪事件。能够拥有如此惊人的体量,在它背后提供支撑的ClickHouse功不可没。ClickHouse已经为Yandex.Metrica存储了超过20万亿行的数据,90%的自定义查询能够在1秒内返回,其集群规模也超过了400台服务器。虽然ClickHouse起初只是为了Yandex.Metrica而研发的,但由于它出众的性能,目前也被广泛应用于Yandex内部其他数十个产品上。
今日头条 内部用ClickHouse来做用户行为分析,内部一共几千个ClickHouse节点,单集群最大1200节点,总数据量几十PB,日增原始数据300TB左右。
腾讯内部用ClickHouse做游戏数据分析,并且为之建立了一整套监控运维体系。
携程内部从18年7月份开始接入试用,目前80%的业务都跑在ClickHouse上。每天数据增量十多亿,近百万次查询请求。
快手内部也在使用ClickHouse,存储总量大约10PB, 每天新增200TB, 90%查询小于3S。
开源的列存储数据库管理系统,支持线性扩展,简单方便,高可靠性,
容错跑分快:比Vertica快5倍,比Hive快279倍,比MySQL快800倍,其可处理的数据级别已达到10亿级别
https://clickhouse.tech/benchmark/dbms/
功能多:支持数据统计分析各种场景,支持类SQL查询,异地复制部署
真正的面向列的DBMS(ClickHouse是一个DBMS,而不是一个单一的数据库。它允许在运行时创建表和数据库、加载数据和运行查询,而无需重新配置和重新启动服务器)
数据压缩(一些面向列的DBMS(INFINIDB CE 和 MonetDB)不使用数据压缩。但是,数据压缩确实是提高了性能)
磁盘存储的数据(许多面向列的DBMS(SPA HANA和GooglePowerDrill))只能在内存中工作。但即使在数千台服务器上,内存也太小了。)
多核并行处理(多核多节点并行化大型查询)
在多个服务器上分布式处理(在clickhouse中,数据可以驻留在不同的分片上。每个分片都可以用于容错的一组副本,查询会在所有分片上并行处理)
SQL支持(ClickHouse sql 跟真正的sql有不一样的函数名称。不过语法基本跟SQL语法兼容,支持JOIN/FROM/IN 和JOIN子句及标量子查询支持子查询)
向量化引擎(数据不仅按列式存储,而且由矢量-列的部分进行处理,这使得开发者能够实现高CPU性能)
实时数据更新(ClickHouse支持主键表。为了快速执行对主键范围的查询,数据使用合并树(MergeTree)进行递增排序。由于这个原因,数据可以不断地添加到表中)
支持近似计算(统计全国到底有多少人?143456754 14.3E)
数据复制和对数据完整性的支持(ClickHouse使用异步多主复制。写入任何可用的复本后,数据将分发到所有剩余的副本。系统在不同的副本上保持相同的数据。数据在失败后自动恢复)
没有完整的事务支持,不支持Transaction,想快就别Transaction
缺少完整Update/Delete操作,缺少高频率、低延迟的修改或删除已存在数据的能力,仅用于批量删除或修改数据。
聚合结果必须小于一台机器的内存大小
支持有限操作系统,正在慢慢完善
不适合Key-value存储,不支持Blob等文档型数据库
ClickHouse 是一个真正的列式数据库管理系统(DBMS)。在 ClickHouse 中,数据始终是按列存储的,包括矢量(向量或列块)执行的过程。
Column和Field是ClickHouse数据最基础的映射单元。内存中的一列数据由一个Column对象表示。
Column对象分为接口和实现两个部分,在IColumn接口对象中,定义了对数据进行各种关系运算的方法。几乎所有的操作都是不可变的:这些操作不会更改原始列,但是会创建一个新的修改后的列。
在大多数场合,ClickHouse都会以整列的方式操作数据,但凡事也有例外。如果需要操作单个具体的数值 ( 也就是单列中的一行数据 ),则需要使用Field对象,Field对象代表一个单值。
与Column对象的泛化设计思路不同,Field对象使用了聚合的设计模式。在Field对象内部聚合了Null、UInt64、String和Array等13种数据类型及相应的处理逻辑。
IDataType 负责序列化和反序列化:读写二进制或文本形式的列或单个值构成的块。
IDataType 直接与表的数据类型相对应。
IDataType 与 IColumn 之间的关联并不大。不同的数据类型在内存中能够用相同的IColumn 实现来表示。
IDataType 仅存储元数据。
数据的序列化和反序列化工作由DataType负责。IDataType接口定义了许多正反序列化的方法,它们成对出现。IDataType也使用了泛化的设计模式,具体方法的实现逻辑由对应数据类型的实例承载。
DataType虽然负责序列化相关工作,但它并不直接负责数据的读取,而是转由从Column或Field对象获取。
Block 是表示内存中表的子集(chunk)的容器,是由三元组: (IColumn, IDataType, 列名) 构成的集合。
ClickHouse内部的数据操作是面向Block对象进行的,并且采用了流的形式。Block对象可以看作数据表的子集。
Block并没有直接聚合Column和DataType对象,而是通过ColumnWithTypeAndName对象进行间接引用。
块流用于处理数据。我们可以使用块流从某个地方读取数据,执行数据转换,或将数据写到某个地方。
IBlockInputStream负责数据的读取和关系运算, IBlockInputStream 具有 read 方法,其能够在数据可用时获取下一个块。
IBlockOutputStream负责将数据输出到下一环节。 IBlockOutputStream 具有 write 方法,其能够将块写到某处。
IBlockInputStream接口总共有60多个实现类,这些实现类大致可以分为三类:
第一类用于处理数据定义的DDL操作
第二类用于处理关系运算的相关操作
第三类则是与表引擎呼应,每一种表引擎都拥有与之对应的BlockInputStream实现
IBlockOutputStream的设计与IBlockInputStream如出一辙。这些实现类基本用于表引擎的相关处理,负责将数据写入下一环节或者最终目的地。
数据格式同块流一起实现。用于向客户端输出数据的»展示«格式
如块流 IBlockOutputStream 提供的 Pretty 格式,也有其它输入输出格式,比如TabSeparated 或 JSONEachRow 。
如行流: IRowInputStream 和 IRowOutputStream 。它们允许你按行 pull/push 数据,而不是按块。
对于面向字节的输入输出,有 ReadBuffer 和 WriteBuffer 这两个抽象类。
ReadBuffer 和 WriteBuffer 由一个连续的缓冲区和指向缓冲区中某个位置的一个指针组成。
ReadBuffer 和 WriteBuffer 的实现用于处理文件、文件描述符和网络套接字(socket),也用于实现压缩和其它用途。
在数据表的底层设计中并没有所谓的Table对象
表由 IStorage 接口表示。该接口的不同实现对应不同的表引擎。
表引擎是ClickHouse的一个显著特性,不同的表引擎由不同的子类实现。
IStorage 中最重要的方法是 read 和 write ,除此之外还有 alter 、 rename 和 drop等方法。
表的 read 方法能够返回多个 IBlockInputStream 对象以允许并行处理数据。多个块输入流能够从一个表中并行读取。
AST 查询被传递给 read 方法,表引擎可以使用它来判断是否能够使用索引,从而从表中读取更少的数据。
查询由一个手写递归下降解析器解析。比如, ParserSelectQuery 只是针对查询的不同部分递归地调用下层解析器。
解析器创建 AST 。 AST 由节点表示,节点是 IAST 的实例。
简单的解释器,如 InterpreterExistsQuery 和 InterpreterDropQuery
复杂的解释器,如 InterpreterSelectQuery 。
SELECT 查询的解释结果是从 FROM 字句的结果集中读取数据的 IBlockInputStream ;
INSERT 查询的结果是写入需要插入的数据的 IBlockOutputStream ;
Parser分析器可以将一条SQL语句以递归下降的方法解析成AST语法树的形式。不同的SQL语句,会经由不同的Parser实现类解析。
ClickHouse主要提供两类函数—普通函数(Functions)和聚合函数(Aggregate Functions)。
普通函数不会改变行数 - 它们的执行看起来就像是独立地处理每一行数据。
实际上,函数不会作用于一个单独的行上,而是作用在以 Block 为单位的数据上,以实现向量查询执行。
普通函数由IFunction接口定义,拥有数十种函数实现,采用向量化的方式直接作用于一整列数据。
聚合函数是状态函数。它们将传入的值激活到某个状态,并允许你从该状态获取结果。
聚合函数由IAggregateFunction接口定义,相比无状态的普通函数,聚合函数是有状态的。
以COUNT聚合函数为例,其AggregateFunctionCount的状态使用整型UInt64记录。
聚合状态可以被序列化和反序列化,以在分布式查询执行期间通过网络传递或者在内存不够的时候将其写到硬盘。
ClickHouse的集群由分片 ( Shard ) 组成,而每个分片又通过副本 ( Replica ) 组成。
这种分层的概念,在一些流行的分布式系统中十分普遍。 ClickHouse的1个节点只能拥有1个分片,也就是说如果要实现1分片、1副本,则至少需要部署2个服务节点。
分片只是一个逻辑概念,其物理承载还是由副本承担的。
表引擎是ClickHouse设计实现中的一大特色
合并树家族自身也拥有多种表引擎的变种。其中MergeTree作为家族中最基础的表引擎,提供了主键索引、数据分区、数据副本和数据采样等基本能力,而家族中其他的表引擎则在MergeTree的基础之上各有所长。
ClickHouse拥有非常庞大的表引擎体系,其共拥有合并树、外部存储、内存、文件、接口和其他6大类20多种表引擎。
MergeTree在写入一批数据时,数据总会以数据片段的形式写入磁盘,且数据片段不可修改。
为了避免片段过多,ClickHouse会通过后台线程,定期合并这些数据片段,属于相同分区的数据片段会被合成一个新的片段。
这种数据片段往复合并的特点,也正是合并树名称的由来。
MergeTree表引擎中的数据是拥有物理存储的,数据会按照分区目录的形式保存到磁盘之上
一张数据表的完整物理结构分为3个层级,依次是数据表目录、分区目录及各分区下具体的数据文件
MergeTree数据分区的规则由分区ID决定,而具体到每个数据分区所对应的ID,则是由分区键的取值决定的
分区键支持使用任何一个或一组字段表达式声明,其业务语义可以是年、月、日或者组织单位等任何一种规则。
针对取值数据类型的不同,分区ID的生成逻辑目前拥有四种规则
如果不使用分区键,即不使用PARTITION BY声明任何分区表达式,则分区ID默认取名为all,所有的数据都会被写入这个all分区。
如果分区键取值属于整型(兼容UInt64,包括有符号整型和无符号整型),且无法转换为日期类型YYYYMMDD格式
则直接按照该整型的字符形式输出,作为分区ID的取值。
如果分区键取值属于日期类型,或者是能够转换为YYYYMMDD格式的整型
则使用按照YYYYMMDD进行格式化后的字符形式输出,并作为分区ID的取值。
如果分区键取值既不属于整型,也不属于日期类型
例如String、Float等,则通过128位Hash算法取其Hash值作为分区ID的取值
201905表示分区目录的ID;
1_1分别表示最小的数据块编号与最大的数据块编号;
而最后的_0则表示目前合并的层级。
PartitionID_MinBlockNum_MaxBlockNum_Level
PartitionID:分区ID
MinBlockNum和MaxBlockNum:顾名思义,最小数据块编号与最大数据块编号。
Level:合并的层级,可以理解为某个分区被合并过的次数,或者这个分区的年龄。数值越高表示年龄越大。
MergeTree的分区目录和传统意义上其他数据库有所不同。
首先,MergeTree的分区目录并不是在数据表被创建之后就存在的,而是在数据写入过程中被创建的。
也就是说如果一张数据表没有任何数据,那么也不会有任何分区目录存在。
在其他某些数据库的设计中,追加数据后目录自身不会发生变化,只是在相同分区目录中追加新的数据文件。
而MergeTree完全不同,伴随着每一批数据的写入(一次INSERT语句),MergeTree都会生成一批新的分区目录。
即便不同批次写入的数据属于相同分区,也会生成不同的分区目录。
也就是说,对于同一个分区而言,也会存在多个分区目录的情况。
在之后的某个时刻(写入后的10~15分钟,也可以手动执行optimize查询语句)
--手动合并分区
optimize table yjxxt.user;
ClickHouse会通过后台任务再将属于相同分区的多个目录合并成一个新的目录。
已经存在的旧分区目录并不会立即被删除,而是在之后的某个时刻通过后台任务被删除(默认8分钟)。
MinBlockNum:取同一分区内所有目录中最小的MinBlockNum值。
MaxBlockNum:取同一分区内所有目录中最大的MaxBlockNum值。
Level:取同一分区内最大Level值并加1。
MergeTree的主键使用PRIMARY KEY定义,待主键定义之后,MergeTree会依据index_granularity间隔(默认8192行),为数据表生成一级索引并保存至primary.idx文件内,索引数据按照PRIMARYKEY排序。
primary.idx文件内的一级索引采用稀疏索引实现。
稠密索引中每一行索引标记都会对应到一行具体的数据记录。
稀疏索引中每一行索引标记对应的是一段数据,而不是一行。
稀疏索引的优势是显而易见的,它仅需使用少量的索引标记就能够记录大量数据的区间位置信息,且数据量越大优势越为明显。
以默认的索引粒度(8192)为例,MergeTree只需要12208行索引标记就能为1亿行数据记录提供索引。
由于稀疏索引占用空间小,所以primary.idx内的索引数据常驻内存,取用速度自然极快。
索引粒度就如同标尺一般,会丈量整个数据的长度,并依照刻度对数据进行标注,最终将数据标记成多个间隔的小段
由于是稀疏索引,所以MergeTree需要间隔index_granularity行数据才会生成一条索引记录,其索引值会依据声明的主键字段获取。
第0(8192 0)行CounterID取值57,第8192(8192 1)行CounterID取值1635,而第16384(8192*2)行CounterID取值3266
最终索引数据将会是57 1635 3266。
MarkRange在ClickHouse中是用于定义标记区间的对象。
MergeTree按照index_granularity的间隔粒度,将一段完整的数据划分成了多个小的间隔数据段,一个具体的数据段即是一个MarkRange。
MarkRange与索引编号对应,使用start和end两个属性表示其区间范围。
通过与start及end对应的索引编号的取值,即能够得到它所对应的数值区间。而数值区间表示了此MarkRange包含的数据范围。
主键ID为String类型,ID的取值从A000开始,后面依次为A001、A002……直至A189为止。
MergeTree的索引粒度index_granularity=3
MergeTree会将此数据片段划分成189/3=63个小的MarkRange
一个区间是由基于主键的查询条件转换而来的条件区间;
一个区间是刚才所讲述的与MarkRange对应的数值区间。
二级索引又称跳数索引,由数据的聚合信息构建而成。
根据索引类型的不同,其聚合信息的内容也不同。跳数索引的目的与一级索引一样,也是帮助查询时减少数据扫描的范围。
跳数索引在默认情况下是关闭的,需要设置allow_experimental_data_skipping_indices
SET allow_experimental_data_skipping_indices = 1
跳数索引需要在CREATE语句内定义,它支持使用元组和表达式的形式声明,其完整的定义语法
INDEX index_name expr TYPE index_type(...) GRANULARITY granularity
granularity定义了一行跳数索引能够跳过多少个index_granularity区间的数据。
首先,按照index_granularity粒度间隔将数据划分成n段,总共有[0,n-1]个区间(n=total_rows/index_granularity,向上取整)
接着,根据索引定义时声明的表达式,从0区间开始,依次按index_granularity粒度从数据中获取聚合信息,每次向前移动1步,聚合信息逐步累加。
最后,当移动granularity次区间时,则汇总并生成一行跳数索引数据。
在MergeTree中,数据按列存储。而具体到每个列字段,数据也是独立存储的,每个列字段都拥有一个与之对应的.bin数据文件。也正是这些.bin文件,最终承载着数据的物理存储。
数据文件以分区目录的形式被组织存放,所以在.bin文件中只会保存当前分区片段内的这一部分数据
一是可以更好地进行数据压缩
二是能够最小化数据扫描的范围
首先,数据是经过压缩的,目前支持LZ4、ZSTD、Multiple和Delta几种算法,默认使用LZ4算法;
其次,数据会事先依照ORDER BY的声明排序;
最后,数据是以压缩数据块的形式被组织并写入.bin文件中的
一个压缩数据块由头信息和压缩数据两部分组成。
头信息固定使用9位字节表示,具体由1个UInt8(1字节)整型和2个UInt32(4字节)整型组成
分别代表使用的压缩算法类型、压缩后的数据大小和压缩前的数据大小
bin压缩文件是由多个压缩数据块组成的,而每个压缩数据块的头信息则是基于CompressionMethod_CompressedSize_UncompressedSize公式生成的
每个压缩数据块的体积,按照其压缩前的数据字节大小,都被严格控制在64KB~1MB
其上下限分别由min_compress_block_size(默认65536)与max_compress_block_size(默认1048576)参数指定。
而一个压缩数据块最终的大小,则和一个间隔(index_granularity)内数据的实际大小相关
MergeTree在数据具体的写入过程中,会依照索引粒度(默认情况下,每次取8192行),按批次获取数据并进行处理
单个批次数据size<64KB :如果单个批次数据小于64KB,则继续获取下一批数据,直至累积到size>=64KB时,生成下一个压缩数据块。
单个批次数据64KB<=size<=1MB :如果单个批次数据大小恰好在64KB与1MB之间,则直接生成下一个压缩数据块。
单个批次数据size>1MB :如果单个批次数据直接超过1MB,则首先按照1MB大小截断并生成下一个压缩数据块。剩余数据继续依照上述规则执行。
其一,虽然数据被压缩后能够有效减少数据大小,降低存储空间并加速数据传输效率,但数据的压缩和解压动作,其本身也会带来额外的性能损耗。所以需要控制被压缩数据的大小,以求在性能损耗和压缩率之间寻求一种平衡。
其二,在具体读取某一列数据时(.bin文件),首先需要将压缩数据加载到内存并解压,这样才能进行后续的数据处理。通过压缩数据块,可以在不读取整个.bin文件的情况下将读取粒度降低到压缩数据块级别,从而进一步缩小数据读取的范围。
数据标记作为衔接一级索引和数据的桥梁,其像极了做过标记小抄的书签,而且书本中每个一级章节都拥有各自的书签。
数据标记和索引区间是对齐的,均按照index_granularity的粒度间隔。
为了能够与数据衔接,数据标记文件也与.bin文件一一对应。
每一个列字段[Column].bin文件都有一个与之对应的[Column].mrk数据标记文件,用于记录数据在.bin文件中的偏移量信息。
一行标记数据使用一个元组表示,元组内包含两个整型数值的偏移量信息。
它们分别表示在此段数据区间内,在对应的.bin压缩文件中,压缩数据块的起始偏移量;
以及将该数据压缩块解压后,其未压缩数据的起始偏移量。
每一行标记数据都表示了一个片段的数据(默认8192行)在.bin压缩文件中的读取位置信息。标记数据与一级索引数据不同,它并不能常驻内存,而是使用LRU(最近最少使用)缓存策略加快其取用速度。
MergeTree在读取数据时,必须通过标记数据的位置信息才能够找到所需要的数据。整个查找过程大致可以分为读取压缩数据块和读取数据两个步骤
1B*8192=8192B,64KB=65536B,65536/8192=8
头信息固定由9个字节组成,压缩后大小为8个字节
12016=8+12000+8
在查询某一列数据时,MergeTree无须一次性加载整个.bin文件,而是可以根据需要,只加载特定的压缩数据块。而这项特性需要借助标记文件中所保存的压缩文件中的偏移量。
在读取解压后的数据时,MergeTree并不需要一次性扫描整段解压数据,它可以根据需要,以index_granularity的粒度加载特定的一小段。为了实现这项特性,需要借助标记文件中保存的解压数据块中的偏移量。
由于压缩数据块的划分,与一个间隔(index_granularity)内的数据大小相关,每个压缩数据块的体积都被严格控制在64KB~1MB。
而一个间隔(index_granularity)的数据,又只会产生一行数据标记。
那么根据一个间隔内数据的实际字节大小,数据标记和压缩数据块之间会产生三种不同的对应关系。
多个数据标记对应一个压缩数据块
当一个间隔(index_granularity)内的数据未压缩大小size小于64KB时
一个数据标记对应一个压缩数据块
当一个间隔(index_granularity)内的数据未压缩大小size大于等于64KB且小于等于1MB时
一个数据标记对应多个压缩数据块
当一个间隔(index_granularity)内的数据未压缩大小size直接大于1MB时
数据写入的第一步是生成分区目录,伴随着每一批数据的写入,都会生成一个新的分区目录。在后续的某一时刻,属于相同分区的目录会依照规则合并到一起;接着,按照index_granularity索引粒度,会分别生成primary.idx一级索引、每一个列字段的.mrk数据标记和.bin压缩数据文件。
从分区目录201403_1_34_3能够得知,该分区数据共分34批写入,期间发生过3次合并。在数据写入的过程中,依据index_granularity的粒度,依次为每个区间的数据生成索引、标记和压缩数据块。其中,索引和标记区间是对齐的,而标记与压缩块则根据区间数据大小的不同,会生成多对一、一对一和一对多三种关系。
数据查询的本质,可以看作一个不断减小数据范围的过程。在最理想的情况下,MergeTree首先可以依次借助分区索引、一级索引和二级索引,将数据扫描范围缩至最小。然后再借助数据标记,将需要解压与计算的数据范围缩至最小。
MergeTree
TTL即Time To Live,顾名思义,它表示数据的存活时间。在MergeTree中,可以为某个列字段或整张表设置TTL。
当时间到达时,如果是列字段级别的TTL,则会删除这一列的数据;
如果是表级别的TTL,则会删除整张表的数据;
如果同时设置了列级别和表级别的TTL,则会以先到期的那个为主。
INTERVAL完整的操作包括SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER和YEAR。
如果一张MergeTree表被设置了TTL表达式,那么在写入数据时,会以数据分区为单位,在每个分区目录内生成一个名为ttl.txt的文件。
ttl.txt文件中通过一串JSON配置保存了TTL的相关信息
{"columns":[{"name":"code","min":1557478860,"max":1557651660}],"table":
{"min":1557565200,"max":1557738000}}
columns用于保存列级别TTL信息;
table用于保存表级别TTL信息;
min和max则保存了当前数据分区内,TTL指定日期字段的最小值、最大值分别与INTERVAL表达式计算后的时间戳。
19.15版本之前,MergeTree只支持单路径存储,所有的数据都会被写入config.xml配置中path指定的路径下,即使服务器挂载了多块磁盘,也无法有效利用这些存储空间。
19.15版本开始,MergeTree实现了自定义存储策略的功能,支持以数据分区为最小移动单元,将分区目录写入多块磁盘目录。
MergeTree原本的存储策略,无须任何配置,所有分区会自动保存到config.xml配置中path指定的路径下
这种策略适合服务器挂载了多块磁盘,但没有做RAID的场景。
JBOD的全称是Just a Bunch of Disks,它是一种轮询策略,每执行一次INSERT或者MERGE,所产生的新分区会轮询写入各个磁盘。
这种策略适合服务器挂载了不同类型磁盘的场景。
HOT区域使用SSD这类高性能存储媒介,注重存取性能;
COLD区域则使用HDD这类高容量存储媒介,注重存取经济性。
数据在写入MergeTree之初,首先会在HOT区域创建分区目录用于保存数据,当分区数据大小累积到阈值时,数据会自行移动到COLD区域。
外部存储表引擎直接从其他的存储系统读取数据
例如直接读取HDFS的文件或者MySQL数据库的表
这些表引擎只负责元数据管理和数据查询,而它们自身通常并不负责数据的写入,数据文件直接由外部系统提供
HDFS
Mysql
JDBC
目前ClickHouse还不支持恰好一次(Exactly once)的语义,因为这需要应用端与Kafka深度配合才能实现。
File
一方面,这意味着拥有较好的查询性能
另一方面,如果表内装载的数据量过大,可能会带来极大的内存消耗和负担
Memory表引擎直接将数据保存在内存中,数据既不会被压缩也不会被格式转换,数据在内存中保存的形态与查询时看到的如出一辙。
当ClickHouse服务重启的时候,Memory表内的数据会全部丢失。
当数据被写入之后,磁盘上不会创建任何数据文件。
Set表引擎是拥有物理存储的,数据首先会被写至内存,然后被同步到磁盘文件中。
所以当服务重启时,它的数据不会丢失,当数据表被重新装载时,文件数据会再次被全量加载至内存。
Set表引擎具有去重的能力,在数据写入的过程中,重复的数据会被自动忽略。
[num].bin数据文件:保存了所有列字段的数据。其中,num是一个自增id,从1开始。伴随着每一批数据的写入(每一次INSERT),都会生成一个新的.bin文件,num也会随之加1。
tmp临时目录:数据文件首先会被写到这个目录,当一批数据写入完毕之后,数据文件会被移出此目录。
Join表引擎可以说是为JOIN查询而生的,它等同于将JOIN查询进行了一层简单封装。在Join表引擎的底层实现中,它与Set表引擎共用了大部分的处理逻辑,所以Join和Set表引擎拥有许多相似之处。
TinyLog是日志家族系列中性能最低的表引擎,它的存储结构由数据文件和元数据两部分组成
数据文件是按列独立存储的,也就是说每一个列字段都拥有一个与之对应的.bin文件。
TinyLog既不支持分区,也没有.mrk标记文件
由于没有标记文件,它自然无法支持.bin文件的并行读取操作,所以它只适合在非常简单的场景下使用
StripeLog表引擎的存储结构由固定的3个文件组成,它们分别是
data.bin:数据文件,所有的列字段使用同一个文件保存,它们的数据都会被写入data.bin
index.mrk:数据标记,保存了数据在data.bin文件中的位置信息。利用数据标记能够使用多个线程,以并行的方式读取data.bin内的压缩数据块,从而提升数据查询的性能。
sizes.json:元数据文件,记录了data.bin和index.mrk大小的信息。
Log表引擎结合了TinyLog表引擎和StripeLog表引擎的长处,是日志家族系列中性能最高的表引擎。
[column].bin:数据文件,数据文件按列独立存储,每一个列字段都拥有一个与之对应的.bin文件。
marks.mrk:数据标记,统一保存了数据在各个[column].bin文件中的位置信息。利用数据标记能够使用多个线程,以并行的方式读取.bin内的压缩数据块,从而提升数据查询的性能。
sizes.json:元数据文件,记录了[column].bin和__marks.mrk大小的信息。
在数据仓库的设计中,数据按年分表存储,例如test_table_2018、test_table_2019和test_table_2020。假如现在需要跨年度查询这些数据 ?
Merge表引擎就如同一层使用了门面模式的代理,它本身不存储任何数据,也不支持数据写入。
它的作用就如其名,即负责合并多个查询的结果集。
Merge表引擎可以代理查询任意数量的数据表,这些查询会异步且并行执行,并最终合成一个结果集返回
被代理查询的数据表被要求处于同一个数据库内,且拥有相同的表结构,但是它们可以使用不同的表引擎以及不同的分区定义
在日常运转的过程中,数据查询也是ClickHouse的主要工作之一。ClickHouse完全使用SQL作为查询语言,能够以SELECT查询语句的形式从数据库中选取数据,这也是它具备流行潜质的重要原因。虽然ClickHouse拥有优秀的查询性能,但是我们也不能滥用查询,掌握ClickHouse所支持的各种查询子句,并选择合理的查询形式是很有必要的。使用不恰当的SQL语句进行查询不仅会带来低性能,还可能导致不可预知的系统错误。
ClickHouse对于SQL语句的解析是大小写敏感的,这意味着SELECT a和SELECT A表示的语义是不相同的。
ClickHouse支持CTE(Common Table Expression,公共表表达式),以增强查询语句的表达
在改用CTE的形式后,可以极大地提高语句的可读性和可维护性
FROM子句表示从何处读取数据,目前支持如下3种形式
SELECT WatchID FROM hits_v1
SELECT MAX_WatchID FROM (SELECT MAX(WatchID) AS MAX_WatchID FROM hits_v1)
SELECT number FROM numbers(5)
在ClickHouse中,并没有数据库中常见的DUAL虚拟表,取而代之的是system.one。
它可以配合CollapsingMergeTree和Versioned-CollapsingMergeTree等表引擎进行查询操作,以强制在查询过程中合并
但由于Final修饰符会降低查询性能,所以应该尽可能避免使用它
SAMPLE子句能够实现数据采样的功能,使查询仅返回采样数据而不是全部数据,从而有效减少查询负载。
SAMPLE子句的采样机制是一种幂等设计,也就是说在数据不发生变化的情况下,使用相同的采样规则总是能够返回相同的数据,所以这项特性非常适合在那些可以接受近似查询结果的场合使用
SAMPLE子句只能用于MergeTree系列引擎的数据表,并且要求在CREATE TABLE时声明SAMPLE BY抽样表达式
ARRAY JOIN子句允许在数据表的内部,与数组或嵌套类型的字段进行JOIN操作,从而将一行数组展开为多行。接下来让我们看看它的基础用法。
JOIN子句可以对左右两张表的数据进行连接
JOIN的语法包含连接精度和连接类型两部分
JOIN查询还可以根据其执行策略被划分为本地查询和远程查询。
连接精度决定了JOIN查询在连接数据时所使用的策略,目前支持ALL、ANY和ASOF三种类型。如果不主动声明,则默认是ALL
对数据是否连接匹配的判断是通过JOIN KEY进行的,目前只支持等式(EQUAL JOIN)。
交叉连接(CROSS JOIN)不需要使用JOINKEY,因为它会产生笛卡儿积。
如果左表内的一行数据,在右表中有多行数据与之连接匹配,则返回右表中全部连接的数据。
而判断连接匹配的依据是左表与右表内的数据,基于连接键(JOIN KEY)的取值完全相等(equal),等同于lex.key=right.key。
如果左表内的一行数据,在右表中有多行数据与之连接匹配,则仅返回右表中第一行连接的数据。
ASOF是一种模糊连接,它允许在连接键之后追加定义一个模糊连接的匹配条件asof_column
最终返回的查询结果符合连接条件a.id=b.id AND a.time>=b.time,且仅返回了右表中第一行连接匹配的数据。
ASOF支持使用USING的简写形式,USING后声明的最后一个字段会被自动转换成asof_colum模糊连接条件。
asof_colum必须是整型、浮点型和日期型这类有序序列的数据类型;
asof_colum不能是数据表内的唯一字段
INNER JOIN表示内连接,在查询时会以左表为基础逐行遍历数据,然后从右表中找出与左边连接的行,它只会返回左表与右表两个数据集合中交集的部分,其余部分都会被排除
OUTER JOIN表示外连接,它可以进一步细分为左外连接(LEFT)、右外连接(RIGHT)和全外连接(FULL)三种形式。根据连接形式的不同,其返回数据集合的逻辑也不尽相同。
CROSS JOIN表示交叉连接,它会返回左表与右表两个数据集合的笛卡儿积。
为了能够优化JOIN查询性能,首先应该遵循左大右小的原则 ,无论使用的是哪种连接方式,右表都会被全部加载到内存中与左表进行比较。
JOIN查询目前没有缓存的支持
如果是在大量维度属性补全的查询场景中,则建议使用字典代替JOIN查询
连接查询的空值是由默认值填充的,这与其他数据库所采取的策略不同(由Null填充)。
连接查询的空值策略是通过join_use_nulls参数指定的
默认为0。当参数值为0时,空值由数据类型的默认值填充;
当参数值为1时,空值由Null填充。