简介

TimeUDB 是一个专门用于存储和分析时间序列数据的数据库,它的出色数据吞吐能力和可靠的性能表现使其成为物联网领域的理想选择,能够为物联网应用提供了高效、可扩展的数据存储和分析解决方案。

 

表和超表

超表是 UnvDB 表,它可以自动按时间对你的数据进行分区。你以与常规 UnvDB 表相同的方式与超表进行交互,但超表具有额外的功能,可以使管理时间序列数据更加容易。

在 TimeUDB 中,超表与常规 UnvDB 表并存。你可以使用超表来存储时间序列数据,这将提高插入和查询性能,并提供有用的时间序列功能。而对于其他关系数据,则使用常规的 UnvDB 表。

通过超表,TimeUDB 可以轻松地通过时间序列数据的时间参数对其进行分区,从而提高插入和查询性能。在幕后,数据库会执行设置和维护超表分区的工作。同时,你可以像插入和查询所有数据都位于单个常规 UnvDB 表中一样进行操作。

数据库由表组成,其中包含你的数据。在 UnvDB 中,这些表是关系表,因此一个表中的数据与另一个表中的数据是相关联的。在 TimeUDB 中,除了特殊的时间序列超表之外,你还可以使用常规的 UnvDB 关系表。

超表是专门为时间序列数据设计的,因此它们具有一些特殊性质,使其与常规的 UnvDB 表不同。超表始终按时间进行分区,但也可以按其他列进行分区。超表的另一个特殊之处在于,它们可以分解为更小的表,这些表称为“块”。

在本部分中,你将为时间序列数据创建一个超表,并为关系数据创建常规的 UnvDB 表。你还可以在超表上创建索引,这不是必需的,但可以帮助你的查询更有效地运行。超表的另一个特殊性质是,如果需要,你可以稍后创建索引。

要了解更多信息,请参阅超表部分。

 

创建您的第一个超表

对于本指南中使用的金融数据集,创建一个名为 stocks_real_time 的超表,其中包含前 100 个交易最频繁的代码的逐秒股票交易数据。

1、 在 ud_sql 命令提示符下,连接到数据库。

2、 使用 CREATE TABLE 命令创建一个常规 UnvDB 表来存储实时股票交易数据 :

CREATE TABLE stocks_real_time (
  time TIMESTAMPTZ NOT NULL,
  symbol TEXT NOT NULL,
  price DOUBLE PRECISION NULL,
  day_volume INT NULL
);

3、 使用 TimeUDB 提供的 create_hypertable() 函数,将常规表转换为按时间列分区的超表。你必须提供表名(stocks_real_time)以及该表中用于分区的时间戳数据所在的列(time):

SELECT create_hypertable('stocks_real_time', by_range('time'));

4、创建索引以支持对 symbol 和 time 列进行高效查询:

CREATE INDEX ix_symbol_time ON stocks_real_time (symbol, time DESC);

 

为关系数据创建常规 UnvDB 表

TimeUDB 并非仅用于超表。当你拥有其他可以增强时间序列数据的关系数据时,你可以像平常一样创建常规的 UnvDB 表。对于此数据集,还有另一个名为 company 的数据表。

创建常规 UnvDB 表:

1、创建一个表来存储股票交易数据的公司名称和代码:

CREATE TABLE company (
  symbol TEXT NOT NULL,
  name TEXT NOT NULL
);

2、您的 TimeUDB 数据库中现在有两个表。一张名为 stocks_real_time 的超表,一张名为 company 的普通 UnvDB 表。您可以通过在 ud_sql 提示符下运行以下命令来检查这一点:

\dt

此命令返回有关表的信息,如下所示:

             List of relations
 Schema |       Name       | Type  | Owner
--------+------------------+-------+-------
 public | company          | table | unvdb
 public | stocks_real_time | table | unvdb
(2 rows)

 

时间序列数据

时间序列数据表示系统、过程或行为随时间的变化情况。例如,如果你每五分钟从温度计测量一次数据,那么你就是在收集时间序列数据。另一个常见的例子是股票价格变动,甚至是你的智能手机的电池寿命。随着时间的推移,这些测量数据会发生变化,每个数据点都会与其时间戳一起记录,从而能够对其进行测量、分析和可视化。

时间序列数据的收集可能非常频繁,如财务数据,也可能不那么频繁,如天气或系统测量数据。它还可以定期收集,例如每毫秒或每小时收集一次,或者不定期收集,例如仅在发生变化时收集。

数据库一直都有时间字段,但使用专门处理时间序列数据的数据库可以使你的数据库工作更加高效。像 TimeUDB 这样的专门时间序列数据库旨在处理大量的数据库写入操作,因此它们的工作速度更快。它们还针对模式更改进行了优化,并使用了更灵活的索引,因此当你做出更改时,无需花费时间迁移数据。

时间序列数据无处不在,但在某些环境中,使用专门的时间序列数据库(如 TimeUDB )尤为重要,例如:

  • 监控系统:虚拟机、服务器、容器指标、CPU、空闲内存、网络/磁盘I/O、服务和应用指标(如请求率和请求延迟)等。

  • 金融交易系统:证券、加密货币、支付和交易事件等。

  • 物联网:工业机器和设备、可穿戴设备、车辆、实体集装箱、托盘和智能家居消费设备上的传感器数据。

  • 事件应用程序:用户或客户交互数据,如点击流、页面浏览量、登录和注册等。

  • 商业智能:跟踪关键指标和整体业务状况。

  • 环境监测:温度、湿度、压力、pH值、花粉计数、气流、一氧化碳、二氧化氮或颗粒物等。

为了探索 TimeUDB 的功能,你需要一些样本数据。本指南使用的是来自Twelve Data的实时股票交易数据,也称为逐笔数据。

 

关于数据集

该数据集包含名为 stocks_real_time 的超表中100个交易量最大的股票符号的秒级股票交易数据。它还包括一个名为 company 的常规 UnvDB 表,其中单独列出公司符号和公司名称。

该数据集每晚更新一次,包含过去四周的数据,通常包含约800万行数据。股票交易记录于周一至周五的实时数据中,时间覆盖纽约证券交易所的正常交易时间(东部标准时间上午9:30至下午4:00)。

 

摄取数据集

要将数据提取到您创建的表中,您需要下载数据集并将数据复制到数据库中。

1、下载real_time_stock_data.zip文件。该文件包含两个.csv 文件;一份包含公司信息,一份包含过去一个月的实时股票交易。下载:

【附件】real_time_stock_data.zip [【附件】real_time_stock_data.zip] real_time_stock_data.zip

2、在新的终端窗口中,运行以下命令来解压缩.csv文件:

unzip real_time_stock_data.zip

3、在 ud_sql 提示符下,使用 COPY 命令将数据传输到您的 TimeUDB 实例中。如果 .csv 文件不在当前目录中,请在以下命令中指定文件路径:

\COPY stocks_real_time from '/data/tutorial_sample_tick.csv' DELIMITER ',' CSV HEADER;

\COPY company from '/data/tutorial_sample_company.csv' DELIMITER ',' CSV HEADER;

由于有数百万行数据,因此该COPY过程可能需要几分钟,具体取决于您的互联网连接和本地客户端资源。

 

查询

TimeUDB 支持完整的 SQL,因此您无需学习自定义查询语言。本节包含一些简单的查询,您可以直接在此页面上运行。当您构建了完美的查询语句时,请使用复制按钮在您自己的数据库上使用它。

本节中的大多数查询都是查找最近四天的数据。这是为了考虑到周末没有股票交易的情况,并确保您的结果中始终能获取到一些数据。

所有 SQL 查询的主要构建块是 SELECT 语句。它是从数据库中选择数据的指令。进行快速 SELECT 查询通常是您使用新数据库时首先要做的事情,只是为了确保您的数据以您期望的方式存储在数据库中。

 

使用 SELECT 返回数据

在 ud_sql 提示符下,键入以下查询:

SELECT * FROM stocks_real_time srt
LIMIT 10;

 

使用 ORDER BY 组织结果

在 ud_sql 提示符处,键入以下查询:

SELECT * FROM stocks_real_time srt
WHERE symbol='TSLA' and day_volume is not null
ORDER BY time DESC, day_volume desc
LIMIT 10;

 

获取第一个和最后一个值

TimeUDB 具有自定义的 SQL 函数,可以帮助使时间序列分析变得更容易、更快捷。在本节中,您将了解两个常见的 TimeUDB 函数:first,用于查找组内的最早值;以及 last,用于查找组内的最近值。

first() 和last() 函数在按另一列排序时,分别检索某一列的第一个和最后一个值。例如,股票数据有一个名为 time 的时间戳列和一个名为 price 的数值列。您可以使用 first(price, time) 来获取按时间列递增排序时 price 列中的第一个值。

在此查询中,您首先选择 stocks_real_time srt 表中每只股票在过去四天内的 first() 和 last() 交易价格;然后,您通过 GROUP BY 语句组织结果,以便能够看到每只股票的第一个和最后一个值,并通过 ORDER BY 语句按字母顺序进行排序,如下所示:

SELECT symbol, first(price,time), last(price, time)
FROM stocks_real_time srt
WHERE time > now() - INTERVAL '4 days'
GROUP BY symbol
ORDER BY symbol
LIMIT 10;

 

使用时间桶获取值

为了更方便地查看不同时间范围内的数字,您可以使用 TimeUDB 的 time_bucket 函数。时间桶用于对数据分组,以便您可以在不同的时间段内执行计算。时间桶代表特定的时间点,因此单个时间桶中的所有数据时间戳都使用时间桶的时间戳。

在本节中,您使用与上一节相同的查询来查找第一个和最后一个值,但首先会将数据组织为1小时的时间桶。在上一部分中,您检索了列的第一个和最后一个值,而这次您将检索1小时时间桶的第一个和最后一个值。

--首先声明要使用的时间桶间隔,并为时间桶命名
SELECT time_bucket('1 hour', time) AS bucket,
--然后,您可以按照之前使用的相同方式添加查询
	first(price,time),
	last(price, time)
FROM stocks_real_time srt
WHERE time > now() - INTERVAL '4 days'
--最后,使用 GROUP BY 语句按时间桶组织结果
GROUP BY bucket;

注意: 创建超级表时,TimeUDB 会自动在时间列上创建索引。但是,您通常还需要过滤其他列上的时间序列数据。适当地使用索引可以帮助您的查询获得更好的性能。有关索引的更多信息,请参阅关于索引部分

 

聚合

聚合是一种组合数据以从中获取见解的方法。简单来说,聚合就像是寻找平均值。例如,如果您拥有显示随时间变化的温度数据,您可以计算这些温度的平均值,或者计算读取的次数。平均、总和和计数都是简单聚合的示例。

然而,聚合计算可能很快就会变得庞大而缓慢。如果您想要找到一系列股票每天的平均开盘价和收盘价,那么您需要一些更复杂的方法。这就是 TimeUDB 连续聚合的用武之地。连续聚合可以最大程度地减少您执行查询时需要查找的记录数量。

 

连续聚合

时间序列数据通常增长非常迅速。这意味着将数据聚合为有用的摘要可能会变得非常慢。连续聚合使数据聚合变得闪电般快速。

如果您非常频繁地收集数据,您可能希望将数据聚合为分钟或小时。例如,如果您有一个每秒记录的温度读数表,您可以找到每小时的平均温度。每次运行此查询时,数据库都需要扫描整个表并重新计算平均值。

连续聚合是一种超表,它会在添加新数据或修改旧数据时,在后台自动刷新。系统会跟踪对数据集的更改,并在后台自动更新连续聚合背后的超表。

您无需手动刷新连续聚合,它们会在后台持续增量更新。连续聚合的维护负担也远低于常规的 UnvDB 物化视图,因为整个视图并非在每次刷新时都从头开始创建。这意味着您可以继续处理数据,而不是维护数据库。

由于连续聚合基于超表,因此您可以像查询其他表一样完全以相同的方式查询它们,并可以对连续聚合启用压缩或分层存储。您甚至可以在连续聚合的基础上创建连续聚合。

默认情况下,查询连续聚合可为您提供实时数据。来自物化视图的预先聚合数据与尚未聚合的最新数据相结合。这让您在每次查询时都能获得最新结果。

有三种主要方式可以简化聚合:物化视图、连续聚合和实时聚合。

物化视图是 UnvDB 的标准功能。它们用于缓存复杂查询的结果,以便您稍后重复使用。虽然您可以根据需要手动刷新它们,但物化视图不会定期更新。

连续聚合是 TimeUDB 独有的功能。它们的工作方式与物化视图类似,但是当新数据添加到您的数据库时,它们会在后台自动更新。连续聚合是持续增量更新的,这意味着它们的维护比物化视图占用的资源更少。连续聚合基于超表,您可以像查询其他表一样查询它们。

实时聚合是 TimeUDB 独有的功能。它们与连续聚合相同,但是它们将最新的原始数据添加到先前聚合的数据中,以提供准确和最新的结果,而无需在写入数据时聚合数据。

在本节中,您将创建一个连续聚合,并查询它以获取有关交易数据的更多信息。

 

创建聚合查询

金融部门经常使用K线图来直观显示资产的价格变化。每根K线代表一个时间段,例如一分钟或一小时,并显示该时间段内资产价格的变化情况。

K线图是根据每个金融资产在时间段内的开盘价、最高价、最低价、收盘价和交易量数据生成的。

在本节中,您将使用 SELECT 语句,通过 min 和 max 函数找到最高和最低值,并通过 first 和 last 函数找到开盘和收盘值。然后,您将数据聚合成1天的时间桶,然后,您按日期和符号组织结果:,如下所示:

SELECT
  time_bucket('1 day', time) AS bucket,
  symbol,
  max(price) AS high,
  first(price, time) AS open,
  last(price, time) AS close,
  min(price) AS low
FROM stocks_real_time srt
WHERE time > now() - INTERVAL '1 week'
GROUP BY bucket, symbol
ORDER BY bucket, symbol
LIMIT 10;

 

创建连续聚合

现在您有了聚合查询,您可以使用它来创建连续聚合。

在本节中,您的查询首先创建一个名为 stock_candlestick_daily 的物化视图,然后将其转换为 TimeUDB 连续聚合:

CREATE MATERIALIZED VIEW stock_candlestick_daily
WITH (timeudb.continuous) AS

然后,将之前创建的聚合查询作为连续聚合的内容:

SELECT
  time_bucket('1 day', "time") AS day,
  symbol,
  max(price) AS high,
  first(price, time) AS open,
  last(price, time) AS close,
  min(price) AS low
FROM stocks_real_time srt
GROUP BY day, symbol
LIMIT 10;

运行此查询时,您将创建视图,并使用聚合计算填充该视图。

当您的连续聚合已经创建并且第一次聚合数据时,您可以查询您的连续聚合。例如,您可以查看所有聚合数据,如下所示:

SELECT * FROM stock_candlestick_daily
  ORDER BY day DESC, symbol;

或者您可以查看一只股票,如下所示:

SELECT * FROM stock_candlestick_daily
WHERE symbol='TSLA';

 

写入数据

 

关于写入数据

TimeUDB 支持以与 UnvDB 相同的方式写入数据,使用INSERT、 UPDATE、INSERT … ON CONFLICT、 和DELETE。

注意: 由于 TimeUDB 是一个时间序列数据库,因此超级表针对插入到最近的时间间隔进行了优化。插入具有最近时间值的数据可提供出色的性能。但是,如果您需要频繁更新较旧的时间间隔,则可能会发现写入吞吐量较低。

 

插入数据

使用标准 INSERT SQL 命令将数据插入到超表中。

 

插入单行

要将单行插入到超表中,请使用语法 INSERT INTO … VALUES。例如,要将数据插入名为 conditions 的超表中:

INSERT INTO conditions(time, location, temperature, humidity)
	VALUES (NOW(), 'office', 70.0, 50.0);

 

插入多行

您还可以使用 INSERT 一次将多行插入到超表中 。这甚至适用于一次数千行。这比逐行插入数据更有效,因此建议尽可能使用。

使用相同的语法,用逗号分隔行:

INSERT INTO conditions
  VALUES
	(NOW(), 'office', 70.0, 50.0),
	(NOW(), 'basement', 66.5, 60.0),
	(NOW(), 'garage', 77.0, 65.2);

注意: 您可以在同一个 INSERT 语句中插入属于不同分块的多行数据。在幕后,TimeUDB 引擎会按分块对行进行批处理,并在单个事务中写入每个分块。

 

插入并返回数据

在同一个INSERT命令中,您可以通过添加RETURNING子句来返回部分或全部插入的数据。例如,要返回所有插入的数据,请运行:

INSERT INTO conditions
  VALUES (NOW(), 'office', 70.1, 50.1)
  RETURNING *;

这个返回:

time                          | location | temperature | humidity
------------------------------+----------+-------------+----------
2017-07-28 11:42:42.846621+00 | office   |        70.1 |     50.1
(1 row)

 

更新数据

使用标准的 UPDATE SQL 命令更新超表中的数据。

 

更新单行数据

使用语法 UPDATE … SET … WHERE 来更新单行数据。例如,要更新 conditions 超表中的一行数据,以新的 temperature 和 humidity 值替换原有数据,请执行以下命令。WHERE 子句用于指定要更新的行。

UPDATE conditions
  SET temperature = 70.2, humidity = 50.0
  WHERE time = '2017-07-28 11:42:42.846621+00'
	AND location = 'office';

 

一次性更新多行

您还可以通过使用筛选多于一行的 WHERE 子句来一次性更新多行。例如,运行以下命令以更新给定 10 分钟范围内的所有 temperature 值:

UPDATE conditions
  SET temperature = temperature + 0.1
  WHERE time >= '2017-07-28 11:40'
	AND time < '2017-07-28 11:50';

 

更新插入数据

更新插入是执行以下两项操作的操作:

  • 如果匹配行尚不存在,则插入新行

  • 如果匹配的行已存在,则更新现有行,或者不执行任何操作

注意: 在 UnvDB 中,主键是一个带有 NOT NULL 约束的唯一索引。如果你有一个主键,那么你就自动拥有了一个唯一索引。

 

创建一个带有唯一约束的表

本节中的示例使用了一个 conditions 表,该表在 (time, location) 列上具有唯一约束。要在定义表时创建唯一约束,请使用 UNIQUE (< COLUMNS >) 。

CREATE TABLE conditions (
  time        TIMESTAMPTZ       NOT NULL,
  location    TEXT              NOT NULL,
  temperature DOUBLE PRECISION  NULL,
  humidity    DOUBLE PRECISION  NULL,
  UNIQUE (time, location)
);

您还可以在创建表后创建唯一约束。使用语法 ALTER TABLE … ADD CONSTRAINT … UNIQUE.在此示例中,约束被命名为 conditions_time_location:

ALTER TABLE conditions
  ADD CONSTRAINT conditions_time_location
	UNIQUE (time, location);

当您向表添加唯一约束时,您无法插入违反该约束的数据。换句话说,如果您尝试将具有相同值的数据插入到约束所覆盖的列内的另一行,则会出现错误。

注意: 唯一约束必须包括所有分区列。这意味着超表的唯一约束必须包括时间列。如果您向超表添加了其他分区列,则约束也必须包括这些列。有关更多信息,请参阅有关超表和唯一索引的部分。

 

向具有唯一约束的表插入或更新数据

您可以告诉数据库,如果不违反约束,则插入新数据;如果违反,则更新现有行。使用语法 INSERT INTO … VALUES … ON CONFLICT … DO UPDATE.

例如,如果要更新具有指定 time 和 location 的行中的 temperature 和 humidity 值,请运行:

INSERT INTO conditions
  VALUES ('2017-07-28 11:42:42.846621+00', 'office', 70.2, 50.1)
  ON CONFLICT (time, location) DO UPDATE
	SET temperature = excluded.temperature,
		humidity = excluded.humidity;

 

对具有唯一约束的表插入或不执行任何操作

如果违反约束,您还可以告诉数据库不执行任何操作。不插入新数据,也不更新旧行。当将许多行作为一批写入时,这非常有用,可以防止整个事务失败。数据库引擎会跳过该行并继续前进。

要插入或不执行任何操作,请使用以下语法 INSERT INTO … VALUES … ON CONFLICT DO NOTHING:

INSERT INTO conditions
  VALUES ('2017-07-28 11:42:42.846621+00', 'office', 70.1, 50.0)
  ON CONFLICT DO NOTHING;

 

删除数据

您可以使用标准的 DELETE SQL 命令从超表中删除数据。如果您想在旧数据达到一定年限后将其删除,您还可以删除整个数据块或设置数据保留策略。

 

使用DELETE命令删除数据

要从表中删除数据,请使用语法 DELETE FROM …. 。在此示例中,如果行的 temperature 或 humidity 低于某个水平,则会从 conditions 表中删除数据:

DELETE FROM conditions WHERE temperature < 35 OR humidity < 60;

重要提示: 如果删除大量数据,请运行 VACUUM 或 VACUUM FULL 以从已删除或过时的行中回收存储。

 

通过删除块来删除数据

TimeUDB 允许您通过从超表中删除块来按年限删除数据。您可以通过手动操作,也可以使用数据保留策略来实现。

   

查询数据

TimeUDB 超表是 UnvDB 表。这意味着您可以使用标准的SQL命令查询它们。除了从 TimeUDB 架构和查询规划中的附加功能外,您还可以使用 UDBStudio 通过集中的SQL查询、交互式可视化和实时协作来处理数据。

 

关于查询数据

在 TimeUDB 中查询数据与在 UnvDB 中查询数据一样。如果您是从另一个 UnvDB 数据库迁移过来的,您可以重用现有的查询。

 

查询数据

您可以使用标准的 SELECT 命令从超表中查询数据。支持所有SQL子句和功能。使用 UDBStudio 进行集中化的SQL查询、交互式可视化和实时协作处理数据。

 

基本查询示例

以下是一些基本 SELECT 查询的示例。

返回表 conditions 中最近的100条记录。按从新到旧的顺序排列行:

SELECT * FROM conditions ORDER BY time DESC LIMIT 100;

返回过去12小时内写入 conditions 表的条目数:

SELECT COUNT(*) FROM conditions
  WHERE time > NOW() - INTERVAL '12 hours';

 

高级查询示例

以下是一些更高级 SELECT 查询的示例。

获取过去 3 小时内每个地点每 15 分钟的天气状况信息 。计算所进行的测量次数、最高温度和最大湿度。按最高温度对结果进行排序。

此示例使用 time_bucket 函数将数据聚合到 15 分钟的存储桶中:

SELECT time_bucket('15 minutes', time) AS fifteen_min,
	location,
	COUNT(*),
	MAX(temperature) AS max_temp,
	MAX(humidity) AS max_hum
  FROM conditions
  WHERE time > NOW() - INTERVAL '3 hours'
  GROUP BY fifteen_min, location
  ORDER BY fifteen_min DESC, max_temp DESC;

计算最近一天报告数据的配备空调的不同位置的数量:

SELECT COUNT(DISTINCT location) FROM conditions
  JOIN locations
	ON conditions.location = locations.location
  WHERE locations.air_conditioning = True
	AND time > NOW() - INTERVAL '1 day';

 

