SELECT returning total booking price - mysql

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

Related

How to query available item leases based on a date range in MySQL?

We have a business that rents out international phone numbers to customers when traveling. When a customer makes an order We want to display to the customer the available phone numbers for his booking dates based on his start_date and end_date and numbers which is not occupied yet.
Since these phone numbers are rented out, I need to select from the table ONLY those numbers that are not rented out yet for dates that would interfere with the current customers dates.
I also don't want to rent out any phone number prior to 7 days after its end date. Meaning, If a customer booked a phone number for 1-1-2020 through 1-20-2020, I don't want this phone number to be booked by another customer before 1-27-2020. I want the phone number to have a 7 day window of being clear.
I have a table with the phone numbers and a table with the orders that is related to the phone numbers table via phone_number_id. The orders table has the current customers start_date and end_date for travel without the phone number id saved yet to it. The orders table also has the start_date and end_date for all other customers dates of travel as well as which phone_number_id was assigned/booked up for their travel dates.
How would the MySQL query look like when trying to select the phone numbers that are available for the current customers dates?
I build below query at the moment
SELECT x.id
, x.area_code
, x.phone_number
, y.start_date
, y.end_date
FROM vir_num_table x
LEFT
JOIN orderitemsdetail_table y
ON y.vn_id = x.id
WHERE y.start_date BETWEEN '2020-01-11' AND '2020-01-18'
OR y.start_date IS NULL
I've build this query but stuck here how can I add end_date logic.
Any help would be appreciated! Thanks in advance.
The way I'd approach the problem would be to look at conceptually, is as a cross product of the set of all phone numbers, along with the reservation timeframe, and then exclude those where there's a conflicting reservation.
A conflict would be an overlap, existing reservation that has a start_date before the end of the proposed reservation AND has an end_date on or after the start of the proposed reservation.
I'd do an anti-join pattern, something like this:
SELECT pn.phone_number
FROM phone_number pn
LEFT
JOIN reservation rs
ON rs.phone_number = pn.phone_number
AND rs.start_dt <= '2019-12-27' + INTERVAL +7 DAY
AND rs.end_dt > '2019-12-20' + INTERVAL -7 DAY
WHERE rs.phone_number IS NULL
That essentially says get all rows from phone number, along with matching rows from reservations (rows that overlap), but then exclude all the rows that had a match, leaving just phone_number rows that did not have a match.
We can make the < test a <= or , subtract 8 days, to tailor the "7 day" window before; we can tweak as we run the query through the test cases,
We can achieve an equivalent result using a NOT EXISTS and a correlated subquery. Some people find this easier to comprehend than the ant-join, but its essentially the same query, doing the same thing, get all rows from phone_number but exclude the rows where there is a matching (overlapping) row in reservation
SELECT pn.phone_number
FROM phone_number pn
WHERE NOT EXISTS
( SELECT 1
FROM reservation rs
WHERE rs.phone_number = pn.phone_number
AND rs.start_dt <= '2019-12-27' + INTERVAL +7 DAY
AND rs.end_dt > '2019-12-20' + INTERVAL -7 DAY
)
There are several questions on StackOverflow about checking for overlap, or no overlap, of date ranges.
See e.g.
How to check if two date ranges overlap in mysql?
PHP/SQL - How can I check if a date user input is in between an existing date range in my database?
MySQL query to select distinct rows based on date range overlapping
EDIT
Based on the SQL added as an edit to the question, I'd do the query like this:
SELECT pn.`id`
, pn.`area_code`
, pn.`phone_number`
FROM `vir_num_table` pn
LEFT
JOIN `orderitemsdetail_table` rs
ON rs.vn_id = pn.id
AND rs.start_date <= '2020-01-18' + INTERVAL +7 DAY
AND rs.end_date > '2020-01-11' + INTERVAL -7 DAY
WHERE rs.vn_id IS NULL
The two "tricky" parts. First is the anti-join, understanding how that works. (An outer join, to return all rows from vir_num_table but exclude any rows that have a matching row in reservations. The second tricky part is checking for the overlap, coming up with the conditions: r.start <= p.end AND r.end >= p.start, then tweaking whether we want to include the equals as an overlap, and tweaking the extra seven days (easiest to me to just subtract the 7 days from the beginning of the proposed reservation)
... now occurs to me like we need to add a guard period of 7 days on the end of the reservation period as well, doh!
Here's a query plus sorting algo to choose the optimal phone number selection for maximum utilization efficiency (i.e. getting as close as possible to exactly 7 days before and after each use).
I set it to give open ends a weight of 9, so that "near perfect" fits (7-8 days before or after) would be selected ahead of open-ended numbers. This will yield a slight efficiency improvement, as open numbers can accommodate any reservation. You can adjust this for your needs. If you set this to 0, for example, it would always select open numbers first.
SELECT ph.phone_number,
COALESCE(
MIN(
IF(res.end_date > res.start_date > '2020-01-18',
NULL, -- ignore before-comparison for reservations starting and ending after date range
DATEDIFF('2020-01-11', res.end_date)
), 9) AS open_days_before,
COALESCE(
MIN(
IF(res.start_date < res.end_date < '2020-01-11',
NULL, -- ignore after-comparison for reservations starting and ending before date range
DATEDIFF(res.start_date, '2020-01-18')
), 9) AS open_days_after
FROM phone_number ph
LEFT JOIN reservation res
ON res.phone_number = ph.phone_number
AND res.end_date >= CURRENT_DATE() - INTERVAL 6 DAY
GROUP BY ph.phone_number
HAVING open_days_before >= 7
AND open_days_after >= 7
ORDER BY open_days_before + open_days_after
LIMIT 1
Edit: updated to add grouping, because I realize this is an aggregate problem.
Edit 2: bug fix, changed MAX to MIN
Edit 3: added res.end_date >= CURRENT_DATE - INTERVAL 6 DAY to ignore past reservations, limiting aggregate data and treating phone number with no reservations between 6 days ago and the beginning of the new order as "open on the front-end"
Edit 4: added IF conditions to eliminate reservations outside the given before-or-after comparison ranges (e.g. comparing reservations after the selected range from influencing the "open days before" number), to prevent negative numbers, except when there's overlap with the selected range.
Based on the info you've added then you shouldn't need to check the start date of phone numbers which have been booked out.
You customer provides you with a start date and an end date.
You only rent out phone numbers 7 days after their last lease ended
All you need to do is fetch back phone numbers which either:
- Are not rented out and therefor aren't in the orderitems table
- OR have an end_date which is 7 days before the new customer's start date.
Here you go:
SELECT
`main_table`.`id`,
`main_table`.`area_code`,
`main_table`.`phone_number`,
`orderitemsdetail_table`.`start_date`,
`orderitemsdetail_table`.`end_date`
FROM
`vir_num_table` AS `main_table`
LEFT JOIN
`orderitemsdetail_table` AS `orderitemsdetail_table` ON main_table.id = orderitemsdetail_table.vn_id
WHERE
(DATE_ADD(orderitemsdetail_table.end_date, INTERVAL 7 DAY) < '<CUSTOMER START DATE>'
AND orderitemsdetail_table.start_date > '<CUSTOMER END DATE>')
OR orderitemsdetail_table.id IS NULL

"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.

Overtime in MySQL timecard over two weeks

I'm trying to figure out how to calculate the overtime on timecard bills that have been or need to be paid. The problem is that bills cover more than one weeks worth of hours and the query grabs more than one employee's history at a time. Any suggestions on how to do this, perhaps with a case statement of some kind?
For example, suppose in my list of employees one works 39 hours one week and 45 the next. The bill would show 84 hours worked, and would also need to show 5 hours of overtime (not four!). This needs to be done in the context of the below query, which handles multiple bills and multiple employees.
Note the query below shows how this would work if the billing period was only one week.
select
username,
CASE
WHEN paidOn IS NULL THEN 'Unpaid'
ELSE paidOn
END as paid,
round(sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600,2) AS hours
, CASE
WHEN round(sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600,2) > 40
THEN round((sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600 - 40) * payrate + 40 * payrate,2)
ELSE
round(sum(TIME_TO_SEC(TIMEDIFF(timeOut, timeIn)))/3600 * payrate, 2)
END as pay
from
timecard
LEFT JOIN
employees
ON
employees.userID = timecard.userID
WHERE
paid != 'd'
GROUP BY
paidOn, timecard.userID
ORDER BY
paid DESC
LIMIT 30;
The way I understand it is that this works fine if the billing period is only 1 week, but not when you extend it to multiple weeks. Then I would simply use the query you have above as a subquery and then aggregate your weeks together. Here's a short example of what I mean:
SELECT
EmployeeId,
SUM(RegularPayHours) * RegularPayRate,
SUM(OverTimeHours) * OverTimeRate
FROM
(SELECT
EmployeeId,
DATEPART(week,TimeCardDate) AS [WorkWeek],
CASE WHEN SUM(HoursWorked) > 40 THEN 40 ELSE SUM(HoursWorked) END AS [RegularPayHours],
CASE WHEN SUM(HoursWorked) > 40 THEN SUM(HoursWorked) - 40 ELSE 0 END AS [OvertimeHours]
FROM
TimeCard
WHERE
TimeCardDate BETWEEN StartDate AND EndDate
GROUP BY
EmployeeId,
DATEPART(week,TimeCardDate)
) a
WHERE
WorkWeek IN (1,2)
GROUP BY
EmployeeId
This will give you the first two work weeks combined as one result but with the overtime calculations done at the week level. You can essentially create any custom pay period that is X weeks long with this method.
You may need to custom define what a work-week is by setting the DATEFIRST value.

how to sum different values of time type in sql table

my query :
SELECT (
`total_hours`
)
FROM work_details
WHERE `employee_id` = '28'
AND DATE
BETWEEN '2012-02-01'
AND '2012-02-01'
LIMIT 0 , 30
// Which takes total hours worked for a given employee in given date from database table work_details
result:
total_hours
02:27:29
00:13:56
03:03:49
00:00:03
00:30:20
01:04:13
//result shows the times an employee worked in a given date,I need to get the total of these values to find total working hour in a given date.
how to get sum of these values in a field???
if i use sum(total_hours) result would be
sum( total_hours )
67870
and that is not correct.I require it in time type itself.
And i have one more help needed, this query worked because i gave same date for the BETWEEN clause.
DATE
BETWEEN '2012-02-01'
AND '2012-02-01'
But i need to calculate total working hours of days in a given range , say
DATE
BETWEEN '2012-02-01'
AND '2012-02-29'
Since a given date has multiple total hours entry , i need get total hours for each distinct day.Help??
try this (taken from here):
SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(`total_hours`))) ... -- rest of your query

Hotel Room Rates for different seasons

I have a database (MySQL) with a table containing date ranges (as startdate and enddate) and a rate field. The date range implies different seasons (low, high etc.). The scenario is such that a person checks in the hotel and his duration of stay is in two seasons. A sample data is like below:
SeasonName SartDate EndDate Rate
Low 01-01-2007 30-04-2007 100.00
High 01-05-2007 31-08-2007 150.00
Peak 01-09-2007 31-12-2007 200.00
The client's Check In Date is 29-04-2007 and Check Out Date is 03-05-2007. I need to calculate the exact number of nights for each season and also calculate the total amount.
The IDE is VB6. Any help will be extremely appreciated.
Thanks
Tom
Thanks for the response. I need the SQL to extract the information. As for the date validity, lets assume the rate applies till midnight (00:00). Hope i have clarified.
Tom
Having worked in a hotel and written the reservation system, hourly time is irrelevant as far
as billing goes. Everything is always charged by night. (Unless you plan to run a place that charges by the hour! ;-)) Check-in and check-out are operational considerations.
Do not use stored procuedures if you actually want to write a real reservation system.
It defeats the purpose of having a database.
Also, writing out dates like this is 2007-04-29 is really great way because not every one is from the same place and this is an international standard. Also notice, if you were to turn this into a string it will still be sorted correctly!
You need make a calandar table as MySQL does not have in built in functions to do it.
This procedure will build up dates for you.
drop table if exists calendar;
create table calendar
(
date_ date primary key
);
drop procedure fill_calendar;
delimiter $$
create procedure fill_calendar(start_date date, end_date date)
begin
declare date_ date;
set date_=start_date;
while date_ < end_date do
insert into calendar values(date_);
set date_ = adddate(date_, interval 1 day);
end while;
end $$
delimiter ;
call fill_calendar('2007-1-1', '2007-12-31');
from: http://www.ehow.com/how_7571744_mysql-calendar-tutorial.html
drop table if exists rates;
create table rates
(
season varchar(100) primary key,
start_date date references calendar(date_),
end_date date references calendar(date_),
rate float
);
insert into rates values ('Low', '2007-01-01', '2007-04-30', 100.00);
insert into rates values ('High', '2007-05-01', '2007-08-31', 150.00);
insert into rates values ('Peak', '2007-09-01', '2007-12-21', 200.00);
select * from rates;
season start_date end_date rate
Low 2007-01-01 2007-04-30 100
High 2007-05-01 2007-08-31 150
Peak 2007-09-01 2007-12-21 200
I'm going to ignore the dates you have given in your question and the assume the client is not travelling backwards in time.
select
date_, rate
from calendar
join rates
on date_ >= start_date and date_ <= end_date
where date_ between '2007-04-29' and '2007-5-01'
;
date_ rate
2007-04-29 100
2007-04-30 100
2007-05-01 150
select
sum(rate)
from calendar
join rates
on date_ >= start_date and date_ <= end_date
where date_ between '2007-04-29' and '2007-5-01'
sum(rate)
350
And, as you can see the sql is quite concise and readable without resorting to functions or procedures. This will be able to scale properly and handle more complex questions. Also, it enables referential checking to be used since the data is table based.
I used the DATEDIFF function to know the number of days between 2 dates. But since this function returns the number of days excluding the last day (e.g. DATEDIFF(d, '2007-04-29', '2007-04-30') return 1 instead of 2), I used a rate table like this:
SeasonName StartDate EndDate Rate
Low 01-01-2007 01-05-2007 100.00
High 01-05-2007 01-09-2007 150.00
Peak 01-09-2007 01-01-2008 200.00
This is the query I used. The inner of the two SELECTs calculates an effective end date for the customer's stay according to the season (either the end date of the season or it's date prior to it's check out date).
I used 02-05-2007 as the customer's stay end date instead of 03-05-2007 since usually a customer does does not pay for the day he checks out.
SELECT SeasonName, DATEDIFF(d, '2007-04-29', EffectiveEndDate) as NumberOfDays, Rate
FROM (
SELECT SeasonName,
CASE
WHEN EndDate < '2007-05-02' THEN EndDate
ELSE '2007-05-02'
END AS EffectiveEndDate,
Rate
FROM HotelRate
WHERE (StartDate <= '2007-04-29' and EndDate > '2007-04-29')
or (StartDate <= '2007-05-02' and EndDate > '2007-05-02')
) as SubSelect
This gives me this result:
SeasonName NumberOfDays Rate
Low 2 100.00
High 3 150.00
I hope it helps :)
Disclaimer: This is not the most efficient one, but it's the more clear, and if you have the price list cached in an array, the performance hit will be meaningless.
Dim numberOfDays As Integer, i As Integer
Dim CheckInDate As Date, CheckOutDate As Date, CurrDate As Date
Dim TotalRate As Currency
TotalRate = 0
CheckInDate = DateSerial(2007, 4, 29)
CheckOutDate = DateSerial(2007, 5, 3)
''// -1 asumming the last day is checkout day
numberOfDays = DateDiff("d", CheckInDate, CheckOutDate) - 1
For i = 0 To numberOfDays
CurrDate = DateAdd("d", i, CheckInDate)
TotalRate = TotalRate + CalculateRateForDay(CurrDate)
Next
Use an auxiliary calendar table:
SELECT S1.client_ID, H1.Rate, C1.dt
FROM HotelRates AS H1
INNER JOIN Calendar AS C1
ON C1.dt BETWEEN H1.SartDate AND H1.EndDate
INNER JOIN Stays AS S1
ON C1.dt >= check_in_date AND
C1.dt < check_out_date;