<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>REPLACE PARTITION on </title>
    <link>/tags/replace-partition/</link>
    <description>Recent content in REPLACE PARTITION on </description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    <lastBuildDate>Sun, 04 Oct 2026 21:20:00 +0800</lastBuildDate><atom:link href="/tags/replace-partition/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>清理 ClickHouse 重复行时行数校验拦不住的情形</title>
      <link>/posts/clickhouse-replace-partition-silent-failures/</link>
      <pubDate>Sun, 04 Oct 2026 21:20:00 +0800</pubDate>
      
      <guid>/posts/clickhouse-replace-partition-silent-failures/</guid>
      <description>这次的重复行来自 producer 重试（入口分析在四个入口那篇）。表还是这个系列一直用的那张按结算日分区的 ReplicatedMergeTree，下文叫它 events，排序键 (settle_ms, id, rev)。一个一千多万行的日分区里，45 分钟的窗口多出一百多行，每个键正好两份。已经逐列核过的那部分重复，两份除 create_time 外完全相同，大约三分之二连 create_time 也一样，是同一秒写进来的。
准备交给 SRE 执行的清理方案，和 REPLACE PARTITION runbook 一样是临时表加 REPLACE PARTITION，整个日分区照样重写一遍，区别在出事的那段窗口：窗口里的行用 GROUP BY 加 argMin 逐列重建，窗口前后两段原样拷贝。方案还没在 Aiven 上的现有集群里执行，评审时在 clickhouse-lab 的三副本集群（25.3.14.14，现有集群是 25.3.14.1）上把每一条 SQL 原样跑了一遍（实验 16），又补了几组实验去造它没覆盖到的情形（实验 17 到 19）。现有集群这一侧只用到了表结构、行数这些事先拿到的元数据。
这篇写的是这次评审跑出来的问题；按这些结论改过的 runbook 是上面链的那一篇，下文提到的 R0、R4、R6 这些步骤编号都出自那里。
窗口重建加换分区的写法 表名换成了 events，列只留了几个，其余照原样：
CREATE TABLE events_tmp LIKE events; -- 窗口里的行按键 GROUP BY，每一列取 create_time 最早的那一份 INSERT INTO events_tmp SELECT id, settle_ms, rev, min(create_time) AS create_time, argMin(amount, create_time) AS amount, argMin(status, create_time) AS status, argMin(ext_id, create_time) AS ext_id -- ……其余每一列都写成 argMin(列, create_time) AS 列 FROM events WHERE settle_ms BETWEEN {窗口起点} AND {窗口终点} GROUP BY id, settle_ms, rev; -- 窗口前后两段原样拷回 INSERT INTO events_tmp SELECT * FROM events WHERE settle_ms &amp;gt;= {当天零点} AND settle_ms &amp;lt; {窗口起点}; INSERT INTO events_tmp SELECT * FROM events WHERE settle_ms &amp;gt; {窗口终点} AND settle_ms &amp;lt; {次日零点}; -- 换分区之前两条校验：临时表窗口内重复数为 0；原表当天计数 - 临时表总数 = 多余行数 ALTER TABLE events REPLACE PARTITION &amp;#39;20260918&amp;#39; FROM events_tmp; DROP TABLE events_tmp; 方案里还有一个被否掉的「方法一」：用轻量删除按 _part、_part_offset 删掉每组里排名靠后的那份。否掉的理由要改，它在 lab 上的代价单独写在轻量删除那篇。</description>
    </item>
    
    <item>
      <title>清理 ClickHouse 重复行的 REPLACE PARTITION runbook</title>
      <link>/posts/clickhouse-replace-partition-dedup-job/</link>
      <pubDate>Wed, 16 Sep 2026 22:40:00 +0800</pubDate>
      
      <guid>/posts/clickhouse-replace-partition-dedup-job/</guid>
      <description>这条 Kafka 到 ClickHouse 的链路上，已经有两批重复行写进了明细表：Kafka Connect 超时重投带来的 234 行（超时重投那篇），和 producer 重试带来的一百多行（四个入口那篇）。去重方案管的是以后的重复，已经落库的这些要单独清，而且重复还会再来，要的是一套能反复跑、跑坏了能回滚的 runbook。
ClickHouse 没有 UPSERT，改数据以 part 为单位（写入路径那篇）。这里的做法是把出问题的那一天从快照重建一遍，验过之后用 REPLACE PARTITION 整个换进去。
这篇最早的版本直接从正在写入的原表建临时表，换分区前数两次行数。后来评审一份同类的清理方案，在 lab 上跑出了几种行数对得上、数据却写坏或丢掉的情形（行数校验拦不住的情形），runbook 按评审的结论改成了下面的 R0 到 R8。整套在 clickhouse-lab 的三副本集群上跑过（25.3.14.14，Aiven 上的现有集群是 25.3.14.1），定稿那一版是实验 19；还没在现有集群上执行。
先低成本地数出有多少重复 清理之前要先知道有多少、集中在哪几个小时。直接对整个分区 GROUP BY 也能数，但两亿行那套部署一个日分区就有 30 GiB，过了 45 天的分区还已经搬到了对象存储上，在现有集群上这么扫一次代价太大。重复只需要一个数字就能看出来：
SELECT count() AS rows, uniqExact(id, settle_ms, rev) AS keys FROM events WHERE _partition_id = &amp;#39;20260730&amp;#39; AND settle_ms &amp;gt;= {hour_start_ms} AND settle_ms &amp;lt; {hour_start_ms} + 3600000 count() - uniqExact(...) 就是这一小时的多余行数。省在 WHERE 的两层裁剪：_partition_id 把日分区写死，settle_ms 是排序键的第一列，按小时切能让主键索引生效。24 条这样的查询覆盖一整天，哪一小时有问题也直接看出来。</description>
    </item>
    
  </channel>
</rss>
