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
20200.13支持物化视图、Colocation Join
20221.0成为 Apache 顶级项目,向量化执行引擎
20232.0湖仓一体、多表物化视图
20243.0存算分离架构、倒排索引增强
20254.0向量检索、全文检索、AI 函数

核心特性

1. 极致性能

  • MPP 并行计算:查询自动分布到多个节点并行执行
  • 向量化执行引擎:批量处理数据,充分利用 CPU SIMD 指令
  • 列式存储:只读取需要的列,大幅减少 I/O
  • 智能索引:前缀索引、ZoneMap、BloomFilter、倒排索引、向量索引

性能对比(TPC-H 1TB 测试):

引擎总耗时相对性能
Doris120s1x(基准)
ClickHouse180s0.67x
Presto450s0.27x
Hive3600s0.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文档检索、智能问答向量检索 + 全文检索
推荐系统实时特征计算、召回排序低延迟点查,高吞吐写入

行业应用

graph LR A[Apache Doris] --> B[互联网] A --> C[金融] A --> D[电商] A --> E[物流] A --> F[制造业] B --> B1[用户行为分析] B --> B2[实时推荐] B --> B3[广告投放分析] C --> C1[风控实时监控] C --> C2[交易分析] C --> C3[客户画像] D --> D1[实时销售大屏] D --> D2[商品分析] D --> D3[库存管理] E --> E1[物流轨迹分析] E --> E2[配送优化] E --> E3[异常检测] F --> F1[生产监控] F --> F2[质量分析] F --> F3[设备预测性维护] style A fill:#4A90E2 style B fill:#7ED321 style C fill:#F5A623 style D fill:#BD10E0 style E fill:#50E3C2 style F fill:#FF6B6B

整体架构

架构设计理念

  • 存算一体:计算和存储在同一节点,减少网络传输
  • 无单点故障:FE 和 BE 都支持多副本
  • 弹性扩展:支持水平扩展,线性提升性能

系统架构图

graph TB subgraph "客户端层" A1[MySQL Client] A2[JDBC/ODBC] A3[BI 工具] A4[Flink/Spark] end subgraph "Frontend 层 (FE)" B1[FE Master] B2[FE Follower] B3[FE Observer] B4[元数据管理] B5[查询解析] B6[查询优化] B7[查询调度] end subgraph "Backend 层 (BE)" C1[BE Node 1] C2[BE Node 2] C3[BE Node N] subgraph "BE 内部" D1[执行引擎] D2[存储引擎] D3[索引管理] D4[Compaction] end end subgraph "存储层" E1[本地磁盘] E2[HDFS] E3[S3/OSS] end A1 --> B1 A2 --> B2 A3 --> B3 A4 --> B1 B1 -.元数据同步.-> B2 B2 -.元数据同步.-> B3 B1 --> C1 B1 --> C2 B1 --> C3 C1 --> D1 C1 --> D2 C1 --> D3 C1 --> D4 D2 --> E1 D2 --> E2 D2 --> E3 style B1 fill:#FF6B6B style B2 fill:#FFA07A style B3 fill:#FFD700 style C1 fill:#4A90E2 style C2 fill:#4A90E2 style C3 fill:#4A90E2

架构层次说明

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 节点信息

元数据存储结构

graph TD A[元数据] --> B[Catalog] B --> C[Database] C --> D[Table] D --> E[Partition] E --> F[Tablet] F --> G[Replica] D --> H[Schema] H --> I[Column] H --> J[Index] style A fill:#FF6B6B style B fill:#FFA07A style C fill:#FFD700 style D fill:#7ED321 style E fill:#4A90E2 style F fill:#BD10E0 style G fill:#50E3C2

2. 查询解析与优化

查询处理流程

graph LR A[SQL 查询] --> B[词法分析] B --> C[语法分析] C --> D[语义分析] D --> E[逻辑计划] E --> F[查询优化] F --> G[物理计划] G --> H[分布式执行计划] H --> I[发送到 BE] style A fill:#4A90E2 style F fill:#FF6B6B style H fill:#7ED321

优化器功能

  • 谓词下推:将过滤条件推到存储层
  • 列裁剪:只读取需要的列
  • 分区裁剪:只扫描相关分区
  • 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:动态生成过滤条件,减少数据扫描

执行引擎架构

graph TB A[查询 Fragment] --> B[Scan Node] B --> C[向量化扫描] C --> D[列裁剪] D --> E[谓词过滤] E --> F[索引加速] F --> G[Aggregation Node] G --> H[预聚合] H --> I[Hash 聚合] I --> J[Join Node] J --> K[Hash Join] J --> L[Broadcast Join] K --> M[Exchange Node] L --> M M --> N[返回结果] style C fill:#4A90E2 style F fill:#FF6B6B style I fill:#7ED321 style K fill:#BD10E0

3. 数据导入

BE 负责接收和写入数据:

  • Stream Load:HTTP 接口,适合小批量实时导入
  • Broker Load:通过 Broker 从 HDFS/S3 导入大批量数据
  • Routine Load:从 Kafka 持续消费数据
  • Insert Into:通过 SQL 插入数据

导入流程

sequenceDiagram participant Client participant FE participant BE participant Storage Client->>FE: 提交导入任务 FE->>FE: 生成导入计划 FE->>BE: 分发导入任务 BE->>BE: 数据解析和转换 BE->>BE: 数据排序和索引构建 BE->>Storage: 写入 Segment 文件 BE->>FE: 报告导入成功 FE->>FE: 更新元数据 FE->>Client: 返回导入结果

4. Compaction(数据合并)

BE 后台自动执行 Compaction,优化存储和查询性能:

Compaction 类型触发条件作用
Base CompactionRowset 数量 > 阈值合并多个小文件,减少文件数
Cumulative Compaction增量数据累积合并增量数据到基线
Vertical Compaction列数较多按列分批合并,降低内存压力

Compaction 优化

  • 自动触发:根据文件数量和大小自动触发
  • 优先级调度:根据 Tablet 访问频率调整优先级
  • 限流控制:避免 Compaction 影响查询性能

元数据管理

元数据架构

Doris 使用 BDBJE(Berkeley DB Java Edition) 存储元数据,基于 Paxos 协议 实现一致性。

元数据同步流程

sequenceDiagram participant Client participant Master participant Follower1 participant Follower2 Client->>Master: DDL 请求 Master->>Master: 生成 Journal 日志 Master->>Follower1: 同步 Journal Master->>Follower2: 同步 Journal Follower1->>Master: ACK Follower2->>Master: ACK Master->>Master: 提交事务 Master->>Client: 返回成功

元数据持久化

  • 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 传统执行

graph LR subgraph "传统执行(线程池)" A1[Scan] --> A2[Filter] A2 --> A3[Agg] A3 --> A4[Join] end subgraph "Pipeline 执行" B1[Scan] -.-> B2[Filter] B2 -.-> B3[Agg] B3 -.-> B4[Join] C1[Scan] -.-> C2[Filter] C2 -.-> C3[Agg] C3 -.-> C4[Join] end style A1 fill:#FFD700 style B1 fill:#4A90E2 style C1 fill:#4A90E2

Pipeline 优势

  • 更高并行度:自动拆分成多个 Pipeline Task
  • 更好的资源利用:动态调度,避免线程阻塞
  • 更低的内存占用:流式处理,减少中间结果物化

Runtime Filter(运行时过滤)

在 Join 执行时动态生成过滤条件,减少数据扫描。

graph LR A[小表 Scan] --> B[构建 Hash Table] B --> C[生成 Runtime Filter] C --> D[下推到大表 Scan] D --> E[过滤数据] E --> F[Join] B --> F style C fill:#FF6B6B style D fill:#7ED321

Runtime Filter 类型

类型适用场景过滤效果
IN Filter小表数据量 < 1024精确过滤
BloomFilter小表数据量较大概率过滤(误判率可控)
MinMax Filter数值类型范围过滤

Compaction 优化

  • 自动触发:根据文件数量和大小自动触发
  • 优先级调度:根据 Tablet 访问频率调整优先级
  • 限流控制:避免 Compaction 影响查询性能

5. 数据副本管理

BE 负责管理数据副本的创建、同步和恢复。

副本同步机制

sequenceDiagram participant FE participant BE1 (Master) participant BE2 (Follower) participant BE3 (Follower) FE->>BE1: 写入数据 BE1->>BE1: 写入本地 BE1->>BE2: 同步数据 BE1->>BE3: 同步数据 BE2->>BE1: ACK BE3->>BE1: ACK BE1->>FE: 写入成功

副本一致性

  • 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)

graph LR A[Version 1] --> D[Compaction] B[Version 2] --> D C[Version 3] --> D D --> E[Merged Version] style D fill:#FF6B6B

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 支持数据冷热分层存储,降低存储成本。

冷热分层架构

graph TB A[热数据] --> B[SSD 本地存储] C[温数据] --> D[HDD 本地存储] E[冷数据] --> F[对象存储 S3/OSS] B --> G[高频查询] D --> H[中频查询] F --> I[低频查询] style B fill:#FF6B6B style D fill:#FFA07A style F fill:#4A90E2

配置冷热分层

-- 创建远程存储资源
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-50ms100-500ms-
适用场景最近 7 天7 天以前-

数据读写流程详解

写入流程

Doris 的写入流程涉及 FE 协调、BE 执行、副本同步、Compaction 等多个环节。

写入流程全景图

sequenceDiagram participant Client participant FE participant BE1 (Master Replica) participant BE2 (Follower Replica) participant BE3 (Follower Replica) participant Storage Client->>FE: 1. 提交写入请求 FE->>FE: 2. 解析和验证 FE->>FE: 3. 生成写入计划 FE->>FE: 4. 选择目标 Tablet FE->>BE1: 5. 分发写入任务 BE1->>BE1: 6. 数据解析和转换 BE1->>BE1: 7. 数据排序(按 Key 列) BE1->>BE1: 8. 构建索引 BE1->>Storage: 9. 写入 Segment 文件 BE1->>BE2: 10. 同步数据到副本 BE1->>BE3: 10. 同步数据到副本 BE2->>BE1: 11. ACK BE3->>BE1: 11. ACK BE1->>FE: 12. 报告写入成功 FE->>FE: 13. 更新元数据(版本号) FE->>Client: 14. 返回写入结果 Note over BE1,Storage: 15. 后台触发 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 选择算法

graph TD A[数据行] --> B{计算分区键} B --> C[确定 Partition] C --> D{计算分桶键} D --> E[Hash 取模] E --> F[确定 Bucket] F --> G[确定 Tablet] G --> H{选择副本} H --> I[Master Replica] style C fill:#FF6B6B style F fill:#4A90E2 style I fill:#7ED321

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 数据处理

数据处理流程

graph LR A[原始数据] --> B[数据解析] B --> C[Schema 转换] C --> D[数据验证] D --> E[数据排序] E --> F[构建索引] F --> G[写入 Segment] style B fill:#4A90E2 style E fill:#FF6B6B style F fill:#7ED321

数据排序

排序规则:
├── 按 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. 副本同步

副本同步机制

graph TB A[Master Replica 写入完成] --> B{选择同步策略} B -->|同步复制| C[等待所有副本 ACK] B -->|异步复制| D[立即返回] C --> E[Quorum 机制] E --> F{多数副本成功?} F -->|是| G[提交事务] F -->|否| H[回滚事务] D --> I[后台同步] I --> J[副本追赶] style E fill:#FF6B6B style G fill:#7ED321 style H fill:#BD10E0

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 元数据

元数据更新流程

sequenceDiagram participant BE participant FE Master participant FE Follower participant BDBJE BE->>FE Master: 报告写入成功 FE Master->>FE Master: 生成版本号 FE Master->>BDBJE: 写入 Journal 日志 FE Master->>FE Follower: 同步 Journal FE Follower->>FE Master: ACK FE Master->>BDBJE: 提交事务 FE Master->>BE: 返回成功

9. Compaction 触发

Compaction 触发条件

Compaction 类型触发条件示例
CumulativeRowset 数量 > 55 个增量文件 -> 1 个文件
BaseCumulative Rowset 数量 > 1010 个 Cumulative -> 1 个 Base
Vertical列数 > 100按列分批合并

Compaction 调度

graph TD A[后台线程扫描] --> B{检查触发条件} B -->|满足| C[计算 Compaction Score] B -->|不满足| A C --> D[选择优先级最高的 Tablet] D --> E[执行 Compaction] E --> F[读取多个 Rowset] F --> G[合并排序] G --> H[写入新 Rowset] H --> I[更新元数据] I --> J[删除旧 Rowset] J --> A style C fill:#FF6B6B style E fill:#4A90E2 style G fill:#7ED321

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 的读取流程涉及查询解析、计划生成、数据扫描、结果聚合等多个环节。

读取流程全景图

sequenceDiagram participant Client participant FE participant BE1 participant BE2 participant BE3 participant Storage Client->>FE: 1. 提交查询请求 FE->>FE: 2. SQL 解析 FE->>FE: 3. 查询优化 FE->>FE: 4. 生成执行计划 FE->>FE: 5. Fragment 划分 FE->>BE1: 6. 分发 Fragment 1 FE->>BE2: 6. 分发 Fragment 2 FE->>BE3: 6. 分发 Fragment 3 BE1->>Storage: 7. 扫描 Tablet BE2->>Storage: 7. 扫描 Tablet BE3->>Storage: 7. 扫描 Tablet Storage->>BE1: 8. 返回数据 Storage->>BE2: 8. 返回数据 Storage->>BE3: 8. 返回数据 BE1->>BE1: 9. 本地聚合 BE2->>BE2: 9. 本地聚合 BE3->>BE3: 9. 本地聚合 BE1->>FE: 10. 返回结果 BE2->>FE: 10. 返回结果 BE3->>FE: 10. 返回结果 FE->>FE: 11. 全局聚合 FE->>Client: 12. 返回最终结果

读取流程详细步骤

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. 查询优化

优化器执行的优化

graph TD A[逻辑计划] --> B[谓词下推] B --> C[列裁剪] C --> D[分区裁剪] D --> E[Join 重排序] E --> F[物化视图改写] F --> G[CBO 优化] G --> H[物理计划] style B fill:#FF6B6B style D fill:#4A90E2 style F fill:#7ED321

优化示例

-- 原始查询
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. 数据扫描

扫描流程

graph TD A[Scan Node] --> B[选择 Tablet] B --> C[选择副本] C --> D[读取 Rowset] D --> E[应用索引过滤] E --> F[读取 Data Page] F --> G[解压缩] G --> H[列裁剪] H --> I[谓词过滤] I --> J[返回数据] style E fill:#FF6B6B style I fill:#4A90E2

索引加速

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/s30%60%
向量化执行500-1000 MB/s80%90%

6. 数据聚合

两阶段聚合

graph TB subgraph "阶段 1:本地聚合(BE)" A1[Scan Node 1] --> B1[Local Agg] A2[Scan Node 2] --> B2[Local Agg] A3[Scan Node 3] --> B3[Local Agg] end subgraph "阶段 2:全局聚合(FE)" B1 --> C[Exchange] B2 --> C B3 --> C C --> D[Global Agg] D --> E[Result] end style B1 fill:#4A90E2 style D fill:#FF6B6B

聚合优化

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 层次结构

graph TD A[Table: orders] --> B[Partition: p20240101] A --> C[Partition: p20240102] B --> D[Bucket 0] B --> E[Bucket 1] B --> F[Bucket 31] D --> G[Tablet: p20240101_0] E --> H[Tablet: p20240101_1] F --> I[Tablet: p20240101_31] G --> J[Replica 1 - BE1] G --> K[Replica 2 - BE2] G --> L[Replica 3 - BE3] style A fill:#FF6B6B style B fill:#FFA07A style D fill:#FFD700 style G fill:#4A90E2 style J fill:#7ED321

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 关系

graph TB subgraph "Table: orders (BUCKETS 4)" A[Table] end subgraph "Partition 层" B[Partition p20240101] C[Partition p20240102] end subgraph "Bucket 层(每个 Partition 都有 4 个 Bucket)" D1[Bucket 0] D2[Bucket 1] D3[Bucket 2] D4[Bucket 3] E1[Bucket 0] E2[Bucket 1] E3[Bucket 2] E4[Bucket 3] end subgraph "Tablet 层(Partition + Bucket)" F1[Tablet: p20240101_0] F2[Tablet: p20240101_1] F3[Tablet: p20240101_2] F4[Tablet: p20240101_3] G1[Tablet: p20240102_0] G2[Tablet: p20240102_1] G3[Tablet: p20240102_2] G4[Tablet: p20240102_3] end subgraph "BE 节点分布(Tablet 分布在不同节点)" H1[BE1: p20240101_0, p20240102_1] H2[BE2: p20240101_1, p20240102_2] H3[BE3: p20240101_2, p20240102_3] H4[BE4: p20240101_3, p20240102_0] end A --> B A --> C B --> D1 B --> D2 B --> D3 B --> D4 C --> E1 C --> E2 C --> E3 C --> E4 D1 --> F1 D2 --> F2 D3 --> F3 D4 --> F4 E1 --> G1 E2 --> G2 E3 --> G3 E4 --> G4 F1 --> H1 F2 --> H2 F3 --> H3 F4 --> H4 G1 --> H4 G2 --> H1 G3 --> H2 G4 --> H3 style A fill:#FF6B6B style B fill:#FFA07A style C fill:#FFA07A style D1 fill:#FFD700 style E1 fill:#FFD700 style F1 fill:#4A90E2 style G1 fill:#4A90E2 style H1 fill:#7ED321

详细示例说明

-- 示例 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 个 Bucket3 个 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 有多个副本分布在不同 BETablet p20240101_18 的 3 个副本在 BE2、BE3、BE4
数据路由先根据分区键确定 Partition,再根据分桶键确定 Bucketdate 确定 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 版本链

graph LR A[Version 1
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增量 RowsetRowset 数 > 5高(分钟级)中等
Base所有 CumulativeCumulative 数 > 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]

术语关系图

完整的数据组织层次

graph TD A[Database] --> B[Table] B --> C[Partition 1] B --> D[Partition 2] C --> E[Bucket 1] C --> F[Bucket 2] E --> G[Tablet] G --> H[Replica 1] G --> I[Replica 2] G --> J[Replica 3] H --> K[Rowset 1] H --> L[Rowset 2] K --> M[Segment 1] K --> N[Segment 2] M --> O[Column 1 Pages] M --> P[Column 2 Pages] M --> Q[Index Data] O --> R[Page 1] O --> S[Page 2] style A fill:#FF6B6B style B fill:#FFA07A style C fill:#FFD700 style E fill:#F5A623 style G fill:#4A90E2 style H fill:#7ED321 style K fill:#50E3C2 style M fill:#BD10E0 style O fill:#B8E986

术语总结表

术语层次说明数量关系
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 列相同也不会合并
  • 仅追加写入:数据只能追加,不能更新或删除
  • 查询性能最优:无需合并数据,直接读取
  • 存储开销最大:保留所有原始数据

数据存储原理

graph LR subgraph "写入数据" A1["Row1: user_id=1, time=10:00, event=click"] A2["Row2: user_id=1, time=10:01, event=view"] A3["Row3: user_id=1, time=10:02, event=click"] end subgraph "存储结果" B1["Row1: user_id=1, time=10:00, event=click"] B2["Row2: user_id=1, time=10:01, event=view"] B3["Row3: user_id=1, time=10:02, event=click"] end A1 --> B1 A2 --> B2 A3 --> B3 style A1 fill:#FFD700 style A2 fill:#FFD700 style A3 fill:#FFD700 style B1 fill:#4A90E2 style B2 fill:#4A90E2 style B3 fill:#4A90E2

建表示例

-- 用户行为日志表
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

数据存储原理

graph LR subgraph "写入数据" A1["Row1: date=2024-01-01, city=Beijing, amount=100"] A2["Row2: date=2024-01-01, city=Beijing, amount=200"] A3["Row3: date=2024-01-01, city=Beijing, amount=150"] end subgraph "存储结果(SUM 聚合)" B1["Row: date=2024-01-01, city=Beijing, amount=450"] end A1 --> B1 A2 --> B1 A3 --> B1 style A1 fill:#FFD700 style A2 fill:#FFD700 style A3 fill:#FFD700 style B1 fill:#4A90E2

建表示例

-- 销售汇总表
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_UNIONHyperLogLog 去重基数估算UV 统计独立访客数
BITMAP_UNIONBitmap 去重精确去重用户去重活跃用户数

聚合过程示例

-- 原始数据
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 合并)
更新支持:★★★☆☆(支持聚合更新)

注意事项

  1. Key 列顺序很重要:影响聚合效率和查询性能
  2. 聚合函数不可更改:建表后无法修改聚合函数
  3. REPLACE 语义:保留最新写入的值(按导入批次)
  4. HLL/BITMAP 精度:HLL 有误差(约 1%),BITMAP 精确但占用空间大

3. Unique 模型(主键模型)

核心特点

  • 主键唯一性:相同主键的数据只保留一条
  • 支持 Upsert:INSERT 时如果主键存在则更新
  • 支持删除:可以删除指定主键的数据
  • 两种实现方式:Merge-on-Read(MoR)和 Merge-on-Write(MoW)

数据存储原理

graph LR subgraph "写入数据" A1["Row1: user_id=1, name=Alice, age=25"] A2["Row2: user_id=1, name=Alice, age=26"] A3["Row3: user_id=1, name=Alice, age=27"] end subgraph "存储结果(保留最新)" B1["Row: user_id=1, name=Alice, age=27"] end A1 --> B1 A2 --> B1 A3 --> B1 style A1 fill:#FFD700 style A2 fill:#FFD700 style A3 fill:#FFD700 style B1 fill:#4A90E2

建表示例

-- 用户信息表(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,默认模式)

graph TB subgraph "写入流程" A1[新数据] --> A2[直接追加到存储] A2 --> A3[标记版本号] end subgraph "查询流程" B1[读取所有版本] --> B2[实时合并] B2 --> B3[返回最新版本] end style A2 fill:#7ED321 style B2 fill:#FF6B6B

特点

  • 写入快:直接追加,无需合并
  • 查询慢:需要实时合并多个版本
  • 存储开销大:保留所有历史版本(Compaction 前)
  • 适用场景:写多读少

Merge-on-Write(MoW)

graph TB subgraph "写入流程" A1[新数据] --> A2[查找旧数据] A2 --> A3[合并更新] A3 --> A4[写入存储] end subgraph "查询流程" B1[直接读取] --> B2[返回结果] end style A3 fill:#FF6B6B style B1 fill:#7ED321

特点

  • 写入慢:需要查找和合并
  • 查询快:直接读取最新数据
  • 存储开销小:只保留最新版本
  • 适用场景:读多写少

性能对比

指标Merge-on-ReadMerge-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(默认)
);

数据模型选择决策树

graph TD A[选择数据模型] --> B{需要更新/删除?} B -->|是| C{读多还是写多?} B -->|否| D{需要聚合?} C -->|读多| E[Unique + MoW] C -->|写多| F[Unique + MoR] D -->|是| G{聚合逻辑固定?} D -->|否| H[Duplicate] G -->|是| I[Aggregate] G -->|否| H style E fill:#7ED321 style F fill:#7ED321 style I fill:#7ED321 style H fill:#7ED321

选择建议总结

需求推荐模型配置
保留所有明细Duplicate默认配置
预聚合统计Aggregate选择合适的聚合函数
实时更新(读多)Unique + MoWenable_unique_key_merge_on_write=true
实时更新(写多)Unique + MoRenable_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(分桶)。

graph TD A[Table] --> B[Partition 1] A --> C[Partition 2] A --> D[Partition N] B --> E[Bucket 1] B --> F[Bucket 2] B --> G[Bucket M] E --> H[Tablet 1] F --> I[Tablet 2] G --> J[Tablet M] H --> K[Replica 1] H --> L[Replica 2] H --> M[Replica 3] style A fill:#FF6B6B style B fill:#FFA07A style E fill:#4A90E2 style H fill:#7ED321 style K fill:#BD10E0

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;

列式存储引擎

列式存储优势

graph LR subgraph "行式存储" A1[Row1: id, name, age, city] A2[Row2: id, name, age, city] A3[Row3: id, name, age, city] end subgraph "列式存储" B1[Column: id, id, id] B2[Column: name, name, name] B3[Column: age, age, age] B4[Column: city, city, city] end style A1 fill:#FFD700 style B1 fill:#4A90E2 style B2 fill:#4A90E2 style B3 fill:#4A90E2 style B4 fill:#4A90E2

