Doris数据库基本概念

Doris官方中文文档:https://doris.apache.org/zh-CN/docs/3.x/gettingStarted/what-is-apache-doris

Apache Doris 是一款基于 MPP (Massively Parallel Processing,大规模并行处理,通过将任务并行分散到多个服务器或节点上,每个节点独立计算后汇总结果,以提升整体性能)架构的高性能、实时分析型数据库。它以高效、简单和统一的特性著称,能够在亚秒级的时间内返回海量数据的查询结果。Doris 既能支持高并发的点查询场景,也能支持高吞吐的复杂分析场景。基于这些优势,Apache Doris 非常适合用于报表分析、即席查询、统一数仓构建、数据湖联邦查询加速等场景。用户可以基于 Doris 构建大屏看板、用户行为分析、AB 实验平台、日志检索分析、用户画像分析、订单分析等应用。

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

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

Apache Doris 主要应用于以下场景:

  1. 实时数据分析:
    1. 实时报表与实时决策: 为企业内外部提供实时更新的报表和仪表盘,支持自动化流程中的实时决策需求。
    2. 交互式探索分析: 提供多维数据分析能力,支持对数据进行快速的商业智能分析和即席查询(Ad Hoc),帮助用户在复杂数据中快速发现洞察。
    3. 用户行为与画像分析: 分析用户参与、留存、转化等行为,支持人群洞察和人群圈选等画像分析场景。
  2. 湖仓融合分析:
    1. 湖仓查询加速: 通过高效的查询引擎加速湖仓数据的查询。
    2. 多源联邦分析: 支持跨多个数据源的联邦查询,简化架构并消除数据孤岛。
    3. 实时数据处理: 结合实时数据流和批量数据的处理能力,满足高并发和低延迟的复杂业务需求。
  3. 半结构化数据分析:
    1. 日志与事件分析: 对分布式系统中的日志和事件数据进行实时或批量分析,帮助定位问题和优化性能。

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、FE、BE

Apache Doris 采用 MySQL 协议,高度兼容 MySQL 语法,支持标准 SQL。用户可以通过各类客户端工具访问 Apache Doris,并支持与 BI (Business Intelligence)工具无缝集成(BI工具是商业智能工具,主要用于数据整合、分析与可视化,帮助企业优化决策‌)。在部署 Apache Doris 时,可以根据硬件环境与业务需求选择存算一体架构或存算分离架构。

存算一体架构

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 进程通过一致性协议来保证服务的高可用性和数据的高可靠性。存算一体架构高度集成,大幅降低了分布式系统的运维成本。

存算分离架构

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

  • 元数据层: 负责请求规划、查询解析以及元数据的存储和管理。
  • 计算层: 由多个计算组组成。每个计算组可以作为一个独立的租户承担业务计算。每个计算组包含多个无状态的 BE 节点,可以随时弹性伸缩 BE 节点。
  • 存储层: 可以使用 S3、HDFS、OSS、COS、OBS、Minio、Ceph 等共享存储来存放 Doris 的数据文件,包括 Segment 文件和反向索引文件等。

Doris 集群由以下两种节点组成:

  • FE 节点(Frontend):管理集群元数据(如表、分片),负责 SQL 的解析与执行规划。
  • BE 节点(Backend):存储数据,负责计算任务的执行。BE 的结果汇总后返回至 FE,再返回给用户。

在 Doris 集群中,FE 主要用于元数据存储,包括元数据 edit log 和 image。BE 节点存储分片的数据,每个分片是 Doris 中数据管理的最小单元,也是数据移动和复制的基本单位。BE 的磁盘空间主要用于存放数据,需要根据业务需求计算。简单来说,‌元数据(Metadata)就是"关于数据的数据",它描述了数据库集群的结构和属性,但不包含用户的实际业务数据。‌可以将其理解为一本书的‌目录和出版信息‌:

  • 书的正文内容‌ = BE上存储的‌用户业务数据‌(例如:用户的交易记录、日志详情)。
  • 书的目录、章节标题、页码、出版信息‌ = FE上存储的‌元数据‌(例如:数据库、表的名字、列的类型、数据存储的位置)。

元数据具体包含哪些内容? FE存储的元数据主要分为以下几大类:

  1. 集群拓扑信息‌

内容‌:记录了整个Doris集群由哪些节点组成,包括所有FE节点(Follower, Observer)和BE节点的IP地址、端口、状态(是否存活)等。

举例‌:假设集群有3个FE和5个BE,那么元数据中就记录着这8个节点的网络地址和角色。当有新的SQL请求进来时,FE根据这个信息就知道该和哪些BE通信。

  1. 数据库和表的Schema(模式)‌

这是元数据中最核心的部分,它定义了数据的结构,包括数据库级、表级、列级。

数据库级‌:数据库的名称、属性(如副本数、默认存储介质)。

表级‌:

  • 表名‌:例如 user_profile, order_records。
  • 表类型‌:是OLAP表(默认)、MySQL外部表、还是Elasticsearch外部表等。
  • 分桶与分区信息‌:表是如何被分区的(例如按日期PARTITION BY RANGE(dt)),以及每个分区内如何分桶(DISTRIBUTED BY HASH(user_id))。
  • 属性‌:副本数、存储冷却时间、动态分区规则等。

列级‌:

  • 列名和数据类型‌:例如 user_id INT, user_name VARCHAR(50), age SMALLINT, city STRING。
  • 聚合类型‌:这是Doris作为聚合模型的关键,例如 SUM, MAX, MIN, REPLACE。
  • 是否允许为NULL‌,‌默认值‌是什么。
  1. 数据分布与副本信息‌

这部分元数据记录了‌具体的用户数据存储在哪个BE的哪个位置‌。

举例‌:对于表 order_records 的 20241121 这个分区,它被分成了10个桶(Tablet)。元数据会精确记录:

  • Tablet 10001‌ 的副本存储在 BE_192.168.1.10 的 /path/to/data/ 目录下。
  • Tablet 10001‌ 的另一个副本(用于容灾)存储在 BE_192.168.1.11 的 /path/to/data/ 目录下。
  • Tablet 10002‌ 的副本存储在 BE_192.168.1.12 和 BE_192.168.1.13 上。
  • 当执行一个查询时,FE通过元数据定位到所需数据所在的Tablet和BE,然后将查询任务分发到对应的BE上执行。
  1. 作业与任务信息‌

