<?xml version="1.0" encoding="utf-8"?>
<ul>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。连接与查看clickhouse-client --host ch-01 --port 9000 --user defaultcurl 'http://ch-01:8123/?query=SELECT%201'SELECT version();SHOW DATABASES;SHOW TABLES FROM analytics;DESCRIBE TABLE analytics.events_local;SHOW CREATE TABLE analytics.events_local;建表模板CREATE TABLE analytics.events_local(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id)TTL event_date + INTERVAL 13 MONTH;常用引擎            引擎      语义                  MergeTree      通用明细              ReplacingMergeTree      按排序键和版本替换              SummingMergeTree      数值列求和              CollapsingMergeTree      sign 抵消              VersionedCollapsingMergeTree      版本化抵消              ReplicatedMergeTree      副本              Distributed      分片入口              Kafka      消息消费              Memory      内存临时表      最新状态查询SELECT    order_id,    argMax(order_status, version) AS status,    argMax(amount, version) AS amount,    max(updated_at) AS updated_atFROM analytics.orders_localGROUP BY order_id;写入clickhouse-client --query "INSERT INTO analytics.events_local FORMAT JSONEachRow" &lt; events.jsonlclickhouse-client --query "INSERT INTO analytics.events_local FORMAT CSVWithNames" &lt; events.csvclickhouse-client --query "INSERT INTO analytics.events_local FORMAT Parquet" &lt; events.parquet推荐写入：单批 1 万到 10 万行间隔数百毫秒到数秒固定并发失败重试并死信查询优化SELECT    query_id,    query,    query_duration_ms,    read_rows,    read_bytes,    memory_usageFROM system.query_logWHERE type = 'QueryFinish'ORDER BY query_duration_ms DESCLIMIT 30;检查顺序：分区条件排序键前缀读取列过滤条件聚合基数JOIN 大小资源限制运行中查询SELECT query_id, elapsed, read_rows, memory_usage, queryFROM system.processesORDER BY elapsed DESC;KILL QUERY WHERE query_id = 'target-query-id';part 与合并SELECT table, partition, count() AS parts, sum(rows) AS rowsFROM system.partsWHERE activeGROUP BY table, partitionORDER BY parts DESC;SELECT database, table, elapsed, num_partsFROM system.merges;副本状态SELECT    database,    table,    replica_name,    is_readonly,    queue_size,    absolute_delayFROM system.replicas;集群与磁盘SELECT cluster, shard_num, replica_num, host_name, is_localFROM system.clusters;SELECT name, path, total_space, free_spaceFROM system.disksORDER BY free_space;Kafka 消费SELECT database, table, consumer_id, assignments.topicFROM system.kafka_consumers;SYSTEM STOP CONSUMERS analytics.kafka_events;SYSTEM START CONSUMERS analytics.kafka_events;权限CREATE ROLE analyst_ro;GRANT SELECT ON analytics.* TO analyst_ro;CREATE USER analyst IDENTIFIED BY 'strong-password';GRANT analyst_ro TO analyst;常见错误            错误      方向                  Too many parts      小写入、分区过多、合并慢              MEMORY_LIMIT_EXCEEDED      高基数聚合、大 JOIN、扫描过大              Disk space insufficient      TTL、扩容、停写              Replica is readonly      Keeper、元数据、恢复状态              Timeout      查询成本、资源争抢              DNS error      域名、网络、远程节点      本章小结本速查手册汇总连接、建表、写入、查询、运维和排障的常用命令。生产使用时要以当前版本文档为准，并把团队常用 SQL 固化到运维平台中。</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。掌握 ClickHouse 不只是记住语法，而是能把业务问题转化为稳定的数据平台。本章给出继续精进的学习路径和工程实践方法。32.1 能力模型            阶段      能力                  入门      安装、SQL、导入导出              进阶      MergeTree、分区排序、物化视图              高级      分片副本、Keeper、容量规划              专家      实时数仓、查询治理、故障演练              大师      架构选型、成本优化、团队规范      32.2 深入源码与文档建议阅读方向：  MergeTree 写入和合并流程；  part 目录和索引格式；  查询Pipeline；  向量化函数实现；  分布式查询协议；  Keeper 副本协调；  存储策略和 Codec。官方文档和 changelog 是第一资料。版本迭代快，博客结论必须结合当前版本验证。32.3 建立实验环境最小集群：3 个 ClickHouse 节点3 个 Keeper 节点1 个 Kafka1 个压测客户端常做实验：  小写入导致 parts 增长；  不同排序键的扫描量差异；  Replacing 的版本语义；  物化视图回填边界；  Projection 写入代价；  副本队列恢复；  磁盘满应急演练；  备份恢复演练。32.4 性能方法论定义查询集固定数据集和硬件记录基线指标一次只改一个变量观察 read_bytes / memory / CPU进行并发压测记录成本变化沉淀规范不要只优化单次耗时，还要看并发稳定性、资源公平性和故障恢复能力。32.5 数据治理大师级实践包括：  表 owner 制度；  指标口径登记；  schema 变更评审；  生命周期和 TTL；  数据分级和权限；  自动对账；  死信修复闭环；  成本归属。技术优化只有变成团队规范，才能长期有效。32.6 容量与成本持续跟踪：            指标      用途                  每日原始数据量      容量预测              压缩率      存储成本              写入峰值      扩容时机              查询扫描量      优化收益              副本和投影成本      总成本              冷热分层收益      成本优化      32.7 故障演练定期演练：  kill 大查询；  单副本故障；  Keeper 少数节点故障；  磁盘满；  Kafka 重放；  误删分区恢复；  滚动升级；  集群迁移。每次演练后更新 runbook 和监控阈值。32.8 学习节奏第 1 阶段：SQL + 单机 MergeTree第 2 阶段：导入导出 + 表引擎语义第 3 阶段：查询优化 + 索引投影第 4 阶段：副本分片 + Keeper第 5 阶段：实时链路 + 数据建模第 6 阶段：容量治理 + 故障演练第 7 阶段：主导架构选型和规范建设本章小结从入门到大师的路径，是从会执行 SQL，到理解存储和执行模型，再到设计高可用实时数仓，最后建立团队级治理体系。ClickHouse 的学习要结合真实数据和真实故障，实验、记录、复盘缺一不可。思考题  你当前阶段的短板在哪里？  如何搭建自己的长期实验环境？  哪些团队规范最值得先落地？  如何量化一次优化的真实收益？  下一次故障演练选什么场景？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章整理高频面试问题，并给出能体现工程判断的回答思路。31.1 ClickHouse 为什么快？回答要点：  列式存储，只读需要的列；  高压缩率，减少 IO；  稀疏索引和分区剪枝；  向量化执行和 SIMD；  MergeTree 批量追加；  分布式并行扫描；  预聚合、投影和字典优化。加分项：说明自己会看 read_rows、read_bytes、memory_usage 和 ProfileEvents 来验证。31.2 MergeTree 的 part 是什么？INSERT 生成 partpart 内按 ORDER BY 排序后台 merge 合并同分区 partinactive part 延迟清理面试重点：解释小写入导致 Too many parts，以及 part 数量、合并和查询性能的关系。31.3 分区和排序键的区别？分区负责生命周期和粗粒度剪枝；排序键决定 part 内物理顺序和稀疏索引。常见错误是把分区当索引，或者按小时分区导致 part 爆炸。31.4 ReplacingMergeTree 是否保证唯一？不能保证查询瞬间唯一。它只在后台合并时按排序键替换，查询侧应使用 argMax(value, version) 或受限 FINAL。分布式场景还要保证同一业务键路由到同一分片。31.5 FINAL 为什么慢？FINAL 会在查询时应用合并语义，可能需要读取和处理多个 part、比较版本、临时物化结果。大范围 FINAL 会放大 CPU、内存和 IO。应先过滤分区和键范围，或预物化最新状态。31.6 SummingMergeTree 查询是否还要 sum？要。合并可能尚未完成，或仍有多个大 part。SummingMergeTree 是减少物理行数的手段，不是查询层免聚合的保证。31.7 UV 为什么不能直接相加？UV 是去重指标，跨批次、跨城市、跨日期相加会重复计数。可选方案：  保留明细；  uniqState / uniqMerge；  近似 uniq；  bitmap 或 HLL；  按业务定义接受误差。31.8 物化视图什么时候触发？源表有新数据块写入时触发，对每个 insert block 执行 SELECT 并写入目标表。历史数据不会自动回填，删除源数据也不会自动删除目标数据。31.9 Projection 和物化视图怎么选？Projection 属于表内数据，可改变排序或固定聚合，由优化器选择，适合简单模式。物化视图是独立对象，写入目标表，适合复杂转换、多目标分发和更灵活的 SQL。31.10 副本和分片的区别？副本存储同一份数据，提高可用性和读吞吐；分片存储不同数据，提高写容量和存储规模。常见组合是每个分片两副本。31.11 Keeper 的作用是什么？保存副本日志、块信息、leader 选举和分布式 DDL 队列等协调元数据。它不是业务数据存储；多数派不可用时副本写入和集群 DDL 会受影响。31.12 分片键如何设计？考虑数据均衡、查询局部性和业务语义。常用实体 ID hash，如 cityHash64(user_id)；状态表必须保证业务键稳定路由。时间做分片容易热点，时间通常交给分区。31.13 如何优化慢查询？标准回答：看 query_log确认 read_rows / read_bytes / memory检查分区条件检查排序键前缀减少列和结果集优化 GROUP BY / JOIN考虑预聚合、投影、字典设置资源限制并发压测31.14 Too many parts 怎么处理？短期限流写入、降低补数并发、处理磁盘瓶颈；长期改成大批量写入、减少分区数、优化表结构或扩容。不要依赖频繁 OPTIMIZE。31.15 为什么 ClickHouse 不适合 OLTP？它没有传统事务、行级锁和高频随机更新的执行模型。设计目标是批量追加和大扫描分析。强事务、点查、高频更新场景应选择 OLTP 数据库。31.16 如何保证数据不丢？分层回答：  Kafka 保留和重放；  写入端重试与死信；  目标表幂等；  副本避免单点；  备份处理误删和集群级故障；  监控 lag 和对账。31.17 Kafka lag 增长怎么办？排查顺序：  生产端是否暴增；  消费者是否停止；  分区是否均衡；  消息是否有坏格式；  ClickHouse 是否写入失败；  磁盘或合并是否到瓶颈；  是否需要扩分区和消费者。31.18 如何设计实时报表？说明分层：DWD 明细、DWS 汇总、ADS 报表；明确事件时间、去重口径、迟到数据、幂等、对账和降级。不要只讲建表 SQL。31.19 系统设计题：亿级事件日增回答框架：容量估算分片副本规划表引擎和排序写入微批Kafka 消费预聚合查询限流监控告警备份恢复扩容方案31.20 高频反模式  高频小 INSERT；  按小时或用户分区；  滥用 FINAL；  UV 直接相加；  大表在线 JOIN；  没有分页限制；  无资源配额；  只有副本没有备份；  不做对账；  忽略版本差异。本章小结面试中展示判断力比背概念更重要：先讲模型和边界，再讲实现和验证方式。ClickHouse 的高频考点集中在 MergeTree、合并语义、分区排序、分布式架构、实时链路和性能治理。思考题  如何向面试官解释 ClickHouse 的性能来源？  ReplacingMergeTree 的一致性边界是什么？  如何设计一个亿级行为分析平台？  数据不丢和不重如何同时设计？  你经历过哪些 ClickHouse 生产反模式？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章把前面内容串成一个实时用户行为分析平台，覆盖埋点接入、实时写入、明细建模、汇总报表、漏斗留存和运维治理。30.1 业务目标平台需要回答：1. 今天各城市、渠道的 PV / UV / GMV 是多少？2. 用户从浏览到支付的漏斗转化如何？3. 某个用户最近发生了什么事件？4. 昨天活动上线后指标是否异常？5. 数据延迟和质量是否达标？指标口径：            指标      定义                  PV      event_type = view 的次数              UV      event_type = view 的去重用户数              GMV      event_type = pay 的 sum(amount)              支付率      支付用户数 / 访问用户数              D1 留存      今日活跃用户在次日仍活跃      30.2 链路架构Web / App   |Collector   |Kafka raw-events   |Flink 清洗 / 补维 / 质量检查   |Kafka clean-events   |ClickHouse Kafka engine   |dwd_events   |dws_city_daily + ads_realtime死信链路：invalid events -&gt; Kafka dead-letter-events -&gt; 修复服务30.3 明细表CREATE TABLE analytics.dwd_events(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    session_id String,    event_type LowCardinality(String),    platform LowCardinality(String),    city_id UInt32,    channel LowCardinality(String),    amount Decimal64(2),    is_valid UInt8)ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, channel, user_id)TTL event_date + INTERVAL 13 MONTH;设计说明：  event_date 来自事件时间；  高频过滤字段均普通列化；  低基数字段使用 LowCardinality；  is_valid 支持坏数据标记；  保留 13 个月，支持跨年对比。30.4 汇总表CREATE TABLE analytics.dws_city_daily(    stat_date Date,    city_id UInt32,    channel LowCardinality(String),    pv UInt64,    pay_count UInt64,    gmv Decimal64(2))ENGINE = SummingMergeTreePARTITION BY toYYYYMM(stat_date)ORDER BY (stat_date, city_id, channel);物化视图：CREATE MATERIALIZED VIEW analytics.dws_city_daily_mvTO analytics.dws_city_dailyASSELECT    event_date AS stat_date,    city_id,    channel,    countIf(event_type = 'view') AS pv,    countIf(event_type = 'pay') AS pay_count,    sumIf(amount, event_type = 'pay') AS gmvFROM analytics.dwd_eventsWHERE is_valid = 1GROUP BY event_date, city_id, channel;UV 需要精确口径时使用 uniqState(user_id) 与 AggregatingMergeTree。30.5 实时报表SELECT    city_id,    channel,    sum(pv) AS pv,    sum(pay_count) AS pay_count,    sum(gmv) AS gmvFROM analytics.dws_city_dailyWHERE stat_date = today()GROUP BY city_id, channelORDER BY gmv DESCLIMIT 50;实时分钟表可按分钟汇总，但保留期短：minute metrics：保留 3 天daily metrics：保留 13 个月raw detail：保留 6 到 13 个月30.6 漏斗分析SELECT    level,    count() AS usersFROM (    SELECT        user_id,        windowFunnel(86400)(            toUnixTimestamp(event_time),            event_type = 'view',            event_type = 'cart',            event_type = 'pay'        ) AS level    FROM analytics.dwd_events    WHERE event_date = today()    GROUP BY user_id)GROUP BY levelORDER BY level;注意窗口起点、事件顺序、跨天用户和多设备归属。30.7 用户查询SELECT    event_time,    event_type,    city_id,    channel,    amountFROM analytics.dwd_eventsWHERE event_date IN (today(), today() - 1)  AND user_id = 1001ORDER BY event_time DESCLIMIT 100;主排序对 user_id 查询不友好，可以增加投影：ALTER TABLE analytics.dwd_eventsADD PROJECTION proj_user(    SELECT *    ORDER BY (user_id, event_date, event_time));上线前对比写入吞吐、磁盘增长和 read_bytes。30.8 权限CREATE ROLE analyst_ro;GRANT SELECT ON analytics.dwd_events TO analyst_ro;GRANT SELECT ON analytics.dws_city_daily TO analyst_ro;CREATE USER analyst IDENTIFIED BY 'strong-password'SETTINGS PROFILE analyst_profile;GRANT analyst_ro TO analyst;分析用户只读，ETL 用户按库表授权，管理员与业务账号分离。30.9 监控            指标      目标                  Kafka lag      小于 1 分钟              写入失败      立即告警              核心表行数      环比异常告警              查询 P95      按报表分级              parts      不持续增长              磁盘      70% 预警              汇总对账      每小时执行      对账：SELECT countIf(event_type = 'pay'), sumIf(amount, event_type = 'pay')FROM analytics.dwd_eventsWHERE event_date = today();SELECT sum(pay_count), sum(gmv)FROM analytics.dws_city_dailyWHERE stat_date = today();30.10 上线清单1. schema review2. 分区和排序键 review3. 写入压测4. 查询压测5. 权限最小化6. 备份恢复演练7. 监控和 runbook8. 数据质量对账9. 灰度切流10. 回滚方案本章小结实战平台的关键不是“能写入能查询”，而是口径清楚、链路可恢复、资源可预算、数据可对账。明细表保留可追溯性，汇总表服务高频报表，投影和字典解决特定查询，监控和权限保障长期稳定。思考题  为什么 UV 不能直接在 SummingMergeTree 中相加？  漏斗窗口如何定义？  用户明细查询为什么需要投影？  实时报表的数据质量如何验证？  上线前必须压测哪些场景？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 和 Elasticsearch 都是常见的数据平台，但优化目标不同：前者面向大规模扫描聚合，后者面向搜索、倒排检索和可观测性场景。29.1 核心差异            维度      ClickHouse      Elasticsearch                  存储模型      列式      面向文档，倒排索引              强项      大范围聚合分析      全文检索、相关性搜索              写入      批量 append      文档近实时写入              更新      Merge 语义      文档 update / delete              聚合      强，成本可控      可用，深度聚合成本高              查询模式      SQL      DSL / QL              典型场景      数仓、报表、行为分析      搜索、日志检索、APM      29.2 日志场景Elasticsearch：优点：检索表达式丰富、生态成熟、权限和索引生命周期完善代价：大范围聚合和长期留存成本高ClickHouse：优点：压缩率高、扫描聚合快、长期存储成本低代价：全文检索和相关性排序能力不如专门搜索系统常见组合：Kafka  -&gt; Elasticsearch：最近 7 天检索  -&gt; ClickHouse：6 个月或更长期分析29.3 用户行为分析漏斗、留存、多维聚合：SELECT city_id, event_type, count(), sum(amount)FROM analytics.eventsWHERE event_date = today()GROUP BY city_id, event_type;这类查询通常更适合 ClickHouse。若需要“搜索符合条件的一批用户”，再用 Elasticsearch 或业务检索服务。29.4 指标监控常见选择：            需求      方案                  高基数指标长期存储      ClickHouse / TSDB              指标采集与告警      Prometheus + TSDB              日志与 trace 检索      Elasticsearch / Loki / ClickHouse              统一可观测性      根据规模和查询模式选择      ClickHouse 可以存 Prometheus 长期指标，但告警链路通常仍由 Prometheus 或其他监控服务承担。29.5 更新语义ClickHouse：ReplacingMergeTree 后台替换CollapsingMergeTree 正负抵消查询需处理版本Elasticsearch：按文档 ID update / delete近实时可见segment merge 后清理如果业务核心是文档 ID 级更新和检索，Elasticsearch 更自然；如果是分析快照，ClickHouse 的版本化合并模型更有优势。29.6 容量与性能比较时要使用相同条件：  相同数据集；  相同压缩和索引配置；  相同查询集；  相同并发；  相同硬件；  相同保留期；  相同一致性要求。Elasticsearch 索引、副本和查询缓存会消耗较多内存；ClickHouse 的成本主要来自磁盘扫描、合并和高并发查询 CPU。29.7 选型流程1. 查询是检索为主还是聚合为主？2. 是否需要全文和相关性行？3. 数据保留多久？4. 写入是批量流还是文档更新？5. 团队运维能力？6. 成本模型如何？7. 是否可以两系统协同？29.8 常见误区            误区      修正                  ClickHouse 可以替代所有搜索      全文检索不是它的核心强项              Elasticsearch 聚合一定慢      取决于规模、映射和查询              只比较单条 SQL 耗时      要看并发、成本和保留期              双写一定能一致      需要边界和对账              迁移后 schema 照搬      要重新建模      本章小结ClickHouse 与 Elasticsearch 是互补关系。ClickHouse 擅长大规模分析、预聚合和长期存储；Elasticsearch 擅长全文检索和文档化查询。根据查询模式、更新语义和成本选择，而不是简单追求一个系统解决所有问题。思考题  哪些日志查询更适合 Elasticsearch？  哪些行为分析更适合 ClickHouse？  两系统并存的边界和口径如何设计？  更新语义有什么差异？  如何做公平的性能对比？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。故障排查要先确定故障域：客户端、网络、ClickHouse 服务、存储、Keeper、上游写入还是查询本身。本章给出一套可复用的排查框架。28.1 排查框架1. 确认影响面：单查询、单表、单节点还是集群2. 确认时间线：开始时间、变更、流量峰值3. 检查服务存活和端口4. 检查资源：CPU、内存、磁盘、网络5. 检查 system.processes6. 检查 query_log / part_log / error log7. 检查副本和 Keeper8. 采取止血动作9. 事后修复根因28.2 服务不可用检查：curl -sS 'http://ch-01:8123/ping'systemctl status clickhouse-serverss -lntp | grep -E '8123|9000'常见原因：            原因      特征                  磁盘满      启动或写入失败              配置错误      启动直接退出              内存不足      进程退出或系统 OOM              端口冲突      监听失败              依赖 Keeper 不可达      副本只读或 DDL 失败              DNS 解析失败      远程节点连接异常      28.3 查询超时运行中查询：SELECT    query_id,    elapsed,    read_rows,    read_bytes,    memory_usage,    queryFROM system.processesORDER BY elapsed DESC;止血：KILL QUERY WHERE query_id = 'target-query-id';随后检查：  是否分区未过滤；  是否扫描超大列；  是否高基数聚合；  是否大 JOIN；  是否被合并或写入争抢 IO；  是否分布式某分片慢。28.4 内存不足错误常见为 MEMORY_LIMIT_EXCEEDED。排查：SELECT query_id, query, memory_usage, read_bytesFROM system.query_logWHERE exception LIKE '%MEMORY_LIMIT%'ORDER BY event_time DESCLIMIT 20;处理：  增加过滤条件；  减少读取列；  降低分组基数；  使用近似聚合；  预聚合；  调整 max_memory_usage；  增加机器内存；  检查并发队列。28.5 Too many parts查看：SELECT table, partition, count() AS parts, sum(rows) AS rowsFROM system.partsWHERE activeGROUP BY table, partitionORDER BY parts DESCLIMIT 20;止血：  暂停非关键写入；  降低补数并发；  增大批次间隔；  处理磁盘 IO 瓶颈；  必要时限制查询流量。根因修复是调整写入频率、分区粒度或容量，而不是频繁执行 OPTIMIZE。28.6 磁盘满SELECT name, path, total_space, free_spaceFROM system.disksORDER BY free_space;处理顺序：  停止或限流写入；  检查异常大表和临时文件；  确认 TTL 是否生效；  删除可安全删除的过期分区；  扩容磁盘；  恢复写入并观察合并。不要手工删除数据目录中的 part，除非有明确恢复方案。28.7 副本异常SELECT    database,    table,    replica_name,    is_readonly,    queue_size,    absolute_delay,    last_queue_exceptionFROM system.replicas;处理：            现象      方向                  readonly      Keeper 连接和元数据              queue 增长      网络、磁盘、源副本              delay 高      同步落后              part missing      从健康副本拉取或恢复              metadata mismatch      表结构一致性      28.8 Kafka 无数据排查：SELECT database, table, consumer_id, assignments.topicFROM system.kafka_consumers;检查：  topic 是否正确；  group offset 是否越界；  消息格式是否匹配；  物化视图是否存在；  目标表是否有异常；  是否坏消息被跳过；  消费者是否停止。28.9 数据不一致对账：SELECT count(), sum(amount)FROM analytics.events_localWHERE event_date = '2026-08-25';SELECT sum(pv), sum(gmv)FROM analytics.city_metric_localWHERE metric_date = '2026-08-25';差异来源：  物化视图创建时间；  历史回填重复；  过滤条件不同；  类型转换；  时区口径；  延迟数据；  消息丢失或重复。28.10 应急预案必备预案：            故障      动作                  恶意大查询      kill、限流、降并发              磁盘满      停写、清理、扩容              Too many parts      限流写入、降合并压力              Keeper 多数派失效      停拓扑变更、恢复 Quorum              单节点故障      摘除流量、副本恢复              数据误删      停写、备份恢复              Kafka 积压      降级、重放、扩消费者      本章小结故障排查要先收敛影响面，再用系统表和日志定位资源或语义问题。止血动作要有限制和记录，根因修复要回到写入模型、表设计、容量规划和监控告警。思考题  查询超时的第一排查动作是什么？  MEMORY_LIMIT_EXCEEDED 如何处理？  为什么不能频繁 OPTIMIZE 解决 Too many parts？  副本 readonly 常见原因有哪些？  数据不一致应从哪些口径入手？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 监控要覆盖服务存活、资源、存储、合并、副本、查询、写入和业务指标。只有服务 green 不代表链路健康。27.1 监控层次基础设施  -&gt; ClickHouse 服务  -&gt; 存储与合并  -&gt; 副本与集群  -&gt; 查询与写入  -&gt; 业务数据质量27.2 系统表常用表：            系统表      用途                  system.metrics      当前指标              system.events      累计事件              system.asynchronous_metrics      异步指标              system.query_log      查询日志              system.part_log      part 操作              system.merges      当前合并              system.replicas      副本状态              system.disks      磁盘              system.clusters      集群      查看核心指标：SELECT metric, valueFROM system.metricsWHERE metric IN (    'Query',    'Merge',    'ReplicatedFetch',    'DistributedSend',    'Read',    'Write');27.3 PrometheusClickHouse 提供 Prometheus 格式的 HTTP 端点，也可以使用 exporter。抓取配置示例：scrape_configs:  - job_name: clickhouse    static_configs:      - targets:          - ch-01:8123          - ch-02:8123采集内容要结合版本确认。常见监控项：            分类      指标                  服务      uptime、version、HTTP 可用              CPU      使用率、iowait              内存      used、swap、OOM              磁盘      空间、IO util、延迟              查询      QPS、失败率、耗时              写入      行数、字节、失败              合并      当前数、耗时、异常              副本      readonly、queue、delay      27.4 磁盘监控SELECT    name,    path,    total_space,    free_space,    unreserved_spaceFROM system.disksORDER BY free_space;告警建议：  空间使用率超过 70% 预警；  超过 85% 处理；  剩余空间低于合并预估临时空间则高危；  磁盘只读立即处理。27.5 合并与 part 监控SELECT    database,    table,    count() AS active_parts,    sum(rows) AS rows,    sum(bytes_on_disk) AS bytesFROM system.partsWHERE activeGROUP BY database, tableORDER BY active_parts DESC;SELECT count() AS running_mergesFROM system.merges;告警：            条件      级别                  单分区 parts 持续增长      预警              接近 max parts_in_total      高危              merge 异常增加      预警              磁盘 IO util 持续 90%+      高危      27.6 副本监控SELECT    database,    table,    replica_name,    is_readonly,    queue_size,    inserts_in_queue,    merges_in_queue,    absolute_delayFROM system.replicas;告警：  任一副本 readonly；  queue_size 持续增长；  absolute_delay 超过阈值；  Keeper 会话断开；  副本消失。27.7 查询监控SELECT    count() AS total,    countIf(type = 'QueryFinish') AS finished,    countIf(type = 'ExceptionWhileProcessing') AS failed,    avg(query_duration_ms) AS avg_ms,    max(query_duration_ms) AS max_msFROM system.query_logWHERE event_date = today();Top 慢查询：SELECT query, query_duration_ms, read_bytes, memory_usageFROM system.query_logWHERE type = 'QueryFinish'ORDER BY query_duration_ms DESCLIMIT 20;27.8 Kafka 链路监控必须采集：  consumer group lag；  topic 分区落后；  消息错误数；  dead letter 数量；  ClickHouse 写入失败；  目标表行数环比。查看 ClickHouse 消费者：SELECT database, table, consumer_id, assignments.topicFROM system.kafka_consumers;27.9 日志常见日志：/var/log/clickhouse-server/clickhouse-server.log/var/log/clickhouse-server/clickhouse-server.err.log搜索关键字：rg -n "Too many parts|MEMORY_LIMIT_EXCEEDED|KeeperException|DNS_ERROR|Timeout|Broken part|Disk space" \  /var/log/clickhouse-server日志保留要与故障回溯窗口匹配，并集中采集到日志系统。27.10 告警分级            级别      示例                  P0      集群不可用、磁盘满、多数 Keeper 失败              P1      大量写入失败、副本 readonly、核心报表无数据              P2      lag 增长、慢查询增加、parts 上涨              P3      配置漂移、低峰任务超时      每条告警应有 runbook：原因、检查 SQL、处理步骤、升级路径。本章小结监控 ClickHouse 不能只看进程和端口。磁盘、合并、副本队列、查询成本、Kafka lag 和业务对账共同决定系统健康。告警必须可行动，并配套可执行的排查手册。思考题  为什么磁盘空间监控要预留合并空间？  副本 queue_size 增长说明什么？  如何发现高成本 SQL？  Kafka lag 应按什么维度告警？  一条合格告警应包含哪些信息？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。写入优化的核心是减少 part 数量、控制合并压力、避免热点分片和保证失败可重试。26.1 写入模型小 INSERT  -&gt; 多个新 part  -&gt; 后台频繁合并  -&gt; CPU / IO / 元数据压力优化后：写入端缓冲  -&gt; 大 block  -&gt; 少量 part  -&gt; 合并压力可控推荐：            指标      参考                  单批行数      1 万到 10 万以上              写入频率      每秒到每数秒              单批数据      根据行宽压测确定              并发      固定有限并发              失败      指数退避重试              死信      保存不可恢复批次      26.2 合并批次应用端伪代码：type Buffer struct {    rows []Event    maxRows int    maxInterval time.Duration}func (b *Buffer) Add(e Event, flush func([]Event) error) error {    b.rows = append(b.rows, e)    if len(b.rows) &gt;= b.maxRows {        return flush(b.rows)    }    return nil}定时器触发：ticker := time.NewTicker(2 * time.Second)for range ticker.C {    if len(buffer.rows) &gt; 0 {        flush(buffer.Take())    }}关闭服务时必须执行 final flush。26.3 控制并发并发写入不是越高越好：  每批都会生成 part；  合并线程有限；  高并发会放大内存和网络；  分布式写入增加转发压力；  分区热点会集中到单节点。建议写入端使用固定 worker 池，并按表、分区、分片维度限流。26.4 分区控制避免一次写入覆盖过多分区：1000 行覆盖 86400 个每分钟分区这会生成大量小 part。分区粒度应与数据量匹配，历史迟到数据可单独低并发补写。检查分区 part：SELECT table, partition, count() AS parts, sum(rows) AS rowsFROM system.partsWHERE activeGROUP BY table, partitionORDER BY parts DESC;26.5 异步插入ClickHouse 提供异步插入设置，适合无法立刻改造的客户端：INSERT INTO analytics.events_localSETTINGS async_insert = 1, wait_for_async_insert = 1FORMAT JSONEachRow...它可以让服务端聚合部分小插入，但会增加延迟，并有故障语义需要评估。更推荐在写入端或消息层完成聚合。26.6 批量文件导入clickhouse-client --query "INSERT INTO analytics.events_local FORMAT CSVWithNames" &lt; events.csv大文件可拆分：split -l 1000000 events.csv part_for f in part_*; do  clickhouse-client --query "INSERT INTO analytics.events_local FORMAT CSVWithNames" &lt; "$f"done补历史数据时控制并发，避免影响实时查询和合并。26.7 分布式写入两种方式：            方式      特点                  写 Distributed 表      简单统一，可能异步转发              应用直连分片      更可控，需要维护拓扑      如果写 Distributed 表，要监控：SELECT database, table, error_count, data_files, bytes_to_sendFROM system.distribution_queue;队列积压时不要盲目重启，先检查目标分片、网络和磁盘。26.8 写入 Schema 优化  减少超宽列；  高频字段使用窄类型；  低基数字符串使用 LowCardinality；  复杂 JSON 先在写入端展开；  避免过多 Nullable；  合理使用 Codec。示例：CREATE TABLE analytics.events_compact(    event_date Date,    event_type LowCardinality(String),    platform LowCardinality(String),    city_id UInt32,    user_id UInt64,    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id);26.9 监控指标            指标      含义                  insert rows / bytes      写入吞吐              failed queries      写入失败              parts per partition      合并压力              merges count / elapsed      合并负载              disk IO      磁盘瓶颈              CPU iowait      IO 等待              distribution queue      分布式积压              too many parts      关键告警      查看合并：SELECT database, table, elapsed, num_parts, total_sizeFROM system.merges;26.10 排查 Too many partsSELECT table, partition, count() AS partsFROM system.partsWHERE activeGROUP BY table, partitionORDER BY parts DESCLIMIT 20;处理：  降低写入频率；  增大批次；  减少分区数；  降低补数并发；  检查磁盘和合并线程；  暂停低优先级写入；  扩容。本章小结写入优化优先发生在写入端：微批、固定并发、稳定分区和失败重试。ClickHouse 侧配合合理 schema、分区和监控。目标不是追求单次最快，而是让 part 生成速度长期低于合并消化速度。思考题  为什么小 INSERT 会拖慢查询？  应用缓冲和异步插入如何取舍？  分布式写入队列积压如何处理？  什么情况下需要降低补数并发？  如何判断集群写入容量已到瓶颈？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。查询优化的第一步是量化成本，而不是盲目加索引。核心指标是扫描行数、扫描字节、内存、耗时和并发影响。25.1 优化流程定位慢 SQL  -&gt; 查看扫描量和内存  -&gt; 检查分区剪枝  -&gt; 检查排序键匹配  -&gt; 减少列和行  -&gt; 改聚合或预计算  -&gt; 压测并发  -&gt; 固化查询规范25.2 找慢查询SELECT    query_id,    query,    query_duration_ms,    read_rows,    read_bytes,    memory_usageFROM system.query_logWHERE type = 'QueryFinish'  AND event_date &gt;= today() - 1ORDER BY query_duration_ms DESCLIMIT 30;按总成本统计：SELECT    normalized_query_hash,    count() AS executions,    sum(read_bytes) AS total_bytes,    avg(query_duration_ms) AS avg_ms,    any(query) AS sample_queryFROM system.query_logWHERE type = 'QueryFinish'GROUP BY normalized_query_hashORDER BY total_bytes DESCLIMIT 20;25.3 分区剪枝较差：SELECT count()FROM analytics.events_localWHERE user_id = 1001;较好：SELECT count()FROM analytics.events_localWHERE event_date = today()  AND user_id = 1001;分区表达式要与写入字段一致。对于：PARTITION BY toYYYYMM(event_date)查询建议使用：WHERE event_date &gt;= '2026-08-01'  AND event_date &lt; '2026-09-01';而不是只依赖 toYYYYMM(event_date) = 202608 这类表达式。不同版本优化能力有差异，直接使用列范围更稳妥。25.4 减少读取列避免：SELECT * FROM analytics.events_local;推荐：SELECT event_date, city_id, count()FROM analytics.events_localWHERE event_date = today()GROUP BY event_date, city_id;列存下 SELECT * 会同时放大 IO、网络和内存成本。25.5 过滤与聚合先过滤再聚合：SELECT city_id, count(), sum(amount)FROM analytics.events_localWHERE event_date = today()  AND event_type = 'pay'GROUP BY city_id;虽然优化器会做许多下推，但 SQL 保持清晰的过滤条件有利于稳定执行，也便于后续治理。25.6 聚合优化高成本：SELECT uniqExact(user_id)FROM analytics.events_local;可评估：SELECT uniq(user_id)FROM analytics.events_local;降基方式：            手段      说明                  缩小时间      分区过滤              低基数类型      UInt32 替代 String              近似函数      uniq、quantile              预聚合      物化视图              聚合状态      uniqState              分桶结果      先按用户桶汇总      计费、审计、财务类场景要明确是否允许近似。25.7 JOIN 优化大表 JOIN 小表：SELECT count()FROM big_table AS bINNER JOIN small_dim AS d ON b.key = d.key;优化方向：  小表使用 Dictionary；  提前过滤大表；  只 SELECT 需要列；  冗余稳定维表字段；  预计算宽表；  控制右表大小；  验证 JOIN 算法设置。查看计划：EXPLAINSELECT count()FROM big_table AS bINNER JOIN small_dim AS d ON b.key = d.key;25.8 LIMIT 与排序深分页会放大成本：LIMIT 100000, 20;可改为键集分页：SELECT event_id, event_timeFROM analytics.events_localWHERE event_date = today()  AND (event_time, event_id) &gt; ('2026-08-25 10:00:00', 'E999')ORDER BY event_time, event_idLIMIT 20;TopN 也要控制返回规模，避免超大中间结果传输。25.9 资源限制ALTER USER report_userSETTINGS    max_execution_time = 30,    max_memory_usage = 4000000000,    max_rows_to_read = 10000000000;报表服务应设置查询超时、最大扫描量、最大内存、并发队列和只读权限。查询限流不是刁难用户，而是保住高峰期的确定性。25.10 常见问题定位            现象      常见原因                  CPU 高      大扫描、解压、高基数聚合              内存高      分组多、JOIN、窗口              IO 高      读取列过多、分区未剪枝              网络高      分布式结果集过大              抖动      并发查询、后台合并、副本同步              偶发超时      磁盘争抢、队列等待      本章小结查询优化围绕扫描量展开：先分区剪枝，再匹配排序键，再减少列和行，最后考虑预聚合、投影、字典和资源限制。用 query_log 建立慢查询台账，避免凭感觉调参。思考题  为什么 read_bytes 比单次耗时更能反映查询成本？  如何验证分区剪枝？  uniqExact 有哪些替代方案？  深分页如何优化？  如何防止报表用户拖垮集群？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 建模围绕宽表、明细、汇总和维度展开。目标是让高频查询少扫描、少 JOIN、少运行时转换。24.1 分层ODS：原始明细，保留字段和可重放信息DWD：清洗明细，统一类型和枚举DWS：主题汇总，常用维度组合ADS：报表结果，直接服务应用DIM：维表和字典示例：            层      表                  ODS      kafka raw events              DWD      events_local              DWS      city_daily_metrics              ADS      realtime_dashboard              DIM      city_dict、product_dim      24.2 明细表CREATE TABLE analytics.dwd_events(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    device_id String,    event_type LowCardinality(String),    platform LowCardinality(String),    city_id UInt32,    channel LowCardinality(String),    amount Decimal64(2),    props Map(String, String))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, channel, user_id)TTL event_date + INTERVAL 13 MONTH;设计原则：  高频过滤字段普通列化；  低基数字段使用 LowCardinality；  金额使用 Decimal；  时间字段明确时区；  props 只放低频扩展属性；  生命周期通过 TTL 和分区表达。24.3 汇总表CREATE TABLE analytics.dws_city_daily(    stat_date Date,    city_id UInt32,    channel LowCardinality(String),    pv UInt64,    pay_count UInt64,    gmv Decimal64(2))ENGINE = SummingMergeTreePARTITION BY toYYYYMM(stat_date)ORDER BY (stat_date, city_id, channel);查询：SELECT stat_date, city_id, sum(pv), sum(gmv)FROM analytics.dws_city_dailyWHERE stat_date &gt;= today() - 7GROUP BY stat_date, city_id;汇总粒度来自高频报表，不是为了消灭明细。没有明细的口径，后期很难追溯。24.4 状态表CREATE TABLE analytics.dwd_order(    order_date Date,    order_id String,    user_id UInt64,    order_status LowCardinality(String),    amount Decimal64(2),    updated_at DateTime,    version UInt64)ENGINE = ReplacingMergeTree(version)PARTITION BY toYYYYMM(order_date)ORDER BY (order_date, order_id);查询最新状态：SELECT    order_id,    argMax(order_status, version) AS status,    argMax(amount, version) AS amount,    max(updated_at) AS updated_atFROM analytics.dwd_orderWHERE order_date = today()GROUP BY order_id;状态表要固定业务日期，避免同一订单跨分区或跨分片导致去重失败。24.5 维表与字典CREATE DICTIONARY analytics.city_dict(    city_id UInt64,    city_name String,    province_id UInt32)PRIMARY KEY city_idSOURCE(CLICKHOUSE(TABLE 'city_dim'))LIFETIME(MIN 300 MAX 600)LAYOUT(HASHED());查询：SELECT    dictGet('analytics.city_dict', 'city_name', toUInt64(city_id)) AS city_name,    count()FROM analytics.dwd_eventsGROUP BY city_name;小而稳定的维表适合字典；大维表或缓慢变化维表建议使用版本化维表和宽表策略。24.6 宽表与 JOIN宽表减少查询时 JOIN：events + city + product + campaign -&gt; event_wide冗余字段选择标准：  查询频率高；  变化不频繁；  类型稳定；  对存储影响可接受；  不引起口径混乱。易变字段保留版本或查询时关联，避免宽表频繁重写。24.7 指标口径指标必须定义清楚：            字段      示例                  指标名      付费 GMV              聚合      sum(amount)              过滤条件      status = PAID              去重规则      订单号唯一              时间口径      支付时间              时区      Asia/Shanghai              延迟处理      支持重跑窗口      同一指标在明细表和汇总表必须可对账。对账 SQL 应成为上线检查项，而不是事后人工记忆。24.8 建模流程1. 收集业务问题2. 定义指标和维度3. 设计 DWD 明细4. 选择表引擎5. 设计分区和排序6. 构建 DWS 汇总7. 建立字典和权限8. 对账和压测9. 接入查询服务本章小结ClickHouse 建模以宽表和明细为主，辅以主题汇总、状态表和字典。高频字段普通列化，可累加指标预聚合，状态数据用版本去重。指标口径和可对账性比表数量更重要。思考题  ODS、DWD、DWS 分别解决什么问题？  哪些字段应该从 Map 提升为普通列？  状态表为什么用 ReplacingMergeTree？  宽表冗余维度的取舍是什么？  如何保证汇总表和明细表口径一致？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。实时链路的目标不是把所有数据尽快写入，而是在延迟、成本、准确性和可恢复性之间找到稳定平衡。23.1 常见架构直接消费：业务 / 埋点  -&gt; Kafka  -&gt; ClickHouse Kafka 引擎  -&gt; MergeTree适合格式稳定、清洗简单、延迟要求不极端的场景。流计算清洗：Kafka raw  -&gt; Flink  -&gt; Kafka clean  -&gt; ClickHouse适合乱序、状态计算、维表补齐、多流 Join 和复杂异常处理。混合链路：明细 -&gt; ClickHouse汇总 -&gt; Flink / ClickHouse 物化视图检索 -&gt; Elasticsearch离线 -&gt; 数据湖不同系统承担不同语义，不强行用一个系统覆盖所有需求。23.2 延迟定义必须拆开指标：            指标      含义                  upstream delay      业务发生到 Kafka              consume delay      Kafka 到处理系统              write delay      处理系统到 ClickHouse              merge delay      合并或去重完成              query delay      查询可见      ReplacingMergeTree 数据通常很快可见，但物理去重可能滞后。报表如果要求最新状态，应查询 argMax 语义，而不是等待合并完成。23.3 写入缓冲推荐写入端微批：最大行数：50000最大间隔：2s失败重试：指数退避最大重试：有限次数死信：本地文件或 Kafka retry topicFlink 伪代码：sink.bufferMaxRows = 50000;sink.bufferInterval = Duration.ofSeconds(2);sink.failurePolicy = RetryThenDeadLetter;缓冲越大吞吐越高，但故障时的损失窗口也越大。金融、计费等关键数据要配合外部可重放机制。23.4 乱序与水印事件时间可能乱序：10:00:0310:00:0110:00:02处理原则：  Flink 使用 watermark 和窗口；  ClickHouse 使用事件日期分区，不用处理时间替代业务日期；  允许延迟数据写入历史分区；  报表支持迟到数据重跑；  状态流用版本表达顺序。23.5 维表补齐            位置      特点                  上游写入前      数据完整，但维表变更复杂              Flink Lookup      实时性好，需缓存和 TTL              ClickHouse 字典      查询时补齐，适合小维表              每日宽表      简单稳定      高频维度建议写入时固化；缓慢变化维度可用字典或版本化维表。23.6 数据质量数据质量关卡：schema 校验必填校验类型转换枚举值校验时间合法性主键唯一性金额范围去重规则分区边界治理结果表：CREATE TABLE analytics.data_quality(    check_time DateTime,    topic String,    rule_name String,    error_count UInt64,    sample String)ENGINE = MergeTreePARTITION BY toYYYYMM(check_time)ORDER BY (check_time, topic, rule_name);23.7 幂等设计典型规则：事件流：event_id + 去重窗口状态流：business_key + version指标流：窗口 + 维度 + 可重放幂等常用策略：  Kafka offset 记录；  批次 ID；  业务版本；  事件唯一 ID；  分区重写；  死信重放。23.8 监控关键链路指标：            层      指标                  业务      事件量、成功率              Kafka      lag、分区均衡、错误              计算      反压、checkpoint、重启              ClickHouse      写入行数、part、合并              查询      延迟、错误、扫描量              业务结果      核心指标对账      告警示例：lag &gt; 5 分钟写入失败率 &gt; 1%目标表行数环比异常too many parts 接近阈值消费者消失核心报表空结果23.9 回压与降级当 ClickHouse 压力过大：  写入端降低并发；  增大批次间隔；  暂停低优先级 topic；  依靠 Kafka 保留数据等待恢复；  查询限流；  关闭高成本报表；  保留死信和重放入口。不要在未定位原因时直接清空队列或重启集群。先确认是 IO、CPU、内存、part 还是依赖故障。本章小结实时链路要把数据契约、缓冲写入、乱序处理、幂等、监控和降级一起设计。简单链路可以直接用 Kafka 引擎，复杂语义交给 Flink 等流计算层。ClickHouse 的职责是稳定批量落表和高效分析。思考题  直接消费和流计算清洗如何选择？  事件延迟包含哪几段？  为什么要用事件时间分区？  维表补齐有哪些方案？  写入压力过大时如何降级？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Kafka 表引擎让 ClickHouse 直接消费 Kafka 消息，通常配合物化视图写入 MergeTree 表。它是实时链路入口，不是长期存储。22.1 架构Kafka topic    |Kafka engine table    |Materialized View    |MergeTree / ReplacingMergeTreeKafka 表负责读取消息，物化视图负责转换，目标表负责存储和查询。22.2 创建表CREATE TABLE analytics.kafka_events(    event_time DateTime,    event_id String,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    amount Decimal64(2))ENGINE = KafkaSETTINGS    kafka_broker_list = 'kafka-01:9092,kafka-02:9092',    kafka_topic_list = 'events',    kafka_group_name = 'clickhouse-analytics',    kafka_format = 'JSONEachRow',    kafka_row_delimiter = '\n',    kafka_num_consumers = 4,    kafka_thread_per_consumer = 1,    kafka_max_block_size = 65536,    kafka_skip_broken_messages = 10;常用设置：            设置      说明                  kafka_broker_list      Kafka 地址              kafka_topic_list      topic，可多个              kafka_group_name      消费组              kafka_format      消息格式              kafka_num_consumers      消费者数量              kafka_max_block_size      批大小              kafka_skip_broken_messages      跳过坏消息数量      22.3 目标表和视图CREATE TABLE analytics.events_local(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id);CREATE MATERIALIZED VIEW analytics.events_mvTO analytics.events_localASSELECT    toDate(event_time) AS event_date,    event_time,    event_id,    user_id,    event_type,    city_id,    amountFROM analytics.kafka_events;22.4 消费位点ClickHouse 使用 Kafka consumer group 保存位点。位点提交语义、重平衡和可见性与 Kafka 客户端版本及设置相关。必须监控：  consumer group lag；  topic 分区数；  消费者数量；  消息格式错误；  ClickHouse 写入异常；  目标表行数。查看消费者：SELECT database, table, consumer_id, assignments.topicFROM system.kafka_consumers;22.5 消费者数量有效消费者数量不能超过 topic 分区数：topic partitions = 12kafka_num_consumers = 4增加 consumers 不一定提升吞吐。还要确认 ClickHouse 资源、目标表写入批次、Kafka 拉取配置、网络带宽和单分区乱序影响。22.6 数据格式JSONEachRow 示例：{"event_time":"2026-08-25 10:00:00","event_id":"E001","user_id":1001,"event_type":"pay","city_id":1,"amount":99.00}Avro、Protobuf 需要相关格式配置和 schema 管理，且版本兼容性要提前验证。生产建议：  上游发送标准化字段；  时间统一时区；  金额用字符串或定点格式；  必填字段稳定；  schema 演进向后兼容。22.7 异常消息kafka_skip_broken_messages 只跳过有限坏消息，不是通用容错方案。更稳妥的链路：Kafka raw topic   |解析服务 / Flink   |valid topic + dead letter topic   |ClickHouse如果直接消费 raw topic，建议至少保存原始消息、错误原因、topic、partition、offset、处理时间和重试状态。22.8 管理操作停止和恢复消费：SYSTEM STOP CONSUMERS analytics.kafka_events;SYSTEM START CONSUMERS analytics.kafka_events;修改 Kafka 表通常需要删除重建。操作顺序：1. 停止写入或记录位点2. 停止 consumers3. 删除物化视图4. 删除 Kafka 表5. 重建 Kafka 表6. 重建物化视图7. 校验无重复无缺失22.9 幂等与重复Kafka 至少一次投递常见，重放会造成重复。            目标表      方案                  MergeTree      事件 ID 查询去重或接受重复              ReplacingMergeTree      业务键 + 版本              CollapsingMergeTree      正负状态记录              SummingMergeTree      指标可累加且重放可控      对账：SELECT event_id, count()FROM analytics.events_localWHERE event_date = today()GROUP BY event_idHAVING count() &gt; 1;22.10 排查            问题      排查                  无数据      group 位点、topic、格式、视图              lag 增长      分区数、消费者、写入压力              坏消息多      schema、字段类型、分隔符              重复      重放或视图重建              缺失      位点越界、坏消息跳过              消费停止      system.kafka_consumers、日志      本章小结Kafka 引擎把消费、转换和存储串成一条链路。生产关键是格式契约、消费位点、坏消息处理、幂等写入和 lag 监控。复杂清洗和乱序处理放在流计算层，ClickHouse 保持批量落表和高效分析。思考题  Kafka 引擎表保存长期数据吗？  为什么需要物化视图？  消费者数量如何确定？  如何处理重复消息？  lag 增长时按什么顺序排查？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。副本处理节点故障，备份处理误删、逻辑错误、集群级故障和合规留存。两者不能互相替代。21.1 需要备份的内容            内容      说明                  表结构      CREATE TABLE、视图、字典、权限              数据 part      分区数据文件              配置文件      config、users、存储策略              集群拓扑      remote_servers、宏、Keeper 配置              任务定义      Kafka 表、物化视图、定时任务              恢复手册      操作步骤和联系人      21.2 备份策略            策略      适合                  分区 FREEZE      单机或小规模自管              clickhouse-backup      常用工具链              对象存储快照      云环境或冷备              远程集群复制      灾备环境              元数据加数据目录      自定义恢复      备份级别：全量：每周或每周期增量：新分区、新 part元数据：高频或每次变更21.3 FREEZE冻结分区：ALTER TABLE analytics.events_localFREEZE PARTITION '202608';指定备份名称：ALTER TABLE analytics.events_localFREEZE PARTITION '202608' WITH NAME 'daily_20260825';冻结不是备份完成。还需要把冻结目录同步到独立存储：rsync -a /var/lib/clickhouse/shadow/daily_20260825/ \  backup-host:/backup/clickhouse/daily_20260825/21.4 恢复分区准备恢复表结构后，将备份 part 放入 detached 目录，再 ATTACH。ALTER TABLE analytics.events_localATTACH PART 'part_name';也可以使用 clickhouse-backup 等工具恢复完整表。关键校验：SELECT count() FROM analytics.events_local;SELECT event_date, count(), sum(amount)FROM analytics.events_localGROUP BY event_date;21.5 元数据备份导出建表语句：SELECT database, table, create_table_queryFROM system.tablesWHERE database = 'analytics';导出权限：SELECT * FROM system.users;SELECT * FROM system.roles;SELECT * FROM system.grants;具体系统表字段因版本而异。权限也可以通过访问控制目录备份，但恢复前要确认版本兼容。21.6 clickhouse-backup常用流程：clickhouse-backup create daily_20260825clickhouse-backup upload daily_20260825clickhouse-backup list remoteclickhouse-backup download daily_20260825clickhouse-backup restore daily_20260825工具版本必须与 ClickHouse 版本和部署方式匹配。生产使用前要在测试环境验证：  大分区备份耗时；  对线上 IO 影响；  对象存储凭据；  恢复速度；  权限和配置恢复；  集群表恢复语义。21.7 分布式集群恢复本地表和 Distributed 表要分别处理：1. 恢复 Keeper 或确认新 Keeper2. 恢复 ClickHouse 配置和宏3. 恢复本地表结构4. 恢复各分片本地数据5. 恢复分布式表定义6. 校验行数和指标7. 恢复写入链路如果分片数量或路由变化，需要重新规划数据分布，不能直接恢复到不一致拓扑。21.8 Kafka 链路恢复Kafka 引擎表恢复前要确认：  topic 保留期；  consumer group 位点；  物化视图是否重建；  数据边界；  重复消费处理；  死信队列。常见选择：            方案      说明                  从备份时间点继续      需要位点可恢复              从 Kafka earliest 重放      可能重复              从上游补数      需要幂等              接受缺失      通常只适合可丢数据      21.9 恢复演练备份必须定期演练：1. 搭建隔离环境2. 恢复元数据3. 恢复数据4. 校验行数和指纹5. 执行核心报表6. 记录耗时7. 更新恢复手册8. 检查权限和配置21.10 故障场景            故障      恢复来源                  单副本磁盘损坏      健康副本              误删分区      备份              错误 UPDATE      备份或上游重放              元数据损坏      元数据备份              集群级故障      灾备环境              依赖组件故障      Kafka 或上游补数      本章小结备份要覆盖表结构、数据、配置、权限和恢复步骤。FREEZE 适合理解原理，生产常用工具和对象存储体系化处理。备份的价值由恢复演练证明，没有演练过的备份不能视为可用。思考题  为什么副本不能替代备份？  FREEZE 后还需要做什么？  分布式集群恢复有哪些步骤？  Kafka 链路如何处理重放和重复？  如何验证恢复后的数据正确性？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。数据均衡不仅看磁盘容量，还要看写入吞吐、查询负载、part 数量、合并压力和副本延迟。ClickHouse 不会自动满足所有维度的“均衡”，需要容量规划和运维机制。20.1 均衡维度            维度      观察                  磁盘空间      分片和磁盘剩余量              写入量      行数、字节、批次              查询量      QPS、扫描量、CPU              part 数      合并压力              副本队列      同步延迟              网络      分片间和应用流量      只按磁盘均衡，可能出现某分片查询 CPU 持续过热。20.2 查看容量SELECT    name,    path,    free_space,    total_space,    keep_free_spaceFROM system.disksORDER BY free_space;按表查看：SELECT    database,    table,    sum(bytes_on_disk) AS bytes,    sum(rows) AS rows,    count() AS partsFROM system.partsWHERE activeGROUP BY database, tableORDER BY bytes DESC;20.3 查看写入分布SELECT event_date, cityHash64(user_id) % 4 AS bucket, count()FROM analytics.events_allGROUP BY event_date, bucketORDER BY event_date, bucket;真实集群应采集各节点本地表的写入指标，并结合监控系统的磁盘吞吐、CPU 和网络曲线判断。20.4 写入热点常见原因：  大租户或大用户集中；  分片键基数低；  路由表达式倾斜；  集群权重配置不当；  某分片副本异常；  应用直连配置错误。处理：  重新评估分片键；  大租户独立路由；  使用 hash 后均匀桶；  调整写入并发；  修复异常副本；  规划数据迁移。20.5 查询热点SELECT    query,    count() AS executions,    sum(read_rows) AS total_read_rows,    avg(query_duration_ms) AS avg_msFROM system.query_logWHERE event_date &gt;= today() - 1GROUP BY normalized_query_hash, queryORDER BY total_read_rows DESCLIMIT 20;处理方式：  慢查询优化；  预聚合；  读写入口分离；  查询排队；  限制扫描量；  大报表错峰；  增加只读副本。20.6 分片权重远程服务器分片可以配置权重：&lt;shard&gt;    &lt;weight&gt;2&lt;/weight&gt;    &lt;replica&gt;...&lt;/replica&gt;&lt;/shard&gt;权重影响写入或查询分布的具体行为与版本和使用方式有关。它不能解决业务键倾斜，也不能替代路由测试。20.7 扩容方式常见策略：            策略      说明                  只加副本      提升读吞吐和可用性，不增加分片存储              加分片      提升写入和存储容量，需要路由规划              混合扩容      常见生产方案              迁移热点表      隔离大租户或高负载业务              冷热分层      历史数据移动到 cheaper 存储      加副本相对简单；加分片必须确认数据路由是否变化。20.8 数据迁移安全流程：1. 停止或暂停目标表写入2. 记录数据边界3. 新集群建表4. 分批复制历史分区5. 校验行数、指纹和指标6. 切换写入7. 观察查询8. 保留回滚窗口复制分区：INSERT INTO new_cluster.analytics.events_localSELECT *FROM old_cluster.analytics.events_localWHERE event_date = '2026-08-01';跨集群远程查询要控制并发和批大小，避免影响生产。20.9 冷热分层ClickHouse 支持存储策略，可以把热数据放 SSD，冷数据放对象存储或机械盘。配置与版本强相关，上线前要验证：  移动策略；  查询性能；  对象存储权限；  网络带宽；  故障恢复；  成本模型。20.10 容量规划估算公式：原始日志大小× 写入放大× 副本数÷ 压缩率+ 索引和投影+ 合并临时空间+ 预留空间= 需要磁盘容量建议预留：  合并临时空间；  备份窗口；  查询临时文件；  升级回滚；  流量增长；  数据重放。本章小结数据均衡是容量、写入、查询、合并和网络的综合治理。分片键决定长期分布，副本影响读能力，存储策略影响成本。扩容前必须明确路由是否变化，并准备分批迁移、校验和回滚方案。思考题  磁盘均衡是否代表系统均衡？  写入热点如何定位？  加副本和加分片分别解决什么问题？  分片路由变化有什么风险？  容量规划为什么要预留合并空间？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Keeper 或 ZooKeeper 是 ClickHouse 集群的协调层，用于副本日志、块去重信息、leader 选举、分布式 DDL 队列和任务锁。19.1 协调内容/clickhouse  /tables    /{shard}      /table        /replicas          /replica-1          /replica-2        /leader        /blocks        /log  /task_queue  /ddl不同版本的路径布局和实现细节可能调整，核心是保存集群协调状态，而不是业务数据。19.2 Keeper 部署生产建议至少三节点：keeper-01  follower / leaderkeeper-02  follower / leaderkeeper-03  follower / leader要求：  低延迟网络；  SSD 和独立资源；  时钟同步；  独立监控；  定期备份配置；  不与其他大 IO 服务混部。19.3 配置示例&lt;clickhouse&gt;    &lt;keeper_server&gt;        &lt;tcp_port&gt;9181&lt;/tcp_port&gt;        &lt;server_id&gt;1&lt;/server_id&gt;        &lt;raft_configuration&gt;            &lt;server&gt;                &lt;id&gt;1&lt;/id&gt;                &lt;hostname&gt;keeper-01&lt;/hostname&gt;                &lt;port&gt;9234&lt;/port&gt;            &lt;/server&gt;            &lt;server&gt;                &lt;id&gt;2&lt;/id&gt;                &lt;hostname&gt;keeper-02&lt;/hostname&gt;                &lt;port&gt;9234&lt;/port&gt;            &lt;/server&gt;            &lt;server&gt;                &lt;id&gt;3&lt;/id&gt;                &lt;hostname&gt;keeper-03&lt;/hostname&gt;                &lt;port&gt;9234&lt;/port&gt;            &lt;/server&gt;        &lt;/raft_configuration&gt;    &lt;/keeper_server&gt;&lt;/clickhouse&gt;ClickHouse 节点连接协调服务：&lt;zookeeper&gt;    &lt;node&gt;        &lt;host&gt;keeper-01&lt;/host&gt;        &lt;port&gt;9181&lt;/port&gt;    &lt;/node&gt;    &lt;node&gt;        &lt;host&gt;keeper-02&lt;/host&gt;        &lt;port&gt;9181&lt;/port&gt;    &lt;/node&gt;&lt;/zookeeper&gt;19.4 元数据与数据文件ClickHouse 元数据包含：database metadatatable create statementcolumn definitionspart informationreplication logaccess control本地数据目录保存实际列文件和索引。恢复集群时必须同时考虑：  元数据；  数据 part；  Keeper 路径；  用户权限；  集群配置；  分布式表定义。19.5 分布式 DDLON CLUSTER 语句写入 DDL 队列，各节点异步执行。查看队列：SELECT    entry,    host,    status,    query,    exceptionFROM system.distributed_ddl_queueORDER BY entry DESCLIMIT 20;常见失败：  节点不可达；  宏缺失；  权限不足；  表已存在；  版本不兼容；  磁盘满；  Keeper 超时。19.6 副本与 Keeper查看副本状态：SELECT    database,    table,    replica_name,    is_readonly,    queue_size,    absolute_delayFROM system.replicas;如果 Keeper 不可用：  已有数据仍可能可读；  写入和复制协调会受影响；  副本可能进入只读；  DDL 可能失败；  集群不要在此时做拓扑变更。19.7 四字命令常用命令：            命令      用途                  ruok      基本存活              stat      版本和连接概览              mntr      监控指标              srvr      服务端信息              conf      配置              isro      是否只读      示例：echo mntr | nc keeper-01 9181命令支持与安全限制依版本和安全模式而异，生产环境应只允许可信网络访问。19.8 监控要点            指标      含义                  leader_count      leader 数量              follower_count      follower 数量              outstanding_requests      积压请求              avg_latency      平均延迟              max_latency      最大延迟              open_file_descriptor_count      文件句柄              disk_free_bytes      磁盘空间              alive_connections      连接数      ClickHouse 侧关注：SELECT countIf(is_readonly) AS readonly_replicas,       sum(queue_size) AS queue_sizeFROM system.replicas;19.9 故障处理少数节点故障多数派仍可用，替换故障节点后加入集群。多数派不可用协调服务不可用，此时不要盲目重启或清空数据。保护现场，恢复多数派或使用备份。脑裂与隔离确保 Quorum 配置正确，节点网络分区时少数派不能提供服务。跨机房部署要评估延迟和故障域。19.10 备份与迁移必须备份：  Keeper 配置；  ClickHouse config；  users 配置；  元数据 SQL；  数据快照；  集群拓扑记录。迁移 Keeper 时应：  逐台替换；  确认多数派健康；  观察 ClickHouse 副本状态；  更新连接配置；  保留回滚窗口。本章小结Keeper 是副本和集群协调的核心，故障影响写入、复制和 DDL。生产上要独立部署、监控 Quorum 和延迟，并把 Keeper 状态、ClickHouse 元数据与数据 part 作为一个整体来制定备份恢复方案。思考题  Keeper 保存业务数据吗？  为什么建议奇数节点？  Keeper 不可用时副本会有什么表现？  分布式 DDL 为什么可能部分成功？  集群恢复为什么要同时考虑元数据和数据？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。分片键决定数据分布在哪个物理分片上。好的分片键能让数据均衡、查询并行、业务键稳定；差的分片键会带来热点、倾斜、跨分片查询和数据迁移难题。18.1 为什么分片键重要events_all  shard 1: user_id hash % 3 = 0  shard 2: user_id hash % 3 = 1  shard 3: user_id hash % 3 = 2影响：            维度      影响                  数据均衡      各分片磁盘和写入量              查询并行      单分片或全分片扫描              业务局部性      同键数据是否同分片              扩容成本      路由变化是否需要迁移              聚合语义      去重和状态合并      18.2 常见分片键随机：ENGINE = Distributed(..., rand())按用户：ENGINE = Distributed(..., cityHash64(user_id))按订单：ENGINE = Distributed(..., sipHash64(order_id))按用户和日期：ENGINE = Distributed(..., cityHash64(user_id, event_date))按业务城市：ENGINE = Distributed(..., city_id)业务字段直连路由容易倾斜，通常更推荐 hash 后由分片权重处理。18.3 选择流程1. 明确查询是否必须按某键聚焦2. 检查键的基数和均匀性3. 检查热点键占比4. 确认同键数据是否需要同分片5. 评估未来扩容6. 测试写入和查询分布7. 记录路由规则18.4 数据倾斜检查SELECT    shardNum() AS shard,    count() AS rows,    sum(amount) AS gmvFROM analytics.events_allWHERE event_date = today()GROUP BY shardORDER BY shard;如果只查本地表，需要在各节点分别执行。更实际的监控方式是采集：            指标      说明                  每分片写入行数      写入热点              每分片写入字节      网络和磁盘热点              每分片磁盘用量      容量倾斜              每分片 CPU      计算热点              每分片 part 数      合并压力      18.5 查询局部性如果查询常按 user_id：SELECT *FROM analytics.events_allWHERE user_id = 1001;使用 cityHash64(user_id) 可以定位一个分片。如果查询按 order_id，而分片键是 user_id，则订单可能分布多分片。对 ReplacingMergeTree 或 CollapsingMergeTree，同一业务键跨分片还会破坏合并语义。18.6 可聚合性优先选择可以让聚合下发的分片方式：            指标      跨分片合并                  sum      可合并              count      可合并              min / max      可合并              avg      需 sum + count              uniqExact      需合并精确集合，成本高              uniq      可合并近似状态              quantile      可合并状态      全局精确去重是最容易出问题的查询。可用方案：  保证去重键与分片键一致；  使用近似 uniq；  离线全局计算；  维护 bitmap 或 HLL 状态；  专用去重服务或引擎。18.7 扩容与路由稳定性传统 hash 取模在分片数变化时会导致大量数据路由改变：hash(key) % 3 -&gt; hash(key) % 5ClickHouse 的加权分片和路由实现与版本、集群配置有关。规划扩容时应评估：  新旧路由规则；  历史数据是否迁移；  查询是否跨新旧分片；  业务键合并语义；  切换窗口；  回滚方案。18.8 复合分片键cityHash64(user_id, event_date)优点：  分布更均匀；  降低单用户热点；缺点：  按 user_id 查询无法定位单分片；  同一用户数据分散；  状态合并更复杂。如果业务需要用户状态表，不要为了均衡拆散业务键。可使用用户分桶，让一个桶内用户仍然聚集。18.9 特殊场景时间序列按时间分片会把写入集中到最新分片，通常不建议。时间主要交给分区，分片键选择实体 ID。多租户小租户可以 hash 后均匀分布；大租户需要独立处理，例如独立集群、独立分片或逻辑表。状态表订单、用户状态等 Replacing/Collapsing 表，应保证业务键稳定路由到同一分片。18.10 分片键评估表            候选键      评估                  rand      均衡，但无法聚焦查询              user_id      适合用户行为和状态              order_id      适合订单明细              device_id      适合设备分析              event_date      容易热点，不适合              city_id      低基数且倾斜              tenant_id      大租户易热点      本章小结分片键设计要同时考虑数据均衡、查询局部性和业务语义。时间交给分区，实体 ID 常用于 hash 分片；状态表必须保证业务键稳定路由。上线前用写入、磁盘、查询和去重场景验证，不要只看理论均匀。思考题  随机分片有什么优缺点？  为什么 event_date 不适合做分片键？  业务状态表对分片键有什么要求？  如何发现数据倾斜？  扩容时为什么要评估路由稳定性？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。分布式表是分片集群的统一入口。它本身不保存业务数据，而是负责把写入路由到分片，把查询下发到各分片本地表，再合并结果。17.1 架构             Distributed table: events_all                        |       +----------------+----------------+       |                |                |    shard-1          shard-2          shard-3 events_local      events_local      events_local       |  replica-1  replica-2本地表是真实存储，分布式表是路由和查询协调层。17.2 创建本地表在每个分片上创建：CREATE TABLE analytics.events_local(    event_date Date,    event_time DateTime,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    amount Decimal64(2))ENGINE = ReplicatedMergeTree(    '/clickhouse/tables/{shard}/events',    '{replica}')PARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id);可以使用 ON CLUSTER：CREATE TABLE analytics.events_localON CLUSTER analytics_cluster(    event_date Date)ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')ORDER BY event_date;集群 DDL 要先在测试环境验证宏配置和权限。17.3 创建分布式表CREATE TABLE analytics.events_allAS analytics.events_localENGINE = Distributed(    analytics_cluster,    analytics,    events_local,    cityHash64(user_id));参数含义：            参数      含义                  analytics_cluster      集群名              analytics      本地数据库              events_local      本地表              cityHash64(user_id)      分片键表达式      常用分片函数：rand()cityHash64(user_id)cityHash64(user_id, event_date)sipHash64(order_id)17.4 写入路径通过 Distributed 写入：应用 -&gt; 任一 ClickHouse 节点     -&gt; 计算目标分片     -&gt; 异步或同步转发     -&gt; 各分片本地表优点是使用简单；缺点是写入节点会承担转发压力，并可能出现本地 buffer 数据。写入端直连分片更可控，但需要应用或代理层维护拓扑。17.5 查询路径客户端  -&gt; Distributed 表  -&gt; 每个 shard 选择副本  -&gt; 本地表并行执行  -&gt; 返回中间结果  -&gt; 发起节点合并查看集群：SELECT cluster, shard_num, replica_num, host_name, is_localFROM system.clustersWHERE cluster = 'analytics_cluster';17.6 聚合语义可下发聚合：SELECT event_date, city_id, count(), sum(amount)FROM analytics.events_allWHERE event_date = today()GROUP BY event_date, city_id;每个分片先算局部结果，再由发起节点合并。需要注意：  countDistinct(user_id) 跨分片需合并集合；  uniq(user_id) 可以合并近似状态；  avg 不能直接对各分片平均值求平均；  min/max/sum 可合并；  LIMIT 可能需要各分片返回局部 TopN。17.7 分布式 DDLCREATE TABLE analytics.events_local ON CLUSTER analytics_cluster(    event_date Date,    user_id UInt64)ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')ORDER BY event_date;查看队列：SELECT cluster, query, host, status, exception_codeFROM system.distributed_ddl_queueORDER BY entry DESCLIMIT 20;集群 DDL 超时或部分节点失败时，要检查各节点实际表结构，不能只看返回结果。17.8 负载均衡与副本常用做法：  应用通过负载均衡访问 ClickHouse；  Distributed 表选择分片副本；  查询节点和存储节点分离；  长查询和短查询隔离；  限流和排队。如果发起节点承担所有合并和结果输出，它可能先成为瓶颈。大规模报表可以引入独立查询入口和资源池。17.9 运维操作查看远程文件系统缓存、分布式队列和 part：SELECT database, table, is_blocked, error_count,       data_files, bytes_to_sendFROM system.distribution_queue;修复分布式表：SYSTEM STOP DISTRIBUTED SENDS analytics.events_all;SYSTEM START DISTRIBUTED SENDS analytics.events_all;当写入积压时，先检查目标分片、网络、磁盘、Too many parts 和权限，而不是盲目重启。17.10 常见错误            问题      原因                  查询缺少数据      分片路由变化              数据倾斜      分片键选择不当              某分片慢      副本延迟或磁盘热点              DDL 部分失败      节点不可达或权限异常              写入队列积压      异步转发受阻              结果不一致      查询到延迟副本      本章小结分布式表把多个分片的本地表组织成统一查询入口，分片键决定数据分布，副本提供可用性。使用时要理解写入转发、查询合并、分布式 DDL 和故障语义。分布式表不是高可用本身，稳定的集群来自分片、副本、监控和容量规划共同作用。思考题  Distributed 表保存业务数据吗？  如何选择分片键？  跨分片 avg 为什么不能直接平均？  分布式 DDL 部分失败如何排查？  应用直连分片和写 Distributed 表各有什么取舍？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。副本用于提高可用性、读吞吐和故障恢复能力。ClickHouse 的 ReplicatedMergeTree 依赖 Keeper 或 ZooKeeper 维护副本日志、块信息和leader 状态。16.1 副本架构              Keeper / ZooKeeper                    |      +-------------+-------------+      |             |             |  replica-1     replica-2     replica-3      |             |             |   local part    local part    local part副本之间通过复制日志同步元数据，并从源副本拉取数据 part。16.2 ReplicatedMergeTreeCREATE TABLE analytics.events_replicated(    event_date Date,    event_time DateTime,    user_id UInt64,    event_type LowCardinality(String),    amount Decimal64(2))ENGINE = ReplicatedMergeTree(    '/clickhouse/tables/{shard}/events',    '{replica}')PARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, user_id);路径要求：  同一分片内各副本路径相同；  不同分片路径不同；  不同表路径不同；  replica 名称唯一；  宏配置在每台节点正确。16.3 写入复制客户端写 replica-1        |        vreplica-1 创建 part        |        v写入复制日志到 Keeper        |        vreplica-2 / replica-3 感知并拉取 part副本写入是异步同步过程。写入成功返回点、part 副本数和一致性语义要结合设置与故障场景确认，不能简单等同于同步多副本持久化。16.4 副本引擎家族            本地引擎      副本版本                  MergeTree      ReplicatedMergeTree              ReplacingMergeTree      ReplicatedReplacingMergeTree              SummingMergeTree      ReplicatedSummingMergeTree              CollapsingMergeTree      ReplicatedCollapsingMergeTree              VersionedCollapsingMergeTree      ReplicatedVersionedCollapsingMergeTree      先确定合并语义，再选择 Replicated 变体。16.5 查询副本选择Distributed 表可以配置负载均衡：&lt;remote_servers&gt;    &lt;analytics_cluster&gt;        &lt;shard&gt;            &lt;replica&gt;                &lt;host&gt;ch-01&lt;/host&gt;                &lt;port&gt;9000&lt;/port&gt;            &lt;/replica&gt;            &lt;replica&gt;                &lt;host&gt;ch-02&lt;/host&gt;                &lt;port&gt;9000&lt;/port&gt;            &lt;/replica&gt;        &lt;/shard&gt;    &lt;/remote_servers&gt;&lt;/remote_servers&gt;常见策略包括随机、轮询、最近主机、错误惩罚等。不同版本和配置支持有差异。生产上要确保：  不把查询集中到单个副本；  异常副本能被隔离；  延迟副本不提供过期结果；  查询超时可控制。16.6 副本状态SELECT    database,    table,    zookeeper_path,    replica_name,    is_readonly,    is_session_expired,    queue_size,    inserts_in_queue,    merges_in_queue,    absolute_delayFROM system.replicas;关键字段：            字段      含义                  is_readonly      副本只读              queue_size      待处理复制任务              absolute_delay      副本延迟              is_session_expired      Keeper 会话异常              log_pointer      复制日志位置      16.7 副本队列查看任务：SELECT    database,    table,    replica_name,    type,    source_replica,    create_time,    num_tries,    last_exceptionFROM system.replication_queueORDER BY create_time;常见任务类型：            类型      说明                  GET_PART      拉取缺失 part              MERGE      执行合并              MUTATE      变更数据              DROP_RANGE      删除范围              ALTER_METADATA      元数据变更      16.8 Keeper 与 ZooKeeperKeeper 是 ClickHouse 内置的高可用协调服务，兼容常用 ZooKeeper 协议场景。生产建议：  至少 3 节点；  独立部署或独立资源；  使用低延迟 SSD；  监控会话和延迟；  避免与其他高 IO 服务争抢磁盘。确认连接：SELECT * FROM system.zookeeperWHERE path = '/clickhouse';16.9 故障场景副本只读可能原因：  Keeper 不可达；  会话过期；  元数据冲突；  副本正在恢复；  管理操作设置。排查：SELECT database, table, is_readonly, last_queue_update, last_queue_exceptionFROM system.replicasWHERE is_readonly = 1;队列积压处理顺序：  检查网络和磁盘；  检查源副本是否可用；  查看任务异常；  暂停高压写入；  修复后等待同步；  必要时从健康副本重建。Part 损坏优先从健康副本拉取。单副本表需要备份恢复。不要在未确认路径和备份的情况下手工删除数据目录。16.10 高可用实践  事实表至少两个副本；  Keeper 三节点以上；  副本跨故障域部署；  监控 readonly、queue、delay；  为写入和查询设置超时；  定期演练副本重建；  保留可恢复备份；  升级时逐节点滚动操作。本章小结副本解决可用性和读扩展问题，核心依赖 ReplicatedMergeTree 与协调服务。生产上要同时关注数据同步、队列积压、副本延迟和故障域规划。副本不是分片，也不能替代备份。思考题  ReplicatedMergeTree 依赖什么组件？  副本路径和 replica 名称为什么必须正确？  如何判断副本延迟和队列积压？  副本和分片的区别是什么？  哪些故障需要备份而不是副本恢复？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。向量化执行是 ClickHouse 快的重要原因之一。它把“一行一行解释执行”变成“一批一批计算”，让 CPU、缓存、SIMD 指令和编译优化充分发挥。15.1 行执行与向量化执行行执行：row1.amount + row1.taxrow2.amount + row2.taxrow3.amount + row3.tax向量化执行：amount = [100, 200, 300]tax    = [6, 12, 18]result = amount + tax = [106, 212, 318]差异：            维度      行执行      向量化执行                  处理单位      单行      一批值              指令分发      多      少              CPU cache      不友好      友好              SIMD      难      易              类型特化      一般      强      15.2 列存与向量化列存让同一列的值连续保存：amount.bin: 100, 200, 300, ...执行器可以一次读取一批 amount，直接调用向量化加法、比较或聚合内核。这也是不建议在热点路径频繁使用复杂 UDF、字符串拆散和高分支逻辑的原因：它们会打断批量处理。15.3 查询流水线读取分区  -&gt; 读取列块  -&gt; 解压 / 反序列化  -&gt; 表达式向量化计算  -&gt; 过滤位图  -&gt; 聚合 hash table  -&gt; 排序 / 合并  -&gt; 输出其中任何一个环节都可能成为瓶颈：            瓶颈      现象                  磁盘 IO      read_bytes 高、IO wait 高              解压 CPU      CPU 高、扫描大              聚合内存      memory_usage 高              排序      外部排序或内存不足              网络      分布式节点间传输慢              结果输出      result_rows 大      15.4 压缩块ClickHouse 列文件由压缩块组成。读取时会按需要解压，不一定整列全部解压。影响压缩和读取的因素：  数据类型宽度；  列的局部性；  值重复度；  排序键设计；  Codec 配置；  part 大小。示例：CREATE TABLE analytics.events_codec(    event_date Date CODEC(DoubleDelta, ZSTD(1)),    user_id UInt64 CODEC(T64, ZSTD(1)),    event_type LowCardinality(String),    amount Decimal64(2) CODEC(ZSTD(1)))ENGINE = MergeTreeORDER BY (event_date, event_type, user_id);Codec 选择要测试，不是压缩率越高越好，解压 CPU 也可能成为瓶颈。15.5 max_threads查询线程数可以通过设置控制：SELECT count()FROM analytics.events_localSETTINGS max_threads = 8;线程越多不一定越快：  小查询调度开销增加；  并发查询互相抢占 CPU；  聚合 hash table 内存增加；  分布式查询放大资源消耗。常见策略：            查询类型      线程策略                  小点查      较少线程              大扫描      适度增加              高并发报表      全局排队和限额              后台补数      独立资源池      15.6 聚合执行SELECT city_id, event_type, count(), sum(amount)FROM analytics.events_localGROUP BY city_id, event_type;执行过程：每个线程构建局部聚合状态        |        v合并局部状态        |        v输出最终分组高基数分组会带来：  大量 hash key；  内存增长；  状态合并成本；  溢写或查询失败。优化方向：  减少维度组合；  先过滤再聚合；  使用低基数类型；  预聚合；  近似函数；  限制查询内存。15.7 表达式优化避免对每行做重复复杂表达式：SELECT    toDate(event_time) AS event_date,    toLowerCase(trim(platform)) AS platform_cleanFROM analytics.events_local;更好做法是写入时固化：event_date Date,platform LowCardinality(String)查询只读取已经标准化后的列，减少运行时计算。15.8 外部函数与 UDFClickHouse 支持可执行 UDF 等扩展，但外部调用通常比内置向量化函数慢得多。使用原则：  优先内置函数；  写入侧预处理；  UDF 只处理小规模数据；  控制超时和资源；  避免在 WHERE 中调用高成本外部函数。15.9 观察执行指标SELECT    query_id,    query_duration_ms,    read_rows,    read_bytes,    memory_usage,    ProfileEventsFROM system.query_logWHERE type = 'QueryFinish'ORDER BY query_duration_ms DESCLIMIT 20;关注事件：            指标      含义                  CPU 时间      计算成本              解压字节      解压压力              读取文件数      小文件问题              聚合状态数      分组基数              溢写临时文件      内存压力              网络字节      分布式传输      15.10 编写向量化友好的 SQL建议：  少用 SELECT *；  高频字段类型尽量窄；  低基数字符串用 LowCardinality；  避免高基数字符串分组；  能写入时固化就不要查询时解析；  避免复杂嵌套表达式；  近似场景使用近似聚合；  大结果集分页或落盘导出。本章小结向量化执行依赖列存、连续数据块、类型特化和批量内核。开发者能做的不是手写 SIMD，而是让数据类型、排序、压缩和 SQL 形态更适配批量执行，同时通过查询日志持续观察 CPU、内存和 IO。思考题  为什么列存更适合向量化？  max_threads 是否越大越好？  高基数 GROUP BY 为什么耗内存？  Codec 如何影响 CPU 和磁盘？  哪些 SQL 写法会削弱向量化收益？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。Projection 是表内的一种物理投影，可以按不同排序键或聚合形式保存数据，查询时由优化器选择。它适合解决同一张表存在多种查询模式的问题。14.1 问题背景主表排序：ORDER BY (event_date, event_type, city_id, user_id)适合按日期和事件类型查询，但下面查询效果较弱：SELECT count()FROM analytics.events_localWHERE user_id = 1001;重新设计排序会影响原有查询。Projection 可以在同一表内保留另一份数据排列。14.2 普通投影ALTER TABLE analytics.events_localADD PROJECTION proj_user(    SELECT *    ORDER BY (user_id, event_date));物化历史数据：ALTER TABLE analytics.events_localMATERIALIZE PROJECTION proj_user;查询可以按 user_id 更有效地跳过 granule。投影数据与主表同生共死，TTL、副本和分区仍跟随表结构。14.3 聚合投影ALTER TABLE analytics.events_localADD PROJECTION proj_city_daily(    SELECT        event_date,        city_id,        event_type,        count(),        sum(amount)    GROUP BY event_date, city_id, event_type);适合固定维度汇总：SELECT    event_date,    city_id,    count(),    sum(amount)FROM analytics.events_localWHERE event_date = '2026-08-25'GROUP BY event_date, city_id;当查询表达式与投影兼容时，优化器可以使用聚合投影代替扫描明细。14.4 使用条件Projection 不是缓存，而是额外物理数据。使用前要确认：  查询频率足够高；  扫描收益明显；  写入可承受额外成本；  磁盘可承受额外空间；  表达式与投影兼容；  当前版本优化行为已验证。14.5 与物化视图对比            维度      Projection      Materialized View                  归属      表内部      独立数据库对象              选择方式      优化器选择      写入目标表              灵活性      受投影表达式限制      SQL 更灵活              生命周期      随主表      独立管理              适用      换排序、固定聚合      复杂转换、分发              数据边界      与主表一致      需手动处理回填      简单排序变化和固定聚合优先考虑 Projection；复杂清洗、多目标分发、跨表加工优先物化视图。14.6 管理操作查看：SELECT database, table, name, queryFROM system.projectionsWHERE table = 'events_local';删除：ALTER TABLE analytics.events_localDROP PROJECTION proj_user;清除数据但保留定义：ALTER TABLE analytics.events_localCLEAR PROJECTION proj_user;重新物化：ALTER TABLE analytics.events_localMATERIALIZE PROJECTION proj_user;14.7 验证是否命中查看执行计划：EXPLAINSELECT count(), sum(amount)FROM analytics.events_localWHERE event_date = today()GROUP BY city_id;再查看查询日志：SELECT query, read_rows, read_bytes, query_duration_msFROM system.query_logWHERE query_id = 'target-id';如果查询读取了不同列、不同过滤条件或聚合粒度，优化器可能仍回退到原始数据。14.8 版本差异Projection 的支持范围、语法、优化器行为和系统表字段在不同版本间存在差异。生产使用前应在目标版本测试，并保留无投影执行基线、写入吞吐变化、磁盘增长和回滚方式。14.9 常见问题            问题      原因                  查询没有命中      表达式不兼容              写入变慢      投影额外计算和落盘              磁盘增长      多份物理数据              历史数据无效      未物化              合并变慢      需要维护投影              优化不稳定      版本或查询形态变化      本章小结Projection 适合为同一张表补充不同排序或固定聚合视图，由优化器自动选择。它比物化视图更内聚，但灵活性和版本行为要谨慎评估。使用前用扫描量、写入吞吐和磁盘成本做完整对比。思考题  普通投影和聚合投影分别解决什么问题？  Projection 和物化视图如何选择？  为什么 Projection 会增加写入成本？  如何确认查询命中投影？  投影上线前需要哪些回滚准备？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。物化视图把查询结果落到目标表中，常用于明细数据实时预聚合。它是一条随写入触发的数据处理管道，不是简单的查询别名。13.1 普通视图与物化视图普通视图只保存 SQL：CREATE VIEW analytics.events_viewASSELECT * FROM analytics.events_localWHERE is_deleted = 0;物化视图保存结果：CREATE MATERIALIZED VIEW analytics.city_metric_mvTO analytics.city_metric_localASSELECT    event_date AS metric_date,    city_id,    count() AS pv,    sum(amount) AS gmvFROM analytics.events_localGROUP BY event_date, city_id;普通视图查询时执行，物化视图在源表写入时更新目标表。13.2 TO 表结构推荐显式指定 TO 目标表：CREATE TABLE analytics.city_metric_local(    metric_date Date,    city_id UInt32,    pv UInt64,    gmv Decimal64(2))ENGINE = SummingMergeTreePARTITION BY toYYYYMM(metric_date)ORDER BY (metric_date, city_id);再创建视图：CREATE MATERIALIZED VIEW analytics.city_metric_mvTO analytics.city_metric_localASSELECT ...优点：  目标表 schema 清晰；  方便重建视图；  可以选择目标引擎；  支持 TTL 和分区；  便于备份恢复。13.3 触发语义物化视图由新写入的数据块触发：INSERT block   |   +-&gt; 写入源表   |   +-&gt; 对该 block 执行视图 SELECT           |           v       写入目标表这意味着：  视图创建前的历史数据不会自动回填；  每个 insert block 内的聚合范围有限；  目标表查询通常仍要聚合；  视图异常会影响写入链路；  删除源表数据不会自动删除目标数据。13.4 回填历史先暂停写入，再手动回填：INSERT INTO analytics.city_metric_localSELECT    event_date AS metric_date,    city_id,    count() AS pv,    sum(amount) AS gmvFROM analytics.events_localWHERE event_date &lt; '2026-08-25'GROUP BY event_date, city_id;再创建物化视图处理新数据。关键是避免同一时间窗重复回填。13.5 聚合状态准确 UV 可以使用聚合状态：CREATE TABLE analytics.city_metric_state(    metric_date Date,    city_id UInt32,    uv_state AggregateFunction(uniq, UInt64),    pv SimpleAggregateFunction(sum, UInt64))ENGINE = AggregatingMergeTreeORDER BY (metric_date, city_id);物化视图：CREATE MATERIALIZED VIEW analytics.city_metric_state_mvTO analytics.city_metric_stateASSELECT    event_date AS metric_date,    city_id,    uniqState(user_id) AS uv_state,    count() AS pvFROM analytics.events_localGROUP BY event_date, city_id;查询：SELECT city_id, uniqMerge(uv_state) AS uv, sum(pv) AS pvFROM analytics.city_metric_stateGROUP BY city_id;13.6 常见用途            用途      示例                  预聚合      明细转日汇总              数据清洗      过滤脏数据、补默认值              维度补齐      Lookup 后写入宽表              多目标分发      一份明细写多个汇总              指标状态      uniqState、quantileState              格式转换      JSON 转结构化列      13.7 视图链路events_local   |   +-&gt; mv_daily -&gt; daily_metrics   |   +-&gt; mv_city -&gt; city_metrics   |   +-&gt; mv_realtime -&gt; realtime_metrics多条视图会同时消耗写入线程资源。要控制视图数量、目标表写入频率、聚合复杂度、失败重试策略和延迟监控。13.8 管理操作查看：SELECT database, name, engine, as_selectFROM system.tablesWHERE is_materialized_view = 1;删除：DROP VIEW analytics.city_metric_mv;物化视图定义修改通常通过删除重建完成。操作前要确认写入暂停窗口和目标表数据边界。13.9 一致性物化视图不提供跨表事务保证，可能出现源表可见而目标表暂不可见的情况。排查角度：  写入时间；  视图创建时间；  分区边界；  历史回填重复；  目标表合并状态；  视图异常日志。对账源表：SELECT count(), sum(amount)FROM analytics.events_localWHERE event_date = '2026-08-25';对账目标表：SELECT sum(pv), sum(gmv)FROM analytics.city_metric_localWHERE metric_date = '2026-08-25';13.10 反模式            反模式      问题                  用视图替代所有模型设计      链路难维护              视图中做复杂 JOIN      写入放大              目标表无 TTL      数据无限增长              忽略 block 内聚合语义      UV 等指标不准              频繁删除重建      数据边界混乱              不监控视图异常      静默丢数据      本章小结物化视图是写入触发的实时加工管道，适合预聚合、清洗和多目标分发。推荐使用 TO 表并显式管理回填边界。聚合状态能处理去重类指标，但不能把物化视图当作事务一致的实时副本。思考题  物化视图何时触发？  为什么历史数据需要手动回填？  uniqState 和 uniqMerge 如何配合？  物化视图有哪些一致性限制？  如何对账源表和汇总表？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 的索引不是传统 B+Tree。主索引是排序键的稀疏索引，跳数索引则是在 granule 之上构建的辅助数据结构。12.1 granuleMergeTree 每隔 index_granularity 行形成一个 granule：rows 0 - 8191       granule 0rows 8192 - 16383   granule 1rows 16384 - 24575  granule 2查询读取列时以 granule 为重要单位。即使只命中一行，也可能需要读取该行所在 granule 对应的列块。12.2 主索引CREATE TABLE analytics.events_local(    event_date Date,    event_type LowCardinality(String),    city_id UInt32,    user_id UInt64,    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id);主索引记录每个 granule 的排序键边界：granule 0: (08-25, pay, 1, 100)granule 1: (08-25, pay, 1, 900)granule 2: (08-25, pay, 2, 120)当条件命中排序键前缀时，可以跳过大量 granule。12.3 跳数索引类型minmax 适合范围查询：ALTER TABLE analytics.events_localADD INDEX idx_amount_minmax amount TYPE minmax GRANULARITY 1;set 适合少量离散值：ALTER TABLE analytics.events_localADD INDEX idx_event_type event_type TYPE set(100) GRANULARITY 1;bloom_filter 适合等值查询：ALTER TABLE analytics.events_localADD INDEX idx_user_bloom user_id TYPE bloom_filter GRANULARITY 3;ngrambf_v1 适合子串匹配：ALTER TABLE analytics.logs_localADD INDEX idx_message_ngram message String TYPE ngrambf_v1(3, 10000, 3, 0)GRANULARITY 1;tokenbf_v1 适合 token 或词匹配：ALTER TABLE analytics.logs_localADD INDEX idx_message_token message String TYPE tokenbf_v1(10000, 3, 0)GRANULARITY 1;12.4 GRANULARITY 含义GRANULARITY 3 表示每 3 个主索引 granule 构建一个跳数索引块：主 granule: 1 2 3 | 4 5 6 | 7 8 9跳数索引:     A       B       C跳数索引块命中时，块内所有主 granule 都可能被读取。粒度小更精准但索引更大，粒度大更省空间但跳过能力弱。12.5 新旧数据生效新增索引只影响新写入数据：ALTER TABLE analytics.events_localADD INDEX idx_user_bloom user_id TYPE bloom_filter GRANULARITY 3;历史数据需要物化：ALTER TABLE analytics.events_localMATERIALIZE INDEX idx_user_bloom;查看任务：SELECT database, table, mutation_id, command, is_doneFROM system.mutationsWHERE table = 'events_local';12.6 索引代价            代价      说明                  写入延迟      需要额外构建索引              磁盘空间      索引本身占空间              合并成本      合并时重建索引              误判      bloom 可能误判存在              维护复杂      过多索引难以治理      Bloom filter 只能判断“可能存在”或“一定不存在”，存在等值误判，因此命中后仍要读取数据判断。12.7 索引选择            条件类型      首选                  排序键前缀等值或范围      主索引              数值范围      minmax              少量枚举      set              高基数等值      bloom_filter              子串匹配      ngrambf_v1              token 匹配      tokenbf_v1              高频全文检索      考虑搜索引擎或专门索引      不要为了每个 WHERE 列都加索引。先看查询频率、扫描收益和维护成本。12.8 与投影配合同一张表可能有不同查询模式：模式 A：按日期 + 城市 + 渠道聚合模式 B：按用户查最近事件排序键适合 A，B 可以用投影：ALTER TABLE analytics.events_localADD PROJECTION proj_user_recent(    SELECT event_date, user_id, event_type, count(), max(event_time)    GROUP BY event_date, user_id, event_type);投影可以改变存储排列，在这些场景常比额外跳数索引更有效。12.9 排查索引效果流程：1. 记录 query_id2. 查看 read_rows 和 read_bytes3. 检查 WHERE 条件与排序键关系4. 检查跳数索引是否物化5. 对比索引调整前后的执行结果6. 结合磁盘和 CPU 指标判断强制关闭跳数索引的会话设置在不同版本中存在差异，使用前以当前版本文档为准。更稳妥的做法是保留调整前后的扫描量对比。本章小结ClickHouse 查询性能来自排序键、分区和 granule 剪枝，跳数索引只是补充。主索引解决物理顺序，跳数索引解决非排序键条件，投影解决不同排序和预聚合需求。索引设计要以高频查询和扫描量收益为依据。思考题  为什么主索引是稀疏索引？  bloom filter 为什么会误判？  新增跳数索引后历史数据如何生效？  GRANULARITY 变大有什么影响？  跳数索引和投影如何取舍？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。分区和排序键是 ClickHouse 表设计中最容易出错的两件事。分区负责生命周期和粗粒度剪枝，排序键决定 part 内数据物理顺序和稀疏索引能力。            维度      PARTITION BY      ORDER BY                  作用范围      一组数据      part 内每行顺序              主要目标      过期删除、分区裁剪      稀疏索引、局部性              粒度      粗      细              常见依据      时间      查询过滤维度              过多后果      part 和目录过多      写入排序成本高      一句话概括：分区解决“哪段时间可以不看或删除”，排序键解决“哪些 granule 可以跳过”。11.1 分区设计按月：PARTITION BY toYYYYMM(event_date)按天：PARTITION BY toDate(event_time)组合分区：PARTITION BY (toYYYYMM(event_date), platform)组合分区会显著放大分区数量，除非生命周期和隔离需求明确，否则不建议。11.2 分区选择流程1. 数据保留多久？2. 每天写入多少行、多少字节？3. 常用查询时间范围是多少？4. 是否需要按天快速删除？5. 合并压力是否可控？参考：            场景      建议                  事件保留 30 天，日写入 TB 级      按天              事件保留 1 年，日写入百 GB 内      按月              小表      不分区              只按城市隔离      谨慎，数据倾斜时部分分区过大      11.3 排序键设计示例：ORDER BY (event_date, event_type, city_id, channel, user_id)前缀查询有效：WHERE event_date = '2026-08-25'WHERE event_date = '2026-08-25' AND event_type = 'pay'WHERE event_date BETWEEN '2026-08-01' AND '2026-08-31'  AND event_type = 'pay'  AND city_id = 1跳跃前缀效果弱：WHERE city_id = 1WHERE user_id = 1001这不是不能查，而是无法充分利用稀疏索引。11.4 排序键设计步骤1. 收集 top 慢查询和高频查询2. 提取必带条件3. 统计过滤列基数4. 低基数等值列放前5. 时间范围列尽量靠前6. 高基数用户 ID 通常放后7. 不同查询模式用投影补齐常见排序：            业务      可选排序                  行为事件      日期，事件类型，城市，用户              监控指标      日期，指标 ID，机器              日志检索      日期，服务，主机，级别              订单分析      日期，状态，城市，商家              用户画像      更新日期，用户 ID      11.5 主键与排序键ClickHouse 允许 PRIMARY KEY 与 ORDER BY 不同，但主键必须是排序键前缀。多数表只设置 ORDER BY 即可。ENGINE = MergeTreeORDER BY (event_date, city_id, user_id)PRIMARY KEY (event_date, city_id)不必要地定义 PRIMARY KEY 容易让使用者误解为 OLTP 唯一键。业务建模时建议避免这种混淆。11.6 采样键大扫描探索分析可使用采样：CREATE TABLE analytics.events_sampled(    event_date Date,    user_id UInt64,    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, user_id)SAMPLE BY user_id;查询：SELECT count(), sum(amount)FROM analytics.events_sampledSAMPLE 0.1WHERE event_date = today();采样必须显式指定 SAMPLE BY。报表和计费类结果不能默认使用采样。11.7 分区操作查看分区：SELECT table, partition, sum(rows) AS rows, sum(bytes_on_disk) AS bytesFROM system.partsWHERE database = 'analytics' AND activeGROUP BY table, partitionORDER BY bytes DESC;删除分区：ALTER TABLE analytics.events_local DROP PARTITION '202608';冻结备份：ALTER TABLE analytics.events_local FREEZE PARTITION '202608';挂载分区：ALTER TABLE analytics.events_localATTACH PARTITION '202608';11.8 修改排序键排序键不能像普通列一样随意修改。常见做法：1. 新建目标表2. 分批 INSERT SELECT3. 校验行数和指标4. 重命名切换表示例：RENAME TABLE analytics.events_local TO analytics.events_old;RENAME TABLE analytics.events_new TO analytics.events_local;切换分布式表或高可用环境时，要考虑写入停止窗口、任务暂停和元数据同步。11.9 反模式            反模式      后果                  每小时一个分区      part 数量失控              高基数城市、用户组合做分区      分区过多且倾斜              排序键过长      写入和索引成本增加              user_id 放排序键首位      日期查询无法利用前缀              所有查询共用一个排序      不同模式性能差异大              用分区替代排序键      细粒度过滤仍需大量扫描      本章小结分区服务于数据生命周期和粗粒度裁剪，排序键服务于物理局部性和稀疏索引。先用时间分区控制范围，再按高频过滤条件设计排序前缀，最后用投影或汇总表覆盖差异化的查询模式。思考题  分区和排序键各自减少什么成本？  为什么不建议按小时分区？  排序键前缀为什么重要？  什么场景适合 SAMPLE？  如何安全重构大表排序键？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。CollapsingMergeTree 用正负记录抵消旧状态，适合把状态变更表达为事件流。它比“不断更新一行”更贴合 ClickHouse 的追加模型，但要求 sign 语义严格。10.1 抵消模型INSERT +1: user 1001 level = 1INSERT -1: user 1001 level = 1INSERT +1: user 1001 level = 2排序键相同的 +1 和 -1 在后台合并时抵消，最终留下：user 1001 level = 2正负记录必须是同一个旧状态的镜像，不能一边 level=1，一边 level=2。10.2 建表CREATE TABLE analytics.user_state_local(    user_id UInt64,    level UInt8,    vip UInt8,    updated_at DateTime,    sign Int8)ENGINE = CollapsingMergeTree(sign)PARTITION BY toYYYYMM(updated_at)ORDER BY (user_id, level, vip, updated_at);如果只按 user_id 查询，排序键可以简化：ORDER BY user_id但所有参与抵消的业务字段必须保证正负行排序键一致。10.3 写入示例INSERT INTO analytics.user_state_local VALUES    (1001, 1, 0, '2026-08-25 10:00:00', 1);-- 抵消旧状态INSERT INTO analytics.user_state_local VALUES    (1001, 1, 0, '2026-08-25 10:00:00', -1);-- 新状态INSERT INTO analytics.user_state_local VALUES    (1001, 2, 1, '2026-08-25 10:05:00', 1);查询当前状态：SELECT    user_id,    level,    vip,    updated_atFROM analytics.user_state_localGROUP BY user_id, level, vip, updated_atHAVING sum(sign) &gt; 0;更简单的聚合方式：SELECT user_id, sum(sign) AS state_countFROM analytics.user_state_localGROUP BY user_id;复杂字段可以配合 argMax(field, updated_at) 使用，但必须先确认正负记录语义。10.4 分区问题如果按 updated_at 分区，旧状态和新状态可能落入不同分区。不同分区之间不会互相抵消，查询时必须跨分区处理。更稳妥的设计：使用固定业务日期或首次创建日期分区例如：CREATE TABLE analytics.user_state_fixed(    user_id UInt64,    create_date Date,    level UInt8,    updated_at DateTime,    sign Int8)ENGINE = CollapsingMergeTree(sign)PARTITION BY toYYYYMM(create_date)ORDER BY (user_id, create_date, level, updated_at);10.5 乱序与 VersionedCollapsingMergeTree普通 CollapsingMergeTree 对写入顺序有要求。Kafka 分区重放、并发写多副本、ETL 重跑都可能导致负记录晚到或乱序。带版本表：CREATE TABLE analytics.user_state_versioned(    user_id UInt64,    level UInt8,    updated_at DateTime,    version UInt64,    sign Int8)ENGINE = VersionedCollapsingMergeTree(sign, version)ORDER BY user_id;引擎先按版本整理，再按 sign 抵消。版本仍然必须来自可信来源，例如业务版本号或有序事件序号。10.6 生成正负记录CDC 示例：旧状态：level=1, version=10变更后：level=2, version=11输出：  -1, level=1, version=10  抵消旧状态  +1, level=2, version=11  写入新状态Flink 伪代码：if (value.isUpdate()) {    State old = stateStore.get(value.getKey());    if (old != null) {        out.collect(old.withSign(-1));    }    out.collect(value.withSign(1));    stateStore.put(value.getKey(), value);}如果使用无状态作业，需要先从数据库、状态主题或 Lookup 表获取旧值。10.7 查询模式当前状态：SELECT user_id, sum(sign) AS cntFROM analytics.user_state_localGROUP BY user_idHAVING cnt &gt; 0;统计各等级人数：SELECT level, sum(sign) AS usersFROM analytics.user_state_localGROUP BY level;使用 FINAL：SELECT *FROM analytics.user_state_localFINAL;大表 FINAL 成本高。查询前先缩小分区或键范围。10.8 与 ReplacingMergeTree 对比            维度      Replacing      Collapsing                  数据形态      多版本快照      正负记录              合并结果      保留最新版本      抵消正负              查询      argMax / FINAL      sum(sign) / FINAL              删除      写入墓碑版本      写 -1              乱序      版本可判断      Versioned 变体      如果上游能提供完整最新行，ReplacingMergeTree 更简单。如果上游只有状态变化事件，Collapsing 更贴近事件语义。10.9 常见错误            错误      结果                  负记录字段与旧状态不一致      无法抵消              忘写 -1      状态重复              只写 -1      状态丢失              sign 不在 Int8 中表达      类型错误              分区随状态变化      跨分区不抵消              查询不用 sign      结果不正确      排查：SELECT    user_id,    level,    sign,    count()FROM analytics.user_state_localWHERE user_id = 1001ORDER BY updated_at, sign;本章小结CollapsingMergeTree 把状态更新转换为追加正负记录，适合事件化状态流。设计关键是镜像旧状态、保证 sign 正确、稳定版本和分区。乱序场景优先选择 VersionedCollapsingMergeTree，查询层必须显式处理 sign。思考题  正负记录为什么必须完全镜像？  普通 Collapsing 对写入顺序有什么要求？  VersionedCollapsing 解决什么问题？  为什么分区字段不能随状态变化？  Replacing 和 Collapsing 如何选择？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。SummingMergeTree 会在后台合并时把排序键相同的数值列求和，用于降低预聚合指标的物理行数。查询时仍需要 sum()，否则结果可能不完整。9.1 基本语义CREATE TABLE analytics.city_metric_local(    metric_date Date,    city_id UInt32,    metric_type LowCardinality(String),    pv UInt64,    uv UInt64,    gmv Decimal64(2))ENGINE = SummingMergeTreePARTITION BY toYYYYMM(metric_date)ORDER BY (metric_date, city_id, metric_type);写入：INSERT INTO analytics.city_metric_local VALUES('2026-08-25', 1, 'page_view', 100, 80, 0),('2026-08-25', 1, 'page_view', 50, 40, 0),('2026-08-25', 1, 'order', 10, 9, 1000.00);合并后前两行可能变成：metric_date=2026-08-25, city_id=1, metric_type=page_view, pv=150, uv=120注意：uv 直接相加变成 120，可能重复计数。不是所有指标都能简单求和。9.2 可累加指标适合：            指标      说明                  PV      可累加              请求次数      可累加              GMV      可累加              时长总量      可累加              错误数      可累加      不适合：            指标      原因                  UV      用户可能重复              余额      应取最新而非求和              转化率      应由分子分母计算              平均单价      应由总额 / 数量计算              去重设备数      需近似或明细集合      UV 的处理方式：  保留明细表；  使用 uniqState 物化聚合状态；  使用近似 HLL 类型；  接受业务定义的累加误差。9.3 聚合状态方案普通 MergeTree 保存聚合状态：CREATE TABLE analytics.city_metric_state(    metric_date Date,    city_id UInt32,    metric_type LowCardinality(String),    pv AggregateFunction(count),    uv_state AggregateFunction(uniq, UInt64),    gmv SimpleAggregateFunction(sum, Decimal64(2)))ENGINE = AggregatingMergeTreePARTITION BY toYYYYMM(metric_date)ORDER BY (metric_date, city_id, metric_type);写入：INSERT INTO analytics.city_metric_stateSELECT    metric_date,    city_id,    metric_type,    countState() AS pv,    uniqState(user_id) AS uv_state,    sumState(gmv) AS gmvFROM analytics.events_localGROUP BY metric_date, city_id, metric_type;查询：SELECT    city_id,    sum(pv),    uniqMerge(uv_state) AS uv,    sum(gmv) AS gmvFROM analytics.city_metric_stateGROUP BY city_id;当 SummingMergeTree 无法表达指标语义时，AggregatingMergeTree 更合适。9.4 查询仍需聚合SELECT    metric_type,    sum(pv) AS pv,    sum(uv) AS uv,    sum(gmv) AS gmvFROM analytics.city_metric_localWHERE metric_date = '2026-08-25'GROUP BY metric_type;原因：未合并前：多行部分合并后：仍可能有多个较大 part查询时：必须再 sum 才能得到最终结果把 SummingMergeTree 当成“减少行数的存储优化”，而不是查询层免聚合的保证。9.5 非数值列处理排序键相同的行合并时，非数值列保留某一行。若查询依赖非数值列，必须把它放进排序键或用聚合函数表达。示例：CREATE TABLE analytics.channel_metric(    metric_date Date,    city_id UInt32,    channel LowCardinality(String),    pv UInt64)ENGINE = SummingMergeTreeORDER BY (metric_date, city_id, channel);如果 channel 不同，它们本来就是不同分组，不会被合并成一行。9.6 建模流程1. 明确业务指标定义2. 区分可累加与不可累加3. 确定维度组合4. 设置 ORDER BY 为维度组合5. 选择 Summing 或 Aggregating6. 查询时统一聚合7. 与明细表对账维度组合通常是：日期 + 城市 + 渠道 + 指标类型如果不同报表使用完全不同维度，建议按主题建多个汇总表，而不是一张超宽表覆盖所有场景。9.7 物化视图配合CREATE MATERIALIZED VIEW analytics.city_metric_mvTO analytics.city_metric_localASSELECT    event_date AS metric_date,    city_id,    event_type AS metric_type,    count() AS pv,    countDistinct(user_id) AS uv,    sum(amount) AS gmvFROM analytics.events_localGROUP BY event_date, city_id, event_type;这里 uv 的误差来自同一用户跨批次重复。若需要准确 UV，使用聚合状态或保留明细。9.8 与 ReplacingMergeTree 对比            维度      ReplacingMergeTree      SummingMergeTree                  合并语义      保留一行      数值列求和              业务问题      去重      可累加汇总              版本      可指定      无相同概念              典型数据      订单状态      指标预聚合              查询      argMax / FINAL      sum      不要把状态数据和可累加指标混在同一个引擎里解决。语义不清时优先普通 MergeTree，再在物化视图中处理。9.9 排查与对账检查维度组合：SELECT metric_date, city_id, metric_type, count()FROM analytics.city_metric_localGROUP BY metric_date, city_id, metric_typeHAVING count() &gt; 1LIMIT 100;与明细对账：SELECT    count() AS detail_pv,    sum(amount) AS detail_gmvFROM analytics.events_localWHERE event_date = '2026-08-25' AND event_type = 'pay';汇总表：SELECT sum(pv), sum(gmv)FROM analytics.city_metric_localWHERE metric_date = '2026-08-25' AND metric_type = 'pay';如果结果不一致，检查物化视图启动时间、过滤条件、维度空值、金额精度和事件丢失。本章小结SummingMergeTree 适合写入端已经聚合出的可累加指标。它通过合并减少行数，但查询仍要聚合。UV、比率、平均值等指标不能盲目相加，必要时使用 AggregatingMergeTree 或保留明细。思考题  为什么查询 SummingMergeTree 仍要 sum()？  哪些指标不能直接累加？  AggregatingMergeTree 解决什么问题？  物化视图如何造成 UV 误差？  如何设计汇总表与明细表的对账？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ReplacingMergeTree 用来处理“同一业务键可能被多次写入”的数据。它在后台合并时按排序键去重，并可结合版本列保留最新记录。它不是实时唯一索引，查询侧仍要正确处理版本。8.1 适用场景适合：  MySQL 表同步；  CDC 变更流；  订单、用户、商品等状态快照；  任务失败重试造成的数据重放；  上游补数和修正。不适合：  需要强一致事务；  高频单键实时更新；  每次查询都必须物理去重；  业务键本身无法稳定表达唯一性。8.2 建表CREATE TABLE analytics.orders_local(    order_date Date,    order_id String,    user_id UInt64,    order_status LowCardinality(String),    amount Decimal64(2),    updated_at DateTime,    version UInt64)ENGINE = ReplacingMergeTree(updated_at)PARTITION BY toYYYYMM(order_date)ORDER BY (order_date, order_id)SETTINGS index_granularity = 8192;版本列可以是 DateTime 或 UInt64。建议版本来源稳定、单调递增，并能覆盖同秒内的多次修改。8.3 去重键去重范围由 ORDER BY 决定，不是某个显式 PRIMARY KEY 猜测值。ORDER BY (order_date, order_id)表示同一个分区中，order_date + order_id 相同的行会在合并时被替换。如果 order_date 会随状态更新而改变，同一订单可能出现在两个日期下，去重就不符合预期。因此同步链路必须固定业务日期字段或把 order_date 排除在更新字段外。8.4 版本语义写入示例：INSERT INTO analytics.orders_local VALUES('2026-08-25', 'O001', 1001, 'CREATED', 100.00, '2026-08-25 10:00:00', 1),('2026-08-25', 'O001', 1001, 'PAID', 100.00, '2026-08-25 10:05:00', 2),('2026-08-25', 'O001', 1001, 'CANCELLED', 100.00, '2026-08-25 10:03:00', 3);如果版本列是 updated_at，第三行版本不是最大值，最终保留第二行。因此不能只看插入顺序，必须确认版本定义。推荐：            版本来源      说明                  upstream version      业务库版本号，优先              binlog 时间戳      注意乱序和精度              事件生成时间      可能与入库时间不同              ETL 批次号      同批内多次更新要处理      8.5 查询最新状态合并前可能仍有重复，稳妥查询：SELECT    order_date,    order_id,    argMax(user_id, version) AS user_id,    argMax(order_status, version) AS order_status,    argMax(amount, version) AS amount,    max(updated_at) AS updated_atFROM analytics.orders_localWHERE order_date = '2026-08-25'GROUP BY order_date, order_id;argMax(value, version) 返回版本最大行的 value。不要使用 max(order_status)，状态字符串大小与业务状态无关。8.6 FINALSELECT *FROM analytics.orders_localFINALWHERE order_date = '2026-08-25';FINAL 会让查询在读取阶段应用合并语义，可能增加 CPU、内存和执行时间。使用原则：  先过滤分区和键范围；  小范围查询可以使用；  大报表优先物化最新状态；  定位慢查询时检查是否 FINAL 导致。8.7 手动合并OPTIMIZE TABLE analytics.orders_local PARTITION 202608;更彻底：OPTIMIZE TABLE analytics.orders_local PARTITION 202608 FINAL;手动 FINAL 合并会强制处理 part，成本高。适合维护窗口或小分区修复，不适合业务高峰循环执行。8.8 与副本和分布式表组合本地表：CREATE TABLE analytics.orders_shard(    order_date Date,    order_id String,    order_status LowCardinality(String),    updated_at DateTime,    version UInt64)ENGINE = ReplicatedReplacingMergeTree(    '/clickhouse/tables/{shard}/orders',    '{replica}',    version)PARTITION BY toYYYYMM(order_date)ORDER BY (order_date, order_id);分布式表：CREATE TABLE analytics.orders_allAS analytics.orders_shardENGINE = Distributed(    analytics_cluster,    analytics,    orders_shard,    cityHash64(order_id));查询分布式表时 FINAL 的下发行为与版本和查询方式有关。核心是保证同一个 order_id 落到同一分片，否则不同分片各有一份记录，无法全局替换。8.9 同步链路设计MySQL binlog / CDC        |        vKafka        |        vETL 解析、补版本、映射日期        |        vReplacingMergeTree        |        v最新状态视图 / 汇总表必须明确：  主键字段；  不可变业务日期；  版本字段；  删除事件表达；  延迟消息处理；  重放窗口；  对账规则。8.10 对账与排查检查重复：SELECT order_id, count() AS cFROM analytics.orders_localWHERE order_date = '2026-08-25'GROUP BY order_idHAVING c &gt; 1ORDER BY c DESC;检查最新状态：SELECT order_id, max(version), argMax(order_status, version)FROM analytics.orders_localGROUP BY order_idORDER BY order_id;常见问题：            现象      原因                  重复一直存在      尚未合并，属正常              旧状态胜出      版本列错误或乱序              同键跨分区      排序键含可变日期              分布式仍重复      分片路由改变或同键跨分片              查询变慢      大范围 FINAL      本章小结ReplacingMergeTree 用后台合并降低物理重复，用版本列表达最新状态。正确的排序键和稳定版本是设计核心；查询侧使用 argMax 或受限 FINAL 是一致性保障。它解决的是幂等分析存储，不是实时事务更新。思考题  ReplacingMergeTree 的去重键由什么决定？  版本列为什么必须单调可信？  FINAL 有什么代价？  分片路由变化如何影响去重？  如何为 CDC 链路设计版本和对账？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。MergeTree 是 ClickHouse 最重要的存储引擎。理解 part、分区、排序键、稀疏索引、后台合并和 TTL，就理解了 ClickHouse 高性能分析的基础。7.1 存储结构一张 MergeTree 表的逻辑结构：table  partition 202608    part all_1_1_0      primary.idx        稀疏主键索引      event_date.bin      列数据      event_type.bin      user_id.bin      amount.bin      count.txt           元信息每个 part 内数据按 ORDER BY 排序，列文件独立保存，并带有压缩块和校验信息。7.2 建表示例CREATE TABLE analytics.events_local(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    channel LowCardinality(String),    amount Decimal64(2),    is_deleted UInt8)ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id)TTL event_date + INTERVAL 13 MONTHSETTINGS index_granularity = 8192;各元素职责：            元素      职责                  PARTITION BY      生命周期管理与粗粒度剪枝              ORDER BY      物理排序、稀疏索引、局部性              TTL      自动过期数据或列              index_granularity      稀疏索引粒度              SETTINGS      引擎行为细节      7.3 part 生命周期INSERT 生成宽 part        |        v后台 merge 合并同分区 part        |        v生成更大 part，旧 part 标记 inactive        |        v超过保留期后物理删除查看 part：SELECT    partition,    name,    active,    rows,    bytes_on_disk,    min_block_number,    max_block_number,    levelFROM system.partsWHERE table = 'events_local' AND activeORDER BY partition, name;强制合并：OPTIMIZE TABLE analytics.events_local PARTITION 202608;生产上不要频繁执行全表 FINAL 合并。强制合并会占用大量 CPU、IO 和内存。7.4 分区设计常用分区：PARTITION BY toYYYYMM(event_date)大表也可以按天：PARTITION BY toDate(event_date)分区不是越细越好。过多分区会带来：  更多目录和元数据；  后台合并调度压力；  查询计划开销；  小文件问题。建议：            场景      分区                  保留 1 年，TB 级事件      按月或按周              保留 7 到 30 天，每天量大      按天              小维表或测试表      通常不分区              明确生命周期管理      优先时间分区      7.5 排序键设计ORDER BY 决定每个 part 内数据的物理顺序，也决定稀疏索引能否跳过无关 granule。设计原则：  低基数字段放前面；  常用等值或范围过滤字段尽量靠前；  避免过高基数字段占据前缀；  与核心报表维度一致；  不追求覆盖所有查询，用投影补齐不同排序。示例：ORDER BY (event_date, event_type, city_id, user_id)查询有效：SELECT count()FROM analytics.events_localWHERE event_date = '2026-08-25'  AND event_type = 'pay'  AND city_id = 1;查询较弱：SELECT count()FROM analytics.events_localWHERE user_id = 1001;第二个查询跳过了排序键前缀，需要扫描更多 granule。针对高频用户查询可以额外建投影或物化汇总表。7.6 稀疏索引ClickHouse 不是每行一条 B+Tree 索引，而是每隔 index_granularity 行记录一次排序键标记：granule 0: min_key = (08-01, view, 1, 10)granule 1: min_key = (08-01, view, 1, 5000)granule 2: min_key = (08-01, view, 2, 20)查询时通过 primary.idx 判断哪些 granule 可能包含数据，再读取列块。查看 granule 信息：SELECT    table,    primary_key,    index_granularityFROM system.tablesWHERE database = 'analytics';index_granularity 一般保持默认。盲目调小会让索引和元数据膨胀，盲目调大可能降低过滤精度。7.7 跳数索引跳数索引是二级数据结构，用于跳过不满足条件的 granule：ALTER TABLE analytics.events_localADD INDEX idx_user_bloom user_id TYPE bloom_filter GRANULARITY 3;ALTER TABLE analytics.events_localADD INDEX idx_amount_minmax amount TYPE minmax GRANULARITY 1;ALTER TABLE analytics.events_local MATERIALIZE INDEX idx_user_bloom;常见类型：            类型      适合                  minmax      数值和时间范围              set(N)      少量离散值              bloom_filter      等值查询，高基数可选              tokenbf_v1      token 匹配              ngrambf_v1      子串匹配      跳数索引有构建成本和存储成本，也会降低写入速度。只给高频过滤条件添加。7.8 TTL行 TTL：ALTER TABLE analytics.events_localMODIFY TTL event_date + INTERVAL 13 MONTH;列 TTL：ALTER TABLE analytics.events_localMODIFY COLUMN    remark String TTL event_date + INTERVAL 1 MONTH;查看 TTL 信息：SELECT database, table, engine_fullFROM system.tablesWHERE table = 'events_local';TTL 删除通常由后台任务执行，不是精确到秒的实时删除。关键保留期要配合监控和审计确认。7.9 合并策略与设置常用设置：            设置      说明                  index_granularity      稀疏索引粒度              min_bytes_for_wide_part      宽 part 和紧凑 part 阈值              merge_max_block_size      合并处理块大小              max_parts_in_total      part 总数保护              ttl_only_drop_parts      按整 part 删除 TTL 数据      按整分区过期可提高删除效率：ALTER TABLE analytics.events_localMODIFY SETTING ttl_only_drop_parts = 1;适用条件：TTL 边界尽量与分区边界对齐。否则效果有限。7.10 常见故障Too many partsSELECT table, count() AS parts, sum(rows) AS rowsFROM system.partsWHERE activeGROUP BY tableORDER BY parts DESC;处理：  降低写入频率；  增大批次；  减少分区数量；  检查合并线程和磁盘 IO；  扩容或降低写入流量。查询扫描量过大SELECT query, read_rows, read_bytes, query_duration_msFROM system.query_logWHERE type = 'QueryFinish'ORDER BY read_bytes DESCLIMIT 20;优先检查分区条件、排序键、投影和预聚合。part 损坏查看异常：SELECT database, table, name, active, reasonFROM system.partsWHERE not activeORDER BY modification_time DESC;处理方式与是否有副本有关。副本表优先从健康副本恢复；单机表需要使用备份恢复，不要盲目删除目录。本章小结MergeTree 通过按列存储、part 合并、分区剪枝、排序键稀疏索引和跳数索引，把海量扫描和聚合做成高吞吐流水线。设计时要先确定数据生命周期和主要查询模式，再决定分区、排序键和索引。频繁小写入、过细分区和滥用强制合并都是常见反模式。思考题  part、partition、granule 分别是什么？  分区和排序键是否都能减少扫描？  排序键前缀为什么重要？  跳数索引有哪些代价？  什么时候使用 ttl_only_drop_parts？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。本章把常用查询能力串起来：投影、过滤、聚合、窗口、排序、去重、格式输出和查询生命周期。目标是写出可预测成本的查询，而不只是得到结果。6.1 SELECT 执行结构SELECT city_id, count() AS pvFROM analytics.events_localWHERE event_date = today()GROUP BY city_idHAVING pv &gt; 100ORDER BY pv DESCLIMIT 20;概念执行顺序：FROM -&gt; WHERE -&gt; GROUP BY -&gt; 聚合 -&gt; HAVING -&gt; SELECT -&gt; ORDER BY -&gt; LIMITClickHouse 会做多种优化，但写 SQL 时仍应先通过过滤条件减少扫描量。6.2 查看查询成本SELECT    query_id,    event_time,    query_duration_ms,    read_rows,    read_bytes,    result_rows,    memory_usage,    exceptionFROM system.query_logWHERE type = 'QueryFinish'ORDER BY event_time DESCLIMIT 20;关键指标：            指标      含义                  read_rows      读取行数              read_bytes      读取字节数              result_rows      返回行数              query_duration_ms      总耗时              memory_usage      内存峰值              ProfileEvents      更细粒度事件      优化优先看扫描量和分区剪枝，而不是只看总耗时。6.3 聚合SELECT    event_type,    count() AS events,    countDistinct(user_id) AS uv,    round(sum(amount), 2) AS gmv,    round(avg(amount), 2) AS avg_amountFROM analytics.events_localWHERE event_date = today()GROUP BY event_type;常用组合：            表达式      说明                  count()      精确计数，通常比 count(*) 更符合习惯              sum(x)      求和，注意空组结果              min(x) / max(x)      极值              uniq(x)      近似去重              uniqExact(x)      精确去重              quantile(0.95)(x)      分位数              topK(10)(x)      高频值      6.4 条件与空值SELECT    if(amount &gt; 0, 'paid', 'free') AS event_scope,    multiIf(        amount &gt;= 1000, 'high',        amount &gt;= 100, 'middle',        'low'    ) AS amount_level,    coalesce(city_name, 'unknown') AS cityFROM analytics.event_view;逻辑条件：SELECT count()FROM analytics.events_localWHERE event_date BETWEEN '2026-08-01' AND '2026-08-31'  AND (city_id = 1 OR city_id = 2)  AND event_type NOT IN ('spam');6.5 子查询与 CTECTE：WITH daily AS (    SELECT        event_date,        city_id,        count() AS pv    FROM analytics.events_local    WHERE event_date &gt;= today() - 7    GROUP BY event_date, city_id)SELECT    city_id,    sum(pv) AS week_pv,    max(pv) AS max_day_pvFROM dailyGROUP BY city_idORDER BY week_pv DESC;CTE 提升可读性，但不等于自动缓存。复杂公共子查询是否能复用要结合执行计划确认。6.6 窗口函数SELECT    event_date,    city_id,    pv,    row_number() OVER (PARTITION BY event_date ORDER BY pv DESC) AS rn,    rank() OVER (PARTITION BY event_date ORDER BY pv DESC) AS rk,    sum(pv) OVER (PARTITION BY event_date) AS day_totalFROM (    SELECT event_date, city_id, count() AS pv    FROM analytics.events_local    WHERE event_date &gt;= today() - 3    GROUP BY event_date, city_id);分组 TopN：SELECT event_date, city_id, pvFROM (    SELECT *,       row_number() OVER (PARTITION BY event_date ORDER BY pv DESC) AS rn    FROM (        SELECT event_date, city_id, count() AS pv        FROM analytics.events_local        GROUP BY event_date, city_id    ))WHERE rn &lt;= 10;窗口函数通常在聚合结果上使用。直接对超大数据集开窗口会造成大量排序和内存消耗。6.7 数组与漏斗展开数组：SELECT    user_id,    arrayJoin(event_ids) AS event_idFROM analytics.user_events;漏斗：SELECT    level,    count() AS usersFROM (    SELECT        user_id,        windowFunnel(86400)(            toUnixTimestamp(event_time),            event_type = 'view',            event_type = 'cart',            event_type = 'pay'        ) AS level    FROM analytics.events_local    WHERE event_date = today()    GROUP BY user_id)GROUP BY levelORDER BY level;留存：SELECT    sum(retention(event_type = 'install', event_date = today())) AS d1,    sum(retention(event_type = 'install', event_date = today() + 1)) AS d2FROM analytics.events_local;漏斗和留存函数的窗口定义、事件顺序和时间语义必须在业务侧先明确。6.8 关联查询内连接：SELECT e.event_id, c.city_nameFROM analytics.events_local AS eINNER JOIN analytics.city_dim AS c    ON e.city_id = c.city_idWHERE e.event_date = today();字典替代小维表：SELECT    event_id,    dictGet('analytics.city_dict', 'city_name', toUInt64(city_id)) AS city_nameFROM analytics.events_localWHERE event_date = today();字典适合小而更新可控的维表，不适合频繁全量刷新的超大维度。6.9 查询限制用户级限制示例：ALTER USER app_userSETTINGS max_execution_time = 30, max_memory_usage = 2000000000;语句级限制：SELECT count()FROM analytics.events_localSETTINGS max_threads = 8, max_execution_time = 30;常用保护：            参数      目的                  max_execution_time      限制执行时间              max_memory_usage      限制单查询内存              max_rows_to_read      限制扫描行数              max_bytes_to_read      限制扫描字节              readonly      禁止写操作              max_result_rows      限制返回规模      6.10 结果格式clickhouse-client --query "SELECT number, number * 2FROM system.numbersLIMIT 3FORMAT PrettyCompact"服务返回 JSON：SELECT count()FROM analytics.events_localFORMAT JSONEachRow;大结果集导出建议用 clickhouse-client 或对象存储，不要通过 HTTP 一次性拉取到应用内存。6.11 查询故障排查慢查询清单：1. 是否有分区条件？2. 条件是否作用在排序键前缀？3. 是否扫描了大量不需要的列？4. 是否高基数 group by 或精确去重？5. 是否大表 JOIN？6. 是否窗口函数作用在明细上？7. 是否返回超大结果集？8. 是否资源被其他查询占用？查看运行中查询：SELECT query_id, elapsed, read_rows, memory_usage, queryFROM system.processesORDER BY elapsed DESC;取消查询：KILL QUERY WHERE query_id = 'target-query-id';本章小结查询 ClickHouse 时，先让分区剪枝和排序键发挥作用，再选择合适聚合与去重函数。窗口函数、精确去重和大 JOIN 都是高成本能力，应配合过滤、预聚合、物化视图和资源限制使用。system.query_log 是性能治理的起点。思考题  如何判断一个查询是否命中分区剪枝？  uniq 和 uniqExact 如何选择？  窗口函数为什么常放在聚合之后？  字典 JOIN 适合什么维表？  你会给报表用户设置哪些查询限制？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 的写入模型与 OLTP 数据库差异很大：它偏好大批量、稳定频率、可重试的追加写入。本章覆盖 INSERT、文件导入、外部表、批量写入策略和常见写入故障。5.1 写入链路INSERT / HTTP / Kafka        |        v生成一个新 part        |        v分区目录 + 每列文件 + 索引文件 + 校验文件        |        v后台 merge 合并小 part每一次小 INSERT 通常都会生成新的 part。part 数量过多后，CPU 会大量消耗在合并、定位和元数据管理上，最终可能触发 Too many parts。推荐写入：            指标      参考                  单批行数      1 万到 10 万以上              写入频率      每秒到每数秒一批              批次字段      尽量完整、类型稳定              并发      控制在写入端可管理范围              失败      按批次重试，必要时幂等      5.2 INSERT 语句INSERT INTO analytics.events_localVALUES('2026-08-25', '2026-08-25 10:00:00', 'E001', 1001, 'view', 1, 0.00),('2026-08-25', '2026-08-25 10:01:00', 'E002', 1002, 'pay', 1, 199.00);指定列写入：INSERT INTO analytics.events_local    (event_date, event_time, event_id, user_id, event_type, city_id, amount)VALUES    ('2026-08-25', '2026-08-25 10:02:00', 'E003', 1003, 'view', 2, 0);从查询写入：INSERT INTO analytics.events_backupSELECT * FROM analytics.events_localWHERE event_date = '2026-08-25';注意：INSERT INTO ... SELECT 会占用源表读取和目标表写入资源，大范围数据迁移应分批执行。5.3 常见输入格式JSONEachRow：{"event_date":"2026-08-25","event_time":"2026-08-25 10:00:00","event_id":"E001","user_id":1001,"event_type":"view","city_id":1,"amount":0}{"event_date":"2026-08-25","event_time":"2026-08-25 10:01:00","event_id":"E002","user_id":1002,"event_type":"pay","city_id":1,"amount":199.00}写入：clickhouse-client --query "INSERT INTO analytics.events_local FORMAT JSONEachRow" &lt; events.jsonlCSV：clickhouse-client --date_time_input_format=best_effort --query "INSERT INTO analytics.events_local FORMAT CSVWithNames" &lt; events.csvParquet：clickhouse-client --query "INSERT INTO analytics.events_local FORMAT Parquet" &lt; events.parquet5.4 HTTP 写入curl -sS 'http://127.0.0.1:8123/' \  --data-binary 'INSERT INTO analytics.events_local FORMAT JSONEachRow{"event_date":"2026-08-25","event_time":"2026-08-25 10:03:00","event_id":"E004","user_id":1004,"event_type":"view","city_id":3,"amount":0}'使用 query_id 便于追踪：curl -sS 'http://127.0.0.1:8123/?query_id=load-20260825-0001' \  --data-binary @events.jsonlHTTP 适合服务端导入；高吞吐应用写入通常优先 Native 协议或 Kafka。5.5 从文件和对象存储读取查询本地文件：SELECT count()FROM file('events.jsonl', JSONEachRow, 'event_date Date, event_time DateTime, event_id String');查询 URL：SELECT *FROM url(    'https://example.com/events.jsonl',    JSONEachRow,    'event_date Date, user_id UInt64');查询 S3：SELECT count()FROM s3(    'https://bucket.s3.amazonaws.com/path/events.parquet',    'Parquet',    'event_date Date, user_id UInt64');访问外部资源要配置访问控制、网络策略、凭据和超时，不要让任意用户直接读取内部地址。5.6 从 MySQL 导入MySQL 引擎：CREATE TABLE mysql_ordersENGINE = MySQLAS SELECT    order_id,    created_at,    status,    amountFROM mysql('mysql-host:3306', 'shop', 'orders', 'user', 'password');写入 ClickHouse：INSERT INTO analytics.orders_localSELECT    toDate(created_at) AS order_date,    order_id,    status,    amount,    toUInt64(unixTimestamp(created_at)) AS versionFROM mysql_ordersWHERE updated_at &gt;= now() - INTERVAL 1 HOUR;增量导入要解决：  时间基准；  时区；  更新时间回拨；  删除同步；  主键映射；  任务失败重放。5.7 应用批量写入简化伪代码：queue = memory bufferlast_flush = nowon event:    queue.add(event)    if queue.size &gt;= 50000 or now - last_flush &gt;= 2s:        flush(queue)flush(queue):    batch = queue.drain()    retry with backoff    on permanent failure:        write to local dead letter or send to Kafka retry topicGo 示例核心结构：type Writer struct {    conn   clickhouse.Conn    batch   *clickhouse.Batch    rows    int    ctx     context.Context}func (w *Writer) Add(e Event) error {    if err := w.batch.Append(        e.EventDate, e.EventTime, e.EventID,        e.UserID, e.EventType, e.CityID, e.Amount,    ); err != nil {        return err    }    w.rows++    return nil}func (w *Writer) Flush() error {    if w.rows == 0 {        return nil    }    return w.batch.Send()}5.8 写入幂等ClickHouse 没有像 OLTP 主键那样提供全局实时唯一约束。常用方案：            方案      做法      适合                  批次 ID      同一重试批次复用 block ID      应用重试              ReplacingMergeTree      排序键 + 版本      订单、用户状态              事件表      append + 查询去重      行为事件              外部幂等      Kafka transactional id 或 ETL 水位      复杂链路      ReplacingMergeTree 只能降低合并后的物理重复，不能把查询实时变成唯一表。5.9 删除和更新轻量删除：DELETE FROM analytics.events_localWHERE event_date = '2026-08-01';变更：ALTER TABLE analytics.events_localUPDATE amount = amount * 2WHERE event_date = '2026-08-01' AND city_id = 1;这类操作会生成变更 part，代价较高。分析表更常用的做法是：  修正上游后重写指定分区；  用替换表切换；  追加修正记录；  在查询层过滤无效标记。5.10 写入问题排查查看异常 part：SELECT    database,    table,    partition,    name,    active,    rows,    bytes_on_diskFROM system.partsWHERE activeORDER BY rows DESCLIMIT 50;查看合并：SELECT database, table, elapsed, num_parts, total_sizeFROM system.mergesORDER BY elapsed DESC;常见问题：            现象      可能原因      处理                  Too many parts      小写入太多、合并慢      批量写入、降并发、扩容              Disk space insufficient      磁盘满      清理 TTL、扩容、停止写入              Merge with where part is lost      副本缺 part      从副本恢复              Quota exceeded      配额限制      调整用户配额              Timeout      写入批次过大或队列拥塞      拆批、重试、限流      本章小结写入 ClickHouse 的关键是把小写入合并成大批量，并让失败可重试、数据可幂等。文件、HTTP、MySQL、S3 和 Kafka 都是输入通道，但都要关注格式、类型、时间分区和资源隔离。删除与更新应被视为特殊操作，而不是日常高频操作。思考题  为什么小 INSERT 会引发 Too many parts？  JSONEachRow 和 Parquet 分别适合什么场景？  应用写入端如何设计缓冲和重试？  ReplacingMergeTree 如何辅助幂等？  大范围 UPDATE 为什么不适合 ClickHouse？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 支持丰富的 SQL 能力和类型系统，但它不是 OLTP 数据库。理解类型、可空性、数组、嵌套、函数和参数化查询，是写出高性能且可控语句的前提。4.1 数据库与表管理CREATE DATABASE IF NOT EXISTS analytics;SHOW TABLES FROM analytics;DESCRIBE TABLE analytics.events_local;SHOW CREATE TABLE analytics.events_local;删除表：DROP TABLE analytics.events_local;清空分区比 DELETE 更高效：ALTER TABLE analytics.events_localDROP PARTITION '202608';4.2 数值类型            类型      说明                  UInt8 / UInt16 / UInt32 / UInt64      无符号整数              Int8 / Int16 / Int32 / Int64      有符号整数              Float32 / Float64      浮点数              Decimal32 / Decimal64 / Decimal128      定点数              UInt128 / UInt256 / Int256      超大整数，慎用于普通业务      金额优先使用 Decimal：SELECT    toDecimal64(19.99, 2) AS price,    price * 3 AS total;不要用 Float64 存资金余额。浮点适合比率、评分、科学计算，不适合精确金额。4.3 时间类型            类型      说明                  Date      日期              Date32      更大日期范围              DateTime      秒级时间              DateTime64      亚秒精度      建表：CREATE TABLE time_demo(    d Date,    dt DateTime,    dt64 DateTime64(3, 'Asia/Shanghai'))ENGINE = Memory;常用函数：SELECT    now() AS now_time,    today() AS today_date,    toDate('2026-08-25') AS d,    toDateTime('2026-08-25 10:00:00', 'Asia/Shanghai') AS dt,    formatDateTime(dt, '%Y-%m-%d %H:%i:%s') AS formatted;ClickHouse 的 DateTime 默认精度是秒。需要毫秒时应显式使用 DateTime64(3)。4.4 字符串与枚举            类型      说明                  String      变长字符串              FixedString(N)      定长字符串              Enum8 / Enum16      枚举              UUID      全局唯一标识              LowCardinality(String)      低基数字典编码      Enum 示例：CREATE TABLE order_demo(    order_id String,    status Enum8('CREATED' = 1, 'PAID' = 2, 'CANCELLED' = 3))ENGINE = Memory;LowCardinality 适合状态、平台、国家、渠道等低基数字段：CREATE TABLE event_demo(    event_type LowCardinality(String),    platform LowCardinality(String))ENGINE = Memory;不建议把用户 ID、URL、trace id 这类高基数字段设置成 LowCardinality，否则字典本身会带来额外成本。4.5 NullableCREATE TABLE nullable_demo(    id UInt64,    remark Nullable(String))ENGINE = Memory;查询：SELECT    id,    isNull(remark) AS is_null,    coalesce(remark, 'empty') AS safe_valueFROM nullable_demo;Nullable 会带来额外存储和计算成本。默认值通常比空值更适合分析表：            场景      建议                  未知城市      UInt32 默认 0，维度表标记未知              无备注      空字符串              无支付金额      Decimal64 默认 0              事件确实无值且需区分      Nullable      4.6 数组、Map 和 Nested数组：SELECT    [1, 2, 3] AS ids,    length(ids) AS n,    arrayJoin(ids) AS expanded_id;Map：SELECT    map('os', 'iOS', 'network', '5G') AS props,    props['os'] AS os;Nested：CREATE TABLE sku_demo(    order_id String,    items Nested(        sku_id UInt64,        quantity UInt32,        price Decimal64(2)    ))ENGINE = Memory;展开：SELECT    order_id,    arrayJoin(items.sku_id) AS sku_idFROM sku_demo;复杂类型灵活，但会降低压缩和过滤效率。高频过滤和聚合字段应提升为普通列。4.7 JSON 类型新版 ClickHouse 提供原生 JSON 能力，不同版本行为差异较大。兼容性要求高的链路常用 String 保存 JSON，再用函数解析：SELECT    JSONExtractString(payload, 'city') AS city,    JSONExtractUInt(payload, 'user_id') AS user_idFROM (    SELECT '{"user_id":1001,"city":"SH"}' AS payload);如果使用原生 JSON 类型，应先确认版本、写入格式、路径访问语法和schema 演进策略。4.8 查询语句SELECT    city_id,    count() AS pv,    countDistinct(user_id) AS uv,    round(sum(amount), 2) AS gmvFROM analytics.events_localWHERE event_date = today()  AND event_type IN ('view', 'pay')GROUP BY city_idHAVING pv &gt; 100ORDER BY gmv DESCLIMIT 20;条件表达式：SELECT    multiIf(        amount &gt;= 1000, 'high',        amount &gt;= 100, 'middle',        'low'    ) AS levelFROM analytics.events_local;去重估算：SELECT uniqExact(user_id) AS exact_uv, uniq(user_id) AS approx_uvFROM analytics.events_local;uniq 更快但近似；uniqExact 精确但成本更高。4.9 JOIN 能力与边界SELECT    e.city_id,    c.city_name,    count() AS pvFROM analytics.events_local AS eLEFT JOIN analytics.city_dim AS c ON e.city_id = c.city_idGROUP BY e.city_id, c.city_name;ClickHouse 支持多种 JOIN 算法，但大表 JOIN 的资源消耗仍要重视。优化思路：  小维表走 Dictionary；  常用汇总先物化；  事实表冗余必要维度字段；  过滤条件下推；  避免在超大表之间做高频在线 JOIN。4.10 系统表常用系统表：            系统表      用途                  system.tables      表元数据              system.parts      part 状态              system.merges      合并任务              system.query_log      查询日志              system.processes      正在执行的查询              system.clusters      集群节点              system.replicas      副本状态              system.disks      磁盘信息      查看当前查询：SELECT    query_id,    elapsed,    read_rows,    memory_usage,    queryFROM system.processesORDER BY elapsed DESC;终止查询：KILL QUERY WHERE query_id = 'query-id';4.11 常见 SQL 错误            错误      原因      处理                  Type mismatch      类型不兼容      显式转换              Unknown identifier      字段或别名作用域错误      检查 SELECT、WHERE、HAVING              Memory limit exceeded      结果或状态过大      过滤、分片、限制内存              Too many parts      小写入过多      改为大批量写入              Readonly user      用户权限不足      授权或换用户      本章小结类型系统决定了存储效率、压缩率和计算成本。数值与时间要明确精度，字符串要区分低基数与高基数，Nullable 和复杂类型要克制使用。SQL 层面应优先利用分区过滤、预聚合、字典和系统表，把查询成本控制在可预测范围内。思考题  为什么金额不应使用 Float64？  LowCardinality 为什么不适合 trace id？  Nullable 有什么代价？  uniq 和 uniqExact 的差异是什么？  大表 JOIN 有哪些常见优化方向？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。表引擎是 ClickHouse 的核心抽象。它决定数据如何写入、如何合并、是否支持副本、能否对接外部系统，以及查询时的行为边界。3.1 表引擎分类            分类      代表引擎      典型用途                  MergeTree 家族      MergeTree、ReplacingMergeTree      事实表、日志表              副本引擎      ReplicatedMergeTree      高可用              分布式引擎      Distributed      分片集群入口              消息引擎      Kafka      消费消息流              外部引擎      MySQL、URL、S3      外部查询和导入              内存引擎      Memory、Buffer      临时数据              简单引擎      TinyLog、Log      小数据测试              字典      Dictionary      维表映射      MergeTree 家族是生产主力。Distributed 不是数据最终存储，Kafka 也不是长期存储，它们常与 MergeTree 组合使用。3.2 MergeTreeCREATE TABLE events(    event_date Date,    event_time DateTime,    user_id UInt64,    event_type LowCardinality(String),    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, user_id)TTL event_date + INTERVAL 13 MONTH;特点：  数据以 part 为单位写入；  后台按规则合并 part；  排序键决定数据物理顺序；  支持分区、TTL、采样、投影和副本；  适合批量追加分析。3.3 ReplacingMergeTree用于在合并阶段按排序键去重，并通过版本列决定保留哪一行。CREATE TABLE orders_local(    order_id String,    updated_at DateTime,    order_status LowCardinality(String),    amount Decimal64(2),    version UInt64)ENGINE = ReplacingMergeTree(version)ORDER BY (order_id);写入两条相同 order_id 的数据：INSERT INTO orders_local VALUES('O001', now() - INTERVAL 10 MINUTE, 'CREATED', 100.00, 1),('O001', now(), 'PAID', 100.00, 2);强制合并：OPTIMIZE TABLE orders_local FINAL;注意：ReplacingMergeTree 的去重发生在后台合并之后，不保证查询瞬间唯一。稳妥做法是在查询时保留最新版本：SELECT    order_id,    argMax(order_status, version) AS order_status,    argMax(amount, version) AS amount,    max(updated_at) AS updated_atFROM orders_localGROUP BY order_id;3.4 SummingMergeTree用于合并时对数值列求和，排序键相同的非数值列取任意一行。CREATE TABLE metrics_local(    metric_date Date,    metric_name LowCardinality(String),    city_id UInt32,    value Float64)ENGINE = SummingMergeTreePARTITION BY toYYYYMM(metric_date)ORDER BY (metric_date, metric_name, city_id);查询时仍要聚合：SELECT metric_name, sum(value) AS totalFROM metrics_localWHERE metric_date = today()GROUP BY metric_name;适合写入端已经能给出可累加指标的预聚合场景，不适合直接存需要精确还原的明细。3.5 CollapsingMergeTree通过 sign = 1 和 sign = -1 抵消状态变化，适合把变更事件转为可折叠记录。CREATE TABLE user_state_local(    user_id UInt64,    level UInt8,    sign Int8)ENGINE = CollapsingMergeTreeORDER BY user_id;插入和抵消：INSERT INTO user_state_local VALUES (1001, 1, 1);INSERT INTO user_state_local VALUES (1001, 1, -1);INSERT INTO user_state_local VALUES (1001, 2, 1);查询：SELECT user_id, sum(sign) AS cnt, max(level) AS levelFROM user_state_localGROUP BY user_idHAVING cnt &gt; 0;限制：写入端必须按正确顺序发送正负记录。乱序场景优先考虑 VersionedCollapsingMergeTree。3.6 VersionedCollapsingMergeTreeCREATE TABLE user_state_v2(    user_id UInt64,    level UInt8,    sign Int8,    version UInt64)ENGINE = VersionedCollapsingMergeTree(sign, version)ORDER BY user_id;引擎按版本整理顺序，再按 sign 折叠，适合消息可能重放或乱序的流式链路。设计时仍要保证同一业务状态的正负记录版本关系清晰。3.7 ReplicatedMergeTree副本引擎依赖 Keeper 或 ZooKeeper 记录副本日志、块 ID 和Leader 选举信息。CREATE TABLE events_replicated(    event_date Date,    user_id UInt64,    event_type LowCardinality(String))ENGINE = ReplicatedMergeTree(    '/clickhouse/tables/{shard}/events',    '{replica}')PARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, user_id);生产事实表通常使用具体的 MergeTree 语义副本版本，例如：ReplicatedReplacingMergeTreeReplicatedSummingMergeTreeReplicatedCollapsingMergeTree3.8 DistributedDistributed 表是查询入口，不保存业务数据，而是把查询路由到各分片的本地表。CREATE TABLE events_allAS events_replicatedENGINE = Distributed(    analytics_cluster,    currentDatabase(),    events_replicated,    rand());常见写入模式：应用 -&gt; Distributed 表 -&gt; 各分片本地表或更可控的模式：Kafka 分区 -&gt; 写入路由层 -&gt; 指定分片本地表3.9 Kafka 引擎Kafka 引擎只负责消费消息，通常配合物化视图写入 MergeTree：Kafka topic -&gt; Kafka 表 -&gt; Materialized View -&gt; MergeTree 表第 22 章会详细展开。这里要记住：Kafka 表不是长期存储表，断点、消费组和数据落表逻辑必须被监控。3.10 Memory 与 BufferCREATE TABLE temp_result(    key String,    value UInt64)ENGINE = Memory;Memory 表重启即丢失，适合临时结果。Buffer 可以缓冲写入，但故障时存在丢失风险：CREATE TABLE buffered_eventsAS eventsENGINE = Buffer(    currentDatabase(),    events,    16,    100,    1000000,    10000000,    100000000,    1000000000);如果能在写入端聚合，优先改写入端，而不是依赖 Buffer 规避小写入问题。3.11 引擎选择决策是否长期存储？  否 -&gt; Memory / 外部表 / 临时视图  是 -&gt; MergeTree 家族是否需要去重？  是 -&gt; ReplacingMergeTree + 查询侧 argMax/FINAL指标是否可累加？  是 -&gt; SummingMergeTree 或普通 MergeTree + sum变更是否可用正负记录表达？  是 -&gt; Collapsing / VersionedCollapsing是否多副本？  是 -&gt; Replicated 变体是否分布式？  是 -&gt; 本地表 Replicated 变体 + Distributed 入口表本章小结表引擎的选择先看数据语义，再看可用性架构。普通 MergeTree 适合明细；ReplacingMergeTree 处理版本化幂等；SummingMergeTree 处理可累加指标；Collapsing 系列处理状态抵消；Replicated 提供高可用；Distributed 提供分片入口。不要把“最终一致”的合并语义误当成实时唯一性保证。思考题  ReplacingMergeTree 为什么查询时仍可能读到重复数据？  SummingMergeTree 中排序键为什么必须包含聚合维度？  Distributed 表的数据存储在哪里？  Buffer 引擎可能带来什么风险？  如果订单状态会多次变更且消息乱序，应选择哪个引擎？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 的环境搭建并不复杂，难的是把开发、测试、生产三种环境的资源边界和配置差异想清楚。本章从单机版开始，再到 Docker 和多节点集群，并给出常用配置与验证方法。2.1 版本选择ClickHouse 版本迭代较快，建议选择当前官方推荐的稳定版本，并在生产环境固定版本。不要把长期未升级的集群直接跨大版本升级。查看版本：SELECT version();版本选择建议：            环境      建议                  本地学习      官方稳定版或 Docker 最新稳定镜像              功能验证      与生产同一大版本              生产      固定版本、灰度升级、回滚方案完备      生产升级前至少确认：  兼容性列表；  表引擎和 SQL 行为差异；  元数据存储格式变化；  Keeper 或 ZooKeeper 版本；  客户端和 JDBC、ODBC 驱动版本。2.2 使用 Docker 启动单节点适合学习和快速验证：docker run -d \  --name clickhouse-server \  -p 8123:8123 \  -p 9000:9000 \  -e ULIMIT_NOFILE=262144:262144 \  clickhouse/clickhouse-server:24.8进入容器：docker exec -it clickhouse-server clickhouse-clientHTTP 验证：curl 'http://127.0.0.1:8123/?query=SELECT%201'常见端口：            端口      协议      用途                  8123      HTTP      HTTP API、健康检查              9000      Native      clickhouse-client、高吞吐写入              9440      Native TLS      加密原生协议              8443      HTTP TLS      加密 HTTP              9181      Keeper      Raft 通信，集群使用      2.3 Linux 单机安装以 Debian、Ubuntu 系为例，官方文档提供安装脚本。生产环境建议使用内部制品库或镜像源，避免构建机直接依赖外网。安装后确认服务：sudo systemctl status clickhouse-serversudo systemctl enable clickhouse-server启动和停止：sudo systemctl start clickhouse-serversudo systemctl stop clickhouse-server连接：clickhouse-client --host 127.0.0.1 --port 9000 --user default2.4 目录结构默认目录可能因安装方式不同而变化，常见位置如下：/etc/clickhouse-server/config.xml        服务配置/etc/clickhouse-server/users.xml          用户配置/var/lib/clickhouse/                      数据目录/var/log/clickhouse-server/               日志目录生产环境必须独立规划数据盘：/data01/clickhouse//data02/clickhouse//data03/clickhouse/多磁盘配置要结合存储策略使用，不要只是简单挂载后不声明策略。2.5 核心配置服务主配置通常是 config.xml，用户相关配置在 users.xml 或用户目录中。推荐使用独立配置文件减少主文件冲突，例如：/etc/clickhouse-server/config.d/logging.xml/etc/clickhouse-server/config.d/storage.xml/etc/clickhouse-server/users.d/app_user.xml资源限制示例：&lt;clickhouse&gt;    &lt;max_server_memory_usage&gt;0&lt;/max_server_memory_usage&gt;    &lt;max_thread_pool_size&gt;10000&lt;/max_thread_pool_size&gt;&lt;/clickhouse&gt;用户配额示例：&lt;clickhouse&gt;    &lt;profiles&gt;        &lt;app_profile&gt;            &lt;max_memory_usage&gt;4294967296&lt;/max_memory_usage&gt;            &lt;max_execution_time&gt;60&lt;/max_execution_time&gt;        &lt;/app_profile&gt;    &lt;/profiles&gt;&lt;/clickhouse&gt;更推荐使用 SQL 方式创建用户和角色：CREATE ROLE app_reader;GRANT SELECT ON analytics.* TO app_reader;CREATE USER app_user IDENTIFIED BY 'strong-password'SETTINGS PROFILE app_profile;GRANT app_reader TO app_user;2.6 命令行客户端执行 SQL 文件：clickhouse-client --multiquery &lt; schema.sql指定格式输出：clickhouse-client --query "SELECT number FROM system.numbers LIMIT 3 FORMAT PrettyCompact"常用格式：            格式      场景                  PrettyCompact      人工查看              TabSeparated      管道处理              CSV / CSVWithNames      表格交换              JSONEachRow      应用写入              Parquet      外部数据交换      写入数据：clickhouse-client --query "INSERT INTO target_table FORMAT CSVWithNames" &lt; data.csv2.7 搭建三节点测试集群测试集群至少包含：2 个 ClickHouse 分片副本混合部署，或 2 个 ClickHouse 节点3 个 ClickHouse Keeper 节点Keeper 负责副本元数据和分布式 DDL 协调。生产通常使用 3 个或 5 个节点，保证奇数多数派。config.xml 中的远程服务器示例：&lt;clickhouse&gt;    &lt;remote_servers&gt;        &lt;analytics_cluster&gt;            &lt;shard&gt;                &lt;replica&gt;                    &lt;host&gt;ch-01.internal&lt;/host&gt;                    &lt;port&gt;9000&lt;/port&gt;                &lt;/replica&gt;                &lt;replica&gt;                    &lt;host&gt;ch-02.internal&lt;/host&gt;                    &lt;port&gt;9000&lt;/port&gt;                &lt;/replica&gt;            &lt;/shard&gt;        &lt;/analytics_cluster&gt;    &lt;/remote_servers&gt;&lt;/clickhouse&gt;查看集群：SELECT * FROM system.clusters;确认 Keeper：echo ruok | nc keeper-01 9181ClickHouse Keeper 使用四字命令时要以 telnet 或 TCP 工具发送，命令支持程度与版本有关。2.8 数据库和表初始化CREATE DATABASE IF NOT EXISTS analytics;CREATE TABLE analytics.events_local(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    amount Decimal64(2))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id);插入测试数据：INSERT INTO analytics.events_localSELECT    today() AS event_date,    now() AS event_time,    toString(number) AS event_id,    number % 10000 AS user_id,    if(number % 3 = 0, 'view', 'pay') AS event_type,    number % 100 AS city_id,    toDecimal64(number % 500, 2) AS amountFROM system.numbersLIMIT 100000;验证：SELECT event_type, count(), round(sum(amount), 2)FROM analytics.events_localGROUP BY event_type;2.9 常见安装问题连接被拒绝排查顺序：systemctl status clickhouse-serverss -lntp | grep -E '8123|9000'docker port clickhouse-server常见原因：  服务未启动；  监听地址只绑定 127.0.0.1；  安全组或防火墙未放行；  客户端使用错协议端口；  TLS 配置不匹配。认证失败确认用户名、密码和网络来源：SELECT name, networks, auth_typeFROM system.users;磁盘空间不足SELECT    name,    path,    free_space,    total_spaceFROM system.disksORDER BY free_space;本章小结单机环境适合学习 SQL 和 MergeTree；测试和生产环境要提前规划版本、端口、数据目录、用户权限、Keeper 集群和资源限制。ClickHouse 的性能高度依赖机器资源和配置边界，环境搭建阶段就应该把可观测性和回滚方案一并纳入。思考题  HTTP 端口和 Native 端口分别适合什么访问方式？  为什么生产环境要独立规划 ClickHouse 数据目录？  Keeper 在集群中承担哪些职责？  如何避免用户执行无限制的大查询？  如果服务启动失败，你会按什么顺序排查？</li>
  <li>这是《ClickHouse 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开，代码块保留了原书可直接运行的版本。ClickHouse 是面向实时分析的列式数据库。它不适合做交易系统，但在海量事件扫描、多维聚合、漏斗留存、监控指标和实时看板上表现出色。一句话概括：MySQL 帮你精准找到几行数据，ClickHouse 帮你快速算完几亿行数据。1.1 为什么行存不适合 OLAP假设事件表有 100 列，而分析只用到 4 列：SELECT city_id, count(*), sum(amount)FROM eventsWHERE event_date = '2026-08-25'GROUP BY city_id;行存数据库必须读取完整行，即使只需要几个字段：Row: [id, user_id, device, ip, ua, event_id, ... 100 columns]列存数据库按列保存：event_date: 2026-08-25, 2026-08-25, ...city_id:    1, 2, 1, ...amount:     99, 199, 39, ...ClickHouse 只读取需要的列，因此：  IO 更少；  压缩率更高；  CPU 可以按列批量计算；  聚合性能显著提升。1.2 OLTP 与 OLAP 的区别            维度      OLTP      OLAP                  系统示例      MySQL      ClickHouse              查询类型      点查、短事务      扫描、聚合、Join              单次行数      少量      大量              写入模式      随机增删改      批量追加              延迟要求      毫秒级事务      毫秒到秒级分析              更新      常态      谨慎或异步              索引      B+Tree      稀疏索引、跳数索引              存储      行存      列存      典型分工：MySQL：下单、支付、库存、账户Kafka：事件流ClickHouse：行为分析、看板、漏斗、留存Elasticsearch：搜索和日志检索1.3 ClickHouse 的核心特性1.3.1 列式存储数据按列文件保存，适合只读部分列的分析。1.3.2 向量化执行CPU 一次处理一批值，而不是一行一行解释执行。行执行：sum(row1.amount), sum(row2.amount), ...向量化：sum([amount1, amount2, amount3, ...])1.3.3 数据压缩同一列的数据类型和模式相近，压缩效果好。            数据      压缩效果                  枚举、状态      极好              时间戳      好              用户 ID      中等              随机字符串      一般      1.3.4 MergeTree 引擎ClickHouse 最重要的表引擎家族，支持：  分区；  主键排序；  稀疏索引；  后台合并；  副本；  TTL；  投影；  多种合并语义。1.4 表引擎概览            引擎      特点      适合                  MergeTree      基础分析表      通用事实表              ReplacingMergeTree      合并时保留版本      幂等导入              SummingMergeTree      数值列汇总      预聚合              CollapsingMergeTree      sign 折叠      变更标记              VersionedCollapsingMergeTree      按版本折叠      乱序变更              ReplicatedMergeTree      副本      生产高可用              Distributed      分布式查询入口      分片集群              Kafka      消费 Kafka      实时链路              Memory      内存表      临时数据              Nullable 特性      空值处理      谨慎使用      ClickHouse 的表引擎不是简单配置，而是决定数据合并语义和查询行为的核心设计。1.5 一个最小事件表CREATE TABLE events_local(    event_date Date,    event_time DateTime,    event_id String,    user_id UInt64,    event_type LowCardinality(String),    city_id UInt32,    amount Decimal64(2),    platform LowCardinality(String),    props Map(String, String))ENGINE = MergeTreePARTITION BY toYYYYMM(event_date)ORDER BY (event_date, event_type, city_id, user_id)SETTINGS index_granularity = 8192;设计要点：  PARTITION BY 控制数据生命周期和剪枝范围；  ORDER BY 决定稀疏索引和数据局部性；  LowCardinality(String) 优化低基数字段；  Map 适合扩展属性，但不要把高频过滤字段只放 Map；  金额使用 Decimal64(2)。查询：SELECT city_id, count() AS pv, sum(amount) AS gmvFROM events_localWHERE event_date = '2026-08-25'GROUP BY city_idORDER BY gmv DESCLIMIT 20;1.6 写入模型ClickHouse 喜欢大批量写入，不喜欢高频小插入。推荐：每批 1 万到 10 万行间隔数百毫秒到数秒写入端按表和分区合并失败可重试且幂等不推荐：每秒每表几十上百次小 insert大量小写入会导致 part 过多、后台合并压力增加、查询变慢，甚至触发 “too many parts” 错误。1.7 与 Elasticsearch 的差异            维度      ClickHouse      Elasticsearch                  强项      大范围聚合分析      全文搜索、倒排索引              文本搜索      基础能力      强              多维聚合      强      可用但成本高              写入      批量 append      文档实时写入              更新      Merge 语义      文档 update              日志场景      大规模分析      检索和排障              典型用途      数仓分析      搜索、可观测性      常见架构是两者共存：Kafka  -&gt; Elasticsearch：最近日志检索  -&gt; ClickHouse：长期分析和报表1.8 适用与不适用场景适合：  用户行为分析；  埋点看板；  漏斗和留存；  监控指标；  广告归因；  风控离线特征；  实时报表；  大规模日志聚合。不适合：  在线交易；  高频单点更新；  强事务；  复杂多表实时 JOIN；  主数据管理；  低延迟键值查询。本章小结ClickHouse 通过列存、压缩、向量化执行和 MergeTree 合并模型，为海量分析场景提供高吞吐查询能力。它的设计围绕批量写入、分区剪枝、排序键和预聚合展开。要用好 ClickHouse，关键是明确它是 OLAP 系统，而不是万能数据库。思考题  行存和列存分别适合什么查询？  PARTITION BY 和 ORDER BY 分别解决什么问题？  为什么 ClickHouse 不适合高频小批量写入？  ClickHouse 和 Elasticsearch 在日志架构中如何分工？  如果要把 MySQL 订单数据同步到 ClickHouse 做报表，你会考虑哪些一致性问题？</li>
</ul>
