Select specific value for all corresponding rows - sql-server

I am trying to get a result set of Top Customers which is ordered by a rank value based on year and total. Easy, right?
BUT, now I want to specify a year, and have ALL rows for that customer return the rank value for that year.
For example, say I have the following data (rank hardcoded for ease):
SELECT * FROM
(
SELECT 'Customer A' as Cust,'123.45' as Total,'2016' as [year],1 as [rank]
UNION
SELECT 'Customer A','123.45','2017',3
UNION
SELECT 'Customer B','46.67','2016',2
UNION
SELECT 'Customer B','423.45','2017',1
UNION
SELECT 'Customer B','123.45','2018',1
UNION
SELECT 'Customer C','23.45','2016',3
UNION
SELECT 'Customer C','223.45','2017',2
UNION
SELECT 'Customer C','23.45','2018',2
) as a
ORDER BY a.[year], a.[rank]
If I specify year 2016, I want to pick the rank value for the year 2016 for each customer, and return that for the customer for each of that customer's rows in the result set - should look like the following:
=>
The closest I can manage is the following, but it just NULLs the other cells:
DECLARE #RankBy VARCHAR(4) = '2016'
SELECT [Year],
Cust,
[Total],
[rank] = (SELECT a.[rank] WHERE Cust = a.Cust AND [Year] = #RankBy)
FROM
(
SELECT 'Customer A' as Cust,'123.45' as Total,'2016' as [year],1 as [rank]
UNION
SELECT 'Customer A','123.45','2017',3
UNION
SELECT 'Customer B','46.67','2016',2
UNION
SELECT 'Customer B','423.45','2017',1
UNION
SELECT 'Customer B','123.45','2018',1
UNION
SELECT 'Customer C','23.45','2016',3
UNION
SELECT 'Customer C','223.45','2017',2
UNION
SELECT 'Customer C','23.45','2018',2
) as a
ORDER BY a.Cust, a.rank
I know I can do this with a temp table and an update statement, but I'm trying to find a way to do it in a single select statement if possible.
(In case anyone is wondering, this is for an SSRS Report, but I don't see that being relevant here.)

Try a Windowed Aggregate over a CASE:
MIN(CASE WHEN [Year] = #RankBy THEN a.[rank] END)
OVER (PARTITION BY Cust)

Related

SSMS Rolling Average over Day of Week

Leadership wants to know how Teammates are performing on Mondays & Fridays in comparison to the rest of the work week. Below is a sample temp dbo of a Teammate X's daily performance over a two-month period. Each subsequent Teammate has a different starting point from whence they are measured. I initially looked at using UNBOUNDED PRECEDING in conjunction with the various start dates, but windows functions are not cooperating. Help!
CREATE TABLE #RollingAverage
(
[Date] DATE PRIMARY KEY
,[Value] INT
);
INSERT INTO #RollingAverage
SELECT '2019-01-02',626
UNION ALL SELECT '2019-01-03',231 UNION ALL SELECT '2019-01-04',572
UNION ALL SELECT '2019-01-07',775 UNION ALL SELECT '2019-01-09',660
UNION ALL SELECT '2019-01-10',662 UNION ALL SELECT '2019-01-11',541
UNION ALL SELECT '2019-01-14',849 UNION ALL SELECT '2019-01-15',632
UNION ALL SELECT '2019-01-16',906 UNION ALL SELECT '2019-01-18',961
UNION ALL SELECT '2019-01-21',501 UNION ALL SELECT '2019-01-24',311
UNION ALL SELECT '2019-01-25',614 UNION ALL SELECT '2019-01-28',296
UNION ALL SELECT '2019-01-29',390 UNION ALL SELECT '2019-01-31',804
UNION ALL SELECT '2019-02-01',928 UNION ALL SELECT '2019-02-05',855
UNION ALL SELECT '2019-02-06',605 UNION ALL SELECT '2019-02-08',283
UNION ALL SELECT '2019-02-12',144 UNION ALL SELECT '2019-02-14',382
UNION ALL SELECT '2019-02-15',862 UNION ALL SELECT '2019-02-18',549
UNION ALL SELECT '2019-02-19',401 UNION ALL SELECT '2019-02-20',515
UNION ALL SELECT '2019-02-21',590 UNION ALL SELECT '2019-02-22',625
UNION ALL SELECT '2019-02-25',304 UNION ALL SELECT '2019-02-26',402
UNION ALL SELECT '2019-02-27',326;
AVG(Value) over (ORDER BY [Date] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) did not work
The first thing you need to understand, is that your "daily" performance is not daily. A simple solution would be to fill the gaps to be able to effectively count the days.
I filled the gaps using a CTE that generates a calendar table on the fly, but you could use a permanent calendar table if available.
WITH
E(n) AS(
SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0))E(n)
),
E2(n) AS(
SELECT a.n FROM E a, E b
),
cteCalendar(calDate) AS(
SELECT TOP (61)
CAST( DATEADD( DD, 1-ROW_NUMBER() OVER(ORDER BY (SELECT NULL)), GETDATE()) AS date) AS calDate
FROM E2
),
cteRollingAverages AS(
SELECT ra.[Date],
ra.value,
AVG(Value) over (ORDER BY calDate ROWS BETWEEN 7 PRECEDING AND CURRENT ROW) RollingAverage
FROM #RollingAverage AS ra
RIGHT JOIN cteCalendar AS c ON ra.[Date] = c.calDate
)
SELECT *
FROM cteRollingAverages
WHERE [Date] IS NOT NULL
ORDER BY [Date];
A different option is to use APPLY. This is not limited by a certain date.
SELECT *
FROM #RollingAverage r
CROSS APPLY( SELECT AVG(i.[Value]) AS RollingAvg
FROM #RollingAverage i
WHERE i.[Date] BETWEEN DATEADD( DD, -7, r.[Date]) AND r.[Date]) av
ORDER BY [Date];

Generating a derived table in SSRS from last purchases in two different SQL Server tables, and then joining with a third

Suppose I have three tables:
one of customer info with a unique ID (CUST_ID)
one of customer pants purchases 3 columns (CUST_ID, PANT_TYPE, PANT_DATE)
one of customer shirts purchases 3 columns (CUST_ID, SHIRT_TYPE, SHIRT_DATE)
Additionally, I wish to exclude certain pant types and certain shirt types (PANT_TYPE IS NOT 'JEANS', SHIRT_TYPE IS NOT 'TUXEDO'), and only grab the most recent pant purchase and shirt purchase.
So, ideally, I would end up with final table like this:
CUST_ID, LAST_PANT_TYPE, LAST_PANT_DATE, LAST_SHIRT_TYPE, LAST_SHIRT_DATE
For four hours at work I tried to get this solution to work, but the 'OVER' statement breaks SSRS and causes problems in testing the query and even drops the fields randomly at times. So, I am thinking derived tables of some sort might work.
I am pretty new to SQL and have learned a lot trying to solve this, but I need to get there STAT.
Using Common Table Expressions for your derived tables, you query could look like this:
WITH
p (cust_id, pant_type, pant_date, rn) AS (
SELECT cust_id, pant_type, pant_date,
ROW_NUMBER() OVER (PARTITION BY cust_id ORDER BY pant_date DESC)
FROM Pants WHERE pant_type NOT IN ('JEANS')
),
s (cust_id, shirt_type, shirt_date, rn) AS (
SELECT cust_id, shirt_type, shirt_date,
ROW_NUMBER() OVER (PARTITION BY cust_id ORDER BY shirt_date DESC)
FROM Shirts WHERE shirt_type NOT IN ('TUXEDO')
)
SELECT c.cust_id,
p.pant_type AS last_pant_type, p.pant_date AS last_pant_date,
s.shirt_type AS last_shirt_type, s.shirt_date AS last_shirt_date
FROM Customers c
LEFT JOIN p ON c.cust_id = p.cust_id AND p.rn = 1
LEFT JOIN s ON c.cust_id = s.cust_id AND s.rn = 1;
You could try using the APPLY operator, e.g.:
SELECT
C.CUST_ID
, PANT.PANT_TYPE LAST_PANT_TYPE
, PANT.PANT_DATE LAST_PANT_DATE
, SHIRT.SHIRT_TYPE LAST_SHIRT_TYPE
, SHIRT.SHIRT_DATE LAST_SHIRT_DATE
FROM
CUSTOMER C
OUTER APPLY
(
SELECT TOP 1
P.PANT_TYPE
, P.PANT_DATE
FROM PANT P
WHERE
P.CUST_ID = C.CUST_ID
AND P.PANT_TYPE <> 'JEANS'
ORDER BY P.PANT_DATE DESC
) PANT
OUTER APPLY
(
SELECT TOP 1
S.SHIRT_TYPE
, S.SHIRT_DATE
FROM SHIRT S
WHERE
S.CUST_ID = C.CUST_ID
AND S.SHIRT_TYPE <> 'TUXEDO'
ORDER BY S.SHIRT_DATE DESC
) SHIRT
Using OUTER APPLY (as opposed to CROSS APPLY) ensures that none of the customers will be filtered out (NULL will be returned in the 'PANT' or 'SHIRT' columns if there is no data for the customer in their respective tables).
I ended up making data and using ROW_NUMBER to identify the last purchase.
SELECT 1 AS CUST_ID
INTO #CUSTOMERS
UNION
SELECT 2
UNION
SELECT 3
UNION
SELECT 4
SELECT 1 AS CUST_ID, 'SLACKS' AS PANT_TYPE, CAST('1/1/2017' AS DATE) AS PANT_DATE
INTO #PANTS
UNION
SELECT 1, 'CARGO SHORTS', CAST('2/2/2018' AS DATE)
UNION
SELECT 1, 'SLACKS', CAST('3/3/2018' AS DATE)
UNION
SELECT 2, 'CARGO PANTS', CAST('2/2/2017' AS DATE)
UNION
SELECT 2, 'SLACKS', CAST('4/4/2018' AS DATE)
UNION
SELECT 3, 'CARGO PANTS', CAST('4/1/2018' AS DATE)
UNION
SELECT 3, 'SLACKS', CAST('5/8/2017' AS DATE)
UNION
SELECT 4, 'CARGO SHORTS', CAST('2/2/2018' AS DATE)
UNION
SELECT 4, 'SLACKS', CAST('3/3/2018' AS DATE)
UNION
SELECT 4, 'CARGO PANTS', CAST('2/2/2017' AS DATE)
UNION
SELECT 4, 'SLACKS', CAST('4/4/2018' AS DATE)
UNION
SELECT 4, 'CARGO PANTS', CAST('4/1/2018' AS DATE)
UNION
SELECT 4, 'SLACKS', CAST('5/8/2017' AS DATE)
SELECT 1 AS CUST_ID, 'POLO' AS SHIRT_TYPE, CAST('1/1/2017' AS DATE) AS SHIRT_DATE
INTO #SHIRTS
UNION
SELECT 1, 'POLO - LONG SLEEVE', CAST('2/2/2018' AS DATE)
UNION
SELECT 1, 'POLO - LONG SLEEVE', CAST('3/3/2018' AS DATE)
UNION
SELECT 2, 'POLO', CAST('2/2/2017' AS DATE)
UNION
SELECT 2, 'POLO - LONG SLEEVE', CAST('4/4/2018' AS DATE)
UNION
SELECT 3, 'T-SHIRT', CAST('4/1/2018' AS DATE)
UNION
SELECT 3, 'POLO', CAST('5/8/2017' AS DATE)
UNION
SELECT 4, 'T-SHIRT', CAST('2/2/2018' AS DATE)
UNION
SELECT 4, 'POLO', CAST('3/3/2018' AS DATE)
UNION
SELECT 4, 'T-SHIRT', CAST('2/2/2017' AS DATE)
UNION
SELECT 4, 'POLO - LONG SLEEVE', CAST('4/4/2018' AS DATE)
UNION
SELECT 4, 'T-SHIRT', CAST('4/1/2018' AS DATE)
UNION
SELECT 4, 'POLO', CAST('5/8/2017' AS DATE)
SELECT C.CUST_ID, P.PANT_TYPE, P.PANT_DATE,
S.SHIRT_TYPE, S.SHIRT_DATE
FROM #CUSTOMERS C
LEFT JOIN (SELECT CUST_ID, PANT_TYPE, PANT_DATE, ROW_NUMBER()OVER(PARTITION BY CUST_ID ORDER BY PANT_DATE DESC) AS ROW_NUM FROM #PANTS) P ON P.CUST_ID = C.CUST_ID AND P.ROW_NUM = 1
LEFT JOIN (SELECT CUST_ID, SHIRT_TYPE, SHIRT_DATE, ROW_NUMBER()OVER(PARTITION BY CUST_ID ORDER BY SHIRT_DATE DESC) AS ROW_NUM FROM #SHIRTS) S ON S.CUST_ID = C.CUST_ID AND S.ROW_NUM = 1

Why not grouping correctly when calculating the moving average in SQL Server

I have the following code which is used to calculate the 12-month moving average. I want to calculate the 12month moving average per ACNBR. When only a single ACNBR is used the code works fine, when I try to calculate the moving average for more than one ACNBR it doesn't work anymore. Please assist. Thank you.
-Sample data code:
CREATE TABLE #RollingTotalsExample
(
[Date] DATE
,[Value] INT
,[ACNBR] INT
,[CIS] INT
);
INSERT INTO #RollingTotalsExample
SELECT '2011-01-01',626,100,12
UNION ALL SELECT '2011-02-01',231,100,12 UNION ALL SELECT '2011-03-01',572,100,12
UNION ALL SELECT '2011-04-01',775,100,12 UNION ALL SELECT '2011-05-01',660,100,12
UNION ALL SELECT '2011-06-01',662,100,12 UNION ALL SELECT '2011-07-01',541,100,12
UNION ALL SELECT '2011-08-01',849,100,12 UNION ALL SELECT '2011-09-01',632,100,12
UNION ALL SELECT '2011-10-01',906,100,12 UNION ALL SELECT '2011-11-01',961,100,12
UNION ALL SELECT '2011-12-01',361,100,12 UNION ALL SELECT '2012-01-01',461,100,12
UNION ALL SELECT '2012-02-01',928,100,12 UNION ALL SELECT '2012-03-01',855,100,12
UNION ALL SELECT '2012-04-01',605,100,12 UNION ALL SELECT '2012-05-01',83,100,12
UNION ALL SELECT '2012-06-01',44,100,12 UNION ALL SELECT '2012-07-01',382,100,12
UNION ALL SELECT '2012-08-01',862,100,12 UNION ALL SELECT '2012-09-01',549,100,12
UNION ALL SELECT '2012-10-01',632,100,12 UNION ALL SELECT '2012-11-01',2,100,12
UNION ALL SELECT '2012-12-01',26,100,12
UNION ALL SELECT '2011-01-01',626,200,12
UNION ALL SELECT '2011-02-01',231,200,12 UNION ALL SELECT '2011-03-01',572,200,12
UNION ALL SELECT '2011-04-01',775,200,12 UNION ALL SELECT '2011-05-01',660,200,12
UNION ALL SELECT '2011-06-01',662,200,12 UNION ALL SELECT '2011-07-01',541,200,12
UNION ALL SELECT '2011-08-01',849,200,12 UNION ALL SELECT '2011-09-01',632,200,12
UNION ALL SELECT '2011-10-01',906,200,12 UNION ALL SELECT '2011-11-01',961,200,12
UNION ALL SELECT '2011-12-01',361,200,12 UNION ALL SELECT '2012-01-01',461,200,12
UNION ALL SELECT '2012-02-01',928,200,12 UNION ALL SELECT '2012-03-01',855,200,12
UNION ALL SELECT '2012-04-01',605,200,12 UNION ALL SELECT '2012-05-01',83,200,12
UNION ALL SELECT '2012-06-01',44,200,12 UNION ALL SELECT '2012-07-01',382,200,12
UNION ALL SELECT '2012-08-01',862,200,12 UNION ALL SELECT '2012-09-01',549,200,12
UNION ALL SELECT '2012-10-01',632,200,12 UNION ALL SELECT '2012-11-01',2,200,12
UNION ALL SELECT '2012-12-01',26,200,12;
-code
SELECT a.[Date]
,a.ACNBR
,Value=MAX(CASE WHEN a.[Date] = b.[Date] THEN a.Value END)
,Rolling12Months=CASE
WHEN ROW_NUMBER() OVER (ORDER BY a.[Date]) < (12)
THEN NULL
ELSE avg(b.Value)
END
FROM #RollingTotalsExample a
JOIN #RollingTotalsExample b ON b.[Date] BETWEEN DATEADD(month, -11, a.[Date]) AND a.[Date]
GROUP BY a.ACNBR,a.[Date]
ORDER BY a.ACNBR,a.[Date]
Are you looking output like this?
select *, sum(value) over(partition by acnbr order by date rows between 11 preceding and current row) from #RollingTotalsExample
Else post the expected output

Trying to pivot event dates in t-sql without using a cursor

I have the following table:
What I want is to get to this:
EventTypeId 1 and 3 are valid start events and EventTypeId of 2 is the only valid end event.
I have tried to do a pivot, but I don't believe a pivot will get me the multiple events for a person in the result set.
SELECT PersonId, [1],[3],[2]
FROM
(
SELECT PersonId, EventTypeId, EventDate
from #PersonEvent
) as SourceTable
PIVOT
(
count(EventDate) FOR EventTypeId
IN ([1],[3],[2])
) as PivotTable
Select PersonID,
Min(Case WHEN EventTypeId IN (1,3) THEN EventDate END) as StartDate,
Min(Case WHEN EventTypeId IN (2) THEN EventDate END) as EndDate
FROM #PersonEvent
group by personid
I can do a cursor, but my original table is over 90,000 rows, and this is to be for a report, so I don't think I can use that option. Any other thoughts that I might be missing?
Assuming the table is called [dbo].[PersonEventRecords] this will work...
With StartEvents As
(
Select *
From [dbo].[PersonEventRecords]
Where EventTypeId In (1,3)
), EndEvents As
(
Select *
From [dbo].[PersonEventRecords]
Where EventTypeId In (2)
)
Select IsNull(se.PersonId,ee.PersonId) As PersonId,
se.EventTypeId As StartEventTypeId,
se.EventDate As StartEventDate,
ee.EventTypeId As EndEventTypeId,
ee.EventDate As EndEventDate
From StartEvents se
Full Outer Join EndEvents ee
On se.PersonId = ee.PersonId
And se.EventSequence = ee.EventSequence - 1
Order By IsNull(se.PersonId,ee.PersonId),
IsNull(se.EventDate,ee.EventDate);
/**** TEST DATA ****/
If Object_ID('[dbo].[PersonEventRecords]') Is Not Null
Drop Table [dbo].[PersonEventRecords];
Create Table [dbo].[PersonEventRecords]
(
PersonId Int,
EventTypeId Int,
EventDate Date,
EventSequence Int
);
Insert [dbo].[PersonEventRecords]
Select 1,1,'2012-10-13',1
Union All
Select 1,2,'2012-10-20',2
Union All
Select 1,1,'2012-11-01',3
Union All
Select 1,2,'2012-11-13',4
Union All
Select 2,1,'2012-05-07',1
Union All
Select 2,2,'2012-06-01',2
Union All
Select 2,3,'2012-07-01',3
Union All
Select 2,2,'2012-08-30',4
Union All
Select 3,2,'2012-04-05',1
Union All
Select 3,1,'2012-05-04',2
Union All
Select 3,2,'2012-05-24',3
Union All
Select 4,1,'2013-01-03',1
Union All
Select 4,1,'2013-02-20',2
Union All
Select 4,2,'2013-03-20',3;
Try this
SELECT E1.PersonId, E1.EventTypeId, E1.EventDate, E2.EventTypeId, E2.EventDate
FROM PersonEvent AS E1
OUTER APPLY(
SELECT TOP 1 PersonEvent.EventTypeId, PersonEvent.EventDate
FROM PersonEvent
WHERE PersonEvent.PersonId = E1.PersonId
AND PersonEvent.EventSequence = E1.EventSequence + 1
AND PersonEvent.EventTypeId = 2
) AS E2
WHERE E1.EventTypeId = 1 OR E1.EventTypeId = 3
UNION
SELECT E3.PersonId, NULL, NULL, E3.EventTypeId, E3.EventDate
FROM PersonEvent E3
WHERE E3.EventTypeId = 2
AND NOT EXISTS(
SELECT *
FROM PersonEvent
WHERE PersonEvent.PersonId = E3.PersonId
AND PersonEvent.EventSequence = E3.EventSequence - 1)
It is not completely clear how do you want the result to be ordered – add order as needed.

Count New Response and Cumulative Percent Per Month

If I have several customers responding month after month, I want to count them only in the first month they responded. I can achieve this by creating temp tables for each month and comparing month over month, but it looks ugly with several temp tables. I'm pretty sure that there's a better way to do this (I don't know if Rank() will work). Can someone show me code please?
declare #Something table
(
CustID Char(10),
MthId char(2),
ResponseDate datetime
)
insert #Something
select 'Cust1', '1', '5/6/13' union all
select 'Cust1', '2', '6/13/13' union all
select 'Cust1', '3', '7/13/13' union all
select 'Cust2', '1', '5/20/13' union all
select 'Cust2', '2', '6/22/13' union all
select 'Cust3', '2', '6/20/13' union all
select 'Cust4', '2', '6/24/13' union all
select 'Cust4', '3', '7/24/13' union all
select 'Cust5', '4', '8/28/13' union all
select 'Cust6', '3', '7/24/13'
This is the output I'm expecting in 3 columns (I don't really need the 2nd col - it's there to explain further)
Month, How many "cumulative" new customers responded that month, What's the cumulative percent of total customers contacted new every month.
MthId NewCustomerResponse CumulativeNewCustomerResponse Cumulative%Responded
1 2 2 33.3
2 2 4 66.7
3 1 5 83.3
4 1 6 100.0
Including your new column:
SQLFiddle to the solution
;with cte as(
select
ROW_NUMBER() over (partition by CustID order by responseDate) as seq
,* from #Something
)
,cte2 as(
select
MthId
,(select Count(*) from cte t2 where t1.MthId=t2.MthId and t2.seq=1) NewCustomerResponse
,(select COUNT(*) from cte t2 where t1.MthId>=t2.MthId and t2.seq=1) CumulativeNewCustomerResponse
,(select COUNT(CustID) from cte where seq=1) total
from cte t1
group by MthId
)
select
MthID
,NewCustomerResponse
,CumulativeNewCustomerResponse
,(cast(CumulativeNewCustomerResponse as decimal(3,1))/CAST(total as decimal(3,1)))*100
from cte2 t1
check this,
declare #Something table
(
CustID Char(10),
MthId char(2),
ResponseDate datetime
)
insert into #Something
select 'Cust1', '1', '5/6/13'
union all
select 'Cust1', '2', '6/13/13'
union all
select 'Cust1', '3', '7/13/13'
union all
select 'Cust2', '1', '5/20/13'
union all
select 'Cust2', '2', '6/22/13'
union all
select 'Cust3', '2', '6/20/13' union all
select 'Cust4', '2', '6/24/13' union all
select 'Cust4', '3', '7/24/13' union all
select 'Cust5', '4', '8/28/13' union all
select 'Cust6', '3', '7/24/13'
;with CTE as
(select *,dense_rank()over(partition by custid order by mthid)rn from #Something)
,CTE1 as
(select a.MthId,count(*) NewCustomerResponse from cte a where rn=1 group by a.MthId )
,cte2 as
(select sum(NewCustomerResponse) totalresponse from cte1)
,cte4 as
(
select a.MthId
,(Select sum(NewCustomerResponse) from CTE1 c where c.mthid<=a.mthid) CumulativeNewCustomerResponse
from cte1 a cross apply cte2 b
)
select a.MthId,a.NewCustomerResponse
,(Select sum(NewCustomerResponse)from CTE1 c where c.mthid<=a.mthid)CumulativeNewCustomerResponse
,case when b.totalresponse>0 then cast((d.CumulativeNewCustomerResponse /cast(b.totalresponse as float))*100 as decimal(10,2)) else 0 end [Cumulative%Responded]
from cte1 a
inner join cte4 d on a.MthId=d.MthId
cross apply cte2 b

Resources