Preventing duplicates in JSON array in MySQL - mysql

I have a JSON-typed column in a MySQl table. This column stores an array of integer values, like '[1,2,3]'.
What I want is: when I insert a new value into this array using JSON_ARRAY_INSERT/JSON_ARRAY_APPEND, if the value already exists, simply ignore it.
How can I do this within MySQL? That is, NOT checking duplicates from calling scripts like PHP or Node.js, etc.
Update: I'd prefer to use a built-in MySQL mechanism for this purpose, because if done in external scripts, there would be a small chance that duplicates happen due to race conditions, even if I check against duplicates before storing the data.
Thanks!

Without knowing your exact table structure it's difficult to give a precise answer, but the following queries demonstrate how it's done with variables. You would just need to adapt this according to your columns.
set #j='["1","2","3"]';
set #v = "4";
select ifnull(json_search(#j, 'one', #v), #j:=json_array_append(#j, '$', #v));
set #v = "1";
select ifnull(json_search(#j, 'one', #v), #j:=json_array_append(#j, '$', #v));
select #j;
Output:
["1", "2", "3", "4"]

Hope this helps.
Schema (MySQL v5.7)
CREATE TABLE test (
id INT,
js JSON
);
INSERT INTO test (id, js) VALUES (1, '[1,2,3]');
INSERT INTO test (id, js) VALUES (2, '[1,2]');
Query #1
select * from test;
| id | js |
| --- | --------- |
| 1 | [1, 2, 3] |
| 2 | [1, 2] |
Query #2
UPDATE test
SET js = JSON_ARRAY_APPEND (js, '$', 3)
WHERE id = 1 and not JSON_CONTAINS(js, '3', '$');
UPDATE test
SET js = JSON_ARRAY_APPEND (js, '$', 3)
WHERE id = 2 and not JSON_CONTAINS(js, '3', '$');
Query #3
select * from test;
| id | js |
| --- | --------- |
| 1 | [1, 2, 3] |
| 2 | [1, 2, 3] |

set #j='[1, 2, 3]';
set #v=1;
SELECT IF(JSON_CONTAINS(#j, CAST(#v AS JSON), '$') = 1, #j, json_array_append(#j, '$', CAST(#v AS JSON)));
OUTPUT:
[1, 2, 3]
set #j='[1, 2, 3]';
set #v=4;
SELECT IF(JSON_CONTAINS(#j, CAST(#v AS JSON), '$') = 1, #j, json_array_append(#j, '$', CAST(#v AS JSON)));
OUTPUT:
[1, 2, 3, 4]

DECLARE lc,leng,du INT(50);
DECLARE conc,vb VARCHAR(50);
DECLARE new_array JSON;
SET new_array='[]';
SET lc=0;
SET leng=JSON_LENGTH(first_array);
WHILE lc<leng DO
SET conc=(SELECT CONCAT('$[',lc,']'));
IF JSON_CONTAINS(JSON_UNQUOTE(new_array),CONCAT('"',JSON_UNQUOTE(JSON_EXTRACT(first_array,conc)),'"')) THEN
SET du=1;
ELSE
SET new_array=(SELECT JSON_ARRAY_APPEND(new_array,'$',(JSON_UNQUOTE(JSON_EXTRACT(first_array,conc)))));
end if;
SET lc=lc+1;
END WHILE;
RETURN new_array;
This function returns non duplicate JSON array in MySQL
first array is a json_array input parrameter

Related

How to insert new value in the middle of a JSON array?

I have a JSON type field in my table, which has a value like this
[1, 3]
and I want to insert another value in the middle of the array, using something like a json splice function (I know it doesn't exist)
/* JSON_ARRAY_SPLICE(array, start, deleteCount, itemToInsert) */
JSON_ARRAY_SPLICE('[1, 3]', 1, 0, 2)
>>> [1, 2, 3]
I'm using php and I can make a function in order to do it, but i'm looking for a mysql solution. Is there a way I can achieve this?
mysql> set #j = '[1, 3]';
mysql> select json_array_insert(#j, '$[1]', 2) as new_j;
+-----------+
| new_j |
+-----------+
| [1, 2, 3] |
+-----------+

Postgres: transform json object into array of key-value pairs

With this select:
SELECT '{"key1": "value1", "key2": "value2"}'::jsonb
can we transform the result object into the following array of objects keeping their keys and values respectively?
[{"key1": "value1"}, {"key2": "value2"}]
In my case, I have many-2-many relationship of tags and operations, and the following query with aggregation of tags into json object for each operation:
SELECT ot.operation_id, json_object_agg(t.name, t.value) AS tagsjson
FROM tag t
INNER JOIN operation_tag ot ON t.tag_id = ot.tag_id
GROUP BY ot.operation_id
This works fine except format of the result: I need array of tags [{name : value},...] instead of whole object containing tags as it's properties:
If I understand you correctly you are in a situation like the following:
your source table contains id, keys and values like the below
create table test_k_v (id int, k varchar, v varchar);
insert into test_k_v values (1, 'key1', 'value1');
insert into test_k_v values (1, 'key2', 'value2');
insert into test_k_v values (2, 'key3', 'value3');
insert into test_k_v values (2, 'key4', 'value4');
insert into test_k_v values (2, 'key5', 'value5');
which results in
defaultdb=> select * from test_k_v;
id | k | v
----+------+--------
1 | key1 | value1
1 | key2 | value2
2 | key3 | value3
2 | key4 | value4
2 | key5 | value5
(5 rows)
If so, you can create an array with all the tags with the following query:
select id, array_agg(json_build_object(k, v)) agg_res
from test_k_v
group by id;
result
id | agg_res
----+---------------------------------------------------------------------------------
2 | {"{\"key3\" : \"value3\"}","{\"key4\" : \"value4\"}","{\"key5\" : \"value5\"}"}
1 | {"{\"key1\" : \"value1\"}","{\"key2\" : \"value2\"}"}
(2 rows)

How to construct single JSON where column1 is property name and column2 is value

In SQL Server 2016 +, how can i convert a table looking like this:
+---------+----------+
| Kee | Val |
+---------+----------+
| aaaaaa | 11111111 |
| bbbbbbb | 2222222 |
+---------+----------+
into an object looking like this:
{
"aaaaaa": "11111111",
"bbbbbbb": "2222222"
}
This is what I've tried:
CREATE TABLE #tmp
(
Kee VARCHAR(100),
Val VARCHAR(100)
)
INSERT INTO #tmp
(
Kee,
Val
)
VALUES
('aaaaaa', '11111111'),
('bbbbbbb', '2222222')
SELECT t.Kee,
t.Val
FROM #tmp AS t
FOR JSON AUTO
DROP TABLE #tmp
But it gives:
[
{
"Kee": "aaaaaa",
"Val": "11111111"
},
{
"Kee": "bbbbbbb",
"Val": "2222222"
}
]
Unfortunately, SQL Server's Json support is not that flexible.
You will have to manually construct that json, but it's quite simple using basic string concatenation techniques.
Prior to 2017 version use for xml path with stuff:
SELECT STUFF(
(
SELECT '","'+ t.Kee +'":"'+ t.Val
FROM #tmp AS t
FOR XML PATH('')
), 1, 2, '{') + '}' As JsonResult
In 2017 SQL Server finally introduced a built-in string_agg function, making the code required to get that result much simpler:
SELECT '{"' + STRING_AGG(t.Kee +'":"'+ t.Val, '","') +'"}'
FROM #tmp As t
Result (in both cases):
{"aaaaaa":"11111111","bbbbbbb":"2222222"}

How to search JSON array in MySQL?

Let's say I have a JSON column named data in some MySQL table, and this column is a single array. So, for example, data may contain:
[1,2,3,4,5]
Now I want to select all rows which have a data column where one of its array elements is greater than 2. Is this possible?
I tried the following, but seems it is always true regardless of the values in the array:
SELECT * from my_table
WHERE JSON_EXTRACT(data, '$[*]') > 2;
You may search an array of integers as follows:
JSON_CONTAINS('[1,2,3,4,5]','7','$') Returns: 0
JSON_CONTAINS('[1,2,3,4,5]','1','$') Returns: 1
You may search an array of strings as follows:
JSON_CONTAINS('["a","2","c","4","x"]','"x"','$') Returns: 1
JSON_CONTAINS('["1","2","3","4","5"]','"7"','$') Returns: 0
Note: JSON_CONTAINS returns either 1 or 0
In your case you may search using a query like so:
SELECT * from my_table
WHERE JSON_CONTAINS(data, '2', '$');
SELECT JSON_SEARCH('["1","2","3","4","5"]', 'one', "2") is not null
is true
SELECT JSON_SEARCH('["1","2","3","4","5"]', 'one', "6") is not null
is false
Since MySQL 8 there is a new function called JSON_TABLE.
CREATE TABLE my_table (id INT, data JSON);
INSERT INTO my_table VALUES
(1, "[1,2,3,4,5]"),
(2, "[0,1,2]"),
(3, "[3,4,-10]"),
(4, "[-1,-2,0]");
SELECT DISTINCT my_table.*
FROM my_table, JSON_TABLE(data, "$[*]" COLUMNS(nr INT PATH '$')) as ids
WHERE ids.nr > 2;
+------+-----------------+
| id | data |
+------+-----------------+
| 1 | [1, 2, 3, 4, 5] |
| 3 | [3, 4, -10] |
+------+-----------------+
2 rows in set (0.00 sec)
I use a combination of JSON_EXTRACT and JSON_CONTAINS (MariaDB):
SELECT * FROM table WHERE JSON_CONTAINS(JSON_EXTRACT(json_field, '$[*].id'), 11, '$');
I don't know if we found the solution.
I found with MariaDB a way, to search path in a array. For example, in array [{"id":1}, {"id":2}], I want find path with id equal to 2.
SELECT JSON_SEARCH('name_field', 'one', 2, null, '$[*].id')
FROM name_table
The result is:
"$[1].id"
The asterisk indicate searching the entire array
This example works for me with mysql 5.7 above
SET #j = '{"a": [ "8428341ffffffff", "8428343ffffffff", "8428345ffffffff", "8428347ffffffff","8428349ffffffff", "842834bffffffff", "842834dffffffff"], "b": 2, "c": {"d": 4}}';
select JSON_CONTAINS(JSON_EXTRACT(#j , '$.a'),'"8428341ffffffff"','$') => returns 1
notice about " around search keyword, '"8428341ffffffff"'
A possible way is to deal with the problem as string matching. Convert the JSON to string and match.
Or you can use JSON_CONTAINS.
You can use JSON extract to search and select data
SELECT data, data->"$.id" as selectdata
FROM table
WHERE JSON_EXTRACT(data, "$.id") = '123'
#ORDER BY c->"$.name";
limit 10 ;
SET #doc = '[{"SongLabels": [{"SongLabelId": "111", "SongLabelName": "Funk"}, {"SongLabelId": "222", "SongLabelName": "RnB"}], "SongLabelCategoryId": "test11", "SongLabelCategoryName": "曲风"}]';
SELECT *, JSON_SEARCH(#doc, 'one', '%un%', null, '$[*].SongLabels[*].SongLabelName')FROM t_music_song_label_relation;
result: "$[0].SongLabels[0].SongLabelName"
SELECT song_label_content->'$[*].SongLabels[*].SongLabelName' FROM t_music_song_label_relation;
result: ["Funk", "RnB"]
I have similar problem, search via function
create function searchGT(threshold int, d JSON)
returns int
begin
set #i = 0;
while #i < json_length(d) do
if json_extract(d, CONCAT('$[', #i, ']')) > threshold then
return json_extract(d, CONCAT('$[', #i, ']'));
end if;
set #i = #i + 1;
end while;
return null;
end;
select searchGT(3, CAST('[1,10,20]' AS JSON));
This seems to be possible with to JSON_TABLE function. It's available in mysql version 8.0 or mariadb version 10.6.
With this test setup
CREATE TEMPORARY TABLE mytable
WITH data(a,json) AS (VALUES ('a','[1]'),
('b','[1,2]'),
('c','[1,2,3]'),
('d','[1,2,3,4]'))
SELECT * from data;
we get the following table
+---+-----------+
| a | json |
+---+-----------+
| a | [1] |
| b | [1,2] |
| c | [1,2,3] |
| d | [1,2,3,4] |
+---+-----------+
It's possible to select every row from mytable wich has a value greater than 2 in the json array with this query.
SELECT * FROM mytable
WHERE TRUE IN (SELECT val > 2
FROM JSON_TABLE(json,'$[*]'
columns (val INT(1) path '$')
) as json
)
Returns:
+---+-----------+
| a | json |
+---+-----------+
| c | [1,2,3] |
| d | [1,2,3,4] |
+---+-----------+

Postgresql merge rows with same key (hstore or json)

I have a table like this:
+--------+--------------------+
| ID | Attribute |
+--------+--------------------+
| 1 |"color" => "red" |
+--------+--------------------+
| 1 |"color" => "green" |
+--------+--------------------+
| 1 |"shape" => "square" |
+--------+--------------------+
| 2 |"color" => "blue" |
+--------+--------------------+
| 2 |"color" => "black" |
+--------+--------------------+
| 2 |"flavor" => "sweat" |
+--------+--------------------+
| 2 |"flavor" => "salty" |
+--------+--------------------+
And I want to run some postgres query that get a result table like this:
+--------+------------------------------------------------------+
| ID | Attribute |
+--------+------------------------------------------------------+
| 1 |"color" => "red, green", "shape" => "square" |
+--------+------------------------------------------------------+
| 2 |"color" => "blue, black", "flavor" => "sweat, salty" |
+--------+------------------------------------------------------+
The attribute column can either be hstore or json format. I wrote it in hstore for an example, but if we cannot achieve this in hstore, but in json, I would change the column to json.
I know that hstore does not support one key to multiple values, when I tried some merge method, it only kept one value for each key. But for json, I didn't find anything that supports multiple value merge like this neither. I think this can be done by function merging values for the same key into a string/text and add it back to the key/value pair. But I'm stuck in implementing it.
Note: if implement this in some function, ideally any key such as color, shape should not appear in the function since keys can be expanded dynamically.
Does anyone have any idea about this? Any advice or brainstorm might help. Thank you!
Just a note before anything else: in your desidered output I would use some proper json and not that kind of lookalike. So a correct output according to me would be:
+--------+----------------------------------------------------------------------+
| ID | Attribute |
+--------+----------------------------------------------------------------------+
| 1 | '{"color":["red","green"], "flavor":[], "shape":["square"]}' |
+--------+----------------------------------------------------------------------+
| 2 | '{"color":["blue","black"], "flavor":["sweat","salty"], "shape":[]}' |
+--------+----------------------------------------------------------------------+
A PL/pgSQL function which parses the json attributes and executes a dynamic query would do the job, something like that:
CREATE OR REPLACE FUNCTION merge_rows(PAR_table regclass) RETURNS TABLE (
id integer,
attributes json
) AS $$
DECLARE
ARR_attributes text[];
VAR_attribute text;
ARR_query_parts text[];
BEGIN
-- Get JSON attributes names
EXECUTE format('SELECT array_agg(name ORDER BY name) AS name FROM (SELECT DISTINCT json_object_keys(attribute) AS name FROM %s) AS s', PAR_table) INTO ARR_attributes;
-- Write json_build_object() query part
FOREACH VAR_attribute IN ARRAY ARR_attributes LOOP
ARR_query_parts := array_append(ARR_query_parts, format('%L, array_remove(array_agg(l.%s), null)', VAR_attribute, VAR_attribute));
END LOOP;
-- Return dynamic query
RETURN QUERY EXECUTE format('
SELECT t.id, json_build_object(%s) AS attributes
FROM %s AS t,
LATERAL json_to_record(t.attribute) AS l(%s)
GROUP BY t.id;',
array_to_string(ARR_query_parts, ', '), PAR_table, array_to_string(ARR_attributes, ' text, ') || ' text');
END;
$$ LANGUAGE plpgsql;
I've tested it and it seems to work, it returns a json with. Here is my test code:
CREATE TABLE mytable (
id integer NOT NULL,
attribute json NOT NULL
);
INSERT INTO mytable (id, attribute) VALUES
(1, '{"color":"red"}'),
(1, '{"color":"green"}'),
(1, '{"shape":"square"}'),
(2, '{"color":"blue"}'),
(2, '{"color" :"black"}'),
(2, '{"flavor":"sweat"}'),
(2, '{"flavor":"salty"}');
SELECT * FROM merge_rows('mytable');
Of course you can pass the id and attribute column names as parameters as well and maybe refine the function a bit, this is just to give you an idea.
EDIT : If you're on 9.4 please consider using jsonb datatype, it's much better and gives you room for improvements. You would just need to change the json_* functions to their jsonb_* equivalents.
If you just want this for display purposes, this might be enough:
select id, string_agg(key||' => '||vals, ', ')
from (
select t.id, x.key, string_agg(value, ',') vals
from t
join lateral each(t.attributes) x on true
group by id, key
) t
group by id;
If you are not on 9.4, you can't use the lateral join:
select id, string_agg(key||' => '||vals, ', ')
from (
select id, key, string_agg(val, ',') as vals
from (
select t.id, skeys(t.attributes) as key, svals(t.attributes) as val
from t
) t1
group by id, key
) t2
group by id;
This will return:
id | string_agg
---+-------------------------------------------
1 | color => red,green, shape => square
2 | color => blue,black, flavor => sweat,salty
SQLFiddle: http://sqlfiddle.com/#!15/98caa/2