-1
declare @Character table (id int, [name] varchar(12));

insert into @Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');

declare @NameToCharacter table (id int, nameId int, characterId int);

insert into @NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);

The Name Table has more than just 1,2,3 and the list to parse on is dynamic

NameTable

id  | name
----------
1      foo
2      bar
3      steak

CharacterTable

id | name
---------
1     tom
2     jerry
3     dog

NameToCharacterTable

id | nameId | characterId
1     1           1
2     1           3
3     1           2
4     2           1 

I am looking for a query that will return a character that has two names. For example With the above data only "tom" will be returned.

SELECT * 
FROM nameToCharacterTable
WHERE nameId in (1,2)

The in clause will return every row that has a 1 or a 3. I want to only return the rows that have both a 1 and a 3.

I am stumped I have tried everything I know and do not want to resort to dynamic SQL. Any help would be great

The 1,3 in this example will be a dynamic list of integers. for example it could be 1,3,4,5,.....

Charlieface
  • 52,284
  • 6
  • 19
  • 43
gh9
  • 10,169
  • 10
  • 63
  • 96
  • 2
    This is a classic relational division question, see eg https://stackoverflow.com/a/70736303/14868997. Do you want to know cases where there are *exactly* those two ("Without Remainder"), or those two and any others ("With Remainder")? – Charlieface Sep 15 '22 at 00:55
  • good question, At minimum of those two. – gh9 Sep 15 '22 at 00:57

2 Answers2

1

Filter out a count of how many times the Character appears in the CharacterToName table matching the list you are providing (which I have assumed you can convert into a table variable or temp table) e.g.

declare @Character table (id int, [name] varchar(12));

insert into @Character (id, [name])
values
(1, 'tom'),
(2, 'jerry'),
(3, 'dog');

declare @NameToCharacter table (id int, nameId int, characterId int);

insert into @NameToCharacter (id, nameId, characterId)
values
(1, 1, 1),
(2, 1, 3),
(3, 1, 2),
(4, 2, 1);

declare @RequiredNames table (nameId int);

insert into @RequiredNames (nameId)
values
(1),
(2);

select *
from @Character C
where (
    select count(*)
    from @NameToCharacter NC
    where NC.characterId = c.id
    and NC.nameId in (select nameId from @RequiredNames)
) = 2;

Returns:

id name
1 tom

Note: Providing DDL+DML as shown here makes it much easier for people to assist you.

Dale K
  • 25,246
  • 15
  • 42
  • 71
  • thank you for the edits and quick response, how would this work with a dynamic list? And a dynamic name table not just 1,2,3 – gh9 Sep 15 '22 at 00:20
  • last sentence in the qustion – gh9 Sep 15 '22 at 00:21
  • upvote for the constructive suggestion about adding table creation sql – gh9 Sep 15 '22 at 00:23
  • 1
    Thats perfect thank you very much. I wouldn't have thought about just getting all of them with the in clause and making sure there were x for them – gh9 Sep 15 '22 at 00:37
1

This is classic Relational Division With Remainder.

There are a number of different solutions. @DaleK has given you an excellent one: inner-join everything, then check that each set has the right amount. This is normally the fastest solution.

If you want to ensure it works with a dynamic amount of rows, just change the last line to

) = (SELECT COUNT(*) FROM @RequiredNames);

Two other common solutions exist.

  • Left-join and check that all rows were joined
SELECT *
FROM @Character c
WHERE EXISTS (SELECT 1
    FROM @RequiredNames rn
    LEFT JOIN @NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
    HAVING COUNT(*) = COUNT(nc.nameId)  -- all rows are joined
);
  • Double anti-join, in other words: there are no "required" that are "not in the set"
SELECT *
FROM @Character c
WHERE NOT EXISTS (SELECT 1
    FROM @RequiredNames rn
    WHERE NOT EXISTS (SELECT 1
        FROM @NameToCharacter nc
        WHERE nc.nameId = rn.nameId AND nc.characterId = c.id
    )
);

A variation on the one from the other answer uses a windowed aggregate instead of a subquery. I don't think this is performant, but it may have uses in certain cases.

SELECT *
FROM @Character c
WHERE EXISTS (SELECT 1
    FROM (
      SELECT *, COUNT(*) OVER () AS cnt
      FROM @RequiredNames
    ) rn
    JOIN @NameToCharacter nc ON nc.nameId = rn.nameId AND nc.characterId = c.id
    HAVING COUNT(*) = MIN(rn.cnt)
);

db<>fiddle

Charlieface
  • 52,284
  • 6
  • 19
  • 43