我是镜镜呀
发布于

OushuDB场景化实践系列(三):数据是如何存储的?

我们都知道,现在计算机的应用中,大多将数据存储到数据库中,但不知道大家是否有这样的一些疑问:

  • 为什么要存到数据库中?

  • 存在数据库的优势在哪里?

  • 是否有必要,将数据存在数据库中?

这就要,先来了解一下文件是如何存储在计算机中了。

文件存储与数据库

我们知道,存储介质大概可以分为:

  • 主存:存储量小,访问快
  • 磁盘:存储量大,访问慢

自然,如果我们想存储大量的数据,存储在磁盘上的成本是我们可以接受的。问题的关键点就变成了:

设计一个高效的磁盘数据结构

从应用开发者的角度看,访问主存几乎是透明的,而对于磁盘的访问,是通过系统调用,感谢虚拟机内存机制的存在,让我们不用手动管理偏移量,我们只要指定目标文件内的相对偏移量,就可以从磁盘上的形式,解析成适合主存的形式

我们需要设计出一种易于构造、修改和解析的文件格式。构建这样的一种文件结构,就像在内存模型的语言中构建数据结构。和我们熟知的 C 语言、Java 一样,我们期望它能自动处理好内存分配、垃圾收集和碎片的问题,让我们更聚焦在数据的使用、分析上,而不是要考虑是否有连续的内存段、内存释放后有什么影响等问题上。

很简单的一个问题:数据在计算机中,是以什么形式存储的?当然是二进制编码啦!

  • 将数据变为二进制的这个过程,叫做序列化

  • 相反,将二进制变为原本的数据,叫做反序列化

所以,为了数据的高效存储,我们需要将其编码成一种紧凑的、方便进行序列化反序列化的格式。

而在数据记录成页之前,我们需要知道一个非常关键的问题:以什么样的结构存储数据?

  • 如何以二进制的形式表示键、数据记录?
  • 如何将多个值组成更复杂的结构?
  • 如何实现可变长度的数据类型和数组?

数据类型

键和值,有很多类型,并且可以用二进制形式表示。(不记得的如何用二进制表示数据的童鞋,建议回顾一下计算机组成相关内容)

  • 大多数的数值类型,用固定大小的值表示
  • 多字节数值,需要在编解码时,使用相同的字节序

数据记录,由原始数据类型,及它们的组合构成。

  • 数值
  • 字符串
  • 布尔值

数值类型

不同的数值类型,可能大小不同(一个字节 byte,是 8 比特位):

  • 短整型:2byte = 16 位
  • 整型:4byte = 32 位
  • 长整型:8byte = 64 位

浮点数,也就是小数,由符号、小数、指数这三部分构成。(类似科学计数法的表示方法,一般使用的是 IEEE 二进制浮点算术标准)。

大多数语言中,都在标准库内置了编码和解码浮点数的方法。

下面,我们将学习一下偶数 DB 中,支持的数值类型,以及相关的细节问题。

整数:smallint、integer、bigint

名字 存储长度 描述 范围
smallint 2 字节 = 16 位 小范围整数 -2^15 ~ 2^15-1 (有一位是符号位)
-32768 到 32767
integer 4 字节 = 32 位 常用的整数 -2^31 ~ 2^31-1
-2147483648 到 +2147483647
bigint 8 字节 = 64 位 大范围的整数 -2^63 ~ 2^63-1
-9223372036854775808 到 +9223372036854775807

bigint 类型,依赖于编译器,是否支持 8 字节整数,所以不一定能在所有平台上使用。如果不支持,bigint 类型将退化成和 integer 一样,但依旧占用存储的 8 字节。

除了上述的 3 种之外,int2、int4、int8 之类的,可也在数据库系统中使用。

浮点数:real、double precision

