Overlapping Booking SQL [duplicate] - mysql

This question already has an answer here:
Overlapping Booking Query
(1 answer)
Closed 4 years ago.
I have a similiar problem with my sql statement like here Room Booking Query.
the following query works if an apartment has only one booking. but if an apartment has more than one booking, this apartment is also in the result, although it is not available in the requested time range.
SELECT DISTINCT `apartment`.*
FROM `apartment` `apartment`
LEFT JOIN `booking` `booking` ON `apartment`.`uid` = `booking`.`apartment`
WHERE (
NOT(
( `booking`.`start` <= '2018-07-23')
AND
( `booking`.`end` >= '2018-07-21')
)
)
Can someone help me please to write the right sql?
UPDATE:
According the hint of Matt Raines i added a field apartment, with the uid of the apartment, to the booking table. I'm very thankful for any suggestion which helps me to write the right SQL statement!
Here the UPDATED Demo Data:
--
-- Table structure for table `apartment`
--
CREATE TABLE `apartment` (
`uid` int(11) NOT NULL,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
`bookings` int(10) UNSIGNED NOT NULL DEFAULT '0'
)
--
-- Data for table `tx_apartments_domain_model_apartment`
--
INSERT INTO `apartment` (`uid`, `title`, `bookings`) VALUES
(1, 'Apartment 1', 2),
(2, 'Apartment 2', 1),
(3, 'Apartment 3', 1),
(4, 'Apartment 4', 1);
--
-- Table structure for table `booking`
--
CREATE TABLE `booking` (
`uid` int(11) NOT NULL,
`start` date DEFAULT '0000-00-00',
`end` date DEFAULT '0000-00-00',
`apartment` int(10) UNSIGNED NOT NULL DEFAULT '0'
)
--
-- Data for table `booking`
--
INSERT INTO `booking` (`uid`, `start`, `end`, `apartment`) VALUES
(1, '2018-07-18', '2018-07-20', 1),
(2, '2018-07-21', '2018-07-23', 1),
(3, '2018-07-18', '2018-07-20', 2);

You are off track thinking this has to do with multiple rows from the join. The problem is with your logic in the WHERE clause. You don't say what you are wanting in terms of the dates, so it is impossible to know what the solution should be.
I simplified down to just looking at the booking table. I get the two rows where you are expecting only one. All you need to do is figure out the conditional that you really want.
mysql> SELECT * FROM booking WHERE NOT(start <= '2018-07-23' AND end >= '2018-07-21');
+-----+------------+------------+-----------+
| uid | start | end | apartment |
+-----+------------+------------+-----------+
| 1 | 2018-07-18 | 2018-07-20 | 1 |
| 3 | 2018-07-18 | 2018-07-20 | 2 |
+-----+------------+------------+-----------+
2 rows in set (0.00 sec)

I think you are looking for a list of apartments that do not have any bookings in the date range in question.
Your query, instead, looks for apartments that have at least one booking which is not in the date range in question.
The answers to the question you have linked to should work, but you could also try reversing the question to find apartments that do have a booking in the date range. Then use a LEFT JOIN and a WHERE booking.uid IS NULL to filter out those results.
SELECT apartment.*
FROM apartment
LEFT JOIN booking ON apartment.uid = booking.apartment
AND booking.start <= '2018-07-23' AND booking.end >= '2018-07-21'
WHERE booking.uid IS NULL
You might also want to look into adding a foreign key for that booking.apartment field. At the very least, it should be the same datatype as apartment.uid (at the moment one is an INT(10) UNSIGNED and the other is an INT(11)).
The start and end dates for the booking should probably be NOT NULL, unless you can have a booking without dates. And the apartment.bookings field now looks redundant.

Related

Return all data from left table, even if where clause is not attended

