TL;DR:与具有两个表并执行连接查询的架构相比,非规范化的单表具有更多优势并且可能性能更高。
要在两种方法之间做出选择,最好考虑将数据插入数据库的应用程序复杂性以及如何查询数据。
查询无关的注意事项
几点,与查询性能无关,但值得考虑:
- 在 PostgreSQL 和 TimescaleDB 中存储 NULL 值很便宜,因此在表中有 2/3 的数据没有通知就可以了。
- TimescaleDB 不允许超表之间的引用约束,因此无法控制两个表之间
vehicle_id 的完整性约束。在单个表中,这不是问题。
-
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)
两个表都排除了块。
因此,可以在规范化架构上高效执行连接查询,但这可能很棘手。请注意,不同数据量的查询计划可能会有所不同。
因此,如果将数据插入到单个非规范化表中并不会给应用程序增加太多复杂性,并且需要同时检索跟踪和通知信息(连接查询)的查询,那么单表方法恕我直言,比使用连接查询的两个表方法更可取。