Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

对重试插入进行去重

插入操作有时会因超时等错误而失败。插入失败时,数据可能已经成功写入,也可能尚未写入。本指南介绍重试插入时的去重机制如何工作,确保相同数据不会被重复插入。

重试插入时,ClickHouse 会尝试判断数据是否已成功插入。如果待插入的数据被标记为重复,ClickHouse 就不会再将其插入目标端表。不过,用户仍会收到成功状态,就像数据已正常插入一样。

去重涵盖同步插入、异步插入和 INSERT ... SELECT 查询。一项设置 deduplicate_insert 可控制同步和异步插入。INSERT ... SELECT 需要额外注意,并且有专属设置。请参阅控制插入去重的设置

局限

插入状态不确定

用户必须持续重试插入操作,直到成功为止。如果所有重试都失败,就无法判断数据是否已插入。当涉及 materialized views 时,也无法确定数据可能写入了哪些表。materialized views 可能与源表不同步。

去重窗口限制

如果在重试过程中发生了超过 *_deduplication_window 次其他插入操作,去重可能无法按预期生效。在这种情况下,相同的数据可能会被多次插入。

控制插入去重的设置

仅当同时满足以下两个条件时,ClickHouse 才会对插入操作进行去重:

  1. 目标表保留去重日志。这是表级别的设置。
  2. 已为查询启用去重。这是查询级别的设置。

表级设置

只有 *MergeTree 引擎支持插入时去重。

对于 *ReplicatedMergeTree 引擎,去重日志默认处于启用状态,由 replicated_deduplication_windowreplicated_deduplication_window_seconds 设置控制。对于非复制表 *MergeTree 引擎,该日志由 non_replicated_deduplication_window 设置控制,其默认值为 0。因此,普通的 MergeTree 表在将该窗口设置为正值之前不会去重任何数据。

上述设置决定了表的去重日志参数。去重日志会存储有限数量的 block_id,而这些 block_id 会决定去重的实现方式 (见下文) 。

查询级别设置

设置 适用范围 默认值 用途
deduplicate_insert 所有 INSERT,无论同步还是异步 enable 插入去重的主开关
deduplicate_insert_select INSERT ... SELECT enable_when_possible 决定 SELECT 结果无法复现时的处理方式
insert_deduplication_token 所有 INSERT '' 使用用户提供的字符串而非数据来标识插入操作
deduplicate_blocks_in_dependent_materialized_views materialized view 下的表 1 将去重扩展到依赖 materialized view 的目标表

deduplicate_insert 支持三个值:

  • enable — 对 INSERT 查询启用去重。
  • disable — 对 INSERT 查询禁用去重。
  • backward_compatible_choice — 由旧版设置 insert_deduplicate (同步插入) 和 async_insert_deduplicate (异步插入) 决定。

请注意,使用 deduplicate_insert = disable 运行的查询不会为其块写入任何 block_id。即使之后使用 deduplicate_insert = enable 重试插入,这些数据也无法去重。目标表不保留去重日志时也是如此:不会记录任何内容,因此重试时也无法进行匹配。

优先次序

  1. 对于 INSERT ... SELECT 查询,由 deduplicate_insert_select 决定。请参阅 INSERT … SELECT 去重
  2. 对于所有其他 INSERT,由 deduplicate_insert 决定。
  3. 仅当 deduplicate_insertbackward_compatible_choice 时,才会读取 insert_deduplicateasync_insert_deduplicate

旧版和已废弃设置

设置 状态 请改用
insert_deduplicate 旧版。仅当 deduplicate_insert = backward_compatible_choice 时才会读取 deduplicate_insert
async_insert_deduplicate 旧版。仅当 deduplicate_insert = backward_compatible_choice 时才会读取 deduplicate_insert
insert_select_deduplicate 已废弃。不起作用 deduplicate_insert_select
update_insert_deduplication_token_in_dependent_materialized_views 已废弃。不起作用

