获取 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
子句中或在外部查询中。
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
较小的类别赢得了这样的比赛。
括号需要包含 LIMIT
或 ORDER BY
子句到 UNION
查询的单个分支。
您只需加入tablecategory
即可获得前7个类别。在这种情况下,先聚合然后再加入通常更便宜。所以不要加入名为cte
的CTE (common table expression)中的基本查询,只加入UNION
查询的第一个SELECT
,这样更便宜。
不确定为什么需要 COALESCE
。如果你有一个从 contents.categoryid
到 category.id
的外键并且 contents.categoryid
和 category.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.
外部查询基本上做同样的事情,将 content
与 category
连接起来,但也会检查是否有来自 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' 中的行分组。获取计数,然后按计数降序排列。
这没有经过测试,只是桌面检查。
我有一个 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
子句中或在外部查询中。
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 byHAVING
). The same is true if it contains aHAVING
clause, even without any aggregate function calls orGROUP 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
较小的类别赢得了这样的比赛。括号需要包含
LIMIT
或ORDER BY
子句到UNION
查询的单个分支。您只需加入table
category
即可获得前7个类别。在这种情况下,先聚合然后再加入通常更便宜。所以不要加入名为cte
的CTE (common table expression)中的基本查询,只加入UNION
查询的第一个SELECT
,这样更便宜。不确定为什么需要
COALESCE
。如果你有一个从contents.categoryid
到category.id
的外键并且contents.categoryid
和category.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.
外部查询基本上做同样的事情,将 content
与 category
连接起来,但也会检查是否有来自 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' 中的行分组。获取计数,然后按计数降序排列。
这没有经过测试,只是桌面检查。