Modelo de datos temporales (o bi-temporales).


Imaginemos una aplicación de gestión de ventas de una tienda en línea que utiliza una base de datos relacional para almacenar su información. Esta aplicación tiene una tabla "Ventas" que almacena información sobre las ventas realizadas en la tienda. La tabla tiene las siguientes columnas:

VENTAS
* VentasID      Ventas ID           Numeric (8)
  VentasFecha   Ventas Fecha        Date
  ClienteID     Cliente ID          Character (10)
  VentasImporte Importe de la venta Numeric (10.2)

CLIENTE
* ClienteID     Cliente ID          Character (10)
  ClienteNombre Cliente Nombre      VarChar (100)
  CiudadID      Ciudad ID           Character (10)

La aplicación tiene la capacidad de registrar las ventas que se realizan, incluyendo la fecha en que se llevan a cabo, y también puede realizar consultas sobre las ventas registradas en la base de datos. Sin embargo, la aplicación no refleja el tiempo en el modelo de datos para las entidades relacionadas con la venta, como los clientes. 

Por ejemplo, si un cliente se muda de cuidad después de realizada una compra, la aplicación no registra esta información histórica en la base de datos. La información del cliente se actualiza en la base de datos en tiempo real, y se sobrescribe la información anterior. 

Para seguir con el ejemplo sencillo, supongamos que tenemos 

Si tenemos los clientes, ciudades y ventas

VENTAS
2023-01-01 -EL_ECO     -    1.000
2023-02-01 -TELEGRAFO  -    5.000
2023-03-01 -TELEGRAFO  -    2.000

CLIENTES
EL_ECO     - PALMIRA    - EL ECO DE PALMIRA
TELEGRAFO  - PAYSANDU   - EL TELEGRAFO

y podemos hacer un reporte de

TOTAL POR CIUDAD
PALMIRA   :      1.000
PAYSANDU  :      7.000
RIVERA    :          0

En un momento el cliente TELEGRAFO se muda a RIVERA.

CLIENTES
EL_ECO     - PALMIRA    - EL ECO DE PALMIRA
TELEGRAFO  - RIVERA     - EL TELEGRAFO

el reporte de totales por ciudad, va a ser de la siguiente forma:

TOTAL POR CIUDAD
PALMIRA   :      1.000
PAYSANDU  :          0
RIVERA    :      7.000

En resumen, aunque la aplicación puede manejar el tiempo para los hechos relacionados con las ventas (como la fecha de la venta), no refleja el tiempo en el modelo de datos para las entidades relacionadas con la venta (como los clientes y los productos). Como resultado, la aplicación no puede realizar un seguimiento histórico completo de la información relacionada con las ventas y sus entidades relacionadas.

Este funcionamiento, puede ser aceptable o no en la aplicación.  Antes era casi la única forma de hacerlo, pero cada vez mas los clientes tienen más requerimientos para que los reportes de ventas pasadas, no se modifiquen por cambios presentes en las entidades relacionadas con la venta. 

Existen varias soluciones posibles (o las mas usadas, al menos)

1) Data warehouse

La opción mas sencilla, es tener una data warehouse, donde se carguen las ventas en forma periódica, manteniendo redundancia en la DW. La aplicación y la base de datos operacional es una foto actual de los datos y guardamos la historia en la DW. Para esto, hay muchos estudios sobre dimensiones que cambian (Slowly Changing Dimensions

Ventajas: 
Es una forma conocida y probada de resolverlo. 

Desventajas:
Hay que hacer programas de carga de la DW. Hay que duplicar el modelo de datos y mantener la infraestructura de la misma. Los reportes históricos deben hacerse en la DW y los operacionales en otra base de datos, lo cual puede ser molesto y confuso para los usuarios.

También es común que los reportes de la DW tengan algún retraso en la carga de los datos, que pueden dificultar la toma de decisiones. 

2) Gestión de datos históricos

Usar herramientas de las base de datos, para guardar datos históricos, que sirve para la auditoria. La mayoría de las base de datos, soportan en forma parcial el standard SQL:2011 que incluyen soporte temporales en SQL y poder consultar el estado de tablas en el pasado (no soporta el futuro). 

Tengo esperanza que Genexus permita definir tablas como SYSTEM_VERSIONING en el futuro, y esto facilitaría un poco este tipo de escenarios.

3) Incorporar manejo temporal al modelo de datos temporales (o bi-temporales).

 Una forma que recomiendan en la bibliografia, es la utilización de un rango de vigencia de los datos, por ejemplo, para la tabla de CLIENTES tendríamos

