How to execute multiple joins with different params in a single query

Viewed 101

I have two tables, question(question_id) and question_exclusion(question_type, question_sub_type, question_id)

I'm able to do it If I specify question_type and question_sub_type.

SELECT *
FROM question AS t1
         LEFT JOIN (SELECT t.question_id
                    FROM question_exclusion as t
                    WHERE t.question_type = 'A'
                      AND t.question_sub_type = 'A_1') AS t2
                   ON t1.question_id = t2.question_id
WHERE t2.question_id is null;

But what I want to achieve is to get all questions with questions_ids in a single query for all possible question_type and questions_sub_type

question_type and questions_sub_type are dynamic parameters I don't know exact values until query execution

Update 1:

actual data:

table: question

question_id|
42
10
2
36
49

table: question_exclusion

question_type|question_sub_type|question_id|
A            | A_1             | 42
A            | A_1             | 10
A            | A_2             | 10
B            | B_1             | 36 
C            | null            | 2

expected result:

question_type|question_sub_type|question_id
A            | A_1             | 2
A            | A_1             | 36
A            | A_1             | 49
A            | A_2             | 42
A            | A_2             | 2
A            | A_2             | 36
A            | A_2             | 49
B            | B_1             | 42
B            | B_1             | 10
B            | B_1             | 2
B            | B_1             | 49
C            | null            | 42
C            | null            | 10
C            | null            | 36
C            | null            | 49

its like a list of lists for each type and sub_type combination taking into consideration exclusion table

For example:

type=A, sub_type=A_1 -> (select * from questions) - (select * from question_exclusion where type='A' and sub_type='A_1')
+
type=A, sub_type=A_2 -> (select * from questions) - (select * from question_exclusion where type='A' and sub_type='A_2')
+
type=B, sub_type=B_1 -> (select * from questions) - (select * from question_exclusion where type='B' and sub_type='B_2')

Of course I can query all distinct (type, sub_type) and make another query by combining with union

SELECT *
FROM question AS t1
         LEFT JOIN (SELECT t.question_id
                    FROM question_exclusion as t
                    WHERE t.question_type = 'A'
                      AND t.question_sub_type = 'A_1') AS t2
                   ON t1.question_id = t2.question_id
WHERE t2.question_id is null
UNION
SELECT *
FROM question AS t1
         LEFT JOIN (SELECT t.question_id
                    FROM question_exclusion as t
                    WHERE t.question_type = 'B'
                      AND t.question_sub_type = 'B_1') AS t2
                   ON t1.question_id = t2.question_id
WHERE t2.question_id is null
...
...
N times for all type and sub_type

I'm looking for another solid way of doing this in a single query

4 Answers
\i tmp.sql

create table question
        (question_id integer not null primary key)
        ;
INSERT INTO question(question_id) VALUES
( 42) , ( 10) , ( 2) , ( 36) , ( 49) ;

create table question_exclusion
        ( question_type text
        , question_sub_type text
        , question_id integer REFERENCES question( question_id)
        );

INSERT INTO question_exclusion(question_type, question_sub_type, question_id) VALUES
 ('A' , 'A_1' , 42 ) , ('A' , 'A_2' , 10 ) , ('B' , 'B_1' , 36  ) , ('C' , null  , 2 ) ;

WITH types AS (
        select distinct question_type, question_sub_type
        FROM question_exclusion
        )
SELECT t.question_type, t.question_sub_type, q.question_id
FROM question q
JOIN types t ON NOT EXISTS (
        SELECT * FROM question_exclusion x
        WHERE 1=1
        AND x.question_id = q.question_id
        AND x.question_type = t.question_type
        AND x.question_sub_type = t.question_sub_type
        )
ORDER BY t.question_type, t.question_sub_type
        ;

Modified:


WITH types AS (
        select distinct question_type, question_sub_type
        FROM question_exclusion
        )
SELECT t.question_type, t.question_sub_type, q.question_id
FROM question q
CROSS JOIN types t
WHERE NOT EXISTS (
        SELECT * FROM question_exclusion x
        WHERE 1=1
        AND x.question_id = q.question_id
        AND x.question_type = t.question_type
        AND x.question_sub_type = t.question_sub_type
        )
ORDER BY t.question_type, t.question_sub_type
        ;

IS NOT DISTINCT FROM


