DATEDIFF excluding summer months - sql

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.

Related

Split date into month and year based on number of months passed in stored procedure into a temp table

I have a stored procedure, where takes number of numbers as a parameter. I do my query with where clause like this
select salesrepid, month(salesdate), year(salesdate), salespercentage
from SalesRecords
where salesdate >= DATEADD(month, -#NumberOfMonths, getdate())
So for example, if #NumberOFmonths passed = 3 and based on todays date,
It should bring, september 9, october 10 and november 11 in my resultset. My query brings it but request is I need to return null for those salesrep who doesnt have a value for a month,
for example:
salerepid month year salespercentage
232 9 2020 80%
232 10 2020 null
232 11 2020 90%
how can I achieve this ? Right now the query brings back only two records and does not bring october data as no value is there, but i want it to return october with null value.
If I follow you correctly, you can generate all start of months within the target interval, and cross join that with the table to generate all possible combinations. Then you can bring the table with a left join:
with all_dates as (
select datefromparts(year(getdate()), month(getdate()), 1) salesdate, 0 lvl
union all
select dateadd(month, - lvl - 1, salesdate), lvl + 1
from all_dates
where lvl < #NumberOfMonths
)
select r.salesrepid, d.salesdate , s.salespercentage
from all_dates d
cross join (select distinct salesrepid from salesrecords) r
left join salesrecord s
on s.salesrepid = r.salesrepid
and s.salesdate >= d.salesdate
and s.salesdate < dateadd(month, 1, d.salesdate )
Your original query and result imply that there is at most one record per sales rep and month, so this works under the same assumption. If that's not the case (which would somehow make more sense), you would need aggregation in the outer query.
Declare #numberofmonths int = 3;
with all_dates as (
select datefromparts(year(getdate()), month(getdate()), 1) dt, 0 lvl
union all
select dateadd(month, - lvl - 1, dt), lvl + 1
from all_dates
where lvl < 3
)
select * from all_dates
This gives me following result:
2020-11-01 0
2020-10-01 1
2020-08-01 2
2020-05-01 3
I want only:
2020-11-01 0
2020-10-01 1
2020-09-01 2

Get average days open for issues per day (SQL)

I'm trying to create a chart in PowerBI that shows the average days open of issues in a database at each given time between two dates.
CREATE TABLE Issues
(IssueID int,IssueName varchar(10),created datetime, closed datetime);
INSERT INTO Issues
VALUES
(1,'a','2012-01-01 00:00:00', '2012-05-01 00:00:00'),
(2,'b','2012-03-01 00:00:00', '2012-06-01 00:00:00');
My first query shows all data in the database:
SELECT IssueID,
DATEDIFF(DAY,created,ISNULL(closed,GETDATE())) AS 'Days_Open'
FROM Issues
Results:
IssueID Days_Open
1 4
2 3
What I want to find is for each day, the issues open that day and average days open?
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2012-01-01'
SET #EndDate = '2012-07-01'
Ex:
Issues open each day
Date IssueID Days_Open
2012-01-01 1 0
2012-02-01 1 1
2012-03-01 1 2
2012-03-01 2 0
2012-04-01 1 3
2012-04-01 2 1
2012-05-01 1 4
2012-05-01 2 2
2012-06-01 2 3
Day 07 has no issues
Average
Date Average_Days_Open
2012-01-01 0 (1 issue just created)
2012-02-01 1 (1 issue - 1 day old)
2012-03-01 1 (2 issues - (2+0)/2 = 1)
2012-04-01 2 (2 issues - (3+1)/2 = 2)
2012-05-01 3 (2 issues - (4+2)/2 = 3)
2012-06-01 3 (1 issue - (0+3)/1 = 3)
2012-07-01 0 (Since there were no issues that day)
If i can get the data from both then I should be able to create a line chart in PowerBi similar to this:
Chart
Can someone please help out?
Based on syntax I guess it is SQL Server:
1) Query:
WITH CTE_DatesTable
AS
(
SELECT CAST('20120101' as datetime) AS [date]
UNION ALL
SELECT DATEADD(dd, 1, [date])
FROM CTE_DatesTable
WHERE DATEADD(dd, 1, [date]) <= '20120106'
)
SELECT [date] , i.IssueId, DATEDIFF(DAY,i.created,d.[date]) AS Days_Open
FROM CTE_DatesTable d
JOIN Issues i
ON d.date BETWEEN i.created AND i.closed
ORDER BY [date], IssueId
OPTION (MAXRECURSION 0);
DBFiddle Demo
2) Query (average):
WITH CTE_DatesTable
AS
(
SELECT CAST('20120101' as datetime) AS [date]
UNION ALL
SELECT DATEADD(dd, 1, [date])
FROM CTE_DatesTable
WHERE DATEADD(dd, 1, [date]) <= '20120106'
)
SELECT [date] ,AVG( DATEDIFF(DAY,i.created,d.[date]))
FROM CTE_DatesTable d
JOIN Issues i
ON d.date BETWEEN i.created AND i.closed
GROUP BY [date]
ORDER BY [date]
OPTION (MAXRECURSION 0);
DBFiddle Demo2
The whole idea is to generate calendar table.
If you need dates that are out of range you could use LEFT JOIN:
WITH CTE_DatesTable
AS
(
SELECT CAST('20120101' as datetime) AS [date]
UNION ALL
SELECT DATEADD(dd, 1, [date])
FROM CTE_DatesTable
WHERE DATEADD(dd, 1, [date]) <= '20120107'
)
SELECT [date] , COALESCE(AVG( DATEDIFF(DAY,i.created,d.[date])),0)
FROM CTE_DatesTable d
LEFT JOIN Issues i
ON d.date BETWEEN i.created AND i.closed
GROUP BY [date]
ORDER BY [date]
OPTION (MAXRECURSION 0);
DBFiddle Demo3

