跳转到主要内容
GROUP BY 子句会将 SELECT 查询切换为聚合模式,其工作方式如下:
  • GROUP BY 子句包含一个表达式列表 (或单个表达式,此时视为长度为 1 的列表) 。该列表充当“分组键”,其中每个单独的表达式称为“键表达式”。
  • SELECTHAVINGORDER BY 子句中的所有表达式都必须基于键表达式进行计算,基于作用于非键表达式 (包括普通列) 的聚合函数进行计算。换句话说,从表中选出的每一列,要么用于键表达式,要么位于聚合函数内部,但不能同时用于两者。
  • 聚合 SELECT 查询的结果中包含的行数,将等于源表中“分组键”唯一值的数量。通常,这会显著减少行数,往往会减少几个数量级,但并非总是如此:如果所有“分组键”值都互不相同,行数将保持不变。
如果你想按列号而不是列名对表中的数据进行分组,请启用设置 enable_positional_arguments
还有一种对表执行聚合的方式。如果查询只在聚合函数内部使用表列,则可以省略 GROUP BY 子句,此时会假定按空键集进行聚合。这类查询始终只返回一行。

NULL 的处理

对于分组,ClickHouse 将 NULL 视为一个值,并且 NULL==NULL。这与大多数其他上下文中对 NULL 的处理方式不同。 下面的示例说明了这意味着什么。 假设你有这样一张表:
查询 SELECT sum(x), y FROM t_null_big GROUP BY y 的结果如下:
你可以看到,对于 y = NULLGROUP BY 会对 x 求和,就好像 NULL 是一个实际的值一样。 如果向 GROUP BY 传递多个键,结果会返回所选项的所有组合,就好像 NULL 是一个特定的值一样。

ROLLUP 修饰符

ROLLUP 修饰符用于根据 GROUP BY 列表中键表达式的顺序计算小计。小计行会附加在结果表之后。 小计按相反的顺序计算:先为列表中的最后一个键表达式计算小计,再为前一个计算,依此类推,直到第一个键表达式。 在小计行中,已“grouped”的键表达式的值会被设为 0 或空字符串。
请注意,HAVING 子句可能会影响小计结果。
示例 假设有表 t:
Query
由于 GROUP BY 部分包含三个键表达式,因此结果中会有四个表,小计按从右到左的顺序逐级“汇总”:
  • GROUP BY year, month, day;
  • GROUP BY year, month (此时 day 列补零) ;
  • GROUP BY year (此时 monthday 列都补零) ;
  • 以及总计 (此时三个键表达式列都为零) 。
Response
同一个查询也可以用 WITH 关键字来写。
Query
另请参阅

CUBE 修饰符

CUBE 修饰符用于计算 GROUP BY 列表中键表达式各种组合的小计。小计行会添加在结果表之后。 在小计行中,所有“已分组”键表达式的值都将被设为 0 或空字符串。
请注意,HAVING 子句可能会影响小计结果。
示例 考虑表 t:
Query
由于 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 中的列会以零填充。
Response
同一个查询也可以用 WITH 关键字来写。
Query
另请参见

WITH TOTALS 修饰符

如果指定了 WITH TOTALS 修饰符,则会额外计算出一行。该行的键列包含默认值 (零或空字符串) ,聚合函数列则包含基于所有行计算出的值 (即“总计”值) 。 这个额外的行仅会在 JSON*TabSeparated*Pretty* 格式中生成,并与其他行分开输出:
  • XMLJSON* 格式中,这一行会作为单独的 totals 字段输出。
  • TabSeparated*CSV*Vertical 格式中,这一行位于主结果之后,前面有一个空行 (也就是在其他数据之后) 。
  • Pretty* 格式中,这一行会在主结果后作为单独的表输出。
  • Template 格式中,这一行会按照指定模板输出。
  • 在其他格式中不可用。
totals 会在 SELECT 查询结果中输出,但不会在 INSERT INTO ... SELECT 中输出。
存在 HAVING 时,WITH TOTALS 的执行方式可能不同。其行为取决于 totals_mode 设置。

配置 totals 处理

默认情况下,totals_mode = 'before_having'。在这种情况下,totals 会基于所有行进行计算,包括未通过 HAVING 和 max_rows_to_group_by 的行。 其他可选值只会将通过 HAVING 的行计入 totals,并且在设置 max_rows_to_group_bygroup_by_overflow_mode = 'any' 时行为有所不同。 after_having_exclusive – 不包含未通过 max_rows_to_group_by 的行。换句话说,如果省略 max_rows_to_group_bytotals 的行数将小于或等于其原本的行数。 after_having_inclusive – 将所有未通过 max_rows_to_group_by 的行都包含在 totals 中。换句话说,如果省略 max_rows_to_group_bytotals 的行数将大于或等于其原本的行数。 after_having_auto – 统计通过 HAVING 的行数。如果该数量超过某个阈值 (默认为 50%) ,则将所有未通过 max_rows_to_group_by 的行都包含在 totals 中;否则不包含这些行。 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 中所有非聚合函数的表达式。 例如:
与……相同
对于一种特殊情况:如果某个函数的参数同时包含聚合函数和其他字段,那么 GROUP BY 键将包含我们能从中提取出的尽可能多的非聚合字段。 例如:
与……一样

示例

示例:
与 MySQL 不同 (这也符合标准 SQL) ,对于既不在键中、也不在聚合函数中的列,你无法获取其某个值 (常量表达式除外) 。要规避这一限制,可以使用 ‘any’ 聚合函数 (获取遇到的第一个值) 或 ‘min/max’。 示例:
对于每个不同的键值,GROUP BY 都会计算出一组聚合函数值。

GROUPING SETS 修饰符

这是最通用的一种修饰符。 该修饰符允许手动指定多个聚合键集合 (grouping sets) 。 系统会分别对每个 grouping set 执行聚合,之后再将所有结果合并。 如果某列未出现在某个 grouping set 中,则会以默认值填充。 换句话说,上述修饰符都可以通过 GROUPING SETS 来表示。 尽管带有 ROLLUPCUBEGROUPING SETS 修饰符的查询在语法上等价,但它们的执行方式可能不同。 GROUPING SETS 会尝试并行执行所有操作,而 ROLLUPCUBE 则会在单个线程中完成聚合结果的最终合并。 当源列包含默认值时,可能很难区分某一行是否属于以这些列作为键的聚合结果。 为了解决这个问题,必须使用 GROUPING 函数。 示例 以下两个查询是等价的。
另请参阅

实现细节

聚合是列式 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_ratio_before_external_group_by,这样只有当查询使用的内存达到一定阈值时,才允许 GROUP BY 使用外部内存。 使用 max_bytes_before_external_group_by 时,我们建议将 max_memory_usage 设为其大约两倍 (或者将 max_bytes_ratio_before_external_group_by 设为 0.5) 。这是因为聚合分为两个阶段:读取数据并生成中间数据 (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) 。
最后修改于 2026年6月25日