How does one retrieve records with all days in date range covered? - sql

Imagine you have something like a action rental or hotel scenario where a unit can have rates for specific days and they can be available or not. I want to be able to search all my units for a specified arrival date and departure date to only bring back units that meet the whole time period.
Lets say each unit has a rate table similar to below
PropertyID Rate Start_Date End_Date Status Type
1 400 03/12/18 03/13/18 Available Daily
1 400 03/13/18 03/14/18 Available Daily
1 400 03/14/18 03/15/18 Available Daily
1 400 03/15/18 03/16/18 Reserved Daily
1 400 03/16/18 03/17/18 Available Daily
1 400 03/17/18 03/18/18 Available Daily
1 400 03/18/18 03/19/18 Available Daily
1 900 03/12/18 03/19/18 Blocked Weekly
Daily rates are split into each day due to the fact you can book a day at any time while a weekly rate needs the full range available. In the above case, it turns out that I would NOT be able to rent the weekly rate as the 15 day is reserved and that is part of the weekly rate.
If someone were to search that they wanted to stay 3/12/18 to 3/19/18 and assuming all of the above was available, I have it it doing in a store procedure (pseudo code)
Select stuff
From Property
Inner Join PropertyRate pr
where pr.Start_Date = #arrrivalDate And pr.End_Date = #departureDate
This would get me the property back because the weekly rate fits. However if the weekly rate was removed and it was only the daily rates, this would not return this property. Assuming all the statuses were available for the daily rates, this property should return because each day of the period specified is available.
How can I check if each day in the requested date range has an available date for that time period?
If it matters, this is Microsoft SQL Server 2014.
Edit 1:
It was requested for table definitions. Here is the property rate table create script:
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[PropertyRates]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[PropertyRates](
[property_rate_id] [int] IDENTITY(1,1) NOT NULL,
[rate_details_id] [int] NOT NULL,
[property_id] [int] NOT NULL,
[rate] [money] NOT NULL,
[start_date] [date] NOT NULL,
[end_date] [date] NOT NULL,
[status] [varchar](20) NOT NULL,
[created] [datetime] NOT NULL,
CONSTRAINT [PK_PropertyRates] PRIMARY KEY CLUSTERED
(
[property_rate_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
SET ANSI_PADDING OFF
GO
I believe that is what was asked.
Edit 2:
I wasn't very clear on what I'm trying to do for an output. Since this SQL is used in a search function, I only want to return the SINGLE property record for the property if it has available rates on every day of the passed in search range. So the end SQL should just have the property record if the many rates for that property are available and cover every day in the passed in date range.
Also note, that besides daily and weekly rates, there are also other such as monthly, bi-monthly, daily 2 day min, etc but they all have the start and end date columns filled in.
So for the above example, for a stay from 3/12 to 3/19, I should NOT get the property back because there is no range of days covered by available because the weekly rate is blocked and the daily rate has a reserved in the middle. If the reserved rate changed to available, the property should be returned.

select propertyid,count(*)
from property
where count(*) = datediff(dd,#startenddate, #enddate)+1
and status='Available'
group by propertyid
This will calculate the number of days requested (datediff) and calculate if there are that number of days in the table that are available.

UNION ALL could be a good way to handle this:
DECLARE #ArrivalDate Date = '20180312'
DECLARE #DepartureDate Date = '20180319'
SELECT *
FROM myTable t1
JOIN (SELECT PropertyID, Type --Handle Weekly Availability
FROM myTable
WHERE Type = 'Weekly'
AND Status = 'Available'
AND Start_Date = #ArrivalDate
AND End_Date = #DepartureDate
UNION ALL
SELECT PropertyID, Type --Handle Daily Availability
FROM myTable
WHERE Type = 'Daily'
AND Status = 'Available'
AND Start_Date >= #ArrivalDate
AND End_Date <= #DepartureDate
GROUP BY PropertyID, Type
HAVING COUNT(DISTINCT Start_Date) = DATEDIFF(Day, #ArrivalDate, #DepartureDate)
) t2 ON t1.PropertyID = t2.PropertyID --JOIN back to retrieve other needed data
AND t1.Type = t2.Type
Fiddle example here
Note, I changed the sample data to have one valid daily and one valid weekly example.

Related

Adding minutes of runtime from on/off records during a time period

I have a SQL database that collects temperature and sensor data from the barn.
The table definition is:
CREATE TABLE [dbo].[DataPoints]
(
[timestamp] [datetime] NOT NULL,
[pointname] [nvarchar](50) NOT NULL,
[pointvalue] [float] NOT NULL
)
The sensors report outside temperature (degrees), inside temperature (degrees), and heating (as on/off).
Sensors create a record when the previous reading has changed, so temperatures are generated every few minutes, one record for heat coming ON, one for heat going OFF, and so on.
I'm interested in how many minutes of heat has been used overnight, so a 24-hour period from 6 AM yesterday to 6 AM today would work fine.
This query:
SELECT *
FROM [home_network].[dbo].[DataPoints]
WHERE (pointname = 'Heaters')
AND (timestamp BETWEEN '2022-12-18 06:00:00' AND '2022-12-19 06:00:00')
ORDER BY timestamp
returns this data:
2022-12-19 02:00:20 | Heaters | 1
2022-12-19 02:22:22 | Heaters | 0
2022-12-19 03:43:28 | Heaters | 1
2022-12-19 04:25:31 | Heaters | 0
The end result should be 22 minutes + 42 minutes = 64 minutes of heat, but I can't see how to get this result from a single query. It also just happens that this result set has two complete heat on/off cycles, but that will not always be the case. So, if the first heat record was = 0, that means that at 6 AM, the heat was already on, but the start time won't show in the query. The same idea applies if the last heat record is =1 at, say 05:15, which means 45 minutes have to be added to the total.
Is it possible to get this minutes-of-heat-time result with a single query? Actually, I don't know the right approach, and it doesn't matter if I have to run several queries. If needed, I could use a small app that reads the raw data, and applies logic outside of SQL to arrive at the total. But I'd prefer to be able to do this within SQL.
This isn't a complete answer, but it should help you get started. From the SQL in the post, I'm assuming you're using SQL Server. I've formatted the code to match. Replace #input with your query above if you want to test on your own data. (SELECT * FROM [home_network].[dbo]...)
--generate dummy table with sample output from question
declare #input as table(
[timestamp] [datetime] NOT NULL,
[pointname] [nvarchar](50) NOT NULL,
[pointvalue] [float] NOT NULL
)
insert into #input values
('2022-12-19 02:00:20','Heaters',1),
('2022-12-19 02:22:22','Heaters',0),
('2022-12-19 03:43:28','Heaters',1),
('2022-12-19 04:25:31','Heaters',0);
--Append a row number to the result
WITH A as (
SELECT *,
ROW_NUMBER() OVER(ORDER BY(SELECT 1)) as row_count
from #input)
--Self join the table using the row number as a guide
SELECT sum(datediff(MINUTE,startTimes.timestamp,endTimes.timestamp))
from A as startTimes
LEFT JOIN A as endTimes on startTimes.row_count=endTimes.row_count-1
--Only show periods of time where the heater is turned on at the start
WHERE startTimes.row_count%2=1
Your problem can be divided into 2 steps:
Filter sensor type and date range, while also getting time span of each record by calculating date difference between timestamp of current record and the next one in chronological order.
Filter records with ON status and summarize the duration
(Optional) convert to HH:MM:SS format to display
Here's the my take on the problem with comments of what I do in each step, all combined into 1 single query.
-- Step 3: Convert output to HH:MM:SS, this is just for show and can be reduced
SELECT STUFF(CONVERT(VARCHAR(8), DATEADD(SECOND, total_duration, 0), 108),
1, 2, CAST(FLOOR(total_duration / 3600) AS VARCHAR(5)))
FROM (
-- Step 2: select records with status ON (1) and aggregate total duration in seconds
SELECT sum(duration) as total_duration
FROM (
-- Step 1: Use LEAD to get next adjacent timestamp and calculate date difference (time span) between the current record and the next one in time order
SELECT TOP 100 PERCENT
DATEDIFF(SECOND, timestamp, LEAD(timestamp, 1, '2022-12-19 06:00:00') OVER (ORDER BY timestamp)) as duration,
pointvalue
FROM [dbo].[DataPoints]
-- filtered by sensor name and time range
WHERE pointname = 'Heaters'
AND (timestamp BETWEEN '2022-12-18 06:00:00' AND '2022-12-19 06:00:00')
ORDER BY timestamp ASC
) AS tmp
WHERE tmp.pointvalue = 1
) as tmp2
Note: As the last record does not have next adjacent timestamp, it will be filled with the end time of inspection (In this case it's 6AM of the next day).
I do not really think it would be possible to achieve within single query.
Option 1:
implement stored procedure where you can implement some logic how to calculate these periods.
Option 2:
add new column (duration) and on insert new record calculate difference between NOW and previous timestamp and update duration for previous record

YoY & YTD - Taking into account the timestamp, as well as date, on records in the previous year

My question is, what do I need to change in the WHERE clause for the SP to take into account the timestamp, as well as the date itself, on the CreatedDate field. For example...If the report was run at 1pm today, it would only bring back records made up to 1pm today this time last year [2015] and obviously all records created in 2016 so far. At present, 2015 is bringing back all records up to the end of the day. Any help would be much appreciated. Claire
#CreatedFrom15 DateTime = NULL,
#CreatedTo15 DateTime = NULL,
#CreatedFrom16 DateTime = NULL,
#CreatedTo16 DateTime = NULL,
SELECT
BookingID,
CreatedYear,
CreatedDate,
NightDuration,
HolidayCost,
TRAVPropertyTypeCategory
FROM Vw_TRAVLodgeResortGlampingFinancialByNights
WHERE
((CreatedYear = DATEPART(YEAR,GETDATE())-1 AND CreatedDate BETWEEN #CreatedFrom15 AND #CreatedTo15) AND (CreatedDate >= #CreatedFrom15 OR #CreatedFrom15 IS NULL) AND (CreatedDate <= #CreatedTo15 OR #CreatedTo15 IS NULL)
OR
(CreatedYear = DATEPART(YEAR,GETDATE()) AND CreatedDate BETWEEN #CreatedFrom16 AND #CreatedTo16) AND (CreatedDate >= #CreatedFrom16 OR #CreatedFrom16 IS NULL)AND(CreatedDate <= #CreatedTo16 OR #CreatedTo16 IS NULL))
can you specify the dates of the parameters?
anyhow, the reference to the year seems unnecessary and also you can include the null case inside the between like this-
where CreatedDate between isnull(#createdFrom16,'20160101') and isnull(#CreatedTo16,'20161231')

Find available openings using SQL

I have a table of appointments with records having two fields - start_date and end_date, both datetime. There is no overlap of time periods in the table.
Given a specific period (search_start and search_end), I need to generate a list of all openings between those appointments (from and to) using SQL.
For example: given two appointments in the table:
September 15, 2016 08:00 to September 15, 2016 09:00
September 15, 2016 10:00 to September 15, 2016 12:00
And given search parameters start= September 1, 2016 00:00 and end= September 30, 2016 23:59, the results should be
September 1, 2016 00:00 to September 15, 2016 08:00
September 15, 2016 09:00 to September 15, 2016 10:00
September 15, 2016 12:00 to September 30, 2016 23:59
Here is a script to generate a sample table:
CREATE TABLE [dbo].[Table_1](
[from_date] [datetime] NOT NULL,
[to_date] [datetime] NULL,
CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED ( [from_date] ASC )
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
INSERT [dbo].[Table_1] ([from_date], [to_date]) VALUES (CAST(0x0000A6820083D600 AS DateTime), CAST(0x0000A682009450C0 AS DateTime))
INSERT [dbo].[Table_1] ([from_date], [to_date]) VALUES (CAST(0x0000A68200A4CB80 AS DateTime), CAST(0x0000A68200C5C100 AS DateTime))
I am using MSSQL 2008 R2
Using your values I got the output you wanted : )
DECLARE #start datetime = '2016-09-01 00:00:00'
DECLARE #finish datetime = '2016-09-30 23:59:00'
WITH rn AS (SELECT *, ROW_NUMBER() OVER (ORDER BY start) AS rn FROM opening)
SELECT CASE WHEN r1.rn = 1 THEN #start
ELSE r2.finish
END as START,
CASE WHEN r1.rn IS NULL THEN #finish
ELSE r1.start
END AS FINISH
FROM rn r1
FULL OUTER JOIN rn r2
ON r1.rn = r2.rn + 1
WHERE ISNULL(r1.start, 0) != #start
opening is your schedule/appointment table. start is the starting date in your table and finish is the end date in your table. #start is the starting date and #finish is the end date. You obvious don't need to use #start, #finish. I just put it there for testing.
SELECT Start_Date, End_Date
FROM TableName
BETWEEN Start_Date AND End_Date
This question seems easy?? I am not sure, if I have oversimplified your question?
You just have to make sure, you add the time bit as well!
I am thinking like Mfredy on this question. But I will give my standard spiel on what to look out for on data time calculations.
Here are some common issues.
Some (like Mysql) sql engines store the data in GMT (some time zone in england). Then they calculate the number of hours off of GMT to get the actual time. So if you are 8 hours from GMT you may be 8 hours off if you used a MsSql client connecting to a MySql database. Because MsSql does not understand this 8 hour delta for GMT used by MySql. You can also see this in applications and operating systems.
The other issue is you are thinking in dates you have to make sure you are thinking in Date and Time unless you converted the date time to a date.
Watch out for < vs <=. If you are filtering for the end date. Say for example Last day in january is to be included but first day of feb is not. It is better to do < 02/01 that it is to do <= 1/31. You may chop off records if you compare using <= and don't go down to the last millisecond before 02/01.
I've had a go trying to re-create your scenario, and then solve it. In some places I've simplified things for the sake of the example, but you should be able to extrapolate it.
I'm sure there are also far more elegant ways, but is a starting point, and I quite enjoyed figuring it out.
The approach I wanted to take was build a list of the available 'time slots'
First, I created a table with the available Hours for bookings
CREATE TABLE [dbo].[HOURS](
HourID [int] NULL)
INSERT INTO dbo.[hours] VALUES (9),(10),(11),(12),(13),(14),(15),(16),(17)
Then did the same for the minute intervals (I went with 5 min intervals for simplicity)
CREATE TABLE [dbo].MINS(
MinID [int] NULL )
INSERT INTO dbo.mins VALUES (5),(10),(15),(20),(25),(30),(35),(40),(45),(50),(55),(60)
and same again for dates I wanted to work with
CREATE TABLE [dbo].DATES(
DATES [Date] NULL)
INSERT INTO dbo.DATES values
('20160901'),('20160902')
Using these tables, I created a view listing all available 'slots'
CREATE VIEW AllTimeSlots AS
SELECT
cast(dates AS datetime) + cast(DATEADD(hour, hourid, DATEADD(minute, minid, DATEADD(second, 00, 0))) AS datetime) AS TimeSlot
FROM dbo.[hours] CROSS JOIN dbo.mins CROSS JOIN dates
I then created a table containing the appointments
CREATE TABLE [dbo].Appointments(
AppointmentStart [Datetime] NULL,
AppointmentEnd [DateTime] null
)
INSERT INTO dbo.Appointments
VALUES
('20160901 10:40:00 AM','20160901 10:55 AM'),('20160901 01:00:00 PM','20160901 02:05:00 PM')
Then, I created another view with married up the slots and the bookings. Take note of the joins I'm using to block out all slots between the two times
CREATE VIEW SlotAvailability
AS
SELECT TimeSlot,CASE WHEN AppointmentStart IS NULL THEN 'Free' ELSE 'Booked' END AS [Availability]
FROM (
SELECT Timeslot,apt.AppointmentStart, apt.AppointmentEnd FROM
dbo.AllTimeSlots ats
LEFT OUTER JOIN dbo.Appointments apt
on ats.TimeSlot >= appointmentstart and ats.timeslot <= appointmentend) q1
Doing
Select Timeslot, [availability] from SlotAvailability where [availability] = 'free'
will list all the available time slots.
The last bit, which I haven't quite got to (and ran out of time for now) is then converting this to a start-end time for each 'free' slot - tried a couple of methods, but didn't crack it - I think if you join that table (view) to itself of timeslot+5mins, you should be able to then min/max values based on whether it's the start / end of a free block
The way I normally address this today is to use the APPLY operator to join the table to itself. There's not enough info in the question to craft a complete query for you, but here's the general pattern:
SELECT *
FROM MyTable t1
OUTER APPLY (
SELECT TOP 1 *
FROM MyTable t
WHERE t.KeyFields = t1.KeyFields
AND t.SequenceField > t1.SequenceField
) t2
Now I can compare each row directly with the one the follows in my WHERE clause, such that the WHERE clause the filter it down to only show rows where there is a gap.
You can also sometimes accomplish this via the LAG or LEAD Windowing Functions.

Calculating working time with overlapping events (SQL)

I have found similar queries on StackOverflow (e.g. Finding simultaneous events in a database between times) but nothing that matches exactly what I am after as far as I can tell so thought it OK to add as a new question.
I have a table that logs jobs (or "Activities"), with a start/end time for the job. I need to calculate working time (you can disregard non-working days, break times etc. as I have that covered). The complication is an individual can work on simultaneous jobs, overlapping at different points (the assumption is equal effort on simultaneous jobs), and the working time needs to reflect that. Minute accuracy is all that is required, not to the second.
Based on other suggestions I have this query, implemented as a table-valued function. It will look at each minute that activity is running, and if any other activities are running in the same period for the same person, and make calculations based on that. It works, but is very inefficient - taking over a minute to execute. Any ideas how I can do this more efficiently?
Running SQL 2005. I have done the obvious such as to add indexes on foreign keys by the way.
CREATE FUNCTION [dbo].[WorkActivity_WorkTimeCalculations] (#StartDate smalldatetime, #EndDate smalldatetime)
RETURNS #retActivity TABLE
(
ActivityID bigint PRIMARY KEY NOT NULL,
WorkMins decimal NOT NULL
)
/********************************************************************
Summary: Calculates the WORKING time on each activity running in a given date/time range
Remarks: Takes into account staff working simultaneously on jobs
(evenly distributes working time across simultaneous jobs)
Input Params: #StartDate - the start of the period to calculate
#EndDate - the end of the period to calculate
Output Params:
Returns: Recordset of activities and associated working time (minutes)
********************************************************************/
AS
BEGIN
-- any work activities still running use the overall end date as the activity's end date for the purpose of calculating
-- simulateneous jobs running
-- POPULATE A TEMP TABLE WITH EVERY MINUTE IN THE DATE RANGE
DECLARE #Minutes TABLE (MinuteDateTime smalldatetime NOT NULL)
;WITH cte AS (
SELECT #StartDate AS myDate
UNION ALL
SELECT DATEADD(minute,1,myDate)
FROM cte
WHERE DATEADD(minute,1,myDate) <= #EndDate
)
INSERT INTO #Minutes (MinuteDateTime)
SELECT myDate FROM cte
OPTION (MAXRECURSION 0)
-- POPULATE A TEMP TABLE WITH WORKLOAD PER EMPLOYEE PER MINUTE
DECLARE #JobsRunningByStaff TABLE (StaffID smallint NOT NULL, MinuteDateTime smalldatetime NOT NULL, JobsRunning decimal NOT NULL)
INSERT INTO #JobsRunningByStaff (StaffID, MinuteDateTime, JobsRunning)
SELECT wka_StaffID, MinuteDateTime, COUNT(DISTINCT wka_ItemID) JobsRunning
FROM dbo.WorkActivities
INNER JOIN #Minutes ON (MinuteDateTime BETWEEN wka_StartTime AND DATEADD(minute,-1,ISNULL(wka_EndTime,#EndDate)))
GROUP BY wka_StaffID, MinuteDateTime
-- FINALLY MAKE THE CALCULATIONS FOR EACH ACTIVITY
INSERT INTO #retActivity
SELECT wka_ActivityID, SUM(1/JobsRunning)WorkMins
FROM dbo.WorkActivities
INNER JOIN #JobsRunningByStaff ON (wka_StaffID = StaffID AND MinuteDateTime BETWEEN wka_StartTime AND DATEADD(minute,-1,ISNULL(wka_EndTime,#EndDate)))
GROUP BY wka_ActivityID
RETURN
END
Some example data (sorry for the poor formatting!)...
Source Data from WorkActivities table:
ACTIVITY ID | START TIME | END TIME | STAFF ID
1 | 03/03/2016 10:30 | 03/03/2016 10:50 | 1
2 | 03/03/2016 10:40 | 03/03/2016 11:00 | 1
And the desired results for a function call of SELECT * FROM dbo.WorkActivity_WorkTimeCalculations ('03-Mar-2016 10:30','03-Mar-2016 11:30'):
ACTIVITY ID | WORKMINS
1 | 25
2 | 15
So, the results take into account between 10:40 and 10:50 there are two jobs happening simultaneously, so calculates 5 mins working time on each over that period.
As suggested by posters, indexing made a significant difference - creating an index with wka_StartTime and wka_EndTime sorted it.
(sorry, couldn't see how to mark the comments made by others as an answer!)

SQL Server find datediff between different rows

I am trying to build a query that analyzes data in our time tracking system. Every time a user punches in or out, it makes a row recording the punch time. So if you punch in at 9:00 and punch out at 5:00 there are two rows with those date stamps recorded accordingly. I need a query that will iterate over the rows at basically sum the datediff between workingpunch_ts (the timestamp column) in hours.
Each row does have an identifier that signifies if the punch is a punch in, or punch out (inout_id, 1 for in, 2 for out).
So for example if you had
ID | workingpunch_ts | inout_id
----------------------------------------------
123 | 2011-02-16 09:00:00.000 | 1
124 | 2011-02-16 17:00:00.000 | 2
That would yield a 8 hours. Now I just need to repeat that process for every pair of rows in the table.
Thoughts on how to accomplish this?
This query will give you problems if people punch in and out multiple times on the same day:
Table schema:
CREATE TABLE [dbo].[TimePunch](
[TimeCardID] [int] IDENTITY(1,1) NOT NULL,
[PunchTime] [datetime] NOT NULL,
[InOrOut] [int] NOT NULL,
[UserID] [int] NOT NULL,
[DayofPunch] [datetime] NOT NULL,
CONSTRAINT [PK_TimePunch] PRIMARY KEY CLUSTERED
(
[TimeCardID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 10) ON [PRIMARY]
) ON [PRIMARY]
Query:
select
tIn.UserID,
tIn.DayOfPunch,
DateDiff(Hour, tIn.PunchTime, tOut.PunchTime) as HoursWorked
FROM
TimePunch tIn,
TimePunch tOut
WHERE
tIn.InOrOut = 1
AND tOut.InOrOut = 2
AND tIn.UserID = tOut.UserID
AND tIn.DayofPunch = tOut.DayOfPunch
In hours, sure
select empid, cast(datediff(d,0,workingpunch_ts) as datetime),
SUM(case when inout_id = 2 then 1 else -1 end *
datediff(MI, datediff(d,0,workingpunch_ts), workingpunch_ts))/60.0 as Hours
from clock
where workingpunch_ts between '20110201' and '20110228 23:59:59.999'
group by empid, datediff(d,0,workingpunch_ts)
As long as the in and outs are paired, you add all the outs and remove all the ins, e.g.
- IN (9)
+ OUT (12)
- IN (13:15)
+ OUT (17)
The main code is in the 2nd and 3rd lines
The datediff-datediff works out the minutes from midnight for each workingpunch_ts, and if it is a punchout, it is made negative using the CASE inout_id statement.
The others are added for real life scenarios where you need to group by employee and day, within a date range.