Question

I have a table and an associated list of attributes in another table. It looks like this:

Table Unit:

ID | State | ...
----------------
 1 |    10 | ...
 2 |    15 | ...
 3 |    10 | ...

(State has no meaning here, I just added it to show that this table contains additional columns)

Table Shapes:

ID | Unit_ID (FK) | Shape
-------------------------
 1 |            1 |    40
 2 |            1 |    40
 3 |            1 |    45
 4 |            3 |    10

So basically every row in Unit can have various associated shapes. The order of the shapes is not important but a shape can be associated multiple times.

In my example the Units have the following Shapes:

  • Unit 1: 40, 40, 45
  • Unit 2: None
  • Unit 3: 10

So far so good. This works fine when fetching / updating the full set of shapes for a given Unit-ID.

But I have the requirement to select all Units which have a certain list of associated Shapes. For example, I want to find all Units which have the Shapes 40, 40, 45 (should return Unit_ID 1).

I have already found some similar questions and articles which use relational division but I'm not really sure if this will work, since most of the solutions cannot handle values which appear more than once. I want to have an exact match, if I search for 40, 45 I want the query to return no results in my example, since there is no Unit with exactly these matching Shapes (I will need a query to handle this case as well - select Units with Shapes that contain a subset - but this will probably be easy once I know how to search for an exact match).

I am using SQL Server 2012.

Was it helpful?

Solution

The following approach starts by putting the list to find in a table (well, a CTE) with each shape and a count of the number of times. Then, it does various checks.

The first is that each shape in shapes appears the right number of times. This is where inner aggregation comes from.

Then, it counts the number of shapes that match and validates that this is the total number of different shapes in tofind:

with tofind as (
      select 40 as shape, 2 as cnt union all
      select 45 as shape, 1 as cnt
     )
select s.unit_id
from (select s.unit_id, tofind.cnt as tf_cnt, count(s.id) as s_cnt
      from shapes s join
           tofind
           on s.shape = tofind.shape
      group by s.unit_id, tofind.shape, tofind.cnt
     ) s
group by s.unit_id
having sum(case when s_cnt = tf_cnt then 1 else 0 end) = (select count(*) from tofind);

EDIT:

Here is a SQL Fiddle demonstrating that it works. However, the above code doesn't look for an exact match, because other shapes could be in the record. The following modification works only for an exact match:

with tofind as (
      select 40 as shape, 2 as cnt union all
      select 45 as shape, 1 as cnt
     )
select s.unit_id
from (select s.unit_id, tofind.cnt as tf_cnt, count(s.id) as s_cnt
      from shapes s left outer join
           tofind
           on s.shape = tofind.shape
      group by s.unit_id, tofind.shape, tofind.cnt
     ) s
group by s.unit_id
having sum(case when s_cnt = tf_cnt then 1 else 0 end) = (select count(*) from tofind) and
       count(*) = sum(case when s_cnt = tf_cnt then 1 else 0 end);

The difference is the left outer join and the additional condition in the having clause.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top