2

Consider this table:

DROP TABLE IF EXISTS `example`;
CREATE TABLE `example` (
  `id` int NOT NULL AUTO_INCREMENT,
  `content` json NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

And these rows:

INSERT INTO example(content) 
VALUES (
  '[  { "date": "1617210148", "name": "John",  "status": "0" },
{ "date": "1617210148", "name": "Jack",  "status": "0" },
{ "date": "1617210148", "name": "Henry",  "status": "0" }]'
);

I'd like to update the value of the key status where name = Jack to 1

The result would be: { "date": "1617210148", "name": "Jack", "status": "1" }

How can I do this using JSON_REPLACE() or JSON_SET() in a SQL query (I'm aiming for a partial update since I'm using MySQL 8.0.25)?

Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
Chuck
  • 98
  • 2
  • 10
  • Out of curiosity, why did you choose to use JSON for this data? It's an array of similar documents (same fields in each document), which would be fine as a second table with a set of rows instead of an array, and normal columns instead of object fields. Then updating the name on a single row would be easy. – Bill Karwin Jun 13 '21 at 17:21
  • I plan to parse the array to populate html rows in a table managed with datatables.net javascript to manage client side filtering and pagination. I prefer to select one big field than let my users do a SELECT of hundreds of rows amongst hundreds of thousands of them. The only manipulation of the data will be the updating of this key value through Ajax. Other than this I don't need to query this data. – Chuck Jun 13 '21 at 17:36
  • You can create an endpoint that returns the collected data as a JSON document, even if the rows are stored in a normalized way in the database. For example, I've done this with JSON_ARRAYAGG() and JSON_OBJECT(). This makes it easy for the client side to use it, but keeps it in a format in the db that is more efficient. – Bill Karwin Jun 13 '21 at 17:39
  • Ultimately I don't really care about having json formatted data, sorry if I was not clear. What I meant is I prefer to store all the data in one field, be it JSON, to query instead of having SELECT queries in a table that would have hundreds of thousands of rows if I had to normalize the data. Caching is not really an option too because of the frequently updating of the key I mentioned above. In terms of speed and query cost I assume using a single json field is better... That is if I can find a way to update the nested key's value. – Chuck Jun 13 '21 at 17:52

1 Answers1

4

This is very awkward, nearly impossible with MySQL's JSON functions.

You can use JSON_REPLACE() or JSON_SET(), but both require that you know the path to the field you want to change. So in this case, we can see that the array element is $[1] but if you didn't know that, you couldn't use this solution.

mysql> select json_pretty(json_replace(content, '$[1].status', '1')) as j 
  from example\G
*************************** 1. row ***************************
j: [
  {
    "date": "1617210148",
    "name": "John",
    "status": "0"
  },
  {
    "date": "1617210148",
    "name": "Jack",
    "status": "1"
  },
  {
    "date": "1617210148",
    "name": "Henry",
    "status": "0"
  }
]

The question like yours has come up before on Stack Overflow, for example JSON update single value in MySQL table. The solution in that case depends on you knowing which array element your pseudo-record exists in.

You can get the path to a JSON element with JSON_SEARCH(), but you can only search by value, not by a key/value pair. If "Jack" occurs in some other field, that would also be found.

mysql> select json_unquote(json_search(content, 'one', 'Jack')) as path from example;
+-----------+
| path      |
+-----------+
| $[1].name |
+-----------+

To search for a key/value pair, you need to use JSON_TABLE() and that requires you upgrade to MySQL 8.0. And that doesn't tell you the path to the element, it only allows you to return a specific row out of the array.

mysql> select j.* from example cross join json_table(content, '$[*]' columns(
  date int unsigned path '$.date',
  name varchar(10) path '$.name',
  status int path '$.status')
) as j where name = 'Jack';
+------------+------+--------+
| date       | name | status |
+------------+------+--------+
| 1617210148 | Jack |      0 |
+------------+------+--------+

Here's a trick: You can extract the name field, and that turns into an array of those values:

mysql> select json_extract(content, '$[*].name') as a from example;
+---------------------------+
| a                         |
+---------------------------+
| ["John", "Jack", "Henry"] |
+---------------------------+

Then you can search that array to get the array position:

mysql> select json_search(json_extract(content, '$[*].name'), 'one', 'Jack') as root from example;
+--------+
| root   |
+--------+
| "$[1]" |
+--------+

Some unquoting and adding .status and you can get the full path to the field you want to update:

mysql> select concat(json_unquote(json_search(json_extract(content, '$[*].name'), 'one', 'Jack')), '.status') as path from example;
+-------------+
| path        |
+-------------+
| $[1].status |
+-------------+

Now use it in a JSON_SET() call:

mysql> select json_pretty( 
    json_set(content,
     concat(json_unquote(json_search(json_extract(content, '$[*].name'), 'one', 'Jack')), '.status'), 
     '1')) as newcontent 
  from example\G
*************************** 1. row ***************************
newcontent: [
  {
    "date": "1617210148",
    "name": "John",
    "status": "0"
  },
  {
    "date": "1617210148",
    "name": "Jack",
    "status": "1"
  },
  {
    "date": "1617210148",
    "name": "Henry",
    "status": "0"
  }
]

Using this in an UPDATE looks like this:

mysql> update example set content = json_set(content, concat(json_unquote(json_search(json_extract(content, '$[*].name'), 'one', 'Jack')), '.status'), '1');

That's a long way to go. Now compare how difficult that is to:

UPDATE content SET status = 1 WHERE name = 'Jack';

Storing data in a JSON document when you eventually want to use SQL expressions to search or update individual fields within the JSON document is a costly mistake. It increases the complexity of any code you write to do it, and the developer who needs to take over maintenance of your code after you have moved on to another project will curse your name.

Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
  • Thank you Bill for taking the time to help me. You code works like a charm, very much appreciated ! I agree with the complexity of the query comparing to a simple row's UPDATE. I'm going to take your remark in consideration and will run some stess test on a dummy table to monitor the performance of the 2 solutions. – Chuck Jun 13 '21 at 21:35
  • @BillKarwin in update query (json_extract(content, '$[*].name'), 'one', 'Jack') --> what's 'ONE' mean there ? – SYED MUSTAFA HUSSAIN Oct 22 '21 at 13:21
  • Good question! I will give you a suggestion. It is actually how I learned to use these JSON functions. It doesn't take long. I am sure it will lead to your success too. I read the [documentation for the JSON_SEARCH() function](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-search). – Bill Karwin Oct 22 '21 at 15:59