将MySQL中的JSON数组转换为行

问题描述 投票:0回答:8

更新: 现在可以通过 JSON_TABLE 函数在 MySQL 8 中实现:https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html

我喜欢 MySQL 5.7 中的新 JSON 函数,但在尝试将 JSON 中的值合并到普通表结构中时遇到了问题。

获取 JSON、操作并从中提取数组等都很简单。一路都是 JSON_EXTRACT。但是反过来呢,从 JSON 数组到行呢?也许我对现有的 MySQL JSON 功能很感兴趣,但我一直无法弄清楚这一点。

例如,假设我有一个 JSON 数组,想要为数组中的每个元素插入一行及其值?我发现的唯一方法是编写一堆 JSON_EXTRACT(... '$[0]') JSON_EXTRACT(... '$[1]') 等并将它们联合在一起。

或者,假设我有一个 JSON 数组并希望将其 GROUP_CONCAT() 为单个逗号分隔的字符串?

换句话说,我知道我能做到:

SET @j = '[1, 2, 3]';
SELECT GROUP_CONCAT(JSON_EXTRACT(@j, CONCAT('$[', x.n, ']'))) AS val
  FROM   
  (    
    SELECT 0 AS n    
    UNION    
    SELECT 1 AS n    
    UNION    
    SELECT 2 AS n    
    UNION    
    SELECT 3 AS n    
    UNION    
    SELECT 4 AS n    
    UNION    
    SELECT 5 AS n    
  ) x
WHERE x.n < JSON_LENGTH(@j);

但这伤害了我的眼睛。还有我的心。

我怎样才能做这样的事情:

SET @j = '[1, 2, 3]';
SELECT GROUP_CONCAT(JSON_EXTRACT(@j, '$[ * ]'))

...并将数组中的值与 JSON 数组本身连接在一起?

我想我在这里寻找的是某种 JSON_SPLIT ,类似于:

SET @j = '[1, 2, 3]';

SELECT GROUP_CONCAT(val)
FROM
  JSON_SPLIT(JSON_EXTRACT(@j, '$[ * ]'), '$')

如果 MySQL 有一个正确的 STRING_SPLIT(val, 'separator') 表返回函数,我可以破解它(该死的逃脱),但这也不可用。

mysql json database-normalization
8个回答
49
投票

以下是如何在 MySQL 8+ 中使用 JSON_TABLE 执行此操作:

SELECT *
     FROM
       JSON_TABLE(
         '[5, 6, 7]',
         "$[*]"
         COLUMNS(
           Value INT PATH "$"
         )
       ) data;

您还可以将其用作 MySQL 所缺少的通用字符串分割函数(类似于 PG 的 regexp_split_to_table 或 MSSQL 的 STRING_SPLIT),方法是获取分隔字符串并将其转换为 JSON 字符串:

set @delimited = 'a,b,c';

SELECT *
     FROM
       JSON_TABLE(
         CONCAT('["', REPLACE(@delimited, ',', '", "'), '"]'),
         "$[*]"
         COLUMNS(
           Value varchar(50) PATH "$"
         )
       ) data;

47
投票

确实,非规范化为 JSON 不是一个好主意,但有时您需要处理 JSON 数据,并且有一种方法可以将 JSON 数组提取到查询中的行中。

技巧是对临时或内联索引表执行联接,这为 JSON 数组中的每个非空值提供一行。即,如果您有一个值为 0、1 和 2 的表,您将其连接到包含两个条目的 JSON 数组“fish”,则 Fish[0] 匹配 0,生成一行,而 Fish1 匹配 1,产生第二行,但 Fish[2] 为空,因此它与 2 不匹配,并且不会在连接中生成行。您需要索引表中的数字与 JSON 数据中任何数组的最大长度一样多。这有点像黑客,和OP的例子一样痛苦,但它非常方便。

示例(需要 MySQL 5.7.8 或更高版本):

