Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

GROUP BY

GROUP BY 子句会将 SELECT 查询切换为聚合模式,其工作方式如下:

  • GROUP BY 子句包含一个表达式列表 (或单个表达式,此时视为长度为 1 的列表) 。该列表充当“分组键”,其中每个单独的表达式称为“键表达式”。
  • SELECTHAVINGORDER BY 子句中的所有表达式都必须基于键表达式进行计算,基于作用于非键表达式 (包括普通列) 的聚合函数进行计算。换句话说,从表中选出的每一列,要么用于键表达式,要么位于聚合函数内部,但不能同时用于两者。
  • 聚合 SELECT 查询的结果中包含的行数,将等于源表中“分组键”唯一值的数量。通常,这会显著减少行数,往往会减少几个数量级,但并非总是如此:如果所有“分组键”值都互不相同,行数将保持不变。

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

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 BY 会对 x 求和,就好像 NULL 是一个实际的值一样。

如果向 GROUP BY 传递多个键,结果会返回所选项的所有组合,就好像 NULL 是一个特定的值一样。

ROLLUP 修饰符

ROLLUP 修饰符用于根据 GROUP BY 列表中键表达式的顺序计算小计。小计行会附加在结果表之后。

小计按相反的顺序计算:先为列表中的最后一个键表达式计算小计,再为前一个计算,依此类推,直到第一个键表达式。

在小计行中,已“已分组”的键表达式的值会被设为 0 或空字符串。

示例

假设有表 t:

┌─year─┬─month─┬─day─┐
│ 2019 │     1 │   5 │
│ 2019 │     1 │  15 │
│ 2020 │     1 │   5 │
│ 2020 │     1 │  15 │
│ 2020 │    10 │   5 │
│ 2020 │    10 │  15 │
└──────┴───────┴─────┘
Querysql
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 (此时 monthday 列都补零) ;
  • 以及总计 (此时三个键表达式列都为零) 。
Responsetext
┌─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 关键字来写。

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

另请参阅

CUBE 修饰符

CUBE 修饰符用于计算 GROUP BY 列表中键表达式各种组合的小计。小计行会添加在结果表之后。

在小计行中,所有“已分组”键表达式的值都将被设为 0 或空字符串。

示例

考虑表 t:

┌─year─┬─month─┬─day─┐
│ 2019 │     1 │   5 │
│ 2019 │     1 │  15 │
│ 2020 │     1 │   5 │
│ 2020 │     1 │  15 │
│ 2020 │    10 │   5 │
│ 2020 │    10 │  15 │
└──────┴───────┴─────┘
Querysql
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 中的列会以零填充。

Responsetext
┌─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 关键字来写。

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

另请参见

WITH TOTALS 修饰符

如果指定了 WITH TOTALS 修饰符,则会额外计算出一行。该行的键列包含默认值 (零或空字符串) ,聚合函数列则包含基于所有行计算出的值 (即“总计”值) 。

这个额外的行仅会在 JSON*TabSeparated*Pretty* 格式中生成,并与其他行分开输出:

  • XMLJSON* 格式中,这一行会作为单独的 totals 字段输出。
  • TabSeparated*CSV*Vertical 格式中,这一行位于主结果之后,前面有一个空行 (也就是在其他数据之后) 。
  • Pretty* 格式中,这一行会在主结果后作为单独的表输出。
  • Template 格式中,这一行会按照指定模板输出。
  • 在其他格式中不可用。

存在 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 中所有非聚合函数的表达式。

例如:

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 -- 获取每个域名中第一个出现的页面标题。
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_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) 。

Navigation