Comment puis-je suivre tous les changements de prix dans une base de données afin d'obtenir le prix du produit «x» à la date «y»

8

J'aurais besoin de suivre les changements de prix d'un produit afin de pouvoir interroger la base de données pour un prix de produit à une date donnée. Les informations sont utilisées dans un système qui calcule les audits historiques de sorte qu'il doit renvoyer le prix correct pour le produit correct en fonction de la date d'achat.

Je préférerais utiliser des postgres dans la construction de la base de données.

J'ai besoin de la conception de la base de données mais toutes les suggestions de meilleures pratiques sont également les bienvenues.

Gunnar Norred
la source
1
copie sur écriture dans une autre table. Si table est pricescréer une table prices_historyavec des colonnes similaires. Hibernate Envers peut automatiser cela pour vous
Neil McGuigan

Réponses:

11

Si je comprends bien le scénario, vous devez définir une table qui conserve une série chronologique de prix ; par conséquent, je suis d'accord, cela a beaucoup à voir avec l' aspect temporel de la base de données avec laquelle vous travaillez.

Règles métier

Commençons par analyser la situation au niveau conceptuel. Donc, si , dans votre domaine d'activité,

  • un produit est acheté à un à de nombreux prix ,
  • chaque prix d'achat devient actuel à une date de début exacte , et
  • le prix EndDate (qui indique la date où un prix cesse d'être actuelle ) est égale à la date de début de la immédiatement après le prix ,

alors cela signifie que

  • il n'y a pas d' écart entre les périodes distinctes pendant lesquelles les prix sont courants (la série chronologique est continue ou conjointe ), et
  • la EndDate d'un prix est une donnée dérivable.

Le diagramme IDEF1X illustré à la figure 1 , bien que très simplifié, illustre un tel scénario:

Fig.1 - Prix des produits Diagramme IDEF1X simplifié - Scénario A

Disposition logique d'exposition

Et la conception de niveau logique SQL-DDL suivante, basée sur ledit diagramme IDEF1X, illustre une approche réalisable que vous pouvez adapter à vos propres besoins exacts:

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

Le Pricetableau a une CLÉ PRIMAIRE composite composée de deux colonnes, c'est-à-dire ProductNumber(contrainte, à son tour, comme une CLÉ ÉTRANGÈRE qui fait référence à Product.ProductNumber) et StartDate(indiquant la Date particulière à laquelle un certain Produit a été acheté à un Prix spécifique ) .

Dans le cas où les produits sont achetés à des prix différents au cours du même jour , au lieu de la StartDatecolonne, vous pouvez en inclure un étiqueté comme StartDateTimequi conserve l' instantané lorsqu'un produit donné a été acheté à un prix exact . La CLÉ PRIMAIRE devrait alors être déclarée comme (ProductNumber, StartDateTime).

Comme démontré, le tableau susmentionné est un tableau ordinaire, car vous pouvez déclarer des opérations SELECT, INSERT, UPDATE et DELETE pour manipuler directement ses données, ce qui (a) permet d'éviter l'installation de composants supplémentaires et (b) peut être utilisé dans tous les les principales plates-formes SQL avec quelques ajustements, si nécessaire.

Exemples de manipulation de données

Pour illustrer certaines opérations de manipulation qui semblent utiles, disons que vous avez INSÉRÉ respectivement les données suivantes dans les tableaux Productet Price:

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

Étant donné que le Price.EndDateest un point de données dérivable, vous devez alors l'obtenir via, précisément, une table dérivée qui peut être créée sous forme de vue afin de produire la série temporelle «complète», comme illustré ci-dessous:

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;

Ensuite, l'opération suivante qui sélectionne directement à partir de cette vue

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

fournit le jeu de résultats suivant:

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.

Supposons maintenant que vous souhaitez obtenir l'intégralité des Pricedonnées pour le Productprincipal identifié par ProductNumber 1750 le Date 2 juin 2017 . Voyant qu'une Priceassertion (ou ligne) est actuelle ou effective pendant tout l' intervalle qui va de (i) son StartDateà (ii) son EndDate, alors cette opération 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.

donne l'ensemble de résultats qui suit

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

qui répond à cette exigence.

Comme indiqué, la PriceWithEndDatevue joue un rôle primordial dans l'obtention de la plupart des données dérivables, et peut être sélectionnée à partir d'une manière assez ordinaire.

Étant donné que votre plate-forme de préférence est PostgreSQL, ce contenu du site de documentation officiel contient des informations sur les vues «matérialisées» , qui peuvent aider à optimiser la vitesse d'exécution au moyen de mécanismes de niveau physique, si cet aspect devient problématique. D'autres systèmes de gestion de bases de données SQL (SGBD) offrent des instruments physiques très similaires, bien que des terminologies différentes puissent être appliquées, par exemple, des vues «indexées» dans Microsoft SQL Server.

Vous pouvez voir les exemples de code DDL et DML discutés en action dans ce violon db <> et dans ce violon SQL .

Ressources associées

  • Dans ce Q & A, nous discutons d'un contexte commercial qui inclut les changements de prix des produits mais a une portée plus étendue, vous pouvez donc le trouver intéressant.

  • Ces publications Stack Overflow couvrent des points très pertinents concernant le type de colonne contenant une donnée de devise dans PostgreSQL.

Réponses aux commentaires

Cela ressemble au travail que j'ai fait, mais j'ai trouvé qu'il était beaucoup plus pratique / efficace de travailler avec une table où un prix (dans ce cas) a une colonne startdate et une colonne enddate - vous recherchez donc simplement des lignes avec targetdate > = date de début et date cible <= date de fin. Bien sûr, si les données ne sont pas stockées avec ces champs (y compris la date de fin du 31 décembre 9999, pas Null, où aucune date de fin réelle n'existe), vous devrez alors travailler pour les produire. En fait, je l'ai fait fonctionner tous les jours, avec date de fin = date d'aujourd'hui par défaut. De plus, ma description requiert la date de fin 1 = la date de début 2 moins 1 jour. - @Robert Carnegie , le 2017-06-22 20: 56: 01Z

