数百万不同类型值的高效存储模式
Efficient storage pattern for millions of values of different types
我即将建立一个SQL 数据库,它将包含数十万个对象的统计计算结果。计划使用 Postgres,但问题同样适用于 MySQL.
例如,假设我有 50 万条 phone 呼叫记录。每个 PhoneCall
现在都将通过后台作业系统计算统计数据。例如,PhoneCall
具有以下统计信息:
call_duration
:以秒为单位(浮动)
setup_time
:以秒为单位(浮动)
dropouts
:检测到音频丢失的时间段(数组),例如[5.23, 40.92]
hung_up_unexpectedly
:真或假(布尔值)
这些只是简单的例子;实际上,统计数据更为复杂。每个统计数据都有一个与之关联的版本号。
我不确定这些类型的计算数据的哪种存储模式最有效。不过,我并没有考虑完全规范化数据库中的所有内容。到目前为止,我提出了以下选项:
选项 1 – 一栏中的长格式
我将统计名称及其值分别存储在一列中,并引用主要交易对象。值列是一个文本字段;该值将被序列化(例如,作为 JSON 或 YAML),以便可以存储不同的类型(字符串、数组...)。统计数据 table 的数据库布局为:
statistic_id
(PK)
phone_call_id
(FK)
statistic_name
(字符串)
statistic_value
(文本,连载)
statistic_version
(整数)
created_at
(日期时间)
我使用这种模式有一段时间了,它的优点是我可以根据 phone 调用和统计名称轻松过滤统计。我还可以轻松添加新类型的统计信息,并按版本和创建时间进行过滤。
但在我看来,值的(反)序列化使得它在处理大量数据方面效率很低。另外,我无法在 SQL 级别上执行计算;我总是必须加载和反序列化数据。还是 Postgres 中的 JSON 支持那么好,以至于我仍然可以选择这种模式?
选项 2 – 统计作为主要对象的属性
我还可以考虑收集所有类型的统计名称并将它们作为新列添加到 phone 调用对象,例如:
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
(表示bool
、string
等的字符串)
statistic_version
(整数)
created_at
(日期时间)
这意味着 table 将非常稀疏,因为只会填充 statistic_value_
列中的一个。这会导致性能问题吗?
选项 4 – 规范化形式
尝试规范化选项 3,我会创建两个 tables:
statistics
id
(PK)
version
created_at
statistic_mapping
phone_call_id
(FK)
statistic_id
(FK)
statistic_type_mapping
statistic_id
(FK)
type
(字符串,表示bool
、string
等)
statistic_values_boolean
statistic_id
(FK)
value
(布尔值)
- …
但这不会有任何进展,因为我无法动态加入另一个 table 名称,可以吗?或者我应该根据统计 ID 加入所有 statistic_values_*
table 吗?我的应用程序必须确保那时不存在重复条目。
总而言之,鉴于此用例,当要求可以添加或更改统计类型时,在关系数据库(例如 Postgres)中存储数百万个统计值的最有效方法是什么,并且几个版本同时存在,查询值应该有点效率吧?
IMO 你可以使用以下简单的数据库结构来解决你的问题。
统计类型字典
一个非常简单的 table - 只是统计的名称和描述。类型:
create table stat_types (
type text not null constraint stat_types_pkey primary key,
description text
);
(如果元素个数有限可以用enum代替)
项目中每种对象的统计table
它包含对对象的 FK,对 stat 的 FK。类型(或只是枚举),这很重要,jsonb
字段带有 任意统计数据。 data 与其类型相关。例如,这样一个 table for phone calls:
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)
);
我在这里假设 table phone_calls
有 uuid
类型的 PK:
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';
我相信这个解决方案非常简单和灵活,具有良好的性能潜力和完美的可扩展性。主要table有一个简单的索引;所有查询都将在其中执行。您始终可以扩展统计数据的数量。类型及其计算。
我即将建立一个SQL 数据库,它将包含数十万个对象的统计计算结果。计划使用 Postgres,但问题同样适用于 MySQL.
例如,假设我有 50 万条 phone 呼叫记录。每个 PhoneCall
现在都将通过后台作业系统计算统计数据。例如,PhoneCall
具有以下统计信息:
call_duration
:以秒为单位(浮动)setup_time
:以秒为单位(浮动)dropouts
:检测到音频丢失的时间段(数组),例如[5.23, 40.92]
hung_up_unexpectedly
:真或假(布尔值)
这些只是简单的例子;实际上,统计数据更为复杂。每个统计数据都有一个与之关联的版本号。
我不确定这些类型的计算数据的哪种存储模式最有效。不过,我并没有考虑完全规范化数据库中的所有内容。到目前为止,我提出了以下选项:
选项 1 – 一栏中的长格式
我将统计名称及其值分别存储在一列中,并引用主要交易对象。值列是一个文本字段;该值将被序列化(例如,作为 JSON 或 YAML),以便可以存储不同的类型(字符串、数组...)。统计数据 table 的数据库布局为:
statistic_id
(PK)phone_call_id
(FK)statistic_name
(字符串)statistic_value
(文本,连载)statistic_version
(整数)created_at
(日期时间)
我使用这种模式有一段时间了,它的优点是我可以根据 phone 调用和统计名称轻松过滤统计。我还可以轻松添加新类型的统计信息,并按版本和创建时间进行过滤。
但在我看来,值的(反)序列化使得它在处理大量数据方面效率很低。另外,我无法在 SQL 级别上执行计算;我总是必须加载和反序列化数据。还是 Postgres 中的 JSON 支持那么好,以至于我仍然可以选择这种模式?
选项 2 – 统计作为主要对象的属性
我还可以考虑收集所有类型的统计名称并将它们作为新列添加到 phone 调用对象,例如:
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
(表示bool
、string
等的字符串)statistic_version
(整数)created_at
(日期时间)
这意味着 table 将非常稀疏,因为只会填充 statistic_value_
列中的一个。这会导致性能问题吗?
选项 4 – 规范化形式
尝试规范化选项 3,我会创建两个 tables:
statistics
id
(PK)version
created_at
statistic_mapping
phone_call_id
(FK)statistic_id
(FK)
statistic_type_mapping
statistic_id
(FK)type
(字符串,表示bool
、string
等)
statistic_values_boolean
statistic_id
(FK)value
(布尔值)
- …
但这不会有任何进展,因为我无法动态加入另一个 table 名称,可以吗?或者我应该根据统计 ID 加入所有 statistic_values_*
table 吗?我的应用程序必须确保那时不存在重复条目。
总而言之,鉴于此用例,当要求可以添加或更改统计类型时,在关系数据库(例如 Postgres)中存储数百万个统计值的最有效方法是什么,并且几个版本同时存在,查询值应该有点效率吧?
IMO 你可以使用以下简单的数据库结构来解决你的问题。
统计类型字典
一个非常简单的 table - 只是统计的名称和描述。类型:
create table stat_types (
type text not null constraint stat_types_pkey primary key,
description text
);
(如果元素个数有限可以用enum代替)
项目中每种对象的统计table
它包含对对象的 FK,对 stat 的 FK。类型(或只是枚举),这很重要,jsonb
字段带有 任意统计数据。 data 与其类型相关。例如,这样一个 table for phone calls:
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)
);
我在这里假设 table phone_calls
有 uuid
类型的 PK:
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';
我相信这个解决方案非常简单和灵活,具有良好的性能潜力和完美的可扩展性。主要table有一个简单的索引;所有查询都将在其中执行。您始终可以扩展统计数据的数量。类型及其计算。