Combining multiple rows to make up a date range - mysql

I'm not sure if this is possible with MySQL and its a little complicated, but I'll try and explain as best as possible.
I amending out accommodation availability & price checking system based on an existing database. The visitor will be able to select an arrival and a departure date and I want to be able to show all the rates/offers that are available for those dates. Easy. However, an offer may be set up to be available for only the first part of the given date range and a second rate may be set up for that room type covering the later part of the date range.
e.g. I select September 29th until October 3rd for my stay. A rate has been set up for room 1 to cover September 1st - 30th, and a second rate for October 1st - 31st.
Can I query the table to check if besides rates that span the entire range, there are pairs (or maybe even more) rates for the same room id that together cover the date range, and retrieve the rates & descriptions for those?
The query below checks for basic rates that are valid between the given dates (where roomtype is the id of the room):
SELECT id, caption, price, startdate, enddate, nights, roomtype,
(#arrival := DATE('2011-09-29')) AS arrivaldate,
(#departure := DATE('2011-10-05')) AS departuredate
FROM bnbrates
WHERE
bnb_ref = 16639
AND active = 'TRUE'
AND rooms != 0
AND #arrival < enddate
AND #departure > startdate
AND roomtype != ''
ORDER BY roomtype, enddate, startdate, price;
and this is a sample of the data stored:
id | caption | price | startdate | enddate | nights | roomtype | arrivaldate | departuredate
-------------------------------------------------------------------------------------------------------------------
23553 | Single | 50 | 2011-10-01 | 2011-10-31 | 1 | 1064 | 2011-09-29 | 2011-10-05
23544 | Double | 55 | 2011-09-01 | 2011-09-30 | 1 | 1647 | 2011-09-29 | 2011-10-05
23545 | Double | 80 | 2011-10-01 | 2011-10-31 | 1 | 1647 | 2011-09-29 | 2011-10-05
30312 | Triple | 109 | 2011-09-01 | 2011-09-30 | 1 | 1649 | 2011-09-29 | 2011-10-05
34234 | Executive | 109 | 2011-09-01 | 2011-09-30 | 1 | 1653 | 2011-09-29 | 2011-10-05
23569 | Executive | 99 | 2011-10-01 | 2011-10-31 | 1 | 1653 | 2011-09-29 | 2011-10-05
The desired result would for example combine 23544 with 23545 and 34234 with 23569
Thank you in advance for your help, and apologies if this doesn't make sense...

Related

Mysql - Get season from current month

I have the following table of seasons:
| id | name | start_month | end_month |
------------------------------------------
| 101 | Summer | 12 | 2 |
| 102 | Winter | 6 | 8 |
| 103 | Spring | 9 | 11 |
| 104 | Fall | 3 | 5 |
I need to get the season by month. Say current month is 2 (February), I want Summer to be the output.
I can get other seasons to work by simply having the where condition start_month >= 4 and end_month <= 4. But this won't work with Summer since the season crosses into next year.
What do I have to do to handle the case of Summer?
One solution I thought was to use dates instead of month number like 1980-12-01 and use between function but it gets a bit complicated for the user end.
It'd be great if it could work with just month numbers.
You could do:
(month(d) between start_month and end_month) or
(start_month>end_month and (month(d)>=start_month or month(d)<=end_month))
See db-fiddle

Find the name, start date and duration in months of projects that have the earliest end date - mysql

I was studying subqueries in sql and couldn't figure out the answer to this question. Following are the tables.
Project(p͟r͟o͟j͟n͟o͟, projname, prestdate, prendate)
+--------+----------------------+------------+------------+
| projno | projname | prstdate | prendate |
+--------+----------------------+------------+------------+
| AD3100 | Admin Services | 2014-01-01 | 2015-02-01 |
| AD3110 | General AD Systems | 2014-01-01 | 2015-02-01 |
| MA2113 | W L Prod Cont Progs | 2014-02-15 | 2014-12-01 |
| PL2100 | Weld Line Planning | 2014-01-01 | 2014-09-15 |
I came up with this but I think its wrong:
select projname
, prstdate
, MONTH(prendate - prstdate) as duration
from Proj
where prendate - prstdate IN (select MIN(prendate - prstdate) from Proj);
You are on the right track. But your query is looking at the duration of the projects, not the end date.
Second, the calculation of the duration is off. The best function to use is TIMESTAMPDIFF().
So, just modify the code to look at the end date instead of the duration:
select projnamek, prstdate
timestampdiff(month, prstdate, prendate) as duration
from Proj
where prendate = (select min(prendate) from Proj);

Insert certain weekday within a week if it doesn't exist

I have a simple table as follows:
DROP TABLE IF EXISTS table_name;
CREATE TABLE table_name
(symbol CHAR(4)
,time DATE
,bid DECIMAL (7,2)
,ask DECIMAL (7,2)
,PRIMARY KEY(symbol,time)
);
insert into table_name
(symbol,`time`,bid,ask)
values
('CN50','2020-09-25',15077.5, 15087.5),
('CN50','2020-09-28',15255 , 15265 ),
('CN50','2020-09-29',15257 , 15267 ),
('CN50','2020-09-30',15258 , 15268 ),
('CN50','2020-10-01',15259 , 15269 );
SELECT *, DATE_FORMAT(time,'%a') dow FROM table_name;
+--------+------------+----------+----------+------+
| symbol | time | bid | ask | dow |
+--------+------------+----------+----------+------+
| CN50 | 2020-09-25 | 15077.50 | 15087.50 | Fri |
| CN50 | 2020-09-28 | 15255.00 | 15265.00 | Mon |
| CN50 | 2020-09-29 | 15257.00 | 15267.00 | Tue |
| CN50 | 2020-09-30 | 15258.00 | 15268.00 | Wed |
| CN50 | 2020-10-01 | 15259.00 | 15269.00 | Thu |
+--------+------------+----------+----------+------+
What I wish to achieve is that if there is no value for Sunday within a week, insert a new row between this Friday and next Monday with Sunday date in time column and copy the bid/ask values from Friday.
In this example, 2020-09-25 is Friday and 2020-09-28 is Monday, I would expect to insert a row with 2020-09-27 as time and keep the bid and ask value from last Friday which is 2020-09-25.
Before:
Symbol
time
bid
ask
CN50
2020-09-25
15077.5
15087.5
CN50
2020-09-28
15255
15265
After:
Symbol
time
bid
ask
CN50
2020-09-25
15077.5
15087.5
CN50
2020-09-27
15077.5
15087.5
CN50
2020-09-28
15255
15265
As mentioned, I would handle this kind of thing in application code, but if you know that Friday's going to be there (and, crucially, that Sunday isn't!) , then something like this would suffice...
SELECT *
FROM table_name
UNION
SELECT symbol
, time + INTERVAL 2 DAY
, bid
, ask
FROM table_name
WHERE DATE_FORMAT(time + INTERVAL 2 DAY,'%a') = 'sun'
ORDER
BY symbol
, time;
+--------+------------+----------+----------+
| symbol | time | bid | ask |
+--------+------------+----------+----------+
| CN50 | 2020-09-25 | 15077.50 | 15087.50 |
| CN50 | 2020-09-27 | 15077.50 | 15087.50 |
| CN50 | 2020-09-28 | 15255.00 | 15265.00 |
| CN50 | 2020-09-29 | 15257.00 | 15267.00 |
| CN50 | 2020-09-30 | 15258.00 | 15268.00 |
| CN50 | 2020-10-01 | 15259.00 | 15269.00 |
+--------+------------+----------+----------+
If there's a chance that Sunday could be there already, then you could left join this result onto the original data set, and use COALESCE to filter the correct value - but by this point you'd probably want switch back to application code or employ the CTE/Windowing tools available in MySQL 8.0+

MySQL complex date calculation in inner join

I am reading the MySql tutorial in the docs and have the following tables and SQL statements:
Event table:
+----------+------------+----------+------------------------------+
| name | date | type | remark |
+----------+------------+----------+------------------------------+
| Fluffy | 1995-05-15 | litter | 4 kittens, 3 females, 1 male |
| Buffy | 1993-06-23 | litter | 5 puppies, 2 female, 3 male |
| Buffy | 1994-06-19 | litter | 3 puppies, 3 female |
| Chirpy | 1999-03-21 | vet | needed beak streightened |
| Slim | 1997-08-03 | vet | broken rib |
| Bowser | 1991-10-12 | kennel | NULL |
| Fang | 1991-10-12 | kennel | NULL |
| Fang | 1998-08-28 | birthday | Gave him new chew toy |
| Claws | 1998-03-17 | birthday | Gave him a flea collar |
| Whistler | 1998-12-09 | birthday | First birthday |
+----------+------------+----------+------------------------------+
Pet table:
+----------+--------+---------+------+------------+------------+
| name | owner | species | sex | birth | death |
+----------+--------+---------+------+------------+------------+
| Fluffy | Harold | cat | f | 1993-02-04 | NULL |
| Claws | Gwen | cat | m | 1994-03-17 | NULL |
| Buffy | Harold | dog | f | 1989-05-13 | NULL |
| Fang | Benny | dog | m | 1990-08-27 | NULL |
| Bowser | Diane | dog | m | 1989-03-31 | 1995-07-29 |
| Chirpy | Gwen | bird | f | 1998-09-11 | NULL |
| Whistler | Gwen | bird | NULL | 1997-12-09 | NULL |
| Slim | Benny | snake | m | 1996-04-29 | NULL |
| Puffball | Diane | hamster | f | 1999-03-30 | NULL |
| Jenny | Robert | dog | f | 2004-01-01 | 2014-05-04 |
+----------+--------+---------+------+------------+------------+
SQL:
select pet.name,
( YEAR(date) - YEAR(birth) ) - ( RIGHT(date,5) < RIGHT(birth,5) ) AS age, remark
from pet inner join event
on pet.name = event.name
where event.type = 'litter';
I understand the SQL statement except for this one:
( YEAR(date) - YEAR(birth) ) - ( RIGHT(date,5) < RIGHT(birth,5) )
A step by step explanation would greatly help. I know that the YEAR() function is used to extract the year from a date.
As you've mentioned YEAR() gets the year from the date.
We will use Fluffy as an example with birth = 1993-02-04 and date = 1995-05-15
Step 1:
Subtract the extracted year from both dates ( YEAR(date) - YEAR(birth) )
You now have 1995 - 1993 which is equal to 2
Step 2:
( RIGHT(date,5) < RIGHT(birth,5) ) this will actually read the date and birth string 5 paces from right to left, so if you perform RIGHT(date,5) you will get the value 0, and if you perform RIGHT(birth,5) you will also get a value of 0.
Step 3:
Now we get on to the < operator, this returns a boolean value of 1 or 0 if it satisfies the condition. Since 0 = 0, the statement is false, so it will return to 0.
The whole function actually checks if the day part of your date is less than the day on your birthdate which will determine if you have a sort of remaining days before a whole year. And if you do, it will return 1 which will be subtracted from the current year - year operation you performed earlier.
But in our case, since the < will return 0, we can definitely say that Fluffy's age is 2 - 0, which is 2.
If, however, Fluffy's birthdate is say, 1993-12-04, this will yield a 1 value for the < operation meaning that the year is not yet complete which will bring a result of 2 - 1, which is 1.
Sorry if its a bit messy.
If you were born in 1980 and I know that right now it's 2015, then I I can almost compute your age by computing YEAR(date) - YEAR(birth) = 2015 - 1980 = 35. The sticking point is that your birthday might not have happened yet. So how can I tell if your birthday has happened? Lets say you were born on July 20, 1980. Then in MySQL date format your birthday would look like 2015-06-20. If I look at the last 5 characters of your birthday, the right-most 5 characters, or RIGHT(birth, 5) I'd get 06-20. If the the last 5 characters of today's date, 06-07 is less than the last 5 characters of your birthday then your birthday hasn't happened yet. Here, by "less than" we mean only that it would "sort before" in normal string order. So, if RIGHT(date, 5) < RIGHT(birth, 5) then your birthday hasn't happened yet. And, luckily, MySQL treats a "true" as the number 1 and a "false" as the number 0. So, the above will subtract 1 from our date computation only if your birthday hasn't happened yet.
It's a clever(?) way of checking if the month and day of the date of birth happened before or after the date from the event (litter) in order to properly calculate the number of years between the events (as the year part in itself isn't enough).
In this part:
( YEAR(date) - YEAR(birth) ) - ( RIGHT(date,5) < RIGHT(birth,5) )
the RIGHT(date,5) returns the month and day part like (03-30) and does a boolean less than comparison which returns either 0 or 1 depending on the result. This is then subtracted from the YEAR(date) - YEAR(birth) calculation so that the years between the events get adjusted correctly.

