Return everything from many-to-many relationship with only one query - sql

I'll give an example to better clarify what I want:
Suppose I have the following classes in my programming language:
Class Person(
int id,
string name,
List<Car> cars
);
Class Car(
int id,
string name,
string brand
)
I want to save that in a PostgreSQL database, so I'll have the following tables:
CREATE TABLE person(
id SERIAL,
name TEXT
);
CREATE TABLE car(
id SERIAL,
name TEXT,
brand TEXT
)
CREATE TABLE person_car(
person_id int,
car_id int,
CONSTRAINT fk_person
FOREIGN KEY (person_id)
REFERENCES person(id),
CONSTRAINT fk_car
FOREIGN KEY (car_id)
REFERENCES car(id)
)
Then, I want to select all people with their cars from DB. I can select all people, then for each person, select their cars. But supposing I have 1000 people, I will have to query the DB 1001 times (one to select all people, and one for each person, to get their cars).
Is there an efficient way to bring all people, each with all their cars in a single query, so that I can fill my classes with the correct data without querying the DB a lot of times?

If you want to return a hierarchical dataset, you can use subqueries with COALESCE, for example :
SELECT
p.id
p.name,
COALESCE((SELECT
json_agg(json_build_object(
'id', c.id,
'name', c.name,
'brand', c.brand
))
FROM car AS c
JOIN person_car pc ON c.id = pc.car_id
WHERE pc.person_id = p.id), '[]'::json) AS cars
FROM person AS p;

You are joining person and car to person_car based on their respective ID’s.
SELECT
person.name,
person.id as person_id,
car.name,
car.brand,
car.id as car_id
FROM
person
JOIN
person_car
ON
person.id = person_car.person_id
JOIN
car
ON
car.id = person_car.car_id

Related

How to join 2 tables and have a list of other table's values nested in the first one in PostgreSQL?

Let's assume that we have these 2 tables: person and car
CREATE TABLE person (
id BIGSERIAL NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL
);
CREATE TABLE car (
id BIGSERIAL NOT NULL PRIMARY KEY,
make VARCHAR NOT NULL,
person_id BIGINT NOT NULL REFERENCES person(id)
);
What I try to do is to find all people, find every car and create an array of objects like this one
[
{
"id": "PERSON_ID",
"name": "PERSON_NAME",
"cars": [
{
"id": "CAR_ID",
"model": "MODEL_NAME",
"person_id": "PERSON_ID"
}
]
}
]
I have tried using the AS alias with a JOIN on person table from car table but it didn't work. Is there a way to do this? Thank you!
You may try the following. See a working fiddle:
Schema (PostgreSQL v13)
CREATE TABLE person (
id BIGSERIAL NOT NULL PRIMARY KEY,
name VARCHAR NOT NULL
);
CREATE TABLE car (
id BIGSERIAL NOT NULL PRIMARY KEY,
make VARCHAR NOT NULL,
person_id BIGINT NOT NULL REFERENCES person(id)
);
insert into person(name) values ('tom'),('harry');
insert into car (id,make,person_id) values (1,'ford',1),(2,'audi',1),(3,'nissan',2);
Query #1
SELECT
p.id,
p.name,
array_agg(row_to_json(c)) cars
FROM
person p
INNER JOIN
(SELECT id, make model, person_id FROM car) c ON p.id = c.person_id
GROUP BY
p.id,
p.name;
id
name
cars
1
tom
[{"id":1,"model":"ford","person_id":1},{"id":2,"model":"audi","person_id":1}]
2
harry
[{"id":3,"model":"nissan","person_id":2}]
View on DB Fiddle
If you want the result set as an array in Postgres, you can use:
select p.*, array_agg(c)
from person p join
car c
on c.person_id = p.id
group by p.id;
You can do something similar if you want JSON returned as well.

SQL cross-reference table self-reference

