Ok, after pondering upon @a_vlad and @erwin-brandstetter answers, I came out with my own solution that I leave here open to discussion:
SELECT
label,
array_to_string(array_agg(color),',') AS color,
array_agg(box_number) AS boxes
FROM (SELECT
label,
CASE WHEN box_number=3 THEN color END AS color,
box_number
FROM items
) AS sub1
GROUP BY label
HAVING 3=ANY(array_agg(box_number))
Here's the SQLFiddle : http://sqlfiddle.com/#!15/eb2ac/11/0
As I explained in the comments to the previous answers, the reason why I'm proposing this solution is that it seems much more faster (even from the tests I've done) because it needs only to iterate 2N times instead of NxN times.
I'm still convinced that this answer can be further refined, maybe using array constructors instead of array_agg()
as Erwin suggested, but I cannot work out how. I would be glad to hear what do you think about.
Here's what is happening on the actual database (column names have been replaced as well as other attributes that needs to be selected/joined/sorted) when I run EXPLAIN SELECT
:
Solution 1 (correlated subquery):
Seq Scan on items m (cost=0.00..1552688.65 rows=355 width=21) (actual time=22.883..5813.845 rows=355 loops=1)
" Filter: ((box_number)::text = '3'::text)"
" Rows Removed by Filter: 27123"
" SubPlan 2"
" -> Result (cost=4361.48..4361.49 rows=1 width=0) (actual time=16.312..16.312 rows=1 loops=355)"
" InitPlan 1 (returns $1)"
" -> Seq Scan on items (cost=0.00..4361.48 rows=2 width=4) (actual time=4.958..16.271 rows=18 loops=355)"
" Filter: ((label)::text = (m.label)::text)"
" Rows Removed by Filter: 27460"
Planning time: 0.200 ms
Execution time: 5814.197 ms
Solution 2 (using LATERAL):
Nested Loop Left Join (cost=4361.48..1552695.75 rows=355 width=53) (actual time=31.448..5693.496 rows=355 loops=1)
" -> Seq Scan on items m (cost=0.00..4361.48 rows=355 width=21) (actual time=11.945..24.483 rows=355 loops=1)"
" Filter: ((box_number)::text = '3'::text)"
" Rows Removed by Filter: 27123"
" -> Result (cost=4361.48..4361.49 rows=1 width=0) (actual time=15.962..15.963 rows=1 loops=355)"
" InitPlan 1 (returns $1)"
" -> Seq Scan on items (cost=0.00..4361.48 rows=2 width=4) (actual time=4.837..15.920 rows=18 loops=355)"
" Filter: ((label)::text = ($0)::text)"
" Rows Removed by Filter: 27460"
Planning time: 0.258 ms
Execution time: 5693.929 ms
Solution 3 (using CASE):
GroupAggregate (cost=6318.73..7231.95 rows=15417 width=25) (actual time=881.221..951.466 rows=340 loops=1)
" Group Key: items.label"
" Filter: ('3'::text = ANY ((array_agg(items.box_number))::text[]))"
" Rows Removed by Filter: 15077"
" -> Sort (cost=6318.73..6387.43 rows=27478 width=25) (actual time=881.066..885.931 rows=27478 loops=1)"
" Sort Key: items.label"
" Sort Method: quicksort Memory: 2906kB"
" -> Seq Scan on items (cost=0.00..4292.78 rows=27478 width=25) (actual time=0.012..21.602 rows=27478 loops=1)"
Planning time: 0.696 ms
Execution time: 952.184 ms
I've repeated the tests several times and they are all alike, and when adding complexity to the query the first two solutions takes so long that I need to stop them and the third always returns within seconds. Furthermore the third query is enough quick that I don't need to touch the indexes since the dataset is not supposed to grow very much.
EDIT:
After checking the self-join solution, it seems to be the faster one when used in a simply case and with only one box_number
. When this has to be performed in a more complex case, for example against several box_number
s, it still takes an unacceptable lapse of time compared to the CASE solution
This are the results of the EXPLAIN ANALYZE for the same (logical) query as the other three.
HashAggregate (cost=9637.72..9640.22 rows=200 width=25) (actual time=63.424..64.045 rows=340 loops=1)
" Group Key: i.label, i.color"
" -> Hash Join (cost=4636.26..9588.61 rows=6548 width=25) (actual time=41.224..57.156 rows=6494 loops=1)"
" Hash Cond: ((i.label)::text = (b.label)::text)"
" -> Seq Scan on items i (cost=0.00..4361.48 rows=355 width=21) (actual time=6.033..18.434 rows=355 loops=1)"
" Filter: ((box_number)::text = '3'::text)"
" Rows Removed by Filter: 27123"
" -> Hash (cost=4292.78..4292.78 rows=27478 width=18) (actual time=35.145..35.145 rows=27478 loops=1)"
" Buckets: 4096 Batches: 1 Memory Usage: 1363kB"
" -> Seq Scan on items b (cost=0.00..4292.78 rows=27478 width=18) (actual time=0.007..19.538 rows=27478 loops=1)"
Planning time: 2.236 ms
Execution time: 64.579 ms
box_number
s andcolor
s in arrays, but what I need is to aggregatebox_number
s discarding allcolor
s in those rows where thebox_number
doesn't have a match with a given value (3
in this case). I was thinking about a solution that iterates only once per row, set the color to NULL if it box_number doesn't match3
, put bothbox_number
s andcolor
s values in arrays so that a simple function likecoalesce
returns the non NULLcolor
from the resulting array of colors.