Select common values when using group by [Postgres] - postgresql

I have three main tables meetings, persons, hobbies with two relational tables.
Table meetings
+---------------+
| id | subject |
+----+----------+
| 1 | Kickoff |
| 2 | Relaunch |
| 3 | Party |
+----+----------+
Table persons
+------------+
| id | name |
+----+-------+
| 1 | John |
| 2 | Anna |
| 3 | Linda |
+----+-------+
Table hobbies
+---------------+
| id | name |
+----+----------+
| 1 | Soccer |
| 2 | Tennis |
| 3 | Swimming |
+----+----------+
Relation Table meeting_person
+-----------------+-----------+
| id | meeting_id | person_id |
+----+------------+-----------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 1 |
| 5 | 2 | 2 |
| 6 | 3 | 1 |
+----+------------+-----------+
Relation Table person_hobby
+----------------+----------+
| id | person_id | hobby_id |
+----+-----------+----------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 1 | 3 |
| 4 | 2 | 1 |
| 5 | 2 | 2 |
| 6 | 3 | 1 |
+----+-----------+----------+
Now I want to to find the common hobbies of all person attending each meeting.
So the desired result would be:
+------------+-----------------+------------------------+
| meeting_id | persons | common_hobbies |
| | (Aggregated) | (Aggregated) |
+------------+-----------------+------------------------+
| 1 | John,Anna,Linda | Soccer |
| 2 | John,Anna | Soccer,Tennis |
| 3 | John | Soccer,Tennis,Swimming |
+------------+-----------------+------------------------+
My current work in progress is:
select
m.id as "meeting_id",
(
select string_agg(distinct p.name, ',')
from meeting_person mp
inner join persons p on mp.person_id = p.id
where m.id = mp.meeting_id
) as "persons",
string_agg(distinct h2.name , ',') as "common_hobbies"
from meetings m
inner join meeting_person mp2 on m.id = mp2.meeting_id
inner join persons p2 on mp2.person_id = p2.id
inner join person_hobby ph2 on p2.id = ph2.person_id
inner join hobbies h2 on ph2.hobby_id = h2.id
group by m.id
But this query lists not the common_hobbies but all hobbies which are at least once mentioned.
+------------+-----------------+------------------------+
| meeting_id | persons | common_hobbies |
+------------+-----------------+------------------------+
| 1 | John,Anna,Linda | Soccer,Tennis,Swimming |
| 2 | John,Anna | Soccer,Tennis,Swimming |
| 3 | John | Soccer,Tennis,Swimming |
+------------+-----------------+------------------------+
Does anyone have any hints for me, on how I could solve this problem?
Cheers

This problem can be solved by implement custom aggregation function (found it here):
create or replace function array_intersect(anyarray, anyarray)
returns anyarray language sql
as $$
select
case
when $1 is null then $2
when $2 is null then $1
else
array(
select unnest($1)
intersect
select unnest($2))
end;
$$;
create aggregate array_intersect_agg (anyarray)
(
sfunc = array_intersect,
stype = anyarray
);
So, the solution can be next:
select
meeting_id,
array_agg(ph.name) persons,
array_intersect_agg(hobby) common_hobbies
from meeting_person mp
join (
select p.id, p.name, array_agg(h.name) hobby
from person_hobby ph
join persons p on ph.person_id = p.id
join hobbies h on h.id = ph.hobby_id
group by p.id, p.name
) ph on ph.id = mp.person_id
group by meeting_id;
Look the example fiddle
Result:
meeting_id | persons | common_hobbies
-----------+-----------------------+--------------------------
1 | {John,Anna,Linda} | {Soccer}
3 | {John} | {Soccer,Tennis,Swimming}
2 | {John,Anna} | {Soccer,Tennis}

Related

Get dummy columns from different tables

I have three different tables that look like that:
Table 1
| id | city|
|----|-----|
| 1 | A |
| 1 | B |
| 2 | C |
Table 2
| id | city|
|----|-----|
| 2 | B |
| 1 | B |
| 3 | C |
Table 3
| id | city|
|----|-----|
| 1 | A |
| 1 | B |
| 2 | A |
I need to create one column for each table, and the dummies values if it's present.
| id | city| is_tbl_1 | is_tbl_2 | is_tbl_3 |
|----|-----|-----------|-------------|------------|
| 1 | A | 1 | 0 | 1 |
| 1 | B | 1 | 1 | 1 |
| 2 | A | 0 | 0 | 1 |
| 2 | C | 1 | 0 | 0 |
| 2 | B | 0 | 1 | 0 |
| 3 | C | 0 | 1 | 0 |
I have tried to add the columns is_tbl# myself on three different selects, UNION all the three tables and group, but it looks ugly, is there a better way to do it?
You can outer-join the 3 tables on id and city, then group by the id and city, and finally count the number of non-null values of the city columns :
SELECT
COALESCE (t1.id, t2.id, t3.id) AS id
, COALESCE (t1.city, t2.city, t3.city) AS city
, count(*) FILTER (WHERE t1.city IS NOT NULL) AS is_tbl_1
, count(*) FILTER (WHERE t2.city IS NOT NULL) AS is_tbl_2
, count(*) FILTER (WHERE t3.city IS NOT NULL) AS is_tbl_3
FROM
t1 AS t1
FULL OUTER JOIN
t2 AS t2 ON t1.id = t2.id AND t1.city = t2.city
FULL OUTER JOIN
t3 AS t3 ON t1.id = t3.id AND t1.city = t3.city
GROUP BY
1,2
ORDER BY
1,2