calculate start date and end date from given quarter SQL

I want to get :
startdate and enddate from a given quarter from between dates
example :
range of dates : 2016-01-01 - 2016-12-31
1 (quarter) - will give me :
start date
2016-01-01
enddate
2016-03-31
2 (quarter) - will give me :
start date
2016-04-01
enddate
2016-06-30
and so on
I made it for only Quarter name and Year, modified it as your need
-- You may need to extend the range of the virtual tally table.
SELECT [QuarterName] = 'Q' + DATENAME(qq,DATEADD(QQ,n,startdate)) + ' ' + CAST(YEAR(DATEADD(QQ,n,startdate)) AS VARCHAR(4))
FROM (SELECT startdate = '01/Jan/2016', enddate = '31/DEC/2016') d
CROSS APPLY (
SELECT TOP(1+DATEDIFF(QQ,startdate,enddate)) n
FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12)) rc(n)
) x
Check below logic to get your answer.
DECLARE #Year DATE = convert(varchar(20),datepart(YEAR,getdate()))+'-01'+'-01'
DECLARE #Quarter INT = 4
SELECT DATEADD(QUARTER, #Quarter - 1, #Year) ,
DATEADD(DAY, -1, DATEADD(QUARTER, #Quarter, #Year))
SELECT DATEADD(QUARTER, d.q, DATEADD(YEAR, DATEDIFF(YEAR, 0,GETDATE()), 0))
AS FromDate,
DATEADD(QUARTER, d.q + 1, DATEADD(YEAR, DATEDIFF(YEAR, 0, GETDATE()), -1))
AS ToDate
FROM (
SELECT 0 UNION ALL
SELECT 1 UNION ALL
SELECT 2 UNION ALL
SELECT 3
) AS d(q)

T-SQL Select minimum values from different records of the table

Let's say I have a query in which I count the number of events per day:
**Date** **NumberOfEvents**
2017-11-1 7
2017-11-2 11
2017-11-3 3
...
2017-11-8 24
2017-11-9 6
2017-11-10 10
2017-11-11 9
...
2017-11-22 22
2017-11-23 11
2017-11-24 14
2017-11-25 17
...
2017-11-28 16
2017-11-29 21
2017-11-30 6
...
Then let's say I would define a variable #StartingDay ='2017-11-3'
I would like to get a query with the minimum value for the same week day +-1 day for the next 4 weeks after #StartingDay, p.ex:
**Period** **DateWithMin** **MinNumberOfEvents**
2017-11-09 To 2017-11-11 2017-11-9 6
2017-11-16 To 2017-11-18 2017-11-17 8
2017-11-23 To 2017-11-25 2017-11-23 11
2017-11-30 To 2017-12-02 2017-11-30 6
I believe I would have to cycle through the different periods to search for the min, but I can't find a way to cycle.
Another way to do this is to generate the From and To dates with a recursive CTE, apply a Row_Number() to the results to find the Min per grouping, and select only those results:
Declare #StartingDay Date = '2017-11-03',
#NumWeeks Int = 4
;With Dates As
(
Select DateFrom = DateAdd(Day, -1, DateAdd(Week, 1, #StartingDay)),
DateTo = DateAdd(Day, 1, DateAdd(Week, 1, #StartingDay))
Union All
Select DateFrom = DateAdd(Week, 1, DateFrom),
DateTo = DateAdd(Week, 1, DateTo)
From Dates
Where DateTo < DateAdd(Day, 1, DateAdd(Week, #NumWeeks, #StartingDay))
), Results As
(
Select PeriodFrom = D.DateFrom,
PeriodTo = D.DateTo,
NumberOfEvents = Y.NumberOfEvents,
RN = Row_Number() Over (Partition By D.DateFrom, D.DateTo
Order By Y.NumberOfEvents),
Date = Y.Date
From YourTable Y
Join Dates D On Y.Date Between D.DateFrom And D.DateTo
)
Select PeriodFrom,
PeriodTo,
DateWithMin = Date,
MinNumberOfEvents = NumberOfEvents
From Results
Where RN = 1
You can use modulo and date arithmetic to get the time periods and groups:
select min(date), max(date), min(NumberOfEvents)
from t
where (datediff(day, #startingday, date) % 7) in (0, 1, 6) and
date > dateadd(day, 1, #startingday) and
date <= dateadd(day, 4 * 7 + 1, #startingday)
group by (datediff(day, #startingday, date) + 1) / 7;
Getting the date of the minimum event is more troublesome. Here is one method:
select min(date), max(date), min(NumberOfEvents),
max(case when seqnum = 1 then date end) as date_at_min
from (select t.*, v.grp,
row_number() over (partition by grp order by numberofevents) as seqnum
from t cross apply
(values ((datediff(day, #startingday, date) + 1) / 7)) v(grp)
) t
where (datediff(day, #startingday, date) % 7) in (0, 1, 6) and
date > dateadd(day, 1, #startingday) and
date <= dateadd(day, 4 * 7 + 1, #startingday)
group by grp;

How can I determine the date of a WeekDay column in a result set with a fixed layout given the start date?

I have a result set that always appears in this format:
LocationId Wed Thu Fri Sat Sun Mon Tue
The input to the function that generates these columns accepts a start date, and it generates a 7 day quantity report. So if I input '2014-02-01' the columns will always appear in that order even though that specific date falls on a Saturday (the dates "wrap around").
I need the date for each column for the purpose of a calculating another value (called 'Fee') that is based on a start + end date for each location. For example, location 21 might have a value of 50 associated with it for dates '2014-01-01' to '2014-02-03', but from '2014-02-04' it has a value of 53. The values under the day columns refer to Sales. So if there's a value (even 0), it means the SalesPerson was present and he should receive an AppearanceFee. One of the difficulties is calculating exactly what Fee the person should receive on a particular day as the report doesn't generate dates. The only information you have is the start date.
eg.
LocationId | Value | StartDate | EndDate
-----------+-------+------------+-----------
21 | 50 | 2014-01-01 | 2014-02-03
21 | 53 | 2014-02-04 | null
To simulate one record, one can use this query:
declare #startdate datetime
select #startdate = '2014-02-01'
select *
, 0 as Fee -- How do I calculate this value?
from
(
select 21 as LocationId
, 30 as Wed
, 33 as Thu
, 36 as Fri
, NULL as Sat
, NULL as Sun
, 19 as Mon
, 24 as Tue
) record
I've thought of using a complex case statement for each day but is there a simpler method?
CASE Left(DATENAME(dw, #startdate), 3)
WHEN 'Wed' THEN
(
(SELECT IsNull(Wed, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND #startdate BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Thu, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 1, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Fri, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 2, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Sat, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 3, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Sun, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 4, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Mon, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 5, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31')) +
(SELECT IsNull(Tue, 0) * Value FROM LocationValue lv WHERE lv.LocationId = record.LocationId AND DateAdd(dd, 6, #startdate) BETWEEN lv.StartDate and IsNull(lv.EndDate, '2050-12-31'))
)
As you can see this case statement is rather unwieldy.
I managed to solve this using a combination of PIVOTs and UNPIVOTs, and a query that generates a date range.
DECLARE #startDate DATETIME = '2014-02-01'
DECLARE #endDate DATETIME = #startDate + 7
SELECT
LocationId,
Sum(Wed) Wed,
Sum(Thu) Thu,
Sum(Fri) Fri,
Sum(Sat) Sat,
Sum(Sun) Sun,
Sum(Mon) Mon,
Sum(Tue) Tue,
Sum(Fee) Fee
FROM
(
SELECT
af.LocationId,
Calendar.Day,
Date,
Sales,
IsNumeric(Sales) * Value AS Fee
FROM
(
SELECT
Left(DateName(DW, datetable.Date), 3) Day,
Convert(DATE, datetable.Date) Date
FROM (
SELECT DATEADD(DAY, -(a.a + (10 * b.a) + (100 * c.a)), getdate()) AS Date
FROM (SELECT 0 AS a
UNION ALL 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) AS a
CROSS JOIN (SELECT 0 AS a
UNION ALL 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) AS b
CROSS JOIN (SELECT 0 AS a
UNION ALL 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) AS c
) datetable
WHERE datetable.Date BETWEEN #startDate AND #endDate
) Calendar LEFT JOIN
(
SELECT
LocationId,
Day,
Sales
FROM dbo.f_FakeReport(#startDate) AS Report
UNPIVOT
(
Sales
FOR Day IN (Wed, Thu, Fri, Sat, Sun, Mon, Tue)
) U) AS Report
ON Calendar.Day = Report.Day
LEFT JOIN AppearanceFee af
ON af.LocationId = Report.LocationId
AND date BETWEEN af.StartDate AND IsNull(af.EndDate, '2099-12-21')
) data
PIVOT
(
Sum(Sales)
FOR Day IN (Wed, Thu, Fri, Sat, Sun, Mon, Tue)
) pvt
WHERE LocationId IS NOT NULL
GROUP BY LocationId
http://sqlfiddle.com/#!3/98759/52