Skip to main content
MergeTree 引擎以及 MergeTree 家族中的其他引擎 (例如 ReplacingMergeTreeAggregatingMergeTree) 是 ClickHouse 中最常用、最稳健的表引擎。 MergeTree 家族表引擎专为高数据摄取速率和海量数据而设计。 插入操作会创建表 parts,这些 parts 会由后台进程与其他表 parts 合并。 MergeTree 家族表引擎的主要特性:
  • 表的主键决定了每个表 part 内部的排序顺序 (聚簇索引) 。主键也不是引用单独的行,而是引用由 8192 行组成、称为粒度的块。这使得海量数据集的主键足够小,能够始终加载在主存中,同时仍可快速访问磁盘上的数据。
  • 表可以使用任意分区表达式进行分区。分区裁剪可确保在查询条件允许时跳过读取某些分区。
  • 数据可以在多个集群节点之间复制,以实现高可用、故障转移和零停机升级。请参阅 数据复制
  • MergeTree 表引擎支持多种统计信息类型和采样方法,以帮助进行查询优化。
尽管名称相似,Merge 引擎与 *MergeTree 引擎并不相同。

创建表

有关参数的详细说明,请参见 CREATE TABLE 语句

查询子句

ENGINE

ENGINE — 引擎名称及其参数。ENGINE = MergeTree()MergeTree 引擎没有参数。

ORDER BY

ORDER BY — 排序键。 由列名或任意表达式组成的 Tuple。例如:ORDER BY (CounterID + 1, EventDate) 如果未定义主键 (即未指定 PRIMARY KEY) ,ClickHouse 会将排序键作为主键使用。 如果不需要排序,可以使用语法 ORDER BY tuple()。 另外,如果启用了设置 create_table_empty_primary_key_by_default,则会在 CREATE TABLE 语句中隐式添加 ORDER BY ()。另请参见选择主键

PARTITION BY

PARTITION BY分区键。可选。在大多数情况下,不需要分区键;即使需要分区,分区键通常也无需细于按月分区。分区不会加速查询 (这与 ORDER BY 表达式不同) 。切勿使用粒度过细的分区方式。不要按客户端标识符或名称对数据进行分区 (应改为将客户端标识符或名称作为 ORDER BY 表达式中的第一列) 。 对于按月分区,请使用 toYYYYMM(date_column) 表达式,其中 date_column 是一个类型为 Date 的日期列。此处的分区名称采用 "YYYYMM" 格式。

PRIMARY KEY

PRIMARY KEY —— 如果主键不同于排序键,则在此处指定。可选。 指定排序键 (使用 ORDER BY 子句) 时,也会隐式指定主键。 通常不需要在排序键之外再单独指定主键。

SAMPLE BY

SAMPLE BY — 采样表达式。可选。 如果指定了 SAMPLE BY,则该表达式必须包含在主键中。 采样表达式的结果必须为无符号整数。 示例:SAMPLE BY intHash32(UserID) ORDER BY (CounterID, EventDate, intHash32(UserID))

TTL

TTL — 一组规则,用于指定行的存储时长,以及 在磁盘和卷之间自动移动 parts 的逻辑。可选。 表达式必须返回 DateDateTime,例如 TTL date + INTERVAL 1 DAY 规则类型 DELETE|TO DISK 'xxx'|TO VOLUME 'xxx'|GROUP BY 指定当表达式条件满足时 (即达到当前时间) 要对 part 执行的操作:删除过期行、将 part (如果表达式对 part 中的所有行都成立) 移动到指定磁盘 (TO DISK 'xxx') 或卷 (TO VOLUME 'xxx') ,或者对过期行中的值进行聚合。规则的默认类型为删除 (DELETE) 。可以指定多条规则,但 DELETE 规则最多只能有一条。 更多详情,请参见 列和表的 TTL

设置

请参阅 MergeTree 设置 Sections 设置的示例
在该示例中,我们按月进行分区。 我们还将采样表达式设置为基于用户 ID 的哈希值。这样可以让表中的数据按每个 CounterIDEventDate 呈现伪随机分布。如果你在查询数据时指定 SAMPLE 子句,ClickHouse 将为部分用户返回均匀的伪随机数据样本。 可以省略 index_granularity 设置,因为 8192 是默认值。

数据存储

