SQL - Counting incidents at time period (done) but including another variable - sql-server

I'm newish to SQL so sorry if the code is a little scruffy.
Basically I am creating a count of fire engines in use on every hour, which I have done, and that bit works. So I have a count of this for the past five years. Sorted.
But now I want to run it for a specific group of incidents (about 300 of them), showing how many engines were at that incident, every hour, and how many others were in use at the same time, but somewhere else.
My basic working code (that I modified from https://stackoverflow.com/a/43337534/5880512) is as follows. It just counts all P1 and P2 mobilisations at the defined time.
DECLARE #startdate datetime = '2018-05-03 00:00:00'
DECLARE #enddate datetime = '2018-05-05 00:00:00'
;with cte as
(
select #startdate startdate
union all
select DATEADD(minute, 60, startdate)
FROM cte
WHERE DATEADD(minute, 60, startdate) < #enddate
)
select convert(varchar(20), startdate, 120) as CreationTime, (select count(*) FROM MB_MOBILISATIONS WHERE MB_SEND < startdate and MB_LEAVE > startdate And (MB_CALL_SIGN Like '%P1' Or MB_CALL_SIGN Like '%P2')) as Count
from cte
option (maxrecursion 0)
To split these up for a particular incident, I can put the incident ref into the where clause, one as = so it will give me engines at that incident, and one as <> so it gives me the rest. This bit works too.
select convert(varchar(20), startdate, 120) as CreationTime, (select count(*) FROM MB_MOBILISATIONS WHERE MB_SEND < startdate and MB_LEAVE > startdate And (MB_CALL_SIGN Like '%P1' Or MB_CALL_SIGN Like '%P2') and MB_IN_REF = 1704009991) as 'At Incident'
, select convert(varchar(20), startdate, 120) as CreationTime, (select count(*) FROM MB_MOBILISATIONS WHERE MB_SEND < startdate and MB_LEAVE > startdate And (MB_CALL_SIGN Like '%P1' Or MB_CALL_SIGN Like '%P2') and MB_IN_REF <> 1704009991) as 'Other Incident'
The bit I can't work out to do, is to make this work for multiple incidents, without having to change the incident reference manually in the where clause for all 300.
The incident references I want to use will be stored in a temporary table. Ideally, I would like it to pick an ID, set the variables #startdate and #enddate, from the start and end of that incident, then do the hourly count for the duration of that incident.
Hopefully the results would look something like this
IncidentRef DateTime At Incident Other Incident
A 2018-05-03 1:00 4 2
A 2018-05-03 2:00 7 3
A 2018-05-03 3:00 5 3
A 2018-05-03 4:00 2 4
B 2017-03-01 9:00 7 2
B 2017-03-01 10:00 8 3
B 2017-03-01 11:00 6 1
B 2017-03-01 12:00 4 2
I hope that makes sense.
Thanks :)

Use something like this to limit the scope of your search to a smaller list. I've just added and referenced another CTE with a filter. If you're looking to parameterize the list you'll need a different approach like storing those id values in another table first.
with cte as (
select #startdate startdate
union all
select dateadd(minute, 60, startdate)
from cte
where dateadd(minute, 60, startdate) < #enddate
), mobi as (
select * from MB_MOBILISATIONS
where MB_IN_REF in (<insert list here>)
)
select convert(varchar(20), startdate, 120) as CreationTime, m."Count"
from cte cross apply (
select count(*) as "Count" from mobi
where MB_SEND < startdate and MB_LEAVE > startdate and
(MB_CALL_SIGN like '%P1' or MB_CALL_SIGN like '%P2')
) m;
I went ahead and rewrote your scalar subquery but I guess that's just a personal preference.

Related

Evaluating datetime into timewindows

