ClickHouse不同数据类型物理存储

ClickHouse提供的不同数据类型供应用程序选择,那么如何选择合适的数据类型,以及最佳实践背后的原理是什么,通过阅读本文可以掌握这些。

Posted by Lance Lee on Monday, December 28, 2020

TOC

一 前言

了解数据类型的底层存储,可以进一步掌握数据类型的使用,在开发过程中可以选择正确的数据类型。但是网上的资料较少,因此专门针对数据类型的存储进行研究,希望大家看完能有所收获。

本文将对ClickHouse提供的数据类型进行一一分析,从存储的原理到底层的二进制存储一一阐述。

需要说明的是,文章虽然说的是ClickHouse数据库的底层物理存储,但是大部分同样适用于其他数据库以及内存存储。比如浮点数的IEEE754规范,基本所有数据库实现都采用该规范存储。

二 数据类型选择

  • 整数不要使用String存储,如果Int8可以存储就不要使用Int16。
  • 日期类型,使用Date或者DateTime,不要使用String。
  • 使用枚举Enum8、Enum16,存储枚举类型。
  • 浮点数有溢出问题,尽量不要使用浮点数,使用Decimal。
  • 字符串如果确定长度使用FixString,比String可以少1字节。

数据类型的选择尽量选择占用字节数少的,这样可以减少磁盘IO,提高查询速度。毕竟,查询中加载100M和1024M大小的文件查询速度肯定不一样。因此,在大多数情况下,选择字节少的数据类型肯不会错。

此处不考虑压缩,因为采用压缩算法后,数据内容相同数据类型不同,最终实际磁盘存储上两者差的不是特别多,因此查询IO阶段影响较小。但是加载到内存解压缩后,不同数据类型占用的内存大小差别非常大。

比如:1亿数据量,有一个日期字段,日期格式为yyyy-MM-dd HH:mm:ss,如果采用DateTime存储,该字段只需要4字节,1亿数据量该字段存储总量为1亿 * 4B = 400000000B = 382MB;而使用String存储,该字段需要20字节,1亿数据量该字段存储总量为1亿 * 20B = 2000000000B = 1908MB;由于是定长,可以使用FixedString(19)存储,该字段需要19字节,1亿数据量该字段存储总量为1亿 * 19B = 1900000000B = 1812MB

上述计算结果相当于内存存储,在磁盘存储时,数据库会进行压缩,最终存储时,DataTime类型可能会压缩到300MB,而String类型和FixString(19)类型可能会被压缩到400MB,这就导致最终物理存储差别不大,但是内存差别非常大。

三 数据类型存储原理

3.1 在线工具

可以使用以上工具减少计算类,提高效率。

3.2 数据类型物理存储

UInt8, UInt16, UInt32, UInt64, UInt256, Int8, Int16, Int32, Int64, Int128, Int256

以Int32举例,4字节存储,直接把数组转为二进制补码存储。其他类似。

举例:111

二进制:01101111
ClickHouse物理存储:01101111 00000000 00000000 00000000

Float32

Float32为4字节。与其他数据库相同,使用IEEE-754规范存储。

一个浮点数有3部分组成:

  • 符号部分:1bit,0 表示正,1表示负。
  • 指数部分:占用8bit的二进制数,可表示数值范围为0-255。
  • 底数部分:使用二进制数来表示此浮点数的实际值,但是最高位始终为1,所以,最高位省去不存储,在存储中占23bit科学计数法。

1位符号位 + 8位指数 + 23位底数

举例:180.5

二进制:10110100.1
科学计数法表示二进制:1.01101001 * 2^7
符号位:0
底数:01101001
指数:实际为7,必须加上127,所以为134。也就是10000110。
IEEE规范存储:0 10000110 01101001000000000000000
ClickHouse物理存储:00000000 10000000 00110100 01000011
# 从后往前读取,组成新的二进制
01000011 00110100 10000000 00000000

Float64

Float32为8字节。与其他数据库相同,使用IEEE-754规范存储。

1位符号位 + 11位指数 + 52位底数

举例:180.5

二进制:10110100.1
科学计数法表示二进制:1.01101001 * 2^7
符号位:0
底数:01101001
指数实际为7,必须加上1023,所以为1030。也就是 10000000110 。
IEEE规范存储:0 10000000110 01101001 00000000 00000000 00000000 00000000 0000
二进制:01000000 01100110 10010000 00000000 00000000 00000000 00000000
ClickHouse物理存储:00000000 00000000 00000000 00000000 00000000 10010000 01100110 01000000
# 从后往前读取,组成新的二进制
二进制:01000000 01100110 10010000 00000000 00000000 00000000 00000000 00000000

Decimal

会直接把去掉小数点,然后数值转换为二进制补码存储。读取时根据定义的精度加入小数点。

若输入数值的小数点后位数比精度少,则会补0。小数点位数过多或者值过大直接抛异常。

举例:123456.12,字段类型:Decimal(15, 2)

