如何对列的可选细分建模
Posted
技术标签:
【中文标题】如何对列的可选细分建模【英文标题】:How to model an optional breakdown of a column 【发布时间】:2012-02-21 17:59:05 【问题描述】:我正在设计一个允许查询历史交易的数据库,但我被这个特殊问题难住了。
比方说,要存储的列之一是每天的销售数量(按各种属性细分)。根据最近的数据,我们可以将其分解为在线和店内销售;但是,在某个截止日期之前,可用于填充此数据库的唯一信息是总销售额数字,没有细分。
我想不出一种特别优雅的方式来呈现这一点,以便更新的数据可以填充逻辑“在线销售”和“店内销售”列,“总销售额”被计算为它们的总和(在view/sproc/computed 列)——但旧数据只能报告总销售额。
此数据的 FWIW 客户会知道销售细分可能存在也可能不存在 - 因此查询的输出将始终具有有效的“总销售额”数字,并且可能缺少在线或店内的值销售量。 (我特别说“缺失”而不是“空”,因为没有强烈要求将其表示为这样,如果替代方案更有意义的话。)
有处理这种情况的规范方法吗?
鉴于到目前为止缺乏强烈的回应,我将发布一些我自己认为是候选人的答案(如果没有更好的答案实现,我最终可能最终需要接受其中一个)。对这些内容的评论、批评和/或投票将被亲切地接受——尤其是对它们的改进。
【问题讨论】:
【参考方案1】:您所描述的是 OLTP 和 OLAP 数据库之间的区别。
OLTP(在线交易处理)这类数据代表日常交易。例如库存添加,更改删除。客户添加到购物车请求、订单、退款。这些是全天发生的细节交易。
OLAP(在线分析处理)这种类型的数据表示给定时间段内的累积数据。例如:每天、每周、每月、每季度、每年。购买将此信息存储在一组单独的表甚至数据库中,您可以运行不同的查询来提供您正在寻找的报告。
您可能会遇到的问题是,当您只有 OLTP 数据时,您需要 OLAP 信息。
如果您希望按不同类别进行每日销售,则创建一组每日 OLAP 表,并在每晚运行一个单独的进程或一组进程,将这些数据归档到这些表中。
每个月您都可以运行不同的流程来创建每月的 OLAP 表。
一开始需要做很多工作,但它可以为您提供两全其美的体验。您可以整天针对您的 OLAP 数据玩假设游戏,而不会影响客户或日常运营。
【讨论】:
是的,这绝对是我正在设计的面向 OLAP 的数据库。但我觉得你的回答并没有完全涵盖 如何 来模拟几个月我可以提供Online sales
和In-store sales
的情况(并希望从中计算出总数),但在其他月份我只有total sales
。 OLAP 事实表中应该有三个可为空的列吗?事实表的两个“子类”?事实和与之关联的(Sales figure type, value)
元组之间的一对多关系?
事实上,我刚刚添加了一个答案来代表我一直在考虑的这些技术中的每一个,我很感激你对哪一个对 OLAP 数据最有意义的想法,以及你如何可能会改善其中任何一个。
为什么有些月份只有总销售额,而没有在线和实体店之间的细分?
因为这个数据当时没有被捕获(并且隐含地没有存储在任何地方,所以现在是未知的)。但我们仍然希望在提供查询的时间序列中包含早期的聚合数据,尤其是在大多数情况下,客户不会关心故障。
如果您采用在线销售的平均百分比(从您开始捕获此数据时开始)并使用此百分比拆分较旧的数据,是否会使数字出现偏差?【参考方案2】:
我可能会添加一个“销售版本”表,以便区分“历史不可破坏的销售”和“较新的销售”。
那么,或许这种结构可以实现:
sales_version 表
列:salesid、salesversion
sales_v1 表
列:salesid、日期时间
我想对于每个销售,详细信息都在一个引用 salesid 的从表中
sales_v2 表
列:salesid、datetime、online、instore 或可能是 salesid、datetime、type('online' 或 'instore')或本身引用销售类型表的类型 id。
我想对于每个销售,详细信息都在一个引用 salesid 的从表中
【讨论】:
【参考方案3】:另一种可能的方法是使用两个不同的事实表,一个用于旧数据和新数据,并根据每种情况下可以确定的确切数据对它们进行建模。
对于查询,总体结果将是来自两个事实表的数据的联合 - 对于特定的销售数据列,旧事实表中的选择被填充NULL
(或类似的)。
这种方法很好,因为它准确地模拟了我们正在记录(并且能够记录)的数据,但是如果两个表除了销售列之外相同,它可能会导致大量重复字段。另外,我有一种(不合格的)感觉,即在合并两个表时,数据库将更难用索引做有用的事情,这样查询性能可能会受到影响。
【讨论】:
【参考方案4】:一种方法是简单地将所有三列都显示在事实表中,并且它们都可以为空。对于较新的数据,仅填充两个更具体的销售数据列,而对于较旧的数据,仅填充总销售额。
查询时,可以有条件地填充总销售额,例如:
CASE WHEN TotalSales IS NULL THEN OnlineSales + InStoreSales ELSE TotalSales END
从应用程序的角度来看,这具有最简单的优点。尽管从数据建模的角度来看,我不喜欢 每条 记录至少会留下一个字段为空的事实。并且很难表达 要么 TotalSales
已填充,或 OnlineSales
和 InStoreSales
两者都必须填充的数据完整性约束。用触发器检查是否被认为是好的做法?
(对此的替代版本是即使在新情况下也填充 TotalSales
字段,但我认为重复和潜在不一致数据的风险不值得稍微简单的查询。)
【讨论】:
【参考方案5】:第三种方法是将销售数据建模为与观察到的事实的多对一关系;也就是说,每个事实都包含多个(可能只有一个)销售数据,每个数据都有特定的类型。在这种情况下,总销售额只是现有数字的总和。
所以架构可能看起来像
DataFact
-------------
DataFactId (PK)
(+any other fact columns apart from sales)
SalesData
---------
DataFactId (FK to DataFact)
SalesDataType ("Total"/"Online" etc - either as varchar or FK to dimension table)
SalesValue (the actual sales figure we want to record)
这种方法的优点是它捕捉到了一个概念,即对于给定的事实,它可能包含也可能不包含任何特定的销售数据实体。它还仅抽象出变化的字段,这意味着任何公共字段仍然只在父 DataFact 表中表达一次(与 separate fact tables 不同)。如果将来有更多的销售细分,扩展也是微不足道的。
缺点是它仍然没有表达必须存在total或online + in-store的约束。事实上,我们甚至不能表示必须存在至少一个 SalesData 条目;这与添加多个可为空的字段具有相同的问题。虽然这种方法非常简洁,但为了充分利用其灵活性,我们希望将销售数据作为某种集合返回给查询客户端,这使得查询比标准 2D 结果集更复杂。 可以使用 SalesData
表上的聚合将其压缩回 2D 表,但我相信您必须使用不同的约束多次将其拉入以确定每个字段。
【讨论】:
以上是关于如何对列的可选细分建模的主要内容,如果未能解决你的问题,请参考以下文章
SonataAdminBundle 如何在列表视图中对列的值求和