具有不同频率的 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 并执行连接的模式具有更多优势并且可能性能更高查询。
要在两种方法之间做出决定,最好考虑将数据插入数据库的应用程序复杂性以及查询数据的方式。
查询无关考虑
几点,与查询性能无关,但值得考虑:
- 在 PostgreSQL 和 TimescaleDB 中存储 NULL 值很便宜,因此在 table 中有 2/3 的数据没有通知就可以了。
- TimescaleDB 不允许 hypertable 之间的引用约束,因此无法控制两个 table 之间
vehicle_id
的完整性约束。而在单身 table 中,这不是问题。
- 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 连接查询方法。
我已经开始了一个 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 并执行连接的模式具有更多优势并且可能性能更高查询。
要在两种方法之间做出决定,最好考虑将数据插入数据库的应用程序复杂性以及查询数据的方式。
查询无关考虑
几点,与查询性能无关,但值得考虑:
- 在 PostgreSQL 和 TimescaleDB 中存储 NULL 值很便宜,因此在 table 中有 2/3 的数据没有通知就可以了。
- TimescaleDB 不允许 hypertable 之间的引用约束,因此无法控制两个 table 之间
vehicle_id
的完整性约束。而在单身 table 中,这不是问题。 - 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 连接查询方法。