数据建模

时间序列数据建模最佳实践:单个或多个分区表(又称超表)

A series of eggs organized by shelf representing time-series data modeling.

作者:Chris Engelbert

收集时间相关信息(即时间序列数据)会产生大量需要管理和建模的数据。存储这些数据需要一个或多个 Timescale 超表,这些超表与 PostgreSQL 分区表非常相似,并且需要在数据库模式设计方面做出许多决策。

虽然我们在窄、中或宽表布局最佳实践文章中讨论了表布局,但本博客文章将讨论是应该使用单个表存储所有数据还是使用多个表,以及它们各自的优缺点。

基础知识:超表和时间序列数据建模

Timescale 超表的工作方式类似于常规 PostgreSQL 表,但为时间序列数据提供了优化的性能和用户体验。使用超表时,数据存储在块中,这些块的工作方式类似于PostgreSQL 的分区表,但支持多个维度和其他功能。

Timescale 基于关系数据库模型构建,这意味着它支持多种模式和数据建模选择,或者说数据可以以多种方式组织和布局。尽早了解数据库设计选择对于找到最佳组合至关重要。

我在加入公司之前就开始使用 Timescale,最初是在我自己的初创公司中使用它来存储物联网指标。我们经历了几次不同的设计迭代,而这些迭代之间的迁移一点也不轻松。由于这种个人经历,我最大的目标之一就是防止其他人遭受同样的痛苦。

利用我们的关系数据库经验

由于基于 PostgreSQL 构建,我们知道有很多方法可以存储数据,包括在分区表中或仅仅是单独的表中。关系世界中的一种常见模式是根据内容将数据划分为不同的表,这也常被称为或实体。这意味着属于一组 A 的数据存储在一个表中,而属于一组 B 的数据存储在另一个表中,代表不同的域。

这就让我们不禁要问,时间序列数据和关系域的概念是如何结合在一起的。不幸的是,这个问题没有简单的答案。

压缩数据与拆分数据

我们主要的选择是将所有数据“压缩”到一个表中,该表可能包含数百个列(基本上是围绕“这只是一堆指标”的想法来定义我们的域),或者将数据拆分到多个列数较少的表中。后一种选择可以通过多种方式对表进行切片,例如按指标类型(温度不同于湿度,股票代码 A 不同于股票代码 B)、客户、数据类型和其他方式,或者上述方式的组合。

这两种可能性都有其自身的优缺点,可以分为四个常见的主题:

  • 易用性

  • 多租户/隐私相关要求(通用数据保护条例或 GDPR/加州消费者隐私法案或 CCPA/其他)

  • 模式迁移或升级/面向未来

  • 工具支持

我选择上述顺序并非偶然:这些问题的顺序重要性可能会影响您在稍后的选择。

单表设计与多表设计:优缺点

如前所述,这两种设计选择都有优缺点,在做出最终的数据建模决策之前,了解这些优缺点至关重要。鉴于之前的一系列主题,让我们先回答几个问题。

易用性

首先也是最重要的一点是,易用性如何,也就是说,您和您的团队是否能够胜任未来需要解决的挑战性任务?潜在的“复杂性”可能包括表名的生成模式或确保类似的表都升级到相同的模式级别。

多租户

接下来,您是否需要提供更高级别的多租户,例如,不仅要使用客户 ID 隔离不同的客户,还需要将它们存储在不同的表、模式甚至数据库中?您的公司是否受法规(例如 GDPR 或 CCPA)的约束,这些法规规定用户和客户可能有权被遗忘?由于时间序列数据通常是只追加的,因此删除部分数据(此特定用户的数据)可能会很棘手。

模式更改

然后,我们还有一个问题,即您是否希望数据模式经常更改。在窄、中或宽表模型的文章中可以找到关于超表面向未来设计的详细讨论。但是,表的数量越多,将来需要升级或迁移的表就越多,这会增加额外的复杂性。

额外支持

最后,其他工具和框架(例如 ORM(对象关系映射)解决方案)的支持有多重要?虽然我个人认为 ORM 框架不太适合时间序列数据(尤其是在使用聚合时),但很多人都在广泛使用它们,因此讨论它们还是有意义的。

无论如何,既然我们已经回答了这些问题,那么让我们更详细地研究一下设计选择。

时间序列数据建模的单表设计

从关系数据库的角度来看,将所有数据存储到单个表中最初可能会让人觉得不负责任。但是,根据我对域模型的划分方式,这可能是一个完全有效的选择。如果我将所有存储的数据(指标、事件、物联网数据、股票价格等)视为单个域,即时间序列,那么这种设计选择是有意义的。

优势

单表设计让一些事情变得超级简单。 

查询

首先,也是大多数人都明白的,就是查询。所有内容都在同一个表中,查询会选择某些列并添加额外的过滤器或 where 子句来选择数据。这与 SQL 的基本原理一样简单。也就是说,查询数据超级简单,而不仅仅是容易。

升级架构

升级表的架构同样简单。添加或删除列只需要一个命令,并且所有数据都同时升级。如果您有多个类似的表,您最终可能会遇到这种情况,即某些表已升级,而其他表却被遗忘了——不需要真正的迁移窗口。

单表可以轻松支持多种不同的值,可以通过多列(宽表布局)、支持各种数据值的 JSONB 列(窄表布局)或基于值的潜在数据类型的列(中等表布局)。不过,这三种选择各有利弊

tsdb=> \d
      Column    |           Type           | Collation | Nullable |      Default
—---------------+--------------------------+-----------+----------+-------------------
 created        | timestamp with time zone |           | not null | now()
 point_id       | uuid                     |           | not null | gen_random_uuid()
 device_id      | uuid                     |           | not null |
 temp           | double precision         |           |          |
 hum            | double precision         |           |          |
 co2            | integer                  |           |          |
 wind_speed     | integer                  |           |          |
 wind_direction | integer                  |           |          |

|                  created | point_id | device_id | temp |  hum | co2 |
 wind_speed | wind_direction |
| 2022-01-01 00:00:00.0+00 |      123 |        10 | 24.7 | 57.1 | 271 |
       NULL |           NULL |

工具和框架集成

最后但同样重要的是,单表可以很好地与 ORM 框架等工具配合使用。将一组特定的 ORM 实体连接到超表很容易,表示完整记录或聚合的结果(这可能需要原生查询而不是自动生成的查询)。

挑战

但就像这个世界上的所有事物一样,这种选择也有很大的缺点。由于时间序列数据的设计理念是仅追加(这意味着修改现有记录的情况很少发生),因此很难删除数据。根据特定要求删除数据更加困难,例如用户或客户要求删除其所有数据。

在这种情况下,我们必须爬取可能长达数年的数据,从各处删除记录。这不仅给 WAL(预写日志)带来了跟踪更改的负担,而且还会产生大量的 I/O、读取和写入。

如果我们尝试将收集和计算出的数据集存储在同一个表中,情况也是如此。由于许多系统经常必须回填数据(例如,从因断网一段时间而一直在本地收集数据的设备),因此可能必须重新计算计算值。这意味着必须使已存储的数据无效(可能意味着删除)并重新插入。

最后,如果您的公司提供不同级别的数据保留,那么祝您好运在单个表上实现这一点。这是前两个问题,但持续不断,而且非常糟糕。

时间序列数据建模的多表设计

现在我们已经了解了单表设计的优缺点,那么当我们改为采用多表设计时,有什么区别呢?

优势

查询

虽然查询仍然很简单,但同时查询多组数据可能会稍微复杂一些,需要使用 JOIN 和 UNION 来合并来自不同表的数据。同时请求多组数据通常是为了提高效率,需要更少的数据库往返次数并最大程度地缩短响应时间。除此之外,在易用性方面没有太大区别,除了表名之外,我们稍后会再讨论这一点。

删除数据

拥有多表的一个主要好处是,尤其是当按客户、用户或对您的用例有意义的任何多租户分隔符进行切片时,可以快速响应与 GDPR 或 CCPA 相关的销毁和删除任何客户相关数据的请求。在这种情况下,就像找到所有客户的表并删除它们一样简单。不过,从备份中删除它们是另一回事。😌

计算数据和收集的数据也是如此。分离这些表使得在收到延迟信息时更容易丢弃和重新计算全部或部分数据。

数据保留

前面提到的数据保留也是如此。许多代表客户存储大量数据的公司根据客户愿意支付的金额提供不同的数据保留策略。按客户对表进行切片可以轻松设置特定于客户的保留策略,甚至可以在客户升级到更高级别时更改这些策略。如果这是您需要的,那么多表设计就是您的选择。

挑战

然而,就像单表一样,多表也有缺点。

除了已经提到的围绕查询的稍微复杂一些的元素(这不一定是一个缺点)之外,拥有多个表还需要规划表命名方案。我们为游戏带来的维度越多(按客户、指标类型等),我们的命名方案就需要越复杂。也就是说,我们最终可能会得到这样的表名,例如 <<customer_name>>__<<metric_type>>。虽然这听起来还不错,但它很快就会变得很糟糕。我们以前都遇到过这种情况。😅

tsdb=> \dt *.cust_*
                    List of relations
 Schema |            Name            | Type  |   Owner
--------+----------------------------+-------+-----------
 public | cust_mycompany_co2         | table | tsdbadmin
 public | cust_mycompany_humidity    | table | tsdbadmin
 public | cust_mycompany_temperature | table | tsdbadmin
 public | cust_timescale_co2         | table | tsdbadmin
 public | cust_timescale_humidity    | table | tsdbadmin
 public | cust_timescale_temperature | table | tsdbadmin
(6 rows)

工具和框架集成

ORM 框架等工具可能会使事情变得更加复杂。这些工具中的许多工具并非设计用于支持任意的、运行时生成的表名,这使得将这些工具与此概念集成变得非常复杂。为每个客户使用不同的 PostgreSQL 数据库模式并减少维度数量可能会有所帮助。

升级和迁移表

还有一个复杂之处:升级和迁移表。由于多表设计,我们最终可能会得到许多由所选的附加维度分隔的类似表。升级表架构时,我们需要确保所有这些表最终都处于一致的状态。

然而,许多自动模式迁移工具并不容易支持这种多表迁移。这迫使我们回过头去编写迁移脚本,尝试查找与特定命名方案匹配的所有表,并确保所有表都以相同的方式升级。如果我们错过了一个表,我们最终会发现它,但可能为时已晚。

简而言之

现在您已经了解了所有内容并回答了这些问题,您可以查看需求,看看您的用例适合哪种情况。

一些硬性要求可能会使您的选择显而易见,而一些“锦上添花”的元素仍然可能会影响最终决定,并暗示哪些元素可能会在不久的将来成为硬性要求。

单表设计

多表设计

易用性

容易

比较容易

多租户/隐私法规

困难

容易

面向未来

容易

比较困难

工具支持

容易

困难

虽然单表设计很容易上手,但如果您需要遵守法规,那可能就很难办到了。但是,多表设计在管理和正确使用方面肯定更加复杂。

如何为您的数据建模选择最佳表

永远没有万能的答案,或者像顾问们喜欢说的那样:视情况而定。

与围绕表布局的设计选择不同,提出真正的建议太复杂了。您只能尝试按照建议的过程来回答上述问题,并查看答案,看看哪些点代表硬性要求,哪些点可能会成为硬性要求,哪些点只是锦上添花。这样,您可能会找到与您的用例相匹配的答案。

混合搭配方法

另外,请记住,您可能会有不同的用例,这些用例的要求各不相同,因此一个用例最终可能完全适合作为单表设计运行,而另一个(或多个)用例可能需要多个表。

此外,您有机会混合搭配两种解决方案的优势。文中已经暗示过,但可以使用简化的多表设计(例如,按指标类型),并将客户维度分离到 PostgreSQL 数据库模式中,每个模式代表一个客户。

同样,可以使用模式来分离客户,并将该特定客户的所有指标/事件/数据存储在单个超表中。有很多选择,只受限于您的想象力。

无论您最终选择哪种方案,都应尽量面向未来。试着想象未来会发生什么,如果您不确定某个比较困难的要求是否会成为必须满足的要求,那么为了安全起见,现在就将其视为硬性要求可能是值得的。

后续步骤

如果您想尽快开始设计您的超表数据库模式,确保您在实现令人难以置信的压缩率的同时,为您的时间序列数据获得最佳性能和用户体验,请查看 Timescale

如果您希望在本地测试它或在本地运行它,那么我们也能满足您的需求:请 查看我们的文档