How do I join tables while putting the results in a json array?

Table name: people
+----+------+-------------+-------+
| id | name | city | state |
+----+------+-------------+-------+
| 1 | Joe | Los Angeles | CA |
+----+------+-------------+-------+
| 2 | Jill | Miami | FL |
+----+------+-------------+-------+
| 3 | Asa | Portland | OR |
+----+------+-------------+-------+
Table name: pets
+----+----------+------+
| id | pet_name | type |
+----+----------+------+
| 1 | Spike | dog |
+----+----------+------+
| 1 | Fluffy | cat |
+----+----------+------+
| 2 | Oscar | dog |
+----+----------+------+
How would I join the two tables above to include a column containing JSON of results matched in the 'pets' table (PostgreSQL)?
+----+------+------------------------------------------------------------+
| id | name | pets |
+----+------+------------------------------------------------------------+
| 1 | Joe | [{name:'Spike', type:'dog'}, {name: 'Fluffy', type:'cat'}] |
+----+------+------------------------------------------------------------+
| 2 | Jill | [{name:'Oscar', type:'dog'}] |
+----+------+------------------------------------------------------------+
| 3 | Asa | [] |
+----+------+------------------------------------------------------------+
Use json_agg() to aggregate over json-objects:
SELECT people.id
, name
, json_agg(
CASE WHEN pet_name IS NOT NULL THEN
json_build_object(
'name', pet_name
, 'type', type
)
END
)
FROM people
LEFT JOIN pets ON people.id = pets.id
GROUP BY
people.id
, name
ORDER BY
people.id;

PostgreSQL - Join three tables and add conditions?

I've got three tables in a PostgreSQL db that looks like this: https://imgur.com/a/bUapsYi
One user can belong to many projects, and one project can have many users, and I'm tying it together through a joined table called "userprojects".
Each table could look like this:
User
| id | firstname | lastname | email |
|----|-----------|----------|----------------|
| 1 | Joe | Green | joe#green.com |
| 2 | Olle | Svensson | olle#gmail.com |
| 3 | Erik | Yapp | erik#yapp.com |
Project
| id | name | owner |
|----|---------------|----------------|
| 1 | Project X | joe#green.com |
| 2 | Peanut Butter | olle#gmail.com |
| 3 | Apollo 11 | erik#yapp.com |
| 4 | RCPP | erik#yapp.com |
Userprojects
| id | user_id | project_id |
|----|---------|------------|
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 3 |
| 4 | 3 | 3 |
Is there some form of inner(?) join that let's me query on users in a project (eg user_id found in userprojects) OR if the user is an owner of a project?
With the example above, an inner join query that looks like this:
SELECT "project".id, "project".name, "email"
FROM userprojects
INNER JOIN project ON userprojects.project_id = project.id
INNER JOIN "user" ON userprojects.user_id = "user".id
would return this:
| id | name | email |
|----|---------------|----------------|
| 1 | Project X | joe#green.com |
| 2 | Peanut Butter | joe#green.com |
| 3 | Apollo 11 | olle#gmail.com |
| 4 | Apollo 11 | erik#yapp.com |
What I wish to add to the query result is also the owner of each project if they are not found in that inner join query - notice that erik#yapp.com is the owner of project RCPP but since that relation is not found in the userprojects table, it won't be returned in the query. Can I somehow also get my query to return those users, eg:
| id | name | email |
|-----|---------------|----------------|
| 1 | Project X | joe#green.com |
| 2 | Peanut Butter | joe#green.com |
| 3 | Apollo 11 | olle#gmail.com |
| 4 | Apollo 11 | erik#yapp.com |
| (?) | RCPP | erik#yapp.com |
Start the joins with the table Project and turn them to LEFT joins:
SELECT
"project".id, "project".name,
COALESCE("User"."email", "project"."owner") "owner"
FROM project
LEFT JOIN userprojects ON userprojects.project_id = project.id
LEFT JOIN "User" ON userprojects.user_id = "User".id
See the demo.
Results:
| id | name | owner |
| --- | ------------- | -------------- |
| 1 | Project X | joe#green.com |
| 2 | Peanut Butter | joe#green.com |
| 3 | Apollo 11 | olle#gmail.com |
| 3 | Apollo 11 | erik#yapp.com |
| 4 | RCPP | erik#yapp.com |
You can create a second INNER JOIN with the table user but this
time matching the email column of the user table to the owner of the project table.
SELECT p.id, P.name, email, CONCAT(u2.firstname, " ", u.lastname) as owner
FROM userprojects up
INNER JOIN project p ON(up.project_id = p.id)
INNER JOIN user u ON(up.user_id = u.id)
INNER JOIN user u2 ON(u2.email = p.owner)