列式存储优势

优势说明
列裁剪只读取需要的列,减少 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

数据压缩

压缩算法对比

压缩算法压缩比压缩速度解压速度适用场景
LZ42-3x很快很快默认推荐,平衡性能和压缩比
ZSTD3-5x存储成本敏感场景
Snappy2x很快很快实时性要求高
ZLIB4-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%)

最佳实践

  1. 默认使用 LZ4:性能和压缩比平衡最好
  2. 存储敏感用 ZSTD:压缩比高,性能损失小
  3. 冷数据用 ZLIB:最高压缩比,查询频率低
  4. 实时场景用 Snappy:压缩解压最快

建表注意事项

在 Doris 中建表是一个需要仔细规划的过程,合理的表结构设计直接影响查询性能、存储成本和运维复杂度。以下是建表时需要重点关注的注意事项。

1. 数据模型选择

选择原则

场景推荐模型理由
日志、事件流Duplicate保留所有明细,查询最快
实时指标汇总Aggregate自动聚合,节省存储
维度表、CDCUnique支持主键更新
用户画像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 字节不在索引中,过滤效果差

优化建议

  1. 高基数列在前:如 user_id、order_id
  2. 时间列优先:如 date、datetime(按时间范围查询常见)
  3. 控制字符串长度:避免超长字符串占用索引空间

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 大小分桶数说明
10GB5GB2小表,分桶少
100GB5GB20中等表
1TB5GB200大表
10TB5GB2000超大表

分桶键选择

-- ❌ 错误:低基数列作为分桶键
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 数值类型

整数类型对比

类型存储空间有符号范围无符号范围使用场景
BOOLEAN1字节0 或 1-布尔标志(是/否、真/假)
TINYINT1字节-128 ~ 1270 ~ 255状态码、枚举值、年龄
SMALLINT2字节-32,768 ~ 32,7670 ~ 65,535小范围计数、端口号
INT4字节-2,147,483,648 ~ 2,147,483,6470 ~ 4,294,967,295常规ID、计数器
BIGINT8字节-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,8070 ~ 18,446,744,073,709,551,615用户ID、订单ID、时间戳
LARGEINT16字节-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;

浮点与定点类型

类型存储空间精度范围使用场景
FLOAT4字节单精度±3.4 × 10^38科学计算、近似值(不推荐金融)
DOUBLE8字节双精度±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 日期时间类型

日期时间类型对比

类型存储空间格式范围精度使用场景
DATE4字节YYYY-MM-DD0000-01-01 ~ 9999-12-31生日、日期统计
DATETIME8字节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变长同 STRINGSTRING 的别名文章内容、描述

字符串编码说明

  • 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 类型对比

类型存储空间格式使用场景
IPv44字节点分十进制IPv4 地址存储与查询
IPv616字节冒号分隔十六进制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, BIGINTVARCHAR节省存储,查询更快
小数DECIMALDOUBLE精度保证
日期DATE, DATETIMEVARCHAR支持日期函数
布尔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"
);

注意事项

  1. 分桶键必须相同:两表的 DISTRIBUTED BY 列必须一致
  2. 分桶数必须相同:BUCKETS 数量必须一致
  3. 副本数必须相同:replication_num 必须一致
  4. 不要滥用:只对高频 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)多列组合 HashHASH(date, user_id)组合唯一性高
RANDOM随机分桶RANDOM无明显分桶键
BUCKETS num分桶数量BUCKETS 322的幂次,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-N3生产环境用 3
compression压缩算法LZ4/ZSTD/ZLIB/SNAPPYLZ4默认 LZ4
storage_medium存储介质SSD/HDDHDD热数据用 SSD
bloom_filter_columnsBloomFilter 列列名列表高基数等值查询
enable_unique_key_merge_on_writeMoW 模式true/falsefalse读多写少用 true
colocate_withColocate 组组名大表 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 注意事项

  1. 只支持 Unique 模型:Duplicate 和 Aggregate 模型不支持 UPDATE
  2. 需要主键条件:WHERE 条件必须包含主键
  3. 性能影响:UPDATE 操作会触发数据重写,影响性能
  4. 推荐使用 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 注意事项

  1. 只支持 Unique 模型:Duplicate 和 Aggregate 模型不支持 DELETE
  2. 需要主键条件:WHERE 条件必须包含主键
  3. 性能影响:DELETE 操作会产生删除标记,需要 Compaction 清理
  4. 批量删除:建议使用分区删除或 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;

事务注意事项

  1. 只支持 Unique 模型:Duplicate 和 Aggregate 模型不支持事务
  2. 单表事务:一个事务只能操作一个表
  3. 超时时间:默认事务超时时间为 60 秒
  4. 隔离级别: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_PARSEJSON 数据解析
BITMAP函数10+BITMAP_UNION, BITMAP_INTERSECT精确去重、集合运算
HLL函数8+HLL_UNION_AGG, HLL_CARDINALITY近似去重、基数估算
向量函数5+COSINE_DISTANCE, L2_DISTANCE向量检索、相似度计算

字符串函数

常用字符串函数

函数语法说明示例
CONCATCONCAT(str1, str2, …)拼接字符串CONCAT(‘Hello’, ’ ‘, ‘World’) → ‘Hello World’
CONCAT_WSCONCAT_WS(sep, str1, str2, …)用分隔符拼接CONCAT_WS(’,’, ‘a’, ‘b’, ‘c’) → ‘a,b,c’
SUBSTRINGSUBSTRING(str, pos, len)截取子串SUBSTRING(‘Hello’, 2, 3) → ’ell’
LENGTHLENGTH(str)字节长度LENGTH(‘你好’) → 6
CHAR_LENGTHCHAR_LENGTH(str)字符长度CHAR_LENGTH(‘你好’) → 2
UPPERUPPER(str)转大写UPPER(‘hello’) → ‘HELLO’
LOWERLOWER(str)转小写LOWER(‘HELLO’) → ‘hello’
TRIMTRIM([BOTH|LEADING|TRAILING] str FROM str)去除空格TRIM(’ hello ‘) → ‘hello’
REPLACEREPLACE(str, from, to)替换字符串REPLACE(‘hello’, ’l’, ‘L’) → ‘heLLo’
SPLIT_PARTSPLIT_PART(str, delim, part)分割取值SPLIT_PART(‘a,b,c’, ‘,’, 2) → ‘b’
REGEXP_EXTRACTREGEXP_EXTRACT(str, pattern, index)正则提取REGEXP_EXTRACT(‘abc123’, ‘[0-9]+’, 0) → ‘123’
REGEXP_REPLACEREGEXP_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;

日期时间函数

常用日期时间函数

