I am trying to use multiple aggregations in the PIVOT function in SQL Server. Currently I have used SUM which is working as expected, I wanted to use AVG also along with SUM. I saw some sample queries but they were too long. Can anyone help me to modify the below query to include AVG of SubTotal for each PaymentMethod and each year.
SELECT PaymentMethod, [2016], [2015]
FROM (SELECT PaymentMethod, SubTotal, YEAR(OrderDate) AS [YEAR]
FROM [TransactionDetails]) AS sq
PIVOT(SUM(SubTotal) FOR [YEAR]
IN([2016], [2015])) AS Pvt;
I can't think of any way of doing this besides running two completely separate PIVOTs based, each time, on the original source data.
So I've moved the subselect into a CTE to avoid repeating its definition and then I'm joining the two pivoted results base on the PaymentMethod:
declare #t table (PaymentMethod varchar(30) not null, SubTotal int not null,
OrderDate date not null)
insert into #t(PaymentMethod,SubTotal,OrderDate) values
('Cash',10,'20150101'), ('Cash',20,'20151231'),
('Cash',30,'20160101'), ('Cash',90,'20161231')
;With ToBePivoted as (
SELECT PaymentMethod, SubTotal, YEAR(OrderDate) AS [YEAR]
FROM #t
)
SELECT s.PaymentMethod, s.[2016],a.[2016], s.[2015], a.[2015]
FROM
ToBePivoted tbp1
PIVOT(SUM(SubTotal) FOR [YEAR] IN([2016], [2015])) AS s
inner join
ToBePivoted tbp2
pivot(AVG(SubTotal) FOR [YEAR] IN([2016],[2015])) AS a
ON
s.PaymentMethod = a.PaymentMethod
Result:
PaymentMethod 2016 2016 2015 2015
------------------------------ ----------- ----------- ----------- -----------
Cash 120 60 30 15
The problem here is that a) We want to use different aggregates (not allowed within a single PIVOT) and b) that a PIVOT "uses up" the columns mentioned in the first part of the PIVOT. This means that after the first PIVOT, there aren't any columns containing the years any more to allow the second pivot to be directly applied.
Related
This is a fairly common issue that I've seen a lot of people duck tape together solutions for, but it's never quite right. Hoping this forum can get it ironed out. I have a table:
create table temp
( PatientID varchar(12),
AdmitDate datetime,
DischargeDate datetime
)
insert into temp values ('Patient1','1/30/2020 13:23:44', '2/2/2020 15:12:52')
What I'd like to count is the number of midnights the patient was admitted in the correct month. So in the example above the patient would be admitted at midnight on 1/31, 2/1, and 2/2 dates. So my output in sql should look something like:
01-2020 02-2020
------- --------
1 2
I know it has to be dynamic SQL, because the columns need to be created with respect to the date range queried. Although, I'm pretty stumped as to next steps.
create table #temp
( PatientID varchar(12),
AdmitDate datetime,
DischargeDate datetime
)
insert into #temp values ('Patient1','1/30/2020 13:23:44', '2/2/2020 15:12:52')
--Virtually creates a dates table
;with dates(thedate) as (
select dateadd(yy,years.number,0)+days.number
from master..spt_values years
join master..spt_values days
on days.type='P' and days.number < datepart(dy,dateadd(yy,years.number+1,0)-1)
where years.type='P' and years.number between 100 and 150
-- note: 100-150 creates dates in the year range 2000-2050
-- adjust as required
)
select dateadd(m,datediff(m, 0, d.thedate),0) [Month], count(1) PatientDays
from dates d
join #temp t on d.thedate between t.[AdmitDate] and t.[DischargeDate]
group by datediff(m, 0, d.thedate)
order by [Month];
I'm working on this query. When I assign a date filter from subquery to main query, the response time increases from 1 second to 4.5 minutes.
I don't know how to solve this problem and fix my query. I'm writing the query and the methods I've tried.
Thank you for your help.
My query:
select
START_DATE as DATE,
[MINUTE] as MIN,
map1.LT,
ISNULL((SELECT
(SELECT CAST((main.MIN) AS FLOAT)) /
(
(nullif(
(select cast(
(select
sum(MIN2)
from fooTable2 d2
CROSS APPLY (select Top(1) LT from FooMap2 where x = d2.x) k2
where k2.LT = map1.LT
**-- PROBLEM CODE START**
and YEAR(d2.DATE) = YEAR(main.DATE) and MONTH(d2.DATE) = MONTH(main.DATE)
**-- PROBLEM CODE END**
) as float)),0))
as XX,
.......
......
from Table1 main
OUTER APPLY (select Top(1) LT from FooMap where x = main.x) map1
I tried creating a virtual table.
But not working.
declare #child table ([Year] smallint, [Month] smallint, [Total] float,[LTCode] nvarchar(20))
insert into #child ([LTCode],[Year],[Month],[Total])
(select
k2.LT,YEAR(d2.DATE) as YIL,MONTH(d2.DATE) as AY,sum(MIN) as SURE
from DURUS d2
CROSS APPLY (select Top(1) LT from FooMap2 where x = d2.x) k2
group by k2.LT,YEAR(d2.DATE),MONTH(d2.DATE))
...
....
(select [Total] from #child where [YEAR] = YEAR(main.DATE) and [MONTH] = MONTH(main.DATE) and [LTCode] = map1.LT)
What should I do ?
The root problem is the data model. You need to filter on month and year but store your data as a DATE, DATETIME or similar. There is no easy way to make this fast:
and YEAR(d2.DATE) = YEAR(main.DATE)
and MONTH(d2.DATE) = MONTH(main.DATE)
WHERE FUNCTION(Input) = FUNCTION(Input) forces a scan against each table, having two such filters means you are touching/evaluating each value (d2.date and main.date) twice for each row in each table. To fix this your best options include:
Adding a persisted computed column on each table for year and month then add the appropriate index (on year, month with all columns involved in your query added as Include columns.
Use an indexed view to pre-join Durus and main, not simple but doable.
Learn how to create and utilize a correctly indexed calendar table. This will require some effort but will also change your career.
Work other filters on the Left side of your joins...
For example: add a WHERE clause after from fooTable2 d2 to filter out any additional rows before the join.
Common ways to optimize:
1) remove calculations from the left in filters
2) render subqueries in separate temporary tables or in intermediate CTE queries
3) do not use table variables
4) in the early stages try to filter as much data as possible before connections
I need to analyse customer payment history to establish if they could be deemed as "New", "Current", "Lapsed" or "Deep Lapsed" based on the time lapsed in months between purchases.
I've figured out an approach (below) that seems to work by self joining a cte that has a count of payments for each customer which then calculating the difference in months between each transaction.
But is there a more elegant way to achieve this (using SQL Server 2012)?
WITH CTE
AS (
SELECT [customerID]
, [transaction_date]
, [payment_count]
FROM [Financial])
SELECT [cur].[customerID]
, CASE
WHEN ABS(DATEDIFF([mm], [cur].[transaction_date], [prv].[transaction_date])) IS NULL
THEN 'New'
WHEN ABS(DATEDIFF([mm], [cur].[transaction_date], [prv].[transaction_date])) BETWEEN 0 AND 24
THEN 'Current'
WHEN ABS(DATEDIFF([mm], [cur].[transaction_date], [prv].[transaction_date])) BETWEEN 24 AND 36
THEN 'Lapsed'
WHEN ABS(DATEDIFF([mm], [cur].[transaction_date], [prv].[transaction_date])) > 36
THEN 'DeepLapsed'
END AS [Customer_Status_at_Time_Of_Transaction]
FROM [CTE] AS [cur]
LEFT JOIN [CTE] AS [prv] ON [cur].[customerID] = [prv].[customerID]
AND [cur].[payment_count] - 1 = [prv].[payment_count]
ORDER BY [customerID];
As you are using SQL-Server 2012 there is the windowing function LAG() for your rescue:
Try something like this:
SELECT [customerID]
, [transaction_date]
, [payment_count]
, LAG([transaction_date]) OVER(PARTITION BY customerID ORDER BY transaction_date DESC) AS Last_transaction_date
--you can use this as parameter too
, DATEDIFF([mm], LAG([transaction_date]) OVER(PARTITION BY customerID ORDER BY transaction_date DESC), [transaction_date]) AS DifferenceInMonths
FROM [Financial]
I am trying to find a way to get the last date by location and product a sum was positive. The only way i can think to do it is with a cursor, and if that's the case I may as well just do it in code. Before i go down that route, i was hoping someone may have a better idea?
Table:
Product, Date, Location, Quantity
The scenario is; I find the quantity by location and product at a particular date, if it is negative i need to get the sum and date when the group was last positive.
select
Product,
Location,
SUM(Quantity) Qty,
SUM(Value) Value
from
ProductTransactions PT
where
Date <= #AsAtDate
group by
Product,
Location
i am looking for the last date where the sum of the transactions previous to and including it are positive
Based on your revised question and your comment, here another solution I hope answers your question.
select Product, Location, max(Date) as Date
from (
select a.Product, a.Location, a.Date from ProductTransactions as a
join ProductTransactions as b
on a.Product = b.Product and a.Location = b.Location
where b.Date <= a.Date
group by a.Product, a.Location, a.Date
having sum(b.Value) >= 0
) as T
group by Product, Location
The subquery (table T) produces a list of {product, location, date} rows for which the sum of the values prior (and inclusive) is positive. From that set, we select the last date for each {product, location} pair.
This can be done in a set based way using windowed aggregates in order to construct the running total. Depending on the number of rows in the table this could be a bit slow but you can't really limit the time range going backwards as the last positive date is an unknown quantity.
I've used a CTE for convenience to construct the aggregated data set but converting that to a temp table should be faster. (CTEs get executed each time they are called whereas a temp table will only execute once.)
The basic theory is to construct the running totals for all of the previous days using the OVER clause to partition and order the SUM aggregates. This data set is then used and filtered to the expected date. When a row in that table has a quantity less than zero it is joined back to the aggregate data set for all previous days for that product and location where the quantity was greater than zero.
Since this may return multiple positive date rows the ROW_NUMBER() function is used to order the rows based on the date of the positive quantity day. This is done in descending order so that row number 1 is the most recent positive day. It isn't possible to use a simple MIN() here because the MIN([Date]) may not correspond to the MIN(Quantity).
WITH x AS (
SELECT [Date],
Product,
[Location],
SUM(Quantity) OVER (PARTITION BY Product, [Location] ORDER BY [Date] ASC) AS Quantity,
SUM([Value]) OVER(PARTITION BY Product, [Location] ORDER BY [Date] ASC) AS [Value]
FROM ProductTransactions
WHERE [Date] <= #AsAtDate
)
SELECT [Date], Product, [Location], Quantity, [Value], Positive_date, Positive_date_quantity
FROM (
SELECT x1.[Date], x1.Product, x1.[Location], x1.Quantity, x1.[Value],
x2.[Date] AS Positive_date, x2.[Quantity] AS Positive_date_quantity,
ROW_NUMBER() OVER (PARTITION BY x1.Product, x1.[Location] ORDER BY x2.[Date] DESC) AS Positive_date_row
FROM x AS x1
LEFT JOIN x AS x2 ON x1.Product=x2.Product AND x1.[Location]=x2.[Location]
AND x2.[Date]<x1.[Date] AND x1.Quantity<0 AND x2.Quantity>0
WHERE x1.[Date] = #AsAtDate
) AS y
WHERE Positive_date_row=1
Do you mean that you want to get the last date of positive quantity come to positive in group?
For example, If you are using SQL Server 2012+:
In following scenario, when the date going to 01/03/2017 the summary of quantity come to 1(-10+5+6).
Is it possible the quantity of following date come to negative again?
;WITH tb(Product, Location,[Date],Quantity) AS(
SELECT 'A','B',CONVERT(DATETIME,'01/01/2017'),-10 UNION ALL
SELECT 'A','B','01/02/2017',5 UNION ALL
SELECT 'A','B','01/03/2017',6 UNION ALL
SELECT 'A','B','01/04/2017',2
)
SELECT t.Product,t.Location,SUM(t.Quantity) AS Qty,MIN(CASE WHEN t.CurrentSum>0 THEN t.Date ELSE NULL END ) AS LastPositiveDate
FROM (
SELECT *,SUM(tb.Quantity)OVER(ORDER BY [Date]) AS CurrentSum FROM tb
) AS t GROUP BY t.Product,t.Location
Product Location Qty LastPositiveDate
------- -------- ----------- -----------------------
A B 3 2017-01-03 00:00:00.000
I have table with sales plan data for every week, which consists of few columns:
SAL_DTDGID -- which is date of every Sunday, for example 20160110, 20160117
SAL_MQuantity --sum of sales plan value
SAL_MQuantityYTD --sum of plans since first day of the year
SAL_CoreElement --sales plan data for few core elements
SAL_Site --unique identifier of place, where sale has happened
How do I sum values in SAL_MQuantityYTD as values of SAL_MQuantity since first records in 2016 to 'now' for every site and every core element?
Every site mentioned in SAL_Site has 52 rows corresponding week count in a year along with 5 different SAL_CoreElement's
Example:
SAL_DTDGID|SAL_MQuantity|SAL_MQuantityYTD|SAL_CoreElement|SAL_Site
20160110 |20000 |20000 |1 |1234
20160117 |10000 |30000 |1 |1234
20160124 |30000 |60000 |1 |1234
If something isn't clear I'll try to explain.
Not sure I completely understand your question, but this should allow you to recreate the running sum for SAL_MQuantityYTD. Replace #test with whatever your table/view is called.
SELECT *,
(SELECT SUM(SAL_MQuantity)
FROM #test T2
WHERE T2.SAL_DTDGID <= T1.SAL_DTDGID
AND T2.SAL_Site = T1.SAL_Site
AND T2.SAL_coreElement = T1.SAL_coreElement) AS RunningTotal
FROM #test T1
If you wanted to create the yearly figure then you could also use a correlated subquery like this
SELECT *,
(SELECT SUM(SAL_MQuantity)
FROM #test T2
WHERE cast(left(T2.SAL_DTDGID,4) as integer) = cast(left(T1.SAL_DTDGID,4) as integer)
AND T2.SAL_Site = T1.SAL_Site
AND T2.SAL_coreElement = T1.SAL_coreElement) AS RunningTotal
FROM #test T1
Edit: Just seen, basically the same answer, using a window function.
Let me explain you an idea. Please try below.
Select A, B,
(Select SUM(SAL_MQuantity)
FORM [Your Table]
WHERE [your date column] between '20160101' AND '[Present date]') AS SAL_MQuantityYTD
FROM [Your Table]
My understanding from your questions is that you want to have the YTD sum of SAL_MQuantity for each year (you can simply 'where' after if you only want 2016), SAL_Site, SAL_CoreElement.
The code below should achieve that and will run on SQL 2008 r2 (im running 2005).
'##t1' is the temp table name I used to test, replace it with your table name.
Select distinct
sum (SAL_MQuantity) over (partition by
left (cast (cast (SAL_DTDGID as int) as varchar (8)),4)
, SAL_Site
, SAL_CoreElement
) as Sum_SAL_DTDGID
,left (cast (cast (SAL_DTDGID as int) as varchar (8)),4) as Time_Period
, SAL_Site
, SAL_CoreElement
from ##t1