CREATE TABLE t1 (rec_num INT, jdoc JSON);
INSERT INTO t1 VALUES 
  (1, '{"fish": ["red", "blue"]}'), 
  (2, '{"fish": ["one", "two", "three"]}');

SELECT
  rec_num,
  idx,
  JSON_EXTRACT(jdoc, CONCAT('$.fish[', idx, ']')) AS fishes
FROM t1
  -- Inline table of sequential values to index into JSON array
JOIN ( 
  SELECT  0 AS idx UNION
  SELECT  1 AS idx UNION
  SELECT  2 AS idx UNION
  -- ... continue as needed to max length of JSON array
  SELECT  3
  ) AS indexes
WHERE JSON_EXTRACT(jdoc, CONCAT('$.fish[', idx, ']')) IS NOT NULL
ORDER BY rec_num, idx;

结果是:

+---------+-----+---------+
| rec_num | idx | fishes  |
+---------+-----+---------+
|       1 |   0 | "red"   |
|       1 |   1 | "blue"  |
|       2 |   0 | "one"   |
|       2 |   1 | "two"   |
|       2 |   2 | "three" |
+---------+-----+---------+

看起来MySQL团队可能会在MySQL 8中添加一个

JSON_TABLE
功能来使这一切变得更容易。 (http://mysqlserverteam.com/mysql-8-0-labs-json-aggregation-functions/)(MySQL 团队has添加了
JSON_TABLE
函数。)


16
投票

