MySQL complex join for multiple inclusion and exclusion - mysql

I've got a client that wants me to make a tagging system. I've got three tables: items, tags, and item_tag_assoc - the last one only serves to associate item_ids and tag_ids. Here's the problem I'm having:
If the user requests included tags [1, 2, 3] and excluded tags [4, 5, 6], the result should be all items that have EVERY tag in [1, 2, 3] (not just one) and NO tags in [4, 5, 6]. How do I write a query to accomplish this?
I researched enough to figure out inner joins for the tag inclusion:
SELECT i.item_id, i.item_title FROM items AS i INNER JOIN tag_item_assoc AS tia1 ON (tia1.item_id = i.item_id AND tia1.tag_id = 1)
...and just chain on the same inner join pattern for as many tags as you want to include. It may be a little bulky, but users won't be choosing more than 4 or 5 tags before they move on, so it'll do.
I was really hoping that I could exclude the same way, and wrap everything into one query:
INNER JOIN tag_item_assoc AS tia2 ON (tia2.item_id = i.item_id AND tia2.tag_id!= 2)
But it became obvious very quickly that wasn't going to work. I read a couple of articles that said LEFT OUTER JOINs could let me exclude while I include, but I couldn't figure them out, mostly because of the stray WHERE clauses. Any permutation of LEFT OUTER JOINs and INNER JOINs I tried either yielded an error or very confusing results.
All that to say - does anyone here know how I can accomplish this? I apologize for not having any useful code examples to provide. I'm ok with starting from scratch if the INNER JOINs are an obstacle - I just need a way to accomplish multiple association inclusion and exclusion at the same time. Thanks in advance for the help and expertise!

SELECT a.*
FROM items a
INNER JOIN item_tag_assoc b
ON a.item_id = b.item_id
INNER JOIN tags c
ON a.tag_id = c.tag_id
WHERE c.tag_id IN(1) AND
c.tag_id IN(2) AND
c.tag_id IN(3) AND
c.tag_id NOT IN (4) AND
c.tag_id NOT IN (5) AND
c.tag_id NOT IN (6) ;

You need to match the number of instances of records to the number of your tags:
SELECT a.item_id -- , add other columns here
FROM items a
INNER JOIN item_tag_assoc b
ON a.item_id = b.item_id
WHERE b.tag_id IN (1, 2, 3) AND
b.tag_id NOT IN (4, 5, 6)
GROUP BY a.item_id
HAVING COUNT(a.item_id) = 3

Found the answer. Instead of adding INNER JOINs like so:
INNER JOIN tag_item_assoc AS tia2 ON (tia2.item_id = i.item_id AND tia2.tag_id!= 2)
I added a sub-query with a single inner join:
WHERE i.item_id NOT IN ( SELECT i.item_id FROM items AS s INNER JOIN tag_item_assoc AS tia1 ON (tia1.item_id = i.item_id AND tia1.tag_id IN (4, 5, 6)))
The sub-query creates a set of all items that have any of the tags in [4, 5, 6], and WHERE i.iten_id NOT IN eliminates any records that match that set.
Thanks everyone for the effort! Hopefully this will help someone down the road.

Related

LEFT JOIN ManyToMany multiple parameters

I have some problems with a MYSQL Query. I'm trying to do a LEFT JOIN with multiple parameters.
DB Structure:
A soldier can have more than one tag, these tags are assigned to the soldier with a ManyToMany relationship
My query is used in a search function where the user is able to add some tags he/she want to look for. So far its possible to search with one tag but as soon you add more than one tag, the query will not return an result even if it should.
Query used:
SELECT *
FROM soldiers s LEFT JOIN
soldier_tag st
ON s.id = st.soldier_id
WHERE st.tag_id = 5;
When the user enters 2 tags, then both tags should math. The query will become this:
SELECT *
FROM soldiers s LEFT JOIN
soldier_tag st
ON s.id = st.soldier_id
WHERE st.tag_id = 5 AND st.tag_id = 7;
Does anyone have an idea how I can fix this problem?
Thanks in advance
You will want to use 'IN' followed by the parameter as done below, this will consider both parameters when the query is executed.
SELECT *
FROM soldiers s LEFT JOIN
soldier_tag st
ON s.id = st.soldier_id
WHERE st.tag_id IN (5,7);
When a join condition is related to a set of values (eg a list of tag) You should use a in clause
SELECT *
FROM soldiers s
LEFT JOIN soldier_tag st ON s.id=st.soldier_id
WHERE st.tag_id in ( 5 , 7)
If you want all tags to match, I would suggest phrasing the query as something like this:
SELECT s.*
FROM soldiers s
WHERE s.id IN (SELECT st.soldier_id
FROM solder_tag st
WHERE st.tag_id IN (5, 7) -- construct an `IN` list instead of a bunch of boolean expressions
GROUP BY st.soldier_id
HAVING COUNT(*) = 2 -- 2 is the number of tags
);
The subquery returns the soldier_ids where all the tags match. You need to put the list in IN and the count in the HAVING clause.
You should use OR operator in your WHERE clause:
SELECT *
FROM soldiers s
LEFT JOIN soldier_tag st ON s.id=st.soldier_id
WHERE st.tag_id=5 OR st.tag_id=7
If it's possible to have more than 2 tags in query, it's better to use IN operator as #scaisEdge mentioned