Im trying to establish for any given datetime a tag that is purely dependent on the time part.
However because the time part is cyclic I cant make it work with simple greater lower than conditions.
I tried a lot of casting and shift one time to 24hour mark to kinda break the cycle However it just gets more and more complicated and still doesnt work.
Im using SQL-Server, here is the situation:
DECLARE #tagtable TABLE (tag varchar(10),[start] time,[end] time);
DECLARE #datetimestable TABLE ([timestamp] datetime)
Insert Into #tagtable (tag, [start], [end])
values ('tag1','04:00:00.0000000','11:59:59.9999999'),
('tag2','12:00:00.0000000','19:59:59.9999999'),
('tag3','20:00:00.0000000','03:59:59.9999999');
Insert Into #datetimestable ([timestamp])
values ('2022-07-24T23:05:23.120'),
('2022-07-27T13:24:40.650'),
('2022-07-26T09:00:00.000');
tagtable:
tag
start
end
tag1
04:00:00.0000000
11:59:59.9999999
tag2
12:00:00.0000000
19:59:59.9999999
tag3
20:00:00.0000000
03:59:59.9999999
for given datetimes e.g. 2022-07-24 23:05:23.120, 2022-07-27 13:24:40.650, 2022-07-26 09:00:00.000
the desired result would be:
date
tag
2022-07-25
tag3
2022-07-27
tag2
2022-07-26
tag1
As I wrote i tried to twist this with casts and adding and datediffs
SELECT
If(Datepart(Hour, a.[datetime]) > 19,
Cast(Dateadd(Day,1,a.[datetime]) as Date),
Cast(a.[datetime] as Date)
) as [date],
b.[tag]
FROM #datetimestable a
INNER JOIN #tagtable b
ON SomethingWith(a.[datetime])
between SomethingWith(b.[start]) and SomethingWith(b.[end])
The only tricky bit here is that your tag time ranges can go over midnight, so you need to check that your time is either between start and end, or if it spans midnight its between start and 23:59:59 or between 00:00:00 and end.
The only other piece is splitting your timestamp column into date and time using a CTE, to save having to repeat the cast.
;WITH splitTimes AS
(
SELECT CAST(timestamp AS DATE) as D,
CAST(timestamp AS TIME) AS T
FROM #datetimestable
)
SELECT
DATEADD(
day,
CASE WHEN b.[end]<b.start THEN 1 ELSE 0 END,
a.D) as timestamp,
b.[tag]
FROM [splitTimes] a
INNER JOIN #tagtable b
ON a.T between b.[start] and b.[end]
OR (b.[end]<b.start AND (a.T BETWEEN b.[start] AND '23:59:59.99999'
OR a.T BETWEEN '00:00:00' AND b.[end]))
Live example: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=506aef05b5a761afaf1f67a6d729446c
Since they're all 8-hour shifts, we can essentially ignore the end (though, generally, trying to say an end time is some specific precision of milliseconds will lead to a bad time if you ever use a different data type (see the first section here) - so if the shift length will change, just put the beginning of the next shift and use >= start AND < end instead of BETWEEN).
;WITH d AS
(
SELECT datetime = [timestamp],
date = CONVERT(datetime, CONVERT(date, [timestamp]))
FROM dbo.datetimestable
)
SELECT date = DATEADD(DAY,
CASE WHEN t.start > t.[end] THEN 1 ELSE 0 END,
CONVERT(date, date)),
t.tag
FROM d
INNER JOIN dbo.tagtable AS t
ON d.datetime >= DATEADD(HOUR, DATEPART(HOUR, t.start), d.date)
AND d.datetime < DATEADD(HOUR, 8, DATEADD(HOUR,
DATEPART(HOUR, t.start), d.date));
Example db<>fiddle
Here's a completely different approach that defines the intervals in terms of starts and durations rather than starts and ends.
This allows the creation of tags that can span multiple days, which might seem like an odd capability to have here, but there might be a use for it if we add some more conditions down the line. For example, say we want to be able say "anything from 6pm friday to 9am monday gets the 'out of hours' tag". Then we could add a day of week predicate to the tag definition, and still use the duration-based interval.
I have defined the duration granularity in terms of hours, but of course this can easily be changed
create table #tags
(
tag varchar(10),
startTimeInclusive time,
durationHours int
);
insert #tags
values ('tag1','04:00:00', 8),
('tag2','12:00:00', 8),
('tag3','20:00:00', 8);
create table #dateTimes (dt datetime)
insert #dateTimes
values ('2022-07-24T23:05:23.120'),
('2022-07-27T13:24:40.650'),
('2022-07-26T09:00:00.000');
select dt.dt,
t.tag
from #datetimes dt
join #tags t on cast(dt.dt as time) >= t.startTimeInclusive
and dt.dt < dateadd
(
hour,
t.durationHours,
cast(cast(dt.dt as date) as datetime) -- strip the time from dt
+ cast(t.startTimeInclusive as datetime) -- add back the time from t
);
Maybe I am looking at this to simple, but,
can't you just take the first tag with an hour greater then your hour in table datetimestable.
With an order by desc it should always give you the correct tag.
This will work well as long as you have no gaps in your tagtable
select case when datepart(hour, tag.tagStart) > 19 then dateadd(day, 1, convert(date, dt.timestamp))
else convert(date, dt.timestamp)
end as [date],
tag.tag
from datetimestable dt
outer apply ( select top 1
tt.tag,
tt.tagStart
from tagtable tt
where datepart(Hour, dt.timestamp) > datepart(hour, tt.tagStart)
order by tt.tagStart desc
) tag
It returns the correct result in this DBFiddle
The result is
date
tag
2022-07-25
tag3
2022-07-27
tag2
2022-07-26
tag1
EDIT
If it is possible that there are gaps in the table,
then I think the most easy and solid solution would be to split that row that passes midnight into 2 rows, and then your query can be very simple
See this DBFiddle
select case when datepart(hour, tag.tagStart) > 19 then dateadd(day, 1, convert(date, dt.timestamp))
else convert(date, dt.timestamp)
end as [date],
tag.tag
from datetimestable dt
outer apply ( select tt.tag,
tt.tagStart
from tagtable tt
where datepart(Hour, dt.timestamp) >= datepart(hour, tt.tagStart)
and datepart(Hour, dt.timestamp) <= datepart(hour, tt.tagEnd)
) tag

