Sql Server - Comparing time only in 24 hour format? - sql

I want to compare time with a time-range and if that particular time lies with-in that time-range, then it should return a record.
For example, Table 'A' has :
+-----------+------+-------------+-----------+
| User_Code | Name | Shift_Start | Shift_End |
+-----------+------+-------------+-----------+
| 1 | ABC | 04:01:00 | 11:00:00 |
| 2 | DEF | 11:01:00 | 20:00:00 |
| 3 | XYZ | 20:01:00 | 04:00:00 |
+-----------+------+-------------+-----------+
Now I want to check whose shift was it at this specific datetime : 2016-09-26 02:51:59. The SQL query should return the User_Code = 3. Shift_Start and Shift_End are of type time.
I have tried converting 2016-09-26 02:51:59 to time and then comparing with shift_start and shift_end using between and using logical operator but I am unable to get the desired result.
This has got me baffled. Its been an hour since I have tried coming up with a solution but I am unable to do so. Tried googling but got nothing there too. Any help will be appreciated. Using SQL Server 2012.

You need a more complicated where condition, because the shift times can be in either order.
So:
where ((shift_start < shift_end) and
cast(#datetime as time) between shift_start and shift_end)
) or
((shift_start > shift_end) and
cast(#datetime as time) not between shift_end and shift_start)
)

declare #t table(
User_Code int,
Name varchar(20),
Shift_Start time,
Shift_End time
);
insert #t(User_Code,Name,Shift_Start,Shift_End)
values
(1,'ABC','04:01:00','11:00:00')
,(2,'DEF','11:01:00','20:00:00')
,(3,'XYZ','20:01:00','04:00:00');
Try
declare #d datetime = '2016-09-26 02:51:59';
select *
from #t
cross apply ( -- intermediate vars: rounded param
select dt = cast(cast(#d as date) as datetime)
) r
where #d between dateadd(d, case when Shift_Start<Shift_End then 0 else -1 end, dt) + cast(Shift_start as datetime)
and dt+cast(Shift_end as datetime);

Approach from the other angle and add the date part of your search criteria to the Shift Start and End Times:
Note the case statement, this handles the situation where the shift end crosses midnight.
DECLARE #dateTime DATETIME = '2016-09-26 02:51:59'
-- Remove the time portion of above
DECLARE #datePortion DATETIME = DATEADD(dd, 0, DATEDIFF(dd, 0, #dateTime))
SELECT * FROM TableA
WHERE #dateTime BETWEEN #datePortion + cast(Shift_Start as datetime)
AND CASE WHEN ShiftEnd < ShiftStart
THEN DATEADD(day, 1, #datePortion)
ELSE #datePortion
END + cast(Shift_End as datetime)

Related

Reporting on time information using start and end time

Is it possible to create a report that sums hours for a day grouped by an Id using a start and end time stamp?
I need to be able to split time that spans days and take part of that time and sum to the correct date group.
NOTE: The date ids are to a date dimension table.
------------------------------------------------------------------------------
TaskId | StartDateId | EndDateId | StartTime | EndTime
------------------------------------------------------------------------------
2 | 20190317 | 20190318 | 2019-03-17 16:30:00 | 2019-03-18 09:00:00
------------------------------------------------------------------------------
1 | 20190318 | 20190318 | 2019-03-18 09:00:00 | 2019-03-18 16:30:00
------------------------------------------------------------------------------
2 | 20190318 | 20190319 | 2019-03-18 16:30:00 | 2019-03-19 09:00:00
------------------------------------------------------------------------------
So based on this, the desired report output would be:
-------------------------
Date | Task | Hours
-------------------------
2019-03-17 | 2 | 7.5
-------------------------
2019-03-18 | 1 | 7.5
-------------------------
2019-03-18 | 2 | 16.5
-------------------------
...
The only working solution I have managed to implement is splitting records so that no record spans multiple days. I was hoping to find a report query solution, rather than an ETL base based solution.
I have tried to simulate your problem here: https://rextester.com/DEV45608 and I hope it helps you :) (The CTE GetDates can be replaced by your date dimension)
DECLARE #minDate DATE
DECLARE #maxDate DATE
CREATE TABLE Tasktime
(
Task_id INT,
Start_time DATETIME,
End_time DATETIME
);
INSERT INTO Tasktime VALUES
(2,'2019-03-17 16:30:00','2019-03-18 09:00:00'),
(1,'2019-03-18 09:00:00','2019-03-18 16:30:00'),
(2,'2019-03-18 16:30:00','2019-03-19 09:00:00');
SELECT #mindate = MIN(Start_time) FROM Tasktime;
SELECT #maxdate = MAX(End_time) FROM Tasktime;
;WITH GetDates AS
(
SELECT 1 AS counter, #minDate as Date
UNION ALL
SELECT counter + 1, DATEADD(day,counter,#minDate)
from GetDates
WHERE DATEADD(day, counter, #minDate) <= #maxDate
)
SELECT counter, Date INTO #tmp FROM GetDates;
SELECT
g.Date,
t.Task_id,
SUM(
CASE WHEN CAST(t.Start_time AS DATE) = CAST(t.End_time AS DATE) THEN
DATEDIFF(second, t.Start_time, t.End_time) / 3600.0
WHEN CAST(t.Start_time AS DATE) = g.Date THEN
DATEDIFF(second, t.Start_time, CAST(DATEADD(day,1,g.Date) AS DATETIME)) / 3600.0
WHEN CAST(t.End_time AS DATE) = g.Date THEN
DATEDIFF(second, CAST(g.Date AS DATETIME), t.End_time) / 3600.0
ELSE
24.0
END) AS hours_on_the_day_for_the_task
from
#tmp g
INNER JOIN
Tasktime t
ON
g.Date BETWEEN CAST(t.Start_time AS DATE) AND CAST(t.End_time AS DATE)
GROUP BY g.Date, t.Task_id
The Desired Date can be joined to the date dimension and return the "calendar date" and you can show that date in the report.
As for the HOURS.. when you are retrieving your dataset in SQL, just do this.. it is as simple as:
cast(datediff(MINUTE,'2019-03-18 16:30:00','2019-03-19 09:00:00') /60.0 as decimal(13,1)) as 'Hours'
So in your case it would be
cast(datediff(MINUTE,sometable.startdate,sometable.enddate) /60.0 as decimal(13,1)) as 'Hours'
Just doing a HOUR will return the whole hour.. and dividing by 60 will return a whole number. Hence the /60.0 and the cast

Create work pattern SQL table

I'm looking to create what appears to be quite a simple table in SQL, however, I'm struggling to create it.
The first date at which the work pattern starts is 01/01/1990 (UK date format: dd/mm/yyyy - happy to have this as 1990/01/01 if necessary). The end date of the first period is 8 weeks from the start date (26/02/1990). The start date of the next period is the day after the previous end date (27/02/1990) and so on. I'd want the last end date to be some time in the future (at least 10 years from now).
This is ideally how I want the table to look:
+--------+------------+------------+
| Period | Start Date | End Date |
+--------+------------+------------+
| 1 | 01/01/1990 | 26/02/1990 |
| 2 | 27/02/1990 | 24/04/1990 |
| 3 | 25/04/1990 | 20/06/1990 |
| 4 | 21/06/1990 | 16/08/1990 |
| 5 | 17/08/1990 | 12/10/1990 |
+--------+------------+------------+
Any help much appreciated.
If you are just adding 8 weeks and not considering the weekends you can follow something like this.
DECLARE #tempTable TABLE(
Period INT IDENTITY PRIMARY KEY,
StartDate DateTime,
EndDate DateTime
);
DECLARE #startDate DATETIME = GETDATE();
DECLARE #endDate DATETIME;
DECLARE #currYear INT = DATEPART(YY,#startDate);
DECLARE #endYear INT = #currYear + 10;
WHILE (#currYear <= #endYear)
BEGIN
SET #endDate = DATEADD(WEEK,8,#startDate);
INSERT INTO #tempTable (StartDate, EndDate) VALUES(#startDate, #endDate);
SET #startDate = DATEADD(dd,1,#endDate);
SET #currYear = DATEPART(YY,#startDate);
END;
SELECT Period, FORMAT(StartDate,'dd/MM/yyyy') AS StartDate, FORMAT(EndDate,'dd/MM/yyyy') AS EndDate FROM #tempTable
CREATE TABLE your_table
(
Period INT,
StartDate DATE,
EndDate DATE
)
INSERT INTO your_table
VALUES
(1,'1990-01-01','1990-02-26')
--etc

Cast(GetDate() as int) comes out incorrect after noon

So is this supposed to happen? or is something wrong with my query.
if I run the following query
select getdate() as [Date],
CAST(getdate() as date) as [Time],
CAST(getdate() as INT) as INT,
Cast(CAST(cast(getdate() as int) as DATETime) as Date) as finish
If I run this at 11:55:00 AM [finish] returns the correct date.
If I run the query at 12:10:00 PM [finish] returns tomorrows date.
I tried to search for this and couldn't find anything so sorry if its been noted before.
I'm trying to group by Date but I don't want to group by time also, that is why I am doing the conversions.
Running Microsoft SQL Server 2008 R2
I think your problem is that (if you absolutely have to do these converts, I know it used to be required long ago) that you are using int instead of float and getting rounding on your converted values:
select getdate() as [Date]
,CAST(getdate() as date) as [Time]
,CAST(getdate() as INT) as INT
,Cast(CAST(cast(getdate() as int) as DATETime) as Date) as finishInt
,CAST(getdate() as float) as FLOAT
,Cast(CAST(cast(getdate() as float) as DATETime) as Date) as finishFloat
Output:
+-------------------------+------------+-------+------------+------------------+-------------+
| Date | Time | INT | finishInt | FLOAT | finishFloat |
+-------------------------+------------+-------+------------+------------------+-------------+
| 2017-02-07 16:47:59.823 | 2017-02-07 | 42772 | 2017-02-08 | 42771.6999979552 | 2017-02-07 |
+-------------------------+------------+-------+------------+------------------+-------------+

Generate more rows if there is a difference between two columns in SQL Server

I'm currently working on some reports from MS Project Server and found this oddity:
For some obscure reason, whenever you appoint to the same task with the same amount of time in consecutive days, instead of creating an entry for each appointment, the application updates the start date and the finish date fields on database, leaving only one entry for that task, but with a range between the dates.
If the amount of time appointed to the task in consecutive days are different, then there will be created one entry per appointment.
(Yes, I know, it's kind of confusing. I don't even know how to explain this better).
I want to know if it is somehow possible to generate more rows within SQL statement whenever there is a difference between the start and the finish date, one for each day in the range.
This is the query I have right now, I already can tell which rows have this date difference, but I don't know what I can do next.
select
r.WRES_ID, r.RES_NAME, PROJ_NAME, p.WPROJ_ID, TASK_NAME, WWORK_VALUE, WWORK_START, WWORK_FINISH,
datediff(d, WWORK_START, WWORK_FINISH) + 1 AS work_days
from MSP_WEB_RESOURCES r
join
MSP_WEB_ASSIGNMENTS a on a.WRES_ID = r.WRES_ID
join
MSP_WEB_PROJECTS p on p.WPROJ_ID = a.WPROJ_ID
join
MSP_WEB_WORK w on w.WASSN_ID = a.WASSN_ID
where RES_NAME = 'HenriqueBarcelos'
and WWORK_TYPE = 1
and WWORK_VALUE > 0
and WWORK_FINISH between '2014-01-27' and '2014-01-31'
order by WWORK_FINISH DESC
I know I could do this at the application level, but I was wondering if I could just do it within the database itself.
Thank's in advance.
Edit:
These are my current results:
WRES_ID | RES_NAME | TASK_NAME | WWORK_VALUE | WWORK_START | WWORK_FINISH | work_days
--------+------------------+-------------------------+---------------+---------------------+---------------------+----------
382 | HenriqueBarcelos | Outsourcing Initiatives | 60000.000000 | 2014-01-30 00:00:00 | 2014-01-30 00:00:00 | 1
382 | HenriqueBarcelos | Internal Training | 289800.000000 | 2014-01-29 00:00:00 | 2014-01-29 00:00:00 | 1
382 | HenriqueBarcelos | Outsourcing Initiatives | 120000.000000 | 2014-01-29 00:00:00 | 2014-01-29 00:00:00 | 1
382 | HenriqueBarcelos | Outsourcing Initiatives | 60000.000000 | 2014-01-27 00:00:00 | 2014-01-28 00:00:00 | 2
382 | HenriqueBarcelos | Infrastructure (TI) | 120000.000000 | 2014-01-27 00:00:00 | 2014-01-27 00:00:00 | 1
Notice that the second last register has a range of 2 days. In deed, there are 2 appointments, one on Jan 27th and other on 28th.
What I want to do is expand this and return one entry per day in this case.
It can be done, but it's not very elegant. First you need a function that will expand the date range into sequence of dates:
CREATE FUNCTION ufn_Expand(#start DATE, #end DATE)
RETURNS TABLE
AS
RETURN
WITH cte AS
(
SELECT #start AS dt
UNION ALL
SELECT DATEADD(dd, 1, dt) FROM cte WHERE dt < #end
)
SELECT dt FROM cte
Then use that in your query with CROSS APPLY:
SELECT /* your columns */, x.dt
FROM /* your joins */
CROSS APPLY ufn_Expand(WWORK_START, WWORK_FINISH) x
I'd use a numbers table (nice and set-based, yum!)
SELECT start_date
, end_date
, DateDiff(dd, start_date, end_date) + 1 As number_of_days --rows to display
FROM your_table
INNER
JOIN dbo.numbers
ON numbers.number BETWEEN 1 AND DateDiff(dd, start_date, end_date) + 1
Use your favourite search engine to find a numbers table script. Here's one I made earlier.
As an aside: if you remove the +1s you just modify the join to be between zero and the DateDiff() - I added the +1s as I thought it might be clearer!
You can see this from another perspective. You don't really want a row per each worked day. What you really need it's the number of worked days, multiplied by the reported worked time. Something like this:
(dbo.MSP_WEB_WORK.WWORK_VALUE / 60000) * (DATEDIFF(day, dbo.MSP_WEB_WORK.WWORK_START, dbo.MSP_WEB_WORK.WWORK_FINISH) + 1)
however, this creates an issue. Let's say you want a given period. If you use the WWORK_START and WWORK_FINISH dates for your report, you need to be careful to include all the work with only some days inside the period. Something like this will do it:
DECLARE #InitDate DATETIME;
DECLARE #EndDate DATETIME;
SET #InitDate = '2016/06/01';
SET #EndDate = '2016/07/01';
--Full list of tasks
SELECT dbo.MSP_WEB_RESOURCES.RES_NAME AS Name, dbo.MSP_WEB_PROJECTS.PROJ_NAME AS Project,
dbo.MSP_WEB_WORK.WWORK_VALUE / 60000 AS ReportedWork,
CASE
WHEN WWORK_START < #InitDate THEN DATEDIFF(day, #InitDate, dbo.MSP_WEB_WORK.WWORK_FINISH) + 1 --If the task started before the start of the period
WHEN WWORK_FINISH > DATEDIFF(day,-1,#EndDate) THEN DATEDIFF(day, WWORK_START, DATEDIFF(day,-1,#EndDate)) + 1 --if the task ended after the end of the period
ELSE DATEDIFF(day, dbo.MSP_WEB_WORK.WWORK_START, dbo.MSP_WEB_WORK.WWORK_FINISH) + 1 --All tasks with start and end date inside the period
END AS RepeatedDays,
CASE
WHEN WWORK_START < #InitDate THEN (dbo.MSP_WEB_WORK.WWORK_VALUE / 60000) * (DATEDIFF(day, #InitDate, dbo.MSP_WEB_WORK.WWORK_FINISH) + 1)
WHEN WWORK_FINISH > DATEDIFF(day,-1,#EndDate) THEN (dbo.MSP_WEB_WORK.WWORK_VALUE / 60000) * (DATEDIFF(day, WWORK_START, DATEDIFF(day,-1,#EndDate)) + 1)
ELSE (dbo.MSP_WEB_WORK.WWORK_VALUE / 60000) * (DATEDIFF(day, dbo.MSP_WEB_WORK.WWORK_START, dbo.MSP_WEB_WORK.WWORK_FINISH) + 1)
END AS ActualWork,
dbo.MSP_WEB_WORK.WWORK_START,
dbo.MSP_WEB_WORK.WWORK_FINISH
FROM dbo.MSP_WEB_RESOURCES INNER JOIN
dbo.MSP_WEB_ASSIGNMENTS INNER JOIN
dbo.MSP_WEB_PROJECTS ON dbo.MSP_WEB_ASSIGNMENTS.WPROJ_ID = dbo.MSP_WEB_PROJECTS.WPROJ_ID INNER JOIN
dbo.MSP_WEB_WORK ON dbo.MSP_WEB_ASSIGNMENTS.WASSN_ID = dbo.MSP_WEB_WORK.WASSN_ID ON
dbo.MSP_WEB_RESOURCES.WRES_ID = dbo.MSP_WEB_ASSIGNMENTS.WRES_ID
WHERE (dbo.MSP_WEB_WORK.WWORK_TYPE = 1) AND
(
#InitDate BETWEEN dbo.MSP_WEB_WORK.WWORK_START and dbo.MSP_WEB_WORK.WWORK_FINISH OR
DATEADD(day,-1,#EndDate) BETWEEN dbo.MSP_WEB_WORK.WWORK_START and dbo.MSP_WEB_WORK.WWORK_FINISH OR
(dbo.MSP_WEB_WORK.WWORK_START >= #InitDate) AND
(dbo.MSP_WEB_WORK.WWORK_FINISH < #EndDate)
)
ORDER BY dbo.MSP_WEB_WORK.WWORK_START;

Elegant way to calculate the nearest time

I have a reference time:
'2012-05-01 23:35:00'
And I have a couple of more times from some table where the 'yyyy-MM-dd' date part is irrelevant but what is relevant is date part with 'HH:minute'
------------------------------
| ID | Time |
| -- |-----------------------|
| 01 | '1900-01-01 13:10:00' |
| 02 | '1900-01-01 07:01:00' |
| 03 | '1900-01-02 00:45:00' |
| 04 | '1900-01-02 18:00:00' |
------------------------------
I am writing a function that will return the row with the nearest time (in above example this would be ID 03), but I don't like what I am writing.
So I started to observe the problem graphically. Is there maybe a way to do this with Atan function?
Edit: I am using MSSQL server 2005
declare #reftime datetime;
set #reftime = '20120501 23:00:00';
select TOP(1) with ties
id, time
from (
select
t.*,
dateadd(d, -datediff(d, 0, #reftime), #reftime) reftime,
dateadd(d, -datediff(d, 0, t.time), t.time) coltime
from tbl t
) x
order by (select min(diff)
from
(select abs(datediff(ms, reftime, coltime))
union all
select abs(datediff(ms, reftime+1, coltime))
union all
select abs(datediff(ms, reftime, coltime+1))) y(diff));
Notes:
The datediff(d, -datediff(... patterns remove the date portion from the datetimes.
To handle cross-midnight scenarios, both reftime+1 and coltime+1 are tested in addition to just abs(reftime-coltime).
The ORDER BY is performed across the minimum difference from testing it over all 3 scenarios.