【问题标题】:Timescaledb design timeseries with different frequencyTimescaledb 设计不同频率的时间序列
【发布时间】:2021-09-07 15:12:50
【问题描述】:

我已经开始了一个 TimescaleDB 表设计,该表设计已经在开发环境中工作了一段时间。我现在想知道这是否是正确的设计。

基本上,我们每 15 秒记录一次车辆的时间序列位置测量值。我们有一个由几辆车组成的车队。大约 1/3 的测量值会触发通知,我们会记录有关通知的数据。

目前,由于频率不同,我将跟踪测量和通知分开使用 2 个分开的表格。

create table tracking(
  time timestamptz NOT NULL,
  vehicle_id int NOT NULL,
  latitude float NOT NULL,
  longitude float NOT NULL
)

create table notification(
  time timestamptz NOT NULL,
  vehicle_id int NOT NULL,
  content jsonb NOT NULL
)

我正在考虑将这两个表合并回来,以便我们可以在获取跟踪数据的同时获取通知内容,但查询的速度让我感到惊讶:

SELECT t.*, n.content FROM tracking t LEFT JOIN notification n ON (n.time = t.time AND t.vehicle_id = n.vehicle_id)

所以我想知道我的原始设计是否确实是正确的,或者我是否应该合并这两个表并最终得到:

create table tracking(
  time timestamptz NOT NULL,
  vehicle_id int NOT NULL,
  latitude float NOT NULL,
  longitude float NOT NULL,
  notification_content JSONB NULL
)

底线,如果时间序列数据的 1/3 与冗长的 json 内容相关联,您是否会将时间序列数据拆分到不同的表中。

【问题讨论】:

  • 您能否发布 EXPLAIN ANALYZE with BUFFER ON 以供您查询?了解查询的性能问题会大有帮助。
  • @k_rus 没有性能问题。我以为会有。我想知道数据库设计是否正确,我的原始设计似乎没有性能损失,只是想知道这通常是否是正确的方法。
  • 感谢您的澄清。我将您的惊讶解释为错误的方向 :) 查询计划仍然可以显示很少的细节,这可能会在将来有所帮助,例如,块排除。有一个关于规范化的类似问题及其答案:stackoverflow.com/a/67202335/840340如果您将来使用它,拥有两个表可能是连续聚合的问题。

标签: database-design timescaledb


【解决方案1】:

TL;DR:与具有两个表并执行连接查询的架构相比,非规范化的单表具有更多优势并且可能性能更高。

要在两种方法之间做出选择,最好考虑将数据插入数据库的应用程序复杂性以及如何查询数据。

查询无关的注意事项

几点,与查询性能无关,但值得考虑:

  1. 在 PostgreSQL 和 TimescaleDB 中存储 NULL 值很便宜,因此在表中有 2/3 的数据没有通知就可以了。
  2. TimescaleDB 不允许超表之间的引用约束,因此无法控制两个表之间vehicle_id 的完整性约束。在单个表中,这不是问题。
  3. Continuous aggregates 不支持连接,因此如果将来考虑连续聚合,单表是一个优势。

查询规划和潜在性能

TimescaleDB 实现了重要的优化,以在查询是准时条件时对大量数据执行查询 - chunk exclusion,它不包括在查询计划时不满足准时条件的块。下面正在调查问题中查询示例的块排除。

为了调查查询计划,我创建了一个超表、索引并插入了少量数据:

SELECT create_hypertable('tracking','time');
SELECT create_hypertable('notification','time');

INSERT INTO tracking VALUES ('2020-02-03', 1, 1.0, 3.2),('2020-02-04', 1, 1.0, 3.2),('2020-02-03', 2, 1.0, 3.2),('2020-02-05', 2, 1.0, 3.2),('2020-02-06', 1, 1.0, 3.2);
INSERT INTO notification VALUES ('2020-02-03', 1, '{"note":"some"}'),('2020-02-05', 2, '{"note":"some"}');
INSERT INTO tracking VALUES ('2020-03-03', 1, 1.0, 3.2),('2020-03-04', 1, 1.0, 3.2),('2020-03-03', 2, 1.0, 3.2),('2020-03-05', 2, 1.0, 3.2),('2020-03-06', 1, 1.0, 3.2);
INSERT INTO notification VALUES ('2020-03-03', 1, '{"note":"some"}'),('2020-03-05', 2, '{"note":"some"}');