名字 存储长度 描述 范围
real 4 字节 = 32 位 可变精度,不精确 6 位十进制数字精度
double precision 8 字节 = 64 位 可变精度,不精确 15 位十进制数字精度
decimal 可变长 用户指定的精度,精确 小数点前 131072 位,
小数点后 16383 为
numeric 可变长 用户指定的精度,精确 小数点前 131072 位,
小数点后 16383 位

浮点数最广为诟病的问题,就是精度问题。不精确 == 以近似值存储存入的数值和取出的数值,可能不一样。

所以,在要求精确计算的场景,比如货币金额,就需要使用任意精度的数值类型,并指定其精度,进行精确的存储和计算。

写法一:NUMERIC(精度(全部数据位的数目)precision,标度(小数部分的位数)scale)

比如 9.99 元,精度为 3,标度为 2。,小数点不占位,这是一个类型 Numeric(3,2)的数据。

写法二:Numeric(精度 precision) // 省略标度

小数点后为 0 时,标度可以直接省略。

写法三: Numeric // 不带精度,也不带标度

这样,是创建了一个,可以实现精度上限的任意精度、标度的数值。

总结:一般推荐在使用 Numeric 时,明确声明精度和标度,保证兼容性和一致性。

字符串和可变长数据

原始的数值类型,有固定的大小。构造复杂的值,类似于 C 语言中的结构体 struct。

将原始值,组合到结构体中,并使用固定长度的数组,或是使用指针志向其他的内存区域。

字符串、可变长的数据类型,可以序列化为:

长度 size + size 个字节

size 个字节,才是实际的数据。对于字符串来说,这样的形式被称为 UCSD 字符串,或是 Pascal 字符串(Pascal 本身也是一种编程语言,以其流行实现而命名)。

// 伪代码表示
String {
	size uint_16
    data byte[size]
}

在偶数 DB 中,支持以下的字符串类型

名字 长度限制 是否定长 备注
character(n), char(n) 不补空白
character varying(n), varchar(n) × 当存储的字符长度,小于定长时,
会使用空白对字符串进行补齐
text × × 无长度限制,可以存储任意长度的字符串
  • varchar(N):长度可以变化,当你的长度小于 N 时,不会有 N 个存储空间,只有你的实际长度加一,加一是因为额外使用一位来保存的长度,这是在 N 小于 255 的前提下
    如果 N 大于 255,则将使用额外的两位来保存长度。

布尔值

这里也给大家区分几个类似的概念:

  • 布尔值:可以用单个字节表示,true 编码为 1,false 编码为 0
  • 枚举值:可以表示为整数,常常用于二进制格式、通信协议,表示重复多、基数少的值
  • 标志:打包的布尔值和枚举值的组合,表示多个非互斥的布尔值参数,类似于子网掩码的作用。比如,表示值是定长还是变长的,是否存在溢出页。(不懂可跳过,不是重点)

对于布尔值,只有两个取值,一般只占一个位。如果要用一个完整的字节存储,8 个位,就太浪费。所以,我们常常将每 8 个布尔值,合成一批,每个布尔值只占一位。

在 OushuDB 中,布尔类型 boolean 的存储,也是只占用 1 字节。

真值的表示为:

  • TRUE
  • true
  • 'true'
  • '1'
  • 'y'
  • 't'

相应的,对于假可以使用:

  • FALSE
  • ‘no'
  • '0'
  • 'false'
  • 'n'
  • 'f'

日期类型

名字 存储空间 描述 范围
日期 date 4 字节 = 32 位 只用户日期 4713 BC 至 5874897 AD
时间 time [(p)] [ without time zone ] 8 字节 = 64 位 只用于一日内时间 00:00:00 至 24:00:00
time [(p)] with time zone 12 字节 带时区 00:00:00+1459 至 24:00:00-1459
时间/
日期
timestamp [( p )] [without time zone] 8 字节 日期和时间(无时区) 4713 BC 至 294276 AD
timestamp [( p )] with time zone 8 字节 有时区 4713 BC 至 294276 AD

面向列与面向行的数据库

表格大家都看过吧~~~

image-20230102210756299

每一个格子,都是行和列的交集。

  • 属于同一列的字段,通常具有相同的数据类型
    • name 列,都是字符串类型的
    • price 列,都是数值类型的
  • 在逻辑上,属于同一条数据记录的值,构成一行

表的存储,可以

  • 水平分区:将属于同一行的值存储在一起
  • 垂直分区:同属于一列的值,存储在一起

image-20230102211014743

面向行

磁盘之类的持久性存储介质中,数据通常是按块访问的,单个块可能包含某行中所有的记录数据。如果我们想要访问某个用户的所有信息,那么这样的布局就非常方便。

在需要按行访问数据的情况下,面向行的存储最有用。整行存储,可以提高空间局部性。

(空间局部性:如果访问一处存储,则其附近的其他存储区域,也会在不久的将来被访问)

适用场景:

  • 数据记录由多个字段组成
  • 某个键,作为唯一标识
  • 经常按行记录进行读取,比如用户的信息
  • 经常单独修改某个字段

但是,如果我们想要获取多个用户的单个信息,比如手机号,就会让开销更大,因为其他字段在访问该块的时候,也会被读入。这个时候,就适合使用面向列的存储方式。

面向列

同一列的值,被连续地存储在磁盘上。不同列的值,存储在不同的文件,或文件段中。在读取的时候,同一列的值,可以一次性地读取出来,而不是对整行进行读取后,再丢弃不要的列。

适用场景:

  • 计算聚合的分析型工作负载,比如查找趋势、计算平均值等

对于不断增长数据集,复杂分析查询的需求,出现了很多面向列的文件格式:

  • Apache Parquet
  • Apache ORC
  • RCFile

面向列的存储:

  • Apache Kudu
  • ClickHouse

行和列的区别,只是存储方式不同吗?

当然不是,这只是我们对查询所做的优化之一。

区别二:缓存利用率、计算效率

在现代化 CPU 上(不是我们上学的时候学的古董 CPU),向量化指令可以使单条 CPU 指令,一次处理多个数据点。所以在一次读取中,从同一列中读取多个值,可以显著提高缓存利用率,和计算效率。

区别三:提高压缩率

数字和数字在一起,字符串和字符串在一起,根据不同的类型使用不同的压缩方法,每种情况选择最有效的压缩方法。

在 OuShuDB 中,也提供了不同的压缩方式,我们在创建表的时候,可以指定压缩方式。

总结:在决定使用那种存储之前,我们需要知道我们的访问方式。

创建表时,指定存储方式

image-20230103225439600

oushuDB 支持按行存储、按列存储、行列混合存储的方式。在创建表的时候,就可以进行指定,还可以同时指定压缩类型:

# 显式地指定row、orc
# 默认创建orc表(我测试时,默认创建的是orc)
CREATE TABLE goods (
	id int
) with (appendonly = true, orientation = row); # 显式地指定存储格式:row、orc

# 指定压缩类型、AO表 
# 如果不指定压缩类型的话,默认不压缩
CREATE TABLE goods (
	id int
) with (appendonly = true, orientation = row, compresstype = snappy) 

# 创建一个magma表
# magma 内部自动实现了压缩,自动实现了什么压缩?待确定。
CREATE TABLE goods(
	id int
) format 'Magma';

宽列式存储

列 ——> 列族 ,每个列族中,数据被逐行存储。

比如,一个 WebTable 表,存储这样的一些信息:

  • 网页内容
  • 属性
  • 它们之间的关系
  • 页面 url 标识
  • ....

可以表示为一个嵌套的映射:

存储的时候:

在偶数 DB 中,支持多种存储格式:AO、ORC、Magma。

现在 AO 不支持,支持 row。

重构 goods 表

重新设计字段

要设计一张表,首先要考虑两个问题:

  • 存储什么样的信息?
  • 访问形式是什么样的?

考虑存储方式

