跳过到主要内容

GROUP BY 子句

GROUP BY 子句将 SELECT 查询切换到聚合模式,其工作原理如下:

  • GROUP BY 子句包含表达式列表(或单个表达式,被视为长度为一的列表)。该列表充当“分组键”,而每个单独的表达式将被称为“键表达式”。
  • SELECTHAVINGORDER BY 子句中的所有表达式**必须**根据键表达式**或**对非键表达式的聚合函数(包括普通列)进行计算。换句话说,从表中选择的每一列都必须在键表达式中使用,或在聚合函数中使用,但不能同时使用两者。
  • 聚合 SELECT 查询的结果将包含与源表中“分组键”的唯一值数量一样多的行。通常,这会显着减少行数,通常是数量级的减少,但并非一定如此:如果所有“分组键”值都是不同的,则行数保持不变。

当您想按列号而不是列名对表中的数据进行分组时,请启用设置enable_positional_arguments

注意

还有一种方法可以对表进行聚合。如果查询仅包含聚合函数内部的表列,则可以省略GROUP BY 子句,并假定按空键集进行聚合。此类查询始终只返回一行。

NULL 处理

对于分组,ClickHouse 将NULL 解释为一个值,且NULL==NULL。这与大多数其他情况下的NULL 处理不同。

以下是一个示例,展示了这意味着什么。

假设您有以下表格

┌─x─┬────y─┐
│ 1 │ 2 │
│ 2 │ ᴺᵁᴸᴸ │
│ 3 │ 2 │
│ 3 │ 3 │
│ 3 │ ᴺᵁᴸᴸ │
└───┴──────┘

查询SELECT sum(x), y FROM t_null_big GROUP BY y 的结果是

┌─sum(x)─┬────y─┐
│ 4 │ 2 │
│ 3 │ 3 │
│ 5 │ ᴺᵁᴸᴸ │
└────────┴──────┘

您可以看到,对于y = NULLGROUP BYx 进行了求和,就好像NULL 是这个值一样。

如果您将多个键传递给GROUP BY,结果将为您提供所有选择组合,就好像NULL 是一个特定值一样。

ROLLUP 修饰符

ROLLUP 修饰符用于根据GROUP BY 列表中键表达式的顺序计算键表达式的子总数。子总数行将添加到结果表之后。

子总数按相反顺序计算:首先计算列表中最后一个键表达式的子总数,然后计算前一个键表达式的子总数,依此类推,直到第一个键表达式。

在子总数行中,已“分组”的键表达式的值将设置为0 或空行。

注意

请注意,HAVING 子句可能会影响子总数结果。

示例

考虑表 t

┌─year─┬─month─┬─day─┐
│ 2019 │ 1 │ 5 │
│ 2019 │ 1 │ 15 │
│ 2020 │ 1 │ 5 │
│ 2020 │ 1 │ 15 │
│ 2020 │ 10 │ 5 │
│ 2020 │ 10 │ 15 │
└──────┴───────┴─────┘

查询

SELECT year, month, day, count(*) FROM t GROUP BY ROLLUP(year, month, day);

由于GROUP BY 部分有三个键表达式,因此结果包含四个表,其中子总数从右到左“滚动”

  • GROUP BY year, month, day;
  • GROUP BY year, month(而day 列填充为零);
  • GROUP BY year(现在month, day 列都填充为零);
  • 以及总计(所有三个键表达式列都为零)。
┌─year─┬─month─┬─day─┬─count()─┐
│ 2020 │ 10 │ 15 │ 1 │
│ 2020 │ 1 │ 5 │ 1 │
│ 2019 │ 1 │ 5 │ 1 │
│ 2020 │ 1 │ 15 │ 1 │
│ 2019 │ 1 │ 15 │ 1 │
│ 2020 │ 10 │ 5 │ 1 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 2019 │ 1 │ 0 │ 2 │
│ 2020 │ 1 │ 0 │ 2 │
│ 2020 │ 10 │ 0 │ 2 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 2019 │ 0 │ 0 │ 2 │
│ 2020 │ 0 │ 0 │ 4 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 0 │ 0 │ 0 │ 6 │
└──────┴───────┴─────┴─────────┘

相同的查询也可以使用WITH 关键字编写。

SELECT year, month, day, count(*) FROM t GROUP BY year, month, day WITH ROLLUP;

另请参阅

CUBE 修饰符

CUBE 修饰符用于计算GROUP BY 列表中键表达式的每个组合的子总数。子总数行将添加到结果表之后。

在子总数行中,所有已“分组”的键表达式的值将设置为0 或空行。

注意

请注意,HAVING 子句可能会影响子总数结果。

示例

考虑表 t

┌─year─┬─month─┬─day─┐
│ 2019 │ 1 │ 5 │
│ 2019 │ 1 │ 15 │
│ 2020 │ 1 │ 5 │
│ 2020 │ 1 │ 15 │
│ 2020 │ 10 │ 5 │
│ 2020 │ 10 │ 15 │
└──────┴───────┴─────┘

