ClickHouse十大最佳实践技巧

2026-05-26 1 阅读 ClickHouse
ClickHouse 是一款开源的列式数据库管理系统,专为对海量数据集进行实时分析查询而设计。它擅长在数毫秒内聚合数十亿行数据,使其成为分析平台、可观测性系统、实时仪表盘和数据仓库的流行选择。ClickHouse 通过其列式存储格式、高效压缩和向量化查询执行来实现这一目标,但要获得最佳性能,需要理解如何与其架构协同工作。 尽管 ClickHouse 具备卓越的开箱即用性能,但设计不佳的 Schema、低效的查询或次优的配置都可能浪费大量的性能潜力。一个原本能在数毫秒内返回结果的表,可能需要数秒。能实现 50 倍压缩的存储,可能最终只能达到 10 倍。这种性能差距往往源于未能理解 ClickHouse 如何存储、压缩和查询数据,并应用正确的技术来使您的用例与其优势相匹配。 无论您是每天插入数十亿事件、运行复杂的分析查询,还是努力降低存储成本,正确的优化都能显著提升性能和效率。对数据类型、表引擎或排序键的微小调整,都可能带来数量级的改进。 在本文中,我将分享 10 项最佳实践,这些是我作为 ClickHouse 解决方案架构师,在日常与客户紧密合作中发现能够带来最大影响的。它们并非纸上谈兵的理论建议,而是我在各种规模的部署中反复运用并取得成效的模式,涵盖了从 Schema 设计、数据建模到查询优化和监控等多个主题。 将 ClickHouse 与 AI 代理一同使用? 如果您正通过 AI 代理或大型语言模型 (LLM) 应用程序查询 ClickHouse,请查阅 ClickHouse best practices for AI agents,获取针对该用例的专属指南( https://clickhouse.com/blog/introducing-clickhouse-agent-skills ")。 1. 选择合适的主键和排序键 在 ClickHouse 中,表定义中的 ORDER BY 子句是您做出的最关键决策之一。它决定了数据在存储中的物理排序方式,这直接影响查询通过主索引剪枝 (primary index pruning) 跳过无关数据的效率。同时,它还会影响压缩效率,因为排序后的数据中相邻行通常共享相似值,从而能实现更高的压缩比。 ClickHouse 写入数据时,会根据您指定的 ORDER BY 列对行进行排序,并在内存中存储每个数据颗粒(granule,默认为 8,192 行)的首个值。在查询时,对这些列应用的过滤器可让 ClickHouse 跳过那些无法包含匹配数据的整个数据颗粒。 关键在于使您的 ORDER BY 顺序与最常见的查询模式保持一致。优先放置像 tenant_id、region 或 category 这样的低基数(low-cardinality)列,随后是基于时间的列。应避免以 UUIDs 或 timestamp 等高基数(high-cardinality)字段开头,因为它们几乎无法提供裁剪优化效果。 让我们以包含逾 1.5 亿行数据的 Amazon reviews dataset " 为例。假设一个表默认按 (marketplace, customer_id, review_date) 排序,执行以下查询: SELECT product_category, toStartOfMonth(review_date) AS month, count() AS review_count, avg(star_rating) AS avg_rating FROM amazon_reviews WHERE product_category = 'Electronics' AND toYear(review_date) = 1999 GROUP BY product_category, month ORDER BY month; 它会执行一次全表扫描,遍历全部 1.5 亿行数据以查找极小部分数据。如果我们更改表的 ORDER BY 顺序为 (product_category, review_date),我们的查询将基于这些列进行过滤,从而使相同的查询运行速度 提升 3 倍,同时所需扫描的数据量 减少 347 倍。对于相同的查询和数据集,如果 ORDER BY 顺序与查询模式匹配,便能带来显著的不同。 2. 使用高效数据类型 ClickHouse 中的数据类型不仅仅关乎正确性,它们直接影响存储大小、压缩比和查询速度。选择适合数据的最小类型,除非 NULL 值确实具有实际意义否则避免使用 Nullable (可为空) 类型,对于低基数文本列使用 LowCardinality(String) (低基数字符串) 类型,以及对于固定值集合优先选择 Enum (枚举) 类型而非自由文本字符串,可以显著提升性能和存储效率。同样的逻辑也适用于整数类型,当数据范围允许时,使用 UInt8 或 UInt32 代替 UInt64 意味着每次查询需要读取、解压缩和处理的数据量更少。 被标记为 Nullable (可为空) 的列要求 ClickHouse 额外存储一个 UInt8 列来追踪 NULL 值,这会增加存储和查询执行的双重开销。因此,除非 NULL 值确实具有实际意义,否则最好避免使用 Nullable 类型。在大多数情况下,一个合理的默认值可以作为有效的替代方案:文本字段使用空字符串、数值计数使用 0、或者对于 0 是有效条目的 ID 字段使用像 -1 这样的哨兵值。对于值集有限的字符串列,LowCardinality(String) (低基数字符串) 类型在底层使用字典编码,这使得它在列中不同值少于约 10,000 个时效率更高。 让我们继续以拥有 1.5 亿行数据的 Amazon 评论数据集为例。一个设计不佳的表,其中许多列是 Nullable (可为空) 类型、数值字段过大、低基数文本列使用普通 String 类型,会占用 30.16 GB 存储空间。通过优化,即通过删除 Nullable (可为空) 类型、调整数值列大小、并在适当位置应用 LowCardinality(String) (低基数字符串) 类型,将其切换到更合适的数据类型后,存储空间可降至 26.8 GB。但其价值不仅仅体现在存储方面,它对性能也有显著提升,如下例所示,查询速度提高了 2 倍。 3. 考虑分区策略,或避免使用分区 ClickHouse 中的分区 (partitioning) 是最容易被误解的特性之一,最常见的错误是将其用作性能优化手段。ClickHouse 中的分区主要是一种数据管理特性,而非通用的性能加速器。ClickHouse 通过主索引剪枝 (primary index pruning) 在数据跳过方面已经极其快速。在此基础上再进行分区,很少有帮助,反而常常适得其反。原因是 ClickHouse 需要大的数据块 (parts)(通常高达 150GB,常含数十亿行数据)才能高效地进行压缩和查询,并且数据块 (parts) 永远不会跨分区边界合并。过度分区,例如按天或按高基数 (high-cardinality) 列(如 tenant_id)进行,常常导致大量小数据块 (parts)、合并速度变慢、内存