用只读权限评审 ClickHouse 补数方案
目录
同事发来一份补数方案,执行前要过一遍评审。事情不复杂:一张 1300 亿行、19.54 TiB 的 ReplicatedMergeTree 表,有一列连续 18 天被写成了 0,要把这 109 万行改回正确值。
方案主体是对的:用 ALTER TABLE … UPDATE 逐个分区原地改,而不是把正确数据重新 INSERT 一遍。评审意见几乎全落在回滚那一节:它的回滚谓词会连带清掉十万行本来就正确的数据,而且这件事一旦补数跑完就再也查不出来了。
背景#
出问题的是一次发布漏了一行映射代码,placed_at(下单时间)没有被赋值,于是这一列在连续 18 天里都是 0。代码侧已经修好并发布,窗口闭合了,剩下的只有历史数据。
表的结构:
CREATE TABLE analytics.events
(
id String,
source_id UInt32,
placed_at UInt64, -- 出问题的列,毫秒时间戳
settled_at UInt64, -- partition key + primary key 首列
retry_num Int8,
ext_id String,
group_id String,
... -- 共 39 列
)
ENGINE = ReplicatedMergeTree
PARTITION BY toYYYYMMDD(toDateTime(settled_at / 1000))
ORDER BY (settled_at, id, retry_num)
SETTINGS storage_policy = 'tiered'
先交代几个前提:
- 全表 810 个分区、约 1300 亿行、19.54 TiB,3 副本。存储策略是
tiered,其中 764 个分区、17.41 TiB 已经落在 remote(对象存储)那一层,本地盘上最旧的分区停在两个月前; - 要补的 1,091,487 行分布在连续 18 个日分区里,只有一列是错的;
- 方案还没有执行。 下面明细表上的数字全部来自评审当天用强制只读的连接(URL 参数
readonly=2)查system表和跑聚合查询测出来的,没有执行过任何写语句;另有两处是后来在 lab 上补测的(实验 08、20),用到的地方标了出处。所以下文没有补完之后的结果; - 服务端版本 25.3.14.1,
timezone()是 UTC。分区表达式里的toDateTime用的是服务器时区,方案里那张 epoch 边界表能对上,靠的就是这一条。
为什么不重新插一遍#
第一个想法通常是把这 109 万笔的正确值从上游日志捞出来重新 INSERT,手上也有一条现成的补数接口。
但这张表是普通的 ReplicatedMergeTree,主键不是唯一约束。MergeTree 文档写得很直接:
ClickHouse does not require a unique primary key. You can insert multiple rows with the same primary key.
插一行 id 已存在的数据,得到的就是两行。对一张流水表来说,营收类指标会直接翻倍,而且这种重复没有办法事后靠一条谓词摘干净。
换成 ReplacingMergeTree 也解决不了这个问题:合并是后台异步发生的,没有时间保证;查询要写 FINAL 或者自己按键聚合才能看到合并后的结果,等于要改所有下游查询;而且它按排序键替换整行,需要凑出完整的一整行,而这里只有一列是错的。
那条补数接口是给缺失的行用的,这里行都在,只是一列值错了。
mutation 重写的是哪些文件#
ALTER TABLE … UPDATE 在 ClickHouse 里不是 OLTP 意义上的 UPDATE。文档自己就在解释这个前缀:
The
ALTER TABLEprefix makes this syntax different from most other systems supporting SQL. It is intended to signify that unlike similar queries in OLTP databases this is a heavy operation not designed for frequent use.
它被实现成 mutation,是「asynchronous background processes similar to merges」。语句提交后立刻返回,实际工作在后台的 merge/mutation 线程池里做。
mutation 没有查询级的原子性,ALTER 文档的原话是:
There is no atomicity — parts are substituted for mutated parts as soon as they are ready and a SELECT query that started executing during a mutation will see data from parts that have already been mutated along with data from parts that have not been mutated yet.
替换按 part 进行,一个正在跑的 SELECT 可能同时看到改过和没改过的 part。对报表列来说这个中间态可以接受,但要在方案里写明,不能只写一句「原子」。
成本取决于被改的列在哪里。同一个 ALTER 文档里还有一句硬约束:
Updating columns that are used in the calculation of the primary or the partition key is not supported.
所以只有两种情况:列在 primary key 或 partition key 上,直接不支持;列不在这两个键上,ClickHouse 只重写这一列的文件,其余列以 hardlink 的方式挂到新 part 上,不重新排序。ALTER 文档里「rewriting whole data parts」的说法容易读重:它说的是替换以 part 为粒度进行,不是每一列的字节都要重写。列级的行为在官方博客 How we built fast UPDATEs for the ClickHouse column store 里写得很直接:
the updated columns … are fully rewritten, and the unchanged columns … are hard linked. No data is copied for those columns, the new part simply reuses the same underlying files via hard links on disk.
要改的 placed_at 不在任何键上,属于能改的那一类;而 settled_at 既是 partition key 又是 primary key 首列,本来也不能碰。
于是这笔账是这样的:
| 量 | 值 |
|---|---|
| 一个目标分区 | 324,026,400 行 / 49.95 GiB |
其中 placed_at 一列 |
656.42 MiB |
| 18 个分区总体积 | 855.89 GiB |
| 18 个分区该列合计 | 10.94 GiB |
856 GiB 里实际重写 11 GiB,评审支持原地改、不重新 INSERT,主要就是因为这个比例;原地改一列也不改变行数,partition key 没被写,行不会跨分区移动。
一个前提要写明:按列 hardlink 只对 Wide 格式的 part 成立。Compact 格式把所有列放在同一个文件里,改一列也得整个重写;part 超过 min_bytes_for_wide_part 之后才按 Wide 存储。这次的目标 part 动辄几十 GiB,不太可能是 Compact,但这件事只读就能确认,预检查里值得加一条:
SELECT part_type, count() FROM system.parts
WHERE active AND database = ? AND table = ? GROUP BY part_type;
这两种格式的差别后来在同一条 25.3 LTS 的三副本 lab(25.3.14.14)上测过(clickhouse-lab 实验 08):Wide part 上带 IN PARTITION 改一列,新 part 里没改的列文件链接数是 2,和旧 part 共用 inode,改掉的那一列是 1;Compact part 上做同样的事,整个 data.bin 重写;分区键和主键上的列直接报 Code: 420 … Cannot UPDATE key column。
这笔账怎么从 system 表里算出来#
这几条查询在这次评审里都用上了,只读账号就能跑:
-- 引擎、键、存储策略,以及容易漏掉的一项:有没有物化视图挂着
SELECT engine, partition_key, sorting_key, storage_policy, dependencies_table
FROM system.tables WHERE database = ? AND name = ?;
-- 每个分区:多大、多少 part、在哪块盘上、最近还在动吗
SELECT partition, count() AS parts, sum(rows) AS rows,
formatReadableSize(sum(bytes_on_disk)) AS size,
groupUniqArray(disk_name) AS disks,
max(modification_time) AS newest_part
FROM system.parts
WHERE active AND database = ? AND table = ?
GROUP BY partition ORDER BY partition;
-- 那一列自己占多少,也就是 mutation 要重写的量
SELECT partition, formatReadableSize(sum(column_bytes_on_disk)) AS col_size
FROM system.parts_columns
WHERE active AND database = ? AND table = ? AND column = 'placed_at'
GROUP BY partition ORDER BY partition;
-- 还有什么会被连带重建
SELECT name, type, expr FROM system.data_skipping_indices
WHERE database = ? AND table = ?;
SELECT count() FROM system.projection_parts
WHERE database = ? AND table = ? AND active;
-- 线程池多大、现在忙不忙、以前有没有人动过这张表
SELECT metric, value FROM system.metrics
WHERE metric LIKE 'BackgroundMergesAndMutations%';
SELECT count() FROM system.merges;
SELECT mutation_id, command, create_time, is_done, latest_fail_reason
FROM system.mutations WHERE database = ? AND table = ? ORDER BY create_time;
system.parts_columns.column_bytes_on_disk 精确到某个分区里某一列占多少字节,正好是列级 mutation 的写入量,不用估。
system.mutations 的历史也值得看一眼。这张表 2024 年 10 到 11 月有过 7 次 mutation,全部 is_done = 1、没有失败记录。不过它们的语句都是 UPDATE _row_exists = 0 … WHERE … id IN ('…','…',…),也就是 lightweight delete,重写的只是 _row_exists 这一列掩码,不碰业务列的文件。所以它们算不上「列级重写在这张表上跑过」的先例,但确实说明这个集群上的 mutation 机制是通的,而且这种把 id 列表写成字面量的写法有人用过。
忘了 IN PARTITION 会发生什么#
placed_at = 0 这个条件和 partition key 没有任何关系,所以它不能帮 ClickHouse 剪分区。不写 IN PARTITION,这条 mutation 就会对全表 810 个分区排队。
写了 IN PARTITION |
忘写 | |
|---|---|---|
| 涉及 part | 1 个分区,5 到 12 个 part | 810 个分区,3,678 个 part |
| 涉及数据 | 49.95 GiB,其中实际重写 656 MiB | 19.54 TiB |
| 其中在对象存储上 | 0 | 17.41 TiB |
差别最大的是对象存储那一行。tiered 策略会把旧分区搬到 remote 那一层,漏了 IN PARTITION,remote 上那 764 个分区、17.41 TiB 也全部进 mutation 的队列。按前面的列级重写模型,要重写的仍然只是 placed_at 一列加上读谓词的列,不是 17 TiB 全部搬一遍;但这些读写都发生在对象存储上,hardlink 在 remote 盘上怎么落地没有实测。不管按哪种口径算,波及范围都从 18 个本地分区变成了全部 810 个。而且漏写不报错:语句语法完全合法,只是要处理的分区和数据量多了几百倍。
全表还有一个 19700101 分区、2,139 行(历史上某批 settled_at = 0 的脏数据),全表 mutation 会连它一起扫。
评审给方案加了两条约束:18 条语句提前生成好、复核一遍,执行时只粘贴不现场手敲;预检查里加一条 SELECT DISTINCT disk_name FROM system.parts WHERE partition = '…',确认这个分区还在本地盘上。加第二条是因为本地盘上最旧的分区停在两个月前,这批数据早晚会被搬走,方案拖久了成本模型就变了。
正确的值从哪来#
方案最初假设要从上游 CSV 或日志回捞每一笔的时间戳。后来发现不用:这类调用里下单和结算是同一个动作,上游只给一个时间戳,所以 placed_at 本该等于 settled_at,值就在同一行里。
这个推断有一段意外的窗口可以验证:修复代码曾在这 18 天中间被误发布过 13 小时又被回滚,那期间写入的 38,846 行,placed_at 和 settled_at 逐行相等,没有例外。于是补数不需要任何外部数据源:
ALTER TABLE analytics.events
UPDATE placed_at = settled_at
IN PARTITION 20260804
WHERE source_id = 76 AND placed_at = 0;
这个做法在别的补数上也能用:补数之前先找一段「行为正确的时期」,比如一次部署窗口、一个没受影响的分区、另一个区域的集群。如果不变量在那段真实数据上严格成立,就能从行内推导正确值,不用再建一条外部回灌管道。
也有代价,得写进方案里:这样补出来的值有 0.12 到 1.2 秒的系统性偏差(坏行里存的是接收时间,不是上游时间,这是从请求日志抽 2000 条样本比出来的)。这个偏差在那 13 小时的边界上本来就存在于数据里,补数没有创造也没有放大它。要不要接受,交给用这列数据的人判断,不在方案里替他们决定。
谓词命中的到底是哪些行#
补数谓词是 source_id = 76 AND placed_at = 0,要确认它只命中想改的行。
做法是把它写成两个集合的差。设 T 是意图修改的集合(这里是所有单发调用产生的行),P 是谓词实际命中的集合:
- P \ T:谓词命中了意图之外的行。这是危险的一种,意味着会改错数据。
- T \ P:意图之内但谓词没命中的行,通常因为它们本来就是对的。这种无害,但要数清楚,它会影响事后的断言,也会影响回滚。
一条查询同时把两边和几个异常项数出来:
SELECT toYYYYMMDD(toDateTime(intDiv(settled_at, 1000))) AS d,
countIf(placed_at = 0 AND ext_id != group_id) AS p_minus_t,
countIf(placed_at != 0 AND ext_id = group_id) AS t_minus_p,
countIf(placed_at != 0 AND ext_id = group_id
AND placed_at = settled_at) AS t_minus_p_eq,
countIf(placed_at = 0 AND (status != 1 OR retry_num != 0)) AS odd_rows
FROM analytics.events
WHERE settled_at >= ? AND settled_at < ? AND source_id = 76
GROUP BY d ORDER BY d;
结果:p_minus_t 在 18 个分区上都是 0,odd_rows 也都是 0(没有未结算的行、没有重试行)。前者也说明那个字符串守卫是冗余的,可以直接从 mutation 里删掉。
t_minus_p 不为 0:105,724 行,集中在三个分区上。这些行会影响回滚,见下一节。
回滚谓词是这份方案里最贵的问题#
方案原来的回滚是这样写的:
ALTER TABLE analytics.events UPDATE placed_at = 0
IN PARTITION 20260814
WHERE source_id = 76 AND placed_at = settled_at;
思路看着没问题:补数把 placed_at 改成等于 settled_at,撤销就是把两者相等的行清回 0。
但「两者相等」不是补数行独有的特征。那 105,724 行本来就相等,来源是三类:老版本代码写的(11,777 行)、修复误发布那 13 小时写的(38,846 行)、修复正式发布之后写的(55,101 行)。方案作者发现了后两个分区并加了豁免,漏掉的是第一个。按原写法在那个分区上回滚,一万多行正确数据会被一起清零。
而且补数会抹掉两类行的区别:
placed_at = 0"] -->|"被补数改写"| A1["placed_at = settled_at"] B2["本来就相等的 105,724 行
placed_at = settled_at"] -->|"谓词命中不到,不变"| A2["placed_at = settled_at"] IDS["补数前抓下的 id 名单"] -.->|"事后区分两者的唯一依据"| A1
补数之前,两类行可以用 placed_at = 0 干净地分开;补数之后,它们在这两列上完全一样。
所以名单只能在补数之前抓,回滚按 id 名单认行,不按列值判断。名单抓到服务端的一张表里:
CREATE TABLE analytics.patch_ids (p UInt32, id String)
ENGINE = MergeTree ORDER BY (p, id);
INSERT INTO analytics.patch_ids
SELECT toYYYYMMDD(toDateTime(intDiv(settled_at, 1000))) AS p, id
FROM analytics.events
WHERE settled_at >= ? AND settled_at < ?
AND source_id = 76 AND placed_at = 0;
109 万行的名单,估算是几十 MB 的量级;这一步和方案里其他写操作一样没有执行过,实际耗时要到执行时才知道。名单按全部 18 个分区抓,全量也只有几十 MB,没必要只抓混着好数据的那三个。抓完之后冻结这张表,在全部验证签收之前不再写入。
方案原来是用客户端 SELECT id 导出成文件,这样不行:一个分区 5 到 8 万个 UUID,大约 2 到 3 MB,塞不进一条语句。在这个集群上查到 max_query_size 是 262144(256 KiB)。
回滚谓词于是变成四部分:
| 谓词 | 作用 |
|---|---|
id IN (名单) |
身份:只有补数碰过的行在册 |
placed_at = settled_at |
状态守卫:只撤当前仍是补数值的行,跑第二遍命中 0 行,天然幂等 |
retry_num = 0 |
钉住行身份,id 在重试行之间是复用的 |
source_id = 76 和 IN PARTITION |
冗余防护 |
补数谓词写错了,通常改不动数据,或者一跑断言就露出来;回滚谓词写错了,会在已有的故障上再改坏一批数据,而且这一次没有回滚可用。所以这次评审的时间大多花在了回滚上。
复制表上的 mutation 是三次独立执行#
复制表的 mutation 是命令文本经 Keeper 分发,三台各自执行一遍,不是主副本改完再同步过去。有两个后果。
验收要问遍三台。system.mutations 是副本本地的视图,而连到这套集群的连接一般经过负载均衡器。评审时连着发了四次 SELECT hostName(),四次都落在同一个节点上,那台报告 is_done,说明不了另外两台。clusterAllReplicas 的文档写的是「same as cluster, but all replicas are queried」,正好用来一次问遍:
SELECT hostName() AS replica, mutation_id, is_done, parts_to_do, latest_fail_reason
FROM clusterAllReplicas('default', system.mutations)
WHERE database = ? AND table = ? AND mutation_id = ?;
每条 ALTER 提交后先在 system.mutations 里按 command 对出它的 mutation_id 记下来,验收按 id 查:三个副本都返回 is_done = 1 才算这条 mutation 完成。只用 NOT is_done 数零行有个盲区:KILL MUTATION 的文档语义是「cancel and remove」,被杀掉的 mutation 也会从列表里消失,零行分不清「跑完了」和「被杀了」。这个表函数在只读账号上可用,评审时用它顺手核过一次三副本的一致性。
谓词里引用别的表,要额外开开关。上一节的回滚要用名单,最自然的写法是 id IN (SELECT id FROM patch_ids WHERE p = ?)。ClickHouse 会拒绝,除非打开 allow_nondeterministic_mutations(这个集群上查到默认值是 0,可以按查询覆盖)。
拒绝的原因是:三台各自执行,如果谓词依赖另一张表的内容,而那张表在三台执行的时刻内容不同(有人插了数据、或者某台还没同步完),三台就会改出不同的结果集,副本之间的数据悄悄不一致。这种损坏不报错,可能很久以后才被发现。这个设置的文档说明举的例子是字典在各节点之间可能不同步,所以从字典取值的 mutation 在复制表上默认不让跑(allow_nondeterministic_mutations);25.3 的源码注释说得更直接,复制表的 mutation 不能用非确定的函数,因为会让副本之间不一致,带子查询的直接拒绝(MutationsInterpreter.cpp)。
DBA 的意见是不开这个开关,评审也是这个结论。更省事的做法是让名单变成语句本身的一部分:
-- 用名单表生成回滚语句,id 以字面量写进去
SELECT concat(
'ALTER TABLE analytics.events UPDATE placed_at = 0 ',
'IN PARTITION ', toString(p), ' ',
'WHERE source_id = 76 AND retry_num = 0 AND placed_at = settled_at ',
'AND id IN (\'', arrayStringConcat(groupArray(id), '\',\''), '\');'
) AS stmt
FROM analytics.patch_ids
WHERE p = ?
GROUP BY p, cityHash64(id) % 20
FORMAT TSVRaw;
语句文本经 Keeper 复制,三台执行的是同一份名单。切 20 批是为了躲开上面测到的 max_query_size 256 KiB:最多的一天 8 万个 UUID 切 20 批约 4000 个一条、160 KB 左右,不过 cityHash64(id) % 20 只是期望上均分,不保证每批都压在线下,语句生成完按字节数复核一遍再执行。
字面量拼接还有一个没写进 SQL 的前提:id 在表结构里是 String,实际内容得是 UUID 这类只含安全字符的文本,拼出来的语句才不用考虑转义,一个带单引号或反斜杠的 id 就能破坏整条语句。名单表冻结后值得加一条断言,返回 0 再继续生成:
SELECT count() FROM analytics.patch_ids
WHERE NOT match(id, '^[0-9a-fA-F-]{36}$');
这也正好是 2024 年那 7 次 lightweight delete 的写法。
断言写在哪一层#
有两件事让方案里的断言要改。
第一,mutation 只覆盖排队时已经存在的 part。ALTER 文档里那句是:
Mutations are also partially ordered with INSERT INTO queries: data that was inserted into the table before the mutation was submitted will be mutated and data that was inserted after that will not be mutated.
补数窗口在过去,但迟到数据确实存在,评审当天就看到 08-26、08-27 各新增了一个只有 1 行的 part 落在窗口内的老分区上(值是正确的)。所以每个分区补完都要复查坏行是否为 0,有就重跑那个分区。这也让流程天然幂等:谓词只碰 placed_at = 0 的行,重跑不会误伤。
第二,行数会自己变动。system.part_log 显示 08-26 一天就有 4 次 merge 重排了约 4.09 亿行,都落在这 18 个分区里,属于正常的后台维护。part_log 只是连上的那个节点的视角,绝对量有偏差,但足以说明这些老分区还在被动。
所以「总行数不变」这种断言在活表上会失效。建议换成:
- 目标指标归零:
countIf(placed_at = 0) = 0 - 差值断言:
eq_settle(后) − eq_settle(前) == to_patch,其中eq_settle是方案里的记号,指countIf(placed_at = settled_at);「前」的值要在预检查里先记下来 - 总行数只作参考;差几行先去
system.parts看有没有新 part,不要直接当事故回滚
原方案的验证是「eq_settle 增量等于补数行数」加「总行数不变」,前者在混着好数据的分区上会误报,后者在活表上会误报。
克隆可以用来验证,但不要 REPLACE PARTITION#
方案里有一节想得挺好:用 ATTACH PARTITION … FROM 把一个分区搬到同结构的克隆表上,在克隆上跑一遍一模一样的 mutation,然后做行级 diff。分区操作文档里这条的关键性质是源表不动,「Data will be deleted neither from table1 nor from table2」,另外两表必须结构、partition key、order by、primary key、storage policy 全都一致,目标表还要包含源表全部的 indices 和 projections。这张表有 6 个 data skipping index,克隆表建表时漏了它们,这一步就过不去。
方案里说这一步「秒级、几乎不占额外磁盘」,理由是 hardlink。文档这一页的措辞是 copy,hardlink 是实现细节;评审时没有执行权限、也没有实测,这句当时改成了「需要 DBA 在克隆上确认」。后来实验 20 测过,本地盘上 ATTACH PARTITION … FROM 确实只挂硬链接、写入 0 行,另外两个副本也是在本地挂(clickhouse-lab 实验 20)。另外在托管服务上 MergeTree 可能被自动创建成 ReplicatedMergeTree,ATTACH 之前先看一眼引擎,不匹配会快速失败。
需要拦下来的是另一半:方案提到可以在克隆上验证完之后,用 REPLACE PARTITION 把克隆换回原表。同一页文档对它的定义是「copies the data partition from table1 to table2 and replaces the existing partition in table2」:目标分区被克隆里的数据整体换掉。克隆是 ATTACH 那一刻的快照,从 ATTACH 到 REPLACE 之间插入原表的行不在克隆里,替换之后也就没有了。而上一节说了,迟到行是真实存在的。
所以克隆只用来验证,实际修改用原地 mutation,行数不变,行也不会跨分区移动。
快照也一样。方案想留一份补数前的分区快照,评审时加了一句「验证完就删」:后台合并会让两边的 part 逐渐分叉,快照持有的旧 part 越滚越多,「不占空间」只是刚做完那一刻的状态。
物化视图看不到 mutation#
ClickHouse 的增量物化视图是 INSERT 触发器,文档的原话是「just a trigger that runs a query on blocks of data as they’re inserted into a table」。mutation 不走 INSERT 通道,所以挂在表上的增量物化视图不会被触发,已经把这段数据聚合出去的下游表也不会自动更新(refreshable materialized view 是按计划整体重算的,不在这个讨论里)。补数之前值得查一次:
SELECT dependencies_database, dependencies_table
FROM system.tables WHERE database = ? AND name = ?;
这次的结果算运气好:整台服务器一个物化视图都没有,system.projection_parts 里 0 个 active projection,6 个 data skipping index 也没有一个包含 placed_at。所以补完只需要重跑报表,不用回填任何物化产物,也不会连带重建索引。
有物化视图的表,补完明细还要回填聚合表,否则明细对了、聚合还是错的,这种不一致往往要过很久才被发现。
只读能评审到什么程度#
这次评审全程只有 SELECT 权限,回头看,能验的东西比预想的多:分区体积、列体积、两种泄漏、105,724 行的来源分解、副本状态、后台负载、历史 mutation、服务器时区、max_query_size、有没有物化视图和 projection,都在 system 表里。
没法只读验证的只有两件:执行账号到底有没有写权限,以及 ATTACH PARTITION … FROM 的引擎相容性。这两件都会快速失败,风险可控。
小结#
- 先弄清引擎的语义再选路线:MergeTree 的 primary key 不做唯一约束,补插一行就是多一行,这条路在流水表上基本不能走;
ALTER … UPDATE是 mutation,只有列不在 primary key 和 partition key 上时才是「重写一列 + hardlink 其余」(Wide part 下);在这两个键上的列,文档写的是不支持;- 成本从
system.parts_columns里算,不要估。这次是 856 GiB 的分区里重写 11 GiB; - 谓词不会剪分区,漏了
IN PARTITION就是全表 mutation,在 tiered 存储上还要读写对象存储里的分区; - 这次改动最大的一条:补数之前抓 id 名单,回滚按名单认行,不按
placed_at = settled_at这个状态。补数会覆盖掉区分「补出来的」和「本来就对的」那一列信息,事后再想分开就来不及了; - 复制表的 mutation 三台各自执行,验收用
clusterAllReplicas;要按名单回滚就把 id 写成字面量,不必去开allow_nondeterministic_mutations; - 活表上的断言选「目标指标归零 + 差值断言」,总行数只作参考。
参考资料#
- ALTER TABLE … UPDATE:heavy operation、
IN PARTITION、primary/partition key 列不支持更新 - ALTER:Mutations:异步后台执行、没有查询级原子性、与 INSERT 的偏序关系
- MergeTree:primary key 不要求唯一、稀疏索引按 granule 工作
- Manipulating Partitions and Parts:
ATTACH PARTITION FROM/REPLACE PARTITION的语义与结构约束 - cluster / clusterAllReplicas:查遍所有副本
- How we built fast UPDATEs for the ClickHouse column store – Part 2: SQL-style UPDATEs:mutation 只重写被改的列、未改列 hardlink 的一手出处
- Incremental materialized view:物化视图是 INSERT 触发器,不响应 mutation
- allow_nondeterministic_mutations、
MutationsInterpreter.cpp@ v25.3.13.19-lts:复制表上带子查询的 mutation 为什么默认被拒 - clickhouse-lab:实验 08(列级 mutation 和 Wide、Compact 两种 part)、20(
ATTACH PARTITION … FROM走硬链接)
相关文章#
- ClickHouse 交易明细系统的整体架构:系列总览和阅读顺序
- ClickHouse 的写入路径:为什么改数据以 part 为单位
- ClickHouse 轻量删除清重复行的代价:同一类 mutation 不带
IN PARTITION的代价 - ClickHouse 报表的对账和重算:物化视图看不到改数据之后,报表怎么修