记录数据导入(如Broker Load, Routine Load)、删除、Schema变更(如ALTER TABLE)等作业的状态和进度。

元数据如何持久化?------ Edit Log 和 Image

为了保证元数据的高可用和一致性,Doris使用了一种与大数据领域(如HDFS, ZooKeeper)类似的机制:Edit Log(编辑日志)和Image(镜像文件)。

  1. Edit Log(编辑日志)‌:记录所有‌更改‌元数据的‌操作‌(例如:创建表、添加列、导入数据导致数据版本更新)。

工作方式‌:类似于数据库的redo log或binlog。当元数据发生变更时,Leader FE会先将这个变更操作以日志的形式追加写入到Edit Log中,然后再应用到内存中的元数据结构。它是一个‌顺序写入的日志流‌。

举例‌:当执行 ALTER TABLE user_profile ADD COLUMN phone VARCHAR(15); 时,这个"添加列"的指令就会被写入Edit Log。

  1. Image(镜像文件)‌:某一时刻内存中完整元数据结构的‌快照‌。

工作方式‌:由于Edit Log会不断增长,如果每次启动都从头回放所有日志会非常慢。因此,FE会定期将内存中的元数据序列化到磁盘上,生成一个Image文件。下次启动时,只需要加载最新的Image文件,然后回放这个Image之后新增的Edit Log即可,大大加快了恢复速度。

举例‌:Image文件就像是游戏存档,而Edit Log是存档之后所有操作记录。

|-------|------------------------------|----------------------|
| 特性 | FE(元数据) | BE(用户数据) |
| 存储内容‌ | 集群结构、库表Schema、数据位置索引 | 实际的业务数据行和列块 |
| 数据性质‌ | "目录"、"地图"、"蓝图" | "货物"、"正文内容" |
| 存储形式‌ | Edit Log(操作日志) + Image(内存快照) | 列式存储文件(如Segment文件) |
| 容量估算‌ | 相对较小,通常GB级别以内 | 根据业务数据量决定,可能达TB/PB级别 |

在规划Doris集群时,‌FE的磁盘不需要很大,但需要有高可用性(建议使用高性能SSD并部署多个Follower)‌,因为它的元数据是整个集群的"大脑"。而‌BE的磁盘空间需要根据业务数据的原始大小、副本数量、压缩比和未来增长来仔细计算‌。

Apache Arrow 是一种为大数据分析设计的、跨平台的、内存中的列式数据格式。它的核心目标是消除不同系统之间数据交换时的序列化和反序列化开销。

核心思想:‌

  • 列式存储‌:与行式存储(如 JDBC ResultSet,它是一行一行地传输数据)不同,Arrow 按列来组织和传输数据。
  • 内存连续‌:每一列的数据在内存中是连续存储的,这非常符合现代 CPU 的缓存友好特性,便于向量化计算。

举例说明:‌

假设有一个简单的用户表,结构如下:

|---------|-----------|-----|-----------|
| user_id | user_name | age | city |
| 101 | Alice | 25 | Beijing |
| 102 | Bob | 30 | Shanghai |
| 103 | Charlie | 28 | Guangzhou |

  1. 在 JDBC ResultSet(行格式)中,数据是这样传输和解析的:‌

传输流‌:(101, "Alice", 25, "Beijing") -> (102, "Bob", 30, "Shanghai") -> (103, "Charlie", 28, "Guangzhou")

客户端解析‌:客户端需要逐行读取这个流,对于每一行,都要解析出各个字段的类型和值(例如,从二进制或文本中解析出 INT, VARCHAR, INT, VARCHAR)。这个过程涉及大量的类型检查和内存跳转。

  1. 在 Arrow(列格式)中,数据在内存中的布局是这样的:‌

它不是一个整体的流,而是‌多个并行的列缓冲区‌。

列 user_id‌:

  • 数据缓冲区‌: [101, 102, 103]
  • 由于都是整数,它在内存中就是一段连续的整数数组。

列 user_name‌:

  • 偏移量缓冲区‌: [0, 5, 8] (表示第一个字符串从0开始,长度5;第二个从5开始,长度3;第三个从8开始,长度7)
  • 数据缓冲区‌: "AliceBobCharlie"
  • 所有字符串值被拼接成一个大的字节数组,通过偏移量来定位每个值。

列 age‌:

  • 数据缓冲区‌: [25, 30, 28]
  • 一段连续的整数数组。

列 city‌:

  • 偏移量缓冲区‌: [0, 7, 15]
  • 数据缓冲区‌: "BeijingShanghaiGuangzhou"

Arrow 的优势在于:‌

  • 零反序列化‌:如果客户端(如一个数据分析程序)本身也使用 Arrow 内存格式,那么从 Arrow Flight SQL 服务端接收到的数据可以直接映射到内存,无需任何解析即可进行计算。这被称为"零拷贝"。
  • 向量化处理‌:程序可以一次性对整个列 age 的数据缓冲区 [25, 30, 28] 进行操作,例如直接计算平均值,这比在行格式中循环遍历每一行并提取 age 字段要高效得多。
  • 高效的传输‌:Flight 协议直接传输这些内存缓冲区,效率极高。

2、表类型

在 Doris 中建表时需要指定表类型,以定义数据存储与管理方式。在 Doris 中提供了明细表、聚合表以及主键表三种表类型,可以应对不同的应用场景需求。不同的表类型具有相应的数据去重、聚合及更新机制。选择合适的表类型有助于实现业务目标,同时保证数据处理的灵活性和高效性。

下面看一个简单的建表语句:

sql 复制代码
CREATE TABLE IF NOT EXISTS `user_actions` (
    `user_id` BIGINT,
    `item_id` BIGINT,
    `action_time` DATETIME,
    `action_type` VARCHAR(32),
    `city` VARCHAR(32)
) ENGINE=OLAP
DUPLICATE KEY(`user_id`, `item_id`, `action_time`);

Apache Doris中ENGINE=OLAP表示使用OLAP(Online Analytical Processing,在线分析处理)存储引擎,这是Doris的核心引擎,专为分析型查询优化,支持大规模数据聚合和复杂查询,与OLTP(Online Transaction Processing)的实时事务处理不同。Doris目前主要提供OLAP引擎,适用于数据仓库场景。

OLAP引擎特点:

  • 专为分析型查询优化,支持复杂聚合、多维分析等操作 。 ‌
  • 适用于报表生成、趋势分析等场景,数据通常从OLTP系统抽取后加载 。 ‌
  • 支持行列混合存储、行级缓存等特性,提升查询性能 。

在上面的建表语句中,使用的表类型是DUPLICATE KEY明细表,并指定`user_id`、 `item_id`、 `action_time`三个字段作为key列,表中的其他字段作为value列。在写建表语句时,Key 列中的字段必须在所有 Value 列之前。

在 Doris 中支持三种表类型:

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

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

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

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

  • Duplicate Key Table:适合任意维度的 Ad-hoc 查询。虽然同样无法利用预聚合的特性,但是不受聚合表的约束,可以发挥列列存的优势(只读取相关列,而不需要读取所有 Key 列)。
  • Unique Key Table:针对需要唯一主键约束的场景,可以保证主键唯一性约束。但是无法利用 ROLLUP 等预聚合带来的查询优势。
  • Aggregate Key Table:可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,非常适合有固定模式的报表类查询场景。但是该类型表对 count(*) 查询很不友好。同时因为固定了 Value 列上的聚合方式,在进行其他类型的聚合查询时,需要考虑语意正确性。

在 Doris 中,数据以列的形式存储,一张表可以分为 key 列与 value 列。其中,key 列用于分组与排序,value 列用于参与聚合。Key 列可以是一个或多个字段,在建表时,按照各种表类型中,Aggregate Key、Unique Key 和 Duplicate Key 的列进行数据排序存储。

不同的表类型都需要在建表时指定 Key 列,分别有不同的意义:对于明细表,Key 列表示排序,没有唯一键的约束。在聚合表与主键表中,会基于 Key 列进行聚合,Key 列既有排序的能力,又有唯一键的约束。

合理使用排序键可以带来以下收益:

  • 加速查询性能:排序键有助于减少数据扫描量。对于范围查询或过滤查询,可以利用排序键直接定位数据的位置。对于需要进行排序的查询,也可以利用排序键进行排序加速;
  • 数据压缩优化:数据按排序键有序存储会提高压缩的效率,相似的数据会聚集在一起,压缩率会大幅度提高,从而减小数据的存储空间。
  • 减少去重成本:当使用 Unique Key 表时,通过排序键,Doris 能更有效地进行去重操作,保证数据唯一性。

选择排序键时,可以遵循以下建议:

  • Key 列必须在所有 Value 列之前。
  • 尽量选择整型类型。因为整型类型的计算和查找效率远高于字符串。
  • 对于不同长度的整型类型的选择原则,遵循够用即可。
  • 对于 VARCHAR 和 STRING 类型的长度,遵循够用即可原则。

1、明细表( Duplicate Key Table

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

  • 保留原始数据:明细表保留了全量的原始数据,适合于存储与查询原始数据。对于需要进行详细数据分析的应用场景,建议使用明细表,以避免数据丢失的风险;
  • 不去重也不聚合:与聚合模型与主键模型不同,明细表不会对数据进行去重与聚合操作。即使两条相同的数据,每次插入时也会被完整保留;
  • 灵活的数据查询:明细表保留了全量的原始数据,可以从完整数据中提取细节,基于全量数据做任意维度的聚合操作,从而进行元数数据的审计及细粒度的分析。
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;

明细表使用场景:

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

  • 日志存储:用于存储各类的程序操作日志,如访问日志、错误日志等。每一条数据都需要被详细记录,方便后续的审计与分析;
  • 用户行为数据:在分析用户行为时,如点击数据、用户访问轨迹等,需要保留用户的详细行为,方便后续构建用户画像及对行为路径进行详细分析;
  • 交易数据:在某些存储交易行为或订单数据时,交易结束时一般不会发生数据变更。明细表适合保留这一类交易信息,不遗漏任意一笔记录,方便对交易进行精确的对账。

2、主键表( Unique Key Table

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

主键表有以下特点:

  • 基于主键进行 UPSERT:在插入数据时,主键重复的数据会更新,主键不存在的记录会插入;
  • 基于主键进行去重:主键表中的 Key 列具有唯一性,会对根据主键列对数据进行去重操作;
  • 高频数据更新:支持高频数据更新场景,同时平衡数据更新性能与查询性能。
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"  # 开启写时合并
);

主键表使用场景:

  • 高频数据更新:适用于上游 OLTP 数据库中的维度表,实时同步更新记录,并高效执行 UPSERT 操作;
  • 数据高效去重:如广告投放和客户关系管理系统中,使用主键表可以基于用户 ID 高效去重;
  • 需要部分列更新:如画像标签场景需要变更频繁改动的动态标签,消费订单场景需要改变交易的状态。通过主键表部分列更新能力可以完成某几列的变更操作。

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

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

  2. 读时合并(merge-on-read):在 1.2 版本前,Doris 中的主键表默认使用读时合并模式,数据在写入时并不进行合并,以增量的方式被追加存储,在 Doris 内保留多个版本。查询或 Compaction 时,会对数据进行相同 Key 的版本合并。读时合并适合写多读少的场景,在查询是需要进行多个版本合并,谓词无法下推,可能会影响到查询速度。

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

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

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

3、聚合表( Aggregate Key Table

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

原理:

  1. 每一次数据导入会在聚合表内形成一个版本,在 Compaction 阶段进行版本合并,在查询时会按照主键进行数据聚合:
  2. 数据导入阶段:数据按批次导入,每批次生成一个版本,并对相同聚合键的数据进行初步聚合(如求和、计数);
  3. 后台文件合并阶段(Compaction):多个版本文件会定期合并,减少冗余并优化存储;
  4. 查询阶段:查询时,系统会聚合同一聚合键的数据,确保查询结果准确。

使用 AGGREGATE KEY 关键字在建表时指定聚合表,并指定 Key 列用于聚合 Value 列。

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 列会按照指定的聚合类型进行维度聚合,例如,对cost字段求和,对max_dwell字段求最大值。

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

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

聚合表使用场景:

  • 明细数据进行汇总:用于电商平台的月销售业绩、金融风控的客户交易总额、广告投放的点击量等业务场景中,进行多维度汇总;
  • 不需要查询原始明细数据:如驾驶舱报表、用户交易行为分析等,原始数据存储在数据湖中,仅需存储汇总后的数据。

3、分区Partition、分桶Bucket

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

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

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

Doris 采用分层存储架构,数据从逻辑表到物理文件的组织关系如下:

表(Table) → 分区(Partition) → 分桶(Bucket) → Segment文件 → Page数据页 → Row行数据。

1、分区策略

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

分区类型:

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

分区模式:

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

分区类型:

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

(2)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)
)

