Compare 2 Tables

  • Paul great stuff. I will add in the remaining 65 columns :).

    Many Thanks,

    Phil.

    PS: If I add all additional columns from the table to which sections of your code must they be added?

    -------------------------------------------------------------------------------------
    A neutron walks into a bar. "I'd like a beer" he says. The bartender promptly serves up a beer. "How much will that be?" asks the neutron. "For you?" replies the bartender, "no charge."

    Two hydrogen atoms walk into a bar. One says, 'I think I've lost an electron.' The other says 'Are you sure?' The first says, 'Yes, I'm positive... '

    Tommy Cooper

  • 2Tall (3/10/2010)


    Paul great stuff. I will add in the remaining 65 columns :). If I add all additional columns from the table to which sections of your code must they be added?

    I thought there might be more columns. Add columns to the Selection CTE and the UNPIVOT list.

    I would encourage you to run each CTE section of the query separately and experiment to make sure you fully understand how it works 🙂

    Paul

  • Demonstration code:

    -- For demonstration purposes

    USE tempdb;

    GO

    -- Drop tempdb objects

    IF OBJECT_ID(N'tempdb.dbo.Products', N'U')

    IS NOT NULL

    DROP TABLE tempdb.dbo.Products;

    IF OBJECT_ID(N'tempdb.dbo.ProductsHistory', N'U')

    IS NOT NULL

    DROP TABLE tempdb.dbo.ProductsHistory;

    IF OBJECT_ID(N'tempdb.dbo.ShowProductAudit', N'P')

    IS NOT NULL

    DROP PROCEDURE dbo.ShowProductAudit;

    GO

    -- Create tables

    CREATE TABLE [dbo].[Products](

    [Product] [bigint] IDENTITY(1,1) NOT NULL,

    [ProductId] [nvarchar](100) NOT NULL CONSTRAINT [DF_Products_Product] DEFAULT (''),

    [ProductDescription] [nvarchar](300) NOT NULL CONSTRAINT [DF_Products_ProductDescription] DEFAULT (''),

    [Type] [char](1) COLLATE Latin1_General_CI_AS NOT NULL CONSTRAINT [DF_Products_Type] DEFAULT (''),

    [StandardPrice] [decimal](17, 5) NOT NULL CONSTRAINT [DF_Products_StandardPrice] DEFAULT ((0)),

    [EngineeringOnly] [bit] NOT NULL CONSTRAINT [DF_Products_EngineeringOnly] DEFAULT ((0)),

    [StructureVersion] [bigint] NULL,

    CONSTRAINT [PK_Products] PRIMARY KEY NONCLUSTERED ([Product] ASC)

    );

    GO

    CREATE TABLE [dbo].[ProductsHistory]

    (

    [ProductHistory] [bigint] IDENTITY(1,1) NOT NULL,

    [Product] [bigint] NOT NULL,

    [ProductId] [nvarchar](100) NOT NULL CONSTRAINT [DF_ProductHistory_Product] DEFAULT (''),

    [ProductDescription] [nvarchar](300) NOT NULL CONSTRAINT [DF_ProductHistory_ProductDescription] DEFAULT (''),

    [Type] [char](1) COLLATE Latin1_General_CI_AS NOT NULL CONSTRAINT [DF_ProductHistory_Type] DEFAULT (''),

    [StandardPrice] [decimal](17, 5) NOT NULL CONSTRAINT [DF_ProductHistory_StandardPrice] DEFAULT ((0)),

    [EngineeringOnly] [bit] NOT NULL CONSTRAINT [DF_ProductHistory_EngineeringOnly] DEFAULT ((0)),

    [StructureVersion] [bigint] NULL,

    [HistoricUser] [nvarchar](20) COLLATE Latin1_General_CI_AS NOT NULL,

    [HistoricDate] [datetime] NOT NULL,

    CONSTRAINT [PK_ProductsHistory] PRIMARY KEY NONCLUSTERED ([ProductHistory] ASC)

    )

    GO

    -- Create the update audit trigger

    -- UPDATEs cause the pre-update data to be written to the history table

    CREATE TRIGGER [dbo.Products AIU]

    ON dbo.Products

    AFTER UPDATE

    AS

    BEGIN

    SET NOCOUNT ON;

    SET ROWCOUNT 0;

    INSERT ProductsHistory

    (Product, ProductId, ProductDescription, Type, StandardPrice, EngineeringOnly, StructureVersion, HistoricUser, HistoricDate)

    SELECT Product, ProductId, ProductDescription, Type, StandardPrice, EngineeringOnly, StructureVersion, SUSER_SNAME(), CURRENT_TIMESTAMP

    FROM deleted;

    END

    GO

    --Insert a sample product

    SET IDENTITY_INSERT dbo.Products ON

    INSERT Products

    (Product,ProductId,ProductDescription,Type,StandardPrice,EngineeringOnly,StructureVersion)

    VALUES (55085, N'11-222-333', N'Assembly', 'A', 123.45678, 0, 55104);

    SET IDENTITY_INSERT dbo.Products OFF;

    GO

    -- The procedure to show audit details for a product

    CREATE PROCEDURE dbo.ShowProductAudit

    @product BIGINT

    AS

    BEGIN

    SET NOCOUNT ON;

    -- Table to hold each column that changed

    CREATE TABLE #KeyValuePairs

    (

    Sequence BIGINT NOT NULL,

    ProductHistory BIGINT NOT NULL,

    Name SYSNAME NOT NULL,

    Value SQL_VARIANT NOT NULL,

    PRIMARY KEY (Name, Sequence)

    );

    WITH SourceData

    AS (

    -- All the columns from the History table

    -- converted to SQL_VARIANT

    SELECT PH.ProductHistory,

    ProductId = CONVERT(SQL_VARIANT, PH.ProductId),

    ProductDescription = CONVERT(SQL_VARIANT, PH.ProductDescription),

    Type = CONVERT(SQL_VARIANT, PH.Type),

    StandardPrice = CONVERT(SQL_VARIANT, PH.StandardPrice),

    EngineeringOnly = CONVERT(SQL_VARIANT, PH.EngineeringOnly),

    StructureVersion = CONVERT(SQL_VARIANT, PH.StructureVersion),

    HistoricUser = CONVERT(SQL_VARIANT, PH.HistoricUser),

    HistoricDate = CONVERT(SQL_VARIANT, PH.HistoricDate)

    FROM dbo.ProductsHistory PH

    WHERE PH.Product = @product

    UNION ALL

    -- The current records

    -- NULLs used for columns that only exist in the History table

    SELECT 2147483647,

    P.ProductId,

    P.ProductDescription,

    P.Type,

    P.StandardPrice,

    P.EngineeringOnly,

    P.StructureVersion,

    NULL,

    NULL

    FROM dbo.Products P

    WHERE P.Product = @product

    )

    INSERT #KeyValuePairs

    (Sequence, ProductHistory, Name, Value)

    SELECT ROW_NUMBER() OVER (

    PARTITION BY Name

    ORDER BY ProductHistory),

    KeyValuePairs.ProductHistory,

    KeyValuePairs.Name,

    KeyValuePairs.Value

    FROM SourceData SD

    UNPIVOT (

    -- Break the audit columns into key/value pairs

    Value

    FOR Name IN

    (

    -- Just the columns to audit changes to

    SD.ProductId,

    SD.ProductDescription,

    SD.[Type],

    SD.StandardPrice,

    SD.EngineeringOnly

    )

    ) KeyValuePairs;

    -- RESULTS

    SELECT -- Audit details

    SubQuery.Name,

    SubQuery.changed_from,

    SubQuery.changed_to,

    SubQuery.ProductHistory,

    -- Extra stuff from the History record

    PH.HistoricUser,

    PH.HistoricDate

    FROM (

    SELECT This.Name,

    changed_from = Preceding.Value,

    changed_to = This.Value,

    Preceding.ProductHistory

    FROM #KeyValuePairs This

    JOIN #KeyValuePairs Preceding

    ON Preceding.Name = This.Name

    AND Preceding.Sequence = This.Sequence - 1

    AND Preceding.Value <> This.Value

    ) SubQuery

    JOIN dbo.ProductsHistory PH

    ON PH.ProductHistory = SubQuery.ProductHistory

    ORDER BY

    SubQuery.Name,

    SubQuery.ProductHistory;

    END;

    GO

    --

    -- === TESTS ===

    --

    -- Clear history

    TRUNCATE TABLE dbo.ProductsHistory;

    -- Show the product

    SELECT *

    FROM dbo.Products

    WHERE Product = 55085;

    -- Do some updates and show the audit after each

    UPDATE Products SET StandardPrice = $1;

    EXECUTE dbo.ShowProductAudit 55085;

    UPDATE Products SET ProductDescription = 'Assembly Desc 2'

    EXECUTE dbo.ShowProductAudit 55085;

    UPDATE Products SET StandardPrice = $2

    EXECUTE dbo.ShowProductAudit 55085;

    UPDATE Products SET StandardPrice = $1;

    EXECUTE dbo.ShowProductAudit 55085;

    UPDATE Products SET ProductDescription = 'Assembly Desc 3'

    EXECUTE dbo.ShowProductAudit 55085;

    GO

    -- Drop tempdb objects

    IF OBJECT_ID(N'tempdb.dbo.Products', N'U')

    IS NOT NULL

    DROP TABLE tempdb.dbo.Products;

    IF OBJECT_ID(N'tempdb.dbo.ProductsHistory', N'U')

    IS NOT NULL

    DROP TABLE tempdb.dbo.ProductsHistory;

    IF OBJECT_ID(N'tempdb.dbo.ShowProductAudit', N'P')

    IS NOT NULL

    DROP PROCEDURE dbo.ShowProductAudit;

    GO

    Paul

  • Stage-by-stage breakdown:

    --

    -- *** STEP 1 ***

    --

    -- Get all the data for a particular product from the

    -- history table and the current Products record

    --

    -- The columns are converted to SQL_VARIANT for the UNPIVOT

    -- that comes next. UNPIVOT requires that all columns have

    -- the same data type. SQL_VARIANT can hold most different

    -- types.

    --

    -- A fixed value of 9223372036854775807 is used as the ProductHistory

    -- reference for the current record. This just ensures that

    -- the current record always has the highest possible value.

    --

    -- 9,223,372,036,854,775,807 is the highest possible value of a BIGINT.

    --

    -- The product to audit

    DECLARE @product BIGINT;

    SET @product = 55085;

    -- Holds the results from step 1

    DECLARE @Step1

    TABLE (

    ProductHistory BIGINT NOT NULL PRIMARY KEY,

    ProductId SQL_VARIANT NULL,

    ProductDescription SQL_VARIANT NULL,

    Type SQL_VARIANT NULL,

    StandardPrice SQL_VARIANT NULL,

    EngineeringOnly SQL_VARIANT NULL

    );

    -- Find the current and history data

    INSERT @Step1

    (ProductHistory, ProductId, ProductDescription, Type, StandardPrice, EngineeringOnly)

    SELECT -- History

    PH.ProductHistory,

    ProductId = CONVERT(SQL_VARIANT, PH.ProductId),

    ProductDescription = CONVERT(SQL_VARIANT, PH.ProductDescription),

    Type = CONVERT(SQL_VARIANT, PH.Type),

    StandardPrice = CONVERT(SQL_VARIANT, PH.StandardPrice),

    EngineeringOnly = CONVERT(SQL_VARIANT, PH.EngineeringOnly)

    FROM dbo.ProductsHistory PH

    WHERE PH.Product = @product

    UNION ALL

    SELECT -- Current

    9223372036854775807,

    P.ProductId,

    P.ProductDescription,

    P.Type,

    P.StandardPrice,

    P.EngineeringOnly

    FROM dbo.Products P

    WHERE P.Product = @product;

    -- Show the results of step 1

    SELECT ProductHistory, ProductId, ProductDescription, Type, StandardPrice, EngineeringOnly

    FROM @Step1

    ORDER BY

    ProductHistory;

    --

    -- *** STEP 2 ***

    --

    -- Use UNPIVOT to break each value up into a column name & value pair

    -- We only UNPIVOT the columns we want to audit

    --

    -- Holds the results from step 2

    DECLARE @Step2

    TABLE (

    Name SYSNAME NOT NULL,

    Value SQL_VARIANT NULL,

    ProductHistory BIGINT NOT NULL,

    PRIMARY KEY (Name, ProductHistory)

    );

    -- The UNPIVOT

    INSERT @Step2

    (Name, Value, ProductHistory)

    SELECT KeyValuePairs.Name,

    KeyValuePairs.Value,

    KeyValuePairs.ProductHistory

    FROM @Step1 S1

    UNPIVOT (

    Value

    FOR Name IN

    (

    -- Just the audit columns. Each value will be split

    -- into Name and Value

    S1.ProductId,

    S1.ProductDescription,

    S1.[Type],

    S1.StandardPrice,

    S1.EngineeringOnly

    )

    ) KeyValuePairs;

    -- Show the results of step 2

    SELECT S2.Name,

    S2.Value,

    S2.ProductHistory

    FROM @Step2 S2

    ORDER BY

    S2.Name,

    S2.ProductHistory;

    --

    -- *** STEP 3 ***

    --

    -- Assign a sequence number to each row,

    -- in the order of ProductHistory

    -- restarting the numbering every time

    -- Name changes.

    --

    -- Holds the results from step 3

    DECLARE @Step3

    TABLE (

    Sequence BIGINT NOT NULL,

    Name SYSNAME NOT NULL,

    Value SQL_VARIANT NOT NULL,

    ProductHistory BIGINT NOT NULL,

    PRIMARY KEY (Name, Sequence)

    );

    -- Row numbering

    INSERT @Step3

    (Sequence, Name, Value, ProductHistory)

    SELECT ROW_NUMBER()

    OVER (

    PARTITION BY S2.Name

    ORDER BY S2.ProductHistory),

    S2.Name,

    S2.Value,

    S2.ProductHistory

    FROM @Step2 S2;

    -- Show the results of step 3

    SELECT S3.Sequence,

    S3.Name,

    S3.Value,

    S3.ProductHistory

    FROM @Step3 S3

    ORDER BY

    S3.Name,

    S3.Sequence;

    -- *** STEP 4 ***

    --

    -- Join adjacent rows to find only values

    -- that changed

    --

    -- Holds the results from step 4

    DECLARE @Step4

    TABLE (

    Name SYSNAME NOT NULL,

    changed_from SQL_VARIANT NULL,

    changed_to SQL_VARIANT NULL,

    ProductHistory BIGINT NOT NULL,

    PRIMARY KEY (Name, ProductHistory)

    );

    -- Join previous and current values

    INSERT @Step4

    (Name, changed_from, changed_to, ProductHistory)

    SELECT This.Name,

    changed_from = Preceding.Value,

    changed_to = This.Value,

    Preceding.ProductHistory

    FROM @Step3 This

    JOIN @Step3 Preceding

    ON Preceding.Name = This.Name -- Same column name

    AND Preceding.Sequence = This.Sequence - 1 -- Adjacent

    AND Preceding.Value <> This.Value; -- The value changed

    -- Show the results from step 4

    SELECT S4.Name,

    S4.changed_from,

    S4.changed_to,

    S4.ProductHistory

    FROM @Step4 S4

    ORDER BY

    S4.Name;

    -- *** STEP 5 **

    --

    -- Join back to ProductHistory to get extra column details

    --

    SELECT -- Audit details

    S4.Name,

    S4.changed_from,

    S4.changed_to,

    S4.ProductHistory,

    -- Extra stuff from the History record

    PH.HistoricUser,

    PH.HistoricDate

    FROM @Step4 S4

    JOIN dbo.ProductsHistory PH

    ON PH.ProductHistory = S4.ProductHistory

    ORDER BY

    S4.Name,

    S4.ProductHistory;

Viewing 4 posts - 16 through 18 (of 18 total)

You must be logged in to reply to this topic. Login to reply