数十亿行数据的最佳数据库和表设计


74

我正在编写一个需要存储和分析大量电气和温度数据的应用程序。

基本上,我需要存储过去几年以及成千上万个位置以后很多年的每小时小时用电量测量值,然后以一种不太复杂的方式分析数据。

我现在需要存储的信息是位置ID,时间戳(日期和时间),温度和用电量。

关于需要存储的数据量,这是一个近似值,但遵循以下原则:
20000多个位置,每月720条记录(每小时测量,每月大约720小时),120个月(十年前) )以及未来的很多年。简单计算得出以下结果:

20 000个位置x 720条记录x 120个月(10年前)= 1 728 000 000条记录

这些是过去的记录,新记录将每月导入,因此大约每月20000 x 720 = 14400 000新记录

总地点也将稳定增长。

对于所有这些数据,将需要执行以下操作:

  1. 检索某个日期和时间段内的数据:某个特定位置ID的所有记录,这些记录介于日期01.01.2013和01.01.2017之间以及07:00和13:00之间。
  2. 在特定日期和时间范围内进行简单的数学运算,例如,在07:00至13:00之间的5年中,某个位置ID的MIN,MAX和AVG的温度和用电量。

数据将每月写入一次,但会(至少)不断被数百个用户读取,因此读取速度显得尤为重要。

我没有使用NoSQL数据库的经验,但是从我的经验来看,它们是在此处使用的最佳解决方案。我已经阅读了最流行的NoSQL数据库,但是由于它们完全不同,并且还允许非常不同的表体系结构,因此我无法决定使用哪种最佳数据库。

我的主要选择是Cassandra和MongoDB,但由于我的知识非常有限,并且在涉及大数据和NoSQL方面没有实际经验,因此我不确定。我还阅读到PostreSQL也可以很好地处理此类数据。

我的问题如下:

  1. 我是否应该将NoSQL数据库用于如此大量的数据。如果不能,我可以坚持使用MySQL吗?
  2. 我应该使用哪个数据库?
  3. 我应该将日期和时间保留在单独的索引索引(如果可能)列中,以便在特定的时间和日期期限内快速检索和处理数据,还是可以通过将时间戳记保留在单个列中来完成此操作?
  4. 时间序列数据建模方法在这里是否合适,如果不合适,您能否为我提供良好表设计的指导?

谢谢。


29
2017。虽然不小,但是对于适当的硬件来说,这并不是特别庞大的数据量。我不愿意告诉您,但到目前为止,您所拥有的听起来像是关系数据。
TomTom

6
我已经通过使用好键(时期),压缩,分区并确保查询/索引分区对齐来在MS SQL Server 2008-2014中存储了数百亿行的多TB表。当我开始获取PB级数据以进行不同的分析和索引时,我不得不转向NoSQL(Hadoop)。NoSQL应该有其他考虑因素,在这种情况下,它似乎不合适。
阿里·拉泽吉

3
@AliRazeghi Hadoop与SQL或NoSQL无关-它只是一个存储引擎。Hadoop支持大量SQL接口。
mustaccio

3
您在软件/许可上花费的限制是什么?
user3067860

1
如果您有无穷的资金,那么我建议您购买SAP HANA设备。这对于大型数据集的聚合非常有用。但是您可能没有无限的金钱。
菲利普

Answers:


90

这正是我每天要做的事情,除了使用5分钟数据,而不是使用小时数据。我每天下载大约2亿条记录,因此您在这里谈论的数量不是问题。5分钟的数据大约为2 TB,我按位置每小时获取一次的气象数据可以追溯到50年前。因此,让我根据我的经验回答您的问题:

  1. 不要为此使用NoSQL。数据具有高度的结构,非常适合关系数据库。
  2. 我个人使用SQL Server 2016,并且在该数据量上应用计算没有问题。当我开始工作时,它最初是在PostgreSQL实例上,它无法像在小型AWS实例上那样处理大量数据。
  3. 强烈建议提取日期的小时部分并将其与日期本身分开存储。相信我,从我的错误中学习!
  4. 我以列表方式存储大多数数据(DATE,TIME,DATAPOINT_ID,VALUE),但这不是人们想要解释数据的方式。准备好对数据和大量数据透视表进行可怕的查询。不要害怕为太大而无法即时计算的结果集创建非规范化表。

