Как получить mode () в оконной функции в Postgres? - PullRequest
2 голосов
/ 05 апреля 2019

Я пытаюсь получить mode() для сгруппированного набора данных, но без группировки результатов.(Используя Postgres 9.5, можно обновить при необходимости.)

например, у пользователей есть «любимый цвет», и они принадлежат к одной группе.Получить список пользователей с mode() «любимым цветом» в своей группе.

Функция окна будет работать для большинства агрегатов, но mode(), похоже, является исключением, которое не совместимо с функциями окна.Есть ли другой способ пойти по этому поводу?Вот что я играл до сих пор ...

Работает, но дает сгруппированные результаты, я ищу результаты для разгруппировки:

SELECT group_id, 
    mode() WITHIN GROUP (ORDER BY color)
FROM users
GROUP BY group_id;

Неверный синтаксис (простопример того, что я пытаюсь выполнить):

SELECT id, color, group_id, 
    mode(color) OVER (PARTITION BY group_id)
FROM users;

Или:

SELECT id, color, group_id, 
    mode() WITHIN GROUP (ORDER BY color) OVER (PARTITION BY group_id)
FROM users;

Я попытался использовать боковое соединение, но не смог заставить его работать правильно безповторение моего предложения WHERE как внутри, так и вне объединения (что я бы предпочел не делать, когда этот запрос усложняется):

SELECT u1.id, u1.group_id, u1.color, mode_color
FROM users u1
LEFT JOIN LATERAL
    (SELECT group_id, mode() WITHIN GROUP (ORDER BY color) as mode_color
     FROM users
     WHERE group_id = d1.group_id
     GROUP BY group_id)
    u2 ON u1.group_id = u2.group_id
WHERE u1.type = 'customer';

Важно, чтобы WHERE u1.type = 'customer' оставался вне подзапроса, поскольку это добавляется к запросу позже, после того, как первая половина уже написана.

1 Ответ

2 голосов
/ 06 апреля 2019

Речь идет о агрегатной функции с упорядоченным набором mode () , представленной в Postgres 9.4. Возможно, вы видели это сообщение об ошибке:

ERROR:  OVER is not supported for ordered-set aggregate mode

Мы можем обойти это. Но какой именно режим?

(Все предположения group_id и type равны NOT NULL, в противном случае вам нужно сделать больше.)

Режим уточняющих строк

Это вычисляет режим на основе только отфильтрованного набора (с type = 'customer').
Вы получаете самый популярный цвет в группе среди "клиентов".

Подзапрос в простом JOIN (без LEFT и LATERAL в этом случае) сделает работу - вычисляя режим один раз для группы, а не для каждой отдельной строки:

SELECT u1.id, u1.group_id, u1.color, u2.mode_color
FROM   users u1
JOIN  (                            -- not LATERAL
   SELECT group_id, type           -- propagate out for the join
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM   users 
   WHERE  type = 'customer'        -- place condition in subquery (cheap)
   GROUP  BY group_id, type
   ) u2 USING (group_id, type);    -- shorthand syntax for matching names
-- WHERE  type = 'customer'        -- or filter later (expensive)

Чтобы не повторять ваше условие, поместите его в подзапрос и распространите его на внешний запрос в предложении соединения - я выбрал совпадающие имена столбцов и в моем примере добавил USING.

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

Существуют способы параметризации вашего запроса. Подготовленные операторы, функция PL / pgSQL, см .:

или , если базовая таблица не сильно меняется, материализованное представление со всеми предварительно вычисленными режимами в (group_id, type) заменяет подзапрос.

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

WITH cte AS (  -- filter result rows first
   SELECT id, group_id, color
   FROM   users u1
   WHERE  type = 'customer'        -- predicate goes here
   )
SELECT *
FROM   cte u1
LEFT   JOIN (                      -- or JOIN, doesn't matter here
   SELECT group_id
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM   cte                      -- based on only qualifying rows
   GROUP  BY 1
   ) u2 USING (group_id);

Мы можем упростить с помощью SELECT *, поскольку USING удобно помещает только one group_id в набор результатов.

Режим всех рядов

Если вы хотите основать режим на всех строках (включая те, где type = 'customer' не соответствует действительности), вам нужен другой запрос.
Вы получаете самый популярный цвет для группы среди всех участников.

Переместить предложение WHERE во внешний запрос:

SELECT u1.id, u1.group_id, u1.color, u2.mode_color
FROM   users u1
LEFT   JOIN (                      -- or JOIN, doesn't matter here
   SELECT group_id
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM   users
   GROUP  BY group_id
   ) u2 USING (group_id)
WHERE  u1.type = 'customer';

Если ваш предикат (type = 'customer') достаточно избирателен, вычисление режима для всех групп может оказаться бесполезным. Сначала отфильтруйте небольшое подмножество и рассчитайте только режим для содержащихся групп Добавьте CTE для этого:

WITH cte AS (  -- filter result rows first
   SELECT id, group_id, color
   FROM   users u1
   WHERE  type = 'customer'
   )
SELECT *
FROM   cte u1
LEFT   JOIN (        -- or JOIN
   SELECT group_id
        , mode() WITHIN GROUP (ORDER BY color) AS mode_color
   FROM  (SELECT DISTINCT group_id FROM cte) g  -- only relevant groups
   JOIN   users USING (group_id)                -- but consider all rows for those
   GROUP  BY 1
   ) u2 USING (group_id);

Аналогично приведенному выше запросу CTE, но на основе всех членов группы в базовой таблице.

...