Apache Doris 深度讲解:从核心概念到实战项目

Apache Doris 深度讲解:从核心概念到实战项目

本文档基于二八法则,聚焦 Doris 最核心的 20% 知识,帮助你解决 80% 的业务场景。 目标:看完之后,你不仅能理解,还能清晰地讲给别人听。


第〇章:一句话理解 Doris 的本质

Apache Doris 是一个 MPP(大规模并行处理)分析型数据库,用一句话概括就是:

"用写 MySQL 的方式,干 Hadoop 的活。"

传统大数据栈需要 Hadoop + Hive + Spark + Presto + HBase + ES 等一堆组件拼在一起,而 Doris 用一个统一的 SQL 引擎就能覆盖绝大部分 OLAP(联机分析处理)场景------实时报表、日志检索、用户画像、数据联邦查询等。

关键定位(给别人讲时先说这句): Doris 不是替代 MySQL 的 OLTP 数据库,而是用来做"查询分析"的。MySQL 擅长的是一条一条地增删改查;Doris 擅长的是一次扫描几千万、几亿行数据,快速聚合出结果。


第一部分:第一周------基础构建与核心存储哲学

第一章:架构------FE 与 BE 的"前台后厨"模型

1.1 类比理解

把 Doris 想象成一家餐厅:

角色 Doris 组件 职责
前台服务员 FE(Frontend) 接待客人(接收 SQL)、看菜单(管理元数据)、告诉后厨做什么菜(生成查询计划)、算账(返回结果)
后厨大厨 BE(Backend) 真正做菜(存储数据 + 执行计算)、切配原料(数据分片存储)、多个厨师同时炒菜(并行计算)

1.2 FE 的三大核心职责

sql 复制代码
用户 SQL 请求
      │
      ▼
┌─────────────────────────────┐
│         FE (Frontend)        │
│                              │
│  1. 查询解析 & 优化           │ ← 把你的 SQL 翻译成最优执行计划
│  2. 元数据管理               │ ← 记录所有表的定义、分区、权限等
│  3. 节点管理                 │ ← 知道有多少个 BE,它们是否健康
│                              │
│  内部用 Raft 协议做高可用     │ ← 多个 FE 之间自动选主
└──────────────┬──────────────┘
               │ 分发执行计划
               ▼
┌──────┐  ┌──────┐  ┌──────┐
│ BE-1 │  │ BE-2 │  │ BE-3 │   ← 并行执行,各干各的
└──────┘  └──────┘  └──────┘

关键点(面试/讲解必说):

  • FE 是"大脑",BE 是"肌肉"
  • FE 之间用 Raft 协议做高可用(一个 Leader + 多个 Follower/Observer)
  • 用户只需连接 FE(MySQL 协议,端口 9030),完全兼容 MySQL 客户端
  • BE 之间是对等的,没有主从之分,数据通过多副本保证可靠性

1.3 两种部署模式

模式 特点 适用场景
存算耦合(经典模式) BE 同时负责存储和计算,数据在本地磁盘 中小规模集群,对延迟要求高
存算分离(2.0+ 新架构) BE 无状态,数据存在对象存储(S3/OSS),本地仅做缓存 弹性扩缩容,云原生场景

1.4 动手验证

用 Docker 一键部署:

bash 复制代码
# 拉取 Doris 官方 Docker 镜像(推荐 2.1+ 版本)
docker run -d --name doris-standalone \
  -p 9030:9030 -p 8030:8030 -p 8040:8040 \
  apache/doris:2.1-standalone

# 用 MySQL 客户端连接 FE
mysql -h 127.0.0.1 -P 9030 -uroot

# 验证 FE 和 BE 状态
SHOW FRONTENDS\G
SHOW BACKENDS\G

连上后如果 SHOW BACKENDS 显示 Alive: true,说明环境就绪。


第二章:极其重要 三大数据模型------Doris 的灵魂

这是 Doris 最核心、最有特色的设计。建表选错模型,后面一切努力都白费。

2.1 总览对比

模型 关键词 数据特征 典型场景 类比
Duplicate(明细模型) DUPLICATE KEY 保留所有原始记录,一条不丢 日志、埋点、流水 记账本------每笔都记
Aggregate(聚合模型) AGGREGATE KEY 相同 Key 的行自动按指标列聚合 报表统计、PV/UV 计数器------同类合并
Unique(唯一模型) UNIQUE KEY 相同 Key 只保留最新一条 订单状态、用户画像 通讯录------同名覆盖

2.2 明细模型(Duplicate Key)------"有多少存多少"

核心思想: 不做任何去重或聚合,原封不动地保存所有写入的数据。

什么时候用:

  • 你不确定未来要做什么分析(先存下来,以后再说)
  • 需要保留每一条明细记录(如用户点击流、系统日志)
  • 需要做即席查询(Ad-hoc),任意维度都可能作为分析条件
sql 复制代码
-- 示例:用户行为日志表
CREATE TABLE user_behavior_log (
    event_time    DATETIME       NOT NULL COMMENT '事件时间',
    user_id       BIGINT         NOT NULL COMMENT '用户ID',
    event_type    VARCHAR(32)    NOT NULL COMMENT '事件类型: click/view/cart/buy',
    page_url      VARCHAR(256)            COMMENT '页面URL',
    item_id       BIGINT                  COMMENT '商品ID',
    device        VARCHAR(32)             COMMENT '设备类型',
    ip            VARCHAR(64)             COMMENT 'IP地址'
)
DUPLICATE KEY(event_time, user_id, event_type)  -- 排序键,决定数据物理排列顺序
PARTITION BY RANGE(event_time) (
    PARTITION p20260301 VALUES LESS THAN ('2026-03-02'),
    PARTITION p20260302 VALUES LESS THAN ('2026-03-03')
)
DISTRIBUTED BY HASH(user_id) BUCKETS 16;

深入理解 DUPLICATE KEY 的含义:

  • 这里的 KEY 不是"唯一约束",而是排序键(Sort Key)
  • 数据在磁盘上会按照 event_time, user_id, event_type 的顺序物理排列
  • 两条完全一样的数据都会被保留,不会去重

给别人讲时的要点: "Duplicate 模型就是最'老实'的模型,你给它什么它就存什么,绝不擅自修改数据。缺点是数据量会很大,优点是信息最完整。"

2.3 聚合模型(Aggregate Key)------"边存边算"

核心思想: 定义好 Key 列(维度)和 Value 列(指标 + 聚合函数),相同 Key 的数据导入时会自动聚合。

什么时候用:

  • 你只关心聚合后的结果,不关心明细(如每天的总 PV、总销售额)
  • 数据量巨大,需要通过预聚合大幅降低存储和查询成本
  • 报表查询模式固定
sql 复制代码
-- 示例:页面访问统计表
CREATE TABLE page_visit_stats (
    visit_date    DATE           NOT NULL COMMENT '访问日期',
    page_url      VARCHAR(256)   NOT NULL COMMENT '页面URL',
    city          VARCHAR(64)    NOT NULL COMMENT '城市',
    -- 以上是 Key 列(维度),以下是 Value 列(指标)
    pv            BIGINT         SUM      COMMENT '页面浏览量',       -- 自动求和
    uv            BITMAP         BITMAP_UNION COMMENT '独立访客(精确去重)', -- Bitmap 聚合
    max_duration  INT            MAX      COMMENT '最大停留时长(秒)', -- 取最大值
    min_duration  INT            MIN      COMMENT '最小停留时长(秒)'  -- 取最小值
)
AGGREGATE KEY(visit_date, page_url, city)
PARTITION BY RANGE(visit_date) (
    PARTITION p202603 VALUES LESS THAN ('2026-04-01')
)
DISTRIBUTED BY HASH(page_url) BUCKETS 16;

聚合函数可选项:

