MySQL JOIN returning unrelated rows when combined with LEFT JOIN, WHERE and OR - mysql

I have the following table structure. The idea is that users have permissions to a forum either by their class or specific user overrides. ('action' in both cases is an enum with values 'read' & 'write')
user (id, class)
forum (id, name)
forum_permissions (forum_id, class_id, action)
forum_user_permissions (forum_id, user_id, action)
With the following query, I'm getting extra results based on rows in forum_permissions that I don't expect. By this I mean that every row on forum_permissions with forum_id = 3 is returned even though the class_id does not match.
SELECT forum.id AS forum_id, forum.name
FROM forum
JOIN forum_permissions ON forum_permissions.forum_id = forum.id
LEFT JOIN forum_user_permissions ON (
forum_user_permissions.forum_id = forum.id AND forum_user_permissions.user_id = 3 )
WHERE (( forum_permissions.class_id = 1 AND forum_permissions.action = 'read' )
OR
( forum_user_permissions.action = 'read' ))
e.g. I get this:
FORUM_ID NAME
1 chat
2 support
3 secret
3 secret
3 secret
3 secret
but expected this:
FORUM_ID NAME
1 chat
2 support
3 secret
I have made an SQL Fiddle with the specific example including data http://sqlfiddle.com/#!2/75c3a/5/0

Your left join is adding those extra lines. mybe if you change the WHERE
WHERE forum_user_permissions.user_id is not null and (
(forum_permissions.class_id = 1 AND forum_permissions.action = 'read')
OR
(forum_user_permissions.action = 'read')
)
Or
SELECT
forum.id AS forum_id, forum.name
FROM
forum
JOIN
forum_permissions
ON
forum_permissions.forum_id = forum.id
LEFT JOIN
forum_user_permissions
ON (
forum_user_permissions.forum_id = forum.id
)
WHERE forum_user_permissions.user_id = 3 and (
(forum_permissions.class_id = 1 AND forum_permissions.action = 'read')
OR
(forum_user_permissions.action = 'read')
)
But it depends on the results your are trying to get

Okay, I solved it myself by using LEFT JOIN's for both permission tables, rather than put the logic in a WHERE clause. I'm not very sure if this is a better approach than my first attempt and will gladly upvote if someone can explain.
SELECT forum.id AS forum_id, forum.name
FROM forum
LEFT JOIN forum_permissions
ON ( forum_permissions.forum_id = forum.id
AND forum_permissions.class_id = 1
AND forum_permissions.action = 'read' )
LEFT JOIN forum_user_permissions
ON ( forum_user_permissions.forum_id = forum.id
AND forum_user_permissions.user_id = 3
AND forum_user_permissions.action = 'read' )
WHERE forum_permissions.forum_id IS NOT null OR forum_user_permissions.forum_id IS NOT null
The full dataset is included below as I guess the fiddle will expire at some point.
INSERT INTO user
(`id`, `class`)
VALUES
(1, 1), (2, 1), (3, 1), (4, 2);
INSERT INTO forum
(`id`, `name`)
VALUES
(1, 'chat'), (2, 'support'), (3, 'secret');
INSERT INTO forum_permissions
(`forum_id`, `class_id`, `action`)
VALUES
(1, 1, 'read'), (1, 1, 'write'),
(1, 2, 'read'), (1, 2, 'write'),
(2, 1, 'read'), (2, 1, 'write'),
(2, 2, 'read'), (2, 2, 'write'),
(3, 2, 'read'), (3, 2, 'write'),
(3, 3, 'read'), (3, 3, 'write');
INSERT INTO forum_user_permissions
(`forum_id`, `user_id`, `action`)
VALUES
(3, 3, 'read'), (3, 3, 'write');

Related

How to get the value of a different ID that is acquired during the SELECT statement?

