SQL one-to-many tables group by - sql

Let consider example: I have following tables - TableA with people and TableB containing language skills of these people. Each row describing person can have none, one or more rows in TableB. Example below:
People
+-----+--------+
| pId | Name |
+-----+--------+
| 0 | Thomas |
| 1 | Henry |
| 2 | John |
+-----+--------+
Skills
+-----+-----+----------+---------------+
| lID | pId | Language | LanguageSkill |
+-----+-----+----------+---------------+
| 0 | 0 | Dutch | 0 |
| 1 | 0 | French | 4 |
| 2 | 0 | Italian | 2 |
| 3 | 2 | Italian | 2 |
+-----+-----+----------+---------------+
Thomas knows dutch, french and italian, Henry doesn't know any foreign language and John knows italian.
What I want to get is the best known language for each person from TableA:
+--------+----------+
| Name | Language |
+--------+----------+
| Thomas | French |
| Henry | NULL |
| John | Italian |
+--------+----------+
I have feeling that is quite easy thing, but don't have idea how to achieve it in a simple way.
Thanks for your responses.

You need to get the best language for each person using the following query:
SELECT pid, language
from TableB
group by pid
having languageskill = max(languageskill)
Then you join it onto the People table:
SELECT a.name, b.language
from TableA a
LEFT JOIN
(
SELECT pid, language, languageskill
from TableB
group by pid
having languageskill = max(languageskill)
) b
ON a.pid = b.pid
Of course, this method would not give more than one row if the person had a 'tied' best language, and you would lose that data about the 'tied' best language.

Related

PostgreSQL check if value exists in another table

I'm trying to find a solution in PostgreSQL of how I can add to the output of the query extra column with value if id exists in another table or not:
I need several things:
Do a join between two tables
Add a new column into the result output where I check if exists in the third table or not
My tables:
announcement
author
chapter
announcement table
| id | author_id | date | group_id | ... |
author table
| id | name | email | ... |
chapter table
| id | announcement_id | ... |
This is what I have now. I did a left outer join and it works as I expected:
select announcement.id, announcement.date, author.id as publisher_id, author.name as publisher_name
from announcement
left outer join author
on announcement.author_id = author.id
where announcement.group_id = 123 and announcement.date >= '2022-06-01'::date;
with output:
| id | date | publisher_id | publisher_name |
| 1 | 2020-07-01 | 12 | John |
| 2 | 2020-07-04 | 123 | Arthur |
Now I can't find a solution of how to add an extra column with_chapters to the query response, where I will check if announcement.id exists in chapter table under announcement_id column.
For example, chapter table can have such data:
| id | announcement_id |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
So we see that some announcements can appear in chapters several times (so i'm looking for at least 1 match). And some announcements doesn't have chapters at all.
Output finally should be like that:
| id | date | publisher_id | publisher_name | with_chapters |
| 1 | 2020-07-01 | 12 | John | true |
| 2 | 2020-07-04 | 123 | Arthur | false |
Thanks a lot for any help :)
While EXISTS (subquery) is usually used in the WHERE clause, it returns an ordinary Boolean and so can be used in the select list.
SELECT blah1, blah2,
EXISTS (select 1 from chapter where chapter.announcement_id=announcement.id) as with_chapter
FROM ...

For each unique entry, include all rows from another list

I have 2 tables as such:
cars: contains price of some parts for each car
| Car | Parts | Price |
| -------- | -------------- | -------|
| A | Windshield | 100 |
| A | Rims | 50 |
| B | Bumper | 200 |
| B | Rims | 60 |
parts: contains all possible parts for a car
| Parts |
|--------------|
| Windshield |
| Rims |
| Bumper |
| Headlights |
I want each car in cars to have every entry in parts. The end result should look like this:
| Car | Parts | Price |
| -------- | -------------- | -------|
| A | Windshield | 100 |
| A | Rims | 50 |
| A | Bumper | 0 |
| A | Headlights | 0 |
| B | Bumper | 200 |
| B | Rims | 60 |
| B | Windshield | 0 |
| B | Headlights | 0 |
Any ideas on how I could do this?
PS: The order matters less
You may use a calendar table approach:
SELECT c.Car, p.Parts, COALESCE(t.Price, 0) AS Price
FROM (SELECT DISTINCT Car FROM cars) c
CROSS JOIN parts p
LEFT JOIN cars t
ON t.Car = c.Car AND t.Parts = p.Parts
ORDER BY c.Car, p.Parts;
But as #Larnu has correctly pointed out in his comment, your schema should have a separate table containing all cars. This would avoid the distinct select I have in my answer above.
If you want to insert the additional rows into cars, then the code would look like:
insert into cars (car, parts, price)
select c.car, p.part, 0
from (select distinct car from cars) c cross join
parts p
where not exists (select 1
from cars c2
where c2.car = c.car and c2.part = c.part
);
This generates all combinations of cars and parts. It then filters out the ones that don't already exist in cars.
Note that left join and not exists are pretty much the same in this context. In an insert query, though, I usually use not exists because I think the intention is clearer.

