How would I join this query in one single SQL statement? - sql

Let's say I have these tables, with these fields:
companies: company_id | name | num_employees
companies_countries: company_id | country_id
countries: country_id | country_iso_code
Assuming this is a 1:1 relationship:
How can I join the country_iso_code directly into the company recordset, when I fetch all the companies? I think I would need two joins here?

A simple example :
select c.name, n.country_iso_code
from companies c,
companies_countries x,
countries n
where x.company_id = c.company_id
and n.country_id = x.country_id
Edit
For a good intro to JOIN, have a look at SQL JOIN.

SELECT
companies.company_id,
companies.name,
companies.num_employees,
countries.country_iso_code
FROM
companies
LEFT JOIN
companies_countries ON (companies_countries.company_id = companies.company_id)
LEFT JOIN
countries ON (countries.country_id = companies_countries.country_id);
A query that uses a LEFT JOIN instead of an INNER JOIN or an implicit join, will return companies even when they have no country assigned. On the other hand, a query with an INNER JOIN would skip companies that do not have an assigned country in companies_countries.
Note that your design is implying that each company can be assigned more than one country. If you want to enforce only one country for each company, simply put a country_id column in your companies table. You would not need the companies_countries table.

select name, country_iso_code
from companies left join companies_countries
on (companies.company_id = companies_countries.company_id)
left join countries
on (companies_countries.country_id = countries.country_id)

SELECT co.*, cu.country_iso_code
FROM companies co
LEFT JOIN companies_countries cc ON cc.company_id = co.company_id
LEFT JOIN countries c ON c.country_id = cc.country_id
Why LEFT JOIN and not the WHERE condition like in other examples? A join table (companies_countries) is not typically used in 1:1 relationships - it is overnormalization.
When your relationship ceases to be 1:1:
SELECT co.*, GROUP_CONCAT(cu.country_iso_code)
FROM companies co
LEFT JOIN companies_countries cc ON cc.company_id = co.company_id
LEFT JOIN countries c ON c.country_id = cc.country_id
This will return results like
CompanyA | Canada,USA,Mexico
CompanyB | Ireland,UK,Japan

This is not a 1:1 relationship. A country can have more the one company.
This is either a 1:N relationship (for some reason implemented using two relational tables), or the M:N relationship which describes multinational companies.
If this is a 1:N relationship, you could just put the country_code field into the companies table, in which case one join would be enough:
SELECT *
FROM companies co
LEFT JOIN
countries cn
ON cn.country_code = co.country_code
Your design is viable for both 1:N and M:N relationships, in which case two joins are required:
SELECT co.*, cn.*
FROM companies co
LEFT JOIN
company_countries cc
ON cc.company_id = co.company_id
LEFT JOIN
countries cn
ON cn.country_code = cc.country_code
If this is a 1:N relationship, you should make company_id a PRIMARY KEY in the company_country table.
If this is a M:N relationship, you should make a composite PRIMARY KEY on company_country (company_id, country_code)
You may want to read this article in my blog about the difference between entity-relationship model and its relational implementation:
What is entity-relationship model?

Yes, you would need two joins.
You can create a view and fake a single join though...

select companies.* from countries, companies_countries, companies where countries.country_id=companies_countries and companies_countries.company_id=company_id and countries.country_iso_code='xxxx'
where xxxx is the iso code you want to match

select c.company_id, c.name, c.num_employees, co.county_iso_code from
companies c, companies_countries cc, countries co
where c.company_id = cc.company_id
and cc.country_id = co.country_id

Related

Fetching distinct rows from multiple joins SQL