I am working on a project where I have a table of
all_names(
team_name TEXT,
member_name TEXT,
member_start INT,
member_end INT);
What I have been tasked with is creating a table of
participants(
ID SERIAL PRIMARY KEY,
type TEXT,
name TEXT);
which contains all team and member names as their own entries. Type may be either "team" or "member".
To compliment this table of participants I am trying to create a cross-reference table that allows a member to be referenced to a team by ID and vice versa. My table looks like this:
belongs_to(
member_id INT REFERENCES participants(ID),
group_id INT REFERENCES participants(ID),
begin_year INT,
end_year INT,
PRIMARY KEY (member_id, group_id);
I am unsure of how to proceed and populate the table properly.
The select query I have so far is:
SELECT DISTINCT ON (member_name, team_name)
id, member_name, team_name, member_begin_year, member_end_year
FROM all_names
INNER JOIN artists ON all_names.member_name = participants.name;
but I am unsure of how to proceed. What is the proper way to populate the cross reference table?
Probably the easiest solution is to use a few statements. Wrap this is a transaction to make sure you don't get concurrency issues:
BEGIN;
INSERT INTO participants (type, name)
SELECT DISTINCT 'team', team_name
FROM all_names
UNION
SELECT DISTINCT 'member', member_name
FROM all_names;
INSERT INTO belongs_to
SELECT m.id, g.id, a.member_start, a.member_end
FROM all_names a
JOIN participants m ON m.name = a.member_name
JOIN participants g ON g.name = a.team_name;
COMMIT;
Members that are part of multiple teams get all of their memberships recorded.

SQL to select a table through a generic relationship

In a relational database I have three tables. Using SQL Server.
person(id, type)
student(id, person_id, type, student specific fields)
teacher(id, person_id, type, teacher specific fields)
Student and teacher are both people, therefore a student will have a record in both the person and student tables, as will the teacher. Student and teacher have foreign keys to person. Student and teacher have different field definitions therefore a union will NOT work.
Now I have the person's id and depending on whether the person is a student or teacher I would like to select * from the relevant table (not person).
For example, if the person is a student I would like my query to select the student table.
I can think of a few inefficient methods but I am looking for the optimum one.
I would suggest a UNION
SELECT student.*
FROM student
WHERE person_id= #id
UNION
SELECT teacher.*
FROM teacher
WHERE person_id= #id
if exists(select person_id from student where person_id = #id)
select * from student where person_id = #id
else
if exists(select person_id from teacher where person_id = #id)
select * from teacher where person_id = #id
If your RDBMS is SQLServer, then I would abstract a view along the lines of podiluska's union, mapping out specific fields in Student and Teacher to common names, and padding with NULLs where no mapping is possible
And assuming that Students and Teachers inherit from person (i.e. both are 0..1 to 1 with Person), then they can share the same primary key, i.e. no need for new surrogates keys on Teacher and Student.
I've assumed that person.type determines whether the person is a Student(S) or Teacher(T).
CREATE VIEW SubClassesOfPerson AS
SELECT p.id as PersonId,
p.name as PersonName,
p.OtherBaseFieldsHere,
s.SomeStudentSpecificField AS MappedField1,
s.SomeStudentSpecificFieldX AS MappedFieldX,
s.SomeStudentSpecificField as MappedFieldForStudentOnly,
NULL as MappedFieldForTeacherOnly -- Pad this because it can't be mapped
FROM person p
INNER JOIN student s
on s.person_id = p.id AND p.type = 'S'
UNION
SELECT p.id as PersonId,
p.name as PersonName,
p.OtherBaseFieldsHere,
t.SomeTeacherSpecificField AS MappedField1,
t.SomeTeacherSpecificFieldX AS MappedFieldX,
NULL as MappedFieldForStudentOnly, -- Pad this because it can't be mapped
t.SomeTeacherSpecificField as MappedFieldForTeacherOnly
FROM person p
INNER JOIN teacher t
on t.person_id = p.id AND p.type = 'T'

Many-to-many relation filter

I need to filter my query with categories table which has many2many relation with another table. Is it possible to filter query with many2many relation?
Table res_partner has many2many field category_id relating to table res_partner_category.res_partner, or let's just say partners can have many categories. What I need is to filter res_partners table where it has category named 'business' or 'retail'. If it doesn't have any of these categories, it should not be shown.
Also there is another field in res_partner which is category_value_ids and has one2many relation with res_partners_category_value:
res_partner has following fields with relations:
category_id to res_partner_category (many2many)
category_value_ids to res_partner_category_value (one2many)
name (char)
res_partner_category has following fields with relations:
partner_ids to res_partner (many2many)
name (char)
res_partner_category_value has following fields with relations:
category_group_id to res_partner_category (many2one)
category_id to res_partner_category (many2one)
object_id tores_partner (many2one)
But if I try to use res_partner_category_value table in SQL query I get error that I can't use it in query.
So for example, if there are 4 partners with these categories:
first: categ1, categ2, business
second: retail
third: retail, business
fourth: categ1, categ2
The query should return first, second and third partners.
One person told me it's not possible to filter like this with many2many relation. So I wonder is it really not possible or just complicated?
EDIT:
I found one more table called res_partner_category_rel. I didn't see it, because in Openerp administration interface, where you can see all objects of database, that table is not shown. You can only see it directly through database.
So I was confused by this "missing" table:
res_partner_category_rel:
partner_id (many2one)
category_id (many2one)
Setup
This is the test case you should have provided:
CREATE TABLE partner (
partner_id serial PRIMARY KEY
, partner text
);
INSERT INTO partner (partner) VALUES
('partner1')
, ('partner2')
, ('partner3')
, ('partner4')
;
CREATE TABLE category (
category_id serial PRIMARY KEY
, category text
);
INSERT INTO category (category) VALUES
('categ1')
, ('categ2')
, ('business')
, ('retail')
;
CREATE TABLE partner_category (
partner_id int REFERENCES partner(partner_id)
, category_id int REFERENCES category(category_id)
, CONSTRAINT cat_pk PRIMARY KEY (partner_id, category_id)
);
INSERT INTO partner_category (partner_id, category_id) VALUES
(1,1), (1,2), (1,3)
, (2,4)
, (3,3), (3,4)
, (4,1), (4,2);
Solution
One way:
SELECT p.*
FROM partner p
WHERE EXISTS (SELECT FROM partner_category pc WHERE pc.partner_id = p.partner_id AND pc.category_id = 3)
OR EXISTS (SELECT FROM partner_category pc WHERE pc.partner_id = p.partner_id AND pc.category_id = 4)
ORDER BY p.partner_id;
Another:
SELECT p.*
FROM (SELECT partner_id FROM partner_category WHERE category_id = 3) pc1
FULL JOIN (SELECT partner_id FROM partner_category WHERE category_id = 4) pc2 USING (partner_id)
JOIN partner p USING (partner_id)
ORDER BY p.partner_id;
fiddle
Old sqlfiddle
The second one assumes unique (partner_id, category_id) in partner_category.
As you already noticed, the many2one category_id is not represented in the database as a table field, but as a table relating Partners and Categories.
The SQL you need could look like this:
SELECT p.*
FROM res_partner p
INNER JOIN res_partner_category_rel rel ON p.id = rel.partner_id
INNER JOIN res_partner_category c ON rel.category_id = c.id
WHERE c.id in (3,4)
If you want to do the filter in the python object, the usual searchcall should work:
list_ids = partner_model.search(cr, uid, [('category_id', 'in', [3,4])])
As a bonus, since Categories are organized in a tree, you could get those categories and all their children using:
list_ids = partner_model.search(cr, uid, [('category_id', 'child of', [3,4])])

SQL query to find rows with at least one of the specified values

Suppose you had two tables. One called MOVIES:
MovieId
MovieName
Then another called ACTORS that contains people who appear in those movies:
MovieId
ActorName
Now, I want to write a query that returns any movie that contains ONE OR MORE of the following actors: "Tom Hanks", "Russell Crowe" or "Arnold Schwarzenegger".
One way to do it would be something like:
SELECT DISTINCT A.MovieId, M.MovieName FROM ACTORS A
INNER JOIN MOVIES M USING (MovieId)
WHERE A.ActorName IN ('Tom Hanks', 'Russell Crowe', 'Arnold Schwarzenegger');
Which is perfectly fine, however in my case I might have several more of these conditions on the WHERE clause so I want to find a way to make the MOVIES table the primary table I select from.
What's the best way to query for this? I'm using Oracle 11g if that matters, but I'm hoping for a standard SQL method.
You can use EXISTS or IN subqueries:
SELECT *
FROM MOVIES m
WHERE EXISTS
(
SELECT *
FROM ACTORS a
WHERE a.MovieId = m.MovieId
AND a.ActorName IN ('Tom Hanks', 'Russell Crowe', 'Arnold Schwarzenegger')
)
or
SELECT *
FROM MOVIES m
WHERE m.MovieId IN
(
SELECT a.MovieId
FROM ACTORS a
WHERE a.ActorName IN ('Tom Hanks', 'Russell Crowe', 'Arnold Schwarzenegger')
)
First you should have a 3rd table implementing the n:m relationship:
CREATE TABLE movie (
movie_id int primary key
,moviename text
-- more fields
);
CREATE TABLE actor (
actor_id int primary key
,actorname text
-- more fields
);
CREATE TABLE movieactor (
movie_id int references movie(movie_id)
,actor_id int references actor(actor_id)
,CONSTRAINT movieactor_pkey PRIMARY KEY (movie_id, actor_id)
);
Then you select like this:
SELECT DISTINCT m.movie_id, m.moviename
FROM movie m
JOIN movieactor ma USING (movie_id)
JOIN actor a USING (actor_id)
WHERE a.actorname IN ('Tom Hanks', 'Russell Crowe', 'Arnold Schwarzenegger');
Note, that text literals are enclose in single quotes!