SQL on Self Table Join

I am trying to do a simple self-join SQL and a join to a 2nd table and for the life of me I can't figure it out. I've done some research and can't seem to glean the answer from similar questions. This query is for MS-Access running in VB.NET.
I have 2 tables:
TodaysTeams
-----------
TeamNum PlayerName PlayerID
------- ---------- --------
1 Mark 100
1 Brian 101
2 Mike 102
2 Mike 102
(Note the last 2 rows above are not a typo. In this case a player can be paired with themselves to form a team)
TodaysTeamsPoints
-----------------
TeamNum Points
------- ------
1 90
2 85
The result I want is (2 rows, 1 for each team):
TeamNum PlayerName1 PlayerName2 Points
------- ----------- ----------- ------
1 Mark Brian 90
2 Mike Mike 85
Here is my SQL:
SELECT DISTINCT A.TeamNum, A.PlayerName as PlayerName1, B.PlayerName AS PlayerName2, C.Points
FROM ((TodaysTeams A INNER JOIN
TodaysTeamsPoints C ON A.TeamNum = C.TeamNum) INNER JOIN
TodaysTeams B ON A.TeamNum = B.TeamNum)
ORDER BY C.Points DESC
I know I am missing another join as I'm returning a cartesian produce (i.e. too many rows).
I would appreciate help as to what I am missing here.
Thank you.
Whilst Gordon's suggested method will work well providing that there are at most two players per team, the method breaks down if ever you add another team member and wish to display them in a separate column.
The difficulty in displaying the data in a manner that you can describe logically but cannot easily produce using a query usually implies that the database structure is sub optimal.
For your particular setup, I would personally recommend the following structure:
+---------------+ +----------+------------+
| Players | | PlayerID | PlayerName |
+---------------+ +----------+------------+
| PlayerID (PK) | | 100 | Mark |
| PlayerName | | 101 | Brian |
+---------------+ | 102 | Mike |
+----------+------------+
+-------------+ +--------+----------+
| Teams | | TeamID | TeamName |
+-------------+ +--------+----------+
| TeamID (PK) | | 1 | Team1 |
| TeamName | | 2 | Team2 |
+-------------+ +--------+----------+
+-------------------+ +--------+--------------+----------+
| TeamPlayers | | TeamID | TeamPlayerID | PlayerID |
+-------------------+ +--------+--------------+----------+
| TeamID (PK) | | 1 | 1 | 100 |
| TeamPlayerID (PK) | | 1 | 2 | 101 |
| PlayerID (FK) | | 2 | 1 | 102 |
+-------------------+ | 2 | 2 | 102 |
+--------+--------------+----------+
Using this method, you can use condition aggregation or a crosstab query pivoting on the TeamPlayerID to produce each of your columns, and you would not be limited to two columns.
You can use aggregation:
SELECT ttp.TeamNum, MIN(tt.PlayerName) as PlayerName1,
MAX(tt.PlayerName) as PlayerName2,
ttp.Points
FROM TodaysTeamsPoints as ttp INNER JOIN
TodaysTeams as tt
ON tt.TeamNum = ttp.TeamNum
GROUP BY ttp.TeamNum, ttp.Points
ORDER BY ttp.Points DESC;

Get full name based on username for 2 columns