(3)BATCH RANGE:批量创建数字类型和时间类型的 RANGE 分区,定义分区的左闭右开区间,设定步长。示例如下:

sql 复制代码
PARTITION BY RANGE(age)
(
    FROM (1) TO (100) INTERVAL 10
)

PARTITION BY RANGE(`date`)
(
    FROM ("2000-11-14") TO ("2021-11-14") INTERVAL 2 YEAR
)

(4)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'))                                                                                                                                                                                
)  
  1. 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"))
)

分区模式:

  1. 手动分区

在编写建表语句时,使用"PARTITION"关键字即可实现手动分区,上面的示例都是手动分区。

sql 复制代码
CREATE TABLE IF NOT EXISTS `user_actions` (
    `user_id` BIGINT,
    `item_id` BIGINT,
    `action_time` DATETIME,
    `action_type` VARCHAR(32),
    `city` VARCHAR(32)
) ENGINE=OLAP
DUPLICATE KEY(`user_id`, `item_id`, `action_time`)
PARTITION BY RANGE(`action_time`)
(	PARTITION p202411 VALUES [('2024-11-01'), ('2024-12-01')),
    PARTITION p202412 VALUES [('2024-12-01'), ('2025-01-01'))
DISTRIBUTED BY HASH(`user_id`) BUCKETS 6;

上面的建表语句使用的分区类型是Range 分区,分区模式是手动分区。

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

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

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

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

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

sql 复制代码
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"
);

如下示例,表示保留12个月的历史数据,超过12个月的数据将被删除。

sql 复制代码
CREATE TABLE `device_data`
(
    `sn`        VARCHAR(64)   NOT NULL COMMENT '设备sn号',
    `create_time` DATETIME    NOT NULL COMMENT '创建时间',
    `event_type`  VARCHAR(128)  NOT NULL COMMENT '事件类型',
    `data`      VARCHAR(8192) NULL COMMENT '数据'
) ENGINE = OLAP DUPLICATE KEY(`sn`, `create_time`)
PARTITION BY RANGE(date_trunc(`create_time`,'month'))()
DISTRIBUTED BY HASH(`sn`) BUCKETS 6
PROPERTIES(
    "dynamic_partition.enable" = "true",
    "dynamic_partition.prefix" = "p",
    "dynamic_partition.start" = "-12",
    "dynamic_partition.end" = "1",
    "dynamic_partition.time_unit" = "month"
);


-- 查看表的分区信息
SHOW PARTITIONS FROM device_data;
  1. 自动分区

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

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

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_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。

建表时,使用AUTO RANGE PARTITION或者AUTO LIST PARTITION语法实现自动分区。示例如下:

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"
  );

或者使用List分区:

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"
  );

LIST 自动分区支持多个分区列,分区列写法同普通 LIST 分区一样: AUTO PARTITION BY LIST (`col1`, `col2`, ...)。

使用自动分区的约束限制:

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

2、分桶策略

一个分区可以根据业务需求进一步划分为多个数据分桶(bucket)。每个分桶都作为一个物理数据分片(tablet)存储。合理的分桶策略可以有效降低查询时的数据扫描量,提升查询性能并增加并发处理能力。分桶是数据组织的第二层逻辑划分,用于在分区内将数据行进一步划分到更小的单元。Doris 支持以下两种分桶方式:

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

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

推荐在以下场景中使用 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 个分桶。

  1. Random 分桶

在每个分区中,使用 Random 分桶会随机地将数据分散到各个分桶中,不依赖于某个字段的 Hash 值进行数据划分。Random 分桶能够确保数据均匀分散,从而避免由于分桶键选择不当而引发的数据倾斜问题。在导入数据时,单次导入作业的每个批次会被随机写入到一个 tablet 中,以此保证数据的均匀分布。

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

  • 在任意维度分析的场景中,业务没有特别针对某一列频繁进行过滤或关联查询时,可以选择 Random 分桶;
  • 当经常查询的列或组合列数据分布极其不均匀时,使用 Random 分桶可以避免数据倾斜。
  • Random 分桶无法根据分桶键进行剪裁,会扫描命中分区的所有数据,不建议在点查场景下使用;
  • 只有 DUPLICATE 表可以使用 Random 分区,UNIQUE 与 AGGREGATE 表无法使用 Random 分桶;

以下示例展示了如何创建带有 Random 分桶的表。详细语法请参考 CREATE TABLE 语句:

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 个分桶。

  1. 选择分桶键

只有 Hash 分桶需要选择分桶键,Random 分桶不需要选择分桶键。

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

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

  • 利用查询过滤条件:使用查询中的过滤条件进行 Hash 分桶,有助于数据的剪裁,减少数据扫描量;
  • 利用高基数列:选择高基数(唯一值较多)的列进行 Hash 分桶,有助于数据均匀的分散在每一个分桶中;
  • 高并发点查场景:建议选择单列或较少列进行分桶。点查可能仅触发一个分桶扫描,不同查询之间触发不同分桶扫描的概率较大,从而减小查询间的 IO 影响。
  • 大吞吐查询场景:建议选择多列进行分桶,使数据更均匀分布。若查询条件不能包含所有分桶键的等值条件,将增加查询吞吐,降低单个查询延迟。
  1. 选择分桶数量

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

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

手动设置分桶数

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

-- 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 命令查看。结果需要除以副本数,即表的数据量。