执行高级分析查询

你可以使用 TimeUDB 进行各种分析查询。其中一些查询是原生的 UnvDB 查询,而另一些则是 TimeUDB 提供的附加函数。本节包含最常见和最有用的分析查询。

 

计算中位数和百分位数

用 percentile_cont 计算百分位数。您还可以使用此函数查找第五十个百分位数或中位数。例如,要查找中值温度:

SELECT percentile_cont(0.5)
  WITHIN GROUP (ORDER BY temperature)
  FROM conditions;

 

计算累计和

用 sum(sum(column)) OVER(ORDER BY group) 求累计和。例如:

SELECT location, sum(sum(temperature)) OVER(ORDER BY location)
  FROM conditions
  GROUP BY location;

 

计算移动平均线

对于简单移动平均值,请 OVER 对多行使用窗口函数,然后对这些行计算聚合函数。例如,要通过对最近十个读数求平均值来查找设备的平滑温度:

SELECT time, AVG(temperature) OVER(ORDER BY time
	  ROWS BETWEEN 9 PRECEDING AND CURRENT ROW)
	AS smooth_temp
  FROM conditions
  WHERE location = 'garage' and time > NOW() - INTERVAL '1 day'
  ORDER BY time DESC;

 

计算增加的值

要计算值的增加,您需要考虑计数器重置。如果主机重新启动或容器重新启动,则可能会发生计数器重置。此示例查找发送的字节数,并考虑计数器重置:

SELECT
  time,
  (
	CASE
	  WHEN bytes_sent >= lag(bytes_sent) OVER w
		THEN bytes_sent - lag(bytes_sent) OVER w
	  WHEN lag(bytes_sent) OVER w IS NULL THEN NULL
	  ELSE bytes_sent
	END
  ) AS "bytes"
  FROM net
  WHERE interface = 'eth0' AND time > NOW() - INTERVAL '1 day'
  WINDOW w AS (ORDER BY time)
  ORDER BY time

 

计算变化率

与增加一样,速率适用于计数器单调增加的情况。如果您的样本间隔是可变的,或者您在不同系列之间使用不同的采样间隔,则将值标准化为公共时间间隔很有帮助,以使计算值具有可比性。此示例查找每秒发送的字节数,并考虑计数器重置:

SELECT
  time,
  (
	CASE
	  WHEN bytes_sent >= lag(bytes_sent) OVER w
		THEN bytes_sent - lag(bytes_sent) OVER w
	  WHEN lag(bytes_sent) OVER w IS NULL THEN NULL
	  ELSE bytes_sent
	END
  ) / extract(epoch from time - lag(time) OVER w) AS "bytes_per_second"
  FROM net
  WHERE interface = 'eth0' AND time > NOW() - INTERVAL '1 day'
  WINDOW w AS (ORDER BY time)
  ORDER BY time

 

计算增量

在许多监控和物联网用例中,设备或传感器报告的指标不会经常更改,任何更改都被视为异常。当您查询这些值随时间的变化时,您通常不想传输所有值,而只想传输观察到变化的值。这有助于最大限度地减少发送的数据量。您可以使用窗口函数和子选择的组合来实现此目的。此示例使用 diff 来过滤值未更改的行,并且仅传输值已更改的行:

SELECT time, value FROM (
  SELECT time,
	value,
	value - LAG(value) OVER (ORDER BY time) AS diff
  FROM hypertable) ht
WHERE diff IS NULL OR diff != 0;

 

计算组内指标的变化

要按某个字段对数据进行分组,并计算每个组内指标的变化,请使用LAG … OVER (PARTITION BY …)。例如,给定一些天气数据,计算每个城市的温度变化:

SELECT ts, city_name, temp_delta
FROM (
  SELECT
	ts,
	city_name,
	avg_temp - LAG(avg_temp) OVER (PARTITION BY city_name ORDER BY ts) as temp_delta
  FROM weather_metrics_daily
) AS temp_change
WHERE temp_delta IS NOT NULL
ORDER BY bucket;

 

将数据分组到时间桶中

TimeUDB 的 time_bucket 函数扩展了 UnvDB 的 date_bin 函数。时间桶接受任意的时间间隔以及可选的偏移量,并返回桶的开始时间。例如:

SELECT time_bucket('5 minutes', time) five_min, location, last(temperature, time)
  FROM conditions
  GROUP BY five_min, location
  ORDER BY five_min DESC LIMIT 12;

 

获取列中的第一个或最后一个值

TimeUDB 的 first 和 last 函数允许您按照另一列的顺序获取一列的值。这通常用于聚合。这些示例查找组的最后一个元素:

SELECT location, last(temperature, time)
  FROM conditions
  GROUP BY location;

 

SELECT time_bucket('5 minutes', time) five_min, location, last(temperature, time)
  FROM conditions
  GROUP BY five_min, location
  ORDER BY five_min DESC LIMIT 12;

 

生成直方图

TimeUDB 的 histogram 函数允许您生成数据的直方图。此示例定义了一个直方图,其中定义了 60 到 85 范围内的五个存储桶。生成的直方图有七个存储桶;第一个是低于最小阈值 60 的值,中间的五个容器是在规定范围内的值,最后一个是高于 85 的值:

SELECT location, COUNT(*),
	histogram(temperature, 60.0, 85.0, 5)
   FROM conditions
   WHERE time > NOW() - INTERVAL '7 days'
   GROUP BY location;

该查询输出如下数据:

 location   | count |        histogram
 -----------+-------+-------------------------
 office     | 10080 | {0,0,3860,6220,0,0,0}
 basement   | 10080 | {0,6056,4024,0,0,0,0}
 garage     | 10080 | {0,2679,957,2420,2150,1874,0}

 

填补时间序列数据的空白

即使部分范围内不存在数据,您也可以显示选定时间范围内的记录。这通常称为间隙填充,通常涉及为任何缺失数据记录空值的操作。

在这个例子中,我们使用了交易数据,这些数据包括:一个时间戳(time)、正在交易的资产代码(asset_code)、资产的价格(price)以及正在交易的资产数量(volume)。

创建一个查询,查询 9 月份每天交易的资产“TIMS”的交易量:

SELECT
	time_bucket('1 day', time) AS date,
	sum(volume) AS volume
  FROM trades
  WHERE asset_code = 'TIMS'
	AND time >= '2021-09-01' AND time < '2021-10-01'
  GROUP BY date
  ORDER BY date DESC;

该查询输出如下数据:

 date                   | volume
------------------------+--------
 2021-09-29 00:00:00+00 |  11315
 2021-09-28 00:00:00+00 |   8216
 2021-09-27 00:00:00+00 |   5591
 2021-09-26 00:00:00+00 |   9182
 2021-09-25 00:00:00+00 |  14359
 2021-09-22 00:00:00+00 |   9855

从输出中你可以看到,09-23、09-24 或 09-30 这几天没有包含任何记录,因为这几天没有记录任何交易数据。为了包含每个缺失天数的时间记录,你可以使用 TimeUDB 的 time_bucket_gapfill 函数,该函数根据给定时间范围内的间隔生成一系列时间桶。在这个例子中,间隔是一天,时间范围是整个九月:

SELECT
  time_bucket_gapfill('1 day', time) AS date,
  sum(volume) AS volume
FROM trades
WHERE asset_code = 'TIMS'
  AND time >= '2021-09-01' AND time < '2021-10-01'
GROUP BY date
ORDER BY date DESC;

该查询输出如下数据:

 date                   | volume
------------------------+--------
 2021-09-30 00:00:00+00 |
 2021-09-29 00:00:00+00 |  11315
 2021-09-28 00:00:00+00 |   8216
 2021-09-27 00:00:00+00 |   5591
 2021-09-26 00:00:00+00 |   9182
 2021-09-25 00:00:00+00 |  14359
 2021-09-24 00:00:00+00 |
 2021-09-23 00:00:00+00 |
 2021-09-22 00:00:00+00 |   9855

你也可以使用 TimeUDB 的 time_bucket_gapfill 函数来生成包括时间戳在内的数据点。这对于需要即使空值也具有时间戳的图形库来说很有用,因为它们可以准确地绘制图形中的空白。在这个例子中,你在过去两周内生成了1080个数据点,用空值填充了空白,并为每个空值分配了一个时间戳。

SELECT
  time_bucket_gapfill(INTERVAL '2 weeks' / 1080, time, now() - INTERVAL '2 weeks', now()) AS btime,
  sum(volume) AS volume
FROM trades
WHERE asset_code = 'TIMS'
  AND time >= now() - INTERVAL '2 weeks' AND time < now()
GROUP BY btime
ORDER BY btime;

该查询输出如下数据:

 btime                  | volume
------------------------+----------
 2021-03-09 17:28:00+00 |  1085.25
 2021-03-09 17:46:40+00 |  1020.42
 2021-03-09 18:05:20+00 |
 2021-03-09 18:24:00+00 |  1031.25
 2021-03-09 18:42:40+00 |  1049.09
 2021-03-09 19:01:20+00 |  1083.80
 2021-03-09 19:20:00+00 |  1092.66
 2021-03-09 19:38:40+00 |
 2021-03-09 19:57:20+00 |  1048.42
 2021-03-09 20:16:00+00 |  1063.17
 2021-03-09 20:34:40+00 |  1054.10
 2021-03-09 20:53:20+00 |  1037.78

 

通过推进最后的观察来填补空白

如果您的数据集合仅在实际值更改时记录行,则您的可视化可能仍需要所有数据点才能正确显示结果。在这种情况下,您可以结转最后一个观测值来填补空白。例如:

SELECT
  time_bucket_gapfill(INTERVAL '5 min', time, now() - INTERVAL '2 weeks', now()) as 5min,
  meter_id,
  locf(avg(data_value)) AS data_value
FROM my_hypertable
WHERE
  time > now() - INTERVAL '2 weeks'
  AND meter_id IN (1,2,3,4)
GROUP BY 5min, meter_id

 

找到每个唯一项的最后一个点

您可以找到数据库中每个唯一项的最后一个点。例如,每个物联网设备的最后记录测量值、资产跟踪中每个项目的最后位置或证券的最后价格。最小化要搜索最后一个点的数据量的标准方法是使用时间谓词来严格限制要遍历的时间量或块的数量。除非所有项目在该时间范围内至少有一条记录,否则此方法不起作用。更可靠的方法是使用最后点查询来确定每个唯一项的最后记录。

在此示例中,您为每台正在跟踪的车辆创建一个元数据表,以及包含给定时间车辆位置的第二个时间序列表,这对于资产跟踪或车队管理很有用:

CREATE TABLE vehicles (
  vehicle_id INTEGER PRIMARY KEY,
  vin_number CHAR(17),
  last_checkup TIMESTAMP
);

CREATE TABLE location (
  time TIMESTAMP NOT NULL,
  vehicle_id INTEGER REFERENCES vehicles (vehicle_id),
  latitude FLOAT,
  longitude FLOAT
);

SELECT create_hypertable('location', by_range('time'));

你可以使用第一个表(它提供了一组不同的车辆)来对位置表执行一个 LATERAL JOIN 操作:

SELECT data.* FROM vehicles v
  INNER JOIN LATERAL (
	SELECT * FROM location l
	  WHERE l.vehicle_id = v.vehicle_id
	  ORDER BY time DESC LIMIT 1
  ) AS data
ON true
ORDER BY v.vehicle_id, data.time DESC;

			time            | vehicle_id | latitude  |  longitude
----------------------------+------------+-----------+-------------
 2017-12-19 20:58:20.071784 |         72 | 40.753690 |  -73.980340
 2017-12-20 11:19:30.837041 |        156 | 40.729265 |  -73.993611
 2017-12-15 18:54:01.185027 |        231 | 40.350437 |  -74.651954

这种方法需要保留一个单独的表来存储不同的项目标识符或名称。你可以通过在超表到元数据表之间使用外键来实现这一点,如示例中的 REFERENCES 定义所示。

元数据表可以通过业务逻辑来填充,例如当车辆首次在系统中注册时。或者,您可以在对超表执行插入或更新时使用触发器动态填充它。例如:

CREATE OR REPLACE FUNCTION create_vehicle_trigger_fn()
  RETURNS TRIGGER LANGUAGE PLPGSQL AS
$BODY$
BEGIN
  INSERT INTO vehicles VALUES(NEW.vehicle_id, NULL, NULL) ON CONFLICT DO NOTHING;
  RETURN NEW;
END
$BODY$;

CREATE TRIGGER create_vehicle_trigger
  BEFORE INSERT OR UPDATE ON location
  FOR EACH ROW EXECUTE PROCEDURE create_vehicle_trigger_fn();

你也可以通过对位置超表执行松散索引扫描来实现这一功能,而无需单独的元数据表,尽管这需要更多的计算资源。

 

查询数据故障排除

本节包含一些解决查询中遇到的常见问题的想法。

 

查询的执行速度比预期慢

要对查询进行故障排除,您可以检查其 EXPLAIN 计划。

UnvDB 的 EXPLAIN 功能允许用户了解 UnvDB 用于执行查询的底层查询计划。 UnvDB 可以通过多种方式执行查询:例如,可以使用慢速序列扫描或更高效的索引扫描来完成查询。计划的选择取决于表上创建的索引、UnvDB 有关数据的统计信息以及各种计划程序设置。 EXPLAIN 输出让您知道 UnvDB 为查询选择哪个计划。

要了解超表上的查询性能,我们建议首先通过运行来确保超表上的规划器统计信息和表维护是最新的 VACUUM ANALYZE < your-hypertable >:

EXPLAIN (ANALYZE on, BUFFERS on) <original query>;

如果您怀疑性能问题是由于磁盘的IO操作缓慢造成的,您可以在运行上述的EXPLAIN之前,通过启用track_io_timing 变量来获取更多的信息。具体做法是执行命令 SET track_io_timing = ‘on’ 。

   

时间桶

时间桶使您能够按时间间隔聚合数据。例如,您可以将数据分组为5分钟、1小时和3天的时间桶,以计算汇总值。

 

关于时间桶

time_bucket 函数允许您将数据聚合到时间段中,例如:5 分钟、1 小时或 3 天。它与 UnvDB 的 date_bin 功能类似,但它在存储桶大小和启动时间方面为您提供了更大的灵活性。

时间桶对于处理时间序列数据至关重要。您可以使用它来汇总数据以进行分析或下采样。例如,您可以计算过去一天传感器读数的 5 分钟平均值。您可以根据需要执行这些汇总,或者在连续聚合中预先计算它们。

本节介绍时间分桶的工作原理。有关该函数的示例。

 

时间桶的工作原理

时间桶将数据分组到时间间隔中。使用 time_bucket ,间隔长度可以是任意数量的微秒、毫秒、秒、分钟、小时、天、周、月、年或世纪。

time_bucket 函数通常与 GROUP BY 结合使用来聚合数据。例如,您可以计算存储桶内的平均值、最大值、最小值或值的总和。

时间桶01

 

原点

原点决定了时间桶的开始和结束时间。默认情况下,一个时间桶并不是从数据中的最早时间戳开始的。通常会有一个更合逻辑的时间点。例如,您可能在 00:37 收集第一个数据点,但您可能希望您的每日桶从午夜开始。同样,您可能在周三收集第一个数据点,但您可能希望您的每周桶从周日或周一开始计算。

相反,时间根据与原点的间隔被划分为多个桶。下图使用 2 周存储桶的示例展示了如何操作。存储桶的第一个可能的开始日期是 origin。存储桶的下一个可能开始日期是 origin + bucket interval。如果您的第一个时间戳不完全落在可能的开始日期,则前一个开始日期将用作存储桶的开始日期。

时间桶02

例如,假设您的数据的最早时间戳是 2020 年 4 月 24 日。如果您以两周为间隔进行存储,则第一个存储桶不会从 4 月 24 日(星期五)开始。它也不是从 4 月 20 日(即前一个星期一)开始。它从 4 月 13 日开始,因为从 2000 年 1 月 3 日(本例中为默认起点)开始以两周为增量计数,您可以到达 2020 年 4 月 13 日。

 

默认原点

对于不包含月或年的间隔,默认原点为 2000 年 1 月 3 日。对于月、年或世纪间隔,默认原点为 2000 年 1 月 1 日。对于整数时间值,默认原点为 0。

这些选择使得时间桶的时间范围更加直观。由于 2000 年 1 月 3 日是星期一,因此每周时间段从星期一开始。这符合计算日历周的 ISO 标准。每月和每年时间段使用 2000 年 1 月 1 日作为原点。这允许他们从日历月或年的第一天开始。

 

时区

起始时间取决于时间值的数据类型。

如果您使用 TIMESTAMP ,默认情况下,桶的开始时间会与 00:00:00 对齐。每日和每周的桶从 00:00:00 开始。较短的桶的开始时间取决于从原点日期的 00:00:00 开始,以桶的增量进行计数的时间。

如果您使用 TIMESTAMPTZ ,默认情况下,桶的开始时间将与协调世界时(UTC)的00:00:00对齐。要将时间桶与另一个时区对齐,请设置 timezone 参数。

 

使用时间桶对时间序列数据进行分组

time_bucket 函数可帮助您对数据进行分组,以便您可以在任意时间间隔内执行聚合计算。为此,它通常与 GROUP BY 结合使用。

本节展示使用示例 time_bucket。

 

按时间段对数据进行分组并计算汇总值

将数据按时间桶分组,并计算某列的总计值。例如,在名为 weather_conditions 的表中计算日平均温度。该表有一个名为 time 的时间列和一个 temperature 温度列。

SELECT time_bucket('1 day', time) AS bucket,
  avg(temperature) AS avg_temp
FROM weather_conditions
GROUP BY bucket
ORDER BY bucket ASC;

time_bucket 函数返回时间桶的开始时间。在这个例子中,第一个桶从 2016 年 11 月 15 日的午夜开始,并聚合了该天的所有数据。

bucket                 |      avg_temp
-----------------------+---------------------
2016-11-15 00:00:00+00 | 68.3704391666665821
2016-11-16 00:00:00+00 | 67.0816684374999347

 

按时间段对数据进行分组并显示该时间段的结束时间

默认情况下,time_bucket 列显示时间桶的开始时间。如果你更喜欢显示结束时间,可以通过对 time 进行数学运算来移动显示的时间。

例如,你可以计算每5分钟间隔的最小和最大CPU使用率,并显示该间隔的结束时间。示例表名为 metrics。它有一个名为 time 的时间列和一个名为 cpu 的CPU使用率列。

SELECT time_bucket('5 min', time) + '5 min' AS bucket,
  min(cpu),
  max(cpu)
FROM metrics
GROUP BY bucket
ORDER BY bucket DESC;

添加 + ‘5 min’ 会将显示的时间戳更改为时间桶的结束时间。它不会更改时间桶所跨越的时间范围。

 

按时间桶对数据进行分组并更改桶的时间范围

要更改时间桶所跨越的时间范围,请使用接受 INTERVAL 参数的 offset 参数。正偏移量会使桶的开始和结束时间向后移动。负偏移量会使桶的开始和结束时间向前移动。

例如,您可以计算 5 小时间隔的平均 CPU 使用率,并将所有存储桶的开始和结束时间移至 1 小时后:

SELECT time_bucket('5 hours', time, '1 hour'::INTERVAL) AS bucket,
  avg(cpu)
FROM metrics
GROUP BY bucket
ORDER BY bucket DESC;

 

计算单个值的时间桶

时间桶通常与 GROUP BY 一起使用来聚合数据。但您也可以在单个时间值上运行 time_bucket。这对于测试和学习很有用,因为您可以看到一个值落入哪个桶中。

例如,要查看2021年1月5日将落入哪个1周时间桶,请运行:

SELECT time_bucket(INTERVAL '1 week', TIMESTAMP '2021-01-05');

该函数的返回值为2021-01-04 00:00:00。时间桶的开始时间是那一周的周一,即午夜时分。

 

解决时间桶问题

本节包含一些解决时间段常见问题的想法。

 

分层连续聚合因桶宽度不兼容而失败

ERROR:  cannot create continuous aggregate with incompatible bucket width
DETAIL:  Time bucket width of "<BUCKET>" [1 year] should be multiple of the time bucket width of "<BUCKET>" [1 day].

如果您尝试创建一个分层连续聚合,您必须使用兼容的时间桶。您不能在一个具有可变宽度时间桶的连续聚合之上,使用固定宽度时间桶来创建连续聚合。分层连续聚合因桶宽度不兼容而失败。

   

超表

超表是具有特殊功能的 UnvDB 表,可以轻松处理时间序列数据。您可以使用常规 UnvDB 表执行的任何操作,都可以使用超级表执行。此外,您还可以获得改进的时间序列数据性能和用户体验的好处。

 

关于超表

超级表是 UnvDB 表,可按时间自动对数据进行分区。您与超表的交互方式与常规 UnvDB 表相同,但具有额外的功能,使管理时间序列数据变得更加容易。

在 TimeUDB 中,超表与常规 UnvDB 表并存。使用超表来存储时间序列数据。这可以提高插入和查询性能,并访问有用的时间序列功能。将常规 UnvDB 表用于其他关系数据。

借助超表,TimeUDB 通过根据时间参数对时间序列数据进行分区,可以轻松提高插入和查询性能。在幕后,数据库执行设置和维护超表分区的工作。同时,您可以插入和查询数据,就好像它们都存在于单个常规 UnvDB 表中一样。

 

超表分区

当您创建和使用超表时,它会自动按时间分区数据,也可以选择按空间分区。

每个超表都由称为块的子表组成。每个块都分配有一个时间范围,并且仅包含该范围内的数据。如果超表也按空间分区,则每个块也会分配空间值的子集。

注意: 当一个数据块被创建时,其创建时间会被存储在目录元数据中。这个数据块的创建时间不应与数据块所包含的数据的分区范围相混淆。在某些情况下,某些功能可以使用这个数据块创建时间的元数据,这是有意义的。

 

时间分区

超表中的每个数据块只存储来自特定时间范围的数据。当您插入来自尚没有数据块的时间范围的数据时, TimeUDB 会自动创建一个数据块来存储它。

默认情况下,每个数据块覆盖7天的时间范围。您可以根据需要进行更改。例如,如果您将 chunk_time_interval 设置为1天,则每个数据块将存储同一天的数据。不同日期的数据将存储在不同的数据块中。

超表01

注意: 在的数据块范围。如果某个潜在的数据块范围存在数据,那么该数据块就会被创建。 在实际应用中,这意味着您最早的数据块的开始时间并不一定等于超表中的最早时间戳。相反,开始时间和最早时间戳之间可能存在时间差。这不会影响您与超表的常规交互,但可能会影响您在检查超表时看到的数据块数量。

 

时间划分的最佳实践

数据块的大小会影响插入和查询性能。您希望数据块足够小以适应内存。这允许您插入和查询最新数据而无需从磁盘读取。但是,您不希望有太多小而稀疏填充的数据块。这可能会影响查询计划时间和压缩效果。

