How to exclude join based on another join in postgresql? - sql

This is just simplified example - actual schema is much more complicated.
Each car is offered in "base" color (offer.model_id = null) or as models in different colors. I need to exclude cars with existing models NOT in BLUE or WHITE but keep cars in base color without extra models.
Schema (PostgreSQL v13)
CREATE TABLE color (
id int4 NOT NULL,
name varchar NOT NULL,
CONSTRAINT color_pkey PRIMARY KEY (id)
);
INSERT INTO color (id, name) VALUES(1, 'WHITE');
INSERT INTO color (id, name) VALUES(2, 'BLUE');
INSERT INTO color (id, name) VALUES(3, 'RED');
INSERT INTO color (id, name) VALUES(4, 'BLACK');
CREATE TABLE car (
id int4 NOT NULL,
name varchar NOT NULL,
CONSTRAINT car_pkey PRIMARY KEY (id)
);
INSERT INTO car (id, name) VALUES(1, 'Ford');
INSERT INTO car (id, name) VALUES(2, 'Skoda');
INSERT INTO car (id, name) VALUES(3, 'Toyota');
CREATE TABLE model (
id int4 NOT NULL,
car_id int4 NOT NULL,
name varchar NOT NULL,
CONSTRAINT model_pkey PRIMARY KEY (id)
);
INSERT INTO model (id, car_id, name) VALUES(1, 1, 'Escort');
INSERT INTO model (id, car_id, name) VALUES(2, 1, 'Puma');
INSERT INTO model (id, car_id, name) VALUES(3, 2, 'Octavia');
INSERT INTO model (id, car_id, name) VALUES(4, 3, 'Yaris');
CREATE TABLE offer (
id int4 NOT NULL,
car_id int4 NOT NULL,
model_id int4,
color_id int4 NOT NULL,
CONSTRAINT offer_pkey PRIMARY KEY (id)
);
ALTER TABLE offer ADD CONSTRAINT "offer_car_id" FOREIGN KEY ("car_id") REFERENCES car(id);
ALTER TABLE offer ADD CONSTRAINT "offer_model_id" FOREIGN KEY ("model_id") REFERENCES model(id);
ALTER TABLE offer ADD CONSTRAINT "offer_color_id" FOREIGN KEY ("color_id") REFERENCES color(id);
CREATE UNIQUE INDEX "offer_car_color" ON offer USING btree (car_id) WHERE (model_id IS NULL);
CREATE UNIQUE INDEX "offer_model_color" ON offer USING btree (car_id, model_id) WHERE (model_id IS NOT NULL);
INSERT INTO offer (id, car_id, model_id, color_id) VALUES(1, 1, null, 1);
INSERT INTO offer (id, car_id, model_id, color_id) VALUES(2, 2, null, 2);
INSERT INTO offer (id, car_id, model_id, color_id) VALUES(3, 3, null, 4);
INSERT INTO offer (id, car_id, model_id, color_id) VALUES(4, 2, 3, 3);
INSERT INTO offer (id, car_id, model_id, color_id) VALUES(5, 3, 4, 2);
Query #1
FULL OFFER - CARS AND MODELS TOGETHER
select car.id, car.name as car_name, model.name as model_name, color.name as offered_color
from offer
left outer join model on model.id = offer.model_id
inner join color on color.id = offer.color_id
inner join car on car.id = offer.car_id
order by car.name, model.name NULLS first;
id
car_name
model_name
offered_color
1
Ford
WHITE
2
Skoda
BLUE
2
Skoda
Octavia
RED
3
Toyota
BLACK
3
Toyota
Yaris
BLUE
Query #2
LIST OF CARS IN WHITE OR BLUE (base color or model's color)
select car.id, car.name as car_name, model.name as model_name, color.name as offered_color
from car
inner join offer on offer.car_id = car.id
inner join color on color.id = offer.color_id
left outer join model on model.id = offer.model_id
where color.name in ('WHITE', 'BLUE')
order by car.name;
id
car_name
model_name
offered_color
1
Ford
WHITE
2
Skoda
BLUE
3
Toyota
Yaris
BLUE
Query #3
REQUIRED QUERY: EXCLUDE CARS WITH EXISTING MODELS BUT NOT IN WHITE OR BLUE
select ...?
id
car_name
model_name
offered_color
1
Ford
WHITE
3
Toyota
Yaris
BLUE
Skoda is excluded even it is offered in base BLUE but it has models and non is in BLUE or WHITE.
View on DB Fiddle

I think I've found the solution. The question is if it's the best one...?
https://www.db-fiddle.com/f/9Fh6SPxATLuHQCxG7iWktk/6
select
car.id,
car.name as car_name,
model.name as model_name,
color.name as color_name
from
car
inner join offer on
car.id = offer.car_id
inner join color on
color.id = offer.color_id
left join model on
model.id = offer.model_id
where
color.name in ('WHITE', 'BLUE')
and
((not exists (
select
1
from
offer o
where
o.car_id = car.id
and o.model_id is not null))
or (
exists (
select
1
from
offer o
inner join color c on
c.id = o.color_id
where
o.car_id = car.id
and o.model_id is not null
and c.name in ('WHITE', 'BLUE'))
))
order by
car.name;

I can't follow the logic in your description, but I might be able to offer some help on how to write the SQL so you can figure it out.
I suggest adopting the style of using CTEs so you can make it more readable and built it with building blocks.
Once you have all the different conditions you want as CTEs, you can use EXISTS() to filter based on whether they exist in each of those various conditions.
Example: (this does not produce the correct result, though)
WITH FULL_OFFER AS (
select
car.id,
car.name as car_name,
model.name as model_name,
color.name as offered_color
from offer
left outer join model on model.id = offer.model_id
inner join color on color.id = offer.color_id
inner join car on car.id = offer.car_id
),
BASE_MODELS AS (
SELECT *
FROM FULL_OFFER
WHERE model_name IS NULL
),
BASE_WHITE_OR_BLUE AS (
SELECT *
FROM BASE_MODELS
WHERE COALESCE(offered_color,'NONE') in('WHITE', 'BLUE')
),
NO_MODELS AS (
SELECT *
FROM FULL_OFFER
WHERE NOT EXISTS (
SELECT 1
FROM BASE_MODELS
WHERE FULL_OFFER.id=BASE_MODELS.id
AND FULL_OFFER.offered_color = BASE_MODELS.offered_color)
),
ANY_WHITE_BLUE AS (
SELECT *
FROM FULL_OFFER
WHERE COALESCE(offered_color,'NONE') in('WHITE', 'BLUE')
)
SELECT *
FROM FULL_OFFER
WHERE
(EXISTS (SELECT 1
FROM BASE_WHITE_OR_BLUE
WHERE FULL_OFFER.id = BASE_WHITE_OR_BLUE.id)
AND EXISTS (SELECT 1
FROM NO_MODELS
WHERE FULL_OFFER.id = NO_MODELS.id)
)
OR EXISTS (SELECT 1
FROM ANY_WHITE_BLUE
WHERE FULL_OFFER.id = ANY_WHITE_BLUE.id)
This doesn't produce what you want, but only because I can't translate the conditions from your description.
Hopefully those 2 tips will help you out, if someone else doesn't come along and help you with the correct SQL.

Related

Find all the persons which does not contain the provided resources

Below are the tables
CREATE TABLE Person(
PersonID INT PRIMARY KEY,
FirstName VARCHAR(10),
LastName VARCHAR(10));
CREATE TABLE Resources(
ResourceID CHAR(3) PRIMARY KEY
);
CREATE TABLE PR (
PersonID INT,
ResourceID CHAR(3),
CONSTRAINT pkpr PRIMARY KEY (PersonID, ResourceID),
CONSTRAINT fkPersonID FOREIGN KEY (PersonID) REFERENCES Person(PersonID),
CONSTRAINT fkResourceID FOREIGN KEY (ResourceID) REFERENCES Resources(ResourceID));
INSERT INTO Person(PersonID, FirstName, LastName) VALUES (1, 'Bill', 'Smith'),(2, 'John','Jones'), (3, 'Tim', 'Jolt');
INSERT INTO Resources (ResourceID) VALUES ('ABC'),('DEF'),('HIJ');
INSERT INTO PR (PersonID, ResourceID) VALUES (1,'ABC'),(1,'DEF'),(2,'ABC'), (2,'HIJ'), (1,'HIJ'), (3, 'DEF');
How to find all the persons which does not have resources ('ABC', 'HIJ') ?
With above inserted data it should return person Tim Jolt
I am using PostgreSql.
So your main source of entity is the Person table. And you need to ignore all Persons which have the given resources.
So the SQL will be like below.
select PersonID from Person where PersonID not in ( select PersonID from PR where ResourceID in ('ABC', 'DEF'))
You can write your query in following 4 ways:
Using NOT IN: same answered by Pratik Soni
select personid from person
where personid not in ( select personid from PR where resourceid in ('ABC', 'HIJ'))
Using NOT EXIST:
select personid from person t1
where not exists (select 1 from PR where personid=t1.personid and resourceid in ('ABC', 'HIJ'))
Using <> ALL:
SELECT personid FROM person WHERE
personid <> ALL(select personid from PR where resourceid in ('ABC', 'HIJ'))
Using LEFT JOIN and IS NULL
SELECT p.personid
FROM person p
LEFT JOIN PR r ON p.personid = r.personid AND r.resourceid in ('ABC', 'HIJ')
where r.personid is null
All 4 methods have their own pros and cons. No body can predict the performance without seeing Explain Analyze result. So check the execution plan using all above queries with real data and decide accordingly what method you should adopt.
DEMO

SQL Join Table as JSON data

I am trying to join reviews and likes onto products, but it seems, for some reason that the output of "reviews" column is duplicated by the length of another foreign table, likes, the output length of "reviews" is
amount of likes * amount of reviews
I have no idea why this is happening
My desired output is that the "reviews" column contains an array of JSON data such that one array is equal to one row of a related review
Products
Title Image
----------------------
Photo photo.jpg
Book book.jpg
Table table.jpg
Users
Username
--------
Admin
John
Jane
Product Likes
product_id user_id
---------------------
1 1
1 2
2 1
2 3
Product Reviews
product_id user_id review
-------------------------------------
1 1 Great Product!
1 2 Looks Great
2 1 Could be better
This is the query
SELECT "products".*,
array_to_json(array_agg("product_review".*)) as reviews,
EXISTS(SELECT * FROM product_like lk
JOIN users u ON u.id = "lk"."user_id" WHERE u.id = 4
AND "lk"."product_id" = products.id) AS liked,
COUNT("product_like"."product_id") AS totalLikes from "products"
LEFT JOIN "product_review" on "product_review"."product_id" = "products"."id"
LEFT JOIN "product_like" on "product_like"."product_id" = "products"."id"
group by "products"."id"
Query to create schema and insert data
CREATE TABLE products
(id SERIAL, title varchar(50), image varchar(50), PRIMARY KEY(id))
;
CREATE TABLE users
(id SERIAL, username varchar(50), PRIMARY KEY(id))
;
INSERT INTO products
(title,image)
VALUES
('Photo', 'photo.jpg'),
('Book', 'book.jpg'),
('Table', 'table.jpg')
;
INSERT INTO users
(username)
VALUES
('Admin'),
('John'),
('Jane')
;
CREATE TABLE product_review
(id SERIAL, product_id int NOT NULL, user_id int NOT NULL, review varchar(50), PRIMARY KEY(id), FOREIGN KEY (product_id) references products, FOREIGN KEY (user_id) references users)
;
INSERT INTO product_review
(product_id, user_id, review)
VALUES
(1, 1, 'Great Product!'),
(1, 2, 'Looks Great'),
(2, 1, 'Could be better')
;
CREATE TABLE product_like
(id SERIAL, product_id int NOT NULL, user_id int NOT NULL, PRIMARY KEY(id), FOREIGN KEY (product_id) references products, FOREIGN KEY (user_id) references users)
;
INSERT INTO product_like
(product_id, user_id)
VALUES
(1, 1),
(1, 2),
(2, 1),
(2, 3)
fiddle with the schema and query:
http://sqlfiddle.com/#!15/dff2c/1
Thanks in advance
The reason you are getting multiple results is because of the one-to-many relationships between product_id and product_review and product_like causing duplication of rows prior to aggregation. To work around that, you need to perform the aggregation of those tables in subqueries and join the derived tables instead:
SELECT "products".*,
"pr"."reviews",
EXISTS(SELECT * FROM product_like lk
JOIN users u ON u.id = "lk"."user_id" WHERE u.id = 4
AND "lk"."product_id" = products.id) AS liked,
COALESCE("pl"."totalLikes", 0) AS totalLikes
FROM "products"
LEFT JOIN (SELECT product_id, array_to_json(array_agg("product_review".*)) AS reviews
FROM "product_review"
GROUP BY product_id) "pr" on "pr"."product_id" = "products"."id"
LEFT JOIN (SELECT product_id, COUNT(*) AS "totalLikes"
FROM "product_like"
GROUP BY product_id) "pl" on "pl"."product_id" = "products"."id"
Output:
id title image reviews liked totallikes
1 Photo photo.jpg [{"id":1,"product_id":1,"user_id":1,"review":"Great Product!"},{"id":2,"product_id":1,"user_id":2,"review":"Looks Great"}] f 2
2 Book book.jpg [{"id":3,"product_id":2,"user_id":1,"review":"Could be better"}] f 2
3 Table table.jpg f 0
Demo on dbfiddle

How to join id occurrences instead of simply showing count(*)?

I did this snippet to demonstrate: http://sqlfiddle.com/#!6/ed243/2
Schema:
create table professional(
id int identity(1,3) primary key,
name varchar(20)
)
insert into professional values('professional A')
insert into professional values('professional B')
insert into professional values('professional C')
create table territory(
id int identity(2,3) primary key,
name varchar(20)
)
insert into territory values('territory A')
insert into territory values('territory B')
insert into territory values('territory C')
create table panel(
id int identity(3,3) primary key,
idProfessional int not null,
idTerritory int not null,
)
insert into panel values(1, 2)
insert into panel values(4, 5)
insert into panel values(7, 8)
insert into panel values(1, 5)
insert into panel values(7, 8)
insert into panel values(7, 2)
And the query I've got so far:
select
p.id, p.name, count(*) as Territories
from
(select distinct idProfessional, idTerritory from panel) panel
inner join
professional p
on p.id = panel.idProfessional
group by
p.id,
p.name
having count(*) > 1
order by p.id
The above query shows as result in how many territories each professional works filtering with distinct and by showing only professionals that work in more than one territory with having:
-------------------------------------------------------
| id | name | Territories |
-------------------------------------------------------
| 1 | professional A | 2 |
| 7 | professional C | 2 |
-------------------------------------------------------
Ok, but.. is it possible to show in Territories each idTerritory joined like "2, 5" instead of count(*) ?
Thanks in advance.
When it's necessary, I usually use the FOR XML function to do this kind of concatenation of multiple rows. I think this query does what you are looking for:
select
p.id, p.name, STUFF(
(select ', ' + CAST(t.id AS VARCHAR(10))
from panel panel2
inner join territory t
ON t.id = panel2.idTerritory
where panel2.idProfessional = p.id
order by t.name
for xml path(''), root('XMLVal'), type
).value('/XMLVal[1]','varchar(max)')
, 1, 2, '') as Territories
from panel
inner join
professional p
on p.id = panel.idProfessional
group by
p.id,
p.name
having count(*) > 1
order by p.id
I used this blog in creating my answer: http://blogs.lobsterpot.com.au/2010/04/15/handling-special-characters-with-for-xml-path/

Select rows that have a specific set of items associated with them through a junction table

Suppose we have the following schema:
CREATE TABLE customers(
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE items(
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE customers_items(
customerid INTEGER,
itemid INTEGER,
FOREIGN KEY(customerid) REFERENCES customers(id),
FOREIGN KEY(itemid) REFERENCES items(id)
);
Now we insert some example data:
INSERT INTO customers(name) VALUES ('John');
INSERT INTO customers(name) VALUES ('Jane');
INSERT INTO items(name) VALUES ('duck');
INSERT INTO items(name) VALUES ('cake');
Let's assume that John and Jane have id's of 1 and 2 and duck and cake also have id's of 1 and 2.
Let's give a duck to John and both a duck and a cake to Jane.
INSERT INTO customers_items(customerid, itemid) VALUES (1, 1);
INSERT INTO customers_items(customerid, itemid) VALUES (2, 1);
INSERT INTO customers_items(customerid, itemid) VALUES (2, 2);
Now, what I want to do is to run two types of queries:
Select names of customers who have BOTH a duck and a cake (should return 'Jane' only).
Select names of customers that have a duck and DON'T have a cake (should return 'John' only).
For the two type of queries listed, you could use the EXISTS clause. Below is an example query using the exists clause:
SELECT cust.name
from customers AS cust
WHERE EXISTS (
SELECT 1
FROM items
INNER JOIN customers_items ON items.id = customers_items.itemid
INNER JOIN customers on customers_items.customerid = cust.id
WHERE items.name = 'duck')
AND NOT EXISTS (
SELECT 1
FROM items
INNER JOIN customers_items ON items.id = customers_items.itemid
INNER JOIN customers on customers_items.customerid = cust.id
WHERE items.name = 'cake')
Here is a working example: http://sqlfiddle.com/#!6/3d362/2

SQL Simple SELECT Query

create table Person(
SSN INT,
Name VARCHAR(20),
primary key(SSN)
);
create table Car(
PlateNr INT,
Model VARCHAR(20),
primary key(PlateNr)
);
create table CarOwner(
SSN INT,
PlateNr INT,
primary key(SSN, PlateNR)
foreign key(SSN) references Person (SSN),
foreign key(PlateNr) references Car (PlateNr)
);
Insert into Person(SSN, Name) VALUES ('123456789','Max');
Insert into Person(SSN, Name) VALUES ('123456787','John');
Insert into Person(SSN, Name) VALUES ('123456788','Tom');
Insert into Car(PlateNr, Model) VALUES ('123ABC','Volvo');
Insert into Car(PlateNr, Model) VALUES ('321CBA','Toyota');
Insert into Car(PlateNr, Model) VALUES ('333AAA','Honda');
Insert into CarOwner(SSN, PlateNr) VALUES ('123456789','123ABC');
Insert into CarOwner(SSN, PlateNr) VALUES ('123456787','333AAA');
The problem I'm having is the SELECTE query I wanna make. I wan't to be able to SELECT everything from the Person and wan't the include the PlateNr of the car he's the owner of, an example:
PERSON
---------------------------------
SSN NAME Car
123456789 Max 123ABC
123456787 John 3338AAA
123456788 Tom
----------------------------------
So, I want to be able to show everything from the Person table and display the content of CarOwner aswell if the person is in fact a CarOwner. What I have so far is: "SELECT * from Person, CarOwner WHERE Person.SSN = CarOwner.SSN;". But this obviously results in only showing the person(s) that are CarOwners.
Hope I explained me well enough, Thanks.
Try this:
SELECT p.*, c.*
FROM Person p
LEFT OUTER JOIN CarOwner co
ON p.SSN = co.SSN
LEFT OUTER JOIN Car c
ON co.PlateNr = c.PlateNr
Show SQLFiddle
P.S. I've changed the type of your primary key PlateNr (in varchar and not in int)
select ssn, name, car
from Person p
LEFT OUTER JOIN CarOwner co
ON p.SSN = co.SSN
LEFT OUTER JOIN Car c
ON co.PlateNr = c.PlateNr