Selecting description of 12 hour time period SQL Server 2008 - sql

I am writing a query for a report in which I need to select if the time period falls into the "Day" or "Night". The table I wish to select from looks like this:
DAYNIGHT_TABLE:
Description | StartTime | EndTime
Night | 18:00:00.0000000 | 06:00:00.0000000
Day | 06:00:00.0000000 | 18:00:00.0000000
For different customers this may be broken up further into smaller time slots but as a starting point I just need to select Day or Night based on a datetime that I retrieve from another table.
If I do something like this then I only get day:
SELECT Description
FROM Table s
WHERE convert(TIME,myTable.MyDateTime) >= s.StartTime
AND convert(TIME,myTable.MyDateTime) < s.StartTime
I feel like I am missing something obvious here but how can I retrieve the Day/Night Description from DAYNIGHT_TABLE based on the known time from myTable.MyDateTime?

If you can't touch the DAYNIGHT_TABLE table at all, read below.
Since all you need is a label "Day" or "Night" I would recommend to alter your DAYNIGHT_TABLE table and include one more row into it, so that no range of times in any row goes across the midnight. It makes query much simpler and efficient:
DAYNIGHT_TABLE:
Description | StartTime | EndTime
Night | 18:00:00.0000000 | 00:00:00.0000000
Night | 00:00:00.0000000 | 06:00:00.0000000
Day | 06:00:00.0000000 | 18:00:00.0000000
Also, make sure that you consistently assume which end of the interval is inclusive and which is exclusive. I'll assume that it is [StartTime; EndTime), i.e. StartTime inclusive and EndTime exclusive.
Have an index on StartTime DESC with Description as included column. It is not really needed and with so few rows in DAYNIGHT_TABLE you would likely not notice any difference, but still.
Now, if you have a table myTable with column MyDateTime and you want to get correct matching description from DAYNIGHT_TABLE, use something like this:
SELECT
myTable.MyDateTime
,CA.Description
FROM
myTable
CROSS APPLY
(
SELECT TOP(1) DAYNIGHT_TABLE.Description
FROM DAYNIGHT_TABLE
WHERE DAYNIGHT_TABLE.StartTime <= CAST(myTable.MyDateTime AS time)
ORDER BY DAYNIGHT_TABLE.StartTime DESC
) AS CA
For every row in myTable CROSS APPLY would find one matching Description.
Make sure that intervals in DAYNIGHT_TABLE cover all 24 hours without gaps.
Notice, that query doesn't use EndTime at all, because it assumes that intervals in the table cover full 24 hours. So, you can remove this column from the table and it will look like this:
Description | StartTime
Night | 00:00:00.0000000
Day | 06:00:00.0000000
Night | 18:00:00.0000000
With such table that has these two columns the only index that is needed is just a unique clustered primary key on StartTime.
If you can't touch the DAYNIGHT_TABLE table at all, but if you still can guarantee that intervals in the table (a) don't overlap, (b) don't have gaps, (c) cover full 24 hours, then there will be one extra step in the query.
If all conditions outlined above hold true, then there can be at most only one row that spans across midnight, which means that its StartTime is greater than its EndTime. This is how we can find it and take care of it. Again, I assume that StartTime is inclusive and EndTime is exclusive.
DECLARE #DAYNIGHT_TABLE TABLE(Description varchar(50), StartTime time, EndTime time);
INSERT INTO #DAYNIGHT_TABLE(Description, StartTime, EndTime) VALUES
('Night', '18:00:00', '06:00:00'),
('Day', '06:00:00', '18:00:00');
DECLARE #myTable TABLE(MyDateTime datetime);
INSERT INTO #myTable (MyDateTime) VALUES
('2015-01-01 00:00:00'),
('2015-01-01 02:02:02'),
('2015-01-01 06:00:00'),
('2015-01-01 12:02:02'),
('2015-01-01 18:00:00'),
('2015-01-01 22:02:02');
WITH
CTE_TimeIntervals
AS
(
SELECT
Description
,StartTime
FROM #DAYNIGHT_TABLE AS DAYNIGHT_TABLE
UNION ALL
SELECT
Description
,CAST('00:00:00' AS time) AS StartTime
FROM #DAYNIGHT_TABLE AS DAYNIGHT_TABLE
WHERE StartTime >= EndTime
)
SELECT
myTable.MyDateTime
,CA.Description
FROM
#myTable AS myTable
CROSS APPLY
(
SELECT TOP(1) CTE_TimeIntervals.Description
FROM CTE_TimeIntervals
WHERE CTE_TimeIntervals.StartTime <= CAST(myTable.MyDateTime AS time)
ORDER BY CTE_TimeIntervals.StartTime DESC
) AS CA
Result set
MyDateTime Description
2015-01-01 00:00:00.000 Night
2015-01-01 02:02:02.000 Night
2015-01-01 06:00:00.000 Day
2015-01-01 12:02:02.000 Day
2015-01-01 18:00:00.000 Night
2015-01-01 22:02:02.000 Night

