irpas技术客

ClickHouse 实战:ClickHouse 基础数据类型极简教程_禅与计算机程序设计艺术

irpas 6429

1.?ClickHouse基础数据类型

道生一,一生二,二生三,三生万物。

——【春秋·老子】道德经·第四十二章??

在第 2 章中,我们通过“一顿猛如虎的操作”,ClickHouse 环境已经准备好。现在,万事俱备只欠东风了。各位看官,先莫急。话说,要想做一道好菜,光有柴米油盐酱醋肉不行,还得有“菜谱”。菜谱是厨师利用烹饪原料,通过各种烹调技法创作出一道好菜的方法。要想成为大厨,不懂选料、切配、烹饪等技艺,那是不行的。要想搞好大数据,那就必须要很懂 Select(选料)、Update/Delete(切配)、Insert(烹饪)等技术才行。

本章先从“选料”开始,主要介绍 ClickHouse 中的“原料”——?数据类型。

1.1.?ClickHouse数据类型

数据类型(Data types)是计算机科学的基础。在关系数据库理论中,也需要一个类型系统来支持。因为,关系本身也是定义在某种数据类型之上的。在介绍 SQL 基础之前,我们先来了解 ClickHouse?SQL 中的基本数据类型。

作为一款分析型数据库,ClickHouse 提供了许多数据类型,它们可以划分为:基础类型、复合类型和特殊类型。如下图所示。

其中基础类型使 ClickHouse 具备了描述数据的基本能力,而另外两种类型则使 ClickHouse 的数据表达能力更加的丰富立体。本节先介绍基础类型部分。关于复合类型和特殊类型我们放到第 5?章来介绍。

1.1.1.?概述

ClickHouse?基础类型有数值、字符串和时间三大类型。完整的数据类型清单,可以执行如下 SQL 获得:

SELECT * FROM system.data_type_families

输出结果如下表。

name

case_sensitive

alias_to

Polygon

0

Ring

0

Point

0

SimpleAggregateFunction

0

MultiPolygon

0

IPv6

0

IntervalSecond

0

IPv4

0

UInt32

0

IntervalYear

0

IntervalQuarter

0

IntervalMonth

0

Int64

0

IntervalDay

0

IntervalHour

0

Int16

0

UInt256

0

LowCardinality

0

AggregateFunction

0

Nothing

0

Decimal256

1

Tuple

0

Array

0

Enum16

0

IntervalMinute

0

FixedString

0

String

0

DateTime

1

Map

0

UUID

0

Decimal64

1

Nullable

0

Enum

1

Int32

0

UInt8

0

Date

1

Decimal32

1

UInt128

0

Float64

0

Nested

0

Int128

0

Decimal128

1

Int8

0

Decimal

1

Int256

0

DateTime64

1

Enum8

0

DateTime32

1

Date32

1

IntervalWeek

0

UInt64

0

UInt16

0

Float32

0

INET6

1

IPv6

INET4

1

IPv4

ENUM

1

Enum

BINARY

1

FixedString

NATIONAL CHAR VARYING

1

String

BINARY VARYING

1

String

NCHAR LARGE OBJECT

1

String

NATIONAL CHARACTER VARYING

1

String

NATIONAL CHARACTER LARGE OBJECT

1

String

NATIONAL CHARACTER

1

String

NATIONAL CHAR

1

String

CHARACTER VARYING

1

String

LONGBLOB

1

String

MEDIUMTEXT

1

String

TEXT

1

String

TINYBLOB

1

String

VARCHAR2

1

String

CHARACTER LARGE OBJECT

1

String

DOUBLE PRECISION

1

Float64

LONGTEXT

1

String

NVARCHAR

1

String

INT1 UNSIGNED

1

UInt8

VARCHAR

1

String

CHAR VARYING

1

String

MEDIUMBLOB

1

String

NCHAR

1

String

CHAR

1

String

SMALLINT UNSIGNED

1

UInt16

TIMESTAMP

1

DateTime

FIXED

1

Decimal

TINYTEXT

1

String

NUMERIC

1

Decimal

DEC

1

Decimal

TINYINT UNSIGNED

1

UInt8

INTEGER UNSIGNED

1

UInt32

INT UNSIGNED

1

UInt32

CLOB

1

String

MEDIUMINT UNSIGNED

1

UInt32

BOOL

1

Int8

SMALLINT

1

Int16

INTEGER SIGNED

1

Int32

NCHAR VARYING

1

String

INT SIGNED

1

Int32

TINYINT SIGNED

1

Int8

BIGINT SIGNED

1

Int64

BINARY LARGE OBJECT

1

String

SMALLINT SIGNED

1

Int16

MEDIUMINT

1

Int32

INTEGER

1

Int32

INT1 SIGNED

1

Int8

BIGINT UNSIGNED

1

UInt64

BYTEA

1

String

INT

1

Int32

SINGLE

1

Float32

FLOAT

1

Float32

MEDIUMINT SIGNED

1

Int32

BOOLEAN

1

Int8

DOUBLE

1

Float64

INT1

1

Int8

CHAR LARGE OBJECT

1

String

TINYINT

1

Int8

BIGINT

1

Int64

CHARACTER

1

String

BYTE

1

Int8

BLOB

1

String

REAL

1

Float32

从上表可以看出,ClickHouse 的 String 类型、Int 类型、Float 类型、Decimal类型等都是大小写敏感的(case_sensitive=1)。需要注意的是,ClickHouse 中的类型是区分大小写的,比如 Array、UInt8、String、DateTime、Float64?等。同时,ClickHouse 中的绝大部分函数也是区分大小写的,例如:array()、arrayCount() 、arrayJoin()、?bitmapAnd() 、bitmapCardinality()、concat() 、dateDiff() 、addDays() 等等。