I have created an entity tblPerson and from this entity I need to get the bGroup of t.adminID and the bGroup from the d.personID. I have tried the below query but it's not returning anything.
`
SELECT t.adminID, p.firstName, p.lastName, t.transID, t.transDate, t.donationID, p.bGroup, b.bankName, d.personID AS 'Donor ID', 'Donor BGroup'
FROM tblTrans t
JOIN tblAdmin a ON t.adminID = a.adminID
JOIN tblPerson p ON a.personID = p.personID
JOIN tblDonation d ON t.donationID = d.donationID
JOIN tblBank b ON d.bankID = b.bankID
WHERE 'Donor BGroup' IN
(SELECT p.bGroup
FROM tblPerson p
JOIN tblDonation d ON p.personID = d.personID
JOIN tblTrans t ON d.donationID = t.donationID);
`
When I execute the subquery, it gives me the bGroup of the d.personID, what do you think is going on, and maybe any alternatives, please?
Sample Data
INSERT INTO tblPerson (personID, firstName, lastName, bGroup)
VALUES ('1A', 'John', 'Doe', 'XY'),
('2A', 'Joe', 'Bishop', 'AB'),
('1B', 'Elly', 'James', 'OP'),
('2B', 'Andre', 'Butch', 'XY'),
('3A', 'Amy', 'Gree', 'AB'),
('3B', 'Alfred', 'Black', 'OP'),
('4C', 'James', 'Brown', 'XY');
INSERT INTO tblAdmin (adminID, personID, description)
VALUES (1, '1A', 'Whatever.'),
(2, '1B', ''),
(3, '4C', 'Anything.'),
(4, '1A', '');
INSERT INTO tblDonation (donationID, bankID, personID, donationDate)
VALUES (1, 1, '3B', '2018-12-27'),
(2, 1, '2A', '2022-12-28'),
(3, 2, '3A', '2022-03-23'),
(4, 2, '4C', '2022-06-19'),
(5, 3, '1B', '2022-08-19'),
(6, 3, '2B', '2022-08-08'),
(7, 3, '3B', '2022-07-20'),
(8, 2, '4C', '2022-11-26'),
(9, 1, '3B', '2022-11-26'),
(10, 2, '2A', '2022-01-16');
INSERT INTO tblBank (bankID, bankName)
VALUES (1, 'Bank 1'),
(2, 'Bank 2'),
(3, 'Bank 3');
INSERT INTO tblTrans (transID, transDate, donationID, adminID)
VALUES (1, '2022-12-31', 1, 1),
(2, '2022-01-01', 2, 1),
(3, '2022-05-23', 3, 2),
(4, '2022-05-23', 4, 2),
(5, '2022-07-09', 5, 3),
(6, '2022-08-20', 6 4),
(7, '2022-12-27', 7,4);
Sample ERD Diagram
Expected Output
Example: 1, John, Doe, 1, 2022-12-31, 1, XY, Bank 1, 3B, OP.
Your WHERE 'Donor BGroup' IN (SELECT...) clause evaluates to WHERE false, because you don't have any rows with that value in tblPerson.bGroup. So, your SELECT statement's result set is empty.
It's hard to puzzle out your requirement from your question.
I managed to find the solution. Since I needed to call the same table twice with different IDs I had to create two aliases and JOIN the table twice, like so:
SELECT t.adminID, person.firstName, person.lastName, t.transID, t.transDate, t.donationID, person.bGroup, b.bankName, donor.personID AS 'Donor ID', donor.bGroup AS 'Donor BGroup'
FROM tblTrans t
JOIN tblAdmin a ON t.adminID = a.adminID
JOIN tblPerson person ON a.personID = person.personID
JOIN tblDonation d ON t.donationID = d.donationID
JOIN tblPerson donor ON
JOIN tblBank b ON d.bankID = b.bankID;

Returning records which only have one specific many to many relation

