Database
 sql >> Base de Dados >  >> RDS >> Database

Executando auditoria de alterações de dados usando a tabela temporal


O SQL Server 2016 introduziu um recurso chamado 'Tabela temporal com versão do sistema'. Usando a tabela normal, você pode recuperar os dados atuais; ao usar uma tabela temporal com versão do sistema, você pode recuperar dados que foram excluídos ou atualizados no passado. Para fazer isso, uma tabela temporal criará uma tabela de histórico. A tabela de histórico armazenará dados antigos com “start_time ” e “horário_final ”. O que indica um período de tempo para o qual o registro estava ativo.

Exemplo:se você atualizar o preço de um produto de 30 para 50 consultando uma tabela normal, poderá recuperar o preço atualizado do produto que é 50. Usando uma tabela temporal, você pode recuperar o valor antigo que é 30.

Usando tabelas temporais, pode-se realizar:
  1. Acompanhar o histórico de um registro :podemos revisar um valor do registro específico, que foi alterado ao longo do tempo.
  2. Recuperação em nível de registro :se excluímos um registro específico da tabela ou um registro está corrompido, podemos recuperá-lo da tabela de histórico.

As tabelas temporais capturam data e hora de um registro com base nas datas físicas (data do calendário) da atualização e exclusão do registro. Atualmente, ele não oferece suporte ao controle de versão com base nas datas lógicas. Por exemplo, se você atualizar o nome do produto usando a instrução UPDATE às 13h, a tabela temporal manterá o histórico do nome do produto até as 13h. Depois disso, um novo nome será aplicável. No entanto, e se a alteração do nome do produto deveria começar a partir das 14h? Isso significa que você deve atualizar a instrução no tempo perfeitamente para fazê-la funcionar e deve ter executado a instrução UPDATE às 14h em vez de 13h.

As tabelas temporais têm os seguintes pré-requisitos:
  1. Uma chave primária deve ser definida.
  2. Duas colunas devem ser definidas para registrar a hora de início e a hora de término com o tipo de dados datetime2. Essas colunas são chamadas de colunas SYSTEM_TIME.

Eles também têm algumas limitações:
  1. Acionadores INSTEAD OF e OLTP na memória não são permitidos.
  2. As tabelas de histórico não podem ter nenhuma restrição.
  3. Os dados na tabela de histórico não podem ser modificados.

Criando uma tabela com versão do sistema


O script a seguir será usado para criar uma tabela simples com versão do sistema:
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));

No script acima, defini HISTORY_TABLE chamado dbo. Product_Change_History. Se você não especificar um nome para a tabela de histórico, o SQL Server criará automaticamente uma tabela de histórico com a seguinte estrutura.

Dbo.MSSQL_TemporalHistoryFor_xxx, onde xxx é o ID do objeto.

A tabela temporal terá a aparência mostrada na captura de tela abaixo:


Como as colunas de período serão atualizadas ao executar a instrução DML na Tabela Temporal?


Sempre que executarmos inserir, atualizar e excluir uma consulta na tabela temporal, as colunas do período (SysStartDate e SysEndDate) serão atualizadas.

Inserir consulta


Quando realizamos a operação INSERT na tabela temporal, o sistema configura o valor da coluna SysStartTime para o horário de início da transação atual e marca a linha como aberta.

Vamos inserir algumas linhas em ‘Produtos ’ e revise como os dados são armazenados nesta tabela.
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



Conforme mostrado na captura de tela acima, o valor de 'Product_Valid_From ' coluna é '2018-04-02 06:55:04.4865670 ' que é a data de inserção da linha. E o valor de 'Product_Valid_To ' coluna é '9999-12-31 23:59:59.9999999 ', que indica que a linha está aberta.

Atualizar consulta


