Tenemos un requisito en el proyecto para almacenar todas las revisiones (Historial de cambios) para las entidades en la base de datos. Actualmente tenemos 2 propuestas diseñadas para esto:
por ejemplo, para la entidad "Empleado"
Diseño 1:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- Holds the Employee Revisions in Xml. The RevisionXML will contain
-- all data of that particular EmployeeId
"EmployeeHistories (EmployeeId, DateModified, RevisionXML)"
Diseño 2:
-- Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"
-- In this approach we have basically duplicated all the fields on Employees
-- in the EmployeeHistories and storing the revision data.
"EmployeeHistories (EmployeeId, RevisionId, DateModified, FirstName,
LastName, DepartmentId, .., ..)"
¿Hay alguna otra forma de hacer esto?
El problema con el "Diseño 1" es que tenemos que analizar XML cada vez que necesite acceder a los datos. Esto ralentizará el proceso y también agregará algunas limitaciones, como no podemos agregar combinaciones en los campos de datos de revisiones.
Y el problema con el "Diseño 2" es que tenemos que duplicar todos y cada uno de los campos en todas las entidades (tenemos alrededor de 70-80 entidades para las que queremos mantener las revisiones).
sql
database
database-design
versioning
Ramesh Soni
fuente
fuente
Respuestas:
fuente
SELECT * FROM EmployeeHistory WHERE LastName = 'Doe'
resultado simple en una exploración de tabla completa . No es la mejor idea para escalar una aplicación.Creo que la pregunta clave para hacer aquí es '¿Quién / Qué va a usar la historia'?
Si va a ser principalmente para informes / historial legible por humanos, hemos implementado este esquema en el pasado ...
Cree una tabla llamada 'AuditTrail' o algo que tenga los siguientes campos ...
A continuación, puede agregar una columna 'LastUpdatedByUserID' a todas sus tablas que debe configurarse cada vez que realice una actualización / inserción en la tabla.
Luego puede agregar un disparador a cada tabla para detectar cualquier inserción / actualización que ocurra y crear una entrada en esta tabla para cada campo que se modifique. Debido a que la tabla también se proporciona con el 'LastUpdateByUserID' para cada actualización / inserción, puede acceder a este valor en el desencadenador y usarlo al agregarlo a la tabla de auditoría.
Usamos el campo RecordID para almacenar el valor del campo clave de la tabla que se está actualizando. Si es una clave combinada, solo hacemos una concatenación de cadenas con un '~' entre los campos.
Estoy seguro de que este sistema puede tener inconvenientes: para bases de datos muy actualizadas, el rendimiento puede verse afectado, pero para mi aplicación web, obtenemos muchas más lecturas que escrituras y parece estar funcionando bastante bien. Incluso escribimos una pequeña utilidad VB.NET para escribir automáticamente los disparadores basados en las definiciones de la tabla.
¡Solo un pensamiento!
fuente
sysname
puede ser un tipo de datos más adecuado para los nombres de tabla y columna.El artículo de Tablas de historia en el blog del Programador de bases de datos puede ser útil: cubre algunos de los puntos planteados aquí y analiza el almacenamiento de deltas.
Editar
En el ensayo de tablas de historia , el autor ( Kenneth Downs ) recomienda mantener una tabla de historia de al menos siete columnas:
Las columnas que nunca cambian, o cuyo historial no es necesario, no deben rastrearse en la tabla de historial para evitar la hinchazón. Almacenar el delta para valores numéricos puede facilitar las consultas posteriores, aunque puede derivarse de los valores antiguos y nuevos.
La tabla del historial debe ser segura, con usuarios que no sean del sistema que no puedan insertar, actualizar o eliminar filas. Solo se debe admitir la purga periódica para reducir el tamaño general (y si el caso de uso lo permite).
fuente
Hemos implementado una solución muy similar a la que sugiere Chris Roberts, y que funciona bastante bien para nosotros.
La única diferencia es que solo almacenamos el nuevo valor. Después de todo, el valor anterior se almacena en la fila del historial anterior
Digamos que tiene una tabla con 20 columnas. De esta manera, solo tiene que almacenar la columna exacta que ha cambiado en lugar de tener que almacenar toda la fila.
fuente
Evitar el diseño 1; no es muy útil una vez que necesite, por ejemplo, revertir a versiones antiguas de los registros, ya sea de forma automática o "manual" utilizando la consola de administradores.
Realmente no veo inconvenientes en el diseño 2. Creo que la segunda tabla de historial debe contener todas las columnas presentes en la primera tabla de registros. Por ejemplo, en mysql puede crear fácilmente una tabla con la misma estructura que otra tabla (
create table X like Y
). Y, cuando esté a punto de cambiar la estructura de la tabla de Registros en su base de datos en vivo, debe usaralter table
comandos de todos modos, y no hay un gran esfuerzo para ejecutar estos comandos también para su tabla de Historial.Notas
RevisionId
columna agregada ;ModifiedBy
: el usuario que creó una revisión particular. También es posible que desee tener un campoDeletedBy
para rastrear quién eliminó una revisión particular.DateModified
debería significar: o significa dónde se creó esta revisión en particular, o significará cuándo esta revisión en particular fue reemplazada por otra. El primero requiere que el campo esté en la tabla Registros, y parece ser más intuitivo a primera vista; Sin embargo, la segunda solución parece ser más práctica para los registros eliminados (fecha en que se eliminó esta revisión en particular). Si elige la primera solución, probablemente necesite un segundo campoDateDeleted
(solo si lo necesita, por supuesto). Depende de ti y de lo que realmente quieras grabar.Las operaciones en Design 2 son muy triviales:
ModificarSi opta por el Diseño 2, todos los comandos SQL necesarios para hacerlo serán muy fáciles, ¡así como el mantenimiento! Tal vez, será mucho más fácil si usa las columnas auxiliares (
RevisionId
,DateModified
) también en la tabla Registros, ¡para mantener ambas tablas exactamente en la misma estructura (excepto las claves únicas)! Esto permitirá comandos SQL simples, que serán tolerantes a cualquier cambio en la estructura de datos:¡No olvides usar transacciones!
En cuanto al escalado , esta solución es muy eficiente, ya que no transforma ningún dato de XML de un lado a otro, simplemente copiando filas completas de la tabla, consultas muy simples, usando índices, ¡muy eficiente!
fuente
Si tiene que almacenar el historial, cree una tabla de sombra con el mismo esquema que la tabla que está rastreando y una columna 'Fecha de revisión' y 'Tipo de revisión' (por ejemplo, 'eliminar', 'actualizar'). Escriba (o genere, consulte a continuación) un conjunto de desencadenantes para completar la tabla de auditoría.
Es bastante sencillo crear una herramienta que lea el diccionario de datos del sistema para una tabla y genere un script que cree la tabla de sombra y un conjunto de disparadores para llenarla.
No intente utilizar XML para esto, el almacenamiento XML es mucho menos eficiente que el almacenamiento de la tabla de base de datos nativa que utiliza este tipo de disparador.
fuente
Ramesh, participé en el desarrollo del sistema basado en el primer enfoque.
Resultó que almacenar revisiones como XML está llevando a un gran crecimiento de la base de datos y ralentizando significativamente las cosas.
Mi enfoque sería tener una tabla por entidad:
donde IsActive es un signo de la última versión
Si desea asociar información adicional con revisiones, puede crear una tabla separada que contenga esa información y vincularla con tablas de entidad utilizando la relación PK \ FK.
De esta forma, puede almacenar todas las versiones de los empleados en una tabla. Ventajas de este enfoque:
Tenga en cuenta que debe permitir que la clave primaria no sea única.
fuente
La forma en que he visto esto en el pasado es tener
Nunca "actualiza" en esta tabla (excepto para cambiar la validez de isCurrent), simplemente inserte nuevas filas. Para cualquier Id. De empleado dado, solo 1 fila puede tener isCurrent == 1.
La complejidad de mantener esto puede estar oculta por las vistas y los desencadenantes "en lugar de" (en Oracle, supongo cosas similares a otras RDBMS), incluso puede ir a vistas materializadas si las tablas son demasiado grandes y no pueden ser manejadas por índices) .
Este método está bien, pero puede terminar con algunas consultas complejas.
Personalmente, me gusta mucho tu forma de hacerlo en Design 2, que es como lo hice en el pasado también. Es simple de entender, simple de implementar y simple de mantener.
También crea muy poca sobrecarga para la base de datos y la aplicación, especialmente al realizar consultas de lectura, que es lo que probablemente hará el 99% del tiempo.
También sería bastante fácil automatizar la creación de las tablas de historial y los desencadenantes para mantener (suponiendo que se haga a través de desencadenantes).
fuente
Las revisiones de datos son un aspecto del concepto de " tiempo válido " de una base de datos temporal. Se ha investigado mucho sobre esto, y han surgido muchos patrones y pautas. Escribí una larga respuesta con un montón de referencias a esta pregunta para los interesados.
fuente
Voy a compartir con ustedes mi diseño y es diferente de sus dos diseños, ya que requiere una tabla por cada tipo de entidad. Encontré que la mejor manera de describir cualquier diseño de base de datos es a través de ERD, aquí está el mío:
En este ejemplo tenemos una entidad llamada empleado . usuario tabla contiene los registros y de los usuarios entidad y entity_revision son dos tablas que contienen el historial de revisiones para todos los tipos de entidades que tendrá en su sistema. Así es como funciona este diseño:
Los dos campos de entity_id y revision_id
Cada entidad en su sistema tendrá una identificación de entidad única propia. Su entidad podría pasar por revisiones pero su entity_id seguirá siendo el mismo. Debe mantener esta identificación de entidad en su tabla de empleados (como una clave externa). También debe almacenar el tipo de su entidad en la tabla de entidades (por ejemplo, 'empleado'). Ahora, en cuanto a revision_id, como lo muestra su nombre, realiza un seguimiento de las revisiones de su entidad. La mejor manera que encontré para esto es usar el employee_id como su revision_id. Esto significa que tendrá identificadores de revisión duplicados para diferentes tipos de entidades, pero esto no es un placer para mí (no estoy seguro de su caso). La única nota importante que debe hacerse es que la combinación de entity_id y revision_id debe ser única.
También hay un campo de estado dentro de la tabla entity_revision que indica el estado de la revisión. Puede tener uno de los tres estados:
latest
,obsolete
odeleted
(no depender de la fecha de revisiones que contribuye en gran medida a aumentar sus consultas).Una última nota sobre revision_id, no creé una clave externa que conecte employee_id a revision_id porque no queremos alterar la tabla entity_revision para cada tipo de entidad que podamos agregar en el futuro.
INSERCIÓN
Para cada empleado que desee insertar en la base de datos, también agregará un registro a la entidad y la entidad_revisión . Estos dos últimos registros lo ayudarán a realizar un seguimiento de quién y cuándo se ha insertado un registro en la base de datos.
ACTUALIZAR
Cada actualización para un registro de empleado existente se implementará como dos inserciones, una en la tabla de empleados y otra en entity_revision. El segundo lo ayudará a saber quién y cuándo se actualizó el registro.
SUPRESIÓN
Para eliminar un empleado, se inserta un registro en entity_revision indicando la eliminación y listo.
Como puede ver en este diseño, nunca se alteran ni eliminan datos de la base de datos y, lo que es más importante, cada tipo de entidad requiere solo una tabla. Personalmente, considero que este diseño es realmente flexible y fácil de trabajar. Pero no estoy seguro de ti, ya que tus necesidades pueden ser diferentes.
[ACTUALIZAR]
Habiendo soportado particiones en las nuevas versiones de MySQL, creo que mi diseño también viene con una de las mejores actuaciones. Uno puede dividir la
entity
tabla usando eltype
campo mientras que la particiónentity_revision
usa sustate
campo. Esto aumentará lasSELECT
consultas por mucho tiempo mientras mantiene el diseño simple y limpio.fuente
Si realmente una pista de auditoría es todo lo que necesita, me inclinaría hacia la solución de la tabla de auditoría (completa con copias desnormalizadas de la columna importante en otras tablas, por ejemplo
UserName
). Sin embargo, tenga en cuenta que esa amarga experiencia indica que una sola tabla de auditoría será un gran cuello de botella en el futuro; Probablemente valga la pena crear tablas de auditoría individuales para todas sus tablas auditadas.Si necesita rastrear las versiones históricas (y / o futuras) reales, entonces la solución estándar es rastrear la misma entidad con múltiples filas utilizando alguna combinación de valores de inicio, finalización y duración. Puede usar una vista para facilitar el acceso a los valores actuales. Si este es el enfoque que adopta, puede encontrarse con problemas si sus datos versionados hacen referencia a datos mutables pero no versionados.
fuente
Si desea hacer el primero, es posible que también desee utilizar XML para la tabla Empleados. La mayoría de las bases de datos más nuevas le permiten consultar campos XML, por lo que esto no siempre es un problema. Y podría ser más sencillo tener una forma de acceder a los datos de los empleados, independientemente de si es la última versión o una versión anterior.
Sin embargo, intentaría el segundo enfoque. Puede simplificar esto teniendo solo una tabla Empleados con un campo FechaModificada. EmployeeId + DateModified sería la clave principal y puede almacenar una nueva revisión simplemente agregando una fila. De esta forma, archivar versiones anteriores y restaurar versiones del archivo también es más fácil.
Otra forma de hacerlo podría ser el modelo de datos de Dan Linstedt. Hice un proyecto para la oficina de estadísticas holandesa que utilizó este modelo y funciona bastante bien. Pero no creo que sea directamente útil para el uso diario de la base de datos. Sin embargo, puede obtener algunas ideas al leer sus documentos.
fuente
Qué tal si:
Haces la clave principal (Id. De empleado, Fecha de modificación), y para obtener los registros "actuales" simplemente seleccionas MAX (Fecha de modificación) para cada Id. De empleado. Almacenar un IsCurrent es una muy mala idea, porque en primer lugar, se puede calcular y, en segundo lugar, es demasiado fácil que los datos se desincronicen.
También puede crear una vista que enumere solo los registros más recientes, y usarla principalmente mientras trabaja en su aplicación. Lo bueno de este enfoque es que no tiene duplicados de datos y no tiene que recopilar datos de dos lugares diferentes (actuales en Empleados y archivados en EmployeesHistory) para obtener todo el historial o reversión, etc. .
fuente
Si desea confiar en los datos del historial (por razones de informes), debe usar una estructura como esta:
O solución global para la aplicación:
Puede guardar sus revisiones también en XML, luego solo tiene un registro para una revisión. Esto se verá así:
fuente
Hemos tenido requisitos similares, y lo que encontramos fue que muchas veces el usuario solo quiere ver lo que ha cambiado, no necesariamente revertir los cambios.
No estoy seguro de cuál es su caso de uso, pero lo que hemos hecho fue crear y auditar una tabla que se actualiza automáticamente con los cambios en una entidad comercial, incluido el nombre descriptivo de cualquier referencia y enumeración de claves externas.
Cada vez que el usuario guarda sus cambios, recargamos el objeto antiguo, ejecutamos una comparación, registramos los cambios y guardamos la entidad (todo se realiza en una sola transacción de la base de datos en caso de que haya algún problema).
Esto parece funcionar muy bien para nuestros usuarios y nos ahorra el dolor de cabeza de tener una tabla de auditoría completamente separada con los mismos campos que nuestra entidad comercial.
fuente
Parece que desea realizar un seguimiento de los cambios en entidades específicas a lo largo del tiempo, por ejemplo, ID 3, "bob", "123 main street", luego otra ID 3, "bob" "234 elm st", y así sucesivamente, en esencia poder para vomitar un historial de revisión que muestra cada dirección "bob" ha estado en.
La mejor manera de hacer esto es tener un campo "es actual" en cada registro y (probablemente) una marca de tiempo o FK en una tabla de fecha / hora.
Luego, las inserciones deben establecer el "es actual" y también desarmar el "es actual" en el registro anterior "es actual". Las consultas deben especificar "es actual", a menos que desee todo el historial.
Hay más ajustes a esto si se trata de una tabla muy grande, o se espera una gran cantidad de revisiones, pero este es un enfoque bastante estándar.
fuente