Apache Doris数据库——大数据技术

Apache Doris

一、简介

1.1、Apache Doris简介

Apache Doris 是一款基于 MPP 架构的高性能、实时分析型数据库。它以高效、简单和统一的特性著称,能够在亚秒级的时间内返回海量数据的查询结果。Doris 既能支持高并发的点查询场景,也能支持高吞吐的复杂分析场景。

  • MPP(Massively Parallel Processing,大规模并行处理)架构通过将任务分解为多个子任务,分配到独立的计算节点并行执行,显著提升数据处理效率。其核心特点是节点间无共享资源(Shared-Nothing),每个节点拥有独立的CPU、内存和存储,通过网络互联协同工作。

基于这些优势,Apache Doris 非常适合用于报表分析、即席查询、统一数仓构建、数据湖联邦查询加速等场景。用户可以基于 Doris 构建大屏看板、用户行为分析、AB 实验平台、日志检索分析、用户画像分析、订单分析等应用。

1.2、Apache Doris 与传统大数据架构相比

Apache Doris 与传统大数据架构(如 Hadoop + Hive + Spark/Impala)相比,在实时分析、运维复杂度、成本效率上有显著优势,但也存在特定场景的局限性。

核心优势对比(Doris vs. 传统架构)

能力维度 Apache Doris 传统架构(Hadoop生态) 优势说明
实时性 ⭐️ 秒级响应 ⚠️ 分钟~小时级延迟 Doris支持流式数据实时写入即可查,替代传统T+1数仓
架构复杂度 ⭐️ 一体化架构(FE/BE) ⚠️ 多组件堆砌(HDFS+Hive+Spark+ZK...) Doris免去组件协调成本,运维负担降低70%+
查询性能 ⭐️ 亚秒~秒级(MPP+列存+向量化) ⚠️ 依赖计算引擎(如Impala秒~分钟级) 高并发场景下Doris吞吐量提升5-10倍
数据新鲜度 ⭐️ 支持秒级数据接入(Kafka/CDC) ⚠️ 批量导入为主(小时级) Doris实现实时分析(如监控大屏、风控)
运维成本 ⭐️ 自动化分片/副本/均衡 ⚠️ 需手动调优HDFS/YARN/计算引擎 Doris无需专职Hadoop团队维护
存储成本 ✅ 列存压缩(ZSTD/LZ4) ✅ HDFS三副本+列存(ORC/Parquet) 两者相当
数据更新 ⭐️ 支持Upsert(主键模型) ⚠️ Hive不支持,需重写分区 Doris满足实时更新需求(如用户画像)
联邦查询 ⭐️ 统一查询Hive/Iceberg/MySQL等 ⚠️ 需Presto/Trino额外组件 Doris减少数据搬迁,直查数据湖

典型案例提升:某电商公司用Doris替换Hive+Spark,订单分析延迟从15分钟降至3秒,服务器资源减少60%。

适用场景对比

场景 Apache Doris 推荐度 传统架构推荐度 原因
实时交互式 BI 报表 ⭐️⭐️⭐️⭐️⭐️ ⭐️⭐️ Doris 高并发查询优势明显
实时数仓(分钟级延迟) ⭐️⭐️⭐️⭐️⭐️ ⭐️ Doris 流批一体,简化链路
大规模离线 ETL(T+1) ⭐️ ⭐️⭐️⭐️⭐️⭐️ Spark 更擅长复杂批处理
日志分析(PB 级) ⭐️⭐️⭐️⭐️ ⭐️⭐️⭐️ Doris 压缩比高,但 ES 更擅长文本检索
高并发点查(在线服务) ⭐️⭐️⭐️⭐️ ⭐️ Doris 主键模型支持毫秒级 KV 查询
机器学习/数据科学 ⭐️ ⭐️⭐️⭐️⭐️⭐️ Spark MLlib 生态更成熟

什么情况下选择doris

指标 推荐选择 Doris 推荐传统架构
延迟要求 秒级~分钟级响应 小时级~天级延迟可接受
数据规模 单集群 ≤10PB ≥10PB 超大规模离线数据
团队规模 中小团队(需降低运维复杂度) 有专职 Hadoop 团队
业务场景 实时大屏/Ad-hoc 查询/高并发报表 复杂 ETL/机器学习/非结构化数据分析
更新需求 需要实时 Upsert 或删除数据 仅追加写入(Append-only)

1.3、doris是java团队掌控大数据能力最优选择

如果你的团队是Java语言团队,在无大数据专家的情况下处理海量数据存储与分析,Apache Doris 是更简单、高效且成本可控的解决方案。它能让您用最小技术代价获得实时分析能力,避免陷入 Hadoop 生态的复杂运维泥潭。

核心价值点

  • 零大数据栈依赖

    → 开箱即用,免部署 Hadoop/Hive/Spark,节省 80% 运维成本。

  • Java 技术栈无缝衔接

    → FE 用 Java 开发,兼容 MySQL 协议/JDBC,团队可立即上手开发。

  • 实时分析与简化架构

    → 支持 秒级数据接入+亚秒级查询,替代传统 T+1 数仓,性能提升 10 倍+。

  • 弹性扩展

    → 从 3 节点起步,随业务增长一键扩容,硬件成本降低 50%。

java团队使用doris与传统架构对比优势

痛点 传统方案(Hive/Spark) Doris 方案
学习成本 需掌握 Hive/Scala/Spark 等多技术栈 仅需 Java + SQL 基础
实时性 分钟~小时级延迟 秒级响应
运维复杂度 需专职大数据团队维护 开发兼职运维
开发效率 需编写 Spark 作业并调优,周期长 JDBC 直连 + 标准 SQL,快速开发

Apache Doris 与 Elasticsearch 的核心对比及选型建议

能力维度 Apache Doris Elasticsearch (ES) 适用场景建议
数据模型 结构化数据(强类型、表结构) 半结构化/JSON 文档(Schema-less) 固定分析用 Doris,多变日志用 ES
查询类型 高性能聚合分析(GROUP BY/JOIN/窗口函数) 全文检索/模糊匹配/KNN 向量搜索 数值分析选 Doris,文本搜索选 ES
实时写入 支持 Upsert,数据写入即可查(毫秒级) 近实时(1s 刷新间隔) 两者均满足实时需求
并发能力 ⭐️ 支持 10K+ QPS(高并发点查+聚合) ⚠️ 高聚合负载下并发受限(通常 ≤1K QPS) 高并发报表用 Doris
运维复杂度 自动化分片/均衡,扩缩容一条 SQL 需手动调优分片、副本、路由规则 Java 团队首选 Doris(省心 50%)
存储成本 列存压缩(ZSTD),节省空间 倒排索引占空间(原始数据 1.5-2 倍) 成本敏感选 Doris
SQL 支持 完整 ANSI SQL,兼容 MySQL 协议 SQL 为插件(功能受限),主用 DSL Java 开发者更熟悉 Doris
生态整合 联邦查询 Hive/MySQL,BI 工具无缝对接 Kibana 生态强,但 BI 对接复杂 多源分析用 Doris,日志看板用 ES

给 Java 团队的建议:

  • 纯分析场景 → 100% 选择 Doris:

    • 用 JDBC 直连,开发效率高,无需学习 ES 的 DSL。

    • 省去 ES 集群的运维负担(分片均衡、JVM 调优)。

  • 日志为主场景 → ES 为核心,Doris 辅助分析:

    • 原始日志存 ES,每日聚合结果同步到 Doris 做深度分析。
  • 新项目启动 → 从 Doris 开始:

    • 未来扩展检索能力时再引入 ES,避免初期架构过重。

结构化数据分析用 Doris,文本检索用 ES,两者混合能应对 99% 的海量数据处理需求。作为 Java 团队,Doris 的 MySQL 协议和简洁架构会让您事半功倍。


文本检索大家都清楚,上面一直说的分析是什么?

分析: 指从原始数据中提取有价值信息的过程,通过系统性的计算、统计和逻辑推理,揭示数据背后的规律、趋势和洞见。

分析的核心目标

  • 描述现状

    回答 "发生了什么?"

    例:统计昨日订单量、用户活跃地域分布。

  • 诊断原因

    回答 "为什么发生?"

    例:分析某商品销量骤降是否因价格上调导致。

  • 预测趋势

    回答 "未来会怎样?"

    例:基于历史数据预测下季度营收。

  • 指导决策

    回答 "该怎么做?"

    例:通过用户行为漏斗分析优化产品转化路径。

简单分析示例:

操作类型 示例场景 SQL 实现(Doris/ES)
聚合计算 统计每日总销售额 SELECT SUM(sales) FROM orders GROUP BY dt
过滤查询 查询某用户最近3次订单 SELECT * FROM orders WHERE user_id=123 ORDER BY dt DESC LIMIT 3
多维度分组 分地区/品类计算销量TOP10 SELECT region, category, SUM(sales) FROM orders GROUP BY region, category ORDER BY SUM(sales) DESC LIMIT 10
去重统计 计算每日活跃用户数(UV) SELECT dt, COUNT(DISTINCT user_id) FROM logs GROUP BY dt

1.4、 OLTP(在线事务处理) 与 OLAP(在线分析处理)

OLTP(Online Transaction Processing在线事务处理)与OLAP(Online Analytical Processing在线分析处理)核心区别:

  • OLTP 是"写操作"为主的实时业务系统(如订单支付),OLAP 是"读操作"为主的分析系统(如销售报表)。
  • 两者如同汽车的 发动机(OLTP) 和 仪表盘(OLAP) ------ 一个负责运行,一个负责洞察。

OLTP与OLAP对比表格

对比维度 OLTP(如MySQL) OLAP(如Doris) 典型应用场景建议
核心目标 处理日常高并发事务 执行复杂数据分析 事务处理用MySQL,分析用Doris
读写比例 读写均衡(70%写+30%读) 读为主(5%写+95%读) 报表类业务适合OLAP
数据规模 处理当前数据(GB~TB级) 分析历史海量数据(TB~PB级) 海量日志/行为数据存储到Doris
操作类型 CRUD操作(增删改查) 复杂查询(GROUP BY/JOIN/窗口函数) 分析语句迁移后性能提升显著
响应速度 毫秒级(保障业务流畅性) 秒级(容忍更高延迟) 报表查询从分钟级优化到秒级
数据结构 高度规范化(3NF减少冗余) 星型/雪花模型(优化分析效率) Doris宽表设计简化分析逻辑
典型系统 订单系统、用户数据库 数据仓库、BI平台 用Doris构建实时数仓

MySQL 适合事务处理(OLTP),Doris 专为海量数据分析(OLAP)设计。

1.5、发展历程

Apache Doris 最初是百度广告报表业务的 Palo 项目。2017 年正式对外开源,2018 年 7 月由百度捐赠给 Apache 基金会进行孵化。在 Apache 导师的指导下,由孵化器项目管理委员会成员进行孵化和运营。2022 年 6 月,Apache Doris 成功从 Apache 孵化器毕业,正式成为 Apache 顶级项目(Top-Level Project,TLP)。

1.6、应用现状

Apache Doris 在中国乃至全球范围内拥有广泛的用户群体。截至目前,Apache Doris 已经在全球超过 5000 家中大型企业的生产环境中得到应用。在中国市值或估值排行前 50 的互联网公司中,有超过 80% 长期使用 Apache Doris,包括百度、美团、小米、京东、字节跳动、阿里巴巴、腾讯、网易、快手、微博等。同时,在金融、消费、电信、工业制造、能源、医疗、政务等传统行业也有着丰富的应用。

在中国,几乎所有的云厂商,如阿里云、华为云、天翼云、腾讯云、百度云、火山引擎等,都在提供托管的 Apache Doris 云服务。

1.7、整体架构

Apache Doris 采用 MySQL 协议,高度兼容 MySQL 语法,支持标准 SQL。用户可以通过各类客户端工具访问 Apache Doris,并支持与 BI 工具无缝集成。

1.7.1、存算一体架构

Apache Doris 存算一体架构精简且易于维护。它包含以下两种类型的进程:

  • Frontend (FE): 主要负责接收用户请求、查询解析和规划、元数据管理以及节点管理。

  • Backend (BE): 主要负责数据存储和查询计划的执行。数据会被切分成数据分片(Shard),在 BE 中以多副本方式存储。

在生产环境中,可以部署多个 FE 节点以实现容灾备份。每个 FE 节点都会维护完整的元数据副本。FE 节点分为以下三种角色:

角色 功能
Master FE Master 节点负责元数据的读写。当 Master 节点的元数据发生变更后,会通过 BDB JE 协议同步给 Follower 或 Observer 节点。
Follower Follower 节点负责读取元数据。当 Master 节点发生故障时,可以选取一个 Follower 节点作为新的 Master 节点。
Observer Observer 节点负责读取元数据,主要目的是增加集群的查询并发能力。Observer 节点不参与集群的选主过程。

FE 和 BE 进程都可以横向扩展。单个集群可以支持数百台机器和数十 PB 的存储容量。FE 和 BE 进程通过一致性协议来保证服务的高可用性和数据的高可靠性。存算一体架构高度集成,大幅降低了分布式系统的运维成本。

1.7.2、存算分离架构

从 3.0 版本开始,可以选择存算分离部署架构。Apache Doris 存算分离版使用统一的共享存储层作为数据存储空间。存储和计算分离后,用户可以独立扩展存储容量和计算资源,从而实现最佳性能和成本效益。存算分离架构分为以下三层:

  • 元数据层: 负责请求规划、查询解析以及元数据的存储和管理。

  • 计算层: 由多个计算组组成。每个计算组可以作为一个独立的租户承担业务计算。每个计算组包含多个无状态的 BE 节点,可以随时弹性伸缩 BE 节点。

  • 存储层: 可以使用 S3、HDFS、OSS、COS、OBS、Minio、Ceph 等共享存储来存放 Doris 的数据文件,包括 Segment 文件和反向索引文件等。

1.8、Apache Doris 的核心特性

  • 高可用: Apache Doris 的元数据和数据均采用多副本存储,并通过 Quorum 协议同步数据日志。当大多数副本完成写入后,即认为数据写入成功,从而确保即使少数节点发生故障,集群仍能保持可用性。Apache Doris 支持同城和异地容灾,能够实现双集群主备模式。当部分节点发生异常时,集群可以自动隔离故障节点,避免影响整体集群的可用性。

  • 高兼容: Apache Doris 高度兼容 MySQL 协议,支持标准 SQL 语法,涵盖绝大部分 MySQL 和 Hive 函数。通过这种高兼容性,用户可以无缝迁移和集成现有的应用和工具。Apache Doris 支持 MySQL 生态,用户可以通过 MySQL 客户端工具连接 Doris,使得操作和维护更加便捷。同时,可以使用 MySQL 协议对 BI 报表工具与数据传输工具进行兼容适配,确保数据分析和数据传输过程中的高效性和稳定性。

  • 实时数仓: 基于 Apache Doris 可以构建实时数据仓库服务。Apache Doris 提供了秒级数据入库能力,上游在线联机事务库中的增量变更可以秒级捕获到 Doris 中。依靠向量化引擎、MPP 架构及 Pipeline 执行引擎等加速手段,可以提供亚秒级数据查询能力,从而构建高性能、低延迟的实时数仓平台。

  • 湖仓一体: Apache Doris 可以基于外部数据源(如数据湖或关系型数据库)构建湖仓一体架构,从而解决数据在数据湖和数据仓库之间无缝集成和自由流动的问题,帮助用户直接利用数据仓库的能力来解决数据湖中的数据分析问题,同时充分利用数据湖的数据管理能力来提升数据的价值。

  • 灵活建模: Apache Doris 提供多种建模方式,如宽表模型、预聚合模型、星型/雪花模型等。数据导入时,可以通过 Flink、Spark 等计算引擎将数据打平成宽表写入到 Doris 中,也可以将数据直接导入到 Doris 中,通过视图、物化视图或实时多表关联等方式进行数据的建模操作。

1.9、技术特点

1.9.1、使用接口

Apache Doris 采用 MySQL 协议,高度兼容 MySQL 语法,支持标准 SQL。用户可以通过各类客户端工具访问 Apache Doris,并支持与 BI 工具无缝集成。Apache Doris 当前支持多种主流的 BI 产品,包括 Smartbi、DataEase、FineBI、Tableau、Power BI、Apache Superset 等。只要支持 MySQL 协议的 BI 工具,Apache Doris 就可以作为数据源提供查询支持。

1.9.2、存储引擎

在存储引擎方面,Apache Doris 采用列式存储,按列进行数据的编码、压缩和读取,能够实现极高的压缩比,同时减少大量非相关数据的扫描,从而更有效地利用 IO 和 CPU 资源。

Apache Doris 也支持多种索引结构,以减少数据的扫描:

  • Sorted Compound Key Index: 最多可以指定三个列组成复合排序键。通过该索引,能够有效进行数据裁剪,从而更好地支持高并发的报表场景。

  • Min/Max Index: 有效过滤数值类型的等值和范围查询。

  • BloomFilter Index: 对高基数列的等值过滤裁剪非常有效。

  • Inverted Index: 能够对任意字段实现快速检索。

在存储模型方面,Apache Doris 支持多种存储模型,针对不同的场景做了针对性的优化:

  • 明细模型(Duplicate Key Model): 适用于事实表的明细数据存储。

  • 主键模型(Unique Key Model): 保证 Key 的唯一性,相同 Key 的数据会被覆盖,从而实现行级别数据更新。

  • 聚合模型(Aggregate Key Model): 相同 Key 的 Value 列会被合并,通过提前聚合大幅提升性能。

Apache Doris 也支持强一致的单表物化视图和异步刷新的多表物化视图。单表物化视图在系统中自动刷新和维护,无需用户手动选择。多表物化视图可以借助集群内的调度或集群外的调度工具定时刷新,从而降低数据建模的复杂性。

1.9.3、查询引擎

Apache Doris 采用大规模并行处理(MPP)架构,支持节点间和节点内并行执行,以及多个大型表的分布式 Shuffle Join,从而更好地应对复杂查询。

Doris 查询引擎是向量化引擎,所有内存结构均按列式布局,可显著减少虚函数调用,提高缓存命中率,并有效利用 SIMD 指令。在宽表聚合场景下,性能是非向量化引擎的 5-10 倍。

Doris 采用自适应查询执行(Adaptive Query Execution)技术,根据运行时统计信息动态调整执行计划。例如,通过运行时过滤(Runtime Filter)技术,可以在运行时生成过滤器并将其推送到 Probe 端,并自动将过滤器穿透到 Probe 端最底层的 Scan 节点,从而大幅减少 Probe 端的数据量,加速 Join 性能。Doris 的运行时过滤器支持 In/Min/Max/Bloom Filter。

Doris 使用 Pipeline 执行引擎,将查询分解为多个子任务并行执行,充分利用多核 CPU 的能力,同时通过限制查询线程数来解决线程膨胀问题。Pipeline 执行引擎减少数据拷贝和共享,优化排序和聚合操作,从而显著提高查询效率和吞吐量。

在优化器方面,Doris 采用 CBO、RBO 和 HBO 相结合的优化策略。RBO 支持常量折叠、子查询重写和谓词下推等优化,CBO 支持 Join Reorder 等优化,HBO 能够基于历史查询信息推荐最优执行计划。多种优化措施确保 Doris 能够在各类查询中枚举出性能优异的查询计划。

二、部署前准备

2.1、软硬件环境检查

2.1.1、硬件环境检查

在硬件环境检查中,要对以下硬件条件进行检查

检查项 建议配置
CPU 支持 AVX2 指令集
内存 建议至少 CPU 4 倍
存储 推荐 SSD 硬盘
文件系统 ext4 或 xfs 文件系统
网卡 10GbE 网卡
  • CPU 检查

当安装 Doris 时,建议选择支持 AVX2 指令集的机器,以利用 AVX2 的向量化能力实现查询向量化加速。

运行以下命令,有输出结果,及表示机器支持 AVX2 指令集。

shell 复制代码
cat /proc/cpuinfo | grep avx2

如果机器不支持 AVX2 指令集,可以使用 no AVX2 的 Doris 安装包进行部署。

  • 内存检查

Doris 没有强制的内存限制。一般在生产环境中,可以根据以下建议选择内存大小:

组件 推荐内存配置
FE 建议至少 16GB 以上
BE 建议内存至少是 CPU 核数的 4 倍(例如,16 核机器至少配置 64G 内存)。在内存是 CPU 核数的 8 倍时,会得到更好的性能。
  • 存储检查

Doris 支持将数据存储在 SSD(固态硬盘,Solid State Drive)、HDD(机械硬盘,Hard Disk Drive) 或对象存储(Object Storage)中。

在以下几种场景中建议使用 SSD 作为数据存储:

  1. 大规模数据量下的高并发点查场景

  2. 大规模数据量下的高频数据更新场景

存储技术对比

维度 HDD SSD 对象存储
存储介质 磁性盘片 + 机械结构 闪存芯片(NAND Flash) 分布式服务器集群
读写速度 慢(MB 级 / 秒) 快(GB 级 / 秒) 中等(依赖网络和集群)
成本(单位容量) 中低
适用数据类型 结构化 / 小文件 高频访问数据 非结构化 / 海量数据
典型场景 冷备份、大容量存储 系统盘、高性能计算 云存储、媒体归档
  • 文件系统检查

Doris 推荐使用 EXT4 或 XFS 文件系统:

  1. EXT4 文件系统:具有良好的稳定性、性能和较低的碎片化问题。

  2. XFS 文件系统:在处理大规模数据和高并发写操作时表现优越,适合高吞吐量应用。

  • 网卡检查

Doris 的计算过程涉及数据分片和并行处理,可能产生网络资源开销。为了最大程度优化 Doris 性能并降低网络资源开销,强烈建议在部署时选用万兆网卡(10 Gigabit Ethernet,即 10GbE)或者更快网络。如果有多块网卡,建议使用链路聚合方式将多块网卡绑定成一块网卡,提高网络带宽、冗余性和复杂均衡的能力。

2.1.2、服务器建议配置

Doris 支持运行和部署在 x86-64 架构的服务器平台或 ARM64 架构的服务器上。

  • 开发及测试环境

    开发与测试环境中可以混合部署 FE 与 BE 实例,遵循以下规则:

    • 验证测试环境中可以在一台服务器上混合部署一个 FE 与 BE,但不建议部署多个 FE 与 BE 实例;

    • 如果需要 3 副本数据,至少需要 3 台服各部署一个 BE 实例。

服务器规格建议如下:

模块 CPU 内存 磁盘(存储类型+容量) 网络配置 实例数量(最低要求)
Frontend 8 核 + 8 GB+ SSD/SATA,10 GB+ 1GbE/10GbE 网卡 1
Backend 8 核 + 16 GB+ SSD/SATA,50 GB+ 1GbE/10GbE 网卡 1
  • 生产环境

    生产环境中建议 FE 与 BE 实例独立部署,遵循以下规则:

    • 如果环境资源紧张,将 FE 与 BE 混部在一台服务器上,建议 FE 与 BE 数据放在不同的硬盘;

    • BE 节点可以配置多块硬盘存储,在一个 BE 实例上绑定多块 HDD 或 SSD 盘。

服务器规格建议如下:

模块 CPU 内存 磁盘 网络 实例数量(最低要求)
Frontend 16 核 + 64 GB+ SSD,100GB+ 10GbE 网卡 1
Backend 16 核 + 64 GB+ SSD 或 SATA,100GB+ 10GbE 网卡 3

2.1.3、硬盘空间计算

在 Doris 集群中,FE 主要用于元数据存储,包括元数据 edit log 和 image。BE 的磁盘空间主要用于存放数据,需要根据业务需求计算。

组件 磁盘空间说明
FE 建议预留 100GB 以上的存储空间,使用 SSD 硬盘
BE Doris 默认 LZ4 压缩方式存储(压缩比 0.3-0.5),按总数据量 * 3(3副本)计算空间,并额外预留40%用于后台 compaction 及临时数据存储

2.1.4、Java 环境检查

Doris 的所有进程都依赖 Java。

  • 在 2.1(含)版本之前,请使用 Java 8,推荐版本:jdk-8u352 之后版本。

  • 从 3.0(含)版本之后,请使用 Java 17,推荐版本:jdk-17.0.10 之后版本。

2.2、集群规划

2.2.1、架构规划

在部署 Doris 时,可以根据业务选择存算一体或存算分离架构:

  • 存算一体:存算一体架构部署简单,性能优异,不依赖与外部的共享存储设备,适合不需要极致弹性扩缩容的业务场景;

  • 存算分离:存算分离架构依赖于共享存储,实现了计算资源的弹性伸缩,适合需要动态调整计算资源的业务场景。

2.2.2、端口规划

Doris 的各个实例通过网络进行通信,其正常运行需要网络环境提供以下端口。管理员可以根据实际环境自行调整 Doris 的端口配置:

以下是整理后的表格格式:

实例名称 端口名称 默认端口 通信方向 说明
BE be_port 9060 FE -> BE BE 上 Thrift Server 的端口,用于接收来自 FE 的请求
BE webserver_port 8040 BE <-> BE BE 上的 HTTP Server 端口
BE heartbeat_service_port 9050 FE -> BE BE 上的心跳服务端口(Thrift),用于接收来自 FE 的心跳
BE brpc_port 8060 FE <-> BE,BE <-> BE BE 上的 BRPC 端口,用于 BE 之间的通信
FE http_port 8030 FE <-> FE,Client <-> FE FE 上的 HTTP Server 端口
FE rpc_port 9020 BE -> FE,FE <-> FE FE 上的 Thrift Server 端口,每个 FE 的配置需保持一致
FE query_port 9030 Client <-> FE FE 上的 MySQL Server 端口
FE edit_log_port 9010 FE <-> FE FE 上的 bdbje 通信端口

2.2.3、节点数量规划

  • FE 节点数量

FE 节点主要负责用户请求的接入、查询解析规划、元数据管理及节点管理等工作。

对于生产集群,一般建议部署至少 3 个节点的 FE 以实现高可用环境。FE 节点分为以下两种角色:

  • Follower 节点:参与选举操作,当 Master 节点宕机时,会选择一个可用的 Follower 节点成为新的 Master。

  • Observer 节点:仅从 Leader 节点同步元数据,不参与选举,可用于横向扩展以提升元数据的读服务能力。

通常情况下,建议部署至少 3 个 Follower 节点。在高并发的场景中,可以通过增加 Observer 节点的数量来提高集群的连接数。

  • BE 节点数量

BE 节点负责数据的存储与计算。在生产环境中,为了数据的可靠性和容错性,通常会使用 3 副本存储数据,因此建议部署至少 3 个 BE 节点。

BE 节点支持横向扩容,通过增加 BE 节点的数量,可以有效提升查询的性能和并发处理能力。

2.3、操作系统检查

在部署 Doris 时,需要对以下操作系统项进行检查:

  • 确保关闭 swap 分区

  • 确保系统关闭透明大页

  • 确保系统有足够大的虚拟内存区域

  • 确保 CPU 不使用省电模式

  • 确保网络连接溢出时自动重置新连接

  • 确保 Doris 相关端口畅通或关闭系统防火墙

  • 确保系统有足够大的打开文件句柄数

  • 确定部署集群机器安装 NTP 服务

2.3.1、关闭 swap 分区

在部署 Doris 时,建议关闭 swap 分区。swap 分区是内核发现内存紧张时,会按照自己的策略将部分内存数据移动到配置的 swap 分区,由于内核策略不能充分了解应用的行为,会对 Doris 性能造成较大影响。所以建议关闭。

通过以下命令可以临时或者永久关闭。

临时关闭,下次机器启动时,swap 还会被打开。

shell 复制代码
swapoff -a

永久关闭,使用 Linux root 账户,注释掉 /etc/fstab 中的 swap 分区,重启即可彻底关闭 swap 分区。

shell 复制代码
# /etc/fstab
# <file system>        <dir>         <type>    <options>             <dump> <pass>
tmpfs                  /tmp          tmpfs     nodev,nosuid          0      0
/dev/sda1              /             ext4      defaults,noatime      0      1
# /dev/sda2              none          swap      defaults              0      0
/dev/sda3              /home         ext4      defaults,noatime      0      2

2.3.2、关闭系统透明大页

在高负载低延迟的场景中,建议关闭操作系统透明大页(Transparent Huge Pages, THP),避免其带来的性能波动和内存碎片问题,确保 Doris 能够稳定高效地使用内存。

使用以下命令临时关闭透明大页:

shell 复制代码
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag

如果需要永久关闭透明大页,可以使用以下命令,在下一次宿主机重启后生效:

shell 复制代码
cat >> /etc/rc.d/rc.local << EOF
   echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
   echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
EOF
chmod +x /etc/rc.d/rc.local

2.3.3、增加虚拟内存区域

为了保证 Doris 有足够的内存映射区域来处理大量数据,需要修改 VMA(虚拟内存区域)。如果没有足够的内存映射区域,Doris 在启动或运行时可能会遇到 Too many open files 或类似的错误。

通过以下命令可以永久修改虚拟内存区域至少为 2000000,并立即生效:

shell 复制代码
cat >> /etc/sysctl.conf << EOF
vm.max_map_count = 2000000
EOF

# Take effect immediately
sysctl -p

2.3.4、禁用 CPU 省电模式

在部署 Doris 时检修关闭 CPU 的省电模式,以确保 Doris 在高负载时提供稳定的高性能,避免由于 CPU 频率降低导致的性能波动、响应延迟和系统瓶颈,提高 Doris 的可靠性和吞吐量。如果您的 CPU 不支持 Scaling Governor,可以跳过此项配置。

通过以下命令可以关闭 CPU 省电模式:

shell 复制代码
echo 'performance' | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

2.3.5、网络连接溢出时自动重置新连接

在部署 Doris 时,需要确保在 TCP 连接的发送缓冲区溢出时,连接会被立即中断,以防止 Doris 在高负载或高并发情况下出现缓冲区阻塞,避免连接被长时间挂起,从而提高系统的响应性和稳定性。

通过以下命令可以永久设置系统自动重置新链接,并立即生效:

shell 复制代码
cat >> /etc/sysctl.conf << EOF
net.ipv4.tcp_abort_on_overflow=1
EOF

# Take effect immediately
sysctl -p

2.3.6、相关端口畅通

如果发现端口不通,可以试着关闭防火墙,确认是否是本机防火墙造成。如果是防火墙造成,可以根据配置的 Doris 各组件端口打开相应的端口通信。

shell 复制代码
sudo systemctl stop firewalld.service
sudo systemctl disable firewalld.service

2.3.7、增加系统的最大文件句柄数

Doris 由于依赖大量文件来管理表数据,所以需要将系统对程序打开文件数的限制调高。

通过以下命令可以调整最大文件句柄数。在调整后,需要重启会话以生效配置:

shell 复制代码
vi /etc/security/limits.conf 
* soft nofile 1000000
* hard nofile 1000000

2.3.8、安装并配置 NTP 服务

Doris 的元数据要求时间精度要小于 5000ms,所以所有集群所有机器要进行时钟同步,避免因为时钟问题引发的元数据不一致导致服务出现异常。

通常情况下,可以通过配置 NTP 服务保证各节点时钟同步。

复制代码
sudo systemctl start_ntpd.service
sudo systemctl enable_ntpd.service

三、部署doris

3.1、手动部署存算一体集群

在完成前置检查及规划后,如环境检查、操作系统检查、集群规划,可以开始部署集存算一体集群。

存算一体集群架构如下,部署存算一体集群分为四步:

  1. 部署 FE Master 节点:部署第一个 FE 节点作为 Master 节点;

  2. 部署 FE 集群:部署 FE 集群,添加 Follower 或 Observer FE 节点;

  3. 部署 BE 节点:向 FE 集群中注册 BE 节点;

  4. 验证集群正确性:部署完成后连接并验证集群正确性。

3.1.1、部署 FE Master 节点

  1. 创建元数据路径

在部署 FE 时,建议与 BE 节点数据存储在不同的硬盘上。

在解压安装包时,会默认附带 doris-meta 目录,建议为元数据创建独立目录,并将其软连接到默认的 doris-meta 目录。生产环境应使用单独的 SSD 硬盘,不建议将其放在 Doris 安装目录下;开发和测试环境可以使用默认配置。

shell 复制代码
## Use a separate disk for FE metadata
mkdir -p <doris_meta_created>
   
## Create FE metadata directory symlink
ln -s <doris_meta_created> <doris_meta_original>

就是创建一个独立目录,然后软连接doris的doris-meta的目录

  1. 修改 FE 配置文件

FE 的配置文件在 FE 部署路径下的 conf 目录中,启动 FE 节点前需要修改conf/fe.conf

在部署 FE 节点之前,建议调整以下配置:

shell 复制代码
## modify Java Heap
JAVA_OPTS="-Xmx16384m -XX:+UseMembar -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=7 -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSClassUnloadingEnabled -XX:-CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -Xloggc:$DORIS_HOME/log/fe.gc.log.$DATE"
   
## modify case sensitivity
lower_case_table_names = 1
  
## modify network CIDR 
priority_networks = 10.1.3.0/24
   
## modify Java Home
JAVA_HOME = <your-java-home-path>
参数 修改建议
JAVA_OPTS 指定参数 -Xmx 调整 Java Heap,生产环境建议 16G 以上。
lower_case_table_names 设置大小写敏感,建议调整为 1,即大小写不敏感。
priority_networks 网络 CIDR,根据网络 IP 地址指定。在 FQDN 环境中可以忽略。
JAVA_HOME 建议 Doris 使用独立于操作系统的 JDK 环境。

更多详细配置项请参考 FE 配置项: https://doris.apache.org/zh-CN/docs/3.0/admin-manual/config/fe-config

  1. 启动 FE 进程

通过以下命令可以启动 FE 进程

shell 复制代码
bin/start_fe.sh --daemon

FE 进程将在后台启动,日志默认保存在 log/ 目录。如果启动失败,可通过查看 log/fe.log 或 log/fe.out 文件获取错误信息。

  1. 检查 FE 启动状态

通过 MySQL 客户端连接 Doris 集群,初始化用户为 root,默认密码为空。

shell 复制代码
mysql -uroot -P<fe_query_port> -h<fe_ip_address>

链接到 Doris 集群后,可以通过 show frontends 命令查看 FE 的状态,通常要确认以下几项

  • Alive 为 true 表示节点存活;

  • Join 为 true 表示节点加入到集群中,但不代表当前还在集群内(可能已失联);

  • IsMaster 为 true 表示当前节点为 Master 节点。

3.1.2、部署 FE 集群(可选)

生产环境建议至少部署 3 个节点。在部署过 FE Master 节点后,需要再部署两个 FE Follower 节点。

  1. 创建元数据目录

参考部署 FE Master 节点,创建 doris-meta 目录

  1. 修改 FE Follower 节点配置文件

参考部署 FE Master 节点,修改 FE Follower 节点配置文件。通常情况下,可以直接复制 FE Master 节点的配置文件。

  1. 在 Doris 集群中注册新的 FE Follower 节点

在启动新的 FE 节点前,需要先在 FE 集群中注册新的 FE 节点。

shell 复制代码
## connect a alive FE node
mysql -uroot -P<fe_query_port> -h<fe_ip_address>

## registe a new FE follower node
ALTER SYSTEM ADD FOLLOWER "<fe_ip_address>:<fe_edit_log_port>"

如果要添加 observer 节点,可以使用 ADD OBSERVER 命令

shell 复制代码
## register a new FE observer node
ALTER SYSTEM ADD OBSERVER "<fe_ip_address>:<fe_edit_log_port>"

注意

  • FE Follower(包括 Master)节点的数量建议为奇数,建议部署 3 个组成高可用模式。
  • 当 FE 处于高可用部署时(1 个 Master,2 个 Follower),我们建议通过增加 Observer FE 来扩展 FE 的读服务能力
  1. 启动 FE Follower 节点

通过以下命令,可以启动 FE Follower 节点,并自动同步元数据。

shell 复制代码
bin/start_fe.sh --helper <helper_fe_ip>:<fe_edit_log_port> --daemon

其中,helper_fe_ip 是 FE 集群中任何存活节点的 IP 地址。--helper 参数仅在第一次启动 FE 时需要,之后重启无需指定。

  1. 判断 Follower 节点状态

与 FE Master 节点状态判断相同,添加 Follower 节点后,可通过 show frontends 命令查看节点状态,IsMaster 应为 false。

3.1.3、部署 BE 节点

  1. 创建数据目录

BE 进程应用于数据的计算与存储。数据目录默认放在 be/storage 下。生产环境通常将 BE 数据与 BE 部署文件分别存储在不同的硬盘上。BE 支持数据分布在多盘上以更好的利用多块硬盘的 I/O 能力。

shell 复制代码
## Create a BE data storage directory on each data disk
mkdir -p <be_storage_root_path>
  1. 修改 BE 配置文件

BE 的配置文件在 BE 部署路径下的 conf 目录中,启动 BE 节点前需要修改 conf/be.conf

shell 复制代码
## modify storage path for BE node,上一步新建的storage目录,可以建多个放不同盘

storage_root_path=/home/disk1/doris,medium:HDD;/home/disk2/doris,medium:SSD

## modify network CIDR 

priority_networks = 10.1.3.0/24

## modify Java Home in be/conf/be.conf

JAVA_HOME = <your-java-home-path>
参数 修改建议
priority_networks 网络 CIDR,根据网络 IP 地址指定。在 FQDN (Fully Qualified Domain Name,完全限定域名)环境中可以忽略。
JAVA_OPTS 指定参数 -Xmx 调整 Java Heap,生产环境建议 2G 以上。
JAVA_HOME 建议 Doris 使用独立于操作系统的 JDK 环境。
  1. 在 Doris 中注册 BE 节点

在启动 BE 节点前,需要先在 FE 集群中注册该节点:

shell 复制代码
## connect a alive FE node
mysql -uroot -P<fe_query_port> -h<fe_ip_address>
   
## registe BE node
ALTER SYSTEM ADD BACKEND "<be_ip_address>:<be_heartbeat_service_port>"
  1. 启动 BE 进程

通过以下命令可以启动 BE 进程:

shell 复制代码
bin/start_be.sh --daemon

BE 进程在后台启动,日志默认保存在 log/ 目录。如果启动失败,请检查 log/be.log 或 log/be.out 文件以获取错误信息。

  1. 查看 BE 启动状态

连接 Doris 集群后,可通过show backends 命令查看 BE 节点的状态。

shell 复制代码
## connect a alive FE node
mysql -uroot -P<fe_query_port> -h<fe_ip_address>
   
## check BE node status
show backends;

通常情况下需要注意以下几项状态:

  • Alive 为 true 表示节点存活

  • TabletNum 表示该节点上的分片数量,新加入的节点会进行数据均衡,TabletNum 逐渐趋于平均。

3.1.4、验证集群正确性

  1. 登录数据库

使用 MySQL 客户端登录 Doris 集群。

shell 复制代码
## connect a alive fe node
mysql -uroot -P<fe_query_port> -h<fe_ip_address>
  1. 检查 Doris 安装信息

通过 show frontendsshow backends 可以查看数据库各实例的信息。

sql 复制代码
-- check fe status
show frontends \G

-- check be status
show backends \G
  1. 修改 Doris 集群密码

在创建 Doris 集群时,系统会自动创建一个名为 root 的用户,并默认设置其密码为空。为了提高安全性,建议在集群创建后立即为 root 用户设置一个新密码。

sql 复制代码
-- check the current user
select user();  
+------------------------+  
| user()                 |  
+------------------------+  
| 'root'@'192.168.88.30' |  
+------------------------+  
     
-- modify the password for current user
SET PASSWORD = PASSWORD('doris_new_passwd');
  1. 创建测试表并插入数据

为了验证集群的正确性,可以在新创建的集群中创建一个测试表,并插入测试数据。

sql-- 复制代码
create database testdb;
 
-- create a test table
CREATE TABLE testdb.table_hash
(
    k1 TINYINT,
    k2 DECIMAL(10, 2) DEFAULT "10.5",
    k3 VARCHAR(10) COMMENT "string column",
    k4 INT NOT NULL DEFAULT "1" COMMENT "int column"
)
COMMENT "my first table"
DISTRIBUTED BY HASH(k1) BUCKETS 32;

Doris 兼容 MySQL 协议,可以使用 INSERT 语句插入数据。

sql 复制代码
-- insert data
INSERT INTO testdb.table_hash VALUES
(1, 10.1, 'AAA', 10),
(2, 10.2, 'BBB', 20),
(3, 10.3, 'CCC', 30),
(4, 10.4, 'DDD', 40),
(5, 10.5, 'EEE', 50);

-- check the data
SELECT * from testdb.table_hash;
+------+-------+------+------+
| k1   | k2    | k3   | k4   |
+------+-------+------+------+
|    3 | 10.30 | CCC  |   30 |
|    4 | 10.40 | DDD  |   40 |
|    5 | 10.50 | EEE  |   50 |
|    1 | 10.10 | AAA  |   10 |
|    2 | 10.20 | BBB  |   20 |
+------+-------+------+------+

3.2、手动部署存算分离集群

在完成前置检查及规划后,如环境检查、集群规划、操作系统检查后,可以开始部署集群。部署集群分为八步:

  1. 准备 FoundationDB 集群:可以使用已有的 FoundationDB 集群,或新建 FoundationDB 集群;

  2. 部署 S3 或 HDFS 服务:可以使用已有的共享存储,或新建共享存储;

  3. 部署 Meta Service:为 Doris 集群部署 Meta Service 服务;

  4. 部署数据回收进程:为 Doris 集群独立部署数据回收进程,可选操作;

  5. 启动 FE Master 节点:启动第一个 FE 节点作为 Master FE 节点;

  6. 创建 FE Master 集群:添加 FE Follower/Observer 节点组成 FE 集群;

  7. 添加 BE 节点:向集群中添加并注册 BE 节点;

  8. 添加 Storage Vault:使用共享存储创建一个或多个 Storage Vault。

3.2.1、准备 FoundationDB

本节提供了脚本 fdb_vars.sh fdb_ctl.sh 配置、部署和启动 FDB(FoundationDB)服务的分步指南。您可以下载 doris tools 并从 fdb 目录获取 fdb_vars.shfdb_ctl.sh

  1. 机器要求

通常,至少需要三台配备 SSD 的机器来组成具有双副本、单机故障容忍的 FoundationDB 集群。如果是测试/开发环境,单台机器也能搭建 FoundationDB。

  1. 配置 fdb_vars.sh 脚本

在配置 fdb_vars.sh 脚本时,必须指定以下配置:

参数名 描述 类型 示例 注意事项
DATA_DIRS 指定 FoundationDB 存储的数据目录 以逗号分隔的绝对路径列表 /mnt/foundationdb/data1,/mnt/foundationdb/data2,/mnt/foundationdb/data3 运行脚本前确保目录已创建,生产环境建议使用 SSD 和独立目录
FDB_CLUSTER_IPS 定义集群 IP 字符串(以逗号分隔的 IP 地址) 172.200.0.2,172.200.0.3,172.200.0.4 生产集群至少应有 3 个 IP 地址,第一个 IP 地址将用作协调器,为高可用性,将机器放置在不同机架上
FDB_HOME 定义 FoundationDB 主目录 绝对路径 /fdbhome 默认路径为 /fdbhome,确保此路径是绝对路径
FDB_CLUSTER_ID 定义集群 ID 字符串 SAQESzbh 每个集群的 ID 必须唯一,可使用 mktemp -u XXXXXXXX 生成
FDB_CLUSTER_DESC 定义 FDB 集群的描述 字符串 dorisfdb 建议更改为对部署有意义的内容

可以选择指定以下自定义配置:

参数 描述 类型 示例 注意事项
MEMORY_LIMIT_GB 定义 FDB 进程的内存限制,单位为 GB 整数 MEMORY_LIMIT_GB=16 根据可用内存资源和 FDB 进程的要求调整此值
CPU_CORES_LIMIT 定义 FDB 进程的 CPU 核心限制 整数 CPU_CORES_LIMIT=8 根据可用的 CPU 核心数量和 FDB 进程的要求设置此值
  1. 部署 FDB 集群

使用 fdb_vars.sh 配置环境后,您可以在每个节点上使用 fdb_ctl.sh 脚本部署 FDB 集群。

shell 复制代码
./fdb_ctl.sh deploy
  1. 启动 FDB 服务

FDB 集群部署完成后,您可以使用 fdb_ctl.sh 脚本启动 FDB 服务。

shell 复制代码
./fdb_ctl.sh start

以上命令启动 FDB 服务,使集群工作并获取 FDB 集群连接字符串,后续可以用于配置 MetaService。

注意:fdb_ctl.sh 脚本中的 clean 命令会清除所有 fdb 元数据信息,可能导致数据丢失,严禁在生产环境中使用!

3.2.2、安装 S3 或 HDFS 服务(可选)

Doris 的存算分离模式依赖于 S3 或 HDFS 服务来存储数据,如果您已经有相关服务,直接使用即可。 如果没有,本文档提供 MinIO 的简单部署教程:

  1. 在 MinIO 的下载页面选择合适的版本以及操作系统,下载对应的 Server 以及 Client 的二进制包或安装包。

  2. 启动 MinIO Server

shell 复制代码
export MINIO_REGION_NAME=us-east-1
export MINIO_ROOT_USER=minio # 在较老版本中,该配置为 MINIO_ACCESS_KEY=minio
export MINIO_ROOT_PASSWORD=minioadmin # 在较老版本中,该配置为 MINIO_SECRET_KEY=minioadmin
nohup ./minio server /mnt/data 2>&1 &
  1. 配置 MinIO Client
shell 复制代码
# 如果你使用的是安装包安装的客户端,那么客户端名为 mcli,直接下载客户端二进制包,则其名为 mc
./mc config host add myminio http://127.0.0.1:9000 minio minioadmin
  1. 创建一个桶
shell 复制代码
./mc mb myminio/doris
  1. 验证是否正常工作
shell 复制代码
# 上传一个文件
./mc mv test_file myminio/doris
# 查看这个文件
./mc ls myminio/doris

3.2.3、Meta Service 部署

  1. 配置

./conf/doris_cloud.conf文件中,主要需要修改以下两个参数:

  • brpc_listen_port:Meta Service 的监听端口,默认为 5000。
  • fdb_cluster:FoundationDB 集群的连接信息,部署 FoundationDB 时可以获取。(如果使用 Doris 提供的 fdb_ctl.sh 部署的话,可在 $FDB_HOME/conf/fdb.cluster 文件里获取该值)。

示例配置:

shell 复制代码
brpc_listen_port = 5000
fdb_cluster = xxx:yyy@127.0.0.1:4500

注意:fdb_cluster 的值应与 FoundationDB 部署机器上的 /etc/foundationdb/fdb.cluster文件内容一致(如果使用 Doris 提供的 fdb_ctl.sh 部署的话,可在 $FDB_HOME/conf/fdb.cluster 文件里获取该值)。

示例,文件的最后一行就是要填到 doris_cloud.conf 里 fdb_cluster 字段的值:

shell 复制代码
cat /etc/foundationdb/fdb.cluster

DO NOT EDIT!
This file is auto-generated, it is not to be edited by hand.
cloud_ssb:A83c8Y1S3ZbqHLL4P4HHNTTw0A83CuHj@127.0.0.1:4500
  1. 启动与停止

在启动前,需要确保已正确设置 JAVA_HOME 环境变量,指向 OpenJDK 17,进入 ms 目录。

启动命令如下:

shell 复制代码
export JAVA_HOME=${path_to_jdk_17}
bin/start.sh --daemon

启动脚本返回值为 0 表示启动成功,否则启动失败。启动成功同时标准输出的最后一行文本信息为 "doris_cloud start successfully"。

停止命令如下:

shell 复制代码
bin/stop.sh

生产环境中请确保至少有 3 个 Meta Service 节点。

3.2.4、数据回收功能独立部署(可选)

Meta Service 本身具备了元数据管理和回收功能,这两个功能可以独立部署,如果需要独立部署数据回收功能,可参考以下步骤。

  1. 创建新的工作目录(如 recycler),并复制 ms 目录内容到新目录:
shell 复制代码
cp -r ms recycler
  1. 在新目录的配置文件中修改 BRPC 监听端口 brpc_listen_port 和 fdb_cluster 的值。

启动数据回收功能

shell 复制代码
export JAVA_HOME=${path_to_jdk_17}
bin/start.sh --recycler --daemon

启动仅元数据操作功能

shell 复制代码
export JAVA_HOME=${path_to_jdk_17}
bin/start.sh --meta-service --daemon

3.2.5、启动 FE Master 节点

  1. 配置 fe.conf 文件

fe.conf 文件中,需要配置以下关键参数:

  • deploy_mode
    • 描述:指定 doris 启动模式
    • 格式:cloud 表示存算分离模式,其它存算一体模式
    • 示例:cloud
  • cluster_id
    • 描述:存算分离架构下集群的唯一标识符,不同的集群必须设置不同的 cluster_id。
    • 格式:int 类型
    • 示例:可以使用如下 shell 脚本 echo $(($((RANDOM << 15)) | $RANDOM)) 生成一个随机 id 使用。
    • 注意:不同的集群必须设置不同的 cluster_id
  • meta_service_endpoint
    • 描述:Meta Service 的地址和端口
    • 格式:IP地址:端口号
    • 示例:127.0.0.1:5000,可以用逗号分割配置多个 meta service。
  1. 启动 FE Master 节点

启动命令:

shell 复制代码
bin/start_fe.sh --daemon

第一个 FE 进程初始化集群并以 FOLLOWER 角色工作。使用 mysql 客户端连接 FE 使用 show frontends 确认刚才启动的 FE 是 master。

3.2.6、注册 FE Follower/Observer 节点

其他节点同样根据上述步骤修改配置文件并启动,使用 mysql 客户端连接 Master 角色的 FE,并用以下 SQL 命令添加额外的 FE 节点:

shell 复制代码
ALTER SYSTEM ADD FOLLOWER "host:port";

host:port 替换为 FE 节点的实际地址和编辑日志端口。更多信息请参见 ADD FOLLOWERADD OBSERVER

生产环境中,请确保在 FOLLOWER 角色中的前端(FE)节点总数,包括第一个 FE,保持为奇数。一般来说,三个 FOLLOWER 就足够了。观察者角色的前端节点可以是任意数量。

3.2.7、添加 BE 节点

要向集群添加 Backend 节点,请对每个 Backend 执行以下步骤:

  1. 配置 be.conf
  • be.conf 文件中,需要配置以下关键参数:
    • deploy_mode
      • 描述:指定 doris 启动模式
      • 格式:cloud 表示存算分离模式,其它存算一体模式
      • 示例:cloud
    • file_cache_path
      • 描述:用于文件缓存的磁盘路径和其他参数,以数组形式表示,每个磁盘一项。path 指定磁盘路径,total_size 限制缓存的大小;-1 或 0 将使用整个磁盘空间。
      • 格式: [{"path":"/path/to/file_cache","total_size":21474836480},{"path":"/path/to/file_cache2","total_size":21474836480}]
      • 示例: [{"path":"/path/to/file_cache","total_size":21474836480},{"path":"/path/to/file_cache2","total_size":21474836480}]
      • 默认: [{"path":"${DORIS_HOME}/file_cache"}]
  1. 启动 BE 进程

    使用以下命令启动 Backend:

shell 复制代码
    bin/start_be.sh --daemon
  1. 将 BE 添加到集群

使用 MySQL 客户端连接到任意 FE 节点:

shell 复制代码
ALTER SYSTEM ADD BACKEND "<ip>:<heartbeat_service_port>" [PROTERTIES propertires];

将 替换为新 Backend 的 IP 地址,将 <heartbeat_service_port> 替换为其配置的心跳服务端口(默认为 9050)。

可以通过 PROPERTIES 设置 BE 所在的 计算组。

更详细的用法请参考 ADD BACKENDREMOVE BACKEND

  1. 验证 BE 状态

检查 Backend 日志文件(be.log)以确保它已成功启动并加入集群。

您还可以使用以下 SQL 命令检查 Backend 状态:

shell 复制代码
SHOW BACKENDS;

这将显示集群中所有 Backend 及其当前状态。

3.2.8、添加 Storage Vault

Storage Vault 是 Doris 存算分离架构中的重要组件。它们代表了存储数据的共享存储层。您可以使用 HDFS 或兼容 S3 的对象存储创建一个或多个 Storage Vault。可以将一个 Storage Vault 设置为默认 Storage Vault,系统表和未指定 Storage Vault 的表都将存储在这个默认 Storage Vault 中。默认 Storage Vault 不能被删除。以下是为您的 Doris 集群创建 Storage Vault 的方法:

  1. 创建 HDFS Storage Vault

要使用 SQL 创建 Storage Vault,请使用 MySQL 客户端连接到您的 Doris 集群

shell 复制代码
CREATE STORAGE VAULT IF_NOT_EXISTS hdfs_vault
    PROPERTIES (
    "type"="hdfs",
    "fs.defaultFS"="hdfs://127.0.0.1:8020"
);
  1. 创建 S3 Storage Vault

要使用兼容 S3 的对象存储创建 Storage Vault,请按照以下步骤操作:

  • 使用 MySQL 客户端连接到您的 Doris 集群。
  • 执行以下 SQL 命令来创建 S3 Storage Vault:
shell 复制代码
CREATE STORAGE VAULT IF_NOT_EXISTS s3_vault
    PROPERTIES (
    "type"="S3",
    "s3.endpoint"="s3.us-east-1.amazonaws.com",
    "s3.access_key" = "ak",
    "s3.secret_key" = "sk",
    "s3.region" = "us-east-1",
    "s3.root.path" = "ssb_sf1_p2_s3",
    "s3.bucket" = "doris-build-1308700295",
    "provider" = "S3"
);

要在其他对象存储上创建 Storage Vault,请参考 创建 Storage Vault

  1. 设置默认 Storage Vault

使用如下 SQL 语句设置一个默认 Storage Vault。

sql 复制代码
SET <storage_vault_name> AS DEFAULT STORAGE VAULT

3.2.9、注意事项

  • 仅元数据操作功能的 Meta Service 进程应作为 FE 和 BE 的 meta_service_endpoint配置目标。
  • 数据回收功能进程不应作为meta_service_endpoint配置目标。

3.3、Doris 内置的 Web UI

Doris FE 内置 Web UI。用户无须安装 MySQL 客户端,即可通过内置的 Web UI 进行 SQL 查询和其它相关信息的查看。

在浏览器中输入 http://fe_ip:fe_port, 比如 http://172.20.63.118:8030,打开 Doris 内置的 Web 控制台。

内置 Web 控制台,主要供集群 root 账户使用,默认安装后 root 账户密码为空。

比如,在 Playground 中,执行如下语句,可以完成对 BE 节点的添加。

sql 复制代码
ALTER SYSTEM ADD BACKEND "be_host_ip:heartbeat_service_port";