我们建议设置 chunk_time_interval,以便25%的主内存可以存储每个活跃超表中的一个数据块(包括其索引)。您可以根据您的数据速率估算所需的时间间隔。例如,如果您每天大约写入2 GB的数据并且拥有64 GB的内存,则将时间间隔设置为1周。如果您在同一台机器上每天大约写入10 GB的数据,则将时间间隔设置为1天。

注意: 如果您使用昂贵的索引类型,例如某些PostGIS地理空间索引,请务必检查数据块及其索引的总大小。

 

超表索引

默认情况下,创建超表时会自动创建索引。您可以通过将 create_default_indexes 选项设置为 false 来阻止索引的创建。

默认索引为:

  • 在所有超表上,时间索引,降序

  • 在具有空间分区的超表上,空间参数和时间的索引

超表对唯一约束和索引有一些限制。如果您想在超表上创建唯一索引,它必须包含表的所有分区列。

 

分析超表

您可以使用 UnvDB 的 ANALYZE 命令来查询超表中的所有数据块。ANALYZE 命令收集的统计数据被 UnvDB 规划器用来创建最佳的查询计划。有关ANALYZE命令的更多信息,请参阅 UnvDB 文档。

 

创建一个超表

要创建超表,您需要创建一个标准的 UnvDB 表,然后将其转换为超表。

 

创建超表

创建 Timeudb 数据库后,您就可以创建第一个超级表了。创建超级表分为两步:

  1. 像往常一样创建 UnvDB 表;

  2. 将其转换为超表;

您还可以创建分布式超表。

超表是为时间序列数据设计的,因此您的表需要有一个用于存储时间值的列。这可以是timestamptz、date或integer类型。请确保将 time 列的数据类型设置为 timestamptz,而不是 timestamp。

1、创建一个标准 UnvDB 表:

CREATE TABLE conditions (
   time        TIMESTAMPTZ       NOT NULL,
   location    TEXT              NOT NULL,
   device      TEXT              NOT NULL,
   temperature DOUBLE PRECISION  NULL,
   humidity    DOUBLE PRECISION  NULL
);

2、将表转换为超表。指定要转换的表的名称以及保存其时间值的列。

SELECT create_hypertable('conditions', by_range('time'));

注意: 如果您的表中已经有数据,您可以在创建超表时迁移这些数据。调用 create_hypertable 函数时,将 migrate_data 参数设置为 true。如果您有大量数据,这可能需要很长时间。

 

更改超表块间隔

调整超表的数据块间隔可以提高数据库的性能。这适用于常规超表和分布式超表。

 

检查当前的数据块间隔设置

通过查询 TimeUDB 目录来检查当前的数据块间隔设置。例如:

SELECT *
  FROM timeudb_information.dimensions
  WHERE hypertable_name = 'metrics';

结果如下:

hypertable_schema |  hypertable_name | dimension_number | column_name |       column_type        | dimension_type | time_interval | integer_interval | integer_now_func | num_partitions
------------------+------------------+------------------+-------------+--------------------------+----------------+---------------+------------------+------------------+----------------
public            | metrics          |                1 | recorded    | timestamp with time zone | Time           | 1 day         |                  |                  |

注意: 基于时间的间隔长度以微秒为单位报告。

 

创建超表时更改块间隔长度

默认的数据块间隔是7天。在创建超表时,如果想要更改这个设置,可以在创建超表时指定一个不同的 chunk_time_interval。在这个例子中,要转换的表名为 conditions,它在一个名为 time 的列中存储时间值:

SELECT create_hypertable(
  'conditions',
  by_range('time', INTERVAL '1 day')
);

 

更改现有超表上的块间隔长度

要更改已存在的超表或分布式超表上的数据块间隔,请使用 set_chunk_time_interval 函数。在这个例子中,超表的名称是 conditions:

SELECT set_chunk_time_interval('conditions', INTERVAL '24 hours');

当你更改 chunk_time_interval 时,新设置仅适用于新的数据块,而不适用于现有的数据块。在实际应用中,这意味着设置一个过长的间隔可能需要很长时间来纠正。例如,如果你将 chunk_time_interval 设置为1年并开始插入数据,那么在那一年里,你无法再缩短数据块的长度。如果你需要纠正这种情况,可以创建一个新的超表并迁移你的数据。

虽然数据块的更换不会降低性能,但创建数据块所需的锁定时间确实比向已创建的数据块中执行正常的 INSERT 操作要长。这意味着,如果同时创建多个数据块,事务会相互阻塞,直到第一个事务完成为止。

 

修改超表

你可以使用 UnvDB 的 ALTER TABLE 命令来修改超表,例如添加列。这对于常规超表和分布式超表都有效。

 

向超表添加一列

你可以使用 ALTER TABLE 命令向超表添加一列。在这个例子中,超表的名称是 conditions,新列的名称是 humidity:

ALTER TABLE conditions
  ADD COLUMN humidity DOUBLE PRECISION NULL;

如果你要添加的列默认值设置为 NULL,或者没有默认值,那么添加列相对较快。如果你将默认值设置为非空值,那么需要的时间会更长,因为它需要为所有现有数据块的所有现有行填充此值。

重要提示: 对于启用了压缩功能的超表,你无法添加带有约束或默认值的列。要添加该列,你需要先对超表中的数据进行解压缩,添加列,然后再压缩数据。

 

重命名超表

你可以使用 ALTER TABLE 命令来更改超表的名称。在这个例子中,超表的名称是 conditions,现在被更改为新名称 weather:

ALTER TABLE conditions
  RENAME TO weather;

 

在超表上创建唯一索引

你可以在超表上使用唯一索引来强制约束。你不需要在超表上创建唯一索引。但是,当你创建唯一索引时,它必须包含超表的所有分区列。

注意: 如果你有一个主键,那么你就有一个唯一索引。在 UnvDB 中,主键是一个带有 NOT NULL 约束的唯一索引。

要在超表上创建唯一索引:

  1. 确定你的分区列。

  2. 创建一个包含所有这些列的唯一索引,并且可选择性地包含额外的列。

 

确定分区列

在创建唯一索引之前,您需要确定超级表上允许使用哪些唯一索引。首先确定您的分区列。

TimeUDB 使用这些列来对超表进行分区。

  • 用于创建超表的时间列。每个 TimeUDB 超表都是按时间分区的。

  • 任何空间分区列。空间分区是可选的,并非每个超表都包含。如果在创建超表时指定了 partitioning_column 参数,那么您就有一个空间分区列。

 

在超表上创建唯一索引

当您在超表上创建唯一索引时,它必须包含您之前确定的所有分区列。它也可以包含其他列,并且这些列可以按任意顺序排列。

注意: 此限制是必要的,以确保索引中的全局唯一性。

使用 CREATE UNIQUE INDEX 命令创建唯一索引。确保在索引中包含所有分区列。如果需要,您还可以包含其他列。

例如,对于一个名为 hypertable_example 的超表,它在 time 和 device_id 上进行分区,您需要在 time 和 device_id 上创建一个索引:

CREATE UNIQUE INDEX idx_deviceid_time
  ON hypertable_example(device_id, time);

您也可以在 time、user_id 和 device_id 上创建唯一索引。请注意,device_id 不是一个分区列,但这样做仍然有效:

CREATE UNIQUE INDEX idx_userid_deviceid_time
  ON hypertable_example(user_id, device_id, time);

您不能在不含 time 列的情况下创建唯一索引,因为 time 是一个分区列。例如,这样做是无效的:

-- This gives you an error
CREATE UNIQUE INDEX idx_deviceid
  ON hypertable_example(device_id);

您收到错误:

ERROR: cannot create a unique index without the column "<COLUMN_NAME>" (used in partitioning)

通过添加 time 到唯一索引来修复错误。

 

从具有唯一索引的表创建超表

如果您在将表转换为超表之前在该表上创建了唯一索引,那么相同的限制会反过来适用。您只能根据唯一索引中的列对表进行分区。

1、创建您的表格。例如:

CREATE TABLE hypertable_example(
  time TIMESTAMPTZ,
  user_id BIGINT,
  device_id BIGINT,
  value FLOAT
);

2、在表上创建唯一索引。在此示例中,索引位于 device_id 和 time 上:

CREATE UNIQUE INDEX idx_deviceid_time
  ON hypertable_example(device_id, time);

3、将表转换为 time 单独分区的超表:

SELECT * from create_hypertable('hypertable_example', by_range('time'));

另外,您还可以将表转换为按 time 和 device_id 分区的超表:

SELECT * FROM create_hypertable('hypertable_example', by_range('time'));
SELECT * FROM add_dimension('hypertable_example', by_hash('device_id', 4));

您无法将表转换为按 time 和 user_id 分区的超表,因为 user_id 不是唯一索引的一部分。这样做是无效的:

-- This gives you an error
SELECT * FROM create_hypertable('hypertable_example', by_range('time'));
SELECT * FROM add_dimension('hypertable_example', by_hash('user_id', 4));

您收到错误:

ERROR: cannot create a unique index without the column "<COLUMN_NAME>" (used in partitioning)

请注意,错误是在创建索引时产生的,而不是在创建超表时。这是因为 TimeUDB 在将表转换为超表后会重新创建索引。

通过添加 user_id 到唯一索引来修复错误。

 

删除超表

使用标准 UnvDB DROP TABLE 命令删除超表:

DROP TABLE <TABLE_NAME>;

 

解决超表问题

本节包含一些解决超表遇到的常见问题的想法。

ERROR: temporary file size exceeds temp_file_limit

当您尝试压缩块时,尤其是块非常大时,您可能会收到此错误。压缩操作将文件写入新的压缩块表,该表写入临时内存中。可用临时内存的最大量由该参数确定 temp_file_limit。您可以通过调整 temp_file_limit 和 maintenance_work_mem 参数来解决此问题。

 

无法将列添加到压缩的超表

ERROR: cannot add column with constraints or defaults to a hypertable that has compression enabled

如果您尝试将带有约束或默认值的列添加到启用了压缩的超表中,则可能会收到此错误。添加列需要先解压超表中的数据,添加列,然后压缩数据。

 

用户权限不允许压缩或解压缩块

ERROR:  must be owner of hypertable "HYPERTABLE_NAME"

如果您尝试使用非特权用户帐户压缩或解压缩块,则可能会收到此错误。要压缩或解压缩块,您的用户帐户必须具有允许其 CREATE INDEX 对块执行的权限。您可以在命令提示符下使用以下命令检查当前用户的权限 ud_sql:

\dn+ <USERNAME>

要解决此问题,请使用以下命令授予您的用户帐户适当的权限:

GRANT PRIVILEGES
	ON TABLE <TABLE_NAME>
	TO <ROLE_TYPE>;

有关 GRANT 命令的更多信息,请参阅 UnvDB 文档。

 

操作超出元组解压限制

ERROR: tuple decompression limit exceeded by operation

当从压缩块中插入、更新或删除元组时,可能需要解压缩元组。当您更新现有元组或有需要在插入时验证的约束时,就会发生这种情况。如果您碰巧使用单个命令触发大量解压,则最终可能会耗尽存储空间。因此,对单个命令可以解压缩的元组数量进行了限制。

可以增加或关闭该限制(设置为 0),如下所示:

-- set limit to a milion tuples
SET timeudb.max_tuples_decompressed_per_dml_transaction TO 1000000;
-- disable limit by setting to 0
SET timeudb.max_tuples_decompressed_per_dml_transaction TO 0;

 

删除块超时

当您删除一个块时,它需要独占锁。如果一个块正在被另一个会话访问,则不能同时删除该块。如果删除块操作无法获得该块的锁,则会超时并且进程失败。要解决此问题,请检查锁定块的内容。在某些情况下,这可能是由连续聚合或访问块的其他进程引起的。当删除块操作可以获得该块的独占锁时,它会按预期完成。

有关锁的更多信息,请参阅 UnvDB 锁监控文档。

 

无法在超表上创建唯一索引,或者无法创建具有唯一索引的超表

ERROR: cannot create a unique index without the column "<COLUMN_NAME>" (used in partitioning)

在两种情况下您可能会遇到唯一索引和分区列错误:

  • 在超表上创建主键或唯一索引时

  • 从已有唯一索引或主键的表创建超表时

有关如何解决此问题的更多信息,请参阅有关在超级表上创建唯一索引的部分。

 

重建超表索引以修复大型索引

ERROR:  invalid attribute number -6 for _hyper_2_839_chunk
CONTEXT:  SQL function "hypertable_local_size" statement 1 PL/pgSQL function hypertable_detailed_size(regclass) line 26 at RETURN QUERY SQL function "hypertable_size" statement 1
SQL state: XX000

如果您的超表索引变得非常大,您可能会看到此错误。要解决该问题,请使用以下命令重建超表索引:

reindex table _timeudb_internal._hyper_2_1523284_chunk

   

架构管理

数据库架构定义了数据库中的表和索引的组织方式。使用适合您的工作负载的架构可以显着提高性能。

 

关于架构

数据库架构定义了数据库中的表和索引的组织方式。使用适合您的工作负载的架构可以显着提高性能。相反,使用不合适的架构可能会导致性能显着下降。

如果您正在处理半结构化数据,例如收集不同测量值的物联网传感器的读数,您可能需要灵活的架构。在这种情况下,您可以使用 UnvDB JSON 和 JSONB 数据类型。

TimeUDB 支持 UnvDB 中支持的所有表对象,包括数据类型、索引和触发器。但是,当您创建超表时,请将time 列的数据类型设置为 timestamptz 而非 timestamp。有关更多信息,请参阅 UnvDB 时间戳。

本节介绍如何设计架构、索引和表空间如何工作以及如何使用 UnvDB 约束类型。它还包含示例来帮助您创建自己的架构,并了解如何使用 JSON 和 JSONB 来处理半结构化数据。

 

数据索引

由于查找数据可能需要很长时间,尤其是当您的超表中包含大量数据时,您可以使用索引来加速从非压缩块(使用它们自己的列索引)中的读取操作。

对于时间序列数据,您可以在任何列组合上创建索引,只要包括 time 列即可。您选择在哪一列上创建索引取决于您存储的数据类型。当您创建超表时,请将 time 列的数据类型设置为 timestamptz 而不是 timestamp。有关更多信息,请参阅 UnvDB 的时间戳。

注意: 虽然可以添加不包括 time 列的索引,但这样做会导致非常慢的摄取速度。对于时间序列数据,按时间列进行索引允许每个块创建一个索引。

以从名为 office 和 garage 的两个位置收集的温度为例:

一个按 (location, time DESC) 排序的索引是这样组织的:

garage-0940
garage-0930
garage-0920
garage-0910
office-0930
office-0920
office-0910

按 (time DESC, location) 排序的索引的组织方式如下:

0940-garage
0930-garage
0930-office
0920-garage
0920-office
0910-garage
0910-office

关于索引的一个好的经验法则是分层思考。首先选择您通常想要运行等式运算符的列,例如 location = garage。然后通过选择要使用范围运算符的列来完成,例如 time > 0930。

作为一个更复杂的示例,假设您有多个设备跟踪 1,000 家不同的零售商店。每个商店有 100 台设备,并且有 5 种不同类型的设备。所有这些设备都将指标报告为 float 值,并且您决定将所有指标存储在同一个表中,如下所示:

CREATE TABLE devices (
	 time timestamptz,
	 device_id int,
	 device_type int,
	 store_id int,
	 value float
);

当您创建此表时,时间列上会自动生成一个索引,从而使基于时间的查询速度更快。

如果您想根据除时间以外的其他条件查询数据,可以创建不同的索引。例如,您可能只想查询给定 device_id 上个月的数据。或者,您可以查询单个 store_id 过去三个月的所有数据。

您想保留时间上的索引,以便您可以快速过滤给定的时间范围,并在 device_id 和 store_id 上添加另一个索引。这会创建一个复合索引。复合索引 (store_id, device_id, time) 首先按 store_id排序。然后,每个唯一的 store_id 将按 device_id 的顺序排序。具有相同 store_id 和 device_id 的每个条目随后按时间排序。要创建此索引,请使用以下命令:

CREATE INDEX ON devices (store_id, device_id, time DESC);

当您的超表上有此复合索引时,您可以运行一系列不同的查询。这里有些例子:

SELECT * FROM devices WHERE store_id = x

这会查询列表中具有特定 store_id 的部分。此查询对该索引是有效的,但可能会有些臃肿;仅对 store_id 建立索引可能会更高效。

SELECT * FROM devices WHERE store_id = x, time > 10

此查询效率不高,因为它需要扫描列表的多个部分。这是因为对于不同设备,time > 10 的数据部分会位于列表的不同区域。在这种情况下,考虑在(store_id,time) 上建立索引可能更为合适。

SELECT * FROM devices WHERE device_id = M, time > 10

对于这个查询来说,示例中的索引是无用的,因为对于每个 store_id,device M 的数据都位于列表的一个完全不同的部分。

SELECT * FROM devices WHERE store_id = M, device_id = M, time > 10

对于这个索引来说,这是一个精确的查询。它将列表范围缩小到了一个非常特定的部分。

 

关于表空间

表空间用于确定数据库中表和索引的物理位置。在大多数情况下,您希望使用较快的存储来存储经常访问的数据,使用较慢的存储来存储不常访问的数据。

超表由许多块组成,每个块可以位于特定的表空间中。这允许您在多个磁盘上扩展超级表。当您创建新块时,会自动选择一个表空间来存储该块的数据。

您可以在超级表上附加和分离表空间。当磁盘空间不足时,您可以从超表中分离整个表空间,然后附加与新磁盘关联的表空间。要查看超表的表空间,请使用 show_tablespaces 命令。

 

超表块如何分配表空间

超表可以在多个维度上进行分区,但只有其中一个维度用于确定分配给特定超表块的表空间。如果超表具有一个或多个哈希分区维度或空间维度,则它使用第一个哈希分区维度。否则,它使用第一个时间维度。

此策略确保哈希分区的超表具有根据哈希分区共同定位的块,只要附加到超表的表空间列表保持不变。使用模计算来选择表空间,因此可以有比表空间更多的分区。例如,如果有两个表空间,则分区号 3 使用第一个表空间。

仅按时间分区的超表会连续添加新分区,因此以类似于循环的方式将块分配给表空间。

注意: 可以附加比超表分区更多的表空间。在这种情况下,某些表空间将保持未使用状态,直到其他表空间被分离或添加其他分区为止。对于哈希分区表尤其如此。

 

关于约束

约束是适用于数据库列的规则。这可以防止您将无效数据输入数据库。当您创建、更改或删除超级表上的约束时,约束将传播到底层块和任何索引。

超表支持所有标准 UnvDB 约束类型,但引用超表中的值的其他表上的外键约束除外。

例如,您可以创建一个仅允许正设备 ID 和非空温度读数的表。您还可以检查所有设备的时间值是否唯一。要创建带有约束的表,请使用以下命令:

CREATE TABLE conditions (
	time       TIMESTAMPTZ
	temp       FLOAT NOT NULL,
	device_id  INTEGER CHECK (device_id > 0),
	location   INTEGER REFERENCES locations (id),
	PRIMARY KEY(time, device_id)
);

SELECT create_hypertable('conditions', by_range('time'));

此示例还使用外键约束引用了另一个 locations 表中的值。

注意: 用于分区的时间列不允许有 NULL 值。如果这些列尚未设置,则默认会为其添加 NOT NULL 约束。

有关如何管理约束的更多信息,请参阅 UnvDB 文档。

 

更改和更新表架构

要修改现有超表的架构,可以使用 ALTER TABLE 命令。当您更改超表架构时,更改也会传播到每个底层块。

注意: 虽然您可以更改现有超表的架构,但无法更改连续聚合的架构。对于连续聚合,唯一允许的更改是重命名视图、设置架构、更改所有者和调整其他参数。

例如,要向名为 address 的表添加一个名为 distributors 的新列:

ALTER TABLE distributors
  ADD COLUMN address varchar(30);

这将创建新列,并记录NULL新列的所有现有条目。

在某些情况下,更改架构可能会消耗大量资源。如果需要重写基础数据,则尤其如此。如果您想在应用架构更改之前检查它,您可以使用 CHECK 约束,如下所示:

ALTER TABLE distributors
  ADD CONSTRAINT zipchk
  CHECK (char_length(zipcode) = 5);

这会扫描表以验证现有行是否满足约束,但不需要重写表。

有关更多信息,请参阅 Unvdb ALTER TABLE 文档。

 

索引数据

您可以在数据库上使用索引来加速读取操作。对于时间序列数据,只要包括 time 列,您可以在任意列组合上创建索引。TimeUDB 支持 UnvDB 中支持的所有表对象,包括数据类型、索引和触发器。

您可以使用 CREATE INDEX 命令来创建索引。例如,要创建一个首先按 location 排序,然后按 time 降序排序的索引,可以这样做:

CREATE INDEX ON conditions (location, time DESC);

您可以在将常规 UnvDB 表转换为超表之前或之后运行此命令。

 

默认索引

当您对数据库执行某些操作时,默认情况下会创建一些索引。

当您使用 create_hypertable 命令创建超表时,系统会在您的数据上创建时间索引。如果你想手动创建时间索引,可以使用以下命令:

CREATE INDEX ON conditions (time DESC);

当您使用 create_hypertable 命令创建超表时,如果除了时间之外还指定了一个可选的哈希分区,例如 location 列,那么会在该可选列和时间上创建一个额外的索引。例如:

CREATE INDEX ON conditions (location, time DESC);

有关声明索引时使用的顺序的更多信息,请参阅关于索引部分。

如果您不想创建这些默认索引,可以在运行 create_hypertable 命令时将 create_default_indexes 设置为 false。例如:

SELECT create_hypertable('conditions', by_range('time'))
  CREATE_DEFAULT_INDEXES false;

 

索引的最佳实践

如果您的数据是稀疏的,并且列中经常包含NULL值,您可以在索引中添加一个子句,如 WHERE column IS NOT NULL。这可以防止索引对 NULL 数据进行索引,从而创建一个更紧凑、更高效的索引。例如:

CREATE INDEX ON conditions (time DESC, humidity)
  WHERE humidity IS NOT NULL;

要将索引定义为 UNIQUE 或 PRIMARY KEY 索引,索引必须包括时间列和分区列(如果使用的话)。例如,唯一索引必须至少包括 (time, location) 列,以及您想要使用的任何其他列。通常,时间序列数据比关系数据更少使用 UNIQUE 索引。

如果您不想在单个事务中创建索引,可以使用 CREATE_INDEX 函数。这会使用单独的函数在每个块上创建索引,而不是为整个超表使用一个单独的事务。这意味着您可以在创建索引的同时对表执行其他操作,而不必等到索引创建完成。

注意: 您还可以使用 UnvDB WITH 子句对单个块执行索引事务。

   

触发器

TimeUDB 支持全系列 UnvDB 触发器。在超表上创建、更改或删除触发器会将更改传播到所有底层块。

 

创建触发器

此示例创建了一个名为 error_conditions 的新表,该表与 conditions 表具有相同的架构,但仅存储被视为错误的记录。在这种情况下,错误是指应用程序发送的 temperature 或 humidity 读数值大于或等于1000。

1、创建一个将错误数据插入 error_conditions 表中的函数:

CREATE OR REPLACE FUNCTION record_error()
  RETURNS trigger AS $record_error$
BEGIN
 IF NEW.temperature >= 1000 OR NEW.humidity >= 1000 THEN
   INSERT INTO error_conditions
	 VALUES(NEW.time, NEW.location, NEW.temperature, NEW.humidity);
 END IF;
 RETURN NEW;
END;
$record_error$ LANGUAGE plpgsql;

2、创建一个触发器,每当将新行插入到超表中时调用此函数:

CREATE TRIGGER record_error
  BEFORE INSERT ON conditions
  FOR EACH ROW
  EXECUTE PROCEDURE record_error();

3、所有数据都会插入到 conditions 表中,但包含错误的行也会添加到 error_conditions 表中。

 

JSONB 对半结构化数据的支持

您可以使用 JSON 和 JSONB 来提供半结构化数据。这对于包含用户定义字段的数据最有用,例如由各个用户定义且因用户而异的字段名称。我们建议以半结构化方式使用它,例如:

CREATE TABLE metrics (
  time TIMESTAMPTZ,
  user_id INT,
  device_id INT,
  data JSONB
);

当您使用JSON定义模式时,请确保将公共字段(如 time, user_id, 和 device_id )从JSONB结构中提取出来并作为列存储。这是因为对表列的字段访问比在JSONB结构内部更高效。存储也更加高效。

您还应该使用JSONB数据类型,即以二进制格式存储的JSON,而不是JSON数据类型。JSONB数据类型在存储开销和查找性能方面都更高效。

注意: 对于用户定义的数据,请使用JSONB,而不是稀疏数据。这对于大多数数据集效果最好。对于稀疏数据,请使用可为空的字段,如果可能的话,在像ZFS这样的压缩文件系统上运行。这将比JSONB数据类型效果更好,除非数据非常稀疏,例如,一行中超过95%的字段都是空的。

 

为JSONB结构建立索引

当您要为 JSONB 数据的所有字段建立索引时,通常最好使用 GIN 索引。在大多数情况下,您可以使用默认的 GIN 操作符,如下所示:

CREATE INDEX idxgin ON metrics USING GIN (data);

有关GIN索引的更多信息,请参阅UnvDB文档。

此索引仅优化 WHERE 子句中使用 ?、?&、?| 或 @> 运算符的查询。有关这些运算符的更多信息,请参阅 UnvDB 文档。

 

为单个字段建立索引

JSONB 列有时具有包含有用值的公共字段,这些值可以单独进行索引。这样的索引对于字段值的排序操作、多列索引以及特定类型的索引(如postGIS地理类型)非常有用。对单个字段值进行索引的另一个优点是,它们通常比对整个JSONB字段进行GIN索引要小。要创建这样的索引,通常最好使用访问该字段的表达式上的部分索引。例如:

CREATE INDEX idxcpu
  ON metrics(((data->>'cpu')::double precision))
  WHERE data ? 'cpu';

在这个例子中,被索引的表达式是 data JSONB 对象内部的 cpu 字段,被强制转换为双精度类型。这种转换通过存储更小的双精度值而不是字符串来减小索引的大小。WHERE 子句确保索引中只包含包含 cpu 字段的行,因为 data ? ‘cpu’ 返回 true。这也可以通过不包括没有 cpu 字段的行来减小索引的大小。请注意,为了使查询使用索引,它必须在WHERE子句中包含 data ? ‘cpu’。

这个表达式也可以与多列索引一起使用,例如,通过添加 time DESC 作为前导列。但是请注意,要启用仅索引扫描,您需要将 data 作为列,而不是完整的表达式 ((data->>’cpu’)::double precision)。

 

架构管理故障排除

本节包含一些解决架构管理中遇到的常见问题的想法。

 

重建超表索引以修复大型索引

ERROR:  invalid attribute number -6 for _hyper_2_839_chunk
CONTEXT:  SQL function "hypertable_local_size" statement 1 PL/pgSQL function hypertable_detailed_size(regclass) line 26 at RETURN QUERY SQL function "hypertable_size" statement 1
SQL state: XX000

如果您的超表索引变得非常大,您可能会看到此错误。要解决该问题,请使用以下命令重建超表索引:

reindex table _timeudb_internal._hyper_2_1523284_chunk

有关更多信息,请参阅超表文档。

   

压缩

时间序列数据可以被压缩以减少所需的存储量,并提高某些查询的速度。这是 TimeUDB 的一个核心特性。当新数据添加到您的数据库时,它是以未压缩行的形式存在的。TimeUDB 使用一个内置的作业调度器将这些数据转换为压缩列的形式。这发生在 TimeUDB 超表的各个块上。

 

关于压缩

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

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

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

Timestamp Device ID Device Type CPU
12:00:01 A SSD 70.11
12:00:01 B HDD 69.70
12:00:02 A SSD 70.12
12:00:02 B HDD 69.69
12:00:03 A SSD 70.14
12:00:03 B HDD 69.70

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

