Apache Doris关系模型与数据划分

作者: 张家锋

本文档主要介绍 Doris 的建表和数据划分,以及建表操作中可能遇到的问题和解决方法。

基本概念

在 Doris 中,数据都以关系表(Table)的形式进行逻辑上的描述。

Row & Column

一张表包括行(Row)和列(Column)。Row 即用户的一行数据。Column 用于描述一行数据中不同的字段。

在默认的数据模型中,Column 只分为排序列和非排序列。存储引擎会按照排序列对数据进行排序存储,并建立稀疏索引,以便在排序数据上进行快速查找。

而在聚合模型中,Column 可以分为两大类:Key 和 Value。从业务角度看,Key 和 Value 可以分别对应维度列和指标列。从聚合模型的角度来说,Key 列相同的行,会聚合成一行。其中 Value 列的聚合方式由用户在建表时指定。关于更多聚合模型的介绍,可以参阅数据模型文档。

Partition & Tablet

在 Doris 的存储引擎中,用户数据首先被划分成若干个分区(Partition),划分的规则通常是按照用户指定的分区列进行范围划分,比如按时间划分。而在每个分区内,数据被进一步的安装Hash的方式分桶,分桶的规则是要找用户指定的分桶列的值进行Hash后分桶。每个分桶就是一个数据分片(Tablet),也是数据划分的最小逻辑单元。 Tablet直接的数据是没有交集的,独立存储的。Tablet也是数据移动、复制等操作的最小物理存储单元。 Partition 可以视为是逻辑上最小的管理单元。数据的导入与删除,都可以或仅能针对一个 Partition 进行。

数据划分

我们以一个建表操作来说明 Doris 的数据划分。 Doris 的建表是一个同步命令,命令返回成功,即表示建表成功。

可以通过 CREATE TABLE查看更多帮助。

本小节通过一个例子,来介绍 Doris 的建表方式。

CREATE TABLE IF NOT EXISTS example_db.expamle_tbl
(
    `user_id` LARGEINT NOT NULL COMMENT "用户id",
    `date` DATE NOT NULL COMMENT "数据灌入日期时间",
    `timestamp` DATETIME NOT NULL COMMENT "数据灌入的时间戳",
    `city` VARCHAR(20) COMMENT "用户所在城市",
    `age` SMALLINT COMMENT "用户年龄",
    `sex` TINYINT COMMENT "用户性别",
    `last_visit_date` DATETIME REPLACE DEFAULT "1970-01-01 00:00:00" COMMENT "用户最后一次访问时间",
    `cost` BIGINT SUM DEFAULT "0" COMMENT "用户总消费",
    `max_dwell_time` INT MAX DEFAULT "0" COMMENT "用户最大停留时间",
    `min_dwell_time` INT MIN DEFAULT "99999" COMMENT "用户最小停留时间"
)
ENGINE=olap
AGGREGATE KEY(`user_id`, `date`, `timestamp`, `city`, `age`, `sex`)
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")
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
    "replication_num" = "3",
    "storage_medium" = "SSD",
    "storage_cooldown_time" = "2018-01-01 12:00:00"
);

列定义

这里我们只以 AGGREGATE KEY 数据模型为例进行说明。更多数据模型参阅 数据模型。

列的基本类型,可以参阅 CREATE TABLE 文档。 AGGREGATE KEY 数据模型中,所有没有指定聚合方式(SUM、REPLACE、MAX、MIN)的列视为 Key 列。而其余则为 Value 列。

定义列时,可参照如下建议:

  1. Key 列必须在所有 Value 列之前。
  2. 尽量选择整型类型。因为整型类型的计算和查找比较效率远高于字符串。
  3. 对于不同长度的整型类型的选择原则,遵循 够用即可
  4. 对于 VARCHAR 和 CHAR 类型的长度,遵循 够用即可
  5. 所有列的总字节长度(包括 Key 和 Value)不能超过 100KB。

分区与分桶

Doris 支持两层的数据划分。第一层是 Partition,仅支持 Range 的划分方式。第二层是 Bucket(Tablet),仅支持 Hash 的划分方式。

