7

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

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 表中没有留下任何类别组时,我仍然会得到“其他”记录。是否可以在一个查询中进行,并使“其他”记录可选?

4

3 回答 3

4

这里的具体困难:在列表中具有一个或多个聚合函数SELECT且没有GROUP BY子句的查询只产生一行,即使在基础表中没有找到行。

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

根据文档:

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

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

以下查询只需要一次表扫描,并返回按计数排序的前 7 个类别。如果(且仅当)有更多的类别,其余的归结为“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赢得这样的比赛。

  • 括号需要在查询的单个分支中包含LIMITor子句。ORDER BYUNION

  • 您只需要加入category前 7 个类别的表格。在这种情况下,先聚合然后再加入通常更便宜。所以不要在名为CTE(公共表表达式)cte的基础查询中加入,只加入查询的第一个SELECTUNION这样更便宜。

  • 不确定为什么需要COALESCE. 如果你有一个外键 from contents.categoryidtocategory.id和 bothcontents.categoryid并且category.name被定义NOT NULL(就像他们可能应该的那样),那么你不需要它。

奇怪的GROUP BY true

这也可以:

...

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

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

SQL Fiddle演示了所有内容。

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

于 2015-06-02T14:26:23.573 回答
1

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

HAVING COUNT(c.id) > 0

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

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


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

要在一个查询中获取整个结果集,您可以执行以下操作

(这还没有测试;只检查了桌面。我不确定postgresql是否接受内联视图中的LIMIT子句......如果不是,我们需要实现不同的机制来限制行数回来。

  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。

外部查询基本上做同样的事情,加入contentwith category,但也会检查是否有匹配的行 from t。因为t可能会返回 NULL,所以我们有一个稍微复杂的比较,我们希望 NULL 值与 NULL 值匹配。(MySQL 方便地为此提供了速记运算符,即 null-safe 比较运算符<=>,但我认为 postgresql 中没有,因此我们必须以不同的方式表达。

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

下一点是让 GROUP BY 工作,我们希望按内联视图返回的 7 个值进行分组t,或者,如果没有匹配的值 from t,则将“其他”行组合在一起。我们可以通过在 GROUP BY 子句中使用布尔表达式来实现这一点。

我们基本上是在说“按'如果有来自 t' 的匹配行进行分组”(真或假),然后按来自 't' 的行进行分组。获取一个计数,然后按计数降序排列。

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

于 2015-06-03T03:35:23.613 回答
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 ;
于 2015-04-10T11:39:29.517 回答