26.2 版本还将 async_insertdeduplicate_blocks_in_dependent_materialized_views 的默认值改为启用。compatibility 设置控制这三项设置。如果将 compatibility 设置为早于 26.2 的版本,这些设置将保留原有默认值:deduplicate_insert 会变为 backward_compatible_choice,并将决定权交给 insert_deduplicateasync_insert_deduplicate。显式指定的设置始终会生效,不受 compatibility 影响。

insert 去重的工作原理

当数据插入 ClickHouse 时,系统会根据行数和字节数将数据拆分为块。

对于使用 *MergeTree 引擎的表,每个块都会分配一个唯一的 block_id,它是该块中数据的哈希值。这个 block_id 会作为插入操作的唯一键使用。如果在去重日志中发现相同的 block_id,则该块会被视为重复块,不会插入表中。

这种方法在 插入 包含不同数据的场景下效果很好。但是,如果你有意多次插入相同的数据,就需要使用 insert_deduplication_token 设置来控制去重过程。该设置允许你为每次 insert 指定一个唯一的标记,ClickHouse 会据此判断数据是否重复。insert_deduplication_token 具有更高的优先级:提供该标记时,ClickHouse 不会使用数据的哈希值。

对于 INSERT ... VALUES 查询,插入数据被拆分为块的方式是确定性的,并且由相关设置决定。因此,重试插入时应使用与初始操作相同的设置值。

INSERT ... SELECT 的去重

对于 INSERT ... SELECT 查询,SELECT 部分必须在每次尝试时都以相同顺序返回相同的数据。否则,块会不同,block_id 也会不同,重试就不会被识别为重复操作。

ClickHouse 无法验证源数据是否未发生变化,但可以检查查询本身能否产生可复现的结果。同时满足以下两个条件时,SELECT 会被视为稳定

  • 查询包含 ORDER BY ALL 子句。仅识别字面量 ORDER BY ALL;普通的 ORDER BY <expressions> 不会被识别,并且由两个或更多 SELECT 组成的 UNION 永远不会稳定。
  • 读取管道最终只有单个 stream。

非空的 insert_deduplication_token 可等效替代稳定性,因为此时标识插入操作的是标记,而非数据。

设置 deduplicate_insert_select 用于决定相应行为:

行为
enable_when_possible (默认) SELECT 稳定或已设置标记时进行去重。否则跳过去重,并向服务器日志写入一条消息。
force_enable 始终进行去重。如果 SELECT 不稳定且未设置标记,则抛出 DEDUPLICATION_IS_NOT_POSSIBLE 异常。
enable_even_for_bad_queries 无论稳定性如何都进行去重。为保持向后兼容性而保留。对于不稳定的 SELECT,重试通常不会被识别为重复操作,因此建议使用其他值。
disable 永不对 INSERT ... SELECT 进行去重。

enable_when_possibleenable_even_for_bad_queries 也会遵循 deduplicate_insert:如果其值为 disable,则不会对查询进行去重。force_enable 会覆盖 deduplicate_insert

请注意,所选表可能会在两次重试之间被更新。这两种方式的行为恰好相反:

  • 不使用 insert_deduplication_token 时,block_id 根据数据计算。变更后的结果会生成不同的 block_id,不会发生去重,重试会在首次尝试已写入的数据之上插入新数据。
  • 使用 insert_deduplication_token 时,仅由标记标识插入操作。重试会被识别为重复操作并被丢弃,即使它本会插入不同的数据。

请选择符合您对重试语义预期的方式。此外,插入大量数据时,块的数量可能会超出去重日志窗口的范围,ClickHouse 将无法识别并去重这些块。

异步插入的去重

异步插入 (async_insert,自 26.2 版本起默认启用) 在重试时的去重方式与同步插入相同。deduplicate_insert 同时控制两者,无需单独设置开关。

