Pivot table with more then one record - sql

I have data that is structured as follow:
LocationId, GroupId, DayOfWeek, Count, DatetimeValue15Min
2 9 4 5 2014-01-02 08:15:00.000
2 9 4 5 2014-01-02 09:15:00.000
I want to calculate the mode for each day, the data above already contains the count to know the mode. I have written a query with a pivot.
SELECT
pvt.LocationId, pvt.GroupId, [1], [2], [3], [4],[5]
FROM
#TempResult
PIVOT
(min ([DatetimeValue15Min])
FOR DayOfWeek IN ( [1], [2], [3], [4],[5])) AS pvt
In this case I have two modes but i want to show them both. My query returns in this case just the mode with the minimum value. I know that I can make a second query with the max value but what if i have more than two modes?
The output should be like:
LocationId GroupId 1 2 3 4 5
2 9 08:15, 09:15
I am using SQL Server 2005.

You are almost there. You just need to build the comma-separated list. A little xml type abuse works really well for that.
;WITH
t1 AS ( --Add a grouping id for quick reference
SELECT
[LocationId],[GroupId],[DayOfWeek],[DatetimeValue15Min],
DENSE_RANK() OVER(ORDER BY [LocationId],[GroupId],[DayOfWeek]) [i]
FROM #TempResult
),
t2 AS ( --Build a comma-separated list of all [DatetimeValue15Min] with same grouping id
SELECT [LocationId],[GroupId],[DayOfWeek],
CAST(REPLACE((SELECT CONVERT(time, [DatetimeValue15Min]) AS a FROM t1 WHERE [i] = t.[i] FOR xml PATH('')),'</a><a>',',') AS xml).value('a[1]','varchar(max)') [dtv_list]
FROM t1 t
)
SELECT pvt.LocationId, pvt.GroupId, [1], [2], [3], [4],[5]
FROM t2
PIVOT
(
min ([dtv_list])
FOR DayOfWeek IN ( [1], [2], [3], [4],[5])
) AS pvt
The xml trick works like this:
SELECT [DatetimeValue15Min] FOR XML -> <a>08:15</a><a>09:15</a>
replace '</a><a>' with ',' -> <a>08:15,09:15</a>
extract first node from xml -> '08:15,09:15'