警告

  • Playground 中执行这种和具体数据库/表没有关系的语句,务必在左侧库栏里随意选择一个数据库,才能执行成功,这个限制,稍后会去掉。

  • 当前内置的 Web 控制台,还不能执行 SET 类型的 SQL 语句,所以,在 Web 控制台,当前还不能通过执行 SET PASSWORD FOR 'user' = PASSWORD('user_password') `类似语句。

四、数据表设计

  • 创建表

使用 CREATE TABLE 语句在 Doris 中创建一个表,也可以使用 CREATE TABKE LIKE 或 CREATE TABLE AS 子句从另一个表派生表定义。

  • 表属性

    Doris 的建表语句中可以指定建表属性,包括:

    • 分桶数 (buckets):决定数据在表中的分布;

    • 存储介质 (storage_medium):控制数据的存储方式,如使用 HDD、SSD 或远程共享存储;

    • 副本数 (replication_num):控制数据副本的数量,以保证数据的冗余和可靠性;

    • 冷热分离存储策略 (storage_policy) :控制数据的冷热分离存储的迁移策略;

这些属性作用于分区,即分区创建之后,分区就会有自己的属性,修改表属性只对未来创建的分区生效,对已经创建好的分区不生效,关于属性更多的信息请参考修改表属性。

  • 注意事项

    • 选择合适的数据模型:数据模型不可更改,建表时需要选择一个合适的数据模型;

    • 选择合适的分桶数:已经创建的分区不能修改分桶数,可以通过替换分区来修改分桶数,可以修改动态分区未创建的分区分桶数;

    • 添加列操作:加减 VALUE 列是轻量级实现,秒级别可以完成,加减 KEY 列或者修改数据类型是重量级操作,完成时间取决于数据量,大规模数据下尽量避免加减 KEY 列或者修改数据类型;

    • 优化存储策略:可以使用层级存储将冷数据保存到 HDD 或者 S3 / HDFS。

创建示例:

sql 复制代码
-- ======================================================================
-- 数据库创建
-- ======================================================================
CREATE DATABASE IF NOT EXISTS ecommerce_analysis 
COMMENT "电商用户行为分析数据库";

USE ecommerce_analysis;

-- ======================================================================
-- 存储资源创建(需提前配置)
-- ======================================================================
-- 创建SSD资源(热数据)
CREATE RESOURCE IF NOT EXISTS SSD_RESOURCE PROPERTIES (
  "type" = "local",              -- 本地存储
  "medium" = "ssd",              -- SSD介质
  "path" = "/path/to/ssd/"       -- SSD存储路径
) COMMENT "热数据存储资源";

-- HDD资源(冷数据)
CREATE RESOURCE IF NOT EXISTS HDD_RESOURCE PROPERTIES (
  "type" = "local",
  "medium" = "hdd",
  "path" = "/data/doris/hdd/"  -- 替换为实际HDD路径
);

-- ======================================================================
-- 用户行为明细表(完整Doris 3.0建表语句)
-- ======================================================================
CREATE TABLE IF NOT EXISTS user_events (
    -- ================== 维度列(用于过滤、分组)==================
    event_time    DATETIME    NOT NULL COMMENT "事件精确时间(毫秒级)", 
    event_date    DATE        NOT NULL COMMENT "事件日期(分区键)",  
    user_id       BIGINT      NOT NULL COMMENT "用户ID(分桶键)",
    device_type   VARCHAR(20)          COMMENT "设备类型(手机/PC/平板)",
    province      VARCHAR(50)          COMMENT "用户所在省份",
    city          VARCHAR(50)          COMMENT "用户所在城市",
    
    -- ================== 指标列(用于聚合计算)==================
    page_view     BIGINT SUM  DEFAULT "0" COMMENT "页面浏览次数",
    cart_add      BIGINT SUM  DEFAULT "0" COMMENT "加购操作次数",
    order_count   BIGINT SUM  DEFAULT "0" COMMENT "下单次数",
    order_amount  DOUBLE SUM  DEFAULT "0" COMMENT "订单总金额(元)",
    bounce_rate   DOUBLE SUM  DEFAULT "0" COMMENT "跳出率",
    
    -- ================== 主键/唯一标识列 ==================
    event_id      VARCHAR(36)          COMMENT "事件唯一ID(UUID格式)",
    
    -- ================== 其他辅助列 ==================
    session_id    VARCHAR(64)          COMMENT "会话ID",
    referrer      VARCHAR(256)         COMMENT "来源页面URL",
    user_agent    VARCHAR(256)         COMMENT "用户代理信息"
)
-- ================== 存储引擎配置 ==================
ENGINE = OLAP  -- OLAP引擎(必须)

-- ================== 数据模型(三选一)==================
-- 选项1:聚合模型(适合预聚合报表)
AGGREGATE KEY(event_date, event_time, user_id, device_type, province, city, event_id)

-- 选项2:主键模型(支持实时更新,适合需要行级更新的场景)
-- UNIQUE KEY(event_id, event_time)  -- 主键(唯一标识行)
-- COMMENT "主键模型:支持行级更新,适用于需要精确更新的场景"

-- 选项3:明细模型(存储原始日志,无聚合)
-- DUPLICATE KEY(event_time, user_id, event_date)  -- 排序键
-- COMMENT "明细模型:存储原始事件日志,适用于全量数据分析"

-- ================== 分区配置 ==================
PARTITION BY RANGE(event_date) (
    -- 初始分区(动态分区会自动创建后续分区)
    PARTITION p_20240101 VALUES [('2024-01-01'), ('2024-01-02')),
    PARTITION p_20240102 VALUES [('2024-01-02'), ('2024-01-03'))
)
COMMENT "按天分区,自动管理生命周期"

-- ================== 分桶配置 ==================
DISTRIBUTED BY HASH(user_id) BUCKETS 32 
COMMENT "按用户ID哈希分桶,32个分桶"

-- ================== 冷热分层存储策略 ==================
PROPERTIES (
    -- ***** 冷热分层核心配置 ****
    "storage_policy" = "hot_cold_policy",       -- 存储策略名称
    "storage_policy.hot.storage_resource" = "SSD_RESOURCE",  -- 热数据存储资源
    "storage_policy.cold.storage_resource" = "HDD_RESOURCE",  -- 冷数据存储资源
    -- 下面的动态分区配置优先级比较高
    "storage_policy.hot.cooldown_ttl" = "7 days",   -- 热数据保留7天
    "storage_policy.cold.cooldown_ttl" = "365 days",-- 冷数据保留1年
    
    -- ***** 动态分区配置(自动管理分区)*****
    "dynamic_partition.enable" = "true",          -- 启用动态分区
    "dynamic_partition.time_unit" = "DAY",        -- 按天分区
    "dynamic_partition.start" = "-30",            -- 保留最近30天分区
    "dynamic_partition.end" = "7",                -- 预创建未来7天分区
    "dynamic_partition.prefix" = "p_",            -- 分区名前缀
    "dynamic_partition.buckets" = "32",           -- 每个分区分桶数
    "dynamic_partition.hot_partition_num" = "7",  -- 关键参数:最近7天为热分区(存储在SSD)
    
    -- ***** 数据副本与高可用 *****
    "replication_num" = "3",       -- 每个分桶3副本(生产环境必须≥3)
    "replication_allocation" = "tag.location.default: 3",  -- 副本分配策略
    
    -- ***** 存储优化参数 *****
    "compression" = "ZSTD",        -- 列存压缩算法(推荐ZSTD)
    "light_schema_change" = "true",-- 3.0新增:快速schema变更
    "enable_unique_key_merge_on_write" = "true", -- 主键模型写时合并
    
    -- ***** 性能优化参数 *****
    "disable_auto_compaction" = "false", -- 启用自动compaction
    "storage_format" = "V2",       -- 存储格式版本(V2性能更好)
    "bloom_filter_columns" = "user_id,event_id", -- 布隆过滤器加速查询
    
    -- ***** 其他配置 *****
    "in_memory" = "false",         -- 全表不在内存(热数据自动缓存)
    "tablet_type" = "COLUMN",      -- 列式存储(默认)
    "dynamic_schema" = "false"     -- 禁用动态schema
)
COMMENT "电商用户行为明细表(Doris 3.0冷热分层存储)";

4.1、数据模型

在 Doris 中支持三种表模型:

  1. 明细模型(Duplicate Key Model):允许指定的 Key 列重复,Doirs 存储层保留所有写入的数据,适用于必须保留所有原始数据记录的情况;

  2. 主键模型(Unique Key Model):每一行的 Key 值唯一,可确保给定的 Key 列不会存在重复行,Doris 存储层对每个 key 只保留最新写入的数据,适用于数据更新的情况;

  3. 聚合模型(Aggregate Key Model):可根据 Key 列聚合数据,Doris 存储层保留聚合后的数据,从而可以减少存储空间和提升查询性能;通常用于需要汇总或聚合信息(如总数或平均值)的情况。

在建表后,表模型的属性已经确认,无法修改。针对业务选择合适的模型至关重要:

  1. Duplicate Key:适合任意维度的 Ad-hoc 查询。虽然同样无法利用预聚合的特性,但是不受聚合模型的约束,可以发挥列存模型的优势(只读取相关列,而不需要读取所有 Key 列)。

  2. Unique Key:针对需要唯一主键约束的场景,可以保证主键唯一性约束。但是无法利用 ROLLUP 等预聚合带来的查询优势。

  3. Aggregate Key:可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,非常适合有固定模式的报表类查询场景。但是该模型对 count(*) 查询很不友好。同时因为固定了 Value 列上的聚合方式,在进行其他类型的聚合查询时,需要考虑语意正确性。

表模型能力对比

能力对比项 明细模型 主键模型 聚合模型
Key 列唯一约束 不支持(Key 列可重复) 支持 支持
同步物化视图 支持 支持 支持
异步物化视图 支持 支持 支持
UPDATE 语句 不支持 支持 不支持
DELETE 语句 部分支持 支持 不支持
导入时整行更新 不支持 支持 不支持
导入时部分列更新 不支持 支持 部分支持

4.1.1、明细模型

明细模型是 Doris 中的默认建表模型,用于保存每条原始数据记录。在建表时,通过 DUPLICATE KEY 指定数据存储的排序列,以优化常用查询。一般建议选择三列或更少的列作为排序键

  • 保留原始数据:明细模型保留了全量的原始数据,适合于存储与查询原始数据。对于需要进行详细数据分析的应用场景,建议使用明细模型,以避免数据丢失的风险;

  • 不去重也不聚合:与聚合模型与主键模型不同,明细模型不会对数据进行去重与聚合操作。即使两条相同的数据,每次插入时也会被完整保留;

  • 灵活的数据查询:明细模型保留了全量的原始数据,可以从完整数据中提取细节,基于全量数据做任意维度的聚合操作,从而进行元数数据的审计及细粒度的分析。

使用场景

一般明细模型中的数据只进行追加,旧数据不会更新。明细模型适用于需要存储全量原始数据的场景:

  • 日志存储:用于存储各类的程序操作日志,如访问日志、错误日志等。每一条数据都需要被详细记录,方便后续的审计与分析;

  • 用户行为数据:在分析用户行为时,如点击数据、用户访问轨迹等,需要保留用户的详细行为,方便后续构建用户画像及对行为路径进行详细分析;

  • 交易数据:在某些存储交易行为或订单数据时,交易结束时一般不会发生数据变更。明细模型适合保留这一类交易信息,不遗漏任意一笔记录,方便对交易进行精确的对账。

建表说明

在建表时,可以通过 DUPLICATE KEY 关键字指定明细模型。明细表必须指定数据的 Key 列,用于在存储时对数据进行排序 。下例的明细表中存储了日志信息,并针对于 log_timelog_typeerror_code 三列进行了排序:

sql 复制代码
CREATE TABLE IF NOT EXISTS example_tbl_duplicate
(
    log_time        DATETIME       NOT NULL,
    log_type        INT            NOT NULL,
    error_code      INT,
    error_msg       VARCHAR(1024),
    op_id           BIGINT,
    op_time         DATETIME
)
DUPLICATE KEY(log_time, log_type, error_code)
DISTRIBUTED BY HASH(log_type) BUCKETS 10;

数据插入与存储

4.1.2、主键模型

当需要更新数据时,可以选择主键模型(Unique Key Model)。该模型保证 Key 列的唯一性,插入或更新数据时,新数据会覆盖具有相同 Key 的旧数据,确保数据记录为最新。与其他数据模型相比,主键模型适用于数据的更新场景,在插入过程中进行主键级别的更新覆盖。

主键模型有以下特点:

  • 基于主键进行 UPSERT:在插入数据时,主键重复的数据会更新,主键不存在的记录会插入;

  • 基于主键进行去重:主键模型中的 Key 列具有唯一性,会对根据主键列对数据进行去重操作;

  • 高频数据更新:支持高频数据更新场景,同时平衡数据更新性能与查询性能。

使用场景

  • 高频数据更新:适用于上游 OLTP 数据库中的维度表,实时同步更新记录,并高效执行 UPSERT 操作;

  • 数据高效去重:如广告投放和客户关系管理系统中,使用主键模型可以基于用户 ID 高效去重;

  • 需要部分列更新:如画像标签场景需要变更频繁改动的动态标签,消费订单场景需要改变交易的状态。通过主键模型部分列更新能力可以完成某几列的变更操作。

实现方式

在 Doris 中主键模型有两种实现方式:

  • 写时合并(merge-on-write) :自 1.2 版本起,Doris 默认使用写时合并模式,数据在写入时立即合并相同 Key 的记录,确保存储的始终是最新数据。写时合并兼顾查询和写入性能,避免多个版本的数据合并,并支持谓词下推到存储层。大多数场景推荐使用此模式

在建表时,使用 UNIQUE KEY 关键字可以指定主键表。通过显示开启 enable_unique_key_merge_on_write 属性可以指定写时合并模式。自 Doris 2.1 版本以后,默认开启写时合并

sql 复制代码
CREATE TABLE IF NOT EXISTS example_tbl_unique
(
    user_id         LARGEINT        NOT NULL,
    user_name       VARCHAR(50)     NOT NULL,
    city            VARCHAR(20),
    age             SMALLINT,
    sex             TINYINT
)
UNIQUE KEY(user_id, user_name)
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
    "enable_unique_key_merge_on_write" = "true"
);
  • 读时合并(merge-on-read):在 1.2 版本前,Doris 中的主键模型默认使用读时合并模式,数据在写入时并不进行合并,以增量的方式被追加存储,在 Doris 内保留多个版本。查询或 Compaction 时,会对数据进行相同 Key 的版本合并。读时合并适合写多读少的场景,在查询是需要进行多个版本合并,谓词无法下推,可能会影响到查询速度。

在建表时,使用 UNIQUE KEY 关键字可以指定主键表。通过显示关闭 enable_unique_key_merge_on_write 属性可以指定读时合并模式。在 Doris 2.1 版本之前,默认开启读时合并

sql 复制代码
CREATE TABLE IF NOT EXISTS example_tbl_unique
(
    user_id         LARGEINT        NOT NULL,
    username        VARCHAR(50)     NOT NULL,
    city            VARCHAR(20),
    age             SMALLINT,
    sex             TINYINT
)
UNIQUE KEY(user_id, username)
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
    "enable_unique_key_merge_on_write" = "false"
);

在 Doris 中基于主键模型更新有两种语义:

  • 整行更新 :Unique Key 模型默认的更新语义为整行UPSERT,即 UPDATE OR INSERT,该行数据的 Key 如果存在,则进行更新,如果不存在,则进行新数据插入。在整行 UPSERT 语义下,即使用户使用 Insert Into 指定部分列进行写入,Doris 也会在 Planner 中将未提供的列使用 NULL 值或者默认值进行填充。

  • 部分列更新 :如果用户希望更新部分字段,需要使用写时合并实现,并通过特定的参数来开启部分列更新的支持。

数据插入与存储

在主键表中,Key 列不仅用于排序,还用于去重,插入数据时,相同 Key 的记录会被覆盖。

4.1.3、聚合模型

Doris 的聚合模型专为高效处理大规模数据查询中的聚合操作设计。它通过预聚合数据,减少重复计算,提升查询性能。聚合模型只存储聚合后的数据,节省存储空间并加速查询

使用场景

  • 明细数据进行汇总:用于电商平台的月销售业绩、金融风控的客户交易总额、广告投放的点击量等业务场景中,进行多维度汇总;

  • 不需要查询原始明细数据:如驾驶舱报表、用户交易行为分析等,原始数据存储在数据湖中,仅需存储汇总后的数据。

原理

每一次数据导入会在聚合模型内形成一个版本,在 Compaction 阶段进行版本合并,在查询时会按照主键进行数据聚合:

  • 数据导入阶段:数据按批次导入,每批次生成一个版本,并对相同聚合键的数据进行初步聚合(如求和、计数);

  • 后台文件合并阶段(Compaction):多个版本文件会定期合并,减少冗余并优化存储;

  • 查询阶段:查询时,系统会聚合同一聚合键的数据,确保查询结果准确。

建表说明

使用 AGGREGATE KEY 关键字在建表时指定聚合模型,并指定 Key 列用于聚合 Value 列。注意下发sql有写了聚合方式,如REPLACE DEFAULT、SUM、MAX

sql 复制代码
CREATE TABLE IF NOT EXISTS example_tbl_agg
(
    user_id             LARGEINT    NOT NULL,
    load_dt             DATE        NOT NULL,
    city                VARCHAR(20),
    last_visit_dt       DATETIME    REPLACE DEFAULT "1970-01-01 00:00:00",
    cost                BIGINT      SUM DEFAULT "0",
    max_dwell           INT         MAX DEFAULT "0",
)
AGGREGATE KEY(user_id, load_dt, city)
DISTRIBUTED BY HASH(user_id) BUCKETS 10;

上例中定义了用户信息和访问的行为事实表,将 user_id、load_dt、city作为 Key 列进行聚合操作。数据导入时,Key 列会聚合成一行,Value 列会按照指定的聚合类型进行维度聚合。

在聚合表中支持以下类型的维度聚合:

聚合方式 描述
SUM 求和,多行的 Value 进行累加。
REPLACE 替代,下一批数据中的 Value 会替换之前导入过的行中的 Value。
MAX 保留最大值。
MIN 保留最小值。
REPLACE_IF_NOT_NULL 非空值替换。与 REPLACE 的区别在于对 null 值,不做替换。
HLL_UNION HLL 类型的列的聚合方式,通过 HyperLogLog 算法聚合。
BITMAP_UNION BITMAP 类型的列的聚合方式,进行位图的并集聚合。

数据插入与存储

在聚合表中,数据基于主键进行聚合操作。数据插入后及完成聚合操作。

4.2、数据划分

4.2.1、数据分布概念

在 Doris 中,数据分布通过合理的分区和分桶策略 ,将数据高效地映射到各个数据分片(Tablet)上,从而充分利用多节点的存储和计算能力,支持大规模数据的高效存储和查询。

1、数据分布概览

  • 数据写入

数据写入时,Doris 首先根据表的分区策略将数据行分配到对应的分区。接着,根据分桶策略将数据行进一步映射到分区内的具体分片,从而确定了数据行的存储位置。

  • 查询执行

查询运行时,Doris 的优化器会根据分区和分桶策略裁剪数据,最大化减少扫描范围。在涉及 JOIN 或聚合查询时,可能会发生跨节点的数据传输(Shuffle)。合理的分区和分桶设计可以减少 Shuffle 并充分利用 Colocate Join 优化查询性能。

2、数据分片(Tablet)

BE 节点的存储数据分片的数据,每个分片是 Doris 中数据管理的最小单元,也是数据移动和复制的基本单位。

BE存储目录结构

3、分区策略

分区是数据组织的第一层逻辑划分,用于将表中的数据划分为更小的子集。Doris 提供以下两种分区类型和三种分区模式:

  • 分区类型

    • Range 分区:根据分区列的值范围将数据行分配到对应分区。
    • List 分区:根据分区列的具体值将数据行分配到对应分区。
  • 分区模式

    • 手动分区:用户手动创建分区(如建表时指定或通过 ALTER 语句增加)。
    • 动态分区:系统根据时间调度规则自动创建分区,但写入数据时不会按需创建分区。
    • 自动分区:数据写入时,系统根据需要自动创建相应的分区,使用时注意脏数据生成过多的分区。

4、分桶策略

分桶是数据组织的第二层逻辑划分,用于在分区内将数据行进一步划分到更小的单元。Doris 支持以下两种分桶方式:

  • Hash 分桶 :通过计算分桶列值的 crc32 哈希值,并对分桶数取模,将数据行均匀分布到分片中。
  • Random 分桶 :随机分配数据行到分片中。使用 Random 分桶时,可以使用 load_to_single_tablet 优化小规模数据的快速写入。

5、数据分布目标

  1. 均匀数据分布 确保数据均匀分布在各 BE 节点上,避免数据倾斜导致部分节点过载,从而提高系统整体性能。

  2. 优化查询性能 合理的分区裁剪可以大幅减少扫描的数据量,合理的分桶数可以提升计算并行度,合理利用 Colocate 可以降低 Shuffle 成本,提升 JOIN 和聚合查询效率。

  3. 灵活数据管理

    • 按时间分区保存冷数据(HDD)与热数据(SSD)。
    • 定期删除历史分区释放存储空间。
  4. 控制元数据规模 每个分片的元数据存储在 FE 和 BE 中,因此需要合理控制分片数量。经验值建议:

    • 每 1000 万分片,FE 至少需 100G 内存。
    • 单个 BE 承载的分片数应小于 2 万。
  5. 优化写入吞吐

    • 分桶数应合理控制(建议 < 128),以避免写入性能下降。
    • 每次写入的分区数量应适量(建议每次写入少量分区)。

通过精心设计和管理分区与分桶策略,Doris 能够高效地支持大规模数据的存储与查询处理,满足各种复杂业务需求。

4.2.2、手动分区

分区列

  • 分区列可以指定一列或多列,分区列必须为 KEY 列。
  • 不论分区列是什么类型,在写分区值时,都需要加双引号。
  • 分区数量理论上没有上限。但默认限制每张表 4096 个分区,如果想突破这个限制,可以修改 FE 配置max_multi_partition_nummax_dynamic_partition_num
  • 当不使用分区建表时,系统会自动生成一个和表名同名的,全值范围的分区。该分区对用户不可见,并且不可删改。
  • 创建分区时不可添加范围重叠的分区。
4.2.2.1、Range 分区

分区列通常为时间列,以方便的管理新旧数据。Range 分区支持的列类型 DATE, DATETIME, TINYINT, SMALLINT, INT, BIGINT, LARGEINT

分区信息,支持四种写法:

  1. FIXED RANGE:定义分区的左闭右开区间。
sql 复制代码
PARTITION BY RANGE(`date`)
(
    PARTITION `p201701` VALUES [("2017-01-01"),  ("2017-02-01")),
    PARTITION `p201702` VALUES [("2017-02-01"), ("2017-03-01")),
    PARTITION `p201703` VALUES [("2017-03-01"), ("2017-04-01"))
)
  1. LESS THAN:仅定义分区上界。下界由上一个分区的上界决定。
sql 复制代码
PARTITION BY RANGE(`date`)
(
    PARTITION `p201701` VALUES LESS THAN ("2017-02-01"),
    PARTITION `p201702` VALUES LESS THAN ("2017-03-01"),
    PARTITION `p201703` VALUES LESS THAN ("2017-04-01"),
    PARTITION `p2018` VALUES [("2018-01-01"), ("2019-01-01")),
    PARTITION `other` VALUES LESS THAN (MAXVALUE)
)
  1. BATCH RANGE:批量创建数字类型和时间类型的 RANGE 分区,定义分区的左闭右开区间,设定步长。
sql 复制代码
PARTITION BY RANGE(age)
(
    FROM (1) TO (100) INTERVAL 10
)
-- 从2000年开始到2021年结束,每 2年 创建一个分区
PARTITION BY RANGE(`date`)
(
    FROM ("2000-11-14") TO ("2021-11-14") INTERVAL 2 YEAR
)
  1. MULTI RANGE:批量创建 RANGE 分区,定义分区的左闭右开区间。
sql 复制代码
PARTITION BY RANGE(col)                                                                                                                                                                                                                
(                                                                                                                                                                                                                                      
   FROM ("2000-11-14") TO ("2021-11-14") INTERVAL 1 YEAR,                                                                                                                                                                              
   FROM ("2021-11-14") TO ("2022-11-14") INTERVAL 1 MONTH,                                                                                                                                                                             
   FROM ("2022-11-14") TO ("2023-01-03") INTERVAL 1 WEEK,                                                                                                                                                                              
   FROM ("2023-01-03") TO ("2023-01-14") INTERVAL 1 DAY,
   PARTITION p_20230114 VALUES [('2023-01-14'), ('2023-01-15'))                                                                                                                                                                                
)                                                                                                                                                                                                                                      
4.2.2.2、List 分区

分区列支持 BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR 数据类型,分区值为枚举值。只有当数据为目标分区枚举值其中之一时,才可以命中分区。

Partition 支持通过 VALUES IN (...) 来指定每个分区包含的枚举值。

sql 复制代码
PARTITION BY LIST(city)
(
    PARTITION `p_cn` VALUES IN ("Beijing", "Shanghai", "Hong Kong"),
    PARTITION `p_usa` VALUES IN ("New York", "San Francisco"),
    PARTITION `p_jp` VALUES IN ("Tokyo")
)

List 分区也支持多列分区,示例如下:

sql 复制代码
PARTITION BY LIST(id, city)
(
    PARTITION p1_city VALUES IN (("1", "Beijing"), ("1", "Shanghai")),
    PARTITION p2_city VALUES IN (("2", "Beijing"), ("2", "Shanghai")),
    PARTITION p3_city VALUES IN (("3", "Beijing"), ("3", "Shanghai"))
)
4.2.2.3、NULL 分区

PARTITION 列默认必须为 NOT NULL 列,如果需要使用 NULL 列,应设置 session variable allow_partition_column_nullable = true。对于 LIST PARTITION,我们支持真正的 NULL 分区。对于 RANGE PARTITION,NULL 值会被划归最小的 LESS THAN 分区, 没有 LESS THAN 分区时,无法插入。

4.2.3、动态分区

动态分区会按照设定的规则,滚动添加、删除分区,从而实现对表分区的生命周期管理(TTL),减少数据存储压力。在日志管理,时序数据管理等场景,通常可以使用动态分区能力滚动删除过期的数据。

下图中展示了使用动态分区进行生命周期管理,其中指定了以下规则:

  • 动态分区调度单位dynamic_partition.time_unit为 DAY,按天组织分区;

  • 动态分区起始偏移量dynamic_partition.start设置为 -1,保留一天前分区;

  • 动态分区结束偏移量dynamic_partition.end设置为 2,保留未来两天分区

依据以上规则,随着时间推移,总会保留 4 个分区,即过去一天分区,当天分区与未来两天分区:

4.2.3.1、使用限制

在使用动态分区时,需要遵守以下规则:

  • 动态分区与跨集群复制(CCR)同时使用时会失效;
  • 动态分区只支持在 DATE/DATETIME 列上进行 Range 类型的分区;
  • 动态分区只支持单一分区键。
4.2.3.2、创建动态分区

在建表时,通过指定dynamic_partition属性,可以创建动态分区表。

shell 复制代码
CREATE TABLE test_dynamic_partition(
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
DUPLICATE KEY(order_id)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(order_id) BUCKETS 10
PROPERTIES(
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-1",
    "dynamic_partition.end" = "2",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true"
);
4.2.3.3、动态分区参数说明

动态分区的规则参数以 dynamic_partition 为前缀,可以设置以下规则参数:

参数 必选 说明
dynamic_partition.enable 是否开启动态分区特性。可指定为 TRUEFALSE。如果指定了动态分区其他必填参数,默认为 TRUE
dynamic_partition.time_unit 动态分区调度的单位。可选值:HOURDAYWEEKMONTHYEAR。分别表示按小时、按天、按星期、按月、按年进行分区创建或删除。
dynamic_partition.start 动态分区的起始偏移,为负数。默认值为 -2147483648,即不删除历史分区。根据 time_unit 的不同,以当天(星期/月)为基准,分区范围在此偏移之前的分区将被删除。
dynamic_partition.end 动态分区的结束偏移,为正数。根据 time_unit 的不同,以当天(星期/月)为基准,提前创建对应范围的分区。
dynamic_partition.prefix 动态创建的分区名前缀。
dynamic_partition.buckets 动态创建的分区所对应的分桶数。设置后会覆盖 DISTRIBUTED 中指定的分桶数。
dynamic_partition.replication_num 动态创建的分区所对应的副本数量。如果不填写,默认为该表创建时指定的副本数量。
dynamic_partition.create_history_partition 默认为 false。当置为 true 时,系统会自动创建所有分区。max_dynamic_partition_num 参数会限制总分区数量。
dynamic_partition.history_partition_num create_history_partitiontrue 时,指定创建历史分区数量。默认值为 -1(未设置)。与 start 作用相同,建议只设置一个。
dynamic_partition.start_day_of_week time_unitWEEK 时,指定每周的起始点。取值范围 171 表示周一,7 表示周日)。默认 1
dynamic_partition.start_day_of_month time_unitMONTH 时,指定每月的起始日期。取值范围 1281 表示每月 1 号)。默认 1。不支持 29、30、31 日。
dynamic_partition.reserved_history_periods 需要保留的历史分区时间范围。格式: - DAY/WEEK/MONTH/YEAR[yyyy-MM-dd,yyyy-MM-dd],[...,...] - HOUR[yyyy-MM-dd HH:mm:ss,yyyy-MM-dd HH:mm:ss],[...,...] 默认 NULL
dynamic_partition.time_zone 动态分区时区,默认为服务器系统时区(如 Asia/Shanghai)。更多时区可参考时区管理。

FE 配置参数

可以在 FE 配置文件或通过 ADMIN SET FRONTEND CONFIG 命令修改 FE 中的动态分区参数配置:

参数 默认值 说明
dynamic_partition_enable false 是否开启 Doris 的动态分区功能。该参数只影响动态分区表的分区操作,不影响普通表。
dynamic_partition_check_interval_seconds 600 动态分区线程的执行频率,单位为秒。
max_dynamic_partition_num 500 用于限制创建动态分区表时可以创建的最大分区数,避免一次创建过多分区。
4.2.3.4、管理动态分区
  1. 修改动态分区属性

提示:在使用 ALTER TABLE 语句修改动态分区时,不会立即生效。会以 dynamic_partition_check_interval_seconds 参数指定的时间间隔轮训检查 dynamic partition 分区,完成需要的分区创建与删除操作。

下例中通过 ALTER TABLE 语句,将非动态分区表修改为动态分区:

sql 复制代码
CREATE TABLE test_dynamic_partition(
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY HASH(order_id) BUCKETS 10;

ALTER TABLE test_partition SET (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-1",
    "dynamic_partition.end" = "2",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.create_history_partition" = "true"
);
  1. 查看动态分区调度情况

通过 SHOW DYNAMIC PARTITION TABLES [ FROM <db_name> ] 可以查看当前数据库下,所有动态分区表的调度情况:

可选参数:<db_name>指定展示动态分区表状态的 DB 名称,如果不指定,则默认展示当前 DB 下的所有动态分区表状态。

sql 复制代码
SHOW DYNAMIC PARTITION TABLES;
+-----------+--------+----------+-------------+------+--------+---------+-----------+----------------+---------------------+--------+------------------------+----------------------+-------------------------+
| TableName | Enable | TimeUnit | Start       | End  | Prefix | Buckets | StartOf   | LastUpdateTime | LastSchedulerTime   | State  | LastCreatePartitionMsg | LastDropPartitionMsg | ReservedHistoryPeriods  |
+-----------+--------+----------+-------------+------+--------+---------+-----------+----------------+---------------------+--------+------------------------+----------------------+-------------------------+
| d3        | true   | WEEK     | -3          | 3    | p      | 1       | MONDAY    | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | [2021-12-01,2021-12-31] |
| d5        | true   | DAY      | -7          | 3    | p      | 32      | N/A       | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
| d4        | true   | WEEK     | -3          | 3    | p      | 1       | WEDNESDAY | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    | 
| d6        | true   | MONTH    | -2147483648 | 2    | p      | 8       | 3rd       | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
| d2        | true   | DAY      | -3          | 3    | p      | 32      | N/A       | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
| d7        | true   | MONTH    | -2147483648 | 5    | p      | 8       | 24th      | N/A            | 2020-05-25 14:29:24 | NORMAL | N/A                    | N/A                  | NULL                    |
+-----------+--------+----------+-------------+------+--------+---------+-----------+----------------+---------------------+--------+------------------------+----------------------+-------------------------+
7 rows in set (0.02 sec)

返回值:

列名 类型 说明
TableName varchar 当前 DB 或指定 DB 的表名称
Enable varchar 是否开启了表的动态分区属性
TimeUnit varchar 动态分区表的分区粒度,有 HOUR,DAY,WEEK,MONTH,YEAR
Start varchar 动态分区的起始偏移,为负数。默认值为 -2147483648,即不删除历史分区。根据 time_unit 属性的不同,以当天(星期/月)为基准,分区范围在此偏移之前的分区将会被删除。
End varchar 动态分区的结束偏移,为正数。根据 time_unit 属性的不同,以当天(星期/月)为基准,提前创建对应范围的分区。
Prefix varchar 动态创建的分区名前缀。
Buckets varchar 动态创建的分区所对应的分桶数量。
ReplicationNum varchar 动态创建的分区所对应的副本数量,如果不填写,则默认为该表创建时指定的副本数量。
ReplicaAllocation varchar 动态创建的分区所对应的副本分布策略,如果不填写,则默认为该表创建时指定的副本分布策略。
StartOf varchar 动态分区每个分区粒度的起始点。当 time_unit 为 WEEK 时,该字段表示每周的起始点,取值为 MONDAY 到 SUNDAY;当 time_unit 为 MONTH 时,表示每月的起始日期,取值为 1rd 至 28rd;当 time_unit 为 MONTH 时,该值默认为 NULL。
LastUpdateTime varchar 动态分区的上一次更新时间,默认为 NULL。
LastSchedulerTime datetime 动态分区的上一次调度时间。
State varchar 动态分区的创建状态。
LastCreatePartitionMsg varchar 最后一次执行动态添加分区调度的错误信息。
LastDropPartitionMsg varchar 最后一次执行动态删除分区调度的错误信息。
ReservedHistoryPeriods varchar 动态分区保留的历史分区的分区区间,它表示在动态分区表中,哪些历史分区应该被保留,而不是被自动删除。
  1. 历史分区管理

在使用 startend 属性指定动态分区数量时,为了避免一次性创建所有的分区造成等待时间过长,不会创建历史分区,只会创建当前时间以后得分区。如果需要一次性创建所有分区,需要开启 create_history_partition 参数。

例如当前日期为 2024-10-11,指定 start = -2,end = 2:

  • 如果指定了 create_history_partition = true,立即创建所有分区,即 [10-09, 10-13] 五个分区;

  • 如果指定了 create_history_partition = false,只创建包含 10-11 以后的分区,即 [10-11, 10-13] 三个分区。

4.2.3.5、动态分区最佳实践
  1. 按天分区,只保留过去 7 天的及当天分区,并且预先创建未来 3 天的分区。
sql 复制代码
CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "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"
);
  1. 按月分区,不删除历史分区,并且预先创建未来 2 个月的分区。同时设定以每月 3 号为起始日。
sql 复制代码
CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "MONTH",
    "dynamic_partition.end" = "2",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "8",
    "dynamic_partition.start_day_of_month" = "3"
);
  1. 按天分区,保留过去 10 天及未来 10 天分区,并且保留 [2020-06-01,2020-06-20] 及 [2020-10-31,2020-11-15] 期间的历史数据。
sql 复制代码
CREATE TABLE tbl1 (
    order_id    BIGINT,
    create_dt   DATE,
    username    VARCHAR(20)
)
PARTITION BY RANGE(create_dt) ()
DISTRIBUTED BY HASH(create_dt)
PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.start" = "-10",
    "dynamic_partition.end" = "10",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.buckets" = "8",
    "dynamic_partition.reserved_history_periods"="[2020-06-01,2020-06-20],[2020-10-31,2020-11-15]"
);

4.2.4、自动分区

4.2.4.1、使用场景

自动分区功能主要解决了用户预期基于某列对表进行分区操作,但该列的数据分布比较零散或者难以预测,在建表或调整表结构时难以准确创建所需分区,或者分区数量过多以至于手动创建过于繁琐的问题。

以时间类型分区列为例,在动态分区功能中,我们支持了按特定时间周期自动创建新分区以容纳实时数据。对于实时的用户行为日志等场景该功能基本能够满足需求。但在一些更复杂的场景下,例如处理非实时数据时,分区列与当前系统时间无关,且包含大量离散值。此时为提高效率我们希望依据此列对数据进行分区,但数据实际可能涉及的分区无法预先掌握,或者预期所需分区数量过大。这种情况下动态分区或者手动创建分区无法满足我们的需求,自动分区功能很好地覆盖了此类需求。

假设我们的表 DDL 如下:

sql 复制代码
CREATE TABLE `DAILY_TRADE_VALUE`
(
    `TRADE_DATE`              datev2 NOT NULL COMMENT '交易日期',
    `TRADE_ID`                varchar(40) NOT NULL COMMENT '交易编号',
    ......
)
UNIQUE KEY(`TRADE_DATE`, `TRADE_ID`)
PARTITION BY RANGE(`TRADE_DATE`)
(
    PARTITION p_2000 VALUES [('2000-01-01'), ('2001-01-01')),
    PARTITION p_2001 VALUES [('2001-01-01'), ('2002-01-01')),
    PARTITION p_2002 VALUES [('2002-01-01'), ('2003-01-01')),
    PARTITION p_2003 VALUES [('2003-01-01'), ('2004-01-01')),
    PARTITION p_2004 VALUES [('2004-01-01'), ('2005-01-01')),
    PARTITION p_2005 VALUES [('2005-01-01'), ('2006-01-01')),
    PARTITION p_2006 VALUES [('2006-01-01'), ('2007-01-01')),
    PARTITION p_2007 VALUES [('2007-01-01'), ('2008-01-01')),
    PARTITION p_2008 VALUES [('2008-01-01'), ('2009-01-01')),
    PARTITION p_2009 VALUES [('2009-01-01'), ('2010-01-01')),
    PARTITION p_2010 VALUES [('2010-01-01'), ('2011-01-01')),
    PARTITION p_2011 VALUES [('2011-01-01'), ('2012-01-01')),
    PARTITION p_2012 VALUES [('2012-01-01'), ('2013-01-01')),
    PARTITION p_2013 VALUES [('2013-01-01'), ('2014-01-01')),
    PARTITION p_2014 VALUES [('2014-01-01'), ('2015-01-01')),
    PARTITION p_2015 VALUES [('2015-01-01'), ('2016-01-01')),
    PARTITION p_2016 VALUES [('2016-01-01'), ('2017-01-01')),
    PARTITION p_2017 VALUES [('2017-01-01'), ('2018-01-01')),
    PARTITION p_2018 VALUES [('2018-01-01'), ('2019-01-01')),
    PARTITION p_2019 VALUES [('2019-01-01'), ('2020-01-01')),
    PARTITION p_2020 VALUES [('2020-01-01'), ('2021-01-01')),
    PARTITION p_2021 VALUES [('2021-01-01'), ('2022-01-01'))
)
DISTRIBUTED BY HASH(`TRADE_DATE`) BUCKETS 10
PROPERTIES (
  "replication_num" = "1"
);

该表内存储了大量业务历史数据,依据交易发生的日期进行分区。可以看到在建表时,我们需要预先手动创建分区。如果分区列的数据范围发生变化,例如上表中增加了 2022 年的数据,则我们需要通过ALTER-TABLE-PARTITION对表的分区进行更改。如果这种分区需要变更,或者进行更细粒度的细分,修改起来非常繁琐。此时我们就可以使用 AUTO PARTITION 改写该表 DDL。

4.2.4.2、语法

建表时,使用以下语法填充CREATE-TABLE时的 partition_info 部分:

  1. AUTO RANGE PARTITION:
sql 复制代码
 AUTO PARTITION BY RANGE (FUNC_CALL_EXPR)
 ()

其中

sql 复制代码
 FUNC_CALL_EXPR ::= date_trunc ( <partition_column>, '<interval>' )

示例:

sql 复制代码
CREATE TABLE `date_table` (
    `TIME_STAMP` datev2 NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`TIME_STAMP`)
AUTO PARTITION BY RANGE (date_trunc(`TIME_STAMP`, 'month'))
(
)
DISTRIBUTED BY HASH(`TIME_STAMP`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
  1. AUTO LIST PARTITION:
sql 复制代码
 AUTO PARTITION BY LIST(`partition_col1`[, `partition_col2`, ...])
 ()

示例:

sql 复制代码
CREATE TABLE `str_table` (
    `str` varchar not null
) ENGINE=OLAP
DUPLICATE KEY(`str`)
AUTO PARTITION BY LIST (`str`)
(
)
DISTRIBUTED BY HASH(`str`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
  1. NULL 值分区

当开启 session variable allow_partition_column_nullable 后:

  1. 对于 AUTO LIST PARTITION,可以使用 NULLABLE 列作为分区列,会正常创建对应的 NULL 值分区:
  2. 对于 AUTO RANGE PARTITION,不支持 NULLABLE 列作为分区列。

约束:

  1. 在 AUTO LIST PARTITION 中,分区名长度不得超过 50. 该长度来自于对应数据行上各分区列内容的拼接与转义,因此实际容许长度可能更短。
  2. 在 AUTO RANGE PARTITION 中,分区函数仅支持 date_trunc,分区列仅支持 DATE 或者 DATETIME 类型;
  3. 在 AUTO LIST PARTITION 中,不支持函数调用,分区列支持 BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR 数据类型,分区值为枚举值。
  4. 在 AUTO LIST PARTITION 中,分区列的每个当前不存在对应分区的取值,都会创建一个独立的新 PARTITION。
4.2.4.3、场景示例

在使用场景一节中的示例,在使用 AUTO PARTITION 后,该表 DDL 可以改写为:

sql 复制代码
CREATE TABLE `DAILY_TRADE_VALUE`
(
    `TRADE_DATE`              datev2 NOT NULL,
    `TRADE_ID`                varchar(40) NOT NULL,
    ......
)
UNIQUE KEY(`TRADE_DATE`, `TRADE_ID`)
AUTO PARTITION BY RANGE (date_trunc(`TRADE_DATE`, 'year'))
(
)
DISTRIBUTED BY HASH(`TRADE_DATE`) BUCKETS 10
PROPERTIES (
  "replication_num" = "1"
);

以此表只有两列为例,此时新表没有默认分区:

sql 复制代码
show partitions from `DAILY_TRADE_VALUE`;
Empty set (0.12 sec)

经过插入数据后再查看,发现该表已经创建了对应的分区:

sql 复制代码
insert into `DAILY_TRADE_VALUE` values ('2012-12-13', 1), ('2008-02-03', 2), ('2014-11-11', 3);

show partitions from `DAILY_TRADE_VALUE`;
+-------------+-----------------+----------------+---------------------+--------+--------------+--------------------------------------------------------------------------------+-----------------+---------+----------------+---------------+---------------------+---------------------+--------------------------+----------+------------+-------------------------+-----------+
| PartitionId | PartitionName   | VisibleVersion | VisibleVersionTime  | State  | PartitionKey | Range                                                                          | DistributionKey | Buckets | ReplicationNum | StorageMedium | CooldownTime        | RemoteStoragePolicy | LastConsistencyCheckTime | DataSize | IsInMemory | ReplicaAllocation       | IsMutable |
+-------------+-----------------+----------------+---------------------+--------+--------------+--------------------------------------------------------------------------------+-----------------+---------+----------------+---------------+---------------------+---------------------+--------------------------+----------+------------+-------------------------+-----------+
| 180060      | p20080101000000 | 2              | 2023-09-18 21:49:29 | NORMAL | TRADE_DATE   | [types: [DATEV2]; keys: [2008-01-01]; ..types: [DATEV2]; keys: [2009-01-01]; ) | TRADE_DATE      | 10      | 1              | HDD           | 9999-12-31 23:59:59 |                     | NULL                     | 0.000    | false      | tag.location.default: 1 | true      |
| 180039      | p20120101000000 | 2              | 2023-09-18 21:49:29 | NORMAL | TRADE_DATE   | [types: [DATEV2]; keys: [2012-01-01]; ..types: [DATEV2]; keys: [2013-01-01]; ) | TRADE_DATE      | 10      | 1              | HDD           | 9999-12-31 23:59:59 |                     | NULL                     | 0.000    | false      | tag.location.default: 1 | true      |
| 180018      | p20140101000000 | 2              | 2023-09-18 21:49:29 | NORMAL | TRADE_DATE   | [types: [DATEV2]; keys: [2014-01-01]; ..types: [DATEV2]; keys: [2015-01-01]; ) | TRADE_DATE      | 10      | 1              | HDD           | 9999-12-31 23:59:59 |                     | NULL                     | 0.000    | false      | tag.location.default: 1 | true      |
+-------------+-----------------+----------------+---------------------+--------+--------------+--------------------------------------------------------------------------------+-----------------+---------+----------------+---------------+---------------------+---------------------+--------------------------+----------+------------+-------------------------+-----------+

经过自动分区功能所创建的 PARTITION,与手动创建的 PARTITION 具有完全一致的功能性质。

4.2.4.4、与动态分区联用

Doris 支持自动分区和动态分区同时使用。此时,二者的功能都生效:

  1. 自动分区将会自动在数据导入过程中按需创建分区
  2. 动态分区将会自动创建、回收、转储分区

二者语法功能不存在冲突,同时设置对应的子句/属性即可。

最佳实践

需要对分区生命周期设限的场景,可以将 Dynamic Partition 的创建功能关闭,创建分区完全交由 Auto Partition 完成,通过 Dynamic Partition 动态回收分区的功能完成分区生命周期的管理:

sql 复制代码
create table auto_dynamic(
    k0 datetime(6) NOT NULL
)
auto partition by range (date_trunc(k0, 'year'))
(
)
DISTRIBUTED BY HASH(`k0`) BUCKETS 2
properties(
    "dynamic_partition.enable" = "true",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.start" = "-50",
    "dynamic_partition.end" = "0", --- Dynamic Partition 不创建分区
    "dynamic_partition.time_unit" = "year",
    "replication_num" = "1"
);

这样我们同时具有了 Auto Partition 的灵活性,且分区名上保持了一致性。

4.2.4.5、分区管理

当启用自动分区后,分区名可以通过 auto_partition_name 函数映射到分区。partitions 表函数可以通过分区名产生详细的分区信息。仍然以 DAILY_TRADE_VALUE 表为例,在我们插入数据后,查看其当前分区:

sql 复制代码
select * from partitions("catalog"="internal","database"="optest","table"="DAILY_TRADE_VALUE") where PartitionName = auto_partition_name('range', 'year', '2008-02-03');
+-------------+-----------------+----------------+---------------------+--------+--------------+--------------------------------------------------------------------------------+-----------------+---------+----------------+---------------+---------------------+---------------------+--------------------------+-----------+------------+-------------------------+-----------+--------------------+--------------+
| PartitionId | PartitionName   | VisibleVersion | VisibleVersionTime  | State  | PartitionKey | Range                                                                          | DistributionKey | Buckets | ReplicationNum | StorageMedium | CooldownTime        | RemoteStoragePolicy | LastConsistencyCheckTime | DataSize  | IsInMemory | ReplicaAllocation       | IsMutable | SyncWithBaseTables | UnsyncTables |
+-------------+-----------------+----------------+---------------------+--------+--------------+--------------------------------------------------------------------------------+-----------------+---------+----------------+---------------+---------------------+---------------------+--------------------------+-----------+------------+-------------------------+-----------+--------------------+--------------+
|      127095 | p20080101000000 |              2 | 2024-11-14 17:29:02 | NORMAL | TRADE_DATE   | [types: [DATEV2]; keys: [2008-01-01]; ..types: [DATEV2]; keys: [2009-01-01]; ) | TRADE_DATE      |      10 |              1 | HDD           | 9999-12-31 23:59:59 |                     | \N                       | 985.000 B |          0 | tag.location.default: 1 |         1 |                  1 | \N           |
+-------------+-----------------+----------------+---------------------+--------+--------------+--------------------------------------------------------------------------------+-----------------+---------+----------------+---------------+---------------------+---------------------+--------------------------+-----------+------------+-------------------------+-----------+--------------------+--------------+

这样每个分区的 ID 和取值就可以精准地被筛选出,用于后续针对分区的具体操作(例如 insert overwrite partition)。

4.2.4.6、注意事项
  • 如同普通分区表一样,AUTO LIST PARTITION 支持多列分区,语法并无区别。
  • 在数据的插入或导入过程中如果创建了分区,而整个导入过程没有完成(失败或被取消),被创建的分区不会被自动删除。
  • 使用 AUTO PARTITION 的表,只是分区创建方式上由手动转为了自动。表及其所创建分区的原本使用方法都与非 AUTO PARTITION 的表或分区相同。
  • 为防止意外创建过多分区,我们通过FE 配置项中的max_auto_partition_num控制了一个 AUTO PARTITION 表最大容纳分区数。如有需要可以调整该值
  • 向开启了 AUTO PARTITION 的表导入数据时,Coordinator 发送数据的轮询间隔与普通表有所不同。具体请见BE 配置项中的olap_table_sink_send_interval_auto_partition_factor。开启前移(enable_memtable_on_sink_node = true)后该变量不产生影响。
  • 在使用insert-overwrite插入数据时 AUTO PARTITION 表的行为详见 INSERT OVERWRITE 文档。
  • 如果导入创建分区时,该表涉及其他元数据操作(如 Schema Change、Rebalance),则导入可能失败。

4.2.5、数据分桶

一个分区可以根据业务需求进一步划分为多个数据分桶(bucket)。每个分桶都作为一个物理数据分片(tablet)存储。合理的分桶策略可以有效降低查询时的数据扫描量,提升查询性能并增加并发处理能力。

4.2.5.1、分桶方式

Doris 支持两种分桶方式:Hash 分桶与 Random 分桶。

4.2.5.1.1、Hash 分桶

在创建表或新增分区时,用户需选择一列或多列作为分桶列 ,并明确指定分桶的数量。在同一分区内,系统会根据分桶键和分桶数量进行哈希计算。哈希值相同的数据会被分配到同一个分桶中。例如,在下图中,p250102 分区根据 region 列被划分为 3 个分桶,哈希值相同的行被归入同一个分桶。

推荐在以下场景中使用 Hash 分桶:

  • 业务需求频繁基于某个字段进行过滤时,可将该字段作为分桶键,利用 Hash 分桶提高查询效率。

  • 当表中的数据分布较为均匀时,Hash 分桶同样是一种有效的选择。

以下示例展示了如何创建带有 Hash 分桶的表。

sql 复制代码
CREATE TABLE demo.hash_bucket_tbl(
    oid         BIGINT,
    dt          DATE,
    region      VARCHAR(10),
    amount      INT
)
DUPLICATE KEY(oid)
PARTITION BY RANGE(dt) (
    PARTITION p250101 VALUES LESS THAN("2025-01-01"),
    PARTITION p250102 VALUES LESS THAN("2025-01-02")
)
DISTRIBUTED BY HASH(region) BUCKETS 8;

示例中,通过 DISTRIBUTED BY HASH(region) 指定了创建 Hash 分桶,并选择 region 列作为分桶键。同时,通过 BUCKETS 8 指定了创建 8 个分桶。

4.2.5.1.2、Random 分桶

在每个分区中,使用 Random 分桶会随机地将数据分散到各个分桶中,不依赖于某个字段的 Hash 值进行数据划分。Random 分桶能够确保数据均匀分散,从而避免由于分桶键选择不当而引发的数据倾斜问题。

在导入数据时,单次导入作业的每个批次会被随机写入到一个 tablet 中,以此保证数据的均匀分布。例如,在一次操作中,8 个批次的数据被随机分配到 p250102 分区下的 3 个分桶中。

在使用 Random 分桶时,可以启用单分片导入模式(通过设置 load_to_single_tablettrue)。这样,在大规模数据导入过程中,单个批次的数据仅写入一个数据分片,能够提高数据导入的并发度和吞吐量,减少因数据导入和压缩(Compaction)操作造成的写放大问题,从而确保集群稳定性。

在以下场景中,建议使用 Random 分桶:

  • 在任意维度分析的场景中,业务没有特别针对某一列频繁进行过滤或关联查询时,可以选择 Random 分桶;

  • 当经常查询的列或组合列数据分布极其不均匀时,使用 Random 分桶可以避免数据倾斜。

  • Random 分桶无法根据分桶键进行剪裁,会扫描命中分区的所有数据,不建议在点查场景下使用;

  • 只有 DUPLICATE 表可以使用 Random 分区,UNIQUE 与 AGGREGATE 表无法使用 Random 分桶;

以下示例展示了如何创建带有 Random 分桶的表。

sql 复制代码
CREATE TABLE demo.random_bucket_tbl(
    oid         BIGINT,
    dt          DATE,
    region      VARCHAR(10),
    amount      INT
)
DUPLICATE KEY(oid)
PARTITION BY RANGE(dt) (
    PARTITION p250101 VALUES LESS THAN("2025-01-01"),
    PARTITION p250102 VALUES LESS THAN("2025-01-02")
)
DISTRIBUTED BY RANDOM BUCKETS 8;

示例中,通过 DISTRIBUTED BY RANDOM 语句指定了使用 Random 分桶,创建 Random 分桶无需选择分桶键,通过 BUCKETS 8 语句指定创建 8 个分桶。

4.2.5.2、选择分桶键

分桶键可以是一列或者多列。如果是 DUPLICATE 表,任何 Key 列与 Value 列都可以作为分桶键。如果是 AGGREGATE 或 UNIQUE 表,为了保证逐渐的聚合性,分桶列必须是 Key 列。

通常情况下,可以根据以下规则选择分桶键:

  • 利用查询过滤条件:使用查询中的过滤条件进行 Hash 分桶,有助于数据的剪裁,减少数据扫描量;

  • 利用高基数列:选择高基数(唯一值较多)的列进行 Hash 分桶,有助于数据均匀的分散在每一个分桶中;

  • 高并发点查场景:建议选择单列或较少列进行分桶。点查可能仅触发一个分桶扫描,不同查询之间触发不同分桶扫描的概率较大,从而减小查询间的 IO 影响。

  • 大吞吐查询场景:建议选择多列进行分桶,使数据更均匀分布。若查询条件不能包含所有分桶键的等值条件,将增加查询吞吐,降低单个查询延迟。

4.2.5.3、选择分桶数量

在 Doris 中,一个 bucket 会被存储为一个物理文件(tablet)。一个表的 Tablet 数量等于 partition_num(分区数)乘以 bucket_num(分桶数)。一旦指定 Partition 的数量,便不可更改。

在确定 bucket 数量时,需预先考虑机器扩容情况。自 2.0 版本起,Doris 支持根据机器资源和集群信息自动设置分区中的分桶数。

4.2.5.3.1、手动设置分桶数

通过 DISTRIBUTED 语句可以指定分桶数量:

sql 复制代码
-- Set hash bucket num to 8
DISTRIBUTED BY HASH(region) BUCKETS 8

-- Set random bucket num to 8
DISTRIBUTED BY RANDOM BUCKETS 8

在决定分桶数量时,通常遵循数量与大小两个原则,当发生冲突时,优先考虑大小原则:

  • 大小原则 :建议一个 tablet 的大小在 1-10G 范围内。过小的 tablet 可能导致聚合效果不佳,增加元数据管理压力;过大的 tablet 则不利于副本迁移、补齐,且会增加 Schema Change 操作的失败重试代价;

  • 数量原则:在不考虑扩容的情况下,一个表的 tablet 数量建议略多于整个集群的磁盘数量。

例如,假设有 10 台 BE 机器,每个 BE 一块磁盘,可以按照以下建议进行数据分桶:

单表大小 建议分桶数量
500MB 4-8 个分桶
5GB 6-16 个分桶
50GB 32 个分桶
500GB 建议分区,每个分区 50GB,每个分区 16-32 个分桶
5TB 建议分区,每个分区 50GB,每个分桶 16-32 个分桶

提示:表的数据量可以通过 SHOW DATA 命令查看。结果需要除以副本数,及表的数据量。

4.2.5.3.2、自动设置分桶数

自动推算分桶数功能会根据过去一段时间的分区大小,自动预测未来的分区大小,并据此确定分桶数量。

sql 复制代码
-- Set hash bucket auto
DISTRIBUTED BY HASH(region) BUCKETS AUTO
properties("estimate_partition_size" = "20G")

-- Set random bucket auto
DISTRIBUTED BY HASH(region) BUCKETS AUTO
properties("estimate_partition_size" = "20G")

在创建分桶时,可以通过 estimate_partition_size 属性来调整前期估算的分区大小。此参数为可选设置,若未给出,Doris 将默认取值为 10GB。请注意,该参数与后期系统通过历史分区数据推算出的未来分区大小无关。

4.2.5.3.3、维护数据分桶

目前,Doris 仅支持修改新增分区的分桶数量,对于以下操作暂不支持:

  1. 不支持修改分桶类型
  2. 不支持修改分桶键
  3. 不支持修改已创建的分桶的分桶数量

4.3、数据类型

Apache Doris 已支持的数据类型列表如下:

数值类型

类型名 存储空间(字节) 描述
BOOLEAN 1 布尔值,0 代表 false,1 代表 true。
TINYINT 1 有符号整数,范围 [-128, 127]。
SMALLINT 2 有符号整数,范围 [-32768, 32767]。
INT 4 有符号整数,范围 [-2147483648, 2147483647]。
BIGINT 8 有符号整数,范围 [-9223372036854775808, 9223372036854775807]。
LARGEINT 16 有符号整数,范围 [-2^127 + 1 ~ 2^127 - 1]。
FLOAT 4 浮点数,范围 [-3.4E+38 ~ 3.4E+38]。
DOUBLE 8 浮点数,范围 [-1.79E+308 ~ 1.79E+308]。
DECIMAL 4/8/16/32 高精度定点数,格式:DECIMAL(P[,S])。P 为有效数字,S 为小数位数。

日期类型

类型名 存储空间(字节) 描述
DATE 4 日期类型,范围 ['0000-01-01', '9999-12-31']。
DATETIME 8 日期时间类型,支持微秒精度(0-6位小数)。

字符串类型

类型名 存储空间(字节) 描述
CHAR M 定长字符串,M 为字节长度(1-255)。
VARCHAR 不定长 变长字符串,M 为字节长度(1-65533)。
STRING 不定长 变长字符串,默认支持 1MB,可调至 2GB。仅用于 Value 列。

半结构类型

类型名 存储空间(字节) 描述
ARRAY 不定长 由 T 类型元素组成的数组。
MAP 不定长 由 K, V 类型元素组成的 map。
STRUCT 不定长 由多个 Field 组成的结构体。
JSON 不定长 二进制 JSON 类型,通过 JSON 函数访问。
VARIANT 不定长 动态可变数据类型,专为半结构化数据设计。

聚合类型

类型名 存储空间(字节) 描述
HLL 不定长 模糊去重,误差约 1%-2%。
BITMAP 不定长 用于高效去重计算。
QUANTILE_STATE 不定长 计算分位数近似值。
AGG_STATE 不定长 聚合函数,需配合函数组合器使用。

IP 类型

类型名 存储空间(字节) 描述
IPv4 4 存储 IPv4 地址,配合 ipv4_* 函数使用。
IPv6 16 存储 IPv6 地址,配合 ipv6_* 函数使用。

可通过 SHOW DATA TYPES; 语句查看 Apache Doris 支持的所有数据类型。

4.4、表索引

数据库索引是用于查询加速的,为了加速不同的查询场景,Apache Doris 支持了多种丰富的索引。

索引分类和原理

从加速的查询和原理来看,Apache Doris 的索引分为点查索引跳数索引两大类。

  • 点查索引 :常用于加速点查,原理是通过索引定位到满足 WHERE 条件的有哪些行,直接读取那些行。点查索引在满足条件的行比较少时效果很好 。Apache Doris 的点查索引包括前缀索引和倒排索引。
    • 前缀索引:Apache Doris 按照排序键以有序的方式存储数据,并每隔 1024 行数据创建一个稀疏前缀索引。索引中的 Key 是当前 1024 行中第一行中排序列的值。如果查询涉及已排序列,系统将找到相关 1024 行组的第一行并从那里开始扫描。
    • 倒排索引:对创建了倒排索引的列,建立每个值到对应行号集合的倒排表。对于等值查询,先从倒排表中查到行号集合,然后直接读取对应行的数据,而不用逐行扫描匹配数据,从而减少 I/O 加速查询。倒排索引还能加速范围过滤、文本关键词匹配,算法更加复杂但是基本原理类似。(备注:之前的 BITMAP 索引已经被更强的倒排索引取代)
  • 跳数索引 :常用于加速分析,原理是通过索引确定不满足 WHERE 条件的数据块,跳过这些不满足条件的数据块,只读取可能满足条件的数据块并再进行一次逐行过滤,最终得到满足条件的行。跳数索引在满足条件的行比较多时效果较好 Apache Doris 的跳数索引包括 ZoneMap 索引、BloomFilter 索引、NGram BloomFilter 索引。
    • ZoneMap 索引:自动维护每一列的统计信息,为每一个数据文件(Segment)和数据块(Page)记录最大值、最小值、是否有 NULL。对于等值查询、范围查询、IS NULL,可以通过最大值、最小值、是否有 NULL 来判断数据文件和数据块是否可以包含满足条件的数据,如果没有则跳过不读对应的文件或数据块减少 I/O 加速查询。
    • BloomFilter 索引:将索引对应列的可能取值存入 BloomFilter 数据结构中,它可以快速判断一个值是否在 BloomFilter 里面,并且 BloomFilter 存储空间占用很低。对于等值查询,如果判断这个值不在 BloomFilter 里面,就可以跳过对应的数据文件或者数据块减少 I/O 加速查询。
    • NGram BloomFilter 索引:用于加速文本 LIKE 查询,基本原理与 BloomFilter 索引类似,只是存入 BloomFilter 的不是原始文本的值,而是对文本进行 NGram 分词,每个词作为值存入 BloomFilter。对于 LIKE 查询,将 LIKE 的 pattern 也进行 NGram 分词,判断每个词是否在 BloomFilter 中,如果某个词不在则对应的数据文件或者数据块就不满足 LIKE 条件,可以跳过这部分数据减少 I/O 加速查询。

上述索引中,前缀索引和 ZoneMap 索引是 Apache Doris 自动维护的内建智能索引,无需用户管理,而倒排索引、BloomFilter 索引、NGram BloomFilter 索引则需要用户自己根据场景选择,手动创建、删除。

各种类型索引特点对比

类型 索引 优点 局限
点查索引 前缀索引 内置索引,性能最好 一个表只有一组前缀索引
点查索引 倒排索引 支持分词和关键词匹配,任意列可建索引,多条件组合,持续增加函数加速 索引存储空间较大,与原始数据相当
跳数索引 ZoneMap 索引 内置索引,索引存储空间小 支持的查询类型少,只支持等于、范围
跳数索引 BloomFilter 索引 比 ZoneMap 更精细,索引空间中等 支持的查询类型少,只支持等于
跳数索引 NGram BloomFilter 索引 支持 LIKE 加速,索引空间中等 支持的查询类型少,只支持 LIKE 加速

索引加速的运算符和函数列表

运算符 / 函数 前缀索引 倒排索引 ZoneMap 索引 BloomFilter 索引 NGram BloomFilter 索引
= YES YES YES YES NO
!= YES YES NO NO NO
IN YES YES YES YES NO
NOT IN YES YES NO NO NO
>, >=, <, <=, BETWEEN YES YES YES NO NO
IS NULL YES YES YES NO NO
IS NOT NULL YES YES NO NO NO
LIKE NO NO NO NO YES
MATCH, MATCH_* NO YES NO NO NO
array_contains NO YES NO NO NO
array_overlaps NO YES NO NO NO
is_ip_address_in_range NO YES NO NO NO

索引设计指南

数据库表的索引设计和优化跟数据特点和查询很相关,需要根据实际场景测试和优化。虽然没有 "银弹",Apache Doris 仍然不断努力降低用户使用索引的难度,用户可以根据下面的简单建议原则进行索引选择和测试。

  1. 最频繁使用的过滤条件指定为 Key 自动建前缀索引,因为它的过滤效果最好,但是一个表只能有一个前缀索引,因此要用在最频繁的过滤条件上

  2. 对非 Key 字段如有过滤加速需求,首选建倒排索引,因为它的适用面广,可以多条件组合,次选下面两种索引:

    • 有字符串 LIKE 匹配需求,再加一个 NGram BloomFilter 索引

    • 对索引存储空间很敏感,将倒排索引换成 BloomFilter 索引

  3. 如果性能不及预期,通过 QueryProfile 分析索引过滤掉的数据量和消耗的时间,具体参考各个索引的详细文档

4.4.1、前缀索引与排序键

4.4.1.1、索引原理

Doris 的数据存储在类似 SSTable(Sorted String Table)的数据结构中。该结构是一种有序的数据结构,可以按照指定的一个或多个列进行排序存储。在这种数据结构上,以排序列的全部或者前面几个作为条件进行查找,会非常的高效。

在 Aggregate、Unique 和 Duplicate 三种数据模型中。底层的数据存储,是按照各自建表语句中,Aggregate Key、Unique Key 和 Duplicate Key 中指定的列进行排序存储的。这些 Key,称为排序键(Sort Key)。借助排序键,在查询时,通过给排序列指定条件,Doris 不需要扫描全表即可快速找到需要处理的数据,降低搜索的复杂度,从而加速查询。

在排序键的基础上,又引入了前缀索引(Prefix Index)。前缀索引是一种稀疏索引。表中按照相应的行数的数据构成一个逻辑数据块 (Data Block)。每个逻辑数据块在前缀索引表中存储一个索引项,索引项的长度不超过 36 字节,其内容为数据块中第一行数据的排序列组成的前缀,在查找前缀索引表时可以帮助确定该行数据所在逻辑数据块的起始行号。由于前缀索引比较小,所以,可以全量在内存缓存,快速定位数据块,大大提升了查询效率。

提示:数据块一行数据的前 36 个字节作为这行数据的前缀索引。当遇到 VARCHAR 类型时,前缀索引会直接截断。如果第一列即为 VARCHAR,那么即使没有达到 36 字节,也会直接截断,后面的列不再加入前缀索引。

4.4.1.2、使用场景

前缀索引可以加速等值查询和范围查询。

4.4.1.3、管理索引

前缀索引没有专门的语法去定义,建表时自动取表的 Key 的前 36 字节作为前缀索引。

前缀索引选择建议

因为一个表的 Key 定义是唯一的,所以一个表只有一组前缀索引,因此设计表结构时选择合适的前缀索引很重要,可以参考下面的建议:

  1. 选择查询中最常用于 WHERE 过滤条件的字段作为 Key。
  2. 越常用的字段越放在前面,因为前缀索引只对 WHERE 条件中字段在 Key 的前缀中才有效。

使用其他不能命中前缀索引的列作为条件进行的查询来说,效率上可能无法满足需求,有两种解决方案:

  1. 对需要加速查询的条件列创建倒排索引,由于一个表的倒排索引可以有很多个。
  2. 对于 Duplicate 表可以通过创建相应的调整了列顺序的单表强一致物化视图来间接实现多种前缀索引
4.4.1.4、使用索引

前缀索引用于加速 WHERE 条件中的等值和范围查询,能加速时自动生效,没有特殊语法。

可以通过 Query Profile 中的下面几个指标分析前缀索引的加速效果。

  • RowsKeyRangeFiltered 前缀索引过滤掉的行数,可以与其他几个 Rows 值对比分析索引过滤效果
4.4.1.5、使用示例
  • 假如表的排序列为如下 5 列,那么前缀索引为:user_id(8 Bytes) + age(4 Bytes) + message(prefix 20 Bytes)。
ColumnName Type
user_id BIGINT
age INT
message VARCHAR(100)
max_dwell_time DATETIME
min_dwell_time DATETIME
  • 假如表的排序列为如下 5 列,则前缀索引为 user_name(20 Bytes)。即使没有达到 36 个字节,因为遇到 VARCHAR,所以直接截断,不再往后继续。
ColumnName Type
user_name VARCHAR(20)
age INT
message VARCHAR(100)
max_dwell_time DATETIME
min_dwell_time DATETIME
  • 当我们的查询条件,是前缀索引的前缀时,可以极大地加快查询速度。比如在第一个例子中,执行如下查询:
sql 复制代码
SELECT * FROM table WHERE user_id=1829239 and age=20;

该查询的效率会远高于如下查询:

sql 复制代码
SELECT * FROM table WHERE age=20;

所以在建表时,正确选择列顺序,能够极大地提高查询效率。

4.4.2、倒排索引

4.4.2.1、索引原理

倒排索引,是信息检索领域常用的索引技术,将文本分成一个个词,构建 词 -> 文档编号 的索引,可以快速查找一个词在哪些文档出现。

从 2.0.0 版本开始,Doris 支持倒排索引,可以用来进行文本类型的全文检索、普通数值日期类型的等值范围查询,快速从海量数据中过滤出满足条件的行。

在 Doris 的倒排索引实现中,Table 的一行对应一个文档、一列对应文档中的一个字段,因此利用倒排索引可以根据关键词快速定位包含它的行,达到 WHERE 子句加速的目的。

与 Doris 中其他索引不同的是,在存储层倒排索引使用独立的文件,跟数据文件一一对应、但物理存储上文件相互独立。这样的好处是可以做到创建、删除索引不用重写数据文件,大幅降低处理开销。

4.4.2.2、使用场景

倒排索引的使用范围很广泛,可以加速等值、范围、全文检索(关键词匹配、短语系列匹配等)。一个表可以有多个倒排索引,查询时多个倒排索引的条件可以任意组合。

倒排索引的功能简要介绍如下:

  1. 加速字符串类型的全文检索
  • 支持关键词检索,包括同时匹配多个关键字 MATCH_ALL、匹配任意一个关键字 MATCH_ANY

  • 支持短语查询 MATCH_PHRASE

    • 支持指定词距 slop
    • 支持短语 + 前缀 MATCH_PHRASE_PREFIX
  • 支持分词正则查询 MATCH_REGEXP

  • 支持英文、中文以及 Unicode 多种分词

  1. 加速普通等值、范围查询,覆盖原来 BITMAP 索引的功能,代替 BITMAP 索引
  • 支持字符串、数值、日期时间类型的 =, !=, >, >=, <, <= 快速过滤

  • 支持字符串、数字、日期时间数组类型的 =, !=, >, >=, <, <=

  1. 支持完善的逻辑组合
  • 不仅支持 AND 条件加速,还支持 OR NOT 条件加速

  • 支持多个条件的任意 AND OR NOT 逻辑组合

  1. 灵活高效的索引管理
  • 支持在创建表上定义倒排索引

  • 支持在已有的表上增加倒排索引,而且支持增量构建倒排索引,无需重写表中的已有数据

  • 支持删除已有表上的倒排索引,无需重写表中的已有数据

  1. 倒排索引的使用有下面一些限制
  • 存在精度问题的浮点数类型 FLOAT 和 DOUBLE 不支持倒排索引,原因是浮点数精度不准确。解决方案是使用精度准确的定点数类型 DECIMAL,DECIMAL 支持倒排索引。

  • 部分复杂数据类型还不支持倒排索引,包括:MAP、STRUCT、JSON、HLL、BITMAP、QUANTILE_STATE、AGG_STATE。其中 MAP、STRUCT 会逐步支持,JSON 类型可以换成 VARIANT 类型获得支持。其他几个类型因为其特殊用途暂不需要支持倒排索引。

  • DUPLICATE 和 开启 Merge-on-Write 的 UNIQUE 表模型支持任意列建倒排索引。但是 AGGREGATE 和 未开启 Merge-on-Write 的 UNIQUE 模型仅支持 Key 列建倒排索引,非 Key 列不能建倒排索引,这是因为这两个模型需要读取所有数据后做合并,因此不能利用索引做提前过滤。

4.4.2.3、管理索引
  • 建表时定义倒排索引

在建表语句中 COLUMN 的定义之后是索引定义:

sql 复制代码
CREATE TABLE table_name
(
  column_name1 TYPE1,
  column_name2 TYPE2,
  column_name3 TYPE3,
  INDEX idx_name1(column_name1) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'],
  INDEX idx_name2(column_name2) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment']
)
table_properties;

语法说明如下:

  1. idx_column_name(column_name) 是必须的,column_name 是建索引的列名,必须是前面列定义中出现过的,idx_column_name 是索引名字,必须表级别唯一,建议命名规范:列名前面加前缀 idx_

  2. USING INVERTED 是必须的,用于指定索引类型是倒排索引

  3. COMMENT 是可选的,用于指定索引注释

  4. PROPERTIES 是可选的,用于指定倒排索引的额外属性,目前支持的属性如下:

sql 复制代码
parser 指定分词器

- 默认不指定代表不分词
- `english` 是英文分词,适合被索引列是英文的情况,用空格和标点符号分词,性能高
- `chinese` 是中文分词,适合被索引列主要是中文的情况,性能比 English 分词低
- `unicode` 是多语言混合类型分词,适用于中英文混合、多语言混合的情况。它能够对邮箱前缀和后缀、IP 地址以及字符数字混合进行分词,并且可以对中文按字符分词

分词的效果可以通过 `TOKENIZE SQL` 函数进行验证,具体参考后续章节。
sql 复制代码
parser_mode

用于指定分词的模式,目前 parser = chinese 时支持如下几种模式:

- fine_grained:细粒度模式,倾向于分出比较短、较多的词,比如 '武汉市长江大桥' 会分成 '武汉', '武汉市', '市长', '长江', '长江大桥', '大桥' 6 个词
- coarse_grained:粗粒度模式,倾向于分出比较长、较少的词,,比如 '武汉市长江大桥' 会分成 '武汉市' '长江大桥' 2 个词
- 默认 coarse_grained
sql 复制代码
support_phrase

用于指定索引是否支持 MATCH_PHRASE 短语查询加速

- true 为支持,但是索引需要更多的存储空间

- false 为不支持,更省存储空间,可以用 MATCH_ALL 查询多个关键字

- 从 2.0.14, 2.1.5 和 3.0.1 版本开始,如果指定了 parser 则默认为 true,否则默认为 false

例如下面的例子指定中文分词,粗粒度模式,支持短语查询加速。
   INDEX idx_name(column_name) USING INVERTED PROPERTIES("parser" = "chinese", "parser_mode" = "coarse_grained", "support_phrase" = "true")
sql 复制代码
char_filter

用于指定在分词前对文本进行预处理,通常用于影响分词行为

char_filter_type:指定使用不同功能的 char_filter(目前仅支持 char_replace)

char_replace 将 pattern 中每个 char 替换为一个 replacement 中的 char

- char_filter_pattern:需要被替换掉的字符数

- char_filter_replacement:替换后的字符数组,可以不用配置,默认为一个空格字符

例如下面的例子将点和下划线替换成空格,达到将点和下划线作为单词分隔符的目的,影响分词行为。
   INDEX idx_name(column_name) USING INVERTED PROPERTIES("parser" = "unicode", "char_filter_type" = "char_replace", "char_filter_pattern" = "._", "char_filter_replacement" = " ")
sql 复制代码
ignore_above

用于指定不分词字符串索引(没有指定 parser)的长度限制

- 长度超过 ignore_above 设置的字符串不会被索引。对于字符串数组,ignore_above 将分别应用于每个数组元素,长度超过 ignore_above 的字符串元素将不被索引。

- 默认为 256,单位是字节
sql 复制代码
lower_case

是否将分词进行小写转换,从而在匹配的时候实现忽略大小写

- true: 转换小写
- false:不转换小写
- 从 2.0.7 和 2.1.2 版本开始默认为 true,自动转小写,之前的版本默认为 false
sql 复制代码
stopwords

指明使用的停用词表,会影响分词器的行为

默认的内置停用词表包含一些无意义的词:'is'、'the'、'a' 等。在写入或者查询时,分词器会忽略停用词表中的词。

- none: 使用空的停用词表
  • 已有表增加倒排索引
  1. ADD INDEX

支持CREATE INDEXALTER TABLE ADD INDEX 两种语法,参数跟建表时索引定义相同

sql 复制代码
-- 语法 1
CREATE INDEX idx_name ON table_name(column_name) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'];
-- 语法 2
ALTER TABLE table_name ADD INDEX idx_name(column_name) USING INVERTED [PROPERTIES(...)] [COMMENT 'your comment'];
  1. BUILD INDEX

CREATE / ADD INDEX 操作只是新增了索引定义,这个操作之后的新写入数据会生成倒排索引,而存量数据需要使用 BUILD INDEX 触发:

sql 复制代码
-- 语法 1,默认给全表的所有分区 BUILD INDEX
BUILD INDEX index_name ON table_name;
-- 语法 2,可指定 Partition,可指定一个或多个
BUILD INDEX index_name ON table_name PARTITIONS(partition_name1, partition_name2);

通过 SHOW BUILD INDEX 查看 BUILD INDEX 进度:

sql 复制代码
SHOW BUILD INDEX [FROM db_name];
-- 示例 1,查看所有的 BUILD INDEX 任务进展
SHOW BUILD INDEX;
-- 示例 2,查看指定 table 的 BUILD INDEX 任务进展
SHOW BUILD INDEX where TableName = "table1";

通过 CANCEL BUILD INDEX 取消 BUILD INDEX

sql 复制代码
CANCEL BUILD INDEX ON table_name;
CANCEL BUILD INDEX ON table_name (job_id1,jobid_2,...);

提示

  • BUILD INDEX 会生成一个异步任务执行,在每个 BE 上有多个线程执行索引构建任务,通过 BE 参数 alter_index_worker_count 可以设置,默认值是 3。

  • 2.0.12 和 2.1.4 之前的版本 BUILD INDEX 会一直重试直到成功,从这两个版本开始通过失败和超时机制避免一直重试。3.0 存算分离模式暂不支持此命令。

    1. 一个 tablet 的多数副本 BUILD INDEX 失败后,整个 BUILD INDEX 失败结束
    2. 时间超过 alter_table_timeout_second ()BUILD INDEX 超时结束
    3. 用户可以多次触发 BUILD INDEX,已经 BUILD 成功的索引不会重复 BUILD
  • 已有表删除倒排索引

sql 复制代码
-- 语法 1
DROP INDEX idx_name ON table_name;
-- 语法 2
ALTER TABLE table_name DROP INDEX idx_name;

DROP INDEX 会删除索引定义,新写入数据不会再写索引,同时会生成一个异步任务执行索引删除操作,在每个 BE 上有多个线程执行索引删除任务,通过 BE 参数 alter_index_worker_count 可以设置,默认值是 3。

  • 查看倒排索引
sql 复制代码
-- 语法 1,表的 schema 中 INDEX 部分 USING INVERTED 是倒排索引
SHOW CREATE TABLE table_name;

-- 语法 2,IndexType 为 INVERTED 的是倒排索引
SHOW INDEX FROM idx_name;
4.4.2.4、使用索引
  • 利用倒排索引加速查询
sql 复制代码
-- 1. 全文检索关键词匹配,通过 MATCH_ANY MATCH_ALL 完成
SELECT * FROM table_name WHERE column_name MATCH_ANY | MATCH_ALL 'keyword1 ...';

-- 1.1 content 列中包含 keyword1 的行
SELECT * FROM table_name WHERE content MATCH_ANY 'keyword1';

-- 1.2 content 列中包含 keyword1 或者 keyword2 的行,后面还可以添加多个 keyword
SELECT * FROM table_name WHERE content MATCH_ANY 'keyword1 keyword2';

-- 1.3 content 列中同时包含 keyword1 和 keyword2 的行,后面还可以添加多个 keyword
SELECT * FROM table_name WHERE content MATCH_ALL 'keyword1 keyword2';


-- 2. 全文检索短语匹配,通过 MATCH_PHRASE 完成
-- 2.1 content 列中同时包含 keyword1 和 keyword2 的行,而且 keyword2 必须紧跟在 keyword1 后面
-- 'keyword1 keyword2','wordx keyword1 keyword2','wordx keyword1 keyword2 wordy' 能匹配,因为他们都包含 keyword1 keyword2,而且 keyword2 紧跟在 keyword1 后面
-- 'keyword1 wordx keyword2' 不能匹配,因为 keyword1 keyword2 之间隔了一个词 wordx
-- 'keyword2 keyword1',因为 keyword1 keyword2 的顺序反了
-- 使用 MATCH_PHRASE 需要再 PROPERTIES 中开启 "support_phrase" = "true"
SELECT * FROM table_name WHERE content MATCH_PHRASE 'keyword1 keyword2';

-- 2.2 content 列中同时包含 keyword1 和 keyword2 的行,而且 keyword1 keyword2 的 `词距`(slop)不超过 3
-- 'keyword1 keyword2', 'keyword1 a keyword2', 'keyword1 a b c keyword2' 都能匹配,因为 keyword1 keyword2 中间隔的词分别是 0 1 3 都不超过 3
-- 'keyword1 a b c d keyword2' 不能能匹配,因为 keyword1 keyword2 中间隔的词有 4 个,超过 3
-- 'keyword2 keyword1', 'keyword2 a keyword1', 'keyword2 a b c keyword1' 也能匹配,因为指定 slop > 0 时不再要求 keyword1 keyword2 的顺序。这个行为参考了 ES,与直觉的预期不一样,因此 Doris 提供了在 slop 后面指定正数符号(+)表示需要保持 keyword1 keyword2 的先后顺序
SELECT * FROM table_name WHERE content MATCH_PHRASE 'keyword1 keyword2 ~3';
-- slop 指定正号,'keyword1 a b c keyword2' 能匹配,而 'keyword2 a b c keyword1' 不能匹配
SELECT * FROM table_name WHERE content MATCH_PHRASE 'keyword1 keyword2 ~3+';

-- 2.3 在保持词顺序的前提下,对最后一个词 keyword2 做前缀匹配,默认找 50 个前缀词(session 变量 inverted_index_max_expansions 控制)
-- 需要保证 keyword1, keyword2 在原文分词后也是相邻的,不能中间有其他词
-- 'keyword1 keyword2abc' 能匹配,因为 keyword1 完全一样,最后一个 keyword2abc 是 keyword2 的前缀
-- 'keyword1 keyword2' 也能匹配,因为 keyword2 也是 keyword2 的前缀
-- 'keyword1 keyword3' 不能匹配,因为 keyword3 不是 keyword2 的前缀
-- 'keyword1 keyword3abc' 也不能匹配,因为 keyword3abc 也不是 keyword2 的前缀
SELECT * FROM table_name WHERE content MATCH_PHRASE_PREFIX 'keyword1 keyword2';

-- 2.4 如果只填一个词会退化为前缀查询,默认找 50 个前缀词(session 变量 inverted_index_max_expansions 控制)
SELECT * FROM table_name WHERE content MATCH_PHRASE_PREFIX 'keyword1';

-- 2.5 对分词后的词进行正则匹配,默认匹配 50 个(session 变量 inverted_index_max_expansions 控制)
-- 类似 MATCH_PHRASE_PREFIX 的匹配规则,只是前缀变成了正则
SELECT * FROM table_name WHERE content MATCH_REGEXP 'key.*';

-- 3. 普通等值、范围、IN、NOT IN,正常的 SQL 语句即可,例如
SELECT * FROM table_name WHERE id = 123;
SELECT * FROM table_name WHERE ts > '2023-01-01 00:00:00';
SELECT * FROM table_name WHERE op_type IN ('add', 'delete');

-- 4. 多列全文检索匹配,通过 multi_match 函数完成
-- 参数说明:
--   前N个参数是要匹配的列名
--   倒数第二个参数指定匹配模式:'any'/'all'/'phrase'/'phrase_prefix'
--   最后一个参数是要搜索的关键词或短语

-- 4.1 在col1,col2,col3任意一列中包含'keyword1'的行(OR逻辑)
SELECT * FROM table_name WHERE multi_match(col1, col2, col3, 'any', 'keyword1');

-- 4.2 在col1,col2,col3所有列中都包含'keyword1'的行(AND逻辑)
SELECT * FROM table_name WHERE multi_match(col1, col2, col3, 'all', 'keyword1');

-- 4.3 在col1,col2,col3任意一列中包含完整短语'keyword1'的行(精确短语匹配)
SELECT * FROM table_name WHERE multi_match(col1, col2, col3, 'phrase', 'keyword1');

-- 4.4 在col1,col2,col3任意一列中包含以'keyword1'开头的短语的行(短语前缀匹配)
-- 例如会匹配"keyword123"这样的内容
SELECT * FROM table_name WHERE multi_match(col1, col2, col3, 'phrase_prefix', 'keyword1');
  • 通过 profile 分析索引加速效果

倒排查询加速可以通过 session 变量 enable_inverted_index_query 开关,默认是 true 打开,有时为了验证索引加速效果可以设置为 false 关闭。

可以通过 Query Profile 中的下面几个指标分析倒排索引的加速效果。

  • RowsInvertedIndexFiltered 倒排过滤掉的行数,可以与其他几个 Rows 值对比分析索引过滤效果

  • InvertedIndexFilterTime 倒排索引消耗的时间

    • InvertedIndexSearcherOpenTime 倒排索引打开索引的时间
    • InvertedIndexSearcherSearchTime 倒排索引内部查询的时间
  • 用分词函数验证分词效果

如果想检查分词实际效果或者对一段文本进行分词行为,可以使用 TOKENIZE 函数进行验证。

TOKENIZE 函数的第一个参数是待分词的文本,第二个参数是创建索引指定的分词参数。

sql 复制代码
SELECT TOKENIZE('武汉长江大桥','"parser"="chinese","parser_mode"="fine_grained"');
+-----------------------------------------------------------------------------------+
| tokenize('武汉长江大桥', '"parser"="chinese","parser_mode"="fine_grained"')       |
+-----------------------------------------------------------------------------------+
| ["武汉", "武汉长江大桥", "长江", "长江大桥", "大桥"]                              |
+-----------------------------------------------------------------------------------+

SELECT TOKENIZE('武汉市长江大桥','"parser"="chinese","parser_mode"="fine_grained"');
+--------------------------------------------------------------------------------------+
| tokenize('武汉市长江大桥', '"parser"="chinese","parser_mode"="fine_grained"')        |
+--------------------------------------------------------------------------------------+
| ["武汉", "武汉市", "市长", "长江", "长江大桥", "大桥"]                               |
+--------------------------------------------------------------------------------------+

SELECT TOKENIZE('武汉市长江大桥','"parser"="chinese","parser_mode"="coarse_grained"');
+----------------------------------------------------------------------------------------+
| tokenize('武汉市长江大桥', '"parser"="chinese","parser_mode"="coarse_grained"')        |
+----------------------------------------------------------------------------------------+
| ["武汉市", "长江大桥"]                                                                 |
+----------------------------------------------------------------------------------------+

SELECT TOKENIZE('I love Doris','"parser"="english"');
+------------------------------------------------+
| tokenize('I love Doris', '"parser"="english"') |
+------------------------------------------------+
| ["i", "love", "doris"]                         |
+------------------------------------------------+

SELECT TOKENIZE('I love CHINA 我爱我的祖国','"parser"="unicode"');
+-------------------------------------------------------------------+
| tokenize('I love CHINA 我爱我的祖国', '"parser"="unicode"')       |
+-------------------------------------------------------------------+
| ["i", "love", "china", "我", "爱", "我", "的", "祖", "国"]        |
+-------------------------------------------------------------------+
4.4.2.5、使用示例

用 HackerNews 100 万条数据展示倒排索引的创建、全文检索、普通查询,包括跟无索引的查询性能进行简单对比。

  • 建表
sql 复制代码
CREATE DATABASE test_inverted_index;

USE test_inverted_index;

-- 创建表的同时创建了 comment 的倒排索引 idx_comment
--   USING INVERTED 指定索引类型是倒排索引
--   PROPERTIES("parser" = "english") 指定采用 "english" 分词,还支持 "chinese" 中文分词和 "unicode" 中英文多语言混合分词,如果不指定 "parser" 参数表示不分词

CREATE TABLE hackernews_1m
(
    `id` BIGINT,
    `deleted` TINYINT,
    `type` String,
    `author` String,
    `timestamp` DateTimeV2,
    `comment` String,
    `dead` TINYINT,
    `parent` BIGINT,
    `poll` BIGINT,
    `children` Array<BIGINT>,
    `url` String,
    `score` INT,
    `title` String,
    `parts` Array<INT>,
    `descendants` INT,
    INDEX idx_comment (`comment`) USING INVERTED PROPERTIES("parser" = "english") COMMENT 'inverted index for comment'
)
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES ("replication_num" = "1");
  • 导入数据

通过 Stream Load 导入数据

sql 复制代码
wget https://qa-build.oss-cn-beijing.aliyuncs.com/regression/index/hacknernews_1m.csv.gz

curl --location-trusted -u root: -H "compress_type:gz" -T hacknernews_1m.csv.gz  http://127.0.0.1:8030/api/test_inverted_index/hackernews_1m/_stream_load
{
    "TxnId": 2,
    "Label": "a8a3e802-2329-49e8-912b-04c800a461a6",
    "TwoPhaseCommit": "false",
    "Status": "Success",
    "Message": "OK",
    "NumberTotalRows": 1000000,
    "NumberLoadedRows": 1000000,
    "NumberFilteredRows": 0,
    "NumberUnselectedRows": 0,
    "LoadBytes": 130618406,
    "LoadTimeMs": 8988,
    "BeginTxnTimeMs": 23,
    "StreamLoadPutTimeMs": 113,
    "ReadDataTimeMs": 4788,
    "WriteDataTimeMs": 8811,
    "CommitAndPublishTimeMs": 38
}

SQL 运行 count() 确认导入数据成功

sql 复制代码
SELECT count() FROM hackernews_1m;
+---------+
| count() |
+---------+
| 1000000 |
+---------+
  • 查询
  1. 01 全文检索
  • 用 LIKE 匹配计算 comment 中含有 'OLAP' 的行数,耗时 0.18s
sql 复制代码
SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%';
+---------+
| count() |
+---------+
|      34 |
+---------+
  • 用基于倒排索引的全文检索 MATCH_ANY 计算 comment 中含有'OLAP'的行数,耗时 0.02s,加速 9 倍,在更大的数据集上效果会更加明显

这里结果条数的差异,是因为倒排索引对 comment 分词后,还会对词进行进行统一成小写等归一化处理,因此 MATCH_ANYLIKE 的结果多一些

sql 复制代码
SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP';
+---------+
| count() |
+---------+
|      35 |
+---------+
  • 同样的对比统计 'OLTP' 出现次数的性能,0.07s vs 0.01s,由于缓存的原因 LIKEMATCH_ANY 都有提升,倒排索引仍然有 7 倍加速
sql 复制代码
SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLTP%';
+---------+
| count() |
+---------+
|      48 |
+---------+


SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLTP';
+---------+
| count() |
+---------+
|      51 |
+---------+
  • 同时出现 'OLAP' 和 'OLTP' 两个词,0.13s vs 0.01s,13 倍加速。
    要求多个词同时出现时(AND 关系)使用 MATCH_ALL 'keyword1 keyword2 ...'
sql 复制代码
SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' AND comment LIKE '%OLTP%';
+---------+
| count() |
+---------+
|      14 |
+---------+


SELECT count() FROM hackernews_1m WHERE comment MATCH_ALL 'OLAP OLTP';
+---------+
| count() |
+---------+
|      15 |
+---------+
  • 任意出现 'OLAP' 和 'OLTP' 其中一个词,0.12s vs 0.01s,12 倍加速

只要求多个词任意一个或多个出现时(OR 关系)使用 MATCH_ANY 'keyword1 keyword2 ...'

sql 复制代码
SELECT count() FROM hackernews_1m WHERE comment LIKE '%OLAP%' OR comment LIKE '%OLTP%';
+---------+
| count() |
+---------+
|      68 |
+---------+

SELECT count() FROM hackernews_1m WHERE comment MATCH_ANY 'OLAP OLTP';
+---------+
| count() |
+---------+
|      71 |
+---------+
  1. 02 普通等值、范围查询
  • DataTime 类型的列范围查询
sql 复制代码
SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00';
+---------+
| count() |
+---------+
|  999081 |
+---------+
  • 为 timestamp 列增加一个倒排索引
sql 复制代码
-- 对于日期时间类型 USING INVERTED,不用指定分词
-- CREATE INDEX 是第一种建索引的语法,另外一种在后面展示
CREATE INDEX idx_timestamp ON hackernews_1m(timestamp) USING INVERTED;
sql 复制代码
BUILD INDEX idx_timestamp ON hackernews_1m;
  • 查看索引创建进度,通过 FinishTime 和 CreateTime 的差值,可以看到 100 万条数据对 timestamp 列建倒排索引只用了 1s
sql 复制代码
SHOW ALTER TABLE COLUMN;
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
| JobId | TableName     | CreateTime              | FinishTime              | IndexName     | IndexId | OriginIndexId | SchemaVersion | TransactionId | State    | Msg  | Progress | Timeout |
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
| 10030 | hackernews_1m | 2023-02-10 19:44:12.929 | 2023-02-10 19:44:13.938 | hackernews_1m | 10031   | 10008         | 1:1994690496  | 3             | FINISHED |      | NULL     | 2592000 |
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
sql 复制代码
-- 若 table 没有分区,PartitionName 默认就是 TableName
SHOW BUILD INDEX;
+-------+---------------+---------------+----------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| JobId | TableName     | PartitionName | AlterInvertedIndexes                                     | CreateTime              | FinishTime              | TransactionId | State    | Msg  | Progress |
+-------+---------------+---------------+----------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| 10191 | hackernews_1m | hackernews_1m | [ADD INDEX idx_timestamp (`timestamp`) USING INVERTED],  | 2023-06-26 15:32:33.894 | 2023-06-26 15:32:34.847 | 3             | FINISHED |      | NULL     |
+-------+---------------+---------------+----------------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
  • 索引创建后,范围查询用同样的查询方式,Doris 会自动识别索引进行优化,但是这里由于数据量小性能差别不大
sql 复制代码
SELECT count() FROM hackernews_1m WHERE timestamp > '2007-08-23 04:17:00';
+---------+
| count() |
+---------+
|  999081 |
+---------+
  • 在数值类型的列 Parent 进行类似 timestamp 的操作,这里查询使用等值匹配
sql 复制代码
SELECT count() FROM hackernews_1m WHERE parent = 11189;
+---------+
| count() |
+---------+
|       2 |
+---------+


-- 对于数值类型 USING INVERTED,不用指定分词
-- ALTER TABLE t ADD INDEX 是第二种建索引的语法
ALTER TABLE hackernews_1m ADD INDEX idx_parent(parent) USING INVERTED;


-- 执行 BUILD INDEX 给存量数据构建倒排索引
BUILD INDEX idx_parent ON hackernews_1m;


SHOW ALTER TABLE COLUMN;
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
| JobId | TableName     | CreateTime              | FinishTime              | IndexName     | IndexId | OriginIndexId | SchemaVersion | TransactionId | State    | Msg  | Progress | Timeout |
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
| 10030 | hackernews_1m | 2023-02-10 19:44:12.929 | 2023-02-10 19:44:13.938 | hackernews_1m | 10031   | 10008         | 1:1994690496  | 3             | FINISHED |      | NULL     | 2592000 |
| 10053 | hackernews_1m | 2023-02-10 19:49:32.893 | 2023-02-10 19:49:33.982 | hackernews_1m | 10054   | 10008         | 1:378856428   | 4             | FINISHED |      | NULL     | 2592000 |
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+

SHOW BUILD INDEX;
+-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| JobId | TableName     | PartitionName | AlterInvertedIndexes                               | CreateTime              | FinishTime              | TransactionId | State    | Msg  | Progress |
+-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| 11005 | hackernews_1m | hackernews_1m | [ADD INDEX idx_parent (`parent`) USING INVERTED],  | 2023-06-26 16:25:10.167 | 2023-06-26 16:25:10.838 | 1002          | FINISHED |      | NULL     |
+-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+


SELECT count() FROM hackernews_1m WHERE parent = 11189;
+---------+
| count() |
+---------+
|       2 |
+---------+
  • 对字符串类型的 author 建立不分词的倒排索引,等值查询也可以利用索引加速
sql 复制代码
SELECT count() FROM hackernews_1m WHERE author = 'faster';
+---------+
| count() |
+---------+
|      20 |
+---------+

-- 这里只用了 USING INVERTED,不对 author 分词,整个当做一个词处理
ALTER TABLE hackernews_1m ADD INDEX idx_author(author) USING INVERTED;


-- 执行 BUILD INDEX 给存量数据加上倒排索引:
BUILD INDEX idx_author ON hackernews_1m;


-- 100 万条 author 数据增量建索引仅消耗 1.5s
SHOW ALTER TABLE COLUMN;
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
| JobId | TableName     | CreateTime              | FinishTime              | IndexName     | IndexId | OriginIndexId | SchemaVersion | TransactionId | State    | Msg  | Progress | Timeout |
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+
| 10030 | hackernews_1m | 2023-02-10 19:44:12.929 | 2023-02-10 19:44:13.938 | hackernews_1m | 10031   | 10008         | 1:1994690496  | 3             | FINISHED |      | NULL     | 2592000 |
| 10053 | hackernews_1m | 2023-02-10 19:49:32.893 | 2023-02-10 19:49:33.982 | hackernews_1m | 10054   | 10008         | 1:378856428   | 4             | FINISHED |      | NULL     | 2592000 |
| 10076 | hackernews_1m | 2023-02-10 19:54:20.046 | 2023-02-10 19:54:21.521 | hackernews_1m | 10077   | 10008         | 1:1335127701  | 5             | FINISHED |      | NULL     | 2592000 |
+-------+---------------+-------------------------+-------------------------+---------------+---------+---------------+---------------+---------------+----------+------+----------+---------+

SHOW BUILD INDEX order by CreateTime desc limit 1;
+-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| JobId | TableName     | PartitionName | AlterInvertedIndexes                               | CreateTime              | FinishTime              | TransactionId | State    | Msg  | Progress |
+-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+
| 13006 | hackernews_1m | hackernews_1m | [ADD INDEX idx_author (`author`) USING INVERTED],  | 2023-06-26 17:23:02.610 | 2023-06-26 17:23:03.755 | 3004          | FINISHED |      | NULL     |
+-------+---------------+---------------+----------------------------------------------------+-------------------------+-------------------------+---------------+----------+------+----------+

-- 创建索引后,字符串等值匹配也有明显加速
SELECT count() FROM hackernews_1m WHERE author = 'faster';
+---------+
| count() |
+---------+
|      20 |
+---------+

4.4.3、BloomFilter 索引

4.4.3.1、索引原理

BloomFilter 索引是基于 BloomFilter 的一种跳数索引。它的原理是利用 BloomFilter 跳过等值查询指定条件不满足的数据块,达到减少 I/O 查询加速的目的。

BloomFilter 是由 Bloom 在 1970 年提出的一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求 100% 正确的场合,BloomFilter 有以下特点:

  • 空间效率高的概率型数据结构,用来检查一个元素是否在一个集合中。

  • 对于一个元素检测是否存在的调用,BloomFilter 会告诉调用者两个结果之一:可能存在或者一定不存在。

BloomFilter 是由一个超长的二进制位数组和一系列的哈希函数组成。二进制位数组初始全部为 0,当给定一个待查询的元素时,这个元素会被一系列哈希函数计算映射出一系列的值,所有的值在位数组的偏移量处置为 1。

下图所示出一个 m=18, k=3(m 是该 Bit 数组的大小,k 是 Hash 函数的个数)的 BloomFilter 示例。集合中的 x、y、z 三个元素通过 3 个不同的哈希函数散列到位数组中。当查询元素 w 时,通过 Hash 函数计算之后只要有一个位为 0,因此 w 不在该集合中。但是反过来全部都是 1 只能说明可能在集合中、不能肯定一定在集合中,因为 Hash 函数可能出现 Hash 碰撞。

反过来如果某个元素经过哈希函数计算后得到所有的偏移位置,若这些位置全都为 1,只能说明可能在集合中、不能肯定一定在集合中,因为 Hash 函数可能出现 Hash 碰撞。这就是 BloomFilter"假阳性",因此基于 BloomFilter 的索引只能跳过不满足条件的数据,不能精确定位满足条件的数据。

Doris BloomFilter 索引以数据块(page)为单位构建,每个数据块存储一个 BloomFilter。写入时,对于数据块中的每个值,经过 Hash 存入数据块对应的 BloomFilter。查询时,根据等值条件的值,判断每个数据块对应的 BloomFilter 是否包含这个值,不包含则跳过对应的数据块不读取,达到减少 I/O 查询加速的目的。

4.4.3.2、使用场景

BloomFilter 索引能够对等值查询(包括 = 和 IN)加速,对高基数字段效果较好,比如 userid 等唯一 ID 字段。

BloomFilter 的使用有下面一些限制:

  • 对 IN 和 = 之外的查询没有效果,比如 !=, NOT INT, >, < 等

  • 不支持对 Tinyint、Float、Double 类型的列建 BloomFilter 索引。

  • 对低基数字段的加速效果很有限,比如"性别"字段仅有两种值,几乎每个数据块都会包含所有取值,导致 BloomFilter 索引失去意义。

如果要查看某个查询 BloomFilter 索引效果,可以通过 Query Profile 中的相关指标进行分析。

  • BlockConditionsFilteredBloomFilterTime 是 BloomFilter 索引消耗的时间

  • RowsBloomFilterFiltered 是 BloomFilter 过滤掉的行数,可以与其他几个 Rows 值对比分析 BloomFilter 索引过滤效果

4.4.3.3、管理索引
  • 建表时创建 BloomFilter 索引

由于历史原因,BloomFilter 索引定义的语法与倒排索引等通用 INDEX 语法不一样。BloomFilter 索引通过表的 PROPERTIES "bloom_filter_columns" 指定哪些字段建 BloomFilter 索引,可以指定一个或者多个字段。

sql 复制代码
PROPERTIES (
"bloom_filter_columns" = "column_name1,column_name2"
);
  • 查看 BloomFilter 索引
sql 复制代码
SHOW CREATE TABLE table_name;
  • 已有表增加、删除 BloomFilter 索引

通过 ALTER TABLE 修改表的 bloom_filter_columns 属性来完成。

为 column_name3 增加 BloomFilter 索引

sql 复制代码
ALTER TABLE table_name SET ("bloom_filter_columns" = "column_name1,column_name2,column_name3");

删除 column_name1 的 BloomFilter 索引

sql 复制代码
ALTER TABLE table_name SET ("bloom_filter_columns" = "column_name2,column_name3");
4.4.3.4、使用索引

BloomFilter 索引用于加速 WHERE 条件中的等值查询,能加速时自动生效,没有特殊语法。

可以通过 Query Profile 中的下面几个指标分析 BloomFilter 索引的加速效果。

  • RowsBloomFilterFiltered BloomFilter 索引过滤掉的行数,可以与其他几个 Rows 值对比分析索引过滤效果
  • BlockConditionsFilteredBloomFilterTime BloomFilter 索引消耗的时间
4.4.3.5、使用示例

下面通过实例来看看 Doris 怎么创建 BloomFilter 索引。

Doris BloomFilter 索引的创建是通过在建表语句的 PROPERTIES 里加上 "bloom_filter_columns"="k1,k2,k3", 这个属性,k1,k2,k3 是要创建的 BloomFilter 索引的 Key 列名称,例如下面对表里的 saler_id,category_id 创建了 BloomFilter 索引。

sql 复制代码
CREATE TABLE IF NOT EXISTS sale_detail_bloom  (
    sale_date date NOT NULL COMMENT "销售时间",
    customer_id int NOT NULL COMMENT "客户编号",
    saler_id int NOT NULL COMMENT "销售员",
    sku_id int NOT NULL COMMENT "商品编号",
    category_id int NOT NULL COMMENT "商品分类",
    sale_count int NOT NULL COMMENT "销售数量",
    sale_price DECIMAL(12,2) NOT NULL COMMENT "单价",
    sale_amt DECIMAL(20,2)  COMMENT "销售总金额"
)
Duplicate  KEY(sale_date, customer_id,saler_id,sku_id,category_id)
DISTRIBUTED BY HASH(saler_id) BUCKETS 10
PROPERTIES (
"replication_num" = "1",
"bloom_filter_columns"="saler_id,category_id"
);

4.4.4、N-Gram 索引

4.4.4.1、索引原理

n-gram 分词是将一句话或一段文字拆分成多个相邻的词组的分词方法。NGram BloomFilter 索引和 BloomFilter 索引类似,也是基于 BloomFilter 的跳数索引。

与 BloomFilter 索引不同的是,NGram BloomFilter 索引用于加速文本 LIKE 查询,它存入 BloomFilter 的不是原始文本的值,而是对文本进行 NGram 分词,每个词作为值存入 BloomFilter。对于 LIKE 查询,将 LIKE '%pattern%' 的 pattern 也进行 NGram 分词,判断每个词是否在 BloomFilter 中,如果某个词不在则对应的数据块就不满足 LIKE 条件,可以跳过这部分数据减少 IO 加速查询。

4.4.4.2、使用场景

NGram BloomFilter 索引只能加速字符串 LIKE 查询,而且 LIKE pattern 中的连续字符个数要大于等于索引定义的 NGram 中的 N。

  • NGram BloomFilter 只支持字符串列,只能加速 LIKE 查询。

  • NGram BloomFilter 索引和 BloomFilter 索引为互斥关系,即同一个列只能设置两者中的一个。

  • NGram BloomFilter 索引的效果分析,跟 BloomFilter 索引类似。

4.4.4.3、管理索引
  • 创建 NGram BloomFilter 索引

在建表语句中 COLUMN 的定义之后是索引定义:

sql 复制代码
  INDEX `idx_column_name` (`column_name`) USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="1024") COMMENT 'username ngram_bf index'

语法说明如下:

  1. idx_column_name(column_name) 是必须的,column_name 是建索引的列名,必须是前面列定义中出现过的,idx_column_name 是索引名字,必须表级别唯一,建议命名规范:列名前面加前缀 idx_

  2. USING NGRAM_BF 是必须的,用于指定索引类型是 NGram BloomFilter 索引

  3. PROPERTIES 是可选的,用于指定 NGram BloomFilter 索引的额外属性,目前支持的属性如下:

    • gram_size:NGram 中的 N,指定 N 个连续字符分词一个词,比如 'This is a simple ngram example' 在 N = 3 的时候分成 'This is a', 'is a simple', 'a simple ngram', 'simple ngram example' 4 个词。

    • bf_size:BloomFilter 的大小,单位是 Bit。bf_size 决定每个数据块对应的索引大小,这个值越大占用存储空间越大,同时 Hash 碰撞的概率也越低。

    • gram_size 建议取 LIKE 查询的字符串最小长度,但是不建议低于 2。一般建议设置 "gram_size"="3", "bf_size"="1024",然后根据 Query Profile 调优。

  4. COMMENT 是可选的,用于指定索引注释

  • 查看 NGram BloomFilter 索引
sql 复制代码
-- 语法 1,表的 schema 中 INDEX 部分 USING NGRAM_BF 是倒排索引
SHOW CREATE TABLE table_name;

-- 语法 2,IndexType 为 NGRAM_BF 的是倒排索引
SHOW INDEX FROM idx_name;
  • 删除 NGram BloomFilter 索引
sql 复制代码
ALTER TABLE table_ngrambf DROP INDEX idx_ngrambf;
  • 修改 NGram BloomFilter 索引
sql 复制代码
CREATE INDEX idx_column_name2(column_name2) ON table_ngrambf USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="1024") COMMENT 'username ngram_bf index';

ALTER TABLE table_ngrambf ADD INDEX idx_column_name2(column_name2) USING NGRAM_BF PROPERTIES("gram_size"="3", "bf_size"="1024") COMMENT 'username ngram_bf index';
4.4.4.4、使用索引

使用 NGram BloomFilter 索引需设置如下参数(enable_function_pushdown 默认为 false):

sql 复制代码
SET enable_function_pushdown = true;

NGram BloomFilter 索引用于加速 LIKE 查询,比如:

sql 复制代码
SELECT count() FROM table1 WHERE message LIKE '%error%';

可以通过 Query Profile 中的下面几个指标分析 BloomFilter 索引(包括 NGram)的加速效果。

  • RowsBloomFilterFiltered BloomFilter 索引过滤掉的行数,可以与其他几个 Rows 值对比分析索引过滤效果
  • BlockConditionsFilteredBloomFilterTime BloomFilter 索引消耗的时间
4.4.4.5、使用示例

以亚马逊产品的用户评论信息的数据集 amazon_reviews 为例展示 NGram BloomFilter 索引的使用和效果。

  • 建表
sql 复制代码
CREATE TABLE `amazon_reviews` (  
  `review_date` int(11) NULL,  
  `marketplace` varchar(20) NULL,  
  `customer_id` bigint(20) NULL,  
  `review_id` varchar(40) NULL,
  `product_id` varchar(10) NULL,
  `product_parent` bigint(20) NULL,
  `product_title` varchar(500) NULL,
  `product_category` varchar(50) NULL,
  `star_rating` smallint(6) NULL,
  `helpful_votes` int(11) NULL,
  `total_votes` int(11) NULL,
  `vine` boolean NULL,
  `verified_purchase` boolean NULL,
  `review_headline` varchar(500) NULL,
  `review_body` string NULL
) ENGINE=OLAP
DUPLICATE KEY(`review_date`)
COMMENT 'OLAP'
DISTRIBUTED BY HASH(`review_date`) BUCKETS 16
PROPERTIES (
"replication_allocation" = "tag.location.default: 1",
"compression" = "ZSTD"
);
  • 导入数据

用 wget 或者其他工具从下面的地址下载数据集

sql 复制代码
https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_2010.snappy.parquet
https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_2011.snappy.parquet
https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_2012.snappy.parquet
https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_2013.snappy.parquet
https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_2014.snappy.parquet
https://datasets-documentation.s3.eu-west-3.amazonaws.com/amazon_reviews/amazon_reviews_2015.snappy.parquet

用 stream load 导入数据

sql 复制代码
curl --location-trusted -u root: -T amazon_reviews_2010.snappy.parquet -H "format:parquet" http://127.0.0.1:8030/api/${DB}/amazon_reviews/_stream_load
curl --location-trusted -u root: -T amazon_reviews_2011.snappy.parquet -H "format:parquet" http://127.0.0.1:8030/api/${DB}/amazon_reviews/_stream_load
curl --location-trusted -u root: -T amazon_reviews_2012.snappy.parquet -H "format:parquet" http://127.0.0.1:8030/api/${DB}/amazon_reviews/_stream_load
curl --location-trusted -u root: -T amazon_reviews_2013.snappy.parquet -H "format:parquet" http://127.0.0.1:8030/api/${DB}/amazon_reviews/_stream_load
curl --location-trusted -u root: -T amazon_reviews_2014.snappy.parquet -H "format:parquet" http://127.0.0.1:8030/api/${DB}/amazon_reviews/_stream_load
curl --location-trusted -u root: -T amazon_reviews_2015.snappy.parquet -H "format:parquet" http://127.0.0.1:8030/api/${DB}/amazon_reviews/_stream_load

SQL 运行 count() 确认导入数据成功

sql 复制代码
mysql> SELECT COUNT() FROM amazon_reviews;
+-----------+
| count(*)  |
+-----------+
| 135589433 |
+-----------+
  • 查询

首先在没有索引的时候运行查询,WHERE 条件中有 LIKE,耗时 7.60s

sql 复制代码
SELECT
    product_id,
    any(product_title),
    AVG(star_rating) AS rating,
    COUNT() AS count
FROM
    amazon_reviews
WHERE
    review_body LIKE '%is super awesome%'
GROUP BY
    product_id
ORDER BY
    count DESC,
    rating DESC,
    product_id
LIMIT 5;


+------------+------------------------------------------+--------------------+-------+
| product_id | any_value(product_title)                 | rating             | count |
+------------+------------------------------------------+--------------------+-------+
| B00992CF6W | Minecraft                                | 4.8235294117647056 |    17 |
| B009UX2YAC | Subway Surfers                           | 4.7777777777777777 |     9 |
| B00DJFIMW6 | Minion Rush: Despicable Me Official Game |              4.875 |     8 |
| B0086700CM | Temple Run                               |                  5 |     6 |
| B00KWVZ750 | Angry Birds Epic RPG                     |                  5 |     6 |
+------------+------------------------------------------+--------------------+-------+
5 rows in set (7.60 sec)

然后添加 NGram BloomFilter 索引,再次运行相同的查询耗时 0.93s,性能提升了 8 倍

sql 复制代码
ALTER TABLE amazon_reviews ADD INDEX review_body_ngram_idx(review_body) USING NGRAM_BF PROPERTIES("gram_size"="10", "bf_size"="10240");
sql 复制代码
+------------+------------------------------------------+--------------------+-------+
| product_id | any_value(product_title)                 | rating             | count |
+------------+------------------------------------------+--------------------+-------+
| B00992CF6W | Minecraft                                | 4.8235294117647056 |    17 |
| B009UX2YAC | Subway Surfers                           | 4.7777777777777777 |     9 |
| B00DJFIMW6 | Minion Rush: Despicable Me Official Game |              4.875 |     8 |
| B0086700CM | Temple Run                               |                  5 |     6 |
| B00KWVZ750 | Angry Birds Epic RPG                     |                  5 |     6 |
+------------+------------------------------------------+--------------------+-------+
5 rows in set (0.93 sec)

4.5、自增列

在 Doris 中,自增列(Auto Increment Column)是一种自动生成唯一数字值的功能,常用于为每一行数据生成唯一的标识符,如主键。每当插入新记录时,自增列会自动分配一个递增的值,避免了手动指定数字的繁琐操作。使用 Doris 自增列,可以确保数据的唯一性和一致性,简化数据插入过程,减少人为错误,并提高数据管理的效率。这使得自增列成为处理需要唯一标识的场景(如用户 ID 等)时的理想选择。

4.5.1、功能

对于具有自增列的表,Doris 处理数据写入的方式如下:

  • 自动填充(列排除): 如果写入的数据不包括自增列,Doris 会生成并填充该列的唯一值。

  • 部分指定(列包含)

    • 空值:Doris 会用系统生成的唯一值替换写入数据中的空值。
    • 非空值:用户提供的值保持不变。用户提供的非空值可能会破坏自增列的唯一性。

唯一性

Doris 保证自增列中生成的值具有表级唯一性。但是:

  • 保证唯一性:这仅适用于系统生成的值。
  • 用户提供的值:Doris 不会验证或强制执行用户在自增列中指定的值的唯一性。这可能导致重复条目。

聚集性

Doris 生成的自增值通常是密集的,但有一些考虑:

  • 潜在的间隙:由于性能优化,可能会出现间隙。每个后端节点(BE)会预分配一块唯一值以提高效率,这些块在节点之间不重叠。

  • 非时间顺序值:Doris 不保证后续写入生成的值大于早期写入的值。自增值不能用于推断写入的时间顺序。

4.5.2、语法

要使用自增列,需要在建表CREATE-TABLE时为对应的列添加AUTO_INCREMENT属性。若要手动指定自增列起始值,可以通过建表时AUTO_INCREMENT(start_value)语句指定,如果未指定,则默认起始值为 1。

示例

  1. 创建一个 Dupliciate 模型表,其中一个 key 列是自增列
sql 复制代码
CREATE TABLE `demo`.`tbl` (
      `id` BIGINT NOT NULL AUTO_INCREMENT,
      `value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
  1. 创建一个 Dupliciate 模型表,其中一个 key 列是自增列,并设置起始值为 100