Timestamp Device ID Device Type 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] [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:

Column Type Collation Nullable Default
time timestamp with time zone not null
device_id integer not null
device_type integer not null
cpu double precision
disk_io double precision

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

  • 您可以使用列中的值作为查找键,在上面的示例中,device_id是这样一列的典型示例。

  • 您可以使用列来分区表。这通常是像上面示例中的时间列那样的时间列,但也可以使用其他类型来分区表。

  • 您可以使用某一列作为过滤器来缩小您选择的数据范围。例如,“device_type”列就是这样一个例子,您可以决定只查看固态硬盘(SSDs)的数据。其余的列通常是您正在收集的值或指标。这些值通常会以聚合或其他方式呈现。“cpu”和“disk_io”列就是此类列的典型示例。

 

SELECT avg(cpu), sum(disk_io)
FROM metrics
WHERE device_type = ‘SSD’
AND time >= now() - ‘1 day’::interval;

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

注意: 在超表上设置的索引仅用于包含未压缩数据的块。TimeUDB 在压缩期间创建并使用自定义索引来结合 segmentby 和 orderby 参数,这些参数在读取压缩数据时使用。下一节将详细介绍此内容。

基于之前的模式,数据过滤应该在一定的时间范围内进行,分析工作则以设备粒度完成。这种数据访问模式使得数据的组织布局适合于压缩。

 

排序和分段。

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

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

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

将产生以下数据布局。

Timestamp Device ID Device Type 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 列过滤数据更加有效。

unvdb=# 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
unvdb=# ALTER TABLE metrics
SET (
	timeudb.compress,
	timeudb.compress_segmentby = 'device_id',
	timeudb.compress_orderby='time'
);
ALTER TABLE
Time: 6,607 ms
unvdb=# SELECT compress_chunk(c) FROM show_chunks('metrics') c;
			 compress_chunk
----------------------------------------
 _timeudb_internal._hyper_2_4_chunk
 _timeudb_internal._hyper_2_5_chunk
 _timeudb_internal._hyper_2_6_chunk
(3 rows)
Time: 3070,626 ms (00:03,071)
unvdb=# 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 (
	timeudb.compress, 
	timeudb.compress_segmentby='device_id', 
	timeudb.compress_orderby='time'
);
time device_id device_type cpu disk_io energy_consumption
[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 用于基于该列的值将数据点分组在一起。这使得访问特定设备更加高效。

unvdb=# \timing
Timing is on.
unvdb=# 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
unvdb=# ALTER TABLE metrics 
SET (
	timeudb.compress, 
	timeudb.compress_segmentby = 'device_id', 
	timeudb.compress_orderby='time'
);
ALTER TABLE
Time: 6,607 ms
unvdb=# SELECT compress_chunk(c) FROM show_chunks('metrics') c;
			 compress_chunk             
----------------------------------------
 _timeudb_internal._hyper_2_4_chunk
 _timeudb_internal._hyper_2_5_chunk
 _timeudb_internal._hyper_2_6_chunk
(3 rows)
Time: 3070,626 ms (00:03,071)
unvdb=# 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。如果您的块不包含足够的数据来创建足够大的批次,则您的压缩率将会降低。定义压缩设置时需要考虑这一点。

 

压缩设计

时间序列数据可以是独特的,因为它需要处理浅查询和宽查询,例如“过去 10 分钟内部署中发生了什么”,也需要处理深查询和窄查询,例如“平均 CPU 使用率是多少”过去 24 小时内该服务器。”时间序列数据通常也有很高的插入率;对于时间序列数据集来说,每秒数十万次写入是非常正常的。此外,时间序列数据通常非常精细,并且以比许多其他数据集更高的分辨率收集数据。随着时间的推移,这可能会导致收集数 TB 的数据。

所有这些意味着,如果您需要很高的压缩率,您可能需要在开始提取数据之前考虑数据库的设计。本节介绍了在设计数据库以获得最大压缩效率时需要考虑的一些事项。

 

压缩数据

TimeUDB 构建于 UnvDB 之上,后者本质上是一个基于行的数据库。由于时间序列数据是按时间顺序访问的,因此当您启用压缩时,TimeUDB 会将许多宽行数据转换为单行数据,称为数组形式。这意味着该新的宽行的每个字段都存储包含整个列的有序数据集。

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

Timestamp Device ID Status Code Temperature
12:00:01 A 0 70.11
12:00:01 B 0 69.70
12:00:02 A 0 70.12
12:00:02 B 0 69.69
12:00:03 A 0 70.14
12:00:03 B 4 69.70

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

Timestamp Device ID Status Code Temperature
[12:00:01, 12:00:01, 12:00:02, 12:00:02, 12:00:03, 12:00:03] [A, B, A, B, A, B] [0, 0, 0, 0, 0, 4] [70.11, 69.70, 70.12, 69.69, 70.14, 69.70]

即使在您压缩任何数据之前,这种格式也可以通过减少每行的开销来立即节省存储空间。UnvDB通常为每行增加少量的字节开销。因此,即使没有任何压缩,此示例中的模式现在在磁盘上也比以前的格式更小。

这种格式会重新排列数据,以便将类似的数据(如时间戳、设备ID或温度读数)连续存储。这意味着您可以使用针对特定类型的压缩算法进一步压缩数据,并且每个数组都是单独压缩的。有关所使用的压缩方法的更多信息,请参阅压缩方法部分。

当数据以数组格式存储时,您可以非常快速地执行需要部分列的查询。例如,如果您有这样的查询,询问过去一天的平均温度:

SELECT time_bucket(‘1 minute’, timestamp) as minute
 AVG(temperature)
FROM table
WHERE timestamp > now() - interval ‘1 day’
ORDER BY minute DESC
GROUP BY minute;

查询引擎可以仅获取和解压缩时间戳和温度列,以高效地计算和返回这些结果。

最后,TimeUDB使用非内联磁盘页面来存储压缩数组。这意味着行内数据指向一个存储压缩数组的次要磁盘页面,而主表中的实际行变得非常小,因为它现在只是数据的指针。当查询以这种方式存储的数据时,仅从磁盘读取所需列的压缩数组,从而通过减少磁盘读写操作进一步提高性能。

 

查询压缩数据

在前面的示例中,数据库无法知道需要提取和解压缩哪些行来解决查询。例如,数据库无法轻松确定哪些行包含过去一天的数据,因为时间戳本身位于压缩列中。您不希望必须解压缩块中的所有数据,甚至解压缩整个超表中的数据来确定需要哪些行。

TimeUDB 自动在行中包含更多信息,并包含其他分组以提高查询性能。当您手动或通过压缩策略压缩超表时,它可以帮助指定 ORDER BY列。

ORDER BY 列指定压缩批次中的行的排序方式。对于大多数时间序列工作负载,这是按时间戳计算的,因此如果您不指定列 ORDER BY,TimeUDB 默认使用时间列。您还可以指定其他维度,例如位置。

对于每个 ORDER BY 列,TimeUDB 会自动创建额外的列来存储该列的最小值和最大值。这样,查询规划器可以查看压缩列中的时间戳范围,而无需进行任何解压缩,并确定该行是否可能与查询匹配。

当您压缩超表时,您还可以选择指定 SEGMENT BY 列。这允许您按特定列对压缩行进行分段,以便每个压缩行对应于有关单个项目的数据,例如特定的设备 ID。这进一步允许查询规划器确定该行是否可能与查询匹配,而不必先解压缩该列。例如:

Device ID Timestamp Status Code Temperature Min Timestamp Max Timestamp
A [12:00:01, 12:00:02, 12:00:03] [0, 0, 0] [70.11, 70.12, 70.14] 12:00:01 12:00:03
B [12:00:01, 12:00:02, 12:00:03] [0, 0, 0] [70.11, 70.12, 70.14] 12:00:01 12:00:03

通过这种方式对数据进行分段,在一个时间间隔内对设备A的查询变得相当快。查询规划器可以使用索引来查找设备 A 中至少包含一些与指定间隔相对应的时间戳的行,甚至顺序扫描也相当快,因为​​评估设备 ID 或时间戳不需要解压缩。这意味着查询执行器仅解压缩与这些选定行对应的时间戳和温度列。

 

关于压缩方法

TimeUDB 使用不同的压缩算法,具体取决于要压缩的数据类型。

对于整数、时间戳和其他类似整数的类型,使用压缩方法的组合:delta 编码、 delta-of-delta、simple-8b和 run-length 编码。

对于没有大量重复值的列, 使用 基于 XOR 的压缩和一些字典压缩。

对于所有其他类型,使用字典压缩。

 U

整数压缩

对于整数、时间戳和其他类似整数的类型,TimeUDB 使用 delta 编码、delta-of-delta、simple-8b 和 run-length 编码的组合。

simple-8b 压缩方法已经被扩展,以便数据可以按相反的顺序进行解压缩。向后扫描查询在时间序列工作负载中很常见。这意味着这类查询运行得更快。

 

Delta 编码

Delta 编码通过仅存储数据对象与一个或多个参考对象之间的差异来减少表示数据对象所需的信息量。这些算法在存在大量冗余信息的情况下效果最佳,并经常用于如版本化文件系统等工作负载中。例如,Dropbox 就是通过这种方式使您的文件保持同步。对时间序列数据应用增量编码意味着您可以用更少的字节来表示一个数据点,因为您只需要存储与上一个数据点的增量。

例如,想象一下您有一个数据集,随着时间的推移收集 CPU 使用率、可用内存、温度和湿度等信息。如果您的时间列存储为整数值(如自 UNIX 纪元以来的秒数),那么您的原始数据可能会像这样:

time cpu mem_free_bytes temperature humidity
2023-04-01 10:00:00 82 1,073,741,824 80 25
2023-04-01 10:05:00 98 858,993,459 81 25
2023-04-01 10:05:00 98 858,904,583 81 25

使用 Delta 编码,您只需存储每个值相对于前一个数据点的变化量,从而可以存储更小的值。因此,在第一行之后,您可以用较少的信息表示后续行,如下所示:

time cpu mem_free_bytes temperature humidity
2020-04-01 10:00:00 82 1,073,741,824 80 25
5 second 16 -214,748,365 1 0
5 seconds 0 -88,876 0 0

将 Delta 编码应用于时间序列数据利用了这样一个事实:大多数时间序列数据集不是随机的,而是代表随时间缓慢变化的数据。数百万行的存储节省可能是巨大的,特别是当值变化很小或根本不变化时。

 

Delta-of-Delta 编码

Delta-of-Delta 编码是将 Delta 编码更进一步,对之前已进行 Delta 编码的数据应用 Delta 编码。对于定期进行数据收集的时间序列数据集,您可以将 delta-of-delta 编码应用于时间列,这样只需要存储一系列零。

换句话说,增量编码存储数据集的一阶导数,而增量增量编码存储数据集的二阶导数。

应用于之前的示例数据集时,delta-of-delta 编码结果如下:

time cpu mem_free_bytes temperature humidity
2020-04-01 10:00:00 82 1,073,741,824 80 25
5 second 16 -214,748,365 1 0
0 0 -88,876 0 0

在此示例中,对于第二行之后的时间列中的每个条目,delta-of-delta 进一步将时间列中的 5 秒压缩为 0,因为每个条目的 5 秒间隙保持不变。请注意,您会在表中看到 delta-delta 0 值之前的两个条目,因为您需要两个增量进行比较。

这会将 8 字节(即 64 位)的完整时间戳压缩为一位,从而实现 64 倍压缩。

 

Simple-8b

通过 delta 和 delta-of-delta 编码,您可以显着减少需要存储的位数。但您仍然需要一种有效的方法来存储较小的整数。前面的示例使用标准整数数据类型作为时间列,在 delta-delta 编码时需要 64 位来表示 0 值。这意味着即使您只存储整数 0,您仍然消耗 64 位来存储它,因此您实际上没有保存任何内容。

Simple-8b 是存储可变长度整数的最简单、最小的方法之一。在此方法中,整数存储为一系列固定大小的块。对于每个块,块内的每个整数都由表示该块中最大整数所需的最小位长度表示。每个块的第一位表示该块的最小位长度。

该技术的优点是只需要为给定块存储一次长度,而不是为每个整数存储一次。由于块的大小是固定的,因此您可以根据存储的整数的大小推断出每个块中整数的数量。

例如,如果您想存储随时间变化的温度,并且应用了增量编码,则最终可能需要存储这组整数:

temperature (deltas)
1
10
11
13
9
100
22
11

如果块大小为 10 位,您可以将这组整数存储为两个块:一个块存储 5 个 2 位数字,第二个块存储 3 个 3 位数字,如下所示:

{2: [01, 10, 11, 13, 09]} {3: [100, 022, 011]}

在此示例中,两个块都存储大约 10 位数字的数据,尽管某些数字必须用前导 0 填充。您可能还会注意到第二个块仅存储 9 位数字,因为 10 不能被 3 整除。

Simple-8b 以这种方式工作,只不过它使用二进制数而不是十进制,并且通常使用 64 位块。一般来说,整数越长,每个块中可以存储的整数数量就越少。

 

Run-length 编码

Simple-8b 可以很好地压缩整数,但是,如果相同值有大量重复,则可以通过游程编码获得更好的压缩。此方法适用于不经常更改的值,或者较早的转换删除了更改的情况。

游程编码是经典的压缩算法之一。对于具有数十亿个连续零的时间序列数据,甚至是具有一百万个相同重复字符串的文档,游程编码的效果非常好。

例如,如果您想存储随时间变化最小的温度,并且应用了增量编码,则最终可能需要存储这组整数:

temperature (deltas)
11
12
12
12
12
12
12
1
12
12
12
12

对于此类值,您不需要存储该值的每个实例,而是需要存储运行时间或重复次数。您可以将这组数字存储为{run; value}对,如下所示:

{1; 11}, {6; 12}, {1; 1}, {4; 12}

该技术使用 11 位存储(1, 1, 1, 6, 1, 2, 1, 1, 4, 1, 2),而不是最佳的可变长度整数系列所需的 23 位(11, 12, 12, 12, 12, 12, 12, 1, 12, 12, 12, 12)。

Run-length 编码还用作许多更高级算法的构建块,例如 Simple-8b RLE,它是一种结合了 Run-length 和 Simple-8b 技术的算法。 TimeUDB 实现了 Simple-8b RLE 的变体。该变体使用与标准 Simple-8b 不同的大小,以便处理 64 位值和 RLE。

 

浮点压缩

对于没有大量重复值的列,TimeUDB 使用基于 XOR 的压缩。

标准的基于 XOR 的压缩方法已得到扩展,以便可以按相反的顺序解压缩数据。向后扫描查询在时间序列工作负载中很常见。这意味着使用向后扫描的查询运行速度要快得多。

 

基于XOR的压缩

浮点数通常比整数更难压缩。固定长度整数通常有前导零,但浮点数通常使用所有可用位,特别是当它们是从十进制数转换而来时,十进制数无法用二进制精确表示。

像增量编码这样的技术对于浮点数来说效果不佳,因为它们不能充分减少位数。这意味着大多数浮点压缩算法往往要么复杂且缓慢,要么截断有效数字。基于 XOR 的压缩是少数简单且快速的无损浮点压缩算法之一,它建立在 Facebook 的 Gorilla 压缩之上。

XOR 是二元函数 exclusive or。在此算法中,连续的浮点数与 XOR 进行比较,差异结果会存储一个位。第一个数据点在不压缩的情况下存储,后续数据点使用其异或值表示。

 

未知数据的压缩

对于非整数或浮点值,TiemUDB 使用字典压缩。

 

字典压缩

字典压缩是最早的无损压缩算法之一,是许多流行压缩方法的基础。字典压缩也可以在计算机科学之外的领域找到,例如医学编码。

字典压缩不是直接存储值,而是通过列出可能出现的值的列表,然后将索引存储到包含唯一值的字典中。这种技术非常通用,无论数据类型如何都可以使用,并且当您拥有一组有限且经常重复的值时,效果特别好。

例如,如果您有前面显示的温度列表,但您想要一个额外的列来存储每个测量的城市位置,则您可能有一组如下所示的值:

City
New York
San Francisco
San Francisco
Los Angeles

您可以存储字典,而不是直接存储所有城市名称,如下所示:

{0: "New York", 1: "San Francisco", 2: "Los Angeles",}

然后,您可以仅将索引存储在列中,如下所示:

City
0
1
1
2

对于具有大量重复的数据集,这可以提供显着的压缩。在示例中,每个城市名称的平均长度为 11 个字节,而索引的长度永远不会超过 4 个字节,从而减少了近 3 倍的空间使用量。在 TimeUDB 中,通过Simple-8b+RLE 方法进一步压缩索引列表,使得存储成本更加小。

字典压缩并不总是能带来节省。如果您的数据集没有很多重复值,则字典的大小与原始数据相同。 TimeUDB 会自动检测这种情况,并在这种情况下回退到不使用字典。

 

压缩策略

您可以通过声明要作为分段依据的列来对各个超表启用压缩。

 

启用压缩策略

此过程使用一个名为 example 的示例表,并按 device_id 列对其进行分段。然后,每个超过 7 天的块都会被标记为自动压缩。源数据的组织方式如下:

time device_id cpu disk_io energy_consumption
8/22/2019 0:00 1 88.2 20 0.8
8/22/2019 0:05 2 300.5 30 0.9

1、在 ud_sql提示符下,更改表:

ALTER TABLE example SET (
  timeudb.compress,
  timeudb.compress_segmentby = 'device_id'
);

2、添加压缩策略来压缩超过 7 天的块:

SELECT add_compression_policy('example', INTERVAL '7 days');

 

查看当前的压缩策略

要查看您设置的压缩策略:

SELECT * FROM timeudb_information.jobs
  WHERE proc_name='policy_compression';

 

删除压缩策略

要删除压缩策略,请使用 remove_compression_policy .例如,要删除名为 cpu 的超表的压缩策略:

SELECT remove_compression_policy('cpu');

 

禁用压缩

您可以在各个超表上完全禁用压缩。仅当您当前没有任何压缩块时,此命令才有效:

ALTER TABLE <TABLE_NAME> SET (timeudb.compress=false);

如果您的超表包含压缩块,则需要先单独解压缩每个块,然后才能关闭压缩。

 

手动压缩

在大多数情况下,自动压缩策略足以手动压缩您的数据块。但是,如果您想对压缩进行更多控制,也可以手动压缩特定的数据块。

警告: 压缩会更改您磁盘上的数据,因此在开始之前请务必进行备份。

在开始之前,您需要一份要压缩的数据块列表。在此示例中,您使用名为 example 的超表,并压缩超过三天的数据块。

1、在 ud_sql 提示符处,选择表中 example 超过三天的所有块:

SELECT show_chunks('example', older_than => INTERVAL '3 days');

2、这将返回一个块列表。记下块的名称:

||show_chunks|
|---|---|
|1|_timeudb_internal_hyper_1_2_chunk|
|2|_timeudb_internal_hyper_1_3_chunk|

当您对要压缩的数据块列表满意时,可以使用这些数据块的名称来手动压缩每一个。

1、在 ud_sql 提示符下,压缩块:

SELECT compress_chunk( '<chunk_name>');

2、使用以下命令检查压缩结果:

SELECT * FROM chunk_compression_stats('example');

结果显示给定超表的块、它们的压缩状态以及一些其他统计信息:

|chunk_schema|chunk_name|compression_status|before_compression_table_bytes|before_compression_index_bytes|before_compression_toast_bytes|before_compression_total_bytes|after_compression_table_bytes|after_compression_index_bytes|after_compression_toast_bytes|after_compression_total_bytes|node_name|
|---|---|---|---|---|---|---|---|---|---|---|---|
|_timeudb_internal|_hyper_1_1_chunk|Compressed|8192 bytes|16 kB|8192 bytes|32 kB|8192 bytes|16 kB|8192 bytes|32 kB||
|_timeudb_internal|_hyper_1_20_chunk|Uncompressed||||||||||

3、对要压缩的所有块重复此操作。

 

使用单个命令手动压缩数据块

或者,您可以选择数据块并使用 show_chunks 命令的输出结果,在单个命令中将它们全部压缩。例如,使用以下命令来压缩一到三周前且尚未压缩的数据块:

SELECT compress_chunk(i, if_not_compressed => true)
	FROM show_chunks(
		'example',
		now()::timestamp - INTERVAL '1 week',
		now()::timestamp - INTERVAL '3 weeks'
	) i;

 

压缩时合并未压缩的数据块

在 TimeUDB 中,您可以将多个未压缩的数据块合并到先前已压缩的数据块中,作为压缩过程的一部分。这允许您设置更小的未压缩数据块间隔,从而减少未压缩数据所使用的磁盘空间。例如,如果您的数据中有多个较小的未压缩数据块,您可以将它们合并到一个已压缩的数据块中。

要将未压缩的数据块合并到已压缩的数据块中,请更改压缩设置以设置压缩数据块的时间间隔,并运行压缩操作以在压缩时合并数据块。

ALTER TABLE example SET (timeudb.compress_chunk_time_interval = '<time_interval>',
                     timeudb.compress_orderby = 'time ASC');
SELECT compress_chunk(c, if_not_compressed => true)
	FROM show_chunks(
		'example',
		now()::timestamp - INTERVAL '1 week'
	) c;

注意: compress_orderby 的默认设置是 ‘time DESC’,这会导致在合并过程中多次重新压缩数据块,可能会导致严重的性能损失。为避免这种损失,请设置timeudb.compress_orderby = ‘time ASC’。

您选择的时间间隔必须是未压缩块间隔的倍数。例如,如果您的未压缩块间隔是一周,那么 < time_interval >压缩块的间隔可能是两周或六周,但不是一个月。

 

插入和修改压缩数据

在 TimeUDB 中,您可以将数据插入压缩块中,并修改压缩行中的数据。

 

将数据插入压缩块中

在 TimeUDB 中,您可以将数据插入压缩块中。即使您要插入的数据具有唯一约束,并且这些约束在插入操作期间会保留,该方法仍然有效。这是通过使用 UnvDB 函数来完成的,该函数在插入期间解压缩相关数据以检查新数据是否破坏唯一检查。这意味着每当您将数据插入压缩块时,都会解压缩少量数据以允许推测插入,并阻止任何可能违反约束的插入。

 

修改压缩行中的数据

在 TimeUDB 中,您还可以使用 UPDATE 和 DELETE 命令来修改压缩块中的现有行。这与插入操作的工作方式类似,其中少量数据被解压缩以便能够运行修改。系统尝试仅解压缩必要的数据,以减少所完成的解压缩量,但在某些情况下,修改命令最终可能会解压缩大量数据。如果没有限定符,或者限定符无法用于过滤,则通常会发生这种情况。您可以尝试通过使用 segmentby 和 orderby 列来尝试避免这种情况,这允许在解压缩和修改操作之前过滤掉尽可能多的数据。

 

解压缩块

TimeUDB 自动支持向已压缩的数据块中插入数据。但是,如果您需要插入大量数据,例如作为批量回填操作的一部分,则应首先解压缩数据块。向已压缩的数据块中插入数据比向未压缩的数据块中插入数据计算成本更高。这在很多行上都会累加。

重要: 压缩数据时,可以减少 TimeUDB 实例的存储空间。但是,您应该始终保留一些额外的存储容量。这为您提供了在必要时解压缩数据块的灵活性,以便执行诸如批量插入等操作。

本节描述了用于解压缩数据块的命令。您可以通过时间过滤来选择要解压缩的数据块。要了解如何回填数据,请参阅回填部分。

 

手动解压缩数据块

有多种方法可以选择数据块并对其进行解压缩。

注意: 在解压缩数据块之前,请停止正在解压缩的超表上的任何压缩策略。完成回填或更新数据后,请重新启用该策略。数据库会在下一个计划的任务中自动重新压缩您的数据块。

 

解压缩各个块

要按名称解压缩单个块,请运行以下命令:

SELECT decompress_chunk('_timeudb_internal.<chunk_name>');

其中,< chunk_name >是要解压缩的块的名称。

 

按时间解压缩块

要基于时间范围解压缩一组数据块,您可以使用 show_chunks 的输出结果来逐个解压缩它们:

SELECT decompress_chunk(c, true)
	FROM show_chunks('table_name', older_than, newer_than) c;

有关 decompress_chunk 函数的更多信息,请参阅decompress_chunk API 参考。

 

基于更精确的约束解压缩数据块

如果您想使用更精确的匹配约束,例如空间分区,您可以构建类似这样的命令:

SELECT tableoid::regclass FROM metrics
  WHERE time = '2000-01-01' AND device_id = 1
  GROUP BY tableoid;

				 tableoid
------------------------------------------
 _timeudb_internal._hyper_72_37_chunk

 

回填压缩块上的历史数据

回填数据时,您是将数据插入到已压缩的块中。进行批量回填时,建议暂停压缩作业直至完成,这样策略就不会压缩您正在处理的块。

本节包含批量回填的程序,引导您完成以下步骤:

  1. 暂时关闭任何现有的压缩策略。这会阻止策略尝试压缩您当前正在处理的块。

  2. 执行插入或回填。

  3. 重新启用压缩策略。这会重新压缩您处理的块。

注意事项:

为了回填数据并强制执行唯一约束,我们最终可能会解压缩一些数据。如果我们要回填大量数据,则手动解压缩您正在处理的块可能会更高效(如下面的手动回填部分所示)。

 

使用提供的函数回填

为了使回填更容易,您可以使用 TimeUDB 中的回填功能。特别是, decompress_backfill 过程会为您自动执行许多回填步骤。

注意: 本节将向您展示如何使用临时表批量回填数据。临时表仅在数据库会话期间存在,然后会自动删除。如果您定期回填,可能会更喜欢使用常规表,以便多个写入者可以同时插入表中。在这种情况下,完成数据回填后,请通过截断表来清理,以为下一次回填做准备

1、在 ud_sql 提示符处,创建一个与要回填到的超表具有相同架构的临时表。在此示例中,表名为 example,临时表名为cpu_temp:

CREATE TEMPORARY TABLE cpu_temp AS SELECT * FROM example WITH NO DATA;

2、将您的数据插入临时表中。

3、调用 decompress_backfill 程序。此过程停止压缩策略,识别回填数据对应的压缩块,解压缩块,将回填表中的数据插入到主超表中,然后重新启用压缩策略:

CALL decompress_backfill(
	staging_table=>'cpu_temp', destination_hypertable=>'example'
);

 

手动回填

如果您不想使用提供的功能,可以手动执行这些步骤。

1、在 ud_sql 提示符下,找到策略的 job_id:

SELECT j.job_id
	FROM timeudb_information.jobs j
	WHERE j.proc_name = 'policy_compression'
		AND j.hypertable_name = <target table>;

2、暂停压缩,以防止策略尝试压缩您当前正在处理的块:

SELECT alter_job(<job_id>, scheduled => false);

3、解压缩要修改的块。

SELECT decompress_chunk('_timeudb_internal._hyper_2_2_chunk');

对每个数据块重复此操作。或者,您可以使用 show_chunks 基于时间范围来解压缩一组数据块:

SELECT decompress_chunk(i)
	FROM show_chunks('conditions', newer_than, older_than) i;

4、当您解压完所有要修改的块后,执行 INSERT或UPDATE命令回填数据。

5、重新启动压缩策略作业。下次作业运行时,它会重新压缩所有已解压的块。

SELECT alter_job(<job_id>, scheduled => true);

或者,要立即重新压缩块,请使用以下run_job命令:

CALL run_job(<job_id>);

 

修改架构

您可以在 TimeUDB 中修改压缩超表的架构。

Schema modification
Add a nullable column
Add a column with a default value and a NOT NULL constraint
Rename a column
Drop a column
Change the data type of a colum

 

添加可为空的列

添加可为空的列:

ALTER TABLE <hypertable> ADD COLUMN <column_name> <datatype>;

例如:

ALTER TABLE conditions ADD COLUMN device_id integer;

 

添加具有默认值和 NOT NULL 约束的列

要添加具有默认值和非空约束的列:

ALTER TABLE <hypertable> ADD COLUMN <column_name> <datatype>
	NOT NULL DEFAULT <default_value>;

例如:

ALTER TABLE conditions ADD COLUMN device_id integer
	NOT NULL DEFAULT 1;

 

重命名列

要重命名列:

ALTER TABLE <hypertable> RENAME <column_name> TO <new_name>;

例如:

ALTER TABLE conditions RENAME device_id TO devid;

 

删除一列

如果列不是 orderby 或 segmentby 列,您可以从压缩的超表中删除该列。删除列:

ALTER TABLE <hypertable> DROP COLUMN <column_name>;

例如:

ALTER TABLE conditions DROP COLUMN temperature;

 

压缩故障排除

本节包含一些解决压缩过程中遇到的常见问题的想法。

 

缩块时超出临时文件大小限制

ERROR: temporary file size exceeds temp_file_limit

当您尝试压缩块时,尤其是块非常大时,您可能会收到此错误。压缩操作将文件写入新的压缩块表,该表写入临时内存中。可用临时内存的最大量由该参数确定temp_file_limit。您可以通过调整temp_file_limit 和 maintenance_work_mem 参数来解决此问题。

 

无法将列添加到压缩的超表

ERROR: cannot add column with constraints or defaults to a hypertable that has compression enabled

如果您尝试将带有约束或默认值的列添加到启用了压缩的超表中,则可能会收到此错误。添加列需要先解压超表中的数据,添加列,然后压缩数据。

 

用户权限不允许压缩或解压缩块

ERROR:  must be owner of hypertable "HYPERTABLE_NAME"

如果您尝试使用非特权用户帐户压缩或解压缩块,则可能会收到此错误。要压缩或解压缩块,您的用户帐户必须具有允许其 CREATE INDEX 对块执行的权限。您可以在命令提示符下使用以下命令检查当前用户的权限ud_sql:

\dn+ <USERNAME>

要解决此问题,请使用以下命令授予您的用户帐户适当的权限:

GRANT PRIVILEGES
	ON TABLE <TABLE_NAME>
	TO <ROLE_TYPE>;

有关 GRANT 命令的更多信息,请参阅 UnvDB 文档。

 

操作超出元组解压限制

ERROR: tuple decompression limit exceeded by operation

当从压缩块中插入、更新或删除元组时,可能需要解压缩元组。当您更新现有元组或有需要在插入时验证的约束时,就会发生这种情况。如果您碰巧使用单个命令触发大量解压,则最终可能会耗尽存储空间。因此,对单个命令可以解压缩的元组数量进行了限制。

可以增加或关闭该限制(设置为 0),如下所示:

-- set limit to a milion tuples
SET timeudb.max_tuples_decompressed_per_dml_transaction TO 1000000;
-- disable limit by setting to 0
SET timeudb.max_tuples_decompressed_per_dml_transaction TO 0;

 

计划作业停止运行

您的计划作业可能会因各种原因停止运行。在自托管的 TimeUDB 上,您可以通过重新启动后台工作程序来解决此问题:

SELECT _timeudb_functions.start_background_workers();

在 TimeUDB 的托管服务上,通过执行以下操作之一重新启动后台工作程序:

  • 运行 SELECT timeudb_pre_restore(),跟着执行 SELECT timeudb_post_restore()。

  • 关闭并重新打开服务。这可能会导致服务从备份恢复并重播预写日志时出现几分钟的停机时间。

 

重建超表索引以修复大型索引

ERROR:  invalid attribute number -6 for _hyper_2_839_chunk
CONTEXT:  SQL function "hypertable_local_size" statement 1 PL/pgSQL function hypertable_detailed_size(regclass) line 26 at RETURN QUERY SQL function "hypertable_size" statement 1
SQL state: XX000

如果您的超表索引变得非常大,您可能会看到此错误。要解决该问题,请使用以下命令重建超表索引:

reindex table _timeudb_internal._hyper_2_1523284_chunk

有关更多信息,请参阅超表文档。

   

连续聚合

连续聚合旨在加快对非常大型数据集的查询速度。TimeUDB 的连续聚合使用 UnvDB 的物化视图在后台持续且增量地刷新查询,因此当您运行查询时,只需要计算已更改的数据,而不需要计算整个数据集。

 

关于连续聚合

时间序列数据通常增长得非常快。这意味着将数据聚合为有用的摘要可能会变得非常慢。连续聚合使数据聚合变得非常迅速。

如果您非常频繁地收集数据,您可能希望将数据聚合为分钟或小时。例如,如果您有一个每秒记录的温度读数表,您可以找到每小时的平均温度。每次运行此查询时,数据库都需要扫描整个表并每次重新计算平均值。

连续聚合是一种超表,当添加新数据或修改旧数据时,它会在后台自动刷新。对数据集的更改会被跟踪,连续聚合背后的超表会在后台自动更新。

您不需要手动刷新连续聚合,它们会在后台持续且增量地更新。与常规的 UnvDB 物化视图相比,连续聚合的维护负担也要低得多,因为整个视图不会在每次刷新时都从头开始创建。这意味着您可以继续处理数据,而不是维护数据库。

由于连续聚合基于超表,因此您可以以与其他表完全相同的方式查询它们,并在连续聚合上启用压缩或分层存储。您甚至可以在连续聚合之上创建连续聚合。

默认情况下,查询连续聚合会为您提供实时数据。物化视图中的预聚合数据与尚未聚合的最新数据相结合。这为您的每个查询提供了最新的结果。

 

聚合类型

有三种主要方法可以使聚合变得更容易:物化视图、连续聚合和实时聚合。

物化视图是 UnvDB 的标准功能。它们用于缓存复杂查询的结果,以便您稍后可以重复使用它。尽管您可以根据需要手动刷新物化视图,但它们不会定期更新。

连续聚合是 TimeUDB 独有的功能。它们的工作方式与物化视图相似,但是当新数据添加到数据库中时,它们会在后台自动更新。连续聚合是持续且增量地更新的,这意味着与物化视图相比,它们的维护对资源的消耗更少。连续聚合基于超表,您可以用与其他表相同的方式查询它们。

实时聚合也是 TimeUDB 独有的功能。它们与连续聚合相同,但它们将最新的原始数据添加到先前聚合的数据中,以提供准确且最新的结果,而无需在写入数据时对数据进行聚合。

 

连续聚合之上的连续聚合

您可以在一个连续聚合之上创建另一个连续聚合。这允许您以不同的粒度汇总数据。例如,您可能有一个包含每秒数据的原始超表。在超表上创建一个连续聚合来计算每小时的数据。为了计算每日数据,请在每小时的连续聚合之上再创建一个连续聚合。

有关更多信息,请参阅关于连续聚合之上的连续聚合的文档。

 

带有JOIN子句的连续聚合

在 TimeUDB 中,连续聚合支持JOIN操作,只要它们满足以下条件:

  • 连接必须在一个超表和一个标准的 UnvDB 表之间进行。JOIN子句中的表顺序不重要。

  • 只跟踪超表的更改,并在刷新连续聚合时更新这些更改。对标准 UnvDB 表的更改不会被跟踪。

  • 必须使用 INNER JOIN,不支持其他连接类型。

  • JOIN条件必须是等式条件,且只能有一个JOIN条件。只要JOIN条件在ON/USING子句中给出,就可以在WHERE子句中添加更多条件。

  • 应使用ON或USING子句来指定JOIN条件,因为如果在WHERE子句中指定JOIN条件,则不允许有进一步的条件。

  • 不支持在连续聚合的物化超表上进行连接。

  • 可以在带有JOIN子句的连续聚合之上创建分层连续聚合,但它们本身不能有JOIN子句。

本节包括一些与连续聚合一起使用的JOIN条件示例。为了使这些示例有效,table_1或table_2必须是超表。哪个是超表,哪个是标准的 UnvDB 表并不重要。

使用ON子句在单个等式条件上进行INNER JOIN:

CREATE MATERIALIZED VIEW my_view WITH (timeudb.continuous) AS
SELECT ...
FROM table_1 t1
JOIN table_2 t2 ON t1.t2_id = t2.id
GROUP BY ...

使用ON子句在单个等式条件上进行INNER JOIN,并在WHERE子句中添加了一个额外条件:

CREATE MATERIALIZED VIEW my_view WITH (timeudb.continuous) AS
SELECT ...
FROM table_1 t1
JOIN table_2 t2 ON t1.t2_id = t2.id
WHERE t1.id IN (1, 2, 3, 4)
GROUP BY ...

在WHERE子句中指定单个等式条件进行INNER JOIN,这是允许的,但不推荐:

CREATE MATERIALIZED VIEW my_view WITH (timeudb.continuous) AS
SELECT ...
FROM table_1 t1, table_2 t2
WHERE t1.t2_id = t2.id
GROUP BY ...

这些是JOIN条件无法与连续聚合一起使用的示例:不允许在多个等式条件上进行INNER JOIN。

CREATE MATERIALIZED VIEW my_view WITH (timeudb.continuous) AS
SELECT ...
FROM table_1 t1
JOIN table_2 t2 ON t1.t2_id = t2.id AND t1.t2_id_2 = t2.id
GROUP BY ...

在WHERE子句中指定的具有单个等式条件的JOIN不能与WHERE子句中的其他条件组合使用。

CREATE MATERIALIZED VIEW my_view WITH (timeudb.continuous) AS
SELECT ...
FROM table_1 t1, table_2 t2
WHERE t1.t2_id = t2.id
AND t1.id IN (1, 2, 3, 4)
GROUP BY ...

 

功能支持

在 TimeUDB 中,连续聚合支持所有 UnvDB 聚合函数。这包括可并行聚合,例如SUM 和AVG,以及不可并行聚合,例如RANK.

下表总结了连续聚合中的聚合函数支持:

Function, clause, or feature
Parallelizable aggregate functions
Non-parallelizable aggregate functions
ORDER BY
Ordered-set aggregates
Hypothetical-set aggregates
DISTINCT in aggregate functions
FILTER in aggregate functions
FROM clause supports JOINS

 

连续聚合的组件

连续聚合包括以下部分:

  • 物化超表:用于存储聚合数据

  • 物化引擎:用于将数据从原始底层表聚合到物化超表

  • 失效引擎:用于确定何时需要重新物化数据,这是由于数据发生了变化

  • 查询引擎:用于访问聚合数据

 

物化超表

连续聚合从原始超表中获取原始数据,对其进行聚合,并将中间状态存储在物化超表中。当您查询连续聚合视图时,会按需返回状态。

使用相同的温度示例,物化表如下所示:

day location chunk avg temperature partial
2021/01/01 New York 1 {3, 219}
2021/01/01 Stockholm 1 {4, 280}
2021/01/02 New York 2
2021/01/02 Stockholm 2 {5, 345}

物化表存储为 TimeUDB 超表,以利用超表提供的扩展和查询优化。物化表包含查询中每个GROUP BY子句的列、一个标识此条目来自原始数据中哪个分块的分块列,以及查询中每个聚合的部分聚合列。

部分列在内部用于计算输出。在此示例中,由于查询寻找平均值,因此部分列包含所见到的行数以及它们所有值的总和。关于部分聚合,最重要的一点是,它们可以组合起来创建跨越所有旧部分聚合行的新部分聚合。如果您组合跨越多个分块的组,这一点很重要。

有关更多信息,请参阅物化超表。

 

物化引擎

物化引擎执行两个事务。第一个事务会阻塞所有的 INSERT、UPDATE和DELETE 操作,确定要物化的时间范围,并更新失效阈值。第二个事务会解除对其他事务的阻塞,并进行聚合的物化。第一个事务非常快,大部分工作都在第二个事务中进行,以确保这些工作不会干扰其他操作。

当您查询连续聚合视图时,物化引擎会将聚合的部分值组合成每个时间范围的单个部分值,并计算返回的值。例如,为了计算平均值,每个部分和会累加到总和,每个部分计数会累加到总计数,然后计算平均值,即总和除以总计数。

 

失效引擎

超表中的数据发生任何更改都可能使某些物化的行失效。失效引擎会检查以确保系统不会因大量的失效操作而陷入困境。

幸运的是,时间序列数据意味着几乎所有的 INSERT 和 UPDATE 操作都有最近的时间戳,所以失效引擎不会物化所有数据,而是物化到一个称为物化阈值的时间点。这个阈值是这样设置的,以至于绝大多数的 INSERT 操作都包含更近的时间戳。这些数据点从未被连续聚合物化过,因此不需要额外的工作来通知连续聚合它们已经被添加。当下一次物化器运行时,它负责确定在不使连续聚合失效的情况下可以物化多少新数据。然后,它会物化更新的数据,并将物化阈值向前移动。这确保了阈值滞后于数据更改常见的时间点,并且大多数 INSERT 操作不需要任何额外的写入。

当更改的数据比失效阈值更旧时,会记录更改行的最大和最小时间戳,并使用这些值来确定聚合表中哪些行需要重新计算。这种日志记录确实会产生一些写入负载,但因为阈值滞后于当前正在更改的数据区域,所以写入操作是少量的且罕见的。

 

创建连续聚合

创建连续聚合是一个两步的过程。您首先需要创建视图,然后启用策略以保持视图的刷新。您可以在超表上创建视图,或者在另一个连续聚合之上创建。每个源表或视图上可以有多个连续聚合。

连续聚合要求超表的时间分区列上有一个 time_bucket。

默认情况下,视图会自动刷新。您可以通过设置 WITH NO DATA 选项来调整这一点。此外,视图不能是安全屏障视图。

连续聚合在后台使用超表,这意味着它们也使用分块时间间隔。默认情况下,连续聚合的分块时间间隔是原始超表分块时间间隔的10倍。例如,如果原始超表的分块时间间隔是7天,那么在其之上的连续聚合的分块时间间隔就是70天。

 

创建连续聚合

在这个例子中,我们使用了一个名为 conditions 的超表,并为每日天气数据创建了一个连续聚合视图。GROUP BY 子句必须包含一个 time_bucket 表达式,该表达式使用超表的时间维度列。此外,SELECT、GROUP BY 和 HAVING 子句中包含的所有函数及其参数都必须是不可变的。

1、在 ud_sql 提示符下,创建物化视图:

CREATE MATERIALIZED VIEW conditions_summary_daily
WITH (timeudb.continuous) AS
SELECT device,
   time_bucket(INTERVAL '1 day', time) AS bucket,
   AVG(temperature),
   MAX(temperature),
   MIN(temperature)
FROM conditions
GROUP BY device, bucket;

2、创建每小时刷新视图的策略:

SELECT add_continuous_aggregate_policy('conditions_summary_daily',
  start_offset => INTERVAL '1 month',
  end_offset => INTERVAL '1 day',
  schedule_interval => INTERVAL '1 hour');

您可以在连续聚合中使用大多数 UnvDB 聚合函数。

 

选择合适的桶间隔

连续聚合需要在超表的时间分区列上有一个 time_bucket。时间桶允许您定义一个时间间隔,而不必使用特定的时间戳。例如,您可以将时间桶定义为五分钟或一天。

当连续聚合被物化时,物化表存储部分值,这些部分值随后用于计算查询结果。这意味着任何查询都需要一定的处理能力,并且随着间隔的变小,所需的处理能力会越来越大。因此,如果您的间隔非常小,对超表中的原始数据运行聚合查询可能会更有效。您应该测试这两种方法,以确定哪种方法最适合您的数据集和期望的桶间隔。

您不能直接在连续聚合中使用 time_bucket_gapfill。这是因为您需要访问之前的数据来确定填充内容,而在创建连续聚合时,这些数据还不可用。您可以通过使用 time_bucket创建连续聚合,然后使用time_bucket_gapfill 查询连续聚合来解决这个问题。

 

使用WITH NO DATA选项

默认情况下,当您首次创建视图时,它会被填充数据。这样做是为了在整个超表上进行聚合计算。如果您不希望这种情况发生,例如,如果表非常大,或者如果正在不断添加新数据,您可以控制数据的刷新顺序。您可以通过使用 WITH NO DATA 选项与连续聚合策略一起添加手动刷新来实现这一点。

WITH NO DATA 选项允许连续聚合立即创建,因此您不必等待数据被聚合。数据仅在策略开始运行时才开始填充。这意味着只有比 start_offset 时间更新的数据才开始填充连续聚合。如果您有比 start_offset 间隔更旧的历史数据,您需要手动将历史刷新到当前的 start_offset,以允许实时查询高效运行。

1、在 ud_sql 提示符下,创建视图:

CREATE MATERIALIZED VIEW cagg_rides_view
WITH (timeudb.continuous) AS
SELECT vendor_id,
time_bucket('1h', pickup_datetime) AS hour,
  count(*) total_rides,
  avg(fare_amount) avg_fare,
  max(trip_distance) as max_trip_distance,
  min(trip_distance) as min_trip_distance
FROM rides
GROUP BY vendor_id, time_bucket('1h', pickup_datetime)
WITH NO DATA;

2、手动刷新视图:

CALL refresh_continuous_aggregate('cagg_rides_view', NULL, localtimestamp - INTERVAL '1 week');

3、添加策略:

SELECT add_continuous_aggregate_policy('cagg_rides_view',
  start_offset => INTERVAL '1 week',
  end_offset   => INTERVAL '1 hour',
  schedule_interval => INTERVAL '30 minutes');

 

使用JOIN创建连续聚合

在TimeUDB 以及 UnvDB 中,您可以使用包含JOIN的查询来创建连续聚合。例如:

CREATE MATERIALIZED VIEW conditions_summary_daily_3
WITH (timeudb.continuous) AS
SELECT time_bucket(INTERVAL '1 day', day) AS bucket,
   AVG(temperature),
   MAX(temperature),
   MIN(temperature),
   name
FROM devices JOIN conditions USING (device_id)
GROUP BY name, bucket;

注意: 有关使用JOIN创建连续聚合的更多信息,包括一些额外的限制,请参阅关于连续聚合的部分。

 

查询连续聚合

当您创建了连续聚合并设置了刷新策略后,您可以使用 SELECT 查询来查询视图。在 FROM 子句中,您只能指定一个超表。不支持在 SELECT 查询中包含更多的超表、表、视图或子查询。此外,请确保您正在查询的超表没有启用行级安全策略。

1、在 ud_sql 提示符下,查询名为 conditions_summary_hourly 的连续聚合视图,以获取设备5记录的2021年第一季度的平均、最低和最高温度:

SELECT *
  FROM conditions_summary_hourly
  WHERE device = 5
  AND bucket >= '2020-01-01'
  AND bucket < '2020-04-01';

2、或者,查询名为conditions_summary_hourly的连续聚合视图,以获取该季度中指标差距最大的前20个数据:

SELECT *
  FROM conditions_summary_hourly
  WHERE max - min > 1800
  AND bucket >= '2020-01-01' AND bucket < '2020-04-01'
  ORDER BY bucket DESC, device DESC LIMIT 20;

 

结合窗口函数使用连续聚合

连续聚合当前不支持窗口函数。但您可以通过以下方法来解决此问题:

  • 为查询的其他部分创建连续聚合,然后

  • 在查询时,对连续聚合使用窗口函数

例如,假设您有一个名为 example 的超表,它有一个 time 列和一个 value 列。您按 time 将数据分桶,并使用 lag 窗口函数计算时间桶之间的差值:

WITH t AS (
  SELECT
	time_bucket('10 minutes', time) as bucket,
	first(value, time) as value
  FROM example GROUP BY bucket
)
SELECT
  bucket,
  value - lag(value, 1) OVER (ORDER BY bucket) delta
  FROM t;

您不能使用此查询创建连续聚合,因为它包含 lag 函数。但是,您可以通过排除 lag 函数来创建连续聚合:

CREATE MATERIALIZED VIEW example_aggregate
  WITH (timeudb.continuous) AS
	SELECT
	  time_bucket('10 minutes', time) AS bucket,
	  first(value, time) AS value
	FROM example GROUP BY bucket;

然后,在查询时,通过对连续聚合使用 lag 函数来计算差值:

SELECT
  bucket,
  value - lag(value, 1) OVER (ORDER BY bucket) AS delta
FROM example_aggregate;

这通过提前计算聚合来加速您的查询。差值仍然需要在查询时计算。

 

分层连续聚合

您可以在其他连续聚合之上创建连续聚合。这使您可以汇总不同粒度级别的数据。例如,您可能有一个每小时连续聚合,用于汇总每分钟的数据。要获取每日摘要,您可以在每小时聚合的基础上创建新的连续聚合。这比在原始超表之上创建每日聚合更有效,因为您可以重用每小时聚合中的计算。

 

在另一个连续聚合之上创建一个连续聚合

在另一个连续聚合之上创建连续聚合的工作方式与在超表之上创建连续聚合的方式相同。在查询中,从连续聚合而不是从超表中进行选择,并使用现有连续聚合中的时间段列作为时间列。

有关更多信息,请参阅创建连续聚合的说明 。

 

使用具有分层连续聚合的实时聚合

默认情况下,所有连续聚合都使用实时聚合。这意味着它们总是返回最新的数据以响应查询。它们通过将连续聚合中的具体化数据与源表或视图中的未具体化原始数据相结合来实现这一点。

当连续聚合堆叠时,每个连续聚合仅知道其直接下方的层。未具体化数据的连接会递归进行,直到达到最底层,使您能够访问到该层的最新数据。

如果您将堆栈中的所有连续聚合都保持为实时聚合,则最底层是源超表。这意味着堆栈中的每个连续聚合都可以访问所有最新数据。

如果堆栈中某个位置存在非实时连续聚合,则递归连接会在此非实时连续聚合处停止。较高级别的连续聚合不会从较低级别接收任何未具体化的数据。

例如,假设您有以下连续聚合:

  • 源超表上的实时小时连续聚合

  • 小时连续聚合上的实时日连续聚合

  • 日连续聚合上的非实时(或仅具体化)月连续聚合

  • 月连续聚合上的实时年连续聚合

对小时和日连续聚合的查询包括来自源超表的实时、未具体化的数据。对月连续聚合的查询仅返回已具体化的数据。对年连续聚合的查询返回来自年连续聚合本身的具体化数据,以及来自月连续聚合的更新数据。但是,数据仅限于月连续聚合中已经具体化的数据,并不会从源超表中获取更新的数据。这是因为仅具体化的连续聚合提供了一个停止点,而年连续聚合不知道该停止点之外的任何层。这与 UnvDB 中堆叠视图的工作方式类似。

为了使对年连续聚合的查询能够访问所有最新数据,您可以:

  • 使月连续聚合变为实时聚合,或者

  • 在日连续聚合之上重新定义年连续聚合。

media/202405/cagg_hierarchy_1715249477.webp

 

汇总计算

在汇总已经汇总过的数据时,请注意堆叠计算的工作方式。并非所有计算都可以通过堆叠得到正确结果。

例如,如果您取几个子集的最大值,然后再取这些最大值中的最大值,您将得到整个集合的最大值。但是,如果您取几个子集的平均值,然后再取这些平均值的平均值,那么结果可能与所有数据的平均值不同。

为了在使用连续聚合之上的连续聚合时简化此类计算,您可以使用 TimeUDB Toolkit中的超函数,例如统计聚合。这些超函数采用两步聚合模式设计,允许您将它们汇总到更大的桶中。第一步是创建一个可以汇总的摘要聚合,就像最大值可以被汇总一样。您可以将此聚合存储在连续聚合中。然后,在从连续聚合查询时,您可以调用访问器函数作为第二步。此访问器从摘要聚合中获取存储的数据并返回最终结果。

例如,您可以像这样在超表上使用 percentile_agg 创建一个小时连续聚合:

CREATE MATERIALIZED VIEW response_times_hourly
WITH (timeudb.continuous)
AS SELECT
	time_bucket('1 h'::interval, ts) as bucket,
	api_id,
	avg(response_time_ms),
	percentile_agg(response_time_ms) as percentile_hourly
FROM response_times
GROUP BY 1, 2;

然后,要在其上再堆叠一个日连续聚合,您可以使用 rollup 函数,像这样:

CREATE MATERIALIZED VIEW response_times_daily
WITH (timeudb.continuous)
AS SELECT
	time_bucket('1 d'::interval, bucket) as bucket_daily,
	api_id,
	mean(rollup(percentile_hourly)) as mean,
	rollup(percentile_hourly) as percentile_daily
FROM response_times_hourly
GROUP BY 1, 2;

TimeUDB Toolkit 的平均函数用于计算汇总值的具体均值。额外的 percentile_daily 属性包含原始的汇总值,这些值可以在此连续聚合之上的附加连续聚合中使用(例如,日值的连续聚合)。

有关使用汇总函数进行堆叠计算的更多信息和示例,请参阅百分位近似API文档。

 

限制

在一个连续聚合之上创建另一个连续聚合时,存在一些限制。在大多数情况下,这些限制是为了确保有效的时间分桶:

  • 您只能在一个已完成的连续聚合之上创建另一个连续聚合。

  • 连续聚合的时间分桶应大于或等于底层连续聚合的时间分桶。它还需要是底层时间分桶的倍数。例如,您可以将一个按小时的连续聚合重新分桶为具有6小时时间分桶的新连续聚合。您不能将按小时的连续聚合重新分桶为具有90分钟时间分桶的新连续聚合,因为90分钟不是1小时的倍数。

  • 具有固定宽度时间分桶的连续聚合不能创建在具有可变宽度时间分桶的连续聚合之上。固定宽度的时间分桶是按秒、分钟、小时和天定义的时间分桶,因为这些时间间隔的长度始终相同。可变宽度的时间分桶是按月或年定义的时间分桶,因为这些时间间隔会因月份或闰年而异。这种限制避免了如尝试将月度分桶重新分桶为61天分桶的情况,因为对于像7月/8月(62天)这样的月份组合,时间分桶之间没有良好的映射。

    请注意,尽管周是固定宽度的时间间隔,但出于同样的原因,您不能在周时间分桶之上使用月度或年度时间分桶。一个月或一年中的周数通常不是整数。

    然而,您可以在固定宽度的时间分桶之上堆叠可变宽度的时间分桶。例如,在日连续聚合之上创建月连续聚合是有效的,也是此功能的主要用例之一。

 

刷新连续聚合策略

连续聚合可以有多种不同的刷新策略。除了使用策略自动刷新连续聚合外,您还可以手动刷新它。

 

更改刷新策略

连续聚合需要一种自动刷新的策略。您可以根据不同的用例进行调整。例如,即使从超表中删除了数据,您也可以使连续聚合和超表保持同步,或者在从超表中删除源数据后,您仍然可以在连续聚合中保留源数据。

您可以通过调整 add_continuous_aggregate_policy 来改变连续聚合的刷新方式。该策略接受三个参数:

  • start_offset:刷新窗口的开始时间,相对于策略运行的时间。

  • end_offset:刷新窗口的结束时间,相对于策略运行的时间。

  • schedule_interval:刷新间隔,以分钟或小时为单位。默认为24小时。

如果您将 start_offset 或 end_offset 设置为 NULL,则范围是开放的,并延伸到时间的开始或结束。但是,建议设置 end_offset,以便至少排除最近的时间桶。对于主要包含按时间戳顺序发生的写入的时间序列数据,看到大量写入的时间桶很快就会有过时的聚合。通过排除正在接收大量写入的时间桶,您可以获得更好的性能。

此外,将最近的时间桶具体化可能会干扰实时聚合。

更改刷新策略以使用NULL start_offset:

1、根据 ud_sql提示,创建一个名为 conditions_summary_hourly 的新策略,使连续聚合保持最新,并每小时运行一次:

SELECT add_continuous_aggregate_policy('conditions_summary_hourly',
  start_offset => NULL,
  end_offset => INTERVAL '1 h',
  schedule_interval => INTERVAL '1 h');

此示例中的策略确保连续聚合中的所有数据均与超表保持同步,除了最后一小时写入的任何数据之外。它也不会刷新连续聚合的最后一个时间桶。由于它具有开放式 start_offset 参数,因此从表中删除的任何数据(例如使用 DELETE 或 with drop_chunks)也会从连续聚合视图中删除。这意味着连续聚合始终反映底层超表中的数据。

如果您希望将数据保留在连续聚合中,即使数据已从基础超表中删除,您可以设置 start_offset 来匹配 源超表上的数据保留策略。例如,如果您的保留策略会删除超过一个月的数据,请设置 start_offset 为一个月或更短时间。这会设置您的策略,以便它不会刷新删除的数据。

更改刷新策略以保留已删除的数据:

1、在 ud_sql 提示符下,创建一个名为 conditions_summary_hourly 的新策略,该策略将数据从连续聚合的超表中删除,并每小时运行一次:

SELECT add_continuous_aggregate_policy('conditions_summary_hourly',
  start_offset => INTERVAL '1 month',
  end_offset => INTERVAL '1 h',
  schedule_interval => INTERVAL '1 h');

注意: 在设置连续聚合策略时,考虑数据保留策略非常重要。如果连续聚合策略窗口覆盖了被数据保留策略删除的数据,那么当这些桶的聚合被刷新时,数据将被删除。例如,如果您有一个数据保留策略,该策略删除所有超过两周的旧数据,那么连续聚合策略将仅包含最后两周的数据。

 

手动刷新连续聚合

如果需要手动刷新连续聚合,可以使用refresh命令。这会重新计算自上次刷新以来底层超表中已更改窗口内的数据。因此,如果只有少数几个桶需要更新,刷新操作会很快。

如果您最近从具有连续聚合的超表中删除了数据,对包含已删除块的区域调用refresh_continuous_aggregate会重新计算聚合,但不包括已删除的数据。有关更多信息,请参阅删除数据。

refresh命令接受三个参数:

  • 要刷新的连续聚合视图的名称

  • 刷新窗口的开始时间戳

  • 刷新窗口的结束时间戳

只有完全在指定范围内的桶才会被刷新。例如,如果指定了’2021-05-01’和’2021-06-01’,那么被刷新的唯一桶是直到但不包括’2021-06-01’的那些。在手动刷新中指定NULL以获得开放范围是可以的,但我们不建议这样做,因为您可能会无意中具体化大量数据,降低性能,并对数据保留等其他策略产生意外影响。

手动刷新连续聚合:

1、要手动刷新连续聚合,请使用refresh命令:

CALL refresh_continuous_aggregate('example', '2021-05-01', '2021-06-01');

避免刷新可能有大量写操作的时间间隔。一般来说,这意味着您永远不应该刷新最近的时间桶。由于底层数据的不断变化,它们不太可能产生准确的聚合。此外,由于写放大,刷新这些数据会降低超表的摄取速率。如果您想在查询中包含最新的桶,请使用实时聚合代替。

 

在连续聚合上创建索引

默认情况下,当您创建连续聚合时,会自动创建一些索引。您可以更改此行为。您还可以手动创建和删除索引。

 

自动创建索引

当您创建连续聚合时,会自动为每个 GROUP BY 列创建一个索引。该索引是一个复合索引,将 GROUP BY 列与时间桶(time_bucket)列组合在一起。

例如,如果您使用 GROUP BY device, location, bucket 定义了一个连续聚合视图,则会创建两个复合索引:一个在 {device, bucket} 上,另一个在 {location, bucket} 上。

 

关闭自动索引创建

要关闭自动索引创建,请在创建连续聚合时将timeudb.create_group_indexes设置为false。

例如:

CREATE MATERIALIZED VIEW conditions_daily
  WITH (timeudb.continuous, timeudb.create_group_indexes=false)
  AS
  ...

 

手动创建和删除索引

您可以使用常规的 UnvDB 语句在连续聚合上创建或删除索引。

例如,要在名为 weather_daily 的具体化超表的 avg_temp 上创建索引,可以使用以下语句:

CREATE INDEX avg_temp_idx ON weather_daily (avg_temp);

索引是在 _timeudb_internal模式下创建的,连续聚合数据就存储在这里。要删除索引,请指定模式。例如,要删除索引 avg_temp_idx,请运行:

DROP INDEX _timeudb_internal.avg_temp_idx

 

创建索引的限制

在 TimeUDB 中,您不能在连续聚合上创建唯一索引。

 

连续聚合时间

不支持依赖于连续聚合内部本地时区设置的函数。您无法调整到本地时间,因为时区设置会因用户而异。

为了管理这个问题,您可以在视图定义中使用明确的时区。或者,您可以为使用整数时间列的表创建自己的自定义聚合方案。

 

声明明确的时区

处理时区的最常见方法是在视图查询中声明一个明确的时区。

声明明确的时区:

1、在 ud_sql 提示符下,创建视图并声明时区:

CREATE MATERIALIZED VIEW device_summary
WITH (timeudb.continuous)
AS
SELECT
  time_bucket('1 hour', observation_time) AS bucket,
  min(observation_time AT TIME ZONE 'EST') AS min_time,
  device_id,
  avg(metric) AS metric_avg,
  max(metric) - min(metric) AS metric_spread
FROM
  device_readings
GROUP BY bucket, device_id;

2、或者,您可以在使用SELECT的视图之后转换为时间戳:

SELECT min_time::timestamp FROM device_summary;

 

基于整数的时间

日期和时间通常以年-月-日和时:分:秒的形式表示。大多数 TimeUDB 数据库使用日期/时间类型的列来表达日期和时间。然而,在某些情况下,您可能需要将这些常见的日期和时间格式转换为使用整数的格式。最常见的整数时间是Unix纪元时间,即自1970年1月1日Unix纪元以来的秒数,但也可能存在其他类型的基于整数的时间格式。

这些示例使用了一个名为 devices 的超表,该表包含 CPU 和磁盘使用信息。设备使用Unix纪元来测量时间。

要创建一个使用基于整数的列作为时间的超表,您需要提供块时间间隔。在这个例子中,每个块是10分钟。

创建具有自定义基于整数的时间列的表:

1、在 ud_sql 提示符下,创建一个表并定义基于整数的时间列:

CREATE TABLE devices(
  time BIGINT,        -- Time in minutes since epoch
  cpu_usage INTEGER,  -- Total CPU usage
  disk_usage INTEGER, -- Total disk usage
  PRIMARY KEY (time)
);

2、定义块时间间隔:

SELECT create_hypertable('devices', by_range('time', 10));

若要在使用基于整数的时间的超表上定义连续聚合,您需要有一个函数以正确的格式获取当前时间,并为其设置超表。您可以使用 set_integer_now_func 函数来实现这一点。它可以定义为常规的 UnvDB 函数,但必须是STABLE的,不接受任何参数,并返回与表中时间列相同类型的整数值。设置好时间处理后,您就可以创建连续聚合了。

使用基于整数的时间创建连续聚合:

1、在 ud_sql 提示符下,设置一个函数将时间转换为 Unix 纪元:

CREATE FUNCTION current_epoch() RETURNS BIGINT
LANGUAGE SQL STABLE AS $$
SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::bigint;$$;

 SELECT set_integer_now_func('devices', 'current_epoch');

2、为 devices 表创建连续聚合:

CREATE MATERIALIZED VIEW devices_summary
WITH (timeudb.continuous) AS
SELECT time_bucket('500', time) AS bucket,
   avg(cpu_usage) AS avg_cpu,
   avg(disk_usage) AS avg_disk
FROM devices
GROUP BY bucket;

3、向表中插入一些行:

CREATE EXTENSION tablefunc;

INSERT INTO devices(time, cpu_usage, disk_usage)
SELECT time,
   normal_rand(1,70,10) AS cpu_usage,
  normal_rand(1,2,1) * (row_number() over()) AS disk_usage
FROM generate_series(1,10000) AS time;

该命令使用 tablefunc 扩展生成正态分布,并使用 row_number 函数将其转换为累积序列。

4、检查视图是否包含正确的数据:

unvdb=# SELECT * FROM devices_summary ORDER BY bucket LIMIT 10;
bucket |       avg_cpu       |       avg_disk
-------+---------------------+----------------------
	 0 | 63.0000000000000000 |   6.0000000000000000
	 5 | 69.8000000000000000 |   9.6000000000000000
	10 | 70.8000000000000000 |  24.0000000000000000
	15 | 75.8000000000000000 |  37.6000000000000000
	20 | 71.6000000000000000 |  26.8000000000000000
	25 | 67.6000000000000000 |  56.0000000000000000
	30 | 68.8000000000000000 |  90.2000000000000000
	35 | 71.6000000000000000 |  88.8000000000000000
	40 | 66.4000000000000000 |  81.2000000000000000
	45 | 68.2000000000000000 | 106.0000000000000000
(10 rows)

 

从连续聚合中删除数据

当您处理连续聚合时,可以删除视图,也可以从底层超表或从连续聚合本身中删除原始数据。刷新和数据保留策略的组合可以帮助您进行数据下采样。这允许您以比最近数据更低的粒度保留历史数据。

但是,您应该注意,如果保留策略可能会从您的超表中删除连续聚合中所需的原始数据,那么这一点需要您特别留意。

为了简化设置下采样的过程,您可以使用可视化工具和代码生成器。

 

删除连续聚合视图

您可以使用 DROP MATERIALIZED VIEW 命令删除连续聚合视图。此命令还会删除在连续聚合上定义的刷新策略。它不会从底层超表中删除数据。

删除连续聚合视图:

1、根据 ud_sql 提示,删除视图:

DROP MATERIALIZED VIEW view_name;

 

从超表中删除原始数据

如果您从用于连续聚合的超表中删除数据,可能会导致连续聚合视图出现问题。在许多情况下,删除基础数据会用 NULL 值替换聚合,这可能会在视图中导致意外结果。

您可以按常规方式使用 drop_chunks 从超表中删除数据,但在执行此操作之前,请始终检查该块是否不在仍需要数据的连续聚合的刷新窗口内。如果您正在手动刷新连续聚合,这一点也很重要。对包含已删除块的区域调用 refresh_continuous_aggregate 会重新计算聚合,但不包括已删除的数据。

如果由于保留策略而删除数据时连续聚合正在刷新,聚合会更新以反映数据的丢失。如果需要在删除基础数据后保留连续聚合,请将聚合策略的 start_offset 值设置为小于保留策略的 drop_after 参数的间隔。

SELECT add_retention_policy('hypertable_name', INTERVAL '1 year');

SELECT add_continuous_aggregate_policy('continuous_aggregate_name',
  start_offset => INTERVAL '11 hours',
  end_offset => INTERVAL '30 minutes',
  schedule_interval => INTERVAL '11 seconds'
);

SELECT add_retention_policy('continuous_aggregate_name', INTERVAL '5 years');

 

物化超表

连续聚合从原始超表中获取原始数据,对其进行聚合,并将中间状态存储在物化超表中。您可以像修改任何其他超表一样修改此物化超表。

 

发现物化超表的名称

要更改物化超表,您需要使用其完全限定名称。要查找正确的名称,请使用 timeudb_information.continuous_aggregates 视图)。然后,您可以使用该名称以与任何其他超表相同的方式修改它。

