Finding the area available for the date range - mysql

Suppose you have a room which is 100sqft and you want to rent it from 1st Aug to 31st Aug.
Bookings Table schema
startdate|enddate|area|storageid
you have following bookings
06-Aug|25-Aug|50|'abc'
05-Aug|11-Aug|40|'xyz'
18-Aug|23-Aug|30|'pqr'
13-Aug|16-Aug|10|'qwe'
Now somebody requests for booking from 08-Aug to 20-Aug. For this date range the maximum area available is 10sqft (Since, for dates 8,9,10 and 11 Aug only 10sq ft is available.)
How would you create an efficient SQL query to get this? Right now I have very messy and inefficient query which gives wrong results for some cases. I am not posting the query because It is so messy that I can't explain it myself.
I don't necessarily want to solve it using SQL only. If there is an algorithm that can solve it efficiently I would extract all the data from database.

Someone removed SQL Server, but here is the algorithm:
DECLARE #startDate date = '2016-08-09';
DECLARE #endDate date = '2016-08-20';
DECLARE #totalArea decimal(19,2) = 100;
WITH Src AS --Your source table
(
SELECT * FROM (VALUES
('2016-08-06', '2016-08-25', 50, 'abc'),
('2016-08-05', '2016-08-11', 40, 'xyz'),
('2016-08-18','2016-08-23',30,'pqr'),
('2016-08-13','2016-08-16',10,'qwe')
)T(startdate, enddate, area, storageid)
), Nums AS --Numbers table 0..N, N must be greater than ranges calculated
(
SELECT 0 N
UNION ALL
SELECT N+1 N FROM Nums
WHERE N<DATEDIFF(DAY,#startDate,#endDate)
) --Query
--You can use total-maxUsed from range of days
SELECT #totalArea-MAX(Used) FROM
(
--Group by day, sum all used areas
SELECT MidDate, SUM(Used) Used FROM
(
--Join table with numbers, split every day, if room used, return area
SELECT DATEADD(DAY, N, #startDate) MidDate, CASE WHEN DATEADD(DAY, N, #startDate) BETWEEN startDate AND endDate THEN area END Used
FROM Src
CROSS APPLY Nums
) T
GROUP BY MidDate
) T

Related

Query giving consecutive dates for following weeks

Is it possible to create a table in SQL, in which 1 column gives the consecutive Sundays. The other column has the upcoming 7 sundays corresponding to each sunday on column1.
Expected output below:
Any help is extremely appreciated.
Try the following:
select date_Sundays,
date_add(date_Sundays,
interval 7*row_number() over (partition by date_sundays order by date_sundays) day)
as nextSundays
from tbl
See a demo.
If you want the whole shebang, something like this:
-- Create Sundays table
CREATE TABLE Sundays (
Date_sundays DATE
);
-- Insert a bunch of Sundays
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SET #StartDate = CAST('2022-03-06' AS DATE)
SET #EndDate = CAST('2023-03-05' AS DATE)
WHILE #StartDate <= #EndDate
BEGIN
INSERT INTO Sundays (Date_sundays)
SELECT #StartDate
SET #StartDate = DATEADD(#StartDate, INTERVAL 7 DAY)
END
;
-- self JOIN to get next Sundays
SELECT
Sundays.Date_sundays,
Sundays2.Date_sundays AS Date_sundays_next
FROM
Sundays
JOIN Sundays Sundays2
ON Sundays2.Date_sundays BETWEEN
DATEADD(Sundays.Date_sundays, INTERVAL 7 DAY)
AND DATEADD(Sundays.Date_sundays, INTERVAL 49 DAY)
ORDER BY
Sundays.Date_sundays,
Sundays2.Date_sundays
;
You can use a function to generate the list of Sundays and then join to itself to get the future 7 Sundays. When calling the second function in the JOIN, make sure the end date is far enough in the future to encompass the future 7 weeks.
--Function to generate a list of Sundays using a number table.
CREATE FUNCTION fun_GetSundaysList
(
--Need to know the date range for generating these dates.
#StartDate date
, #EndDate date
)
RETURNS TABLE
AS
RETURN
(
--Using a numbers table to generate a list of dates.
--Concept borrowed from this post: https://stackoverflow.com/a/17529962/2452207
SELECT DATEADD(DAY,number+1,#StartDate) [Date]
FROM master..spt_values
WHERE type = 'P'
AND DATEADD(DAY,number+1,#StartDate) < #EndDate
AND DATEPART(WEEKDAY, DATEADD(DAY,number+1,#StartDate)) = 1 --Narrow list to only Sundays.
)
GO
--Select the list of Sundays and JOIN to the same list.
SELECT
s.[Date] as main_sunday
, s1.[Date] as future7_sundays
FROM fun_GetSundaysList ('2022-10-1', '2023-1-1') as s
JOIN fun_GetSundaysList ('2022-10-1', '2023-3-1') as s1
ON s1.[Date] > s.[Date]
AND s1.[Date] < DATEADD(week,8,s.[Date])
ORDER BY s.[Date], s1.[Date]
Sample of list Generated:
Edit: Looking again, I don't like using the master..spt_values for generating the list of dates. The reasoning is that it is an undocumented table, and it only gives you up to 2048 values to use. It is fast. Getting 2048 values from spt_values takes 2ms where generating 10k values using the below joins takes 187ms on my server. Either way, you need to generate a sequence of numbers to help create the dates so here's another way to build the numbers list in the function:
ALTER FUNCTION fun_GetSundaysList
(
--Need to know the date range for generating these dates.
#StartDate date = '10/11/2022'
, #EndDate date = '1/1/2023'
)
RETURNS TABLE
AS
RETURN
(
WITH x AS (SELECT n FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n))
, y as (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as number
FROM x ones, x tens, x hundreds, x thousands
--ORDER BY 1
)
--Using a numbers table.
SELECT DATEADD(DAY,number+1,#StartDate) [Date]
FROM y
WHERE DATEADD(DAY,number+1,#StartDate) < #EndDate
AND DATEPART(WEEKDAY, DATEADD(DAY,number+1,#StartDate)) = 1
)

MySQL finding MAX of a given field and returning the entire row

Introduction
Hi,
I have a weather station that records the current weather every half an hour.
Among other sensors, it has a thermometer. It is the subject in this enquiry.
I have summarized the fields of my table, but please be aware that HiTemp field stores the maximum temperature.
Situation
Given the following table structure in a SQL query
CREATE TABLE weather (
Id INT AUTO_INCREMENT PRIMARY KEY,
HiTemp FLOAT,
Date VARCHAR(8),
Time VARCHAR(5),
Epoch BIGINT
);
Note that HiTemp is stored in celsius degrees, Date follows dd/mm/YY format and Time is stored in CET (GMT+1) timezone
Therefore, a sample record would be..
1, 19, "13/12/19", "13:00", 1576238400
My goal is to sort through the records and find the maximum value of HiTemp along its other fields for every year in the table.
To find unique years a simple substring on Date works fine
SELECT SUBSTR(Date, -2) AS Year FROM weather GROUP BY Year;
I was thinking to use a PROCEDURE (function) to be able to use some kind of loop to go over the years
This query right here is the closest to my goal I have been able to figure out on my own
SELECT MAX(HiTemp) MaxTemp, SUBSTR(Date, -2) Year, Date, Time FROM weather GROUP BY Year;
Problem here is that as far as I know MAX is a group function and the other selected fields are irrelevant to its value
Nevertheless, the output of this query would have a format like the following
40.1, 19, "28/06/19", "17:00"
37.5, 18, "04/08/18", "14:00"
35.5, 17, "05/08/17", "15:30"
Note that these records are correct in the sense that the resulting selected fields belong to the very same record (they were handpicked)
Any ideas? Subqueries, Joins, Procedures.. Any insight on this is very much appreciated, I'm pretty lost at this point.
Thanks.
You could use a query with a subquery for max temp group by year
select * from weather w
INNER JOIN (
select MAX(HiTemp) MaxTemp, SUBSTR(Date, -2) Year
FROM weather GROUP BY Year
) t on t.MaxTemp = w.HiTemp and t.year = SUBSTR(w.Date, -2)
or
select w.HiTemp, t.Year, w.date, w.time from weather w
INNER JOIN (
select MAX(HiTemp) MaxTemp, SUBSTR(Date, -2) Year
FROM weather GROUP BY Year
) t on t.MaxTemp = w.HiTemp and t.year = SUBSTR(w.Date, -2) Year

Re-selecting from a table based on query results

my sql knowledge is fairly basic and I would be grateful for some advice. I have a table with columns like:
date, time, readings, .... comments1, comments2
What I would like to do is filter the table to show the results when comments1 is equal to a string, which I can achieve. The tricky bit is I then want to find the readings when the time is between 5 and 7 hours after the times returned/identified by the initial query (comments1 = string"). Is there a way to do this with and what would be the best strategy?
Thank you.
You should really store date and time in a single column, otherwise midnight boundaries are extremely difficult to select across. My example assumes your "date" column is a datetime type that also stores the timestamp.
I believe something like this is what you're looking for:
WITH CommentTime AS (
SELECT TOP 1 date
FROM tblRecords
WHERE comments1 = 'The comment to find'
)
SELECT *
FROM tblRecords
WHERE date >= DATEADD(hour, 5, (SELECT date FROM CommentTime))
AND date < DATEADD(hour, 7, (SELECT date FROM CommentTime))
New Answer:
(Reworking Dans answer to instead use a variable)
DECLARE #CommentTime AS DateTime = (SELECT TOP 1 [date] FROM tblRecords WHERE comments1 = 'string')
SELECT * FROM tblRecords
WHERE [date] >= DATEADD(HOUR, 5, #CommentTime)
AND [date] < DATEADD(HOUR, 7, #CommentTime)

"neater" way of counting holidays

I have a neat table of holidays showing date and recurrence if such holiday is recurrent (i.e. New Year)
Now, I need to count the number of holidays between two dates. If it was a simple list of dates without info about recurrence (so i.e. it would show all New Years between 2000-01-01 and 2015-01-01) it would be quite easy, i.e. something like
declare #start_Date Date= '2013-01-02',
#end_date Date ='2014-01-02'
SELECT COUNT(CE.name) AS holidays_count
FROM dbo.argo_cal_event AS CE INNER JOIN dbo.argo_cal_event_type AS CET
ON CE.event_type_gkey = CET.gkey
WHERE (CET.name = 'EXEMPT_DAY') AND (CE.name <> 'Sundays')
AND (CE.occ_start BETWEEN #start_Date AND #end_date)
But now we have a neat recurrence, so the query above won't count all the New Years, Christmases etc that have been declared as happening "every year starting from".
I COULD create a table with such list, but I've been wondering, is there any other way?
EDIT: Let me precise what I had in mind: I'd like to count the event normally if it event occurs once (I will assume here that user will have to populate all the irregular holidays as i.e. Easter), but when the recurrence <> Once, then get the occurrence start and count the years between that date and final date.
EDIT2: I think I've got it - for the recurrent holidays I can use
SELECT sum (datediff (year, ce.occ_start, #end_date)) as recurrent_holidays
FROM dbo.argo_cal_event AS CE INNER JOIN dbo.argo_cal_event_type AS CET
ON CE.event_type_gkey = CET.gkey
WHERE (CET.name = 'EXEMPT_DAY') and (CE.repeat_interval ='ANNUALLY')
EDIT3: unfortunately this solution doesn't work (or at least getting quite complicated) if I'd like to count between TWO dates, in which one is taken from another table, i.e if I'd like to count recurring holidays between unit.time_in and getddate() :/
The most straightforward way would be to make them all one-time occurrences.
The table isn't going to be that big that you can't add in every Sunday. It's only 52 or 53 entries per year.
If you do it this way now you can do something like,
select count(*) from events where event_date between start_date and end_date
Done.
The main reason to do it this way, though, is that some of your holidays are tricky to calculate. You'll need to be calculating (or looking up) the dates for Good Friday, Easter Monday, and others. They're based on the phase of the moon.
Why not just calculate them all and make your query really easy?
In the end, it matters a lot less how "neat" your data structures are and a lot more on whether your code does what it's supposed to do, how long it takes to do it, and how much effort it takes to get it working.
You could create a function that searches your holiday table for holidays of each type (weekly, monthly, yearly, one-off, etcetera) or even "Nth/Last/Nth Last Whateverday of Whichevermonth" (which would be a variant on Yearly), and for each holiday, interval by interval, loops through and increments each holiday date and checks if it falls between the StartDate and EndDate dates that the function would need to take as parameters. You could even find an Easter Date calculation algorithm and insert that too.
However, such a function wouldn't be simple or as fast as a simple Select statement - you need to decide whether it is worth expending the time and effort to develop such a function and the performance penalty it could impose on your queries is worth the flexibility it would give you and your users.
Almost anything can be done; you should also ask if it should be done...
One terrible approach:
declare #Holidays as Table ( Name VarChar(16), OccurrenceStart Date, Recurrence VarChar(10) );
insert into #Holidays ( Name, OccurrenceStart, Recurrence ) values
( 'New Year Day', '20000101', 'Annually' ),
( 'Sundays', '20000202', 'Weekly' ),
( 'Labour Day 2014', '20141027', 'Once' );
select * from #Holidays;
declare #Start as Date = '20130102';
declare #End as Date = '20150101';
with DateRange as ( -- All dates from #Start to #End .
select #Start as ADate
union all
select DateAdd( day, 1, DR.ADate )
from DateRange as DR
where DR.ADate < #End ),
Once as ( -- Holidays that occur once within the date range.
select DR.ADate as Holiday
from DateRange as DR inner join
#Holidays as H on H.OccurrenceStart = DR.ADate and H.Recurrence = 'Once' ),
Weekly as ( -- Holidays that occur weekly within the date range, give or take.
select DateAdd( week, case when H.OccurrenceStart < #Start then DateDiff( week, H.OccurrenceStart, #Start ) else 0 end, H.OccurrenceStart ) as Holiday
from #Holidays as H
where H.OccurrenceStart <= #End and H.Recurrence = 'Weekly'
union all
select DateAdd( week, 1, W.Holiday )
from Weekly as W
where DateAdd( week, 1, W.Holiday ) <= #End ),
Annually as ( -- Holidays that occur annually within the date range, give or take.
select DateAdd( year, case when H.OccurrenceStart < #Start then DateDiff( year, H.OccurrenceStart, #Start ) else 0 end, H.OccurrenceStart ) as Holiday
from #Holidays as H
where H.OccurrenceStart <= #End and H.Recurrence = 'Annually'
union all
select DateAdd( year, 1, A.Holiday )
from Annually as A
where DateAdd( year, 1, A.Holiday ) <= #End )
select Count( 42 ) as 'Number of Holidays'
from DateRange as DR inner join
( select Holiday from Once union
select Holiday from Weekly union
select Holiday from Annually ) as H on H.Holiday = DR.ADate
option ( MaxRecursion 0 );
Note that multiple 'hits' on one day, e.g. New Years Day falling on a Sunday, are counted as a single holiday. There are more efficient ways to generate DateRange, e.g. with a numbers table. And the whole thing is hideous.
Ask and ye shall be deceived.

SQL Server 2008 CASE Logic in SELECT statement

Hello again SQL Server 2008 gurus.
I need to apply the following rules to the setting of a worker's start and end times for their work day (hourly employees) in a SELECT statement. I apologize in advance for my SQL ignorance.
The rule is to set their start time to a value stored in a table field for that worker, if they login on or before their start time (a time stored in the worker starttime column) and therefore get credit for starting at their start time.
If they log out within a 10 minute period before or anytime after their end time stored in a column for the worker, they get credit for their full day, another value stored in a column of the worker table, otherwise they are penalized some percentage of an hour, i.e. their log out time rounded to .25 of an hour less closest to the time they logged out. i.e. if they are set to log out at 4:30, and they log out at 4:18, their log out time is 4:15. If they log out at 4:20, and are set to log out at 4:30, their log out time is 4:30.
The first rule applies to all hourly employees where their workday hours is less than or equal to their expected workday value. The caveat is, for those where overtime is ok (a bit value set to 1). If overtime is permitted, the number of billable hours can exceed the full day value stored for them, and therefore the value of their logout - login time can exceed their fullday value.
The question is, can these rules be calculated in the SELECT statement and if so can I get some help with the code?
The columns containing the information are:
worker.startime (TIME)
worker.endtime (TIME)
worker.overtimeallowed (BIT)
worker.workdayhours (decimal (12,2))
worker.penaltyvalue (decimal (12,2))
If it requires a UDF or stored procedure (since I'm using the Telerik ReportViewer) I'm not sure it would be supported, but that's probably another question.
So far I've gotten some help with applying some CASE logic - calculating whether a worker get's credit for their 1/2 lunch. The code that was supplied works as promised. This, I believe may be an extension to that logic - so I'll provide the code I have here:
-- for testing purposes only.
DECLARE #StartDate AS DateTime
SET #StartDate = CAST('03/25/2012' AS DATE)
DECLARE #EndDate AS DateTime
SET #EndDate = CAST('04/10/2012' AS DATE)
SELECT
w.Firstname
,w.Lastname
,wf.Login
,wf.logout
,ROUND(CAST(DATEDIFF(MI, wf.Login, wf.Logout) AS DECIMAL)/60,2)
- CASE
WHEN DATEDIFF(hour, wf.Login, wf.Logout) < w.MinimumHours THEN
w.LunchDeduction
ELSE
0
END AS [Hours Credited]
FROM Workers AS w
JOIN Workflow AS wf
ON wf.LoggedInWorkerid = w.ID
JOIN Roles AS r
ON w.RoleID = r.RoleID
WHERE (r.Descript = 'Hourly')
AND wf.Login >= #StartDate AND wf.Logout <= #EndDate
ORDER BY w.Lastname, w.Firstname
Here is a sample select dealing with constraints you described. CTEs create tables for testing purposes. Main query shows the calculations. You have worked with datediffs and dateadds so there is no mistery. If you haven't use % before, it is modulo operator used to round time to 15 minutes.
;with worker (ID, overtime, startTime, endTime) as
(
select 1, 1, CAST ('08:30' as time), CAST ('16:30' as time)
union all
select 2, 0, CAST ('08:30' as time), CAST ('16:30' as time)
union all
select 3, 0, CAST ('08:30' as time), CAST ('16:30' as time)
),
-- Test table of workflows
wf (workerID, login, logout) as
(
select 1, CAST ('2012-03-11 08:20' as datetime), CAST ('2012-03-11 19:33' as datetime)
union all
select 2, CAST ('2012-03-11 08:50' as datetime), CAST ('2012-03-11 16:20' as datetime)
union all
select 3, CAST ('2012-03-11 08:22' as datetime), CAST ('2012-03-11 16:18' as datetime)
)
select wf.workerID,
wf.login,
wf.logout,
-- if starttime > login return startTime else login
case when DATEDIFF(MI, w.startTime, cast (wf.login as time)) < 0
then cast(CAST (wf.login AS date) AS datetime) + w.startTime
else wf.login
end roundedLogin,
case when w.overtime = 1 -- Round to 15 minutes whenever finished
OR
-- Round to 15 minutes if left ten or more minutes before endTime
DATEDIFF(MI, cast (wf.logout as time), dateadd (MI, -10, w.endTime)) > 0
then dateadd (MI, -(DATEPART (MI, wf.logout) % 15), wf.logout)
-- stop at endTime if overtime = 0 OR left job at apropriate time
else cast(CAST (wf.logout AS date) AS datetime) + w.endTime
end roundedLogout
from worker w
inner join wf
on w.ID = wf.workerID
There will be a problem with this approach. When you start to integrate mathematics into original query you will notice that you have to write expressions evaluating roundedLogin and roundedLogout again to calculate billable hours. You cannot reuse alias defined in the same scope, but you can create derived table or view or even calculated fields. View returning columns from workflows and all additional expressions would probably be the best.
Using this view in other queries would simplify things by encapsulating logic at one place.