I have a Customer table with the following structure.
CustomerId Name Address Phone
1 Joe 123 Main NULL
I also have an Audit table that tracks changes to the Customer table.
Id Entity EntityId Field OldValue NewValue Type AuditDate
1 Customer 1 Name NULL Joe Add 2016-01-01
2 Customer 1 Phone NULL 567-54-3332 Add 2016-01-01
3 Customer 1 Address NULL 456 Centre Add 2016-01-01
4 Customer 1 Address 456 Centre 123 Main Edit 2016-01-02
5 Customer 1 Phone 567-54-3332 843-43-1230 Edit 2016-01-03
6 Customer 1 Phone 843-43-1230 NULL Delete 2016-01-04
I have a CustomerHistory reporting table that will be populated with a daily ETL job. It has the same fields as Customer Table with additional field SnapShotDate.
I need to write a query that takes the records in Audit table, transforms and inserts into CustomerHistory as seen below.
CustomerId Name Address Phone SnapShotDate
1 Joe 456 Centre 567-54-3332 2016-01-01
1 Joe 123 Main 567-54-3332 2016-01-02
1 Joe 123 Main 843-43-1230 2016-01-03
1 Joe 123 Main NULL 2016-01-04
I guess the solution would involve a self-join on Audit table or a recursive CTE. I would appreciate any help with developing this solution.
Note: Unfortunately, I do not have the option to use triggers or change the Audit table schema. Query performance is not a concern since this will be a nightly ETL process.
You can use below script.
DROP TABLE #tmp
CREATE TABLE #tmp (
id INT Identity
, EntityId INT
, NAME VARCHAR(10)
, Address VARCHAR(100)
, Phone VARCHAR(20)
, Type VARCHAR(10)
, SnapShotDate DATETIME
)
;with cte1 as (
select AuditDate, EntityId, Type, [Name], [Address], [Phone]
from
(select AuditDate, EntityId, Type, Field, NewValue from #Audit) p
pivot
(
max(NewValue)
for Field in ([Name], [Address], [Phone])
) as xx
)
insert into #tmp (EntityId, Name, Address, Phone, Type, SnapShotDate)
select EntityId, Name, Address, Phone, Type, AuditDate
from cte1
-- update NULLs columns with the most recent value
update #tmp
set Name = (select top 1 Name from #tmp tp2
where EntityId = tp2.EntityId and Name is not null
order by id desc)
where Name is null
update #tmp
set Address = (select top 1 Address from #tmp tp2
where EntityId = tp2.EntityId and Address is not null
order by id desc)
where Address is null
update #tmp
set Phone = (select top 1 Phone from #tmp tp2
where EntityId = tp2.EntityId and Phone is not null
order by id desc)
where Phone is null
To Create Test Data, use below script
CREATE TABLE #Customer (
CustomerId INT
, NAME VARCHAR(10)
, Address VARCHAR(100)
, Phone VARCHAR(20)
)
INSERT INTO #Customer
VALUES (1, 'Joe', '123 Main', NULL)
CREATE TABLE #Audit (
Id INT
, Entity VARCHAR(50)
, EntityId INT
, Field VARCHAR(20)
, OldValue VARCHAR(100)
, NewValue VARCHAR(100)
, Type VARCHAR(10)
, AuditDate DATETIME
)
insert into #Audit values
(1, 'Customer', 1, 'Name' ,NULL ,'Joe' ,'Add' ,'2016-01-01'),
(2, 'Customer', 1, 'Phone' ,NULL ,'567-54-3332' ,'Add' ,'2016-01-01'),
(3, 'Customer', 1, 'Address' ,NULL ,'456 Centre' ,'Add' ,'2016-01-01'),
(4, 'Customer', 1, 'Address' ,'456 Centre' ,'123 Main' ,'Edit' ,'2016-01-02'),
(5, 'Customer', 1, 'Phone' ,'567-54-3332' ,'843-43-1230' ,'Edit' ,'2016-01-03'),
(6, 'Customer', 1, 'Phone' ,'843-43-1230' ,NULL ,'Delete' ,'2016-01-04'),
(7, 'Customer', 2, 'Name' ,NULL ,'Peter' ,'Add' ,'2016-01-01'),
(8, 'Customer', 2, 'Phone' ,NULL ,'111-222-3333' ,'Add' ,'2016-01-01'),
(8, 'Customer', 2, 'Address' ,NULL ,'Parthenia' ,'Add' ,'2016-01-01')
Result
EntityId Name Address Phone Type SnapShotDate
1 Joe 456 Centre 567-54-3332 Add 2016-01-01
1 Joe 123 Main 843-43-1230 Edit 2016-01-02
1 Joe 123 Main 843-43-1230 Edit 2016-01-03
1 Joe 123 Main 843-43-1230 Delete 2016-01-04
Related
We receive data on a weekly and monthly basis with information regarding customers. We also sometimes have the same information stored from another source. The two sources sometimes provide contradictory information regarding customers.
How would I write a query which tells me the mismatched CustomerId and corresponding Vehicle? For example, CustomerId 947623 is associated with Kia in the vendor extract [Table 1] whereas we have the same customer stored as related to Hyundai [Table 2].
Table 1: Data received from the vendor.
CustomerId
FirstName
LastName
Vehicle
MiscColumns
027548
Jane
Doe
Honda
MiscData
947623
John
Smith
Kia
MiscData
549816
Erin
Woods
Chevy
MiscData
739232
Henry
Jackson
Ford
MiscData
Table 2: Internal data records
CustomerId
FirstName
LastName
Vehicle
MiscColumns
027548
Jane
Doe
Honda
MiscData
947623
John
Smith
Hyundai
MiscData
549816
Erin
Woods
Chevy
MiscData
739232
Henry
Jackson
Ford
MiscData
Please try the following solution.
It will work starting from SQL Server 2016 onwards.
SQL
-- DDL and sample data population, start
DECLARE #TableA TABLE (CustomerId CHAR(6) PRIMARY KEY, FirstName VARCHAR(100), LastName VARCHAR(100), Vehicle VARCHAR(100));
DECLARE #TableB table (CustomerId CHAR(6) PRIMARY KEY, FirstName VARCHAR(100), LastName VARCHAR(100), Vehicle VARCHAR(100));
INSERT INTO #TableA (CustomerId, FirstName, LastName, Vehicle) VALUES
('027548', 'Jane', 'Doe', 'Honda'),
('947623', 'John', 'Smith', 'Kia'),
('549816', 'Erin', 'Woods', 'Chevy'),
('739232', 'Henry', 'Jackson', 'Ford');
INSERT INTO #TableB (CustomerId, FirstName, LastName, Vehicle) VALUES
('027548', 'Jane', 'Doe', 'Honda'),
('947623', 'John', 'Smith', 'Hyundai'),
('549816', 'Erin', 'Woods', 'Chevy'),
('739232', 'Henry', 'Jackson', 'Ford');
-- DDL and sample data population, end
SELECT CustomerId
,[key] AS [column]
,Org_Value = MAX( CASE WHEN Src=1 THEN Value END)
,New_Value = MAX( CASE WHEN Src=2 THEN Value END)
FROM (
SELECT Src=1
,CustomerId
,B.*
FROM #TableA A
CROSS APPLY ( SELECT [Key]
,COALESCE(Value, '') AS Value
FROM OpenJson( (SELECT A.* For JSON Path,Without_Array_Wrapper,INCLUDE_NULL_VALUES))
) AS B
UNION ALL
SELECT Src=2
,CustomerId
,B.*
FROM #TableB A
CROSS APPLY ( SELECT [Key]
,COALESCE(Value, '') AS Value
FROM OpenJson( (SELECT A.* For JSON Path,Without_Array_Wrapper,INCLUDE_NULL_VALUES))
) AS B
) AS A
GROUP BY CustomerId,[key]
HAVING MAX(CASE WHEN Src=1 THEN Value END)
<> MAX(CASE WHEN Src=2 THEN Value END)
ORDER BY CustomerId,[key];
Output
CustomerId
column
Org_Value
New_Value
947623
Vehicle
Kia
Hyundai
I have found lots of examples to create a employee reporting structure hierarchy. However, I want to take this a little further and for each employee find the executive they report up through whom reports directly to the CEO. Given the following data:
ID FullName HRLevel SupervisorID
----------- -------------------------------------------------- ---------- ------------
1 Pam Beesly CEO NULL
2 Angela Martin SVP 1
3 Kelly Kapoor SVP 1
4 Meredith Palmer SVP 1
5 Phyllis Vance AVP 3
6 Jan Levinson AVP 4
7 Erin Hannon Associate 5
8 Karen Filippelli Intern 5
I would like a list of all employees with an HRLevel of SVP and below to show the SVP level employee they report up to. So it could be their immediate supervisor or it could be 4 levels above them. Also, if an employee reports directly to the CEO I want that employee to be listed as the DepartmentHead.
Here is what I have so far:
;WITH cte_employees (ID, FullName, HRLevel, SupervisorID, SupervisorName) AS
(
SELECT ID
, FullName
, HRLevel
, SupervisorID
, CONVERT(VARCHAR(50), NULL) AS [SupervisorName]
FROM #Employees
WHERE HRLevel = 'SVP'
UNION ALL
SELECT e.ID
, e.FullName
, e.HRLevel
, e.SupervisorID
, c.FullName AS [SupervisorName]
FROM #Employees e
INNER JOIN cte_employees c
ON e.SupervisorID = c.ID
)
SELECT c2.ID
, c2.FullName
, c2.HRLevel
, c2.SupervisorID
, COALESCE(c2.SupervisorName, c2.FullName) AS [DepartmentHead]
FROM cte_employees c2;
Which yields the immediate supervisor, not the SVP DepartmentHead:
ID FullName HRLevel SupervisorID DepartmentHead
----------- -------------------------------------------------- ---------- ------------ --------------------------------------------------
2 Angela Martin SVP 1 Angela Martin
3 Kelly Kapoor SVP 1 Kelly Kapoor
4 Meredith Palmer SVP 1 Meredith Palmer
5 Phyllis Vance AVP 3 Kelly Kapoor
6 Jan Levinson AVP 4 Meredith Palmer
7 Erin Hannon Associate 5 Phyllis Vance
8 Karen Filippelli Intern 5 Phyllis Vance
The only difference in what I am getting and what I want is that Employee IDs 7 and 8 should have a SVP DepartmentHead of Kelly Kapoor.
Here is my dbfiddle showing this in action.
You are vert close:
IF OBJECT_ID('tempdb.dbo.#Employees ', 'U') IS NOT NULL DROP TABLE #Employees;
CREATE TABLE #Employees(ID INT,
FullName VARCHAR(50),
HRLevel VARCHAR(10),
SupervisorID INT)
INSERT INTO #Employees VALUES (1, 'Pam Beesly', 'CEO', NULL)
INSERT INTO #Employees VALUES (2, 'Angela Martin', 'SVP', 1)
INSERT INTO #Employees VALUES (3, 'Kelly Kapoor', 'SVP', 1)
INSERT INTO #Employees VALUES (4, 'Meredith Palmer', 'SVP', 1)
INSERT INTO #Employees VALUES (5, 'Phyllis Vance', 'AVP', 3)
INSERT INTO #Employees VALUES (6, 'Jan Levinson', 'AVP', 4)
INSERT INTO #Employees VALUES (7, 'Erin Hannon', 'Associate', 5)
INSERT INTO #Employees VALUES (8, 'Karen Filippelli', 'Intern', 5)
;WITH cte_employees (ID, FullName, HRLevel, SupervisorID, SupervisorName) AS
(
SELECT ID
, FullName
, HRLevel
, SupervisorID
, FullName AS [SupervisorName]
FROM #Employees
WHERE HRLevel = 'SVP'
UNION ALL
SELECT e.ID
, e.FullName
, e.HRLevel
, e.SupervisorID
, c.SupervisorName AS [SupervisorName]
FROM #Employees e
INNER JOIN cte_employees c
ON e.SupervisorID = c.ID
)
SELECT c2.ID
, c2.FullName
, c2.HRLevel
, c2.SupervisorID
, c2.SupervisorName AS [DepartmentHead]
FROM cte_employees c2;
I'm trying to implement similar functionality to temporal tables
https://msdn.microsoft.com/en-us/library/dn935015.aspx
Due to some complex business requirements, I cannot use SQL Server's temporal tables, so I'm trying to implement my own version.
Basically, I need a way to be able to determine what is the data in a specific point in time.
The table will store deltas, and a bitmask to determine what columns actually changed (changing the data to NULL is a valid scenario, that's why we need this update mask)
I do have a prototype working, but my query seems very expensive. Instead of one subquery per column, I've been trying to combine them into just 1 query (with a cursor-like behavior).
Tables:
CREATE TABLE user_efdt
(
user_id INT not null,
date_effective DATE not null,
first_name NVARCHAR(100),
last_name NVARCHAR(100),
address1 NVARCHAR(100),
position NVARCHAR(100),
modified_fields VARBINARY(128) NOT NULL
)
ALTER TABLE user_efdt
ADD CONSTRAINT PK_user_efdt
PRIMARY KEY CLUSTERED (user_id, date_effective);
GO
Data setup:
DECLARE #first_name_bit INT
DECLARE #last_name_bit INT
DECLARE #address1_bit INT
DECLARE #position_bit INT
SET #first_name_bit = 3
SET #last_name_bit = 4
SET #address1_bit = 5
SET #position_bit = 6
-- user john does gets added to system on 1-may-16 with first name and last name
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (1000, '1-may-16', 'john', 'doe', null, null, CAST(POWER(2, #first_name_bit) + POWER(2, #last_name_bit) AS VARBINARY))
-- user mary ann gets added to system on 1-may-16 with first name, last name, address and position
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (2000, '1-may-16', 'mary', 'ann', '123 main st', 'manager', CAST(POWER(2, #first_name_bit) + POWER(2, #last_name_bit) + POWER(2, #address1_bit) + POWER(2, #position_bit) AS VARBINARY))
-- john doe gets address updated on 1-apr-16
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (1000, '1-apr-16', null, null, '456 el dorado st', null, CAST(POWER(2, #address1_bit) AS VARBINARY))
-- john doe gets position updated
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (1000, '1-mar-16', null, null, null, 'engineer', CAST(POWER(2, #position_bit) AS VARBINARY))
-- john doe gets position updated again
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (1000, '1-jun-16', null, null, null, 'engineer 2', CAST(POWER(2, #position_bit) AS VARBINARY))
-- john doe gets position updated to NULL (erased)
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (1000, '1-jul-16', null, null, null, null, CAST(POWER(2, #position_bit) AS VARBINARY))
-- mary ann gets address updated
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (2000, '1-jun-16', null, null, '1443 hoover st', null, CAST(POWER(2, #address1_bit) AS VARBINARY))
-- mary ann gets position updated
INSERT INTO user_efdt (user_id, date_effective, first_name, last_name, address1, position, modified_fields)
VALUES (2000, '1-jul-16', null, null, null, 'manager 2', CAST(POWER(2, #position_bit) AS VARBINARY))
Here's my version of the query:
CREATE PROCEDURE as_of_date_query (#date DATE)
AS
BEGIN
DECLARE #first_name_bit INT
DECLARE #last_name_bit INT
DECLARE #address1_bit INT
DECLARE #position_bit INT
SET #first_name_bit = 3
SET #last_name_bit = 4
SET #address1_bit = 5
SET #position_bit = 6
SELECT
q1.date_effective, fn.val first_name, ln.val last_name,
addr.val address1, pos.val position
FROM
(SELECT
user_id, MAX(date_effective) date_effective
FROM
user_efdt
WHERE
date_effective <= #date
GROUP BY
user_id) q1
LEFT JOIN
(SELECT DISTINCT
user_id,
FIRST_VALUE(first_name) OVER (PARTITION BY user_id ORDER BY date_effective DESC) val
FROM
user_efdt
WHERE
(modified_fields & POWER(2, #first_name_bit)) > 0
AND date_effective <= #date) fn ON q1.user_id = fn.user_id
LEFT JOIN
(SELECT DISTINCT
user_id,
FIRST_VALUE(last_name) OVER (PARTITION BY user_id ORDER BY date_effective DESC) val
FROM
user_efdt
WHERE
(modified_fields & POWER(2, #last_name_bit)) > 0
AND date_effective <= #date) ln ON q1.user_id = ln.user_id
LEFT JOIN (
SELECT DISTINCT user_id, FIRST_VALUE(address1) OVER (PARTITION BY user_id ORDER BY date_effective DESC) val
FROM user_efdt
WHERE (modified_fields & POWER(2, #address1_bit)) > 0
AND date_effective <= #date
) addr ON q1.user_id = addr.user_id
LEFT JOIN (
SELECT DISTINCT user_id, FIRST_VALUE(position) OVER (PARTITION BY user_id ORDER BY date_effective DESC) val
FROM user_efdt
WHERE (modified_fields & POWER(2, #position_bit)) > 0
AND date_effective <= #date
) pos ON q1.user_id = pos.user_id
END
expected results:
EXEC as_of_date_query '1-mar-16'
-- 2016-03-01 NULL NULL NULL engineer
EXEC as_of_date_query '5-apr-16'
-- 2016-04-01 NULL NULL 456 el dorado st engineer
EXEC as_of_date_query '15-may-16'
-- 2016-05-01 john doe 456 el dorado st engineer
-- 2016-05-01 mary ann 123 main st manager
EXEC as_of_date_query '5-jun-16'
-- 2016-06-01 john doe 456 el dorado st engineer 2
-- 2016-06-01 mary ann 1443 hoover st manager
EXEC as_of_date_query '1-jul-16'
-- 2016-07-01 john doe 456 el dorado st NULL
-- 2016-07-01 mary ann 1443 hoover st manager 2
I have two tables :
Table 1:
Id | PersonId |Variable | Value|
1 12 FirstName NULL
2 12 Address NULL
------------------------
Table2:
Id | PersonId | FirstName| LastName| Address | Phone
1 12 Tommy Stark NY 12365
I need to copy data from table 2 into table 1 and
I need output like:
Table 1:
Id | PersonId |Variable | Value|
1 12 FirstName Tommy
2 12 Address NY
You could use a series of case expressions to match table 1 values to table 2 column names. It's clunky as heck, but it should work:
UPDATE t1
SET t1.value = CASE t1.variable
WHEN 'FirstName' THEN t2.firstname
ELSE t1.value
END,
t1.value = CASE t1.variable
WHEN 'LastName' THEN t2.lastname
ELSE t1.value
END,
t1.value = CASE t1.variable
WHEN 'Address' THEN t2.address
ELSE t1.value
END,
t1.value = CASE t1.phone
WHEN 'Phone' THEN t2.phone
ELSE t1.value
END
FROM t1
JOIN t2 ON t1.personid = t2.personid
Declare #Table1 TABLE
(Id int, PersonId int, Variable varchar(9), Value varchar(4))
;
INSERT INTO #Table1
(Id, PersonId, Variable, Value)
VALUES
(1, 12, 'FirstName', NULL),
(2, 12, 'Address', NULL)
;
DECLARE #Table2 TABLE
(Id int, PersonId int, FirstName varchar(5), LastName varchar(5), Address varchar(2), Phone int)
;
INSERT INTO #Table2
(Id, PersonId, FirstName, LastName, Address, Phone)
VALUES
(1, 12, 'Tommy', 'Stark', 'NY', 12365)
select TT.Id,
TT.PersonId,
TT.Variable,
CASE
WHEN T.col = TT.Variable
THEN T.val
END value
from #Table1 TT
INNER JOIN (
select col,val from #Table2 t CROSS APPLY (values ('Id',CAST(Id AS VARCHAR)), ('PersonId',CAST(PersonId AS VARCHAR)),
('FirstName',CAST(FirstName AS VARCHAR)),
('LastName',CAST(LastName AS VARCHAR)),
('Address',CAST(Address AS VARCHAR)),
('Phone',CAST(Phone AS VARCHAR)))cs(col,val))T
ON T.col = TT.Variable
Need to write to a table all the rows where values have changed between 2 datacuts.
This must be done in sql and not using any third party tools.
I can find the difference between 2 datacuts easily by using "Except".
I have not tried chksum but added a column just in case.
What I am struggling with and need your help is
How do pull all the data out from my findings into my #Changes table?
WANTED RESULT
EmployeeId ColumnName OldValue NewValue
3 MaritalStatus Single Married
3 Surname Malone Evans
10 MaritalStatus Single Married
SETUP TEST DATA
Dummy data set up (2 Employees with Id(3,10) have changes) if you notice
employee id(3) has 2 columns changes.
IF OBJECT_ID('tempdb..#Employee') IS NOT NULL DROP TABLE #Employee
GO
IF OBJECT_ID('tempdb..#Changes') IS NOT NULL DROP TABLE #Changes
GO
CREATE TABLE #Employee
(
[Id] [int] NOT NULL,
EmployeeNo INT NOT NULL,
[DataCut] [int] NULL,
[Name] [varchar](50) NULL,
[Surname] [varchar](50) NULL,
[Gender] [varchar](10) NULL,
[MaritalStatus] [varchar](10) NULL,
[Chksum] [int] NULL,
CONSTRAINT [PK_#Employee]
PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY]
CREATE TABLE #Changes
(
[EmployeeNo] [int] ,
[ColumnName] [varchar](50) NULL,
[OldValue] [varchar](50) NULL,
[NewValue] [varchar](50) NULL
)
INSERT INTO #Employee([Id], EmployeeNo,[DataCut], [Name], [Surname], [Gender], [MaritalStatus],[Chksum])
SELECT 1, 1,1, N'Jo', N'Bloggs', N'Male', N'Single', NULL UNION ALL
SELECT 2, 2,1, N'Mark', N'Smith', N'Male', N'Single', NULL UNION ALL
SELECT 3, 3,1, N'Jenny', N'Malone', N'Female', N'Single', NULL UNION ALL
SELECT 4, 4,1, N'Mario', N'Rossi', N'Male', N'Single', NULL UNION ALL
SELECT 5, 5,1, N'Richard', N'Jones', N'Male', N'Single', NULL UNION ALL
SELECT 6, 1,2, N'Jo', N'Bloggs', N'Male', N'Single', NULL UNION ALL
SELECT 7, 2,2, N'Mark', N'Smith', N'Male', N'Single', NULL UNION ALL
SELECT 8, 3,2, N'Jenny', N'Evans', N'Female', N'Married', NULL UNION ALL
SELECT 9, 4,2, N'Mario', N'Rossi', N'Male', N'Single', NULL UNION ALL
SELECT 10,5,2, N'Richard', N'Jones', N'Male', N'Married', NULL
--Find all the Rows that have changed between 2 datacuts using EXCEPT
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=1
EXCEPT
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=2
UNION
--do the opposite so that we get all the rows.
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=2
EXCEPT
SELECT EmployeeNo,Name, Surname, Gender, MaritalStatus
FROM #Employee
WHERE DataCut=1
--HOW DO I FILL MY #CHANGES TABLES TO MATCH MY WANTED RESULT?
DROP TABLE #Changes
DROP TABLE #Employee
You can use UNPIVOT:
;WITH UnpivotedTable AS (
SELECT Id, EmployeeNo, DataCut, Val, Col
FROM
(SELECT Id, EmployeeNo, DataCut, CAST(Name AS VARCHAR(50)) AS Name,
CAST(Surname AS VARCHAR(50)) AS Surname,
CAST(Gender AS VARCHAR(50)) AS Gender,
CAST(MaritalStatus AS VARCHAR(50)) AS MaritalStatus
FROM #Employee) AS src
UNPIVOT
(Val FOR Col IN
(Name, Surname, Gender, MaritalStatus)) AS unpvt
)
SELECT t1.Id As EmployeeId,
t1.Col AS ColumnName,
t1.Val AS OldValue,
t2.Val AS NewValue
FROM UnpivotedTable AS t1
INNER JOIN UnpivotedTable AS t2
ON t1.EmployeeNo = t2.EmployeeNo AND t1.Col = t2.Col AND
t1.DataCut = 1 AND t2.DataCut = 2
WHERE t1.Val <> t2.Val
Demo here
Explanation:
Here's an excerpt of the data returned by the CTE (for EmployeeNo = 1):
Id EmployeeNo DataCut Val Col
---------------------------------------------
1 1 1 Jo Name
1 1 1 Bloggs Surname
1 1 1 Male Gender
1 1 1 Single MaritalStatus
6 1 2 Jo Name
6 1 2 Bloggs Surname
6 1 2 Male Gender
6 1 2 Single MaritalStatus
Using the above table expression we can easily get the expected result performing an INNER JOIN operation: we just have to compared 'old' (DataCut = 1) vs new (DataCut = 2) values for the same EmployeeNo and Col.