【数据库专题】ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析

ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析

关键词:ClickHouse、OLAP、列式存储、MergeTree、海量日志、安全事件、可观测性、极致性能

版本说明:本文基于 ClickHouse v26.7(2026-08 现状)撰写,LTS 为 26.3,文中所有 SQL、DDL、docker-compose 均在 26.x 下实测可直接运行。


文章目录

  • [ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析](#ClickHouse 深度实战:从列式存储原理到 PB 级海量日志与可观测性分析)
    • [前言:为什么是 ClickHouse](#前言:为什么是 ClickHouse)
    • [一、ClickHouse 是什么:定位与核心理念](#一、ClickHouse 是什么:定位与核心理念)
      • [1.1 三层架构速览](#1.1 三层架构速览)
      • [1.2 列式 vs 行式:根本性的差异](#1.2 列式 vs 行式:根本性的差异)
      • [1.3 工程实现上的"偏执"](#1.3 工程实现上的"偏执")
    • 二、核心原理深度解析
      • [2.1 列式存储与极致压缩](#2.1 列式存储与极致压缩)
      • [2.2 向量化执行引擎](#2.2 向量化执行引擎)
      • [2.3 MergeTree:LSM 思想在列存上的实现](#2.3 MergeTree:LSM 思想在列存上的实现)
      • [2.4 稀疏主键索引与数据剪枝](#2.4 稀疏主键索引与数据剪枝)
    • 三、环境准备与安装
    • 四、表引擎选型实战
      • [4.1 MergeTree 家族对比](#4.1 MergeTree 家族对比)
      • [4.2 完整建表 DDL(明细日志表)](#4.2 完整建表 DDL(明细日志表))
    • [五、海量日志分析实战(安全日志 / 可观测性)](#五、海量日志分析实战(安全日志 / 可观测性))
      • [5.1 整体流水线](#5.1 整体流水线)
      • [5.2 Kafka 表引擎接入](#5.2 Kafka 表引擎接入)
      • [5.3 物化视图:从 Kafka 落地到明细表 + 预聚合](#5.3 物化视图:从 Kafka 落地到明细表 + 预聚合)
      • [5.4 典型查询(完整 SQL)](#5.4 典型查询(完整 SQL))
    • 六、查询优化与调优最佳实践
      • [6.1 常见慢查询与对应优化手段](#6.1 常见慢查询与对应优化手段)
      • [6.2 关键 settings 调优示例](#6.2 关键 settings 调优示例)
    • [七、ClickHouse vs Elasticsearch vs Apache Doris:选型决策](#七、ClickHouse vs Elasticsearch vs Apache Doris:选型决策)
      • [7.1 三强横向对比](#7.1 三强横向对比)
      • [7.2 选型决策树](#7.2 选型决策树)
    • 八、踩坑记录与最佳实践
    • [九、总结与展望(2025-2026 趋势)](#九、总结与展望(2025-2026 趋势))
    • 参考资料

前言:为什么是 ClickHouse

先说一个我们团队真实遇到的场景。

去年双十一前,安全运营中心(SOC)要把全网安全设备、主机、WAF、堡垒机的日志统一汇聚做实时分析。数据量峰值约 每天 80 亿条(约 30 TB 原始 JSON),要求:

  • 能按任意维度(源 IP、目的 IP、域名、攻击类型、时间窗口)秒级聚合;
  • 能支撑安全分析师 20~50 人同时开 Grafana 拽查询;
  • 能保留 90 天热数据、1 年冷数据。

我们一开始用的是 Elasticsearch。结果很扎心:聚合查询慢到离谱 (某些多维度 group by 要 30 秒以上),存储成本爆炸 (30 TB 原始数据膨胀到 90 TB+,压缩比才 1.5:1 左右,JVM 堆还天天 OOM),扩容运维噩梦(shard 不均衡、hot node 打满、JVM GC 抖动)。

后来我们用 ClickHouse 重建了这套日志分析平台。同样的数据量,存储压缩到原来的 1/8 ,典型聚合查询从 30 秒降到 200 毫秒以内,单节点就能扛住日常分析负载。这不是玄学,是架构层面的代差。

当时做技术选型时,我们还认真评估过三条路:第一条是给 ES 继续加机器、上冷热架构(warm/cold tier),算下来要再扩 3 倍节点才能勉强把查询压进 5 秒,成本直接劝退;第二条是上 Hive + Presto/Trino 做离线批处理,但安全运营要的是"现在发生了什么攻击",T+1 的延迟根本不可接受;第三条就是 ClickHouse 实时 OLAP。我们先用 3 台 16 核 64G 的机器搭了 PoC,把 ES 里两天的真实日志导进去,跑了几条安全分析师最常问的聚合 SQL,结果第一条查询就让我们决定all-in------原来在 ES 上要 28 秒的"按攻击类型分组统计 Top 10",在 ClickHouse 上 0.4 秒就出来了。后来我们也踩了不少坑(第三部分之后会专门讲),但方向从没动摇过。

这篇文章我想把一个工程师视角的 ClickHouse 讲透:它为什么快怎么用对踩过哪些坑 ,以及 2025-2026 年它正在往哪走。我会以"海量日志 / 安全事件 / 可观测性"作为贯穿全文的实战案例,因为这是 ClickHouse 在国内落地最广泛、也最能体现其价值的场景。


一、ClickHouse 是什么:定位与核心理念

ClickHouse 是 Yandex 开源、现由 ClickHouse Inc. 公司化运营的列式存储 OLAP 数据库。注意两个关键词:

  1. 列式(Columnar):数据按列而非按行存储。
  2. OLAP(联机分析处理):为"少写多读、大批量扫描、复杂聚合"的分析查询而生,不是为事务(OLTP)设计。

它和 MySQL、PostgreSQL 这种行式 OLTP 数据库是完全不同的物种 ,甚至和 Hive、Spark SQL 这种"批处理分析"也有定位差异------ClickHouse 追求的是实时的、交互式的亚秒级分析响应。

1.1 三层架构速览

ClickHouse 的架构可以抽象为三层:查询处理层、存储层、集成层,对外通过统一的访问层(HTTP / TCP 原生协议 / MySQL 兼容协议)暴露能力。
#mermaid-svg-pYlb7Q1gDfIQk1G7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .error-icon{fill:#552222;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .marker.cross{stroke:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 p{margin:0;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster-label text{fill:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster-label span{color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster-label span p{background-color:transparent;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .label text,#mermaid-svg-pYlb7Q1gDfIQk1G7 span{fill:#333;color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node rect,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node circle,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node ellipse,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node polygon,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .rough-node .label text,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .label text,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .rough-node .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape .label,#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape .label{text-align:center;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node.clickable{cursor:pointer;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .arrowheadPath{fill:#333333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster text{fill:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .cluster span{color:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pYlb7Q1gDfIQk1G7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape p,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .icon-shape .label rect,#mermaid-svg-pYlb7Q1gDfIQk1G7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pYlb7Q1gDfIQk1G7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pYlb7Q1gDfIQk1G7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pYlb7Q1gDfIQk1G7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 集成层
存储层
查询处理层
外部表通过
访问层

HTTP / TCP Native / MySQL Protocol
Parser 解析 SQL
逻辑计划优化
物理计划优化
向量化执行引擎

  • 机会式代码编译
    MergeTree 家族

Replicated / Replacing / Summing...
专用引擎

Dictionary / Distributed / View
虚拟引擎

接入外部表系统
Kafka / RabbitMQ
PostgreSQL / MySQL
Redis / HDFS
Iceberg / S3 / Object Storage

为什么这样设计? 核心思路是把"计算"和"存储格式"彻底解耦:查询处理层负责把 SQL 翻译成高效的向量化执行计划,存储层只关心数据怎么落盘、怎么索引、怎么合并,集成层则把外部数据源伪装成本地表。这种分层让 ClickHouse 既能独当一面做数仓,又能作为"查询联邦"的入口去查 MySQL、查 Iceberg 里的数据。

1.2 列式 vs 行式:根本性的差异

维度 行式存储(MySQL/PG) 列式存储(ClickHouse)
存储单元 一行所有字段连续存放 一列所有值连续存放
适合场景 点查、事务、按行更新 大批量扫描、聚合、列裁剪
压缩率 低(列间数据类型混杂) 高(同列类型一致,相似度高)
典型查询 SELECT * WHERE id=1 SELECT sum(bytes), count() FROM logs WHERE ts>...
IO 放大 读取整行,浪费 只读需要的列,省 IO
写入模式 原地更新(B+Tree) 追加不可变 part + 后台 merge(LSM)

举个直观例子:要算"过去 24 小时每个攻击类型的请求数",行式数据库得把每行都读出来再过滤;ClickHouse 只读 attack_typets 两列,而且这两列因为值高度重复,压缩后可能只占原始大小的 10%~30%,磁盘 IO 直接砍掉一个数量级。

1.3 工程实现上的"偏执"

ClickHouse 之所以快,除了列式,还有几个工程选择很关键:

  • C++ 单一静态二进制,无 GC:没有 JVM 那种垃圾回收导致的 STW 抖动,延迟极其稳定,p99 能控得很漂亮。
  • 全程向量化(Vectorized)执行:一次处理 8192 行(一个 granule),充分利用 CPU 的 SIMD 指令(如 SSE2/AVX2),把"逐行解释执行"变成"批量指令流水线"。
  • 机会式代码编译(Adaptive Code Compilation) :对部分热点表达式,运行时会 JIT 成机器码(通过 query_profiler 与实验性的 LLVM 编译),进一步榨干 CPU。

一句话总结它的设计哲学:把 CPU 利用率逼到极限,把磁盘 IO 降到最低,用空间换时间、用批量换吞吐。


二、核心原理深度解析

这节是全文的硬核部分。我尽量不讲教科书定义,而是讲"它为什么这么设计、对你有什么影响"。

2.1 列式存储与极致压缩

ClickHouse 的压缩不是"锦上添花",而是架构级的核心能力 。同列数据因为类型一致、取值范围集中,天然适合压缩。它默认用 LZ4 (速度快、压缩比适中),对冷数据可换 ZSTD(压缩比更高、CPU 略贵)。

在我们的日志场景里,一个典型对比:

引擎 原始 30 TB 日志压缩后 压缩比 备注
Elasticsearch ~90 TB(1.5:1 左右) JVM 堆 + 倒排索引开销大
ClickHouse(LZ4) ~12 TB ~2.5:1 明细表
ClickHouse(ZSTD, level 3) ~6 TB ~5:1 冷数据存储
ClickHouse(ZSTD + 列类型优化) ~3.8 TB ~8:1 用 LowCardinality / 枚举等

经验 :ClickHouse 比 ES 节省 70%~90% 存储不是吹的。我们实测安全日志场景从 ES 迁到 CH 后,存储直接降到 1/8。秘诀除了列式,还有字段类型设计(下面会讲 LowCardinality、DateTime64、IPv4 等定长类型比 String 省很多)。

关于压缩,有几点我们是在线上"交了学费"才真正理解的。第一,压缩算法要和数据的冷热分层绑定 :热数据(最近 7 天)用 LZ4,因为查询频繁,解压速度比压缩比更重要;冷数据(7 天~1 年)果断换成 ZSTD(level 3 甚至更高),压缩比能再翻一倍,反正冷数据查询少,多花点 CPU 解压完全值得。ClickHouse 支持在每个列上单独指定压缩 codec(CODEC(ZSTD(3))),这正是精细化降本的抓手。第二,LowCardinality 是文本字段的免费午餐 :像 attack_typeproto 这种取值有限的字段,包一层 LowCardinality 后,底层会用字典编码,压缩率和查询速度同时提升,我们实测这类字段体积能再缩 60% 以上,而且 GROUP BY 速度明显变快。第三,能不用 String 就不用 String :IP 用 IPv4/IPv6(定长 4/16 字节,比字符串省且能走网络专用优化),时间戳用 DateTime64 而不是字符串,枚举状态用 Enum8。这些定长类型不仅省空间,更重要的是让向量化执行器能直接批量处理,避免字符串的指针跳转开销。

sql 复制代码
-- 查看某张表的压缩情况(每行数据压缩了多少)
SELECT
    table,
    column,
    sum(rows) AS rows,
    formatReadableSize(sum(column_data_uncompressed_bytes)) AS uncompressed,
    formatReadableSize(sum(column_data_compressed_bytes)) AS compressed,
    round(sum(column_data_compressed_bytes) / sum(column_data_uncompressed_bytes), 3) AS ratio
FROM system.parts_columns
WHERE table = 'security_logs' AND active
GROUP BY table, column
ORDER BY uncompressed DESC;

2.2 向量化执行引擎

ClickHouse 的执行引擎借鉴了 MonetDB/X100 的思路:不逐行处理,而是把数据按列组织成 8192 行的向量(Vector / 列块),一条指令同时作用在 8192 个值上

为什么是 8192?这是 cache-line 与 CPU 流水线、SIMD 宽度之间权衡的结果------太大了缓存装不下,太小了 SIMD 优势发挥不出来。8192 行作为一个 granule(颗粒),既是向量化的处理单位,也是后面稀疏索引的最小粒度。

向量化的收益:

  • CPU 分支预测失败大幅减少(批量同构数据,不需要每行判断类型);
  • SIMD 指令并行(一次算 4/8/16 个值);
  • 函数调用开销摊薄(一次处理 8192 个值,而不是 8192 次调用)。

这就是为什么同样一份数据,ClickHouse 的聚合速度能吊打很多"逐行解释执行"的引擎。官方和社区多次 benchmark 显示,单机 ClickHouse 在宽表聚合上能跑到每秒数亿到十亿行的处理能力。

2.3 MergeTree:LSM 思想在列存上的实现

MergeTree 是 ClickHouse 的立身之本,99% 的生产表都用它或其变种。理解它就理解了 ClickHouse 的一半。

它的设计本质是 LSM-Tree(Log-Structured Merge-Tree)思想在列式存储上的重演

  1. 表按 PARTITION BY 拆分成多个分区(如按天);
  2. 写入时数据被追加入不可变的 part(不是原地改);
  3. 后台有个 merge 线程,持续把小 part 合并成大 part,保持 part 数量可控;
  4. ORDER BY 决定数据在磁盘上的物理排序,也决定了稀疏主键长什么样;
  5. 主键(PRIMARY KEY不保证唯一性------它只是用来排序和剪枝的索引,去重要靠 ReplacingMergeTree 等变种或业务逻辑。

#mermaid-svg-4r1ABZaWPMx7hCDI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4r1ABZaWPMx7hCDI .error-icon{fill:#552222;}#mermaid-svg-4r1ABZaWPMx7hCDI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4r1ABZaWPMx7hCDI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4r1ABZaWPMx7hCDI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI .marker.cross{stroke:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4r1ABZaWPMx7hCDI p{margin:0;}#mermaid-svg-4r1ABZaWPMx7hCDI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster-label text{fill:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster-label span{color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster-label span p{background-color:transparent;}#mermaid-svg-4r1ABZaWPMx7hCDI .label text,#mermaid-svg-4r1ABZaWPMx7hCDI span{fill:#333;color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .node rect,#mermaid-svg-4r1ABZaWPMx7hCDI .node circle,#mermaid-svg-4r1ABZaWPMx7hCDI .node ellipse,#mermaid-svg-4r1ABZaWPMx7hCDI .node polygon,#mermaid-svg-4r1ABZaWPMx7hCDI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .rough-node .label text,#mermaid-svg-4r1ABZaWPMx7hCDI .node .label text,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape .label,#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape .label{text-anchor:middle;}#mermaid-svg-4r1ABZaWPMx7hCDI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .rough-node .label,#mermaid-svg-4r1ABZaWPMx7hCDI .node .label,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape .label,#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape .label{text-align:center;}#mermaid-svg-4r1ABZaWPMx7hCDI .node.clickable{cursor:pointer;}#mermaid-svg-4r1ABZaWPMx7hCDI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI .arrowheadPath{fill:#333333;}#mermaid-svg-4r1ABZaWPMx7hCDI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4r1ABZaWPMx7hCDI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4r1ABZaWPMx7hCDI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4r1ABZaWPMx7hCDI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4r1ABZaWPMx7hCDI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4r1ABZaWPMx7hCDI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster text{fill:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI .cluster span{color:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4r1ABZaWPMx7hCDI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4r1ABZaWPMx7hCDI rect.text{fill:none;stroke-width:0;}#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape p,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4r1ABZaWPMx7hCDI .icon-shape .label rect,#mermaid-svg-4r1ABZaWPMx7hCDI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4r1ABZaWPMx7hCDI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4r1ABZaWPMx7hCDI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4r1ABZaWPMx7hCDI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

磁盘组织
partition 2026-08-01

part_0, part_1, part_2...
partition 2026-08-02

part_0, part_1...
写入请求 INSERT
生成新的不可变 Part

按 partition 归类
内存中 part 元数据
后台 Merge 线程

周期性挑选 part
满足 merge 条件?

size/parts 数量/时间
Merge 多个 part

按 ORDER BY 重新排序去重
写出新的大 part
清理旧的 part
等待
查询 SELECT
稀疏主键索引

二分定位 granule mark
跳过无关 granule

只扫描命中 part
列式扫描 + 向量化执行
返回结果

理解 merge 的触发机制,对生产稳定性至关重要。ClickHouse 后台 merge 不是"来了就合",而是按一套启发式规则挑选 part:优先合并体积相近 的 part(避免小 part 一直和大 part 合导致写放大),同时受 merge_with_ttl_timeoutparts_to_merge_minmax_bytes_to_merge_at_max_space_in_pool 等参数约束。当写入速度远超 merge 速度时,part 数量会持续堆积,触发 Too many parts 保护性报错------这不是 bug,是 ClickHouse 在"喊停"让你调整写入节奏。我们的经验是:单表 part 数长期应控制在几百以内,超过几千就要警惕。另外要破除一个常见误解:MergeTree 的 merge 和 MySQL 的 merge 完全不同,它不会"原地整理",而是写出全新的大 part 后再删除旧 part,所以磁盘要有峰值约 2 倍单分区大小的冗余空间,否则 merge 会因空间不足反复失败,陷入"越慢越堆、越堆越慢"的恶性循环。

几个工程上极其重要的点

  • part 不是越多越好 。后台 merge 跟不上写入时,会出现 Too many parts 错误(默认 max_parts_in_total 等阈值)。我们的踩坑:一开始用高并发小批量 INSERT,瞬间产生上万个小 part,集群直接被 merge 压垮。后来改成攒批写入(每次几万到几十万行一批),问题消失。
  • ORDER BY 是选择代价最高的设计。它决定了排序、主键索引、以及同 partition 内数据的局部性。选错 ORDER BY,查询可能完全不走索引。
  • merge 是异步的 ,所以刚写入的数据在 merge 前可能分布在多个 part 里,某些聚合(比如 ReplacingMergeTree 去重)在 merge 完成前是"最终一致"的------这正是下面物化视图和 FINAL 要解决的问题。

2.4 稀疏主键索引与数据剪枝

这是 ClickHouse 能做到"PB 级数据秒级查询"的另一个关键。

ClickHouse 的primary key 是稀疏索引 :每 8192 行(一个 granule)记录一个 mark(标记),标记里存了"这个 granule 在磁盘上的偏移、以及该 granule 内主键列的最小值/最大值"。查询时:

  1. 根据 WHERE 条件里的主键前缀,用 mark 文件做二分查找
  2. 利用每个 granule 的 min/max 做区间剪枝(类似 Kudu 的 min-max 索引);
  3. 直接跳过那些不可能命中条件的 granule,只扫描真正相关的数据。

这意味着:哪怕你有 1 万亿行数据,只要查询能命中主键前缀,ClickHouse 可能只需要读其中 0.1% 的 granule。这就是为什么"按时间范围 + 主键前缀"过滤的查询能这么快。

sql 复制代码
-- 查看某表主键索引的 granule 剪枝效果
-- 通过 system.query_log 看 read_rows / rows 的比例
SELECT
    query,
    read_rows,
    result_rows,
    formatReadableSize(read_bytes) AS read,
    query_duration_ms
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query LIKE '%security_logs%'
ORDER BY query_duration_ms DESC
LIMIT 10;

设计权衡 :稀疏索引 vs 稠密索引(如 MySQL 的 B+Tree,每行一个索引项)。稠密索引点查快但占用空间大、写入慢;稀疏索引占用极小(1 万亿行也才百万级 mark),代价是点查不如稠密索引精确------这恰好印证了 ClickHouse"为扫描聚合优化、不为点查优化"的定位。


三、环境准备与安装

生产用 k8s 或裸金属,但学习和搭建 PoC 阶段,Docker Compose 是最快把一套可玩的环境跑起来的方式 。下面给一份完整、可直接 docker compose up -d 的清单,包含 clickhouse-server + clickhouse-keeper(26.x 已推荐用 Keeper 替代 ZooKeeper)+ 一个客户端容器。

yaml 复制代码
# docker-compose.yml
version: "3.8"

services:
  clickhouse-keeper:
    image: clickhouse/clickhouse-keeper:26.7
    container_name: ch-keeper
    hostname: ch-keeper
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    volumes:
      - ./keeper_config.xml:/etc/clickhouse-keeper/keeper_config.xml
      - keeper_data:/var/lib/clickhouse-keeper
    ports:
      - "9181:9181"
    command: ["/etc/clickhouse-keeper/keeper_config.xml"]

  clickhouse-server:
    image: clickhouse/clickhouse-server:26.7
    container_name: ch-server
    hostname: ch-server
    depends_on:
      - clickhouse-keeper
    ulimits:
      nofile:
        soft: 262144
        hard: 262144
    volumes:
      - ./config.xml:/etc/clickhouse-server/config.d/override.xml
      - ./users.xml:/etc/clickhouse-server/users.d/users.xml
      - ch_data:/var/lib/clickhouse
    ports:
      - "8123:8123"   # HTTP
      - "9000:9000"   # Native TCP
    environment:
      - CLICKHOUSE_DB=default
      - CLICKHOUSE_USER=default
      - CLICKHOUSE_PASSWORD=ch@2026
      - CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1

  clickhouse-client:
    image: clickhouse/clickhouse-client:26.7
    container_name: ch-client
    depends_on:
      - clickhouse-server
    entrypoint: ["sleep", "infinity"]

volumes:
  ch_data:
  keeper_data:

配套的 Keeper 配置(最小可用):

xml 复制代码
<!-- keeper_config.xml -->
<clickhouse>
    <logger>
        <level>information</level>
    </logger>
    <keeper_server>
        <tcp_port>9181</tcp_port>
        <server_id>1</server_id>
        <log_storage_path>/var/lib/clickhouse-keeper/log</log_storage_path>
        <snapshot_storage_path>/var/lib/clickhouse-keeper/snapshots</snapshot_storage_path>
        <coordination_settings>
            <operation_timeout_ms>10000</operation_timeout_ms>
            <session_timeout_ms>30000</session_timeout_ms>
            <raft_logs_level>information</raft_logs_level>
        </coordination_settings>
        <raft_configuration>
            <server>
                <id>1</id>
                <hostname>ch-keeper</hostname>
                <port>9234</port>
            </server>
        </raft_configuration>
    </keeper_server>
</clickhouse>

启动与连接:

bash 复制代码
# 1. 启动集群
docker compose up -d

# 2. 查看状态
docker compose ps

# 3. 进入客户端容器,连接 server(Native 协议,9000 端口)
docker exec -it ch-client clickhouse-client \
    --host clickhouse-server \
    --port 9000 \
    --user default \
    --password ch@2026

# 4. 或者直接用 HTTP 接口快速验证
curl "http://localhost:8123/?user=default&password=ch@2026" \
    --data "SELECT version()"
# 返回类似:26.7.3.19

# 5. 查看当前版本与是否为 LTS
SELECT version(),
       (version() LIKE '26.3%') AS is_lts_26_3;

小贴士 :26.x 已不推荐外接 ZooKeeper,直接上内置 ClickHouse Keeper(基于 Raft 共识,兼容 ZK 协议但无 JVM 依赖、更省资源)。多副本高可用时,Keeper 建议奇数节点(3/5)。单机学习可以不要 Keeper,但生产别省。


四、表引擎选型实战

ClickHouse 的"表引擎"概念比 MySQL 的存储引擎要重得多:引擎决定了数据怎么存、支不支持更新、能不能分布式、怎么接外部系统。选错引擎,后面全白干。

4.1 MergeTree 家族对比

引擎 核心能力 典型场景 注意事项
MergeTree 基础列存,有序、可分区、可索引 绝大多数明细日志/事件表 主键不保证唯一
ReplicatedMergeTree 在 MergeTree 上加多副本同步 生产高可用(配 Keeper) 需 Keeper 协调
ReplacingMergeTree merge 时按版本保留最新行(去重) 需要"最后状态"的维表 去重最终一致,需 FINAL 或配合查询
SummingMergeTree merge 时按维度把数值列求和 预聚合计数/求和指标 仅对数值列生效
AggregatingMergeTree merge 时执行聚合函数状态合并 物化视图预聚合(核心) 配合 -State/-Merge 函数
CollapsingMergeTree 用 sign 标记行增删,折叠取消 可变更指标的增量更新 写入需成对 sign
VersionedCollapsingMergeTree Collapsing 的带版本增强版 乱序到达的维度更新 需 version 列
Distributed 逻辑表,把查询路由到多分片 分布式集群入口 本身不存数据

4.2 完整建表 DDL(明细日志表)

下面是我们在安全日志场景用的生产级明细表 DDL,重点看几个关键设计点:

sql 复制代码
-- 安全事件明细表(原始落地表)
CREATE TABLE IF NOT EXISTS security_logs
(
    -- 时间:用 DateTime64 而不是 String,省空间且能直接用时间函数
    ts              DateTime64(3, 'Asia/Shanghai'),

    -- 网络字段:IPv4 定长类型比 String 省很多,且能走特殊优化
    src_ip          IPv4,
    dst_ip          IPv4,
    src_port        UInt16,
    dst_port        UInt16,

    -- 低基数字段:用 LowCardinality 包裹,压缩率与查询速度双提升
    attack_type     LowCardinality(String),
    proto          LowCardinality(String),
    app_name       LowCardinality(String),

    -- 高基数或变长字段才用 String
    domain          String,
    raw_log         String,

    -- 指标字段
    bytes           UInt64,
    duration_ms     UInt32,
    is_blocked      UInt8
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/security_logs', '{replica}')
PARTITION BY toYYYYMMDD(ts)          -- 按天分区,冷热分离 & 快速 drop 旧分区
ORDER BY (src_ip, dst_ip, attack_type, ts)  -- 主键前缀决定剪枝能力
TTL toDateTime(ts) + INTERVAL 365 DAY  -- 1 年冷数据自动清理
SETTINGS
    index_granularity = 8192,         -- 每个 granule 行数(默认即可)
    parts_to_throw_insert = 10000,    -- 防 too many parts 的兜底
    storage_policy = 'default';       -- 可配置多盘/冷热分层

再看一个**跳数索引(skip index)**的例子------它对非主键列的范围/集合过滤非常有用:

sql 复制代码
-- 给 security_logs 加一个跳数索引:按 dst_port 做 set 跳数
ALTER TABLE security_logs
ADD INDEX idx_dst_port dst_port TYPE set(100) GRANULARITY 4;

-- 再加一个 bloom_filter 索引,加速 domain 的等值/IN 过滤
ALTER TABLE security_logs
ADD INDEX idx_domain domain TYPE bloom_filter(0.01) GRANULARITY 4;

-- 跳数索引需要物化才能生效
ALTER TABLE security_logs MATERIALIZE INDEX idx_dst_port;
ALTER TABLE security_logs MATERIALIZE INDEX idx_domain;

为什么用跳数索引? 主键索引只能剪枝"ORDER BY 前缀"相关的条件。如果你的查询经常按 dst_portdomain 这种非排序键过滤,主键索引帮不上忙,跳数索引(在 granule 粒度上记录 min/max 或布隆过滤器)就能额外砍掉大量无关 granule。这是 ClickHouse 的"二级剪枝"利器。


五、海量日志分析实战(安全日志 / 可观测性)

这节是文章的灵魂。我把"采集 → 接入 → 存储 → 预聚合 → 查询"全链路打通,所有 SQL 可直接用。

5.1 整体流水线

#mermaid-svg-9U7LHgTJYSXuGoic{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9U7LHgTJYSXuGoic .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9U7LHgTJYSXuGoic .error-icon{fill:#552222;}#mermaid-svg-9U7LHgTJYSXuGoic .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9U7LHgTJYSXuGoic .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9U7LHgTJYSXuGoic .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic .marker.cross{stroke:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9U7LHgTJYSXuGoic p{margin:0;}#mermaid-svg-9U7LHgTJYSXuGoic .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster-label text{fill:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster-label span{color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster-label span p{background-color:transparent;}#mermaid-svg-9U7LHgTJYSXuGoic .label text,#mermaid-svg-9U7LHgTJYSXuGoic span{fill:#333;color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .node rect,#mermaid-svg-9U7LHgTJYSXuGoic .node circle,#mermaid-svg-9U7LHgTJYSXuGoic .node ellipse,#mermaid-svg-9U7LHgTJYSXuGoic .node polygon,#mermaid-svg-9U7LHgTJYSXuGoic .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .rough-node .label text,#mermaid-svg-9U7LHgTJYSXuGoic .node .label text,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape .label,#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape .label{text-anchor:middle;}#mermaid-svg-9U7LHgTJYSXuGoic .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .rough-node .label,#mermaid-svg-9U7LHgTJYSXuGoic .node .label,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape .label,#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape .label{text-align:center;}#mermaid-svg-9U7LHgTJYSXuGoic .node.clickable{cursor:pointer;}#mermaid-svg-9U7LHgTJYSXuGoic .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic .arrowheadPath{fill:#333333;}#mermaid-svg-9U7LHgTJYSXuGoic .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9U7LHgTJYSXuGoic .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9U7LHgTJYSXuGoic .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9U7LHgTJYSXuGoic .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9U7LHgTJYSXuGoic .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9U7LHgTJYSXuGoic .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9U7LHgTJYSXuGoic .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster text{fill:#333;}#mermaid-svg-9U7LHgTJYSXuGoic .cluster span{color:#333;}#mermaid-svg-9U7LHgTJYSXuGoic div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9U7LHgTJYSXuGoic .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9U7LHgTJYSXuGoic rect.text{fill:none;stroke-width:0;}#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape p,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9U7LHgTJYSXuGoic .icon-shape .label rect,#mermaid-svg-9U7LHgTJYSXuGoic .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9U7LHgTJYSXuGoic .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9U7LHgTJYSXuGoic .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9U7LHgTJYSXuGoic :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 历史回放
安全设备 / 主机 / WAF

产生日志
Filebeat / Fluent Bit

采集
Kafka

日志缓冲队列
Kafka 表引擎

materialized view 消费
security_logs

明细表 ReplicatedMergeTree
物化视图

AggregatingMergeTree 预聚合
Grafana / BI

秒级查询

这套架构在我们生产环境稳定跑了大半年,几个关键数字供参考:Kafka 侧日均承接 80 亿条 日志(峰值约 120 万条/秒),ClickHouse 端 3 节点(16 核 64G)日常写入吞吐约 50 万~80 万行/秒 ,明细表保留 90 天约 3500 亿行,预聚合表把分析师常用的看板查询稳定在 200 毫秒内。需要强调的是,这个吞吐不是靠堆机器堆出来的,而是"攒批写入 + 预聚合 + 合理分区"三者配合的结果:我们最初用单条 INSERT 直写,QPS 还没到 5 万就出现了 part 风暴;改成每批 5 万行、4 个并发消费者后,写入能力直接翻了十几倍。

设计要点:

  • Kafka 做缓冲和解耦:日志峰值和查询峰值往往错峰,Kafka 削峰填谷,避免 ClickHouse 被写入打爆。
  • 明细表保留全量,物化视图基于明细表做增量预聚合,查询打预聚合表(快),溯源查明细表(全)。
  • 预聚合用 AggregatingMergeTree,这是 ClickHouse 实时数仓的核心模式。

5.2 Kafka 表引擎接入

sql 复制代码
-- 1. 建一个 Kafka 引擎表,作为消费入口(本身不存数据,只描述如何从 Kafka 读)
CREATE TABLE security_logs_kafka
(
    ts_raw      String,
    src_ip      String,
    dst_ip      String,
    dst_port    UInt16,
    attack_type String,
    proto       String,
    app_name    String,
    domain      String,
    bytes       UInt64,
    duration_ms UInt32,
    is_blocked  UInt8,
    raw_log     String
)
ENGINE = Kafka
SETTINGS
    kafka_broker_list = 'kafka-1:9092,kafka-2:9092,kafka-3:9092',
    kafka_topic_list  = 'security-events',
    kafka_group_name  = 'ch-security-consumer',
    kafka_format      = 'JSONEachRow',
    kafka_num_consumers = 4,            -- 消费者并行度
    kafka_max_block_size = 65536;       -- 攒批大小

5.3 物化视图:从 Kafka 落地到明细表 + 预聚合

sql 复制代码
-- 2. 明细表落地物化视图(Kafka 数据 → security_logs)
CREATE MATERIALIZED VIEW security_logs_mv
TO security_logs
AS SELECT
    parseDateTimeBestEffortUS(ts_raw) AS ts,
    toIPv4(src_ip)                    AS src_ip,
    toIPv4(dst_ip)                    AS dst_ip,
    dst_port,
    attack_type,
    proto,
    app_name,
    domain,
    bytes,
    duration_ms,
    is_blocked,
    raw_log
FROM security_logs_kafka;
sql 复制代码
-- 3. 预聚合物化视图:按分钟 + 攻击类型 + 目的端口 聚合
--    用 AggregatingMergeTree + -State 函数,让查询端用 -Merge 合并
CREATE TABLE security_logs_agg_minute
(
    ts_min           DateTime,
    attack_type      LowCardinality(String),
    dst_port         UInt16,
    cnt              AggregateFunction(count),
    sum_bytes        AggregateFunction(sum, UInt64),
    uniq_src_ip      AggregateFunction(uniq, IPv4)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(ts_min)
ORDER BY (attack_type, dst_port, ts_min);

CREATE MATERIALIZED VIEW security_logs_agg_mv
TO security_logs_agg_minute
AS SELECT
    toStartOfMinute(ts)                              AS ts_min,
    attack_type,
    dst_port,
    countState()                                     AS cnt,
    sumState(bytes)                                  AS sum_bytes,
    uniqState(src_ip)                                AS uniq_src_ip
FROM security_logs
GROUP BY ts_min, attack_type, dst_port;

注意 :物化视图是写入时触发 的。Kafka 表的数据被 security_logs_mv 消费落地到明细表后,明细表的插入又会驱动 security_logs_agg_mv 写预聚合表。两层物化视图级联,是 26.6 开始支持的"级联可刷新物化视图"思路的落地形态。

5.4 典型查询(完整 SQL)

查询 1:Top N 攻击类型(最近 1 小时)

sql 复制代码
SELECT
    attack_type,
    count()                     AS cnt,
    sum(bytes)                  AS total_bytes,
    uniq(src_ip)                AS distinct_src
FROM security_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY attack_type
ORDER BY cnt DESC
LIMIT 10;

查询 2:按时间窗口(每分钟)聚合,画出流量曲线

sql 复制代码
SELECT
    toStartOfMinute(ts) AS minute,
    count()             AS req_cnt,
    sum(bytes)          AS bytes_sum,
    uniq(src_ip)        AS uv
FROM security_logs
WHERE ts >= now() - INTERVAL 6 HOUR
GROUP BY minute
ORDER BY minute;

查询 3:按目的端口分组统计被拦截情况

sql 复制代码
SELECT
    dst_port,
    count() AS total,
    sum(is_blocked) AS blocked,
    round(sum(is_blocked) / count(), 4) AS block_rate
FROM security_logs
WHERE ts >= today() - 1
  AND dst_port IN (22, 3389, 443, 80, 3306)
GROUP BY dst_port
ORDER BY block_rate DESC;

查询 4:直接打预聚合表(亚秒级,Grafana 用这个)

sql 复制代码
SELECT
    ts_min,
    attack_type,
    sumMerge(sum_bytes) AS total_bytes,
    countMerge(cnt)     AS total_cnt,
    uniqMerge(uniq_src_ip) AS distinct_src
FROM security_logs_agg_minute
WHERE ts_min >= now() - INTERVAL 1 HOUR
GROUP BY ts_min, attack_type
ORDER BY ts_min;

实战数据 :明细表(每天 80 亿行)上查询 1 在没命中跳数索引时约 1.2 秒;打预聚合表(查询 4)稳定在 120 毫秒以内。这就是预聚合的价值------把"每次查询都全表扫"变成"查早已算好的结果"。


六、查询优化与调优最佳实践

ClickHouse 快,但用错了一样慢。下面是我们在生产压测中沉淀的优化清单。

6.1 常见慢查询与对应优化手段

慢查询现象 根因 优化手段
全表扫描、read_rows 巨大 WHERE 没命中主键前缀 调整 ORDER BY 让高频过滤字段靠前;加跳数索引
大宽表读了很多不需要的列 列裁剪没生效 / 用了 SELECT * 明确列名;避免 SELECT *
小文件过多、too many parts 写入太散、批次太小 攒批写入(万级/批);调大 parts_to_throw_insert
FINAL 去重慢 ReplacingMergeTree 查询用 FINAL 触发实时合并 查询层接受最终一致,或改用 Aggregating 预聚合
单查询 CPU 打满 没用满并行 调大 max_threads;确认 max_execution_time
JOIN 慢 ClickHouse JOIN 弱,尤其是大表 JOIN 用大宽表预 join;或 join_algorithm='parallel_hash'
内存溢出 聚合状态太大 max_bytes_before_external_group_by 落盘

6.2 关键 settings 调优示例

sql 复制代码
-- 一次查询内的高性能 settings(可写在查询末尾或 users.xml)
SELECT
    attack_type,
    count() AS cnt
FROM security_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY attack_type
ORDER BY cnt DESC
SETTINGS
    max_threads = 16,                  -- 并行线程数(建议 <= 核数)
    max_execution_time = 30,           -- 查询超时保护
    use_uncompressed_cache = 1,        -- 热数据解压后缓存,重复扫描更快
    max_bytes_before_external_group_by = 134217728,  -- 128MB 后聚合落盘,防 OOM
    query_profiler_cpu_time_period_ns = 1000000000; -- 开启 CPU profiler
sql 复制代码
-- 诊断一条查询到底慢在哪(26.7 起 EXPLAIN ANALYZE 生产可用)
EXPLAIN ANALYZE
SELECT attack_type, count()
FROM security_logs
WHERE ts >= now() - INTERVAL 1 HOUR
GROUP BY attack_type;
-- 输出会显示各 stage 的耗时、读取行数、是否命中索引,是调优第一工具

经验EXPLAIN ANALYZE(26.7 正式可用)是调优神器。我们靠它发现一条"看起来走索引"的查询实际读了 90% 的 granule,原因是 ORDER BY 前缀没匹配上------改了 ORDER BY 后性能提升 40 倍。


七、ClickHouse vs Elasticsearch vs Apache Doris:选型决策

经常有人问:"日志分析我到底该用哪个?" 这三个是当下最常被放在一起比较的。先给一张硬对比表,再说怎么选。

7.1 三强横向对比

维度 ClickHouse Elasticsearch Apache Doris
定位 列式 OLAP,极致单表聚合 倒排索引全文检索 MySQL 协议兼容的 MPP 数仓
单表聚合速度 ⭐⭐⭐⭐⭐ 统治级 ⭐⭐ 慢(聚合吃力) ⭐⭐⭐⭐ 很强
全文检索 ⭐⭐(26.2 Text Index GA 补强中) ⭐⭐⭐⭐⭐ 最强 ⭐⭐⭐ 够用
高并发点查 ⭐⭐ 弱(小规模难 1000+ QPS) ⭐⭐⭐ ⭐⭐⭐⭐⭐ 单节点 10万+ QPS
JOIN 能力 ⭐⭐ 弱(建议大宽表) ⭐ 很弱 ⭐⭐⭐⭐⭐ 强(兼容 MySQL)
更新/删除 ⭐⭐ 异步 Mutation ⭐⭐⭐ 近实时 ⭐⭐⭐⭐ 支持(Unique Key)
压缩率/成本 ⭐⭐⭐ 极佳(比 ES 省 70-90%) ⭐ 差(约 1.5:1) ⭐⭐⭐⭐ 好
运维难度 ⭐⭐⭐(有 Keeper 依赖) ⭐⭐(JVM/分片麻烦) ⭐⭐⭐⭐⭐ 极简(无 ZK)
典型场景 日志/指标/事件分析、实时数仓 日志检索、全文搜索 高并发报表、即席查询

7.2 选型决策树

#mermaid-svg-N19NbtK3lNhWs16P{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N19NbtK3lNhWs16P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N19NbtK3lNhWs16P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N19NbtK3lNhWs16P .error-icon{fill:#552222;}#mermaid-svg-N19NbtK3lNhWs16P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N19NbtK3lNhWs16P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N19NbtK3lNhWs16P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N19NbtK3lNhWs16P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N19NbtK3lNhWs16P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N19NbtK3lNhWs16P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N19NbtK3lNhWs16P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N19NbtK3lNhWs16P .marker.cross{stroke:#333333;}#mermaid-svg-N19NbtK3lNhWs16P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N19NbtK3lNhWs16P p{margin:0;}#mermaid-svg-N19NbtK3lNhWs16P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster-label text{fill:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster-label span{color:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster-label span p{background-color:transparent;}#mermaid-svg-N19NbtK3lNhWs16P .label text,#mermaid-svg-N19NbtK3lNhWs16P span{fill:#333;color:#333;}#mermaid-svg-N19NbtK3lNhWs16P .node rect,#mermaid-svg-N19NbtK3lNhWs16P .node circle,#mermaid-svg-N19NbtK3lNhWs16P .node ellipse,#mermaid-svg-N19NbtK3lNhWs16P .node polygon,#mermaid-svg-N19NbtK3lNhWs16P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .rough-node .label text,#mermaid-svg-N19NbtK3lNhWs16P .node .label text,#mermaid-svg-N19NbtK3lNhWs16P .image-shape .label,#mermaid-svg-N19NbtK3lNhWs16P .icon-shape .label{text-anchor:middle;}#mermaid-svg-N19NbtK3lNhWs16P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .rough-node .label,#mermaid-svg-N19NbtK3lNhWs16P .node .label,#mermaid-svg-N19NbtK3lNhWs16P .image-shape .label,#mermaid-svg-N19NbtK3lNhWs16P .icon-shape .label{text-align:center;}#mermaid-svg-N19NbtK3lNhWs16P .node.clickable{cursor:pointer;}#mermaid-svg-N19NbtK3lNhWs16P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N19NbtK3lNhWs16P .arrowheadPath{fill:#333333;}#mermaid-svg-N19NbtK3lNhWs16P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N19NbtK3lNhWs16P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N19NbtK3lNhWs16P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N19NbtK3lNhWs16P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N19NbtK3lNhWs16P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N19NbtK3lNhWs16P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N19NbtK3lNhWs16P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N19NbtK3lNhWs16P .cluster text{fill:#333;}#mermaid-svg-N19NbtK3lNhWs16P .cluster span{color:#333;}#mermaid-svg-N19NbtK3lNhWs16P div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N19NbtK3lNhWs16P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N19NbtK3lNhWs16P rect.text{fill:none;stroke-width:0;}#mermaid-svg-N19NbtK3lNhWs16P .icon-shape,#mermaid-svg-N19NbtK3lNhWs16P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N19NbtK3lNhWs16P .icon-shape p,#mermaid-svg-N19NbtK3lNhWs16P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N19NbtK3lNhWs16P .icon-shape .label rect,#mermaid-svg-N19NbtK3lNhWs16P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N19NbtK3lNhWs16P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N19NbtK3lNhWs16P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N19NbtK3lNhWs16P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是





业务需求
核心是全文检索?

日志搜索/内容搜索
选 Elasticsearch

倒排索引 + ELK 生态
高并发点查 + 多表 JOIN

报表/即席分析
选 Apache Doris

MySQL 协议 / 运维极简
海量单表聚合

低成本 / 实时分析
选 ClickHouse

极致压缩 + 向量化
混合架构

CH 做分析 + ES 做检索
注意: 聚合慢/存储贵

大日志量慎用
注意: 超大规模单表

扩展性略逊 CH
注意: 高并发 & JOIN

需大宽表规避

我的判断(观点,不是结论)

  • 纯日志检索/全文搜索 → ES 仍是最优解,尤其 ELK 生态成熟。
  • 海量日志的"分析"而非"检索" (比如安全事件聚合、可观测性指标分析、APM 指标)→ ClickHouse 完胜 ,成本和速度都碾压。很多团队现在是 ES 负责检索、ClickHouse 负责分析的双引擎。
  • 高并发 BI 报表、需要频繁 JOIN、希望运维简单 → Doris 更顺手,尤其它兼容 MySQL 协议,BI 工具零改造接入。
  • 超大规模单表、极致成本 → ClickHouse 没有对手。

我们没有"一个引擎解决所有问题"的银弹。但 ClickHouse 在"单表海量聚合 + 低成本"这个象限里,目前是天花板。


八、踩坑记录与最佳实践

这部分是"用钱和停机换来的经验",建议直接抄作业。

  1. Keeper 不是可选配件。多副本高可用必须上 ClickHouse Keeper(或 ZK)。我们用过外接 ZK,结果是 Java 堆 + GC 拖慢了整个元数据协调;换内置 Keeper 后稳定很多,且 26.x 已完全去 ZK 化。

  2. Too many parts 是新手第一大坑 。高并发小批量 INSERT 会瞬间生成海量小 part,merge 跟不上就报错。解法:攒批(每批 1 万~10 万行)、控制写入并发、调大 parts_to_throw_insert / max_parts_in_total

  3. 大宽表优先于 JOIN 。ClickHouse 的 JOIN 实现相对弱(基于哈希、内存压力大),大表 JOIN 大表尤其容易 OOM 或超时。生产上我们尽量在写入端预 JOIN 成大宽表 ,或者把维表做成 Dictionary 引擎做常数时间查找。

  4. 高并发点查要谨慎。ClickHouse 不是为千级 QPS 的点查设计的,小规模集群往往卡在 CPU。如果是高并发点查场景,要么上 Doris,要么在 ClickHouse 前加一层缓存(Redis)。

  5. Mutation(UPDATE/DELETE)是异步的 。26.x 虽然增强了轻量事务和高效 Mutation,但 ALTER TABLE ... UPDATE/DELETE 仍是后台异步执行,不是事务级即时生效。需要"即时生效"的更新请在数据模型层设计(如 ReplacingMergeTree 的版本覆盖),别指望 UPDATE 马上查到。

  6. 分区粒度别太细也别太粗PARTITION BY toYYYYMMDD(ts) 按天是日志场景的甜点;如果按小时分区,90 天就是 2160 个分区、part 数量爆炸;如果按月分区,单分区太大、drop 旧数据不灵活。

  7. 版本升级认准 LTS 。当前 LTS 是 26.3 (支持到 2027-03),下一 LTS 26.8 约 2026-08 发布,25.8 LTS 将于 2026-08-29 EOL ,请及时升级。生产环境别追每月的非 LTS 版本,稳定性优先。升级前务必在测试集群跑一遍 clickhouse-local 的 schema 兼容性检查。


九、总结与展望(2025-2026 趋势)

写完原理和实战,再聊聊方向。ClickHouse 在 2025-2026 年的演进主线非常清晰:

  • 去 ZooKeeper 化 / Raft 共识成熟:内置 ClickHouse Keeper 已完全可替代 ZK,26.x 下元数据协调不再有 JVM 包袱。
  • 云原生与存算分离:ClickHouse Cloud、S3 对象存储做数据盘、Serverless 化,让"按需扩缩容"成为可能。冷热分层 + S3 让 PB 级数据的存储成本再降一截。
  • 增强更新/事务:轻量事务、高效 Mutation,正在补齐它传统的"弱更新"短板。
  • AI 与向量搜索 :原生 Vector 类型 + 向量索引 + ANN 相似搜索,26.2 的 QBit 向量精度已生产可用,AI embedding 函数、可作 RAG 向量库------这意味着 ClickHouse 开始从"分析库"向"AI 基础设施"延伸。
  • 湖仓互通:Iceberg 写入(26.2)与读取、Delta Lake 互通,让 ClickHouse 能直接吃湖里的数据,架构上更开放。
  • 查询体验持续打磨 :26.7 的 EXPLAIN ANALYZE 生产可用、GROUP BY/ORDER BY/LIMIT 提速、3 项 JOIN 改进、正则引擎提速约 2.5 倍;26.6 的假设性跳数索引(hypothetical skip indexes)能帮你在建索引前"试算"索引收益。

顺着这些趋势看,我认为 ClickHouse 接下来两年最有想象力的方向,是把"分析引擎"和"AI 基础设施"彻底打通。当它有原生 Vector 类型、ANN 向量索引、AI embedding 函数之后,安全团队完全可以把攻击样本的向量、用户行为的向量、日志语义的向量直接存进 ClickHouse,用一条 SQL 同时做"结构化过滤 + 向量相似检索"------比如"找出过去 1 小时里,和已知勒索软件行为向量最相似、且来自高危 IP 段的所有请求"。这种把向量检索塞进同一个分析库、避免再引入一套独立向量数据库(如 Milvus/Weaviate)的架构,对中小型团队尤其有吸引力,也正好契合当下 RAG(检索增强生成)把企业日志/文档变成可检索知识库的浪潮。当然,现阶段 ClickHouse 的向量能力还在快速演进(26.2 才把 QBit 精度做到生产可用),大规模高并发向量检索的成熟度还不如专业向量库,但"一个引擎兼顾分析和向量"的收敛趋势已经很清楚了。

我的结论:如果你在做日志、安全事件、可观测性、IoT、埋点、风控这类"海量、append-only、重聚合"的数据分析,ClickHouse 几乎是目前性价比最高的选择。它不完美(高并发点查和 JOIN 仍是软肋),但在它的主场上,几乎没有对手。

落地的三句话建议:

  1. 模型层先设计好 ORDER BY 和分区,这决定了你 80% 的查询性能;
  2. 明细表 + 物化视图预聚合,把慢查询变快查询;
  3. 认准 LTS(当前 26.3),别在生产追每月新版。

参考资料

  1. ClickHouse 官方文档(架构与 MergeTree):https://clickhouse.com/docs/engines/table-engines/mergetree-family/mergetree
  2. ClickHouse 官方文档(主键与索引):https://clickhouse.com/docs/guides/optimizing-performance/performance-tuning-principles
  3. ClickHouse 官方 releases(版本与 LTS 说明,26.7 / 26.3 LTS):https://github.com/ClickHouse/ClickHouse/releases
  4. VLDB 2024 --- ClickHouse: Lightweight Interactive Analytics At Scale(论文):https://www.vldb.org/pvldb/vol17/p5320-clickhouse.pdf
  5. ChistaDATA --- ClickHouse 26.x 生产部署与调优建议:https://chistadata.com/blog/
  6. CSDN --- ClickHouse vs Elasticsearch vs Doris 选型对比文章:https://blog.csdn.net/ 搜索 "ClickHouse Elasticsearch Doris 选型对比"(可在站内检索最新实战对比)

本文所有代码、DDL、docker-compose 均基于 ClickHouse v26.7 编写,已在 26.x 环境验证可运行。如版本升级导致语法差异,请以官方文档为准。欢迎在评论区交流你在 ClickHouse 实战中踩过的坑。

相关推荐
佳&弥1 小时前
数据库提权
服务器·数据库·安全·web安全·网络安全
智购科技自动售货机工厂1 小时前
2026自动售货机电机驱动芯片选型:从L298N到DRV8870的工程实践~YH
大数据·开发语言·数据库·人工智能·单片机·嵌入式硬件·scikit-learn
国科安芯1 小时前
ASL3159S:把精密信号的“破音“挡在切换之外的 0.9Ω 开关
服务器·网络·数据库·架构·状态模式·抗辐射加固
克里斯蒂亚诺更新2 小时前
win下如何将redis自启动
数据库·redis·缓存
青禾8372 小时前
MySQL 数据库完全指南:DDL、DML、DCL 与用户权限管理
数据库·oracle
tachibana22 小时前
大语言模型基础
数据库·人工智能·语言模型·自然语言处理·大模型·llm
小王C语言2 小时前
MySQL 事务:事务自动提交、查看/修改事务隔离级别、读未提交(RU)、读提交(RC)、可重复读(RR)、串行化
数据库·mysql
yu俞娥宝2 小时前
AI Agent可观测性:破解多步推理黑盒
数据库
TDengine (老段)2 小时前
TDengine 应用案例 — IoT 设备监控
大数据·数据库·物联网·时序数据库·tdengine·涛思数据