发现物化超表的名称:

1、根据 ud_sql提示,查询 timeudb_information.continuous_aggregates:

SELECT view_name, format('%I.%I', materialization_hypertable_schema,
		materialization_hypertable_name) AS materialization_hypertable
	FROM timeudb_information.continuous_aggregates;

2、在查询结果中找到要调整的超表的名称。结果如下:

        view_name         |            materialization_hypertable
--------------------------+---------------------------------------------------
conditions_summary_hourly | _timeudb_internal._materialized_hypertable_30
conditions_summary_daily  | _timeudb_internal._materialized_hypertable_31
(2 rows)

 

实时聚合

连续聚合不包括底层超表中最新的数据块。实时聚合使用聚合数据,并向其中添加最新的原始数据,以提供准确和最新的结果,而无需在写入数据时聚合数据。在TimeUDB 中,实时聚合默认禁用。

 

使用实时聚合

您可以在创建或更改视图时通过设置 materialized_only 参数来启用和禁用实时聚合 。

1、对于现有表,在 ud_sql 提示符下禁用实时聚合:

ALTER MATERIALIZED VIEW table_name set (timeudb.materialized_only = true);

2、重新启用实时聚合:

ALTER MATERIALIZED VIEW table_name set (timeudb.materialized_only = false);

 