WITH types AS (
        select distinct question_type, question_sub_type
        FROM question_exclusion
        )
SELECT t.question_type, t.question_sub_type, q.question_id
FROM question q
CROSS JOIN types t
WHERE NOT EXISTS (
        SELECT * FROM question_exclusion x
        WHERE 1=1
        AND x.question_id = q.question_id
        AND (x.question_type, x.question_sub_type) IS NOT DISTINCT FROM
            (t.question_type, t.question_sub_type)
        )
ORDER BY t.question_type, t.question_sub_type
        ;

Here's an example query that accomplishes what you describe:

SELECT DISTINCT x.question_type, x.question_sub_type, q.question_id
FROM question_exclusion x
CROSS APPLY question q 
WHERE NOT EXISTS (
    SELECT 1
    FROM question_exclusion
    WHERE question_type = x.question_type
        AND (question_sub_type = x.question_sub_type OR (question_sub_type IS NULL AND x.question_sub_type IS NULL))
        AND question_id = q.question_id)
ORDER BY question_type, question_sub_type

This uses a CROSS APPLY to create the cartesian product of both tables, a NOT EXISTS subquery to filter out the excluded rows, and a DISTINCT to remove duplicates from the cartesian product (as there are multiple records in question_exclusion with the same question_type and question_sub_type).

Instead of a subquery, this could also be done with a LEFT JOIN as follows:

SELECT DISTINCT a.question_type, a.question_sub_type, a.question_id
FROM (
    SELECT x.question_type, x.question_sub_type, q.question_id
    FROM question_exclusion x
        CROSS APPLY question q
    ) a
    LEFT JOIN question_exclusion r
        ON a.question_id = r.question_id
            AND a.question_type = r.question_type
            AND (a.question_sub_type = r.question_sub_type OR (a.question_sub_type IS NULL AND r.question_sub_type IS NULL))
WHERE r.question_id IS NULL
ORDER BY question_type, question_sub_type

I add row (A | A_1 | 10) into question_exclusion in your data for more clean solution:

with question(question_id) as (
    select 42 union all
    select 10 union all
    select 2 union all
    select 36 union all
    select 49
),
question_exclusion(question_type, question_sub_type, question_id) as (
    select 'A', 'A_1', 42 union all
    select 'A', 'A_1', 10 union all
    select 'A', 'A_2', 10 union all
    select 'B', 'B_1', 36 union all 
    select 'C', null, 2
),
question_types(question_type, question_sub_type) as (
    select distinct question_type, question_sub_type from question_exclusion
)
select
    qt.question_type, qt.question_sub_type, q.question_id
from
    question q
    left join question_types qt on (1 = 1)
    left join question_exclusion qe on (q.question_id = qe.question_id and qt.question_type = qe.question_type and coalesce(qt.question_sub_type, '_') = coalesce(qe.question_sub_type, '_'))
where
    qe.question_id is null
order by 
    qt.question_type, qt.question_sub_type
with
    -- questions
    q(id) as (select * from unnest('{42,10,2,36,49}'::int[])),
    -- excluded questions
    qe(tp, stp, id) as (select * from unnest(
        '{A,A,A,B,C}'::text[],
        '{A_1,A_1,A_2,B_1,NULL}'::text[],
        '{42,10,10,36,2}'::int[])),
    -- combinations of types and subtypes
    qt(tp, stp) as (select distinct tp, stp from qe)
select qt.tp, qt.stp, q.id from q cross join qt
except
select tp, stp, id from qe
order by 1, 2, 3;

┌────┬──────┬────┐
│ tp │ stp  │ id │
├────┼──────┼────┤
│ A  │ A_1  │  2 │
│ A  │ A_1  │ 36 │
│ A  │ A_1  │ 49 │
│ A  │ A_2  │  2 │
│ A  │ A_2  │ 36 │
│ A  │ A_2  │ 42 │
│ A  │ A_2  │ 49 │
│ B  │ B_1  │  2 │
│ B  │ B_1  │ 10 │
│ B  │ B_1  │ 42 │
│ B  │ B_1  │ 49 │
│ C  │ ░░░░ │ 10 │
│ C  │ ░░░░ │ 36 │
│ C  │ ░░░░ │ 42 │
│ C  │ ░░░░ │ 49 │
└────┴──────┴────┘

demo

Related