Complex SQL query issue (MYSQL Workbench 6.3) - mysql

I am having trouble figuring out how to write this query.
Let me explain the situation.
So, the question,
I need to display all the player names who have scored a score greater than 99, who have played matches in all the same grounds where a certain player (e.g. pid = 1) has played and has scored a score greater than 99.
(They could have played in other grounds besides the one pid = 1 has played, but the minimum requirement being they must have played in all the same grounds as him).
I have a database, which consist of 3 tables; player, ground, matches. And following data.
create database test1;
use test1;
CREATE TABLE `player` (
`pid` int(11) NOT NULL,
`pname` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `ground` (
`gid` int(11) NOT NULL,
`gname` varchar(20) DEFAULT NULL,
`country` varchar(10) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `matches` (
`pid` int(11) DEFAULT NULL,
`gid` int(11) DEFAULT NULL,
`score` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `player` ADD PRIMARY KEY (`pid`);
ALTER TABLE `ground` ADD PRIMARY KEY (`gid`);
ALTER TABLE `matches`
ADD KEY `gid` (`gid`),
ADD KEY `pid` (`pid`);
INSERT INTO `player` (`pid`, `pname`) VALUES
(1, 'afridi'),
(2, 'kohli'),
(3, 'imam'),
(4, 'fawad'),
(5, 'baven'),
(6, 'awais');
INSERT INTO `ground` (`gid`, `gname`, `country`) VALUES
(1, 'Qaddafi', 'PK'),
(2, 'National', 'PK'),
(3, 'Eden Garden', 'IND'),
(4, 'Lords', 'ENG'),
(5, 'MCG', 'AUS'),
(6, 'Arbab Nayyaz', 'PK');
INSERT INTO `matches` (`pid`, `gid`, `score`) VALUES
(1, 2, 23),
(1, 1, 111),
(2, 3, 107),
(2, 5, 103),
(1, 3, 117),
(1, 4, 55),
(1, 5, 101),
(1, 6, 44),
(2, 6, 103),
(2, 4, 103),
(2, 2, 117),
(2, 1, 103),
(4, 1, 77),
(3, 1, 13),
(5, 2, 22),
(3, 2, 101),
(3, 3, 101),
(5, 1, 101),
(5, 4, 101),
(5, 5, 101),
(6, 1, 101),
(6, 2, 101),
(6, 3, 101),
(6, 4, 101),
(6, 5, 101),
(6, 4, 101);
Relatively a simple database.
I've written the following query which displays the names of 4 players. It is displaying all the players who have played in the same grounds as pid = 1. How to display only those players which have played in all the same grounds as pid = 1.
select p.pname
from player p
join matches mn on mn.pid = p.pid
where (p.pid != 1) and (mn.score > 99) and exists (select m.gid from matches m where (m.pid = 1) and (mn.gid = m.gid))
group by pname;
According to the data provided in the tables,
Afridi (pid = 1) has scored century in the following grounds; 1, 3, and 5.
Respectively, players (pid) 2, 3, 5 ,6 have scored century in grounds = 1, 3, and 5.
These players have made centuries in other grounds as well but this query displays all players who have played in any of the 3 grounds.
The players could've played in other grounds as well, but the minimum requirement being that the players have to play in all the grounds; 1, 3, 5.
So, what I need is, only all those players, which have played in all of the same grounds, as in grounds; 1, 3, 5.
From observing the data in table matches we can see the players that have played in all the same grounds are only 2, being pid = 2, 6.
Any idea how to go about this?

I think this query should do what you want. It creates a table of grounds where the first player has played and made a century (g1), and joins that to the players who have also played at those grounds. If the number of different grounds that the other player has played at is the same as the number of different grounds that the first player has played at, they must have both played at the same set of grounds. Note there are a couple of places (in both subqueries) where you need to set the player id for comparison.
SELECT p.pname
FROM (SELECT gid, pid FROM matches WHERE pid=1 AND score >= 100) g1
LEFT JOIN matches m
ON m.gid = g1.gid AND m.pid != g1.pid
JOIN player p
ON p.pid = m.pid
GROUP BY m.pid
HAVING COUNT(DISTINCT m.gid) = (SELECT COUNT(DISTINCT gid) FROM matches WHERE pid=1 AND score >= 100)
ORDER BY m.pid
SQLFiddle Demo

Related

How do i get a limited number of rows for each value in a field? (db is mysql but could be changed if easier)

I found this difficult to search for this question. I have a table of sports fixtures (tbl_fixture) and a table of sports participants (tbl_participant) which have a many-to-many relationship via a linking table (tbl_fixture_participant)
I need to return the most recent 3 fixtures (ie latest tbl_fixture.start_datetime) of multiple participants and whether they won each of the fixtures, (eg more recent 3 fixtures of participant 1 and most recent 3 fixtures of participant 2, and most recent 3 fixtures of participant 3, with each record returning the fixture_id, participant_id, start_datetime and is_winner fields).
The number of participants that i need to get the data for could be between 1 and 100.
If there's a better way to structure my data, or a better database for this type of query (graph db?) then i'm happy to look into those.
Here's a sample schema:
CREATE TABLE tbl_fixture (
fixture_id INT AUTO_INCREMENT PRIMARY KEY,
start_datetime DATETIME NOT NULL
);
CREATE TABLE tbl_participant (
participant_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE tbl_fixture_participant (
fixture_id INT NOT NULL,
participant_id INT NOT NULL,
is_winner TINYINT NOT NULL,
FOREIGN KEY (fixture_id)
REFERENCES tbl_fixture (fixture_id)
ON UPDATE RESTRICT ON DELETE CASCADE,
FOREIGN KEY (participant_id)
REFERENCES tbl_participant (participant_id)
ON UPDATE RESTRICT ON DELETE CASCADE
);
INSERT INTO tbl_fixture (fixture_id, start_datetime)
VALUES (1, "2021-01-14 15:00:00"),
(2, "2021-01-13 16:00:00"),
(3, "2021-01-12 17:00:00"),
(4, "2021-01-11 15:00:00"),
(5, "2021-01-19 16:00:00"),
(6, "2021-01-18 17:00:00"),
(7, "2021-01-05 15:00:00"),
(8, "2021-01-03 16:00:00"),
(9, "2021-01-03 17:00:00"),
(10, "2021-01-11 15:00:00"),
(11, "2021-01-12 16:00:00"),
(12, "2021-01-13 17:00:00"),
(13, "2021-01-14 15:00:00"),
(14, "2021-01-19 16:00:00");
INSERT INTO tbl_participant (participant_id, name) VALUES
( 1,"Team 1"),
( 2,"Team 2"),
( 3,"Team 3");
INSERT INTO tbl_fixture_participant (fixture_id, participant_id, is_winner)
VALUES (1, 1, 0)
,(2, 1, 1)
,(2, 2, 0)
,(3, 1, 1)
,(12, 2, 0)
,(4, 3, 1)
,(4, 2, 0)
,(6, 3, 1)
,(1, 2, 1)
,(10, 1, 1)
,(5, 2, 0)
,(6, 1, 0)
,(11, 1, 1)
,(14, 1, 0)
,(7, 2, 0)
,(7, 3, 1)
,(3, 3, 0)
,(8, 1, 0)
,(5, 3, 1)
,(13, 2, 0)
,(8, 3, 1)
,(13, 3, 1)
,(9, 1, 0)
,(9, 2, 1)
,(10, 2, 0)
,(11, 3, 0)
,(12, 3, 1)
,(14, 3, 1);
And SQL Fiddle of same.
I would like the data to come back like:
fixture_id
start_datetime
participant_id
is_winner
14
2021-01-19T16:00:00Z
1
0
6
2021-01-18T17:00:00Z
1
0
1
2021-01-14T15:00:00Z
1
0
5
2021-01-19T16:00:00Z
2
0
13
2021-01-14T15:00:00Z
2
0
1
2021-01-14T15:00:00Z
2
1
EDITED TO REFLECT FACT THAT DATES ARE NOT NECESSARILY SEQUENTIAL...
E.g. (for older versions of MySQL)...
SELECT x.*
, fx.start_datetime
FROM fixture_participant x
JOIN fixture fx
ON fx.fixture_id = x.fixture_id
JOIN fixture_participant y
ON y.participant_id = x.participant_id
JOIN fixture fy
ON fy.fixture_id = y.fixture_id
AND fy.start_datetime > fx.start_datetime
GROUP
BY x.fixture_id
, x.participant_id
, x.is_winner
, fx.start_datetime
HAVING COUNT(x.fixture_id) <=3
ORDER
BY participant_id,fixture_id;
...or something like that.

Ponderate average MYSQL

We have a little simulator of a tour-operator DB (MYSQL) and we are asked to get a Query that gives us the weighted avg of duration of the tours that we have.
https://en.wikipedia.org/wiki/Weighted_arithmetic_mean
Using subquery I got to this point where I have the days that each tour lasts and the weight of each tour from the total of tours, but I am stuck and don't know how to get the weighted avg from here. I know I have to use another select from the result I already got but I would appreciate some help.
SQLfiddle down here:
http://sqlfiddle.com/#!9/53d80/2
Tables and data
CREATE TABLE STAGE
(
ID INT AUTO_INCREMENT NOT NULL,
TOUR INT NOT NULL,
TYPE INT NOT NULL,
CITY INT NOT NULL,
DAYS INT NOT NULL,
PRIMARY KEY (ID)
);
CREATE TABLE TOUR
(
ID INT AUTO_INCREMENT NOT NULL,
DESCRIPTION VARCHAR(255) CHARACTER SET UTF8 COLLATE UTF8_UNICODE_CI
NOT NULL,
STARTED_ON DATE NOT NULL,
TYPE INT NOT NULL,
PRIMARY KEY (ID)
);
INSERT INTO TOUR (DESCRIPTION, STARTED_ON, TYPE) VALUES
('Mediterranian Cruise','2018-01-01',3),
('Trip to Nepal','2017-12-01',1),
('Tour in Nova York','2015-04-24',5),
('A week at the Amazones','2014-09-11',2),
('Visiting the Machu Picchu','2013-02-19',4);
INSERT INTO STAGE (TOUR, TYPE, CITY, DAYS) VALUES
(1, 1, 38254, 1),
(1, 2, 22460, 3),
(1, 2, 47940, 3),
(1, 2, 42600, 4),
(1, 3, 38254, 1),
(2, 1, 13097, 1),
(2, 2, 29785, 5),
(2, 3, 13097, 1),
(3, 1, 788, 2); ,
(3, 2, 48019, 6),
(3, 3, 788, 1),
(4, 1, 38254, 2),
(4, 2, 8703, 3);,
(4, 3, 38254, 4),
(5, 1, 10453, 1),
(5, 2, 32045, 5),
(5, 3, 10453, 2);
Query:
SELECT
AVG(TD.TOUR_DAYS) AS AVERAGE_DAYS,
COUNT(TD.TOUR_ID) AS WEIGHT
FROM
(
SELECT
TOUR.ID AS TOUR_ID,
SUM(DAYS) AS TOUR_DAYS,
COUNT(STAGE.ID) AS STAGE_DAYS
FROM
TOUR
INNER JOIN
STAGE
ON
TOUR.ID = STAGE.TOUR
GROUP BY
TOUR.ID
) AS TD
GROUP BY
TD.TOUR_DAYS
weigthed avg would be:
(1×7+1×8+2×9+1×12) / (1+1+2+1) = 9
Wheighted AVG can be calculated with SUM(value * wheight) / SUM(wheight). In your case:
SELECT SUM(AVERAGE_DAYS * WEIGHT) / SUM(WEIGHT)
FROM (
SELECT
AVG(TD.TOUR_DAYS) AS AVERAGE_DAYS,
COUNT(TD.TOUR_ID) AS WEIGHT
FROM
(
SELECT
TOUR.ID AS TOUR_ID,
SUM(DAYS) AS TOUR_DAYS,
COUNT(STAGE.ID) AS STAGE_DAYS
FROM
TOUR
INNER JOIN
STAGE
ON
TOUR.ID = STAGE.TOUR
GROUP BY
TOUR.ID
) AS TD
GROUP BY
TD.TOUR_DAYS
) sub
http://sqlfiddle.com/#!9/53d80/4
I'm not 100% sure, but it looks like the following query is doing exactly the same:
SELECT AVG(TOUR_DAYS)
FROM (
SELECT TOUR, SUM(DAYS) AS TOUR_DAYS
FROM STAGE
GROUP BY TOUR
) sub;
Or even without any subqueries:
SELECT SUM(DAYS) / COUNT(DISTINCT TOUR)
FROM STAGE;
That would mean, the requirement should be simplified to "Get average number of days per tour".

MySQL sort column by values

I want to sort my database result based on a column value. I thought I needed to do this:
SELECT * FROM products
ORDER BY FIELD(brand_id, 4, 1, 6)
But this doesn't sort my result at all. Or at least in a way I don't understand.
What I want to achieve is that I get all products, ordered by brand_id. First all with brand_id 4, then 1 then 6 and then the rest.
Here is my example on jsfiddle.
If not working (jsfiddle not working fine here), see my code below:
Schema:
CREATE TABLE `products` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`brand_id` int(10) unsigned NOT NULL,
`price` decimal(8,2) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO `products` (`id`, `brand_id`, `price`)
VALUES
(1, 1, 10.32),
(2, 4, 15.32),
(3, 2, 23.32),
(4, 6, 56.32),
(5, 4, 23.32),
(6, 4, 23.32),
(7, 1, 25.32),
(8, 5, 15.32),
(9, 3, 55.32),
(10, 5, 23.32);
The problem is here:
FIELD(brand_id, 4, 1, 6)
will return 0 if brand_id is not present in the list, so brands not present will be at the top. You can use this:
ORDER BY FIELD(brand_id, 6, 1, 4) DESC
with all of the values in reverse order, and then you have to order in DESC order.
Normally, when you use field() for sorting you also have in:
SELECT p.*
FROM products p
WHERE brand_id IN (4, 1, 6)
ORDER BY FIELD(brand_id, 4, 1, 6);
Then it works as advertised.
Instead of reversing values, you can also take a more explicit approach:
SELECT p.*
FROM products p
ORDER BY brand_id IN (4, 1, 6) DESC,
FIELD(brand_id, 4, 1, 6);
This is longer, but the intention is clear.

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
)

multiple WHERE clauses on a joined table

I have a simple application that tracks diners and their favorite flavors and desserts. The records table is just the diner's name and ID, the mid table tracks the desserts and flavors (again by an ID linked to another table of values).
CREATE TABLE IF NOT EXISTS `records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=3 ;
INSERT INTO `records` (`id`, `name`) VALUES
(1, 'Jimmy Jones'),
(2, 'William Henry');
CREATE TABLE IF NOT EXISTS `mid` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`diner` int(11) NOT NULL,
`dessert` int(11) NOT NULL DEFAULT '0',
`flavor` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=11 ;
INSERT INTO `mid` (`id`, `diner`, `dessert`, `flavor`) VALUES
(1, 1, 3, 0),
(2, 1, 2, 0),
(3, 1, 15, 0),
(4, 1, 0, 1),
(5, 2, 3, 0),
(6, 2, 6, 0),
(7, 2, 0, 4),
(8, 1, 34, 0),
(9, 2, 0, 4),
(10, 2, 0, 22);
I'm a little stumped by what should be a simple query-- I want to get all IDs from the records table where certain dessert or flavor requirements are met:
SELECT a.id
FROM records AS a
JOIN mid AS b ON a.id = b.diner
WHERE b.dessert IN (3,2,6)
AND b.flavor IN (4,22)
This query returns no rows, even though there are records that match the where clauses. I am pretty sure I'm missing something obvious with the JOIN but I've tried INNER, OUTER, LEFT and RIGHT with no success.
Can someone put me on the right track and explain what I'm missing?
Thanks
You seem to want diners that have the combinations. Here is one way:
select diner
from records
group by diner
having max(b.dessert = 3) = 1 and
max(b.dessert = 2) = 1 and
max(b.dessert = 6) = 1 and
max(b.flavor = 4) = 1 and
max(b.flavor = 22) = 1
This answers your comment:
select diner
from records
group by diner
having max(case when b.dessert in (2, 3, 6) then 1 esle 0 end) = 1 and
max(case when b.dessert in (4, 22) then 1 else 0 end) = 1
If you are just looking for the records in a that match the conditions, use:
select r.*, d.name
from records r join
diner d
on r.diner = d.id
where b.dessert IN (3,2,6) AND b.flavor IN (4,22)
If this is what you want, the join condition in your query is wrong (a.id should be a.diner).
You SQL statement is fine, but non of your sample records meet your condition, records that would match should look like this
dessert flavor
3 4
3 22
2 4
2 33
6 4
6 22
Non of your input record has any of these combinations
Your WHERE condition does not fit any record in the "mid" table.
There are no records that have dessert in (3, 2, 6) AND flavor in (4, 22), so the query (correctly)returns no result.
You don't have any records that match both where conditions.
( 1, 1, 3, 0) - Matches dessert IN (3,2,6)
( 2, 1, 2, 0) - Matches dessert IN (3,2,6)
( 3, 1, 15, 0)
( 4, 1, 0, 1)
( 5, 2, 3, 0) - Matches dessert IN (3,2,6)
( 6, 2, 6, 0) - Matches dessert IN (3,2,6)
( 7, 2, 0, 4) - Matches flavor IN (4,22)
( 8, 1, 34, 0)
( 9, 2, 0, 4) - Matches flavor IN (4,22)
(10, 2, 0, 22) - Matches flavor IN (4,22)
Perhaps you meant OR?
SELECT a.id
FROM records AS a
JOIN mid AS b ON a.id = b.diner
WHERE b.dessert IN (3,2,6)
OR b.flavor IN (4,22)
Should return 7 results.
Also, your thoughts on JOIN are a red herring. The difference between LEFT and RIGHT is just which table gets precedence when the join clause doesn't match records between them. The difference between INNER and OUTER is just what happens when there isn't a matching record between the two tables. Try this explanative article from coding horror for more details on joins (helpfully pointed out to me in a different SO question, heh).