I've found:
However, I'm having trouble putting it to use in my case.
I have a table like so (the real myid
values are hashes, but simplified here for illustration):
create temp table a (myid text, ip inet);
insert into a (myid, ip)
values
('0a', '10.10.1.1'),
('0a', '10.10.1.2'),
('0a', '10.10.1.3'),
('0b', '10.10.1.2'),
('0b', '10.10.1.4'),
('0c', '10.10.1.5'),
('0d', '10.10.1.3'),
('0e', '10.10.1.6'),
('0e', '10.10.1.7'),
('0f', '10.10.1.8'),
('0f', '10.10.1.9'),
('10', '10.10.1.9'),
('11', '10.10.1.10'),
('12', '10.10.1.11'),
('12', '10.10.1.4'),
('1a', '10.10.1.2'),
('1a', '10.10.1.4'),
('1e', '10.10.1.11'),
('1f', '10.10.1.12'),
('23', '10.10.1.12');
The result I can't work out how to produce is:
ids | ips
---------------------+------------------------------------------------------
{0a,0b,0d,12,1a,1e} | {10.10.1.1,10.10.1.2,10.10.1.3,10.10.1.4,10.10.1.11}
{0c} | {10.10.1.5}
{0e} | {10.10.1.6,10.10.1.7}
{0f,10} | {10.10.1.8,10.10.1.9}
{11} | {10.10.1.10}
{1f,23} | {10.10.1.12}
The logic here is that any ids with ips in common are grouped together, transitively. For instance, 0a
has an ip in common with 0b
; 0b
has one in common with 12
; 12
has one in common with 1e
, and so forth.
There are tens of thousands of rows, no specific limit on how many ips for any given id, and no specific limit on how many ids may show any given ip.
I know how to aggregate by ip, or aggregate by id, but doing both transitively is giving me trouble. I tried a recursive CTE but I couldn't seem to get it right, and I'm not sure if that was the right approach in the first place. (If I could first group by id and then group by overlapping array of ips, and avoid duplicates in the aggregation, I would be all set, but there may be a better approach.)
Is there a way to produce the above results with standard SQL? Or at least with standard Postgres? (I'm using 9.6.6.)
Here is a failed attempt. (It is a legal query that does return results, but not the desired results.) It fails because:
- It includes intermediate results rather than replacing them with later results, and
- It doesn't sort the array concatenations, so it includes each result multiple times. This is also a hugely slow query for the actual dataset I am working with, since it returns up to
n!
times each result.
Here is the query:
with recursive b as (
select
array[myid] as ids,
array_agg(ip) as ips
from a
group by myid
), c as (
select
ids,
ips
from b
union
select
b.ids || c.ids,
b.ips || c.ips
from
b
join c on
(not b.ids && c.ids)
and (b.ips && c.ips)
)
select * from c
;
Best Answer
One of the key parts of Jack Douglas's solution in Group by array overlapping is the
|
(pipe) operator used on arrays in the recursive part of the recursivet
CTE like this:This operator concatenates two arrays suppressing duplicate items. The reason that answer cannot be directly applied to your setup is because apparently the
|
operator is defined forint
arrays only, while you need a way to perform the same operation oninet
arrays.You can do that by treating the arrays as row sets. If you notice, what the
|
operator produces is effectively a union of two sets. Therefore, if youunnest
both arrays,union
them and aggregate the combined set back as an array, you will get the same result. So, this expression,can be replaced with a correlated subquery:
Yes, the substitution is quite unwieldy in comparison, but it does the job, and that is something to start with.
Adapting the solution to your example (and adding a bit of white space to the original code), the complete query would look like this:
You can test the query in this demo at db<>fiddle.uk.
Note also that Jack's word of caution probably applies to your situation as well: