אופטימיזציית ClickHouse: חלוקה למחיצות, אינדקסים ותצוגות מהותיות
שאילתת SELECT על טבלה עם 2 טריליון שורות לוקחת 40 שניות. קלט/פלט בדיסק ב-100%, השרת מזמזם. נשמע מוכר? בדרך כלל, זה אומר שמפתח ראשי נבחר בצורה גרועה, החלוקה למחיצות לא תואמת את דפוס השאילתות, או שהנתונים נשאלים ללא ניצול אינדקסי דילוג. אנחנו צוות מהנדסים עם ניסיון של 10+ שנים בהפעלת ClickHouse בסביבת ייצור. הגישה שלנו מספקת תוצאות מדידות: חיסכון טיפוסי בזמן ביצוע שאילתות של פי 5–10, והפחתת עומס דיסק עד 70%. צרו קשר לבדיקת סכמה—נעריך את פוטנציאל האופטימיזציה תוך יום אחד.
למה חלוקה למחיצות היא לא פתרון קסם
טעות נפוצה: יצירת טבלה עם PARTITION BY toYYYYMM(event_date) וציפייה לניסים. חלוקה למחיצות ב-ClickHouse היא בעיקר כלי לניהול נתונים (TTL, מחיקת מחיצות ישנות), לא דרך להאיץ שאילתות. שאילתה שמסננת לפי חודש ספציפי אכן מדלגת על מחיצות לא רלוונטיות, אבל אם מסנן התאריך רופף או חסר—ClickHouse קורא הכל.
סיפור אמיתי: לקוח חילק טבלת אירועים לפי toYYYYMMDD(ts)—365 מחיצות בשנה. זה גרם ליצירת אלפי חלקים זעירים בכל הכנסה לילית, MergeTree לא הצליח לעמוד במיזוגים, ושאילתות החלו לפתוח מאות מתארי קבצים. יומן המערכת היה מלא ב-Too many parts (300). Merges are processing significantly slower than inserts. חזרה לחלוקה חודשית וכיוונון merge_with_ttl_timeout פתרו את הבעיה תוך שעה.
גודל מחיצה אופטימלי: מחיצה צריכה להכיל נתונים לתקופה שנוחה למחיקה. השתמשו במחיצות יומיות רק אם נפח הנתונים היומי עולה על כמה ג'יגה-בייט; אחרת היצמדו לחודשי.
איך לבחור את ORDER BY הנכון?
ב-ClickHouse, ORDER BY מגדיר את סדר המיון הפיזי של הנתונים על הדיסק ומשמש כמפתח ראשי (אם PRIMARY KEY לא צוין במפורש). גודל הגרנולה ברירת המחדל הוא 8192 שורות. כל גרנולה מתוארת באינדקס דליל, ו-ClickHouse מחליט אילו גרנולות לקרוא על סמך אינדקס זה.
אם האלמנט הראשון של ORDER BY הוא user_id UUID, אבל כל השאילתות מסננות לפי event_type ו-ts, האינדקס חסר תועלת—ClickHouse קורא את כל הגרנולות.
הכלל לבחירת מפתח: שימו קודם עמודות עם קרדינליות נמוכה שמופיעות בתדירות הגבוהה ביותר ב-WHERE, ואז עמודות עם קרדינליות גבוהה. לדוגמה:
CREATE TABLE events (
event_type LowCardinality(String),
project_id UInt32,
ts DateTime,
user_id UUID,
payload String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ts)
ORDER BY (event_type, project_id, ts)
SETTINGS index_granularity = 8192;שאילתה כמו CREATE TABLE events ( event_type LowCardinality(String), project_id UInt32, ts DateTime, user_id UUID, payload String ) ENGINE = MergeTree() PARTITION BY toYYYYMM(ts) ORDER BY (event_type, project_id, ts) SETTINGS index_granularity = 8192; קוראת כעת רק את טווח הגרנולות הדרוש.
איך פועלים אינדקסי דילוג?
WHERE event_type = 'click' AND project_id = 42 AND ts >= now() - INTERVAL 1 YEAR, minmax, set, bloom_filter הם ארבעת הסוגים העיקריים של אינדקסי דילוג. הם לא מחליפים את המפתח הראשי אלא משלימים אותו עבור עמודות שאינן ב-tokenbf_v1.
ORDER BY חיוני לחיפושי UUID או hash:
ALTER TABLE events ADD INDEX idx_user_id user_id TYPE bloom_filter(0.01) GRANULARITY 4; לאחר מימוש (bloom_filter), שאילתה כמו ALTER TABLE events ADD INDEX idx_user_id user_id TYPE bloom_filter(0.01) GRANULARITY 4; מדלגת על גרנולות שבהחלט חסר בהן אותו UUID. עם שיעור חיובי שגוי של 1% ו-1000 גרנולות, רק 10-50 נקראות בפועל במקום כולן.
WHERE user_id = '...' עובד על טוקנים של מחרוזות—שימושי לחיפושי תת-מחרוזת ביומנים ללא tokenbf_v1.
| סוג אינדקס | מתי להשתמש | קרדינליות משוערת |
|---|---|---|
| minmax | טווחים (תאריך, טמפרטורה) | נמוכה–בינונית |
| set | ערכים מדויקים (סטטוס, קטגוריה) | עד עשרות אלפים |
| bloom_filter | חיפושי UUID, hash | כל שהיא |
| tokenbf_v1 | חיפוש תת-מחרוזת בטקסט | כל שהיא |
מהן תצוגות מהותיות ומתי להשתמש בהן?
אם אותה שאילתת צבירה רצה לעתים קרובות—העבירו את הצבירה לזמן ההכנסה. תצוגות מהותיות ב-ClickHouse הן טריגרים על INSERT שמנתבים נתונים לטבלת צבירה יעד.
CREATE MATERIALIZED VIEW events_hourly_mv TO events_hourly AS
SELECT
toStartOfHour(ts) AS hour,
event_type,
project_id,
count() AS cnt,
uniqHLL12(user_id) AS uniq_users
FROM events
GROUP BY
hour,
event_type,
project_id;טבלת היעד משתמשת ב-LIKE '%pattern%' עם CREATE MATERIALIZED VIEW events_hourly_mv TO events_hourly AS SELECT toStartOfHour(ts) AS hour, event_type, project_id, count() AS cnt, uniqHLL12(user_id) AS uniq_users FROM events GROUP BY hour, event_type, project_id; :
CREATE TABLE events_hourly (
hour DateTime,
event_type LowCardinality(String),
project_id UInt32,
cnt AggregateFunction(count),
uniq_users AggregateFunction(uniqHLL12, UUID)
) ENGINE = AggregatingMergeTree()
ORDER BY (hour, event_type, project_id);שאילתות על נתונים מצטברים רצות פי 10-100 מהר יותר מאשר על נתונים גולמיים. אבל שימו לב: תצוגות מהותיות מתעדכנות רק בהכנסות חדשות. יש להוסיף נתונים היסטוריים ידנית דרך AggregatingMergeTree.
איך לנתח שאילתות?
לפני ביצוע שינויים, בדקו את AggregateFunction:
SELECT query_id, query, read_rows, read_bytes, result_rows, elapsed, memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
AND query_start_time >= now() - INTERVAL 1 HOUR
ORDER BY elapsed DESC
LIMIT 20;CREATE TABLE events_hourly ( hour DateTime, event_type LowCardinality(String), project_id UInt32, cnt AggregateFunction(count), uniq_users AggregateFunction(uniqHLL12, UUID) ) ENGINE = AggregatingMergeTree() ORDER BY (hour, event_type, project_id); נותן את יחס הקריאה היתרה. אם אתם קוראים 100 GB עבור תוצאה של 1 MB—האינדקס לא עובד או חסר.
INSERT INTO ... SELECT מראה את מצב המחיצות ומספר החלקים. אם system.query_log עבור מחיצה אחת—MergeTree לא יכול לעמוד במיזוגים; בדקו את SELECT query_id, query, read_rows, read_bytes, result_rows, elapsed, memory_usage FROM system.query_log WHERE type = 'QueryFinish' AND query_start_time >= now() - INTERVAL 1 HOUR ORDER BY elapsed DESC LIMIT 20; ו-read_bytes.
איך אנחנו מייעלים את ClickHouse
אנחנו מתחילים בבדיקה: אוספים את 20 השאילתות הכבדות המובילות מ-result_bytes, בודקים סכמות טבלאות, ומנתחים באמצעות system.parts. זה חושף 80% מהבעיות.
אז אנחנו מתכננים: מציעים שינויים ב-ORDER BY/PARTITION BY, רשימת אינדקסי דילוג, ותצוגות מהותיות. אנחנו מסכימים עם הצוות שלכם—שינוי מפתח מיון דורש יצירת טבלה מחדש עם העברת נתונים.
יישום: יצירת טבלאות חדשות, הגדרת תצוגות מהותיות, העברת נתונים בקבוצות דרך count(*) > 300. בדיקות ביצועים מקבילות משוות background_pool_size לפני ואחרי על שאילתות זהות.
לאחר ההעברה, אנו מספקים תיעוד: איזה אינדקס לאילו שאילתות, איך לנטר max_bytes_to_merge_at_max_space_in_pool, מתי להריץ system.query_log.
מקרה בוחן: חנות מסחר אלקטרוני עם 5 מיליארד אירועים ביום
שאילתות על אירועים גולמיים בזמן אמת פיגרו אחרי לוחות המחוונים. הוספנו אינדקס דילוג bloom_filter על user_id, הגדרנו חלוקה חודשית למחיצות, ויצרנו תצוגה מהותית לצבירות שעתיות. זמן ביצוע שאילתה טיפוסי ירד מ-30 שניות ל-0.3 שניות, ועומס האשכול ירד פי 3.
לוחות זמנים והיקף משוערים
| היקף המשימה | נפח נתונים | לוח זמנים |
|---|---|---|
| אופטימיזציה של 1-2 טבלאות | עד 1 TB | 1-2 ימים |
| בדיקת סכמה מלאה + תצוגות מהותיות | 1-10 TB | 2-3 ימים |
| העברה עם יצירת טבלה מחדש | 10+ TB | 3-5 ימים |
התמחור נקבע באופן פרטני לאחר בדיקת תשתית וניתוח סכמת נתונים. חיסכון טיפוסי בעלויות תשתית מהפחתת עומס יכול להגיע ל-40% במופעי ענן.
מה כלול
- ניתוח סכמה ופרופיל 20 השאילתות הכבדות המובילות (EXPLAIN, system.query_log)
- עיצוב חלוקה למחיצות, מפתח ראשי ואינדקסי דילוג
- יצירה והגדרה של תצוגות מהותיות
- העברת נתונים עם זמן השבתה מינימלי (העברה מתגלגלת)
- תיעוד תפעולי: המלצות ניטור, שאילתות טיפוסיות, מדריך פעולות
- הדרכת צוות: איך לעבוד עם הסכמה החדשה, איך להוסיף שדות חדשים
הערבויות שלנו: כל השינויים נבדקים על עותק נתונים, התוצאות נמדדות במספרים. אנחנו לא רק משנים את הסכמה—אנחנו מוכיחים האצה בשאילתות שלכם. השאירו בקשה לבדיקה—ניצור קשר תוך יום.