自动设置分桶数

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

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。请注意,该参数与后期系统通过历史分区数据推算出的未来分区大小无关。

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

  • 不支持修改分桶类型
  • 不支持修改分桶键
  • 不支持修改已创建的分桶的分桶数量

Doris为什么需要分区?为什么需要分桶?分区与分桶的区别是什么?

  1. 为什么需要分区?
  • 数据管理:分区允许将大表划分为更小的、更易管理的部分,通常按照时间(如按月、按天)或业务维度(如地区、类别)进行划分。
  • 查询优化(分区剪枝):查询时,Doris可以根据查询条件只扫描相关的分区,避免全表扫描,大幅提升查询效率。
  • 数据生命周期管理:可以方便地对旧数据进行删除(比如删除整个过期的分区)或归档。
  1. 为什么需要分桶?
  • 数据分布与并行计算:分桶是将一个分区内的数据进一步划分为多个桶(Tablet),每个桶是数据移动、复制和均衡的最小单元。通过分桶,数据可以分布到集群的不同节点上,实现并行处理,提高查询和导入效率。
  • 避免数据倾斜:通过合理的分桶键(如选择高基数列,高基数列是指不同值的数量很多‌的列,该列的数据基本上不重复)和分桶数,可以确保数据均匀分布到各个桶中,避免某些节点负载过重。
  • 优化点查询:分桶键通常选择查询中常用的过滤条件(如用户ID),这样点查询可以快速定位到某个桶,减少扫描的数据量。
  1. 分区与分桶的区别是什么?
  • 目的不同:
    1. 分区主要用于数据管理(如按时间管理)和查询剪枝。
    2. 分桶主要用于数据分布、并行计算和避免数据倾斜。
  • 划分依据不同:
    1. 分区:按照连续的、有业务意义的范围(如时间范围)或枚举值进行划分。
    2. 分桶:根据哈希函数对分桶键(一个或多个列)计算哈希值,然后按桶数取模,将数据分散到各个桶中。
  • 粒度不同:
    1. 分区是粗粒度的,一个分区包含的数据量可能很大(如一个月的数据)。
    2. 分桶是细粒度的,一个分桶(Tablet)是数据物理存储和移动的最小单元。
  • 调整灵活性:
    1. 分区可以动态增加或删除(如每天新增一个分区,删除一个月前的分区)。
    2. 分桶在表创建时确定,一旦确定后无法更改(除非重新建表)。因此,分桶数的设置需要提前规划。

分区与分桶的核心区别:

|-------|-----------------|----------------|
| 特性 | ‌分区(Partition)‌ | ‌分桶(Bucket)‌ |
| 目的‌ | 数据管理与查询剪枝 | 数据分布与并行计算 |
| 粒度‌ | 粗粒度(时间/地域) | 细粒度(哈希分布) |
| 划分依据‌ | 业务维度(连续范围) | 哈希计算(离散分布) |
| 灵活性‌ | 支持动态增删分区 | 建表后固定不变 |
| 数据量‌ | 每个分区数据量较大 | 每个分桶约256MB-1GB |
| 优化场景‌ | 时间范围查询 | 分布式并行计算 |

3、Tablet和Segment

  1. Tablet

Tablet 是一张表实际的物理存储单元,一张表按照分区和分桶后在 BE 构成分布式存储层中以 Tablet 为单位进行存储,每个 Tablet 包括元信息及若干个连续的 RowSet。

Bucket(逻辑分桶):Bucket是逻辑概念,是数据分布的哈希规则,由建表语句 DISTRIBUTED BY HASH(...) BUCKETS N 指定,决定某一行数据应该存储在哪个位置,建表后固定不变。

Tablet(物理分片)‌:Tablet是物理实体,是数据存储的实际单元,系统根据Bucket规则自动创建Tablet,支持副本和负载均衡,系统会默认为每个Bucket创建3个Tablet副本(建表时可以在PROPERTIES中指定副本数量),同一个Bucket的多个副本中的数据都是一样的。Tablet的数量可以动态调整(如通过分桶分裂)。Tablet 是 Doris 中多副本高可用、集群间数据调度与均衡的最小物理存储单位。

总Tablet数量 = 分区数 × 分桶数 × 副本数

sql 复制代码
PROPERTIES
    (
        "replication_num" = "3"
);

下图示例,有两个表分别导入 Doris,表 1 导入后按 3 副本存储,表 2 导入后按 2 副本存储。

  1. RowSet

RowSet 是 Tablet 中一次数据变更的数据集合,数据变更包括了数据导入、删除、更新等。RowSet 按版本信息进行记录,每次变更会生成一个版本。RowSet 是内存中的数据集合,用于批量处理。在每个分桶内,数据首先写入内存中的 RowSet,当RowSet达到一定大小(默认256MB)或满足其他条件时,从内存刷写到磁盘上的Segment文件中。

  1. Version

Version由 Start、End 两个属性构成,维护数据变更的记录信息。通常用来表示 RowSet 的版本范围,每次导入都会生成一个新的 Version,在一次新导入后生成一个 Start、End 相等的 RowSet,在 Compaction 后生成一个带范围的 RowSet 版本。

  1. Compaction

连续版本的 RowSet 合并的过程称为 Compaction,合并过程中会对数据进行压缩操作。

  1. Segment(段文件)

Segment表示 RowSet 中的数据分段,多个 Segment 构成一个 RowSet。Segment是‌数据分桶后的物理存储文件‌,是Doris中数据导入、压缩、查询的基本单元。

特点‌:

  • 每个 Bucket 对应一个或多个 Segment 文件
  • 包含数据区和索引区
  • 按列组织,支持高效的列式扫描
  1. Page(数据页)

Page是 Segment 文件内的数据分块‌,通常是 64KB 大小。

作用‌:

  • 减少 I/O 操作,按需读取特定 Page
  • 支持数据压缩和编码

数据存储结构

Doris 通过 storage_root_path 进行存储路径配置,Segment 文件存放在 tablet_id 目录下按 SchemaHash 管理。Segment 文件可以有多个,一般按照大小进行分割,默认为 256MB。存储目录以及 Segment 文件命名规则为:

{storage_root_path}/data/{shard}/{tablet_id}/{schema_hash}/{rowset_id}_{segment_id}.dat

