将MySQL中的JSON数组转换为行

Convert JSON array in MySQL to rows

更新:现在可以通过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()变成一个逗号分隔的字符串?

换句话说,我知道我可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);

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

我该怎么做:

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

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

我猜我在这里寻找的是某种类似于JSON_SPLIT的代码:

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

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

如果MySQL具有适当的STRING_SPLIT(val,'separator')表返回函数,则可以对其进行破解(该死的该死),但这也不可用。


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

技巧是在索引的临时表或内联表上执行联接,从而为JSON数组中的每个非空值提供一行。即,如果您有一个值分别为0、1和2的表,并连接到具有两个条目的JSON数组" fish",则fish [0]匹配0,结果为一行,fish1匹配1,结果为第二行,但是fish [2]为null,因此它与2不匹配,并且在联接中不产生任何行。索引表中需要的数字与JSON数据中任何数组的最大长度一样多。这有点骇人听闻,和OP的示例一样痛苦,但是非常方便。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

结果是:

1
2
3
4
5
6
7
8
9
+---------+-----+---------+
| 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团队添加了JSON_TABLE函数。)


这是在MySQL 8+中使用JSON_TABLE的方法:

1
2
3
4
5
6
7
8
9
SELECT *
     FROM
       JSON_TABLE(
         '[5, 6, 7]',
        "$[*]"
         COLUMNS(
           Value INT PATH"$"
         )
       ) data;

您还可以通过使用带分隔符的字符串并将其转换为JSON字符串,将其用作MySQL否则缺少的常规字符串拆分函数(类似于PG的regexp_split_to_table或MSSQL的STRING_SPLIT):

1
2
3
4
5
6
7
8
9
10
11
set @delimited = 'a,b,c';

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

在2018年。我要为这种情况做些什么。

  • 准备一个表,该表的行必须连续不断。

    1
    2
    3
    4
    5
    6
    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;
  • 将来可以在行中使用简单的JSON数组。

    1
    2
    3
    4
    5
    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);
  • 这样就像"克里斯·海恩斯"那样。但是您不需要知道数组大小。

    良好:清晰,简短的代码,无需知道数组大小,无需循环,无需调用其他函数将很快。

    坏:您还需要一个具有足够行的表。


  • 创建一个新表pseudo_rows,其值介于0到99之间-这些值将用作键(如果数组具有多个值,则将更多值添加到pseudo_rows中)。
  • 注意:如果您正在运行MariaDB,则可以跳过此步骤,而只需使用伪序列表(例如seq_0_to_99)。

    1
    2
    3
    4
    5
    6
    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)
  • 对于此示例,我将使用表events来存储艺术家组:
  • 1
    2
    3
    4
    5
    6
    7
    8
    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\"}]');

    获取所有艺术家的查询,每行一个,如下所示:

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

    结果集是:

    1
    2
    3
    4
    5
    performer
    ---------
    Pink Floyd
    Nirvana
    Eminem

    在我的案例中,JSON功能不可用,因此我使用了hack。
    如Chris MYSQL所述,它没有STRING_SPLIT,但是确实有substring_index

    对于输入

    1
    2
    3
    4
    5
    6
    {
       "requestId":"BARBH17319901529",
       "van":"0xxxxx91317508",
       "source":"AxxxS",
       "txnTime":"15-11-2017 14:08:22"
    }

    您可以使用:

    1
    2
    3
    4
    5
    6
    7
    8
    trim(
        replace(
            substring_index(
                substring(input,
                    locate('requestid',input)
                        + length('requestid')
                        + 2), ',', 1), '"', '')
    ) as Requestid`

    输出将是:

    1
    BARBH17319901529

    您可以根据需要进行修改。


    简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    select subtotal, sku
    from t1,
         json_table(t1.refund_line_items,
                    '$[*]' columns (
                        subtotal double path '$.subtotal',
                        sku char(50) path '$.line_item.sku'
                        )
             ) refunds

    我正在一份报告中工作,其中一列中有一个很大的json数组列表。我修改了数据模型以将关系1与*进行存储,而不是将所有内容存储在单个列中。为了执行此过程,由于不知道最大大小,我不得不在存储过程中使用一段时间:

    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
    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 ;