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

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?

Related

MySQL JOIN two tables by one value and take other results (without that value too)

I have some problem with a mySQL query.
The table A is this:
A.id
A.value1
A.user
Table B is:
B.id
B.user
I need to find value_that_i_need from query, by searching for B.user.
But I don't need only values with A.user, i need all values from Table A with the same A.id (inside Table A) that matches B.user.
So I need all distinct id (where there is B.user=A.user) and search for them inside table A by A.id.
I want to avoid 2 different queries! Already tried differents JOIN, nothing works for me.
EDIT
Ok, i will ty to explain the problem in a easiest way.
I have this table:
+---------+------------+
| id_user | another_id |
+---------+------------+
id_user -> unique id for each user
another_id -> an id related to something like a group
another_id can be the same to more users, but i need to take only users who are inside my same groups.
So i will have to check my groups (by searching my id_user) and then i have to see all users with my same another_id.
Problem is that if i query something like this:
SELECT * FROM table0 AS t0, something_like_groups AS slg
JOIN user_inside_group as uig ON slg.id_group=uig.group_id AND slg.id_user='my_user_id'
WHERE slg.id='id_group' AND t0.user_id=uig.user_id
Actually i have to join 3 tables, but the problem is that i need to find the "group" inside i am and get ALL informations about all users inside my same group. (without an additional query)
Perhaps you just want to find the min id based on b user and then get all the rows from a which match. for example
drop table if exists t,t1;
create table t( id int,user varchar(10));
create table t1( id int,user varchar(10));
insert into t values
(1,'aaa'),(1,'bbb'),(2,'ccc');
insert into t1 values
(1,'bbb'),(2,'ccc')
;
select t.id,t.user
from t
join
(
select t1.user,min(t.id) minid
from t1
join t on t.user = t1.user
group by t1.user
) s
on t.id = s.minid;
+------+------+
| id | user |
+------+------+
| 1 | aaa |
| 1 | bbb |
| 2 | ccc |
+------+------+
3 rows in set (0.00 sec)

mysql select rows including related

