61. Apache Doris
目录
点击展开目录
Doris 概述与架构
Doris简介
Apache Doris 是一个基于 MPP(Massively Parallel Processing,大规模并行处理)架构的高性能实时分析数据库,主要用于 OLAP(在线分析处理)场景。Doris 由百度开发并于 2018 年贡献给 Apache 基金会,2022 年成为 Apache 顶级项目。
核心定位:
- 实时数仓:支持秒级数据导入和查询响应
- 统一分析平台:一套系统支持多种分析场景
- AI-Native:4.x 版本开始深度集成 AI 能力
发展历程:
| 时间 | 版本 | 重要特性 |
|---|---|---|
| 2017 | 内部版本 | 百度内部开发,命名为 Palo |
| 2018 | 开源 | 贡献给 Apache 基金会,改名 Doris |
| 2020 | 0.13 | 支持物化视图、Colocation Join |
| 2022 | 1.0 | 成为 Apache 顶级项目,向量化执行引擎 |
| 2023 | 2.0 | 湖仓一体、多表物化视图 |
| 2024 | 3.0 | 存算分离架构、倒排索引增强 |
| 2025 | 4.0 | 向量检索、全文检索、AI 函数 |
核心特性
1. 极致性能
- MPP 并行计算:查询自动分布到多个节点并行执行
- 向量化执行引擎:批量处理数据,充分利用 CPU SIMD 指令
- 列式存储:只读取需要的列,大幅减少 I/O
- 智能索引:前缀索引、ZoneMap、BloomFilter、倒排索引、向量索引
性能对比(TPC-H 1TB 测试):
| 引擎 | 总耗时 | 相对性能 |
|---|---|---|
| Doris | 120s | 1x(基准) |
| ClickHouse | 180s | 0.67x |
| Presto | 450s | 0.27x |
| Hive | 3600s | 0.03x |
2. 实时性
- 秒级数据可见:Stream Load 支持微批导入,延迟 < 1s
- 增量更新:支持 Unique Key 模型的实时 Upsert
- 实时物化视图:数据写入时自动更新物化视图
3. 易用性
- 标准 SQL:兼容 MySQL 协议,支持复杂 SQL 查询
- 多种导入方式:Stream Load、Broker Load、Routine Load、Flink CDC
- 丰富的生态:与 Flink、Spark、Kafka、Hive 等无缝集成
4. 高可用
- 多副本机制:数据自动复制到多个节点
- 自动故障转移:节点故障时自动切换到副本
- 在线扩缩容:支持不停机动态增减节点
5. AI 增强(4.x 核心亮点)
- 向量检索:基于 HNSW 算法,支持亿级向量毫秒级查询
- 全文检索:倒排索引 + BM25 算法,精准文本检索
- 混合检索:一条 SQL 同时支持向量召回和关键词召回
- AI 函数:数据库内直接调用大模型进行推理
应用场景
典型应用领域:
| 场景 | 描述 | 优势 |
|---|---|---|
| 实时数据大屏 | 实时展示业务指标、监控告警 | 秒级延迟,高并发查询 |
| 用户行为分析 | 用户画像、漏斗分析、留存分析 | 支持复杂多维分析 |
| 日志分析 | 系统日志、应用日志、审计日志 | 全文检索 + 结构化查询 |
| 企业报表 | BI 报表、管理驾驶舱 | 物化视图加速,查询稳定 |
| A/B 测试分析 | 实验效果评估、指标对比 | 实时数据,灵活聚合 |
| 企业知识库 RAG | 文档检索、智能问答 | 向量检索 + 全文检索 |
| 推荐系统 | 实时特征计算、召回排序 | 低延迟点查,高吞吐写入 |
行业应用:
整体架构
架构设计理念:
- 存算一体:计算和存储在同一节点,减少网络传输
- 无单点故障:FE 和 BE 都支持多副本
- 弹性扩展:支持水平扩展,线性提升性能
系统架构图:
架构层次说明:
1. Frontend (FE) - 前端节点
| 角色 | 职责 | 数量 |
|---|---|---|
| Master | 元数据管理、DDL 执行、查询调度 | 1 个(自动选举) |
| Follower | 元数据备份、查询解析和优化 | 2+ 个(推荐奇数) |
| Observer | 只读查询、负载均衡 | 0+ 个(可选) |
2. Backend (BE) - 后端节点
- 数据存储:负责数据的实际存储
- 查询执行:执行具体的计算任务
- 数据副本:每个 Tablet 有多个副本分布在不同 BE
- 自动负载均衡:数据自动均衡分布
3. 存储层
- 本地磁盘:默认存储方式,性能最优
- HDFS:支持存算分离,降低成本
- 对象存储:S3/OSS,云原生部署
关键设计:
无共享架构(Shared-Nothing):
- 每个 BE 节点独立存储和计算
- 节点间通过网络交换数据
- 避免单点瓶颈,线性扩展
元数据高可用:
- FE 使用 BDBJE(Berkeley DB Java Edition)存储元数据
- 基于 Paxos 协议实现一致性
- Master 故障时自动选举新 Master
数据分片(Tablet):
- 表数据按分区和分桶切分成 Tablet
- Tablet 是数据管理和副本的基本单位
- 默认大小 256MB,自动 Compaction
核心组件与原理
Frontend (FE)
FE 核心职责:
1. 元数据管理
FE 维护整个集群的元数据信息,包括:
- 数据库和表结构:Schema 信息、分区分桶规则
- 数据分布:Tablet 位置、副本信息
- 用户权限:用户、角色、权限配置
- 集群拓扑:FE 和 BE 节点信息
元数据存储结构:
2. 查询解析与优化
查询处理流程:
优化器功能:
- 谓词下推:将过滤条件推到存储层
- 列裁剪:只读取需要的列
- 分区裁剪:只扫描相关分区
- Join 重排序:选择最优 Join 顺序
- 物化视图改写:自动使用物化视图加速查询
- CBO(基于成本优化):根据统计信息选择最优执行计划
3. 查询调度
FE 负责将查询任务分发到各个 BE 节点:
- Fragment 划分:将查询计划切分成多个 Fragment
- 负载均衡:根据 BE 负载选择执行节点
- 容错处理:BE 故障时重新调度任务
4. DDL 执行
FE Master 负责执行所有 DDL 操作:
- 建表:创建表结构,分配 Tablet
- 数据导入:协调数据导入任务
- Schema 变更:在线修改表结构
- 副本管理:副本创建、删除、均衡
FE 高可用机制:
| 机制 | 说明 |
|---|---|
| 主从复制 | Follower 实时同步 Master 的元数据 |
| 自动选举 | Master 故障时,Follower 自动选举新 Master |
| Checkpoint | 定期生成元数据快照,加速恢复 |
| Journal 日志 | 记录所有元数据变更,保证一致性 |
Backend (BE)
BE 核心职责:
1. 数据存储
BE 使用列式存储引擎存储数据:
- Segment 文件:数据按 Segment 组织,每个 Segment 包含多行数据
- 列式布局:每列独立存储,支持高效压缩和列裁剪
- 索引文件:前缀索引、ZoneMap、BloomFilter、倒排索引、向量索引
存储目录结构:
/data/doris/be/storage/
├── data/
│ ├── 0/ # 磁盘路径 ID
│ │ ├── 10001/ # Tablet ID
│ │ │ ├── 10001.hdr # Tablet 元数据
│ │ │ ├── 20001_0.dat # Rowset 数据文件
│ │ │ ├── 20001_0.idx # 索引文件
│ │ │ └── 20002_0.dat # 另一个 Rowset
│ │ └── 10002/ # 另一个 Tablet
│ └── 1/ # 另一个磁盘路径
└── meta/ # BE 本地元数据
2. 查询执行
BE 执行 FE 下发的查询任务:
- 向量化执行:批量处理数据(默认 4096 行)
- 并行执行:多线程并行扫描和计算
- Pipeline 执行:流水线处理,减少中间结果物化
- Runtime Filter:动态生成过滤条件,减少数据扫描
执行引擎架构:
3. 数据导入
BE 负责接收和写入数据:
- Stream Load:HTTP 接口,适合小批量实时导入
- Broker Load:通过 Broker 从 HDFS/S3 导入大批量数据
- Routine Load:从 Kafka 持续消费数据
- Insert Into:通过 SQL 插入数据
导入流程:
4. Compaction(数据合并)
BE 后台自动执行 Compaction,优化存储和查询性能:
| Compaction 类型 | 触发条件 | 作用 |
|---|---|---|
| Base Compaction | Rowset 数量 > 阈值 | 合并多个小文件,减少文件数 |
| Cumulative Compaction | 增量数据累积 | 合并增量数据到基线 |
| Vertical Compaction | 列数较多 | 按列分批合并,降低内存压力 |
Compaction 优化:
- 自动触发:根据文件数量和大小自动触发
- 优先级调度:根据 Tablet 访问频率调整优先级
- 限流控制:避免 Compaction 影响查询性能
元数据管理
元数据架构:
Doris 使用 BDBJE(Berkeley DB Java Edition) 存储元数据,基于 Paxos 协议 实现一致性。
元数据同步流程:
元数据持久化:
- Journal 日志:记录所有元数据变更操作
- Image 镜像:定期生成元数据快照(Checkpoint)
- 回放机制:启动时加载 Image + 回放 Journal
元数据恢复:
# FE 启动时的元数据恢复流程
1. 加载最新的 Image 文件
2. 回放 Image 之后的 Journal 日志
3. 与其他 FE 节点同步元数据
4. 完成恢复,开始提供服务
查询执行引擎
向量化执行引擎:
Doris 2.0 引入了全新的向量化执行引擎,性能提升 3-10 倍。
向量化 vs 火山模型:
| 特性 | 火山模型(传统) | 向量化执行 |
|---|---|---|
| 处理单位 | 单行(Tuple) | 批量(Batch,4096 行) |
| 函数调用 | 每行调用一次 | 每批调用一次 |
| CPU 利用率 | 低(分支预测失败多) | 高(SIMD 指令) |
| 缓存命中率 | 低 | 高 |
| 性能 | 基准 | 3-10x |
向量化执行示例:
// 传统火山模型(伪代码)
for (int i = 0; i < row_count; i++) {
if (column_a[i] > 100) {
result[i] = column_b[i] * 2;
}
}
// 向量化执行(伪代码)
// 1. 批量过滤
auto filter = column_a > 100; // SIMD 指令
// 2. 批量计算
result = column_b * 2; // SIMD 指令
// 3. 应用过滤
result = result[filter];
Pipeline 执行引擎:
Doris 3.0 引入 Pipeline 执行引擎,进一步提升并行度和资源利用率。
Pipeline vs 传统执行:
Pipeline 优势:
- 更高并行度:自动拆分成多个 Pipeline Task
- 更好的资源利用:动态调度,避免线程阻塞
- 更低的内存占用:流式处理,减少中间结果物化
Runtime Filter(运行时过滤):
在 Join 执行时动态生成过滤条件,减少数据扫描。
Runtime Filter 类型:
| 类型 | 适用场景 | 过滤效果 |
|---|---|---|
| IN Filter | 小表数据量 < 1024 | 精确过滤 |
| BloomFilter | 小表数据量较大 | 概率过滤(误判率可控) |
| MinMax Filter | 数值类型 | 范围过滤 |
Compaction 优化:
- 自动触发:根据文件数量和大小自动触发
- 优先级调度:根据 Tablet 访问频率调整优先级
- 限流控制:避免 Compaction 影响查询性能
5. 数据副本管理
BE 负责管理数据副本的创建、同步和恢复。
副本同步机制:
副本一致性:
- Quorum 机制:多数副本写入成功即可返回
- 版本控制:每个 Rowset 有唯一版本号
- 副本修复:定期检查副本一致性,自动修复不一致副本
副本均衡:
-- 查看副本分布
SHOW PROC '/statistic';
-- 手动触发副本均衡
ADMIN SET FRONTEND CONFIG ("tablet_rebalancer_type" = "disk_and_tablet");
ADMIN SET FRONTEND CONFIG ("balance_load_score_threshold" = "0.1");
副本均衡策略:
| 策略 | 说明 | 触发条件 |
|---|---|---|
| 磁盘均衡 | 均衡各磁盘使用率 | 磁盘使用率差异 > 10% |
| Tablet 均衡 | 均衡各 BE 的 Tablet 数量 | Tablet 数量差异 > 10% |
| 副本修复 | 修复缺失或损坏的副本 | 副本数 < 配置值 |
6. 数据版本管理(MVCC)
Doris 使用 MVCC(多版本并发控制)实现事务隔离。
版本管理原理:
Tablet 版本链:
Version 1: [Row1, Row2, Row3]
Version 2: [Row1', Row4] (Row1 更新,新增 Row4)
Version 3: [Row2'] (Row2 更新)
查询时选择版本:
- 读已提交:读取最新已提交版本
- 快照读:读取查询开始时的版本
版本合并(Compaction):
Compaction 类型详解:
Base Compaction:
- 触发条件:Cumulative Rowset 数量 > 阈值(默认 10)
- 合并范围:所有 Cumulative Rowset 合并到 Base Rowset
- 执行频率:较低(小时级)
- 资源消耗:高
Cumulative Compaction:
- 触发条件:增量 Rowset 数量 > 阈值(默认 5)
- 合并范围:增量 Rowset 之间合并
- 执行频率:较高(分钟级)
- 资源消耗:中等
Vertical Compaction:
- 适用场景:列数较多的表(> 100 列)
- 优化策略:按列分批合并,降低内存压力
- 性能提升:减少内存峰值 50-70%
Compaction 调优:
-- 调整 Compaction 参数
ALTER TABLE large_table SET (
"compaction_policy" = "time_series", -- 时序数据优化
"time_series_compaction_goal_size_mbytes" = "1024", -- 目标文件大小
"time_series_compaction_file_count_threshold" = "10", -- 文件数阈值
"time_series_compaction_time_threshold_seconds" = "3600" -- 时间阈值
);
-- 查看 Compaction 状态
SHOW PROC '/compactions';
-- 手动触发 Compaction
ADMIN COMPACT TABLE large_table;
7. 数据删除机制
Doris 支持多种数据删除方式。
Delete 语句:
-- 标记删除(逻辑删除)
DELETE FROM user_table WHERE age < 18;
-- 删除原理:
-- 1. 生成 Delete Predicate
-- 2. 查询时过滤被删除的数据
-- 3. Compaction 时物理删除
Truncate 分区:
-- 快速删除整个分区
TRUNCATE TABLE sales_data PARTITION (p20240101);
TTL(Time To Live):
-- 自动删除过期数据
ALTER TABLE logs SET (
"storage_cooldown_time" = "2024-01-01 00:00:00" -- 冷却时间
);
-- 动态分区自动删除
ALTER TABLE logs SET (
"dynamic_partition.enable" = "true",
"dynamic_partition.start" = "-7" -- 只保留最近 7 天
);
8. 数据冷热分层
Doris 支持数据冷热分层存储,降低存储成本。
冷热分层架构:
配置冷热分层:
-- 创建远程存储资源
CREATE RESOURCE "s3_resource"
PROPERTIES (
"type" = "s3",
"s3.endpoint" = "s3.amazonaws.com",
"s3.region" = "us-west-2",
"s3.bucket" = "doris-cold-data",
"s3.access_key" = "your_access_key",
"s3.secret_key" = "your_secret_key"
);
-- 创建冷热分层策略
CREATE STORAGE POLICY cold_storage_policy
PROPERTIES (
"storage_resource" = "s3_resource",
"cooldown_datetime" = "2024-01-01 00:00:00"
);
-- 应用到表
ALTER TABLE large_table SET (
"storage_policy" = "cold_storage_policy"
);
冷热分层优势:
| 指标 | 热数据(SSD) | 冷数据(S3) | 节省 |
|---|---|---|---|
| 存储成本 | $0.10/GB/月 | $0.02/GB/月 | 80% |
| 查询延迟 | 10-50ms | 100-500ms | - |
| 适用场景 | 最近 7 天 | 7 天以前 | - |
数据读写流程详解
写入流程
Doris 的写入流程涉及 FE 协调、BE 执行、副本同步、Compaction 等多个环节。
写入流程全景图:
写入流程详细步骤:
1. 客户端提交写入请求
-- Stream Load 示例
curl --location-trusted -u root: \
-H "label:load_20240310_001" \
-H "column_separator:," \
-T data.csv \
http://fe_host:8030/api/db_name/table_name/_stream_load
2. FE 解析和验证
FE 执行的检查:
├── 权限验证:检查用户是否有写入权限
├── Schema 验证:检查列类型、数量是否匹配
├── 分区验证:检查分区是否存在
├── Label 去重:检查 Label 是否已存在(幂等性保证)
└── 资源检查:检查集群资源是否充足
3. 生成写入计划
写入计划包含:
├── 目标表信息:Database、Table、Partition
├── 数据格式:CSV、JSON、Parquet
├── 列映射关系:源列 -> 目标列
├── 转换表达式:类型转换、计算列
└── 过滤条件:WHERE 子句
4. 选择目标 Tablet
Tablet 选择算法:
Tablet 选择示例:
# 伪代码:Tablet 选择算法
def select_tablet(row, table_schema):
# 1. 计算分区键,确定 Partition
partition_key = row[table_schema.partition_column]
partition = find_partition(partition_key)
# 2. 计算分桶键,确定 Bucket
bucket_key = row[table_schema.bucket_column]
bucket_hash = hash(bucket_key)
bucket_id = bucket_hash % table_schema.bucket_num
# 3. 确定 Tablet
tablet = partition.buckets[bucket_id]
# 4. 选择 Master Replica
master_replica = tablet.get_master_replica()
return master_replica.be_node
实际示例:
-- 表定义
CREATE TABLE orders (
order_id BIGINT,
order_date DATE,
user_id BIGINT,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03")
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 写入数据
INSERT INTO orders VALUES (1001, '2024-01-01', 12345, 99.99);
-- Tablet 选择过程:
-- 1. order_date = '2024-01-01' -> Partition p20240101
-- 2. user_id = 12345 -> hash(12345) % 32 = 17 -> Bucket 17
-- 3. 确定 Tablet: p20240101_bucket_17
-- 4. 选择 Master Replica 所在的 BE 节点
5. BE 数据处理
数据处理流程:
数据排序:
排序规则:
├── 按 Key 列顺序排序(前缀索引)
├── 相同 Key 按版本号排序
└── 排序算法:快速排序 + 归并排序
索引构建:
构建的索引:
├── 前缀索引(Short Key Index):每 1024 行一个索引项
├── ZoneMap 索引:记录每个 Page 的 Min/Max 值
├── BloomFilter 索引:为指定列构建 BloomFilter
├── 倒排索引:为全文检索列构建倒排索引
└── 向量索引:为向量列构建 HNSW 索引
6. 写入 Segment 文件
Segment 文件结构:
Segment 文件(.dat):
├── File Header
│ ├── Magic Number: 0x4F524953 ('DORIS')
│ ├── Version: 文件格式版本
│ └── Metadata Offset: 元数据偏移量
│
├── Column Data Blocks
│ ├── Column 1 Data
│ │ ├── Data Page 1 (压缩后)
│ │ ├── Data Page 2 (压缩后)
│ │ └── ...
│ ├── Column 2 Data
│ └── ...
│
├── Index Data
│ ├── Short Key Index
│ ├── ZoneMap Index
│ ├── BloomFilter Index
│ ├── Inverted Index
│ └── Vector Index
│
├── Metadata
│ ├── Column Schema
│ ├── Index Metadata
│ ├── Statistics (行数、大小、Min/Max)
│ └── Compression Info
│
└── File Footer
├── Metadata Offset
└── Checksum
写入过程:
// 伪代码:Segment 写入过程
class SegmentWriter {
void write_batch(RowBatch batch) {
// 1. 按列拆分数据
for (column in batch.columns) {
// 2. 压缩列数据
compressed_data = compress(column.data);
// 3. 写入 Data Page
write_data_page(compressed_data);
// 4. 更新 ZoneMap
update_zonemap(column.min, column.max);
// 5. 更新 BloomFilter
if (column.has_bloom_filter) {
update_bloom_filter(column.data);
}
}
// 6. 每 1024 行写入一个前缀索引项
if (row_count % 1024 == 0) {
write_short_key_index(batch.first_row);
}
}
void close() {
// 7. 写入索引数据
write_indexes();
// 8. 写入元数据
write_metadata();
// 9. 写入 Footer
write_footer();
// 10. Fsync 刷盘
fsync();
}
}
7. 副本同步
副本同步机制:
Quorum 机制:
副本数 = 3,Quorum = 2(多数)
写入成功条件:
├── Master Replica 写入成功
└── 至少 1 个 Follower Replica 写入成功
容错能力:
├── 可容忍 1 个副本故障
└── 保证数据不丢失
副本同步协议:
1. Master 写入本地成功
2. Master 向 Follower 发送数据
3. Follower 写入本地
4. Follower 返回 ACK
5. Master 收到 Quorum 个 ACK
6. Master 提交事务
7. Master 返回客户端成功
8. 元数据更新
版本号管理:
Tablet 版本链:
├── Version 1: [Rowset 1]
├── Version 2: [Rowset 1, Rowset 2]
├── Version 3: [Rowset 1, Rowset 2, Rowset 3]
└── ...
每次写入生成新版本:
├── 版本号递增
├── 记录 Rowset 信息
└── 更新 Tablet 元数据
元数据更新流程:
9. Compaction 触发
Compaction 触发条件:
| Compaction 类型 | 触发条件 | 示例 |
|---|---|---|
| Cumulative | Rowset 数量 > 5 | 5 个增量文件 -> 1 个文件 |
| Base | Cumulative Rowset 数量 > 10 | 10 个 Cumulative -> 1 个 Base |
| Vertical | 列数 > 100 | 按列分批合并 |
Compaction 调度:
Compaction Score 计算:
# 伪代码:Compaction Score 计算
def calculate_compaction_score(tablet):
# Cumulative Compaction Score
cumulative_score = tablet.cumulative_rowset_count / 5.0
# Base Compaction Score
base_score = tablet.base_rowset_count / 10.0
# 访问频率加权
access_weight = tablet.query_count / 1000.0
# 最终得分
score = (cumulative_score + base_score) * (1 + access_weight)
return score
Compaction 执行过程:
1. 选择要合并的 Rowset
├── Cumulative: 选择所有增量 Rowset
└── Base: 选择所有 Cumulative Rowset
2. 读取 Rowset 数据
├── 按 Key 列归并排序
├── 应用 Delete Predicate(删除标记)
└── 聚合相同 Key 的数据(Aggregate/Unique 模型)
3. 写入新 Rowset
├── 按列写入
├── 构建索引
└── 压缩数据
4. 更新元数据
├── 新增 Rowset 版本
├── 标记旧 Rowset 为删除
└── 更新 Tablet 版本号
5. 删除旧 Rowset
├── 等待所有查询完成
└── 物理删除文件
读取流程
Doris 的读取流程涉及查询解析、计划生成、数据扫描、结果聚合等多个环节。
读取流程全景图:
读取流程详细步骤:
1. SQL 解析
-- 示例查询
SELECT city, SUM(amount) AS total_amount
FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2024-01-02'
GROUP BY city
HAVING SUM(amount) > 10000
ORDER BY total_amount DESC
LIMIT 10;
解析过程:
1. 词法分析:将 SQL 拆分成 Token
├── SELECT, city, SUM, amount, ...
2. 语法分析:构建语法树(AST)
├── SelectStmt
│ ├── SelectList: [city, SUM(amount)]
│ ├── FromClause: orders
│ ├── WhereClause: order_date >= '2024-01-01' AND ...
│ ├── GroupByClause: [city]
│ ├── HavingClause: SUM(amount) > 10000
│ ├── OrderByClause: [total_amount DESC]
│ └── LimitClause: 10
3. 语义分析:类型检查、权限验证
├── 检查表是否存在
├── 检查列是否存在
├── 检查类型是否匹配
└── 检查用户权限
2. 查询优化
优化器执行的优化:
优化示例:
-- 原始查询
SELECT o.order_id, u.username, o.amount
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.order_date = '2024-01-01' AND u.city = 'Beijing';
-- 优化后的执行计划:
-- 1. 谓词下推:将 order_date 和 city 过滤下推到扫描层
-- 2. 列裁剪:只读取需要的列(order_id, user_id, username, amount)
-- 3. 分区裁剪:只扫描 p20240101 分区
-- 4. Join 重排序:小表(users)作为 Build 端
-- 5. Runtime Filter:从 users 表生成 user_id 的 BloomFilter,下推到 orders 表
3. 生成执行计划
执行计划示例:
PLAN FRAGMENT 0 (Coordinator)
OUTPUT EXPRS: city, sum(amount)
PARTITION: UNPARTITIONED
RESULT SINK
4:MERGING-EXCHANGE
limit: 10
order by: sum(amount) DESC
PLAN FRAGMENT 1 (BE)
PARTITION: HASH_PARTITIONED: city
STREAM DATA SINK
EXCHANGE ID: 04
UNPARTITIONED
3:TOP-N
order by: sum(amount) DESC
limit: 10
2:AGGREGATE (update finalize)
group by: city
aggregate: sum(amount)
1:EXCHANGE
PLAN FRAGMENT 2 (BE)
PARTITION: RANDOM
STREAM DATA SINK
EXCHANGE ID: 01
HASH_PARTITIONED: city
0:OlapScanNode
TABLE: orders
PARTITIONS: p20240101
PREDICATES: order_date >= '2024-01-01' AND order_date < '2024-01-02'
PROJECTIONS: city, amount
4. 数据扫描
扫描流程:
索引加速:
1. 前缀索引(Short Key Index)
├── 二分查找定位起始 Row
└── 跳过不符合条件的 Data Block
2. ZoneMap 索引
├── 检查 Min/Max 值
└── 跳过整个 Page
3. BloomFilter 索引
├── 快速判断值是否存在
└── 跳过不包含目标值的 Page
4. 倒排索引
├── 全文检索加速
└── 直接定位包含关键词的行
5. Runtime Filter
├── Join 时动态生成
└── 过滤大表数据
扫描优化:
// 伪代码:优化后的扫描过程
class OptimizedScanner {
void scan() {
// 1. 分区裁剪
partitions = prune_partitions(predicates);
for (partition in partitions) {
// 2. 选择 Tablet
tablets = partition.get_tablets();
for (tablet in tablets) {
// 3. 选择副本(负载均衡)
replica = select_replica(tablet);
// 4. 读取 Rowset(按版本)
rowsets = tablet.get_rowsets(version);
for (rowset in rowsets) {
// 5. 前缀索引定位
start_row = short_key_index.seek(predicates);
// 6. 读取 Data Page
for (page in rowset.pages[start_row:]) {
// 7. ZoneMap 过滤
if (!zonemap.match(predicates)) {
continue; // 跳过整个 Page
}
// 8. BloomFilter 过滤
if (!bloom_filter.match(predicates)) {
continue; // 跳过整个 Page
}
// 9. 读取和解压缩
data = decompress(page.data);
// 10. 列裁剪
data = project_columns(data, required_columns);
// 11. 谓词过滤
data = filter(data, predicates);
// 12. 返回数据
yield data;
}
}
}
}
}
}
5. 向量化执行
向量化处理:
// 传统执行(逐行处理)
for (int i = 0; i < row_count; i++) {
if (column_a[i] > 100 && column_b[i] < 200) {
result[i] = column_c[i] * 2;
}
}
// 向量化执行(批量处理)
// 1. 批量过滤
auto filter1 = column_a > 100; // SIMD 指令
auto filter2 = column_b < 200; // SIMD 指令
auto filter = filter1 && filter2;
// 2. 批量计算
auto result = column_c * 2; // SIMD 指令
// 3. 应用过滤
result = result[filter];
性能对比:
| 执行方式 | 处理速度 | CPU 利用率 | 缓存命中率 |
|---|---|---|---|
| 逐行执行 | 100 MB/s | 30% | 60% |
| 向量化执行 | 500-1000 MB/s | 80% | 90% |
6. 数据聚合
两阶段聚合:
聚合优化:
1. 预聚合(Aggregate 模型)
├── 数据写入时已聚合
└── 查询时无需再聚合
2. 本地聚合
├── 在 BE 节点本地聚合
└── 减少网络传输
3. 流式聚合
├── 边扫描边聚合
└── 减少内存占用
4. Hash 聚合
├── 使用 Hash Table
└── O(1) 查找和更新
7. 结果返回
结果集处理:
1. 排序(ORDER BY)
├── 本地排序:每个 BE 节点排序
└── 全局归并:FE 归并排序
2. 限制(LIMIT)
├── 提前终止:达到 LIMIT 后停止扫描
└── Top-N 优化:使用堆排序
3. 分页(OFFSET)
├── 跳过前 N 行
└── 返回后续数据
4. 结果缓存
├── 查询结果缓存
└── 相同查询直接返回缓存
性能优化总结:
| 优化技术 | 优化效果 | 适用场景 |
|---|---|---|
| 分区裁剪 | 减少扫描量 50-90% | 按时间查询 |
| 列裁剪 | 减少 I/O 50-80% | 查询少量列 |
| 索引加速 | 加速过滤 10-100x | 等值/范围查询 |
| 向量化执行 | 提升性能 3-10x | 所有查询 |
| Runtime Filter | 减少 Join 数据量 50-90% | 大表 Join |
| 物化视图 | 加速聚合查询 10-100x | 预聚合场景 |
数据模型与存储
数据模型核心术语
在深入了解 Doris 的数据模型之前,需要先理解一些核心术语和概念。这些术语描述了数据在 Doris 中的组织和存储方式。
逻辑层术语
Table(表):
Table 是数据的逻辑容器,包含:
├── Schema:列定义、数据类型、约束
├── Data Model:Duplicate/Aggregate/Unique
├── Partition:分区定义
├── Distribution:分桶策略
└── Properties:表属性配置
Partition(分区):
Partition 是表的第一级数据分片:
├── 作用:按范围或列表划分数据
├── 类型:Range 分区、List 分区
├── 优势:分区裁剪、独立管理、并行处理
└── 示例:按天分区、按地区分区
分区示例:
-- Range 分区(按时间)
CREATE TABLE orders (
order_id BIGINT,
order_date DATE,
amount DECIMAL(18, 2)
)
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03"),
PARTITION p20240103 VALUES LESS THAN ("2024-01-04")
);
-- 数据分布:
-- p20240101: order_date = '2024-01-01' 的数据
-- p20240102: order_date = '2024-01-02' 的数据
-- p20240103: order_date = '2024-01-03' 的数据
Bucket(分桶):
Bucket 是表的第二级数据分片:
├── 作用:将分区内的数据进一步分散
├── 方式:Hash 分桶、Random 分桶
├── 优势:并行处理、负载均衡
└── 数量:创建后不可修改
分桶示例:
-- Hash 分桶
CREATE TABLE user_orders (
user_id BIGINT,
order_id BIGINT,
amount DECIMAL(18, 2)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 数据分布:
-- Bucket 0: hash(user_id) % 32 = 0 的数据
-- Bucket 1: hash(user_id) % 32 = 1 的数据
-- ...
-- Bucket 31: hash(user_id) % 32 = 31 的数据
Tablet(数据分片):
Tablet 是 Doris 中最小的数据管理单元:
├── 定义:Partition + Bucket 的组合
├── 数量:Partition 数 × Bucket 数
├── 副本:每个 Tablet 有多个 Replica
├── 存储:分布在不同的 BE 节点
└── 大小:建议 1GB - 10GB
Tablet 层次结构:
Tablet 计算示例:
-- 表定义
CREATE TABLE sales_data (
date DATE,
product_id INT,
amount DECIMAL(18, 2)
)
PARTITION BY RANGE(date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03"),
PARTITION p20240103 VALUES LESS THAN ("2024-01-04")
)
DISTRIBUTED BY HASH(product_id) BUCKETS 16;
-- Tablet 数量计算:
-- Partition 数:3
-- Bucket 数:16
-- Tablet 总数:3 × 16 = 48
-- Tablet 列表:
-- p20240101_0, p20240101_1, ..., p20240101_15 (16 个)
-- p20240102_0, p20240102_1, ..., p20240102_15 (16 个)
-- p20240103_0, p20240103_1, ..., p20240103_15 (16 个)
Partition、Bucket、Tablet 关系详解:
核心概念澄清:
1. Partition 不是只在一个节点
├── 一个 Partition 会被分成多个 Bucket
├── 每个 Bucket 对应一个 Tablet
├── 这些 Tablet 会分布在多个 BE 节点上
└── 所以一个 Partition 的数据分布在多个节点
2. Bucket 和 Tablet 的关系
├── 无分区表:Bucket 和 Tablet 一一对应
│ └── 例如:BUCKETS 32 → 32 个 Tablet
├── 有分区表:每个 Partition 都有自己的 Bucket 集合
│ └── 例如:3 个 Partition × 32 Buckets = 96 个 Tablet
└── Tablet = Partition + Bucket 的组合
3. 建表时的 BUCKETS 配置
├── DISTRIBUTED BY HASH(column) BUCKETS 32
├── 这个 32 是指每个 Partition 有 32 个 Bucket
├── 不是整个表只有 32 个 Bucket
└── 每增加一个 Partition,就会新增 32 个 Tablet
图解 Partition、Bucket、Tablet 关系:
详细示例说明:
-- 示例 1:无分区表
CREATE TABLE no_partition_table (
id BIGINT,
name VARCHAR(100)
)
DISTRIBUTED BY HASH(id) BUCKETS 32;
-- 数据分布:
-- Bucket 数:32
-- Partition 数:1(默认分区)
-- Tablet 总数:1 × 32 = 32
-- 结论:Bucket 和 Tablet 一一对应
-- Tablet 分布在多个 BE 节点:
-- BE1: Tablet_0, Tablet_8, Tablet_16, Tablet_24
-- BE2: Tablet_1, Tablet_9, Tablet_17, Tablet_25
-- BE3: Tablet_2, Tablet_10, Tablet_18, Tablet_26
-- BE4: Tablet_3, Tablet_11, Tablet_19, Tablet_27
-- ...
-- 示例 2:有分区表
CREATE TABLE partition_table (
date DATE,
user_id BIGINT,
amount DECIMAL(18, 2)
)
PARTITION BY RANGE(date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03"),
PARTITION p20240103 VALUES LESS THAN ("2024-01-04")
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 数据分布:
-- Bucket 数:32(每个 Partition 都有 32 个 Bucket)
-- Partition 数:3
-- Tablet 总数:3 × 32 = 96
-- 详细分布:
-- Partition p20240101:
-- ├── Bucket 0 -> Tablet p20240101_0
-- ├── Bucket 1 -> Tablet p20240101_1
-- ├── ...
-- └── Bucket 31 -> Tablet p20240101_31
--
-- Partition p20240102:
-- ├── Bucket 0 -> Tablet p20240102_0
-- ├── Bucket 1 -> Tablet p20240102_1
-- ├── ...
-- └── Bucket 31 -> Tablet p20240102_31
--
-- Partition p20240103:
-- ├── Bucket 0 -> Tablet p20240103_0
-- ├── Bucket 1 -> Tablet p20240103_1
-- ├── ...
-- └── Bucket 31 -> Tablet p20240103_31
-- Tablet 分布在多个 BE 节点(假设 4 个 BE):
-- BE1: p20240101_0, p20240101_4, ..., p20240102_1, p20240102_5, ...
-- BE2: p20240101_1, p20240101_5, ..., p20240102_2, p20240102_6, ...
-- BE3: p20240101_2, p20240101_6, ..., p20240102_3, p20240102_7, ...
-- BE4: p20240101_3, p20240101_7, ..., p20240102_0, p20240102_4, ...
数据写入流程示例:
-- 写入一条数据
INSERT INTO partition_table VALUES ('2024-01-01', 12345, 99.99);
-- 数据路由过程:
-- 1. 根据 date = '2024-01-01' 确定 Partition
-- └── 命中 Partition p20240101
--
-- 2. 根据 user_id = 12345 计算 Bucket
-- ├── hash(12345) = 1234567890
-- ├── 1234567890 % 32 = 18
-- └── 确定 Bucket 18
--
-- 3. 确定 Tablet
-- └── Tablet = p20240101_18
--
-- 4. 选择 Tablet 的 Master Replica 所在的 BE 节点
-- └── 假设 p20240101_18 的 Master Replica 在 BE2
--
-- 5. 数据写入 BE2 节点
-- └── 路径:/data/doris/be/storage/data/0/10018/
--
-- 6. 同步到 Follower Replica
-- ├── BE3: Follower Replica 1
-- └── BE4: Follower Replica 2
查询数据流程示例:
-- 查询数据
SELECT * FROM partition_table
WHERE date = '2024-01-01' AND user_id = 12345;
-- 查询路由过程:
-- 1. 分区裁剪:根据 date = '2024-01-01'
-- └── 只扫描 Partition p20240101(跳过其他 Partition)
--
-- 2. Bucket 裁剪:根据 user_id = 12345
-- ├── hash(12345) % 32 = 18
-- └── 只扫描 Bucket 18(跳过其他 31 个 Bucket)
--
-- 3. 确定 Tablet
-- └── 只扫描 Tablet p20240101_18
--
-- 4. 选择副本
-- └── 选择负载最低的副本(可能是 BE2、BE3 或 BE4)
--
-- 5. 扫描数据
-- └── 只扫描 1 个 Tablet,而不是全部 96 个 Tablet
--
-- 性能提升:
-- ├── 无分区裁剪:扫描 96 个 Tablet
-- ├── 有分区裁剪:扫描 32 个 Tablet(减少 67%)
-- └── 有分区 + Bucket 裁剪:扫描 1 个 Tablet(减少 99%)
关键要点总结:
| 概念 | 说明 | 示例 |
|---|---|---|
| Partition 分布 | 一个 Partition 的数据分布在多个 BE 节点 | p20240101 的 32 个 Tablet 分布在 4 个 BE |
| Bucket 配置 | BUCKETS 32 表示每个 Partition 有 32 个 Bucket | 3 个 Partition × 32 Buckets = 96 个 Tablet |
| Bucket 和 Tablet | 每个 Partition 内,Bucket 和 Tablet 一一对应 | p20240101_0 = Partition p20240101 + Bucket 0 |
| Tablet 命名 | Tablet 名称 = Partition 名称 + Bucket 编号 | p20240101_18 表示 p20240101 分区的第 18 个 Bucket |
| 副本分布 | 每个 Tablet 有多个副本分布在不同 BE | Tablet p20240101_18 的 3 个副本在 BE2、BE3、BE4 |
| 数据路由 | 先根据分区键确定 Partition,再根据分桶键确定 Bucket | date 确定 Partition,user_id 确定 Bucket |
Replica(副本):
Replica 是 Tablet 的物理副本:
├── 数量:由 replication_num 配置(默认 3)
├── 角色:Master Replica、Follower Replica
├── 分布:分布在不同的 BE 节点
├── 同步:Master 写入后同步到 Follower
└── 容错:可容忍 (副本数 - 1) / 2 个节点故障
副本示例:
-- 创建 3 副本表
CREATE TABLE important_data (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32
PROPERTIES (
"replication_num" = "3" -- 3 个副本
);
-- 副本分布:
-- Tablet 1:
-- ├── Replica 1 (Master): BE1
-- ├── Replica 2 (Follower): BE2
-- └── Replica 3 (Follower): BE3
--
-- 容错能力:可容忍 1 个 BE 节点故障
物理层术语
Rowset(行集):
Rowset 是一批数据的集合:
├── 定义:一次导入或 Compaction 生成的数据
├── 版本:每个 Rowset 有唯一版本号
├── 类型:Base Rowset、Delta Rowset
├── 文件:包含多个 Segment 文件
└── 元数据:记录行数、大小、版本等信息
Rowset 类型:
| 类型 | 说明 | 生成方式 | 特点 |
|---|---|---|---|
| Base Rowset | 基线数据 | Base Compaction | 数据量大,文件少 |
| Delta Rowset | 增量数据 | 数据导入 | 数据量小,文件多 |
| Cumulative Rowset | 累积数据 | Cumulative Compaction | 介于两者之间 |
Rowset 版本链:
Base Rowset] --> B[Version 2
+ Delta Rowset 1] B --> C[Version 3
+ Delta Rowset 2] C --> D[Version 4
+ Delta Rowset 3] D --> E[Compaction] E --> F[Version 5
New Base Rowset] style A fill:#4A90E2 style D fill:#FFD700 style F fill:#7ED321
Segment(段文件):
Segment 是实际存储数据的文件:
├── 格式:列式存储,每列独立
├── 大小:默认 256MB
├── 压缩:支持 LZ4、ZSTD、ZLIB 等
├── 索引:包含前缀索引、ZoneMap、BloomFilter 等
└── 文件:.dat 数据文件 + .idx 索引文件
Segment 文件结构:
Segment 文件(.dat):
├── File Header (文件头)
│ ├── Magic Number: 0x4F524953 ('DORIS')
│ ├── Version: 文件格式版本号
│ ├── Schema Hash: Schema 的 Hash 值
│ └── Num Rows: 总行数
│
├── Column Data (列数据)
│ ├── Column 1
│ │ ├── Data Page 1 (4KB-64KB,压缩后)
│ │ ├── Data Page 2
│ │ └── ...
│ ├── Column 2
│ │ ├── Data Page 1
│ │ └── ...
│ └── ...
│
├── Index Data (索引数据)
│ ├── Short Key Index (前缀索引)
│ │ ├── Index Entry 1: (Key, Offset)
│ │ ├── Index Entry 2: (Key, Offset)
│ │ └── ...
│ ├── ZoneMap Index (区间索引)
│ │ ├── Page 1: (Min, Max, Null Count)
│ │ ├── Page 2: (Min, Max, Null Count)
│ │ └── ...
│ ├── BloomFilter Index (布隆过滤器)
│ │ ├── Column 1 BloomFilter
│ │ ├── Column 2 BloomFilter
│ │ └── ...
│ ├── Inverted Index (倒排索引)
│ │ ├── Term 1 -> [Doc IDs]
│ │ ├── Term 2 -> [Doc IDs]
│ │ └── ...
│ └── Vector Index (向量索引)
│ ├── HNSW Graph
│ └── Vector Data
│
├── Metadata (元数据)
│ ├── Column Schema (列定义)
│ ├── Index Metadata (索引元数据)
│ ├── Statistics (统计信息)
│ │ ├── Row Count: 行数
│ │ ├── Data Size: 数据大小
│ │ ├── Min/Max Values: 最小最大值
│ │ └── Null Count: 空值数量
│ └── Compression Info (压缩信息)
│
└── File Footer (文件尾)
├── Metadata Offset: 元数据偏移量
├── Index Offset: 索引偏移量
└── Checksum: 校验和
Segment 示例:
# Segment 文件示例
/data/doris/be/storage/data/0/10001/
├── 20001_0.dat # Segment 数据文件(256MB)
├── 20001_0.idx # Segment 索引文件(10MB)
├── 20002_0.dat # 另一个 Segment
└── 20002_0.idx
# 文件命名规则:
# {rowset_id}_{segment_id}.dat
# 20001: Rowset ID
# 0: Segment ID(一个 Rowset 可能有多个 Segment)
Page(数据页):
Page 是 Segment 内部的数据单元:
├── 大小:4KB - 64KB(默认 64KB)
├── 压缩:每个 Page 独立压缩
├── 类型:Data Page、Index Page
├── 读取:按需读取,减少 I/O
└── 缓存:Page Cache 缓存热数据
Page 结构:
Data Page:
├── Page Header
│ ├── Page Type: DATA_PAGE
│ ├── Uncompressed Size: 解压后大小
│ ├── Compressed Size: 压缩后大小
│ ├── Num Rows: 行数
│ └── Encoding: 编码方式
│
├── Page Data (压缩后的列数据)
│ ├── Encoded Data
│ └── Compressed Data
│
└── Page Footer
└── Checksum: 校验和
Key 列与 Value 列:
Key 列:
├── 定义:DUPLICATE KEY / AGGREGATE KEY / UNIQUE KEY 指定的列
├── 作用:数据排序、前缀索引、数据去重/聚合
├── 顺序:影响查询性能
└── 限制:前 36 字节建立稀疏索引
Value 列:
├── 定义:非 Key 列
├── 作用:存储实际数据
├── 聚合:Aggregate 模型中可指定聚合函数
└── 更新:Unique 模型中可更新
Key 列示例:
-- Duplicate 模型
CREATE TABLE log_table (
user_id BIGINT, -- Key 列
event_time DATETIME, -- Key 列
event_type VARCHAR(50),-- Value 列
page_url VARCHAR(500) -- Value 列
)
DUPLICATE KEY(user_id, event_time);
-- Aggregate 模型
CREATE TABLE sales_table (
date DATE, -- Key 列
city VARCHAR(50), -- Key 列
product_id INT, -- Key 列
sales_amount DECIMAL(18, 2) SUM, -- Value 列(聚合)
order_count INT SUM -- Value 列(聚合)
)
AGGREGATE KEY(date, city, product_id);
-- Unique 模型
CREATE TABLE user_table (
user_id BIGINT, -- Key 列(主键)
username VARCHAR(100), -- Value 列
age INT, -- Value 列
city VARCHAR(50) -- Value 列
)
UNIQUE KEY(user_id);
存储层术语
Storage Path(存储路径):
Storage Path 是 BE 节点的数据存储目录:
├── 配置:be.conf 中的 storage_root_path
├── 多路径:支持配置多个存储路径
├── 介质:SSD、HDD、对象存储
├── 容量:每个路径有容量限制
└── 均衡:数据自动均衡到各路径
存储路径配置:
# be.conf 配置
storage_root_path=/data1/doris,medium:SSD;/data2/doris,medium:HDD
# 存储目录结构:
/data1/doris/
├── data/ # 数据目录
│ ├── 0/ # 路径 ID
│ │ ├── 10001/ # Tablet ID
│ │ ├── 10002/
│ │ └── ...
│ └── 1/
├── meta/ # 元数据目录
└── snapshot/ # 快照目录
Compaction(数据合并):
Compaction 是后台数据合并过程:
├── 目的:合并小文件、删除过期数据、优化查询性能
├── 类型:Base Compaction、Cumulative Compaction
├── 触发:自动触发或手动触发
├── 调度:根据 Compaction Score 优先级调度
└── 限流:避免影响查询性能
Compaction 类型对比:
| 类型 | 合并范围 | 触发条件 | 执行频率 | 资源消耗 |
|---|---|---|---|---|
| Cumulative | 增量 Rowset | Rowset 数 > 5 | 高(分钟级) | 中等 |
| Base | 所有 Cumulative | Cumulative 数 > 10 | 低(小时级) | 高 |
| Vertical | 按列合并 | 列数 > 100 | 按需 | 低(内存) |
Version(版本):
Version 是 Tablet 的数据版本:
├── 定义:每次写入或 Compaction 生成新版本
├── 格式:[start_version, end_version]
├── 递增:版本号单调递增
├── 可见性:查询读取指定版本的数据
└── 清理:旧版本在 Compaction 后删除
版本链示例:
Tablet 版本演进:
├── Version [0-1]: 初始数据(Base Rowset)
├── Version [2-2]: 第 1 次导入(Delta Rowset)
├── Version [3-3]: 第 2 次导入(Delta Rowset)
├── Version [4-4]: 第 3 次导入(Delta Rowset)
├── Version [0-4]: Compaction 合并(New Base Rowset)
└── Version [5-5]: 第 4 次导入(Delta Rowset)
查询时选择版本:
├── 读已提交:读取最新版本 [0-5]
├── 快照读:读取查询开始时的版本 [0-4]
└── 时间旅行:读取指定版本 [0-3]
术语关系图
完整的数据组织层次:
术语总结表:
| 术语 | 层次 | 说明 | 数量关系 |
|---|---|---|---|
| Database | 逻辑层 | 数据库 | 1 个集群多个 Database |
| Table | 逻辑层 | 表 | 1 个 Database 多个 Table |
| Partition | 逻辑层 | 分区 | 1 个 Table 多个 Partition |
| Bucket | 逻辑层 | 分桶 | 1 个 Partition 多个 Bucket |
| Tablet | 逻辑层 | 数据分片 | Partition × Bucket |
| Replica | 物理层 | 副本 | 1 个 Tablet 多个 Replica |
| Rowset | 物理层 | 行集 | 1 个 Replica 多个 Rowset |
| Segment | 物理层 | 段文件 | 1 个 Rowset 多个 Segment |
| Page | 物理层 | 数据页 | 1 个 Segment 多个 Page |
实际示例:
-- 创建表
CREATE TABLE user_behavior (
user_id BIGINT,
event_time DATETIME,
event_type VARCHAR(50)
)
DUPLICATE KEY(user_id, event_time)
PARTITION BY RANGE(event_time) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03")
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES ("replication_num" = "3");
-- 数据组织:
-- Database: default_cluster:db_name
-- └── Table: user_behavior
-- ├── Partition: p20240101
-- │ ├── Bucket 0 -> Tablet 10001
-- │ │ ├── Replica 1 (BE1)
-- │ │ │ ├── Rowset 1 (Version 0-1)
-- │ │ │ │ └── Segment 1 (256MB)
-- │ │ │ │ ├── Column: user_id (Pages)
-- │ │ │ │ ├── Column: event_time (Pages)
-- │ │ │ │ ├── Column: event_type (Pages)
-- │ │ │ │ └── Indexes
-- │ │ │ └── Rowset 2 (Version 2-2)
-- │ │ ├── Replica 2 (BE2)
-- │ │ └── Replica 3 (BE3)
-- │ ├── Bucket 1 -> Tablet 10002
-- │ └── ...
-- └── Partition: p20240102
-- └── ...
数据模型类型
Doris 支持三种数据模型,适用于不同的业务场景。数据模型的选择直接影响数据的存储方式、查询性能和更新机制。
数据模型对比:
| 数据模型 | 适用场景 | 更新方式 | 查询性能 | 存储开销 | 数据去重 |
|---|---|---|---|---|---|
| Duplicate | 明细数据、日志分析 | 仅追加 | 最快 | 最大 | 不去重 |
| Aggregate | 预聚合、指标汇总 | 聚合合并 | 快 | 小 | 按 Key 聚合 |
| Unique | 维度表、实时更新 | 主键更新 | 中等 | 中等 | 按主键去重 |
1. Duplicate 模型(明细模型)
核心特点:
- 不做任何聚合:保留所有明细数据,即使 Key 列相同也不会合并
- 仅追加写入:数据只能追加,不能更新或删除
- 查询性能最优:无需合并数据,直接读取
- 存储开销最大:保留所有原始数据
数据存储原理:
建表示例:
-- 用户行为日志表
CREATE TABLE user_behavior_log (
user_id BIGINT,
event_time DATETIME,
event_type VARCHAR(50),
page_url VARCHAR(500),
device_type VARCHAR(20),
session_id VARCHAR(100)
)
DUPLICATE KEY(user_id, event_time)
COMMENT "用户行为日志表"
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"compression" = "LZ4"
);
Key 列选择原则:
- 高频过滤列在前:如 user_id、event_time
- 前缀索引优化:前 36 字节建立稀疏索引
- 不影响数据去重:Key 列仅用于排序和索引
使用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 日志分析 | 保留所有日志明细 | 访问日志、操作日志、审计日志 |
| 事件流 | 记录所有事件 | 用户行为、系统事件、传感器数据 |
| 原始数据 | 作为数据源 | ODS 层原始数据 |
| 明细查询 | 需要查询每条记录 | 订单明细、交易明细 |
性能特点:
写入性能:★★★★★(最快,无需合并)
查询性能:★★★★★(最快,无需实时合并)
存储效率:★★☆☆☆(最低,保留所有数据)
更新支持:☆☆☆☆☆(不支持更新和删除)
2. Aggregate 模型(聚合模型)
核心特点:
- 自动聚合:相同 Key 的数据自动按聚合函数合并
- 预聚合:写入时即完成聚合,查询时无需再聚合
- 节省存储:相同 Key 只保留聚合结果
- 支持多种聚合函数:SUM、MAX、MIN、REPLACE、HLL_UNION、BITMAP_UNION
数据存储原理:
建表示例:
-- 销售汇总表
CREATE TABLE sales_summary (
date DATE,
city VARCHAR(50),
product_id INT,
sales_amount DECIMAL(18, 2) SUM, -- 销售额求和
order_count INT SUM, -- 订单数求和
max_price DECIMAL(18, 2) MAX, -- 最高价格
min_price DECIMAL(18, 2) MIN, -- 最低价格
last_update_time DATETIME REPLACE -- 最后更新时间(保留最新)
)
AGGREGATE KEY(date, city, product_id)
COMMENT "销售汇总表"
DISTRIBUTED BY HASH(product_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3"
);
聚合函数详解:
| 聚合函数 | 说明 | 聚合逻辑 | 适用场景 | 示例 |
|---|---|---|---|---|
| SUM | 求和 | 累加所有值 | 金额、数量统计 | 销售额、订单数 |
| MAX | 最大值 | 保留最大值 | 峰值统计 | 最高价格、最大库存 |
| MIN | 最小值 | 保留最小值 | 最小值统计 | 最低价格、最小库存 |
| REPLACE | 替换 | 保留最新值 | 状态更新 | 订单状态、用户状态 |
| REPLACE_IF_NOT_NULL | 非空替换 | 非空时替换 | 可选字段更新 | 备注、描述 |
| HLL_UNION | HyperLogLog 去重 | 基数估算 | UV 统计 | 独立访客数 |
| BITMAP_UNION | Bitmap 去重 | 精确去重 | 用户去重 | 活跃用户数 |
聚合过程示例:
-- 原始数据
INSERT INTO sales_summary VALUES
('2024-01-01', 'Beijing', 1001, 100.00, 1, 100.00, 100.00, '2024-01-01 10:00:00'),
('2024-01-01', 'Beijing', 1001, 200.00, 1, 200.00, 200.00, '2024-01-01 11:00:00'),
('2024-01-01', 'Beijing', 1001, 150.00, 1, 150.00, 150.00, '2024-01-01 12:00:00');
-- 存储结果(自动聚合)
-- date | city | product_id | sales_amount | order_count | max_price | min_price | last_update_time
-- 2024-01-01 | Beijing | 1001 | 450.00 | 3 | 200.00 | 100.00 | 2024-01-01 12:00:00
Key 列与 Value 列:
CREATE TABLE example_agg (
-- Key 列(聚合键)
date DATE,
city VARCHAR(50),
product_id INT,
-- Value 列(聚合值)
sales_amount DECIMAL(18, 2) SUM,
order_count INT SUM
)
AGGREGATE KEY(date, city, product_id); -- 前 3 列为 Key 列
使用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 实时大屏 | 预聚合指标展示 | 销售额、订单数、UV/PV |
| 报表系统 | 按维度聚合 | 日报、周报、月报 |
| 指标监控 | 累计值统计 | 累计销售额、累计用户数 |
| 数据仓库 | DWS 层汇总表 | 各维度汇总宽表 |
性能特点:
写入性能:★★★☆☆(需要合并,略慢)
查询性能:★★★★☆(预聚合,查询快)
存储效率:★★★★☆(高,相同 Key 合并)
更新支持:★★★☆☆(支持聚合更新)
注意事项:
- Key 列顺序很重要:影响聚合效率和查询性能
- 聚合函数不可更改:建表后无法修改聚合函数
- REPLACE 语义:保留最新写入的值(按导入批次)
- HLL/BITMAP 精度:HLL 有误差(约 1%),BITMAP 精确但占用空间大
3. Unique 模型(主键模型)
核心特点:
- 主键唯一性:相同主键的数据只保留一条
- 支持 Upsert:INSERT 时如果主键存在则更新
- 支持删除:可以删除指定主键的数据
- 两种实现方式:Merge-on-Read(MoR)和 Merge-on-Write(MoW)
数据存储原理:
建表示例:
-- 用户信息表(Merge-on-Write)
CREATE TABLE user_profile (
user_id BIGINT,
username VARCHAR(100),
age INT,
city VARCHAR(50),
register_time DATETIME,
last_login_time DATETIME,
status TINYINT
)
UNIQUE KEY(user_id)
COMMENT "用户信息表"
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true" -- 开启 MoW
);
Merge-on-Read vs Merge-on-Write 详解:
Merge-on-Read(MoR,默认模式):
特点:
- 写入快:直接追加,无需合并
- 查询慢:需要实时合并多个版本
- 存储开销大:保留所有历史版本(Compaction 前)
- 适用场景:写多读少
Merge-on-Write(MoW):
特点:
- 写入慢:需要查找和合并
- 查询快:直接读取最新数据
- 存储开销小:只保留最新版本
- 适用场景:读多写少
性能对比:
| 指标 | Merge-on-Read | Merge-on-Write |
|---|---|---|
| 写入延迟 | 低(< 10ms) | 中等(10-50ms) |
| 写入吞吐 | 高 | 中等 |
| 查询延迟 | 高(需合并) | 低 |
| 查询吞吐 | 低 | 高 |
| 存储空间 | 大 | 小 |
| Compaction 压力 | 大 | 小 |
Upsert 操作示例:
-- 插入数据
INSERT INTO user_profile VALUES
(1, 'Alice', 25, 'Beijing', '2024-01-01 10:00:00', '2024-01-01 10:00:00', 1);
-- 更新数据(相同主键)
INSERT INTO user_profile VALUES
(1, 'Alice', 26, 'Shanghai', '2024-01-01 10:00:00', '2024-01-02 10:00:00', 1);
-- 查询结果(只保留最新数据)
SELECT * FROM user_profile WHERE user_id = 1;
-- user_id | username | age | city | register_time | last_login_time | status
-- 1 | Alice | 26 | Shanghai | 2024-01-01 10:00:00 | 2024-01-02 10:00:00 | 1
删除操作:
-- 删除指定主键的数据
DELETE FROM user_profile WHERE user_id = 1;
-- 批量删除
DELETE FROM user_profile WHERE status = 0;
部分列更新:
-- 只更新部分列(需要开启部分列更新)
ALTER TABLE user_profile SET ("enable_unique_key_partial_update" = "true");
-- 只更新 age 和 last_login_time
INSERT INTO user_profile (user_id, age, last_login_time) VALUES
(1, 27, '2024-01-03 10:00:00');
使用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 维度表 | 缓慢变化维度 | 用户信息、商品信息、地区信息 |
| CDC 同步 | 数据库实时同步 | MySQL CDC 到 Doris |
| 状态表 | 实时状态更新 | 订单状态、设备状态 |
| 去重场景 | 保证数据唯一性 | 用户注册、订单创建 |
性能特点:
写入性能:★★★☆☆(MoR 快,MoW 慢)
查询性能:★★★★☆(MoW 快,MoR 慢)
存储效率:★★★☆☆(中等)
更新支持:★★★★★(完全支持 Upsert 和 Delete)
选择建议:
-- 读多写少场景(推荐 MoW)
CREATE TABLE user_dim (
user_id BIGINT,
...
)
UNIQUE KEY(user_id)
PROPERTIES (
"enable_unique_key_merge_on_write" = "true" -- MoW
);
-- 写多读少场景(推荐 MoR)
CREATE TABLE order_status (
order_id BIGINT,
...
)
UNIQUE KEY(order_id)
PROPERTIES (
"enable_unique_key_merge_on_write" = "false" -- MoR(默认)
);
数据模型选择决策树
选择建议总结:
| 需求 | 推荐模型 | 配置 |
|---|---|---|
| 保留所有明细 | Duplicate | 默认配置 |
| 预聚合统计 | Aggregate | 选择合适的聚合函数 |
| 实时更新(读多) | Unique + MoW | enable_unique_key_merge_on_write=true |
| 实时更新(写多) | Unique + MoR | enable_unique_key_merge_on_write=false |
| CDC 同步 | Unique + MoW | 开启 MoW + 部分列更新 |
| 日志分析 | Duplicate | 按时间分区 |
| 实时大屏 | Aggregate | 预聚合关键指标 |
常见错误:
-- ❌ 错误 1:日志表使用 Unique 模型
CREATE TABLE access_log (
log_id BIGINT,
...
)
UNIQUE KEY(log_id); -- 日志不需要去重,浪费性能
-- ✅ 正确:使用 Duplicate 模型
CREATE TABLE access_log (
log_id BIGINT,
...
)
DUPLICATE KEY(log_id);
-- ❌ 错误 2:维度表使用 Duplicate 模型
CREATE TABLE user_dim (
user_id BIGINT,
...
)
DUPLICATE KEY(user_id); -- 会产生重复数据
-- ✅ 正确:使用 Unique 模型
CREATE TABLE user_dim (
user_id BIGINT,
...
)
UNIQUE KEY(user_id)
PROPERTIES ("enable_unique_key_merge_on_write" = "true");
-- ❌ 错误 3:Aggregate 模型用于明细查询
CREATE TABLE order_detail (
order_id BIGINT,
amount DECIMAL(18, 2) SUM
)
AGGREGATE KEY(order_id); -- 无法查询每笔订单明细
-- ✅ 正确:使用 Duplicate 模型
CREATE TABLE order_detail (
order_id BIGINT,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id);
分区与分桶
数据分片策略:
Doris 使用 两级分片:Partition(分区) + Bucket(分桶)。
1. Partition(分区)
分区类型:
| 分区类型 | 说明 | 示例 |
|---|---|---|
| Range 分区 | 按范围分区(常用于时间) | 按天、按月分区 |
| List 分区 | 按枚举值分区 | 按地区、按类型分区 |
Range 分区示例:
CREATE TABLE sales_data (
order_id BIGINT,
order_date DATE,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03"),
PARTITION p20240103 VALUES LESS THAN ("2024-01-04")
)
DISTRIBUTED BY HASH(order_id) BUCKETS 16;
动态分区:
-- 自动创建和删除分区
ALTER TABLE sales_data SET (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7", -- 保留最近 7 天
"dynamic_partition.end" = "3", -- 提前创建未来 3 天
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "16"
);
分区裁剪优化:
-- 查询时自动裁剪分区
SELECT * FROM sales_data
WHERE order_date >= '2024-01-01' AND order_date < '2024-01-03';
-- 只扫描 p20240101 和 p20240102 两个分区
2. Bucket(分桶)
分桶策略:
| 分桶方式 | 说明 | 适用场景 |
|---|---|---|
| Hash 分桶 | 按 Hash 值分桶 | 数据均匀分布 |
| Random 分桶 | 随机分桶 | 无明显分桶键 |
写入分布与裁剪机制补充:
- 写入路径:一条数据进入 Doris 后,会先对
DISTRIBUTED BY HASH指定的分桶键做 Hash 计算,再映射到具体 Bucket;在同一个 Partition 内,Bucket 与 Tablet 是一一对应关系,最终 Tablet 的多个副本会落到不同 BE 节点。 - 排序边界:Doris 的“有序”主要体现在 Tablet 内部按 Key 组织,并通过后台 Compaction 逐步逼近全局有序,而不是插入瞬间就完成全表强排序。
- 查询剪裁顺序:线上排查扫描量时,要先看 Partition 剪裁,再看 Bucket 剪裁,最后再看 Tablet 内部是否命中了前缀索引、ZoneMap、Bloom Filter 等存储层过滤能力。
| 查询条件类型 | 是否可裁剪 Bucket | 说明 |
|---|---|---|
| 分区键范围过滤 | 先裁剪 Partition | 先决定扫描哪些分区 |
| 分桶键等值 / IN | 可以 | 可精确定位到少量 Bucket |
| 分桶键范围过滤 | 通常不可以 | Hash 打散后无法按范围映射到连续 Bucket |
| 非分桶键过滤 | 不可以 | 需要扫描全部 Bucket,再依赖 Tablet 内索引过滤 |
线上设计经验:
- Bucket 数量不仅影响并行度,也直接影响 Rowset 生成速率和 Compaction 压力。
- 单个 Tablet 建议控制在 1GB 到 10GB,经验上以 5GB 左右 最容易在查询并行度与后台维护成本之间取得平衡。
- 经验公式可写成:
Bucket 数 ≈ max(单分区数据量 / 目标 Tablet 大小, BE 节点数)。 - 经验上限通常控制在 BE 节点数的 1 到 3 倍起步,极端场景尽量不要超过 BE 节点数 × 10;如果分区很多,还要同时控制单表总 Tablet 数,避免元数据和调度开销失控。
资料来源:融合自 raw 文档《Doris 数据插入与排序》,原始整理链接见 原文链接。
分桶数量选择:
-- 分桶数量建议
-- 1. 单个 Tablet 大小控制在 1GB - 10GB
-- 2. 分桶数 = 数据量 / 单 Tablet 大小
-- 3. 分桶数建议为 BE 节点数的倍数
-- 示例:100GB 数据,期望单 Tablet 5GB
-- 分桶数 = 100GB / 5GB = 20
CREATE TABLE large_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 20; -- 20 个分桶
Colocate Join(本地 Join):
将关联表的数据分布到相同的 BE 节点,避免数据 Shuffle。
-- 创建 Colocate Group
CREATE TABLE fact_table (
user_id BIGINT,
amount DECIMAL(18, 2)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"colocate_with" = "user_group"
);
CREATE TABLE dim_table (
user_id BIGINT,
username VARCHAR(100)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"colocate_with" = "user_group" -- 与 fact_table 在同一 Group
);
-- Join 时无需 Shuffle
SELECT f.user_id, d.username, f.amount
FROM fact_table f
JOIN dim_table d ON f.user_id = d.user_id;
列式存储引擎
列式存储优势:
列式存储优势:
| 优势 | 说明 |
|---|---|
| 列裁剪 | 只读取需要的列,减少 I/O |
| 高压缩比 | 同列数据类型相同,压缩效果好 |
| 向量化执行 | 批量处理同列数据,SIMD 加速 |
| 索引友好 | 每列独立索引,加速过滤 |
Segment 文件结构:
Segment 文件 (.dat)
├── Header(文件头)
│ ├── Magic Number
│ ├── Version
│ └── Metadata
├── Column Data(列数据)
│ ├── Column 1 Data Block
│ ├── Column 2 Data Block
│ └── Column N Data Block
├── Index Data(索引数据)
│ ├── Short Key Index(前缀索引)
│ ├── ZoneMap Index(区间索引)
│ ├── BloomFilter Index(布隆过滤器)
│ ├── Inverted Index(倒排索引)
│ └── Vector Index(向量索引)
└── Footer(文件尾)
├── Index Offset
└── Checksum
数据编码:
Doris 支持多种编码方式,自动选择最优编码:
| 编码方式 | 适用场景 | 压缩比 |
|---|---|---|
| Plain | 无规律数据 | 1x |
| RLE(Run Length Encoding) | 重复值多 | 5-10x |
| Dictionary | 低基数字符串 | 3-5x |
| Delta | 递增数值 | 2-3x |
| Bit Packing | 小范围整数 | 2-4x |
数据压缩
压缩算法对比:
| 压缩算法 | 压缩比 | 压缩速度 | 解压速度 | 适用场景 |
|---|---|---|---|---|
| LZ4 | 2-3x | 很快 | 很快 | 默认推荐,平衡性能和压缩比 |
| ZSTD | 3-5x | 快 | 快 | 存储成本敏感场景 |
| Snappy | 2x | 很快 | 很快 | 实时性要求高 |
| ZLIB | 4-6x | 慢 | 中等 | 冷数据归档 |
压缩配置:
-- 建表时指定压缩算法
CREATE TABLE compressed_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 16
PROPERTIES (
"compression" = "ZSTD" -- 使用 ZSTD 压缩
);
-- 修改压缩算法
ALTER TABLE compressed_table SET ("compression" = "LZ4");
压缩效果示例:
原始数据大小:1TB
├── LZ4 压缩后:400GB(压缩比 2.5x)
├── ZSTD 压缩后:250GB(压缩比 4x)
└── ZLIB 压缩后:200GB(压缩比 5x)
查询性能对比(相对 LZ4):
├── LZ4:1.0x(基准)
├── ZSTD:0.95x(略慢 5%)
└── ZLIB:0.7x(慢 30%)
最佳实践:
- 默认使用 LZ4:性能和压缩比平衡最好
- 存储敏感用 ZSTD:压缩比高,性能损失小
- 冷数据用 ZLIB:最高压缩比,查询频率低
- 实时场景用 Snappy:压缩解压最快
建表注意事项
在 Doris 中建表是一个需要仔细规划的过程,合理的表结构设计直接影响查询性能、存储成本和运维复杂度。以下是建表时需要重点关注的注意事项。
1. 数据模型选择
选择原则:
| 场景 | 推荐模型 | 理由 |
|---|---|---|
| 日志、事件流 | Duplicate | 保留所有明细,查询最快 |
| 实时指标汇总 | Aggregate | 自动聚合,节省存储 |
| 维度表、CDC | Unique | 支持主键更新 |
| 用户画像 | Aggregate + REPLACE_IF_NOT_NULL | 支持部分字段更新 |
常见错误:
-- ❌ 错误:日志表使用 Unique 模型
CREATE TABLE access_log (
log_id BIGINT,
user_id BIGINT,
access_time DATETIME
)
UNIQUE KEY(log_id) -- 日志不需要去重,浪费性能
DISTRIBUTED BY HASH(log_id) BUCKETS 32;
-- ✅ 正确:使用 Duplicate 模型
CREATE TABLE access_log (
log_id BIGINT,
user_id BIGINT,
access_time DATETIME
)
DUPLICATE KEY(log_id, user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
2. Key 列顺序设计
前缀索引规则:
- Doris 对 Key 列的前 36 字节建立稀疏索引
- Key 列顺序直接影响查询性能
- 高频过滤列应放在前面
顺序优化示例:
-- ❌ 错误:低频列在前
CREATE TABLE sales_data (
product_id INT, -- 低频过滤
region VARCHAR(50), -- 低频过滤
date DATE, -- 高频过滤
amount DECIMAL(18, 2)
)
DUPLICATE KEY(product_id, region, date)
DISTRIBUTED BY HASH(product_id) BUCKETS 16;
-- ✅ 正确:高频列在前
CREATE TABLE sales_data (
date DATE, -- 高频过滤(按天查询)
region VARCHAR(50), -- 中频过滤(按地区查询)
product_id INT, -- 低频过滤
amount DECIMAL(18, 2)
)
DUPLICATE KEY(date, region, product_id)
DISTRIBUTED BY HASH(date) BUCKETS 16;
36 字节限制示例:
-- Key 列字节计算
CREATE TABLE user_table (
user_id BIGINT, -- 8 字节
username VARCHAR(20), -- 20 字节
email VARCHAR(50), -- 50 字节(超出 36 字节)
age INT -- 4 字节
)
DUPLICATE KEY(user_id, username, email)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 前缀索引只覆盖:user_id (8) + username (20) + email 的前 8 字节
-- email 的后 42 字节不在索引中,过滤效果差
优化建议:
- 高基数列在前:如 user_id、order_id
- 时间列优先:如 date、datetime(按时间范围查询常见)
- 控制字符串长度:避免超长字符串占用索引空间
3. 分区设计
分区策略选择:
| 场景 | 分区方式 | 示例 |
|---|---|---|
| 时序数据 | Range 分区(按天/月) | 日志、订单、事件流 |
| 地域数据 | List 分区(按地区) | 多租户、多地域业务 |
| 小表 | 不分区 | < 1GB 的维度表 |
动态分区配置:
-- ✅ 推荐:自动管理分区
CREATE TABLE order_data (
order_id BIGINT,
order_date DATE,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(order_date) () -- 空分区列表
DISTRIBUTED BY HASH(order_id) BUCKETS 16
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.buckets" = "16",
"dynamic_partition.create_history_partition" = "true"
);
分区粒度选择:
| 数据量/天 | 推荐粒度 | 理由 |
|---|---|---|
| < 1GB | 按月分区 | 避免分区过多 |
| 1GB - 10GB | 按天分区 | 平衡查询和管理 |
| > 10GB | 按天分区 + 多分桶 | 单分区不宜过大 |
常见错误:
-- ❌ 错误:分区过细(按小时分区)
PARTITION BY RANGE(order_time) (
PARTITION p2024010100 VALUES LESS THAN ("2024-01-01 01:00:00"),
PARTITION p2024010101 VALUES LESS THAN ("2024-01-01 02:00:00"),
... -- 每天 24 个分区,管理复杂
)
-- ✅ 正确:按天分区
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03")
)
4. 分桶设计
分桶数量计算:
分桶数 = 数据量 / 期望单 Tablet 大小
期望单 Tablet 大小:1GB - 10GB(推荐 5GB)
分桶数量示例:
| 数据量 | 单 Tablet 大小 | 分桶数 | 说明 |
|---|---|---|---|
| 10GB | 5GB | 2 | 小表,分桶少 |
| 100GB | 5GB | 20 | 中等表 |
| 1TB | 5GB | 200 | 大表 |
| 10TB | 5GB | 2000 | 超大表 |
分桶键选择:
-- ❌ 错误:低基数列作为分桶键
CREATE TABLE user_orders (
user_id BIGINT,
gender VARCHAR(10), -- 只有 2 个值(男/女)
order_id BIGINT
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(gender) BUCKETS 32; -- 数据严重倾斜
-- ✅ 正确:高基数列作为分桶键
CREATE TABLE user_orders (
user_id BIGINT,
gender VARCHAR(10),
order_id BIGINT
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32; -- 数据均匀分布
分桶数量调整:
-- 注意:分桶数创建后无法修改,只能重建表
-- 建议:初期预留一定余量
-- 当前数据量 100GB,预期增长到 500GB
-- 分桶数 = 500GB / 5GB = 100
CREATE TABLE growth_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 100; -- 预留增长空间
5. 副本数设置
副本数选择:
| 场景 | 副本数 | 理由 |
|---|---|---|
| 生产环境 | 3 | 高可用,容忍 2 个节点故障 |
| 测试环境 | 1 | 节省资源 |
| 临时表 | 1 | 短期使用,无需高可用 |
| 核心业务 | 3 | 数据安全优先 |
-- 生产环境推荐配置
CREATE TABLE production_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32
PROPERTIES (
"replication_num" = "3", -- 3 副本
"replication_allocation" = "tag.location.default: 3"
);
6. 字段类型详解
Apache Doris 支持标准 SQL 语法,使用 MySQL 网络连接协议,在数据类型支持上尽可能与 MySQL 保持一致。合理选择字段类型对存储效率、查询性能和数据精度都有重要影响。
6.1 数值类型
整数类型对比:
| 类型 | 存储空间 | 有符号范围 | 无符号范围 | 使用场景 |
|---|---|---|---|---|
| BOOLEAN | 1字节 | 0 或 1 | - | 布尔标志(是/否、真/假) |
| TINYINT | 1字节 | -128 ~ 127 | 0 ~ 255 | 状态码、枚举值、年龄 |
| SMALLINT | 2字节 | -32,768 ~ 32,767 | 0 ~ 65,535 | 小范围计数、端口号 |
| INT | 4字节 | -2,147,483,648 ~ 2,147,483,647 | 0 ~ 4,294,967,295 | 常规ID、计数器 |
| BIGINT | 8字节 | -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807 | 0 ~ 18,446,744,073,709,551,615 | 用户ID、订单ID、时间戳 |
| LARGEINT | 16字节 | -2^127 + 1 ~ 2^127 - 1 | - | 超大整数、高精度计算 |
整数类型使用示例:
CREATE TABLE user_behavior (
user_id BIGINT, -- 用户ID,范围大
age TINYINT, -- 年龄,0-120 足够
login_count INT, -- 登录次数
is_vip BOOLEAN, -- 是否VIP,0/1
status TINYINT, -- 状态码:0-待审核 1-正常 2-冻结
total_amount LARGEINT -- 累计金额(分),超大数值
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
浮点与定点类型:
| 类型 | 存储空间 | 精度 | 范围 | 使用场景 |
|---|---|---|---|---|
| FLOAT | 4字节 | 单精度 | ±3.4 × 10^38 | 科学计算、近似值(不推荐金融) |
| DOUBLE | 8字节 | 双精度 | ±1.79 × 10^308 | 高精度科学计算、坐标 |
| DECIMAL(P,S) | 4/8/16/32字节 | 精确 | 根据精度决定 | 金融计算、货币金额(强烈推荐) |
DECIMAL 精度与存储:
-- DECIMAL(P, S):P=总位数,S=小数位数
-- 存储空间根据精度自动选择:
-- P ≤ 9:4字节
-- 9 < P ≤ 18:8字节
-- 18 < P ≤ 38:16字节
-- 38 < P ≤ 76:32字节(需开启 enable_decimal256=true)
CREATE TABLE financial_data (
order_id BIGINT,
price DECIMAL(18, 2), -- 价格:最多16位整数+2位小数
discount DECIMAL(5, 4), -- 折扣:0.0000 ~ 9.9999
total_amount DECIMAL(20, 2), -- 总金额:支持更大范围
tax_rate DECIMAL(6, 4), -- 税率:0.0000 ~ 99.9999
latitude DOUBLE, -- 纬度:高精度浮点
longitude DOUBLE -- 经度:高精度浮点
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
数值类型选择建议:
- 金融场景:必须使用 DECIMAL,避免浮点误差
- ID字段:优先 BIGINT,避免溢出
- 枚举/状态:使用 TINYINT,节省空间
- 计数器:根据范围选择 INT 或 BIGINT
- 科学计算:DOUBLE 适合高精度,FLOAT 适合性能优先
6.2 日期时间类型
日期时间类型对比:
| 类型 | 存储空间 | 格式 | 范围 | 精度 | 使用场景 |
|---|---|---|---|---|---|
| DATE | 4字节 | YYYY-MM-DD | 0000-01-01 ~ 9999-12-31 | 天 | 生日、日期统计 |
| DATETIME | 8字节 | YYYY-MM-DD HH:mm:ss[.ffffff] | 0000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 微秒 | 订单时间、日志时间 |
日期时间使用示例:
CREATE TABLE order_log (
order_id BIGINT,
user_id BIGINT,
order_date DATE, -- 订单日期(按天统计)
create_time DATETIME, -- 创建时间(精确到秒)
update_time DATETIME(6), -- 更新时间(精确到微秒)
payment_time DATETIME -- 支付时间
)
DUPLICATE KEY(order_id, user_id)
PARTITION BY RANGE(order_date) (
PARTITION p202401 VALUES LESS THAN ("2024-02-01"),
PARTITION p202402 VALUES LESS THAN ("2024-03-01")
)
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
日期时间函数示例:
-- 日期计算
SELECT
order_date,
DATE_ADD(order_date, INTERVAL 7 DAY) AS delivery_date,
DATEDIFF(NOW(), order_date) AS days_ago,
DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') AS formatted_time
FROM order_log;
-- 时间戳转换
SELECT
FROM_UNIXTIME(1609459200) AS datetime_value,
UNIX_TIMESTAMP('2024-01-01 00:00:00') AS timestamp_value;
日期类型选择建议:
- 分区字段:优先使用 DATE,便于按天分区
- 精确时间:使用 DATETIME,支持到微秒
- 时间戳存储:可用 BIGINT 存储 Unix 时间戳,查询时转换
- 避免字符串:不要用 VARCHAR 存储日期,无法使用日期函数
6.3 字符串类型
字符串类型对比:
| 类型 | 存储空间 | 长度范围 | 特点 | 使用场景 |
|---|---|---|---|---|
| CHAR(M) | M字节(固定) | 1 ~ 255 字符 | 固定长度,空间浪费 | 固定长度编码(如国家代码) |
| VARCHAR(M) | 变长 | 1 ~ 65,533 字符 | 变长存储,节省空间 | 常规字符串(推荐) |
| STRING | 变长 | 默认1MB,最大2GB | 超长文本 | 长文本、JSON、日志 |
| TEXT | 变长 | 同 STRING | STRING 的别名 | 文章内容、描述 |
字符串编码说明:
- UTF-8 编码:英文1字节,中文3字节
- VARCHAR(100):最多存储 100 个字符(不是字节)
- 实际存储空间:根据实际内容动态分配
字符串使用示例:
CREATE TABLE product_info (
product_id BIGINT,
product_code CHAR(10), -- 固定长度产品编码
product_name VARCHAR(200), -- 产品名称
short_desc VARCHAR(500), -- 简短描述
detail_desc STRING, -- 详细描述(可能很长)
tags VARCHAR(1000), -- 标签列表
json_data STRING, -- JSON 数据
country_code CHAR(2) -- 国家代码:CN, US
)
DUPLICATE KEY(product_id)
DISTRIBUTED BY HASH(product_id) BUCKETS 32;
字符串长度设置原则:
-- ✅ 正确:根据实际需求设置合理长度
CREATE TABLE user_profile (
username VARCHAR(50), -- 用户名:通常不超过50字符
email VARCHAR(100), -- 邮箱:标准长度
phone VARCHAR(20), -- 手机号:国际格式
address VARCHAR(500), -- 地址:中等长度
bio STRING -- 个人简介:可能很长
)
DUPLICATE KEY(username)
DISTRIBUTED BY HASH(username) BUCKETS 32;
-- ❌ 错误:长度设置不合理
CREATE TABLE bad_example (
username VARCHAR(10000), -- 过长,浪费空间
phone VARCHAR(5), -- 过短,无法存储完整号码
email STRING -- 不必要的超长类型
)
DUPLICATE KEY(username)
DISTRIBUTED BY HASH(username) BUCKETS 32;
字符串类型选择建议:
- 常规字符串:优先 VARCHAR,指定合理长度
- 固定长度:CHAR 适合编码、状态码
- 超长文本:使用 STRING(如文章、日志)
- JSON 数据:使用 STRING 或 JSON 类型
- 避免过长:VARCHAR 长度不要设置过大,影响性能
6.4 复杂类型
复杂类型对比:
| 类型 | 存储空间 | 结构 | 使用限制 | 使用场景 |
|---|---|---|---|---|
| ARRAY | 变长 | 数组 | 仅 Value 列,Duplicate/Unique 模型 | 标签列表、多值字段 |
| MAP<K,V> | 变长 | 键值对 | 仅 Value 列,Duplicate/Unique 模型 | 属性映射、配置项 |
| STRUCT<F1:T1, F2:T2> | 变长 | 结构体 | 仅 Value 列,Duplicate 模型 | 嵌套对象、复合字段 |
| JSON | 变长 | JSON对象 | 仅 Value 列 | 半结构化数据 |
| VARIANT | 变长 | 动态类型 | 仅 Value 列 | 动态 JSON、灵活字段 |
ARRAY 类型示例:
CREATE TABLE user_tags (
user_id BIGINT,
tags ARRAY<VARCHAR(50)>, -- 标签数组
scores ARRAY<INT>, -- 分数数组
login_times ARRAY<DATETIME> -- 登录时间数组
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 查询数组
SELECT
user_id,
tags,
ARRAY_SIZE(tags) AS tag_count,
tags[1] AS first_tag, -- 数组下标从1开始
ARRAY_CONTAINS(tags, '活跃用户') AS is_active
FROM user_tags
WHERE ARRAY_SIZE(tags) > 0;
-- 展开数组
SELECT
user_id,
tag
FROM user_tags
LATERAL VIEW EXPLODE(tags) tmp AS tag;
MAP 类型示例:
CREATE TABLE product_attributes (
product_id BIGINT,
attributes MAP<VARCHAR(50), VARCHAR(200)>, -- 属性映射
prices MAP<VARCHAR(20), DECIMAL(10,2)> -- 不同地区价格
)
DUPLICATE KEY(product_id)
DISTRIBUTED BY HASH(product_id) BUCKETS 32;
-- 查询 MAP
SELECT
product_id,
attributes,
attributes['brand'] AS brand,
attributes['color'] AS color,
MAP_SIZE(attributes) AS attr_count,
MAP_KEYS(attributes) AS all_keys,
MAP_VALUES(attributes) AS all_values
FROM product_attributes;
STRUCT 类型示例:
CREATE TABLE order_detail (
order_id BIGINT,
shipping_info STRUCT<
address: VARCHAR(500),
city: VARCHAR(50),
province: VARCHAR(50),
zipcode: VARCHAR(20),
phone: VARCHAR(20)
>,
receiver STRUCT<
name: VARCHAR(50),
phone: VARCHAR(20)
>
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- 查询 STRUCT
SELECT
order_id,
shipping_info.address,
shipping_info.city,
receiver.name
FROM order_detail;
JSON 类型示例:
CREATE TABLE event_log (
event_id BIGINT,
event_time DATETIME,
event_data JSON -- JSON 数据
)
DUPLICATE KEY(event_id)
DISTRIBUTED BY HASH(event_id) BUCKETS 32;
-- 查询 JSON
SELECT
event_id,
JSON_EXTRACT(event_data, '$.user_id') AS user_id,
JSON_EXTRACT(event_data, '$.action') AS action,
JSON_EXTRACT(event_data, '$.properties.page') AS page
FROM event_log
WHERE JSON_EXTRACT(event_data, '$.action') = 'click';
复杂类型选择建议:
- 多值字段:ARRAY 适合标签、列表
- 键值对:MAP 适合属性、配置
- 嵌套对象:STRUCT 适合固定结构
- 灵活数据:JSON/VARIANT 适合半结构化
- 性能考虑:复杂类型查询性能低于基础类型,谨慎使用
6.5 聚合类型
聚合类型对比:
| 类型 | 存储空间 | 功能 | 误差率 | 使用场景 |
|---|---|---|---|---|
| BITMAP | 变长 | 精确去重 | 0% | 用户去重、UV 计算 |
| HLL | 变长 | 近似去重 | 1-2% | 大数据集去重、基数估算 |
| QUANTILE_STATE | 变长 | 分位数计算 | 近似 | 百分位数、中位数 |
| AGG_STATE | 变长 | 通用聚合 | 取决于函数 | 自定义聚合逻辑 |
BITMAP 类型示例:
-- 创建 BITMAP 聚合表
CREATE TABLE user_visit_bitmap (
visit_date DATE,
page_id INT,
user_ids BITMAP BITMAP_UNION -- BITMAP 类型,聚合方式为 BITMAP_UNION
)
AGGREGATE KEY(visit_date, page_id)
DISTRIBUTED BY HASH(page_id) BUCKETS 32;
-- 导入数据
INSERT INTO user_visit_bitmap
SELECT
visit_date,
page_id,
BITMAP_HASH(user_id) AS user_ids
FROM user_visit_log
GROUP BY visit_date, page_id;
-- 查询 UV
SELECT
visit_date,
page_id,
BITMAP_UNION_COUNT(user_ids) AS uv
FROM user_visit_bitmap
GROUP BY visit_date, page_id;
-- 交集、并集计算
SELECT
BITMAP_INTERSECT_COUNT(user_ids) AS common_users,
BITMAP_UNION_COUNT(user_ids) AS total_users
FROM user_visit_bitmap
WHERE visit_date BETWEEN '2024-01-01' AND '2024-01-07';
HLL 类型示例:
-- 创建 HLL 聚合表
CREATE TABLE page_uv_hll (
visit_date DATE,
page_id INT,
user_hll HLL HLL_UNION -- HLL 类型,聚合方式为 HLL_UNION
)
AGGREGATE KEY(visit_date, page_id)
DISTRIBUTED BY HASH(page_id) BUCKETS 32;
-- 导入数据
INSERT INTO page_uv_hll
SELECT
visit_date,
page_id,
HLL_HASH(user_id) AS user_hll
FROM user_visit_log
GROUP BY visit_date, page_id;
-- 查询近似 UV
SELECT
visit_date,
SUM(HLL_UNION_AGG(user_hll)) AS total_uv
FROM page_uv_hll
GROUP BY visit_date;
QUANTILE_STATE 类型示例:
-- 创建分位数聚合表
CREATE TABLE response_time_quantile (
service_name VARCHAR(100),
log_date DATE,
response_time_state QUANTILE_STATE QUANTILE_UNION
)
AGGREGATE KEY(service_name, log_date)
DISTRIBUTED BY HASH(service_name) BUCKETS 32;
-- 导入数据
INSERT INTO response_time_quantile
SELECT
service_name,
log_date,
TO_QUANTILE_STATE(response_time, 2048) AS response_time_state
FROM service_log
GROUP BY service_name, log_date;
-- 查询 P50、P95、P99
SELECT
service_name,
log_date,
QUANTILE_PERCENT(QUANTILE_UNION(response_time_state), 0.5) AS p50,
QUANTILE_PERCENT(QUANTILE_UNION(response_time_state), 0.95) AS p95,
QUANTILE_PERCENT(QUANTILE_UNION(response_time_state), 0.99) AS p99
FROM response_time_quantile
GROUP BY service_name, log_date;
聚合类型选择建议:
- 精确去重:BITMAP(数据量适中)
- 大数据去重:HLL(千万级以上,容忍误差)
- 分位数计算:QUANTILE_STATE(性能监控)
- BITMAP vs HLL:BITMAP 精确但占用空间大,HLL 近似但节省空间
6.6 IP 类型
IP 类型对比:
| 类型 | 存储空间 | 格式 | 使用场景 |
|---|---|---|---|
| IPv4 | 4字节 | 点分十进制 | IPv4 地址存储与查询 |
| IPv6 | 16字节 | 冒号分隔十六进制 | IPv6 地址存储与查询 |
IP 类型示例:
CREATE TABLE access_log (
log_id BIGINT,
client_ipv4 IPv4, -- IPv4 地址
client_ipv6 IPv6, -- IPv6 地址
access_time DATETIME
)
DUPLICATE KEY(log_id)
DISTRIBUTED BY HASH(log_id) BUCKETS 32;
-- IP 函数使用
SELECT
client_ipv4,
IPv4NumToString(client_ipv4) AS ip_string,
IPv4StringToNum('192.168.1.1') AS ip_num,
IPv4CIDRToRange(IPv4StringToNum('192.168.1.0'), 24) AS ip_range
FROM access_log;
6.7 类型转换规则
隐式类型转换:
-- 数值类型自动提升
SELECT
TINYINT_col + INT_col, -- 结果为 INT
INT_col + BIGINT_col, -- 结果为 BIGINT
FLOAT_col + DOUBLE_col, -- 结果为 DOUBLE
INT_col + DECIMAL_col; -- 结果为 DECIMAL
-- 字符串与数值转换
SELECT
'123' + 456, -- 字符串转数值:579
CONCAT(123, '456'); -- 数值转字符串:'123456'
显式类型转换:
-- CAST 函数
SELECT
CAST('123' AS INT), -- 字符串转整数
CAST(123.45 AS INT), -- 浮点转整数:123
CAST(123 AS VARCHAR), -- 整数转字符串
CAST('2024-01-01' AS DATE), -- 字符串转日期
CAST(NOW() AS DATE); -- 时间戳转日期
-- 类型转换注意事项
SELECT
CAST('abc' AS INT), -- 转换失败返回 NULL
CAST(9999999999999999999 AS INT); -- 溢出返回 NULL
类型转换最佳实践:
- 避免隐式转换:显式 CAST 提高可读性
- 注意精度损失:DOUBLE → INT 会截断小数
- 检查溢出:大数值转小类型可能溢出
- 日期转换:使用日期函数而非字符串拼接
7. 数据类型选择
类型选择原则:
| 数据类型 | 推荐使用 | 避免使用 | 理由 |
|---|---|---|---|
| 整数 | INT, BIGINT | VARCHAR | 节省存储,查询更快 |
| 小数 | DECIMAL | DOUBLE | 精度保证 |
| 日期 | DATE, DATETIME | VARCHAR | 支持日期函数 |
| 布尔 | TINYINT (0/1) | VARCHAR (’true’/‘false’) | 节省存储 |
| 枚举 | TINYINT + 映射表 | VARCHAR | 节省存储,加速过滤 |
常见错误:
-- ❌ 错误:滥用 VARCHAR
CREATE TABLE bad_table (
user_id VARCHAR(50), -- 应该用 BIGINT
age VARCHAR(10), -- 应该用 INT
is_active VARCHAR(10), -- 应该用 TINYINT
create_time VARCHAR(50) -- 应该用 DATETIME
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- ✅ 正确:使用合适的类型
CREATE TABLE good_table (
user_id BIGINT,
age INT,
is_active TINYINT, -- 0/1
create_time DATETIME
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
VARCHAR 长度设置:
-- 根据实际数据长度设置,避免浪费
CREATE TABLE user_info (
username VARCHAR(50), -- 用户名最长 50 字符
email VARCHAR(100), -- 邮箱最长 100 字符
address VARCHAR(500), -- 地址最长 500 字符
description TEXT -- 超长文本用 TEXT
)
DUPLICATE KEY(username)
DISTRIBUTED BY HASH(username) BUCKETS 32;
7. 索引设计
索引创建时机:
-- 建表时创建索引
CREATE TABLE product_table (
product_id BIGINT,
product_name VARCHAR(200),
description TEXT,
category VARCHAR(50),
price DECIMAL(18, 2),
INDEX idx_name (product_name) USING INVERTED, -- 倒排索引
INDEX idx_desc (description) USING INVERTED, -- 全文检索
INDEX idx_category (category) USING BITMAP -- Bitmap 索引
)
DUPLICATE KEY(product_id)
DISTRIBUTED BY HASH(product_id) BUCKETS 32;
索引选择建议:
| 列特征 | 推荐索引 | 示例 |
|---|---|---|
| 低基数列 | Bitmap 索引 | 性别、状态、类型 |
| 字符串模糊查询 | 倒排索引 | 商品名称、标题 |
| 长文本检索 | 倒排索引 + 分词 | 文章内容、评论 |
| 向量数据 | HNSW 向量索引 | Embedding 向量 |
| 高基数列 | BloomFilter(自动) | user_id、order_id |
8. 表属性配置
常用表属性:
CREATE TABLE optimized_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32
PROPERTIES (
-- 副本配置
"replication_num" = "3",
-- 压缩配置
"compression" = "LZ4",
-- 存储介质(冷热分层)
"storage_medium" = "SSD",
"storage_cooldown_time" = "2024-12-31 23:59:59", -- 冷却时间
-- Bloom Filter
"bloom_filter_columns" = "id", -- 为 id 列创建 BloomFilter
-- Unique Key 模式
"enable_unique_key_merge_on_write" = "true",
-- 数据导入
"in_memory" = "false", -- 是否常驻内存
-- Compaction 配置
"compaction_policy" = "time_series" -- 时序数据优化
);
9. Colocate Join 配置
适用场景:
- 大表 Join 大表
- Join 键与分桶键相同
- 两表分桶数相同
-- 事实表
CREATE TABLE fact_orders (
user_id BIGINT,
order_id BIGINT,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(user_id, order_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"colocate_with" = "user_group",
"replication_num" = "3"
);
-- 维度表
CREATE TABLE dim_users (
user_id BIGINT,
username VARCHAR(100),
city VARCHAR(50)
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32 -- 必须相同
PROPERTIES (
"colocate_with" = "user_group", -- 必须相同
"replication_num" = "3"
);
注意事项:
- 分桶键必须相同:两表的 DISTRIBUTED BY 列必须一致
- 分桶数必须相同:BUCKETS 数量必须一致
- 副本数必须相同:replication_num 必须一致
- 不要滥用:只对高频 Join 的表使用
10. 建表检查清单
建表前检查:
- 数据模型:是否选择了合适的模型(Duplicate/Aggregate/Unique)
- Key 列顺序:高频过滤列是否在前,是否超过 36 字节
- 分区设计:是否需要分区,分区粒度是否合理
- 分桶数量:是否根据数据量计算,是否预留增长空间
- 分桶键:是否选择高基数列,是否会导致数据倾斜
- 副本数:生产环境是否设置为 3
- 数据类型:是否使用了合适的类型,VARCHAR 长度是否合理
- 索引设计:是否为高频查询列创建索引
- 压缩算法:是否选择了合适的压缩算法
- Colocate Join:是否需要配置,配置是否正确
建表后验证:
-- 1. 查看表结构
SHOW CREATE TABLE table_name;
-- 2. 查看分区信息
SHOW PARTITIONS FROM table_name;
-- 3. 查看 Tablet 分布
SHOW TABLETS FROM table_name;
-- 4. 查看表统计信息
SHOW DATA FROM table_name;
-- 5. 查看索引信息
SHOW INDEX FROM table_name;
性能测试:
-- 导入测试数据
INSERT INTO table_name VALUES (...);
-- 执行典型查询,查看执行计划
EXPLAIN SELECT * FROM table_name WHERE ...;
-- 查看查询性能
SELECT * FROM table_name WHERE ... LIMIT 100;
DDL 与 DML 常用语法
Doris 支持标准 SQL 语法,同时提供了一些扩展语法用于数据定义(DDL)和数据操作(DML)。
DDL(数据定义语言)
1. 数据库操作
创建数据库:
-- 基础创建
CREATE DATABASE db_name;
-- 指定字符集
CREATE DATABASE db_name
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_general_ci;
-- 如果不存在则创建
CREATE DATABASE IF NOT EXISTS db_name;
-- 添加注释
CREATE DATABASE db_name COMMENT '业务数据库';
查看数据库:
-- 查看所有数据库
SHOW DATABASES;
-- 查看数据库创建语句
SHOW CREATE DATABASE db_name;
-- 使用数据库
USE db_name;
修改数据库:
-- 修改数据库属性
ALTER DATABASE db_name SET DATA QUOTA 1024GB;
-- 修改副本数配额
ALTER DATABASE db_name SET REPLICA QUOTA 1024;
删除数据库:
-- 删除数据库
DROP DATABASE db_name;
-- 如果存在则删除
DROP DATABASE IF EXISTS db_name;
-- 强制删除(即使有表)
DROP DATABASE db_name FORCE;
2. 表操作
创建表完整语法:
CREATE TABLE [IF NOT EXISTS] [database.]table_name
(
column_name1 column_type1 [COMMENT 'column comment'],
column_name2 column_type2 [aggregation_type] [COMMENT 'column comment'],
...
)
[DUPLICATE KEY(column_list) | AGGREGATE KEY(column_list) | UNIQUE KEY(column_list)]
[COMMENT 'table comment']
[PARTITION BY RANGE(column_name) (
PARTITION partition_name VALUES LESS THAN ("value"),
...
)]
DISTRIBUTED BY HASH(column_name) BUCKETS bucket_num
[PROPERTIES (
"key" = "value",
...
)];
DISTRIBUTED BY 语法详解:
-- Hash 分桶(最常用)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 多列 Hash 分桶
DISTRIBUTED BY HASH(user_id, order_id) BUCKETS 32;
-- Random 分桶(随机分布)
DISTRIBUTED BY RANDOM BUCKETS 32;
DISTRIBUTED BY 参数说明:
| 参数 | 说明 | 示例 | 推荐值 |
|---|---|---|---|
| HASH(column) | 按列 Hash 分桶 | HASH(user_id) | 高基数列 |
| HASH(col1, col2) | 多列组合 Hash | HASH(date, user_id) | 组合唯一性高 |
| RANDOM | 随机分桶 | RANDOM | 无明显分桶键 |
| BUCKETS num | 分桶数量 | BUCKETS 32 | 2的幂次,16-128 |
分桶数量计算:
-- 公式:分桶数 = 数据量 / 期望单 Tablet 大小
-- 期望单 Tablet 大小:1GB - 10GB(推荐 5GB)
-- 示例 1:100GB 数据,期望单 Tablet 5GB
-- 分桶数 = 100GB / 5GB = 20
DISTRIBUTED BY HASH(user_id) BUCKETS 20;
-- 示例 2:1TB 数据,期望单 Tablet 5GB
-- 分桶数 = 1000GB / 5GB = 200
DISTRIBUTED BY HASH(user_id) BUCKETS 200;
-- 示例 3:小表(< 10GB)
DISTRIBUTED BY HASH(id) BUCKETS 8;
PARTITION BY 语法详解:
-- Range 分区(按范围)
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02"),
PARTITION p20240102 VALUES LESS THAN ("2024-01-03"),
PARTITION p20240103 VALUES LESS THAN ("2024-01-04")
)
-- Range 分区(MAXVALUE)
PARTITION BY RANGE(order_date) (
PARTITION p202401 VALUES LESS THAN ("2024-02-01"),
PARTITION p202402 VALUES LESS THAN ("2024-03-01"),
PARTITION p_max VALUES LESS THAN MAXVALUE
)
-- List 分区(按枚举值)
PARTITION BY LIST(city) (
PARTITION p_beijing VALUES IN ("Beijing", "Tianjin"),
PARTITION p_shanghai VALUES IN ("Shanghai", "Hangzhou"),
PARTITION p_guangzhou VALUES IN ("Guangzhou", "Shenzhen")
)
PROPERTIES 常用属性:
CREATE TABLE example_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32
PROPERTIES (
-- 副本数
"replication_num" = "3",
-- 副本分配策略
"replication_allocation" = "tag.location.default: 3",
-- 压缩算法
"compression" = "LZ4", -- LZ4/ZSTD/ZLIB/SNAPPY
-- 存储介质
"storage_medium" = "SSD", -- SSD/HDD
-- 冷却时间(数据从 SSD 迁移到 HDD 的时间)
"storage_cooldown_time" = "2024-12-31 23:59:59",
-- Bloom Filter 列
"bloom_filter_columns" = "id,user_id",
-- Unique Key 模式
"enable_unique_key_merge_on_write" = "true",
-- 部分列更新
"enable_unique_key_partial_update" = "true",
-- 动态分区
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "32",
-- Colocate Join
"colocate_with" = "group_name",
-- 数据导入
"in_memory" = "false",
-- Compaction 策略
"compaction_policy" = "time_series" -- time_series/size_based
);
PROPERTIES 属性详解:
| 属性 | 说明 | 可选值 | 默认值 | 推荐场景 |
|---|---|---|---|---|
| replication_num | 副本数 | 1-N | 3 | 生产环境用 3 |
| compression | 压缩算法 | LZ4/ZSTD/ZLIB/SNAPPY | LZ4 | 默认 LZ4 |
| storage_medium | 存储介质 | SSD/HDD | HDD | 热数据用 SSD |
| bloom_filter_columns | BloomFilter 列 | 列名列表 | 空 | 高基数等值查询 |
| enable_unique_key_merge_on_write | MoW 模式 | true/false | false | 读多写少用 true |
| colocate_with | Colocate 组 | 组名 | 空 | 大表 Join 优化 |
查看表:
-- 查看所有表
SHOW TABLES;
-- 查看表结构
DESC table_name;
DESCRIBE table_name;
-- 查看建表语句
SHOW CREATE TABLE table_name;
-- 查看表详细信息
SHOW TABLE STATUS LIKE 'table_name';
-- 查看分区信息
SHOW PARTITIONS FROM table_name;
-- 查看 Tablet 信息
SHOW TABLETS FROM table_name;
修改表:
-- 添加列
ALTER TABLE table_name ADD COLUMN new_column INT COMMENT '新列';
-- 添加列(指定位置)
ALTER TABLE table_name ADD COLUMN new_column INT AFTER existing_column;
ALTER TABLE table_name ADD COLUMN new_column INT FIRST;
-- 删除列
ALTER TABLE table_name DROP COLUMN column_name;
-- 修改列类型
ALTER TABLE table_name MODIFY COLUMN column_name BIGINT;
-- 修改列名
ALTER TABLE table_name RENAME COLUMN old_name new_name;
-- 修改列顺序
ALTER TABLE table_name ORDER BY (col1, col2, col3);
-- 修改表属性
ALTER TABLE table_name SET ("replication_num" = "2");
-- 修改表注释
ALTER TABLE table_name MODIFY COMMENT "新的表注释";
分区操作:
-- 添加分区
ALTER TABLE table_name ADD PARTITION p20240104 VALUES LESS THAN ("2024-01-05");
-- 添加多个分区
ALTER TABLE table_name ADD
PARTITION p20240104 VALUES LESS THAN ("2024-01-05"),
PARTITION p20240105 VALUES LESS THAN ("2024-01-06");
-- 删除分区
ALTER TABLE table_name DROP PARTITION p20240101;
-- 删除多个分区
ALTER TABLE table_name DROP PARTITION p20240101, p20240102;
-- 清空分区数据
TRUNCATE TABLE table_name PARTITION (p20240101);
-- 修改分区属性
ALTER TABLE table_name MODIFY PARTITION p20240101 SET ("replication_num" = "2");
-- 替换分区(原子操作)
ALTER TABLE table_name REPLACE PARTITION (p20240101)
WITH TEMPORARY PARTITION (tp20240101);
索引操作:
-- 创建倒排索引
CREATE INDEX idx_name ON table_name (column_name) USING INVERTED;
-- 创建 Bitmap 索引
CREATE INDEX idx_category ON table_name (category) USING BITMAP;
-- 创建 NGRAM BloomFilter 索引
CREATE INDEX idx_text ON table_name (text_column) USING NGRAM_BF PROPERTIES("gram_num"="3", "bf_size"="1024");
-- 删除索引
DROP INDEX idx_name ON table_name;
-- 查看索引
SHOW INDEX FROM table_name;
删除表:
-- 删除表
DROP TABLE table_name;
-- 如果存在则删除
DROP TABLE IF EXISTS table_name;
-- 强制删除
DROP TABLE table_name FORCE;
重命名表:
-- 重命名表
ALTER TABLE old_table_name RENAME new_table_name;
清空表:
-- 清空表数据
TRUNCATE TABLE table_name;
-- 清空指定分区
TRUNCATE TABLE table_name PARTITION (p20240101);
3. 视图操作
创建视图:
-- 创建视图
CREATE VIEW view_name AS
SELECT column1, column2
FROM table_name
WHERE condition;
-- 创建或替换视图
CREATE OR REPLACE VIEW view_name AS
SELECT column1, column2
FROM table_name;
-- 创建物化视图
CREATE MATERIALIZED VIEW mv_name AS
SELECT date, city, SUM(amount) AS total_amount
FROM sales_data
GROUP BY date, city;
查看视图:
-- 查看所有视图
SHOW VIEWS;
-- 查看视图定义
SHOW CREATE VIEW view_name;
-- 查看物化视图
SHOW MATERIALIZED VIEWS FROM table_name;
删除视图:
-- 删除视图
DROP VIEW view_name;
-- 删除物化视图
DROP MATERIALIZED VIEW mv_name ON table_name;
DML(数据操作语言)
1. 插入数据
INSERT INTO 语法:
-- 插入单行
INSERT INTO table_name VALUES (value1, value2, value3);
-- 插入多行
INSERT INTO table_name VALUES
(value1, value2, value3),
(value4, value5, value6),
(value7, value8, value9);
-- 指定列插入
INSERT INTO table_name (col1, col2) VALUES (value1, value2);
-- 从查询结果插入
INSERT INTO table_name
SELECT col1, col2, col3
FROM source_table
WHERE condition;
-- 插入并指定 Label(幂等性保证)
INSERT INTO table_name WITH LABEL label_name
VALUES (value1, value2, value3);
-- 插入到指定分区
INSERT INTO table_name PARTITION (p20240101)
VALUES (value1, value2, value3);
INSERT OVERWRITE 语法:
-- 覆盖整表
INSERT OVERWRITE TABLE table_name
SELECT * FROM source_table;
-- 覆盖指定分区
INSERT OVERWRITE TABLE table_name PARTITION (p20240101)
SELECT * FROM source_table WHERE date = '2024-01-01';
-- 动态分区覆盖
INSERT OVERWRITE TABLE table_name PARTITION (*)
SELECT * FROM source_table;
2. 更新数据
UPDATE 语法:
-- 基础更新
UPDATE table_name
SET column1 = value1, column2 = value2
WHERE condition;
-- 使用子查询更新
UPDATE table_name t1
SET t1.column1 = (SELECT column1 FROM table2 t2 WHERE t2.id = t1.id)
WHERE EXISTS (SELECT 1 FROM table2 t2 WHERE t2.id = t1.id);
-- 批量更新
UPDATE table_name
SET status = 1
WHERE id IN (1, 2, 3, 4, 5);
UPDATE 注意事项:
- 只支持 Unique 模型:Duplicate 和 Aggregate 模型不支持 UPDATE
- 需要主键条件:WHERE 条件必须包含主键
- 性能影响:UPDATE 操作会触发数据重写,影响性能
- 推荐使用 INSERT:对于 Unique 模型,推荐使用 INSERT 实现 Upsert
3. 删除数据
DELETE 语法:
-- 基础删除
DELETE FROM table_name WHERE condition;
-- 删除指定主键
DELETE FROM table_name WHERE id = 123;
-- 批量删除
DELETE FROM table_name WHERE id IN (1, 2, 3, 4, 5);
-- 删除指定分区的数据
DELETE FROM table_name PARTITION (p20240101) WHERE condition;
-- 使用子查询删除
DELETE FROM table_name
WHERE id IN (SELECT id FROM temp_table WHERE status = 0);
DELETE 注意事项:
- 只支持 Unique 模型:Duplicate 和 Aggregate 模型不支持 DELETE
- 需要主键条件:WHERE 条件必须包含主键
- 性能影响:DELETE 操作会产生删除标记,需要 Compaction 清理
- 批量删除:建议使用分区删除或 TRUNCATE
4. 查询数据
SELECT 基础语法:
-- 基础查询
SELECT column1, column2 FROM table_name;
-- 条件查询
SELECT * FROM table_name WHERE condition;
-- 排序
SELECT * FROM table_name ORDER BY column1 DESC, column2 ASC;
-- 限制返回行数
SELECT * FROM table_name LIMIT 100;
-- 分页查询
SELECT * FROM table_name LIMIT 100 OFFSET 200;
-- 去重
SELECT DISTINCT column1 FROM table_name;
-- 聚合查询
SELECT
city,
COUNT(*) AS user_count,
SUM(amount) AS total_amount,
AVG(age) AS avg_age
FROM table_name
GROUP BY city
HAVING COUNT(*) > 100;
JOIN 语法:
-- INNER JOIN
SELECT t1.*, t2.name
FROM table1 t1
INNER JOIN table2 t2 ON t1.id = t2.id;
-- LEFT JOIN
SELECT t1.*, t2.name
FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.id;
-- RIGHT JOIN
SELECT t1.*, t2.name
FROM table1 t1
RIGHT JOIN table2 t2 ON t1.id = t2.id;
-- FULL OUTER JOIN
SELECT t1.*, t2.name
FROM table1 t1
FULL OUTER JOIN table2 t2 ON t1.id = t2.id;
-- CROSS JOIN
SELECT t1.*, t2.*
FROM table1 t1
CROSS JOIN table2 t2;
子查询:
-- WHERE 子查询
SELECT * FROM table1
WHERE id IN (SELECT id FROM table2 WHERE status = 1);
-- FROM 子查询
SELECT t.city, t.total_amount
FROM (
SELECT city, SUM(amount) AS total_amount
FROM sales_data
GROUP BY city
) t
WHERE t.total_amount > 10000;
-- EXISTS 子查询
SELECT * FROM table1 t1
WHERE EXISTS (SELECT 1 FROM table2 t2 WHERE t2.id = t1.id);
窗口函数:
-- ROW_NUMBER
SELECT
user_id,
order_date,
amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn
FROM orders;
-- RANK
SELECT
user_id,
score,
RANK() OVER (ORDER BY score DESC) AS rank
FROM user_scores;
-- SUM 窗口函数
SELECT
order_date,
amount,
SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_sum_7d
FROM orders;
-- LAG/LEAD
SELECT
order_date,
amount,
LAG(amount, 1) OVER (ORDER BY order_date) AS prev_amount,
LEAD(amount, 1) OVER (ORDER BY order_date) AS next_amount
FROM orders;
CTE(公共表表达式):
-- 单个 CTE
WITH user_summary AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
)
SELECT * FROM user_summary WHERE order_count > 10;
-- 多个 CTE
WITH
user_orders AS (
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
),
user_amount AS (
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
)
SELECT
u1.user_id,
u1.order_count,
u2.total_amount
FROM user_orders u1
JOIN user_amount u2 ON u1.user_id = u2.user_id;
5. 事务支持
BEGIN/COMMIT/ROLLBACK:
-- 开始事务
BEGIN;
-- 执行操作
INSERT INTO table_name VALUES (1, 'data1');
UPDATE table_name SET status = 1 WHERE id = 2;
DELETE FROM table_name WHERE id = 3;
-- 提交事务
COMMIT;
-- 或回滚事务
ROLLBACK;
事务注意事项:
- 只支持 Unique 模型:Duplicate 和 Aggregate 模型不支持事务
- 单表事务:一个事务只能操作一个表
- 超时时间:默认事务超时时间为 60 秒
- 隔离级别:Read Committed
DDL/DML 最佳实践
1. 建表最佳实践:
-- ✅ 推荐:完整的建表语句
CREATE TABLE IF NOT EXISTS db_name.table_name (
id BIGINT COMMENT '主键ID',
user_id BIGINT COMMENT '用户ID',
order_date DATE COMMENT '订单日期',
amount DECIMAL(18, 2) COMMENT '订单金额',
status TINYINT COMMENT '订单状态:0-待支付,1-已支付,2-已取消'
)
DUPLICATE KEY(id, user_id)
COMMENT '订单表'
PARTITION BY RANGE(order_date) (
PARTITION p20240101 VALUES LESS THAN ("2024-01-02")
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"compression" = "LZ4",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "32"
);
2. 数据导入最佳实践:
-- ✅ 推荐:使用 INSERT INTO SELECT(批量导入)
INSERT INTO target_table
SELECT * FROM source_table
WHERE date >= '2024-01-01';
-- ❌ 不推荐:逐行 INSERT(性能差)
INSERT INTO table_name VALUES (1, 'data1');
INSERT INTO table_name VALUES (2, 'data2');
INSERT INTO table_name VALUES (3, 'data3');
-- ✅ 推荐:批量 INSERT
INSERT INTO table_name VALUES
(1, 'data1'),
(2, 'data2'),
(3, 'data3');
3. 分区管理最佳实践:
-- ✅ 推荐:使用动态分区自动管理
ALTER TABLE table_name SET (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30", -- 保留最近 30 天
"dynamic_partition.end" = "3" -- 提前创建未来 3 天
);
-- ✅ 推荐:批量删除历史分区
ALTER TABLE table_name DROP PARTITION p20240101, p20240102, p20240103;
-- ❌ 不推荐:DELETE 删除历史数据(性能差)
DELETE FROM table_name WHERE date < '2024-01-01';
4. 查询优化最佳实践:
-- ✅ 推荐:使用分区裁剪
SELECT * FROM table_name
WHERE order_date >= '2024-01-01' AND order_date < '2024-01-02';
-- ✅ 推荐:使用列裁剪
SELECT id, user_id, amount FROM table_name;
-- ❌ 不推荐:SELECT *
SELECT * FROM table_name;
-- ✅ 推荐:使用 LIMIT
SELECT * FROM table_name LIMIT 1000;
-- ✅ 推荐:使用索引列过滤
SELECT * FROM table_name WHERE user_id = 123;
常用函数与特有函数
Doris 提供了丰富的内置函数,涵盖字符串处理、日期时间、数值计算、聚合分析、数组操作等多个领域。同时,Doris 还提供了一些特有的函数,如 BITMAP、HLL、向量检索等,用于解决特定场景的性能问题。
函数分类概览
函数类型对比:
| 函数类型 | 数量 | 典型函数 | 使用场景 |
|---|---|---|---|
| 字符串函数 | 50+ | CONCAT, SUBSTRING, REGEXP | 文本处理、模式匹配 |
| 日期时间函数 | 40+ | DATE_FORMAT, DATE_ADD, DATEDIFF | 时间计算、格式转换 |
| 数值函数 | 30+ | ROUND, ABS, MOD | 数学计算、统计分析 |
| 聚合函数 | 20+ | SUM, AVG, COUNT, GROUP_CONCAT | 数据汇总、分组统计 |
| 窗口函数 | 15+ | ROW_NUMBER, RANK, LAG | 排名、移动计算 |
| 数组函数 | 25+ | ARRAY_AGG, EXPLODE, ARRAY_CONTAINS | 数组操作、行列转换 |
| JSON函数 | 15+ | JSON_EXTRACT, JSON_PARSE | JSON 数据解析 |
| BITMAP函数 | 10+ | BITMAP_UNION, BITMAP_INTERSECT | 精确去重、集合运算 |
| HLL函数 | 8+ | HLL_UNION_AGG, HLL_CARDINALITY | 近似去重、基数估算 |
| 向量函数 | 5+ | COSINE_DISTANCE, L2_DISTANCE | 向量检索、相似度计算 |
字符串函数
常用字符串函数:
| 函数 | 语法 | 说明 | 示例 |
|---|---|---|---|
| CONCAT | CONCAT(str1, str2, …) | 拼接字符串 | CONCAT(‘Hello’, ’ ‘, ‘World’) → ‘Hello World’ |
| CONCAT_WS | CONCAT_WS(sep, str1, str2, …) | 用分隔符拼接 | CONCAT_WS(’,’, ‘a’, ‘b’, ‘c’) → ‘a,b,c’ |
| SUBSTRING | SUBSTRING(str, pos, len) | 截取子串 | SUBSTRING(‘Hello’, 2, 3) → ’ell’ |
| LENGTH | LENGTH(str) | 字节长度 | LENGTH(‘你好’) → 6 |
| CHAR_LENGTH | CHAR_LENGTH(str) | 字符长度 | CHAR_LENGTH(‘你好’) → 2 |
| UPPER | UPPER(str) | 转大写 | UPPER(‘hello’) → ‘HELLO’ |
| LOWER | LOWER(str) | 转小写 | LOWER(‘HELLO’) → ‘hello’ |
| TRIM | TRIM([BOTH|LEADING|TRAILING] str FROM str) | 去除空格 | TRIM(’ hello ‘) → ‘hello’ |
| REPLACE | REPLACE(str, from, to) | 替换字符串 | REPLACE(‘hello’, ’l’, ‘L’) → ‘heLLo’ |
| SPLIT_PART | SPLIT_PART(str, delim, part) | 分割取值 | SPLIT_PART(‘a,b,c’, ‘,’, 2) → ‘b’ |
| REGEXP_EXTRACT | REGEXP_EXTRACT(str, pattern, index) | 正则提取 | REGEXP_EXTRACT(‘abc123’, ‘[0-9]+’, 0) → ‘123’ |
| REGEXP_REPLACE | REGEXP_REPLACE(str, pattern, replacement) | 正则替换 | REGEXP_REPLACE(‘abc123’, ‘[0-9]+’, ‘X’) → ‘abcX’ |
字符串函数示例:
-- 1. 字符串拼接
SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
CONCAT_WS('-', year, month, day) AS date_str
FROM user_info;
-- 2. 字符串截取与分割
SELECT
SUBSTRING(phone, 1, 3) AS area_code,
SPLIT_PART(email, '@', 1) AS username,
SPLIT_PART(email, '@', 2) AS domain
FROM contacts;
-- 3. 正则表达式提取
SELECT
REGEXP_EXTRACT(log_message, 'user_id=([0-9]+)', 1) AS user_id,
REGEXP_EXTRACT(log_message, 'ip=([0-9.]+)', 1) AS ip_address
FROM access_log;
-- 4. 字符串替换与清洗
SELECT
REPLACE(product_name, ' ', ' ') AS cleaned_name,
REGEXP_REPLACE(phone, '[^0-9]', '') AS phone_digits,
TRIM(BOTH ' ' FROM description) AS trimmed_desc
FROM products;
-- 5. UTF-8 字符处理
SELECT
CHAR_LENGTH('你好世界') AS char_count, -- 4
LENGTH('你好世界') AS byte_count, -- 12
SUBSTRING('你好世界', 2, 2) AS sub_str -- '好世'
FROM dual;
字符串函数性能优化:
-- ❌ 避免:在大表上使用 LIKE '%keyword%'
SELECT * FROM large_table WHERE content LIKE '%关键词%';
-- ✅ 推荐:使用倒排索引 + MATCH_ANY
SELECT * FROM large_table WHERE content MATCH_ANY '关键词';
-- ❌ 避免:重复调用字符串函数
SELECT
UPPER(name),
LOWER(name),
LENGTH(name)
FROM large_table;
-- ✅ 推荐:使用子查询或 CTE 缓存结果
WITH name_processed AS (
SELECT name, UPPER(name) AS upper_name FROM large_table
)
SELECT upper_name, LENGTH(upper_name) FROM name_processed;
日期时间函数
常用日期时间函数:
| 函数 | 语法 | 说明 | 示例 |
|---|---|---|---|
| NOW | NOW() | 当前时间 | NOW() → ‘2024-03-20 10:30:00’ |
| CURDATE | CURDATE() | 当前日期 | CURDATE() → ‘2024-03-20’ |
| DATE_FORMAT | DATE_FORMAT(date, format) | 格式化日期 | DATE_FORMAT(NOW(), ‘%Y-%m-%d’) → ‘2024-03-20’ |
| DATE_ADD | DATE_ADD(date, INTERVAL expr unit) | 日期加法 | DATE_ADD(‘2024-01-01’, INTERVAL 7 DAY) → ‘2024-01-08’ |
| DATE_SUB | DATE_SUB(date, INTERVAL expr unit) | 日期减法 | DATE_SUB(‘2024-01-08’, INTERVAL 7 DAY) → ‘2024-01-01’ |
| DATEDIFF | DATEDIFF(date1, date2) | 日期差(天) | DATEDIFF(‘2024-01-08’, ‘2024-01-01’) → 7 |
| UNIX_TIMESTAMP | UNIX_TIMESTAMP([date]) | 转时间戳 | UNIX_TIMESTAMP(‘2024-01-01 00:00:00’) → 1704038400 |
| FROM_UNIXTIME | FROM_UNIXTIME(timestamp) | 时间戳转日期 | FROM_UNIXTIME(1704038400) → ‘2024-01-01 00:00:00’ |
| YEAR | YEAR(date) | 提取年份 | YEAR(‘2024-03-20’) → 2024 |
| MONTH | MONTH(date) | 提取月份 | MONTH(‘2024-03-20’) → 3 |
| DAY | DAY(date) | 提取日 | DAY(‘2024-03-20’) → 20 |
| HOUR | HOUR(datetime) | 提取小时 | HOUR(‘2024-03-20 10:30:00’) → 10 |
| WEEK | WEEK(date) | 提取周数 | WEEK(‘2024-03-20’) → 12 |
| DAYOFWEEK | DAYOFWEEK(date) | 星期几(1-7) | DAYOFWEEK(‘2024-03-20’) → 4 |
日期时间函数示例:
-- 1. 日期格式化
SELECT
order_time,
DATE_FORMAT(order_time, '%Y-%m-%d') AS order_date,
DATE_FORMAT(order_time, '%Y年%m月%d日 %H:%i:%s') AS cn_format,
DATE_FORMAT(order_time, '%W, %M %d, %Y') AS en_format
FROM orders;
-- 2. 日期计算
SELECT
order_date,
DATE_ADD(order_date, INTERVAL 7 DAY) AS delivery_date,
DATE_SUB(order_date, INTERVAL 1 MONTH) AS last_month,
DATEDIFF(NOW(), order_date) AS days_ago
FROM orders;
-- 3. 时间戳转换
SELECT
event_timestamp,
FROM_UNIXTIME(event_timestamp) AS event_time,
UNIX_TIMESTAMP(create_time) AS create_timestamp
FROM events;
-- 4. 日期提取与分组
SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY year, month;
-- 5. 周维度分析
SELECT
DATE_FORMAT(order_date, '%Y-W%u') AS week,
DAYOFWEEK(order_date) AS weekday,
COUNT(*) AS order_count
FROM orders
GROUP BY week, weekday;
-- 6. 时间范围查询优化
-- ❌ 避免:在日期列上使用函数
SELECT * FROM orders WHERE DATE(order_time) = '2024-03-20';
-- ✅ 推荐:使用范围查询
SELECT * FROM orders
WHERE order_time >= '2024-03-20 00:00:00'
AND order_time < '2024-03-21 00:00:00';
日期时间格式符号:
| 格式符 | 说明 | 示例 |
|---|---|---|
| %Y | 4位年份 | 2024 |
| %y | 2位年份 | 24 |
| %m | 2位月份 | 03 |
| %c | 月份(1-12) | 3 |
| %d | 2位日期 | 20 |
| %e | 日期(1-31) | 20 |
| %H | 24小时制小时 | 14 |
| %h | 12小时制小时 | 02 |
| %i | 分钟 | 30 |
| %s | 秒 | 45 |
| %W | 星期名称 | Wednesday |
| %w | 星期数字(0-6) | 3 |
| %M | 月份名称 | March |
数值函数
常用数值函数:
| 函数 | 语法 | 说明 | 示例 |
|---|---|---|---|
| ROUND | ROUND(x, d) | 四舍五入 | ROUND(3.1415, 2) → 3.14 |
| CEIL | CEIL(x) | 向上取整 | CEIL(3.14) → 4 |
| FLOOR | FLOOR(x) | 向下取整 | FLOOR(3.99) → 3 |
| ABS | ABS(x) | 绝对值 | ABS(-10) → 10 |
| MOD | MOD(x, y) | 取模 | MOD(10, 3) → 1 |
| POWER | POWER(x, y) | 幂运算 | POWER(2, 3) → 8 |
| SQRT | SQRT(x) | 平方根 | SQRT(16) → 4 |
| RAND | RAND([seed]) | 随机数 | RAND() → 0.123456 |
| GREATEST | GREATEST(x1, x2, …) | 最大值 | GREATEST(1, 5, 3) → 5 |
| LEAST | LEAST(x1, x2, …) | 最小值 | LEAST(1, 5, 3) → 1 |
数值函数示例:
-- 1. 数值计算与格式化
SELECT
price,
ROUND(price, 2) AS rounded_price,
CEIL(price) AS ceil_price,
FLOOR(price) AS floor_price,
ROUND(price * 0.8, 2) AS discount_price
FROM products;
-- 2. 统计计算
SELECT
category,
COUNT(*) AS product_count,
ROUND(AVG(price), 2) AS avg_price,
ROUND(STDDEV(price), 2) AS stddev_price,
MIN(price) AS min_price,
MAX(price) AS max_price
FROM products
GROUP BY category;
-- 3. 随机采样
SELECT * FROM large_table
WHERE RAND() < 0.01 -- 1% 采样
LIMIT 1000;
-- 4. 数值范围处理
SELECT
score,
GREATEST(score, 0) AS non_negative_score,
LEAST(score, 100) AS capped_score,
GREATEST(LEAST(score, 100), 0) AS normalized_score
FROM exam_results;
聚合函数
常用聚合函数:
| 函数 | 语法 | 说明 | 示例 |
|---|---|---|---|
| COUNT | COUNT(*) / COUNT(col) | 计数 | COUNT(*) → 总行数 |
| SUM | SUM(col) | 求和 | SUM(amount) → 总金额 |
| AVG | AVG(col) | 平均值 | AVG(score) → 平均分 |
| MIN | MIN(col) | 最小值 | MIN(price) → 最低价 |
| MAX | MAX(col) | 最大值 | MAX(price) → 最高价 |
| GROUP_CONCAT | GROUP_CONCAT(col [ORDER BY] [SEPARATOR]) | 字符串聚合 | GROUP_CONCAT(name, ‘,’) → ‘a,b,c’ |
| STDDEV | STDDEV(col) | 标准差 | STDDEV(score) → 标准差 |
| VARIANCE | VARIANCE(col) | 方差 | VARIANCE(score) → 方差 |
| PERCENTILE | PERCENTILE(col, p) | 百分位数 | PERCENTILE(response_time, 0.95) → P95 |
聚合函数示例:
-- 1. 基础聚合统计
SELECT
category,
COUNT(*) AS product_count,
SUM(sales) AS total_sales,
AVG(price) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price
FROM products
GROUP BY category;
-- 2. 字符串聚合
SELECT
user_id,
GROUP_CONCAT(tag ORDER BY tag SEPARATOR ',') AS tags,
GROUP_CONCAT(DISTINCT category SEPARATOR '|') AS categories
FROM user_tags
GROUP BY user_id;
-- 3. 百分位数计算(性能监控)
SELECT
service_name,
PERCENTILE(response_time, 0.50) AS p50,
PERCENTILE(response_time, 0.95) AS p95,
PERCENTILE(response_time, 0.99) AS p99,
MAX(response_time) AS max_time
FROM service_log
GROUP BY service_name;
-- 4. 多维度聚合(ROLLUP)
SELECT
COALESCE(region, 'ALL') AS region,
COALESCE(category, 'ALL') AS category,
SUM(sales) AS total_sales
FROM sales_data
GROUP BY ROLLUP(region, category);
窗口函数
常用窗口函数:
| 函数 | 语法 | 说明 | 使用场景 |
|---|---|---|---|
| ROW_NUMBER | ROW_NUMBER() OVER(…) | 行号(连续) | 排序、分页 |
| RANK | RANK() OVER(…) | 排名(跳跃) | 排行榜 |
| DENSE_RANK | DENSE_RANK() OVER(…) | 排名(连续) | 排行榜 |
| LAG | LAG(col, n) OVER(…) | 前n行的值 | 环比计算 |
| LEAD | LEAD(col, n) OVER(…) | 后n行的值 | 预测分析 |
| FIRST_VALUE | FIRST_VALUE(col) OVER(…) | 窗口第一个值 | 基准对比 |
| LAST_VALUE | LAST_VALUE(col) OVER(…) | 窗口最后一个值 | 趋势分析 |
| SUM | SUM(col) OVER(…) | 累计求和 | 累计统计 |
| AVG | AVG(col) OVER(…) | 移动平均 | 趋势平滑 |
窗口函数示例:
-- 1. 排名与分组排名
SELECT
student_id,
class_id,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rank,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rank,
RANK() OVER (PARTITION BY class_id ORDER BY score DESC) AS class_rank
FROM exam_scores;
-- 2. 环比与同比计算
SELECT
date,
sales,
LAG(sales, 1) OVER (ORDER BY date) AS prev_day_sales,
sales - LAG(sales, 1) OVER (ORDER BY date) AS day_diff,
LAG(sales, 7) OVER (ORDER BY date) AS prev_week_sales,
ROUND((sales - LAG(sales, 7) OVER (ORDER BY date)) / LAG(sales, 7) OVER (ORDER BY date) * 100, 2) AS week_growth_rate
FROM daily_sales;
-- 3. 累计统计
SELECT
order_date,
amount,
SUM(amount) OVER (ORDER BY order_date) AS cumulative_amount,
AVG(amount) OVER (ORDER BY order_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7
FROM orders;
-- 4. Top N 查询(每个分类的前3名)
SELECT * FROM (
SELECT
category,
product_name,
sales,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) AS rn
FROM products
) t
WHERE rn <= 3;
-- 5. 窗口帧定义
SELECT
date,
sales,
-- 前7天移动平均
AVG(sales) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma7,
-- 前后3天移动平均
AVG(sales) OVER (ORDER BY date ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING) AS ma7_centered,
-- 累计求和
SUM(sales) OVER (ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumsum
FROM daily_sales;
数组函数
常用数组函数:
| 函数 | 语法 | 说明 | 示例 |
|---|---|---|---|
| ARRAY_AGG | ARRAY_AGG(col) | 聚合成数组 | ARRAY_AGG(tag) → [’tag1’, ’tag2’] |
| COLLECT_LIST | COLLECT_LIST(col) | 聚合成数组(含NULL) | COLLECT_LIST(value) → [1, 2, NULL, 3] |
| ARRAY_CONTAINS | ARRAY_CONTAINS(arr, val) | 是否包含 | ARRAY_CONTAINS([1,2,3], 2) → true |
| ARRAY_SIZE | ARRAY_SIZE(arr) | 数组长度 | ARRAY_SIZE([1,2,3]) → 3 |
| ARRAY_DISTINCT | ARRAY_DISTINCT(arr) | 数组去重 | ARRAY_DISTINCT([1,2,2,3]) → [1,2,3] |
| ARRAY_SORT | ARRAY_SORT(arr) | 数组排序 | ARRAY_SORT([3,1,2]) → [1,2,3] |
| ARRAY_UNION | ARRAY_UNION(arr1, arr2) | 数组并集 | ARRAY_UNION([1,2], [2,3]) → [1,2,3] |
| ARRAY_INTERSECT | ARRAY_INTERSECT(arr1, arr2) | 数组交集 | ARRAY_INTERSECT([1,2], [2,3]) → [2] |
| ARRAY_EXCEPT | ARRAY_EXCEPT(arr1, arr2) | 数组差集 | ARRAY_EXCEPT([1,2,3], [2]) → [1,3] |
| EXPLODE | EXPLODE(arr) | 数组展开 | 配合 LATERAL VIEW 使用 |
数组函数示例:
-- 1. 行转列:聚合成数组
SELECT
user_id,
ARRAY_AGG(tag) AS tags,
COLLECT_LIST(score) AS scores,
ARRAY_AGG(DISTINCT category) AS categories
FROM user_behavior
GROUP BY user_id;
-- 2. 数组查询与过滤
SELECT
user_id,
tags,
ARRAY_SIZE(tags) AS tag_count,
ARRAY_CONTAINS(tags, '活跃用户') AS is_active,
tags[1] AS first_tag -- 数组下标从1开始
FROM user_tags
WHERE ARRAY_SIZE(tags) > 0;
-- 3. 数组集合运算
SELECT
user_id,
ARRAY_UNION(tags_2023, tags_2024) AS all_tags,
ARRAY_INTERSECT(tags_2023, tags_2024) AS common_tags,
ARRAY_EXCEPT(tags_2024, tags_2023) AS new_tags
FROM user_tag_history;
-- 4. 列转行:数组展开(LATERAL VIEW)
SELECT
user_id,
tag
FROM user_tags
LATERAL VIEW EXPLODE(tags) tmp AS tag;
-- 5. 数组展开后聚合
SELECT
tag,
COUNT(DISTINCT user_id) AS user_count
FROM user_tags
LATERAL VIEW EXPLODE(tags) tmp AS tag
GROUP BY tag
ORDER BY user_count DESC
LIMIT 10;
-- 6. 多个数组同时展开
SELECT
user_id,
tag,
score
FROM user_data
LATERAL VIEW EXPLODE(tags) t1 AS tag
LATERAL VIEW EXPLODE(scores) t2 AS score;
BITMAP 函数(Doris 特有)
BITMAP 函数列表:
| 函数 | 语法 | 说明 | 使用场景 |
|---|---|---|---|
| BITMAP_HASH | BITMAP_HASH(col) | 创建 BITMAP | 数据导入 |
| BITMAP_UNION | BITMAP_UNION(bitmap) | BITMAP 并集 | 聚合去重 |
| BITMAP_INTERSECT | BITMAP_INTERSECT(bitmap) | BITMAP 交集 | 交集计算 |
| BITMAP_UNION_COUNT | BITMAP_UNION_COUNT(bitmap) | 并集计数 | UV 统计 |
| BITMAP_INTERSECT_COUNT | BITMAP_INTERSECT_COUNT(bitmap) | 交集计数 | 共同用户数 |
| BITMAP_CONTAINS | BITMAP_CONTAINS(bitmap, value) | 是否包含 | 用户判断 |
| BITMAP_HAS_ANY | BITMAP_HAS_ANY(bitmap1, bitmap2) | 是否有交集 | 快速判断 |
| BITMAP_TO_STRING | BITMAP_TO_STRING(bitmap) | 转字符串 | 调试查看 |
| BITMAP_FROM_STRING | BITMAP_FROM_STRING(str) | 从字符串创建 | 数据导入 |
BITMAP 使用场景:
BITMAP 是 Doris 的核心特性之一,用于精确去重和集合运算,相比 COUNT DISTINCT 性能提升 10-100 倍。
BITMAP 完整示例:
-- 1. 创建 BITMAP 聚合表
CREATE TABLE user_visit_bitmap (
visit_date DATE,
page_id INT,
user_ids BITMAP BITMAP_UNION
)
AGGREGATE KEY(visit_date, page_id)
DISTRIBUTED BY HASH(page_id) BUCKETS 32;
-- 2. 导入数据(将 user_id 转为 BITMAP)
INSERT INTO user_visit_bitmap
SELECT
visit_date,
page_id,
BITMAP_HASH(user_id) AS user_ids
FROM user_visit_log
GROUP BY visit_date, page_id;
-- 3. 查询 UV(独立访客数)
SELECT
visit_date,
page_id,
BITMAP_UNION_COUNT(user_ids) AS uv
FROM user_visit_bitmap
GROUP BY visit_date, page_id;
-- 4. 多页面 UV 并集(访问过任一页面的用户数)
SELECT
visit_date,
BITMAP_UNION_COUNT(user_ids) AS total_uv
FROM user_visit_bitmap
WHERE page_id IN (1, 2, 3)
GROUP BY visit_date;
-- 5. 多页面 UV 交集(同时访问多个页面的用户数)
SELECT
visit_date,
BITMAP_INTERSECT_COUNT(user_ids) AS common_uv
FROM user_visit_bitmap
WHERE page_id IN (1, 2, 3)
GROUP BY visit_date;
-- 6. 留存分析(第1天访问且第7天也访问的用户数)
SELECT
BITMAP_INTERSECT_COUNT(
CASE WHEN visit_date = '2024-01-01' THEN user_ids END,
CASE WHEN visit_date = '2024-01-07' THEN user_ids END
) AS retention_count
FROM user_visit_bitmap
WHERE visit_date IN ('2024-01-01', '2024-01-07');
-- 7. 用户标签交集(同时拥有多个标签的用户数)
SELECT
BITMAP_INTERSECT_COUNT(user_bitmap) AS user_count
FROM user_tag_bitmap
WHERE tag IN ('高价值用户', '活跃用户', '付费用户');
-- 8. BITMAP 转字符串查看(调试用)
SELECT
page_id,
BITMAP_TO_STRING(user_ids) AS user_id_list
FROM user_visit_bitmap
WHERE visit_date = '2024-01-01'
LIMIT 5;
BITMAP vs COUNT DISTINCT 性能对比:
-- ❌ 慢:COUNT DISTINCT(全表扫描+去重)
SELECT
visit_date,
COUNT(DISTINCT user_id) AS uv
FROM user_visit_log
GROUP BY visit_date;
-- 耗时:10s+
-- ✅ 快:BITMAP(预聚合)
SELECT
visit_date,
BITMAP_UNION_COUNT(user_ids) AS uv
FROM user_visit_bitmap
GROUP BY visit_date;
-- 耗时:< 100ms
HLL 函数(Doris 特有)
HLL(HyperLogLog)函数列表:
| 函数 | 语法 | 说明 | 误差率 |
|---|---|---|---|
| HLL_HASH | HLL_HASH(col) | 创建 HLL | - |
| HLL_UNION_AGG | HLL_UNION_AGG(hll) | HLL 并集聚合 | 1-2% |
| HLL_CARDINALITY | HLL_CARDINALITY(hll) | HLL 基数估算 | 1-2% |
| HLL_UNION | HLL_UNION(hll1, hll2) | HLL 合并 | 1-2% |
HLL 使用场景:
HLL 用于近似去重,适合千万级以上的大数据集,相比 BITMAP 节省存储空间,但有 1-2% 的误差。
HLL 完整示例:
-- 1. 创建 HLL 聚合表
CREATE TABLE page_uv_hll (
visit_date DATE,
page_id INT,
user_hll HLL HLL_UNION
)
AGGREGATE KEY(visit_date, page_id)
DISTRIBUTED BY HASH(page_id) BUCKETS 32;
-- 2. 导入数据
INSERT INTO page_uv_hll
SELECT
visit_date,
page_id,
HLL_HASH(user_id) AS user_hll
FROM user_visit_log
GROUP BY visit_date, page_id;
-- 3. 查询近似 UV
SELECT
visit_date,
page_id,
HLL_CARDINALITY(HLL_UNION_AGG(user_hll)) AS approx_uv
FROM page_uv_hll
GROUP BY visit_date, page_id;
-- 4. 全站 UV(所有页面的独立访客数)
SELECT
visit_date,
HLL_CARDINALITY(HLL_UNION_AGG(user_hll)) AS total_uv
FROM page_uv_hll
GROUP BY visit_date;
BITMAP vs HLL 选择:
| 对比项 | BITMAP | HLL |
|---|---|---|
| 精度 | 精确(100%) | 近似(98-99%) |
| 存储空间 | 较大(基数/8 字节) | 固定(~16KB) |
| 适用基数 | < 1亿 | > 1亿 |
| 性能 | 快 | 更快 |
| 使用建议 | 精确去重、集合运算 | 超大基数估算 |
向量函数(Doris 4.x 特有)
向量距离函数:
| 函数 | 语法 | 说明 | 使用场景 |
|---|---|---|---|
| COSINE_DISTANCE | COSINE_DISTANCE(vec1, vec2) | 余弦距离 | 文本相似度 |
| L2_DISTANCE | L2_DISTANCE(vec1, vec2) | 欧氏距离 | 图像相似度 |
| IP_DISTANCE | IP_DISTANCE(vec1, vec2) | 内积距离 | 推荐系统 |
向量检索示例:
-- 1. 创建向量表
CREATE TABLE document_vectors (
doc_id BIGINT,
title VARCHAR(500),
embedding ARRAY<FLOAT>,
INDEX idx_embedding (embedding) USING HNSW
)
DUPLICATE KEY(doc_id)
DISTRIBUTED BY HASH(doc_id) BUCKETS 32;
-- 2. 向量相似度查询(Top K)
SELECT
doc_id,
title,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS distance
FROM document_vectors
ORDER BY distance ASC
LIMIT 10;
-- 3. 向量索引加速查询
SELECT
doc_id,
title,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS distance
FROM document_vectors
WHERE COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) < 0.5
ORDER BY distance ASC
LIMIT 10;
JSON 函数
常用 JSON 函数:
| 函数 | 语法 | 说明 |
|---|---|---|
| JSON_EXTRACT | JSON_EXTRACT(json, path) | 提取 JSON 值 |
| JSON_EXTRACT_STRING | JSON_EXTRACT_STRING(json, path) | 提取字符串 |
| JSON_EXTRACT_INT | JSON_EXTRACT_INT(json, path) | 提取整数 |
| JSON_EXTRACT_DOUBLE | JSON_EXTRACT_DOUBLE(json, path) | 提取浮点数 |
| JSON_PARSE | JSON_PARSE(str) | 解析 JSON 字符串 |
| JSON_ARRAY | JSON_ARRAY(val1, val2, …) | 创建 JSON 数组 |
| JSON_OBJECT | JSON_OBJECT(key1, val1, …) | 创建 JSON 对象 |
JSON 函数示例:
-- 1. JSON 数据提取
SELECT
event_id,
JSON_EXTRACT(event_data, '$.user_id') AS user_id,
JSON_EXTRACT_STRING(event_data, '$.action') AS action,
JSON_EXTRACT_INT(event_data, '$.duration') AS duration,
JSON_EXTRACT(event_data, '$.properties.page') AS page
FROM event_log;
-- 2. JSON 数组展开
SELECT
order_id,
item
FROM orders
LATERAL VIEW EXPLODE(JSON_EXTRACT(order_data, '$.items')) tmp AS item;
-- 3. JSON 构造
SELECT
user_id,
JSON_OBJECT(
'name', username,
'age', age,
'tags', JSON_ARRAY(tag1, tag2, tag3)
) AS user_json
FROM users;
函数使用最佳实践
1. 避免在 WHERE 子句中使用函数:
-- ❌ 避免:无法使用索引
SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- ✅ 推荐:使用范围查询
SELECT * FROM orders
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';
2. 使用 BITMAP/HLL 替代 COUNT DISTINCT:
-- ❌ 慢:COUNT DISTINCT
SELECT COUNT(DISTINCT user_id) FROM large_table;
-- ✅ 快:BITMAP(精确)
SELECT BITMAP_UNION_COUNT(user_bitmap) FROM bitmap_table;
-- ✅ 快:HLL(近似)
SELECT HLL_CARDINALITY(HLL_UNION_AGG(user_hll)) FROM hll_table;
3. 窗口函数优化:
-- ❌ 避免:多次窗口计算
SELECT
ROW_NUMBER() OVER (ORDER BY score DESC) AS rn1,
RANK() OVER (ORDER BY score DESC) AS rn2
FROM large_table;
-- ✅ 推荐:使用子查询缓存
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY score DESC) AS rn
FROM large_table
)
SELECT * FROM ranked WHERE rn <= 100;
4. 数组函数性能注意:
-- ❌ 避免:频繁 EXPLODE 大数组
SELECT user_id, tag
FROM large_table
LATERAL VIEW EXPLODE(tags) tmp AS tag; -- tags 数组很大
-- ✅ 推荐:先过滤再 EXPLODE
SELECT user_id, tag
FROM (
SELECT * FROM large_table WHERE ARRAY_SIZE(tags) > 0 LIMIT 10000
) t
LATERAL VIEW EXPLODE(tags) tmp AS tag;
索引与查询优化
索引类型
Doris 支持多种索引类型,自动选择最优索引加速查询。
索引类型对比:
| 索引类型 | 原理 | 适用场景 | 过滤效果 | 存储开销 |
|---|---|---|---|---|
| 前缀索引 | 稀疏索引,类似 InnoDB | 前缀列等值/范围查询 | 高 | 极小 |
| ZoneMap | 记录每个 Page 的 Min/Max | 数值范围过滤 | 中等 | 极小 |
| BloomFilter | 概率数据结构 | 等值查询、IN 查询 | 高(有误判) | 小 |
| 倒排索引 | 词项到文档的映射 | 全文检索、字符串匹配 | 高 | 中等 |
| 向量索引 | HNSW 图结构 | 向量相似度检索 | 高 | 较大 |
| Bitmap 索引 | 位图结构 | 低基数列等值查询 | 高 | 小 |
1. 前缀索引(Short Key Index)
原理:
- 对前 36 字节的列建立稀疏索引
- 每 1024 行数据生成一个索引项
- 类似 MySQL InnoDB 的聚簇索引
索引结构:
建表示例:
CREATE TABLE user_table (
user_id BIGINT, -- 8 字节
username VARCHAR(50), -- 最多 50 字节
age INT, -- 4 字节
city VARCHAR(50),
register_time DATETIME
)
DUPLICATE KEY(user_id, username, age) -- 前 3 列作为前缀索引(8+50+4=62 字节,取前 36 字节)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
查询优化:
-- 高效查询(使用前缀索引)
SELECT * FROM user_table WHERE user_id = 12345;
SELECT * FROM user_table WHERE user_id = 12345 AND username = 'alice';
-- 低效查询(无法使用前缀索引)
SELECT * FROM user_table WHERE age = 25; -- age 不是前缀列
SELECT * FROM user_table WHERE city = 'Beijing'; -- city 不在前缀索引中
最佳实践:
- 将高频过滤列放在前面
- 将高基数列放在前面(如 user_id)
- 控制前缀索引列的总长度 ≤ 36 字节
2. ZoneMap 索引
原理:
- 记录每个 Data Page(默认 64KB)的 Min/Max 值
- 查询时根据 Min/Max 跳过不相关的 Page
索引结构:
Page 1: [Min: 1, Max: 1000]
Page 2: [Min: 1001, Max: 2000]
Page 3: [Min: 2001, Max: 3000]
查询: WHERE id > 1500
跳过: Page 1(Max < 1500)
扫描: Page 2, Page 3
适用场景:
- 数值类型的范围查询
- 日期时间范围查询
- 有序或半有序数据
3. BloomFilter 索引
原理:
- 使用多个 Hash 函数将数据映射到位数组
- 查询时快速判断数据是否可能存在
- 有误判(False Positive),但无漏判(False Negative)
创建 BloomFilter 索引:
CREATE TABLE user_table (
user_id BIGINT,
email VARCHAR(100),
phone VARCHAR(20)
)
DUPLICATE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"bloom_filter_columns" = "email,phone" -- 为 email 和 phone 创建 BloomFilter
);
查询优化:
-- 高效查询(使用 BloomFilter)
SELECT * FROM user_table WHERE email = '[email protected]';
SELECT * FROM user_table WHERE phone IN ('13800138000', '13900139000');
-- 无效查询(BloomFilter 不支持)
SELECT * FROM user_table WHERE email LIKE '%alice%'; -- 模糊查询不支持
误判率控制:
-- 调整 BloomFilter 参数
ALTER TABLE user_table SET (
"bloom_filter_fpp" = "0.05" -- 误判率 5%(默认 0.05)
);
向量索引 (HNSW)
HNSW(Hierarchical Navigable Small World) 是 Doris 4.x 引入的向量索引算法,用于高效的向量相似度检索。
HNSW 原理:
HNSW 特点:
- 多层图结构:上层稀疏,下层密集
- 快速搜索:从上层快速定位,下层精确搜索
- 高召回率:通过调整参数平衡性能和准确性
创建向量索引:
-- 创建包含向量列的表
CREATE TABLE document_embeddings (
doc_id BIGINT,
title VARCHAR(200),
content TEXT,
embedding ARRAY<FLOAT> -- 向量列(1536 维)
)
DUPLICATE KEY(doc_id)
DISTRIBUTED BY HASH(doc_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3"
);
-- 创建 HNSW 向量索引
ALTER TABLE document_embeddings
ADD INDEX embedding_idx(embedding) USING HNSW
PROPERTIES (
"metric_type" = "L2", -- 距离度量:L2(欧氏距离)或 COSINE(余弦相似度)
"M" = "16", -- 每个节点的最大连接数(默认 16)
"ef_construction" = "200" -- 构建索引时的搜索深度(默认 200)
);
向量检索查询:
-- 查询最相似的 Top 10 文档
SELECT doc_id, title,
L2_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS distance
FROM document_embeddings
ORDER BY distance ASC
LIMIT 10;
-- 使用余弦相似度
SELECT doc_id, title,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS similarity
FROM document_embeddings
ORDER BY similarity DESC
LIMIT 10;
混合检索(向量 + 标量过滤):
-- 先过滤再向量检索
SELECT doc_id, title,
L2_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS distance
FROM document_embeddings
WHERE doc_id > 1000 AND title LIKE '%AI%' -- 标量过滤
ORDER BY distance ASC
LIMIT 10;
HNSW 参数调优:
| 参数 | 说明 | 推荐值 | 影响 |
|---|---|---|---|
| M | 每个节点的最大连接数 | 16-32 | 越大召回率越高,索引越大 |
| ef_construction | 构建索引时的搜索深度 | 200-400 | 越大索引质量越高,构建越慢 |
| ef_search | 查询时的搜索深度 | 100-200 | 越大召回率越高,查询越慢 |
性能对比:
数据量:1 亿向量(1536 维)
查询:Top 10 相似向量
无索引(暴力搜索):
├── 查询时间:30-60 秒
└── 召回率:100%
HNSW 索引:
├── 查询时间:10-50 毫秒
├── 召回率:95-99%(可调)
└── 索引大小:原始数据的 10-20%
倒排索引与全文检索
倒排索引原理:
创建倒排索引:
-- 创建包含文本列的表
CREATE TABLE articles (
article_id BIGINT,
title VARCHAR(200),
content TEXT,
author VARCHAR(100),
publish_time DATETIME
)
DUPLICATE KEY(article_id)
DISTRIBUTED BY HASH(article_id) BUCKETS 32;
-- 为 title 和 content 创建倒排索引
ALTER TABLE articles
ADD INDEX title_idx(title) USING INVERTED PROPERTIES("parser" = "chinese");
ALTER TABLE articles
ADD INDEX content_idx(content) USING INVERTED PROPERTIES(
"parser" = "chinese", -- 中文分词
"support_phrase" = "true", -- 支持短语查询
"char_filter_type" = "char_replace" -- 字符过滤
);
全文检索查询:
-- 单词匹配
SELECT * FROM articles
WHERE title MATCH 'Apache Doris';
-- 短语匹配
SELECT * FROM articles
WHERE content MATCH_PHRASE 'real-time analytics';
-- 布尔查询
SELECT * FROM articles
WHERE content MATCH_ALL 'Doris SQL query' -- 必须包含所有词
OR content MATCH_ANY 'Doris ClickHouse'; -- 包含任意词
-- 组合查询(全文 + 结构化)
SELECT * FROM articles
WHERE content MATCH 'Apache Doris'
AND publish_time >= '2024-01-01'
AND author = 'Alice';
BM25 相关性评分:
Doris 4.0 引入 BM25 算法进行相关性评分。
-- 按相关性排序
SELECT article_id, title,
BM25_SCORE(content, 'Apache Doris') AS score
FROM articles
WHERE content MATCH 'Apache Doris'
ORDER BY score DESC
LIMIT 10;
BM25 公式:
BM25(D, Q) = Σ IDF(qi) * (f(qi, D) * (k1 + 1)) / (f(qi, D) + k1 * (1 - b + b * |D| / avgdl))
其中:
- D: 文档
- Q: 查询词集合
- qi: 查询词
- f(qi, D): qi 在文档 D 中的词频
- |D|: 文档 D 的长度
- avgdl: 平均文档长度
- k1, b: 调节参数(默认 k1=1.2, b=0.75)
中文分词:
Doris 支持多种中文分词器:
| 分词器 | 说明 | 适用场景 |
|---|---|---|
| chinese | 基于词典的分词 | 通用场景 |
| unicode | 按 Unicode 字符分词 | 多语言混合 |
| english | 英文分词 | 英文文本 |
分词示例:
-- 查看分词结果
SELECT TOKENIZE('Apache Doris 是一个高性能实时分析数据库', 'chinese');
-- 结果:['Apache', 'Doris', '是', '一个', '高性能', '实时', '分析', '数据库']
查询优化器
查询优化流程:
逻辑优化规则:
| 优化规则 | 说明 | 示例 |
|---|---|---|
| 谓词下推 | 将过滤条件推到数据源 | WHERE 条件下推到 Scan |
| 列裁剪 | 只读取需要的列 | SELECT a, b 只读取 a, b 列 |
| 分区裁剪 | 只扫描相关分区 | WHERE date = ‘2024-01-01’ 只扫描该分区 |
| 常量折叠 | 编译时计算常量表达式 | WHERE 1 + 1 = 2 → WHERE TRUE |
| 子查询展开 | 将子查询转换为 Join | IN 子查询 → Semi Join |
| 外连接消除 | 将 Outer Join 转换为 Inner Join | LEFT JOIN + WHERE 条件 → INNER JOIN |
物理优化规则:
| 优化规则 | 说明 | 触发条件 |
|---|---|---|
| Broadcast Join | 广播小表到所有节点 | 小表 < 10MB |
| Shuffle Join | 按 Join Key 重分布数据 | 大表 Join 大表 |
| Colocate Join | 利用数据本地性 | 表在同一 Colocate Group |
| Runtime Filter | 动态生成过滤条件 | Join 场景 |
| 物化视图改写 | 使用物化视图加速查询 | 存在匹配的物化视图 |
CBO(基于成本优化):
Doris 使用统计信息进行成本估算:
-- 收集统计信息
ANALYZE TABLE user_table;
-- 查看统计信息
SHOW COLUMN STATS user_table;
-- 查看查询计划和成本
EXPLAIN COST SELECT * FROM user_table WHERE age > 18;
统计信息包括:
- 行数:表的总行数
- NDV(Number of Distinct Values):列的唯一值数量
- NULL 值数量:列中 NULL 值的数量
- 最小值/最大值:列的数据范围
- 平均长度:字符串列的平均长度
查询 Hint:
手动指定优化策略:
-- 强制使用 Broadcast Join
SELECT /*+ BROADCAST(t2) */ *
FROM large_table t1
JOIN small_table t2 ON t1.id = t2.id;
-- 强制使用 Shuffle Join
SELECT /*+ SHUFFLE(t1, t2) */ *
FROM table1 t1
JOIN table2 t2 ON t1.id = t2.id;
-- 禁用物化视图改写
SELECT /*+ NO_REWRITE */ *
FROM user_table;
查询 Hint:
手动指定优化策略:
-- 强制使用 Broadcast Join
SELECT /*+ BROADCAST(t2) */ *
FROM large_table t1
JOIN small_table t2 ON t1.id = t2.id;
-- 强制使用 Shuffle Join
SELECT /*+ SHUFFLE(t1, t2) */ *
FROM table1 t1
JOIN table2 t2 ON t1.id = t2.id;
-- 禁用物化视图改写
SELECT /*+ NO_REWRITE */ *
FROM user_table;
查询优化深度解析:
1. 谓词下推(Predicate Pushdown)
原理:将过滤条件尽可能推到数据源,减少数据扫描量。
优化前:
-- 扫描全表,然后过滤
SELECT * FROM large_table WHERE age > 18;
-- 执行计划:
-- Scan(large_table) -> Filter(age > 18)
-- 扫描数据:1TB
优化后:
-- 过滤条件下推到 Scan 节点
-- 执行计划:
-- Scan(large_table, predicate: age > 18)
-- 扫描数据:100GB(减少 90%)
谓词下推类型:
| 谓词类型 | 是否下推 | 示例 |
|---|---|---|
| 简单比较 | ✅ | age > 18, name = ‘Alice’ |
| IN 查询 | ✅ | id IN (1, 2, 3) |
| LIKE 前缀匹配 | ✅ | name LIKE ‘A%’ |
| LIKE 模糊匹配 | ❌ | name LIKE ‘%Alice%’ |
| 函数计算 | ❌ | UPPER(name) = ‘ALICE’ |
| OR 条件 | 部分 | age > 18 OR age < 10 |
2. 列裁剪(Column Pruning)
原理:只读取查询需要的列,减少 I/O。
优化前:
-- 读取所有列(100 列)
SELECT id, name FROM wide_table WHERE age > 18;
-- I/O:10GB(所有列)
优化后:
-- 只读取 id, name, age 三列
-- I/O:300MB(减少 97%)
列裁剪优化技巧:
-- 不佳:使用 SELECT *
SELECT * FROM large_table WHERE id = 123;
-- 优化:只选择需要的列
SELECT id, name, age FROM large_table WHERE id = 123;
-- 不佳:子查询中使用 SELECT *
SELECT a.id, a.name
FROM (SELECT * FROM large_table) a
WHERE a.age > 18;
-- 优化:子查询中只选择需要的列
SELECT a.id, a.name
FROM (SELECT id, name, age FROM large_table) a
WHERE a.age > 18;
3. 分区裁剪(Partition Pruning)
原理:根据分区列的过滤条件,只扫描相关分区。
分区裁剪示例:
-- 表有 365 个分区(按天分区)
CREATE TABLE sales_data (
order_id BIGINT,
order_date DATE,
amount DECIMAL(18, 2)
)
PARTITION BY RANGE(order_date) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- 查询只扫描 1 个分区
SELECT * FROM sales_data
WHERE order_date = '2024-01-01';
-- 扫描数据:1/365 = 0.27%
-- 查询扫描 7 个分区
SELECT * FROM sales_data
WHERE order_date >= '2024-01-01'
AND order_date < '2024-01-08';
-- 扫描数据:7/365 = 1.9%
动态分区裁剪:
-- 启用动态分区裁剪
SET enable_partition_prune = true;
SET enable_dynamic_partition_prune = true;
-- Join 时动态裁剪
SELECT f.*, d.date_name
FROM fact_table f
JOIN dim_date d ON f.date_id = d.date_id
WHERE d.year = 2024 AND d.month = 1;
-- 优化器会根据 dim_date 的过滤条件
-- 动态裁剪 fact_table 的分区
4. Join 重排序(Join Reordering)
原理:选择最优的 Join 顺序,减少中间结果集大小。
Join 顺序选择:
-- 三表 Join
SELECT *
FROM table1 t1 -- 1000 万行
JOIN table2 t2 ON t1.id = t2.id -- 100 万行
JOIN table3 t3 ON t2.id = t3.id -- 10 万行
-- 方案 1:(t1 JOIN t2) JOIN t3
-- 中间结果:500 万行
-- 总成本:高
-- 方案 2:(t2 JOIN t3) JOIN t1
-- 中间结果:5 万行
-- 总成本:低(优化器选择)
Join 重排序规则:
- 小表优先:先 Join 小表,减少中间结果
- 过滤条件优先:先 Join 有过滤条件的表
- 索引优先:先 Join 有索引的表
5. 子查询优化(Subquery Optimization)
子查询展开:
-- 原始查询(IN 子查询)
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);
-- 优化后(Semi Join)
SELECT u.* FROM users u
SEMI JOIN orders o ON u.id = o.user_id
WHERE o.amount > 1000;
子查询类型优化:
| 子查询类型 | 优化策略 | 性能提升 |
|---|---|---|
| IN 子查询 | 转换为 Semi Join | 10-100x |
| EXISTS 子查询 | 转换为 Semi Join | 10-100x |
| 标量子查询 | 转换为 Left Join | 5-50x |
| 相关子查询 | 去相关化 | 10-100x |
6. 外连接消除(Outer Join Elimination)
原理:将 Outer Join 转换为 Inner Join,减少数据扫描。
-- 原始查询(LEFT JOIN)
SELECT a.id, a.name, b.amount
FROM users a
LEFT JOIN orders b ON a.id = b.user_id
WHERE b.status = 'completed';
-- 优化后(INNER JOIN)
-- WHERE 条件过滤了 b.status,说明 b 必须存在
SELECT a.id, a.name, b.amount
FROM users a
INNER JOIN orders b ON a.id = b.user_id
WHERE b.status = 'completed';
7. 常量折叠(Constant Folding)
原理:编译时计算常量表达式。
-- 原始查询
SELECT * FROM users
WHERE age > 10 + 8 AND status = CONCAT('act', 'ive');
-- 优化后
SELECT * FROM users
WHERE age > 18 AND status = 'active';
8. 表达式简化(Expression Simplification)
-- 原始查询
SELECT * FROM users
WHERE age > 18 AND age > 20;
-- 优化后
SELECT * FROM users
WHERE age > 20;
-- 原始查询
SELECT * FROM users
WHERE (age > 18 OR age > 20) AND city = 'Beijing';
-- 优化后
SELECT * FROM users
WHERE age > 18 AND city = 'Beijing';
9. Limit 下推(Limit Pushdown)
原理:将 LIMIT 下推到数据源,减少数据传输。
-- 原始查询
SELECT * FROM large_table
ORDER BY id
LIMIT 10;
-- 优化后
-- 每个 BE 只返回 Top 10
-- FE 再从所有 BE 的结果中选择 Top 10
10. 聚合下推(Aggregate Pushdown)
原理:将聚合操作下推到数据源,减少数据传输。
-- 原始查询
SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id;
-- 优化后(两阶段聚合)
-- Stage 1:每个 BE 本地预聚合
-- Stage 2:FE 全局聚合
查询优化最佳实践:
- 使用 EXPLAIN 分析执行计划
- 收集统计信息(ANALYZE TABLE)
- 合理使用索引(前缀索引、BloomFilter、倒排索引)
- **避免 SELECT ***
- 使用分区裁剪
- 使用物化视图加速查询
- 合理使用 Join Hint
- 避免复杂的子查询
AI 增强功能
Apache Doris 4.x 版本的核心亮点是 AI 增强功能,将数据库与 AI 深度融合,成为大模型时代的"超级外挂"。
向量检索能力
为什么需要向量检索?
在 RAG(检索增强生成)架构中,向量检索是核心环节:
传统 RAG 架构的痛点:
| 组件 | 职责 | 问题 |
|---|---|---|
| MySQL | 存储元数据 | 无法存储向量 |
| Elasticsearch | 文本检索 | 不支持向量检索 |
| Milvus/Pinecone | 向量检索 | 需要额外维护 |
| 问题 | 数据分散在多个系统 | 架构复杂、一致性难保证 |
Doris 统一解决方案:
向量检索实战:
1. 创建向量表:
CREATE TABLE knowledge_base (
doc_id BIGINT,
title VARCHAR(200),
content TEXT,
category VARCHAR(50),
create_time DATETIME,
embedding ARRAY<FLOAT> -- 1536 维向量(OpenAI text-embedding-3-small)
)
DUPLICATE KEY(doc_id)
DISTRIBUTED BY HASH(doc_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3"
);
-- 创建 HNSW 向量索引
ALTER TABLE knowledge_base
ADD INDEX embedding_idx(embedding) USING HNSW
PROPERTIES (
"metric_type" = "COSINE", -- 余弦相似度
"M" = "16",
"ef_construction" = "200"
);
2. 插入向量数据:
-- 假设已通过 Embedding 模型生成向量
INSERT INTO knowledge_base VALUES
(1, 'Apache Doris 简介', 'Doris 是一个高性能实时分析数据库...', 'Database', NOW(),
[0.123, 0.456, ..., 0.789]), -- 1536 维向量
(2, 'Doris 向量检索', 'Doris 4.x 支持 HNSW 向量索引...', 'AI', NOW(),
[0.234, 0.567, ..., 0.890]);
3. 向量相似度检索:
-- 查询最相似的 Top 10 文档
SELECT doc_id, title, category,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS similarity
FROM knowledge_base
ORDER BY similarity DESC
LIMIT 10;
4. 混合检索(向量 + 标量过滤):
-- 先过滤类别,再向量检索
SELECT doc_id, title,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS similarity
FROM knowledge_base
WHERE category = 'AI' -- 标量过滤
AND create_time >= '2024-01-01'
ORDER BY similarity DESC
LIMIT 10;
Doris 向量检索的优势:
| 优势 | 说明 |
|---|---|
| 极速性能 | MPP 并行计算,亿级向量毫秒级查询 |
| SQL 交互 | 无需学习新 API,直接用 SQL |
| 混合查询 | 一条 SQL 同时支持向量检索和标量过滤 |
| 架构简化 | 无需额外的向量数据库 |
| 实时性 | 数据写入即可查询,无需同步 |
性能对比:
测试场景:1 亿向量(1536 维),查询 Top 10
Milvus(专用向量数据库):
├── 查询延迟:20-30ms
├── QPS:500-1000
└── 需要额外维护
Doris(统一数据库):
├── 查询延迟:10-50ms
├── QPS:1000-2000(MPP 并行)
└── 无需额外组件
全文检索与 BM25
全文检索 + 向量检索 = 混合检索
高质量的 RAG 系统通常采用 混合检索(Hybrid Search) 策略:
- 向量检索:语义相似度召回
- 关键词检索:精确匹配召回
- 结果融合:综合排序
为什么需要混合检索?
| 场景 | 向量检索 | 关键词检索 |
|---|---|---|
| 语义理解 | ✅ 优秀 | ❌ 较差 |
| 专有名词 | ❌ 较差 | ✅ 优秀 |
| 精确匹配 | ❌ 较差 | ✅ 优秀 |
| 模糊查询 | ✅ 优秀 | ❌ 较差 |
混合检索实战:
1. 创建倒排索引:
-- 为 title 和 content 创建倒排索引
ALTER TABLE knowledge_base
ADD INDEX title_idx(title) USING INVERTED PROPERTIES("parser" = "chinese");
ALTER TABLE knowledge_base
ADD INDEX content_idx(content) USING INVERTED PROPERTIES(
"parser" = "chinese",
"support_phrase" = "true"
);
2. 关键词检索:
-- 关键词召回
SELECT doc_id, title,
BM25_SCORE(content, 'Apache Doris') AS keyword_score
FROM knowledge_base
WHERE content MATCH 'Apache Doris'
ORDER BY keyword_score DESC
LIMIT 20;
3. 向量检索:
-- 向量召回
SELECT doc_id, title,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS vector_score
FROM knowledge_base
ORDER BY vector_score DESC
LIMIT 20;
4. 混合检索(一条 SQL 完成):
-- 混合检索 + 结果融合
WITH keyword_recall AS (
SELECT doc_id, title, content,
BM25_SCORE(content, 'Apache Doris') AS keyword_score
FROM knowledge_base
WHERE content MATCH 'Apache Doris'
ORDER BY keyword_score DESC
LIMIT 20
),
vector_recall AS (
SELECT doc_id, title, content,
COSINE_DISTANCE(embedding, [0.1, 0.2, ..., 0.5]) AS vector_score
FROM knowledge_base
ORDER BY vector_score DESC
LIMIT 20
)
SELECT
COALESCE(k.doc_id, v.doc_id) AS doc_id,
COALESCE(k.title, v.title) AS title,
COALESCE(k.content, v.content) AS content,
COALESCE(k.keyword_score, 0) * 0.4 + COALESCE(v.vector_score, 0) * 0.6 AS final_score
FROM keyword_recall k
FULL OUTER JOIN vector_recall v ON k.doc_id = v.doc_id
ORDER BY final_score DESC
LIMIT 10;
BM25 算法详解:
BM25(Best Matching 25)是目前最流行的文本相关性评分算法。
BM25 公式:
BM25(D, Q) = Σ IDF(qi) * (f(qi, D) * (k1 + 1)) / (f(qi, D) + k1 * (1 - b + b * |D| / avgdl))
参数说明:
- IDF(qi): 逆文档频率,衡量词的重要性
- f(qi, D): 词 qi 在文档 D 中的词频
- |D|: 文档 D 的长度
- avgdl: 平均文档长度
- k1: 词频饱和度参数(默认 1.2)
- b: 长度归一化参数(默认 0.75)
BM25 调优:
-- 调整 BM25 参数
ALTER TABLE knowledge_base SET (
"bm25_k1" = "1.5", -- 增大 k1,词频影响更大
"bm25_b" = "0.8" -- 增大 b,长度归一化更强
);
AI 函数集成
Doris 支持在数据库内直接调用 AI 模型,实现 In-Database AI。
AI 函数架构:
AI 函数示例:
1. 文本 Embedding:
-- 创建 UDF 调用 Embedding 模型
CREATE FUNCTION text_embedding(VARCHAR text)
RETURNS ARRAY<FLOAT>
PROPERTIES (
"symbol" = "embedding_udf",
"type" = "JAVA_UDF",
"file" = "http://your-server/embedding-udf.jar"
);
-- 批量生成 Embedding
INSERT INTO knowledge_base (doc_id, title, content, embedding)
SELECT doc_id, title, content,
text_embedding(content) AS embedding -- 调用 AI 函数
FROM raw_documents;
2. 文本分类:
-- 创建文本分类 UDF
CREATE FUNCTION classify_text(VARCHAR text)
RETURNS VARCHAR
PROPERTIES (
"symbol" = "classify_udf",
"type" = "JAVA_UDF",
"file" = "http://your-server/classify-udf.jar"
);
-- 批量分类
UPDATE knowledge_base
SET category = classify_text(content)
WHERE category IS NULL;
3. 文本摘要:
-- 创建摘要生成 UDF
CREATE FUNCTION summarize_text(VARCHAR text, INT max_length)
RETURNS VARCHAR
PROPERTIES (
"symbol" = "summarize_udf",
"type" = "JAVA_UDF",
"file" = "http://your-server/summarize-udf.jar"
);
-- 生成摘要
SELECT doc_id, title,
summarize_text(content, 200) AS summary
FROM knowledge_base;
In-Database AI 的优势:
| 优势 | 说明 |
|---|---|
| 数据不出库 | 数据在数据库内处理,更安全 |
| 简化流程 | 无需 ETL,直接 SQL 调用 |
| 批量处理 | 利用 MPP 并行处理大批量数据 |
| 实时更新 | 数据变更时自动触发 AI 处理 |
RAG 系统构建
完整 RAG 系统架构:
RAG 系统实现:
1. 数据准备:
-- 创建知识库表
CREATE TABLE rag_knowledge_base (
chunk_id BIGINT,
doc_id BIGINT,
doc_title VARCHAR(200),
chunk_text TEXT,
chunk_index INT,
metadata JSON,
create_time DATETIME,
embedding ARRAY<FLOAT>
)
DUPLICATE KEY(chunk_id)
DISTRIBUTED BY HASH(chunk_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3"
);
-- 创建索引
ALTER TABLE rag_knowledge_base
ADD INDEX embedding_idx(embedding) USING HNSW
PROPERTIES ("metric_type" = "COSINE", "M" = "16");
ALTER TABLE rag_knowledge_base
ADD INDEX text_idx(chunk_text) USING INVERTED
PROPERTIES ("parser" = "chinese");
2. 混合检索:
-- 混合检索函数
CREATE VIEW rag_search AS
WITH vector_results AS (
SELECT chunk_id, doc_title, chunk_text,
COSINE_DISTANCE(embedding, :query_embedding) AS vector_score
FROM rag_knowledge_base
ORDER BY vector_score DESC
LIMIT 50
),
keyword_results AS (
SELECT chunk_id, doc_title, chunk_text,
BM25_SCORE(chunk_text, :query_text) AS keyword_score
FROM rag_knowledge_base
WHERE chunk_text MATCH :query_text
ORDER BY keyword_score DESC
LIMIT 50
)
SELECT
COALESCE(v.chunk_id, k.chunk_id) AS chunk_id,
COALESCE(v.doc_title, k.doc_title) AS doc_title,
COALESCE(v.chunk_text, k.chunk_text) AS chunk_text,
COALESCE(v.vector_score, 0) * 0.6 + COALESCE(k.keyword_score, 0) * 0.4 AS final_score
FROM vector_results v
FULL OUTER JOIN keyword_results k ON v.chunk_id = k.chunk_id
ORDER BY final_score DESC
LIMIT 10;
3. 应用层调用:
import pymysql
import openai
# 连接 Doris
conn = pymysql.connect(
host='doris-fe',
port=9030,
user='root',
password='',
database='rag_db'
)
def rag_query(question: str) -> str:
# 1. 生成问题的 Embedding
query_embedding = openai.Embedding.create(
model="text-embedding-3-small",
input=question
)['data'][0]['embedding']
# 2. 混合检索
cursor = conn.cursor()
cursor.execute("""
SELECT chunk_text FROM rag_search
WHERE query_embedding = %s AND query_text = %s
""", (query_embedding, question))
contexts = [row[0] for row in cursor.fetchall()]
# 3. 构建 Prompt
prompt = f"""
基于以下上下文回答问题:
上下文:
{chr(10).join(contexts)}
问题:{question}
答案:
"""
# 4. 调用大模型生成答案
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
return response['choices'][0]['message']['content']
# 使用示例
answer = rag_query("Apache Doris 的向量检索性能如何?")
print(answer)
RAG 系统优化:
| 优化点 | 方法 | 效果 |
|---|---|---|
| 检索质量 | 混合检索(向量 + 关键词) | 召回率提升 20-30% |
| 检索速度 | HNSW 索引 + MPP 并行 | 延迟降低 10-100 倍 |
| 上下文质量 | 文档分块 + 重排序 | 答案准确率提升 15-25% |
| 成本控制 | 缓存常见问题 | 成本降低 50-70% |
Doris RAG 的核心优势:
- 架构极简:一个数据库搞定所有(结构化 + 非结构化 + 向量)
- 高性能:MPP 并行计算,亿级向量毫秒级查询
- 实时性:数据写入即可查询,无需同步
- 成本低:减少组件数量,降低运维复杂度
数据导入与同步
导入方式对比
Doris 支持多种数据导入方式,适用于不同的场景。
导入方式对比:
| 导入方式 | 适用场景 | 数据量 | 延迟 | 吞吐量 | 事务性 |
|---|---|---|---|---|---|
| Stream Load | 实时小批量导入 | MB-GB | 秒级 | 中等 | 支持 |
| Broker Load | 离线大批量导入 | GB-TB | 分钟级 | 高 | 支持 |
| Routine Load | Kafka 实时消费 | 持续流式 | 秒级 | 高 | 支持 |
| Insert Into | SQL 插入 | 小批量 | 秒级 | 低 | 支持 |
| Flink Doris Connector | 实时流式写入 | 持续流式 | 秒级 | 很高 | 支持 |
导入流程对比:
Stream Load
Stream Load 是最常用的实时导入方式,通过 HTTP 接口提交数据。
基本用法:
# 导入 CSV 文件
curl --location-trusted -u root: \
-H "label:stream_load_20240310_001" \
-H "column_separator:," \
-T data.csv \
http://fe_host:8030/api/db_name/table_name/_stream_load
# 导入 JSON 文件
curl --location-trusted -u root: \
-H "label:stream_load_20240310_002" \
-H "format:json" \
-T data.json \
http://fe_host:8030/api/db_name/table_name/_stream_load
高级参数:
curl --location-trusted -u root: \
-H "label:stream_load_20240310_003" \
-H "column_separator:," \
-H "columns:id,name,age,city" \
-H "where:age > 18" \
-H "max_filter_ratio:0.1" \
-H "timeout:3600" \
-H "strict_mode:true" \
-T data.csv \
http://fe_host:8030/api/db_name/table_name/_stream_load
参数说明:
| 参数 | 说明 | 默认值 |
|---|---|---|
| label | 导入任务唯一标识(幂等性保证) | 必填 |
| column_separator | 列分隔符 | \t |
| columns | 列映射关系 | 按顺序映射 |
| where | 过滤条件 | 无 |
| max_filter_ratio | 最大容错率 | 0 |
| timeout | 超时时间(秒) | 600 |
| strict_mode | 严格模式(类型转换失败则报错) | false |
列转换:
# 列转换和计算
curl --location-trusted -u root: \
-H "label:stream_load_20240310_004" \
-H "columns:id,name,age_str,city,age=cast(age_str as int)" \
-T data.csv \
http://fe_host:8030/api/db_name/table_name/_stream_load
导入 JSON:
# 简单 JSON
curl --location-trusted -u root: \
-H "label:stream_load_20240310_005" \
-H "format:json" \
-H "strip_outer_array:true" \
-T data.json \
http://fe_host:8030/api/db_name/table_name/_stream_load
# 嵌套 JSON
curl --location-trusted -u root: \
-H "label:stream_load_20240310_006" \
-H "format:json" \
-H "jsonpaths:[\"$.id\", \"$.user.name\", \"$.user.age\"]" \
-H "columns:id,name,age" \
-T data.json \
http://fe_host:8030/api/db_name/table_name/_stream_load
返回结果:
{
"TxnId": 123456,
"Label": "stream_load_20240310_001",
"Status": "Success",
"Message": "OK",
"NumberTotalRows": 10000,
"NumberLoadedRows": 9950,
"NumberFilteredRows": 50,
"NumberUnselectedRows": 0,
"LoadBytes": 1048576,
"LoadTimeMs": 1234,
"BeginTxnTimeMs": 10,
"StreamLoadPutTimeMs": 100,
"ReadDataTimeMs": 500,
"WriteDataTimeMs": 600
}
Python 示例:
import requests
import pandas as pd
def stream_load(df: pd.DataFrame, table: str):
# 转换为 CSV
csv_data = df.to_csv(index=False, header=False)
# 提交 Stream Load
url = f"http://fe_host:8030/api/db_name/{table}/_stream_load"
headers = {
"label": f"stream_load_{int(time.time())}",
"column_separator": ",",
"columns": ",".join(df.columns)
}
response = requests.put(
url,
data=csv_data.encode('utf-8'),
headers=headers,
auth=("root", "")
)
result = response.json()
if result['Status'] == 'Success':
print(f"导入成功:{result['NumberLoadedRows']} 行")
else:
print(f"导入失败:{result['Message']}")
# 使用示例
df = pd.DataFrame({
'id': [1, 2, 3],
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35]
})
stream_load(df, 'user_table')
Broker Load
Broker Load 用于从 HDFS、S3 等外部存储导入大批量数据。
基本用法:
-- 从 HDFS 导入
LOAD LABEL db_name.label_20240310_001
(
DATA INFILE("hdfs://namenode:9000/data/user_data.csv")
INTO TABLE user_table
COLUMNS TERMINATED BY ","
(id, name, age, city)
)
WITH BROKER "hdfs_broker"
PROPERTIES
(
"timeout" = "3600",
"max_filter_ratio" = "0.1"
);
-- 从 S3 导入
LOAD LABEL db_name.label_20240310_002
(
DATA INFILE("s3://bucket/data/*.csv")
INTO TABLE user_table
COLUMNS TERMINATED BY ","
)
WITH BROKER "s3_broker"
(
"aws.s3.access_key" = "your_access_key",
"aws.s3.secret_key" = "your_secret_key",
"aws.s3.endpoint" = "s3.amazonaws.com"
)
PROPERTIES
(
"timeout" = "7200"
);
导入 Parquet 文件:
LOAD LABEL db_name.label_20240310_003
(
DATA INFILE("hdfs://namenode:9000/data/*.parquet")
INTO TABLE user_table
FORMAT AS "parquet"
(id, name, age, city)
)
WITH BROKER "hdfs_broker";
分区导入:
LOAD LABEL db_name.label_20240310_004
(
DATA INFILE("hdfs://namenode:9000/data/20240310/*.csv")
INTO TABLE sales_data
PARTITION (p20240310)
COLUMNS TERMINATED BY ","
)
WITH BROKER "hdfs_broker";
查看导入状态:
-- 查看导入任务
SHOW LOAD WHERE LABEL = 'label_20240310_001';
-- 查看导入详情
SHOW LOAD FROM db_name WHERE LABEL = 'label_20240310_001'\G
-- 取消导入任务
CANCEL LOAD FROM db_name WHERE LABEL = 'label_20240310_001';
Routine Load
Routine Load 用于从 Kafka 持续消费数据,实现实时数据同步。
创建 Routine Load 任务:
CREATE ROUTINE LOAD db_name.routine_load_user_events ON user_events
COLUMNS(event_id, user_id, event_type, event_time, properties)
PROPERTIES
(
"desired_concurrent_number" = "3",
"max_batch_interval" = "20",
"max_batch_rows" = "300000",
"max_batch_size" = "104857600",
"strict_mode" = "false"
)
FROM KAFKA
(
"kafka_broker_list" = "broker1:9092,broker2:9092,broker3:9092",
"kafka_topic" = "user_events",
"kafka_partitions" = "0,1,2,3",
"kafka_offsets" = "OFFSET_BEGINNING",
"property.group.id" = "doris_routine_load_group",
"property.client.id" = "doris_routine_load_client"
);
参数说明:
| 参数 | 说明 | 推荐值 |
|---|---|---|
| desired_concurrent_number | 并发消费任务数 | Kafka 分区数 |
| max_batch_interval | 最大批次间隔(秒) | 10-20 |
| max_batch_rows | 最大批次行数 | 100000-500000 |
| max_batch_size | 最大批次大小(字节) | 100MB-500MB |
| strict_mode | 严格模式 | false |
消费 JSON 数据:
CREATE ROUTINE LOAD db_name.routine_load_json_events ON json_events
COLUMNS(id, name, age, city)
PROPERTIES
(
"format" = "json",
"jsonpaths" = "[\"$.id\", \"$.name\", \"$.age\", \"$.city\"]",
"strip_outer_array" = "true"
)
FROM KAFKA
(
"kafka_broker_list" = "broker1:9092",
"kafka_topic" = "json_events"
);
管理 Routine Load:
-- 查看任务状态
SHOW ROUTINE LOAD FOR db_name.routine_load_user_events\G
-- 暂停任务
PAUSE ROUTINE LOAD FOR db_name.routine_load_user_events;
-- 恢复任务
RESUME ROUTINE LOAD FOR db_name.routine_load_user_events;
-- 停止任务
STOP ROUTINE LOAD FOR db_name.routine_load_user_events;
-- 修改任务配置
ALTER ROUTINE LOAD FOR db_name.routine_load_user_events
PROPERTIES
(
"desired_concurrent_number" = "5"
);
监控 Routine Load:
-- 查看消费进度
SELECT
Name,
State,
DataSourceType,
CurrentTaskNum,
JobProperties,
DataSourceProperties,
LatestSourceBatch,
LatestSinkBatch,
TotalRows,
TotalBytes,
ErrorRows
FROM information_schema.routine_loads
WHERE Name = 'routine_load_user_events';
Flink Doris Connector
Flink Doris Connector 是 Doris 官方提供的 Flink 连接器,支持高性能流式写入和批量读取。
Maven 依赖配置
<dependencies>
<!-- Flink Doris Connector -->
<dependency>
<groupId>org.apache.doris</groupId>
<artifactId>flink-doris-connector-1.16</artifactId>
<version>1.5.2</version>
</dependency>
<!-- Flink 依赖 -->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java</artifactId>
<version>1.16.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table-api-java-bridge</artifactId>
<version>1.16.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
版本对应关系:
| Flink 版本 | Connector 版本 | Doris 版本 |
|---|---|---|
| 1.14.x | flink-doris-connector-1.14 | 1.2+ |
| 1.15.x | flink-doris-connector-1.15 | 1.2+ |
| 1.16.x | flink-doris-connector-1.16 | 2.0+ |
| 1.17.x | flink-doris-connector-1.17 | 2.0+ |
| 1.18.x | flink-doris-connector-1.18 | 2.1+ |
Flink SQL 方式
1. 创建 Doris Sink 表
-- 基础配置
CREATE TABLE doris_sink (
user_id BIGINT,
username STRING,
age INT,
city STRING,
register_time TIMESTAMP(3),
PRIMARY KEY (user_id) NOT ENFORCED
) WITH (
'connector' = 'doris',
'fenodes' = 'fe1:8030,fe2:8030,fe3:8030',
'table.identifier' = 'db_name.user_table',
'username' = 'root',
'password' = '',
'sink.label-prefix' = 'doris_flink_sink'
);
-- 高级配置
CREATE TABLE doris_sink_advanced (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(18, 2),
order_time TIMESTAMP(3),
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'doris',
'fenodes' = 'fe1:8030,fe2:8030,fe3:8030',
'table.identifier' = 'db_name.order_table',
'username' = 'root',
'password' = '',
-- Stream Load 配置
'sink.properties.format' = 'json',
'sink.properties.read_json_by_line' = 'true',
'sink.properties.strip_outer_array' = 'true',
-- 批次配置
'sink.buffer-flush.max-rows' = '500000',
'sink.buffer-flush.max-bytes' = '524288000', -- 500MB
'sink.buffer-flush.interval' = '10s',
-- 性能配置
'sink.max-retries' = '3',
'sink.enable-2pc' = 'true', -- 开启两阶段提交
'sink.enable-delete' = 'false'
);
2. 从 Kafka 读取并写入 Doris
-- 创建 Kafka Source 表
CREATE TABLE kafka_source (
user_id BIGINT,
username STRING,
age INT,
city STRING,
event_time TIMESTAMP(3),
WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'user_events',
'properties.bootstrap.servers' = 'kafka1:9092,kafka2:9092',
'properties.group.id' = 'flink_doris_group',
'scan.startup.mode' = 'latest-offset',
'format' = 'json'
);
-- 实时写入 Doris
INSERT INTO doris_sink
SELECT
user_id,
username,
age,
city,
event_time AS register_time
FROM kafka_source;
3. 聚合后写入 Doris
-- 实时统计每分钟的订单数和金额
INSERT INTO doris_sink_agg
SELECT
TUMBLE_START(order_time, INTERVAL '1' MINUTE) AS window_start,
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM kafka_order_source
GROUP BY
TUMBLE(order_time, INTERVAL '1' MINUTE),
user_id;
4. 创建 Doris Source 表(读取)
CREATE TABLE doris_source (
user_id BIGINT,
username STRING,
age INT,
city STRING
) WITH (
'connector' = 'doris',
'fenodes' = 'fe1:8030',
'table.identifier' = 'db_name.user_table',
'username' = 'root',
'password' = '',
-- 读取配置
'doris.request.retries' = '3',
'doris.request.connect.timeout' = '30s',
'doris.request.read.timeout' = '30s',
'doris.batch.size' = '4096',
'doris.exec.mem.limit' = '8GB',
'doris.deserialize.arrow.async' = 'true',
'doris.deserialize.queue.size' = '64'
);
-- 读取 Doris 数据
SELECT * FROM doris_source WHERE age > 18;
Flink DataStream API 方式
1. 基础写入示例
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.doris.flink.cfg.DorisExecutionOptions;
import org.apache.doris.flink.cfg.DorisOptions;
import org.apache.doris.flink.sink.DorisSink;
import org.apache.doris.flink.sink.writer.SimpleStringSerializer;
import java.util.Properties;
public class FlinkDorisBasicExample {
public static void main(String[] args) throws Exception {
// 创建执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);
// 模拟数据源
DataStream<String> dataStream = env.fromElements(
"{\"user_id\":1,\"username\":\"Alice\",\"age\":25,\"city\":\"Beijing\"}",
"{\"user_id\":2,\"username\":\"Bob\",\"age\":30,\"city\":\"Shanghai\"}",
"{\"user_id\":3,\"username\":\"Charlie\",\"age\":35,\"city\":\"Guangzhou\"}"
);
// 配置 Doris 连接
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030,fe2:8030")
.setTableIdentifier("db_name.user_table")
.setUsername("root")
.setPassword("")
.build();
// 配置执行参数
Properties streamLoadProps = new Properties();
streamLoadProps.put("format", "json");
streamLoadProps.put("read_json_by_line", "true");
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("flink_doris_basic")
.setStreamLoadProp(streamLoadProps)
.setBufferFlushMaxRows(100000)
.setBufferFlushMaxBytes(100 * 1024 * 1024) // 100MB
.setBufferFlushInterval(10000) // 10秒
.setMaxRetries(3)
.build();
// 创建 Doris Sink
DorisSink<String> dorisSink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
// 写入 Doris
dataStream.sinkTo(dorisSink).name("Doris Sink");
// 执行任务
env.execute("Flink Doris Basic Example");
}
}
2. 自定义 POJO 写入
import org.apache.flink.api.common.serialization.SerializationSchema;
import org.apache.doris.flink.sink.writer.serializer.DorisRecordSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
// 定义 POJO
public class UserEvent {
public Long userId;
public String username;
public Integer age;
public String city;
public Long timestamp;
// 构造函数、getter、setter
}
// 自定义序列化器
public class UserEventSerializer implements DorisRecordSerializer<UserEvent> {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public byte[] serialize(UserEvent record) throws IOException {
// 转换为 JSON 字符串
String json = objectMapper.writeValueAsString(record);
return json.getBytes("UTF-8");
}
}
// 使用示例
public class FlinkDorisPojoExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 创建 POJO 数据流
DataStream<UserEvent> dataStream = env.fromElements(
new UserEvent(1L, "Alice", 25, "Beijing", System.currentTimeMillis()),
new UserEvent(2L, "Bob", 30, "Shanghai", System.currentTimeMillis())
);
// 配置 Doris Sink
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030")
.setTableIdentifier("db_name.user_table")
.setUsername("root")
.setPassword("")
.build();
Properties streamLoadProps = new Properties();
streamLoadProps.put("format", "json");
streamLoadProps.put("read_json_by_line", "true");
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("flink_doris_pojo")
.setStreamLoadProp(streamLoadProps)
.setBufferFlushMaxRows(100000)
.setBufferFlushMaxBytes(100 * 1024 * 1024)
.setBufferFlushInterval(10000)
.build();
// 使用自定义序列化器
DorisSink<UserEvent> dorisSink = DorisSink.<UserEvent>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new UserEventSerializer())
.build();
dataStream.sinkTo(dorisSink);
env.execute("Flink Doris POJO Example");
}
}
3. CDC 实时同步示例
import com.ververica.cdc.connectors.mysql.source.MySqlSource;
import com.ververica.cdc.connectors.mysql.table.StartupOptions;
import com.ververica.cdc.debezium.JsonDebeziumDeserializationSchema;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
public class FlinkDorisCDCExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(10000); // 10秒 checkpoint
// 配置 MySQL CDC Source
MySqlSource<String> mySqlSource = MySqlSource.<String>builder()
.hostname("mysql_host")
.port(3306)
.databaseList("source_db")
.tableList("source_db.user_table")
.username("root")
.password("password")
.startupOptions(StartupOptions.initial()) // 全量 + 增量
.deserializer(new JsonDebeziumDeserializationSchema())
.build();
// 读取 MySQL CDC 数据
DataStream<String> cdcStream = env
.fromSource(mySqlSource, WatermarkStrategy.noWatermarks(), "MySQL CDC Source")
.setParallelism(1);
// 数据转换(提取 after 字段)
DataStream<String> transformedStream = cdcStream
.map(json -> {
JSONObject obj = JSON.parseObject(json);
String op = obj.getString("op");
if ("c".equals(op) || "u".equals(op)) {
// INSERT 或 UPDATE
return obj.getJSONObject("after").toJSONString();
} else if ("d".equals(op)) {
// DELETE(需要配置 enable-delete)
return obj.getJSONObject("before").toJSONString();
}
return null;
})
.filter(Objects::nonNull);
// 配置 Doris Sink
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030")
.setTableIdentifier("target_db.user_table")
.setUsername("root")
.setPassword("")
.build();
Properties streamLoadProps = new Properties();
streamLoadProps.put("format", "json");
streamLoadProps.put("read_json_by_line", "true");
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("flink_doris_cdc")
.setStreamLoadProp(streamLoadProps)
.setBufferFlushMaxRows(50000)
.setBufferFlushMaxBytes(50 * 1024 * 1024)
.setBufferFlushInterval(5000)
.setMaxRetries(3)
.setEnable2PC(true) // 开启两阶段提交,保证 Exactly-Once
.build();
DorisSink<String> dorisSink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
transformedStream.sinkTo(dorisSink);
env.execute("Flink Doris CDC Example");
}
}
4. 从 Doris 读取数据
import org.apache.doris.flink.cfg.DorisReadOptions;
import org.apache.doris.flink.source.DorisSource;
import org.apache.doris.flink.source.reader.DorisRecordEmitter;
import org.apache.doris.flink.deserialization.SimpleListDeserializationSchema;
public class FlinkDorisReadExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 配置 Doris Source
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030")
.setTableIdentifier("db_name.user_table")
.setUsername("root")
.setPassword("")
.build();
DorisReadOptions readOptions = DorisReadOptions.builder()
.setDeserializeArrowAsync(true)
.setDeserializeQueueSize(64)
.setExecMemLimit(8L * 1024 * 1024 * 1024) // 8GB
.setRequestQueryTimeoutS(3600)
.setRequestRetries(3)
.build();
// 创建 Doris Source
DorisSource<List<?>> dorisSource = DorisSource.<List<?>>builder()
.setDorisOptions(dorisOptions)
.setDorisReadOptions(readOptions)
.setDeserializer(new SimpleListDeserializationSchema())
.build();
// 读取数据
DataStream<List<?>> dataStream = env
.fromSource(dorisSource, WatermarkStrategy.noWatermarks(), "Doris Source");
// 处理数据
dataStream.map(row -> {
Long userId = (Long) row.get(0);
String username = (String) row.get(1);
Integer age = (Integer) row.get(2);
String city = (String) row.get(3);
return String.format("User: %s, Age: %d, City: %s", username, age, city);
}).print();
env.execute("Flink Doris Read Example");
}
}
5. 性能优化配置
public class FlinkDorisOptimizedExample {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 环境配置优化
env.setParallelism(8); // 根据 CPU 核数设置
env.enableCheckpointing(60000); // 1分钟 checkpoint
env.getCheckpointConfig().setCheckpointTimeout(300000); // 5分钟超时
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000); // 最小间隔 30秒
// Doris 配置优化
Properties streamLoadProps = new Properties();
streamLoadProps.put("format", "json");
streamLoadProps.put("read_json_by_line", "true");
streamLoadProps.put("load_to_single_tablet", "false"); // 并行写入多个 Tablet
streamLoadProps.put("timeout", "600"); // 10分钟超时
streamLoadProps.put("max_filter_ratio", "0.1"); // 容错率 10%
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("flink_doris_optimized")
.setStreamLoadProp(streamLoadProps)
// 批次大小优化(根据数据量调整)
.setBufferFlushMaxRows(500000) // 50万行
.setBufferFlushMaxBytes(500 * 1024 * 1024) // 500MB
.setBufferFlushInterval(5000) // 5秒
// 重试和容错
.setMaxRetries(5)
.setEnable2PC(true) // Exactly-Once 语义
// 性能优化
.setBufferCount(3) // 缓冲区数量
.setCheckInterval(10000) // 检查间隔
.build();
DorisSink<String> dorisSink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
dataStream.sinkTo(dorisSink);
env.execute("Flink Doris Optimized Example");
}
}
常见配置参数
Sink 配置参数:
| 参数 | 说明 | 默认值 | 推荐值 |
|---|---|---|---|
| sink.buffer-flush.max-rows | 最大缓冲行数 | 50000 | 100000-500000 |
| sink.buffer-flush.max-bytes | 最大缓冲字节数 | 10MB | 100MB-500MB |
| sink.buffer-flush.interval | 刷新间隔(毫秒) | 10000 | 5000-10000 |
| sink.max-retries | 最大重试次数 | 3 | 3-5 |
| sink.enable-2pc | 开启两阶段提交 | false | true(Exactly-Once) |
| sink.enable-delete | 支持删除操作 | false | 根据需求 |
Source 配置参数:
| 参数 | 说明 | 默认值 | 推荐值 |
|---|---|---|---|
| doris.request.retries | 请求重试次数 | 3 | 3 |
| doris.request.connect.timeout | 连接超时 | 30s | 30s |
| doris.request.read.timeout | 读取超时 | 30s | 60s |
| doris.batch.size | 批次大小 | 1024 | 4096 |
| doris.exec.mem.limit | 内存限制 | 2GB | 4GB-8GB |
| doris.deserialize.arrow.async | 异步反序列化 | false | true |
监控与调优
1. Flink Web UI 监控
访问 Flink Web UI:http://flink_jobmanager:8081
关键指标:
├── numRecordsOut:输出记录数
├── numRecordsOutPerSecond:输出速率
├── currentSendTime:当前发送时间
├── totalFlushBytes:总刷新字节数
└── totalFlushRows:总刷新行数
2. Doris 监控
-- 查看 Stream Load 任务
SHOW STREAM LOAD FROM db_name WHERE LABEL LIKE 'flink_doris%' ORDER BY CreateTime DESC LIMIT 10;
-- 查看导入统计
SELECT
LABEL,
STATE,
LoadStartTime,
LoadFinishTime,
NumberTotalRows,
NumberLoadedRows,
NumberFilteredRows,
LoadBytes,
LoadTimeMs
FROM information_schema.loads
WHERE LABEL LIKE 'flink_doris%'
ORDER BY LoadStartTime DESC
LIMIT 20;
Rowset 视角补充:
- 真实链路不是“一个 Flink 批次对应一个 Rowset”,而是
Flink Sink 攒批 -> 触发 Stream Load -> Doris 按分桶键分发 -> 每个被写入的 Tablet 分别生成 Rowset。 - 因此要看的是 Rowset 生成速率,它更接近
Flush 频率 × 本批次命中的 Bucket 数。如果flush.interval太小、分桶又很多,Rowset 会快速堆积,最终把问题放大到查询变慢、Compaction 拖延、磁盘 IO 打满。 - 业务上常见误区是只盯写入 TPS,不看碎片数量;实际上 写得进 ≠ 系统稳定,关键是后台合并速度能否持续追平前台写入速度。
Compaction 关键阈值:
| 观测项 | 查看方式 | 经验阈值 | 风险信号 |
|---|---|---|---|
| RowsetNum | SHOW TABLET FROM db.table; | < 100 | > 200 进入高风险区 |
| CompactionScore | SHOW PROC '/backends'; | < 20 | > 50 表示有压力,> 100 常见于堆积 |
| 导入状态 | SHOW LOAD; / SHOW STREAM LOAD | 以 FINISHED / Success 为主 | 重试、超时、过滤行持续升高 |
| BE 资源 | iostat -x 1 / top / 监控平台 | 磁盘 util < 80% | 磁盘长期打满、CPU 抖动 |
调参原则:
- 延迟优先场景可以缩短
sink.buffer-flush.interval,但需要接受更高的 Rowset 与 Compaction 压力。 - 稳定性优先场景应尽量把单次 Flush 控制在 50MB 到 200MB,刷新间隔控制在 10 秒到 60 秒。
sink.parallelism通常按 BE 节点数的 1 到 2 倍起步,不宜盲目拉高;并发更高不一定更快,很多时候只是在更快地产生碎片。
排障口诀:
写慢看写入,查慢看 Rowset,Rowset 多看合并,合并慢看 IO,IO 不够加机器,频率太高调 Flush,Bucket 不均查分布,热点问题换 Key。
资料来源:融合自 raw 文档《Doris 数据插入与排序》,原始整理链接见 原文链接。
3. 性能调优建议
// 根据数据特征调整参数
// 场景 1:高吞吐量(大批量数据)
DorisExecutionOptions.builder()
.setBufferFlushMaxRows(1000000) // 100万行
.setBufferFlushMaxBytes(1024 * 1024 * 1024) // 1GB
.setBufferFlushInterval(30000) // 30秒
.build();
// 场景 2:低延迟(实时性要求高)
DorisExecutionOptions.builder()
.setBufferFlushMaxRows(10000) // 1万行
.setBufferFlushMaxBytes(10 * 1024 * 1024) // 10MB
.setBufferFlushInterval(1000) // 1秒
.build();
// 场景 3:平衡模式(推荐)
DorisExecutionOptions.builder()
.setBufferFlushMaxRows(100000) // 10万行
.setBufferFlushMaxBytes(100 * 1024 * 1024) // 100MB
.setBufferFlushInterval(10000) // 10秒
.build();
4. 常见问题排查
// 问题 1:写入速度慢
// 解决方案:增大批次大小,减少刷新频率
.setBufferFlushMaxRows(500000)
.setBufferFlushInterval(30000)
// 问题 2:内存溢出
// 解决方案:减小批次大小,增加并行度
.setBufferFlushMaxBytes(50 * 1024 * 1024)
env.setParallelism(16)
// 问题 3:数据丢失
// 解决方案:开启两阶段提交
.setEnable2PC(true)
env.enableCheckpointing(60000)
// 问题 4:连接超时
// 解决方案:增加超时时间,增加重试次数
streamLoadProps.put("timeout", "1200") // 20分钟
.setMaxRetries(5)
Flink Doris 连接优化配置详解
优化配置全景图:
1. 连接层优化配置
FE 连接配置:
DorisOptions dorisOptions = DorisOptions.builder()
// FE 节点配置(多个 FE 实现负载均衡和高可用)
.setFenodes("fe1:8030,fe2:8030,fe3:8030")
// 表标识
.setTableIdentifier("db_name.table_name")
// 认证信息
.setUsername("root")
.setPassword("password")
// JDBC URL(可选,用于元数据查询)
.setJdbcUrl("jdbc:mysql://fe1:9030/db_name")
// 自动重定向(FE 故障自动切换)
.setAutoRedirect(true)
// 连接池配置
.setMaxConnectionPoolSize(20) // 最大连接数
.setMinConnectionPoolSize(5) // 最小连接数
.build();
连接参数对比:
| 参数 | 说明 | 默认值 | 推荐值 | 影响 |
|---|---|---|---|---|
| fenodes | FE 节点列表 | 必填 | 3个以上 | 高可用 |
| autoRedirect | 自动重定向 | true | true | 故障切换 |
| maxConnectionPoolSize | 最大连接数 | 10 | 20-50 | 并发能力 |
| minConnectionPoolSize | 最小连接数 | 1 | 5-10 | 连接复用 |
2. 写入层优化配置
批次大小优化:
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
// ========== 批次大小配置 ==========
// 最大批次行数(触发刷新的行数阈值)
.setBufferFlushMaxRows(500000) // 推荐:100000-1000000
// 最大批次字节数(触发刷新的字节数阈值)
.setBufferFlushMaxBytes(500 * 1024 * 1024) // 推荐:100MB-1GB
// 刷新间隔(毫秒,定时刷新)
.setBufferFlushInterval(10000) // 推荐:5000-30000
// 缓冲区数量(多缓冲区并行写入)
.setBufferCount(3) // 推荐:2-5
// 检查间隔(毫秒,检查是否需要刷新)
.setCheckInterval(3000) // 推荐:1000-5000
.build();
批次大小选择策略:
| 场景 | 行数 | 字节数 | 间隔 | 吞吐量 | 延迟 |
|---|---|---|---|---|---|
| 高吞吐 | 1000000 | 1GB | 30s | 很高 | 高 |
| 平衡 | 500000 | 500MB | 10s | 高 | 中等 |
| 低延迟 | 100000 | 100MB | 5s | 中等 | 低 |
| 实时 | 10000 | 10MB | 1s | 低 | 很低 |
Stream Load 属性优化:
Properties streamLoadProps = new Properties();
// ========== 数据格式配置 ==========
streamLoadProps.put("format", "json"); // json/csv
streamLoadProps.put("read_json_by_line", "true"); // 按行读取 JSON
streamLoadProps.put("strip_outer_array", "true"); // 去除外层数组
// ========== 性能优化配置 ==========
// 并行写入多个 Tablet(重要!)
streamLoadProps.put("load_to_single_tablet", "false");
// 超时时间(秒)
streamLoadProps.put("timeout", "600"); // 推荐:300-1200
// 最大容错率(允许的错误行比例)
streamLoadProps.put("max_filter_ratio", "0.1"); // 推荐:0.01-0.2
// 严格模式(类型转换失败是否报错)
streamLoadProps.put("strict_mode", "false"); // 推荐:false
// ========== 内存配置 ==========
// 单次导入的内存限制(字节)
streamLoadProps.put("exec_mem_limit", "8589934592"); // 8GB
// ========== 压缩配置 ==========
// 传输压缩(减少网络传输)
streamLoadProps.put("compress_type", "gz"); // none/gz/lz4/bz2
// ========== 事务配置 ==========
// 事务超时时间(秒)
streamLoadProps.put("load_mem_limit", "0"); // 0 表示不限制
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setStreamLoadProp(streamLoadProps)
.build();
Stream Load 参数详解:
| 参数 | 说明 | 默认值 | 推荐值 | 性能影响 |
|---|---|---|---|---|
| load_to_single_tablet | 是否写入单个 Tablet | true | false | 并行度 ↑↑↑ |
| timeout | 超时时间(秒) | 600 | 600-1200 | 稳定性 ↑ |
| max_filter_ratio | 最大容错率 | 0 | 0.01-0.1 | 容错性 ↑ |
| exec_mem_limit | 内存限制(字节) | 2GB | 4GB-8GB | 大批次支持 ↑ |
| compress_type | 压缩类型 | none | gz/lz4 | 网络传输 ↓↓ |
并发控制优化:
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
// ========== 重试配置 ==========
// 最大重试次数
.setMaxRetries(5) // 推荐:3-5
// ========== 两阶段提交(Exactly-Once)==========
// 开启两阶段提交(保证精确一次语义)
.setEnable2PC(true) // 推荐:true(生产环境)
// ========== 删除支持 ==========
// 支持删除操作(CDC 场景)
.setEnableDelete(false) // 根据需求设置
// ========== Label 配置 ==========
// Label 前缀(用于幂等性保证)
.setLabelPrefix("flink_doris_" + System.currentTimeMillis())
// ========== 异步刷新 ==========
// 异步刷新(提高吞吐量)
.setFlushQueueSize(2) // 推荐:2-5
.build();
// ========== Flink Checkpoint 配置 ==========
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// Checkpoint 间隔(毫秒)
env.enableCheckpointing(60000); // 推荐:30000-120000
// Checkpoint 超时时间(毫秒)
env.getCheckpointConfig().setCheckpointTimeout(600000); // 10分钟
// 最小 Checkpoint 间隔(毫秒)
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000); // 30秒
// 最大并发 Checkpoint 数量
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// Checkpoint 模式(Exactly-Once)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// 任务取消时保留 Checkpoint
env.getCheckpointConfig().setExternalizedCheckpointCleanup(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION
);
并发配置对比:
| 配置项 | 低并发 | 中并发 | 高并发 | 说明 |
|---|---|---|---|---|
| 并行度 | 2-4 | 8-16 | 32-64 | 根据 CPU 核数 |
| bufferCount | 2 | 3 | 5 | 缓冲区数量 |
| maxRetries | 3 | 3 | 5 | 重试次数 |
| checkpointInterval | 120s | 60s | 30s | Checkpoint 间隔 |
3. 读取层优化配置
Doris Source 优化:
DorisReadOptions readOptions = DorisReadOptions.builder()
// ========== 查询配置 ==========
// 请求重试次数
.setRequestRetries(3) // 推荐:3
// 连接超时时间
.setRequestConnectTimeoutMs(30000) // 30秒
// 读取超时时间
.setRequestReadTimeoutMs(60000) // 60秒
// 查询超时时间(秒)
.setRequestQueryTimeoutS(3600) // 1小时
// ========== 批次配置 ==========
// 批次大小(每次读取的行数)
.setRequestBatchSize(4096) // 推荐:1024-8192
// Tablet 大小(字节,用于分片)
.setRequestTabletSize(1024 * 1024 * 1024) // 1GB
// ========== 内存配置 ==========
// 查询内存限制(字节)
.setExecMemLimit(8L * 1024 * 1024 * 1024) // 8GB
// ========== 反序列化优化 ==========
// 异步反序列化 Arrow 数据(重要!)
.setDeserializeArrowAsync(true) // 推荐:true
// 反序列化队列大小
.setDeserializeQueueSize(64) // 推荐:32-128
// ========== 过滤下推 ==========
// 启用过滤条件下推
.setFilterQuery(true) // 推荐:true
.build();
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030")
.setTableIdentifier("db_name.table_name")
.setUsername("root")
.setPassword("")
.build();
DorisSource<List<?>> dorisSource = DorisSource.<List<?>>builder()
.setDorisOptions(dorisOptions)
.setDorisReadOptions(readOptions)
.setDeserializer(new SimpleListDeserializationSchema())
.build();
读取优化参数详解:
| 参数 | 说明 | 默认值 | 推荐值 | 性能影响 |
|---|---|---|---|---|
| requestBatchSize | 批次大小 | 1024 | 4096-8192 | 吞吐量 ↑↑ |
| execMemLimit | 内存限制 | 2GB | 4GB-8GB | 大查询支持 ↑ |
| deserializeArrowAsync | 异步反序列化 | false | true | CPU 利用率 ↑↑ |
| deserializeQueueSize | 队列大小 | 64 | 64-128 | 并发度 ↑ |
| filterQuery | 过滤下推 | false | true | 数据传输 ↓↓ |
分区和列裁剪:
-- Flink SQL 自动支持分区裁剪和列裁剪
CREATE TABLE doris_source (
user_id BIGINT,
username STRING,
age INT,
city STRING,
create_date DATE
) WITH (
'connector' = 'doris',
'fenodes' = 'fe1:8030',
'table.identifier' = 'db_name.user_table',
'username' = 'root',
'password' = '',
-- 读取优化配置
'doris.request.retries' = '3',
'doris.request.read.timeout' = '60s',
'doris.batch.size' = '4096',
'doris.exec.mem.limit' = '8GB',
'doris.deserialize.arrow.async' = 'true',
'doris.deserialize.queue.size' = '64'
);
-- 列裁剪:只读取需要的列
SELECT user_id, username FROM doris_source;
-- 分区裁剪:只读取指定分区
SELECT * FROM doris_source WHERE create_date >= '2024-01-01';
-- 过滤下推:过滤条件下推到 Doris
SELECT * FROM doris_source WHERE age > 18 AND city = 'Beijing';
4. 网络层优化配置
网络传输优化:
Properties streamLoadProps = new Properties();
// ========== 压缩配置 ==========
// 数据压缩(减少网络传输量)
streamLoadProps.put("compress_type", "lz4"); // none/gz/lz4/bz2/zstd
// 压缩级别(gz/zstd 支持)
streamLoadProps.put("compress_level", "6"); // 1-9
// ========== 超时配置 ==========
// HTTP 连接超时(秒)
streamLoadProps.put("http.connect.timeout", "30");
// HTTP 读取超时(秒)
streamLoadProps.put("http.socket.timeout", "600");
// Stream Load 超时(秒)
streamLoadProps.put("timeout", "600");
// ========== 重试配置 ==========
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
// 最大重试次数
.setMaxRetries(5)
// 重试间隔(毫秒)
.setRetryInterval(1000) // 1秒
// 指数退避
.setRetryBackoffMultiplier(2.0) // 每次重试间隔翻倍
// 最大重试间隔(毫秒)
.setMaxRetryInterval(60000) // 60秒
.build();
压缩算法对比:
| 压缩算法 | 压缩比 | 压缩速度 | 解压速度 | 网络节省 | CPU 开销 | 推荐场景 |
|---|---|---|---|---|---|---|
| none | 1x | - | - | 0% | 无 | 内网高速网络 |
| lz4 | 2-3x | 很快 | 很快 | 50-70% | 低 | 推荐 |
| gz | 3-5x | 中等 | 快 | 70-80% | 中等 | 跨机房传输 |
| zstd | 3-5x | 快 | 快 | 70-80% | 中等 | 平衡选择 |
| bz2 | 4-6x | 慢 | 慢 | 75-85% | 高 | 不推荐 |
5. 完整优化配置示例
生产环境推荐配置:
public class FlinkDorisProductionConfig {
public static void main(String[] args) throws Exception {
// ========== Flink 环境配置 ==========
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 并行度(根据 CPU 核数设置)
env.setParallelism(16);
// Checkpoint 配置
env.enableCheckpointing(60000); // 1分钟
env.getCheckpointConfig().setCheckpointTimeout(600000); // 10分钟
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000); // 30秒
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
// ========== Doris 连接配置 ==========
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030,fe2:8030,fe3:8030") // 多 FE 高可用
.setTableIdentifier("db_name.table_name")
.setUsername("root")
.setPassword("password")
.setJdbcUrl("jdbc:mysql://fe1:9030/db_name")
.setAutoRedirect(true)
.build();
// ========== Stream Load 属性配置 ==========
Properties streamLoadProps = new Properties();
// 数据格式
streamLoadProps.put("format", "json");
streamLoadProps.put("read_json_by_line", "true");
streamLoadProps.put("strip_outer_array", "true");
// 性能优化
streamLoadProps.put("load_to_single_tablet", "false"); // 并行写入
streamLoadProps.put("timeout", "600");
streamLoadProps.put("max_filter_ratio", "0.05"); // 5% 容错率
streamLoadProps.put("exec_mem_limit", "8589934592"); // 8GB
// 压缩传输
streamLoadProps.put("compress_type", "lz4");
// ========== 执行选项配置 ==========
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
// Label 配置
.setLabelPrefix("flink_doris_prod_" + System.currentTimeMillis())
// 批次配置(平衡模式)
.setBufferFlushMaxRows(500000) // 50万行
.setBufferFlushMaxBytes(500 * 1024 * 1024) // 500MB
.setBufferFlushInterval(10000) // 10秒
// 缓冲区配置
.setBufferCount(3)
.setCheckInterval(3000)
// 重试配置
.setMaxRetries(5)
// 两阶段提交(Exactly-Once)
.setEnable2PC(true)
// Stream Load 属性
.setStreamLoadProp(streamLoadProps)
.build();
// ========== 创建 Doris Sink ==========
DorisSink<String> dorisSink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
// ========== 数据流处理 ==========
dataStream.sinkTo(dorisSink).name("Doris Sink");
env.execute("Flink Doris Production Job");
}
}
不同场景的配置模板:
// ========== 场景 1:高吞吐量(离线批处理)==========
DorisExecutionOptions highThroughput = DorisExecutionOptions.builder()
.setBufferFlushMaxRows(2000000) // 200万行
.setBufferFlushMaxBytes(2048 * 1024 * 1024) // 2GB
.setBufferFlushInterval(60000) // 60秒
.setBufferCount(5)
.setEnable2PC(false) // 关闭两阶段提交,提高性能
.build();
// ========== 场景 2:低延迟(实时大屏)==========
DorisExecutionOptions lowLatency = DorisExecutionOptions.builder()
.setBufferFlushMaxRows(10000) // 1万行
.setBufferFlushMaxBytes(10 * 1024 * 1024) // 10MB
.setBufferFlushInterval(1000) // 1秒
.setBufferCount(2)
.setEnable2PC(true)
.build();
// ========== 场景 3:CDC 实时同步 ==========
DorisExecutionOptions cdcSync = DorisExecutionOptions.builder()
.setBufferFlushMaxRows(50000) // 5万行
.setBufferFlushMaxBytes(50 * 1024 * 1024) // 50MB
.setBufferFlushInterval(5000) // 5秒
.setBufferCount(3)
.setEnable2PC(true) // 必须开启
.setEnableDelete(true) // 支持删除
.build();
// ========== 场景 4:大宽表写入 ==========
DorisExecutionOptions wideTable = DorisExecutionOptions.builder()
.setBufferFlushMaxRows(100000) // 10万行
.setBufferFlushMaxBytes(1024 * 1024 * 1024) // 1GB
.setBufferFlushInterval(15000) // 15秒
.setBufferCount(3)
.setEnable2PC(true)
.build();
Properties wideTableProps = new Properties();
wideTableProps.put("exec_mem_limit", "17179869184"); // 16GB
wideTableProps.put("timeout", "1200"); // 20分钟
6. 性能调优检查清单
配置检查清单:
- FE 配置:是否配置多个 FE 节点(高可用)
- 并行写入:
load_to_single_tablet是否设置为false - 批次大小:是否根据数据量合理设置(100MB-500MB)
- 压缩传输:是否启用 lz4 压缩(跨机房必须)
- 两阶段提交:生产环境是否开启
enable2PC - Checkpoint:是否配置合理的 Checkpoint 间隔(30s-120s)
- 并行度:是否根据 CPU 核数设置并行度
- 内存限制:
exec_mem_limit是否足够(4GB-8GB) - 超时时间:是否根据数据量设置合理超时(10-20分钟)
- 重试次数:是否配置重试(3-5次)
- 异步反序列化:读取时是否开启
deserializeArrowAsync - 容错率:
max_filter_ratio是否合理(1%-10%)
性能监控指标:
-- 监控 Stream Load 性能
SELECT
LABEL,
LoadTimeMs / 1000 AS LoadTimeSec,
LoadBytes / 1024 / 1024 AS LoadMB,
LoadBytes / LoadTimeMs * 1000 / 1024 / 1024 AS ThroughputMBps,
NumberTotalRows,
NumberLoadedRows,
NumberFilteredRows,
NumberFilteredRows * 100.0 / NumberTotalRows AS FilterRatio
FROM information_schema.loads
WHERE LABEL LIKE 'flink_doris%'
ORDER BY LoadStartTime DESC
LIMIT 20;
性能基准:
| 指标 | 良好 | 一般 | 需优化 |
|---|---|---|---|
| 吞吐量 | > 500 MB/s | 200-500 MB/s | < 200 MB/s |
| 延迟 | < 5s | 5-30s | > 30s |
| 过滤率 | < 1% | 1%-5% | > 5% |
| 重试率 | < 1% | 1%-5% | > 5% |
| CPU 使用率 | 60%-80% | 40%-60% | < 40% 或 > 90% |
| 内存使用率 | 60%-80% | 40%-60% | > 90% |
导入性能对比:
测试场景:1 亿行数据(10GB)
Stream Load(单线程):
├── 导入时间:30-60 分钟
└── 吞吐量:30-60 MB/s
Broker Load(并行):
├── 导入时间:5-10 分钟
└── 吞吐量:200-400 MB/s
Flink Doris Connector(并行):
├── 导入时间:3-5 分钟
└── 吞吐量:400-800 MB/s
└── 吞吐量:400-800 MB/s
**导入最佳实践**:
**1. 批量大小选择**:
| 数据量 | 推荐方式 | 批次大小 |
|--------|----------|----------|
| **< 1MB** | Insert Into | 1000-10000 行 |
| **1MB-100MB** | Stream Load | 10MB-100MB |
| **100MB-10GB** | Stream Load | 100MB-500MB |
| **> 10GB** | Broker Load | 1GB-10GB |
**2. 并发导入优化**:
```python
import concurrent.futures
import requests
def stream_load_batch(batch_data, batch_id):
url = f"http://fe_host:9030/api/db/table/_stream_load"
headers = {
"label": f"stream_load_{batch_id}",
"column_separator": ","
}
response = requests.put(
url,
data=batch_data,
headers=headers,
auth=("root", "")
)
return response.json()
# 并发导入
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = []
for i, batch in enumerate(data_batches):
future = executor.submit(stream_load_batch, batch, i)
futures.append(future)
for future in concurrent.futures.as_completed(futures):
result = future.result()
print(f"导入结果:{result['Status']}")
3. 错误处理与重试:
import time
def stream_load_with_retry(data, max_retries=3):
for attempt in range(max_retries):
try:
result = stream_load(data)
if result['Status'] == 'Success':
return result
elif result['Status'] == 'Fail':
# 检查错误类型
if 'timeout' in result['Message'].lower():
# 超时错误,重试
time.sleep(2 ** attempt) # 指数退避
continue
else:
# 其他错误,不重试
raise Exception(result['Message'])
except Exception as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
raise Exception("导入失败,已达最大重试次数")
4. 数据质量检查:
def validate_and_load(df):
# 1. 数据类型检查
assert df['id'].dtype == 'int64', "id 列类型错误"
assert df['amount'].dtype == 'float64', "amount 列类型错误"
# 2. 空值检查
null_counts = df.isnull().sum()
if null_counts.any():
print(f"警告:存在空值\n{null_counts}")
# 3. 重复值检查
duplicate_count = df.duplicated(subset=['id']).sum()
if duplicate_count > 0:
print(f"警告:存在 {duplicate_count} 条重复数据")
df = df.drop_duplicates(subset=['id'])
# 4. 数据范围检查
assert df['amount'].min() >= 0, "amount 不能为负数"
# 5. 导入数据
return stream_load(df)
5. 导入监控:
def monitor_load_progress(label):
while True:
cursor = doris_conn.cursor()
cursor.execute(f"""
SHOW LOAD WHERE LABEL = '{label}'
""")
result = cursor.fetchone()
state = result[2] # State 列
if state == 'FINISHED':
print(f"导入成功:{label}")
break
elif state == 'CANCELLED':
print(f"导入失败:{label}")
break
else:
print(f"导入中:{label}, 状态:{state}")
time.sleep(5)
6. 导入性能调优:
# Stream Load 性能调优
curl --location-trusted -u root: \
-H "label:stream_load_001" \
-H "max_filter_ratio:0.1" \
-H "timeout:3600" \
-H "strict_mode:false" \
-H "load_mem_limit:2147483648" \ # 2GB 内存限制
-H "exec_mem_limit:4294967296" \ # 4GB 执行内存
-T large_file.csv \
http://fe_host:8030/api/db/table/_stream_load
7. 实时 CDC 同步:
MySQL CDC 同步示例:
// Flink CDC + Doris Connector
import com.ververica.cdc.connectors.mysql.source.MySqlSource;
import org.apache.doris.flink.sink.DorisSink;
// 创建 MySQL CDC Source
MySqlSource<String> mySqlSource = MySqlSource.<String>builder()
.hostname("mysql_host")
.port(3306)
.databaseList("db_name")
.tableList("db_name.user_table")
.username("root")
.password("password")
.deserializer(new JsonDebeziumDeserializationSchema())
.build();
// 创建 Doris Sink
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030,fe2:8030")
.setTableIdentifier("db_name.user_table")
.setUsername("root")
.setPassword("")
.build();
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("cdc_sync")
.setStreamLoadProp(Properties.builder()
.put("format", "json")
.put("read_json_by_line", "true")
.build())
.setBufferFlushMaxRows(100000)
.setBufferFlushInterval(5000)
.build();
DorisSink<String> sink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
// 执行同步
env.fromSource(mySqlSource, WatermarkStrategy.noWatermarks(), "MySQL CDC")
.sinkTo(sink);
env.execute("MySQL CDC to Doris");
8. 数据转换与清洗:
-- Stream Load 时进行数据转换
curl --location-trusted -u root: \
-H "label:stream_load_transform" \
-H "columns:id,name,age_str,city,age=cast(age_str as int),create_time=now()" \
-H "where:age > 0 and age < 150" \
-T data.csv \
http://fe_host:8030/api/db/table/_stream_load
9. 分区导入优化:
-- 导入到指定分区
LOAD LABEL db_name.label_20240310
(
DATA INFILE("hdfs://namenode:9000/data/20240310/*.csv")
INTO TABLE sales_data
PARTITION (p20240310) -- 指定分区
COLUMNS TERMINATED BY ","
)
WITH BROKER "hdfs_broker";
10. 导入故障排查:
-- 查看导入任务详情
SHOW LOAD FROM db_name WHERE LABEL = 'label_20240310'\G
-- 查看导入错误日志
SHOW LOAD WARNINGS ON 'label_20240310';
-- 常见错误及解决方案:
-- 1. "too many filtered rows"
-- 解决:调整 max_filter_ratio 或检查数据质量
-- 2. "tablet writer write failed"
-- 解决:检查磁盘空间、BE 状态
-- 3. "timeout"
-- 解决:增大 timeout 参数或减小批次大小
-- 4. "memory limit exceeded"
-- 解决:增大 load_mem_limit 或减小批次大小
物化视图
物化视图是 Doris 的核心功能之一,用于加速查询和预聚合数据。
物化视图类型
Doris 支持两种物化视图:
| 类型 | 说明 | 更新方式 | 适用场景 |
|---|---|---|---|
| 同步物化视图 | 数据写入时自动更新 | 实时同步 | 固定聚合查询 |
| 异步物化视图 | 定时或手动刷新 | 异步刷新 | 复杂多表关联 |
同步物化视图 vs 异步物化视图:
创建与使用
1. 同步物化视图
创建聚合物化视图:
-- 基表
CREATE TABLE sales_detail (
order_id BIGINT,
order_date DATE,
user_id BIGINT,
product_id INT,
amount DECIMAL(18, 2),
quantity INT
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(order_date) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- 创建按日期聚合的物化视图
CREATE MATERIALIZED VIEW sales_daily_mv AS
SELECT
order_date,
SUM(amount) AS total_amount,
SUM(quantity) AS total_quantity,
COUNT(DISTINCT user_id) AS user_count
FROM sales_detail
GROUP BY order_date;
-- 创建按用户聚合的物化视图
CREATE MATERIALIZED VIEW sales_user_mv AS
SELECT
user_id,
SUM(amount) AS total_amount,
COUNT(order_id) AS order_count
FROM sales_detail
GROUP BY user_id;
支持的聚合函数:
| 聚合函数 | 说明 |
|---|---|
| SUM | 求和 |
| MIN | 最小值 |
| MAX | 最大值 |
| COUNT | 计数 |
| BITMAP_UNION | Bitmap 去重 |
| HLL_UNION | HyperLogLog 去重 |
查询自动改写:
-- 原始查询
SELECT order_date, SUM(amount) AS total_amount
FROM sales_detail
WHERE order_date >= '2024-01-01'
GROUP BY order_date;
-- 自动改写为(使用物化视图)
SELECT order_date, total_amount
FROM sales_daily_mv
WHERE order_date >= '2024-01-01';
2. 异步物化视图
创建多表关联物化视图:
-- 创建异步物化视图
CREATE MATERIALIZED VIEW user_order_summary_mv
BUILD IMMEDIATE
REFRESH COMPLETE ON MANUAL
AS
SELECT
u.user_id,
u.username,
u.city,
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_amount,
MAX(o.order_date) AS last_order_date
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.username, u.city;
刷新策略:
| 刷新方式 | 说明 | 语法 |
|---|---|---|
| MANUAL | 手动刷新 | REFRESH COMPLETE ON MANUAL |
| SCHEDULE | 定时刷新 | REFRESH COMPLETE ON SCHEDULE EVERY 1 HOUR |
| COMMIT | 数据变更时刷新 | REFRESH COMPLETE ON COMMIT |
定时刷新示例:
CREATE MATERIALIZED VIEW hourly_summary_mv
BUILD IMMEDIATE
REFRESH COMPLETE ON SCHEDULE EVERY 1 HOUR
AS
SELECT
DATE_TRUNC('hour', event_time) AS hour,
event_type,
COUNT(*) AS event_count
FROM user_events
GROUP BY hour, event_type;
手动刷新:
-- 手动刷新物化视图
REFRESH MATERIALIZED VIEW user_order_summary_mv;
-- 查看刷新状态
SHOW MATERIALIZED VIEW user_order_summary_mv;
自动改写
Doris 查询优化器会自动判断是否使用物化视图。
改写规则:
1. 聚合查询改写:
-- 原始查询
SELECT user_id, SUM(amount)
FROM sales_detail
WHERE order_date >= '2024-01-01'
GROUP BY user_id;
-- 自动改写(使用 sales_user_mv)
SELECT user_id, total_amount
FROM sales_user_mv;
2. 上卷查询改写:
-- 物化视图:按天聚合
CREATE MATERIALIZED VIEW sales_daily_mv AS
SELECT order_date, SUM(amount) AS total_amount
FROM sales_detail
GROUP BY order_date;
-- 查询:按月聚合(上卷)
SELECT DATE_TRUNC('month', order_date) AS month, SUM(total_amount)
FROM sales_daily_mv
GROUP BY month;
3. 部分匹配改写:
-- 物化视图
CREATE MATERIALIZED VIEW sales_mv AS
SELECT order_date, user_id, product_id, SUM(amount) AS total_amount
FROM sales_detail
GROUP BY order_date, user_id, product_id;
-- 查询(部分维度)
SELECT order_date, user_id, SUM(total_amount)
FROM sales_mv
GROUP BY order_date, user_id;
查看改写计划:
-- 查看是否使用物化视图
EXPLAIN SELECT order_date, SUM(amount)
FROM sales_detail
GROUP BY order_date;
-- 输出示例
-- OlapScanNode
-- TABLE: sales_detail
-- MATERIALIZED VIEW: sales_daily_mv <-- 使用了物化视图
禁用物化视图改写:
-- 禁用特定查询的物化视图改写
SELECT /*+ NO_REWRITE */ order_date, SUM(amount)
FROM sales_detail
GROUP BY order_date;
-- 禁用表的所有物化视图
SET enable_materialized_view_rewrite = false;
最佳实践
1. 物化视图设计原则
| 原则 | 说明 |
|---|---|
| 高频查询 | 为高频查询创建物化视图 |
| 固定聚合 | 聚合维度固定的查询 |
| 减少计算 | 复杂计算(如 JOIN、聚合)提前完成 |
| 控制数量 | 物化视图不宜过多(影响写入性能) |
2. 同步物化视图 vs 异步物化视图选择
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 单表聚合 | 同步物化视图 | 实时更新,查询快 |
| 多表关联 | 异步物化视图 | 避免写入时关联开销 |
| 实时性要求高 | 同步物化视图 | 数据实时可见 |
| 复杂计算 | 异步物化视图 | 避免影响写入性能 |
3. 物化视图监控
-- 查看所有物化视图
SHOW MATERIALIZED VIEW FROM db_name;
-- 查看物化视图详情
SHOW CREATE MATERIALIZED VIEW sales_daily_mv;
-- 查看物化视图大小
SELECT
TABLE_NAME,
INDEX_NAME,
DATA_SIZE,
ROW_COUNT
FROM information_schema.materialized_views
WHERE TABLE_SCHEMA = 'db_name';
-- 删除物化视图
DROP MATERIALIZED VIEW sales_daily_mv ON sales_detail;
4. 性能对比
查询场景:按日期聚合销售额(1 亿行数据)
无物化视图:
├── 查询时间:10-30 秒
├── 扫描数据:10GB
└── CPU 使用:高
同步物化视图:
├── 查询时间:100-500 毫秒
├── 扫描数据:10MB
└── CPU 使用:低
性能提升:20-100 倍
5. 物化视图更新开销
写入场景:每秒 10000 行
无物化视图:
├── 写入延迟:50ms
└── 吞吐量:10000 行/秒
1 个同步物化视图:
├── 写入延迟:60ms(+20%)
└── 吞吐量:9000 行/秒(-10%)
3 个同步物化视图:
├── 写入延迟:80ms(+60%)
└── 吞吐量:7500 行/秒(-25%)
建议:同步物化视图数量 ≤ 3 个
6. 物化视图优化技巧
技巧 1:使用 Bitmap/HLL 去重
-- 使用 Bitmap 精确去重(适合低基数)
CREATE MATERIALIZED VIEW user_daily_active_mv AS
SELECT
event_date,
BITMAP_UNION(TO_BITMAP(user_id)) AS active_users
FROM user_events
GROUP BY event_date;
-- 查询去重用户数
SELECT event_date, BITMAP_COUNT(active_users) AS uv
FROM user_daily_active_mv;
-- 使用 HLL 近似去重(适合高基数)
CREATE MATERIALIZED VIEW page_view_mv AS
SELECT
page_url,
HLL_UNION(HLL_HASH(user_id)) AS unique_visitors
FROM page_views
GROUP BY page_url;
-- 查询近似去重数
SELECT page_url, HLL_CARDINALITY(unique_visitors) AS uv
FROM page_view_mv;
技巧 2:分区物化视图
-- 基表按日期分区
CREATE TABLE events (
event_id BIGINT,
event_date DATE,
user_id BIGINT,
event_type VARCHAR(50)
)
PARTITION BY RANGE(event_date) ()
DISTRIBUTED BY HASH(event_id) BUCKETS 32;
-- 物化视图继承分区
CREATE MATERIALIZED VIEW events_daily_mv AS
SELECT
event_date,
event_type,
COUNT(*) AS event_count
FROM events
GROUP BY event_date, event_type;
-- 查询时自动分区裁剪
SELECT * FROM events_daily_mv
WHERE event_date >= '2024-01-01';
技巧 3:多级物化视图
-- 一级物化视图:按小时聚合
CREATE MATERIALIZED VIEW sales_hourly_mv AS
SELECT
DATE_TRUNC('hour', order_time) AS hour,
SUM(amount) AS total_amount
FROM sales_detail
GROUP BY hour;
-- 二级物化视图:按天聚合(基于一级物化视图)
CREATE MATERIALIZED VIEW sales_daily_mv AS
SELECT
DATE_TRUNC('day', hour) AS day,
SUM(total_amount) AS total_amount
FROM sales_hourly_mv
GROUP BY day;
技巧 4:物化视图与分区结合
-- 基表按日期分区
CREATE TABLE orders (
order_id BIGINT,
order_date DATE,
user_id BIGINT,
amount DECIMAL(18, 2)
)
PARTITION BY RANGE(order_date) (
PARTITION p20240301 VALUES LESS THAN ("2024-03-02"),
PARTITION p20240302 VALUES LESS THAN ("2024-03-03"),
PARTITION p20240303 VALUES LESS THAN ("2024-03-04")
)
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- 物化视图继承分区
CREATE MATERIALIZED VIEW orders_daily_mv AS
SELECT
order_date,
user_id,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM orders
GROUP BY order_date, user_id;
-- 查询时自动分区裁剪
SELECT * FROM orders_daily_mv
WHERE order_date >= '2024-03-01' AND order_date < '2024-03-03';
-- 只扫描 p20240301 和 p20240302 两个分区
技巧 5:物化视图与索引结合
-- 创建物化视图
CREATE MATERIALIZED VIEW user_summary_mv AS
SELECT
user_id,
username,
SUM(order_amount) AS total_amount,
COUNT(order_id) AS order_count
FROM user_orders
GROUP BY user_id, username;
-- 为物化视图创建 BloomFilter 索引
ALTER TABLE user_orders SET (
"bloom_filter_columns" = "user_id"
);
-- 查询时同时利用物化视图和索引
SELECT * FROM user_summary_mv
WHERE user_id = 12345; -- 使用 BloomFilter 快速定位
技巧 6:物化视图刷新策略优化
-- 异步物化视图:定时刷新
CREATE MATERIALIZED VIEW complex_summary_mv
BUILD IMMEDIATE
REFRESH COMPLETE ON SCHEDULE EVERY 1 HOUR
AS
SELECT
u.user_id,
u.username,
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_amount,
AVG(o.amount) AS avg_amount,
MAX(o.order_date) AS last_order_date
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.username;
-- 手动刷新(数据变更后立即刷新)
REFRESH MATERIALIZED VIEW complex_summary_mv;
-- 查看刷新历史
SHOW MATERIALIZED VIEW complex_summary_mv;
技巧 7:物化视图性能监控
-- 查看物化视图使用情况
SELECT
table_name,
materialized_view_name,
hit_count,
query_count,
hit_rate
FROM information_schema.materialized_view_stats
WHERE table_schema = 'db_name'
ORDER BY hit_rate DESC;
-- 查看物化视图大小
SELECT
table_name,
index_name AS mv_name,
data_size / 1024 / 1024 / 1024 AS size_gb,
row_count
FROM information_schema.materialized_views
WHERE table_schema = 'db_name';
-- 分析物化视图效果
-- 对比使用物化视图前后的查询性能
EXPLAIN COST SELECT * FROM base_table WHERE condition; -- 不使用物化视图
EXPLAIN COST SELECT * FROM mv_table WHERE condition; -- 使用物化视图
物化视图实战案例:
案例 1:电商实时销售大屏
-- 场景:实时展示销售数据,查询 QPS 1000+
-- 基表:订单明细(日增 1 亿行)
CREATE TABLE order_detail (
order_id BIGINT,
order_time DATETIME,
user_id BIGINT,
product_id INT,
category VARCHAR(50),
region VARCHAR(50),
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id, order_time)
PARTITION BY RANGE(order_time) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 64;
-- 物化视图 1:按小时 + 地区聚合
CREATE MATERIALIZED VIEW order_hourly_region_mv AS
SELECT
DATE_TRUNC('hour', order_time) AS hour,
region,
SUM(amount) AS total_amount,
COUNT(*) AS order_count,
COUNT(DISTINCT user_id) AS user_count
FROM order_detail
GROUP BY hour, region;
-- 物化视图 2:按小时 + 类目聚合
CREATE MATERIALIZED VIEW order_hourly_category_mv AS
SELECT
DATE_TRUNC('hour', order_time) AS hour,
category,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM order_detail
GROUP BY hour, category;
-- 查询 1:地区销售排行(使用物化视图 1)
SELECT region, SUM(total_amount) AS amount
FROM order_hourly_region_mv
WHERE hour >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY region
ORDER BY amount DESC
LIMIT 10;
-- 查询时间:< 100ms(vs 5-10s 扫描明细)
-- 查询 2:类目销售趋势(使用物化视图 2)
SELECT
DATE_FORMAT(hour, '%H:00') AS time,
category,
SUM(total_amount) AS amount
FROM order_hourly_category_mv
WHERE hour >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY hour, category
ORDER BY hour;
-- 查询时间:< 200ms(vs 10-20s 扫描明细)
案例 2:用户行为分析
-- 场景:分析用户留存、活跃度
-- 基表:用户行为事件(日增 10 亿行)
CREATE TABLE user_events (
event_id BIGINT,
user_id BIGINT,
event_time DATETIME,
event_type VARCHAR(50),
page_url VARCHAR(500)
)
DUPLICATE KEY(event_id, user_id, event_time)
PARTITION BY RANGE(event_time) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 128;
-- 物化视图:每日活跃用户(使用 Bitmap 去重)
CREATE MATERIALIZED VIEW user_daily_active_mv AS
SELECT
DATE(event_time) AS date,
event_type,
BITMAP_UNION(TO_BITMAP(user_id)) AS active_users
FROM user_events
GROUP BY date, event_type;
-- 查询:计算 DAU(日活跃用户数)
SELECT
date,
BITMAP_COUNT(active_users) AS dau
FROM user_daily_active_mv
WHERE date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
ORDER BY date;
-- 查询时间:< 50ms(vs 30-60s 扫描明细)
-- 查询:计算留存率
WITH day0_users AS (
SELECT active_users AS users
FROM user_daily_active_mv
WHERE date = '2024-03-01'
),
day1_users AS (
SELECT active_users AS users
FROM user_daily_active_mv
WHERE date = '2024-03-02'
),
day7_users AS (
SELECT active_users AS users
FROM user_daily_active_mv
WHERE date = '2024-03-08'
)
SELECT
BITMAP_COUNT((SELECT users FROM day0_users)) AS day0_count,
BITMAP_COUNT(BITMAP_INTERSECT((SELECT users FROM day0_users), (SELECT users FROM day1_users))) AS day1_retention,
BITMAP_COUNT(BITMAP_INTERSECT((SELECT users FROM day0_users), (SELECT users FROM day7_users))) AS day7_retention,
BITMAP_COUNT(BITMAP_INTERSECT((SELECT users FROM day0_users), (SELECT users FROM day1_users))) * 100.0 /
BITMAP_COUNT((SELECT users FROM day0_users)) AS day1_retention_rate,
BITMAP_COUNT(BITMAP_INTERSECT((SELECT users FROM day0_users), (SELECT users FROM day7_users))) * 100.0 /
BITMAP_COUNT((SELECT users FROM day0_users)) AS day7_retention_rate;
-- 查询时间:< 100ms(vs 5-10 分钟扫描明细)
案例 3:BI 报表加速
-- 场景:企业 BI 报表,复杂多表关联
-- 基表 1:订单表
CREATE TABLE orders (
order_id BIGINT,
order_date DATE,
user_id BIGINT,
product_id INT,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(order_date) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- 基表 2:用户表
CREATE TABLE users (
user_id BIGINT,
username VARCHAR(100),
city VARCHAR(50),
register_date DATE
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32;
-- 基表 3:商品表
CREATE TABLE products (
product_id INT,
product_name VARCHAR(200),
category VARCHAR(50),
price DECIMAL(18, 2)
)
UNIQUE KEY(product_id)
DISTRIBUTED BY HASH(product_id) BUCKETS 16;
-- 异步物化视图:预关联 + 预聚合
CREATE MATERIALIZED VIEW order_summary_mv
BUILD IMMEDIATE
REFRESH COMPLETE ON SCHEDULE EVERY 1 HOUR
AS
SELECT
o.order_date,
u.city,
p.category,
COUNT(o.order_id) AS order_count,
SUM(o.amount) AS total_amount,
AVG(o.amount) AS avg_amount,
COUNT(DISTINCT o.user_id) AS user_count
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN products p ON o.product_id = p.product_id
GROUP BY o.order_date, u.city, p.category;
-- 查询:城市 + 类目销售分析(使用物化视图)
SELECT
city,
category,
SUM(total_amount) AS amount,
SUM(order_count) AS orders
FROM order_summary_mv
WHERE order_date >= '2024-01-01'
GROUP BY city, category
ORDER BY amount DESC
LIMIT 100;
-- 查询时间:< 500ms(vs 30-60s 三表关联)
物化视图故障排查:
问题 1:物化视图未被使用
-- 排查步骤
-- 1. 查看执行计划
EXPLAIN SELECT * FROM base_table WHERE condition;
-- 2. 检查物化视图是否存在
SHOW MATERIALIZED VIEW FROM db_name;
-- 3. 检查查询是否匹配物化视图
-- 物化视图:GROUP BY a, b
-- 查询:GROUP BY a, b, c -- 不匹配(维度更多)
-- 查询:GROUP BY a -- 匹配(维度更少,可以上卷)
-- 4. 强制使用物化视图
SELECT /*+ USE_MV(mv_name) */ * FROM base_table WHERE condition;
问题 2:物化视图更新慢
-- 排查步骤
-- 1. 查看物化视图刷新状态
SHOW MATERIALIZED VIEW mv_name;
-- 2. 检查基表数据量
SELECT COUNT(*) FROM base_table;
-- 3. 检查物化视图复杂度
-- 复杂的 JOIN 和聚合会导致刷新慢
-- 4. 优化方案
-- 方案 1:增加刷新间隔
ALTER MATERIALIZED VIEW mv_name
REFRESH COMPLETE ON SCHEDULE EVERY 2 HOUR;
-- 方案 2:拆分物化视图
-- 将复杂物化视图拆分成多个简单物化视图
-- 方案 3:使用增量刷新(如果支持)
ALTER MATERIALIZED VIEW mv_name
REFRESH INCREMENTAL ON SCHEDULE EVERY 10 MINUTE;
问题 3:物化视图占用空间过大
-- 排查步骤
-- 1. 查看物化视图大小
SELECT
table_name,
index_name,
data_size / 1024 / 1024 / 1024 AS size_gb
FROM information_schema.materialized_views
WHERE table_schema = 'db_name'
ORDER BY data_size DESC;
-- 2. 分析原因
-- 原因 1:聚合维度过多(接近明细数据)
-- 原因 2:包含大字段(TEXT、JSON)
-- 3. 优化方案
-- 方案 1:减少聚合维度
-- 方案 2:排除大字段
-- 方案 3:使用压缩
ALTER TABLE base_table SET ("compression" = "ZSTD");
性能调优
查询性能优化
1. 索引优化
前缀索引优化:
-- 不佳设计(低基数列在前)
CREATE TABLE user_table (
city VARCHAR(50), -- 低基数
age INT, -- 低基数
user_id BIGINT -- 高基数
)
DUPLICATE KEY(city, age, user_id);
-- 优化设计(高基数列在前)
CREATE TABLE user_table (
user_id BIGINT, -- 高基数
city VARCHAR(50),
age INT
)
DUPLICATE KEY(user_id, city, age);
BloomFilter 索引:
-- 为高基数列创建 BloomFilter
ALTER TABLE user_table SET (
"bloom_filter_columns" = "email,phone,id_card"
);
2. 分区裁剪
-- 启用动态分区裁剪
SET enable_partition_prune = true;
-- 查询时使用分区列过滤
SELECT * FROM sales_data
WHERE order_date >= '2024-01-01'
AND order_date < '2024-02-01'; -- 只扫描 1 月分区
3. 列裁剪
-- 不佳查询(读取所有列)
SELECT * FROM large_table WHERE id = 123;
-- 优化查询(只读取需要的列)
SELECT id, name, age FROM large_table WHERE id = 123;
4. Join 优化
Broadcast Join:
-- 小表 < 10MB,自动使用 Broadcast Join
SELECT /*+ BROADCAST(small_table) */ *
FROM large_table l
JOIN small_table s ON l.id = s.id;
Colocate Join:
-- 创建 Colocate Group
CREATE TABLE fact_table (
user_id BIGINT,
amount DECIMAL(18, 2)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES ("colocate_with" = "user_group");
CREATE TABLE dim_table (
user_id BIGINT,
username VARCHAR(100)
)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES ("colocate_with" = "user_group");
-- Join 无需 Shuffle
SELECT f.*, d.username
FROM fact_table f
JOIN dim_table d ON f.user_id = d.user_id;
5. 聚合优化
预聚合:
-- 使用物化视图预聚合
CREATE MATERIALIZED VIEW sales_summary_mv AS
SELECT
order_date,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM sales_detail
GROUP BY order_date;
两阶段聚合:
-- Doris 自动使用两阶段聚合
-- Stage 1: 本地预聚合
-- Stage 2: 全局聚合
SELECT user_id, SUM(amount)
FROM large_table
GROUP BY user_id;
6. Runtime Filter
-- 启用 Runtime Filter
SET runtime_filter_mode = 'GLOBAL';
SET runtime_filter_wait_time_ms = 1000;
-- Join 时自动生成 Runtime Filter
SELECT l.*, s.name
FROM large_table l
JOIN small_table s ON l.id = s.id
WHERE s.status = 'active';
写入性能优化
1. 批量写入
-- 不佳:单条插入
INSERT INTO user_table VALUES (1, 'Alice', 25);
INSERT INTO user_table VALUES (2, 'Bob', 30);
-- 优化:批量插入
INSERT INTO user_table VALUES
(1, 'Alice', 25),
(2, 'Bob', 30),
(3, 'Charlie', 35);
2. Stream Load 优化
# 增大批次大小
curl --location-trusted -u root: \
-H "label:stream_load_001" \
-H "max_filter_ratio:0.1" \
-H "timeout:3600" \
-T large_file.csv \
http://fe_host:8030/api/db/table/_stream_load
3. 分区分桶优化
-- 合理设置分桶数
-- 分桶数 = 数据量 / 单 Tablet 大小(1-10GB)
CREATE TABLE optimized_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32 -- 根据数据量调整
PROPERTIES (
"replication_num" = "3"
);
4. Compaction 优化
-- 调整 Compaction 参数
ALTER TABLE large_table SET (
"compaction_policy" = "time_series", -- 时序数据优化
"time_series_compaction_goal_size_mbytes" = "1024",
"time_series_compaction_file_count_threshold" = "10"
);
5. 写入并发控制
-- 控制并发导入任务数
SET max_running_txn_num_per_db = 100;
-- 控制单个导入任务的并发度
SET load_parallel_instance_num = 4;
6. 实时写入稳定性补充
- 核心目标不是单纯提高写入频率,而是让 Rowset 生成速度小于等于 Compaction 处理能力。
- 如果你使用 Flink Doris Connector,建议从更保守的参数开始,例如:
sink.buffer-flush.max-bytes=100MB、sink.buffer-flush.interval=30s、sink.buffer-flush.max-rows=500000,再依据RowsetNum与CompactionScore逐步微调。 - 遇到周期性抖动时,不要只看导入链路,还要结合 Base Compaction 是否在固定时段拉高磁盘 IO;很多“写入抖动”本质上是存储层碎片治理跟不上。
- 如果发现某些 Bucket 长期更热,要优先复查 分桶键基数、热点值分布、上游并发 key 偏斜,而不是先把 Flush 调得更碎。
资料来源:融合自 raw 文档《Doris 数据插入与排序》,原始整理链接见 原文链接。
资源管理
1. 内存管理
BE 内存配置:
# be.conf
mem_limit = 80% # BE 可用内存比例
# 查询内存限制
query_mem_limit = 8GB
# Compaction 内存限制
compaction_mem_limit = 4GB
查询内存限制:
-- 设置单个查询的内存限制
SET query_mem_limit = 8589934592; -- 8GB
-- 设置 Scan 节点内存限制
SET scan_queue_mem_limit = 2147483648; -- 2GB
2. CPU 管理
并行度控制:
-- 设置并行扫描线程数
SET parallel_fragment_exec_instance_num = 8;
-- 设置 Pipeline 并行度
SET pipeline_dop = 16;
3. 磁盘管理
多盘配置:
# be.conf
storage_root_path = /data1;/data2;/data3;/data4
磁盘均衡:
-- 查看磁盘使用情况
SHOW PROC '/backends';
-- 手动触发磁盘均衡
ADMIN SET FRONTEND CONFIG ("tablet_rebalancer_type" = "disk_and_tablet");
4. 资源组(Workload Group)
创建资源组:
-- 创建高优先级资源组
CREATE WORKLOAD GROUP high_priority_group
PROPERTIES (
"cpu_share" = "1024",
"memory_limit" = "50%",
"enable_memory_overcommit" = "false"
);
-- 创建低优先级资源组
CREATE WORKLOAD GROUP low_priority_group
PROPERTIES (
"cpu_share" = "256",
"memory_limit" = "30%",
"enable_memory_overcommit" = "true"
);
-- 用户绑定资源组
ALTER USER 'user1' SET PROPERTIES (
"default_workload_group" = "high_priority_group"
);
慢查询分析
1. 查看慢查询
-- 查看正在执行的查询
SHOW PROCESSLIST;
-- 查看慢查询日志
SHOW QUERY PROFILE;
-- 查看查询统计
SELECT
query_id,
query_start_time,
query_time_ms,
scan_rows,
scan_bytes,
cpu_time_ms,
mem_usage_bytes
FROM information_schema.queries_statistics
WHERE query_time_ms > 10000 -- 查询时间 > 10 秒
ORDER BY query_time_ms DESC
LIMIT 20;
2. 查询 Profile 分析
-- 开启 Profile
SET enable_profile = true;
-- 执行查询
SELECT * FROM large_table WHERE id > 1000;
-- 查看 Profile
SHOW QUERY PROFILE '/query_id';
Profile 关键指标:
| 指标 | 说明 | 优化方向 |
|---|---|---|
| ScanTime | 扫描时间 | 索引优化、分区裁剪 |
| ShuffleTime | Shuffle 时间 | Join 优化、Colocate |
| AggregateTime | 聚合时间 | 物化视图、预聚合 |
| MemoryUsage | 内存使用 | 增加内存、减少数据量 |
| NetworkTime | 网络传输时间 | 减少 Shuffle、本地化 |
3. Explain 分析
-- 查看执行计划
EXPLAIN SELECT * FROM large_table WHERE id > 1000;
-- 查看详细执行计划
EXPLAIN VERBOSE SELECT * FROM large_table WHERE id > 1000;
-- 查看成本估算
EXPLAIN COST SELECT * FROM large_table WHERE id > 1000;
4. 常见慢查询优化
场景 1:全表扫描
-- 问题查询
SELECT * FROM large_table WHERE name = 'Alice';
-- 优化方案
-- 1. 为 name 创建 BloomFilter 索引
ALTER TABLE large_table SET ("bloom_filter_columns" = "name");
-- 2. 或者创建物化视图
CREATE MATERIALIZED VIEW name_index_mv AS
SELECT name, id FROM large_table;
场景 2:大表 Join
-- 问题查询
SELECT l.*, r.*
FROM large_table1 l
JOIN large_table2 r ON l.id = r.id;
-- 优化方案
-- 1. 使用 Colocate Join
ALTER TABLE large_table1 SET ("colocate_with" = "join_group");
ALTER TABLE large_table2 SET ("colocate_with" = "join_group");
-- 2. 或者使用 Bucket Shuffle Join
SET enable_bucket_shuffle_join = true;
场景 3:聚合查询慢
-- 问题查询
SELECT order_date, SUM(amount)
FROM sales_detail
GROUP BY order_date;
-- 优化方案:创建物化视图
CREATE MATERIALIZED VIEW sales_daily_mv AS
SELECT order_date, SUM(amount) AS total_amount
FROM sales_detail
GROUP BY order_date;
场景 4:数据倾斜
-- 问题:某个分桶数据量过大
-- 优化方案 1:重新选择分桶键
ALTER TABLE skewed_table
DISTRIBUTED BY HASH(user_id, order_id) BUCKETS 32;
-- 优化方案 2:增加分桶数
ALTER TABLE skewed_table
DISTRIBUTED BY HASH(user_id) BUCKETS 64;
5. 性能监控指标
-- 查看 BE 性能指标
SHOW PROC '/backends';
-- 查看表统计信息
SHOW TABLE STATS table_name;
-- 查看 Tablet 分布
SHOW TABLETS FROM table_name;
-- 查看 Compaction 状态
SHOW PROC '/compactions';
关键性能指标:
| 指标 | 正常范围 | 告警阈值 |
|---|---|---|
| 查询延迟(P99) | < 1s | > 5s |
| QPS | 根据业务 | 下降 50% |
| CPU 使用率 | < 70% | > 90% |
| 内存使用率 | < 80% | > 95% |
| 磁盘使用率 | < 80% | > 90% |
| Compaction 延迟 | < 10min | > 1h |
| Compaction 延迟 | < 10min | > 1h |
6. 查询并发优化
并发控制参数:
-- FE 并发控制
SET max_running_txn_num_per_db = 100; -- 每个数据库最大并发事务数
SET query_queue_max_queued_queries = 1024; -- 查询队列最大长度
-- BE 并发控制
SET parallel_fragment_exec_instance_num = 8; -- 并行扫描实例数
SET pipeline_dop = 16; -- Pipeline 并行度
连接池优化:
// JDBC 连接池配置
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://fe_host:9030/db_name");
config.setUsername("root");
config.setPassword("");
config.setMaximumPoolSize(50); // 最大连接数
config.setMinimumIdle(10); // 最小空闲连接数
config.setConnectionTimeout(30000); // 连接超时 30s
config.setIdleTimeout(600000); // 空闲超时 10min
config.setMaxLifetime(1800000); // 最大生命周期 30min
HikariDataSource ds = new HikariDataSource(config);
查询队列管理:
-- 创建查询队列
CREATE WORKLOAD GROUP high_priority_queue
PROPERTIES (
"cpu_share" = "1024",
"memory_limit" = "50%",
"max_concurrency" = "100",
"max_queue_size" = "1000",
"queue_timeout" = "60000" -- 队列超时 60s
);
-- 用户绑定队列
ALTER USER 'user1' SET PROPERTIES (
"default_workload_group" = "high_priority_queue"
);
7. 存储优化
数据压缩优化:
-- 选择合适的压缩算法
CREATE TABLE compressed_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 16
PROPERTIES (
"compression" = "ZSTD", -- LZ4, ZSTD, Snappy, ZLIB
"compression_level" = "3" -- 压缩级别 1-9
);
压缩算法选择:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 实时查询 | LZ4 | 解压速度最快 |
| 存储成本敏感 | ZSTD | 压缩比高,性能损失小 |
| 冷数据归档 | ZLIB | 压缩比最高 |
| 高吞吐写入 | Snappy | 压缩速度最快 |
数据编码优化:
-- Doris 自动选择最优编码
-- 可以通过 SHOW CREATE TABLE 查看实际编码
-- 常见编码类型:
-- 1. Plain: 无编码(默认)
-- 2. RLE: 重复值编码(适合重复值多的列)
-- 3. Dictionary: 字典编码(适合低基数字符串)
-- 4. Delta: 增量编码(适合递增数值)
-- 5. Bit Packing: 位打包(适合小范围整数)
8. 网络优化
Shuffle 优化:
-- 减少 Shuffle 数据量
-- 1. 使用 Broadcast Join(小表)
SELECT /*+ BROADCAST(small_table) */ *
FROM large_table l
JOIN small_table s ON l.id = s.id;
-- 2. 使用 Colocate Join(数据本地化)
ALTER TABLE table1 SET ("colocate_with" = "group1");
ALTER TABLE table2 SET ("colocate_with" = "group1");
-- 3. 启用 Bucket Shuffle Join
SET enable_bucket_shuffle_join = true;
网络参数调优:
# be.conf
# 网络线程数
brpc_num_threads = 64
# RPC 超时时间
rpc_timeout_ms = 60000
# 数据传输缓冲区大小
streaming_load_rpc_max_alive_time_sec = 1200
9. 内存优化
内存分配策略:
# be.conf
# BE 总内存限制
mem_limit = 80%
# 查询内存限制
query_mem_limit = 8GB
# Compaction 内存限制
compaction_mem_limit = 4GB
# Page Cache 大小
storage_page_cache_limit = 20%
内存监控:
-- 查看内存使用情况
SHOW PROC '/backends';
-- 查看查询内存使用
SELECT
query_id,
mem_usage_bytes,
mem_usage_bytes / 1024 / 1024 / 1024 AS mem_usage_gb
FROM information_schema.queries_statistics
WHERE mem_usage_bytes > 1073741824 -- > 1GB
ORDER BY mem_usage_bytes DESC;
内存泄漏排查:
# 查看 BE 内存使用
curl http://be_host:8040/mem_tracker
# 查看 JVM 内存(FE)
jmap -heap <fe_pid>
# 生成 Heap Dump
jmap -dump:format=b,file=heap.bin <fe_pid>
10. CPU 优化
CPU 亲和性:
# 绑定 BE 进程到特定 CPU 核心
taskset -c 0-15 /path/to/doris/be/bin/start_be.sh
NUMA 优化:
# 查看 NUMA 拓扑
numactl --hardware
# 绑定到特定 NUMA 节点
numactl --cpunodebind=0 --membind=0 /path/to/doris/be/bin/start_be.sh
CPU 监控:
-- 查看 CPU 使用情况
SELECT
backend_id,
host,
cpu_cores,
cpu_usage_percent
FROM information_schema.backend_active_tasks;
11. 磁盘 I/O 优化
磁盘选择:
| 磁盘类型 | IOPS | 吞吐量 | 适用场景 |
|---|---|---|---|
| NVMe SSD | 100K+ | 3GB/s+ | 热数据、高并发查询 |
| SATA SSD | 10K+ | 500MB/s+ | 温数据、中等并发 |
| HDD | 100-200 | 100MB/s | 冷数据、归档 |
多盘配置:
# be.conf
# 配置多个数据目录
storage_root_path = /data1;/data2;/data3;/data4
# 每个目录可以指定存储介质
storage_root_path = /ssd1,medium:SSD;/hdd1,medium:HDD
I/O 调度优化:
# 查看 I/O 调度器
cat /sys/block/sda/queue/scheduler
# 设置为 deadline(推荐 SSD)
echo deadline > /sys/block/sda/queue/scheduler
# 设置为 noop(推荐 NVMe)
echo noop > /sys/block/nvme0n1/queue/scheduler
12. 操作系统优化
文件描述符:
# 增加文件描述符限制
ulimit -n 655350
# 永久配置
echo "* soft nofile 655350" >> /etc/security/limits.conf
echo "* hard nofile 655350" >> /etc/security/limits.conf
TCP 参数:
# 增加 TCP 连接队列
sysctl -w net.core.somaxconn=32768
sysctl -w net.ipv4.tcp_max_syn_backlog=32768
# 启用 TCP 快速回收
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=30
# 增加 TCP 缓冲区
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
透明大页:
# 禁用透明大页(推荐)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
13. JVM 优化(FE)
JVM 参数调优:
# fe.conf
JAVA_OPTS="-Xmx16g -Xms16g" # 堆内存
JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC" # 使用 G1 GC
JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=200" # GC 暂停时间
JAVA_OPTS="$JAVA_OPTS -XX:+ParallelRefProcEnabled" # 并行引用处理
JAVA_OPTS="$JAVA_OPTS -XX:+UnlockExperimentalVMOptions"
JAVA_OPTS="$JAVA_OPTS -XX:+UseCGroupMemoryLimitForHeap" # 容器环境
GC 日志:
JAVA_OPTS="$JAVA_OPTS -Xloggc:$DORIS_HOME/log/fe.gc.log"
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails"
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDateStamps"
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCTimeStamps"
高可用与容灾
副本机制
副本配置:
-- 建表时指定副本数
CREATE TABLE ha_table (
id BIGINT,
data VARCHAR(1000)
)
DUPLICATE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 32
PROPERTIES (
"replication_num" = "3" -- 3 副本
);
-- 修改副本数
ALTER TABLE ha_table SET ("replication_num" = "3");
副本分布策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 默认策略 | 副本均匀分布在不同 BE | 通用场景 |
| 机架感知 | 副本分布在不同机架 | 机架级容灾 |
| 可用区感知 | 副本分布在不同可用区 | 跨可用区容灾 |
故障恢复
FE 故障恢复:
# FE Master 故障,自动选举新 Master
# 1. Follower 检测到 Master 心跳超时
# 2. 发起选举投票
# 3. 选举出新 Master
# 4. 新 Master 接管服务
# 查看 FE 状态
SHOW FRONTENDS;
BE 故障恢复:
# BE 故障,自动切换到副本
# 1. FE 检测到 BE 心跳超时
# 2. 标记 BE 为不可用
# 3. 查询自动路由到其他副本
# 4. 后台自动补齐副本
# 查看 BE 状态
SHOW BACKENDS;
数据备份
备份表:
-- 创建备份仓库
CREATE REPOSITORY backup_repo
WITH BROKER "hdfs_broker"
ON LOCATION "hdfs://namenode:9000/doris_backup"
PROPERTIES (
"username" = "hdfs",
"password" = ""
);
-- 备份数据库
BACKUP SNAPSHOT db_name.snapshot_20240310
TO backup_repo
ON (table1, table2)
PROPERTIES ("type" = "full");
-- 查看备份状态
SHOW BACKUP FROM db_name;
恢复数据:
-- 恢复数据
RESTORE SNAPSHOT db_name.snapshot_20240310
FROM backup_repo
ON (table1, table2)
PROPERTIES (
"backup_timestamp" = "2024-03-10-12-00-00",
"replication_num" = "3"
);
-- 查看恢复状态
SHOW RESTORE FROM db_name;
跨集群同步
CCR(Cross Cluster Replication):
-- 在目标集群创建同步任务
CREATE SYNC JOB sync_job_name
(
FROM source_cluster.db_name.table_name
INTO db_name.table_name
)
WITH BROKER "broker_name"
PROPERTIES (
"source_cluster.fe_host" = "source_fe:8030",
"source_cluster.user" = "root",
"source_cluster.password" = ""
);
-- 查看同步状态
SHOW SYNC JOB;
监控与运维
监控指标
系统级监控:
| 指标类别 | 关键指标 | 采集方式 |
|---|---|---|
| CPU | 使用率、负载 | Prometheus + Node Exporter |
| 内存 | 使用率、可用内存 | Prometheus + Node Exporter |
| 磁盘 | 使用率、IOPS、吞吐量 | Prometheus + Node Exporter |
| 网络 | 带宽、丢包率 | Prometheus + Node Exporter |
Doris 监控指标:
# FE 监控端口
http://fe_host:8030/metrics
# BE 监控端口
http://be_host:8040/metrics
核心监控指标:
# 查询性能
doris_fe_query_latency_ms{quantile="0.99"} # P99 延迟
doris_fe_query_total # 查询总数
doris_fe_query_err_total # 查询错误数
# 导入性能
doris_be_stream_load_rows_total # 导入行数
doris_be_stream_load_bytes_total # 导入字节数
# 资源使用
doris_be_cpu_usage # CPU 使用率
doris_be_mem_usage # 内存使用率
doris_be_disk_usage # 磁盘使用率
# Compaction
doris_be_compaction_score # Compaction 分数
doris_be_compaction_deltas_total # 待合并文件数
系统表与元数据查询
Doris 提供了丰富的系统表(System Tables)用于查询集群状态、元数据信息、性能指标等。系统表位于 information_schema 数据库中。
系统表分类
系统表全景图:
元数据类系统表
databases 表(数据库信息):
-- 查看所有数据库
SELECT * FROM information_schema.databases;
-- 查看数据库详细信息
SELECT
CATALOG_NAME,
SCHEMA_NAME AS database_name,
DEFAULT_CHARACTER_SET_NAME,
DEFAULT_COLLATION_NAME
FROM information_schema.databases
WHERE SCHEMA_NAME NOT IN ('information_schema', '__internal_schema');
-- 统计每个数据库的表数量
SELECT
TABLE_SCHEMA AS database_name,
COUNT(*) AS table_count
FROM information_schema.tables
GROUP BY TABLE_SCHEMA
ORDER BY table_count DESC;
tables 表(表信息):
-- 查看所有表
SELECT
TABLE_SCHEMA AS database_name,
TABLE_NAME AS table_name,
TABLE_TYPE,
ENGINE,
TABLE_ROWS,
AVG_ROW_LENGTH,
DATA_LENGTH,
CREATE_TIME,
UPDATE_TIME,
TABLE_COMMENT
FROM information_schema.tables
WHERE TABLE_SCHEMA = 'dq_qhc';
-- 查看表大小排行
SELECT
TABLE_SCHEMA,
TABLE_NAME,
TABLE_ROWS,
ROUND(DATA_LENGTH / 1024 / 1024 / 1024, 2) AS data_size_gb,
ROUND(INDEX_LENGTH / 1024 / 1024 / 1024, 2) AS index_size_gb,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 2) AS total_size_gb
FROM information_schema.tables
WHERE TABLE_SCHEMA NOT IN ('information_schema', '__internal_schema')
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC
LIMIT 20;
-- 查看最近创建的表
SELECT
TABLE_SCHEMA,
TABLE_NAME,
CREATE_TIME,
TABLE_COMMENT
FROM information_schema.tables
WHERE TABLE_SCHEMA NOT IN ('information_schema', '__internal_schema')
ORDER BY CREATE_TIME DESC
LIMIT 10;
columns 表(列信息):
-- 查看表的列信息
SELECT
COLUMN_NAME,
ORDINAL_POSITION,
COLUMN_DEFAULT,
IS_NULLABLE,
DATA_TYPE,
COLUMN_TYPE,
COLUMN_KEY,
EXTRA,
COLUMN_COMMENT
FROM information_schema.columns
WHERE TABLE_SCHEMA = 'your_database'
AND TABLE_NAME = 'your_table'
ORDER BY ORDINAL_POSITION;
-- 查找包含特定列名的表
SELECT
TABLE_SCHEMA,
TABLE_NAME,
COLUMN_NAME,
DATA_TYPE
FROM information_schema.columns
WHERE COLUMN_NAME LIKE '%user_id%'
AND TABLE_SCHEMA NOT IN ('information_schema', '__internal_schema');
-- 统计各数据类型的使用情况
SELECT
DATA_TYPE,
COUNT(*) AS column_count
FROM information_schema.columns
WHERE TABLE_SCHEMA = 'your_database'
GROUP BY DATA_TYPE
ORDER BY column_count DESC;
partitions 表(分区信息):
-- 查看表的分区信息
SELECT
TABLE_SCHEMA,
TABLE_NAME,
PARTITION_NAME,
PARTITION_ORDINAL_POSITION,
PARTITION_METHOD,
PARTITION_EXPRESSION,
PARTITION_DESCRIPTION,
TABLE_ROWS,
AVG_ROW_LENGTH,
DATA_LENGTH,
CREATE_TIME,
UPDATE_TIME
FROM information_schema.partitions
WHERE TABLE_SCHEMA = 'your_database'
AND TABLE_NAME = 'your_table'
ORDER BY PARTITION_ORDINAL_POSITION;
-- 查看分区大小排行
SELECT
TABLE_SCHEMA,
TABLE_NAME,
PARTITION_NAME,
TABLE_ROWS,
ROUND(DATA_LENGTH / 1024 / 1024 / 1024, 2) AS data_size_gb
FROM information_schema.partitions
WHERE TABLE_SCHEMA = 'your_database'
ORDER BY DATA_LENGTH DESC
LIMIT 20;
-- 查找空分区
SELECT
TABLE_SCHEMA,
TABLE_NAME,
PARTITION_NAME,
CREATE_TIME
FROM information_schema.partitions
WHERE TABLE_SCHEMA = 'your_database'
AND TABLE_ROWS = 0
ORDER BY CREATE_TIME DESC;
table_constraints 表(约束信息):
-- 查看表的约束
SELECT
CONSTRAINT_SCHEMA,
TABLE_NAME,
CONSTRAINT_NAME,
CONSTRAINT_TYPE
FROM information_schema.table_constraints
WHERE CONSTRAINT_SCHEMA = 'your_database'
AND TABLE_NAME = 'your_table';
-- 查看所有主键约束
SELECT
TABLE_SCHEMA,
TABLE_NAME,
CONSTRAINT_NAME
FROM information_schema.table_constraints
WHERE CONSTRAINT_TYPE = 'PRIMARY KEY'
AND TABLE_SCHEMA NOT IN ('information_schema', '__internal_schema');
任务类系统表
loads 表(导入任务):
-- 查看最近的导入任务
SELECT
LABEL,
DATABASE_NAME,
STATE,
PROGRESS,
TYPE,
PRIORITY,
SCAN_ROWS,
FILTERED_ROWS,
UNSELECTED_ROWS,
SINK_ROWS,
ETL_INFO,
TASK_INFO,
ERROR_MSG,
CREATE_TIME,
ETL_START_TIME,
ETL_FINISH_TIME,
LOAD_START_TIME,
LOAD_FINISH_TIME
FROM information_schema.loads
WHERE DATABASE_NAME = 'your_database'
ORDER BY CREATE_TIME DESC
LIMIT 20;
-- 查看失败的导入任务
SELECT
LABEL,
DATABASE_NAME,
STATE,
ERROR_MSG,
CREATE_TIME,
LOAD_FINISH_TIME
FROM information_schema.loads
WHERE STATE = 'CANCELLED'
AND DATABASE_NAME = 'your_database'
ORDER BY CREATE_TIME DESC
LIMIT 10;
-- 统计导入任务状态
SELECT
STATE,
COUNT(*) AS task_count,
SUM(SINK_ROWS) AS total_rows,
ROUND(SUM(SINK_ROWS) / COUNT(*), 0) AS avg_rows_per_task
FROM information_schema.loads
WHERE DATABASE_NAME = 'your_database'
AND CREATE_TIME >= DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY STATE;
-- 查看导入性能统计
SELECT
LABEL,
SINK_ROWS,
ROUND((UNIX_TIMESTAMP(LOAD_FINISH_TIME) - UNIX_TIMESTAMP(LOAD_START_TIME)), 2) AS load_time_sec,
ROUND(SINK_ROWS / (UNIX_TIMESTAMP(LOAD_FINISH_TIME) - UNIX_TIMESTAMP(LOAD_START_TIME)), 0) AS rows_per_sec
FROM information_schema.loads
WHERE STATE = 'FINISHED'
AND DATABASE_NAME = 'your_database'
AND LOAD_START_TIME IS NOT NULL
AND LOAD_FINISH_TIME IS NOT NULL
ORDER BY CREATE_TIME DESC
LIMIT 20;
routine_loads 表(Routine Load 任务):
-- 查看 Routine Load 任务状态
SELECT
NAME,
CREATE_TIME,
PAUSE_TIME,
END_TIME,
DATABASE_NAME,
TABLE_NAME,
STATE,
DATA_SOURCE_TYPE,
CURRENT_TASK_NUM,
JOB_PROPERTIES,
DATA_SOURCE_PROPERTIES,
CUSTOM_PROPERTIES,
STATISTIC,
PROGRESS,
REASON_OF_STATE_CHANGED,
ERROR_LOG_URLS,
TRACKING_SQL
FROM information_schema.routine_loads
WHERE DATABASE_NAME = 'your_database';
-- 查看 Routine Load 消费进度
SELECT
NAME,
STATE,
CURRENT_TASK_NUM,
JSON_EXTRACT(PROGRESS, '$.partitionIdToOffset') AS partition_offset,
JSON_EXTRACT(STATISTIC, '$.receivedBytes') AS received_bytes,
JSON_EXTRACT(STATISTIC, '$.loadedRows') AS loaded_rows,
JSON_EXTRACT(STATISTIC, '$.errorRows') AS error_rows
FROM information_schema.routine_loads
WHERE DATABASE_NAME = 'your_database';
-- 查看 Routine Load 错误信息
SELECT
NAME,
STATE,
REASON_OF_STATE_CHANGED,
ERROR_LOG_URLS
FROM information_schema.routine_loads
WHERE STATE IN ('PAUSED', 'CANCELLED')
AND DATABASE_NAME = 'your_database';
stream_loads 表(Stream Load 任务):
-- 查看 Stream Load 任务
SELECT
LABEL,
DATABASE_NAME,
TABLE_NAME,
USER,
CLIENT_IP,
STATUS,
MESSAGE,
URL,
TOTAL_ROWS,
LOADED_ROWS,
FILTERED_ROWS,
UNSELECTED_ROWS,
LOAD_BYTES,
START_TIME,
FINISH_TIME
FROM information_schema.stream_loads
WHERE DATABASE_NAME = 'your_database'
ORDER BY START_TIME DESC
LIMIT 20;
-- 统计 Stream Load 性能
SELECT
DATE(START_TIME) AS load_date,
COUNT(*) AS task_count,
SUM(LOADED_ROWS) AS total_rows,
ROUND(SUM(LOAD_BYTES) / 1024 / 1024 / 1024, 2) AS total_gb,
ROUND(AVG(TIMESTAMPDIFF(SECOND, START_TIME, FINISH_TIME)), 2) AS avg_time_sec
FROM information_schema.stream_loads
WHERE DATABASE_NAME = 'your_database'
AND STATUS = 'Success'
AND START_TIME >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(START_TIME)
ORDER BY load_date DESC;
tasks 表(后台任务):
-- 查看后台任务
SELECT
TASK_NAME,
CREATE_TIME,
SCHEDULE,
DATABASE,
DEFINITION,
EXPIRE_TIME
FROM information_schema.tasks
WHERE DATABASE = 'your_database';
性能类系统表
backend_active_tasks 表(BE 活跃任务):
-- 查看 BE 上的活跃任务
SELECT
BE_ID,
FE_HOST,
QUERY_ID,
TASK_TIME_MS,
TASK_CPU_TIME_MS,
SCAN_ROWS,
SCAN_BYTES,
BE_PEAK_MEMORY_BYTES,
CURRENT_USED_MEMORY_BYTES,
SHUFFLE_SEND_BYTES,
SHUFFLE_SEND_ROWS
FROM information_schema.backend_active_tasks
ORDER BY TASK_TIME_MS DESC
LIMIT 20;
-- 查看内存使用最多的任务
SELECT
BE_ID,
QUERY_ID,
ROUND(BE_PEAK_MEMORY_BYTES / 1024 / 1024 / 1024, 2) AS peak_memory_gb,
ROUND(CURRENT_USED_MEMORY_BYTES / 1024 / 1024 / 1024, 2) AS current_memory_gb,
TASK_TIME_MS
FROM information_schema.backend_active_tasks
ORDER BY BE_PEAK_MEMORY_BYTES DESC
LIMIT 10;
workload_groups 表(资源组):
-- 查看资源组配置
SELECT
ID,
NAME,
CPU_SHARE,
MEMORY_LIMIT,
ENABLE_MEMORY_OVERCOMMIT,
MAX_CONCURRENCY,
MAX_QUEUE_SIZE,
QUEUE_TIMEOUT,
CPU_HARD_LIMIT
FROM information_schema.workload_groups;
-- 查看资源组使用情况
SELECT
NAME,
CPU_SHARE,
MEMORY_LIMIT,
MAX_CONCURRENCY,
RUNNING_QUERY_NUM,
WAITING_QUERY_NUM
FROM information_schema.workload_groups
WHERE RUNNING_QUERY_NUM > 0 OR WAITING_QUERY_NUM > 0;
query_statistics 表(查询统计):
-- 查看查询统计信息
SELECT
QUERY_ID,
QUERY_START_TIME,
QUERY_TIME_MS,
WORKLOAD_GROUP,
DATABASE,
SQL,
QUERY_TYPE,
SCAN_ROWS,
SCAN_BYTES,
RETURN_ROWS,
CPU_TIME_MS,
SHUFFLE_SEND_BYTES,
SHUFFLE_SEND_ROWS
FROM information_schema.query_statistics
WHERE DATABASE = 'your_database'
ORDER BY QUERY_START_TIME DESC
LIMIT 20;
-- 查看慢查询
SELECT
QUERY_ID,
QUERY_START_TIME,
QUERY_TIME_MS,
DATABASE,
LEFT(SQL, 100) AS sql_preview,
SCAN_ROWS,
RETURN_ROWS
FROM information_schema.query_statistics
WHERE QUERY_TIME_MS > 10000 -- 超过 10 秒
ORDER BY QUERY_TIME_MS DESC
LIMIT 20;
-- 统计查询类型分布
SELECT
QUERY_TYPE,
COUNT(*) AS query_count,
ROUND(AVG(QUERY_TIME_MS), 2) AS avg_time_ms,
ROUND(AVG(SCAN_ROWS), 0) AS avg_scan_rows
FROM information_schema.query_statistics
WHERE QUERY_START_TIME >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY QUERY_TYPE
ORDER BY query_count DESC;
集群类系统表
backends 表(BE 节点信息):
-- 查看 BE 节点状态
SELECT
BACKEND_ID,
HOST,
HEARTBEAT_PORT,
BE_PORT,
HTTP_PORT,
BRPC_PORT,
IS_ALIVE,
SYSTEM_DECOMMISSIONED,
CLUSTER_ID,
VERSION,
STATUS,
HEARTBEAT_FAILURE_COUNTER,
NODE_ROLE
FROM information_schema.backends;
-- 查看 BE 节点资源使用
SELECT
HOST,
IS_ALIVE,
TOTAL_CAPACITY_B / 1024 / 1024 / 1024 AS total_capacity_gb,
AVAILABLE_CAPACITY_B / 1024 / 1024 / 1024 AS available_capacity_gb,
USED_CAPACITY_B / 1024 / 1024 / 1024 AS used_capacity_gb,
ROUND(USED_CAPACITY_B * 100.0 / TOTAL_CAPACITY_B, 2) AS usage_percent
FROM information_schema.backends
ORDER BY usage_percent DESC;
-- 查看异常 BE 节点
SELECT
BACKEND_ID,
HOST,
IS_ALIVE,
HEARTBEAT_FAILURE_COUNTER,
LAST_HEARTBEAT_TIME
FROM information_schema.backends
WHERE IS_ALIVE = 'false' OR HEARTBEAT_FAILURE_COUNTER > 0;
frontends 表(FE 节点信息):
-- 查看 FE 节点状态
SELECT
NAME,
HOST,
EDIT_LOG_PORT,
HTTP_PORT,
QUERY_PORT,
RPC_PORT,
ROLE,
IS_MASTER,
CLUSTER_ID,
JOIN,
ALIVE,
REPLAY_LAG_MS,
LAST_HEARTBEAT_TIME,
IS_HELPER,
ERR_MSG,
VERSION
FROM information_schema.frontends;
-- 查看 Master FE
SELECT
NAME,
HOST,
QUERY_PORT,
IS_MASTER,
VERSION
FROM information_schema.frontends
WHERE IS_MASTER = 'true';
-- 查看 FE 同步延迟
SELECT
NAME,
HOST,
ROLE,
REPLAY_LAG_MS,
LAST_HEARTBEAT_TIME
FROM information_schema.frontends
WHERE ALIVE = 'true'
ORDER BY REPLAY_LAG_MS DESC;
catalogs 表(Catalog 信息):
-- 查看所有 Catalog
SELECT
CATALOG_ID,
CATALOG_NAME,
CATALOG_TYPE,
PROPERTY,
VALUE
FROM information_schema.catalogs;
-- 查看外部 Catalog
SELECT
CATALOG_NAME,
CATALOG_TYPE,
PROPERTY,
VALUE
FROM information_schema.catalogs
WHERE CATALOG_TYPE != 'internal';
系统表使用最佳实践
监控脚本示例:
-- 每日健康检查脚本
-- 1. 检查集群状态
SELECT 'BE Status' AS check_item, COUNT(*) AS total, SUM(IS_ALIVE = 'true') AS alive
FROM information_schema.backends
UNION ALL
SELECT 'FE Status', COUNT(*), SUM(ALIVE = 'true')
FROM information_schema.frontends;
-- 2. 检查磁盘使用
SELECT
'Disk Usage' AS check_item,
HOST,
ROUND(USED_CAPACITY_B * 100.0 / TOTAL_CAPACITY_B, 2) AS usage_percent
FROM information_schema.backends
WHERE USED_CAPACITY_B * 100.0 / TOTAL_CAPACITY_B > 80;
-- 3. 检查失败的导入任务
SELECT
'Failed Loads' AS check_item,
COUNT(*) AS failed_count
FROM information_schema.loads
WHERE STATE = 'CANCELLED'
AND CREATE_TIME >= DATE_SUB(NOW(), INTERVAL 1 DAY);
-- 4. 检查慢查询
SELECT
'Slow Queries' AS check_item,
COUNT(*) AS slow_query_count
FROM information_schema.query_statistics
WHERE QUERY_TIME_MS > 30000
AND QUERY_START_TIME >= DATE_SUB(NOW(), INTERVAL 1 HOUR);
-- 5. 检查表大小
SELECT
'Large Tables' AS check_item,
TABLE_SCHEMA,
TABLE_NAME,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 2) AS size_gb
FROM information_schema.tables
WHERE (DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024 > 100
ORDER BY size_gb DESC
LIMIT 10;
常用查询模板:
-- 模板 1:查找大表
SELECT
TABLE_SCHEMA,
TABLE_NAME,
TABLE_ROWS,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 2) AS total_size_gb
FROM information_schema.tables
WHERE TABLE_SCHEMA = 'your_database'
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC
LIMIT 10;
-- 模板 2:查找最近失败的导入
SELECT
LABEL,
STATE,
ERROR_MSG,
CREATE_TIME
FROM information_schema.loads
WHERE STATE = 'CANCELLED'
AND DATABASE_NAME = 'your_database'
ORDER BY CREATE_TIME DESC
LIMIT 10;
-- 模板 3:查看表的分区分布
SELECT
PARTITION_NAME,
TABLE_ROWS,
ROUND(DATA_LENGTH / 1024 / 1024 / 1024, 2) AS data_size_gb,
CREATE_TIME
FROM information_schema.partitions
WHERE TABLE_SCHEMA = 'your_database'
AND TABLE_NAME = 'your_table'
ORDER BY PARTITION_ORDINAL_POSITION;
-- 模板 4:查看 BE 负载分布
SELECT
BE_ID,
COUNT(*) AS task_count,
SUM(SCAN_ROWS) AS total_scan_rows,
ROUND(SUM(BE_PEAK_MEMORY_BYTES) / 1024 / 1024 / 1024, 2) AS total_memory_gb
FROM information_schema.backend_active_tasks
GROUP BY BE_ID
ORDER BY task_count DESC;
-- 模板 5:查看 Routine Load 消费延迟
SELECT
NAME,
STATE,
CURRENT_TASK_NUM,
JSON_EXTRACT(STATISTIC, '$.loadedRows') AS loaded_rows,
JSON_EXTRACT(STATISTIC, '$.errorRows') AS error_rows,
REASON_OF_STATE_CHANGED
FROM information_schema.routine_loads
WHERE DATABASE_NAME = 'your_database';
系统表查询注意事项:
- 性能影响:系统表查询会访问元数据,频繁查询可能影响性能
- 权限要求:需要相应的权限才能查询系统表
- 数据时效性:部分系统表数据可能有延迟
- 查询优化:使用 WHERE 条件过滤,避免全表扫描
- 定期清理:历史任务数据会占用空间,需要定期清理
日志管理
日志位置:
# FE 日志
/path/to/doris/fe/log/
├── fe.log # 主日志
├── fe.out # 标准输出
├── fe.warn.log # 警告日志
└── fe.audit.log # 审计日志
# BE 日志
/path/to/doris/be/log/
├── be.INFO # 信息日志
├── be.WARNING # 警告日志
├── be.ERROR # 错误日志
└── be.out # 标准输出
日志级别调整:
-- 动态调整日志级别
SET FRONTEND CONFIG ("sys_log_level" = "INFO"); -- DEBUG, INFO, WARN, ERROR
常见问题排查
1. 查询慢
-- 排查步骤
-- 1. 查看执行计划
EXPLAIN SELECT * FROM table WHERE condition;
-- 2. 查看 Profile
SHOW QUERY PROFILE '/query_id';
-- 3. 检查索引
SHOW CREATE TABLE table_name;
-- 4. 检查统计信息
SHOW TABLE STATS table_name;
2. 导入失败
# 排查步骤
# 1. 查看导入任务状态
SHOW LOAD WHERE LABEL = 'label_name'\G
# 2. 查看 BE 日志
tail -f /path/to/doris/be/log/be.INFO
# 3. 检查数据格式
# 4. 检查磁盘空间
df -h
3. BE 节点宕机
# 排查步骤
# 1. 查看 BE 状态
SHOW BACKENDS;
# 2. 查看 BE 日志
tail -f /path/to/doris/be/log/be.ERROR
# 3. 检查系统资源
top
free -h
df -h
# 4. 重启 BE
sh /path/to/doris/be/bin/stop_be.sh
sh /path/to/doris/be/bin/start_be.sh
4. Flink 实时写入抖动或查询突然变慢
# 排查步骤
# 1. 看导入是否只是“成功”,还是已经带来大量碎片
SHOW STREAM LOAD FROM db_name ORDER BY StartTime DESC LIMIT 20;
SHOW TABLET FROM db_name.table_name;
SHOW PROC '/backends';
# 2. 看 BE 资源是否被 Compaction 拖满
iostat -x 1
top
# 3. 检查 Flink Sink 的 flush 参数与并发
# 4. 检查分桶键是否存在热点,是否命中过多 Bucket
- 典型现象:写入成功率正常,但查询 P99 突然升高、磁盘 IO 抖动、部分 Tablet 的
RowsetNum明显偏高。 - 常见根因:
flush.interval过小、分桶过多、分桶键热点、BE 磁盘吞吐不足、Base Compaction 堆积。 - 处理顺序:先控碎片,再看 IO,最后再评估是否扩容;如果上来就加并发,通常只会更快把系统推向 Compaction 堆积。
资料来源:融合自 raw 文档《Doris 数据插入与排序》,原始整理链接见 原文链接。
升级与扩容
滚动升级:
# 1. 升级 BE(逐个升级)
# 停止 BE
sh /path/to/doris/be/bin/stop_be.sh
# 替换二进制文件
cp new_doris_be /path/to/doris/be/
# 启动 BE
sh /path/to/doris/be/bin/start_be.sh
# 2. 升级 FE(先 Follower,后 Master)
# 停止 FE
sh /path/to/doris/fe/bin/stop_fe.sh
# 替换二进制文件
cp new_doris_fe /path/to/doris/fe/
# 启动 FE
sh /path/to/doris/fe/bin/start_fe.sh
扩容 BE:
-- 添加 BE 节点
ALTER SYSTEM ADD BACKEND "be_host:9050";
-- 查看 BE 状态
SHOW BACKENDS;
-- 触发数据均衡
ADMIN SET FRONTEND CONFIG ("tablet_rebalancer_type" = "disk_and_tablet");
扩容 FE:
-- 添加 FE Follower
ALTER SYSTEM ADD FOLLOWER "fe_host:9010";
-- 添加 FE Observer
ALTER SYSTEM ADD OBSERVER "fe_host:9010";
-- 查看 FE 状态
SHOW FRONTENDS;
Doris vs 其他 OLAP 引擎
Doris vs ClickHouse
架构对比:
| 特性 | Doris | ClickHouse |
|---|---|---|
| 架构 | MPP(存算一体) | Shared-Nothing |
| 元数据管理 | FE 集中管理 | ZooKeeper 分布式 |
| 数据分布 | 自动均衡 | 手动配置 |
| SQL 兼容性 | MySQL 协议 | 自定义协议 |
| 实时更新 | 支持 Unique Key | 不支持(需 ReplacingMergeTree) |
| 向量检索 | 支持 HNSW | 不支持 |
| 全文检索 | 支持倒排索引 + BM25 | 支持(但功能较弱) |
性能对比:
TPC-H 1TB 测试:
├── Doris:120s
├── ClickHouse:180s
└── 结论:Doris 快 50%
并发查询(100 并发):
├── Doris:稳定
├── ClickHouse:性能下降明显
└── 结论:Doris 并发性能更好
选型建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 实时数仓 | Doris | 实时更新、高并发 |
| 日志分析 | ClickHouse | 写入性能极致 |
| BI 报表 | Doris | SQL 兼容性好 |
| 时序数据 | ClickHouse | 时序优化 |
| AI 应用(RAG) | Doris | 向量检索 + 全文检索 |
Doris vs StarRocks
关系:StarRocks 是 Doris 的分支项目。
差异对比:
| 特性 | Doris | StarRocks |
|---|---|---|
| 社区 | Apache 顶级项目 | 独立开源项目 |
| 存算分离 | 3.0+ 支持 | 原生支持 |
| 物化视图 | 同步 + 异步 | 异步为主 |
| 向量检索 | 4.0+ 支持 | 不支持 |
| 生态集成 | 更丰富 | 较少 |
选型建议:
- Doris:社区活跃、生态丰富、AI 增强
- StarRocks:存算分离、云原生
Doris vs Presto
架构对比:
| 特性 | Doris | Presto |
|---|---|---|
| 架构 | 存算一体 | 存算分离 |
| 数据存储 | 自有存储 | 外部存储(HDFS/S3) |
| 查询性能 | 快(本地存储) | 中等(网络 I/O) |
| 实时写入 | 支持 | 不支持 |
| 数据源 | 单一 | 多数据源联邦查询 |
选型建议:
- Doris:实时数仓、高性能查询
- Presto:多数据源联邦查询、即席查询
选型建议
选择 Doris 的场景:
- 实时数据大屏:秒级延迟、高并发查询
- 用户行为分析:实时更新、复杂多维分析
- 企业 BI 报表:SQL 兼容性好、物化视图加速
- AI 应用(RAG):向量检索 + 全文检索 + 结构化查询
- 日志分析:全文检索 + 结构化查询
不选择 Doris 的场景:
- 极致写入性能:ClickHouse 更优
- 多数据源联邦查询:Presto/Trino 更优
- 云原生存算分离:StarRocks 更优
实战案例
实时数据大屏
场景描述:
- 实时展示电商平台的销售数据
- 秒级延迟、高并发查询(1000+ QPS)
- 多维度分析(时间、地区、商品类别)
架构设计:
表设计:
-- 实时销售明细表
CREATE TABLE sales_realtime (
order_id BIGINT,
order_time DATETIME,
user_id BIGINT,
product_id INT,
category VARCHAR(50),
region VARCHAR(50),
amount DECIMAL(18, 2),
quantity INT
)
DUPLICATE KEY(order_id, order_time)
PARTITION BY RANGE(order_time) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "HOUR",
"dynamic_partition.start" = "-24",
"dynamic_partition.end" = "3"
);
-- 创建聚合物化视图
CREATE MATERIALIZED VIEW sales_hourly_mv AS
SELECT
DATE_TRUNC('hour', order_time) AS hour,
region,
category,
SUM(amount) AS total_amount,
SUM(quantity) AS total_quantity,
COUNT(DISTINCT user_id) AS user_count
FROM sales_realtime
GROUP BY hour, region, category;
查询示例:
-- 实时销售额(最近 1 小时)
SELECT
DATE_FORMAT(hour, '%H:%i') AS time,
SUM(total_amount) AS amount
FROM sales_hourly_mv
WHERE hour >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY hour
ORDER BY hour;
-- 地区销售排行
SELECT region, SUM(total_amount) AS amount
FROM sales_hourly_mv
WHERE hour >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY region
ORDER BY amount DESC
LIMIT 10;
用户行为分析
场景描述:
- 分析用户行为路径、留存、转化
- 支持漏斗分析、用户画像
- 数据量:日增 10 亿行
表设计:
-- 用户行为事件表
CREATE TABLE user_events (
event_id BIGINT,
user_id BIGINT,
event_time DATETIME,
event_type VARCHAR(50),
page_url VARCHAR(500),
properties JSON
)
DUPLICATE KEY(event_id, user_id, event_time)
PARTITION BY RANGE(event_time) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 64
PROPERTIES (
"replication_num" = "3"
);
-- 创建 Bitmap 去重物化视图
CREATE MATERIALIZED VIEW user_daily_active_mv AS
SELECT
DATE(event_time) AS date,
event_type,
BITMAP_UNION(TO_BITMAP(user_id)) AS active_users
FROM user_events
GROUP BY date, event_type;
漏斗分析:
-- 注册 -> 浏览商品 -> 加购 -> 下单 转化漏斗
WITH funnel_users AS (
SELECT
user_id,
MAX(CASE WHEN event_type = 'register' THEN 1 ELSE 0 END) AS step1,
MAX(CASE WHEN event_type = 'view_product' THEN 1 ELSE 0 END) AS step2,
MAX(CASE WHEN event_type = 'add_cart' THEN 1 ELSE 0 END) AS step3,
MAX(CASE WHEN event_type = 'order' THEN 1 ELSE 0 END) AS step4
FROM user_events
WHERE event_time >= '2024-01-01'
GROUP BY user_id
)
SELECT
SUM(step1) AS register_count,
SUM(step2) AS view_count,
SUM(step3) AS cart_count,
SUM(step4) AS order_count,
SUM(step2) * 100.0 / SUM(step1) AS view_rate,
SUM(step3) * 100.0 / SUM(step2) AS cart_rate,
SUM(step4) * 100.0 / SUM(step3) AS order_rate
FROM funnel_users;
企业知识库 RAG
场景描述:
- 构建企业内部知识库问答系统
- 支持向量检索 + 关键词检索
- 数据量:100 万文档、1 亿文档块
表设计:
-- 知识库表
CREATE TABLE knowledge_base (
chunk_id BIGINT,
doc_id BIGINT,
doc_title VARCHAR(200),
chunk_text TEXT,
chunk_index INT,
category VARCHAR(50),
create_time DATETIME,
embedding ARRAY<FLOAT> -- 1536 维向量
)
DUPLICATE KEY(chunk_id)
DISTRIBUTED BY HASH(chunk_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3"
);
-- 创建向量索引
ALTER TABLE knowledge_base
ADD INDEX embedding_idx(embedding) USING HNSW
PROPERTIES (
"metric_type" = "COSINE",
"M" = "16",
"ef_construction" = "200"
);
-- 创建倒排索引
ALTER TABLE knowledge_base
ADD INDEX text_idx(chunk_text) USING INVERTED
PROPERTIES ("parser" = "chinese");
混合检索:
-- 向量召回 + 关键词召回 + 结果融合
WITH vector_results AS (
SELECT chunk_id, doc_title, chunk_text,
COSINE_DISTANCE(embedding, :query_embedding) AS vector_score
FROM knowledge_base
WHERE category = :category
ORDER BY vector_score DESC
LIMIT 50
),
keyword_results AS (
SELECT chunk_id, doc_title, chunk_text,
BM25_SCORE(chunk_text, :query_text) AS keyword_score
FROM knowledge_base
WHERE chunk_text MATCH :query_text
AND category = :category
ORDER BY keyword_score DESC
LIMIT 50
)
SELECT
COALESCE(v.chunk_id, k.chunk_id) AS chunk_id,
COALESCE(v.doc_title, k.doc_title) AS doc_title,
COALESCE(v.chunk_text, k.chunk_text) AS chunk_text,
COALESCE(v.vector_score, 0) * 0.6 + COALESCE(k.keyword_score, 0) * 0.4 AS final_score
FROM vector_results v
FULL OUTER JOIN keyword_results k ON v.chunk_id = k.chunk_id
ORDER BY final_score DESC
LIMIT 10;
日志分析系统
场景描述:
- 分析应用日志、系统日志
- 支持全文检索 + 结构化查询
- 数据量:日增 1TB
系统架构:
表设计:
-- 日志表
CREATE TABLE application_logs (
log_id BIGINT,
log_time DATETIME,
level VARCHAR(10),
service VARCHAR(50),
host VARCHAR(100),
message TEXT,
trace_id VARCHAR(50),
user_id BIGINT,
request_id VARCHAR(50),
duration_ms INT,
status_code INT,
error_stack TEXT
)
DUPLICATE KEY(log_id, log_time)
PARTITION BY RANGE(log_time) (
-- 动态分区,自动创建和删除
)
DISTRIBUTED BY HASH(log_id) BUCKETS 64
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7", -- 保留最近 7 天
"dynamic_partition.end" = "1", -- 提前创建明天的分区
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "64",
"compression" = "ZSTD" -- 使用 ZSTD 压缩,节省存储
);
-- 创建倒排索引(全文检索)
ALTER TABLE application_logs
ADD INDEX message_idx(message) USING INVERTED
PROPERTIES (
"parser" = "english",
"support_phrase" = "true"
);
ALTER TABLE application_logs
ADD INDEX error_stack_idx(error_stack) USING INVERTED
PROPERTIES ("parser" = "english");
-- 创建 BloomFilter 索引(精确匹配)
ALTER TABLE application_logs SET (
"bloom_filter_columns" = "trace_id,user_id,request_id"
);
-- 创建聚合物化视图(错误统计)
CREATE MATERIALIZED VIEW error_summary_mv AS
SELECT
DATE_TRUNC('minute', log_time) AS minute,
service,
level,
COUNT(*) AS error_count
FROM application_logs
WHERE level IN ('ERROR', 'FATAL')
GROUP BY minute, service, level;
数据导入(Flink CDC):
// Flink 实时消费 Kafka 日志,写入 Doris
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.doris.flink.sink.DorisSink;
public class LogIngestion {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 消费 Kafka
FlinkKafkaConsumer<String> kafkaSource = new FlinkKafkaConsumer<>(
"application-logs",
new SimpleStringSchema(),
kafkaProps
);
// 配置 Doris Sink
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030,fe2:8030")
.setTableIdentifier("log_db.application_logs")
.setUsername("root")
.setPassword("")
.build();
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("log_ingestion")
.setStreamLoadProp(Properties.builder()
.put("format", "json")
.put("read_json_by_line", "true")
.build())
.setBufferFlushMaxRows(100000) // 10 万行刷新一次
.setBufferFlushMaxBytes(100 * 1024 * 1024) // 100MB 刷新一次
.setBufferFlushInterval(10000) // 10 秒刷新一次
.build();
DorisSink<String> sink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
// 执行任务
env.addSource(kafkaSource)
.sinkTo(sink);
env.execute("Log Ingestion to Doris");
}
}
查询示例:
1. 全文检索 + 结构化过滤
-- 查询包含 "error" 和 "exception" 的错误日志
SELECT
log_time,
level,
service,
host,
message,
error_stack
FROM application_logs
WHERE message MATCH_ALL 'error exception' -- 全文检索
AND level = 'ERROR' -- 结构化过滤
AND log_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
ORDER BY log_time DESC
LIMIT 100;
-- 查询时间:< 500ms(使用倒排索引)
-- vs 30-60s(全表扫描)
2. 链路追踪
-- 根据 trace_id 追踪完整请求链路
SELECT
log_time,
service,
level,
message,
duration_ms,
status_code
FROM application_logs
WHERE trace_id = 'trace_abc123' -- 使用 BloomFilter 快速定位
ORDER BY log_time;
-- 查询时间:< 100ms(使用 BloomFilter 索引)
3. 错误趋势分析
-- 查询最近 24 小时错误趋势(使用物化视图)
SELECT
DATE_FORMAT(minute, '%Y-%m-%d %H:%i') AS time,
service,
SUM(error_count) AS errors
FROM error_summary_mv
WHERE minute >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY minute, service
ORDER BY minute;
-- 查询时间:< 200ms(使用物化视图)
-- vs 10-30s(扫描明细数据)
4. Top 错误排行
-- 查询最近 1 小时 Top 10 错误
SELECT
message,
COUNT(*) AS error_count,
COUNT(DISTINCT user_id) AS affected_users,
MIN(log_time) AS first_occurrence,
MAX(log_time) AS last_occurrence
FROM application_logs
WHERE level = 'ERROR'
AND log_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY message
ORDER BY error_count DESC
LIMIT 10;
5. 慢请求分析
-- 查询最近 1 小时慢请求(耗时 > 1 秒)
SELECT
log_time,
service,
request_id,
user_id,
duration_ms,
message
FROM application_logs
WHERE duration_ms > 1000
AND log_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
ORDER BY duration_ms DESC
LIMIT 100;
6. 用户行为分析
-- 查询特定用户的操作日志
SELECT
log_time,
service,
level,
message,
duration_ms
FROM application_logs
WHERE user_id = 12345 -- 使用 BloomFilter 快速定位
AND log_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY log_time DESC;
告警规则配置:
-- 创建告警视图
CREATE VIEW alert_rules AS
SELECT
service,
COUNT(*) AS error_count,
COUNT(DISTINCT user_id) AS affected_users
FROM application_logs
WHERE level = 'ERROR'
AND log_time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
GROUP BY service
HAVING error_count > 100 -- 5 分钟内错误数 > 100
OR affected_users > 50; -- 或影响用户数 > 50
Python 告警脚本:
import pymysql
import requests
import time
def check_alerts():
conn = pymysql.connect(
host='fe_host',
port=9030,
user='root',
password='',
database='log_db'
)
cursor = conn.cursor()
cursor.execute("SELECT * FROM alert_rules")
alerts = cursor.fetchall()
for service, error_count, affected_users in alerts:
# 发送告警
send_alert(
title=f"服务 {service} 错误告警",
message=f"错误数:{error_count},影响用户:{affected_users}",
level="critical"
)
cursor.close()
conn.close()
def send_alert(title, message, level):
# 发送到钉钉、企业微信等
webhook_url = "https://your-webhook-url"
payload = {
"msgtype": "text",
"text": {
"content": f"【{level.upper()}】{title}\n{message}"
}
}
requests.post(webhook_url, json=payload)
# 每分钟检查一次
while True:
check_alerts()
time.sleep(60)
日志分析 Dashboard(Grafana):
-- Panel 1:实时错误率
SELECT
DATE_FORMAT(minute, '%H:%i') AS time,
SUM(error_count) AS errors
FROM error_summary_mv
WHERE minute >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY minute
ORDER BY minute;
-- Panel 2:服务错误分布
SELECT
service,
SUM(error_count) AS errors
FROM error_summary_mv
WHERE minute >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY service
ORDER BY errors DESC
LIMIT 10;
-- Panel 3:错误级别分布
SELECT
level,
COUNT(*) AS count
FROM application_logs
WHERE log_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY level;
-- Panel 4:Top 错误消息
SELECT
SUBSTRING(message, 1, 100) AS error_msg,
COUNT(*) AS count
FROM application_logs
WHERE level = 'ERROR'
AND log_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY error_msg
ORDER BY count DESC
LIMIT 10;
性能优化:
1. 分区管理
-- 自动删除 7 天前的分区,节省存储
ALTER TABLE application_logs SET (
"dynamic_partition.start" = "-7"
);
-- 手动删除历史分区
ALTER TABLE application_logs DROP PARTITION p20240301;
2. 压缩优化
-- 使用 ZSTD 压缩,压缩比 4-5x
ALTER TABLE application_logs SET ("compression" = "ZSTD");
-- 压缩效果:
-- 原始数据:1TB/天
-- 压缩后:200-250GB/天
-- 节省存储:75-80%
3. 查询优化
-- 使用分区裁剪
SELECT * FROM application_logs
WHERE log_time >= '2024-03-10 00:00:00'
AND log_time < '2024-03-11 00:00:00';
-- 只扫描 p20240310 分区
-- 使用倒排索引
SELECT * FROM application_logs
WHERE message MATCH 'error';
-- 使用倒排索引,快速定位
-- 使用 BloomFilter
SELECT * FROM application_logs
WHERE trace_id = 'trace_123';
-- 使用 BloomFilter,快速过滤
4. 写入优化
// Flink Doris Connector 写入优化
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setBufferFlushMaxRows(500000) // 增大批次(50 万行)
.setBufferFlushMaxBytes(500 * 1024 * 1024) // 增大批次(500MB)
.setBufferFlushInterval(5000) // 减小刷新间隔(5 秒)
.setMaxRetries(3) // 重试次数
.build();
性能指标:
| 指标 | 目标值 | 实际值 |
|---|---|---|
| 写入吞吐量 | > 100 万行/秒 | 150-200 万行/秒 |
| 写入延迟 | < 10s | 3-5s |
| 查询延迟(全文检索) | < 1s | 200-500ms |
| 查询延迟(精确匹配) | < 500ms | 50-100ms |
| 存储压缩比 | > 4x | 4-5x |
| 数据保留 | 7 天 | 7 天 |
故障排查:
问题 1:写入延迟高
# 排查步骤
# 1. 查看 BE 负载
SHOW BACKENDS;
# 2. 查看 Compaction 状态
SHOW PROC '/compactions';
# 3. 检查磁盘 I/O
iostat -x 1
# 4. 优化方案
# - 增加 BE 节点
# - 调整 Compaction 参数
# - 使用 SSD 磁盘
问题 2:查询慢
-- 排查步骤
-- 1. 查看执行计划
EXPLAIN SELECT * FROM application_logs WHERE condition;
-- 2. 检查索引是否生效
SHOW CREATE TABLE application_logs;
-- 3. 检查分区裁剪
-- 确保查询条件包含分区列(log_time)
-- 4. 优化方案
-- - 创建倒排索引
-- - 创建 BloomFilter 索引
-- - 使用物化视图
高频面试题
基础概念题
1. 什么是 Apache Doris?它的核心特点是什么?
答案: Apache Doris 是一个基于 MPP 架构的高性能实时分析数据库,主要用于 OLAP 场景。
核心特点:
- 极致性能:MPP 并行计算 + 向量化执行引擎 + 列式存储
- 实时性:秒级数据导入和查询响应
- 易用性:兼容 MySQL 协议,标准 SQL
- 高可用:多副本机制,自动故障转移
- AI 增强:向量检索 + 全文检索 + AI 函数(4.x 版本)
2. Doris 的架构由哪些组件组成?各自的职责是什么?
答案:
| 组件 | 职责 |
|---|---|
| Frontend (FE) | 元数据管理、查询解析优化、任务调度 |
| Backend (BE) | 数据存储、查询执行、数据导入 |
FE 角色:
- Master:元数据管理、DDL 执行(1 个,自动选举)
- Follower:元数据备份、查询服务(2+ 个)
- Observer:只读查询、负载均衡(可选)
3. Doris 支持哪些数据模型?各自的适用场景是什么?
答案:
| 数据模型 | 适用场景 | 更新方式 |
|---|---|---|
| Duplicate | 明细数据、日志分析 | 仅追加 |
| Aggregate | 预聚合、指标汇总 | 聚合合并 |
| Unique | 维度表、实时更新 | 主键更新 |
架构原理题
4. Doris 的查询执行流程是怎样的?
答案:
1. 客户端提交 SQL 到 FE
2. FE 解析 SQL,生成逻辑计划
3. FE 优化器优化逻辑计划(谓词下推、列裁剪、分区裁剪)
4. FE 生成物理计划,选择 Join 策略
5. FE 将查询计划分发到各个 BE
6. BE 并行执行查询(向量化执行)
7. BE 将结果返回给 FE
8. FE 聚合结果,返回给客户端
关键优化:
- 谓词下推:将过滤条件推到存储层
- 列裁剪:只读取需要的列
- 分区裁剪:只扫描相关分区
- Runtime Filter:动态生成过滤条件
5. Doris 的数据是如何分布的?
答案:
Doris 使用 两级分片:Partition(分区) + Bucket(分桶)
Table
├── Partition 1(按时间范围分区)
│ ├── Bucket 1(按 Hash 分桶)
│ │ └── Tablet 1(副本 1, 2, 3)
│ ├── Bucket 2
│ └── Bucket N
├── Partition 2
└── Partition N
分区:按范围(时间)或枚举值分区,支持分区裁剪 分桶:按 Hash 值分桶,数据均匀分布 Tablet:数据管理和副本的基本单位,默认 256MB
6. Doris 的向量检索是如何实现的?
答案:
Doris 4.x 使用 HNSW(Hierarchical Navigable Small World) 算法实现向量检索。
HNSW 原理:
- 多层图结构:上层稀疏(快速定位),下层密集(精确搜索)
- 快速搜索:从上层快速跳跃,下层精确查找
- 高召回率:通过调整参数平衡性能和准确性
性能:
- 支持亿级向量毫秒级查询
- 召回率 95-99%(可调)
- 索引大小约为原始数据的 10-20%
优势:
- MPP 并行:利用 Doris 的并行计算能力
- 混合查询:一条 SQL 同时支持向量检索和标量过滤
- SQL 交互:无需学习新 API
性能调优题
7. 如何优化 Doris 的查询性能?
答案:
索引优化:
- 前缀索引:将高频过滤列、高基数列放在前面
- BloomFilter:为高基数列创建 BloomFilter 索引
- 倒排索引:为文本列创建倒排索引
分区裁剪:
- 使用分区列过滤,只扫描相关分区
- 启用动态分区裁剪
Join 优化:
- 小表使用 Broadcast Join
- 大表使用 Colocate Join(数据本地化)
- 启用 Runtime Filter
物化视图:
- 为高频查询创建物化视图
- 查询自动改写,加速 20-100 倍
8. Doris 的数据导入性能如何优化?
答案:
批量导入:
- 增大批次大小(100MB-500MB)
- 减少导入频率
并行导入:
- 使用 Flink Doris Connector 并行写入
- 增加并发度
分区分桶优化:
- 合理设置分桶数(单 Tablet 1-10GB)
- 分桶数为 BE 节点数的倍数
Compaction 优化:
- 调整 Compaction 参数
- 避免 Compaction 影响查询
9. 如何处理 Doris 的数据倾斜问题?
答案:
原因:
- 分桶键选择不当(低基数列)
- 数据分布不均匀
解决方案:
方案 1:重新选择分桶键
-- 使用高基数列或组合列
ALTER TABLE table_name
DISTRIBUTED BY HASH(user_id, order_id) BUCKETS 32;
方案 2:增加分桶数
ALTER TABLE table_name
DISTRIBUTED BY HASH(user_id) BUCKETS 64;
方案 3:使用 Colocate Join
-- 将关联表放在同一 Colocate Group
ALTER TABLE table1 SET ("colocate_with" = "group1");
ALTER TABLE table2 SET ("colocate_with" = "group1");
实战应用题
10. 如何在 Doris 中构建 RAG 系统?
答案:
架构设计:
原始文档 → 文本清洗 → 分块切分 → Embedding 生成 → 写入 Doris
用户问题 → Embedding 生成 → 混合检索(向量 + 关键词) → Top K 文档 → LLM 生成答案
表设计:
CREATE TABLE knowledge_base (
chunk_id BIGINT,
doc_title VARCHAR(200),
chunk_text TEXT,
embedding ARRAY<FLOAT> -- 1536 维向量
)
DUPLICATE KEY(chunk_id)
DISTRIBUTED BY HASH(chunk_id) BUCKETS 32;
-- 创建向量索引
ALTER TABLE knowledge_base
ADD INDEX embedding_idx(embedding) USING HNSW;
-- 创建倒排索引
ALTER TABLE knowledge_base
ADD INDEX text_idx(chunk_text) USING INVERTED;
混合检索:
-- 向量召回 + 关键词召回 + 结果融合
WITH vector_results AS (
SELECT chunk_id, chunk_text,
COSINE_DISTANCE(embedding, :query_embedding) AS score
FROM knowledge_base
ORDER BY score DESC LIMIT 50
),
keyword_results AS (
SELECT chunk_id, chunk_text,
BM25_SCORE(chunk_text, :query_text) AS score
FROM knowledge_base
WHERE chunk_text MATCH :query_text
ORDER BY score DESC LIMIT 50
)
SELECT chunk_id, chunk_text,
v.score * 0.6 + k.score * 0.4 AS final_score
FROM vector_results v
FULL OUTER JOIN keyword_results k USING (chunk_id)
ORDER BY final_score DESC LIMIT 10;
优势:
- 架构简化:一个数据库搞定所有
- 高性能:MPP 并行,亿级向量毫秒级查询
- 实时性:数据写入即可查询
11. Doris 和 ClickHouse 如何选型?
答案:
选择 Doris:
- 实时数仓(需要实时更新)
- 高并发查询(1000+ QPS)
- BI 报表(SQL 兼容性好)
- AI 应用(向量检索 + 全文检索)
选择 ClickHouse:
- 日志分析(极致写入性能)
- 时序数据(时序优化)
- 单机部署(运维简单)
性能对比:
- 查询性能:Doris 快 30-50%
- 写入性能:ClickHouse 快 20-30%
- 并发性能:Doris 更稳定
12. 如何保证 Doris 的高可用?
答案:
FE 高可用:
- 部署 3 个 FE(1 Master + 2 Follower)
- 基于 Paxos 协议自动选举
- Master 故障时自动切换
BE 高可用:
- 数据 3 副本(默认)
- 副本分布在不同 BE 节点
- BE 故障时自动切换到副本
数据备份:
- 定期备份到 HDFS/S3
- 支持全量备份和增量备份
跨集群同步:
- 使用 CCR(Cross Cluster Replication)
- 主备集群实时同步
监控告警:
- Prometheus + Grafana 监控
- 关键指标告警(CPU、内存、磁盘、查询延迟)
总结
Apache Doris 已经从单纯的 OLAP 数据库进化为 AI-Native 数据平台。通过向量检索、全文检索、AI 函数的深度集成,Doris 成为大模型时代构建 RAG 系统的理想选择。
核心优势:
- 架构极简:一个数据库统一存储结构化、非结构化和向量数据
- 极致性能:MPP 并行计算,亿级向量毫秒级查询
- 实时性强:秒级数据导入和查询响应
- 成本低:减少组件数量,降低运维复杂度
适用场景:
- 实时数据大屏
- 用户行为分析
- 企业 BI 报表
- 企业知识库 RAG
- 日志分析系统
13. Doris 的 Compaction 机制是如何工作的?如何优化?
答案:
Compaction 机制:
Compaction 是 Doris 后台自动执行的数据合并过程,用于优化存储和查询性能。
工作原理:
Compaction 类型:
1. Cumulative Compaction(增量合并):
- 触发条件:增量 Rowset 数量 > 5(默认)
- 合并范围:增量 Rowset 之间合并
- 执行频率:高(分钟级)
- 资源消耗:中等
2. Base Compaction(基线合并):
- 触发条件:Cumulative Rowset 数量 > 10(默认)
- 合并范围:所有 Cumulative Rowset 合并到 Base Rowset
- 执行频率:低(小时级)
- 资源消耗:高
3. Vertical Compaction(垂直合并):
- 适用场景:列数较多的表(> 100 列)
- 优化策略:按列分批合并,降低内存压力
- 性能提升:减少内存峰值 50-70%
Compaction 优化策略:
-- 1. 调整 Compaction 策略
ALTER TABLE large_table SET (
"compaction_policy" = "time_series", -- 时序数据优化
"time_series_compaction_goal_size_mbytes" = "1024",
"time_series_compaction_file_count_threshold" = "10"
);
-- 2. 调整触发阈值
ALTER TABLE large_table SET (
"cumulative_compaction_num_singleton_deltas" = "5",
"base_compaction_num_cumulative_deltas" = "10"
);
-- 3. 手动触发 Compaction
ADMIN COMPACT TABLE large_table;
-- 4. 查看 Compaction 状态
SHOW PROC '/compactions';
Compaction 监控指标:
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| compaction_score | Compaction 分数 | > 100 |
| compaction_deltas_total | 待合并文件数 | > 50 |
| compaction_bytes_total | 待合并数据量 | > 100GB |
优化建议:
- 控制写入频率:减少小批量写入,增大批次大小
- 合理设置分桶数:避免单个 Tablet 过大
- 调整 Compaction 并发度:根据 CPU 和磁盘 I/O 调整
- 监控 Compaction 延迟:及时发现 Compaction 堆积
14. Doris 的元数据是如何管理的?FE 故障如何恢复?
答案:
元数据管理架构:
Doris 使用 BDBJE(Berkeley DB Java Edition) 存储元数据,基于 Paxos 协议 实现一致性。
元数据存储内容:
元数据包含:
├── 数据库和表结构(Schema)
├── 分区和分桶信息
├── Tablet 位置和副本信息
├── 用户权限配置
├── 集群拓扑信息
└── 统计信息
元数据同步机制:
元数据持久化:
1. Journal 日志:
- 记录所有元数据变更操作
- 顺序写入,保证一致性
- 定期清理旧日志
2. Image 镜像(Checkpoint):
- 定期生成元数据快照
- 加速 FE 启动恢复
- 默认每 50000 条 Journal 生成一次
FE 故障恢复流程:
场景 1:Follower 故障
# 1. Follower 故障,Master 检测到心跳超时
# 2. Master 标记 Follower 为不可用
# 3. 查询请求自动路由到其他 FE
# 4. 重启 Follower 后自动同步元数据
# 重启 Follower
sh /path/to/doris/fe/bin/stop_fe.sh
sh /path/to/doris/fe/bin/start_fe.sh
# 查看同步状态
SHOW FRONTENDS;
场景 2:Master 故障
# 1. Follower 检测到 Master 心跳超时
# 2. Follower 发起选举投票
# 3. 选举出新 Master(多数派投票)
# 4. 新 Master 接管服务
# 5. 客户端自动重连新 Master
# 选举过程(自动):
# - 投票超时:5-10 秒
# - 选举时间:< 30 秒
# - 服务中断:< 1 分钟
场景 3:多数派 FE 故障
# 如果多数派 FE 故障,集群无法提供服务
# 需要手动恢复
# 1. 恢复至少 2 个 FE(多数派)
# 2. 从 Image + Journal 恢复元数据
# 3. 重新选举 Master
# 元数据恢复
sh /path/to/doris/fe/bin/start_fe.sh --helper <existing_fe_host>:9010
元数据备份与恢复:
# 备份元数据
cp -r /path/to/doris/fe/doris-meta /backup/doris-meta-20240310
# 恢复元数据
sh /path/to/doris/fe/bin/stop_fe.sh
rm -rf /path/to/doris/fe/doris-meta
cp -r /backup/doris-meta-20240310 /path/to/doris/fe/doris-meta
sh /path/to/doris/fe/bin/start_fe.sh
最佳实践:
- 部署奇数个 FE:3 个或 5 个(保证多数派)
- 定期备份元数据:每天备份 doris-meta 目录
- 监控 FE 状态:心跳、元数据同步延迟
- 跨机房部署:FE 分布在不同机房,提高可用性
15. Doris 的查询优化器是如何工作的?CBO 和 RBO 的区别是什么?
答案:
查询优化器架构:
RBO(Rule-Based Optimizer,基于规则优化):
优化规则:
| 规则 | 说明 | 示例 |
|---|---|---|
| 谓词下推 | 将过滤条件推到数据源 | WHERE 条件下推到 Scan |
| 列裁剪 | 只读取需要的列 | SELECT a, b 只读取 a, b 列 |
| 分区裁剪 | 只扫描相关分区 | WHERE date = ‘2024-01-01’ 只扫描该分区 |
| 常量折叠 | 编译时计算常量表达式 | WHERE 1 + 1 = 2 → WHERE TRUE |
| 子查询展开 | 将子查询转换为 Join | IN 子查询 → Semi Join |
| 外连接消除 | 将 Outer Join 转换为 Inner Join | LEFT JOIN + WHERE 条件 → INNER JOIN |
RBO 示例:
-- 原始查询
SELECT a.id, a.name, b.amount
FROM users a
LEFT JOIN orders b ON a.id = b.user_id
WHERE b.status = 'completed';
-- RBO 优化后(外连接消除)
SELECT a.id, a.name, b.amount
FROM users a
INNER JOIN orders b ON a.id = b.user_id
WHERE b.status = 'completed';
CBO(Cost-Based Optimizer,基于成本优化):
成本模型:
查询成本 = CPU 成本 + I/O 成本 + 网络成本
CPU 成本 = 处理行数 × CPU 单位成本
I/O 成本 = 扫描数据量 × I/O 单位成本
网络成本 = 传输数据量 × 网络单位成本
统计信息:
-- 收集统计信息
ANALYZE TABLE user_table;
-- 查看统计信息
SHOW COLUMN STATS user_table;
-- 统计信息包含:
-- 1. 行数(Row Count)
-- 2. NDV(Number of Distinct Values,唯一值数量)
-- 3. NULL 值数量
-- 4. 最小值/最大值
-- 5. 平均长度(字符串列)
CBO 优化示例:
场景:Join 顺序选择
-- 三表 Join
SELECT *
FROM table1 t1
JOIN table2 t2 ON t1.id = t2.id
JOIN table3 t3 ON t2.id = t3.id;
-- CBO 根据统计信息选择 Join 顺序:
-- 方案 1:(t1 JOIN t2) JOIN t3
-- 方案 2:(t1 JOIN t3) JOIN t2
-- 方案 3:(t2 JOIN t3) JOIN t1
-- CBO 计算每个方案的成本,选择成本最低的方案
Join 策略选择:
| Join 策略 | 成本估算 | 选择条件 |
|---|---|---|
| Broadcast Join | 小表大小 × BE 数量 + 大表扫描成本 | 小表 < 10MB |
| Shuffle Join | 两表扫描成本 + Shuffle 成本 | 大表 Join 大表 |
| Colocate Join | 两表扫描成本(无 Shuffle) | 表在同一 Colocate Group |
RBO vs CBO 对比:
| 特性 | RBO | CBO |
|---|---|---|
| 优化依据 | 固定规则 | 统计信息 + 成本模型 |
| 优化效果 | 稳定,但可能不是最优 | 更优,但依赖统计信息 |
| 执行时间 | 快 | 慢(需要计算成本) |
| 适用场景 | 简单查询 | 复杂查询、多表 Join |
查询 Hint:
-- 强制使用 Broadcast Join
SELECT /*+ BROADCAST(t2) */ *
FROM large_table t1
JOIN small_table t2 ON t1.id = t2.id;
-- 强制使用 Shuffle Join
SELECT /*+ SHUFFLE(t1, t2) */ *
FROM table1 t1
JOIN table2 t2 ON t1.id = t2.id;
-- 禁用 CBO
SELECT /*+ NO_CBO */ *
FROM user_table;
16. Doris 的向量检索和传统向量数据库(如 Milvus)相比有什么优势和劣势?
答案:
架构对比:
Doris 向量检索优势:
1. 架构简化:
- 传统方案:MySQL + Elasticsearch + Milvus(3 个系统)
- Doris 方案:Doris(1 个系统)
- 优势:减少组件数量,降低运维复杂度
2. 混合查询:
-- Doris:一条 SQL 完成向量检索 + 标量过滤
SELECT doc_id, title,
COSINE_DISTANCE(embedding, :query_embedding) AS similarity
FROM knowledge_base
WHERE category = 'AI' -- 标量过滤
AND create_time >= '2024-01-01'
ORDER BY similarity DESC
LIMIT 10;
-- Milvus:需要两步
-- 1. 向量检索(Milvus)
-- 2. 标量过滤(应用层或 MySQL)
3. 实时性:
- Doris:数据写入即可查询(秒级)
- Milvus:需要构建索引(分钟级)
4. 事务一致性:
- Doris:结构化数据和向量数据在同一事务
- Milvus:需要应用层保证一致性
5. SQL 交互:
- Doris:标准 SQL,学习成本低
- Milvus:专用 API,学习成本高
Doris 向量检索劣势:
1. 向量检索性能:
测试场景:1 亿向量(1536 维),查询 Top 10
Milvus(专用向量数据库):
├── 查询延迟:10-20ms
├── QPS:2000-5000
└── 召回率:99%+
Doris(通用数据库):
├── 查询延迟:10-50ms
├── QPS:1000-2000
└── 召回率:95-99%
结论:Milvus 在纯向量检索性能上略优
2. 向量索引算法:
- Milvus:支持多种索引算法(HNSW、IVF、FLAT 等)
- Doris:目前只支持 HNSW
3. 向量维度支持:
- Milvus:支持超高维向量(> 10000 维)
- Doris:推荐 1536 维以内
4. 专用优化:
- Milvus:针对向量检索深度优化(GPU 加速、量化压缩)
- Doris:通用 OLAP 优化
选型建议:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 企业知识库 RAG | Doris | 混合查询、架构简化 |
| 推荐系统召回 | Milvus | 纯向量检索、极致性能 |
| 实时数据分析 + 向量检索 | Doris | 统一平台、实时性 |
| 超大规模向量检索(10 亿+) | Milvus | 专用优化、GPU 加速 |
| 多模态检索(图像、视频) | Milvus | 专用功能 |
混合方案:
场景:电商推荐系统
├── 召回阶段:Milvus(纯向量检索,极致性能)
└── 排序阶段:Doris(特征计算 + 业务规则)
17. 如何设计一个高性能的实时数据大屏系统?
答案:
系统架构:
设计要点:
1. 数据分层:
-- ODS 层:原始明细数据
CREATE TABLE ods_sales_detail (
order_id BIGINT,
order_time DATETIME,
user_id BIGINT,
product_id INT,
amount DECIMAL(18, 2)
)
DUPLICATE KEY(order_id, order_time)
PARTITION BY RANGE(order_time) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 32;
-- DWS 层:聚合数据(物化视图)
CREATE MATERIALIZED VIEW dws_sales_hourly AS
SELECT
DATE_TRUNC('hour', order_time) AS hour,
SUM(amount) AS total_amount,
COUNT(*) AS order_count,
COUNT(DISTINCT user_id) AS user_count
FROM ods_sales_detail
GROUP BY hour;
-- ADS 层:应用层数据(Redis 缓存)
2. 查询优化:
-- 使用物化视图加速查询
SELECT hour, total_amount
FROM dws_sales_hourly
WHERE hour >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY hour;
-- 查询时间:< 100ms(使用物化视图)
-- vs 1-5s(扫描明细数据)
3. 缓存策略:
import redis
import pymysql
redis_client = redis.Redis(host='localhost', port=6379)
doris_conn = pymysql.connect(host='fe_host', port=9030, user='root')
def get_realtime_sales(cache_ttl=10):
# 1. 尝试从 Redis 获取
cache_key = 'realtime_sales'
cached_data = redis_client.get(cache_key)
if cached_data:
return json.loads(cached_data)
# 2. 从 Doris 查询
cursor = doris_conn.cursor()
cursor.execute("""
SELECT hour, total_amount
FROM dws_sales_hourly
WHERE hour >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
""")
data = cursor.fetchall()
# 3. 写入 Redis 缓存
redis_client.setex(cache_key, cache_ttl, json.dumps(data))
return data
4. 并发控制:
-- 创建高优先级资源组
CREATE WORKLOAD GROUP dashboard_group
PROPERTIES (
"cpu_share" = "1024",
"memory_limit" = "30%",
"max_concurrency" = "100"
);
-- 大屏查询用户绑定资源组
ALTER USER 'dashboard_user' SET PROPERTIES (
"default_workload_group" = "dashboard_group"
);
5. 监控告警:
# 监控查询延迟
def monitor_query_latency():
cursor = doris_conn.cursor()
cursor.execute("""
SELECT
AVG(query_time_ms) AS avg_latency,
MAX(query_time_ms) AS max_latency
FROM information_schema.queries_statistics
WHERE query_start_time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
""")
avg_latency, max_latency = cursor.fetchone()
# 告警阈值
if avg_latency > 1000: # 平均延迟 > 1s
send_alert(f"查询延迟过高:{avg_latency}ms")
if max_latency > 5000: # 最大延迟 > 5s
send_alert(f"查询超时:{max_latency}ms")
性能指标:
| 指标 | 目标值 | 实际值 |
|---|---|---|
| 查询延迟(P99) | < 500ms | 100-300ms |
| 并发 QPS | > 1000 | 1500-2000 |
| 数据延迟 | < 10s | 3-5s |
| 可用性 | > 99.9% | 99.95% |
来源:根据 Apache Doris 官方文档和社区最佳实践整理
21. Doris 的存算分离架构是如何实现的?有什么优势和劣势?
答案:
存算分离架构:
Doris 3.0+ 版本支持存算分离架构,将计算和存储解耦。
架构对比:
存算分离组件:
| 组件 | 职责 | 说明 |
|---|---|---|
| FE | 元数据管理、查询调度 | 与存算一体相同 |
| Compute Node | 查询计算 | 无状态,可弹性扩缩容 |
| Meta Service | 元数据服务 | 管理对象存储的元数据 |
| 对象存储(S3/OSS) | 数据存储 | 存储实际数据 |
存算分离优势:
1. 弹性扩缩容
# 存算一体:扩容需要迁移数据
# 1. 添加 BE 节点
# 2. 数据均衡(耗时数小时到数天)
# 3. 才能提供服务
# 存算分离:扩容无需迁移数据
# 1. 添加 Compute Node
# 2. 立即提供服务(秒级)
2. 成本优化
存算一体:
├── 计算资源:高配 SSD($1000/月/节点)
├── 存储资源:高配 SSD($1000/月/节点)
└── 总成本:$2000/月/节点
存算分离:
├── 计算资源:高配 CPU/内存($500/月/节点)
├── 存储资源:对象存储 S3($0.02/GB/月)
└── 总成本:$500/月/节点 + 存储成本
成本节省:50-70%(对于存储密集型场景)
3. 资源隔离
-- 不同业务使用不同的 Compute Node 集群
-- 业务 A:实时查询(高优先级)
CREATE COMPUTE CLUSTER realtime_cluster
PROPERTIES (
"min_nodes" = "10",
"max_nodes" = "50"
);
-- 业务 B:离线分析(低优先级)
CREATE COMPUTE CLUSTER batch_cluster
PROPERTIES (
"min_nodes" = "5",
"max_nodes" = "20"
);
4. 冷热分层
-- 热数据:本地缓存
-- 温数据:对象存储
-- 冷数据:归档存储
ALTER TABLE large_table SET (
"storage_policy" = "tiered_storage",
"hot_data_ttl" = "7d", -- 热数据保留 7 天
"warm_data_ttl" = "30d", -- 温数据保留 30 天
"cold_data_ttl" = "365d" -- 冷数据保留 1 年
);
存算分离劣势:
1. 查询性能
存算一体:
├── 数据本地读取
├── 延迟:1-10ms
└── 吞吐量:1-5GB/s
存算分离:
├── 数据网络读取(S3)
├── 延迟:10-100ms
└── 吞吐量:100-500MB/s
性能差距:10-50 倍(无缓存情况下)
2. 缓存依赖
# 存算分离严重依赖缓存
# 缓存命中率 > 80% 才能保证性能
# 缓存策略:
# 1. 本地 SSD 缓存(热数据)
# 2. 内存缓存(超热数据)
# 3. 预热机制(提前加载)
3. 网络带宽
# 存算分离需要大量网络带宽
# 推荐:10Gbps+ 网络
# 网络成本:
# 数据传输费用(S3 → Compute Node)
# 可能占总成本的 20-30%
存算分离适用场景:
| 场景 | 推荐架构 | 原因 |
|---|---|---|
| 存储密集型 | 存算分离 | 存储成本低 |
| 计算密集型 | 存算一体 | 查询性能高 |
| 弹性需求高 | 存算分离 | 快速扩缩容 |
| 实时性要求高 | 存算一体 | 延迟低 |
| 多租户隔离 | 存算分离 | 资源隔离 |
22. 如何在 Doris 中实现实时数据去重?
答案:
去重场景:
在实时数据处理中,经常需要对数据进行去重,例如:
- 用户去重(UV 统计)
- 订单去重(防止重复计算)
- 事件去重(防止重复消费)
去重方案对比:
| 方案 | 精确度 | 性能 | 存储开销 | 适用场景 |
|---|---|---|---|---|
| Unique Key 模型 | 精确 | 中等 | 高 | 维度表、订单表 |
| Bitmap 去重 | 精确 | 高 | 中等 | 低基数去重(< 100 万) |
| HLL 去重 | 近似(误差 < 2%) | 很高 | 低 | 高基数去重(> 100 万) |
| 窗口去重 | 精确 | 低 | 高 | 小数据量 |
方案 1:Unique Key 模型
-- 创建 Unique Key 表
CREATE TABLE user_profile (
user_id BIGINT,
username VARCHAR(100),
age INT,
city VARCHAR(50),
update_time DATETIME
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true" -- 写时合并,查询快
);
-- 插入数据(自动去重)
INSERT INTO user_profile VALUES
(1, 'Alice', 25, 'Beijing', NOW()),
(1, 'Alice', 26, 'Shanghai', NOW()); -- 相同 user_id,后者覆盖前者
-- 查询(已去重)
SELECT * FROM user_profile WHERE user_id = 1;
-- 结果:(1, 'Alice', 26, 'Shanghai', NOW())
优点:
- 精确去重
- 支持实时更新
- 查询简单
缺点:
- 写入性能较低(需要合并)
- 存储开销大(保留所有版本)
方案 2:Bitmap 去重
-- 创建 Bitmap 聚合表
CREATE TABLE user_daily_active (
date DATE,
event_type VARCHAR(50),
active_users BITMAP BITMAP_UNION
)
AGGREGATE KEY(date, event_type)
DISTRIBUTED BY HASH(date) BUCKETS 16;
-- 插入数据(自动去重)
INSERT INTO user_daily_active VALUES
('2024-03-10', 'login', TO_BITMAP(1)),
('2024-03-10', 'login', TO_BITMAP(2)),
('2024-03-10', 'login', TO_BITMAP(1)); -- 重复,自动去重
-- 查询去重后的用户数
SELECT
date,
event_type,
BITMAP_COUNT(active_users) AS uv
FROM user_daily_active
WHERE date = '2024-03-10';
-- 结果:('2024-03-10', 'login', 2) -- 去重后只有 2 个用户
Bitmap 高级用法:
-- 交集:同时登录和购买的用户
SELECT BITMAP_COUNT(
BITMAP_INTERSECT(
(SELECT active_users FROM user_daily_active WHERE event_type = 'login'),
(SELECT active_users FROM user_daily_active WHERE event_type = 'purchase')
)
) AS overlap_users;
-- 并集:登录或购买的用户
SELECT BITMAP_COUNT(
BITMAP_UNION(
(SELECT active_users FROM user_daily_active WHERE event_type = 'login'),
(SELECT active_users FROM user_daily_active WHERE event_type = 'purchase')
)
) AS total_users;
-- 差集:登录但未购买的用户
SELECT BITMAP_COUNT(
BITMAP_ANDNOT(
(SELECT active_users FROM user_daily_active WHERE event_type = 'login'),
(SELECT active_users FROM user_daily_active WHERE event_type = 'purchase')
)
) AS non_purchase_users;
优点:
- 精确去重
- 性能高
- 支持集合运算
缺点:
- 只适合低基数(< 100 万)
- 存储开销随基数增长
方案 3:HLL 去重
-- 创建 HLL 聚合表
CREATE TABLE page_view_stats (
page_url VARCHAR(500),
date DATE,
unique_visitors HLL HLL_UNION
)
AGGREGATE KEY(page_url, date)
DISTRIBUTED BY HASH(page_url) BUCKETS 32;
-- 插入数据(自动去重)
INSERT INTO page_view_stats VALUES
('/home', '2024-03-10', HLL_HASH(1)),
('/home', '2024-03-10', HLL_HASH(2)),
('/home', '2024-03-10', HLL_HASH(1)); -- 重复,自动去重
-- 查询去重后的用户数(近似)
SELECT
page_url,
date,
HLL_CARDINALITY(unique_visitors) AS uv
FROM page_view_stats
WHERE date = '2024-03-10';
-- 结果:('/home', '2024-03-10', 2) -- 近似值,误差 < 2%
HLL 高级用法:
-- 多天 UV 合并
SELECT
page_url,
HLL_CARDINALITY(HLL_UNION(unique_visitors)) AS total_uv
FROM page_view_stats
WHERE date >= '2024-03-01' AND date <= '2024-03-10'
GROUP BY page_url;
优点:
- 适合高基数(> 100 万)
- 存储开销固定(12KB)
- 性能极高
缺点:
- 近似去重(误差 < 2%)
- 不支持精确查询
实时去重最佳实践:
场景 1:用户维度表去重
-- 使用 Unique Key 模型
CREATE TABLE user_dim (
user_id BIGINT,
username VARCHAR(100),
age INT,
city VARCHAR(50)
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"enable_unique_key_merge_on_write" = "true"
);
场景 2:UV 统计去重
-- 低基数(< 100 万):使用 Bitmap
CREATE MATERIALIZED VIEW daily_uv_bitmap AS
SELECT
date,
BITMAP_UNION(TO_BITMAP(user_id)) AS active_users
FROM user_events
GROUP BY date;
-- 高基数(> 100 万):使用 HLL
CREATE MATERIALIZED VIEW daily_uv_hll AS
SELECT
date,
HLL_UNION(HLL_HASH(user_id)) AS unique_visitors
FROM user_events
GROUP BY date;
性能对比:
测试场景:1 亿用户,统计 DAU
Unique Key 模型:
├── 查询时间:5-10s
├── 存储空间:100GB
└── 精确度:100%
Bitmap 去重:
├── 查询时间:100-500ms
├── 存储空间:10GB
└── 精确度:100%
HLL 去重:
├── 查询时间:50-100ms
├── 存储空间:12KB
└── 精确度:98-99%
总结
Apache Doris 已经从单纯的 OLAP 数据库进化为 AI-Native 数据平台。通过向量检索、全文检索、AI 函数的深度集成,Doris 成为大模型时代构建 RAG 系统的理想选择。
核心优势:
- 架构极简:一个数据库统一存储结构化、非结构化和向量数据
- 极致性能:MPP 并行计算,亿级向量毫秒级查询
- 实时性强:秒级数据导入和查询响应
- 成本低:减少组件数量,降低运维复杂度
- 易用性好:兼容 MySQL 协议,标准 SQL
适用场景:
- 实时数据大屏
- 用户行为分析
- 企业 BI 报表
- 企业知识库 RAG
- 日志分析系统
- 推荐系统特征计算
技术亮点:
- 向量检索:HNSW 算法,支持亿级向量毫秒级查询
- 全文检索:倒排索引 + BM25 算法,精准文本检索
- 混合检索:一条 SQL 同时支持向量召回和关键词召回
- 物化视图:查询加速 20-100 倍
- 存算分离:弹性扩缩容,成本优化 50-70%
未来展望:
- 更强的 AI 能力(模型推理、特征工程)
- 更好的云原生支持(Kubernetes、Serverless)
- 更丰富的生态集成(Flink、Spark、Kafka)
- 更智能的查询优化(自适应优化、自动调优)
来源:根据 Apache Doris 官方文档和社区最佳实践整理
附录:Doris 完整实战案例
案例:构建电商实时数据分析平台
业务需求:
某电商平台需要构建实时数据分析平台,支持以下功能:
- 实时销售大屏(秒级延迟)
- 用户行为分析(漏斗分析、留存分析)
- 商品推荐(实时特征计算)
- 运营报表(多维分析)
技术架构:
数据模型设计:
1. ODS 层(原始数据层)
-- 订单明细表
CREATE TABLE ods_orders (
order_id VARCHAR(50),
user_id BIGINT,
product_id INT,
product_name VARCHAR(200),
category VARCHAR(50),
price DECIMAL(18, 2),
quantity INT,
amount DECIMAL(18, 2),
order_time DATETIME,
status VARCHAR(20)
)
DUPLICATE KEY(order_id, order_time)
PARTITION BY RANGE(order_time) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 64
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7",
"dynamic_partition.end" = "3"
);
-- 用户行为表
CREATE TABLE ods_user_behavior (
event_id BIGINT,
user_id BIGINT,
event_time DATETIME,
event_type VARCHAR(50),
page_url VARCHAR(500),
product_id INT,
properties JSON
)
DUPLICATE KEY(event_id, user_id, event_time)
PARTITION BY RANGE(event_time) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 128
PROPERTIES (
"replication_num" = "3",
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-7",
"dynamic_partition.end" = "3"
);
-- 用户维度表
CREATE TABLE dim_users (
user_id BIGINT,
username VARCHAR(100),
gender VARCHAR(10),
age INT,
city VARCHAR(50),
register_time DATETIME,
user_level VARCHAR(20)
)
UNIQUE KEY(user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true"
);
-- 商品维度表
CREATE TABLE dim_products (
product_id INT,
product_name VARCHAR(200),
category VARCHAR(50),
brand VARCHAR(100),
price DECIMAL(18, 2),
stock INT
)
UNIQUE KEY(product_id)
DISTRIBUTED BY HASH(product_id) BUCKETS 16
PROPERTIES (
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true"
);
2. DWS 层(汇总数据层)
-- 实时销售汇总(物化视图)
CREATE MATERIALIZED VIEW dws_sales_realtime AS
SELECT
DATE_TRUNC('minute', order_time) AS minute,
category,
city,
SUM(amount) AS total_amount,
SUM(quantity) AS total_quantity,
COUNT(DISTINCT order_id) AS order_count,
COUNT(DISTINCT user_id) AS user_count
FROM ods_orders o
JOIN dim_users u ON o.user_id = u.user_id
GROUP BY minute, category, city;
-- 用户行为汇总(Bitmap 去重)
CREATE MATERIALIZED VIEW dws_user_behavior_daily AS
SELECT
DATE(event_time) AS date,
event_type,
BITMAP_UNION(TO_BITMAP(user_id)) AS active_users
FROM ods_user_behavior
GROUP BY date, event_type;
-- 商品销售排行
CREATE MATERIALIZED VIEW dws_product_sales_rank AS
SELECT
DATE(order_time) AS date,
product_id,
product_name,
category,
SUM(amount) AS total_amount,
SUM(quantity) AS total_quantity,
COUNT(DISTINCT user_id) AS buyer_count
FROM ods_orders
GROUP BY date, product_id, product_name, category;
3. ADS 层(应用数据层)
-- 实时大屏指标
CREATE VIEW ads_realtime_dashboard AS
SELECT
DATE_FORMAT(minute, '%H:%i') AS time,
SUM(total_amount) AS gmv,
SUM(order_count) AS orders,
SUM(user_count) AS buyers
FROM dws_sales_realtime
WHERE minute >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY minute
ORDER BY minute;
-- 类目销售排行
CREATE VIEW ads_category_rank AS
SELECT
category,
SUM(total_amount) AS amount,
SUM(order_count) AS orders
FROM dws_sales_realtime
WHERE minute >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY category
ORDER BY amount DESC
LIMIT 10;
-- 地区销售分布
CREATE VIEW ads_city_distribution AS
SELECT
city,
SUM(total_amount) AS amount,
SUM(user_count) AS buyers
FROM dws_sales_realtime
WHERE minute >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY city
ORDER BY amount DESC
LIMIT 20;
数据导入实现:
Flink 实时导入:
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;
import org.apache.doris.flink.sink.DorisSink;
public class RealtimeDataPipeline {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(8);
env.enableCheckpointing(60000); // 1 分钟 Checkpoint
// 消费 Kafka 订单数据
FlinkKafkaConsumer<String> orderSource = new FlinkKafkaConsumer<>(
"orders",
new SimpleStringSchema(),
getKafkaProps()
);
// 配置 Doris Sink
DorisOptions dorisOptions = DorisOptions.builder()
.setFenodes("fe1:8030,fe2:8030,fe3:8030")
.setTableIdentifier("ecommerce.ods_orders")
.setUsername("root")
.setPassword("")
.build();
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setLabelPrefix("order_ingestion")
.setStreamLoadProp(Properties.builder()
.put("format", "json")
.put("read_json_by_line", "true")
.put("load_to_single_tablet", "false")
.build())
.setBufferFlushMaxRows(100000)
.setBufferFlushMaxBytes(100 * 1024 * 1024)
.setBufferFlushInterval(10000)
.setMaxRetries(3)
.build();
DorisSink<String> sink = DorisSink.<String>builder()
.setDorisOptions(dorisOptions)
.setDorisExecutionOptions(executionOptions)
.setSerializer(new SimpleStringSerializer())
.build();
// 执行任务
env.addSource(orderSource)
.sinkTo(sink);
env.execute("Realtime Order Ingestion");
}
private static Properties getKafkaProps() {
Properties props = new Properties();
props.setProperty("bootstrap.servers", "kafka1:9092,kafka2:9092");
props.setProperty("group.id", "doris-consumer");
props.setProperty("auto.offset.reset", "latest");
return props;
}
}
查询实现:
1. 实时大屏查询:
import pymysql
import json
class RealtimeDashboard:
def __init__(self):
self.conn = pymysql.connect(
host='fe_host',
port=9030,
user='root',
password='',
database='ecommerce'
)
def get_realtime_metrics(self):
"""获取实时指标"""
cursor = self.conn.cursor()
# 最近 1 小时 GMV 趋势
cursor.execute("""
SELECT time, gmv, orders, buyers
FROM ads_realtime_dashboard
ORDER BY time
""")
gmv_trend = cursor.fetchall()
# 类目销售排行
cursor.execute("""
SELECT category, amount, orders
FROM ads_category_rank
""")
category_rank = cursor.fetchall()
# 地区销售分布
cursor.execute("""
SELECT city, amount, buyers
FROM ads_city_distribution
""")
city_distribution = cursor.fetchall()
return {
'gmv_trend': gmv_trend,
'category_rank': category_rank,
'city_distribution': city_distribution
}
def get_current_gmv(self):
"""获取当前 GMV"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT SUM(total_amount) AS gmv
FROM dws_sales_realtime
WHERE minute >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
""")
result = cursor.fetchone()
return result[0] if result else 0
# 使用示例
dashboard = RealtimeDashboard()
metrics = dashboard.get_realtime_metrics()
print(json.dumps(metrics, indent=2))
2. 用户行为分析:
class UserBehaviorAnalysis:
def __init__(self):
self.conn = pymysql.connect(
host='fe_host',
port=9030,
user='root',
password='',
database='ecommerce'
)
def calculate_funnel(self, start_date, end_date):
"""计算转化漏斗"""
cursor = self.conn.cursor()
cursor.execute("""
WITH funnel_users AS (
SELECT
user_id,
MAX(CASE WHEN event_type = 'page_view' THEN 1 ELSE 0 END) AS step1,
MAX(CASE WHEN event_type = 'product_view' THEN 1 ELSE 0 END) AS step2,
MAX(CASE WHEN event_type = 'add_cart' THEN 1 ELSE 0 END) AS step3,
MAX(CASE WHEN event_type = 'order' THEN 1 ELSE 0 END) AS step4
FROM ods_user_behavior
WHERE event_time >= %s AND event_time < %s
GROUP BY user_id
)
SELECT
SUM(step1) AS page_view_users,
SUM(step2) AS product_view_users,
SUM(step3) AS add_cart_users,
SUM(step4) AS order_users,
SUM(step2) * 100.0 / SUM(step1) AS view_rate,
SUM(step3) * 100.0 / SUM(step2) AS cart_rate,
SUM(step4) * 100.0 / SUM(step3) AS order_rate
FROM funnel_users
""", (start_date, end_date))
return cursor.fetchone()
def calculate_retention(self, cohort_date):
"""计算留存率"""
cursor = self.conn.cursor()
cursor.execute("""
WITH day0_users AS (
SELECT active_users AS users
FROM dws_user_behavior_daily
WHERE date = %s AND event_type = 'login'
),
day1_users AS (
SELECT active_users AS users
FROM dws_user_behavior_daily
WHERE date = DATE_ADD(%s, INTERVAL 1 DAY) AND event_type = 'login'
),
day7_users AS (
SELECT active_users AS users
FROM dws_user_behavior_daily
WHERE date = DATE_ADD(%s, INTERVAL 7 DAY) AND event_type = 'login'
)
SELECT
BITMAP_COUNT((SELECT users FROM day0_users)) AS day0_count,
BITMAP_COUNT(BITMAP_INTERSECT(
(SELECT users FROM day0_users),
(SELECT users FROM day1_users)
)) AS day1_retention,
BITMAP_COUNT(BITMAP_INTERSECT(
(SELECT users FROM day0_users),
(SELECT users FROM day7_users)
)) AS day7_retention
""", (cohort_date, cohort_date, cohort_date))
return cursor.fetchone()
# 使用示例
analysis = UserBehaviorAnalysis()
funnel = analysis.calculate_funnel('2024-03-01', '2024-03-10')
retention = analysis.calculate_retention('2024-03-01')
print(f"转化漏斗:{funnel}")
print(f"留存率:{retention}")
性能优化:
1. 查询优化:
-- 使用物化视图加速查询
-- 查询时间:< 100ms(vs 5-10s 扫描明细)
-- 使用 Bitmap 去重
-- UV 计算时间:< 50ms(vs 30-60s 扫描明细)
-- 使用分区裁剪
-- 只扫描相关分区,减少 90% 数据扫描
2. 写入优化:
// Flink Doris Connector 优化
DorisExecutionOptions executionOptions = DorisExecutionOptions.builder()
.setBufferFlushMaxRows(500000) // 增大批次
.setBufferFlushMaxBytes(500 * 1024 * 1024)
.setBufferFlushInterval(5000) // 减小刷新间隔
.build();
// 写入吞吐量:150-200 万行/秒
// 写入延迟:3-5 秒
3. 资源优化:
-- 创建资源组
CREATE WORKLOAD GROUP realtime_query_group
PROPERTIES (
"cpu_share" = "1024",
"memory_limit" = "40%",
"max_concurrency" = "200"
);
-- 大屏查询用户绑定资源组
ALTER USER 'dashboard_user' SET PROPERTIES (
"default_workload_group" = "realtime_query_group"
);
监控告警:
import requests
def monitor_system():
"""监控系统指标"""
# 查询延迟监控
cursor = doris_conn.cursor()
cursor.execute("""
SELECT
AVG(query_time_ms) AS avg_latency,
MAX(query_time_ms) AS max_latency,
COUNT(*) AS query_count
FROM information_schema.queries_statistics
WHERE query_start_time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)
""")
avg_latency, max_latency, query_count = cursor.fetchone()
# 告警
if avg_latency > 1000:
send_alert(f"查询延迟过高:{avg_latency}ms")
if max_latency > 5000:
send_alert(f"查询超时:{max_latency}ms")
# 数据延迟监控
cursor.execute("""
SELECT
MAX(order_time) AS latest_order_time,
TIMESTAMPDIFF(SECOND, MAX(order_time), NOW()) AS data_delay
FROM ods_orders
""")
latest_order_time, data_delay = cursor.fetchone()
if data_delay > 60:
send_alert(f"数据延迟过高:{data_delay}秒")
def send_alert(message):
"""发送告警"""
webhook_url = "https://your-webhook-url"
payload = {
"msgtype": "text",
"text": {"content": f"【告警】{message}"}
}
requests.post(webhook_url, json=payload)
项目成果:
| 指标 | 目标值 | 实际值 | 说明 |
|---|---|---|---|
| 查询延迟(P99) | < 500ms | 100-300ms | 使用物化视图加速 |
| 数据延迟 | < 10s | 3-5s | Flink 实时写入 |
| 并发 QPS | > 1000 | 1500-2000 | 资源组隔离 |
| 写入吞吐量 | > 100 万行/秒 | 150-200 万行/秒 | Flink 并行写入 |
| 存储成本 | - | 节省 70% | 使用 ZSTD 压缩 |
| 可用性 | > 99.9% | 99.95% | 3 副本 + 自动故障转移 |
经验总结:
- 数据分层:ODS → DWS → ADS,职责清晰
- 物化视图:为高频查询创建物化视图,加速 20-100 倍
- Bitmap 去重:UV 统计使用 Bitmap,性能提升 100 倍
- 分区设计:按天分区,自动删除历史数据
- 资源隔离:不同业务使用不同资源组
- 监控告警:实时监控查询延迟、数据延迟
- 压缩优化:使用 ZSTD 压缩,节省 70% 存储
来源:根据实际项目经验总结
常见问题 FAQ
Q1: Doris 和 ClickHouse 如何选择?
A: 根据具体场景选择:
- 选择 Doris:需要实时更新(Unique Key)、高并发查询、AI 应用(向量检索)、标准 SQL
- 选择 ClickHouse:极致写入性能、时序数据、单机部署
Q2: Doris 支持事务吗?
A: Doris 支持导入事务,保证单次导入的原子性。但不支持跨表事务(ACID)。
Q3: Doris 如何实现高可用?
A:
- FE:3 个节点(1 Master + 2 Follower),基于 Paxos 自动选举
- BE:3 副本,自动故障转移
- 数据备份:定期备份到 HDFS/S3
Q4: Doris 的查询性能如何?
A:
- 简单查询:10-100ms
- 复杂聚合:100ms-1s
- 多表 Join:1-5s
- 使用物化视图可加速 20-100 倍
Q5: Doris 支持哪些数据类型?
A:
- 数值类型:TINYINT、SMALLINT、INT、BIGINT、LARGEINT、FLOAT、DOUBLE、DECIMAL
- 字符串类型:CHAR、VARCHAR、STRING、TEXT
- 日期时间类型:DATE、DATETIME、TIMESTAMP
- 复杂类型:ARRAY、MAP、STRUCT、JSON、BITMAP、HLL
Q6: Doris 如何处理数据倾斜?
A:
- 重新选择分桶键(使用高基数列或组合列)
- 增加分桶数
- 使用 Random 分桶
- 数据预处理(加盐)
Q7: Doris 的物化视图会自动刷新吗?
A:
- 同步物化视图:数据写入时自动更新
- 异步物化视图:需要手动刷新或定时刷新
Q8: Doris 支持向量检索吗?
A: 支持。Doris 4.x 版本支持 HNSW 向量索引,可以进行亿级向量毫秒级查询。
Q9: Doris 如何优化查询性能?
A:
- 创建合适的索引(前缀索引、BloomFilter、倒排索引)
- 使用物化视图
- 分区裁剪
- 使用 Colocate Join
- 启用 Runtime Filter
Q10: Doris 的存储成本如何?
A:
- 使用 ZSTD 压缩,压缩比 4-5x
- 存算分离架构,使用对象存储(S3),成本降低 70%
- 冷热分层,冷数据归档
Q11: Doris 支持实时更新吗?
A: 支持。使用 Unique Key 模型,支持主键更新(Upsert)。
Q12: Doris 如何监控?
A:
- Prometheus + Grafana 监控
- 关键指标:查询延迟、QPS、CPU、内存、磁盘使用率
- 日志分析:FE 日志、BE 日志
Q13: Doris 支持多租户吗?
A: 支持。通过资源组(Workload Group)实现资源隔离。
Q14: Doris 的学习曲线如何?
A:
- 基础使用:简单(兼容 MySQL 协议,标准 SQL)
- 高级优化:中等(需要了解架构原理)
- 运维管理:中等(需要了解监控、调优)
Q15: Doris 适合哪些场景?
A:
- 实时数据大屏
- 用户行为分析
- 企业 BI 报表
- 日志分析
- 企业知识库 RAG
- 推荐系统特征计算
Q16: Doris 的社区活跃度如何?
A:
- Apache 顶级项目
- GitHub Star 12K+
- 贡献者 500+
- 企业用户 3000+(包括百度、美团、小米、京东等)
Q17: Doris 支持哪些数据源?
A:
- 关系型数据库:MySQL、PostgreSQL、Oracle、SQL Server
- 大数据存储:HDFS、Hive、Iceberg、Hudi
- 消息队列:Kafka
- 对象存储:S3、OSS、COS
Q18: Doris 的版本升级如何进行?
A:
- 滚动升级:先升级 BE,再升级 FE
- 灰度升级:先升级部分节点,验证后再升级全部
- 版本兼容性:小版本兼容,大版本需要测试
Q19: Doris 支持 SQL 标准吗?
A: 支持 SQL-92 标准,兼容 MySQL 协议。
Q20: Doris 的未来规划是什么?
A:
- 更强的 AI 能力(模型推理、特征工程)
- 更好的云原生支持(Kubernetes、Serverless)
- 更丰富的生态集成(Flink、Spark、Kafka)
- 更智能的查询优化(自适应优化、自动调优)
参考资源
官方资源:
- 官方网站:https://doris.apache.org
- GitHub:https://github.com/apache/doris
- 官方文档:https://doris.apache.org/docs/
- 社区论坛:https://github.com/apache/doris/discussions
学习资源:
- Doris 官方博客:https://doris.apache.org/blog
- Doris 技术公众号:Apache Doris
- Doris 视频教程:B 站搜索"Apache Doris"
- Doris 实战案例:官方文档 Best Practices
社区交流:
- 微信群:关注公众号"Apache Doris"获取入群方式
- Slack:https://join.slack.com/t/apachedoriscommunity
- 邮件列表:[email protected]
相关技术:
- Flink:https://flink.apache.org
- Kafka:https://kafka.apache.org
- Spark:https://spark.apache.org
- ClickHouse:https://clickhouse.com
本文档基于 Apache Doris 4.0 版本编写,内容来源于官方文档、社区最佳实践和实际项目经验。
版本历史
| 版本 | 发布时间 | 重要特性 |
|---|---|---|
| 0.8 | 2018-09 | 开源首个版本 |
| 0.9 | 2019-02 | 支持物化视图 |
| 0.10 | 2019-07 | 支持 Colocation Join |
| 0.11 | 2019-11 | 支持 Broker Load |
| 0.12 | 2020-04 | 支持 Routine Load |
| 0.13 | 2020-09 | 支持动态分区 |
| 0.14 | 2021-05 | 支持 Flink Doris Connector |
| 0.15 | 2021-11 | 支持 Array 类型 |
| 1.0 | 2022-04 | 成为 Apache 顶级项目,向量化执行引擎 |
| 1.1 | 2022-08 | 支持 Light Schema Change |
| 1.2 | 2023-01 | 支持多表物化视图 |
| 2.0 | 2023-08 | 湖仓一体、倒排索引增强 |
| 2.1 | 2024-03 | 支持 Workload Group |
| 3.0 | 2024-08 | 存算分离架构 |
| 4.0 | 2025-01 | 向量检索、全文检索、AI 函数 |
致谢
感谢 Apache Doris 社区的所有贡献者,感谢百度、美团、小米、京东等企业的实践经验分享。
特别感谢:
- Apache Doris PMC 成员
- Doris 核心开发者
- Doris 社区活跃贡献者
- 使用 Doris 的企业用户
文档更新记录
| 日期 | 版本 | 更新内容 |
|---|---|---|
| 2026-03-10 | v1.0 | 初始版本,基于 Doris 4.0 |
| 2026-03-30 | v1.1 | 融合 raw 文档中的 Doris 写入分布、Bucket/Tablet 关系、Flink 实时写入与 Compaction 排障经验 |
文档说明:
本文档旨在提供 Apache Doris 的全面技术指南,涵盖架构原理、性能调优、实战案例和面试题等内容。文档内容基于 Doris 4.0 版本,结合官方文档和实际项目经验编写。
适用人群:
- 大数据工程师
- 数据分析师
- 系统架构师
- 运维工程师
- 准备面试的求职者
使用建议:
- 初学者:从"Doris 概述与架构"开始,逐步深入
- 开发者:重点关注"数据模型与存储"、“索引与查询优化”
- 运维人员:重点关注"性能调优"、“高可用与容灾”、“监控与运维”
- 面试准备:重点关注"高频面试题"章节
反馈与建议:
如有任何问题或建议,欢迎通过以下方式反馈:
- GitHub Issue
- 邮件联系
- 社区讨论
本文档持续更新中,欢迎关注最新版本。