我将需要跟踪产品价格的变化,以便可以查询给定日期的数据库中的产品价格。该信息用于计算历史审计的系统中,因此它必须根据购买日期返回正确产品的正确价格。
我宁愿在构建数据库时使用postgres。
我需要数据库的设计,但也欢迎所有最佳实践建议。
我将需要跟踪产品价格的变化,以便可以查询给定日期的数据库中的产品价格。该信息用于计算历史审计的系统中,因此它必须根据购买日期返回正确产品的正确价格。
我宁愿在构建数据库时使用postgres。
我需要数据库的设计,但也欢迎所有最佳实践建议。
Answers:
如果我适当地了解了这种情况,则应该定义一个保留Price时间序列的表;因此,我同意,这与您正在使用的数据库的时间方面有很大关系。
让我们从概念层面开始分析情况。因此,如果在您的业务领域中,
那意味着
图1所示的IDEF1X图表虽然进行了高度简化,但却描述了这种情况:
下面的基于IDEF1X图表的SQL-DDL逻辑级设计说明了一种可行的方法,可以适应您自己的确切需求:
-- At the physical level, you should define a convenient
-- indexing strategy based on the data manipulation tendencies
-- so that you can supply an optimal execution speed of the
-- queries declared at the logical level; thus, some testing
-- sessions with considerable data load should be carried out.
CREATE TABLE Product (
ProductNumber INT NOT NULL,
Etcetera CHAR(30) NOT NULL,
--
CONSTRAINT Product_PK PRIMARY KEY (ProductNumber)
);
CREATE TABLE Price (
ProductNumber INT NOT NULL,
StartDate DATE NOT NULL,
Amount INT NOT NULL, -- Retains the amount in cents, but there are other options regarding the type of use.
--
CONSTRAINT Price_PK PRIMARY KEY (ProductNumber, StartDate),
CONSTRAINT Price_to_Product_FK FOREIGN KEY (ProductNumber)
REFERENCES Product (ProductNumber),
CONSTRAINT AmountIsValid_CK CHECK (Amount >= 0)
);
该Price
表具有由两列组成的复合主键,即ProductNumber
(依次受约束,作为引用的FOREIGN KEY Product.ProductNumber
)和StartDate
(指出了以特定价格购买某产品的特定日期) 。
如果在同一天以不同的价格购买了产品,而不是在该列中,您可能会加上一个标签,因为当以特定的价格购买给定产品时,该标签会保持即时。然后必须将PRIMARY KEY声明为。StartDate
StartDateTime
(ProductNumber, StartDateTime)
如图所示,上述表是一个普通表,因为您可以声明SELECT,INSERT,UPDATE和DELETE操作来直接操作其数据,因此(a)避免安装其他组件,并且(b)可以在所有表中使用如果需要,可以对主要的SQL平台进行一些调整。
为了举例说明一些看似有用的操作,我们假设您已分别在Product
和Price
表中插入以下数据:
INSERT INTO Product
(ProductNumber, Etcetera)
VALUES
(1750, 'Price time series sample');
INSERT INTO Price
(ProductNumber, StartDate, Amount)
VALUES
(1750, '20170601', 1000),
(1750, '20170603', 3000),
(1750, '20170605', 4000),
(1750, '20170607', 3000);
由于Price.EndDate
是可派生的数据点,因此您必须通过一个可以作为视图创建的派生表来获取它,以产生“完整”时间序列,如下所示:
CREATE VIEW PriceWithEndDate AS
SELECT P.ProductNumber,
P.Etcetera AS ProductEtcetera,
PR.Amount AS PriceAmount,
PR.StartDate,
(
SELECT MIN(StartDate)
FROM Price InnerPR
WHERE P.ProductNumber = InnerPR.ProductNumber
AND InnerPR.StartDate > PR.StartDate
) AS EndDate
FROM Product P
JOIN Price PR
ON P.ProductNumber = PR.ProductNumber;
然后直接从该视图中进行选择的以下操作
SELECT ProductNumber,
ProductEtcetera,
PriceAmount,
StartDate,
EndDate
FROM PriceWithEndDate
ORDER BY StartDate DESC;
提供下一个结果集:
ProductNumber ProductEtcetera PriceAmount StartDate EndDate
------------- ------------------ ----------- ---------- ----------
1750 Price time series… 4000 2017-06-07 NULL -- (*)
1750 Price time series… 3000 2017-06-05 2017-06-07
1750 Price time series… 2000 2017-06-03 2017-06-05
1750 Price time series… 1000 2017-06-01 2017-06-03
-- (*) A ‘sentinel’ value would be useful to avoid the NULL marks.
现在,让我们假设您有兴趣获取1750年在2017 年6月2日确定Price
的Product
主要数据。看到断言(或行)在从(i)到(ii)其的整个时间间隔内是当前的或有效的,则此DML操作ProductNumber
Date
Price
StartDate
EndDate
SELECT ProductNumber,
ProductEtcetera,
PriceAmount,
StartDate,
EndDate
FROM PriceWithEndDate
WHERE ProductNumber = 1750 -- (1)
AND StartDate <= '20170602' -- (2)
AND EndDate >= '20170602'; -- (3)
-- (1), (2) and (3): You can supply parameters in place of fixed values to make the query more versatile.
产生以下结果集
ProductNumber ProductEtcetera PriceAmount StartDate EndDate
------------- ------------------ ----------- ---------- ----------
1750 Price time series… 1000 2017-06-01 2017-06-03
满足了上述要求。
如图所示,PriceWithEndDate
视图在获取大多数可导出数据中起着至关重要的作用,并且可以以相当普通的方式从中选择视图。
考虑到您的首选平台是PostgreSQL,来自官方文档网站的内容包含有关“物化”视图的信息,如果上述方面有问题,它可以通过物理级机制帮助优化执行速度。其他SQL数据库管理系统(DBMS)提供的物理工具非常相似,尽管可以应用不同的术语,例如Microsoft SQL Server中的“索引”视图。
您可以在此db <> fiddle和此SQL Fiddle中查看所讨论的DDL和DML代码示例。
在本问答中,我们讨论一个业务环境,其中包括产品价格的变化,但范围更广,因此您可能会感兴趣。
这些Stack Overflow帖子涵盖了有关在PostgreSQL 中保存货币数据的列类型的非常相关的要点。
这看起来与我所做的工作类似,但是我发现使用价格(在这种情况下)具有startdate列和enddate列的表更方便/有效-因此,您只需要查找具有targetdate的行> =开始日期和目标日期<=结束日期。当然,如果数据没有与这些字段一起存储(包括9999年12月31日的结束日期,而不是Null,那里没有实际的结束日期),那么您就必须做一些工作来产生它。我实际上使它每天运行,默认情况下,结束日期=今天的日期。另外,我的描述要求结束日期1 =开始日期2减去1天。– @Robert Carnegie,在2017-06-22 20:56:01Z
我上面提出的方法解决了前面描述的特征的业务领域,因此建议您使用有关将EndDate
as 声明为命名表的列(不同于“字段”)的建议,Price
这暗示数据库的逻辑结构将不能正确反映概念方案,必须精确定义和反映概念方案,包括(1)基本信息与(2)可衍生信息的区别。
除此之外,这样的操作过程将引入重复,因为EndDate
然后可以通过(a)可派生表以及还通过(b)名为的基表(Price
因此具有重复的EndDate
列)来获得重复操作。虽然这是有可能的,但是如果从业者决定采用上述方法,则他或她应明确警告数据库用户有关它带来的不便和效率低下的情况。这些不便和低效之一是例如迫切需要开发一种机制,该机制始终确保每个Price.EndDate
值等于Price.StartDate
当前Price.ProductNumber
值的紧接连续行的列的值。
相反,老实说,我所提出的产生相关数据的工作根本不是什么特别的工作,并且要求(i)保证数据库的逻辑和概念抽象级别之间的正确对应关系,以及(ii) )确保数据完整性,如前所述,这两个方面绝对至关重要。
如果您所讨论的效率方面与某些数据处理操作的执行速度有关,则必须基于(1 )特定的查询趋势,以及(2)使用DBMS提供的特定物理机制。否则,牺牲适当的概念逻辑映射并损害所涉及数据的完整性,很容易将健壮的系统(即,宝贵的组织资产)变成不可靠的资源。
不连续或不连续的时间序列
另一方面,在某些情况下,保留EndDate
时间序列表中的每一行不仅更方便,更有效,而且要求更高,尽管这当然完全取决于特定于业务环境的要求。这种情况的一个例子是
我已经在图2中显示的IDEF1X图中表示了上述场景。
在那种情况下,是的,Price
必须以类似于以下方式声明假设表:
CREATE TABLE Price (
ProductNumber INT NOT NULL,
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
Amount INT NOT NULL,
--
CONSTRAINT Price_PK PRIMARY KEY (ProductNumber, StartDate, EndDate),
CONSTRAINT Price_to_Product_FK FOREIGN KEY (ProductNumber)
REFERENCES Product (ProductNumber),
CONSTRAINT DatesOrder_CK CHECK (EndDate >= StartDate)
);
而且,是的,该逻辑DDL设计简化了物理级别的管理,因为您可以建立一种索引策略,该索引策略EndDate
以相对容易的配置包含该列(如图所示,该列在基表中声明)。
然后,执行以下选择操作
SELECT P.ProductNumber,
P.Etcetera,
PR.Amount,
PR.StartDate,
PR.EndDate
FROM Price PR
JOIN Product P
WHERE P.ProductNumber = 1750
AND StartDate <= '20170602'
AND EndDate >= '20170602';
可用于推导1750年在2017 年6月2日最初识别的整体Price
数据。Product
ProductNumber
Date
我相信您会想要查看Temporal Tables的。这些提供的功能可以完全满足您的需求,并且在Postgres中提供了具有适当扩展名的功能。
这个概念似乎也与数据库无关,因为它在各种RDBMS平台上提供。
prices
创建表prices_history
,则创建具有相似列的表。Hibernate Envers可以为您实现自动化