sql query to find the Items with the highest difference - sql-server

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

Related

Select N rows before and N rows after the record

I have a table where some values are stored for months and years.
Example:
Month | Year | Value
1 | 2013 | 1.86
2 | 2013 | 2.25
3 | 2013 | 2.31
...
3 | 2016 | 1.55
4 | 2016 | 1.78
Month and Year combination is a complex primary key. It is guaranteed that all values for all past years exist in the table.
User can select specific month and specific year. Let's say user selected 2014 as year and 6 as month, I need to show 15 rows before and 15 rows after the selected combination.
But if there are not enough rows (less than 15) after the selected combination than I need to get more rows before.
Basically all i need is to return 31 rows (always 31 unless there are not enough rows in the entire table) of data where the selected combination will be as close as possible to the center.
What is the proper way to do that?
Currently I'm stuck with this:
;WITH R(N) AS
(
SELECT 0
UNION ALL
SELECT N+1
FROM R
WHERE N < 29
)
SELECT * FROM MyTable e
LEFT OUTER JOIN (
SELECT N, MONTH(DATEADD(MONTH,-N,iif(#year != Year(GETDATE()), DATEFROMPARTS(#year, 12, 31) ,GETDATE()))) AS [Month],
YEAR(DATEADD(MONTH,-N,iif(#year!= Year(GETDATE()), DATEFROMPARTS(#year, 12, 31) ,GETDATE()))) AS [Year]
FROM R) s
ON s.[Year] = e.[Year] AND s.[Month] = e.[Month]
WHERE s.[N] is not null
This is not really what I want to do, since it just cuts off next year months
How about something simple like this:
;WITH CTE AS (
SELECT Month
,Year
,Value
,ROW_NUMBER() OVER (ORDER BY Year, Month) rn
FROM MyTable
)
SELECT Month
,Year
,Value
FROM CTE
WHERE rn >= (SELECT rn - 15 FROM MyTable WHERE Year = #Year AND Month = #Month)
AND rn <= (SELECT rn + 15 FROM MyTable WHERE Year = #Year AND Month = #Month);
I'm sure there's a more efficient way to do it, but this strikes me as the most maintainable way to do it. It should even work when you pick a value close to the first or last records in the table.
I can't tell if you want 31 rows no matter what. At one point it sounds like you do, and at another point it sounds like you don't.
EDIT: Ok, so you do always want 31 rows if available.
Alright, try this:
;WITH CTE AS (
SELECT Month
,Year
,Value
,ROW_NUMBER() OVER (ORDER BY Year, Month) rn
FROM MyTable
),
CTE_2 AS (
SELECT TOP (31) Month
,Year
,Value
FROM CTE
ORDER BY ABS(rn - (SELECT rn FROM MyTable WHERE Year = #Year AND Month = #Month)) ASC
)
SELECT Month
,Year
,Value
FROM CTE_2
ORDER BY Year, Month;
Basically, you calculate the difference from the target row number, get the first 31 rows there, and then resort them for output.
Check this out,
DECLARE #iPrevRows int
DECLARE #iPostRows int
DECLARE #Year int = 2016
DECLARE #Month int = 2
SELECT #iPrevRows= Count(*)
FROM
[GuestBook].[dbo].[tblTest]
where (year < #Year )
or (year =#Year and month < #Month)
SELECT #iPostRows= count(*) from
[GuestBook].[dbo].[tblTest]
where (year > #Year )
or (year =#Year and month > #Month)
if (#iPrevRows > 15)
select #iPrevRows =15
if (#iPostRows > 15)
select #iPostRows =15
if (#iPrevRows < 15 )
select #iPostRows = #iPostRows + (15-#iPrevRows)
else if (#iPostRows < 15 )
select #iPrevRows = #iPrevRows + (15-#iPostRows)
CREATE TABLE #tempValues
(
Year int NOT NULL,
Month int NOT NULL,
Value float
)
insert into #tempValues
SELECT top (#iPrevRows) Month, Year, Value
from
[GuestBook].[dbo].[tblTest]
where (year < #Year )
or (year =#Year and month < #Month)
order by 2 desc,1 desc
insert into #tempValues
SELECT Month, Year, Value
from
[GuestBook].[dbo].[tblTest]
where (year =#Year and month = #Month)
insert into #tempValues
SELECT top (#iPostRows) Month, Year, Value
from
[GuestBook].[dbo].[tblTest]
where (year > #Year )
or (year =#Year and month > #Month)
order by 2 ,1
select * from #tempValues
order by 2,1
Here is what I've done, seems to be working
select * from (
select top(31) * from MyTable r
order by ABS(DATEDIFF(month, DATEFROMPARTS(r.Year, r.Month, 1), DATEFROMPARTS(#Year, #Month, 1)))) s
order by Year, Month
I did it that way.
DECLARE #year INT = 2014, #month INT = 6;
WITH TableAux
AS (SELECT MyTable.Month
, MyTable.Year
FROM MyTable
WHERE MyTable.Year = #year
AND MyTable.Month = #month)
SELECT tb1.Month
, tb1.Year
, tb1.Value
FROM
(
SELECT TOP 16 MyTable.Month
, MyTable.Year
, MyTable.Value
FROM MyTable
CROSS JOIN TableAux
WHERE MyTable.Month <= TableAux.Month
AND MyTable.Year <= TableAux.Year
ORDER BY MyTable.Month DESC, MyTable.Year DESC
) tb1
UNION ALL
SELECT tb2.Month
, tb2.Year
, tb2.Value
FROM
(
SELECT TOP 15 MyTable.Month
, MyTable.Year
, MyTable.Value
FROM MyTable
CROSS JOIN TableAux
WHERE MyTable.Month > TableAux.Month
AND MyTable.Year > TableAux.Year
ORDER BY MyTable.Month, MyTable.Year
) tb2
ORDER BY Year, Month

TSQL loop months in sequence

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 );

Rolling Prior13 months with Current Month Sales

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.

Checking a date between two months sql server

I need to check if the date I have is (for example) between 10th of march and 20th of april and it should work on any year.
I'm really new to sql server, I found really good discussions on comparing dates, but they all include years and I don't want to hard-code the year.
How this should be done in a neat way?
You can extract parts of the date DAY, MONTH and YEAR respectively. For example
Select MONTH(GETDATE())
Will give you the Month Number for the current date.
If you want to check if a month is between certain values before doing an insert:
If Exists (Select 'x' Where Month(GETDATE()) Between 3 and 4 --March to April
Begin
--Do work here
End
If you want to get all values from a table where the CreatedDate is between two values:
Select *
From MyTable
Where MONTH(CreatedDate) Between 3 and 4 -- March to April
Finally, to check if CreatedDate is between two specific dates in a year (but any year), you could do the below.
Select *
From MyTable
Where CreatedDate Between Convert(Varchar, YEAR(CreatedDate)) + '-03-12'
and Convert(Varchar, YEAR(CreatedDate)) + '-03-15'
There's probably a shorter way, but you could do
SELECT *
FROM your table
--this will transform your date in a varchar with 2 digits for month and 2 digits for day
WHERE RIGHT('0' + RTRIM(CAST (MONTH(<yourdatefield>) as varchar(2))), 2) +
RIGHT('0' + RTRIM(cast(DAY(<yourdatefield>) as varchar(2))), 2)
BETWEEN '0310' and '0410'
see SqlFiddle
You could use following approach which generates StartDateTime - StopDateTime ranges for every year from Sales.SalesOrderHeader.OrderDate column (I've used AdventureWorks sample database: SQL2008+). This approach is SARG-able and with an index on OrderDate column the execution plan for the last SELECT statement will include an Index Seek:
CREATE INDEX IN_SalesOrderHeader_OrderDate
ON Sales.SalesOrderHeader(OrderDate);
GO
DECLARE #StartDay TINYINT,
#StartMonth TINYINT,
#StopDay TINYINT,
#StopMonth TINYINT;
SELECT #StartDay = 10,
#StartMonth = 3,
#StopDay = 20,
#StopMonth = 4;
DECLARE #FirstYear SMALLINT, #LastYear SMALLINT;
SELECT TOP(1) #FirstYear = YEAR(h.OrderDate)
FROM Sales.SalesOrderHeader h
WHERE h.OrderDate IS NOT NULL
ORDER BY h.OrderDate ASC;
SELECT TOP(1) #LastYear = YEAR(h.OrderDate)
FROM Sales.SalesOrderHeader h
WHERE h.OrderDate IS NOT NULL
ORDER BY h.OrderDate DESC;
WITH N10(Num)
AS
(
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL
SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
), N100(Num)
AS
(
SELECT (a.Num - 1)*10 + b.Num
FROM N10 a CROSS JOIN N10 b
)
SELECT COUNT(z.SalesOrderID) AS SalesOrderCount
FROM
(
SELECT x.CurrentYear,
StartDateTime = DATEADD(DAY, #StartDay-1, DATEADD(MONTH, #StartMonth-1, DATEADD(YEAR, x.CurrentYear - 1900, 0))),
StopDateTime = DATEADD(DAY, #StopDay/*-1*/, DATEADD(MONTH, #StopMonth-1, DATEADD(YEAR, x.CurrentYear - 1900, 0)))
FROM
(
SELECT #FirstYear + n.Num - 1 AS CurrentYear
FROM N100 n
WHERE n.Num <= (#LastYear - #FirstYear + 1)
) x
) y
CROSS APPLY
(
SELECT h.SalesOrderID
FROM Sales.SalesOrderHeader h
WHERE h.OrderDate >= y.StartDateTime
AND h.OrderDate < y.StopDateTime
) z
The execution plan:
Note: this solution assumes that Sales.SalesOrderHeader.OrderDate column has maximum 100 years.
Please refer to the following link for EXTRACT function which helps you get only the month from a date:
http://www.w3schools.com/sql/func_extract.asp

Get a list of dates between two dates using a function

My question is similar to this MySQL question, but intended for SQL Server:
Is there a function or a query that will return a list of days between two dates? For example, lets say there is a function called ExplodeDates:
SELECT ExplodeDates('2010-01-01', '2010-01-13');
This would return a single column table with the values:
2010-01-01
2010-01-02
2010-01-03
2010-01-04
2010-01-05
2010-01-06
2010-01-07
2010-01-08
2010-01-09
2010-01-10
2010-01-11
2010-01-12
2010-01-13
I'm thinking that a calendar/numbers table might be able to help me here.
Update
I decided to have a look at the three code answers provided, and the results of the execution - as a % of the total batch - are:
Rob Farley's answer : 18%
StingyJack's answer : 41%
KM's answer : 41%
Lower is better
I have accepted Rob Farley's answer, as it was the fastest, even though numbers table solutions (used by both KM and StingyJack in their answers) are something of a favourite of mine. Rob Farley's was two-thirds faster.
Update 2
Alivia's answer is much more succinct. I have changed the accepted answer.
this few lines are the simple answer for this question in sql server.
WITH mycte AS
(
SELECT CAST('2011-01-01' AS DATETIME) DateValue
UNION ALL
SELECT DateValue + 1
FROM mycte
WHERE DateValue + 1 < '2021-12-31'
)
SELECT DateValue
FROM mycte
OPTION (MAXRECURSION 0)
Try something like this:
CREATE FUNCTION dbo.ExplodeDates(#startdate datetime, #enddate datetime)
returns table as
return (
with
N0 as (SELECT 1 as n UNION ALL SELECT 1)
,N1 as (SELECT 1 as n FROM N0 t1, N0 t2)
,N2 as (SELECT 1 as n FROM N1 t1, N1 t2)
,N3 as (SELECT 1 as n FROM N2 t1, N2 t2)
,N4 as (SELECT 1 as n FROM N3 t1, N3 t2)
,N5 as (SELECT 1 as n FROM N4 t1, N4 t2)
,N6 as (SELECT 1 as n FROM N5 t1, N5 t2)
,nums as (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 1)) as num FROM N6)
SELECT DATEADD(day,num-1,#startdate) as thedate
FROM nums
WHERE num <= DATEDIFF(day,#startdate,#enddate) + 1
);
You then use:
SELECT *
FROM dbo.ExplodeDates('20090401','20090531') as d;
Edited (after the acceptance):
Please note... if you already have a sufficiently large nums table then you should use:
CREATE FUNCTION dbo.ExplodeDates(#startdate datetime, #enddate datetime)
returns table as
return (
SELECT DATEADD(day,num-1,#startdate) as thedate
FROM nums
WHERE num <= DATEDIFF(day,#startdate,#enddate) + 1
);
And you can create such a table using:
CREATE TABLE dbo.nums (num int PRIMARY KEY);
INSERT dbo.nums values (1);
GO
INSERT dbo.nums SELECT num + (SELECT COUNT(*) FROM nums) FROM nums
GO 20
These lines will create a table of numbers containing 1M rows... and far quicker than inserting them one by one.
You should NOT create your ExplodeDates function using a function that involves BEGIN and END, as the Query Optimizer becomes unable to simplify the query at all.
This does exactly what you want, modified from Will's earlier post. No need for helper tables or loops.
WITH date_range (calc_date) AS (
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, '2010-01-13') - DATEDIFF(DAY, '2010-01-01', '2010-01-13'), 0)
UNION ALL SELECT DATEADD(DAY, 1, calc_date)
FROM date_range
WHERE DATEADD(DAY, 1, calc_date) <= '2010-01-13')
SELECT calc_date
FROM date_range;
DECLARE #MinDate DATETIME = '2012-09-23 00:02:00.000',
#MaxDate DATETIME = '2012-09-25 00:00:00.000';
SELECT TOP (DATEDIFF(DAY, #MinDate, #MaxDate) + 1) Dates = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #MinDate)
FROM sys.all_objects a CROSS JOIN sys.all_objects b;
I'm an oracle guy, but I believe MS SQL Server has support for the connect by clause:
select sysdate + level
from dual
connect by level <= 10 ;
The output is:
SYSDATE+LEVEL
05-SEP-09
06-SEP-09
07-SEP-09
08-SEP-09
09-SEP-09
10-SEP-09
11-SEP-09
12-SEP-09
13-SEP-09
14-SEP-09
Dual is just a 'dummy' table that comes with oracle (it contains 1 row and the word 'dummy' as the value of the single column).
A few ideas:
If you need the list dates in order to loop through them, you could have a Start Date and Day Count parameters and do a while loop whilst creating the date and using it?
Use C# CLR Stored Procedures and write the code in C#
Do this outside the database in code
Would all these dates be in the database already or do you just want to know the days between the two dates? If it's the first you could use the BETWEEN or <= >= to find the dates between
EXAMPLE:
SELECT column_name(s)
FROM table_name
WHERE column_name
BETWEEN value1 AND value2
OR
SELECT column_name(s)
FROM table_name
WHERE column_name
value1 >= column_name
AND column_name =< value2
All you have to do is just change the hard coded value in the code provided below
DECLARE #firstDate datetime
DECLARE #secondDate datetime
DECLARE #totalDays INT
SELECT #firstDate = getDate() - 30
SELECT #secondDate = getDate()
DECLARE #index INT
SELECT #index = 0
SELECT #totalDays = datediff(day, #firstDate, #secondDate)
CREATE TABLE #temp
(
ID INT NOT NULL IDENTITY(1,1)
,CommonDate DATETIME NULL
)
WHILE #index < #totalDays
BEGIN
INSERT INTO #temp (CommonDate) VALUES (DATEADD(Day, #index, #firstDate))
SELECT #index = #index + 1
END
SELECT CONVERT(VARCHAR(10), CommonDate, 102) as [Date Between] FROM #temp
DROP TABLE #temp
A Bit late to the party, but I like this solution quite a bit.
CREATE FUNCTION ExplodeDates(#startDate DateTime, #endDate DateTime)
RETURNS table as
return (
SELECT TOP (DATEDIFF(DAY, #startDate, #endDate) + 1)
DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #startDate) AS DATE
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
)
Before you use my function, you need to set up a "helper" table, you only need to do this one time per database:
CREATE TABLE Numbers
(Number int NOT NULL,
CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number ASC)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
DECLARE #x int
SET #x=0
WHILE #x<8000
BEGIN
SET #x=#x+1
INSERT INTO Numbers VALUES (#x)
END
here is the function:
CREATE FUNCTION dbo.ListDates
(
#StartDate char(10)
,#EndDate char(10)
)
RETURNS
#DateList table
(
Date datetime
)
AS
BEGIN
IF ISDATE(#StartDate)!=1 OR ISDATE(#EndDate)!=1
BEGIN
RETURN
END
INSERT INTO #DateList
(Date)
SELECT
CONVERT(datetime,#StartDate)+n.Number-1
FROM Numbers n
WHERE Number<=DATEDIFF(day,#StartDate,CONVERT(datetime,#EndDate)+1)
RETURN
END --Function
use this:
select * from dbo.ListDates('2010-01-01', '2010-01-13')
output:
Date
-----------------------
2010-01-01 00:00:00.000
2010-01-02 00:00:00.000
2010-01-03 00:00:00.000
2010-01-04 00:00:00.000
2010-01-05 00:00:00.000
2010-01-06 00:00:00.000
2010-01-07 00:00:00.000
2010-01-08 00:00:00.000
2010-01-09 00:00:00.000
2010-01-10 00:00:00.000
2010-01-11 00:00:00.000
2010-01-12 00:00:00.000
2010-01-13 00:00:00.000
(13 row(s) affected)
Perhaps if you wish to go an easier way, this should do it.
WITH date_range (calc_date) AS (
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, CURRENT_TIMESTAMP) - 6, 0)
UNION ALL SELECT DATEADD(DAY, 1, calc_date)
FROM date_range
WHERE DATEADD(DAY, 1, calc_date) < CURRENT_TIMESTAMP)
SELECT calc_date
FROM date_range;
But the temporary table is a very good approach also. Perhaps shall you also consider a populated calendar table.
Definately a numbers table, though tyou may want to use Mark Redman's idea of a CLR proc/assembly if you really need the performance.
How to create the table of dates (and a super fast way to create a numbers table)
/*Gets a list of integers into a temp table (Jeff Moden's idea from SqlServerCentral.com)*/
SELECT TOP 10950 /*30 years of days*/
IDENTITY(INT,1,1) as N
INTO #Numbers
FROM Master.dbo.SysColumns sc1,
Master.dbo.SysColumns sc2
/*Create the dates table*/
CREATE TABLE [TableOfDates](
[fld_date] [datetime] NOT NULL,
CONSTRAINT [PK_TableOfDates] PRIMARY KEY CLUSTERED
(
[fld_date] ASC
)WITH FILLFACTOR = 99 ON [PRIMARY]
) ON [PRIMARY]
/*fill the table with dates*/
DECLARE #daysFromFirstDateInTheTable int
DECLARE #firstDateInTheTable DATETIME
SET #firstDateInTheTable = '01/01/1998'
SET #daysFromFirstDateInTheTable = (SELECT (DATEDIFF(dd, #firstDateInTheTable ,GETDATE()) + 1))
INSERT INTO
TableOfDates
SELECT
DATEADD(dd,nums.n - #daysFromFirstDateInTheTable, CAST(FLOOR(CAST(GETDATE() as FLOAT)) as DateTime)) as FLD_Date
FROM #Numbers nums
Now that you have a table of dates, you can use a function (NOT A PROC) like KM's to get the table of them.
CREATE FUNCTION dbo.ListDates
(
#StartDate DATETIME
,#EndDate DATETIME
)
RETURNS
#DateList table
(
Date datetime
)
AS
BEGIN
/*add some validation logic of your own to make sure that the inputs are sound.Adjust the rest as needed*/
INSERT INTO
#DateList
SELECT FLD_Date FROM TableOfDates (NOLOCK) WHERE FLD_Date >= #StartDate AND FLD_Date <= #EndDate
RETURN
END
Declare #date1 date = '2016-01-01'
,#date2 date = '2016-03-31'
,#date_index date
Declare #calender table (D date)
SET #date_index = #date1
WHILE #date_index<=#date2
BEGIN
INSERT INTO #calender
SELECT #date_index
SET #date_index = dateadd(day,1,#date_index)
IF #date_index>#date2
Break
ELSE
Continue
END
-- ### Six of one half dozen of another. Another method assuming MsSql
Declare #MonthStart datetime = convert(DateTime,'07/01/2016')
Declare #MonthEnd datetime = convert(DateTime,'07/31/2016')
Declare #DayCount_int Int = 0
Declare #WhileCount_int Int = 0
set #DayCount_int = DATEDIFF(DAY, #MonthStart, #MonthEnd)
select #WhileCount_int
WHILE #WhileCount_int < #DayCount_int + 1
BEGIN
print convert(Varchar(24),DateAdd(day,#WhileCount_int,#MonthStart),101)
SET #WhileCount_int = #WhileCount_int + 1;
END;
In case you want to print years starting from a particular year till current date. Just altered the accepted answer.
WITH mycte AS
(
SELECT YEAR(CONVERT(DATE, '2006-01-01',102)) DateValue
UNION ALL
SELECT DateValue + 1
FROM mycte
WHERE DateValue + 1 < = YEAR(GETDATE())
)
SELECT DateValue
FROM mycte
OPTION (MAXRECURSION 0)
This query works on Microsoft SQL Server.
select distinct format( cast('2010-01-01' as datetime) + ( a.v / 10 ), 'yyyy-MM-dd' ) as aDate
from (
SELECT ones.n + 10 * tens.n + 100 * hundreds.n + 1000 * thousands.n as v
FROM (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) ones(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tens(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) hundreds(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) thousands(n)
) a
where format( cast('2010-01-01' as datetime) + ( a.v / 10 ), 'yyyy-MM-dd' ) < cast('2010-01-13' as datetime)
order by aDate asc;
Now let's look at how it works.
The inner query merely returns a list of integers from 0 to 9999. It will give us a range of 10,000 values for calculating dates. You can get more dates by adding rows for ten_thousands and hundred_thousands and so forth.
SELECT ones.n + 10 * tens.n + 100 * hundreds.n + 1000 * thousands.n as v
FROM (VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) ones(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) tens(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) hundreds(n),
(VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) thousands(n)
) a;
This part converts the string to a date and adds a number to it from the inner query.
cast('2010-01-01' as datetime) + ( a.v / 10 )
Then we convert the result into the format you want. This is also the column name!
format( cast('2010-01-01' as datetime) + ( a.v / 10 ), 'yyyy-MM-dd' )
Next we extract only the distinct values and give the column name an alias of aDate.
distinct format( cast('2010-01-01' as datetime) + ( a.v / 10 ), 'yyyy-MM-dd' ) as aDate
We use the where clause to filter in only dates within the range you want. Notice that we use the column name here since SQL Server does not accept the column alias, aDate, within the where clause.
where format( cast('2010-01-01' as datetime) + ( a.v / 10 ), 'yyyy-MM-dd' ) < cast('2010-01-13' as datetime)
Lastly, we sort the results.
order by aDate asc;
if you're in a situation like me where procedures and functions are prohibited, and your sql user does not have permissions for insert, therefore insert not allowed, also "set/declare temporary variables like #c is not allowed", but you want to generate a list of dates in a specific period, say current year to do some aggregation, use this
select * from
(select adddate('1970-01-01',t4*10000 + t3*1000 + t2*100 + t1*10 + t0) gen_date from
(select 0 t0 union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
(select 0 t1 union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
(select 0 t2 union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2,
(select 0 t3 union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t3,
(select 0 t4 union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t4) v
where gen_date between '2017-01-01' and '2017-12-31'
WITH TEMP (DIA, SIGUIENTE_DIA ) AS
(SELECT
1,
CAST(#FECHAINI AS DATE)
FROM
DUAL
UNION ALL
SELECT
DIA,
DATEADD(DAY, DIA, SIGUIENTE_DIA)
FROM
TEMP
WHERE
DIA < DATEDIFF(DAY, #FECHAINI, #FECHAFIN)
AND DATEADD(DAY, 1, SIGUIENTE_DIA) <= CAST(#FECHAFIN AS DATE)
)
SELECT
SIGUIENTE_DIA AS CALENDARIO
FROM
TEMP
ORDER BY
SIGUIENTE_DIA
The detail is on the table DUAL but if your exchange this table for a dummy table this works.
SELECT dateadd(dd,DAYS,'2013-09-07 00:00:00') DATES
INTO #TEMP1
FROM
(SELECT TOP 365 colorder - 1 AS DAYS from master..syscolumns
WHERE id = -519536829 order by colorder) a
WHERE datediff(dd,dateadd(dd,DAYS,'2013-09-07 00:00:00'),'2013-09-13 00:00:00' ) >= 0
AND dateadd(dd,DAYS,'2013-09-07 00:00:00') <= '2013-09-13 00:00:00'
SELECT * FROM #TEMP1
Answer is avialbe here
How to list all dates between two dates
Create Procedure SelectDates(#fromDate Date, #toDate Date)
AS
BEGIN
SELECT DATEADD(DAY,number,#fromDate) [Date]
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY,number,#fromDate) < #toDate
END
DECLARE #StartDate DATE = '2017-09-13', #EndDate DATE = '2017-09-16'
SELECT date FROM ( SELECT DATE = DATEADD(DAY, rn - 1, #StartDate) FROM (
SELECT TOP (DATEDIFF(DAY, #StartDate, DATEADD(DAY,1,#EndDate)))
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
Result:
2017-09-13
2017-09-14
2017-09-15
2017-09-16

Resources