函数语法说明示例
NOWNOW()当前时间NOW() → ‘2024-03-20 10:30:00’
CURDATECURDATE()当前日期CURDATE() → ‘2024-03-20’
DATE_FORMATDATE_FORMAT(date, format)格式化日期DATE_FORMAT(NOW(), ‘%Y-%m-%d’) → ‘2024-03-20’
DATE_ADDDATE_ADD(date, INTERVAL expr unit)日期加法DATE_ADD(‘2024-01-01’, INTERVAL 7 DAY) → ‘2024-01-08’
DATE_SUBDATE_SUB(date, INTERVAL expr unit)日期减法DATE_SUB(‘2024-01-08’, INTERVAL 7 DAY) → ‘2024-01-01’
DATEDIFFDATEDIFF(date1, date2)日期差(天)DATEDIFF(‘2024-01-08’, ‘2024-01-01’) → 7
UNIX_TIMESTAMPUNIX_TIMESTAMP([date])转时间戳UNIX_TIMESTAMP(‘2024-01-01 00:00:00’) → 1704038400
FROM_UNIXTIMEFROM_UNIXTIME(timestamp)时间戳转日期FROM_UNIXTIME(1704038400) → ‘2024-01-01 00:00:00’
YEARYEAR(date)提取年份YEAR(‘2024-03-20’) → 2024
MONTHMONTH(date)提取月份MONTH(‘2024-03-20’) → 3
DAYDAY(date)提取日DAY(‘2024-03-20’) → 20
HOURHOUR(datetime)提取小时HOUR(‘2024-03-20 10:30:00’) → 10
WEEKWEEK(date)提取周数WEEK(‘2024-03-20’) → 12
DAYOFWEEKDAYOFWEEK(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';

日期时间格式符号

格式符说明示例
%Y4位年份2024
%y2位年份24
%m2位月份03
%c月份(1-12)3
%d2位日期20
%e日期(1-31)20
%H24小时制小时14
%h12小时制小时02
%i分钟30
%s45
%W星期名称Wednesday
%w星期数字(0-6)3
%M月份名称March

数值函数

常用数值函数

函数语法说明示例
ROUNDROUND(x, d)四舍五入ROUND(3.1415, 2) → 3.14
CEILCEIL(x)向上取整CEIL(3.14) → 4
FLOORFLOOR(x)向下取整FLOOR(3.99) → 3
ABSABS(x)绝对值ABS(-10) → 10
MODMOD(x, y)取模MOD(10, 3) → 1
POWERPOWER(x, y)幂运算POWER(2, 3) → 8
SQRTSQRT(x)平方根SQRT(16) → 4
RANDRAND([seed])随机数RAND() → 0.123456
GREATESTGREATEST(x1, x2, …)最大值GREATEST(1, 5, 3) → 5
LEASTLEAST(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;

聚合函数

常用聚合函数

函数语法说明示例
COUNTCOUNT(*) / COUNT(col)计数COUNT(*) → 总行数
SUMSUM(col)求和SUM(amount) → 总金额
AVGAVG(col)平均值AVG(score) → 平均分
MINMIN(col)最小值MIN(price) → 最低价
MAXMAX(col)最大值MAX(price) → 最高价
GROUP_CONCATGROUP_CONCAT(col [ORDER BY] [SEPARATOR])字符串聚合GROUP_CONCAT(name, ‘,’) → ‘a,b,c’
STDDEVSTDDEV(col)标准差STDDEV(score) → 标准差
VARIANCEVARIANCE(col)方差VARIANCE(score) → 方差
PERCENTILEPERCENTILE(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_NUMBERROW_NUMBER() OVER(…)行号(连续)排序、分页
RANKRANK() OVER(…)排名(跳跃)排行榜
DENSE_RANKDENSE_RANK() OVER(…)排名(连续)排行榜
LAGLAG(col, n) OVER(…)前n行的值环比计算
LEADLEAD(col, n) OVER(…)后n行的值预测分析
FIRST_VALUEFIRST_VALUE(col) OVER(…)窗口第一个值基准对比
LAST_VALUELAST_VALUE(col) OVER(…)窗口最后一个值趋势分析
SUMSUM(col) OVER(…)累计求和累计统计
AVGAVG(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_AGGARRAY_AGG(col)聚合成数组ARRAY_AGG(tag) → [’tag1’, ’tag2’]
COLLECT_LISTCOLLECT_LIST(col)聚合成数组(含NULL)COLLECT_LIST(value) → [1, 2, NULL, 3]
ARRAY_CONTAINSARRAY_CONTAINS(arr, val)是否包含ARRAY_CONTAINS([1,2,3], 2) → true
ARRAY_SIZEARRAY_SIZE(arr)数组长度ARRAY_SIZE([1,2,3]) → 3
ARRAY_DISTINCTARRAY_DISTINCT(arr)数组去重ARRAY_DISTINCT([1,2,2,3]) → [1,2,3]
ARRAY_SORTARRAY_SORT(arr)数组排序ARRAY_SORT([3,1,2]) → [1,2,3]
ARRAY_UNIONARRAY_UNION(arr1, arr2)数组并集ARRAY_UNION([1,2], [2,3]) → [1,2,3]
ARRAY_INTERSECTARRAY_INTERSECT(arr1, arr2)数组交集ARRAY_INTERSECT([1,2], [2,3]) → [2]
ARRAY_EXCEPTARRAY_EXCEPT(arr1, arr2)数组差集ARRAY_EXCEPT([1,2,3], [2]) → [1,3]
EXPLODEEXPLODE(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_HASHBITMAP_HASH(col)创建 BITMAP数据导入
BITMAP_UNIONBITMAP_UNION(bitmap)BITMAP 并集聚合去重
BITMAP_INTERSECTBITMAP_INTERSECT(bitmap)BITMAP 交集交集计算
BITMAP_UNION_COUNTBITMAP_UNION_COUNT(bitmap)并集计数UV 统计
BITMAP_INTERSECT_COUNTBITMAP_INTERSECT_COUNT(bitmap)交集计数共同用户数
BITMAP_CONTAINSBITMAP_CONTAINS(bitmap, value)是否包含用户判断
BITMAP_HAS_ANYBITMAP_HAS_ANY(bitmap1, bitmap2)是否有交集快速判断
BITMAP_TO_STRINGBITMAP_TO_STRING(bitmap)转字符串调试查看
BITMAP_FROM_STRINGBITMAP_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_HASHHLL_HASH(col)创建 HLL-
HLL_UNION_AGGHLL_UNION_AGG(hll)HLL 并集聚合1-2%
HLL_CARDINALITYHLL_CARDINALITY(hll)HLL 基数估算1-2%
HLL_UNIONHLL_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 选择

对比项BITMAPHLL
精度精确(100%)近似(98-99%)
存储空间较大(基数/8 字节)固定(~16KB)
适用基数< 1亿> 1亿
性能更快
使用建议精确去重、集合运算超大基数估算

向量函数(Doris 4.x 特有)

向量距离函数

函数语法说明使用场景
COSINE_DISTANCECOSINE_DISTANCE(vec1, vec2)余弦距离文本相似度
L2_DISTANCEL2_DISTANCE(vec1, vec2)欧氏距离图像相似度
IP_DISTANCEIP_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_EXTRACTJSON_EXTRACT(json, path)提取 JSON 值
JSON_EXTRACT_STRINGJSON_EXTRACT_STRING(json, path)提取字符串
JSON_EXTRACT_INTJSON_EXTRACT_INT(json, path)提取整数
JSON_EXTRACT_DOUBLEJSON_EXTRACT_DOUBLE(json, path)提取浮点数
JSON_PARSEJSON_PARSE(str)解析 JSON 字符串
JSON_ARRAYJSON_ARRAY(val1, val2, …)创建 JSON 数组
JSON_OBJECTJSON_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 的聚簇索引

索引结构

graph LR A[索引项 1: Row 0] --> B[Data Block 1: Row 0-1023] C[索引项 2: Row 1024] --> D[Data Block 2: Row 1024-2047] E[索引项 3: Row 2048] --> F[Data Block 3: Row 2048-3071] style A fill:#FF6B6B style C fill:#FF6B6B style E fill:#FF6B6B

建表示例

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 原理

graph TD subgraph "Layer 2(稀疏层)" A1[Node 1] --- A2[Node 5] A2 --- A3[Node 9] end subgraph "Layer 1(中间层)" B1[Node 1] --- B2[Node 3] B2 --- B3[Node 5] B3 --- B4[Node 7] B4 --- B5[Node 9] end subgraph "Layer 0(密集层)" C1[Node 1] --- C2[Node 2] C2 --- C3[Node 3] C3 --- C4[Node 4] C4 --- C5[Node 5] C5 --- C6[Node 6] C6 --- C7[Node 7] C7 --- C8[Node 8] C8 --- C9[Node 9] end A1 -.-> B1 A2 -.-> B3 A3 -.-> B5 B1 -.-> C1 B2 -.-> C3 B3 -.-> C5 B4 -.-> C7 B5 -.-> C9 style A1 fill:#FF6B6B style B1 fill:#FFA07A style C1 fill:#4A90E2

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%

倒排索引与全文检索

倒排索引原理

graph LR subgraph "文档" D1[Doc1: Apache Doris is fast] D2[Doc2: Doris supports SQL] D3[Doc3: Fast query engine] end subgraph "倒排索引" T1[apache] --> P1[Doc1] T2[doris] --> P2[Doc1, Doc2] T3[fast] --> P3[Doc1, Doc3] T4[sql] --> P4[Doc2] T5[query] --> P5[Doc3] T6[engine] --> P6[Doc3] end style T1 fill:#4A90E2 style T2 fill:#4A90E2 style T3 fill:#4A90E2

创建倒排索引

-- 创建包含文本列的表
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', '是', '一个', '高性能', '实时', '分析', '数据库']

查询优化器

查询优化流程

graph LR A[SQL 查询] --> B[解析] B --> C[语义分析] C --> D[逻辑计划] D --> E[逻辑优化] E --> F[物理计划] F --> G[物理优化] G --> H[代码生成] H --> I[执行] style E fill:#FF6B6B style G fill:#4A90E2

逻辑优化规则

优化规则说明示例
谓词下推将过滤条件推到数据源WHERE 条件下推到 Scan
列裁剪只读取需要的列SELECT a, b 只读取 a, b 列
分区裁剪只扫描相关分区WHERE date = ‘2024-01-01’ 只扫描该分区
常量折叠编译时计算常量表达式WHERE 1 + 1 = 2 → WHERE TRUE
子查询展开将子查询转换为 JoinIN 子查询 → Semi Join
外连接消除将 Outer Join 转换为 Inner JoinLEFT 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 重排序规则

  1. 小表优先:先 Join 小表,减少中间结果
  2. 过滤条件优先:先 Join 有过滤条件的表
  3. 索引优先:先 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 Join10-100x
EXISTS 子查询转换为 Semi Join10-100x
标量子查询转换为 Left Join5-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 全局聚合

查询优化最佳实践

  1. 使用 EXPLAIN 分析执行计划
  2. 收集统计信息(ANALYZE TABLE)
  3. 合理使用索引(前缀索引、BloomFilter、倒排索引)
  4. **避免 SELECT ***
  5. 使用分区裁剪
  6. 使用物化视图加速查询
  7. 合理使用 Join Hint
  8. 避免复杂的子查询

AI 增强功能

Apache Doris 4.x 版本的核心亮点是 AI 增强功能,将数据库与 AI 深度融合,成为大模型时代的"超级外挂"。

向量检索能力

为什么需要向量检索?

在 RAG(检索增强生成)架构中,向量检索是核心环节:

graph LR A[用户问题] --> B[Embedding 模型] B --> C[查询向量] C --> D[向量数据库] D --> E[相似文档] E --> F[大语言模型] F --> G[生成答案] H[知识库文档] --> I[Embedding 模型] I --> J[文档向量] J --> D style D fill:#FF6B6B style F fill:#4A90E2

传统 RAG 架构的痛点

组件职责问题
MySQL存储元数据无法存储向量
Elasticsearch文本检索不支持向量检索
Milvus/Pinecone向量检索需要额外维护
问题数据分散在多个系统架构复杂、一致性难保证

Doris 统一解决方案

graph TB A[Apache Doris] --> B[结构化数据] A --> C[非结构化文本] A --> D[向量数据] B --> E[SQL 查询] C --> F[全文检索] D --> G[向量检索] E --> H[混合查询] F --> H G --> H style A fill:#FF6B6B style H fill:#4A90E2

向量检索实战

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 函数架构

graph LR A[SQL 查询] --> B[Doris FE] B --> C[AI 函数] C --> D[外部模型服务] D --> E[OpenAI API] D --> F[DeepSeek API] D --> G[本地模型] E --> H[返回结果] F --> H G --> H H --> I[写回表] style C fill:#FF6B6B style D fill:#4A90E2

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 系统架构

graph TB subgraph "数据准备" A1[原始文档] --> A2[文本清洗] A2 --> A3[分块切分] A3 --> A4[Embedding 生成] A4 --> A5[写入 Doris] end subgraph "检索阶段" B1[用户问题] --> B2[Embedding 生成] B2 --> B3[向量检索] B2 --> B4[关键词检索] B3 --> B5[结果融合] B4 --> B5 B5 --> B6[Top K 文档] end subgraph "生成阶段" C1[Top K 文档] --> C2[构建 Prompt] B1 --> C2 C2 --> C3[大语言模型] C3 --> C4[生成答案] end A5 -.存储.-> B3 A5 -.存储.-> B4 style A5 fill:#FF6B6B style B5 fill:#4A90E2 style C3 fill:#7ED321

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 的核心优势

  1. 架构极简:一个数据库搞定所有(结构化 + 非结构化 + 向量)
  2. 高性能:MPP 并行计算,亿级向量毫秒级查询
  3. 实时性:数据写入即可查询,无需同步
  4. 成本低:减少组件数量,降低运维复杂度

数据导入与同步

导入方式对比

Doris 支持多种数据导入方式,适用于不同的场景。

导入方式对比

导入方式适用场景数据量延迟吞吐量事务性
Stream Load实时小批量导入MB-GB秒级中等支持
Broker Load离线大批量导入GB-TB分钟级支持
Routine LoadKafka 实时消费持续流式秒级支持
Insert IntoSQL 插入小批量秒级支持
Flink Doris Connector实时流式写入持续流式秒级很高支持

导入流程对比

graph TB subgraph "Stream Load" A1[HTTP 请求] --> A2[FE 协调] A2 --> A3[BE 写入] A3 --> A4[返回结果] end subgraph "Broker Load" B1[提交任务] --> B2[FE 调度] B2 --> B3[Broker 读取 HDFS/S3] B3 --> B4[BE 写入] B4 --> B5[异步返回] end subgraph "Routine Load" C1[创建任务] --> C2[持续消费 Kafka] C2 --> C3[微批写入] C3 --> C2 end style A3 fill:#4A90E2 style B4 fill:#4A90E2 style C3 fill:#4A90E2

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 是 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.xflink-doris-connector-1.141.2+
1.15.xflink-doris-connector-1.151.2+
1.16.xflink-doris-connector-1.162.0+
1.17.xflink-doris-connector-1.172.0+
1.18.xflink-doris-connector-1.182.1+

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;

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最大缓冲行数50000100000-500000
sink.buffer-flush.max-bytes最大缓冲字节数10MB100MB-500MB
sink.buffer-flush.interval刷新间隔(毫秒)100005000-10000
sink.max-retries最大重试次数33-5
sink.enable-2pc开启两阶段提交falsetrue(Exactly-Once)
sink.enable-delete支持删除操作false根据需求

Source 配置参数

参数说明默认值推荐值
doris.request.retries请求重试次数33
doris.request.connect.timeout连接超时30s30s
doris.request.read.timeout读取超时30s60s
doris.batch.size批次大小10244096
doris.exec.mem.limit内存限制2GB4GB-8GB
doris.deserialize.arrow.async异步反序列化falsetrue

监控与调优

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 关键阈值

观测项查看方式经验阈值风险信号
RowsetNumSHOW TABLET FROM db.table;< 100> 200 进入高风险区
CompactionScoreSHOW PROC '/backends';< 20> 50 表示有压力,> 100 常见于堆积
导入状态SHOW LOAD; / SHOW STREAM LOADFINISHED / 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)

优化配置全景图

graph TB subgraph "连接层优化" A1["FE 连接池配置"] A2["HTTP 连接参数"] A3["负载均衡策略"] end subgraph "写入层优化" B1["批次大小配置"] B2["刷新策略"] B3["并发控制"] B4["两阶段提交"] end subgraph "读取层优化" C1["分区裁剪"] C2["列裁剪"] C3["并行度配置"] C4["内存管理"] end subgraph "网络层优化" D1["压缩传输"] D2["超时配置"] D3["重试策略"] end style A1 fill:#FF6B6B style B1 fill:#4A90E2 style C1 fill:#7ED321 style D1 fill:#F5A623
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();

连接参数对比

参数说明默认值推荐值影响
fenodesFE 节点列表必填3个以上高可用
autoRedirect自动重定向truetrue故障切换
maxConnectionPoolSize最大连接数1020-50并发能力
minConnectionPoolSize最小连接数15-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();

批次大小选择策略

场景行数字节数间隔吞吐量延迟
高吞吐10000001GB30s很高
平衡500000500MB10s中等
低延迟100000100MB5s中等
实时1000010MB1s很低

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是否写入单个 Tablettruefalse并行度 ↑↑↑
timeout超时时间(秒)600600-1200稳定性 ↑
max_filter_ratio最大容错率00.01-0.1容错性 ↑
exec_mem_limit内存限制(字节)2GB4GB-8GB大批次支持 ↑
compress_type压缩类型nonegz/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-48-1632-64根据 CPU 核数
bufferCount235缓冲区数量
maxRetries335重试次数
checkpointInterval120s60s30sCheckpoint 间隔
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批次大小10244096-8192吞吐量 ↑↑
execMemLimit内存限制2GB4GB-8GB大查询支持 ↑
deserializeArrowAsync异步反序列化falsetrueCPU 利用率 ↑↑
deserializeQueueSize队列大小6464-128并发度 ↑
filterQuery过滤下推falsetrue数据传输 ↓↓

分区和列裁剪

-- 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 开销推荐场景
none1x--0%内网高速网络
lz42-3x很快很快50-70%推荐
gz3-5x中等70-80%中等跨机房传输
zstd3-5x70-80%中等平衡选择
bz24-6x75-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/s200-500 MB/s< 200 MB/s
延迟< 5s5-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 异步物化视图

graph TB subgraph "同步物化视图" A1[数据写入] --> A2[同时更新物化视图] A2 --> A3[查询直接读取] end subgraph "异步物化视图" B1[数据写入] --> B2[基表更新] B3[定时任务] --> B4[刷新物化视图] B2 -.触发.-> B3 B5[查询] --> B6[读取物化视图] end style A2 fill:#4A90E2 style B4 fill:#FF6B6B

创建与使用

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_UNIONBitmap 去重
HLL_UNIONHyperLogLog 去重

查询自动改写

-- 原始查询
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=100MBsink.buffer-flush.interval=30ssink.buffer-flush.max-rows=500000,再依据 RowsetNumCompactionScore 逐步微调。
  • 遇到周期性抖动时,不要只看导入链路,还要结合 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扫描时间索引优化、分区裁剪
ShuffleTimeShuffle 时间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 SSD100K+3GB/s+热数据、高并发查询
SATA SSD10K+500MB/s+温数据、中等并发
HDD100-200100MB/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 数据库中。

系统表分类

系统表全景图

graph TB subgraph "元数据类" A1["databases"] A2["tables"] A3["columns"] A4["partitions"] A5["table_constraints"] end subgraph "任务类" B1["loads"] B2["routine_loads"] B3["stream_loads"] B4["tasks"] end subgraph "性能类" C1["backend_active_tasks"] C2["workload_groups"] C3["query_statistics"] end subgraph "集群类" D1["backends"] D2["frontends"] D3["catalogs"] end style A1 fill:#FF6B6B style B1 fill:#4A90E2 style C1 fill:#7ED321 style D1 fill:#F5A623

元数据类系统表

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';

系统表查询注意事项

  1. 性能影响:系统表查询会访问元数据,频繁查询可能影响性能
  2. 权限要求:需要相应的权限才能查询系统表
  3. 数据时效性:部分系统表数据可能有延迟
  4. 查询优化:使用 WHERE 条件过滤,避免全表扫描
  5. 定期清理:历史任务数据会占用空间,需要定期清理

日志管理

日志位置

# 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

架构对比

特性DorisClickHouse
架构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 报表DorisSQL 兼容性好
时序数据ClickHouse时序优化
AI 应用(RAG)Doris向量检索 + 全文检索

Doris vs StarRocks

关系:StarRocks 是 Doris 的分支项目。

差异对比

特性DorisStarRocks
社区Apache 顶级项目独立开源项目
存算分离3.0+ 支持原生支持
物化视图同步 + 异步异步为主
向量检索4.0+ 支持不支持
生态集成更丰富较少

选型建议

  • Doris:社区活跃、生态丰富、AI 增强
  • StarRocks:存算分离、云原生

Doris vs Presto

架构对比

特性DorisPresto
架构存算一体存算分离
数据存储自有存储外部存储(HDFS/S3)
查询性能快(本地存储)中等(网络 I/O)
实时写入支持不支持
数据源单一多数据源联邦查询

选型建议

  • Doris:实时数仓、高性能查询
  • Presto:多数据源联邦查询、即席查询

选型建议

选择 Doris 的场景

  1. 实时数据大屏:秒级延迟、高并发查询
  2. 用户行为分析:实时更新、复杂多维分析
  3. 企业 BI 报表:SQL 兼容性好、物化视图加速
  4. AI 应用(RAG):向量检索 + 全文检索 + 结构化查询
  5. 日志分析:全文检索 + 结构化查询

不选择 Doris 的场景

  1. 极致写入性能:ClickHouse 更优
  2. 多数据源联邦查询:Presto/Trino 更优
  3. 云原生存算分离:StarRocks 更优

实战案例

实时数据大屏

场景描述

  • 实时展示电商平台的销售数据
  • 秒级延迟、高并发查询(1000+ QPS)
  • 多维度分析(时间、地区、商品类别)

架构设计

graph LR A[业务系统] --> B[Kafka] B --> C[Flink] C --> D[Doris] D --> E[数据大屏] style D fill:#FF6B6B

表设计

-- 实时销售明细表
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

系统架构

graph LR A[应用服务] --> B[Filebeat] B --> C[Kafka] C --> D[Flink] D --> E[Doris] E --> F[Kibana/Grafana] G[告警规则] --> E E --> H[告警系统] style E fill:#FF6B6B

表设计

-- 日志表
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 万行/秒
写入延迟< 10s3-5s
查询延迟(全文检索)< 1s200-500ms
查询延迟(精确匹配)< 500ms50-100ms
存储压缩比> 4x4-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 系统的理想选择。

核心优势

  1. 架构极简:一个数据库统一存储结构化、非结构化和向量数据
  2. 极致性能:MPP 并行计算,亿级向量毫秒级查询
  3. 实时性强:秒级数据导入和查询响应
  4. 成本低:减少组件数量,降低运维复杂度

适用场景

  • 实时数据大屏
  • 用户行为分析
  • 企业 BI 报表
  • 企业知识库 RAG
  • 日志分析系统

13. Doris 的 Compaction 机制是如何工作的?如何优化?

答案

Compaction 机制

Compaction 是 Doris 后台自动执行的数据合并过程,用于优化存储和查询性能。

工作原理

graph LR A[数据写入] --> B[生成 Rowset] B --> C[累积多个 Rowset] C --> D[触发 Compaction] D --> E[合并 Rowset] E --> F[删除旧 Rowset] style D fill:#FF6B6B

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_scoreCompaction 分数> 100
compaction_deltas_total待合并文件数> 50
compaction_bytes_total待合并数据量> 100GB

优化建议

  1. 控制写入频率:减少小批量写入,增大批次大小
  2. 合理设置分桶数:避免单个 Tablet 过大
  3. 调整 Compaction 并发度:根据 CPU 和磁盘 I/O 调整
  4. 监控 Compaction 延迟:及时发现 Compaction 堆积

14. Doris 的元数据是如何管理的?FE 故障如何恢复?

答案

元数据管理架构

Doris 使用 BDBJE(Berkeley DB Java Edition) 存储元数据,基于 Paxos 协议 实现一致性。

元数据存储内容

元数据包含:
├── 数据库和表结构(Schema)
├── 分区和分桶信息
├── Tablet 位置和副本信息
├── 用户权限配置
├── 集群拓扑信息
└── 统计信息

元数据同步机制

sequenceDiagram participant Client participant Master participant Follower1 participant Follower2 Client->>Master: DDL 请求 Master->>Master: 生成 Journal 日志 Master->>Follower1: 同步 Journal Master->>Follower2: 同步 Journal Follower1->>Master: ACK Follower2->>Master: ACK Master->>Master: 提交事务(多数派确认) Master->>Client: 返回成功

元数据持久化

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

最佳实践

  1. 部署奇数个 FE:3 个或 5 个(保证多数派)
  2. 定期备份元数据:每天备份 doris-meta 目录
  3. 监控 FE 状态:心跳、元数据同步延迟
  4. 跨机房部署:FE 分布在不同机房,提高可用性

15. Doris 的查询优化器是如何工作的?CBO 和 RBO 的区别是什么?

答案

查询优化器架构

graph LR A[SQL 查询] --> B[解析器] B --> C[语义分析] C --> D[逻辑计划] D --> E[RBO 优化] E --> F[CBO 优化] F --> G[物理计划] G --> H[代码生成] style E fill:#FFA07A style F fill:#FF6B6B

RBO(Rule-Based Optimizer,基于规则优化)

优化规则

规则说明示例
谓词下推将过滤条件推到数据源WHERE 条件下推到 Scan
列裁剪只读取需要的列SELECT a, b 只读取 a, b 列
分区裁剪只扫描相关分区WHERE date = ‘2024-01-01’ 只扫描该分区
常量折叠编译时计算常量表达式WHERE 1 + 1 = 2 → WHERE TRUE
子查询展开将子查询转换为 JoinIN 子查询 → Semi Join
外连接消除将 Outer Join 转换为 Inner JoinLEFT 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 对比

特性RBOCBO
优化依据固定规则统计信息 + 成本模型
优化效果稳定,但可能不是最优更优,但依赖统计信息
执行时间慢(需要计算成本)
适用场景简单查询复杂查询、多表 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)相比有什么优势和劣势?

答案

架构对比

graph TB subgraph "传统 RAG 架构" A1[MySQL] --> A4[应用层] A2[Elasticsearch] --> A4 A3[Milvus] --> A4 A4 --> A5[结果融合] end subgraph "Doris RAG 架构" B1[Doris] --> B2[一条 SQL] B2 --> B3[结果返回] end style A4 fill:#FFD700 style B1 fill:#FF6B6B

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 优化

选型建议

场景推荐方案原因
企业知识库 RAGDoris混合查询、架构简化
推荐系统召回Milvus纯向量检索、极致性能
实时数据分析 + 向量检索Doris统一平台、实时性
超大规模向量检索(10 亿+)Milvus专用优化、GPU 加速
多模态检索(图像、视频)Milvus专用功能

混合方案

场景:电商推荐系统
├── 召回阶段:Milvus(纯向量检索,极致性能)
└── 排序阶段:Doris(特征计算 + 业务规则)

17. 如何设计一个高性能的实时数据大屏系统?

答案

系统架构

graph LR A[业务系统] --> B[Kafka] B --> C[Flink] C --> D[Doris] D --> E[Redis 缓存] E --> F[数据大屏] D --> G[物化视图] G --> E style D fill:#FF6B6B style E fill:#4A90E2

设计要点

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)< 500ms100-300ms
并发 QPS> 10001500-2000
数据延迟< 10s3-5s
可用性> 99.9%99.95%

来源:根据 Apache Doris 官方文档和社区最佳实践整理

21. Doris 的存算分离架构是如何实现的?有什么优势和劣势?

答案

存算分离架构

Doris 3.0+ 版本支持存算分离架构,将计算和存储解耦。

架构对比

graph TB subgraph "存算一体(传统架构)" A1[FE] --> A2[BE 1] A1 --> A3[BE 2] A1 --> A4[BE 3] A2 --> A5[本地磁盘] A3 --> A6[本地磁盘] A4 --> A7[本地磁盘] end subgraph "存算分离(新架构)" B1[FE] --> B2[Compute Node 1] B1 --> B3[Compute Node 2] B1 --> B4[Compute Node 3] B2 --> B5[对象存储 S3/OSS] B3 --> B5 B4 --> B5 B6[Meta Service] --> B5 end style A2 fill:#FFD700 style B2 fill:#4A90E2 style B5 fill:#FF6B6B

存算分离组件

组件职责说明
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 系统的理想选择。

核心优势

  1. 架构极简:一个数据库统一存储结构化、非结构化和向量数据
  2. 极致性能:MPP 并行计算,亿级向量毫秒级查询
  3. 实时性强:秒级数据导入和查询响应
  4. 成本低:减少组件数量,降低运维复杂度
  5. 易用性好:兼容 MySQL 协议,标准 SQL

适用场景

  • 实时数据大屏
  • 用户行为分析
  • 企业 BI 报表
  • 企业知识库 RAG
  • 日志分析系统
  • 推荐系统特征计算

技术亮点

  • 向量检索:HNSW 算法,支持亿级向量毫秒级查询
  • 全文检索:倒排索引 + BM25 算法,精准文本检索
  • 混合检索:一条 SQL 同时支持向量召回和关键词召回
  • 物化视图:查询加速 20-100 倍
  • 存算分离:弹性扩缩容,成本优化 50-70%

未来展望

  • 更强的 AI 能力(模型推理、特征工程)
  • 更好的云原生支持(Kubernetes、Serverless)
  • 更丰富的生态集成(Flink、Spark、Kafka)
  • 更智能的查询优化(自适应优化、自动调优)

来源:根据 Apache Doris 官方文档和社区最佳实践整理

附录:Doris 完整实战案例

案例:构建电商实时数据分析平台

业务需求

某电商平台需要构建实时数据分析平台,支持以下功能:

  1. 实时销售大屏(秒级延迟)
  2. 用户行为分析(漏斗分析、留存分析)
  3. 商品推荐(实时特征计算)
  4. 运营报表(多维分析)

技术架构

graph TB subgraph "数据源" A1[订单系统] A2[用户系统] A3[商品系统] A4[行为埋点] end subgraph "数据采集" B1[Canal CDC] B2[Filebeat] end subgraph "消息队列" C1[Kafka] end subgraph "实时计算" D1[Flink] end subgraph "数据存储" E1[Doris] end subgraph "数据应用" F1[实时大屏] F2[BI 报表] F3[推荐系统] end A1 --> B1 A2 --> B1 A3 --> B1 A4 --> B2 B1 --> C1 B2 --> C1 C1 --> D1 D1 --> E1 E1 --> F1 E1 --> F2 E1 --> F3 style E1 fill:#FF6B6B

数据模型设计

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)< 500ms100-300ms使用物化视图加速
数据延迟< 10s3-5sFlink 实时写入
并发 QPS> 10001500-2000资源组隔离
写入吞吐量> 100 万行/秒150-200 万行/秒Flink 并行写入
存储成本-节省 70%使用 ZSTD 压缩
可用性> 99.9%99.95%3 副本 + 自动故障转移

经验总结

  1. 数据分层:ODS → DWS → ADS,职责清晰
  2. 物化视图:为高频查询创建物化视图,加速 20-100 倍
  3. Bitmap 去重:UV 统计使用 Bitmap,性能提升 100 倍
  4. 分区设计:按天分区,自动删除历史数据
  5. 资源隔离:不同业务使用不同资源组
  6. 监控告警:实时监控查询延迟、数据延迟
  7. 压缩优化:使用 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.82018-09开源首个版本
0.92019-02支持物化视图
0.102019-07支持 Colocation Join
0.112019-11支持 Broker Load
0.122020-04支持 Routine Load
0.132020-09支持动态分区
0.142021-05支持 Flink Doris Connector
0.152021-11支持 Array 类型
1.02022-04成为 Apache 顶级项目,向量化执行引擎
1.12022-08支持 Light Schema Change
1.22023-01支持多表物化视图
2.02023-08湖仓一体、倒排索引增强
2.12024-03支持 Workload Group
3.02024-08存算分离架构
4.02025-01向量检索、全文检索、AI 函数

致谢

感谢 Apache Doris 社区的所有贡献者,感谢百度、美团、小米、京东等企业的实践经验分享。

特别感谢:

  • Apache Doris PMC 成员
  • Doris 核心开发者
  • Doris 社区活跃贡献者
  • 使用 Doris 的企业用户

文档更新记录

日期版本更新内容
2026-03-10v1.0初始版本,基于 Doris 4.0
2026-03-30v1.1融合 raw 文档中的 Doris 写入分布、Bucket/Tablet 关系、Flink 实时写入与 Compaction 排障经验

文档说明

本文档旨在提供 Apache Doris 的全面技术指南,涵盖架构原理、性能调优、实战案例和面试题等内容。文档内容基于 Doris 4.0 版本,结合官方文档和实际项目经验编写。

适用人群

  • 大数据工程师
  • 数据分析师
  • 系统架构师
  • 运维工程师
  • 准备面试的求职者

使用建议

  1. 初学者:从"Doris 概述与架构"开始,逐步深入
  2. 开发者:重点关注"数据模型与存储"、“索引与查询优化”
  3. 运维人员:重点关注"性能调优"、“高可用与容灾”、“监控与运维”
  4. 面试准备:重点关注"高频面试题"章节

反馈与建议

如有任何问题或建议,欢迎通过以下方式反馈:

  • GitHub Issue
  • 邮件联系
  • 社区讨论

本文档持续更新中,欢迎关注最新版本。