Count objects in jsonb array with Postgres - postgresql

Assume that every jsonb value in a table have this structure:
{
"level1": [
{
"level2": [
{
"key": key,
"value": value,
"messages": [
]
},
{
"key": key,
"value": value,
"messages": [
]
},
{
"key": key,
"value": value,
"messages": [
]
}
]
}
]
}
The names of key level1 is dynamic, so can be anything (that's why I'm using the jsonb_object_keys).
I need to check if any object inside level2.messages is empty per date.
That is: if all level2.messages in a date are empty, return false. Otherwise (at least one of the objects with message has a non-empty array), return true.
I thought I could use json functions in a subquery, but they are not known inside the subquery.
I have something like this:
SELECT t2.date,
(SELECT 1 FROM fields WHERE jsonb_array_length(fields ->> 'messages') = 1 LIMIT 1) AS hasMessages
FROM table1 t1
INNER JOIN table2 t2 ON t2.id = t1.id,
jsonb_object_keys(t1.result) AS rootNode,
jsonb_array_elements(t1.result -> rootNode) AS level2,
jsonb_array_elements(level2 -> 'level2') AS fields
GROUP BY t2.date

Based on the fragmentary info in the question, this would work:
SELECT date
, count(*) AS message_count
, count(*) FILTER (WHERE l2_val->'messages' = '[]') AS empty_message_count
FROM table1 t1
, jsonb_object_keys(result) AS key1
, jsonb_array_elements(result->key1->0->'level2') AS l2_val
GROUP BY 1
-- HAVING ?
This is assuming:
Always only one key name in the outer level of the JSON object.
Always only one array element in level1.
Key name of nested array is level2'.
I guess you want to identify those that do have messages, but all empty ...

Related

How can I find a outer (wildcard) key that contains certain parameters within a JSONB field in PostgreSQL?

I have a PostgreSQL (V14) database containing info in JSONB format. The info of one cell could be something like this:
{
"Car23": {
"color": "blue",
"year": 1982,
"engine": [
12,
23.3
],
"broke": [
2,
8.5
]
},
"Banana": {
"color": "yellow",
"year": 2022,
"taste": "ok"
},
"asdf": {
"taste": "bad",
"year": [
1945,
6
],
"engine": [
24,
53.534
]
},
"Unique": {
"broke": [
342,
2.5
]
}
}
The outer key, i.o "Car23" or "Banana" has a random name created by an outside program. I want to do queries that allow me to get where the outer key contains a certain key:value.
For instance:
Find outer key(s) that broke. ("Car23" and "Unique")
find outer key(s) that have a year above 1988. ("Banana")
Find outer key(s) that have engine info and the second array number is higher then 50. ("asdf")
In Sql this seems pretty standard stuff, however I don't know how to do this within JSONB when the outer keys have random names...
I red that outer wildcard keys aren't possible, so I'm hoping there's another way of doing this within Postgresql.
You will need to unnest the JSON elements and then pick the ones you want. The fact that some values are sometimes stored in an array, and sometimes as a plain value makes things even more complicated.
I assume that "things that broke" just means those, that have a key broke:
select j.key
from the_table t
cross join lateral (
select *
from jsonb_each(t.the_column) as j(key, item)
where j.item ? 'broke'
) j;
To find those with a year > 1988 is tricky because of the two different ways of storing the year:
select j.key
from the_table t
cross join lateral (
select *
from jsonb_each(t.the_column) as j(key, item)
where case
when jsonb_typeof(j.item -> 'year') = 'array' then (j.item -> 'year' -> 0)::int
else (j.item ->> 'year')::int
end > 1988
) j;
When checking for the "engine" array item, you probably should also check if it's really an array:
select j.key
from the_table t
cross join lateral (
select *
from jsonb_each(t.the_column) as j(key, item)
where jsonb_typeof(j.item -> 'engine') = 'array'
and (j.item -> 'engine' ->> 1)::numeric > 50
) j;

How to remove field from each elements json array postgres?

I have table
CREATE TABLE items (
id BIGINT PRIMARY KEY ,
data jsonb
);
Format of data
[
{
"id": "d20fe90c-137c-4713-bde1-2d4e75178ad3",
"text": "text",
"count": 1
},
{
"id": "d20fe90c-137c-4713-bde1-2d4e75178ad4",
""text": "text",
"count": 1
}
]
How I can remove field count from all elements of data json array?
I try
UPDATE items
SET data = data #- '{count}';
But this query requires index of array element before count as
UPDATE items
SET data = data #- '{0, count}';
There is no operator or built-in function to do that. Unnest the array and aggregate modified elements in the way like this:
update items t
set data = (
select jsonb_agg(elem- 'count')
from items
cross join lateral jsonb_array_elements(data) as arr(elem)
where id = t.id)
Test it in db<>fiddle.

Update selected values in a jsonb column containing a array

