获取 n 个分组类别并将其他类别加总为一个

Get n grouped categories and sum others into one

我有一个 table 结构如下:

Contents (
  id
  name
  desc
  tdate
  categoryid
  ...
)

我需要用这个table中的数据做一些统计。例如,我想通过分组和该类别的 id 来获取具有相同类别的行数。此外,我想按降序将它们限制为 n 行,如果有更多可用类别,我想将它们标记为 "Others"。到目前为止,我已经对数据库提出了 2 个查询:

Select 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 其他行作为一个:

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 table 中没有类别组时,我仍然会得到一条 "Others" 记录。是否可以在一个查询中完成并使 "Others" 记录可选?

您可以使用嵌套聚合来解决这个问题。内部聚合计算计数和序号。你想把所有数字小于或等于 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 ;

这里的具体难度:在SELECT列表中使用一个或多个聚合函数且没有GROUP BY子句的查询恰好产生一行,即使在基础 table.

中未找到任何行

您无法在 WHERE 子句中执行任何操作来抑制该行。您必须在 事后 排除这样的行,即在 HAVING 子句中或在外部查询中。

Per documentation:

If a query contains aggregate function calls, but no GROUP BY clause, grouping still occurs: the result is a single group row (or perhaps no rows at all, if the single row is then eliminated by HAVING). The same is true if it contains a HAVING clause, even without any aggregate function calls or GROUP BY clause.

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

下面的查询只需要单个table扫描和returns按计数排序的前7个类别。 if(and only if)分类比较多,剩下的汇总成'Others':

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 较小的类别赢得了这样的比赛。

  • 括号需要包含 LIMITORDER BY 子句到 UNION 查询的单个分支。

  • 您只需加入tablecategory即可获得前7个类别。在这种情况下,先聚合然后再加入通常更便宜。所以不要加入名为cteCTE (common table expression)中的基本查询,只加入UNION查询的第一个SELECT,这样更便宜。

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

奇数GROUP BY true

这也行得通:

...

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

我什至得到了稍微快一点的查询计划。但这是一个相当奇怪的 hack ...

SQL Fiddle 演示全部。

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

  • Sum results of a few queries and then find top 5 in SQL

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

HAVING COUNT(c.id) > 0

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

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


问题的后半部分有点复杂。

要在一次查询中获得整个结果集,您可以这样做

(这是 尚未 测试过的;仅在桌面上检查过。我不确定 postgresql 是否在内联视图中接受 LIMIT 子句...如果它不接受我们需要实施不同的机制来限制行数 returned.

  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 类别 table 值的列表,或 6 [=18] =] 来自类别 table 的值和 NULL.

外部查询基本上做同样的事情,将 contentcategory 连接起来,但也会检查是否有来自 t 的匹配行。因为 t 可能是 return 一个 NULL,我们有一个稍微复杂的比较,我们希望 NULL 值匹配 NULL 值。 (MySQL 方便地为我们提供了 shorthand 运算符,空安全比较运算符 <=>,但我认为这在 postgresql 中不可用,因此我们必须以不同的方式表达。

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

下一步是让 GROUP BY 起作用,我们希望按内联视图 t 编辑的 7 个值 return 进行分组,或者,如果 t,将 "other" 行组合在一起。我们可以通过在 GROUP BY 子句中使用布尔表达式来实现这一点。

我们基本上是在说 "group by 'if there was a matching row from t'"(真或假),然后按 't' 中的行分组。获取计数,然后按计数降序排列。

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