I have table:
id | parent | regno | person
1 | 0 | 12 | 5
2 | 1 | 12 | 15
3 | 0 | 13 | 5
4 | 0 | 14 | 6
I have MySQL query...
SELECT *
FROM table
WHERE person='5';
...that returns rows 1 and 3.
In this table row 1 and 2 are related (same regno).
How can i build this query to include related rows?
Basically when searching for person 5 i need MySQL query to return following:
id | parent | regno | person
1 | 0 | 12 | 5
2 | 1 | 12 | 15
3 | 0 | 13 | 5
Parent column has id of column it is related to, but it can be positive and negative integer. All related rows always have same regno.
Thank you.
You want all people who have a regno that is the same as the regno of anyone who is person 5:
--this main query finds all people with the regno from the subquery
SELECT *
FROM table
WHERE regno IN
( --this subquery finds the list of regno
SELECT regno
FROM table
WHERE person = '5'
)
There are other ways to write this; i'm not a fan of IN, and personally would write it like this:
SELECT t.*
FROM table t
INNER JOIN
(
SELECT DISTINCT regno
FROM table
WHERE person = '5'
) u
WHERE t.regno = u.regno
But it's harder to understand, and it's quite likely that these queries would end up being executed identically internally anyway. In this form the DISTINCT is required to make the regno from the subquery unique. If it were not, joined rows would end up duplicated. Why do I prefer it over IN? In some database systems IN's implementation can be very naive and low performing. "Never use IN to create a list longer then you would write by hand" is an old mantra I tend to stick to. This join pattern is also more flexible, can work with multiple values. Not every database supports Oracle-esque where x,y in ((1,3),(3,4)) value multiples
As an aside (and partly in response to the first comment on this answer) it would be more typical and more useful/usual to have the database prepare a set of rows that had parent and child data on the same line
It would look more like this:
SELECT *
FROM
table c
LEFT OUTER JOIN
table p
ON c.regno = p.regno AND p.parent = 1
WHERE c.person = '5' AND c.parent=0
This is assuming your "parent" column is 0 1 indicating true false.. you seem to have made a comment that parent is the id of the relative (not sure if it's parent-of or parent-is)
For a table where there is an id, and parentid column, and the parentid is set to a value when the row is a child of that other id;
id, parentid, name
1, null, Daddy
2, 1, Little Jonny
3, 1, Little Sarah
That looks like:
SELECT *
FROM
table c
INNER JOIN
table p
ON c.parentid = p.id
WHERE p.parentid ID NULL
Rows can have only one parent. A NULL in the parent id defines the row as being a parent, otherwise it's a child. You could turn this logic on its head if you wanted, call the column isparentof and have all child rows with null in the isparentof, and anyone who is a parent of a child, out the child id in isparentof. This then limits you to one child per multiple parents (single child families).. the query to pull them out is broadly the same
You can get all the id values for the person = '5' in a Derived Table.
Now, join back to the main table, matching either the absolute of parent (to get the child row(s)) or the id (to get the parent id row itself).
Based on discussion in comments, Try:
SELECT t.*
FROM your_table AS t
JOIN
(
SELECT id AS parent_id
FROM your_table
WHERE person = '5'
) AS dt
ON dt.parent_id = ABS(t.parent) OR
dt.parent_id = t.id
It is hard to comprehend though, why would you put negative values in parent!

Why should I use EXISTS() function in 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

How to sort items in mysql based on data from another table?

I have the following tables:
word_list:
id | word
1 | ball
2 | car
3 | small
items_word_match:
itemid | wordid | in_title | in_description
1 | 1 | 1 | 0 //The word #1 occurs once in the title of the item
1 | 3 | 0 | 2 //The word #3 occurs twice in the title of the item
3 | 3 | 1 | 2
2 | 1 | 1 | 0
search:
wordid | importance
1 | 1
2 | 5
3 | 2 //word #3 is more important than the word #1 but less so than word #2
I want to sort the items based on the keywords from the search table and how important the keywords are.
And if the keyword is in the title the importance should increase by 1 and if the word appears 2 times the importance of the word should be importance*2
Neither of those answers from Denis or Johan are correct. Instead you could use this:
select
itemid,
sum(word_importance) as item_importance
from
(select
itemid,
search.wordid,
(
in_title * (importance + 1)
+ in_description * importance
) as word_importance
from
items_word_match,
search
where
i.wordid = s.wordid
)
group by itemid
As Johan pointed out, you need to add an order clause to the end, , order by item_importance desc
Feel a bit lazy today, so I'm just going to answer the question in the title:
How to sort items in mysql based on data from another table ?
You can sort the outcome of a query by any criterium you wish.
SELECT word_list.* FROM word_list
INNER JOIN search ON (search.wordid = wordlist.id)
ORDER BY search.importance, word_list.id DESC
Note that the JOIN needed to link the two tables together can have a profound effect on which rows in the word_list table are selected, but you need to do a JOIN somehow.
Otherwise MySQL will not know what the relationship between the two tables is and cannot sort the fields.
SELECT
i.itemid
, SUM( i.in_description * s.importance
+ i.in_title * ( s.importance + 1 )
)
AS item_importance
FROM
items_word_match i
LEFT JOIN
search s
ON s.wordid = i.wordid
GROUP BY
i.itemid
ORDER BY
item_importance DESC
CORRECTION:
I used LEFT JOIN catch the case when some words do not appear in the search table. But then the importance of those words seems appropriate to be 0 and not NULL, so the SUM should be changed into:
, SUM( i.in_description * COALESCE(s.importance, 0)
+ i.in_title * COALESCE(s.importance, 1)
)
your order by clause can contain fields from any of the tables:
select table1.*
from table1
join table2 using (table1_id)
order by table2.field, table1.field, etc.

How to select an item, the one below and the one above in MYSQL

I have a database with ID's that are non-integers like this:
b01
b02
b03
d01
d02
d03
d04
s01
s02
s03
s04
s05
etc. The letters represent the type of product, the numbers the next one in that group.
I'd like to be able to select an ID, say d01, and get b03, d01, d02 back. How do I do this in MYSQL?
Here is another way to do it using UNIONs. I think this is a little easier to understand and more flexible than the accepted answer. Note that the example assumes the id field is unique, which appears to be the case based on your question.
The SQL query below assumes your table is called demo and has a single unique id field, and the table has been populated with the values you listed in your question.
( SELECT id FROM demo WHERE STRCMP ( 'd01', id ) > 0 ORDER BY id DESC LIMIT 1 )
UNION ( SELECT id FROM demo WHERE id = 'd01' ORDER BY id ) UNION
( SELECT id FROM demo WHERE STRCMP ( 'd01', id ) < 0 ORDER BY id ASC LIMIT 1 )
ORDER BY id
It produces the following result: b03, d01, d02.
This solution is flexible because you can change each of the LIMIT 1 statements to LIMIT N where N is any number. That way you can get the previous 3 rows and the following 6 rows, for example.
Note: this is from M$ SQL Server, but the only thing that needs tweaking is the isnull function.
select *
from test m
where id between isnull((select max(id) from #test where col < 'd01'),'d01')
and isnull((select min(id) from #test where col > 'd01'),'d01')
Find your target row,
SELECT p.id FROM product WHERE id = 'd01'
and the row above it with no other row between the two.
LEFT JOIN product AS p1 ON p1.id > p.id -- gets the rows above it
LEFT JOIN -- gets the rows between the two which needs to not exist
product AS p1a ON p1a.id > p.id AND p1a.id < p1.id
and similarly for the row below it. (Left as an exercise for the reader.)
In my experience this is also quite efficient.
SELECT
p.id, p1.id, p2.id
FROM
product AS p
LEFT JOIN
product AS p1 ON p1.id > p.id
LEFT JOIN
product AS p1a ON p1a.id > p.id AND p1a.id < p1.id
LEFT JOIN
product AS p2 ON p2.id < p.id
LEFT JOIN
product AS p2a ON p2a.id < p.id AND p2a.id > p2.id
WHERE
p.id = 'd01'
AND p1a.id IS NULL
AND p2a.ID IS NULL
Although not a direct answer to your question I personally wouldn't rely on the natural order, since it may change duo to import/exports and produce side effects not easily understandable by fellow programmers. What about creating an alternate INTEGER index and fire up another query? "WHERE id > ...yourdesiredid ... LIMIT 1"?
mysql> describe test;
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | varchar(50) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
mysql> select * from test;
+------+
| id |
+------+
| b01 |
| b02 |
| b03 |
| b04 |
+------+
mysql> select * from test where id >= 'b02' LIMIT 3;
+------+
| id |
+------+
| b02 |
| b03 |
| b04 |
+------+
What about using a cursor? This would let you traverse the returned set one row at a time. using it with two variables (like "current" and "last"), you could inchworm along the result until you hit your target. Then return the value of "last" (for n-1), your entered target (n), and then traverse / iterate one more time and return the "current" (n+1).