MySQL foreign key multi join - mysql

I have this table setup (simplified):
ads
- id
- brand_id
brands
- id
tags
- taggable_type
- taggable_id
- tag
The taggable_type in the tags table will be either "Ad" or "Brand", the taggable_id will identify the ad_id or brand_id respectively, and then the tag is a string.
Ads inherit their brand's tags, and I'm trying to write a search query that pulls up a set of ads for a given set of tags, where those tags belong to either the ad itself or the brand associated with that ad.
These tables can all be fairly large, so I need it to be efficient.
Here's what I have (this isn't working)
SELECT
a.*
FROM
ads a
JOIN
((
SELECT
*
FROM
tags
WHERE
tag IN ({$tags})
AND taggable_type = "Ad"
) t FULL JOIN (
SELECT
*
FROM
tags
WHERE
tag IN ({$tags})
AND taggable_type = "Brand"
)) tt
ON (t.taggable_id = a.id) OR (tt.taggable_id = a.brand_id);
For starters, I'm getting an error on the full join. I've also tried an inner join and a left join, and it's still not working. I feel like I'm just doing something fundamentally stupid. Any help?

Like this it should work
SELECT DISTINCT
ads.*
FROM
ads
LEFT JOIN brands
ON ads.brand_id = brands.id
INNER JOIN tags
ON (
tags.taggable_type = 'brand'
AND brands.id = tags.taggable_id)
OR (
tags.taggable_type = 'ad'
and ads.id = tags.taggable_id)
WHERE
tags.tag IN ('tag 1', 'tag 2', 'tag 7')
SQL Fiddle
But you might also want to think about your database structure again.
Perhaps a setup like the following would suit you better.
ads(id, brand_id)
brands(id)
tags(id)
tags_ads(tag_id, ad_id)
tags_brands(tag_id, brand_id)
Would have the benefit of being able to assign a tag to more than one brand or ad. Or to a brand and an ad...

Related

SQL SELECT and GROUP

