Aggregation with rollup and empty records also - mysql

Hello guys I have 3 tables.
Here is the creation data:
CREATE TABLE `positionstyp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL,
`bau_nr_komplett` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`),
CONSTRAINT `positionstyp_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `positionstyp` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
CREATE TABLE `projektposition` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`projekt_id` int(11) NOT NULL,
`preis_vertragskosten` float DEFAULT NULL,
`positionstyp_id` int(11),
PRIMARY KEY (`id`),
KEY `positionstyp_id` (`positionstyp_id`),
CONSTRAINT `fk_projektposition_positionstypID` FOREIGN KEY (`positionstyp_id`) REFERENCES `positionstyp` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB;
CREATE TABLE `menge` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`baufortschritt` int(11) NOT NULL DEFAULT 0,
`menge` float NOT NULL,
`projektposition_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `projektposition_id` (`projektposition_id`),
CONSTRAINT `menge_ibfk_10` FOREIGN KEY (`projektposition_id`) REFERENCES `projektposition` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB;
INSERT INTO `positionstyp` (`id`, `parent_id`, `bau_nr_komplett`) VALUES
(1, NULL, '1'),
(2, NULL, '2'),
(3, NULL, '3'),
(4, NULL, '4'),
(5, NULL, '5'),
(6, NULL, '6'),
(7, 3, '325'),
(8, 7, '325.0'),
(9, 7, '325.1'),
(10, 8, '325.0.0');
INSERT INTO `projektposition` (`id`, `projekt_id`, `preis_vertragskosten`, `positionstyp_id`) VALUES
(1, 1325, 100, 3),
(2, 1325, 300, 7),
(3, 1325, 150, 7),
(4, 1325, 200, 10),
(5, 1325, 50, 9);
INSERT INTO `menge` (`id`, `menge`, `baufortschritt`, `projektposition_id`) VALUES
(1, 10, 1, 1),
(2, 20, 20, 2),
(3, 30, 30, 3),
(4, 40, 40, 4),
(5, 50, 100, 5),
(6, 60, 87, 1),
(7, 70, 90, 2),
(8, 80, 10, 3),
(9, 90, 50, 4),
(10, 100, 0, 5),
(11, 1000, 100, 5),
(12, 100, 10, 6);
A "menge" is bound to one "projektposition" and a "projektposition" is bound to one "positionstyp". Then a "positionstyp" may have one parent "positionstyp".
In the "menge" there is a field "menge" and a field "baufortschritt" that weightens the "menge". Then the field "preis_vertragskosten" give this combination a price.
So the price for a menge is (menge.menge * menge.baufortschritt * projektposition.preis_vertragskosten)
In my SELECT I want all "positionstyp"-entries with summarized prices and also the "positionstyp"-entries that have no "menge"/"projektposition" under it and so have no price and only for a specific "projekt_id"
My first try was something with group by and rollup but it failed with all "positionstyp" without prices.
Now I have this code that comes near what I want:
SELECT
pt.id,
pt.parent_id,
SUM(IF(m.menge IS NULL,0, m.menge * p.preis_vertragskosten * m.baufortschritt/100 * (p.projekt_id=1325))) as summe,
pt.bau_nr_komplett
FROM positionstyp pt
LEFT JOIN projektposition p ON (p.positionstyp_id=pt.id)
LEFT JOIN menge m ON (m.projektposition_id=p.id)
GROUP BY pt.id
The output summarizes the menge correctly but of course doesn't summarize up the recursive positionstyp_parents. The recursivity of the positionstyp-table can easier be found in the bau_nr_komplett field: A positionstyp is a children of all positionstyp, thats bau_nr_komplett are the beginning of the a child-positionstypsbau_nr_komplett` (don't have to be the direct child though).
Does some have an idea?
Thanks and best regards,
Tobias
The result now is:
bau_nr_komplett summe
1 0
2 0
3 5230
4 0
5 0
6 0
325 22650
325.0 0
325.1 52500
325.0.0 12200
The desired result is:
bau_nr_komplett summe
1 0
2 0
3 5230+22650+52500+12200
4 0
5 0
6 0
325 22650+52500+12200
325.0 12200
325.1 52500
325.0.0 12200
-- edit 2:
I found a bad solution. And bad means it works with the test datasets. But with the real database and 100.000s of datasets it need half an hour ;D
SELECT pt.bau_nr_komplett, (
SELECT
SUM(IF(m1.menge IS NULL,0,m1.menge*p1.preis_vertragskosten*m1.baufortschritt/100*(p1.projekt_id=1325)))
FROM positionstyp pt1
LEFT JOIN projektposition p1 ON ( p1.positionstyp_id = pt1.id )
LEFT JOIN menge m1 ON ( m1.projektposition_id = p1.id )
WHERE pt1.bau_nr_komplett LIKE CONCAT( pt.bau_nr_komplett, "%" )
) as summe
FROM positionstyp pt
LEFT JOIN projektposition p ON ( p.positionstyp_id = pt.id )
LEFT JOIN menge m ON ( m.projektposition_id = p.id )
GROUP BY pt.id

I found the answer for myself and added some detailed information also:
CREATE PROCEDURE `getProjektsummen`(IN projektID int)
BEGIN
SELECT tttt.id, tttt.parent_id, tttt.bau_nr_komplett, tttt.summe_kostenanschlag, tttt.summe_kostenanschlag_baufortschritt,
tttt.summe_vertragskosten, tttt.summe_vertragskosten_baufortschritt,
tttt.summe_gemittelteangebotskosten, tttt.summe_gemittelteangebotskosten_baufortschritt
FROM (
SELECT id, parent_id,bau_nr_komplett, summe_kostenanschlag, summe_kostenanschlag_baufortschritt,
summe_vertragskosten, summe_vertragskosten_baufortschritt,
summe_gemittelteangebotskosten, summe_gemittelteangebotskosten_baufortschritt
FROM
(SELECT tt.id, tt.parent_id, tt.bau_nr_komplett,
MAX(tt.summe_kostenanschlag) as summe_kostenanschlag,MAX(tt.summe_kostenanschlag_baufortschritt) as summe_kostenanschlag_baufortschritt,
MAX(tt.summe_vertragskosten) as summe_vertragskosten,MAX(tt.summe_vertragskosten_baufortschritt) as summe_vertragskosten_baufortschritt,
MAX(tt.summe_gemittelteangebotskosten) as summe_gemittelteangebotskosten,MAX(tt.summe_gemittelteangebotskosten_baufortschritt) as summe_gemittelteangebotskosten_baufortschritt
FROM (
SELECT ptt.id, ptt.parent_id, coalesce(t.sub9,t.sub8,t.sub7,t.sub6,t.sub5,t.sub4,t.sub3,t.sub2,t.sub1) as bau_nr_komplett,
ROUND(IFNULL(t.summe_kostenanschlag,0),0) as summe_kostenanschlag,
ROUND(IFNULL(t.summe_kostenanschlag_baufortschritt,0),0) as summe_kostenanschlag_baufortschritt,
ROUND(IFNULL(t.summe_vertragskosten,0),0) as summe_vertragskosten,
ROUND(IFNULL(t.summe_vertragskosten_baufortschritt,0),0) as summe_vertragskosten_baufortschritt,
ROUND(IFNULL(t.summe_gemittelteangebotskosten,0),0) as summe_gemittelteangebotskosten,
ROUND(IFNULL(t.summe_gemittelteangebotskosten_baufortschritt,0),0) as summe_gemittelteangebotskosten_baufortschritt
FROM
(
SELECT DISTINCT
sum(menge * preis_kostenanschlag *(p.projekt_id=projektID)) as summe_kostenanschlag,
sum(menge * preis_kostenanschlag * baufortschritt/100 *(p.projekt_id=projektID)) as summe_kostenanschlag_baufortschritt,
sum(menge * preis_vertragskosten *(p.projekt_id=projektID)) as summe_vertragskosten,
sum(menge * preis_vertragskosten * baufortschritt/100 *(p.projekt_id=projektID)) as summe_vertragskosten_baufortschritt,
sum(menge * preis_gemittelte_angebotskosten *(p.projekt_id=projektID)) as summe_gemittelteangebotskosten,
sum(menge * preis_gemittelte_angebotskosten * baufortschritt/100 *(p.projekt_id=projektID)) as summe_gemittelteangebotskosten_baufortschritt,
SUBSTRING(pt.bau_nr_komplett,1,1) as sub1,
SUBSTRING(pt.bau_nr_komplett,1,2) as sub2,
SUBSTRING(pt.bau_nr_komplett,1,3) as sub3,
SUBSTRING(pt.bau_nr_komplett,1,4) as sub4,
SUBSTRING(pt.bau_nr_komplett,1,5) as sub5,
SUBSTRING(pt.bau_nr_komplett,1,6) as sub6,
SUBSTRING(pt.bau_nr_komplett,1,7) as sub7,
SUBSTRING(pt.bau_nr_komplett,1,8) as sub8,
SUBSTRING(pt.bau_nr_komplett,1,9) as sub9
FROM menge m
join projektposition p ON (m.projektposition_id=p.id)
join positionstyp pt ON (p.positionstyp_id=pt.id)
GROUP BY SUBSTRING(pt.bau_nr_komplett,1,1),
SUBSTRING(pt.bau_nr_komplett,1,2),
SUBSTRING(pt.bau_nr_komplett,1,3),
SUBSTRING(pt.bau_nr_komplett,1,4),
SUBSTRING(pt.bau_nr_komplett,1,5),
SUBSTRING(pt.bau_nr_komplett,1,6),
SUBSTRING(pt.bau_nr_komplett,1,7),
SUBSTRING(pt.bau_nr_komplett,1,8),
SUBSTRING(pt.bau_nr_komplett,1,9)
WITH ROLLUP
) t join positionstyp ptt on (coalesce(t.sub9,t.sub8,t.sub7,t.sub6,t.sub5,t.sub4,t.sub3,t.sub2,t.sub1) = ptt.bau_nr_komplett)
WHERE
LENGTH(coalesce(t.sub9,t.sub8,t.sub7,t.sub6,t.sub5,t.sub4,t.sub3,t.sub2,t.sub1))<>2 AND
SUBSTRING(reverse(coalesce(t.sub9,t.sub8,t.sub7,t.sub6,t.sub5,t.sub4,t.sub3,t.sub2,t.sub1)),1,1) <> '.' AND
(
(ISNULL(t.sub9) AND NOT ISNULL(t.sub8) AND LENGTH(t.sub8)=8) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND NOT ISNULL(t.sub7) AND LENGTH(t.sub7)=7) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND ISNULL(t.sub7) AND NOT ISNULL(t.sub6) AND LENGTH(t.sub6)=6) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND ISNULL(t.sub7) AND ISNULL(t.sub6) AND NOT ISNULL(t.sub5) AND LENGTH(t.sub5)=5) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND ISNULL(t.sub7) AND ISNULL(t.sub6) AND ISNULL(t.sub5) AND NOT ISNULL(t.sub4) AND LENGTH(t.sub4)=4) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND ISNULL(t.sub7) AND ISNULL(t.sub6) AND ISNULL(t.sub5) AND ISNULL(t.sub4)AND NOT ISNULL(t.sub3) AND LENGTH(t.sub3)=3) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND ISNULL(t.sub7) AND ISNULL(t.sub6) AND ISNULL(t.sub5) AND ISNULL(t.sub4) AND ISNULL(t.sub3)AND NOT ISNULL(t.sub2) AND LENGTH(t.sub2)=2) OR
(ISNULL(t.sub9) AND ISNULL(t.sub8) AND ISNULL(t.sub7) AND ISNULL(t.sub6) AND ISNULL(t.sub5) AND ISNULL(t.sub4) AND ISNULL(t.sub3) AND ISNULL(t.sub2)AND NOT ISNULL(t.sub1) AND LENGTH(t.sub1)=1))
UNION
SELECT pptt.id as id, pptt.parent_id as parent_id, pptt.bau_nr_komplett as bau_nr_komplett,
0 as summe_kostenanschlag,
0 as summe_kostenanschlag_baufortschritt,
0 as summe_vertragskosten,
0 as summe_vertragskosten_baufortschritt,
0 as summe_gemittelteangebotskosten,
0 as summe_gemittelteangebotskosten_baufortschritt
FROM positionstyp pptt
) tt
GROUP BY tt.id) ttt
UNION
SELECT p1.id, null, concat(pt1.bau_nr_komplett," #"),
IFNULL(ROUND(sum(m1.menge * p1.preis_kostenanschlag),0),0) as summe_kostenanschlag,
IFNULL(ROUND(sum(m1.menge * p1.preis_kostenanschlag * m1.baufortschritt/100 ),0),0) as summe_kostenanschlag_baufortschritt,
IFNULL(ROUND(sum(m1.menge * p1.preis_vertragskosten ),0),0) as summe_vertragskosten,
IFNULL(ROUND(sum(m1.menge * p1.preis_vertragskosten * m1.baufortschritt/100 ),0),0) as summe_vertragskosten_baufortschritt,
IFNULL(ROUND(sum(m1.menge * p1.preis_gemittelte_angebotskosten ),0),0) as summe_gemittelteangebotskosten,
IFNULL(ROUND(sum(m1.menge * p1.preis_gemittelte_angebotskosten * m1.baufortschritt/100 ),0),0) as summe_gemittelteangebotskosten_baufortschritt
FROM menge m1 JOIN projektposition p1 ON (m1.projektposition_id=p1.id) JOIN positionstyp pt1 ON (p1.positionstyp_id=pt1.id)
WHERE p1.projekt_id=projektID AND p1.positionstyp_id IS NOT NULL
GROUP BY p1.id
) tttt
ORDER BY tttt.bau_nr_komplett;
END
And I'm very happy that it works with over 1 Million datasets in menge and 10000 in projektposition and 500 in positionstyp
in 2.5 seconds ^^ !!!

