¿Cómo rastrearía todos los cambios de precio en una base de datos para obtener el precio del producto 'x' en la fecha 'y'?

8

Tendría que rastrear los cambios en el precio de los productos para poder consultar en la base de datos el precio de un producto en una fecha determinada. La información se utiliza en un sistema que calcula auditorías históricas, por lo que debe devolver el precio correcto para el producto correcto según la fecha de compra.

Preferiría usar postgres en la construcción de la base de datos.

Necesito el diseño de la base de datos, pero todas y cada una de las sugerencias de mejores prácticas también son bienvenidas.

Gunnar Norred
fuente
1
copiar en escribir en otra tabla. Si la tabla es pricescrear una tabla prices_historycon columnas similares. Hibernate Envers puede automatizar esto por usted
Neil McGuigan

Respuestas:

11

Si entiendo el escenario adecuadamente, debe definir una tabla que conserve una serie temporal de precios ; por lo tanto, estoy de acuerdo, esto tiene mucho que ver con el aspecto temporal de la base de datos con la que está trabajando.

Reglas del negocio

Comencemos analizando la situación desde el nivel conceptual. Entonces, si , en su dominio comercial,

  • un producto se compra a precios de uno a muchos ,
  • cada Precio de compra se convierte en Actual en una Fecha de inicio exacta , y
  • la fecha de finalización del precio (que indica la fecha en que un precio deja de ser actual ) es igual a la fecha de inicio del precio inmediatamente posterior ,

entonces eso significa que

  • no hay brechas entre los distintos períodos durante los cuales los precios son actuales (la serie temporal es continua o conjunta ), y
  • El EndDate of a Price es un dato derivable.

El diagrama IDEF1X que se muestra en la Figura 1 , aunque está muy simplificado, representa este escenario:

Fig. 1 - Diagrama IDEF1X simplificado de los precios del producto - Escenario A

Diseño lógico expositivo

Y el siguiente diseño de nivel lógico SQL-DDL, basado en dicho diagrama IDEF1X, ilustra un enfoque factible que puede adaptar a sus propias necesidades exactas:

-- 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 Pricetabla tiene una CLAVE PRIMARIA compuesta compuesta por dos columnas, es decir, ProductNumber(restringida, a su vez, como una CLAVE EXTRANJERA que hace referencia a Product.ProductNumber) y StartDate(señalando la Fecha particular en la que se compró un determinado Producto a un Precio específico ) .

En caso de que los Productos se compren a Precios diferentes durante el mismo Día , en lugar de la StartDatecolumna, puede incluir uno etiquetado como StartDateTimeque mantiene el Instant cuando se compra un Producto determinado a un Precio exacto . La CLAVE PRIMARIA tendría que declararse como (ProductNumber, StartDateTime).

Como se demostró, la tabla mencionada es ordinaria, porque puede declarar las operaciones SELECT, INSERT, UPDATE y DELETE para manipular sus datos directamente, por lo tanto (a) permite evitar la instalación de componentes adicionales y (b) se puede utilizar en todos las principales plataformas SQL con algunos ajustes, si es necesario.

Muestras de manipulación de datos.

Para ejemplificar algunas operaciones de manipulación que parecen útiles, digamos que ha INSERTADO los siguientes datos en las tablas Producty 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);

Dado que Price.EndDatees un punto de datos derivable, debe obtenerlo a través de, precisamente, una tabla derivada que se puede crear como una vista para producir la serie temporal "completa", como se ejemplifica a continuación:

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;

Luego, la siguiente operación que SELECCIONA directamente desde esa vista

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

suministra el siguiente 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.

Ahora, supongamos que está interesado en obtener todos los Pricedatos de los Productidentificados principalmente para ProductNumber 1750 el Date 2 de junio de 2017 . Al ver que una Priceaserción (o fila) es actual o efectiva durante todo el intervalo que va desde (i) StartDatea (ii) su EndDate, entonces esta operación 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.

produce el conjunto de resultados que sigue

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

que aborda dicho requisito.

Como se muestra, la PriceWithEndDatevista juega un papel primordial en la obtención de la mayoría de los datos derivables, y se puede SELECCIONAR DESDE de una manera bastante ordinaria.

Teniendo en cuenta que su plataforma de preferencia es PostgreSQL, este contenido del sitio de documentación oficial contiene información sobre vistas "materializadas" , que pueden ayudar a optimizar la velocidad de ejecución mediante mecanismos de nivel físico, si dicho aspecto se vuelve problemático. Otros sistemas de administración de bases de datos SQL (DBMS) ofrecen instrumentos físicos muy similares, aunque se pueden aplicar diferentes terminologías, por ejemplo, vistas "indexadas" en Microsoft SQL Server.

Puede ver los ejemplos de código DDL y DML discutidos en acción en este db <> fiddle y en este SQL Fiddle .

Recursos Relacionados

Respuestas a comentarios

