Within a SQL Server 2012 database, I have a table with two columns customerid and date. I am interested in getting by year-month, a count of customers that have purchased in current month but not in prior 13 months. The table is extremely large so something efficient would be highly appreciated. Results table is shown after the input data. In essence, it is a count of customers that purchased in current month but not in prior 13 months (by year and month).
---input table-----
declare #Sales as Table ( customerid Int, date Date );
insert into #Sales ( customerid, date) values
( 1, '01/01/2012' ),
( 1, '04/01/2013' ),
( 1, '01/01/2014' ),
( 1, '01/01/2014' ),
( 1, '04/06/2014' ),
( 2, '04/01/2014' ),
( 3, '01/03/2012' ),
( 3, '01/03/2014' ),
( 4, '01/04/2012' ),
( 4, '04/04/2013' ),
( 5, '02/01/2010' ),
( 5, '02/01/2013' ),
( 5, '04/01/2014' )
select customerid, date
from #Sales;
---desired results ----
yearmth monthpurchasers monthpurchasernot13m
201002 1 1
201201 3 3
201302 1 1
201304 2 2
201401 2 1
201404 3 2
Thanks very much for looking at this!
Dev
You didn't provide the expected result, but I believe this is pretty close (at least logically):
;with g as (
select customerid, year(date)*100 + month(date) as mon
from #Sales
group by customerid, year(date)*100 + month(date)
),
x as (
select *,
count(*) over(partition by customerid order by mon
rows between 13 preceding and 1 preceding) as cnt
from g
),
y as (
select mon, count(*) as cnt from x
where cnt = 0
group by mon
)
select g.mon,
count(distinct(g.customerid)) as monthpurchasers,
isnull(y.cnt, 0) as cnt
from g
left join y on g.mon = y.mon
group by g.mon, y.cnt
order by g.mon
Tell me if this query helps. It extracts all the rows which meet your condition into a Table variable. Then, I use your query and join to this table.
declare #startDate datetime
declare #todayDate datetime
declare #tbl_Custs as Table(customerid int)
set #startDate = '04/01/2014' -- mm/dd/yyyy
set #todayDate = GETDATE()
insert into #tbl_Custs
-- purchased only this month
select customerid
from Sales
where ([date] >= #startDate and [date] <= #todayDate)
and customerid NOT in
(
-- purchased in past 13 months
select distinct customerid
from Sales
where ([date] >= DATEADD(MONTH,-13,[date])
and [date] < #startDate)
)
-- your query goes here
select year(date) as year
,month(date) as month
,count(distinct(c.customerid)) as monthpurchasers
from #tbl_Custs as c right join
Sales as s
on c.customerid = s.customerid
group by year(date) , month(date)
order by year(date) , month(date)
Below query will produce what you are looking for. I am not sure how performance will be on a big table (how big is your table?) but it is pretty straight forward so I think it will be ok. I simply calculate the 13 months earlier on CTE to find my sale window. Than join to the Sales table within that window / customer id and grouping records based on the unmatched records. You don't actually need 2 CTE's here you can do the DATEADD(mm,-13,date) on the join part of the second CTE but I thought it might be more clear this way.
P.S. If you need to change the time frame from 13 months to something else all you have to change is the DATEADD(mm,-13,date) this simply substracts 13 months from the date value.
Hope this helps or at least leads to a better solution
;WITH PurchaseWindow AS (
select customerid, date, DATEADD(mm,-13,date) minsaledate
FROM #Sales
), JoinBySaleWindow AS (
SELECT a.customerid, a.date,a.minsaledate,b.date earliersaledate
FROM PurchaseWindow a
LEFT JOIN #sales b ON a.customerid =b.customerid
--Find the sales for the customer within the last 13 months of original sale
AND b.date BETWEEN a.date AND a.minsaledate
)
SELECT DATEPART(yy,date) AS [year], DATEPART(mm, date) AS [month], COUNT(DISTINCT customerid) monthpurchases
FROM JoinBySaleWindow
--Exclude records where a sale within last 13 months occured
WHERE earliersaledate IS NULL
GROUP BY DATEPART(mm, date), DATEPART(yy,date)
Sorry about the typos they are fixed now.
Related
I am fairly new to SSIS, and now I have this requirement to exclude weekends in order to do a performance management. Now I have created a calendar and marked the weekends; what I am trying to do, using SSIS, is get the start and end date of every status and count how many weekends are there. I am kind of struggling to know which component to use to achieve this task.
So I have mainly two tables:
1- Table Calendar
2- Table History-Log
Calendar has the following columns:
1- ID
2- date
3- year
4- month
5- day of week
6- isweekend
History-Log has the following:
1- ID
2- Status
3- startdate
4- enddate
Your help is really appreciated.
I'm not an SSIS user, so apologies if this answer does not help, but if I wanted to get the result you describe, based on some test data:
DECLARE #Calendar TABLE (
ID INT,
[Date] DATETIME,
[Year] INT,
[Month] INT,
[DayOfWeek] VARCHAR(10),
IsWeekend BIT
)
DECLARE #HistoryLog TABLE (
ID INT,
[Status] INT,
StartDate DATETIME,
EndDate DATETIME
)
DECLARE #StartDate DATE = '20100101', #NumberOfYears INT = 10
DECLARE #CutoffDate DATE = DATEADD(YEAR, #NumberOfYears, #StartDate);
INSERT INTO #Calendar
SELECT ROW_NUMBER() OVER (ORDER BY d) AS ID,
d AS [Date],
DATEPART(YEAR,d) AS [Year],
DATEPART(MONTH,d) AS [Month],
DATENAME(WEEKDAY,d) AS [DayOfWeek],
CASE WHEN DATENAME(WEEKDAY,d) IN ('Saturday','Sunday') THEN 1 ELSE 0 END AS IsWeekend
FROM
(
SELECT d = DATEADD(DAY, rn - 1, #StartDate)
FROM
(
SELECT TOP (DATEDIFF(DAY, #StartDate, #CutoffDate))
rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
ORDER BY s1.[object_id]
) AS x
) AS y;
INSERT INTO #HistoryLog
SELECT 1, 3, '2016-01-05', '2016-01-20'
UNION
SELECT 2, 7, '2016-01-08', '2016-01-25'
UNION
SELECT 3, 4, '2016-01-01', '2016-02-03'
UNION
SELECT 4, 3, '2016-02-09', '2016-02-10'
I would use a query like this to return all of the HistoryLog records with a count of the number of weekend days between their StartDate and EndDate:
SELECT h.ID,
h.[Status],
h.StartDate,
h.EndDate,
COUNT(c.ID) AS WeekendDays
FROM #HistoryLog h
LEFT JOIN #Calendar c ON c.[Date] >= h.StartDate AND c.[Date] <= h.EndDate AND c.IsWeekend = 1
GROUP BY h.ID, h.[Status], h.StartDate, h.EndDate
ORDER BY 1
If you wanted to know the number of weekends, rather than the number of weekend days, we'd need to slightly amend this logic (and define how a range containing only one weekend day - or one starting on a Sunday and ending on a Saturday inclusive - should be handled). Assuming you just want to know how many distinct weekends are at least partially within the date range, you could do:
SELECT h.ID,
h.[Status],
h.StartDate,
h.EndDate,
COUNT(weekends.ID) AS Weekends
FROM #HistoryLog h
LEFT JOIN
(
SELECT c.ID,
c.[Date] AS SatDate,
DATEADD(DAY,1,c.[Date]) AS SunDate
FROM #Calendar c
WHERE c.[DayOfWeek] = 'Saturday'
) weekends ON h.StartDate BETWEEN weekends.SatDate AND weekends.SunDate
OR h.EndDate BETWEEN weekends.SatDate AND weekends.SunDate
OR (h.StartDate <= weekends.SatDate AND h.EndDate >= weekends.SunDate)
GROUP BY h.ID, h.[Status], h.StartDate, h.EndDate
Good Day! I am working on a chart where I need to display all the days of the current week to show the sales per Week. So far, I am able to display all the days of the current week, I'm just having a trouble in displaying the sales for each day of the week.Since there are no records in the database for the days of the week, it the TOTAL_SALES column should all return a Null value. Instead, it returns the total sales recorded in the database. Here is my Stored Procedure query so far.
WITH DAYSOFTHEWEEK AS
(
SELECT 0 DAY
UNION ALL
SELECT DAY + 1 FROM DAYSOFTHEWEEK WHERE DAY < 6
)
SELECT DATEADD(DAY, DAY, DATEADD(DAY, 2-DATEPART(WEEKDAY, CONVERT (date, GETDATE())), CONVERT (date, GETDATE()))) AS DAY_OF_THE_WEEK,
SUM([ORDER].NET_AMOUNT) AS TOTAL_SALES
FROM DAYSOFTHEWEEK, [ORDER]
GROUP BY DAYSOFTHEWEEK.DAY
I tried adding this condition statement,
WHERE DAYSOFTHEWEEK.DAY IN ([ORDER].ORDER_DATE)
But it returns this error
Operand type clash: date is incompatible with int
Can someone help me out on this?Is there a work around with the code that I already have? Thanks in advance!
What I think you're after is a SUM of each day's sales for the current week with NULL if there are no sales. The secret is to left join your date list onto your data:
-- Setup some fake sales data
WITH TestData(N, Order_Date, Net_Amount) AS (
SELECT 1 N, CAST(GETDATE() AS DATE) Order_Date, RAND() * 100 Net_Amount
UNION ALL
SELECT N+1 N, CAST(GETDATE()-N/5 AS DATE) Order_Date, RAND(CHECKSUM(NEWID())) * 100 Net_Amount FROM TestData
WHERE N < 20
)
SELECT TestData.Order_Date, TestData.Net_Amount INTO #Order FROM TestData
--Set the first day of the week (if required)
SET DATEFIRST 7 --Sunday
;WITH Days(N,DayOfTheWeek) AS (
SELECT 1 N, DATEADD(DAY, 1-DATEPART(WEEKDAY, GETDATE()), CONVERT(DATE,GETDATE())) DayOfTheWeek
UNION ALL
SELECT N+1 N,DATEADD(DAY, 1, DayOfTheWeek) DayOfTheWeek FROM Days
WHERE N < 7
)
SELECT d.DayOfTheWeek, SUM(Net_Amount) TotalAmount
FROM Days d
LEFT JOIN #Order ON d.DayOfTheWeek = Order_Date
GROUP BY d.DayOfTheWeek
DayOfTheWeek TotalAmount
------------ ----------------------
2016-08-07 219.036784917497
2016-08-08 273.319570812461
2016-08-09 271.148114731087
2016-08-10 194.780039228967
2016-08-11 NULL
2016-08-12 NULL
2016-08-13 NULL
Here is every day this week, starting at your datefirst date, which can be temporarily varied for the query with SET DATEFIRST if you need to have some other week start date
I think you have some sales table there that you haven't shown us, you need to join to that on date, then group by
WITH DAYSOFTHEWEEK AS
(
SELECT cast(dateadd(
day,
-datepart(weekday,getdate()) + 1 ,
GETDATE()
)
as date) [DAY], 0 as cnt
UNION ALL
SELECT dateadd(day,1,[DAY]), cnt + 1 FROM DAYSOFTHEWEEK WHERE cnt < 6
)
select DAYSOFTHEWEEK.[day], SUM([ORDER].NET_AMOUNT) AS TOTAL_SALES from daysoftheweek
JOIN
SalesTable on
CAST(SalesTable.SalesDate date) = DAYSOFTHEWEEK.[day]
GROUP BY DAYSOFTHEWEEK.[day]
A little over complicated for me:
To get name of the week use, for example
SELECT DATENAME(dw,getdate())
But you really need something like this:
SELECT ProductName,Sum(Sales) From NameOfTable GROUP BY
DATENAME(ww,salesDate)
*Edit (Hopefully to be more clear)
Table below, I would like to count ids and count duplicate ids where the createddate has a gap of 3 months or more for that ID.
Query I have so far...
if object_id('tempdb..#temp') is not null
begin drop table #temp end
select
top 100
a.id, a.CreatedDate
into #temp
from tbl a
where 1=1
--and year(CreatedDate) = '2015'
if object_id('tempdb..#temp2') is not null
begin drop table #temp2 end
select t.id, count(t.id) as Total_Cnt
into #temp2
from #temp t
group by id
select distinct #temp2.Total_Cnt, #temp2.id, #temp.CreatedDate, DENSE_RANK() over (partition by #temp.id order by createddate) RK
from #temp2
inner join #temp on #temp2.id = #temp.id
where 1=1
order by Total_Cnt desc
Results:
Total_cnt id createddate rk
3 1 01-01-2015 1
3 1 03-02-2015 2
3 1 01-02-2015 3
2 2 05-01-2015 1
2 2 05-02-2015 2
1 3 06-01-2015 1
1 4 07-01-2015 1
Count ids and only count duplicate ids when the createddate from the id is greater than 3 months.
Something like this...
Total_cnt id Countwith3monthgap
3 1 2
2 2 1
1 3 1
1 4 1
You can use a cte and ROW_NUMBER to get your order and self join the cte based on the order..
WITH cte AS
( SELECT
*,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY CreatedDate) Rn
FROM
Test
)
SELECT
c1.ID,
COUNT(CASE WHEN c2.CreatedDate IS NULL THEN 1
WHEN c1.CreatedDate >= DATEADD(month,3,c2.CreatedDate) THEN 1
END)
FROM
cte c1
LEFT JOIN cte c2 ON c1.ID = c2.ID
AND c1.RN = c2.RN + 1
GROUP BY
c1.ID
You also need to use a conditional count where the Previous CreatedDate is null or if the Current CreatedDate is >= the Previous CreatedDate + 3 months
If you happen to be using SQL 2012+ you can also use LAG here to get the same result
SELECT
ID,
COUNT(*)
FROM
(SELECT
ID,
CreatedDate CurrentDate,
LAG(CreatedDate) OVER (PARTITION BY ID ORDER BY CreatedDate) PreviousDate
FROM
Test
) T
WHERE
PreviousDate IS NULL
OR CurrentDate >= DATEADD(month, 3, PreviousDate)
GROUP BY
ID
You can use a lag to get the previous date, Null for the first in the list
SELECT
id,
lag(CreatedDate,1) OVER (PARTITION BY Id ORDER BY CreatedDate) AS PreviousCreateDate,
CreatedDate
FROM #t
You can use that as a subquery and get the difference in months using DATEDIFF
SELECT sub.id,DATEDiff(month, sub.PreviousCreateDate ,sub.CreatedDate)
FROM (SELECT
id,
lag(CreatedDate,1) OVER (PARTITION BY Id ORDER BY CreatedDate) AS PreviousCreateDate,
CreatedDate
FROM #t) sub
WHERE DATEDiff(month, sub.PreviousCreateDate ,sub.CreatedDate) >=3
OR sub.PreviousCreateDate IS NULL
You can then take your totals
SELECT sub.id,COUNT(sub.id) as cnt
FROM (SELECT
id,
lag(CreatedDate,1) OVER (PARTITION BY Id ORDER BY CreatedDate) AS PreviousCreateDate,
CreatedDate
FROM #t) sub
WHERE DATEDIFF(month, sub.PreviousCreateDate ,sub.CreatedDate) >=3
OR sub.PreviousCreateDate IS NULL
GROUP BY sub.id
Note that using datediff the last day of january is three months before the first day of march. That appears to be the logic you were after.
You might want to define your three month gap criteria as
WHERE sub.PreviousCreateDate <= DATEADD(month, -3, sub.CreatedDate)
OR sub.PreviousCreateDate IS NULL
or
WHERE sub.CreatedDate >= DATEADD(month, +3, sub.PreviousCreateDate )
OR sub.PreviousCreateDate IS NULL
I'm guessing that your desired definition of three-month gap doesn't coincide with datediff()'s. Most of the logic here is to look back at the previous date and decide if the gap is big enough to qualify.
When datediff() counts three months difference we still need to make sure the day of month is later than the first one (per example and ID 5). If difference is more than three months then we're good automatically.
But I'm also assuming that you would want to treat the distance from November 30th to February 28th (or 29th in a leap year) as a full three months because the end date falls on the final day of the month. By adjusting the end date by an extra day this is an easy scenario to snag as it will bump the date into the following month and increase the month difference by one as well. If that's not what you want then just remove the dateadd(day, 1, ...) portion and use only the raw CreatedDate value.
You sample data is limited so I'm also making the assumption that the gaps are measure between consecutive dates. If you're wanting to find blocks of runs that don't span more than three months across the set, then that's a different problem and you should clarify with more information.
Since you've indicated that you're probably on SQL Server 2008 you'll have to do without the lag() function. Although the first query could be adjusted for that it's likely easier to go with the second approach at the end.
with diffs as (
select
ID,
row_number() over (partition by ID order by CreatedDate) as RN,
case when
datediff(
month,
lag(CreatedDate, 1) over (partition by ID order by CreatedDate),
CreatedDate
) = 3
and
datepart(
day,
lag(CreatedDate, 1) over (partition by ID order by CreatedDate)
) <= datepart(day, CreatedDate)
or
datediff(
month,
lag(CreatedDate, 1) over (partition by ID order by CreatedDate),
/* adding one day to handle gaps like Nov30 - Feb28/29 and Jan31 - Apr30 */
dateadd(day, 1, CreatedDate)
) >= 4
then 1
else 0
end as GapFlag
from <T> /* <--- your table name here */
), gaps as (
select
ID, RN,
sum(1 + GapFlag) over (partition by ID order by RN) as Counter
from diffs
)
select ID, count(distinct Counter - RN) as "Count"
from gaps
group by ID
The rest of the logic is a typical gaps and islands scenario looking for holes in the sum(1 + GapCount) sequence with the offset of 1 acting pretty much like row_number().
http://sqlfiddle.com/#!6/61b12/3
JamieD77's approach is also valid. I was originally thinking your problem involved more than looking at the rows in sequence. Here's how I would tweak it for the gap definition I've been running with:
with data as (
select ID, CreatedDate, row_number() over (partition by ID order by CreatedDate) as RN
from T
)
select ID, count(*) as "Count"
from data d1 left outer join data d0
on d0.ID = d1.ID and d0.RN = d1.RN - 1 /* connect to the one before */
where
datediff(month, d0.CreatedDate, d1.CreatedDate) = 3
and datepart(day, d0.CreatedDate) <= datepart(day, d0.CreatedDate)
or datediff(month, d0.CreatedDate, dateadd(day, 1, d0.CreatedDate)) >= 4
or d0.ID is null
group by ID
Edit: You have changed the question since yesterday.
Change this line in the first query to include the total count:
...
select count(*) as TotalCnt, ID, count(distinct Counter - RN) as GapCount
...
Second would look like:
with data as (
select ID, CreatedDate, row_number() over (partition by ID order by CreatedDate) as RN
from T
)
select
count(*) as TotalCnt, ID,
count(case when
datediff(month, d0.CreatedDate, d1.CreatedDate) = 3
and datepart(day, d0.CreatedDate) <= datepart(day, d0.CreatedDate)
or datediff(month, d0.CreatedDate, dateadd(day, 1, d0.CreatedDate)) >= 4
or d0.ID is null then 1 end
) as GapCount
from data d1 left outer join data d0
on d0.ID = d1.ID and d0.RN = d1.RN - 1 /* connect to the one before */
where
group by ID
I have an query that I'm feeling out-of-my depth with.
I need to loop through months between two dates and return a subset of data for each month with a blank row for months with no data.
For example:
TransactionID | Date | Value
1 | 01/01/2015 | £10
2 | 16/01/2015 | £15
3 | 21/01/2015 | £5
4 | 15/03/2015 | £20
5 | 12/03/2015 | £15
6 | 23/04/2015 | £10
Needs to return:
Month | Amount
January | £30
February | £0
March | £35
April | £10
My query will rely on specifying a date range so I can set the first and last date of the query.
I feel like I maybe over thinking this, but have gotten to that stage where you start to feel like you tying yourself in knots.
The key is having access to a list of integers to represent the months in the range. If you don't have a Numbers Table, then spt_values will do in a pinch.
SqlFiddle Demo
SELECT
[Year] = YEAR(DATEADD(month,[i],#range_start))
,[Month] = DATENAME(month,DATEADD(month,[i],#range_start))
,[Amount] = ISNULL(SUM([Value]),0)
FROM (
SELECT TOP (DATEDIFF(month,#range_start,#range_end)+1)
ROW_NUMBER() OVER(ORDER BY (SELECT 1))-1 [i]
FROM master.dbo.spt_values
) t1
LEFT JOIN #MyTable t2
ON (t1.[i] = DATEDIFF(month,#range_start,t2.[Date]) )
GROUP BY [i]
ORDER BY [i]
SQL is a tricky language at first. You actually do not want a loop. In fact, you pretty much never want to loop in SQL except in very few cases. Try this out:
DECLARE #StartDate DATE,
#EndDate DATE;
SET #StartDate = '01 January 2015';
SET #EndDate = '30 April 2015';
WITH CTE_Months
AS
(
SELECT #StartDate dates
UNION ALL
SELECT DATEADD(MONTH,1,dates)
FROM CTE_Months
WHERE DATEADD(MONTH,1,dates) < #EndDate
)
SELECT YEAR(B.[date]) AS yr,
DATENAME(MONTH,B.[Date]) AS month_name,
SUM(ISNULL(B.Value,0)) AS Amount
FROM CTE_Months A
LEFT JOIN yourTable B
ON YEAR(A.[date]) = YEAR(B.[date])
AND MONTH(A.[date]) = MONTH(B.[date])
GROUP BY YEAR(B.[date]),DATENAME(MONTH,B.[Date])
One way: create a table called months with a monthnum int field and 12 rows of [1..12]
declare #start date = '01 jan 2015',
#end date = '30 apr 2015'
select
datename(month, dateadd(month, monthnum, 0) - 1),
isnull(Amount, 0)
from months
left join (
select
month(date) Month,
sum(Value) Amount
from tbl
where date between #start and #end
group by month(date)
) T on (T.Month = months.monthnum)
where months.monthnum between month(#start) and month(#end)
order by monthnum
The following code will generate one output row for each month between the first and last transaction dates. Spanning a year boundary, or multiple years, is handled correctly.
-- Some sample data.
declare #Transactions as Table
( TransactionId Int Identity, TransactionDate Date, Value Int );
insert into #Transactions ( TransactionDate, Value ) values
( '20141125', 10 ), ( '20150311', 20 ), ( '20150315', 5 ), ( '20150509', 42 );
select * from #Transactions;
with
-- Determine the first and last dates involved.
Range as (
select Min( TransactionDate ) as FirstDate, Max( TransactionDate ) as LastDate
from #Transactions ),
-- Generate a set of all of the months in the range.
Months as (
select DateAdd( month, DateDiff( month, 0, FirstDate ), 0 ) as Month,
DateAdd( month, DateDiff( month, 0, LastDate ), 0 ) as LastMonth
from Range
union all
select DateAdd( month, 1, Month ), LastMonth
from Months
where Month < LastMonth )
-- Summarize the transactions.
select M.Month, Coalesce( Sum( T.Value ), 0 ) as Total
from Months as M left outer join
#Transactions as T on DateAdd( month, DateDiff( month, 0, T.TransactionDate ), 0 ) = M.Month
group by M.Month
order by M.Month
option ( MaxRecursion 1000 );
I have my database table ABC as shown below :
ItemId Month Year Sales
1 1 2013 333
1 2 2013 454
2 1 2013 434
and so on .
I would like to write a query to find the top 3 items that have had the highest increase in sales from last month to this month , so that I see somethinglike this in the output.
Output :
ItemId IncreaseInSales
1 +121
9 +33
6 +16
I came up to here :
select
(select Sum(Sales) from ABC where [MONTH] = 11 )
-
(select Sum(Sales) from ABC where [MONTH] = 10)
I cannot use a group by as it is giving an error . Can anyone point me how I can
proceed further ?
Assuming that you want the increase for a given month, you can also do this with an aggregation query:
select top 3 a.ItemId,
((sum(case when year = #YEAR and month = #MONTH then 1.0*sales end) /
sum(case when year = #YEAR and month = #MONTH - 1 or
year = #YEAR - 1 and #Month = 1 and month = 12
then sales end)
) - 1
) * 100 as pct_increase
from ABC a
group by a.ItemId
order by pct_increase desc;
You would put the year/month combination you care about in the variables #YEAR and #MONTH.
EDIT:
If you just want the increase, then do a difference:
select top 3 a.ItemId,
(sum(case when year = #YEAR and month = #MONTH then 1.0*sales end) -
sum(case when year = #YEAR and month = #MONTH - 1 or
year = #YEAR - 1 and #Month = 1 and month = 12
then sales
end)
) as difference
from ABC a
group by a.ItemId
order by difference desc;
Here is the SQL Fiddle that demonstrates the below query:
SELECT TOP(3) NewMonth.ItemId,
NewMonth.Month11Sales - OldMonth.Month10Sales AS IncreaseInSales
FROM
(
SELECT s1.ItemId, Sum(s1.Sales) AS Month11Sales
FROM ABC AS s1
WHERE s1.MONTH = 11
AND s1.YEAR = 2013
GROUP BY s1.ItemId
) AS NewMonth
INNER JOIN
(
SELECT s2.ItemId, Sum(s2.Sales) AS Month10Sales
FROM ABC AS s2
WHERE s2.MONTH = 10
AND s2.YEAR = 2013
GROUP BY s2.ItemId
) AS OldMonth
ON NewMonth.ItemId = OldMonth.ItemId
ORDER BY NewMonth.Month11Sales - OldMonth.Month10Sales DESC
You never mentioned if you could have more than one record for an ItemId with the same Month, so I made the query to handle it either way. Obviously you were lacking the year = 2013 in your query. Once you get past this year you will need that.
Another option could be something on these lines:
SELECT top 3 a.itemid, asales-bsales increase FROM
(
(select itemid, month, sum(sales) over(partition by itemid) asales from ABC where month=2
and year=2013) a
INNER JOIN
(select itemid, month, sum(sales) over(partition by itemid) bsales from ABC where month=1
and year=2013) b
ON a.itemid=b.itemid
)
ORDER BY increase desc
if you need to cater for months without sales then you can do a FULL JOIN and calculate increase as isnull(asales,0) - isnull(bsales,0)
You could adapt this solution based on PIVOT operator:
SET NOCOUNT ON;
DECLARE #Sales TABLE
(
ItemID INT NOT NULL,
SalesDate DATE NOT NULL,
Amount MONEY NOT NULL
);
INSERT #Sales (ItemID, SalesDate, Amount)
VALUES
(1, '2013-01-15', 333), (1, '2013-01-14', 111), (1, '2012-12-13', 100), (1, '2012-11-12', 150),
(2, '2013-01-11', 200), (2, '2012-12-10', 150), (3, '2013-01-09', 900);
-- Parameters (current year & month)
DECLARE #pYear SMALLINT = 2013,
#pMonth TINYINT = 1;
DECLARE #FirstDayOfCurrentMonth DATE = CONVERT(DATE, CONVERT(CHAR(4), #pYear) + '-' + CONVERT(CHAR(2), #pMonth) + '-01');
DECLARE #StartDate DATE = DATEADD(MONTH, -1, #FirstDayOfCurrentMonth), -- Begining of the previous month
#EndDate DATE = DATEADD(DAY, -1, DATEADD(MONTH, 1, #FirstDayOfCurrentMonth)) -- End of the current month
SELECT TOP(3) t.ItemID,
t.[2]-t.[1] AS IncreaseAmount
FROM
(
SELECT y.ItemID, y.Amount,
DENSE_RANK() OVER(ORDER BY y.FirstDayOfSalesMonth ASC) AS MonthNum -- 1=Previous Month, 2=Current Month
FROM
(
SELECT x.ItemID, x.Amount,
DATEADD(MONTH, DATEDIFF(MONTH, 0, x.SalesDate), 0) AS FirstDayOfSalesMonth
FROM #Sales x
WHERE x.SalesDate BETWEEN #StartDate AND #EndDate
) y
) z
PIVOT( SUM(z.Amount) FOR z.MonthNum IN ([1], [2]) ) t
ORDER BY IncreaseAmount DESC;
SQLFiddle demo
Your sample data seems to be incomplete, however, here is my try. I assume that you want to know the three items with the greatest sales-difference from one month to the next:
WITH Increases AS
(
SELECT a1.itemid,
a1.sales - (SELECT a2.sales
FROM dbo.abc a2
WHERE a1.itemid = a2.itemid
AND ( ( a1.year = a2.year
AND a1.month > 1
AND a1.month = a2.month + 1 )
OR ( a1.year = a2.year + 1
AND a1.month = 1
AND a2.month = 12 ) ))AS IncreaseInSales
FROM dbo.abc a1
)
SELECT TOP 3 ItemID, MAX(IncreaseInSales) AS IncreaseInSales
FROM Increases
GROUP BY ItemID
ORDER BY MAX(IncreaseInSales) DESC
Demo
SELECT
cur.[ItemId]
MAX(nxt.[Sales] - cur.[Sales]) AS [IncreaseInSales]
FROM ABC cur
INNER JOIN ABC nxt ON (
nxt.[Year] = cur.[Year] + cur.[month]/12 AND
nxt.[Month] = cur.[Month]%12 + 1
)
GROUP BY cur.[ItemId]
I'd do this this way. It should work in all the tagged versions of SQL Server:
SELECT TOP 3 [ItemId],
MAX(CASE WHEN [Month] = 2 THEN [Sales] END) -
MAX(CASE WHEN [Month] = 1 THEN [Sales] END) [Diff]
FROM t
WHERE [Month] IN (1, 2) AND [Year] = 2013
GROUP BY [ItemId]
HAVING COUNT(*) = 2
ORDER BY [Diff] DESC
Fiddle here.
The reason why I'm adding the HAVING clause is that if any item is added in only one of the months then the numbers will be all wrong. So I'm only comparing items that are only present in both months.
The reason of the WHERE clause would be to filter in advance only the needed months and improve the efficiency of the query.
An SQL Server 2012 solution could also be:
SELECT TOP 3 [ItemId], [Diff] FROM (
SELECT [ItemId],
LEAD([Sales]) OVER (PARTITION BY [ItemId] ORDER BY [Month]) - [Sales] Diff
FROM t
WHERE [Month] IN (1, 2) AND [Year] = 2013
) s
WHERE [Diff] IS NOT NULL
ORDER BY [Diff] DESC