1. 行式存储是什么:先搞清楚“一行数据”到底存在哪
1.1 从表到页到行:存储引擎的最小组织单元
很多人一听到“行式存储”,第一反应就是“这不就是普通的数据库表吗”。这个理解没有错,但不完整。行式存储的准确含义是:数据以“行”为单位连续落在物理存储介质上,同一行的所有字段紧密排列在一起,谁来了都能整行拎走。
我最早接触这个概念是在刚做数据库相关工作时,那时候排查 SQL 慢查询,老大让我去查“页”的概念。我当时的想法是:数据库不是一张表吗,跟页有什么关系。后来才明白,表只是逻辑概念,真正的物理存储单位是“页”(Page),默认一页 16KB。InnoDB 把一页分成若干槽位,每个槽位里放的就是一条条完整的行记录,行与行之间按主键顺序排列。你一查SELECT * FROM user WHERE id = 123,存储引擎的查找路径是这样的:先通过 B+ 树从根节点定位到叶子节点所在的页,再把那一页读进内存缓冲池,在页内二分查找命中的行。整个过程的核心假设是:你要的数据就在某几页里,而不是分布在整个表的几百个页里。
这就是行式存储与其他存储方式最本质的差异。它在一开始就被设计成“面向单行操作”的结构,而不是“面向大批量列的聚合操作”的结构。大数据场景里你经常看到的列式存储、宽表存储、OLAP 引擎,走的是另一条完全不同的路,这一点后面我会展开说。
1.2 行的“内部结构”:记录头、定长/变长字段、NULL 位图
行式存储看起来简单,但一条行记录的真实结构比你想的复杂。以 InnoDB 的 Compact 行格式为例,一条记录分为两部分:记录头信息和记录体数据。
记录头信息大约占 5 字节,包含了一些关键的元数据,比如“该记录是否被删除”的标记位、“同一页中下一条记录的相对位置”、字段数量信息等。这些信息普通开发者看不到,但存储引擎靠它来维护页内的记录链表。记录体这部分就有意思了:对于定长字段(比如 INT、BIGINT、DATETIME),直接按固定字节存储;对于变长字段(比如 VARCHAR、TEXT),则先存储一个长度前缀,再存实际数据。你还得考虑 NULL 值怎么处理,Compact 格式里会用一个 NULL 位图来标记哪些字段是 NULL,并不是真的在每个字段里存一个“NULL”字符串。
我举一个实际例子。假设有一张用户表:
CREATE TABLE user_info ( id BIGINT PRIMARY KEY, name VARCHAR(50), age INT, profile TEXT, created_at DATETIME ) ROW_FORMAT=COMPACT;当你向这张表插入一条数据时,物理上它大致长这样:先是 5 字节记录头,然后是id的 8 字节,再是name的变长长度(1~2 字节)+ 实际字符数据,再是age的 4 字节,然后是profile的长度前缀 + 数据,最后是created_at的 8 字节。注意,变长字段的实际内容并没有和主键字段对齐,而是紧凑地按顺序排列。这意味着:只要这条记录被定位到,无论你想取哪个字段,存储引擎必须要读取整条记录才能把字段解析出来。这就是“行式”二字的字面意思。
很多做大数据开发的同学后来习惯了列式存储,第一次回看 MySQL 的行结构会觉得“浪费”。确实,如果你只需要age这一列,行式存储也要把profile这种大字段一起读进来。这是行式存储的天然代价,也是我们后面要谈应用场景时必须考虑的核心前提。
1.3 聚簇索引:为什么主键查询能把响应压到毫秒级
行式存储的查询性能高度依赖索引,而索引里最关键的就是聚簇索引。InnoDB 的表数据本身就是按照主键构建的 B+ 树,叶子节点直接存储整行数据。所以“主键点查”这条路径非常短:根据主键在 B+ 树中二分查找,走 3~4 层树高就能定位到行,然后把叶子节点所在页读入内存,返回结果。
这里有一个很容易忽略的细节:二级索引(非主键索引)的叶子节点存的是什么?它不是整行数据,而是主键值。如果你建了一个name字段的索引,执行SELECT * FROM user_info WHERE name = '张三',流程是:先在二级索引 B+ 树中查到张三对应的主键id,然后“回表”到聚簇索引里再查一次完整行。这就是为什么有时候明明有索引,查询还是很慢——回表次数一多,IO 开销就上来了。
理解聚簇索引的意义在于:行式存储把主键点查做到了极致,但前提是你得“顺着主键去查”。如果你把一张表设计成没有明确主键、全靠二级索引过滤,性能会大打折扣。这也是行式存储应用场景里一个反复出现的判断标准:你的核心查询是不是围绕主键展开的?是,就适合行式;不是,就要谨慎。
2. 行式存储为什么快:硬盘 IO 与缓存体系下的核心优势
2.1 局部性原理:一次读盘拿到整行
行式存储最核心的性能逻辑是“局部性原理”。磁盘 IO 的最小单位不是一个字节,而是一个块(通常也是 4KB~16KB 的倍数)。当你需要读取一行数据时,实际上是把整页加载进来。如果是行式存储,这一页里包含的是多个完整的行,而且这些行往往逻辑相邻(比如同一时间插入的用户数据),所以一次 IO 能同时满足多个行的读取需求,命中率极高。
我做过的实际案例里,一个典型的用户登录场景就是吃这个红利。用户表每天几百万条记录,但单次登录查询只关注一个用户 ID。这个查询走主键,一次 B+ 树索引定位,最多加载一两个页,IO 次数稳定在个位数,加上缓冲池命中,P99 响应时间轻松压在 10ms 以内。换成列式存储,虽然压缩率和批量扫描很强,但要定位某一个用户的所有字段,你需要去各个列文件中找数据,反而把简单问题复杂化了。
所以行式存储的第一个优势总结成一句话:当你需要“某一行的所有字段”时,它是最省 IO 的存储布局。
2.2 写放大与随机读写:事务型负载的天然适配
事务型系统(OLTP)的特点是什么?大量并发的小事务,每个事务改的数据量不多,但对响应时间极其敏感。行式存储在这方面几乎是量身定做的。
MySQL 处理一条UPDATE,本质上是在原行位置做修改,或者因为页分裂而移动记录。无论哪种情况,它只需要定位这一行、锁住这一行、修改对应页的数据,然后写 redo log。整个过程不涉及把整列数据重写一遍。列式存储则正好相反,它的写入往往是批量追加,单行更新非常别扭,因为一行数据被拆在几十个列文件里,改一个字段就要动一个文件。这也是为什么即使在大数据时代,在线交易系统依然死守行式存储。
另外,行式存储的随机读写能力也被很多文章低估。大家总说“随机 IO 慢”,但现代存储引擎通过缓冲池、预读、组提交等手段,把随机写转换成了理论上可控制的异步刷盘。InnoDB 的 Change Buffer 就是一个典型设计:二级索引的更新先缓存在内存中,后续再批量合并。这类机制只有在行式存储这种“单行操作频繁”的模型下才有意义。
2.3 与应用程序对象模型的直接映射
这一点很少被讲透,但实际开发中特别重要。绝大多数后端应用是面向对象写的,一个 Java 类或者 Go struct,对应数据库一张表,一个实例对应一行数据。ORM 框架(MyBatis、Hibernate、GORM)在底层做对象关系映射时,天然适合行式存储:查出来一行,塞进一个对象,完事。
如果是列式存储,你要查 10 个字段,得从 10 个列文件分别捞数据,再在应用层拼装成对象。这个过程在大数据框架里叫“物化”,是需要额外成本的。而行式存储“SELECT *”直接返回完整行,应用层代码写起来极其顺畅。
我记得有一次把一个分析模块从 ClickHouse 迁回 MySQL,原因就很简单:数据量不大,但业务逻辑全是单行更新和对象级映射,用列式引擎纯属给自己找麻烦。反过来,如果你都是几百亿行的聚合统计,天天要求我写SELECT COUNT(*) FROM ... GROUP BY ...,那你就该往列式方向走。这没有对错,只有适配。
3. 行式存储的“短板”:为什么大数据分析场景常绕开它
3.1 扫描整表的 IO 代价
行式存储最大的短板,就是全表扫描。
道理其实很朴素:既然一行数据的所有字段都紧紧挨着,那你要扫 100 万行的某个字段,也得把 100 万行的其他字段全部读出来。在机械硬盘时代,这是致命的;在 SSD 时代,虽然随机 IO 大幅改善,但数据量一上来,IO 吞吐依然是硬瓶颈。
你可以做个简单估算。假设一张表 1 亿行,每行平均 1KB,那么这张表就是 100GB。如果执行一个聚合查询,只需要其中 3 个字段,总共约 30GB 的数据。行式存储不得不把 100GB 全部读进内存再去过滤。而列式存储只读那 3 个列文件,IO 量直接降一个数量级。这就是为什么像 ClickHouse、Doris、HBase 的列族设计、Parquet 这类格式,在分析型场景里能跑出行式存储望尘莫及的速度。
3.2 压缩率低与列裁剪缺失
列式存储还有两个行式存储很难追的优势:高压缩率和列裁剪。
列式存储里,同一列的数据类型一致、值域相对集中,很容易利用字典编码、RLE(游程编码)、Delta 编码等手段把数据压缩到原尺寸的 20%~30% 甚至更低。行式存储由于每行数据都是不同字段混杂,类型不统一,压缩算法只能在整页层面做,效果差一大截。压缩率又直接关系到 IO 量的多少,这又叠加了前面说的扫描差距。
列裁剪则更是行式存储的死穴。所谓列裁剪,就是查询里只访问指定的列,存储引擎可以跳过无关列。行式存储的结构根本不支持这种跳过,你再怎么裁剪,物理上整行还是会读出来。如果一张表有 50 个字段,业务查询只需要 5 个,行式存储的 IO 浪费就是 10 倍起步。
3.3 行式 vs 列式:一张决策表讲透差异
我在选型的时候习惯用一张表把问题摆在桌面上,每次和团队评审都直接对着聊:
| 维度 | 行式存储 | 列式存储 |
|---|---|---|
| 物理布局 | 按行连续存储,一行数据集中在一起 | 按列连续存储,同列数据集中在一起 |
| 典型代表 | MySQL InnoDB、PostgreSQL、Oracle | ClickHouse、Parquet、ORC、Doris |
| 单行点查 | 极快,走主键索引 | 较慢,需要跨列文件组装 |
| 高频更新 | 天然支持,行锁粒度小 | 不擅长,重写列文件开销大 |
| 批量聚合 | 全表扫描代价高 | 列裁剪 + 高压缩,性能碾压 |
| 压缩率 | 一般,跨类型混存 | 高,同类型聚集便于编码 |
| 典型场景 | OLTP、点查、事务、实时写多 | OLAP、聚合报表、数仓、批量扫描 |
注意这张表不能只看“谁快谁慢”,核心是“负载类型与存储结构的匹配度”。我见过不少团队把业务库扩大几倍后,把报表查询直接怼在业务库上,跑不动就怪行式存储不给力。实际上你用错了工具。行式存储是给“交易”用的,列式存储是给“分析”用的,两者在同一个大数据系统中往往是互补关系。
4. 行式存储应用场景全解析:什么时候该选它
4.1 事务型系统:订单、账户、库存的实时读写
第一个必须用行式存储的场景,就是事务型在线系统。这里最典型的例子是三高(高并发、高可用、高一致)交易系统:订单表、账户表、库存表。
这类系统的特点是什么?单条记录访问极其频繁,而且必须立刻看到最新状态。比如电商库存扣减,查询SELECT stock FROM inventory WHERE sku_id = ?,随后执行UPDATE inventory SET stock = stock - 1 WHERE sku_id = ?。这种操作要求在同一行上做事务性读写,要求行级锁控制并发,要求事务提交后数据立即一致。这套能力是行式存储的看家本领。
我记得在某个电商项目中,库存接口的峰值 QPS 到了两万左右,底层就是 MySQL 行式存储。我们没有用什么高端分布式数据库,靠合理的分库分表 + 行级锁 + 缓存降级,就稳稳扛了下来。如果这时候强行上列式存储,光是把一行库存数据从多个列文件中重新组装起来,延迟就无法接受。
4.2 主键点查与数据修改密集场景:用户中心、会话管理、配置中心
除了交易系统,还有一类场景也天然适合行式存储:主键点查与修改密集的系统。
用户中心就是最典型的例子。用户注册、登录、资料查看、资料修改,这些操作全是围绕user_id展开的单行读写。会话管理也是一样,每次请求都要按session_id去查对应的会话数据,更新过期时间。配置中心更不用说,几千个配置项,每个配置项都是独立修改的单元。
这类场景的共同特点是:访问路径高度收敛,数据修改频率高,单次访问数据量小。这些条件一旦满足,行式存储的表现就是最优的。而且这类系统的数据量通常不会大到超过单机上限,一个主从架构就能解决容灾和读写分离问题,成本远低于引入一套分布式列式引擎。
我之前接手过一个会话清理项目,原本所有会话数据放在 Redis,但因为要支持持久化和历史查询,需要把过期会话落库。当时有人提议用 ClickHouse,我直接建议落 MySQL 行式存储。原因很简单:会话查询全是点查,写入是单条 upsert,偶尔做批量删除,这些操作列式引擎全都别扭。最终 MySQL 一张表轻松搞定,查询延迟和更新延迟都在合理范围。
4.3 小表与元数据表:频繁关联的数据量不大
还有一种经常被低估的行式存储场景,就是小表与元数据表。
大数据平台里经常有各种“维度表”,比如地区表、类目表、状态枚举表、产品线映射表。这些表可能就几千行甚至几百行,但被大表高频 join。这种情况下,用什么存储引擎影响不大,但行式存储的管理和运维成本最低,和业务系统的 ORM、事务、导出导入工具链兼容性最好。
有人可能会说,几千行的表放哪都一样。对,但你要体会“频繁关联”这四个字。维度表 join 往往需要重复读取同一批行,行式存储天然适合把整行反复拉取、放进缓冲池热区。列式存储虽然也能放小表,但它的优势反而发挥不出来——列裁剪和压缩在这种数据量面前毫无存在感。
4.4 不应硬选行式的场景:离线分析、大规模宽表、全表聚合
有适合就要有不适合,这里我得把话说明白,帮大家避坑。
如果你面对的是离线分析、BI 报表、用户行为日志聚合这类场景,数据量动辄数十亿行,查询模式是SELECT region, COUNT(*) FROM ... GROUP BY region ORDER BY cnt DESC,那就不要用行式存储硬扛。行式存储在这种负载下会被列式引擎按在地上摩擦,不管你怎么加索引、怎么调参,全表扫描的先天劣势摆在那里。
还有一个容易踩坑的“大规模宽表”场景。业务方为了省事,把几十个业务字段塞进一张表,然后对你提需求:我要看这 30 个字段的分布情况。在行式存储里,这基本上等于每次查询都要做一次全表 IO,还可能把缓冲池打爆。正确的姿势是把这类宽表同步到数仓或 OLAP 引擎,用列式存储来处理;或者回到源头,把宽表拆成符合范式的窄表。
我见过最惨的案例是一个日志分析系统,初期直接用 MySQL 存用户点击日志,一天写入几亿行,查询还要跨字段统计。上线没多久,磁盘 IO 直接被拖垮,连正常的业务查询都被连累。最后改造方案就是把日志链路切换到消息队列 + 列式存储分析引擎,MySQL 只保留最近几小时的“热数据”供在线查询。这才算把行式存储放回了正确的位置。
5. 实操要点:行式存储的参数选型与调优
5.1 行格式选型:Compact / Dynamic / Redundant
实操层面,行式存储的配置调优有很多“文档里不写”的细节。先从最基础的行格式说起。以 MySQL InnoDB 为例,行格式主要有三种:REDUNDANT、COMPACT、DYNAMIC、COMPRESSED(后两者其实是 COMPACT 的扩展)。
REDUNDANT 是最老的格式,现在基本没人用了,它不支持变长字段的离线存储优化,行内空间浪费严重。COMPACT 解决了变长字段和 NULL 存储的问题,是 5.6 之前的默认格式。DYNAMIC 是 5.7 之后推荐使用的格式,它最重要的改进是:当一行数据太长,无法放进一个页时,变长字段(比如 TEXT、BLOB 类型的大字段)会被“溢出”存放到单独的页中,而行记录里只保留一个 20 字节的指针。COMPRESSED 则是在 DYNAMIC 基础上支持页级压缩。
我在建表时的默认建议是:
CREATE TABLE example_table ( ... ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;为什么默认 DYNAMIC?因为现在的业务表里几乎都会有 VARCHAR(255)、TEXT、JSON 这类变长字段,DYNAMIC 格式可以有效减少“行溢出”带来的行内膨胀,让单页能容纳更多行记录,从而提升缓冲池的命中率。如果你用 COMPACT,遇到大字段就会遇到尴尬情形:一个页只能放几行甚至一行,点查时每次都要多读几个页,性能自然下降。
5.2 主键设计与页分裂的连锁效应
行式存储的主键设计,不只是唯一标识那么简单。InnoDB 聚簇索引本质上就是主键 B+ 树,主键插入的顺序直接决定了页的填充率与分裂频率。主键如果是自增 ID,新行永远追加到 B+ 树的最右侧,写操作只碰当前的“热点页”,页分裂概率低,顺序写入性能好。主键如果是 UUID 或随机字符串,每次插入都要落在树中间某个位置,可能导致页分裂、页移位,随机写放大严重。
我做过一个对比测试:同样是两千万行数据,自增主键表的批量导入耗时不到 1 分钟,UUID 主键表耗时整整多了 5 倍不止。原因不是索引本身慢,而是页分裂带来的额外 IO 和碎片整理成本。
所以操作建议很直接:
- 单机行式存储表,优先使用自增主键或单调递增的业务序列号。
- 如果有分布式 ID 需求,用雪花算法生成的 ID 也是递增趋势,需要注意时钟回拨问题,但整体比 UUID 好得多。
- 如果业务要求必须用 UUID 做主键,建议把 UUID 转成 BINARY(16) 存储,而不是 VARCHAR(36),至少能减少索引体积和比较开销。
另外,业务上要区分“主键”和“逻辑主键”。你完全可以在表里加一个无业务含义的id BIGINT AUTO_INCREMENT作为物理主键,再把业务唯一键(比如订单号)建成唯一索引。这样既享受了顺序写入的优点,又能保证业务约束。
5.3 缓冲池、页大小与事务参数:可调的但别乱调
行式存储的性能高度依赖内存缓冲。以 InnoDB 为例,innodb_buffer_pool_size是重中之重,一般建议设为可用物理内存的 60%~70%。如果缓冲池太小,热点数据频繁被淘汰,每次查询都要从磁盘重新读页,即便行式存储的点查再快也会被磁盘 IO 拖死。
页大小innodb_page_size默认 16KB。这个参数通常在实例初始化时确定,后期无法修改。页大小越大,单页容纳的行越多,顺序扫描时 IO 效率更高,但也会增加单页写入的锁竞争;页越小,适合大量随机小点查,但索引树会更高。绝大多数场景保持默认 16KB 即可,不要为了“优化”乱动。
事务参数方面,innodb_flush_log_at_trx_commit是一个需要权衡的点。如果设为 1,每次事务提交都要刷盘,保证数据不丢失但性能稍差;设为 2,则每秒刷一次盘,性能提升但可能丢最近 1 秒的事务数据。金融类业务建议保持 1,日志类、非关键业务可以考虑 2。很多人不理解为什么要在这个参数上纠结,其实就是一致性、性能、成本三者的取舍,没有绝对最优。
5.4 索引与回表的取舍:覆盖索引是最优解
行式存储索引调优里,我特别想讲“覆盖索引”这个概念。
前面说了,二级索引叶子节点保存的是主键值,如果查询列不在索引里,就需要回表。回表一次两次还好,但如果查询结果集有几千上万行,回表次数就是几千上万次,性能瞬间崩塌。
避免回表的办法,就是建立覆盖索引:把查询需要的字段都包含在同一个二级索引中。比如:
CREATE INDEX idx_name_age ON user_info(name, age);当查询为SELECT name, age FROM user_info WHERE name = '张三'时,优化器发现二级索引里已经包含name和age两个字段,就不需要回表了。这里有个实际开发里常见的优化案例:某个列表页查询,原本SELECT *加排序,回表严重,我改成只返回列表页需要的几个字段,并建立联合索引覆盖排序字段和返回字段,查询耗时从 200ms 降到 5ms。变化就是这么明显。
但覆盖索引也不是越多越好。每多一个索引,写操作的代价就多一份。表上的索引数量尽量不要超过 5~6 个,否则写入瓶颈会让你的数据库抗并发能力急剧下滑。
6. 常见问题排查与避坑实录
6.1 “行数不多但查询慢”:要会看执行计划
我在实际排障时最常遇到的一句抱怨是:“这表才几百万行,怎么查一个用户要 1 秒多?”几百万行确实不多,但查询慢的根因往往不是数据量,而是执行计划出了问题。
第一件事永远是看执行计划。以 MySQL 为例:
EXPLAIN SELECT * FROM user_info WHERE mobile = '13800000000';看到type=ALL,说明是全表扫描;看到type=ref或const,才是索引命中。再看rows,这个字段是优化器估算的扫描行数。如果 rows 是几十万而实际命中的只有一条,那就说明索引失效了。
索引失效的常见原因有:字段上使用了函数(WHERE DATE(created_at) = ...)、隐式类型转换(字段是 VARCHAR 但传入的是数字)、前导模糊查询(LIKE '%abc')、联合索引没走最左前缀。每一个我都在生产环境里踩过,排查时照着这个清单逐项排除,基本能定位 80% 的问题。
6.2 热点行与死锁:并发事务的现实碰撞
行式存储解决并发靠的是锁,但锁本身就是一种代价。高并发场景下最典型的问题有两个:热点行竞争和死锁。
热点行竞争很好理解。比如秒杀商品,所有请求都在减同一行的库存。虽然 InnoDB 行锁粒度小,但同一行的并发更新会串行化。你把这行的更新从每秒 500 次提到每秒 2000 次,数据库就会开始锁等待,随之而来的是连接堆积。解决办法通常是削峰:要么用 Redis 预扣库存,要么用异步队列把请求串行化,再要么升级成行级批量扣减。总之一句话:不要让数据库单行成为系统的唯一瓶颈。
死锁则是另一个让人头疼的问题。两个事务各自持有对方需要的锁,互相等待,最终被 InnoDB 检测机制判定并回滚其中一个。排查死锁最直接的办法是看:
SHOW ENGINE INNODB STATUS;观察LATEST DETECTED DEADLOCK部分,里面会给出两个事务的 SQL 和锁信息。实践经验是:多个事务如果要操作多张表,尽量保持相同的访问顺序;如果操作多行,尽量一次性锁定所有需要的行。比如先查两个 ID,排序后再更新,这样能显著降低死锁概率。事务里宁可多带几条 SQL,也不要分多次开启,每多一次交互就多一次锁等待窗口。
6.3 大字段溢出与行碎片:隐形杀手
前面说了 DYNAMIC 行格式会把大字段溢出到独立页,这虽然解决了“行过大”的问题,但也带来了查询代价:当你 SELECT 大字段时,存储引擎需要额外读取溢出页。一条带 TEXT 字段的行,可能在主行读完后还要再跳一次 IO。如果业务查询里频繁 SELECT *,并且表里有几个 TEXT/BLOB 字段,IO 消耗会比预想的大得多。
我的建议是:把大字段单独拆表。比如用户表里放基础资料,用户简介、头像 URL、JSON 扩展字段放到独立的user_profile_detail表,一对一关联。需要列举用户列表时只查主表;需要看详情时才 join 副表。这个设计在行式存储下特别有效,既能减少主表行宽,让单页容纳更多记录,又能避免大字段拖慢所有小查询。
行碎片的问题则和随机删除、随机更新有关。频繁 DELETE 会在页中留下“删除标记”的空洞,频繁 UPDATE 变长字段可能让行移动到新位置,页内碎片越来越多。解决方式一般就是定期 OPTIMIZE TABLE 或重建表。我记得有个业务表数据量没涨但查询越来越慢,跑了 ANALYZE 之后发现页填充率只有 60%,做了一次重建才把查询时间压回去。
6.4 什么时候该迁移:识别“行式不适用”的信号
最后我想聊一个同样重要的问题:怎么判断系统该从行式存储迁出去了。经验丰富的人不会等到数据库挂了才想迁移,而是会提前识别信号。
几个非常典型的信号:
- 查询里大面积出现
GROUP BY、COUNT、SUM、JOIN大表,而且响应时间持续恶化。 - 表行数达到几亿,而且大多数查询并不是“按主键查单行”,而是“按非唯一字段过滤多条记录”。
- 业务对数据的实时性要求降低,允许延迟几秒到几分钟,但仍然跨不过性能瓶颈。
- 数据增长迅猛,冗余和归档数据越积越多,在线事务库越来越臃肿。
出现这些信号时,我的处理思路不是“立刻换库”,而是先做分层:在线热数据继续留在行式存储,历史数据和离线分析数据同步到列式存储/数仓。常见做法是用数据同步工具把 MySQL 实时同步到 ClickHouse 或 Doris,业务查询按需路由:实时点查走 MySQL,统计聚合走 OLAP 引擎。这个架构不折腾、不冒进,几乎是所有大数据团队都会走的一条成熟路径。
我个人在实际操作中有一个小习惯:每次建表前先问自己三句话——这张表主要按什么条件查?一次查询要拿多少行?拿到的行里要几个字段?如果答案都是“按主键查单行、拿整行”,那就放心用行式存储。如果答案是“按范围查几万行、只取一两个字段做统计”,那要么换列式引擎,要么做好长期优化索引的心理准备。这个判断帮我避了无数个坑。行式存储不是过时技术,它是整个大数据体系里绕不开的基石,关键是你得知道它的边界在哪,才能把它用在刀刃上。