'How to group multiple columns into a single array or similar?

I would like my query to return a result structured like this, where tags is an array of arrays or similar:

id | name | tags
1    a      [[1, "name1", "color1"], [2, "name2", color2"]]
2    b      [[1, "name1", "color1"), (3, "name3", color3"]]

I expected this query to work, but it gives me an error:

SELECT  i.id, i.name, array_agg(t.tag_ids, t.tag_names, t.tag_colors) as tags 
FROM    ITEMS
LEFT OUTER JOIN (
  SELECT      trm.target_record_id
            , array_agg(tag_id) as tag_ids
            , array_agg(t.tag_name) as tag_names
            , array_agg(t.tag_color) as tag_colors
  FROM        tags_record_maps trm
  INNER JOIN  tags t on t.id = trm.tag_id
  GROUP BY    trm.target_record_id
) t on t.target_record_id = i.id;

Error:

PG::UndefinedFunction: ERROR:  function array_agg(integer[], character varying[], character varying[]) does not exist
LINE 1: ..., action_c2, action_c3, action_name, action_desc, array_agg(...
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.

This query works and produces similar results (but not quite what I want):

SELECT  i.id, i.name, t.tag_ids, t.tag_names, t.tag_colors as tags  as tags 
FROM    ITEMS
LEFT OUTER JOIN (
  SELECT      trm.target_record_id, array_agg(tag_id) as tag_ids, array_agg(t.tag_name) as tag_names, array_agg(t.tag_color) as tag_colors
  FROM        tags_record_maps trm
  INNER JOIN  tags t on t.id = trm.tag_id
  GROUP BY    trm.target_record_id
) t on t.target_record_id = i.id;

Result:

id | name | tag_ids | tag_names         | tag_colors          
1    a      [1, 2]    ["name1, "name2"]   ["color1", "color2"]
1    a      [1, 3]    ["name1, "name3"]   ["color1", "color3"]

Edit:

This query almost produces what I'm looking for, except it names the json keys f1, f2, f3. It would be perfect if I could name them id, name, color:

  SELECT        trm.target_record_id, json_agg( (t.id, t.tag_name, t.tag_color) )
  FROM          tags_record_maps trm
  INNER JOIN    tags t on t.site_id = trm.site_id and t.id = trm.tag_id
  GROUP BY      trm.target_record_id
  having count(*) > 1;

Result:

[{"f1":1,"f2":"name1","f3":"color1"},{"f1":2,"f2":"name2","f3":"color2"}]


Solution 1:[1]

(t.id, t.tag_name, t.tag_color) is short syntax for ROW(t.id, t.tag_name, t.tag_color) - and a ROW constructor does not preserve nested attribute names. The manual:

By default, the value created by a ROW expression is of an anonymous record type. If necessary, it can be cast to a named composite type — either the row type of a table, or a composite type created with CREATE TYPE AS.

Bold emphasis mine. To also get proper key names in the result, cast to a registered composite type as advised in the quote, use a nested subselect, or simply use json_build_object() in Postgres 9.4 or newer (effectively avoiding the ROW constructor a priori):

SELECT trm.target_record_id
     , json_agg(json_build_object('id', t.id
                                , 'tag_name', t.tag_name
                                , 'tag_color', t.tag_color)) AS tags
FROM   tags_record_maps trm
JOIN   tags             t USING (site_id)
WHERE  t.id = trm.tag_id
GROUP  BY trm.target_record_id
HAVING count(*) > 1;

I use original column names, but you can chose your key names freely. In your case:

       json_agg(json_build_object('id', t.id
                                , 'name', t.tag_name
                                , 'color', t.tag_color)) AS tags

Detailed explanation:

Solution 2:[2]

array_agg() puts one argument into an array. You could try to concatenate the values together:

array_agg(t.tag_ids || ':' || t.tag_names || ':' || t.tag_colors)

Or perhaps use a row constructor:

array_agg( (t.tag_ids, t.tag_names, t.tag_colors) )

Solution 3:[3]

Why not try a Json_Agg()?

SELECT 
     json_agg(tag_ids, tag_names, tag_colors)
FROM items

Etc...

Solution 4:[4]

DB fiddle
let's play with composite type.
create type tags as(tag_id bigint, tag_name text,tag_color text);

using array_agg:

select item_id,name,  array_agg(row(trm.tag_id, tag_name, tag_color)::tags) as tags
from items i join tags_record_maps trm on i.item_id = trm.target_record_id
group by 1,2;

to json.

select item_id,name, to_json( array_agg(row(trm.tag_id, tag_name, tag_color)::tags)) as tags
from items i join tags_record_maps trm on i.item_id = trm.target_record_id
group by 1,2;

access individual/base element of composite type:

with a as(
select item_id,name,  array_agg(row(trm.tag_id, tag_name, tag_color)::tags) as tags
from items i join tags_record_maps trm on i.item_id = trm.target_record_id
group by 1,2)
select a.item_id, a.tags[2].tag_id from a;

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Gordon Linoff
Solution 3 dwir182
Solution 4 Mark