关系模型与数据划分

          PALO

          关系模型与数据划分

          本文档主要介绍 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

            • Partition 列可以指定一列或多列。分区列必须为 KEY 列。多列分区的使用方式在后面 多列分区 小结介绍。
            • 不论分区列是什么类型,在写分区值时,都需要加双引号。
            • 分区列通常为时间列,以方便的管理新旧数据。
            • 分区数量理论上没有上限。
            • 当不使用 Partition 建表时,系统会自动生成一个和表名同名的,全值范围的 Partition。该 Partition 对用户不可见,并且不可删改。
            • Partition 支持通过 VALUES LESS THAN (...) 仅指定上界,系统会将前一个分区的上界作为该分区的下界,生成一个左闭右开的区间。通过,也支持通过 VALUES [...) 指定同时指定上下界,生成一个左闭右开的区间。
            • 通过 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 语句增加分区时,分区的下界紧接上一个分区的上界。

            不可添加范围重叠的分区。

          2. Bucket

            • 如果使用了 Partition,则 DISTRIBUTED ... 语句描述的是数据在各个分区内的划分规则。如果不使用 Partition,则描述的是对整个表的数据的划分规则。
            • 分桶列可以是多列,但必须为 Key 列。分桶列可以和 Partition 列相同或不同。
            • 分桶列的选择,是在 查询吞吐查询并发 之间的一种权衡:

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

            • 一个表的 Tablet 总数量等于 (Partition num * Bucket num)。
            • 一个表的 Tablet 数量,在不考虑扩容的情况下,推荐略多于整个集群的磁盘数量。
            • 单个 Tablet 的数据量理论上没有上下界,但建议在 1G - 10G 的范围内。如果单个 Tablet 数据量过小,则数据的聚合效果不佳,且元数据管理压力大。如果数据量过大,则不利于副本的迁移、补齐,且会增加 Schema Change 或者 Rollup 操作失败重试的代价(这些操作失败重试的粒度是 Tablet)。
            • 当 Tablet 的数据量原则和数量原则冲突时,建议优先考虑数据量原则。
            • 在建表时,每个分区的 Bucket 数量统一指定。但是在动态增加分区时(ADD PARTITION),可以单独指定新分区的 Bucket 数量。可以利用这个功能方便的应对数据缩小或膨胀。
            • 一个 Partition 的 Bucket 数量一旦指定,不可更改。所以在确定 Bucket 数量时,需要预先考虑集群扩容的情况。比如当前只有 3 台 host,每台 host 有 1 块盘。如果 Bucket 的数量只设置为 3 或更小,那么后期即使再增加机器,也不能提高并发度。
            • 举一些例子:假设在有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    --> 无法导入
          上一篇
          高级使用指南
          下一篇
          数据模型