Diseño de bases de datos para revisiones?

125

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

Ramesh Soni
fuente
1
FYI: En caso de que pueda ayudar .Sql server 2008 y superior tiene tecnología que muestra el historial de los cambios en la tabla ... visite simple-talk.com/sql/learn-sql-server/… para saber más y estoy seguro de que DB's como Oracle también tendrá algo como esto.
Durai Amuthan.H
Tenga en cuenta que algunas columnas pueden almacenar XML o JSON ellos mismos. Si no es el caso ahora, podría suceder en el futuro. Mejor asegúrese de que no necesita anidar tales datos uno en otro.
jakubiszon

Respuestas:

38
  1. No lo ponga todo en una tabla con un atributo discriminador IsCurrent. Esto solo causa problemas en el futuro, requiere claves sustitutas y todo tipo de otros problemas.
  2. El diseño 2 tiene problemas con los cambios de esquema. Si cambia la tabla de Empleados, debe cambiar la tabla de Historia del Empleado y todos los sprocs relacionados que la acompañan. Potencialmente duplica su esfuerzo de cambio de esquema.
  3. El diseño 1 funciona bien y, si se hace correctamente, no cuesta mucho en términos de un impacto en el rendimiento. Puede usar un esquema xml e incluso índices para superar posibles problemas de rendimiento. Su comentario sobre el análisis del xml es válido, pero puede crear fácilmente una vista usando xquery, que puede incluir en las consultas y unirse. Algo como esto...
CREATE VIEW EmployeeHistory
AS
, FirstName, , DepartmentId

SELECT EmployeeId, RevisionXML.value('(/employee/FirstName)[1]', 'varchar(50)') AS FirstName,

  RevisionXML.value('(/employee/LastName)[1]', 'varchar(100)') AS LastName,

  RevisionXML.value('(/employee/DepartmentId)[1]', 'integer') AS DepartmentId,

FROM EmployeeHistories 
Simon Munro
fuente
25
¿Por qué dice que no almacene todo en una tabla con el desencadenador IsCurrent? ¿Podría señalarme algunos ejemplos en los que esto sería problemático?
Nathan W
@Simon Munro ¿Qué pasa con una clave principal o clave agrupada? ¿Qué clave podemos agregar en la tabla de historial de Design 1 para agilizar la búsqueda?
gotqn
Supongo que un 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.
Kaii
54

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 ...

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[OldValue] [varchar](5000) NULL,
[NewValue] [varchar](5000) NULL

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!

Chris Roberts
fuente
55
No es necesario almacenar el NewValue, ya que está almacenado en la tabla auditada.
Petrus Theron
17
Estrictamente hablando, eso es cierto. Pero, cuando hay una serie de cambios en el mismo campo durante un período de tiempo, almacenar el nuevo valor hace que las consultas como 'muéstrame todos los cambios realizados por Brian' sean mucho más fáciles, ya que toda la información sobre una actualización se guarda en un registro ¡Solo un pensamiento!
Chris Roberts el
1
Creo que sysnamepuede ser un tipo de datos más adecuado para los nombres de tabla y columna.
Sam
2
@Sam usando sysname no agrega ningún valor; incluso podría ser confuso ... stackoverflow.com/questions/5720212/…
Jowen
19

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:

  1. Marca de tiempo del cambio,
  2. Usuario que realizó el cambio,
  3. Un token para identificar el registro que se modificó (donde el historial se mantiene separado del estado actual),
  4. Si el cambio fue una inserción, actualización o eliminación,
  5. El viejo valor,
  6. El nuevo valor,
  7. El delta (para cambios en los valores numéricos).

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

Mark Streatfield
fuente
14

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

