Database
 sql >> Base de données >  >> RDS >> Database

Réalisation d'un audit des modifications de données à l'aide d'une table temporelle

SQL Server 2016 a introduit une fonctionnalité appelée "Table temporelle versionnée par le système". En utilisant une table normale, vous pouvez récupérer les données actuelles ; tout en utilisant une table temporelle versionnée par le système, vous pouvez récupérer des données qui ont été supprimées ou mises à jour dans le passé. Pour cela, une table temporelle va créer une table d'historique. La table d'historique stockera les anciennes données avec "start_time ” et “end_time ”. Ce qui indique une période pendant laquelle l'enregistrement a été actif.

Exemple :Si vous mettez à jour le prix d'un produit de 30 à 50 en interrogeant une table normale, vous pouvez récupérer le prix du produit mis à jour qui est de 50. En utilisant une table temporelle, vous pouvez récupérer l'ancienne valeur qui est de 30.

A l'aide de tables temporelles, on peut effectuer :

  1. Suivre l'historique d'un enregistrement  :nous pouvons examiner une valeur de l'enregistrement spécifique, qui a été modifiée au fil du temps.
  2. Récupération au niveau des enregistrements  :si nous avons supprimé un enregistrement spécifique de la table ou si un enregistrement est corrompu, nous pouvons le récupérer à partir de la table d'historique.

Les tables temporelles capturent la date et l'heure d'un enregistrement en fonction des dates physiques (date calendaire) de la mise à jour et de la suppression de l'enregistrement. Actuellement, il ne prend pas en charge la gestion des versions basée sur les dates logiques. Par exemple, si vous mettez à jour le nom du produit à l'aide de l'instruction UPDATE à 13h00, la table temporelle conservera l'historique du nom du produit jusqu'à 13h00. Après cela, un nouveau nom sera applicable. Cependant, que se passerait-il si le changement de nom du produit devait commencer à 14h 00 ? Cela signifie que vous devez parfaitement mettre à jour l'instruction à temps pour qu'elle fonctionne et que vous auriez dû exécuter l'instruction UPDATE à 14h00 au lieu de 13h00.

Les tables temporelles ont les prérequis suivants :

  1. Une clé primaire doit être définie.
  2. Deux colonnes doivent être définies pour enregistrer l'heure de début et l'heure de fin avec le type de données datetime2. Ces colonnes sont appelées colonnes SYSTEM_TIME.

Ils ont également certaines limitations :

  1. Les déclencheurs INSTEAD OF et l'OLTP en mémoire ne sont pas autorisés.
  2. Les tables d'historique ne peuvent avoir aucune contrainte.
  3. Les données du tableau d'historique ne peuvent pas être modifiées.

Création d'une table versionnée par le système

Le script suivant sera utilisé pour créer une simple table versionnée par le système :

Use DemoDatabase
Go
CREATE TABLE dbo.Prodcuts
	(
	      Product_ID int identity (1,1) primary key
	    , Product_Name varchar (500)
	    , Product_Cost int
	    , Quantity int
	    , Product_Valid_From datetime2 GENERATED ALWAYS AS ROW START NOT NULL
	    , Product_Valid_TO datetime2 GENERATED ALWAYS AS ROW END NOT NULL
	    , PERIOD FOR SYSTEM_TIME (Product_Valid_From,Product_Valid_TO)
	)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE =dbo.Product_Change_History));

Dans le script ci-dessus, j'ai défini HISTORY_TABLE nommé dbo. Product_Change_History. Si vous ne spécifiez pas de nom pour la table d'historique, SQL Server créera automatiquement une table d'historique avec la structure suivante.

Dbo.MSSQL_TemporalHistoryFor_xxx, où xxx est l'ID de l'objet.

La table temporelle ressemblera à la capture d'écran ci-dessous :

Comment les colonnes de période seront-elles mises à jour lors de l'exécution de l'instruction DML sur la table temporelle ?

