How to get delta records using pure SQL? - sql

I have two tables T_Person and T_Person_New in Oracle SQL.
For ease, lets take Name as the unique identifier of both tables.
Can i Compare both tables to get the delta records using an SQL query?
The delta records should consist of the following condition:
If it is a change in an existing record. I.e. a change in DOB / Gender / Name
If its a new record.
Thanks.

We can try using an EXISTS clause here:
SELECT ID, Name, DOB, Gender
FROM T_Person_New t1
WHERE NOT EXISTS (SELECT 1 FROM T_Person t2
WHERE t1.Name = t2.Name AND t1.DOB = t2.DOB AND t1.Gender = t2.Gender);
The logic here is to return every new record for which we cannot find an exactly matching record in the original table. This covers the case that the person already exists, but one or more of the fields have changed. And it also covers the case where the person is completely new, and did not even exist previously.

I have added one more record each in your sample data for old (t_person) and new (t_person_new) tables to cover for missing records from either tables.
I assume that id column is primary key in both tables (it's not clear from you description although you did mention name is unique).
old table sample data
insert into t_person values (1, 'Tom', '2000-01-01', 'M');
insert into t_person values (2, 'Gary', '2000-01-01', 'M');
insert into t_person values (3, 'Pam', '2000-01-01', 'F');
insert into t_person values (4, 'Hans', '2000-01-01', 'M');
new table sample data
insert into t_person_new values (1, 'Tom', '2000-01-01', 'M');
insert into t_person_new values (2, 'Gary', '2001-01-01', 'F');
insert into t_person_new values (3, 'Pamela', '2000-01-01', 'F');
insert into t_person_new values (5, 'Jane', '2000-01-02', 'F');
Here is a query that could show you all possible differences. I have done it only on name column, you can expand it for all columns.
select case when t.id is null then 'NEW: MISSING-FROM-OLD'
else case when tn.id is null then 'DELETED: MISSING-FROM-NEW'
else 'EXISTS-IN-BOTH'
end
end record_type
,case when tn.name is null then 'MISSING-FROM-NEW, VALUE-IN-OLD (' + t.name + ')'
else case when t.name is null then 'MISSING-FROM-OLD, VALUE-IN-NEW (' + tn.name + ')'
else case when t.name = tn.name then 'SAME-IN-BOTH (' + t.name +')'
else 'CHANGED, VALUE-IN-OLD (' + t.name + '), VALUE-IN-NEW (' + tn.name +')'
end
end
end name_state
from t_person_new tn
full outer join t_person t on tn.id = t.id
Note: for Oracle you will have to use '||' instead of '+' to concatenate. I used '+' as I have SQL Server

Related

How to vary result of LISTAGG() depending on number of aggregated elements in Oracle 11g+?

How to print different output within LISTAGG() depending on number of aggregated elements?
Is it possible to get number of aggreated elements without additional COUNT(*) query?
There is an example DDL:
create table shepherds (
SHEPHERD_ID NUMBER(19),
SHEPHERD_NAME VARCHAR2(50 CHAR)
);
create table sheeps (
SHEEP_ID VARCHAR2(10 CHAR),
SHEEP_NAME VARCHAR2(50 CHAR),
SHEEP_SHEPHERD_ID NUMBER(19)
);
-- insert shepherds
insert into shepherds VALUES (111, 'Asher');
insert into shepherds VALUES (222, 'Joseph');
insert into shepherds VALUES (333, 'Nicodemus');
-- first shepherd (one sheep)
insert into sheeps VALUES ('A', 'Mark', 111);
-- second shepherd (two sheeps)
insert into sheeps VALUES ('A', 'Andres', 222);
insert into sheeps VALUES ('B', 'Jeffrey', 222);
-- third shepherd (three sheeps)
insert into sheeps VALUES ('B', 'Jeffrey', 333);
insert into sheeps VALUES ('A', 'Andres', 333);
insert into sheeps VALUES ('D', 'Andres', 333);
Now I want to display all shepherds with new-line separated sheep names in the following way:
SELECT
SHEPHERD_NAME,
(SELECT
listagg(SHEEP_ID || ': ' || SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
FROM SHEEPS
WHERE SHEEP_SHEPHERD_ID = SHEPHERD_ID)
FROM SHEPHERDS;
The result is: http://sqlfiddle.com/#!4/881a7/3
However, I want to hide sheep's ID letter for those shepherds who have only one sheep.
I tried the following:
SELECT
SHEPHERD_NAME,
(SELECT
listagg(
CASE WHEN COUNT(*) > 1 THEN SHEEP_ID || ': ' ELSE '' END
|| SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
FROM SHEEPS
WHERE SHEEP_SHEPHERD_ID = SHEPHERD_ID)
FROM SHEPHERDS;
However, I get error:
ORA-00978: nested group function without GROUP BY
http://sqlfiddle.com/#!4/881a7/7
Is it possible to return different string from LISTAGG() if there is only one element to aggregate?
How to detect number of aggregated elements without slowing down query performance in Oracle 11g or higher?
A conditional expression in the subquery should do what you want:
SELECT sh.SHEPHERD_NAME,
(SELECT (CASE WHEN COUNT(*) = 1 THEN MAX(s.SHEEP_NAME)
ELSE LISTAGG(s.SHEEP_ID || ': ' || s.SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY s.SHEEP_ID)
END) as SHEEPS
FROM SHEEPS s
WHERE s.SHEEP_SHEPHERD_ID = sh.SHEPHERD_ID
) as SHEEPS
FROM SHEPHERDS sh;
Here is a db<>fiddle.
The solution without a subquery use a simple GROUP BY, COUNT(*) = 1 to distinct the sheep count and two different LISTAGG statements
SELECT
s.SHEPHERD_NAME,
case when count(*) = 1 then
listagg(SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID)
else
listagg(SHEEP_ID || ': ' || SHEEP_NAME, CHR(10)) WITHIN GROUP (ORDER BY SHEEP_ID) end as SHEEPS
FROM SHEPHERDS s
JOIN SHEEPS sh on s.SHEPHERD_ID = sh.SHEEP_SHEPHERD_ID
GROUP BY s.SHEPHERD_NAME /* add SHEPHERD_ID in GROUP BY if the name is not unique */
returns
SHEPHERD_NAME, SHEEPS
Asher Mark
Joseph A: Andres
B: Jeffrey
Nicodemus A: Andres
B: Jeffrey
D: Andres

How to concatenate two columns values from another table in the insert statement?

I have a table Comment and I need to insert the values to the table Comment from another table ProComment as shown here:
INSERT INTO Comment (id, insertdate, commenttext, reviewdate)
VALUES (1, GETDATE(), 'This is the new review period from ', GETDATE())
But I want to get the reviewstartdate and reviewenddate from ProComment and need to append with commenttext in the Comment table, like this:
INSERT INTO Comment (id, insertdate, commenttext, reviewdate)
VALUES (1, GETDATE(),
'This is the new review period from ' + (SELECT reviewstartdate, 'to ', reviewenddate FROM ProComment WHERE id = 1),
GETDATE())
Expected results will be "This is the new review period from 2018-05-05 to 2019-05-05" on the comment section
You need to get rid of the Table Value Constructor values and put a regular select statement including CONCAT() function or + operator to concatenate the strings :
insert into Comment( id, insertdate, commenttext, reviewdate)
select 1, getdate(),
concat('This is the new review period from ', reviewstartdate,' to ',reviewenddate),
getdate()
from ProComment
where id = 1

T-SQL - Concatenation of names on TWO tables/orphans

I'm prepared to be crucified for asking my first question on SO and what is a potentially duplicate question, but I cannot find it for the life of me.
I have three tables, a product table, a linking table, and a child table with names. Preloaded on SQLFiddle >> if I still have your attention.
CREATE TABLE Product (iProductID int NOT NULL PRIMARY KEY
, sProductName varchar(50) NOT NULL
, iPartGroupID int NOT NULL)
INSERT INTO Product VALUES
(10001, 'Avionic Tackle', '1'),
(10002, 'Eigenspout', '2'),
(10003, 'Impulse Polycatalyst', '3'),
(10004, 'O-webbing', '2'),
(10005, 'Ultraservo', '3'),
(10006, 'Yttrium Coil', '5')
CREATE TABLE PartGroup (iPartGroupID int NOT NULL
, iChildID int NOT NULL)
INSERT INTO PartGroup VALUES
(1, 1),
(2, 2),
(3, 1),
(3, 2),
(3, 3),
(3, 4),
(4, 5),
(4, 6),
(5, 1)
CREATE TABLE PartNames (iChildID int NOT NULL PRIMARY KEY
, sPartNameText varchar(50) NOT NULL)
INSERT INTO PartNames VALUES
(1, 'Bulbcap Lube'),
(2, 'Chromium Deltaquartz'),
(3, 'Dilation Gyrosphere'),
(4, 'Fliphose'),
(5, 'G-tightener Bypass'),
(6, 'Heisenberg Shuttle')
I am trying to find out how to list all the part groups (that may or may not belong to a product), and translate their child names. That is, how do I use only the linking table and child name table to list all the translated elements of the linking table. I am trying to find orphans.
I have two queries:
SELECT P.iPartGroupID
,STUFF(
(SELECT
CONCAT(', ', PN.sPartNameText)
FROM PartGroup PG
INNER JOIN PartNames PN ON PN.iChildID = PG.iChildID
WHERE PG.iPartGroupID = P.iPartGroupID
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)')
, 1, 2, ''
) AS [Child Elements]
FROM Product P
GROUP BY P.iPartGroupID
This lists all the part groups that belong to a product, and their child elements by name. iPartGroupID = 4 is not here.
I also have:
SELECT PG.iPartGroupID
,STUFF(
(SELECT
CONCAT(', ', PGList.iChildID)
FROM PartGroup PGList
WHERE PGList.iPartGroupID = PG.iPartGroupID
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)')
, 1, 2, ''
) AS [Child Elements]
FROM PartGroup PG
GROUP BY PG.iPartGroupID
This lists all the part groups, and their child elements by code. iPartGroupID = 4 is covered here, but the names aren't translated.
What query can I use to list the orphan part groups (and also the orphan parts):
4 G-tightener Bypass, Heisenberg Shuttle
Ideally it is included in a list of all the other part groups, but if not, I can union the results.
Every other SO question I've looked up uses either 3 tables, or only 1 table, self joining with aliases. Does anyone have any ideas?
No XML in the part names, no particular preference for CONCAT or SELECT '+'.
I would link to other posts, but I can't without points :(
I'm not entirely sure what do you mean, exactly, when you use the word "translate". And your required output seems to contradict your sample data (if I'm not lost something).
Nevertheless, try this query, maybe it's what you need:
select sq.iPartGroupID, cast((
select pn.sPartNameText + ',' as [data()] from #PartNames pn
inner join #PartGroup p on pn.iChildID = p.iChildID
where p.iPartGroupID = sq.iPartGroupID
order by pn.iChildID
for xml path('')
) as varchar(max)) as [GroupList]
from (select distinct pg.iPartGroupID from #PartGroup pg) sq
left join #Product pr on sq.iPartGroupID = pr.iPartGroupID
where pr.iProductID is null;
Following way you can use to get the answer you want
SELECT pg.iPartGroupID,
CASE COUNT(pg.iPartGroupID)
WHEN 1 THEN (
SELECT pn2.sPartNameText
FROM PartNames pn2
WHERE pn2.iChildID = pg.iPartGroupID
)
ELSE (
SELECT CASE ROW_NUMBER() OVER(ORDER BY(SELECT 1))
WHEN 1 THEN ''
ELSE ','
END + pn2.sPartNameText
FROM PartNames pn2
INNER JOIN PartGroup pg2
ON pg2.iChildID = pn2.iChildID
WHERE pg2.iPartGroupID = pg.iPartGroupID
FOR XML PATH('')
)
END
FROM PartGroup pg
GROUP BY
pg.iPartGroupID

Build string from a SELECT statement [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
SQL Server Group Concat with Different characters
I need an example on SELECT where the output is a ',' separated string
e.g. SELECT (... something ...) name AS output FROM name_table
gives me
output
-----------------------------
'Ann', 'Tom', 'Wilson', .....
How would you do that in SQL server 2008 R2?
Thank you!
Assuiming you have a schema like this,
CREATE TABLE Table1
([GROUP_ID] int, [PERSON_NAME] varchar(6));
INSERT INTO Table1
([GROUP_ID], [PERSON_NAME])
VALUES
(1001, 'ALEX'),
(1002, 'MATHEW'),
(1001, 'GEORGE'),
(1002, 'THOMAS'),
(1001, 'JAMES');
create a query something like this to produce a comma separated value,
SELECT
GROUP_ID,
STUFF(
(SELECT ', ' + PERSON_NAME
FROM Table1
WHERE [GROUP_ID] = a.GROUP_ID
FOR XML PATH (''))
, 1, 1, '') AS NamesList
FROM Table1 AS a
GROUP BY GROUP_ID
SQLFiddle Demo

Query to reflect actual significant change in data

Given a table with employee statuses and effective dates, how can I retrieve just the data that reflects a change in status?
For example, given the following structure:
DECLARE #STATUSES TABLE(
EMPLOYEE_ID INT NOT NULL,
EFFECTIVE_DATE DATE NOT NULL,
STATUS_CODE CHAR(1) NOT NULL
)
INSERT #STATUSES VALUES (1, '2012-01-01', 'A')
INSERT #STATUSES VALUES (1, '2012-02-28', 'A')
INSERT #STATUSES VALUES (1, '2012-03-01', 'T')
INSERT #STATUSES VALUES (2, '2012-01-01', 'A')
INSERT #STATUSES VALUES (2, '2012-02-14', 'A')
INSERT #STATUSES VALUES (2, '2012-03-10', 'A')
INSERT #STATUSES VALUES (3, '2012-02-01', 'A')
INSERT #STATUSES VALUES (3, '2012-03-17', 'A')
INSERT #STATUSES VALUES (3, '2012-03-18', 'T')
INSERT #STATUSES VALUES (3, '2012-04-01', 'A')
INSERT #STATUSES VALUES (4, '2012-03-01', 'A')
What query can be used to result in the following?
EMPLOYEE_ID EFFECTIVE_DATE STATUS_CODE
1 2012-01-01 A
1 2012-03-01 T
2 2012-01-01 A
3 2012-02-01 A
3 2012-03-18 T
3 2012-04-01 A
4 2012-03-01 A
In other words, I want to leave out those records that have the same employee id and status code as the one before it, if one exists with an earlier effective date. Notice that employee 1 is listed only twice because there were only two actual changes in status--the one on 2012-02-28 is inconsequential since the status didn't change from the earlier date. Also notice that employee 2 is listed just once since his status never changed despite there being three records. Only the earliest date is shown for each change.
With some further experimenting, it looks like this will do what I want.
;WITH cte
AS (SELECT ROW_NUMBER() OVER (PARTITION BY EMPLOYEE_ID ORDER BY EFFECTIVE_DATE) AS rownum
,EMPLOYEE_ID
,EFFECTIVE_DATE
,STATUS_CODE
FROM #STATUSES)
SELECT t2.EMPLOYEE_ID
,t2.EFFECTIVE_DATE
,t2.STATUS_CODE
FROM cte t2
LEFT JOIN cte t1
ON t2.EMPLOYEE_ID = t1.EMPLOYEE_ID
AND t2.STATUS_CODE = t1.STATUS_CODE
AND t2.rownum = t1.rownum + 1
WHERE t1.EMPLOYEE_ID IS NULL
You could use a CURSOR
You'd need two sets of variables: #PreviousRecord and #CurrentRecord
Declare the cursor for table sorted by employeeid and date
Fetch the first record from the cursor into the #PreviousRecord variables - depending on your requirement register this as a significant change or not (write the record to a temp table)
Then set up a loop that:
Fetches the next record into the #CurrentRecord variables
Compares it with the previous record and if it matches your requirement for a significant change write it to the temp table
Move the #CurrentRecord values into the #PreviousRecord variables
I'd be interested to know if the CTE method was more efficient
SELECT
EMPLOYEE_ID, MIN(EFFECTIVE_DATE) AS EFFECTIVE_DATE, STATUS_CODE
FROM
(
SELECT
T1.EMPLOYEE_ID, T1.EFFECTIVE_DATE, T1.STATUS_CODE,
MAX(T2.EFFECTIVE_DATE) AS MOST_RECENT_PREVIOUS_STATUS_DATE
FROM
#STATUSES T1
LEFT JOIN
#STATUSES T2
ON
T1.EMPLOYEE_ID = T2.EMPLOYEE_ID
AND
T1.EFFECTIVE_DATE > T2.EFFECTIVE_DATE
AND
T1.STATUS_CODE <> T2.STATUS_CODE
GROUP BY
T1.EMPLOYEE_ID, T1.EFFECTIVE_DATE, T1.STATUS_CODE
) SubQuery
GROUP BY
EMPLOYEE_ID, STATUS_CODE, MOST_RECENT_PREVIOUS_STATUS_DATE