进入 storage_root_path 目录,可以看到如下存储结构:

  • ${shard}:即上图中的 0、1,是存储目录下 BE 自动创建的,是随机的,会随着数据的增多而增多。
  • ${tablet_id}:是Bucket 的 ID,即上图中的 15123、27003 等。
  • ${schema_hash}:即上图中的 727041558、1102328406 等。因为一个表的结构可能会被变更,所以对每个 Schema 的版本生成一个 SchemaHash,来标识该版本下的数据。
  • {segment_id}.dat:其中前面的为 rowset_id,即上图中的 02000000000000e3ba4924368a21695d8cc3cf8525f80789;{segment_id} 为当前 RowSet 的 segment_id,从 0 开始递增。

下面以一个示例看一下数据在分区、分桶、segment中写入和读取过程。

假设有 user_actions 表:

sql 复制代码
CREATE TABLE `user_actions` (
    `user_id` BIGINT,
    `item_id` BIGINT,
    `action_time` DATETIME,
    `action_type` VARCHAR(32),
    `city` VARCHAR(32)
) ENGINE=OLAP
DUPLICATE KEY(`user_id`, `item_id`, `action_time`)
PARTITION BY RANGE(`action_time`)
(
    PARTITION p202411 VALUES [('2024-11-01'), ('2024-12-01')) ,
    PARTITION p202412 VALUES [('2024-12-01'), ('2025-01-01'))
DISTRIBUTED BY HASH(`user_id`) BUCKETS 6;

1、数据写入过程

当向Doris表中插入数据时,数据会经过以下层级最终写入磁盘:

数据流向:分区 → 分桶 → RowSet → Segment文件

步骤 1:数据分区

user_actions/

├── p202411/ -- 2024年11月分区

└── p202412/ -- 2024年12月分区

分区路由:原始数据 → 根据分区键(如action_time)计算 → 确定目标分区。

系统根据 PARTITION BY RANGE(action_time) 规则判断数据属于哪个分区,如果数据不符合任何分区范围,则写入会失败。

步骤 2:数据分桶‌

分桶路由:分区内数据 → 哈希(user_id) % 6 → 确定目标分桶。

DISTRIBUTED BY HASH(`user_id`) BUCKETS 6表示在每个分区内,数据根据 HASH(user_id) 分散到 6 个桶中。

步骤 3:Segment 文件生成‌

在每个分桶内,数据首先写入内存中的 RowSet,当RowSet达到一定大小(默认256MB)或满足其他条件时,从内存刷写到磁盘上的Segment文件中。

在 p202411 分区中,每个桶生成一个或多个 Segment 文件,是.dat后缀格式的,如下所示:

bash 复制代码
p202411/xxx_0.dat # 桶0
p202411/xxx_0.dat # 桶1
p202411/xxx_0.dat # 桶2
p202411/xxx_0.dat # 桶3
p202411/xxx_0.dat # 桶4
p202411/xxx_0.dat # 桶5

2、查询时的数据访问流程

当执行如下查询时:

SELECT * FROM user_actions WHERE user_id = 1234 AND action_time BETWEEN '2024-11-20' AND '2024-11-21'

步骤 1:分区剪枝‌

只扫描 p202411 分区。

步骤 2:分桶路由‌

计算 user_id = 1234 的哈希值,确定访问哪个 Bucket。

步骤 3:索引定位‌

使用前缀索引定位到包含 user_id = 1234 的 Page。

步骤 4:数据读取‌

只读取相关 Page,避免全 Segment 扫描。

3、Segment 文件内部结构

0.dat(约 100MB)

├── 数据区

│ ├── user_id.col -- 用户ID列数据

│ ├── item_id.col -- 商品ID列数据

│ ├── action_time.col -- 行为时间列数据

│ ├── action_type.col -- 行为类型列数据

│ └── city.col -- 城市列数据

└── 索引区

├── 前缀索引 (sparse_index)

├── ZoneMap 索引

├── BloomFilter 索引

└── 页索引 (page_index)

Segment 整体的文件格式分为数据区域、索引区域和 Footer 三个部分,如下图所示:

  • Data Region:用于存储各个列的数据信息,这里的数据是按需分 Page 加载的,其中 Page 中包含了列的数据,每个 Page 为 64k。
  • Index Region:Doris 中将各个列的 Index 数据统一存储在 Index Region,这里的数据会按照列粒度进行加载,所以跟列的数据信息分开存储。
  • Footer 信息:包含文件的元数据信息、内容的 Checksum 等。

4、Page 内部结构示例

user_id.col (约 16MB)

├── Page 0 (64KB)

│ └── [1001, 1002, 1003, ..., 1124]

├── Page 1 (64KB)

│ └── [1125, 1126, ..., 1248]

├── Page 2 (64KB)

│ └── [1249, 1250, ..., 1372]

└── ...

5、索引文件的磁盘存储

  1. 前缀索引存储

sparse_index.data

├── 索引项 0: (1001, 2001, '2024-11-20 10:00:00') -> Page 0

├── 索引项 1: (1125, 2105, '2024-11-20 10:01:00') -> Page 1

└── 索引项 2: (1249, 2209, '2024-11-20 10:02:00') -> Page 2

  1. ZoneMap 索引存储

为每个列的每个 Page 存储最小值、最大值:

zonemap_index.data

├── user_id:

│ ├── Page 0: min=1001, max=1124

│ ├── Page 1: min=1125, max=1248

│ ├── Page 2: min=1249, max=1372

└── item_id:

├── Page 0: min=2001, max=2104

4、表索引

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

  1. 点查索引:常用于加速点查,原理是通过索引定位到满足 WHERE 条件的有哪些行,直接读取那些行。点查索引在满足条件的行比较少时效果很好。Apache Doris 的点查索引包括前缀索引和倒排索引。
  • 前缀索引:Apache Doris 按照排序键以有序的方式存储数据,并每隔 1024 行数据创建一个稀疏前缀索引。索引中的 Key 是当前 1024 行中第一行中排序列的值。如果查询涉及已排序列,系统将找到相关 1024 行组的第一行并从那里开始扫描。
  • 倒排索引:对创建了倒排索引的列,建立每个值到对应行号集合的倒排表。对于等值查询,先从倒排表中查到行号集合,然后直接读取对应行的数据,而不用逐行扫描匹配数据,从而减少 I/O 加速查询。倒排索引还能加速范围过滤、文本关键词匹配,算法更加复杂但是基本原理类似。(备注:之前的 BITMAP 索引已经被更强的倒排索引取代)
  1. 跳数索引:常用于加速分析,原理是通过索引确定不满足 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 |

索引设计指南:

数据库表的索引设计和优化跟数据特点和查询很相关,需要根据实际场景测试和优化。用户可以根据下面的简单建议原则进行索引选择和测试。

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

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

  • 有字符串 LIKE 匹配需求,再加一个 NGram BloomFilter 索引
  • 对索引存储空间很敏感,将倒排索引换成 BloomFilter 索引
  1. 如果性能不及预期,通过 QueryProfile 分析索引过滤掉的数据量和消耗的时间,具体参考各个索引的详细文档。

1、前缀索引

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

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

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

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

  1. ‌前缀索引的基本概念‌
  • 稀疏索引‌:前缀索引是一种稀疏索引,这意味着它不会为表中的每一行都创建索引条目,而是为‌逻辑数据块‌(Data Block)创建一个索引条目。
  • 逻辑数据块‌:表中的数据被划分为多个逻辑数据块,每个数据块包含一定数量的行(例如1024行)。
  • 索引项内容‌:每个数据块在前缀索引表中存储一个索引项,索引项的内容是该数据块中‌第一行数据的排序列的前缀‌,长度不超过36字节。
  1. ‌前缀索引的构建规则‌

前缀截断规则‌:

  • 从第一行数据开始,按排序键的列顺序,依次将列值拼接到前缀中。
  • 如果拼接后的前缀长度达到36字节,则停止拼接,后续列不再加入前缀索引。
  • 如果遇到VARCHAR类型的列,无论是否达到36字节,都会直接截断,后续列不再加入前缀索引。

索引项的作用‌:前缀索引项用于快速定位数据块,在查询时可以帮助确定目标行所在的逻辑数据块的起始行号。前缀索引是稀疏索引,不能精确定位到 Key 所在的行,只能粗粒度地定位出 Key 可能存在的范围,然后使用二分查找算法精确地定位 Key 的位置。

  1. ‌前缀索引的工作流程‌

(1)数据分块‌:表中的数据被划分为多个逻辑数据块,每个块包含一定数量的行。

(2)构建索引‌:

  • 对每个数据块,取第一行数据。
  • 按照排序键的列顺序,拼接列值,形成前缀索引项,长度不超过36字节。
  • 将索引项存储到前缀索引表中。

(3)查询优化‌:

  • 查询时,先在前缀索引表中查找匹配的索引项。
  • 根据索引项确定目标行所在的逻辑数据块。
  • 仅扫描该数据块中的行查找目标行的数据,而不是全表扫描。
  1. 使用场景

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

  1. 管理索引

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

  1. 前缀索引选择建议

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

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

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

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

2、倒排索引

1、倒排索引原理

倒排索引是信息检索领域常用的索引技术,将文本分成一个个词,构建"词 -> 文档编号"的索引,可以快速查找一个词在哪些文档出现。从 2.0.0 版本开始,Doris 支持倒排索引,可以用来进行文本类型的全文检索、普通数值日期类型的等值范围查询,快速从海量数据中过滤出满足条件的行。

Doris 的倒排索引实现中, Table 的一行对应一个文档、一列对应文档中的一个字段,因此利用倒排索引可以根据关键词快速定位包含它的行,达到 WHERE 子句加速的目的。与 Doris 中其他索引不同的是,在存储层倒排索引使用独立的文件,跟数据文件一一对应、但物理存储上文件相互独立。这样的好处是可以做到创建、删除索引不用重写数据文件,大幅降低处理开销。

2、使用场景

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

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

  1. 加速字符串类型的全文检索
  • 支持关键词检索,包括同时匹配多个关键字 MATCH_ALL、匹配任意一个关键字 MATCH_ANY
  • 支持短语查询 MATCH_PHRASE
    1. 支持指定词距 slop
    2. 支持短语 + 前缀 MATCH_PHRASE_PREFIX
  • 支持分词正则查询 MATCH_REGEXP
  • 支持英文、中文以及 Unicode 多种分词
  1. 加速普通等值、范围查询,覆盖原来 BITMAP 索引的功能,代替 BITMAP 索引
  • 支持字符串、数值、日期时间、数组类型的 =, !=, >, >=, <, <=
  1. 支持完善的逻辑组合
  • 不仅支持 AND 条件加速,还支持 OR NOT 条件加速
  • 支持多个条件的任意 AND OR NOT 逻辑组合
  1. 灵活高效的索引管理
  • 支持在创建表上定义倒排索引
  • 支持在已有的表上增加倒排索引,而且支持增量构建倒排索引,无需重写表中的已有数据
  • 支持删除已有表上的倒排索引,无需重写表中的已有数据

3、倒排索引的使用限制

  1. 存在精度问题的浮点数类型 FLOAT 和 DOUBLE 不支持倒排索引,原因是浮点数精度不准确。解决方案是使用精度准确的定点数类型 DECIMAL,DECIMAL 支持倒排索引。
  2. 部分复杂数据类型还不支持倒排索引,包括:MAP、STRUCT、JSON、HLL、BITMAP、QUANTILE_STATE、AGG_STATE,其中 MAP、STRUCT 会逐步支持,JSON 类型可以换成 VARIANT 类型获得支持,其他几个类型因为其特殊用途暂不需要支持倒排索引。
  3. DUPLICATE 和 开启 Merge-on-Write 的 UNIQUE 表模型支持任意列建倒排索引。但是 AGGREGATE 和 未开启 Merge-on-Write 的 UNIQUE 模型仅支持 Key 列建倒排索引,非 Key 列不能建倒排索引,这是因为这两个模型需要读取所有数据后做合并,因此不能利用索引做提前过滤。

4、管理索引

  1. 建表时定义倒排索引

在建表语句中 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;

语法说明如下:

  • idx_column_name(column_name) 是必须的,column_name 是建索引的列名,必须是前面列定义中出现过的,idx_column_name 是索引名字,必须表级别唯一,建议命名规范:列名前面加前缀 idx_。
  • USING INVERTED 是必须的,用于指定索引类型是倒排索引。
  • PROPERTIES 是可选的,用于指定倒排索引的额外属性,目前支持的属性有parser 指定分词器、parser_mode、support_phrase、char_filter、ignore_above、dict_compression (该功能自 3.1.0 版本开始支持)等。
  • COMMENT 是可选的,用于指定索引注释
  • 表级属性 inverted_index_storage_format (该功能自 3.1.0 版本开始支持),要使用新的 V3 存储格式,在建表时指定此属性:
sql 复制代码
CREATE TABLE table_name (
    column_name TEXT,
    INDEX idx_name(column_name) USING INVERTED PROPERTIES("parser" = "english", "dict_compression" = "true")
) PROPERTIES (
    "inverted_index_storage_format" = "V3"
);
  1. 给已有表增加倒排索引

支持CREATE INDEX 和 ALTER 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'];

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 进度:

SHOW BUILD INDEX [FROM db_name];

sql 复制代码
-- 示例 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 存算分离模式暂不支持此命令。

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

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

具体使用示例可以参考官方文档。

3、BloomFilter 索引

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 查询加速的目的。

2、使用场景

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

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

  • 对 IN 和 = 之外的查询没有效果,比如 !=, NOT IN, >, < 等
  • 不支持对 Tinyint、Float、Double 类型的列建 BloomFilter 索引。
  • 对低基数字段的加速效果很有限,比如"性别"字段仅有两种值,几乎每个数据块都会包含所有取值,导致 BloomFilter 索引失去意义。

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

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

3、管理索引

  1. 建表时创建 BloomFilter 索引

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

sql 复制代码
PROPERTIES (
"bloom_filter_columns" = "column_name1,column_name2"
);
  1. 查看 BloomFilter 索引

SHOW CREATE TABLE table_name;

  1. 已有表增加、删除 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");
  1. 使用示例

BloomFilter 索引用于加速 WHERE 条件中的等值查询,能加速时自动生效,没有特殊语法。可以通过 Query Profile 中的下面几个指标分析 BloomFilter 索引的加速效果。

  • RowsBloomFilterFiltered BloomFilter 索引过滤掉的行数,可以与其他几个 Rows 值对比分析索引过滤效果
  • BlockConditionsFilteredBloomFilterTime 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、N-Gram 索引

1、索引原理

n-gram 分词是将一句话或一段文字拆分成多个相邻的词组的分词方法。NGram BloomFilter 索引和 BloomFilter 索引类似,也是基于 BloomFilter 的跳数索引。 与 BloomFilter 索引不同的是,NGram BloomFilter 索引用于加速文本 LIKE 查询,它存入 BloomFilter 的不是原始文本的值,而是对文本进行 NGram 分词,每个词作为值存入 BloomFilter。对于 LIKE 查询,将 LIKE '%pattern%' 的 pattern 也进行 NGram 分词,判断每个词是否在 BloomFilter 中,如果某个词不在,则对应的数据块就不满足 LIKE 条件,可以跳过这部分数据,减少 IO,加速查询。

2、使用场景

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

提示:NGram BloomFilter 只支持字符串列,只能加速 LIKE 查询。NGram BloomFilter 索引和 BloomFilter 索引为互斥关系,即同一个列只能设置两者中的一个。NGram BloomFilter 索引的效果分析,跟 BloomFilter 索引类似。

3、管理索引

  1. 创建 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'

语法说明如下:

  • idx_column_name(column_name) 是必须的,column_name 是建索引的列名,必须是前面列定义中出现过的,idx_column_name 是索引名字,必须表级别唯一,建议命名规范:列名前面加前缀 idx_。
  • USING NGRAM_BF 是必须的,用于指定索引类型是 NGram BloomFilter 索引。
  • PROPERTIES 是可选的,用于指定 NGram BloomFilter 索引的额外属性,目前支持的属性如下:
    1. 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 个词。
    2. bf_size:BloomFilter 的大小,单位是 Bit。bf_size 决定每个数据块对应的索引大小,这个值越大占用存储空间越大,同时 Hash 碰撞的概率也越低。
    3. gram_size 建议取 LIKE 查询的字符串最小长度,但是不建议低于 2。一般建议设置 "gram_size"="3", "bf_size"="1024",然后根据 Query Profile 调优。
  • COMMENT 是可选的,用于指定索引注释。
  1. 查看 NGram BloomFilter 索引
sql 复制代码
-- 语法 1:表的 schema 中 INDEX 部分 USING NGRAM_BF 是NGram BloomFilter索引
SHOW CREATE TABLE table_name;
-- 语法 2:IndexType 为 NGRAM_BF 的是NGram BloomFilter索引
SHOW INDEX FROM idx_name;
  1. 删除 NGram BloomFilter 索引
sql 复制代码
ALTER TABLE table_ngrambf DROP INDEX idx_ngrambf;
  1. 修改 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';
  1. 使用索引

使用 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 索引消耗的时间
相关推荐
翼龙云_cloud2 小时前
阿里云渠道商:cpu 弹性扩容有哪些限制条件?
数据库·阿里云·云计算
陈聪.2 小时前
HRCE简单实验
linux·运维·数据库
APIshop2 小时前
实战代码解析:item_get——获取某鱼商品详情接口
java·linux·数据库
洛_尘2 小时前
MySQL 5:增删改查操作
数据库·mysql
老邓计算机毕设2 小时前
SSM养老院老人健康信息管理系统t4p4x(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·计算机毕业设计·ssm 框架·养老院老人健康管理系统
AC赳赳老秦3 小时前
跨境科技服务的基石:DeepSeek赋能多语言技术文档与合规性说明的深度实践
android·大数据·数据库·人工智能·科技·deepseek·跨境
理智的煎蛋3 小时前
达梦数据库全流程操作指南
数据库·oracle
FreeBuf_3 小时前
欧盟漏洞数据库正式上线,采用去中心化模式运营
数据库·去中心化·区块链
东方轧线3 小时前
给 AI 安装高速缓存:实战 MCP 对接 Redis,实现热点数据的毫秒级读取与状态共享
数据库·人工智能·redis