Find rows in relation with at least n rows in a different table without joins

I have a table as such (tbl):
+----+------+-----+
| pk | attr | val |
+----+------+-----+
| 0 | ohif | 4 |
| 1 | foha | 56 |
| 2 | slns | 2 |
| 3 | faso | 11 |
+----+------+-----+
And another table in n-to-1 relationship with tbl (tbl2):
+----+-----+
| pk | rel |
+----+-----+
| 0 | 0 |
| 1 | 1 |
| 2 | 0 |
| 3 | 2 |
| 4 | 2 |
| 5 | 3 |
| 6 | 1 |
| 7 | 2 |
+----+-----+
(tbl2.rel -> tbl.pk.)
I would like to select only the rows from tbl which are in relationship with at least n rows from tbl2.
I.e., for n = 2, I want this table:
+----+------+-----+
| pk | attr | val |
+----+------+-----+
| 0 | ohif | 4 |
| 1 | foha | 56 |
| 2 | slns | 2 |
+----+------+-----+
This is the solution I came up with:
SELECT DISTINCT ON (tbl.pk) tbl.*
FROM (
SELECT tbl.pk
FROM tbl
RIGHT OUTER JOIN tbl2 ON tbl2.rel = tbl.pk
GROUP BY tbl.pk
HAVING COUNT(tbl2.*) >= 2 -- n
) AS tbl_candidates
LEFT OUTER JOIN tbl ON tbl_candidates.pk = tbl.pk
Can it be done without selecting the candidates with a subquery and re-joining the table with itself?
I'm on Postgres 10. A standard SQL solution would be better, but a Postgres solution is acceptable.
OK, just join once, as below:
select
t1.pk,
t1.attr,
t1.val
from
tbl t1
join
tbl2 t2 on t1.pk = t2.rel
group by
t1.pk,
t1.attr,
t1.val
having(count(1)>=2) order by t1.pk;
pk | attr | val
----+------+-----
0 | ohif | 4
1 | foha | 56
2 | slns | 2
(3 rows)
Or just join once and use CTE(with clause), as below:
with tmp as (
select rel from tbl2 group by rel having(count(1)>=2)
)
select b.* from tmp t join tbl b on t.rel = b.pk order by b.pk;
pk | attr | val
----+------+-----
0 | ohif | 4
1 | foha | 56
2 | slns | 2
(3 rows)
Is the SQL clearer?

calculate sum of sal based on two tables

Two tables like
table1
+----+-------+-----+
| id | sname | sal |
+----+-------+-----+
| 1 | X | 100 |
| 2 | Y | 200 |
| 3 | Z | 400 |
+----+-------+-----+
Table2
+----+-------+-----+
| id | sname | sal |
+----+-------+-----+
| 1 | A | 500 |
| 2 | B | 200 |
| 3 | C | 400
| 4 A 100
+----+-------+-----+
Both the tables having relation ship id column
i need calculate the sum sal group by table1.sname at the same time those who are matched to table2
the output like
+-------+-------+---------------------
| Table1.sname | Table2.sname | sum |
+-------+-------+-----+ ----------------
| A | W | 600 |
| B | Y | 200 |
| B | F | 300 |
| C | Z | 400 |
+-------+-------+----------------------
select sum(sal),a.sname,b.sname
from table1 a,
(select id,sname from table2 group by sname,id) as b
where a.id=b.id
group by a.sname,b.sname;
but its not given proper o/p
your question is little ambiguous...but maybe you want this.
query
select Table11.id, Table1.sname, Table2.sname, (Table1.sal+Table2.sal) as Sum
from Table1, Table2
where Table1.id = Table2.id;
result
Table1.id | Table2.sname | Table2.sname | sum
-----------+--------------+--------------+-----
1 | a | d | 500
2 | b | e | 700
3 | c | f | 900