一个表由按主键排序的数据 parts 组成。 当数据插入表中时,会创建独立的数据 parts,并且每个 part 都会按主键的字典序排序。例如,如果主键是 (CounterID, Date),则 part 中的数据先按 CounterID 排序,并在每个 CounterID 内再按 Date 排序。 属于不同分区的数据会分开存放到不同的数据 parts 中。在后台,ClickHouse 会合并数据 parts 以提高存储效率。属于不同分区的 parts 不会被合并。合并机制并不保证具有相同主键的所有行都位于同一个数据 part 中。 数据 parts 可以采用 WideCompact 格式存储。在 Wide 格式中,每一列都存储为文件系统中的单独文件;在 Compact 格式中,所有列都存储在一个文件中。Compact 格式可用于提升小批量高频写入的性能。 数据存储格式由表引擎的 min_bytes_for_wide_partmin_rows_for_wide_part 设置控制。如果数据 part 中的字节数或行数小于相应设置值,则该 part 以 Compact 格式存储。否则,以 Wide 格式存储。如果这两个设置都未设置,则数据 parts 以 Wide 格式存储。 每个数据 part 在逻辑上都会划分为粒度。粒度是 ClickHouse 在查询数据时读取的最小不可分割数据集。ClickHouse 不会拆分行或值,因此每个粒度始终包含整数数量的行。粒度的第一行会用该行主键的值进行标记。对于每个数据 part,ClickHouse 都会创建一个存储这些标记的索引文件。对于每一列,无论它是否属于主键,ClickHouse 也会存储相同的标记。这些标记使你能够直接在列文件中定位数据。 粒度大小受表引擎的 index_granularityindex_granularity_bytes 设置限制。粒度中的行数位于 [1, index_granularity] 范围内,具体取决于行的大小。如果单行大小大于该设置的值,则粒度大小可以超过 index_granularity_bytes。在这种情况下,粒度大小等于该行的大小。

查询中的主键和索引

(CounterID, Date) 主键为例。在这种情况下,排序方式和索引如下所示:
如果数据查询指定了:
  • CounterID in ('a', 'h'),服务器会读取标记范围 [0, 3)[6, 8) 中的数据。
  • CounterID IN ('a', 'h') AND Date = 3,服务器会读取标记范围 [1, 3)[7, 8) 中的数据。
  • Date = 3,服务器会读取标记范围 [1, 10] 中的数据。
上述示例表明,使用索引始终比全表扫描更高效。 稀疏索引会导致读取额外的数据。读取主键的单个范围时,每个数据块中最多可能会额外读取 index_granularity * 2 行。 稀疏索引使你能够处理数量非常庞大的表中的行,因为在大多数情况下,这类索引都能装入计算机的 RAM。 ClickHouse 不要求主键是唯一的。你可以插入多行具有相同主键的数据。 你可以在 PRIMARY KEYORDER BY 子句中使用 Nullable 类型的表达式,但强烈不建议这样做。要启用此功能,请打开 allow_nullable_key 设置。对于 ORDER BY 子句中的 NULL 值,适用 NULLS_LAST 原则。

选择主键

主键包含的列数没有明确限制。你可以根据数据结构,在主键中包含更多或更少的列。这可能会:
  • 提高索引性能。 如果主键是 (a, b),那么在满足以下条件时,加入另一列 c 可以提升性能:
    • 存在对列 c 进行条件过滤的查询。
    • 值相同的 (a, b) 对应的长数据范围 (长度达到 index_granularity 的数倍) 很常见。换句话说,增加另一列后,就能跳过相当长的数据范围。
  • 改善数据压缩效果。 ClickHouse 按主键对数据排序,因此数据一致性越高,压缩效果越好。
  • CollapsingMergeTreeSummingMergeTree 引擎合并数据 parts 时,提供额外的逻辑。 在这种情况下,指定一个不同于主键的排序键是合理的。
较长的主键会对插入性能和内存消耗产生负面影响,但主键中的额外列不会影响 ClickHouse 在 SELECT 查询期间的性能。 你可以使用 ORDER BY tuple() 语法创建不带主键的表。在这种情况下,ClickHouse 会按插入顺序存储数据。如果你希望在通过 INSERT ... SELECT 查询插入数据时保留数据顺序,请设置 max_insert_threads = 1 如果要按初始顺序读取数据,请使用单线程 SELECT 查询。

选择与排序键不同的主键

可以指定一个不同于排序键的主键 (即一个表达式,其值会针对每个标记写入索引文件) 。排序键则是用于对数据 parts 中的行进行排序的表达式。在这种情况下,主键表达式 Tuple 必须是排序键表达式 Tuple 的前缀。 当使用 SummingMergeTreeAggregatingMergeTree 表引擎时,此功能很有帮助。在这些引擎的常见用法中,表通常包含两类列:维度度量。典型查询会对度量列的值进行聚合,使用任意的 GROUP BY,并按维度过滤。由于 SummingMergeTree 和 AggregatingMergeTree 会聚合排序键值相同的行,因此很自然会将所有维度都加入排序键。这样一来,键表达式就会由一长串列组成,而且每次新增维度时,都需要频繁更新这个列表。 在这种情况下,更合理的做法是仅在主键中保留少量能够提供高效范围扫描的列,并将其余维度列加入排序键 Tuple。 对排序键执行 ALTER 是一种轻量级操作,因为当新列同时添加到表和排序键中时,现有的数据 parts 不需要修改。由于旧排序键是新排序键的前缀,并且新添加的列中还没有数据,因此在修改表的那一刻,数据同时满足按旧排序键和新排序键排序。

在查询中使用索引和分区

