Row values check in consecutive rows using SQL Server - sql

I have a data set as below. I want to check the row values consecutive in L/P column one by one .
As an example for the tradedate between 2019-02-12 and 2019-02-14 L/P return values were 1 0 0. for that same pattern there were records for tradedate between 2019-02-16 and 2019-02-18.
How do I check that same pattern in L/P column using SQL Server?
tradedate price lost / profit L/P
---------- ----- ------------ ----
2019-02-11 150.00 0.00 1
2019-02-12 330.00 180.00 1
2019-02-13 329.00 -1.00 0
2019-02-14 151.00 -178.00 0
2019-02-15 148.00 -3.00 0
2019-02-16 329.00 181.00 1
2019-02-17 326.00 -3.00 0
2019-02-18 146.00 -180.00 0
The DDL statement for table creation
CREATE TABLE dbo.Ticker
(
symbol VARCHAR(10) NOT NULL,
tradedate DATE NOT NULL,
price NUMERIC(12, 2) NOT NULL,
CONSTRAINT PK_Ticker
PRIMARY KEY (symbol, tradedate)
);
GO
INSERT INTO dbo.Ticker(symbol, tradedate, price)
VALUES
('STOCK1', '20190211', 150.00),
('STOCK1', '20190212', 330.00),
('STOCK1', '20190213', 329.00),
('STOCK1', '20190214', 151.00),
('STOCK1', '20190215', 148.00),
('STOCK1', '20190216', 329.00),
('STOCK1', '20190217', 326.00),
('STOCK1', '20190218', 146.00);
SQL query for getting above data set
SELECT
tradedate,
price,
((LAG(price, 1, price) OVER (ORDER BY tradedate)) - price) * -1 AS 'lost / profit',
CASE WHEN ((LAG(price, 1, 0) OVER (ORDER BY tradedate)) - price) < 0 THEN 1 ELSE 0 END AS 'L / P'
FROM
Ticker t
WHERE
tradedate BETWEEN '2019-02-11' AND '2019-02-18'
````

Try using a aggregate function like STRING_AGG():
(UPDATED):
SELECT
STRING_AGG([L / P], ' ')
FROM
(SELECT tradedate
,price
,((LAG(price, 1, price) OVER (ORDER BY tradedate)) - price) * -1 AS 'lost / profit'
,CASE WHEN ((LAG(price, 1, 0) OVER (ORDER BY tradedate)) - price) < 0 THEN 1 ELSE 0 END AS 'L / P'
FROM Ticker t
WHERE tradedate BETWEEN '2019-02-12' AND '2019-02-14')
EXCEPT
SELECT
STRING_AGG([L / P], ' ')
FROM
(SELECT tradedate
,price
,((LAG(price, 1, price) OVER (ORDER BY tradedate)) - price) * -1 AS 'lost / profit'
,CASE WHEN ((LAG(price, 1, 0) OVER (ORDER BY tradedate)) - price) < 0 THEN 1 ELSE 0 END AS 'L / P'
FROM Ticker t
WHERE tradedate BETWEEN '2019-02-16' AND '2019-02-18')

Related

How to distribute sales with partitions

