ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ClickHouse行存迷思:轻量引擎、紧凑格式与行级操作解析

ClickHouse行存迷思:轻量引擎、紧凑格式与行级操作解析 这个问题我在好几个技术社群里都被追问过而且每次问法都差不多“ClickHouse 是不是不支持行存”“如果支持是从哪个版本开始的”尤其是一些从 MySQL 迁移过来的团队一上来就拿着行存、列存的旧观念去套 ClickHouse结果越查越糊涂。今天这篇就把这个事彻底聊清楚顺便把 ClickHouse 里和“行”相关的存储引擎、行级操作、紧凑格式以及很多人容易踩的坑一次性梳理出来。先把结论放在前面严格意义上ClickHouse 原生并没有像 MySQL、PostgreSQL 那样把“一整行数据连续写在一个物理块里”的行存储引擎。但“按行读写的能力”和“面向小数据量的紧凑存储格式”一直都存在而且在不同版本里有不同的体现方式。如果你听某篇文章说“ClickHouse 从 xx 版本开始支持行存”那大概率是把“轻量表引擎”“compact part 格式”或者“行级 DELETE/UPDATE”误读成了行存。这篇文章会带你去区分三种完全不同的“行存”语境盘点 ClickHouse 从开源至今和“行”相关的真实能力再给出一套适合实际业务的落地方案。无论你是正在做 ClickHouse 选型还是已经上手但被行存问题困扰这篇都值得耐心看完。1. 先把“行存”这个词拆明白你到底在问哪种行存1.1 行存和列存区别不只是“存储方向”要搞清楚 ClickHouse 到底支不支持行存首先要正视一个问题数据库里说的“行存”和“列存”并不只是磁盘上数据排列方向不同而是整套 IO 模型、压缩策略、索引结构都不同。行存数据库如 MySQL 的 InnoDB面向的是“某一行数据的完整增删改查”。当你执行一条SELECT * FROM orders WHERE order_id 12345时存储引擎可以顺着主键索引找到那行数据的物理位置然后把这一行的所有字段一次性读出来。这非常适合高频点查、复杂事务、行级锁。列存数据库如 ClickHouse 的 MergeTree面向的是“某一列数据的大规模批量计算”。当你执行SELECT sum(amount) FROM orders WHERE date today()时存储引擎只需要读取 amount 这一列的所有数据块完全不需要理会订单号、用户 ID 这些无关字段。配合列式压缩分析型查询速度是行存的数倍甚至数十倍。把这层关系类比成超市仓库行存就是把每个顾客的清单贴在同一张卡片上你要找某个顾客抽出一张卡就行列存则是把所有顾客的“鸡蛋购买数”放在一张大表上所有顾客的“牛奶购买数”放在另一张表上。你要统计全超市鸡蛋卖了多少直接拿鸡蛋那张表算没必要把几千张顾客卡全部翻一遍。ClickHouse 从第一天起就坚定走列存路线它的底层文件格式、稀疏索引、向量化执行全都是围绕列存设计的。所以如果有人告诉你“ClickHouse 在某版本忽然支持行存了”你应该先怀疑一下这不符合 ClickHouse 的架构哲学。1.2 三种被混为一谈的“行存”在实际讨论中“ClickHouse 支持行存吗”这句话往往夹杂着三种完全不同的意思我把它们拆开讲第一层物理存储格式上的行存。即一行数据的所有字段是否连续放在磁盘的一个区域里。ClickHouse 原生表引擎中答案是“不支持”。Log、TinyLog、StripeLog、Memory 这些引擎看似轻量、按行语义读写但底层每个字段仍然拆到独立文件或独立数组里不是物理行存。第二层表能力上的“行式操作”。即能不能对单行数据进行高频率的插入、更新、删除。答案是“一直支持但代价不同”。ClickHouse 从早期版本开始就支持ALTER TABLE ... DELETE/UPDATE21.6 版本之后又加入了轻量级DELETE FROM和is_deleted标记机制让行级删除的代价大幅降低。第三层使用感受上的“像 MySQL 一样点查”。即能不能按主键快速定位一行、只查一行。答案是要看表引擎和数据量。小数据量用 Memory/Log 当然飞快大数据量下 MergeTree 依靠主键稀疏索引也能做到极快的单行点查但它天然不适合高频行级更新。所以如果有人一直追问“到底哪个版本支持的行存”你得先反问回去“你说的行存是物理行存还是行级操作能力”这个答案完全不同。1.3 一张表看懂结论你问的“行存”是否支持从什么时候代表引擎/能力物理行存一行数据连续落盘否至今不支持无轻量小表引擎面向行语义读写是2016 年开源时就有Log、TinyLog、StripeLog、Memory对小表合并为单文件compact part是早期版本逐渐引入MergeTree 的 compact 格式行级 UPDATE/DELETE异步 mutation是2018-2019 年版本已成熟ALTER TABLE UPDATE/DELETE轻量级删除is_deleted 标记是21.6 版本起DELETE FROM ... 配合后台清理外部行存数据库表引擎是较早版本开始持续完善MySQL、PostgreSQL、SQLite 表引擎按主键快速点查单行是一直都有MergeTree 稀疏主键索引这张表基本就是你今天最需要的答案。接下来我沿着这张表把每一行背后的细节和实战经验都展开。2. 被误传成“行存引擎”的轻量小表Log、TinyLog、StripeLog、Memory2.1 它们到底从什么时候开始支持ClickHouse 的早期版本里就有四个非常轻量的表引擎TinyLog、Log、StripeLog、Memory。网上很多文章把它们归类为“行存储引擎”说它们是 ClickHouse 支持行存的证据。这个说法极易误导人。首先明确时间ClickHouse 在 2016 年由 Yandex 开源时这些引擎就已经存在。不是某个大版本才“开始支持”而是从一开始就内置了。如果你在 2017 年写过一个ENGINE Log的表那你在当时就已经用上了所谓的“行存”。其次明确本质它们并不是物理行存。以TinyLog为例官方文档非常清楚地写着“每一列都保存到磁盘上一个独立的文件”。查询时按需读取列文件压缩和编码也是按列处理的这本质还是列式文件组织只是比 MergeTree 简单得多没有主键、没有分区、没有索引、没有合并机制。那为什么很多人把它们称为“行存”呢因为使用体验上它们像行存数据库的小表你可以简单地INSERT INTO一行数据可以SELECT * FROM全表扫出来不需要关心分区和排序键也不需要担心后台 part 合并。像 MySQL 里的一张普通小表。但“用起来像行存”和“物理上是行存”是两码事。2.2 实际操作四个轻量引擎怎么用、有什么差异建表语法非常简单不需要 ORDER BY不需要分区键比如-- 最简单的行式语义小表 CREATE TABLE stats_log ( event_time DateTime, url String, cnt UInt32 ) ENGINE TinyLog; -- 支持并发读、适合小量日志的引擎 CREATE TABLE stats_log2 ( event_time DateTime, url String, cnt UInt32 ) ENGINE Log; -- 所有列存在一个文件里更省文件句柄 CREATE TABLE stats_log3 ( event_time DateTime, url String, cnt UInt32 ) ENGINE StripeLog; -- 纯内存表重启数据丢 CREATE TABLE stats_mem ( event_time DateTime, url String, cnt UInt32 ) ENGINE Memory;这四个引擎的适用边界我直接说结论TinyLog适合测试、临时表、写一次读一次的小数据。不要并发写不要并发查数据量控制在几十万行以内。官方明确说“不建议在生产环境用于核心表”。Log适合“小批量追加写 偶尔并发读”的日志类场景。因为带有标记文件mark读的时候可以跳过不必要的列块比 TinyLog 灵活一些。同样只建议百万行以内。StripeLog把表内所有列的文件合并成一个好处是磁盘文件数量少适合你不太需要删除部分列的场景。在容器环境、文件句柄紧张时比较友好但并发写性能比 Log 弱一点。Memory完全驻留内存断电即失适合几万行以内的字典表、配置表、临时中间表。性能极快但不能当持久化存储。我实际踩过的一个坑是有人拿Log引擎存几千万行业务流水结果查询越来越慢因为 Log 引擎没有后台合并机制也没有主键索引全查询靠暴力扫描数据量一旦上去基本就是灾难。后来我把数据迁移到 MergeTree按日期分区加主键索引查询时间从十几秒降到几百毫秒。这个例子也说明轻量引擎只适合小需求ClickHouse 真正的吃饭家伙永远是 MergeTree 家族。2.3 为什么轻量表引擎不适合当在线数据库很多团队从 MySQL 迁移到 ClickHouse 时看到Log引擎就觉得“这就是行存”于是想用它替代 MySQL 里的在线业务表。这是最典型的误区。问题不在于引擎不能读、不能写而在于它的设计目标根本不是高并发在线事务没有事务支持写一半挂了表数据可能处于中间状态。没有主键约束和唯一性约束重复插入不会报错。没有单行更新的廉价路径你要改一行数据通常只能全表重写。并发写入要靠表级锁来保护多个客户端同时写容易报“Table is locked”之类的错误。数据文件不参与 MergeTree 的合并和分区淘汰长时间运行后文件碎片和膨胀无法收敛。所以我对这类引擎的定位永远是临时、测试、辅助、小规模。真要存储和分析业务数据果断 MergeTree。3. 支持“行级操作”的真实时间线UPDATE、DELETE、轻量删除3.1 早期版本只能靠分区重写后来才有的 mutation如果你关心的是“ClickHouse 什么时候能对某一行进行 UPDATE/DELETE 操作”这个问题的答案和物理行存完全不是一回事。在 ClickHouse 开源后很长一段时间里大家拿它做大宽表的分析存储几乎不聊行级更新。因为 MergeTree 是 LSM 思想数据写入以后会生成一个个不可变的 part后台再把 part 合并排序。你要改一行数据最朴素的做法是改写分区或重插数据。后来大致在 2018-2019 年的版本迭代里ClickHouse 引入了完整的 mutation 机制也就是ALTER TABLE ... UPDATE和ALTER TABLE ... DELETE。它的实现方式是异步改写执行语句后 ClickHouse 提交一个 mutation 版本号后台对相关 part 逐行重写标记旧数据失效最终把新数据替换进去。从使用角度mutation 让 ClickHouse 第一次有了标准的“改行”“删行”SQL。举个例子-- 把 user_id 100 的用户的积分改成 0 ALTER TABLE user_points UPDATE points 0 WHERE user_id 100; -- 删除昨天以前的所有事件 ALTER TABLE events DELETE WHERE event_date today() - 30;这两条语法直到现在也仍然可用。但要注意它是异步操作不是像 MySQL 那样执行完立刻生效。执行之后你可以在system.mutations表里看到这个 mutation 的状态等它跑完底层 part 才真正完成重写。从“什么时候支持”的角度我个人的时间线判断是如果要用于生产环境早期版本属于尝试期建议至少从 19.x 版本开始使用 mutation 场景。20.3 LTS 是我见过很多大厂在生产环境大量使用 mutation 的起点稳定性已经足够可靠。3.2 21.6 版本引入轻量级 DELETE这才是很多人眼里的“行存更新”真正的转折点在21.6。ClickHouse 在这个版本里加入了轻量级 DELETE 能力语法非常直观DELETE FROM events WHERE event_date today() - 365;这看起来和 MySQL 的DELETE FROM一模一样但它背后的实现机制完全不同。ClickHouse 并不会真的立即把物理数据删掉而是给符合条件的数据行打上删除标记is_deleted相关结构让查询时“看不见”这些行真正的空间释放和物理清理还是交给后台机制异步处理。这个能力解决了什么问题它把“删一行”的代价从 mutation 的全表重写级别降到了“标记 后台清理”级别对于按主键删少量行、清理过期数据这类场景特别友好。很多做用户标签、实时会话表的团队从 21.6 开始才真正敢把 ClickHouse 当成一个“可以删行”的在线分析库来用。需要单独说明的是轻量级 DELETE 适用范围是 MergeTree 家族表引擎但在启用它时也要注意如果频繁高频删除还是会产生大量“无效数据块”查询性能会出现波动。所以设计上应当把轻量删除用于低频清理而不是当 MySQL 一样高频删行。3.3 一张时间线表直接抄走版本阶段“行级操作”能力说明开源早期INSERT 按行写入但底层已是列存2018-2019ALTER TABLE UPDATE/DELETE mutation异步重写代价大20.3 LTSmutation 生产可用规划迁移时考虑21.6轻量级 DELETE FROM 语法标记删除代价小21.x 后续对 ReplacingMergeTree 的优化支持更灵活的去重适合 upsert 场景22.x 起事务能力实验性起步不要当常规事务库用注意最后一行ClickHouse 后面几个版本陆续加入了一些带BEGIN/COMMIT语法雏形的事务能力但直到如今也不建议把它当作可以频繁执行回滚、并发事务控制的数据库来用。它和 MySQL 的事务模型仍然不是一回事。3.4 实战验证用 Flink 同步 MySQL 到 ClickHouse 时更新该走哪条路聊到行级更新就必须提到热词里的“使用 Flink 实现 MySQL 同步到 ClickHouse”。这也是最多人踩坑的地方。很多团队用 Flink CDC 同步 MySQL 业务数据到 ClickHouseMySQL 里一行数据变了Flink 拿到 cdc 事件后要同步去 ClickHouse 更新对应的一行。如果 ClickHouse 表是简单的 MergeTree没有去重约束那么 CDC 的 UPDATE 事件在目标端执行时就非常尴尬——不知道要改哪个 part 里的哪一行。所以实际生产方案通常有三个方案一用 ReplacingMergeTree 加业务主键。建表时把业务主键作为排序键例如CREATE TABLE dwd_user_sync ( user_id UInt64, name String, points UInt32, update_time DateTime ) ENGINE ReplacingMergeTree(update_time) ORDER BY user_id;在 Flink 端把 MySQL 的 UPDATE 事件转成一次 INSERT或者 UPSERT直接插一条新数据。ReplacingMergeTree 在后台合并时会按照update_time保留最新一条等效实现了“按行更新”。方案二先轻量删除再插入。如果 Flink 版本支持你可以在目标端先执行DELETE FROM ... WHERE user_id xxx再插入新数据。21.6 之后这种模式可用但删除和插入之间如果间隔太长中间可能出现短暂的数据空缺或重复所以尽量用事务边界收口。方案三对高频更新场景用 Mutable/聚合引擎做特殊设计。比如用一个“增量表”记录所有变更事件再配合视图或物化视图聚合成最新状态。这已经不算是同步 MySQL而是事件驱动建模适合状态变化频繁的业务。从实践上我接触过的绝大多数同步链路都用方案一因为 ReplacingMergeTree 的异步合并对 Flink 端压力最小只要保证 INSERT 的主键唯一、时间字段正确最终一致性完全可以接受。4. “紧凑格式”是行存吗很多人看到 data.bin 就误会了4.1 什么是 Compact Part Format还有一个很容易让新手误会“ClickHouse 支持行存”的点就是 MergeTree 表的 part 存储格式。ClickHouse 在写入数据时会先把内存里攒的一批数据生成一个不可变的 part。早期wide 格式一个 part 里面每个列一个独立文件比如my_table_0_5_1/ ├── columns.txt ├── count.txt ├── primary.idx ├── col_name.bin ├── col_points.bin └── ...这种格式列文件很多当表特别小、part 特别多的时候会产生大量小文件严重浪费 inode也拖慢合并。所以后面 ClickHouse 引入了compact part 格式把一个 part 中所有列的数据合并写入同一个data.bin文件配合一个data.mrk2标记文件。就是这一个data.bin让很多人产生了错觉这不是把一行数据都塞在一起了吗这不就是行存吗4.2 Compact 格式的本质文件合并不是数据模型变换要打破这个错觉你只需要做一件事去看data.bin里面的布局。在 compact part 里ClickHouse 仍然按列组织数据只是把多个列的数据“拼盘”放进同一个文件的不同区域读数据时通过 mark 文件定位各列的位置。它合并的是“文件数量”没有改变“列式存取”的本质。一个直观的理解把 100 本书的内容装进一个口袋和把 100 本书分别装在 100 个口袋里内容的编排方式没有变化。compact 格式只是换了个“袋子”。什么时候触发 compact 格式呢和两个参数有关min_rows_for_compact_part默认值通常是 8192 行左右。min_bytes_for_compact_part默认值通常是 10485760 字节10MB。当新建的 part 行数小于上面的行数阈值、字节数小于字节阈值时ClickHouse 就会偏向写紧凑格式如果超过阈值自动切回 wide 格式。这样设计的原因很简单小 part 用 wide 格式会产生太多小文件compact 格式能有效减少文件数量大 part 再用 compact 格式会让随机读取定位变得复杂所以继续用 wide。查看 part 实际存储格式时你可以查系统表SELECT name, part_type FROM system.parts WHERE table my_table LIMIT 10;结果里你会看到Wide或Compact两种类型。很多 DBA 看到Compact就以为“这表是行存”这是一个非常常见的误区。4.3 那“Compact 格式是不是行存”的最终答案最终答案是不是。Compact 格式只是把每列的数据块塞进同一个文件它在读取时依旧按列加载在压缩时依旧按列压缩在索引处理时依旧按主键列做稀疏索引。它既不是RowStore也不会让单行随机读写变得更快。我见过一个特别典型的项目团队误把 compact part 当成行存来规划以为可以像 MySQL 一样高频更新单行结果上线后点查和更新性能都不符合预期。后来排查下来发现底层是列存模型无论文件怎么合并单行更新的代价还是远远高于真正的行存数据库。所以这个坑要写在最前面别被 data.bin 骗了。5. 想要“真正行存”该如何落地三种务实方案5.1 方案一小数据量直接用 Memory / Log配合字典表使用如果你的业务场景本质上就是需要一张“小表”几千行几万行按行查、偶尔更新那完全可以让 ClickHouse 当内存字典用。比如你有一张商品分类映射表、一张用户等级配置表数据量就几 KB但查询频次很高可以放 Memory 引擎。再配合一个启动脚本每次服务启动时从 MySQL 或者文本文件重新加载进 ClickHouse。这样既享受 ClickHouse 极快查询又规避了持久化问题。如果你需要持久化但又确实只有几十万行可以用 Log 引擎定期全量覆盖重建。比如每天凌晨把业务系统生成的标签文件导一次白天只读不写。这种用法完全没问题。5.2 方案二数据量中等需要用 Join 引擎或外部行存引擎ClickHouse 还有几个特殊的引擎值得关注Join引擎右表数据加载进内存哈希表左表大查询时直接JOIN效果像在行存里做个关联。DictionaryClickHouse 的字典功能天然适合存放“小维度表”本质就是把外部数据加载进内存按 key 精准取 value。外部表引擎MySQL、PostgreSQL、SQLite可以在 ClickHouse 里直接扫描一张 MySQL 的行存表。以 SQLite 引擎为例CREATE TABLE sqlite_test ENGINE SQLite(path/to/sqlite.db, table_name);通过这种引擎你实际上做到了“ClickHouse 里能查行存数据库的数据”。但需要注意这类查询是直接把请求推到外部数据库执行实时性和并发能力由外部数据库决定ClickHouse 只是做了个远程连接桥。适合偶尔关联查一下不适合核心数据链路。5.3 方案三大表也需要按行定位用 MergeTree 主键索引实现高效点查如果你的数据量很大千万、亿级但仍然需要按某个 ID 查一行那也不需要行存。MergeTree 的主键稀疏索引足以支撑高性能点查。关键在于建表设计CREATE TABLE user_events ( user_id UInt64, event_time DateTime, event_type String, payload String ) ENGINE MergeTree PARTITION BY toYYYYMM(event_time) ORDER BY (user_id, event_time);当你执行SELECT * FROM user_events WHERE user_id 12345 ORDER BY event_time DESC LIMIT 10;ClickHouse 会通过稀疏索引快速定位到user_id 12345所在的 granule默认 8192 行一个粒度然后只扫描这一小块数据而不是全表扫描。我的实际经验是在百亿级表里按主键点查毫秒到几十毫秒级别是完全可以做到的。有人会问那如果我不小心把粒度设得太大点查会变慢吗会。这就是为什么建表时 ORDER BY 的设计那么重要。你不能随便拿一个字段当排序键而要根据最常见的查询条件去设计。ClickHouse 不是行存库但通过合理设计排序键和主键它可以把单行查询做得很快。5.4 方案四需要严格意义上的“按行更新”用 ReplacingMergeTree 做 UPSERT前面聊 Flink 同步时提到过 ReplacingMergeTree这里单独展开一下。如果你不只是查单行还希望“这行数据变了最终要反映到 ClickHouse”那必须接受一个模型转变把 UPDATE 变 INSERT靠合并去重。CREATE TABLE orders_sink ( order_id UInt64, order_status String, total_amount Decimal(18,2), updated_at DateTime ) ENGINE ReplacingMergeTree(updated_at) ORDER BY order_id;每次 MySQL 里订单状态变了你就在 ClickHouse 里插一条新数据order_id 相同、updated_at 更新。ClickHouse 后台合并时保留 updated_at 最大的一行从而在最终状态上实现“更新”。这个方案的优点是写入路径简单、吞吐高、天然适配列存缺点是合并不是实时的如果查询没有触发合并短时间内可能看到同一个 order_id 的多条版本数据。所以查询时通常要加FINAL关键字或者在查询时自己按 updated_at 取最大SELECT * FROM orders_sink WHERE order_id 999 ORDER BY updated_at DESC LIMIT 1;你要问我什么场景用这个我建议所有“从业务库同步大量数据到分析库”的场景都优先考虑这种模式而不是反复去执行 mutation。mutation 可以偶尔用高频用一定出问题。6. 常见问题与排查技巧实录6.1 我建了 Log 表为什么大量并发写入会报错Log 引擎使用表级锁并发写入时会出现Table is locked或者Attempt to read lock之类的报错。解决办法很简单换成 MergeTree 表或者对 Log 表做单写者约束。另外Log 引擎写入时要追加数据到文件如果你频繁写入又频繁重启文件可能损坏重新 select 时直接报异常。在实际环境中我曾经见过一个团队把日志采集的落地点用 Log 引擎十几个采集线程同时写时不时就出现写入失败。后来改成 MergeTree 按小时分区写入不再有锁冲突查询性能也更稳。6.2 轻量级 DELETE 执行完数据还是能查到这是 21.6 开始的轻量删除最容易遇到的问题。DELETE FROM在默认配置下是一个异步操作执行完立刻SELECT有时还能看到删除的数据原因是删除标记还没有生效或者后台清理还没跑。你可以用这个查询确认删除状态SELECT name, is_deleted, rows FROM system.parts WHERE table your_table AND active 1;如果希望删除尽快可见可以调整lightweight_deletes_sync设置或者强制执行一次 mutation 同步等待SET lightweight_deletes_sync 1; DELETE FROM your_table WHERE ...;这里有一个需要接受的现实ClickHouse 不是强实时 OLTP它是强实时写入、准实时一致的分析库。把“删除可见性”也当 hBase 或 MySQL 来要求会让你很痛苦。6.3 Doris 和 ClickHouse 选型时能不能把“行存”当成差异点不能。热词里有“Doris 和 ClickHouse 的选型”我多说一句。Apache Doris 同样是一个列存分析型数据库它的主要存储模型也是列存不是行存。所以如果你在 Doris 和 ClickHouse 之间犹豫不要拿“谁支持行存”来选型因为两者在这一点上没有质的差别。真正需要比较的是集群运维成本Doris 的 FE/BE 架构对大规模集群更友好ClickHouse 的集群要求你更懂 shard/replica 的设计。JOIN 能力Doris 的查询优化器在复杂 JOIN 上比 ClickHouse 更稳定ClickHouse 强在单表大宽表聚合。高并发点查两者都在优化但都不如真正的 OLTP 行存数据库。生态和社区ClickHouse 生态更大Flink/BI 工具适配更成熟。把“行存”作为选型标准大概率是走进死胡同。6.4 银河麒麟离线部署注意版本和包的选择热词里还出现了“银河麒麟 ClickHouse 离线安装包下载”。如果你正在国产化环境做部署ClickHouse 的 RPM/DEB 包在 x86_64 或 ARM64 下都有对应产物。离线部署时建议直接下载对应发行版的包例如 CentOS 系的.rpm再用yum localinstall或rpm -ivh安装。一个很实用的经验ClickHouse 21.8.15.7 这一代 LTS 版本在离线环境里很好用因为它是 ClickHouse 官方长期支持的版本功能覆盖了轻量 DELETE、ReplacingMergeTree 优化、良好的备份恢复工具适合在锁网环境里部署。装完以后记得把system.text_log、system.query_thread_log这些系统日志表打开否则排查问题会少很多线索。最后聊点实在的写到这里核心问题已经回答完了。我个人在实际操作中的体会是纠结 ClickHouse 是否支持行存往往是因为你还没有从“行存思维”切换到“列存思维”。一个高效使用 ClickHouse 的团队不会天天问“怎么改一行”而是会问“这一行数据的变化能否建模成一行新数据的插入”。顺着这个思路走你会发现 ClickHouse 的能力远比你想象得大。再分享一个小技巧如果某天你在 ClickHouse 文档或文章里看到“row storage”之类的词先别急着下结论。去确认一下它到底说的是物理存储格式还是外部数据源还是某种轻量引擎的别名。你以为的“行存”八成不是你以为的那个意思。ClickHouse 的强项从来不是“像 MySQL 一样存一行”而是“用列存的方式把海量数据算得飞快”。理解这一点很多架构纠结都能迎刃而解。
返回列表