SQL Merge Data into Single Row - sql

I have a table set up that tracks changes to a user's account.
It has ID, UserAccountNo, OldVal, NewVal, ChangeColumnName columns.
I have a query set up similar to this:
Select case
when ChangeColumnName = 'Address1' then NewVal else '' end as Address1,
when ChangeColumnName = 'Address2' then NewVal else '' end as Address2,
when ChangeColumnName = 'City' then NewVal else '' end as City,
when ChangeColumnName = 'State' then NewVal else '' end as State,
when ChangeColumnName = 'Zip' then NewVal else '' end as Zip,
when ChangeColumnName = 'Phone' then NewVal else '' end as Phone
from table
Where (Conditions)
If someone changes the city, state, and zip, there are 3 entries in the table. When I run this query, I get 3 rows returned. I would like to get them all together in one row, and haven't been able to figure out how.
When I tried using groupby with max(colname) as suggested in other posts, it gives the max NewVal value, so I end up with email addresses in Phone columns.
Is this possible to do in SQL 2008 without reforming the entire table?

Try this
create table #t
(
id int,
userAccountNo int,
oldVal varchar(255),
newVal varchar(255),
changeColName varchar(255)
);
insert #t values (1, 1, '123 main st', '123 s. main st.', 'Address1'),
(2, 1, 'Springville', 'Springfield', 'City'),
(3, 1, 'Springfield', 'N. Springfield', 'City'),
(4, 2, '12345', '12346', 'Zip');
with U as (select distinct userAccountNo from #t),
Address1 as (select userAccountNo, newVal from #t as T1 where changeColName = 'Address1' and id >=ALL
(select id from #t as T2 where T1.userAccountNo = T2.userAccountNo and T1.changeColName = T2.changeColName)),
City as (select userAccountNo, newVal from #t as T1 where changeColName = 'City' and id >=ALL
(select id from #t as T2 where T1.userAccountNo = T2.userAccountNo and T1.changeColName = T2.changeColName)),
Zip as (select userAccountNo, newVal from #t as T1 where changeColName = 'Zip' and id >=ALL
(select id from #t as T2 where T1.userAccountNo = T2.userAccountNo and T1.changeColName = T2.changeColName))
select
U.userAccountNo,
A1.newVal as [Address1],
C.newVal as [City],
Z.newVal as [Zip]
from
U
full outer join Address1 as A1 on U.userAccountNo = A1.userAccountNo
full outer join City as C on U.userAccountNo = C.userAccountNo
full outer join Zip as Z on U.userAccountNo = Z.userAccountNo;
And if it seems to work it can be extended to cover all of your columns.

I suggest that you use pivot command, use this script and let me know :
IF OBJECT_ID('_temp') IS NOT NULL DROP TABLE _temp
SELECT *
INTO _temp
FROM (
Select 'PostalCode' AS ChangeColumnName, '95100' AS NewValue UNION ALL
Select 'City' AS ChangeColumnName, 'Argenteuil' AS NewValue UNION ALL
Select 'LastName' AS ChangeColumnName, 'DAOUI' AS NewValue UNION ALL
Select 'FirstName' AS ChangeColumnName, 'Youssef' AS NewValue UNION ALL
Select 'Phone Number' AS ChangeColumnName, '00212 6 60 93 36 12' AS NewValue
) AS Temp
DECLARE #v_ListeColonnes VARCHAR(MAX) = ''
,#v_sql VARCHAR(MAX) = ''
SELECT #v_ListeColonnes = #v_ListeColonnes + ',' + QUOTENAME(ChangeColumnName)
FROM _temp
IF LEN(#v_ListeColonnes) > 1
BEGIN
SELECT #v_ListeColonnes = RIGHT(#v_ListeColonnes, LEN(#v_ListeColonnes)-1)
SET #v_sql = 'SELECT '+CHAR(13)
+' ' + #v_ListeColonnes + ' '+CHAR(13)
+'FROM _temp '+CHAR(13)
+'PIVOT (MAX(NewValue) '+CHAR(13)
+' FOR ChangeColumnName in(' + #v_ListeColonnes + ')) as pvt '+CHAR(13)
EXEC(#v_sql)
END
IF OBJECT_ID('_temp') IS NOT NULL DROP TABLE _temp
I hope this will help you.

I assumed you need one row and one column for all changes, it works for any number of columns changed.
SQL FIDDLE TEST
declare #changes as varchar(max)
declare #UserAccountNo int
set #UserAccountNo=1
set #changes=''
select #changes=#changes + ColumnChanged +'-'
from changes where UserAccountNo=#UserAccountNo
select #UserAccountNo 'UserAccountNo', #changes 'Changes'

Related

How to specify multiple values in when using case statement in sql server

I have the following table
Id Number TypeOfChange
1 2X Scope,Cost,Schedule,EVM,PA
2 3x Scope,Cost
Expected output:
Id Number TypeOfChange Scope Cost Schedule EVM PA
1 2X Scope,Cost,Schedule,EVM,PA X X X X X
2 3x Scope,Cost X X
I try the following script but its not working
SELECT
Id,
Number,
TypeOfChange,
Scope = CASE
WHEN TypeOfChange = 'Scope' THEN 'X'
ELSE '' END,
Cost = CASE
WHEN TypeOfChange = 'Cost' THEN 'X'
ELSE '' END,
Schedule = CASE
WHEN TypeOfChange = 'Schedule' THEN 'X'
ELSE '' END,
EVM = CASE
WHEN TypeOfChange = 'EVM' THEN 'X'
ELSE '' END,
PA = CASE
WHEN TypeOfChange = 'PA' THEN 'X'
ELSE '' END
FROM A
Use Like operator.
SELECT
Id,
Number,
TypeOfChange,
Scope = CASE
WHEN TypeOfChange Like '%Scope%' THEN 'X'
ELSE '' END,
Cost = CASE
WHEN TypeOfChange Like '%Cost%' THEN 'X'
ELSE '' END,
Schedule = CASE
WHEN TypeOfChange Like '%Schedule%' THEN 'X'
ELSE '' END,
EVM = CASE
WHEN TypeOfChange Like '%EVM%' THEN 'X'
ELSE '' END,
PA = CASE
WHEN TypeOfChange Like '%PA%' THEN 'X'
ELSE '' END
FROM A
Example:
we can try charindex or patindex
SELECT
Id,
Number,
TypeOfChange,
Scope = CASE
WHEN CHARINDEX('Scope',TypeOfChange)>0 THEN 'X'
ELSE '' END,
Cost = CASE
WHEN CHARINDEX('Cost',TypeOfChange)>0 THEN 'X'
ELSE '' END,
Schedule = CASE
WHEN CHARINDEX('Schedule',TypeOfChange)>0 THEN 'X'
ELSE '' END,
EVM = CASE
WHEN CHARINDEX('EVM',TypeOfChange)>0 THEN 'X'
ELSE '' END,
PA = CASE
WHEN CHARINDEX('PA',TypeOfChange)>0 THEN 'X'
ELSE '' END
FROM #AA
output
Id Number TypeOfChange Scope Cost Schedule EVM PA
1 2X Scope,Cost,Schedule,EVM,PA X X X X X
2 3x Scope,Cost X X
If TypeOfChange is a dynamic value, you may want to go the dynamic route.
select * into [T1] from
(values (1, '2X', 'Scope,Cost,Schedule,EVM,PA'), (2, '3x', 'Scope,Cost'), (3, '4x', 'someOtherType')) t(Id, Number, TypeOfChange)
--typeOfChange into column list
Declare #SQL varchar(max) = Stuff((
SELECT distinct ',' + QuoteName(LTRIM(RTRIM(m.n.value('.[1]','varchar(8000)'))))
FROM ( SELECT CAST('<XMLRoot><RowData>' + REPLACE(TypeOfChange,',','</RowData><RowData>') + '</RowData></XMLRoot>' AS XML) AS x
FROM [T1]) t CROSS APPLY x.nodes('/XMLRoot/RowData')m(n)
Order by 1 For XML Path('')),1,1,'')
Select #SQL = '
Select [Id],[Number], [TypeOfChange],' + #SQL + '
From (
SELECT Id, Number, TypeOfChange,
LTRIM(RTRIM(m.n.value(''.[1]'',''varchar(8000)''))) AS [Type], ''X'' as Value
FROM ( SELECT Id, Number, TypeOfChange, CAST(''<XMLRoot><RowData>'' + REPLACE(TypeOfChange,'','',''</RowData><RowData>'') + ''</RowData></XMLRoot>'' AS XML) AS x
FROM [T1]) t CROSS APPLY x.nodes(''/XMLRoot/RowData'')m(n)
) A
Pivot (max(Value) For [Type] in (' + #SQL + ') ) pvt'
Exec(#SQL);
Alternatively you may want to define your Types in a lookup table
select * into [Types] from
(values (1, 'Scope'), (2, 'Cost'), (3, 'Schedule'), (4, 'EVM'), (5, 'PA'), (6, 'someOtherType')) a (Id, TypeOfChange)
Then change the above --typeOfChange into column.. block like this:
--typeOfChange into column list
Declare #SQL varchar(max) = Stuff((
SELECT distinct ',' + QuoteName(TypeOfChange)
FROM [Types]
Order by 1 For XML Path('')),1,1,'')
I think, using LIKE is wrong approach. Especcialy in cases, when one of your strings become f.e."Periscope". You will get false positives.
Try to create user defined function to split strs:
CREATE FUNCTION [dbo].[str__split](
#str NVARCHAR(MAX)
,#delimiter NVARCHAR(MAX)
)
RETURNS #split TABLE(
[str] NVARCHAR(MAX)
)
AS
BEGIN
INSERT INTO #split(
[str]
)
SELECT
[X].[C].[value]('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT
[X] = CONVERT(XML, '<i>' + REPLACE(#str, #delimiter, '</i><i>') + '</i>').query('.')
) AS [A]
CROSS APPLY
[X].[nodes]('i') AS [X]([C]);
RETURN;
END
And then use query:
SELECT
[t].*
,[Scope] = CASE WHEN [t2].[Scope] IS NULL THEN NULL ELSE 'X' END
,[Cost] = CASE WHEN [t2].[Cost] IS NULL THEN NULL ELSE 'X' END
,[Schedule] = CASE WHEN [t2].[Schedule] IS NULL THEN NULL ELSE 'X' END
,[EVM] = CASE WHEN [t2].[EVM] IS NULL THEN NULL ELSE 'X' END
,[PA] = CASE WHEN [t2].[PA] IS NULL THEN NULL ELSE 'X' END
FROM
[your table] AS [t]
OUTER APPLY
(
SELECT * FROM (SELECT [str] from [dbo].[str__split]([TypeOfChange], ',')) AS [d]
PIVOT
(MAX([str]) FOR [str] IN ([Scope], [Cost], [Schedule], [EVM], [PA])) AS [piv]
) AS [t2]

Get separate row for each column change in cdc

I Created a simple Employee table with 3 fields
FirstName , LastName , CurrentPayScale
This is what I did so far
DECLARE #begin_lsn BINARY(10), #end_lsn BINARY(10) , #A NVARCHAR(100)
SET #begin_lsn = sys.fn_cdc_get_min_lsn('dbo_Employee')
SET #end_lsn = sys.fn_cdc_get_max_lsn()
SELECT __$operation AS OperationTypeId,
CASE __$operation WHEN 1 THEN 'has deleted' WHEN 2 THEN 'has added' ELSE 'has updated' END AS [Action],
( SELECT CC.column_name + ','
FROM cdc.captured_columns CC
INNER JOIN cdc.change_tables CT ON CC.[object_id] = CT.[object_id]
WHERE capture_instance = 'dbo_Employee'
AND sys.fn_cdc_is_bit_set(CC.column_ordinal, EmployeeCDC.__$update_mask) = 1
FOR XML PATH('')) AS ChangedColumns,
(SELECT FIRSTNAME + ','
FROM [cdc].[dbo_Employee_CT]
WHERE __$start_lsn = EmployeeCDC.__$start_lsn
AND __$operation = 3 FOR XML PATH('')) as 'Old Value',
EmployeeCDC.FirstName AS 'New Value'
FROM [cdc].[dbo_Employee_CT] AS EmployeeCDC
WHERE EmployeeCDC.__$operation <> 3
This query return this result
Now in third row you can see 2 columns were changed FirstName and CurrentPayScale
I just want to see result by each column, I think it can be done with pivot as far as I searched, but I also don't know how to use pivot.

Converting updated column values to a table as rows

ID State Name Department City
1 O George Sales Phoenix
1 N George Sales Denver
2 O Michael Order Process San diego
2 N Michael Marketing San jose
I got a situation that I need to convert the above tables values to the following format.(Consider the top row is column names)
ID Column OldValue New Value
1 Department Phoenix Denver
2 Department Order Process Marketing
2 City San diego San jose
I.e : I need to capture the changed column values for a table from its old and new records and record them in a different table.But the problem is we have many tables like that and the column names and no of columns are different for each table.
If anyone come with a solution that would be greatly appreciated..!
Thank you in advance.
Is this what you want?
ID Column OldValue New Value
1 City Phoenix Denver
2 Department Order Process Marketing
2 City San Diego San jose
Here is the dynamic code:
DECLARE #sqlStm varchar(max);
DECLARE #sqlSelect varchar(max);
DECLARE #tablename varchar(200);
SET #tablename = 'testtable';
-- Assume table has ID column and State column.
SET #sqlSelect = ''
SET #sqlStm = 'WITH old AS
(
SELECT *
FROM '+#tablename+'
WHERE State=''O''
), new AS
(
SELECT *
FROM '+#tablename+'
WHERE State=''N''
)';
DECLARE #aCol varchar(128)
DECLARE curCols CURSOR FOR
SELECT column_name
FROM information_schema.columns
WHERE table_name = #tablename
AND UPPER(column_name) NOT IN ('ID','STATE')
OPEN curCols
FETCH curCols INTO #aCol
WHILE (##FETCH_STATUS = 0)
BEGIN
SET #sqlStm = #sqlStm +
', changed'+#aCol+' AS
(
SELECT n.ID, '''+#aCol+''' AS [Column], o.['+#aCol+'] AS oldValue, n.['+#aCol+'] AS newValue
FROM new n
JOIN old o ON n.ID = o.ID AND n.['+#aCol+'] != o.['+#aCol+']
)'
IF LEN(#sqlSelect) > 0 SET #sqlSelect = #sqlSelect + ' UNION ALL '
SET #sqlSelect = #sqlSelect + '
SELECT * FROM changed'+#aCol
FETCH curCols INTO #aCol
END
CLOSE curCols
DEALLOCATE curCols
SET #sqlSelect = #sqlSelect + '
ORDER BY id, [Column]'
PRINT #sqlStm+#sqlSelect
EXEC (#sqlStm+#sqlSelect)
Which in my test output the following:
WITH old AS
(
SELECT *
FROM testtable
WHERE State='O'
), new AS
(
SELECT *
FROM testtable
WHERE State='N'
), changedName AS
(
SELECT n.ID, 'Name' AS [Column], o.[Name] AS oldValue, n.[Name] AS newValue
FROM new n
JOIN old o ON n.ID = o.ID AND n.[Name] != o.[Name]
), changedDepartment AS
(
SELECT n.ID, 'Department' AS [Column], o.[Department] AS oldValue, n.[Department] AS newValue
FROM new n
JOIN old o ON n.ID = o.ID AND n.[Department] != o.[Department]
), changedCity AS
(
SELECT n.ID, 'City' AS [Column], o.[City] AS oldValue, n.[City] AS newValue
FROM new n
JOIN old o ON n.ID = o.ID AND n.[City] != o.[City]
)
SELECT * FROM changedName UNION ALL
SELECT * FROM changedDepartment UNION ALL
SELECT * FROM changedCity
ORDER BY id, [Column]
Original answer below:
I would do it like this -- because I think it is clearer than other ways which might be faster:
with old as
(
Select ID, Name,Department,City
From table1
Where State='O'
), new as
(
Select ID, Name,Department,City
From table1
Where State='N'
), oldDepartment as
(
Select ID, 'Department' as Column, o.Department as oldValue, n.Department as newValue
From new
join old on new.ID = old.ID and new.Department != old.Department
), oldCity as
(
Select ID, 'City' as Column, o.City as oldValue, n.City as newValue
From new
join old on new.ID = old.ID and new.City != old.City
)
select * from oldDepartment
union all
select * from oldCity
Depending on many things (size of tables and indexes etc) it might actually be faster than using pivots or cases or grouping. It really depends on your data. If this is a one-off run I'd just go for the easiest to grok.
The cleanest approach is probably to unpivot the data and then use aggregation. This does require custom coding for each table, which you might be able to generalize by using some form a dynamic SQL.
For your particular example, here is an illustration of what to do:
select id, col,
max(case when OldNew = 'Old' then value end) as OldValue,
max(case when OldNew = 'New' then value end) as NewValue
from ((select ID, OldNew, 'Name' as col, Name as value
from t
) union all
(select ID, OldNew, 'Department' as col, Department as value
from t
) union all
(select ID, OldNew, 'City' as col, City as value
from t
)
) unpvt
group by id, col
having max(value) <> min(value) and max(value) is not null;
This is for illustration purposes. The unpivot can be done more efficiently than using union all, particularly when there are many scans. Here is a more efficient version, although the exact syntax depends on the database:
select id, col,
max(case when OldNew = 'Old' then value end) as OldValue,
max(case when OldNew = 'New' then value end) as NewValue
from (select ID, OldNew, cols.col,
(case when cols.col = 'Name' then Name
when cols.col = 'Department' then Department
when cols.col = 'City' then City
end) as value
from t cross join
(select 'Name' as col union all select 'Department' union all select 'City') cols
) unpvt
group by id, col
having max(value) <> min(value) and max(value) is not null;
This is more efficient because it will typically only scan your table once, rather than once for each column as in the union all version.
In either version, there is an implicit assumption that all the columns have the same character type. This is implicit in the format you are converting to, where all the values are in a single column.

How to count in SQL all fields with null values in one record?

Is there any way to count all fields with null values for specific record excluding PrimaryKey column?
Example:
ID Name Age City Zip
1 Alex 32 Miami NULL
2 NULL 24 NULL NULL
As output I need to get 1 and 3. Without explicitly specifying column names.
declare #T table
(
ID int,
Name varchar(10),
Age int,
City varchar(10),
Zip varchar(10)
)
insert into #T values
(1, 'Alex', 32, 'Miami', NULL),
(2, NULL, 24, NULL, NULL)
;with xmlnamespaces('http://www.w3.org/2001/XMLSchema-instance' as ns)
select ID,
(
select *
from #T as T2
where T1.ID = T2.ID
for xml path('row'), elements xsinil, type
).value('count(/row/*[#ns:nil = "true"])', 'int') as NullCount
from #T as T1
Result:
ID NullCount
----------- -----------
1 1
2 3
Update:
Here is a better version. Thanks to Martin Smith.
;with xmlnamespaces('http://www.w3.org/2001/XMLSchema-instance' as ns)
select ID,
(
select T1.*
for xml path('row'), elements xsinil, type
).value('count(/row/*[#ns:nil = "true"])', 'int') as NullCount
from #T as T1
Update:
And with a bit faster XQuery expression.
;with xmlnamespaces('http://www.w3.org/2001/XMLSchema-instance' as ns)
select ID,
(
select T1.*
for xml path('row'), elements xsinil, type
).value('count(//*/#ns:nil)', 'int') as NullCount
from #T as T1
SELECT id,
CASE WHEN Name IS NULL THEN 1 ELSE 0 END +
CASE WHEN City IS NULL THEN 1 ELSE 0 END +
CASE WHEN Zip IS NULL THEN 1 ELSE 0 END
FROM YourTable
If you do not want explicit column names in query, welcome to dynamic querying
DECLARE #sql NVARCHAR(MAX) = ''
SELECT #sql = #sql + N' CASE WHEN '+QUOTENAME(c.name)+N' IS NULL THEN 1 ELSE 0 END +'
FROM sys.tables t
JOIN sys.columns c
ON t.object_id = c.object_id
WHERE
c.is_nullable = 1
AND t.object_id = OBJECT_ID('YourTableName')
SET #sql = N'SELECT id, '+#sql +N'+0 AS Cnt FROM [YourTableName]'
EXEC(#sql)
This should solve your problem:
select count (id)
where ( isnull(Name,"") = "" or isnull(City,"") = "" or isnull(Zip,"") = "" )
Not a smart solution, but it should do the work.
DECLARE #tempSQL nvarchar(max)
SET #tempSQL = N'SELECT '
SELECT #tempSQL = #tempSQL + 'sum(case when ' + cols.name + ' is null then 1 else 0 end) "Null Values for ' + cols.name + '",
sum(case when ' + cols.name + ' is null then 0 else 1 end) "Non-Null Values for ' + cols.name + '",' FROM sys.columns cols WHERE cols.object_id = object_id('TABLE1');
SET #tempSQL = SUBSTRING(#tempSQL, 1, LEN(#tempSQL) - 1) + ' FROM TABLE1;'
EXEC sp_executesql #tempSQL

Coalesce and Pivot in TSQL

I am having trouble figuring out how to coalesce or pivot on a SQL recordset that looks like this:
ID VALUE GROUP
3 John 18
4 Smith 18
5 Microsoft 18
3 Randy 21
4 Davis 21
5 IBM 21
etc
and I want formatted like this
NEWVALUE GROUP
Smith, John (Microsft) 18
Davis, Randy (IBM) 21
thanks for any suggestions and help!
This is what i done, i hope it fits for you
DECLARE #t table (id int, value VARCHAR(20), grupo int)
INSERT #T VALUES (3, 'John', 18)
INSERT #T VALUES (4, 'Smith', 18)
INSERT #T VALUES (5, 'Microsoft', 18)
INSERT #T VALUES (3, 'Randy', 21)
INSERT #T VALUES (4, 'Davis', 21)
INSERT #T VALUES (5, 'IBM', 21)
SELECT grupo, (SELECT value FROM #t t2 WHERE t2.grupo = t.grupo AND id = 4) + ', ' +
(SELECT value FROM #t t2 WHERE t2.grupo = t.grupo AND id = 3) + ' (' +
(SELECT value FROM #t t2 WHERE t2.grupo = t.grupo AND id = 5) + ')'
FROM #t t
GROUP BY grupo
SELECT LEFT(gvalue, LEN(gvalue) - 1) AS newvalue, _group
FROM (
SELECT DISTINCT _group
FROM mytable
) qo
CROSS APPLY
(
SELECT value + ', '
FROM mytable qi
WHERE qi._group = qo._group
FOR XML PATH ('')
) gr(qvalue)
If you always have a set of three hardcoded ID's for each _group, you can just use:
SELECT m3._group, m3.value + ', ' + m4.value + '(' + m5.value + ')' AS newvalue
FROM mytable m3
LEFT JOIN
mytable m4
ON m4._group = m3.group
LEFT JOIN
mytable m5
ON m5._group = m3.group
WHERE m3.id = 3
AND m4.id = 4
AND m5.id = 5
What you need is not pivoted query but a simple select with group by and an aggregate string concatenation function. But i don't remember the exact function in tsql.
Update: there is no aggregate concatenation function in tsql but since sql2005 you can write your own extension to implement such function. There is plenty of examples on google search for: tsql 2005 concatenation aggregate example.
This is a little hokey, but I think it should work reasonably well for a small data set. If you've got a lot of data you need to create a cursor and a loop.
select max(case when ID = 4 then VALUE else null end) + ', ' +
max(case when ID = 4 then VALUE else null end) + '( ' +
max(case when ID = 5 then VALUE else null end) + ') as NEWVALUE,
[GROUP]
group by [GROUP]