Map类型,存储k-v
键值对,含义类似Java中的Map。主要分为隐式列 Map(默认类型,也称为 ByteMap)和 KV Map,二者主要区别在于数据存储的方式,隐式列 Map 会将每个 key 对应的所有 value 单独存为一个隐式列,KV Map 则类似于 Array(Tuple(K, V)),会将出现的 key 和 value 统一存储。
Map key类型允许为 Integers
(包括 Int8
、UInt64
等), Floats32
, Float64
, Date
, DateTime
, String
, FixedString
, LowCardinality
, UUID
, Enums
,不支持为 Nullable
。
KV Map 的 value 类型没有限制,隐式列 Map value 类型允许为 Integers
(包括 Int8
、UInt64
等), Floats32
, Float64
, Date
, DateTime
, String
, FixedString
, LowCardinality(Nullable())
, Array
, 不支持最外层为Nullable
或者包含 Tuple
的类型。
定义时在类型后增加 KV
后缀可以使用 KV Map,否则为隐式列 Map。例如 Map(Int16, String)
, Map(String, Array(Int32)) KV
, Map(String, LowCardinality(Nullable(String)))
。
SELECT m{'k1'} FROM xx
,查询使用花括号 {}
(等价于 mapElement
函数)可以有较好的性能;SELECT m FROM xx
;{"a":1,"b":2,"d":3}
,查询时结果为{"d":3, "a":1, "b":2}
。在创建表时,可以指定列的数据类型为 Map
。
CREATE TABLE example_map_table ( id UInt64, m1 Map(String, String), -- 隐式列 Map m2 Map(String, String) KV -- KV Map ) ENGINE = CnchMergeTree() ORDER BY id;
插入数据时,可以使用键值对的格式或者在 SELECT 中使用 map
函数。
-- INSERT INTO xx VALUES,使用类似 JSON 的格式(替换为单引号) INSERT INTO example_map_table (id, m1, m2) VALUES (1, {'name': 'Alice', 'age': '30'}, {'name': 'Bob', 'age': '25'}); -- INSERT INTO xx SELECT,可以使用 map 函数来生成 Map 类型数据 INSERT INTO example_map_table (id, m1, m2) SELECT 2, map('name', 'Alice', 'age', '31'), map('name', 'Bob', 'age', '26');
使用 Map 提供的函数可以方便地查询和处理 Map 数据。
查询 Map 数据中的特定 Key
-- 1. 使用 {} 或者 mapElement 函数 -- 返回值类型为 Nullable(ValueType),key 不存在时返回 Null -- 查询隐式列 Map 时能够只读取对应隐式列的数据,有较好的性能 SELECT id, m1{'name'} AS m1_name, toTypeName(m1_name) AS m1_name_type, mapElement(m2, 'city') AS m2_city, toTypeName(m2_city) AS m2_city_type FROM example_map_table; -- result: -- 1 'Alice' 'Nullable(String)' NULL 'Nullable(String)' -- 2 'Alice' 'Nullable(String)' NULL 'Nullable(String)' -- 1. 使用 [] 或者 arrayElement 函数 -- 返回值类型为 ValueType,key 不存在时返回默认值 -- 查询隐式列 Map 时需要先读出整个 Map,性能比较差 SELECT id, m1['name'] AS m1_name, toTypeName(m1_name) AS m1_name_type, arrayElement(m2, 'city') AS m2_city, toTypeName(m2_city) AS m2_city_type FROM example_map_table; -- 1 'Alice' 'String' '' 'String' -- 2 'Alice' 'String' '' 'String'
查询整个 Map 列
-- 隐式列 Map 若 key 数量较多时此类查询性能较差,可以考虑使用 KV Map SELECT id, m1, m2 FROM example_map_table; -- result: -- 1 {'age':'30','name':'Alice'} {'name':'Bob','age':'25'} -- 2 {'age':'31','name':'Alice'} {'name':'Bob','age':'26'}
使用 Map Key(对应 Value)作为筛选条件
SELECT id, m1{'age'} FROM example_map_table WHERE m1{'name'} = 'Alice' AND m2{'age'} = '26'; -- result: -- 2 31
在某些应用场景中,不同的记录可能具有不同的属性。MAP 数据类型可以方便地存储这些动态属性。
CREATE TABLE dynamic_attributes ( entity_id UInt64, attributes Map(String, String) ) ENGINE = CnchMergeTree() ORDER BY entity_id; INSERT INTO dynamic_attributes (entity_id, attributes) VALUES (1, {'color': 'red', 'size': 'M')), (2, {'brand': 'Nike', 'material': 'cotton'));
我们可以查询所有具有特定属性的记录:
SELECT entity_id, attributes FROM dynamic_attributes WHERE attributes{'color'} = 'red';
ARRAYT
类型元素组成的数组。T
可以是任意类型,包含数组类型。 但不推荐使用多维数组,ByteHouse 对多维数组的支持有限。例如,不能存储在 MergeTree
表中存储多维数组。
您可以使用array函数来创建数组:
array(T)
示例:
SELECT array(1, 2) AS x, toTypeName(x)
┌─x─────┬─toTypeName(array(1, 2))─┐ │ [1,2] │ Array(UInt8) │ └───────┴─────────────────────────┘
SELECT [1, 2] AS x, toTypeName(x)
┌─x─────┬─toTypeName([1, 2])─┐ │ [1,2] │ Array(UInt8) │ └───────┴────────────────────┘
ByteHouse会自动检测数组元素,并根据元素计算出存储这些元素最小的数据类型。如果在元素中存在 NULL 或存在 可为空 类型元素,那么数组的元素类型将会变成 可为空。
如果 ByteHouse 无法确定数据类型,它将产生异常。当尝试同时创建一个包含字符串和数字的数组时会发生这种情况 (SELECT array(1, 'a'))。
自动数据类型检测示例:
SELECT array(1, 2, NULL) AS x, toTypeName(x)
┌─x──────────┬─toTypeName(array(1, 2, NULL))─┐ │ [1,2,NULL] │ Array(Nullable(UInt8)) │ └────────────┴───────────────────────────────┘
如果您尝试创建不兼容的数据类型数组,ByteHouse 将引发异常:
SELECT array(1, 'a')
Code: 386, Error: DB::Exception: There is no supertype for types There is no supertype for types UInt8, String because some of them are String/FixedString and some of them are not SQLSTATE: HY000
可以使用 size0
子列找到数组的大小,而无需读取整个列。对于多维数组,您可以使用 sizeN-1
,其中 N
是所需的维度。
示例
SQL查询:
CREATE TABLE t_arr (`arr` Array(Array(Array(UInt32)))) ENGINE = CnchMergeTree 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]] │ └───────────┴───────────┴───────────┘
说明
元组 的每个元素都有单独的 类型。
不能在表中存储元组(除了内存表)。它们可以用于临时列分组。在查询中,IN 表达式和带特定参数的 lambda 函数可以来对临时列进行分组。
元组可以是查询的结果。在这种情况下,对于JSON以外的文本格式,括号中的值是逗号分隔的。在JSON格式中,元组作为数组输出(在方括号中)。
可以使用函数来创建元组:
tuple(T1, T2, ...)
创建元组的示例:
SELECT tuple(1,'a') AS x, toTypeName(x)
┌─x───────┬─toTypeName(tuple(1, 'a'))─┐ │ (1,'a') │ Tuple(UInt8, String) │ └─────────┴───────────────────────────┘
在动态创建元组时,ByteHouse 会自动为元组的每一个参数赋予最小可表达的类型。如果参数为 NULL,那这个元组对应元素是 可为空。
自动数据类型检测示例:
SELECT tuple(1, NULL) AS x, toTypeName(x)
┌─x────────┬─toTypeName(tuple(1, NULL))──────┐ │ (1,NULL) │ Tuple(UInt8, Nullable(Nothing)) │ └──────────┴─────────────────────────────────┘
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于阅读和编写。ByteHouse 支持 JSON 数据类型,使得用户能够直接在数据库中存储和查询 JSON 格式的数据。
要在 ByteHouse 中使用 JSON 数据类型,可以在创建表时指定列的数据类型为 JSON
。
CREATE TABLE example_json_table ( id UInt64, data JSON ) ENGINE = CnchMergeTree() ORDER BY id;
可以直接插入 JSON 格式的数据。
INSERT INTO example_json_table (id, data) VALUES (1, '{"name": "Alice", "age": 30, "city": "New York"}'), (2, '{"name": "Bob", "age": 25, "city": "San Francisco"}');
使用 JSON 提供的函数可以方便地查询和处理 JSON 数据。
-- 查询 JSON 数据中的特定字段 SELECT id, JSONExtractString(data, 'name') AS name, JSONExtractInt(data, 'age') AS age FROM example_json_table; -- 查询符合特定条件的 JSON 数据 SELECT id, data FROM example_json_table WHERE JSONExtractString(data, 'city') = 'New York';
包括 Enum8
和 Enum16
类型。Enum
保存 'string'= integer
的对应关系。在 ClickHouse 中,尽管用户使用的是字符串常量,但所有含有 Enum
数据类型的操作都是按照包含整数的值来执行。这在性能方面比使用 String
数据类型更有效。
Enum8
用 'String'= Int8
对描述。Enum16
用 'String'= Int16
对描述。创建一个带有一个枚举 Enum8('hello' = 1, 'world' = 2)
类型的列:
CREATE TABLE t_enum ( x Enum8('hello' = 1, 'world' = 2) ) ENGINE = TinyLog
这个 x
列只能存储类型定义中列出的值:'hello'
或'world'
。如果您尝试保存任何其他值,ClickHouse 抛出异常。
INSERT INTO t_enum VALUES ('hello'), ('world'), ('hello')
Ok. 3 rows in set. Elapsed: 0.002 sec.
INSERT INTO t_enum VALUES('a')
Exception on client: Code: 49. DB::Exception: Unknown element 'a' for type Enum8('hello' = 1, 'world' = 2)
当您从表中查询数据时,ClickHouse 从 Enum
中输出字符串值。
SELECT * FROM t_enum
┌─x─────┐ │ hello │ │ world │ │ hello │ └───────┘
如果需要看到对应行的数值,则必须将 Enum
值转换为整数类型。
SELECT CAST(x, 'Int8') FROM t_enum
┌─CAST(x, 'Int8')─┐ │ 1 │ │ 2 │ │ 1 │ └─────────────────┘
在查询中创建枚举值,您还需要使用 CAST
。
SELECT toTypeName(CAST('a', 'Enum8(\'a\' = 1, \'b\' = 2)'))
┌─toTypeName(CAST('a', 'Enum8(\'a\' = 1, \'b\' = 2)'))─┐ │ Enum8('a' = 1, 'b' = 2) │ └──────────────────────────────────────────────────────┘
Enum8
类型的每个值范围是 -128 ... 127
,Enum16
类型的每个值范围是 -32768 ... 32767
。所有的字符串或者数字都必须是不一样的。允许存在空字符串。如果某个 Enum 类型被指定了(在表定义的时候),数字可以是任意顺序。然而,顺序并不重要。Enum
中的字符串和数值都不能是 NULL。Enum
包含在 可为空 类型中。因此,如果您使用此查询创建一个表
CREATE TABLE t_enum_nullable ( x Nullable( Enum8('hello' = 1, 'world' = 2) ) ) ENGINE = TinyLog
不仅可以存储 'hello'
和 'world'
,还可以存储 NULL
。
INSERT INTO t_enum_nullable Values('hello'),('world'),(NULL)
在内存中,Enum
列的存储方式与相应数值的 Int8
或 Int16
相同。
当以文本方式读取的时候,ClickHouse 将值解析成字符串然后去枚举值的集合中搜索对应字符串。如果没有找到,会抛出异常。当读取文本格式的时候,会根据读取到的字符串去找对应的数值。如果没有找到,会抛出异常。
当以文本形式写入时,ClickHouse 将值解析成字符串写入。如果列数据包含垃圾数据(不是来自有效集合的数字),则抛出异常。Enum 类型以二进制读取和写入的方式与 Int8
和 Int16
类型一样的。
隐式默认值是数值最小的值。
在 ORDER BY
,GROUP BY
,IN
,DISTINCT
等等中,Enum 的行为与相应的数字相同。例如,按数字排序。对于等式运算符和比较运算符,Enum 的工作机制与它们在底层数值上的工作机制相同。
枚举值不能与数字进行比较。枚举可以与常量字符串进行比较。如果与之比较的字符串不是有效Enum值,则将引发异常。可以使用 IN 运算符来判断一个 Enum 是否存在于某个 Enum 集合中,其中集合中的 Enum 需要用字符串表示。
大多数具有数字和字符串的运算并不适用于Enums;例如,Enum 类型不能和一个数值相加。但是,Enum有一个原生的 toString
函数,它返回它的字符串值。
Enum 值使用 toT
函数可以转换成数值类型,其中 T 是一个数值类型。若 T
恰好对应 Enum 的底层数值类型,这个转换是零消耗的。
Enum 类型可以被 ALTER
无成本地修改对应集合的值。可以通过 ALTER
操作来增加或删除 Enum 的成员(只要表没有用到该值,删除都是安全的)。作为安全保障,改变之前使用过的 Enum 成员将抛出异常。
通过 ALTER
操作,可以将 Enum8
转成 Enum16
,反之亦然,就像 Int8
转 Int16
一样。
BitMap64是 ByteHouse 中提供的一种数据类型(DataType),可以参考Array(UInt64)这种类型。这种类型在数据表示上等同于社区 ClickHouse 提供的数据类型。AggregateFunction(groupBitmap, UInt64)
。你可以定义该类型的字段,写入数据并读取数据进行计算。
⚠️Tips: BitMap64是数据类型,底层使用了 RoaringBitmap 开源库。ByteHouse 中有较多功能都利用了 RoaringBimap 实现,其中
BitmapIndex
比较容易和 BitMap64类型混淆。区别如下:
BitMap64: ByteHouse提供的物理列数据类型,直接把RoaringBitmap定义成了一个数据类型,可写可读;相同功能的社区数据类型是AggregateFunction(groupBitmap, UInt64)
。
BitmapIndex: ByteHouse提供的一种数据索引能力,目前用于Array + Int8/16/32/64/UInt/8/16/32/64/String类型,它为Array中每个元素构建一个基于RoaringBitmap实现的索引,查询时使用arraySetCheck(column, (item)) 检测item是否在数组里,并使用bitmap-index做过滤,从而避免大量的array数据读取。索引文件本身不可读取。
如果是在查询中,你可以通过如下函数产生BitMap64 类型的数据:
select bitmapFromColumn(number) from numbers(10); select arrayToBitmap(groupArray(number)) from numbers(10;
使用BitMap64
关键字作为该类型的定义方法。示例如下:
CREATE TABLE bitmap_table ( `tag_id` Int32, `tag_value` String, `id_map` BitMap64, `p_date` Date ) ENGINE = CnchMergeTree PARTITION BY p_date ORDER BY (tag_id, tag_value);
如果是文本格式的数据源(如 CSV/JSONEachRow)等,既可以使用和数组一样的表示格式(使用[]
包裹数据),也可使用 BitMap64 的格式(使用{}
包裹数据)填充字段值。示例如下:
insert into bitmap_table values (1, 'aaa', [2,3,4,5,6], '2024-01-01'); insert into bitmap_table format JSONEachRow {"tag_id":2,"tag_value":"bbb","id_map":"{4,5,6}","p_date":"2024-01-01"}
如果是 Parquet 格式,则使用 LIST(UINT64)
,和数组一样的方式填充数据。
如果使用insert select
的方式写入,则和其他类型一样把列对应上即可。
BitMap64
是可打印类型,你可以直接查询该字段,会以文本的方式进行展示。示例如下:
select * from bitmap_table where p_date = '2024-01-01' and tag_id = 1; --- 查询结果 tag_id tag_value id_map p_date 1 aaa {2,3,4,5,6} 2024-01-01
还可以使用 bitmap函数来对 BitMap64 类型数据进行复杂运算,这部分参考【函数 - BitMap64函数】章节。
把其它数据类型转变为字典编码类型。
LowCardinality(data_type)
data_type
— String, FixedString, Date, DateTime,包括数字类型,但是Decimal除外。对一些数据类型来说,LowCardinality
并不高效,详查allow_suspicious_low_cardinality_types设置描述。LowCardinality
是一种改变数据存储和数据处理方法的概念。 ClickHouse会把 LowCardinality
所在的列进行dictionary coding。对很多应用来说,处理字典编码的数据可以显著的增加SELECT查询速度。
使用 LowCarditality
数据类型的效率依赖于数据的多样性。如果一个字典包含少于10000个不同的值,那么ClickHouse可以进行更高效的数据存储和处理。反之如果字典多于10000,效率会表现的更差。
当使用字符类型的时候,可以考虑使用 LowCardinality
代替Enum。 LowCardinality
通常更加灵活和高效。
创建一个 LowCardinality
类型的列:
CREATE TABLE lc_t ( `id` UInt16, `strings` LowCardinality(String) ) ENGINE = MergeTree() ORDER BY id
设置:
函数:
IPv4
是与UInt32
类型保持二进制兼容的Domain类型,其用于存储IPv4地址的值。它提供了更为紧凑的二进制存储的同时支持识别可读性更加友好的输入输出格式。
CREATE TABLE hits (url String, from IPv4) ENGINE = MergeTree() ORDER BY url; DESCRIBE TABLE hits;
┌─name─┬─type───┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┐ │ url │ String │ │ │ │ │ │ from │ IPv4 │ │ │ │ │ └──────┴────────┴──────────────┴────────────────────┴─────────┴──────────────────┘
同时您也可以使用IPv4
类型的列作为主键:
CREATE TABLE hits (url String, from IPv4) ENGINE = MergeTree() ORDER BY from;
在写入与查询时,IPv4
类型能够识别可读性更加友好的输入输出格式:
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 │ └────────────────────────────────────┴────────────────┘
同时它提供更为紧凑的二进制存储格式:
SELECT toTypeName(from), hex(from) FROM hits LIMIT 1;
┌─toTypeName(from)─┬─hex(from)─┐ │ IPv4 │ B7F7E83A │ └──────────────────┴───────────┘
不可隐式转换为除UInt32
以外的其他类型类型。如果要将IPv4
类型的值转换成字符串,你可以使用IPv4NumToString()
显示的进行转换:
SELECT toTypeName(s), IPv4NumToString(from) as s FROM hits LIMIT 1;
┌─toTypeName(IPv4NumToString(from))─┬─s──────────────┐ │ String │ 183.247.232.58 │ └───────────────────────────────────┴────────────────┘
或可以使用CAST
将它转换为UInt32
类型:
SELECT toTypeName(i), CAST(from as UInt32) as i FROM hits LIMIT 1;
┌─toTypeName(CAST(from, 'UInt32'))─┬──────────i─┐ │ UInt32 │ 3086477370 │ └──────────────────────────────────┴────────────┘
IPv6
是与FixedString(16)
类型保持二进制兼容的Domain类型,其用于存储IPv6地址的值。它提供了更为紧凑的二进制存储的同时支持识别可读性更加友好的输入输出格式。
CREATE TABLE hits (url String, from IPv6) ENGINE = MergeTree() ORDER BY url; DESCRIBE TABLE hits;
┌─name─┬─type───┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┐ │ url │ String │ │ │ │ │ │ from │ IPv6 │ │ │ │ │ └──────┴────────┴──────────────┴────────────────────┴─────────┴──────────────────┘
同时您也可以使用IPv6
类型的列作为主键:
CREATE TABLE hits (url String, from IPv6) ENGINE = MergeTree() ORDER BY from;
在写入与查询时,IPv6
类型能够识别可读性更加友好的输入输出格式:
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 │ └────────────────────────────────────┴───────────────────────────────┘
同时它提供更为紧凑的二进制存储格式:
SELECT toTypeName(from), hex(from) FROM hits LIMIT 1;
┌─toTypeName(from)─┬─hex(from)────────────────────────┐ │ IPv6 │ 200144C8012926320033000002520002 │ └──────────────────┴──────────────────────────────────┘
不可隐式转换为除FixedString(16)
以外的其他类型类型。如果要将IPv6
类型的值转换成字符串,你可以使用IPv6NumToString()
显示的进行转换:
SELECT toTypeName(s), IPv6NumToString(from) as s FROM hits LIMIT 1;
┌─toTypeName(IPv6NumToString(from))─┬─s─────────────────────────────┐ │ String │ 2001:44c8:129:2632:33:0:252:2 │ └───────────────────────────────────┴───────────────────────────────┘
或使用CAST
将其转换为FixedString(16)
:
SELECT toTypeName(i), CAST(from as FixedString(16)) as i FROM hits LIMIT 1;
┌─toTypeName(CAST(from, 'FixedString(16)'))─┬─i───────┐ │ FixedString(16) │ ��� │ └───────────────────────────────────────────┴─────────┘
聚合函数的中间状态,可以通过聚合函数名称加-State
后缀的形式得到它。与此同时,当您需要访问该类型的最终状态数据时,您需要以相同的聚合函数名加-Merge
后缀的形式来得到最终状态数据。AggregateFunction
— 参数化的数据类型。
参数
如果函数具备多个参数列表,请在此处指定其他参数列表中的值。
示例
CREATE TABLE t ( column1 AggregateFunction(uniq, UInt64), column2 AggregateFunction(anyIf, String, UInt8), column3 AggregateFunction(quantiles(0.5, 0.9), UInt64) ) ENGINE = ...
上述中的uniq, anyIf (任何+如果) 以及 分位数 都为ClickHouse中支持的聚合函数。
当需要写入数据时,您需要将数据包含在INSERT SELECT
语句中,同时对于AggregateFunction
类型的数据,您需要使用对应的以-State
为后缀的函数进行处理。
函数使用示例
uniqState(UserID) quantilesState(0.5, 0.9)(SendTiming)
不同于uniq
和quantiles
函数返回聚合结果的最终值,以-State
后缀的函数总是返回AggregateFunction
类型的数据的中间状态。
对于SELECT
而言,AggregateFunction
类型总是以特定的二进制形式展现在所有的输出格式中。例如,您可以使用SELECT
语句将函数的状态数据转储为TabSeparated
格式的同时使用INSERT
语句将数据转储回去。
当从AggregatingMergeTree
表中查询数据时,对于AggregateFunction
类型的字段,您需要使用以-Merge
为后缀的相同聚合函数来聚合数据。对于非AggregateFunction
类型的字段,请将它们包含在GROUP BY
子句中。
以-Merge
为后缀的聚合函数,可以将多个AggregateFunction
类型的中间状态组合计算为最终的聚合结果。
例如,如下的两个查询返回的结果总是一致:
SELECT uniq(UserID) FROM table SELECT uniqMerge(state) FROM (SELECT uniqState(UserID) AS state FROM table GROUP BY RegionID)