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:
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 Price
tabela 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 StartDate
coluna, você pode incluir um rotulado para StartDateTime
manter 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 Product
e 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 Price
dados dos Product
principais identificados até ProductNumber
1750 em Date
2 de junho de 2017 . Vendo que uma Price
asserção (ou linha) é atual ou efetiva durante todo o intervalo que vai de (i) StartDate
a (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 PriceWithEndDate
exibiçã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 EndDate
como uma coluna (que é diferente de um "campo") da tabela base nomeada Price
implicaria 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 EndDate
poderia ser obtida em virtude de (a) uma tabela derivável e também em virtude de (b) a tabela base denominada Price
, com a EndDate
coluna 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.EndDate
valor seja igual ao da Price.StartDate
coluna da linha imediatamente sucessiva para o Price.ProductNumber
valor 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 EndDate
de 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 .
Nesse caso, sim, a Price
tabela 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 EndDate
coluna (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 Price
dados para os Product
primariamente identificados até ProductNumber
1750 em Date
2 de junho de 2017 .
prices
crie uma tabelaprices_history
com colunas semelhantes. Hibernate Envers pode automatizar isso para você