Gec*_*ata 104 nosql database-design database-recommendation
我正在编写一个需要存储和分析大量电气和温度数据的应用程序。
基本上,我需要存储过去几年和未来数万个地点的大量每小时用电量测量值,然后以不太复杂的方式分析数据。
我需要(目前)存储的信息是位置 ID、时间戳(日期和时间)、温度和电力使用情况。
关于需要存储的数据量,这是一个近似值,但大致如下:
20 000 多个位置,每月 720 条记录(每小时测量,每月大约 720 小时),120 个月(10 年前) ) 和未来许多年。简单的计算得出以下结果:
20 000 个位置 x 720 条记录 x 120 个月(10 年前)= 1 728 000 000 条记录。
这些是过去的记录,新记录将每月导入,因此每月大约有 20 000 x 720 = 14 400 000 条新记录。
总位置也将稳步增长。
对于所有这些数据,需要执行以下操作:
数据将每月写入一次,但会被数百名用户(至少)不断读取,因此读取速度更为重要。
我没有使用 NoSQL 数据库的经验,但从我收集到的信息来看,它们是此处使用的最佳解决方案。我已经阅读了最流行的 NoSQL 数据库,但由于它们完全不同,并且还允许非常不同的表架构,我一直无法决定使用什么是最好的数据库。
我的主要选择是 Cassandra 和 MongoDB,但由于我的知识非常有限,而且在大数据和 NoSQL 方面没有实际经验,我不太确定。我还读到 PostreSQL 也能很好地处理如此大量的数据。
我的问题如下:
谢谢你。
Wor*_*DBA 117
这正是我每天所做的,除了使用 5 分钟数据而不是使用每小时数据。我每天下载大约2亿条记录,所以你在这里谈论的数量不是问题。5 分钟的数据大小约为 2 TB,我有按位置每小时可追溯到 50 年前的天气数据。所以,让我根据我的经验回答你的问题:
一般提示:我将大部分数据存储在两个数据库之间,第一个是直截了当的时间序列数据并进行了规范化。我的第二个数据库非常非规范化,包含预先聚合的数据。与我的系统一样快,我并没有忽视用户甚至不想等待 30 秒来加载报告的事实——即使我个人认为 30 秒来处理 2 TB 数据是非常快的。
要详细说明为什么我建议将小时与日期分开存储,以下是我这样做的几个原因:
DATETIME 柱子。 正如我上面所说,这一切都基于我的个人经验,让我告诉你,我经历了艰难的几年和大量的重新设计才达到现在的位置。不要像我做的那样,从我的错误中吸取教训,并确保在做出有关数据库的决策时让系统的最终用户(或开发人员、报告作者等)参与进来。
Eva*_*oll 74
自己测试一下。这在带有 ssd 的 5 年旧笔记本电脑上不是问题。
EXPLAIN ANALYZE
CREATE TABLE electrothingy
AS
SELECT
x::int AS id,
(x::int % 20000)::int AS locid, -- fake location ids in the range of 1-20000
now() AS tsin, -- static timestmap
97.5::numeric(5,2) AS temp, -- static temp
x::int AS usage -- usage the same as id not sure what we want here.
FROM generate_series(1,1728000000) -- for 1.7 billion rows
AS gs(x);
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
Function Scan on generate_series gs (cost=0.00..15.00 rows=1000 width=4) (actual time=173119.796..750391.668 rows=1728000000 loops=1)
Planning time: 0.099 ms
Execution time: 1343954.446 ms
(3 rows)
Run Code Online (Sandbox Code Playgroud)
所以创建表花了 22 分钟。主要是因为该表只有 97GB。接下来我们创建索引,
CREATE INDEX ON electrothingy USING brin (tsin);
CREATE INDEX ON electrothingy USING brin (id);
VACUUM ANALYZE electrothingy;
Run Code Online (Sandbox Code Playgroud)
创建索引也花了很长时间。虽然因为它们是 BRIN,所以它们只有 2-3 MB,并且它们很容易存储在 ram 中。读取 96 GB 不是即时的,但在您的工作负载下,这对我的笔记本电脑来说不是真正的问题。
现在我们查询它。
explain analyze
SELECT max(temp)
FROM electrothingy
WHERE id BETWEEN 1000000 AND 1001000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=5245.22..5245.23 rows=1 width=7) (actual time=42.317..42.317 rows=1 loops=1)
-> Bitmap Heap Scan on electrothingy (cost=1282.17..5242.73 rows=993 width=7) (actual time=40.619..42.158 rows=1001 loops=1)
Recheck Cond: ((id >= 1000000) AND (id <= 1001000))
Rows Removed by Index Recheck: 16407
Heap Blocks: lossy=128
-> Bitmap Index Scan on electrothingy_id_idx (cost=0.00..1281.93 rows=993 width=0) (actual time=39.769..39.769 rows=1280 loops=1)
Index Cond: ((id >= 1000000) AND (id <= 1001000))
Planning time: 0.238 ms
Execution time: 42.373 ms
(9 rows)
Run Code Online (Sandbox Code Playgroud)
这里我们生成一个具有不同时间戳的表,以满足对时间戳列进行索引和搜索的请求,创建需要更长的时间,因为to_timestamp(int)它比now()(为事务缓存的)慢得多
EXPLAIN ANALYZE
CREATE TABLE electrothingy
AS
SELECT
x::int AS id,
(x::int % 20000)::int AS locid,
-- here we use to_timestamp rather than now(), we
-- this calculates seconds since epoch using the gs(x) as the offset
to_timestamp(x::int) AS tsin,
97.5::numeric(5,2) AS temp,
x::int AS usage
FROM generate_series(1,1728000000)
AS gs(x);
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
Function Scan on generate_series gs (cost=0.00..17.50 rows=1000 width=4) (actual time=176163.107..5891430.759 rows=1728000000 loops=1)
Planning time: 0.607 ms
Execution time: 7147449.908 ms
(3 rows)
Run Code Online (Sandbox Code Playgroud)
现在我们可以对时间戳值运行查询,
explain analyze
SELECT count(*), min(temp), max(temp)
FROM electrothingy WHERE tsin BETWEEN '1974-01-01' AND '1974-01-02';
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=296073.83..296073.84 rows=1 width=7) (actual time=83.243..83.243 rows=1 loops=1)
-> Bitmap Heap Scan on electrothingy (cost=2460.86..295490.76 rows=77743 width=7) (actual time=41.466..59.442 rows=86401 loops=1)
Recheck Cond: ((tsin >= '1974-01-01 00:00:00-06'::timestamp with time zone) AND (tsin <= '1974-01-02 00:00:00-06'::timestamp with time zone))
Rows Removed by Index Recheck: 18047
Heap Blocks: lossy=768
-> Bitmap Index Scan on electrothingy_tsin_idx (cost=0.00..2441.43 rows=77743 width=0) (actual time=40.217..40.217 rows=7680 loops=1)
Index Cond: ((tsin >= '1974-01-01 00:00:00-06'::timestamp with time zone) AND (tsin <= '1974-01-02 00:00:00-06'::timestamp with time zone))
Planning time: 0.140 ms
Execution time: 83.321 ms
(9 rows)
Run Code Online (Sandbox Code Playgroud)
结果:
count | min | max
-------+-------+-------
86401 | 97.50 | 97.50
(1 row)
Run Code Online (Sandbox Code Playgroud)
因此,在 83.321 毫秒内,我们可以在具有 17 亿行的表中聚合 86,401 条记录。那应该是合理的。
计算小时结束也很容易,将时间戳截断,然后简单地添加一个小时。
SELECT date_trunc('hour', tsin) + '1 hour' AS tsin,
count(*),
min(temp),
max(temp)
FROM electrothingy
WHERE tsin >= '1974-01-01'
AND tsin < '1974-01-02'
GROUP BY date_trunc('hour', tsin)
ORDER BY 1;
tsin | count | min | max
------------------------+-------+-------+-------
1974-01-01 01:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 02:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 03:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 04:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 05:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 06:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 07:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 08:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 09:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 10:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 11:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 12:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 13:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 14:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 15:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 16:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 17:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 18:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 19:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 20:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 21:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 22:00:00-06 | 3600 | 97.50 | 97.50
1974-01-01 23:00:00-06 | 3600 | 97.50 | 97.50
1974-01-02 00:00:00-06 | 3600 | 97.50 | 97.50
(24 rows)
Time: 116.695 ms
Run Code Online (Sandbox Code Playgroud)
重要的是要注意,它没有在聚合上使用索引,尽管它可以。如果这是您的典型查询,您可能希望在date_trunc('hour', tsin)其中使用 BRIN 存在一个小问题,因为date_trunc它不是一成不变的,因此您必须首先将其包装起来。
关于 PostgreSQL 的另一个重要信息是 PG 10 带来了分区 DDL。因此,例如,您可以轻松地为每年创建分区。将您的普通数据库分解为很小的次要数据库。这样做时,您应该能够使用和维护 btree 索引,而不是 BRIN,后者会更快。
CREATE TABLE electrothingy_y2016 PARTITION OF electrothingy
FOR VALUES FROM ('2016-01-01') TO ('2017-01-01');
Run Code Online (Sandbox Code Playgroud)
管他呢。
Vér*_*ace 14
令我惊讶的是,这里没有人提到基准测试- 直到@EvanCarroll 做出出色的贡献!
如果我是你,我会花一些时间(是的,我知道这是一种宝贵的商品!)设置系统,运行你认为将要运行的东西(在这里获取最终用户的输入!),比如你的 10 个最常见的查询。
我个人的想法:
NoSQL 解决方案对于特定用例可以很好地工作,但对于即席查询通常不灵活。有关 MySQL 前首席架构师 Brian Aker 对 NoSQL 的有趣看法,请参见此处!
我同意 @Mr.Brownstone 的观点,即您的数据非常适合关系解决方案(Evan Carroll已经证实了这一观点)!
如果我要承担任何支出,那就是我的磁盘技术!我会在 NAS 或 SAN 或一些 SSD 磁盘上花费我可以支配的任何钱来保存我很少写入的聚合数据!
首先我会看看我现在有什么。运行一些测试并向决策者展示结果。您已经拥有EC 工作形式的代理!但是,在您自己的硬件上进行一两个快速测试会更有说服力!
那就想想花钱吧!如果您要花钱,请先看硬件而不是软件。AFAIK,您可以在试用期内租用磁盘技术,或者更好的是,在云上启动几个概念验证。
对于这样的项目,我个人的第一个调用端口是 PostgreSQL。这并不是说我会排除专有解决方案,但物理定律和磁盘定律对每个人都是一样的!“Yae cannae 遵循物理定律吉姆”:-)
小智 7
如果您还没有,请查看时间序列 DBMS,因为它针对存储和查询主要关注日期/时间类型的数据进行了优化。通常时间序列数据库用于记录分钟/秒/亚秒范围内的数据,所以我不确定它是否仍然适用于每小时增量。也就是说,这种类型的 DBMS 似乎值得研究。目前 InfluxDB 似乎是最成熟和使用最广泛的时间序列数据库。
小智 6
显然这不是 NoSQL 问题,但我建议虽然 RDBMS 解决方案可以工作,但我认为 OLAP 方法会更适合,并且考虑到所涉及的数据范围非常有限,我强烈建议调查基于列的数据库的使用而不是基于行的。这样想一想,您可能有 17 亿条数据,但您仍然只需要 5 位来索引每个可能的小时或月日值。
我在类似的问题领域有经验,其中 Sybase IQ(现在是 SAP IQ)用于每小时存储多达 3 亿个计数器的电信设备性能管理数据,但我怀疑您是否有这种解决方案的预算。在开源领域,MariaDB ColumnStore 是一个非常有前途的候选者,但我也建议调查 MonetDB。
由于查询性能是您的主要驱动因素,因此请考虑如何表达查询。这就是 OLAP 和 RDBMS 显示出它们最大区别的地方:- 使用 OLAP,您可以标准化查询性能,而不是减少重复、减少存储甚至强制一致性。所以除了原始时间戳(我希望你记得捕获它的时区?)有一个单独的字段用于 UTC 时间戳,其他字段用于日期和时间,还有更多用于年、月、日、小时、分钟和 UTC 偏移量。如果您有关于位置的其他信息,请随时将其保存在可以按需查找的单独位置表中,并随意将那个表的键保存在您的主记录中,但将完整的位置名称保存在您的主表中毕竟,
作为最后的建议,对流行的聚合数据使用单独的表并使用批处理作业来填充它们,这样您就不必为使用聚合值的每个报告重复练习,并进行比较当前与历史或历史的查询。从历史到历史要容易得多,而且要快得多。