Come tracciare tutte le variazioni di prezzo in un db per ottenere il prezzo del prodotto 'x' alla data 'y'


8

Avrei bisogno di tenere traccia delle variazioni del prezzo di un prodotto in modo da poter interrogare il db per un prezzo del prodotto in una determinata data. Le informazioni vengono utilizzate in un sistema che calcola gli audit storici, quindi devono restituire il prezzo corretto per il prodotto corretto in base alla data di acquisto.

Preferirei usare Postgres nella costruzione del db.

Ho bisogno della progettazione del database, ma anche tutti i suggerimenti relativi alle migliori pratiche sono i benvenuti.


1
copia su scrivi in ​​un'altra tabella. Se la tabella è pricescreare una tabella prices_historycon colonne simili. Hibernate Envers può automatizzare questo per te
Neil McGuigan il

Risposte:


11

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:

Fig. 1 - Diagramma IDEF1X dei prezzi dei prodotti semplificato - Scenario A

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 Pricetabella 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 StartDatecolonna, è possibile includerne uno etichettato in quanto StartDateTimemantiene 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 Producte 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 Pricedati per l' Productidentificazione principale entro il ProductNumber 1750 del Date 2 giugno 2017 . Vedendo che Priceun'asserzione (o riga) è corrente o effettiva durante l'intero intervallo che va da (i) suo StartDatea (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 PriceWithEndDatevista 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 EndDatecome una colonna - che è diversa da un "campo" - della tabella di base denominata Priceimplicherebbe 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 EndDatepotrebbe quindi ottenere in virtù di (a) una tabella derivabile e anche in virtù di (b) la tabella di base denominata Price, con la EndDatecolonna 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.EndDatevalore sia uguale a quello della Price.StartDatecolonna della riga immediatamente successiva per il Price.ProductNumbervalore 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 EndDatedi 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 .

Fig. 2 - Diagramma IDEF1X dei prezzi dei prodotti semplificato - Scenario B

In tal caso, sì, la Pricetabella 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 EndDatecolonna (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 Pricedati per quelli Productidentificati principalmente entro il ProductNumber 1750 il Date 2 giugno 2017 .


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. Ovviamente, se i dati non sono archiviati con quei campi (incluso il 31 dicembre Trump settembre 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 di default. Inoltre, la mia descrizione richiede enddate 1 = startdate 2 meno 1 giorno.
Robert Carnegie,


1

Ho dato una risposta qui che è relativamente semplice e non richiede estensioni speciali al database (quindi funzionerà con qualsiasi database).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.