对于 SELECT 查询,ClickHouse 会分析是否能够使用索引。如果 WHERE/PREWHERE 子句中包含某个表达式 (无论是作为合取条件之一,还是整个子句本身) ,且该表达式表示等值或不等值比较,或者包含带固定前缀的 INLIKE,并且作用于主键或分区键中的列或表达式、这些列的某些部分重复函数,或这些表达式之间的逻辑关系,那么就可以使用索引。 因此,可以在主键的一个或多个范围上快速执行查询。在此示例中,如果查询条件是特定跟踪标签、特定标签加日期范围、特定标签加日期、多个标签加日期范围等,查询都会很快。 下面来看如下配置的引擎:
在这种情况下,在查询语句中:
ClickHouse 将使用主键索引裁剪不符合条件的数据,并使用按月分区键裁剪日期范围不合适的分区。 上述查询表明,即使对于复杂表达式,也会使用索引。对表的读取方式经过精心组织,因此使用索引的速度不会比全表扫描更慢。 在下面的示例中,索引无法使用。
要检查 ClickHouse 在执行查询时是否能够使用索引,请使用设置 force_index_by_dateforce_primary_key 按月分区的键可以只读取包含相应日期范围的数据块。在这种情况下,一个数据块可能包含多个日期的数据 (最多可覆盖整整一个月) 。在一个块内,数据按主键排序,而主键的第一列不一定是日期。因此,如果查询仅包含日期条件,且未指定主键前缀,那么读取的数据会比只查询单个日期时更多。

在主键中为确定性表达式使用索引

主键不仅可以包含列名,也可以包含表达式。这些表达式并不限于简单的函数链;只要是确定性的,它们就可以是任意的表达式树 (例如嵌套函数和复合表达式) 。 如果一个表达式对相同的输入值始终返回相同的结果 (例如:length()toDate()lower()left()cityHash64()toUUID();不同于 now()rand()) ,那么它就是确定性的。如果主键包含确定性表达式,ClickHouse 就可以将它们应用到查询中的常量值上,并利用结果在主键索引上构造条件。这使得 =INhas 等谓词也能触发数据跳过。 一个常见用例是让主键保持紧凑 (例如,存储哈希而不是较长的 String) ,同时仍然允许针对原始列的谓词使用索引。 确定性 (但非单射) 主键示例:
可使用该索引的谓词示例:
在这些情况下,ClickHouse 会将 length('alice') (以及其他常量) 预先计算一次,并利用这些长度值来缩小主键索引中的范围。由于字符串长度不是单射函数,不同的 user_id 字符串可能具有相同的长度,因此索引可能会读取额外的粒度 (假阳性) 。不过结果仍然是正确的,因为读取之后仍会继续应用原始谓词 (user_id = ...IN 等) 。 如果该确定性表达式同时也是单射的 (对于所使用的参数类型,不同输入不会产生相同输出) ,那么 ClickHouse 还可以将索引有效地用于其否定形式:!=NOT INNOT has(...)。例如,reverse(p)hex(p)String 来说都是单射的。 单射主键示例:
还支持更复杂的单射表达式,例如:
可使用该索引的谓词示例:

部分单调主键的索引使用

例如,一个月中的日期在当月内构成单调序列,但如果时间跨度更长,就不再具有单调性。这就是部分单调序列。如果用户使用部分单调主键创建表,ClickHouse 仍会像往常一样创建稀疏索引。当用户从这类表中查询数据时,ClickHouse 会分析查询条件。如果用户要获取索引中两个索引标记之间的数据,并且这两个索引标记都位于同一个月内,那么在这种特定情况下,ClickHouse 可以使用索引,因为它能够计算查询参数与索引标记之间的距离。 如果查询参数范围内的主键值不构成单调序列,ClickHouse 就无法使用索引。在这种情况下,ClickHouse 会采用全表扫描。 ClickHouse 不仅将这一逻辑用于“月中日期”这类序列,也适用于任何表示部分单调序列的主键。

数据跳过索引

索引声明位于 CREATE 语句的 columns 部分。
对于 *MergeTree 家族的表,可以指定数据跳过索引。 这些索引会在由 granularity_value 个粒度组成的块上,为指定表达式聚合一些信息 (粒度大小通过表引擎中的 index_granularity 设置指定) 。随后,这些聚合信息会用于 SELECT 查询,通过跳过那些无法满足 where 查询条件的大块数据,减少从磁盘读取的数据量。 GRANULARITY 子句可以省略,granularity_value 的默认值为 1。 示例
示例中的索引可供 ClickHouse 在以下查询中使用,以减少需要从磁盘读取的数据量:
还可以在复合列上创建数据跳过索引:

跳过索引类型

MergeTree 表引擎支持以下类型的跳过索引。 有关如何使用跳过索引来优化性能的更多信息, 请参阅“了解 ClickHouse 数据跳过索引”

MinMax 跳过索引

对于每个索引粒度,都会存储表达式的最小值和最大值。 (如果该表达式的类型为 tuple,则会分别存储元组中每个元素的最小值和最大值。)
Syntax

Set