You need somthing like this, test it with different values:
http://www.sqlfiddle.com/#!3/d6328/1/0
select s1.myDate, t.Descr
from s s1
inner join DAYNIGHT_TABLE t on datepart(hour, s1.myDate) >= datepart(hour, t.StartTime)
AND datepart(hour, s1.myDate) < datepart(hour, t.EndTime)
OR ((t.EndTime < t.StartTime) AND ( datepart(hour, s1.myDate) >= datepart(hour, t.StartTime)) AND datepart(hour, s1.mydate) < (datepart(hour, t.StartTime) + datepart(hour, t.EndTime)))

Related

T-sql count number of times a week on rows with date interval

If you have table like this:
Name
Data type
UserID
INT
StartDate
DATETIME
EndDate
DATETIME
With data like this:
UserID
StartDate
EndDate
21
2021-01-02 00:00:00
2021-01-02 23:59:59
21
2021-01-03 00:00:00
2021-01-04 15:42:00
24
2021-01-02 00:00:00
2021-01-06 23:59:59
And you want to calculate number of users that is represented on each day in a week with a result like this:
Year
Week
NumberOfTimes
2021
1
8
2021
2
10
2021
3
4
Basically I want to to a Select like this:
SELECT YEAR(dateColumn) AS yearname, WEEK(dateColumn)as week name, COUNT(somecolumen)
GROUP BY YEAR(dateColumn) WEEK(dateColumn)
The problem I have is the start and end date if the date goes over several days I want it to counted each day. Preferably I don't want the same user counted twice each day. There are millions of rows that are constantly being deleted and added so speed is key.
The database is MS-SQL 2019
I would suggest a recursive CTE:
with cte as (
select userid, startdate, enddate
from t
union all
select userid, startdate,
enddate
from cte
where startdate < enddate and
week(startdate) <> week(enddate)
)
select year(startdate), week(startdate), count(*)
from cte
group by year(startdate), week(startdate)
option (maxrecursion 0);
The CTE expands the data by adding 7 days to each row. This should be one day per week.
There is a little logic in the second part to handle the situation where the enddate ends in the same week as the last start date. The above solution assumes that the dates are all in the same year -- which seems quite reasonable given the sample data. There are other ways to prevent this problem.
You need to cross-join each row with the relevant dates.
Create a calendar table with columns of years and weeks, include a start and end date of the week. See here for an example of how to create one, and make sure you index those columns.
Then you can cross-join like this
SELECT
YEAR(dateColumn) AS yearname,
WEEK(dateColumn)as weekname,
COUNT(somecolumen)
FROM Table t
JOIN CalendarWeek c ON c.StartDate >= t.StartDate AND c.EndDate <= t.EndDate
GROUP BY YEAR(dateColumn), WEEK(dateColumn)

Sql split entries into two entries if EndDateTime is the next day(after midnight)

