Diseño de base de datos: ¿cómo manejar el problema del "archivo"?

18

Estoy bastante seguro de que muchas aplicaciones, aplicaciones críticas, bancos, etc., hacen esto a diario.

La idea detrás de todo eso es:

  • todas las filas deben tener un historial
  • todos los enlaces deben permanecer coherentes
  • debería ser fácil hacer solicitudes para obtener columnas "actuales"
  • Los clientes que hayan comprado cosas obsoletas aún deben ver lo que han comprado aunque este producto ya no sea parte del catálogo.

y así.

Esto es lo que quiero hacer, y explicaré los problemas que estoy enfrentando.

Todas mis tablas tendrán esas columnas:

  • id
  • id_origin
  • date of creation
  • start date of validity
  • start end of validity

Y aquí están las ideas para las operaciones CRUD:

  • crear = insertar nueva fila con id_origin= id, date of creation= ahora, start date of validity= ahora, end date of validity= nulo (= significa que es el registro activo actual)
  • actualización =
    • leer = leer todos los registros con end date of validity== nulo
    • actualizar el registro "actual" end date of validity= nulo con end date of validity= ahora
    • cree uno nuevo con los nuevos valores y end date of validity= nulo (= significa que es el registro activo actual)
  • eliminar = actualizar el registro "actual" end date of validity= nulo con end date of validity= ahora

Así que aquí está mi problema: con asociaciones de muchos a muchos. Tomemos un ejemplo con valores:

  • Tabla A (id = 1, id_origin = 1, start = now, end = null)
  • Tabla A_B (inicio = ahora, fin = nulo, id_A = 1, id_B = 48)
  • Tabla B (id = 48, id_origin = 48, start = now, end = null)

Ahora quiero actualizar la tabla A, id de registro = 1

  • Marco id de registro = 1 con end = ahora
  • Inserto un nuevo valor en la tabla A y ... maldita sea, he perdido mi relación A_B a menos que también duplique la relación ... esto terminaría en una tabla:

  • Tabla A (id = 1, id_origin = 1, start = now, end = now + 8mn)

  • Tabla A (id = 2, id_origin = 1, start = now + 8mn, end = null)
  • Tabla A_B (inicio = ahora, fin = nulo, id_A = 1, id_B = 48)
  • Tabla A_B (inicio = ahora, fin = nulo, id_A = 2, id_B = 48)
  • Tabla B (id = 48, id_origin = 48, start = now, end = null)

Y ... bueno, tengo otro problema: la relación A_B: ¿debo marcar (id_A = 1, id_B = 48) como obsoleto o no (A - id = 1 es obsoleto, pero no B - 48)?

Como lidiar con esto?

Tengo que diseñar esto a gran escala: productos, socios, etc.

¿Cuál es tu experiencia en esto? ¿Cómo lo harías (cómo lo has hecho)?

- Editar

Encontré este artículo muy interesante , pero no trata adecuadamente con la "obsolescencia en cascada" (= lo que estoy preguntando en realidad)

Olivier Pons
fuente
¿Qué tal copiar los datos del registro de actualización antes de que se actualice a uno nuevo con una nueva identificación que mantenga la lista vinculada del historial con el campo id_hist_prev. Entonces, la identificación del récord actual nunca cambia
En lugar de reinventar la rueda, ¿ha considerado usar, por ejemplo, Flashback Data Archive en Oracle?
Jack Douglas

Respuestas:

4

No tengo claro si estos requisitos son para fines de auditoría o simplemente una referencia histórica simple, como con CRM y carritos de compras.

De cualquier manera, considere tener una tabla main y main_archive para cada área principal donde se requiera. "Main" solo tendrá entradas actuales / activas, mientras que "main_archive" tendrá una copia de todo lo que entra en main. Insertar / actualizar en main_archive puede ser un desencadenante de insertar / actualizar en main. Las eliminaciones contra main_archive pueden ejecutarse a lo largo de un período de tiempo más largo, si alguna vez.

Para los problemas de referencia como Cust X compró el Producto Y, la forma más fácil de resolver su preocupación referencial de cust_archive -> product_archive es nunca eliminar entradas de product_archive. En general, la rotación debería ser mucho más baja en esa tabla, por lo que el tamaño no debería ser una gran preocupación.

HTH.


