接触一个新组件时,我以前习惯先看它支持什么:吞吐多少、延迟多低、能不能横向扩展。后来发现,只记这些结论很容易在选型时失去判断力。同一句“查询很快”,放在不同数据量、查询模式和硬件条件下,可能完全不是一回事。
更有用的问题是:它为什么快,代价放在了哪里?
以 ClickHouse 和 MySQL 为例。两者都能写 SQL,但解决的问题不同。
InnoDB 以行为单位组织数据,表本身按主键构成聚簇 B+ 树。一条记录的各列放在一起,主键点查、范围更新和事务处理都很自然。代价是分析查询只需要三列时,仍可能读入大量无关列;遍历海量记录做聚合,也不是它最擅长的路径。
ClickHouse 的 MergeTree 按列存储,同一列的数据类型相同,更容易压缩,也只需读取查询涉及的列。数据按 ORDER BY 排序,稀疏主键索引先排除不相关的数据块,再由多个线程、多个节点并行扫描,聚合过程还能利用向量化执行。
这解释了为什么合适模型下,ClickHouse 能在很大的明细表上保持很低的分析延迟。但“11 个节点、亿级数据、毫秒查询”不是无条件成立的。查询命中多少分区、排序键是否匹配、是否使用预聚合、数据是否在缓存、节点和网络配置如何,都会改变结果。一个扫描全部高基数字段的复杂 Join,不会因为换了 ClickHouse 就自动变成毫秒级。
反过来,ClickHouse 也不是更强的 MySQL。频繁单行更新、强事务、外键约束、按主键处理订单状态,这些场景继续选择 MySQL 往往更稳妥。
所以选型不能写成“ClickHouse 快,所以选 ClickHouse”,至少要回答四件事:
- 业务主要是点查、事务写入,还是大范围聚合?
- 性能优势来自哪种存储和执行设计?
- 为这个优势放弃了什么?
- 数据规模、查询模式变化后,原来的选择是否仍成立?
大数据平台接入 MySQL、ClickHouse、Hive 等数据源时,我越来越觉得“统一接口”只应统一连接和管理体验,不应该抹平底层差异。知道差异从哪里来,下一次遇到日志分析、实时指标或交易数据,才有重新选择的能力。
再看一个更小的选择:MySQL 主键
技术差异不只发生在数据库之间,同一个数据库内部,一个字段的选择也可能改变底层行为。
分布式系统里,UUID 很诱人:不依赖数据库就能生成,跨节点几乎不会冲突,也不会像自增 ID 那样暴露大致的数据量。但在 InnoDB 中,把随机 UUID 直接作为主键,需要付出几笔隐形成本。
InnoDB 的表数据按主键组织成聚簇 B+ 树。自增主键大体按顺序写到索引右侧,新记录的位置集中,页分裂和随机 I/O 相对少。随机 UUID 会把插入位置打散到整棵树,数据量大时更容易触发页分裂,也会降低缓存局部性。
主键长度也会继续放大差异。BIGINT 是 8 字节,文本形式的 CHAR(36) UUID 更大;而每个二级索引叶子节点都会保存主键值。主键一长,所有二级索引一起变胖,缓存能容纳的索引项变少。
大量插入场景下,可以把内部主键和外部业务标识拆开:
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
biz_id BINARY(16) NOT NULL UNIQUE
内部关联和聚簇组织使用自增 ID,对外暴露 UUID。若确实要求分布式生成主键,也可以考虑时间有序的 UUID、ULID 或 Snowflake 类 ID,并用 BINARY(16) 代替字符串存储 UUID。
自增 ID 同样有代价。跨库合并要处理冲突,分库分表需要额外的 ID 方案,连续编号还可能泄露业务规模。关键仍然是追到底层:主键是否同时承担了物理存储顺序、内部关联和外部标识?这三件事是否真的必须绑在一列上?
参考: