mooncow
mooncow

Reputation: 413

How can I join two "unnamed tables/selections" in PostgreSQL?

Given the tables person and car and a relationship based on car.id between the two tables, I could join them using the following code:

SELECT * FROM person
JOIN car
ON person.car_id = car.id;

However, I fail to figure out how to do this if I am working with two unnamed selections. Specifically, I would like to join the below selections based on id.

SELECT id, phone_number
FROM student
WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
UNION
SELECT id, phone_number
FROM administrator
WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
SELECT
    account_id,
    EXTRACT ('day' FROM (NOW()::timestamp - expiry_date::timestamp))
FROM loan
WHERE id IN
    (SELECT loan_id
    FROM fine
    WHERE paid_amount < fine_amount)

To clarify, if the first selection would be called table1 and the second selection would be called table2, I would like to join the selections according to the below code

SELECT * FROM table1
JOIN table2
ON table1.id = table2.account_id;

Upvotes: 0

Views: 529

Answers (2)

Caius Jard
Caius Jard

Reputation: 74670

You can give them a name each by putting the queries inline:


SELECT * FROM
(

--begin your first query
  SELECT id, phone_number
  FROM student
  WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
  UNION
  SELECT id, phone_number
  FROM administrator
  WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
--end your first query

) t1
INNER JOIN
(

--begin your second query
  SELECT
    account_id,
    EXTRACT ('day' FROM (NOW()::timestamp - expiry_date::timestamp))
  FROM loan
  WHERE id IN
    (SELECT loan_id
    FROM fine
    WHERE paid_amount < fine_amount)
--end your second query


) t2
ON
t1.id = t2.account_id

You can also turn each query into a named CTE:

WITH table1 AS (

  SELECT id, phone_number
  FROM student
  WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
  UNION
  SELECT id, phone_number
  FROM administrator
  WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
),

table2 as (
  SELECT
    account_id,
    EXTRACT ('day' FROM (NOW()::timestamp - expiry_date::timestamp))
  FROM loan
  WHERE id IN
    (SELECT loan_id
    FROM fine
    WHERE paid_amount < fine_amount)
)

--begin the query that joins the CTEs
SELECT * FROM table1
JOIN table2
ON table1.id = table2.account_id;

I'm struck though that you could simplify this query, something like this:

SELECT
  people.*,
  EXTRACT ('day' FROM (NOW()::timestamp - l.expiry_date::timestamp))
FROM
(
  SELECT 'student' as typ, id, phone_number FROM student
  UNION ALL
  SELECT 'admin', id, phone_number FROM administrator
) people

INNER JOIN loan l ON people.id = l.account_id
INNER JOIN fine f ON l.id = f.loan_id
WHERE f.paid_amount < f.fine_amount

You should get into the habit of aliasing everything you put in a query (unless it's a subquery that filters one table only) and then use the aliases:

SELECT s.id as studentid, l.id as loanid FROM
  student s
  INNER JOIN loan l ON s.id = l.account_id

The reasons are many, but by using an alias and fully qualifying all your column references you prevent your query from breaking if someone adds a new column to one of the tables in future, where the name clashes with an existing column. By aliasing tables it allows you to join tables in multiple times. Example a student has a term_address and a home address:

SELECT * FROM
  student s
  INNER JOIN address ahome on s.home_address_id = ahome.id
  INNER JOIN address aterm on s.term_address_id = aterm.id

This way you don't need a table for term addresses and a table for home addresses - the address table stores all addresses and you alias it differently. A different subset of records participates in each join

Anything that represents a block of data can be aliased, be it a table or a subquery, or possibly some other things.

SELECT * FROM
  (subquery here) aliasForSubquery
  INNER JOIN
  (anther subquery) aliasForANotherSubquery
  ON
    aliasForSubquery.column = aliasForAnotherSubquery.column

The only thing you don't need to(can't) alias is the subquery that creates your IN(...) list

Upvotes: 1

forpas
forpas

Reputation: 164164

You can use ctes:

with 
cte1 as (
SELECT id, phone_number
FROM student
WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount))
UNION
SELECT id, phone_number
FROM administrator
WHERE id IN
    (SELECT account_id
    FROM loan
    WHERE id IN
        (SELECT loan_id
        FROM fine
        WHERE paid_amount < fine_amount)
),
cte2 as (
SELECT
    account_id,
    EXTRACT ('day' FROM (NOW()::timestamp - expiry_date::timestamp))
FROM loan
WHERE id IN
    (SELECT loan_id
    FROM fine
    WHERE paid_amount < fine_amount)
)
SELECT * 
FROM cte1 JOIN cte2
ON cte1.id = cte2.account_id;

Upvotes: 1

Related Questions