Change Data Capture e o binário __ $ update_mask

9

Estamos usando o CDC para capturar alterações feitas em uma tabela de produção. As linhas alteradas estão sendo exportadas para um armazém de dados (informatica). Eu sei que a coluna __ $ update_mask armazena quais colunas foram atualizadas em um formulário varbinário. Sei também que posso usar uma variedade de funções do CDC para descobrir a partir dessa máscara quais eram essas colunas.

Minha pergunta é essa. Alguém pode definir para mim a lógica por trás dessa máscara para que possamos identificar as colunas que foram alteradas no armazém? Como estamos processando fora do servidor, não temos acesso fácil a essas funções do MSSQL CDC. Prefiro apenas quebrar a máscara em código. O desempenho das funções cdc na extremidade SQL é problemático para esta solução.

Em resumo, gostaria de identificar manualmente as colunas alteradas no campo __ $ update_mask.

Atualizar:

Como alternativa, o envio de uma lista legível por humanos de colunas alteradas para o armazém também era aceitável. Descobrimos que isso poderia ser realizado com desempenho muito superior à nossa abordagem original.

A resposta do CLR a esta pergunta abaixo atende a essa alternativa e inclui detalhes de interpretação da máscara para futuros visitantes. No entanto, a resposta aceita usando XML PATH é a mais rápida ainda para o mesmo resultado final.

RThomas
fonte

Respostas:

11

E a moral da história é ... teste, tente outras coisas, pense grande, depois pequeno, sempre assuma que existe um caminho melhor.

Tão cientificamente interessante quanto minha última resposta foi. Eu decidi tentar outra abordagem. Lembrei-me de que podia concatenar com o truque XML PATH (''). Desde que eu soube como obter o ordinal de cada coluna alterada da lista capture_column da resposta anterior, pensei que valeria a pena testar se a função de bit MS funcionaria melhor dessa maneira para o que precisávamos.

SELECT __$update_mask ,
        ( SELECT    CC.column_name + ','
          FROM      cdc.captured_columns CC
                    INNER JOIN cdc.change_tables CT ON CC.[object_id] = CT.[object_id]
          WHERE     capture_instance = 'dbo_OurTableName'
                    AND sys.fn_cdc_is_bit_set(CC.column_ordinal,
                                              PD.__$update_mask) = 1
        FOR
          XML PATH('')
        ) AS changedcolumns
FROM    cdc.dbo_MyTableName PD

É muito mais limpo do que (embora não seja tão divertido quanto) todo esse CLR, retorna a abordagem apenas ao código SQL nativo. E, drum roll .... retorna os mesmos resultados em menos de um segundo . Como os dados de produção são 100 vezes maiores a cada segundo conta.

Estou deixando a outra resposta para fins científicos - mas, por enquanto, esta é a nossa resposta correta.

RThomas
fonte
Acrescente _CT ao nome da tabela na cláusula FROM.
21714 Chris Morley #:
11
Obrigado por retornar e responder a isso, estou procurando uma solução muito semelhante para que possamos filtrá-la adequadamente no código assim que uma chamada SQL for feita. Não gosto de fazer uma chamada para todas as colunas em todas as linhas retornadas do CDC!
Nik0lias
2

Portanto, após algumas pesquisas, decidimos ainda fazer isso no lado SQL antes de passar para o data warehouse. Mas estamos adotando essa abordagem muito melhorada (com base em nossas necessidades e nova compreensão de como a máscara funciona).

Nós obtemos uma lista dos nomes das colunas e suas posições ordinais com esta consulta. O retorno volta em um formato XML para que possamos passar para o SQL CLR.

DECLARE @colListXML varchar(max);

SET @colListXML = (SELECT column_name, column_ordinal
    FROM  cdc.captured_columns 
    INNER JOIN cdc.change_tables 
    ON captured_columns.[object_id] = change_tables.[object_id]
    WHERE capture_instance = 'dbo_OurTableName'
    FOR XML Auto);

Em seguida, passamos esse bloco XML como uma variável e o campo de máscara para uma função CLR que retorna uma string delimitada por vírgula das colunas que foram alteradas pelo campo binário _ $ update_mask. Essa função clr interroga o campo de máscara para obter o bit de alteração de cada coluna na lista xml e, em seguida, retorna o nome do ordinal relacionado.

SELECT  cdc.udf_clr_ChangedColumns(@colListXML,
        CAST(__$update_mask AS VARCHAR(MAX))) AS changed
    FROM cdc.dbo_OurCaptureTableName
    WHERE NOT __$update_mask IS NULL;

O código c # clr tem a seguinte aparência: (compilado em um assembly chamado CDCUtilities)

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public partial class UserDefinedFunctions
{
    [Microsoft.SqlServer.Server.SqlFunction]
    public static SqlString udf_clr_cdcChangedColumns(string columnListXML, string updateMaskString)
    {
        /*  xml of column ordinals shall be formatted as follows:

            <cdc.captured_columns column_name="Column1" column_ordinal="1" />                
            <cdc.captured_columns column_name="Column2" column_ordinal="2" />                

        */

        System.Text.ASCIIEncoding encoding=new System.Text.ASCIIEncoding();
        byte[] updateMask = encoding.GetBytes(updateMaskString);

        string columnList = "";
        System.Xml.XmlDocument colList = new System.Xml.XmlDocument();
        colList.LoadXml("<columns>" + columnListXML + "</columns>"); /* generate xml with root node */

        for (int i = 0; i < colList["columns"].ChildNodes.Count; i++)
        {
            if (columnChanged(updateMask, int.Parse(colList["columns"].ChildNodes[i].Attributes["column_ordinal"].Value)))
            {
                columnList += colList["columns"].ChildNodes[i].Attributes["column_name"].Value + ",";
            }
        }

        if (columnList.LastIndexOf(',') > 0)
        {
            columnList = columnList.Remove(columnList.LastIndexOf(','));   /* get rid of trailing comma */
        }

        return columnList;  /* return the comma seperated list of columns that changed */
    }

    private static bool columnChanged(byte[] updateMask, int colOrdinal)
    {
        unchecked  
        {
            byte relevantByte = updateMask[(updateMask.Length - 1) - ((colOrdinal - 1) / 8)];
            int bitMask = 1 << ((colOrdinal - 1) % 8);  
            var hasChanged = (relevantByte & bitMask) != 0;
            return hasChanged;
        }
    }
}

E a função para o CLR assim:

CREATE FUNCTION [cdc].[udf_clr_ChangedColumns]
       (@columnListXML [nvarchar](max), @updateMask [nvarchar](max))
RETURNS [nvarchar](max) WITH EXECUTE AS CALLER
AS 
EXTERNAL NAME [CDCUtilities].[UserDefinedFunctions].[udf_clr_cdcChangedColumns]

Em seguida, anexamos esta lista de colunas ao conjunto de linhas e passamos para o data warehouse para análise. Ao usar a consulta e o clr, evitamos ter que usar duas chamadas de função por linha por alteração. Podemos pular direto para a carne com resultados personalizados para nossa instância de captura de alterações.

Graças a este post de stackoverflow sugerido por Jon Seigel pela maneira de interpretar a máscara.

Em nossa experiência com essa abordagem, podemos obter uma lista de todas as colunas alteradas de 10k linhas de cdc em menos de 3 segundos.

RThomas
fonte
Obrigado por retornar com uma solução, posso usá-lo em breve.
Mark Storey-Smith
Confira minha NOVA resposta antes de você. Tão legal quanto o CLR é ... encontramos uma maneira ainda melhor. Boa sorte.
RThomas