Как получить агрегат оконной функции в Postgres?

11

У меня есть таблица, содержащая два столбца перестановок / комбинаций целочисленных массивов, и третий столбец, содержащий значение, например, так:

CREATE TABLE foo
(
  perm integer[] NOT NULL,
  combo integer[] NOT NULL,
  value numeric NOT NULL DEFAULT 0
);
INSERT INTO foo
VALUES
( '{3,1,2}', '{1,2,3}', '1.1400' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0.9280' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,1,2}', '{1,2,3}', '1.2680' ),
( '{3,1,2}', '{1,2,3}', '0' ),
( '{3,2,1}', '{1,2,3}', '0' ),
( '{3,2,1}', '{1,2,3}', '0.8000' )

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

SELECT
  f1.perm,
  f2.combo,
  f1.perm_average_value,
  f2.combo_average_value,
  f1.perm_stddev,
  f2.combo_stddev,
  f1.perm_count,
  f2.combo_count
FROM
(
  SELECT
    perm,
    combo,
    avg( value ) AS perm_average_value,
    stddev_pop( value ) AS perm_stddev,
    count( * ) AS perm_count
  FROM foo
  GROUP BY perm, combo
) AS f1
JOIN
(
  SELECT
    combo,
    avg( value ) AS combo_average_value,
    stddev_pop( value ) AS combo_stddev,
    count( * ) AS combo_count
  FROM foo
  GROUP BY combo
) AS f2 ON ( f1.combo = f2.combo );

Однако этот запрос может быть довольно медленным, когда у меня много данных, потому что таблицу "foo" (которая на самом деле состоит из 14 разделов, каждый из которых содержит примерно 4 миллиона строк) необходимо сканировать дважды.

Недавно я узнал, что Postgres поддерживает «оконные функции», которые в основном похожи на GROUP BY для определенного столбца. Я изменил свой запрос, чтобы использовать их так:

SELECT
  perm,
  combo,
  avg( value ) as perm_average_value,
  avg( avg( value ) ) over w_combo AS combo_average_value,
  stddev_pop( value ) as perm_stddev,
  stddev_pop( avg( value ) ) over w_combo as combo_stddev,
  count( * ) as perm_count,
  sum( count( * ) ) over w_combo AS combo_count
FROM foo
GROUP BY perm, combo
WINDOW w_combo AS ( PARTITION BY combo );

Хотя это работает для столбца «combo_count», столбцы «combo_average_value» и «combo_stddev» более не точны. Похоже, что среднее значение берется для каждой перестановки, а затем усредняется во второй раз для каждой комбинации, что неверно.

Как я могу это исправить? Можно ли здесь использовать оконные функции в качестве оптимизации?

Скотт Смолл
источник
Предполагая текущую версию Postgres 9.2? Оконные функции пришли с 8.4.
Эрвин Брандштеттер
Извините, я забыл уточнить. Да, я использую последнюю версию Postgres 9.2.4.
Скотт Смолл

Ответы:

9

Вы можете иметь оконные функции для результата агрегатных функций на одном уровне запросов.

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

SELECT perm
      ,combo
      ,avg(value)                 AS perm_average_value
      ,sum(avg(value) * count(*)) OVER w_combo /
       sum(count(*)) OVER w_combo AS combo_average_value
      ,stddev_pop(value)          AS perm_stddev
      ,0                          AS combo_stddev  -- doesn't work!
      ,count(*)                   AS perm_count
      ,sum(count(*)) OVER w_combo AS combo_count
FROM   foo
GROUP  BY perm, combo
WINDOW w_combo  AS (PARTITION BY combo);

Для combo_average_valueвас потребуется это выражение

sum(avg(value) * count(*)) OVER w_combo / sum(count(*)) OVER w_combo

Так как вам нужно средневзвешенное значение. (Средняя группа из 10 человек весит больше, чем группа из 2 человек!)

Это работает :

