order by using terms in where clause - sql

I have a simple select query -
SELECT ID, NAME
FROM PERSONS
WHERE NAME IN ('BBB', 'AAA', 'ZZZ')
-- ORDER BY ???
I want this result to be ordered by the sequence in which NAMES are provided, that is,
1st row in result set should be the one with NAME = BBB, 2nd is AAA, 3rd it ZZZ.
Is this possible in SQL server ? I would like to know how to do it if there is a simple and short way of doing it, like maybe 5-6 lines of code.

You could create an ordered split function:
CREATE FUNCTION [dbo].[SplitStrings_Ordered]
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
AS
RETURN (SELECT [Index] = ROW_NUMBER() OVER (ORDER BY Number), Item
FROM (SELECT Number, Item = SUBSTRING(#List, Number,
CHARINDEX(#Delimiter, #List + #Delimiter, Number) - Number)
FROM (SELECT ROW_NUMBER() OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2) AS n(Number)
WHERE Number <= CONVERT(INT, LEN(#List))
AND SUBSTRING(#Delimiter + #List, Number, LEN(#Delimiter)) = #Delimiter
) AS y);
Then alter your input slightly (a single comma-separated list instead of three individual strings):
SELECT p.ID, p.NAME
FROM dbo.PERSONS AS p
INNER JOIN dbo.SplitStrings_Ordered('BBB,AAA,ZZZ', ',') AS s
ON p.NAME = s.Item
ORDER BY s.[Index];

You could store the names in a temp table with an order. Example:
DECLARE #Names TABLE (
Name VARCHAR(MAX),
SortOrder INT
)
INSERT INTO #Names (Name, SortOrder) VALUES ('BBB', 1)
INSERT INTO #Names (Name, SortOrder) VALUES ('AAA', 2)
INSERT INTO #Names (Name, SortOrder) VALUES ('ZZZ', 3)
SELECT P.ID, P.NAME
FROM PERSONS P
JOIN #Names N ON P.Name = N.Name
ORDER BY N.SortOrder

There is no way to do this using the order in the IN predicate, however, you could create a table of constants giving your constants an order by value:
SELECT p.ID, p.NAME
FROM PERSONS p
INNER JOIN
( VALUES
('BBB', 1),
('AAA', 2),
('ZZZ', 3)
) t (Name, SortOrder)
ON p.Name = t.Name
ORDER BY t.SortOrder;
The other (and in my option less attractive) solution is to use CASE
SELECT ID, NAME
FROM PERSONS
WHERE NAME IN ('BBB', 'AAA', 'ZZZ')
ORDER BY CASE Name
WHEN 'BBB' THEN 1
WHEN 'AAA' THEN 2
WHEN 'ZZZ' THEN 3
END;

SELECT ID, NAME
FROM PERSONS
WHERE NAME IN ('BBB', 'AAA', 'ZZZ')
ORDER BY CASE
WHEN NAME = 'BBB' THEN 1
WHEN NAME = 'AAA' THEN 2
WHEN NAME = 'ZZZ' THEN 3
END ASC

I think this must work:
ORDER BY CASE
WHEN NAME = 'BBB' THEN 0
WHEN NAME = 'AAA' THEN 1
WHEN NAME = 'ZZZ' THEN 2
ELSE 3
END ASC

Related

Put numbers for each rows in STRING_AGG in SQL Server

I have to put some columns and group them together by STRING_AGG also I want to put number first of each rows
What I have:
Name
Cake
Coca
ice-cream
Same
one
five
six
Sara
one
one
NULL
John
two
two
NULL
I want the output be something like this:
Name
Description
Sam
1.two 2.five 3. six
Sara
1.one 2.one
John
1.two 2.two
My Code:
SELECT Name, STRIN_AGG(CONCAT(Cake, ' ,', Coca,' ,', ice-cream))
FROM FoodTable
but I do not know how to consider numbers first of each rows in STRING_AGG
You don't need string_agg():
select name,
concat('1.' + cake, ' 2.' + coca, ' 3.' + ice_cream)
from t;
Note that + returns NULL if any value is NULL. However, concat() simply ignores NULL values.
If you really, really wanted to use string_agg() you could:
select t.name, v.all_together
from t cross apply
(select string_agg(v.n + v.val, ' ') within group (order by v.n) as all_together
from (values ('1.', t.cake),
('2.', t.coca),
('3.', t.ice_cream)
) v(n, val)
) v;
Here is a complete dynamic sql approach. No need to serialize and deserialize the data using XML or JSON. In this case the list of food items is contained in a temporary table so it reads the column names from tempdb.sys.columns.
The query uses CROSS APPLY to unpivot the columns (of food items) and assigns a ROW_NUMBER() to each non NULL item value. Something like this
drop table if exists #FoodTable;
go
create table #FoodTable(
[Name] varchar(100) not null,
Cake varchar(100) null,
Coca varchar(100) null,
[ice-cream] varchar(100) null);
--select * from dbo.test_actuals
insert #FoodTable values
('Sam', 'one', 'five', 'six'),
('Sara', 'one', 'one', null),
('Jon', 'two', 'two', null);
;with unpvt_cte([Name], item, val, rn) as (
select f.[Name], v.*, row_number() over (partition by [Name] order by (select null))
from #FoodTable f
cross apply (values ('Cake', Cake),
('Coca', Coca),
('IceCream', [ice-cream])) v(item, val)
where v.val is not null)
select [Name], string_agg(concat(rn, '.', val), ' ') within group (order by rn) answer
from unpvt_cte
group by [Name];
Name answer
Jon 1.two 2.two
Sam 1.one 2.five 3.six
Sara 1.one 2.one
to make the query dynamic
declare #food_list nvarchar(max);
select #food_list=string_agg(quotename(concat_ws(',', quotename(sysc.[name], ''''),
quotename(sysc.[name], '[]')), '()'), ',')
from tempdb.sys.columns sysc
where object_id = Object_id('tempdb..#FoodTable')
and [name]<>'Name';
declare
#sql_prefix nvarchar(max)=N'
;with unpvt_cte([Name], item, val, rn) as (
select f.[Name], v.*, row_number() over (partition by [Name] order by (select null))
from #FoodTable f
cross apply (values ',
#sql_suffix nvarchar(max)=N'
) v(item, val)
where v.val is not null)
select [Name], string_agg(concat(rn, ''.'', val), '' '') within group (order by rn) answer
from unpvt_cte
group by [Name];';
declare
#sql nvarchar(max)=concat(#sql_prefix, #food_list, #sql_suffix);
print(#sql);
exec sp_executesql #sql;
The print statement outputs the following
;with unpvt_cte([Name], item, val, rn) as (
select f.[Name], v.*, row_number() over (partition by [Name] order by (select null))
from #FoodTable f
cross apply (values ('Cake',[Cake]),('Coca',[Coca]),('ice-cream',[ice-cream])
) v(item, val)
where v.val is not null)
select [Name], string_agg(concat(rn, '.', val), ' ') within group (order by rn) answer
from unpvt_cte
group by [Name];
You may use a union to acquire the numbers for each column. Here I've used a cte but you could have used a subquery. Each query in the union renames the food type column to food and adds a column num that will be used in the final query. In the final query the where clause filters NULL foods and a group by with the string_agg and concat is used to retrieve the data in the desired format. I've included a working fiddle below:
WITH FoodTableNums AS (
SELECT Name, Cake as food, 1 as num FROM FoodTable UNION ALL
SELECT Name, Coca as food, 2 as num FROM FoodTable UNION ALL
SELECT Name, icecream as food, 3 as num FROM FoodTable
)
SELECT
Name,
STRING_AGG(CONCAT(num,'.', food),',' ) WITHIN GROUP( ORDER BY num asc) as Description
FROM
FoodTableNums
WHERE
food IS NOT NULL
GROUP BY
Name
Name
Description
John
1.two,2.two
Sam
1.one,2.five,3.six
Sara
1.one,2.one
db<>fiddle here
Let me know if this works for you.
Here is an option that is a bit more dynamic. You only have to Exclude certain columns ... in this case NAME
We use a bit of JSON to dynamically UNPIVOT the row, and then string_agg() to consolidate.
Example or dbFiddle
Select A.Name
,B.NewValue
From YourTable A
Cross Apply (
Select NewValue=STRING_AGG(concat(Seq,'.',Value),' ') within group (order by Seq)
From (
Select [Key]
,[Value]
,[Seq] = row_number() over (order by ##spid)
From OpenJson( (Select A.* For JSON Path,Without_Array_Wrapper ) )
Where [Key] not in ('Name')
) B1
) B
Results
Name NewValue
Same 1.one 2.five 3.six
Sara 1.one 2.one
John 1.two 2.two
Here is s generic way regardless of how many columns in a table.
It is based on XML/XQuery.
No need to UNPIVOT the rows, and then STRING_AGG() to consolidate.
All data in each row stays in a row.
SQL
-- DDL and data population, start
DECLARE #tbl table (
[Name] varchar(100) not NULL PRIMARY KEY,
Cake varchar(100) null,
Coca varchar(100) null,
[ice-cream] varchar(100) null);
INSERT #tbl VALUES
('Sam', 'one', 'five', 'six'),
('Sara', 'one', 'one', null),
('Jon', 'two', 'two', null);
-- DDL and data population, end
SELECT p.[Name]
, x.query('
for $r in /root/*[local-name()!="Name"]/text()
let $pos := count(root/*[. << $r]) - 1
return concat(string($pos), ".", $r)').value('text()[1]', 'VARCHAR(MAX)') AS Result
FROM #tbl AS p
CROSS APPLY (SELECT * FROM #tbl AS c
WHERE c.[Name] = p.[Name]
FOR XML PATH(''), TYPE, ROOT('root')) AS t(x);
Output
+------+--------------------+
| Name | Result |
+------+--------------------+
| Jon | 1.two 2.two |
| Sam | 1.one 2.five 3.six |
| Sara | 1.one 2.one |
+------+--------------------+
Although I agree with those saying that it would be much better to normalise your tables, but if you can't do that then this proposal makes GGordon's solution dynamic, building an SQL statement that retrieves all the columns on your FoodTable. No matter if they are 3 or 100 food columns.
CREATE TABLE FoodTable (
Name VARCHAR(4),
Cake VARCHAR(3),
Coca VARCHAR(4),
icecream VARCHAR(4)
);
INSERT INTO FoodTable ("Name", "Cake", "Coca", "icecream")
VALUES ('Sam', 'one', 'five', 'six'),
('Sara', 'one', 'one', NULL),
('John', 'two', 'two', NULL);
declare #SQL nvarchar(max);
WITH Food As (
SELECT ORDINAL_POSITION - 1 AS Num, COLUMN_NAME AS Food
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = N'FoodTable' AND COLUMN_NAME <> 'Name'
)
SELECT #SQL = N'WITH FoodTableNums AS ( ' +
string_agg('SELECT Name, ' + Food + ' as Food, ' + convert(varchar(20), Num) + ' as Num FROM FoodTable', ' UNION ALL ') +
') SELECT Name, STRING_AGG(CONCAT(num,''.'', food),'','' ) WITHIN GROUP( ORDER BY num asc) as Description FROM FoodTableNums WHERE food IS NOT NULL GROUP BY Name'
FROM Food;
EXECUTE sp_ExecuteSQL #SQL;
You can see it working here : Fiddle

Adding 'and' at the end of a comma separated list

Currently, I'm using the stuff function to create a comma separated list per each row.
x,y,z
What I want is to add commas for n-1 items in the list, with the final item being preceded by 'and'
x,y, and z.
For these purposes, just checking row number won't work because this list is being generated per unique Id, therefore I can't just iterate to the end of the table. Code below:
SELECT DISTINCT (sw.OwnerID)
,stuff((
SELECT DISTINCT ', ' + e.pn
FROM fct.enrtablev e
WHERE sw.OwnerID = e.OwnerId
FOR XML PATH('')), 1, 1, '') AS [Pet(s)]
A bit of a hack... AND string_agg() would be a better fit if 2017+
Here we use test the row_number() of the item count sum(1) over(), when equal this is the last item in the list
Example
Declare #YourTable table (OwnerID int,pn varchar(50))
Insert Into #YourTable values
(1,'X')
,(1,'Y')
,(1,'Z')
,(1,'Z')
,(2,'Apples')
Select Distinct
OwnerID
,stuff( ( Select case when row_number() over(order by pn) = nullif(sum(1) over() ,1)
then ', and '
else ', '
end + pn
FROM (Select distinct pn
From #YourTable
Where OwnerID = A.OwnerId
) e
Order By PN
For XML Path('')), 1, 2, '') AS [Pet(s)]
From #YourTable A
Returns
OwnerID Pet(s)
1 X, Y, and Z
2 Apples
XQUery and XML data model is based on ordered sequences. Exactly what we need.
Here is a simple solution based on XQuery and its FLWOR expression.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (OwnerID int, pn VARCHAR(50));
INSERT INTO #tbl (OwnerID, pn) VALUES
(1,'X'),
(1,'Y'),
(1,'Z'),
(2,'Apples');
-- DDL and sample data population, end
SELECT p.OwnerID
, (SELECT *
FROM #tbl AS c
WHERE c.OwnerID = p.OwnerID
FOR XML PATH('r'), TYPE, ROOT('root')
).query('
for $x in /root/r/pn/text()
return if ($x is (/root/r[last()]/pn/text())[1]) then
if (count(/root/r) gt 1) then concat("and ", $x) else string($x)
else concat($x, ",")
').value('.', 'VARCHAR(MAX)') AS Result
FROM #tbl AS p
GROUP BY p.OwnerID;
Output
+---------+----------------+
| OwnerID | Result |
+---------+----------------+
| 1 | X, Y, and Z |
| 2 | Apples |
+---------+----------------+
You can achieve this using ORDER BY and count.
Declare #YourTable table (OwnerID int,pn varchar(50))
Insert Into #YourTable values
(1,'X')
,(1,'Y')
,(1,'Z')
,(2,'Apples')
;WITH CTE_OwnerIdRank as
(
SELECT ownerid, pn, row_number() over (order by ownerId) as totalrn,
count(*) over(partition by ownerid order by ownerid) as ownercnt
from #yourtable
)
SELECT distinct OwnerId,
stuff((
SELECT ', ' + CASE WHEN c.totalrn = c.ownercnt then CONCAT(' and ',c.pn) else c.pn end
FROM CTE_OwnerIdRank as c
WHERE c.OwnerID = o.OwnerId
order by c.totalrn
FOR XML PATH('')), 1, 1, '') AS [Pet(s)]
from #yourtable as o
OwnerId
Pet(s)
1
X, Y, and Z
2
Apples

How to split multiple strings and insert SQL Server FN_SplitStr

I have 2 strings and one integer:
#categoryID int = 163,
#Ids nvarchar(2000) = '1,2,3',
#Names nvarchar(2000) = 'Bob,Joe,Alex'
I need to select 3 columns 3 rows; The most accomplished is 3 rows 2 columns:
select #categoryID,items from FN_SplitStr(#Ids,',')
resulting:
163,1
163,2
163,3
But I can't figure out how to split both strings.
I tried many ways like:
select #categoryID,items from FN_SplitStr((#Ids,#Names),',')
select #categoryID,items from FN_SplitStr(#Ids,','),items from FN_SplitStr(#Names,',')
EXPECTED OUTPUT:
163,1,Bob
163,2,Joe
163,3,Alex
NOTE1: I looked over tens of questions the most similar is:
How to split string and insert values into table in SQL Server AND SQL Server : split multiple strings into one row each but this question is different.
NOTE2: FN_SplitStr is a function for spliting strings in SQL. And I'm trying to create a stored procedure.
Based on your expected output, you have to use cross apply twice and then create some sort of ranking to make sure that you are getting the right value. As IDs and Names don't seem to have any relationship cross apply will create multiple rows (when you split the string to Names and ID)
There might be better way but this also gives your expected output. You can change this string split to your local function.
1st Dense rank is to make sure that we get three unique names and 2nd dense rank is the rank within the name based on order by with ID and outside of the sub query you have to do some comparison to get only 3 rows.
Declare #categoryID int = 163,
#Ids nvarchar(2000) = '1,2,3',
#Names nvarchar(2000) = 'Bob,Joe,Alex'
select ConcatenatedValue, CategoryID, IDs, Names from (
select concat(#categoryID,',',a.value,',',b.value) ConcatenatedValue, #categoryID CategoryID,
A.value as IDs, b.value as Names , DENSE_RANK() over (order by b.value) as Rn,
DENSE_RANK() over (partition by b.value order by a.value) as Ranked
from string_split(#IDs,',') a
cross apply string_split(#names,',') B ) t
where Rn - Ranked = 0
Output:
Inside your stored procedure do a string split of #Ids and insert into #temp1 table with an identity(1,1) column rowed. You will get:
163,1,1
163,2,2
163,3,3
Then do the second string split of #Names and insert into #temp2 table with an identity(1,1) column rowed. You will get:
Bob,1
Joe,2
Alex,3
You can then do an inner join with #temp1 and #temp2 on #temp1.rowid = #temp2.rowid and get:
163,1,Bob
163,2,Joe
163,3,Alex
I hope this solves your problem.
You can do this with a recursive CTE:
with cte as (
select #categoryId as categoryId,
convert(varchar(max), left(#ids, charindex(',', #ids + ',') - 1)) as id,
convert(varchar(max), left(#names, charindex(',', #names + ',') - 1)) as name,
convert(varchar(max), stuff(#ids, 1, charindex(',', #ids + ','), '')) as rest_ids,
convert(varchar(max), stuff(#names, 1, charindex(',', #names + ','), '')) as rest_names
union all
select categoryId,
convert(varchar(max), left(rest_ids, charindex(',', rest_ids + ',') - 1)) as id,
convert(varchar(max), left(rest_names, charindex(',', rest_names + ',') - 1)) as name,
convert(varchar(max), stuff(rest_ids, 1, charindex(',', rest_ids + ','), '')) as rest_ids,
convert(varchar(max), stuff(rest_names, 1, charindex(',', rest_names + ','), '')) as rest_names
from cte
where rest_ids <> ''
)
select categoryid, id, name
from cte;
Here is a db<>fiddle.
You need to split CSV value with record number. For that you need to use ROW_NUMBER() function to generate record wise unique ID as column like "RID", while you split CSV columns in row.
You can use table value split function or XML as used below.
Please check this let us know your solution is found or not.
DECLARE
#categoryID int = 163,
#Ids nvarchar(2000) = '1,2,3',
#Names nvarchar(2000) = 'Bob,Joe,Alex'
SELECT
#categoryID AS categoryID,
q.Id,
w.Names
FROM
(
SELECT
ROW_NUMBER() OVER (ORDER BY f.value('.','VARCHAR(10)')) AS RID,
f.value('.','VARCHAR(10)') AS Id
FROM
(
SELECT
CAST('<a>' + REPLACE(#Ids,',','</a><a>') + '</a>' AS XML) AS idXML
) x
CROSS APPLY x.idXML.nodes('a') AS e(f)
) q
INNER JOIN
(
SELECT
ROW_NUMBER() OVER (ORDER BY h.value('.','VARCHAR(10)')) AS RID,
h.value('.','VARCHAR(10)') AS Names
FROM
(
SELECT
CAST('<a>' + REPLACE(#Names,',','</a><a>') + '</a>' AS XML) AS namesXML
) y
CROSS APPLY y.namesXML.nodes('a') AS g(h)
) w ON w.RID = q.RID

Replace multiple characters in SQL

I have a problem where I want to replace characters
I am using replace function but that is not giving desired output.
Values of column table_value needs to replaced with their fill names like
E - Email
P - Phone
M - Meeting
I am using this query
select table_value,
replace(replace(replace(table_value, 'M', 'MEETING'), 'E', 'EMAIL'), 'P', 'PHONE') required_value
from foobar
so second required_value row should be EMAIL,PHONE,MEETING and so on.
What should I do so that required value is correct?
The below will work (even it's not a smart solution).
select
table_value,
replace(replace(replace(replace(table_value, 'M', 'MXXTING'), 'E', 'XMAIL'), 'P', 'PHONX'), 'X', 'E') required_value
from foobar
You can do it using CTE to split the table values into E, P and M, then replace and put back together.
I assumed each record has a unique identifer Id but please replace that with whatever you have.
;WITH cte
AS
(
SELECT Id, SUBSTRING(table_value, 1, 1) AS SingleValue, 1 AS ValueIndex
FROM replacetable
UNION ALL
SELECT replacetable.Id, SUBSTRING(replacetable.table_value, cte.ValueIndex + 1, 1) AS SingleValue, cte.ValueIndex + 1 AS ValueIndex
FROM cte
INNER JOIN replacetable ON cte.ValueIndex < LEN(replacetable.table_value)
)
SELECT DISTINCT Id,
STUFF((SELECT DISTINCT ','+ CASE SingleValue
WHEN 'E' THEN 'EMAIL'
WHEN 'P' THEN 'PHONE'
WHEN 'M' THEN 'MEETING'
END
FROM cte c
WHERE c.Id = cte.Id
AND SingleValue <> ','
FOR XML PATH ('')),1,1,'')
FROM cte
Sorry , for mess code, maybe this is not best way to solve this, but what I've tried:
SET QUOTED_IDENTIFIER ON
SET ANSI_NULLS ON
GO
CREATE Function [dbo].[fn_CSVToTable]
(
#CSVList Varchar(max)
)
RETURNS #Table TABLE (ColumnData VARCHAR(100))
AS
BEGIN
DECLARE #S varchar(max),
#Split char(1),
#X xml
SELECT #Split = ','
SELECT #X = CONVERT(xml,' <root> <s>' + REPLACE(#CSVList,#Split,'</s> <s>') + '</s> </root> ')
INSERT INTO #Table
SELECT CASE RTRIM(LTRIM(T.c.value('.','varchar(20)'))) WHEN 'M' THEN 'Meeting'
WHEN 'P' THEN 'Phone'
WHEN 'E' THEN 'Email'
End
FROM #X.nodes('/root/s') T(c)
RETURN
END
GO
Then When I run this:
Select Main.table_value,
Left(Main.ColumnData,Len(Main.ColumnData)-1) As ColumnData
From
(
Select distinct tt2.table_value,
(
Select tt1.ColumnData+ ',' AS [text()]
From (
SELECT
*
FROM dbo.TestTable tt
CROSS APPLY dbo.fn_CSVToTable(tt.table_value)
) tt1
Where tt1.table_value = tt2.TABLE_value
ORDER BY tt1.table_value
For XML PATH ('')
) ColumnData
From dbo.TestTable tt2
) [Main]
I get this:
table_value ColumnData
-------------------------------------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
E,P Email,Phone
E,P,M Email,Phone,Meeting
P,E Phone,Email
P,M Phone,Meeting
(4 row(s) affected)
You could also use a DECODE or CASE to translate the values.

Displaying the difference between rows in the same table in SQL SERVER

I'm using SQL Server 2005.
I have a table that has an archive of rows each time some field was changed. I have to produce a report that displays fields that were changed for each employee.
My table schema:
tblEmp(empid, name, salary, createddate)
My table data:
Row 1: 1, peter, 1000, 11/4/2012
Row 2: 1, peter, 2000, 11/5/2012
Row 3: 1, pete, 2000, 11/6/2012
Row 4: 1, peter, 4000, 11/7/2012
Based on the above data for employee Peter (employee id 1), the output (changes) would be:
resultset:
1, oldsalary: 1000 newsalary: 2000 (changed on 11/5/2012)
1, oldname: peter newname: pete (changed on 11/6/2012)
1, oldname: pete newname: peter, oldsalary:2000, newsalary: 4000 (changed on 11/7/2012)
I'm trying to come up with the sql that would produce the above resultset.
I've tried to do something similar to the first answer in this thread: How to get difference between two rows for a column field?
However, it's not coming together, so wondering if anyone could help.
You are looking at the difference column by column. This suggests using unpivot. The following creates output with each change in a column, along with the previous value and date:
DECLARE #t TABLE(empid INT,name SYSNAME,salary INT,createddate DATE);
INSERT #t SELECT 1, 'peter', 1000, '20121104'
UNION ALL SELECT 1, 'peter', 2000, '20121105'
UNION ALL SELECT 1, 'pete', 2000, '20121106'
UNION ALL SELECT 1, 'peter', 4000, '20121107';
with cv as (
select empid, createddate, col, val
from (select empid, CAST(name as varchar(8000)) as name,
CAST(salary as varchar(8000)) as salary, createddate
from #t
) t
unpivot (val for col in (name, salary)) as unpvt
),
cvr as (
select cv.*,
ROW_NUMBER() over (partition by empid, col order by createddate) as seqnum_all
from (select cv.*, ROW_NUMBER() over (partition by empid, col, thegroup order by createddate) as seqnum_group
from (select cv.*,
(ROW_NUMBER() over (partition by empid, col order by createddate) -
ROW_NUMBER() over (partition by empid, col, val order by createddate)
) as thegroup
from cv
) cv
) cv
where seqnum_group = 1
) -- select * from cvr
select cvr.*, cvrprev.val as preval, cvrprev.createddate as prevdate
from cvr left outer join
cvr cvrprev
on cvr.empid = cvrprev.empid and
cvr.col = cvrprev.col and
cvr.seqnum_all = cvrprev.seqnum_all + 1
Perhaps these joined CTE's with ROW_NUMBER + CASE:
WITH cte AS
(
SELECT empid,
name,
salary,
rn=ROW_NUMBER()OVER(PARTITION BY empid ORDER BY createddate)
FROM tblemp
)
SELECT oldname=CASE WHEN c1.Name=c2.Name THEN '' ELSE C1.Name END,
newname=CASE WHEN c1.Name=c2.Name THEN '' ELSE C2.Name END,
oldsalary=CASE WHEN c1.salary=c2.salary THEN NULL ELSE C1.salary END,
newsalary=CASE WHEN c1.salary=c2.salary THEN NULL ELSE C2.salary END
FROM cte c1 INNER JOIN cte c2
ON c1.empid=c2.empid AND c2.RN=c1.RN + 1
Sql-Fiddle Demo
DECLARE #t TABLE(empid INT,name SYSNAME,salary INT,createddate DATE);
INSERT #t SELECT 1, 'peter', 1000, '20121104'
UNION ALL SELECT 1, 'peter', 2000, '20121105'
UNION ALL SELECT 1, 'pete', 2000, '20121106'
UNION ALL SELECT 1, 'peter', 4000, '20121107';
;WITH x AS
(
SELECT empid, name, salary, createddate, rn = ROW_NUMBER() OVER
(PARTITION BY empid ORDER BY createddate)
FROM #t
-- WHERE empid = 1 -- for example
)
SELECT LTRIM(
CASE WHEN x.salary <> y.salary THEN
'oldsalary: ' + RTRIM(x.salary)
+ ' newsalary: ' + RTRIM(y.salary)
ELSE '' END
+ CASE WHEN x.name <> y.name THEN
' oldname: ' + x.name
+ ' newname: ' + y.name
ELSE '' END
+ ' (changed on ' + CONVERT(CHAR(10), y.createddate, 101) + ')')
FROM x INNER JOIN x AS y
ON x.rn = y.rn - 1
AND x.empid = y.empid
AND
(
x.salary <> y.salary
OR x.name <> y.name
);
Unless you have a where clause to target a specific empid, however, the output is not very useful unless it also includes empid. SQLfiddle demo
Based on what you explain, It would be easier to create a Trigger when this table is Changed and then create the table with the result you expect, Since you have in that moment the old values and the New values, there should be not a problem to come up with the result you expect.