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 ?)
Related
I'm trying to build a search function but the current table structure troubles me.
So each row identifies a 'tag' association with an audio ID.
TABLE audio_tag_assoc example
I get the name of each tag id by joining the table tag_association.
TABLE tag_association example
Therefore I need to get all the audio ids that match two or even three tag names.
What I tried is the following but you can probably tell that it doesn't work. I would have later added a join in between those parentheses to change the IDs of the IN with strings.
SELECT *
FROM audio_tag_assoc a
JOIN tag_association b ON a.tag = b.id
WHERE a.audio = (SELECT *
FROM audio_tag_assoc
WHERE tag IN (2,3)
)
Initially I tried having b.name = 'Male' AND b.name = 'Film' but of course, that's not how mysql WHERE clause works.
Do :
SELECT
*
FROM
audio_tag_assoc
INNER JOIN
tag_association ON audio_tag_assoc.tag = tag_association.id
WHERE
audio_tag_assoc.id IN (SELECT
a.id
FROM audio_tag_assoc a
WHERE
a.tag IN (2,3)
GROUP BY
a.audio
HAVING COUNT(a.id) >= 2
);
I have a MySQL database with a media table, and a keywords table, and a many-to-many relationship between media and keywords via a media_keywords join table.
I want to fetch all records from the media table where the following set of conditions match:
'description' is like 'dog' OR
'media.keywords' includes the id for the 'dog' keyword [100]
And exclude from the found set any records where:
'description' is like 'cat' OR
'media.keywords' includes the id for the 'cat' keyword [400]
And also exclude any row where:
'media.keywords' includes the id for the 'monochrome' keyword [500]
I also want to return only distinct rows, so I'm using GROUP By 'media.id'
The SQL statement I have at the moment is as follows:
SELECT DISTINCT
`media`.`id`,`media`.`description`,
`keywords`.`id` AS `keywords.id`,
`keywords->media_keywords`.`id` AS `keywords.media_keywords.id`,
`keywords->media_keywords`.`media_id` AS `keywords.media_keywords.media_id`,
`keywords->media_keywords`.`keyword_id` AS `keywords.media_keywords.keyword_id`
FROM database.media
LEFT OUTER JOIN
(
`media_keywords` AS `keywords->media_keywords`
INNER JOIN `keywords` AS `keywords`
ON `keywords`.`id` = `keywords->media_keywords`.`keyword_id`
)
ON `media`.`id` = `keywords->media_keywords`.`media_id`
WHERE
(
(`media`.`description` LIKE '%dog%' )
OR `keywords`.`id` IN (100)
)
AND NOT
(
(`media`.`description` LIKE '%cat%' )
OR `keywords`.`id` IN (400,500)
)
GROUP BY `media`.`id` ;
This correctly fetches records where 'dog' is in the description or is a keyword, but ignores the exclusions completely.
Can anyone see what I'm doing wrong here?
I would use a where clause:
select m.*
from media m
where (m.description like '%dog%' or
exists (select 1
from keywords k
where k.media_id = m.id and
k.keyword_id = 100
)
) and
(m.description not like '%cat%' or
exists (select 1
from keywords k
where k.media_id = m.id and
k.keyword_id in (400, 500)
)
);
This is pretty much a direct translation of your conditions.
I don't work with mySQL much, but I would suggestion a different approach
Move the "DOG" condition inside the LEFT JOIN (so only get keywords
matching "DOG") and make it a JOIN. Now you'll have a list of all
matches.
add a subquery in the WHERE clause
WHERE id not in (SELECT id FROM ... WHERE LIKE '%cat%')
I have a table called documents with a row called Nigel Harding
DOCUMENTS
id | label
24 | Nigel Harding
He has been tagged with two other documents one with an ID of 1 & 12 that table is called document tags
DOCUMENT_TAGS
id | label | Document_id
1 | TAG A | 24
12 | TAG B | 24
I am trying to create a query where I can find one result where Nigel Harding will appear once if searching for the tags 1 AND 12 but i'm having no luck.
I figured out the query for searching one tag id but i'm trying to do the query for both tags.
SELECT documents.id
FROM documents
LEFT JOIN documents_tags
ON documents.id=documents_tags.document_id
WHERE documents_tags.tag_id = 1 ORDER BY documents.label
I understand why adding...
AND documents_tags.tag_id = 12
...to the end of that will not work but i'm not sure what i need to do get the correct query display my one result as my understanding of SQL is very basic.
If I understand your question, you're just lacking IN and DISTINCT:
SELECT DISTINCT documents.id
FROM documents
LEFT JOIN documents_tags
ON documents.id=documents_tags.document_id
WHERE documents_tags.tag_id IN (1, 12) ORDER BY documents.label
Using IN you'll be getting documents for any of the tag ids specified, and using DISTINCT you'll make sure to get each document id only once.
EDIT: Since you're ordering by documents.label, I'm guessing that's what you want displayed. Don't forget you can change your SELECT statement to:
SELECT DISTINCT documents.label
or even to:
SELECT DISTINCT documents.id, documents.label
I think the most flexible way to approach this type of problem is to use aggregation with a having clause. Here is one example:
SELECT dt.document_id
FROM documents_tags dt
GROUP BY dt.document_id
HAVING sum(dt.tag_id = 1) > 0 and
sum(dt.tag_id = 12) > 0;
Each condition in the having clause counts the number of document tags that are 1 (or 12) and the filter passes only when both are found. You could also write this as:
SELECT dt.document_id
FROM documents_tags dt
WHERE dt.tag_id in (1, 12)
GROUP BY dt.document_id
HAVING count(distinct dt.tag_id) = 2;
Another way to accomplish this (adjusted to search by tag label which I think is your goal, if not just replace dt.Label = 'etc' with dt.ID = etc):
SELECT documents.id
FROM documents d
WHERE
exists (select dt.id from documents_tags dt
where dt.document_id = d.document_id and dt.label = 'TAG A')
AND
exists (select dt.id from documents_tags dt
where dt.document_id = d.document_id and dt.label = 'TAG B')
one WHERE word is enough:
SELECT documents.id
FROM documents
LEFT JOIN documents_tags
ON documents.id=documents_tags.document_id
WHERE (documents_tags.tag_id = 1 OR documents_tags.tag_id = 12) ORDER BY documents.label
alternatively, if you are going to work with n different tags and don't want to add OR documents_tags.tag_id = x ever time you can use IN operator
SELECT documents.id
FROM documents
LEFT JOIN documents_tags
ON documents.id=documents_tags.document_id
WHERE documents_tags.tag_id IN(1,12) ORDER BY documents.label
It sounds like you're trying to aggregate the results when you search for multiple document tags, so you should use GROUP BY, which is for grouping multiple rows into one row:
SELECT documents.id
FROM documents
LEFT JOIN documents_tags ON documents.id = documents_tags.document_id
WHERE documents_tags.tag_id IN (1, 12)
GROUP BY documents.id
This will give you one row for each document that matches the given tag ids. And you can generalize to any list of tag ids by adding to the list without too much pain. You could even add a COUNT(*) to the select statement to find out how many tag ids where matched for the given document.
my tables look like this:
tags: id, name, description
tag_relations: tag, item
item references the id of another table and tag references the id of the tags table.
So I'm trying to select the most used tags:
SELECT t.*, COUNT(r.item) AS item_count
FROM tag_relations as r
INNER JOIN tags as t ON r.tag = t.id
GROUP BY t.id
ORDER BY item_count
which works, but if I add
WHERE t.id = ?
the item_count is always 1...
Is there any way I could still have the global tag count with a select statement that selects only 1 tag or a specific set of tags?
Sql fiddle at
http://www.sqlfiddle.com/#!2/ba97d/1
SELECT name,count(item) as counter_item
FROM tag_relations
INNER JOIN tags ON
tag_relations.tag =tags.id
order by counter_item
the line
where tags.id=1
Can be added if needed
I don't have access to MySQL, but I do have access to Microsoft SQLServer. I realize your tags specify mysql. Even so, the query you presented fails in SQLServer with error
Msg 8120, Level 16, State 1, Line 1
Column 'tags.name' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
... because the select t.* is not included in the group by clause.
Anyways, to address your specific question you can derive a global number while still selecting a specific record using cross join...
select
t.*
, vTagRelations.GlobalCountOfTagRelations
, vTags.GlobalCountOfTags
from
tags t
cross join (select
count(tag_relations.tag) as GlobalCountOfTagRelations
from
tag_relations) vTagRelations
cross join (select
count(tags.id) as GlobalCountOfTags
from
tags) vTags
where
t.id = 2
In SQLite, using sub-query:
SELECT *, (SELECT COUNT() FROM tag_relations WHERE tag=tags.id) AS item_count FROM tags WHERE id=?;
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...