Resolve many to many relationship in SQL - postgresql

I'm using Postgresql. Let's say I have 3 tables:
Classes
id | name
1 | Biology
2 | Math
Students
id | name
1 | John
2 | Jane
Student_Classes
id | student_id | class_id | registration_token
1 | 1 | 1 | abc
2 | 1 | 2 | def
3 | 2 | 1 | zxc
I want to obtain a result set like this:
Results
student_name | biology | math
John | abc | def
Jane | zxc | NULL
I can get this result set with this query:
SELECT
student.name as student_name,
biology.registration_token as biology,
math.registration_token as math
FROM
Students
LEFT JOIN (
SELECT registration_token FROM Student_Classes WHERE class_id = (
SELECT id FROM Classes WHERE name = 'Biology'
)
) AS biology
ON Students.id = biology.student_id
LEFT JOIN (
SELECT registration_token FROM Student_Classes WHERE class_id = (
SELECT id FROM Classes WHERE name = 'Math'
)
) AS math
ON Students.id = math.student_id
Is there a way to get this same result set without having a join statement for each class? With this solution, if I want to add a class, I need to add another join statement.

You can do this via postgresql tablefunc extension crosstab but such presentation requirements may be handled better outside of sql.

Related

Reset column with numeric value that represents the order when destroying a row

I have a table of users that has a column called order that represents the order in they will be elected.
So, for example, the table might look like:
| id | name | order |
|-----|--------|-------|
| 1 | John | 2 |
| 2 | Mike | 0 |
| 3 | Lisa | 1 |
So, say that now Lisa gets destroyed, I would like that in the same transaction that I destroy Lisa, I am able to update the table so the order is still consistent, so the expected result would be:
| id | name | order |
|-----|--------|-------|
| 1 | John | 1 |
| 2 | Mike | 0 |
Or, if Mike were the one to be deleted, the expected result would be:
| id | name | order |
|-----|--------|-------|
| 1 | John | 1 |
| 3 | Lisa | 0 |
How can I do this in PostgreSQL?
If you are just deleting one row, one option uses a cte and the returning clause to then trigger an update
with del as (
delete from mytable where name = 'Lisa'
returning ord
)
update mytable
set ord = ord - 1
from del d
where mytable.ord > d.ord
As a more general approach, I would really recommend trying to renumber the whole table after every delete. This is inefficient, and can get tedious for multi-rows delete.
Instead, you could build a view on top of the table:
create view myview as
select id, name, row_number() over(order by ord) ord
from mytable

How to get unique values out of a GROUP BY clause

I have a table that has a unique identifier column, a relational key column, and a varchar column.
| Id | ForeignId | Fruits |
---------------------------------
| 1 | 1 | Apple |
| 2 | 2 | Apple |
| 3 | 3 | Apple |
| 4 | 4 | Banana |
What I would like to do, is group the data by the Fruits column, and also return a list of all the ForeignId keys that are in that particular group. Like so:
| Fruit | ForeignKeys |
---------------------------
| Apple | 1, 2, 3 |
| Banana | 4 |
So far I have the SQL that I gets me the grouped Fruit values as that is trivial. But I cannot seem to find a good solution for retrieving the ForeignKeyId values that are contained within the group.
SELECT Fruits FROM FruitTable
I found the GROUP_CONCAT function in MySql which appears to do what I need but it doesn't seem to be available in SQL Server 2017.
Any help on this would be appreciated.
If you are using SQL Server 2014 or older:
SELECT Fruit = Fruits
,ForeignKeys = STUFF(
(SELECT ',' + CAST(ForeignId AS VARCHAR(100))
FROM FruitTable t2
WHERE t1.Fruits = t2.Fruits
FOR XML PATH ('')
)
,1,
1,
''
)
FROM FruitTable t1
GROUP BY Fruits;
If you are using SQL Server 2016 or later, you can also write this simpler way:
SELECT Fruit = Fruits, ForeignKeys = STRING_AGG(ForeignId, ',')
FROM FruitTable
GROUP BY Fruits;
SELECT fruits, foreignkeys = STUFF(
(SELECT ',' + foreignid
FROM fruittable t1
WHERE t1.id = t2.id
FOR XML PATH (''))
, 1, 1, '')
from fruittable
group by fruits
This should work.

Postgresql use more than one row as expression in sub query