查询

SELECT year, month, day, count(*) FROM t GROUP BY CUBE(year, month, day);

由于GROUP BY 部分有三个键表达式,因此结果包含八个表,其中包含所有键表达式组合的子总数

  • GROUP BY year, month, day
  • GROUP BY year, month
  • GROUP BY year, day
  • GROUP BY year
  • GROUP BY month, day
  • GROUP BY month
  • GROUP BY day
  • 以及总计。

GROUP BY 中排除的列将填充为零。

┌─year─┬─month─┬─day─┬─count()─┐
│ 2020 │ 10 │ 15 │ 1 │
│ 2020 │ 1 │ 5 │ 1 │
│ 2019 │ 1 │ 5 │ 1 │
│ 2020 │ 1 │ 15 │ 1 │
│ 2019 │ 1 │ 15 │ 1 │
│ 2020 │ 10 │ 5 │ 1 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 2019 │ 1 │ 0 │ 2 │
│ 2020 │ 1 │ 0 │ 2 │
│ 2020 │ 10 │ 0 │ 2 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 2020 │ 0 │ 5 │ 2 │
│ 2019 │ 0 │ 5 │ 1 │
│ 2020 │ 0 │ 15 │ 2 │
│ 2019 │ 0 │ 15 │ 1 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 2019 │ 0 │ 0 │ 2 │
│ 2020 │ 0 │ 0 │ 4 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 0 │ 1 │ 5 │ 2 │
│ 0 │ 10 │ 15 │ 1 │
│ 0 │ 10 │ 5 │ 1 │
│ 0 │ 1 │ 15 │ 2 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 0 │ 1 │ 0 │ 4 │
│ 0 │ 10 │ 0 │ 2 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 0 │ 0 │ 5 │ 3 │
│ 0 │ 0 │ 15 │ 3 │
└──────┴───────┴─────┴─────────┘
┌─year─┬─month─┬─day─┬─count()─┐
│ 0 │ 0 │ 0 │ 6 │
└──────┴───────┴─────┴─────────┘

相同的查询也可以使用WITH 关键字编写。

SELECT year, month, day, count(*) FROM t GROUP BY year, month, day WITH CUBE;

另请参阅

WITH TOTALS 修饰符

如果指定了WITH TOTALS 修饰符,将计算另一行。该行将包含包含默认值(零或空行)的键列,以及包含跨所有行计算的值(“总计”值)的聚合函数列。

此额外的行仅在JSON*TabSeparated*Pretty* 格式中生成,与其他行分开

  • XMLJSON* 格式中,该行将作为单独的“总计”字段输出。
  • TabSeparated*CSV*Vertical 格式中,该行位于主结果之后,前面有一个空行(在其他数据之后)。
  • Pretty* 格式中,该行将作为主结果之后的单独表输出。
  • Template 格式中,该行将根据指定的模板输出。
  • 在其他格式中,它不可用。
注意

总计在SELECT 查询的结果中输出,而在INSERT INTO ... SELECT 中不输出。

当存在HAVING 时,WITH TOTALS 可以以不同的方式运行。行为取决于totals_mode 设置。

配置总计处理

默认情况下,totals_mode = 'before_having'。在这种情况下,“总计”是在所有行上计算的,包括那些未通过 HAVING 的行,以及max_rows_to_group_by

其他替代方案包括仅在“总计”中包括通过 HAVING 的行,并且在设置max_rows_to_group_bygroup_by_overflow_mode = 'any' 时表现不同。

after_having_exclusive – 不要包括未通过max_rows_to_group_by 的行。换句话说,“总计”的行数将少于或等于省略max_rows_to_group_by 时的行数。

after_having_inclusive – 将所有未通过max_rows_to_group_by 的行包含在“总计”中。换句话说,“总计”的行数将大于或等于省略max_rows_to_group_by 时的行数。

after_having_auto – 计算通过 HAVING 的行数。如果它超过某个数量(默认情况下为 50%),则将所有未通过max_rows_to_group_by 的行包含在“总计”中。否则,不要包含它们。

totals_auto_threshold – 默认情况下为 0.5。after_having_auto 的系数。

如果未使用max_rows_to_group_bygroup_by_overflow_mode = 'any',则after_having 的所有变体都相同,您可以使用它们中的任何一个(例如,after_having_auto)。

您可以在子查询中使用WITH TOTALS,包括JOIN 子句中的子查询(在这种情况下,相应的总计值将合并)。

GROUP BY ALL

GROUP BY ALL 等效于列出所有未作为聚合函数的 SELECT-ed 表达式。

例如

SELECT
a * 2,
b,
count(c),
FROM t
GROUP BY ALL

与以下相同

SELECT
a * 2,
b,
count(c),
FROM t
GROUP BY a * 2, b