Joining three tables together based on common ids

What I'm looking to generate is the wp_tylerposts table as it is, with the same specifications as the query (selecting only those with post_type='sponsor', but I'd like to reference the other two tables (object_id from wp_tylerterm_relationships and the corresponding name from wp_tylerterms) so that added on to wp_tylerpostswould be the corresponding name value from wp_tylerterms (added to the table would be "Gold Sponsors", for example).
Hopefully that makes sense. I'm sure there's a pretty simple solution, and I've tried my hand at some join queries without any luck... haven't done any of the stuff in a long time. Any help would be much appreciated!
Edit: I've come closer, I think, but I still can't retrieve the "name" column value from wp_tylerterms, here's what I have:
SELECT
c.ID, c.post_title, a.name
FROM
wp_tylerposts c
LEFT JOIN
wp_tylerterm_relationships b
ON
c.ID = b.object_id
LEFT JOIN
wp_tylerterms a
ON
b.object_id = a.term_id
WHERE
c.post_type = 'sponsor'
In second join, you use b.object_id instead of b.term_taxonomy_id. Your query should look like this:
SELECT
c.ID, c.post_title, a.name
FROM
wp_tylerposts c
LEFT JOIN
wp_tylerterm_relationships b
ON
c.ID = b.object_id
LEFT JOIN
wp_tylerterms a
ON
b.term_taxonomy_id = a.term_id
WHERE
c.post_type = 'sponsor'

Self-referencing SQL statement

Overview: I need to write an custom SQL question in ExpressionEngine.
I have a product table (exp_channel_titles) that is assigned categories (exp_category_posts). In EE categories are grouped (as defined in exp_categories).
Here's what I'm trying to do...
When I'm displaying a particular product (single row), I want to go back to the product table and pull random "suggestions" as based on the categories in a certain category group of the displayed product. Finally, I want to be sure to exclude the displayed product from the suggestions.
My SQL is ok but this is fairly advance in my book. I'm here hoping to get some helps.
Thus far I can get the single row and it's categories from the category group I'm interested in. But now what?
SELECT *
FROM exp_channel_titles
LEFT JOIN exp_category_posts
ON exp_channel_titles.entry_id = exp_category_posts.entry_id
LEFT JOIN exp_categories
ON exp_category_posts.cat_id = exp_categories.cat_id
WHERE exp_channel_titles.entry_id = "17" AND exp_categories.group_id = "3"
Thanks in advance for your help.
-- UPDATE --
I suppose this is brute force but it works, almost.
SELECT *
FROM exp_channel_titles my1
LEFT JOIN exp_category_posts my2
ON my1.entry_id = my2.entry_id
LEFT JOIN exp_category_posts my3
ON my1.entry_id = my3.entry_id
LEFT JOIN exp_category_posts my4
ON my1.entry_id = my4.entry_id
WHERE my3.cat_id = "7" AND my2.cat_id = "2" AND my1.entry_id != "17" AND my4.cat_id IN (
SELECT exp_category_posts.cat_id
FROM exp_category_posts
LEFT JOIN exp_categories ON exp_category_posts.cat_id = exp_categories.cat_id
WHERE exp_category_posts.entry_id = '17' AND exp_categories.group_id = '3'
)
Since I'm needing to filter by categories from 3 different category groups I do 3 LEFT JOINs. The first two are just focused on a single category (2=published, 7=music), the third could return multiple values from the subquery so I'm using IN. The subquest asked For entry_id = X (in this case 17, which is the main/single product being displayed) what categories from group_id=3 (music) should I be suggesting?
The final question is, since the subquery returned multiple rows, the main select can return dupes. I want a list of unique products. What now? Group by? Order by?
Pardon this nooby question but to be honest Google sucks when it comes to finding answers. There's tons of crap examples, unanswered questions, etc. I'm know the concepts, but my syntax is rusty. Please help a brother out.
If anyone see any easy way to optimize the above, I'll glad listen. Thank you again.
You can do this with native EE tags using related categories mode. Just make a new channel:entries loop outside of your main product channel:entries tag, or embed it if you have to.
I'd suggest embedding a template within your channel:entries loop, and passing the current entry_id as an embed parameter.
{embed="embeds/related_products" entry_id="{entry_id}"}
Then in your embedded template:
{exp:channel:entries channel="products" category_group="3|4" entry_id="not {embed:entry_id}" limit="5" orderby="random"}
// variables here
{/exp:channel:entries}
DISCLAIMER: Not sure if this will work, and it could very likely do with some optimization, but perhaps this will work:
SELECT DISTINCT ect.*
FROM
exp_channel_titles ect
LEFT JOIN exp_category_posts ecp ON ect.entry_id = ecp.entry_id
LEFT JOIN exp_categories ec ON ecp.cat_id = ec.cat_id
WHERE
ect.entry_id != '17'
AND ec.group_id IN (
SELECT ec.group_id
FROM
exp_channel_titles ect
LEFT JOIN exp_category_posts ecp ON ect.entry_id = ecp.entry_id
LEFT JOIN exp_categories ec ON ecp.cat_id = ec.cat_id
WHERE
ect.entry_id = '17'
);

Messy self-join Mysql SELECT

I need to return a list of product id's that are...
Within a specific category (for example 'Clothing')
Which have various attributes, such as 'Red' or 'Green'
Which are themselves within attribute 'groups' such as 'Color'
I'm getting stuck when I need to select MULTIPLE attribute options within MULTIPLE attribute groups. For example, if I need to return a list of products where Color is 'blue' OR 'red' AND size is 'Medium' OR 'XXL'.
This is my code:
SELECT `products.id`
FROM
`products` ,
`categories` ,
`attributes` att1,
`attributes` att2
WHERE products.id = categories.productid
AND `categories.id` = 3
AND att1.productid = products.id
AND att1.productid = att2.productid
AND
(att1.attributeid = 58 OR att1.attributeid = 60)
AND
(att2.attributeid = 12 OR att2.attributeid = 9)
I believe this code works, but It looks pretty messy and I'm not sure my 'dirty' self-join is the correct way to go. Has anyone got any ideas on a more 'elegant' solution to my problem?
Please use the modern join syntax:
SELECT products.id
FROM products
join categories on products.id = categories.productid
join attributes att1 on att1.productid = products.id
join attributes att2 on att1.productid = att2.productid
WHERE categories.id = 3
AND att1.attributeid IN (58, 60)
AND att2.attributeid IN (12, 9)
It's easier to read because it clearly demarques join conditions from row filtering conditions. It's also easier for the SQL optimizer to identify these distinctions and create better query plans
Edited
I alsp added the use of IN (...). Not only does it look nicer, the DB wil use an index with IN but usually not with OR, even though they mean the same thing
SELECT p.id
FROM products p
JOIN categories c ON c.productid = p.id
JOIN attributes a1 ON a1.productid = p.id
JOIN attributes a2 ON a2.productid = p.id
WHERE categories.id = 3
AND a1.attributeid IN (58, 60)
AND a2.attributeid IN (12, 9)
I think you had a mistake where you join the second attribute to the first attribute instead of joining it to the product. I fixed that.
On second thought, this may be intentional, and my correction wrong. It is a messy design, though, to mix attributes with attribute groups in the same table.
I also simplified your syntax and use explicit JOINs which are more readable.

SQL: Multiple matches towards same table?

Probably a very basic solution to this, but I can't seem to figure it out. I'm trying to match
multiple ID's in one table towards another.
Structure is something like this,
tt_staff (ID - name)
2 - Lenny
3 - Carl
tt_run (producer1 - producer2)
2 (i.e Lenny)
3 (i.e Carl)
I want to create a view with a single row that shows me the names of both Carl and Lenny, rather then their ID's.
Tried the following,
SELECT e.*, run.*, s.s_name FROM tt_run AS run, tt_events AS e, tt_staff AS s
WHERE e.e_ID = run.e_ID
AND run.e_bandproducer1 = s.s_ID
AND run.e_bandproducer2 = s.s_ID
This obviously doesn't work, since the ID is found on producer1. I've also tried with UNION, but not familiar enough with it (I did manage to get the right result, but in two rows).
As always, thanks for any replies.
It looks like you just need to join tt_staff twice:
SELECT e.*, r.*, s1.s_name, s2.s_name
FROM tt_events e
INNER JOIN tt_run r ON e.e_ID = r.e_ID
INNER JOIN tt_staff s1 ON r.e_bandproducer1 = s1.s_ID
INNER JOIN tt_staff s2 ON r.e_bandproducer2 = s2.s_ID