battles, ships sql query - sql

Here is a schema about battleships and the battles they fought in:
Ships(name, yearLaunched, country, numGuns, gunSize, displacement)
Battles(ship, battleName, result)
A typical Ships tuple would be:
('New Jersey', 1943, 'USA', 9, 16, 46000)
which means that the battleship New Jersey was launched in 1943; it belonged to the USA, carried 9 guns of size 16-inch (bore, or inside diameter of the barrel), and weighted (displaced, in nautical terms) 46,000 tons. A typical tuple for Battles is:
('Hood', 'North Atlantic', 'sunk')
That is, H.M.S. Hood was sunk in the battle of the North Atlantic. The other possible results are 'ok' and 'damaged'
Question: List all the pairs of countries that fought each other in battles. List each pair only once, and list them with the country that comes first in alphabetical order first
Answer: I wrote this:
SELECT
a.country, b.country
FROM
ships a, ships b, battles b1, battles b2
WHERE
name = ship
and b1.battleName = b2.battleName
and a.country > b.country
But it says ambiguous column name. How do I resolve it? Thanks in advance

Well, name = ship is the problem. name could be from a or b, and ship from b1 or b2
you could do something like that :
select distinct s1.country, s2.country
from ships s1
inner join Battles b1 on b.ship = s1.name
inner join Battles b2 on b2.ship <> s1.name and b2.battleName = b1.battleName
inner join ships s2 on s2.name = b2.ship and s2.country < s1.country

Could you try a nested query getting a table of winners and losers and then joining them on the battle name?
SELECT
WINNER.country as Country1
,LOSER.country as Country2
FROM
(
SELECT DISTINCT country, battleName
FROM Battles
INNER JOIN Ships ON Battles.ship = Ships.name
WHERE Battles.Result = 1
) AS WINNER
INNER JOIN
(
SELECT DISTINCT country, battleName
FROM Battles
INNER JOIN Ships ON Battles.ship = Ships.name
WHERE Battles.Result = 0
) AS LOSER
ON WINNER.battlename = LOSER.battlename
ORDER BY WINNER.country

try this, i think it's concise:
SELECT DISTINCT s1.country, s2.country
FROM ships s1, ships s2, battles b1, battles b2 -- i just list all tables here but you can use the left join etc.
WHERE s1.name=b1.ship AND s2.name=b2.ship AND b1.battleName=b2.battleName -- for the tables to join each other
AND s1.country < s2.country --key condition
ORDER BY s1.country ASC

Related

How can I write SQL Query?

I need a request that displays the name and surname of the clients with the smallest credit limit - among married women who do not live in Japan, Brazil or Italy.
Diagram:
This will Give all the people not in 'Japan', 'Brazil' or 'Italy') ,
Select C.Cust_first_name,C.Cust_Last_name from Customers C
Inner Join Countries C1
on C.Country_Id=C1.Country_Id
Where C1.Country_Name Not in('Japan', 'Brazil' or 'Italy')
and
C.Cust_Credit_Limit=(Select Min(Cust_Credit_Limit) From from Customers C)
If we Convert The Question to Code that will be the above code,
The Script would return the name and last name of person not in ('Japan', 'Brazil' or 'Italy') And has the lowest salary In the entire customer Base.
select top 1 * from Customers C
Inner Join
(
select MIn(cust_credit_limit) from Customers C1
Inner Join Countries CT on C1.Country_id = C1.Country_id
where CT.Country_Name Not in ('Japan', 'Brazil','Italy')
)
C2 on C2.cust_credit_limit = C.cust_credit_limit

Oracle SQL: Nested sub-queries, outer are the same, inner differs

I'm in a situation where I have several sub-queries, joined by UNION, each nested with an inner sub-query. The outer sub-queries are fully identical to each other, while the inner queries differ and are unique.
Reuse of the entire outer sub-queries is cumbersome for reading and making changes, and it would be greatly beneficial if they could be defined once and reused. So this is a question about creating reusable SQL-queries, but with distinct inner sub-queries that are passed on as arguments.
For my examples, I will present a simplified case that has the same issue as my real code.
We're using Oracle SQL for our project.
Say that we have a database for a school or university, with the tables PERSON, STUDENT, GRADE and COURSE, and all are connected by FK-relationships.
I need to run a query which gathers a list, counting up the number of people once for every criteria:
Number of students whose last name begin on the letter 'E'
Number of students older than 20 years
Number of people (including but not limited to students) who are female
Number of students who passed the course "Intermediate Norwegian" with grade B or higher
The expected outcome:
| Description | Number_of_students
1 | Last names beginning with letter "E" | 32
2 | Older than 20 years | 154
3 | All female persons | 356
4 | Passed "Intermediate Norwegian" with grade >= B | 12
Below is a query which should satisfy what I need.
It consists of several sub-queries joined by UNION, and all have their own distinct inner query.
The code is far from brilliant, but that is beside the point. The real question is about drastically improving readability. The outer sub-queries have the same structure that could be re-used, but the inner ones are different.
SELECT * FROM
-- 1st entry: Number of students on the last name 'E'
(SELECT 'Last names beginning with letter "E"' AS Description, count(*) AS Number_of_students FROM
FROM PERSON p, STUDENT s, GRADE g, COURSE c
WHERE p.ID = s.PERSON_ID
AND s.ID = g.STUDENT_ID
AND g.COURSE_ID = c.ID
-- ... other complex code here
AND p.ID IN(
SELECT DISTINCT p1.ID FROM PERSON p1, STUDENT s1
WHERE p1.ID = s1.PERSON_ID AND p1.LASTNAME LIKE 'E%'
)
)
UNION
-- 2nd entry: Number of students older than 20 years
(SELECT 'Older than 20 years' AS Description, count(*) AS Number_of_students FROM
FROM PERSON p, STUDENT s, GRADE g, COURSE c
WHERE p.ID = s.PERSON_ID
AND s.ID = g.STUDENT_ID
AND g.COURSE_ID = c.ID
-- ... other complex code here
AND p.ID IN(
SELECT DISTINCT p2.ID FROM PERSON p2, STUDENT s2
WHERE p2.ID = s2.PERSON_ID AND p2.AGE > 20
)
)
UNION
-- 3rd entry: Number of female persons, including but not limited to students
(SELECT 'All female persons' AS Description, count(*) AS Number_of_students FROM
FROM PERSON p, STUDENT s, GRADE g, COURSE c
WHERE p.ID = s.PERSON_ID
AND s.ID = g.STUDENT_ID
AND g.COURSE_ID = c.ID
-- ... other complex code here
AND p.ID IN(
SELECT DISTINCT p3.ID FROM PERSON p3 WHERE p3.GENDER = 'Female'
)
)
UNION
-- 4th entry: Students who passed the course "Intermediate Norwegian" with grade B or higher
(SELECT 'Passed "Intermediate Norwegian" with grade >= B' AS Description, count(*) AS Number_of_students FROM
FROM PERSON p, STUDENT s, GRADE g, COURSE c
WHERE p.ID = s.PERSON_ID
AND s.ID = g.STUDENT_ID
AND g.COURSE_ID = c.ID
-- ... other complex code here
AND p.ID IN(
SELECT DISTINCT p4.ID FROM PERSON p4, STUDENT s4, GRADE g4 AND COURSE c4
WHERE p4.ID = s4.PERSON_ID
AND s4.ID = g4.STUDENT_ID
AND g4.COURSE_ID = c4.ID
AND (g4.GRADE = 'A' OR g4.GRADE = 'B')
AND c4.COURSE_NAME = 'Intermediate Norwegian'
)
)
Like I said, the code is far from brilliant. I won't be surprised if some of you cringed at what you just read.
For instance, the entire fourth one could easily be replaced by a query where you replace the entire inner query with g.GRADE = 'A' OR 'B' and c.COURSE_NAME = 'Intermediate Norwegian'.
But like I said, that is not the point here.
Every outer sub-query has the same structure:
(SELECT 'Passed "Intermediate Norwegian" with grade >= B' AS Description, count(*) AS Number_of_students FROM
FROM PERSON p, STUDENT s, GRADE g, COURSE c
WHERE p.ID = s.PERSON_ID
AND s.ID = g.STUDENT_ID
AND g.COURSE_ID = c.ID
-- ... other complex code here
AND p.ID IN(
-- Inner Sub-query here
)
While every Sub-query has an inner one that differs from each other. Like the 1st and the 3rd one:
SELECT DISTINCT p1.ID FROM PERSON p1, STUDENT s1 WHERE p1.ID = s1.PERSON_ID AND p1.LASTNAME LIKE 'E%'
and
SELECT DISTINCT p3.ID FROM PERSON p3 WHERE p3.GENDER = 'Female'
What I need:
The real code I'm working with is far more complex, but has the same following issues as presented in the example above.
The result must be a list with several numbers, each categorized by their own distinct criteria (preferably described in the first column).
It consists of several sub-queries, joined by UNION
Each of these sub-queries are completely identical, with the exception of an inner sub-query that completely unique, and different from the others.
The resulting code is a huge beast, but could in theory be made far more readable if the outer code had been written only once, and reused with different inner code passed on as arguments.
I have recently come across the WITH-clause in Oracle SQL.
Something similar to this following change would be very beneficial:
WITH outer_sub_query AS (
SELECT 'DESCRIPTION HERE' AS Description, count(*) AS Number_of_students FROM
FROM PERSON p, STUDENT s, GRADE g, COURSE c
WHERE p.ID = s.PERSON_ID
AND s.ID = g.STUDENT_ID
AND g.COURSE_ID = c.ID
-- ... other complex code here
AND p.ID IN(
-- INSERT INNER SUB-QUERY HERE
)
)
SELECT * FROM (
outer_sub_query -- Last Names beginning with letter 'E'
UNION
outer_sub_query -- Age > 20
UNION
outer_sub_query -- All female
UNION
outer_sub_query -- Passed that course with grade >= B
)
Unfortunately, my needs are not yet satisfied. I still need to pass on the inner sub-queries, as well as descriptions. Something similar to this:
SELECT * FROM (
outer_sub_query(
'Last names beginning with letter "E",'
SELECT DISTINCT p1.ID FROM PERSON p1, STUDENT s1
WHERE p1.ID = s1.PERSON_ID AND p1.LASTNAME LIKE 'E%'
)
UNION
outer_sub_query(
'Older than 20 years.'
SELECT DISTINCT p2.ID FROM PERSON p2, STUDENT s2
WHERE p2.ID = s2.PERSON_ID AND p2.AGE > 20
)
UNION
outer_sub_query(
'All female persons'
SELECT DISTINCT p3.ID FROM PERSON p3 WHERE p3.GENDER = 'Female'
)
UNION
outer_sub_query(
'Passed "Intermediate Norwegian" with grade >= B'
SELECT DISTINCT p4.ID FROM PERSON p4, STUDENT s4, GRADE g4 AND COURSE c4
WHERE p4.ID = s4.PERSON_ID
AND s4.ID = g4.STUDENT_ID
AND g4.COURSE_ID = c4.ID
AND (g4.GRADE = 'A' OR g4.GRADE = 'B')
AND c4.COURSE_NAME = 'Intermediate Norwegian'
)
)
The questions:
Now, defining a FUNCTION easily comes to mind. But it still brings me some questions:
It first glance, it seems that WITH-clause does not take in parameters that can be passed on. Is there any other pre-existing clauses or functions in SQL or Oracle SQL that handles this?
Is it possible to extract the inner sub-query out from the outer one, and still achieve the same result? (Remember: No changes in the outer sub-query itself).
If I am to define a FUNCTION that handles this, is it possible to pass on pure SQL-codes like I have done above?
Is there any other smart solutions that I am missing?
Thank you for your advice(s).
A common table expression (such as you already suggested) seems a likely approach for reducing code duplication in your case, but you're trying to get it to do too much for you. CTEs cannot be parameterized in the way you hope; if they were, then a use such as you envision would no longer have them in common.
Yes, you could write a table-valued function, but that seems way overkill, and it could well be difficult for the query planner to analyze. Here's about as far as you can go with a CTE:
WITH student_grades AS (
SELECT
p.id AS id,
p.lastname AS lastname,
p.age AS age,
p.gender AS gender,
c.course_name AS course_name,
g.grade AS grade
FROM
-- You really, really should use ANSI JOIN syntax:
PERSON p
JOIN STUDENT s ON p.ID = s.PERSON_ID
JOIN GRADE g ON s.ID = g.STUDENT_ID
JOIN COURSE c ON g.COURSE_ID = c.ID
-- WHERE ... other complex code here
)
You might then continue your query with ...
-- 1st entry: Number of students on the last name 'E'
SELECT
'Last names beginning with letter "E"' AS Description,
count(distinct sg1.id) AS Number_of_students
FROM student_grades sg1
WHERE sg1.lastname LIKE 'E%'
UNION
-- 2nd entry: Number of students older than 20 years
SELECT
'Older than 20 years' AS Description,
count(distinct sg2.id) AS Number_of_students
FROM student_grades sg2
WHERE sg2.AGE > 20
UNION
-- 3rd entry: Number of female persons, including but not limited to students
-- NOTE: THIS ONE MATCHES YOUR ORIGINAL, WHICH IS INCORRECT
SELECT
'All female persons' AS Description,
count(distinct sg3.id) AS Number_of_students
FROM student_grades sg3
WHERE sg3.GENDER = 'Female'
UNION
-- 4th entry: Students who passed the course "Intermediate Norwegian" with grade B or higher
SELECT
'Passed "Intermediate Norwegian" with grade >= B' AS Description,
count(distinct sg4.id) AS Number_of_students
FROM student_grades sg4
WHERE
sg4.COURSE_NAME = 'Intermediate Norwegian'
AND sg4.grade IN ('A', 'B')
And that's actually a significant improvement. Note in particular that you don't need to pass conditions, subquery or not, into the CTE; instead, you query the CTE (which you could also join to other tables, etc.). Of course, in part that's because your "inner" subqueries were a pretty horrible way of doing things; instead, I use count(distinct sg.id), which achieves the same thing as those subqueries as long as person.id is non-null, which I presume it is on account of being a PK.
But note also that even the need for a distinct count (and the bugginess of the third part of the query) arise from trying to do all four parts with the same common intermediate results in the first place. You don't need to join course or grade information in order to query information related strictly to personal characteristics, and as long as student has a 0,1:1 relationship with person, leaving out the course and grade information would give you a distinct count for free.
And as for the third part, joining the student table restricts your results to students, which you didn't want. The fact that you don't put that restriction in the "inner" subquery is irrelevant; you're using that subquery to filter results that only include people who are students in the first place. Thus, *your approach cannot produce the results you want in this case.**
Maybe your desire to factor out a big chunk of common query arises from the mysterious "other complex code". I don't see how such a thing applies to the question as you've presented it, but I'm inclined to suspect that you would be better off finding a way -- or maybe separate ways per item -- to simplify or eliminate that code. If it were the case that that code could be ignored then I might write your query like so:
WITH student_person AS (
SELECT
p.lastname AS lastname,
p.age AS age,
p.gender AS gender,
s.id AS student_id
FROM
PERSON p
JOIN STUDENT s ON p.ID = s.PERSON_ID
)
-- 1st entry: Number of students on the last name 'E'
SELECT
'Last names beginning with letter "E"' AS Description,
count(*) AS Number_of_students
FROM student_person sp1
WHERE sp1.lastname LIKE 'E%'
UNION ALL
-- 2nd entry: Number of students older than 20 years
SELECT
'Older than 20 years' AS Description,
count(*) AS Number_of_students
FROM student_person sp2
WHERE sp2.AGE > 20
UNION ALL
-- 3rd entry: Number of female persons, including but not limited to students
-- NOTE: THIS ONE MATCHES YOUR ORIGINAL, WHICH IS INCORRECT
SELECT
'All female persons' AS Description,
count(*) AS Number_of_students
-- must select from PERSON, not STUDENT_PERSON:
FROM person p2
WHERE p2.GENDER = 'Female'
UNION ALL
-- 4th entry: Students who passed the course "Intermediate Norwegian" with grade B or higher
SELECT
'Passed "Intermediate Norwegian" with grade >= B' AS Description,
count(distinct sp3.student_id) AS Number_of_students
FROM student_person sp3
JOIN grades g ON sp3.student_id = g.student_id
JOIN course c ON g.course_id = c.id
WHERE
c.COURSE_NAME = 'Intermediate Norwegian'
AND g.grade IN ('A', 'B')
Take it from here, I'm sure you'll manage.
select count(case when lastname like 'e%' then 1 end) as lastname_starts_with_e
,count(case when age > 20 then 1 end) as age_greater_than_20
,count(case when gender = 'Female' then 1 end) as is_female
from person
;

SQL Query structure example for recursive lookup

I'm struggling a bit with this query. Perhaps someone can give me a hint?
My setup consists of 3 tables (Actors, Movies, and Relations). What I want to do is:
Find all actors whose name starts with an A that play in any movie alongside Person1.
That's how far i got:
SELECT Name
FROM Actors a, Movies m, Relations r
WHERE a.Name='A%'
AND Person1.keyPerson = m.KeyPerson
AND m.KeyMovies ....
An example on how to continue would be nice!
UPADATE - tables look like this:
Actors (actorsID, name)
Movies (moviesID, title)
Relations (actorsID, moviesID)
There are several ways you can do this - one method you can do is via an IN statement, pulling the movies Person1 has been in, and checking the A% person's movies.
Select Distinct A.Name
From Actors A
Join Relations R On R.ActorId = A.ActorId
Where A.Name Like 'A%'
And R.MovieId In
(
Select R2.MovieId
From Actors A2
Join Relations R2 On R2.ActorId = A2.ActorId
Where A2.Name = 'Person1'
)
Another method is via an EXISTS statement, looking for any movies that Person1 has been in that A% has also been in:
Select A.Name
From Actors A
Join Relations R On R.ActorId = A.ActorId
Where A.Name Like 'A%'
And Exists
(
Select *
From Movies M2
Join Relations R2 On R2.MovieId = M2.MovieId
Join Actors A2 On R2.ActorId = R2.ActorId
Where A2.Name = 'Person1'
And M2.MovieId = R.MovieId
)
A solution without subquery:
SELECT DISTINCT a.Name
FROM Actors a
INNER JOIN Relations r
ON r.actorsID = a.actorsID
INNER JOIN Relations r2
ON r.moviesID = r2.moviesID
INNER JOIN Actors a2
ON a2.actorsID = r2.actorsID
AND a2.Name = 'Person1'
WHERE a.Name LIKE 'A%'

How to get multiple fields from one criteria using SQL group by?

This query could return the wrong name, because the names I want to query are the ones of the smallest animals of each species. How could I get the right a.name
SELECT
a.name,
MIN(a.size)
FROM animal a
LEFT JOIN species s ON s.idSpecies = a.idAnimal
GROUP BY s.id
One approach for this, is to first find the smallest size of animal per species (as you have done), although I am assuming that species can never be null since an animal must belong to a species, it also does not require a join to species at this point:
SELECT a.IDSpecies, MIN(a.Size) AS Size
FROM Animal AS a
GROUP BY a.IDSpecies
Now you can join the result of this query back to your main query to filter the results.
SELECT a.Name AS AnimalName,
a.Size,
s.Name AS SpeciesName
FROM Animal AS a
INNER JOIN Species AS s
ON s.ID = a.IDSpecies
INNER JOIN
( SELECT a.IDSpecies, MIN(a.Size) AS Size
FROM Animal AS a
GROUP BY a.IDSpecies
) AS ma
ON ma.IDSpecies = a.IDSpecies
AND ma.Size = a.Size;
Another way of doing this, is to use NOT EXISTS:
SELECT a.Name AS AnimalName,
a.Size,
s.Name AS SpeciesName
FROM Animal AS a
INNER JOIN Species AS s
ON s.ID = a.IDSpecies
WHERE NOT EXISTS
( SELECT 1
FROM Animal AS a2
WHERE a2.IDSpecies = a.IDSpecies
AND a2.Size < a.Size
);
So you start with a simple select, then use NOT EXISTS to remove any animals, where a smaller animal exists in the same species.
Since MySQL will optimize LEFT JOIN/IS NULL better than NOT EXISTS, then the better way of writing the query in MySQL is:
SELECT a.Name AS AnimalName,
a.Size,
s.Name AS SpeciesName
FROM Animal AS a
INNER JOIN Species AS s
ON s.ID = a.IDSpecies
LEFT JOIN Animal AS a2
ON a2.IDSpecies = a.IDSpecies
AND a2.Size < a.Size
WHERE a2.ID IS NULL;
The concept is exactly the same as NOT EXISTS, but does not require a correlated subquery.
Following the simple example of Group By :
SELECT C.CountryName Country, SN.StateName, COUNT(CN.ID) CityCount
FROM Table_StatesName SN
JOIN Table_Countries C ON C.ID = SN.CountryID
JOIN Table_CityName CN ON CN.StateID = SN.ID
GROUP BY C.CountryName, SN.StateName ORDER BY SN.StateName
OUTPUT :
Country StateName CityCount
Australia Australian Capital Territory 219
Australia New South Wales 2250
Australia Northern Territory 218
Australia Queensland 2250
Australia South Australia 1501
Australia Tasmania 613
Australia Victoria 2250
Get all rows with size = smallest size. Use a correlated sub-quert to find that smallest size:
SELECT *
FROM animal a
LEFT JOIN species s ON s.idSpecies = a.idAnimal
where a.size = (select min(a2.size)
from animal a2
LEFT JOIN species s2 ON ss.idSpecies = as.idAnimal
where s.id = s2.id)

SQL query tutorials

Given the following tables (i wrote it in this manner to keep things neater)
airline (name, telephone, designator)
flight (name, number, departure, arrival, origin, destination)
airport (name, country, designator)
I would like to print the pairs of names of competing airline. Airlines with the same route or having the same origin and destination. Removing the symmetry from the result as an airline does not compete with itself. Would this work
SELECT a1.name, a2.name
FROM airline a1, airline a2, flight f1, flight f2
WHERE
a1.name <> a2.name
AND a1.name = f1.name
AND a2.name = f2.name
AND f1.origin = f2.origin
AND f1.destination = f2.destination
GROUP BY
a1.name, a2.name;
Then, print the names of the airlines that have flights to all listed airports in thailand.
SELECT DISTINCT a.name
FROM airline a, flight f
WHERE f.destination = ALL(SELECT a.name
FROM airport a
WHERE a.country = 'Thailand');
The first query seem fine, but the second won't work as using ALL in the way you do states that the destination must be equal to all the airport names returned from the subquery which it obviously can't be.
A solution is to write the query on the form show the name of all airlines for which there are no Thai airports that are not in the list of destinations that the airline traffics:
SELECT a.name
FROM airline a
JOIN flight f ON a.name = f.name
WHERE NOT EXISTS (
SELECT name
FROM airport
WHERE country = 'Thailand'
AND name NOT IN (
SELECT destination
FROM flight
WHERE f.name = name
)
)
GROUP BY a.name;