获得n个分组类别并将其他类别合并为一个

时间:2015-04-10 11:33:20

标签: sql postgresql query-optimization aggregate sql-limit

我有一个具有以下结构的表:

Contents (
  id
  name
  desc
  tdate
  categoryid
  ...
)

我需要对此表中的数据进行一些统计。例如,我希望通过该类别的分组和ID获得具有相同类别的行数。另外,我想按降序限制n行,如果有更多类别可用,我想将它们标记为"其他"。到目前为止,我已经向数据库提出了2个查询:

按降序选择n行:

SELECT COALESCE(ca.NAME, 'Unknown') AS label
    ,ca.id AS catid
    ,COUNT(c.id) AS data
FROM contents c
LEFT OUTER JOIN category ca ON ca.id = c.categoryid
GROUP BY label
    ,catid
ORDER BY data DESC LIMIT 7

选择其他行为一个:

SELECT 'Others' AS label
    ,COUNT(c.id) AS data
FROM contents c
LEFT OUTER JOIN category ca ON ca.id = c.categoryid
WHERE c.categoryid NOT IN ($INCONDITION)

但是当我在db表中没有类别组时,我仍然会得到一个"其他"记录。是否有可能在一个查询中制作并制作"其他"记录可选吗?

3 个答案:

答案 0 :(得分:4)

特定难度此处:SELECT列表中没有GROUP BY子句的一个或多个聚合函数的查询产生正好一行,即使没有行在基础表中找到

WHERE子句中,您无法阻止该行。您必须在事实之后排除这样的行,即在HAVING子句中或在外部查询中。

Per documentation:

  

如果查询包含聚合函数调用,但没有GROUP BY子句,   分组仍然发生:结果是单个组行(或者可能没有   如果单行随后由HAVING消除,则完全为行。相同   如果它包含HAVING子句,即使没有任何聚合也是如此   函数调用或GROUP BY子句。

应该注意的是,添加GROUP BY子句只有一个常量表达式(否则完全没有意义!)也可以。 参见下面的例子。但我宁愿不使用这个技巧,即使它很简单,便宜又简单,因为它的作用并不明显。

以下查询仅需要单表扫描并返回按计数排序的前7个类别。如果(且仅当)有更多类别,则其余类别汇总为“其他”:

WITH cte AS (
   SELECT categoryid, count(*) AS data
        , row_number() OVER (ORDER BY count(*) DESC, categoryid) AS rn
   FROM   contents
   GROUP  BY 1
   )
(  -- parentheses required again
SELECT categoryid, COALESCE(ca.name, 'Unknown') AS label, data
FROM   cte
LEFT   JOIN category ca ON ca.id = cte.categoryid
WHERE  rn <= 7
ORDER  BY rn
)
UNION ALL
SELECT NULL, 'Others', sum(data)
FROM   cte
WHERE  rn > 7         -- only take the rest
HAVING count(*) > 0;  -- only if there actually is a rest
-- or: HAVING  sum(data) > 0
  • 如果多个类别在第7/8列中具有相同的计数,则需要断开关系。在我的示例中,categoryid较小的类别赢得了这样的竞赛。

  • 括号必须在LIMIT查询的单个分支中包含ORDER BYUNION子句。

  • 您只需加入表格category即可获得前7个类别。而且在这种情况下,首先聚合并稍后加入通常会更便宜。因此,请不要加入名为cte的{​​{3}}中的基本查询,只加入SELECT查询的第一个UNION,这样会更便宜。

  • 不确定为什么需要COALESCE。如果您有contents.categoryidcategory.id的外键,contents.categoryidcategory.name都定义为NOT NULL(就像他们可能应该这样),那么你不需要它。

奇数GROUP BY true

这也可行:

...

UNION ALL
SELECT NULL , 'Others', sum(data)
FROM   cte
WHERE  rn > 7
GROUP BY true; 

我的查询计划稍微快一点。但这是一个相当奇怪的黑客......

CTE (common table expression)展示所有。

相关答案以及UNION ALL / LIMIT技术的更多解释:

答案 1 :(得分:1)

快速修复,使'Others'行有条件的是为该查询添加一个简单的HAVING子句。

HAVING COUNT(c.id) > 0

(如果contents表中没有其他行,则COUNT(c.id)将为零。)

这只回答了问题的一半,如何使该行的返回成为条件。

问题的后半部分涉及更多。

要在一个查询中获取整个结果集,您可以执行类似这样的操作

(这是尚未测试;仅限桌面检查..我不确定postgresql是否在内联视图中接受LIMIT子句...如果它不是&我们& #39; d需要实现一种不同的机制来限制返回的行数。

  SELECT IFNULL(t.name,'Others') AS name
       , t.catid                 AS catid
       , COUNT(o.id)             AS data 
    FROM contents o
    LEFT 
    JOIN category oa
      ON oa.id = o.category_id
    LEFT
    JOIN ( SELECT COALESCE(ca.name,'Unknown') AS name
                , ca.id                       AS catid
                , COUNT(c.id)                 AS data
             FROM contents c
             LEFT
             JOIN category ca
               ON ca.id = c.categoryid
            GROUP 
               BY COALESCE(ca.name,'Unknown')
                , ca.id
            ORDER
               BY COUNT(c.id) DESC
                , ca.id DESC
            LIMIT 7
         ) t
      ON ( t.catid = oa.id OR (t.catid IS NULL AND oa.id IS NULL)) 
   GROUP
      BY ( t.catid = oa.id OR (t.catid IS NULL AND oa.id IS NULL)) 
       , t.catid
   ORDER
      BY COUNT(o.id) DESC
       , ( t.catid = oa.id OR (t.catid IS NULL AND oa.id IS NULL)) DESC
       , t.catid DESC
   LIMIT 7

内联视图t基本上得到与第一个查询相同的结果,类别表中的(最多)7个id值列表,或类别表中的6个id值和一个NULL。

外部查询基本上做同样的事情,将content加入category,但也要检查t中是否有匹配的行。因为t可能返回NULL,所以我们有一个稍微复杂的比较,我们希望NULL值匹配NULL值。 (MySQL方便地为我们提供了简写操作符,null安全比较运算符<=>,但我不认为它可以在postgresql中使用,所以我们必须以不同的方式表达。

     a = b OR (a IS NULL AND b IS NULL)

下一步是让GROUP BY工作,我们希望按内联视图t返回的7个值进行分组,或者,如果t的值不匹配,分组&#34;其他&#34;一起排。我们可以通过在GROUP BY子句中使用布尔表达式来实现这一点。

我们基本上说&#34;如果有来自t&#39;&#34; (真或假)然后按来自&#39; t&#39;的行分组。获取计数,然后按计数降序排序。

这未经过测试,只有桌面检查。

答案 2 :(得分:0)

您可以使用嵌套聚合来处理此问题。内部聚合计算计数以及序号。您想要获取数量为7或更少的所有内容,然后将其他所有内容合并到others类别中:

SELECT (case when seqnum <= 7 then label else 'others' end) as label,
       (case when seqnum <= 7 then catid end) as catid, sum(cnt)
FROM (SELECT ca.name AS label, ca.id AS catid, COUNT(c.id) AS cnt,
             row_number() over (partition by ca.name, catid order by count(c.id) desc) as seqnum
      FROM contents c LEFT OUTER JOIN
           category ca
           ON ca.id = c.categoryid
      GROUP BY label, catid
     ) t
GROUP BY (case when seqnum <= 7 then label else 'others' end),
         (case when seqnum <= 7 then catid end) 
ORDER BY cnt DESC ;