La forma más eficiente de generar una diferencia

8

Tengo una tabla en el servidor SQL que se ve así:

Id    |Version  |Name    |date    |fieldA   |fieldB ..|fieldZ
1     |1        |Foo     |20120101|23       |       ..|25334123
2     |2        |Foo     |20120101|23       |NULL   ..|NULL
3     |2        |Bar     |20120303|24       |123......|NULL
4     |2        |Bee     |20120303|34       |-34......|NULL

Estoy trabajando en un procedimiento almacenado para diferenciar, que toma datos de entrada y un número de versión. Los datos de entrada tienen columnas de Nombre hasta campoZ. Se espera que la mayoría de las columnas de campo sean NULL, es decir, cada fila generalmente tiene datos solo para los primeros campos, el resto son NULL. El nombre, la fecha y la versión forman una restricción única en la tabla.

Necesito diferenciar los datos que se ingresan con respecto a esta tabla, para una versión dada. Cada fila debe diferenciarse: una fila se identifica por el nombre, la fecha y la versión, y cualquier cambio en cualquiera de los valores en las columnas del campo deberá mostrarse en la diferencia.

Actualización: no es necesario que todos los campos sean de tipo decimal. Algunos de ellos pueden ser nvarchars. Preferiría que se produzca el diff sin convertir el tipo, aunque la salida de diff podría convertir todo a nvarchar, ya que se debe usar solo para la visualización intencionada.

Supongamos que la entrada es la siguiente y la versión solicitada es 2:

Name    |date    |fieldA   |fieldB|..|fieldZ
Foo     |20120101|25       |NULL  |.. |NULL
Foo     |20120102|26       |27    |.. |NULL
Bar     |20120303|24       |126   |.. |NULL
Baz     |20120101|15       |NULL  |.. |NULL

El diff debe estar en el siguiente formato:

name    |date    |field    |oldValue    |newValue
Foo     |20120101|FieldA   |23          |25
Foo     |20120102|FieldA   |NULL        |26
Foo     |20120102|FieldB   |NULL        |27
Bar     |20120303|FieldB   |123         |126
Baz     |20120101|FieldA   |NULL        |15

Mi solución hasta ahora es generar primero un diff, usando EXCEPT y UNION. Luego, convierta el diff al formato de salida deseado usando unir y aplicar cruz. Aunque esto parece estar funcionando, me pregunto si hay una forma más limpia y eficiente de hacerlo. El número de campos es cercano a 100, y cada lugar en el código que tiene un ... es en realidad una gran cantidad de líneas. Se espera que tanto la tabla de entrada como la tabla existente sean bastante grandes con el tiempo. Soy nuevo en SQL y todavía estoy tratando de aprender el ajuste del rendimiento.

Aquí está el SQL para ello:

CREATE TABLE #diff
(   [change] [nvarchar](50) NOT NULL,
    [name] [nvarchar](50) NOT NULL,
    [date] [int] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    .....
    [FieldZ] [decimal](38, 10) NULL
)

--Generate the diff in a temporary table
INSERT INTO #diff
SELECT * FROM
(

(
    SELECT
        'old' as change,
        name,
        date,
        FieldA,
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
    EXCEPT
    SELECT 'old' as change,* FROM @diffInput
)
UNION

(
    SELECT 'new' as change, * FROM @diffInput
    EXCEPT
    SELECT
        'new' as change,
        name,
        date,
        FieldA, 
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version 
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
) 
) AS myDiff

SELECT 
d3.name, d3.date, CrossApplied.field, CrossApplied.oldValue, CrossApplied.newValue
FROM
(
    SELECT 
        d2.name, d2.date, 
        d1.FieldA AS oldFieldA, d2.FieldA AS newFieldA, 
        d1.FieldB AS oldFieldB, d2.FieldB AS newFieldB,
        ...
        d1.FieldZ AS oldFieldZ, d2.FieldZ AS newFieldZ,
    FROM #diff AS d1
    RIGHT OUTER JOIN #diff AS d2
    ON 
        d1.name = d2.name
        AND d1.date = d2.date
        AND d1.change = 'old'
    WHERE d2.change = 'new'
) AS d3
CROSS APPLY (VALUES ('FieldA', oldFieldA, newFieldA), 
                ('FieldB', oldFieldB, newFieldB),
                ...
                ('FieldZ', oldFieldZ, newFieldZ))
                CrossApplied (field, oldValue, newValue)