一般提示:我将大多数数据存储在两个数据库之间,第一个是直接时间序列数据并已规范化。我的第二个数据库非常不规范,并包含预先聚合的数据。尽管我的系统速度如此之快,但我什至不会忘记用户甚至不想等待30秒来加载报告这一事实-即使我个人认为30秒来处理2 TB数据也非常快。

要详细说明为什么我建议将小时与日期分开存储,以下是我这样做的一些原因:

  1. 电气数据的显示方式是小时结束–因此,01:00实际上是前一小时的平均电力,而00:00是24小时制。(这很重要,因为您实际上必须搜索两个日期以包括24小时值,即您所选择的日期正在寻找,加上第二天的第一个标记。)但是,天气数据实际上是以向前的方式显示的(实际和接下来一小时的预报)。根据我对这些数据的经验,消费者希望分析天气对电力价格/需求的影响。如果要使用直接日期比较,则实际上是在比较前一个小时的平均价格与下一个小时的平均温度,即使时间戳相同。DATETIME 柱。
  2. 性能。我要说的是,我生成的报告中至少有90%是图表,通常是针对单个日期或一系列日期将价格相对于小时作图。必须从日期中分出时间,这可能会降低要生成报表的查询的速度,具体取决于您要查看的日期范围。消费者通常希望看到过去30年中的单个年份(同比)(实际上,对于天气而言,这是生成30年正常值所必需的),这种情况并不罕见-这可能很慢。当然,您可以优化查询并添加索引,并且相信我,我有一些我不希望拥有的疯狂索引,但是它使系统运行得很快。
  3. 生产率。我讨厌不得不多次编写同一段代码。我曾经将日期和时间存储在同一列中,直到不得不一次又一次地写相同的查询来提取时间部分。一段时间后,我厌倦了必须执行此操作并将其提取到自己的专栏中。您编写的代码越少,出现错误的机会就越少。另外,不必编写更少的代码意味着您可以更快地发布报告,没有人愿意整天等待报告。
  4. 终端用户。并非所有最终用户都是超级用户(即知道如何编写SQL)。数据已经以可以轻松导入Excel(或其他类似工具)的格式存储,将使您成为办公室的英雄。如果用户无法轻松访问或操纵数据,则他们将不会使用您的系统。相信我,几年前我设计了完美的系统,因此没有人使用它。数据库设计不仅要遵循一组预定义的规则/准则,还要使系统可用。

就像我上面说的,这一切都是基于我的个人经验,让我告诉你,经过几年的艰苦努力和大量的重新设计才能达到我现在的状态。不要做我做的事情,从我的错误中吸取教训,并确保在做出有关数据库的决策时让系统的最终用户(或开发人员,报告作者等)参与进来。


我刚刚使用Epoch date感到很幸运,但是您的建议对您的用例很有趣。感谢分享。
阿里·

我最初将日期/时间存储在UTC中,但是后来消费者抱怨了,因为他们总是不得不调整为当地时间。最终,我的设计进行了更改,以使消费者更轻松地使用数据。
布朗斯通先生

4
我不同意很多。正如这里的实际数字所示,这与现代数据库无关。如果数据用户太愚蠢而无法使用sql,则需要为他们创建一个接口-您无需调整架构。提取小时数不是一个好主意
Evan Carroll

1
您的硬件是什么样的?
肯尼斯,

1
这是令人难以置信的硬件,具体取决于您服务的用户数量。由于这是伪优化响应,因此我认为包括您的技术很有用。听到您可以在30秒内压缩2 TB的消息,我感到非常震惊-这是非常快的。除了我自己的个人判断,我认为这对于希望优化时间序列数据的未来人很有用!
Kennes

57

PostgreSQL和BRIN索引

自己测试一下。这对于使用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)

因此创建表花费了22分钟。很大程度上是因为该表只有97GB。接下来,我们创建索引,

CREATE INDEX ON electrothingy USING brin (tsin);
CREATE INDEX ON electrothingy USING brin (id);    
VACUUM ANALYZE electrothingy;

创建索引也花了很长时间。尽管因为它们是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)

带时间戳更新

在这里,我们生成具有不同时间戳的表,以便满足对索引列和在时间戳列上进行搜索的请求,创建时间要长一些,因为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)

现在,我们可以改为对时间戳值运行查询,

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)

结果:

 count |  min  |  max  
-------+-------+-------
 86401 | 97.50 | 97.50
(1 row)

因此,在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

重要的是要注意,尽管可以,但它没有在聚合上使用索引。如果这是您通常的查询,则可能希望date_trunc('hour', tsin)其中存在一个BRIN 的小问题,因为date_trunc它不是一成不变的,因此您必须先将其包装起来。