Chaque fois que nous exécutons insert, update et delete une requête sur la table temporelle, les colonnes de période (SysStartDate et SysEndDate) seront mises à jour.

Insérer une requête

Lorsque nous effectuons l'opération INSERT sur la table temporelle, le système définit la valeur de la colonne SysStartTime sur l'heure de début de la transaction en cours et marque la ligne comme ouverte.

Insérons quelques lignes dans les "Produits ' et examinez comment les données sont stockées dans cette table.

INSERT INTO prodcuts 
            (product_name, 
             product_cost, 
             quantity) 
VALUES      ( 'Mouse', 
              500, 
              10 ), 
            ( 'Key-Board', 
              200, 
              5 ), 
            ( 'Headset', 
              500, 
              1 ), 
            ( 'Laptop', 
              50000, 
              1 )
 select * from Prodcuts

Comme indiqué dans la capture d'écran ci-dessus, la valeur de 'Product_Valid_From ' la colonne est '2018-04-02 06:55:04.4865670 ' qui est la date d'insertion de la ligne. Et la valeur de 'Product_Valid_To ' la colonne est '9999-12-31 23:59:59.9999999 ', ce qui indique que la ligne est ouverte.

Mettre à jour la requête

Lorsque nous exécutons une requête de mise à jour sur la table temporelle, le système stocke les valeurs de ligne précédentes dans la table d'historique et définit l'heure de transaction actuelle sur EndTime et mettre à jour la table actuelle avec une nouvelle valeur. HeureDébutSys sera l'heure de début de la transaction et SysEndTime sera le maximum de 9999-12-31.

Changeons le coût du produit de 'Souris ' de 500 à 250. Nous allons vérifier la sortie de 'Produit '.

Begin tran UpdatePrice
Update Prodcuts set Product_cost=200 where Product_name='Mouse'
Commit tran UpdatePrice

select * from Prodcuts where Product_name='Mouse'

Comme vous pouvez le voir dans la capture d'écran ci-dessus, une valeur de 'Product_Valid_From ' a été modifiée. La nouvelle valeur est l'heure de transaction actuelle (UTC). Et la valeur de 'Product_Valid_To ' la colonne est '9999-12-31 23:59:59.9999999 ', ce qui indique que la ligne est ouverte et a mis à jour le prix.

Observons la sortie de Product_change_history table en l'interrogeant.

select * from Product_Change_History where Product_name='Mouse'

Comme vous pouvez le voir dans la capture d'écran ci-dessus, une ligne a été ajoutée dans Product_change_history table, qui a une ancienne version de la ligne. Valeur de 'Product_cost ' est 500, valeur de 'Product_valid_From ' est l'heure à laquelle l'enregistrement a été inséré et la valeur de Product_Valid_To correspond à la valeur de la colonne Product_cost a été mis à jour. Cette version de ligne est considérée comme fermée.

Supprimer la requête

Lorsque nous supprimons un enregistrement de la table temporelle, le système stocke la version actuelle de la ligne dans la table d'historique et définit l'heure de transaction actuelle comme EndTime et supprime l'enregistrement de la table actuelle.

Supprimons l'enregistrement de "Casque".

Begin tran DeletePrice
    delete from Prodcuts where product_name='Headset'
Commit tran DeletePrice

Observons la sortie de Product_change_history table en l'interrogeant.

select * from Product_Change_History where Product_name='Headset'

Comme vous pouvez le voir dans la capture d'écran ci-dessus, une ligne a été ajoutée dans Product_change_history table, qui a été supprimée de la table actuelle. Valeur de 'Product_valid_From ' est l'heure à laquelle l'enregistrement a été inséré et la valeur de Product_Valid_To colonne est l'heure à laquelle la ligne a été supprimée, ce qui indique que la version de la ligne est fermée.

Vérifier les changements de données pendant une durée spécifique