对于每个索引粒度,最多存储指定表达式的 max_rows 个唯一值。 max_rows = 0 表示“存储所有唯一值”。
Syntax

布隆过滤器

对于每个索引粒度,都会为指定列存储一个 布隆过滤器
Syntax
false_positive_rate 参数可以取 0 到 1 之间的值 (默认值:0.025) ,用于指定产生误报的概率 (这会增加需要读取的数据量) 。 支持以下数据类型:
  • (U)Int*
  • Float*
  • Enum
  • Date
  • DateTime
  • String
  • FixedString
  • Array
  • LowCardinality
  • Nullable
  • UUID
  • Map
Map 数据类型:使用键或值创建索引对于 Map 数据类型,客户端可以使用 mapKeysmapValues 函数来指定索引是基于键还是基于值创建的。
JSON 数据类型:为 JSON 路径创建索引对于 JSON 数据类型,可以使用 JSONAllPaths 函数在路径集合上创建 bloom filter 索引。这样可以跳过不存在所查询 JSON 路径的粒度。详见 JSON 的数据跳过索引

N-gram 布隆过滤器 (已弃用)

自 ClickHouse 26.2 版本起,随着 text 索引正式进入 GA,ngrambf_v1 索引已不再建议用于全文检索。详情请参阅页面 “使用文本索引进行全文检索”
对于每个索引粒度,都会为指定列的 n-grams 存储一个 布隆过滤器
Syntax
该索引仅适用于以下数据类型: 要估算 ngrambf_v1 的参数,可以使用以下用户自定义函数 (UDF)
UDFs for ngrambf_v1
要使用这些函数,您至少需要指定两个参数:
  • total_number_of_all_grams
  • probability_of_false_positives
例如,粒度中有 4300 个 ngram,并且您预计误报率会低于 0.0001。 然后可以通过执行以下查询来估算其他参数:
当然,你也可以使用这些函数来为其他场景估算参数。 上述函数参考了这里的布隆过滤器计算器。

标记布隆过滤器

自 ClickHouse 26.2 版本起,随着 text 索引进入正式可用 (GA) 阶段,tokenbf_v1 索引已不再推荐用于全文检索。详情请参见页面 “使用文本索引进行全文检索”
Syntax

稀疏 grams 布隆过滤器

稀疏 grams 布隆过滤器与 ngrambf_v1 类似,但它使用的是稀疏 grams 标记,而非 ngrams。
Syntax

文本索引

在分词后的字符串数据上构建倒排索引,以支持高效且具确定性的全文搜索。详见此处

向量相似性

支持近似最近邻搜索,详见此处

函数支持

WHERE 子句中的条件包含对作用于列的函数调用。如果该列属于某个索引的一部分,ClickHouse 会在执行这些函数时尝试使用该索引。ClickHouse 对可使用索引的函数支持不同的子集。 set 类型的索引可用于所有函数。其他索引类型的支持情况如下: 常量参数小于 ngram 大小的函数,不能被 ngrambf_v1 用于查询优化。 (*) 要使 hasTokenCaseInsensitivehasTokenCaseInsensitiveOrNull 生效,必须基于转为小写的数据创建 tokenbf_v1 索引,例如 INDEX idx (lower(str_col)) TYPE tokenbf_v1(512, 3, 0)
布隆过滤器可能出现误报,因此 ngrambf_v1tokenbf_v1sparse_gramsbloom_filter 索引不能用于优化那些函数结果预期为 false 的查询。例如:
  • 可以优化:
    • s LIKE '%test%'
    • NOT s NOT LIKE '%test%'
    • s = 1
    • NOT s != 1
    • startsWith(s, 'test')
  • 不能优化:
    • NOT s LIKE '%test%'
    • s NOT LIKE '%test%'
    • NOT s = 1
    • s != 1
    • NOT startsWith(s, 'test')

投影

投影类似于 materialized views,但定义在分片级别。它们既提供一致性保证,也会在查询中自动使用。
在实现投影时,还应考虑 force_optimize_projection 设置。
带有 FINAL 修饰符的 SELECT 语句不支持投影。

投影查询

投影查询用于定义投影。它会隐式地从父表中选择数据。 语法
可以使用 ALTER 语句修改或删除投影。

投影索引

投影索引通过提供一种轻量且显式的方式来定义投影级索引,扩展了投影子系统。 从外部看,投影索引仍然属于投影,但语法更简洁、意图更明确:它定义的是专用于过滤的表达式,而不是用于提供 materialized 数据。 在内部,投影索引不像常规投影那样按置换后的行顺序物化原始表。 相反,这种置换会以数值置换列 _part_offset 的形式存储,即 SELECT _part_offset ORDER BY <index_expr>

语法

示例:

索引类型

当前支持:
  • basic:等同于表达式上的普通 MergeTree 索引。
该框架今后还可支持更多索引类型。

投影存储