分区

关于PostgreSQL的另一个重要信息是PG 10带了分区DDL。因此,例如,您可以轻松地每年创建分区。将适度的数据库分解为较小的较小数据库。这样,您应该能够使用并维护btree索引,而不是BRIN索引,因为BRIN索引会更快。

CREATE TABLE electrothingy_y2016 PARTITION OF electrothingy
    FOR VALUES FROM ('2016-01-01') TO ('2017-01-01');

管他呢。


13

令我惊讶的是,这里没有人提到基准测试 -直到@EvanCarroll做出了自己的杰出贡献!

如果我是您,我将花费一些时间(是的,我知道这是一种宝贵的商品!)来建立系统,运行您认为会的内容(在此处获得最终用户的输入!),例如10个最常见的查询。

我自己的想法:

NoSQL解决方案在特定的用例中可以很好地工作,但对于即席查询通常不灵活。有关MySQL前首席架构师Brian Aker对NoSQL的有趣评论,请参见此处

我同意@Brownstone先生的观点,即您的数据非常适合关系解决方案(并且该观点已被Evan Carroll证实)!

如果我要承担任何支出,那将是我的磁盘技术!我将花我所有的钱花在NAS或SAN上,或者花些SSD磁盘来保存我很少写入的聚合数据!

首先,我将看看我现在有什么。运行一些测试,并将结果显示给决策者。您已经以EC的工作形式获得了代理!但是,在您自己的硬件上进行一两次快速测试会更有说服力!

然后考虑花钱!如果您要花钱,请先关注硬件而不是软件。AFAIK,您可以在试用期内租用磁盘技术,或者更好的是,在云上启动几个概念验证。

对于这样的项目,我个人的第一呼叫口就是PostgreSQL。这并不是说我会排除专有解决方案,但是每个人的物理定律和磁盘定律都是相同的!“ Yae cannae甜菜的物理定律吉姆” :-)


6

如果还没有,请看一下时间序列DBMS,因为它是为存储和查询主要针对日期/时间类型的数据而优化的。通常,时间序列数据库用于记录分钟/秒/亚秒范围内的数据,因此我不确定它是否仍然适合小时增量。也就是说,这种类型的DBMS似乎值得研究。当前,InfluxDB似乎是最成熟且使用最广泛的时间序列数据库。


1
时间序列DBMS的示例是什么?
主教

2
在这里看看。
Vérace

4

显然,这不是NoSQL问题,但是我建议,尽管RDBMS解决方案可以工作,但我认为OLAP方法会更好,并且鉴于涉及的数据范围非常有限,我强烈建议调查基于列的数据库的使用而不是基于行。以这种方式考虑,您可能有17亿条数据,但是您仍然只需要5位就可以索引小时或月中的每个可能值。

我在类似的问题域中也有经验,在该领域中,Sybase IQ(现为SAP IQ)用于每小时存储多达3亿个计数器的电信设备性能管理数据,但是我怀疑您是否有用于此类解决方案的预算。在开放源代码领域,MariaDB ColumnStore是非常有前途的候选人,但我建议您还应调查MonetDB。

由于查询性能是您的主要驱动力,因此请考虑如何用短语表达查询。这是OLAP和RDBMS表现出最大差异的地方:-使用OLAP,您可以标准化查询性能,而不是减少重复,减少存储甚至增强一致性。因此,除了原始时间戳(我希望您还记得捕获其时区?)之外,还有一个单独的UTC时间戳字段,其他字段用于日期和时间,还有更多字段用于年,月,日,小时,分钟。和UTC偏移量。如果您还有其他有关位置的信息,可以随时将其保存在一个单独的位置表中,可以按需查找该表,并可以将该表的关键字保留在主记录中,但可以将完整的位置名保留在主表中,例如好吧,毕竟

作为最后的建议,对流行的聚合数据使用单独的表,并使用批处理作业填充它们,这样您就不必为每个使用聚合值并进行将当前值与历史值或历史值进行比较的查询的报告重复该练习。从历史到历史要容易得多,而且要快得多。


如果您正在寻找的话,您可能还会将Greenplum视为柱状商店!作为“奖励”-它基于PostgreSQL!
Vérace

我在HP Vertica方面拥有丰富的经验。我们有一个包含9列的单个表,该表具有1300亿行,而无需进行大量调整。它只是工作。
ThatDataGuy
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.