WHERE 
    crossApplied.oldValue != crossApplied.newValue 
    OR (crossApplied.oldValue IS NULL AND crossApplied.newValue IS NOT NULL) 
    OR (crossApplied.oldValue IS NOT NULL AND crossApplied.newValue IS NULL)  

¡Gracias!

ame
fuente

Respuestas:

5

Aquí hay otro enfoque:

SELECT
  di.name,
  di.date,
  x.field,
  x.oldValue,
  x.newValue
FROM
  @diffInput AS di
  LEFT JOIN dbo.myTable AS mt ON
    mt.version = @version
    AND mt.name = di.name
    AND mt.date = di.date
  CROSS APPLY
  (
    SELECT
      'fieldA',
      mt.fieldA,
      di.fieldA
    WHERE
      NOT EXISTS (SELECT mt.fieldA INTERSECT SELECT di.fieldA)

    UNION ALL

    SELECT
      'fieldB',
      mt.fieldB,
      di.fieldB
    WHERE
      NOT EXISTS (SELECT mt.fieldB INTERSECT SELECT di.fieldB)

    UNION ALL

    SELECT
      'fieldC',
      mt.fieldC,
      di.fieldC
    WHERE
      NOT EXISTS (SELECT mt.fieldC INTERSECT SELECT di.fieldC)

    UNION ALL

    ...
  ) AS x (field, oldValue, newValue)
;

Así es como funciona:

  1. Las dos tablas se unen usando una unión externa, que @diffInputse encuentra en el lado externo para que coincida con su unión derecha.

  2. El resultado de la unión no se divide condicionalmente usando CROSS APPLY, donde "condicionalmente" significa que cada par de columnas se prueba individualmente y se devuelve solo si las columnas difieren.

  3. El patrón de cada condición de prueba

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    es equivalente a tu

    oldValue != newValue
    OR (oldValue IS NULL AND newValue IS NOT NULL)
    OR (oldValue IS NOT NULL AND newValue IS NULL)

    Solo más conciso. Puede leer más sobre este uso de INTERSECT en detalle en el artículo de Paul White Planes de consulta indocumentados: comparaciones de igualdad .

En una nota diferente, ya que estás diciendo,

Se espera que tanto la tabla de entrada como la tabla existente sean bastante grandes con el tiempo

puede considerar reemplazar la variable de tabla que está utilizando para la tabla de entrada con una tabla temporal. Hay una respuesta muy completa de Martin Smith que explora las diferencias entre los dos:

En resumen, ciertas propiedades de las variables de tabla, como, por ejemplo, la ausencia de estadísticas de columna, pueden hacer que las consultas sean menos optimizadas para su escenario que las tablas temporales.

Andriy M
fuente
Si el tipo de datos no es el mismo para los campos AZ, los 2 campos en las instrucciones de selección deben convertirse a varchar o la declaración de unión no funcionará.
Andre
5

Edite con respecto a los campos que tienen diferentes tipos, no solo decimal.

Puedes intentar usar sql_varianttype. Nunca lo usé personalmente, pero puede ser una buena solución para su caso. Para probarlo, simplemente reemplace todo [decimal](38, 10)con sql_variantel script SQL. La consulta en sí permanece exactamente como está, no se necesita una conversión explícita para realizar la comparación. El resultado final tendría una columna con valores de diferentes tipos. Lo más probable es que eventualmente tenga que saber de alguna manera qué tipo está en qué campo procesar los resultados en su aplicación, pero la consulta en sí misma debería funcionar bien sin conversiones.


Por cierto, es una mala idea almacenar fechas como int.

En lugar de usar EXCEPTy UNIONpara calcular la diferencia, usaría FULL JOIN. Para mí, personalmente, es difícil seguir la lógica EXCEPTy el UNIONenfoque.

Comenzaría por desconectar los datos, en lugar de hacerlo en último lugar (usando CROSS APPLY(VALUES)como lo hace). Puede deshacerse de la desconexión de la entrada, si lo hace con anticipación, en el lado de la persona que llama.

