how to unnest multiple arrays and transpose table in postgres 9.5 - postgresql

Lets suppose i have this table:
project |leader|coordination|staff |support |
---------|------|------------|-------------------|-----------|
project_a|Bob |{Alice} |{Terry,Mandy,James}|{Bob,Peter}|
project_b|Alice |{Terry, Bob}|{Mandy,James} |{Maggie} |
project_c|Maggie|{Bob} |{Terry,Peter} |{Alice,Bob}|
and i want to transform it to something like this:
person|project_1|role_1|project_2|role_2 |project_3|role_3 |project_4|role_4 |
------|---------|------|---------|------------|---------|-------|---------|------------|
Alice |project_b|leader|project_a|coordination|project_c|support| | |
Bob |project_a|leader|project_a|support |project_b|staff |project_c|coordination|
Terry...
Maggie...
...
Big thanks to #abelisto for pointing me in the right direction in the comments.
The problem with crosstab is, that i need the project also in the row:
SELECT *
FROM crosstab(
'
select
person,
project,
role
from
tmp_projects
cross join lateral (
select
\'leader\'::text as role, leader as person
union all
select
\'coordination\', x
from
unnest(coordination) as x
union all
select
\'staff\', x
from
unnest(staff) as x
union all
select
\'support\', x
from
unnest(support) as x) as t order by 1,2' -- needs to be "ORDER BY 1,2" here
, 'SELECT DISTINCT project FROM tmp_projects ORDER BY 1'
) AS ct ("Person" text, "Project_1" text,"Project_2" text,"Project_3" text);

#Abelisto had the correct idea and a solution could be:
create view tmp_view_projects_unnest as select
person,
row_number() OVER (PARTITION by person) AS rn,
project,
role
from
tmp_projects
cross join lateral (
select
'leader'::text as role, leader as person
union all
select
'coordination', x
from
unnest(coordination) as x
union all
select
'staff', x
from
unnest(staff) as x
union all
select
'support', x
from
unnest(support) as x) as t order by 1,2
person|rn|project |role |
------|--|---------|------------|
Alice | 1|project_a|leader |
Alice | 2|project_d|coordination|
Alice | 3|project_c|support |
Bob | 1|project_d|coordination|
Bob | 2|project_c|coordination|
Bob | 3|project_c|support |
Maggie| 1|project_c|leader |
Mandy | 1|project_d|leader |
Marry | 1|project_d|staff |
Peter | 1|project_d|support |
Peter | 2|project_c|staff |
Peter | 3|project_a|coordination|
Terry | 1|project_a|coordination|
Terry | 2|project_c|staff |
select tvp.person,
tvp.project as project_1, tvp.role as role_1 ,
tvp2.project as project_2, tvp2.role as role_2,
tvp3.project as project_3, tvp3.role as role_3,
tvp4.project as project_4, tvp4.role as role_4
from tmp_view_projects_unnest tvp
left join tmp_view_projects_unnest tvp2 on tvp2.person = tvp.person and tvp2.rn =2
left join tmp_view_projects_unnest tvp3 on tvp3.person = tvp.person and tvp3.rn =3
left join tmp_view_projects_unnest tvp4 on tvp4.person = tvp.person and tvp4.rn =4
where tvp.rn=1;
person|project_1|role_1 |project_2|role_2 |project_3|role_3 |project_4|role_4|
------|---------|------------|---------|------------|---------|------------|---------|------|
Alice |project_a|leader |project_d|coordination|project_c|support | | |
Bob |project_d|coordination|project_c|coordination|project_c|support | | |
Maggie|project_c|leader | | | | | | |
Mandy |project_d|leader | | | | | | |
Marry |project_d|staff | | | | | | |
Peter |project_d|support |project_c|staff |project_a|coordination| | |
Terry |project_a|coordination|project_c|staff | | | | |

Related

Postgresql - How to use string_to_array on another column value

How can I use string_to_array or split_part on another column value.
I want do something like select * from tenants where id IN (select string_to_array(select ancestry from tenants where id = 39,'/'));
-[ RECORD 1 ]-------------+----------------------
id | 1
domain |
subdomain |
name | My Company
login_text |
logo_file_name |
logo_content_type |
logo_file_size |
logo_updated_at |
login_logo_file_name |
login_logo_content_type |
login_logo_file_size |
login_logo_updated_at |
ancestry |
divisible | t
description | Tenant for My Company
use_config_for_attributes | t
default_miq_group_id | 1
source_type |
source_id |
-[ RECORD 3 ]-------------+----------------------
id | 35
domain |
subdomain |
name | Tenant_2
login_text |
logo_file_name |
logo_content_type |
logo_file_size |
logo_updated_at |
login_logo_file_name |
login_logo_content_type |
login_logo_file_size |
login_logo_updated_at |
ancestry | 1
divisible | t
description | Tenant_2
use_config_for_attributes | f
default_miq_group_id | 36
source_type |
source_id |
-[ RECORD 7 ]-------------+----------------------
id | 39
domain |
subdomain |
name | Child_Teanant_202
login_text |
logo_file_name |
logo_content_type |
logo_file_size |
logo_updated_at |
login_logo_file_name |
login_logo_content_type |
login_logo_file_size |
login_logo_updated_at |
ancestry | 1/35
divisible | t
description | Child_Teanant_202
use_config_for_attributes | f
default_miq_group_id | 52
source_type |
source_id |
Use regex to enforce word boundaries:
select *
from tenants
where (select ancestry from tenants where id = 39)
~ ('\y' || id || '\y')
See live demo.
Without the word boundaries an id of 1 would match an ancestry of 123.
Note Postgres's unusual regex for word boundary \y, which elsewhere is \b.
There are two ways to solve this.
One is to simply unnest the elements of ancestry
select *
from tenants
where id in (select a.id::int
from tenants t2
cross join unnest(string_to_array(t2.ancestry, '/')) as a(id)
where t2.id = 39);
Converting the string to an array in order to be able to use the = ANY() operator is a bit tricky, because you need two levels of parentheses plus a type cast to an integer array to make that work:
select *
from tenants
where id = any ((select string_to_array(t2.ancestry, '/')
from tenants t2
where t2.id = 39)::int[]);
Online example

Make sure every distinct value of Column1 has a row with every distinct value of Column2, by populating a table with 0s - postgresql

Here's a crude example I've made up to illustrate what I want to achieve:
table1:
| Shop | Product | QuantityInStock |
| a | Prod1 | 13 |
| a | Prod3 | 13 |
| b | Prod2 | 13 |
| b | Prod3 | 13 |
| b | Prod4 | 13 |
table1 becomes:
| Shop | Product | QuantityInStock |
| a | Prod1 | 13 |
| a | Prod2 | 0 | -- new
| a | Prod3 | 13 |
| a | Prod4 | 0 | -- new
| b | Prod1 | 0 | -- new
| b | Prod2 | 13 |
| b | Prod3 | 13 |
| b | Prod4 | 13 |
In this example, I want to represent every Shop/Product combination
every Shop {a,b} to have a row with every Product {Prod1, Prod2, Prod3, Prod4}
QuantityInStock=13 has no significance, I just wanted a placeholder number :)
Use a calendar table cross join approach:
SELECT s.Shop, p.Product, COALESCE(t1.QuantityInStock, 0) AS QuantityInStock
FROM (SELECT DISTINCT Shop FROM table1) s
CROSS JOIN (SELECT DISTINCT Product FROM table1) p
LEFT JOIN table1 t1
ON t1.Shop = s.Shop AND
t1.Product = p.Product
ORDER BY
s.Shop,
p.Product;
The idea here is to generate an intermediate table containing of all shop/product combinations via a cross join. Then, we left join this to table1. Any shop/product combinations which do not have a match in the actual table are assigned a zero stock quantity.

Select common values when using group by [Postgres]

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}

How do I make a conditional join?