实时汇总并刷新历史数据

当您查询连续聚合时,实时聚合会自动添加最新数据。换句话说,它们包含比您最后一个物化存储桶更新的数据。

如果您将新的历史数据添加到已经物化的存储桶中,它将不会反映在实时聚合中。您应该等待下一次计划的刷新,或者通过调用 refresh_continuous_aggregate 手动刷新。您可以将实时聚合视为历史数据的最终一致性。

 

压缩连续聚合体

连续聚合通常用于对历史数据进行下采样。如果数据仅用于分析查询且从未修改,您可以压缩聚合以节省存储空间。

对连续聚合的压缩与对超表的压缩类似。当启用压缩且未提供其他选项时,segment_by 值将自动设置为连续聚合的按列分组,并且 time_bucket 列将用作 order_by 压缩配置中的列。

 

对连续聚合启用压缩

您可以在更改视图时通过设置 compress 参数来启用和禁用连续聚合的压缩 。

1、对于现有的连续聚合,在 ud_sql出现提示时启用压缩:

ALTER MATERIALIZED VIEW cagg_name set (timeudb.compress = true);

2、禁用压缩:

ALTER MATERIALIZED VIEW cagg_name set (timeudb.compress = false);

如果存在与连续聚合关联的压缩块,则对连续聚合禁用压缩会失败。在这种情况下,您需要解压缩块,然后在禁用压缩之前删除连续聚合上的任何压缩策略。有关更多详细信息,请参阅解压缩块部分:

SELECT decompress_chunk(c, true) FROM show_chunks('cagg_name') c;

 

连续聚合的压缩策略

在连续聚合上设置压缩策略之前,您应该设置刷新策略。应设置压缩策略间隔,以便主动刷新的区域不被压缩。这是为了防止刷新策略失败。例如,考虑这样的刷新策略:

SELECT add_continuous_aggregate_policy('cagg_name',
  start_offset => INTERVAL '30 days',
  end_offset => INTERVAL '1 day',
  schedule_interval => INTERVAL '1 hour');

采用这种刷新策略,压缩策略需要的参数 compress_after 大于 start_offse 连续聚合策略的参数:

SELECT add_compression_policy('cagg_name', compress_after=>'45 days'::interval);

 

将连续聚合迁移到新形式

在 TimeUDB 中,连续聚合使用了一种新格式,该格式提高了性能并使其与更多的 SQL 查询兼容。在创建但将选项 timeudb.finalized 设置为 false 的连续聚合,都使用旧格式。

要将连续聚合从旧格式迁移到新格式,您可以使用此过程。它会自动复制您的数据和策略。在迁移过程中,您可以继续使用连续聚合。

请连接到您的数据库并执行:

CALL cagg_migrate('<CONTINUOUS_AGGREGATE_NAME>');

 

配置连续聚合迁移

迁移过程提供了两个布尔配置参数:override 和 drop_old。默认情况下,您的新连续聚合的名称是旧连续聚合的名称,并带有后缀 _new。

将 override 设置为 true,可以使用原始名称重命名您的新连续聚合。旧的连续聚合将被重命名,并带有后缀 _old。

要同时重命名并完全删除旧的连续聚合,请将两个参数都设置为 true。请注意,drop_old 必须与 override一起使用。

 

检查连续聚合迁移状态

要检查连续聚合迁移的进度,请查询迁移计划表:

SELECT * FROM _timeudb_catalog.continuous_agg_migrate_plan_step;

 

故障排除

迁移连续聚合时出现权限错误

使用 cagg_migrate 将连续聚合从旧格式迁移到新格式时,您可能会遇到权限错误。执行迁移的用户必须具有以下权限:

  • 对_timescale_catalog.continuous_agg_migrate_plan 和 _timescale_catalog.continuous_agg_migrate_plan_step 表的选择、插入和更新权限

  • 对序列 _timeudb_catalog.continuous_agg_migrate_plan_step_step_id_seq 的使用权限

为了解决这个问题,请切换到一个有权限授权的用户,并将以下权限授予执行迁移的用户:

	GRANT SELECT, INSERT, UPDATE ON TABLE _timeudb_catalog.continuous_agg_migrate_plan TO <USER>;
GRANT SELECT, INSERT, UPDATE ON TABLE _timeudb_catalog.continuous_agg_migrate_plan_step TO <USER>;
GRANT USAGE ON SEQUENCE _timeudb_catalog.continuous_agg_migrate_plan_step_step_id_seq TO <USER>;

 

解决连续聚合问题

本节包含一些解决连续聚合遇到的常见问题的想法。

 

连续聚合的水印在未来

连续聚合使用水印来指示哪些时间桶已经被物化。当您查询连续聚合时,您的查询会返回水印之前的物化数据。它会返回水印之后的实时、非物化数据。

在某些情况下,水印可能在未来。如果发生这种情况,所有桶(包括最近的桶)都被物化并且处于水印之下。不会返回实时数据。

如果您在时间窗口 < START_TIME >, NULL 上刷新连续聚合(这会物化所有最新数据),可能会发生这种情况。如果您使用 WITH DATA 选项创建连续聚合,也可能会发生这种情况。这也会使用 NULL, NULL 的窗口隐式刷新您的连续聚合。

要解决这个问题,请使用 WITH NO DATA 选项创建一个新的连续聚合。然后使用一个策略在明确的时间窗口上刷新这个连续聚合。

使用显式刷新窗口创建新的连续聚合:

1、使用 WITH NO DATA 选项创建连续聚合:

CREATE MATERIALIZED VIEW <continuous_aggregate_name>
	WITH (timeudb.continuous)
	AS SELECT time_bucket('<interval>', <time_column>),
	<other_columns_to_select>,
	...
	FROM <hypertable>
	GROUP BY bucket, <optional_other_columns>
	WITH NO DATA;

2、使用带有明确 end_offset 的策略来刷新连续聚合。例如:

SELECT add_continuous_aggregate_policy('<continuous_aggregate_name>',
	start_offset => INTERVAL '30 day',
	end_offset => INTERVAL '1 hour',
	schedule_interval => INTERVAL '1 hour');

3、检查您的新连续聚合的水印,确保它在过去,而不是在未来。

获取包含实际连续聚合数据的物化超表的ID:

SELECT id FROM _timeudb_catalog.hypertable
	WHERE table_name=(
		SELECT materialization_hypertable_name
			FROM timeudb_information.continuous_aggregates
			WHERE view_name='<continuous_aggregate_name>'
	);

4、使用返回的ID查询水印的时间戳:

SELECT COALESCE(
	_timeudb_functions.to_timestamp(_timeudb_functions.cagg_watermark(<ID>)),
	'-infinity'::timestamp with time zone
);

警告: 如果您选择在创建新连续聚合后删除旧的连续聚合,请注意历史数据丢失。如果旧的连续聚合包含从原始超表中删除的数据(例如通过数据保留策略),则删除的数据不会包含在新的连续聚合中。

 

分层连续聚合因桶宽度不兼容而失败

ERROR:  cannot create continuous aggregate with incompatible bucket width
DETAIL:  Time bucket width of "<BUCKET>" [1 year] should be multiple of the time bucket width of "<BUCKET>" [1 day]

如果您尝试创建分层连续聚合,则必须使用兼容的时间段。您无法在具有可变宽度时间段的连续聚合之上创建具有固定宽度时间段的连续聚合。

 

超表保留策略不适用于连续聚合

在超表上设置的保留策略不适用于由该超表生成的任何连续聚合。这允许您为原始数据和汇总数据设置不同的保留期限。要将保留策略应用于连续聚合,请在连续聚合本身上设置策略。

 

连续聚合不会刷新新插入的历史数据

物化视图通常与有序数据一起使用。如果插入历史数据或与当前时间无关的数据,则需要刷新策略并重新评估从过去拖动到当前的值。

您可以为超表或更新插入设置插入后规则,以触发可以验证数据合并时需要刷新的内容的规则。

假设您插入了名为 A、B、D 和 F 的有序时间范围,并且您已经有一个连续聚合来查找该数据。如果您现在插入 E,则需要刷新 E 和 F。但是,如果您插入 C,我们将需要刷新 C、D、E 和 F

例如:

  1. A、B、D 和 F 已在包含所有数据的视图中具体化。

  2. 要插入 C,请将数据分为AB和DEF子集。

  3. AB一致,物化数据也一致;你只需要重复使用它。

  4. 插入 C、DEF,并在 C 之后刷新策略。

这可能会使用大量资源来处理,特别是如果您过去有任何重要数据也需要带到现在。

考虑一个示例,其中单个超表上有 300 列,并在连续聚合中使用其中的 5 个列。在这种情况下,可能很难刷新,并且将这些列隔离在另一个超表中更有意义。或者,您可以为每个指标创建一个超表并独立刷新它们

 

迁移连续聚合时出现权限错误

使用 cagg_migrate 将连续聚合从旧格式迁移到新格式时,您可能会遇到权限错误。执行迁移的用户必须具有以下权限:

  • 对表 _timescale_catalog.continuous_agg_migrate_plan 和 _timescale_catalog.continuous_agg_migrate_plan_step 的选择、插入和更新权限.

  • 对序列 _timeudb_catalog.continuous_agg_migrate_plan_step_step_id_seq 的使用权限.

解决方法:切换为有权限的用户,并为执行迁移的用户授予以下权限:

GRANT SELECT, INSERT, UPDATE ON TABLE _timeudb_catalog.continuous_agg_migrate_plan TO <USER>;
GRANT SELECT, INSERT, UPDATE ON TABLE _timeudb_catalog.continuous_agg_migrate_plan_step TO <USER>;
GRANT USAGE ON SEQUENCE _timeudb_catalog.continuous_agg_migrate_plan_step_step_id_seq TO <USER>;

 

定义连续聚合时查询失败,但适用于常规表

ERROR:  invalid continuous aggregate view
SQL state: 0A000

连续聚合并不适用于所有查询。如果您使用连续聚合不支持的函数,您会看到上面的错误。

TimeUDB 不支持连续聚合上的窗口函数.

下表总结了连续聚合中的聚合函数支持:

Function, clause, or feature
Parallelizable aggregate functions
Non-parallelizable aggregate function
ORDER BY
Ordered-set aggregates
Hypothetical-set aggregates
DISTINCT in aggregate functions
FILTER in aggregate functions
FROM clause supports JOINS

 

使用locf()的查询不会将NULL值视为缺失值

当您的查询使用前向填充(last observation carried forward, locf)函数时,该查询默认会携带 NULL 值。如果您希望该函数忽略NULL值,您可以在查询中将 treat_null_as_missing=TRUE 设置为第二个参数。例如:

dev=# select * FROM (select time_bucket_gapfill(4, time,-5,13), locf(avg(v)::int,treat_null_as_missing:=true) FROM (VALUES (0,0),(8,NULL)) v(time, v) WHERE time BETWEEN 0 AND 10 GROUP BY 1) i ORDER BY 1 DESC;
 time_bucket_gapfill | locf
---------------------+------
				  12 |    0
				   8 |    0
				   4 |    0
				   0 |    0
				  -4 |
				  -8 |
(6 rows)

 

计划作业停止运行

您的计划作业可能会因各种原因停止运行。在自托管的 TimeUDB 上,您可以通过重新启动后台工作程序来解决此问题:

SELECT _timeudb_functions.start_background_workers();

对于 TimeUDB 的托管服务,通过执行以下操作之一来重新启动后台工作进程:

  • 运行 SELECT timeudb_pre_restore(),然后运行 SELECT timeudb_post_restore()。

  • 关闭服务后再重新打开。这可能会导致几分钟的停机时间,因为服务会从备份中恢复并重新播放预写日志。

 

对先前物化区域的更新不会显示在实时聚合中

实时聚合在查询连续聚合时会自动添加最新的数据。换句话说,它们包括比最后一个物化的存储桶更新的数据。

如果您将新的历史数据添加到已经物化的存储桶中,它将不会反映在实时聚合中。您应该等待下一个计划的刷新,或者通过调用 refresh_continuous_aggregate 手动刷新。您可以将实时聚合视为对历史数据的最终一致性。

以下示例展示了这是如何工作的:

创建并填充超表:

CREATE TABLE conditions(
  day DATE NOT NULL,
  city text NOT NULL,
  temperature INT NOT NULL);

SELECT create_hypertable(
  'conditions', by_range('day', INTERVAL '1 day')
);

INSERT INTO conditions (day, city, temperature) VALUES
  ('2021-06-14', 'Moscow', 26),
  ('2021-06-15', 'Moscow', 22),
  ('2021-06-16', 'Moscow', 24),
  ('2021-06-17', 'Moscow', 24),
  ('2021-06-18', 'Moscow', 27),
  ('2021-06-19', 'Moscow', 28),
  ('2021-06-20', 'Moscow', 30),
  ('2021-06-21', 'Moscow', 31),
  ('2021-06-22', 'Moscow', 34),
  ('2021-06-23', 'Moscow', 34),
  ('2021-06-24', 'Moscow', 34),
  ('2021-06-25', 'Moscow', 32),
  ('2021-06-26', 'Moscow', 32),
  ('2021-06-27', 'Moscow', 31);

创建连续聚合但不具体化任何数据。请注意,默认情况下启用实时聚合:

CREATE MATERIALIZED VIEW conditions_summary
WITH (timeudb.continuous) AS
SELECT city,
   time_bucket('7 days', day) AS bucket,
   MIN(temperature),
   MAX(temperature)
FROM conditions
GROUP BY city, bucket
WITH NO DATA;

The select query returns data as real time aggregates are enabled. The query on
the continuous aggregate fetches data directly from the hypertable:
SELECT * FROM conditions_summary ORDER BY bucket;
  city  |   bucket   | min | max
--------+------------+-----+-----
 Moscow | 2021-06-14 |  22 |  30
 Moscow | 2021-06-21 |  31 |  34

将数据具体化为连续聚合:

CALL refresh_continuous_aggregate('conditions_summary', '2021-06-14', '2021-06-21');

The select query returns the same data, as expected, but this time the data is
fetched from the underlying materialized table
SELECT * FROM conditions_summary ORDER BY bucket;
  city  |   bucket   | min | max
--------+------------+-----+-----
 Moscow | 2021-06-14 |  22 |  30
 Moscow | 2021-06-21 |  31 |  34

更新之前物化桶中的数据:

UPDATE conditions
SET temperature = 35
WHERE day = '2021-06-14' and city = 'Moscow';

当您查询连续聚合时,更新的数据尚不可见。这是因为这些更改尚未实现。(同样,任何 INSERT 或 DELETE 也将不可见)。

SELECT * FROM conditions_summary ORDER BY bucket;
  city  |   bucket   | min | max
--------+------------+-----+-----
 Moscow | 2021-06-14 |  22 |  30
 Moscow | 2021-06-21 |  31 |  34

再次刷新数据以更新之前物化的区域:

CALL refresh_continuous_aggregate('conditions_summary', '2021-06-14', '2021-06-21');

SELECT * FROM conditions_summary ORDER BY bucket;
  city  |   bucket   | min | max
--------+------------+-----+-----
 Moscow | 2021-06-14 |  22 |  35
 Moscow | 2021-06-21 |  31 |  34

   

数据保留

数据保留通过删除旧数据帮助您节省存储成本。您可以将数据保留与连续聚合相结合,以对数据进行降采样。

 

关于数据保留

在时间序列应用中,数据随着时间的推移会变得越来越没有用处。如果您不需要历史数据,可以在其达到一定年限后将其删除。TimeUDB 允许您设置自动数据保留策略以丢弃旧数据。您还可以通过手动删除数据块来微调数据保留。

通常,您希望保留历史数据的摘要,但不需要原始数据。您可以通过将数据保留与连续聚合相结合来对旧数据进行降采样。

 

按块删除数据

TimeUDB 的数据保留是基于数据块而非基于数据行的。逐行删除数据,例如使用 UnvDB 的 DELETE 命令,可能会很慢。但是,按块删除数据更快,因为它会从磁盘中删除整个文件。它不需要进行垃圾回收和碎片整理。

无论您是使用策略还是手动删除数据块,TimeUDB 都是按块删除数据的。它仅删除所有数据都在指定时间范围内的数据块。

例如,考虑以下设置,其中包含3个数据块的数据:

  • 超过36小时的数据

  • 12至36小时之间的数据

  • 过去12小时内的数据

您手动删除超过24小时的数据块。只有最老的数据块会被删除。中间的数据块会被保留,因为它包含一些新于24小时的数据。不会从该数据块中删除任何单独的行。

 

关于连续聚合的数据保留

您可以通过将数据保留策略与连续聚合相结合来对数据进行降采样。如果正确设置了刷新策略,您可以从超表中删除旧数据,而不会从任何连续聚合中删除它。这让您能够在节省原始数据存储空间的同时,保留汇总数据以供历史分析。

警告: 为了在删除原始数据时保留聚合数据,您必须小心刷新聚合数据。您可以从基础表中删除原始数据,而不会从连续聚合中删除数据,但前提是不要在已删除的数据上刷新聚合。当您刷新连续聚合时,TimeUDB 会根据刷新窗口中原始数据的更改来更新聚合。如果它发现原始数据已被删除,它还会删除聚合数据。为防止这种情况,请确保聚合的刷新窗口不会与任何已删除的数据重叠。有关更多信息,请参阅以下示例。

例如,假设您向存储设备温度的conditions超表中添加了一个连续聚合:

CREATE MATERIALIZED VIEW conditions_summary_daily (day, device, temp)
WITH (timeudb.continuous) AS
  SELECT time_bucket('1 day', time), device, avg(temperature)
  FROM conditions
  GROUP BY (1, 2);

SELECT add_continuous_aggregate_policy('conditions_summary_daily', '7 days', '1 day', '1 day');

这将创建一个名为 conditions_summary_daily 的聚合,它按设备存储每日温度。该聚合每天刷新。每次刷新时,它都会更新从7天前到1天前的任何数据变化。

您不应该在 conditions 超表上设置24小时的数据保留策略。如果这样做,超过1天的数据块将被删除。然后聚合会根据数据变化进行刷新。由于数据变化是删除超过1天的数据,因此聚合也会删除这些数据。最终, conditions_summary_daily 表中将没有任何数据。

为了解决这个问题,请设置更长的数据保留策略,例如30天:

SELECT add_retention_policy('conditions', INTERVAL '30 days');

现在,超过30天的数据块会被删除。但是,当聚合刷新时,它不会查找超过30天的更改。它只查找7天前到1天前的更改。原始超表仍然包含该时间段的数据。因此,您的聚合会保留数据。

 

对连续聚合本身的数据保留

您还可以在连续聚合本身上应用数据保留策略。例如,如前所述,您可以保留30天的原始数据。同时,您可以保留600天的日数据,而超出此范围的数据则不保留。

 

创建数据保留策略

一旦数据的时间值超过一定时间间隔,就会自动删除数据。当您创建数据保留策略时,TimeUDB 会自动安排后台作业来删除旧数据块。

 

添加数据保留策略

使用 add_retention_policy 功能添加数据保留策略。

1、选择您想要添加策略的超表。决定在删除数据之前想要保留数据的时间长度。在此示例中,名为 conditions 的超表将保留数据24小时。

2、调用 add_retention_policy:

SELECT add_retention_policy('conditions', INTERVAL '24 hours');

注意: 数据保留策略仅允许您基于数据块过去的时间长短来删除它们。若要根据数据块距离未来的时间长短来删除它们,请手动删除数据块。

 

