写在前面
承接上一篇,本文聚焦 Doris 的实战核心:表设计、分区与分桶、索引体系、查询优化、物化视图、Colocate Join,以及典型应用场景(实时数仓 / 多维分析)。基于 Doris 2.1.x。
一、表设计核心:分区与分桶
Doris 一张表的物理结构是 Partition(分区)→ Bucket(分桶)→ Tablet(数据片)。表设计是否合理,80% 取决于分区和分桶。
1.1 分区(Partition)
分区是逻辑切分,最常见的是按时间分区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
CREATE TABLE access_log (
dt DATE, ts DATETIME, user_id BIGINT, page VARCHAR(128), cost_ms INT
)
DUPLICATE KEY(dt, ts, user_id)
PARTITION BY RANGE(dt) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 4
PROPERTIES(
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30", -- 保留最近 30 天
"dynamic_partition.end" = "3", -- 预创建未来 3 天
"dynamic_partition.prefix" = "p",
"dynamic_partition.replication_num" = "1"
);
|
1
2
3
4
5
6
7
8
|
动态分区:
- 自动按天创建/删除分区
- 常用配置:start=-30(删历史 30 天前的),end=3(预创建 3 天)
为什么必须分区:
- 按时间裁剪:SELECT ... WHERE dt='2026-06-01' 只扫一个分区
- 生命周期管理:TTL 删旧分区,比逐行删除快几个数量级
- 避免扫描全表:OLAP 查询 99% 都是按时间筛选
|
1.2 分桶(Bucket / Tablet)
分桶是物理切分,决定数据如何分布到 BE 节点,以及查询并行度。
1
|
DISTRIBUTED BY HASH(user_id) BUCKETS 8
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
关键选择:
1. 分桶列:
- 选高基数、常用于过滤/JOIN 的列
- 一般选 user_id、order_id 等
- 不要选 dt(dt 已经是分区列,重复使用没意义)
- 不要选低基数列(如性别:会造成数据倾斜)
2. 分桶数(BUCKETS):
- 单 Tablet 大小推荐 1~10GB
- 估算:每天数据量 / (单 Tablet 大小) = 分桶数
- 例:每天 100GB 数据,单 Tablet 2GB → BUCKETS = 50
3. 并发度:
- 一条查询的并行度 ≈ 涉及 Tablet 数
- 太少(如 1 个)查询无法并行
- 太多(如几千个)元数据开销大
|
1.3 估算分桶数的公式
1
2
3
4
5
6
7
8
9
10
|
假设:
- 日增量 100GB(含索引、压缩后实际存储约 30GB)
- 单 Tablet 推荐 1~2GB
- 按天分区
分桶数 = 单分区数据量 / 单 Tablet 大小
= 30GB / 1.5GB
≈ 20 桶
后验:通过 SHOW PARTITIONS 查看每个 Tablet 的大小,调整。
|
二、索引体系
Doris 的索引分两类:内置索引(建表自动)和显式索引(用户创建)。
2.1 前缀索引(Prefix Index)
Doris 默认对Key 列的前 36 字节建立前缀索引,类似 MySQL 的聚簇索引。
1
2
3
4
5
|
CREATE TABLE t (
dt DATE, ts DATETIME, user_id BIGINT, page VARCHAR(128)
)
DUPLICATE KEY(dt, ts, user_id) -- 这三列自动建立前缀索引
...
|
1
2
3
4
5
6
7
8
9
|
含义:
WHERE dt='2026-06-01' ✅ 走前缀索引
WHERE dt='2026-06-01' AND ts > '10:00' ✅ 走前缀索引
WHERE user_id = 1001 ❌ 不走(跳过了 dt、ts)
设计建议:
- Key 列顺序按"查询过滤频率从高到低"排
- 通常顺序:dt → ts → user_id → ...
- Key 列总长度 ≤ 36 字节(超出部分不进入前缀索引)
|
2.2 倒排索引(Inverted Index)
Doris 2.0+ 引入,对字符串/数值列做全文检索,类似 Elasticsearch。
1
2
3
4
5
6
7
|
ALTER TABLE access_log
ADD INDEX idx_page (page) USING INVERTED
PROPERTIES("parser" = "english")
;
ALTER TABLE access_log ADD INDEX idx_user (user_id) USING INVERTED;
ALTER TABLE access_log BUILD INDEX;
|
1
2
3
4
5
|
-- 用法:直接 WHERE,自动用索引
SELECT * FROM access_log
WHERE page MATCH_ANY 'home list detail';
SELECT * FROM access_log WHERE user_id = 1001;
|
1
2
3
4
|
特点:
- 大幅提升等值/范围查询
- 支持全文检索(MATCH_ALL / MATCH_ANY)
- 替代了老版本的 BITMAP_INDEX(更通用)
|
2.3 Bloom Filter 索引
针对高基数列的等值查询,简单且高效:
1
2
3
|
ALTER TABLE access_log SET (
"bloom_filter_columns" = "user_id,page"
);
|
1
2
3
4
5
6
7
8
9
|
适用:
- 高基数列(user_id, trace_id 等)
- 等值查询(=)
- IN 列表查询
不适用:
- 低基数列(性别、状态)
- 范围查询(>, <)
- 加入了反而浪费空间
|
2.4 索引选型
| 场景 |
推荐索引 |
| Key 列前缀范围查询 |
前缀索引(自动) |
| 高基数列等值 |
Bloom Filter |
| 字符串全文检索 |
倒排索引 |
| 低基数列等值/范围 |
倒排索引 |
三、查询优化
3.1 CBO 优化器
Doris 默认开启 CBO(Cost-Based Optimizer)。
1
2
3
|
-- 查看实际执行计划
EXPLAIN VERBOSE
SELECT dt, COUNT(*) FROM access_log GROUP BY dt;
|
1
2
3
4
5
6
|
CBO 会考虑:
- 表的统计信息(行数、基数、选择率)
- JOIN 顺序(小表驱动大表)
- 是否广播 vs 重分布
需要:定期 ANALYZE TABLE 让统计信息新鲜
|
1
|
ANALYZE TABLE access_log WITH SYNC;
|
3.2 JOIN 策略
Doris 有三种 JOIN 执行方式:
1
2
3
4
5
6
7
8
9
10
11
|
1. Broadcast Join(广播)
小表(< 1GB 默认)广播到所有 BE 节点
适合:大表 JOIN 小表(如事实表 JOIN 维度表)
2. Shuffle Join(重分布)
两表都按 JOIN KEY 重分布,再本地 JOIN
适合:大表 JOIN 大表
3. Colocate Join(同分布,零 Shuffle)
两表 JOIN KEY 类型一致、分桶方式一致、副本一致
适合:频繁 JOIN 的大表(如订单表 JOIN 用户表)
|
Colocate Join 配置示例:
1
2
3
4
5
6
7
8
|
-- 1. 建一个 colocate group
ALTER TABLE orders SET ("colocate_with" = "user_grp");
ALTER TABLE users SET ("colocate_with" = "user_grp");
-- 2. 两表都按 user_id 分桶,自动按相同方式分布
-- 3. JOIN 时 FE 识别到 colocate,跳过网络 Shuffle
SELECT a.order_id, b.name
FROM orders a JOIN users b USING(user_id);
|
1
2
3
|
性能差异(实测):
Shuffle Join: ~10s(网络 + 落盘)
Colocate Join: ~1s(零 Shuffle)
|
3.3 Runtime Filter
大表 JOIN 小表时,FE 会动态生成一个过滤条件下推到扫描节点。
1
2
3
4
5
|
-- 大表 JOIN 小表,会自动启用 Runtime Filter
SELECT count(*)
FROM access_log a
JOIN user_dim b ON a.user_id = b.user_id
WHERE b.country = 'CN';
|
1
2
3
4
5
6
|
原理:
1. 先扫描 user_dim(小表)过滤后拿到 user_id 集合
2. 把这个集合推给 BE 节点扫描 access_log 时使用
3. 大表扫描时直接过滤掉不匹配的行
效果:扫描数据量减少 10~1000 倍
|
3.4 常见慢查询排查
1
2
3
4
5
6
7
8
9
10
11
12
|
-- 1. 看查询 Profile
SET enable_profile = true;
-- 跑一次慢查询,然后 Web UI → QueryProfile 查看每一步耗时
-- 2. 慢的常见原因
-- - 没走分区裁剪:扫描了所有分区
-- - 没走索引:Key 列顺序设计不当
-- - 数据倾斜:某个 Tablet 特别大
-- - JOIN 错:用了 Shuffle 而非 Colocate
-- 3. 看 Tablet 分布是否均衡
SHOW TABLET FROM access_log;
|
四、物化视图(Materialized View)
物化视图 = 预计算 + 自动重写查询。
4.1 同步物化视图(Rollup)
适合单表聚合加速,写入时自动维护:
1
2
3
4
|
CREATE MATERIALIZED VIEW mv_user_pv AS
SELECT dt, user_id, COUNT(*) AS pv
FROM access_log
GROUP BY dt, user_id;
|
1
2
3
4
|
原理:
- 创建后,新写入的数据会同时写入主表和物化视图
- 查询时优化器自动判断是否使用物化视图
- 用户无需改 SQL,对应用透明
|
4.2 异步物化视图(Multi-Table)
Doris 2.1+ 支持,可以跨表、跨库聚合:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
CREATE MATERIALIZED VIEW mv_daily_summary
DISTRIBUTED BY HASH(dt) BUCKETS 4
REFRESH ASYNC EVERY(INTERVAL 1 HOUR) -- 每小时异步刷新
AS
SELECT
a.dt,
b.country,
COUNT(*) AS pv,
COUNT(DISTINCT a.user_id) AS uv,
SUM(a.cost_ms) AS total_cost
FROM access_log a
JOIN user_dim b ON a.user_id = b.user_id
GROUP BY a.dt, b.country;
|
1
2
3
4
|
适用场景:
- 复杂的实时大盘报表
- 上百个 Dashboard 的指标预聚合
- 流量减少:把 1TB 大表查询变成 10GB 物化视图查询
|
4.3 物化视图 vs Rollup
| 维度 |
Rollup(同步) |
异步物化视图 |
| 单表/多表 |
单表 |
单/多表 |
| 维护 |
写入时同步 |
异步刷新 |
| 延迟 |
实时 |
分钟级 |
| 复杂度 |
简单 |
较复杂 |
| 推荐 |
简单聚合 |
复杂报表 |
五、数据导入实战
5.1 Stream Load(实时小批量)
1
2
3
4
5
6
|
curl -u root: \
-H "label:order_20260614_001" \
-H "column_separator:," \
-H "columns:order_id,user_id,amount,created_at" \
-T /tmp/orders.csv \
http://localhost:8030/api/demo/orders/_stream_load
|
1
2
3
4
5
6
7
8
9
10
11
|
关键参数:
- label:导入的唯一标识,保证幂等(同一 label 不会重复导入)
- column_separator:CSV 分隔符
- columns:CSV 列顺序 → 表列映射
返回值(成功示例):
{
"Status": "Success",
"NumberTotalRows": 1000,
"NumberLoadedRows": 1000
}
|
5.2 Routine Load(消费 Kafka)
1
2
3
4
5
6
7
8
9
10
|
CREATE ROUTINE LOAD rl_orders ON orders
WITH PROPERTIES (
"format" = "json",
"jsonpaths" = "[\"$.order_id\",\"$.user_id\",\"$.amount\",\"$.ts\"]"
)
FROM KAFKA (
"kafka_broker_list" = "kafka:9092",
"kafka_topic" = "orders",
"kafka_group_id" = "doris_orders"
);
|
1
2
3
4
5
6
7
|
特点:
- 持续消费 Kafka topic
- 支持 Exactly-Once(通过 label + Kafka offset)
- 自动容错:BE 失败自动重试
典型链路:
MySQL → Canal/Debezium → Kafka → Doris Routine Load → 实时数仓
|
5.3 Broker Load(批量导入)
1
2
3
4
5
6
7
8
9
10
|
LOAD LABEL demo.broker_load_001
(
DATA INFILE("hdfs://namenode:8020/data/orders/*.parquet")
INTO TABLE orders
FORMAT AS PARQUET
)
WITH BROKER "broker1"
PROPERTIES (
"timeout" = "3600"
);
|
适合一次性大规模历史数据迁移(数百 GB ~ TB)。
六、典型应用场景
6.1 实时数仓
1
2
3
4
5
6
7
8
9
10
11
12
13
|
数据链路:
业务 DB → Canal → Kafka → Doris Routine Load
↓
Doris 实时表(明细模型 / 主键模型)
↓
Doris 物化视图(按小时/天聚合)
↓
BI / 报表查询
特点:
- 秒级延迟(数据写入即可查)
- 主键模型保证 UPSERT 一致性
- BI 直接用 MySQL 协议连接
|
6.2 多维分析(OLAP)
1
2
3
4
5
6
7
8
9
10
11
|
-- 用户行为分析:UV / PV / 漏斗
SELECT
dt,
page,
COUNT(DISTINCT user_id) AS uv,
COUNT(*) AS pv,
AVG(cost_ms) AS avg_cost
FROM access_log
WHERE dt >= '2026-06-01'
GROUP BY dt, page
ORDER BY dt, uv DESC;
|
1
2
3
4
|
为什么 Doris 适合:
- BITMAP_UNION 高效去重(UV 计算)
- 列存 + 向量化扫描快
- 物化视图加速常用维度组合
|
6.3 日志检索
1
2
3
4
5
6
|
-- 利用倒排索引做日志搜索
SELECT * FROM access_log
WHERE page MATCH_ALL 'home error'
AND ts >= '2026-06-14 00:00:00'
ORDER BY ts DESC
LIMIT 100;
|
新版本 Doris 的倒排索引能力,可以部分替代 Elasticsearch 做日志场景。
七、小结
本文学习了 Doris 的进阶内容:
- 表设计核心:分区(按时间)+ 分桶(按基数列)+ 副本
- 索引体系:前缀索引、倒排索引、Bloom Filter
- 查询优化:CBO 优化器、JOIN 策略、Colocate Join、Runtime Filter
- 物化视图:同步 Rollup(单表)和异步物化视图(多表)
- 数据导入:Stream Load、Routine Load、Broker Load
- 典型场景:实时数仓、多维分析、日志检索
1
2
3
4
5
6
|
落地建议:
- 表设计:先按时间分区,再选高基数列分桶,BUCKETS 数按数据量算
- 模型选择:明细用 Duplicate,更新用 Primary Key
- 索引:低基数 + 字符串检索 → 倒排;高基数等值 → Bloom Filter
- JOIN:维度表用 Broadcast,事实表频繁 JOIN 用 Colocate
- 加速:高频聚合查询用物化视图,复杂报表用异步物化视图
|
Doris 的核心价值在于用极简的部署 + MySQL 协议,覆盖实时数仓和 OLAP 分析两大场景。后续如果需要进一步深入,可以研究:
- FE 元数据选主机制(BDB-JE)
- BE 向量化执行引擎
- CBO 优化器的代价模型
- 存算分离架构(Doris 3.x Cloud 版)