【问题标题】:Efficient storage pattern for millions of values of different types数百万不同类型值的高效存储模式
【发布时间】:2019-07-04 17:55:44
【问题描述】:

我即将构建一个 SQL 数据库,该数据库将包含数十万个对象的统计计算结果。计划使用 Postgres,但问题同样适用于 MySQL。

例如,假设我有 50 万条电话记录。现在,每个PhoneCall 都将通过后台作业系统计算统计信息。例如,PhoneCall 具有以下统计信息:

  • call_duration:以秒为单位(浮点数)
  • setup_time:以秒为单位(浮点数)
  • dropouts:检测到音频丢失的时段(数组),例如[5.23, 40.92]
  • hung_up_unexpectedly: true or false (boolean)

这些只是简单的例子;实际上,统计数据更为复杂。每个统计信息都有一个与之关联的版本号。

我不确定这些类型的计算数据的哪种存储模式最有效。不过,我并没有考虑完全规范化数据库中的所有内容。到目前为止,我提出了以下选项:

选项 1 – 一列中的长格式

我将统计名称及其值分别存储在一列中,并引用主事务对象。值列是一个文本字段;该值将被序列化(例如 JSON 或 YAML),以便可以存储不同的类型(字符串、数组等)。统计表的数据库布局为:

  • statistic_id (PK)
  • phone_call_id (FK)
  • statistic_name(字符串)
  • statistic_value(文本,序列化)
  • statistic_version(整数)
  • created_at(日期时间)

我已经使用这种模式有一段时间了,它的好处是我可以轻松地根据电话和统计名称过滤统计信息。我还可以轻松添加新类型的统计信息,并按版本和创建时间进行过滤。

但在我看来,值的(反)序列化使其在处理大量数据方面效率很低。此外,我无法在 SQL 级别执行计算;我总是必须加载和反序列化数据。还是 Postgres 中的 JSON 支持那么好,所以我仍然可以选择这种模式?

选项 2 – 统计作为主要对象的属性

我还可以考虑收集所有类型的统计名称并将它们作为新列添加到电话对象中,例如:

  • id (PK)
  • call_duration
  • setup_time
  • dropouts
  • hung_up_unexpectedly
  • ...

这将非常有效,并且每一列都有自己的类型,但我不能再存储不同版本的统计信息,或者根据它们的创建时间过滤它们。统计的整个业务逻辑消失了。添加新的统计数据也不是一件容易的事,因为名称已经包含在内。

选项 3 – 不同列的统计数据

这可能是最复杂的。我只存储对统计类型的引用,并且将根据该列查找该列:

  • statistic_id (PK)
  • phone_call_id (FK)
  • statistic_name(字符串)
  • statistic_value_bool(布尔值)
  • statistic_value_string(字符串)
  • statistic_value_float(浮点数)
  • statistic_value_complex(序列化或复杂数据类型)
  • statistic_value_type(表示boolstring等的字符串)
  • statistic_version(整数)
  • created_at(日期时间)

这意味着该表将非常稀疏,因为只会填充statistic_value_ 列之一。这会导致性能问题吗?

选项 4 – 标准化形式

尝试规范化选项 3,我将创建两个表:

  • statistics
    • id (PK)
    • version
    • created_at
  • statistic_mapping
    • phone_call_id (FK)
    • statistic_id (FK)
  • statistic_type_mapping
    • statistic_id (FK)
    • type(字符串,表示boolstring等)
  • statistic_values_boolean
    • statistic_id (FK)
    • value(布尔)

但这不会发生,因为我不能动态加入另一个表名,可以吗?或者我是否应该根据统计 ID 加入所有 statistic_values_* 表?我的应用程序必须确保不存在重复的条目。

总而言之,给定这个用例,当要求可以添加或更改统计类型并且需要多个版本时,在关系数据库(例如 Postgres)中存储数百万个统计值的最有效方法是什么同时存在,那么查询值应该有点效率?

【问题讨论】:

  • (我也愿意接受以下建议:将所有内容转储到 NoSQL DB 或某个集群中。)
  • 我在这里遗漏了一件关键的事情,即将对这些数据执行哪些操作(如在哪些查询中)。
  • @RomanKonoval 每个统计类型,如果是数字:平均值、方差、统计检验等。如果它们是字符串:基本上是过滤/分组。

标签: sql database postgresql performance entity-attribute-value


【解决方案1】:

