MySQL 8 search JSON key by value in array - mysql

I've got MySQL table with JSON field, where I store data in such a format.
{
"fields": {
"1": {
"s": "y"
},
"2": {
"s": "n"
}
}
}
I need to obtain the keys in fields, e.g. 1 or 2 given the value of s.
Example query:
create table mytable ( mycol json );
insert into mytable set mycol = '{"fields": {"1": {"s": "y"},"2": {"s": "n"}}}';
select j.* from mytable, JSON_TABLE(mycol,
'$.fields.*' COLUMNS (
json_key VARCHAR(10) PATH '$',
s VARCHAR(10) PATH '$.s'
)
) AS j where j.s = 'y';
gives:
# json_key, s
null, y
I would expect to get
# json_key, s
1, y
Is it possible to get that data somehow?
I don't need the results in row / table format. I would be happy to get the comma separated list of IDs (json_keys) meeting my criterium.
EDIT:
I was also thinking about getting the paths using JSON_SEARCH and passing that to JSON_EXTRACT, this was achieved here: Combining JSON_SEARCH and JSON_EXTRACT get me: "Invalid JSON path expression."
Unfortunately the difference is that I would need to use JSON_SEARCH in all mode, as I need all results. In such a mode JSON_SEARCH returns list of paths, where as JSON_EXTRACT accepts list of arguments.

Try FOR ORDINALITY (see 12.17.6 JSON Table Functions), this type enumerates rows in the COLUMNS clause:
SELECT
JSON_UNQUOTE(
JSON_EXTRACT(
JSON_KEYS(`mycol` ->> '$.fields'),
CONCAT('$[', `j`.`row` - 1, ']')
)
) `json_key`,
`j`.`s`
FROM
`mytable`,
JSON_TABLE(
`mycol`,
'$.fields.*' COLUMNS (
`row` FOR ORDINALITY,
`s` VARCHAR(10) PATH '$.s'
)
) `j`
WHERE
`j`.`s` = 'y';
See dbfiddle.

Related

MariaDB/MySQL - Convert keys and values from json object into rows, using JSON_TABLE