Tendría que enumerar las 100 columnas solo en CROSS APPLY(VALUES).

La consulta final es bastante simple, por lo que la tabla temporal no es realmente necesaria. Creo que es más fácil de escribir y mantener que su versión. Aquí está SQL Fiddle .

Configurar datos de muestra

DECLARE @TMain TABLE (
    [ID] [int] NOT NULL,
    [Version] [int] NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TMain ([ID],[Version],[Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
(1,1,'Foo','20120101',23,23  ,25334123),
(2,2,'Foo','20120101',23,NULL,NULL),
(3,2,'Bar','20120303',24,123 ,NULL),
(4,2,'Bee','20120303',34,-34 ,NULL);

DECLARE @TInput TABLE (
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TInput ([Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
('Foo','20120101',25,NULL,NULL),
('Foo','20120102',26,27  ,NULL),
('Bar','20120303',24,126 ,NULL),
('Baz','20120101',15,NULL,NULL);

DECLARE @VarVersion int = 2;

Consulta principal

CTE_Mainson datos originales sin pivotar filtrados a los dados Version. CTE_Inputes la tabla de entrada, que podría proporcionarse ya en este formato. La consulta principal utiliza FULL JOIN, que se agrega a las filas de resultados con Bee. Creo que deberían devolverse, pero si no desea verlos, puede filtrarlos agregando AND CTE_Input.FieldValue IS NOT NULLo tal vez usando en LEFT JOINlugar de FULL JOIN, no busqué detalles allí, porque creo que deberían devolverse.

WITH
CTE_Main
AS
(
    SELECT
        Main.ID
        ,Main.Version
        ,Main.Name
        ,Main.dt
        ,FieldName
        ,FieldValue
    FROM
        @TMain AS Main
        CROSS APPLY
        (
            VALUES
                ('FieldA', Main.FieldA),
                ('FieldB', Main.FieldB),
                ('FieldZ', Main.FieldZ)
        ) AS CA(FieldName, FieldValue)
    WHERE
        Main.Version = @VarVersion
)
,CTE_Input
AS
(
    SELECT
        Input.Name
        ,Input.dt
        ,FieldName
        ,FieldValue
    FROM
        @TInput AS Input
        CROSS APPLY
        (
            VALUES
                ('FieldA', Input.FieldA),
                ('FieldB', Input.FieldB),
                ('FieldZ', Input.FieldZ)
        ) AS CA(FieldName, FieldValue)
)

SELECT
    ISNULL(CTE_Main.Name, CTE_Input.Name) AS FullName
    ,ISNULL(CTE_Main.dt, CTE_Input.dt) AS FullDate
    ,ISNULL(CTE_Main.FieldName, CTE_Input.FieldName) AS FullFieldName
    ,CTE_Main.FieldValue AS OldValue
    ,CTE_Input.FieldValue AS NewValue
FROM
    CTE_Main
    FULL JOIN CTE_Input ON 
        CTE_Input.Name = CTE_Main.Name
        AND CTE_Input.dt = CTE_Main.dt
        AND CTE_Input.FieldName = CTE_Main.FieldName
WHERE
    (CTE_Main.FieldValue <> CTE_Input.FieldValue)
    OR (CTE_Main.FieldValue IS NULL AND CTE_Input.FieldValue IS NOT NULL)
    OR (CTE_Main.FieldValue IS NOT NULL AND CTE_Input.FieldValue IS NULL)
--ORDER BY FullName, FullDate, FullFieldName;

Resultado

FullName    FullDate    FullFieldName   OldValue        NewValue
Foo         2012-01-01  FieldA          23.0000000000   25.0000000000
Foo         2012-01-02  FieldA          NULL            26.0000000000
Foo         2012-01-02  FieldB          NULL            27.0000000000
Bar         2012-03-03  FieldB          123.0000000000  126.0000000000
Baz         2012-01-01  FieldA          NULL            15.0000000000
Bee         2012-03-03  FieldB          -34.0000000000  NULL
Bee         2012-03-03  FieldA          34.0000000000   NULL
Vladimir Baranov
fuente