ReplacingMergeTree tablosunda aynı anahtarlı iki satır görmek bir hata değildir. Motor tekrarları yalnız arka plandaki birleşmede (merge) eler ve ClickHouse belgesi bunu açıkça söyler: "Merging occurs in the background at an unknown time, so you can't plan for it." (ReplacingMergeTree belgesi). Tekil satır gerekiyorsa sorguyu buna göre yazarsınız: FINAL ya da argMax. OPTIMIZE ... FINAL rutin çözüm olmamalı. Aşağıda çift satırın nasıl oluştuğu, üç okuma yolunun farkı, silme işareti ve Debezium/CDC eşlemesi belge örnekleri üzerinden anlatılıyor.
Çift satırı birlikte görelim
Tekrarın tanımı ORDER BY bölümündedir: belge, satır tekilliğinin PRIMARY KEY ile değil ORDER BY ile belirlendiğini yazar. Aşağıdaki tablo key alanını tekil sayar ve satırın hangi sürümünün kalacağını eventTime ile seçer:
CREATE TABLE musteri_durum
(
`key` Int64,
`someCol` String,
`eventTime` DateTime
)
ENGINE = ReplacingMergeTree(eventTime)
ORDER BY key;
INSERT INTO musteri_durum VALUES (1, 'ilk', '2026-10-01 10:00:00');
INSERT INTO musteri_durum VALUES (1, 'guncel', '2026-10-02 10:00:00');
SELECT count() FROM musteri_durum; -- belirsiz: 2 de olabilir, 1 de
SELECT count() FROM musteri_durum FINAL; -- 1İki ayrı INSERT iki ayrı veri parçası (part) üretir. Parçalar birleşmeden önce count() 2 döner; birleşme olduktan sonra 1 döner ve bunun ne zaman olacağını siz seçemezsiniz. İki satırın eventTime değeri aynıysa belgeye göre en son eklenen satır kalır.
Başka bir tuzak partition'dır. Altinity bilgi bankası, ClickHouse'un parçaları yalnız tek partition kapsamında birleştirdiğini, bu yüzden farklı partition'a düşen aynı anahtarlı iki satırın hiçbir zaman tek satıra inmeyeceğini anlatır; aynı kaynağa göre FINAL ise do_not_merge_across_partitions_select_final ayarı açılmadıkça tüm partition'ları birleştirir (Altinity KB). PARTITION BY alanını güncellenebilen bir sütundan (ör. durum ya da tarih) seçerseniz tekrarlar kalıcı olabilir.
Tekilleştirme iki yerde olur, ikisi aynı şey değil
| Ekleme tekilleştirmesi | Birleşme tekilleştirmesi | |
|---|---|---|
| Ne zaman çalışır | INSERT anında; replike olmayan tabloda yalnız pencere ayarı açıksa | Arka plandaki birleşmede |
| Neyi yakalar | Aynı partinin iki kez eklenmesi | Aynı ORDER BY anahtarlı farklı sürümler |
| Anahtar | Parti içeriği ya da insert_deduplication_token | ORDER BY anahtarı ve ver |
| Zamanı | Anında | Belirsiz |
Birincisi tekrar denemeye karşıdır: istemci zaman aşımına uğrayıp aynı partiyi yeniden gönderdiğinde aynı satırlar iki kez yazılmasın diye vardır. ClickHouse belgesi, kendi token'ınızı her parti için verebileceğinizi söyler (deduplication rehberi). Bu korumanın açık olup olmadığı tablo türüne bağlıdır. ClickHouse'un tekrar denemelerde ekleme tekilleştirmesi rehberi, replike olmayan *MergeTree tablolarında kaydın non_replicated_deduplication_window ayarıyla yönetildiğini ve bu ayarın varsayılanının 0 olduğunu yazar. Yani pencere pozitif bir değere ayarlanmadıkça düz bir MergeTree tablosunda hiçbir ekleme tekilleştirilmez. Replicated*MergeTree tablolarında tekilleştirme günlüğü varsayılan olarak açıktır. Bu yazıdaki musteri_durum replike olmayan bir tablodur; yukarıdaki DDL'de pencere ayarı yoktur, bu yüzden aynı partiyi iki kez gönderirseniz iki kopya da yazılır. Ayar sayfasındaki MergeTree örneğinde pencere açıkça verilmiştir (ayar sayfası):
CREATE TABLE test_table (A Int64)
ENGINE = MergeTree
ORDER BY A
SETTINGS non_replicated_deduplication_window = 100;
INSERT INTO test_table
SETTINGS insert_deduplication_token = 'test'
VALUES (1);İkincisi ReplacingMergeTree'nin işidir ve aynı rehber onu şöyle tarif eder: "The actual removal of duplicate rows occurs during the merging of parts". Kafka ya da CDC'den aynı olay farklı partilerde, farklı içerikle ya da güncellenmiş hâliyle gelebilir; token bunu çözmez. Bu durumda ReplacingMergeTree ve ver kurar, okurken tekilleştirirsiniz.
Okurken tekilleştirmenin üç yolu
FINAL: belgeye göre "When FINAL is specified, ClickHouse fully merges the data before returning the result." Aynı sayfa, normalde birleşme anında yapılacak işin sorgu anında bellekte yapıldığını ve bu yüzden daha fazla sistem kaynağı gerektiğini yazar (SELECT ... FROM). FINAL'lı sorgular paralel çalışır; kullanılan iş parçacığı sayısını max_final_threads sınırlar.
argMax: belirli sütunların en yeni değerini GROUP BY ile alır. Altinity örneğinin kendi tablonuza uyarlanmış hâli:
SELECT
key,
argMax(someCol, eventTime) AS son_durum,
max(eventTime) AS son_zaman
FROM musteri_durum
WHERE key = 1
GROUP BY key;OPTIMIZE TABLE musteri_durum FINAL: birleşmeyi zorlar. Belge uyarır: "do not count on using it, because the OPTIMIZE query will read and write a large amount of data."
| Durum | Seçim |
|---|---|
| Birkaç anahtar için son durum (ör. tek müşterinin kartı) | argMax + WHERE key = … |
| Tablonun tamamı üzerinde toplu rapor, silme işareti var | FINAL, ardından silinmiş satırı süz |
| Her sorguda tekil satır gerekiyor, tablo büyük | FINAL maliyetini ölç; sık okunan sonuç için ayrı bir tablo ya da görünüm düşün |
| Bir kerelik temizlik, bakım penceresi var | OPTIMIZE … FINAL, rutin değil |
Bu tablodaki süre ya da bellek sayısı yoktur; kendi tablonuzda FINAL'lı ve FINAL'sız aynı sorguyu çalıştırıp karşılaştırın.
Silinen satırlar: is_deleted ve ver
Silmeyi is_deleted sütunuyla işaretlersiniz. Belgeye göre sütun UInt8 olur; 1 silinmiş satır, 0 normal durum satırıdır. Kural: "is_deleted can only be enabled when ver is used." Yani sürüm sütunu olmadan ReplacingMergeTree(is_deleted) yazılamaz. Belgedeki örnek biçimi:
CREATE TABLE musteri_durum2
(
`key` Int64,
`someCol` String,
`eventTime` DateTime,
`is_deleted` UInt8
)
ENGINE = ReplacingMergeTree(eventTime, is_deleted)
ORDER BY key;Belgedeki örnekte aynı anahtara önce is_deleted = 0, sonra aynı eventTime ile is_deleted = 1 eklenir ve SELECT … FINAL sıfır satır döner. Dikkat: belgedeki bu tablo SETTINGS allow_experimental_replacing_merge_with_cleanup = 1 ile oluşturulmuştur; yukarıdaki DDL'de bu ayar yoktur. Aynı belge, varsayılan durumda ClickHouse'un silme satırı olsa bile bir anahtarın son satırını tuttuğunu söyler. Bu yüzden silinmiş satırı sorguda WHERE is_deleted = 0 ile de süzmek güvenli yoldur. Silme satırlarının diskten de gitmesini isterseniz belge aynı ayarı ve OPTIMIZE TABLE … FINAL CLEANUP komutunu gösterir. Ayarın adında "experimental" geçer; üretime almadan önce kendi sürümünüzde deneyin.
Eski yazılarda gördüğünüz clean_deleted_rows ayarına güvenmeyin. ClickHouse 23.12 sürüm notu (2023-12-28) şunu yazar: "The MergeTree setting clean_deleted_rows is deprecated, it has no effect anymore. The CLEANUP keyword for the OPTIMIZE is not allowed by default (it can be unlocked with the allow_experimental_replacing_merge_with_cleanup setting)." (23.12 notları). Altinity bilgi bankasındaki örnek clean_deleted_rows='Always' kullanır; yukarıdaki sürüm notuna göre bu ayarı kopyalarsanız etkisi olmaz.
Debezium/CDC olayları tabloya nasıl eşlenir
CDC akışında asıl iş, kaynaktan gelen her olayı üç sütuna dağıtmaktır: anahtar, sürüm, silme işareti. ClickHouse'un iki resmî örneği aynı mantığı kullanır:
- ClickPipes Postgres belgesi: "updates are modeled as inserts with a newer version (
_peerdb_version) of the row, while deletes are inserts with a newer version and_peerdb_is_deletedmarked as true". TabloReplacingMergeTree(_peerdb_version)ile kurulur ve sorguFINAL WHERE _peerdb_is_deleted=0ile yazılır (ClickPipes). - Debezium'lu CDC blog yazısı (2023): sürüm
source.lsnalanından gelir,if(op = 'd', 1, 0) AS deletedile silme işareti kurulur; motorReplacingMergeTree(version, deleted)olur (ClickHouse blogu). Yazı kendisi daha yeni yol olarak ClickPipes'ı anar.
Debezium PostgreSQL olayından tabloya eşleme:
| Debezium olayı | ReplacingMergeTree sütunu | Not |
|---|---|---|
after.<alan> (op = c, u, r) | veri sütunları | r anlık görüntü (snapshot) olayıdır; silmede after boştur |
before.<anahtar> (op = d) | anahtar sütunu | before içeriği tablonun REPLICA IDENTITY ayarına bağlıdır; DEFAULT yalnız birincil anahtarı taşır |
source.lsn | ver | Blog örneğindeki seçim |
op = 'd' | is_deleted = 1 | Diğer op değerleri 0 |
tombstone (silme olayının ardından gelen, aynı anahtarlı, değeri null kayıt) | veri sütunu ya da is_deleted değeri taşımaz; silmeyi op = 'd' olayı bildirir | Kafka log sıkıştırması içindir; tombstones.on.delete ayarına bağlıdır |
Debezium belgesi şunu yazar: "the PostgreSQL connector follows a delete event with a special tombstone event that has the same key but a null value." Amaç, Kafka'nın aynı anahtarlı eski mesajları log sıkıştırmasında silebilmesidir; kayıt tombstones.on.delete ayarıyla yönetilir (PostgreSQL bağlayıcısı). Tombstone'un değeri null olduğundan içinde veri sütunu ya da silme bilgisi yoktur; silme bilgisi önceki op = 'd' olayındadır. Tüketicinizin ya da Kafka bağlayıcınızın boş değerli kayıtları nasıl ele aldığını kendi kurulumunuzda kontrol edin. REPLICA IDENTITY DEFAULT iken silme olayının before alanında yalnız birincil anahtar vardır ve after boştur; bu yüzden silme satırında anahtar dışındaki sütunların dolu geleceğine güvenmeyin.
Sorguyu şöyle kurarsınız:
SELECT key, someCol
FROM musteri_durum2 FINAL
WHERE is_deleted = 0;Hangi durumda ReplacingMergeTree yetmez
Her sorguda tekil satır bekleniyorsa ve tablo büyükse, her sorguya FINAL eklemek ek işlemci ve bellek harcar; belge bunu açıkça yazar. FINAL maliyetini kendi tablonuzda ölçmeden karar vermeyin; iki sistemin rol farkı için ClickHouse ile PostgreSQL raporlama farkı yazısına bakın. Temel kavramlar için ClickHouse rehberi, olay akışının kaynağı için Kafka CDC rehberi var. Tablo ve akış tasarımını dışarıdan gözden geçirtmek isterseniz ClickHouse danışmanlığı sayfasına bakabilirsiniz.