I have one main table called deliveries and it has one to many relationship with deliveries_languages as dl, deliveries_markets dm and deliveries_tags dt having delivery_id as foreign key. These 3 tables have one to one relation with languages , markets and tags respectively. Additionaly, deliveries, table have one to one relation with companies and have company_is as foreign key. Following is a query that I have written:
SELECT deliveries.*, languages.display_name, markets.default_name, tags.default_name, companies.name
FROM deliveries
JOIN deliveries_languages dl ON dl.delivery_id = deliveries.id
JOIN deliveries_markets dm ON dm.delivery_id = deliveries.id
JOIN deliveries_tags dt ON dt.delivery_id = deliveries.id
JOIN languages ON languages.id = dl.language_id
JOIN markets ON markets.id = dm.market_id
JOIN tags ON tags.id = dt.tag_id
JOIN companies ON companies.id = deliveries.company_id
WHERE
deliveries.name ILIKE '%new%' AND
deliveries.created_by = '5f331347-fb58-4f63-bcf0-702f132f97c5' AND
deliveries.deleted_at IS NULL
LIMIT 10
Here I am getting redundant delivery_ids because for each delivery_id there are multiple languages, markets and tags. I want to use limit on distinct delivery_ids. So, limit 10 should not give me 10 records from above join query but 10 records where there is distinct delivery_id (deliveries.id). I probably can use derived table concept here but I am not sure how can I do that. Can someone please help me to resolve this issue.
In Postgres, you can use distinct on:
SELECT DISTINCT ON (d.id) d.*, l.display_name, m.default_name, t.default_name, c.name
FROM deliveries d JOIN
deliveries_languages dl
ON dl.delivery_id = d.id JOIN
deliveries_markets dm
ON dm.delivery_id = d.id JOIN
deliveries_tags dt
ON dt.delivery_id = d.id JOIN
languages l
ON l.id = dl.language_id JOIN
markets m
ON m.id = dm.market_id JOIN
tags
ON t.id = dt.tag_id JOIN
companies c
ON c.id = d.company_id
WHERE d.name ILIKE '%new%' AND
d.created_by = '5f331347-fb58-4f63-bcf0-702f132f97c5' AND
d.deleted_at IS NULL
ORDER BY d.id
LIMIT 10;
For larger amounts of data, this can be very inefficient. Multiple junction tables result in Cartesian products when used like this. However, for a smallish amount of data, this should solve your problem.

SQL LEFT JOIN multiple tables including link to 1 table for each join

For the sake of explaining my problem lets say I am building cars. In my DB a car is made out of parts (engine, chassis, wheel). Each part has a physical location (part_location from the table locations) which is linked via (part_location = location_id).
In my case, there will be only 1 of each part, each part is unique
In my query I am trying to assembly my cars in a query, including the city and country of each part (not the id)
The beginning of my query:
SELECT car_name, car_type, engine_name, engine_type, chassis_name, chassis_type, wheel_name, wheel_type
FROM cars
LEFT JOIN engines ON car_id = engine_car_id
LEFT JOIN chassis ON car_id = chassis_car_id
LEFT JOIN wheels ON car_id = wheel_car_id
How can I include the location of each part (e.g. car_city, car_country; an alias for each part from location_city, location_country)?
My tables are set up as follows:
Table cars
car_id
car_name
car_type
Table engines (same for chassis, wheels)
engine_id
engine_name
engine_type
engine_car_id
engine_location_id
Table locations
location_id
location_city
location_country
I think the idea behind your query is correct. You should write it with explicit aliases, so I would expect something like this:
SELECT c.car_name, c.car_type, e.engine_name, e.engine_type,
ch.chassis_name, ch.chassis_type,
w.wheel_name, w.wheel_type,
el.city as engine_city
FROM cars c LEFT JOIN
engines e
ON e.car_id = c.engine_id LEFT JOIN
chassis ch
ON ch.car_id = c.chassis_id LEFT JOIN
wheels w
ON w.wheel_car_id = c.car_id LEFT JOIN
locations el
ON el.location_id = e.location_id
However, without the table layouts or sample data, this is just a guess.

LEFT JOIN across three tables (with junction table)