Pour auditer les changements de données pour une table spécifique, nous devons effectuer l'analyse temporelle des tables temporelles. Pour ce faire, nous devons utiliser le 'FOR SYSTEM_TIME ' avec les sous-clauses spécifiques au temps ci-dessous pour les données de requête dans les tables actuelles et historiques. Permettez-moi d'expliquer la sortie des requêtes à l'aide de différentes sous-clauses. Ci-dessous la configuration :

  1. J'ai inséré un produit nommé "Flat Washer 8" avec un prix catalogue de 0,00 dans le tableau temporel à 09:02:25 AM.
  2. J'ai modifié le prix catalogue à 10:13:56 AM. Le nouveau prix est de 500,00.

À PARTIR DU

Cette clause sera utilisée pour récupérer l'état des enregistrements pour un temps donné dans le AS OF sous-clause. Pour le comprendre, exécutons plusieurs requêtes :

Tout d'abord, nous allons exécuter une requête en utilisant le AS OF clause avec ”SystemTime =10:15:59 ”.

select Name, ListPrice,rowguid,Product_Valid_From,Product_Valid_TO from DemoDatabase.dbo.tblProduct  FOR system_time as of '2018-04-20 10:15:56
where name ='Flat Washer 8'

Maintenant, comme vous pouvez le voir dans la capture d'écran ci-dessus, la requête a renvoyé une ligne avec la valeur mise à jour de "ListPrice ” et la valeur de Product_Valid_To est le maximum de date.

Exécutons une autre requête en utilisant le AS OF c lause avec "SystemTime =09:10:56 : ”.

Maintenant, comme vous pouvez le voir dans la capture d'écran ci-dessus, la valeur de "ListPrice ” vaut 0,00.

De à

Cette clause renverra les lignes actives entre et . Pour le comprendre, exécutons la requête suivante en utilisant le De..À sous-clause avec "SystemTime From '2018-04-20 09:02:25' to '2018-04-20 10:14:56 '".

select Name, ListPrice,rowguid,Product_Valid_From,Product_Valid_TO,ListPrice from DemoDatabase.dbo.tblProduct  FOR system_time from '2018-04-20 09:02:25 to '2018-04-20 10:13:56 where name =  'Flat Washer 8'

La capture d'écran suivante montre le résultat de la requête :

ENTRE et

Cette clause est similaire à FROM.. À clause. La seule différence est qu'il inclura les enregistrements qui étaient actifs à . Pour le comprendre, exécutons la requête suivante :

select Name, ListPrice,rowguid,Product_Valid_From,Product_Valid_TO,ListPrice from DemoDatabase.dbo.tblProduct  FOR system_time between '2018-04-20 09:02:25.1265684' and '2018-04-20 10:13:56.1265684' where name =  'Flat Washer 8'

La capture d'écran suivante montre le résultat de la requête :

Inclus DANS (, )

Cette sous-clause inclura les enregistrements qui sont devenus actifs et se sont terminés dans la plage de dates spécifiée. Il n'inclut pas les enregistrements actifs. Pour le comprendre, exécutez la requête ci-dessous en utilisant "Contained IN '2018-04-20 09:02:25au ‘2018-04-20 10:14:56’

select Name, ListPrice,rowguid,Product_Valid_From,Product_Valid_TO,ListPrice from DemoDatabase.dbo.tblProduct  FOR system_time Contained IN( '2018-04-20 09:02:25' , '2018-04-20 10:13:56 ') where name =  'Flat Washer 8'

La capture d'écran suivante montre le résultat de la requête :

Scénario

Une organisation utilise un logiciel d'inventaire. Ce logiciel d'inventaire utilise une table de produits qui est une table temporelle de version du système. En raison d'un bug de l'application, peu de produits ont été supprimés et les prix des produits ont également été mis à jour à tort.

