Фильтрация в агрегатах Postrgres
У меня есть стол в 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 ответа
Я думаю, что это более сложная проблема, чем вы можете себе представить. Следующее добавляет обзор модератора к каждому отзыву, не являющемуся модератором:
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;
Пример данных
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')
;
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
по сравнению с любым значением никогда не возвращает истину. mod_result
для объектов, которые не имеют модератора, будут иметь значения NULL и оба result <> mod_result
а также result = mod_result
выдаст false, поэтому такие строки не влияют ни на счет.
окончательный HAVING reviewer_email NOT LIKE '%@moderators-domain.net'
удаляет счетчик результатов модератора самостоятельно.
Опять же, вам не нужно использовать CTE здесь, и я использовал его в первую очередь для удобства чтения. Я бы рекомендовал сначала запустить CTE и изучить промежуточные результаты, чтобы понять, как работает запрос.
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)
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