I have this query:
select
sth.Id,
sth.CreatedDateTime,
EndDateTime = Lead(sth.CreatedDateTime, 1) over (partition by sth.Id order by sth.Id, sth.CreatedDateTime)
from
Sth as sth
order by
sth.Id, sth.CreatedDateTime
Which returns these results:
Id StartDateTime EndDate
--------------------------------------------------------------------
2746743 2019-11-20 14:35:05.5841266 NULL
2746744 2019-11-20 14:35:05.5841266 NULL
3 2018-06-25 23:35:12.2799952 2018-06-26 09:57:27.8943163
13 2018-06-26 09:57:27.8943163 2018-06-26 10:41:19.2973307
I have been asked to update the above query, split the row with Id=3 into two rows.
Meaning: as you can see the record with Id 3 starts at 23:35 and ends the **next day** at 09:57
What I need it to split this record into two.
The first one should be from 23:35 -> 23:59
And the one below should be from 00:00 -> 09:57
If records span for more than one day. Nothing needs to be done. Also the end solution should be able to work for a history table. More than 3 million rows.
So the record should result to sth like this
Id StartDateTime EndDateTime
3 2018-06-25 23:35:12.2799952 2018-06-25 23:59:59.000000
3 2018-06-26 00:00:00.0000000 2018-06-26 09:57:27.8943163
I hope this makes sense!
All other records will yield similar results. There are records that do not need to be splitted.
The result set in your question cannot be a result from the query you have specified (every id would have a null value for the end date). So, I am interpreting the question as handling the situation where the end date is present and one day after the start date.
I would just use a lateral join:
with t as (
select sth.*, CreatedDateTime as StartDateTime,
Lead(sth.CreatedDateTime, 1) over (partition by sth.Id order by sth.Id, sth.CreatedDateTime) as EndDateTime
from Sth as sth
)
select t.id, v.*
from t cross apply
(values (startdatetime,
(case when datediff(day, startdatetime, enddatetime) = 1
then dateadd(second, -1, dateadd(day, 1, convert(datetime, convert(date, startdatetime))))
else enddatetime
end)
),
(dateadd(day, 1, convert(date, startdatetime)),
(case when datediff(day, startdatetime, enddatetime) = 1
then enddatetime
end)
)
) v(startdatetime, enddatetime)
where v.enddatetime is not null;
Here is a db<>fiddle.

How can I find the time difference in two dates?