[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NULL,
[EventDate] [datetime] NOT NULL,
[TableName] [varchar](50) NOT NULL,
[RecordID] [varchar](20) NOT NULL,
[FieldName] [varchar](50) NULL,
[NewValue] [varchar](5000) NULL

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.

Kjetil Watnedal
fuente
14

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 usar alter tablecomandos de todos modos, y no hay un gran esfuerzo para ejecutar estos comandos también para su tabla de Historial.

Notas

  • La tabla de registros contiene solo la última revisión;
  • La tabla de historial contiene todas las revisiones anteriores de registros en la tabla de registros;
  • La clave principal de la tabla de historial es una clave principal de la tabla Registros con una RevisionIdcolumna agregada ;
  • Piense en campos auxiliares adicionales como ModifiedBy: el usuario que creó una revisión particular. También es posible que desee tener un campo DeletedBypara rastrear quién eliminó una revisión particular.
  • Piense en lo que DateModifieddeberí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 campo DateDeleted(solo si lo necesita, por supuesto). Depende de ti y de lo que realmente quieras grabar.

Las operaciones en Design 2 son muy triviales:

Modificar
  • copie el registro de la tabla Registros a la tabla Historial, dele un nuevo RevisionId (si aún no está presente en la tabla Registros), maneje DateModified (depende de cómo lo interprete, vea las notas anteriores)
  • continuar con la actualización normal del registro en la tabla Registros
Eliminar
  • haga exactamente lo mismo que en el primer paso de la operación Modificar. Handle DateModified / DateDeleted en consecuencia, según la interpretación que haya elegido.
Recuperar (o deshacer)
  • tome la revisión más alta (¿o alguna en particular?) de la tabla Historial y cópiela en la tabla Registros
Listar el historial de revisión para un registro particular
  • seleccione de la tabla Historial y la tabla Registros
  • piense exactamente qué espera de esta operación; probablemente determinará qué información necesita de los campos DateModified / DateDeleted (consulte las notas anteriores)

Si 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:

insert into EmployeeHistory select * from Employe where ID = XX

¡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!

TMS
fuente
12

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.

Preocupado por TunbridgeWells
fuente
3
+1 por simplicidad! Algunos sobredimensionarán por miedo a cambios posteriores, mientras que la mayoría de las veces no se producen cambios. Además, es mucho más fácil administrar las historias en una tabla y los registros reales en otra que tenerlos todos en una tabla (pesadilla) con alguna bandera o estado. Se llama 'KISS' y normalmente te recompensará a largo plazo.
Jeach
¡+1 completamente de acuerdo, exactamente lo que digo en mi respuesta ! Simple y poderoso!
TMS
8

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:

Employee (Id, Name, ... , IsActive)  

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:

  • Estructura de base de datos simple
  • No hay conflictos ya que la tabla se convierte solo en apéndice
  • Puede retroceder a la versión anterior simplemente cambiando el indicador IsActive
  • No es necesario unirse para obtener el historial de objetos

Tenga en cuenta que debe permitir que la clave primaria no sea única.

aku
fuente
66
Usaría una columna "RevisionNumber" o "RevisionDate" en lugar de o además de IsActive, para que pueda ver todas las revisiones en orden.
Sklivvz
Usaría un "parentRowId" porque eso le brinda un fácil acceso a las versiones anteriores, así como la capacidad de encontrar la base y el final rápidamente.
chacham15
6

La forma en que he visto esto en el pasado es tener

Employees (EmployeeId, DateModified, < Employee Fields > , boolean isCurrent );

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

Matthew Watson
fuente
4

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.

Henrik Gustafsson
fuente
4

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:

ingrese la descripción de la imagen aquí

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, obsoleteo deleted(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 entitytabla usando el typecampo mientras que la partición entity_revisionusa su statecampo. Esto aumentará las SELECTconsultas por mucho tiempo mientras mantiene el diseño simple y limpio.

Mehran
fuente
3

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.

Hank Gay
fuente
3

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.

Mendelt
fuente
2

Qué tal si:

  • ID de empleado
  • Fecha modificada
    • y / o número de revisión, dependiendo de cómo quiera rastrearlo
  • ModifiedByUSerId
    • además de cualquier otra información que desee rastrear
  • Campos de empleados

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. .

Gregmac
fuente
Un inconveniente de este enfoque es que la tabla crecerá más rápidamente que si usa dos tablas.
cdmckay
2

Si desea confiar en los datos del historial (por razones de informes), debe usar una estructura como esta:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds the Employee revisions in rows.
"EmployeeHistories (HistoryId, EmployeeId, DateModified, OldValue, NewValue, FieldName)"

O solución global para la aplicación:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, OldValue, NewValue, FieldName)"

Puede guardar sus revisiones también en XML, luego solo tiene un registro para una revisión. Esto se verá así:

// Holds Employee Entity
"Employees (EmployeeId, FirstName, LastName, DepartmentId, .., ..)"

// Holds all entities revisions in rows.
"EntityChanges (EntityName, EntityId, DateModified, XMLChanges)"
dariol
fuente
1
Mejor: use el abastecimiento de eventos :)
dariol
1

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.

mattruma
fuente
0

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.

Steve Moon
fuente