Writing a JOIN query with two counts from joined table - mysql

I've spent a few hours fighting with this, but I can't get the counts to work. Hopefully someone can help?!
I have a project table and task table, linked on the project_id. I can get the project_id, project_name, and the status_id with the query below:
SELECT
a.project_id,
a.project_name,
b.status_id
FROM project_list as a
INNER JOIN task_list as b
ON a.project_id=b.project_id
I'd like to select a single record for each project and add two count fields based on the status_id. In pseudo code:
SELECT
a.project_id,
a.project_name,
(SELECT COUNT(*) FROM task_list WHERE status_id < 3) as not_completed,
(SELECT COUNT(*) FROM task_list WHERE status_id = 3) as completed
FROM project_list as a
INNER JOIN task_list as b
ON a.project_id=b.project_id
GROUP BY project_id
My create table scripts are below:
CREATE TABLE `project_list` (
`project_id` int(11) NOT NULL AUTO_INCREMENT,
`topic_id` int(11) DEFAULT NULL,
`project_name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`project_id`)
)
CREATE TABLE `task_list` (
`task_id` int(11) NOT NULL AUTO_INCREMENT,
`project_id` int(11) DEFAULT NULL,
`task_name` varchar(45) DEFAULT NULL,
`status_id` int(11) DEFAULT '0',
PRIMARY KEY (`task_id`)
)
Any help is much appreciated. Thanks!
EDIT: ANSWER:
SELECT
a.project_id,
project_name,
SUM(status_id != 3) AS not_completed,
SUM(status_id = 3) AS completed,
SUM(status_id IS NOT NULL) as total
FROM tasks.project_list as a
INNER JOIN tasks.task_list as b
ON a.project_id=b.project_id
GROUP BY a.project_id

The problem is that in your subqueries you are counting all the rows in the whole table rather than just the rows that have the correct project_id. You could fix this by modifying the WHERE clause in each of your subqueries.
(SELECT COUNT(*)
FROM task_list AS c
WHERE c.status_id < 3
AND a.project_id = c.project_id)
However a simpler approach is to use SUM with a boolean condition instead of COUNT to count the rows that match the condition:
SELECT
a.project_id,
a.project_name,
SUM(b.status_id < 3) AS not_completed,
SUM(b.status_id = 3) AS completed,
FROM project_list as a
INNER JOIN task_list as b
ON a.project_id = b.project_id
GROUP BY project_id
This works because TRUE evaluates to 1 and FALSE evaluates to 0.

Related

Mysql Inner join - how to use value of first table column in join clause

In my "bookings" table, each booking has a number of persons and an "event_time" , which is one of three time slots which is bookable.
In my query I am trying to return how many free seats there are left for each restaurant and time slot (event_time number)
I select restaurants and do an INNER JOIN to include the bookings table, but I would need access to the "number_of_seats_max" column from the restaurants table inside the inner join, which does not seem possible.
Here is fiddle.
Tables:
CREATE TABLE `restaurants` (
`id` int(10) UNSIGNED NOT NULL,
`title` text COLLATE utf8mb4_unicode_ci,
`number_of_seats_max` int(11) DEFAULT NULL
);
CREATE TABLE `bookings` (
`id` int(10) UNSIGNED NOT NULL,
`event_date` timestamp NULL DEFAULT NULL,
`event_time` int(11) NOT NULL,
`number_of_persons` int(11) NOT NULL,
`restaurant_id` int(11) NOT NULL
);
The below query works, but in this case I have hard coded "80" instead of the max seats column ( r.number_of_seats_max ). Thats the column I need to use. If you put r.number_of_seats_max instead, you get the error "unknown column".
SELECT r.title, r.number_of_seats_max, innerquery.free_seats_left,
innerquery.num_persons_booked
FROM restaurants r
INNER JOIN(
select
restaurant_id,
SUM(number_of_persons) as num_persons_booked,
(80 - SUM(number_of_persons)) AS free_seats_left // <-- 80 is hard coded
from bookings
WHERE event_date = '2019-07-18'
group by event_time,restaurant_id
ORDER BY free_seats_left DESC
) as innerquery
ON innerquery.restaurant_id = r.id;
How can I solve it?
Do the subtraction in the main query, not the subquery.
SELECT r.title, innerquery.event_time, r.number_of_seats_max,
r.number_of_seats_max - innerquery.num_persons_booked AS free_seats_left,
innerquery.num_persons_booked
FROM restaurants r
INNER JOIN(
select
restaurant_id,
event_time,
SUM(number_of_persons) as num_persons_booked
from bookings
WHERE event_date = '2019-07-18'
group by event_time,restaurant_id
) as innerquery
ON innerquery.restaurant_id = r.id
ORDER BY free_seats_left DESC
I added event_time to the SELECT list of both the subquery and the main query, so you can show the available seats for each time slot.

MySQL left join doesnt give me what i expect

I'd like some help with an left join statement thats not doing what i, probably incorrectly, think it should do.
there are two tables:
cd:
CREATE TABLE `cd` (
`itemID` int(11) NOT NULL AUTO_INCREMENT,
`title` text NOT NULL,
`artist` text NOT NULL,
`genre` text NOT NULL,
`tracks` int(11) NOT NULL,
PRIMARY KEY (`itemID`)
)
loans
CREATE TABLE `loans` (
`itemID` int(11) NOT NULL,
`itemType` varchar(20) NOT NULL,
`userID` int(11) NOT NULL,
`dueDate` date NOT NULL,
PRIMARY KEY (`itemID`,`itemType`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
and i want to select all cd's thats not in loans using a left join and then an where dueDate is null
select
t.itemID,
t.artist as first,
t. title as second,
(select AVG(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `rating avarage`,
(select COUNT(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `number of ratings`
from
cd t left join loans l
on t.itemID = l.itemID
where l.itemType = 'cd' and l.dueDate is null;
this one however returns an empty table even though there are plenty rows in cd with itemIDs thats not in loans
now i was under the understanding that the left join should preserv the righthandside and fill the columns from the lefthandside with null values
but this does not seem to be the case, can anbyone enlighten me?
Your WHERE condition causes the error. The L.ItemType = 'cd' will always return false if the L.DueDate IS NULL is true. (All of your fields are NOT NULL, so the DueDate can only be NULL if there is no matching records, but in this case the ItemType field will be NULL too).
Another point is that your query is semantically incorrect. You are trying to get the record from the cd table where the loans table do not contains any rows with dueDates.
The second table acts as a condition, so it should go to the WHERE conditions.
Consider to use the EXISTS statement to achieve your goal:
SELECT
t.itemID,
t.artist as first,
t. title as second,
(select AVG(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `rating avarage`,
(select COUNT(rating) from ac9039.ratings where itemType = 'cd' and itemId = t.itemID) as `number of ratings`
FROM
cd t
WHERE
NOT EXISTS (SELECT 1 FROM loans l WHERE t.itemID = l.itemID AND L.itemType = 'cd')
Based on your data model you have to add another condition to the subquery to filter out those records which are out-of-date now (dueDate is earlier than the current time)
This is the case, when you do not delete outdated loan records.
NOT EXISTS (SELECT 1 FROM loans l WHERE t.itemID = l.itemID AND AND L.itemType = 'cd' l.dueDate > NOW())

Exclusion of all rows possible via outer join?

I'll start off with the schema:
CREATE TABLE CustomersActions (
`caID` int(10) unsigned NOT NULL AUTO_INCREMENT primary key,
`cusID` int(11) unsigned NOT NULL,
`caTimestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
)
CREATE TABLE `Assignments` (
`asgID` int(10) unsigned NOT NULL AUTO_INCREMENT primary key,
`cusID` int(11) unsigned NOT NULL,
`asgAssigned` date DEFAULT NULL,
`astID` int(10) unsigned NOT NULL
)
CREATE TABLE `AssignmentStatuses` (
`astID` int(10) unsigned NOT NULL AUTO_INCREMENT primary key,
`astStatus` varchar(255) DEFAULT ''
)
My original query is:
SELECT DISTINCT
ca.cusID
FROM
CustomersActions ca
WHERE
NOT EXISTS (
SELECT
TRUE
FROM
Assignments asg
NATURAL JOIN AssignmentStatuses
WHERE
asg.cusID = ca.cusID
AND (
DATE_ADD(asgAssigned, INTERVAL 6 DAY) > NOW()
OR astStatus IN('Not contacted', 'Follow-up')
)
)
What this does is select all cusID entries from CustomersActions if said Customer does not have a row in Assignments that is in "Not contacted" or "Follow-up" (for any date range) OR has an assignment of any status from less than six days ago.
I tried writing the same query using LEFT JOIN to exclude from Assignments like so:
SELECT DISTINCT
ca.cusID
FROM
CustomersActions ca
LEFT JOIN (
Assignments asg
NATURAL JOIN AssignmentStatuses
) ON (ca.cusID = asg.cusID)
WHERE
asgID IS NULL
OR DATE_ADD(asgAssigned, INTERVAL 6 DAY) < NOW()
OR astStatus IN('Not contacted', 'Follow-up')
The problem is that it's possible for a customer to have multiple entries in Assignments so a cusID can be selected even if they have an existing row that should force them to be excluded. This makes sense to me, and the NOT EXISTS solves this problem.
What I'm wondering is if there is a way to perform a single query that has the same effect as the query when using NOT EXISTS. That is, a customer should be excluded if they have any rows that satisfy the exclusion condition, not only if all of their rows satisfy the exclusion condition (or if they have none).
Have you tried using NOT IN clause, like:
SELECT DISTINCT
ca.cusID
FROM
CustomersActions ca
WHERE cusID
NOT IN (
SELECT
cusID
FROM
Assignments asg
INNER JOIN AssignmentStatuses ast
ON asg.astID = ast.astID
WHERE
DATE_ADD(asgAssigned, INTERVAL 6 DAY) > NOW()
OR astStatus IN('Not contacted', 'Follow-up')
)

Need help creating SQL query

I have thus two tables:
CREATE TABLE `workers` (
`id` int(7) NOT NULL AUTO_INCREMENT,
`number` int(7) NOT NULL,
`percent` int(3) NOT NULL,
`order` int(7) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE `data` (
`id` bigint(15) NOT NULL AUTO_INCREMENT,
`workerId` int(7) NOT NULL,
PRIMARY KEY (`id`)
);
I want to return the first worker (order by order ASC) that his number of rows in the table data times percent(from table workers) /100 is smaller than number(from table workers.
I have tried this query:
SELECT workers.id, COUNT(data.id) AS `countOfData`
FROM `workers` as workers, `data` as data
WHERE data.workerId = workers.id
AND workers.percent * `countOfData` < workers.number
LIMIT 1
But I get the error:
#1054 - Unknown column 'countOfData' in 'where clause'
This should work:
SELECT A.id
FROM workers A
LEFT JOIN (SELECT workerId, COUNT(*) AS Quant
FROM data
GROUP BY workerId) B
ON A.id = B.workerId
WHERE (COALESCE(Quant,0) * `percent`)/100 < `number`
ORDER BY `order`
LIMIT 1
You could calculate the number of rows per worker in a subquery. The subquery can be joined to the worker table. If you use a left join, a worker with no data rows will be considered:
select *
from workers w
left join
(
select workerId
, count(*) as cnt
from data
group by
workerId
) d
on w.id = d.workerId
where coalesce(d.cnt, 0) * w.percent / 100 < w.number
order by
w.order
limit 1

MySQL joins involving aggregate data

What I require is a SQL query which can report on data from aggregate and singular tables. The current database I have is as follows.
CREATE TABLE IF NOT EXISTS `faults_days` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`employee_id` int(11) NOT NULL,
`day_date` date NOT NULL,
`actioned_calls_out` int(11) NOT NULL,
`actioned_calls_in` int(11) NOT NULL,
`actioned_tickets` int(11) NOT NULL,
)
CREATE TABLE IF NOT EXISTS `faults_departments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(40) NOT NULL,
)
CREATE TABLE IF NOT EXISTS `faults_employees` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`team_id` int(11) NOT NULL,
`name` varchar(127) NOT NULL,
)
CREATE TABLE IF NOT EXISTS `faults_qos` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`qos_date` datetime NOT NULL,
`employee_id` int(11) NOT NULL,
`score` double NOT NULL,
)
CREATE TABLE IF NOT EXISTS `faults_teams` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`department_id` int(11) NOT NULL,
`name` varchar(40) NOT NULL,
)
A row in Day tracks a single employee's performance for a single day (number of calls taken, number of tickets actioned). A Qos is a measure of an employee's quality on a day (there can be more than one Qos per day - what I need to obtain is the average score). Also, a Qos can be performed on a day where the employee has no performance entry in the database, and this will still need to be shown on the report.
The required end result is 4 reports, which show the employee performance grouped by different columns. A breakdown of a single employee's performance per day, an employee's total performance over a period of time, a team's performance over a period of time, and a whole department's performance over a period of time.
My problem, is that my current queries are a little convoluted, and require two separate queries for the Day data, and the Qos data. My PHP application then combines the data before outputting the report. What I would like, is a single query which returns both total performance, and average quality scores.
The current queries I have to show employee performances are:
SELECT
`Employee`.`name` ,
`Team`.`name` ,
`Department`.`name` ,
SUM( `Day`.`actioned_calls_in` ) + SUM( `Day`.`actioned_calls_out` ) ,
SUM( `Day`.`actioned_tickets` )
FROM
`faults_days` AS `Day`
JOIN
`faults_employees` AS `Employee` ON `Day`.`employee_id` = `Employee`.`id`
JOIN
`faults_teams` AS `Team` ON `Employee`.`team_id` = `Team`.`id`
JOIN
`faults_departments` AS `Department` ON `Team`.`department_id` = `Department`.`id`
WHERE
`Day`.`day_date` >= '2011-06-01'
AND `Day`.`day_date` <= '2011-06-07'
GROUP BY `Employee`.`id`
WITH ROLLUP
and
SELECT
`Employee`.`name` ,
`Team`.`name` ,
`Department`.`name` ,
COUNT( `Qos`.`score` ) ,
AVG( `Qos`.`score` )
FROM
`faults_qos` AS `Qos`
JOIN
`faults_employees` AS `Employee` ON `Qos`.`employee_id` = `Employee`.`id`
JOIN
`faults_teams` AS `Team` ON `Employee`.`team_id` = `Team`.`id`
JOIN
`faults_departments` AS `Department` ON `Team`.`department_id` = `Department`.`id`
WHERE
`Qos`.`qos_date` >= '2011-06-01'
AND `Qos`.`qos_date` <= '2011-06-07'
GROUP BY `Employee`.`id`
WITH ROLLUP
I have also tried simply joining the Qos table, but because it returns multiple rows it messes up the SUM() totals, and also has problems due to the missing FULL OUTER JOIN functionality.
EDIT:
I've made some small progress with this. It looks like using subqueries is the way to go, but everything I'm doing is pure guesswork. Here's what I've got so far, its only showing a row if there's an entry in both the Day and Qos tables, which is not what I want, and I've no idea how to expand it to include the various groupings described above.
SELECT
`Employee`.`name` ,
`Team`.`name` ,
`Department`.`name`,
`Day`.`Calls`,
`Day`.`Tickets`,
`Qos`.`NumQos`,
`Qos`.`Score`
FROM `faults_employees` AS `Employee`
JOIN
`faults_teams` AS `Team` ON `Employee`.`team_id` = `Team`.`id`
JOIN
`faults_departments` AS `Department` ON `Team`.`department_id` = `Department`.`id`
JOIN
(SELECT
`Day`.`employee_id` AS `eid`,
SUM(`Day`.`actioned_calls_in`) + SUM(`Day`.`actioned_calls_out`) AS `Calls`,
SUM(`Day`.`actioned_tickets`) AS `Tickets`
FROM `faults_days` AS `Day`
WHERE
`Day`.`day_date` = '2011-03-02'
GROUP BY `Day`.`employee_id`
) AS `Day`
ON `Day`.`eid` = `Employee`.`id`
JOIN
(SELECT
`Qos`.`employee_id` AS qid,
COUNT(`Qos`.`id`) AS `NumQos`,
AVG(`Qos`.`score`) AS `Score`
FROM `faults_qos` AS `Qos`
WHERE
`Qos`.`qos_date` = '2011-03-02'
GROUP BY `Qos`.`employee_id`
) AS `Qos`
ON `Qos`.`qid` = `Employee`.`id`
GROUP BY `Employee`.`id`
You do want the left joins on the fault_qos and fault_days subqueries. That's what will give you a result even if there isn't a corresponding row in one or both. A left join says that the value is necessary in the table(s) to the left of the join that are involved in the join but not the one on the right. I haven't tested this, and it's late, so I might not be thinking clearly, but if you change your query to this it should work:
SELECT
`Employee`.`name` ,
`Team`.`name` ,
`Department`.`name`,
`Day`.`Calls`,
`Day`.`Tickets`,
`Qos`.`NumQos`,
`Qos`.`Score`
FROM `faults_employees` AS `Employee`
JOIN
`faults_teams` AS `Team` ON `Employee`.`team_id` = `Team`.`id`
JOIN
`faults_departments` AS `Department` ON `Team`.`department_id` = `Department`.`id`
LEFT JOIN
(SELECT
`Day`.`employee_id` AS `eid`,
SUM(`Day`.`actioned_calls_in`) + SUM(`Day`.`actioned_calls_out`) AS `Calls`,
SUM(`Day`.`actioned_tickets`) AS `Tickets`
FROM `faults_days` AS `Day`
WHERE
`Day`.`day_date` = '2011-03-02'
GROUP BY `Day`.`employee_id`
) AS `Day`
ON `Day`.`eid` = `Employee`.`id`
LEFT JOIN
(SELECT
`Qos`.`employee_id` AS qid,
COUNT(`Qos`.`id`) AS `NumQos`,
AVG(`Qos`.`score`) AS `Score`
FROM `faults_qos` AS `Qos`
WHERE
`Qos`.`qos_date` = '2011-03-02'
GROUP BY `Qos`.`employee_id`
) AS `Qos`
ON `Qos`.`qid` = `Employee`.`id`
GROUP BY `Employee`.`id`