投影存储在分片目录中。它类似于索引,但会包含一个子目录,用于存储匿名 MergeTree 表的分片。该表由投影的定义查询推导而来。如果存在 GROUP BY 子句,底层存储引擎就会变为 AggregatingMergeTree,并且所有聚合函数都会转换为 AggregateFunction。如果存在 ORDER BY 子句,MergeTree 表会将其用作主键表达式。在合并过程中,投影分片会通过其存储引擎的合并例程进行合并。父表分片的校验和会与投影分片的校验和组合在一起。其他维护作业与跳过索引类似。

查询分析

  1. 检查投影是否可用于回答给定的查询,也就是说,它产生的结果应与查询基表得到的结果相同。
  2. 选择最佳的可行匹配,即需要读取的粒度最少的匹配。
  3. 使用投影的查询管道会不同于使用原始 parts 的查询管道。如果某些 parts 中缺少投影,我们可以在运行时添加一个管道来即时对其进行“投影”。

并发数据访问

对于表的并发访问,我们采用多版本机制。换句话说,当表同时发生读取和更新时,读取的数据来自查询发起时当前的一组 parts。整个过程无需长时间加锁。插入操作不会影响读取操作。 从表中读取数据会自动并行执行。

列和表的 生存时间 (TTL)

用于确定值的生存时间。 可以为整个表以及每个单独的列设置 TTL 子句。表级 TTL 还可以指定数据在磁盘和卷之间自动移动的规则,或者在数据全部过期后重新压缩 parts。 表达式的求值结果必须为 DateDate32DateTimeDateTime64 数据类型。
避免在 TTL 表达式中使用非确定性函数TTL 会在后台合并期间计算,而不是在写入时计算。 rand()now()now64() 等函数会在每次合并时重新计算,从而导致不可预测的删除行为。 ClickHouse 会阻止完全不依赖任何列的表达式,但目前不会拒绝与列引用混合使用的非确定性函数 (例如 ts + rand()) 。为了获得可预测的结果,TTL 表达式应仅基于确定性的列派生值。
语法 为列设置生存时间:
要定义 interval,请使用时间间隔运算符,例如:

列 生存时间 (TTL)

当列中的值过期时,ClickHouse 会将其替换为该列数据类型的默认值。如果数据分区片段中该列的所有值都已过期,ClickHouse 会从文件系统中的该数据分区片段删除该列。 TTL 子句不能用于键列。 示例

创建带 TTL 的表:

为现有表的列添加生存时间 (TTL)

修改列的生存时间 (TTL)

表生存时间 (TTL)

表可以包含一个用于删除过期行的表达式,以及多个用于在磁盘或卷之间自动移动 分片 的表达式。当表中的行过期时,ClickHouse 会删除所有对应的行。对于 分片 的移动或重新压缩,某个 分片 中的所有行都必须满足 TTL 表达式的条件。
每个 TTL 表达式后面都可以指定一个 TTL 规则类型。它决定了当表达式满足时 (即达到当前时间) 要执行的操作:
  • DELETE - 删除已过期的行 (默认操作) ;
  • RECOMPRESS codec_name - 使用 codec_name 重新压缩数据分区片段;
  • TO DISK 'aaa' - 将分区移动到磁盘 aaa
  • TO VOLUME 'bbb' - 将分区移动到卷 bbb
  • GROUP BY - 对已过期的行进行聚合。
DELETE 操作可以与 WHERE 子句配合使用,根据过滤条件仅删除部分已过期的行:
GROUP BY 表达式必须是表主键的前缀。 如果某一列不在 GROUP BY 表达式中,且未在 SET 子句中显式设置,则在结果行中,该列会包含分组后各行中的某个值 (就像对其应用了聚合函数 any 一样) 。 示例

创建带有 TTL 的表:

修改表的 TTL

创建一个表,其中行会在一个月后过期。过期后,日期为星期一的行会被删除:

创建表,并对过期行重新压缩:

创建一个表,其中过期的行会被聚合。在结果行中,x 包含分组后各行中的最大值,y——最小值,d——分组后各行中的任意一个值。

删除过期数据

带有已过期 TTL 的数据会在 ClickHouse 合并数据 parts 时被删除。 当 ClickHouse 检测到数据已过期时,会执行一次计划外合并。要控制此类合并的频率,可以设置 merge_with_ttl_timeout。如果该值过低,系统会执行大量计划外合并,可能消耗大量资源。 如果你在两次合并之间执行 SELECT 查询,可能会读到已过期的数据。为避免这种情况,请在执行 SELECT 之前先使用 OPTIMIZE 查询。 另请参见

磁盘类型

除了本地块设备外,ClickHouse 还支持以下存储类型:

使用多个块设备存储数据

简介