La méthode que je propose ci-dessus concerne un domaine d'activité des caractéristiques décrites précédemment , en appliquant par conséquent votre suggestion de déclarer le en EndDatetant que colonne - qui est différente d'un «champ» - de la table de base nommée Priceimpliquerait que la structure logique de la base de données serait ne pas refléter correctement le schéma conceptuel, et un schéma conceptuel doit être défini et reflété avec précision, y compris la différenciation (1) des informations de base des (2) informations dérivables .

En dehors de cela, une telle ligne de conduite introduirait une duplication, car elle EndDatepourrait alors être obtenue en vertu de (a) une table dérivable et également en vertu de (b) la table de base nommée Price, avec la EndDatecolonne donc dupliquée . Bien que cela soit une possibilité, si un praticien décide de suivre cette approche, il ou elle devrait décidément avertir les utilisateurs de la base de données des inconvénients et des inefficacités que cela implique. L'un de ces inconvénients et inefficacités est, par exemple, le besoin urgent de développer un mécanisme qui garantit, à tout moment , que chaque Price.EndDatevaleur est égale à celle de la Price.StartDatecolonne de la ligne immédiatement successive pour la Price.ProductNumbervaleur en question.

En revanche, le travail pour produire les données dérivées en question, comme je l'ai avancé, n'est honnêtement pas spécial du tout et est nécessaire pour (i) garantir la correspondance correcte entre les niveaux d'abstraction logiques et conceptuels de la base de données et (ii ) garantissent l'intégrité des données, deux aspects qui, comme indiqué précédemment, sont décidément d'une grande importance.

Si l'aspect d'efficacité dont vous parlez est lié à la vitesse d'exécution de certaines opérations de manipulation de données, alors il doit être géré à l'endroit approprié, c'est-à-dire au niveau physique, via, par exemple, une stratégie d'indexation avantageuse, basée sur (1 ) les tendances de requête particulières et (2) les mécanismes physiques particuliers fournis par le SGBD d'utilisation. Sinon, sacrifier le mappage conceptuel-logique approprié et compromettre l'intégrité des données impliquées transforme facilement un système robuste (c'est-à-dire un précieux actif organisationnel) en une ressource non fiable.

Séries chronologiques discontinues ou discontinues

D'un autre côté, il existe des circonstances dans lesquelles le maintien EndDatede chaque ligne dans un tableau de séries chronologiques est non seulement plus pratique et efficace, mais aussi exigé , bien que cela dépende entièrement des exigences spécifiques à l'environnement commercial, bien sûr. Un exemple de ce genre de circonstances survient lorsque

  • à la fois la date de début et les EndDate éléments d'information ont lieu avant (et conservés via) chaque insertion, et
  • il peut y avoir des lacunes au milieu des périodes distinctes pendant lesquelles les prix sont courants (c'est-à-dire que la série chronologique est discontinue ou disjointe ).

J'ai représenté ledit scénario dans le diagramme IDEF1X illustré à la figure 2 .

Fig.2 - Prix des produits Diagramme IDEF1X simplifié - Scénario B

Dans ce cas, oui, le Pricetableau hypothétique doit être déclaré d'une manière similaire à ceci:

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

Et, oui, cette conception DDL logique simplifie l'administration au niveau physique, car vous pouvez mettre en place une stratégie d'indexation qui englobe la EndDatecolonne (qui, comme illustré, est déclarée dans une table de base) dans des configurations relativement plus faciles .

Ensuite, une opération SELECT comme celle ci-dessous

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

peut être utilisé pour dériver l'ensemble des Pricedonnées pour les Productprincipales identifiées par ProductNumber 1750 le Date 2 juin 2017 .

MDCCL
la source
Cela ressemble au travail que j'ai fait, mais j'ai trouvé qu'il était beaucoup plus pratique / efficace de travailler avec une table où un prix (dans ce cas) a une colonne startdate et une colonne enddate - vous recherchez donc simplement des lignes avec targetdate > = date de début et date cible <= date de fin. Bien sûr, si les données ne sont pas stockées avec ces champs (y compris la date de fin du 31 septembre 9999, pas Null, où aucune date de fin réelle n'existe), alors vous devrez travailler pour les produire. En fait, je l'ai fait fonctionner tous les jours, avec date de fin = date d'aujourd'hui par défaut. De plus, ma description requiert la date de fin 1 = la date de début 2 moins 1 jour.
Robert Carnegie
4

Je crois que vous allez vouloir regarder les tables temporelles . Ceux-ci fournissent des fonctionnalités pour faire exactement ce que vous cherchez et sont disponibles dans Postgres avec les extensions appropriées.

Ce concept semble également être assez indépendant de la base de données, car il est proposé sur une variété de plates-formes RDBMS .

John Eisbrener
la source
1

J'ai donné ici une réponse qui est relativement simple et ne nécessite aucune extension spéciale à la base de données (fonctionnera donc avec n'importe quelle base de données).

TommCatt
la source