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


8

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

我宁愿在构建数据库时使用postgres。

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


1
复制时写入另一张表。如果prices创建表prices_history,则创建具有相似列的表。Hibernate Envers可以为您实现自动化
Neil McGuigan

Answers:


11

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

商业规则

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

  • 一个产品的购买在一个一对多的价格
  • 每个购买价格在确切的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)
);

Price表具有由两列组成的复合主键,即ProductNumber(依次受约束,作为引用的FOREIGN KEY 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);

由于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日确定PriceProduct主要数据。看到断言(或行)在从(i)到(ii)其的整个时间间隔内是当前的或有效的,则此DML操作ProductNumber Date PriceStartDateEndDate

 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

我上面提出的方法解决了前面描述的特征的业务领域,因此建议您使用有关将EndDateas 声明为命名表的列(不同于“字段”)的建议,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)
);

而且,是的,该逻辑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数据。ProductProductNumber Date


这看起来与我所做的工作类似,但是我发现使用价格(在这种情况下)具有startdate列和enddate列的表更方便/有效-因此,您只需要查找具有targetdate的行> =开始日期和目标日期<=结束日期。当然,如果数据没有与这些字段一起存储(包括终止日期31stumptember 9999,而不是Null,则没有实际的终止日期),那么您必须做一些工作来产生它。我实际上使它每天运行,默认情况下,结束日期=今天的日期。另外,我的描述要求结束日期1 =开始日期2减去1天。
罗伯特·卡内基


1

在这里给出了一个相对简单的答案,不需要对数据库进行特殊扩展(因此可以与任何数据库一起使用)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.