How do i pivot PostgreSQL table? - sql

I have table company_representatives which looks like that:
Create table script:
CREATE TABLE IF NOT EXISTS company_representatives (
_id integer NOT NULL,
name varchar(50) NOT NULL,
surname varchar(100) NOT NULL,
date_of_join date NOT NULL,
role varchar(250) NOT NULL,
company_id integer NOT NULL,
CONSTRAINT PK_company_representatives PRIMARY KEY ( _id ),
CONSTRAINT FK_144 FOREIGN KEY ( company_id ) REFERENCES companies ( _id )
);
INSERT INTO company_representatives VALUES
(1,'random name','random surname', '2001-01-23', 'CEO', 1),
(2,'next random name','next random surname', '2001-01-23', 'Co-founder', 1),
(3,'John','Doe', '2003-02-12', 'HR', 1),
(4,'Bread','Pitt', '2001-01-23', 'Security officer', 1),
(5,'Toast','Malone', '1997-11-05', 'CEO', 2),
...
I need to pivot this table to make it's columns look like that:
company_id | CEO | Co-Founder | HR | Security Officer
1 1 2 3 4 "_id of company's representatives"
2 5 6 7 8
3 9 10 11 12

You can simply use FILTER directly in the SELECT clause:
SELECT DISTINCT ON (company_id)
company_id,
count(*) FILTER (WHERE role = 'CEO') AS CEO,
count(*) FILTER (WHERE role = 'Co-founder') AS "Co-Founder",
count(*) FILTER (WHERE role = 'HR') AS HR,
count(*) FILTER (WHERE role = 'Security officer') AS "Security Officer"
FROM company_representatives
GROUP BY company_id;
In question it is not clear what the values attached to the roles actually mean, so I assumed you just want to count them. If not, just change it to other aggregate function.
EDIT (see comments): pivot table using crosstab, assuming there is one record for each role in all companies:
SELECT *
FROM crosstab(
'SELECT company_id, _id, name
FROM company_representatives ORDER BY company_id,role'
) AS ct(company_id integer,ceo text,co_founder text,hr text,security_officer text);
Demo: db<>fiddle

Related

How to add N entries to json{b}_build_object where N is number of rows in table?

Given this table setup:
create table accounts (
id char(4) primary key,
first_name varchar not null
);
create table roles (
account_id char(4) references accounts not null,
role_type varchar not null,
role varchar not null,
primary key (account_id, role_type)
);
and initial account insertion:
insert into accounts (id, first_name) values ('abcd', 'Bob');
I want to get all the account info of someone, along w/ the roles they have as key-value pairs. Using a join for this one-to-many relationship would duplicate the account information across each row containing the role, so I want to create a JSON object instead. Using this query:
select
first_name,
coalesce(
(select jsonb_build_object(role_type, role) from roles where account_id = id),
'{}'::jsonb
) as roles
from accounts where id = 'abcd';
I get this expected result:
first_name | roles
------------+-------
Bob | {}
(1 row)
After adding a first role:
insert into roles (account_id, role_type, role) values ('abcd', 'my_role_type', 'my_role');
I get another expected result:
first_name | roles
------------+-----------------------------
Bob | {"my_role_type": "my_role"}
(1 row)
But after adding a second role:
insert into roles (account_id, role_type, role) values ('abcd', 'my_other_role_type', 'my_other_role');
I get:
ERROR: more than one row returned by a subquery used as an expression
How do I replace this error with
first_name | roles
------------+-----------------------------
Bob | {"my_role_type": "my_role", "my_other_role_type": "my_other_role"}
(1 row)
?
I'm on Postgres v13.
You may use json_object and array_agg with a group by to achieve this outcome. See example with working fiddle below:
Query #1
select
a.first_name,
json_object(
array_agg(role_type),
array_agg(role)
)
from accounts a
inner join roles r on r.account_id = a.id
where a.id = 'abcd'
group by a.first_name;
first_name
json_object
Bob
{"my_role_type":"my_role","my_other_role_type":"my_other_role"}
View on DB Fiddle
Edit 1:
The following modification using a left join and case expression to provide an alternative for results containing null values:
select
a.first_name,
CASE
WHEN COUNT(role_type)=0 THEN '{}'::json
ELSE
json_object(
array_agg(role_type),
array_agg(role)
)
END as role
from accounts a
left join roles r on r.account_id = a.id
group by a.first_name;
View on DB Fiddle
Let me know if this works for you.