IMO 您可以使用以下简单的数据库结构来解决您的问题。

统计类型字典

一个非常简单的表格 - 只是统计数据的名称和描述。类型:

create table stat_types (
  type        text not null constraint stat_types_pkey primary key,
  description text  
);

(如果元素数量有限,可以将其替换为枚举)

项目中各类对象的统计表

它包含对对象的 FK,对 stat 的 FK。类型(或只是枚举),这很重要,jsonb 字段具有 任意统计信息。与其类型相关的数据。例如,电话呼叫这样的表格:

create table phone_calls_statistics ( 
  phone_call_id uuid  not null references phone_calls,
  stat_type     text  not null references stat_types,
  data          jsonb,
  constraint phone_calls_statistics_pkey primary key (phone_call_id, stat_type)  
);

我在这里假设表phone_calls 的PK 类型为uuid

create table phone_calls (
  id uuid not null constraint phone_calls_pkey primary key
-- ...
);

data 字段具有不同的结构,具体取决于其统计信息。类型。 通话时长示例:

{
   "call_duration": 120.0
}

辍学

{
   "dropouts": [5.23, 40.92]
}

我们来玩数据吧:

insert into phone_calls_statistics values 
  ('9fc1f6c3-a9d3-4828-93ee-cf5045e93c4c', 'CALL_DURATION', '{"call_duration": 100.0}'),
  ('86d1a2a6-f477-4ed6-a031-b82584b1bc7e', 'CALL_DURATION', '{"call_duration": 110.0}'),
  ('cfd4b301-bdb9-4cfd-95db-3844e4c0625c', 'CALL_DURATION', '{"call_duration": 120.0}'),
  ('39465c2f-2321-499e-a156-c56a3363206a', 'CALL_DURATION', '{"call_duration": 130.0}'),
  ('9fc1f6c3-a9d3-4828-93ee-cf5045e93c4c', 'UNEXPECTED_HANGUP', '{"unexpected_hungup": true}'),
  ('86d1a2a6-f477-4ed6-a031-b82584b1bc7e', 'UNEXPECTED_HANGUP', '{"unexpected_hungup": true}'),
  ('cfd4b301-bdb9-4cfd-95db-3844e4c0625c', 'UNEXPECTED_HANGUP', '{"unexpected_hungup": false}'),
  ('39465c2f-2321-499e-a156-c56a3363206a', 'UNEXPECTED_HANGUP', '{"unexpected_hungup": false}');

获取平均、最短和最长通话时长:

select 
  avg((pcs.data ->> 'call_duration')::float) as avg,
  min((pcs.data ->> 'call_duration')::float) as min,
  max((pcs.data ->> 'call_duration')::float) as max
from 
  phone_calls_statistics pcs 
where 
  pcs.stat_type = 'CALL_DURATION';

获取意外挂断次数:

select 
  sum(case when (pcs.data ->> 'unexpected_hungup')::boolean is true then 1 else 0 end) as hungups  
from 
  phone_calls_statistics pcs 
where 
  pcs.stat_type = 'UNEXPECTED_HANGUP'; 

我相信这个解决方案非常简单灵活,具有良好的性能潜力和完美的可扩展性。主表有一个简单的索引;所有查询都将在其中执行。您始终可以扩展 stat 的数量。类型及其计算。

现场示例:https://www.db-fiddle.com/f/auATgkRKrAuN3jHjeYzfux/0

【讨论】:

  • 是的,这看起来可行。另外,我会对统计数据进行分区,以便更容易删除旧数据。我也会考虑 not 使用 JSON,这会减慢处理速度。如果统计类型足够好地确定数据,您可以拥有 - 比如说 - 一个 integer、一个 double precision 和一个 boolean 列,这些列将根据类型填充或不填充,那就去吧。
  • @LaurenzAlbe 谢谢。您能否解释一下为什么使用 JSON 会减慢处理速度?该字段不参与搜索,只参与聚合...
  • 从 JSON 中提取属性需要一点时间。但这可能可以忽略不计。我想我想提倡只在 JSON 提供优势时才使用它。
  • 对于那些感兴趣的人,这个概念被称为Entity–Attribute–Value model
  • @slhck 我认为您可以将该标签添加到您的问题中;)
猜你喜欢
  • 2014-06-11
  • 2017-03-15
  • 1970-01-01
  • 2014-02-24
  • 2011-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多