Ändern Sie die Datenerfassung und die Binärdatei __ $ update_mask


9

Wir verwenden CDC, um Änderungen an einer Produktionstabelle zu erfassen. Die geänderten Zeilen werden in ein Data Warehouse (informatica) exportiert. Ich weiß, dass die Spalte __ $ update_mask speichert, welche Spalten in einer varbinären Form aktualisiert wurden. Ich weiß auch, dass ich eine Vielzahl von CDC-Funktionen verwenden kann , um anhand dieser Maske herauszufinden, was diese Spalten waren.

Meine Frage ist dies. Kann jemand für mich die Logik hinter dieser Maske definieren, damit wir die Spalten identifizieren können, die im Lager umgestellt wurden? Da wir außerhalb des Servers verarbeiten, haben wir keinen einfachen Zugriff auf diese MSSQL CDC-Funktionen. Ich würde die Maske lieber selbst in Code zerlegen. Die Leistung der cdc-Funktionen auf SQL-Seite ist für diese Lösung problematisch.

Kurz gesagt, ich möchte geänderte Spalten von Hand aus dem Feld __ $ update_mask identifizieren.

Aktualisieren:

Als Alternative war es auch akzeptabel, eine von Menschen lesbare Liste geänderter Spalten an das Lager zu senden. Wir haben festgestellt, dass dies mit einer Leistung durchgeführt werden kann, die weit über unserem ursprünglichen Ansatz liegt.

Die unten stehende CLR-Antwort auf diese Frage erfüllt diese Alternative und enthält Details zur Interpretation der Maske für zukünftige Besucher. Die akzeptierte Antwort mit XML PATH ist jedoch die bisher schnellste für das gleiche Endergebnis.


Antworten:


11

Und die Moral der Geschichte ist ... testen, andere Dinge ausprobieren, groß denken, dann klein, immer davon ausgehen, dass es einen besseren Weg gibt.

So wissenschaftlich interessant wie meine letzte Antwort war. Ich beschloss, einen anderen Ansatz zu versuchen. Ich erinnerte mich, dass ich mit dem XML PATH ('') - Trick konatieren konnte. Da ich wusste, wie ich die Ordnungszahl jeder geänderten Spalte aus der Liste der erfassten Spalten aus der vorherigen Antwort ermitteln konnte, hielt ich es für sinnvoll, zu testen, ob die MS-Bit-Funktion auf diese Weise für das, was wir brauchten, besser funktioniert.

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

Es ist viel sauberer als (wenn auch nicht so unterhaltsam wie) die gesamte CLR und gibt den Ansatz nur für nativen SQL-Code zurück. Und Trommelwirbel ... liefert die gleichen Ergebnisse in weniger als einer Sekunde . Da die Produktionsdaten 100-mal größer sind, zählt jede Sekunde.

Ich lasse die andere Antwort für wissenschaftliche Zwecke offen - aber im Moment ist dies unsere richtige Antwort.


Hängen Sie _CT an den Tabellennamen in der FROM-Klausel an.
Chris Morley

1
Vielen Dank, dass Sie zurückgekommen sind und darauf geantwortet haben. Ich suche nach einer sehr ähnlichen Lösung, damit wir sie nach Abschluss eines SQL-Aufrufs im Code entsprechend filtern können. Ich möchte nicht für jede Spalte in jeder von CDC zurückgegebenen Zeile einen Anruf tätigen!
Nik0lias

2

Nach einigen Recherchen haben wir uns daher entschlossen, dies weiterhin auf der SQL-Seite zu tun, bevor wir es an das Data Warehouse übergeben. Aber wir verfolgen diesen viel verbesserten Ansatz (basierend auf unseren Bedürfnissen und dem neuen Verständnis der Funktionsweise der Maske).

Mit dieser Abfrage erhalten wir eine Liste der Spaltennamen und ihrer Ordnungspositionen. Die Rückgabe erfolgt in einem XML-Format, damit wir sie an SQL CLR weitergeben können.

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

Wir übergeben diesen XML-Block dann als Variable und das Maskenfeld an eine CLR-Funktion, die eine durch Kommas getrennte Zeichenfolge der Spalten zurückgibt, die sich gemäß dem Binärfeld _ $ update_mask geändert haben. Diese clr-Funktion fragt das Maskenfeld nach dem Änderungsbit für jede Spalte in der XML-Liste ab und gibt dann den Namen von der zugehörigen Ordnungszahl zurück.

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

Der c # clr-Code sieht folgendermaßen aus: (kompiliert in eine Assembly namens 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;
        }
    }
}

Und die Funktion zur CLR lautet wie folgt:

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]

Anschließend hängen wir diese Spaltenliste an das Rowset an und übergeben sie zur Analyse an das Data Warehouse. Durch die Verwendung der Abfrage und des clr müssen keine zwei Funktionsaufrufe pro Zeile und Änderung verwendet werden. Wir können direkt zum Fleisch mit Ergebnissen springen, die für unsere Änderungserfassungsinstanz angepasst wurden.

Dank dieses von Jon Seigel vorgeschlagenen Stackoverflow-Beitrags für die Interpretation der Maske.

Nach unserer Erfahrung mit diesem Ansatz können wir in weniger als 3 Sekunden eine Liste aller geänderten Spalten aus 10.000 cdc-Zeilen abrufen.


Vielen Dank, dass Sie mit einer Lösung zurückgekehrt sind, die ich möglicherweise bald verwenden werde.
Mark Storey-Smith

Schauen Sie sich meine NEUE Antwort an, bevor Sie dies tun. So cool die CLR auch ist ... wir haben einen noch besseren Weg gefunden. Viel Glück.
RThomas
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.