开发者生态
morning
时序存储:影响成本与性能的设计选择
2026-05-18
1 阅读
作者:Nirmesh Khandelwal
使用任何时序数据库都需要做出一系列的存储设计决策:如何布局行、何时进行压缩、基于什么进行分区。这些决策对成本和查询性能的影响,甚至比数据库本身的选择更为关键。本文将从第一性原理出发,系统地探讨这些基本问题,并借助 PostgreSQL 和 Apache Parquet 等广泛可用的工具来量化评估每一项权衡。 什么是时序数据? 时序数据是一系列随时间推移记录的测量值。标准数据库记录追踪的是当前状态(如“账户余额为 50 美元”),而时序数据则追踪变化历程(如“10:00、10:01、10:02 时的余额……”)。 现代系统会持续发出有关运行状况和活动状态的信号: 生物识别技术(智能手表每五秒记录一次心率,以便追踪运动强度)。交通运输(拼车应用通过追踪 GPS 坐标来计算车费和预计到达时间)。金融(市场行情显示系统每秒捕捉数千次价格更新)。基础设施(监控工具追踪 CPU 趋势,以便预测资源耗尽情况)。 廉价的传感器和存储设备使得几乎所有数据都能被即时记录并保存完整的历史数据,而不仅仅是快照。核心数据点遵循一种重复的结构:时间戳、标识符和数值。这种简单性具有欺骗性,因为在海量数据规模下,这种重复性反而会成为问题。以下是一个数据序列示例: 表1:维度和度量示例 此处,“序列”被定义为一个唯一的标识属性组合。例如,{device=thermostat, location=living_room}。 图1:多序列和数据点示例 每个数据点包含三个部分: 时间戳——测量发生的时间。维度(或标签)——用于识别和分组数据序列的属性。指标(或字段)——该时间点的测量值。 一个实用的原则是:将稳定的标识符放在维度中,将变化的测量值放在指标中。 这种划分很重要,因为它们在查询中的用途不同:维度用于过滤和分组(WHERE、GROUP BY device_id);指标用于计算(AVG(temperature_c)、MAX(humidity_pct))。 关系型存储:扁平化 vs. 规范化 要在 PostgreSQL 这样的关系型数据库中存储时序数据,我们可以将所有属性存储在一个扁平表中,也可以将标识符规范化到一个单独的注册表中。 选项 A :扁平化(简单)模式 扁平模式将维度和指标存储在同一行中。这种布局虽然易于实现和查询,但会导致高度冗余。 CREATE TABLE readings_flat ( ts timestamptz NOT NULL, device_id text NOT NULL, location text NOT NULL, region text NOT NULL, metric_name text NOT NULL, value double precision NOT NULL ); CREATE INDEX idx_flat_device_ts ON readings_flat (device_id, ts); 选项 B:规范化模式 规范化操作会将稳定的标识符移入 series_dim 表中。每个测量值都会引用一个 series_id,而不是重复使用标识符字符串。 CREATE TABLE series_dim ( series_id serial PRIMARY KEY, device_id text NOT NULL, location text NOT NULL, region text NOT NULL, UNIQUE (device_id, location, region) ); CREATE TABLE readings_normalized ( series_id integer REFERENCES series_dim(series_id), ts timestamptz NOT NULL, metric_name text NOT NULL, value double precision NOT NULL ); CREATE INDEX idx_norm_series_ts ON readings_normalized (series_id, ts); 存储开销实验结果 我们通过一项涉及一千个序列和 280 万行数据的 PostgreSQL 16 实验,测量了重复操作的成本。规范化处理使总存储空间减少了约 42%(节省了 289 MB)。 表2:扁平化模式和规范化模式的存储开销对比 成本模型 效率差距取决于维度字节数与什么相乘。对于扁平化数据,TotalBytes ≈ N_rows * (timestamp + metric + dimensions)。对于规范化数据,TotalBytes ≈ N_rows * (timestamp + metric + series_id) + N_series * dimensions。 这两种模式的总存储量均为 O(N_rows)。区别在于每行的开销:在扁平化模式中,每行都携带完整的维度字符串。而在规范化模式中,每行仅携带一个空间占用较小的序列 ID,而维度字符串则每个序列存储一次。由于序列 ID 远小于完整的维度数据,而且在高频监控中 N_series ≪ N_rows,所以在行数相同的情况下,规范化模式存储的数据量要少得多。 查询性能(缓存预热) 通过对比“扁平化”和“规范化”这两种方式,我们可以获得一些有趣的发现。在范围查询中,两者的性能表现相同:0.74 毫秒(扁平化)vs. 0.74 毫秒(规范化)。而在小时平均值的查询中,规范化方式更快:215.51 毫秒(扁平化)vs. 164.41 毫秒(规范化)。通过仅存储一次维度并使用 ID 进行引用,可以在不增加查询开销的情况下减少存储占用。 高基数时规范化失效 当许多行具有相同的标识时,规范化会有所帮助。但当标识字段具有高基数且每行几乎唯一时,规范化的优势就会减弱。 情况 A:可重复维度(规范化有帮助) 表3:维度可重复示例 这里,N_rows = 3 且 N_series = 1。规范化布局将维度信息仅在 series_dim 中存储一次,并在每个数据点中复用 series_id。 情况 B:事件唯一标识符(规范化效果减弱) 表4:高基数维度示例 如果 request_id 是序列标识的一部分,N_series 将趋近于 N_rows。此时,规范化存储的去重效果将大大降低。从实际应用的角度来看,应将稳定的维度保留在序列标识中。此外,除非查询模式有此要求,否则应将 request_id 等事件级 ID 排除在序列标识之外。 外部的一些指南也体现了同样的基数行为。AWS CloudWatch 将每个唯一的维度组合定义为一个独立的指标流。高基数维度会直接增加指标体量。CloudWatch Logs 的指标过滤器则更进一步:它们明确警告用户不要使用 requestID 和 IPAddress 之类的维度,并且尽可能禁用会产生过多独立流的过滤器。InfluxDB 的基数指南也指出了同样的模式:标签中的唯一 ID、哈希值和随机值会导致序列数量膨胀,并同时降低写入和查询性能。 模式演进设计 固定列的模式在标签演进之前都能正常工作。新标签的引入会导致表结构发生变化,需要进行数据回填以及