Showing History of changes from a History table - sql

I have a History table, which is created by Insert and Update triggers. The History row contains the row as it was on the Insert/Update.
What I am being asked to do is show the changes for each user through time. So, below is what I have in the form of my History table, and then, I created a dummy expected results.
DECLARE #MyTable TABLE
(
id INT NOT NULL IDENTITY(1,1),
userId INT NOT NULL,
locationId INT NOT NULL,
roleId INT NOT NULL,
lastUpdateUserId INT NOT NULL,
lastUpdateDate DATETIME NOT NULL
)
INSERT INTO #MyTable
(userId, locationId, roleId, lastUpdateUserId, lastUpdateDate)
SELECT 1, 1000, 1, 7, GETDATE()+1 UNION
SELECT 2, 1100, 5, 9, GETDATE()+2 UNION
SELECT 2, 1110, 5, 6, GETDATE()+3 UNION
SELECT 1, 1100, 3, 6, GETDATE()+4 UNION
SELECT 4, 1500, 5, 8, GETDATE()+5 UNION
SELECT 7, 1000, 8, 9, GETDATE()+6 UNION
SELECT 7, 1100, 9, 9, GETDATE()+7 UNION
SELECT 1, 1000, 3, 7, GETDATE()+8 UNION
SELECT 9, 1100, 5, 2, GETDATE()+9 UNION
SELECT 9, 1100, 6, 5, GETDATE()+10
SELECT * FROM #MyTable ORDER BY Id
DECLARE #ExpectedResult TABLE
(
ChangeType CHAR(1), -- I=Insert, U=Update
UserId INT,
ChangeDate DATETIME,
ChangedByUser INT,
FieldName VARCHAR(20),
OldValue INT,
NewValue INT
)
INSERT INTO #ExpectedResult
(ChangeType, UserId, ChangeDate, ChangedByUser, FieldName, OldValue, NewValue)
SELECT 'I', 1, '2015-APR-30 09:56:28', 7, 'locationId', NULL, 1000 UNION -- Row1
SELECT 'I', 1, '2015-APR-30 09:56:28', 7, 'roleId', NULL, 1 UNION -- Row1
SELECT 'U', 1, '2015-APR-07 10:27:42', 7, 'roleId', 1, 3 UNION -- Row 2
SELECT 'U', 1, '2015-MAY-03 10:27:42', 6, 'locationId', 1000, 1100 UNION -- Row 3
SELECT 'I', 2, '2015-MAY-01 10:27:42', 9, 'roleId', NULL, 5 UNION -- Row5
SELECT 'I', 2, '2015-MAY-01 10:27:42', 9, 'locationId', NULL, 1100 -- Row5
SELECT * FROM #ExpectedResult
#MyTable has the data as it is at the moment. I am trying to transform that into #ExpectedResults. We're reporting on changes to roleId and locationId. On each change, it needs to have a separate line for each column. So, on insert, we have two lines (As we monitor changes to both fields). When one column is updated, it needs to be represented as one 'U' line. If both fields are updated in the same UPDATE statement, then that would result in two update rows in #Expected.
I started with a Cursor, but was hoping there would be a more efficient way to achieve this.

