Change Data Capture e il binario __ $ update_mask


9

Stiamo utilizzando CDC per acquisire le modifiche apportate a una tabella di produzione. Le righe modificate vengono esportate in un data warehouse (informatica). So che la colonna __ $ update_mask memorizza quali colonne sono state aggiornate in una forma varbinary. So anche che posso usare una varietà di funzioni CDC per scoprire da quella maschera quali fossero quelle colonne.

La mia domanda è questa Qualcuno può definire per me la logica dietro quella maschera in modo da poter identificare le colonne che sono state cambiate nel magazzino? Dal momento che stiamo elaborando al di fuori del server non abbiamo un facile accesso a quelle funzioni MSCQL CDC. Preferirei solo abbattere la maschera me stesso in codice. Le prestazioni delle funzioni cdc sull'estremità SQL sono problematiche per questa soluzione.

In breve, vorrei identificare manualmente le colonne modificate dal campo __ $ update_mask.

Aggiornare:

In alternativa, era accettabile anche inviare un elenco leggibile dall'uomo di colonne modificate al magazzino. Abbiamo scoperto che questo potrebbe essere eseguito con prestazioni di gran lunga superiori al nostro approccio originale.

La risposta CLR a questa domanda che segue soddisfa questa alternativa e include dettagli sull'interpretazione della maschera per i futuri visitatori. Tuttavia, la risposta accettata utilizzando XML PATH è la più veloce per lo stesso risultato finale.


Risposte:


11

E la morale della storia è ... prova, prova altre cose, pensa in grande, poi in piccolo, supponi sempre che ci sia un modo migliore.

Scientificamente interessante quanto la mia ultima risposta è stata. Ho deciso di provare un altro approccio. Mi sono ricordato che avrei potuto concatenarmi con il trucco XML PATH (''). Poiché sapevo come ottenere l'ordinale di ogni colonna modificata dall'elenco captured_column dalla risposta precedente, ho pensato che valesse la pena testare se la funzione di bit MS funzionasse meglio in questo modo per ciò di cui avevamo bisogno.

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

È molto più pulito di (anche se non altrettanto divertente) di tutto quel CLR, restituisce l'approccio solo al codice SQL nativo. E, rullo di tamburi .... restituisce gli stessi risultati in meno di un secondo . Poiché i dati di produzione sono 100 volte più grandi ogni secondo conta.

Lascio l'altra risposta a fini scientifici - ma per ora, questa è la nostra risposta corretta.


Aggiungi _CT al nome della tabella nella clausola FROM.
Chris Morley,

1
Grazie per essere tornato e aver risposto a questa domanda, sto cercando una soluzione molto simile in modo da poterla filtrare di conseguenza all'interno del codice una volta effettuata una chiamata SQL. Non mi va di fare una chiamata per ogni colonna su ogni riga restituita da CDC!
nik0lias,

2

Quindi, dopo alcune ricerche, abbiamo deciso di farlo ancora sul lato SQL prima di passare al data warehouse. Ma stiamo adottando questo approccio molto migliorato (basato sulle nostre esigenze e sulla nuova comprensione di come funziona la maschera).

Otteniamo un elenco dei nomi delle colonne e delle loro posizioni ordinali con questa query. Il ritorno ritorna in un formato XML in modo da poter passare a CLR SQL.

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

Passiamo quindi quel blocco XML come variabile e il campo maschera a una funzione CLR che restituisce una stringa delimitata da virgole delle colonne che sono cambiate nel campo binario _ $ update_mask. Questa funzione clr interroga il campo maschera per il bit di modifica per ogni colonna nell'elenco xml e quindi restituisce il nome dal relativo ordinale.

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

Il codice c # clr è simile al seguente: (compilato in un assembly chiamato 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 la funzione al CLR in questo modo:

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]

Aggiungiamo quindi questo elenco di colonne al set di righe e passiamo al data warehouse per l'analisi. Usando la query e il clr evitiamo di usare due chiamate di funzione per riga per modifica. Possiamo saltare direttamente alla carne con risultati personalizzati per la nostra istanza di acquisizione delle modifiche.

Grazie a questo post stackoverflow suggerito da Jon Seigel per il modo in cui interpretare la maschera.

Nella nostra esperienza con questo approccio siamo in grado di ottenere un elenco di tutte le colonne modificate da 10 k righe di file in meno di 3 secondi.


Grazie per essere tornato con una soluzione, potrei averne presto bisogno.
Mark Storey-Smith,

Controlla la mia NUOVA risposta prima di te. Per quanto bello sia il CLR ... abbiamo trovato un modo ancora migliore. In bocca al lupo.
RThomas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.