Eu tenho várias tabelas grandes, cada uma com mais de 300 colunas. O aplicativo que estou usando cria "arquivos" de linhas alteradas fazendo uma cópia da linha atual em uma tabela secundária.
Considere um exemplo trivial:
CREATE TABLE dbo.bigtable
(
UpdateDate datetime,
PK varchar(12) PRIMARY KEY,
col1 varchar(100),
col2 int,
col3 varchar(20),
.
.
.
colN datetime
);
Tabela de arquivo:
CREATE TABLE dbo.bigtable_archive
(
UpdateDate datetime,
PK varchar(12) NOT NULL,
col1 varchar(100),
col2 int,
col3 varchar(20),
.
.
.
colN datetime
);
Antes de qualquer atualização ser executada em dbo.bigtable
, uma cópia da linha é criada em dbo.bigtable_archive
, e dbo.bigtable.UpdateDate
é atualizada com a data atual.
Portanto UNION
, juntar as duas tabelas e agrupar por PK
cria uma linha do tempo de alterações, quando ordenadas por UpdateDate
.
Desejo criar um relatório detalhando as diferenças entre as linhas, ordenadas por UpdateDate
, agrupadas por PK
, no seguinte formato:
PK, UpdateDate, ColumnName, Old Value, New Value
Old Value
e New Value
podem ser as colunas relevantes convertidas para a VARCHAR(MAX)
(não há TEXT
ou BYTE
colunas envolvidas), pois não preciso fazer nenhum pós-processamento dos próprios valores.
No momento, não consigo pensar em uma maneira sensata de fazer isso para uma grande quantidade de colunas, sem recorrer a gerar as consultas programaticamente - talvez tenha que fazer isso.
Aberto a muitas ideias, então adicionarei uma recompensa à pergunta após 2 dias.
Isso não vai ficar bonito, especialmente considerando as mais de 300 colunas e a indisponibilidade de
LAG
, nem é provável que tenha um desempenho muito bom, mas apenas como algo para começar, eu tentaria a seguinte abordagem:UNION
as duas tabelas.OUTER APPLY
+TOP (1)
como um pobre homemLAG
).varchar(max)
e desvire-os em pares, ou seja, o valor atual e o anterior (CROSS APPLY (VALUES ...)
funciona bem para esta operação).O Transact-SQL do acima como eu vejo:
Se você não dinamizar os dados para uma tabela temporária
Você pode combinar as linhas para encontrar o valor novo e antigo com uma autojunção em
PK
,ColumnName
eVersion = Version + 1
.A parte não tão bonita é, é claro, fazer o unpivot de suas 300 colunas na tabela temporária das duas tabelas base.
XML para o resgate para tornar as coisas menos complicadas.
É possível desdinamizar dados com XML sem precisar saber quais colunas reais existem na tabela que serão desdinamizadas. Os nomes das colunas devem ser válidos como nomes de elementos em XML ou falharão.
A ideia é criar um XML para cada linha com todos os valores dessa linha.
elements xsinil
existe para criar elementos para colunas comNULL
.O XML pode então ser fragmentado usando
nodes('*')
para obter uma linha para cada coluna e usarlocal-name(.)
para obter o nome do elemento etext()
obter o valor.Solução completa abaixo. Observe que
Version
está invertido. 0 = última versão.Sugiro outra abordagem.
Embora você não possa alterar o aplicativo atual, pode ser que você possa alterar o comportamento do banco de dados.
Se possível, adicionaria dois TRIGGERS nas tabelas atuais.
Um INSTEAD OF INSERT em dbo.bigtable_archive que adiciona o novo registro somente se ele não existir no momento.
E um gatilho AFTER INSERT no bigtable que faz exatamente o mesmo trabalho, mas usando dados do bigtable.
Ok, eu configurei um pequeno exemplo aqui com esses valores iniciais:
Agora você deve inserir em bigtable_archive todos os registros pendentes do bigtable.
Agora, na próxima vez que o aplicativo tentar inserir um registro na tabela bigtable_archive, os gatilhos detectarão se ele existe e a inserção será evitada.
Obviamente, agora você pode obter a linha do tempo das alterações consultando apenas a tabela de arquivos. E o aplicativo nunca perceberá que um gatilho está silenciosamente fazendo o trabalho nos bastidores.
dbfiddle aqui
Working proposal, w/ some sample data, can be found @ rextester: bigtable unpivot
The gist of the operation:
1 - Use syscolumns and for xml to dynamically generate our column lists for the unpivot operation; all values will be converted to varchar(max), w/ NULLs being converted to the string 'NULL' (this addresses issue with unpivot skipping NULL values)
2 - Generate a dynamic query to unpivot data into the #columns temp table
3 - Perform a self join of the #temp table to generate the desired output
Cutting-n-pasting from rextester ...
Create some sample data and our #columns table:
The guts of the solution:
And the results:
Note: apologies ... couldn't figure out an easy way to cut-n-paste the rextester output into a code block. I'm open to suggestions.
Potential issues/concerns:
1 - conversion of data to a generic varchar(max) can lead to loss of data precision which in turn can mean we miss some data changes; consider the following datetime and float pairs which, when converted/cast to the generic 'varchar(max)', lose their precision (ie, the converted values are the same):
While data precision could be maintained it would require a bit more coding (eg, casting based on source column datatypes); for now I've opted to stick with the generic varchar(max) per the OP's recommendation (and assumption that the OP knows the data well enough to know that we won't run into any issues of data precision loss).
2 - for really large sets of data we run the risk of blowing out some server resources, whether it be tempdb space and/or cache/memory; primary issue comes from the data explosion that occurs during an unpivot (eg, we go from 1 row and 302 pieces of data to 300 rows and 1200-1500 pieces of data, including 300 copies of the PK and UpdateDate columns, 300 column names)
Essa abordagem usa consulta dinâmica para gerar um sql para obter as alterações. O SP recebe um nome de tabela e esquema e fornece a saída desejada.
As suposições são de que as colunas PK e UpdateDate estejam presentes em todas as tabelas. E todas as tabelas de arquivo têm o formato originalTableName + "_archive"..
NB: Eu não verifiquei o desempenho.
NB: como isso usa sql dinâmico, devo adicionar uma advertência sobre injeção de segurança/sql. Restrinja o acesso ao SP e adicione outras validações para evitar injeção de sql.
Chamada de amostra:
I am using AdventureWorks2012`,Production.ProductCostHistory and Production.ProductListPriceHistory in my example.It may not be perfect history table example, "but script is able to put together the desire output and correct output".
You can take any other table name with fewer column name to understand my script.Any Explanation need then ping me.