I have the following query
SELECT p.first_name || ' ' || p.last_name as full_name, p.user_id,
u.email, u.username,
(
SELECT EXTRACT (year from (age(now(), p.date_of_birth)))
)age,
co.name as country, s.name as state, c.name as city,
(
SELECT country.phonecode FROM countries country WHERE ph.country_id = country.id
)phone_code, ph.area_code, ph.number as phone_number,
(
SELECT i.path FROM images i WHERE i.id = p.image_id
)avatar
FROM people p
JOIN users u ON u.id = p.user_id
JOIN addresses a ON a.id = p.address_id
JOIN cities c ON c.id = a.city_id
JOIN states s ON s.id = c.state_id
JOIN countries co ON co.id = s.country_id
JOIN phones ph ON ph.person_id = p.id
WHERE (
SELECT u.status FROM users u WHERE u.id = p.user_id
) = 'U'
ORDER BY p.id DESC
However, some people don't necessarily have information in Address and Phone and I want to bring that information anyway, how could I do it?
Little example:
Users
+----+-----------+-----------+---------------+-------------------------+
| id | username | role | status | email |
+----+-----------+-----------+---------------+-------------------------+
| 7 | Shibari | P | U | deathwrap#live.ca |
| 12 | Josh23 | P | U | heeman#gmail.ca |
| 13 | Test22 | P | U | test#gmail.ca |
+----+-----------+-----------+---------------+-------------------------+
People
+----+----------+-----------+-------------+-------+------------+----------+
| id |first_name| last_name |date_of_birth|user_id| address_id | image_id |
+----+----------+-----------+-------------+-------+------------+----------+
| 7 | Edward | Test | 1983-10-13 | 7 | 11 | [null] |
| 12 | Josh | Test | 1996-04-11 | 12 | [null] | 7 |
| 13 | Gavin | Cringe | 1991-04-11 | 13 | 18 | 8 |
+----+----------+-----------+-------------+-------+-----------------------+
Addresses
+----+---------------------+-----------+---------------+
| id | name | zip_code | city_id |
+----+---------------------+-----------+---------------+
| 11 | Rue De La Fonderie |[null] | 26497 |
| 18 | Av. Test |[null] | 6486 |
+----+---------------------+-----------+---------------+
Cities
+-----+---------------------+-----------+
| id | name | state_id |
+-----+---------------------+-----------+
|6486 | Resistencia |210 |
|26497| Luxembourg |2237 |
+-----+---------------------+-----------+
States
+-----+---------------------+-----------+
| id | name |country_id |
+-----+---------------------+-----------+
|210 | Chaco |10 |
|2237 | Luxembourg |127 |
+-----+---------------------+-----------+
Countries
+-----+---------------------+-----------+
| id | name | phonecode |
+-----+---------------------+-----------+
|10 | Argentina |54 |
|127 | Luxembourg |352 |
+-----+---------------------+-----------+
Phones
+----+-----------+-----------+---------------+-------------+
| id | area_code | number | person_id | country_id |
+----+-----------+-----------+---------------+-------------+
| 6 | 661 | 12345 | 7 | 127 |
+----+-----------+-----------+---------------+-------------+
Images
+-----+----------------+
| id | path |
+-----+----------------+
|7 | 7.jpg |
|8 | 8.jpg |
+-----+----------------+
Here's the output I desire
+------------+-------+-----------------+--------+---+----------+----------+-----------+----------+
| full_name |user_id| email |username|age|country |state |city |phone_code|
+------------+-------+-----------------+--------+---+----------+----------+-----------+----------+
|Edward Test |7 |deathwrap#live.ca|Shibari |37 |Luxembourg|Luxembourg|Luxembourg |352 |
|Josh Test |12 |heeman#gmail.ca |Josh23 |24 |[null] |[null] |[null] |[null] |
|Gavin Cringe|13 |test#gmail.ca |Test22 |29 |Argentina |Chaco |Resistencia|[null] |
+------------+-------+-----------------+--------+---+----------+----------+-----------+----------+
+------------+------------+-----------------+
| area_code |phone_number| avatar |
+------------+------------+-----------------+
|661 |12345 | [null] |
|[null] |[null] | 7.jpg |
|[null] |[null] | 8.jpg |
+------------+------------+-----------------+
At the moment it's just printing the first entry because it has both an address and a phone number linked, but I want to print this info even when there's no phone number or address linked, how could I do this?
Use left joins instead of inner joins:
SELECT ...
FROM people p
LEFT JOIN users u ON u.id = p.user_id AND u.status = 'U'
LEFT JOIN addresses a ON a.id = p.address_id
LEFT JOIN cities c ON c.id = a.city_id
LEFT JOIN states s ON s.id = c.state_id
LEFT JOIN countries co ON co.id = s.country_id
LEFT JOIN phones ph ON ph.person_id = p.id
Using the above left join approach should ensure that every person record appear in the output even if it doesn't match to one or more of the otber tables. Note that I have moved the check on status from the WHERE clause to the ON clause of the left join with the users table.

Select all columns from two tables

Lets say I have the following:
table_a
| id | date | order_id | sku | price |
--------------------------------------------
| 10 | 2016-08-18 | 111 | ABC | 10 |
table_b
| id | date | order_id | description | type | notes | valid |
-------------------------------------------------------------------
| 50 | 2016-08-18 | 111 | test | AA | | true |
I want to get get all columns from both tables, so the resulting table looks like this:
| id | date | order_id | sku | price | description | type | notes | valid |
---------------------------------------------------------------------------------
| 10 | 2016-08-18 | 111 | ABC | 10 | | | | |
---------------------------------------------------------------------------------
| 50 | 2016-08-18 | 111 | | | test | AA | | true |
I tried union:
(
SELECT *
from table_a
where table_a.date > Date('today')
)
UNION
(
SELECT *
from table_b
where table_b.date > Date('today')
)
But I get a:
ERROR: each UNION query must have the same number of columns
How can this be fixed / is there another way to do this?
Easily :)
(
SELECT id, date, order_id, sku, price, NULL AS description, NULL AS type, NULL AS notes, NULL AS valid
from table_a
where table_a.date > Date('today')
)
UNION
(
SELECT id, date, order_id, NULL AS sku, NULL AS price, description, type, notes, valid
from table_b
where table_b.date > Date('today')
)
Alternatively, instead of UNION you can just JOIN them:
SELECT *
FROM table_a A
JOIN table_b B USING ( id )
WHERE A.date > TIMESTAMP 'TODAY'
AND B.date > TIMESTAMP 'TODAY';
See more options: https://www.postgresql.org/docs/9.5/static/queries-table-expressions.html#QUERIES-JOIN