趋近智
数据库的性能从根本上受数据在存储介质上物理排列方式的制约。尽管SQL抽象了数据获取过程,数据库引擎在处理请求时最终都必须从磁盘或内存读取数据块。这些数据块的排列方式决定了一个系统是擅长处理单个事务还是汇总数百万条记录。
要明白分析系统为何偏爱列式存储,我们必须考察I/O传输单位。数据库并非读取单个行或单元格;它们读取的是数据块(或页)。一个典型数据块大小,根据系统配置,通常介于4KB到32KB之间。
当一个查询请求单个数据点时,引擎会将包含该点的整个数据块加载到内存中。数据库架构的效率主要由在此操作中加载的有用数据与总加载数据之比来衡量。
在行式数据库中(如PostgreSQL、MySQL或SQL Server),数据按行顺序存放。如果有一个Sales表,包含Date、Product_ID、Store_ID和Revenue列,存储引擎会先写入第一个事务的数据,紧接着就是第二个事务的数据。
考量三个事务的内存排列:
内存布局图示了行式系统如何交错存储属性。行1的所有属性都彼此紧邻存放。
这种布局非常适合操作型(OLTP)工作负载。当用户登录或查看其具体的订单历史时,查询通常是这样的:
SELECT * FROM Sales WHERE Transaction_ID = 105;
数据库寻找包含事务105的特定数据块。由于数据是行式存储,引擎可以在一次I/O操作中获取该事务的日期、产品、商店和收入信息。这对于“点查询”非常有效,其目的是获取特定实体的所有相关信息。
当目标转向分析时,性能表现会大幅改变。分析查询很少关注单个事务。它们通常是扫描数据区域以计算聚合结果。
考量一个计算总收入的查询:
SELECT SUM(Revenue) FROM Sales;
在上面所示的行式布局中,数据库必须读取包含销售数据的每个数据块。然而,对于加载的每个数据块,只有Revenue(收入)这个整数是有用的。Date(日期)、Product_ID(产品ID)和Store_ID(商店ID)都被加载到内存中,占用了带宽和缓存空间,但最终却被废弃。
如果Revenue列只占总行宽度的5%,那么95%的I/O吞吐量就被浪费了。这种低效是操作型数据库在数据仓库工作负载下表现不佳的主要原因。
列式存储通过改变物理布局来解决这种低效问题。它不是按行存储数据,而是按列存储。Date列的所有值都顺序存放,随后是所有Product_ID值的独立存储区域,以此类推。
这种结构变化意味着,一个计算收入总和的查询只需访问包含Revenue数据的特定数据块。引擎可以完全不理会包含日期、产品或客户备注的数据块。
列式布局将属性分离到不同的存储块中。一个仅请求收入的查询,只会访问紫色块。
我们可以用数学方式定义性能差异。设N为表中的行数,W为行的平均字节宽度。
在行式存储中,全表扫描需要读取的总数据量Vrow为:
Vrow=N×W
在列式存储中,如果我们只用到列的一个子集C,其组合宽度为wc,那么扫描的数据量Vcol是:
Vcol=N×wc
由于分析查询通常只选择列的一个小部分(通常是50多列中的2到5列),所以wc明显小于W。
比如,如果一个表有50列,总宽度为500字节,而我们只需要1列,其宽度为4字节:
下图显示了随着选择列数增加,扫描时间所受的影响。
扫描时间对比。行式存储(红色)由于读取整行数据,不管选择多少列,都会产生固定的高成本。列式存储(蓝色)则根据所需列数线性增长。
除了简单的I/O减少,列式存储还能实现更优异的数据压缩。在行式数据块中,数据类型差别很大,一个字符串后可能跟着一个整数,再跟着一个时间戳。这种无序性使得压缩算法的效果不佳。
在列式数据块中,每个值都具有相同的数据类型,并且通常属于类似的范围。存储“国家”的列可能会一个接一个地出现“USA”这个值数千次。
列式数据库采用**行程长度编码(RLE)**等编码方案,以便更好地处理这种多次出现的情况。如果一列中一个接一个地出现500次“USA”这个值,数据库会将其存储为简化的元组('USA', 500),而不是写入500次“USA”。这可以将存储空间减少10到50倍,此外还能加快扫描速度,因为CPU可以直接在L1/L2缓存中处理压缩数据。
数据库架构中没有免费的午餐。对读取速度的优化会给写入操作带来很大的开销。
在行式存储中,插入一条记录是对活动文件末尾进行一次追加操作。在列式存储中,插入一条记录需要数据库进行以下步骤:
这个过程,被称为“元组拆分”,开销较大。因此,分析系统(OLAP)通常避免使用单条INSERT语句。它们更依赖于批量加载过程,在其中数千或数百万行数据被缓存并同时写入,从而分摊了重构成本。这种基本的架构不同之处决定了我们为何将分析型数据仓库与实时事务数据库分开。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造