Filter based on multiple date ranges? - sql

The user clicks on a month and then this stored procedure is executed. It checks for the total booked time and what groups have been filtered.
| Job Group | Month Booked | Time (hrs) |
Cleaning Jan 7
I have the following SQL:
SELECT
tsks.grouping_ref, ttg.description AS grouping_desc,
SUM(ts.booked_time) AS booked_time_total,
DATENAME(MONTH, ts.start_dtm) + ' ' + DATENAME(YEAR, ts.start_dtm) AS month_name,
#month_ref AS month_ref
FROM
timesheets ts
JOIN
timesheet_categories cat ON ts.timesheet_cat_ref = cat.timesheet_cat_ref
JOIN
timesheet_tasks tsks ON ts.task_ref = tsks.task_ref
JOIN
timesheet_task_groupings ttg ON tsks.grouping_ref = ttg.grouping_ref
WHERE
ts.status IN(1, 2) --Booked and approved
AND cat.is_leave_category = 0 --Ignore leave
AND DATEPART(YEAR, ts.start_dtm) = #Year
AND DATEPART(MONTH, ts.start_dtm) = #Month
GROUP BY
tsks.grouping_ref, ttg.description,
DATENAME(MONTH, ts.start_dtm),
DATENAME(YEAR, ts.start_dtm)
ORDER BY
grouping_desc
I want to filter based on multiple date ranges.
I thought about adding this:
AND ((ts.start_dtm BETWEEN '2011-12-28' AND '2012-01-01')
OR (ts.start_dtm BETWEEN '2012-01-02' AND '2012-01-29'))
But then realized it wouldn't matter what month the user clicked it would still show all the records as it will carry out the OR statement.
What I need is something that's based on the month_ref, eg:
CASE WHEN #month_ref = 81201 THEN
AND (ts.start_dtm BETWEEN '2011-12-28' AND '2012-01-01')
END
But the case statement needs to go just after the WHERE clause.
I have about 12 accounting months for 2012 which I need to add as case statements so that when the user clicks on March, it will fire the correct filter.
In the database ts.start_dtm looks like this:
2011-04-01 00:00:00.000
Hope that was enough information for my first post?
I'm stuck writing the case statement and where to put it, been trying for hours now.
Hope you can help :)