How to insert a column which sets unique id based on values in another column (SQL)?

I will create table where I will insert multiple values for different companies. Basically I have all values that are in the table below but I want to add a column IndicatorID which is linked to IndicatorName so that every indicator has a unique id. This will obviously not be a PrimaryKey.
I will insert the data with multiple selects:
CREATE TABLE abc
INSERT INTO abc
SELECT company_id, 'roe', roevalue, metricdate
FROM TABLE1
INSERT INTO abc
SELECT company_id, 'd/e', devalue, metricdate
FROM TABLE1
So, I don't know how to add the IndicatorID I mentioned above.
EDIT:
Here is how I populate my new table:
INSERT INTO table(IndicatorID, Indicator, Company, Value, Date)
SELECT [the ID that I need], 'NI_3y' as 'Indicator', t.Company, avg(t.ni) over (partition by t.Company order by t.reportdate rows between 2 preceding and current row) as 'ni_3y',
t.reportdate
FROM table t
LEFT JOIN IndicatorIDs i
ON i.Indicator = roe3 -- the part that is not working if I have separate indicatorID table
I am going to insert different indicators for the same companies. And I want indicatorID.
Your "indicator" is a proper entity in its own right. Create a table with all indicators:
create table indicators (
indicator_id int identity(1, 1) primary key,
indicator varchar(255)
);
Then, use the id only in this table. You can look up the value in the reference table.
Your inserts are then a little more complicated:
INSERT INTO indicators (indicator)
SELECT DISTINCT roevalue
FROM table1 t1
WHERE NOT EXISTS (SELECT 1 FROM indicators i2 WHERE i2.indicator = t1.roevalue);
Then:
INSERT INTO ABC (indicatorId, companyid, value, date)
SELECT i.indicatorId, t1.company, v.value, t1.metricdate
FROM table1 t1 CROSS APPLY
(VALUES ('roe', t1.roevalue), ('d/e', t1.devalue)
) v(indicator, value) JOIN
indicators i
ON i.indicator = v.indicator;
This process is called normalization and it is the typical way to store data in a database.
DDL and INSERT statement to create an indicators table with a unique constraint on indicator. Because the ind_id is intended to be a foreign key in the abc table it's created as a non-decomposable surrogate integer primary key using the IDENTITY property.
drop table if exists test_indicators;
go
create table test_indicators (
ind_id int identity(1, 1) primary key not null,
indicator varchar(20) unique not null);
go
insert into test_indicators(indicator) values
('NI'),
('ROE'),
('D/E');
The abc table depends on the ind_id column from indicators table as a foreign key reference. To populate the abc table company_id's are associated with ind_id's.
drop table if exists test_abc
go
create table test_abc(
a_id int identity(1, 1) primary key not null,
ind_id int not null references test_indicators(ind_id),
company_id int not null,
val varchar(20) null);
go
insert into test_abc(ind_id, company_id)
select ind_id, 102 from test_indicators where indicator='NI'
union all
select ind_id, 103 from test_indicators where indicator='ROE'
union all
select ind_id, 104 from test_indicators where indicator='D/E'
union all
select ind_id, 103 from test_indicators where indicator='NI'
union all
select ind_id, 105 from test_indicators where indicator='ROE'
union all
select ind_id, 102 from test_indicators where indicator='NI';
Query to get result
select i.ind_id, a.company_id, i.indicator, a.val
from test_abc a
join test_indicators i on a.ind_id=i.ind_id;
Output
ind_id company_id indicator val
1 102 NI NULL
2 103 ROE NULL
3 104 D/E NULL
1 103 NI NULL
2 105 ROE NULL
1 102 NI NULL
I was finally able to find the solution for my problem which seems to me very simple, although it took time and asking different people about it.
First I create my indicators table where I assign primary key for all indicators I have:
CREATE TABLE indicators (
indicator_id int identity(1, 1) primary key,
indicator varchar(255)
);
Then I populate easy without using any JOINs or CROSS APPLY. I don't know if this is optimal but it seems as the simplest choice:
INSERT INTO table(IndicatorID, Indicator, Company, Value, Date)
SELECT
(SELECT indicator_id from indicators i where i.indicator = 'NI_3y) as IndicatorID,
'NI_3y' as 'Indicator',
Company,
avg(ni) over (partition by Company order by reportdate rows between 2 preceding and current row) as ni_3y,
reportdate
FROM TABLE1

Accessing to total number in each second level(Postgres Hierarchical Query Practice)

I was practicing on Postgres and stuck on a point that I couldn't find a way to achieve. I have a simple database which are the attributes:
CREATE TABLE public.department
(
"deptId" integer NOT NULL PRIMARY KEY,
name character varying(30) COLLATE pg_catalog."default" NOT NULL,
"parentId" integer,
"numEmpl" integer NOT NULL,
CONSTRAINT "department_parentId_fkey" FOREIGN KEY ("parentId")
REFERENCES public.department ("deptId") MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
)
and then I have some data in the table. Short example is
insert into department values (1, 'Headquarter', 1, 10);
insert into department values (2, 'Sales', 1, 15);
insert into department values (3, 'Logistics', 1, 25);
...
I was trying to get the total number of people who are employeed in each second level department.
I am able to get the total number of employeed people in each department but according to my search in the internet this is possible with "Hierarchical Queries". Currently, I am using
parentId=1
while querying.
Any solutions for this? Thank you.
Here is one option:
with recursive cte as (
select deptid as rootid, deptid from department where parentid = 1 and deptid <> 1
union all
select c.rootid, d.deptid
from cte c
inner join department d on d.parentid = c.deptid and d.deptid <> 1
)
select rootid, count(*) cnt from cte group by rootid

PostgreSQL query not returning result as intended

I would like to generate a list of all days where every sailor booked a boat in that particular day.
The table scheme is as follows:
CREATE TABLE SAILOR(
SID INTEGER NOT NULL,
NAME VARCHAR(50) NOT NULL,
RATING INTEGER NOT NULL,
AGE FLOAT NOT NULL,
PRIMARY KEY(SID)
);
CREATE TABLE BOAT(
BID INTEGER NOT NULL,
NAME VARCHAR(50) NOT NULL,
COLOR VARCHAR(50) NOT NULL,
PRIMARY KEY(BID)
);
CREATE TABLE RESERVE (
SID INTEGER NOT NULL REFERENCES SAILOR(SID),
BID INTEGER NOT NULL REFERENCES BOAT(BID),
DAY DATE NOT NULL,
PRIMARY KEY(SID, BID, DAY));
The data is as follows:
INSERT INTO SAILOR(SID, NAME, RATING, AGE)
VALUES
(64, 'Horatio', 7, 35.0),
(74, 'Horatio', 9, 35.0);
INSERT INTO BOAT(BID, NAME, COLOR)
VALUES
(101, 'Interlake', 'blue'),
(102, 'Interlake', 'red'),
(103, 'Clipper', 'green'),
(104, 'Marine', 'red');
INSERT INTO RESERVE(SID, BID, DAY)
VALUES+
(64, 101, '09/05/98'),
(64, 102, '09/08/98'),
(74, 103, '09/08/98');
I have tried using this code:
SELECT DAY
FROM RESERVE R
WHERE NOT EXISTS (
SELECT SID
FROM SAILOR S
EXCEPT
SELECT S.SID
FROM SAILOR S, RESERVE R
WHERE S.SID = R.SID)
GROUP BY DAY;
but it returns a list of all days, no exception. The only day that it should return is "09/08/98". How do I solve this?
I would phrase your query as:
SELECT r.DAY
FROM RESERVE r
GROUP BY r.DAY
HAVING COUNT(DISTINCT r.SID) = (SELECT COUNT(*) FROM SAILOR);
Demo
The above query says to return any day in the RESERVE table whose distinct SID sailor count matches the count of every sailor.
This assumes that SID sailor entries in the RESERVE table would only be made with sailors that actually appear in the SAILOR table. This seems reasonable, and can be enforced using primary/foreign key relationships between the two tables.
Taking a slightly different approach of just counting unique sailors per day:
SELECT day FROM (
SELECT COUNT(DISTINCT sid), day FROM reserve GROUP BY day
) AS sailors_per_day
WHERE count = (SELECT COUNT(*) FROM sailor);
+------------+
| day |
|------------|
| 1998-09-08 |
+------------+

How to see table according to another table's ID?

See table according to another tables ID.
Example: see what countries from country table an id = 5 from users table has visited.
I think it needs a subquery for this task.
Select * from country where (select id from users where id = 5)
It doesn't work correctly.
Edit
user table
CREATE TABLE `user` (
`id` int(6) NOT NULL,
`name` varchar(30) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `user` (`id`, `name`) VALUES
('1', 'jackie'),
('2', 'maria'),
('3', 'sandra')
country table
CREATE TABLE `country` (
`name` varchar(30) NOT NULL,
`capital` varchar(30) NOT NULL,
) ;
INSERT INTO `country` (`name`, `capital`) VALUES
('Italy', 'Rome'),
('Portugal', 'Lisbon'),
('China', 'Beijing'),
('Norway', 'Oslo');
Purpose: users can mark the countries visited so the program should have the ability to show which countries a user has visited.
Hence we want a query that shows the info for each country a user with id = 5 has visited.
Example user = 5 has visited Italy and Norway.
UPDATE NEW
to add countries a user has visited:
CREATE TABLE `country_visited`(
`userId` INT(6) not null,
`country` varchar(30) not null
);
INSERT INTO `country_visited` (`userid`,`country`) VALUES
('1', 'Spain'),
('2', 'Norway'),
('3', 'Italy'),
('4', 'Spain'),
('5', 'Italy'); #if this has visited 3 countries how do you add it?
You first need a table to store which countries a user had visited. For example
CREATE TABLE countryVisited(
userId INT not null
country varchar(30) not null
)
Insert into the table:
INSERT INTO countryVisited (userId,country) VALUES
(5, 'Norway'),
(4, 'Spain'),
(5, 'Italy')
You can then join as follows:
SELECT id, name, c.country, capital
FROM users u
INNER JOIN countryVisited cv
ON u.id = cv. userId
INNER JOIN country c
ON cv.country = c.country
WHERE u.id=5
Even better, add an id field to the country table and only store that value in the countryVisited table instead of the country name.
You should have another table which stores the relation between User and Country. Something like this:
UserCountries Table
UserID CountryId
------- -------------
1 1
1 2
2 1
2 3
Then you should write a query like this:
Select * from Country where Id in
(select CountryId from UserCountries
where UserId = 5)
After edit your question I can understand the matter. Now you can use the structure for solved your problem.
Edit your user table as like as user_id, user_name, visited_country_id.
when a user choose his visited country then update user table visited_country_id using comma separator. For an Example
user_id user_name visited_country_id
5 Selim 1,3,4
Now you execute query like select* from country where country_id in (select visited_country_id from user where user_id=5)