"neater" way of counting holidays - sql-server-2008

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.

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
)

Finding the area available for the date range

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

SQL Statement Database

I have a Mysql Table that holds dates that are booked (for certain holiday properties).
Example...
Table "listing_availability"
Rows...
availability_date (this shows the date format 2013-04-20 etc)
availability_bookable (This can be yes/no. "Yes" = the booking changeover day and it is "available". "No" means the property is booked for those dates)
All the other dates in the year (apart from the ones with "No") are available to be booked. These dates are not in the database, only the booked dates.
My question is...
I have to make a SQL Statement that first calls the Get Date Function (not sure if this is correct terminology)
Then removes the dates from "availability_date" WHERE "availability_bookable" = "No"
This will give me the dates that are available for bookings, for the year, for a property.
Can anyone help?
Regards M
Seems like you've almost written the query.
SELECT availability_date FROM listing_availability
WHERE availability_bookable <> 'NO'
AND availability_date >= CURDATE()
AND YEAR(CURDATE()) = YEAR(availability_date)
I think I understand, and you'll obviously confirm. Your "availability_booking" has some records in it, but not every single day of the year, only those that may have had something, and not all are committed, some could have yes, some no.
So, you want to simulate All dates within a given date range... Say April 1 - July 1 as someone is looking to book a party within that time period. Instead of pre-filling your production table, you can't say that April 27th is open and available... since no such record exists.
To SIMULATE a calendar of days for a date range, you can do it using MySQL variables and join to "any" table in your database provided it has enough records to SIMULATE the date range you want...
select
#myDate := DATE_ADD( #myDate, INTERVAL 1 DAY ) as DatesForAvailabilityCheck
from
( select #myDate := '2013-03-31' ) as SQLVars,
AnyTableThatHasEnoughRows
limit
120;
This will just give you a list of dates starting with April 1, 2013 (the original #myDate is 1 day before the start date since the field selection adds 1 day to it to get to April 1, then continues... for a limit of 120 days (or whatever you are looking for range based -- 30days, 60, 90, 22, whatever). The "AnyTableThatHasEnoughRows" could actually be your "availability_booking" table, but we are just using it as a table with rows, no join or where condition, just enough to get ... 120 records.
Now, we can use this to join to whatever table you want and apply your condition. You just created a full calendar of days to compare against. Your final query may be different, but this should get it most of the way for you.
select
JustDates.DatesForAvailabilityCheck,
from
( select
#myDate := DATE_ADD( #myDate, INTERVAL 1 DAY ) as DatesForAvailabilityCheck
from
( select #myDate := '2013-03-31' ) as SQLVars,
listing_availability
limit
120 ) JustDates
LEFT JOIN availability_bookable
on JustDates.DatesForAvailabilityCheck = availability_bookable.availability_date
where
availability_bookable.availability_date IS NULL
OR availability_bookable.availability_bookable = "Yes"
So the above uses the sample calendar and looks to the availability. If no such matching date exists (via the IS NULL), then you want it meaning there is no conflict. However, if there IS a record in the table, you only want those where YES, you CAN book it, the entry on file might not be committed and CAN be in your result query of available dates.

SELECT returning total booking price

I have the following table SEASONS with time periods defining a booking prices for each period day;
ID, STARTDATE, ENDDATE, PRICE
6, 2012-06-01, 2012-06-30, 20
7, 2012-07-01, 2012-07-31, 35
8, 2012-08-01, 2012-08-31, 30
9, 2012-09-01, 2012-09-30, 25
This table defines pricing periods (start and end dates of pricing period with price of booking for each day in that particular pricing period). The question is how to create a query which will return the total price of booking for all days in some given booking period? For example, how to calculate (SELECT?) the total (SUM) booking price for period from 2012-06-10 to 2012-08-20 ?
(Of course one can easily calculate it manually = 21(days in Jun)x20 + 31(days in Jul)x35 + 20(days in Aug)x30 = 2105) How SELECT statement returning that total booking price should look like?
Use DATEDIFF function:
SELECT SUM(DATEDIFF(ENDDATE,STARTDATE) * PRICE) AS TOTAL_PRICE
FROM SEASONS
WHERE STARTDATE <= '2012-06-10' AND ENDDATE >= '2012-08-20'
Also, we can add a check for booking start/end since they fall somewhere in the middle and you don't need to include all the period...
So:
SET #START='2012-06-10';
SET #END='2012-08-20';
SELECT SUM(
DATEDIFF(
IF(YEAR(ENDDATE)=YEAR(#END) AND MONTH(ENDDATE)=MONTH(#END), #END, ENDDATE),
IF(YEAR(STARTDATE)=YEAR(#START) AND MONTH(STARTDATE)=MONTH(#START), #START, STARTDATE)
)
) AS TOTAL_SUM
FROM SEASONS
WHERE STARTDATE <= #END AND ENDDATE >= #START
Just for other readers, as a result from previous answer the final query is:
SET #START='2012-06-10';
SET #END='2012-08-20';
SELECT SUM(
(DATEDIFF(
IF(enddate>=#END,#END,enddate),
IF(startdate<=#START,#START,startdate)
)+1)*price ) AS TOTAL_SUM
FROM seasons WHERE startdate<=#END AND enddate>=#START
1 should be added to date difference in order both starting and ending date to be included in pricing range and, of course, it should be multiplied with price.
#poncha thank you very much, I already created a procedure which loops through all dates in booking period fetching multiple prices and summing them as a solution but I knew there should be a simpler and more efficient solution

MySQL: Find Missing Dates Between a Date Range

I need some help with a mysql query. I've got db table that has data from Jan 1, 2011 thru April 30, 2011. There should be a record for each date. I need to find out whether any date is missing from the table.
So for example, let's say that Feb 2, 2011 has no data. How do I find that date?
I've got the dates stored in a column called reportdatetime. The dates are stored in the format: 2011-05-10 0:00:00, which is May 5, 2011 12:00:00 am.
Any suggestions?
This is a second answer, I'll post it separately.
SELECT DATE(r1.reportdate) + INTERVAL 1 DAY AS missing_date
FROM Reports r1
LEFT OUTER JOIN Reports r2 ON DATE(r1.reportdate) = DATE(r2.reportdate) - INTERVAL 1 DAY
WHERE r1.reportdate BETWEEN '2011-01-01' AND '2011-04-30' AND r2.reportdate IS NULL;
This is a self-join that reports a date such that no row exists with the date following.
This will find the first day in a gap, but if there are runs of multiple days missing it won't report all the dates in the gap.
CREATE TABLE Days (day DATE PRIMARY KEY);
Fill Days with all the days you're looking for.
mysql> INSERT INTO Days VALUES ('2011-01-01');
mysql> SET #offset := 1;
mysql> INSERT INTO Days SELECT day + INTERVAL #offset DAY FROM Days; SET #offset := #offset * 2;
Then up-arrow and repeat the INSERT as many times as needed. It doubles the number of rows each time, so you can get four month's worth of rows in seven INSERTs.
Do an exclusion join to find the dates for which there is no match in your reports table:
SELECT d.day FROM Days d
LEFT OUTER JOIN Reports r ON d.day = DATE(r.reportdatetime)
WHERE d.day BETWEEN '2011-01-01' AND '2011-04-30'
AND r.reportdatetime IS NULL;`
It could be done with a more complicated single query, but I'll show a pseudo code with temp table just for illustration:
Get all dates for which we have records:
CREATE TEMP TABLE AllUsedDates
SELECT DISTINCT reportdatetime
INTO AllUsedDates;
now add May 1st so we track 04-30
INSERT INTO AllUsedData ('2011-05-01')
If there's no "next day", we found a gap:
SELECT A.NEXT_DAY
FROM
(SELECT reportdatetime AS TODAY, DATEADD(reportdatetime, 1) AS NEXT_DAY FROM AllUsed Dates) AS A
WHERE
(A.NEXT_DATE NOT IN (SELECT reportdatetime FROM AllUsedDates)
AND
A.TODAY <> '2011-05-01') --exclude the last day
If you mean reportdatetime has the entry of "Feb 2, 2011" but other fields associated to that date are not present like below table snap
reportdate col1 col2
5/10/2011 abc xyz
2/2/2011
1/1/2011 bnv oda
then this query works fine
select reportdate from dtdiff where reportdate not in (select df1.reportdate from dtdiff df1, dtdiff df2 where df1.col1 = df2.col1)
Try this
SELECT DATE(t1.datefield) + INTERVAL 1 DAY AS missing_date FROM table t1 LEFT OUTER JOIN table t2 ON DATE(t1.datefield) = DATE(t2.datefield) - INTERVAL 1 DAY WHERE DATE(t1.datefield) BETWEEN '2020-01-01' AND '2020-01-31' AND DATE(t2.datefield) IS NULL;
If you want to get missing dates in a datetime field use this.
SELECT CAST(t1.datetime_field as DATE) + INTERVAL 1 DAY AS missing_date FROM table t1 LEFT OUTER JOIN table t2 ON CAST(t1.datetime_field as DATE) = CAST(t2.datetime_field as DATE) - INTERVAL 1 DAY WHERE CAST(t1.datetime_field as DATE) BETWEEN '2020-01-01' AND '2020-07-31' AND CAST(t2.datetime_field as DATE) IS NULL;
The solutions above seem to work, but they seem EXTREMELY slow (taking possibly hours, I waited for 30 min only) at least in my database.
This clause takes less than a second in same database (of course you need to repeat it manually dozen times and possibly change function names to find the actual dates). pvm = my datetime, WEATHER = my table.
mysql> select year(pvm) as _year,count(distinct(date(pvm))) as _days from WEATHER where year(pvm)>=2000 and month(pvm)=1 group by _year order by _year asc;
--ako