Łatwo wyświetlaj wiersze różniące się między dwiema tabelami lub zapytaniami

19

Wyobraź sobie, że masz dwie różne tabele / zapytania, które powinny mieć / zwracać identyczne dane. Chcesz to zweryfikować. Jaki jest łatwy sposób na wyświetlenie niepasujących wierszy z każdej tabeli, tak jak w poniższym przykładzie, porównując każdą kolumnę? Załóżmy, że w tabelach znajduje się 30 kolumn, z których wiele ma wartość NULLable.

Gdy nie ma PK lub mogą istnieć duplikaty na PK, łączenie tylko na kolumnach PK nie wystarczy, a wykonaniem PEŁNEGO DOŁĄCZENIA z 30 warunkami łączenia, które poprawnie obsługują wartości NULL, jest nieprzyjemne, a także paskudny GDZIE aby wykluczyć dopasowane wiersze.

Zwykle problem jest najgorszy, a pisząc nowe zapytanie do nieskrobanych lub nie w pełni zrozumiałych danych, a prawdopodobieństwo logicznie dostępnego PK jest bardzo niskie. Przygotowuję dwa różne sposoby rozwiązania problemu, a następnie porównuję ich wyniki, różnice podkreślają szczególne przypadki w danych, których nie znałem.

Wynik musi wyglądać następująco:

Which   Col1   Col2   Col3   ... Col30
------  ------ ------ ------     ------
TableA  Cat    27     86               -- mismatch
TableB  Cat    27     105              -- mismatch
TableB  Cat    27     87               -- mismatch 2
TableA  Cat    128    92               -- no corresponding row
TableB  Lizard 83     NULL             -- no corresponding row

Jeśli [Col1, Col2]zdarzy się, że jest kluczem złożonym i zamawiamy go w naszym wyniku końcowym, możemy łatwo zobaczyć, że A i B mają jeden wiersz inny, który powinien być taki sam, a każdy ma jeden wiersz, którego nie ma w drugim.

W powyższym przykładzie dwukrotne wyświetlenie pierwszego rzędu nie jest pożądane.

Oto DDL i DML, aby skonfigurować przykładowe tabele i dane:

CREATE TABLE dbo.TableA (
   Col1 varchar(10),
   Col2 int,
   Col3 int,
   Col4 varchar(10),
   Col5 varchar(10),
   Col6 varchar(10),
   Col7 varchar(10),
   Col8 varchar(10),
   Col9 varchar(10),
   Col10 varchar(10),
   Col11 varchar(10),
   Col12 varchar(10),
   Col13 varchar(10),
   Col14 varchar(10),
   Col15 varchar(10),
   Col16 varchar(10),
   Col17 varchar(10),
   Col18 varchar(10),
   Col19 varchar(10),
   Col20 varchar(10),
   Col21 varchar(10),
   Col22 varchar(10),
   Col23 varchar(10),
   Col24 varchar(10),
   Col25 varchar(10),
   Col26 varchar(10),
   Col27 varchar(10),
   Col28 varchar(10),
   Col29 varchar(10),
   Col30 varchar(10)
);

CREATE TABLE dbo.TableB (
   Col1 varchar(10),
   Col2 int,
   Col3 int,
   Col4 varchar(10),
   Col5 varchar(10),
   Col6 varchar(10),
   Col7 varchar(10),
   Col8 varchar(10),
   Col9 varchar(10),
   Col10 varchar(10),
   Col11 varchar(10),
   Col12 varchar(10),
   Col13 varchar(10),
   Col14 varchar(10),
   Col15 varchar(10),
   Col16 varchar(10),
   Col17 varchar(10),
   Col18 varchar(10),
   Col19 varchar(10),
   Col20 varchar(10),
   Col21 varchar(10),
   Col22 varchar(10),
   Col23 varchar(10),
   Col24 varchar(10),
   Col25 varchar(10),
   Col26 varchar(10),
   Col27 varchar(10),
   Col28 varchar(10),
   Col29 varchar(10),
   Col30 varchar(10)
);

INSERT dbo.TableA (Col1, Col2, Col3, Col4, Col5, Col6, Col7, Col8, Col9, Col10, Col11, Col12, Col13, Col14, Col15, Col16, Col17, Col18, Col19, Col20, Col21, Col22, Col23, Col24, Col25, Col26, Col27, Col28, Col29, Col30)
VALUES
   ('Cat', 27, 86, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Cat', 128, 92, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Porcupine', NULL, 42, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Tapir', NULL, NULL, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0')
