Como rastrear todas as alterações de preço em um banco de dados para obter o preço do produto 'x' na data 'y'

8

Eu precisaria acompanhar as alterações de preço de um produto para poder consultar o banco de dados em busca de um preço de produto em uma determinada data. As informações são usadas em um sistema que calcula auditorias históricas, portanto, é necessário retornar o preço correto para o produto correto com base na data da compra.

Eu preferiria usar o postgres na construção do banco de dados.

Eu preciso do design do banco de dados, mas todas e quaisquer sugestões de boas práticas também são bem-vindas.

Gunnar Norred
fonte
11
copiar em escrever para outra tabela. Se tabela for, pricescrie uma tabela prices_historycom colunas semelhantes. Hibernate Envers pode automatizar isso para você
Neil McGuigan

Respostas:

11

Se eu entendo o cenário adequadamente, você deve definir uma tabela que retenha uma série temporal de preço ; portanto, eu concordo, isso tem muito a ver com o aspecto temporal do banco de dados com o qual você está trabalhando.

Regras do negócio

Vamos começar a analisar a situação do nível conceitual. Então, se , no seu domínio comercial,

  • um produto é comprado a preços um para muitos ,
  • cada preço de compra se torna atual em um exata StartDate e
  • o preço EndDate (que indica a data quando um Preço deixa de ser atual ) é igual ao StartDate do imediatamente posterior Preço ,

então isso significa que

  • não existem lacunas entre os períodos distintos durante os quais os preços são atuais (a série temporal é contínua ou em conjunto ) e
  • a data final de um preço é um dado derivável.

O diagrama IDEF1X mostrado na Figura 1 , embora altamente simplificado, descreve um cenário como esse:

Fig. 1 - Diagrama simplificado do preço de produto IDEF1X - Cenário A

Layout lógico expositivo

E o design de nível lógico SQL-DDL a seguir, com base no referido diagrama IDEF1X, ilustra uma abordagem viável que você pode adaptar às suas próprias necessidades exatas:

-- 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)
);

A Pricetabela possui uma CHAVE PRIMÁRIA composta de duas colunas, ou seja, ProductNumber(restrita, por sua vez, como CHAVE ESTRANGEIRA que faz referência a Product.ProductNumber) e StartDate(apontando a Data específica em que um determinado Produto foi comprado a um Preço específico ) .

Caso os produtos sejam comprados a preços diferentes durante o mesmo dia , em vez da StartDatecoluna, você pode incluir um rotulado para StartDateTimemanter o instante em que um determinado produto foi comprado a um preço exato . A CHAVE PRIMÁRIA teria que ser declarada como (ProductNumber, StartDateTime).

Como demonstrado, a tabela mencionada acima é comum, porque você pode declarar operações SELECT, INSERT, UPDATE e DELETE para manipular seus dados diretamente, portanto, (a) permite evitar a instalação de componentes adicionais e (b) pode ser usado em todos os as principais plataformas SQL com alguns poucos ajustes, se necessário.

Amostras de manipulação de dados

Para exemplificar algumas operações de manipulação que parecem úteis, digamos que você inseriu os seguintes dados nas tabelas Producte Price, respectivamente:

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);

Como o Price.EndDateé um ponto de dados derivável, você deve obtê-lo por meio de, precisamente, uma tabela derivada que pode ser criada como uma exibição para produzir a série temporal “completa”, como exemplificado abaixo:

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;

Em seguida, a operação a seguir que SELECT diretamente diretamente dessa exibição

  SELECT ProductNumber,
         ProductEtcetera,
         PriceAmount,
         StartDate,
         EndDate
    FROM PriceWithEndDate 
ORDER BY StartDate DESC;

fornece o próximo conjunto de resultados:

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.

Agora, suponhamos que você esteja interessado em obter todos os Pricedados dos Productprincipais identificados até ProductNumber 1750 em Date 2 de junho de 2017 . Vendo que uma Priceasserção (ou linha) é atual ou efetiva durante todo o intervalo que vai de (i) StartDatea (ii) sua EndDate, então esta operação 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.

produz o conjunto de resultados a seguir

ProductNumber  ProductEtcetera     PriceAmount  StartDate   EndDate
-------------  ------------------  -----------  ----------  ----------
         1750  Price time series         1000  2017-06-01  2017-06-03

que aborda o referido requisito.

Como mostrado, a PriceWithEndDateexibição desempenha um papel primordial na obtenção da maioria dos dados deriváveis ​​e pode ser SELECIONADA DE de uma maneira bastante comum.

Considerando que sua plataforma preferida é o PostgreSQL, este conteúdo do site oficial de documentação contém informações sobre visualizações "materializadas" , que podem ajudar a otimizar a velocidade de execução por meio de mecanismos de nível físico, se esse aspecto se tornar problemático. Outros sistemas de gerenciamento de banco de dados SQL (DBMSs) oferecem instrumentos físicos muito parecidos, embora diferentes terminologias possam ser aplicadas, por exemplo, visualizações "indexadas" no Microsoft SQL Server.

Você pode ver as amostras de código DDL e DML discutidas em ação neste db <> fiddle e neste SQL Fiddle .