En tant que DBA, nous devons enquêter sur ce problème et récupérer les données qui ont été mises à jour par erreur et supprimées de la table.

Pour simuler le scénario ci-dessus, créons une table avec des données significatives. Je vais créer une nouvelle table temporelle nommée 'tblProduct ' sur la base de données Demo qui est un clone de [Production].[Products] table de la base de données AdventureWorks2014.

Pour effectuer la tâche ci-dessus, j'ai suivi les étapes ci-dessous :

  1. Extrait du "script de création de table" [Production]. [Produits] de la base de données AdventureWorks2014.
  2. Suppression de toutes les "contraintes et index" du script.
  3. A conservé la structure des colonnes inchangée.
  4. Pour le convertir en table temporelle, j'ai ajouté les colonnes SysStartTime et SysEndTime.
  5. System_Versioning activé.
  6. Table d'historique spécifiée.
  7. Exécution du script sur la base de données edemo.

Ci-dessous le script :

USE [DemoDatabase]
GO
CREATE TABLE [tblProduct](
	[ProductID] [int] IDENTITY(1,1) Primary Key,
	[Name] varchar(500) NOT NULL,
	[ProductNumber] [nvarchar](25) NOT NULL,
	[Color] [nvarchar](15) NULL,
	[SafetyStockLevel] [smallint] NOT NULL,
	[ReorderPoint] [smallint] NOT NULL,
	[StandardCost] [money] NOT NULL,
	[ListPrice] [money] NOT NULL,
	[Size] [nvarchar](5) NULL,
	[SizeUnitMeasureCode] [nchar](3) NULL,
	[WeightUnitMeasureCode] [nchar](3) NULL,
	[Weight] [decimal](8, 2) NULL,
	[DaysToManufacture] [int] NOT NULL,
	[ProductLine] [nchar](2) NULL,
	[Class] [nchar](2) NULL,
	[Style] [nchar](2) NULL,
	[ProductSubcategoryID] [int] NULL,
	[ProductModelID] [int] NULL,
	[SellStartDate] [datetime] NOT NULL,
	[SellEndDate] [datetime] NULL,
	[DiscontinuedDate] [datetime] NULL,
	[rowguid] [uniqueidentifier] ROWGUIDCOL  NOT NULL,
	[ModifiedDate] [datetime] NOT NULL,
	Product_Valid_From datetime2 GENERATED ALWAYS AS ROW START NOT NULL
    , Product_Valid_TO datetime2 GENERATED ALWAYS AS ROW END NOT NULL
    , PERIOD FOR SYSTEM_TIME (Product_Valid_From,Product_Valid_TO)
 )
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE =dbo.Product_History));
GO

J'ai importé des données de la table des produits de la base de données "AdventureWorks2014" vers la table des produits de "DemoDatabase" en exécutant le script suivant :

insert into DemoDatabase.dbo.tblProduct
(Name
,ProductNumber
,Color
,SafetyStockLevel
,ReorderPoint
,StandardCost
,ListPrice
,Size
,SizeUnitMeasureCode
,WeightUnitMeasureCode
,Weight
,DaysToManufacture
,ProductLine
,Class
,Style
,ProductSubcategoryID
,ProductModelID
,SellStartDate
,SellEndDate
,DiscontinuedDate
,rowguid
,ModifiedDate)
select top 50
Name
,ProductNumber
,Color
,SafetyStockLevel
,ReorderPoint
,StandardCost
,ListPrice
,Size
,SizeUnitMeasureCode
,WeightUnitMeasureCode
,Weight
,DaysToManufacture
,ProductLine
,Class
,Style
,ProductSubcategoryID
,ProductModelID
,SellStartDate
,SellEndDate
,DiscontinuedDate
,rowguid
,ModifiedDate
from AdventureWorks2014.Production.Product

J'ai supprimé les enregistrements de nom de produit qui commencent par "Thin-Jam Hex Nut" de tblProduct. J'ai également changé le prix des produits dont les noms commencent par Flat Washer sur 'tblProduct ' en exécutant la requête suivante :

