Dynamically converting multiple row values to columns with composite key - sql

I have a following data with 2 Unique Identifiers (ID1, ID2), and multiple values that spread across various times. I am trying to pivot it dynamically, because one combination of ID1 & ID2 do not have a fixed number of Values and Times. Any help is appreciated! Thanks!
DECLARE #DataSource TABLE
(
ID1 INT
,ID2 INT
,Val INT
,[Time] Datetime
)
INSERT INTO #DataSource (ID1, ID2, Val, [Time])
VALUES (1,1,10,'01/01/2021 12:00')
,(1,1,20,'01/01/2021 15:00')
,(1,2,30,'01/02/2021 17:00')
,(1,2,35,'01/02/2021 18:00')
,(2,1,40,'02/02/2021 08:00')
,(2,2,50,'02/02/2021 10:00')
,(2,2,60,'05/01/2021 11:00')
SELECT *
FROM #DataSource
I am trying to pivot it so it looks something like this and Value and Time are next to each other in sequence.

You can group by ROW_NUMBER divided by 2, this gets you every two rows in a group
SELECT
ID1,
ID2,
Val1 = MIN(CASE WHEN rn % 2 = 1 THEN Val END),
Time1 = MIN(CASE WHEN rn % 2 = 1 THEN [Time] END),
Val2 = MIN(CASE WHEN rn % 2 = 0 THEN Val END),
Time2 = MIN(CASE WHEN rn % 2 = 0 THEN [Time] END)
FROM (
SELECT *,
rn = ROW_NUMBER() OVER (PARTITION BY ID1, ID2 ORDER BY [Time])
FROM YourTable t
) t
GROUP BY
t.ID1,
t.ID2,
(t.rn - 1) / 2

Related

Previous value and current value

I would like to pivot the following data such that I can have a previous column and a current column based on the values in column rn. If there is only 1 record then the current and the previous values will be the same value.
(I am using ssms 2008)
CREATE TABLE #TEST1
(ACCT_ID INT, RN INT, LoadDate Date)
INSERT INtO #TEST1 VALUES (1, 1, '2016-12-21')
INSERT INtO #TEST1 VALUES (2, 1, NULL)
INSERT INtO #TEST1 VALUES (3, 1, '2017-10-06')
INSERT INtO #TEST1 VALUES (3, 2, NULL)
INSERT INtO #TEST1 VALUES (4, 1, '2016-12-21')
SELECT * FROM #TEST1
ACCT_ID RN LoadDate
1 1 2016-12-21
2 1 NULL
3 1 2017-10-06
3 2 NULL
4 1 2016-12-21
Based on the data above, I did a pivot table
SELECT ACCT_ID, [1] as Prev, [2] as Curr FROM
(
SELECT * fROM #TEST1 S
pivot( MAX(LoadDate) for RN IN ([1],[2]) ) U
)X
How can for example acct_id 1 and 4 have the same value in current as the previous value.
I guess you need something like this. It displays the Curr value if the Prev is empty.
select acct_id, curr, case when prev = ' ' then null else coalesce(prev, curr) end
from
(
SELECT acct_id,
max(CASE WHEN RN = 1 THEN loaddate END) as Curr,
max(CASE WHEN RN = 2 THEN coalesce(loaddate, ' ') END) as Prev
FROM #test1 t
GROUP BY acct_id
) t
demo
I'm not sure if pivot will work in this case, I apologize if you specifically require an answer using pivot.
Here I'm taking the MAX(RN) via subquery, then doing select statements for the previous and current LoadDate. The current date is the date provided by the MAX(RN). The previous date is the date of MAX(RN) - 1 if MAX(RN) is greater than 1, else it is the previous date where RN equals 1.
SELECT dT.ACCT_ID
,(SELECT T1.LoadDate FROM #TEST1 T1 WHERE T1.ACCT_ID = dT.ACCT_ID AND T1.RN = CASE WHEN dT.MaxRN = 1 THEN 1 ELSE dT.MaxRN - 1 END) [Prev]
,(SELECT T1.LoadDate FROM #TEST1 T1 WHERE T1.ACCT_ID = dT.ACCT_ID AND T1.RN = dT.MaxRN) [Curr]
FROM (
SELECT ACCT_ID
,MAX(RN) [MaxRN]
FROM #TEST1
GROUP BY ACCT_ID
) AS dT
I would use conditional aggregation:
select acct_id,
max(case when rn = 1 then loaddate end) as load_date,
max(case when rn = 2 then loaddate end) as load_date_prev
from #test1 t
group by acct_id;

Selecting unique records from a table and giving weight to repetitions

So I need to select a bunch of document IDs and a Value from a table and then give weight for each of them depending on the Value. It works as follows:
Say the table has the values:
DocID Value
1 1
1 1
2 1
2 2
The select will select each unique DocID and then give it a weight value. A Document with the same value repeated twice will have less weight than a document with 2 different values. So output will be something like this:
DocID Weight
1 1.5
2 2
So as you see, since Document 1 has the value 1 repeated twice it will have the initial +1 to weight from first value and then +0.5 since it repeated once (will get +0.5 again if value repeats more than that). Then Document 2 has the weight of 2 since the value 1 appeared once (+1) and value 2 appeared once (+2)
Any help will be highly appreciated.
First, aggregate the data to get a count per document/value. Then, if I understand the logic correctly, the rest is just an addition aggregation:
select docid,
sum(case when cnt > 1 then 1 + 0.5 * (cnt - 1) else 0 end)
from (select docid, value, count(*) as cnt
from t
group by docid, value
) dv
group by docid;
Actually, I think the outer query could be simplified to:
select docid,
0.5 + 0.5 * sum(cnt)
And, the whole thing could be expressed with count(distinct):
select docid,
(0.5 * count(distinct value) +
0.5 * count(*)
)
from t
group by docid;
Try this,
declare #t table(DocID int, Value int)
insert into #t values
(1, 1)
,(1, 1)
,(2, 1)
,(2, 2)
;
WITH CTE
AS (
SELECT docid
,value
,ROW_NUMBER() OVER (
PARTITION BY docid ORDER BY docid
) rownum
FROM #t
)
,CTE1
AS (
SELECT docid
,value
,rownum
,1.00 wtg
FROM cte
WHERE rownum = 1
UNION ALL
SELECT a.docid
,b.value
,b.rownum + 1
,CASE
WHEN a.Value = b.value
THEN 0.50
ELSE 1
END
FROM CTE A
INNER JOIN cte1 B ON a.docid = b.DocID
AND A.rownum > b.rownum
)
SELECT docid
,SUM(wtg) wtg
FROM cte1
GROUP BY docid

convert row to column using Pivot without any clause

I have a table like below.
I need to get the data like below.
I have created two temp tables and achieved the result like this. Please help me to do the same with PIVOT.
At least I wouldn't use pivot for that, to my mind this is simpler to do with group by and row_number:
select UserId, max(starttime) as starttime, max(endtime) as endtime
from (
select UserId,
case when StartOrEnd = 'S' then time end as starttime,
case when StartOrEnd = 'E' then time end as endtime,
row_number() over (partition by UserID order by time asc)
+ case when StartOrEnd = 'S' then 1 else 0 end as GRP
from table1
) X
group by UserId, GRP
order by starttime
The derived table splits the time into start / end time columns (to handle cases where only one exists) and uses a trick with row number to group the S / E items together. The outer select just groups the rows into the same row.
Example in SQL Fiddle
Not a efficient solution as JamesZ but should work
create table #tst (userid int,start_end char(1),times datetime)
insert #tst values
(1,'S','07-27-2015 16:45'),
(1,'E','07-27-2015 16:46'),
(2,'S','07-27-2015 16:47'),
(2,'E','07-27-2015 16:48'),
(1,'S','07-27-2015 16:49'),
(1,'E','07-27-2015 16:50')
WITH cte
AS (SELECT Row_number()OVER(ORDER BY times) rn,*
FROM #tst),
cte1
AS (SELECT a.userid,
a.start_end,
a.times,
CASE WHEN a.userid = b.userid THEN 0 ELSE 1 END AS com,
a.rn
FROM cte a
LEFT OUTER JOIN cte b
ON a.rn = b.rn + 1),
cte2
AS (SELECT userid,
start_end,
times,
(SELECT Sum(com)
FROM cte1 b
WHERE b.rn <= a.rn) AS row_num
FROM cte1 a)
SELECT USERID,
starttime=Min(CASE WHEN start_end = 's' THEN times END),
endtime=Max(CASE WHEN start_end = 'e' THEN times END)
FROM cte2
GROUP BY USERID,
row_num
Here is another method
declare #t table(userid int, StartOrEnd char(1), time datetime)
insert into #t
select 1,'S','2015-07-27 16:45' union all
select 1,'E','2015-07-27 16:46' union all
select 2,'S','2015-07-27 16:47' union all
select 2,'E','2015-07-27 16:48' union all
select 1,'S','2015-07-27 16:49' union all
select 1,'E','2015-07-27 16:50'
select userid,min(time) as minimum_time, max(time) as maximum_time from
(
select *, row_number() over (partition by cast(UserID as varchar(10))
+StartOrEnd order by time asc) as sno
from #t
) as t
group by userid,sno
Result
userid minimum_time maximum_time
----------- ----------------------- -----------------------
1 2015-07-27 16:45:00.000 2015-07-27 16:46:00.000
2 2015-07-27 16:47:00.000 2015-07-27 16:48:00.000
1 2015-07-27 16:49:00.000 2015-07-27 16:50:00.000

SQL group by if values are close

Class| Value
-------------
A | 1
A | 2
A | 3
A | 10
B | 1
I am not sure whether it is practical to achieve this using SQL.
If the difference of values are less than 5 (or x), then group the rows (of course with the same Class)
Expected result
Class| ValueMin | ValueMax
---------------------------
A | 1 | 3
A | 10 | 10
B | 1 | 1
For fixed intervals, we can easily use "GROUP BY". But now the grouping is based on nearby row's value. So if the values are consecutive or very close, they will be "chained together".
Thank you very much
Assuming MSSQL
You are trying to group things by gaps between values. The easiest way to do this is to use the lag() function to find the gaps:
select class, min(value) as minvalue, max(value) as maxvalue
from (select class, value,
sum(IsNewGroup) over (partition by class order by value) as GroupId
from (select class, value,
(case when lag(value) over (partition by class order by value) > value - 5
then 0 else 1
end) as IsNewGroup
from t
) t
) t
group by class, groupid;
Note that this assumes SQL Server 2012 for the use of lag() and cumulative sum.
Update:
*This answer is incorrect*
Assuming the table you gave is called sd_test, the following query will give you the output you are expecting
In short, we need a way to find what was the value on the previous row. This is determined using a join on row ids. Then create a group to see if the difference is less than 5. and then it is just regular 'Group By'.
If your version of SQL Server supports windowing functions with partitioning the code would be much more readable.
SELECT
A.CLASS
,MIN(A.VALUE) AS MIN_VALUE
,MAX(A.VALUE) AS MAX_VALUE
FROM
(SELECT
ROW_NUMBER()OVER(PARTITION BY CLASS ORDER BY VALUE) AS ROW_ID
,CLASS
,VALUE
FROM SD_TEST) AS A
LEFT JOIN
(SELECT
ROW_NUMBER()OVER(PARTITION BY CLASS ORDER BY VALUE) AS ROW_ID
,CLASS
,VALUE
FROM SD_TEST) AS B
ON A.CLASS = B.CLASS AND A.ROW_ID=B.ROW_ID+1
GROUP BY A.CLASS,CASE WHEN ABS(COALESCE(B.VALUE,0)-A.VALUE)<5 THEN 1 ELSE 0 END
ORDER BY A.CLASS,cASE WHEN ABS(COALESCE(B.VALUE,0)-A.VALUE)<5 THEN 1 ELSE 0 END DESC
ps: I think the above is ANSI compliant. So should run in most SQL variants. Someone can correct me if it is not.
These give the correct result, using the fact that you must have the same number of group starts as ends and that they will both be in ascending order.
if object_id('tempdb..#temp') is not null drop table #temp
create table #temp (class char(1),Value int);
insert into #temp values ('A',1);
insert into #temp values ('A',2);
insert into #temp values ('A',3);
insert into #temp values ('A',10);
insert into #temp values ('A',13);
insert into #temp values ('A',14);
insert into #temp values ('b',7);
insert into #temp values ('b',8);
insert into #temp values ('b',9);
insert into #temp values ('b',12);
insert into #temp values ('b',22);
insert into #temp values ('b',26);
insert into #temp values ('b',67);
Method 1 Using CTE and row offsets
with cte as
(select distinct class,value,ROW_NUMBER() over ( partition by class order by value ) as R from #temp),
cte2 as
(
select
c1.class
,c1.value
,c2.R as PreviousRec
,c3.r as NextRec
from
cte c1
left join cte c2 on (c1.class = c2.class and c1.R= c2.R+1 and c1.Value < c2.value + 5)
left join cte c3 on (c1.class = c3.class and c1.R= c3.R-1 and c1.Value > c3.value - 5)
)
select
Starts.Class
,Starts.Value as StartValue
,Ends.Value as EndValue
from
(
select
class
,value
,row_number() over ( partition by class order by value ) as GroupNumber
from cte2
where PreviousRec is null) as Starts join
(
select
class
,value
,row_number() over ( partition by class order by value ) as GroupNumber
from cte2
where NextRec is null) as Ends on starts.class=ends.class and starts.GroupNumber = ends.GroupNumber
** Method 2 Inline views using not exists **
select
Starts.Class
,Starts.Value as StartValue
,Ends.Value as EndValue
from
(
select class,Value ,row_number() over ( partition by class order by value ) as GroupNumber
from
(select distinct class,value from #temp) as T
where not exists (select 1 from #temp where class=t.class and Value < t.Value and Value > t.Value -5 )
) Starts join
(
select class,Value ,row_number() over ( partition by class order by value ) as GroupNumber
from
(select distinct class,value from #temp) as T
where not exists (select 1 from #temp where class=t.class and Value > t.Value and Value < t.Value +5 )
) ends on starts.class=ends.class and starts.GroupNumber = ends.GroupNumber
In both methods I use a select distinct to begin because if you have a dulpicate entry at a group start or end things go awry without it.
Here is one way of getting the information you are after:
SELECT Under5.Class,
(
SELECT MIN(m2.Value)
FROM MyTable AS m2
WHERE m2.Value < 5
AND m2.Class = Under5.Class
) AS ValueMin,
(
SELECT MAX(m3.Value)
FROM MyTable AS m3
WHERE m3.Value < 5
AND m3.Class = Under5.Class
) AS ValueMax
FROM
(
SELECT DISTINCT m1.Class
FROM MyTable AS m1
WHERE m1.Value < 5
) AS Under5
UNION
SELECT Over4.Class,
(
SELECT MIN(m4.Value)
FROM MyTable AS m4
WHERE m4.Value >= 5
AND m4.Class = Over4.Class
) AS ValueMin,
(
SELECT Max(m5.Value)
FROM MyTable AS m5
WHERE m5.Value >= 5
AND m5.Class = Over4.Class
) AS ValueMax
FROM
(
SELECT DISTINCT m6.Class
FROM MyTable AS m6
WHERE m6.Value >= 5
) AS Over4

row convert to column in sql 2008

I want to convert a series of rows into a series of columns
create table #cusphone(cusid int,cusph1 int)
insert into #cusphone values(1,48509)
insert into #cusphone values(1,48508)
insert into #cusphone values(1,48507)
insert into #cusphone values(2,48100)
so that the output is like this
1 48509 48508 48507
2 48100 null null
You can use the same approach of rank() and then use the new PIVOT function as follows:
with cusCte as(
select cusid,cusph1,RANK() over (partition by cusid order by cusph1) r
from #cusphone)
SELECT cusid, [1] AS C1, [2] AS C2, [3] AS C3
FROM
(SELECT cusid,cusph1,r
FROM cusCte) p
PIVOT
(
MIN (cusph1)
FOR r IN
( [1], [2], [3] )
) AS pvt;
You did not specify the rules by which something should appear in the first column vs the second column so I guessed that this is based on the occurrence (and thus sorting) of the cusph1 value.
With RankedItems As
(
Select cusid, cusph1
, ROW_NUMBER() OVER( PARTITION BY cusid ORDER BY cusph1 DESC) As Num
From #cusphone
)
Select cusid
, Min(Case When Num = 1 Then cusph1 End) As Col1
, Min(Case When Num = 2 Then cusph1 End) As Col2
, Min(Case When Num = 3 Then cusph1 End) As Col3
From RankedItems
Group By cusid