goods 商品表,存储商品的信息,访问时,需要一条记录一条记录的访问,根据 id 或是 isbn 号,查询相应的商品价格,卖出或是删减库存等操作。所以,对于 goods 这样的表,我们应该按行存储、访问。

但是,在 OushuDB 中,按行存储还不支持数据的更新删除,所以我想,要不要用 magma 存储。

同样经过测试,magma 不支持默认设置,所以,我又试了 orc 存储,orc 目前支持的比较全面。

所以,最后我们选择 orc 来建表满足我们的需求。

重新设计字段

还记得 goods 表中的字段吗?我们重新设计一下吧~~~

字段 类型 是否必填 缺省值 说明
title varchar(255) Y 商品名称
purchase_price numeric(10,2) Y 商品进价
sell_price numeric(10,2) Y 商品售价
stock integer 0 商品库存
isbn varchar(26) 商品 ISBN 号
unit varchar(6) 单位
spec varchar(20) 规格
brand varchar(255) 品牌
supplier varchar(255) 生产厂商
made_in varchar(255) 生产地
create_time timestamp now() 创建时间
update_time timestamp now() 更新时间
delete_time timestamp now() 删除时间
  • 删除旧表
# 查看数据库内,现有的资源
# goods是我们之前创建的表,id_increment是我们创建的自增函数
# 同时还可以看到,goods的存储方式是orc,自增函数的存储方式是heap,也就是堆
happymarket=# \d
                  List of relations
 Schema |     Name     |   Type   |  Owner  | Storage 
--------+--------------+----------+---------+---------
 public | goods        | table    | gpadmin | orc
 public | id_increment | sequence | gpadmin | heap
(2 rows)

# 删除表
happymarket=# drop table goods;
DROP TABLE

# 再次查看,可以看到goods表已经删除了。
happymarket=# \d 
                  List of relations
 Schema |     Name     |   Type   |  Owner  | Storage 
--------+--------------+----------+---------+---------
 public | id_increment | sequence | gpadmin | heap
(1 row)

创建新表

我们现在来创建完整的 goods 表:

# 可复制直接执行,下一段给具体的注释
CREATE TABLE goods(
	id int,
	title varchar(255) NOT NUll,
	purchase_price numeric(10,2) NOT NULL,
	sell_price numeric(10,2) NOT NULL,
	stock int DEFAULT 0,
	isbn varchar(26),
	unit varchar(6),
	spec varchar(20),
	brand varchar(255),
	supplier varchar(255),
	made_in varchar(255),
	create_time timestamp DEFAULT now(),
	update_time timestamp DEFAULT now(),
	delete_time timestamp) with (appendonly = true, orientation = orc);

happymarket=# 
CREATE TABLE goods(  # 创建goods表
	id int,
	title varchar(255) NOT NUll, # NOT NULL 表示该字段不允许为空
	purchase_price numeric(10,2) NOT NULL,
	sell_price numeric(10,2) NOT NULL,
	stock int DEFAULT 0,
	isbn varchar(26),
	unit varchar(6),
	spec varchar(20),
	brand varchar(255),
	supplier varchar(255),
	made_in varchar(255),
	create_time timestamp DEFAULT now(), # 设置默认值为当前的时间
	update_time timestamp DEFAULT now(),
	delete_time timestamp
)with(appendonly = true, orientation = orc); # 设置存储格式为row


CREATE TABLE
happymarket=# \d
                    List of relations
 Schema |     Name     |   Type   |  Owner  |   Storage   
--------+--------------+----------+---------+-------------
 public | goods        | table    | gpadmin | append only
 public | id_increment | sequence | gpadmin | heap
(3 rows)

导入一些数据

进了一些货(批量插入数据)