fuente
2
Gran respuesta, pero me gustaría agregar que otro beneficio de tener una tabla de archivo es que tienden a estar desnormalizados, lo que hace que informar sobre dichos datos sea mucho más eficiente. Considere también las necesidades de informes de su aplicación con este enfoque.
maple_shaft
1
En la mayoría de las bases de datos que diseño, todas las tablas 'principales' tienen un prefijo del nombre del producto LP_, y cada tabla importante tiene un equivalente LH_, con desencadenantes que insertan filas históricas en insertar, actualizar y eliminar. No funciona para todos los casos, pero ha sido un modelo sólido para las cosas que hago.
Estoy de acuerdo: si la mayoría de las consultas son para las filas "actuales", probablemente obtendrá una ventaja de rendimiento al dividir la corriente del historial en dos tablas. Una vista podría unirlos nuevamente, como una conveniencia. De esta manera, las páginas de datos con filas actuales están todas juntas y probablemente permanecen mejor en caché, y no tiene que calificar constantemente las consultas para datos actuales con lógica de fecha.
onupdatecascade
1
@onupdatecascade: tenga en cuenta que (al menos en algunos RDBMS) puede poner índices en esa UNIONvista, lo que le permite hacer cosas interesantes como aplicar una restricción única en los registros actuales e históricos.
Jon of All Trades
5 años después, he hecho toneladas de cosas y todo el tiempo te he devuelto tu idea. Lo único que cambié es que en las tablas de historial, tengo una columna " id" y " id_ref". id_refes una referencia a la idea real de la tabla. Ejemplo: personyperson_h . en person_hlo que tengo " id" y " id_ref", donde id_refse relaciona con ' person.id' para que pueda tener muchas filas con el mismo person.id(= cuando una fila de personse modifica) y todo lo idque está de todos mis cuadros son AutoInc.
Olivier Pons
2

Esto tiene cierta superposición con la programación funcional; específicamente el concepto de inmutabilidad.

Tiene una tabla llamada PRODUCTy otra llamada PRODUCTVERSIONo similar. Cuando cambia un producto, no realiza una actualización, simplemente inserta una nueva PRODUCTVERSIONfila. Para obtener la última versión, puede indexar la tabla por número de versión (desc), marca de tiempo (desc) o puede tener un indicador ( LatestVersion).

Ahora, si tiene algo que hace referencia a un producto, puede decidir a qué tabla apunta. ¿Apunta a la PRODUCTentidad (siempre se refiere a este producto) oa la PRODUCTVERSIONentidad (solo se refiere a esta versión del producto)?

Se pone complicado. ¿Qué pasa si tienes fotos del producto? Deben apuntar a la tabla de versiones, porque podrían cambiarse, pero en muchos casos, no lo harán y no querrá duplicar datos innecesariamente. Eso significa que necesita una PICTUREmesa y una relación de PRODUCTVERSIONPICTUREmuchos a muchos.


fuente
1

He implementado todas las cosas desde aquí con 4 campos que están en todas mis tablas:

  • carné de identidad
  • fecha_creación
  • date_validity_start
  • fecha_validez_final

Cada vez que un registro tiene que modificarse, lo duplico, marco el registro duplicado como "antiguo" = date_validity_end=NOW()y el actual como el bueno date_validity_start=NOW()y date_validity_end=NULL.

El truco está en las relaciones de muchos a muchos y de uno a muchos: ¡funciona sin tocarlos! Se trata de las consultas que son más complejas: para consultar un registro en una fecha precisa (= no ahora), tengo para cada combinación, y para la tabla principal, agregar esas restricciones:

WHERE (
  (date_validity_start<=:dateparam AND date_validity_end IS NULL)
  OR
  (date_validity_start<=:dateparam AND date_validity_start>=:dateparam)
)

Entonces, con productos y atributos (relación de muchos a muchos):

SELECT p.*,a.*

FROM products p

JOIN products_attributes pa
ON pa.id_product = p.id
AND (
  (pa.date_validity_start<=:dateparam AND pa.date_validity_end IS NULL)
  OR
  (pa.date_validity_start<=:dateparam AND pa.date_validity_start>=:dateparam)
)

JOIN attributes a
ON a.id = pa.id_attribute
AND (
  (a.date_validity_start<=:dateparam AND a.date_validity_end IS NULL)
  OR
  (a.date_validity_start<=:dateparam AND a.date_validity_start>=:dateparam)
)

WHERE (
  (p.date_validity_start<=:dateparam AND p.date_validity_end IS NULL)
  OR
  (p.date_validity_start<=:dateparam AND p.date_validity_start>=:dateparam)
)
Olivier Pons
fuente
0

¿Qué tal esto? Parece simple y bastante efectivo para lo que he hecho en el pasado. En su tabla de "historial", use una PK diferente. Por lo tanto, su campo "CustomerID" es el PK en su tabla Customer, pero en la tabla "history", su PK es "NewCustomerID". "CustomerID" se convierte en otro campo de solo lectura. Eso deja "CustomerID" sin cambios en el historial y todas sus relaciones permanecen intactas.

Dimondwoof
fuente
Muy buena idea. Lo que he hecho es muy similar: duplico el registro y marco el nuevo como "obsoleto" para que el registro actual siga siendo el mismo. Tenga en cuenta que quería crear un activador en cada tabla, pero mysql prohíbe las modificaciones de una tabla cuando está en un activador de esta tabla. PostGRESQL hace esto. El servidor SQL hace esto. Oracle hace esto. En resumen, MySQL todavía tiene un largo camino por recorrer, y la próxima vez lo pensaré dos veces al elegir mi servidor de base de datos.
Olivier Pons