SQL - 比较表中的行以查找列差异 - 自联接

时间:2017-01-06 08:22:37

标签: sql sql-server sql-server-2012 self-join

我有下表:

DECLARE @TABLE_A TABLE (
   id int identity, 
   name varchar(20), 
   start_date datetime, 
   end_date datetime, 
   details nvarchar(500), 
   copied_from int)

用户可以克隆一行并将其重新插入到同一个表中,我们会记录它从哪一行复制。因此,如果您有一个ID = 1的行并且您复制了所有列并重新插入(从UI),您将获得ID = 5的新行,而新行的copied_from字段的值将为1。

此用户可以更新新的行值(本例中为ID 5),我们需要一种方法来查看2行之间的差异。我已经写了下面的内容来获取ID 1和ID 5之间的差异。

DECLARE @id int = 5
DECLARE @TABLE_A TABLE (id int identity, name varchar(20), start_date datetime, end_date datetime, details nvarchar(500), copied_from int)

INSERT INTO @TABLE_A (name, start_date, end_date, details, copied_from)
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'John', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up - changed</p>', 1 

SELECT 
    'Name' AS column_name,
    ISNULL(s.name, '') AS value_before, 
    ISNULL(t.name, '') AS value_after, 
    t.id, 
    t.copied_from
FROM @TABLE_A s
FULL OUTER JOIN @TABLE_A t ON s.id = t.copied_from
WHERE t.id = @id AND ISNULL(s.name, '') <> ISNULL(t.name, '')
UNION ALL
SELECT 
    'Details' AS column_name,
    ISNULL(s.details, '') AS value_before, 
    ISNULL(t.details, '') AS value_after, 
    t.id, 
    t.copied_from
FROM @TABLE_A s
FULL OUTER JOIN @TABLE_A t ON s.id = t.copied_from
WHERE t.id = @id AND ISNULL(s.details, '') <> ISNULL(t.details, '')

.......

正如您所看到的,ID和COPIED_FROM字段中存在自联接,并且对于每个列,我检查是否存在差异。

这有效,但不知何故我对每列的重复UNIONS感到不满意,我想知道是否还有其他方法可以达到这个目的?

由于

4 个答案:

答案 0 :(得分:1)

尝试以下脚本,这可能会对您有所帮助。使用CASE WHEN表达式,我们可以识别修改的列。但是这将只返回包含所有细节的单个记录(在值之前,在值和状态之后 - 1:修改/ 0:不是)。

SELECT  ISNULL(s.name, '')                                      AS name_before, 
        ISNULL(t.name, '')                                      AS name_after, 
        (case when s.name <> t.name then 1 else 0 end)          AS name_status,

        ISNULL(s.details, '')                                   AS details_before, 
        ISNULL(t.details, '')                                   AS details_after, 
        (case when s.details <> t.details then 1 else 0 end)    AS details_status
FROM    @TABLE_A s 
INNER JOIN @TABLE_A t ON s.id = t.copied_from
WHERE   t.id = @id

答案 1 :(得分:1)

你可以使用动态脚本,即使表有数百列,也没关系。

   CREATE TABLE #tt (id int identity, name varchar(20), start_date datetime, end_date datetime, details nvarchar(500), copied_from int)

    INSERT INTO #tt (name, start_date, end_date, details, copied_from)
    SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
    SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
    SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
    SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
    SELECT 'John', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up - changed</p>', 1 
    DECLARE @cols VARCHAR(max),@sql VARCHAR(max)
    SELECT @cols=ISNULL(@cols+',('''+c.Name+''',CONVERT(VARCHAR,o.[','('''+c.Name+''',CONVERT(VARCHAR,o.[')+c.name+']),CONVERT(VARCHAR,c.['+c.name+']))'
    FROM tempdb.sys.columns AS c WHERE c.object_id=OBJECT_ID('tempdb..#tt')
    PRINT @cols
    SET @sql='
    SELECT x.* FROM #tt AS c
    LEFT JOIN #tt AS o ON c.copied_from=o.id
    CROSS APPLY(values'+@cols+') AS x(columnName,OrignalValue,CopiedValue) 
    WHERE c.copied_from IS NOT NULL'
    PRINT @sql
    EXEC(@sql)
columnName  OrignalValue                   CopiedValue
----------- ------------------------------ ------------------------------
id          1                              5
name        Tom                            John
start_date  Jan  1 2017 12:00AM            Jan  1 2017 12:00AM
end_date    Feb  1 2017 12:00AM            Feb  1 2017 12:00AM
details     

this column can contain htm

this column can contain htm copied_from NULL 1

答案 2 :(得分:1)

好吧,我想最初的要求是将所有更改聚合到一个集合中(ColumnName,before,after,id,copied_from)。

我想您可能希望使用unpivot,例如:

DECLARE @id int = 5
DECLARE @TABLE_A TABLE (id int identity, name varchar(20), start_date datetime, end_date datetime, details nvarchar(500), copied_from int)

INSERT INTO @TABLE_A (name, start_date, end_date, details, copied_from)
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'Tom', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up</p>', null UNION ALL
SELECT 'John', '2017-01-01', '2017-02-01', '<p>this column can contain html mark up - changed</p>', 1 ;

WITH    t AS ( SELECT   id,
                        CAST(ISNULL(name, '') AS NVARCHAR(500)) name,
                        CAST(start_date AS NVARCHAR(500)) start_date,
                        CAST(end_date AS NVARCHAR(500)) end_date,
                        details,
                        copied_from
               FROM     @TABLE_A
             ),
        m AS ( SELECT   u.id,
                        u.copied_from,
                        u.column_name,
                        u.data
               FROM     t UNPIVOT( data FOR column_name IN ( name, start_date,
                                                             end_date, details ) ) u
             )
    SELECT  toT.column_name,
            fromT.data value_before,
            toT.data value_after,
            toT.id,
            toT.copied_from
    FROM    m fromT
    INNER JOIN m toT ON toT.copied_from = fromT.id AND
                        toT.column_name = fromT.column_name AND
                        toT.data <> fromT.data;

注意:我必须将所有字段强制转换为nvarchar(对于需要取消忽略的所有列都是一致的),否则UNPIVOT将无效...

答案 3 :(得分:0)

您也可以通过使用INNER JOIN来实现这一点,但我想说这将是以单行显示更改值并且更具可读性的正确方法

    select s.name AS [name_before], 
    t.name AS [name_after], 
    s.details AS [detail_before], 
    t.details AS [detail_after], 
    t.id, 
    s.id AS [copied_from]
FROM @TABLE_A s 
INNER JOIN @TABLE_A t ON s.id = t.copied_from
WHERE   t.id = @id
    AND (s.name <> t.name OR s.details <> t.details)

我们可以使用OR我们必须显示两者或一个来自名称&amp;细节是变化。