ClickHouse物理存储:00001100 01100001 10111100 00000000 00000000 00000000 00000000 00000000
# 从后往前,组成新的二进制
二进制:00000000 00000000 00000000 00000000 00000000 10111100 01100001 00001100
十进制:12345612

例如:定义Decimal(15, 4),但是输入123456.12345,小数点后有5位,会直接抛出异常Decimal value is too small

举例:123456.123,字段类型:Decimal(15, 4)

ClickHouse物理存储:11001110 11101000 10010101 01001001 00000000 00000000 00000000 00000000
# 从后往前读取,组成新的二进制
二进制:00000000 00000000 00000000 00000000 01001001 10010101 11101000 11001110
十进制:1234561230

可以看到,123456.123存储时会补0,存储为123456.1230

String

使用UTF8编码存储,存储时会存储为:字符串字节长度 + 字符串。

举例:20201101000000

一个英文或符号用到1个字节

字符串长度:十六进制下是OE,对应的十进制的14字节
字符串:一个英文或符号用到1个字节,与实际内容相同。

ASCII码显示20201101000000,因此存储总共需要15字节。

举例:张三

# 张
二进制:11100101 10111100 10100000
十六进制:e4b889

# 三
二进制:11100100 10111000 10001001
十六进制:e4b889
ClickHouse物理存储:00000110 11100101 10111100 10100000 11100100 10111000 10001001

总共7字节,第一个字节标明字符长度为6字节。

FixedString

FixedString是固定长度的字符串。存储与String相同,但是不需要第一个字节标识字符长度,若位数不足的会补0。

UUID

16字节,存储32个字符,每个字符取值0-f。因此每1字节存2个字符。

举例:使用generateUUIDv4函数生成一个UUID。

SELECT generateUUIDv4();
dbd5fd5b-bd57-4d3b-b0c0-932bddad3683

字符数依次为:8-4-4-4-12 前16个字符使用8字节大端模式存储。后16个字符使用8字节大端模式存储。

dbd5fd5b-bd57-4d3b
大端模式:3b 4d 57 bd 5b fd d5 db
二进制:00111011 01001101 01010111 10111101 01011011 11111101 11010101 11011011

b0c0-932bddad3683
大端模式:83 36 ad dd 2b 93 c0 b0
二进制:10000011 00110110 10101101 11011101 00101011 10010011 11000000 10110000
ClickHouse物理存储:
00111011 01001101 01010111 10111101 01011011 11111101 11010101 11011011 10000011 00110110 10101101 11011101 00101011 10010011 11000000 10110000

Date

用两个字节存储,表示从 1970-01-01 (无符号) 到当前的日期值。日期中没有存储时区信息。

举例:当前日期为2020-12-14,那么和1970-01-01间隔的天数为18610

间隔天数十进制 = 2020-12-14 - 1970-01-01 = 18610 天
二进制:01001000 10110010 
ClickHouse物理存储:10110010 01001000

从后往前读取,也就是第一个字节和第二个字节互换位置。

DateTime

真实时间:2020/12/14 10:38:30
对应(Unix时间戳):1607913510
二进制:01011111 11010110 11010000 00100110 
ClickHouse物理存储:00100110 11010000 11010110 01011111
# 从后往前读取,组成新的二进制
01011111 11010110 11010000 00100110 

Enum,Enum8,Enum16

Enum会根据枚举的Integer值自行确定是1字节,还是2字节。底层存储时存储的是有符号整形,即Int8 / Int16。

比如:Enum(‘false’ = -1, ‘true’ = 0);

# false
十进制:-1
ClickHouse物理存储:11111111

若超出范围,则抛异常

DB::Exception: Value 1111 for element 'true' exceeds range of Enum8.

Array

依次为1字节的数组大小加元素大小。例如:Array(UInt8),存储大小为1字节 + 元素个数 * 1字节。

元素存储和基本类型存储一致。

举例:array(1, 2),存储大小为3字节。

ASCII:2 1 2
二进制:00000010 00000001 00000010
ClickHouse物理存储:00000010 00000001 00000010

Tuple

Tuple存储时不需要像Array一样首字节存储数组大小,二是直接顺序存储。例如:Tuple(UInt8, FixedString(6)) ,直接存储1字节的UInt8以及6字节的String。

举例:Tuple(UInt8, String()),tuple(1, ‘a’)。 依次为1字节的数值,1字节的字符串长度 + 字符串a

ASCII:1 1 a
ClickHouse物理存储:00000001 00000001 01100001

IPv4

4字节,以点分割,每字节存一段。同样是大端模式。 举例:‘183.247.232.58’

二进制:10110111 11110111 11101000 00111010
ClickHouse物理存储:00111010 11101000 11110111 10110111

IPv6

16字节,按顺序存储

举例:‘2a02:e980:1e::1’,中间多段都是0

十六进制:2a 02 e9 80 00 1e 00 00 00 00 00 00 00 00 00 01
ClickHouse物理存储:00101010 00000010 11101001 10000000 00000000 00011110 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

「如果这篇文章对你有用,请支持一下哦」

Attack On Programmer

如果这篇文章对你有用,请支持一下哦

使用微信扫描二维码完成支付