How can I use IN() clause in the INNER JOIN? - mysql

Here is my query:
SELECT p.*
FROM posts p
INNER JOIN pivot pi on p.id = pi.post_id
INNER JOIN tags t on t.id = pi.tag_id and t.name = "mysql"
All I'm trying to do is getting posts that are tagged with a specific tag. In query above, I'm selecting all posts with mysql tag.
Now I need to make it working for multiple-tags. I mean, I want to select all post which are tagged with both mysql and php. I guess I have to use IN() clause in the query. But I don't know how exactly. Any idea?
EX: Here are my tables:
// posts
+----+---------+------------+
| id | subject | body |
+----+---------+------------+
| 1 | sub1 | content1 |
| 2 | sub2 | content2 |
+----+---------+------------+
// tags
+----+--------+
| id | name |
+----+--------+
| 1 | mysql |
| 2 | php |
+----+--------+
// pivot
+---------+--------+
| post_id | tag_id |
+---------+--------+
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
+---------+--------+
The expected result for [mysql]:
| 1 | sub1 | content1 |
| 2 | sub2 | content2 |
The expected result for [mysql][php]:
| 2 | sub2 | content2 |

I would use a WHERE clause for the conditions and then aggregation:
SELECT p.*
FROM posts p INNER JOIN
pivot pi
ON p.id = pi.post_id INNER JOIN
tags t
ON t.id = pi.tag_id
WHERE t.name IN ('mysql', 'php')
GROUP BY p.id
HAVING COUNT(*) = 2; -- this is the number of tags in the list
This assumes that tags are not duplicated.
A note about using GROUP BY p.id with SELECT p.*. This is actually valid and allowed by the ANSI standard, assuming that p.id is unique in posts. The specific rule is based on functional dependency.
I do think the most recent versions of MySQL disallow the construct by default. You can always do something like:
SELECT p.*
FROM posts p JOIN
(SELECT pi.post_id
FROM pivot pi INNER JOIN
tags t
ON t.id = pi.tag_id
WHERE t.name IN ('mysql', 'php')
GROUP BY p.id
HAVING COUNT(*) = 2 -- this is the number of tags in the list
) t
ON p.id = t.post_id ;

I want to select all post which are tagged with both mysql and php. I guess I have to use IN() clause in the query.
Nope. That would show you posts which are tagged one or the other. To find posts with both, I would join to the tables twice:
SELECT p.*
FROM posts p
INNER JOIN pivot pi1 on p.id = pi1.post_id
INNER JOIN tags t1 on t1.id = pi1.tag_id and t1.name = "mysql"
INNER JOIN pivot pi2 on p.id = pi2.post_id
INNER JOIN tags t2 on t2.id = pi2.tag_id and t2.name = "php"
...

I don't know why should we complicate this ..
Instade of in clause just use string input to query and compare it with string aggregate.
SELECT *
FROM posts P
INNER JOIN (
SELECT post_id, CONCAT ("(", Group_concat(t.name SEPARATOR ' ,'), ")") AS tgs
FROM pivote p
INNER JOIN tag t
ON (t.id = p.tag_id)
GROUP BY post_id
) AA
ON P.id = post_id
WHERE tgs LIKE "(mysql,php)

Related

MYSQL: select 1 record per product, prefer if client is set?

