How to compare n:m assignments? - sql

I have two tables (entity and kind) plus a n:m table (entity_kind).
CREATE TABLE
entity
(
entity_id INT
, name NVARCHAR(100)
, PRIMARY KEY(entity_id)
)
CREATE TABLE
kind
(
kind_id INT
, name NVARCHAR(100)
, PRIMARY KEY(kind_id)
)
CREATE TABLE
entity_kind
(
entity_id INT
, kind_id INT
, PRIMARY KEY(entity_id, kind_id)
)
Test data:
INSERT INTO
entity
VALUES
(1, 'Entity A')
, (2, 'Entity B')
, (3, 'Entity C')
INSERT INTO
kind
VALUES
(1, 'Kind 1')
, (2, 'Kind 2')
, (3, 'Kind 3')
, (4, 'Kind 4')
INSERT INTO
entity_kind
VALUES
(1, 1)
, (1, 3)
, (2, 1)
, (2, 2)
, (3, 4)
My code so far:
DECLARE
#selected_entities
TABLE
(
entity_id INT
)
DECLARE
#same_kinds BIT;
INSERT INTO
#selected_entities
VALUES
(1), (2)
-- Missing code here
SELECT
#same_kinds AS "same_kinds"
The table var #selected_entities is filled with entities that should be compared.
The logical var #same_kinds should indicate whether the selected entities have exactly the same kinds assigned.
How can I achieve this?

This is a compare two sets of things type problem. The query I'm going to show gives all pairs along with a flag. You can easily incorporate comparing a subquery by changing the first two entity tables to the table of ids you want to compare.
This query has a few parts. First, it produces all pairs of entities from the entity tables. This is important, because this will pick up even entities that have no "kinds" associated with them. You want a flag, rather than just a list of those that match.
Then the heart of the logic is to do a self-join on the entity-kinds table with the match on "kind". This is then aggregated by the two entities. The result is a count of the kinds that two entities share.
The final logic is to compare this count to the count of "kinds" on each entity. If all of these counts are the same, then the entities match. If not, they do not. This approach does assume that there are no duplicates in entity_kinds.
select e1.entity_id as e1, e2.entity_id as e2,
(case when count(ek1.entity_id) = max(ek1.numkinds) and
count(ek2.entity_id) = count(ek1.entity_id) and
max(ek1.numkinds) = max(ek2.numkinds)
then 1
else 0
end) as IsSame
from entity e1 join
entity e2
on e1.entity_id < e2.entity_id left outer join
(select ek.*, count(*) over (partition by entity_id) as numkinds
from entity_kind ek
) ek1
on e1.entity_id = ek1.entity_id left outer join
(select ek.*, count(*) over (partition by entity_id) as numkinds
from entity_kind ek
) ek2
on e2.entity_id = ek2.entity_id and
ek2.kind_id = ek1.kind_id
group by e1.entity_id, e2.entity_id;
The SQL Fiddle is here.