CLIENTE
* ClienteID     Cliente ID          Character (10)
* Cliente_FechaInicial              Date
  Cliente_FechaFinal                Date
  ClienteNombre Cliente Nombre      VarChar (100)
  CiudadID      Ciudad ID           Character (10)

Modificando la clave de clientes y con eso podemos guardar en una única tabla todos los datos históricos de clientes.  

Para poder consultar dicha tabla, siempre hay que ir con una fecha (con hoy, veo el registro actual). 
Tiene como contra, que no se pueden utilizar funcionalidades de la base de datos, como integridad referencial y también se complica bastante la realización de joins. 

Otro de los problemas, es que hay que controlar que los rangos de fechas de vigencia, sean coherentes. Siempre la FechaInicial debe ser menor igual que la FechaFinal. 
Además los intervalos de tiempos entre diferentes registros no se deberían superponer.

Dar de alta un cliente, es agregar un registro a la tabla de clientes. 

Actualizar un cliente, es cerrar la vigencia de la versión anterior del cliente y dar de alta un nuevo registro de cliente con una nueva vigencia. 

Dar de baja un cliente, alcanza con poner la fecha de finalización la vigencia, a la fecha que se haga 

Para poder utilizarlo correctamente es necesario manejar patrones de desarrollo para no ocasionar problemas. 

4) Modelo Mixto (Dimensiones con Revisiones)

Uno modelo que me ha servido en algunas oportunidades es tener un modelo mixto. La entidad de clientes seria modelada con dos tablas. 

CLIENTE
* ClienteID     Cliente ID          Character (10)
  ClienteNombre Cliente Nombre      VarChar (100)

  CLIENTE_REVISION
  * Cliente_RevisionID                Number (8)
    Cliente_FechaInicial              Date
  
  Cliente_FechaFinal                Date
    CiudadID      Ciudad ID           Character (10)

El nombre del cliente, puede ir en la tabla de arriba o en la revision, dependiendo si considera que el nombre del cliente es significativo. Para algunas entidades puedes ser bueno dejarlo junto a la clave (o sea no llevar historia de ese campo) y para otras se va a necesitar llevar la revisiones de ese atributo tambien. 

Las ventajas de este modelo, es que se tiene integridad referencial en la base de datos, en los casos que se deseen. 