聚合函数 含义 典型用途
SUM 求和 PV、销售额、点击次数
MAX 取最大值 最大订单金额、最晚登录时间
MIN 取最小值 最小停留时长
REPLACE 用新值覆盖旧值 状态字段(但更推荐用 Unique 模型)
REPLACE_IF_NOT_NULL 非空时才覆盖 部分更新场景
HLL_UNION HyperLogLog 聚合 模糊去重(误差 < 1%,但速度极快)
BITMAP_UNION Bitmap 聚合 精确去重(零误差)

聚合发生在什么时候?

markdown 复制代码
数据导入 ──→ MemTable(内存中初次聚合) ──→ 写入磁盘(Segment)
                                              │
后台 Compaction 会定期合并多个 Segment ─────────→ 进一步聚合
                                              │
查询时,如果还有未合并的 Segment ──────────────→ 查询时再次聚合

注意事项: 聚合模型下,你查询时如果不按 Key 列 GROUP BY,Doris 仍然会在底层做聚合,但你在 SQL 层面看到的可能是"部分聚合"的中间结果。所以最佳实践是查询时始终带上 GROUP BY

2.4 唯一模型(Unique Key)------"同 Key 只留最新"

核心思想: 保证 Key 列的唯一性,新数据覆盖旧数据(UPSERT 语义)。

什么时候用:

  • 需要数据更新的场景(订单状态变更、用户信息修改)
  • 从 MySQL 等 OLTP 数据库同步数据到 Doris(CDC 场景)
  • 需要保证主键唯一
sql 复制代码
-- 示例:订单表(从 MySQL 同步到 Doris)
CREATE TABLE order_detail (
    order_id      BIGINT         NOT NULL COMMENT '订单ID',
    user_id       BIGINT         NOT NULL COMMENT '用户ID',
    order_status  TINYINT                 COMMENT '订单状态: 0待付款 1已付款 2已发货 3已完成 4已退款',
    total_amount  DECIMAL(10,2)           COMMENT '订单金额',
    pay_time      DATETIME                COMMENT '支付时间',
    ship_time     DATETIME                COMMENT '发货时间',
    create_time   DATETIME       NOT NULL COMMENT '创建时间',
    update_time   DATETIME                COMMENT '更新时间'
)
UNIQUE KEY(order_id)
PARTITION BY RANGE(create_time) (
    PARTITION p202603 VALUES LESS THAN ('2026-04-01')
)
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true"  -- 推荐开启 Merge-on-Write
);

2.4.1 Merge-on-Write vs Merge-on-Read(重要面试题)

这是 Unique 模型下的两种实现方式,理解它们的区别至关重要:

vbnet 复制代码
┌─────────────────────────────────────────────────────────────┐
│              Merge-on-Write(写时合并)                       │
│                                                             │
│  写入时:新数据进来 → 立即找到旧数据 → 标记删除旧数据           │
│                    → 只保留最新版本                           │
│  读取时:直接读,不需要合并 → 查询快!                         │
│                                                             │
│  特点:写入稍慢,查询很快                                     │
│  适用:读多写少,或读写均衡                                    │
│  推荐:Doris 2.1+ 默认,绝大多数场景使用这个                   │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│              Merge-on-Read(读时合并)                        │
│                                                             │
│  写入时:新数据直接追加写入,不管旧数据 → 写入快!              │
│  读取时:发现同一个 Key 有多个版本 → 现场合并取最新版本         │
│                                                             │
│  特点:写入快,查询慢(因为要现场去重)                         │
│  适用:写多读少                                               │
│  注意:谓词无法下推,性能较差                                   │
└─────────────────────────────────────────────────────────────┘

给别人讲时的关键句: "Merge-on-Write 是提前做好功课(写入时就合并),查询时就轻松了;Merge-on-Read 是先堆起来,查询时再临时整理,所以查询会慢。现在默认用 Merge-on-Write,因为大部分业务都是读多写少。"

2.5 思考题解答

同样是记录用户的购买行为,什么情况下用 Aggregate?什么情况下用 Unique?

用 Aggregate 的场景:

  • 你只关心"每个用户每天买了多少钱的东西"(聚合结果)
  • 不需要知道每笔订单的具体详情
  • 数据量极大,需要通过预聚合降低存储量
sql 复制代码
-- Aggregate 模式:只关心汇总数据
CREATE TABLE user_purchase_summary (
    purchase_date  DATE    NOT NULL,
    user_id        BIGINT  NOT NULL,
    total_amount   DECIMAL(10,2) SUM,     -- 自动累加每日购买总额
    order_count    BIGINT        SUM      -- 自动累加订单数
)
AGGREGATE KEY(purchase_date, user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 16;

用 Unique 的场景:

  • 每条购买记录都有一个唯一的订单号
  • 订单状态会变化(待付款 → 已付款 → 已发货),需要更新
  • 需要查看每笔订单的完整信息
sql 复制代码
-- Unique 模式:每笔订单都要精确追踪
CREATE TABLE user_purchase_detail (
    order_id       BIGINT         NOT NULL,
    user_id        BIGINT         NOT NULL,
    purchase_date  DATE           NOT NULL,
    status         TINYINT,
    amount         DECIMAL(10,2)
)
UNIQUE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 16;

一句话总结:

  • 问"一共多少" → Aggregate
  • 问"每一条什么情况" → Unique 或 Duplicate

第三章:数据分布策略------分区与分桶

建表语句中最容易写错、对性能影响最大的就是分区和分桶。

3.1 为什么需要分区和分桶?

想象你有一个图书馆,里面有 1 亿本书:

markdown 复制代码
不分区不分桶:所有书堆在一个大房间里
              → 找一本书要翻遍整个房间 ❌

分区(Partition):按"出版年份"分成不同楼层
                   → 找 2026 年的书,只去 2026 楼层 ✓

分桶(Bucket):每个楼层内部,按"作者姓氏首字母"分成不同书架
               → 找张三的书,只去 Z 书架 ✓✓

3.2 两层数据划分

scss 复制代码
                    Table(表)
                       │
          ┌────────────┼────────────┐
          │            │            │
     Partition-1  Partition-2  Partition-3     ← 第一层:分区(通常按时间)
     (2026-03-01) (2026-03-02) (2026-03-03)
          │
    ┌─────┼─────┐
    │     │     │
  Bucket  Bucket  Bucket                       ← 第二层:分桶(按 Hash Key)
  (Tab-1) (Tab-2) (Tab-3)
    │
  Tablet                                       ← 最小物理存储单元(含多副本)

Tablet 是 Doris 数据管理的原子单位:

  • 每个 Tablet 是一个独立的数据分片
  • 多副本(默认 3 副本)分布在不同的 BE 节点上
  • 查询时,多个 Tablet 可以被不同的 BE 并行扫描

3.3 分区(Partition)详解

分区的作用:数据裁剪(Partition Pruning)。 查询时,Doris 根据 WHERE 条件自动跳过不相关的分区,大幅减少扫描数据量。

3.3.1 Range 分区(最常用)

按连续范围划分,典型场景是时间

sql 复制代码
-- 手动 Range 分区
PARTITION BY RANGE(event_date) (
    PARTITION p20260301 VALUES LESS THAN ('2026-03-02'),
    PARTITION p20260302 VALUES LESS THAN ('2026-03-03'),
    PARTITION p20260303 VALUES LESS THAN ('2026-03-04')
)

动态分区(自动创建、自动清理):

sql 复制代码
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"           -- 每个分区的分桶数
)

自动分区(Doris 2.1+,数据驱动):

sql 复制代码
-- 数据进来时自动按月创建分区
PARTITION BY RANGE(event_date) ()
PROPERTIES (
    "auto_partition" = "true",
    "auto_partition.time_unit" = "MONTH"
)
3.3.2 List 分区

按离散值划分,适用于地区、类目等:

sql 复制代码
PARTITION BY LIST(city) (
    PARTITION p_bj VALUES IN ('北京'),
    PARTITION p_sh VALUES IN ('上海'),
    PARTITION p_gz VALUES IN ('广州', '深圳')  -- 多个值可以放一个分区
)

3.4 分桶(Bucket)详解

分桶的作用:并行度 + 负载均衡。 分桶数决定了一个分区内数据被切成几块,也就决定了查询时最多能有多少个线程并行扫描。