这两种插入类型还共用同一个去重日志,并以相同方式计算 block_id。因此,您可以将客户端从同步插入切换为异步插入,或反向切换,而不会影响去重;以一种模式发送的重试仍会被识别为另一种模式下发送尝试的重复项。对于依赖去重的表,将工作负载从同步插入迁移到异步插入同样是安全的。

异步插入的去重粒度

服务器会将多个异步插入汇集为一个批次,并将该批次写入一个或多个 parts;每个不同的分区键值至少会写入一个 part。去重以用户查询为单位,而非以批次为单位:

  • 队列中的每个查询都会为该批次提供一个去重标记。
  • 如果查询提供了 insert_deduplication_token,该标记即为其值;否则,该标记为此查询所提供行的哈希值。
  • 批处理不会影响标记,insert_deduplication_token 也不会影响查询分组为批次的方式。

这会带来两个结果:

  • 如果批次中的某个查询重复,ClickHouse 只会移除该查询的行,批次中的其余数据仍会正常插入。仅当一个 part 中的所有行都被移除时,才会完全跳过该 part。
  • 如果同一批次中的两个查询携带相同的标记,第二个查询会在写入 part 前被丢弃。这一规则按分区生效:如果两个查询将行写入不同分区,则两者都会保留。

system.events 中的 DuplicatedAsyncInsertsSelfDuplicatedAsyncInserts 事件分别统计这两种情况。

异步插入与 materialized view

异步插入的去重机制与依赖的 materialized view 配合使用。规则很简单:一个块输入,一个块输出。如果视图的内部查询将一个输入块转换为一个输出块,去重即可正常工作。如果视图输出第二个块,ClickHouse 会抛出 NOT_IMPLEMENTED 异常。

当视图的输出无法容纳在一个块中时,便会输出第二个块。max_block_size 用于设置一个块可容纳的行数。列转换、筛选和聚合不会增加行数,因此始终只会产生一个块。JOIN 可能会增加行数。结果不超过 max_block_size 时可以正常工作,超过该值则会失败。

若要通过输出多个块的视图进行插入,请将 deduplicate_blocks_in_dependent_materialized_views = 0,或使用同步插入。

使用 materialized view 的插入去重

当一个表具有一个或多个 materialized view 时,插入的数据也会在经过定义的转换后写入这些视图的目标端。转换后的数据在重试时也会进行去重。ClickHouse 对 materialized view 执行去重的方式,与其对插入目标表的数据进行去重的方式相同。

你可以使用源表的以下设置来控制此过程:

materialized view 下游表中的去重还受用户 profile 设置 deduplicate_blocks_in_dependent_materialized_views 控制,该设置自 26.2 版本起默认启用。两个开关都必须允许去重:deduplicate_insert 会对插入源表的数据进行去重,而 deduplicate_blocks_in_dependent_materialized_views 还会对依赖表中的数据进行去重。如果希望实现完整去重,请同时启用两者。

当向 materialized view 下游的表中插入块时,ClickHouse 会通过对一个字符串进行哈希来计算 block_id,该字符串由源表中的 block_id 和其他附加标识符组合而成。这可确保 materialized view 中的去重准确无误,使系统能够根据数据的原始插入情况对其进行区分,而不受数据到达 materialized view 下游目标表之前所执行转换的影响。

示例

materialized view 转换后生成的相同数据块

在 materialized view 中转换过程中生成的相同数据块不会被去重,因为它们基于不同的插入数据。

下面是一个示例:

CREATE TABLE dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000;

CREATE MATERIALIZED VIEW mv_dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000
AS SELECT
    0 AS key,
    value AS value
FROM dst;
SET max_block_size=1;
SET min_insert_block_size_rows=0;
SET min_insert_block_size_bytes=0;

上述设置使我们能够从一个由一系列每块仅包含一行的块组成的表中进行查询。这些较小的块不会被合并,在插入表之前会一直保持不变。

尽管 materialized view 默认启用去重,我们仍显式启用它:

SET deduplicate_blocks_in_dependent_materialized_views=1;
INSERT INTO dst SELECT
    number + 1 AS key,
    IF(key = 0, 'A', 'B') AS value
