3. 物化视图的存储架构:行存、列存与混合存储在物化视图中的应用

物化视图说白了就是“预计算好的结果集”。但结果集怎么存,直接决定了查询快不快、更新痛不痛、存储省不省。我这些年调过不少物化视图,发现很多人只关注 SQL 怎么写,却忽略了底层存储选型——这其实是个大坑。

今天咱们就聊聊三种存储模式:行存、列存、混合存。它们各自适合什么场景?在物化视图里怎么选?我踩过的坑也一并分享出来。

3.1 行存:OLTP 的“老本行”

行存,就是按行来组织数据。每一行的所有字段连续存放在磁盘上。你想想看,传统关系型数据库基本都是这个路子。

核心特点:一次 I/O 就能拿到一整行数据。适合点查、按主键更新的场景。

我在项目中遇到过这样一个案例:某电商系统的订单物化视图,需要实时统计每个用户的订单数和总金额。因为每次只查一个用户,行存就很合适——一次 I/O 拉回整行,直接算完返回。

-- 行存物化视图示例(PostgreSQL)
CREATE MATERIALIZED VIEW user_order_stats AS
SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;

-- 查询某个用户
SELECT * FROM user_order_stats WHERE user_id = 12345;
-- 行存下,这个查询非常快

但行存也有短板。如果物化视图有 50 个字段,而你只查其中 2 个,行存还是会读整行数据。这就是所谓的“读放大”。

场景 行存表现 原因
点查(按主键) 优秀 一次 I/O 拿整行
范围扫描 良好 顺序读效率高
聚合查询(少字段) 较差 读放大严重
频繁更新 优秀 原地更新成本低

我的建议:如果物化视图的查询模式以“查某一行”为主,或者需要频繁增量更新,行存是首选。别盲目追求列存,有时候“老办法”反而最稳。

3.2 列存:OLAP 的“杀手锏”

列存正好反过来——按列存储数据。同一列的所有值连续存放。这听起来有点反直觉,但做分析查询时,效果惊人。

我记得有一次帮客户优化一个报表物化视图,里面存了 30 个维度字段和 10 个度量字段。业务方只查其中 3 个度量。行存下每次扫描要读 40 列的数据,换成列存后只读 3 列,I/O 直接降了一个数量级。

-- 列存物化视图示例(ClickHouse)
CREATE MATERIALIZED VIEW sales_daily_mv
ENGINE = SummingMergeTree()
ORDER BY (date, product_id)
AS SELECT
    toDate(order_time) AS date,
    product_id,
    sum(quantity) AS total_qty,
    sum(amount) AS total_amount
FROM orders
GROUP BY date, product_id;

-- 查询只涉及两列,列存下只读这两列
SELECT date, total_amount FROM sales_daily_mv
WHERE date = '2024-01-01';

列存还有两个隐藏优势:

  • 高压缩比:同一列的数据类型相同,重复值多,压缩率能到 5:1 甚至 10:1。我见过一个日志物化视图,行存 500GB,列存压到 80GB。
  • 向量化执行:列存天然适合 SIMD 指令,一次处理一批数据,CPU 效率高。

注意:列存不适合频繁的单行更新。你想啊,更新一行要改所有列文件,写放大很严重。我曾经有个项目,物化视图每秒钟要更新几千行,用列存后写入延迟飙升,最后不得不切回行存。

3.3 混合存:既要又要的“折中方案”

行存和列存各有优劣。那能不能“我全都要”?混合存储就是干这个的。

混合存的核心思路:热点数据行存,冷数据列存;或者行级操作走行存,分析查询走列存。

我目前在用的一个方案是这样的:

-- 混合存物化视图设计思路(伪代码)
-- 1. 行存部分:存储最近 7 天的明细数据,支持快速更新
CREATE TABLE mv_recent (
    user_id INT,
    event_time TIMESTAMP,
    event_type VARCHAR(50),
    detail JSONB,
    PRIMARY KEY (user_id, event_time)
) USING row_storage;

-- 2. 列存部分:存储历史聚合数据,支持高效分析
CREATE TABLE mv_history (
    event_date DATE,
    event_type VARCHAR(50),
    event_count BIGINT,
    sum_value DECIMAL(18,2)
) USING column_storage;

-- 3. 查询时自动路由
SELECT * FROM mv_recent WHERE user_id = 12345 AND event_time > now() - INTERVAL '1 day'
UNION ALL
SELECT * FROM mv_history WHERE event_date = '2024-01-01';

这种架构在实时数仓里很常见。比如 Apache Doris 的“明细模型”和“聚合模型”就是混合存的典型代表。

存储模式 适用层 典型引擎 更新频率
行存 实时明细层 PostgreSQL, MySQL 高(秒级)
列存 聚合分析层 ClickHouse, Parquet 低(分钟级)
混合存 分层组合 Doris, StarRocks 中(按策略)

避坑指南:混合存不是银弹。我曾经在一个项目里强行搞混合存,结果数据路由逻辑写得太复杂,查询反而变慢了。后来发现业务 90% 的查询都是最近 3 天的数据,干脆全用行存,简单又高效。所以,先分析业务模式,再决定要不要混合。

3.4 一张图看懂三种存储架构

下面这张图是我自己画的,把三种存储的核心差异和适用场景串起来了。

物化视图存储架构对比图 物化视图存储架构对比 行存 按行连续存储 一次I/O拿整行 适合:点查、高频更新 代表:PostgreSQL 列存 按列独立存储 只读需要的列 适合:分析、聚合查询 代表:ClickHouse 混合存 行存+列存组合 热点行存,冷数据列存 适合:分层数仓 代表:Doris 选型建议 查询模式以点查为主 → 行存 查询模式以分析聚合为主 → 列存 既有实时更新又有分析需求 → 混合存(但要做好分层设计) 注:箭头表示存储模式之间的演进关系,并非强制转换路径 行存:更新友好,读放大 适合 OLTP 场景 列存:压缩率高,写放大 适合 OLAP 场景 混合存:灵活但复杂 适合分层数仓架构

3.5 实战中的选型决策

说了这么多,到底怎么选?我总结了一个简单的决策流程:

  1. 先看查询模式:如果 80% 的查询是“查某一行”,直接选行存。别犹豫。
  2. 再看更新频率:如果物化视图每秒更新上千次,列存基本不可行。行存或混合存更靠谱。
  3. 然后看存储成本:如果数据量超过 10TB,列存的压缩优势就很明显了。我有个项目,列存比行存省了 70% 的存储费用。
  4. 最后看维护复杂度:混合存虽然灵活,但需要额外的路由逻辑和数据同步机制。团队能力够吗?运维成本能接受吗?

我的经验:大多数场景下,行存 + 列存分层比“纯混合存”更实用。比如用行存做实时明细层,用列存做离线聚合层,中间通过物化视图刷新来同步。这样既发挥了各自的优势,又避免了混合存的复杂性。

嗯,存储架构这块就聊到这儿。记住一句话:没有最好的存储,只有最合适的存储。选型前多花点时间分析业务模式,比事后优化要划算得多。