具有不同频率的 Timescaledb 设计时间序列

Timescaledb design timeseries with different frequency

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

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

目前,因为频率不同,我将跟踪测量和通知分开 tables 使用相同的时间。

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
)

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

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

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

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 内容相关联,您是否会将时间序列数据拆分为不同的 tables。

TL;DR: 非规范化单个 table 比具有两个 table 并执行连接的模式具有更多优势并且可能性能更高查询。

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

查询无关考虑

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

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

查询规划和潜在性能

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

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

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);

由于 hypertables 是使用默认块大小创建的,即 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 个中的 2 个,notification 中的 2 个是 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)

两个 table 都排除了块。

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

因此,如果将数据插入单个非规范化 table 和检索跟踪和通知信息的查询(连接查询)不会给应用程序增加太多复杂性) 是预期的,那么单个 table 方法是,恕我直言,优于两个 tables 连接查询方法。