I am trying to use the DATEDIFF() function to find the difference between two dates within a table. The problem I am having is the understanding how to subtract the time from the most recent date in the table VS the starting date.
Dates are in the format: YYYY-MM-DD HH:MM:SS
I have tried this:
select FileName, '20:00' as StartTime, ModifiedDate, DATEDIFF(MINUTE,
'20:00', ModifiedDate) as 'BackupTime'
from BackLogData
But it returns the minutes from the start time.
Here is a sample of the table:
+-----------+-----------------------------+------------+
| StartTime | ModifiedDate | BackupTime |
+-----------+-----------------------------+------------+
| 20:00 | 2019-06-10 01:04:17.3692999 | 62817424 |
| 20:00 | 2019-06-10 00:53:23.4900986 | 62817413 |
| 20:00 | 2019-06-10 00:51:09.2363761 | 62817411 |
+-----------+-----------------------------+------------+
The correct table:
+-----------+-----------------------------+------------+--+
| StartTime | ModifiedDate | BackupTime | |
+-----------+-----------------------------+------------+--+
| 20:00 | 2019-06-10 01:04:17.3692999 | 11 | |
| 20:00 | 2019-06-10 00:53:23.4900986 | 2 | |
| 20:00 | 2019-06-10 00:51:09.2363761 | 291 | |
+-----------+-----------------------------+------------+--+
You can take your difference in minutes and transform it to time datatype using dateadd and cast. Please note that if your difference is bigger then 24 hours then this won't work (time data type stores up to 24 hours).
SELECT FileName, '20:00' AS StartTime, ModifiedDate,
cast(dateadd(minute,DATEDIFF(MINUTE, RecordDate, ModifiedDate),'19000101') as time(0)) AS 'BackupTime'
FROM BackLogData
Example:
SELECT
cast(dateadd(minute,DATEDIFF(MINUTE, '2019-05-05 16:00:00', '2019-05-05 18:00:00'),'19000101') as time(0)) AS 'BackupTime'
Output:
02:00:00
If all you're wanting is the difference of minutes from hour '20:00' compared to the time of ModifiedDate, you have to just compare the time values:
Try:
SELECT [FileName]
, '20:00' AS [StartTime]
, [ModifiedDate]
, DATEDIFF(MINUTE, '20:00', CONVERT(TIME, [ModifiedDate])) AS 'BackupTime' --convert your modified date to time
FROM [BackLogData];
Reason your getting a weird large value is you were trying to basically find the different between 1900-01-01 20:00 and your ModifiedDate.
Marc Guillot was on the right track but i found some issues with his query. Here's a revision:
--this is setup, you don't need this
CREATE TABLE t
([StartTime] time, [ModifiedDate] datetime)
;
INSERT INTO t
([StartTime], [ModifiedDate])
VALUES
('20:00', '2019-06-10 01:04:17'),
('20:00', '2019-06-10 00:53:23'),
('20:00', '2019-06-10 00:51:09')
;
--we now have a table with a TIME column (cast it in the cte if yours is not), a DATETIME
with LOGS as (
select StartTime,
ModifiedDate,
DATEADD(DAY, -1, CAST(CAST(ModifiedDate as DATE) as DATETIME)) as ModifiedMidnightDayBefore,
CAST(StartTime as DateTime) as StartDateTime,
row_number() over (order by ModifiedDate) as num
from t
)
select curr.StartTime,
curr.ModifiedDate,
datediff(minute,
COALESCE(
prev.ModifiedDate,
curr.ModifiedMidnightDayBefore + curr.StartDateTime
),
curr.ModifiedDate) as BackupTime
from
LOGS curr
left join LOGS as prev on prev.num = curr.num - 1
order by curr.num
The LOGS CTE is joined to itself on num = num-1 thereby putting the current row and previous row data together on a row. One row will have no previous data (blank) so when we are doing our datediff, we use coalesce, which is like ISNULL but is supported by all major db vendors. COALESCE returns the first non null argument. It is used to fill in a value if there is no PREVious value for the modified date
DATEDIFF of prev vs current is fairly obvious. The trick is in the logic if ther eis no previous value:
The CTE also casts the modifieddate datetime, to a date, to drop the time component (set it to midnight) and back to a datetime (so it emerges from the dateadd as a datetime). Dateadd subtracts one day from it, so it is midnight on the pervious day, and then we add our start time (8pm) to this. So effectively the min date in the table is converted to midnight, bumped back a day and then has 8pm added, so it becomes "8pm on the day prior to the modified date", and then we can datediff this nicely to 291 minutes
To get the previous time you can join your table with itself. But first I would number the rows on a CTE, so you can now set an easy condition to join each row with the previous row.
This query returns the difference between each ModifiedTime and its previous one (or StartDate on the first row), resulting in the desired result set that you have posted :
declare #StartTime time = convert(time, '20:00');
declare #StartDate datetime = (select convert(datetime, dateadd(day, -1, convert(date, max(ModifiedDate)))) +
convert(datetime, #StartTime)
from BackLogData);
with LOGS as (
select ModifiedDate,
row_number() over (order by ModifiedDate) as num
from BackLogData
)
select #StartTime as StartTime,
LOGS.ModifiedDate,
datediff(minute,
case when LOGS.num = 1 then #StartDate else PREVIOUS.ModifiedDate end,
LOGS.ModifiedDate) as BackupTime
from LOGS
left join LOGS as PREVIOUS on PREVIOUS.num = LOGS.num - 1
order by LOGS.num
PS: As Caius Jard noted, to be able to directly calculate the time difference between ModifiedDate and StartTime, we have to convert StartTime to a datetime using the date part of the last ModifiedDate minus one (meaning it started the day before).

SQL - How to ignore seconds and round down minutes in DateTime data type

