【问题标题】:Convert JSON array in MySQL to rows将 MySQL 中的 JSON 数组转换为行
【发布时间】:2017-02-15 19:33:46
【问题描述】:

更新:现在可以通过 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') 表返回函数,我可以破解它(该死的转义),但这也不可用。

【问题讨论】:

  • 我不认为你可以这样做,出于同样的原因你不能这样做SPLIT_STRING():查询不能从没有连接的输入表的同一行创建多行。
  • 是的,你可能是对的。我曾假设支持表值函数,因为几乎所有其他 DBMS 都拥有它们。显然 MySQL 是个奇怪的人。例如,SQL Server 有一个非常好的 STRING_SPLIT:msdn.microsoft.com/en-us/library/mt684588.aspx。 Postgress 甚至在 regexp_split_to_table 中有一个正则表达式拆分。啊,MySQL...
  • 对。 MySQL 除了表之外没有类似数组的数据结构。 JSON 函数不应被视为非规范化架构的一揽子许可。
  • 好吧,如果有像其他 DBMS 那样的表值函数,那么它会返回一个表,而不是其他类似数组的结构......你可以从函数中选择。

标签: mysql json database-normalization


【解决方案1】:

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

诀窍是在临时或内联索引表上执行连接,它为 JSON 数组中的每个非空值提供一行。即,如果您有一个值为 0、1 和 2 的表,您将其连接到具有两个条目的 JSON 数组“fish”,则 fish[0] 匹配 0,产生一行,而 fish1 匹配 1,产生第二行,但 fish[2] 为空,因此它与 2 不匹配,并且不会在连接中产生一行。您需要索引表中的数字与 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/)(MySQL 团队添加了JSON_TABLE 函数。)

【讨论】:

  • 是的,这与我在问题中的第一个示例基本相同。有效,但很难看,您需要多次复制 UNION。这里的基本问题是 MySQL 不支持表值函数,不支持内置函数,不支持用户定义。希望他们在 MySQL 8 中添加 JSON_TABLE 和 STRING_SPLIT,并允许其他用户定义的表值函数来填补空白。
  • 我遇到了类似的情况,我需要将 JSON 数组转换为例如 {"2018Apr": "1000", "2018Jun": "7000", "2018May": "2000"} 到行,例如: Date Price 2018Apr 1000 2018May 2000 2018Jun 7000 但我面临的问题是我的数组的长度是可变的(即可以是数组中的“n”个元素)。在这种情况下,您会推荐什么? @JimTheFrog
  • @Veer3383,只要您在内联索引表中有足够的记录来匹配 JSON 数组的最大长度,上面的 hack 就可以工作。而且您不必使用内联表。例如,如果您的数组中可能有多达 5,000 个元素,请预先生成一个包含 0 到 4,999 值的单列索引表,并在 JOIN 中使用它。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    为此。有点像“克里斯海因斯”的方式。但你不需要知道数组大小。

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

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

    【讨论】:

    • 这样的“整数”表在 MySQL 中总是有用的(因为它不能像 Oracle 的 AFAIR 那样动态生成),但是你有没有找到一种方法来使用 CONCAT 部分?
    • @Xenos 到目前为止,没有更好的方法,因为它就是这样工作的。 JSON_EXTRACT(json_doc, path[, path] ...)
    • 如果您手头有足够大的表格,那么insert into numbers select @row := @row + 1 from big_table join (select @row:=0) t2 limit 50; 将为您生成数字表格。
    • 这里是用于生成这些t_list_row 不同100010000100000 大小的表的sql 查询:gist.github.com/milosb793/812d5e7c33a0bfd37ed2c3dcad0cea1c
    【解决方案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
    

    【讨论】:

      【解决方案5】:

      对于 MySQL 8+,请参阅this answer

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

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

      注意:如果您正在运行 MariaDB,则可以跳过此步骤并简单地使用伪序列表(例如 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
      

      【讨论】:

        【解决方案6】:

        在我的情况下,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
        

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

        【讨论】:

        • 这假设 requestid 不会出现在序列化数据的早期(我不会依赖 JSON 序列化程序的字段顺序,也就是 "source": "AxxxSrequestid" 可能出现在 "requestId":"BAR..." 部分之前并快速打破这个解析器
        【解决方案7】:

        如果您不能使用 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 获取值。

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

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

            在我的 MySQL 表 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)
            

            【讨论】:

              猜你喜欢
              • 2016-12-11
              • 2014-01-23
              • 2018-05-30
              • 2017-04-05
              • 2015-02-09
              • 1970-01-01
              • 1970-01-01
              • 2018-01-09
              • 1970-01-01
              相关资源
              最近更新 更多