【问题标题】:Get n grouped categories and sum others into one获取 n 个分组类别并将其他类别加起来为一个
【发布时间】:2015-06-03 15:52:45
【问题描述】:

我有一个结构如下的表:

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

【问题讨论】:

  • 您能否提供带有完整 DDL 表 Contents 的示例数据(行)和预期输出?
  • 您的表定义应该显示数据类型和约束。基本上你在 psql 中使用\d contents 得到什么。一些具有预期输出的示例数据将有助于澄清。

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


【解决方案1】:

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

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

Per documentation:

如果查询包含聚合函数调用,但没有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 较小的类别会赢得这样的比赛。

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

  • 您只需加入表 category 即可获得前 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
GROUP BY true; 

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

SQL Fiddle 演示全部。

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

【讨论】:

  • 这个答案令人难以置信。按预期工作,并且执行速度比 spencer7593 建议的速度快 2 倍。非常感谢!
  • 要获得与原始查询相同的行为,即将来自contents 的行与不匹配的categoryid 值合并在一起,在catid 值为NULL 下,使用单个计数,公用表表达式中的查询需要对category 执行外连接,并按category 表中的id 值分组。 (只有在“未知”的合并计数是最高的 7 个计数之一时才会观察到差异。)
  • @spencer7593:好点。不过,我确实怀疑原始查询的方向是错误的。 contents 与不同的 categoryid 通常不会混为一谈,即使该类别的 name 缺失(无论是 categoryname IS NULL 中的缺失行)。在具有参照完整性的设计中,两者都不应该是可能的(我在回答中对此进行了评论),但如果它可能发生,我建议最好的做法是保持类别分开并用'Unknown' 表示丢失的数据,就像我实现它一样.
  • @ErwinBrandstetter:我同意你提出的每一点。 *(实际的表定义可能会使整个讨论变得毫无意义,因为这将揭示一个 NOT NULL 约束并揭示一个(启用和强制执行的)FOREIGN KEY 约束。没有外键约束,可能会有大量行categoryid 的各种值。如果我们将它们聚合在一起,我们只会(最多)获得一行“未知”类别(这是原始查询所做的)。按单个类别 ID 聚合,我们可以得到多行名称为“未知”。
  • @spencer7593:没错。这也是为什么我在结果中使用contents.categoryid 而不是category.id 来排除引用表中缺少条目的可能性。好吧,那也几乎没有任何充分的理由拥有它。
【解决方案2】:

使'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。

外部查询基本上做同样的事情,将contentcategory 连接起来,但还会检查t 中是否有匹配的行。因为t 可能返回 NULL,所以我们有一个稍微复杂的比较,我们希望 NULL 值与 NULL 值匹配。 (MySQL 方便地为此提供了速记运算符,即 null 安全比较运算符 &lt;=&gt;,但我认为 postgresql 中没有,因此我们必须以不同的方式表达。

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

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

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

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

【讨论】:

  • 2 个错误我自己修复了,name 字段不在group by 语句中,并将ifnull 更改为coalesce。之后它按预期工作。但是,在我测试了您和@ErwinBrandstetter 的答案后,我发现您的查询比他的要慢 2 倍。总之非常感谢!非常感谢您的回答。
  • 修复是适当的。我的“MySQL”正在显示。 MySQL 有一个 IFNULL 函数,并且(默认情况下)扩展 GROUP BY 的行为以允许 SELECT 列表中的额外非聚合。 MySQL 也不支持 Common Table Expressions 或提供像 row_number() 这样的分析函数,在 Erwin 的答案中的查询中使用。 (我们可以让这个查询在 MySQL 中运行得更快,但它在 postgresql 中不起作用。) Erwin 的答案中的查询更快,因为它只需要从 contents 进行一次聚合(在 cte 中)。此答案中的查询会执行两次。
  • 我确实注意到这个答案中的查询和 Erwin 在极端情况下的行为之间的一个差异......当content 中的行在categoryid 列中包含多个未出现的值时在category 表中。在 Erwin 回答的查询中,这些行将按 categoryid 列的值聚合。但是这个答案中的查询会将所有这些行与 catid 的 NULL 值聚合在一起,作为“未知”的一个集体总数(就像原始查询一样)。当“未知”的聚合集具有七个最高计数之一时,差异就出现了。
【解决方案3】:

您可以使用嵌套聚合来解决此问题。内部聚合计算计数以及序号。您想取所有编号为 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 ;

【讨论】:

  • 遇到错误:ERROR: column "label" does not exist LINE 4: row_number() over (partition by label, catid or...
  • 现在又出现一个错误:ERROR: column "catid" does not exist LINE 4: ... row_number() over (partition by ca.name, catid orde...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-26
  • 1970-01-01
  • 2017-10-04
  • 2019-11-20
  • 2022-09-30
  • 2023-03-22
  • 1970-01-01
相关资源
最近更新 更多