另外,ClickHouse 中没有 true、false值的布尔类型(Bool),所以,一般用整型(UInt8)表示布尔类型,1 为真,0 为假。

为了快速理解 ClickHouse 中的基础数据类型,我们用熟悉的 MySQL 中的数据类型进行比较,如下表。

ClickHouse

MySQL

关于 ClickHouse 的类型说明

UInt8, UInt16, UInt32, UInt64, UInt128, UInt256

TINYINT UNSIGNED, SMALLINT UNSIGNED, INT UNSIGNED, BIGINT UNSIGNED

无符号整型。数据范围:

UInt8:[0:255],即

UInt16:[0:65535],即

UInt32:[0:4294967295],即

UInt64:[0:18446744073709551615],即

UInt128:[0 : 340282366920938463463374607431768211455],即

UInt256:[0 : 115792089237316195423570985008687907853269984665640564039457584007913129639935],即

Int8, Int16, Int32, Int64, Int128, Int256

TINYINT SIGNED, SMALLINT SIGNED, INT SIGNED, BIGINT SIGNED

有符号整型。数据范围:

Int8:[-128:127],即

Int16:[-32768:32767],即

Int32:[-2147483648:2147483647],即

Int64:[-9223372036854775808:9223372036854775807],即

Int128:[-170141183460469231731687303715884105728 : 170141183460469231731687303715884105727],即

Int256:[-57896044618658097711785492504343953926634992332820282019728792003956564819968 : 57896044618658097711785492504343953926634992332820282019728792003956564819967],即

Float32, Float64

FLOAT, DOUBLE

浮点数类型。ClickHouse支持 inf, -inf, nan,但是一般情况下不用。

Decimal32,Decimal64,

Decimal128

DECIMAL

Decimal(P,S),

参数:

P - 精度。有效范围:[1:38],决定可以有多少个十进制数字(包括分数)。

S - 规模。有效范围:[0:P],决定数字的小数部分中包含的小数位数。

定点数类型。有符号的定点数,可在加、减和乘法运算过程中保持精度。对于除法,最低有效数字会被丢弃(不舍入)。

String

BLOB, TEXT, VARCHAR, VARBINARY

字符串可以任意长度的。它可以包含任意的字节集,包含空字节。因此,字符串类型可以代替其他 DBMSs 中的 VARCHAR、BLOB、CLOB 等类型。字符串在ClickHouse中没有进行编码,可以是任意的字节集,按它们原本的方式进行存储和输出。推荐使用 UTF-8。事实上,它的行为就像一个 BLOB。

FixedString(n)

CHAR, BINARY

\0 填充。可用的函数比 String 少,实际上它的行为类似于 BINARY。

Date

DATE

日期类型,用2个字节存储,表示从 1970-01-01 (无符号) 到当前的日期值。允许存储从 Unix 纪元开始到编译阶段定义的上限阈值常量(目前上限是2106年,但最终完全支持的年份为2105)。最小值输出为1970-01-01。取值范围: [1970-01-01, 2149-06-06]。日期中没有存储时区信息。

DateTime

DATETIME, TIMESTAMP

时间戳类型。用4个字节(无符号)存储 Unix 时间戳)。允许存储与日期类型相同的范围内的值。最小值为 1970-01-01 00:00:00。时间戳类型值精确到秒(没有闰秒)。取值范围: [1970-01-01 00:00:00, 2106-02-07 06:28:15]。

Enum

ENUM

类似于 MySQL 的枚举ENUM。行为类似于 Int8/16。

Array(T)

n.a.

类型数组。T 可以是任意类型,包含数组类型。但不推荐使用多维数组,ClickHouse 对多维数组的支持有限。例如,不能存储在 MergeTree 表中存储多维数组。

Map(key, value)

n.a.

Map(key, value) 映射字典数据类型。存储键值对。参数说明:

key:Map字典 key 值,可以是?String, Integer, LowCardinality, 或?FixedString类型。

value:Map字典 value?值,可以是?String, Integer, Array, LowCardinality, 或者FixedString类型。

假设 a 列的类型是Map<String,String>, 使用?a[‘K’]?获取 key=’K’ 对应的 value值。该操作的复杂度是线性的。

Tuple(T1,T2,...)

n.a.

元组类型。元组中的每个元素都有单独的类型。

Nested(Name1 Type1, Name2 Type2, ...)

n.a.

嵌套类型。参数说明:

Name1:嵌套变量名(key)

Type1:嵌套类型(value)

MySQL 中最接近的等价物是 JSON。

AggregateFunction

(name, types_of_arguments?...)

n.a.

聚合函数类型。参数说明:

name:聚合函数名,通常带State后缀;

types_of_arguments:聚合函数参数的类型。

生成聚合函数状态的常见方法是调用带有-State后缀的聚合函数。获取该类型的最终状态数据时,则以相同的聚合函数名加-Merge后缀的形式来实现。

Set

n.a.

集合类型。用在 IN 表达式的右半部分。例如:

SELECT UserID IN (123, 456) FROM t

SELECT (CounterID, UserID) IN ((34, 123), (101500, 456)) FROM t

Expression

n.a.

表达式(其实就是Function)类型。用于表示高阶函数中的Lambd表达式。

LowCardinality

n.a

低基数类型,把其它数据类型转变为字典编码类型,以提升查询性能。

语法:?LowCardinality(data_type)

参数说明: data_type,可以是String, FixedString, Date, DateTime,包括数字类型,但是Decimal除外。对一些数据类型来说,LowCardinality 并不高效,参考allow_suspicious_low_cardinality_types参数设置。

LowCardinality 是一种改变数据存储和数据处理方法的概念。ClickHouse会把 LowCardinality 所在的列进行dictionary coding。对很多应用来说,处理字典编码的数据可以显著的增加SELECT查询速度。

