tags:

views:

76

answers:

4

Let's say we have a table (EnsembleMembers) in an SQL database with the following data. It lists the musicians which are part of various ensembles, along with their instruments.

EnsembleID (FK)   MusicianID (FK)   Instrument
----------------------------------------------
'1'               '1'               'Clarinet'
'1'               '4'               'Clarinet'
'1'               '100'             'Saxophone'
'2'               '200'             'Saxophone'
'2'               '300'             'Saxophone'
'2'               '320'             'Flute'
'99'              '300'             'Clarinet'

I want to select the ensemble IDs where the ensemble has one or more saxophone or one or more clarinet players, but not both. I have tried the following SQL statement, but it is returning 1,2,2,99, rather than the expected 2,99.

SELECT e1.EnsembleID 
  FROM ensemblemembers e1 
 WHERE e1.Instrument = 'Saxophone' 
    OR e1.Instrument = 'Clarinet' 
  AND NOT EXISTS (SELECT * 
                    FROM ensemblemembers e2 
                   WHERE (    e1.Instrument = 'Saxophone' 
                          AND e2.Instrument = 'Clarinet' 
                          AND e1.EnsembleID = e2.EnsembleID) 
                      OR (    e1.Instrument = 'Clarinet' 
                          AND e2.Instrument = 'Saxophone' 
                          AND e1.EnsembleID = e2.EnsembleID));

What am I doing wrong?

PS - I don't want to use DISTINCT for performance reasons.

+3  A: 
SELECT EnsembleID
FROM EnsembleMembers
WHERE Instrument IN ('Saxophone', 'Clarinet')
GROUP BY EnsembleID
HAVING COUNT(DISTINCT Instrument) = 1

You can also use a FULL OUTER JOIN for this, but this type of join is not supported by MySQL and a few other minor databases.

SELECT COALESCE(e1.EnsembleID, e2.EnsembleID) AS EnsembleID
FROM EnsembleMembers e1 FULL OUTER JOIN EnsembleMembers e2
  ON e1.EnsembleID = e2.EnsembleID 
  AND e1.Instrument = 'Saxophone' 
  AND e2.Instrument = 'Clarinet'
WHERE e1.EnsembleID IS NULL OR e2.EnsembleID IS NULL

If you need this to work without FULL OUTER JOIN, try this:

SELECT e1.EnsembleID, e1.Instrument
FROM EnsembleMembers e1 LEFT OUTER JOIN EnsembleMembers e2
  ON e1.EnsembleID = e2.EnsembleID 
  AND e2.Instrument = 'Clarinet'
WHERE e1.Instrument = 'Saxophone' AND e2.EnsembleID IS NULL
UNION
SELECT e1.EnsembleID, e1.Instrument, e2.EnsembleID, e2.Instrument
FROM EnsembleMembers e1 LEFT OUTER JOIN EnsembleMembers e2
  ON e1.EnsembleID = e2.EnsembleID 
  AND e2.Instrument = 'Saxophone'
WHERE e1.Instrument = 'Clarinet' AND e2.EnsembleID IS NULL;

In the future, please tag your question with the brand of RDBMS you use.

Bill Karwin
WITHOUT using DISTINCT?
Jake Petroules
In the GROUP BY solution, you have to use DISTINCT if there can be multiple saxophones or multiple clarinets in one ensemble. Also see the second solution I just added, which may work depending on what brand of database you're using.
Bill Karwin
Are you sure the full outer join example is correct? I know MySQL doesn't support them but I duplicated your example, changed one to left outer join, the other to right outer join, and union'd the two (which is the way to emulate the functionality AFAIK), but I still retrieved an ensemble ID of an ensemble which had both a saxophone and clarinet player.
Jake Petroules
Tagged question with MySQL. Will do so in future, sorry.
Jake Petroules
+1  A: 

Here is the most stupid solution (but I like it somehow):

   SELECT EnsembleID FROM EnsembleMembers
MINUS
( 
      SELECT EnsembleID
        FROM EnsembleMembers
       WHERE Instrument = 'Saxophone'
   INTERSECT
      SELECT EnsembleID
        FROM EnsembleMembers
       WHERE Instrument = 'Clarinet' );
Alexander Malakhov
Change `MINUS` to `EXCEPT` and it becomes SQL-92 syntax and works on SQL Server ...so I it like it too :) Out of interest, `MINUS` doesn't appear to be in any of the SQL Standards, is is proprietary to a particular SQL product?
onedaywhen
@onedaywhen: yes, I'm on Oracle. Haven't thought it's not ANSI, thanx!
Alexander Malakhov
+1  A: 

Here's how you could do the query using XOR:

select a.EnsembleID from 
(
    select max(EnsembleID) as EnsembleID,
    max(case when Instrument = 'Saxophone' then 1 else 0 end) as Saxophone,
    max(case when Instrument = 'Clarinet' then 1 else 0 end) as Clarinet
    from 
        EnsembleMembers
    group by EnsembleID
) a 
where 
    a.Saxophone ^ a.Clarinet = 1
kristian
+1  A: 

I assume you have a table called ENSEMBLES:

select E.id from ensembles E where exists (
  select 1 from ensemblemembers M where E.id = M.ensembleid
  and M.instrument in ('clarinet', 'saxophone')
) and not (
  exists (
    select 1 from ensemblemembers M where E.id = M.ensembleid
    and M.instrument = 'clarinet'
  ) and exists (
    select 1 from ensemblemembers M where E.id = M.ensembleid
    and M.instrument = 'saxophone'
  )
)

You want to avoid using DISTINCT, so one way to do it is by using the main ENSEMBLES table. From there, pick ensemble rows that have 'clarinet' OR 'saxophone'. Then the third step is to remove all ensemble rows that have 'clarinet' AND 'saxophone'.

endy_c