SQL Multiple Left joins - mysql

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.

Related

How can I list all rows in table and count from multiple table that related with each rows?

I have made Q & A website that a questions can have tags in it and the user can subscribe to tags they want. So, I have create tag page for list all tags and count all questions and users who subscribed to these tags.I found similar question here but it more complex for me.
The code below is SQL I'm trying and it seems very slow for query times. I want SQL below is to write by using JOIN/LEFT JOIN/RIGHT JOIN I think it will be faster. But I have no idea.
SQL
select tag.*,
(select count(*) from taggedquestion where taggedquestion.tagid = tag.id) questionCount,
(select count(*) from tagsubscription where tagsubscription.tagid = tag.id) userCount
from tag order by tag.id asc
Tags page
Tag name question count user count
----------------------------------------------------------------
computer 12 5
science 10 3
travel 6 2
programing 18 3
... ... ...
... ... ...
This is your query:
select tag.*,
(select count(*) from taggedquestion where taggedquestion.tagid = tag.id and ids = '0'
) as questionCount,
(select count(*) from tagsubscription where tagsubscription.tagid = tag.id and ids = '0'
) as userCount
from tag
order by tag.id asc;
This is a reasonable query. Probably what you need for performance are the following indexes (if you don't already have them):
taggedquestion(tagid, ids)
tagsubscription(tagid, ids)
tag(id)
You probably already have the last one, because id is presumably a primary key on the tag table.
On your second posted query, you are missing SELECT and so getting the posted error. Your second query should look like below
SELECT tag.*,
( SELECT COUNT(*) FROM taggedquestion
LEFT JOIN tag ON tag.id = taggedquestion.tagid
and taggedquestion.ids = '0') AS postCount,
(SELECT COUNT(*) FROM tagsubscription
LEFT JOIN tag ON tag.id = tagsubscription.tagid
and tagsubscription.ids = '0') AS userCount
FROM tag
Try like this then using SUM with CASE condition. Since you haven't shown your tables structure or sample data; can't say for sure but you might want to group by the taggedquestion.ids like
SELECT tag.*,
( SELECT SUM(CASE WHEN taggedquestion.ids = '0' THEN 1 ELSE 0 END)
FROM taggedquestion
INNER JOIN tag
ON tag.id = taggedquestion.tagid
GROUP BY taggedquestion.ids) AS postCount,
(SELECT SUM(CASE WHEN tagsubscription.ids = '0' THEN 1 ELSE 0 END)
FROM tagsubscription
INNER JOIN tag
ON tag.id = tagsubscription.tagid
GROUP BY tagsubscription.ids) AS userCount
FROM tag

Select records that match a field, but order them by a different field

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=?;

Comparing two values from the same select query

I have a select query which selects all products from my inventory table and joins them with two other tables (tables l_products and a_products)
SELECT
i.*,
b.title,
ROUND((i.price/100*80) - l.price,2) AS margin,
l.price AS l_price,
a.price AS a_price,
ROUND((a.price/100*80) - l.price, 2) AS l_margin
FROM inventory i
LEFT JOIN products b ON i.id = b.id
LEFT JOIN a_products a ON i.id = a.id
LEFT JOIN l_products l ON i.id = l.id
WHERE
a.condition LIKE IF(i.condition = 'New', 'New%', 'Used%')
AND l.condition LIKE IF(i.condition = 'New', 'New%', 'Used%')
This select query will normally give me a table such as...
id, title, condition, margin, l_price, a_price ...
001-new ... new 10 20 10
001-used ... used 10 25 20
002....
Now I need a condition in the query which will ignore all used products that are more expensive (have a higher a_price) than their 'new' counterparts, such as in the example above you can see that 001-used has a higher a_price than 001-new.
How can I achieve this with out having to resolve to using php
FULL JOIN this query with it self on a column which has a uniquely same value for each id prefix.
You may achieve this effect by adding another field to your SELECT call which produces same unique value for 001-new and 001-used, 002-new and 002-used...
Such value generation can be done by defining your own SQL Routine to extract first 3 characters from a column.

Select parents based on all children statisfying condition

This is such a simple problem but for some reason I cannot get my head round it today.
I have two entities:- title and product each respectively named tbl_title and tbl_product. Each title can have many products.
The product table has a field called unwanted which can be either null, 0 or 1.
I wish to select all titles based on where all products (ALL) have unwanted set to 1. So in other words I wish to select the parent based upon all children filling a certain condition. So if a title has one product that is unwanted but another that is not I do not wish for this title to enter the result set.
When I try this the most I get out of my head is:
SELECT * FROM `tbl_title`
left join tbl_product on tbl_product.title_id = tbl_title.id
where tbl_product.unwanted = 1
group by tbl_title.id
Which obviously does not work.
So how do I code such a query?
select * from tbl_title
where id not in (select title_id from tbl_product where unwanted = 0)
In English, this query eliminates all titles that have a wanted product.
From a style point of view, it would be better to call your column wanted, because unwanted = 0 is a double-negative of wanted = 1. It's always easier to get your head around positives.
SELECT t.id
FROM `tbl_title` t
left join tbl_product p on p.title_id = t.id
group by t.id
having sum(p.unwanted = 0 or p.unwanted is null) = 0
Try using a subquery like this:
SELECT * FROM `tbl_title` AS t
WHERE EXISTS (SELECT 1 FROM products WHERE title_id = t.id AND unwanted = 1)
AND NOT EXISTS (SELECT 1 FROM products WHERE title_id = t.id AND (unwanted = 0 OR unwanted IS NULL))
Just for the fields in title table
SELECT *
FROM `tbl_title` AS t
JOIN tbl_product AS v ON t.id = v.title_id
WHERE NOT EXISTS(
SELECT *
FROM tbl_product
WHERE (t.id = title_id)
AND (unwanted = 0 OR unwanted IS NULL)
GROUP BY t.id

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 ?)