MergeTree 家族表引擎可以将数据存储在多个块设备上。例如,当某个表的数据天然分为“热”数据和“冷”数据时,这会很有用。较新的数据会被频繁查询,但只占用少量空间。相反,呈长尾分布的历史数据很少被查询。如果有多个磁盘可用,“热”数据可以放在高速磁盘上 (例如 NVMe SSD 或内存中) ,而“冷”数据则放在相对较慢的磁盘上 (例如 HDD) 。 这适用于所有磁盘类型,包括 S3 和其他对象存储磁盘。例如,你可以在单个卷内将数据分散到多个 S3 存储桶中,或者创建分层策略,将数据从本地磁盘迁移到 S3。详见使用带多个卷的 S3 磁盘 数据分区片段是 MergeTree 引擎表中最小的可移动单元。一个数据分区片段中的数据存储在同一个磁盘上。parts 既可以在后台于磁盘之间移动 (根据用户设置) ,也可以通过 ALTER 查询移动。

术语

  • 磁盘 — 挂载到文件系统上的块设备。
  • 默认磁盘 — 用于存储 path 服务器设置中指定路径的磁盘。
  • 卷 — 由相同类型磁盘组成的有序集合 (类似于 JBOD) 。
  • 存储策略 — 由一组卷及其之间的数据移动规则构成。
上述实体的名称可在系统表 system.storage_policiessystem.disks 中找到。要为某个表应用已配置的存储策略,请使用 MergeTree 引擎家族表的 storage_policy 设置。

配置

磁盘、卷和存储策略应在 <storage_configuration> 标签内声明,并放在 config.d 目录中的某个文件中。
也可以在查询的 SETTINGS 部分中声明磁盘。这对于即席分析很有用, 例如临时附加一个托管在 URL 上的磁盘。 更多详情请参阅动态存储
配置结构:
标签:
  • <disk_name_N> — 磁盘名称。所有磁盘的名称都必须不同。
  • path — 服务器用于存储数据 (datashadow 文件夹) 的路径,应以 ’/’ 结尾。
  • keep_free_space_bytes — 需要预留的磁盘可用空间大小。
磁盘定义的顺序并不重要。 存储策略配置标记:
标签:
  • policy_name_N — 策略名称。策略名称必须唯一。
  • volume_name_N — 卷名称。卷名称必须唯一。
  • disk — 卷中的一个磁盘。
  • max_data_part_size_bytes — 该卷中任一磁盘可存储的数据分区片段的最大大小。如果估算出的合并后分区片段大小超过 max_data_part_size_bytes,则该分区片段会写入下一个卷。该功能主要用于将新的或较小的parts保存在热 (SSD) 卷上,并在其变大后移动到冷 (HDD) 卷上。如果你的策略只有一个卷,请不要使用此设置。
  • move_factor — 当可用空间低于该系数时,数据会自动开始移动到下一个卷 (如果存在,默认值为 0.1) 。ClickHouse 会按大小从大到小 (降序) 对现有parts进行排序,并选择总大小足以满足 move_factor 条件的parts。如果所有parts的总大小仍不足,则会移动全部parts。
  • perform_ttl_move_on_insert — 禁止在数据分区片段 INSERT 时执行 TTL 移动。默认情况下 (启用时) ,如果插入的数据分区片段按照 TTL 移动规则已经过期,它会立即进入移动规则中声明的卷/磁盘。如果目标卷/磁盘较慢 (例如 S3) ,这可能会显著拖慢插入速度。如果禁用,则已过期的数据分区片段会先写入默认卷,然后立即移动到 TTL 卷。
  • load_balancing - 磁盘均衡策略,round_robinleast_used
  • least_used_ttl_ms - 配置所有磁盘可用空间更新的超时时间 (毫秒) (0 - 始终更新,-1 - 从不更新,默认值为 60000) 。注意,如果该磁盘仅供 ClickHouse 使用,且不会发生在线文件系统扩容/缩容,可以使用 -1;其他情况下不建议这样做,因为最终会导致空间分布不正确。
  • prefer_not_to_merge — 你不应该使用此设置。它会禁用该卷上的数据分区片段合并 (这有害,并会导致性能下降) 。启用此设置时 (不要这样做) ,将不允许在该卷上合并数据 (这很糟糕) 。这让你可以 (但其实不需要) 控制 (如果你想控制这个,说明你做错了) ClickHouse 如何使用慢速磁盘 (但 ClickHouse 更清楚该怎么做,所以请不要使用此设置) 。
  • volume_priority — 定义卷的填充优先级 (顺序) 。值越小,优先级越高。参数值应为自然数,并且整体上必须连续覆盖从 1 到 N (N 为最低优先级) 之间的范围,中间不能跳过任何数字。
    • 如果 所有 卷都设置了该标签,则按给定顺序确定优先级。
    • 如果只有 部分 卷设置了该标签,则未设置标签的卷优先级最低,并按其在配置中定义的顺序确定优先级。
    • 如果 没有 卷设置该标签,则其优先级将按它们在配置中声明的顺序确定。
    • 两个卷不能具有相同的优先级值。
