在工作中遇到了一个 Map 写入 ClickHouse 时类型转换的问题，解决后记录如下。GORM 动态写入 ClickHouse 字段需求场景业务上游会生成多个下面这种 JSON 字符串，以及这样的表结构。{ "name": "张三", "age": 18, "phone": 15300000000, "sex": 1, "ip": "127.0.0.1", "time": 1687007258 }表结构是：CREATE TABLE gorm.test ( `name` String, `age` UInt8 ) ENGINE = MergeTree ORDER BY name SETTINGS index_granularity = 8192;在实现写入逻辑时，我们需要考虑这几个业务前提：上游提供的 JSON 字段可能会增加研发或运维或许会动态的调整数据库的字段所以我们需要根据 JSON 中有的字段以及数据表中的字段进行动态的匹配写入，这就要求我们编码时不能使用 Struct 结构体来定义 GORM Model，需要将 JSON 变成 Map 然后使用 GORM 写入数据库。暴露问题db, _ := gorm.Open(clickhouse.Open("clickhouse://default:@192.168.0.10:19000/gorm"), &gorm.Config{}) var ( str = "{\"name\":\"dbkuaizi\",\"age\":18,\"phone\":15300000000,\"sex\":1,\"ip\":\"127.0.0.1\",\"time\":1687007258}" Data map[string]any ) json.Unmarshal([]byte(str), &Data) if err := db.Table("test").Select("name", "age").Create(Data).Error; err != nil { fmt.Println(err) }运行程序输出了一个错误：2023/06/24 18:33:27 E:/code/ginweb/main.go:18 clickhouse [AppendRow]: age clickhouse [AppendRow]: converting float64 to UInt8 is unsupported [1.334ms] [rows:0] INSERT INTO `test` (`age`,`name`) VALUES (18.000000,'dbkuaizi') clickhouse [AppendRow]: age clickhouse [AppendRow]: converting float64 to UInt8 is unsupported 出错的原因是 ClickHouse 数据库中 age 字段类型是 Uint8，而 Json 反序列化时映射成了 Float64 类型， 接着 GORM 在入库前会对数据的类型进行校验，如果与字段类型不一致，且不能无损转换，就会报错。(Gorm ClickHouse 驱动实现依赖 ClickHouse官方提供的 go 驱动包，同样也会继承官方 Go 驱动的类型校验逻辑，具体可以参考 https://github.com/ClickHouse/clickhouse-go/blob/main/TYPES.md ) 解决方案这个出错的原因从官方的角度看是非常合理的，但我们数据是从 JSON 反序列化过来的，即使我们明知入库存储不存在范围的问题，也会因为数据类型的原因无法入库。所以我们如果想实现使用 map[string]any 插入数据到 clickhouse ，就需要根据数据表中实际的字段类型对 map[string]any 进行一次转换。获取字段类型实现类型转换的第一步，我们需要首先知道目标表有哪些字段需要写入，以及这些字段的类型。可以通过读取 clickhouse 的系统表获取字段信息：SELECT `name`,`type` FROM `system`.columns WHERE `database` = 'gorm' AND `table` = 'test' -- 结果 name|type | ----+------+ name|String| age |UInt8 |然后我们使用 Gorm 来读取： // 先定义一个结构体用来获取数据 type FieldType struct { FieldName string `gorm:"column:name"` FieldType string `gorm:"column:type"` } func main() { ... var DbTypes []FieldType db.Raw("SELECT `name`,`type` FROM `system`.columns WHERE `database` = ? AND `table` = ?", "gorm", "test").Find(&DbTypes) // [{FieldName:name FieldType:String} {FieldName:age FieldType:UInt8}] fmt.Printf("%+v", DbTypes) } 数据类型转换接着我们写一个类型映射的方法，根据 ClickHouse 的字段类型，对 Data 数据类型进行转换。在遍历的同时，记录动态写入的字段，这样就可以根据数据库的字段调整自动写入数据。// 字段类型映射 func DataTypeMap(DbTypes *[]FieldType, Data *map[string]any) []string { // 用一个变量来接受 Select 动态插入字段 var selectFields []string // 遍历 Clickhouse 字段 for _, field := range *DbTypes { // 如果 map 中存在这个字段 if val, state := (*Data)[field.FieldName]; state { // 就加入到 插入字段中 selectFields = append(selectFields, field.FieldName) // 接着对数据进行类型转换 switch field.FieldType { case "Int8": (*Data)[field.FieldName] = cast.ToInt8(val) case "Int16": (*Data)[field.FieldName] = cast.ToInt16(val) case "Int32": (*Data)[field.FieldName] = cast.ToInt32(val) case "Int64": (*Data)[field.FieldName] = cast.ToInt64(val) case "UInt8": (*Data)[field.FieldName] = cast.ToUint8(val) case "UInt16": (*Data)[field.FieldName] = cast.ToUint16(val) case "UInt32": (*Data)[field.FieldName] = cast.ToUint32(val) case "UInt64": (*Data)[field.FieldName] = cast.ToUint64(val) case "Decimal": (*Data)[field.FieldName] = decimal.NewFromFloat(cast.ToFloat64(val)) case "String": (*Data)[field.FieldName] = cast.ToString(val) } } } // 将需要插入的字段进行返回 return selectFields } 完整代码示例package main import ( "encoding/json" "fmt" "github.com/shopspring/decimal" "github.com/spf13/cast" "gorm.io/driver/clickhouse" "gorm.io/gorm" ) type FieldType struct { FieldName string `gorm:"column:name"` FieldType string `gorm:"column:type"` } func main() { db, _ := gorm.Open(clickhouse.Open("clickhouse://default:@192.168.0.10:19000/gorm"), &gorm.Config{}) var ( str = "{\"name\":\"dbkuaizi\",\"age\":18,\"phone\":15300000000,\"sex\":1,\"ip\":\"127.0.0.1\",\"time\":1687007258}" Data map[string]any DbTypes []FieldType ) // 获取字段名称和类型 db.Raw("SELECT `name`,`type` FROM `system`.columns WHERE `database` = ? AND `table` = ?", "gorm", "test").Find(&DbTypes) // 拿到反序列化后的数据 json.Unmarshal([]byte(str), &Data) // 进行类型转换，并拿到需要动态写入的字段 selectFields := DataTypeMap(&DbTypes, &Data) if err := db.Table("test").Select(selectFields).Create(Data).Error; err != nil { fmt.Println(err) } } // 字段类型映射 func DataTypeMap(DbTypes *[]FieldType, Data *map[string]any) []string { // 用一个变量来接受 Select 动态插入字段 var selectFields []string // 遍历 Clickhouse 字段 for _, field := range *DbTypes { // 如果 map 中存在这个字段 if val, state := (*Data)[field.FieldName]; state { // 就加入到 插入字段中 selectFields = append(selectFields, field.FieldName) // 接着对数据进行类型转换（这里只是转换了一部分类型，做个演示） switch field.FieldType { case "Int8": (*Data)[field.FieldName] = cast.ToInt8(val) case "Int16": (*Data)[field.FieldName] = cast.ToInt16(val) case "Int32": (*Data)[field.FieldName] = cast.ToInt32(val) case "Int64": (*Data)[field.FieldName] = cast.ToInt64(val) case "UInt8": (*Data)[field.FieldName] = cast.ToUint8(val) case "UInt16": (*Data)[field.FieldName] = cast.ToUint16(val) case "UInt32": (*Data)[field.FieldName] = cast.ToUint32(val) case "UInt64": (*Data)[field.FieldName] = cast.ToUint64(val) case "Decimal": (*Data)[field.FieldName] = decimal.NewFromFloat(cast.ToFloat64(val)) case "String": (*Data)[field.FieldName] = cast.ToString(val) } } } // 将需要插入的字段进行返回 return selectFields }动态写入测试随后修改数据库字段，增加一个 phone 的字段，在不需要调整代码的前提下，数据也可以正常入库。这样就实现了字段动态写入的需求。name |age|phone | --------+---+-----------+ dbkuaizi| 18| | dbkuaizi| 18|15300000000| 需要注意的是，demo 中没有并发场景，所以直接使用了 map 类型。如果实际使用中有并发场景，考虑到并发安全，应使用 sync.Map 类型。
对于一款支持海量数据分析系统而言，能支持 DML 查询实属难得可贵。在海量的数据场景下，很多看似简单的操作也会变得举步维艰，所以一些系统会选择做减法来规避一些难题。而 ClickHouse 支持完备的 DML 命令，包括 INSERT、SELECT、UPDATE 和 DELETE。虽UPDATE 和 DELETE 可能会存在一些性能问题，但这些能力的提供确实丰富了架构师的筹码，在架构设计师可以多几个选择。作为一款完备的 DBMS，ClickHouse 提供了 DDL 和 DML 功能，并支持大部分标准的SQL。这也是 ClickHouse 相对容易上手的原因。但作为一款异军突起的 OLAP 数据库黑马，ClickHouse 有着属于自己的设计目标，高性能才是它的根本。比如 ClickHouse 在基础数据方面相比于其他常规数据库会少一些，但它又同时提供了实用的复合数据类型，而这些复合类型是常规数据库所不具备的。再比如，ClickHouse 所提供的 DDL 和 DML 查询，在部分细节上和其他数据库也是不同的（例如 UPDATE、DELETE 是借助 ALTER 的变种实现的）。所以掌握 ClickHouse 中的数据定义方法是很重要的，这能帮助我们深刻理解和使用 ClickHouse。ClickHouse 数据类型作为一款分析型数据库，ClickHouse 具备了很多数据类型，他们可以划分为：基础类型、复合类型和特殊类型。其中基础类型使 ClickHouse 具备了描述数据的基础能力，另外两个类型则使得 ClickHouse 数据的表达能力更加丰富立体。基础类型基础的数据类型只有数值、字符串、时间三种类型，没有 Boolean 类型，但是可以用 0 和 1 替代。数值类型整型名称大小(字节)范围普遍概念Int81-128 到 127TinyintInt162-32768 到 32767SmallintInt324-2147483648 到 2147483647IntInt648-9223372036854775808 到 9223372036854775807BigintUInt810 到 255Tinyint UnsignedUInt1620 到 65535Smallint UnsignedUInt3240 到 4294967295Int UnsignedUInt6480 到 18446744073709551615Bigint Unsigned在传统观念中，常用 Tinyint、Smallint、Int 和 Bigint 指代整数的不同取值范围。而 ClickHouse 则直接用Int8、Int16、Int32、Int64 来指代四种大小的 Int 类型，其末尾的数字用来表明占用字节的大小（8位 = 1 字节）。无符号则使用前缀 U 来表示（Unsigned）。浮点型名称大小(字节)有效精度（位数）普遍概念Float3247FloatFloat64816Double在使用浮点数的时候，要意识到他的精度是有限的。例如，Float32 的有效长度为小数点后 7 位，实际存储可以存储 8 位，但第八位的精度不准，Float64 则为是从第十七位开始，精度不准。dbkuaizi-clickhouse :) SELECT toFloat32('0.123456789') as f32, toTypeName(f32); SELECT toFloat32('0.123456789') AS f32, toTypeName(f32) Query id: 502f6090-cc47-41d7-819e-37cf6b90b0aa ┌────────f32─┬─toTypeName(toFloat32('0.123456789'))─┐ │ 0.12345679 │ Float32 │ └────────────┴──────────────────────────────────────┘ 1 row in set. Elapsed: 0.001 sec. ClickHouse 的浮点型支持正无穷、负无穷、以及 非数字三种表达方式：dbkuaizi-clickhouse :) SELECT 0.8/0,-0.8/0,0/0; SELECT 0.8 / 0, -0.8 / 0, 0 / 0 Query id: 789b7cc8-df3d-49eb-ad18-de1aec7867b4 ┌─divide(0.8, 0)─┬─divide(-0.8, 0)─┬─divide(0, 0)─┐ │ inf │ -inf │ nan │ └────────────────┴─────────────────┴──────────────┘ 1 row in set. Elapsed: 0.001 sec. Decimal如果要求更高精度的运算，则需要使用定点数。ClickHouse 提供了 Decimal32、Decimal64 和 Decimal128 三种精度的定点数。可以通过两种方式声明定点数：简写方式有 Decimal32(S)、Decimal64(S)、Decimal128(S) 三种，原生方式为 Decimal(P,S)，其中：P 代表精度，决定总位数（整数位数+小数位数），取值范围是 1 ~ 38；S 代表规模，代表小数位数，取值范围是 0 ~ P。简写方式和原生方式对比如图：名称等效声明范围Decimal32(S)Decimal(1 ~ 9,S)-1 10^(9-S) 到 1 10^(9-S)Decimal64(S)Decimal(10 ~ 18,S)-1 10^(18-S) 到 1 10^(18-S)Decimal128(S)Decimal(19 ~ 38,S)-1 10^(38-S) 到 1 10^(38-S)在使用两个不同精度的定点数进行四则运算时，他们的小数点位数 S 会发生变化。规则如下：加法运算：S 取最大值，例如 toDecimal64(2,4) 与 toDecimal32(2,2) 相加后 S = 4减法运算：S 取最大值，与加法相同乘法运算：S 取两者之和，例如 toDecimal64(2,4) 与 toDecimal32(2,2) 相乘后 S = 4+2=6除法运算：S 取被除数的值，此时要求被除数的 S 必须大于除数的 S，否则就会报错。例如 toDecimal64(2,4) 与 toDecimal32(2,2) 相除后 S = 4 最后需要注意的是，因为现代计算机系统只支持 32 和 64 位系统的 CPU，所以 Decimal128 是在软件层面模拟实现，速度会明显慢于 Decimal32 和 Decimal64。 字符串类型字符串类型可以细分为 String、FixedString、UUID 三类。String字符串由 String 定义，长度不限。因此在使用此类型时无需声明大小，完全代替了传统数据库中的 VarChar、Text、Clob 和 Blob 类型。同时也没字符集的概念，可以将任意编码的字符存入其中。但是可维护性，需要在同一套程序中遵循使用统一的编码，例如“统一使用 UTF-8”编码这样的约定。FixedStringFixedString 类型和传统意义上的 Char 类型有些类似，对于一些字符有明确长度的场合，可以使用固定长度的字符串。定长字符串通过 FixedString(N) 来声明，N 表示字符串长度。与 Char 类型不同的是 FixedString 使用 null 填充末尾字符，而 Char 通常使用空格填充。UUIDUUID 是一种常见的数据库主键类型，在 ClickHouse 中被直接当作一种数据类型。UUID 有 32 位。它的格式为 8-4-4-4-12。如果一个 UUID 类型在写入时没有赋值，就会按照格式使用 0 填充，即 00000000-0000-0000-0000-000000000000。时间类型时间类型分为 DateTime、DateTime64 和 Date 三类。DateTime时间戳类型，用四个字节的（无符号）整数型存储 Unix 时间戳，允许存储与日期类型相同范围内的值。值的范围: [1970-01-01 00:00:00, 2106-02-07 06:28:15]。INSERT INTO Datetime_TEST VALUES('2023-04-22 00:00:00')DateTime64此类型允许以 日期加时间的形式来存储一个时间值，具有定义的亚秒精度，语法如下：DateTime64(precision, [timezone])precision 表示时间刻度（精度）大小，最大 8 位timezone 表示时区，此信息存储在列的元数据中值的范围: [1900-01-01 00:00:00, 2299-12-31 23:59:59.99999999]Date日期类型，用两个字节存储，表示从 1970-01-01 (无符号) 到当前的日期值。值的范围: [1970-01-01, 2149-06-06]，日期类型没有时区概念。Date32一种日期表示方式，支持与 DateTime64 相同的日期范围，值代表自 1970-01-01 以来的天数（可以理解为输入一个时间戳，但只存储年月日）。值的范围: [1970-01-01, 2299-12-31]复合类型除了基础的数据类型外，ClickHouse 还提供了数组、元组、枚举和嵌套之类的复合类型。这些类型通常是其他数据库原生所不具备的特性，拥有了复合类型，ClickHouse 的数据模型表达能力更强了。Array 数组ClickHouse 的数组与其他强类型语言原理类似，同一类型的数据组合。创建数组有两种方式创建数组，常规方式为 array(T) 或简写方式 [T]：localhost :) SELECT array(1,2) as a, toTypeName(a); ┌─a─────┬─toTypeName([1, 2])─┐ │ [1,2] │ Array(UInt8) │ └───────┴────────────────────┘ localhost :) SELECT [1,2] as a, toTypeName(a); ┌─a─────┬─toTypeName([1, 2])─┐ │ [1,2] │ Array(UInt8) │ └───────┴────────────────────┘数组类型在定义数组时，不需要主动声明数组的元素类型（动态创建数组）。因为 ClickHouse 的数组拥有类型推断的功能，推断依据为：以最小的存储代价为原则，使用最小可表达的数据类型。例如上面的 array(1,2) 会自动推断为 Uint8 作为数组的存储类型。但是数组元素如果存在 Null 值，元素类型将会变为 Nullabel (此类型后面会讲)， 例如：localhost :) SELECT [1,2,null] as a, toTypeName(a); ┌─a──────────┬─toTypeName([1, 2, NULL])─┐ │ [1,2,NULL] │ Array(Nullable(UInt8)) │ └────────────┴──────────────────────────┘在定义表字段时，需要明确指定数组元素类型：CREATE TABLE Array_TEST ( c1 Array(String) ) engine = Memory 如果 ClickHouse 无法确定数据类型，就会报错，例如：array(1,'a') 这样的数组无法推断使用什么类型来存储数组元素的最大数量限制为一百万个 数组大小如果想知道数组的元素数量，而不想读取整个字段时，可以通过 sizeN 来实现，对于多维数组，可以使用 sizeN-1,其中 N 代表查询层级:CREATE TABLE t_arr (`arr` Array(Array(Array(UInt32)))) ENGINE = MergeTree ORDER BY tuple(); INSERT INTO t_arr VALUES ([[[12, 13, 0, 1],[12]]]); SELECT arr.size0, arr.size1, arr.size2 FROM t_arr; ┌─arr.size0─┬─arr.size1─┬─arr.size2─┐ │ 1 │ [2] │ [[4,1]] │ └───────────┴───────────┴───────────┘Tuple 元组元组由 1 ~ N 个元素组成，每个元素都可以设置不同的数据类型，且不要求兼容。同时，元组也支持类型推断，依据依然是以最小存储代价为原则。创建元组元组的创建方式与数组类似，元组也可以使用两种方式定义，常规方式为 tuple(T) 或简写方式 (T)。SELECT tuple(1,'a') AS x, toTypeName(x) ┌─x───────┬─toTypeName(tuple(1, 'a'))─┐ │ (1,'a') │ Tuple(UInt8, String) │ └─────────┴───────────────────────────┘ -- 简写示例 SELECT (1, 'a') AS x, (today(), rand(), 'someString') y, ('a') not_a_tuple; ┌─x───────┬─y──────────────────────────────────────┬─not_a_tuple─┐ │ (1,'a') │ ('2022-09-21',2006973416,'someString') │ a │ └─────────┴────────────────────────────────────────┴─────────────┘元素类型元组会自动检测每个元素最小可存储类型，如果类型为 Null 则元组元素的类型为 NullabelSELECT tuple(1, NULL) AS x, toTypeName(x) ┌─x────────┬─toTypeName(tuple(1, NULL))──────┐ │ (1,NULL) │ Tuple(UInt8, Nullable(Nothing)) │ └──────────┴─────────────────────────────────┘在定义表字段时，元组也要指定明确的元素类型CREATE TABLE Tuple_TEST ( c1 Tuple(String,Int8) ) ENGINE = Memory;获取元组可以使用索引或名称读取命名元组的元素：CREATE TABLE named_tuples (`a` Tuple(s String, i Int64)) ENGINE = Memory; INSERT INTO named_tuples VALUES (('y', 10)), (('x',-10)); SELECT a.s FROM named_tuples; ┌─a.s─┐ │ y │ │ x │ └─────┘ SELECT a.2 FROM named_tuples; ┌─tupleElement(a, 2)─┐ │ 10 │ │ -10 │ └────────────────────┘Enum 枚举ClickHouse 支持由命名值组成的枚举类型，命名值声明为 'string' = int 或 'string' 名称，ClickHouse 只存储数字，但支持通过名称对值进行操作。ClickHouse 支持两种 Enum 类型：8 位 Enum，最多可以存储 [-128 ~ 127]，范围内枚举的 256 个值，16 位 Enum ，最多可以存储 [-32768 ~ 32767] 范围内枚举的 65535 个值。ClickHouse 会在定义表结构时，根据设定的命名值自动推断 Enum 类型。也可以使用 Enum8 或 Enum16 来指定存储的类型。使用 Enum 的优点或许有的人会疑惑，使用 String 完全可以代替掉枚举，为什么还需要专门的枚举类型呢？这么做的原因是处于性能的考虑，虽然枚举定义中 Key 仍然为 String 类型，但数据库底层操作中实际使用的是 Int 类型的 Value 值，效率比String 高得多。Enum 创建创建 Enum 需要注意以下几点：Key 和 Value 不允许重复，要保证唯一性Key 和 Value 的值都不允许为 Null， 但 Key 允许是空字符串Value 值的数字可以是任意顺序，无关紧要创建一个带有 Enum8('hello' = 1, 'world' = 2) 类型的字段：CREATE TABLE t_enum ( x Enum('hello' = 1, 'world' = 2) ) ENGINE = TinyLog也可以省略数字，ClickHouse 默认会自动从 1 开始分配编号。CREATE TABLE t_enum ( x Enum('hello', 'world') ) ENGINE = TinyLog当然也可以手动指定一个合法起始编号：CREATE TABLE t_enum ( x Enum('hello' = -128, 'world') ) ENGINE = TinyLogEnum 插入Enum 列定义完成后，只能存储定义字段时列出的值，如果想尝试保存其他值，ClickHouse 会引发异常。例如，以上列示例中表为例：-- 正常写入 INSERT INTO t_enum VALUES ('hello'), ('world'), ('hello') -- 报错 INSERT INTO t_enum values('abc')Enum 查询直接查询 Enum 列时，会显示对应的字符串，若要查看对应的 Int值，则可以使用 CAST() 函数 实现。dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum ┌─x─────┬─CAST(x, 'Int8')─┐ │ hello │ 1 │ │ world │ 2 │ │ hello │ 1 │ └───────┴─────────────────┘ 3 rows in set. Elapsed: 0.010 sec. Enum NullEnum 中 Key 和 Value 不允许为字符串，但可以通过 Nullable 的方式实现，将 NULL 包含在 Nllable 类型中：CREATE TABLE t_enum_nullable ( x Nullable( Enum8('hello' = 1, 'world' = 2) ) ) ENGINE = TinyLog;这也就可以同时存储 字符串和 Null 了：INSERT INTO t_enum_nullable Values('hello'),('world'),(NULL)Enum 读取当以文本形式(Value)读取时，ClickHouse 将值被解析为字符串，并从 Enum 的集合中搜索相应的字符串，若未找到则会报错。当以文本格式(Key)读取时，读取字符串并查找对应的的数值，若未找到则会报错。当以文本形式(Value)写入时，它将数值写成相应的字符串。如果列数据包含垃圾（不属于有效集合的数字），就会抛出一个异常。当以二进制的形式读取和写入的方式与 Int8 和 Int16 类型一致。 -- 个人理解以文本形式写入 dbkuaizi-clickhouse :) INSERT INTO t_enum VALUES (2); -- 转换成字符串了 dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum ┌─x─────┬─CAST(x, 'Int8')─┐ │ hello │ 1 │ │ world │ 2 │ │ hello │ 1 │ │ world │ 2 │ └───────┴─────────────────┘ dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x =1 ┌─x─────┬─CAST(x, 'Int8')─┐ │ hello │ 1 │ │ hello │ 1 │ └───────┴─────────────────┘ 2 rows in set. Elapsed: 0.145 sec. dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x = 'hello' ┌─x─────┬─CAST(x, 'Int8')─┐ │ hello │ 1 │ │ hello │ 1 │ └───────┴─────────────────┘ 2 rows in set. Elapsed: 0.049 sec. Enum 其他在 Order By 、Group By、IN、DISTINCT 等操作中， Enum 的行为与对应的数字相同，例如 按数字排序。在等式运算和比较运算时，Enum 的工作机制与它们底侧的那个数值的工作机制相同。枚举值（字符串）不能直接与数字比较，被拒之可以与常量字符串比较。 如果比较的字符串不在 Enum 有效值中，将会引发异常。SELECT x,CAST(x, 'Int8') FROM t_enum where x = 'hello'; -- 正常运行 SELECT x,CAST(x, 'Int8') FROM t_enum where x = 'abc'; -- 会报错可以使用 IN 运算符来判断一个 Enum 是否存在于某个 Enum 集合中，其中 IN 集合中字符串，也必须是 Enum 的有效值，否则将会引发异常。dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x IN('hello'); -- 正常 dbkuaizi-clickhouse :) SELECT x,CAST(x, 'Int8') FROM t_enum where x IN('hello','abc'); -- 会报错枚举值也可以通过 toT 函数转为数字类型，其中 T 是数字类型，当 T 对应的是枚举的基础数字类型时，这种转换是零成本的。-- enum8 int类型底层是 int8，可以直接这样零损耗转换 dbkuaizi-clickhouse :) SELECT x,toInt8(x) FROM t_enum; ┌─x─────┬─toInt8(x)─┐ │ hello │ 1 │ │ world │ 2 │ │ hello │ 1 │ │ world │ 2 │ └───────┴───────────┘Enum 类型可以被 ALTER 无成本的修改对应合集的值，可以通过 ALTER 操作来增加或删除 Enum 的成员（只要该表没有用到此值，直接删除都是安全的），如果改变已经被使用的值，将会抛出异常通过 ALTER 操作可以将 Enum8 转为 Enum16，反之毅然。与 Int8 转 Int16 一样。Nested 嵌套嵌套类型的数据结构就像是单元格中的表格，嵌套数据结构在声明时，与创建表时声明字段的方式完全一样。每行数据中的嵌套字段，可以包含任意数量的行。 换一个说法，如果将 ClickHouse 的表看作是一个列式存储的 JSON 对象数组，那么嵌套类型就是某个字段里面又嵌入了一个 列式存储的 JSON 对象数组。 Nested 创建下面创建一个员工表，每个员工负责 N 项目，我们把项目id 和名称作为一个嵌套结构来存储（感觉有点像多对多中间表的用法）CREATE TABLE employee ( name String, project Nested( id UInt8, name String ) ) ENGINE = Memory;Nested 插入需要注意的是：每个嵌套类型的单元格中所有的列长度必须相同。-- 使用数组的方式插入数据 INSERT INTO employee VALUES ('张三' ,[100,101], ['项目1','项目2']); -- 如果两个列长度不一致，就会报错。 INSERT INTO employee VALUES ('张三' ,[100,101,104], ['项目1','项目2']); -- 长度一致，可以插入 INSERT INTO employee VALUES ('张三' ,[100,101,104], ['项目1','项目2','项目3']);Nested 查询大多情况下，当使用嵌套结构时，其中列是以点分割的列名称指定的，这些列组成了一个匹配类型的数组，单个嵌套结构的所有列数组都具有相同长度。最容易理解的方式是将嵌套数据结构视为一组相同长度的多个列数组。dbkuaizi-clickhouse :) SELECT name,project.id,project.name FROM employee; ┌─name─┬─project.id─┬─project.name──────┐ │ 张三 │ [100,101] │ ['项目1','项目2'] │ └──────┴────────────┴───────────────────┘ ┌─name─┬─project.id─┬─project.name──────┐ │ 李四 │ [103,105] │ ['项目3','项目4'] │ └──────┴────────────┴───────────────────┘可以使用 ARRAY JOIN 字句将嵌套结构展开:dbkuaizi-clickhouse :) SELECT name,p.id,p.name FROM employee ARRAY JOIN project AS p ; ┌─name─┬─p.id─┬─p.name─┐ │ 张三 │ 100 │ 项目1 │ │ 张三 │ 101 │ 项目2 │ └──────┴──────┴────────┘ ┌─name─┬─p.id─┬─p.name─┐ │ 李四 │ 103 │ 项目3 │ │ 李四 │ 105 │ 项目4 │ └──────┴──────┴────────┘补充：只是命令行这么显示，实际查询出来是一个列表name|p.id|p.name| ----+----+------+ 李四 | 103|项目3 | 李四 | 105|项目4 | 张三 | 100|项目1 | 张三 | 101|项目2 | 嵌套结构的字段不支持查询操作，只能在 SELELCT 中列出来，例如下面的 SQL 就无法执行：SELECT name,project.id,project.name FROM employee WHERE project.id = 100; ALTER 操作需要注意的是，如果要对嵌套结构里的字段进行 ALTER 操作，会具有一定的局限性。Map 类型Map(Key,Value) 类型存储 Key-Value 键值对，Key,Value 类型要求如下：Key 可选类型为：String、Integer、LowCardinality、FixedString、UUID、Date、DateTime、Date32、EnumValue 可选类型为：任何类型，甚至可以是 Map 和 Array 类型Map 创建创建 Map 字段时，需要明确指定 Key 和 Value 类型CREATE TABLE table_map (a Map(String, UInt64)) ENGINE=Memory;Map 插入插入数据时要使用 要使用花括号的格式插入，类似于 JOSN 对象的写法INSERT INTO table_map VALUES ({'key1':1, 'key2':10}), ({'key1':2,'key2':20}), ({'key1':3,'key2':30});Map 查询查询 Map 类型时，如果只获取某个Value，需要用中括号的方式取值：MiWiFi-RA69-srv :) SELECT a['key2'] FROM table_map; ┌─arrayElement(a, 'key2')─┐ │ 10 │ │ 20 │ │ 30 │ └─────────────────────────┘当获取不存在的 Key 时，会返回零：MiWiFi-RA69-srv :) SELECT a['key3'] FROM table_map; ┌─arrayElement(a, 'key3')─┐ │ 0 │ │ 0 │ │ 0 │ └─────────────────────────┘将 Tuple 转为 MapMiWiFi-RA69-srv :) SELECT CAST(([1, 2, 3], ['Ready', 'Steady', 'Go']), 'Map(UInt8, String)') AS map; ┌─map───────────────────────────┐ │ {1:'Ready',2:'Steady',3:'Go'} │ └───────────────────────────────┘Map.keys 和 Map.values如果只想获取所有的 Key 或者所有的 Value，可以这样查询MiWiFi-RA69-srv :) SELECT a.keys,a.values FROM table_map; ┌─a.keys──────────┬─a.values─┐ │ ['key1','key2'] │ [1,10] │ │ ['key1','key2'] │ [2,20] │ │ ['key1','key2'] │ [3,30] │ └─────────────────┴──────────┘ ┌─a.keys──────────┬─a.values─┐ │ ['key1','key3'] │ [1,40] │ └─────────────────┴──────────┘特殊类型ClickHouse 还有一些特殊类型。LowCardinality 低基数将其他数据类型在存储时转为字典编码。LowCardinality(data_type)data_type 可选类型: String、FixedString、Date、DateTime、Float、Integer，LowCardinality 对某些数据类型的效率并不高，具体参考：settings#allow_suspicious_low_cardinality_types。LowCardinality 改变了数据的存储方式和数据的处理规则，ClickHouse 将 字典编码 应用于LowCardinality 列，使用字典编码数据操作可以显著提高 SELECT 查询性能。使用 LowCardinality 的效率取决于数据的多样性，如果字典包含的值多样性少于 10000 个，那么 ClickHouse 会表现出更高的数据读取或存储效率，如果字典值的多样性超过 10000 个，那么与普通的数据类型相比，ClickHouse 的效率可能会更差。 在处理字符串时，应该优先考虑使用 LowCardinality 而不是枚举，因为 LowCardinality 拥有更大的灵活性以及更高的效率。 LowCardinality 创建CREATE TABLE lc_t ( `id` UInt16, `strings` LowCardinality(String) ) ENGINE = MergeTree() ORDER BY idLowCardinality 实践Reducing Clickhouse Storage Cost with the Low Cardinality Type – Lessons from an Instana Engineer聊聊ClickHouse中的低基数（LowCardinality）类型Nullable 可为空在设定字段类型时使用 Nullabel 修饰，将允许存储 Null 来代表值缺失。例如 Nullabel(Int8) 类型, 除了存储 Int8 标准数值之外还允许存储 Null。Nullabel(typeName)typeName 不能是复合数据类型，例如 Array、Map、Tuple，复合数据类型可以包含 Nullable 类型值，例如：Array(Nullable(Int8)) , Nullable 类型不能包含在表索引中。设计表结构时应慎用 Nullable 类型，Nullable 类型对查询和写入性能都有负面影响。 使用 -- 创建表 dbkuaizi :) CREATE TABLE nullable (`n` Nullable(UInt32)) ENGINE = MergeTree ORDER BY tuple(); -- 写入数据 dbkuaizi :) INSERT INTO nullable VALUES (1) (NULL) (2) (NULL); -- 查询数据 dbkuaizi :) SELECT n.null FROM nullable; ┌─n.null─┐ │ 0 │ │ 1 │ │ 0 │ │ 1 │ └────────┘ -- 查询不是 null 的记录 dbkuaizi :) SELECT * FROM nullable where n.null == 0; Query id: bb83f038-408c-4e51-9db8-7d507741be1d ┌─n─┐ │ 1 │ │ 2 │ └───┘通过 字段.null 的方法可以返回会值是否是 null，如果是 null 则返回 1 否则返回 0。Nullabel 运算当 Null 值与其他值进行运算时，结果为 Null。CREATE TABLE t_null(x Int8, y Nullable(Int8)) ENGINE TinyLog; INSERT INTO t_null VALUES (1, NULL), (2, 3); SELECT x + y FROM t_null; ┌─plus(x, y)─┐ │ ᴺᵁᴸᴸ │ │ 5 │ └────────────┘Domains 域域类型分为 IPv4 和 IPv6 两类，本质上是对整型和字符串类型进一步封装，并且在写入时对格式进行相应的校验，不符合规范的数据无法被写入。 无论是 IPv4 还是 IPv6 底层存储是都是转为紧凑的二进制进行存储 IPv4IPv4 是一个基于 Uint32 类型的域，用作存储 IPv4 值的类型代替。它提供了人性化的输入校验，以及更小的存储占用。使用CREATE TABLE hits (url String, from IPv4) ENGINE = MergeTree() ORDER BY from; INSERT INTO hits (url, from) VALUES ('https://wikipedia.org', '116.253.40.133')('https://clickhouse.com', '183.247.232.58')('https://clickhouse.com/docs/en/', '116.106.34.242'); SELECT * FROM hits; ┌─url────────────────────────────────┬───────────from─┐ │ https://clickhouse.com/docs/en/ │ 116.106.34.242 │ │ https://wikipedia.org │ 116.253.40.133 │ │ https://clickhouse.com │ 183.247.232.58 │ └────────────────────────────────────┴────────────────┘ 虽然查询时 IPv4 会自动格式化显示，但我们需要清楚的是 IPv4 不能隐式转换为除 Uint32 类型以外的类型。所以不能直接当字符串用，必须使用 IPv4NumToString() 函数进行转换操作。 SELECT toTypeName(s), IPv4NumToString(from) as s FROM hits LIMIT 1; ┌─toTypeName(IPv4NumToString(from))─┬─s──────────────┐ │ String │ 183.247.232.58 │ └───────────────────────────────────┴────────────────┘或转为 Uint32 类型的值：SELECT toTypeName(i), CAST(from as UInt32) as i FROM hits LIMIT 1; ┌─toTypeName(CAST(from, 'UInt32'))─┬──────────i─┐ │ UInt32 │ 3086477370 │ └──────────────────────────────────┴────────────┘IPv6IPv6 是一个基于 FixedString(16) 类型的域，用作存储 IPv6 值的类型替代。与 IPv4 一样提供了输入校验和小的存储占用。使用CREATE TABLE hits (url String, from IPv6) ENGINE = MergeTree() ORDER BY from; INSERT INTO hits (url, from) VALUES ('https://wikipedia.org', '2a02:aa08:e000:3100::2')('https://clickhouse.com', '2001:44c8:129:2632:33:0:252:2')('https://clickhouse.com/docs/en/', '2a02:e980:1e::1'); SELECT * FROM hits; ┌─url────────────────────────────────┬─from──────────────────────────┐ │ https://clickhouse.com │ 2001:44c8:129:2632:33:0:252:2 │ │ https://clickhouse.com/docs/en/ │ 2a02:e980:1e::1 │ │ https://wikipedia.org │ 2a02:aa08:e000:3100::2 │ └────────────────────────────────────┴───────────────────────────────┘ 值不能隐式的转为除 FixedString(16) 意外的类型，如果需要转为字符串，必须使用 IPv6NumToString() 函数进行转换。 地理位置类型这玩意用的实在太少了，后面用到了再整理。官方文档链接地址：https://clickhouse.com/docs/en/sql-reference/data-types/geo笔记少部分内容来自 《ClickHouse 原理解析与应用实践》第四章 第一小节 ClickHouse 的数据类型大部分内容来自 ClickHouse 英文版官方文档（说一嘴，不同语言的文档详细程度是不同的）
ClickHouse 支持 X84-64、ARM(aarch64) 或 PowerPC 三种 CPU 架构的 Linux、FreeBSD 或 macOS 系统上运行。安装使用 RPM 离线安装rpm 发行包地址：https://packages.clickhouse.com/rpm/stable/我们以 x86_64 架构，下载 23.2.4.12 版本为例，需要下载如下三个文件：clickhouse-client-23.2.4.12.x86_64.rpmclickhouse-common-static-23.2.4.12.x86_64.rpmclickhouse-server-23.2.4.12.x86_64.rpm官方提供了对每个版本分别提供了五个安装文件，他们的含义如下：clickhouse-common-static 安装 ClickHouse 编译的二进制文件clickhouse-server 创建符号链接和默认服务器配置clickhouse-client 为其他客户端创建符号链接，并安装客户端配置文件。clickhouse-common-static-dbg 安装带有调试信息的 ClickHouse 编译的二进制文件clickhouse-keeper 在专用的 Clickhouse Keeper 节点上安装 ClickHouse Keeper。如果您在与ClickHouse服务器相同的服务器上运行ClickHouse Keeper，则无需安装此软件包。安装 ClickHouse Keeper 和默认的 ClickHouse Keeper 配置文件。执行命令安装：[root@MiWiFi-RA69-srv rpms]# ll 总用量 256628 -rw-r--r-- 1 root root 34978 3月 11 01:51 clickhouse-client-23.2.4.12.x86_64.rpm -rw-r--r-- 1 root root 262688105 3月 27 21:41 clickhouse-common-static-23.2.4.12.x86_64.rpm -rw-r--r-- 1 root root 61353 3月 27 21:20 clickhouse-server-23.2.4.12.x86_64.rpm [root@MiWiFi-RA69-srv rpms]# rpm -ivh clickhouse-*在线安装使用 yumsudo yum install yum-utils sudo rpm --import https://repo.clickhouse.com/CLICKHOUSE-KEY.GPG sudo yum-config-manager --add-repo https://repo.clickhouse.com/rpm/clickhouse.repo sudo yum install clickhouse-server clickhouse-client sudo /etc/init.d/clickhouse-server start使用 apt 安装sudo apt-get install clickhouse-server clickhouse-client clickhouse-common-static -- 也可以指定版本 sudo apt-get install clickhouse-server=21.8.5.7 clickhouse-client=21.8.5.7 clickhouse-common-static=21.8.5.7其他系统安装如果是 Ubuntu 或 Debian 官方提供了 deb 安装包，如果是其他发行版 官方也提供了 tgz 包的安装方式：https://clickhouse.com/docs/en/install访问接口ClickHouse 自身提供了很多种访问协议（接口），比较有代表性的是 TCP 协议（原生接口）和 HTTP 协议，默认端口 9000 主要用于 CLI 客户端连接或其他语言通过 TCP 连接使用，TCP 协议的性能最高；HTTP 协议则拥有更好的兼容性，可以在任何编程语言的任何平台上使用 ClickHouse，HTTP 接口下相比原生 TCP 接口收到了更多的限制，但也拥有更好的兼容性。除了 TCP 协议 和 HTTP 协议之外，ClickHouse 还支持了 MySQL 通讯协议（9004）和 PostgreSQL 通讯协议（9005）。例如可以直接使用 MySQL 命令行工具进行连接。PS C:\Users\Administrator> mysql -h 192.168.0.100 -u default -p123456 -P 9004 mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 Server version: 23.2.3.17-ClickHouse Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> select version(); +-----------+ | version() | +-----------+ | 23.2.3.17 | +-----------+ 1 row in set (0.06 sec) Read 1 rows, 1.00 B in 0.050673 sec., 19 rows/sec., 19.73 B/sec.在本人实际的开发工作中，目前有用到 C++、PHP、Go 等不同的编程语言操作 ClickHouse 数据库，常用的语言均可以得到完善的功能支持。图形化客户端如果想通过图形化客户端管理 ClickHouse 数据库的话，官方也推荐了一些软件，具体可以查看 https://clickhouse.com/docs/zh/interfaces/third-party/gui本人日常使用 DBeaver，除了 ClickHouse 之外，还能顺便管理其他数据库一举多得。笔记来自：《ClickHouse 原理解析与应用实践》第三章 ClickHouse 安装与部署
随着业务的增长，ClickHouse 已经为 Yandex.Metrica 存储了超过二十万亿行数据，90% 的自定义查询都能在 1 秒内响应结果，集群规模也超过了 400 台服务器。ClickHouse 的核心特性ClickHouse 是一款 MPP 架构的列式数据库，但 MPP 和列式存储并不是什么新鲜玩意，为什么只有 ClickHouse 的性能如此出众？通过前面的介绍，我们知道ClickHouse 发展至今的演进过程。一共经历了四个阶段，每一个阶段的迭代都是取其精华去其糟粕。可以说 ClickHouse 吸收了个各种技术的精髓，下面了解一下 ClickHouse 数据库的一些和新特性，正是这些特性使得 ClickHouse 如此有优秀。完整的 DBMS 功能ClickHouse 具备完整的管理功能，所以称得上 DBMS (Datbase Management System，数据库管理系统)，不仅仅是一个数据库，作为一个 DBMS，还具备以下功能（仅为代表性功能，并非全部功能）：DDL (数据库定义语言)：动态的创建、修改或删除数据库、表或视图。DML (数据操作语言)：查询、插入、修改、删除记录。权限控制：按照用户粒度设置数据库或者表的操作权限，保障数据安全。数据备份与恢复：提供数据库备份与导入导出恢复机制，满足生产环境需求。分部署管理：提供集群模式，能够自动管理多个数据库节点。列式存储和数据压缩列式存储和数据压缩，对于一款高性能数据库来说一个必不可少的特性。如果你想让查询变得更快，最简单有效的方法是减少数据扫描范围和数据传输时的大小，而列式存储可以帮助我们实现上述两点。列式存储和数据压缩通常是相伴而生的，一般来说列式存储是数据压缩的前提。列式存储相比行式存储，可以减少查询时所需扫描的数据量。例如一个用户表有 50 个字段，查询前 5 个字段，因为是行式存储，所以逐行扫描时实际会获取所有的字段，然后返回前五个。这样就意味着需要扫描磁盘更多空间，然后不返回这些数据。如果是按列获取数据，就不会存在这样的问题，直接扫描并返回前五列数据即可，避免了多余的数据扫描。按照列式存储相比按行式存储另一个优势就是对数据压缩的友好性。ClickHouse 的数据按列进行组织，属于同一列的数据会被保存在一起，而压缩的本质就是如此，同一列的数据类型和语义相同，数据具有更高比例的重复性，对应的压缩比也会也大。数据默认采用LZ4 压缩算法，在 Yandex.Metrica 的生产环境中，数据总体的压缩比可以达到 8:1 (由 17PB 压缩至 2PB)。列式存储除了降低 IO 和存储的压力之外，还为向量化执行做好了铺垫。HBase，BigTable 这些系统也能实现列式存储，但无法有效处理分析查询需求。在这些系统中，每秒可以实现数十万的数据吞吐量，但不会达到数亿行的级别。向量化执行引擎向量化执行是 CPU 寄存器硬件层面的特性，为上层应用带来指数级的提升。向量化执行可以看作是一项消除程序中循环结果的优化。为了实现向量化执行，需要利用 CPU 的 SIMD 指令（全称是 ingle Instruction Multiple Data 即 单条指令操作多条数据）。它是通过数据并行以提高性能的一种实现方式，原理是在 CPU 寄存器层面实现并行操作。具体实现原理参考：Clickhouse SIMD 使用初探 - 简书关系模型和 SQL 查询相比 Redis 和 HBase 这类 NoSQL 数据库，ClickHouse 使用关系模型描述数据并提供了传统数据库的概念（数据库、表、视图和函数等）。与此同时，ClickHouse 完全支持使用 SQL 作为查询语言，兼容大多数标准 SQL 。也正因为 ClickHouse 提供标准协议的 SQL 查询接口，是的现有的第三方系统可以轻松与它对接。在 SQL 解析方面，ClickHouse 是大小写敏感的。关系模型相比文档或键值对等其他模型，拥有更好的描述能力，也能够更加清晰的表述实体之间的关系。更重要的是，在 OLAP 领域，已有的大量数据建模工作都是基于关系模型展开的（星型模型、雪花模型或宽表模型）。ClickHouse 使用了关系模型，所以构建在传统关系型数据库或数据仓库之上的系统迁移到 ClickHouse 的成本会变得更低。多样化的表引擎因为 Yandex.Metrica 的最初架构是基于 MySQL 实现的，所以在 ClickHouse 的设计中，能够看到 MySQL 的影子，表引擎的设计就是其中之一。与 MySQL 类似， ClickHouse 也将存储部分进行了抽象，把存储引擎作为一层独立的接口。ClickHouse 拥有合并树、内存、文件、接口和其他 6 大类 20 多种表引擎，每个引擎都有各自的特点，用户需要根据实际的业务场景选择适合的表引擎。在 IT 技术和发展的长河中，并不会存在能够适应任何场景的通用系统，所以将表引擎独立设计的好处显而易见，通过特定的表引擎支撑特定的场景，可以直接使用简单的引擎降低成本，而复杂的场景也有合适的选择。多线程和分布式如果说向量化执行是通过数据级并行的方式提升了性能，那么多线程执行就是通过线程级并行的方式实现了性能提升。相比基于底层硬件实现的向量化执行 SIMD，线程级并行通常由更高层次软件层面控制。现在市面上的服务器早已普及多处理器架构，具有良好的多核心多线程处理能力。由于 SIMD 不适用于较多的分支判断场景，所以 ClickHouse 也大量使用了多线程技术进行提速，以和向量化执行形成互补。如果一台服务器性能吃紧，那么就要利用的多台服务器的资源协同处理。为了实现这一目标，首先要在数据层面实现数据的分布式。因为在分布式领域，有一条金科玉律：“计算移动比数据移动更划算”。在各服务器之间，使用网络传输数据的成本的高昂的，所以相比移动数据，更聪明的做法是预先将数据分布到各台服务器上，将数据的计算查询直接推送到数据所在的服务器。ClickHouse 在数据存取方面，既支持分区（纵向扩展，利用多线程原理），也支持分片（横向扩展，利用分布式原理）。多主架构HDFS、Spark、HBase 和 Elasticsearch 这类分布式系统，都采用了 Master - Slave 主从架构，让一个节点作为 Leader 掌控全局。而 ClickHouse 采用 Multi - Master 的多主架构，集群中的每个节点角色对等，客户端访问任意一个节点都能达到相同的结果。这种多主架构有许多优势，例如对等的角色使系统架构变的更加简单，不再区分主控节点、数据节点和计算节点，集群中所有节点的功能相同。正因如此，ClickHouse 天然规避了单点故障的问题，非常适用于多数据中心、异地多活等场景。在线查询ClickHouse 经常被拿来和其他的分析型数据库做对比，例如 Vertica、SparkSQL、Hive 和 Elasticsearch 等， ClickHouse 与这些数据库确实有相似之处，例如都可以支持海量数据的查询场景，都拥有分布式架构、列式存储、数据分配、计算下推等特性。这也侧面说明了 ClickHouse 在设计上确实吸取了各路的奇技淫巧。在具备这些功能的同时，ClickHouse 也拥有明显的优势。例如，Vertica 这类商业数据库价高昂；SparkSQL 与 Hive 这类系统无法保障 90% 的查询都在一秒内返回，在大数据量的情况下的复杂查询甚至需要分钟级的响应时间；而 ES 这类搜索引擎在处理过亿级别数据的聚合查询时显得捉襟见肘。其他的开源系统太慢，商用的系统太贵，而 ClickHouse 在成本和性能之间做了完美平衡。同时 ClickHouse 当之无愧的阐释了 "在线" 二字的含义，即便是在复杂的查询场景下，也能做到极快的响应，而无需对数据进行预处理加工。数据分片与分布式查询数据分片是将数据横向切分，这是一种面对海量数据的场景下，解决存储和查询瓶颈的有效手段，是一种分治思想的体现。ClickHouse 支持分片，而分片依赖集群。每个集群由一个到多个分片组成，而每个分片对应了 ClickHouse 的一个服务节点。分片数量的上限取决于节点数量（一个分片只能对应一个服务节点）。ClickHouse 并不像其他分布式系统那样，拥有高度自动化的分片功能。ClickHouse 提供了本地表（Local Table）和分布式表 （Distributed Table） 的概念。一张本地表扽沟通与一份数据的分片，而分布式表本身不存储任何数据，他只是本地表的代理访问，类似于其他分库中间件。借助分表能够代理访问多个数据分片，从而实现分布式查询。这种设计类似于数据库的分库分表，十分灵活。比如系统上线初期，数据量不高，只需要使用单台服务器（单个数据分片）即可满足需求，等到业务增长，数据量增大时，再通过新增分布式表的方式分流数据，并通过分布式表实现分布式查询。ClickHouse 为什么如此之快虽然前面解释了 ClickHouse 是列式存储并且用了向量化引擎所以快，但依旧不能解释所有的疑问。因为市面上由很多数据库同样使用着这些技术，但依然没有 ClickHouse 这么快，所以我们从另一个角度探讨一下 ClickHouse 的秘诀到底是什么。首先我们要抛出一个疑问：在设计软件架构时，做设计的原则应该是自上而下的去设计，还是自下而上的去设计呢？在传统的观念中，自然是自上而下的去设计，通常我们都被教导过要做好顶层设计。而 ClickHouse 的设计则是采用自下而上的方式。着手硬件，先想后做首先从硬件方面下手，需要思考如下的问题：我们使用的硬件水平如何？包含 cpu、硬盘、内存、网络等。在这样的硬件上，我们需要达到什么样的性能，包括吞吐、延迟等。我们准备使用怎样的数据结构，包含 String、HashTable、Vector 等。选择这些数据结构，在我们的硬件上会如何工作？基于将硬件功效最大化的目的，ClickHouse 会在内存中进行 GROUP BY，并使用 HashTable 来装在数据。同时，技术团队非常在一 CPU L3 级别的缓存，因为一次 L3 级别的缓存失效将会带来 70 ~ 100ns 的延迟。意味着在单核 CPU 上会浪费 4000万次/秒的运算；在 32 线程的 CPU 上，可能会浪费 5 亿次/秒的运算。别小看这些运算细节，一点一滴的累加起来，带来的数据是非常可观的。正因为 ClickHouse 在设计之初就考虑到了这些细节，所以才能再基准查询中做到 1.75 亿次/秒的数据扫描性能。算法在前，抽象在后ClickHouse 在底层的实现中，经常会面对一些重复的场景，例如字符串子串查询、数组排序、使用 HashTable 等。如何才能实现性能的最大化？算法的选择才是重中之重。例如在字符串搜索方面，针对不同的场景，ClickHouse 选择了这些算法：对于常量，使用 VoInitsky 算法；对于非常量，使用 CPU 的向量化执行 SIMD，暴力优化；正则匹配使用 re2 和 hyperscan 算法。性能是算法选择的重要考量指标。勇于尝鲜，不行就换除了字符串之外，其他的场景也类似，ClickHouse 会使用最合适、最快的算法。如果市面上出现了性能更强大的新算法，ClickHouse 团队会立即对其进行验证。如果效果不错，就会纳入 ClickHouse 中，并替换掉现有的算法。特定场景，特殊优化针对同一个场景的不同状况，使用不同的实现方式，尽可能的将性能最大化。签名介绍的字符串查询，针对不同的场景选择不同的算法就是这一思路的体现。还例如去重计数 uniqCombined 函数，会根据数据量的不同选择不同的算法：当数据量较小时，会使用 Array 保存；当数据量中等时，会使用 HashSet；当数据量很大时，则会使用 HyperLogLog 算法。对于数据结构比较清晰的场景，会通过代码生成技术展开循环，以减少循环次数。接着就是利用了向量化执行。SIMD 被更广泛的应用于文本转换、数据过滤、数据解压和 JSON 转换等场景。相较于单纯的使用CPU，利用寄存器暴力优化也算是一种降维打击了。持续测试，持续改进如果只是单纯在上述细节上下功夫，也不足以构建出强大的 ClickHouse，还需要拥有一个能够持续验证和改进的机制。这也是 Yandex 的天然优势，ClickHouse 经常会使用真实的数据进行测试，这一点很好的保证了测试数据的真实性，与此同时，ClickHouse 的发版节奏也是非常快的，差不多每个月都有个新版本。小结本篇文章分析了 ClickHouse 的核心特性，通过核心特性的部分展示，ClickHouse 如此强悍的缘由已初见端倪，列式存储、向量化引擎和表引擎都是它的杀手锏。
如果要说明白为什么需要在业务中引入 ClickHouse ，我们就需要从BI系统的发展开始说起。传统 BI 系统随着互联网技术的不断发展，越来越多的企业通过 ERP、CRM 等系统将自己的业务进行数字化管理，我们通常将这类系统称为“联机事务处理 (OLTP) 系统”。在企业的生产经营过程中，并不是只关注流程审批、数据录入这些工作。站在监管和决策层面，还需要另一种分析类的视角，例如分析报表、分析决策等。而 IT 系统的早期建设过程一般都缺少与其他系统互相配合的规划，数据散落在各个独立的系统中，互不相通。为了解决数据孤岛的问题，人们提出了数据仓库的概念，通过引入一个专门用于分析类场景的数据库，将分散的数据汇总在一起。随着数据库仓库的发展，专注于提供数据分析、决策类功能的系统于解决方案应运而生。最终在二十世纪九十年代，第一次有人提出了 BI （商业智能）系统的概念。自此之后，人们通常用 BI 一次指代这类分析系统。相对于“联机事务处理系统”，我们把这类 BI 系统称为 “联机分析 (OLAP) 系统”传统 BI 系统的设计初衷虽好，但实际应用效果却不令人满意，主要原因有如下几点：传统 BI 系统对企业的信息化水平要求较高。按照传统的 BI 设计思路，只有中大型企业才有能力实施。因为它的定位是站在企业视角通盘分析并辅助决策，所以如果企业的信息化水平不高，基础设施不完善，想要实施 BI 根本无从谈起。狭小的受众制约了传统 BI 系统发展的生命力。传统 BI 系统的主要受众是企业中的管理层或决策层。这类用户通常有着较高的话语权和决策权，但用户基数太小，同时他们对系统的参与度和使用度不高，久而久之这类所谓的 BI 系统就沦为了领导视察时用于演示的特供系统。研发过程滞后了需求的响应时效，传统 BI 不够灵活，有任何需求都想要大量的开发人员参与，一个分析需求的产生到最终实现，可能想要数周或者几个月的时间。现代 BI 系统SaaS 模式的兴起，为传统企业软件系统的商业模式带来了新思路，是一次新的技术普惠。一方面 SaaS 模式将之前只服务于大型企业的软件系统放到了互联网，扩展了它的受众；另一方面，由于互联网用户的基本特征和软件诉求，加上同类系统的互相竞争，又倒逼了这些软件系统在方方面面进行革新于升级。而这次的技术普惠，同样也影响了 BI 系统在设计思路上的变革。随着时代的发展，BI 系统的受众变的更加多元化，几乎所有人都可以成为数据分析师。现代的 BI 系统不在需要研发人员深度参与，用户直接通过自助的形式，通过简单拖拽、搜索就能得到自己想要的分析结果。经过互联网的系列，即使现代 BI 系统仍私有化部署在企业内部，只服务于企业用户，但它也必须具有快速应答，简单易用的使用体验，从某种角度来看，互联网浪潮帮助企业系统在易用性和用户体验上进行了革命性的提升。如果说 SaaS 化为现代 BI 系统提供了新的思路与契机，那么背后的技术创新则保障了其思想的落地。在传统 BI 系统的架构体系中，数据存储使用的是传统的关系型数据库（OLTP 数据库）。为了能够解决在海量数据下分析查询的性能问题，在数据库仓库的基础上衍生了众多新的概念：数据分层：通过对对数据进行分层，通过层层递进形成数据集市，从而减少最终查询到数据体量。数据立方体：通过对数据进行预先处理，以空间换时间，提高查询性能。然而，无论如何努力，数据库设计的局限性始终是无法突破的瓶颈，OLTP 技术诞生的那一刻起，就注定不是为了数据分析而生的，于是很多人将目光投向了新的研究方向。随着 Google 陆续发表的三篇论文开启了大数据时代的技术普惠，Hadoop 生态由此刻开始一发不可收拾，数据分析开启了新纪元。从某种角度来看，使用 Hadoop 生态为代表的这类非传统关系型数据库技术所实现的 BI 系统，可以被称之为现代 BI 系统。换装了大马力发动机的现代 BI 系统在面对海量数据分析时的场景时，显得更加游刃有余。然而 Hadoop 技术也不是银弹，在现代 BI 系统的构建中仍面临诸多挑战，在海量数据下要实现多维度分析的实时应答，仍困难重重。::: alert-info现代 BI 系统的典型应用场景是多维分析，某些时候可以直接使用 OLAP 指代这类场景:::OLAP 常见架构分类OLAP 技术发展至今，分析型数据库百花齐放。然而，看似手上拥有很多筹码的架构师们，有时却面临无牌可打的窘境，为什么会这样？要理解这个问题，我们需要重温一下什么是 OLAP，以及实现 OLAP 的几种思路。之前说过，OLAP 为联机分析，又称多维分析，是由数据库关系之父 埃德加 ·科德 于 1993 年提出的概念。顾名思义，它是通过多种不同的维度审视数据，进行深层次的分析。维度可以看作是观察数据的一种视角，例如人类能看到的世界是三维的，包含长宽高三个维度（不考虑时间维度）。直接地说，维度就好比是一张数据表的字段，而多维分析就是针对这些字段进行聚合查询。那么多维分析通常都包含哪些基本操作？为了更好地理解多维分析的概念，可以使用一个立方体的图像具象化操作。例如，对于张销售明细表，数据立方体可以进行如下操作。下钻 从高层次向低层次明细数据穿透。例如从省下钻到市，从湖北省穿透到武汉和宜昌。上卷 和下钻相反，从低层次向高层次汇聚，例如从市汇聚成省，将武汉和宜昌汇聚成湖北。切片 观察立方体的一层，将一个或多个维度设为单个固定值，然后观察剩余的维度，例如将商品维度固定为足球。切块 与切片类似，只是将单个固定值变成多个固定值。例如将商品的维度设置为足球、篮球和乒乓球。选择 旋转立方体的一面，如果要将数据映射到一张二维表，那么就要进行旋转，也就是行列置换。为了使能实现上面的操作，常见的 OLAP 架构大致分为三大类:ROLAP第一类架构称为 ROLAP （Relational OLAP 关系型 OLAP），正如这个名字所说，它是直接使用关系模型构建，数据模型通常是星型模型或雪花模型。这是最先能想到，也是最为直接的实现方法。因为 OLAP 概念在最初提出的时候，是建立在关系型数据库技术之上，多维分析操作可以直接转换为 SQL 查询。例如通过上卷操作查看省份销售额，可以通过下面的SQL实现：SELECT SUM(price) FROM selling_price GROUP BY province但是这样的架构对数据的实时处理能力要求很高，如果单张表数据达到上一条，分组字段达到数十个字段，那么不但执行查询的性能消耗是巨大的，而且查询所需的时间消耗也会很长。MOLAP第二类架构称为 MOLAP （Multidimensional OLAP 多维型 OLAP），它的出现主要是为了缓解 ROLAP 性能问题。MOLAP 使用的是多维数组的形式保存数据，也就是我们所说的空间换时间。具体的实现方法就是先确定维度字段， 过预处理的方式对多维数据进行聚合运算，然后将聚合结果缓存起来。这样一来在后续的查询过程中可以直接返回预先缓存的数据，但这样并不完美，多维度的预处理可能会导致缓存数据量的膨胀。以前面的销售明细表为例，如果数据立方体包含了五个维度的数据，那么维度组合的方式有 2^5^ (2^n^, 其中 n 代表维度个数) 个。当一张数据量为千万级的数据表，聚合后的数据量可以膨胀到亿级别。人们意识到这个问题之后，也实现了一些能够降低数据膨胀率的优化手段，但治标不治本。另外，预处理的方式会造成数据立方体有一定的滞后性，不能进行实时的数据分析。而且数据立方体只保留了聚合后的查询数据，不能查询明细数据。HOLAP第三类架构称为 HOLAP （Hybrid OLAP 混合架构 OLAP）。这个架构的思路是 ROLAP 和 MOLAP 的融合版，将两者的优点进行互补。对于查询频繁而耗时的SQL，通过预计算来提速。对于查询较快，发生次数较少或查询条件不固定的需求，像 ROLAP 一样操作物理表和维度表。OLAP 实现技术的演进在了解完 OLAP 的几种架构后，再看看这些架构背后技术的演进过程，我们可以把这些过程分为两个阶段。第一阶段可以称为关系型数据库阶段，在这个阶段中，OLAP 主要是基于 Oracle、MySQL 为代表的一系列关系型数据实现。在 ROLAP 架构下，直接使用这些数据库作为存储与计算的载体。在 MOLAP 架构下，则借助物化视图的形式实现数据立方体。在这个发展阶段，无论是 ROLAP 还是 MOLAP 在数据体量巨大、维度众多的情况下都存在严重性能问题。第二阶段可以称为大数据技术阶段，由于大数据处理技术的普及，人们开始使用大数据技术重构 ROLAP 和 MOLAP。以 ROLAP 为例，传统关系型数据库被 Hive 和 SparkSQL 这类新兴技术所取代。虽然以 SparkSQL 为代表的分布式计算系统，相比 Oracle、MySQL 这类传统数据库系统来说，在海量数据处理方面已经优秀了很多，但面向终端用户的查询需求还是太慢。互联网用户普遍缺乏耐心，如果一个查询请求需要几十秒或数分钟才能响应，依然不能满足需求。再看 MOALP 架构，MOLAP 的背后技术也转为依托 MapReduce 或 Spark 这类新兴技术，将其作为立方体计算引擎，加速立方体的构建过程。其聚合结果的存储载体也转为 HBase 这类高性能分布式数据库系统。在大数据阶段，主流的 MOLAP 架构已经能够在亿万级数据的体量下，实现毫秒级的查询响应。尽管如此，MOLAP 依然存在维度爆炸、数据同步实施性不高的问题。不难发现，虽然 OLAP 在经历大数据的洗礼后，在其各方面性能有了脱胎换骨的改观，但不论是ROLAP 还是 MOLAP，仍然存在各自的痛点。如果单从模型角度考虑，很显然 ROLAP 架构更胜一筹。因为 ROLAP 模型拥有更好的“群众基础”，也更简单且容易理解。它可以直接面向明细数据查询，由于不需要预处理自然也没有维度组合爆炸、数据实时性、额外的更新逻辑之类的繁琐问题。那么有没有一种技术，既可以使用 ROLAP 模型，又拥有比肩 MOLAP 的性能呢。横空出世的黑马上文提到，以 Spark 为代表的新一代 ROLAP 方案虽然可以一站式处理海量数据，但无法真正做到实时应答和高并发，更适合作为一个后端的查询系统。而新一代 MOLAP 方案虽然解决了大部分查询性能的问题，能做到实时应答，但数据膨胀和预处理的问题并没有被解决。除了以上两种方案，还有一种另辟蹊径的选择，既抛弃 ROLAP 和 MOLAP 转而使用搜索引擎来实现 OLAP 查询，ElasticSearch 就是这类方案的代表。ElasticSearch 支持实时更新，在百万级别数据的场景下可以做到实时的聚合查询，但随着数据量的持续增加，它的查询性能也开始捉襟见肘。面对上面提到的种种问题，也就引出了书讨论的主角。天下武功 唯快不破Clickhouse 具有 ROLAP、在线实时查询、完整的DBMS、列式存储、不需要任何数据预处理、支持批量写入/更新、拥有非常完善的 SQL 语法支持和函数、支持高可用，不依赖 Hadoop 复杂生态、部署方便和开箱即用等许多特点。特别是 ClickHouse 那离谱的查询性能，相信大多数刚接触 ClickHouse 的开发者一定会因为它的性能指标而动容。在官方公布的基准测试对比中，ClickHouse 都是遥遥领先对手，而参与对比的数据库，不乏一些我们耳熟能详的名字。所有对比的数据库都使用了相同的服务器，在单机部署的场景下，对一张拥有 133 个字段的数据库表分别在 1000 万、1 亿、10 亿 三种数据体量下执行基准测试，基准测试范围覆盖 43 项 SQL 查询。ClickHouse 的平均响应速度是 MySQL 的 751 倍、MonetDB 的 3.28 倍、SQLite 的 251 倍、MongoDB 的 116 倍。详细测试结果可以查询 https://benchmark.clickhouse.com，需要注意的是因版本不断更新的原因，官方的测试结果可能会随之更新，测试结果可能与本文会有所不同。活跃的社区ClickHouse 是一款开源软件，遵循 Apache License 2.0 协议。同时 ClickHouse 开源项目在 Github 上也十分活跃，截至目前（2023-1-17）已经拥有超过1100 位贡献者，发布版本达到 425 个。惊人的更新频率、友好的开源协议、活跃的社区、积极的响应，这意味着我们可以及时获取最新特性并且遇到问题时也能得到修复缺陷的补丁。ClickHouse 的发展ClickHouse 的研发团队是来自于俄罗斯的 Yandex 公司，这家公司的核心产品是搜索引擎，目前已占据了 俄罗斯 53% 的的搜索市场，是目前世界上最大的俄语搜索引擎。搜索引擎的营收非常依赖流量和在线广告业务。所以通常搜索引擎公司为了更好的帮助自身以及用户分析网站流量，都会推出自家的在线流量分析产品，例如谷歌的 Google Analytics、百度的百度统计。而 Yandex 也拥有自家的在线流量分析产品 Yandex.Metrica。ClickHouse 就是在这样的产品背景下诞生的，伴随着 Yandex.Metrica 业务的发展，其底层架构经历了四个阶段，一步一步形成了目前大家所看到的 ClickHouse。纵观这四个阶段的发展，俨然是数据分析产品形态以及 OLAP 架构历史演进的缩影。通过了解这段演进过程，我们能够更加透彻地了解 OLAP 面对的挑战，以及 ClickHouse 能够解决的问题。MySQL 时期从技术发展的角度来看，当时还处于关系型数据库的霸主时期，所以 Yandex 在其他的内部产品中使用了 MySQL 数据库作为统计信息系统的底层存储方案。Yandex.Metrica 的第一版架构就顺理成章地延续了这套成熟稳定的 MySQL 方案，并将其作为数据存储和分析引擎的解决方案。Yandex.Metrica 的表引擎为 MyISAM 引擎，这类分析场景更关注数据的写入和查询性能，不关注事务。相比 InnoDB 表引擎， MyISAM 表引擎在分析场景中具有更好的性能。业内有一个常识性的认知，按照顺序存储的数据会拥有更高的查询性能，因为读取顺序文件会减少磁盘寻址时间。同时顺序读取也能充分利用操作系统层面文件缓存的预读功能。所以数据库查询时如果按照顺序读取，会比随机读取的查询性能会高很多。但 Yandex 内部的 MySQL 方案却无法做到顺序存储。MyISAM 表引擎使用的是 B + Tree 结构存储索引，而数据则是使用单独的存储文件。如果只考虑单线程写入场景，并且写入过程中不涉及数据删除或更新操作，那么数据会按照写入顺序一次被写入磁盘，然而实际的业务场景不可能如此简单。流量数据采集的链路是这样的，Web 端的应用通过 Yandex 提供的站点 SDK 实时采集数据并发送到远端的接收系统，再由接收系统将数据写入 MySQL 集群。由于数据接收系统的个分布式系统，所以他们会并行、随机将数据写入 MySQL 集群。这也导致了数据最终在磁盘里是完全随机存储的，并且会产生大量的磁盘碎片。市面上一块 7200 RPM SATA 磁盘的 IOPS (每秒能处理请求数量) 仅为 100 左右，也就是每秒只能执行 100 次随机读取。假设一次随机读取返回十行数据，那么查询 10 万行记录则需要至少 100 秒，这样的响应时间显然是不可接受的。虽然可以通过 RAID 提高磁盘的 IOPS 性能，但并不能解决根本问题，SSD 随机读取性能很高，但考虑到硬件成本和集群规模，不可能全部采用 SSD 存储。随着时间的推移，MySQL 中存储的数据越来越多（截止 2011 年，存储数据已超过 5800 亿行），虽然 Yandex 又额外做了很多优化，成功的将 90% 的分析报告控制在26秒内返回，但这套方案越来越显得力不从心。Metrage 时期由于 MySQL 带来的局限性，Yandex 自研了一套全新的系统并命名为 Metrage。Metrage 在设计上与 MySQL 完全不同，他选择了另一条完全不同的道路：在模型层面，它使用了 Key-Value 模型代替了关系模型；在索引层面，他使用了 LSM 代替了 B+ 树；在数据处理层面，由实时查询的方式改为了预处理；LSM Tree 是一种非常流行的索引结构，发源于 Google 的 Big Table，现在最具有代表性使用 LSM Tree 索引结构系统的是 HBase。LSM 本质上可以看作是将原本的一颗大树拆成了许多棵小树，每一批次写入的数据都会经过如下的过程：首先会在内存中构建一棵小树，构建完毕即算写入成功。写入动作只发生在内存中，不涉及磁盘操作，所以极大地提升了写入的性能。其次小树在构建的过程中会进行排序，这样保证了数据的有序性。最后当内存中的小树的数量大达到某个阈值时，跳过后台线程将小树刷入磁盘，并生成一个小的数据段。在每个数据段中，数据局部有序。也正是因为数据有序，所以能够使用稀疏索引来优化查询性能。借助 LSM Tree 索引，可以让 Metrage 引擎在软硬件层面同时得到优化（磁盘顺序读取、预读缓存、稀疏索引等），最终有效提高系统的综合性能。在索引结构优化的基础上，还是不能解决根本的性能问题。Metrage 的第二个重大转变就是通过预处理的方式将数据进行预先聚合。按照数据立方体的思想计算所有维度组合，最后将聚合的结果按照 Key-Value 的形式存储。这样一来，对于固定分析场景，可以直接利用数据立方体的聚合结果立即返回相关数据。这套系统的实现思路与现如今的一些 MOLAP 系统如出一辙。经过一系列的转变，Metrage 为 Yandex.Metrica 的性能带来了革命性地提升。截至 2015 年，Metrage 内部存储超过了3 万亿行数据，其集群规模超过了 60 台服务器，查询耗时也由之前的 26 秒下降到 1 秒内。然而，使用立方体的思路会产生一个新的问题，就是维度组合爆炸，因为需要预先对所有的维度组合进行计算，这是一种指数级的增长方式，维度组合的爆炸会直接导致数据膨胀。OLAPServer 时期在 Yandex.Metrica 的产品初期，它只支持固定报表的分析功能。随着时间的推移，这种固定化的分析形式早已不能满足用户的诉求，于是 Yandex.Metrica 计划推出自定义分析报告的功能。然而 Metrage 系统却无法满足这类自定义的分析场景，因为它需要预先聚合，并且只提供了内置的 40 多种固定分析场景。单独为每个用户提供个人的预聚合功能显然是不切实际的。在这种背景下，Yandex.Metrica 研发团队只能寻求自我突破，于是研发了 OLAPServer 系统。OLAPServer 系统被设计成了专门处理自定义报告这样临时性分析需求的系统，与 Metrage 系统形成互补关系。结合之前两个阶段的建设经验，OLAPServer 在设计思路是取众家之所长。数据模型方面又换回了关系模型，因为相比 Key-Value 模型来说，关系模型拥有更好的描述能力，使用 SQL 作为查询语言，也会拥有更好的“群众基础”。而在存储结构和索引方面，OLAPServer 吸收了 MyISAM 和 LSM Tree 最精华的部分。存储结构，它与 MyISAM 表引擎类似，分别为索引文件和数据文件两个部分。索引方面，它并没有完全沿用 LSM Tree，而是使用了 LSM Tree 所使用的稀疏索引。数据文件，在数据文件的设计上，沿用了 LSM Tree 中数据段的思想，即数据段内数据有序，借助稀疏索引定位数据段。在拥有上述基础后，OLAPServer 引入了列式存储的思想，将索引文件按照列字段的颗粒度进行拆分，每个列字段各自独立存储，以进一步减少数据读取的范围。虽然 OLAPServer 在实时聚合方面性能相比 MySQL 有了质的飞跃，但从功能完备的角度来说，还是差了一个量级。因此 OLAPServer 的定位只是和 Metrage 形成互补，所以它确实列一些基础的功能，例如 它只支持定长的 INT 类型，且没有 DBMS 应有的基础管理功能。ClickHouse 时代在初步满足需求后，Yandex.Metrica 又面临了新的选择题，实时聚合还是预先聚合？实时聚合的方案在查询性能方面带来了质的提升，成功的将查询报告所需的时间从 26 秒降低到 1 秒内，但又带来了新的问题。预先聚合只能支持固定的分析场景，所以无法满足自定义需求的分析。维度组合爆炸会导致数据膨胀，会造成不必要的计算和存储开销。因为用户不一定会用到所有维度的组合，那些用到的组合将成为浪费的开销。流量数据是在线实时接收的，所以预聚合还需要考虑如何及时地更新数据。所以，如果查询性能可以得到保障，实时聚合会是一个更为简洁的架构。由于 OLAPServer 的成功经验，选择倾向于实时聚合这一方。OLAPServer 的查询性能并不比 Metrage 差太多，在查询的灵活性方面反而更胜一筹。于是 Yandex.Metrica 的研发团队以 OLAPServer 为基础进一步完善，以实现一个完备的数据库管理系统（DBMS）为目标 ，最终打造出了 ClickHouse，并于 2016 年开源。ClickHouse 名称的含义由于 Yandex.Metrica 是一款 Web 流量分析工具，采集用户在网页上的点击行为数据，然后进行一系列的数据分析，类似于数据库仓库的 OLAP 分析。所以 ClickHouse 的全称是 Click Stream, Data WareHouse , 简称为 ClickHouse。ClickHouse 适用场景因为 ClickHouse 在诞生之初就是为了服务自家的 Web 流量分析产品 Yandex.Metrica, 所以在存储数据量超过 20 万亿行的情况下，ClickHouse 做到了 90% 的查询都能在 1 秒内响应结果。随后，ClickHouse 进一步被应用到 Yandex 内部大大小小数十个其他的分析场景中。可以说 ClickHouse 是具备了人们对一款高性能 OLAP 数据库的向往。所以它基本能胜任各种数据分析类场景，并随着数据体量的增大，它的优势也会变的越为明显。ClickHouse 非常适用于商业智能领域（也就是 BI 领域），除此之外，他能够被广泛应用于广告流量、Web、App流量、电信、金融、电子商务、信息安全、网络游戏、物联网等众多其他领域。ClickHouse 不适用场景ClickHouse 作为一款高性能 OLAP 数据库，虽然足够优秀，但也不是万能的，我们不应该把它用于任何 OLTP 事务性操作场景，因为它有以下几点不足：不支持事务不擅长根据主键按行粒度进行查询（虽然支持），故不应该把 ClickHouse 当作 Key-Value 数据库使用。不擅长按行删除数据（虽然支持）虽然 ClickHouse 数据库有上面这些缺点，但事实上 其他同类高性能的 OLAP 数据库同样也不擅长上述的这些方面。因为对于一款 OLAP 数据库来说，上述的这些能力并不是重点，所以为了极致的查询性能做出权衡。都有谁在使用除了 Yandex 自己将 ClickHouse 用来计算广告收益和流量分析统计外，还被众多商业公司运用到了生产环境。CENRN（欧洲核子研究中心）将其用于保存强对撞机试验后记录下的数十亿时间的测试数据，并将数据查找时间由几个小时缩短到几秒。Mail.ru 使用 ClickHouse 作为其网络游戏平台的数据分析引擎，用于实时统计玩家行为和游戏数据。Uber 使用 ClickHouse 作为其运营数据分析平台，用于实时监控和优化其网约车业务。Samsung 使用 ClickHouse 作为其物联网平台的数据分析引擎，用于统计设备数据和监控设备状态。Wargaming 使用 ClickHouse 作为其多人在线游戏平台的数据分析引擎，用于实时统计玩家行为和游戏数据。阿里巴巴集团在其电商、金融、物流等业务中使用 ClickHouse 来实现实时大数据分析。腾讯在其社交、游戏、广告等业务中使用 ClickHouse 来实现实时大数据分析。百度在搜索、广告等业务中使用 ClickHouse 来实现实时大数据分析。字节跳动在其新闻、短视频等业务中使用 ClickHouse 来实现实时大数据分析。哔哩哔哩、永辉超市、京东云、网易、新浪等国内的互联网公司也都有应用。笔记来自：《ClickHouse 原理解析与应用实践》第一章 ClickHouse 的前世今生