Esto se parece al trabajo que hice, pero me pareció mucho más conveniente / eficiente trabajar con una tabla donde un precio (en este caso) tiene una columna de fecha de inicio y una columna de fecha de finalización, por lo que solo está buscando filas con fecha de destino > = fecha de inicio y fecha de destino <= fecha final. Por supuesto, si los datos no se almacenan con esos campos (incluida la fecha de finalización el 31 de diciembre de 9999, no Null, donde no existe una fecha de finalización real), entonces tendría que trabajar para producirlo. De hecho, lo hice correr todos los días, con la fecha de finalización = la fecha de hoy por defecto. Además, mi descripción requiere enddate 1 = startdate 2 menos 1 día. - @Robert Carnegie , en 2017-06-22 20: 56: 01Z

El método que propongo anteriormente aborda un dominio comercial de las características descritas anteriormente , aplicando consecuentemente su sugerencia sobre declarar EndDatecomo una columna, que es diferente de un "campo", de la tabla base nombrada Priceimplicaría que la estructura lógica de la base de datos no debe reflejar el esquema conceptual correctamente, y un esquema conceptual debe definirse y reflejarse con precisión, incluida la diferenciación de (1) información base de (2) información derivable .

Aparte de eso, tal curso de acción introduciría duplicación, ya que EndDatepodría obtenerse en virtud de (a) una tabla derivable y también en virtud de (b) la tabla base nombrada Price, con la EndDatecolumna por lo tanto duplicada . Si bien esa es una posibilidad, si un profesional decide seguir dicho enfoque, debe advertir decididamente a los usuarios de la base de datos acerca de los inconvenientes e ineficiencias que conlleva. Uno de esos inconvenientes e ineficiencias es, por ejemplo, la necesidad urgente de desarrollar un mecanismo que garantice, en todo momento , que cada Price.EndDatevalor sea igual al de la Price.StartDatecolumna de la fila inmediatamente sucesiva para el Price.ProductNumbervalor en cuestión.

En contraste, el trabajo para producir los datos derivados en cuestión, como lo expuse, honestamente, no es nada especial, y se requiere para (i) garantizar la correspondencia correcta entre los niveles lógicos y conceptuales de abstracción de la base de datos y (ii ) aseguran la integridad de los datos, ambos aspectos que, como se señaló anteriormente, son decididamente de gran importancia.

Si el aspecto de eficiencia del que está hablando está relacionado con la velocidad de ejecución de algunas operaciones de manipulación de datos, entonces debe gestionarse en el lugar apropiado, es decir, a nivel físico, mediante, por ejemplo, una estrategia de indexación ventajosa, basada en (1 ) las tendencias particulares de consulta y (2) los mecanismos físicos particulares proporcionados por el DBMS de uso. De lo contrario, sacrificar el mapeo conceptual-lógico apropiado y comprometer la integridad de los datos involucrados convierte fácilmente un sistema robusto (es decir, un activo organizacional valioso) en un recurso no confiable.

Series temporales discontinuas o disjuntas

Por otro lado, hay circunstancias en las que retener la EndDatefila de cada fila en una tabla de series de tiempo no solo es más conveniente y eficiente, sino que se exige , aunque eso depende por completo de los requisitos específicos del entorno empresarial, por supuesto. Un ejemplo de ese tipo de circunstancias se produce cuando

  • tanto la información de StartDate como la de EndDate se guardan antes (y se retienen a través de) cada INSERTion, y
  • puede haber brechas en el medio de los distintos períodos durante los cuales los precios son actuales (es decir, la serie temporal es discontinua o disyuntiva ).

He representado dicho escenario en el diagrama IDEF1X que se muestra en la Figura 2 .

Fig. 2 - Diagrama de IDEF1X de precios de producto simplificado - Escenario B

En ese caso, sí, la Pricetabla hipotética debe declararse de manera similar a esto:

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

Y sí, ese diseño lógico DDL simplifica la administración a nivel físico, porque puede presentar una estrategia de indexación que abarque la EndDatecolumna (que, como se muestra, se declara en una tabla base) en configuraciones relativamente más fáciles .

Luego, una operación SELECT como la siguiente

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

puede usarse para derivar todos los Pricedatos de los Productidentificados principalmente para ProductNumber 1750 el Date 2 de junio de 2017 .

MDCCL
fuente
Esto se parece al trabajo que hice, pero me pareció mucho más conveniente / eficiente trabajar con una tabla donde un precio (en este caso) tiene una columna de fecha de inicio y una columna de fecha de finalización, por lo que solo está buscando filas con fecha de destino > = fecha de inicio y fecha de destino <= fecha final. Por supuesto, si los datos no se almacenan con esos campos (incluida la fecha de finalización el 31 de septiembre de 9999, no Null, donde no existe una fecha de finalización real), entonces tendría que trabajar para producirla. De hecho, lo hice correr todos los días, con la fecha de finalización = la fecha de hoy por defecto. Además, mi descripción requiere enddate 1 = startdate 2 menos 1 día.
Robert Carnegie
4

Creo que querrás mirar las tablas temporales . Estos proporcionan funcionalidad para hacer exactamente lo que está buscando y están disponibles en Postgres con las extensiones adecuadas.

Este concepto también parece ser bastante independiente de DB, ya que se ofrece en una variedad de plataformas RDBMS .

John Eisbrener
fuente
1

Di una respuesta aquí que es relativamente simple y no requiere extensiones especiales de la base de datos (por lo tanto, funcionará con cualquier base de datos).

TommCatt
fuente