I have a seller_commissions table, where are related with two other tables: products and sellers (users)
I need to make a painel, where admin can update seller commissions for each product.
Products will be created over time, so I don't want to insert data in seller_commissions table when this occurs, because I would need to do this multiples times. So, my solution was:
get all products data for user's update. If seller_commissions are null for specific product, this means the target seller never has your commission updated. In other words, all sellers have commission = 0 in first moment.
I try the following queries:
-- This is the result what I want, but filtering by seller_id, but, unfortannaly this return all products for each seller (I want to specify the seller_id)
select fpp.name as product_name,
fsc.seller_id,
fsc.commission
from fp_products as fpp
left join fp_sellers_commissions as fsc
on fsc.product_id = fpp.id
left join fp_users as fpu
on fpu.id = fsc.seller_id;
-- If I use 'where' clause, not all products are returned, because seller_id is none
select fpp.name as product_name,
fsc.seller_id,
fsc.commission
from fp_products as fpp
left join fp_sellers_commissions as fsc
on fsc.product_id = fpp.id
left join fp_users as fpu
on fpu.id = fsc.seller_id
where seller_id = 1;
result for first query:
result for second query:
expected results:
product_name
seller_id
commission
shirt
1
250
shoes
null
0
black shirt
null
0
In first query, is something similiar with what I want. Get all products and seller_commission, but I want this for a specific seller, but when I try to use WHERE clause, I don't get all products, because seller_id can be null. I try some variations of these queries, but can't get the expected result :/. Appreciate any help.
to build the schema, use:
-- Create schema
CREATE TABLE `fp_sellers_commissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commission` float NOT NULL DEFAULT '0',
`product_id` int(11) NOT NULL,
`seller_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `fp_products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) CHARACTER SET latin1 NOT NULL,
`createdAt` datetime DEFAULT CURRENT_TIMESTAMP,
`disabled` tinyint(4) DEFAULT '0',
PRIMARY KEY (`id`)
);
CREATE TABLE `fp_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET latin1 NOT NULL,
`surname` varchar(32) CHARACTER SET latin1 NOT NULL,
PRIMARY KEY (`id`)
);
-- Inserting data:
INSERT INTO `fp_products`
(`id`, `name`, `createdAt`, `disabled`)
VALUES
(1, 'shirt', '00:00:00', 0),
(2, 'shoes', '00:00:00', 0),
(3, 'black shirt', '00:00:00', 0);
INSERT INTO `fp_users`
(`id`,
`name`,
`surname`)
VALUES
(1, 'bilbo', 'aaa'),
(2, 'frodo', 'aaa');
INSERT INTO `fp_sellers_commissions`
(`id`, `commission`, `product_id`, `seller_id`)
VALUES
(1, 100, 1, 1),
(2, 500, 1, 2);
Or you can acess SQL FIDDLE: http://sqlfiddle.com/#!9/d6559f/5
I'm not sure why the expected result should be with a commission of "250" for the seller "1", but I think I got what you are searching for. If you want to filter the seller's commission and still display the other products with nulls, you could put the filter condition directly on the left join, kinda like the following.
select fpp.name as product_name,
fsc.seller_id,
fsc.commission
from fp_products as fpp
left join fp_sellers_commissions as fsc
on fsc.product_id = fpp.id and fsc.seller_id = 1
left join fp_users as fpu
on fpu.id = fsc.seller_id;
What happens here, is that the filtering condition is applied at the moment you do the left join, so if it does not match, since it is a "left" join, the results will still be returned with nulls. If you put it in the "where" clause, it will be applied after the join is applied, and it will filter out the results that do not match.
My suggestion is
select fpp.name as product_name,
fsc.seller_id,
SUM(ifnull(fsc.commission, 0)) as commission
from fp_products as fpp
left join fp_sellers_commissions as fsc
on fpp.id = fsc.product_id and fsc.seller_id = 1
group by fpp.name, fsc.seller_id
order by fsc.seller_id desc;
with this must be getting the result you need. Note: I added a group summing to commissions, but if not is the goal, just remove the group by and the sum function.
Hoping this can help you.

Max from joined table based on value from first table

I have 2 tables.
First holds job details, second one the history of those job runs. First one also contains job period, per customer which is minimum time to wait before running next job for same customer. The time comparison needs to happen on started_on field of second table.
I need to find out the job ids to run next.
Schemas
job_details table
CREATE TABLE `job_details` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`customer_id` varchar(128) NOT NULL,
`period_in_minutes` int(11) unsigned NOT NULL,
`status` enum('ACTIVE','INACTIVE','DELETED') DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
job_run_history table
CREATE TABLE `job_run_history` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`job_id` int(10) unsigned NOT NULL,
`started_on` timestamp NULL DEFAULT NULL,
`status` enum('STREAMING','STREAMED','UPLOADING','UPLOADED','NO_RECORDS','FAILED') DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_job_id` (`job_id`),
CONSTRAINT `fk_job_id` FOREIGN KEY (`job_id`) REFERENCES `job_details` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Sample data for job_details table:
INSERT INTO `job_details` (`id`, `customer_id`, `period_in_minutes`, `status`)
VALUES
(1, 'cust1', 1, 'ACTIVE'),
(2, 'cust2', 1, 'ACTIVE'),
(3, 'cust3', 2, 'ACTIVE');
Sample data for job_run_history table:
INSERT INTO `job_run_history`(`job_id`, `started_on`, `status`)
VALUES
(1, '2021-07-01 14:38:00', 'UPLOADED'),
(2, '2021-07-01 14:37:55', 'UPLOADED');
Expected output (When run at 2021-07-01 14:38:56):
id
2,3
id => 1 did NOT get selected because the last job started within last 1 minute
id => 2 DID get selected because the last job started more than last 1 minute ago
id => 3 DID get selected because it has no run history
I have tried this, but this doesn't compare with max of start_time, hence, doesn't work:
select jd.id, max(jrh.started_on) from job_details jd
left join job_run_history jrh on jrh.job_id=jd.id
where
jd.status='ACTIVE'
and (jrh.status is null or jrh.status not in ('STREAMING','STREAMED','UPLOADING'))
and (jrh.`started_on` is null or jrh.`started_on` < date_sub(now(), interval jd.`period_in_minutes`*60 second))
group by jd.id;
MySql Version: 5.7.34
Any help please? Thanks in advance..
I'd prefer to use UNION ALL (it must be more fast than one complex query):
-- the subquery for the rows which have matched ones in 2nd table
SELECT t1.id
FROM job_details t1
JOIN job_run_history t2 ON t1.id = t2.job_id
WHERE t1.status = 'ACTIVE'
AND t2.status not in ('STREAMING','STREAMED','UPLOADING')
AND CURRENT_TIMESTAMP - INTERVAL t1.period_in_minutes MINUTE > t2.started_on
UNION ALL
-- the subquery for the rows which have no matched ones in 2nd table
SELECT id
FROM job_details t1
WHERE NOT EXISTS ( SELECT NULL
FROM job_run_history t2
WHERE t1.id = t2.job_id )
AND status = 'ACTIVE';
https://dbfiddle.uk/?rdbms=mysql_5.7&fiddle=8dcad95bf43ce711fdf40deda627e879
select jd.id from job_details jd
left join job_run_history jrh on jd.id= jrh.job_id
where jd.status = 'ACTIVE'
group by jd.id
having
max(jrh.started_on) < current_timestamp - interval max(jd.period_in_minutes) minute
or
max(jrh.id) is null
I'm not sure what's this filter about since you didn't explain it in your question so I didn't put it in the query: jrh.status not in ('STREAMING','STREAMED','UPLOADING'). However, I'm sure you can implement it in the query I posted.