FROM numbers(2);

SELECT
    *,
    _part
FROM dst
ORDER BY all;
┌─key─┬─value─┬─_part─────┐
│   1 │ B     │ all_0_0_0 │
│   2 │ B     │ all_1_1_0 │
└─────┴───────┴───────────┘

这里我们可以看到,两个 parts 已插入 dst 表。select 产生了 2 个块——插入时生成 2 个 parts。这些 parts 包含的数据不同。

SELECT
    *,
    _part
FROM mv_dst
ORDER BY all;
┌─key─┬─value─┬─_part─────┐
│   0 │ B     │ all_0_0_0 │
│   0 │ B     │ all_1_1_0 │
└─────┴───────┴───────────┘

这里我们可以看到,已有 2 个 parts 被插入到 mv_dst 表中。这两个 parts 包含相同的数据,但并未被去重。

INSERT INTO dst SELECT
    number + 1 AS key,
    IF(key = 0, 'A', 'B') AS value
FROM numbers(2);

SELECT
    *,
    _part
FROM dst
ORDER BY all;
┌─key─┬─value─┬─_part─────┐
│   1 │ B     │ all_0_0_0 │
│   2 │ B     │ all_1_1_0 │
└─────┴───────┴───────────┘
SELECT
    *,
    _part
FROM mv_dst
ORDER by all;
┌─key─┬─value─┬─_part─────┐
│   0 │ B     │ all_0_0_0 │
│   0 │ B     │ all_1_1_0 │
└─────┴───────┴───────────┘

这里可以看到,当我们重试这些插入操作时,所有数据都会被去重。去重对 dstmv_dst 表都生效。

插入时的相同数据块

CREATE TABLE dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000;

SET max_block_size=1;
SET min_insert_block_size_rows=0;
SET min_insert_block_size_bytes=0;

插入:

INSERT INTO dst SELECT
    0 AS key,
    'A' AS value
FROM numbers(2);

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER BY all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   0 │ A     │ all_0_0_0 │
└────────────┴─────┴───────┴───────────┘

使用上述设置后,select 会产生两个块——因此,插入到表 dst 中的也应该是两个块。然而,我们看到只有一个块被插入了表 dst。这是因为第二个块被去重了。它的数据相同,而且用于去重的键 block_id 也是一样的;block_id 是根据插入的数据计算出的哈希值。这种行为并不符合预期。这类情况虽然很少见,但理论上是可能发生的。为了正确处理这类情况,用户必须提供 insert_deduplication_token。下面的示例将说明如何修正这个问题:

使用 insert_deduplication_token 插入时的相同数据块

CREATE TABLE dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000;

SET max_block_size=1;
SET min_insert_block_size_rows=0;
SET min_insert_block_size_bytes=0;

插入:

INSERT INTO dst SELECT
    0 AS key,
    'A' AS value
FROM numbers(2)
SETTINGS insert_deduplication_token='some_user_token';

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER BY all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   0 │ A     │ all_2_2_0 │
│ from dst   │   0 │ A     │ all_3_3_0 │
└────────────┴─────┴───────┴───────────┘

两个相同的块都已按预期插入。

SELECT 'second attempt';

INSERT INTO dst SELECT
    0 AS key,
    'A' AS value
FROM numbers(2)
SETTINGS insert_deduplication_token='some_user_token';

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER BY all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   0 │ A     │ all_2_2_0 │
│ from dst   │   0 │ A     │ all_3_3_0 │
└────────────┴─────┴───────┴───────────┘

重试后的插入操作会按预期被去重。

SELECT 'third attempt';

INSERT INTO dst SELECT
    1 AS key,
    'b' AS value
FROM numbers(2)
SETTINGS insert_deduplication_token='some_user_token';

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER BY all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   0 │ A     │ all_2_2_0 │
│ from dst   │   0 │ A     │ all_3_3_0 │
└────────────┴─────┴───────┴───────────┘