I have 2 tables:
1st table columns: ItemCode int, Amount float (I have over 1000 ItemCodes)
2nd table columns: ItemCode int, SoldAmount float, Price float (I have over 10000 sale rows for different items)
Example:
ItemId 1528's Amount in 1st table is 244. That items sales in the 2nd table is as below:
Sale 1 Amount = 120, Price = 10
Sale 2 Amount = 120, Price = 30
Sale 3 Amount = 100, Price = 20
Sale 4 Amount = 10, Price = 25
ItemCode
Amount
1528
244
1530
150
ItemCode
Date
Amount
Price
1528
2021.11.01
120
10
1530
2021.10.01
120
30
1528
2021.09.01
100
20
1530
2021.08.01
10
25
Tried cursor and loop , but no desired output.
The desired outcome is to distribute that 100 amount with the sales above like following:
Sale 1 Amount 60: 100 - 60 = 40 with price 5 --- So we continue to the next row and subtract whatever is left
Sale 2 Amount 30: 40 - 30 = 10 with price 6 --- So we continue to the next row and subtract whatever is left
Sale 3 Amount 20: 10 - 20 = -10 with price 7 --- So we stop here as the amount is equal to 0 or below .
As the result we should get this:
60 * 5 = 300
30 * 6 = 180
10 * 7 = 70 (that 10 is derived from whatever could be subtracted before it hits 0)
Desired table as below
ItemCode
Date
Amount
Price
1528
2021.11.01
120
10
1528
2021.10.01
120
30
1528
2021.09.01
4
20
My last attempt was as below
WITH CTE AS (
SELECT ItemCode, SUM(Amount) AS Amount
FROM table 1
GROUP BY STOCKREF )
SELECT *,
IIF(LAG(table1.Amount - table2.amount) OVER (PARTITION BY table1.Amount ORDER BY Date DESC) IS NULL, table1.Amount - table2.amount,
LAG(table1.Amount - table2.amount) OVER (PARTITION BY CTE.ItemCode ORDER BY Date DESC) - table2.AMOUNT) AS COL
FROM CTE JOIN (SELECT ItemCode, DATE_, AMOUNT, PRICE FROM table2) table 2 ON table1.ItemCode = table2.Amount
Hopefully this addresses the right question - if you're trying to create a running total per item_code, deducting the sale quantity from starting inventory from first-to-last sale, maybe this would work:
CREATE TABLE #items (item_code INT,
item_amount INT);
INSERT INTO #items (item_code, item_amount)
VALUES (1528, 244);
INSERT INTO #items (item_code, item_amount)
VALUES (1529, 240);
CREATE TABLE #sales (item_code INT,
sale_date DATE,
sale_amount INT,
sale_price DECIMAL(12,2));
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-12-01', 50, 5);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-11-29', 120, 6.76292);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-11-15', 120, 6.6453);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1528, '2021-11-01', 100, 6.96875);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-30', 48, 7.2);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-18', 48, 3.5);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-09', 96, 3.9);
INSERT INTO #sales (item_code, sale_date, sale_amount, sale_price)
VALUES (1529, '2021-11-05', 96, 3.75);
;WITH all_sales_with_running_totals AS ( --Calculate the running total of each item, deducting sale amount from total starting amount, in order of first sale to last
SELECT s.item_code,
s.sale_date,
s.sale_price,
i.item_amount AS starting_amount,
s.sale_amount,
i.item_amount - SUM(sale_amount) OVER(PARTITION BY s.item_code
ORDER BY s.sale_date
ROWS UNBOUNDED PRECEDING
) AS running_sale_amount
FROM #sales AS s
JOIN #items AS i ON s.item_code = i.item_code
),
sales_with_prev_running_total AS ( --Add the previous rows' running total, to assist with the final calculation
SELECT item_code,
sale_date,
sale_price,
starting_amount,
sale_amount,
running_sale_amount,
LAG(running_sale_amount, 1, NULL) OVER(PARTITION BY item_code
ORDER BY sale_date
)AS prev_running_sale_amount
FROM all_sales_with_running_totals
)
SELECT item_code, --Return the final running sale amount for each sale - if the inventory has already run out, return null. If there is insufficient inventory to fill the order, fill it with the qty remaining. Otherwise, fill the entire order.
sale_date,
sale_price,
starting_amount,
sale_amount,
running_sale_amount,
prev_running_sale_amount,
CASE WHEN prev_running_sale_amount <= 0
THEN NULL
WHEN running_sale_amount < 0
THEN prev_running_sale_amount
ELSE sale_amount
END AS result_sale_amount
FROM sales_with_prev_running_total;

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().

SQL - Same Table join to calculate profit from last entry