Give the irregular nature of your dates would preclude using dateparts; I would build a temporary table of the permissible dates based on the user query and join on it. The static integers table in my app has 1 through 64000 your tables may vary.
DECLARE
#startdate DateTime = '2012-05-01',
#EndDate DateTime = '2012-06-03'
DECLARE
#AllDates TABLE (MyDate DateTime)
INSERT INTO #AllDates
SELECT
DATEADD(dd, StaticInteger, #startdate)
FROM dbo.tblStaticIntegers
WHERE StaticInteger <= DATEDIFF(dd, #startdate, #EndDate)

One option would be to have a table mapping the month reference number to a start and end date thus retrieving those values and using them in your ts.start_dtm check. ie it would have:
Month-ref | Start | End
81201 | 2011-12-28 | 2012-01-01
81202 | 2012-01-02 | 2012-01-29
etc
You can just join to this reference table or alternatively retrieve the two dates before your main query

Related

Generate List of dates between 2 dates for each Id

I have a table with PersonId's that each have a FirstSubscription date and LastSubscriptionDate.
What I need to do is between those 2 dates, generate 1 date for each month. This is for reporting purposes on the front end, as this data will end up inside PowerBI and I need these dates to join to a ReportingCalendar.
This Calendar is accessible by SQL so it can be used in this calculation. I am using it to generate the dates (using first of the month) between the First and LastSubDate but I need to find a way to join this with the rest of the ID's that way I get a list of date for each ID.
Here is my code to generate the dates.
DECLARE #MinDate DATE
DECLARE #MaxDate DATE
SET #MinDate = '2020-08-31'
SET #MaxDate = '2022-08-30'
SELECT DATEADD(month, DATEDIFF(month, 0, date), 0)
FROM dbo.ReportingCalendar
WHERE Date >= #MinDate
AND Date < #MaxDate
GROUP BY
DATEADD(month, DATEDIFF(month, 0, date), 0)
My PersonSubscription table looks like this
|PersonId|FirstSubDate|LastSubDate|
|--------|------------|-----------|
|1186 |8/31/2020 |8/30/2022 |
|2189 |7/30/2019 |7/31/2021 |
So I would want to end up with an output where each PersonId has 1 entry for each month between those 2 dates. So PersonId has 25 entries from 8/2020 until 8/2022. We don't care about the actual date of the sub since this data is looked at monthly and will primarily be looked at using a Distinct Count each month, so we only care if they were subbed at any time in that month.
I just needed to do a Cross Apply.
I took my code that got me all of the PersonId's and their FirstSubDate and LastSubDate and then did a cross apply to the code I listed above, referencing the MinDate and MaxDate with the FirstSubDate and LastSubDate.

How do I include months that have no data?

I am trying to create a report that shows how many training records will expire within a chosen date range however when I run the report it excludes months that have no training records going out of date. I have tried various solutions I've seen posted but I haven't been able to get any of them to work in my case.
This is my query:
SELECT COUNT(ISNULL(TRAININGRECORDID, 0)) AS NUMBEROFRECORDS
,DEPARTMENTNUMBER
,DATENAME( Month, EXPIRY ) + '-' + DATENAME( Year, EXPIRY ) AS [MONTHYEAR]
FROM Training_Records TR
JOIN Departments TD ON TR.DEPARTMENTID = TD.DEPARTMENTID
WHERE TR.EXPIRY IS NOT NULL
AND TD.DEPARTMENTNUMBER IN (#DEPTNO)
AND TR.EXPIRY BETWEEN #StartDate AND #EndDate
GROUP BY TD.DEPARTMENTNUMBER, DATENAME(Year, TR.EXPIRY), DATENAME(Month, TR.EXPIRY)
ORDER BY TD.DEPARTMENTNUMBER, [MONTHYEAR]
An example of results from this query looks like this:
NUMBEROFRECORDS DEPARTMENTNUMBER MONTHYEAR
1 21 April-2023
4 23 June-2023
1 83 August-2023
I am displaying the results of this query in a matrix with MONTHYEAR as the columns. In the example above the report will display April, June and August 2023 but will skip over the months May, July 2023 because there are no records going out of date in those months but despite that I still want them displayed in my report/returned in my query.
I've tried various solutions I've found on here but none of them have worked for me. How would I go about including these months with no records going out of date?
You need to first get all of the months, and then outer join to them (not using BETWEEN). Here is an example that gets April, May, June, and July, and then shows how you would outer join that against your table.
DECLARE #StartDate date = '20220405',
#EndDate date = '20220708';
;WITH Months(TheMonth) AS
(
SELECT DATEFROMPARTS(YEAR(#StartDate), MONTH(#StartDate), 1)
UNION ALL
SELECT DATEADD(MONTH, 1, TheMonth)
FROM Months
WHERE TheMonth < DATEFROMPARTS(YEAR(#EndDate), MONTH(#EndDate), 1)
)
SELECT TheMonth -- , COALESCE(SUM({your table}.{column}),0)
FROM Months AS m
-- LEFT OUTER JOIN {your table}
-- ON {your table}.{date column} >= m.TheMonth
-- AND {your table}.{date column} < DATEADD(MONTH, 1, m.TheMonth);
Output:
TheMonth
2022-04-01
2022-05-01
2022-06-01
2022-07-01
Example db<>fiddle
If your range could last more than 100 months, you'll need to add:
OPTION (MAXRECURSION 0);

Date closest to current date in SQL (prior dates or future dates)

I am running into a bit of an issue with finding out how to look for dates closest to the current date. I looked at this among others:
Get closest date to current date sql
I also looked here: https://bytes.com/topic/sql-server/answers/79502-get-closest-date
I am using MS SQL 2012 and there have been many questions asked about this, so I apologize for bringing it back up. I can't seem to get my query to work.
Basically I have a table as follows:
ITEMNMBR | MINDATE | MAXDATE | CLOSESTDATE
------------------------------------------------
123456 | 2017-10-15 | 2017-11-04 | NULL
654321 | 2017-09-29 | 2017-12-08 | NULL
The current date would be today, '2017-10-03'. We would find for item number '123456' the closest date of purchase is 2017-10-15. For item number '654321' the closest date of purchase is 2017-09-29 as that happened much more recently than how long it will take for the next purchase to take effect (hence I am looking at an absolute value of the difference between the dates). You'll have to forgive me, but the query I am including doesn't include the "CLOSESTDATE" column. I've included it there to let you know that any of my calculations have rendered that column NULL. So here's what I have:
--Lines commented below are not used in the current iteration of the query
--DECLARE #dt DATETIME
--SET #dt = GETDATE()
SELECT
I.ITEMNMBR,
MIN(PDATE1) AS MINDATE,
MAX(PDATE1) AS MAXDATE
FROM dbo.IV00101 I
LEFT OUTER JOIN
(SELECT P.[Item Number],
P.[Req Date] AS PDATE1
FROM dbo.Purchases P
WHERE ((P.[Document Status] = 'Open') AND
(P.[POStat] <> 'Closed') AND
(P.[POStat] <> 'Received')) AND P.[Req Date] >= DATEADD(d, -15,
DATEDIFF(d, 0, GETDATE()))
) AS P ON P.[Item Number]= I.ITEMNMBR
WHERE P.[Item Number] = '123456'
GROUP BY
I.ITEMNMBR
ORDER BY MINDATE DESC
When I run this query, I get the table I outlined previously, minus the "CLOSESTDATE" column. The "CLOSESTDATE" column is what I want to use to display what date of purchase is closest to TODAY. Basically, if a date of purchase happened three days ago and the next date of purchase is a month out, then I want to show the date of purchase from three days ago. Also, the query can be written without using a subquery, but I was using other calculations within the subquery prior to reverting back to the rather simplistic original query. Thus, the query can be written like this:
SELECT
I.ITEMNMBR,
MIN(P.[Req Date]) AS MINDATE,
MAX(P.[Req Date]) AS MAXDATE
FROM dbo.IV00101 I
LEFT OUTER JOIN
Purchases P ON P.[Item Number] = I.ITEMNMBR
WHERE P.[Item Number] = '123456' ((P.[Document Status] = 'Open') AND
(P.[POStat] <> 'Closed') AND
(P.[POStat] <> 'Received')) AND P.[Req Date] >= DATEADD(d, -15,
DATEDIFF(d, 0, GETDATE()))
GROUP BY
I.ITEMNMBR
ORDER BY MINDATE DESC
Lastly, as you can see, I have a date constraint for the past 15 days so that anything older than that won't show up at all.
Many thanks in advance!
I'm still not quite understanding your question, but I hope this gives you a start, you can try the rextester sample here
But what it looks like to me is you need a simple case statement:
select ITEMNMBR
,case when abs(datediff(day, MINDATE, convert(date,getdate()))) > abs(datediff(day, MAXDATE, convert(date,getdate()))) then 'MINDATE is greater' else 'MAXDATE is greater' end as ClosestDate
from myTable
Kindly let me know if you have any questions. I'd be happy to help.
I have taken your data and schema and made an approximation of what I believe your underlying data actually looks like. From that, your problem is actually very simple:
declare #t table(ItemNumber int, ReqDate datetime, DocumentStatus nvarchar(100), POStat nvarchar(100))
insert into #t values
(123456,'2017-10-15','Open','Not Closed')
,(123456,'2017-11-04','Open','Not Closed')
,(654321,'2017-09-29','Open','Not Closed')
,(654321,'2017-12-08','Open','Not Closed')
,(123456,'2017-10-11','Open','Closed')
,(123456,'2017-11-01','Closed','Not Closed')
,(654321,'2017-09-21','Closed','Not Closed')
,(654321,'2017-12-01','Open','Received');
select t.ItemNumber
,min(t.ReqDate) as MinDate
,max(t.ReqDate) as MaxDate
-- Find the difference in days for both Min and Max dates, converting to positive numbers where negative,
,case when abs(datediff(d,min(t.ReqDate),getdate())) < abs(datediff(d,max(t.ReqDate),getdate()))
then min(t.ReqDate) -- And then return the appropriate one.
else max(t.ReqDate)
end as ClosestDate
from #t t
where t.DocumentStatus = 'Open'
and t.POStat not in('Closed','Received')
and t.ReqDate >= dateadd(d,-15,cast(getdate() as date))
group by t.ItemNumber
order by MinDate desc;
Output:
+------------+-------------------------+-------------------------+-------------------------+
| ItemNumber | MinDate | MaxDate | ClosestDate |
+------------+-------------------------+-------------------------+-------------------------+
| 123456 | 2017-10-15 00:00:00.000 | 2017-11-04 00:00:00.000 | 2017-10-15 00:00:00.000 |
| 654321 | 2017-09-29 00:00:00.000 | 2017-12-08 00:00:00.000 | 2017-09-29 00:00:00.000 |
+------------+-------------------------+-------------------------+-------------------------+
Change the initial part of the query with this:
SELECT DISTINCT
I.ITEMNMBR,
MIN(PDATE1) AS MINDATE,
MAX(PDATE1) AS MAXDATE
IF(ABS(DATEDIFF(MIN(PDATE1)-SYSDATETIME())) >
ABS(DATEDIFF(MAX(PDATE1)-SYSDATETIME())),
MAX(PDATE1),MIN(PDATE1)) as CLOSESTDATE

How to return a default value when no rows are returned from the select statement

I have a select statement that returns two columns, a date column, and a count(value) column. When the count(value) column doesn't have any records, I need it to return a 0. Currently, it just skips that date record all together.
Here is the basics of the query.
select convert(varchar(25), DateTime, 101) as recordDate,
count(Value) as recordCount
from History
where Value < 700
group by convert(varchar(25), DateTime, 101)
Here are some results that I'm getting.
+------------+-------------+
| recordDate | recordCount |
+------------+-------------+
| 02/26/2014 | 143 |
| 02/27/2014 | 541 |
| 03/01/2014 | 21 |
| 03/02/2014 | 60 |
| 03/03/2014 | 113 |
+------------+-------------+
Notice it skips 2/28/2014. This is because the count(value) column doesn't have anything to count. How can I add the record in there that has the date of 2/28/2014, with a recordCount of 0?
To generate rows for missing dates you can join your data to a date dimension table
It would look something like this:
select convert(varchar(25), ddt.DateField, 101) as recordDate,
count(t.Value) as recordCount
from History h
right join dbo.DateDimensionTable ddt
on ddt.DateField = convert(varchar(25), h.DateTime, 101)
where h.Value < 700
group by convert(varchar(25), h.DateTime, 101)
If your table uses the DateTime column to store dates only (meaning the time is always midnight), then you can replace this
right join dbo.DateDimensionTable ddt
on ddt.DateField = convert(varchar(25), h.DateTime, 101)
with this
right join dbo.DateDimensionTable ddt
on ddt.DateField = h.DateTime
You may use COUNT(*). It will return zero if nothing was found for the column. Also you may group result set by value column if it is needed.
select convert(varchar(25), DateTime, 101) as recordDate,
CASE WHEN count(value) =0 THEN 0 ELSE COUNT(value) END recordCount
from History
where Value < 700
group by convert(varchar(25), DateTime, 101)
When you use a group by, it only creates a distinct list of values that exist in your records. Since 20140228 has no records, it will not show up in the group by.
Your best bet is to generate a list of values, dates in your case, and left join or apply that table against your history table.
I can't seem to copy my T-SQL in here so here's a hastebin.
http://hastebin.com/winaqutego.vbs
The best practice would be for you to have a datamart where a separate dimensional table for dates is kept with all dates you might be interested at - even if they lack amounts. DMason's answer shows the query with such a dimensional table.
To keep with the best practices you would have a fact table where you'd keep these historical data already pre-grouped at the granularity level you need (daily, in this case), so you wouldn't need a GROUP BY unless you needed a coarser granularity (weekly, monthly, yearly).
And in both your operational and datamart databases the dates would be stored as dates, not...
But then, since this is real world and you might not be able to change what somebody else made... If you: a) only care about the dates that appear in [History], and b) such dates are never stored with hours/minutes; then following query might be what you'd need:
SELECT MyDates.DateTime, COUNT(*)-1 AS RecordCount
FROM (
SELECT DISTINCT DateTime FROM History
) MyDates
LEFT JOIN History H
ON MyDates.DateTime = H.Datetime
AND H.Value < 700
GROUP BY MyDates.DateTime
Do try to add an index over DateTime and to further constrain the query with an earliest/latest date for better performance results.
I agree that a Dates table (AKA time dimension) is the right solution, but there is a simpler option:
SELECT
CONVERT(VARCHAR(25), DateTime, 101) AS RecordDate,
SUM(CASE WHEN Value < 700 THEN 1 ELSE 0 END) AS RecordCount
FROM
History
GROUP BY
CONVERT(VARCHAR(25), DateTime, 101)
Try this:
DECLARE #Records TABLE (
[RecordDate] DATETIME,
[RecordCount] INT
)
DECLARE #Date DATETIME = '02/26/2014' -- Enter whatever date you want to start with
DECLARE #EndDate DATETIME = '03/31/2014' -- Enter whatever date you want to stop
WHILE (1=1)
BEGIN
-- Insert the date into the temp table along with the count
INSERT INTO #Records (RecordDate, RecordCount)
VALUES (CONVERT(VARCHAR(25), #Date, 101),
(SELECT COUNT(*) FROM dbo.YourTable WHERE RecordDate = #Date))
-- Go to the next day
#Date = DATEADD(d, 1, #Date)
-- If we have surpassed the end date, break out of the loop
IF (#Date > #EndDate) BREAK;
END
SELECT * FROM #Records
If your dates have time components, you would need to modify this to check for start and end of day in the SELECT COUNT(*)... query.

Calculate items, loop by month, adding month each time through

I have a table of tickets. I am trying to calculate how many tickets were "open" at each month end over the course of the current year. As well, I am pushing this to a bar chart and I am needing out put this into an array through LINQ.
My SQL query to get my calculation is:
SELECT
(SELECT COUNT(*) FROM tblMaintenanceTicket t WHERE (CreateDate < DATEADD(MM, 1, '01/01/2012')))
-
(SELECT COUNT(*) FROM tblMaintenanceTicket t WHERE (CloseDate < DATEADD(MM, 1, '01/01/2012'))) AS 'Open #Month End'
My logic is the following: Count all tickets open between first and end of the month. Subtract that count from the tickets closed before the end of the month.
UPDATED:
I have updated my query with the comments below and it is not working with errors in the GROUP, but I am not truly understanding the logic I guess, my lack of skill in SQL is to blame.
I have added a SQL Fiddle example to show you my query: http://sqlfiddle.com/#!3/c9b638/1
Desired output:
-----------
| Jan | 3 |
-----------
| Feb | 4 |
-----------
| Mar | 0 |
-----------
Your SQL has several erros . . . are grouping by CreateDate but you don't have it as a column from the subqueries. And, you don't have a column alias on the count(*).
I think this is what you are trying to do:
select DATENAME(MONTH,CreateDate), DATEPART(YEAR,CreateDate),
(sum(case when CreateDate < DATEADD(MM, 1, '01/01/2012') then 1 else 0 end) -
sum(case when CloseDate < DATEADD(MM, 1, '01/01/2012') then 1 else 0 end)
)
from tblMaintenanceTicket
group by DATENAME(MONTH,CreateDate), DATEPART(YEAR,CreateDate)
Your comment seems to elucidate what you want clearer than your question (the explanation in the question is a bit buried). What you need is a driver table of months and then join this to your table. Something like:
select mons.yr, mons.mon, count(*) as OpenTickets
from (select month(CreateDate) as mon, year(CreateDate) as yr,
cast(min(CreateDate) as date) as MonthStart,
cast(max(CreateDate) as date) as monthEnd
from tblMaintenanceTicket
group by month(CreateDate), year(CreateDate)
) mons left outer join
tblMaintenanceTicket mt
on mt.CreateDate <= mons.MonthEnd and
(mt.CloseDate > mons.MonthEnd or mt.CloseDate is null)
group by mons.yr, mons.mon
I am assuming records are created on every day. This is a convenience so I don't have to think about getting the first and last day of each month using other SQL functions.
If your query is returning what you need, then simply use DATENAME(MONTH, yourDate) to retrieve the month and group by Month,Year:
SELECT SUM(*), DATENAME(MONTH,yourDate), DATEPART(YEAR,yourDate)
FROM
(
your actual query here
)
GROUP BY DATENAME(MONTH,yourDate), DATEPART(YEAR,yourDate)