build month Start and End dates intervals SQL

I have my getdate() = '2022-03-21 09:24:34.313'
I'd like to build Start Month and End Month dates intervals with SQL language (SQL server) , with the following screen :
You can use EOMONTH function and DATEADD function to get the data you want.
But, the best approach would be to use a calendar table and map it against the current date and get the data you want.
DECLARE #DATE DATE = getdate()
SELECT DATEADD(DAY,1,EOMONTH(#DATE,-1)) AS MonthM_Start, EOMONTH(#DATE) AS MonthM_End,
DATEADD(DAY,1,EOMONTH(#DATE,-2)) AS MonthOneBack_Start, EOMONTH(#DATE,-1) AS MonthOneBack_End,
DATEADD(DAY,1,EOMONTH(#DATE,-3)) AS MonthTwoBack_Start, EOMONTH(#DATE,-2) AS MonthTwoBack_End,
DATEADD(DAY,1,EOMONTH(#DATE,-4)) AS MonthThreeBack_Start, EOMONTH(#DATE,-3) AS MonthThreeBack_End
MonthM_Start
MonthM_End
MonthOneBack_Start
MonthOneBack_End
MonthTwoBack_Start
MonthTwoBack_End
MonthThreeBack_Start
MonthThreeBack_End
2022-03-01
2022-03-31
2022-02-01
2022-02-28
2022-01-01
2022-01-31
2021-12-01
2021-12-31
You can use a recursive CTE to avoid having to hard-code an expression for each month boundary you need, making it very easy to handle fewer or more months by just changing a parameter.
Do you really need the end date for processing? Seems more appropriate for a label, since date/time types can vary - meaning the last day of the month at midnight isn't very useful if you're trying to pull any data from after midnight on the last day of the month.
This also shows how to display the data for each month even if there isn't any data in the table for that month.
DECLARE #number_of_months int = 4,
#today date = DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1);
;WITH m(s) AS
(
SELECT #today UNION ALL SELECT DATEADD(MONTH, -1, s) FROM m
WHERE s > DATEADD(MONTH, 1-#number_of_months, #today)
)
SELECT MonthStart = m.s, MonthEnd = EOMONTH(m.s)--, other cols/aggs
FROM m
--LEFT OUTER JOIN dbo.SourceTable AS t
--ON t.datetime_column >= m
--AND t.datetime_column < DATEADD(MONTH, 1, m);
Output (without the join):
MonthStart
MonthEnd
2022-03-01
2022-03-31
2022-02-01
2022-02-28
2022-01-01
2022-01-31
2021-12-01
2021-12-31
Example db<>fiddle
But, as mentioned in a comment, you could easily store this information in a calendar table, too, and just outer join to that:
SELECT c.TheFirstOfMonth, c.TheLastOfMonth --, other cols/aggs
FROM dbo.CalendarTable AS c
LEFT OUTER JOIN dbo.SourceTable AS t
ON t.datetime_column >= c.TheFirstOfMonth
AND t.datetime_column < c.TheFirstOfNextMonth
WHERE c.FirstOfMonth >= DATEADD(MONTH, -4, GETDATE())
AND c.FirstOfMonth < GETDATE();

GROUP BY DAY, CUMULATIVE SUM

I have a table in MSSQL with the following structure:
PersonId
StartDate
EndDate
I need to be able to show the number of distinct people in the table within a date range or at a given date.
As an example i need to show on a daily basis the totals per day, e.g. if we have 2 entries on the 1st June, 3 on the 2nd June and 1 on the 3rd June the system should show the following result:
1st June: 2
2nd June: 5
3rd June: 6
If however e.g. on of the entries on the 2nd June also has an end date that is 2nd June then the 3rd June result would show just 5.
Would someone be able to assist with this.
Thanks
UPDATE
This is what i have so far which seems to work. Is there a better solution though as my solution only gets me employed figures. I also need unemployed on another column - unemployed would mean either no entry in the table or date not between and no other entry as employed.
CREATE TABLE #Temp(CountTotal int NOT NULL, CountDate datetime NOT NULL);
DECLARE #StartDT DATETIME
SET #StartDT = '2015-01-01 00:00:00'
WHILE #StartDT < '2015-08-31 00:00:00'
BEGIN
INSERT INTO #Temp(CountTotal, CountDate)
SELECT COUNT(DISTINCT PERSON.Id) AS CountTotal, #StartDT AS CountDate FROM PERSON
INNER JOIN DATA_INPUT_CHANGE_LOG ON PERSON.DataInputTypeId = DATA_INPUT_CHANGE_LOG.DataInputTypeId AND PERSON.Id = DATA_INPUT_CHANGE_LOG.DataItemId
LEFT OUTER JOIN PERSON_EMPLOYMENT ON PERSON.Id = PERSON_EMPLOYMENT.PersonId
WHERE PERSON.Id > 0 AND DATA_INPUT_CHANGE_LOG.Hidden = '0' AND DATA_INPUT_CHANGE_LOG.Approved = '1'
AND ((PERSON_EMPLOYMENT.StartDate <= DATEADD(MONTH,1,#StartDT) AND PERSON_EMPLOYMENT.EndDate IS NULL)
OR (#StartDT BETWEEN PERSON_EMPLOYMENT.StartDate AND PERSON_EMPLOYMENT.EndDate) AND PERSON_EMPLOYMENT.EndDate IS NOT NULL)
SET #StartDT = DATEADD(MONTH,1,#StartDT)
END
select * from #Temp
drop TABLE #Temp
You can use the following query. The cte part is to generate a set of serial dates between the start date and end date.
DECLARE #ViewStartDate DATETIME
DECLARE #ViewEndDate DATETIME
SET #ViewStartDate = '2015-01-01 00:00:00.000';
SET #ViewEndDate = '2015-02-25 00:00:00.000';
;WITH Dates([Date])
AS
(
SELECT #ViewStartDate
UNION ALL
SELECT DATEADD(DAY, 1,Date)
FROM Dates
WHERE DATEADD(DAY, 1,Date) <= #ViewEndDate
)
SELECT [Date], COUNT(*)
FROM Dates
LEFT JOIN PersonData ON Dates.Date >= PersonData.StartDate
AND Dates.Date <= PersonData.EndDate
GROUP By [Date]
Replace the PersonData with your table name
If startdate and enddate columns can be null, then you need to add
addditional conditions to the join
It assumes one person has only one record in the same date range
You could do this by creating data where every start date is a +1 event and end date is -1 and then calculate a running total on top of that.
For example if your data is something like this
PersonId StartDate EndDate
1 20150101 20150201
2 20150102 20150115
3 20150101
You first create a data set that looks like this:
EventDate ChangeValue
20150101 +2
20150102 +1
20150115 -1
20150201 -1
And if you use running total, you'll get this:
EventDate Total
2015-01-01 2
2015-01-02 3
2015-01-15 2
2015-02-01 1
You can get it with something like this:
select
p.eventdate,
sum(p.changevalue) over (order by p.eventdate asc) as total
from
(
select startdate as eventdate, sum(1) as changevalue from personnel group by startdate
union all
select enddate, sum(-1) from personnel where enddate is not null group by enddate
) p
order by p.eventdate asc
Having window function with sum() requires SQL Server 2012. If you're using older version, you can check other options for running totals.
My example in SQL Fiddle
If you have dates that don't have any events and you need to show those too, then the best option is probably to create a separate table of dates for the whole range you'll ever need, for example 1.1.2000 - 31.12.2099.
-- Edit --
To get count for a specific day, it's possible use the same logic, but just sum everything up to that day:
declare #eventdate date
set #eventdate = '20150117'
select
sum(p.changevalue)
from
(
select startdate as eventdate, 1 as changevalue from personnel
where startdate <= #eventdate
union all
select enddate, -1 from personnel
where enddate < #eventdate
) p
Hopefully this is ok, can't test since SQL Fiddle seems to be unavailable.

Getting plot data from date-range efficiently

I'm trying to create utilization graph for a telephone system. I have sets of data which is in the table format
ID *
StartDate
EndDate
From
To
What I'm trying to do is get SQL to to output me a list of plot points every 5 minutes, so basically
The count of active calls (between StartDate and EndDate) for every 5 minutes in a day.
The result beign something like
Date Time Count
2000-01-01 00:00:00 10
2000-01-01 00:05:00 2
2000-01-01 00:10:00 7
Can anyone suggest a way to generate said data? I'm at a loss here! The stuff I've been thinking abut all involves a creating a big loop and running a query for every 5 seconds which seems super inefficient.
The method I was originally thinking was :-
storedProc GetSamples(SampleStartDate, SampleEndDate)
Create memory table for result data
for every 5mins as sample between SampleStartDate and SampleEndDate
SELECT #SampleCount = COUNT(1) FROM Samples where 5mins BETWEEN StartDate AND EndDate
UPDATE memoryTable SET count=#SampleCount WHERE time = 5mins
end
end
If you have a recent enough version of SQL Server (2008+), you ought to be able to do this with a CTE joined to your phone call log, like this (CTE base found here):
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2000-01-01 00:00:00.000'
SET #EndDate = '2000-01-02 00:00:00.000'
;WITH DateSequence( [PlotPointDate] ) AS
(
SELECT #StartDate AS [PlotPointDate]
UNION ALL
SELECT DATEADD(MINUTE, 5, [PlotPointDate] )
FROM DateSequence
WHERE [PlotPointDate] < #EndDate
)
--select result
SELECT
PlotPointDate, COUNT(YourTable.ID) AS TotalActiveCalls
FROM
DateSequence
LEFT JOIN
YourTable ON
YourTable.StartDate <= DateSequence.PlotPointDate AND
(YourTable.EndDate >= DateSequence.PlotPointDate OR YourTable.EndDate IS NULL)
GROUP BY PlotPointDate
OPTION (MaxRecursion 10000)

SQL QUERY ( SQL SERVER) Date & Time Difference

I have a SQL SERVER Table which has only one field "StartDate" the records are as follows
**
2011-07-28 19:30:00.000
2011-07-29 21:50:00.000
2011-07-25 09:20:00.000
**
What i want to do is :
SHOW RECORDS if its CURRENT DATE ( todays date ) and the time difference between current time the StartDate is not less then 5 minutes, i have written the following code but it doesnt show me the time difference ?
SELECT * FROM table WHERE DATEDIFF(day, StartDate, GETDATE()) <= 0
SELECT StartDate
FROM table
WHERE YEAR(StartDate)=YEAR(GETDATE())
AND MONTH(StartDate)=MONTH(GETDATE())
AND DAY(StartDate)=DAY(GETDATE())
AND (DATEDIFF(minute, StartDate, GETDATE()) >= 5
OR
DATEDIFF(minute, StartDate, GETDATE()) <= 5)
How about:
SELECT StartDate
,GETDATE()
,DATEDIFF(day, StartDate, GETDATE())
,DATEDIFF(minute, StartDate, GETDATE())
,*
FROM table
WHERE DATEDIFF(day, StartDate, GETDATE()) <= 0
AND DATEDIFF(minute, StartDate, GETDATE()) >= 5
There are two ways to do it one being DateDiff the other is DATEADD. Judging by the way I read your question DateAdd should do it. Here is an example;
SELECT *
FROM [dbo].[TABLE]
WHERE [LAST_UPDATE] > DATEADD(minute,-5,GetDate())
Using BETWEEN is probably a little more optimal than two AND statements (maybe not). Try not to do a calculation on each row if you don't have to. Doing DATEADD only on the current date will only be calculated once.
SELECT
whatever
FROM
table
WHERE
StartDate
BETWEEN FLOOR( CAST( GETDATE() AS FLOAT ) )
AND DATEADD(minute, -5, GETDATE())
I interpret the question as looking for rows where the date is today (between the start of today) but not within the last 5 minutes (and less than 5 minutes ago). That might not be what you were going for.
Hope that helps

Resources