在Postrgres聚合中过滤

时间:2018-05-07 01:22:41

标签: sql postgresql subquery aggregate-functions

我在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

Bob和Alice各自做了三次评论。在一次审查中,他们同意,因此没有适度。他们对其他两次评论持不同意见并被推翻一次,并由主持人确认一次。

我相信上面的代码让我走在正确的轨道上,但我对其他方法肯定感兴趣。

3 个答案:

答案 0 :(得分:5)

我认为这是一个比你意识到的更难的问题。以下内容将主持人审核附加到每个非主持人审核:

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;

答案 1 :(得分:2)

在我看来,只需添加一些更改就可以让查询更容易理解。 我猜我们还需要考虑我们的用户从未被肯定或否决的情况(因此他们的计数将为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

答案 2 :(得分:2)

示例数据

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')
;

<强> Query 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进行分区,以便为每行提供调节器结果,而无需显式自连接。您可以在此处使用任何汇总功能(SUMMINMAX),因为每个entity_name的主持人最多会有一行。

然后使用条件聚合的简单分组给我们计数。

此处条件聚合使用NULL与任何值相比永远不会返回true的事实。 mod_result对于没有主持人的实体会有空值,而result <> mod_resultresult = mod_result都会产生错误,因此这些行不会导致任何计数。

最终HAVING reviewer_email NOT LIKE '%@moderators-domain.net'会自行删除主持人结果的计数。

同样,你不必在这里使用CTE,我主要用它来提高可读性。我建议先运行CTE并检查中间结果,以了解查询的工作原理。

<强> Query 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'