Why should I use EXISTS() function in MySQL? - mysql

I have this query:
SELECT * FROM mytable t1
WHERE t1.id = :id AND
EXISTS(SELECT 1 FROM t2 WHERE t2.post_id = :id)
And when I remove that EXISTS() function, still my code works:
SELECT * FROM mytable t1
WHERE t1.id = :id AND
(SELECT 1 FROM t2 WHERE t2.post_id = :id LIMIT 1)
So why should I write that? What's its advantage?

In short:
EXISTS returns when it finds the first result instead of fetching all matching records (so it is more efficient when there are multiple records matching the criteria)
EXISTS is semantically correct.
When there is a column name instead of 1 in the second query, and the column contains NULL, FALSE, 0, etc, MySQL will implicitly convert it to FALSE, which leads to a false result.
EXISTS is actually defined by the ANSI standard, while the second form is not. (The second query may fail in other DBMS)
As an extra side note, you are fine with * too when you are using EXISTS, since it checks if there is a matching record, not the value.

I gonna show the use case of EXISTS. Here: http://sqlfiddle.com/#!9/066db1/1
Note that a book can be co-authored by multiple authors
The original result:
| author | books |
|--------|----------------|
| A 1 | Book 1, Book 2 |
| A 2 | Book 3 |
| A 3 | Book 1, Book 4 |
Filter using Book 1 with WHERE condition:
| author | books |
|--------|--------|
| A 1 | Book 1 |
| A 3 | Book 1 |
Filter using Book 1 with EXISTS:
|author | books |
|--------|----------------|
| A 1 | Book 2, Book 1 |
| A 3 | Book 1, Book 4 |
The query:
SELECT
a.name AS author,
group_concat(b.content SEPARATOR ", ") AS books
FROM
books b
INNER JOIN bookAuthors ba ON
ba.bookID = b.id
INNER JOIN authors a ON
a.id = ba. authorID
WHERE
EXISTS (
SELECT
1
FROM
bookAuthors
WHERE
bookAuthors.authorID = a.id
AND bookAuthors.bookID = 1
)
GROUP BY
a.name;

If a subquery returns any rows at all, EXISTS subquery is TRUE, and NOT EXISTS subquery is FALSE.
And when you use ... (SELECT 1 FROM t2 WHERE t2.post_id = :id LIMIT 1) you either return 1 on success or NULL on no thing which consider as True or False respectively.
Working with Exists is more professional because:
Traditionally, an EXISTS subquery starts with SELECT *, but it could begin with SELECT 5 or SELECT column1 or anything at all. MySQL ignores the SELECT list in such a subquery, so it makes no difference.
It takes the best way to return True or False.
reference from MySQL Dev site

Related

Problems using SQL ALL operator

I'm having trouble using/understanding the SQL ALL operator. I have a table FOLDER_PERMISSION with the following columns:
+----+-----------+---------+----------+
| ID | FOLDER_ID | USER_ID | CAN_READ |
+----+-----------+---------+----------+
| 1 | 34353 | 45453 | 0 |
| 2 | 46374 | 342532 | 1 |
| 3 | 46374 | 32352 | 1 |
+----+-----------+---------+----------+
I want to select the folders where all the users have permission to read, how could I do it?
Use aggregation and having:
select folder_id
from t
group by folder_id
having min(can_read) = 1;
Gordon's answer seems better but for the sake of completeness, using ALL a query could look like:
SELECT x1.folder_id
FROM (SELECT DISTINCT
fp1.folder_id
FROM folder_permission fp1) x1
WHERE 1 = ALL (SELECT fp2.can_read
FROM folder_permission fp2
WHERE fp2.folder_id = x1.folder_id);
If you have a table for the folders themselves replace the derived table (aliased x1) with it.
But this only respects users present in folder_permissions. If not all users have a reference in that table you possibly won't get the folders really all users can read.
You can do aggregation :
SELECT fp.FOLDER_ID
FROM folder_permission fp
GROUP BY fp.FOLDER_ID
HAVING SUM( can_read = 0 ) = 0;
You can also express it :
SELECT fp.FOLDER_ID
FROM folder_permission fp
GROUP BY fp.FOLDER_ID
HAVING MIN(CAN_READ) = MAX(CAN_READ) AND MIN(CAN_READ) = 1;
If you wanted to return the full matching records, you could try using some exists logic:
SELECT ID, FOLDER_ID, USER_ID, CAN_READ
FROM yourTable t1
WHERE NOT EXISTS (SELECT 1 FROM yourTable t2
WHERE t2.FOLDER_ID = t1.FOLDER_ID AND t2.CAN_READ = 0);
Demo
The existence of a matching record in the above exists subquery would imply that there exist one or more users for that folder who do not have read access rights.

How to find all the opposite combinations between two columns in SQL

