Фильтрация в агрегатах Postrgres - PullRequest
0 голосов
/ 07 мая 2018

У меня есть таблица в Postgres, которая называется tasks. Он записывает механические задачи в стиле турка. Имеет следующие столбцы:

entity_name, text (the thing being reviewed)
reviewer_email, text (the email address of the person doing the reviewing)
result, boolean (the entry provided by the reviewer)

Каждая сущность, которую необходимо проверить, приводит к созданию двух строк задач, каждая из которых назначается разному рецензенту. Когда оба рецензента не согласны (например, их значения для result не равны), приложение запускает третье задание, назначенное модератору. У модераторов всегда один и тот же почтовый домен.

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

SELECT
  reviewer_email,
  COUNT(*) FILTER(
    WHERE entity_name IN (
      SELECT entity_name
      FROM tasks
      GROUP BY entity_name
      HAVING
        COUNT(*) FILTER (WHERE result IS NOT NULL) = 3 -- find the entities that have exactly three reviews
      AND
        -- this is the tricky part: 
        -- need something like:
        -- WHERE current_review.result = moderator_review.result
    )
  ) AS overruled_count
FROM
  tasks
WHERE
  result IS NOT NULL
GROUP BY
  reviewer_email
HAVING
  reviewer_email NOT LIKE '%@moderators-domain.net'

Пример данных:

id | entity_name | reviewer_email             | result
 1 | apple       | bob@email.net              | true
 2 | apple       | alice@email.net            | false
 3 | apple       | mod@@moderators-domain.net | true
 4 | pair        | bob@email.net              | true
 5 | pair        | alice@email.net            | false
 6 | pair        | mod@@moderators-domain.net | false
 7 | kiwi        | bob@email.net              | true
 8 | kiwi        | alice@email.net            | true

Желаемые результаты:

reviewer_email  | overruled_count | affirmed_count
bob@email.net   |               1 |              1
alice@email.net |               1 |              1

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

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

Ответы [ 3 ]

0 голосов
/ 10 мая 2018

Просто добавив некоторые изменения, чтобы сделать запрос немного легче для понимания, на мой взгляд. Я предполагаю, что мы также должны рассмотреть случай, когда у нас есть пользователи, которые никогда не были ни подтверждены, ни отвергнуты (поэтому их количество будет равно 0)

SELECT
    tasks.reviewer_email,
    COUNT(*) FILTER (WHERE tasks.result = modtasks.result) AS affirmed_count,
    COUNT(*) FILTER (WHERE tasks.result <> modtasks.result) AS overruled_count
  FROM tasks
  LEFT JOIN tasks modtasks
      ON modtasks.entity_name = tasks.entity_name
      AND modtasks.reviewer_email LIKE '%@moderators-domain.net'
 WHERE tasks.reviewer_email NOT LIKE '%@moderators-domain.net'
 GROUP BY tasks.reviewer_email
0 голосов
/ 10 мая 2018

Пример данных

CREATE TABLE tasks
    ("id" int, "entity_name" text, "reviewer_email" text, "result" boolean)
;

INSERT INTO tasks
    ("id", "entity_name", "reviewer_email", "result")
VALUES
    (1, 'apple', 'bob@email.net', 'true'),
    (2, 'apple', 'alice@email.net', 'false'),
    (3, 'apple', 'mod@@moderators-domain.net', 'true'),
    (4, 'pair', 'bob@email.net', 'true'),
    (5, 'pair', 'alice@email.net', 'false'),
    (6, 'pair', 'mod@@moderators-domain.net', 'false'),
    (7, 'kiwi', 'bob@email.net', 'true'),
    (8, 'kiwi', 'alice@email.net', 'true')
;

Запрос 1

WITH
CTE_moderated_tasks
AS
(
  SELECT
    id AS mod_id
    ,entity_name AS mod_entity_name
    ,result AS mod_result
  FROM tasks
  WHERE reviewer_email LIKE '%@moderators-domain.net'
)
SELECT
  tasks.reviewer_email
  ,SUM(CASE WHEN tasks.result <> mod_result THEN 1 ELSE 0 END) AS overruled_count 
  ,SUM(CASE WHEN tasks.result = mod_result THEN 1 ELSE 0 END) AS affirmed_count 
FROM
  CTE_moderated_tasks
  INNER JOIN tasks ON 
    tasks.entity_name = CTE_moderated_tasks.mod_entity_name
    AND tasks.id <> CTE_moderated_tasks.mod_id
