ClickHouse入门笔记

ClickHouse入门笔记 ClickHouse 入门:从原理到实战一、ClickHouse 是什么,解决什么问题ClickHouse 是 Yandex 开源的列式存储数据库(DBMS),主打 OLAP(在线分析处理)场景。核心命题只有一个:海量数据下,如何让聚合查询(sum、count、group by)快到毫秒级。要理解它为什么快,得先理解它和 MySQL 这类 OLTP 数据库的根本分歧——存储方式。列式存储 vs 行式存储假设有一张表:IdNameAge1张三182李四223王五34行式存储在磁盘上是这样组织的:1 张三 18 | 2 李四 22 | 3 王五 34想查张三的所有信息,一次磁盘寻址顺序读就搞定。但想算所有人的平均年龄,需要跳着读,或者全表扫描——大量不需要的字段(Name)也被迫一起读进来了。列式存储反过来:1 2 3 | 张三 李四 王五 | 18 22 34算平均年龄,只需要把 Age 这一列整体读出来。这个差异在数据量小的时候不明显,但数据量到千万级、亿级时,I/O 差距会被放大到数量级级别。这也是为什么 ClickHouse 单表查询能做到 presto、Hive 的几十倍甚至上百倍(PDF 里的 benchmark 数据:单表查询总耗时,ClickHouse 85.9,而 Hive 是 4096——差了近 48 倍)。列式存储还带来一个副产品:压缩率暴涨。同一列数据类型高度一致,压缩算法能针对性优化,压缩比远超行式存储。压缩比高了,磁盘 I/O 和 cache 命中率都跟着受益。但它不是万能的单条 Query 能利用整机所有 CPU 核心并行处理——这是靠分区(partition)→索引粒度(index granularity)→多核并行这套机制实现的。代价是:单条查询独占多核,不适合高并发的高 QPS 查询。如果你的场景是C 端用户高频小查询,ClickHouse 不是对的选择;如果是内部报表、离线分析、大批量聚合,这正是它的主场。二、表引擎:ClickHouse 的灵魂MySQL 里存储引擎(InnoDB/MyISAM)是可选项,大多数人一辈子只用 InnoDB。但在 ClickHouse 里,表引擎决定了数据怎么存、支持什么查询、能不能并发、要不要索引——选错引擎,等于用错了整个数据库。引擎大致分四类:合并树系列(MergeTree family)、日志类、接口类、其他。日常开发 99% 精力都在 MergeTree 系列上,其他两类了解即可:TinyLog:列文件存磁盘,不支持索引,没并发控制。适合练手,生产环境基本不用。Memory:数据存内存,重启即丢失。读写不阻塞,不支持索引,简单查询能跑到 10G/s 以上。适合数据量不大(上限约1亿行) 要求超高性能的场景。真正的重头戏是 MergeTree。三、MergeTree:三个参数决定一切建表语句长这样:createtablet_order_mt(id UInt32,sku_id String,total_amountDecimal(16,2),create_timeDatetime)engineMergeTreepartitionbytoYYYYMMDD(create_time)primarykey(id)orderby(id,sku_id);三个关键字理解透了MergeTree 就理解了一半:partition by(分区,可选)作用和 Hive 一样:缩小扫描范围。不填就只有一个分区,所有数据混一起。容易踩的坑:每一批 insert 进来的数据,都会先落地成一个临时分区,不会立即合并进已有分区。ClickHouse 会在后台(大概 10-15 分钟后)自动 merge,你也可以手动触发:optimizetablet_order_mt final;这意味着什么?如果你的 Flink 任务频繁地一条条 insert 写入 ClickHouse,会疯狂产生临时分区,后台合并压力剧增,严重时直接拖垮整个集群的写入性能。这是生产环境的高发事故点,对应到物流数仓场景:Flink Sink 到 ClickHouse必须走批量写入(攒够一批,用 JDBC batch 或者官方 connector 的 flush 机制),不能来一条写一条。primary key(主键,可选)这里要打破一个直觉:ClickHouse 的主键不是唯一约束,它只是一级索引,允许重复值存在。它的作用是配合order by字段做二分查找,定位到某个index granularity(索引粒度)区间,避免全表扫描。这就是所谓的稀疏索引——不像 MySQL 的 B 树精确定位到某一行,ClickHouse 只定位到大概在这个区间,然后在区间内做一点扫描。默认粒度是 8192 行,官方不建议改,除非某列存在大量重复值。order by(排序键,唯一必填项)这是 MergeTree 里唯一必须填的字段,甚至比 primary key 更重要。原因是:如果你没显式设置 primary key,ClickHouse 会隐式用 order by 做主键;更关键的是,后面讲的去重(ReplacingMergeTree)、预聚合(SummingMergeTree)都是**依照 order by 字段来判定哪些行算同一组**的。有个硬约束要记住:primary key 必须是 order by 字段的前缀。比如 order by (id, sku_id),那 primary key 只能是 id 或 (id, sku_id),不能是 sku_id 单独拎出来。四、MergeTree 的变种:按场景选型这部分是重点中的重点——三个引擎对应三种业务场景,选错了等于埋雷。ReplacingMergeTree:去重,但不是你想的那种去重createtablet_order_rmt(id UInt32,sku_id String,total_amountDecimal(16,2),create_timeDatetime)engineReplacingMergeTree(create_time)partitionbytoYYYYMMDD(create_time)primarykey(id)orderby(id,sku_id);ReplacingMergeTree()括号里填的是版本字段——重复数据保留版本字段值最大的那一条;不填的话,默认按插入顺序保留最后一条。这里有三个反直觉的坑:去重只在后台 merge 时发生,不是实时的。你 insert 完立刻 select,大概率还能看到重复数据。去重不能跨分区。如果表按天分区,同一个 id 的数据分别落在两天的分区里,永远不会被去重。重复的判定标准是 order by 字段,不是你自以为的主键。这意味着:如果你的场景需要CDC 同步过来的订单状态变化,只保留最新状态这种强一致去重需求,ReplacingMergeTree不能完全兜底,它只是后台悄悄清理重复数据以节省空间,不保证查询时没有重复。真正要精确拿最新状态,查询时还得加FINAL关键字或者自己写argMax聚合,但FINAL会牺牲性能。SummingMergeTree:预聚合,专治只看汇总不看明细createtablet_order_smt(id UInt32,sku_id String,total_amountDecimal(16,2),create_timeDatetime)engineSummingMergeTree(total_amount)partitionbytoYYYYMMDD(create_time)primarykey(id)orderby(id,sku_id);如果一张表你只关心按维度汇总的结果,不关心明细,用普通 MergeTree 会导致存储空间和查询时聚合的双重浪费。SummingMergeTree 在后台 merge 时,会把order by 字段相同的行自动做 sum 合并。规则:只有 SummingMergeTree() 括号里指定的列(必须是数字列)会被求和,不填就默认所有非维度的数字列都参与求和order by 的字段是维度列,合并时保留其他非数字、非维度的列,只保留合并中第一行的值同样不能跨分区聚合,同样是后台 merge 时才生效,查询时可能读到未合并的临时明细一个常见的认知误区:很多人以为设了 SummingMergeTree 之后,select sum(total_amount) from t where ...可以省略,直接select total_amount from t where ...就能拿到汇总值。这是错的——因为可能存在还没来得及合并的临时分区数据。查汇总值,永远老老实实用sum()聚合,只是效率会比普通 MergeTree 高一些(但因为列式存储本身效率已经很高,提升不会特别明显)。怎么选:一张表选型速查场景选型明细表,不去重不聚合普通 MergeTree需要按最新状态去重展示(弱一致可接受)ReplacingMergeTree只关心汇总指标,不看明细SummingMergeTree强一致去重/精确更新Mutation(alter update/delete),但代价很高,见下节五、Update / Delete:能做,但很重ClickHouse 支持 Update/Delete,官方称为Mutation 查询,本质上是 Alter 的一种:altertablet_order_smtdeletewheresku_idsku_001;altertablet_order_smtupdatetotal_amounttoDecimal32(2000.00,2)whereid102;重体现在:每次 Mutation 都会导致原分区被放弃、数据重建新分区。而且不支持事务。执行时分两步——先标记旧分区逻辑失效并写新分区,真正物理删除旧数据要等到分区合并触发。结论:能不做 Update/Delete 就不做,要做也尽量批量做,不要频繁对小数据做变更。六、SQL 层面与 MySQL 的差异ClickHouse 基本兼容标准 SQL,以下是几个值得注意的不同点:支持子查询、CTE(with 子句)、各种 JOIN,但JOIN 不能使用缓存,即使两次一模一样的 JOIN 语句也会被当成两条新 SQL 重新执行不支持自定义函数GROUP BY支持with rollup(从右至左逐级去掉维度做小计)、with cube(双向去掉维度做小计)、with totals(只算总计),用来快速做多维度汇总,不用手写多条 union窗口函数在官方测试阶段(以 PDF 版本为准,后续版本可能已转正)七、副本与分片:高可用怎么做副本(Replication):解决数据可用性问题。一台节点宕机,其他节点还有全量数据。依赖 ZooKeeper 协调写入日志,写入流程是:client 写入 A 节点 → A 提交写入日志到 ZK 集群 → 目标副本节点 B 从 ZK 收到通知 → B 从 A 拉取新数据。副本只能同步数据,不能同步表结构,每台机器要手动建表(引擎用ReplicatedMergeTree)。分片(Sharding):解决横向扩容问题。副本只能保证每台机器都有全量数据,但单机容量到顶就没辙了。分片把数据切开分布到不同节点,再通过Distributed 表引擎把各分片拼接成一张逻辑表对外提供查询。Distributed 引擎本身不存数据,只是一层路由中间件(类似 MySQL 生态里的 MyCat)。一个业内常见的现实是:大部分企业只做副本,不做分片——因为分片会显著增加查询复杂度(尤其是跨分片的 JOIN、去重、聚合),在数据量没有大到必须水平扩展之前,复杂度收益不成正比。