SELECT LocationId, GroupId
, STUFF (
MAX (CASE WHEN DayOfWeek = 1 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 1 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [1]
, STUFF (
MAX (CASE WHEN DayOfWeek = 2 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 2 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [2]
, STUFF (
MAX (CASE WHEN DayOfWeek = 3 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 3 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [3]
, STUFF (
MAX (CASE WHEN DayOfWeek = 4 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 4 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [4]
, STUFF (
MAX (CASE WHEN DayOfWeek = 5 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 5 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [5]
FROM (
SELECT LocationId, GroupId, DayOfWeek, ', ' + CONVERT (VARCHAR(5), DatetimeValue15Min, 8) AS Value
, ROW_NUMBER() OVER(PARTITION BY LocationId, GroupId, DayOfWeek ORDER BY DatetimeValue15Min) AS num
FROM #TempResult
) T
GROUP BY LocationId, GroupId
UPD. Solution for unlimited number of values per day.
WITH cte AS (
SELECT LocationId, GroupId, DayOfWeek, CONVERT (VARCHAR(5), DatetimeValue15Min, 8) AS Value
, ROW_NUMBER() OVER(PARTITION BY LocationId, GroupId, DayOfWeek ORDER BY DatetimeValue15Min) AS num
FROM #TempResult
)
SELECT LocationId, GroupId
, MIN (CASE DayOfWeek WHEN 1 THEN Value END) AS [1]
, MIN (CASE DayOfWeek WHEN 2 THEN Value END) AS [2]
, MIN (CASE DayOfWeek WHEN 3 THEN Value END) AS [3]
, MIN (CASE DayOfWeek WHEN 4 THEN Value END) AS [4]
, MIN (CASE DayOfWeek WHEN 5 THEN Value END) AS [5]
FROM (
SELECT DISTINCT c1.LocationId, c1.GroupId, c1.DayOfWeek
, STUFF ( (
SELECT ', ' + c2.Value
FROM cte c2
WHERE c1.LocationId = c2.LocationId AND c1.GroupId = c2.GroupId AND c1.DayOfWeek = c2.DayOfWeek
FOR XML PATH ('')
), 1, 2, '') AS Value
FROM cte c1
) t
GROUP BY LocationId, GroupId

Related

Indicate a row that cause an abnormal case (SQL)

I have a result as below using the following script:
SELECT
id, (2022 - age) yearId, age, [value],
CASE
WHEN LAG([value], 1, 0) OVER (PARTITION BY id ORDER BY [age]) = 0
THEN 'Base'
WHEN [value] > LAG([value], 1, -1) OVER (PARTITION BY id ORDER BY [age])
THEN 'Increasing'
WHEN [value] = LAG([value], 1, -1) OVER (PARTITION BY id ORDER BY [age])
THEN 'No Change'
ELSE 'Decreasing'
END AS [Order]
FROM Test
Values
And I manage to get a group of ids with an id causing a "flip: decreasing and then increasing or the other way around" as:
Abnormal Case
Now I want to print out the same result as above but with a column indicates the row that cause the flip, something like this (the row causes the flip should be place at the top of each partition):
Id
age
value
flip
1
4
3
1
1
0
5
0
1
1
4
0
1
2
3
0
1
3
2
0
1
5
3
0
1
6
4
0
Thank you!
Expanding your existing logic to get the previous order value then conditionally ordering
with cte as
(
SELECT
id, (2022 - age) yearId, age, [value],
CASE
WHEN LAG([value], 1, 0) OVER (PARTITION BY id ORDER BY [age]) = 0
THEN 'Base'
WHEN [value] > LAG([value], 1, -1) OVER (PARTITION BY id ORDER BY [age])
THEN 'Increasing'
WHEN [value] = LAG([value], 1, -1) OVER (PARTITION BY id ORDER BY [age])
THEN 'No Change'
ELSE 'Decreasing'
END AS [Order]
FROM T1
) ,
cte1 as
(select cte.*,concat(cte.[order], lag([order]) over (partition by id order by age)) concatlag
from cte)
select * ,
case when concatlag in('IncreasingDecreasing','DecreasingIncreasing') then 1 else 0 end
from cte1
order by
case when concatlag in('IncreasingDecreasing','DecreasingIncreasing') then 1 else 0 end desc,
age

Convert rows to columns with values based on ranking

I have a table of customers who have several kinds of "accounts", each with a different integer balance, such as
CUSTID AcctType Balance
12345 Checking 1000
12345 Savings 5000
12345 Investment 3000
98765 Savings 2000
98765 Checking 8000
98765 Investment 1000
98765 Retirement 2500
I do not know how many accounts a customer may have (it could be anywhere from 1 to 6). I have to create a result showing the Accounts and Balances as columns, in order from highest to lowest, like this:
CUSTID AcctType1 Balance1 AcctType2 Balance2 AcctType3 Balance3 AcctType4 Balance 4
12345 Savings 5000 Investment 3000 Checking 1000
98765 Checking 8000 Retirement 2500 Savings 2000 Investment 1000
Any ideas how to create this in SQL Server? (Ideally as a View)
You can unpivot the columns first into rows, then pivot the rows back into column with row number, something like this:
WITH CTE
AS
(
SELECT
CAST(CUSTID AS NVARCHAR(50)) AS CUSTID
,CAST(AcctType AS NVARCHAR(50)) AS AcctType
,CAST(Balance AS NVARCHAR(50)) AS Balance
,ROW_NUMBER() OVER(PARTITION BY [CUSTID] ORDER BY Balance DESC) AS RN
FROM Data
), unpivoted
AS
(
SELECT CUSTID, val, col + ' ' + CAST(RN AS NVARCHAR(50)) AS col
FROM CTE
UNPIVOT
(
val
FOR col IN(AcctType, Balance)
) AS u
)
SELECT *
FROM unpivoted AS u
PIVOT
(
MAX(val)
FOR col IN([AcctType 1], [Balance 1],
[AcctType 2], [Balance 2],
[AcctType 3], [Balance 3],
[AcctType 4], [Balance 4])
) AS p;
SQL Fiddle Demo
Update:
If you want to make this dynamic for any number of customers, you have to do it dynamically like this:
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
WITH CTE
AS
(
SELECT
CAST(CUSTID AS NVARCHAR(50)) AS CUSTID
,CAST(AcctType AS NVARCHAR(50)) AS AcctType
,CAST(Balance AS NVARCHAR(50)) AS Balance
,ROW_NUMBER() OVER(PARTITION BY [CUSTID] ORDER BY Balance DESC) AS RN
FROM Data
), Data
AS
(
SELECT col, MAX(RN) AS RN
FROM
(
SELECT RN, col + CAST(RN AS NVARCHAR(50)) AS col
FROM CTE
UNPIVOT
(
val
FOR col IN(AcctType, Balance)
) AS u
) AS t
GROUP BY col
)
select #cols = STUFF((SELECT ',' +
QUOTENAME(col)
FROM Data
ORDER BY RN
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
, 1, 1, '');
SELECT #query = 'WITH CTE
AS
(
SELECT
CAST(CUSTID AS NVARCHAR(50)) AS CUSTID
,CAST(AcctType AS NVARCHAR(50)) AS AcctType
,CAST(Balance AS NVARCHAR(50)) AS Balance
,ROW_NUMBER() OVER(PARTITION BY [CUSTID] ORDER BY Balance DESC) AS RN
FROM Data
), unpivoted
AS
(
SELECT CUSTID, val, col + CAST(RN AS NVARCHAR(50)) AS col
FROM CTE
UNPIVOT
(
val
FOR col IN(AcctType, Balance)
) AS u
)
SELECT *
FROM unpivoted AS u
PIVOT
(
MAX(val)
FOR col IN('+ #cols + ')
) AS p;';
EXECUTE(#query);
Dynamic Demo
If it's a limited amount of columns(as you said 6 per CUSTID) then you can use conditional aggregation with ROW_NUMBER():
SELECT t.custID,
MAX(CASE WHEN t.rnk = 1 THEN t.accttype END) as accttype1,
MAX(CASE WHEN t.rnk = 1 THEN t.balance END) as balance1,
MAX(CASE WHEN t.rnk = 2 THEN t.accttype END) as accttype2,
MAX(CASE WHEN t.rnk = 2 THEN t.balance END) as balance2,
MAX(CASE WHEN t.rnk = 3 THEN t.accttype END) as accttype3,
MAX(CASE WHEN t.rnk = 3 THEN t.balance END) as balance3,
MAX(CASE WHEN t.rnk = 4 THEN t.accttype END) as accttype4,
MAX(CASE WHEN t.rnk = 4 THEN t.balance END) as balance4,
MAX(CASE WHEN t.rnk = 5 THEN t.accttype END) as accttype5,
MAX(CASE WHEN t.rnk = 5 THEN t.balance END) as balance5,
MAX(CASE WHEN t.rnk = 6 THEN t.accttype END) as accttype6,
MAX(CASE WHEN t.rnk = 6 THEN t.balance END) as balance6
FROM (SELECT s.*,
ROW_NUMBER() OVER(PARTITION BY s.custID ORDER BY s.balance DESC) as rnk
FROM YourTable s) t
GROUP BY t.custID

Set Value With NULL When Pivot SQL Selection Return 0 Rows

How to set NULL value when you select data using pivot return 0 rows?
;WITH cte AS (
SELECT
DATENAME(month, RPT.DateID) as Month,
ISNULL(SUM(RPT.TransactionIn), 0) as ATransactionIn,
ISNULL(SUM(RPT.TransactionOut), 0) as BTransactionOut,
ISNULL(SUM(RPT.OutstandingTransaction), 0) as COutstandingTransaction
FROM RPT_SummaryPOApproval RPT
WHERE RPT.Deleted = 0 --AND RPT.DivisionCode = 'asd'
GROUP BY DATENAME(month, RPT.DateID)
), pivoted
as
(
SELECT *
FROM (
SELECT [Month], [Transactions], [Values]
FROM (
SELECT *
FROM cte
) as p
UNPIVOT (
[Values] FOR [Transactions] IN (ATransactionIn, BTransactionOut, COutstandingTransaction )
) as unpvt
) as k
PIVOT (
MAX([Values]) FOR [Month] IN ([January],[February],[March],[April],[May],[June],[July],[August],[September],[October],[November],[December])
) as pvt
)
SELECT * FROM pivoted
ORDER BY [Transactions] ASC
Those code will result something like this:
Transaction January February March .... Dec
ATransactionIn 12 0 0 0
BTransactionOut 10 0 0 0
COutstandingTransaction 2 0 0 0
When I uncomment Filter by DivisionCode (on the first code)
WHERE RPT.Deleted = 0 AND RPT.DivisionCode = 'asd'
The result become like this
Transaction January February March .... Dec
How can I show the result like this?
Transaction January February March .... Dec
ATransactionIn 0 0 0 0
BTransactionOut 0 0 0 0
COutstandingTransaction 0 0 0 0
You can use conditional aggregation for the pivot:
WITH cte AS (
SELECT DATENAME(month, RPT.DateID) as Month,
SUM(CASE WHEN RPT.DivisionCode = 'asd' THEN RPT.TransactionIn ELSE 0 END) as ATransactionIn,
SUM(CASE WHEN RPT.DivisionCode = 'asd' THEN RPT.TransactionOut ELSE 0 END) as BTransactionOut,
SUM(CASE WHEN RPT.DivisionCode = 'asd' THEN RPT.OutstandingTransaction ELSE 0 END) as COutstandingTransaction
FROM RPT_SummaryPOApproval RPT
WHERE RPT.Deleted = 0
GROUP BY DATENAME(month, RPT.DateID)
), . . .
The rest of the query should be the same.
I solved my problem by using union to show 0 when there's no data to display.
;WITH cte AS (
SELECT
DATENAME(month, RPT.DateID) as Month,
ISNULL(SUM(RPT.TransactionIn), 0) as ATransactionIn,
ISNULL(SUM(RPT.TransactionOut), 0) as BTransactionOut,
ISNULL(SUM(RPT.OutstandingTransaction), 0) as COutstandingTransaction
FROM RPT_SummaryPOApproval RPT
WHERE RPT.Deleted = 0 --AND RPT.DivisionCode = 'asd'
GROUP BY DATENAME(month, RPT.DateID)
UNION ALL
SELECT '0', '0', '0', '0'
), pivoted
as
(
SELECT *
FROM (
SELECT [Month], [Transactions], [Values]
FROM (
SELECT *
FROM cte
) as p
UNPIVOT (
[Values] FOR [Transactions] IN (ATransactionIn, BTransactionOut, COutstandingTransaction )
) as unpvt
) as k
PIVOT (
MAX([Values]) FOR [Month] IN ([January],[February],[March],[April],[May],[June],[July],[August],[September],[October],[November],[December])
) as pvt
)
SELECT * FROM pivoted
ORDER BY [Transactions] ASC

How to use SQL Server 2005 Pivot based on lookup table

table [Status] has the following data:
ID Status
1 PaymentPending
2 Pending
3 Paid
4 Cancelled
5 Error
====================================
Data Table has the following structure:
ID WeekNumber StatusID
1 1 1
2 1 2
3 1 3
4 2 1
5 2 2
6 2 2
7 2 3
Looking for a Pivot
Week # PaymentPending Pending Paid Cancelled
Week 1 1 1 1 0
Week 2 1 2 1 0
SELECT 'Week '+CAST(coun.WeekNumber AS VARCHAR(10)) [Week #],[PaymentPending],[Pending],[Paid],[Cancelled],[Error] FROM
(SELECT [WeekNumber],[Status] FROM dbo.WeekDetails
INNER JOIN [dbo].[Status] AS s
ON [dbo].[WeekDetails].[StatusID] = [s].[ID]) AS wee
PIVOT (COUNT(wee.[Status]) FOR wee.[Status]
IN ([PaymentPending],[Pending],[Paid],[Cancelled],[Error])) AS Coun
A pivot might look like this:
SELECT * FROM
(SELECT
'Week ' + CAST(D.WeekNumber AS varchar(2)) [Week #],
S.Status
FROM DataTbl D
INNER JOIN Status S ON D.StatusID = S.ID
) Derived
PIVOT
(
COUNT(Status) FOR Status IN
([PaymentPending], [Pending], [Paid], [Cancelled]) -- add [Error] if needed
) Pvt
If you expect the number of items in theStatustable to change you might want to consider using a dynamic pivot to generate the column headings. Something like this:
DECLARE #sql AS NVARCHAR(MAX)
DECLARE #cols AS NVARCHAR(MAX)
SELECT #cols = ISNULL(#cols + ',','') + QUOTENAME(Status)
FROM (SELECT ID, Status FROM Status) AS Statuses ORDER BY ID
SET #sql =
N'SELECT * FROM
(SELECT ''Week '' + CAST(D.WeekNumber AS varchar(2)) [Week #], S.Status
FROM Datatbl D
INNER JOIN Status S ON D.StatusID = S.ID) Q
PIVOT (
COUNT(Status)
FOR Status IN (' + #cols + ')
) AS Pvt'
EXEC sp_executesql #sql;
Sample SQL Fiddle
You can use CASE based aggregation with GROUP BY
SELECT 'Week ' + cast(WeekNumber as varchar(10)) as 'Week#',
SUM ( CASE WHEN StatusId =1 THEN 1 else 0 end) as 'PaymentPending',
SUM ( CASE WHEN StatusId =2 THEN 1 else 0 end) as 'Pending',
SUM ( CASE WHEN StatusId =3 THEN 1 else 0 end) as 'Paid',
SUM ( CASE WHEN StatusId =4 THEN 1 else 0 end) as 'Cancelled'
FROM DataTbl D
GROUP BY 'Week ' + cast(WeekNumber as varchar(10))

MS SQL - One to Many Relationship - Need to return single row

I have the following tables -
Search Result
----------------
SearchResultID PK
ProductID FK
SearchQuery
WebsiteName
URL
IsFound
CreatedOn
BatchID
Name
SearchResultItem
-----------------
SearchResultItemID PK
SearchResultID FK
Name
Value
These tables have a one to many relationship, so one Search Result, can have many Search Result Items.
I can do an INNER JOIN on these tables however that obviously gives one row per each Search Result Item. Ideally I would like one row per Search Result, for example...
SearchResultID | ProductID | SearchQuery | WebsiteName | URL | IsFound |
CreatedOn | BatchID | Name | SearchResultItemID | Name 1 | Value 1 | Name 2 |
Value 2 | Name 3 | Value 3 |
Is this possible to do? And if so, can someone point me in the right direction as to how I would do this - I think it would be something like this, only in ms-sql - one to many sql select into single row - mysql
You can use the ROW_NUMBER() function to give each search result item a rank within each search result:
SELECT SearchResultItemID,
SearchResultID,
Name,
Value,
RowNumber = ROW_NUMBER() OVER(PARTITION BY SearchResultID ORDER BY SearchresultItemID)
FROM SearchResultItem;
If you have a know number of items then you can use aggregate functions to get each name/value pair:
WITH RankedItem AS
( SELECT SearchResultItemID,
SearchResultID,
Name,
Value,
RowNumber = ROW_NUMBER() OVER(PARTITION BY SearchResultID ORDER BY SearchresultItemID)
FROM SearchResultItem
)
SELECT SearchResultID,
Name1 = MIN(CASE WHEN RowNumber = 1 THEN Name END),
Value1 = MIN(CASE WHEN RowNumber = 1 then Value END),
Name2 = MIN(CASE WHEN RowNumber = 2 THEN Name END),
Value2 = MIN(CASE WHEN RowNumber = 2 then Value END),
Name3 = MIN(CASE WHEN RowNumber = 3 THEN Name END),
Value3 = MIN(CASE WHEN RowNumber = 3 then Value END),
Name4 = MIN(CASE WHEN RowNumber = 4 THEN Name END),
Value5 = MIN(CASE WHEN RowNumber = 4 then Value END)
FROM RankedItem
GROUP BY SearchResultID;
You can then join this back to your Search result table giving a full query:
WITH RankedItem AS
( SELECT SearchResultItemID,
SearchResultID,
Name,
Value,
RowNumber = ROW_NUMBER() OVER(PARTITION BY SearchResultID ORDER BY SearchresultItemID)
FROM SearchResultItem
), Items AS
( SELECT SearchResultID,
Name1 = MIN(CASE WHEN RowNumber = 1 THEN Name END),
Value1 = MIN(CASE WHEN RowNumber = 1 then Value END),
Name2 = MIN(CASE WHEN RowNumber = 2 THEN Name END),
Value2 = MIN(CASE WHEN RowNumber = 2 then Value END),
Name3 = MIN(CASE WHEN RowNumber = 3 THEN Name END),
Value3 = MIN(CASE WHEN RowNumber = 3 then Value END),
Name4 = MIN(CASE WHEN RowNumber = 4 THEN Name END),
Value4 = MIN(CASE WHEN RowNumber = 4 then Value END)
FROM RankedItem
GROUP BY SearchResultID
)
SELECT SearchResult.SearchResultID,
SearchResult.ProductID,
SearchResult.SearchQuery,
SearchResult.WebsiteName,
SearchResult.URL,
SearchResult.IsFound,
SearchResult.CreatedOn,
SearchResult.BatchID,
SearchResult.Name,
Items.Name1,
Items.Value1,
Items.Name2,
Items.Value2,
Items.Name3,
Items.Value3,
Items.Name4,
Items.Value4
FROM SearchResult
INNER JOIN Items
ON SearchResult.SearchResultID = Items.SearchResultID;
Example on SQL Fiddle
If you want to return a variable number of values then you will need to use dynamic SQL:
DECLARE #SQL NVARCHAR(MAX) = '';
SELECT #SQL = #SQL + ',[Name' + rn + '], [Value' + rn + '] '
FROM ( SELECT DISTINCT
rn = CAST(ROW_NUMBER() OVER(PARTITION BY SearchResultID ORDER BY SearchresultItemID) AS VARCHAR)
FROM SearchResultItem
) p;
SET #SQL = 'WITH RankedItem AS
( SELECT SearchResultItemID,
SearchResultID,
Name,
Value,
RowNumber = ROW_NUMBER() OVER(PARTITION BY SearchResultID ORDER BY SearchresultItemID)
FROM SearchResultItem
), UnPivoted AS
( SELECT upvt.SearchResultID,
Name = upvt.n + CAST(RowNumber AS VARCHAR),
upvt.v
FROM RankedItem
UNPIVOT
( n
FOR v IN ([Name], [Value])
) upvt
), Pivoted AS
( SELECT *
FROM UnPivoted
PIVOT
( MAX(V)
FOR Name IN (' + STUFF(#SQL, 1, 1, '') + ')
) pvt
)
SELECT SearchResult.SearchResultID,
SearchResult.ProductID,
SearchResult.SearchQuery,
SearchResult.WebsiteName,
SearchResult.URL,
SearchResult.IsFound,
SearchResult.CreatedOn,
SearchResult.BatchID,
SearchResult.Name' + #SQL + '
FROM SearchResult
INNER JOIN Pivoted
ON SearchResult.SearchResultID = Pivoted.SearchResultID;';
EXECUTE SP_EXECUTESQL #SQL;
Example on SQL Fiddle
N.B. I have intentionally used a different way of doing this in dynamic sql just to show there is more than one way to achieve the result of combining the rows.