查询引擎层:Trino / Doris / ClickHouse
本层定位:三类查询引擎的角色与组合
在存算分离的湖仓架构中,数据统一落在对象存储 + 开放表格式(Iceberg/Delta/Hudi)之上, 计算与查询引擎作为无状态(或轻状态)的消费者按需扩缩。此时"查询引擎"不再是一个单体组件, 而是按查询模式分化出的三类角色,它们共享同一份湖仓数据,各司其职:
联邦查询引擎 · Trino
不落存储、只做计算。通过 Connector 体系同时连接 Iceberg 湖仓、业务库、ES 等异构数据源, 支撑跨源 JOIN 与 Ad-hoc 探索分析,是"数据不动、查询动"的典型实现。
实时 OLAP 引擎 · Doris
FE/BE 架构 + 列存 + 预聚合,面向高并发报表、实时大屏、多维分析。 通过 Multi-Catalog 直接读取湖仓外表,也可将热点数据沉淀为内表获得极致性能。
时序列存引擎 · ClickHouse
MergeTree 列存家族 + 向量化执行,单表扫描性能极致。面向日志、监控指标、 用户行为流等时序场景,以极低的存储成本支撑海量明细的即席圈选与聚合。
三类引擎在湖仓整体分层中的位置如下图所示——上层按 SLA 路由查询,下层共享一份数据:
组合使用策略
- 一份数据、多引擎共享:明细数据统一以 Iceberg 表落在对象存储,Trino/Doris 直接读取, 避免在多个引擎间反复同步造成的数据冗余与一致性问题。
- 按 SLA 路由:交互式探索(分钟级可接受)走 Trino;高并发固定报表(亚秒级、千 QPS)走 Doris; 海量日志检索与圈选(毫秒~秒级)走 ClickHouse。
- 冷热分层:热点数据沉淀到 Doris/ClickHouse 内表作为"加速层",冷数据留在湖仓由 Trino 兜底, 加速层可随时从湖仓重建(可丢弃缓存语义)。
- 避免引擎蔓延:每个新引擎都是一笔运维成本,引入前先确认现有引擎调优后确实无法满足场景。
Apache Trino:MPP 联邦查询引擎
架构定位:coordinator + worker 的 MPP 模型
Trino 源自 Presto,是为交互式分析设计的 MPP(Massively Parallel Processing)查询引擎, 存算分离的天然搭档。集群由两类进程组成:
- Coordinator(协调节点):接收 SQL,完成解析、语法分析、逻辑计划、基于代价的优化(CBO)、 生成分布式物理计划,并把任务(Stage/Task)调度到 Worker;本身不执行计算,是集群的"大脑"。
- Worker(工作节点):从 Connector 拉取数据(Split 逻辑分片)、执行算子 pipeline、 通过 Exchange 在节点间 Shuffle 数据,最终由 Stage 汇聚结果。
Trino 不自己管理存储,数据在哪里由 Connector 决定——这正是它联邦查询能力的根基。 查询生命周期为:SQL → AST → 逻辑计划 → 优化(谓词下推、列裁剪、join 分布策略选择)→ 分片调度(NodeScheduler)→ 并行执行 → Exchange 汇聚 → 返回。
Connector 体系与联邦查询
Trino 中每个 Catalog 绑定一个 Connector,表名采用
catalog.schema.table 三段式。一条 SQL 可以同时引用多个 catalog,
Trino 将各源数据拉入集群内做统一分布式 JOIN,这就是"联邦查询"。核心 Connector 包括:
iceberg(湖仓一等公民)、hive、delta-lake、mysql、
postgresql、doris、elasticsearch、kafka 等 50 余种。
为湖仓配置 Iceberg catalog(Hive Metastore 型),文件 etc/catalog/iceberg.properties:
# 连接 Hive Metastore 作为 Iceberg catalog
connector.name=iceberg
iceberg.catalog.type=hive_metastore
hive.metastore.uri=thrift://hive-meta:9083
# 原生 S3 客户端访问对象存储(免 Hadoop 依赖,性能更好)
fs.native-s3.enabled=true
s3.endpoint=https://oss.internal:443
s3.path-style-access=true
s3.aws-access-key=${ENV:AWS_ACCESS_KEY_ID}
s3.aws-secret-key=${ENV:AWS_SECRET_ACCESS_KEY}
# 读取优化:文件级统计与 Bloom 过滤
hive.parquet.use-bloom-filters=true
同时挂载 PostgreSQL 与 Doris 两个数据源(联邦查询的另外两端):
# etc/catalog/pg.properties —— 业务库(CRM)只读账号
connector.name=postgresql
connection-url=jdbc:postgresql://pg-host:5432/crm
connection-user=ro_user
connection-password=******
# etc/catalog/doris.properties —— Doris 加速层
connector.name=doris
doris.host=fe-host
doris.port=9030
doris.user=trino
doris.password=*******
跨源 JOIN 示例:一条 SQL 同时访问 Iceberg 湖仓、PostgreSQL 业务库与Doris 加速层:
-- 月度 GMV 归因:明细在湖仓,维表在业务库,用户分层在 Doris
SELECT c.region AS 大区,
substr(o.dt, 1, 7) AS 月份,
sum(o.amount) AS gmv
FROM iceberg.lake.dwd_sales_orders o -- 湖仓明细(Iceberg,主体事实表)
JOIN pg.crm.customers c -- PostgreSQL 业务库维表
ON o.customer_id = c.id
JOIN doris.doris.user_profile u -- Doris 中的实时用户分层结果
ON u.uid = c.uid
WHERE o.dt BETWEEN DATE '2026-07-01' AND DATE '2026-09-30'
AND u.level IN ('V1', 'V2')
GROUP BY c.region, substr(o.dt, 1, 7)
ORDER BY gmv DESC;
内存管理:query.max-memory 三级限制
Trino 是内存 pinned 执行模型,用户内存(User Memory)在编译期即可预估。
集群通过三级配额防止单条 Ad-hoc 查询拖垮整体:
query.max-memory(单查询全集群)、query.max-memory-per-node(单查询单节点用户内存)、
query.max-total-memory-per-node(单查询单节点用户 + 系统内存,含 exchange 缓冲)。
# etc/config.properties(coordinator 侧核心配置)
coordinator=true
node-scheduler.include-coordinator=false
http-server.http.port=8080
discovery.uri=http://trino-coord:8080
# 内存三级限制(示例:16 节点集群,每节点 64G 堆)
query.max-memory=600GB
query.max-memory-per-node=12GB
query.max-total-memory-per-node=16GB
# 集群内存紧张时,优先杀掉占用最多且被阻塞的查询,保护小查询
query.low-memory-killer.policy=total-reservation-on-blocked-nodes
# etc/jvm.config(worker 与 coordinator:堆外直内存 + G1)
-server
-Xmx64G
-XX:InitialRAMPercentage=80
-XX:MaxRAMPercentage=80
-XX:G1HeapRegionSize=32M
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
Exchange 与执行并发调优
Exchange 是 Trino 节点间 Shuffle 数据的通道。大表 JOIN/聚合时 exchange 缓冲会占用大量内存, 2022 之后版本支持 spooling exchange(交换数据落盘),让超大 shuffle 不再 OOM:
# etc/config.properties(worker 侧)
# 超大 shuffle 落盘,用磁盘换内存稳定性
exchange.spooling.enabled=true
exchange.spooling.shared-storage-path=file:///data1/trino-spool,file:///data2/trino-spool
spiller.max-used-space=500GB
# 调度并发:单节点同时执行的 split 数与排队上限
node-scheduler.max-splits-per-node=256
node-scheduler.max-queued-splits-per-task=2000
- split 数量调优:湖仓表的小文件过多会导致 split 爆炸、调度开销剧增;
读写侧统一设置
write.target-file-size-bytes(如 128~512MB)保证文件规整。 - join 分布策略:用
EXPLAIN确认大表 JOIN 走 partitioned 而非误广播; 小维表可用 hint"broadcast"强制广播,省一次全表 shuffle。 - 会话级控制:Ad-hoc 平台建议默认给用户设置
SET SESSION query_max_run_time = '30min'与内存配额,防跑飞。 - 写并发:CTAS/INSERT 大结果集时调大 Iceberg 会话参数
iceberg.max-partitions-per-writer相关写入并发,避免写入成为长尾。
EXPLAIN ANALYZE——先看哪个 Stage
最慢、数据量在哪个算子膨胀,再针对性下推谓词、调整 join 策略或加分区过滤,而不是盲目改参数。
Apache Doris:高并发实时 OLAP
架构定位:FE + BE 的实时分析引擎
Doris 是 MPP 架构的实时分析型数据库,MySQL 协议兼容让 BI 工具零成本接入。 集群由两类组件构成:
- FE(Frontend):存储全量元数据(基于 BerkeleyDB JE / RocksDB Raft 复制), 负责 SQL 解析、查询计划、元数据管理与导入调度。FE 分 Leader/Follower/Observer 三种角色, Follower 参与选主,Observer 只读扩展查询接入点。
- BE(Backend):存储数据与执行查询。数据按
分区 → 分桶(Bucket)→ Tablet三级组织,Tablet 是数据复制、迁移的最小单元,默认三副本,副本间通过 Raft 协议保证一致性。
与 Trino"只算不存"不同,Doris 通常以"内表"形式持有数据(存算耦合部署,亦可选存算分离模式), 因此既承担查询也承担实时写入(Stream Load / Routine Load / INSERT INTO SELECT)。 在湖仓架构中,Doris 的典型角色是高并发加速层:湖仓保存全量冷数据, Doris 内表保存近期热数据,或通过 Multi-Catalog 直查湖仓。
表模型选型:Duplicate / Aggregate / Unique
建表时必须先回答"这份数据要不要去重、要不要预聚合",三种模型对应三类语义:
| 表模型 | 语义 | 典型场景 | 注意事项 |
|---|---|---|---|
| Duplicate(明细模型) | 不做任何聚合,重复行全部保留 | ODS 明细入湖、日志宽表、明细圈选 | 最通用;聚合靠查询时算,配合 Rollup/MV 加速 |
| Aggregate(聚合模型) | Key 相同时 Value 按 AGG 类型合并(SUM/REPLACE/MAX/MIN/BITMAP) | 流量统计、用户行为预聚合、PV/UV | 查询结果与明细不完全等价;无法回查明细 |
| Unique(主键模型) | 主键唯一,新数据覆盖旧数据 | 订单状态流转、用户画像、维表同步 | 推荐 merge-on-write(写时合并),点查与聚合性能更好 |
三种模型的建表示例(同一份订单数据的三种建模姿势):
-- 1) Duplicate 明细模型:湖仓 ODS 层标准姿势,明细全保留
CREATE TABLE lake.dwd_order_detail (
dt DATE NOT NULL,
order_id BIGINT NOT NULL,
user_id BIGINT,
sku_id BIGINT,
qty INT,
amount DECIMAL(12, 2)
) ENGINE = OLAP
DUPLICATE KEY(dt, order_id)
PARTITION BY RANGE(dt) (
PARTITION p202610 VALUES [("2026-10-01"), ("2026-11-01")),
PARTITION p202611 VALUES [("2026-11-01"), ("2026-12-01"))
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"store_row_column" = "true" -- 行列混存:高频点查大宽表时显著提速
);
-- 2) Aggregate 聚合模型:按天按用户预聚合,写入即聚合
CREATE TABLE lake.dws_user_daily (
dt DATE NOT NULL,
user_id BIGINT NOT NULL,
order_cnt BIGINT SUM,
gmv DECIMAL(16, 2) SUM,
last_tag VARCHAR(64) REPLACE, -- 非 key 列必须声明聚合类型
first_time DATETIME MIN
) ENGINE = OLAP
AGGREGATE KEY(dt, user_id)
PARTITION BY RANGE(dt) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES ("replication_num" = "3");
-- 3) Unique 主键模型:merge-on-write,CDC 同步订单状态首选
CREATE TABLE lake.ods_orders_pk (
order_id BIGINT NOT NULL,
user_id BIGINT,
status TINYINT,
amount DECIMAL(12, 2),
updated_at DATETIME
) ENGINE = OLAP
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true"
);
Rollup 与物化视图加速
两者的共同思想是"空间换时间",由优化器自动匹配改写查询,对业务方完全透明:
- Rollup(上卷表):按更粗粒度的 Key 建立子表,导入时自动同步聚合。
- 异步物化视图(Async MV):支持多表 JOIN、复杂计算、定时/手动刷新,可串联(MV 套 MV), 是湖仓分层的"DWS 加速层"利器。
-- Rollup:按天+商品聚合,报表按商品维度查询自动命中
ALTER TABLE lake.dwd_order_detail
ADD ROLLUP rollup_sku_day(dt, sku_id, qty, amount);
-- 异步物化视图:每小时刷新的分区级 GMV 汇总(支持湖仓外表参与)
CREATE MATERIALIZED VIEW lake.mv_city_gmv
BUILD IMMEDIATE
REFRESH AUTO ON SCHEDULE EVERY 1 HOUR
PROPERTIES ("grace_period" = "600")
AS
SELECT dt, city_id, sum(amount) AS gmv, count(DISTINCT user_id) AS uv
FROM lake.dwd_order_detail
GROUP BY dt, city_id;
-- 验证:查询被改写命中物化视图
EXPLAIN SELECT dt, sum(amount) FROM lake.dwd_order_detail
WHERE dt = '2026-11-01' GROUP BY dt; -- 计划中出现 mv_city_gmv 即命中
湖仓外表:Multi-Catalog 直查 Iceberg/Hive/Hudi
Doris 的 Multi-Catalog 机制把外部数据源(Iceberg、Hive、Hudi、ES、JDBC 等)挂载为独立 catalog, 无需导入即可查询,还支持与内表 JOIN、作为物化视图的基表——这是它扮演 湖仓查询门户的关键能力:
-- 挂载 Iceberg catalog(Hive Metastore 型)
CREATE CATALOG iceberg_lake PROPERTIES (
"type" = "iceberg",
"iceberg.catalog.type" = "hive",
"hive.metastore.uris" = "thrift://hive-meta:9083",
"aws.s3.enable_path_style_access" = "true",
"aws.s3.endpoint" = "https://oss.internal",
"aws.s3.access_key" = "******",
"aws.s3.secret_key" = "******"
);
-- 湖仓外表与 Doris 内表直接 JOIN:明细在湖,维表在库
SELECT o.dt, c.city_name, sum(o.amount) AS gmv
FROM iceberg_lake.lake.dwd_order_detail o -- Iceberg 外表
JOIN lake.dim_city c ON o.city_id = c.id -- Doris 内表维表
WHERE o.dt >= '2026-10-01'
GROUP BY o.dt, c.city_name;
-- 外表数据下沉:加热点分区到内表,获得极致并发
INSERT INTO lake.dwd_order_detail_hot
SELECT * FROM iceberg_lake.lake.dwd_order_detail
WHERE dt >= date_sub(current_date(), INTERVAL 7 DAY);
分区/分桶设计与 Colocate Join
- 分区(Partition):RANGE 按时间分区是 OLAP 标配,查询带时间条件即可裁剪掉绝大多数 tablet; 建议配合动态分区属性自动滚动建删分区。
- 分桶(Bucket):分桶列选高基数、常用于 JOIN/过滤的列(如 user_id),
让数据均匀打散到 tablet;桶数经验公式:
桶数 ≈ 表大小 / 单 tablet 目标大小(5~10GB),且为 BE 数量的整数倍。 - Colocate Join:把两表按相同分桶列、相同桶数放在同一组(Colocate Group), JOIN 时数据已按同键分布,免 Shuffle 本地 JOIN,是大表 JOIN 大表的终极速手段。
-- 动态分区:按天滚动,保留 90 天,预建 7 天
ALTER TABLE lake.dwd_order_detail SET (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-90",
"dynamic_partition.end" = "7",
"dynamic_partition.prefix" = "p"
);
-- 订单表与用户画像表同组同桶,本地 JOIN 免 Shuffle
CREATE TABLE lake.dws_user_profile (
user_id BIGINT NOT NULL,
level VARCHAR(8),
city_id INT,
updated_at DATETIME
) ENGINE = OLAP
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"colocate_with" = "shop_user_group" -- 与 dwd_order_detail 同组
);
-- 验证计划:出现 COLOCATE JOIN 即命中本地 JOIN
EXPLAIN SELECT /*+ SET_VAR(enable_nereids_planner=true) */
p.level, count(*) AS orders
FROM lake.dwd_order_detail o
JOIN lake.dws_user_profile p ON o.user_id = p.user_id
GROUP BY p.level;
ClickHouse:时序列存分析
架构定位与 MergeTree 家族
ClickHouse(CH)是面向真正的列式存储的 OLAP 引擎,以向量化执行、
稀疏索引、极致压缩著称。数据按 分片(Shard)→ 副本(Replica) 组织,
DDL 建在本地表(如 MergeTree)之上,再由 Distributed 表做分布式路由。
其核心引擎是 MergeTree 家族——数据按 ORDER BY 键排序存储,后台不断合并(Merge)数据分块(Part),
家族成员按"合并时做什么"分化:
| 引擎 | 合并行为 | 典型场景 |
|---|---|---|
| MergeTree | 仅排序合并,不去重不聚合 | 通用明细表:日志、事件、行为流水 |
| ReplacingMergeTree | 按 ORDER BY 键去重,保留版本(ver)最大的行 | 有更新语义的同步表(订单状态),需最终一致 |
| AggregatingMergeTree | 按 ORDER BY 键合并 AggregateFunction 状态 | 预聚合中间层(配合物化视图做增量聚合) |
| SummingMergeTree | 数值列按 ORDER BY 键求和 | 简单指标累加(金额、次数) |
| CollapsingMergeTree | 用 sign=1/-1 折叠行,删撤事件 | 不断变化的统计(实时正确计数) |
| ReplicatedMergeTree 系 | 上述引擎 + 副本复制(基于 Replication Log) | 所有生产表的标配前缀 Replicated* |
-- 日志明细表:ReplicatedMergeTree + 跳数索引 + TTL
CREATE TABLE logs.log_events ON CLUSTER log_cluster
(
event_time DateTime64(3),
server LowCardinality(String),
level Enum8('DEBUG' = 1, 'INFO' = 2, 'WARN' = 3, 'ERROR' = 4),
trace_id String,
message String CODEC(ZSTD(3)), -- 列级压缩算法,文本列高压缩比
attrs Map(String, String),
INDEX idx_trace trace_id TYPE bloom_filter GRANULARITY 4 -- 跳数索引:等值查 trace_id
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/log_events', '{replica}')
PARTITION BY toDate(event_time)
ORDER BY (server, event_time, trace_id) -- 排序键前缀决定查询快慢
TTL toDateTime(event_time) + INTERVAL 90 DAY -- 行级 TTL:过期自动物理删除
SETTINGS index_granularity = 8192;
-- 分布式表:业务查询入口,按 sharding_key 路由到分片
CREATE TABLE logs.log_events_all ON CLUSTER log_cluster
AS logs.log_events
ENGINE = Distributed(log_cluster, logs, log_events, rand());
时序 / 日志场景为何占优
- 排序键即索引:查询条件命中 ORDER BY 前缀(如 server + 时间范围)时, 稀疏主索引直接跳到目标 granule,扫描量呈数量级下降。
- 压缩比极高:按排序列存储后相邻值相似,日志/时序场景 ZSTD 压缩 10:1 很常见, 存储+扫描成本双低。
- 跳数索引(Data Skipping Index):
minmax、bloom_filter、tokenbf_v1等对非排序列(trace_id、ERROR 码)提供二级过滤。 - 适合追加写:时序/日志天然 append-only,与 MergeTree 的批量写入 + 后台合并模型完美契合。
物化视图与 TTL
CH 的物化视图是插入触发的增量计算:源表每批写入,MV 按查询逻辑把结果增量写入目标表,
搭配 AggregatingMergeTree 可实现"边写边聚合",查询时用 *Merge 函数合并中间状态:
-- 错误指标按分钟预聚合,写入即触发,无需定时任务
CREATE MATERIALIZED VIEW logs.log_error_1m
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(window_start)
ORDER BY (window_start, server)
AS
SELECT toStartOfMinute(event_time) AS window_start,
server,
countState() AS error_cnt, -- 存聚合状态而非结果
maxState(event_time) AS last_err
FROM logs.log_events_all
WHERE level >= 3
GROUP BY window_start, server;
-- 查询:用 *Merge 函数合并状态,得到最终指标
SELECT window_start, server,
countMerge(error_cnt) AS errors,
maxMerge(last_err) AS last_error_time
FROM logs.log_error_1m
WHERE window_start >= now() - INTERVAL 1 DAY
GROUP BY window_start, server
ORDER BY errors DESC;
-- 分层 TTL:热数据 SSD 保留 7 天,冷数据 HDD 保留 90 天后删除
ALTER TABLE logs.log_events
MODIFY TTL toDateTime(event_time) + INTERVAL 7 DAY TO VOLUME 'ssd_hot',
toDateTime(event_time) + INTERVAL 90 DAY TO VOLUME 'hdd_cold',
toDateTime(event_time) + INTERVAL 365 DAY DELETE;
ClickHouse vs Doris 详细对比
| 维度 | Doris | ClickHouse |
|---|---|---|
| 架构 | FE(元数据/计划)+ BE(存储/执行),自动分桶副本,部署简单 | 分片 + 副本,本地表 + Distributed 表,ZooKeeper/Keeper 协调复制 |
| 高并发 | 强:简单查询可承载数百~上千 QPS,并发模型友好 | 中:大查询吃满 CPU,高并发需限流/读写分离/预聚合兜底 |
| JOIN 能力 | 强:多表 JOIN、Colocate、Bucket Shuffle、MV 透明改写 | 偏弱:推荐大宽表;JOIN 需谨慎选择算法与内存 |
| 数据更新 | Unique MoW 支持高频 upsert,准实时可见 | ReplacingMergeTree 异步合并去重,查询需 FINAL(有代价) |
| 湖仓生态 | Multi-Catalog 直查 Iceberg/Hive/Hudi,可回写 | Iceberg/HDFS 引擎表可查但能力较浅,主打自有表格式 |
| 协议与工具 | MySQL 协议,BI/应用无缝接入 | 原生 TCP + HTTP + JDBC,生态偏观测/日志平台(Grafana 友好) |
| 运维复杂度 | 中:扩缩容自动均衡,学习曲线平缓 | 高:合并调优、分片规划、ZK 依赖、故障手工介入多 |
三引擎选型对比
三个引擎并非"三选一"的竞争关系,而是按查询模式分工的协作关系。 下表从六个维度给出横向对比,作为选型决策的速查表:
| 维度 | Trino | Doris | ClickHouse |
|---|---|---|---|
| 查询延迟定位 | 秒 ~ 分钟级(交互式 Ad-hoc) | 亚秒 ~ 秒级(固定报表) | 毫秒 ~ 秒级(单表扫描聚合) |
| 并发能力 | 中:数十~数百并发,受大查询挤占影响 | 高:简单查询可达千级 QPS | 中低:大查询独占资源,需预聚合兜底 |
| JOIN 能力 | 最强:跨源、大表、CBO 优化成熟 | 强:Colocate/Bucket Shuffle/MV 改写 | 偏弱:大宽表哲学,JOIN 是短板 |
| 湖仓支持 | 原生 Connector 一等公民,读 Iceberg/Delta/Hudi | Multi-Catalog 读 Iceberg/Hive/Hudi + 数据下沉 | Iceberg 等外表可查但较浅,多同步自建 |
| 数据更新 | 依赖表格式自身(写入场景弱) | Unique MoW 高频 upsert | 后台合并去重,异步最终一致 |
| 运维复杂度 | 中:进程无状态,K8s 友好 | 中:FE/BE 自动化程度高 | 高:合并/分片/副本/ZK 需持续关注 |
场景决策树
交互式联邦 → Trino
数据分析师在 Notebook 里做探索、需要 JOIN 业务库/湖仓/第三方库、 查询模式不可预知、数据规模在 PB 级以内但延迟容忍秒级。
Ad-hoc 跨源 JOIN SQL 方言标准高并发报表 → Doris
固定报表、实时大屏、面向 C 端的数据服务 API:查询模式收敛、要求亚秒与高 QPS、 维表关联与指标口径统一管理。
实时大屏 QPS 敏感 upsert 语义海量日志时序 → ClickHouse
日志检索、APM 指标、安全风控的行为流分析:数据体量巨大、按时间追加、 以扫描聚合与圈选为主,几乎不做复杂 JOIN。
日志/APM 高压缩比 明细圈选查询加速共性手段
无论选用哪个引擎,查询加速的底层逻辑是相通的:少扫数据、少算数据、不重复算。 以下五类手段在 Trino/Doris/ClickHouse 以及表格式侧各有对应实现:
1. 缓存
- 结果缓存:Doris Query Cache 按 tablet 分片缓存聚合结果,分区数据更新自动失效;
ClickHouse 23 年后内置
query_cache;Trino 侧常用 BI 层缓存(如 Metabase 缓存)。 - 数据块缓存:Trino
hive.parquet.pushdown-filter-enabled配合文件缓存、 Doris 外表/内表的 Page Cache、CH 的mark_cache/uncompressed_cache, 本质都是"热数据驻留内存,冷数据按需拉取"。 - 对象存储前置缓存:Alluxio / JuiceFS / JindoFS 等,为"存算分离 + 远端存储" 的架构补上本地性,对反复扫描的热分区收益显著。
2. 分区裁剪
查询条件命中分区列时,引擎在计划期直接跳过无关分区的全部文件。Iceberg 的隐藏分区
(partition by days(ts))让业务方按原始列 WHERE ts >= ... 查询也能裁剪,
避免了 Hive 时代"分区函数写错导致全表扫"的经典事故。
3. 谓词下推与文件跳过
引擎把 WHERE 条件尽量下推到存储层:Parquet 的 Row Group 级 min/max 统计、Bloom Filter, 加上 Iceberg 记录在 manifest 里的文件级统计,可以在不读文件的情况下判断"这个文件要不要打开"。
4. 物化视图与预聚合
高频固定的聚合查询(大屏、日报)应沉到物化层:Doris Async MV、CH AggregatingMergeTree+MV、 或直接在湖仓侧用 Spark/Flink 定时生成 DWS 汇总表(引擎无关,Trino/Doris 都能查)。 判断标准:查询模式收敛 + 消费频率高 + 口径稳定。
5. 排序与聚簇
- Iceberg SORT BY:写入时按高频过滤列排序,让数据局部性最大化:
ALTER TABLE lake.dwd_sales_orders WRITE ORDERED BY (city_id, dt, user_id); -- 重写时按过滤列排序聚簇 -- 或用分布式策略(多列等权过滤场景) ALTER TABLE lake.dwd_user_events WRITE ORDERED BY zorder(campaign_id, channel, dt); - Doris 分桶即聚簇:分桶列选高频过滤/JOIN 列;CH 的 ORDER BY 前缀同理。 三者的共同思想:让"经常一起被过滤的列"在物理上相邻。
Iceberg 统计信息(write.metrics)对引擎的价值
Iceberg 在 manifest 中为每个数据文件记录列级统计(min/max、null 计数、NaN 计数、值计数),
由写入侧表属性 write.metadata.metrics.default 控制收集粒度。
Trino/Doris/Spark 读表时利用这些统计做文件跳过(File Skipping),
统计越精细,跳过越激进,扫描 I/O 越少:
-- 建表时设置统计收集模式
CREATE TABLE lake.dwd_sales_orders (
dt DATE,
order_id BIGINT,
city_id INT,
user_id BIGINT,
amount DECIMAL(12, 2)
) USING iceberg
PARTITIONED BY (days(dt))
TBLPROPERTIES (
'write.metadata.metrics.default' = 'truncate(16)', -- 字符串列截断统计,均衡体积与精度
'write.metadata.metrics.column.structures' = 'full', -- 高基数 ID 列用全文统计
'write.metadata.metrics.column.contents' = 'none' -- 大字段不收集,控制元数据膨胀
);
| metrics 模式 | 收集内容 | 适用建议 |
|---|---|---|
| none | 仅记录行数 | 大对象/超宽表,元数据优先 |
| counts | 行数 + null/NaN 计数 | 过滤主要靠分区与排序列时 |
| truncate(N) | counts + 截断到 N 字节的 min/max | 默认推荐:字符串列防元数据膨胀 |
| full | counts + 完整 min/max | 定长数值/高基数过滤列,跳过率最优 |
适用边界
何时不引入新引擎
- 现有引擎未调优到位:加了索引/MV/分区裁剪就能达标时,先调优再谈选型; "换引擎"应是量化证据(压测报告)驱动的决策,而非追逐新技术的冲动。
- 数据规模未到量级:GB~百 GB 级、并发个位数,Spark Thrift Server / 单机 DuckDB 甚至 Postgres 就能胜任。
- 团队无运维承接能力:每个引擎都是 7×24 的运维对象(升级、故障、安全补丁), 没有专职或轮值团队之前,引擎数量应保持克制。
- 场景可被"加速层"覆盖:多数报表加速需求,用 Doris 一个引擎 + MV 即可覆盖, 不需要再引入 CH/StarRocks 等第二 OLAP。
何时不该用 OLAP 引擎做 ETL
- 缺乏作业级容错语义:OLAP 引擎为交互查询设计,没有 Spark/Flink 那样的 stage 重试、checkpoint、失败恢复机制;长 ETL 中途失败往往只能整体重跑。
- 大 shuffle 与多阶段转换是常态:复杂清洗、多级 JOIN、回填重算会长时间占用 查询资源,与线上 Ad-hoc 争抢内存(Trino 会互相杀查询),抖动直接传导给业务。
- 写放大与事务保障不足:OLAP 引擎的写入面向"批量导入",不是面向"事务性多表写入"; 跨表一致性、幂等重写、Schema 演进等湖仓写入语义应由 Spark/Flink + 表格式承担。
- 正确分工:ETL/ELT 归 Spark/Flink,交互查询归 OLAP 引擎; "用 Doris 的 INSERT INTO SELECT 做轻量调度加工"可以,"把它当日批主引擎"不行。