I'm running MySQL 5.6.
I have two related tables:
CREATE TABLE Cars (
id INT NOT NULL AUTO_INCREMENT,
plate VARCHAR(16) NOT NULL,
flag TINYINT,
PRIMARY KEY(id)
)
and:
CREATE TABLE Rents (
id INT NOT NULL AUTO_INCREMENT,
out_date DATE NOT NULL,
in_date DATE,
car_id INT,
FOREIGN KEY (car_id) REFERENCES Cars(id),
PRIMARY KEY(id)
)
I can have multiple rents for each car (0 to many).
I need to select all vehicles in table Cars (with flag = 1) along with their status i.e. I need to know if each car is currently unavailable (only out_date is filled) or availabe (out_date and in_date filled) of course also vehicles without any rents are to be considered available.
The result set need to include out_date and in_date values [Update 17/07/2022].
I tought to use something like:
SELECT
*,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rent.Car_id WHERE Cars.Flag = 1
but this of course will just return all the rows with positive flag match and a status evaluation (0 unavailable, 1 available):
id | plate | flag | id | out_date | in_date | car_id | status
---------------------------------------------------------------------
'1', 'FA787MX', '1', '1', '2022-07-14', '2022-07-15', '1', '1'
'1', 'FA787MX', '1', '2', '2022-07-16', NULL, '1', '0'
'3', 'AB124DF', '1', '4', '2022-07-13', '2022-07-14', '3', '1'
'4', 'CC666VC', '1', NULL, NULL, NULL, NULL, '1'
'5', 'GG435ED', '1', '5', '2022-07-16', NULL, '5', '0'
While I need to have this (edited 17/07/2022):
'1', 'FA787MX', '1', '2', '2022-07-16', NULL, '1', '0'
'3', 'AB124DF', '1', '4', '2022-07-13', '2022-07-14', '3', '1'
'4', 'CC666VC', '1', NULL, NULL, NULL, NULL, '1'
'5', 'GG435ED', '1', '5', '2022-07-16', NULL, '5', '0'
i.e. only the second row of FA787MX car should be mantained since it's the most recent out_date value (no matter if it's id is higher or lower).
For the sake of completeness: There is no guarantee that rental ids will be kept consistent with their rental history. In other words you cannot be sure that for a given car the rental where in_date = NULL is the correct one but you should compare them by out_date value.
Data sample:
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (1, 'FA787MX', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (2, 'EX431YY', 0);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (3, 'AB124DF', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (4, 'CC666VC', 1);
INSERT INTO `Cars` (`id`, `plate`, `flag`) VALUES (5, 'GG435ED', 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (1, '2022-07-14', '2022-07-15', 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (2, '2022-07-16', NULL, 1);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (3, '2022-07-16', NULL, 2);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (4, '2022-07-13', '2022-07-14', 3);
INSERT INTO `Rents` (`id`, `out_date`, `in_date`, `car_id`) VALUES (5, '2022-07-16', NULL, 5);
One option is to join to find only those rentals that are still outstanding (in_date IS NULL). That will drop the old rentals having in_date not null.
Based on the updated requirements, there are a few ways to do it. One is a simple outer join to find the most recent rental per car to obtain the corresponding in_date as well...
MySQL 5.6 fiddle
SELECT Cars.*
, Rents.out_date
, Rents.in_date
, Rents.id IS NULL OR Rents.in_date IS NOT NULL AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
LEFT JOIN Rents AS r2
ON Rents.out_date < r2.out_date
AND Rents.Car_id = r2.Car_id
WHERE Cars.Flag = 1
AND r2.Car_id IS NULL
ORDER BY Cars.id
;
The result:
id
plate
flag
out_date
in_date
status_final
1
FA787MX
1
2022-07-16
0
3
AB124DF
1
2022-07-13
2022-07-14
1
4
CC666VC
1
1
5
GG435ED
1
2022-07-16
0
Based on the original requirements: Try this (fiddle):
SELECT Cars.*
, Rents.in_date
, CASE WHEN in_date IS NOT NULL OR Rents.id IS NULL THEN 1 ELSE 0 END AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
AND in_date IS NULL
WHERE Cars.Flag = 1
;
and if the results contain only those with in_date IS NULL, this reduces to:
SELECT Cars.*
, out_date
, Rents.in_date
, Rents.id IS NULL AS status_final
FROM Cars
LEFT JOIN Rents
ON Cars.id = Rents.Car_id
AND in_date IS NULL
WHERE Cars.Flag = 1
;
Result:
id
plate
flag
out_date
in_date
status_final
1
FA787MX
1
2022-07-16
0
3
AB124DF
1
1
4
CC666VC
1
1
5
GG435ED
1
2022-07-16
0
If your version of MySql is 8.0+ use ROW_NUMBER() window function to pick the latest row for each car in Rents:
SELECT c.*, r.*,
r.out_date IS NULL OR r.in_date IS NOT NULL status
FROM Cars c
LEFT JOIN (
SELECT *, ROW_NUMBER() OVER (PARTITION BY car_id ORDER BY out_date DESC) rn
FROM Rents
) r ON r.car_id = c.id AND r.rn = 1
WHERE c.flag = 1;
For previous versions use NOT EXISTS:
SELECT c.*, r.*,
r.out_date IS NULL OR r.in_date IS NOT NULL status
FROM Cars c
LEFT JOIN (
SELECT r1.*
FROM Rents r1
WHERE NOT EXISTS (
SELECT *
FROM Rents r2
WHERE r2.car_id = r1.car_id AND r2.out_date > r1.out_date
)
) r ON r.car_id = c.id
WHERE c.flag = 1;
See the demo.
If you imagine the result of your query as a table, you can easily write a query that would give you what you need (the subquery is just yours with the select spelled out to give a unique column name to the second id column, as it seemed useful - the only way to uniquely identify a row):
SELECT MAX(rent_id) FROM (
SELECT
Cars.id as id,
plate,
flag,
Rents.id as rent_id,
out_date,
in_date,
car_id,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rents.car_id WHERE Cars.Flag = 1
) as rental_status
WHERE status = 0
GROUP BY car_id;
Which tells you which rows are interesting:
+--------------+
| MAX(rent_id) |
+--------------+
| 2 |
| 5 |
+--------------+
Now you can use a join to return the results of your initial query only for the interesting rows. To avoid having to spell out that query all over again, MySQL 8 has a way to stash the results of your core query and use it like a table:
WITH
status_data AS (
SELECT
Cars.id as id,
plate,
flag,
Rents.id as rent_id,
out_date,
in_date,
car_id,
IF(Rents.in_date IS NOT NULL AND Rents.out_date IS NOT NULL, 1, IF(Rents.id IS NULL, 1, 0)) AS status
FROM Cars
LEFT JOIN Rents ON Cars.id = Rents.car_id WHERE Cars.Flag = 1
)
SELECT * from status_data
JOIN (
SELECT MAX(rent_id) as rent_id FROM status_data
WHERE status = 0
GROUP BY car_id
) as ids using(rent_id);
Giving the result:
+---------+----+---------+------+------------+---------+--------+--------+
| rent_id | id | plate | flag | out_date | in_date | car_id | status |
+---------+----+---------+------+------------+---------+--------+--------+
| 2 | 1 | FA787MX | 1 | 2022-07-16 | NULL | 1 | 0 |
| 5 | 5 | GG435ED | 1 | 2022-07-16 | NULL | 5 | 0 |
+---------+----+---------+------+------------+---------+--------+--------+
I'm trying to create a list of students whose behaviour is statistically worst across each of our school's year groups.
We have a table named students.
We then have behavioural flags and alerts, plus sanctions.
However, different categories of flag/alert/sanction are deemed more serious than others. These are stored with labels in their respective _categories table, e.g. flag_categories and sanction_categories. The flag table will then have a column called Category_ID (alerts is a bit different as it's just a Type field with 'A', 'C', 'P' and 'S' values).
If I want to look at data which shows our highest-flagged students in a specific year group, I'd run this query:
SELECT
CONCAT(stu.Firstname, " ", stu.Surname) AS `Student`,
COUNT(f.ID) AS `Flags`
FROM `students` stu
LEFT JOIN `flags` f ON f.Student_ID = stu.id
WHERE stu.Year_Group = 9
GROUP BY stu.id
ORDER BY `Flags` DESC
LIMIT 0, 20
If I wanted to show our students with the most Crisis alerts in a specific year group, I'd run this query:
SELECT
CONCAT(stu.Firstname, " ", stu.Surname) AS `Student`,
COUNT(f.ID) AS `Flags`
FROM `students` stu
LEFT JOIN `flags` f ON f.Student_ID = stu.id
WHERE stu.Year_Group = 9
AND f.Category_ID = 10
GROUP BY stu.id
ORDER BY `Flags` DESC
LIMIT 0, 20
If I want to find how many Late or Mobile flags a student has, and perhaps add these together (with weightings), I can run the following query:
SELECT
CONCAT(stu.Firstname, " ", stu.Surname) AS `Student`,
SUM(CASE WHEN f.Category_ID = 10 THEN 1 ELSE 0 END) AS `Late Flags`,
SUM(CASE WHEN f.Category_ID = 12 THEN 2 ELSE 0 END) AS `Mobile Flags`,
## not sure about this line below... is there a nicer way of doing it? `Late Flags` isn't recognised as a field apparently
## so I can't just do ( `Late Flags` + `Mobile Flags` )
(SUM(CASE WHEN f.Category_ID = 10 THEN 1 ELSE 0 END) + SUM(CASE WHEN f.Category_ID = 12 THEN 2 ELSE 0 END)) AS `Points`
FROM `flags` f
LEFT JOIN `students` stu ON f.Student_ID = stu.id
WHERE stu.Year_Group = 9
GROUP BY stu.id
ORDER BY `Points` DESC
LIMIT 0, 20
What I don't understand is how I would do this across myriad tables. I need to be able to weight:
Late (flags, Category_ID = 10), Absconded (flags, Category_ID = 15) and Community flags (flags, Category_ID = 13) plus Safeguarding alerts (alerts, Type = 'S') are all worth 1 point
Behavioural flags (flags, Category_ID IN (1, 7, 8)) are worth 2 points
Process alerts (alerts, Type = 'P') and detention sanctions (sanctions, Category_ID = 1) are worth 3 points
So on and so forth. That's far from an exhaustive list but I've included enough variables to help me get my head round a multi-table weighted sum.
The outcome I'm looking for is just 2 columns - student's name and weighted points.
So, according to the bullet points above, if a student has received 2 Late flags (1 point each) and 1 Process alert (3 points), the output should just say Joe Bloggs and 5.
Can anyone help me to understand how I can get these weighted values from different tables into one SUM'd output for each student?
[edit] SQLFiddle here: http://sqlfiddle.com/#!9/449218/1/0
Note, I am not doing this for the bounty. Please give to someone else.
This could be done with a few LEFT JOINs of derived tables. Note you did not supply the sanctions table. But the below would appear to be well illustrative. So I created a temp table. It would seem to allow for maximum flexibility without overcomplicating a larger left join notion that might be hard to debug. Afterall, you said your real querying will be much more complicated than this. As such, build out the temp table structure more.
This loads a tmp table up with default 0's for the students in the "passed by parameter Student Year" to a stored procedure. Two updates are performed. Then selects for a result set.
Schema / Load:
create schema s38741386; -- create a test database
use s38741386;
CREATE TABLE `students` (
`id` int(11) PRIMARY KEY,
`Firstname` varchar(50) NOT NULL,
`Surname` varchar(50) NOT NULL,
`Year_Group` int(2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
# STUDENT INSERTS
INSERT INTO `students`
(`id`, `Firstname`, `Surname`, `Year_Group`)
VALUES
(201, 'Student', 'A', 9),
(202, 'Student', 'B', 9),
(203, 'Student', 'C', 9),
(204, 'Student', 'D', 9),
(205, 'Student', 'E', 9);
CREATE TABLE `alert` (
`ID` int(11) PRIMARY KEY,
`Staff_ID` int(6) NOT NULL,
`Datetime_Raised` datetime NOT NULL,
`Room_Label` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
`Type` enum('A','C','P','Q','S') COLLATE utf8_unicode_ci NOT NULL COMMENT 'A=Absconded, C=Crisis, P=Process, Q=Quiet, S=Safeguarding',
`Details` text COLLATE utf8_unicode_ci,
`Responder` int(8) DEFAULT NULL,
`Datetime_Responded` datetime DEFAULT NULL,
`Room_ID` int(11) NOT NULL COMMENT 'will be linked to internal room id.',
`Status` varchar(1) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'O:ngoing, R:esolved'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
# ALERT INSERTS
INSERT INTO `alert`
(`ID`, `Staff_ID`, `Datetime_Raised`, `Room_Label`, `Type`, `Details`, `Responder`, `Datetime_Responded`, `Room_ID`, `Status`)
VALUES
(1, '101', '2016-08-04 00:00:00', NULL, 'P', NULL, '103', '2016-08-04 00:00:01', '15', 'R'),
(2, '102', '2016-08-04 00:00:00', NULL, 'P', NULL, '103', '2016-08-04 00:00:01', '15', 'R'),
(3, '102', '2016-08-04 00:00:00', NULL, 'P', NULL, '103', '2016-08-04 00:00:01', '15', 'R'),
(4, '101', '2016-08-04 00:00:00', NULL, 'P', NULL, '103', '2016-08-04 00:00:01', '15', 'R');
CREATE TABLE `alert_students` (
`ID` int(11) PRIMARY KEY,
`Alert_ID` int(6) NOT NULL,
`Student_ID` int(6) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
# ALERT_STUDENT INSERTS
INSERT INTO `alert_students`
(`ID`, `Alert_ID`, `Student_ID`)
VALUES
(1, '1', '201'),
(2, '1', '202'),
(3, '2', '201'),
(4, '3', '202'),
(5, '4', '203'),
(6, '5', '204');
CREATE TABLE `flags` (
`ID` int(11) PRIMARY KEY,
`Staff_ID` int(11) NOT NULL,
`Student_ID` int(11) NOT NULL,
`Datetime` datetime NOT NULL,
`Category_ID` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
# ALERT INSERTS
-- TRUNCATE TABLE flags;
INSERT INTO `flags`
(`ID`, `Staff_ID`, `Student_ID`, `Datetime`, `Category_ID`)
VALUES
(1, '101', '201', '2016-08-04 00:00:01', 10),
(2, '102', '202', '2016-08-04 00:00:02', 12),
(3, '102', '203', '2016-08-04 00:00:03', 10),
(4, '101', '204', '2016-08-04 00:00:04', 13),
(5, '102', '202', '2016-08-04 00:00:02', 12),
(6, '102', '203', '2016-08-04 00:00:03', 10),
(7, '101', '204', '2016-08-04 00:00:04', 13),
(8, '102', '202', '2016-08-04 00:00:02', 10),
(9, '102', '203', '2016-08-04 00:00:03', 10),
(10, '101', '204', '2016-08-04 00:00:04', 7),
(11, '101', '204', '2016-08-04 00:00:07', 8),
(12, '101', '204', '2016-08-04 00:00:08', 1),
(13, '101', '204', '2016-08-04 00:00:09', 8);
Stored Procedure:
DROP PROCEDURE IF EXISTS rptSM_by_year;
DELIMITER $$
CREATE PROCEDURE rptSM_by_year
( pSY INT -- parameter student year
)
BEGIN
DROP TEMPORARY TABLE IF EXISTS tmpStudentMetrics;
CREATE TEMPORARY TABLE tmpStudentMetrics
( `StudentId` int(11) PRIMARY KEY,
LateFP INT NOT NULL,
MobiFP INT NOT NULL,
AbscFP INT NOT NULL,
CommFP INT NOT NULL,
SafeAP INT NOT NULL,
BehaFP INT NOT NULL,
ProcAP INT NOT NULL
)ENGINE=InnoDB;
INSERT tmpStudentMetrics (StudentId,LateFP,MobiFP,AbscFP,CommFP,SafeAP,BehaFP,ProcAP)
SELECT id,0,0,0,0,0,0,0
FROM students
WHERE Year_Group = pSY;
UPDATE tmpStudentMetrics tmp
JOIN
( SELECT
stu.id,
SUM(CASE WHEN f.Category_ID = 10 THEN 1 ELSE 0 END) AS `LateFP`,
SUM(CASE WHEN f.Category_ID = 15 THEN 1 ELSE 0 END) AS `AbscFP`,
SUM(CASE WHEN f.Category_ID = 13 THEN 1 ELSE 0 END) AS `CommFP`,
SUM(CASE WHEN f.Category_ID = 12 THEN 2 ELSE 0 END) AS `MobiFP`,
SUM(CASE WHEN f.Category_ID IN (1,7,8) THEN 2 ELSE 0 END) AS `BehaFP`
FROM `flags` f
LEFT JOIN `students` stu ON f.Student_ID = stu.id
WHERE stu.Year_Group = pSY
GROUP BY stu.id
) xDerived
ON xDerived.id=tmp.StudentId
SET tmp.LateFP=xDerived.LateFP,
tmp.AbscFP=xDerived.AbscFP,
tmp.CommFP=xDerived.CommFP,
tmp.MobiFP=xDerived.MobiFP,
tmp.BehaFP=xDerived.BehaFP;
UPDATE tmpStudentMetrics tmp
JOIN
( SELECT
stu.id,
SUM(CASE WHEN a.Type = 'S' THEN 1 ELSE 0 END) AS `SafeAP`,
SUM(CASE WHEN a.Type = 'P' THEN 3 ELSE 0 END) AS `ProcAP`
FROM `alert_students` als
JOIN `alert` a
ON a.ID=als.Alert_ID
JOIN `students` stu
ON stu.id=als.Student_ID and stu.Year_Group = pSY
GROUP BY stu.id
) xDerived
ON xDerived.id=tmp.StudentId
SET tmp.SafeAP=xDerived.SafeAP,
tmp.ProcAP=xDerived.ProcAP;
-- SELECT * FROM tmpStudentMetrics; -- check detail
SELECT stu.id,
CONCAT(stu.Firstname, " ", stu.Surname) AS `Student`,
tmp.LateFP+tmp.MobiFP+tmp.AbscFP+tmp.CommFP+tmp.SafeAP+tmp.BehaFP+tmp.ProcAP AS `Points`
FROM `students` stu
JOIN tmpStudentMetrics tmp
ON tmp.StudentId=stu.id
WHERE stu.`Year_Group` = pSY
ORDER BY stu.id;
-- SELECT * FROM tmpStudentMetrics; -- check detail
DROP TEMPORARY TABLE IF EXISTS tmpStudentMetrics;
-- TEMP TABLES are connection based. Explicityly dropped above for safety when done.
-- Depends on your connection type and life-span otherwise.
END$$
DELIMITER ;
Test:
call rptSM_by_year(9);
+-----+-----------+--------+
| id | Student | Points |
+-----+-----------+--------+
| 201 | Student A | 7 |
| 202 | Student B | 11 |
| 203 | Student C | 6 |
| 204 | Student D | 10 |
| 205 | Student E | 0 |
+-----+-----------+--------+
Cleanup:
drop schema s38741386; -- drop the test database
Think all you have asked can be done with a subquery and a couple of sub-SELECTs:
SELECT `Student`,
`Late Flags` * 1
+ `Absconded Flags` * 1
+ `Community Flags` * 1
+ `Safeguarding Alerts Flags` * 1
+ `Behavioural flags` * 2
+ `Process Alerts Flags` * 3 AS `Total Points`
FROM
(
SELECT
CONCAT(stu.Firstname, " ", stu.Surname) AS `Student`,
SUM(CASE WHEN f.Category_ID = 10 THEN 1 ELSE 0 END) AS `Late Flags`,
SUM(CASE WHEN f.Category_ID = 12 THEN 1 ELSE 0 END) AS `Mobile Flags`,
SUM(CASE WHEN f.Category_ID = 15 THEN 1 ELSE 0 END) AS `Absconded Flags`,
SUM(CASE WHEN f.Category_ID = 13 THEN 1 ELSE 0 END) AS `Community Flags`,
(SELECT COUNT(*) FROM `alert` a JOIN `alert_students` ast ON ast.`Alert_ID` = a.`ID`
WHERE ast.`Student_ID` = stu.`id` AND a.`Type` = 'S') AS `Safeguarding Alerts Flags`,
SUM(CASE WHEN f.Category_ID IN (1, 7, 8) THEN 1 ELSE 0 END) AS `Behavioural flags`,
(SELECT COUNT(*) FROM `alert` a JOIN `alert_students` ast ON ast.`Alert_ID` = a.`ID`
WHERE ast.`Student_ID` = stu.`id` AND a.`Type` = 'P') AS `Process Alerts Flags`
FROM `students` stu
LEFT JOIN `flags` f ON f.Student_ID = stu.id
WHERE stu.Year_Group = 9
GROUP BY stu.id
LIMIT 0, 20
) subq
ORDER BY `Total Points` DESC;
The above query includes everything you mentioned apart from sanctions (as your original SQL Fiddle demo didn't include this table).
Demo
An updated fiddle with the above query is here: http://sqlfiddle.com/#!9/449218/39.
You could use union all
Basically you create all your individual queries for each table and connect them all together using union all.
Here is an example, I used your student table twice but you would change the second one to what ever other table you want. SQLFiddle
You can do it with LEFT JOINS:
SELECT CONCAT(stu.firstname,' ', stu.surname) student,
COALESCE(f_group.weight_sum,0) + COALESCE(a_group.weight_sum,0) + COALESCE(s_group.weight_sum,0) points
FROM students stu
LEFT JOIN (
SELECT s_f.id, SUM(f.category_id IN (10,13,15) + 2 * f.category_id IN (1,7,8)) weight_sum
FROM students s_f
JOIN flags f
ON f.student_id = s_f.id
AND f.category_id IN (1,7,8,10,13,15)
WHERE s_f.year_group = :year_group
GROUP BY s_f.id
) f_group
LEFT JOIN (
SELECT s_a.id, 3 * COUNT(*) weight_sum
FROM students s_a
JOIN alerts a
ON a.student_id = s_a.id
AND a.type = 'P'
WHERE s_a.year_group = :year_group
GROUP BY s_a.id
) a_group
LEFT JOIN (
SELECT s_s.id, COUNT(*) weight_sum
FROM students s_s
JOIN sanctions s
ON s.student_id = s_s.id
AND s.category_id = 1
WHERE s_s.year_group = :year_group
GROUP BY s_s.id
) s_group
WHERE stu.year_group = :year_group
ORDER BY points DESC
LIMIT 0, 20
BUT if you have full access to the DB I'd be putting those weights in the respective categories and types, which will simplify the logic.