multiple WHERE clauses on a joined table - mysql

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).

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.

Complex SQL query issue (MYSQL Workbench 6.3)

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

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 to fetch similar "match" results by percentage

This table stores user votes between user matches. There is always one winner, one loser and the voter.
CREATE TABLE `user_versus` (
`id_user_versus` int(11) NOT NULL AUTO_INCREMENT,
`id_user_winner` int(10) unsigned NOT NULL,
`id_user_loser` int(10) unsigned NOT NULL,
`id_user` int(10) unsigned NOT NULL,
`date_versus` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id_user_versus`),
KEY `id_user_winner` (`id_user_winner`,`id_user_loser`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=17 ;
INSERT INTO `user_versus` (`id_user_versus`, `id_user_winner`, `id_user_loser`, `id_user`, `date_versus`) VALUES
(1, 6, 7, 1, '2013-10-25 23:02:57'),
(2, 6, 8, 1, '2013-10-25 23:02:57'),
(3, 6, 9, 1, '2013-10-25 23:03:04'),
(4, 6, 10, 1, '2013-10-25 23:03:04'),
(5, 6, 11, 1, '2013-10-25 23:03:10'),
(6, 6, 12, 1, '2013-10-25 23:03:10'),
(7, 6, 13, 1, '2013-10-25 23:03:18'),
(8, 6, 14, 1, '2013-10-25 23:03:18'),
(9, 7, 6, 2, '2013-10-26 04:02:57'),
(10, 8, 6, 2, '2013-10-26 04:02:57'),
(11, 9, 8, 2, '2013-10-26 04:03:04'),
(12, 9, 10, 2, '2013-10-26 04:03:04'),
(13, 9, 11, 2, '2013-10-26 04:03:10'),
(14, 9, 12, 2, '2013-10-26 04:03:10'),
(15, 9, 13, 2, '2013-10-26 04:03:18'),
(16, 9, 14, 2, '2013-10-26 04:03:18');
I'm working on a query that fetches similar profiles. A profile is similar, when the voting percentage (wins vs loses) is +/- 10% of the specified profile.
SELECT id_user_winner AS id_user,
IFNULL(wins, 0) AS wins,
IFNULL(loses, 0) AS loses,
IFNULL(wins, 0) + IFNULL(loses, 0) AS total,
IFNULL(wins, 0) / (IFNULL(wins, 0) + IFNULL(loses, 0)) AS percent
FROM
(
SELECT id_user_winner AS id_user FROM user_versus
UNION
SELECT id_user_loser FROM user_versus
) AS u
LEFT JOIN
(
SELECT id_user_winner, COUNT(*) AS wins
FROM user_versus
GROUP BY id_user_winner
) AS w
ON u.id_user = id_user_winner
LEFT JOIN
(
SELECT id_user_loser, COUNT(*) AS loses
FROM user_versus
GROUP BY id_user_loser
) AS l
ON u.id_user = l.id_user_loser
This is the current result:
It's currently returning NULL rows, and they shouldn't be there. What still needs to get optimized (and can't quite put my finger on it) is:
bring users similar to user ABC only
specify condition that defines who is a similar user to, e.g. user id = 6 (where similar users have +/- 10% difference in percentage with user id 6)
Any help will be appreciated. Thanks!
To calculate wins and losses of each user without having to join the table to itself and use OUTER joins, it is possible to just select wins and losses separately and do a UNION ALL between them, but with additional information if given row represents a win for the user, or a loss.
Then, it's easy to calculate all wins and losses for each user. The tricky part was to incorporate the option for specifying to which user you would like to compare the profiles. I did that with a variable which is set to the value of percentage of the user with given user_id, which you can change from a constant to a variable.
Here is my proposal (comparing to user with id = 6):
SELECT
player_id AS id_user,
wins,
losses,
wins + losses AS total,
wins / (wins + losses) AS percent
FROM (
SELECT
player_id,
SUM(is_a_win) wins,
SUM(is_a_loss) losses,
CASE
WHEN player_id = 6
THEN #the_user_score := SUM(is_a_win) / (SUM(is_a_win) + SUM(is_a_loss))
ELSE NULL
END
FROM (
SELECT id_user_winner AS player_id, 1 AS is_a_win, 0 AS is_a_loss FROM user_versus
UNION ALL SELECT id_user_loser, 0, 1 FROM user_versus
) games
GROUP BY player_id
) data
WHERE
ABS(wins / (wins + losses) - #the_user_score) <= 0.1
;
Output:
ID_USER WINS LOSSES TOTAL PERCENT
6 8 2 10 0.8
9 6 1 7 0.8571
You could of course remove the user whose profile is the base for comparison by adding player_id != 6 (or, in the final solution, some variable name) condition to the outermost WHERE clause.
Example at SQLFiddle: Matching Profiles - Example
Could you provide some feedback if this is what you were looking for, and, if not, what output would you expect?

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
)