CREATE INDEX tracking_vt ON tracking (vehicle_id, time);
CREATE INDEX tracking_vt ON notification (vehicle_id, time);

由于超表是使用默认块大小(即 7 天)创建的,因此数据被插入到以下块中:

SELECT hypertable_name, chunk_name, range_start, range_end FROM timescaledb_information.chunks;
 hypertable_name |    chunk_name     |      range_start       |       range_end
-----------------+-------------------+------------------------+------------------------
 tracking        | _hyper_3_8_chunk  | 2020-01-30 01:00:00+01 | 2020-02-06 01:00:00+01
 notification    | _hyper_4_9_chunk  | 2020-01-30 01:00:00+01 | 2020-02-06 01:00:00+01
 tracking        | _hyper_3_10_chunk | 2020-02-27 01:00:00+01 | 2020-03-05 01:00:00+01
 tracking        | _hyper_3_11_chunk | 2020-03-05 01:00:00+01 | 2020-03-12 01:00:00+01
 notification    | _hyper_4_12_chunk | 2020-02-27 01:00:00+01 | 2020-03-05 01:00:00+01
(5 rows)

现在让我们获取查询的解释计划:

EXPLAIN ANALYZE SELECT t.*, n.content FROM tracking t LEFT JOIN notification n ON (n.time = t.time AND t.vehicle_id = n.vehicle_id);
                                                            QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
 Hash Left Join  (cost=2.12..5.35 rows=10 width=60) (actual time=0.051..0.069 rows=10 loops=1)
   Hash Cond: ((t.vehicle_id = n.vehicle_id) AND (t."time" = n."time"))
   ->  Append  (cost=0.00..3.15 rows=10 width=28) (actual time=0.011..0.022 rows=10 loops=1)
         ->  Seq Scan on _hyper_3_8_chunk t  (cost=0.00..1.05 rows=5 width=28) (actual time=0.010..0.011 rows=5 loops=1)
         ->  Seq Scan on _hyper_3_10_chunk t_1  (cost=0.00..1.04 rows=4 width=28) (actual time=0.004..0.005 rows=4 loops=1)
         ->  Seq Scan on _hyper_3_11_chunk t_2  (cost=0.00..1.01 rows=1 width=28) (actual time=0.004..0.004 rows=1 loops=1)
   ->  Hash  (cost=2.06..2.06 rows=4 width=44) (actual time=0.026..0.026 rows=4 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 9kB
         ->  Append  (cost=0.00..2.06 rows=4 width=44) (actual time=0.007..0.014 rows=4 loops=1)
               ->  Seq Scan on _hyper_4_9_chunk n  (cost=0.00..1.02 rows=2 width=44) (actual time=0.006..0.007 rows=2 loops=1)
               ->  Seq Scan on _hyper_4_12_chunk n_1  (cost=0.00..1.02 rows=2 width=44) (actual time=0.005..0.006 rows=2 loops=1)
 Planning Time: 1.881 ms
 Execution Time: 0.117 ms
(13 rows)

按时无条件查询并不常见,因为在大型数据集上要检索的数据量将是巨大的。因此,我添加了一个 WHERE 子句,其条件是准时只检索一半的数据:

EXPLAIN ANALYZE SELECT t.*, n.content FROM tracking t LEFT JOIN notification n ON (n.time = t.time AND t.vehicle_id = n.vehicle_id) WHERE t.time > '2020-02-20';
                                                            QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
 Hash Right Join  (cost=2.10..4.20 rows=2 width=60) (actual time=0.099..0.106 rows=5 loops=1)
   Hash Cond: ((n.vehicle_id = t.vehicle_id) AND (n."time" = t."time"))
   ->  Append  (cost=0.00..2.06 rows=4 width=44) (actual time=0.005..0.011 rows=4 loops=1)
         ->  Seq Scan on _hyper_4_9_chunk n  (cost=0.00..1.02 rows=2 width=44) (actual time=0.004..0.005 rows=2 loops=1)
         ->  Seq Scan on _hyper_4_12_chunk n_1  (cost=0.00..1.02 rows=2 width=44) (actual time=0.004..0.005 rows=2 loops=1)
   ->  Hash  (cost=2.07..2.07 rows=2 width=28) (actual time=0.044..0.044 rows=5 loops=1)
         Buckets: 1024  Batches: 1  Memory Usage: 9kB
         ->  Append  (cost=0.00..2.07 rows=2 width=28) (actual time=0.011..0.019 rows=5 loops=1)
               ->  Seq Scan on _hyper_3_10_chunk t  (cost=0.00..1.05 rows=1 width=28) (actual time=0.011..0.012 rows=4 loops=1)
                     Filter: ("time" > '2020-02-20 00:00:00+01'::timestamp with time zone)
               ->  Seq Scan on _hyper_3_11_chunk t_1  (cost=0.00..1.01 rows=1 width=28) (actual time=0.005..0.005 rows=1 loops=1)
                     Filter: ("time" > '2020-02-20 00:00:00+01'::timestamp with time zone)
 Planning Time: 4.546 ms
 Execution Time: 0.157 ms
(14 rows)

请注意,从tracking 读取的块数是 3 分之二,notification 是 2 分之二。这意味着chunk exclusion 是在tracking 上完成的,但没有从notification 中排除任何块。如果 TimescaleDB 的查询计划器错过了排除块,这可能是大型数据集上的严重性能问题。

我调整了查询​​以限制针对notification 而不是tracking 的时间,结果更好:

EXPLAIN ANALYZE SELECT t.*, n.content FROM tracking t LEFT JOIN notification n ON (n.time = t.time AND t.vehicle_id = n.vehicle_id) WHERE n.time > '2020-02-20';
                                                         QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.00..3.13 rows=1 width=60) (actual time=0.019..0.037 rows=2 loops=1)
   Join Filter: ((t."time" = n."time") AND (t.vehicle_id = n.vehicle_id))
   Rows Removed by Join Filter: 8
   ->  Seq Scan on _hyper_4_12_chunk n  (cost=0.00..1.02 rows=1 width=44) (actual time=0.012..0.013 rows=2 loops=1)
         Filter: ("time" > '2020-02-20 00:00:00+01'::timestamp with time zone)
   ->  Append  (cost=0.00..2.07 rows=2 width=28) (actual time=0.003..0.007 rows=5 loops=2)
         ->  Seq Scan on _hyper_3_10_chunk t  (cost=0.00..1.05 rows=1 width=28) (actual time=0.003..0.004 rows=4 loops=2)
               Filter: ("time" > '2020-02-20 00:00:00+01'::timestamp with time zone)
         ->  Seq Scan on _hyper_3_11_chunk t_1  (cost=0.00..1.01 rows=1 width=28) (actual time=0.002..0.003 rows=1 loops=2)
               Filter: ("time" > '2020-02-20 00:00:00+01'::timestamp with time zone)
 Planning Time: 1.161 ms
 Execution Time: 0.071 ms
(12 rows)

两个表都排除了块。

因此,可以在规范化架构上高效执行连接查询,但这可能很棘手。请注意,不同数据量的查询计划可能会有所不同。

因此,如果将数据插入到单个非规范化表中并不会给应用程序增加太多复杂性,并且需要同时检索跟踪和通知信息(连接查询)的查询,那么单表方法恕我直言,比使用连接查询的两个表方法更可取。

【讨论】:

  • 你的观点很好。 NULL 值的存储很有趣。不支持加入的连续聚合是我决定的。我最终重构了它。非常感谢
  • @Sylvain 谢谢你的评论!我根据它重新安排了我的答案,因此它可以对其他开发人员有所帮助。
猜你喜欢
  • 2021-08-07
  • 2020-11-09
  • 2020-10-13
  • 2014-11-28
  • 1970-01-01
  • 2021-03-29
  • 2019-03-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多