3.4.1 Hash 分桶(最常用)
sql 复制代码
-- 按 user_id 做 Hash 分桶,分成 16 个桶
DISTRIBUTED BY HASH(user_id) BUCKETS 16;

分桶列选择原则:

  1. 高基数列(如 user_id、order_id),确保数据均匀分布
  2. 常用查询条件列,加速等值查询
  3. JOIN 关联列,配合 Colocate Join 提升关联性能
3.4.2 Random 分桶
sql 复制代码
-- 随机分桶,仅 Duplicate 模型支持
DISTRIBUTED BY RANDOM BUCKETS 16;

适用于:数据分布极不均匀、无法找到好的 Hash 列时。

3.5 分桶数量怎么定?

黄金法则:让每个 Tablet 的数据量在 1GB ~ 10GB 之间。

分区数据量 推荐分桶数
500MB 4 ~ 8
5GB 8 ~ 16
50GB 32
500GB 建议先拆更细的分区,每分区 16 ~ 32

计算公式: 表总 Tablet 数 = 分区数 × 分桶数 × 副本数

常见错误:

  • 分桶数设太小 → 并行度低,查询慢
  • 分桶数设太大 → 小文件过多,元数据压力大
  • 分桶数一旦创建无法修改(这一点非常重要!)

3.6 完整建表实战

sql 复制代码
-- 需求:存储 APP 用户访问记录,按天分区,按 user_id 分桶
CREATE DATABASE IF NOT EXISTS app_analytics;
USE app_analytics;

CREATE TABLE user_access_log (
    access_date   DATE           NOT NULL COMMENT '访问日期',
    access_time   DATETIME       NOT NULL COMMENT '精确访问时间',
    user_id       BIGINT         NOT NULL COMMENT '用户ID',
    session_id    VARCHAR(64)    NOT NULL COMMENT '会话ID',
    page_url      VARCHAR(512)            COMMENT '页面URL',
    referrer      VARCHAR(512)            COMMENT '来源URL',
    device_type   VARCHAR(16)             COMMENT '设备类型',
    os            VARCHAR(32)             COMMENT '操作系统',
    browser       VARCHAR(32)             COMMENT '浏览器',
    ip            VARCHAR(64)             COMMENT 'IP地址',
    duration_sec  INT                     COMMENT '停留时长(秒)'
)
DUPLICATE KEY(access_date, access_time, user_id)
PARTITION BY RANGE(access_date) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES (
    "replication_num" = "1",                    -- 单机部署设为 1
    "auto_partition" = "true",
    "auto_partition.time_unit" = "DAY",
    "dynamic_partition.enable" = "true",
    "dynamic_partition.start" = "-90",          -- 保留 90 天数据
    "dynamic_partition.end" = "3"               -- 提前创建 3 天分区
);

第四章:索引机制------不花钱的性能加速器

4.1 索引全景图

scss 复制代码
┌────────────────────────────────────────────────┐
│              Doris 索引体系                      │
│                                                │
│  ┌──────────────────┐  ┌────────────────────┐  │
│  │   自动内建索引     │  │   手动创建索引      │  │
│  │  (无需操心)      │  │  (按需添加)       │  │
│  │                   │  │                    │  │
│  │  • 前缀索引       │  │  • 倒排索引        │  │
│  │    (Prefix Index) │  │    (Inverted)      │  │
│  │  • ZoneMap 索引   │  │  • BloomFilter     │  │
│  │                   │  │  • N-Gram BF       │  │
│  └──────────────────┘  └────────────────────┘  │
└────────────────────────────────────────────────┘

4.2 前缀索引(Prefix Index)------"目录页"

原理: Doris 的数据是按排序键排列的(类似 SSTable)。每 1024 行形成一个 Data Block,每个 Block 取第一行的前 36 字节作为索引条目,形成一个稀疏索引。

关键限制:

  • 索引长度最多 36 字节
  • 遇到 VARCHAR 类型直接截断(即使不到 36 字节)
  • 只对排序键的前缀有效

示例分析:

sql 复制代码
DUPLICATE KEY(user_id, age, message)
-- user_id: BIGINT = 8 字节
-- age:     INT    = 4 字节
-- message: VARCHAR(100) = 遇到 VARCHAR 截断
-- 实际前缀索引 = user_id(8B) + age(4B) + message前24B = 36B

-- 这个查询走前缀索引 ✓(命中 user_id)
SELECT * FROM t WHERE user_id = 12345;

-- 这个查询走前缀索引 ✓(命中 user_id + age)
SELECT * FROM t WHERE user_id = 12345 AND age = 25;

-- 这个查询不走前缀索引 ✗(age 不是前缀的第一列)
SELECT * FROM t WHERE age = 25;

给别人讲的比喻: "前缀索引就像字典的目录------你只能按拼音首字母查(前缀匹配)。如果你想按笔画查,字典的目录帮不上忙。"

4.3 ZoneMap 索引------"自动的范围标注"

原理: 对每一列的每个 Data Block,自动记录该 Block 内的 最小值(Min)和最大值(Max)。查询时,如果 WHERE 条件的值不在某 Block 的 Min, Max 范围内,直接跳过该 Block。

ini 复制代码
Block-1:  age: [18, 35]    → WHERE age = 50  → 跳过 ✓
Block-2:  age: [30, 65]    → WHERE age = 50  → 需要扫描
Block-3:  age: [60, 90]    → WHERE age = 50  → 跳过 ✓

特点:

  • 完全自动,无需手动创建
  • 对数值型和日期型效果好
  • 对高基数字符串(如 UUID)效果差

4.4 倒排索引(Inverted Index)------"Doris 版 Elasticsearch"

这是 Doris 2.0+ 的杀手级特性,目前业界正在用它替代 ES 处理日志检索场景。

原理: 和搜索引擎一样,把文本拆分成词(分词),建立"词 → 行号"的映射。

arduino 复制代码
原始数据:
行1: "用户登录失败,密码错误"
行2: "支付超时,请重试"
行3: "用户登录成功"

倒排索引:
"用户"   → [行1, 行3]
"登录"   → [行1, 行3]
"失败"   → [行1]
"密码"   → [行1]
"支付"   → [行2]
"超时"   → [行2]
"成功"   → [行3]

创建方式:

sql 复制代码
CREATE TABLE error_logs (
    log_time      DATETIME       NOT NULL,
    level         VARCHAR(16)    NOT NULL,
    service_name  VARCHAR(64)    NOT NULL,
    error_message TEXT,
    stack_trace   TEXT,
    -- 为各字段创建倒排索引
    INDEX idx_level (level) USING INVERTED,
    INDEX idx_service (service_name) USING INVERTED,
    INDEX idx_error_msg (error_message) USING INVERTED
        PROPERTIES("parser" = "unicode"),         -- 中英文混合分词
    INDEX idx_stack (stack_trace) USING INVERTED
        PROPERTIES("parser" = "english")           -- 英文分词
)
DUPLICATE KEY(log_time, level, service_name)
DISTRIBUTED BY HASH(service_name) BUCKETS 16;

查询语法:

sql 复制代码
-- 查找包含"timeout"的错误
SELECT * FROM error_logs
WHERE error_message MATCH_ANY 'timeout';

-- 查找同时包含"NullPointer"和"Exception"的堆栈
SELECT * FROM error_logs
WHERE stack_trace MATCH_ALL 'NullPointer Exception';

-- 查找包含"connection refused"这个短语的错误
SELECT * FROM error_logs
WHERE error_message MATCH_PHRASE 'connection refused';

分词器选择:

分词器 适用场景 性能
不设置(默认) 等值/范围查询(不分词) 最快
english 纯英文文本
unicode 中英文混合 中等
chinese 纯中文文本 较慢

对已有表添加倒排索引(不需要重写数据!):

sql 复制代码
-- 为已存在的表增加倒排索引
ALTER TABLE error_logs ADD INDEX idx_new_col (some_column) USING INVERTED;
-- 构建索引(对已有数据)
BUILD INDEX idx_new_col ON error_logs;