SELECT DISTINCT ON (perm, combo)
       perm
      ,combo
      ,avg(value)        OVER wpc AS perm_average_value
      ,avg(value)        OVER wc  AS combo_average_value
      ,stddev_pop(value) OVER wpc AS perm_stddev
      ,stddev_pop(value) OVER wc  AS combo_stddev
      ,count(*)          OVER wpc AS perm_count
      ,count(*)          OVER wc  AS combo_count
FROM   foo
WINDOW wc  AS (PARTITION BY combo)
      ,wpc AS (PARTITION BY perm, combo);

Я использую здесь два разных окна и сокращаю количество строк, к DISTINCTкоторым применяется даже после оконных функций.

Но я серьезно сомневаюсь, что это будет быстрее, чем ваш оригинальный запрос. Я уверен, что это не так.

Лучшая производительность с измененной разметкой стола

Размер массива составляет 24 байта (небольшие различия в зависимости от типа). Кроме того, у вас, кажется, довольно много элементов в массиве и много повторений. Для такой огромной таблицы, как ваша, было бы полезно нормализовать схему. Пример макета:

CREATE TABLE combo ( 
  combo_id serial PRIMARY KEY
 ,combo    int[] NOT NULL
);

CREATE TABLE perm ( 
  perm_id  serial PRIMARY KEY
 ,perm     int[] NOT NULL
);

CREATE TABLE value (
  perm_id  int REFERENCES perm(perm_id)
 ,combo_id int REFERENCES combo(combo_id)
 ,value numeric NOT NULL DEFAULT 0
);

Если вам не нужна ссылочная целостность, вы можете опустить ограничения внешнего ключа.

Соединение с combo_idтакже может быть помещено в таблицу perm, но в этом сценарии я бы сохранил его (слегка ненормализованный) valueдля повышения производительности.

Это приведет к размеру строки 32 байта (заголовок кортежа + заполнение: 24 байта, 2 x int (8 байт), без заполнения) плюс неизвестный размер вашего numericстолбца. (Если вам не нужна предельная точность, столбец double precisionили realстолбец тоже могут это сделать.)

Подробнее о физическом хранении в этом связанном ответе на SO или здесь:
Настройка PostgreSQL для производительности чтения

В любом случае, это лишь малая часть того, что у вас есть сейчас, и ваш запрос будет намного быстрее только по размеру. Группировка и сортировка по простым целым числам также намного быстрее.

Вы бы первый агрегат в подзапрос и затем присоединиться к permи comboдля лучшей производительности.

Эрвин Брандштеттер
источник
Спасибо за четкий и краткий ответ. Вы правы, казалось бы, нет способа получить стандартное отклонение подгруппы таким образом. При этом мне нравится простота вашего решения. Исключение GROUP BY делает результирующий запрос намного более читабельным. К сожалению, как вы подозревали, производительность ниже среднего. Мне пришлось убить запрос после выполнения более 30 минут.
Скотт Смолл
@ScottSmall: Вы могли бы что- то сделать для производительности ... см. Обновление, чтобы ответить.
Эрвин Брандстеттер
Чтобы упростить мой вопрос, я удалил из fooтаблицы столбцы , которые не были релевантными. В действительности, есть еще несколько столбцов, которые не используются в этом запросе, поэтому я не уверен, что нормализация перестановок и комбинаций обеспечит значительное повышение скорости для этого конкретного случая использования.
Скотт Смолл
Кроме того, целочисленные значения, составляющие каждую перестановку и комбинацию, поступают из другой таблицы в БД. Предварительная генерация этих данных вычислительно дорогая. Максимальная длина perm / combo составляет 5, однако 5Pn и 5Cn становятся достаточно большими для больших значений n (в настоящее время около 1000, но растет ежедневно) ... в любом случае, оптимизация - это вопрос другого дня. Еще раз спасибо за вашу помощь, Эрвин.
Скотт Смолл