I am looking for an output like:
| BOOK | ANALYST | SUPERVISOR |
|-------|----------------|--------------------|
| BookA | (null) | Dani Sant |
| BookB | (null) | North Andre Miles |
| BookC | Andrea Plus | Andrea Plus |
| BookD | Jeff Dron Math | Jeff Dron Math |
| BookE | Theo Phillip | Julian Rhode |
What I am getting is:
| BOOK | ANALYST | SUPERVISOR |
|-------|----------------|--------------|
| BookA | (null) | dani.sant |
| BookB | (null) | north.miles |
| BookC | Andrea Plus | andrea.plus |
| BookD | Jeff Dron Math | jeff.math |
| BookE | Theo Phillip | julian.rhode |
I can do the join with one column, but when I try for both, the result isn't showing like it should. Thanks for any information on this.
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE books
(
book varchar(10),
analyst varchar(100),
supervisor varchar(100)
);
INSERT INTO books (book, analyst, supervisor)
VALUES
('BookA', NULL, 'dani.sant'),
('BookB', NULL, 'north.miles'),
('BookC', 'andrea.plus', 'andrea.plus'),
('BookD', 'jeff.math', 'jeff.math'),
('BookE', 'theo.phil', 'julian.rhode');
CREATE TABLE names
(
username varchar(100),
fullname varchar(500)
);
INSERT INTO names (username, fullname)
VALUES
('dani.sant', 'Dani Sant'),
('north.miles', 'North Andre Miles'),
('andrea.plus', 'Andrea Plus'),
('jeff.math', 'Jeff Dron Math'),
('theo.phil', 'Theo Phillip'),
('julian.rhode', 'Julian Rhode');
Query 1:
SELECT
books.book AS Book,
names.fullname AS Analyst,
books.supervisor AS Supervisor
FROM
books left join names on books.analyst = names.username
Results:
| BOOK | ANALYST | SUPERVISOR |
|-------|----------------|--------------|
| BookA | (null) | dani.sant |
| BookB | (null) | north.miles |
| BookC | Andrea Plus | andrea.plus |
| BookD | Jeff Dron Math | jeff.math |
| BookE | Theo Phillip | julian.rhode |
You need a second join to the names table to get the supervisor's full name:
SELECT b.book AS Book, bn.fullname AS Analyst,
sn.fullname AS Supervisor
FROM books b left join
names bn
on b.analyst = bn.username left join
names sn
on b.supervisor = sn.username;
Below will provide the output you desire.
SELECT
b.book AS Book,
n.fullname AS Analyst,
(SELECT fullname FROM names where username=b.Supervisor) AS Supervisor
FROM
books b left join names n on b.analyst = n.username

Split column into two columns based on type code in third column

My SQL is very rusty. I'm trying to transform this table:
+----+-----+--------------+-------+
| ID | SIN | CONTACT | TYPE |
+----+-----+--------------+-------+
| 1 | 737 | b#bacon.com | email |
| 2 | 760 | 250-555-0100 | phone |
| 3 | 737 | 250-555-0101 | phone |
| 4 | 800 | 250-555-0102 | phone |
| 5 | 850 | l#lemon.com | email |
+----+-----+--------------+-------+
Into this table:
+----+-----+--------------+-------------+
| ID | SIN | PHONE | EMAIL |
+----+-----+--------------+-------------+
| 1 | 737 | 250-555-0101 | b#bacon.com |
| 2 | 760 | 250-555-0100 | |
| 4 | 800 | 250-555-0102 | |
| 5 | 850 | | l#lemon.com |
+----+-----+--------------+-------------+
I wrote this query:
SELECT *
FROM (SELECT *
FROM people
WHERE TYPE = 'phone') phoneNumbers
FULL JOIN (SELECT *
FROM people
WHERE TYPE = 'email') emailAddresses
ON phoneNumbers.SIN = emailAddresses.SIN;
Which produces:
+----+-----+--------------+-------+------+-------+-------------+--------+
| ID | SIN | CONTACT | TYPE | ID_1 | SIN_1 | CONTACT_1 | TYPE_1 |
+----+-----+--------------+-------+------+-------+-------------+--------+
| 2 | 760 | 250-555-0100 | phone | | | | |
| 3 | 737 | 250-555-0101 | phone | 1 | 737 | b#bacon.com | email |
| 4 | 800 | 250-555-0102 | phone | | | | |
| | | | | 5 | 850 | l#lemon.com | email |
+----+-----+--------------+-------+------+-------+-------------+--------+
I know that I can select the columns I want, but the SIN column is incomplete. I seem to recall that I should join in the table a third time to get a complete SIN column, but I cannot remember how.
How can I produce my target table (ID, SIN, PHONE, EMAIL)?
Edit and clarification: I am grateful for the answers I have received so far, but as a SQL greenhorn I am unfamiliar with the techniques you are using (case statements, conditional aggregation, and pivoting). Can this not be done using JOIN and SELECT? Please excuse my ignorance in this matter. (It's not that I am not interested in superior techniques, but I do not want to move too fast too soon.)
One way to approach this is conditional aggregation:
select min(ID), SIN,
max(case when type = 'phone' then contact end) as phone,
max(case when type = 'email' then contact end) as email
from people t
group by sin;
Seems a pivot (oracle.com) would work easily here.
SELECT ID, SIN, PHONE, EMAIL
FROM PEOPLE
PIVOT (
MAX(CONTACT)
FOR TYPE IN ('EMAIL', 'PHONE')
)
I realize this is less elegant than all the solutions posted, but here it is anyhow, a solution using only JOIN and SELECT:
SELECT sins.SIN, phone, email
FROM ((SELECT SIN email_sin, contact email
FROM people
WHERE TYPE = 'email') email
FULL JOIN (SELECT SIN phone_sin, contact phone
FROM people
WHERE TYPE = 'phone') phone
ON email.email_sin = phone.phone_sin)
RIGHT JOIN (SELECT DISTINCT SIN FROM people) sins
ON sins.SIN = phone_sin OR sins.SIN = email_sin;
This lacks the ID column.