第二部分:第二周------数据流转与查询提速

第五章:数据导入------把数据灌进 Doris 的三条管道

5.1 导入方式全景

scss 复制代码
                    数据源
                      │
      ┌───────────────┼───────────────┐
      │               │               │
  本地文件/HTTP     Kafka 消息队列    HDFS/S3/OSS
      │               │               │
      ▼               ▼               ▼
  Stream Load     Routine Load     Broker Load
  (微批/实时)      (持续订阅)       (批量历史数据)
      │               │               │
      └───────────────┼───────────────┘
                      │
                      ▼
                  Doris 表

5.2 Stream Load------"HTTP POST 一把梭"

核心思想: 通过 HTTP 协议直接把数据推给 Doris。最简单、最直接的导入方式。

适用场景: 本地文件导入、程序中实时写入(微批)、文件 < 10GB

导入 CSV 文件:

bash 复制代码
# 假设有一个 user_log.csv 文件
# 格式:2026-03-01,2026-03-01 10:00:00,1001,s001,/home,/search,mobile,iOS,Safari,1.2.3.4,30

curl -u root: \
  -H "label:load_20260301_001" \
  -H "column_separator:," \
  -T user_log.csv \
  http://127.0.0.1:8040/api/app_analytics/user_access_log/_stream_load

参数解析:

参数 含义
-u root: 认证信息(用户名:密码)
label 导入标签,全局唯一,用于幂等性保证(同一个 label 不会重复导入)
column_separator 列分隔符,默认是 \t
-T 要上传的文件

导入 JSON 文件:

bash 复制代码
# json_data.json 内容示例:
# [
#   {"access_date":"2026-03-01", "user_id":1001, "page_url":"/home", ...},
#   {"access_date":"2026-03-01", "user_id":1002, "page_url":"/cart", ...}
# ]

curl -u root: \
  -H "label:load_json_001" \
  -H "format:json" \
  -H "strip_outer_array:true" \
  -T json_data.json \
  http://127.0.0.1:8040/api/app_analytics/user_access_log/_stream_load

返回结果解读:

json 复制代码
{
    "TxnId": 1003,
    "Label": "load_json_001",
    "Status": "Success",           // Publish Timeout / Fail 表示失败
    "NumberTotalRows": 1000,       // 总行数
    "NumberLoadedRows": 998,       // 成功导入行数
    "NumberFilteredRows": 2,       // 被过滤的行数(数据质量问题)
    "NumberUnselectedRows": 0,     // 不满足条件的行数
    "LoadBytes": 56789,
    "LoadTimeMs": 150
}

Stream Load 的原子性保证: 一次 Stream Load 要么全部成功,要么全部失败。Label 机制保证了幂等性------如果网络超时不确定是否成功,用相同 Label 重试即可。

5.3 Routine Load------"Kafka 消费者机器人"

核心思想: Doris 主动去 Kafka 拉取数据,7×24 小时不间断运行。

适用场景: 实时数据管道、CDC 数据同步、日志持续导入

sql 复制代码
-- 创建 Routine Load 任务
CREATE ROUTINE LOAD app_analytics.kafka_log_load ON user_access_log
COLUMNS(access_date, access_time, user_id, session_id, page_url,
        referrer, device_type, os, browser, ip, duration_sec)
PROPERTIES (
    "desired_concurrent_number" = "3",    -- 并发消费者数量
    "max_batch_interval" = "10",          -- 最大攒批间隔(秒)
    "max_batch_rows" = "200000",          -- 最大攒批行数
    "max_batch_size" = "104857600",       -- 最大攒批大小(100MB)
    "strict_mode" = "true",              -- 严格模式:不合格数据直接报错
    "format" = "json",
    "jsonpaths" = "[\"$.access_date\",\"$.access_time\",\"$.user_id\",
                    \"$.session_id\",\"$.page_url\",\"$.referrer\",
                    \"$.device_type\",\"$.os\",\"$.browser\",
                    \"$.ip\",\"$.duration_sec\"]"
)
FROM KAFKA (
    "kafka_broker_list" = "localhost:9092",
    "kafka_topic" = "user_access_log",
    "property.group.id" = "doris_consumer_group",
    "property.kafka_default_offsets" = "OFFSET_END"  -- 从最新位置开始消费
);

运维管理命令:

sql 复制代码
-- 查看任务状态
SHOW ROUTINE LOAD FOR kafka_log_load\G

-- 暂停任务
PAUSE ROUTINE LOAD FOR kafka_log_load;

-- 恢复任务
RESUME ROUTINE LOAD FOR kafka_log_load;

-- 停止任务(不可恢复)
STOP ROUTINE LOAD FOR kafka_log_load;

-- 查看消费进度
SHOW ROUTINE LOAD TASK WHERE JobName = 'kafka_log_load'\G

Routine Load 的两级调度机制:

arduino 复制代码
              Routine Load Job
                    │
       ┌────────────┼────────────┐
       │            │            │
    Task-1       Task-2       Task-3      ← FE 将 Job 拆分为多个 Task
   (partition0) (partition1) (partition2)
       │            │            │
       ▼            ▼            ▼
      BE-1         BE-2         BE-3      ← 每个 Task 在一个 BE 上执行
   (拉 Kafka →   (拉 Kafka →  (拉 Kafka →
    写 Doris)     写 Doris)    写 Doris)

Exactly-Once 语义保证: Doris 在 FE 端记录每个 Kafka Partition 的消费 Offset。每个微批次在事务中完成"拉取数据 + 写入 Doris + 提交 Offset"三个动作,保证不丢不重。

5.4 Broker Load------"搬家公司搬大数据"

核心思想: 异步提交导入任务,后台执行,适合大规模历史数据迁移。

适用场景: 从 HDFS、S3、OSS 等对象存储批量导入 TB 级数据

sql 复制代码
-- 从 S3 导入
LOAD LABEL app_analytics.s3_load_20260301
(
    DATA INFILE("s3://my-bucket/logs/2026-03-01/*.parquet")
    INTO TABLE user_access_log
    FORMAT AS "parquet"
)
WITH S3 (
    "AWS_ENDPOINT" = "s3.cn-north-1.amazonaws.com.cn",
    "AWS_ACCESS_KEY" = "your_access_key",
    "AWS_SECRET_KEY" = "your_secret_key",
    "AWS_REGION" = "cn-north-1"
)
PROPERTIES (
    "timeout" = "3600"  -- 超时时间 1 小时
);

-- 查看导入进度
SHOW LOAD WHERE LABEL = 's3_load_20260301'\G

5.5 三种导入方式对比

特性 Stream Load Routine Load Broker Load
数据源 本地文件/HTTP 请求 Kafka HDFS/S3/OSS
同步/异步 同步 持续异步 异步
适合数据量 < 10GB(单次) 持续流式 TB 级
实时性 秒级 秒级 分钟~小时级
使用方式 curl / HTTP 客户端 SQL 创建任务 SQL 提交任务
原子性 单次全成功或全失败 微批级别 单次任务

第六章:Multi-Catalog------"不搬数据,直接查"

6.1 核心价值

传统数据分析流程:MySQL → ETL → Hive → Doris → 查询(链路长、延迟高)

Multi-Catalog 之后:直接在 Doris 里用 SQL 查 MySQL/Hive/S3 的数据(ZeroETL)

scss 复制代码
┌─────────────────────────────────────────────┐
│                Doris SQL 引擎                │
│                                             │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐    │
│  │ Internal │  │ MySQL   │  │ Hive    │    │
│  │ Catalog  │  │ Catalog │  │ Catalog │    │
│  │ (本地表) │  │ (外部)  │  │ (外部)  │    │
│  └────┬────┘  └────┬────┘  └────┬────┘    │
│       │            │            │           │
└───────┼────────────┼────────────┼───────────┘
        │            │            │
   Doris BE     MySQL Server    HDFS/S3
   (本地存储)    (远程读取)      (远程读取)

6.2 创建外部 Catalog