配置示例:
在给定示例中,hdd_in_order 策略采用了 round-robin 方法。因此,该策略只定义了一个卷 (single) ,parts 会按循环顺序存储到该卷的所有磁盘上。如果系统挂载了多个性能相近的磁盘,但未配置 RAID,这种策略会非常有用。请注意,单个磁盘本身并不可靠,你可能需要通过将复制因子设为 3 或更高来弥补这一点。 如果系统中有不同类型的磁盘,则可以改用 moving_from_ssd_to_hdd 策略。卷 hot 由一个 SSD 磁盘 (fast_ssd) 组成,且可存储在该卷上的单个分片最大为 1GB。所有大于 1GB 的 parts 都会直接存储到 cold 卷上,该卷包含一个 HDD 磁盘 disk1。 此外,一旦 fast_ssd 磁盘的使用率超过 80%,后台进程就会将数据转移到 disk1 在存储策略中,卷的枚举顺序很重要,尤其是在所列卷中至少有一个未显式设置 volume_priority 参数时。 一旦某个卷满了,数据就会被移动到下一个卷。磁盘的枚举顺序也同样重要,因为数据会依次存储到这些磁盘上。 创建表时,可以为其应用一个已配置的存储策略:
default 存储策略表示只使用一个卷,而该卷仅包含 <path> 中指定的一个磁盘。 你可以在创建表后使用 [ALTER TABLE … MODIFY SETTING] 查询更改存储策略;新策略应包含所有旧磁盘和卷,且名称必须保持不变。 执行数据分区片段后台移动的线程数可以通过 background_move_pool_size 设置进行调整。

详细说明

对于 MergeTree 表,数据会通过不同方式写入磁盘: 除变更和分区冻结外,在上述所有情况下,parts 都会根据给定的存储策略存储到某个卷和磁盘上:
  1. 选择第一个 (按定义顺序) 有足够磁盘空间存储 parts (unreserved_space > current_part_size) ,并且允许存储该大小 parts (max_data_part_size_bytes > current_part_size) 的卷。
  2. 在该卷内,选择紧随上一个用于存储前一个 chunk 数据的磁盘之后的那个磁盘,并且该磁盘的可用空间大于 parts 的大小 (unreserved_space - keep_free_space_bytes > current_part_size) 。
在底层实现中,变更和分区冻结会使用硬链接。不同磁盘之间不支持硬链接,因此在这种情况下,生成的 parts 会存储在与原始 parts 相同的磁盘上。 在后台,parts 会根据空闲空间量 (move_factor parameter) ,按照各卷在配置文件中声明的顺序,在卷之间移动。 数据永远不会从最后一个卷转移到第一个卷,也不会从第一个卷转移到最后一个卷。可以使用系统表 system.part_log (field type = MOVE_PART) 和 system.parts (fields pathdisk) 来监控后台移动。此外,也可以在服务器日志中找到详细信息。 用户可以使用查询 ALTER TABLE … MOVE PART|PARTITION … TO VOLUME|DISK … 强制将某个 parts 或分区从一个卷移动到另一个卷或磁盘,同时会考虑后台操作的所有限制条件。该查询会自行发起移动,不会等待后台操作完成。如果可用空间不足,或者任何必需条件未满足,用户将收到错误消息。 移动数据不会干扰数据复制。因此,对于同一张表,可以在不同副本上指定不同的存储策略。 后台合并和变更完成后,旧 parts 只有在经过一定时间 (old_parts_lifetime) 之后才会被删除。 在此期间,它们不会被移动到其他卷或磁盘。因此,在这些 parts 最终被删除之前,它们仍会被计入已占用磁盘空间的评估中。 用户可以使用 min_bytes_to_rebalance_partition_over_jbod setting,将新的大 parts 以均衡方式分配到某个 JBOD 卷的不同磁盘上。

使用外部存储来存储数据

MergeTree 家族表引擎可以通过类型分别为 s3azure_blob_storagehdfs 的磁盘,将数据存储到 S3AzureBlobStorageHDFS 中。更多详情请参见配置外部存储选项 以下示例展示了如何使用类型为 s3 的磁盘将 S3 用作外部存储。 配置如下:
另见配置外部存储选项

在多个卷中使用 S3 磁盘

S3 (以及其他对象存储) 磁盘可像本地磁盘一样用于多磁盘和多卷存储策略。这样,你就可以在单个卷内将数据分布到多个 S3 存储桶中 (类似 JBOD) ,或者使用 S3 卷来设置分层存储策略。 例如,要以轮询方式将数据分布到两个 S3 存储桶中:
您还可以在分层存储策略中结合本地卷和 S3 卷,例如随着数据变旧,将其从本地 SSD 移动到 S3:
使用 use_environment_credentials 进行 S3 身份验证时,环境凭据 (AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_SESSION_TOKEN) 会在所有 S3 磁盘之间共享。无法为不同磁盘使用不同的环境凭据。如果每个 S3 磁盘都需要不同的凭据,请改为在每个磁盘上显式设置 access_key_idsecret_access_key
可以在共享存储上为非复制的 MergeTree 表配置单写入者、多读取者的场景。这是通过自动刷新 parts 列表实现的,并且可以在读取端进行设置。请注意,这要求副本之间共享文件系统元数据 (或者在使用表本地磁盘时设置 table_disk = true) 。请参阅 refresh_parts_interval and table_disk
缓存配置ClickHouse 22.3 到 22.7 版本使用不同的缓存配置;如果您使用的是其中一个版本,请参阅 using local cache

