Se capisco lo scenario in modo appropriato, è necessario definire una tabella che conserva una serie temporale dei prezzi ; pertanto, sono d'accordo, questo ha molto a che fare con l' aspetto temporale del database con cui stai lavorando.
Regole di business
Cominciamo ad analizzare la situazione dal livello concettuale. Quindi, se , nel tuo dominio aziendale,
- un prodotto viene acquistato in uno-a-molti prezzi ,
- ogni Prezzo di acquisto diventa Corrente a una Data di inizio esatta e
- il prezzo DataFine (che indica la data in cui un prezzo cessa di essere attuale ) è uguale alla DataInizio del immediatamente successivo Price ,
allora questo significa che
- non vi sono spazi vuoti tra i periodi distinti durante i quali i prezzi sono correnti (la serie temporale è continua o congiunta ) e
- la Data di fine di un prezzo è un dato derivabile.
Il diagramma IDEF1X mostrato in Figura 1 , sebbene altamente semplificato, descrive un simile scenario:
Layout logico dell'esposizione
E la seguente progettazione a livello logico SQL-DDL, basata su detto diagramma IDEF1X, illustra un approccio fattibile che è possibile adattare alle proprie esigenze esatte:
-- 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)
);
La Price
tabella ha una CHIAVE PRIMARIA composita composta da due colonne, ovvero ProductNumber
(vincolata, a sua volta, come CHIAVE ESTERA che fa riferimento Product.ProductNumber
) e StartDate
(sottolineando la Data particolare in cui un determinato Prodotto è stato acquistato ad un Prezzo specifico ) .
Nel caso in cui i Prodotti vengano acquistati a Prezzi diversi nello stesso Giorno , anziché nella StartDate
colonna, è possibile includerne uno etichettato in quanto StartDateTime
mantiene l' Istantaneo quando un determinato Prodotto è stato acquistato ad un Prezzo esatto . Il PRIMARY KEY dovrebbe quindi essere dichiarato come (ProductNumber, StartDateTime)
.
Come dimostrato, la tabella di cui sopra è ordinaria, poiché è possibile dichiarare le operazioni SELECT, INSERT, UPDATE e DELETE per manipolare direttamente i suoi dati, quindi (a) consente di evitare l'installazione di componenti aggiuntivi e (b) può essere utilizzato in tutti le principali piattaforme SQL con alcune modifiche, se necessario.
Campioni di manipolazione dei dati
Per esemplificare alcune operazioni di manipolazione che sembrano utili, diciamo che hai INSERITO i seguenti dati nelle tabelle Product
e Price
, rispettivamente:
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);
Dato che Price.EndDate
è un punto dati derivabile, è necessario ottenerlo tramite, precisamente, una tabella derivata che può essere creata come vista al fine di produrre le serie temporali "complete", come esemplificato di seguito:
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;
Quindi la seguente operazione che SELEZIONA direttamente da quella vista
SELECT ProductNumber,
ProductEtcetera,
PriceAmount,
StartDate,
EndDate
FROM PriceWithEndDate
ORDER BY StartDate DESC;
fornisce il prossimo set di risultati:
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.
Ora, supponiamo che tu sia interessato a ottenere tutti i Price
dati per l' Product
identificazione principale entro il ProductNumber
1750 del Date
2 giugno 2017 . Vedendo che Price
un'asserzione (o riga) è corrente o effettiva durante l'intero intervallo che va da (i) suo StartDate
a (ii) suo EndDate
, quindi questa operazione 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.
restituisce il set di risultati che segue
ProductNumber ProductEtcetera PriceAmount StartDate EndDate
------------- ------------------ ----------- ---------- ----------
1750 Price time series… 1000 2017-06-01 2017-06-03
che risponde a tale requisito.
Come mostrato, la PriceWithEndDate
vista gioca un ruolo fondamentale nell'ottenere la maggior parte dei dati derivabili e può essere SELEZIONATA DA in un modo abbastanza ordinario.
Tenendo conto del fatto che la tua piattaforma preferita è PostgreSQL, questo contenuto del sito di documentazione ufficiale contiene informazioni su viste "materializzate" , che possono aiutare a ottimizzare la velocità di esecuzione mediante meccanismi a livello fisico, se detto aspetto diventa problematico. Altri sistemi di gestione di database SQL (DBMS) offrono strumenti fisici molto simili, anche se è possibile applicare una terminologia diversa, ad esempio viste "indicizzate" in Microsoft SQL Server.
Puoi vedere gli esempi di codice DDL e DML discussi in azione in questo violino db <> e in questo violino SQL .
Risorse correlate
In queste domande e risposte discutiamo di un contesto aziendale che include le modifiche dei prezzi dei prodotti ma ha un ambito più ampio, quindi potresti trovarlo di interesse.
Questi post Stack Overflow coprono punti molto rilevanti riguardo al tipo di colonna che contiene un dato di valuta in PostgreSQL.
Risposte ai commenti
Sembra simile al lavoro che ho fatto, ma ho trovato molto più conveniente / efficiente lavorare con una tabella in cui un prezzo (in questo caso) ha una colonna di data di inizio e una colonna di fine - quindi stai solo cercando righe con targetdate > = startdate e targetdate <= enddate. Naturalmente, se i dati non sono archiviati con quei campi (inclusa la data di fine del 31 dicembre 9999, non Null, dove non esiste una data di fine effettiva), allora dovresti fare un lavoro per produrlo. In realtà l'ho fatto funzionare ogni giorno, con la data di fine = la data di oggi per impostazione predefinita. Inoltre, la mia descrizione richiede enddate 1 = startdate 2 meno 1 giorno. - @Robert Carnegie , il 22/06/2017 20: 56: 01Z
Il metodo che propongo sopra affronta un dominio aziendale delle caratteristiche precedentemente descritte , applicando di conseguenza il tuo suggerimento di dichiarare EndDate
come una colonna - che è diversa da un "campo" - della tabella di base denominata Price
implicherebbe che la struttura logica del database sarebbe non riflettere correttamente lo schema concettuale e uno schema concettuale deve essere definito e riflesso con precisione, inclusa la differenziazione di (1) informazioni di base da (2) informazioni derivabili .
A parte ciò, una tale linea di condotta introdurrebbe una duplicazione, poiché la si EndDate
potrebbe quindi ottenere in virtù di (a) una tabella derivabile e anche in virtù di (b) la tabella di base denominata Price
, con la EndDate
colonna quindi duplicata . Sebbene questa sia una possibilità, se un professionista decide di seguire tale approccio, dovrebbe avvertire in modo deciso gli utenti del database degli inconvenienti e delle inefficienze che comporta. Uno di questi inconvenienti e inefficienze è, ad esempio, l'urgente necessità di sviluppare un meccanismo che assicuri, in ogni momento , che ciascun Price.EndDate
valore sia uguale a quello della Price.StartDate
colonna della riga immediatamente successiva per il Price.ProductNumber
valore a portata di mano.
Al contrario, il lavoro per produrre i dati derivati in questione come ho presentato, onestamente, non è affatto speciale ed è necessario per (i) garantire la corretta corrispondenza tra i livelli logici e concettuali di astrazione del database e (ii ) assicurano l'integrità dei dati, entrambi aspetti che, come osservato in precedenza, sono decisamente di grande importanza.
Se l'aspetto di efficienza di cui stai parlando è legato alla velocità di esecuzione di alcune operazioni di manipolazione dei dati, allora deve essere gestito nel luogo appropriato, cioè a livello fisico, tramite, ad esempio, una strategia di indicizzazione vantaggiosa, basata su (1 ) le particolari tendenze della query e (2) i particolari meccanismi fisici forniti dal DBMS di utilizzo. Altrimenti, sacrificare la mappatura concettuale-logica appropriata e compromettere l'integrità dei dati coinvolti trasforma facilmente un sistema solido (cioè un bene organizzativo prezioso) in una risorsa non affidabile.
Serie temporali discontinue o disgiunte
D'altra parte, ci sono circostanze in cui il mantenimento EndDate
di ciascuna riga in una tabella delle serie temporali non è solo più conveniente ed efficiente ma anche richiesto , sebbene ciò dipenda interamente dai requisiti specifici dell'ambiente di lavoro. Un esempio di quel tipo di circostanze si presenta quando
- sia le informazioni StartDate che EndDate sono conservate prima (e conservate tramite) ogni INSERTion e
- possono esserci degli spazi vuoti nel mezzo di periodi distinti durante i quali i prezzi sono correnti (ovvero, le serie temporali sono discontinue o disgiunte ).
Ho rappresentato questo scenario nel diagramma IDEF1X mostrato in Figura 2 .
In tal caso, sì, la Price
tabella ipotetica deve essere dichiarata in modo simile al seguente:
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 sì, quel design logico DDL semplifica l'amministrazione a livello fisico, perché è possibile elaborare una strategia di indicizzazione che comprenda la EndDate
colonna (che, come mostrato, è dichiarata in una tabella di base) in configurazioni relativamente più semplici .
Quindi, un'operazione SELEZIONA come quella qui sotto
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';
può essere utilizzato per ricavare tutti i Price
dati per quelli Product
identificati principalmente entro il ProductNumber
1750 il Date
2 giugno 2017 .
prices
creare una tabellaprices_history
con colonne simili. Hibernate Envers può automatizzare questo per te