2018年,我为这个案例做了什么。

  1. 准备一个表格,其中只有连续的行数。

    CREATE TABLE `t_list_row` (
    `_row` int(10) unsigned NOT NULL,
    PRIMARY KEY (`_row`)
    ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
    
    INSERT t_list_row VALUES (0), (1), (2) .... (65535) big enough;
    
  2. 将来享受简单的 JSON 数组到行。

    SET @j = '[1, 2, 3]';
    SELECT 
    JSON_EXTRACT(@j, CONCAT('$[', B._row, ']'))
    FROM (SELECT @j AS B) AS A
    INNER JOIN t_list_row AS B ON B._row < JSON_LENGTH(@j);
    

就这样。有点像“克里斯·海因斯”的方式。但您不需要知道数组大小。

好:清晰,简短,简单的代码,不需要知道数组大小,没有循环,不调用其他函数会很快。

不好:您还需要一张有足够行数的表。


4
投票

简单的例子:

select subtotal, sku
from t1,
     json_table(t1.refund_line_items,
                '$[*]' columns (
                    subtotal double path '$.subtotal',
                    sku char(50) path '$.line_item.sku'
                    )
         ) refunds

4
投票

对于 MySQL 8+,请参阅此答案

对于旧版本,我是这样做的:

  1. 创建一个新表
    pseudo_rows
    ,其值从 0 到 99 - 这些将用作键(如果您的数组有超过一百个值,请向
    pseudo_rows
    添加更多值)。

注意:如果您正在运行 MariaDB,则可以跳过此步骤,只需使用伪 Sequence 表(例如

seq_0_to_99
)。

CREATE TABLE `pseudo_rows` (
  `row` int(10) unsigned NOT NULL,
  PRIMARY KEY (`row`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT pseudo_rows VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12), (13), (14), (15), (16), (17), (18), (19), (20), (21), (22), (23), (24), (25), (26), (27), (28), (29), (30), (31), (32), (33), (34), (35), (36), (37), (38), (39), (40), (41), (42), (43), (44), (45), (46), (47), (48), (49), (50), (51), (52), (53), (54), (55), (56), (57), (58), (59), (60), (61), (62), (63), (64), (65), (66), (67), (68), (69), (70), (71), (72), (73), (74), (75), (76), (77), (78), (79), (80), (81), (82), (83), (84), (85), (86), (87), (88), (89), (90), (91), (92), (93), (94), (95), (96), (97), (98), (99)
  1. 对于这个例子,我将使用一个表
    events
    来存储艺术家组:
CREATE TABLE `events` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `artists` json DEFAULT NOT NULL,
  PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

INSERT INTO `events` (`id`, `artists`) VALUES ('1', '[{\"id\": 123, \"name\": \"Pink Floyd\"}]');
INSERT INTO `events` (`id`, `artists`) VALUES ('2', '[{\"id\": 456, \"name\": \"Nirvana\"}, {\"id\": 789, \"name\": \"Eminem\"}]');

获取所有艺术家(每行一位)的查询如下:

SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(events.artists, CONCAT('$[', pseudo_rows.row, '].name'))) AS performer
FROM events
JOIN pseudo_rows
HAVING performer IS NOT NULL

结果集是:

performer
---------
Pink Floyd
Nirvana
Eminem

3
投票

如果您无法使用 JSON_TABLE 函数,但可以使用递归 CTE,则可以执行以下操作:

SET @j = '[1, 2, 3]';
WITH RECURSIVE x AS (
    /* Anchor, start at -1 in case empty array */
    SELECT -1 AS n

    UNION

    /* Append indexes up to the length of the array */
    SELECT x.n + 1
    FROM x
    WHERE x.n < JSON_LENGTH(@j) - 1
)
/* Use the table of indexes to extract each item and do your GROUP_CONCAT */ 
SELECT GROUP_CONCAT(JSON_EXTRACT(@j, CONCAT('$[', x.n, ']')))
FROM x
/* This prevents selecting from empty array */
WHERE x.n >= 0

这会为每个数组项生成一个顺序索引表,您可以使用 JSON_EXTRACT 来获取值。


1
投票

就我而言,

JSON
功能不可用,所以我使用了 hack。 正如 Chris 所提到的,MYSQL 没有
STRING_SPLIT
,但它确实有
substring_index

用于输入

{
    "requestId":"BARBH17319901529",
    "van":"0xxxxx91317508",
    "source":"AxxxS",
    "txnTime":"15-11-2017 14:08:22"
}

您可以使用:

trim(
    replace(
        substring_index(
            substring(input, 
                locate('requestid',input) 
                    + length('requestid') 
                    + 2), ',', 1), '"', '')
) as Requestid`

输出将是:

BARBH17319901529

您可以根据您的要求进行修改。


0
投票

我正在编写一份报告,其中一列中有一个很大的 json 数组列表。我修改了数据模型以存储 1 到 * 的关系,而不是将所有内容存储在一列中。为了完成这个过程,我不得不在存储过程中使用一段时间,因为我不知道最大大小:

DROP PROCEDURE IF EXISTS `test`;

DELIMITER #

CREATE PROCEDURE `test`()
PROC_MAIN:BEGIN
DECLARE numNotes int;
DECLARE c int;
DECLARE pos varchar(10);

SET c = 0;
SET numNotes = (SELECT 
ROUND (   
        (
            LENGTH(debtor_master_notes)
            - LENGTH( REPLACE ( debtor_master_notes, "Id", "") ) 
        ) / LENGTH("Id")        
    ) AS countt FROM debtor_master
order by countt desc Limit 1);

DROP TEMPORARY TABLE IF EXISTS debtorTable;
CREATE TEMPORARY TABLE debtorTable(debtor_master_id int(11), json longtext, note int);
WHILE(c <numNotes) DO
SET pos = CONCAT('$[', c, ']');
INSERT INTO debtorTable(debtor_master_id, json, note)
SELECT debtor_master_id, JSON_EXTRACT(debtor_master_notes, pos), c+1
FROM debtor_master
WHERE debtor_master_notes IS NOT NULL AND debtor_master_notes like '%[%' AND JSON_EXTRACT(debtor_master_notes, pos) IS NOT NULL AND JSON_EXTRACT(debtor_master_notes, pos) IS NOT NULL;
SET c = c + 1;
END WHILE;
SELECT * FROM debtorTable;
END proc_main #

DELIMITER ;
© www.soinside.com 2019 - 2024. All rights reserved.