;

INSERT dbo.TableB (Col1, Col2, Col3, Col4, Col5, Col6, Col7, Col8, Col9, Col10, Col11, Col12, Col13, Col14, Col15, Col16, Col17, Col18, Col19, Col20, Col21, Col22, Col23, Col24, Col25, Col26, Col27, Col28, Col29, Col30)
VALUES
   ('Cat', 27, 105, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Cat', 27, 87, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Lizard', 83, NULL, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Porcupine', NULL, 42, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0'),
   ('Tapir', NULL, NULL, 'a', 'b', 'c', 'd', 'e', 'f', 'g',' h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0');
ErikE
źródło

Odpowiedzi:

17

Tutaj nie potrzebujesz 30 warunków dołączenia FULL OUTER JOIN.

Możesz po prostu wykonać pełny łącznik zewnętrzny na PK, zachować rzędy z co najmniej jedną różnicą WHERE EXISTS (SELECT A.* EXCEPT SELECT B.*)i użyć, CROSS APPLY (SELECT A.* UNION ALL SELECT B.*)aby rozdzielić obie strony JOINrzędów ed na poszczególne rzędy.

WITH TableA(Col1, Col2, Col3) 
     AS (SELECT 'Dog',1,1     UNION ALL 
         SELECT 'Cat',27,86   UNION ALL 
         SELECT 'Cat',128,92), 
     TableB(Col1, Col2, Col3) 
     AS (SELECT 'Dog',1,1     UNION ALL 
         SELECT 'Cat',27,105  UNION ALL 
         SELECT 'Lizard',83,NULL) 
SELECT CA.*
FROM   TableA A 
       FULL OUTER JOIN TableB B 
         ON A.Col1 = B.Col1 
            AND A.Col2 = B.Col2 
/*Unpivot the joined rows*/
CROSS APPLY (SELECT 'TableA' AS what, A.* UNION ALL
             SELECT 'TableB' AS what, B.*) AS CA     
/*Exclude identical rows*/
WHERE  EXISTS (SELECT A.* 
               EXCEPT 
               SELECT B.*) 
/*Discard NULL extended row*/
AND CA.Col1 IS NOT NULL      
ORDER BY CA.Col1, CA.Col2

Daje

what   Col1   Col2        Col3
------ ------ ----------- -----------
TableA Cat    27          86
TableB Cat    27          105
TableA Cat    128         92
TableB Lizard 83          NULL

Lub wersja zajmująca się przeniesionymi słupkami bramki.

SELECT DISTINCT CA.*
FROM   TableA A 
       FULL OUTER JOIN TableB B 
         ON EXISTS (SELECT A.*  INTERSECT  SELECT B.*) 
CROSS APPLY (SELECT 'TableA' AS what, A.* UNION ALL
             SELECT 'TableB' AS what, B.*) AS CA     
WHERE NOT EXISTS (SELECT A.*  INTERSECT  SELECT B.*) 
AND CA.Col1 IS NOT NULL
ORDER BY CA.Col1, CA.Col2  

W przypadku tabel z wieloma kolumnami identyfikacja konkretnych kolumn (kolumn) może być nadal trudna. Do tego możesz potencjalnie skorzystać z poniższych.

(chociaż tylko na stosunkowo małych tabelach, ponieważ w przeciwnym razie ta metoda prawdopodobnie nie będzie miała odpowiedniej wydajności)

SELECT t1.primary_key,
       y1.c,
       y1.v,
       y2.v
FROM   t1
       JOIN t2
         ON t1.primary_key = t2.primary_key
       CROSS APPLY (SELECT t1.*
                    FOR xml path('row'), elements xsinil, type) x1(x)
       CROSS APPLY (SELECT t2.*
                    FOR xml path('row'), elements xsinil, type) x2(x)
       CROSS APPLY (SELECT n.n.value('local-name(.)', 'sysname'),
                           n.n.value('.', 'nvarchar(max)')
                    FROM   x1.x.nodes('row/*') AS n(n)) y1(c, v)
       CROSS APPLY (SELECT n.n.value('local-name(.)', 'sysname'),
                           n.n.value('.', 'nvarchar(max)')
                    FROM   x2.x.nodes('row/*') AS n(n)) y2(c, v)
WHERE  y1.c = y2.c
       AND EXISTS(SELECT y1.v
                  EXCEPT
                  SELECT y2.v) 
Martin Smith
źródło
22

Można to obsłużyć za pomocą EXCEPT i / lub INTERSECT. http://msdn.microsoft.com/en-us/library/ms188055.aspx

Najpierw znajdź wszystkie rekordy znajdujące się w tabeli 1, których nie ma w tabeli 2, a następnie znajdź wszystkie rekordy znajdujące się w tabeli 2, których nie ma w tabeli 1.

SELECT * FROM table1
EXCEPT
SELECT * FROM table2

UNION

SELECT * FROM table2
EXCEPT
SELECT * FROM table1

Jest bez wątpienia bardziej skuteczny sposób, aby to zrobić, ale jest to pierwsze „szybkie i brudne” rozwiązanie z mojej głowy. Ponadto nie polecam używania * symbolu wieloznacznego, ale nadaje się tutaj dla zwięzłości.

Alternatywnie możesz użyć operatora INTERSECT i wykluczyć z niego wszystkie wyniki.

hqrsie
źródło
6
Zawiń SELECT ... EXCEPT ... SELECTinną SELECTinstrukcję i dodaj nazwę tabeli, tj. SELECT "table1", T1.* FROM (SELECT ... EXCEPT ... SELECT) T1Następnie UNIONdrugą połowę zapytania.
Simon Righarts,
7

Łatwo to zrobić za pomocą narzędzia innej firmy, takiego jak Data Compare, lub po prostu zrób to na kliencie. W kontekście testowania jednostkowego procedur przechowywanych właśnie napisaliśmy trochę kodu C #.

Oto używany przez nas kod C # cytowany ze starego artykułu: Zamknij te luki - testowanie przechowywanych procedur

   internal static class DataSetComparer
   {
      internal static bool Compare(DataSet one, DataSet two)
      {
         if(one.Tables.Count != two.Tables.Count)
            return false;

         for(int i = 0; i < one.Tables.Count; i++)
            if(!CompareTables(one.Tables[i], two.Tables[i]))
               return false;

         return true;
        }

      private static bool CompareTables(DataTable one, DataTable two)
      {
         if(one.Rows.Count != two.Rows.Count)
            return false;

         for(int i = 0; i < one.Rows.Count; i++)
            if(!CompareRows(one.Rows[i], two.Rows[i]))
               return false;

         return true;
      }

      private static bool CompareRows(DataRow one, DataRow two)
      {
         if(one.ItemArray.Length != two.ItemArray.Length)
            return false;

         for(int i = 0; i < one.ItemArray.Length; i++)
            if(!CompareItems(one.ItemArray[i], two.ItemArray[i]))
               return false;

         return true;
      }

      private static bool CompareItems(object value1, object value2)
      {
         if(value1.GetType() != value2.GetType())
            return false;

         if(value1 is DBNull)
            return true;

         if(value1 is DateTime)
            return ((DateTime) value1).CompareTo((DateTime) value2)
                                                              == 0;

         if(value1 is byte[])
         {
            if(((byte[]) value1).Length != ((byte[]) value2).Length)
               return false;

            for(int i = 0; i < ((byte[]) value1).Length; i++)
               if(((byte[]) value1)[i] != ((byte[]) value2)[i])
                  return false;

            return true;
         }

         return value1.ToString().Equals(value2.ToString());
      }
   }
AK
źródło
4

Oto sposób, aby pokazać, o co poproszono:

SELECT
   Which = 'TableA',
   *
FROM (
   SELECT * FROM dbo.TableA
   EXCEPT
   SELECT * FROM dbo.TableB
) X
UNION ALL
SELECT
   'TableB',
   *
FROM (
   SELECT * FROM dbo.TableB
   EXCEPT
   SELECT * FROM dbo.TableA
) X
ORDER BY
   Col1, Col2, Col3, Col4, Col5, Col6, Col7, Col8, Col9, Col10, Col11, Col12, Col13, Col14, Col15, Col16, Col17, Col18, Col19, Col20, Col21, Col22, Col23, Col24, Col25, Col26, Col27, Col28, Col29, Col30
;
ErikE
źródło