mysql split a string in a where clause

I have an event system and for my repeat events I am using a cron like system.
Repeat Event:
+----+----------+--------------+
| id | event_id | repeat_value |
+----+----------+--------------+
| 1 | 11 | *_*_* |
| 2 | 12 | *_*_2 |
| 3 | 13 | *_*_4/2 |
| 4 | 14 | 23_*_* |
| 5 | 15 | 30_05_* |
+----+----------+--------------+
NOTE: The cron value is day_month_day of week
Event:
+----+------------------------+---------------------+---------------------+
| id | name | start_date_time | end_date_time |
+----+------------------------+---------------------+---------------------+
| 11 | Repeat daily | 2014-04-30 12:00:00 | 2014-04-30 12:15:00 |
| 12 | Repeat weekly | 2014-05-06 12:00:00 | 2014-05-06 13:00:00 |
| 13 | Repeat every two weeks | 2014-05-08 12:45:00 | 2014-05-08 13:45:00 |
| 14 | Repeat monthly | 2014-05-23 15:15:00 | 2014-05-23 16:00:00 |
| 15 | Repeat yearly | 2014-05-30 07:30:00 | 2014-05-30 10:15:00 |
+----+------------------------+---------------------+---------------------+
Anyway I have a query to select the events:
SELECT *
FROM RepeatEvent
JOIN `Event`
ON `Event`.`id` = `RepeatEvent`.`event_id`
That produces:
+----+----------+--------------+----+------------------------+---------------------+---------------------+
| id | event_id | repeat_value | id | name | start_date_time | end_date_time |
+----+----------+--------------+----+------------------------+---------------------+---------------------+
| 1 | 11 | *_*_* | 11 | Repeat daily | 2014-04-30 12:00:00 | 2014-04-30 12:15:00 |
| 2 | 12 | *_*_2 | 12 | Repeat weekly | 2014-05-06 12:00:00 | 2014-05-06 13:00:00 |
| 3 | 13 | *_*_4/2 | 13 | Repeat every two weeks | 2014-05-08 12:45:00 | 2014-05-08 13:45:00 |
| 4 | 14 | 23_*_* | 14 | Repeat monthly | 2014-05-23 15:15:00 | 2014-05-23 16:00:00 |
| 5 | 15 | 30_05_* | 15 | Repeat yearly | 2014-05-30 07:30:00 | 2014-05-30 10:15:00 |
+----+----------+--------------+----+------------------------+---------------------+---------------------+
However, I want to select events within a month. I will only have certain conditions: daily, weekly, every two weeks, month and yearly.
I want to put in my where clause a way to divide the string of the repeat value and if it fits any of the following conditions to show it as a result (repeatEvent is row that is being interrogated, search is the date being looked for):
array(3) = string_divide(repeat_value, '_')
daily = array(0)
monthy = array(1)
dayOfWeek = array(2)
if(daily == '*' && month == '*' && dayOfWeek == '*') //returns all the daily events as they will happen
return repeatEvent
if(if(daily == '*' && month == '*' && dayOfWeek == search.dayOfWeek) //returns all the events on specific day
return repeatEvent
if(daily == search.date && month == '*' && dayOfWeek == '*') //returns all the daily events as they will happen
return repeatEvent
if (contains(dayOfWeek, '/'))
array(2) = string_divide(dayOfWeek,'/')
specificDayOfWeek = array(0);
if(specificDayOfWeek == repeatEvent.start_date.dayNumber)
if(timestampOf(search.timestamp)-timestampOf(repeatEvent.start_date)/604800 == (0 OR EVEN)
return repeatEvent
if(daily == search.date && month == search.month && dayOfWeek == '*') //returns a single yearly event (shouldn't often crop up)
return repeatEvent
//everything else is either an unknown format of repeat_value or not an event on this day
To summarise I want to run a query in which the repeat value is split in the where clause and I can interrogate the split items. I have looked at cursors but the internet seems to advise against them.
I could process the results of selecting all the repeat events in PHP, however, I imagine this being very slow.
Here is what I would like to see if looking at the month of April:
+----------+--------------+----+------------------------+---------------------+---------------------+
| event_id | repeat_value | id | name | start_date_time | end_date_time |
+----------+--------------+----+------------------------+---------------------+---------------------+
| 11 | *_*_* | 11 | Repeat daily | 2014-04-30 12:00:00 | 2014-04-30 12:15:00 |
+----------+--------------+----+------------------------+---------------------+---------------------+
Here is what I would like to see if looking at the month of May
+----------+--------------+----+------------------------+---------------------+---------------------+
| event_id | repeat_value | id | name | start_date_time | end_date_time |
+----------+--------------+----+------------------------+---------------------+---------------------+
| 11 | *_*_* | 11 | Repeat daily | 2014-04-30 12:00:00 | 2014-04-30 12:15:00 |
| 12 | *_*_2 | 12 | Repeat weekly | 2014-05-06 12:00:00 | 2014-05-06 13:00:00 |
| 13 | *_*_4/2 | 13 | Repeat every two weeks | 2014-05-08 12:45:00 | 2014-05-08 13:45:00 |
| 14 | 23_*_* | 14 | Repeat monthly | 2014-05-23 15:15:00 | 2014-05-23 16:00:00 |
| 15 | 30_05_* | 15 | Repeat yearly | 2014-05-30 07:30:00 | 2014-05-30 10:15:00 |
+----------+--------------+----+------------------------+---------------------+---------------------+
Here is what I would like to see if looking at the month of June
+----------+--------------+----+------------------------+---------------------+---------------------+
| event_id | repeat_value | id | name | start_date_time | end_date_time |
+----------+--------------+----+------------------------+---------------------+---------------------+
| 11 | *_*_* | 11 | Repeat daily | 2014-04-30 12:00:00 | 2014-04-30 12:15:00 |
| 12 | *_*_2 | 12 | Repeat weekly | 2014-05-06 12:00:00 | 2014-05-06 13:00:00 |
| 13 | *_*_4/2 | 13 | Repeat every two weeks | 2014-05-08 12:45:00 | 2014-05-08 13:45:00 |
| 14 | 23_*_* | 14 | Repeat monthly | 2014-05-23 15:15:00 | 2014-05-23 16:00:00 |
+----------+--------------+----+------------------------+---------------------+---------------------+
You could put a bandaid on this, but no one would be doing you any favors to tell you that that is the answer.
If your MySQL database can be changed I would strongly advise you to split your current column with underscores day_month_day of year to three separate columns, day, month, and day_of_year. I would also advise you to change your format to be INT rather than VARCHAR. This will make it faster and MUCH easier to search and parse, because it is designed in a way that doesn't need to be translated into computer language through complicated programs... It is most of the way there already.
Here's why:
Reason 1: Your Table is not Optimized
Your table is not optimized and will be slowed regardless of what you choose to do at this stage. SQL is not built to have multiple values in one column. The entire point of an SQL database is to split values into different columns and rows.
The advantage to normalizing this table is that it will be far quicker to search it, and you will be able to build queries in MySQL. Take a look at Normalization. It is a complicated concept, but once you get it you will avoid creating messy and complicated programs.
Reason 2: Your Table could be tweaked slightly to harness the power of computer date/time functions.
Computers follow time based on Unix Epoch Time. It counts seconds and is always running in your computer. In fact, computers have been counting this since, as the name implies, the first Unix computer was ever switched on. Further, each computer and computer based program/system, has built in, quick date and time functions. MySQL is no different.
I would also recommend also storing all of these as integers. repeat_doy (day of year) can easily be a smallint or at least a standard int, and instead of putting a month and day, you can put the actual 1-365 day of the year. You can use DAY_OF_YEAR(NOW()) to input this into MySQL. To pull it back out as a date you can use MAKEDATE(YEAR(NOW),repeat_doy). Instead of an asterisk to signify all, you can either use 0's or NULL.
With a cron like system you probably will not need to do that sort of calculation anyway.
Instead, it will probably be easier to just measure the day of year elsewhere (every computer and language can do this. In Unix it is just date "%j").
Solution
Split your one repeat_value into three separate values and turn them all into integers based on UNIX time values. Day is 1-7 (or 0-6 for Sunday to Saturday), Month is 1-12, and day of year is 1-365 (remember, we are not including 366 because we are basing our year on an arbitrary non-leap year).
If you want to pull information in your SELECT query in your original format, it is much easier to use concat to merge the three columns than it is to try to search and split on one column. You can also easily harness built in MySQL functions to quickly turn what you pull into real, current, days, without a bunch of effort on your part.
To implement it in your SQL database:
+----+----------+--------------+--------------+------------+
| id | event_id | repeat_day | repeat_month | repeat_doy |
+----+----------+--------------+--------------+------------+
| 1 | 11 | * | * | * |
| 2 | 12 | * | * | 2 |
| 3 | 13 | * | * | 4/2 |
| 4 | 14 | 23 | * | * |
| 5 | 15 | 30 | 5 | * |
+----+----------+--------------+--------------+------------+
Now you should be able to build one query to get all of this data together regardless of how complicated your query. By normalizing your table, you will be able to fully harness the power of relational databases, without the headaches and hacks.
Edit
Hugo Delsing made a great point in the comments below. In my initial example I provided a fix to leap years for day_of_year in which I chose to ignore Feb 29. A much better solution removes the need for a fix. Split day_of_year to month and day with a compound index. He also has a suggestion about weeks and number of weeks, but I will just recommend you read it for more details.
Try to write where condition using this:
substring_index(repeat_value,'_', 1)
instead of daily
substring_index(substring_index(repeat_value,'_', -2), '_', 1)
instead of monthly
and
substring_index(substring_index(repeat_value,'_', -1), '_', 1)
instead of dayOfWeek
I think you are overthinking the problem if you only want the events per month and not per day. Assuming that you always correctly fill the repeat_value, the query is very basic.
Basically all event occur every month where the repeat_value is either LIKE '%_*_%' or LIKE '%_{month}_%'.
Since you mentions PHP I'm assuming you are building the query in PHP and thus I used the same.
<?php
function buildQuery($searchDate) {
//you could/should do some more checking if the date is valid if the user provides the string
$searchDate = empty($searchDate) ? date("Y-m-d") : $searchDate;
$splitDate = explode('-', $searchDate);
$month = $splitDate[1];
//Select everything that started after the searchdate
//the \_ is because else the _ would match any char.
$query = 'SELECT *
FROM RepeatEvent
JOIN `Event`
ON `Event`.`id` = `RepeatEvent`.`event_id`
WHERE `Event`.`start_date_time` < \''.$searchDate.'\'
AND
(
`RepeatEvent`.`repeat_value` LIKE \'%\_'.$month.'\_%\'
OR `RepeatEvent`.`repeat_value` LIKE \'%\_*\_%\'
)
';
return $query;
}
//show querys for all months on current day/year
for ($month = 1; $month<=12; $month++) {
echo buildQuery(date('Y-'.$month.'-d')) . '<hr>';
}
?>
Now if the repeat_value could be wrong, you could add a simple regex check to make sure the value is always like *_*_* or *_*_*/*
You can use basic regular expressions in MySQL:
http://dev.mysql.com/doc/refman/5.0/en/pattern-matching.html
For a monthly event in May (first day) you can use a pattern like this (not tested):
[0-9\*]+\_[5\*]\_1
You can generate this pattern via PHP