成为数据治理专家:行式存储VS列式存储
行式存储
Row-based storage storesatable in a sequence of rows
常见的 TP 库,如 Oracle、DB2、MySQL、SQL SERVER 等采用行式存储法(Row-based),在基于行式存储的数据库中, 数据是按照行数据为基础逻辑存储单元进行存储的, 一行中的数据在存储介质中以连续存储形式存在。
列式存储
Column-based storage storesatable in a sequence of columns
列式存储(Column-based)是相对于行式存储来说的,常见的 AP 库如 Hbase、HP Vertica、EMC Greenplum 、 Clickhouse 等数据库均采用列式存储。在基于列式存储的数据库中, 数据是按照列为基础的逻辑存储单元进行存储的,一列中的数据在存储介质中以连续存储形式存在。
简单来说 行式存储
的存储单位是行,列式存储
的基本单位是列。似乎有点听君一席话如听一席话的感觉吧,不要打我下面我们详细介绍。
行存储、列存储对比
在数据写入上的对比
-
行存储的写入是一次完成。如果这种写入建立在操作系统的文件系统上,可以保证写入过程的成功或者失败,数据的完整性因此可以确定。
-
列存储由于需要把一行记录拆分成单列保存,写入次数明显比行存储多(意味着磁头调度次数多,而磁头调度是需要时间的,一般在1ms~10ms),再加上磁头需要在盘片上移动和定位花费的时间,实际时间消耗会更大。所以,行存储在写入上占有很大的优势。
-
还有数据修改,这实际也是一次写入过程。不同的是,数据修改是对磁盘上的记录做删除标记。行存储是在指定位置写入一次,列存储是将磁盘定位到多个列上分别写入,这个过程仍是行存储的列数倍。所以,数据修改也是以行存储占优。
行式存储的优势是写操作。基于行存的数据,在写入、更新、删除时,只需要找到这一行进行一次操作即可。
在读取方面,哪怕只想读取其中的一列都需要找到整行,再从整行中进行读取。
在数据读取上的对比
-
数据读取时,行存储通常将一行数据完全读出,如果只需要其中几列数据的情况,就会存在冗余列,出于缩短处理时间的考量,消除冗余列的过程通常是在内存中进行的。
-
列存储每次读取的数据是集合的一段或者全部,不存在冗余性问题。
-
两种存储的数据分布。由于列存储的每一列数据类型是同质的,不存在二义性问题。比如说某列数据类型为整型(int),那么它的数据集合一定是整型数据。这种情况使数据解析变得十分容易。相比之下,行存储则要复杂得多,因为在一行记录中保存了多种类型的数据,数据解析需要在多种数据类型之间频繁转换,这个操作很消耗CPU,增加了解析的时间。所以,列存储的解析过程更有利于分析大数据。
-
从数据的压缩以及更性能的读取来对比,列存储由于是同质性数据,可以针对性的进行数据压缩和查询性能提升。
如下图:首先将Customes Name列及Material列做逻辑化索引标识,查询时分别匹配Materia=Refrigerator及Customes Name=Miller的数据,然后做交叉匹配。
列式存储的优势是读取。在写入、更新、删除时,需要找到多个列极端情况下是所有的列,要对多个列分别进行操作,写入的次数变得较多。
在读取方面,列式存储非常的灵活,只需要找到要读取的某几个列即可。
代码模拟行存和列存
下面我们用简单的代码对行存储和列存储进行模拟。
模拟行存:
private static void rowBased() {
Map<Integer , Map<String , Object>> rows = new HashMap<>();
Map<String , Object> row1 = new HashMap<>();
row1.put("id" , 1);
row1.put("firstname" , "smith");
row1.put("lastname" , "joe");
row1.put("age" , 12);
Map<String , Object> row2 = new HashMap<>();
row2.put("id" , 2);
row2.put("firstname" , "jones");
row2.put("lastname" , "mary");
row2.put("age" , 12);
rows.put(1 , row1);
rows.put(2 , row2);
// 更新
// 1. 找到行,更新
Map<String, Object> targetRow = rows.get(1);
targetRow.put("lastname" , "Lee");
targetRow.put("firstname" , "DevX");
targetRow.put("age" , 22);
// 删除
row1.remove(1);
// 读取 lastname 是 mary 数据
// 如果还想读取别的列,必须在找到这一行后再进行读取
for (Map.Entry<Integer, Map<String, Object>> entry : rows.entrySet()) {
Map<String, Object> row = entry.getValue();
if ("mary".equals(row.get("lastname"))) {
System.out.println(row);
for (Map.Entry<String, Object> columns : row.entrySet()) {
if ("age".equals(columns.getKey())) {
System.out.println("age = " + columns.getValue());
}
}
}
}
}
模拟列存:
private static void columnBased() {
// key 是 id value 是对应列的值
Map<Integer , Integer> idColumns = new HashMap<>();
idColumns.put(1 , 1);
idColumns.put(2 , 2);
Map<Integer , String> firstNameColumns = new HashMap<>();
firstNameColumns.put(1 , "smith");
firstNameColumns.put(2 , "jones");
Map<Integer , String> lastNameColumns = new HashMap<>();
lastNameColumns.put(1 , "joe");
lastNameColumns.put(2 , "mary");
Map<Integer , Integer> ageColumns = new HashMap<>();
ageColumns.put(1 , 12);
ageColumns.put(2 , 16);
// 更新
firstNameColumns.put(1 , "Lee");
lastNameColumns.put(1 , "DevX");
ageColumns.put(1 , 22);
// 删除
idColumns.remove(1);
firstNameColumns.remove(1);
lastNameColumns.remove(1);
ageColumns.remove(1);
// 基于列式存储可以并行的读取多个不同的列
// 读取 lastname 是 mary 数据
for (Object value : lastNameColumns.values()) {
if ("mary".equals(value)) {
System.out.println(value);
}
}
// 读取 firstname 是 DevX 数据
for (Object value : firstNameColumns.values()) {
if ("DevX".equals(value)) {
System.out.println(value);
}
}
}
行存储的写入是一次性完成,消耗的时间比列存储少,并且能够保证数据的完整性,缺点是数据读取过程中会产生冗余数据,如果只有少量数据,此影响可以忽略;数量大可能会影响到数据的处理效率。
列存储在写入效率、保证数据完整性上都不如行存储,它的优势是在读取过程,不会产生冗余数据,这对数据完整性要求不高的大数据处理领域,比如互联网,犹为重要。查询过程中,可针对各列的运算并发执行(SMP),***在内存中聚合完整记录集,***可能降低查询响应时间;可在数据列中高效查找数据,无需维护索引(任何列都能作为索引),查询过程中能够尽量减少无关IO,避免全表扫描;因为各列独立存储,且数据类型已知,可以针对该列的数据类型、数据量大小等因素动态选择压缩算法,以提高物理存储利用率;如果某一行的某一列没有数据,那在列存储时,就可以不存储该列的值,这将比行式存储更节省空间。
Row-based | Column-based | |
---|---|---|
写入 | 每一行的所有字段都存在一起,优点:对数据进行插入和修改操作很方便 | 当一条新数据到来,每一列单独存储,缺点:插入和修改操作麻烦 |
查询 | 查询时即使只涉及某几列,所有数据也都会被读取;优点:适合随机查询;在整行的读取上,要优于列式存储;缺点:行式存储不适合扫描,这意味着要查询一个范围的数据 | 查询时只有涉及到的列会被读取;缺点:查询完成时,被查询的列要重新进行组装 |
寻道范围 | 读取数据的时候硬盘寻址范围很大 | 由于仅对需要的列进行查找,因此硬盘寻道范围小 |
索引 | 缺点:要加速查询的话需要建立索引,建立索引需要花费很多时间 | 优点:任何列都能作为索引(每一列单独存储,查询个别列的时候,可以仅读取需要的那几个列,相当于为每一列都建立了索引) |
压缩 | 缺点:不利于压缩 | 把一列数据保存在一起,而一列的数据类型相同 ;优点:利于压缩 |
空间 | 按行存储,不利于压缩,压缩比较差,占空间大 | 列式存储的时候可以为每一列创建一个字典,存储的时候就仅存储数字编码即可,降低了存储空间需求 |
聚合 | 不利于聚合操作 | 按列存储,利于数据聚合操作 |
使用场景 | OLTP(存储关系型数据,用于使用数据的时候需要经常用到数据之间的依赖关系的场景,即读取的时候需要整行数据或者整行中大部分列的数据,需要经常用到插入、修改操作) | OLAP(分布式数据库和数据仓库,适合于对大量数据进行统计分析,列与列之间关联性不强,仅进行插入和读取操作的场景) |
行式存储、列式存储的主流数据库
行式存储数据库
- MySQL
- Oracle
- SQLServer
- DB2
- PostgreSQL
行式存储数据库一般都已 OLTP 见长。
列式存储数据库
- Druid
- Kudu
- Clickhouse
- StarRocks
- Hbase
- HP Vertica
- EMC Greenplum
列式存储数据库一般都已 OLAP 见长。
行列混存数据库
- Oracle 双模式架构
对于Oracle而言,如果开启了In-Memory选项的话,会提供双模式架构:
-
行式模式:磁盘(数据文件)和高速缓存(buffer cache)中,能够快速访问记录中的所有列,适合DML,满足OLTP类型应用。
-
列式模式:根据设置加载到In-Memory Area中,适合数据分析和聚合等操作,满足OLAP类型的查询需求。
- TiDB
- Hologres
- PolarDB
本文简单介绍了行存储和列存储,对于两种存储方式来说并没有孰优孰劣,只是擅长的方向不同而已。在实际系统中往往是两者配合使用。
在做技术选型时,需要深入了解业务场景特点,进行模拟测试后,根据评测结果选择合适的技术。