Recursos relacionados

  • Nestas perguntas e respostas , discutimos um contexto de negócios que inclui as alterações nos preços dos produtos, mas que tem um escopo mais extenso, para que você possa achar interessante.

  • Essas postagens de estouro de pilha cobrem pontos muito relevantes em relação ao tipo de uma coluna que contém um dado de moeda no PostgreSQL.

Respostas aos comentários

Isso se parece com o trabalho que eu fiz, mas achei muito mais conveniente / eficiente trabalhar com uma tabela em que um preço (neste caso) tem uma coluna de data de início e uma coluna de data de término - então você está apenas procurando linhas com data de destino > = data de início e data de destino <= data de término. Obviamente, se os dados não forem armazenados com esses campos (incluindo data final em 31 de dezembro de 9999, e não Nulo, onde não existe data final real), você precisará trabalhar para produzi-los. Na verdade, eu fiz rodar todos os dias, com data final = data de hoje por padrão. Além disso, minha descrição requer data final 1 = data inicial 2 menos 1 dia. - @Robert Carnegie , em 22-06-2017 20: 56: 01Z

O método que proponho acima aborda um domínio comercial com as características descritas anteriormente , consequentemente aplicando sua sugestão sobre declarar o EndDatecomo uma coluna (que é diferente de um "campo") da tabela base nomeada Priceimplicaria que a estrutura lógica do banco de dados seria não esteja refletindo o esquema conceitual corretamente, e um esquema conceitual deve ser definido e refletido com precisão, incluindo a diferenciação de (1) informações básicas de (2) informações deriváveis .

Além disso, esse curso de ação introduziria duplicação, uma vez que a mesma EndDatepoderia ser obtida em virtude de (a) uma tabela derivável e também em virtude de (b) a tabela base denominada Price, com a EndDatecoluna duplicada . Embora essa seja uma possibilidade, se um profissional decidir seguir a abordagem, ele ou ela deve avisar decididamente os usuários do banco de dados sobre os inconvenientes e ineficiências que isso envolve. Um desses inconvenientes e ineficiências é, por exemplo, a necessidade urgente de desenvolver um mecanismo que garanta, a todo momento , que cada Price.EndDatevalor seja igual ao da Price.StartDatecoluna da linha imediatamente sucessiva para o Price.ProductNumbervalor em questão.

Por outro lado, o trabalho para produzir os dados derivados em questão, como apresento, não é, honestamente, nada especial e é necessário para (i) garantir a correspondência correta entre os níveis lógico e conceitual de abstração do banco de dados e (ii) ) garantem a integridade dos dados, ambos os aspectos que foram observados anteriormente são decididamente de grande importância.

Se o aspecto de eficiência que você está falando estiver relacionado à velocidade de execução de algumas operações de manipulação de dados, ele deverá ser gerenciado no local apropriado, ou seja, no nível físico, por meio de, por exemplo, uma estratégia de indexação vantajosa, baseada em (1 ) as tendências de consulta específicas e (2) os mecanismos físicos específicos fornecidos pelo DBMS de uso. Caso contrário, sacrificar o mapeamento conceitual-lógico apropriado e comprometer a integridade dos dados envolvidos transforma facilmente um sistema robusto (ou seja, um ativo organizacional valioso) em um recurso não confiável.

Séries temporais descontínuas ou disjuntas

Por outro lado, há circunstâncias em que a retenção EndDatede cada linha em uma tabela de séries temporais não é apenas mais cômoda e eficiente, mas exigida , embora isso dependa inteiramente dos requisitos específicos do ambiente de negócios. Um exemplo desse tipo de circunstância ocorre quando

  • as informações StartDate e EndDate são mantidas antes (e retidas por) a cada INSERÇÃO, e
  • pode haver lacunas no meio dos períodos distintos durante os quais os preços são atuais (ou seja, a série cronológica é descontínua ou disjunta ).

Eu representei o referido cenário no diagrama IDEF1X exibido na Figura 2 .

Fig. 2 - Diagrama simplificado do preço de produto IDEF1X - Cenário B

Nesse caso, sim, a Pricetabela hipotética deve ser declarada de maneira semelhante a esta:

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)
);

E, sim, esse design lógico de DDL simplifica a administração no nível físico, porque você pode criar uma estratégia de indexação que inclua a EndDatecoluna (que, como mostrado, é declarada em uma tabela base) em configurações relativamente mais fáceis .

Em seguida, uma operação SELECT como a abaixo

 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';

pode ser usado para derivar todos os Pricedados para os Productprimariamente identificados até ProductNumber 1750 em Date 2 de junho de 2017 .

MDCCL
fonte
Isso se parece com o trabalho que eu fiz, mas achei muito mais conveniente / eficiente trabalhar com uma tabela em que um preço (neste caso) tem uma coluna de data de início e uma coluna de data de término - então você está apenas procurando linhas com data de destino > = data de início e data de destino <= data de término. Obviamente, se os dados não forem armazenados com esses campos (incluindo data final de 31 de novembro de 9999, e não Nulo, onde não existe data de término real), você precisará trabalhar para produzi-los. Na verdade, eu fiz rodar todos os dias, com data final = data de hoje por padrão. Além disso, minha descrição requer data final 1 = data inicial 2 menos 1 dia.
Robert Carnegie
1

Dei uma resposta aqui que é relativamente simples e não requer extensões especiais para o banco de dados (portanto, funcionará com qualquer banco de dados).

TommCatt
fonte