How to "zip" multiple nested JSON arrays without using id key? - sql

I'm trying to merge some nested JSON arrays without looking at the id. Currently I'm getting this when I make a GET request to /surveyresponses:
{
"surveys": [
{
"id": 1,
"name": "survey 1",
"isGuest": true,
"house_id": 1
},
{
"id": 2,
"name": "survey 2",
"isGuest": false,
"house_id": 1
},
{
"id": 3,
"name": "survey 3",
"isGuest": true,
"house_id": 2
}
],
"responses": [
{
"question": "what is this anyways?",
"answer": "test 1"
},
{
"question": "why?",
"answer": "test 2"
},
{
"question": "testy?",
"answer": "test 3"
}
]
}
But I would like to get it where each survey has its own question and answers so something like this:
{
"surveys": [
{
"id": 1,
"name": "survey 1",
"isGuest": true,
"house_id": 1
"question": "what is this anyways?",
"answer": "test 1"
}
]
}
Because I'm not going to a specific id I'm not sure how to make the relationship work. This is the current query I have that's producing those results.
export function getSurveyResponse(id: number): QueryBuilder {
return db('surveys')
.join('questions', 'questions.survey_id', '=', 'surveys.id')
.join('questionAnswers', 'questionAnswers.question_id', '=', 'questions.id')
.select('surveys.name', 'questions.question', 'questions.question', 'questionAnswers.answer')
.where({ survey_id: id, question_id: id })
}

Assuming jsonb in current Postgres 10 or 11, this query does the job:
SELECT t.data, to_jsonb(s) AS new_data
FROM t
LEFT JOIN LATERAL (
SELECT jsonb_agg(s || r) AS surveys
FROM (
SELECT jsonb_array_elements(t.data->'surveys') s
, jsonb_array_elements(t.data->'responses') r
) sub
) s ON true;
db<>fiddle here
I unnest both nested JSON arrays in parallel to get the desired behavior of "zipping" both directly. The number of elements in both nested JSON arrays has to match or you need to do more (else you lose data).
This builds on implementation details of how Postgres deals with multiple set-returning functions in a SELECT list to make it short and fast. See:
What is the expected behaviour for multiple set-returning functions in select clause?
One could be more explicit with a ROWS FROM expression, which works properly since Postgres 9.4:
SELECT t.data
, to_jsonb(s) AS new_data
FROM tbl t
LEFT JOIN LATERAL (
SELECT jsonb_agg(s || r) AS surveys
FROM ROWS FROM (jsonb_array_elements(t.data->'surveys')
, jsonb_array_elements(t.data->'responses')) sub(s,r)
) s ON true;
The manual about combining multiple table functions.
Or you could use WITH ORDINALITY to get original order of elements and combine as you wish:
PostgreSQL unnest() with element number

Related

Average of numeric values in Postgres JSON column

I have a jsonb column with the following structure:
{
"key1": {
"type": "...",
"label": "...",
"variables": [
{
"label": "Height",
"value": 131315.9289,
"variable": "myVar1"
},
{
"label": "Width",
"value": 61085.7525,
"variable": "myVar2"
}
]
},
}
I want to query for the average height across all rows. The top-level key values are unknown, so I have something like this:
select id,
avg((latVars ->> 'value')::numeric) as avg
from "MyTable",
jsonb_array_elements((my_json_field->jsonb_object_keys(my_json_field)->>'variables')::jsonb) as latVars
where my_json_field is not null
group by id;
It's throwing the following error:
ERROR: set-returning functions must appear at top level of FROM
Moving the jsonb_array_elements function above MyTable in the FROM clause doesn't work.
I'm following the basic advice found in this SO answer to no avail.
Any advice?
jsonb_array_elements is not relevant until my_json_field is a json array at the top level.
You can use instead the jsonb_path_query function based on the jsonpath language if postgres >= 12 :
select id
, avg(v.value :: numeric) as avg
from "MyTable"
, jsonb_path_query(my_json_field, '$.*.variables[*] ? (#.label == "Height").value') AS v(value)
where my_json_field is not null
group by id;

Can't hide node "data"

Column data is jsonb
SELECT
json_agg(shop_order)
FROM (
SELECT data from shop_order
WHERE data->'contacts'->'customer'->>'phone' LIKE '%1234567%' LIMIT 3 OFFSET 3
) shop_order
and here result as array:
[
{
"data": {
"id": 211111,
"cartCount": 4,
"created_at": "2020-10-28T12:58:33.387Z",
"modified_at": "2020-10-28T12:58:33.387Z"
}
}
]
Nice. But... I need to hide node data.
The result must be
[
{
"id": 211111,
"cartCount": 4,
"created_at": "2020-10-28T12:58:33.387Z",
"modified_at": "2020-10-28T12:58:33.387Z"
}
]
Is it possible?
you should be able to perform a second select on the result. then specificaly select data
SELECT (result->>'data') as result,
FROM result
example

Postgres query to remove duplicates in multiple joined arrays using knex