Related

MYSQL 5.6 get latest data of each user

My Database table is as shown below. I need to get latest mark of each student. Latest entry is the row with maximum udate and maximum oder. (The oder will be incremented by one on each entry with same date)
In my example, I have two students Mujeeb, Zakariya and two subjects ENGLISH, MATHS. I need to get latest mark of each student for each subject. My expectd result is as follows
My sample data is
DROP TABLE IF EXISTS `students`;
CREATE TABLE IF NOT EXISTS `students` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`udate` date NOT NULL,
`oder` int(11) NOT NULL,
`name` varchar(20) NOT NULL,
`Subject` varchar(20) NOT NULL,
`mark` int(11) NOT NULL,
PRIMARY KEY (`uid`)
) ENGINE=MyISAM AUTO_INCREMENT=13 DEFAULT CHARSET=latin1;
INSERT INTO `students` (`uid`, `udate`, `oder`, `name`, `Subject`, `mark`) VALUES
(1, '2021-08-01', 1, 'Mujeeb', 'ENGLISH', 10),
(2, '2021-08-01', 1, 'Zakariya', 'ENGLISH', 20),
(3, '2021-08-10', 2, 'Mujeeb', 'ENGLISH', 50),
(4, '2021-08-11', 2, 'Zakariya', 'ENGLISH', 60),
(5, '2021-08-02', 1, 'Mujeeb', 'ENGLISH', 100),
(6, '2021-08-03', 1, 'Zakariya', 'ENGLISH', 110),
(7, '2021-08-10', 1, 'Mujeeb', 'ENGLISH', 500),
(8, '2021-08-11', 1, 'Zakariya', 'ENGLISH', 600),
(9, '2021-08-01', 2, 'Mujeeb', 'MATHS', 100),
(10, '2021-08-01', 2, 'Zakariya', 'MATHS', 75),
(11, '2021-08-10', 3, 'Mujeeb', 'MATHS', 50),
(12, '2021-08-11', 3, 'Zakariya', 'MATHS', 60);
Use NOT EXISTS:
SELECT s1.*
FROM students s1
WHERE NOT EXISTS (
SELECT 1
FROM students s2
WHERE s2.name = s1.name AND s2.Subject = s1.Subject
AND (s2.udate > s1.udate OR (s2.udate = s1.udate AND s2.oder > s1.oder))
);
Or with a correlated subquery in the WHERE clause:
SELECT s1.*
FROM students s1
WHERE s1.uid = (
SELECT s2.uid
FROM students s2
WHERE s2.name = s1.name AND s2.Subject = s1.Subject
ORDER BY s2.udate DESC, s2.oder DESC LIMIT 1
);
See the demo.
As ROW_NUMBER() function doesn't work at lower version of MySQL, So alternate way of row_number() is used for this solution.
-- MySQL (v5.6)
SELECT p.uid, p.udate, p.oder, p.name, p.Subject, p.mark
FROM (SELECT #row_no := IF((#prev_val = t.name && #prev_val1 = t.Subject), #row_no + 1, 1) AS row_number
, #prev_val := t.name AS name
, #prev_val1 := t.Subject AS Subject
, t.mark
, t.oder
, t.uid
, t.udate
FROM students t,
(SELECT #row_no := 0) x,
(SELECT #prev_val := '') y,
(SELECT #prev_val1 := '') z
ORDER BY t.name, t.Subject, t.udate DESC, t.oder DESC ) p
WHERE p.row_number = 1
ORDER BY p.name, p.Subject;
Please check the url http://sqlfiddle.com/#!9/b5befe/18

Matching two column values

I have 2 tables. 'user_cities' and 'visits'. I want to check if a user visited a city which he was not supposed to visit.
CREATE TABLE `user_cities` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT,
`name` varchar(255),
`city_id` INT,
PRIMARY KEY (`id`)
);
CREATE TABLE `visits` (
`id` INT NOT NULL AUTO_INCREMENT,
`user_id` INT,
`visit_id` INT,
`city_id` INT,
PRIMARY KEY (`id`)
);
INSERT INTO `user_cities` VALUES
(1, 1, 'John', 35),
(2, 1, 'John', 36),
(3, 1, 'John', 37),
(4, 2, 'Michael', 38),
(5, 2, 'Michael', 39);
INSERT INTO `visits` VALUES
(1, 1, 1, 35),
(2, 1, 2, 36),
(3, 1, 3, 38),
(4, 2, 4, 38),
(5, 2, 5, 39);
http://sqlfiddle.com/#!9/68c658
Example: John must visit only 35, 36 and 37. Michael must visit 38 and 39. These are defined in 'user_cities'
However, John has visited 38 (visits id 3)
How can i query users that visited the wrong city?
If you're not familiar with SQL this kind of problem is a bit trickier than it appears.
You want to select the information from the visits where that visit is not specified by user_cities. The usual way to do this is to use a LEFT JOIN to the user_cities table for the record that validates the visit, and then to find entries where the LEFT JOIN can't find a match.
So your query would be:
SELECT *
FROM visits
LEFT JOIN user_cities
ON(visits.user_id = user_cities.user_id AND
visits.city_id = user_cities.city_id)
WHERE user_cities.id IS NULL
Of course you would probably also extend the query to give you the name of the user (and the coty, but that's not in your example code)
You can try using left join
DEMO
select a.user_id,a.city_id
FROM visits a left join user_cities b on a.user_id=b.user_id and
a.city_id=b.city_id
where b.user_id is null
OUTPUT:
1 John 37
The only way I can see to do this is via a full outer join (which MySQL doesn't even support, so we have to use a workaround):
SELECT COALESCE(user_id_1, user_id_2) AS user_id_matching
FROM
(
SELECT
uc.user_id AS user_id_1,
uc.city_id AS city_id_1,
v.user_id AS user_id_2,
v.city_id AS city_id_2
FROM user_cities uc
LEFT JOIN visits v
ON uc.user_id = v.user_id AND uc.city_id = v.city_id
UNION ALL
SELECT
uc.user_id AS user_id_1,
uc.city_id AS city_id_1,
v.user_id AS user_id_2,
v.city_id AS city_id_2
FROM user_cities uc
RIGHT JOIN visits v
ON uc.user_id = v.user_id AND uc.city_id = v.city_id
WHERE uc.user_id IS NULL
) t
GROUP BY COALESCE(user_id_1, user_id_2)
HAVING
COUNT(CASE WHEN user_id_1 IS NULL THEN 1 END) = 0 AND
COUNT(CASE WHEN user_id_2 IS NULL THEN 1 END) = 0;
Demo

SQL Join multiple rows

I am looking for how to select in a JOIN only the line with the cheapest amount. Because the ON clause responds to multiple lines.
By default, MySQL takes the first line that it finds and I can not act on it.
SELECT g.id
, g.name
, p.description
, x.annually
FROM tblproductgroups g
JOIN tblproducts p
ON p.gid = g.id
AND p.hidden = 0
JOIN tblpricing x
ON x.relid = p.id
WHERE g.hidden = 0
AND g.id in (1,2,3)
AND x.type = 'product'
GROUP
BY g.id
I have to modify the JOIN of such tblpricing, but any comparison operation of the column "annually" gives me an error.
Edit: samples
CREATE TABLE `tblproductgroups` (
`id` int(10) NOT NULL,
`name` text COLLATE utf8_unicode_ci NOT NULL,
`hidden` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `tblproductgroups` (`id`, `name`, `hidden`) VALUES
(1, 'Hébergement WEB', 0),
(2, 'Serveurs virtuels KVM', 0),
(3, 'Serveurs dédiés Pro', 0),
(5, 'Colocation', 0);
CREATE TABLE `tblproducts` (
`id` int(10) NOT NULL,
`gid` int(10) NOT NULL,
`name` text COLLATE utf8_unicode_ci NOT NULL,
`description` text COLLATE utf8_unicode_ci NOT NULL,
`hidden` tinyint(1) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `tblproducts` (`id`, `gid`, `name`, `description`, `hidden`) VALUES
(1, 1, 'Web Basic 2018', 'blablabla', 0),
(2, 1, 'Web Classic 2018', 'blablabla', 0),
(3, 1, 'Web Advanced 2018', 'blablabla', 0),
(5, 2, 'VPS Basic', 'blablabla', 0),
(6, 2, 'VPS Classic', 'blablabla', 0),
(7, 2, 'VPS Advanced', 'blablabla', 0),
(8, 3, 'SD-S 2018', 'blablabla', 0),
(9, 3, 'SD-L 2016', 'blablabla', 1),
(10, 3, 'SD-M 2018', 'blablabla', 0),
(11, 3, 'SD-XL 2018', 'blablabla', 0);
CREATE TABLE `tblpricing` (
`id` int(10) NOT NULL,
`type` enum('product','addon') COLLATE utf8_unicode_ci NOT NULL,
`relid` int(10) NOT NULL,
`annually` decimal(10,2) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `tblpricing` (`id`, `type`, `relid`, `annually`) VALUES
(1, 'product', 1, '30'),
(39, 'product', 2, '20'),
(40, 'product', 3, '10'),
(42, 'product', 5, '100'),
(43, 'product', 6, '50'),
(44, 'product', 7, '25'),
(45, 'product', 8, '2000'),
(46, 'product', 9, '1000'),
(47, 'product', 9, '500'),
(48, 'product', 10, '250');
Result of my query is:
1 Hébergement WEB blablabla 30.00
2 Serveurs virtuels KVM blablabla 100.00
3 Serveurs dédiés Pro blablabla 2000.00
the correct result is:
1 Hébergement WEB blablabla 10.00
2 Serveurs virtuels KVM blablabla 25.00
3 Serveurs dédiés Pro blablabla 250.00
Crudely...
SELECT a.*
FROM
( SELECT g.id
, g.name
, p.description
, x.annually
FROM tblproductgroups g
JOIN tblproducts p
ON p.gid = g.id
AND p.hidden = 0
JOIN tblpricing x
ON x.relid = p.id
WHERE g.hidden = 0
AND g.id in (1,2,3)
AND x.type = 'product'
) a
JOIN
( SELECT id
, MIN(annually) annually
FROM
( SELECT g.id
, g.name
, p.description
, x.annually
FROM tblproductgroups g
JOIN tblproducts p
ON p.gid = g.id
AND p.hidden = 0
JOIN tblpricing x
ON x.relid = p.id
WHERE g.hidden = 0
AND g.id in (1,2,3)
AND x.type = 'product'
) x
GROUP
BY id
) b
ON b.id = a.id
AND b.annually = a.annually
This should do it :
SELECT g.id
, g.name
, p.name
, x.annually
FROM tblproductgroups g
JOIN tblproducts p
ON p.gid = g.id
AND p.hidden = 0
JOIN tblpricing x
ON x.relid = p.id
/* Subtable containing the minimum annual fee per group */
JOIN (SELECT subg.id, MIN(subx.annually) AS annually FROM tblproductgroups subg
INNER JOIN tblproducts subp On subg.id = subp.gid
AND subp.hidden = 0
INNER JOIN tblpricing subx ON subx.relid = subp.id
WHERE subg.hidden = 0
AND subg.id in (1,2,3)
AND subx.type = 'product'
GROUP BY subg.id) m
ON g.id = m.id AND x.annually = m.annually
WHERE g.hidden = 0
AND g.id in (1,2,3)
AND x.type = 'product'
Don't use GROUP BY if you're not actually using any aggregation function in your column definitions. It might work in MySQL but the results will be unpredictable unless you know exactly what you're doing.

Creating a weighted sum of values from different tables

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.

SQL "where IN" query in a many to many relation of 2 tables

I maybe ask a relatively simple question. But I cannot find a solution to this. It's a matter of two tables MANY TO MANY, so there's a third table between them. The schema below:
CREATE TABLE `options` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `options` (`id`, `name`) VALUES
(1, 'something'),
(2, 'thing'),
(3, 'some option'),
(4, 'other thing'),
(5, 'vacuity'),
(6, 'etc');
CREATE TABLE `person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `person` (`id`, `name`) VALUES
(1, 'ROBERT'),
(2, 'BOB'),
(3, 'FRANK'),
(4, 'JOHN'),
(5, 'PAULINE'),
(6, 'VERENA'),
(7, 'MARCEL'),
(8, 'PAULO'),
(9, 'SCHRODINGER');
CREATE TABLE `person_option_link` (
`person_id` int(11) NOT NULL,
`option_id` int(11) NOT NULL,
UNIQUE KEY `person_id` (`person_id`,`option_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `person_option_link` (`person_id`, `option_id`) VALUES
(1, 1),
(2, 1),
(2, 2),
(3, 2),
(3, 3),
(3, 4),
(3, 5),
(4, 1),
(4, 3),
(4, 6),
(5, 3),
(5, 4),
(5, 5),
(6, 1),
(7, 2),
(8, 3),
(9, 4)
(5, 6);
The idea is as follow: I would like to retrieve all people who have a link to option_id=1 AND option_id=3.
The expected result should be one person: John.
But I tried with something like that, which doesn't work because it returns also people who have 1 OR 3:
SELECT *
FROM person p
LEFT JOIN person_option_link l ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
What is the best practice in this case?
//////// POST EDITED: I need to focus on an other important point ////////
And what if we add a new condition with NOT IN? like:
SELECT *
FROM person p
LEFT JOIN person_option_link l ON p.id = l.person_id
WHERE l.option_id IN ( 3, 4 )
AND l.option_id NOT IN ( 6 )
In this case, the result should be FRANK, because PAULINE who has also 3 and 4, have the option 6 and we don't want that.
Thanks!
This is a Relational Division Problem.
SELECT p.id, p.name
FROM person p
INNER JOIN person_option_link l
ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
GROUP BY p.id, p.name
HAVING COUNT(*) = 2
SQLFiddle Demo
if a unique constraint was not enforce on option_id for every id, a DISTINCT keyword is required to filter unique option_ID
SELECT p.id, p.name
FROM person p
INNER JOIN person_option_link l
ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
GROUP BY p.id, p.name
HAVING COUNT(DISTINCT l.option_id) = 2
SQL of Relational Division
Use GROUP BY and COUNT:
SELECT p.id, p.name
FROM person p
LEFT JOIN person_option_link l ON p.id = l.person_id
WHERE l.option_id IN ( 1, 3 )
GROUP BY p.id, p.name
HAVING COUNT(Distinct l.option_id) = 2
I prefer using COUNT DISTINCT in case you could have the same option id multiple times.
Good luck.
It may not be the best option, but you could use a 'double join' to the person_option_link table:
SELECT *
FROM person AS p
JOIN person_option_link AS l1 ON p.id = l1.person_id AND l1.option_id = 1
JOIN person_option_link AS l2 ON p.id = l2.person_id AND l2.option_id = 3
This ensures that there is simultaneously a row with option ID of 1 and another with option ID of 3 for the given user.
The GROUP BY alternatives certainly work; they might well be quicker too (but you'd need to scrutinize query plans to be sure). The GROUP BY alternatives scale better to handle more values: for example, a list of the users with option IDs 2, 3, 5, 7, 11, 13, 17, 19 is fiddly with this variant but the GROUP BY variants work without structural changes to the query. You can also use the GROUP BY variants to select users with at least 4 of the 8 values which is substantially infeasible using this technique.
Using the GROUP BY does require a slight restatement (or rethinking) of the query, though, to:
How can I select people who have 2 of the option IDs in the set {1, 3}?
How can I select people who have 8 of the option IDs in the set {2, 3, 5, 7, 11, 13, 17, 19}?
How can I select people who have at least 4 of the option IDs in the set {2, 3, 5, 7, 11, 13, 17, 19}?
For the "has not these ids" part of the question, simply add a WHERE clause:
WHERE person_id NOT IN
(
SELECT person_id
FROM person_option_link
WHERE option_id = 4
)