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

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"}]

(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:
Return multiple columns of the same row as JSON array of objects

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) )

Why not try a Json_Agg()?
SELECT
json_agg(tag_ids, tag_names, tag_colors)
FROM items
Etc...

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;

Related

How to use alias column name in oracle query

I have a two table one is Mn_Fdd_Cong and another is test_t .Here i have to split a column value in table Mn_Fdd_Cong based on character underscore and take first part and join with second table. But in my query alias is not working
select regexp_substr(a.Ids, '[^_]+', 1, 1) as "Id" ,a.*,b.*
from Mn_Fdd_Cong a
left join test_t b on a.Id = b.Site_Id where b.G_VAL='U'
You can't use it that way; either extract "new" ID in a subquery (or a CTE) and then re-use it in "main" query, or - simpler - use it directly in JOIN operation.
I used simple substr + instr combination instead of regular expressions (performs better on large data sets).
select a.*, b.*
from md_fdd_cong a join test_t b on b.site_id = substr(a.ids, 1, instr(a.ids, '_') - 1)
where b.g_val = 'U'
Here's the 1st option I suggested (a CTE):
select x.*, b.*
from (select a.*,
substr(a.ids, 1, instr(a.ids, '_') - 1) id --> this is ID ...
from md_fdd_cong a
) x
join test_t b on b.site_id = x.id --> ... which is then used here (as "x.id")
where b.g_val = 'U';
You can't use an alias you define in your select in the join (logically, the join happens before the select). You could duplicate the calculation
select regexp_substr(a.Ids, '[^_]+', 1, 1) as "Id" ,a.*,b.*
from Mn_Fdd_Cong a
left join test_t b on regexp_substr(a.Ids, '[^_]+', 1, 1) = b.Site_Id
where b.G_VAL='U'
Or you could nest the calculation. Personally, I'd put the mn_fdd_cong manipulation into a common table expression (CTE). Note that if you put an identifier like Id in double-quotes, you're creating a case-sensitive identifier so subsequent references to it would need to use the same case-sensitive identifier and would need to use the double quotes.
with new_a as (
select regexp_substr(a.Ids, '[^_]+', 1, 1) as "Id" ,a.*
from Mn_Fdd_Cong a
)
select a.*, b.*
from new_a a
join test_t b on a."Id" = b.Site_Id
where b.G_VAL='U'

Sequelize.js postgres LATERAL usage

I have a postgres table with JSONB field.
json contains array of objects
| id | my_json_field |
-------------------------------------------------------
| 1234 | [{"id": 1, type: "c"}, {"id": 2, type: "v"}] |
| 1235 | [{"id": 1, type: "e"}, {"id": 2, type: "d"}] |
I need to sort/filter table by type key of json field.
Server accept id, so if id=1 - I need to sort by "c","e", if id=2 - by "v","d"
I have next SQL:
LEFT JOIN LATERAL (
SELECT elem ->> 'type' AS my_value
FROM jsonb_array_elements(my_json_field) a(elem)
WHERE elem ->> 'id' = '1'
) a ON true
this will add my_value field to the results and I can use it to sort/filter the table
This works fine in console, but I didn't find a way to add this using Sequelize.js
Also I'm open for any other solutions, thanks!
Edit, full query:
SELECT my_value FROM "main_table" AS "main_table"
LEFT OUTER JOIN ( "table2" AS "table2"
LEFT OUTER JOIN "form_table" AS "table2->form_table" ON "table2"."id" = "table2->form_table"."table2_id")
ON "main_table"."id" = "table2"."main_table_id"
LEFT JOIN LATERAL (
SELECT elem ->> 'type' AS my_value
FROM jsonb_array_elements("table2->form_table".structure) a(elem)
WHERE elem ->> 'id' = '1'
) a ON TRUE
ORDER BY "my_value" DESC;
You don't really need the keyword LATERAL as that is implied if you use a set returning function directly and not in a sub-select.
The following should do the same thing as your query and doesn't need the LATERAL keyword:
SELECT a.elem ->> 'type' as my_value
FROM "main_table"
LEFT JOIN "table2" ON "main_table"."id" = "table2"."main_table_id"
LEFT JOIN "form_table" AS "table2->form_table" ON "table2"."id" = "table2->form_table"."table2_id")
LEFT JOIN jsonb_array_elements("table2->form_table".structure) a(elem) on a.elem ->> 'id' = '1'
ORDER BY my_value DESC;
I also removed the useless parentheses around the outer joins and the aliases that don't give the table a new name to simplify the syntax.
Maybe that allows you to use the query with your ORM (aka "obfuscation layer")

How to find the key for the minimum value in jsonb column of postgres?

I need to find the key of the minimum value in a jsonb object,I have found out minimum value, need to find the key of the same in the same query.
Query I am using
SELECT id,min((arr ->> 2)::numeric) AS custom_value
FROM (
SELECT id, jdoc
FROM table,
jsonb_each(column1) d (key, jdoc)
) sub,
jsonb_each(jdoc) doc (key, arr)
group by 1
This will do the job.
The left join ... on 1=1 is for keeping IDs with empty json
select t.id
,j.key
,j.value
from mytable t
left join lateral (select j.key,j.value
from jsonb_each(column1) as j
order by j.value
limit 1
) j
on 1=1