As the title says, I need to create a query where I SELECT all items from one table and use those items as expressions in another query. Suppose I have the main table that looks like this:
main_table
-------------------------------------
id | name | location | //more columns
---|------|----------|---------------
1 | me | pluto | //
2 | them | mercury | //
3 | we | jupiter | //
And the sub query table looks like this:
some_table
---------------
id | item
---|-----------
1 | sub-col-1
2 | sub-col-2
3 | sub-col-3
where each item in some_table has a price which is in an amount_table like so:
amount_table
--------------
1 | 1000
2 | 2000
3 | 3000
So that the query returns results like this:
name | location | sub-col-1 | sub-col-2 | sub-col-3 |
----------------------------------------------------|
me | pluto | 1000 | | |
them | mercury | | 2000 | |
we | jupiter | | | 3000 |
My query currently looks like this
SELECT name, location, (SELECT item FROM some_table)
FROM main_table
INNER JOIN amount_table WHERE //match the id's
But I'm running into the error more than one row returned by a subquery used as an expression
How can I formulate this query to return the desired results?
you should decide on expected result.
to get one-tp-many relation:
SELECT name, location, some_table.item
FROM main_table
JOIN some_table on true -- or id if they match
INNER JOIN amount_table --WHERE match the id's
to get one-to-one with all rows:
SELECT name, location, (SELECT array_agg(item) FROM some_table)
FROM main_table
INNER JOIN amount_table --WHERE //match the id's

Postgresql Split single row to multiple rows

I'm new to postgresql. I'm getting below results from a query and now I need to split single row to obtain multiple rows.
I have gone through below links, but still couldn't manage it. Please help.
unpivot and PostgreSQL
How to split a row into multiple rows with a single query?
Current result
id,name,sub1code,sub1level,sub1hrs,sub2code,sub2level,sub2hrs,sub3code,sub3level,sub3hrs --continue till sub15
1,Silva,CHIN,L1,12,MATH,L2,20,AGRW,L2,35
2,Perera,MATH,L3,30,ENGL,L1,10,CHIN,L2,50
What we want
id,name,subcode,sublevel,subhrs
1,Silva,CHIN,L1,12
1,Silva,MATH,L2,20
1,Silva,AGRW,L2,35
2,Perera,MATH,L3,30
2,Perera,ENGL,L1,10
2,Perera,CHIN,L2,50
Use union:
select id, 1 as "#", name, sub1code, sub1level, sub1hrs
from a_table
union all
select id, 2 as "#", name, sub2code, sub2level, sub2hrs
from a_table
union all
select id, 3 as "#", name, sub3code, sub3level, sub3hrs
from a_table
order by 1, 2;
id | # | name | sub1code | sub1level | sub1hrs
----+---+--------+----------+-----------+---------
1 | 1 | Silva | CHIN | L1 | 12
1 | 2 | Silva | MATH | L2 | 20
1 | 3 | Silva | AGRW | L2 | 35
2 | 1 | Perera | MATH | L3 | 30
2 | 2 | Perera | ENGL | L1 | 10
2 | 3 | Perera | CHIN | L2 | 50
(6 rows)
The # column is not necessary if you want to get the result sorted by subcode or sublevel.
You should consider normalization of the model by splitting the data into two tables, e.g.:
create table students (
id int primary key,
name text);
create table hours (
id int primary key,
student_id int references students(id),
code text,
level text,
hrs int);

Postgresql : Filtering duplicate pair

I am asking this from mobile, so apologies for bad formatting. For the following table.
Table players
| ID | name |matches_won|
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
| 1 | bob | 3 |
| 2 | Paul | 2 |
| 3 | John | 4 |
| 4 | Jim | 1 |
| 5 | hal | 0 |
| 6 | fin | 0 |
I want to pair two players together in a query. Who have a similar or near similar the number of matches won. So the query should display the following result.
| ID | NAME | ID | NAME |
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
| 3 | John | 1 | bob |
| 2 | paul | 4 | Jim |
| 5 | hal | 6 | fin |
Until now I have tried this query. But it gives repeat pairs.
Select player1.ID,player1.name,player2.ID,player2.name
From player as player1,
player as player2
Where
player1.matches_won >= player2.matches_won
And player1.ID ! = player2.ID;
The query will pair the player with the most won matches with everyone of the other players. While I only want one player to appear only once in the result. With the player who is nearest to his wins.
I have tried sub queries. But I don't know how to go about it, since it only returns one result. Also aggregates don't work in the where clause. So I am not sure how to achieve this.
An easier way, IMHO, to achieve this would be to order the players by their number of wins, divide these ranks by two to create matches and self join. CTEs (with expressions) allow you to do this relatively elegantly:
WITH wins AS (
SELECT id, name, ROW_NUMNBER() OVER (ORDER BY matches_won DESC) AS rn
FROM players
)
SELECT w1.id, w1.name, w2.id, w2.name
FROM (SELECT id, name, rn / 2 AS rn
FROM wins
WHERE rn % 2 = 1) w1
LEFT JOIN (SELECT id, name, (rn - 1) / 2 AS rn
FROM wins
WHERE rn % 2 = 0) w2 ON w1.rn = w2.rn
Add row numbers in descending order by won matches to the table and join odd row numbers with adjacent even row numbers:
with players as (
select *, row_number() over (order by matches_won desc) rn
from player)
select a.id, a.name, b.id, b.name
from players a
join players b
on a.rn = b.rn- 1
where a.rn % 2 = 1
id | name | id | name
----+------+----+------
3 | John | 1 | bob
2 | Paul | 4 | Jim
5 | hal | 6 | fin
(3 rows)