Table faults contains column recacc (jsonb) which contains an array of json objects. Each of them contains a field action. If the value for action is abc, I want to change it to cba. Changes to be applied to all rows.
[
{
"action": "abc",
"created": 1128154425441
},
{
"action": "lmn",
"created": 1228154425441
},
{
"action": "xyz",
"created": 1328154425441
}
]
The following doesn't work, probably because of the data being in array format
update faults
set recacc = jsonb_set(recacc,'{action}', to_jsonb('cbe'::TEXT),false)
where recacc ->> 'action' = 'abc'
I'm not sure if this is the best option, but you may first get the elements of jsonb using jsonb_array_elements, replace it and then reconstruct the json using array_agg and array_to_json.
UPDATE faults SET recacc = new_recacc::jsonb
FROM
(SELECT array_to_json(array_agg(s)) as new_recacc
FROM
( SELECT
replace(c->>'action','abc','cba') , --this to change the value
c->>'created' FROM faults f
cross join lateral jsonb_array_elements(f.recacc) as c
) as s (action,created)
) m;
Demo

Postgresql jsonb traversal

I am very new to the PG jsonb field.
I have for example a jsonb field containing the following
{
"RootModule": {
"path": [
1
],
"tags": {
"ModuleBase1": {
"value": 40640,
"humanstring": "40640"
},
"ModuleBase2": {
"value": 40200,
"humanstring": "40200"
}
},
"children": {
"RtuInfoModule": {
"path": [
1,
0
],
"tags": {
"in0": {
"value": 11172,
"humanstring": "11172"
},
"in1": {
"value": 25913,
"humanstring": "25913"
}
etc....
Is there a way to query X levels deep and search the "tags" key for a certain key.
Say I want "ModuleBase2" and "in1" and I want to get their values?
Basically I am looking for a query that will traverse a jsonb field until it finds a key and returns the value without having to know the structure.
In Python or JS a simple loop or recursive function could easily traverse a json object (or dictionary) until it finds a key.
Is there a built in function PG has to do that?
Ultimately I want to do this in django.
Edit:
I see I can do stuff like
SELECT data.key AS key, data.value as value
FROM trending_snapshot, jsonb_each(trending_snapshot.snapshot-
>'RootModule') AS data
WHERE key = 'tags';
But I must specify the the levels.
You can use a recursive query to flatten a nested jsonb, see this answer. Modify the query to find values for specific keys (add a condition in where clause):
with recursive flat (id, path, value) as (
select id, key, value
from my_table,
jsonb_each(data)
union
select f.id, concat(f.path, '.', j.key), j.value
from flat f,
jsonb_each(f.value) j
where jsonb_typeof(f.value) = 'object'
)
select id, path, value
from flat
where path like any(array['%ModuleBase2.value', '%in1.value']);
id | path | value
----+--------------------------------------------------+-------
1 | RootModule.tags.ModuleBase2.value | 40200
1 | RootModule.children.RtuInfoModule.tags.in1.value | 25913
(2 rows)
Test it in SqlFiddle.

PostgreSQL - jsonb_each

I have just started to play around with jsonb on postgres and finding examples hard to find online as it is a relatively new concept.I am trying to use jsonb_each_text to printout a table of keys and values but get a csv's in a single column.
I have the below json saved as as jsonb and using it to test my queries.
{
"lookup_id": "730fca0c-2984-4d5c-8fab-2a9aa2144534",
"service_type": "XXX",
"metadata": "sampledata2",
"matrix": [
{
"payment_selection": "type",
"offer_currencies": [
{
"currency_code": "EUR",
"value": 1220.42
}
]
}
]
}
I can gain access to offer_currencies array with
SELECT element -> 'offer_currencies' -> 0
FROM test t, jsonb_array_elements(t.json -> 'matrix') AS element
WHERE element ->> 'payment_selection' = 'type'
which gives a result of "{"value": 1220.42, "currency_code": "EUR"}", so if i run the below query I get (I have to change " for ')
select * from jsonb_each_text('{"value": 1220.42, "currency_code": "EUR"}')
Key | Value
---------------|----------
"value" | "1220.42"
"currency_code"| "EUR"
So using the above theory I created this query
SELECT jsonb_each_text(data)
FROM (SELECT element -> 'offer_currencies' -> 0 AS data
FROM test t, jsonb_array_elements(t.json -> 'matrix') AS element
WHERE element ->> 'payment_selection' = 'type') AS dummy;
But this prints csv's in one column
record
---------------------
"(value,1220.42)"
"(currency_code,EUR)"
The primary problem here, is that you select the whole row as a column (PostgreSQL allows that). You can fix that with SELECT (jsonb_each_text(data)).* ....
But: don't SELECT set-returning functions, that can often lead to errors (or unexpected results). Instead, use f.ex. LATERAL joins/sub-queries:
select first_currency.*
from test t
, jsonb_array_elements(t.json -> 'matrix') element
, jsonb_each_text(element -> 'offer_currencies' -> 0) first_currency
where element ->> 'payment_selection' = 'type'
Note: function calls in the FROM clause are implicit LATERAL joins (here: CROSS JOINs).
WITH testa AS(
select jsonb_array_elements
(t.json -> 'matrix') -> 'offer_currencies' -> 0 as jsonbcolumn from test t)
SELECT d.key, d.value FROM testa
join jsonb_each_text(testa.jsonbcolumn) d ON true
ORDER BY 1, 2;
tetsa get the temporal jsonb data. Then using lateral join to transform the jsonb data to table format.