insert into goods (id, isbn, title, spec,unit,purchase_price, sell_price, brand, supplier, made_in)values
(1,6901121300298,'500g万丽厕精','500g','瓶',4.5,9,'万丽','广州市浪奇实业股份有限公司','广州'),
(2,6901294171206,'六神清凉爽肤沐浴露200ml','200ml','瓶',9.9,19.8,'六神','上海家化联合股份有限公司','上海'),
(3,6901294171213,'六神清凉爽肤沐浴露450ml','450ml','瓶',17.5,35,'六神','上海家化联合股份有限公司','上海'),
(4,6901294171381,'六神百合除菌香皂','125克','块',4.3,8.6,'六神','上海家化联合股份有限公司','上海'),
(5,6901294171794,'六神除菌香皂(金盏菊)125g','125g','块',4.2,8.4,'六神','上海家化联合股份有限公司','上海'),
(6,6901294172159,'六神冰凉超爽沐浴露200ml','200ml','瓶',10.5,21,'六神','上海家化联合股份有限公司','上海'),
(7,6901294177017,'六神清凉舒爽祛痱止痒花露水195ml','195ml','瓶',15,30,'六神','上海家化联合股份有限公司','上海'),
(8,6901294177024,'六神清凉舒爽祛痱止痒花露水95ml','95ml','瓶',10,20,'六神','上海家化联合股份有限公司','上海'),
(9,6901333290660,'南方黑芝麻糊','360克','袋',9.9,19.8,'南方黑芝麻','南方黑芝麻集团股份有限公司','广西'),
(10,6901377002007,'莲花味精','200克','包',3.6,7.2,'莲花','莲花健康产业集团股份有限公司','郑州'),
(11,6901377004001,'莲花味精','400克','包',7.2,14.4,'莲花','莲花健康产业集团股份有限公司','郑州'),
(12,6901377005008,'99%莲花味精','500g/500g','袋',10.9,21.8,'','莲花健康产业集团股份有限公司','河南'),
(13,6901435907015,'精制沱牌福酒35°','','瓶',4.5,9,'','舍得酒业股份有限公司','四川'),
(14,6901668053893,'奧利奧迷你原味小餅乾','55G','桶',5.5,11,'奧利奧','亿滋食品企业管理(上海)有限公司','江苏'),
(15,6901668053916,'奧利奧迷你巧克力餅乾','55G','桶',7,14,'奧利奧','亿滋食品企业管理(上海)有限公司','江苏'),
(16,6901668054012,'多种口味任选儿童宝宝休闲零食办公室小吃','106g','盒',23,46,'奥利奥','亿滋食品企业管理(上海)有限公司','江苏'),
(17,6901668054029,'奥利奥缤纷双果味夹心饼干树草莓+蓝莓味106g盒装','106g/106g','包',6.5,13,'奥利奥','亿滋食品企业管理(上海)有限公司','江苏'),
(18,6901668054050,'奥利奥130G夹心原味','130g','包',20,40,'奥利奥','亿滋食品企业管理(上海)有限公司','江苏'),
(19,6924097901160,'黑金刚大蘑头小蘑菇松脆小饼干(普通饼干~香浓巧克力)33298_45克','45g','盒',3.6,7.2,'黑金刚','天津市黑金刚食品有限公司','天津'),
(20,6924097901351,'黑金刚卡奇脆巧克力','4983_33克','块',1,2,'','天津市黑金刚食品有限公司','天津'),
(21,6924097901368,'黑金刚14g可可来代可可脂巧克力','*1/件','块',5.5,11,'','天津市黑金刚食品有限公司','天津'),
(22,6924254673381,'什锦水果罐头(岭南杂果)','700g/瓶','瓶',14.9,29.8,'','湛江市欢乐家食品有限公司','广东'),
(23,6924254673428,'欢乐家桔子罐头','700g','瓶',14,28,'','湛江市欢乐家食品有限公司','广东'),
(24,6924254673503,'糖水杨梅罐头','700g','瓶',13.5,27,'欢乐家','湛江市欢乐家食品有限公司','广东'),
(25,6924254673572,'糖水桃罐头(久宝桃)','700g','瓶',8.2,16.4,'欢乐家','湛江市欢乐家食品有限公司','广东'),
(26,6924534823277,'华发秀色12CM双层碗HF-056','','个',19.9,39.8,'','潮州市潮安区彩塘华丰不锈钢制品厂','福建'),
(27,6924583225206,'博远衣架','','包',20.9,41.8,'','西华县龙义和调味品厂','河南'),
(28,6924583291690,'方便胡辣汤','','包',21.9,43.8,'','西华县龙义和调味品厂','河南');

