我将如何跟踪数据库中的所有价格变化,以便在“y”日期获得“x”产品的价格

Gun*_*red 9 database-design best-practices

我需要跟踪产品价格变化,以便我可以在数据库中查询给定日期的产品价格。该信息用于计算历史审计的系统中,因此它必须根据购买日期返回正确产品的正确价格。

我更喜欢使用 postgres 来构建数据库。

我需要数据库的设计,但也欢迎任何和所有最佳实践建议。

MDC*_*CCL 13

如果我适当地理解了场景,您应该定义一个保留价格时间序列的表;因此,我同意,这与您正在使用的数据库的时间方面有很大关系。

商业规则

让我们从概念层面开始分析情况。因此,如果,在您的业务领域中,

  • 一个产品的购买在一个一对多的价格
  • 每个购买价格在确切的StartDate变为Current,并且
  • 价格 结束日期(表示日期价格不再是当前)等于起始日期紧随其后的价格

那么这意味着

  • 没有间隙的明显间,在此期间价格当前(时间序列是连续的相合),和
  • 结束日期一的价格是衍生数据。

图 1 中显示的IDEF1X图虽然高度简化,但描绘了这样一个场景:

图 1 - 产品价格简化 IDEF1X 图 - 场景 A

说明性逻辑布局

以下基于上述 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)
);
Run Code Online (Sandbox Code Playgroud)

Price表有一个复合 PRIMARY KEY 由两列组成,即,ProductNumber(反过来,约束,作为引用的外键Product.ProductNumber)和StartDate(指出以特定价格购买特定产品的特定日期) .

如果产品是在同一天以不同的价格购买的,那么您可以添加一个标记为当给定产品以确切价格购买时保持即时的列,而不是列。然后必须将 PRIMARY KEY 声明为.StartDateStartDateTime(ProductNumber, StartDateTime)

如图所示,上述表是一个普通的表,因为您可以声明 SELECT、INSERT、UPDATE 和 DELETE 操作来直接操作其数据,因此它 (a) 允许避免安装额外的组件 (b) 可以在所有如果需要,可以对主要的 SQL 平台进行一些调整。

数据操作示例

为了举例说明一些看起来很有用的操作,假设您分别在ProductPrice表中插入了以下数据:

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);
Run Code Online (Sandbox Code Playgroud)

由于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;
Run Code Online (Sandbox Code Playgroud)

然后直接从该视图中选择以下操作

  SELECT ProductNumber,
         ProductEtcetera,
         PriceAmount,
         StartDate,
         EndDate
    FROM PriceWithEndDate 
ORDER BY StartDate DESC;
Run Code Online (Sandbox Code Playgroud)

提供下一个结果集:

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.
Run Code Online (Sandbox Code Playgroud)

现在,让我们假设你有兴趣让整个Price的数据Product主要是鉴定ProductNumber 1750Date 2017年6月2日。看到Price断言(或行)在从 (i) its到 (ii) its的整个Interval期间是当前或有效StartDateEndDate,那么此 DML 操作

 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.
Run Code Online (Sandbox Code Playgroud)

产生以下结果集

ProductNumber  ProductEtcetera     PriceAmount  StartDate   EndDate
-------------  ------------------  -----------  ----------  ----------
         1750  Price time series…         1000  2017-06-01  2017-06-03
Run Code Online (Sandbox Code Playgroud)

这解决了上述要求。

如图所示,PriceWithEndDate视图在获取大部分可派生数据方面起着至关重要的作用,并且可以以相当普通的方式从 SELECTed FROM 中获取。

考虑到您的首选平台是 PostgreSQL,官方文档站点的此内容包含有关“物化”视图的信息,如果该方面出现问题,可以通过物理级机制帮助优化执行速度。其他 SQL 数据库管理系统 (DBMS) 提供非常相似的物理工具,尽管可能会应用不同的术语,例如Microsoft SQL Server 中的“索引”视图

您可以在此 db<>fiddle此 SQL Fiddle 中查看所讨论的 DDL 和 DML 代码示例。

相关资源

  • 本问答中,我们讨论了一个业务背景,其中包括产品价格的变化,但范围更广,因此您可能会感兴趣。

  • 这些 Stack Overflow 帖子涵盖了与在 PostgreSQL中保存货币数据的列的类型相关的要点。

对评论的回应

这看起来与我所做的工作相似,但我发现使用价格(在这种情况下)具有开始日期列和结束日期列的表更方便/有效 - 所以你只是在寻找带有目标日期的行>= 开始日期和目标日期 <= 结束日期。当然,如果数据没有与这些字段一起存储(包括 9999 年 12 月 31 日的结束日期,而不是 Null,其中不存在实际结束日期),那么您必须做一些工作来生成它。我实际上让它每天运行,默认情况下结束日期=今天的日期。另外,我的描述要求结束日期 1 = 开始日期 2 减去 1 天。– @罗伯特卡内基2017-06-22 20:56:01Z

我上面提出的方法解决了前面描述的特征的业务领域,因此应用您关于将EndDate命名的基表的列(不同于“字段”)声明为列的建议Price将暗示数据库的逻辑结构将不能正确反映概念图式,必须精确定义和反映概念图式,包括区分 (1)基础信息和 (2)可推导信息。

除此之外,这样的操作过程会引入重复,因为EndDate然后可以通过 (a) 可推导表以及 (b) 名为 的基表Price获得重复的EndDate列。虽然这是一种可能性,但如果从业者决定遵循上述方法,他或她应该明确地警告数据库用户它所涉及的不便和低效。这些不便和低效之一是,例如,迫切需要开发一种机制,以确保在任何时候,每个Price.EndDate值都等于手头值Price.StartDate的紧接连续行的列的Price.ProductNumber值。

相比之下,老实说,生成有问题的派生数据的工作一点也不特别,并且需要 (i) 保证数据库抽象的逻辑和概念级别之间的正确对应,以及 (ii) ) 确保数据完整性,如前所述,这两个方面都非常重要。

如果您所谈论的效率方面与某些数据操作操作的执行速度有关,那么必须在适当的地方进行管理,即在物理级别,通过例如基于(1)的有利索引策略) 特定的查询趋势和 (2) 使用的 DBMS 提供的特定物理机制。否则,牺牲适当的概念逻辑映射并损害所涉及数据的完整性很容易将强大的系统(即有价值的组织资产)变成不可靠的资源。

不连续或不连续的时间序列

另一方面,在某些情况下,保留EndDate时间序列表中每一行的 不仅更加方便和高效,而且需要,尽管这当然完全取决于特定于业务环境的要求。这种情况的一个例子出现在

  • 两个起始日期结束日期的信息段之前保持(和经由保留)在每次插入和
  • 可以存在间隙在中间的不同的时期,在此期间价格当前(即,时间序列是不连续的间断)。

我已经在图 2 中显示的 IDEF1X 图中表示了上述场景。

图 2 - 产品价格简化 IDEF1X 图 - 场景 B

在这种情况下,是的,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)
);
Run Code Online (Sandbox Code Playgroud)

而且,是的,这种逻辑 DDL 设计简化了物理级别的管理,因为您可以EndDate在相对简单的配置中建立包含列(如图所示,在基表中声明)的索引策略。

然后,像下面这样的 SELECT 操作

 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';
Run Code Online (Sandbox Code Playgroud)

可以被用来导出整个Price的数据Product通过初步鉴定ProductNumber 1750Date 2017 6月2日