sql 复制代码
-- 连接外部 MySQL
CREATE CATALOG mysql_biz PROPERTIES (
    'type' = 'jdbc',
    'user' = 'readonly_user',
    'password' = 'password123',
    'jdbc_url' = 'jdbc:mysql://10.0.0.1:3306',
    'driver_url' = 'mysql-connector-j-8.3.0.jar',
    'driver_class' = 'com.mysql.cj.jdbc.Driver'
);

-- 连接 Hive(通过 HMS)
CREATE CATALOG hive_dw PROPERTIES (
    'type' = 'hms',
    'hive.metastore.uris' = 'thrift://10.0.0.2:9083'
);

-- 查看所有 Catalog
SHOW CATALOGS;

-- 切换到 MySQL Catalog
SWITCH mysql_biz;
SHOW DATABASES;
USE order_db;
SELECT * FROM orders LIMIT 10;

-- 跨 Catalog JOIN(核心能力!)
SELECT
    d.order_id,
    d.total_amount,
    m.user_name,
    m.vip_level
FROM internal.app_analytics.order_detail d          -- Doris 本地表
JOIN mysql_biz.user_db.user_info m ON d.user_id = m.id  -- 外部 MySQL 表
WHERE d.order_status = 3;

给别人讲的关键句: "Multi-Catalog 让 Doris 变成了一个'SQL 网关'------不管你的数据在 MySQL、Hive、还是 S3 里,在 Doris 这里都是一张表,写一条 SQL 就能跨源 JOIN。"


第七章:物化视图------"用空间换时间的艺术"

7.1 核心思想

物化视图 = 预计算 + 缓存结果。把经常要跑的复杂查询的结果提前算好、存起来,查询时直接取结果,不再从原始数据算。

类比: 你每天早上都要算"昨天全国各城市的销售额"。与其每次从几亿条订单明细里现场 SUM,不如每天晚上算好存一张汇总表------物化视图就是 Doris 自动帮你做这件事。

7.2 同步物化视图(单表加速)

特点:

  • 基于单表创建
  • 数据导入时强同步更新
  • 查询原表时,优化器自动路由到物化视图(用户无感知!)
sql 复制代码
-- 原始明细表
CREATE TABLE user_access_log (
    access_date   DATE,
    user_id       BIGINT,
    page_url      VARCHAR(512),
    duration_sec  INT
)
DUPLICATE KEY(access_date, user_id)
DISTRIBUTED BY HASH(user_id) BUCKETS 16;

-- 创建同步物化视图:按天、按页面统计 PV 和 平均停留时长
CREATE MATERIALIZED VIEW mv_daily_page_stats AS
SELECT
    access_date,
    page_url,
    COUNT(*) AS pv,
    SUM(duration_sec) AS total_duration,
    COUNT(DISTINCT user_id) AS uv
FROM user_access_log
GROUP BY access_date, page_url;

查询时的智能路由:

sql 复制代码
-- 用户写的 SQL(查原表)
SELECT access_date, page_url, COUNT(*) AS pv
FROM user_access_log
WHERE access_date = '2026-03-15'
GROUP BY access_date, page_url;

-- Doris 优化器自动识别:这个查询可以命中 mv_daily_page_stats!
-- 实际执行:直接读物化视图,速度提升 10x ~ 100x

验证是否命中物化视图:

sql 复制代码
EXPLAIN SELECT access_date, page_url, COUNT(*) AS pv
FROM user_access_log
WHERE access_date = '2026-03-15'
GROUP BY access_date, page_url;

-- 在执行计划中看到 "rollup: mv_daily_page_stats" 就说明命中了

7.3 异步物化视图(多表 JOIN 加速)

特点:

  • 支持多表 JOIN
  • 最终一致性(可定时/手动刷新)
  • 可直接查询
sql 复制代码
-- 场景:经常需要关联订单表和用户表做分析
CREATE MATERIALIZED VIEW mv_order_user_stats
BUILD IMMEDIATE                    -- 创建后立即构建
REFRESH AUTO                       -- 自动增量刷新
ON SCHEDULE EVERY 1 HOUR           -- 每小时刷新一次
DISTRIBUTED BY HASH(user_id) BUCKETS 8
AS
SELECT
    o.user_id,
    u.city,
    u.vip_level,
    DATE_TRUNC(o.create_time, 'day') AS order_date,
    COUNT(*) AS order_count,
    SUM(o.total_amount) AS total_sales
FROM order_detail o
JOIN user_info u ON o.user_id = u.user_id
GROUP BY o.user_id, u.city, u.vip_level, DATE_TRUNC(o.create_time, 'day');

-- 可以直接查询这个物化视图
SELECT city, SUM(total_sales) FROM mv_order_user_stats
WHERE order_date = '2026-03-15'
GROUP BY city;

7.4 同步 vs 异步物化视图对比

特性 同步物化视图 异步物化视图
表数量 仅单表 支持多表 JOIN
数据一致性 强一致(实时同步) 最终一致(定时刷新)
能否直接查 不能(自动路由) 可以
刷新方式 数据导入时自动 定时/手动/数据变更触发
适用场景 加速单表聚合查询 加速跨表 JOIN 分析

第八章:性能分析------"医生给 SQL 做体检"

8.1 EXPLAIN------看执行计划

EXPLAIN 就像是 SQL 的"X 光片"------不实际执行查询,只展示 Doris 打算怎么执行。

sql 复制代码
EXPLAIN VERBOSE
SELECT access_date, COUNT(*) AS pv, COUNT(DISTINCT user_id) AS uv
FROM user_access_log
WHERE access_date BETWEEN '2026-03-01' AND '2026-03-07'
GROUP BY access_date
ORDER BY access_date;

执行计划关键信息解读:

ini 复制代码
┌───────────────────────────────────────────────┐
│  重点关注这几个信息:                            │
│                                               │
│  1. partitions=7/90     分区裁剪!             │
│     只扫描 7 个分区(共 90 个)→ 裁剪了 92%     │
│                                               │
│  2. rollup: mv_daily_page_stats               │
│     命中了物化视图!→ 不扫描原始数据              │
│                                               │
│  3. tabletNum=112, tabletList=10001,10002...   │
│     并行扫描 112 个 Tablet                      │
│                                               │
│  4. PREDICATES: access_date >= '2026-03-01'   │
│     谓词下推成功!→ 在存储层就过滤了数据          │
└───────────────────────────────────────────────┘

执行计划中的红色预警信号:

  • partitions=90/90 → 全分区扫描,没有裁剪,WHERE 条件可能没命中分区列
  • rollup: user_access_log(原表名) → 没命中物化视图
  • 巨大的 EXCHANGE 节点 → 数据在 BE 之间大量 Shuffle

8.2 Query Profile------"手术刀级的性能诊断"

Profile 是查询执行后的"体检报告",告诉你时间到底花在了哪里。

sql 复制代码
-- 开启 Profile 收集
SET enable_profile = true;

-- 执行你的查询
SELECT ...;

-- 方法1:通过 Web UI 查看(推荐)
-- 浏览器打开 http://FE_IP:8030 → QueryProfile

-- 方法2:通过 API 获取
-- curl http://FE_IP:8030/api/profile?query_id=xxx

Profile 核心指标:

指标 含义 异常信号
ScanRows 扫描行数 远大于返回行数 → 过滤效率低
RowsKeyRangeFiltered 前缀索引过滤的行数 为 0 → 前缀索引没生效
BlockConditionsFilteredRows ZoneMap 过滤的行数 为 0 → 考虑排序优化
InvertedIndexFilteredRows 倒排索引过滤的行数 未出现 → 倒排索引没用上
PeakMemoryUsage 内存峰值 过大 → 可能导致 OOM
NetworkTime 网络传输时间 占比高 → 数据 Shuffle 过多
IOTime 磁盘 IO 时间 占比高 → 考虑增加索引或物化视图

8.3 常用的调优手段速查