delete from DemoDatabase.dbo.Product where name like '%Thin-Jam Hex Nut%'
waitfor delay '00:01:00'
update DemoDatabase.dbo.tblProduct set ListPrice=500.00 where name like '%Flat Washer%'

Nous connaissons le moment où les données ont été supprimées. Par conséquent, pour identifier quelles données ont été supprimées, nous utiliserons la sous-clause Contained-IN. Comme je l'ai mentionné ci-dessus, cela me donnera la liste des enregistrements dont les versions de ligne sont devenues actives et se sont terminées dans la plage de dates spécifiée. Ensuite, exécutez la requête ci-dessous :

declare @StartDateTime datetime
declare @EndDateTime datetime
set @StartDateTime=convert (datetime2, getdate()-1)
set @EndDateTime=convert (datetime2, getdate())
select ProductID, Name, ProductNumber,Product_Valid_From, Product_Valid_To from Product For SYSTEM_TIME Contained IN ( @StartDateTime , @EndDateTime)

En exécutant la requête ci-dessus, 22 lignes ont été récupérées.

Le Contenu-IN La clause remplira les lignes qui ont été mises à jour et supprimées pendant le temps donné.

Remplir les enregistrements supprimés :

Pour remplir les enregistrements supprimés, nous devons ignorer les enregistrements qui ont été mis à jour pendant la durée spécifiée dans la clause Contained-IN. Dans le script ci-dessous, le " " La clause ignorera les produits qui sont présents dans le tblProduct table. Nous allons exécuter la requête suivante :

declare @StartDateTime datetime
declare @EndDateTime datetime
set @StartDateTime=convert(datetime2,getdate()-1)
set @EndDateTime=convert(datetime2,getdate())

select ProductID, Name, ProductNumber,Product_Valid_From, Product_Valid_To from tblProduct For SYSTEM_TIME Contained IN ( @StartDateTime , @EndDateTime) Where Name not in (Select Name from tblProduct)

La requête ci-dessus a ignoré les enregistrements qui ont été mis à jour ; par conséquent, il a renvoyé 13 lignes. Voir la capture d'écran ci-dessous :

En utilisant la méthode ci-dessus, nous pourrons obtenir la liste des produits qui ont été supprimés du tblProduct tableau.

Remplir les enregistrements mis à jour

Pour remplir les enregistrements mis à jour, nous devons ignorer les enregistrements qui ont été supprimés pendant le temps spécifié dans le Contained-IN clause. Dans le script ci-dessous, le " La clause ” inclura les produits qui sont présents dans le tblProduct table. Nous allons exécuter la requête suivante :

 declare @StartDateTime datetime
declare @EndDateTime datetime
set @StartDateTime=convert(datetime2,getdate()-1)
set @EndDateTime=convert(datetime2,getdate())
select ProductID, Name, ProductNumber,Product_Valid_From, Product_Valid_To from tblProduct For SYSTEM_TIME Contained IN ( @StartDateTime , @EndDateTime) Where Name in (Select Name from tblProduct)

La requête ci-dessus a ignoré les enregistrements qui ont été mis à jour et a donc renvoyé 9 lignes. Voir la capture d'écran ci-dessous :

En utilisant la méthode ci-dessus, nous pourrons identifier les enregistrements qui ont été mis à jour avec des valeurs erronées et les enregistrements qui ont été supprimés de la table temporelle.

Résumé

Dans cet article, j'ai couvert :

  1. Introduction de haut niveau des tables temporelles.
  2. Explique comment les colonnes de période seront mises à jour en exécutant des requêtes DML.
  3. Une démo pour récupérer la liste des produits qui a été supprimé et mis à jour avec le mauvais prix, à partir de la table temporelle. Ce rapport peut être utilisé à des fins d'audit.