数据库工作负载:
OLTP(联机事务处理):操作速度快、执行时间短、操作重复性强,且查询通常比较简单,每次仅针对单个实体进行操作。OLTP 工作负载通常处理的写操作多于读操作,并且每次仅读取或更新少量数据。
OLAP(联机分析处理):OLAP 工作负载的特点是:查询运行时间长、逻辑复杂,且需要读取数据库的大部分内容。在 OLAP 工作负载中,数据库系统通常负责对从 OLTP 端收集到的现有数据进行分析,并从中推导出新的数据(结论)。
HTAP(混合事务与处理分析):HTAP 是近年来变得流行的一种新型工作负载类型。在这种模式下,OLTP 和 OLAP 工作负载在同一个数据库上共同存在。
存储模型:
N步存储模型(NSM):在 N 步存储模型中,DBMS 将单个元组的所有属性物理地连续存储在单个页面中。这种方法非常适合以插入为主 (insert-heavy) 且事务往往只针对单个实体进行操作的 OLTP 工作负载。之所以理想,是因为只需一次取值(Fetch)就能获取单条元组的所有属性。
-
优点:
-
插入、更新和删除速度快。
-
对于需要获取整个元组的查询非常友好。
-
-
缺点:
- 对于扫描表的大部分内容和/或仅扫描属性子集(部分列)的查询效率较低。
分解存储模型(DSM):在分解存储模型中,DBMS 将所有元组的单个属性(列)连续存储在一个数据块中。因此,它也被称为列存储 (Column Store)。该模型非常适合 OLAP 工作负载,这类负载包含许多只读查询,且需要对表的属性子集进行大规模扫描。
-
优点:
-
减少了浪费的 I/O 量,因为 DBMS 只读取该查询所需的数据。
-
由于增强了局部性(Locality)和缓存数据复用,查询处理性能更好。
-
数据压缩效果更好。
-
-
缺点:
- 对于点查询(Point Query)、插入、更新和删除操作较慢,因为涉及元组的拆分/缝合(Splitting/Stitching)。
元组重组:当使用列存储时,为了将元组重新组合在一起,有两种常用的方法:
-
固定长度偏移量 (Fixed-length offsets):这是最常用的方法。在给定的某一列中,其特定偏移量处的值,与另一列中相同偏移量处的值属于同一个元组。因此,该列中的每一个值都必须具有相同的长度。
-
嵌入式元组 ID (Embedded tuple ids):这种方法较少见。对于列中的每个属性,DBMS 都会随之存储一个元组 ID(例如:主键)。然后,系统还会存储一个映射表,告诉它如何跳转到具有该 ID 的每个属性。注意,这种方法存储开销很大,因为它需要为每个属性条目都存储一个元组 ID。
混合存储模型(PAX):在混合存储模型中,DBMS在数据库页面内对属性进行垂直分区,这样做的目的是在保留行存储的空间局部性优势的同时,获得列存储的高效处理能力。在PAX中,行被水平划分为行组,在每个行组内部,属性被垂直划分为列,每个行组对于其所包含的行子集来说,就像是一个微型的列存储。PAX文件有一个全局头部,包含一个记录文件内各行组偏移量的目录,每个行组也维护自己的头部,记录有关其内容的元数据。
数据库压缩:由于磁盘I/O几乎始终是性能的主要瓶颈,压缩技术在基于磁盘的DBMS中得到了广泛应用,它在具有只读分析型负载的系统中尤为流行,如果元组事先经过压缩,DBMS就能一次性获取更多有用的元组,代价是压缩和解压会带来更大的计算开销。
内存数据库(In-memory DBMS)的情况则更为复杂,因为它们不需要从磁盘获取数据来执行查询。虽然内存比磁盘快得多,但压缩数据库可以降低对 DRAM(随机存取存储器)的需求并减少处理量。它们必须在速度与压缩比之间取得平衡。压缩数据库不仅能节省内存,还可能在查询执行期间降低 CPU 成本。
如果数据集完全由随机位组成,则无法进行压缩,然而,现实世界的数据集具有一些利于压缩的关键属性:
-
属性值分布高度倾斜(例如布朗语料库中的齐夫分布,即少数数据出现的频率极高,大多数数据出现的频率极低,压缩算法可以用短编码代表高频数据来节省巨大空间)。
-
同一元组的属性之间具有高度相关性(例如邮政编码与城市、下单日期与发货日期)。
基于此,我们希望数据库压缩方案具备以下特性:
-
必须生成定长值。唯一的例外是存储在独立池中的变长数据。这是因为 DBMS 应当遵循字对齐,并且能够通过偏移量访问数据。
-
允许 DBMS 在查询执行过程中尽可能推迟解压,即后期物化------这也使列存数据库高性能的原因之一,如果可能,DBMS会直接在压缩的数据上进行过滤或聚合,而不是先全部解压成原始元组。
-
必须是无损方案,因为用户不喜欢丢失数据。任何形式的有损压缩都必须在应用层完成。
压缩粒度:我们想要压缩的数据类型很大程度上影响了可以选择的压缩方案,压缩粒度通常可以分为四个级别。
-
块级 (Block Level):对同一张表的一块元组进行压缩。
-
元组级 (Tuple Level):压缩整个元组的内容(仅限 NSM 行存模型)。
-
属性级 (Attribute Level):压缩单条元组中的单个属性值。可以针对同一元组的多个属性。
-
列级 (Columnar Level):对存储在多个元组中的一个或多个属性的多个值进行压缩(仅限 DSM 列存模型)。这允许使用更复杂的压缩方案。
朴素压缩:DBMS 使用通用算法(如 gzip、LZO、LZ4、Snappy、Brotli、Oracle OZIP、Zstd)对数据进行压缩。尽管有多种压缩算法可供选择,但工程师通常会选择那些压缩率较低但压缩/解压速度更快的算法。
MySQL InnoDB 是使用朴素压缩的一个典型例子。DBMS 对磁盘页面进行压缩,将它们填充到 2KB 的整数倍,然后存储到缓冲池中。然而,每当 DBMS 尝试读取或修改数据时,缓冲池中的压缩数据必须先进行解压。
由于访问数据需要先解压,这限制了压缩方案的作用域,如果目标是将整张表压缩成一个巨大的数据块,使用朴素压缩方案将是行不通的,因为每次访问都需要对整张表进行压缩/解压,而朴素算法需要从文件的开头解压,直到还原出你需要的位置的数据。因此,由于压缩作用域有限,MySQL 会将表拆分成较小的块。
另一个问题是,这些朴素方案不考虑数据的高层含义或语义。算法既不了解数据的结构,也不了解查询计划如何访问数据。因此,这消除了利用后期物化的机会,因为 DBMS 无法判断何时可以推迟数据的解压。
列式压缩
运行长度编码:RLE将单列中相同值的运行(连续出现的实例)压缩为三元组。
-
属性的值
-
该值在列段中的起始位置
-
该运行中包含的元素数量
DBMS应当事先对列进行智能排序,以最大限度地增加压缩机会。排序可以将重复的属性聚集在一起,从而提高压缩率。需要注意的是,RLE的有效性在很大程度上取决于底层数据的特征(例如每个数据中属性的数量和频率)。
位填充编码:当一个属性的所有值都小于该属性声明的最大大小时,使用更少的比特位来存储它们。
多数派编码:这是位填充的一种变体,它使用一个特殊的标记来指出某个值何时超过了最大大小,并维护一个查找表来存储这些异常值。
位图编码:DBMS 为特定属性的每个唯一值存储一个单独的位图,向量中的偏移量对应于一个元组。位图中的第 i 个位置对应于表中的第 i 个元组,用于指示该值是否存在。位图通常被分割成块,以避免分配大块的连续内存。
这种方法仅在值的基数(即唯一值的数量)较低时才切实可行,因为位图的大小与属性值的基数成线性正比。如果值的基数很高,位图的大小可能会超过原始数据集。
增量编码:与其存储精确值,不如记录同一列中前后相邻值之间的差异。基准值可以内联存储,也可以存储在独立的查找表中。我们还可以对存储的增量使用 RLE,以获得更好的压缩率。
增量前缀编码:这是增量编码的一种类型,其中记录了公共的前缀或后缀及其长度,从而不需要重复存储。这种方法在处理有序数据时效果最好。
字典压缩:最常见的数据库压缩方案是字典编码。DBMS 用更短的代码替换数值中频繁出现的模式。随后,它仅存储这些代码以及将这些代码映射回原始值的属性结构(即字典)。字典压缩方案需要支持快速的编码/解码以及范围查询。
-
编码与解码:字典需要决定如何编码(将未压缩的值转换为压缩形式)和解码(将压缩值转换回原始形式)。因此,不可能使用哈希函数(因为哈希不保序)。
-
保序性:编码后的值需要支持与原始值相同的排序顺序(即保序编码)。这确保了在压缩数据上运行的压缩查询所返回的结果,与在原始数据上运行的未压缩查询的结果是一致的。这种保序特性允许直接在代码上执行操作(如比较)。