sql 复制代码
性能差?按这个顺序排查:
│
├── 1. WHERE 条件命中分区键了吗?
│       → 没有 → 加上时间条件 / 调整分区策略
│
├── 2. 命中物化视图了吗?
│       → 没有 → 创建匹配的物化视图
│
├── 3. 前缀索引/倒排索引生效了吗?
│       → 没有 → 调整排序键顺序 / 添加倒排索引
│
├── 4. JOIN 时数据 Shuffle 太多?
│       → 是 → 考虑 Colocate Join(两张表用相同的分桶策略)
│       → 或 → 小表用 Broadcast Join
│
└── 5. 以上都没问题?
        → 增加 BE 节点(线性扩展算力)
        → 或增加分桶数(提高并行度)

第三部分:实战项目详解

项目一:电商用户行为日志分析平台

P1.1 项目架构

scss 复制代码
模拟数据生成器                    Doris
(Python 脚本)                      │
     │                    ┌────────┼────────┐
     │                    │        │        │
     ▼                    ▼        ▼        ▼
  CSV 文件  ──Stream Load──→  明细表    聚合表    物化视图
                          (Duplicate) (Aggregate) (SYNC MV)
                              │        │        │
                              └────────┼────────┘
                                       │
                                   SQL 分析
                            (PV/UV/留存/漏斗)

P1.2 第一步:生成模拟数据

python 复制代码
# generate_user_logs.py
import csv
import random
from datetime import datetime, timedelta

events = ['click', 'view', 'add_cart', 'purchase', 'search']
pages = ['/home', '/category/phones', '/product/iphone15', '/product/macbook',
         '/cart', '/checkout', '/order/success', '/search?q=laptop']
devices = ['mobile', 'desktop', 'tablet']
os_list = ['iOS', 'Android', 'Windows', 'macOS']