At work we did a project that required a team to count students 8 times a day over 5 days at specific time periods. They are, as follows :-
09:00, 10:00, 11:00, 13:15, 14:15, 14:50, 15:50, 16:20.
Now, the data collected was put directly into a database via a web app. The problem is that database recorded each record using the standard YYYY-MM-DD HH:MM:SS.MIL, but if I were to order the records by date and then by student count it would cause the following problem;
e.g.:-
if the students counted in a room was 5 at 09:00:12, but another room had a count of 0 at 09:02:20 and I did the following:
select student_count, audit_date
from table_name
order by audit_date, student_count;
The query will return:
5 09:00:12
0 09:02:20
but I want:
0 09:00:00
5 09:00:00
because we're looking for the number of students in each room for the period 09:00, but unfortunately to collect the data it required us to do so within that hour and obviously the database will pick up on that accuracy. Furthermore, this issue becomes more problematic when it gets to the periods 14:15 and 14:50, where we will need to be able to distinguish between the two periods.
Is there a way to ignore the seconds part of the DateTime, and the round the minutes down to the nearest ten minute?
I'm using SQL Server Management Studio 2012. If none of this made sense, I'm sorry!
You may want some sort of Period table to store your segments. Then you can use that to join to your counts table.
CREATE TABLE [Periods]
( -- maybe [id] INT,
[start_time] TIME,
[end_time] TIME
);
INSERT INTO [Periods]
VALUES ('09:00','10:00'),
('10:00','11:00'),
('11:00','13:15'),
('13:15','14:15'),
('14:15','14:50'),
('14:50','15:50'),
('15:50','16:20'),
('16:20','17:00')
SELECT
student_count, [start_time]
FROM table_name A
INNER JOIN [Periods] B
ON CAST(A.[audit_date] AS TIME) >= B.[start_time]
AND CAST(A.[audit_date] AS TIME) < B.[end_time]
You can use the DATEADDand DATEPARTfunctions to accomplish this together with a CASEexpression. If you want more precise cutoffs between the .14and .50periods you can easily adjust the case statement and also if you want to minutes to be .10or.15
-- some test data
declare #t table (the_time time)
insert #t values ('09:00:12')
insert #t values ('14:16:12')
insert #t values ('09:02:12')
insert #t values ('14:22:12')
insert #t values ('15:49:12')
insert #t values ('15:50:08')
select
the_time,
case
when datepart(minute,the_time) < 15 then
dateadd(second, -datepart(second,the_time),dateadd(minute, -datepart(minute,the_time),the_time))
when datepart(minute,the_time) >= 15 and datepart(minute,the_time) < 50 then
dateadd(second, -datepart(second,the_time),dateadd(minute, -datepart(minute,the_time)+10,the_time))
else
dateadd(second, -datepart(second,the_time),dateadd(minute, -datepart(minute,the_time)+50,the_time))
end as WithoutSeconds
from #t
Results:
the_time WithoutSeconds
---------------- ----------------
09:00:12.0000000 09:00:00.0000000
14:16:12.0000000 14:10:00.0000000
09:02:12.0000000 09:00:00.0000000
14:22:12.0000000 14:10:00.0000000
15:49:12.0000000 15:10:00.0000000
15:50:08.0000000 15:50:00.0000000
Try this:
SELECT
CAST(
DATEADD(SECOND, - (CONVERT(INT, RIGHT(CONVERT(CHAR(2),
DATEPART(MINUTE, GETDATE())),1))*60) - (DATEPART(SECOND,GETDATE())), GETDATE())
AS SMALLDATETIME);
You can try ORDER BY this formula
DATEADD(minute, floor((DATEDIFF(minute, '20000101', audit_date) + 5)/10)*10, '20000101')
e.g.
WITH tbl AS(
SELECT * FROM ( VALUES (5,'2014-03-28 09:00:09.793'),(0,'2014-03-28 09:02:20.123')) a (student_count, audit_date)
)
SELECT *,DATEADD(minute, floor((DATEDIFF(minute, '20000101', audit_date) + 5)/10)*10, '20000101') as ORDER_DT
FROM tbl
ORDER BY ORDER_DT,student_count
SQL Fiddle

How to generate row of time sequences using SQL (SQL-Server)

Right now, I want to create function that generate row of time sequence from date x to date y using SQL.
The function have two date inputs: x and y. They are date without time (i.e. time is 00:00:00). The function will look like this:
function (#x date, #y date)
Then, the result will generate rows of time sequence from date x to date y. Each time has difference of 15 minutes. The result will not include date y.
For example, if I call function ('2013-06-19', '2013-06-21'), the result will be:
'2013-06-19 00:00:00'
'2013-06-19 00:15:00'
'2013-06-19 00:30:00'
...
'2013-06-19 23:30:00'
'2013-06-19 23:45:00'
'2013-06-20 00:00:00'
'2013-06-20 00:15:00'
...
'2013-06-20 23:30:00'
'2013-06-20 23:45:00'
From a performance point of view, you'd rather create a table with all the dates you could need.
Even if you'd rather not have such a table then you'll need a tally table (tables of numbers) anyway.
tally table
select
DATEADD(mi, n * 15, #x)
from (
select ROW_NUMBER() over (order by (select null)) - 1
from
master.dbo.spt_values
) as virtualTally(n)
where n < datediff(HH, #x , #y) * 4
output is:
2013-06-19 00:00:00.000
2013-06-19 00:15:00.000
2013-06-19 00:30:00.000
...
2013-06-20 23:15:00.000
2013-06-20 23:30:00.000
2013-06-20 23:45:00.000