How to pivot on multiple columns? - sql

I'm trying to pivot on multiple columns and I'm using SQL Server 2014, however, I cannot figure out how to do that. Here's what I've tried so far:
DECLARE #Table TABLE (
Name NVARCHAR(MAX),
TypeId INT,
TotalOrders INT,
GrandTotal MONEY
)
INSERT INTO #Table
(Name, TypeId, TotalOrders, GrandTotal)
VALUES
('This Month', 1, 10, 1),
('This Month', 2, 5, 7),
('This Week', 1, 8, 3),
('Last Week', 1, 8, 12),
('Yesterday', 1, 10, 1),
('Yesterday', 2, 1, 5)
Which produces the following result:
Name TypeId TotalOrders GrandTotal
-------------------------------- ----------- ----------- ---------------------
This Month 1 10 1.00
This Month 2 5 7.00
This Week 1 8 3.00
Last Week 1 8 12.00
Yesterday 1 10 1.00
Yesterday 2 1 5.00
To bring those rows into columns, I've tried this:
SELECT
TypeId,
ISNULL([Yesterday], 0) AS YesterdayTotalOrders,
ISNULL([This Week], 0) AS ThisWeekTotalOrders,
ISNULL([Last Week], 0) AS LastWeekTotalOrders,
ISNULL([This Month], 0) AS ThisMonthTotalOrders
FROM
(SELECT Name, TypeId, TotalOrders FROM #Table) AS src
PIVOT (
SUM(TotalOrders) FOR Name IN (
[Yesterday],
[This Week],
[Last Week],
[This Month]
)
) AS p1
Which produces the following result set:
TypeId YesterdayTotalOrders ThisWeekTotalOrders LastWeekTotalOrders ThisMonthTotalOrders
----------- -------------------- ------------------- ------------------- --------------------
1 10 8 8 10
2 1 0 0 5
Now, I need to have few other columns for GrandTotal such as YesterdayGrandTotal, ThisWeekGrandTotal, and so on and so forth but I can't figure out how to achieve this.
Any help would be highly appreciated.
UPDATE#1: Here's the expected result set:
TypeId YesterdayTotalOrders ThisWeekTotalOrders LastWeekTotalOrders ThisMonthTotalOrders YesterdayGrandTotal ThisWeekGrandTotal LastWeekGrandTotal ThisMonthGrandTotal
----------- -------------------- ------------------- ------------------- -------------------- --------------------- --------------------- --------------------- ---------------------
1 10 8 8 10 1.00 3.00 12.00 1.00
2 1 0 0 5 5.00 0.00 0.00 7.00

Conditional aggregation may be a solution:
select typeID,
SUM(case when name = 'Yesterday' then totalOrders else 0 end) as YesterdayTotalOrders,
SUM(case when name = 'This Week' then totalOrders else 0 end) as ThisWeekTotalOrders,
SUM(case when name = 'Last Week' then totalOrders else 0 end) as LastWeekTotalOrders,
SUM(case when name = 'This Month' then totalOrders else 0 end) as ThisMonthTotalOrders,
SUM(case when name = 'Yesterday' then GrandTotal else 0 end) as YesterdayGrandTotal,
SUM(case when name = 'This Week' then GrandTotal else 0 end) as ThisWeekGrandTotal,
SUM(case when name = 'Last Week' then GrandTotal else 0 end) as LastWeekGrandTotal,
SUM(case when name = 'This Month' then GrandTotal else 0 end) as ThisMonthGrandTotal
from #table
group by typeID
or, you can use the CROSS APPLY and PIVOT like this
SELECT
TypeId,
ISNULL([Yesterday], 0) AS YesterdayTotalOrders,
ISNULL([This Week], 0) AS ThisWeekTotalOrders,
ISNULL([Last Week], 0) AS LastWeekTotalOrders,
ISNULL([This Month], 0) AS ThisMonthTotalOrders,
ISNULL([grant Yesterday], 0) AS YesterdayGrandTotal,
ISNULL([grant This Week], 0) AS ThisWeekGrandTotal,
ISNULL([grant Last Week], 0) AS LastWeekGrandTotal,
ISNULL([grant This Month], 0) AS ThisMonthGrandTotal
FROM
(
SELECT t.*
FROM #Table
CROSS APPLY (values(Name, TypeId, TotalOrders),
('grant ' + Name, TypeId, GrandTotal))
t(Name, TypeId, TotalOrders)
) AS src
PIVOT (
SUM(TotalOrders) FOR Name IN (
[Yesterday],
[This Week],
[Last Week],
[This Month],
[grant Yesterday],
[grant This Week],
[grant Last Week],
[grant This Month]
)
) AS p1
demo
Both solutions will scan the input table just once and they have a very similar query plan. Both solutions are better than JOIN of two pivots (the solution that I have originally provided) since two pivots need to scan the input table twice.

You could also use of CTE by separating your pivots.. P1 for TotalOrders & P2 for GrandTotal
;WITH CTE AS
(
SELECT
P1.TypeId,
ISNULL(P1.[Yesterday], 0) AS YesterdayTotalOrders,
ISNULL(P1.[This Week], 0) AS ThisWeekTotalOrders,
ISNULL(P1.[Last Week], 0) AS LastWeekTotalOrders,
ISNULL(P1.[This Month], 0) AS ThisMonthTotalOrders
FROM
(SELECT Name, TypeId, TotalOrders FROM #Table) AS src
PIVOT (SUM(TotalOrders) FOR src.Name IN (
[Yesterday],
[This Week],
[Last Week],
[This Month])) AS P1
), CTE1 AS
(
SELECT
P1.TypeId,
ISNULL(P1.[Yesterday], 0) AS YesterdayGrandTotal,
ISNULL(P1.[This Week], 0) AS ThisWeekTGrandTotal,
ISNULL(P1.[Last Week], 0) AS LastWeekGrandTotal,
ISNULL(P1.[This Month], 0) AS ThisMonthGrandTotal
FROM
(SELECT Name, TypeId, GrandTotal FROM #Table) AS src
PIVOT (SUM(GrandTotal) FOR src.Name IN (
[Yesterday],
[This Week],
[Last Week],
[This Month])) AS P1
)
SELECT C.TypeId , C.YesterdayTotalOrders, C.ThisWeekTotalOrders, C.LastWeekTotalOrders, C.ThisMonthTotalOrders , C1.YesterdayGrandTotal , C1.ThisWeekTGrandTotal ,C1.LastWeekGrandTotal , C1.ThisMonthGrandTotal FROM CTE C
INNER JOIN CTE1 C1 ON C1.TypeId = C.TypeId
Result :
TypeId YesterdayTotalOrders ThisWeekTotalOrders LastWeekTotalOrders ThisMonthTotalOrders YesterdayGrandTotal ThisWeekGrandTotal LastWeekGrandTotal ThisMonthGrandTotal
----------- -------------------- ------------------- ------------------- -------------------- --------------------- --------------------- --------------------- ---------------------
1 10 8 8 10 1.00 3.00 12.00 1.00
2 1 0 0 5 5.00 0.00 0.00 7.00

IN oracle i would do something like tihs. it works fine for me
select * from (
SELECT Name, TypeId,TotalOrders
--hier
,GrandTotal
FROM test_b )
PIVOT ( SUM(TotalOrders)TotalOrders,
--hier
SUM(grandtotal) grandtotal FOR Name IN
('Yesterday'Yesterday,'This Week'ThisWeek,'Last Week'LastWeek,'This
Month'ThisMonth )
) ;
so try this in sql server
SELECT *
/* TypeId,
ISNULL([Yesterday], 0) AS YesterdayTotalOrders,
ISNULL([This Week], 0) AS ThisWeekTotalOrders,
ISNULL([Last Week], 0) AS LastWeekTotalOrders,
ISNULL([This Month], 0) AS ThisMonthTotalOrders*/
FROM
(SELECT Name, TypeId, TotalOrders
,grandtotal
FROM #Table) AS src
PIVOT (
SUM(TotalOrders)TotalOrders, SUM(grandtotal)grandtotal FOR Name IN (
[Yesterday]Yesterday,
[This Week]ThisWeek,
[Last Week]LastWeek,
[This Month]ThisMonth
)
) AS p1

Related

How to PIVOT with 2 grouping columns in the result set?

I have a query that outputs the following:
ApptDate Truck_ID Item Qty
'8-19-20' TruckA ItemA 100
'8-19-20' TruckB ItemA 200
'8-20-20' TruckC ItemB 300
'8-20-20' TruckD ItemB 400
...
I need to PIVOT so that it returns this:
Item Truck_ID Day1 Day2 ... Day14
ItemA TruckA 100 0 0
ItemA TruckB 200 0 0
ItemB TruckC 0 300 0
ItemB TruckD 0 400 0
I tried this, but it gave an error:
Msg 8114, Level 16, State 1, Line 413
Error converting data type nvarchar to datetime.
Msg 473, Level 16, State 1, Line 413
The incorrect value "Day1" is supplied in the PIVOT operator.
select
item, truck_id, Day1, Day2, Day3, Day4, Day5, Day6, Day7, Day8, Day9, Day10, Day11, Day12, Day13, Day14
from(
select
ds.ApptDate
, c.truck_id
, c.item
, sum(c.qty) qty
from
maintable c with(nolock)
inner join secondtable ds with(nolock) on c.truck_id = ds.truckid and ds.type = 'O'
where
ds.apptdate between cast(getdate() as date) and dateadd(day, 14, cast(getdate() as date))
and coalesce(ds.CancelTruck, 0) <> 1
and ds.Status <> '5'
group by
c.truck_id
, c.item
, ds.ApptDate
) sourcetable
pivot
(
sum(qty)
for apptdate in ([Day1], [Day2], [Day3], [Day4], [Day5], [Day6], [Day7], [Day8], [Day9], [Day10], [Day11], [Day12], [Day13], [Day14])
) as pivottable
Since you expect a fixed number of columns, we don't necessarily need dynamic SQL. One option uses conditional aggregation... and lot of repeated typing:
select
item,
truck_id,
sum(case when appt_date = cast(getdate() as date) then qty else 0 end) day0,
sum(case when appt_date = dateadd(day, -1 , cast(getdate() as date)) then qty else 0 end) day1,
sum(case when appt_date = dateadd(day, -2 , cast(getdate() as date)) then qty else 0 end) day2,
...
sum(case when appt_date = dateadd(day, -14, cast(getdate() as date)) then qty else 0 end) day14
from ( -- your current query here --) t
group by item, truck_id
This approach uses datediff's on the minimum date and the AppDate.
;with
min_dt_cte(min_dt) as (select min(cast(AppDate as date)) from MyTable),
pvt_dt_cte(ApptDate, Truck_ID, Item, Qty, DayNum) as (
select t.*, datediff(d, mdc.min_dt, cast(AppDate as date))
from min_dt_cte mdc
cross join
MyTable t)
select
pdc.Item, pdc.Truck_ID,
iif(pdc.DayNum=1, Qty, 0) Day1,
iif(pdc.DayNum=2, Qty, 0) Day2,
...
iif(pdc.DayNum=14, Qty, 0) Day14
from
pvt_dt_cte pdc;

total Sales count for last current week, month and yearly - SQL

How do you find the total sales count for a salesperson by weekly, monthly and yearly ?
SELECT AdjusterName,
SUM(CASE WHEN TaskDate >= DATEADD(WEEK, DATEDIFF(WEEK, 0, SYSDATETIME()), 0)
AND TaskDate < DATEADD(WEEK, DATEDIFF(WEEK, 0, SYSDATETIME()) + 1, 0)
THEN 1 ELSE 0 END) AS WTD,
sum(case when MONTH(TaskDate) = MONTH(GetDate()) then 1 else 0 end ) as MTD,
sum(case when year(TaskDate) = year(GetDate()) then 1 else 0 end ) as YTD
FROM cte
GROUP BY AdjusterName
for example:-
name WTD MTD YTD
SalesPersnA 2 5 10
Sample Data
ID SalesPerson NAME TaskDate TaskAge DocumentType TaskStatus OverdueCheck
2000378 Willy Akron FNOL Supervisor Team 1 2015-02-04 1258 Claim.Reassigned.File.Text Completed WithinSLA
2000378 Amanda Akron FNOL Supervisor Team 1 2015-02-04 1258 ClaimLifecycle.Open.RD.Text Completed WithinSLA
2000388 Amanda Akron FNOL Supervisor Team 1 2016-08-06 709 ClaimLifecycle.Open.RD.Text Completed WithinSLA
2000388 Willy Akron FNOL Supervisor Team 1 2016-08-06 709 Claim.Reassigned.File.Text Completed WithinSLA
2000388 Schutz Akron FNOL Supervisor Team 1 2016-09-21 663 ISO.Failure.Diary.Text: Completed WithinSLA
2000388 Stephannie Akron FNOL Supervisor Team 1 2016-09-26 658 Claim.Reassigned.File.Text Completed WithinSLA
Count ignore nulls, so you could use a count call on top of a case expression for the last week and month. The condition on the year could be expressed in the where clause:
SELECT SalesPerson,
COUNT(CASE DATEDIFF(WEEK, TaskDate, GETDATE()) = 0 THEN 1 END) AS WTD,
COUNT(CASE DATEDIFF(MONTH, TaskDate, GETDATE()) = 0 THEN 1 END) AS MTD,
COUNT(*)
FROM mytable
WHERE DATEDIFF(YEAR, TaskDate, GETDATE()) = 0
GROUP BY SalesPerson
If you want results for year, month, week in columns you can achieve with window functions:
declare #test table (ID int, SalesPerson nvarchar(100), [Name] nvarchar(100), TaskDate date, TaskAge int, DocumentType nvarchar(max), TaskStatus nvarchar(20), OverdueCheck nvarchar(20))
insert into #test values
(2000378, 'Willy' , 'Akron FNOL Supervisor Team 1', '2015-02-04', 1258, 'Claim.Reassigned.File.Text ', 'Completed', 'WithinSLA')
, (2000378, 'Amanda' , 'Akron FNOL Supervisor Team 1', '2015-02-04', 1258, 'ClaimLifecycle.Open.RD.Text', 'Completed', 'WithinSLA')
, (2000388, 'Amanda' , 'Akron FNOL Supervisor Team 1', '2016-08-06', 709 , 'ClaimLifecycle.Open.RD.Text', 'Completed', 'WithinSLA')
, (2000388, 'Willy' , 'Akron FNOL Supervisor Team 1', '2016-08-06', 709 , 'Claim.Reassigned.File.Text ', 'Completed', 'WithinSLA')
, (2000388, 'Schutz' , 'Akron FNOL Supervisor Team 1', '2016-09-21', 663 , 'ISO.Failure.Diary.Text: ', 'Completed', 'WithinSLA')
, (2000388, 'Stephannie', 'Akron FNOL Supervisor Team 1', '2016-09-26', 658 , 'Claim.Reassigned.File.Text ', 'Completed', 'WithinSLA')
select SalesPerson
, Year(TaskDate) as [Year]
, month(taskdate) as [Month]
, DATEPART( wk, taskdate) as [Week]
, count(*) over (Partition by salesperson, Year(taskdate)) as [YearCount]
, count(*) over (Partition by salesperson, Year(taskdate), Month(taskdate)) as [MonthCount]
, count(*) over (Partition by salesperson, Year(taskdate), DATEPART( wk, taskdate)) as [WeekCount]
from #test
This worked for me! Thank you all for your suggestions and time.
select AdjusterName, NAME,
sum(case when DATEPART( wk,TaskDate) = DATEPART ( wk, getdate()) and YEAR(taskDate) = 2018 then 1 else 0 end ) as WTD,
sum(case when DATEPART (m, TaskDate ) = DATEPART ( m, getdate()) and YEAR(taskDate) = 2018 then 1 else 0 end ) as MTD,
sum(case when DATEPART (YEAR, TaskDate) = DATEPART ( YEAR, getdate()) and YEAR(taskDate) = 2018 then 1 else 0 end ) as YTD
from cte
group by AdjusterName,NAME

SQL Server query for new and repeat orders per month

I am working in SQL Server 2008 R2 and having a hard time gathering new vs repeat customer orders.
I have data in this format:
OrderID OrderDate Customer OrderAmount
-----------------------------------------------
1 1/1/2017 A $10
2 1/2/2017 B $20
3 1/3/2017 C $30
4 4/1/2017 C $40
5 4/2/2017 D $50
6 4/3/2017 D $60
7 1/6/2018 B $70
Here's what we want:
New defined as: customer has not placed any orders in any prior months.
Repeat defined as: customer has placed an order in a prior month (even if many years ago).
This means that if a new customer places multiple orders in her first month, they would all be considered "new" customer orders. And orders placed in subsequent months would all be considered "repeat" customer orders.
We want to get New orders (count and sum) and Repeat orders (count and sum) per year, per month:
Year Month NewCount NewSum RepeatCount RepeatSum
-----------------------------------------------------------------------------
2017 1 3 (A,B,C) $60 (10+20+30) 0 $0
2017 4 2 (D,D) $110 (50+60) 1 (C) $40 (40)
2018 1 0 $0 1 (B) $70 (70)
(The info in () parenthesis is not part of the result; just putting it here for clarity)
The SQL is easy to write for any single given month, but I don't know how to do it when gathering years worth of months at a time...
If there is a month with no orders of any kind then NULL or 0 values for the year:month would be preferred.
You can use dense_rank to find new and old customers. This query returns your provided output
declare #t table (OrderID int, OrderDate date, Customer char(1), OrderAmount int)
insert into #t
values (1, '20170101', 'A', 10)
, (2, '20170102', 'B', 20), (3, '20170103', 'C', 30)
, (4, '20170401', 'C', 40), (5, '20170402', 'D', 50)
, (6, '20170403', 'D', 60), (7, '20180106', 'B', 70)
select
[year], [month], NewCount = isnull(sum(case when dr = 1 then 1 end), 0)
, NewSum = isnull(sum(case when dr = 1 then OrderAmount end), 0)
, RepeatCount = isnull(sum(case when dr > 1 then 1 end), 0)
, RepeatSum = isnull(sum(case when dr > 1 then OrderAmount end), 0)
from (
select
*, [year] = year(OrderDate), [month] = month(OrderDate)
, dr = dense_rank() over (partition by Customer order by dateadd(month, datediff(month, 0, OrderDate), 0))
from
#t
) t
group by [year], [month]
Output
year month NewCount NewSum RepeatCount RepeatSum
----------------------------------------------------------
2017 1 3 60 0 0
2018 1 0 0 1 70
2017 4 2 110 1 40
You must get combination of each year in the table with all months at first if you want to display months without orders. Then join with upper query
select
*
from
(select distinct y = year(OrderDate) from #t) t
cross join (values (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12)) q(m)
First, start by summarizing the data with one record per customer per month.
Then, you can use a self-join or similar construct to get the information you need:
with cm as (
select customer, dateadd(day, 1 - day(orderdate), orderdate) as yyyymm
sum(orderamount) as monthamount, count(*) as numorders
from orders
group by customer
)
select year(cm.yyyymm) as yr, month(cm.yyyymm) as mon,
sum(case when cm.num_orders > 0 and cm_prev.customer is null then 1 else 0 end) as new_count,
sum(case when cm.num_orders > 0 and cm_prev.customer is null then monthamount else 0 end) as new_amount,
sum(case when cm.num_orders > 0 and cm_prev.customer > 0 then 1 else 0 end) as repeat_count,
sum(case when cm.num_orders > 0 and cm_prev.customer > 0 then monthamount else 0 end) as repeat_amount
from cm left join
cm cm_prev
on cm.customer = cm_prev.customer and
cm.yyyymm = dateadd(month, 1, cm_prev.yyyymm)
group by year(cm.yyyymm), month(cm.yyyymm)
order by year(cm.yyyymm), month(cm.yyyymm);
This would be a bit easier in SQL Server 2012, where you can use lag().

JOIN, GROUP BY AND SUM in single Query

Invoices Table
invoice_id invoice_date
------------ --------------
1 2013-11-27
2 2013-10-09
3 2013-09-12
Orders Table
order_id invoice_id product quantity total
--------- ---------- --------- --------- -------
1 1 Product 1 100 1000
2 1 Product 2 50 200
3 2 Product 1 40 400
4 3 Product 2 50 200
And i want a single sql query that produces following result
products Month 9 Total Month 10 Total Mont 11 Total
-------- ------------- -------------- -------------
Product 1 0 400 100
Product 2 200 0 200
I have tried the following sql query
SELECT orders.products, DATEPART(Year, invoices.invoice_date) Year, DATEPART(Month, invoices.invoice_date) Month, SUM(orders.total) [Total],
FROM invoices INNER JOIN orders ON invoices.invoice_id=orders.invoice_id
GROUP BY orders.products, DATEPART(Year, invoices.invoice_date), DATEPART(Month, invoices.invoice_date)
But it returns nothing. Is it possible to get this result with single query and what should i do for that ? Thanks
I think you want to use PIVOT here ...
Try this:
WITH tmp
AS
(
SELECT orders.products,
DATEPART(Year, invoices.invoice_date) Year,
DATEPART(Month, invoices.invoice_date) Month,
SUM(orders.total) [Total]
FROM invoices INNER JOIN orders ON invoices.invoice_id = orders.invoice_id
GROUP BY
orders.products,
DATEPART(Year, invoices.invoice_date),
DATEPART(Month, invoices.invoice_date)
)
SELECT products,
ISNULL([9],0) AS Nine, ISNULL([10],0) AS Ten, ISNULL([11],0) as Eleven
FROM tmp
PIVOT
(
SUM([Total])
FOR Month IN
( [9], [10], [11])
) as PVT;
You can edit it here: http://sqlfiddle.com/#!6/6f80f/6
You better use pivot for this. Just remember that you have to list every month explicitly in pivot..for..in clause.
select
*
into #invoices
from (
select 1 as invoice_id, '2013-11-27' as invoice_date union all
select 2,'2013-10-09' union all
select 3,'2013-09-12'
) x
select
*
into #orders
from (
select 1 as order_id,1 as invoice_id,'Product 1' as product,100 as quantity,1000 as total union all
select 2,1,'Product 2',50,200 union all
select 3,2,'Product 1',40,400 union all
select 4,3,'Product 2',50,200
) x
GO
select
Product,
[9], [10], [11]
from (
select
o.product, datepart([month],i.invoice_date) as mon,
o.total
from #invoices i
join #orders o
on i.invoice_id=o.invoice_id
) x
pivot (
sum(total) for mon in ([9],[10],[11])
) p
One way to do it is with Cases:
Select
O.product,
Sum(Case
When DATEPART(M, I.invoice_Date) = 9 Then O.total
Else 0
End) as Month9,
Sum(Case
When DATEPART(M, I.invoice_Date) = 10 Then O.total
Else 0
End) as Month10,
Sum(Case
When DATEPART(M, I.invoice_Date) = 11 Then O.total
Else 0
End) as Month11
From Invoice I
Left join Orders O on I.invoice_id = O.invoice_id
Group by O.product
There is another way using Pivots (depending on your version of SQL).
Select product, [9], [10], [11]
From
(
Select O.Product, O.total, DatePart(M, I.Invoice_Date) as [MonthNum]
From Invoice I
Left join Orders O on I.invoice_id = O.invoice_id
) P
PIVOT
(
Sum(P.total)
For [MonthNum] in ([9], [10], [11])
) as O

SQL Stairstep Query

I need some help producing a MS SQL 2012 query that will match the desired stair-step output. The rows summarize data by one date range (account submission date month), and the columns summarize it by another date range (payment date month)
Table 1: Accounts tracks accounts placed for collections.
CREATE TABLE [dbo].[Accounts](
[AccountID] [nchar](10) NOT NULL,
[SubmissionDate] [date] NOT NULL,
[Amount] [money] NOT NULL,
CONSTRAINT [PK_Accounts] PRIMARY KEY CLUSTERED (AccountID ASC))
INSERT INTO [dbo].[Accounts] VALUES ('1000', '2012-01-01', 1999.00)
INSERT INTO [dbo].[Accounts] VALUES ('1001', '2012-01-02', 100.00)
INSERT INTO [dbo].[Accounts] VALUES ('1002', '2012-02-05', 350.00)
INSERT INTO [dbo].[Accounts] VALUES ('1003', '2012-03-01', 625.00)
INSERT INTO [dbo].[Accounts] VALUES ('1004', '2012-03-10', 50.00)
INSERT INTO [dbo].[Accounts] VALUES ('1005', '2012-03-10', 10.00)
Table 2: Trans tracks payments made
CREATE TABLE [dbo].[Trans](
[TranID] [int] IDENTITY(1,1) NOT NULL,
[AccountID] [nchar](10) NOT NULL,
[TranDate] [date] NOT NULL,
[TranAmount] [money] NOT NULL,
CONSTRAINT [PK_Trans] PRIMARY KEY CLUSTERED (TranID ASC))
INSERT INTO [dbo].[Trans] VALUES (1000, '2012-01-15', 300.00)
INSERT INTO [dbo].[Trans] VALUES (1000, '2012-02-15', 300.00)
INSERT INTO [dbo].[Trans] VALUES (1000, '2012-03-15', 300.00)
INSERT INTO [dbo].[Trans] VALUES (1002, '2012-02-20', 325.00)
INSERT INTO [dbo].[Trans] VALUES (1002, '2012-04-20', 25.00)
INSERT INTO [dbo].[Trans] VALUES (1003, '2012-03-24', 625.00)
INSERT INTO [dbo].[Trans] VALUES (1004, '2012-03-28', 31.00)
INSERT INTO [dbo].[Trans] VALUES (1004, '2012-04-12', 5.00)
INSERT INTO [dbo].[Trans] VALUES (1005, '2012-04-08', 7.00)
INSERT INTO [dbo].[Trans] VALUES (1005, '2012-04-28', 3.00)
Here's what the desired output should look like
*Total Payments in Each Month*
SubmissionYearMonth TotalAmount | 2012-01 2012-02 2012-03 2012-04
--------------------------------------------------------------------
2012-01 2099.00 | 300.00 300.00 300.00 0.00
2012-02 350.00 | 325.00 0.00 25.00
2012-03 685.00 | 656.00 15.00
The first two columns sum Account.Amount grouping by month.
The last 4 columns sum the Tran.TranAmount, by month, for Accounts placed in the given month of the current row.
The query I've been working with feel close. I just don't have the lag correct.
Here's the query I'm working with thus far:
Select SubmissionYearMonth,
TotalAmount,
pt.[0] AS MonthOld0,
pt.[1] AS MonthOld1,
pt.[2] AS MonthOld2,
pt.[3] AS MonthOld3,
pt.[4] AS MonthOld4,
pt.[5] AS MonthOld5,
pt.[6] AS MonthOld6,
pt.[7] AS MonthOld7,
pt.[8] AS MonthOld8,
pt.[9] AS MonthOld9,
pt.[10] AS MonthOld10,
pt.[11] AS MonthOld11,
pt.[12] AS MonthOld12,
pt.[13] AS MonthOld13
From (
SELECT Convert(Char(4),Year(SubmissionDate)) + '-' + Right('00' + Convert(VarChar(2), DatePart(Month, SubmissionDate)),2) AS SubmissionYearMonth,
SUM(Amount) AS TotalAmount
FROM Accounts
GROUP BY Convert(Char(4),Year(SubmissionDate)) + '-' + Right('00' + Convert(VarChar(2), DatePart(Month, SubmissionDate)),2)
)
AS AccountSummary
OUTER APPLY
(
SELECT *
FROM (
SELECT CASE WHEN DATEDIFF(Month, SubmissionDate, TranDate) < 13
THEN DATEDIFF(Month, SubmissionDate, TranDate)
ELSE 13
END AS PaymentMonthAge,
TranAmount
FROM Trans INNER JOIN Accounts ON Trans.AccountID = Accounts.AccountID
Where Convert(Char(4),Year(TranDate)) + '-' + Right('00' + Convert(VarChar(2), DatePart(Month, TranDate)),2)
= AccountSummary.SubmissionYearMonth
) as TransTemp
PIVOT (SUM(TranAmount)
FOR PaymentMonthAge IN ([0],
[1],
[2],
[3],
[4],
[5],
[6],
[7],
[8],
[9],
[10],
[11],
[12],
[13])) as TransPivot
) as pt
It's producing the following output:
SubmissionYearMonth TotalAmount MonthOld0 MonthOld1 MonthOld2 MonthOld3 ...
2012-01 2099.00 300.00 NULL NULL NULL ...
2012-02 350.00 325.00 300.00 NULL NULL ...
2012-03 685.00 656.00 NULL 300.00 NULL ...
As for the column date headers. I'm not sure what the best option is here. I could add an additional set of columns and create a calculated value that I could use in the resulting report.
SQL Fiddle: http://www.sqlfiddle.com/#!6/272e5/1/0
Since you are using SQL Server 2012, we can use the Format function to make the date pretty. There is no need to group by the strings. Instead, I find it useful to use the proper data type for as long as I can and only use Format or Convert on display (or not at all and let the middle tier handle the display).
In this solution, I arbitrarily assumed the earliest TransDate and extract from it, the first day of that month. However, one could easily replace that expression with a static value of the start date desired and this solution would take that and the next 12 months.
With SubmissionMonths As
(
Select DateAdd(d, -Day(A.SubmissionDate) + 1, A.SubmissionDate) As SubmissionMonth
, A.Amount
From dbo.Accounts As A
)
, TranMonths As
(
Select DateAdd(d, -Day(Min( T.TranDate )) + 1, Min( T.TranDate )) As TranMonth
, 1 As MonthNum
From dbo.Accounts As A
Join dbo.Trans As T
On T.AccountId = A.AccountId
Join SubmissionMonths As M
On A.SubmissionDate >= M.SubmissionMonth
And A.SubmissionDate < DateAdd(m,1,SubmissionMonth)
Union All
Select DateAdd(m, 1, TranMonth), MonthNum + 1
From TranMonths
Where MonthNum < 12
)
, TotalBySubmissionMonth As
(
Select M.SubmissionMonth, Sum( M.Amount ) As Total
From SubmissionMonths As M
Group By M.SubmissionMonth
)
Select Format(SMT.SubmissionMonth,'yyyy-MM') As SubmissionMonth, SMT.Total
, Sum( Case When TM.MonthNum = 1 Then T.TranAmount End ) As Month1
, Sum( Case When TM.MonthNum = 2 Then T.TranAmount End ) As Month2
, Sum( Case When TM.MonthNum = 3 Then T.TranAmount End ) As Month3
, Sum( Case When TM.MonthNum = 4 Then T.TranAmount End ) As Month4
, Sum( Case When TM.MonthNum = 5 Then T.TranAmount End ) As Month5
, Sum( Case When TM.MonthNum = 6 Then T.TranAmount End ) As Month6
, Sum( Case When TM.MonthNum = 7 Then T.TranAmount End ) As Month7
, Sum( Case When TM.MonthNum = 8 Then T.TranAmount End ) As Month8
, Sum( Case When TM.MonthNum = 9 Then T.TranAmount End ) As Month9
, Sum( Case When TM.MonthNum = 10 Then T.TranAmount End ) As Month10
, Sum( Case When TM.MonthNum = 11 Then T.TranAmount End ) As Month11
, Sum( Case When TM.MonthNum = 12 Then T.TranAmount End ) As Month12
From TotalBySubmissionMonth As SMT
Join dbo.Accounts As A
On A.SubmissionDate >= SMT.SubmissionMonth
And A.SubmissionDate < DateAdd(m,1,SMT.SubmissionMonth)
Join dbo.Trans As T
On T.AccountId = A.AccountId
Join TranMonths As TM
On T.TranDate >= TM.TranMonth
And T.TranDate < DateAdd(m,1,TM.TranMonth)
Group By SMT.SubmissionMonth, SMT.Total
SQL Fiddle version
The following query pretty much returns what you want. You need to do the to operations separately. I just join the results together:
select a.yyyymm, a.Amount,
t201201, t201202, t201203, t201204
from (select LEFT(convert(varchar(255), a.submissiondate, 121), 7) as yyyymm,
SUM(a.Amount) as amount
from Accounts a
group by LEFT(convert(varchar(255), a.submissiondate, 121), 7)
) a left outer join
(select LEFT(convert(varchar(255), a.submissiondate, 121), 7) as yyyymm,
sum(case when trans_yyyymm = '2012-01' then tranamount end) as t201201,
sum(case when trans_yyyymm = '2012-02' then tranamount end) as t201202,
sum(case when trans_yyyymm = '2012-03' then tranamount end) as t201203,
sum(case when trans_yyyymm = '2012-04' then tranamount end) as t201204
from Accounts a join
(select t.*, LEFT(convert(varchar(255), t.trandate, 121), 7) as trans_yyyymm
from trans t
) t
on a.accountid = t.accountid
group by LEFT(convert(varchar(255), a.submissiondate, 121), 7)
) t
on a.yyyymm = t.yyyymm
order by 1
I am getting a NULL where you have a 0.00 in two cells.
Thomas, I used your response as inspiration for the following solution I ended up using.
I first create a SubmissionDate, TranDate cross join skeleton date matrix, that I later use to join on the AccountSummary and TranSummary data.
The resulting query output isn't formatted in columns, per TranDate month. Rather I'm using output in a SQL Server Reporting Services matrix, and using a column grouping, based off the TranSummaryMonthNum column, to get the desired formatted output.
SQL Fiddle version
;
WITH
--Generate a list of Dates, from the first SubmissionDate, through today.
--Note: Requires the use of: 'OPTION (MAXRECURSION 0)' to generate a list with more than 100 dates.
CTE_AutoDates AS
( Select Min(SubmissionDate) as FiscalDate
From Accounts
UNION ALL
SELECT DATEADD(Day, 1, FiscalDate)
FROM CTE_AutoDates
WHERE DATEADD(Day, 1, FiscalDate) <= GetDate()
),
FiscalDates As
( SELECT FiscalDate,
DATEFROMPARTS(Year(FiscalDate), Month(FiscalDate), 1) as FiscalMonthStartDate
FROM CTE_AutoDates
--Optionaly filter Fiscal Dates by the last known Math.Max(SubmissionDate, TranDate)
Where FiscalDate <= (Select Max(MaxDate)
From (Select Max(SubmissionDate) as MaxDate From Accounts
Union All
Select Max(TranDate) as MaxDate From Trans
) as MaxDates
)
),
FiscalMonths as
( SELECT Distinct FiscalMonthStartDate
FROM FiscalDates
),
--Matrix to store the reporting date groupings for the Account submission and payment periods.
SubmissionAndTranMonths AS
( Select AM.FiscalMonthStartDate as SubmissionMonthStartDate,
TM.FiscalMonthStartDate as TransMonthStartDate,
DateDiff(Month, (Select Min(FiscalMonthStartDate) From FiscalMonths), TM.FiscalMonthStartDate) as TranSummaryMonthNum
From FiscalMonths AS AM
Join FiscalMonths AS TM
ON TM.FiscalMonthStartDate >= AM.FiscalMonthStartDate
),
AccountData as
( Select A.AccountID,
A.Amount,
FD.FiscalMonthStartDate as SubmissionMonthStartDate
From Accounts as A
Inner Join FiscalDates as FD
ON A.SubmissionDate = FD.FiscalDate
),
TranData as
( Select T.AccountID,
T.TranAmount,
AD.SubmissionMonthStartDate,
FD.FiscalMonthStartDate as TranMonthStartDate
From Trans as T
Inner Join AccountData as AD
ON T.AccountID = AD.AccountID
Inner Join FiscalDates AS FD
ON T.TranDate = FD.FiscalDate
),
AccountSummaryByMonth As
( Select ASM.FiscalMonthStartDate,
Sum(AD.Amount) as TotalSubmissionAmount
From FiscalMonths as ASM
Inner Join AccountData as AD
ON ASM.FiscalMonthStartDate = AD.SubmissionMonthStartDate
Group By
ASM.FiscalMonthStartDate
),
TranSummaryByMonth As
( Select STM.SubmissionMonthStartDate,
STM.TransMonthStartDate,
STM.TranSummaryMonthNum,
Sum(TD.TranAmount) as TotalTranAmount
From SubmissionAndTranMonths as STM
Inner Join TranData as TD
ON STM.SubmissionMonthStartDate = TD.SubmissionMonthStartDate
AND STM.TransMonthStartDate = TD.TranMonthStartDate
Group By
STM.SubmissionMonthStartDate,
STM.TransMonthStartDate,
STM.TranSummaryMonthNum
)
--#Inspect 1
--Select * From SubmissionAndTranMonths
--OPTION (MAXRECURSION 0)
--#Inspect 1 Results
--SubmissionMonthStartDate TransMonthStartDate TranSummaryMonthNum
--2012-01-01 2012-01-01 0
--2012-01-01 2012-02-01 1
--2012-01-01 2012-03-01 2
--2012-01-01 2012-04-01 3
--2012-02-01 2012-02-01 1
--2012-02-01 2012-03-01 2
--2012-02-01 2012-04-01 3
--2012-03-01 2012-03-01 2
--2012-03-01 2012-04-01 3
--2012-04-01 2012-04-01 3
--#Inspect 2
--Select * From AccountSummaryByMonth
--OPTION (MAXRECURSION 0)
--#Inspect 2 Results
--FiscalMonthStartDate TotalSubmissionAmount
--2012-01-01 2099.00
--2012-02-01 350.00
--2012-03-01 685.00
--#Inspect 3
--Select * From TranSummaryByMonth
--OPTION (MAXRECURSION 0)
--#Inspect 3 Results
--SubmissionMonthStartDate TransMonthStartDate TranSummaryMonthNum TotalTranAmount
--2012-01-01 2012-01-01 0 300.00
--2012-01-01 2012-02-01 1 300.00
--2012-01-01 2012-03-01 2 300.00
--2012-02-01 2012-02-01 1 325.00
--2012-02-01 2012-04-01 3 25.00
--2012-03-01 2012-03-01 2 656.00
--2012-03-01 2012-04-01 3 15.00
Select STM.SubmissionMonthStartDate,
ASM.TotalSubmissionAmount,
STM.TransMonthStartDate,
STM.TranSummaryMonthNum,
TSM.TotalTranAmount
From SubmissionAndTranMonths as STM
Inner Join AccountSummaryByMonth as ASM
ON STM.SubmissionMonthStartDate = ASM.FiscalMonthStartDate
Left Join TranSummaryByMonth AS TSM
ON STM.SubmissionMonthStartDate = TSM.SubmissionMonthStartDate
AND STM.TransMonthStartDate = TSM.TransMonthStartDate
Order By STM.SubmissionMonthStartDate, STM.TranSummaryMonthNum
OPTION (MAXRECURSION 0)
--#Results
--SubmissionMonthStartDate TotalSubmissionAmount TransMonthStartDate TranSummaryMonthNum TotalTranAmount
--2012-01-01 2099.00 2012-01-01 0 300.00
--2012-01-01 2099.00 2012-02-01 1 300.00
--2012-01-01 2099.00 2012-03-01 2 300.00
--2012-01-01 2099.00 2012-04-01 3 NULL
--2012-02-01 350.00 2012-02-01 1 325.00
--2012-02-01 350.00 2012-03-01 2 NULL
--2012-02-01 350.00 2012-04-01 3 25.00
--2012-03-01 685.00 2012-03-01 2 656.00
--2012-03-01 685.00 2012-04-01 3 15.00
The following query exactly duplicates the results of your final query in your own answer but takes no more than 1/30th the CPU (or better), plus is a whole lot simpler.
If I had the time & energy I am sure I could find even more improvements... my gut tells me I might not have to hit the Accounts table so many times. But in any case, it's a huge improvement and should perform very well even for very large result sets.
See the SqlFiddle for it.
WITH L0 AS (SELECT 1 N UNION ALL SELECT 1),
L1 AS (SELECT 1 N FROM L0, L0 B),
L2 AS (SELECT 1 N FROM L1, L1 B),
L3 AS (SELECT 1 N FROM L2, L2 B),
L4 AS (SELECT 1 N FROM L3, L2 B),
Nums AS (SELECT N = Row_Number() OVER (ORDER BY (SELECT 1)) FROM L4),
Anchor AS (
SELECT MinDate = DateAdd(month, DateDiff(month, '20000101', Min(SubmissionDate)), '20000101')
FROM dbo.Accounts
),
MNums AS (
SELECT N
FROM Nums
WHERE
N <= DateDiff(month,
(SELECT MinDate FROM Anchor),
(SELECT Max(TranDate) FROM dbo.Trans)
) + 1
),
A AS (
SELECT
AM.AccountMo,
Amount = Sum(A.Amount)
FROM
dbo.Accounts A
CROSS APPLY (
SELECT DateAdd(month, DateDiff(month, '20000101', A.SubmissionDate), '20000101')
) AM (AccountMo)
GROUP BY
AM.AccountMo
), T AS (
SELECT
AM.AccountMo,
TM.TranMo,
TotalTranAmount = Sum(T.TranAmount)
FROM
dbo.Accounts A
CROSS APPLY (
SELECT DateAdd(month, DateDiff(month, '20000101', A.SubmissionDate), '20000101')
) AM (AccountMo)
INNER JOIN dbo.Trans T
ON A.AccountID = T.AccountID
CROSS APPLY (
SELECT DateAdd(month, DateDiff(month, '20000101', T.TranDate), '20000101')
) TM (TranMo)
GROUP BY
AM.AccountMo,
TM.TranMo
)
SELECT
SubmissionStartMonth = A.AccountMo,
TotalSubmissionAmount = A.Amount,
M.TransMonth,
TransMonthNum = N.N - 1,
T.TotalTranAmount
FROM
A
INNER JOIN MNums N
ON N.N >= DateDiff(month, (SELECT MinDate FROM Anchor), A.AccountMo) + 1
CROSS APPLY (
SELECT TransMonth = DateAdd(month, N.N - 1, (SELECT MinDate FROM Anchor))
) M
LEFT JOIN T
ON A.AccountMo = T.AccountMo
AND M.TransMonth = T.TranMo
ORDER BY
A.AccountMo,
M.TransMonth;