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 的世界。

相关推荐
攒了一袋星辰2 小时前
SequenceGenerator高并发有序顺序号生成中间件 - 架构设计文档
java·后端·spring·中间件·架构·kafka·maven
码农刚子2 小时前
字符串拼接用“+”还是 StringBuilder?别再凭感觉写了
后端·代码规范
茶杯梦轩2 小时前
面试常问:DNS,CDN,Cookie,Session和Token详解及实战避坑指南
后端·网络协议·面试
Memory_荒年2 小时前
TiDB 单机部署与监控完整指南
运维·数据库·后端
犯困的饭团2 小时前
3_【自动化引擎Ansible Runner】深入功能模块 - 不止于 Playbook
后端
写Cpp的小黑黑2 小时前
WHEP 拉流技术详解(基于一个 html/js demo)
后端
GetcharZp2 小时前
告别 Selenium!这款 Go 语言神器,让网页自动化与爬虫快到飞起!
后端
天下无贼2 小时前
【Python】2026版——FastAPI 框架快速搭建后端服务
后端·python·aigc
橙序员小站2 小时前
当所有人都在做 Agent,我想聊聊被遗忘的基础设施
后端·开源·aigc