My goal is to remove duplicate productID's, making the query pick the record that has a clientID over the one that doesn't.
The idea is to link files (like manuals) to a product. However, when a file is also linked to a client, it should take priority over the globally linked file as that file will be for this specific client only.
I'm not sure if this is even possible, the tables look like this:
client_products
| client | product |
products
| ID | name |
product_specifications
| filepath | product | client |
Now, this is what I've got so far:
SELECT products.*, ps.specification, cp.client
FROM products
LEFT JOIN product_specifications ps on ps.product = products.ID
RIGHT JOIN client_products cp ON cp.product = products.ID
WHERE cp.client = 1
AND products.ID = cp.product
ORDER BY products.name
Obviously, I'm doing a basic select query with some joins. I'm grouping it by the products.ID to make sure I don't get the product back twice, but this does not guarantee that I will get the specific client's linked specification file, right? Any ideas would be greatly appreciated!
Example results:
What i'm getting (without the GROUP BY)
| ID | specification | client |
| 1 | 1 | 1 |
| 1 | 1 | NULL |
| 2 | 3 | NULL |
What I would like to get when the client column is not null:
| ID | specification | client |
| 1 | 1 | 1 |
| 2 | 3 | NULL |
See my sql fiddle here
I've solved a similar fallback problem here. The "trick" is to use a LEFT JOIN to look if a client specific file exists:
select p.*, ps.*
from client_products cp
join products p on p.ID = cp.product
left join product_specifications ps
on ps.product = p.ID
and (ps.client = 1 or ps.client IS NULL)
left join product_specifications ps1
on ps1.product = p.ID
and ps1.client = 1
and ps.client IS NULL
where cp.client = 1
and ps1.client IS NULL
http://sqlfiddle.com/#!9/d9d705/29
If you only need one column from the product_specifications table, you could also try this one:
select p.*, coalesce(ps.specification, ps1.specification) as specification
from client_products cp
join products p on p.ID = cp.product
left join product_specifications ps
on ps.product = p.ID
and ps.client = 1
left join product_specifications ps1
on ps1.product = p.ID
and ps1.client IS NULL
where cp.client = 1
http://sqlfiddle.com/#!9/d9d705/34
You join the product_specifications table twice. Once for client = 1 and once for client IS NULL. Using COALESCE() you select the second one only if the first one doesn't exit.
I think the kernel of your problem can be solved with the following (I may have amended column names slightly)...
SELECT x.*
FROM product_specifications x
JOIN
( SELECT product_id
, MAX(client_id) client_id
FROM product_specifications
GROUP
BY product_id
) y
ON y.product_id = x.product_id
AND (y.client_id = x.client_id OR y.client_id IS NULL);

MySQL left join with NULL values

I'm certain this has been asked and answered already but don't know how exactly the question should be.
I have two tables
ID | name
=========
1 | foo
2 | bar
3 | lou
4 | sue
and a meta table:
p_ID | key | value
===================
1 | poo | 1
2 | zoo | 'whatever'
3 | clu | 423
4 | poo | 1
I like to get all entries from the first table which doesn'T have a poo value assigned:
ID | name
=========
2 | bar
3 | lou
My approach was
SELECT *
FROM table AS p
LEFT JOIN meta AS m
ON m.p_id = p.ID
WHERE m.key = 'poo'
AND m.value IS NULL
but this returns an empty result
You have to move the m.key = 'poo' expression into the ON-clause. Everything that is in the WHERE-clause MUST be present, even in a LEFT JOIN.
SELECT *
FROM table AS p
LEFT
JOIN meta AS m
ON m.p_id = p.ID
AND m.key = 'poo'
WHERE m.value IS NULL
I think you should want this;)
SELECT *
FROM table AS p
LEFT JOIN meta AS m
ON m.p_id = p.ID
AND m.key = 'poo'
WHERE m.value IS NULL
When you use m.key = 'poo' in WHERE clause, this will be computed as INNER JOIN, so you only get records with m.key = 'poo', not all rows in table.
You can use INNER JOIN instead.
SELECT *
FROM `table` T
INNER JOIN meta M ON M.p_id = T.ID
WHERE M.key <> 'poo'

How can I query a many-to-many join with filtering on the same relation?