Given this structure
CREATE TABLE locations
(`id` int, `Name` varchar(128))
;
INSERT INTO locations
(`id`, `Name`)
VALUES
(1, 'Location 1'),
(2, 'Location 2'),
(3, 'Location 3')
;
CREATE TABLE locations_publications
(`id` int, `publication_id` int, `location_id` int)
;
INSERT INTO locations_publications
(`id`, `publication_id`, `location_id`)
VALUES
(1, 1, 1),
(2, 2, 1),
(3, 2, 2),
(4, 1, 3)
;
I would like to find only Location 2 based on the fact that it has only one relation with a publication_id = 2.
It should not return location one due to the fact that it has two relation rows.
This is sort of what I'm looking for but of course dosnt work because it limits the relationship to where publication_id = 2.
select * from locations
join locations_publications on locations_publications.location_id = locations.id
where locations_publications.publication_id = 2
group by (locations.location_id)
having count(*) = 1
You can do this with aggregation:
select location_id
from locations_publications
group by location_id
having count(*) = 1
If a location might have multiple records with the same publication, change the having criteria to count(distinct publication_id) = 1
Given your edits, you can use conditional aggregation for that:
select location_id
from locations_publications
group by location_id
having count(*) = sum(case when publication_id = 2 then 1 else 0 end)

how does count work in sql query

I'm doing a exercise about SQL query and I came up with an error when it comes to SUM. I know what is does, but I somehow get an error...
select
client.LName, client.FName,
COUNT(b.Total)
from
ClientBank client
INNER JOIN
Bank b ON (client.ClientID = b.ClientID)
where
client.LName = 'name' AND client.FName = 'a';
But then I get this error...
Column 'ClientBank.LName' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
I have tried to do a Group BY, don't really know how to use it well.. Any help would be great!
You have not given the structure of the ClientBank and Bank tables. I am taking that you inserted records something like this:
INSERT INTO `ClientBank`
(`ClientID`, `FName`, `Lname`)
VALUES
(1, 'Abraham', 'Backman'),
(2, 'Carl', 'Dacosta'),
(3, 'Erwin', 'Fabio'),
(4, 'Gabriel', 'Haddon');
INSERT INTO `Bank`
(`DepositID`, `ClientID`, `Deposit`)
VALUES
(1, 1, 100),
(2, 2, 200),
(3, 3, 300),
(4, 4, 400),
(5, 2, 500),
(6, 3, 600);
You can get the total of deposit for each client with this:
SELECT
client.LName, client.FName, SUM(b.Deposit) as Total
FROM
ClientBank client
INNER JOIN
Bank b ON (client.ClientID = b.ClientID)
GROUP By
client.LName, client.FName;
If you want only for a particular client (filtering using their name than Client Id):
SELECT
client.LName, client.FName, SUM(b.Deposit) as Total
FROM
ClientBank client
INNER JOIN
Bank b ON (client.ClientID = b.ClientID)
WHERE
client.LName = 'Fabio' AND client.FName = 'Erwin'
GROUP By
client.LName, client.FName;

Get reply numbers in mysql

I am working on a product review page where it will display several submitted reviews as well as the number of comments to each of them.
I thought I could use
SELECT title AS review_title,COUNT(id_group) AS Approved_reply_number
WHERE approved <> '0'
GROUP BY id_group`
but read somewhere that it isn't possible to copy the id values into another row on the insert process. So if someone submits a review, the id_group field for the reviews has to be left empty.
Here is the table example:
CREATE TABLE product_review
(`ID` int, `title` varchar(21), `id_group` int,`approved` int)
;
INSERT INTO product_review
(`ID`, `title`, `id_group`,`approved`)
VALUES
(1, 'AAA', Null,1),
(2, 'BBB', 1,1),
(3, 'CCC', Null,1),
(4, 'DDD', 3,0),
(5, 'EEE', 1,1),
(6, 'FFF', Null,1),
(7, 'GGG', 6,1),
(8, 'HHH',1,1),
(9, 'III', 6,1)
;
Those that are Null in id_group are the submitted reviews. The rest are replies and they contain the id of their corresponding reviews. I was wondering how can I get an output like this:
review_title approved_reply_number
AAA 3
CCC 0
FFF 2
You can use a self join and count query with group by and also a where clause to filter out reviews only
select t.title review_title ,count(*) approved_reply_number
from product_review t
left join product_review t1 on(t.id = t1.id_group)
where t.id_group is null
group by t.id
Demo

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
)