Powrót do strony głównej

Partycjonowanie w ClickHouse: strategie i operacje

Artykuł wyjaśnia partycjonowanie w ClickHouse jako mechanizm fizycznego podziału danych na poziomie folderów. Omówiono funkcje PARTITION BY (toYYYYMM, toYYYYMMDD), przeglądanie przez system.parts, operacje DROP/DETACH/ATTACH/FREEZE/MOVE PARTITION, strategię wyboru rozmiaru partycji (100–1000 na serwer) i automatyczne usuwanie starych danych przez skrypt.

Partycjonowanie w ClickHouse: kompletny przewodnik
Advertisement 728x90

Partycyjonowanie w ClickHouse: Jak zarządzać danymi na poziomie folderów

1. Po co partycyjonowanie — izolacja danych według okresów

Wyobraź sobie, że przechowujesz wszystkie zakłady z kasyna online z ostatnich trzech lat. To miliardy wierszy. Właściciel biznesu nagle mówi: „Potrzebujemy danych tylko z ostatnich 6 miesięcy, wszystko starsze usuń”.

W zwykłej bazie danych napisałbyś DELETE FROM bets WHERE created_at < '2025-01-01'. W ClickHouse takie zapytanie będzie działać… bardzo długo. Ponieważ DELETE w ClickHouse to nie natychmiastowe usunięcie, ale asynchroniczne przepisywanie fragmentów danych (parts) bez oznaczonych wierszy.

Ale jest sposób, aby usunięcie było natychmiastowe — partycyjonowanie. Jeśli dane są podzielone na partycje (partition — logiczne fragmenty, z których każdy fizycznie leży w osobnym folderze na dysku), to DROP PARTITION usuwa cały folder w milisekundach. Nie trzeba skanować wierszy, nie trzeba przepisywać — po prostu rm -rf na poziomie systemu plików.

Google AdInline article slot

Analogia z życia: Wyobraź sobie, że masz papierowe archiwa z kilku lat. Są przechowywane w pudełkach, każde pudełko to jeden miesiąc. Jeśli chcesz usunąć dane za styczeń 2024, po prostu wynosisz pudełko z napisem „styczeń 2024” na śmietnik. Nie musisz przeglądać każdej kartki. Partycje w ClickHouse to właśnie te pudełka.

Po co jeszcze partycyjonowanie:

  • Kopie zapasowe — możesz zamrozić (FREEZE) tylko potrzebną partycję.
  • Przenoszenie danych — gorące dane (często używane) na szybkie SSD, zimne (rzadkie) na wolne HDD lub do chmury (S3).
  • Przyspieszenie SELECT — jeśli w WHERE podano kolumnę partycjonowania, ClickHouse od razu wie, które foldery czytać, a które pominąć.

2. PARTITION BY — składnia i przykłady

Partycyjonowanie definiuje się podczas tworzenia tabeli za pomocą PARTITION BY. Najczęstszy wzorzec to podział według daty.

