Arhitektūra

ClickHouse ReplacingMergeTree dublikāti: kāpēc tie paliek un kā lasīt pareizi

ReplacingMergeTree nesola tabulu bez dublikātiem. Tas tos noņem galu galā, sapludināšanas laikā un tikai tur, kur sapludināšana redz abas rindas. Kāpēc dublikāti paliek, kā tomēr vaicāt pareizi un kas mainās terabaitu mērogā.

· 15 min lasīšanai · AG Data komanda

ClickHouse ReplacingMergeTree dublikāti ir viens no biežākajiem pārsteigumiem komandām, kas nāk no PostgreSQL vai MySQL. Jūs izvēlaties šo dzinēju, jo vēlaties upsert, ievietojat rindas jaunu versiju, un nākamajā dienā vecā versija joprojām ir tur. Panelis rāda divkāršus ieņēmumus. Nekas nav salūzis: dzinējs ir veidots tieši tā.

Dokumentācija to saka tieši: deduplikācija notiek tikai sapludināšanas (merge) laikā, sapludināšana notiek fonā nezināmā laikā, un dzinējs negarantē, ka dublikātu nebūs. Kad to pieņemat, darbs sadalās divās daļās: vaicāt tā, lai rezultāts būtu pareizs vēl pirms sapludināšanas, un veidot tabulu tā, lai sapludināšana dublikātus tiešām varētu noņemt.

Kāpēc dublikāti paliek

Rinda tiek aizstāta tikai tad, kad fona sapludināšana nolasa abas versijas un ieraksta vienu. Viss, kas neļauj abām versijām satikties vienā sapludināšanā, uztur dublikātu dzīvu:

Ievietošanas deduplikācija ir kas cits. Replicated* tabulas var izlaist atkārtotu viena un tā paša bloka ievietošanu (iestatījums insert_deduplicate, pēc noklusējuma ieslēgts). Tas pasargā no vienas partijas atkārtojumiem. Tas neko nedara ar rindas jaunu versiju, kas atnāk citā partijā.

Pārbaudiet, cik dublikātu tiešām ir

-- how many duplicates are still physically there?
SELECT count() AS rows, uniqExact(order_id) AS keys, rows - keys AS extra_versions
FROM orders;

-- where are they? (same key in several parts or partitions)
SELECT order_id, count() AS versions, groupUniqArray(_partition_id) AS partitions
FROM orders
GROUP BY order_id
HAVING versions > 1
ORDER BY versions DESC
LIMIT 20;

extra_versions parāda, cik tabula saruktu pēc pilnas sapludināšanas un cik nepareizs šobrīd ir parasts count(). Otrais vaicājums parāda, vai dublikāti atrodas vienā partīcijā (tie, visticamāk, vēlāk tiks sapludināti) vai dažādās (tie netiks nekad). Lielā tabulā palaidiet to pa vienai partīcijai.

ver un is_deleted izvēle

Dzinējs pieņem divus neobligātus parametrus. ver ir versijas kolonna ar tipu UInt*, Date, DateTime vai DateTime64. Ar to uzvar rinda ar lielāko versiju; bez tās uzvar pēdējā ievietotā rinda. Izmantojiet īstu versiju, piemēram, atjaunināšanas laiku no avota vai žurnāla pozīciju no CDC. "Pēdējā ievietotā" ir atkarīga no secības, kādā atnāk partijas, un atkārtotas ielādes vai vēsturisko datu aizpilde mierīgi pārrakstīs jaunus datus ar veciem.

is_deleted ir UInt8 kolonna, kur 1 nozīmē, ka rinda ir dzēsta. Dokumentācija norāda, ka to var ieslēgt tikai tad, ja izmanto ver. Lai dzēstu rindu, jūs ievietojat tās jaunu versiju ar lielāku ver un is_deleted = 1. Pēc noklusējuma pats dzēšanas marķieris pēc sapludināšanas paliek, lai vēlāk atnākusi vecāka versija nevarētu rindu atdzīvināt. Vaicājumi ar FINAL neatgriež rindas, kuru jaunākā versija ir dzēšana.

Tabula, kas izskatās pareiza, bet kurai ir partīciju problēma:

CREATE TABLE orders
(
    order_id   UInt64,
    status     LowCardinality(String),
    amount     Decimal(18, 2),
    updated_at DateTime64(3),
    is_deleted UInt8 DEFAULT 0
)
ENGINE = ReplacingMergeTree(updated_at, is_deleted)
PARTITION BY toYYYYMM(updated_at)   -- changes with every new version
ORDER BY order_id;

Pasūtījumam, kas izveidots oktobrī un atjaunināts novembrī, tagad ir pa versijai katrā mēneša partīcijā, un tās nekad netiks sapludinātas. Drošāks variants partīcijo pēc nemainīgas kolonnas un liek to pašu izteiksmi kārtošanas atslēgas sākumā:

CREATE TABLE orders
(
    order_id   UInt64,
    created_at DateTime,
    status     LowCardinality(String),
    amount     Decimal(18, 2),
    updated_at DateTime64(3),
    is_deleted UInt8 DEFAULT 0
)
ENGINE = ReplacingMergeTree(updated_at, is_deleted)
PARTITION BY toYYYYMM(created_at)
ORDER BY (toYYYYMM(created_at), order_id);

Tagad rinda nevar pārvietoties starp partīcijām, un saskaņā ar ceļvedi kopš versijas 23.12 ClickHouse vaicājuma laikā izvairās no sapludināšanas starp partīcijām, ja partīcijas atslēga ir kārtošanas atslēgas prefikss. Tas padara arī FINAL lētāku. Ceļvedis arī iesaka ORDER BY kolonnas turēt nemainīgas un avota primāro atslēgu pievienot beigās.

Trīs veidi, kā lasīt pareizus rezultātus

-- 1. FINAL: merge at query time
SELECT status, sum(amount)
FROM orders FINAL
WHERE created_at >= '2026-11-01'
GROUP BY status;

-- 2. argMax: keep the latest version yourself
SELECT order_id,
       argMax(status, updated_at) AS status,
       argMax(amount, updated_at) AS amount
FROM orders
GROUP BY order_id
HAVING argMax(is_deleted, updated_at) = 0;

-- 3. LIMIT BY: one row per key
SELECT *
FROM orders
ORDER BY order_id, updated_at DESC
LIMIT 1 BY order_id;

Ja katram vaicājumam pret tabulu vajag FINAL, iestatījums final to piemēro automātiski sesijai vai lietotāja profilam. Partīcijotām tabulām do_not_merge_across_partitions_select_final = 1 liek FINAL apstrādāt partīcijas neatkarīgi. Izmantojiet to tikai tad, ja rindas versijas nekad nevar būt dažādās partīcijās, citādi dublikāti atgriežas.

OPTIMIZE FINAL un CLEANUP: lietojiet taupīgi

OPTIMIZE TABLE ... FINAL piespiež sapludināšanu. Dokumentācija brīdina, ka tas nolasīs un ierakstīs lielu datu apjomu, un ceļvedis piebilst, ka tas var prasīt daudz atmiņas un var būt jāatkārto, nākot jauniem datiem. Tas arī ignorē parasto sapludināšanas izmēra ierobežojumu, tāpēc viena komanda var pārrakstīt veselu simtiem gigabaitu lielu partīciju. Palaidiet to pa partīcijām, vecām partīcijām, kurās vairs neraksta, klusās stundās un tikai tad, kad tiešām jāatgūst diska vieta.

Parastā sapludināšana dzēšanas marķierus nekad nenoņem. Lai tos izmestu, dokumentācija prasa eksperimentālo iestatījumu allow_experimental_replacing_merge_with_cleanup un vai nu manuālu OPTIMIZE ... FINAL CLEANUP, vai automātisku veco partīciju piespiedu sapludināšanu ar min_age_to_force_merge_seconds un saistītiem iestatījumiem:

ALTER TABLE orders MODIFY SETTING allow_experimental_replacing_merge_with_cleanup = 1;

OPTIMIZE TABLE orders PARTITION 202610 FINAL CLEANUP;

Tīriet tikai tās partīcijas, kurās vēlas, nesecīgas versijas vairs nevar atnākt; citādi veca versija var atdzīvoties. Ja vajag īstu dzēšanu, nevis marķierus, salīdziniet iespējas mūsu ceļvedī par rindu dzēšanu ClickHouse.

Kas mainās terabaitu mēroga tabulās

Galvenās atziņas

ClickHouse ReplacingMergeTree Deduplikācija Datu modelēšana
Sadarbība ar mums

Dublikāti jūsu ClickHouse atskaitēs?

Mēs pārskatām ReplacingMergeTree shēmas, partīciju un šardēšanas atslēgas un salabojam vaicājumus, kas skaita divreiz, nepārrakstot jūsu datu plūsmu.

Sazināties Vairāk no emuāra