tags:

views:

178

answers:

7

here is the schema:

alt text

here is the question:

Point out the battles in which at least three ships from the same country took part.

here is my answer:

  SELECT battles.name 
    FROM battles,
         outcomes,
         ships,
         classes 
   WHERE outcomes.ship = ships.name 
GROUP BY battles.name 
  HAVING COUNT(classes.country) >= 3;

Can you please tell me why it is wrong and help me to correct it!

A: 

I have quite never used the HAVING clause of the GROUP BY statement. However, GROUP BY should be used for aggregation purposes only. So, if you have no agregation function (SUM, MAX, MIN, AVG, COUNT, etc.) selected, you will not be able to GROUP BY.

Will Marcouiller
no, you will still be able to use GROUP BY even without those functions as long as the selected columns are also in the GROUP BY clause.
hallie
+1 Comment. This must be that I have never seen the use of GROUP BY without aggregation functions. Thanks for the update!
Will Marcouiller
A: 

You have messed the joins on the tables. You can't just list the tables, you have to specify how they join together

From Battles
Inner Join Outcomes On Outcomes.Battle = Battles.name
Inner Join Ships.Name On Ship.Name = Outcomes.Ship
Inner Join Classes.Class = Ship.Class
TFD
doesnt work: SELECT battles.name FROM BattlesInner Join Outcomes On Outcomes.Battle = Battles.nameInner Join Ships.Name On Ships.Name = Outcomes.ShipInner Join Classes.Class = Ships.Class WHERE outcomes.ship = ships.name GROUP BY battles.name HAVING count(classes.country) >= 3
I__
What doesn't work?
TFD
+5  A: 

This:

SELECT battles.name 
  FROM battles,
       outcomes,
       ships,
       classes 
 WHERE outcomes.ship = ships.name 

...fundamentally flawed because there's only join criteria between the OUTCOMES and SHIPS. The result is a cartesian product. It's valid ANSI-89 syntax, but won't return a resultset remotely resembling what you would expect.

If you only want the battle "name", use:

  SELECT o.battle
    FROM OUTCOMES o
    JOIN SHIPS s ON s.name = o.ship
    JOIN CLASSES c ON c.class = s.class
GROUP BY o.battle
  HAVING COUNT(c.country) >= 3

If you want the battle table details, use:

SELECT b.*
 FROM BATTLES b
 JOIN (SELECT o.battle
         FROM OUTCOMES o
         JOIN SHIPS s ON s.name = o.ship
         JOIN CLASSES c ON c.class = s.class
     GROUP BY o.battle
       HAVING COUNT(c.country) >= 3) x ON x.battle = b.name
OMG Ponies
what does O and S and C stand for?
I__
@every_answer_gets_a_point: They are table aliases, so you don't have to re-type the full table name every time you want to show which table you are referencing.
OMG Ponies
@OMG Ponies, I don't think this assert that there were 3 ships from the same country. It merely checks that there were 3 ships (possibly from 3 different countries, if somehow the "distinct" is implied.)
mjv
@mjv: Tested on MySQL - confirmed, works as is.
OMG Ponies
@OMG Ponies: Hum..., surprising. Again, I wasn't challenging the syntax, but the fact that the query would likely return a battle where you had say a ship from Italy, another from France and another one from the UK, even though none of the countries had 3 ship there.
mjv
@mjv: I understand what you believe should be occurring, but you should really test it yourself if you don't accept that I tested as diligently as you. If I added `c.country` to the SELECT list, I would see the country with three or more ships attached to an outcome record.
OMG Ponies
@OMG: quite fair! I _should_ test for myself, and typically would; I wasn't in the position to do this lately, being on the road, with my "mySQL box" back at home. I hope I didn't offend you with what may have seen like a challenge. I've come to respect your authority on SO, in particular with SQL related questions and indeed upvoted you a few times [I'm a cheap upvoter on the whole]. That's indeed because of my trust in your expertise that I quesionned the results as they seemed odd to me and could have been the result of a misunderstanding. Cheers! (and +1 for the "pain") ;-)
mjv
@mjv: It doesn't have to be MySQL - no vendor has been identified, and the query is portable. I'm flattered, but I'd rather learn if I'm not correct and I do appreciate code review.
OMG Ponies
+1  A: 
 SELECT 
   battles.name 
 FROM battles,
      outcomes,
      ships,
      classes 
 WHERE battles.name=outcome.battle 
 AND   outcomes.ship = ships.name 
 AND   ships.class=classes.class
 GROUP BY battles.name
 HAVING COUNT(classes.country) >= 3;
hallie
This doesn't assert the requirement that 3 ships _from the same country_ were there. I think that the only way to do this is to introduce country as one of the group by columns.
mjv
Works, even if it is ANSI-89 join syntax
OMG Ponies
+4  A: 
SELECT DISTINCT BattleName
FROM (
   SELECT Battles.name AS BattleName, Class.Country, COUNT(*)
   FROM Battles
   JOIN Outcomes ON battles.name = Outcomes.battle 
   JOIN Ships ON Outcomes.ship = Ships.name
   JOIN Classes ON Ships.class = Class.class
   GROUP BY battles.name, Classes.country 
   HAVING COUNT(*) >= 3;
)

The main flaw in the original query was that the joins between tables weren't expressed, resulting in plain cartesian product.

Another problem was that the country wasn't listed to allow a count of ship per country. This modified query was then made the subquery, to allow selecting the BattleName only once.

Notes:
- I added the COUNT(*) in the SELECT list. This is more by safety/ignorance, I think that some SQL implementation would require the aggregate value found in the HAVING clause to exist in the SELECT list. (I may be wrong on this).
- Rather than COUNT(*) one may need to use say COUNT(Classes.country) or any other field (if this this MS-Access as it appears, the COUNT(*) syntax may not be allowed).

mjv
As is, this will return an error - there's a comma in the `FROM` statement, likely carried over from converted from ANSI-89 syntax. `COUNT (*)` in the SELECT is redundant/unused, and there really isn't a need for a subquery/derived table/inline view.
OMG Ponies
@OMG Ponies: Thank you for pointing out the typo. I'll also try your suggestions.
mjv
A: 

I can't see you image (filtered out by big brother)

However, just remember, WHERE filters the entire pool of rows first, GROUP BY combines those remaining rows, and Having filters those groups.

FROM & JOINs determine & filter rows WHERE more filters on the rows
GROUP BY combines those rows into groups
HAVING filters groups
ORDER BY arranges the remaining rows/groups

KM
A: 

select distinct A.battle from (select country, o.battle from outcomes o left join ships s on s.name = o.ship left join classes c on c.class = s.class
group by o.battle, country having count(country) >= 3) A

Alvin