sql 复制代码
CREATE TABLE `demo`.`tbl` (
      `id` BIGINT NOT NULL AUTO_INCREMENT(100),
      `value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
  1. 创建一个 Dupliciate 模型表,其中一个 value 列是自增列
sql 复制代码
CREATE TABLE `demo`.`tbl` (
      `uid` BIGINT NOT NULL,
      `name` BIGINT NOT NULL,
      `id` BIGINT NOT NULL AUTO_INCREMENT,
      `value` BIGINT NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`uid`, `name`)
DISTRIBUTED BY HASH(`uid`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
  1. 创建一个 Unique 模型表,其中一个 key 列是自增列
sql 复制代码
CREATE TABLE `demo`.`tbl` (
      `id` BIGINT NOT NULL AUTO_INCREMENT,
      `name` varchar(65533) NOT NULL,
      `value` int(11) NOT NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);
  1. 创建一个 Unique 模型表,其中一个 value 列是自增列
sql 复制代码
CREATE TABLE `demo`.`tbl` (
      `text` varchar(65533) NOT NULL,
      `id` BIGINT NOT NULL AUTO_INCREMENT,
) ENGINE=OLAP
UNIQUE KEY(`text`)
DISTRIBUTED BY HASH(`text`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

约束和限制

  1. 仅 Duplicate 模型表和 Unique 模型表可以包含自增列。
  2. 一张表最多只能包含一个自增列。
  3. 自增列的类型必须是 BIGINT 类型,且必须为 NOT NULL。
  4. 自增列手动指定的起始值必须大于等于 0。

4.5.3、使用方式

以下表为例:

sql 复制代码
CREATE TABLE `demo`.`tbl` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `name` varchar(65533) NOT NULL,
    `value` int(11) NOT NULL
) ENGINE=OLAP
UNIQUE KEY(`id`)
DISTRIBUTED BY HASH(`id`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 3"
);

使用 insert into 语句导入并且不指定自增列id时,id列会被自动填充生成的值。

sql 复制代码
insert into tbl(name, value) values("Bob", 10), ("Alice", 20), ("Jack", 30);

select * from tbl order by id;
+------+-------+-------+
| id   | name  | value |
+------+-------+-------+
|    1 | Bob   |    10 |
|    2 | Alice |    20 |
|    3 | Jack  |    30 |
+------+-------+-------+

4.6、冷热数据分层

4.6.1、冷热数据分层概述

为了帮助用户节省存储成本,Doris 针对冷数据提供了灵活的选择。

冷数据选择 适用条件 特性
存算分离 用户具备部署存算分离的条件 - 数据以单副本完全存储在对象存储中 - 通过本地缓存加速热数据访问 - 存储与计算资源独立扩展,显著降低存储成本
本地分层 存算一体模式下,用户希望进一步优化本地存储资源 - 支持将冷数据从 SSD(固态硬盘) 冷却到 HDD(机械硬盘) - 充分利用本地存储层级特性,节省高性能存储成本
远程分层 存算一体模式下,使用廉价的对象存储或者 HDFS 进一步降低成本 - 冷数据以单副本形式保存到对象存储或者 HDFS 中 - 热数据继续使用本地存储 - 不能对一个表和本地分层混合使用

通过上述模式,Doris 能够灵活适配用户的部署条件,实现查询效率与存储成本的平衡。

4.6.2、SSD 和 HDD 层级存储

Doris 支持在不同磁盘类型(SSD 和 HDD)之间进行分层存储,结合动态分区功能,根据冷热数据的特性将数据从 SSD 动态迁移到 HDD。这种方式既降低了存储成本,又在热数据的读写上保持了高性能。

4.6.2.1、动态分区与层级存储

通过配置动态分区参数,用户可以设置哪些分区存储在 SSD 上,以及冷却后自动迁移到 HDD 上。

  • 热分区:最近活跃的分区,优先存储在 SSD 上,保证高性能。
  • 冷分区:较少访问的分区,会逐步迁移到 HDD,以降低存储开销。
4.6.2.2、参数说明

dynamic_partition.hot_partition_num

  • 功能:

    • 指定最近的多少个分区为热分区,这些分区存储在 SSD 上,其余分区存储在 HDD 上。
  • 注意:

    • 必须同时设置 dynamic_partition.storage_medium = HDD,否则此参数不会生效。
    • 如果存储路径下没有 SSD 设备,则该配置会导致分区创建失败。

示例说明:

假设当前日期为 2021-05-20,按天分区,动态分区配置如下:

sql 复制代码
dynamic_partition.hot_partition_num = 2
dynamic_partition.start = -3
dynamic_partition.end = 3

系统会自动创建以下分区,并配置其存储介质和冷却时间:

sql 复制代码
p20210517:["2021-05-17", "2021-05-18") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
p20210518:["2021-05-18", "2021-05-19") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
p20210519:["2021-05-19", "2021-05-20") storage_medium=SSD storage_cooldown_time=2021-05-21 00:00:00
p20210520:["2021-05-20", "2021-05-21") storage_medium=SSD storage_cooldown_time=2021-05-22 00:00:00
p20210521:["2021-05-21", "2021-05-22") storage_medium=SSD storage_cooldown_time=2021-05-23 00:00:00
p20210522:["2021-05-22", "2021-05-23") storage_medium=SSD storage_cooldown_time=2021-05-24 00:00:00
p20210523:["2021-05-23", "2021-05-24") storage_medium=SSD storage_cooldown_time=2021-05-25 00:00:00

dynamic_partition.storage_medium

  • 功能:

    • 指定动态分区的最终存储介质。默认是 HDD,可选择 SSD。
  • 注意:

    • 当设置为 SSD 时,hot_partition_num 属性将不再生效,所有分区将默认为 SSD 存储介质并且冷却时间为 9999-12-31 23:59:59。
4.6.2.3、示例
  1. 创建一个分层存储表
sql 复制代码
 CREATE TABLE tiered_table (k DATE)
  PARTITION BY RANGE(k)()
  DISTRIBUTED BY HASH (k) BUCKETS 5
  PROPERTIES
  (
      "dynamic_partition.storage_medium" = "hdd",
      "dynamic_partition.enable" = "true",
      "dynamic_partition.time_unit" = "DAY",
      "dynamic_partition.hot_partition_num" = "2",
      "dynamic_partition.end" = "3",
      "dynamic_partition.prefix" = "p",
      "dynamic_partition.buckets" = "5",
      "dynamic_partition.create_history_partition"= "true",
      "dynamic_partition.start" = "-3"
  );
  1. 检查分区存储介质
sql 复制代码
 SHOW PARTITIONS FROM tiered_table;

可以看见 7 个分区,5 个使用 SSD, 其它的 2 个使用 HDD。

sql 复制代码
  p20210517:["2021-05-17", "2021-05-18") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
  p20210518:["2021-05-18", "2021-05-19") storage_medium=HDD storage_cooldown_time=9999-12-31 23:59:59
  p20210519:["2021-05-19", "2021-05-20") storage_medium=SSD storage_cooldown_time=2021-05-21 00:00:00
  p20210520:["2021-05-20", "2021-05-21") storage_medium=SSD storage_cooldown_time=2021-05-22 00:00:00
  p20210521:["2021-05-21", "2021-05-22") storage_medium=SSD storage_cooldown_time=2021-05-23 00:00:00
  p20210522:["2021-05-22", "2021-05-23") storage_medium=SSD storage_cooldown_time=2021-05-24 00:00:00
  p20210523:["2021-05-23", "2021-05-24") storage_medium=SSD storage_cooldown_time=2021-05-25 00:00:00

参数详解:

  1. dynamic_partition.time_unit

指定动态分区的时间粒度。这里设置为 DAY,表示按天来创建和管理分区。可选值包括 DAY、WEEK、MONTH。

  1. dynamic_partition.end

预先创建的未来分区的时间范围(相对于当前时间)。这里的 3 表示创建从当前时间(不含)开始,到未来第3天(含)的分区。

计算示例:

如果今天是 2025-07-31,动态分区功能会确保以下未来分区存在:

  • p20250801(明天,未来第1天)
  • p20250802(后天,未来第2天)
  • p20250803(大后天,未来第3天)
  1. dynamic_partition.start

动态分区管理的历史分区的时间范围(相对于当前时间)。这里的 -3 表示管理从当前时间(含)开始,回溯到3天前(含)的分区。结合 end 定义了动态分区管理的总时间窗口。

计算示例:

如果今天是 2025-07-31,动态分区管理的时间窗口是:

  • p20250728(3天前)
  • p20250729(2天前)
  • p20250730(1天前)
  • p20250731(今天)
  • p20250801(明天)
  • p20250802(后天)
  • p20250803(大后天)

总共 7 个分区(3过去 + 1今天 + 3未来)。

注意:start 为负数表示过去,end 为正数表示未来,时间窗口包含 start 和 end 定义的天数。

  1. dynamic_partition.prefix

动态生成的分区名称的前缀。分区名称由 prefix + 分区对应日期的字符串组成。

示例:对于日期 2025-07-31,分区名将是 p20250731。

  1. dynamic_partition.buckets

动态创建的分区所使用的分桶数量。此值会覆盖表级 DISTRIBUTED BY ... BUCKETS ... 语句中指定的分桶数(在动态分区场景下)。 确保每个新创建的动态分区都使用 5 个 Bucket 进行数据分布。

与建表语句关联:建表语句中的 DISTRIBUTED BY HASH (k) BUCKETS 5 设置了表级默认分桶数和分桶列。动态分区的 buckets 参数 5 确保每个动态分区继承此分桶数。如果设置为 10,动态分区会用 10 个 Bucket,而建表语句的 BUCKETS 5 只对非动态分区或初始分区有效。

  1. dynamic_partition.create_history_partition

启用动态分区功能时,是否自动创建历史分区。

  • true:启用动态分区时,Doris 会立即根据 start 和 end 的值,创建当前时间窗口 [start, end] 内所有还不存在的分区(包括历史分区和未来分区)。
  • false:只创建未来的分区([当前日期+1, end]),历史分区需手动创建或通过数据导入触发创建。

本例设置为 true,结合 start=-3 和 end=3,在启用动态分区的瞬间,Doris 会自动创建 p20250728、p20250729、p20250730、p20250731、p20250801、p20250802、p20250803 这 7 个分区(如果它们之前不存在)。

  1. dynamic_partition.hot_partition_num

定义热分区的数量。热分区是指距离当前时间最近的几个分区。

计算:热分区范围是 [当前日期 - (hot_partition_num - 1), 当前日期]。

本例 hot_partition_num=2,如果今天是 2025-07-31,则热分区是:

  • p20250730(昨天)
  • p20250731(今天)

作用:Doris 内部可能对热分区进行优化,例如存储策略、查询优化器等。

限制:hot_partition_num 的值必须小于等于 -dynamic_partition.start(即 start 负数的绝对值)。本例中 -start=3,hot_partition_num=2(2 ≤ 3)合法;若设置为 4(4 > 3)则为非法配置。

  1. dynamic_partition.storage_medium

动态创建的分区使用的初始存储介质。可选值为 SSD(固态硬盘)或 HDD(机械硬盘)。

作用:结合 hot_partition_num 和 Doris 的存储策略功能,可后续通过额外配置自动迁移数据(如热分区到 SSD、冷分区回 HDD)。此参数仅设置分区初始存储位置。

注意:需 BE 节点的存储路径配置同时指定 SSD 和 HDD 才有效。若所有存储路径均为 HDD,设置为 SSD 也不会生效。

4.6.3、远程存储

远程存储支持将冷数据放到外部存储(例如对象存储,HDFS)上。

远程存储的数据只有一个副本,数据可靠性依赖远程存储的数据可靠性,您需要保证远程存储有 ec(擦除码)或者多副本技术确保数据可靠性。

4.6.3.1、使用方法
  • 冷数据保存到 S3 兼容存储

第一步: 创建 S3 Resource。

sql 复制代码
CREATE RESOURCE "remote_s3"
PROPERTIES
(
    "type" = "s3",
    "s3.endpoint" = "bj.s3.com",
    "s3.region" = "bj",
    "s3.bucket" = "test-bucket",
    "s3.root.path" = "path/to/root",
    "s3.access_key" = "bbb",
    "s3.secret_key" = "aaaa",
    "s3.connection.maximum" = "50",
    "s3.connection.request.timeout" = "3000",
    "s3.connection.timeout" = "1000"
);

创建 S3 RESOURCE 的时候,会进行 S3 远端的链接校验,以保证 RESOURCE 创建的正确。

第二步: 创建 STORAGE POLICY。

之后创建 STORAGE POLICY,关联上文创建的 RESOURCE:

sql 复制代码
CREATE STORAGE POLICY test_policy
PROPERTIES(
    "storage_resource" = "remote_s3",
    "cooldown_ttl" = "1d"
);

第三步: 建表时使用 STORAGE POLICY。

sql 复制代码
CREATE TABLE IF NOT EXISTS create_table_use_created_policy 
(
    k1 BIGINT,
    k2 LARGEINT,
    v1 VARCHAR(2048)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH (k1) BUCKETS 3
PROPERTIES(
    "enable_unique_key_merge_on_write" = "false",
    "storage_policy" = "test_policy"
);

注意:UNIQUE 表如果设置了 "enable_unique_key_merge_on_write" = "true" 的话,无法使用此功能。

  • 冷数据保存到 HDFS(分布式文件系统)

第一步: 创建 HDFS RESOURCE:

sql 复制代码
CREATE RESOURCE "remote_hdfs" PROPERTIES (
        "type"="hdfs",
        "fs.defaultFS"="fs_host:default_fs_port",
        "hadoop.username"="hive",
        "hadoop.password"="hive",
        "root_path"="/my/root/path",
        "dfs.nameservices" = "my_ha",
        "dfs.ha.namenodes.my_ha" = "my_namenode1, my_namenode2",
        "dfs.namenode.rpc-address.my_ha.my_namenode1" = "nn1_host:rpc_port",
        "dfs.namenode.rpc-address.my_ha.my_namenode2" = "nn2_host:rpc_port",
        "dfs.client.failover.proxy.provider.my_ha" = "org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider"
    );

第二步: 创建 STORAGE POLICY。

sql 复制代码
CREATE STORAGE POLICY test_policy PROPERTIES (
    "storage_resource" = "remote_hdfs",
    "cooldown_ttl" = "300"
)

第三步: 使用 STORAGE POLICY 创建表。

sql 复制代码
CREATE TABLE IF NOT EXISTS create_table_use_created_policy (
    k1 BIGINT,
    k2 LARGEINT,
    v1 VARCHAR(2048)
)
UNIQUE KEY(k1)
DISTRIBUTED BY HASH (k1) BUCKETS 3
PROPERTIES(
"enable_unique_key_merge_on_write" = "false",
"storage_policy" = "test_policy"
);

注意:UNIQUE 表如果设置了 "enable_unique_key_merge_on_write" = "true" 的话,无法使用此功能。

  • 存量表冷却到远程存储

除了新建表支持设置远程存储外,Doris 还支持对一个已存在的表或者 PARTITION,设置远程存储。

对一个已存在的表,设置远程存储,将创建好的 STORAGE POLICY 与表关联:

sql 复制代码
ALTER TABLE create_table_not_have_policy set ("storage_policy" = "test_policy");

对一个已存在的 PARTITION,设置远程存储,将创建好的 STORAGE POLICY 与 PARTITON 关联:

sql 复制代码
ALTER TABLE create_table_partition MODIFY PARTITION (*) SET("storage_policy"="test_policy");

注意,如果用户在建表时给整张 Table 和部分 Partition 指定了不同的 Storage Policy,Partition 设置的 Storage policy 会被无视,整张表的所有 Partition 都会使用 table 的 Policy. 如果您需要让某个 Partition 的 Policy 和别的不同,则可以使用上文中对一个已存在的 Partition,关联 Storage policy 的方式修改。

  • 配置 compaction

    • BE 参数cold_data_compaction_thread_num可以设置执行远程存储的 Compaction 的并发,默认是 2。

    • BE 参数cold_data_compaction_interval_sec可以设置执行远程存储的 Compaction 的时间间隔,默认是 1800,单位:秒,即半个小时。

4.6.3.2、限制
  • 使用了远程存储的表不支持备份。

  • 不支持修改远程存储的位置信息,比如 endpoint、bucket、path。

  • Unique 模型表在开启 Merge-on-Write 特性时,不支持设置远程存储。

  • Storage policy 支持创建、修改和删除,删除前需要先保证没有表引用此 Storage policy。

4.6.3.3、冷数据空间

查看

方式一:通过 show proc '/backends'可以查看到每个 BE 上传到对象的大小,RemoteUsedCapacity 项,此方式略有延迟。

方式二:通过 show tablets from tableName 可以查看到表的每个 tablet 占用的对象大小,RemoteDataSize 项。

垃圾回收

远程存储上可能会有如下情况产生垃圾数据:

  1. 上传 rowset 失败但是有部分 segment 上传成功。
  2. 上传的 rowset 没有在多副本达成一致。
  3. Compaction 完成后,参与 compaction 的 rowset。

垃圾数据并不会立即清理掉。BE 参数remove_unused_remote_files_interval_sec可以设置远程存储的垃圾回收的时间间隔,默认是 21600,单位:秒,即 6 个小时。

4.6.3.4、查询与性能优化

为了优化查询的性能和对象存储资源节省,引入了本地 Cache。在第一次查询远程存储的数据时,Doris 会将远程存储的数据加载到 BE 的本地磁盘做缓存,Cache 有以下特性:

  • Cache 实际存储于 BE 本地磁盘,不占用内存空间。

  • Cache 是通过 LRU 管理的,不支持 TTL。

相关推荐
SickeyLee5 小时前
产品经理的成长路径与目标总结
大数据·人工智能
苹果企业签名分发6 小时前
腾讯云市场排名
大数据
沫儿笙7 小时前
OTC焊接机器人节能技巧
大数据·人工智能·机器人
lifallen7 小时前
深入解析RocksDB的MVCC和LSM Tree level
大数据·数据结构·数据库·c++·lsm-tree·lsm tree
金融小师妹7 小时前
AI量化模型解析黄金3300关口博弈:市场聚焦“非农数据”的GRU-RNN混合架构推演
大数据·人工智能·算法
金融小师妹7 小时前
基于LSTM-GRU混合网络的动态解析:美联储维稳政策与黄金单日跌1.5%的非线性关联
大数据·人工智能·算法
Aurora_NeAr8 小时前
大数据之路:阿里巴巴大数据实践——OneData数据中台体系
大数据
黄雪超9 小时前
Kafka——关于Kafka动态配置
大数据·分布式·kafka
大公产经晚间消息10 小时前
网易云音乐硬刚腾讯系!起诉SM娱乐滥用市场支配地位
大数据·人工智能·娱乐