Cuando se realiza una venta, se va a buscar cual es la revisión correspondiente a dicha venta (con una condicion Cliente_FechaInicial <= FechaVenta y FechaVenta < Cliente_FechaFinal. 

Una vez encontrada la revision, se guarda dicha revisión en la tabla de ventas y a partir de ahi, fiunciona correctamente el join entre Ventas y Cliente_revision y tambien se puede delegar a la base de datos la integridad referencial. 

La tabla de Ventas (con manejo de tiempo en las dimensiones quedaría)

VENTAS

* VentasID Numeric (8)
  VentasFecha Date
     ClienteID Character (10)
  Cliente_RevisionId Numeric (8)
  
  VentaImporte Numeric (10.2)

Por cada dimensión que necesite manejo temporal, debo agregar un numero de revision, que debe calcularse en el momento de generar el hecho (en este caso, la venta). 

La alternativa que manejan en los libros, es el utilizar rangos de fechas y siempre manejar la fecha de la venta, para ir a buscar cual es la revisión correspondiente, pero eso es mucho mas costoso desde el punto de vista computacional, para sistemas que graban una vez y luego son leidos muchas veces. 

Por ejemplo, en este caso, se realiza la venta y se grabaría el registro y luego dicho registro va a ser consultado muchas veces para consultas, impresiones, etc. 

Ventajas:

Utilización de la base de datos relacional para manejo de integridad referencial y joins. 

Manejo relativamente sencillo con Genexus para el manejo de datos temporales en dimensiones. 

El impacto en el rendimiento y en el almacenamiento no es muy grande. 

Desventajas

Existen tablas adicionales, que al ser accedida deberán ser filtradas por una fecha (ej. ClienteRevision)

Hay que validar en el mantenimiento de datos básicos, que los periodos de validez de las revisiones sean coherentes. 

Las modificaciones de datos en el pasado, pueden necesitar ajustes (cambios en la revisión en los hechos).  Por ejemplo, si detecto que una revisión anterior, ya fue usada en una venta, pero tenia algun dato equivocado, puedo necesitar crear nuevas revisiones y cambiar la revisión en la tabla de Ventas. 

El borrado de dimensiones (en este caso CLIENTES) no puede ser físico, sino que hay que agregar una flag o una fecha de borrado, pues siempre tendrá referencias. 

La tablas de hechos van a quedar con tantos campos de revisión, como dimensiones temporales haga referencia la tabla, lo cual puede hacer un poco mas incomodo el hacer consultas manuales sobre las tablas. 

En el caso de la ejecución anterior

VENTAS
2023-04-21 -ELECO      -    1000.00
2023-03-22 -TELEGRAFO  -    5000.00
2023-05-01 -TELEGRAFO  -    2000.00

CLIENTES usando fecha 2023-05-01
ELECO      - PALMIRA    - EL ECO DE PALMIRA
TELEGRAFO  - RIVERA     - EL TELEGRAFO
--
TOTAL POR CIUDAD
PALMIRA   :      1000
PAYSANDU  :      7000
RIVERA    :         0

A pesar que el cliente TELEGRAFO esta ahora en RIVERA, las ventas siguen reflejando que se hicieron en PAYSANDU, que era lo que queriamos, que no cambiara de ciudad las ventas realizadas en el pasado. 



Lo unico que hay que agregar es lo que esta marcado en rojo. 


Estoy buscando contraejemplos, donde este modelo tenga problemas, pero por ahora no los estoy encontrando. 

Existen otras soluciones (por ejemplo guardar redundante en los hechos las dimensiones que se quieran o usar dos rangos de fechas (uno para la fecha en que se realiza la operación y otro rango de fechas para la validez de dicha información).  Queda bastante mas complejo el modelo, aunque permite corrección de datos en el pasado. 


Conclusión

A medida que los sistemas se hacen mas críticos y abarcan cada vez mas actividades de la vida cotidiana, vamos a tener que poder modelar mejor como evolucionan los mismos. Los modelos de datos actuales, no reflejan bien los cambios que suceden y seria bueno que los proveedores de herramientas (base de datos, herramientas de desarrollo, frameworks, etc) provean las componentes basicos para poder hacer mas fácil estos desarrollos. 
Por ejemplo, seria bueno poder contar con tipos de datos básicos como PERIODOS (del tipo fecha inicial y fecha final) y sus operaciones como chequear que una fecha este en un periodo o que dos periodos se superponen o se contienen, etc. Las bases de datos están incorporando a un ritmo lento estas funcionalidades. Espero que en el futuro serán mas comunes. 

En particular, con GeneXus, me gustaría contar con un Pattern de diseño, que permita mantener este tipo de dimensiones con soporte temporal, validando las operaciones de alta/baja y modificaciones y que brinde las funciones necesarias para buscar las revisiones por fechas. 

Si contáramos con la posibilidad de manejo de periodos como un tipo basico, tambien seria un poco mas sencilla de entender la programación, el desplegar en pantalla, usar un UC para el ingreso de rangos de fechas, etc.

También se puede agregar validaciones a la KB, que avise si existe un programa que acceda a la tabla de ClienteRevision, y no filtra por la clave primaria o no toma en cuenta el rango de fecha.  Puede ser valido, en caso que quiera listar todos las revisiones, pero serian las excepciones.  

Nota: En los ejemplos, use Fecha para elegir algo sencillo, pero los periodos o rangos dependen de la precisión que necesite la aplicación. Pueden ser años, meses, días, días/horas, milisegundos, etc

Links útiles

UPDATE 

Como dice Pablo en los comentarios, también se pueden usar Dynamic Transactions. Con mis pruebas, no me fue muy bien, pero tal vez estaba haciendo algo mal. Voy a revisar y hago otro post comparando las soluciones. 

Comentarios

  1. Como lo modelarias en este caso?
    Las que se me han ocurrido, terminan teniendo atributos que son iguales pero con nombres diferentes y no me gusta demasiado porque confunden. Seguramente existan mejores implementaciones que no encontré aun.

    ResponderBorrar
  2. Hola, en casos en que necesito registrar datos que pueden cambiar en el tiempo como el ejemplo que indicas (sin crear redundancia), agrego un campo longvarchar donde guardo un json con los campos variables que pueden ir apareciendo en el tiempo. Luego para leer cargo todo en una estructura (sdt) que luego ordeno como se necesite.

    Saludos

    Leonardo Zepeda A.

    ResponderBorrar
    Respuestas
    1. Leonardo, esta solucion es buena para agregar campos variables.
      No me doy cuenta como puede ampliarse para el manejo de datos temporales, sin complicar mucho la cosa.

      Borrar

Publicar un comentario

1) Lee el post
2) Poné tu opinión sobre el mismo.
Todos los comentarios serán leidos y la mayoría son publicados.

Entradas más populares de este blog

La nefasta influencia del golero de Cacho Bochinche en el fútbol uruguayo

Aplicación monolítica o distribuida?

Funcionalidades de GeneXus que vale la pena conocer: DATE Constants.