Collapse down calendar table by grouping like fields - sql

I have a script that generates a calendar table and fills in the public holidays and weekends for England and Wales. I would like to have the non working days as start and end periods, so taking the input
1980-04-01 00:00:00.000 Tuesday 0
1980-04-02 00:00:00.000 Wednesday 0
1980-04-03 00:00:00.000 Thursday 0
1980-04-04 00:00:00.000 Friday 1
1980-04-05 00:00:00.000 Saturday 1
1980-04-06 00:00:00.000 Sunday 1
1980-04-07 00:00:00.000 Monday 1
1980-04-08 00:00:00.000 Tuesday 0
1980-04-09 00:00:00.000 Wednesday 0
It would give the output
Start end
1980-04-03 1980-04-08
in other words, the day before the first non working day and the day after the last non working day of the group. To do this I would presumably use a min and max with a group and dateadd, but how do I go about obtaining a unique number to group each set of days by? This is using 2008 R2.
IF OBJECT_ID('tempdb.dbo.#calendar', 'U') IS NOT NULL
DROP TABLE #calendar
IF OBJECT_ID('tempdb.dbo.#easter', 'U') IS NOT NULL
DROP TABLE #easter;
-- CREATE TABLE CALENDAR
CREATE TABLE #calendar
(
[CalendarDate] DATETIME,
dayofwk varchar(20),
)
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
--INPUTS GO HERE
SET #StartDate = '01-01-1980'
SET #EndDate = '31-12-2018'
DECLARE #Startyear int
DECLARE #endyear int
set #startyear = YEAR(#StartDate)
set #endyear = YEAR(#EndDate)
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO #calendar
(
CalendarDate, dayofwk
)
SELECT
#StartDate, datename(dw,#startdate)
SET #StartDate = DATEADD(dd, 1, #StartDate)
END
---CREATE LIST OF EASTER MONDAY & GOOD FRIDAYS
create table #easter(
eastersunday_goodfriday date)
DECLARE #y int,
#EpactCalc INT,
#PaschalDaysCalc INT,
#NumOfDaysToSunday INT,
#EasterMonth INT,
#EasterDay INT
WHILE #Startyear <= #endyear
BEGIN
SET #y = #startyear
SET #EpactCalc = (24 + 19 * (#Y % 19)) % 30
SET #PaschalDaysCalc = #EpactCalc - (#EpactCalc / 28)
SET #NumOfDaysToSunday = #PaschalDaysCalc - (
(#Y + #Y / 4 + #PaschalDaysCalc - 13) % 7
)
SET #EasterMonth = 3 + (#NumOfDaysToSunday + 40) / 44
SET #EasterDay = #NumOfDaysToSunday + 28 - (
31 * (#EasterMonth / 4)
)
insert into #easter
SELECT dateadd(d,-2, CONVERT
( SMALLDATETIME,
RTRIM(#Y)
+ RIGHT('0'+RTRIM(#EasterMonth), 2)
+ RIGHT('0'+RTRIM(#EasterDay), 2) ))
insert into #easter
SELECT dateadd(d,1, CONVERT
( SMALLDATETIME,
RTRIM(#Y)
+ RIGHT('0'+RTRIM(#EasterMonth), 2)
+ RIGHT('0'+RTRIM(#EasterDay), 2) ))
SET #Startyear =#Startyear +1
end
select calendardate, dayofwk,
--NEW YEAR'S DAY
case
when day(calendardate) = 1 and month(calendardate) = 1 and dayofwk not in ('Saturday', 'sunday') then 1
when day(calendardate) between 2 and 3 and month(calendardate) = 1 and dayofwk = 'Monday' then 1
--GOOD FRIDAY, EASTER MONDAY
WHEN eastersunday_goodfriday IS NOT NULL THEN 1
--EARLY MAY BANK HOLIDAY
WHEN (datepart(weekday, CALENDARDATE) + ##DATEFIRST - 2) % 7 + 1 = 1
and (datepart(day, CALENDARDATE) - 1) / 7 + 1 = 1
AND MONTH(CALENDARDATE) = 5
THEN 1
--LATE MAY BANK HOLIDAY
WHEN (datepart(weekday, CALENDARDATE) + ##DATEFIRST - 2) % 7 + 1 = 1
and (datepart(day, CALENDARDATE) - 1) / 7 + 1 = 4
AND MONTH(CALENDARDATE) = 5
THEN 1
--LATE AUGUST BANK HOLIDAY
WHEN (datepart(weekday, CALENDARDATE) + ##DATEFIRST - 2) % 7 + 1 = 1
and (datepart(day, CALENDARDATE) - 1) / 7 + 1 = 4
AND MONTH(CALENDARDATE) = 8
THEN 1
--CHRISTMAS DAY
WHEN DAY(CalendarDate) = 25 AND MONTH(CALENDARDATE) = 12 AND dayofwk NOT IN ('SATURDAY', 'SUNDAY') THEN 1
--BOXING DAY
WHEN DAY(CalendarDate) = 26 AND MONTH(CALENDARDATE) = 12 AND dayofwk NOT IN ('SATURDAY', 'SUNDAY') THEN 1
WHEN DAY(CalendarDate) between 27 and 28 AND MONTH(CALENDARDATE) = 12 AND DAYOFWK IN ('MONDAY','TUESDAY') THEN 1
--SAT&SUN
WHEN DATENAME(DW,CALENDARDATE) IN ('SATURDAY','SUNDAY') THEN 1
ELSE 0
end as ISNONWORKINGDAY
from #calendar c
left join #easter e
on c.calendardate = e.eastersunday_goodfriday
ORDER BY C.CALENDARDATE

This seams like a typical island problem. Given what you posted you could do this:
DECLARE #table TABLE (dt DATE, isNonWorkDay BIT);
INSERT #table(dt,isNonWorkDay)
VALUES
('1980-04-01', 0),
('1980-04-02', 0),
('1980-04-03', 0),
('1980-04-04', 1),
('1980-04-05', 1),
('1980-04-06', 1),
('1980-04-07', 1),
('1980-04-08', 0),
('1980-04-09', 0);
SELECT DATEADD(DAY,-1,MIN(t.dt)), DATEADD(DAY,1,MAX(t.dt))
FROM #table t
WHERE t.isNonWorkDay = 1;
Returns: 1980-04-03, 1980-04-08
For multiple islands you could do this:
DECLARE #table TABLE (dt DATE, isNonWorkDay BIT);
INSERT #table(dt,isNonWorkDay)
VALUES
('1980-04-01', 0),
('1980-04-02', 0),
('1980-04-03', 0),
('1980-04-04', 1),
('1980-04-05', 1),
('1980-04-06', 1),
('1980-04-07', 1),
('1980-04-08', 0),
('1980-04-09', 0),
('1980-04-10', 0),
('1980-04-11', 1),
('1980-04-12', 1),
('1980-04-13', 1),
('1980-04-14', 0),
('1980-04-15', 0);
WITH x AS
(
SELECT *, rn = ROW_NUMBER() OVER (ORDER BY t.dt)
FROM #table t
),
xx AS
(
SELECT *, grouper = x.rn - ROW_NUMBER() OVER (ORDER BY x.dt)
FROM x
WHERE x.isNonWorkDay = 1
)
SELECT dtStart = DATEADD(DAY,-1,MIN(xx.dt)), dtend = DATEADD(DAY,1,MAX(xx.dt))
FROM xx
GROUP BY xx.grouper;
Returns:
dtStart dtend
---------- ----------
1980-04-03 1980-04-08
1980-04-10 1980-04-14

You can use the difference of a consecutive numbering of all days and a consecutive numbering of all non-workingdays to use for the grouping.
For a consecutive group of non-workingdays, these differences will all have the same value, and because of the gaps (workingsdays) between the groups, the next group will have a different (higher) value.
For numbering all days, you can use DATEDIFF, for the numbering of non-workingdays you can use ROW_NUMBER. The query could look like this:
WITH
numbering (calendardate, grp_num) AS (
SELECT
calendardate,
DATEDIFF(day, 0, calendardate) - ROW_NUMBER() OVER (ORDER BY calendardate)
FROM YourTable WHERE ISNONWORKINGDAY = 1
)
SELECT
DATEADD(day, -1, MIN(calendardate)) AS StartDate,
DATEADD(day, +1, MAX(calendardate)) AS EndDate
FROM numbering GROUP BY grp_num;

Related

Calculate quarter for dates given an example end date and quarter number

I have an issue where I need to determine fiscal quarters, but won't always know the start/end dates for the quarters. They will, however, always be 3 months long. What I will know is the ending date of the current quarter, and what quarter and year that refers to. For example, I might be given:
Current Quarter: Q4
Current Year: 2021
Current Quarter End Date: 1/31/2021
How can I get the quarter for any other date? If any of those 3 values were to change, the query still needs to provide the quarter for any given date based on those 3 parameters.
I came up with the following, which puts the last 4 years into a temp table:
DECLARE #QuarterEnd DATE = '1/31/2022'
, #CurrentQuarter INT = 1
, #CurrentYear INT = 2022
, #Counter INT = 16
, #qs INT = 0
, #qe INT = 2
, #DateToTest DATE = '12/15/2021'
CREATE TABLE #Quarters (
StartDate DATE
, EndDate DATE
, Qtr INT
, Yr INT
)
WHILE #Counter <> 0
BEGIN
INSERT INTO #Quarters VALUES (
cast(DATEADD(MONTH, DATEDIFF(MONTH, 0, #QuarterEnd)-#qe , 0) as date)
, cast(DATEADD(MONTH, DATEDIFF(MONTH, -1, #QuarterEnd)-#qs, -1) as date)
, #CurrentQuarter
, #CurrentYear
)
SET #Counter = #Counter - 1
SET #qs = #qs + 3
SET #qe = #qe + 3
SET #CurrentQuarter = CASE WHEN #CurrentQuarter = 1 THEN 4 ELSE #CurrentQuarter - 1 END
SET #CurrentYear = CASE WHEN #CurrentQuarter = 4 THEN #CurrentYear - 1 ELSE #CurrentYear END
END
SELECT #DateToTest
, (SELECT CONCAT('Q', Qtr, ' ', Yr) FROM #Quarters WHERE #DateToTest BETWEEN StartDate and EndDate)
FROM #Quarters
However, this doesn't seem to be practical when I'm running queries that will return hundreds of thousands of records.
I suppose I can throw that into a function and call it with:
SELECT MyQuarter = dbo.MyQuarterFunction(#QuarterEnd, #CurrentQuarter, #CurrentYear, #DateToTest)
There has to be a more efficient way to do this. Any suggestions?
Just create a permanent table called Quarters.
CREATE TABLE dbo.Quarters
(
StartDate date,
QuarterNumber tinyint,
FiscalYear int,
NextQuarterStartDate AS (DATEADD(MONTH, 3, StartDate))
);
INSERT dbo.Quarters(StartDate, QuarterNumber, FiscalYear)
VALUES('20200201',1,2020),
('20200501',2,2020),
('20200801',3,2020),
('20201101',4,2020),
('20210201',1,2021),
('20210501',2,2021),
('20210801',3,2021),
('20211101',4,2021),
('20220201',1,2022),
('20220501',2,2022),
('20220801',3,2022),
('20221101',4,2022);
Now any time you are given a date (like GETDATE()) you can find the other information easily:
DECLARE #date date = GETDATE();
SELECT * FROM dbo.Quarters
WHERE #date >= StartDate
AND #date < NextQuarterStartDate;
Example db<>fiddle
If you need to support multiple fiscal calendars simultaneously, just add a column (like CalendarID or CompanyID or CustomerID).
And really, you don't even need a calendar or quarters table for this. You already have a table of clients, right? Just add a column to store what month their fiscal year starts. That's really all you need.
CREATE TABLE dbo.Clients
(
ClientID int NOT NULL CONSTRAINT PK_Clients PRIMARY KEY,
Name nvarchar(200) NOT NULL CONSTRAINT UQ_ClientName UNIQUE,
FiscalYearStart tinyint NOT NULL CONSTRAINT CK_ValidMonth
CHECK (FiscalYearStart BETWEEN 1 AND 12)
);
Now let's insert a few rows with some clients with different fiscal years:
INSERT dbo.Clients(ClientID, Name, FiscalYearStart)
VALUES(1, N'ClientFeb', 2), -- fiscal year starts in February
(2, N'ClientMay', 5), -- fiscal year starts in May
(3, N'ClientNormal', 1); -- fiscal year matches calendar
Now, yes, we need a function, but let's not do any while loops or counters or #temp tables.
CREATE FUNCTION dbo.GetLast16Quarters
(
#DateToTest date,
#ClientID int
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
(
WITH n(n) AS
(
SELECT n = 1 UNION ALL
SELECT n + 1 FROM n WHERE n < 20
),
Last20Quarters(QuarterStart, FiscalYearStart) AS
(
SELECT QuarterStart = DATEADD(QUARTER, 1-n,
DATEFROMPARTS(YEAR(#DateToTest)+1, FiscalYearStart, 1)),
FiscalYearStart
FROM dbo.Clients CROSS JOIN n WHERE ClientID = #ClientID
),
Last16Quarters AS
(
SELECT TOP (16) QuarterStart,
y = YEAR(DATEADD(MONTH, 1-FiscalYearStart, QuarterStart))
FROM Last20Quarters WHERE QuarterStart < #DateToTest
ORDER BY QuarterStart DESC
)
SELECT QuarterStart,
QuarterEnd = EOMONTH(QuarterStart, 2),
FiscalYear = y,
QuarterNumber = ROW_NUMBER() OVER
(PARTITION BY y ORDER BY QuarterStart)
FROM Last16Quarters);
Then to call it:
DECLARE #DateToTest date = '20211215';
SELECT * FROM dbo.GetLast16Quarters(#DateToTest, 1);
Output:
QuarterStart
QuarterEnd
FiscalYear
QuarterNumber
2018-02-01
2018-04-30
2018
1
2018-05-01
2018-07-31
2018
2
2018-08-01
2018-10-31
2018
3
2018-11-01
2019-01-31
2018
4
2019-02-01
2019-04-30
2019
1
2019-05-01
2019-07-31
2019
2
2019-08-01
2019-10-31
2019
3
2019-11-01
2020-01-31
2019
4
2020-02-01
2020-04-30
2020
1
2020-05-01
2020-07-31
2020
2
2020-08-01
2020-10-31
2020
3
2020-11-01
2021-01-31
2020
4
2021-02-01
2021-04-30
2021
1
2021-05-01
2021-07-31
2021
2
2021-08-01
2021-10-31
2021
3
2021-11-01
2022-01-31
2021
4
Example db<>fiddle
Assuming that you have two input variables:
declare #quarter_end date = '2021-01-31';
declare #current_quarter int = 4;
You can calculate the first month of financial year:
declare #first_month_of_fy int = (month(#quarter_end) - #current_quarter * 3 + 12) % 12 + 1;
-- 2 i.e. February
And use that value to calculate the quarter and year for any date using some math:
select *
from (values
('2020-12-15'),
('2021-01-15'),
('2021-12-15'),
('2022-01-15')
) as t(testdate)
cross apply (select
(month(testdate) - #first_month_of_fy + 12) % 12 + 1
) as ca1(month_of_fy)
cross apply (select
(month_of_fy - 1) / 3 + 1,
year(dateadd(month, 12 - month_of_fy, dateadd(day, - day(testdate) + 1, testdate)))
) as ca2(fy_quarter, fy_year)
DB<>Fiddle
I ended up creating a function to handle this. Since I'm given the last day of the quarter, which quarter it is, and which year it is, I can determine the start and end date of that fiscal year. Since a quarter is always 3 months, I can also determine which months fall into which quarter.
The first 4 variables, #qa, #qb, #qc, #qd hold a comma separated list of the months within each quarter (#qa is current quarter, #qb is current quarter -1, #qc is current quarter -2, and #qd is current quarter -3)
The second 2 variables determine the first day and last day of the fiscal calendar
To get the quarter & year, I first get the month from the supplied date (#Date) and see if it's in #qa, #qb, #qc or #qd. That tells me the fiscal quarter.
Finally, I compare the given date to the start and end date of the current fiscal year, and to the 6 years prior (going back 6 years is enough for what I need)
CREATE FUNCTION [dbo].[FunctionNameHere]
(
#Date DATE
, #QuarterEnd DATE
, #CurrentQuarter INT
, #CurrentYear INT
)
RETURNS VARCHAR(7)
AS
BEGIN
DECLARE #qa VARCHAR(8) = (concat(datepart(m, dateadd(m, 0, #QuarterEnd)),',', datepart(m, dateadd(m, -1, #QuarterEnd)),',', datepart(m, dateadd(m, -2, #QuarterEnd))))
DECLARE #qb VARCHAR(8) = (concat(datepart(m, dateadd(m, -3, #QuarterEnd)),',', datepart(m, dateadd(m, -4, #QuarterEnd)),',', datepart(m, dateadd(m, -5, #QuarterEnd))))
DECLARE #qc VARCHAR(8) = (concat(datepart(m, dateadd(m, -6, #QuarterEnd)),',', datepart(m, dateadd(m, -7, #QuarterEnd)),',', datepart(m, dateadd(m, -8, #QuarterEnd))))
DECLARE #qd VARCHAR(8) = (concat(datepart(m, dateadd(m, -9, #QuarterEnd)),',', datepart(m, dateadd(m, -10, #QuarterEnd)),',', datepart(m, dateadd(m, -11, #QuarterEnd))))
DECLARE #YearStart DATE = DATEADD(d, 1, DATEADD(q, -#CurrentQuarter, #QuarterEnd))
DECLARE #YearEnd DATE = DATEADD(q, 4-#CurrentQuarter, #QuarterEnd)
DECLARE #Qtr VARCHAR(8) = CONCAT('Q', CASE WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qa, ',')) THEN #CurrentQuarter
WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qb, ',')) THEN CASE WHEN #CurrentQuarter = 1 THEN 4
WHEN #CurrentQuarter = 2 THEN 1
WHEN #CurrentQuarter = 3 THEN 2
WHEN #CurrentQuarter = 4 THEN 3 END
WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qc, ',')) THEN CASE WHEN #CurrentQuarter = 1 THEN 3
WHEN #CurrentQuarter = 2 THEN 4
WHEN #CurrentQuarter = 3 THEN 1
WHEN #CurrentQuarter = 4 THEN 2 END
WHEN DATEPART(m, #Date) IN (SELECT value FROM string_split(#qd, ',')) THEN CASE WHEN #CurrentQuarter = 1 THEN 2
WHEN #CurrentQuarter = 2 THEN 3
WHEN #CurrentQuarter = 3 THEN 4
WHEN #CurrentQuarter = 4 THEN 1 END
END,
' ',
CASE WHEN #Date BETWEEN #YearStart AND #YearEnd THEN #CurrentYear
WHEN #Date BETWEEN dateadd(Year, -1, #YearStart) AND dateadd(Year, -1, #YearEnd) THEN #CurrentYear - 1
WHEN #Date BETWEEN dateadd(Year, -2, #YearStart) AND dateadd(Year, -2, #YearEnd) THEN #CurrentYear - 2
WHEN #Date BETWEEN dateadd(Year, -3, #YearStart) AND dateadd(Year, -3, #YearEnd) THEN #CurrentYear - 3
WHEN #Date BETWEEN dateadd(Year, -4, #YearStart) AND dateadd(Year, -4, #YearEnd) THEN #CurrentYear - 4
WHEN #Date BETWEEN dateadd(Year, -5, #YearStart) AND dateadd(Year, -5, #YearEnd) THEN #CurrentYear - 5
WHEN #Date BETWEEN dateadd(Year, -6, #YearStart) AND dateadd(Year, -6, #YearEnd) THEN #CurrentYear - 6
ELSE 9999 END)
return #Qtr
END

Building a table of year and quarter combinations

Say I have a year and a quarter and I want to build a table lists a specific number of year and quarter combinations ending with the given year and quarter.
E.g.
**Input:**
Year = 2018
Quarter = 3
NumRows = 4
**Output:**
Year.....|Quarter
2018.....|3
2018.....|2
2018.....|1
2017.....|4
You can use recursive generation.
First you need to build a DATETIME object from the year and quarter.
DECLARE #Quarter INT = 3;
DECLARE #Year INT = 2018;
DECLARE #NumRows INT = 4;
DECLARE #initial_date DATETIME = DATEADD(quarter, #Quarter-1, DATEADD(year, #Year-1900, 0));
Then you can recursively generate the year & quarter combinations like this.
;WITH quarters AS (
SELECT #initial_date AS [qdate]
UNION ALL
SELECT DATEADD(quarter, -1, [qdate]) AS [qdate]
FROM quarters
WHERE [qdate] > DATEADD(quarter, -1*(#NumRows-1), #initial_date)
)
SELECT DATEPART(year, qdate) as year, DATEPART(quarter, qdate) as quarter FROM quarters
this uses a recursive cte
for
declare #year int = 2018,
#quarter int = 3,
#numrows int = 6
; with rcte as
(
select n = 1,
yr = #year,
qtr = #quarter
union all
select n = n + 1,
yr = yr - (((qtr - 1 + 4 - 1) % 4 + 1) / 4),
qtr = (qtr - 1 + 4 - 1) % 4 + 1
from rcte
where n < #numrows
)
select *
from rcte
order by n
/* result:
n yr qtr
1 2018 3
2 2018 2
3 2018 1
4 2017 4
5 2017 3
6 2017 2
*/
You can try to use cte recursive with some calculation
DECLARE #Year INT= 2018
DECLARE #Quarter INT = 3
DECLARE #NumRows INT= 10
;WITH CTE AS (
SELECT (#Year - #NumRows/4) yr,
4 - (#NumRows % 4) Quarter,
#NumRows NumRows
UNION ALL
SELECT yr + Quarter / 4,
CASE WHEN (Quarter + 1) % 4 = 0 THEN 4
ELSE (Quarter + 1) % 4
END,
NumRows- 1
FROM CTE
WHERE NumRows > 1
)
select yr,Quarter
from cte
sqlfiddle

SQL Query to Count number of values in a column Per Month

id date sales
1 01/01/2015 100
2 01/01/2015 100
3 02/01/2015 100
4 03/01/2015 100
What I need is to count the number of sales in a given date range (per month)which is based user input which are StartDate and EndDate. For Example the User inputs StartDate - 01/01/2015 and EndDate - 04/01/2015
the output would be like this
Month StartMonth EndMonth TotalSales
1 01/01/2015 01/31/2015 200
2 02/01/2015 02/28/2015 100
3 03/01/2015 03/31/2015 100
4 04/01/2015 04/30/2015 0
Started to something like this
set #Start_act = cast(DATEADD(month, DATEDIFF(month, 0, #StartDate), 0) as date)
set #End_act = cast(DATEADD(d, -1, DATEADD(m, DATEDIFF(m, 0, #Enddate)+1, 0)) as date)
set #counter = DATEDIFF(month, #Start_act, #End_act)
if(#counter = 1)
begin
set #counter = #counter
end
else
set #counter = #counter + 1
end
set #count = 0
CREATE TABLE #TempTableID
(
Month int,
StartMonth date,
EndMonth date,
TotalSales
)
while (#count <= #counter)
begin
set #count = #count + 1;
if(#count = 1)
begin
set #Start = #Start_act
set #End = cast(DATEADD(d, -1, DATEADD(m, DATEDIFF(m, 0, #Start_act)+1, 0)) as date)
set #plannedHorseCapacity = 123
end
else
begin
set #Start = cast(DATEADD(d, 1, #End)as date)
set #End = cast(DATEADD(d, -1, DATEADD(m, DATEDIFF(m, 0, #Start)+1, 0)) as date)
set #plannedHorseCapacity = 456
end
Insert into #TempTableID
(
Month
StartMonth
EndMonth
TotalSales
)
Values
(
#count,
#Start,
#End,
#TotalSales
)
if(#count > #counter)
begin
break
end
else
begin
continue
end
end
Select * from #TempTableID
You can use below script -
WITH cte
AS
(SELECT
CONVERT(VARCHAR(20), MONTH([date]))
+ '-' + CONVERT(VARCHAR(20), YEAR([date])) monthyear
,([sales])
FROM [sales])
--add where condition for from and to date here
SELECT
monthyear
,SUM(sales) totalsales
FROM cte
GROUP BY monthyear
Output will be
monthyear totalsales
1-2015 400
2-2015 50
3-2015 150
Option 2 With Start and End Date
WITH cte
AS
(SELECT
FORMAT(MONTH([date]), '0#')
+ '-' + CONVERT(VARCHAR(20), YEAR([date])) monthyear
,([sales])
FROM [sales])
--add where condition for from and to date here
SELECT
monthyear
,cast (SUBSTRING(monthyear, 1, 2) as int) [Month]
,SUBSTRING(monthyear, 4, 4) [Year]
,(SELECT
DATEADD(MONTH, SUBSTRING(monthyear, 1, 2) - 1, DATEADD(YEAR, SUBSTRING(monthyear, 4, 4) - 1900, 0)))
StartDate
,(SELECT
DATEADD(DAY, -1, DATEADD(MONTH, CAST(SUBSTRING(monthyear, 1, 2) AS INT), DATEADD(YEAR, SUBSTRING(monthyear, 4, 4) - 1900, 0))))
EndDate
,SUM(sales) totalsales
FROM cte
GROUP BY monthyear
Output will be
monthyear Month Year StartDate EndDate totalsales
01-2015 1 2015 2015-01-01 00:00:00.000 2015-01-31 00:00:00.000 400
02-2015 2 2015 2015-02-01 00:00:00.000 2015-02-28 00:00:00.000 50
03-2015 3 2015 2015-03-01 00:00:00.000 2015-03-31 00:00:00.000 50
11-2015 11 2015 2015-11-01 00:00:00.000 2015-11-30 00:00:00.000 100
EDIT - Sorting
If you have multiple year data, then the dates will not come in sequence to fix that add order by [StartDate] in last.
output will be
monthyear Month Year StartDate EndDate totalsales
01-2015 1 2015 2015-01-01 00:00:00 2015-01-31 00:00:00.000 400
02-2015 2 2015 2015-02-01 00:00:00 2015-02-28 00:00:00.000 50
03-2015 3 2015 2015-03-01 00:00:00 2015-03-31 00:00:00.000 50
11-2015 11 2015 2015-11-01 00:00:00 2015-11-30 00:00:00.000 100
01-2016 1 2016 2016-01-01 00:00:00 2016-01-31 00:00:00.000 125
11-2016 11 2016 2016-11-01 00:00:00 2016-11-30 00:00:00.000 55
You can achieve it like this,
;WITH [CTE_DATE]
AS
(
SELECT MONTH(#fromdt) AS [Month]
,DATEADD(mm, DATEDIFF(mm, 0, #fromdt), 0) AS StartMonth
,DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#fromdt)+1,0)) AS EndMonth
UNION ALL
SELECT [Month] + 1 AS [Month]
,DATEADD(MONTH,1,StartMonth) AS StartMonth
,DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,DATEADD(MONTH,1,StartMonth))+1,0)) AS EndMonth
FROM [CTE_DATE] WHERE [Month] < MONTH(#todt)
)
SELECT *
,(SELECT SUM(sales) FROM yourtable where [date] between StartMonth and EndMonth) as TotalSales
FROM [CTE_DATE]
You can find end of month as below:
select [Month] = Month([date]), [date] as StartMonth,
EndMonth = convert(date, dateadd(month,datediff(month,0,'2017-02-11')+1,0)-1),
TotalSales from (
select [date], sum(sales) as totalSales
from #yoursales
group by [date]
) a
You could use a CTE like this
DECLARE #SampleData AS TABLE
(
id int,
[date] date,
sales int
)
INSERT INTO #SampleData
VALUES
( 1 , '2015-01-01', 100 ),
( 2 , '2015-01-01', 100 ),
( 3 , '2015-02-01', 100 ),
( 4 , '2015-03-01', 100 )
DECLARE #StartDate date = '2015-01-01'
DECLARE #EndDate date = '2015-08-01'
;WITH temp AS -- calendar table
(
SELECT dateadd(month, datediff(month,0,#StartDate),0) AS [StartMonthDate],
dateadd(day, -1 ,dateadd(month, datediff(month,0,#StartDate) + 1,0)) AS [EndMonthDate]
UNION ALL
SELECT dateadd(month, 1, t.[StartMonthDate]),
dateadd(day,-1,dateadd(month, 2, t.[StartMonthDate]))
FROM temp t
WHERE dateadd(month, 1, t.[StartMonthDate]) <= #EndDate
)
SELECT datepart(year,t.StartMonthDate) AS year,
datepart(month,t.StartMonthDate) AS month,
t.StartMonthDate,
t.EndMonthDate,
coalesce(ap.TotalSales,0) AS TotalSales
FROM temp t
CROSS APPLY
(
SELECT SUM(sales) AS TotalSales
FROM #SampleData sd
WHERE sd.[date] BETWEEN t.StartMonthDate AND t.EndMonthDate
) ap
OPTION (MAXRECURSION 0)
Demo link: http://rextester.com/LWUX67185
;With cte(id,date,sales)
AS
(
SELECT 1,'01/01/2015',100 UNION ALL
SELECT 2,'01/01/2015',100 UNION ALL
SELECT 3,'02/01/2015',100 UNION ALL
SELECT 4,'03/01/2015',100 UNION ALL
SELECT 5,'04/01/2015',NULL
)
SELECT [Id],CONVERT(VARCHAR(10),[DATE], 101) AS [DATE],
CONVERT(VARCHAR(10),[EndMonth],101) AS [EndMonth],
[SumOfSale] From
(
SELECT id,[DATE],EndMonth,SUM(sales) OVER(Partition by [DATE],EndMonth order by EndMonth) AS SumOfSale,
Row_NUmber ()OVER(Partition by [DATE],EndMonth order by id)As Seq From
(
SELECT id, CAST([DATE] AS DATE)[DATE], EOMONTH(date)AS EndMonth,ISNULL(sales,0)As Sales from cte
)Dt
)Final
WHERE Final.Seq=1
OutPut
Month StartMonth EndMonth TotalSales
1 01/01/2015 01/31/2015 200
2 02/01/2015 02/28/2015 100
3 03/01/2015 03/31/2015 100
4 04/01/2015 04/30/2015 0

How do I calculate Workdays except Weekends in SQL query?

I want to calculate which of my holidays are conflict with working days.
I have a Holidays table which is below.
NameOfHoliday StartDate DurationByDay
Christmas 26.12.2015 5
26 and 27 are at weekend. So this shouldt be calculated. So I have to get only 4 days as result.
DECLARE #t TABLE(
NameOfHoliday VARCHAR(10),
StartDate DATE,
DurationByDay SMALLINT
)
INSERT INTO #t
VALUES ('Christmas', '20151226', 5)
;WITH cte AS
(
SELECT *, cnt = 0
FROM #t
UNION ALL
SELECT t2.NameOfHoliday, DATEADD(DAY, t1.cnt + 1, t2.StartDate), t2.DurationByDay, t1.cnt + 1
FROM cte t1
JOIN #t t2 ON t1.NameOfHoliday = t2.NameOfHoliday
WHERE t1.cnt < t1.DurationByDay
)
SELECT NameOfHoliday, StartDate, DT
FROM (
SELECT *
, DT = DATENAME(DW, StartDate)
, RowNum = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY NameOfHoliday)
FROM cte
) t
WHERE RowNum = 1
AND DT NOT IN ('Saturday', 'Sunday')
OPTION (MAXRECURSION 0)
output -
NameOfHoliday StartDate DT
------------- ---------- ------------------
Christmas 2015-12-28 Monday
Christmas 2015-12-29 Tuesday
Christmas 2015-12-30 Wednesday
Christmas 2015-12-31 Thursday
I Hope your requirement is like this..
if object_id('tempdb..#Holidays') is not null drop table #Holidays
create table #Holidays(id int identity(1,1),Holiday date)
insert into #Holidays values ('2015-12-25'),('2015-12-28')
set dateformat ymd
declare #StartDate date = '2015-12-01'
declare #EndtDate date = '2015-12-31'
--some times #EndtDate might be NULL so, set to getdate()
set #EndtDate = isnull(#EndtDate,cast(getdate() as date))
declare #Holiday int
set #Holiday = (select count(*) from #Holidays where Holiday between #StartDate and #EndtDate)
SELECT
(DATEDIFF(dd, #StartDate, #EndtDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndtDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndtDate) = 'Saturday' THEN 1 ELSE 0 END)
-#Holiday

DATEDIFF excluding summer months

We are running reports for a seasonal business, with expected lulls during the summer months. For some metrics, we'd essentially like to pretend that those months don't even exist.
Thus consider the default behavior of:
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-06-01') -- answer = 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-07-01') -- 2
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-08-01') -- 3
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-09-01') -- 4
We want to ignore June and July, so we would like those answers to look like this:
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-06-01') -- answer = 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-07-01') -- 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-08-01') -- 1
SELECT DATEDIFF(MONTH, '2015-05-01', '2015-09-01') -- 2
What is the easiest way to accomplish this? I'd like a pure SQL solution, rather than something using TSQL, but writing a custom function such as NOSUMMER_DATEDIFF could also work.
Also, keep in mind the reports will span multiple years, so the solution should be able to handle that.
If you are only interested month differences, then I would suggest a trick here. Count the number of months since some date 0, but ignore the summer months. For example:
'2015-05-01' --> 2015 * 10 + 5 = 20155
'2015-06-01' --> 2015 * 10 + 6 = 20156
'2015-07-01' --> 2015 * 10 + 6 = 20156
'2015-08-01' --> 2015 * 10 + 6 = 20156
'2015-09-01' --> 2015 * 10 + 7 = 20157
This is a fairly easy calculation:
select (case when month(date2) <= 6 then year(date2) * 10 + month(date2)
when month(date2) in (7, 8) then year(date2) * 10 + 6
else year(date2) * 10 + (month(date2) - 2)
end)
For the difference:
select ((case when month(date2) <= 6 then year(date2) * 10 + month(date2)
when month(date2) in (7, 8) then year(date2) * 10 + 6
else year(date2) * 10 + (month(date2) - 2)
end) -
(case when month(date1) <= 6 then year(date1) * 10 + month(date1)
when month(date1) in (7, 8) then year(date1) * 10 + 6
else year(date1) * 10 + (month(date1) - 2)
end)
)
To able to achieve that, you have to "split" dates ranges to an "array" of dates for every single range of dates. CTE might be helpful in this case.
See:
--your table which holds dates ranges
DECLARE #dates TABLE(id INT IDENTITY(1,1), dFrom DATE, dTo DATE)
INSERT INTO #dates (dFrom, dTo)
VALUES('2015-05-01', '2015-06-01'),
('2015-05-01', '2015-07-01'),
('2015-05-01', '2015-08-01'),
('2015-05-01', '2015-09-01')
--summer month table
DECLARE #summermonths TABLE(summMonth INT)
INSERT INTO #summermonths(summMonth)
VALUES(6), (7)
--here Common Table Expressions is in action to "split" dates ranges to an array of dates for every single date range
;WITH CTE AS
(
SELECT id, DATEADD(MM, 0, dFrom) AS ndFrom, dTo, CASE WHEN MONTH(DATEADD(MM, 0, dFrom)) = 6 OR MONTH(DATEADD(MM, 0, dFrom)) = 7 THEN 0 ELSE 1 END AS COfMonth
FROM #dates
WHERE DATEADD(MM, 1, dFrom)<=dTo
UNION ALL
SELECT id, DATEADD(MM, 1, ndFrom) AS ndFrom, dTo, CASE WHEN MONTH(DATEADD(MM, 1, ndFrom)) = 6 OR MONTH(DATEADD(MM, 1, ndFrom)) = 7 THEN 0 ELSE 1 END AS COfMonth
FROM CTE
WHERE DATEADD(MM, 1, ndFrom)<=dTo
)
SELECT t1.id, t2.dFrom, t2.dTo, SUM(t1.COfMonth) AS MyDateDiff
FROM CTE AS t1 INNER JOIN #dates AS t2 ON t1.id = t2.id
GROUP BY t1.id, t2.dFrom , t2.dTo
Result:
id dFrom dTo MyDateDiff
1 2015-05-01 2015-06-01 1
2 2015-05-01 2015-07-01 1
3 2015-05-01 2015-08-01 2
4 2015-05-01 2015-09-01 3 --not 2, because of 5, 8, 9
Got it?
Note: a solution might be differ in case of dFrom and dTo is not the first date of month.