Google AdInline article slot
-- Tworzymy tabelę zakładów z partycjami miesięcznymi
CREATE TABLE bets
(
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(created_at)   -- Partycja = rok + miesiąc, np. 202512
ORDER BY (user_id, created_at);

Co tu się dzieje:

  • toYYYYMM(created_at) — funkcja ClickHouse, która z daty-czasu 2025-12-15 14:30:00 zwraca liczbę 202512 (rok 2025, miesiąc 12). Wszystkie wiersze z tym samym 202512 trafiają do jednej partycji.
  • Nazwa partycji to po prostu ciąg znaków. ClickHouse sam utworzy folder na dysku o nazwie np. 202512_1_1_0 (szczegóły nie są ważne, ale wewnątrz będzie powiązanie z 202512).

Inne warianty partycjonowania według czasu:

-- Według dni (ostrożnie! może być zbyt wiele partycji)
PARTITION BY toYYYYMMDD(created_at)   -- 20251215

-- Według godzin (bardzo drobno, prawie nigdy niepotrzebne)
PARTITION BY toStartOfHour(created_at)   -- 2025-12-15 14:00:00

-- Według tygodni
PARTITION BY toWeek(created_at)   -- Numer tygodnia w roku

-- Według lat
PARTITION BY toYear(created_at)   -- 2025

Co się stanie, jeśli nie podasz PARTITION BY? ClickHouse utworzy jedną partycję o nazwie all. Wszystkie dane będą leżeć w jednym folderze. Usuwanie będzie możliwe tylko przez DELETE (wolno) lub TRUNCATE (wszystko naraz). Dla większości tabel z szeregami czasowymi to zły pomysł.

Google AdInline article slot

3. Strategia wyboru partycji — złoty środek

Dlaczego partycje nie powinny być zbyt małe?

ClickHouse nie lubi, gdy partycji jest zbyt wiele (zaleca się nie więcej niż 1000–2000). Ponieważ:

  • Każda partycja to osobny folder z metadanymi.
  • Podczas wstawiania danych ClickHouse może tworzyć fragmenty w różnych partycjach.
  • Tła scalania (merge) działają wewnątrz partycji, ale nie między nimi.
  • System system.parts (tabela z metadanymi o fragmentach) będzie ogromny.

Co się stanie, jeśli zrobisz partycje godzinowe przy obciążeniu 1 mln wierszy na godzinę? W ciągu roku otrzymasz 365 * 24 = 8760 partycji. To już źle. Przy każdym wstawieniu ClickHouse będzie otwierać deskryptory na tysiące folderów. SELECT z filtrowaniem po dacie będzie szybki, ale operacje systemowe (kopie zapasowe, usuwanie, przeglądanie listy partycji) zaczną zwalniać.

Dlaczego partycje nie powinny być zbyt duże?

Jeśli partycja to rok, to DROP PARTITION usunie wszystko za rok naraz. To wygodne, ale:

  • Przy SELECT nawet za jeden dzień trzeba skanować całą roczną partycję.
  • Nie można szybko usunąć jednego miesiąca — trzeba usunąć rok lub zrobić DELETE (wolno).
  • Przenoszenie „gorących” i „zimnych” danych (np. ostatni miesiąc na SSD, reszta na HDD) jest niemożliwe na poziomie jednego miesiąca.

Zasada kciuka

Obciążenie Rozmiar partycji Przykład
Miliardy wierszy miesięcznie Dzień (toYYYYMMDD) Dane z czujników IoT, logi CDN
Setki milionów miesięcznie Miesiąc (toYYYYMM) Zakłady w kasynie, transakcje
Dziesiątki milionów miesięcznie Miesiąc lub kwartał Analityka sprzedaży
Tysiące wierszy miesięcznie Rok Wolno zmieniające się słowniki

Główny kryterium: Po partycjonowaniu powinno być od 100 do 1000 partycji na serwer. Jeśli masz 10 000 partycji — przesadziłeś.

4. Przeglądanie partycji — system.parts

Aby zobaczyć, jakie partycje są w tabeli i ile w nich danych, używa się tabeli systemowej system.parts.

-- Przeglądamy wszystkie partycje tabeli bets
SELECT 
    partition,                          -- Nazwa partycji (np. 202512)
    name,                               -- Nazwa konkretnego fragmentu (part)
    rows,                               -- Liczba wierszy w tym fragmencie
    bytes_on_disk,                      -- Rozmiar na dysku w bajtach
    modification_time,                  -- Czas ostatniej modyfikacji
    active                              -- Czy fragment jest aktywny (1) czy już usunięty (0)
FROM system.parts
WHERE table = 'bets' AND active = 1    -- tylko aktywne fragmenty
ORDER BY partition DESC;

Co jest co:

  • partition — to, co podałeś w PARTITION BY. Przydatne do grupowania.
  • name — wewnętrzna nazwa fragmentu danych, zawiera numer partycji i wersję.
  • active = 1 — fragment jest używany do odczytu. Jeśli 0 — fragment oznaczony do usunięcia, ale fizycznie jeszcze istnieje.
  • bytes_on_disk — pokazuje, ile miejsca zajmuje partycja.

Dlaczego w jednej partycji może być kilka fragmentów? Ponieważ wstawiasz dane paczkami. ClickHouse nie scala ich natychmiast. Wewnątrz jednej partycji (202512) może być 5–10 małych fragmentów, które z czasem połączą się w jeden duży.

Zapytanie zagregowane — suma według partycji:

SELECT 
    partition,
    sum(rows) AS total_rows,
    sum(bytes_on_disk) AS total_bytes,
    formatReadableSize(sum(bytes_on_disk)) AS human_size
FROM system.parts
WHERE table = 'bets' AND active = 1
GROUP BY partition
ORDER BY partition DESC;

5. Operacje na partycjach — DROP, DETACH, ATTACH, FREEZE

To główny powód, dla którego partycjonowanie jest tak lubiane. Z partycjami można robić rzeczy niedostępne na poziomie wierszy.

DROP PARTITION — natychmiastowe usunięcie

-- Usuń wszystkie dane za grudzień 2025
ALTER TABLE bets DROP PARTITION '202512';

-- Usuń dane starsze niż 90 dni (dynamicznie)
-- Najpierw obliczamy nazwę partycji sprzed 90 dni
ALTER TABLE bets DROP PARTITION toYYYYMM(today() - interval 90 day);

Po tym poleceniu folder z partycją znika z dysku. Operacja trwa milisekundy, niezależnie od tego, ile wierszy było w środku — milion czy miliard.

Dlaczego to bezpieczne? Ponieważ partycje są izolowane. Usunięcie jednej nie wpływa na inne.

DETACH PARTITION — tymczasowe odłączenie

-- Odłączamy partycję (dane pozostają na dysku, ale są niedostępne dla SELECT)
ALTER TABLE bets DETACH PARTITION '202512';

Partycja przenosi się do podfolderu detached. Nie uczestniczy w zapytaniach, ale można ją przywrócić.

Kiedy to potrzebne:

  • Podejrzewasz, że dane są uszkodzone i chcesz je tymczasowo ukryć.
  • Chcesz skopiować partycję na inny serwer (ATTACH później w nowym miejscu).

ATTACH PARTITION — przywrócenie odłączonej partycji

-- Przywracamy partycję (jeśli jest w folderze detached)
ALTER TABLE bets ATTACH PARTITION '202512';

FREEZE PARTITION — natychmiastowa kopia zapasowa

-- Tworzymy kopię zapasową partycji grudzień 2025
ALTER TABLE bets FREEZE PARTITION '202512';

ClickHouse tworzy twarde linki (hard links) do plików partycji w folderze shadow/. To nie kopiowanie danych (szybkie), a jedynie tworzenie dodatkowych wskaźników do tych samych bloków na dysku. Potem możesz skopiować shadow na inny serwer.

Dlaczego to lepsze niż zwykła kopia zapasowa? Ponieważ nie trzeba zatrzymywać zapisu i nie marnuje się miejsca na duplikowanie danych.

MOVE PARTITION — dla tiered storage

Więcej o tym w sekcji 6.

6. MOVE PARTITION — tiered storage (SSD → HDD → S3)

W ClickHouse można skonfigurować wiele magazynów o różnym koszcie i szybkości. Na przykład:

  • Gorące dane (ostatni miesiąc) — na szybkich NVMe SSD
  • Ciepłe dane (2–6 miesięcy) — na zwykłych HDD
  • Zimne dane (starsze niż 6 miesięcy) — w chmurze S3 (tanio, ale wolno)

Partycje pozwalają przenosić dane między tymi poziomami jednym poleceniem.

Konfiguracja w config.xml (uproszczona):

<storage_configuration>
    <disks>
        <ssd>
            <path>/mnt/ssd/clickhouse/</path>
        </ssd>
        <hdd>
            <path>/mnt/hdd/clickhouse/</path>
        </hdd>
    </disks>
    <policies>
        <hot_to_cold>
            <volumes>
                <hot>
                    <disk>ssd</disk>
                </hot>
                <cold>
                    <disk>hdd</disk>
                </cold>
            </volumes>
        </hot_to_cold>
    </policies>
</storage_configuration>

Przenoszenie partycji z SSD na HDD:

-- Dane za styczeń 2025 (już niepotrzebne szybko) przenosimy na HDD
ALTER TABLE bets MOVE PARTITION '202501' TO VOLUME 'cold';

Co się stanie: ClickHouse fizycznie przeniesie folder partycji z SSD na HDD. SELECT nadal będzie działać, ale trochę wolniej (z powodu HDD). DROP PARTITION nadal jest szybki.

Analogia: Przekładasz stare foldery z szybkiej szafki obok siebie do dalekiej archiwalnej szafki. Dostęp jest, ale droga dłuższa.

7. Partycjonowanie według wielu kolumn

Czasami trzeba dzielić dane nie tylko według czasu, ale także według innego wymiaru. Na przykład masz platformę wielomarkową (kilka kasyn w jednej bazie). Chcesz usuwać stare dane osobno dla każdej marki i ewentualnie przechowywać je na różnych dyskach.

-- Tabela zakładów dla 5 marek, partycje według miesiąca I marki
CREATE TABLE bets_multi_brand
(
    brand_id    UInt8,          -- 1 = casino_A, 2 = casino_B, ...
    user_id     UInt64,
    amount      Decimal(18,2),
    created_at  DateTime
)
ENGINE = MergeTree()
PARTITION BY (toYYYYMM(created_at), brand_id)   -- Dwie kolumny!
ORDER BY (brand_id, user_id, created_at);

Jak to działa:

  • Każda unikalna kombinacja (rok+miesiąc, brand_id) staje się osobną partycją.
  • Dla grudnia 2025 i marki 1 — partycja ('202512', 1).
  • Dla grudnia 2025 i marki 2 — partycja ('202512', 2).

Dlaczego to przydatne:

  • DROP PARTITION może usunąć dane konkretnej marki za konkretny miesiąc.
  • MOVE PARTITION może przenieść stare dane marki A na HDD, a marki B (premium) zostawić na SSD.

Jak usunąć partycję dla konkretnej marki:

-- Usuwamy dane casino_A za grudzień 2025
ALTER TABLE bets_multi_brand DROP PARTITION ('202512', 1);

Ale jest niuans: Nazwa partycji to teraz krotka (tuple). W system.parts będzie wyglądać jak ('202512', '1'). To normalne.

8. Jak partycjonowanie wpływa na wydajność

Wpływ na INSERT

Podczas wstawiania danych ClickHouse patrzy na wyrażenie PARTITION BY i kieruje wiersz do odpowiedniej partycji. Jeśli masz 1000 partycji — to szybko. Jeśli 100 000 — każde wstawienie musi określić właściwy folder, co spowalnia zapis.

Co się stanie, jeśli w jednym INSERT wiersze pochodzą z różnych partycji? ClickHouse rozłoży je do różnych folderów — to normalne, ale trochę wolniejsze niż gdyby wszystkie wiersze były z jednej partycji.

Rada: Jeśli wstawiasz dane paczkami (np. raz na minutę), staraj się, aby w jednej paczce były wiersze z krótkiego przedziału czasu. Wtedy trafią do jednej-dwóch partycji, a wstawienie będzie szybsze.

Wpływ na SELECT

Partycyjonowanie przyspiesza SELECT tylko jeśli w WHERE jest warunek na kolumnę partycjonowania. ClickHouse używa partycji do wstępnego odcięcia: sprawdza, które partycje są potrzebne, i czyta tylko ich foldery.

-- SZYBKO: partycja odetnie wszystkie dane poza grudniem 2025
SELECT count() FROM bets 
WHERE created_at BETWEEN '2025-12-01' AND '2025-12-31';

-- WOLNO: partycja nie jest używana, skanujemy wszystkie partycje
SELECT count() FROM bets 
WHERE amount > 1000;   -- brak filtra po created_at

Dlaczego nie zawsze trzeba partycjonować bardzo drobno? Ponieważ:

  • Czytanie z 1000 małych partycji (np. dziennych przez 3 lata) może być wolniejsze niż z 36 dużych (miesięcznych), jeśli zapytanie obejmuje duży zakres. ClickHouse otwiera deskryptor pliku na partycję.
  • Indeksy (klucz podstawowy) często są skuteczniejsze niż drobne partycjonowanie.

Zasada empiryczna: Najpierw optymalizuj ORDER BY (indeks), potem partycjonowanie. Partycje służą do zarządzania danymi (DROP, MOVE), a nie tylko do przyspieszania SELECT.

9. Typowy błąd: zbyt szczegółowe partycjonowanie

To najczęstszy ból u początkujących w ClickHouse.

Przykład złego podejścia:

-- ŹLE: partycja dzienna dla tabeli z 10 mln wierszy dziennie
CREATE TABLE logs_bad
(
    created_at DateTime,
    message String
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(created_at)   -- Ostrożnie!
ORDER BY created_at;

Po 3 latach otrzymasz 365 * 3 = 1095 partycji. To jeszcze znośne (ale już na granicy). Po 10 latach — 3650 partycji, co jest złe. System zacznie zwalniać.

Jeszcze gorzej — godzinowe dla danych z obciążeniem 1 mln rekordów na godzinę: W ciągu roku 8760 partycji. Przy każdym wstawieniu ClickHouse będzie szukać lub tworzyć folder dla bieżącej godziny. SELECT * FROM system.parts zacznie zajmować sekundy. Tła scalania będą się dusić.

Objawy, że masz zbyt wiele partycji:

  1. Zapytanie SELECT * FROM system.parts WHERE table = 'mytable' wykonuje się dłużej niż 1 sekundę.
  2. W logach ClickHouse pojawiają się ostrzeżenia: Too many parts (300+).
  3. ALTER TABLE ... DROP PARTITION działa nie natychmiastowo, ale kilka sekund.

Co zrobić, jeśli już zrobiłeś zbyt drobne partycje? Można połączyć partycje przez ALTER TABLE ... MODIFY PARTITION BY ..., ale to skomplikowane. Prościej utworzyć nową tabelę z prawidłowym partycjonowaniem, przenieść dane i zmienić nazwę.

10. Skrypt automatycznego usuwania starych partycji

W praktyce musisz przechowywać dane np. tylko z ostatnich 90 dni. Przypominanie sobie co miesiąc o usuwaniu starych partycji to nie opcja. Potrzebna automatyzacja.

Wariant 1: Okresowe zapytanie przez cron lub Task

-- Usuwamy wszystkie partycje starsze niż 90 dni
-- Wykonywać raz dziennie o 3:00 nad ranem
ALTER TABLE bets DROP PARTITION WHERE partition < toYYYYMM(today() - interval 90 day);

Jak to działa:

  • toYYYYMM(today() - interval 90 day) — oblicza nazwę partycji dla daty sprzed 90 dni. Na przykład, jeśli dziś jest 2026-06-11, to 90 dni temu — 2026-03-13, toYYYYMM da 202603.
  • partition < 202603 — usunie wszystkie partycje o nazwach 202602, 202601, 202512 itd.

Ważne: To zapytanie działa tylko jeśli nazwy partycji są liczbami (rok+miesiąc). Dla nazw tekstowych (np. '2025-12') potrzebne jest inne porównanie.

Wariant 2: Bezpieczniejszy skrypt przez przechowywane listy

-- Znajdujemy listę partycji starszych niż N dni
SELECT partition
FROM system.parts
WHERE table = 'bets' 
  AND active = 1
  AND toDate(parseDateTimeBestEffort(partition)) < today() - interval 90 day
GROUP BY partition;

-- Dla każdej znalezionej partycji wykonujemy DROP (ze skryptu zewnętrznego, nie bezpośrednio z SQL)

Dlaczego nie robić DROP PARTITION WHERE partition < ... z dynamicznym obliczaniem? Ponieważ jeśli w tabeli są partycje o niestandardowych nazwach (np. 'all' lub utworzone ręcznie), warunek może zadziałać nieoczekiwanie. Lepiej najpierw zobaczyć listę.

Pełny przykład skryptu w Bash / Python:

#!/bin/bash
# Usuwamy partycje starsze niż 90 dni dla tabeli bets

CLICKHOUSE_CLIENT="clickhouse-client --host localhost"
TABLE="bets"
DAYS_TO_KEEP=90

# Obliczamy datę graniczną
BORDER_DATE=$(date -d "today - $DAYS_TO_KEEP days" +%Y%m)

# Pobieramy listę partycji starszych niż granica
PARTITIONS=$($CLICKHOUSE_CLIENT --query="
    SELECT DISTINCT partition 
    FROM system.parts 
    WHERE table = '$TABLE' 
      AND active = 1 
      AND partition < '$BORDER_DATE'
      AND partition != 'all'
    FORMAT TabSeparated
")

# Usuwamy każdą partycję
for PARTITION in $PARTITIONS; do
    echo "Dropping partition $PARTITION from $TABLE"
    $CLICKHOUSE_CLIENT --query="ALTER TABLE $TABLE DROP PARTITION '$PARTITION'"
done

Automatyzacja przez cron:

# Uruchomienie codziennie o 3:00
0 3 * * * /usr/local/bin/cleanup_clickhouse_partitions.sh >> /var/log/cleanup.log 2>&1

Co dalej

Teraz rozumiesz, jak partycjonowanie zamienia usuwanie danych z bolesnej operacji w natychmiastowe działanie. Następne tematy:

  • Zaawansowane strategie TTL (time-to-live) — automatyczne usuwanie lub przenoszenie wierszy bez twojego udziału.
  • Konfiguracja storage policies — jak zrobić automatyczny MOVE partycji z SSD na S3 według harmonogramu.
  • Optymalizacja SELECT za pomocą partycji — jak pisać zapytania, aby ClickHouse odcinał 99% partycji.

Podsumowanie: Partycjonowanie w ClickHouse to narzędzie do zarządzania cyklem życia danych, a nie tylko do przyspieszania zapytań. Wybieraj rozmiar partycji tak, aby było ich od 100 do 1000 na serwer. Nie rozdrabniaj się, nie przesadzaj w drugą stronę. I zawsze patrz na system.parts — ona powie prawdę.


Poprzedni:
Następny: ORDER BY i PRIMARY KEY w ClickHouse: jak nie przegapić z indeksem

— Editorial Team

Advertisement 728x90

Czytaj dalej