Here is my problem:
SELECT *
FROM posts
JOIN tags
ON tags.did = posts.did
JOIN users
ON users.username = posts.username
GROUP by tags.did
Now the result only shows one tag when I have two tags for the did. I want it to show all the tags I have for it kinda like this
a post | tag 1, tag 2
but right now, it's showing it like this
a post | tag 1
And I don't know why it won't show the other tag. I hope this is enough relevant information. (Pardon me, I'm a bit of a SQL beginner right now)
TRY (essential part, not tested )
SELECT p.postid, GROUP_CONCAT(t.tags)
FROM posts p
INNER JOIN tags t ON t.did = p.did
INNER JOIN users u ON u.username = p.username
GROUP by t.did
Pseudocode
SELECT post.Text, Coalesce('',select tag +',' from tags where tags.did=posts.did)
FROM posts
or us Group_concat
group concat
SELECT GROUP_CONCAT(Language) As Languages FROM CountryLanguage WHERE CountryCode = 'THA'
I dont have a mysql to test against but you need something like this.My Sql Coalsce

Custom site search by category

I want to create a simple search for a site that I'm working on. I have items in my db that all hold a specific category id and can optionally be linked to multiple tags.
I would like to take whatever search terms come in and query the category.name and tag.name fields to find the items that match those terms.
I'm looking for advice on how to create an efficient/quick query that does this AND orders the results by the items that match closest(most matches)
Here's a quick version of my relevant tables:
item
id | category | title | description
category
id | name | parentId
tag
id | name | uses
item_tag
itemId | tagId
I still didn't entirely understand what you want.
Well, here's a first version for us to discuss.
I suggest you to create a view as the following:
CREATE OR REPLACE VIEW `search_view` AS
SELECT
i.id AS `item_id`,
i.title AS `item_title`,
c.id AS `cat_id`,
c.name AS `cat_name`,
t.id AS `tag_id`,
t.name AS `tag_name`
FROM item AS i
LEFT OUTER JOIN item_tag AS it
ON (i.id = it.itemId)
LEFT OUTER JOIN tag AS t
ON (it.tagId = t.id)
LEFT OUTER JOIN category AS c
ON (i.category = c.id)
WHERE ((t.id IS NOT NULL) OR (c.id IS NOT NULL));
And then you query from the view using something near
SELECT
*,
(
IF(tag_name like ?, 2, 0)
+ IF(cat_name like ?, 4, 0)
+ IF(item_title like ?, 1, 0)
) AS `priority`
FROM search_view
GROUP BY item_id
ORDER BY SUM(priority);
No tests in the code above. Report any problems you have
[first edition] You use PHP, don't you?
Well, you can normalize your queries using PHP string functions; one way is to replace every occurrence of ',' by '|', remove extra spaces and perform the following query: [I'll give an example using #VAR (actually you'll replace it with your input string)]
SET #VAR = 'notebook|samsung';
SELECT
*,
(
IF (tag_name REGEXP CONCAT('.*(', #VAR, ').*'), 2, 0)
+ IF (cat_name REGEXP CONCAT('.*(', #VAR, ').*'), 4, 0)
+ IF (item_title REGEXP CONCAT('.*(', #VAR, ').*'), 1, 0)
) AS `priority`
FROM search_view
ORDER BY priority DESC;
This time I tested. Yes, you can use MySQL functions, something about REPLACE(REPLACE(#VAR,' ',''), ',', '|'). But I recommend you to do it in PHP (or java, python etc).
SELECT item.*
FROM items
LEFT JOIN categories
ON categories.name = '< input name >'
AND categories.id = items.category
LEFT JOIN item_tags
AND item_tags.itemId = items.id
LEFT JOIN tags
ON tags.name = '< input name >'
AND tags.id = item_tags.tagId
WHERE categories.id IS NOT NULL
OR tags.id IS NOT NULL
ORDER BY COUNT(items.id) DESC;
This may not be the fastest way. Basically you'll left join categories and tags to items while making sure the category and tag have the correct name. Filter out all items that don't match a category or an item_tag in the where clause.
Another alternative would be to create temporary tables. Create one for all categories with the correct name, and one with all tags with the correct name. You could then SELECT items WHERE items.id IN categories_table OR items.id IN tags_table

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.

Mysql Tag Query - Repeated values

I'm working on a tag-based search. The user can search one tag or multiple tags. I've got 3 tables: Content, Content_Tags and Tags. Content_Tags does the link between content and tags. The query has to return all the information of the content retrieved, this includes a concatenated string of all tags from that content.
Using an example from other question I've managed to reach this query:
SELECT content.Name,GROUP_CONCAT(t2.Tag SEPARATOR ' ') FROM content
JOIN content_tags ct1 ON content.ContentID = ct1.ContentID
JOIN tags t1 on ct1.TagID = t1.TagID AND t1.Tag IN('grass','texture')
JOIN content_tags ct2 ON ct2.ContentID = content.ContentID JOIN tags t2 ON ct2.TagID = t2.TagID GROUP BY content.ContentID;
The query works fine when searching for 1 tag, but using IN('grass','texture') the GROUP_CONCAT will return 'grass grass texture texture' (repeating the tags).
I don't know MySQL this deep, how could I fix this problem?
SELECT content.name,
(
SELECT GROUP_CONCAT(cta.tag)
FROM content_tags cta
WHERE cta.contentID = ct.contentID
)
FROM content_tags ct
JOIN content c
ON c.contentId = ct.contentId
WHERE ct.tag IN ('grass', 'texture')
GROUP BY
ct.contentId
HAVING COUNT(*) = 2

Select from table1 WHERE table2 contains ALL search parameters

I have two tables (notes and tags). Tags has a foreign key to notes. There may be several tag records to a single note record.
I'm trying to select only the notes that contain all of the desired tags.
SELECT notes.*, tags.* FROM notes LEFT JOIN tags ON notes.id = tags.note_id
WHERE {my note contains all three tags I would like to search on}
Using WHERE tag.name IN ('fruit','meat','vegetable') will bring back all the notes that have a "fruit", "meat", OR "vegetable" tag. I only want to return notes that have all three "fruit", "meat", AND "vegetable" tags.
I'm ok to bring back multiple records (the query above would yield a record for each tag).
I need help with my where clause. Is it possible to do this without a sub-select?
Assuming tags(note_id, tag) is declared UNIQUE or PK, then you can use:
SELECT note_id, COUNT(tag) FROM tags
WHERE tag IN ('fruit', 'vegetable', 'meat')
GROUP BY note_id
HAVING COUNT(tag) >= 3
Further answer based on OP's comment below. To get all tags for the records that match:
SELECT * FROM tags
INNER JOIN
(
SELECT note_id, COUNT(tag) FROM tags
WHERE tag IN ('fruit', 'vegetable', 'meat')
GROUP BY note_id
HAVING COUNT(tag) >= 3
) search_results
ON search_results.note_id = tags.note_id
Without a subselect, as per request:
SELECT notes.*
FROM notes
JOIN tags
ON tag.note = notes.id
AND tag.name IN ('fruit','meat','vegetable')
GROUP BY
notes.id
HAVING COUNT(*) = 3
More efficient method would be:
SELECT notes.*
FROM (
SELECT to.note
FROM tags to
WHERE to.name = 'meat'
AND EXISTS
(
SELECT NULL
FROM tags ti
WHERE ti.note = to.note
AND to.name IN ('fruit', 'vegetable')
LIMIT 1, 1
)
) t
JOIN notes
ON note.id = t.note
The trick here is to put the search on the most selective tag ('meat' in my example) on the first place.
If it is not too late, wouldn't it be better to have a NoteTag table - so you will have notes, tags, notetag tables and you can use simple queries and AND operator to find what you want ?)