数据库基础体系 · 第 82/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。
OLTP、OLAP 与 Lakehouse:工作负载、存储布局和数据链路选型
在数据库选型中,OLTP、OLAP 和 Lakehouse 经常被并列讨论,但它们并不处于同一个抽象层次:
- **OLTP(Online Transaction Processing,在线事务处理)**描述的是一类以短事务、并发写入和单行或少量数据访问为主的工作负载。
- **OLAP(Online Analytical Processing,在线分析处理)**描述的是一类以大范围扫描、聚合、连接和趋势分析为主的工作负载。
- **Lakehouse(湖仓)**描述的是一种数据组织与数据平台架构:在对象存储上的文件数据之上,增加表格式、元数据、事务语义和计算引擎,使数据湖具备一部分数据仓库能力。
因此,“OLTP 还是 OLAP”主要是在选择工作负载对应的执行与存储方式;“是否采用 Lakehouse”主要是在选择分析数据的组织、治理和链路形态。一个系统可以同时包含三者:业务数据库承担 OLTP,Lakehouse 保存历史明细,OLAP 引擎服务报表和交互式查询。
一、先从工作负载定义,而不是从产品名称开始
1. OLTP 的核心:许多并发的小状态变更
OLTP 系统维护的是业务状态。例如,电商订单从“待支付”变为“已支付”,本质上是一个状态转换:
其中:
- 是某一时刻数据库中的业务状态;
- 是一次业务事件,例如支付成功;
- 是事务执行的状态转换;
- 是事务提交后的新状态。
一次典型订单支付事务可能同时完成:
- 检查订单当前状态;
- 扣减库存;
- 写入支付记录;
- 更新订单状态;
- 提交这些变化。
如果第 3 步失败,第 1、2、4 步通常也不能单独生效。这就是事务原子性在业务上的含义。
OLTP 常见特征是:
- 单次事务访问的行数较少;
- 读写都要求较低延迟;
- 并发连接数较高;
- 更新集中在当前状态数据;
- 需要唯一性、外键、检查约束等一致性规则;
- 事务之间可能访问相同的行,因此需要并发控制。
这里的“单次访问行数较少”是工作负载特征,不是数据库语法限制。OLTP 数据库也可以执行全表扫描和复杂查询,只是这些查询往往不应与高峰期的在线事务争夺同一组资源。
2. OLAP 的核心:对大量数据执行低频或中频计算
OLAP 关注的不是某一行当前状态,而是数据集合的性质。例如:
SELECT
date_trunc('month', paid_at) AS month,
region,
sum(amount) AS revenue,
count(*) AS order_count
FROM orders
WHERE paid_at >= DATE '2025-01-01'
AND paid_at < DATE '2026-01-01'
GROUP BY 1, 2
ORDER BY 1, 2;
该查询需要:
- 找到时间范围内的大量订单;
- 读取
paid_at、region、amount等列; - 按月份和地区分组;
- 对金额求和、对行计数;
- 排序并返回结果。
OLAP 的典型特征是:
- 扫描行数多;
- 访问列数可能少于表的总列数;
- 聚合、排序、连接和窗口函数较多;
- 单条查询可能消耗较多 CPU、内存和 I/O;
- 查询并发数不一定高,但资源峰值可能很大;
- 更关心吞吐量、扫描效率和结果延迟,而不是每个事务几毫秒完成。
一个常见误区是把 OLAP 简化成“只读”。分析系统也可能写入:
- 批量导入事实表;
- 生成汇总表;
- 刷新物化视图;
- 执行分区重写或数据整理。
区别在于,这些写入通常是批量、追加或后台维护,而不是每秒大量修改随机单行状态。
3. 负载不能只按“读多写少”分类
以下几个维度比“读多还是写多”更有区分度:
| 维度 | OLTP 常见形态 | OLAP 常见形态 |
|---|---|---|
| 事务规模 | 少量行、短事务 | 批量作业或长查询 |
| 访问模式 | 点查、范围查、单行更新 | 大范围扫描、聚合、连接 |
| 延迟目标 | 稳定的低延迟 | 查询吞吐和可接受的分析延迟 |
| 并发特征 | 高并发、事务彼此竞争 | 中低并发、单查询资源消耗大 |
| 数据变化 | 当前状态频繁变化 | 追加明细、批量修正、重算 |
| 一致性重点 | 事务原子性、约束、隔离 | 快照一致性、批处理结果一致 |
| 数据生命周期 | 热数据和当前状态 | 长历史、明细与汇总并存 |
还有一类常见的混合负载,例如实时风控、运营看板和 HTAP。它们可能同时需要在线更新和聚合查询,但“混合”不等于单一存储布局可以无代价地同时优化两者。
二、OLTP 为什么通常采用行式存储
1. 行式布局适合重建一条业务记录
假设订单表有:
(order_id, user_id, status, amount, created_at, shipping_address, ...)
一次订单详情查询往往需要同一个订单的多个字段:
SELECT order_id, user_id, status, amount, shipping_address
FROM orders
WHERE order_id = 1001;
行式存储会把同一行的字段组织得相对接近。通过主键索引找到目标行后,数据库可以读取这一行及其相关页,而不必为每个字段分别扫描整个列区域。
这并不表示物理上所有行一定连续排列,也不表示一次查询只读取一个磁盘块。具体页结构、溢出列、索引组织和缓存行为取决于数据库实现。但“同一行字段共同服务于记录访问”是行式布局的基本优势。
2. 随机更新需要处理行版本和索引维护
以 PostgreSQL 为例,普通表使用 MVCC(多版本并发控制)保存事务可见性信息。更新一行时,数据库通常不是简单地在原位置覆盖所有内容,而是创建新的行版本,并让并发事务根据快照判断哪个版本可见。旧版本最终需要由清理过程回收。
一次更新的成本不仅是写入新行,还可能包括:
- 修改受影响的索引;
- 写入 WAL(Write-Ahead Log,预写式日志);
- 维护页面和可见性信息;
- 处理并发事务;
- 后台回收失效版本。
因此,OLTP 的性能不应只看“每次更新写了多少业务字段”,还要看索引数量、行宽、冲突程度和日志量。
一个简化的写入成本模型可以写成:
其中:
- :读取和写入数据页的成本;
- :需要维护的索引数量;
- :每个索引更新的平均成本;
- :日志写入和刷盘成本;
- :等待并发事务的成本;
- :旧版本后续清理的成本。
这不是某个数据库的精确性能公式,而是帮助分析瓶颈来源的模型。
3. 一个 PostgreSQL 事务示例
下面的例子使用 PostgreSQL 语义。它要求两个表已存在,并且 orders.status、库存数量等约束根据业务补充。
BEGIN;
SELECT status
FROM orders
WHERE order_id = 1001
FOR UPDATE;
UPDATE inventory
SET available = available - 1
WHERE product_id = 42
AND available > 0;
-- 应用程序应检查上一条 UPDATE 的影响行数是否为 1。
-- 为避免库存不足时仍然创建订单,实际程序需要在这里判断并回滚。
INSERT INTO payments(order_id, paid_at, amount)
VALUES (1001, clock_timestamp(), 99.00);
UPDATE orders
SET status = 'paid'
WHERE order_id = 1001
AND status = 'pending';
COMMIT;
这个事务包含几个重要边界:
FOR UPDATE表示当前事务要锁定目标订单行,避免两个支付流程同时修改同一订单;- 库存扣减使用条件
available > 0,但应用仍需检查影响行数; - 只有
COMMIT成功后,其他事务才应观察到完整结果; - 如果支付插入失败,应执行
ROLLBACK,不能只回滚某一条语句后继续提交。
在 PostgreSQL 中,事务块、行锁、MVCC 和约束共同决定了并发语义。MySQL 的具体行为还取决于存储引擎,尤其是表是否使用支持事务的 InnoDB;不能把某一产品的事务行为无条件推广到所有 MySQL 表或所有数据库。
4. 行存储并不意味着“不能分析”
对 PostgreSQL 的订单表执行聚合是合法的:
EXPLAIN (ANALYZE, BUFFERS)
SELECT region, sum(amount)
FROM orders
WHERE created_at >= TIMESTAMP '2025-01-01'
AND created_at < TIMESTAMP '2025-02-01'
GROUP BY region;
但当 orders 很大时,数据库可能需要访问大量数据页。即使查询只需要三列,行式页面往往仍包含同一行的其他字段。索引可以帮助缩小范围,但如果时间条件命中大部分表,顺序扫描可能反而更合理。
EXPLAIN (ANALYZE, BUFFERS) 的价值在于验证实际路径:
Seq Scan表示顺序扫描;Index Scan或Bitmap Heap Scan表示使用索引定位数据;Buffers可帮助判断缓存命中与实际读盘;- 计划估算行数与实际行数差异很大时,可能需要检查统计信息、数据倾斜或谓词选择性。
索引不是“所有查询的加速器”。它对高选择性的点查和范围查更有帮助,但会增加写入成本,并可能在低选择性查询中不如顺序扫描。
三、OLAP 为什么通常采用列式存储和向量化执行
1. 列式布局减少无关数据读取
设事实表有 100 个字段,而一个报表只使用:
sale_date, product_id, quantity, amount
列式存储可以主要读取这 4 列,而不是读取每条记录的全部 100 个字段。一个简单的扫描成本近似为:
其中 是查询实际需要的列集合。
如果查询只访问少数列,列裁剪(column pruning)就能减少 I/O 和解压成本。相同列通常还具有更强的值相似性,例如状态列、地区列和日期列更容易压缩。
但列式存储不是在所有场景都占优。若应用经常按主键取出一整行,列式引擎可能需要读取多个列段并重建结果;大量单行更新也往往需要重写更大的列块或数据片段。
2. 向量化执行降低逐行解释开销
OLAP 引擎通常不会为每一行都调用一次完整的解释器逻辑,而是以一批行组成的向量或批次执行:
- 从存储层读取一批列数据;
- 对批次执行谓词过滤;
- 对剩余值进行表达式计算;
- 将结果送入聚合或连接算子;
- 重复处理下一批。
例如条件:
WHERE amount > 100
可以对一批 amount 值生成布尔选择向量,再把选择结果传给后续算子。这样可以减少函数调用、改善 CPU 缓存利用,并更容易使用 SIMD 等底层优化。
3. 分区、排序和数据跳过不是同一件事
这三个概念经常被混淆:
- 分区(partitioning):把数据按一个或多个分区键拆成更大的目录、表分区或数据单元;
- 排序键(sort key / ordering key):控制同一数据单元内部记录的排列顺序;
- 数据跳过索引或统计信息(data skipping):根据最小值、最大值、布隆过滤器等元数据判断某个数据块是否不可能满足谓词。
例如按月份分区、按 user_id 排序:
2025-01/
按 user_id 排序的数据块
2025-02/
按 user_id 排序的数据块
查询:
WHERE created_at >= '2025-01-01'
AND created_at < '2025-02-01'
AND user_id = 42
可能先跳过其他月份,再在一月份数据内利用排序和块级统计缩小读取范围。
但是:
- 分区不是索引;
- 排序键不保证所有查询都快;
- 数据跳过依赖数据分布、块大小、统计信息和谓词形态;
- 分区过多会增加元数据和调度开销;
- 如果分区键基数极高,例如按用户 ID 建立数百万分区,通常会造成管理和查询问题。
4. ClickHouse MergeTree 中的具体表现
在 ClickHouse 中,MergeTree 系列表使用数据 part 保存数据。写入通常先形成新的 part,后台再进行合并。其常见建模元素包括:
PARTITION BY:决定 part 的分区归属;ORDER BY:决定 part 内部的排序键和主索引组织;- 数据跳过索引:在适合的列和谓词上减少需要读取的 granule;
- 后台 merge:将多个 part 合并为更大的 part。
示例:
CREATE TABLE events
(
event_time DateTime,
tenant_id UInt64,
event_type LowCardinality(String),
value Float64
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (tenant_id, event_time);
这个设计表达的是:
- 按月形成分区,便于按时间范围管理和删除;
- 每个分区内按
(tenant_id, event_time)排序; - 适合包含租户和时间条件的查询,例如:
SELECT
toDate(event_time) AS day,
sum(value)
FROM events
WHERE tenant_id = 10
AND event_time >= '2025-01-01 00:00:00'
AND event_time < '2025-02-01 00:00:00'
GROUP BY day
ORDER BY day;
但 ORDER BY 不是传统 OLTP 数据库意义上的唯一主键约束。它主要影响物理组织和查询剪枝,不能仅凭设置 ORDER BY (tenant_id, event_time) 就推断 tenant_id, event_time 不会重复。
MergeTree 的后台合并也带来一个重要事实:插入成功与数据已经合并完成是两个不同状态。查询通常可以读取多个 part,后台再逐步整理它们。过度频繁的小批量插入会产生大量小 part,增加合并压力;这属于写入链路和后台状态共同造成的问题,而不是单纯的 SQL 问题。
四、OLTP 与 OLAP 的查询语义也不同
1. 当前状态查询与历史事实查询
OLTP 中的订单表通常表达“订单当前是什么状态”:
order_id = 1001, status = paid
OLAP 中的事实表更关心“发生过什么”:
order_id = 1001, event = payment_succeeded, event_time = ...
order_id = 1001, event = shipped, event_time = ...
如果只把 OLTP 当前表定期复制到分析库,历史状态变化可能已经丢失。此时分析系统能够回答“现在有多少已支付订单”,却未必能回答“上个月每天有多少订单从待支付转为已支付”。
这就是数据链路设计中“快照同步”和“变更捕获”的区别:
- 快照同步:周期性复制源表当前内容;
- CDC(Change Data Capture):捕获插入、更新、删除及其顺序或事务位置。
2. 事实表、维度表和去规范化
OLTP 通常倾向于规范化,以减少更新异常。例如用户信息独立保存,订单通过 user_id 引用用户。
OLAP 常见星型模型:
- 事实表:订单、支付、曝光等可度量事件;
- 维度表:用户、商品、地区、日期等描述性实体;
- 度量:金额、数量、时长等可聚合值。
是否去规范化取决于访问模式。分析查询频繁连接小维度表时,保留维度表可以减少重复;对固定报表而言,适度宽表可能减少连接成本。但去规范化会引入维度变更处理问题,例如商品所属类目改变后,历史订单应使用当时类目还是当前类目。
五、Lakehouse 到底增加了什么
1. 数据湖、数据仓库和 Lakehouse 的边界
可以先区分三个概念:
- 数据湖:通常以对象存储上的文件保存原始或加工数据,格式可以是 Parquet、JSON、CSV 等,治理和事务能力取决于外围系统;
- 数据仓库:通常由统一系统管理表、存储、执行、权限和事务,面向结构化分析;
- Lakehouse:以低成本、开放格式的文件存储为基础,再增加表级元数据、快照、提交、模式管理和多个计算引擎可访问的语义。
Lakehouse 不是“把数据库文件放进对象存储”这么简单。一个可用的 Lakehouse 表至少要解决:
- 文件属于哪张表;
- 哪些文件是当前快照的一部分;
- 某次提交新增或删除了哪些文件;
- 并发写入如何提交;
- 读者如何获得一致快照;
- 模式如何演进;
- 旧文件何时可以清理;
- 多个引擎如何解释同一批数据。
具体表格式的事务、删除、更新和并发提交语义由相应规范和实现决定,不能把某个表格式的能力泛化为所有 Parquet 文件都具备。
2. 文件格式、表格式、目录和计算引擎
Lakehouse 中常见的组件可以分为四层:
文件格式
例如 Parquet 负责:
- 按列组织数据;
- 保存列类型和统计信息;
- 支持压缩和编码;
- 让多个引擎读取同一文件。
Parquet 本身不是完整的表事务系统。直接把多个 Parquet 文件放在一个目录中,并不能自动获得原子提交、并发写入和可靠删除语义。
表格式
表格式在文件之上维护表快照和提交元数据。它通常需要记录:
快照 N:
file-001.parquet
file-002.parquet
快照 N+1:
保留 file-001.parquet
删除 file-002.parquet
新增 file-003.parquet
读者读取快照 N+1 时,不应看到“只完成了一半”的文件替换。
目录与权限
目录服务负责发现表、解析名称、授权访问,并可能保存 schema、分区信息和表属性。目录不一定等价于事务提交日志;两者的职责要区分。
计算引擎
Spark、Trino、Flink、DuckDB 以及其他引擎可能读取相同的开放文件或表格式,但 SQL 方言、事务支持、谓词下推和写入语义并不完全相同。能读同一个文件,不代表所有引擎都支持同样的更新、删除和并发提交能力。
3. Lakehouse 的快照与失败路径
设一个批处理任务要新增两个文件:
file-101.parquet
file-102.parquet
较安全的流程是:
- 任务写入临时路径;
- 对文件执行校验,确认行数、schema 和文件完整性;
- 生成提交元数据;
- 以条件提交方式发布新快照;
- 读者从新快照中同时看到两个文件;
- 旧快照仍保留到满足保留策略后再清理。
如果任务在第 1 步崩溃,通常只留下临时文件,不影响当前快照。
如果两个任务都基于快照 10 产生新版本:
任务 A: 快照 10 -> 快照 11A
任务 B: 快照 10 -> 快照 11B
提交时需要检测当前版本仍然是 10。先成功的任务发布新快照,后提交的任务必须重试、合并或失败;否则两个任务可能互相覆盖。
这说明 Lakehouse 的“事务”至少涉及:
- 数据文件;
- 提交元数据;
- 当前版本指针;
- 对象存储的一致性和并发条件;
- 失败后的孤儿文件清理。
不能因为文件已经上传成功,就认为数据已经对查询可见。上传完成、提交完成、查询可见、旧文件可回收是不同状态。
六、从 OLTP 到 OLAP 或 Lakehouse 的数据链路
一个常见链路如下:
业务请求
-> OLTP 数据库
-> WAL / binlog / CDC 采集
-> 消息队列或日志缓冲
-> 清洗、去重、补充维度
-> Lakehouse 明细表
-> OLAP 引擎或查询引擎
-> 报表、指标、机器学习特征
每一段都有独立的正确性问题。
1. 采集位置:事务日志优于重复查询
如果通过不断执行:
SELECT *
FROM orders
WHERE updated_at > :last_time;
来增量同步,会遇到几个问题:
- 两条记录可能具有相同时间戳;
- 时钟精度不足导致遗漏;
- 更新后时间变化与事务提交顺序不一致;
- 删除记录可能无法通过普通查询发现;
- 长事务提交顺序与行更新时间可能不同。
CDC 通常使用数据库的事务日志,并携带日志位置、事务 ID 或类似游标。消费者应保存一个可恢复的进度点:
已处理到日志位置 L
处理失败时,从 L 或安全重放位置继续读取。若消息至少一次投递,同一事件可能被处理多次,因此下游需要幂等设计。
2. 幂等不是“重复执行不会报错”
一个操作幂等,意味着执行一次和执行多次的最终业务结果相同:
例如,使用事件唯一 ID 建立去重表:
event_id = abc123 已处理
再次收到 abc123 时跳过应用,可以实现事件级幂等。
但以下操作通常不是天然幂等的:
UPDATE account
SET balance = balance - 100;
同一事件执行两次会扣款两次。更安全的方式是让事件 ID 参与约束,或把“账户变更”和“已处理事件记录”放在同一个事务中。
3. 乱序、删除和迟到数据
CDC 或消息系统可能出现:
- 同一主键的更新事件乱序;
- 删除事件晚于新增事件到达;
- 网络重试造成重复事件;
- 迟到数据属于已经关闭的分区;
- 维度表更新早于或晚于事实事件。
因此,Lakehouse 明细层常见做法是保留原始事件和来源位置,再在下游构建当前视图或事实表。对于同一业务键,需要依据明确的版本字段、事务序号或事件时间决定哪条记录有效,而不能简单使用“最后到达的数据”。
事件时间与处理时间也不同:
- 事件时间:业务事件实际发生的时间;
- 处理时间:数据管道处理它的时间。
按处理时间统计可能及时,但迟到事件会改变历史结果;按事件时间统计更符合业务含义,却需要窗口关闭、迟到修正或重算机制。
七、DuckDB、Parquet 与本地分析链路
DuckDB 是嵌入式分析数据库,适合在单机进程中直接查询本地文件、对象存储映射文件或内存数据。它不是一个负责承载大量在线事务连接的服务端 OLTP 数据库。
一个最小的 Parquet 查询示例:
SELECT
region,
sum(amount) AS revenue
FROM read_parquet('data/orders/*.parquet')
WHERE order_date >= DATE '2025-01-01'
AND order_date < DATE '2025-02-01'
GROUP BY region
ORDER BY revenue DESC;
其大致执行过程是:
- 读取 Parquet 文件的 schema 和行组元数据;
- 根据
order_date谓词尝试跳过不可能匹配的行组; - 只读取
region、amount、order_date等需要的列; - 使用向量化算子执行过滤和聚合;
- 返回聚合结果,而不是把所有明细加载到应用程序内存。
写出分析文件时,可以将查询结果物化为 Parquet:
COPY (
SELECT
order_date,
region,
sum(amount) AS revenue
FROM read_parquet('data/orders/*.parquet')
GROUP BY order_date, region
) TO 'output/daily_revenue.parquet'
(FORMAT PARQUET);
前置条件是进程对输入和输出路径有访问权限,且输入文件 schema 可被统一解析。需要注意:
- 多个进程同时写同一个输出路径时,不能仅凭
COPY推断获得跨进程事务; - 文件写完不等于已经注册到某个 Lakehouse 表;
- 如果输入文件正在被其他任务覆盖,查询可能读取到不完整或不一致的数据集合;
- 大规模并发服务场景需要评估连接管理、资源隔离和部署边界。
DuckDB 很适合:
- 本地数据分析;
- CI 中验证 SQL;
- 处理 Parquet 的批处理脚本;
- 数据工程中的抽样、转换和质量检查。
它不自动替代分布式表格式、目录服务或在线事务数据库。
八、如何根据访问模式选择存储和链路
1. 选择 OLTP 数据库的条件
优先考虑 OLTP 数据库,当系统需要:
- 多个请求同时修改相同业务实体;
- 事务跨多个表原子提交;
- 强依赖唯一键、外键和检查约束;
- 按主键或高选择性索引低延迟访问;
- 明确的当前状态模型;
- 在线请求失败后可以回滚。
此时应重点设计:
- 主键和唯一约束;
- 事务边界;
- 隔离级别;
- 锁顺序和死锁重试;
- 索引数量与写入成本;
- 长事务和旧版本清理。
2. 选择 OLAP 引擎的条件
优先考虑列式 OLAP 引擎,当系统需要:
- 高频扫描大量历史数据;
- 大量聚合、连接和窗口计算;
- 报表和分析查询只读取部分列;
- 追加写入多于随机单行更新;
- 通过分区、排序键和数据跳过降低扫描范围;
- 将计算吞吐放在单行更新延迟之前。
应根据主要谓词设计排序和分区,而不是按表字段随意选择。可以先列出高频查询:
按 tenant_id + 时间范围查询
按时间范围聚合
按 event_type 过滤
再判断哪些字段适合作为分区键,哪些适合作为排序前缀。排序键应服务于真实查询;否则即使数据“已经排序”,也可能无法有效剪枝。
3. 选择 Lakehouse 的条件
Lakehouse 更适合以下情况:
- 需要保留长时间历史明细;
- 数据规模或存储成本使对象存储有吸引力;
- 希望多个计算引擎访问相同数据;
- 需要批处理、机器学习、交互式分析共享数据;
- 数据写入以追加和批量变更为主;
- 能接受数据从业务库到分析层存在延迟;
- 团队能够维护表格式、目录、作业和数据质量体系。
如果系统只是一个小型应用,数据量有限,报表也不复杂,直接在 OLTP 库上建立合适索引或只读副本,可能比建设 Lakehouse 更简单可靠。
九、常见错误与诊断方法
错误一:把 OLTP 库直接当报表引擎
表现:
- 高峰期报表导致业务接口延迟升高;
- 大查询占用大量缓存和 CPU;
- 事务锁等待增加;
- 备份、维护和查询互相影响。
诊断方式:
EXPLAIN (ANALYZE, BUFFERS)
SELECT ...;
同时观察数据库活动、锁等待、I/O、缓存命中和长事务。解决方案可能是优化 SQL,也可能是只读副本、汇总表、数据抽取或独立 OLAP 系统;不能只靠“再加一个索引”解决所有问题。
错误二:把列式数据库当作单行更新存储
表现:
- 大量小更新形成许多小数据片段;
- 后台合并持续占用资源;
- 查询延迟抖动;
- 更新语义与应用预期不一致。
诊断时应检查写入批次大小、数据片段数量、后台合并积压、更新模式和查询是否依赖最终整理结果。很多列式系统更适合追加事件,再通过版本选择、批量重算或定期整理形成当前视图。
错误三:只按日期分区,却没有考虑排序和数据分布
按日期分区可以帮助时间范围管理,但如果每个分区内部数据完全无序,查询:
WHERE customer_id = 42
仍可能扫描整个时间分区。反过来,按高基数字段建立过细分区,也会带来大量文件、目录和元数据。
应区分:
- 分区是否能排除大块数据;
- 排序是否使目标值集中;
- 文件或数据块统计是否有效;
- 查询谓词是否与这些元数据可匹配。
错误四:把 Parquet 目录当成事务表
表现:
- 查询偶尔读到半批文件;
- 重跑任务产生重复数据;
- 删除文件后旧查询无法复现;
- 多个任务互相覆盖输出;
- 文件已上传但目录查询不到。
诊断时要分别检查:
- 文件是否完整;
- 文件是否被提交到表快照;
- 查询使用的是哪个快照;
- 作业是否有唯一批次 ID;
- 失败后的临时文件是否清理;
- 清理旧文件是否仍被长时间运行的读事务需要。
十、一个可操作的组合架构
一个较常见的分层方案是:
PostgreSQL / MySQL
保存用户、订单、库存等当前业务状态
|
| CDC 或定期快照
v
对象存储上的原始事件层
保存不可变原始记录、来源位置和采集时间
|
v
Lakehouse 明细层
统一 schema、去重、处理删除和迟到数据
|
+--> DuckDB:本地分析、质量检查、开发验证
|
+--> ClickHouse 或其他 OLAP 引擎:交互式报表
|
+--> 批处理引擎:宽表、汇总表、特征数据
这个架构的关键不是组件数量,而是边界清晰:
- OLTP 是业务写入事实的权威来源;
- CDC 游标是可恢复的处理状态;
- 原始层保留重放和审计能力;
- Lakehouse 表快照定义分析数据的可见范围;
- OLAP 引擎可以是服务层缓存或派生副本,不应被误认为业务交易的唯一事实来源;
- 汇总结果需要说明刷新时间、迟到数据处理规则和重算方式。
最终选型可以用三个问题收敛:
-
一次业务操作是否需要跨多行、跨多表原子提交?
如果需要,优先从事务数据库建模。 -
主要查询是点查当前状态,还是扫描历史集合做聚合?
前者偏向行式 OLTP,后者偏向列式 OLAP。 -
是否需要低成本保存长期明细,并让多个引擎共享同一份分析数据?
如果需要,评估 Lakehouse;但必须同时评估表格式事务、CDC、幂等、模式演进和数据治理,而不能只选择一个文件格式。
OLTP、OLAP 与 Lakehouse 的关系,归根结底是三种不同问题的组合:业务状态如何可靠变更,历史数据如何高效计算,以及数据如何在不同系统之间可恢复、可验证地流动。只有先明确这三个问题,再决定行式还是列式、数据库还是文件、单体还是分层,选型才不会停留在产品名称的比较上。
系列导航与关联阅读
- 系列入口:数据库完整学习路线:从关系模型、事务索引到分布式与向量检索
- 上一篇:数据库 CDC:日志捕获、Debezium、Schema 演进、顺序和重复消费
- 下一篇:MySQL 数据类型、字符集与排序规则:精度、编码和索引影响
- 延伸:ClickHouse 列式建模:MergeTree、排序键、分区和数据跳过
- 延伸:DuckDB 嵌入式分析:列式执行、Parquet、SQL 和本地数据工程
官方资料
本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。

评论
0 条讨论