Using Mariadb 10.6 - In the following example, I try to convert the entries of the json object into table rows:
SELECT *
FROM JSON_TABLE('{
"1": [1, 123.25],
"10": [2, 110.5],
"100": [3, 105.75]
}', '$.*' COLUMNS (
col1 decimal(13,2) PATH '$',
col2 int PATH '$[0]',
col3 decimal(17,2) PATH '$[1]'
)) table1
The result is:
col1
col2
col3
NULL
1
123.25
NULL
2
110.50
NULL
3
105.75
Is there any way to fill "col1" with the property keys ("1", "10", "100")?
I guess there is some "key word" to reference the key, but I can't find any information on this in the docs from MariaDB or MySQL.
I already made a routine that creates a temporary table by looping output from JSON_KEYS, but it would be more elegant if I can use JSON_TABLE for this job.
This is an other way to do it using CROSS JOIN, JSON_TABLE & JSON_KEYS:
JSON_KEYS(json) will give us ["1", "10", "100"]
CROSS JOIN is used to generate multi rows from ["1", "10", "100"]
WITH data AS
(
SELECT '{
"1": [1, 123.25],
"10": [2, 110.5],
"100": [3, 105.75]
}' AS json
)
SELECT k.key, c.col2, c.col3
FROM data
CROSS JOIN JSON_TABLE(
JSON_KEYS(json),
'$[*]' COLUMNS(
rowid FOR ORDINALITY,
key TEXT PATH '$'
)
) k
INNER JOIN
(SELECT cols.*
FROM data,
JSON_TABLE(
json,
'$.*' COLUMNS(
rowid FOR ORDINALITY,
col2 int PATH '$[0]',
col3 decimal(17, 2) PATH '$[1]'
)
) AS cols) AS c
ON c.rowid = k.rowid;
demo here
Here's one way to do it without routines:
extract your json values using JSON_TABLE, alongside a row number using FOR ORDINALITY
extract your keys using JSON_KEYS
for each record, extract the i-th key corresponding to i-th ranking value, given by row number, using JSON_EXTRACT
SELECT JSON_EXTRACT(JSON_KEYS(#json),
CONCAT('$[', table1.rowid-1, ']')) AS col1,
table1.col2,
table1.col3
FROM JSON_TABLE(#json, '$.*' COLUMNS (
rowid FOR ORDINALITY,
col2 int PATH '$[0]',
col3 decimal(17,2) PATH '$[1]'
)) table1
Output:
col1
col2
col3
"1"
1
123.25
"10"
2
110.50
"100"
3
105.75
Check the demo here.
I answer to my own question:
I'm sorry that there apparently is'nt any native option to reference the key names in a json object with JSON_TABLE (yet), and the two workarounds currently posted are great.
I ended up using a mixture from both:
SET #json = '{ "1": [1, 123.25], "10": [2, 110.5], "100": [3, 105.75] }';
SELECT
col1,
JSON_EXTRACT(#json, CONCAT('$."', col1, '"[0]')) col2,
JSON_EXTRACT(#json, CONCAT('$."', col1, '"[1]')) col3
FROM JSON_TABLE(JSON_KEYS(#json), '$[*]' COLUMNS (col1 varchar(20) PATH '$')) t1;

Update value inside of nested json array

I have JSON stored in a table. The JSON is nested and has the following structure
[
{
"name": "abc",
"ques": [
{
"qId": 100
},
{
"qId": 200
}
]
},{
"name": "xyz",
"ques": [
{
"qId": 100
},
{
"qId": 300
}
]
}
]
Update TABLE_NAME
set COLUMN_NAME = jsonb_set(COLUMN_NAME, '{ques,qId}', '101')
WHERE COLUMN_NAME->>'qId'=100
I am trying to update qId value from JSON. If qId is 100, I want to update it to 101.
1st solution, simple but to be used carefully
You convert your json data into text and you use the replace function :
Update TABLE_NAME
set COLUMN_NAME = replace(COLUMN_NAME :: text,'"qId": 100}', '"qId": 101}') :: jsonb
2nd solution more elegant and more complex
jsonb_set cannot make several replacements in the same jsonb data at the same time. To do so, you need to create your own aggregate based on the jsonb_set function :
CREATE OR REPLACE FUNCTION jsonb_set(x jsonb, y jsonb, path text[], new_value jsonb) RETURNS jsonb LANGUAGE sql AS $$
SELECT jsonb_set(COALESCE(x, y), path, new_value) ; $$ ;
CREATE OR REPLACE AGGREGATE jsonb_set_agg(x jsonb, path text[], new_value jsonb)
( stype = jsonb, sfunc = jsonb_set);
Then you get your result with the following query :
UPDATE TABLE_NAME
SET COLUMN_NAME =
( SELECT jsonb_set_agg(COLUMN_NAME :: jsonb, array[(a.id - 1) :: text, 'ques', (b.id - 1) :: text], jsonb_build_object('qId', 101))
FROM jsonb_path_query(COLUMN_NAME :: jsonb, '$[*]') WITH ORDINALITY AS a(content, id)
CROSS JOIN LATERAL jsonb_path_query(a.content->'ques', '$[*]') WITH ORDINALITY AS b(content, id)
WHERE (b.content)->'qId' = to_jsonb(100)
)
Note that this query is not universal, and it must breakdown the jsonb data according to its structure.
Note that jsonb_array_elements can be used in place of jsonb_path_query, but you will get an error with jsonb_array_elements when the jsonb data is not an array, whereas you won't get any error with jsonb_path_query in lax mode which is the default mode.
Full test results in dbfiddle
You must specify the whole path to the value.
In this case your json is an array so you need to address which element of this array your are trying to modify.
A direct approach (over your example) would be:
jsonb_set(
jsonb_set(
COLUMN_NAME
, '{0,ques,qId}'
, '101'
)
, '{1,ques,qId}'
, '101'
)
Of course, if you want to modify every element of different arrays of different lengths you would need to elaborate this approach disassembling the array to modify every contained element.

Conditionally select values from an array in a nested JSON string in a Mysql database

I am struggling to conditionally extract values from a nested JSON string in the Mysql table.
{"users": [{"userId": "10000001", "userToken": "11000000000001", "userTokenValidity": 1}, {"userId": "10000002", "userToken": "12000000000001", "userTokenValidity": 1}, {"userId": "10000003", "userToken": "13000000000001", "userTokenValidity": 0}]}
I want to select a userToken but only if the userTokenValidity is 1. So in this example only "11000000000001" and "12000000000001" should get selected.
This will extract the whole array ... how should I filter the result?
SELECT t.my_column->>"$.users" FROM my_table t;
SELECT CAST(value AS CHAR) output
FROM test
CROSS JOIN JSON_TABLE(test.data, '$.users[*]' COLUMNS (value JSON PATH '$')) jsontable
WHERE value->>'$.userTokenValidity' = 1
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=4876ec22a9df4f6d2e75a476a02a2615

Expand JSON with unknown keys to rows with MySQL JSON_TABLE

I have a MySQL 8.0.22 JSON column containing objects with keys that aren't known in advance:
'{"x": 1, "y": 2, "z": 3}'
'{"e": 4, "k": 5}'
I want to use JSON_TABLE to expand these values into multiple rows containing key value pairs:
key
value
x
1
y
2
z
3
e
4
k
5
The difficulty of course is that the keys aren't known a priori. The best thing I've come up with is...
SET #json_doc = '{"x": 1, "y": 2, "z": 3}';
SELECT a.seq, b.k, a.v
FROM
JSON_TABLE(
#json_doc,
"$.*"
COLUMNS(
seq FOR ordinality,
v INT PATH "$"
)
) AS a,
JSON_TABLE(
JSON_KEYS(#json_doc),
"$[*]"
COLUMNS(
seq FOR ordinality,
k CHAR(1) PATH "$"
)
) AS b
WHERE a.seq = b.seq;
This feels strange because it uses two JSON_TABLE calls, does a cross join on the values and keys, then keeps the ones that align. I'd like to find a simpler query like this...
SELECT a.seq, b.k, a.v
FROM
JSON_TABLE(
#json_doc,
"$.*"
COLUMNS(
seq FOR ordinality,
k CHAR(1) PATH "?" -- <-- what do I put here to find each key?
v INT PATH "$"
)
) AS a,
I know this problem can probably be solved with CTEs or a numbers table and JSON_EXTRACT. But, I'd like to find something performant and readable if possible.
You can use enumarete by using ROW_NUMBER() window function while determining the key values through use of JSON_KEYS(), and then extract the respective keys by using JSON_EXTRACT() from the arrays we got such as
WITH k AS
(
SELECT *,
ROW_NUMBER() OVER(PARTITION BY `jsdata` ORDER BY value DESC) AS rn,
JSON_KEYS(`jsdata`) AS jk
FROM `tab` AS t
JOIN JSON_TABLE(`jsdata`,'$.*' COLUMNS (value INT PATH '$')) j
)
SELECT JSON_UNQUOTE(JSON_EXTRACT(jk, CONCAT('$[',rn-1,']'))) AS "key",
value
FROM k
or use the following query as being more straightforward
SELECT JSON_UNQUOTE(
JSON_EXTRACT(JSON_KEYS(`jsdata`),
CONCAT('$[',
ROW_NUMBER() OVER(PARTITION BY `jsdata` ORDER BY value DESC)-1,
']'))
) AS "key", value
FROM `tab` AS t
JOIN JSON_TABLE(`jsdata`,'$.*' COLUMNS (value INT PATH '$')) j
Demo
Try to do JSON_EXTRACT directly after you got the JSON_KEYS as rows:
WITH j AS (
SELECT CAST('{"a": 1, "b": "-1", "c": null}' AS JSON) o UNION ALL
SELECT CAST('{"x": 2, "y": "-2", "z": null}' AS JSON)
)
SELECT k, JSON_EXTRACT(j.o, CONCAT('$."', jt.k, '"')) v
FROM j
, JSON_TABLE(JSON_KEYS(o), '$[*]' COLUMNS (k VARCHAR(200) PATH '$')) jt;
The answer by Barbaros can solve your problem with the demo data you provided, but it may not get what you want if your json objects have same value under different keys.

mysql filter with dynamic json key

I need to select SUM of all value which is realized (=true) from the mysql data set below. In this example, I should get 1000 only.
Please note that the key is dynamic (year\month\day).
I am able to get a list of value by running the sql below:
SELECT (JSON_EXTRACT(json_value, "$**.value")) AS total FROM milestone
However, i can't seem to get the filter part works with this:
JSON_CONTAINS_PATH(json_value, 'all', "$**.realized") IS NOT NULL
id=1, column json_value
{
"2018": {
"5": {
"4": {
"value": "5000"
}
},
"12": {
"4": {
"value": "80000",
"realized": "false"
}
}
}
}
id=2, column json_value
{
"2016": {
"12": {
"4": {
"value": "1000",
"realized": "true"
}
}
}
}
In modern versions of MySQL (>= 8.0.4) the query would be relatively simple (see JSON_TABLE):
SELECT
`milestone`.`id`,
SUM(`der`.`value`) `total`
FROM
`milestone`,
JSON_TABLE(
JSON_ARRAY(`json_value`),
'$[*]' COLUMNS(
NESTED PATH '$**.*' COLUMNS(
`value` DECIMAL(10, 2) PATH '$.value',
`realized` VARCHAR(5) PATH '$.realized'
)
)
) `der`
WHERE
`der`.`value` IS NOT NULL AND
`der`.`realized` = 'true'
GROUP BY
`milestone`.`id`;
See db-fiddle.
Here's a query that works on MySQL 5.7:
SELECT SUM(
JSON_UNQUOTE(
JSON_EXTRACT(
json_value,
CONCAT(
SUBSTRING_INDEX(
JSON_UNQUOTE(JSON_SEARCH(json_value, 'all', 'true')), '.', 4),
'.value'
)
)
)
) AS sum
FROM milestone
WHERE SUBSTRING_INDEX(
JSON_UNQUOTE(JSON_SEARCH(json_value, 'all', 'true')),
'.', -1) = 'realized'
This is quite complex, and hard to develop and maintain. Also it might not handle cases where you have multiple realized: true entries in a given JSON document. Or if there are other JSON keys besides "realized" that have the value "true". It will be quite tricky to even think of the edge cases, let alone handle them in code.
Out of curiosity, why not store this data as a traditional table?
CREATE TABLE milestone (
milestone_id INT NOT NULL,
date DATE NOT NULL,
value INT NOT NULL,
realized BOOL,
PRIMARY KEY (milestone_id, date)
);
INSERT INTO milestone VALUES
(1, '2018-05-04', 5000, NULL),
(1, '2018-12-04', 80000, false),
(2, '2016-12-04', 1000, true);
Then it's simpler to do your query:
SELECT SUM(value) FROM milestone WHERE realized = true;
I'm curious because I'm seeing more people using JSON in MySQL for cases where it would be easier to use normal tables and column. Easier to write the query, easier to understand the query, and more efficient to store the data and optimize the query performance.
Using JSON in the way you are doing makes queries more difficult than they should be.