Modifier la capture de données et le binaire __ $ update_mask


9

Nous utilisons CDC pour capturer les modifications apportées à une table de production. Les lignes modifiées sont exportées vers un entrepôt de données (informatica). Je sais que la colonne __ $ update_mask stocke quelles colonnes ont été mises à jour sous une forme varbinaire. Je sais également que je peux utiliser une variété de fonctions CDC pour découvrir à partir de ce masque quelles étaient ces colonnes.

Ma question est la suivante. Quelqu'un peut-il définir pour moi la logique derrière ce masque afin que nous puissions identifier les colonnes qui ont été changées dans l'entrepôt? Puisque nous traitons en dehors du serveur, nous n'avons pas facilement accès à ces fonctions CDC MSSQL. Je préfère simplement décomposer le masque moi-même en code. Les performances des fonctions cdc du côté SQL sont problématiques pour cette solution.

En bref, j'aimerais identifier manuellement les colonnes modifiées à partir du champ __ $ update_mask.

Mise à jour:

En tant qu'alternative, l'envoi d'une liste lisible par l'homme des colonnes modifiées à l'entrepôt était également acceptable. Nous avons constaté que cela pouvait être effectué avec des performances bien supérieures à notre approche d'origine.

La réponse du CLR à cette question ci-dessous répond à cette alternative et comprend des détails d'interprétation du masque pour les futurs visiteurs. Cependant, la réponse acceptée en utilisant XML PATH est la plus rapide pour le même résultat final.


Réponses:


11

Et la morale de l'histoire est ... tester, essayer d'autres choses, voir grand, puis petit, supposer toujours qu'il y a une meilleure façon.

Aussi intéressant scientifiquement que ma dernière réponse. J'ai décidé d'essayer une autre approche. Je me suis souvenu que je pouvais concaténer avec l'astuce XML PATH (''). Comme je savais comment obtenir l'ordinal de chaque colonne modifiée à partir de la liste capturée_colonne de la réponse précédente, je pensais que cela valait la peine de tester si la fonction de bit MS fonctionnerait mieux de cette façon pour ce dont nous avions besoin.

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

C'est beaucoup plus propre que (mais pas aussi amusant que) tout ce CLR, renvoie l'approche au code SQL natif uniquement. Et, roulement de tambour .... renvoie les mêmes résultats en moins d'une seconde . Étant donné que les données de production sont 100 fois plus importantes chaque seconde compte.

Je laisse l'autre réponse à des fins scientifiques - mais pour l'instant, c'est notre bonne réponse.


Ajoutez _CT au nom de la table dans la clause FROM.
Chris Morley

1
Merci d'être revenu et d'avoir répondu à cela, je recherche une solution très similaire afin que nous puissions la filtrer en conséquence dans le code une fois qu'un appel SQL a été effectué. Je n'ai pas envie de faire un appel pour chaque colonne sur chaque ligne retournée par CDC!
nik0lias

2

Ainsi, après quelques recherches, nous avons décidé de continuer à le faire côté SQL avant de passer à l'entrepôt de données. Mais nous adoptons cette approche bien améliorée (basée sur nos besoins et une nouvelle compréhension du fonctionnement du masque).

Nous obtenons une liste des noms de colonnes et leurs positions ordinales avec cette requête. Le retour revient dans un format XML afin que nous puissions passer à 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);

Nous passons ensuite ce bloc XML en tant que variable et le champ de masque à une fonction CLR qui renvoie une chaîne délimitée par des virgules des colonnes modifiées par le champ binaire _ $ update_mask. Cette fonction clr interroge le champ de masque pour le bit de changement pour chaque colonne de la liste xml, puis renvoie son nom à partir de l'ordinal associé.

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

Le code c # clr ressemble à ceci: (compilé dans un assembly appelé 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;
        }
    }
}

Et la fonction du CLR comme ceci:

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]

Nous ajoutons ensuite cette liste de colonnes à l'ensemble de lignes et passons à l'entrepôt de données pour analyse. En utilisant la requête et le clr, nous évitons d'avoir à utiliser deux appels de fonction par ligne par changement. Nous pouvons passer directement à la viande avec des résultats personnalisés pour notre instance de capture de changement.

Merci à ce post stackoverflow suggéré par Jon Seigel pour la manière d'interpréter le masque.

D'après notre expérience avec cette approche, nous sommes en mesure d'obtenir une liste de toutes les colonnes modifiées à partir de 10 000 lignes cdc en moins de 3 secondes.


Merci d'être revenu avec une solution, j'aurais peut-être besoin de ça bientôt.
Mark Storey-Smith

Consultez ma NOUVELLE réponse avant de le faire. Aussi cool que soit le CLR ... nous avons trouvé un moyen encore meilleur. Bonne chance.
RThomas
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.