You can do this with two checks: First, if the kind-count on each entity is not the same, then they cannot match. Second, provided the count is the same, you just need to find one kind that doesn't match the list of an arbitrary other entity (I just take the first entity in the compare list). In code:
DECLARE #firstEntity int = (SELECT TOP 1 entity_id from #selected_entities)
IF EXISTS(SELECT TOP 1 se.entity_id FROM #selected_entities se
INNER JOIN entity_kind ek ON ek.entity_id = se.entity_id
WHERE ek.kind_id NOT IN (SELECT kind_id from entity_kind where entity_id = #firstEntity)
OR ((SELECT COUNT(1) FROM entity_kind WHERE entity_id = ek.entity_id)
<> (SELECT COUNT(1) FROM entity_kind WHERE entity_id = #firstEntity)))
SET #same_kinds = 0
ELSE
SET #same_kinds = 1

DECLARE #first_entity_id INT;
SET #first_entity_id = (SELECT TOP(1) se.entity_id FROM #selected_entities se);
DECLARE #dummyvar INT;
SELECT DISTINCT #dummyvar = COUNT(ek.kind_id)
FROM dbo.entity_kind ek
LEFT JOIN (
SELECT ek.kind_id
FROM dbo.entity_kind ek
WHERE ek.entity_id = #first_entity_id
) k ON ek.kind_id = k.kind_id
WHERE ek.entity_id IN (SELECT se.entity_id FROM #selected_entities se)
GROUP BY ek.entity_id;
SET #same_kinds = CASE WHEN ##ROWCOUNT = 1 THEN 1 ELSE 0 END;
SELECT #same_kinds AS [#same_kinds];
Note: #selected_entities should be declared thus:
DECLARE
#selected_entities
TABLE
(
entity_id INT PRIMARY KEY
)

Related

Get the number of rows per tenant of several tables

Let's say I have 2 tables: Tenants and Wargles. Wargles has a Foreign Key towards Tenants called TenantId. If I want to get number of wargles per tenant, I can do this:
SELECT t.Id as TenantId, count(w.Id) as WargleCount
FROM Tenants t
JOIN Wargles w ON w.TenantId = t.Id
GROUP BY t.Id
Now, let's say I have another table, Fiddles, that, as Wargles has a FK towards Tenants. How can I add another column to the query above, so I get the number of wargles and the number of fiddles for each tenant?
I tried with this:
SELECT t.Id as TenantId, count(w.Id) as WargleCount, count(f.Id) as FiddleCount
FROM Tenants t
JOIN Wargles w ON w.TenantId = t.Id
JOIN Fiddles f ON f.TenantId = t.Id
GROUP BY t.Id
But this won't work, since it would give me the same number both for WargleCount and FiddleCount, the product of the rows from both tables.
Use two subselects
SELECT t.Id as TenantId,
(SELECT Count(1) FROM Fiddles F WHERE F.TenantId = T.Id) as FiddleCount,
(SELECT Count(1) FROM Wargles W WHERE W.TenantId = T.Id) as WargleCount
FROM Tenants t
The most efficient method is probably to use correlated subqueries:
SELECT t.Id as TenantId,
(SELECT COUNT(*)
FROM Wargles w
WHERE w.TenantId = t.Id
) as WargleCount, count(f.Id) as FiddleCount
(SELECT COUNT(*)
FROM Fiddles f
WHERE f.TenantId = t.Id
) as FiddleCount
FROM Tenants t;
In particular, this can take advantage of indexes on Wargles(TenantId) and Fiddles(TenantId).
In your case, as extendable solution, I would recommend Scalar Function usage.
/* SAMPLE DATA ARRANGE */
CREATE TABLE Tenants (Id INT, Title NVARCHAR(5)) ; INSERT INTO Tenants VALUES (1, 'A'), (2, 'B') , (3, 'C');
CREATE TABLE Wargles (Id INT,TenantId INT);INSERT INTO Wargles VALUES (1, 1), (2, 1) , (3, 1) , (4, 2), (5, 2) , (6, 1), (7, 3) , (8, 3);
CREATE TABLE Fiddles (Id INT,TenantId INT);INSERT INTO Fiddles VALUES (1, 1), (2, 1) , (3, 1) , (4, 2), (5, 2) , (6, 2), (7, 3) , (8, 2);
The Function
/*NEEDED CODE*/
CREATE FUNCTION dbo.ufnGetTenantsNo ( #Id AS INT , #Tb AS INT)
RETURNS INT
AS
BEGIN
DECLARE #Result INT = 0;
IF (#TB = 1)
SELECT #Result = COUNT(*)
FROM Wargles
WHERE TenantId = #Id
ELSE
SELECT #Result = COUNT(*)
FROM Fiddles
WHERE TenantId = #Id
RETURN #Result
END
GO
Select Statement
SELECT Id AS TenantId
,dbo.ufnGetTenantsNo(Id, 1) AS WargleCount
,dbo.ufnGetTenantsNo(Id, 2) AS FiddleCount
FROM Tenants

Get hierarchical data is SQL SERVER with fallback logic

Consider the below schema
dbo.Cultures (Id, CultureCode, ParentId)
Culture table stores the data in the parent-child relationship.
Suppose we have below demo data
5 es-ES 3
Now I have another table which stores the multilingual data for the different cultures.
Schema for the table is as following
dbo.LangData(KeyName, CultureId, Value)
here cultureId is the foreign key of dbo.Cultures table.
Suppose this table has following data
Now I require to fetch the data for all the cultures which are in the Culture table and the corresponding value column in the LangData table.
The culture Ids which are not in the LangData table, for those the Value column will the value of the corresponding parent culture Id columns value. I.e. Data will be retrieved using fallback logic
E.g. For the above values the Result set will be following.
5 es-ES Colour_IN
Here for de-DE data is missing in LangData so it's value will be the data in it's parent culture i.e. en-IN, if in case data also not found in en-IN then it will pick the data of it's parent en-US.
Tried Soloution
First I fetch the culture hierarchy using CTE
CREATE FUNCTION [dbo].[ufnGetCultureHierarchyAll] ()
RETURNS #hierarchyResult TABLE(RowNo INT, CultureId INT, ParentCultureId INT)
AS
BEGIN
WITH CultureHierarchy_CTE(RowNo, CultureId, ParentCultureId)
AS (
SELECT 1,
Id,
ParentId
FROM [dbo].Cultures
UNION ALL
SELECT RowNo + 1,
ou.Id,
ou.ParentId
FROM [dbo].Cultures ou
JOIN CultureHierarchy_CTE cte
ON ou.Id = cte.ParentCultureId
)
-- inserting desired records into table and returning
INSERT INTO #hierarchyResult (RowNo,CultureId,ParentCultureId )
SELECT RowNo, CultureId , ParentCultureId FROM CultureHierarchy_CTE
RETURN;
END
This will return the hierarchy of the all the cultures
Now I attempted to apply join of the result set with the LangData table,
DECLARE #cultureHierarchy AS TABLE(
RowNumber INT,
CultureId INT,
ParentCultureId INT
)
--SELECT * FROM master.Cultures
----Get and store culture hierarchy
INSERT INTO #cultureHierarchy
SELECT RowNo, CultureId, ParentCultureId
FROM ufnGetCultureHierarchyAll()
SELECT c.Code AS [CultureCode],
c.CultureId AS [CultureId],
rv.Value
FROM dbo.LangData rv WITH (NOLOCK)
JOIN #cultureHierarchy c ON rv.CultureId = c.CultureId
END
but it is not working.
Is someone have any Idea regarding same.
Solution using Itzik Ben-Gan's hierarchy model. If you can extend the dbo.Cultures table with Hierarchy, Lvl and Root columns and index on Hierarchy, query will be faster. It has to be rewrited in that case though.
drop table if exists dbo.Cultures;
create table dbo.Cultures (
ID int
, Code varchar(50)
, ParentID int
);
insert into dbo.Cultures (ID, Code, ParentID)
values (1, 'en-US', null), (2, 'en-IN', 1), (3, 'de-DE', 2), (4, 'hi-HI', 2)
drop table if exists dbo.LangData;
create table dbo.LangData (
KeyName varchar(100)
, CultureID int
, Value varchar(100)
);
insert into dbo.LangData (KeyName, CultureID, Value)
values ('lblColourName', 1, 'Color'), ('lblColourName', 2, 'Colour-IN');
with cteCultures as (
select
c.ID, c.Code, c.ParentID, 0 as Lvl
, convert(varchar(max), '.' + CONVERT(varchar(50), c.ID) + '.') as Hierarchy
, c.ID as Root
from dbo.Cultures c
where c.ParentID is null
union all
select
c.ID, c.Code, c.ParentID, cc.Lvl + 1 as Lvl
, cc.Hierarchy + convert(varchar(50), c.ID) + '.' as Hierarchy
, cc.Root as Root
from dbo.Cultures c
inner join cteCultures cc on c.ParentID = cc.ID
)
select
ccr.ID
, ccr.Code
, coalesce(ld.Value, ld2.Value) as Value
from cteCultures ccr
left join dbo.LangData ld on ccr.ID = ld.CultureID
outer apply (
select
top (1) tcc.ID
from cteCultures tcc
inner join dbo.LangData tld on tcc.ID = tld.CultureID
where ld.KeyName is null
and ccr.Hierarchy like tcc.Hierarchy + '%'
and ccr.Hierarchy <> tcc.Hierarchy
order by tcc.Lvl desc
) tt
left join dbo.LangData ld2 on tt.ID = ld2.CultureID
If I understand your question:
We just build your hierarchy (SEQ and Lvl are optional) and then perform TWO left joins in concert with a Coalesce().
Example
Declare #Cultures table (id int,ParentId int,Code varchar(50))
Insert into #Cultures values
( 1, NULL,'en-US')
,( 2, 1 ,'en-IN')
,( 3, 2 ,'de-DE')
,( 4, 2 ,'hi-HI')
Declare #LangData table (keyName varchar(50),CultureId int,Value varchar(50))
Insert Into #LangData values
('lblColourName',1,'Color')
,('lblColourName',2,'Color_IN')
;with cteP as (
Select Seq = cast(10000+Row_Number() over (Order by Code) as varchar(500))
,ID
,ParentId
,Lvl=1
,Code
From #Cultures
Where ParentId is null
Union All
Select Seq = cast(concat(p.Seq,'.',10000+Row_Number() over (Order by r.Code)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Code
From #Cultures r
Join cteP p on r.ParentId = p.ID)
Select CultureId = A.ID
,A.Code
,Value = Coalesce(C.Value,B.Value)
From cteP A
Left Join #LangData B on (A.ParentId=B.CultureId)
Left Join #LangData C on (A.Id=C.CultureId)
Order By Seq
Returns
CultureId Code Value
1 en-US Color
2 en-IN Color_IN
3 de-DE Color_IN
4 hi-HI Color_IN

How can I write a better multiple join that matches multiple values across rows?

I'm trying to write a SQL statement that will allow me to select a series of articles from a table based on their keywords. What I've got so far is a token table, an article table, and a many-to-many table for tokens & articles:
tokens
rowid
token
token_article
token_rowid
article_rowid
articles
rowid
What I'm doing is taking a search query, splitting it up by spaces, then select all articles that contains those keywords. So far I've come up with this:
select * from
(select * from tokens
inner join token_article on
tokens.rowid = token_article.token_rowid and
token = 'ABC'
) as t1,
(select * from tokens
inner join token_article on
tokens.rowid = token_article.token_rowid and
token = 'DEF'
) as t2
where t1.article_rowid = t2.article_rowid and t2.article_rowid = articles.rowid
Which works but of course its doing a select on all articles that match ABC and all articles that DEF then selecting them.
Now I'm trying to figure out a better way. What I imagine in my mind that would work would be to select all the articles that match ABC and from those match any with DEF. This is what I imagine it to look like but does not work (receive error message "no such columns: tokens.rowid")
select * from
(select * from
(select * from tokens
inner join token_article on
tokens.rowid = token_article.token_rowid and
token = 'ABC'
)
inner join token_article on
tokens.rowid = token_article.token_rowid and
token = 'DEF'
)
Because there is more than one way to do this...this method uses GROUP BY and HAVING clauses. The query is looking for all articles that have either the ABC or DEF token, but then grouping by the article ID where the count of tokens for the article is equal to the number of tokens being queried.
Note that I've used MSSQL syntax here, but the concept should work in most SQL implementations.
Edit: I should point out that this has a fairly clean syntax as you add more tokens to the query. If you add more tokens, then you just need to modify the t.token_in criteria and adjust the HAVING COUNT(*) = x clause accordingly.
DECLARE #tokens TABLE
(
rowid INT NOT NULL,
token VARCHAR(255) NOT NULL
)
DECLARE #articles TABLE
(
rowid INT NOT NULL,
title VARCHAR(255) NOT NULL
)
DECLARE #token_article TABLE
(
token_rowid INT NOT NULL,
article_rowid INT NOT NULL
)
INSERT INTO #tokens VALUES (1, 'ABC'), (2, 'DEF')
INSERT INTO #articles VALUES (1, 'This is article 1.'), (2, 'This is article 2.'), (3, 'This is article 3.'), (4, 'This is article 4.'), (5, 'This is article 5.'), (6, 'This is article 6.')
INSERT INTO #token_article VALUES (1, 1), (2, 1), (1, 2), (2, 3), (1, 4), (2, 4), (1, 5), (1, 6)
-- Get the article IDs that have all of the tokens
-- Use this if you just want the IDs
SELECT a.rowid FROM #articles a
INNER JOIN #token_article ta ON a.rowid = ta.article_rowid
INNER JOIN #tokens t ON ta.token_rowid = t.rowid
WHERE t.token IN ('ABC', 'DEF')
GROUP BY a.rowid
HAVING COUNT(*) = 2 -- This should match the number of tokens
rowid
-----------
1
4
-- Get the articles themselves
-- Use this if you want the articles
SELECT * FROM #articles WHERE rowid IN (
SELECT a.rowid FROM #articles a
INNER JOIN #token_article ta ON a.rowid = ta.article_rowid
INNER JOIN #tokens t ON ta.token_rowid = t.rowid
WHERE t.token IN ('ABC', 'DEF')
GROUP BY a.rowid
HAVING COUNT(*) = 2 -- This should match the number of tokens
)
rowid title
----------- ------------------
1 This is article 1.
4 This is article 4.
Here is one way to do it. The script was tested in SQL Server 2012 database.
Script:
CREATE TABLE dbo.tokens
(
rowid INT NOT NULL IDENTITY
, token VARCHAR(10) NOT NULL
);
CREATE TABLE dbo.articles
(
rowid INT NOT NULL IDENTITY
, name VARCHAR(10) NOT NULL
);
CREATE TABLE dbo.token_article
(
token_rowid INT NOT NULL
, article_rowid INT NOT NULL
);
INSERT INTO dbo.tokens (token) VALUES
('ABC'),
('DEF');
INSERT INTO dbo.articles (name) VALUES
('Article 1'),
('Article 2'),
('Article 3');
INSERT INTO dbo.token_article (token_rowid, article_rowid) VALUES
(1, 2),
(2, 3),
(1, 3),
(1, 1),
(2, 2);
SELECT out1.rowid
, out1.token
, out1.token_rowid
, out1.article_rowid
, ta2.token_rowid
, ta2.article_rowid
, t2.rowid
, t2.token
FROM
(
SELECT t.rowid
, t.token
, ta1.token_rowid
, ta1.article_rowid
FROM dbo.tokens t
INNER JOIN dbo.token_article ta1
ON ta1.token_rowid = t.rowid
WHERE t.token = 'ABC'
) out1
INNER JOIN dbo.token_article ta2
ON ta2.article_rowid = out1.article_rowid
INNER JOIN dbo.tokens t2
ON t2.rowid = ta2.token_rowid
AND t2.token = 'DEF';
Output:
rowid token token_rowid article_rowid token_rowid article_rowid rowid token
----- ----- ----------- ------------- ----------- ------------- ----- -----
1 ABC 1 2 2 2 2 DEF
1 ABC 1 3 2 3 2 DEF

SQL: How do I loop through the results of a SELECT statement?

How do I loop through the results of a SELECT statement in SQL? My SELECT statement will return just 1 column but n results.
I have created a fictional scenario below complete with the Pseudo code of what I'm trying to do.
Scenario:
Students are registering for their classes. They submit a form with multiple course selections (ie. select 3 different courses at once). When they submit their registration I need to ensure there is still room left int the courses they have selected (note I will do a similar check before presenting them with course selection UI but I need to verify afterwards in case somebody else has gone in and swipped up the remaining spots).
Pseudo Code:
DECLARE #StudentId = 1
DECLARE #Capacity = 20
-- Classes will be the result of a Select statement which returns a list of ints
#Classes = SELECT classId FROM Student.CourseSelections
WHERE Student.CourseSelections = #StudentId
BEGIN TRANSACTION
DECLARE #ClassId int
foreach (#classId in #Classes)
{
SET #SeatsTaken = fnSeatsTaken #classId
if (#SeatsTaken > #Capacity)
{
ROLLBACK; -- I'll revert all their selections up to this point
RETURN -1;
}
else
{
-- set some flag so that this student is confirmed for the class
}
}
COMMIT
RETURN 0
My real problem is a similar "ticketing" problem. So if this approach seems very wrong please feel free to recommend something more practical.
EDIT:
Attempting to implement the solution below. At this point it doesn't work. Always returns "reserved".
DECLARE #Students TABLE
(
StudentId int
,StudentName nvarchar(max)
)
INSERT INTO #Students
(StudentId ,StudentName)
VALUES
(1, 'John Smith')
,(2, 'Jane Doe')
,(3, 'Jack Johnson')
,(4, 'Billy Preston')
-- Courses
DECLARE #Courses TABLE
(
CourseId int
,Capacity int
,CourseName nvarchar(max)
)
INSERT INTO #Courses
(CourseId, Capacity, CourseName)
VALUES
(1, 2, 'English Literature'),
(2, 10, 'Physical Education'),
(3, 2, 'Photography')
-- Linking Table
DECLARE #Courses_Students TABLE
(
Course_Student_Id int
,CourseId int
,StudentId int
)
INSERT INTO #Courses_Students
(Course_Student_Id, StudentId, CourseId)
VALUES
(1, 1, 1),
(2, 1, 3),
(3, 2, 1),
(4, 2, 2),
(5, 3, 2),
(6, 4, 1),
(7, 4, 2)
SELECT Students.StudentName, Courses.CourseName FROM #Students Students INNER JOIN
#Courses_Students Courses_Students ON Courses_Students.StudentId = Students.StudentId INNER JOIN
#Courses Courses ON Courses.CourseId = Courses_Students.CourseId
DECLARE #StudentId int = 4
-- Ideally the Capacity would be database driven
-- ie. come from the Courses.Capcity.
-- But I didn't want to complicate the HAVING statement since it doesn't seem to work already.
DECLARE #Capacity int = 1
IF EXISTS (Select *
FROM
#Courses Courses INNER JOIN
#Courses_Students Courses_Students ON Courses_Students.CourseId = Courses.CourseId
WHERE
Courses_Students.StudentId = #StudentId
GROUP BY
Courses.CourseId
HAVING
COUNT(*) > #Capacity)
BEGIN
SELECT 'full' as Status
END
ELSE BEGIN
SELECT 'reserved' as Status
END
No loop needed. You're looking at a standard aggregate with COUNT and GROUP.
Of course, some details are needed but the principle is this...
DECLARE #StudentId = 1
DECLARE #Capacity = 20
-- Classes will be the result of a Select statement which returns a list of ints
IF EXISTS (SELECT *
FROM
Student.CourseSelections CS
JOIN
---this is where you find out course allocations somehow
ClassTable C ON CS.classId = C.classId
WHERE
Student.CourseSelections = #StudentId
GROUP BY --change this, it depends on where you find out course allocations
ClassID
HAVING
COUNT(*) > #Capacity)
'no'
ELSE
'yes'
Edit:
I've changed the link table. Course_Student_ID is usually not needed in link tables.
The JOIN now
gets the courses for that student
then looks at all students on this course and compares to capacity
Cut down version of above:
...
-- Linking Table
DECLARE #Courses_Students TABLE (
,CourseId int
,StudentId int)
INSERT INTO #Courses_Students
(StudentId, CourseId)
VALUES (1, 1), (1, 3), (2, 1), (2, 2), (3, 2), (4, 1), (4, 2)
DECLARE #StudentId int = 4
--straight list
SELECT
C.CourseName, C.Capacity, COUNT(*)
FROM
#Courses_Students CSThis
JOIN
#Courses C ON CSThis.CourseId = C.CourseId
JOIN
#Courses_Students CSOthers ON CSOthers.CourseId = C.CourseId
WHERE
CSThis.StudentId = #StudentId
GROUP BY
C.CourseName, C.Capacity
--oversubscribed list
SELECT
C.CourseName, C.Capacity, COUNT(*)
FROM
#Courses_Students CSThis
JOIN
#Courses C ON CSThis.CourseId = C.CourseId
JOIN
#Courses_Students CSOthers ON CSOthers.CourseId = C.CourseId
WHERE
CSThis.StudentId = #StudentId
GROUP BY
C.CourseName, C.Capacity
HAVING
COUNT(*) > C.Capacity
Avoid looping through result sets in SQL as much as you can. If you really can't (if you really are a standard programmer but profession leads you into SQL) use cursors. They don't smell nice, but are unavoidable at times.
Another option would be to implement a CHECK Constraint on your table that contains the Course information. The check constraint could call your existing function to check that there are free seats.
Wrap all of your Inserts/Updates in to one transaction. If any of the Inserts/Updates fails then the entire transaction will be rolled back.

SELECT item types not provided by an entity

So I've got some data. There are entities. Entities have an arbitrary number of items. Items can be one of a defined set of types. An entity can have more than one item of a given type. I can get a list of items that an entity has. What I want is to get a list of types that an entity doesn't have an item for.
Here's my schema:
entities
id name
1 Bob
2 Alice
item_types
id name
1 red
2 yellow
3 green
4 blue
5 orange
items
entity_id item_type_id name
1 1 apple
1 2 banana
1 3 lime
1 3 tree
2 3 money
2 5 traffic cone
I would like to query Bob's id (1) and get this list:
4 blue
5 orange
And query Alice's id (2) and get:
1 red
2 yellow
4 blue
It's probably starting me in the face. I'm gonna keep working on it but I bet you SO peeps beat me to it. Thank you kindly for your time.
select id, name
from item_types
where id not in
(select i.item_type_id
from items i
inner join entities e
on e.id = t.entity_id
where e.Name = 'Bob')
or (sometimes faster, but optimizers are getting better all the time):
select disctinct t.id, t.name
from item_types t
left outer join items i
on i.item_type_id = t.id
left outer join entities e
on e.id = i.entity_id
and e.Name = 'Bob'
where e.id is null
for Bob
SELECT
t.id, t.name
FROM
items i
INNER JOIN
entities e ON e.id = i.entity_id
INNER JOIN
item_types t ON t.id = i.item_type_id
WHERE
e.id <> 1
for Alice just swap e.id <> 1 to e.id <> 2
I think this is what you are looking for:
SELECT id, name
FROM item_types
WHERE id NOT IN
(
SELECT DISTINCT item_type_id
FROM items
WHERE entity_id = 1
)
The "entity_id = 1" represents Bob, change it as necessary.
I will rework this to make it better, but here is a working solution
set nocount on
go
drop table #entities
drop table #itemtype
drop table #items
create table #Entities
(
EntityId int,
EntityName varchar (250)
)
create table #ItemType
(
ItemTypeId int,
ItemTypeName varchar (250)
)
create table #Items
(
EntityId int,
ItemTypeId int,
ItemName varchar (250)
)
go
insert into #entities values (1, 'Bob')
insert into #entities values (2, 'Alice')
go
insert into #ItemType values (1, 'red')
insert into #ItemType values (2, 'yellow')
insert into #ItemType values (3, 'green')
insert into #ItemType values (4, 'blue')
insert into #ItemType values (5, 'orange')
go
insert into #Items values (1, 1, 'apple')
insert into #Items values (1, 2, 'banana')
insert into #Items values (1, 3, 'lime')
insert into #Items values (1, 3, 'tree')
insert into #Items values (2, 3, 'money')
insert into #Items values (2, 5, 'traffic cone')
go
;WITH ENTITY AS (
SELECT #Entities.EntityId, EntityName, ItemTypeId, ItemName
FROM #Entities, #Items
WHERE #Entities.EntityId = #Items.EntityId
AND #Entities.EntityName = 'Bob'
)
SELECT #ItemType.* FROM ENTITY
RIGHT JOIN #ItemType ON ENTITY.ItemTypeId = #ItemType.ItemTypeId
WHERE EntityId is NULL
;WITH ENTITY AS (
SELECT #Entities.EntityId, EntityName, ItemTypeId, ItemName
FROM #Entities, #Items
WHERE #Entities.EntityId = #Items.EntityId
AND #Entities.EntityName = 'Alice'
)
SELECT #ItemType.* FROM ENTITY
RIGHT JOIN #ItemType ON ENTITY.ItemTypeId = #ItemType.ItemTypeId
WHERE EntityId is NULL