SQL - using ANSI standard - sql

My below query using two select statements works:
create or replace view q2 as
select count(p.id) nstudents,
(select count(p.id) nstaff
from people p, staff s where p.id = s.id)
from people p, students s where p.id = s.id;
but when i include a third sub query:
create or replace view q2 as
select count(p.id) nstudents,
(select count(p.id) nstaff,
(select count(p.id) nboth
from people p, students s, staff t where p. id = s.id and p.id = t.id)
from people p, staff t where p.id = t.id)
from people p, students s where p.id = s.id;
it gives me the following error:
ERROR: subquery must return only one column
LINE 3: (select count(p.id) nstaff,
Am i making some mistake while including a third query or is there a limit of just 2 nested select statements?

The version with the additional sub-query fails because you insert the second sub-query into the first, when it should come after it like this:
select count(p.id) nstudents,
(select count(p.id) from people p, staff t where p.id = t.id) nstaff ,
(select count(p.id) from people p, students s, staff t where p. id = s.id and p.id = t.id) nboth
from people p, students s where p.id = s.id;
However, the query could just as well be written using conditional aggregation (and to use explicit ANSI standard joins) like this:
select
sum(case when s.id is not null then 1 end) nstudents,
sum(case when t.id is not null then 1 end) nstaff,
sum(case when s.id is not null and t.id is not null then 1 end) nboth
from people p
left join students s on p. id = s.id
left join staff t on p.id = t.id

Related

How to create columns for different fields without applying the pivoting function

So i have a question for the tough hearted! I have had an issue with this concept for awhile and I need it to be cleared...
The following code shows students who have more than 1 language
SELECT DISTINCT s.studentnumber as studentnr,
p.firstname AS name,
sl.gradenumber as gradenumber,
string_agg(DISTINCT l.text, ', ') as languages
FROM student s
JOIN pupil p ON p.id = s.pupilid
JOIN pupillanguage pl on pl.pupilid = p.id
JOIN language l on l.id = pl.languageid
JOIN schoollevel sl ON sl.id = p.schoollevelid
GROUP BY s.studentnumber,
p.firstname
Result
Now my question is simple yet I'm not sure how the answer would be... I want to split up those languages and put them in seperate fields so its looks something like this
Wanted Result
There is also 1 major issue. This is using Postgresql 9.3 which doesnt now allow grouping set or rollup or cube. I tried this code however and its not working correctly, im not sure how to deal with it and im not sure how to go forward about it...
(select distinct l.text
from language
join pupillanguage pl2 on pl2.languageid = language.id
join pupil on p.id = pl2.personid
limit 1) as language1
This is how the Tables looks like
Language Table
iD PK
shorttext char varying
text char varying
PupilLanguage Table
id PK
languageid FK
personid FK
displayorder int
If you have a fixed number of columns, you can use an array:
select studentnr, name, gradenumber,
languages[1] as language_1,
languages[2] as language_2,
languages[3] as language_3,
languages[4] as language_4,
languages[5] as language_5
FROM (
SELECT s.studentnumber as studentnr,
p.firstname AS name,
sl.gradenumber as gradenumber,
array_agg(DISTINCT l.text) as languages
FROM student s
JOIN pupil p ON p.id = s.pupilid
JOIN pupillanguage pl on pl.pupilid = p.id
JOIN language l on l.id = pl.languageid
JOIN schoollevel sl ON sl.id = p.schoollevelid
GROUP BY s.studentnumber, p.firstname
) t
Note that distinct is usually not needed when you use group by
A SQL query returns a fixed number of columns. If you want just three additional columns, then you can use dynamic aggregation:
with t as (
SELECT s.studentnumber as studentnr, p.firstname AS name,
sl.gradenumber as gradenumber, l.text as language,
dense_rank() over (partition by s.studentnumber, p.firstname, sl.gradenumber order by l.text) as seqnum
FROM student s JOIN
pupil p
ON p.id = s.pupilid JOIN
pupillanguage pl
ON pl.pupilid = p.id JOIN
language l
ON l.id = pl.languageid JOIN
schoollevel sl
ON sl.id = p.schoollevelid
)
select studentnr, name, gradenumber,
max(case when seqnum = 1 then language end) as language_1,
max(case when seqnum = 2 then language end) as language_2,
max(case when seqnum = 3 then language end) as language_3
from t
group by studentnr, name, gradenumber;
However, if you want a variable number, then you need crosstab or dynamic SQL. I might also suggest that you consider arrays instead.

How to make LEFT JOIN with row having max date?

I have two tables in Oracle DB
Person (
id
)
Bill (
id,
date,
amount,
person_id
)
I need to get person and amount from last bill if exist.
I trying to do it this way
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id AND b.date = (SELECT MAX(date) FROM Bill WHERE person_id = 1)
WHERE p.id = 1;
But this query works only with INNER JOIN. In case of LEFT JOIN it throws ORA-01799 a column may not be outer-joined to a subquery
How can I get amoun from the last bill using left join?
Please try the below avoiding sub query to be outer joined
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN(select * from Bill where date =
(SELECT MAX(date) FROM Bill b1 WHERE person_id = 1)) b ON b.person_id = p.id
WHERE p.id = 1;
What you are looking for is a way to tell in bills, for each person, what is the latest record, and that one is the one to join with. One way is to use row_number:
select * from person p
left join (select b.*,
row_number() over (partition by person_id order by date desc) as seq_num
from bills b) b
on p.id = b.person_id
and seq_num = 1
You cannot have a subquery inside an ON statement.
Instead you need to convert your LEFT JOIN statement into a whole subquery.
Not tested but this should work.
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN (
SELECT id FROM Bill
WHERE person_id = p.id
AND date = (SELECT date FROM Bill WHERE person_id = 1)) b
WHERE p.id = 1;
I'm not quite sure why you would want to filter for the date though.
Simply filtering for the person_id should do the trick
you should join Person and Bill to the result for max date in bill related to person_id
select Person.id, bill.amount
from Person
left join bill on bill.person_id = person.id
left join (
select person_id, max(date) as max_date
from bill
group by person_id ) t on t.person_id = Person.id and b.date = t.max_date
Hey you can do like this
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id AND b.date = (SELECT max(date) FROM Bill WHERE person_id = 1)
WHERE p.id = 1
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id
WHERE (SELECT max(date) FROM bill AS sb WHERE sb.person_id=p.id LIMIT 1)=b.date;
SELECT
p.id,
c.amount
FROM Person p
LEFT JOIN (select b.person_id as personid,b.amount as amount from Bill b where b.date1= (select max(date1) from Bill where person_id=1)) c
ON c.personid = p.id
WHERE p.id = 1;
try this
select * from person p
left join (select MAX(id) KEEP (DENSE_RANK FIRST ORDER BY date DESC)
from bills b) b
on p.id = b.person_id
I use GREATEST() function in join condition:
SELECT
p.id,
b.amount
FROM Person p
LEFT JOIN Bill b
ON b.person_id = p.id
AND b.date = GREATEST(b.date)
WHERE p.id = 1
This allows you to grab the whole row if necessary and grab the top x rows
SELECT p.id
,b.amount
FROM person p
LEFT JOIN
(
SELECT * FROM
(
SELECT date
,ROW_NUMBER() OVER (PARTITION BY person_id ORDER BY date DESC) AS row_num
FROM bill
)
WHERE row_num = 1
) b ON p.id = b.person_id
WHERE p.id = 1
;

SQL: How to save order in sql query?

I have PostgreSQL database and I try to print all my users (Person).
When I execute this query
-- show owners
-- sorted by maximum cars amount
SELECT p.id
FROM car c JOIN person p ON c.person_id = p.id
GROUP BY p.id
ORDER BY COUNT(p.name) ASC;
I get all owners sorted by cars amount
Output: 3 2 4 1
And all order goes wrong when I try to link owner id.
SELECT *
FROM person p
WHERE p.id IN (
SELECT p.id
FROM car c JOIN person p ON c.person_id = p.id
GROUP BY p.id
ORDER BY COUNT(p.name) ASC);
Output: 1 2 3 4 and other data
You see than order is wrong. So here is my question how can I save that order?
Instead Of subquery use join. Try this.
SELECT p.*
FROM person p
JOIN (SELECT p.id,
Count(p.NAME)cnt
FROM car c
JOIN person p
ON c.person_id = p.id
GROUP BY p.id) b
ON p.id = b.id
ORDER BY cnt ASC
Untangle the mess. Aggregate first, join later:
SELECT p.*
FROM person p
JOIN (
SELECT person_id, count(*) AS ct
FROM car
GROUP BY person_id
) c ON c.person_id = p.id
ORDER BY c.cnt;
No need to join to person twice. This should be fastest if you count most or all rows.
For a small selection, correlated subqueries are faster:
SELECT p.*
FROM person p
ORDER BY (SELECT count(*) FROM car c WHERE c.person_id = p.id)
WHERE p.id BETWEEN 10 AND 20; -- some very selective predicate
As for your original: IN takes a set on the right hand, order of elements is ignored, so ORDER BY is pointless in the subuery.

SQL Query to Count() multiple tables

I have a table which has several one to many relationships with other tables. Let's say the main table is a person, and the other tables represent pets, cars and children. I would like a query that returns details of the person,the number of pets, cars and children they have e.g.
Person.Name Count(cars) Count(children) Count(pets)
John Smith 3 2 4
Bob Brown 1 3 0
What is the best way to do this?
Subquery Factoring (9i+):
WITH count_cars AS (
SELECT t.person_id
COUNT(*) num_cars
FROM CARS c
GROUP BY t.person_id),
count_children AS (
SELECT t.person_id
COUNT(*) num_children
FROM CHILDREN c
GROUP BY t.person_id),
count_pets AS (
SELECT p.person_id
COUNT(*) num_pets
FROM PETS p
GROUP BY p.person_id)
SELECT t.name,
NVL(cars.num_cars, 0) 'Count(cars)',
NVL(children.num_children, 0) 'Count(children)',
NVL(pets.num_pets, 0) 'Count(pets)'
FROM PERSONS t
LEFT JOIN count_cars cars ON cars.person_id = t.person_id
LEFT JOIN count_children children ON children.person_id = t.person_id
LEFT JOIN count_pets pets ON pets.person_id = t.person_id
Using inline views:
SELECT t.name,
NVL(cars.num_cars, 0) 'Count(cars)',
NVL(children.num_children, 0) 'Count(children)',
NVL(pets.num_pets, 0) 'Count(pets)'
FROM PERSONS t
LEFT JOIN (SELECT t.person_id
COUNT(*) num_cars
FROM CARS c
GROUP BY t.person_id) cars ON cars.person_id = t.person_id
LEFT JOIN (SELECT t.person_id
COUNT(*) num_children
FROM CHILDREN c
GROUP BY t.person_id) children ON children.person_id = t.person_id
LEFT JOIN (SELECT p.person_id
COUNT(*) num_pets
FROM PETS p
GROUP BY p.person_id) pets ON pets.person_id = t.person_id
you could use the COUNT(distinct x.id) synthax:
SELECT person.name,
COUNT(DISTINCT car.id) cars,
COUNT(DISTINCT child.id) children,
COUNT(DISTINCT pet.id) pets
FROM person
LEFT JOIN car ON (person.id = car.person_id)
LEFT JOIN child ON (person.id = child.person_id)
LEFT JOIN pet ON (person.id = pet.person_id)
GROUP BY person.name
I would probably do it like this:
SELECT Name, PersonCars.num, PersonChildren.num, PersonPets.num
FROM Person p
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Cars ON Cars.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonCars ON PersonCars.PersonID = p.PersonID
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Children ON Children.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonChildren ON PersonChildren.PersonID = p.PersonID
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Pets ON Pets.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonPets ON PersonPets.PersonID = p.PersonID
Note, that it depends on your flavour of RDBMS, whether it supports nested selects like the following:
SELECT p.name AS name
, (SELECT COUNT(*) FROM pets e WHERE e.owner_id = p.id) AS pet_count
, (SELECT COUNT(*) FROM cars c WHERE c.owner_id = p.id) AS world_pollution_increment_device_count
, (SELECT COUNT(*) FROM child h WHERE h.parent_id = p.id) AS world_population_increment
FROM person p
ORDER BY p.name
IIRC, this works at least with PostgreSQL and MSSQL. Not tested, so your mileage may vary.
Using subselects not very good practice, but may be here it will be good
select p.name, (select count(0) from cars c where c.idperson = p.idperson),
(select count(0) from children ch where ch.idperson = p.idperson),
(select count(0) from pets pt where pt.idperson = p.idperson)
from person p
You could do this with three outer joins:
SELECT
Person.Name,
sum(case when cars.id is not null then 1 else 0 end) car_count,
sum(case when children.id is not null then 1 else 0 end) child_count,
sum(case when pets.id is not null then 1 else 0 end) pet_count
FROM
Person
LEFT OUTER JOIN
cars on
Person.id = cars.person_id
LEFT OUTER JOIN
children on
Person.id = children.person_id
LEFT OUTER JOIN
pets on
Person.id = pets.person_id
GROUP BY
Person.Name
I belive that Oracle now supports the case when syntax, but if not you could use a decode.
You'd need to include multiple count statements in the query. Off the top of my head,
SELECT p.Name, COUNT(DISTINCT t.Cars), COUNT(DISTINCT o.Children), Count(DISTINCT p.Pets)
FROM Person p
INNER JOIN Transport t ON p.ID = t.PersonID
LEFT JOIN Offspring o ON p.ID = o.PersonID
LEFT JOIN Pets p ON p.ID = o.OwnerID
GROUP BY p.Name
ORDER BY p.Name

How to count number of different items in SQL

Database structure:
Clubs: ID, ClubName
Teams: ID, TeamName, ClubID
Players: ID, Name
Registrations: PlayerID, TeamID, Start_date, End_date, SeasonID
Clubs own several teams. Players may get registered into several teams (inside same club or into different club) during one year.
I have to generate a query to list all players that have been registered into DIFFERENT CLUBS during one season. So if player swapped teams that were owned by the same club then it doesn't count.
My attempts so far:
SELECT
c.short_name,
p.surname,
r.start_date,
r.end_date,
(select count(r2.id) from ejl_registration as r2
where r2.player_id=r.player_id and r2.season=r.season) as counter
FROM
ejl_registration AS r
left Join ejl_players AS p ON p.id = r.player_id
left Join ejl_teams AS t ON r.team_id = t.id
left Join ejl_clubs AS c ON t.club_id = c.id
WHERE
r.season = '2008'
having counter >1
I can't figure out how to count and show only different clubs... (It's getting too late for clear thinking). I use MySQL.
Report should be like: Player name, Club name, Start_date, End_date
This is a second try at this answer, simplifying it to merely count the distinct clubs, not report a list of club names.
SELECT p.surname, r.start_date, r.end_date, COUNT(DISTINCT c.id) AS counter
FROM ejl_players p
JOIN ejl_registration r ON (r.player_id = p.id)
JOIN ejl_teams t ON (r.team_id = t.id)
JOIN ejl_clubs c ON (t.club_id = c.id)
WHERE r.season = '2008'
GROUP BY p.id
HAVING counter > 1;
Note that since you're using MySQL, you can be pretty flexible with respect to columns in the select-list not matching columns in the GROUP BY clause. Other brands of RDBMS are more strict about the Single-Value Rule.
There's no reason to use a LEFT JOIN as in your example.
Okay, here's the first version of the query:
You have a chain of relationships like the following:
club1 <-- team1 <-- reg1 --> player <-- reg2 --> team2 --> club2
Such that club1 must not be the same as club2.
SELECT p.surname,
CONCAT_WS(',', GROUP_CONCAT(DISTINCT t1.team_name),
GROUP_CONCAT(DISTINCT t2.team_name)) AS teams,
CONCAT_WS(',', GROUP_CONCAT(DISTINCT c1.short_name),
GROUP_CONCAT(DISTINCT c2.short_name)) AS clubs
FROM ejl_players p
-- Find a club where this player is registered
JOIN ejl_registration r1 ON (r1.player_id = p.id)
JOIN ejl_teams t1 ON (r1.team_id = t1.id)
JOIN ejl_clubs c1 ON (t1.club_id = c1.id)
-- Now find another club where this player is registered in the same season
JOIN ejl_registration r2 ON (r2.player_id = p.id AND r1.season = r2.season)
JOIN ejl_teams t2 ON (r2.team_id = t2.id)
JOIN ejl_clubs c2 ON (t2.club_id = c2.id)
-- But the two clubs must not be the same (use < to prevent duplicates)
WHERE c1.id < c2.id
GROUP BY p.id;
Here's a list of players for one season.
SELECT sub.PlayerId
FROM
(
SELECT
r.PlayerId,
(SELECT t.ClubID FROM Teams t WHERE r.TeamID = t.ID) as ClubID
FROM Registrations r
WHERE r.Season = '2008'
) as sub
GROUP BY PlayerId
HAVING COUNT(DISTINCT sub.ClubID) > 1
Here's a list of players and seasons, for all seasons.
SELECT PlayerId, Season
FROM
(
SELECT
r.PlayerId,
r.Season,
(SELECT t.ClubID FROM Teams t WHERE r.TeamID = t.ID) as ClubID
FROM Registrations r
) as sub
GROUP BY PlayerId, Season
HAVING COUNT(DISTINCT sub.ClubID) > 1
By the way, this works in MS SQL.
SELECT p.Name, x.PlayerID, x.SeasonID
FROM (SELECT DISTINCT r.PlayerID, r.SeasonID, t.ClubID
FROM Registrations r
JOIN Teams t ON t.ID = r.TeamID) x
JOIN Players p ON p.ID = x.PlayerID
GROUP BY p.rName, x.PlayerID, x.SeasonID
HAVING COUNT(*) > 1