移除数据保留策略

使用 remove_retention_policy 函数来移除现有的数据保留策略。将要从中移除策略的超表名称传递给它。

SELECT remove_retention_policy('conditions');

 

查看计划的数据保留作业

要查看计划的数据保留作业及其作业统计信息,请查询 timeudb_information.jobs和 timeudb_information.job_stats表。例如:

SELECT j.hypertable_name,
	   j.job_id,
	   config,
	   schedule_interval,
	   job_status,
	   last_run_status,
	   last_run_started_at,
	   js.next_start,
	   total_runs,
	   total_successes,
	   total_failures
  FROM timeudb_information.jobs j
  JOIN timeudb_information.job_stats js
	ON j.job_id = js.job_id
  WHERE j.proc_name = 'policy_retention';

结果如下:

-[ RECORD 1 ]-------+-----------------------------------------------
hypertable_name     | conditions
job_id              | 1000
config              | {"drop_after": "5 years", "hypertable_id": 14}
schedule_interval   | 1 day
job_status          | Scheduled
last_run_status     | Success
last_run_started_at | 2022-05-19 16:15:11.200109+00
next_start          | 2022-05-20 16:15:11.243531+00
total_runs          | 1
total_successes     | 1
total_failures      | 0

 

手动删除块

手动按时间值删除数据块。例如,删除包含超过30天数据的数据块。

注意: 手动删除数据块是一次性操作。要随着数据块的老化自动删除它们,请设置数据保留策略。

 

删除早于特定日期的数据块

要删除早于特定日期的数据块,请使用drop_chunks函数。提供要删除数据块的超表名称,以及一个时间间隔,超过该间隔的数据块将被删除。

例如,要删除包含超过24小时数据的数据块:

SELECT drop_chunks('conditions', INTERVAL '24 hours');

 

删除两个日期之间的数据块

您还可以删除两个日期之间的数据块。例如,删除包含3到4个月前数据的数据块。

为 newer_than 截止时间提供第二个 INTERVAL 参数:

SELECT drop_chunks(
  'conditions',
  older_than => INTERVAL '3 months',
  newer_than => INTERVAL '4 months'
)

 

删除未来的数据块

您也可以删除未来的数据块,例如用于纠正带有错误时间戳的数据。例如,要删除所有超过未来3个月的数据块:

SELECT drop_chunks(
  'conditions',
  newer_than => now() + INTERVAL '3 months'
);

 

数据保留故障排除

本节包含一些解决数据保留中遇到的常见问题的思路。

 

超表保留策略不适用于连续聚合

在超表上设置的保留策略不适用于从该超表生成的任何连续聚合。这允许您为原始数据和汇总数据设置不同的保留期限。要将保留策略应用于连续聚合,请在连续聚合本身上设置策略。

 

删除数据块超时

当您删除一个数据块时,它需要一个排它锁。如果另一个会话正在访问数据块,则您不能同时删除该数据块。如果删除数据块的操作无法获取数据块上的锁,则会超时并且进程失败。要解决此问题,请检查是什么锁定了数据块。在某些情况下,这可能是由连续聚合或其他正在访问数据块的进程引起的。当删除数据块的操作能够获取数据块上的排它锁时,它将按预期完成。

有关锁的更多信息,请参阅UnvDB锁监视文档。

 

计划的任务停止运行

您的计划任务可能由于各种原因停止运行。在自托管的 TimeUDB 上,您可以通过重新启动后台工作程序来解决此问题:

SELECT _timeudb_functions.start_background_workers();

在 TimeUDB 的托管服务上,通过执行以下操作之一来重新启动后台工作程序:

  • 运行SELECT timeudb_pre_restore(),然后运行SELECT timeudb_post_restore()。

  • 将服务关闭然后再次开启。这可能会导致几分钟的停机时间,因为服务会从备份中恢复并重放预写日志。

 

对超表重建索引以修复大型索引

ERROR:  invalid attribute number -6 for _hyper_2_839_chunk
CONTEXT:  SQL function "hypertable_local_size" statement 1 PL/pgSQL function hypertable_detailed_size(regclass) line 26 at RETURN QUERY SQL function "hypertable_size" statement 1
SQL state: XX000

如果您的超表索引变得非常大,您可能会看到此错误。要解决该问题,请使用以下命令重建超表索引:

reindex table _timeudb_internal._hyper_2_1523284_chunk

   

分层存储

分层存储是 TimeUDB 的分级存储管理架构。它专为无限低成本可扩展性而设计,适用于您在 TimeUDB 中创建的时间序列和分析实例。

分层存储包括:

  • 高性能层:快速访问最新且经常访问的数据。

  • 对象存储层:存储很少访问且性能要求较低的数据。例如,为了长时间甚至永久保存旧数据以供审计或报告之用。对象存储是建立在 UnvDB-TO 上的低成本、无容量限制的数据存储。您可以使用它来避免与高性能层相关的高成本和数据大小限制。

无论您的数据存储在哪个层级,都可以在需要时查询它。TimeUDB 无缝访问正确的存储层级并生成响应。

media/202405/timescale-tiered-storage-architecture_1715309366.png

您可以使用API来定义分层策略,这些策略会随着数据的老化而自动将数据从高性能存储层迁移到对象存储。您还使用保留策略来从对象存储中删除非常旧的数据。

有了分层存储,您无需 ETL(Extract, Transform, Load)流程、基础设施更改或定制解决方案来卸载数据到二级存储,并在需要时将其取回。您只需高枕无忧,我们会为您完成这些工作。

信息: 分层存储仅适用于您在 TimeUDB 中创建的时间序列和分析实例。分层存储不适用于自托管的 TimeUDB 的托管服务。

 

关于对象存储层

分层存储架构通过低成本的对象存储层补充了 TimeUDB 的标准高性能存储层。

您可以将超表数据在不同存储层之间移动,以获得最佳的价格性能比。对于需要快速访问的数据,您可以使用标准高性能存储层;而对于很少使用的历史数据,则可以使用低成本的对象存储层。无论您的数据存储在何处,您仍然可以使用标准SQL查询它。

 

对象存储层的优势

对象存储层不仅仅是一个归档解决方案,它还具有以下优势:

  • 成本效益。以成本高效的方式存储大量数据。您只需为实际存储的数据付费,而查询不会产生额外费用。

  • 可扩展性。突破可以直接附加到 TimeUDB 服务上的存储限制(当前为16 TB)。

  • 在线性。您的数据始终可用,并可以在需要时进行查询。

 

架构

分层存储后端通过周期性地和异步地将较旧的数据块移动到对象存储层来工作;该对象存储是构建在 UnvDB-TO 上的。在迁移期间和迁移之后,数据都保持可访问状态。

默认情况下,从 TimeUDB 服务查询时不会包含分层数据。但是,可以通过为会话、查询或甚至所有会话启用分层读取来访问分层数据。

启用分层读取后,当您运行常规的SQL查询时,一个幕后进程会透明地从数据实际所在的位置拉取数据:无论是标准的高性能存储层、对象存储层,还是两者兼有。各种 SQL 优化限制了需要从 S3 读取的内容:

  • 数据块排除避免了处理查询时间窗口之外的数据块。

  • 数据库使用关于行组和列偏移量的元数据,因此只需要从 S3 读取对象的一部分。

结果是跨标准 UnvDB 存储和 S3 存储的透明查询,因此您的查询获取与以前相同的数据。

 

限制

  • 有限的模式修改。对于具有分层数据块的超表,某些模式修改是不允许的。

    允许的修改包括:重命名超表、添加带有 NULL 默认值的列、添加索引、更改或重命名超表模式,以及添加 CHECK 约束。对 于CHECK 约束,仅验证未分层的数据。

    不允许的修改包括:添加带有非 NULL 默认值的列、重命名列、删除列、更改列的数据类型,以及向列添加 NOT NULL 约束。

  • 有限的数据更改。您不能向已分层的数据块中插入数据、更新数据或删除数据。这些限制在数据块被安排进行分层时立即生效。

  • 对非本地数据类型的查询规划器过滤效率低下。查询规划器通过使用元数据来过滤出不满足查询的列和行组,从而加速从我们的对象存储层读取数据。这适用于所有本地数据类型,但不适用于非本地类型,如 JSON、JSONB 和 GIS。

  • 延迟。S3 的访问延迟高于本地存储。这可能会影响延迟敏感环境中查询的执行时间,尤其是较轻的查询。

  • 维度。您不能在与多维分区的超表上使用分层存储。在启用分层存储之前,请确保您的超表仅按时间进行分区。

   

高可用性和副本

您可以在 TimeUDB 服务上使用高可用性和只读副本,以显著降低因故障导致的停机和数据丢失风险,并更有效地扩展您的服务限制。

TimeUDB 中的 HA(高可用)副本是您的数据库的精确、最新拷贝,如果主数据库不可用(包括维护期间),它们会自动接管操作。

只读副本可以创建一个隔离的环境来运行繁重的分析查询,这样您就不需要在生产实例上运行它们,从而避免影响性能的风险。

在 TimeUDB 中创建HA副本

在 TimeUDB 中创建只读副本

如果您使用的是 TimeUDB 托管服务,请参阅 TimeUDB 托管服务的故障转移部分。

如果您使用的是自托管的 TimeUDB,请参阅自托管HA部分。

 

高可用性

所有 TimeUDB 服务都默认启用快速恢复功能。快速恢复确保在最常见的故障场景和维护期间,所有服务都能将停机时间和数据损失降到最低。对于对停机时间容忍度非常低的服务,TimeUDB 提供高可用性(HA)副本。HA副本显著降低了因故障导致的停机和数据丢失风险,并允许服务在常规维护期间避免停机。本节将介绍这些功能各自的工作方式,以帮助您做出明智的决策,选择适合您服务的方案。

 

HA 副本

HA副本是数据库的精确且最新的拷贝,如果主数据库不可用(包括维护期间),它们会自动接管操作。用技术术语来说,HA副本是多可用区、异步热备。它们使用流复制技术以最小化故障转移期间数据丢失的可能性。本节稍后将提供更多关于这些术语的信息。

HA副本还具有一个单独的唯一地址,可用于处理读取请求,但在故障期间,这个只读唯一地址并非高度可用。也就是说,当HA复制的主数据库发生故障,且您的连接自动“故障转移”到之前的HA副本时,该只读唯一地址将不再可访问,直到新的HA副本完全恢复。这种恢复是自动发生的,但其恢复期取决于多个因素,包括数据库大小。

 

维护停机时间

数据库上的一些操作无法避免停机时间,例如升级到 UnvDB 的新主要版本。对于常规维护,如升级到 UnvDB 的新次要版本,可能需要重新启动服务,但这只会在您设置的维护窗口期间发生。

向您的服务添加 HA 副本可避免维护事件期间的停机时间,因为维护是单独应用于每个节点的。例如,可以对您的副本执行维护,而主数据库仍保持运行状态。维护完成后,该副本会被提升为主数据库,然后对其他节点进行维护。

 

故障转移

故障转移是在主数据库无响应后的15秒内,将流量从主数据库重定向到HA副本的过程。作为故障转移的一部分,HA副本会被提升为新的主数据库,并重置连接。在后台,会立即为新的主数据库配置一个新的副本。

故障转移还有助于减少通常会导致服务重置的常见操作的停机时间,如维护事件和服务大小调整。在这些情况下,会依次对每个节点进行更改,以确保始终有一个节点可用。

在正常操作状态下,应用程序连接到主数据库,并可选择连接到其副本。负载均衡器处理连接并定义每个节点的角色。

media/202405/tsc-replication-replicas-normal-state_1715312331.webp

当主数据库发生故障时,平台会更新角色。副本会被提升为主角色,并且主负载均衡器会将流量重定向到新的主数据库。在此期间,系统开始恢复故障节点。在副本恢复完成之前,之前的只读副本连接将保持不可用状态。

media/202405/tsc-replication-replicas-failover-state_1715312431.webp

当故障节点恢复或创建新节点时,该节点将承担副本角色。先前提升为主节点的节点继续保持主节点角色,将WAL(预写日志)流传输到其副本。此时,只读副本连接再次变得可用。

media/202405/tsc-replication-replicas-repaired-state_1715312480.webp

新的副本将在一个新的可用区中创建,以帮助防范可用区停机。

 

快速恢复

默认情况下,所有 TimeUDB 服务都启用了快速恢复功能。由于计算和存储是分开处理的,因此针对不同类型的故障,可以采用不同的方法,而且并不总是需要从备份中恢复。特别是,TimeUDB 服务可以从计算故障中迅速恢复,但通常需要从备份中完全恢复以应对存储故障。

计算故障是数据库故障的最常见原因。计算故障可能由硬件故障引起,也可能由诸如未优化的查询等导致负载增加,进而使 CPU 使用率达到最大。在这些情况下,由于磁盘上的数据未受影响,因此仅需替换计算和内存资源。如果发生此类故障,您的 TimeUDB 服务将立即配置一个新的数据库实例,并将数据库的现有存储挂载到新实例上。然后重播任何之前在内存中的WAL(预写日志)。此过程通常仅需三十秒,但在某些情况下可能需要长达二十分钟,具体取决于需要重播的WAL数量。即使在最坏的情况下,这种恢复也比标准的从备份恢复程序快一个数量级。整个检测和从此类计算故障中恢复的过程是完全自动化的,您无需采取任何操作。

虽然计算故障更为常见,但磁盘硬件也可能发生故障。这种情况很少见,但如果发生,您的 TimeUDB 服务将自动从备份中执行完全恢复。有关备份和恢复的更多信息,请参阅备份部分。

重要提示: 始终尽量避免可能导致CPU使用率最大化的情况。如果您的CPU使用率长时间处于高位,可能会导致一些问题,例如WAL归档被其他进程排队等待,这可能导致故障并可能造成更大的数据丢失。TimeUDB 服务会监控这些类型的场景,以试图在发生故障之前防止数据丢失事件的发生。

 

HA 副本详细信息

HA副本是跨多个可用区(AZ)的、异步的热备份。它们使用流式复制,以最小化故障转移期间的数据丢失风险。本节将更详细地定义这些术语。

 

异步提交

TimeUDB HA副本是异步的。这意味着,一旦事务在本地完成,主数据库就会报告成功。它不会等待确认副本是否也成功提交了该事务。这提高了数据摄取率,并允许您即使在节点发生故障时也能继续向数据库写入数据。

TimeUDB 当前不提供同步副本。

 

热备份

TimeUDB 副本是热备份。这意味着,当主数据库发生故障时,它们已准备好接管。这也意味着,即使主数据库正在运行,您也可以从副本中读取数据。通过分布读取查询,您可以降低主数据库的负载

 

流式复制

为了保持主数据库和副本之间的数据同步,主数据库会将其预写日志(WAL)进行流式传输。WAL记录一旦写入就会立即进行流式传输,而不会等待被批量打包和传输。这降低了数据丢失的风险。

 

读取缩放

只读副本是数据库的只读拷贝,使您能够安全地扩展超出服务限制的范围。您可以根据需要创建任意数量的只读副本。只读副本可以为您的读密集型应用、商业智能工具或两者同时提供支持。每个只读副本都表现为其自己的服务。该副本使用唯一的连接字符串,该字符串与父级和任何HA副本不同。

对只读副本的查询对父级或主要服务的性能影响最小,这使它们成为使用最新生产数据创建隔离实例以进行分析或扩展读取的理想选择。

重要提示: 使用单独的只读副本进行只读访问提供了安全性和资源隔离。这意味着具有只读权限的用户不能直接访问主数据库。如果您需要限制只读用户的访问权限,但又不希望隔离资源,则可以在数据库中创建一个只读角色。有关更多信息,请参阅安全部分。

 

分析

只读副本可以为业务分析师创建一个隔离的环境,以便他们运行繁重的分析查询,而不是在生产实例上运行这些查询,从而避免影响性能的风险。只读副本可以是短期的,分析完成后即删除,也可以长期运行以支持商业智能(BI)工具。

在为分析创建只读副本时,建议您还为使用该副本的人员创建一个新的只读用户。用户必须在主数据库上创建,然后会传播到只读副本。只读副本是只读的,并拥有自己的连接字符串。由于两者的凭据相同,因此拥有一个只读用户可在您不小心使用错误的连接字符串进行连接时提供额外的安全保障。

只读副本还可以拥有与主数据库不同的配置。分析环境可以从更高的 CPU 配置中受益,以处理繁重的查询,或者从更低的 CPU 配置中节省成本,以支持长期运行的仪表盘。

 

关于只读副本

只读副本可用于为应用程序提供读取服务。这减轻了主数据库的负载,并允许主数据库提高数据摄取性能。在读取流量非常不稳定并可能影响数据摄取性能的环境中,或者在读取应该始终比写入具有更低优先级的环境中,这样做尤其有用。

使用这种方法的一个考虑因素是只读副本使用异步复制。这可能会导致父服务出现轻微延迟,在某些情况下这是可以接受的。通过调整 max_standby_streaming_delay 和 max_standby_archive_delay 参数,可以显著减少允许的延迟。不过,不建议您在必须立即反映更改的情况下使用此方法,例如对于用户凭据的更改。

 

高可用副本

HA 副本自动配备一个只读端点,可用于处理读取查询。与只读副本不同,对该端点的查询可能会影响主数据库的性能。主数据库会保留任何可能影响在HA副本上当前执行的查询的WAL(预写日志),这可能导致性能下降。默认情况下,HA副本上的查询超时时间较短,约为30秒,这有助于降低上述风险。这种方法对于简单的读取查询可能是可行的,但更复杂的读取查询可能会面临超时或导致主数据库性能下降的风险。有关更多信息,请参阅高可用性部分。

   

TimeUDB 限制

 

局限性

虽然 TimeUDB 通常提供超出 UnvDB 原生功能的能力,但使用超表(hypertables),特别是分布式超表(distributed hypertables)时,仍存在一些限制。本节记录了使用常规超表和分布式超表时的常见限制。

 

超表限制

  • 不支持引用超表的外键约束。

  • 用于分区的时间维度(列)不能包含NULL值。

  • 唯一索引必须包含所有作为分区维度的列。

  • 不支持在分区(数据块)之间移动值的 UPDATE 语句。这包括 upserts(INSERT…ON CONFLICT UPDATE)。

 

分布式超表限制

常规超表的所有限制也适用于分布式超表。此外,以下限制专门适用于分布式超表:

  • 不支持后台作业的分布式调度。在访问节点上创建的后台作业将在该访问节点上调度和执行,而不会将作业分发到数据节点。

  • 连续聚合可以聚合分布在数据节点上的数据,但连续聚合本身必须位于访问节点上。这可能会对安装的可扩展性造成一定限制,但由于连续聚合是数据的下采样,因此这通常不会造成问题。

  • 不支持重新排序数据块。

  • 无法在访问节点上将表空间附加到分布式超表上。仍然可以在数据节点上附加表空间。

  • 在分布式数据库中,假定角色和权限在节点之间是一致的,但并未强制执行一致性。

  • 不支持数据节点上的连接。将分布式超表与另一个表连接时,要求另一个表位于访问节点上。这也限制了分布式超表上连接的性能。

  • 在分布式超表中,被外键约束引用的表必须存在于访问节点和所有数据节点上。这也适用于被引用的值。

  • 不支持并行感知扫描和追加。

  • 分布式超表本身并不为跨节点的备份和恢复提供一致的恢复点。请使用 create_distributed_restore_point 命令,并确保在将单个备份恢复到访问节点和数据节点时小心处理。

  • 有关原生复制的限制,请参阅原生复制部分。

  • 用户定义的函数必须手动安装在数据节点上,以便在访问节点和数据节点上都可用函数定义。这对于使用 set_integer_now_func 注册的函数尤其重要。

请注意,这些限制涉及从访问节点的使用。某些当前不支持的功能可能仍在单个数据节点上有效,但此类使用既未经过测试也未获得官方支持。未来版本的 TimeUDB 可能会消除其中的一些限制。

   

TimeUDB 故障排除

如果您在使用 TimeUDB 时遇到问题,您可以执行以下操作。本节提供了一些常见错误的解决方案以及输出有关您的设置的诊断信息的方法。

 

常见错误

 

使用第三方 UnvDB 管理工具更新 TimeUDB 时出错

ALTER EXTENSION timeudb UPDATE 命令必须是在连接到数据库后执行的第一个命令。某些管理工具会在此之前执行其他命令,这可能会干扰更新过程。您可能需要使用 ud_sql 手动更新数据库。有关详细信息,请参阅更新文档。

 

计划任务停止运行

您的计划任务可能由于各种原因停止运行。在自托管的 TimeUDB 上,您可以通过重新启动后台工作程序来解决此问题:

SELECT _timeudb_internal.restart_background_workers();

在 TimeUDB 托管服务上,通过执行以下操作之一来重新启动后台工作程序:

  • 运行 SELECT timeudb_pre_restore(),然后运行 SELECT timeudb_post_restore()。

  • 关闭服务并再次打开。这可能会导致几分钟的停机时间,因为服务会从备份中恢复并重放预写日志。

 

无法启动后台工作程序

如果后台工作程序配置不正确,您可能会在日志中看到此错误消息:

"<TYPE_OF_BACKGROUND_JOB>": failed to start a background worker

要解决此错误,请确保正确设置了 max_worker_processes、max_parallel_workers 和 timeudb.max_background_workers 。timeudb.max_background_workers 应等于数据库数量加上并发后台工作程序的数量。max_worker_processes 应等于 timeudb.max_background_workers 与 max_parallel_workers 之和。

 

获取更多信息

 

解释查询性能

UnvDB 的 EXPLAIN 功能允许用户了解 UnvDB 用于执行查询的底层查询计划。UnvDB 可以通过多种方式执行查询:例如,查询可能使用慢速的顺序扫描或更高效的索引扫描来完成。计划的选择取决于表上创建的索引、UnvDB 拥有的关于您的数据的统计信息以及各种计划设置。EXPLAIN 输出可让您知道 UnvDB 为特定查询选择了哪个计划。UnvDB 对此功能有深入的解释说明。

要了解超表上的查询性能,我们建议首先通过运行 VACUUM ANALYZE ;确保超表的计划统计信息和表维护是最新的。然后,我们建议运行以下的 EXPLAIN:

EXPLAIN (ANALYZE on, BUFFERS on) <original query>;

如果您怀疑性能问题是由于磁盘IO缓慢导致的,可以在运行上述EXPLAIN之前,通过设置 SET track_io_timing = ‘on’;来启用 track_io_timing 变量,从而获取更多信息。

 

解释查询性能

 

转储 TimeUDB 元数据

为了帮助在请求支持和报告错误时提供信息,TimeUDB 包含了一个SQL脚本,该脚本可从内部 TimeUDB 表以及版本信息中输出元数据。该脚本在源代码分发版中的 scripts/目录下提供,也可以单独下载。要使用它,请运行:

ud_sql [your connect flags] -d your_timeudb_db < dump_meta_data.sql > dumpfile.txt

然后,在将 dump_file.txt 与错误报告或支持问题一起发送之前,请先检查该文件