I have three simple tables:
Items
ItemID (int, PK)
ItemName (nvarchar50)
ItemCost (int)
Tags
TagID (int, PK)
TagName (nvarchar50)
ItemTags
ItemID (int, FK->Items)
TagID (int, FK->Tags)
How can I write a query to the effect of 'find all items tagged with tag1 and tag2, but not tag3'?
Do I need to use a completely different schema?
I like to do this with GROUP BY
and HAVING
:
SELECT it.ItemId
FROM ItemTags it JOIN
Tags t
ON t.TagId = it.TagId
GROUP BY it.ItemId
HAVING SUM(CASE WHEN t.TagName = 'Tag1' THEN 1 ELSE 0 END) > 0 AND
SUM(CASE WHEN t.TagName = 'Tag2' THEN 1 ELSE 0 END) > 0 AND
SUM(CASE WHEN t.TagName = 'Tag3' THEN 1 ELSE 0 END) = 0;
Each condition in the HAVING
clause checks for one tag. The first two use > 0
to say the tag must exist for the item. The third uses = 0
to say the tag must not exist for the item.
You can use OUTER APPLY:
SELECT i.*
FROM Items i
OUTER APPLY (
SELECT COUNT(*) as TagsCount
FROM ItemTags it
INNER JOIN Tags t
ON t.TagID = it.TagID
WHERE i.ItemID = it.ItemID
AND t.TagName IN ('tag1','tag2')
) as tt
WHERE TagsCount = 2
At first we get all ItemID
's with counted TagsID
's. Then join with Items
tables with filtering only those who have TagsCount = 2
EDIT#1
Add a sample:
;WITH Items AS (
SELECT *
FROM (VALUES
(1,'Item1',100),(2,'Item2',50),(3,'Item3',90),(4,'Item4',63),(5,'Item5',75)
)as t(ItemID,ItemName,ItemCost)
)
, Tags AS (
SELECT *
FROM (VALUES
(1,'tag1'),(2,'tag2'),(3,'tag3'),(4,'tag4'),(5,'tag5')
) as t(TagID, TagName)
)
, ItemTags AS (
SELECT *
FROM (VALUES
(1,1),(1,2), --This
(2,1),(2,2),(2,3), --and that records we need to get
(3,1), (3,3),(3,4),
(4,2), (4,5),
(5,1)
) as t(ItemID, TagID)
)
SELECT i.*
FROM Items i
CROSS APPLY (
SELECT COUNT(*) as TagsCount
FROM ItemTags it
INNER JOIN Tags t
ON t.TagID = it.TagID
WHERE i.ItemID = it.ItemID
AND t.TagName IN ('tag1','tag2')
HAVING COUNT(*) = 2
) as tt
Output:
ItemID ItemName ItemCost
1 Item1 100
2 Item2 50
EDIT#2
If you want to filter items with no tag3
tag present you can add left join.
SELECT i.*
FROM Items i
CROSS APPLY (
SELECT COUNT(*) as TagsCount
FROM ItemTags it
INNER JOIN Tags t
ON t.TagID = it.TagID
WHERE i.ItemID = it.ItemID
AND t.TagName IN ('tag1','tag2')
HAVING COUNT(*) = 2
) as tin
LEFT JOIN (
SELECT it.Itemid
FROM ItemTags it
INNER JOIN Tags t
ON t.TagID = it.TagID
WHERE t.TagName IN ('tag3')
) tnot
ON tnot.Itemid = i.itemid
WHERE tnot.ItemId is NULL
If yo want to filter by some tags you can use temporary table with items that has one tags, and has tags you don't need and then join them. Also dynamic SQL may be an option.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With