PostgreSQL - Select All Results From Many-To-Many - postgresql

I have a simple set of tables used for a many-to-many relationship - a book can have many authors, an author can have many books:
CREATE TABLE book (
book_id serial PRIMARY KEY, -- implicit primary key constraint
book_name text NOT NULL
);
CREATE TABLE author (
author_id serial PRIMARY KEY, -- implicit primary key constraint
author_name text NOT NULL
);
CREATE TABLE book_author (
book_author_id serial PRIMARY KEY,
book_id int,
author_id numeric NOT NULL
);
I can query for a particular book to get all the authors:
SELECT
author_name
from
author a
where
a.book_id = 23
But there could be any number of rows returned, because there could be any number of authors for the book.
What I want is a way to format the result (no matter how many rows) into a nicely formatted string (like "Robert Heinlein, Arthur C. Clarke, Ray Bradbury").
Is this possible in postgreSQL?

Related

How to update table on postgres with join statement

I have three tables on postgresql DB, and tried to update table but I failed to get result what I want. Please help me with getting valid result.
The first table is "employee".
On this table, the first three characters of "employee_id" mean employee type.
For example, employee_id="AA1-11111" is a member of employee_type="AA1".
employee_id
department
AA1-11111
A
AA1-22222
B
AB1-11111
A
The second table is "assessment".
On this table, assessment criteria is defined for (employee_type, department).
For example, an employee of employee_type="AA1" and department="A" will be evaluated by assessment_criteria="XX1X".
employee_type
department
assessment_criteria
AA1
A
XX1X
AA1
B
XX1Y
AA2
A
XX2X
The third table is "employee_assessment". On this table assessment_criteria for each employee is defined. (This table is calculated from "employee" and "assessment" by night batch processing.)
employee_id
department
assessment_criteria
AA1-11111
A
XX1X
AA1-22222
B
XX1Y
AB1-11111
A
Null
What I want to do is... to update "employee_assessment" table when "assessment" table is updated.
When "assessment" table is updated as like below...
employee_type
department
assessment_criteria
AA1
A
XX1X
AA1
B
NEW
AA2
A
Null
I want to update "employee_assessment" table like this.
employee_id
department
assessment_criteria
AA1-11111
A
XX1X
AA1-22222
B
NEW
AB1-11111
A
Null
I tried
UPDATE
employee_assessment
SET assessment_criteria=employee_assessment.assessment_criteria
FROM employee
LEFT JOIN (SELECT employee_id, LEFT(employee_id,3) as emp_type, department as emp_department from employee) as t1
ON
employee.employee_id=t1.employee_id
and
employee.department=t1.emp_department
left join assessment
on
t1.emp_type=assessment.employee_type
and
t1.emp_department=assessment.department;
But I got this result.
employee_id
department
assessment_criteria
AA1-11111
A
XX1X
AA1-22222
B
XX1X
AB1-11111
A
XX1X
My query seems to be wrong.
The actual cause of the problem is that you schema isn't properly normalized. Therefore you should solve this by fixing and normalizing your schema. Then you can simply use a view, that is "updated" automatically.
First have tables for the types and departments (unless you have that already (that's unclear)).
CREATE TABLE type
(id serial,
name varchar(64),
PRIMARY KEY (id));
CREATE TABLE department
(id serial,
name varchar(64),
PRIMARY KEY (id));
Then, in the table for the employees, just reference the types and departments. Don't have a column that actually are two columns, i.e. the type id has to have its own column and must not be concatenated to any other.
CREATE TABLE employee
(id serial,
type integer,
department integer,
given_name varchar(64),
surname varchar(64),
PRIMARY KEY (id),
FOREIGN KEY (type)
REFERENCES type
(id),
FOREIGN KEY (department)
REFERENCES department
(id));
In the table for the assessments reference the types and departments too.
CREATE TABLE assessment
(id serial,
type integer,
department integer,
name varchar(64),
criteria varchar(64),
PRIMARY KEY (id),
FOREIGN KEY (type)
REFERENCES type
(id),
FOREIGN KEY (department)
REFERENCES department
(id));
Now you can create view for the employee assessments that joins the data from the other tables and is always up to date. There's no need for any manual UPDATE.
CREATE VIEW employee_assessment
AS
SELECT e.id employee_id,
e.department employee_department,
a.criteria assessment_criteria
FROM employee e
LEFT JOIN assessment a
ON a.type = e.type
AND a.department = e.department;
A view also has the advantage that it cannot contain inconsistent data as the table you have now could.

Insert data into strongly normalized DB and maintain the integrity (Postgres)

I'm trying to develop a simple database for the phonebook. This is what I wrote:
CREATE TABLE phone
(
phone_id SERIAL PRIMARY KEY,
phone CHAR(15),
sub_id INT, -- subscriber id --
cat_id INT -- category id --
);
CREATE TABLE category
(
cat_id SERIAL PRIMARY KEY, -- category id --
cat_name CHAR(15) -- category name --
);
CREATE TABLE subscriber
(
sub_id SERIAL PRIMARY KEY,
name CHAR(20),
fname CHAR(20), -- first name --
lname CHAR(20), -- last name --
);
CREATE TABLE address
(
addr_id SERIAL PRIMARY KEY,
country CHAR(20),
city CHAR(20),
street CHAR(20),
house_num INT,
apartment_num INT
);
-- many-to-many relation --
CREATE TABLE sub_link
(
sub_id INT REFERENCES subscriber(sub_id),
addr_id INT
);
I created a link table for many-to-many relation because few people can live at the same address and one person can live in different locations at different times.
But I cannot figure out how to add data in strongly normalized DB like this and maintain the integrity of the data.
The first improvement was that I added inique key on address table bacause this table should not contain duplicated data:
CREATE TABLE address
(
addr_id SERIAL PRIMARY KEY,
country CHAR(20),
city CHAR(20),
street CHAR(20),
house_num INT,
apartment_num INT,
UNIQUE (country, city, street, house_num, apartment_num)
);
Now the problem is how to add a new record about some person into DB. I think I should use the next order of actions:
Insert a record into subscriber table, because sub_link and phone tables must use id of a new subscriber.
Insert a record into address table because addr_id must exist before adding record into sub_link.
Link last records from subscriber and address in sub_link table. But at this step I have a new problem: how can I get sub_id and addr_id from steps 1) and 2) in PostgreSQL effectively?
Then I need to insert a record into the phone table. As at 3) step I dont know how to get sub_id from previous queries effectively.
I read about WITH block in the Postgres but I cannot figure out how to use it in my case.
UPDATE
I've done like ASL suggested:
-- First record --
WITH t0 AS (
WITH t1 AS (
INSERT INTO subscriber
VALUES(DEFAULT, 'Twilight Sparkle', NULL, NULL)
RETURNING sub_id
),
t2 AS (
INSERT INTO address
VALUES(DEFAULT, 'Equestria', 'Ponyville', NULL, NULL, NULL)
RETURNING addr_id
)
INSERT INTO sub_link
VALUES((SELECT sub_id FROM t1), (SELECT addr_id FROM t2))
)
INSERT INTO phone
VALUES (DEFAULT, '000000', (SELECT sub_id FROM t1), 1);
But I have an error: WITH clause containing a data-modifying statement must be at the top level
LINE 2: WITH t1 AS (INSERT INTO subscriber VALUES(DEFAULT,
You can do it all in one query using a WITH block with a RETURNING clause. See PostgreSQL docs on INSERT. For example:
WITH t1 AS (INSERT INTO subscriber VALUES ... RETURNING sub_id),
t2 AS (INSERT INTO address VALUES ... RETURNING addr_id)
INSERT INTO sub_link VALUES ((SELECT sub_id FROM t1), (SELECT addr_id FROM t2))
Note that this simple form will only work when inserting a single row into each table.
This is somewhat off the topic of your question, but I suggest you also consider making sub_id and cat_id columns in the phone table foreign keys (use REFERENCES).
You got the idea. Insert data from topmost tables so that you have their IDs before inserting references to them.
In PostgreSQL you can use INSERT/UPDATE ... RETURNING id construct. If you are not using some ORM which do it automatically, this may be useful.
The only thing here is that in step 2 you probably want to check if the address already exists before inserting:
SELECT addr_id FROM address WHERE country = ? AND city = ? ...

How to query a JSONB array?

I have a table
Visitor: (id, .., custom::jsonb[])
custom is an array of JSON objects of the form {field:string, value:string}. Example:
{"field": "apps_created", "value": "3"}
Say I want to find all the Visitors with 3 or more apps_created, how would I go about this? Note: every Visitor can have different fields and often there is not overlap with other visitors.
I've tried to consult the postgres documentation or other stackoverflow questions, but I have a hard time figuring out what functions/operators are used in this situation.
Any help is much appreciated
select *
from visitor
where
exists (
select 1
from unnest(custom) t(x)
where x->>'field' = 'apps_created' and (x->>'value')::int >= 3);
Upd
However the classic way to implement such things in relational databases is (schematic):
create table entity (
entity_id serial not null primary key,
...);
create table param (
param_id serial not null primary key,
param_name varchar not null unique,
...);
create table param_value (
param_id int references param(param_id),
entity_id int references entity(entity_id) on delete cascade,
value varchar,
...
primary key (param_id, entity_id));

"polymorphism" for FOREIGN KEY constraints

There is this field in a table:
room_id INT NOT NULL CONSTRAINT room_id_ref_room REFERENCES room
I have three 2 tables for two kinds of rooms: standard_room and family_room
How to do something like this:
room_id INT NOT NULL CONSTRAINT room_id_ref_room REFERENCES standard_room or family_room
I mean, room_id should reference either standard_room or family_room.
Is it possible to do so?
Here is the pattern I've been using.
CREATE TABLE room (
room_id serial primary key,
room_type VARCHAR not null,
CHECK CONSTRAINT room_type in ("standard_room","family_room"),
UNIQUE (room_id, room_type)
);
CREATE_TABLE standard_room (
room_id integer primary key,
room_type VARCHAR not null default "standard_room",
FOREIGN KEY (room_id, room_type) REFERENCES room (room_id, room_type),
CHECK CONSTRAINT room_type = "standard_room"
);
CREATE_TABLE family_room (
room_id integer primary key,
room_type VARCHAR not null default "family_room",
FOREIGN KEY (room_id, room_type) REFERENCES room (room_id, room_type),
CHECK CONSTRAINT room_type = "family_room"
);
That is, the 'subclasses' point at the super-class, by way of a type descriminator column (such that the pointed to base class is of the correct type, and that primary key of the super class is the same as the child classes.
Here's the same SQL from the accepted answer that works for PostGres 12.8. There's a few issues not only the CREATE_TABLE syntax mistake:
CREATE TABLE room (
room_id serial primary key,
room_type VARCHAR not null,
CONSTRAINT room_in_scope CHECK (room_type in ('standard_room','family_room')),
CONSTRAINT unique_room_type_combo UNIQUE (room_id, room_type)
);
CREATE TABLE standard_room (
room_id integer primary key,
room_type VARCHAR not null default 'standard_room',
CONSTRAINT roomid_std_roomtype_fk FOREIGN KEY (room_id, room_type) REFERENCES public."room" (room_id, room_type),
CONSTRAINT std_room_constraint CHECK (room_type = 'standard_room')
);
CREATE TABLE family_room (
room_id integer primary key,
room_type VARCHAR not null default 'family_room',
CONSTRAINT roomid_fam_roomtype_fk FOREIGN KEY (room_id, room_type) REFERENCES "room" (room_id, room_type),
CONSTRAINT fam_room_constraint CHECK (room_type = 'family_room')
);
NOTE: The SQL above uses constraints to enforce the child room_type values default to the parent tables' room_type values: 'standard_room' or 'family_room'.
PROBLEM: Since the child tables Primary Key's expect either the standard and family room Primary Key that means you can't insert more than one record in thsee two child tables.
insert into room (room_type) VALUES ('standard_room'); //Works
insert into room (room_type) values ('family_room'); //Works
insert into standard_room (room_id,pictureAttachment) VALUES (1,'Before Paint'); //Works
insert into standard_room (room_id,pictureAttachment) VALUES (1,'After Paint'); //Fails
insert into standard_room (room_id,pictureAttachment) VALUES (1,'With Furniture');
insert into family_room (room_id,pictureAttachment) VALUES (2, 'Beofre Kids'); //Works
insert into family_room (room_id,pictureAttachment) VALUES (2,'With Kids'); //Fails
To make the tables accept > 1 row you have to remove the Primary Keys from the 'standard_room' and 'family_room' tables which is BAD database design.
Despite 26 upvotes I will ping OP about this as I can see the answer was typed free hand.
Alternate Solutions
For smallish tables with less than a handful of variations a simple alterative is a single table with Bool columns for different table Primary Key fields.
Single Table "Room"
Id
IsStandardRoom
IsFamilyRoom
Desc
Dimensions
1
True
False
Double Bed, BIR
3 x 4
2
False
True
3 Set Lounge
5.5 x 7
SELECT * FROM Room WHERE IsStdRoom = true;
At the end of the day, in a relational database it's not very common to be adding Room Types when it involves creating the necessary related database tables using DDL commands (CREATE, ALTER, DROP).
A typical future proof database design allowing for more Tables would look something like this:
Multi Many-To-Many Table "Room"
Id
TableName
TableId
1
Std
8544
2
Fam
236
3
Std
4351
Either Standard or Family:
select * from standard_room sr where sr.room_id in
(select TableId from room where TableName = 'Std');
select * from family_room fr where fr.room_id in
(select id from room where TableName = 'Fam');
Or both:
select * from standard_room sr where sr.room_id in
(select TableId from room where TableName = 'Std')
UNION
select * from family_room fr where fr.room_id in
(select id from room where TableName = 'Fam');
Sample SQL to demo Polymorphic fields:
If you want to have different Data Types in the polymorphic foreign key fields then you can use this solution. Table r1 stores a TEXT column, r2 stores a TEXT[] Array column and r3 a POLYGON column:
CREATE OR REPLACE FUNCTION null_zero(anyelement)
RETURNS INTEGER
LANGUAGE SQL
AS $$
SELECT CASE WHEN $1 IS NULL THEN 0 ELSE 1 END;
$$;
CREATE TABLE r1 (
r1_id SERIAL PRIMARY KEY
, r1_text TEXT
);
INSERT INTO r1 (r1_text)
VALUES ('foo bar'); --TEXT
CREATE TABLE r2 (
r2_id SERIAL PRIMARY KEY
, r2_text_array TEXT[]
);
INSERT INTO r2 (r2_text_array)
VALUES ('{"baz","blurf"}'); --TEXT[] ARRAY
CREATE TABLE r3 (
r3_id SERIAL PRIMARY KEY
, r3_poly POLYGON
);
INSERT INTO r3 (r3_poly)
VALUES ( '((1,2),(3,4),(5,6),(7,8))' ); --POLYGON
CREATE TABLE flex_key_shadow (
flex_key_shadow_id SERIAL PRIMARY KEY
, r1_id INTEGER REFERENCES r1(r1_id)
, r2_id INTEGER REFERENCES r2(r2_id)
, r3_id INTEGER REFERENCES r3(r3_id)
);
ALTER TABLE flex_key_shadow ADD CONSTRAINT only_one_r
CHECK(
null_zero(r1_id)
+ null_zero(r2_id)
+ null_zero(r3_id)
= 1)
;
CREATE VIEW flex_key AS
SELECT
flex_key_shadow_id as Id
, CASE
WHEN r1_id IS NOT NULL THEN 'r1'
WHEN r2_id IS NOT NULL THEN 'r2'
WHEN r3_id IS NOT NULL THEN 'r3'
ELSE 'wtf?!?'
END AS "TableName"
, CASE
WHEN r1_id IS NOT NULL THEN r1_id
WHEN r2_id IS NOT NULL THEN r2_id
WHEN r3_id IS NOT NULL THEN r3_id
ELSE NULL
END AS "TableId"
FROM flex_key_shadow
;
INSERT INTO public.flex_key_shadow (r1_id,r2_id,r3_id) VALUES
(1,NULL,NULL),
(NULL,1,NULL),
(NULL,NULL,1);
SELECT * FROM flex_key;

Primary key for multiple columns in PostgreSQL?

How to provide primary key for multiple column in a single table using PostgreSQL?
Example:
Create table "Test"
(
"SlNo" int not null primary key,
"EmpID" int not null, /* Want to become primary key */
"Empname" varchar(50) null,
"EmpAddress" varchar(50) null
);
Note: I want to make "EmpID" also a primary key.
There can only be one primary key per table - as indicated by the word "primary".
You can have additional UNIQUE columns like:
CREATE TABLE test(
sl_no int PRIMARY KEY, -- NOT NULL due to PK
emp_id int UNIQUE NOT NULL,
emp_name text,
emp_addr text
);
Columns that are (part of) the PRIMARY KEY are marked NOT NULL automatically.
Or use a table constraint instead of a column constraint to create a single multicolumn primary key. This is semantically different from the above: Now, only the combination of both columns must be unique, each column can hold duplicates on its own.
CREATE TABLE test(
sl_no int, -- NOT NULL due to PK below
emp_id int , -- NOT NULL due to PK below
emp_name text,
emp_addr text,
PRIMARY KEY (sl_no, emp_id)
);
Multicolumn UNIQUE constraints are possible, too.
Aside: Don't use CaMeL-case identifiers in Postgres. Use legal, lower-case identifiers so you never have to use double-quotes. Makes your life easier. See:
Are PostgreSQL column names case-sensitive?
In case you want to specify the name of the primary key constraint:
CREATE TABLE test(
sl_no int not null,
emp_id int not null,
emp_name text,
emp_addr text,
constraint pk_test primary key (sl_no, emp_id)
);
Source: https://www.postgresqltutorial.com/postgresql-primary-key/