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

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 条新记录

总位置也将稳步增长。

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

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

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

我没有使用 NoSQL 数据库的经验,但从我收集到的信息来看,它们是此处使用的最佳解决方案。我已经阅读了最流行的 NoSQL 数据库,但由于它们完全不同,并且还允许非常不同的表架构,我一直无法决定使用什么是最好的数据库。

我的主要选择是 Cassandra 和 MongoDB,但由于我的知识非常有限,而且在大数据和 NoSQL 方面没有实际经验,我不太确定。我还读到 PostreSQL 也能很好地处理如此大量的数据。

我的问题如下:

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

谢谢你。

Wor*_*DBA 117

这正是我每天所做的,除了使用 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(或其他类似工具)的格式存储,将使您成为办公室中的英雄。如果用户无法轻松访问或操作数据,他们将不会使用您的系统。相信我,几年前我设计了完美的系统,因为这个原因没有人使用它。数据库设计不仅仅是要遵守一组预定义的规则/准则,还要使系统可用。

正如我上面所说,这一切都基于我的个人经验,让我告诉你,我经历了艰难的几年和大量的重新设计才达到现在的位置。不要像我做的那样,从我的错误中吸取教训,并确保在做出有关数据库的决策时让系统的最终用户(或开发人员、报告作者等)参与进来。

  • 我不同意很多。对于现代数据库而言,这些都不是真正的问题[如此处的实际数字所示](https://dba.stackexchange.com/a/188681/2639)。如果数据的用户太愚蠢而无法使用 sql,那么您需要为他们创建一个接口——您不要修改架构。**提取小时是个坏主意** (6认同)
  • @EvanCarrollQWERHJKL 很抱歉你有这样的感觉,几年前我会同意你的观点。你的答案是一个很好的答案,但其中包含的查询与我每天必须运行的一些查询并不接近,我的答案是基于我每天使用OP指定的确切数据集所做的工作。 (2认同)
  • 你的硬件是什么样的? (2认同)
  • @kennes 物理、16 核、256GB RAM、100GB 操作系统驱动器、带有 TempDB 数据的 500GB 本地 SSD、带有 8TB SSD 缓存的混合 SAN 和能够实现 100,000 iops/秒的 40TB 主轴磁盘。数据库实现使用列存储、压缩、内存表、分区和表格 SSAS 实例。 (2认同)
  • 这是令人难以置信的硬件,具体取决于您服务的用户数量。由于这是一个伪优化响应,我认为包括您的技术是有用的。当我听说您可以在 30 秒内处理 2TB 数据时,我感到非常震惊——这速度快得令人难以置信。抛开我个人的判断不谈,我认为这对于未来寻求优化时间序列数据的人来说是有用的! (2认同)

Eva*_*oll 74

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)
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 似乎是最成熟和使用最广泛的时间序列数据库。

  • 看看[这里](https://blog.outlyer.com/top10-open-source-time-series-databases)。 (2认同)

小智 6

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

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

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

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