with open('user_logs.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    base_time = datetime(2026, 3, 1)

    for i in range(50000):  # 生成 5 万条
        dt = base_time + timedelta(
            days=random.randint(0, 6),
            hours=random.randint(0, 23),
            minutes=random.randint(0, 59),
            seconds=random.randint(0, 59)
        )
        user_id = random.randint(1, 5000)
        writer.writerow([
            dt.strftime('%Y-%m-%d'),
            dt.strftime('%Y-%m-%d %H:%M:%S'),
            user_id,
            f'sess_{user_id}_{random.randint(1,100)}',
            random.choice(pages),
            random.choice(pages),
            random.choice(devices),
            random.choice(os_list),
            'Chrome',
            f'192.168.1.{random.randint(1,255)}',
            random.randint(1, 300)
        ])

print("生成完成: user_logs.csv (50000 条)")

P1.3 第二步:建表

sql 复制代码
CREATE DATABASE IF NOT EXISTS ecommerce_analytics;
USE ecommerce_analytics;

-- 表1:明细模型(存原始日志)
CREATE TABLE user_behavior_detail (
    event_date    DATE           NOT NULL,
    event_time    DATETIME       NOT NULL,
    user_id       BIGINT         NOT NULL,
    session_id    VARCHAR(64),
    event_type    VARCHAR(32),
    page_url      VARCHAR(256),
    referrer      VARCHAR(256),
    device        VARCHAR(32),
    os            VARCHAR(32),
    browser       VARCHAR(32),
    ip            VARCHAR(64),
    duration_sec  INT
)
DUPLICATE KEY(event_date, event_time, user_id)
PARTITION BY RANGE(event_date) (
    PARTITION p20260301 VALUES LESS THAN ('2026-03-02'),
    PARTITION p20260302 VALUES LESS THAN ('2026-03-03'),
    PARTITION p20260303 VALUES LESS THAN ('2026-03-04'),
    PARTITION p20260304 VALUES LESS THAN ('2026-03-05'),
    PARTITION p20260305 VALUES LESS THAN ('2026-03-06'),
    PARTITION p20260306 VALUES LESS THAN ('2026-03-07'),
    PARTITION p20260307 VALUES LESS THAN ('2026-03-08')
)
DISTRIBUTED BY HASH(user_id) BUCKETS 8
PROPERTIES ("replication_num" = "1");

-- 表2:聚合模型(自动计算 PV/UV)
CREATE TABLE page_stats_agg (
    event_date    DATE           NOT NULL,
    page_url      VARCHAR(256)   NOT NULL,
    device        VARCHAR(32)    NOT NULL,
    pv            BIGINT         SUM,              -- PV 自动累加
    uv_hll        HLL            HLL_UNION,        -- UV 模糊去重(HyperLogLog)
    uv_bitmap     BITMAP         BITMAP_UNION,     -- UV 精确去重(Bitmap)
    max_duration  INT            MAX,
    min_duration  INT            MIN
)
AGGREGATE KEY(event_date, page_url, device)
PARTITION BY RANGE(event_date) (
    PARTITION p20260301 VALUES LESS THAN ('2026-03-02'),
    PARTITION p20260302 VALUES LESS THAN ('2026-03-03'),
    PARTITION p20260303 VALUES LESS THAN ('2026-03-04'),
    PARTITION p20260304 VALUES LESS THAN ('2026-03-05'),
    PARTITION p20260305 VALUES LESS THAN ('2026-03-06'),
    PARTITION p20260306 VALUES LESS THAN ('2026-03-07'),
    PARTITION p20260307 VALUES LESS THAN ('2026-03-08')
)
DISTRIBUTED BY HASH(page_url) BUCKETS 8
PROPERTIES ("replication_num" = "1");

P1.4 第三步:导入数据

bash 复制代码
# 导入明细表
curl -u root: \
  -H "label:detail_load_001" \
  -H "column_separator:," \
  -T user_logs.csv \
  http://127.0.0.1:8040/api/ecommerce_analytics/user_behavior_detail/_stream_load

导入聚合表时需要用到特殊函数:

sql 复制代码
-- 用 INSERT INTO SELECT 从明细表导入聚合表
INSERT INTO page_stats_agg
SELECT
    event_date,
    page_url,
    device,
    1,                                -- 每行 PV = 1,SUM 后就是总 PV
    HLL_HASH(user_id),               -- 将 user_id 转为 HLL
    TO_BITMAP(user_id),              -- 将 user_id 转为 Bitmap
    duration_sec,
    duration_sec
FROM user_behavior_detail;

P1.5 第四步:分析查询

sql 复制代码
-- 1. 每日 PV/UV 统计
SELECT
    event_date,
    SUM(pv) AS total_pv,
    HLL_UNION_AGG(uv_hll) AS approx_uv,       -- HLL 模糊去重
    BITMAP_UNION_COUNT(uv_bitmap) AS exact_uv  -- Bitmap 精确去重
FROM page_stats_agg
GROUP BY event_date
ORDER BY event_date;

-- 2. 页面热度排行(基于明细表)
SELECT
    page_url,
    COUNT(*) AS pv,
    COUNT(DISTINCT user_id) AS uv,
    AVG(duration_sec) AS avg_duration
FROM user_behavior_detail
WHERE event_date = '2026-03-05'
GROUP BY page_url
ORDER BY pv DESC
LIMIT 10;

-- 3. 简易漏斗分析:浏览 → 加购 → 购买 转化率
WITH funnel AS (
    SELECT
        COUNT(DISTINCT CASE WHEN event_type = 'view' THEN user_id END) AS view_users,
        COUNT(DISTINCT CASE WHEN event_type = 'add_cart' THEN user_id END) AS cart_users,
        COUNT(DISTINCT CASE WHEN event_type = 'purchase' THEN user_id END) AS buy_users
    FROM user_behavior_detail
    WHERE event_date BETWEEN '2026-03-01' AND '2026-03-07'
)
SELECT
    view_users,
    cart_users,
    buy_users,
    ROUND(cart_users * 100.0 / view_users, 2) AS view_to_cart_rate,
    ROUND(buy_users * 100.0 / cart_users, 2) AS cart_to_buy_rate,
    ROUND(buy_users * 100.0 / view_users, 2) AS overall_conversion_rate
FROM funnel;

-- 4. 次日留存率
SELECT
    t1.event_date AS day0,
    COUNT(DISTINCT t1.user_id) AS day0_users,
    COUNT(DISTINCT t2.user_id) AS day1_retained,
    ROUND(COUNT(DISTINCT t2.user_id) * 100.0 / COUNT(DISTINCT t1.user_id), 2) AS retention_rate
FROM user_behavior_detail t1
LEFT JOIN user_behavior_detail t2
    ON t1.user_id = t2.user_id
    AND t2.event_date = DATE_ADD(t1.event_date, INTERVAL 1 DAY)
WHERE t1.event_date BETWEEN '2026-03-01' AND '2026-03-06'
GROUP BY t1.event_date
ORDER BY t1.event_date;

P1.6 HLL vs Bitmap 深入理解

ini 复制代码
┌─────────────────────────────────────────────────────────────┐
│                   精确去重 vs 模糊去重                         │
│                                                             │
│  Bitmap(精确去重)                                           │
│  ───────────────                                            │
│  原理:用一个超大的位数组,每个用户 ID 对应一个 bit             │
│  user_id=1 → bit[1]=1                                      │
│  user_id=5 → bit[5]=1                                      │
│  UV = popcount(bitmap) = 统计有多少个 1                      │
│                                                             │
│  优点:精确,零误差                                           │
│  缺点:如果 user_id 值域很大(如 64 位),内存消耗大            │
│  适用:user_id 是连续的小整数,或总去重数 < 千万级               │
│                                                             │
│  HLL(HyperLogLog,模糊去重)                                 │
│  ──────────────────────────                                  │
│  原理:概率算法,通过哈希值的"前导零"个数估算基数               │
│  只用固定的几 KB 内存,就能估算几十亿的去重数                    │
│                                                             │
│  优点:内存极小(固定 ~16KB),速度极快                         │
│  缺点:有 ~1% 的误差                                         │
│  适用:去重数 > 千万级,对精确度要求不高(如 UV 统计)            │
└─────────────────────────────────────────────────────────────┘

项目二:实时订单业务看板(CDC + 数据更新)

P2.1 项目架构

markdown 复制代码
MySQL (OLTP)     Debezium/Flink CDC        Kafka         Doris
┌──────────┐    ┌──────────────────┐    ┌──────────┐    ┌──────────────┐
│ 订单表    │───→│  监听 Binlog     │───→│ Topic:   │───→│ Routine Load │
│ (增删改)  │    │  生成变更事件    │    │ orders   │    │ → Unique 表  │
└──────────┘    └──────────────────┘    └──────────┘    └──────┬───────┘
                                                               │
                                                          BI 工具
                                                        (Superset/
                                                         Metabase)

P2.2 Doris 端建表

sql 复制代码
CREATE DATABASE IF NOT EXISTS realtime_dashboard;
USE realtime_dashboard;

-- 订单实时表(Unique Key + Merge-on-Write)
CREATE TABLE rt_orders (
    order_id       BIGINT         NOT NULL COMMENT '订单ID(主键)',
    user_id        BIGINT         NOT NULL,
    product_id     BIGINT,
    product_name   VARCHAR(128),
    quantity       INT,
    unit_price     DECIMAL(10,2),
    total_amount   DECIMAL(10,2),
    order_status   VARCHAR(32)    COMMENT 'CREATED/PAID/SHIPPED/COMPLETED/REFUNDED',
    payment_method VARCHAR(32),
    create_time    DATETIME       NOT NULL,
    update_time    DATETIME
)
UNIQUE KEY(order_id)
PARTITION BY RANGE(create_time) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 16
PROPERTIES (
    "replication_num" = "1",
    "enable_unique_key_merge_on_write" = "true",
    "auto_partition" = "true",
    "auto_partition.time_unit" = "DAY"
);

P2.3 Routine Load 消费 Kafka

sql 复制代码
-- 假设 Kafka 中的 JSON 格式(Debezium 的 after 字段):
-- {
--   "order_id": 10001,
--   "user_id": 2001,
--   "product_id": 3001,
--   "product_name": "iPhone 15",
--   "quantity": 1,
--   "unit_price": 7999.00,
--   "total_amount": 7999.00,
--   "order_status": "PAID",
--   "payment_method": "wechat",
--   "create_time": "2026-03-15 14:30:00",
--   "update_time": "2026-03-15 14:31:00"
-- }

CREATE ROUTINE LOAD realtime_dashboard.orders_from_kafka ON rt_orders
COLUMNS(order_id, user_id, product_id, product_name, quantity,
        unit_price, total_amount, order_status, payment_method,
        create_time, update_time)
PROPERTIES (
    "desired_concurrent_number" = "3",
    "max_batch_interval" = "5",
    "max_batch_rows" = "100000",
    "format" = "json",
    "strict_mode" = "true"
)
FROM KAFKA (
    "kafka_broker_list" = "localhost:9092",
    "kafka_topic" = "mysql_orders_cdc",
    "property.group.id" = "doris_orders_consumer"
);

P2.4 实时看板查询

sql 复制代码
-- 1. 今日实时销售看板
SELECT
    order_status,
    COUNT(*) AS order_count,
    SUM(total_amount) AS total_sales,
    AVG(total_amount) AS avg_order_amount
FROM rt_orders
WHERE create_time >= CURDATE()
GROUP BY order_status;

-- 2. 每小时销售趋势
SELECT
    DATE_FORMAT(create_time, '%Y-%m-%d %H:00:00') AS hour_slot,
    COUNT(*) AS orders,
    SUM(total_amount) AS sales
FROM rt_orders
WHERE create_time >= CURDATE()
  AND order_status IN ('PAID', 'SHIPPED', 'COMPLETED')
GROUP BY hour_slot
ORDER BY hour_slot;

-- 3. 热销商品 TOP 10
SELECT
    product_name,
    SUM(quantity) AS total_sold,
    SUM(total_amount) AS total_revenue
FROM rt_orders
WHERE create_time >= CURDATE()
  AND order_status != 'REFUNDED'
GROUP BY product_name
ORDER BY total_revenue DESC
LIMIT 10;

-- 4. 退款率监控
SELECT
    ROUND(
        COUNT(CASE WHEN order_status = 'REFUNDED' THEN 1 END) * 100.0
        / COUNT(*), 2
    ) AS refund_rate
FROM rt_orders
WHERE create_time >= CURDATE();

P2.5 数据更新的核心机制

ini 复制代码
时间线:
  14:30:00  订单 10001 创建  → order_status = 'CREATED'
  14:31:00  订单 10001 支付  → order_status = 'PAID'       ← Kafka 推送新消息
  14:35:00  订单 10001 发货  → order_status = 'SHIPPED'    ← Kafka 推送新消息

Unique Key (Merge-on-Write) 行为:
  14:30:00  写入 (10001, CREATED)
  14:31:00  写入 (10001, PAID)    → 发现 order_id=10001 已存在
                                  → 标记旧记录删除,写入新记录
                                  → 表中只保留 (10001, PAID)
  14:35:00  同理,最终只保留 (10001, SHIPPED)

项目三:全域数据探索与海量日志检索引擎

P3.1 项目架构

scss 复制代码
┌──────────────────────────────────────────────────────────────┐
│                    统一 SQL 查询入口                           │
│                      (Doris FE)                              │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │
│  │ Internal      │  │ MySQL        │  │ Hive/S3      │       │
│  │ Catalog       │  │ Catalog      │  │ Catalog      │       │
│  │              │  │              │  │              │       │
│  │ 日志表       │  │ 用户表       │  │ 历史订单表    │       │
│  │ (倒排索引)   │  │ (外部只读)   │  │ (外部只读)   │       │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘       │
│         │                 │                 │                │
│         └─────── JOIN ────┴──── JOIN ───────┘                │
└──────────────────────────────────────────────────────────────┘

P3.2 日志检索引擎

sql 复制代码
CREATE DATABASE IF NOT EXISTS log_search;
USE log_search;

-- 海量错误日志表(带倒排索引)
CREATE TABLE system_error_logs (
    log_time       DATETIME       NOT NULL COMMENT '日志时间',
    log_level      VARCHAR(16)    NOT NULL COMMENT 'ERROR/WARN/INFO',
    service_name   VARCHAR(64)    NOT NULL COMMENT '服务名称',
    host_ip        VARCHAR(32)             COMMENT '主机IP',
    trace_id       VARCHAR(64)             COMMENT '链路追踪ID',
    error_code     VARCHAR(32)             COMMENT '错误码',
    error_message  TEXT                    COMMENT '错误信息',
    stack_trace    TEXT                    COMMENT '堆栈轨迹',
    request_url    VARCHAR(512)            COMMENT '请求URL',
    request_params TEXT                    COMMENT '请求参数',

    -- 倒排索引定义
    INDEX idx_level (log_level) USING INVERTED,
    INDEX idx_service (service_name) USING INVERTED,
    INDEX idx_trace (trace_id) USING INVERTED,
    INDEX idx_error_code (error_code) USING INVERTED,
    INDEX idx_error_msg (error_message) USING INVERTED
        PROPERTIES("parser" = "unicode"),
    INDEX idx_stack (stack_trace) USING INVERTED
        PROPERTIES("parser" = "english"),
    INDEX idx_url (request_url) USING INVERTED
        PROPERTIES("parser" = "unicode")
)
DUPLICATE KEY(log_time, log_level, service_name)
PARTITION BY RANGE(log_time) ()
DISTRIBUTED BY HASH(service_name) BUCKETS 32
PROPERTIES (
    "replication_num" = "1",
    "auto_partition" = "true",
    "auto_partition.time_unit" = "DAY"
);

日志检索查询示例:

sql 复制代码
-- 1. 全文检索:查找包含 "timeout" 或 "connection refused" 的错误
SELECT log_time, service_name, error_message
FROM system_error_logs
WHERE error_message MATCH_ANY 'timeout connection refused'
  AND log_time >= '2026-03-15'
ORDER BY log_time DESC
LIMIT 50;

-- 2. 精确短语搜索:查找 NullPointerException
SELECT log_time, service_name, stack_trace
FROM system_error_logs
WHERE stack_trace MATCH_PHRASE 'NullPointerException'
  AND log_level = 'ERROR'
  AND log_time >= '2026-03-15'
LIMIT 20;

-- 3. 错误统计面板:按服务和错误码聚合
SELECT
    service_name,
    error_code,
    COUNT(*) AS error_count,
    MIN(log_time) AS first_seen,
    MAX(log_time) AS last_seen
FROM system_error_logs
WHERE log_time >= CURDATE()
  AND log_level = 'ERROR'
GROUP BY service_name, error_code
ORDER BY error_count DESC
LIMIT 20;

-- 4. 链路追踪:通过 trace_id 串联一次请求的所有日志
SELECT log_time, service_name, log_level, error_message
FROM system_error_logs
WHERE trace_id = 'abc123def456'
ORDER BY log_time;

P3.3 联邦查询:跨源 JOIN

sql 复制代码
-- 创建外部数据源 Catalog
CREATE CATALOG mysql_prod PROPERTIES (
    'type' = 'jdbc',
    'user' = 'reader',
    'password' = 'readonly_pwd',
    'jdbc_url' = 'jdbc:mysql://mysql-prod:3306',
    'driver_url' = 'mysql-connector-j-8.3.0.jar',
    'driver_class' = 'com.mysql.cj.jdbc.Driver'
);

-- 跨源 JOIN:将本地日志表与外部 MySQL 用户表关联
-- 场景:查找 VIP 用户遇到的错误,优先处理
SELECT
    l.log_time,
    l.service_name,
    l.error_message,
    u.user_name,
    u.vip_level,
    u.phone
FROM log_search.system_error_logs l
JOIN mysql_prod.user_db.users u
    ON CAST(
        REGEXP_EXTRACT(l.request_params, '"user_id":(\\d+)', 1)
        AS BIGINT
    ) = u.id
WHERE l.log_time >= '2026-03-15'
  AND l.log_level = 'ERROR'
  AND u.vip_level >= 3
ORDER BY u.vip_level DESC, l.log_time DESC
LIMIT 50;

P3.4 性能调优:JOIN 策略

sql 复制代码
-- 查看执行计划,关注 JOIN 策略
EXPLAIN VERBOSE
SELECT ...  -- 你的跨源 JOIN 查询

-- 常见 JOIN 策略及适用场景:
--
-- 1. Broadcast Join(广播 JOIN)
--    小表广播到所有 BE 节点,适合大表 JOIN 小表
--    SET broadcast_row_count_limit = 10000000;  -- 调整广播阈值
--
-- 2. Shuffle Join(Hash Join)
--    按 JOIN Key 做 Hash 重新分布,适合两个大表
--    这是默认策略
--
-- 3. Colocate Join(本地 JOIN)
--    两张表使用相同的分桶策略,数据天然在同一节点
--    查询时无需 Shuffle,性能最好
--    要求:相同 Colocate Group、相同分桶列、相同分桶数

-- 创建 Colocate 表示例:
CREATE TABLE table_a (...)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES ("colocate_with" = "group_1");

CREATE TABLE table_b (...)
DISTRIBUTED BY HASH(user_id) BUCKETS 16
PROPERTIES ("colocate_with" = "group_1");

-- 此时 table_a JOIN table_b ON user_id 会自动用 Colocate Join

第四部分:知识体系总结

全局数据流图(核心记忆图)

scss 复制代码
                          数据源
        ┌──────────────────┼──────────────────┐
        │                  │                  │
    本地文件/API        Kafka             HDFS/S3/OSS
        │                  │                  │
        ▼                  ▼                  ▼
    Stream Load      Routine Load       Broker Load
        │                  │                  │
        └──────────────────┼──────────────────┘
                           │
                           ▼
                    ┌──────────────┐
                    │   Doris 表    │
                    │              │
                    │  • Duplicate │ ← 日志、明细
                    │  • Aggregate │ ← 报表、聚合
                    │  • Unique    │ ← 更新、去重
                    │              │
                    │  分区 + 分桶  │ ← 数据分布
                    │  索引体系     │ ← 加速过滤
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
          物化视图      EXPLAIN       Multi-Catalog
          (预计算)     (执行计划)    (联邦查询)
              │            │            │
              └────────────┼────────────┘
                           │
                           ▼
                    用户 SQL 查询

面试 / 讲解速查卡

问题 关键回答
Doris 是什么? MPP 分析型数据库,兼容 MySQL 协议,一个引擎覆盖实时报表+日志检索+数据联邦
三大模型怎么选? 要明细用 Duplicate,要聚合用 Aggregate,要更新用 Unique
Merge-on-Write vs Read? Write 写时合并查得快,Read 追加写入但查得慢。默认用 Write
分区 vs 分桶? 分区做数据裁剪(通常按时间),分桶做并行度和负载均衡(通常按 Hash)
前缀索引的限制? 最多 36 字节,遇到 VARCHAR 截断,只对排序键前缀有效
为什么用 Doris 替代 ES? 倒排索引 + SQL 接口 + 统一存储,运维成本更低,生态更统一
Stream Load vs Routine Load? Stream 是推(HTTP POST),Routine 是拉(Kafka 消费)
物化视图的价值? 空间换时间,预计算常用聚合结果,查询可加速 10x~100x
Multi-Catalog 的价值? ZeroETL,不搬数据直接跨源 JOIN

最后的建议: 理论看三遍不如动手做一遍。按照上面的项目,从项目一开始实操。每个 SQL 都亲自敲一遍,每个 EXPLAIN 都看一遍,很快你就能给别人讲清楚整个 Doris 的世界。

相关推荐
星辰徐哥6 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥6 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约6 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee6 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐6 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs6 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐6 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司6 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
码农阿豪6 小时前
从零到一:Spring Boot快速接入金仓数据库实战
数据库·spring boot·后端
追逐时光者6 小时前
一个基于 .NET 与 Avalonia 构建、面向 TrinityCore 的开源 WoW 数据库编辑器
后端·.net