Quando executamos qualquer consulta de atualização na tabela temporal, o sistema armazenará os valores da linha anterior na tabela de histórico e definirá o tempo de transação atual como EndTime e atualize a tabela atual com um novo valor. SysStartTime será a hora de início da transação e SysEndTime será o máximo de 9999-12-31.

Vamos alterar o custo do produto de 'Mouse ' de 500 para 250. Verificaremos a saída de 'Produto '.
Begin tran UpdatePrice
Update Prodcuts set Product_cost=200 where Product_name='Mouse'
Commit tran UpdatePrice

select * from Prodcuts where Product_name='Mouse'



Como você pode ver na captura de tela acima, um valor de 'Product_Valid_From ' coluna foi alterada. O novo valor é o tempo de transação atual (UTC). E o valor de 'Product_Valid_To ' coluna é '9999-12-31 23:59:59.9999999 ', que indica que a linha está aberta e atualizou o preço.

Vamos observar a saída do Product_change_history tabela consultando-a.
select * from Product_Change_History where Product_name='Mouse'



Como você pode ver na captura de tela acima, uma linha foi adicionada em Product_change_history table, que tem uma versão antiga da linha. Valor de 'Produto_custo ' é 500, Valor de 'Product_valid_From ' é a hora em que o registro foi inserido e o valor de Product_Valid_To coluna é quando o valor da coluna Product_cost foi atualizado. Esta versão de linha é considerada fechada.

Excluir consulta


Quando excluímos um registro da tabela temporal, o sistema armazenará a versão atual da linha na tabela de histórico e definirá o horário atual da transação como EndTime e excluirá o registro da tabela atual.

Vamos deletar o registro de ‘Headset’.
Begin tran DeletePrice
    delete from Prodcuts where product_name='Headset'
Commit tran DeletePrice

Vamos observar a saída de Product_change_history tabela consultando-a.
select * from Product_Change_History where Product_name='Headset'



Como você pode ver na captura de tela acima, uma linha foi adicionada em Product_change_history tabela, que foi excluída da tabela atual. Valor de 'Product_valid_From ' é a hora em que o registro foi inserido e o valor do Product_Valid_To coluna é a hora em que a linha foi excluída, o que indica que a versão da linha está fechada.

Auditoria de alterações de dados para um horário específico


Para auditar as alterações de dados de uma tabela específica, devemos realizar a análise baseada em tempo das tabelas temporais. Para fazer isso, devemos usar o 'FOR SYSTEM_TIME ' com as subcláusulas específicas do tempo abaixo para os dados da consulta nas tabelas atuais e de histórico. Deixe-me explicar a saída de consultas usando diferentes subcláusulas. Abaixo segue a configuração:
  1. Inseri um produto chamado "Arruela plana 8" com preço de tabela 0,00 na tabela temporal às 09:02:25.
  2. Alterei o preço de tabela às 10:13:56. O novo preço é 500,00.

A partir de


Esta cláusula será usada para recuperar o estado dos registros por um determinado tempo no AS OF subcláusula. Para entendê-lo, vamos executar várias consultas:

Primeiro, executaremos uma consulta usando o AS OF cláusula com ”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'



Agora, como você pode ver na captura de tela acima, a consulta retornou uma linha com o valor atualizado de “ListPrice ” e valor de Product_Valid_To é o máximo da data.

Vamos executar outra consulta usando o AS OF c lause com “SystemTime =09:10:56: ”.



Agora, como você pode ver na captura de tela acima, o valor de “ListPrice ” é 0,00.

De até


Esta cláusula retornará as linhas ativas entre e . Para entendê-lo, vamos executar a seguinte consulta usando o From..To subcláusula com "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'

A captura de tela a seguir mostra o resultado da consulta:


ENTRE e


Esta cláusula é semelhante à FROM.. Para cláusula. A única diferença é que incluirá os registros que estavam ativos em . Para entendê-lo, vamos executar a seguinte consulta:
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'

A captura de tela a seguir mostra o resultado da consulta:


Incluído em (, )


Esta subcláusula incluirá os registros que se tornaram ativos e terminaram dentro do intervalo de datas especificado. Não inclui os registros ativos. Para entendê-lo, execute a consulta abaixo usando “Contained IN ‘2018-04-20 09:02:25 ' para '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'

A captura de tela a seguir mostra o resultado da consulta:


Cenário


Uma organização está usando um software de inventário. Esse software de inventário usa uma tabela de produtos que é uma tabela temporal da versão do sistema. Devido a um bug do aplicativo, poucos produtos foram excluídos e os preços dos produtos também foram atualizados incorretamente.

Como DBAs, devemos investigar esse problema e recuperar os dados que foram atualizados e excluídos incorretamente da tabela.

Para simular o cenário acima, vamos criar uma tabela com alguns dados significativos. Vou criar uma nova tabela temporal chamada 'tblProduct ' no banco de dados Demo, que é um clone do [Production].[Products] tabela do banco de dados AdventureWorks2014.

Para executar a tarefa acima, segui os passos abaixo:
  1. Extraído “script de criação de tabela” [Produção]. [Produtos] do banco de dados AdventureWorks2014.
  2. Removidas todas as “restrições e índices” do script.
  3. Mantive a estrutura da coluna inalterada.
  4. Para convertê-lo em uma tabela temporal, adicionei as colunas SysStartTime e SysEndTime.
  5. System_Versioning ativado.
  6. Tabela de histórico especificada.
  7. Executou o script no banco de dados edemo.

Abaixo segue o roteiro:
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

Importei dados da tabela de produtos do banco de dados “AdventureWorks2014” para a tabela de produtos do “DemoDatabase” executando o seguinte script:
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

Excluí os registros de nome de produto que começam com 'Thin-Jam Hex Nut' do tblProduct. Também alterei o preço dos produtos cujos nomes começam com Flat Washer em 'tblProduct ’ executando a seguinte consulta:
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%'

Estamos cientes do momento em que os dados foram excluídos. Portanto, para identificar quais dados foram excluídos, usaremos a subcláusula Contained-IN. Como mencionei acima, ele me dará a lista de registros que possuem versões de linha que se tornaram ativas e terminaram dentro do intervalo de datas especificado. Em seguida, executado abaixo da consulta:
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)



Ao executar a consulta acima, 22 linhas foram recuperadas.

O Contido-IN cláusula preencherá as linhas que foram atualizadas e excluídas durante o tempo determinado.

Preencher registros excluídos:

Para preencher os registros excluídos, devemos pular os registros que foram atualizados durante o tempo especificado na cláusula Contained-IN. No script abaixo, o “Onde ” irá ignorar os produtos que estão presentes no tblProduct tabela. Vamos executar a seguinte consulta:
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)

A consulta acima ignorou os registros que foram atualizados; portanto, ele retornou 13 linhas. Veja abaixo a captura de tela:



Usando o método acima, poderemos obter a lista de produtos que foram excluídos do tblProduct tabela.

Preencher registros atualizados

Para preencher os registros atualizados, devemos ignorar os registros que foram excluídos durante o tempo especificado no Contained-IN cláusula. No script abaixo, o “Onde ” incluirá os produtos que estão presentes no tblProduct tabela. Vamos executar a seguinte consulta:
 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)

A consulta acima ignorou os registros que foram atualizados, portanto, retornou 9 linhas. Veja abaixo a captura de tela:



Usando o método acima, poderemos identificar os registros que foram atualizados com valores errados e os registros que foram excluídos da tabela temporal.

Resumo


Neste artigo, abordei:
  1. Introdução de alto nível de tabelas temporais.
  2. Explicado como as colunas de período serão atualizadas ao executar consultas DML.
  3. Uma demonstração para recuperar a lista de produtos que foram excluídos e atualizados com o preço errado, da tabela temporal. Este relatório pode ser usado para fins de auditoria.