I am making a web dating app that needs to match users and let them chat with each other.
I want to figure out how to find all the matches for a particular user.
Right now I have a table called follows that has 2 columns.
UserID | MatchUserID
--------------------
1 | 2
2 | 1
1 | 3
1 | 4
1 | 5
4 | 1
5 | 4
The idea is that for two users to match they need to follow one another. The table above shows which user follows which.
Assuming that the user who is currently logged on is UserID = 1.
I need a query that will return from the MatchUserID table the following results:
2, 4
In a way, I am looking to find all the opposite combinations between the two columns.
This is the code I use to create the table.
CREATE TABLE Match
(
UserID INT NOT NULL,
MatchUserID INT NOT NULL,
PRIMARY KEY (UserID, MatchUserID)
);
You can do it with a self join:
select m.MatchUserID
from `Match` m inner join `Match` mm
on mm.MatchUserID = m.UserId
where
m.UserId = 1
and
m.MatchUserID = mm.UserId
See the demo.
Results:
| MatchUserID |
| ----------- |
| 2 |
| 4 |
The simplest way possibly is to use EXISTS and a correlated subquery that searches for the other match.
SELECT t1.matchuserid
FROM elbat t1
WHERE t1.userid = 1
AND EXISTS (SELECT *
FROM elbat t2
WHERE t2.matchuserid = t1.userid
AND t2.userid = t1.matchuserid);

mysql select twice itself again in one table

How to select in 1 query below. This query need re search that's find value to their own loop.
This is different from other sub query , using 1 table only
TAble T
| num| WHOSE
| 1 | A
| 1 | C
| 2 | B
| 2 | C
| 3 | D
Criteria to match records (conditions):
The value in column whose is not C
The value in column num does not match a value for another record in condition 1.
I want to find the record the value 3 in column num (which has D for column whose).
select * from T where whose <> C and ( num is not one of c's)
1 A can not because C has 1
2 B can not because C has 2
3 D is what I want, because it doesn't have C in column whose nor share a value in column num with a record that does have C in the column whose.
First select num of those records where whose is C. Then select those records where whose is not C and also where num is not one of the ones in subquery.
Select * from T where whose <> 'C' and num not in (Select Num from T where whose = 'C' )
Another way to achieve the same result is with a LEFT JOIN on the same table:
SELECT T.*
FROM T
LEFT JOIN T t2 on t2.num = T.num and t2.whose = 'C'
WHERE T.whose <> 'C' AND t2.whose IS NULL
Check it out on this SQL Fiddle, where the result is:
| num | whose |
| 3 | D |
Additionally, a similar way to write the query is to use the NOT EXISTS clause in the WHERE conditions, like this:
SELECT T.* from T
WHERE T.whose <> 'C' AND NOT EXISTS (SELECT 1 FROM T t2 WHERE
t2.num = T.num AND t2.whose = 'C')
Check it out in this SQL fiddle.
To read more about the comparison between EXISTS and LEFT JOIN see this article. In the summary at the end it has the following conclusions:
MySQL can optimize all three methods to do a sort of NESTED LOOPS ANTI JOIN.
...
However, these three methods generate three different plans which are executed by three different pieces of code. The code that executes EXISTS predicate is about 30% less efficient than those that execute index_subquery and LEFT JOIN optimized to use Not exists method.
That's why the best way to search for missing values in MySQL is using a LEFT JOIN / IS NULL or NOT IN rather than NOT EXISTS.

Select from one table but filtering other two

Let's say i've got this database:
book
| idBook | name |
|--------|----------|
| 1 |Book#1 |
category
| idCateg| category |
|--------|----------|
| 1 |Adventures|
| 2 |Science F.|
book_categ
| id | idBook | idCateg | DATA |
|--------|--------|----------|--------|
| 1 | 1 | 1 | (null) |
| 2 | 1 | 2 | (null) |
I'm trying to select only the books which are in category 1 AND category 2 something like this
SELECT book.* FROM book,book_categ
WHERE book_categ.idCateg = 1 AND book_categ.idCateg = 2
Obviously, this giving 0 results becouse each row has only one idCateg it does work width OR but the results are not what I need. I've also tried to use a join, but I just can't get the results I expect.
Here it's the SQLFiddle of my current project, with my current DB, the data at the begining is just a sample. SQLFiddle
Any help will be really appreciated.
Solution using EXISTS:
select *
from book b
where exists (select 'x'
from book_categ x
where x.idbook = b.idbook
and x.idcateg = 1)
and exists (select 'x'
from book_categ x
where x.idbook = b.idbook
and x.idcateg = 2)
Solution using join with an inline view:
select *
from book b
join (select idbook
from book_categ
where idcateg in (1, 2)
group by idbook
having count(*) = 2) x
on b.idbook = x.idbook
You could try using ALL instead of IN (if you only want values that match all criteria to be returned):
SELECT book.*
FROM book, book_categ
WHERE book_categ.idCateg = ALL(1 , 2)
One way to get the result is to do join to the book_categ table twice, something like
SELECT b.*
FROM book b
JOIN book_categ c1
ON c1.book_id = b.id
AND c1.idCateg = 1
JOIN book_categ c2
ON c2.book_id = b.id
AND c2.idCateg = 2
This assumes that (book_id, idCateg) is constrained to be unique in the book_categ table. If it isn't unique, then this query can return duplicate rows. Adding a GROUP BY clause or the DISTINCT keyword will eliminate any generated duplicates.
There are several other queries that can get generate the same result.
For example, another approach to finding book_id that are in two categories is to get all the rows with idCateg values of 1 or 2, and then GROUP BY book_id and get a count of DISTINCT values...
SELECT b.*
FROM book b
JOIN ( SELECT d.book_id
FROM book_categ d
WHERE d.idCateg IN (1,2)
GROUP BY d.book_id
HAVING COUNT(DISTINCT d.idCateg) = 2
) c
ON c.book_id = b.id

MYSQL join results set wiped results during IN () in where clause?

Editted heavily!
The original question was based on a misunderstanding of how IN() treats a column from a results set from a join. I thought IN( some_join.some_column ) would treat a results column as a list and loop through each row in place. It turns out it only looks at the first row.
So, the adapted question: Is there anything in MySQL that can loop through a column of results from a join from a WHERE clause?
Here's the super-simplified code I'm working with, stripped down from a complex crm search function. The left join and general idea are relics from that query. So for this query, it has to be an exclusive search - finding people with ALL specified tags, not just any.
First the DB
Table 1: Person
+----+------+
| id | name |
+----+------+
| 1 | Bob |
| 2 | Jill |
+----+------+
Table 2: Tag
+-----------+--------+
| person_id | tag_id |
+-----------+--------+
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 2 | 3 |
+-----------+--------+
Nice and simple. So, naturally:
SELECT name, GROUP_CONCAT(tag.tag_id) FROM person LEFT JOIN tag ON person.id = tag.person_id GROUP BY name;
+------+--------------------------+
| name | GROUP_CONCAT(tag.tag_id) |
+------+--------------------------+
| Bob | 1,2 |
| Jill | 2,3 |
+------+--------------------------+
So far so good. So what I'm looking for is something that would find only Bob in the first case and only Jill in the second - without using HAVING COUNT(DISTINCT ...) because that doesn't work in the broader query (there's a seperate tags inheritance cache and a ton of other stuff).
Here's my original sample queries - based on the false idea that IN() would loop through all rows at once.
SELECT DISTINCT name FROM person LEFT JOIN tag ON person.id = tag.person_id
WHERE ( ( 1 IN (tag.tag_id) ) AND ( 2 IN (tag.tag_id) ) );
Empty set (0.00 sec)
SELECT DISTINCT name FROM person LEFT JOIN tag ON person.id = tag.person_id
WHERE ( ( 2 IN (tag.tag_id) ) AND ( 3 IN (tag.tag_id) ) );
Empty set (0.00 sec)
Here's my new latest failed attempt to give an idea of what I'm aiming for...
SELECT name, GROUP_CONCAT(tag.tag_id) FROM person LEFT JOIN tag ON person.id = tag.person_id
GROUP BY person.id HAVING ( ( 1 IN (GROUP_CONCAT(tag.tag_id) ) ) ) AND ( 2 IN (GROUP_CONCAT(tag.tag_id)) );
Empty set (0.00 sec)
So it seems it's taking a GROUP_CONCAT string, of either 1,2 or 2,3, and is treating it as a single entity rather than an expression list. Is there any way to turn a grouped column into an expression list that IN () or =ANY() will treat as a list?
Essentially, I'm trying to make IN() loop iteratively over something that resembles an array or a dynamic expression list, which contains all the rows of data that come from a join.
Think about what your code is doing logically:
( 1 IN (tag.tag_id) ) AND ( 2 IN (tag.tag_id) )
is equivalent to
( 1 = (tag.tag_id) ) AND (2 = (tag.tag_id) )
There's no way tag.tag_id can satisfy both conditions at the same time, so the AND is never true.
It looks like the OR version you cited in your question is the one you really want:
SELECT DISTINCT name FROM person LEFT JOIN tag ON person.id = tag.person_id
WHERE ( ( 1 IN (tag.tag_id) ) OR ( 2 IN (tag.tag_id) ) );
Using the IN clause more appropriately, you could write that as:
SELECT DISTINCT name FROM person LEFT JOIN tag ON person.id = tag.person_id
WHERE tag.tag_id in (1,2);
One final note, because you're referencing a column from the LEFT JOINed table in your WHERE clause (tag.tag_id), you're really forcing that to behave like an INNER JOIN. To truly get a LEFT JOIN, you'd need to move the criteria out of the WHERE and make it part of the JOIN conditions instead:
SELECT DISTINCT name FROM person LEFT JOIN tag ON person.id = tag.person_id
AND tag.tag_id in (1,2);
WHERE ( ( 1 IN (tag.tag_id) ) AND ( 2 IN (tag.tag_id) ) );
This will never return any results since tag.tag_id cannot be 1 and 2 at the same time.
Additionally is there a reason you're using 1 IN (blah) rather than blah = 1?