JSONB subset of array - sql

I have the following JSONB field on my posts table: comments.
comments looks like this:
[
{"id": 1, "text": "My comment"},
{"id": 2, "text": "My other comment"},
]
I would like to select some information about each comments.
SELECT comments->?????? FROM posts WHERE posts.id = 1;
Is there a way for me to select only the id fields of my JSON. Eg. the result of my SQL query should be:
[{"id": 1}, {"id": 2}]
Thanks!

You can use jsonb_to_recordset to split each comment into its own row. Only columns you specify will end up in the row, so you can use this to keep only the id column. Then you can aggregate the comments for one post into an array using json_agg:
select json_agg(c)
from posts p
cross join lateral
jsonb_to_recordset(comments) c(id int) -- Only keep id
where p.id = 1
This results in:
[{"id":1},{"id":2}]
Example at db-fiddle.com

You may use json_build_object + json_agg
select json_agg(json_build_object('id',j->>'id'))
from posts cross join jsonb_array_elements(comments) as j
where (j->>'id')::int = 1;
DEMO

Related

Write SQL query so all question choices are in the same JSON object as the answer

I have one table with Questions in it, and the other table with choices to those questions.
If I have 5 questions, and every question has 5 choices, is there a way to write a query so I get one question and all its choices in one JSON object, so I would get 5 objects, and not 25 as I am getting now.
So if I have in table "Questions" a question '5+5=?', with ID=1 and in table "Choices", 5 choices: 7, 8, 9, 10, 11 with QUESTION_ID=1. I would like to export it as:
{"question":"5+5=?", "choice":"1", "choice":"2"...}
and not {"question":"5+5=?", "choice":"1"}, {"question":"5+5=?", "choice":"2"}.
Also {"question":"5+5=?", "choices":{"choice":"1", "choice":"2"...}} would be great.
I have tried this query but it only separates choices by comma:
SELECT q.question,
string_agg(choice, ', ') as answer
FROM question q
join question_choice qc on q.id = qc.question_id
GROUP BY 1;
{"question":"5+5=?", "choice":"1", "choice":"2"...} and {"choice":"1", "choice":"2"...} are not allowed in the json type because the same key cannot appear several times in the same json object.
Instead you can get the following result {"question":"5+5=?", "choices": ["1", "2", ...]} where the list of choices is a json array with the following query :
SELECT jsonb_build_object('question', q.question, 'choices', array_agg(qc.choice))
FROM question q
INNER JOIN question_choice qc
ON q.id = qc.question_id
GROUP BY q.question ;
or you can get the following result {"question":"5+5=?", "choices": [{"choice": "1"}, {"choice": "2"}, ...]} where the list of choices is a json array with the following query :
SELECT jsonb_build_object('question', q.question, 'choices', array_agg(jsonb_build_object('choice', qc.choice)))
FROM question q
INNER JOIN question_choice qc
ON q.id = qc.question_id
GROUP BY q.question ;

Select items from jsonb array in postgres 12

I'm trying to pull elements from JSONB column.
I have table like:
id NUMBER
data JSONB
data structure is:
[{
"id": "abcd",
"validTo": "timestamp"
}, ...]
I'm querying that row with SELECT * FROM testtable WHERE data #> '[{"id": "abcd"}]', and it almost works like I want to.
The trouble is data column is huge, like 100k records, so I would like to pull only data elements I'm looking for.
For example if I would query for
SELECT * FROM testtable WHERE data #> '[{"id": "abcd"}]' OR data #> '[{"id": "abcde"}]' I expect data column to contain only records with id abcd or abcde. Like that:
[
{"id": "abcd"},
{"id": "abcde"}
]
It would be okay if query would return separate entries with single data record.
I have no ideas how to solve it, trying lot options since days.
To have separate output for records having multiple matches
with a (id, data) as (
values
(1, '[{"id": "abcd", "validTo": 2}, {"id": "abcde", "validTo": 4}]'::jsonb),
(2, '[{"id": "abcd", "validTo": 3}, {"id": "abc", "validTo": 6}]'::jsonb),
(3, '[{"id": "abc", "validTo": 5}]'::jsonb)
)
select id, jsonb_array_elements(jsonb_path_query_array(data, '$[*] ? (#.id=="abcd" || #.id=="abcde")'))
from a;
You will need to unnest, filter and aggregate back:
select t.id, j.*
from testtable t
join lateral (
select jsonb_agg(e.x) as data
from jsonb_array_elements(t.data) as e(x)
where e.x #> '{"id": "abcd"}'
or e.x #> '{"id": "abcde"}'
) as j on true
Online example
With Postgres 12 you could use jsonb_path_query_array() as an alternative, but that would require to repeat the conditions:
select t.id,
jsonb_path_query_array(data, '$[*] ? (#.id == "abcd" || #.id == "abcde")')
from testtable t
where t.data #> '[{"id": "abcd"}]'
or t.data #> '[{"id": "abcde"}]'
Didn't quite get your question.Are you asking that the answer should only contain data column without id column .Then I think this is the query:
Select data from testtable where id="abcd" or id="abcde";

Extracting data from an array of JSON objects for specific object values

