簡體   English   中英

按 MySQL 和 MariaDB 上的 JSON 數組列排序行

[英]Ordering rows by JSON array column on MySQL & MariaDB

PostgreSQL 允許按arrays對行進行排序。 它比較每個數組的第一個值,然后是第二個值,依此類推(小提琴):

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"
大批
[2, 3, 4]
[2, 4]
[10]
[10, 11]

MySQL 和 MariaDB 上最接近的等價物似乎是JSON ZA3CBC3F9D0CE2F2C1554E1B671D7

MySQL 顯然按長度或多或少隨機地訂購 arrays (小提琴):

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`
大批
[10]
[2, 4]
[10, 11]
[2, 3, 4]

MariaDB 有點按價值排序,但做錯了( 小提琴)。 整數像字符串一樣排序( 2之前的10 )和相同開頭的 arrays 被顛倒( [10, 11]之前[10] ):

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`
大批
[10, 11]
[10]
[2, 3, 4]
[2, 4]

有沒有辦法在 MySQL 和/或 MariaDB 上復制 PostgreSQL 的數組排序?

arrays 可以有任意長度,我不知道最大長度。

我目前看到的唯一解決方法/技巧是將數組連接成一個字符串,並將0 s 的值左填充到相同的長度: 002.004010.011等。

使用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<>小提琴演示

Output:

在此處輸入圖像描述

對我來說它看起來像一個錯誤。 根據文檔

如果兩個 JSON arrays 長度相同且 arrays 中對應位置的值相等,則它們相等。

如果 arrays 不相等,則它們的順序由第一個 position 中存在差異的元素確定。 position 中值較小的數組排在最前面。 如果較短數組的所有值都等於較長數組中的相應值,則較短數組首先排序。

但是ORDER BY看起來根本不遵循這樣的規則。

這是 MySQL 8 和 5.7 的數據庫小提琴

我正在使用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 的另一個觀察結果是,當使用子查詢時, >正在執行類似字符串比較的操作,它需要再次轉換為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

文檔目前說

JSON 值的ORDER BYGROUP BY根據以下原則工作:

[...]

  • 當前不支持對非標量值進行排序,並且會出現警告。

JSON arrays 是非標量值,您的代碼確實會在 MySQL 8 中產生以下警告

等級 代碼 信息
警告 1235 此版本的 MySQL 尚不支持“非標量 JSON 值的排序”

不幸的是,除了等待 MySQL 實現上述功能之外,您無能為力。 或者使用這樣的技巧,它需要 MySQL 8 OPEN_JSON將 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

db<>fiddle 上的演示

你想要負數支持嗎?

你需要浮點數支持嗎?

總是需要輸入很長的 CTE 查詢感到懶惰?

這是您的選擇。 您可以創建這四個函數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 ) data order by json_pad(`array`,max(json_maxdigits(`array`)) over ());

如果您確定最大值不大於 11,則可以跳過 window 部分,只需使用 json_weight 並將基本參數設置為 11(或最大絕對位數為 2):

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`, 11);
--or ) 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 ,否則因為+小於-在 ascii 順序中):

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

您可以使用浮動權重或填充字符串對查詢結果集進行排序。 提供這兩個選項是因為:

  • 當你有很長的 json arrays 時,浮點權重會失去精度,但有浮點支持
  • 填充字符串具有很高的精度,這里設置為 512 位,你甚至可以增加這個數字,但是它們不提供浮點支持(無論如何你沒有要求它)。

如果您使用浮動重量,則必須設置基礎。 您手動設置它或使用最大的數字作為基數,通過使用max(json_max(column_name)) over ()獲得。 如果您使用小於該最大值的基值,您可能會得到不一致的結果,如果您使用的數字太高,您將失去精度。

同樣,當使用填充字符串進行排序時,您必須提供最大絕對值消耗的最大位數(-35 將是 2 個絕對數字)。

注意:這些功能適用於 MariaDB 的早期版本,但仍然不支持json_table function。

如果您不能對數組的長度做出假設,並且您不想使用諸如將數組重新格式化為填充值的字符串之類的技巧,那么您不能在單個查詢中執行此操作。

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]  |
+-----------+

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM