How to create a query which selects products of given features where feature statement is formed by "and" or "or" condition depending on a group they belong to?
Description of the situation
Test environment
http://sqlfiddle.com/#!12/f4db7
"OR" Query
It works except for those product which have no features.
SELECT product_id
FROM product_features
WHERE product_features.feature_id IN (
SELECT feature_id FROM features
LEFT JOIN feature_groups
ON features.feature_group_id = feature_groups.feature_group_id
WHERE feature_id IN (11, 12, 13) AND feature_groups.disjunction = TRUE
)
GROUP BY product_id
"AND" Query
This query cannot be used because the number of features where disjunction is false is not known.
SELECT product_id FROM product_features
WHERE feature_id IN (43, 53, 63)
GROUP BY product_id
HAVING COUNT(DISTINCT feature_id) = 3
Simpler and faster:
SELECT DISTINCT pf.product_id
FROM product_features pf
LEFT JOIN features f USING (feature_id)
LEFT JOIN feature_groups fg USING (feature_group_id)
WHERE (f.feature_id = ANY (_my_arr)
AND fg.disjunction)
OR _my_arr = '{}';
... where _my_arr
can be '{11, 12, 13}'::int[]
or '{}'::int[]
. If _my_arr
would be NULL
use _my_arr IS NULL
instead.
Due to operator precedence AND
binds before OR
and parentheses are not required. They may improve readability, though.
DISTINCT
or GROUP BY
.. either is good here.
AND fg.disjunction
.. since this is a boolean type, you can shorten the syntax.
JOINs are generally faster than another IN
clause.
USING
is just a notational shortcut that works with your (useful!) naming convention.
Or, even faster (for more than 1 feature) - and simpler to split cases:
SELECT product_id
FROM products p
WHERE EXISTS (
SELECT 1
FROM product_features pf
JOIN features f USING (feature_id)
JOIN feature_groups fg USING (feature_group_id)
WHERE pf.product_id = p.product_id
AND f.feature_id = ANY (_my_arr)
AND fg.disjunction
)
OR _my_arr = '{}';
I would rather split the case in your app depending on input (with features / no features). Trivial to do with the second form.
This is a classical case of relational division. We have assembled a whole arsenal of query techniques to deal with it in under this related question:
How to filter SQL results in a has-many-through relation
Could be:
SELECT product_id
FROM product_features p1
JOIN product_features p2 USING (product_id)
JOIN product_features p3 USING (product_id)
...
WHERE p1.feature_id = 43
AND p2.feature_id = 53
AND p3.feature_id = 63
...
I am ignoring NOT feature_groups.disjunction
in the example since it is not in the question either. Add it if you need it.
I would select valid feature_id before building the query.