压缩您的时序数据可以让您将数据块大小减少 90% 以上。这节省了存储成本,并保持您的查询以闪电般的速度运行。

当您启用压缩时,超表中的数据会逐块压缩。当数据块被压缩时,多个记录被分组到单行中。此行的列保存类似数组的结构,用于存储所有数据。这意味着它不是使用大量行来存储数据,而是将相同的数据存储在单行中。由于单行占用的磁盘空间小于多行,因此它减少了所需的磁盘空间量,并且还可以加快您的查询速度。

例如,如果您有一个表格,其中的数据看起来有点像这样

时间戳设备 ID设备类型CPU
12:00:01ASSD70.11
12:00:01BHDD69.70
12:00:02ASSD70.12
12:00:02BHDD69.69
12:00:03ASSD70.14
12:00:03BHDD69.70

您可以将其转换为数组形式的单行,如下所示

时间戳设备 ID设备类型CPU磁盘 IO
[12:00:01, 12:00:01, 12:00:02, 12:00:02, 12:00:03, 12:00:03][A, B, A, B, A, B][SSD, HDD, SSD, HDD, SSD, HDD][70.11, 69.70, 70.12, 69.69, 70.14, 69.70][13.4, 20.5, 13.2, 23.4, 13.0, 25.2]

本节介绍如何启用原生压缩,然后详细介绍最重要的压缩设置,以帮助您获得最佳的压缩率。

每个表都有不同的模式,但它们确实有一些您需要考虑的共同点。

考虑具有以下属性的表 metrics

类型排序规则可为空默认值
timetimestamp with time zonenot null
device_idintegernot null
device_typeintegernot null
cpudouble precision
disk_iodouble precision

所有超表都有一个主维度,用于将表分区为数据块。主维度在创建超表时给出。在下面的示例中,您可以看到一个经典的时序用例,其中 time 列作为主维度。此外,还有两列 cpudisk_io 包含随时间捕获的值,以及一列 device_id 用于捕获值的设备。列可以用几种不同的方式使用

  • 您可以使用列中的值作为查找键,在上面的示例中,device_id 是此类列的典型示例。
  • 您可以使用列来对表进行分区。这通常是时间列,例如上面示例中的 time,但也可以使用其他类型对表进行分区。
  • 您可以使用列作为过滤器来缩小您选择的数据范围。列 device_type 是一个示例,您可以在其中决定仅查看固态硬盘 (SSD)。其余列通常是您正在收集的值或指标。这些通常以其他方式聚合或呈现。列 cpudisk_io 是此类列的典型示例。
SELECT avg(cpu), sum(disk_io)
FROM metrics
WHERE device_type = ‘SSD’
AND time >= now() -1 day’::interval;

当超表中的数据块被压缩时,存储在其中的数据会重新组织并以列顺序而不是行顺序存储。因此,不可能使用相同未压缩模式版本的数据块,并且必须创建不同的模式。这由 TimescaleDB 自动处理,但它有一些含义:压缩率和查询性能非常依赖于压缩数据的顺序和结构,因此在设置压缩时需要考虑一些因素。超表上的索引并非总是可以以相同的方式用于压缩数据。

注意

在超表上设置的索引仅在包含未压缩数据的数据块上使用。Timescale 创建并使用自定义索引来合并压缩期间的 segmentbyorderby 参数,这些参数在读取压缩数据时使用。有关此内容的更多信息,请参见下一节。

根据之前的模式,数据过滤应该发生在某个时间段内,并且分析是在设备粒度上完成的。这种数据访问模式适合组织适合压缩的数据布局。

对数据进行排序将对压缩率和查询性能产生重大影响。在一个维度上变化的行应该彼此靠近。由于我们主要处理时序数据,因此时间维度是一个很好的选择。大多数时候,数据以可预测的方式变化,遵循一定的趋势。我们可以利用这一事实对数据进行编码,使其占用更少的存储空间。例如,如果您按时间顺序对记录进行排序,它们将按该顺序压缩,然后也按相同的顺序访问。

在我们的示例表上使用以下配置设置

ALTER TABLE metrics
SET (timescaledb.compress, timescaledb.compress_orderby='time');

将产生以下数据布局。

时间戳设备 ID设备类型CPU
[12:00:01, 12:00:01, 12:00:02, 12:00:02, 12:00:03, 12:00:03][A, B, A, B, A, B][SSD, HDD, SSD, HDD, SSD, HDD][70.11, 69.70, 70.12, 69.69, 70.14, 69.70]

time 列用于对数据进行排序,这使得使用 time 列过滤数据更加高效。

postgres=# select avg(cpu) from metrics where time >= '2024-03-01 00:00:00+01' and time < '2024-03-02 00:00:00+01';
avg
--------------------
0.4996848437842719
(1 row)
Time: 87,218 ms
postgres=# ALTER TABLE metrics
SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby='time'
);
ALTER TABLE
Time: 6,607 ms
postgres=# SELECT compress_chunk(c) FROM show_chunks('metrics') c;
compress_chunk
----------------------------------------
_timescaledb_internal._hyper_2_4_chunk
_timescaledb_internal._hyper_2_5_chunk
_timescaledb_internal._hyper_2_6_chunk
(3 rows)
Time: 3070,626 ms (00:03,071)
postgres=# select avg(cpu) from metrics where time >= '2024-03-01 00:00:00+01' and time < '2024-03-02 00:00:00+01';
avg
------------------
0.49968484378427
(1 row)
Time: 45,384 ms

这使得时间列成为对数据进行排序的完美选择,因为测量值会随着时间的推移而演变。如果您仅将其用作压缩设置,您很可能会获得足够好的压缩率来节省大量存储空间。但是,有效访问数据取决于您的用例和查询。使用此设置,您始终必须通过使用时间维度来访问数据,然后根据任何其他条件过滤所有行。

分段压缩数据应基于您访问数据的方式。基本上,您希望以这样一种方式分段您的数据,以便您可以更轻松地让您的查询在正确的时间获取正确的数据。也就是说,您的查询应该决定您如何分段数据,以便可以优化它们并产生更好的查询性能。

例如,如果您想使用特定的 device_id 值(所有记录或特定时间范围)访问单个设备,您将需要在行访问期间逐个过滤所有这些记录。为了解决这个问题,您可以使用 device_id 列进行分段。如果您正在寻找特定的设备 ID,这将使您能够更快地对压缩数据运行分析查询。

考虑以下查询

SELECT device_id, AVG(cpu) AS avg_cpu, AVG(disk_io) AS avg_disk_io
FROM metrics
WHERE device_id = 5
GROUP BY device_id;

如您所见,查询根据 device_id 标识符通过将其所有值分组在一起完成大量工作。我们可以利用这一事实通过设置压缩来围绕此列中的值分段数据,从而加快这些类型的查询。

在我们的示例表上使用以下配置设置

ALTER TABLE metrics
SET (
timescaledb.compress,
timescaledb.compress_segmentby='device_id',
timescaledb.compress_orderby='time'
);

将产生以下数据布局。

timedevice_iddevice_typecpudisk_io能量消耗
[12:00:02, 12:00:01]1[SSD,SSD][88.2, 88.6][20, 25][0.8, 0.85]
[12:00:02, 12:00:01]2[HDD,HDD][300.5, 299.1][30, 40][0.9, 0.95]
..................

分段列 device_id 用于根据该列的值将数据点分组在一起。这使得访问特定设备更加高效。

postgres=# \timing
Timing is on.
postgres=# SELECT device_id, AVG(cpu) AS avg_cpu, AVG(disk_io) AS avg_disk_io
FROM metrics
WHERE device_id = 5
GROUP BY device_id;
device_id | avg_cpu | avg_disk_io
-----------+--------------------+---------------------
5 | 0.4972598866221261 | 0.49820356730280524
(1 row)
Time: 177,399 ms
postgres=# ALTER TABLE metrics
SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id',
timescaledb.compress_orderby='time'
);
ALTER TABLE
Time: 6,607 ms
postgres=# SELECT compress_chunk(c) FROM show_chunks('metrics') c;
compress_chunk
----------------------------------------
_timescaledb_internal._hyper_2_4_chunk
_timescaledb_internal._hyper_2_5_chunk
_timescaledb_internal._hyper_2_6_chunk
(3 rows)
Time: 3070,626 ms (00:03,071)
postgres=# SELECT device_id, AVG(cpu) AS avg_cpu, AVG(disk_io) AS avg_disk_io
FROM metrics
WHERE device_id = 5
GROUP BY device_id;
device_id | avg_cpu | avg_disk_io
-----------+-------------------+---------------------
5 | 0.497259886622126 | 0.49820356730280535
(1 row)
Time: 42,139 ms
注意

在单个批次中一起压缩的行数(如我们上面看到的那些)是 1000。如果您的数据块没有包含足够的数据来创建足够大的批次,您的压缩率将会降低。在定义压缩设置时,需要考虑这一点。

关键词

在此页面上发现问题?报告问题 或 在 GitHub 上编辑此页