Cambiar captura de datos y el binario __ $ update_mask

9

Estamos utilizando CDC para capturar los cambios realizados en una tabla de producción. Las filas modificadas se exportan a un almacén de datos (informática). Sé que la columna __ $ update_mask almacena qué columnas se actualizaron en forma varbinary. También sé que puedo usar una variedad de funciones CDC para descubrir de esa máscara cuáles eran esas columnas.

Mi pregunta es esta ¿Alguien puede definir para mí la lógica detrás de esa máscara para que podamos identificar las columnas que se cambiaron en el almacén? Como estamos procesando fuera del servidor, no tenemos fácil acceso a esas funciones MSSQL CDC. Prefiero romper la máscara yo mismo en código. El rendimiento de las funciones de CDC en el extremo SQL es problemático para esta solución.

En resumen, me gustaría identificar las columnas modificadas a mano desde el campo __ $ update_mask.

Actualizar:

Como alternativa, el envío de una lista legible por humanos de columnas cambiadas al almacén también era aceptable. Descubrimos que esto podría realizarse con un rendimiento mucho mayor que nuestro enfoque original.

La respuesta CLR a esta pregunta a continuación cumple con esta alternativa e incluye detalles de interpretación de la máscara para futuros visitantes. Sin embargo, la respuesta aceptada utilizando XML PATH es la más rápida hasta el momento para el mismo resultado final.

RThomas
fuente

Respuestas:

11

Y la moraleja de la historia es ... probar, probar otras cosas, pensar en grande, luego en pequeño, siempre asumir que hay una mejor manera.

Tan científicamente interesante como fue mi última respuesta. Decidí probar otro enfoque. Recordé que podía hacer concat con el truco XML PATH (''). Como sabía cómo obtener el ordinal de cada columna modificada de la lista capture_column de la respuesta anterior, pensé que valdría la pena probar si la función de bit MS funcionaría mejor de esa manera para lo que necesitábamos.

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 mucho más limpio que (aunque no tan divertido como) todo ese CLR, devuelve el enfoque solo al código SQL nativo. Y, redoble de tambor ... devuelve los mismos resultados en menos de un segundo . Dado que los datos de producción son 100 veces mayores, cada segundo cuenta.

Dejo la otra respuesta con fines científicos, pero por ahora, esta es nuestra respuesta correcta.

RThomas
fuente
Agregue _CT al nombre de la tabla en la cláusula FROM.
Chris Morley
1
Gracias por regresar y responder esto, estoy buscando una solución muy similar para que podamos filtrarla en consecuencia dentro del código una vez que se haya realizado una llamada SQL. ¡No me gustaría hacer una llamada para cada columna en cada fila devuelta por CDC!
nik0lias
2

Entonces, después de algunas investigaciones, decidimos seguir haciendo esto en el lado de SQL antes de entregarlo al almacén de datos. Pero estamos adoptando este enfoque mejorado (basado en nuestras necesidades y una nueva comprensión de cómo funciona la máscara).

Obtenemos una lista de los nombres de columna y sus posiciones ordinales con esta consulta. El retorno vuelve en un formato XML para que podamos pasar a 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);

Luego pasamos ese bloque XML como variable y el campo de máscara a una función CLR que devuelve una cadena delimitada por comas de las columnas que cambió según el campo binario _ $ update_mask. Esta función clr interroga el campo de máscara para el bit de cambio para cada columna en la lista xml y luego devuelve su nombre desde el 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;

El código c # clr se ve así: (compilado en un ensamblaje llamado 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;
        }
    }
}

Y la función para el CLR es la siguiente:

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]

Luego agregamos esta lista de columnas al conjunto de filas y la pasamos al almacén de datos para su análisis. Al usar la consulta y el clr, evitamos tener que usar dos llamadas de función por fila por cambio. Podemos pasar directamente a la carne con resultados personalizados para nuestra instancia de captura de cambios.

Gracias a esta publicación de stackoverflow sugerida por Jon Seigel por la manera de interpretar la máscara.

En nuestra experiencia con este enfoque, podemos obtener una lista de todas las columnas modificadas de 10k filas de cdc en menos de 3 segundos.

RThomas
fuente
Gracias por regresar con una solución, podría haberlo usado pronto.
Mark Storey-Smith
Mira mi NUEVA respuesta antes de hacerlo. Tan genial como el CLR es ... encontramos una forma aún mejor. Buena suerte.
RThomas