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