也可以仅使用一层分区。使用一层分区时,只支持 Bucket 划分。

  1. Partition

  2. Partition 列可以指定一列或多列。分区列必须为 KEY 列。多列分区的使用方式在后面 多列分区 小结介绍。

  3. 不论分区列是什么类型,在写分区值时,都需要加双引号。

  4. 分区列通常为时间列,以方便的管理新旧数据。

  5. 分区数量理论上没有上限。

  6. 当不使用 Partition 建表时,系统会自动生成一个和表名同名的,全值范围的 Partition。该 Partition 对用户不可见,并且不可删改。

  7. Partition 支持通过 VALUES LESS THAN (...) 仅指定上界,系统会将前一个分区的上界作为该分区的下界,生成一个左闭右开的区间。通过,也支持通过 VALUES [...) 指定同时指定上下界,生成一个左闭右开的区间。

  8. 通过 VALUES [...) 同时指定上下界比较容易理解。这里举例说明,当使用 VALUES LESS THAN (...) 语句进行分区的增删操作时,分区范围的变化情况:

    • 如上示例,当建表完成后,会自动生成如下3个分区:

      p201701: [MIN_VALUE,  2017-02-01)
      p201702: [2017-02-01, 2017-03-01)
      p201703: [2017-03-01, 2017-04-01)
      
    • 当我们增加一个分区 p201705 VALUES LESS THAN (“2017-06-01”),分区结果如下:

      p201701: [MIN_VALUE,  2017-02-01)
      p201702: [2017-02-01, 2017-03-01)
      p201703: [2017-03-01, 2017-04-01)
      p201705: [2017-04-01, 2017-06-01)
      
    • 此时我们删除分区 p201703,则分区结果如下:

      p201701: [MIN_VALUE,  2017-02-01)
      p201702: [2017-02-01, 2017-03-01)
      p201705: [2017-04-01, 2017-06-01)
      

      注意到 p201702 和 p201705 的分区范围并没有发生变化,而这两个分区之间,出现了一个空洞:[2017-03-01, 2017-04-01)。即如果导入的数据范围在这个空洞范围内,是无法导入的。

    • 继续删除分区 p201702,分区结果如下:

      p201701: [MIN_VALUE,  2017-02-01)
      p201705: [2017-04-01, 2017-06-01)
      空洞范围变为:[2017-02-01, 2017-04-01)
      
    • 现在增加一个分区 p201702new VALUES LESS THAN (“2017-03-01”),分区结果如下:

      p201701:    [MIN_VALUE,  2017-02-01)
      p201702new: [2017-02-01, 2017-03-01)
      p201705:    [2017-04-01, 2017-06-01)
      

      可以看到空洞范围缩小为:[2017-03-01, 2017-04-01)

    • 现在删除分区 p201701,并添加分区 p201612 VALUES LESS THAN (“2017-01-01”),分区结果如下:

      p201612:    [MIN_VALUE,  2017-01-01)
      p201702new: [2017-02-01, 2017-03-01)
      p201705:    [2017-04-01, 2017-06-01) 
      

      即出现了一个新的空洞:[2017-01-01, 2017-02-01)

    综上,分区的删除不会改变已存在分区的范围。删除分区可能出现空洞。通过 VALUES LESS THAN 语句增加分区时,分区的下界紧接上一个分区的上界。 不可添加范围重叠的分区。

  9. Bucket

  10. 如果使用了 Partition,则 DISTRIBUTED ... 语句描述的是数据在各个分区内的划分规则。如果不使用 Partition,则描述的是对整个表的数据的划分规则。

  11. 分桶列可以是多列,但必须为 Key 列。分桶列可以和 Partition 列相同或不同。

  12. 分桶列的选择,是在 查询吞吐查询并发 之间的一种权衡:

    1. 如果选择多个分桶列,则数据分布更均匀。如果一个查询条件不包含所有分桶列的等值条件,那么该查询会触发所有分桶同时扫描,这样查询的吞吐会增加,单个查询的延迟随之降低。这个方式适合大吞吐低并发的查询场景。
    2. 如果仅选择一个或少数分桶列,则对应的点查询可以仅触发一个分桶扫描。此时,当多个点查询并发时,这些查询有较大的概率分别触发不同的分桶扫描,各个查询之间的IO影响较小(尤其当不同桶分布在不同磁盘上时),所以这种方式适合高并发的点查询场景。
  13. 分桶的数量理论上没有上限。

  14. 关于 Partition 和 Bucket 的数量和数据量的建议。

  15. 一个表的 Tablet 总数量等于 (Partition num * Bucket num)。

  16. 一个表的 Tablet 数量,在不考虑扩容的情况下,推荐略多于整个集群的磁盘数量。

  17. 单个 Tablet 的数据量理论上没有上下界,但建议在 1G - 10G 的范围内。如果单个 Tablet 数据量过小,则数据的聚合效果不佳,且元数据管理压力大。如果数据量过大,则不利于副本的迁移、补齐,且会增加 Schema Change 或者 Rollup 操作失败重试的代价(这些操作失败重试的粒度是 Tablet)。

  18. 当 Tablet 的数据量原则和数量原则冲突时,建议优先考虑数据量原则。

  19. 在建表时,每个分区的 Bucket 数量统一指定。但是在动态增加分区时(ADD PARTITION),可以单独指定新分区的 Bucket 数量。可以利用这个功能方便的应对数据缩小或膨胀。

  20. 一个 Partition 的 Bucket 数量一旦指定,不可更改。所以在确定 Bucket 数量时,需要预先考虑集群扩容的情况。比如当前只有 3 台 host,每台 host 有 1 块盘。如果 Bucket 的数量只设置为 3 或更小,那么后期即使再增加机器,也不能提高并发度。

  21. 举一些例子:假设在有10台BE,每台BE一块磁盘的情况下。如果一个表总大小为 500MB,则可以考虑4-8个分片。5GB:8-16个。50GB:32个。500GB:建议分区,每个分区大小在 50GB 左右,每个分区16-32个分片。5TB:建议分区,每个分区大小在 50GB 左右,每个分区16-32个分片。

    注:表的数据量可以通过 show data 命令查看,结果除以副本数,即表的数据量。

多列分区

Doris 支持指定多列作为分区列,示例如下:

PARTITION BY RANGE(`date`, `id`)
(
    PARTITION `p201701_1000` VALUES LESS THAN ("2017-02-01", "1000"),
    PARTITION `p201702_2000` VALUES LESS THAN ("2017-03-01", "2000"),
    PARTITION `p201703_all`  VALUES LESS THAN ("2017-04-01")
)

在以上示例中,我们指定 date(DATE 类型) 和 id(INT 类型) 作为分区列。以上示例最终得到的分区如下:

* p201701_1000:    [(MIN_VALUE,  MIN_VALUE), ("2017-02-01", "1000")   )
* p201702_2000:    [("2017-02-01", "1000"),  ("2017-03-01", "2000")   )
* p201703_all:     [("2017-03-01", "2000"),  ("2017-04-01", MIN_VALUE)) 

注意,最后一个分区用户缺省只指定了 date 列的分区值,所以 id 列的分区值会默认填充 MIN_VALUE。当用户插入数据时,分区列值会按照顺序依次比较,最终得到对应的分区。举例如下:

* 数据  -->  分区
* 2017-01-01, 200     --> p201701_1000
* 2017-01-01, 2000    --> p201701_1000
* 2017-02-01, 100     --> p201701_1000
* 2017-02-01, 2000    --> p201702_2000
* 2017-02-15, 5000    --> p201702_2000
* 2017-03-01, 2000    --> p201703_all
* 2017-03-10, 1       --> p201703_all
* 2017-04-01, 1000    --> 无法导入
* 2017-05-01, 1000    --> 无法导入

文章列表

更多推荐

更多
  • Pulsar消息队列-一套高可用实时消息系统实现 实时消息【即时通信】系统,有群聊和单聊两种方式,其形态异于消息队列:1 大量的 group 信息变动,群聊形式的即时通信系统在正常服务形态下,瞬时可能有大量用户登入登出。2 ...
  • Pulsar消息队列-Pulsar对比Kafka笔记 很多人查看 Pulsar 之前可能对 Kafka 很熟悉,参照上图可见二者内部结构的区别,Pulsar 和 Kafka 都是以 Topic 描述一个基本的数据集合,Topic 数据又分为若干 Partition,即对数据进行逻辑上的 ...
  • Pulsar消息队列-对 2017 年一套 IM 系统的反思 信系统的开发,前前后后参与或者主导了六七个 IM 系统的研发。上一次开发的 IM 系统的时间点还是 2018 年,关于该系统的详细描述见 [一套高可用实时消息系统实现][1] ...
  • Apache APISIX文档-快速入门指南-如何构建 Apache APISIX 如何构建 Apache APISIX,步骤1:安装 Apache APISIX,步骤2:安装 etcd,步骤3:管理 Apache APISIX 服务,步骤4:运行测试案例,步骤5:修改 Admin API key,步骤6:为 Apac
  • Apache APISIX文档-快速入门指南-快速入门指南 快速入门指南,概述,前提条件,第一步:安装 Apache APISIX,第二步:创建路由,第三步:验证,进阶操作,工作原理,创建上游服务Upstream,绑定路由与上游服务,添加身份验证,为路由添加前缀,APISIX Dashboard
  • Apache APISIX文档-架构设计-APISIX APISIX,软件架构,插件加载流程,插件内部结构,配置 APISIX,插件加载流程,比如指定 APISIX 默认监听端口为 8000,并且设置 etcd 地址为 http://foo:2379, 其他配置保持默认。在 ...
  • Apache APISIX文档-架构设计-Service Service 是某类 API 的抽象(也可以理解为一组 Route 的抽象)。它通常与上游服务抽象是一一对应的,Route 与 Service 之间,通常是 N:1 的关系,参看下图。不同 Route 规则同时绑定到一个 Service ...
  • Apache APISIX文档-架构设计-Plugin Config 如果你想要复用一组通用的插件配置,你可以把它们提取成一个 Plugin config,并绑定到对应的路由上。举个例子,你可以这么做:创建 Plugin config,如果这个路由已经配置了 plugins,那么 Plugin config ...
  • Apache APISIX文档-架构设计-Debug Mode 注意:在 APISIX 2.10 之前,开启基本调试模式曾经是设置 conf/config.yaml 中的 apisix.enable_debug 为 true。设置 conf/debug.yaml 中的选项,开启高级调试模式。由于 ...
  • Apache APISIX文档-架构设计-Consumer 如上图所示,作为 API 网关,需要知道 API Consumer(消费方)具体是谁,这样就可以对不同 API Consumer 配置不同规则。授权认证:比如有 [key-auth] 等。获取 consumer_...
  • 近期文章

    更多
    文章目录

      推荐作者

      更多