In Postgres, is there a way to perform a left join between tables linked by a junction table, with some filtering on the linked table?
Say, I have two tables, humans and pets, and I want to perform a query where I have the human ID, and the pet name. If the human ID exists, but they don't have a pet with that name, I still want the human's row to be returned.
If I had a FK relationship from pets to humans, this would work:
select h.*, p.*
from humans as h
left join pets as p on p.human_id = h.id and p.name = 'fluffy'
where h.id = 13
and I'd get a row with human 13's details, and fluffy's values. In addition, if human 13 didn't have a pet named 'fluffy', I'd get a row with human 13's values, and empty values for the pet's columns.
BUT, I don't have a direct FK relationship, I have a junction table between humans and pets, so I'm trying a query like:
select h.*, p.*
from humans as h
left join humans_pets_junction as j on j.human_id = h.id
left join pets as p on j.pet_id = p.id and p.name = 'fluffy'
where h.id = 13
Which returns rows for all of human 13's pets, with empty columns except for fluffy's row.
If I add p.name = 'fluffy' to the WHERE clause, that filters out all the empty rows, but also means I get 0 rows if human 13 doesn't have a pet named fluffy at all.
Is there a way to replicate the behavior of the FK-style left join, but when used with a junction table?
One method is to do the comparison in the where clause:
select h.*, p.*
from humans as h left join
humans_pets_junction as j
on j.human_id = h.id left join
pets as p
on j.pet_id = p.id and p.name = 'fluffy'
where h.id = 13 and (p.name = 'fluffy' or p.id is null);
Alternatively, join the junction table and the pets table as a subquery or CTE:
select h.*, p.*
from humans h left join
(select j.*
from humans_pets_junction j join
pets p
on j.pet_id = p.id and p.name = 'fluffy'
) pj
on pj.human_id = h.id
where h.id = 13;
In Postgres you can use parentheses to prioritize JOIN order. You do not need a subquery:
SELECT h.*, p.id AS p_id, p.name AS pet_name
FROM humans h
LEFT JOIN (pets p
JOIN humans_pets_junction j ON p.name = 'fluffy'
AND j.pet_id = p.id
AND j.human_id = 13) ON TRUE
WHERE h.id = 13;
Per documentation:
Parentheses can be used around JOIN clauses to control the join order.
In the absence of parentheses, JOIN clauses nest left-to-right.
I added the predicate j.human_id = 13 to the join between your junction table and the pets to eliminate irrelevant rows at the earliest opportunity. The outer LEFT JOIN only needs the dummy condition ON TRUE.
SQL Fiddle.
Aside 1: I assume you are aware that you have a textbook implementation of a n:m (many-to-many) relationship?
How to implement a many-to-many relationship in PostgreSQL?
Aside 2: The unfortunate naming convention in the example makes it necessary to deal out column aliases. Don't use "id" and "name" as column names in your actual tables to avoid such conflicts. Use proper names like "pet_id", "human_id" etc.

SQL join over five tables

I have five tables:
models: id, name, specification
models_networks: id, model_id, network_id
networks: id, name, description
countries_networks: id, country_id, network_id
countries: id, countryName, etc, etc
the models table is connected to the networks table via models_networks with a many to many relation.
the networks table is connected to the countries table via countries_networks with a many to many relation
I need to do the following query, but I'm stuck:
Select all the models that will work in a specific country.
e.g.: say France has two networks. PigNetwork and CowNetwork. I want to get all the models that work on PigNetwork or CowNetwork, basically any that work in that country one way or the other.
If I've made myself clear, can someone help with the JOIN query please? I've only ever gone as far as joining two tables before. Thanks.
SELECT
m.name AS model_name,
c.countryName,
COUNT(*) AS network_count
FROM
models AS m
INNER JOIN models_networks AS mn ON mn.model_id = m.id
INNER JOIN networks AS n ON n.id = mn.network_id
INNER JOIN countries_networks AS cn ON cn.network_id = n.id
INNER JOIN countries AS c ON c.id = cn.country_id
WHERE
c.countryName = 'France'
GROUP BY
m.name,
c.countryName
Something along the lines of this should work...
SELECT M.Name As ModelName FROM Countries C
INNER JOIN Countries_Networks CN
ON C.CountryId = CN.CountryId
INNER JOIN Networks N
ON CN.NetworkId = N.NetworkId
INNER JOIN ModelNetworks MN
ON MN.NetworkId = N.NetworkId
INNER JOIN Model M
ON M.ModelId = MN.ModelId
WHERE C.CountryName = 'FRANCE'
SELECT m.id
FROM model m
WHERE EXISTS
(
SELECT NULL
FROM model_networks mn
JOIN countries_networks cn
ON cn.network_id = mn.network_id
AND cn.country_id = #code_of_france
WHERE mn.model_id = m.id
)
This is efficient since it returns a model right that moment it finds the first suitable network.
Make sure you have the following UNIQUE indexes:
model_networks (model_id, network_id)
country_network (country_id, network_id)
SELECT models.id, models.name, models.specifications JOIN models_networks ON models.id=models_networks.model_id WHERE models_networks.networks_id IN (1,2)
(replace 1,2 with your network ids )
Doesn't look like you need two joins if you just want the models. You only need more joins if you don't know the network ids or if you need columns out of the other tables. The syntax for that is the same though. You just start with JOIN <table> ON <field>=<field> before the WHERE statement
select models.id, models.name, models.specification from models inner join models_networks on models.id = models_network.network_id inner join countries_networks on models_network.network_id = countries_networks.network_id where countries_networks.countryName = 'France'

SQL NOT EXISTS vs count distinct

I have three tables; Apartment (apartmentnr, floor, apartmenttype), Floor (floornr, house), House (housenr, adress)
Now, I want to show housenr and adresses of houses that have apartments of all apartmenttypes (1-4), using NOT EXISTS.
As #onedaywhen hints, not exists is pretty ponderous for this task, and count distinct offers leaner syntax (performance issues are mentioned in the article he points to):
SELECT House.adress
FROM House
JOIN Floor ON (House.housenr=Floor.house)
JOIN Apartment ON (Floor.floornr=Apartment.floor)
GROUP BY House.housenr
HAVING COUNT(DISTINCT Apartment.apartmenttype)=4
which essentially says "show adress [[sic]] of houses with 4 different types of apartments". Only good reason to force the use of not exists would be, as others hinted, homework...
Ah then you want to use Chris Dates approach, rather than the one made popular Joe Celko? If you are not sure, both approaches are discussed here:
Relational Division by Joe Celko
If I understand your relationships properly you could something like this. Remember you could use alias's for the tables to make it slightly easier to read, I've just kept them in to make it clear what I'm doing.
Select House.hoursenr, address
inner join Floor on House.housenr = Floor.house
inner join Apartment on Apartment.floor = Floor.floornr
where Apartment.apartmenttype in (1,2,3,4)
I would argue that some of your entity relationships are the wrong way round in your table definitions.
Given a structure like this:
Houses
ID
Address
Floors
ID
HouseID
Apartments
Id
FloorId
ApartmentType
I think this will do the trick:
SELECT H.ID FROM Houses H JOIN Floors F ON H.Id = F.HouseId JOIN Apartments A on F.Id = A.FloorId AND A.ApartmentType = 1 AND H.Id IN
(SELECT H.ID FROM Houses H JOIN Floors F ON H.Id = F.HouseId JOIN Apartments A on F.Id = A.FloorId AND A.ApartmentType = 2) AND H.Id IN
(SELECT H.ID FROM Houses H JOIN Floors F ON H.Id = F.HouseId JOIN Apartments A on F.Id = A.FloorId AND A.ApartmentType = 3) AND H.Id IN
(SELECT H.ID FROM Houses H JOIN Floors F ON H.Id = F.HouseId JOIN Apartments A on F.Id = A.FloorId AND A.ApartmentType = 4)
In English, it means, give me the set of all houses with an apartment of type 1 that are also in the set of houses with an apartment of type 2...and so on.