In my table, there is a column of JSON type which contains an array of objects describing time offsets:
[
{
"type": "start"
"time": 1.234
},
{
"type": "end"
"time": 50.403
}
]
I know that I can extract these with JSON_EACH() and JSON_EXTRACT():
CREATE TEMPORARY TABLE Items(
id INTEGER PRIMARY KEY,
timings JSON
);
INSERT INTO Items(timings) VALUES
('[{"type": "start", "time": 12.345}, {"type": "end", "time": 67.891}]'),
('[{"type": "start", "time": 24.56}, {"type": "end", "time": 78.901}]');
SELECT
JSON_EXTRACT(Timings.value, '$.type'),
JSON_EXTRACT(Timings.value, '$.time')
FROM
Items,
JSON_EACH(timings) AS Timings;
This returns a table like:
start 12.345
end 67.891
start 24.56
end 78.901
What I really need though is to:
Find the timings of specific types. (Find the first object in the array that matches a condition.)
Take this data and select it as a column with the rest of the table.
In other words, I'm looking for a table that looks like this:
id start end
-----------------------------
0 12.345 67.891
1 24.56 78.901
I'm hoping for some sort of query like this:
SELECT
id,
JSON_EXTRACT(timings, '$.[type="start"].time'),
JSON_EXTRACT(timings, '$.[type="end"].time')
FROM Items;
Is there some way to use path in the JSON functions to select what I need? Or, some other way to pivot what I have in the first example to apply to the table?
One possibility:
WITH cte(id, json) AS
(SELECT Items.id
, json_group_object(json_extract(j.value, '$.type'), json_extract(j.value, '$.time'))
FROM Items
JOIN json_each(timings) AS j ON json_extract(j.value, '$.type') IN ('start', 'end')
GROUP BY Items.id)
SELECT id
, json_extract(json, '$.start') AS start
, json_extract(json, '$.end') AS "end"
FROM cte
ORDER BY id;
which gives
id start end
---------- ---------- ----------
1 12.345 67.891
2 24.56 78.901
Another one, that uses the window functions added in sqlite 3.25 and avoids creating intermediate JSON objects:
SELECT DISTINCT Items.id
, max(json_extract(j.value, '$.time'))
FILTER (WHERE json_extract(j.value, '$.type') = 'start') OVER ids AS start
, max(json_extract(j.value, '$.time'))
FILTER (WHERE json_extract(j.value, '$.type') = 'end') OVER ids AS "end"
FROM Items
JOIN json_each(timings) AS j ON json_extract(j.value, '$.type') IN ('start', 'end')
WINDOW ids AS (PARTITION BY Items.id)
ORDER BY Items.id;
The key is using the ON clause of the JOIN to limit results to just the two objects in each array that you care about, and then merging those up to two rows for each Items.id into one with a couple of different approaches.

postgreSQL query empty array fields within jsonb column

device_id | device
-----------------------------
9809 | { "name" : "printer", "tags" : [] }
9810 | { "name" : "phone", "tags" : [{"count": 2, "price" : 77}, {"count": 3, "price" : 37} ] }
For the following postgres SQL query on a jsonb column "device" that contains array 'tags':
SELECT t.device_id, elem->>'count', elem->>'price'
FROM tbl t, json_array_elements(t.device->'tags') elem
where t.device_id = 9809
device_id is the primary key.
I have two issues that I don't know how to solve:
tags is an array field that may be empty, in which case I got 0 rows. I want output no matter tags is empty or not. Dummy values are ok.
If tags contain multiple elements, I got multiple rows for the same device id. How to aggregate those multiple elements into one row?
Your first problem can be solved by using a left outer join, that will substitute NULL values for missing matches on the right side.
The second problem can be solved with an aggregate function like json_agg, array_agg or string_agg, depending on the desired result type:
SELECT t.device_id,
jsonb_agg(elem->>'count'),
jsonb_agg(elem->>'price')
FROM tbl t
LEFT JOIN LATERAL jsonb_array_elements(t.device->'tags') elem
ON TRUE
GROUP BY t.device_id;
You will get a JSON array containing just null for those rows where the array is empty, I hope that is ok for you.

SQL: Select * where attribute matches all items in array

Working in a Rails App, I have the following table structure (pertinent columns only)
Photos (id: integer)
Taggings (photo_id: integer, tag_id: integer)
Tags (id: integer, name:string)
I have the following SQL query:
SELECT distinct photos.*
FROM \"photos\" INNER JOIN \"taggings\" ON \"photos\".\"id\" = \"taggings\".\"photo_id\"
INNER JOIN \"tags\" ON \"tags\".\"id\" = \"taggings\".\"tag_id\"
WHERE \"tags\".\"name\" IN ('foo', 'bar')
When I generate this query I'm passing in an array of tags (in this case ["foo","bar"]). The query correctly searches for photos that match ANY of the tags passed in the array.
How can I change this query to select records with ALL of the given tags (ie a photo only matches if tagged with "foo" AND "bar", instead of selecting records with ANY of the given tags?
There may be a better way, but this should do it
SELECT photos.id,max(otherColumn)
FROM \"photos\"
INNER JOIN \"taggings\"
ON \"photos\".\"id\" = \"taggings\".\"photo_id\"
INNER JOIN \"tags\"
ON \"tags\".\"id\" = \"taggings\".\"tag_id\"
WHERE \"tags\".\"name\" IN ('foo', 'bar')
group by photos.id
having count(*) = 2 --2 is the number of items in your array of tags.
If you are in rails you don't need query to do this.
Tags.find(1).taggings should give you an array of all photos with that tag
you can also use Tags.find_by_name("foo").taggings
you can similarly iterate over all tags, and collect the arrays and then just do something like on the arrays you have got.
[ 1, 1, 3, 5 ] & [ 1, 2, 3 ] #=> [ 1, 3 ]
Basically 'and' the arrays and get the unique photos.This way you can get the photos that match all the tags.