虚拟列

  • _part — 分片 的名称。
  • _part_index — 该 分片 在查询结果中的顺序索引。
  • _part_starting_offset — 该 分片 在查询结果中的累计起始行。
  • _part_offset — 该 分片 中的行号。
  • _part_granule_offset — 该 分片 中的粒度编号。
  • _partition_id — 分区名称。
  • _part_uuid — 分片 的唯一标识符 (如果启用了 MergeTree 设置 assign_part_uuids) 。
  • _part_data_version — 分片 的数据版本 (最小块编号或变更版本) 。
  • _partition_valuepartition by 表达式的值 (一个元组) 。
  • _sample_factor — 样本系数 (来自查询) 。
  • _block_number — 在插入时分配给该行的原始块编号;启用设置 enable_block_number_column 后,在合并时会保留。
  • _block_offset — 在插入时分配给该行的原始块内行号;启用设置 enable_block_offset_column 后,在合并时会保留。
  • _disk_name — 用于存储的磁盘名称。

列统计信息

对于 *MergeTree* 家族的表,统计信息是在 CREATE 查询的 columns 部分中声明的:
我们也可以使用 ALTER 语句来修改统计信息:
这些轻量级统计信息用于汇总列中值的分布情况。统计信息存储在每个分片中,并会在每次插入时更新。 只有启用 set use_statistics = 1 后,才能将其用于 prewhere 优化。

利用统计信息进行分片裁剪

启用 use_statistics_for_part_pruning 后,即可使用统计信息进行分片裁剪。 目前,只有 basic 统计信息 (以及已弃用的 minmax 统计信息) 支持分片裁剪。为某一列定义此类统计信息后,ClickHouse 会跟踪该列在每个数据分片中的最小值和最大值。 分片裁剪可以在查询过滤条件不可能匹配该分片中的任何行时,跳过读取整个数据分片。 示例:

可用的列统计信息类型

  • basic 从列中提取的一组紧凑的单值摘要。根据列类型,会填充以下内容:
    • 对于任何以数字表示值的列 (整数、浮点数、Decimal*Date*DateTime*Enum*IPv4,……) :最小值和最大值,可用于估算范围过滤器的选择性并启用分片裁剪;
    • 对于 StringFixedString 列:非 NULL 值的总字节长度 (可由此推导出平均字符串长度) ;
    • 对于 NullableLowCardinality(Nullable) 列:NULL 值的数量,优化器会用它在选择性估算中扣除 NULL 行。 单个 basic 统计信息可以同时填充其中多项——例如在 Nullable(UInt32) 列上,它会同时跟踪数值最小/最大值和 NULL 计数。与 minmax 相比,basic 还可用于 String / FixedString 列,并且也可以仅为了跟踪 NULL 计数而声明在 UUIDIPv6 等类型的 Nullable 包装类型上。
  • minmax (已弃用)
minmax 统计信息已弃用,且无法再创建 (CREATE TABLE ... STATISTICS(minmax)ALTER TABLE ... ADD/MODIFY STATISTICS ... TYPE minmax 会返回错误) 。带有 minmax 统计信息的现有表和 parts 仍可继续工作。请改用 basic 统计信息。
  • tdigest
tdigest 类型的统计信息创建成本较高,并且可能会降低数据摄取速度。
TDigest 草图,可用于计算数值列的近似分位数 (例如第 90 百分位) 。
  • uniq BJKST 草图,可用于估算某列包含多少个不同值。其内部使用 uniq
  • uniq_v2 uniq 类似,但内部使用 uniqCombined(12) (HyperLogLog 的一种变体) 。其内存消耗比 uniq 更少,并且构建速度更快。
  • countmin
countmin 类型的统计信息创建成本较高,并且可能会降低数据摄取速度。
CountMin 草图,可提供列中各个值出现频率的近似计数。

支持的数据类型

上述各项也都支持所列类型的 NullableLowCardinality(Nullable) 包装类型。Basic 还可以额外声明在诸如 UUIDIPv6 等类型的 Nullable 包装器上,仅用于统计空值数量。

支持的操作

对于 String / FixedString 列上的 basic,该统计信息仅记录非 NULL 值的总字节长度 (用于估算平均字符串长度) 以及 NULL 计数; 它不用于范围过滤器或分片裁剪。

列级设置

某些 MergeTree 设置可以在列级别进行覆盖:
  • max_compress_block_size — 写入表时,压缩前未压缩数据块的最大大小。
  • min_compress_block_size — 写入下一个标记时,触发压缩所需的未压缩数据块最小大小。
示例:
可以使用 ALTER MODIFY COLUMN 修改或删除列级设置,例如:
  • 从列声明中删除 SETTINGS
  • 修改设置:
  • 重置一个或多个设置,并移除该表 CREATE 查询中列表达式里的设置声明。
最后修改于 2026年7月24日