Nothing

n.a

空数据类型。表示未知NULL值的数据类型。例如,文本 NULL 的类型为 Nullable(Nothing)。不能创建一个 Nothing 类型的值。

Nullable(T)

n.a

可空值类型。例如,c1?列的类型是Nullable(Int8) ,表示 c1?列可以存储 Int8 类型值,没有值的行将存储 NULL。另外,注意一下 3 点:

1.类型 T,不支持复合数据类型Array 和 Tuple。

2.Nullable 类型字段不能包含在表索引中。

3.Nullable 类型的默认值为NULL。如果有特殊需求,可以在ClickHouse Server配置中自定义。

上面的ClickHouse 与 MySQL 数据类的对应关系可以到 ClickHouse 源代码convertMySQLDataType.cpp中找到。为了方便阅读,节省大家的查阅的时间,这里直接贴出相关的源码:

#include "convertMySQLDataType.h"

#include <Core/Field.h>

#include <base/types.h>

#include <Core/MultiEnum.h>

#include <Core/SettingsEnums.h>

#include <Parsers/ASTFunction.h>

#include <Parsers/IAST.h>

#include "DataTypeDate.h"

#include "DataTypeDateTime.h"

#include "DataTypeDateTime64.h"

#include "DataTypeEnum.h"

#include "DataTypesDecimal.h"

#include "DataTypeFixedString.h"

#include "DataTypeNullable.h"

#include "DataTypeString.h"

#include "DataTypesNumber.h"

#include "IDataType.h"

namespace DB

{

DataTypePtr convertMySQLDataType(MultiEnum<MySQLDataTypesSupport> type_support,

????????const std::string & mysql_data_type,

????????bool is_nullable,

????????bool is_unsigned,

????????size_t length,

????????size_t precision,

????????size_t scale)

{

????// Mysql returns mysql_data_type as below:

????// 1. basic_type

????// 2. basic_type options

????// 3. type_with_params(param1, param2, ...)

????// 4. type_with_params(param1, param2, ...) options

????// The options can be unsigned, zerofill, or some other strings.

????auto data_type = std::string_view(mysql_data_type);

????const auto type_end_pos = data_type.find_first_of(R"(( )"); // FIXME: fix style-check script instead

????const auto type_name = data_type.substr(0, type_end_pos);

????DataTypePtr res;

????if (type_name == "tinyint")

????{

????????if (is_unsigned)

????????????res = std::make_shared<DataTypeUInt8>();

????????else

????????????res = std::make_shared<DataTypeInt8>();

????}

????else if (type_name == "smallint")

????{

????????if (is_unsigned)

????????????res = std::make_shared<DataTypeUInt16>();

????????else

????????????res = std::make_shared<DataTypeInt16>();

????}

????else if (type_name == "int" || type_name == "mediumint")

????{

????????if (is_unsigned)

????????????res = std::make_shared<DataTypeUInt32>();

????????else

????????????res = std::make_shared<DataTypeInt32>();

????}

????else if (type_name == "bigint")

????{

????????if (is_unsigned)

????????????res = std::make_shared<DataTypeUInt64>();

????????else

????????????res = std::make_shared<DataTypeInt64>();

????}

????else if (type_name == "float")

????????res = std::make_shared<DataTypeFloat32>();

????else if (type_name == "double")

????????res = std::make_shared<DataTypeFloat64>();

????else if (type_name == "date")

????????res = std::make_shared<DataTypeDate>();

????else if (type_name == "binary")

????????res = std::make_shared<DataTypeFixedString>(length);

????else if (type_name == "datetime" || type_name == "timestamp")

????{

????????if (!type_support.isSet(MySQLDataTypesSupport::DATETIME64))

????????{

????????????res = std::make_shared<DataTypeDateTime>();

????????}

????????else if (type_name == "timestamp" && scale == 0)

????????{

????????????res = std::make_shared<DataTypeDateTime>();

????????}

????????else if (type_name == "datetime" || type_name == "timestamp")

????????{

????????????res = std::make_shared<DataTypeDateTime64>(scale);

????????}

????}

????else if (type_name == "bit")

????{

????????res = std::make_shared<DataTypeUInt64>();

????}

????else if (type_support.isSet(MySQLDataTypesSupport::DECIMAL) && (type_name == "numeric" || type_name == "decimal"))

????{

????????if (precision <= DecimalUtils::max_precision<Decimal32>)

????????????res = std::make_shared<DataTypeDecimal<Decimal32>>(precision, scale);

????????else if (precision <= DecimalUtils::max_precision<Decimal64>) //-V547

????????????res = std::make_shared<DataTypeDecimal<Decimal64>>(precision, scale);

????????else if (precision <= DecimalUtils::max_precision<Decimal128>) //-V547

????????????res = std::make_shared<DataTypeDecimal<Decimal128>>(precision, scale);

????}

????/// Also String is fallback for all unknown types.

????if (!res)

????????res = std::make_shared<DataTypeString>();

????if (is_nullable)

????????res = std::make_shared<DataTypeNullable>(res);

????return res;

}

}

从最后的几行代码中可以看到,在 ClickHouse 中针对所有的未知类型,都是用String 来填充。关于 ClickHouse 数据类型的相关源码,在src/DataTypes?目录下。一个典型的关于 ClickHouse 全部数据类型清单价的代码片段在IDataType.h中。

1.1.2.?数值类型

ClickHouse数值类型分为整数类型、浮点数类型和 Decimal 类型三大类, 各个大类自己又分别有8、16、32、64、128、256?位等不同位数的细分。清单如下表。

数据类型

ClickHouse 类型

无符号整型(取值≥0)

UInt8

UInt16

UInt32

UInt64

UInt128

UInt256

有符号整型(可取负值)

Int8

Int16

Int32

Int64

Int128

Int256

定点数值型

Decimal32

Decimal64

Decimal128

Decimal256

浮点型

Float32

Float64

在 ClickHouse 源码DataTypesNumber.cpp中,定义了如上数值类型。关键代码如下:

void registerDataTypeNumbers(DataTypeFactory & factory)

{

????factory.registerDataType("UInt8", createNumericDataType<UInt8>);

????factory.registerDataType("UInt16", createNumericDataType<UInt16>);

????factory.registerDataType("UInt32", createNumericDataType<UInt32>);

????factory.registerDataType("UInt64", createNumericDataType<UInt64>);

????factory.registerDataType("Int8", createNumericDataType<Int8>);

????factory.registerDataType("Int16", createNumericDataType<Int16>);

????factory.registerDataType("Int32", createNumericDataType<Int32>);

????factory.registerDataType("Int64", createNumericDataType<Int64>);

????factory.registerDataType("Float32", createNumericDataType<Float32>);

????factory.registerDataType("Float64", createNumericDataType<Float64>);

????factory.registerSimpleDataType("UInt128", [] { return DataTypePtr(std::make_shared<DataTypeUInt128>()); });

????factory.registerSimpleDataType("UInt256", [] { return DataTypePtr(std::make_shared<DataTypeUInt256>()); });

????factory.registerSimpleDataType("Int128", [] { return DataTypePtr(std::make_shared<DataTypeInt128>()); });

????factory.registerSimpleDataType("Int256", [] { return DataTypePtr(std::make_shared<DataTypeInt256>()); });

????/// These synonyms are added for compatibility.

????factory.registerAlias("TINYINT", "Int8", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INT1", "Int8", DataTypeFactory::CaseInsensitive); ???/// MySQL

????factory.registerAlias("BYTE", "Int8", DataTypeFactory::CaseInsensitive); ???/// MS Access

????factory.registerAlias("SMALLINT", "Int16", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INT", "Int32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INTEGER", "Int32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BIGINT", "Int64", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("FLOAT", "Float32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("REAL", "Float32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("SINGLE", "Float32", DataTypeFactory::CaseInsensitive); ??/// MS Access

????factory.registerAlias("DOUBLE", "Float64", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("MEDIUMINT", "Int32", DataTypeFactory::CaseInsensitive); ???/// MySQL

????factory.registerAlias("DOUBLE PRECISION", "Float64", DataTypeFactory::CaseInsensitive);

????/// MySQL

????factory.registerAlias("TINYINT SIGNED", "Int8", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INT1 SIGNED", "Int8", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("SMALLINT SIGNED", "Int16", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("MEDIUMINT SIGNED", "Int32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INT SIGNED", "Int32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INTEGER SIGNED", "Int32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BIGINT SIGNED", "Int64", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("TINYINT UNSIGNED", "UInt8", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INT1 UNSIGNED", "UInt8", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("SMALLINT UNSIGNED", "UInt16", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("MEDIUMINT UNSIGNED", "UInt32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INT UNSIGNED", "UInt32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("INTEGER UNSIGNED", "UInt32", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BIGINT UNSIGNED", "UInt64", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BIT", "UInt64", DataTypeFactory::CaseInsensitive); ?/// MySQL

????factory.registerAlias("SET", "UInt64", DataTypeFactory::CaseInsensitive); ?/// MySQL

????factory.registerAlias("YEAR", "UInt16", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("TIME", "Int64", DataTypeFactory::CaseInsensitive);

}

通过源码可以看出,ClickHouse SQL数据类型做了针对MySQL、MS Access等数据类型的兼容处理。接下来,我们通过SQL实例介绍这些数据类型的使用。

Int

ClickHouse 整数类型有:UInt8, UInt16, UInt32, UInt64, UInt128, UInt256, Int8, Int16, Int32, Int64, Int128, Int256。其中,U 开头的是无符号整型,也就是大于等于 0 的。后面的 8、16、32、64、128、256分别是各个类型所占的 bit 位数。

整型范围

Int8 — [-128 : 127]

Int16 — [-32768 : 32767]

Int32 — [-2147483648 : 2147483647]

Int64 — [-9223372036854775808 : 9223372036854775807]

Int128 — [-170141183460469231731687303715884105728 : 170141183460469231731687303715884105727]

Int256 — [-57896044618658097711785492504343953926634992332820282019728792003956564819968 : 57896044618658097711785492504343953926634992332820282019728792003956564819967]

别名:

Int8 — TINYINT, BOOL, BOOLEAN, INT1.

Int16 — SMALLINT, INT2.

Int32 — INT, INT4, INTEGER.

Int64 — BIGINT.

无符号整型范围

UInt8 — [0 : 255]

UInt16 — [0 : 65535]

UInt32 — [0 : 4294967295]

UInt64 — [0 : 18446744073709551615]

UInt128 — [0 : 340282366920938463463374607431768211455]

UInt256 — [0 : 115792089237316195423570985008687907853269984665640564039457584007913129639935]

我们可以通过内置的数学函数?exp2(n) 计算2的 n 次幂。比如说 UInt8?的取值范围是[0:255], 我们可以计算出来这个最大值就是:

SELECT exp2(8) - 1

执行结果:

┌minus(exp2(8), 1)?┐

│ ????????????255????│

└──────────┘

UInt256的最大值是:

SELECT exp2(256) - 1

输出结果:

┌──minus(exp2(256), 1)─┐

│ 1.157920892373162e77 ???│

└──────────────┘

可以看到,在终端输出的是1.157920892373162e77?科学计数法的结果。但是我们想要看具体的数字,这个时候我们可能想用类型转换函数 toUInt256(x) 来实现。但是,事与愿违,ClickHouse?Client 终端执行的结果并不是我们所期望的。

SELECT toUInt256(exp2(256)) - 1

minus(toUInt256(exp2(256)), 1)

57896044618658097702369839901263932781391731748390190090761097376371310591999

因为,exp2(x) 返回值类型是 Float64,精度有丢失。

执行如下 SQL:

select toUInt256(115792089237316195423570985008687907853269984665640564039457584007913129639935)

输出:

SELECT toUInt256(1.157920892373162e77)

toUInt256(1.157920892373162e77)

57896044618658097702369839901263932781391731748390190090761097376371310592000

可以发现结果也不是我们期望的。

执行如下 SQL 来看一下UInt256?上限值在 ClickHouse 中的类型是什么:

select toTypeName(115792089237316195423570985008687907853269984665640564039457584007913129639935);

输出:

Float64

可以看出,并不是 UInt256。Float64?在进行计算的时候,会有精度丢失:小数点后除去左边的零后第17位起会产生数据溢出。

需要注意的是,UInt128、UInt256能表示的整数范围十分巨大,占用的字节大小也随之增大,用得比较少。

Float

ClickHouse 浮点数有:单精度浮点数是Float32(等同于C语言中的类型float)和双精度浮点数是Float64(等同于C语言中的类型double),遵循IEEE 754浮点数标准。如下表。

类型

bytes

bits位数

有效精度

(排除最左边0的小数位数)

取值范围

Float32

别名:FLOAT

4

32

7,小数点后除去左边的零后第8位起会产生数据溢出。也就是说Float32的精度为6~7位。

[-3.40E+38: +3.40E+38],

负值取值范围为 [-3.4028235E+38:-1.4012984E-45]

正值取值范围为 [-1.4012984E-45:3.4028235E+38]

Float64

别名:DOUBLE

8

64

16,小数点后除去左边的零后第17位起会产生数据溢出。也即Float64的精度为15~16位。

[-1.79E+308: +1.79E+308],

负值取值范围为 [-1.79769313486231570E+308:-4.94065645841246544E-324]

正值取值范围为 [4.94065645841246544E-324:1.79769313486231570E+308]

浮点数计算的精度

对浮点数进行计算可能引起四舍五入的误差。

实例?SQL:

SELECT

????1 - 0.9,

????toTypeName(1 - 0.9)

输出:

┌───────minus(1, 0.9)─┬─toTypeName(minus(1, 0.9))─┐

│ 0.09999999999999998 ?????│ ??Float64 ??????????????????│

└──────────────┴──────────────┘

可以看出,小数点后除去左边的零后第17位起产生数据溢出,计算结果是 Float64?类型。

因为浮点数有精度丢失,所以在实际工作中,我们通常都使用整数形式存储数据,将固定精度的数字转换为整数值。例如货币单位使用分,页面加载时间用毫秒为单位表示等。

无穷与非数字值类型

另外,ClickHouse 的浮点数支持正无穷(inf)、负无穷(-inf)以及非数字(nan)特殊数值符号。通过执行如下 SQL可以看到结果。

SELECT

????1 / 0,

????-1 / 0,

????0 / 0,

????sqrt(-1)

输出结果:

┌─divide(1, 0)─┬─divide(-1, 0)─┬─divide(0, 0)─┬─sqrt(-1)─┐

│ ????????inf │ ????????-inf ??│ ??????nan │ ?????nan │

└──────┴───────┴───────┴──────┘

另外,?inf/-inf 得到的结果是 nan。

执行 SQL:

SELECT (1 / 0) / (-1 / 0)

输出:

Query id: 52de21e8-d82d-4fdd-9050-a9e8ff4b3bae

┌─divide(divide(1, 0), divide(-1, 0))┐

│ ???????????nan ??????????????│

└───────────────┘

Decimal

ClickHouse 中高精度数值类型为Decimal,也称为为定点数。

用法:Decimal(P,S)

其中,

P:总位数?= 整数位数?+ 小数位数,取值范围是[1,76]。也叫精度(precision)。

S:小数位数,取值范围是[0,P]。也叫规模(scale)。

定点数加减计算

通过使用toDecimal32(value, S)?函数来进行定点数的计算。SQL 实例:

SELECT

????1 - 0.9,

????toDecimal64(1 - 0.9, 4),

????toDecimal64(1.23, 3) + toDecimal64(1.234, 5),

????toDecimal64(1.23, 3) - toDecimal64(1.234, 5)

执行结果:

minus(1, 0.9):0.09999999999999998

toDecimal64(minus(1, 0.9), 4):0.0999

plus(toDecimal64(1.23, 3), toDecimal64(1.234, 5)):2.464

minus(toDecimal64(1.23, 3), toDecimal64(1.234, 5)):-0.004

定点数乘法运算

乘法运算时,小数位数S为两者之和。

执行 SQL:

SELECT toDecimal32(22.9876312, 3) * toDecimal32(33.123450011, 2)

输出:

multiply(toDecimal32(22.9876312, 3), toDecimal32(33.123450011, 2)):761.32944

定点数除法运算

除法运算时,小数位数S为被除数位数。此时要求 S(被除数) > S(除数),否则报错。

实例?SQL:

SELECT toDecimal64(22.9876312, 3) / toDecimal64(33.123450011, 7)

输出:

Received exception from server (version 21.12.1):

Code: 69. DB::Exception: Received from localhost:9000. DB::Exception: Decimal result's scale is less than argument's one: While processing toDecimal64(22.9876312, 3) / toDecimal64(33.123450011, 7). (ARGUMENT_OUT_OF_BOUND)

另外,因为当前计算机系统只支持 32 或者 64 位,所以高于这个位数的数字类型:Int128、Int256、UInt128、UInt256、Decimal128、Decimal256等,其实是在软件层面实现的,性能上会比 Int32、Int64、UInt32、UInt64、Decimal32、Decimal64等硬件支持的数据类型要差很多。

1.1.3.?字符串类型

ClickHouse?中字符串类型主要包括:

l?不定长字符串String,也就是动态长度变化字符串。

l?固定长度字符串FixedString(N)。这里的N是最大字节数,而不是长度,例如UTF-8字符占用3个字节,GBK字符占用2个字节。

l?特殊字符串UUID。背后存储的是数值。

ClickHouse中的字符串,没有针对字符作编码操作。字符串可以包含一组任意字节,这些字节按原样存储和输出。编码和解码操作留给客户端。如果存储文本类型内容,ClickHouse推荐使用UTF-8编码。读取和写入数据都约定统一使用UTF-8编码。

String

String类型不限制字符串的长度,可以直接替代其他数据库的VARCHAR、BLOB、CLOB等字符串类型,相比VARCHAR这类要考虑预测数据最大长度,显然String要方便很多。

实例?SQL:

SELECT

????'123',

????'abc',

????toTypeName('123'),

????toTypeName('abc')

输出:

┌─'123'─┬─'abc'─┬─toTypeName('123')─┬─toTypeName('abc')─┐

│ 123 ???│ abc ???│ String ??????????????│ String ?????????????│

└────┴────┴───────────┴──────────┘

下面的 SQL 展示了在 ClickHouse 中计算字符串的长度和截取字串的方法。

select length('abc'), substring('abc', 2), substring('abc', 1);

输出:

┌─length('abc')─┬─substring('abc', 2)─┬─substring('abc', 1)─┐

│ ? ? ? ? ? ? 3 ??│ bc? ? ? ? ? ? ? ????? │ abc ? ? ? ? ? ? ? ? │

└───────────────┴─────────────────────┴─────────────────────┘

另外,需要注意的是,ClickHouse?中的String字符串是用单引号的。用双引号,会报错。

实例。终端执行:

select "123"

输出:

SELECT `123`

Received exception from server (version 21.12.1):

Code: 47. DB::Exception: Received from localhost:9000. DB::Exception: Missing columns: '123' while processing query: 'SELECT `123`', required columns: '123'. (UNKNOWN_IDENTIFIER)

通过报错日志:Missing columns: '123' ,可以看出,对双引号里面的内容,ClickHouse 是当成列名来处理的。

我们可以通过源码?DataTypeString.cpp 了解ClickHouse 中的?String类型与其他数据库的等价类型。相关代码如下。

void registerDataTypeString(DataTypeFactory & factory)

{

????factory.registerDataType("String", create);

????/// These synonims are added for compatibility.

????factory.registerAlias("CHAR", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NCHAR", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("CHARACTER", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("VARCHAR", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NVARCHAR", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("VARCHAR2", "String", DataTypeFactory::CaseInsensitive); /// Oracle

????factory.registerAlias("TEXT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("TINYTEXT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("MEDIUMTEXT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("LONGTEXT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BLOB", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("CLOB", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("TINYBLOB", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("MEDIUMBLOB", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("LONGBLOB", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BYTEA", "String", DataTypeFactory::CaseInsensitive); /// PostgreSQL

????factory.registerAlias("CHARACTER LARGE OBJECT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("CHARACTER VARYING", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("CHAR LARGE OBJECT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("CHAR VARYING", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NATIONAL CHAR", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NATIONAL CHARACTER", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NATIONAL CHARACTER LARGE OBJECT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NATIONAL CHARACTER VARYING", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NATIONAL CHAR VARYING", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NCHAR VARYING", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("NCHAR LARGE OBJECT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BINARY LARGE OBJECT", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("BINARY VARYING", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("VARBINARY", "String", DataTypeFactory::CaseInsensitive);

????factory.registerAlias("GEOMETRY", "String", DataTypeFactory::CaseInsensitive); //mysql

}

FixedString

FixedString 类型存储定长字符串。

用法:FixedString(N)

其中,N代表N个字节(N bytes)。

当写入FixedString类型数据的时候,如果数据字节数大于N,则会返回一个Too large value for FixedString(N)的异常。如果数据字节数小于N,则会使用NULL 字节来填充末尾字符。

在查询条件WHERE中,如果需要匹配FixedString类型的列,传入的查询参数要自行补尾部的\0,否则有可能导致查询条件失效,因为要求写入数据和查询条件都是固定字节数。

实例。查询语句:

SELECT * FROM FixedStringTable WHERE a = 'b'

不会返回任何结果。需要使用空字节来填充筛选条件。改成这样即可:

SELECT * FROM FixedStringTable?WHERE a = 'b\0'

使用FixedString类型的典型场景:

l?存储IP地址,如使用FixedString(16)存储IPV6地址二进制值。

l?存储哈希值,如FixedString(16)存储MD5的二进制值,FixedString(32)存储SHA256的二进制值。

实例。使用 toFixedString() 函数来生成 FixedString。执行 SQL:

SELECT

????toFixedString('abc', 5) AS s,

????toTypeName(s),

????length(s)

输出:

┌─s──┬toTypeName(toFixedString('abc', 5))┬length(toFixedString('abc', 5))┐

│ abc ?│ FixedString(5) ??????????????????????│ ??????????????????????????????5 ?????│

└───┴─────────────────────┴───────────────────┘

UUID

UUID?(Universally Unique IDentifier , 通用唯一标识符)是一个16字节的数字,用于标识记录。UUID也称为 GUID(全球唯一标识符)。

UUID核心特性:

l?全局时空唯一性

l?固定长度128比特,也就是16字节(1 byte = 8 bit)

l?分配速率极高,单机每秒可以生成超过1000万个UUID(实际上更高)

UUID 最初用于Apollo 网络计算系统和后来的开放软件Foundation 的 (OSF) 分布式计算环境 (DCE)。UUID规范参考:https://www.ietf.org/rfc/rfc4122.txt。

UUID 在编程语言中通常是一种SDK 提供的类型,例如Java中的UUID.randomUUID()是用SecureRandom实现的完全随机数,这是一种在密码学意义上安全的随机数,随机位越多越安全。而在 ClickHouse 中直接把它作为一种数据类型。

生成 UUID

ClickHouse提供了内置函数generateUUIDv4()?来生成 UUID。

SQL 实例:

SELECT generateUUIDv4()

输出:

┌─generateUUIDv4()───────────┐

│fa7a3205-90c2-47ba-b6db-3b1a6fd385c1│

└──────────────────────┘

从上面的结果可以看到,UUID 共有 32 位(中间的分隔符不算),它的格式为 8-4-4-4-12。UUID 类型默认值为:00000000-0000-0000-0000-000000000000。

我们可以用toTypeName() 函数看一下UUID 的数据类型。

SQL实例:

SELECT

????generateUUIDv4() AS uuid,

????toTypeName(uuid)

输出:

┌─uuid───────────────────┬─toTypeName(generateUUIDv4())─┐

│ 1a55960c-161a-493e-ba96-725ec453ae91 │ UUID ??????????????????????????????│

└───────────────────────┴──────────────────┘

字符串转 UUID

可以通过toUUID() 函数把符合格式的String字符串?(如果格式不对,会报Cannot parse UUID from String?错误) 转成 UUID 类型。

SQL 实例:

SELECT

????toUUID('1a55960c-161a-493e-ba96-725ec453ae91') AS uuid,

????toTypeName(uuid)

输出:

uuid:1a55960c-161a-493e-ba96-725ec453ae91

toTypeName(): UUID

??Tips: UUID算法简介

开放软件基金会(OSF)规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素。利用这些元素来生成UUID。

UUID是由一组32位数的16进制数字所构成,由128位二进制组成?(可能的组合数量2^128,几乎不可能生成重复项,除非你能做到每秒生成 10 亿个 UUID,并持续大约 85 年?),显示为 (128/4) = 32 个十六进制数字。一般转换成十六进制,然后用String表示。

UUID 生成算法通常有 4 个版本,如下表。

版本

算法原理

版本 1

基于时间戳+ MAC 地址

版本 2

基于时间戳+ MAC 地址 + DCE 安全版本

版本 3

基于命名空间名称。通过对命名空间标识符和名称进行散列生成。使用MD5作为散列算法。

版本 4

随机算法(random)。版本 4 UUID 生成的过程如下:1.生成 16 个随机字节(=128 位)2.根据 RFC 4122 第 4.4 节调整某些位,如下所示:3.将第 7 个字节的最高 4 位设置为 0100'B,因此高半字节为“4”4.将第 9 个字节的两个最高有效位设置为 10'B,因此高半字节将是“8”、“9”、“a”或“b”之一。5.将调整后的字节编码为 32 个十六进制数字6.添加四个连字符“-”字符以获得 8、4、4、4 和 12 个十六进制数字的块。7.输出生成的 36 个字符的字符串“XXXXXXXXX-XXXX-X1XXX-X2XXX-XXXXXXXXXXXX”

版本 5

基于命名空间名称。通过对命名空间标识符和名称进行散列生成。使用SHA-1作为散列算法。RFC 4122 建议使用版本 5 (SHA-1) 而不是版本 3 (MD5),并警告不要使用任一版本的 UUID 作为安全凭证。

上面位置 X1 的数字始终为“4”,位置 X2 的数字始终为“8”、“9”、“A”或“B”之一。典型的 UUID 字符串如下:

ccfbd0e9-e284-42be-bf62-35631ab61890

97dc2c56-210e-4d24-be4b-ec1580ec3712

ddca7525-a5f3-418b-9965-51bba972879c

c02bbcc0-f1c1-49ee-8f1c-901b10487e3f

dad54c4b-377f-4500-9dc3-f31021372d47

919b3557-e98d-4c24-bce5-e85c9dbac0b2

4317a815-9f51-46b8-8b1b-2f5f75a1cd6d

4999f513-5494-412a-8cb0-9eaebc89ebf4

1.1.4.?日期时间类型

时间数据类型有 DateTime、DateTime64、Date和Date32。其中,Date使用 2 个字节(16bits)存储,DateTime、Date32使用 4?个字节(32bits)存储,DateTime64用 8 个字节存储。

首先,我们看一下计算当前时间的 SQL:

SELECT now()

输出:

┌────now()────┐

│2022-03-01 20:23:09│

└───────────┘

获取当前时区:

SELECT timeZone()

┌─timeZone()──┐

│ Asia/Shanghai │

└─────────┘

Date

日期类型。用2个字节(16 bits)存储,表示从 1970-01-01到当前的日期值。最小值为0000-00-00。日期取值范围:[1970-01-01, 2149-06-06]。

Date类型只精确到天,并且和 DateTime、DateTime64 一样,支持字符串写入。Date 类型中没有时分秒信息,存储的日期值不带时区。

日期字符串转 Date 类型

SQL 实例:

SELECT

????toDate('2022-03-02') AS dt,

????toTypeName(dt)

FORMAT Vertical

输出:

dt: 2022-03-02

toTypeName(toDate('2022-03-02')): Date

计算当前日期距离 1970-01-01?总共多少天

SQL 实例:

SELECT

????toDate('2022-03-02') AS dt,

????toTypeName(dt),

????toInt32(dt)

FORMAT Vertical

输出:

dt: 2022-03-02

toTypeName(toDate('2022-03-02')): Date

toInt32(toDate('2022-03-02')): ???19053

dateDiff() 函数

我们可以使用dateDiff() 函数来验证上面的19053天是否一致:

SELECT dateDiff('day', toDate('1970-01-01'), toDate('2022-03-02'))

FORMAT Vertical

输出:19053

Date 的取值范围计算原理

另外,我们也可以看一下 Date 的取值范围计算原理。因为 Date 使用 2 个字节存储,也就是 16 bits,取值范围是??= 65535。我们可以用下面的 SQL 来验证:

SELECT

????dateDiff('day', toDate('1970-01-01'), toDate('2149-06-06')) AS days,

????exp2(16) - 1 AS int16val

FORMAT Vertical

输出:

days: ??65535

int16val: 65535

Date32

日期类型。支持与Datetime64相同的日期范围。存储占用4个字节。日期取值范围:[1925-01-01, 2283-11-11]。我们可以试验一下使用 Date 来赋值1925-01-01的效果。

SQL:

SELECT

????toDate('1925-01-01') AS dt1,

????toDate32('1925-01-01') AS dt2

FORMAT Vertical

输出:

dt1: 2104-06-07

dt2: 1925-01-01

可以看出,toDate32('1925-01-01') 正确解析了日期1925-01-01。而toDate('1925-01-01')解析成了2104-06-07,其背后的计算逻辑简单分析如下。

首先,我们使用 toInt32()来看看背后真正存储的值是多少:

SELECT

????toDate('1925-01-01') AS dt1,

????toDate32('1925-01-01') AS dt2,

????toInt32(dt1) AS i1,

????toInt32(dt2) AS i2

FORMAT Vertical

输出:

dt1: 2104-06-07

dt2: 1925-01-01

i1: ?49100

i2: ?-16436

这个 49100?是这个逻辑算出来的:

SELECT dateDiff('day', toDate('1970-01-01'), toDate('1925-01-01'))

输出:49100?

所以,toDate('1925-01-01')?计算逻辑就是:

SELECT addDays(toDate('1970-01-01'), 49100)

输出:2104-06-07。

DateTime

时间戳类型。存储占用4个字节(32?位)存储 Unix 时间戳。时间戳类型值精确到秒。DateTime 类型包含年、月、日、时、分、秒信息,支持使用字符串的方式写入。时间戳支持的有效值范围:[1970-01-01 00:00:00, 2106-02-07 06:28:15](UTC 时区)。最小值为0000-00-00 00:00:00。

计算DateTime 取值上限

同样的逻辑,我们可以通过具体的 SQL计算出时间戳取值范围。

SQL实例:

SELECT addSeconds(toDateTime('1970-01-01 00:00:00', 'UTC'), exp2(32) - 1)

输出:2106-02-07 06:28:15

当前时间函数 now()

当前时间函数 now()返回的是一个时间戳DateTime类型。

SQL 实例:

SELECT

????now() AS now,

????toTypeName(now)

FORMAT Vertical

输出:

now: ?2022-03-02 02:03:44

toTypeName(now()): DateTime

使用toInt32()函数计算具体秒数

使用toInt32()函数,可以计算当前时间距离1970-01-01 00:00:00具体的秒数。

SQL:

SELECT

????now() AS now,

????toInt32(now),

????toUnixTimestamp(now)

FORMAT Vertical

输出:

now: ???????????????????????2022-03-02 02:57:05

toInt32(now()): ??????????1646161025

toUnixTimestamp(now()): 1646161025

我们可以使用dateDiff() 函数来验证这个?1646161025?数字。

SQL:

SELECT dateDiff('second', toDateTime('1970-01-01 00:00:00'), toDateTime('2022-03-02 02:57:05'))

FORMAT Vertical

输出:1646161025

字符串转成DateTime

通过使用toDateTime() 函数可以把字符串转成DateTime。

SQL实例:

SELECT

????toDateTime('2022-03-02 02:03:44') AS dt,

????toTypeName(dt)

FORMAT Vertical

输出:

dt: 2022-03-02 02:03:44

toTypeName(toDateTime('2022-03-02 02:03:44')): DateTime

DateTime64

时间戳类型。支持定义亚秒精度。DateTime64类型在内部以Int64类型存储数据,值是自1970-01-01 00:00:00UTC开始的时间刻度数(ticks)。时间戳取值范围:[1925-01-01 00:00:00,2283-11-11 23:59:59.9999999],最大值的精度为8。

使用DateTime64类型

语法:DateTime64(precision, [timezone]),时间刻度的精度由precision参数确定。

实例 SQL:

SELECT toDateTime64(now(), 3, timeZone())

输出:2022-03-02 03:16:00.000

过滤 DateTime64 类型的值

与 DateTime 不同, DateTime64 类型的值不会自动从 String 类型的值转换过来,需要单独调用类型转换函数?toDateTime64() 。

实例 SQL:

SELECT * FROM t WHERE timestamp = toDateTime64('2022-03-02?03:21:05', 3, 'Asia/Shanghai')

1.2.?本章小结

数学中有整数、有理数、无理数、复数等数域的概念,而在计算机科学中,数据类型仍然是非常重要的概念——无论是在程序设计语言、算法或是数据库系统。关于类型理论的思想来源于哲学中的归类(classification)方法论,即把具有共同特点的类集合成超类的思维过程和方法。在计算机领域也有专门的类型理论研究。从最基础的类型开始,然后进行各种嵌套、组合就可以实现复杂领域模型的抽象。

ClickHouse支持多种数据类型。数据模型定义具体字段属性(例如,String、Int、Long、Double?...),每个字段的属性值,又都定义为某种具体的数据类型。本节主要介绍的是 ClickHouse基础数据类型。既然有基础类型,就有高级一点的“复合数据类型”。在下一章中,我们将会介绍 ClickHouse高级数据类型与函数主题。


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #ClickHouse #字符串转array #春秋老子道德经第四十二章在第 #2 #环境已经