ClickHouse 实战:ClickHouse 基础数据类型极简教程

Posted 东海陈光剑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ClickHouse 实战:ClickHouse 基础数据类型极简教程相关的知识,希望对你有一定的参考价值。

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')

以上是关于ClickHouse 实战:ClickHouse 基础数据类型极简教程的主要内容,如果未能解决你的问题,请参考以下文章

《ClickHouse企业级应用:入门进阶与实战》5 ClickHouse函数

《ClickHouse企业级应用:入门进阶与实战》6 ClickHouse SQL基础

《ClickHouse企业级应用:入门进阶与实战》6 ClickHouse SQL基础

《ClickHouse企业级应用:入门进阶与实战》1 全面了解ClickHouse

极富参考价值!第1章 ClickHouse 简介《ClickHouse 企业级大数据分析引擎实战》...

《ClickHouse企业级应用:入门进阶与实战》8 基于ClickHouse Bitmap实现DMP用户画像标签圈人