Is it possible to COUNT if one value occured more than the other value in a column using SQL

I have this table called task_status which has the following structure:
CREATE TABLE `task_status` (
`task_status_id` int(11) NOT NULL,
`status_id` int(11) NOT NULL,
`task_id` int(11) NOT NULL,
`date_recorded` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `task_status`
ADD PRIMARY KEY (`task_status_id`);
ALTER TABLE `task_status`
MODIFY `task_status_id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
INSERT INTO `task_status` (`task_status_id`, `status_id`, `task_id`, `date_recorded`) VALUES
(1, 1, 16, 'Wednesday 6th of January 2021 09:20:35 AM'),
(2, 2, 17, 'Wednesday 6th of January 2021 09:20:35 AM'),
(3, 3, 18, 'Wednesday 6th of January 2021 09:20:36 AM');
and a status_list table that has the possible statuses available
CREATE TABLE `status` (
`statuses_id` int(11) NOT NULL,
`status` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `status`
ADD PRIMARY KEY (`statuses_id`);
ALTER TABLE `status`
MODIFY `statuses_id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
COMMIT;
INSERT INTO `status` (`statuses_id`, `status`) VALUES
(1, 'Yes'),
(2, 'Inprogress'),
(3, 'No');
Now what I want to do is check which number occurred more inside the status_id column 1 occurred more, 2 occurred more or 3 occurred more? using SQL.
Is it possible to do and if so how to?
You can try OVER and PARTITION BY clauses, you simply specify the column you want to partition your aggregated results by.
Example code
select status_id,count(*) over (partition by status_id) as Count_1 from task_status
You can count the column first then filter with max
there is a lot of different way to do this but i prefer using cte.
Here is a example :
with cte as(
select status_id,count(*) cnt from task_status
group by status_id
)
select * from cte
where cnt = (select max(cnt) from cte)
also here is db<>fiddle for better examine.
I modify some data to show the much more understandable output. But idea is same.
also I don't really think status table have any work doing here, but remind me if I misunderstand what you mean.
If you want exactly one status that occurs more often than the others, then I would recommend group by with order by and limit:
select status_id, count(*) as cnt
from task_status
group by status_id
order by cnt desc
limit 1;
This always returns one row, so if there are ties for the most common, then you only get one of the ties.

Mysql check availability between times

I need to check if is available time between 10:00 to 10:30 (should return 0 rows) or to book from 12:30 to 17:00 (should return 1 row)
CREATE TABLE IF NOT EXISTS `clase` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fechaClase` date NOT NULL,
`horaInicio` time NOT NULL,
`horaFin` time NOT NULL,
PRIMARY KEY (`id`));
INSERT INTO `clase` (`id`, `fechaClase`, `horaInicio`, `horaFin`) VALUES
(1, '2019-10-28', '10:30:00', '12:00:00'),
(2, '2019-10-28', '09:00:00', '10:00:00'),
(3, '2019-10-28', '13:30:00', '15:00:00');
https://www.db-fiddle.com/f/jefiEkboU7qo4baEMW25VV/0
Three cases subsist, start booking time falls into already booked range, end booking time falls into already booked range, start and end booking times straddle already booked range. A left join will find booked/no booked and a null test will print message.
drop table if exists clase;
CREATE TABLE IF NOT EXISTS `clase` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fechaClase` date NOT NULL,
`horaInicio` time NOT NULL,
`horaFin` time NOT NULL,
PRIMARY KEY (`id`));
INSERT INTO `clase` (`id`, `fechaClase`, `horaInicio`, `horaFin`) VALUES
(1, '2019-10-28', '10:30:00', '12:00:00'),
(2, '2019-10-28', '09:00:00', '10:00:00'),
(3, '2019-10-28', '13:30:00', '14:00:00');
select distinct
case when c.id is null then concat(s.horainicio,':',s.horafin,' available')
else concat(s.horainicio,':',s.horafin,' not available')
end as sta
from
(
select '10:00' as horaInicio, '10:30' as horafin
union
select '14:30' ,'17:00'
union
select '13:00' ,'15:00'
) s
left join clase c on (s.horainicio between c.horaInicio and c.horafin) or
(s.horafin between c.horaInicio and c.horafin) or
(c.horainicio between s.horainicio and s.horafin) or
(c.horafin between s.horainicio and s.horafin)
+---------------------------+
| sta |
+---------------------------+
| 10:00:10:30 not available |
| 13:00:15:00 not available |
| 14:30:17:00 available |
+---------------------------+
3 rows in set (0.06 sec)

Joining table with min(amount) does not work

I have 3 tables, but data is only fetch from 2 tables.
I'm trying to get the lowest bids for selected items and display user name with the lowest bid.
Currently query works until when we display user name, it shows wrong user name, which does not match the bid.
Below is working example of structure and query.
SQL Fiddle
MySQL 5.6 Schema Setup:
CREATE TABLE `bid` (
`id` int(11) NOT NULL,
`amount` float NOT NULL,
`user_id` int(11) NOT NULL,
`item_id` int(11) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1;
INSERT INTO `bid` (`id`, `amount`, `user_id`, `item_id`) VALUES
(1, 9, 1, 1),
(2, 5, 2, 1),
(3, 4, 3, 1),
(4, 3, 4, 1),
(5, 4, 2, 2),
(6, 22, 5, 1);
-- --------------------------------------------------------
CREATE TABLE `item` (
`id` int(11) NOT NULL,
`name` varchar(100) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
INSERT INTO `item` (`id`, `name`) VALUES
(1, 'chair'),
(2, 'sofa'),
(3, 'table'),
(4, 'box');
-- --------------------------------------------------------
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`name` varchar(100) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1;
INSERT INTO `user` (`id`, `name`) VALUES
(1, 'James'),
(2, 'Don'),
(3, 'Hipes'),
(4, 'Sam'),
(5, 'Zakam');
ALTER TABLE `bid`
ADD PRIMARY KEY (`id`);
ALTER TABLE `item`
ADD PRIMARY KEY (`id`);
ALTER TABLE `user`
ADD PRIMARY KEY (`id`);
ALTER TABLE `bid`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=7;
ALTER TABLE `item`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=5;
ALTER TABLE `user`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=5;
Query 1:
SELECT b.id, b.item_id, MIN(b.amount) as amount, b.user_id, p.name
FROM bid b
LEFT JOIN user p ON p.id = b.user_id
WHERE b.item_id in (1, 2)
GROUP BY b.item_id
ORDER BY b.amount, b.item_id
Results:
| id | item_id | amount | user_id | name |
|----|---------|--------|---------|-------|
| 5 | 2 | 4 | 2 | Don |
| 1 | 1 | 3 | 1 | James |
Explanation of query:
Get the selected items (1, 2).
get the lowest bid for thous items - MIN(b.amount)
display user names, who has given the bid - LEFT JOIN user p on p.id = b.user_id (this is not working or I'm doing something wrong)
[Note] I can't use sub-query, I'm doing this in doctrine2 (php code) which limits mysql sub-query
No, you are not necessarily fetching the user_id who has given the bid. You group by item_id, so you get one result row per item. So you are aggregating and for every column you say what value you want to see for that item. E.g.:
MIN(b.amount) - the minimum amount of the item's records
MAX(b.amount) - the maximum amount of the item's records
AVG(b.amount) - the avarage amount of the item's records
b.amount - one of the amounts of the item's records arbitrarily chosen (as there are many amounts and you don't specify which you want to see, the DBMS simply choses one of them)
This said, b.user_id isn't necessarily the user who made the lowest bid, but just one random user of the users who made a bid.
Instead find the minimum bids and join again with your bid table to access the realted records:
select bid.id, bid.item_id, bid.amount, user.id as user_id, user.name
from bid
join
(
select item_id, min(amount) as amount
from bid
group by item_id
) as min_bid on min_bid.item_id = bid.item_id and min_bid.amount = bid.amount
join user on user.id = bid.user_id
order by bid.amount, bid.item_id;
You can solve this using a subquery. I am not 100% sure if this is the most efficient way, but at least it works.
SELECT b1.id, b1.item_id, b1.amount, b1.user_id, p.name
FROM bid b1
LEFT JOIN user p ON p.id = b1.user_id
WHERE b1.id = (
SELECT b2.id
FROM bid b2
WHERE b2.item_id IN (1, 2)
ORDER BY b2.amount LIMIT 1
)
This first selects for the lowest bid with for item 1 or 2 and then uses the id of that bid to find the information you need.
Edit
You are saying that Doctrine does not support subqueries. I have not used Doctrine a lot, but something like this should work:
$subQueryBuilder = $entityManager->createQueryBuilder();
$subQuery = $subQueryBuilder
->select('b2.id')
->from('bid', 'b2')
->where('b2.item_id IN (:items)')
->orderBy('b2.amount')
->setMaxResults(1)
->getDql();
$queryBuilder = $entityManager->createQueryBuilder();
$query = $queryBuilder
->select('b1.id', 'b1.item_id', 'b1.amount', 'b1.user_id', 'p.name')
->from('bid', 'b1')
->leftJoin('user', 'p', 'with', 'p.id = b1.user_id')
->where('b1.id = (' . $subQuery . ')')
->setParameter('items', [1, 2])
->getQuery()->getSingleResult();