将 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 中的值合并到正常的 table结构。

抓取 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') table 返回函数,我可以破解它(该死的逃避),但那不可用要么。

确实反规范化成JSON不是一个好主意,但是有时候需要处理JSON数据,有办法把一个JSON数组抽取出来查询中的行。

诀窍是对索引的临时或内联 table 执行连接,这会为 JSON 数组中的每个 non-null 值提供一行。也就是说,如果你有一个值为 0、1 和 2 的 table,你将它加入到一个有两个条目的 JSON 数组“fish”,那么 fish[0] 匹配 0,结果是一行, fish1 匹配 1,产生第二行,但 fish[2] 为空,因此它不匹配 2 并且不会在连接中产生一行。您需要索引 table 中的数字与 JSON 数据中任何数组的最大长度一样多。这有点 hack,和 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/) (The MySQL team has added a JSON_TABLE 函数。)

在我的案例中,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

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

我正在处理一份报告,其中一列中有一个很大的 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 ;

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

  1. 准备一个table,只在行中连续编号。

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

对于这种方式。有点像 'Chris Hynes' 方式。但您不需要知道数组大小。

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

差:您还需要一个 table 行数足够的。

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

对于 MySQL 8+,请参阅

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

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

注意:如果您是 运行 MariaDB,则可以跳过此步骤并简单地使用伪序列 tables(例如 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. 对于这个例子,我将使用 table 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

简单示例:

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

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

在此处使用此引用 https://dba.stackexchange.com/questions/190527/list-json-array-in-mysql-as-rows/243671#243671

在我的 MySQL table Customers 中输入 JSON 的列 AddressIdentifiers,数据样本如下所示:

[
  {
    "code": "123",
    "identifier": "0219d5780f6b",
    "type": "BILLING",
    "info": null
  },
  {
    "code": "240",
    "identifier": "c81aaf2c5a1f",
    "type": "DELIVERY",
    "info": null
  }
]

要有这样的输出

Identifier   AddressType
------------------------
0219d5780f6b  BILLING
c81aaf2c5a1f  DELIVERY

此解决方案适用于 MySQL 5.7,您必须在其中手动完成工作。在 MySQL 8.0+ 的情况下,您可以简单地使用 JSON_TABLE

SELECT
    JSON_EXTRACT(C.AddressIdentifiers, CONCAT('$[', Numbers.N - 1, '].Identifier')) AS Identifier,
    JSON_EXTRACT(C.AddressIdentifiers, CONCAT('$[', Numbers.N - 1, '].AddressType')) AS AddressType,
FROM
(
    SELECT @row := @row + 1 AS N FROM 
    (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) T2,
    (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) T1, 
    (SELECT @row:=0) T0
) Numbers -- Natural numbers from 1 to 100
INNER JOIN Customers C ON Numbers.N < JSON_LENGTH(C.AddressIdentifiers)