Find related tags to group of tags

I am working with a system where users can attach tags to messages for easier searching/identification (just like here on SO). This is the simplified schema:
message: message_id
tag: tag_id, tag_name
tag_message: tag_id (FK), message_id (FK)
The problem I'm facing is as follows:
Given an input list of tag_id's I want to find what other tags appear in messages tagged with the inputed tags
This is the query I came up with:
SELECT
tag2.tag_name,
COUNT(*) AS tagged_message_count
FROM tag AS tag1
LEFT JOIN tag_message ON tag_message.tag_id = tag1.tag_id
LEFT JOIN message ON message.message_id = tag_message.message_id
LEFT JOIN tag_message AS tag_message2 ON tag_message2.message_id = message.message_id
LEFT JOIN tag AS tag2 ON tag_message2.tag_id = tag2.tag_id
WHERE
tag1.tag_id = ?
AND
tag1.tag_id <> tag2.tag_id
GROUP BY
tag2.tag_id;
It works BUT it works only for 1 tag and I need it to work with groups of tags.
Given tag IDs 1,2,3 we should first find all messages that are tagged with these three tags, then look at what other tags they have and return them.
I have a feeling there will have to be additional joins for each tag, but
I am not sure how to modify the query to acommodate it.
You can find messages tagged with 1, 2, and 3 using:
select tm.message_id
from tag_message tm
where tm.tag_id in (1, 2, 3)
group by tm.message_id
having count(*) = 3;
You can find other tags using:
select tag_id, count(*)
from tag_message
where message_id in (select tm.message_id
from tag_message tm
where tm.tag_id in (1, 2, 3)
group by tm.message_id
having count(*) = 3
) and
tag_id not in (1, 2, 3)
group by tag_id
order by count(*) desc;
If you want messages that have tags 1, 2, or 3, then remove the having clause.
This may look overly complex, but it at least is an alternative solution ...
-- For convience: put the arguments to the query into a CTE
-- (a temp table *could* be faster for larger sets)
-- -------------------------------------------
WITH args(tagid) AS (
VALUES (2), (3) , (4)
)
-- messages that have ALL the tags
-- [the double NOT EXISTS
-- is a standard relational-division solution]
, msg_with_all_args AS (
SELECT msgid FROM msg m
WHERE NOT EXISTS (
SELECT * FROM tag t
WHERE EXISTS (SELECT * FROM args a WHERE a.tagid = t.tagid )
AND NOT EXISTS (
SELECT * FROM msg_tag mt
WHERE mt.msgid = m.msgid
AND mt.tagid = t.tagid
)
)
)
-- additional tags, associated with
-- messages with all tags
, more_tags AS (
SELECT * FROM tag t
WHERE EXISTS (
SELECT *
FROM msg_tag mt
JOIN msg_with_all_args at ON at.msgid = mt.msgid
AND mt.tagid = t.tagid
)
-- exclude the tags that are already in the set
AND NOT EXISTS ( SELECT * FROM args nx where nx.tagid = t.tagid)
)
SELECT * FROM more_tags
;

How to create a subset query in sql?

I have two tables as follows:
CREATE List (
id INTEGER,
type INTEGER REFERENCES Types(id),
data TEXT,
PRIMARY_KEY(id, type)
);
CREATE Types (
id INTEGER PRIMARY KEY,
name TEXT
);
Now I want to create a query that determines all ids of List which has given type strings.
For example,
List:
1 0 "Some text"
1 1 "Moar text"
2 0 "Foo"
3 1 "Bar"
3 2 "BarBaz"
4 0 "Baz"
4 1 "FooBar"
4 2 "FooBarBaz"
Types:
0 "Key1"
1 "Key2"
2 "Key3"
Given the input "Key1", "Key2", the query should return 1, 4.
Given the input "Key2", "Key3", the query should return 3, 4.
Given the input "Key2", the query should return 1, 3, 4.
Thanks!
select distinct l.id
from list l
inner join types t on t.id = l.type
where t.name in ('key1', 'key2')
group by l.id
having count(distinct t.id) = 2
You have to adjust the having clause to the number of keys you are putting in your where clause. Example for just one key:
select distinct l.id
from list l
inner join types t on t.id = l.type
where t.name in ('key2')
group by l.id
having count(distinct t.id) = 1
SQlFiddle example
You can use the following trick to extend Jurgen's idea:
with keys as (
select distinct t.id
from types t
where t.name in ('key1', 'key2')
)
select l.id
from list l join
keys k
on l.type = keys.id cross join
(select count(*) as keycnt from keys) k
group by l.id
having count(t.id) = max(k.keycnt)
That is, calculate the matching keys in a subquery, and then use this for the counts. This way, you only have to change one line to put in key values, and you can have as many keys as you would like. (Just as a note, I haven't tested this SQL so I apologize for any syntax errors.)
If you can dynamically produce the SQL, this may be one of the most efficent ways, in many DBMS:
SELECT l.id
FROM List l
JOIN Types t1 ON t1.id = l.type
JOIN Types t2 ON t2.id = l.type
WHERE t1.name = 'Key1'
AND t2.name = 'Key2' ;
See this similar question, with more than 10 ways to get the same result, plus some benchmarks (for Postgres): How to filter SQL results in a has-many-through relation