I simplified a many-to-many relationship case
with these mockup tables.
Posts:
------------------------------
| id | title | body |
------------------------------
| 1 | One | text1 |
| 2 | Two | text2 |
| 3 | Three | text3 |
------------------------------
Tags:
-------------------
| id | name |
-------------------
| 1 | SQL |
| 2 | GLSL |
| 3 | PHP |
-------------------
Post_tags:
------------------------------
| id | p_id | t_id |
------------------------------
| 1 | 1 | 1 |
| 2 | 1 | 3 |
| 3 | 2 | 1 |
| 3 | 3 | 2 |
------------------------------
My goal is to query POSTS with specific TAGS, which I have no problem with, but I also want to display all related tags to the post not just the one I queried for.
My query looks like this:
SELECT p.Title, p.Body, t.name
FROM Posts p
LEFT JOIN Post_tags pt ON p.id = pt.p_id
LEFT JOIN Tags t ON t.id = pt.t_id
WHERE t.name LIKE '%SQL%'
This gets the posts with the "SQL" tag, but it only joins the posts table with tags where it found the "SQL" string, so the other tag "PHP" associated with the post doesn't get joined.
Obviously the problem is I'm joining the table on the WHERE clause, but how would one solve this in one query or (preferably with subqueries)?
Currently I'm doing this in two separate queries in my application, one for selecting matching posts and another one that is retreiving full post data. This isn't so efficient and also seems like a lame solution, and I haven't found a better yet, so I decided to ask the StackOverflow community.
My old answer is not the shortest, here's the shortest one:
select p.*, '' as x, t.name, t.name like '%SQL%'
from Posts p
join Posts_tags pt on pt.p_id = p.id
join Tags t on t.id = pt.t_id;
Output:
ID TITLE BODY X NAME T.NAME LIKE '%SQL%'
1 One text1 SQL 1
1 One text1 PHP 0
2 Two text2 SQL 1
3 Three text3 GLSL 0
So if we group by ID, and check that if at least one (aided by bit_or; Postgresql has this too, aptly named bool_or) of the elements in the group satisfied the '%SQL%' criteria, its bit is ON (aka boolean = true). We can pick that group and we retain all the tags under that group, example, tag id 1 appear on post 1, and post 1 has other tags, which is #3 or PHP. All tags that belong to same Post ID will not be discarded, as we will not be using WHERE filter, we will be using HAVING filter instead:
select p.*, group_concat(t.name) as tags
from Posts p
join Posts_tags pt on pt.p_id = p.id
join Tags t on t.id = pt.t_id
group by p.id
having bit_or(t.name like '%SQL%');
We can also rewrite that to this:
select p.*, group_concat(t.name) as tags
from Posts p
join Posts_tags pt on pt.p_id = p.id
join Tags t on t.id = pt.t_id
group by p.id
having sum(t.name like '%SQL%') >= 1;
BIT_OR is like IN, or ANY, so it's more semantic than evaluating things by SUM
Output:
D TITLE BODY TAGS
1 One text1 PHP,SQL
2 Two text2 SQL
Live test: http://www.sqlfiddle.com/#!2/52b3b/26
I'm learning so much on stackoverflow. After my old answer, I'm thinking how to make an equivalent shorter code in Postgresql using windowing function(which MySQL don't have) via SUM OVER partition. Then I thought of Postgresql's bool_or,bool_and and every function. Then I remember MySQL has bit_or :-)
The last solution using SUM is just an afterthought, when I thought up that bit_or is just a semantic of at least one is true, then it's obvious that you can use HAVING SUM(condition) >= 1 too. Now it works on all database :-)
I ended up not solving it by windowing function, the solution above now works on all database :-)
The most concise (might be fast) I can think of:
select p.*, '' as x, t.name
from Posts p
join Posts_tags pt
ON pt.p_id = p.id
AND pt.p_id in (select p_id
from Posts_tags
join Tags on Tags.id = Posts_tags.t_id
where Tags.name like '%SQL%')
join Tags t on t.id = pt.t_id;
If you need the tags collapsed in one line, use GROUP_CONCAT:
select p.*, group_concat(t.name) as tags
from Posts p
join Posts_tags pt
ON pt.p_id = p.id
AND pt.p_id in (select p_id
from Posts_tags
join Tags on Tags.id = Posts_tags.t_id
where Tags.name like '%SQL%')
join Tags t on t.id = pt.t_id
group by p.id;
Output:
ID TITLE BODY TAGS
1 One text1 SQL,PHP
2 Two text2 SQL
Live test: http://www.sqlfiddle.com/#!2/52b3b/2
UPDATE
There's a solution more optimized than this, see here: https://stackoverflow.com/a/10471529
Put on a separate inner join for all tags
SELECT p.Title, p.Body, t2.name
FROM Posts p
LEFT JOIN Post_tags pt ON p.id = pt.p_id
LEFT JOIN Tags t ON t.id = pt.t_id
INNER JOIN Post_tags pt2 ON p.id = pt2.p_id
INNER JOIN Tags t2 on ON t2.id = pt2.t_id
WHERE t.name LIKE '%SQL%'
Try this:
SELECT p.Title, p.Body, t.name,GROUP_CONCAT(t2.name) AS `tags`
FROM Posts p
LEFT JOIN Post_tags pt ON p.id = pt.p_id
LEFT JOIN Tags t ON t.id = pt.t_id
JOIN Tags t2 ON t2.id = p.id
WHERE t.name LIKE '%SQL%'
This uses GROUP_CONCAT to create a comma-separated list of tags associated with that particular post. Output for your query:
TITLE BODY NAME tags
One text1 SQL SQL,GLSL
SQL fiddle: http://sqlfiddle.com/#!2/2f698/9
Yet another way to do this is built around an inner join of posts_tags with itself:
SELECT *
FROM posts_tags pt1
JOIN posts_tags pt2
USING(p_id)
WHERE pt2.t_id = 1;
+------+------+------+
| p_id | t_id | t_id |
+------+------+------+
| 1 | 1 | 1 |
| 1 | 3 | 1 |
| 1 | 4 | 1 |
| 3 | 1 | 1 |
| 3 | 2 | 1 |
| 5 | 1 | 1 |
| 5 | 3 | 1 |
| 7 | 1 | 1 |
+------+------+------+
8 rows in set (0.00 sec)
Without the WHERE clause the inner join would give the full cartesian product (t_id 1, t_id 2) of all tags associated with each post. Applying the WHERE clause to half the cartesian product gives you the "all members of sets containing x" structure you're looking for. (The example above demonstrates that only posts associated with tag id 1 have been retrieved; further, all tags associated with those posts are present as well.) Now it's two more simple joins to fetch the information associated with p_id and t_id:
SELECT title,name
FROM posts_tags pt1
JOIN posts_tags pt2
ON(pt1.p_id = pt2.p_id)
JOIN posts
ON(pt1.p_id = posts.id)
JOIN tags
ON (pt1.t_id = tags.id)
WHERE pt2.t_id = 1;
+---------+--------+
| title | name |
+---------+--------+
| first | php |
| first | skiing |
| first | tuna |
| third | php |
| third | sql |
| fifth | php |
| fifth | skiing |
| seventh | php |
+---------+--------+
8 rows in set (0.01 sec)

how to write this self join query in mysql

Hello I have a table structure like this
products_id | model_num | master_model_num
1 | cth001 | 0
2 | cth002 | 0
3 | cth003 | cth001
4 | cth004 | cth001
5 | cth005 | 0
6 | cth006 | cth002
My Problem
I will provide the products_id to the table and it will get all product ids whoes master_model_num is equal to the model_num of the given products_id
I have tried following query but it doen't generate the result that I want
SELECT p.products_id
FROM products p,products pp
WHERE p.products_id=pp.products_id
AND p.products_model=pp.products_master_model
AND p.products_id='1'
SELECT pp.products_id
FROM products p
INNER JOIN products pp
ON p.model_num = pp.master_model_num
WHERE p.products_id = '1'
Wouldn't
SELECT products_id
FROM products
WHERE master_model_num = (SELECT model_num
FROM products
WHERE products_id = 1)
make more sense in this case? By having AND p.products_id='1' on the end of your query, you're guaranteeing that you'll only get one record back.
Try this
SELECT
p.products_id
FROM
products p
INNER JOIN
products pp
ON
pp.products_master_model = p.products_model
SELECT p.products_id FROM products p,products pp where p.model_num=pp.master_model_num and p.products_id='1'

How can I select all rows in a table, and cross-check it with another table?

MySQL Tables
MySQL: Plants
+----+--------+
| id | plant |
+----+--------+
| 1 | tree |
+----+--------+
| 2 | flower |
+----+--------+
| 3 | tree |
+----+--------+
MySQL: Trees
+----+------+
| id | type |
+----+------+
| 1 | hard |
+----+------+
| 3 | soft |
+----+------+
What I'm trying to do
I want to select all rows in the table plants.
But if plant = tree, then the type in the table trees must be hard in order for it to be displayed.
So, the example above should output: 1=tree and 2=flower.
3=tree should be excluded because its type = soft.
What I've tried so far...but didn't work
a) I guess I cannot use union, because the columns are different.
b) I already tried left join, but that didn't work either:
select p.id, p.plant
from plants AS p
left join
(
select `id`, `type`
from `trees`
) AS t ON p.id = t.id
WHERE t.type = 'hard'
any ideas on how I can do it?
yes
select p.id, p.plant from plants as p left join trees as t on (p.id = t.id) where t.type = 'hard' or t.type is null
I hope this works. Anyway the problem with your current query is not the left join (that is ok actually), but that you specify the tree type must be hard so you eliminate the rows with null tree that the left join gave you.
Does this work for you:
select p.id, p.plant, t.id as tree_id, t.type from plants p left join trees t ON (p.id = t.id) WHERE t.type = 'hard'
SELECT p.id, p.plant
FROM plants AS p
LEFT JOIN trees AS t ON p.id = t.id
WHERE t.type = 'hard'
OR t.type IS NULL