对于特殊情况,如果函数同时具有聚合函数和其他字段作为其参数,则GROUP BY 键将包含我们可以从中提取的最大非聚合字段。

例如

SELECT
substring(a, 4, 2),
substring(substring(a, 1, 2), 1, count(b))
FROM t
GROUP BY ALL

与以下相同

SELECT
substring(a, 4, 2),
substring(substring(a, 1, 2), 1, count(b))
FROM t
GROUP BY substring(a, 4, 2), substring(a, 1, 2)

示例

示例

SELECT
count(),
median(FetchTiming > 60 ? 60 : FetchTiming),
count() - sum(Refresh)
FROM hits

与 MySQL(以及符合标准 SQL)不同,您无法获取不在键或聚合函数中的某些列的某些值(除了常量表达式)。为了解决这个问题,您可以使用“any”聚合函数(获取遇到的第一个值)或“min/max”。

示例

SELECT
domainWithoutWWW(URL) AS domain,
count(),
any(Title) AS title -- getting the first occurred page header for each domain.
FROM hits
GROUP BY domain

对于遇到的每个不同的键值,GROUP BY 计算一组聚合函数值。

GROUPING SETS 修饰符

这是最通用的修饰符。此修饰符允许手动指定多个聚合键集(分组集)。对每个分组集分别执行聚合,然后将所有结果组合在一起。如果列未出现在分组集中,则会填充默认值。

换句话说,上面描述的修饰符可以通过 GROUPING SETS 来表示。尽管使用 ROLLUPCUBEGROUPING SETS 修饰符的查询在语法上是相等的,但它们的执行方式可能不同。当 GROUPING SETS 尝试并行执行所有内容时,ROLLUPCUBE 在单个线程中执行聚合的最终合并。

在源列包含默认值的情况下,可能难以区分一行是使用这些列作为键的聚合的一部分还是不是。为了解决这个问题,必须使用 GROUPING 函数。

示例

以下两个查询是等效的。

-- Query 1
SELECT year, month, day, count(*) FROM t GROUP BY year, month, day WITH ROLLUP;

-- Query 2
SELECT year, month, day, count(*) FROM t GROUP BY
GROUPING SETS
(
(year, month, day),
(year, month),
(year),
()
);

另请参阅

实现细节

聚合是列式 DBMS 最重要的功能之一,因此它的实现是 ClickHouse 中优化最重的部分之一。默认情况下,聚合是在内存中使用哈希表完成的。它有 40 多种专门化,这些专门化会根据“分组键”数据类型自动选择。

根据表排序键进行 GROUP BY 优化

如果表按某个键排序,并且 GROUP BY 表达式至少包含排序键的前缀或单射函数,则聚合可以更有效地执行。在这种情况下,当从表中读取新键时,可以完成聚合的中间结果并发送到客户端。此行为通过 optimize_aggregation_in_order 设置打开。这种优化减少了聚合期间的内存使用,但在某些情况下可能会减慢查询执行速度。

外部内存中的 GROUP BY

您可以启用将临时数据转储到磁盘以限制 GROUP BY 期间的内存使用。该 max_bytes_before_external_group_by 设置确定将 GROUP BY 临时数据转储到文件系统的阈值 RAM 使用量。如果设置为 0(默认值),则禁用它。

当使用 max_bytes_before_external_group_by 时,我们建议您将 max_memory_usage 设置为大约两倍高。这是必要的,因为聚合有两个阶段:读取数据并形成中间数据(1)和合并中间数据(2)。将数据转储到文件系统只能在阶段 1 期间进行。如果临时数据未转储,则阶段 2 可能需要与阶段 1 相同的内存量。

例如,如果 max_memory_usage 设置为 10000000000 并且您想要使用外部聚合,那么将 max_bytes_before_external_group_by 设置为 10000000000 以及 max_memory_usage 设置为 20000000000 是有意义的。当触发外部聚合(如果至少有一个临时数据转储)时,RAM 的最大消耗仅略高于 max_bytes_before_external_group_by

使用分布式查询处理,外部聚合在远程服务器上执行。为了使请求服务器仅使用少量 RAM,请将 distributed_aggregation_memory_efficient 设置为 1。

当合并刷新到磁盘的数据以及当启用 distributed_aggregation_memory_efficient 设置时从远程服务器合并结果时,会消耗总 RAM 量中的 1/256 * the_number_of_threads

当启用外部聚合时,如果数据少于 max_bytes_before_external_group_by(即数据未刷新),则查询运行速度与没有外部聚合一样快。如果任何临时数据被刷新,运行时间将延长几倍(大约三倍)。

如果您在 GROUP BY 之后有一个带有 LIMITORDER BY,那么使用的 RAM 量取决于 LIMIT 中的数据量,而不是整个表中的数据量。但是,如果 ORDER BY 没有 LIMIT,请不要忘记启用外部排序(max_bytes_before_external_sort)。