To get the roleID and locationID on separate rows you can use a simple UNION ALL.
And to combine the old and new values use the ROW_NUMBER() window function, like this:
;with t as(
select *,
ROW_NUMBER() OVER(partition by userid Order BY lastUpdateDate) rn
from #MyTable
),
a as (
select userId, 'locationId' as fieldname,
locationId as value, lastUpdateUserId, lastUpdateDate, rn
from t
UNION ALL
select userId, 'roleId' as fieldname,
roleId as value, lastUpdateUserId, lastUpdateDate, rn
from t
)
select CASE WHEN a2.userId IS NULL THEN 'I' ELSE 'U' END as ChangeType,
a1.userId, a1.lastUpdateDate, a1.lastUpdateUserId, a1.fieldname, a1.value as newValue, a2.value as oldvalue
FROM a a1 LEFT JOIN a a2
ON a1.userId = a2.userId and a1.fieldname = a2.fieldname
AND a1.rn = a2.rn+1
order by 2,3,5
The a1 alias in the query above contains the "new values", the a2 contains the "old values". When you use the real data you will also need to partition by the fieldname (and perhaps table name) and also to join by them
The result:
ChangeType userId lastUpdateDate lastUpdateUserId fieldname newValue oldvalue
---------- ----------- ----------------------- ---------------- ---------- ----------- -----------
I 1 2015-04-30 12:20:59.183 7 locationId 1000 NULL
I 1 2015-04-30 12:20:59.183 7 roleId 1 NULL
U 1 2015-05-03 12:20:59.183 6 locationId 1100 1000
U 1 2015-05-03 12:20:59.183 6 roleId 3 1
U 1 2015-05-07 12:20:59.183 7 locationId 1000 1100
U 1 2015-05-07 12:20:59.183 7 roleId 3 3
I 2 2015-05-01 12:20:59.183 9 locationId 1100 NULL
I 2 2015-05-01 12:20:59.183 9 roleId 5 NULL
U 2 2015-05-02 12:20:59.183 6 locationId 1110 1100
U 2 2015-05-02 12:20:59.183 6 roleId 5 5
I 4 2015-05-04 12:20:59.183 8 locationId 1500 NULL
I 4 2015-05-04 12:20:59.183 8 roleId 5 NULL
I 7 2015-05-05 12:20:59.183 9 locationId 1000 NULL
I 7 2015-05-05 12:20:59.183 9 roleId 8 NULL
U 7 2015-05-06 12:20:59.183 9 locationId 1100 1000
U 7 2015-05-06 12:20:59.183 9 roleId 9 8
I 9 2015-05-08 12:20:59.183 2 locationId 1100 NULL
I 9 2015-05-08 12:20:59.183 2 roleId 5 NULL
U 9 2015-05-09 12:20:59.183 5 locationId 1100 1100
U 9 2015-05-09 12:20:59.183 5 roleId 6 5
(20 row(s) affected)

Related

Count over Partition by with one condition (/don't count the NULL values)

I want to count how many houses are within a building. Dataset like the following:
BuildingID, HouseID
1, 1
1, 2
1, 3
2, 4
2, 5
2, 6
NULL, 7
NULL, 8
With the following code it shows the total count of the houses, however, houses 7 and 8 don't have a building, so it shouldn't count anything.
SELECT BuildingID
, HouseID
, COUNT(HouseID) OVER (PARTITION BY BuildingID) AS 'Houses in Building'
FROM BUILDING
The result I get:
BuildingID, HouseID, Houses in Building
1, 1, 3
1, 2, 3
1, 3, 3
2, 4, 3
2, 5, 3
2, 6, 3
NULL, 7, 2
NULL, 8, 2
The result I want:
BuildingID, HouseID, Houses in Building
1, 1, 3
1, 2, 3
1, 3, 3
2, 4, 3
2, 5, 3
2, 6, 3
NULL, 7, NULL --or 0
NULL, 8, NULL --or 0
Any suggestions?
Just count the BuildingID. The COUNT function does not count nulls so it'll work:
COUNT(BuildingID) OVER (PARTITION BY BuildingID) AS 'Houses in Building'
Note that it assumes that HouseID is not null.
You could simply use a case expression to only show a count where the BuildingID is not null, or you could change your count to be COUNT(BuildingID) rather than COUNT(HouseID) (Since COUNT(NULL) gives 0). Both yield your required results:
DECLARE #Building TABLE (BuildingID INT, HouseID INT);
INSERT #Building (BuildingID, HouseID)
VALUES
(1, 1), (1, 2), (1, 3), (2, 4), (2, 5),
(2, 6), (NULL, 7), (NULL, 8);
SELECT BuildingID,
HouseID,
CountBuildingID = COUNT(BuildingID) OVER (PARTITION BY BuildingID),
CaseExpression = CASE WHEN BuildingID IS NOT NULL THEN COUNT(HouseID) OVER (PARTITION BY BuildingID) END
FROM #Building
ORDER BY HouseID;
OUTPUT
BuildingID HouseID CountBuildingID CaseExpression
-------------------------------------------------------
1 1 3 3
1 2 3 3
1 3 3 3
2 4 3 3
2 5 3 3
2 6 3 3
NULL 7 0 NULL
NULL 8 0 NULL
You can check this following self join option-
WITH your_table (BuildingID, HouseID)
AS
(
SELECT 1, 1 UNION ALL
SELECT 1, 2 UNION ALL
SELECT 1, 3 UNION ALL
SELECT 2, 4 UNION ALL
SELECT 2, 5 UNION ALL
SELECT 2, 6 UNION ALL
SELECT NULL, 7 UNION ALL
SELECT NULL, 8
)
SELECT A.BuildingID,A.HouseID,COUNT(A.BuildingID)Count
FROM your_table A
LEFT JOIN your_table B ON A.BuildingID = B.BuildingID
GROUP BY A.BuildingID,A.HouseID
Output is-
BuildingID HouseID Count
1 1 3
1 2 3
1 3 3
2 4 3
2 5 3
2 6 3
NULL 7 0
NULL 8 0
You can use case when condition in Count function like below,
COUNT(CASE WHEN BuildingID IS NOT NULL THEN HouseID END) OVER (PARTITION BY BuildingID) AS 'Houses in Building'

Find out if column contains two distinct values for each group

I want to select the rows which satisfy both conditions of the same column. Below is the table schema.
Security Table
Id RoleId CompId SecurityToken Accesstype
1 1 10 abc 2
2 1 10 xyz 2
3 12 10 abc 2
4 16 12 abc 2
5 16 12 xyz 2
6 30 13 abc 2
7 1 10 efg 2
8 1 10 lmn 0
I want "All rows for each RoleID/CompID combination where accesstype = 2 and there is both a row containing securitytoken "abc" and a row containing SecurityToken "xyz" for that role/compID combination"
Output should be
Id RoleId CompId SecurityToken Accesstype
1 1 10 abc 2
2 1 10 xyz 2
4 16 12 abc 2
5 16 12 xyz 2
I believe the following query will produce the desired output:
SELECT *
FROM testdata
WHERE Accesstype = 2
AND SecurityToken IN ('abc', 'xyz')
AND EXISTS (
SELECT 1
FROM testdata AS tmp
WHERE RoleId = testdata.RoleId
AND CompId = testdata.CompId
AND Accesstype = testdata.AccessType
AND SecurityToken IN ('abc', 'xyz')
HAVING COUNT(DISTINCT SecurityToken) = 2
)
SQL Fiddle
In order to eliminate sets that contain extra security tokens (such as efg and lmn) change the WHERE and HAVING clause to:
WHERE RoleId = testdata.RoleId
AND CompId = testdata.CompId
AND Accesstype = testdata.AccessType
HAVING COUNT(DISTINCT SecurityToken) = 2
AND COUNT(DISTINCT SecurityToken) = COUNT(CASE WHEN SecurityToken IN ('abc', 'xyz') THEN 1 END)
CREATE TABLE #Table1
([Id] int, [RoleId] int, [CompId] int, [SecurityToken] varchar(3), [Accesstype] int)
;
INSERT INTO #Table1
([Id], [RoleId], [CompId], [SecurityToken], [Accesstype])
VALUES
(1, 1, 10, 'abc', 2),
(2, 1, 10, 'xyz', 2),
(3, 12, 10, 'abc', 2),
(4, 16, 12, 'abc', 2),
(5, 16, 12, 'xyz', 2),
(6, 30, 13, 'abc', 2)
;
WITH cte AS (
SELECT *,ROW_NUMBER() OVER (PARTITION BY [ROLEID],[COMPID] ORDER BY ID) AS RN FROM #TABLE1
),
COUNTED AS (
SELECT
*,
COUNT(*) OVER (PARTITION BY [ROLEID],[COMPID]) AS CNT
FROM cte
)
SELECT
[ID], [ROLEID], [COMPID], [SECURITYTOKEN], [ACCESSTYPE]
FROM COUNTED
WHERE CNT >= 2
output
ID ROLEID COMPID SECURITYTOKEN ACCESSTYPE
1 1 10 abc 2
2 1 10 xyz 2
4 16 12 abc 2
5 16 12 xyz 2
or
WITH CTE AS
(
SELECT
*,
COUNT(*) OVER (PARTITION BY [ROLEID],[COMPID]) AS CNT
FROM #TABLE1)
SELECT [ID], [ROLEID], [COMPID], [SECURITYTOKEN], [ACCESSTYPE] FROM CTE WHERE CNT>=2
One method uses exists:
select t.*
from t
where t.Accesstype = 2 and
t.securityToken in ('abc', 'xyz') and
exists (select 1
from t t2
where t2.RoleId = t.RoleId and
t2.CompId = t.CompId and
t2.Accesstype = t.AccessType and
t2.SecurityToken in ('abc', 'xyz') and
t2.SecrityToken <> t.SecurityToken
);
Perhaps a simpler method uses window functions:
select t.*
from (select t.*,
min(securitytoken) over (partition by roleid, compid) as min_st,
min(securitytoken) over (partition by roleid, compid) as max_st
from t
where t.Accesstype = 2 and
t.SecurityToken in ('abc', 'xyz')
) t
where minsecuritytoken = 'abc' and
maxsecuritytoken = 'xyz;

Group the column value based on selective rows for an id

I have a table which have 4 dimensions for a foreignid.
I want to find unique combination based on 2 dimensions.
TABLE1
-----------------------------
ID NAME VALUE TABLE2ID
-----------------------------
1 TYPE 10 1
2 DIR IN 1
3 STATE MA 1
4 COUNT 100 1
5 TYPE 10 2
6 DIR IN 2
7 STATE SA 2
8 COUNT 200 2
9 TYPE 20 3
10 DIR OUT 3
11 STATE MA 3
12 COUNT 300 3
-----------------------------
Here, I want the TABLE2IDs based on the combination of TYPE and DIR rows which is unique.
So, here if you aggregate the row values based on TYPE and DIR you will get
-----------------------------
TYPE DIR TABLE2ID
-----------------------------
10 IN 1
10 IN 2
20 OUT 3
-----------------------------
Note:
The above question is answered
Additional Question related to this.
I have another table which have the count for table2 id based on hour.
I want to group all the count for all hours in a day for unique combination in table1(Don't worry about table 2 structure).
TABLE3
-----------------------------
ID TIME COUNT TABLE2ID
-----------------------------
1 2016101601 10 1
2 2016101602 20 1
3 2016101603 30 1
4 2016101604 40 1
5 2016101601 10 2
6 2016101602 20 2
7 2016101603 30 2
8 2016101604 40 2
9 2016101601 10 3
10 2016101602 20 3
11 2016101603 30 3
12 2016101604 40 3
-----------------------------
Here, I want the output be grouped based on unique value of table 1 according to type and name(regardless of table2id)
----------------------------------
TYPE DIR DATE COUNT
----------------------------------
10 IN 20161016 200
20 OUT 20161016 100
---------------------------------
Use a PIVOT:
Oracle Setup:
CREATE TABLE table1 ( id, name, value, table2id ) AS
SELECT 1, 'TYPE', '10', 1 FROM DUAL UNION ALL
SELECT 2, 'DIR', 'IN', 1 FROM DUAL UNION ALL
SELECT 3, 'STATE', 'MA', 1 FROM DUAL UNION ALL
SELECT 4, 'COUNT', '100', 1 FROM DUAL UNION ALL
SELECT 5, 'TYPE', '10', 2 FROM DUAL UNION ALL
SELECT 6, 'DIR', 'IN', 2 FROM DUAL UNION ALL
SELECT 7, 'STATE', 'SA', 2 FROM DUAL UNION ALL
SELECT 8, 'COUNT', '200', 2 FROM DUAL UNION ALL
SELECT 9, 'TYPE', '20', 3 FROM DUAL UNION ALL
SELECT 10, 'DIR', 'OUT', 3 FROM DUAL UNION ALL
SELECT 11, 'STATE', 'MA', 3 FROM DUAL UNION ALL
SELECT 12, 'COUNT', '300', 3 FROM DUAL;
Query:
SELECT *
FROM ( SELECT name, value, table2id FROM table1 )
PIVOT ( MAX(value) FOR name IN ( 'TYPE' AS type, 'DIR' AS DIR ) );
Output:
TABLE2ID TYPE DIR
-------- ---- ---
1 10 IN
2 10 IN
3 20 OUT
Or as an alternative:
SELECT table2id,
MAX( CASE WHEN name = 'TYPE' THEN value END ) AS type,
MAX( CASE WHEN name = 'DIR' THEN value END ) AS dir
FROM table1
GROUP BY table2id;
You could join two subqueries, one that selects the types and one that selects the dirs for the same id:
SELECT type, dir, a.table2id
FROM (SELECT value AS type, table2id
FROM table1
WHERE name = 'TYPE') a
JOIN (SELECT value AS dir, table2id
FROM table1
WHERE name = 'DIR') b ON a.table2id = b.table2id

How to Mapping Columns in a Self-Join table!

I have a parent/child table and want to update Its PK and FK to new values. the problem is that oldParent Ids Should Sync with new ones matching with Old Ids. so:
I have this data as a temp table:
OldID | OldParentID | NewID | NewParentID
1 0 10 NULL
2 0 11 NULL
3 2 13 NULL
4 3 14 NULL
and I Need Update NewParentID as Follows:
OldID | OldParentID | NewID | NewParentID
1 0 10 0
2 0 11 0
3 2 13 11
4 3 14 13
declare #T table
(
OldID int,
OldParentID int,
NewID int,
NewParentID int
)
insert into #T
select 1, 0, 10, null union all
select 2, 0, 11, null union all
select 3, 2, 13, null union all
select 4, 3, 14, null
update T1
set T1.NewParentID = coalesce(T2.NewID, 0)
from #T as T1
left outer join #T as T2
on T1.OldParentID = T2.OldID

problem in writing a sql query

Getting a problem in writing an sql query.
two tables:
1st: created patient table 2nd: already created doc table
patientid patientname docid workstatus docid docname
1 aaa 2 10 1 ggg
2 bbb 2 20 2 hhh
3 ccc 1 10 3 iii
4 ddd 3 10
5 eee 3 20
6 fff 2 10
expected output:
docname workstatus(10) workstatus(20)
ggg 1 0
hhh 2 1
iii 1 1
can also use temporary tables between the queries
Thanks in advance
Try this
Full working example
declare #patient as table(
patientID int IDENTITY(1,1) NOT NULL,
patientName varchar(25),
docID int,
workstatus smallint
)
declare #doc as table(
docID int IDENTITY(1,1) NOT NULL,
docname varchar(25)
)
insert into #patient
select 'aaa', 2, 10
union all
select 'bbb', 2, 20
union all
select 'ccc', 1, 10
union all
select 'ddd', 3, 10
union all
select 'eee', 3, 20
union all
select 'fff', 2, 10
insert into #doc
select 'ggg'
union all
select 'hhh'
union all
select 'iii'
select docname,
SUM(case when t1.workstatus = 10 THEN 1 ELSE 0 END) as [workstatus(10)],
SUM(case when t1.workstatus = 20 THEN 1 ELSE 0 END) as [workstatus(20)]
from #patient t1
inner join #doc t2 on t1.docid=t2.docid
GROUP BY docname
Select d.docname,
SUM(case when c.workstatus = 10 THEN 1 ELSE 0 END) as [WorkStatus(10)],
SUM(case when c.workstatus = 20 THEN 1 ELSE 0 END) as [WorkStatus(20)]
from created_patient_table c
inner join already_created_doc_table d on c.docid=d.docid
group by d.docid,d.docname