该插入操作也会被去重,即使其中插入的数据不同。请注意,insert_deduplication_token 的优先级更高:提供 insert_deduplication_token 时,ClickHouse 不会使用数据的哈希值。

不同的插入操作经过转换后,会在 materialized view 的底层表中生成相同的数据

CREATE TABLE dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000;

CREATE MATERIALIZED VIEW mv_dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000
AS SELECT
    0 AS key,
    value AS value
FROM dst;

SET deduplicate_blocks_in_dependent_materialized_views=1;

select 'first attempt';

INSERT INTO dst VALUES (1, 'A');

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER by all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   1 │ A     │ all_0_0_0 │
└────────────┴─────┴───────┴───────────┘
SELECT
    'from mv_dst',
    *,
    _part
FROM mv_dst
ORDER by all;
┌─'from mv_dst'─┬─key─┬─value─┬─_part─────┐
│ from mv_dst   │   0 │ A     │ all_0_0_0 │
└───────────────┴─────┴───────┴───────────┘
select 'second attempt';

INSERT INTO dst VALUES (2, 'A');

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER by all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   1 │ A     │ all_0_0_0 │
│ from dst   │   2 │ A     │ all_1_1_0 │
└────────────┴─────┴───────┴───────────┘
SELECT
    'from mv_dst',
    *,
    _part
FROM mv_dst
ORDER by all;
┌─'from mv_dst'─┬─key─┬─value─┬─_part─────┐
│ from mv_dst   │   0 │ A     │ all_0_0_0 │
│ from mv_dst   │   0 │ A     │ all_1_1_0 │
└───────────────┴─────┴───────┴───────────┘

我们每次插入的数据都不同。但插入到 mv_dst 表中的数据却相同。由于源数据不同,因此不会发生去重。

不同 materialized view 向同一底层表插入等效数据

CREATE TABLE dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000;

CREATE TABLE mv_dst
(
    `key` Int64,
    `value` String
)
ENGINE = MergeTree
ORDER BY tuple()
SETTINGS non_replicated_deduplication_window=1000;

CREATE MATERIALIZED VIEW mv_first
TO mv_dst
AS SELECT
    0 AS key,
    value AS value
FROM dst;

CREATE MATERIALIZED VIEW mv_second
TO mv_dst
AS SELECT
    0 AS key,
    value AS value
FROM dst;

SET deduplicate_blocks_in_dependent_materialized_views=1;

select 'first attempt';

INSERT INTO dst VALUES (1, 'A');

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER by all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   1 │ A     │ all_0_0_0 │
└────────────┴─────┴───────┴───────────┘
SELECT
    'from mv_dst',
    *,
    _part
FROM mv_dst
ORDER by all;
┌─'from mv_dst'─┬─key─┬─value─┬─_part─────┐
│ from mv_dst   │   0 │ A     │ all_0_0_0 │
│ from mv_dst   │   0 │ A     │ all_1_1_0 │
└───────────────┴─────┴───────┴───────────┘

两个相同的块已插入表 mv_dst (符合预期) 。

SELECT 'second attempt';

INSERT INTO dst VALUES (1, 'A');

SELECT
    'from dst',
    *,
    _part
FROM dst
ORDER BY all;
┌─'from dst'─┬─key─┬─value─┬─_part─────┐
│ from dst   │   1 │ A     │ all_0_0_0 │
└────────────┴─────┴───────┴───────────┘
SELECT
    'from mv_dst',
    *,
    _part
FROM mv_dst
ORDER by all;
┌─'from mv_dst'─┬─key─┬─value─┬─_part─────┐
│ from mv_dst   │   0 │ A     │ all_0_0_0 │
│ from mv_dst   │   0 │ A     │ all_1_1_0 │
└───────────────┴─────┴───────┴───────────┘

该次重试操作在 dstmv_dst 两个表上都会被去重。

Navigation