Как получить массивы из нормализованной таблицы, в которой хранятся элементы массива по индексу? - PullRequest
2 голосов
/ 20 сентября 2019

У меня есть таблица, хранящая элементы массива по массиву, к которому они принадлежат, и их индекс в массиве.Это казалось разумным, потому что массивы должны были быть редкими, а их элементы обновляться индивидуально.Допустим, это таблица:

CREATE TABLE values (
    pk TEXT,
    i INTEGER,
    value REAL,
    PRIMARY KEY (pk, i)
);

 pk | i | value
----+---+-------
 A  | 0 | 17.5
 A  | 1 | 32.7
 A  | 3 | 5.3
 B  | 1 | 13.5
 B  | 2 | 4.8
 B  | 4 | 89.1

Теперь я хотел бы получить их в виде реальных массивов, т.е. {17.5, 32.7, NULL, 53} для A и {NULL, 13.5, 4.8, NULL, 89.1} для B.

Я бы ожидал, что это легко сделать с помощью группирующего запроса и соответствующей агрегатной функции.Однако оказалось, что нет такой функции, которая бы помещала элементы в массив по его индексу (или по нижнему индексу, как его называет postgres).Было бы намного проще, если бы элементы были последовательными - я просто мог бы использовать array_agg с ORDER BY i.Но я хочу получить нулевые значения в массивах результатов.

В результате я получил этого монстра:

SELECT
  pk,
  ARRAY( SELECT
    ( SELECT value
      FROM values innervals
      WHERE innervals.pk = outervals.pk AND i = generate_series
    )
    FROM generate_series(0, MAX(i))
    ORDER BY generate_series -- is this really necessary?
  )
FROM values outervals
GROUP BY pk;

Дважды SELECT … FROM values ужасно, а планировщик запросов - нетКажется, я могу оптимизировать это.

Есть ли простой способ ссылаться на сгруппированные строки как отношение в подзапросе , чтобы я мог просто SELECT value FROM generate_series(0, MAX(i)) LEFT JOIN ????

Было бы более уместно решить эту проблему, определив пользовательскую агрегатную функцию ?


Edit : Кажется,то, что я искал, возможно с несколькими аргументами unnest и array_agg, хотя это не особенно элегантно:

SELECT
  pk,
  ARRAY( SELECT val
    FROM generate_series(0, MAX(i)) AS series (series_i)
    LEFT OUTER JOIN
      unnest( array_agg(value ORDER BY i),
              array_agg(i ORDER BY i) ) AS arr (val, arr_i)
      ON arr_i = series_i
    ORDER BY series_i
  )
FROM values
GROUP BY pk;

Планировщик запросов даже, кажется, понимает, что может выполнитьотсортированные слияния JOIN на отсортированных series_i и arr_i, хотя мне нужно приложить еще больше усилий для реального понимания вывода EXPLAIN. Редактировать 2 : На самом деле это хеш-соединение между series_i и arr_i, только агрегирование внешних групп использует "отсортированную" стратегию.

Ответы [ 3 ]

2 голосов
/ 20 сентября 2019

Не уверен, что это квалифицируется как «более простой» - лично мне легче следовать, хотя:

with idx as (
  select pk, 
         generate_series(0, max(i)) as i
  from "values"
  group by pk
)
select idx.pk, 
       array_agg(v.value order by idx.i) as vals
from idx 
  left join "values" v on v.i = idx.i and v.pk = idx.pk
group by idx.pk;

CTE idx генерирует все возможные значения индекса для каждого значения PK и затем используетчто для агрегирования значений

онлайн пример

1 голос
/ 25 сентября 2019

Было бы более уместно решить эту проблему, определив пользовательскую агрегатную функцию ?

Это по крайней мере значительно упрощает запрос:

SELECT pk, array_by_subscript(i+1, value)
FROM "values"
GROUP BY pk;

Использование

CREATE FUNCTION array_set(arr anyarray, index int, val anyelement) RETURNS anyarray
AS $$
BEGIN
    arr[index] = val;
    RETURN arr;
END
$$ LANGUAGE plpgsql STRICT;

CREATE FUNCTION array_fillup(arr anyarray) RETURNS anyarray
AS $$
BEGIN
   -- necessary for nice to_json conversion of arrays that don't start at subscript 1
   IF array_lower(arr, 1) > 1 THEN
       arr[1] = NULL;
   END IF;
   RETURN arr;
END
$$ LANGUAGE plpgsql STRICT;

CREATE AGGREGATE array_by_subscript(int, anyelement) (
 sfunc = array_set,
 stype = anyarray,
 initcond = '{}',
 finalfunc = array_fillup
);

Онлайн пример .У него также есть хороший план запросов, который выполняет простое линейное сканирование на values, . Мне нужно будет оценить, насколько эффективен array_set при увеличении массива .
Это на самом делесамое быстрое решение, в соответствии с тестом EXPLAIN ANALYZE для набора тестовых данных разумного размера.Это заняло 55 мс по сравнению с 80 мс для решения ARRAY + UNNEST и значительно быстрее, чем 160 мс соединения с общим табличным выражением.

0 голосов
/ 20 сентября 2019

Я думаю, что это квалифицируется как решение (намного лучше, чем моя первоначальная попытка), поэтому я опубликую его как ответ.Из этого ответа я понял, что действительно могу поместить несколько значений в array_agg, используя синтаксис записи, это только заставляет меня объявлять типы в определении столбца:

SELECT
  pk,
  ARRAY( SELECT val
    FROM generate_series(0, MAX(i)) AS series (series_i)
    LEFT OUTER JOIN
      unnest(array_agg( (value, i) )) AS arr (val real, arr_i integer)
--                      ^^^^^^^^^^                ^^^^        ^^^^^^^
      ON arr_i = series_i
    ORDER BY series_i
  )
FROM values
GROUP BY pk;

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

...