我们每种商品,进了 100 个,所以为每种商品,设置库存为 100

Update goods Set stock=100;

更新表中的数据的方法:

UPDATE goods SET title = '新title' where id = 1; # where条件下修改
UPDATE goods SET sell_price = sell_price + 1;  # 执行计算修改

# 同时更新多个字段
UPDATE goods SET sell_price = 12, purchase_price = 9 where id = 1

ps: 在实际操作中,创建了三次表,不同的存储格式:

  • row 不支持更新
  • magma 不支持默认值、缺省值设置

记录商品卖出记录

有了商品,我们的超市可以开张了,当顾客购买了一件商品时,需要进行相关的记录,并执行减库存的操作。

对于相关的记录表,我们的需要大概如下:

  • 不需要更改
  • 需要经常查询,做分析

所以,我们依然选择 orc 存储。

字段设计

我们设计这个表的时候,是为了查询的时候能快捷方便,所以设计了按列存储。

但是,同样对于商品的信息,比如生产商,已经在 goods 表中存储过了,就形成了冗余存储。

  • 如果不冗余存储,在查询的时候,goods 表也是按列存储的,按行查就很慢
  • 如果冗余存储,每一条记录都会多存一些信息,积少成多

对此,为了满足高效查询的要求,就选择冗余存储吧。希望在按行存储时,能支持更新操作。

字段 类型 商品名称
id bigint 记录 id,订单 id
isbn varchar(26) 商品 ISBN 号
goods_id int 商品 id
title varchar(255) 商品名称
purchase_price numeric(10,2) 商品进价
sell_price numeric(10,2) 商品售价
unit varchar(6) 单位
spec varchar(20) 规格
brand varchar(255) 品牌
supplier varchar(255) 供应商
made_in varchar(255) 产地
sell_time timestamp 售出时间

创建表

CREATE TABLE orders(
	id bigint,
    goods_id int,
	title varchar(255) NOT NUll,
	purchase_price numeric(10,2) NOT NULL,
	sell_price numeric(10,2) NOT NULL,
	isbn varchar(26),
	unit varchar(6),
	spec varchar(20),
	brand varchar(255),
	supplier varchar(255),
	made_in varchar(255),
	sell_time timestamp DEFAULT now()) with (appendonly = true, orientation = orc);

# 查看现在,表的情况

happymarket=# \d
              List of relations
 Schema |  Name  | Type  |  Owner  | Storage 
--------+--------+-------+---------+---------
 public | goods  | table | gpadmin | orc
 public | orders | table | gpadmin | orc
(2 rows)

happymarket=# \d+ orders
                               Orc Table "public.orders"
     Column     |            Type             |   Modifiers   | Storage  | Description 
----------------+-----------------------------+---------------+----------+-------------
 id             | bigint                      |               | plain    | 
 goods_id       | integer                     |               | plain    | 
 title          | character varying(255)      | not null      | extended | 
 purchase_price | numeric(10,2)               | not null      | main     | 
 sell_price     | numeric(10,2)               | not null      | main     | 
 isbn           | character varying(26)       |               | extended | 
 unit           | character varying(6)        |               | extended | 
 spec           | character varying(20)       |               | extended | 
 brand          | character varying(255)      |               | extended | 
 supplier       | character varying(255)      |               | extended | 
 made_in        | character varying(255)      |               | extended | 
 sell_time      | timestamp without time zone | default now() | plain    | 
Has OIDs: no
Options: appendonly=true, orientation=orc
Table Bucket Number: 8
Distributed randomly

happymarket=# 

总结

现在,我们来总结一下,相关的命令:


评论
    test