【问题标题】:Ordering rows by JSON array column on MySQL & MariaDB在 MySQL 和 MariaDB 上按 JSON 数组列排序行
【发布时间】:2022-01-06 11:24:35
【问题描述】:

PostgreSQL 允许按arrays 对行进行排序。它比较每个数组的第一个值,然后是第二个值,依此类推 (fiddle):

select array[2, 4] as "array"
union
select array[10] as "array"
union
select array[2, 3, 4] as "array"
union
select array[10, 11] as "array"
order by "array"
array
[2, 3, 4]
[2, 4]
[10]
[10, 11]

MySQL 和 MariaDB 上最接近的等价物似乎是 JSON arrays

MySQL 显然 按长度 或多或少地随机排序数组 (fiddle):

select json_array(2, 4) as `array`
union
select json_array(10) as `array`
union
select json_array(2, 3, 4) as `array`
union
select json_array(10, 11) as `array`
order by `array`
array
[10]
[2, 4]
[10, 11]
[2, 3, 4]

MariaDB 有点按价值排序,但做错了 (fiddle)。整数像字符串一样排序(2 之前的10)并且开头相同的数组是相反的([10] 之前的[10, 11]):

select json_array(2, 4) as `array`
union
select json_array(10) as `array`
union
select json_array(2, 3, 4) as `array`
union
select json_array(10, 11) as `array`
order by `array`
array
[10, 11]
[10]
[2, 3, 4]
[2, 4]

有没有办法在 MySQL 和/或 MariaDB 上复制 PostgreSQL 的数组排序?

数组可以有任意长度,我不知道最大长度。

目前我看到的唯一解决方法/hack 是将 concatenating 数组转换为字符串,然后用 0s 将值左填充到相同的长度:002.004010.011 等。

【问题讨论】:

标签: mysql json mariadb sql-order-by mysql-json


【解决方案1】:

这里有一个解决方案:

  • 负数支持

  • 支持浮点数

  • 避免输入长 CTE 查询*

* 这里的好处是当你必须经常输入查询时,CTE 仍然是一个不错的选择

您只需select * from data order by json_weight(json_column,base_value);

为了能够做到这一点,请创建json_maxjson_weightjson_maxdigitsjson_pad 这四个函数,并在 order by 子句中使用它们:

delimiter //
create or replace function json_max(j json) returns float deterministic
  begin
    declare l int;
    declare mv float;
    declare v float;
    set l = json_length(j);
    for i in 0..l-1 do
      set v = abs(json_value(j,concat('$[',i,']')));
      if (mv is null) or (v > mv) then
        set mv = v;
      end if;
    end for;
    return mv;
  end
//
create or replace function json_weight(j json, base int) returns float deterministic
  begin
    declare l int;
    declare w float;
    set w = 0;
    set l = json_length(j);
    for i in 0..l-1 do
      set w = w + pow(base,-i) * json_value(j,concat('$[',i,']'));
    end for;
    return w;
  end
//
create or replace function json_maxdigits(j json) returns int deterministic
  return length(cast(floor(abs(json_max(j))) as char(16)))
//
create or replace function json_pad(j json, digitcount int) returns varchar(512) deterministic
  begin
    declare l int;
    declare v int;
    declare w varchar(512);
    set w = '';
    set l = json_length(j);
    for i in 0..l-1 do
      set v = json_value(j,concat('$[',i,']'));
      set w = concat(w, if(v>=0,'0','-'), lpad(v, digitcount, 0));
    end for;
    return w;
  end
//
delimiter ;

然后按如下方式使用它们:

select * from (
select json_array(2, 4) as `array`
union
select json_array(10) as `array`
union
select json_array(2, 3, 4) as `array`
union
select json_array(10, 11) as `array`
) data order by json_weight(`array`,max(json_max(`array`)) over ());
-- or if you know that 11 is the max value:
--) data order by json_weight(`array`,11);
-- alternative method:
--) data order by json_pad(`array`,max(json_maxdigits(`array`)) over ());
-- alternative method and you know that only two digits are enough to represent numbers in the array:
--) data order by json_pad(`array`,2);

解释:

json_max 为您提供 json_array 中的最大绝对值:

select json_max('[22,33,-55]'); -- 55

json_maxdigits 为您提供 json_array 中的最大位数(绝对数):

select json_maxdigits('[21,151,-4]'); -- 3

json_weight 将您的 json 数组转换为等效浮点值,其中数组的每个数字都等效于您指定为参数的基数中的一个数字:

select json_weight('[1,3,5,7]', 10); -- 1.357
select json_weight('[1,0,1]', 2); -- 1.25 (like binary floats)

json_pad 将您的 json 数组转换为一串零填充数字,其中包含减号信号作为额外符号以保证负序(或额外符号 0 否则因为 + 小于 - in ascii 顺序):

select json_pad('[1,-3,15,7]', 2); --'001-03015007'

您可以使用浮动权重或填充字符串对查询结果集进行排序。提供这两个选项的原因是:

  • 当您有长 json 数组但支持浮点数时,浮点权重会失去精度
  • 填充字符串具有很高的精度,此处设置为 512 位,您甚至可以增加此数字,但它们不提供浮点支持(无论如何您都没有要求)。

如果您使用浮动重量,则必须设置基础。您可以手动设置它或使用最大的数字作为基数,通过使用max(json_max(column_name)) over () 获得。如果您使用小于该最大值的基值,则可能会得到不一致的结果,如果您使用的数字太高,则会失去精度。

类似地,当使用填充字符串进行排序时,您必须提供最大绝对值消耗的最大位数(-35 将是 2 个绝对数字)。

注意:这些函数适用于早期版本的 MariaDB,但仍不支持 json_table 函数。

【讨论】:

    【解决方案2】:

    documentation currently says that:

    ORDER BYGROUP BY 的 JSON 值根据这些工作 原则:

    [...]

    • 目前不支持对非标量值进行排序,并且会出现警告。

    JSON 数组是非标量值,您的代码 does produce the following warning in MySQL 8:

    Level Code Message
    Warning 1235 This version of MySQL doesn't yet support 'sorting of non-scalar JSON values'

    不幸的是,除了等待 MySQL 实现上述功能之外,您无能为力。或者使用这样的 hack,它需要 MySQL 8 JSON_TABLE 将 json 数组拆分为行,然后填充值并再次将它们分组连接以创建可排序的字符串:

    select *, (
        select group_concat(lpad(jt.v, 8, '0') order by jt.i)
        from json_table(t.array, '$[*]' columns(i for ordinality, v int path '$')) as jt
    ) as sort_str
    from t
    order by sort_str
    

    Demo on db<>fiddle

    【讨论】:

      【解决方案3】:

      如果您无法对数组的长度做出假设,并且不想使用诸如将数组重新格式化为填充值字符串等技巧,那么您不能在单个查询中执行此操作。

      ORDER BY 子句中的表达式必须在查询开始读取任何行之前修复,就像查询的其他部分一样,例如选择列表的列。

      但您可以使用查询来生成动态 SQL 查询,在 ORDER BY 子句中包含足够多的术语来说明最大长度数组。

      演示:

      create table mytable (array json);
      
      insert into mytable values  ('[2, 3, 4]'), ('[2, 4]'), ('[10]'), ('[10, 11]');
      
      select max(json_length(array)) as maxlength from mytable;
      +-----------+
      | maxlength |
      +-----------+
      |         3 |
      +-----------+
      

      然后进行递归 CTE,生成从 0 到最大长度减 1 的整数:

      with recursive array as (
          select max(json_length(array)) as maxlength from mytable
      ),
      num as (
          select 0 as num
          union
          select num+1 from num cross join array where num < maxlength-1
      )   
      select num from num;
      +------+
      | num  |
      +------+
      |    0 |
      |    1 |
      |    2 |
      +------+
      

      这些整数可用于格式化表达式以在ORDER BY 子句中使用:

      with recursive array as (
          select max(json_length(array)) as maxlength from mytable
      ),
      num as (
          select 0 as num
          union
          select num+1 from num cross join array where num < maxlength-1
      )
      select concat('CAST(JSON_EXTRACT(array, ', quote(concat('$[', num, ']')), ') AS UNSIGNED)') AS expr from num;
      +-----------------------------------------------+
      | expr                                          |
      +-----------------------------------------------+
      | CAST(JSON_EXTRACT(array, '$[0]') AS UNSIGNED) |
      | CAST(JSON_EXTRACT(array, '$[1]') AS UNSIGNED) |
      | CAST(JSON_EXTRACT(array, '$[2]') AS UNSIGNED) |
      +-----------------------------------------------+
      

      然后使用这些表达式生成一个 SQL 查询:

      with recursive array as (
          select max(json_length(array)) as maxlength from mytable
      ),
      num as (
          select 0 as num
          union
          select num+1 from num cross join array where num < maxlength-1
      ),
      orders as (
          select num, concat('CAST(JSON_EXTRACT(array, ', quote(concat('$[', num, ']')), ') AS UNSIGNED)') AS expr from num
      )
      select concat(
          'SELECT array FROM mytable\nORDER BY \n  ',
          group_concat(expr order by num separator ',\n  '),
          ';'
      ) as query
      from orders\G
      
      query: SELECT array FROM mytable
      ORDER BY 
        CAST(JSON_EXTRACT(array, '$[0]') AS UNSIGNED),
        CAST(JSON_EXTRACT(array, '$[1]') AS UNSIGNED),
        CAST(JSON_EXTRACT(array, '$[2]') AS UNSIGNED);
      

      最后,捕获该查询的结果,并将其作为新的动态 SQL 查询执行:

      SELECT array FROM mytable
      ORDER BY 
        CAST(JSON_EXTRACT(array, '$[0]') AS UNSIGNED),
        CAST(JSON_EXTRACT(array, '$[1]') AS UNSIGNED),
        CAST(JSON_EXTRACT(array, '$[2]') AS UNSIGNED);
      +-----------+
      | array     |
      +-----------+
      | [2, 3, 4] |
      | [2, 4]    |
      | [10]      |
      | [10, 11]  |
      +-----------+
      

      【讨论】:

        【解决方案4】:

        对我来说它看起来像一个错误。根据docs

        如果两个 JSON 数组的长度和值相同,则它们相等 数组中对应的位置相等。

        如果数组不相等,则它们的顺序由元素决定 在有差异的第一个位置。数组与 该位置的较小值首先排序。如果所有的值 较短的数组等于较长的数组中的相应值 数组,较短的数组先排序。

        ORDER BY 看起来根本不遵守这些规则。

        这是 MySQL 8 和 5.7 的 DB fiddle

        我正在使用 CROSS JOIN 和显式比较来获得预期的排序。

        SELECT f.`array`, SUM(f.`array` > g.`array`) cmp
        FROM jsons f
        CROSS JOIN jsons g
        GROUP BY f.`array`
        ORDER BY cmp
        ;
        

        MySQL 5.7 的另一个观察结果是,当使用子查询时,&gt; 正在执行类似字符串比较的操作,它需要再次转换为 JSON 才能获得正确的结果,而 MySQL8 不需要这样做。

        SELECT f.`array`, SUM(CAST(f.`array` AS JSON) > CAST(g.`array` AS JSON)) cmp
        FROM (
         select json_array(2, 4) as `array`
         union
         select json_array(10) as `array`
         union
         select json_array(2, 3, 4) as `array`
         union
         select json_array(10, 11) as `array`
        ) f
        CROSS JOIN (
         select json_array(2, 4) as `array`
         union
         select json_array(10) as `array`
         union
         select json_array(2, 3, 4) as `array`
         union
         select json_array(10, 11) as `array`
        ) g
        GROUP BY f.`array`
        ORDER BY cmp
        ;
        

        以上在 MariaDB 中不起作用

        https://mariadb.com/kb/en/incompatibilities-and-feature-differences-between-mariadb-106-and-mysql-80/

        在 MySQL 中,JSON 是根据 json 值进行比较的。在 MariaDB JSON 中 字符串是普通字符串,作为字符串进行比较。

        以下查询适用于 MariaDB

        WITH RECURSIVE jsons AS (
         select json_array(2, 4) as `array`
         union
         select json_array(10) as `array`
         union
         select json_array(2, 3, 4) as `array`
         union
         select json_array(10, 11) as `array`
        ),
        maxlength AS (
         SELECT MAX(JSON_LENGTH(`array`)) maxlength
         FROM jsons
        ),
        numbers AS (
         SELECT 0 AS n
         FROM maxlength
         UNION ALL
         SELECT n + 1
         FROM numbers
         JOIN maxlength ON numbers.n < maxlength.maxlength - 1
        ),
        expanded AS (
         SELECT a.`array`, b.n, JSON_EXTRACT(a.`array`, CONCAT('$[', b.n, ']')) v
         FROM jsons a
         CROSS JOIN numbers b
        ),
        maxpadding AS (
         SELECT MAX(LENGTH(v)) maxpadding
         FROM expanded
        )
        SELECT a.`array`
        FROM expanded a
        CROSS JOIN maxpadding b
        GROUP BY a.`array`
        ORDER BY GROUP_CONCAT(LPAD(a.v, b.maxpadding, '0') ORDER BY a.n ASC)
        

        【讨论】:

        • 感谢文档参考。 MySQL 听起来与我正在寻找的行为完全一样。 MariaDB 解释了我得到的结果。
        • 他们在同一页面上的文档中承认,对于非标量值没有实现 order by。 MariaDB 也很可能支持json_table,因此不需要递归方法。
        • @SalmanA 好点
        【解决方案5】:

        使用JSON_VALUE

        WITH cte AS (
          select json_array(2, 4) as `array`
          union
          select json_array(10) as `array`
          union
          select json_array(2, 3, 4) as `array`
          union
          select json_array(10, 11) as `array`
        )
        select *
        from cte
        order by CAST(JSON_VALUE(`array`, '$[0]') AS INT),
                 CAST(JSON_VALUE(`array`, '$[1]') AS INT),
                 CAST(JSON_VALUE(`array`, '$[2]') AS INT)
                -- ...;
        
        
        -- MySQL 8.0.21+
        select *
        from cte
        order by
         JSON_VALUE(`array`, '$[0]' RETURNING SIGNED),
         JSON_VALUE(`array`, '$[1]' RETURNING SIGNED),
         JSON_VALUE(`array`, '$[2]' RETURNING SIGNED)
        

        db<>fiddle demo

        输出:

        【讨论】:

        • 这种方法需要我知道最大数组长度,对吧?
        • @JonasStaudenmeir 通常是的,但是如果您尝试访问不存在的索引,它将返回 null。 `CAST(JSON_VALUE(array, '$[2]') AS INT)` 用于 json_array(10)。根据需要,您最多可以设置 10 个主要元素。
        • 不幸的是,我不知道我的用例中的最大数组长度。数组可能包含数十个或数百个值,并且查询需要对所有这些值都有效。
        猜你喜欢
        • 2018-03-02
        • 2018-07-26
        • 1970-01-01
        • 2014-12-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多