I have a table of transactions for various products. I want to calculate the profit made on each
Product Date Profit Incremental Profit
--------------------- --------------------------- -----------
Apple 2016-05-21 100
Banana 2016-05-21 60
Apple 2016-06-15 30
Apple 2016-08-20 10
Banana 2016-08-20 5
Can I create a SQL query that can group based on product and give me incremental profit on every date for each product. For example on 21-05-2015 since it is first date so incremental profit will be 0. But on 15-06-2016 it will be -70 (30-100).
The expected output is:
Product Date Profit Incremental Profit
--------------------- --------------------------- -----------
Apple 2016-05-21 100 0
Banana 2016-05-21 60 0
Apple 2016-06-15 30 -70
Apple 2016-08-20 10 -20
Banana 2016-08-20 5 -55
maybe u can use this.
select
a.product
,a.date
,a.profit
,isnull(a.profit - (select top 1 x.profit from profit x where x.product = a.product and x.date < a.date),0) as profit
from PROFIT a
order by product, date
Try this
DECLARE #Tbl TABLE (Product NVARCHAR(50), Date_ DATETIME, Profit INT)
INSERT INTO #Tbl
VALUES
('Apple' , '2016-05-21', 100),
('Banana', '2016-05-21', 60 ),
('Apple', '2016-06-15', 30 ),
('Apple', '2016-08-20', 10 ),
('Banana', '2016-08-20', 5 )
;WITH CTE
AS
(
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY Product ORDER BY Date_) RowId
FROM #Tbl
)
SELECT
CurrentRow.Product ,
CurrentRow.Date_ ,
CurrentRow.Profit ,
CurrentRow.Profit - ISNULL(PrevRow.Profit, CurrentRow.Profit) 'Incremental Profit'
FROM
CTE CurrentRow LEFT JOIN
(SELECT CTE.Product ,CTE.Profit, CTE.RowId + 1 RowId FROM CTE) PrevRow ON CurrentRow.Product = PrevRow.product AND
CurrentRow.RowId = PrevRow.RowId
ORDER BY CurrentRow.Date_
Result:
Product Date_ Profit Incremental Profit
Apple 2016-05-21 100 0
Banana 2016-05-21 60 0
Apple 2016-06-15 30 -70
Apple 2016-08-20 10 -20
Banana 2016-08-20 5 -55
Edit:
UPDATE #Tbl
SET [Incremental Profit] = A.[Incremental Profit]
FROM
(
SELECT
CurrentRow.Product ,
CurrentRow.Date_ ,
CurrentRow.Profit ,
CurrentRow.Profit - ISNULL(PrevRow.Profit, CurrentRow.Profit) 'Incremental Profit'
FROM
(SELECT *, ROW_NUMBER() OVER (PARTITION BY Product ORDER BY Date_) RowId FROM #Tbl) CurrentRow LEFT JOIN
(SELECT *, ROW_NUMBER() OVER (PARTITION BY Product ORDER BY Date_) + 1 RowId FROM #Tbl) PrevRow ON CurrentRow.Product = PrevRow.Product AND
CurrentRow.RowId = PrevRow.RowId
) A
WHERE
[#Tbl].Product = A.Product AND
[#Tbl].Date_ = A.Date_

SQL query to calculate interval discount

I have trouble understanding how I can solve this problem with a T-SQL query.
I have a price column and a volume column. In another table a have discounts at different levels of volume. So my discount table could have values as
(StartLevel, DiscountFactor)
(0, 1);
(25, 0.95);
(50, 0.90);
(100, 0.75)
What I want is to calculate a total price. If Volume is 35, I want it to multiply
Price x ((35-25) x 0.95 + (25-0) x 1)
If the volume is 200, it should be
Price x ((200-100) x 0.75 + (100-50) x .9+(50-25) x .95+(25) x 1)
Can anybody help me with a query that solves this?
This can help:
DECLARE #products TABLE
(
id INT ,
price MONEY ,
volume INT
)
DECLARE #discounts TABLE
(
id INT ,
Level INT ,
Factor MONEY
)
INSERT INTO #products
VALUES ( 1, 10, 35 ),
( 2, 15, 200 )
INSERT INTO #discounts
VALUES ( 1, 0, 1 ),
( 2, 25, 0.95 ),
( 3, 50, 0.90 ),
( 4, 100, 0.75 )
SELECT p.id, p.price * SUM(ca.m)
FROM #products p
CROSS APPLY ( SELECT * ,
Factor * ( -Level + LEAD(Level) OVER ( PARTITION BY p.id ORDER BY Level, d ) ) AS m
FROM ( SELECT 1 AS d ,
Level ,
Factor
FROM #discounts
WHERE Level < p.volume
UNION ALL
SELECT 2 AS d ,
p.volume ,
0
) t
) ca
GROUP BY p.id, p.price
Without grouping it returns:
id price volume d Level Factor m
1 10.00 35 1 0 1.00 25.00
1 10.00 35 1 25 0.95 9.50
1 10.00 35 2 35 0.00 NULL
2 15.00 200 1 0 1.00 25.00
2 15.00 200 1 25 0.95 23.75
2 15.00 200 1 50 0.90 45.00
2 15.00 200 1 100 0.75 75.00
2 15.00 200 2 200 0.00 NULL
Then just group by product and sum of m results in:
id Total
1 345.00
2 2531.25
For a given Volume and Price you can get the discount based on interval using LEAD which is available in SQL Server 2012+ onwards.
Sample Data
DECLARE #PriceTable TABLE(Volume INT,Price DECIMAL(9,2) )
DECLARE #Discount TABLE(StartLevel int, DiscountFactor DECIMAL(9,2))
INSERT INTO #PriceTable
VALUES(75, 20.5),
(150, 20),
(250, 20.5),
(0, 15);
INSERT INTO #Discount
VALUES(0, 1),
(25, 0.95),
(50, 0.90),
(100, 0.75);
Query
SELECT Volume,Price,FinalPrice
FROM #PriceTable P
CROSS APPLY(
SELECT SUM(CASE WHEN (MaxLevel >=StartLevel) THEN (MaxLevel-StartLevel) ELSE 0 END *DiscountFactor)*P.Price as FinalPrice
FROM
(
SELECT CASE WHEN LEAD(StartLevel)OVER(ORDER BY StartLevel) < P.Volume THEN LEAD(StartLevel)OVER(ORDER BY StartLevel) ELSE P.Volume END MaxLevel,StartLevel, DiscountFactor
FROM #Discount
) IQ
)T
Output
Volume Price FinalPrice
75 20.50 1460.6250
150 20.00 2625.0000
250 20.50 4228.1250
0 15.00 0.0000

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;