I'm using knex to build a postgres query and have a table of recipes with a many to many relationship to both a table of ingredients and steps (each step being a part of an instruction). I'm trying to aggregate both the steps and ingredients into their own arrays within the query. My problem is as soon as I join the second array both arrays lose their distinctiveness (ie. table a has 2 elements, table b has 3 elements; after I join table b; both arrays now have 6 elements).
I've tried using distinct but every attempt has resulted in an error being thrown.
Here's what I'm trying to output:
"id": 1,
"title": "sometitle",
"ingredients": [
{
"ingredient": "avacado",
"quantity": 24
},
{
"ingredient": "asparagus",
"quantity": 42
},
],
"instructions": [
{
"step": 1,
"instruction": "one"
},
{
"step": 2,
"instruction": "two"
},
{
"step": 3,
"instruction": "three"
},
]
Here's what I have so far:
knex(`recipes as r`)
.where({'r.id': 1})
.join('ingredients_list as list', {'list.recipe_id': 'r.id'})
.join('ingredients', {'list.ingredient_id': 'ingredients.id'})
.join('instructions', {'instructions.recipe_id': 'r.id'})
.select(
'r.id',
db.raw(`json_agg(json_build_object(
'ingredient', ingredients.name,
'quantity', list.quantity
)) as ingredients`),
db.raw(`json_agg(json_build_object(
'step', instructions.step_number,
'instruction', instructions.description
)) as instructions`)
)
.groupBy('r.id')
.first()
Here's the solution I came up with in case anyone else runs into this issue. I assume this works because postgres is unable to evaluate equality of json objects; whereas jsonb is a binary object. I'd love a more thorough explanation of this is somebody has one.
distinct json_agg(jsonb_build_object(...))
knex(`recipes as r`)
.where({'r.id': 1})
.join('ingredients_list as list', {'list.recipe_id': 'r.id'})
.join('ingredients', {'list.ingredient_id': 'ingredients.id'})
.join('instructions', {'instructions.recipe_id': 'r.id'})
.select(
'r.id',
db.raw(`distinct json_agg(jsonb_build_object(
'ingredient', ingredients.name,
'quantity', list.quantity
)) as ingredients`),
db.raw(`distinct json_agg(jsonb_build_object(
'step', instructions.step_number,
'instruction', instructions.description
)) as instructions`)
)
.groupBy('r.id')
.first()

jsonb LIKE query on nested objects in an array

My JSON data looks like this:
[{
"id": 1,
"payload": {
"location": "NY",
"details": [{
"name": "cafe",
"cuisine": "mexican"
},
{
"name": "foody",
"cuisine": "italian"
}
]
}
}, {
"id": 2,
"payload": {
"location": "NY",
"details": [{
"name": "mbar",
"cuisine": "mexican"
},
{
"name": "fdy",
"cuisine": "italian"
}
]
}
}]
given a text "foo" I want to return all the tuples that have this substring. But I cannot figure out how to write the query for the same.
I followed this related answer but cannot figure out how to do LIKE.
This is what I have working right now:
SELECT r.res->>'name' AS feature_name, d.details::text
FROM restaurants r
, LATERAL (SELECT ARRAY (
SELECT * FROM json_populate_recordset(null::foo, r.res#>'{payload,
details}')
)
) AS d(details)
WHERE d.details #> '{cafe}';
Instead of passing the whole text of cafe I want to pass ca and get the results that match that text.
Your solution can be simplified some more:
SELECT r.res->>'name' AS feature_name, d.name AS detail_name
FROM restaurants r
, jsonb_populate_recordset(null::foo, r.res #> '{payload, details}') d
WHERE d.name LIKE '%oh%';
Or simpler, yet, with jsonb_array_elements() since you don't actually need the row type (foo) at all in this example:
SELECT r.res->>'name' AS feature_name, d->>'name' AS detail_name
FROM restaurants r
, jsonb_array_elements(r.res #> '{payload, details}') d
WHERE d->>'name' LIKE '%oh%';
db<>fiddle here
But that's not what you asked exactly:
I want to return all the tuples that have this substring.
You are returning all JSON array elements (0-n per base table row), where one particular key ('{payload,details,*,name}') matches (case-sensitively).
And your original question had a nested JSON array on top of this. You removed the outer array for this solution - I did the same.
Depending on your actual requirements the new text search capability of Postgres 10 might be useful.
I ended up doing this(inspired by this answer - jsonb query with nested objects in an array)
SELECT r.res->>'name' AS feature_name, d.details::text
FROM restaurants r
, LATERAL (
SELECT * FROM json_populate_recordset(null::foo, r.res#>'{payload, details}')
) AS d(details)
WHERE d.details LIKE '%oh%';
Fiddle here - http://sqlfiddle.com/#!15/f2027/5

Postgresql SELECTing from JSON column

Assume I am using PG 9.3 and I have a post table with a json column 'meta_data':
Example content of the json column 'meta_data'
{
"content": "this is a post body",
"comments": [
{
"user_id": 1,
"content": "hello"
},
{
"user_id": 2,
"content": "foo"
},
{
"user_id": 3,
"content": "bar"
}
]
}
How can I find all the posts where the user_id = 1 from the comments array from the meta_data column?
I'm almost positive I'm implementing this incorrectly but try this
select *
from posts
where id in (
select id from (
select id,
json_array_elements(meta_data->'comments')->'user_id' as user_id
from posts
) x
where cast(user_id as varchar) = '1'
);
There's probably an array operator like #> that will remove the need for the nested select statements but I can't seem to get it to work right now.
Let me know if this is going down the correct track, I'm sure we could figure it out if required.