GROUP BY
  tasks.reviewer_email

Я разделил запрос на две части.

Сначала я хочу найти все задачи, в которых участвовал модератор (CTE_moderated_tasks). Предполагается, что модератор не может участвовать более одного раза в одной задаче.

Этот результат внутренне объединен с исходной таблицей tasks, таким образом, естественным образом отфильтровывая все задачи, в которых не участвовал модератор. Это также дает нам мнение модератора рядом с мнением рецензента. Это предполагает, что для выполнения одной и той же задачи есть только два рецензента.

Теперь осталось только сгруппировать рецензентов и посчитать, сколько раз совпадало мнение рецензента и модератора. Я использовал классический SUM(CASE ...) для этого условного агрегата.

Вам не нужно использовать CTE, я использовал его в первую очередь для удобства чтения.

Я также хотел бы подчеркнуть, что этот запрос использует LIKE только во время одного сканирования таблицы. Если на entity_name есть индекс, объединение может быть довольно эффективным.

Результат

|  reviewer_email | overruled_count | affirmed_count |
|-----------------|-----------------|----------------|
| alice@email.net |               1 |              1 |
|   bob@email.net |               1 |              1 |

.

Вариант без самостоятельного соединения

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

В этом запросе используется оконная функция с разбиением по entity_name, чтобы получить результат модератора для каждой строки без явного самосоединения. Здесь вы можете использовать любую статистическую функцию (SUM или MIN или MAX), потому что для каждого entity_name.

будет не более одной строки от модератора.

Тогда простая группировка с условным агрегатом дает нам счет.

Здесь условный агрегат использует тот факт, что NULL по сравнению с любым значением никогда не возвращает true. mod_result для сущностей, у которых нет модератора, будут нулевые значения, а result <> mod_result и result = mod_result приведут к ложному значению, поэтому такие строки не влияют ни на один из них.

Final HAVING reviewer_email NOT LIKE '%@moderators-domain.net' удаляет счетчик результатов модератора.

Опять же, вам не нужно использовать CTE здесь, и я использовал его в первую очередь для удобства чтения. Я бы рекомендовал сначала запустить только CTE и изучить промежуточные результаты, чтобы понять, как работает запрос.

Запрос 2

WITH
CTE
AS
(
  SELECT
    id
    ,entity_name
    ,reviewer_email
    ,result::int
    ,SUM(result::int)
      FILTER (WHERE reviewer_email LIKE '%@moderators-domain.net') 
      OVER (PARTITION BY entity_name) AS mod_result
  FROM tasks
)
SELECT
  reviewer_email
  ,SUM(CASE WHEN result <> mod_result THEN 1 ELSE 0 END) AS overruled_count 
  ,SUM(CASE WHEN result = mod_result THEN 1 ELSE 0 END) AS affirmed_count 
FROM CTE
GROUP BY reviewer_email
HAVING reviewer_email NOT LIKE '%@moderators-domain.net'
0 голосов
/ 07 мая 2018

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

select t.*, tm.result as moderator_result
from tasks t join
    tasks tm
    on t.entity_name = tm.entity_name
where t.reviewer_email NOT LIKE '%@moderators-domain.net' and
      tm.reviewer_email LIKE '%@moderators-domain.net';

Исходя из этого, мы можем агрегировать результаты, которые вы хотите:

select reviewer_email,
       sum( (result = moderator_result)::int ) as moderator_agrees,
       sum( (result <> moderator_result)::int ) as moderator_disagrees
from (select t.*, tm.result as moderator_result
      from tasks t join
           tasks tm
           on t.entity_name = tm.entity_name
      where t.reviewer_email NOT LIKE '%@moderators-domain.net' and
            tm.reviewer_email LIKE '%@moderators-domain.net'
     ) t
group by reviewer_email;

Может быть способ сделать это, используя filter и даже оконные функции. Этот метод кажется мне наиболее естественным.

Я должен отметить, что подзапрос, конечно, не нужен:

select t.reviewer_email,
       sum( (t.result = tm.result)::int ) as moderator_agrees,
       sum( (t.result <> tm.result)::int ) as moderator_disagrees
from tasks t join
     tasks tm
     on t.entity_name = tm.entity_name
where t.reviewer_email NOT LIKE '%@moderators-domain.net' and
      tm.reviewer_email LIKE '%@moderators-domain.net'
group by t.reviewer_email;
...