一.First
1.架构和使用场景
(1) 使用场景
Ø 报表分析
(1)实时看板 (Dashboards)
(2)面向企业内部分析师和管理者的报表
(3)面向用户或者客户的高并发报表分析(Customer Facing Analytics)。
比如面向网站主的站点分析、面向广告主的广告报表,并发通常要求成千上万的 QPS ,查询延时要求毫秒级响应。著名的电商公司京东在广告报表中使用 Apache Doris ,每天写入 100 亿行数据,查询并发 QPS 上万,99 分位的查询延时 150ms。
Ø 即席查询(Ad-hoc Query)
面向分析师的自助分析,查询模式不固定,要求较高的吞吐。小米公司基于 Doris 构建了增长分析平台(Growing Analytics,GA),利用用户行为数据对业务进行增长分析,平均查询延时 10s,95 分位的查询延时 30s 以内,每天的 SQL 查询量为数万条。
Ø 统一数仓构建
一个平台满足统一的数据仓库建设需求,简化繁琐的大数据软件栈。海底捞基于 Doris 构建的统一数仓,替换了原来由 Spark、Hive、Kudu、Hbase、Phoenix 组成的旧架构,架构大大简化。
Ø 数据湖联邦查询
通过外表的方式联邦分析位于 Hive、Iceberg、Hudi 中的数据,在避免数据拷贝的前提下,查询性能大幅提升。
(2) 架构
Ø Frontend(FE)
部署的时候,FE建议是奇数台(选举机制和zk一样,半数选举)
主要负责用户请求的接入、查询解析规划、元数据的管理、节点管理相关工作。
主要有三个角色:
(1) Leader 和Follower
主要是用来达到元数据的高可用,保证单节点宕机的情况下,元数据能够实时地在线恢复,而不影响整个服务。
(2) Observer
用来扩展查询节点,同时起到元数据备份的作用。如果在发现集群压力非常大的情况下,需要去扩展整个查询的能力,那么可以加 observer 的节点。observer 不参与任何的写入,只参与读取。
Ø Backend(BE)
主要负责数据存储、查询计划的执行。
数据的可靠性由 BE 保证,BE 会对整个数据存储多副本或者是三副本。副本数可根据需求动态调整。
Ø MySQL Client
Doris借助MySQL协议,用户使用任意MySQL的ODBC/JDBC以及MySQL的客户端,都可以直接访问Doris。
Ø Broker
Broker 是 Doris 集群中一种可选进程,主要用于支持 Doris 读写远端存储上的文件和目录,如 HDFS、BOS 和 AFS 等。
(3) 各个服务所占用的端口号
内部端口
| 实例名称 | 端口名称 | 默认端口 | 通讯方向 | 说明 |
|---|---|---|---|---|
| BE | be_prot | 9060 | FE-->BE | BE上thrift server的端口 用于接收来自FE 的请求 |
| BE | webserver_port | 8040 | BE<-->FE | BE上的http server端口 |
| BE | heartbeat_service_port | 9050 | FE-->BE | BE上心跳服务端口 用于接收来自FE的心跳 |
| BE | brpc_prot* | 8060 | FE<-->BE BE<-->BE | BE上的brpc端口 用于BE之间通信 |
| FE | http_port | 8030 | FE<-->FE 用户<--> FE | FE上的http_server端口 |
| FE | rpc_port | 9020 | BE-->FE FE<-->FE | FE上thirt server端口号 |
| FE | query_port | 9030 | 用户<--> FE | FE上的mysql server端口 |
| FE | edit_log_port | 9010 | FE<-->FE | FE上bdbje之间通信用的端口 |
| Broker | broker_ipc_port | 8000 | FE-->BROKER BE-->BROKER | Broker上的thrift server 用于接收请求 |
2. 启动
#临时关闭swqp
sudo swapoff -a
#启动FE
/opt/module/doris/fe/bin/start_fe.sh --daemon
#启动BE
/opt/module/doris/be/bin/start_be.sh --daemon
#查询 BE 状态
mysql -hhadoop102 –P9030 -uroot –paaaaaa
show proc '/backends';
3. 数据表设计
(1) 数据类型
<1> Aggregate
建表语句则如下
CREATE TABLE IF NOT EXISTS test_db.example_site_visit
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE 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 "用户最小停留时间"
)
AGGREGATE KEY(`user_id`, `date`, `city`, `age`, `sex`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10
properties(
"replication_num"="1"
);
插入数据
insert into test_db.example_site_visit values
(10000,'2017-10-01','北京',20,0,'2017-10-01 06:00:00' ,20,10,10),
(10000,'2017-10-01','北京',20,0,'2017-10-01 07:00:00',15,2,2),
(10001,'2017-10-01','北京',30,1,'2017-10-01 17:05:45',2,22,22),
(10002,'2017-10-02','上海',20,1,'2017-10-02 12:59:12' ,200,5,5),
(10003,'2017-10-02','广州',32,0,'2017-10-02 11:20:00',30,11,11),
(10004,'2017-10-01','深圳',35,0,'2017-10-01 10:00:15',100,3,3),
(10004,'2017-10-03','深圳',35,0,'2017-10-03 10:20:22',11,6,6);
注意:Insert into 单条数据这种操作在Doris里只能演示不能在生产使用,会引发写阻塞。
说明:
- 表中的REPLACE SUM MAX MIN 叫AggregationType (聚合类型), 目前只有这四种聚合类型.
- 没有设置聚合类型的叫 key(维度列), 设置了聚合类型的叫 value(指标列)
- 当我们导入数据的时候, 会按照 key 对 value 使用他们自己的聚合类型进行聚合
- 在同一个导入批次中的数据,对于 REPLACE 这种聚合方式,替换顺序不做保证。而对于不同导入批次中的数据,可以保证,后一批次的数据会替换前一批次。
- 经过聚合,Doris 中最终只会存储聚合后的数据。换句话说,即明细数据会丢失,用户不能够再查询到聚合前的明细数据了。
- 如果想要保留明细数据不让 doris 聚合, 则主要保证每条数据的 key 不一样就可以了.(多个key 中有一个不一样就行)
<2> Unique
建表
CREATE TABLE IF NOT EXISTS test_db.user
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`username` VARCHAR(50) NOT NULL COMMENT "用户昵称",
`city` VARCHAR(20) COMMENT "用户所在城市",
`age` SMALLINT COMMENT "用户年龄",
`sex` TINYINT COMMENT "用户性别",
`phone` LARGEINT COMMENT "用户电话",
`address` VARCHAR(500) COMMENT "用户地址",
`register_time` DATETIME COMMENT "用户注册时间"
)
UNIQUE KEY(`user_id`, `username`)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 10
properties(
"replication_num"="1"
);
插入语句
insert into test_db.user values\
(10000,'wuyanzu','北京',18,0,12345678910,'北京朝阳区','2017-10-01 07:00:00'),
(10000,'wuyanzu','北京',19,0,12345678910,'北京朝阳区','2017-10-01 0:00:00'),
(10000,'zhangsan','北京',20,0,12345678910,'北京海淀区','2017-11-15 06:10:20');
其实Unique 模型完全可以用聚合模型中的 REPLACE 方式替代。其内部的实现方式和数据存储方式也完全一样。这里不再继续举例说明。
<3> Duplicate
建表
CREATE TABLE IF NOT EXISTS test_db.example_log
(
`timestamp` DATETIME NOT NULL COMMENT "日志时间",
`type` INT NOT NULL COMMENT "日志类型",
`error_code` INT COMMENT "错误码",
`error_msg` VARCHAR(1024) COMMENT "错误详细信息",
`op_id` BIGINT COMMENT "负责人id",
`op_time` DATETIME COMMENT "处理时间"
)
DUPLICATE KEY(`timestamp`, `type`)
DISTRIBUTED BY HASH(`timestamp`) BUCKETS 10;
插入语句
insert into test_db.example_log values
('2017-10-01 08:00:05',1,404,'not found page', 101, '2017-10-01 08:00:05'),
('2017-10-01 08:00:05',1,404,'not found page', 101, '2017-10-01 08:00:05'),
('2017-10-01 08:00:05',2,404,'not found page', 101, '2017-10-01 08:00:06'),
('2017-10-01 08:00:06',2,404,'not found page', 101, '2017-10-01 08:00:07');
这种数据模型区别于 Aggregate 和 Unique 模型。数据完全按照导入文件中的数据进行存储,不会有任何聚合。即使两行数据完全相同,也都会保留。 而在建表语句中指定的 DUPLICATE KEY,只是用来指明底层数据按照那些列进行排序。
在DUPLICATE KEY的选择上,我们建议适当的选择前2-4列就可以。
这种数据模型适用于既没有聚合需求,又没有主键唯一性约束的原始数据的存储。适合日志表存储
(2) 建表语法
使用 CREATE TABLE 命令建立一个表(Table)。更多详细参数可以查看:
HELP CREATE TABLE;
建表语法:
CREATE [EXTERNAL] TABLE [IF NOT EXISTS] [database.]table_name
(column_definition1[, column_definition2, ...]
[, index_definition1[, index_definition12,]])
[ENGINE = [olap|mysql|broker|hive|es]]
[key_desc]
[COMMENT "table comment"];
[partition_desc]
[distribution_desc]
[rollup_index]
[PROPERTIES ("key"="value", ...)]
[BROKER PROPERTIES ("key"="value", ...)];
注意:默认的replication_num是3,但是不允许超过当前集群配置的be数量
(3) 数据划分
<1> 列定义
Key-->粒度,维度
Value-->度量
这里我们只以 AGGREGATE KEY 数据模型为例进行说明,更多数据模型参阅Doris数据模型。
列的基本类型,可以通过在mysql-client中执行HELP CREATE TABLE; 查看。
AGGREGATE KEY数据模型中,所有没有指定聚合方式(SUM、REPLACE、MAX、MIN)的列视为Key列。而其余则为Value列。
定义列时,可参照如下建议:
Ø Key列必须在所有Value列之前。
Ø 尽量选择整型类型。因为整型类型的计算和查找比较效率远高于字符串。
Ø 对于不同长度的整型类型的选择原则,遵循够用即可。
Ø 对于VARCHAR和STRING类型的长度,遵循够用即可。
Ø 所有列的总字节长度(包括 Key和Value)不能超过100KB。·
<2> ENGINE(引擎)
Doris 支持的引擎有: olap|mysql|broker|hive
olap 是默认的引擎, 在 Doris 中,只有这个 ENGINE 类型是由 Doris 负责数据管理和存储的。其他 ENGINE 类型,如 mysql、broker、es、hive等等,本质上只是对外部其他数据库或系统中的表的映射,以保证 Doris 可以读取这些数据。而 Doris 本身并不创建、管理和存储任何非 olap ENGINE 类型的表和数据。
<3> 分区和分桶的概念
Doris支持两层的数据划分。第一层是 Partition,支持 Range和List的划分方式。第二层是 Bucket(Tablet),仅支持Hash的划分方式。
也可以仅使用一层分区。使用一层分区时,只支持Bucket划分。
分区(partiton)
Ø Partition列可以指定一列或多列。分区列必须为KEY中的列。多列分区的使用方式在后面介绍。
Ø 不论分区列是什么类型,在写分区值时,都需要加双引号。
Ø 分区数量理论上没有上限。
Ø 当不使用Partition建表时,系统会自动生成一个和表名同名的,全值范围的 Partition。该Partition对用户不可见,并且不可删改。
Ø 创建分区时不可添加范围重叠的分区。
分桶
分桶列可以是多列,但必须为 Key 列。分桶列可以和 Partition 列相同或不同。 分桶的数量理论上没有上限。 分桶列的选择,是在查询吞吐和查询并发之间的一种权衡: 如果选择多个分桶列,则数据分布更均匀。如果一个查询条件不包含所有分桶列的等值条件,那么该查询会触发所有分桶同时扫描,这样查询的吞吐会增加,单个查询的延迟随之降低。这个方式适合大吞吐低并发的查询场景。 如果仅选择一个或少数分桶列,则对应的点查询可以仅触发一个分桶扫描。此时,当多个点查询并发时,这些查询有较大的概率分别触发不同的分桶扫描,各个查询之间的IO影响较小(尤其当不同桶分布在不同磁盘上时),所以这种方式适合高并发的点查询场景。
<4> Rang 分区(范围分区)
Range Partition建表示例
CREATE TABLE IF NOT EXISTS test_db.example_range_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE 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`, `city`, `age`, `sex`)
PARTITION BY RANGE(`date`)
( --注意范围是[)
PARTITION `p201701` VALUES LESS THAN ("2017-02-01"),
PARTITION `p201702` VALUES LESS THAN ("2017-03-01"),--这里的上界是上一条的下届2017-02-01
PARTITION `p201703` VALUES LESS THAN ("2017-04-01")
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
"replication_num" = "1",
"storage_medium" = "SSD",
"storage_cooldown_time" = "2025-01-01 12:00:00"
);
分区列通常为时间列( PARTITION BY RANGE(`date`) ),以方便的管理新旧数据
VALUES LESS THAN (...) 仅指定上界,系统会将前一个分区的上界作为该分区的下界,生成一个左闭右开的区间。
VALUES [...) 指定上下界,生成一个左闭右开的区间。
通过 VALUES [...) 同时指定上下界比较容易理解。这里举例说明,当使用VALUES LESS THAN (...) 语句进行分区的增删操作时,分区范围的变化情况:
如上 example_range_tbl 示例,当建表完成后,会自动生成如下3个分区:
p201701: [MIN_VALUE, 2017-02-01)
p201702: [2017-02-01, 2017-03-01)
p201703: [2017-03-01, 2017-04-01)
查看一个表的所有分区信息
show partitions from example_range_tbl;
插入数据匹配到p201701分区
insert into test_db.example_range_tbl values (10000,'2017-01-01','北京',20,0,'2017-01-01 06:00:00',20,10,10);
没有对应的分区, 插入不成功, 但不会抛出异常高版本会抛出
insert into test_db.example_range_tbl values (20000,'2017-11-01','北京',20,0,'2017-11-01 06:00:00',20,10,10);
当我们增加一个分区 p201705 VALUES LESS THAN ("2017-06-01"),分区结果如下(新增的分区会取它上一个分区的下届作为上届,同时更新下一个分区的上届)
新增分区语法:
alter table example_range_tbl add partition 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,则分区结果如下:
删除分区语法:
alter table example_range_tbl drop partition 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) 即如果导入的数据范围在这个空洞范围内,是无法导入的。
综上,分区的删除不会改变已存在分区的范围。删除分区可能出现空洞。通过 VALUES LESS THAN 语句增加分区时,分区的下界紧接上一个分区的上界。
<5> List分区
List partition建表示例
CREATE TABLE IF NOT EXISTS test_db.example_list_tbl
(
`user_id` LARGEINT NOT NULL COMMENT "用户id",
`date` DATE NOT NULL COMMENT "数据灌入日期时间",
`city` VARCHAR(20) NOT NULL 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`, `city`, `age`, `sex`)
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")
)
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
PROPERTIES
(
"replication_num" = "1",
"storage_medium" = "SSD",
"storage_cooldown_time" = "2025-01-01 12:00:00"
);
分区列支持BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, LARGEINT, DATE, DATETIME, CHAR, VARCHAR 数据类型,分区值为枚举值。只有当数据为目标分区枚举值其中之一时,才可以命中分区
Partition 支持通过 VALUES IN (...) 来指定每个分区包含的枚举值。
如上 example_list_tbl 示例,当建表完成后,会自动生成如下3个分区:
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_jp: ("Tokyo")
插入数据,匹配到p_cn分区
insert into test_db.example_list_tbl values (10000,'2017-01-01','Beijing',20,0,'2017-01-01 06:00:00',20,10,10)
插入不成功, 不进入任何分区
insert into test_db.example_list_tbl values (20000,'2017-01-01','shenzhen',20,0,'2017-01-01 06:00:00',20,10,10)
当我们增加一个分区 p_uk VALUES IN ("London"),分区结果如下:
alter table example_list_tbl add partition p_uk values in ('London');
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_jp: ("Tokyo")
p_uk: ("London")
当我们删除分区 p_jp,分区结果如下:
alter table example_list_tbl drop partition p_jp;
p_cn: ("Beijing", "Shanghai", "Hong Kong")
p_usa: ("New York", "San Francisco")
p_uk: ("London")
<6> 分桶
DISTRIBUTED BY HASH(`user_id`) BUCKETS 16
如果使用了 Partition,则 DISTRIBUTED ... 语句描述的是数据在各个分区内的划分规则。如果不使用 Partition,则描述的是对整个表的数据的划分规则。
分桶列可以是多列,但必须为 Key 列。分桶列可以和 Partition 列相同或不同。
分桶的数量理论上没有上限。
分桶列的选择,是在查询吞吐和查询并发之间的一种权衡:
如果选择多个分桶列,则数据分布更均匀。如果一个查询条件不包含所有分桶列的等值条件,那么该查询会触发所有分桶同时扫描,这样查询的吞吐会增加,单个查询的延迟随之降低。这个方式适合大吞吐低并发的查询场景。
如果仅选择一个或少数分桶列,则对应的点查询可以仅触发一个分桶扫描。此时,当多个点查询并发时,这些查询有较大的概率分别触发不同的分桶扫描,各个查询之间的IO影响较小(尤其当不同桶分布在不同磁盘上时),所以这种方式适合高并发的点查询场景。
(4) 动态分区
<1> 使用方式
动态分区的规则可以在建表时指定,或者在运行时进行修改。当前仅支持对单分区列的分区表设定动态分区规则。
建表时指定
CREATE TABLE tbl1
(...)
PROPERTIES
(
"dynamic_partition.prop1" = "value1",
"dynamic_partition.prop2" = "value2",
...
)
运行时修改
ALTER TABLE tbl1 SET
(
"dynamic_partition.prop1" = "value1",
"dynamic_partition.prop2" = "value2",
...
)
<2> 相关参数
| 参数名 | 介绍 |
|---|---|
| dynamic_partition.enable | 是否开启动态分区特性,可指定true或false,默认为true 如果为 FALSE,则 Doris 会忽略该表的动态分区规则。 |
| dynamic_partition.time_unit | 动态分区调度的单位,可指定HOUR、DAY、WEEK、MONTH。 HOUR,后缀格式为 yyyyMMddHH,分区列数据类型不能为 DATE。 DAY,后缀格式为 yyyyMMdd。 WEEK,后缀格式为yyyy_ww。即当前日期属于这一年的第几周。 MONTH,后缀格式为 yyyyMM。 |
| dynamic_partition.time_zone | 动态分区的时区,如果不填写,则默认为当前机器的系统的时区 |
| dynamic_partition.start | 动态分区的起始偏移,为负数。根据 time_unit属性的不同,以当天(星期/月)为基准,分区范围在此偏移之前的分区将会被删除。如果不填写默认值为Interger.Min_VALUE 即-2147483648,即不删除历史分区 |
| dynamic_partition.end | 动态分区的结束偏移,为正数。根据 time_unit 属性的不同,以当天(星期/月)为基准,提前创建对应范围的分区 |
| dynamic_partition.prefix | 动态创建的分区名前缀 |
| dynamic_partition.buckets | 动态创建的分区所对应分桶数量 |
| dynamic_partition.replication_num | 动态创建的分区所对应的副本数量,如果不填写,则默认为该表创建时指定的副本数量。 |
| dynamic_partition.start_day_of_week | 当 time_unit 为 WEEK 时,该参数用于指定每周的起始点。取值为 1 到 7。其中 1 表示周一,7 表示周日。默认为 1,即表示每周以周一为起始点 |
| dynamic_partition.start_day_of_month | 当 time_unit 为 MONTH 时,该参数用于指定每月的起始日期。取值为 1 到 28。其中 1 表示每月1号,28 表示每月28号。默认为 1,即表示每月以1号位起始点。暂不支持以29、30、31号为起始日,以避免因闰年或闰月带来的歧义 |
| dynamic_partition.create_history_partition | 默认为 false。当置为 true 时,Doris 会自动创建所有分区,当期望创建的分区个数大于 max_dynamic_partition_num 值时,操作将被禁止。当不指定 start 属性时,该参数不生效。 |
| dynamic_partition.hot_partition_num | 指定最新的多少个分区为热分区。对于热分区,系统会自动设置其 storage_medium 参数为SSD,并且设置 storage_cooldown_time。 hot_partition_num 是往前 n 天和未来所有分区 我们举例说明。假设今天是 2021-05-20,按天分区,动态分区的属性设置为:hot_partition_num=2, end=3, start=-3。则系统会自动创建以下分区,并且设置 storage_medium 和 storage_cooldown_time 参数: 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.reserved_history_periods | 需要额外保留的历史分区的时间范围。当dynamic_partition.time_unit 设置为 "DAY/WEEK/MONTH" 时,需要以 [yyyy-MM-dd,yyyy-MM-dd],[...,...] 格式进行设置。当dynamic_partition.time_unit 设置为 "HOUR" 时,需要以 [yyyy-MM-dd HH:mm:ss,yyyy-MM-dd HH:mm:ss],[...,...] 的格式来进行设置。如果不设置,默认为 "NULL"。 我们举例说明。假设今天是 2021-09-06,按天分类,动态分区的属性设置为: time_unit="DAY", \ end=3, \ start=-3, \ reserved_history_periods="[2020-06-01,2020-06-20],[2020-10-31,2020-11-15]"。 则系统会自动保留: ["2020-06-01","2020-06-20"], ["2020-10-31","2020-11-15"] 或者 time_unit="HOUR", \ end=3, \ start=-3, \ reserved_history_periods="[2020-06-01 00:00:00,2020-06-01 03:00:00]". 则系统会自动保留: ["2020-06-01 00:00:00","2020-06-01 03:00:00"] 这两个时间段的分区。其中,reserved_history_periods 的每一个 [...,...] 是一对设置项,两者需要同时被设置,且第一个时间不能大于第二个时间``。 |
<3> 创建历史分区规则
当create_history_partition为 true,即开启创建历史分区功能时,Doris 会根据 dynamic_partition.start 和 dynamic_partition.history_partition_num 来决定创建历史分区的个数。
假设需要创建的历史分区数量为 expect_create_partition_num,根据不同的设置具体数量如下:
create_history_partition = true
dynamic_partition.history_partition_num 未设置,即 -1.
则expect_create_partition_num = end - start + 1;
② dynamic_partition.history_partition_num 已设置
则expect_create_partition_num = end - max(start, -histoty_partition_num) + 1;
create_history_partition = false
不会创建历史分区,expect_create_partition_num = end - 0 + 1;
当 expect_create_partition_num > max_dynamic_partition_num(默认500)时,禁止创建过多分区。
总结:今天的分区(1) + end(未来的分区) + 过去的分区(start 和 history-num 谁少听谁的)
假设今天是 2021-05-20,按天分区,动态分区的属性设置为:create_history_partition=true, end=3, start=-3, history_partition_num=1,则系统会自动创建以下分区:
p20210519
p20210520
p20210521
p20210522
p20210523
history_partition_num=5,其余属性与 1 中保持一直,则系统会自动创建以下分区:
p20210517
p20210518
p20210519
p20210520
p20210521
p20210522
p20210523
history_partition_num=-1 即不设置历史分区数量,其余属性与 1 中保持一直,则系统会自动创建以下分区:
p20210517
p20210518
p20210519
p20210520
p20210521
p20210522
p20210523
注意:
动态分区使用过程中,如果因为一些意外情况导致 dynamicpartition.start 和 dynamicpartition.end 之间的某些分区丢失,那么当前时间与 dynamicpartition.end 之间的丢失分区会被重新创建,dynamicpartition.start与当前时间之间的丢失分区不会重新创建。
<4> 案例
创建动态分区表
create table student_dynamic_partition1
(
id int,
time date,
name varchar(50),
age int
)
duplicate key(id,time)
PARTITION BY RANGE(time)()
distributed by hash(`id`)
PROPERTIES(
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.create_history_partition" = "true",
"dynamic_partition.history_partition_num" = "3",
"dynamic_partition.start" = "-7",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "10",
"replication_num" = "1"
);
查看动态分区表调度情况
SHOW DYNAMIC PARTITION TABLES
LastUpdateTime: 最后一次修改动态分区属性的时间
LastSchedulerTime: 最后一次执行动态分区调度的时间
State: 最后一次执行动态分区调度的状态
LastCreatePartitionMsg: 最后一次执行动态添加分区调度的错误信息
LastDropPartitionMsg: 最后一次执行动态删除分区调度的错误信息
查看表的分区情况
SHOW PARTITIONS FROM student_dynamic_partition1\G
插入测试数据(需要修改日期)
insert into student_dynamic_partition1 values(1,'2022-08-11 11:00:00','name1',18);
insert into student_dynamic_partition1 values(1,'2022-08-10 11:00:00','name1',18);
insert into student_dynamic_partition1 values(1,'2022-08-13 11:00:00','name1',18);
动态分区表与手动分区表相互转换
对于一个表来说,动态分区和手动分区可以自由转换,但二者不能同时存在,有且只有一种状态。
手动分区转换为动态分区
如果一个表在创建时未指定动态分区,可以通过ALTER TABLE在运行时修改动态分区相关属性来转化为动态分区,具体示例可以通过HELP ALTER TABLE查看。
注意:如果已设定dynamic_partition.start,分区范围在动态分区起始偏移之前的历史分区将会被删除。
动态分区转换为手动分区
ALTER TABLE tbl_name SET ("dynamic_partition.enable" = "false")
关闭动态分区功能后,Doris将不再自动管理分区,需要用户手动通过ALTER TABLE 的方式创建或删除分区。
(5) Rollup
Rollup的基本思想是创建数据的汇总版本,这样当执行某些查询时,如聚合查询或范围查询,可以直接使用这些预计算的结果而不是从原始数据中重新计算。这样可以显著减少查询所需的时间,特别是在处理大量数据的情况下。
例如,如果你有一个包含每天销售记录的数据表,并且经常需要查询每个月或者每个季度的总销售额,那么你可以创建一个Rollup来预先计算每个月或每个季度的总销售额,并将这些信息存储起来。这样,当你查询每个月或季度的销售额时,Doris可以直接返回Rollup中的数据,而不需要扫描整个原始数据集。
<1> Aggregate 和 Unique 模型中的 ROLLUP
查看表结构信息
desc example_site_visit all;
创建 rollup
示例1:比如需要查看某个用户的总消费,那么可以建立一个只有user_id和cost的rollup
alter table example_site_visit add rollup rollup_cost_userid(user_id,cost);
示例2:获得不同城市,不同年龄段用户的总的消费、最长和最短页面驻留时间
alter table example_site_visit add rollup rollup_city_age_cost_maxd_mind(city,age,cost,max_dwell_time,min_dwell_time);
explain SELECT city, age, sum(cost), max(max_dwell_time), min(min_dwell_time) FROM example_site_visit GROUP BY city, age;
explain SELECT city, sum(cost), max(max_dwell_time), min(min_dwell_time) FROM example_site_visit GROUP BY city;
explain SELECT city, age, sum(cost), min(min_dwell_time) FROM example_site_visit GROUP BY city, age;
查看命令完成状况
SHOW ALTER TABLE ROLLUP\G;
<2> Duplicate 模型中的 ROLLUP
因为 Duplicate 模型没有聚合的语意。所以该模型中的 ROLLUP,已经失去了“上卷”这一层含义。而仅仅是作为调整列顺序,以命中前缀索引的作用。下面详细介绍前缀索引,以及如何使用ROLLUP改变前缀索引,以获得更好的查询效率。可以按照指定的列进行排序存储。在这种数据结构上,以排序列作为条件进行查找,会非常的高效。
Base 表结构如下:
| ColumnName | Type |
|---|---|
| user_id | BIGINT |
| age | INT |
| message | VARCHAR(100) |
| max_dwell_time | DATETIME |
| min_dwell_time | DATETIME |
我们可以在此基础上创建一个 ROLLUP 表:
| ColumnName | Type |
|---|---|
| age | INT |
| user_id | BIGINT |
| message | VARCHAR(100) |
| max_dwell_time | DATETIME |
| min_dwell_time | DATETIME |
可以看到,ROLLUP和Base表的列完全一样,只是将user_id和age的顺序调换了。那么当我们进行如下查询时:
SELECT * FROM table where age=20 and message LIKE "%error%";
会优先选择ROLLUP 表,因为ROLLUP的前缀索引匹配度更高。
(6) 物化视图
<1> 原理
物化视图是将预先计算(根据定义好的 SELECT 语句)好的数据集,存储在 Doris 中的一个特殊的表。
物化视图的出现主要是为了满足用户,既能对原始明细数据的任意维度分析,也能快速的对固定维度进行分析查询。
场景
Ø 分析需求覆盖明细数据查询以及固定维度查询两方面(主要)。
Ø 查询仅涉及表中的很小一部分列或行。
Ø 查询包含一些耗时处理操作,比如:时间很久的聚合操作等。
Ø 查询需要匹配不同前缀索引。
<2> 使用
原则 1
从查询语句中抽象出,多个查询共有的分组和聚合方式作为物化视图的定义。
原则 2
不需要给所有维度组合都创建物化视图。
案例如下:
案例1:假设用户有一张销售记录明细表,存储了每个交易的交易id,销售员,售卖门店,销售时间,以及金额。
1) 创建一个 Base 表
create table sales_records(
record_id int,
seller_id int,
store_id int,
sale_date date,
sale_amt bigint
)
distributed by hash(record_id)
properties("replication_num" = "1");
插入数据
insert into sales_records values(1,2,3,'2020-02-02',10);
2) 基于这个Base表的数据提交一个创建物化视图的任务
create materialized view store_amt as
select
store_id,
sum(sale_amt)
from sales_records
group by store_id;
3) 检查物化视图是否构建完成
由于创建物化视图是一个异步的操作,用户在提交完创建物化视图任务后,需要异步的通过命令检查物化视图是否构建完成。
SHOW ALTER TABLE MATERIALIZED VIEW FROM test_db;
查看Base表的所有物化视图
desc sales_records all;
4) 检验当前查询是否匹配到了合适的物化视图
EXPLAIN SELECT store_id, sum(sale_amt) FROM sales_records GROUP BY store_id;
5) 删除物化视图语法
DROP MATERIALIZED VIEW 物化视图名 on Base表名;
4.Flink读写Doris
(0) 导入依赖
如果doris 连接器官方仓库还没有,需要根据源码手动编译,以Flink1.17为例
下载和编译源码
git clone https://github.com/apache/doris-flink-connector.git
cd doris-flink-connector/flink-doris-connector
./build.sh
选择 1.17
编译成功之后进入 target目录 可以看到编译成功的jar 包
把 jar 安装到本地库(windows)
mvn install:install-file -DgroupId=org.apache.doris -DartifactId=flink-doris-connector-1.17 -Dversion=1.4.0 -Dpackaging=jar -Dfile=./flink-doris-connector-1.4.0-SNAPSHOT.jar
导入依赖
<dependency>
<groupId>org.apache.doris</groupId>
<artifactId>flink-doris-connector-1.17</artifactId>
<version>1.5.2</version>
</dependency>
(1) API的方式读写(流)
/**
* @author Felix
* @date 2024/9/03
* 该案例演示了Flink从Doris中读写数据_API
*/
public class Test02_Doris_API {
public static void main(String[] args) throws Exception {
//TODO 1.环境准备
//1.1 指定流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//1.2 设置并行度
env.setParallelism(1);
//TODO 2.检查点相关的设置(如果处理的是无界数据,在向Doris中写数据额的时候,必须开启检查点)
env.enableCheckpointing(5000L, CheckpointingMode.EXACTLY_ONCE);
//TODO 3.从Doris表中读取数据
DorisOptions.Builder builder = DorisOptions.builder()
.setFenodes("hadoop102:7030")
.setTableIdentifier("test.table1")
.setUsername("root")
.setPassword("aaaaaa");
DorisSource<List<?>> dorisSource = DorisSource.<List<?>>builder()
.setDorisOptions(builder.build())
.setDorisReadOptions(DorisReadOptions.builder().build())
.setDeserializer(new SimpleListDeserializationSchema())
.build();
DataStreamSource<List<?>> stream1 = env.fromSource(dorisSource, WatermarkStrategy.noWatermarks(), "doris source");
stream1.print();
//TODO 4.向Doris表中写入数据
DataStreamSource<String> source = env
.fromElements(
"{\"siteid\": \"550\", \"citycode\": \"1001\", \"username\": \"ww\",\"pv\": \"100\"}");
//如果是json字符串必须配置下面的properties
Properties props = new Properties();
props.setProperty("format", "json");
props.setProperty("read_json_by_line", "true"); // 每行一条 json 数据
DorisSink<String> sink = DorisSink.<String>builder()
.setDorisReadOptions(DorisReadOptions.builder().build())
.setDorisOptions(DorisOptions.builder() // 设置 doris 的连接参数
.setFenodes("hadoop102:7030")
.setTableIdentifier("test.table1")
.setUsername("root")
.setPassword("aaaaaa")
.build())
.setDorisExecutionOptions(DorisExecutionOptions.builder() // 执行参数
//.setLabelPrefix("doris-label") // stream-load 导入的时候的 label 前缀
.disable2PC() // 开启两阶段提交后,labelPrefix 需要全局唯一,为了测试方便禁用两阶段提交
.setDeletable(false)
.setBufferCount(3) // 用于缓存stream load数据的缓冲条数: 默认 3
.setBufferSize(1024*1024) //用于缓存stream load数据的缓冲区大小: 默认 1M
.setMaxRetries(3)
.setStreamLoadProp(props) // 设置 stream load 的数据格式 默认是 csv,根据需要改成 json
.build())
.setSerializer(new SimpleStringSerializer())
.build();
source.sinkTo(sink);
env.execute();
}
}
(2) sql的方式读写
/**
* @author Felix
* @date 2024/9/03
* 该案例演示了Flink从Doris中读写数据_SQL
*/
public class Test01_Doris_SQL {
public static void main(String[] args) {
//TODO 1.环境准备
//1.1 指定流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//1.2 设置并行度
env.setParallelism(1);
//1.3 指定表执行环境
StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);
//TODO 2.检查点相关的设置(如果处理的是无界数据,在向Doris中写数据额的时候,必须开启检查点)
env.enableCheckpointing(5000L, CheckpointingMode.EXACTLY_ONCE);
//TODO 3.从Doris表中读取数据
tableEnv.executeSql("CREATE TABLE flink_doris ( " +
" siteid INT, " +
" citycode SMALLINT, " +
" username STRING, " +
" pv BIGINT " +
" ) " +
" WITH ( " +
" 'connector' = 'doris', " +
" 'fenodes' = 'hadoop102:7030', " +
" 'table.identifier' = 'test.table1', " +
" 'username' = 'root', " +
" 'password' = 'aaaaaa' " +
") ");
// 读
tableEnv.sqlQuery("select * from flink_doris").execute().print();
//TODO 4.向Doris表中写入数据
tableEnv.executeSql("CREATE TABLE flink_doris ( " +
" siteid INT, " +
" citycode INT, " +
" username STRING, " +
" pv BIGINT " +
")WITH (" +
" 'connector' = 'doris', " +
" 'fenodes' = 'hadoop102:7030', " +
" 'table.identifier' = 'test.table1', " +
" 'username' = 'root', " +
" 'password' = 'aaaaaa', " +
" 'sink.properties.format' = 'json', " +
" 'sink.enable-2pc' = 'false' " + // 测试阶段可以关闭两阶段提交,方便测试
") ");
tableEnv.executeSql("insert into flink_doris values(33, 3, '深圳', 3333)");
}
}