Агрегация по массиву JSONB фиксированного размера в PostgreSQL

я изо всех сил делаю агрегации на поле JSONB в базе данных PostgreSQL. Это, вероятно, проще объяснить примером, поэтому, если создать и заполнить таблицу с именем analysis С 2 столбцами (id и analysis) следующим образом: -

create table analysis (
  id serial primary key,
  analysis jsonb
);

insert into analysis 
  (id, analysis) values
  (1,  '{"category" : "news",    "results" : [1,   2,  3,  4,  5 , 6,  7,  8,  9,  10,  11,  12,  13,  14, null, null]}'),
  (2,  '{"category" : "news",    "results" : [11, 12, 13, 14, 15, 16, 17, 18, 19,  20,  21,  22,  23,  24, null,   26]}'),
  (3,  '{"category" : "news",    "results" : [31, 32, 33, 34, 35, 36, 37, 38, 39,  40,  41,  42,  43,  44,   45,   46]}'),
  (4,  '{"category" : "sport",   "results" : [51, 52, 53, 54, 55, 56, 57, 58, 59,  60,  61,  62,  63,  64,   65,   66]}'),
  (5,  '{"category" : "sport",   "results" : [71, 72, 73, 74, 75, 76, 77, 78, 79,  80,  81,  82,  83,  84,   85,   86]}'),
  (6,  '{"category" : "weather", "results" : [91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104,  105,  106]}');

Как видите,analysis поле JSONB всегда содержит 2 атрибута category и results. Атрибут results всегда будет содержать массив фиксированной длины размером 16. Я использовал различные функции, такие как jsonb_array_elements но что Я пытаюсь сделать следующее: -

  1. группа по анализу - > 'категория'
  2. среднее значение каждого элемента массива

когда я хочу, чтобы оператор возвращал 3 строки, сгруппированные по категориям (т. е. news, sport и weather) и массив фиксированной длины 16, содержащий средние значения. Чтобы еще больше все усложнить, если есть nulls в массиве, тогда мы должны игнорировать их (т. е. мы не просто суммируем и усреднение по количеству строк). Результат должен выглядеть примерно так: -

 category  | analysis_average
-----------+--------------------------------------------------------------------------------------------------------------
 "news"    | [14.33, 15.33, 16.33, 17.33, 18.33, 19.33, 20.33, 21.33, 22.33, 23.33, 24.33, 25.33, 26.33, 27.33,  45,  36]
 "sport"   | [61,       62,    63,    64,    65,    66,    67,    68,    69,    70,    71,    72,    73,    74,  75,  76]
 "weather" | [91,       92,    93,    94,    95,    96,    97,    98,    99,    00,   101,   102,   103,   104, 105, 106]

Примечание: уведомления 45 и 36 в последних 2 массивах itmes в 1-й строке, которая иллюстрирует игнорирование nullss.

я рассматривал возможность создания представления, которое разбило массив на 16 столбцов, т. е.

create view analysis_view as
select a.*,
(a.analysis->'results'->>0)::int as result0,
(a.analysis->'results'->>1)::int as result1
/* ... etc for all 16 array entries .. */
from analysis a;

это мне кажется крайне безвкусным и удаляет преимущества использования массива в первую очередь, но вероятно, можно было бы что-то взломать, используя этот подход.

любые указатели или советы будут наиболее оценены!

также производительность действительно важна здесь, так что чем выше производительность, тем лучше!

3 ответов


Это будет работать для любой длины массива

select category, array_agg(average order by subscript) as average
from (
    select
        a.analysis->>'category' category,
        subscript,
        avg(v)::numeric(5,2) as average
    from
        analysis a,
        lateral unnest(
            array(select jsonb_array_elements_text(analysis->'results')::int)
        ) with ordinality s(v,subscript)
    group by 1, 2
) s
group by category
;
 category |                                                 average                                                  
----------+----------------------------------------------------------------------------------------------------------
 news     | {14.33,15.33,16.33,17.33,18.33,19.33,20.33,21.33,22.33,23.33,24.33,25.33,26.33,27.33,45.00,36.00}
 sport    | {61.00,62.00,63.00,64.00,65.00,66.00,67.00,68.00,69.00,70.00,71.00,72.00,73.00,74.00,75.00,76.00}
 weather  | {91.00,92.00,93.00,94.00,95.00,96.00,97.00,98.00,99.00,100.00,101.00,102.00,103.00,104.00,105.00,106.00}

таблица функций - с ordinality

боковая


потому что массив всегда одинаковой длины, вы можете использовать generate_series вместо того, чтобы вводить индекс каждого элемента массива самостоятельно. Вы пересекаете соединение с этим сгенерированным рядом, поэтому индекс применяется к каждой категории, и вы можете получить каждый элемент в позиции s из массива. Тогда это просто агрегирование данных с помощью GROUP BY.

запрос становится:
SELECT category, array_agg(val ORDER BY s) analysis_average
FROM (
  SELECT analysis->'category' category, s, AVG((analysis->'results'->>s)::numeric) val
  FROM analysis
  CROSS JOIN generate_series(0, 15) s
  GROUP BY category,s
) q
GROUP BY category

15-в этом случае последний индекс массива (16-1).


Это можно сделать более традиционным способом, как

select
  (t.analysis->'category')::varchar,
  array_math_avg(array(select jsonb_array_elements_text(t.analysis->'results')::int))::numeric(9,2)[]
from
  analysis t
group by 1 order by 1;

но нам нужно сделать некоторую подготовку:

create type t_array_math_agg as(
  c int[],
  a numeric[]
);

create or replace function array_math_sum_f(in t_array_math_agg, in numeric[]) returns t_array_math_agg as $$
declare
  r t_array_math_agg;
  i int;
begin
  if  is null then
    return ;
  end if;
  r := ;
  for i in array_lower(,1)..array_upper(,1) loop
    if coalesce(r.a[i],[i]) is null then
      r.a[i] := null;
    else
      r.a[i] := coalesce(r.a[i],0) + coalesce([i],0);
      r.c[i] := coalesce(r.c[i],0) + 1;
    end if; 
  end loop;
  return r;
end; $$ immutable language plpgsql;

create or replace function array_math_avg_final(in t_array_math_agg) returns numeric[] as $$
declare
  r numeric[];
  i int;
begin
  if array_lower(.a, 1) is null then
    return null;
  end if;
  for i in array_lower(.a,1)..array_upper(.a,1) loop
    r[i] := .a[i] / .c[i]; 
  end loop;
  return r;
end; $$ immutable language plpgsql;

create aggregate array_math_avg(numeric[]) (
  sfunc=array_math_sum_f,
  finalfunc=array_math_avg_final,
  stype=t_array_math_agg,
  initcond='({},{})'
);