ClickHouse 架构概述
ClickHouse 是一个真正的列式数据库管理系统(DBMS)。在 ClickHouse 中,数据始终是按列存储的,包括矢量(向量或列块)执行的过程。只要有可能,操作都是基于矢量进行分派的,而不是单个的值,这被称为«矢量化查询执行»,它有利于降低实际的数据处理开销。
这个想法并不新鲜,其可以追溯到
APL
编程语言及其后代:A +
、J
、K
和Q
。矢量编程被大量用于科学数据处理中。即使在关系型数据库中,这个想法也不是什么新的东西:比如,矢量编程也被大量用于Vectorwise
系统中。
通常有两种不同的加速查询处理的方法:矢量化查询执行和运行时代码生成。在后者中,动态地为每一类查询生成代码,消除了间接分派和动态分派。这两种方法中,并没有哪一种严格地比另一种好。运行时代码生成可以更好地将多个操作融合在一起,从而充分利用 CPU 执行单元和流水线。矢量化查询执行不是特别实用,因为它涉及必须写到缓存并读回的临时向量。如果 L2 缓存容纳不下临时数据,那么这将成为一个问题。但矢量化查询执行更容易利用 CPU 的 SIMD 功能。朋友写的一篇研究论文表明,将两种方法结合起来是更好的选择。ClickHouse 使用了矢量化查询执行,同时初步提供了有限的运行时动态代码生成。
列(Columns)
要表示内存中的列(实际上是列块),需使用 IColumn
接口。该接口提供了用于实现各种关系操作符的辅助方法。几乎所有的操作都是不可变的:这些操作不会更改原始列,但是会创建一个新的修改后的列。比如,IColumn::filter
方法接受过滤字节掩码,用于 WHERE
和 HAVING
关系操作符中。另外的例子:IColumn::permute
方法支持 ORDER BY
实现,IColumn::cut
方法支持 LIMIT
实现等等。
不同的 IColumn
实现(ColumnUInt8
、ColumnString
等)负责不同的列内存布局。内存布局通常是一个连续的数组。对于数据类型为整型的列,只是一个连续的数组,比如 std::vector
。对于 String
列和 Array
列,则由两个向量组成:其中一个向量连续存储所有的 String
或数组元素,另一个存储每一个 String
或 Array
的起始元素在第一个向量中的偏移。而 ColumnConst
则仅在内存中存储一个值,但是看起来像一个列。
字段
尽管如此,有时候也可能需要处理单个值。表示单个值,可以使用 Field
。Field
是 UInt64
、Int64
、Float64
、String
和 Array
组成的联合。IColumn
拥有 operator[]
方法来获取第 n
个值成为一个 Field
,同时也拥有 insert
方法将一个 Field
追加到一个列的末尾。这些方法并不高效,因为它们需要处理表示单一值的临时 Field
对象,但是有更高效的方法比如 insertFrom
和 insertRangeFrom
等。
Field
中并没有足够的关于一个表(table)的特定数据类型的信息。比如,UInt8
、UInt16
、UInt32
和 UInt64
在 Field
中均表示为 UInt64
。
抽象漏洞
IColumn
具有用于数据的常见关系转换的方法,但这些方法并不能够满足所有需求。比如,ColumnUInt64
没有用于计算两列和的方法,ColumnString
没有用于进行子串搜索的方法。这些无法计算的例程在 Icolumn
之外实现。
列(Columns)上的各种函数可以通过使用 Icolumn
的方法来提取 Field
值,或根据特定的 Icolumn
实现的数据内存布局的知识,以一种通用但不高效的方式实现。为此,函数将会转换为特定的 IColumn
类型并直接处理内部表示。比如,ColumnUInt64
具有 getData
方法,该方法返回一个指向列的内部数组的引用,然后一个单独的例程可以直接读写或填充该数组。实际上,«抽象漏洞(leaky abstractions)»允许我们以更高效的方式来实现各 种特定的例程。
数据类型
IDataType
负责序列化和反序列化:读写二进制或文本形式的列或单个值构成的块。IDataType
直接与表的数据类型相对应。比如,有 DataTypeUInt32
、DataTypeDateTime
、DataTypeString
等数据类型。
IDataType
与 IColumn
之间的关联并不大。不同的数据类型在内存中能够用相同的 IColumn
实现来表示。比如,DataTypeUInt32
和 DataTypeDateTime
都是用 ColumnUInt32
或 ColumnConstUInt32
来表示的。另外,相同的数据类型也可以用不同的 IColumn
实现来表示。比如,DataTypeUInt8
既可以使用 ColumnUInt8
来表示,也可以使用过 ColumnConstUInt8
来表示。
IDataType
仅存储元数据。比如,DataTypeUInt8
不存储任何东西(除了 vptr);DataTypeFixedString
仅存储 N
(固定长度字符串的串长度)。
IDataType
具有针对各种数据格式的辅助函数。比如如下一些辅助函数:序列化一个值并加上可能的引号;序列化一个值用于 JSON 格式;序列化一个值作为 XML 格式的一部分。辅助函数与数据格式并没有直接的对应。比如,两种不同的数据格式 Pretty
和 TabSeparated
均可以使用 IDataType
接口提供的 serializeTextEscaped
这一辅助函数。