Doris入门
简介
之前交百度Palo,2018年贡献到Apache社区后,更名Doris
一个现代化的MPP分析型
- 亚秒级别相应
- 架构简单易于运维
- 支持10PB以上的超大数据集
满足多种数据分析需求,MPP架构,支持sql,兼容mysql,高效的聚合表技术,高性能高可用高可靠,弹性伸缩
固定历史报表,实时数据分析,交互式数据分析,探索式数据分析
再百度内部有超过200个产品线再使用,部署机器超过1000台,单一业务最大可到达上百TB
架构
Doris的架构很简洁,只社FE(Frontend),BE(Backend)俩种角色,俩个进程,不依赖于外部组件,都可线性扩展
FE
存储维护集群元数据:负责接收,解析查询请求,规划查询计划,调度查询执行,返回查询结果,主要有三个角色
leader和Follower
主要是原来达到元数据的高可用,保证单节点宕机的情况下,元数据能够实时 在线恢复,而不影响整个业务
observer
原来扩展查询节点,同时起到元数据备份的作用.作为在发现集群压力非常大的情况下,需要扩展整个查询能力,那么可以加observer的节点.observer不参与任何的写入只参与读取
BE(Backend)
负责物理数据的存储和计算 依据FE生成的物理计划,分布式执行查询
数据的可靠性,由BE保证, BE会对整个数据存储多副本或者是三副本.副本数可根据需求动态调整
mysql client
Doris借助Mysql协议,用户使用任意的mysql的ODBC/JDBC以及MYSQL的客户端都可以直接访问Doris
broker
broker为一个独立的无状态进程.封装了文件系统接口,提供Doris读取远端存储系统中文件的能力,包括HDFS,S3,BOS等
- 生产环境建议不要放在Doris安装目录下,最好是单独磁盘(如果有SSD最好)
- 如果机器有多个ip,比如内网外网,虚拟机docker等,需要进行ip绑定,才能正确识别
- JAVA_OPTS默认java最大堆内存为4g建议生产环境修改到8g以上
基本概念
数据结构
doris因为是mysql协议所以体验上和mysql很类似
首先ddl和dml语法上基本和mysql相同
和mysql一直有row&Column&table的概念
不同的是
- doris的Column分为key和value,key有点类似mysql的索引,同事提供索引的快速查询功能
- doris的value可以根据key自动聚合,也就是doris的数据模型
doris字段分为key和value,key有以下几种方式
doris字段分为key和value,key有以下几种方式
模型 | 特点 | 优势 | 劣势 | 建表指定 |
---|---|---|---|---|
aggregate模型 | 按照key进行预聚合目前有四种聚合方式:sum:求和,多行的value进行累加。replace:替代,下一批数据中的value会替换之前导入过的行中的value。max:保留最大值。min:保留最小值。 | 降低聚合查询时所需扫描的数据量和查询的计算量,非常适合有固定模式的报表类查询场景 | 对count()查询很不友好固定了聚合方式例:已经设置了max聚合,表取数对该字段min求最小值就会出现不一致问题不保留明细数据,只有聚合后的数据解决:key加上时间戳 | AGGREGATE KEY(user_id , date ) |
uniq模型 | 唯一Key,同一key数据自动覆盖,本质上是聚合模型的replace | 保证 Key 的唯一性 | 无法利 用 ROLLUP 等预聚合带来的查询优势 | UNIQUE KEY(user_id , date ) |
duplicate模型 | 既没有主键,也没有聚合需求时 | 不受聚合模型的约束,可以发挥列模型的优势 | 无法利 用 ROLLUP 等预聚合带来的查询优势 | DUPLICATE KEY(user_id , date ) |
- key列必须在所有value列之前
- 劲量选择整形类型.因为整形类型的计算和查找比较效率高于字符串
- 对不同长度的整形类型的选择原则,遵循够用即可
- 对于varchar和string类型的长度醉熏够用即可
- 所有列的总字段长度 包括key和value(不能超过100kb)
选择建议
数据模型在建表时就已经确定,且无法修改,所以选择一个合适的数据模型非常重要
aggregate模型可以通过预聚合,极大降低聚合查询所需要扫描数量和查询的计算量,非常适合有固定模式报表查询场景.但是该模型对count(*)查询很不友好.同事因为固定了value列上的聚合方式,在教你写其他类型的聚合查询时,需要考虑语义的正确性
uniq模型对需要唯一约束的场景,可以保证主键的唯一性约束.但是无法利用RALLUP预聚合带来的查询有事(因为本质是Replace,没有sum这种聚合方式)
duplicate适合任意维度的ad-hoc差U型你.虽然同样无法利用聚合的特性,但是不受聚合模型的约束,可以发挥列存模型的优势(只需读取相关列,而不需要读取所有key列)
再业务上有频繁count(*)
查询时,我们建议用户通过增加一个值为1的聚合类型的sum的列来模拟count(*)
分区分桶
Doris支持俩层的数据划分,第一层是patition,支持range和List的划分方式.第二层是Bucket(Tablet),仅支持hash的划分方式
也可以仅适用一层分区.适用一层分区时,只支持bucket划分
partition
- partition列可以指定一列或多列.分区类必须为key列
- 不论分区是什么类型,再写分区值时,都要加双引号
- 分区理论上没有上限
- 当不使用partition建表时,系统会自动生成一个和表名同名的,全值范围的partition.该partition对用户不可见,不可删改
range分区
分区通常为时间列,以方便管理 新就数据,不可添加范围重叠的分区
#通常为时间列,范围划分分区
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")
)
#删除分区会出现空洞
p201701: [MIN_VALUE, 2017-02-01)
p201702: [2017-02-01, 2017-03-01)
p201704: [2017-04-01, 2017-05-01)
删除p201703: [2017-03-01, 2017-04-01),不会改变已生成的分区,该部分的数据无法导入
多列分区:当用户插入数据时,分区列值会按照顺序依次比较,最终得到对应的分区
list分区
##直接指定,但是可以命中多个
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") )
- 不设置分区
其实当不使用partition by建表的时候,系统会自动生成一个和表名同名的,全值范围的partition
- 动态分区
createtable student_dynamic_partition1 (id int,timedate,name varchar(50),age int
)
duplicate key(id,time)
PARTITIONBYRANGE(time)()
DISTRIBUTED BY HASH(id) buckets 10
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" = "10","replication_num" = "1");
是doris0.12版本引入的.目的再对表级别实现生命周期管理(TTL)减少用户负担
原理
再某些场景下,用户会降表按照天进行划分,每天定时执行例行任务,这是需要使用方手动管理分区,否则可能有雨适用房没有创建分区导致数据导入失败,这给使用方带来了额外成本
通过动态分区的功能,用户可以在建表时动态设定分区的规则,FE会启动一个后台线程,根据用户指定的规则创建和删除分区,用户也可以在运行时对现有规则进行变更
分桶
分桶可以是多列,但必须为key列,分桶可以和partition列系统或者不同
分桶列的选择是在查询吞吐和查询并发之间的一种权衡
- 一个或少数分桶列
对应的点查询可以仅触发一个分桶扫描,当多个点查询并发时,这些查询有较大的概率分别触发不同的分桶扫描,各 个查询之间的 IO 影响较小,适合 高并发的点查询场景
- 多个分桶列
数据分布更均匀,但一个查询条件不包含所有分桶列的等值条件,会触发所有分桶同时扫描,查询吞吐会增加,单个查询的延迟随之降低,适合大吞吐低并发的查询场景
分桶的理论数量没有上限
使用符合分区的场景
大部分场景都会使用复合分区,比如
- 有时间维度或者类似带有序值的维度,可以一这种类维度作为区分列.区分粒度可以根据导入频次,分区数据量等进行评估
- 隶书数据删除需求,比如保留最近N天的数据,适用复合分区,可以通过删除历史分区来达到目的,也可以在指定分区内发哦是哪个dleete语句进行删除
- 解决数据倾斜问题:每个分区可以单独指定分桶数量,如按天分区,当每天的数据量差异很大时,可以通过指定分区的分桶数,合理划分不同分区的数据,分桶列建议选择区分度很大的列
properties
再建表语句最后
每个Tablet的副本数量.默认为3,建议保持默认即可.再建表语句中,所有partition中的Tablet副本数量统一制定.而在新加分区时.可以单独指定新分区中的tablet的副本数量
副本可以在运行时修改建议保持奇数
最大部分数量取决于集群中独立的ip数量(注意不是BE数量,意思是说一台机器部署多态be也没有作用).Doris中副本的分布的原则是,不允许同一个Tablet副本分布在同一台物理机上,而识别物理机即通过了IP.所以同一台物理机上部署了3个或更多BE实例,如果这些BE的IP相同,则依然只能设置副本数为1
对于一些小,并且不频繁更新的维度表可以考虑设置更多副本数.这样在join查询时,可以有更大概率进行本地数据join
storage_medium&storage_cooldown_time
BE的存储目录可以显式指定为SSD或者HDD.建表时可以指定存储介质,(不会检查是否与实际介质相符,只是作为记号而已)
engine
再dorids中只有engine类型是doris负责管理存储的,其他的类型如mysql,broker,es等,本质上只是对外部其他数据库或系统表中的映射,保证doris可以读取这些数据.而doris本身不创建,管理和存储任何非olap engine类型的表和数据
RollUP
ROLLUP在多维分析是上卷的意思,即将数据按某种指定的粒度进行进一步聚合
再doris中我们将用户通过建表语句创建出来的表称为base表base表中保存着用户建表一句指定方式存储的基础数据
再base表之上,我们可以创建多个rollup表.这些rollup的数据是基于base表产生的,并且在物理上是独立存储的
rollup表的基本作用在于在base表的基础上,获得更粗粒度的聚合数据
比如要查看某个用户的消费总额,可以建立一个只有user_id和cost的rollup
alter table exaple_site_vist2 add rollup rollup_const_userid(userid,cost)
查看表的结构信息
desc example_site_vist2
通过explain查看执行计划
explain select user_id,sum(cost) from table group by user_id
doris会自动命中这个rollup表,从而只需扫描极少的数据量即可完成这次聚合查询
duplicate中的rollup
因为duplicate模型没有聚合的语义,所以该模型中的rollup是去了上卷这一层含义.而仅仅是作为调整列顺序,以命中前缀索引的作用
前缀索引
不同于传统的数据库设计doris不支持在任意列上创建索引,doris这类mpp架构的olap数据库,通常都是通过提高并发,来处理大量数据的
本质上Doris的数据存储在类似SStable的数据结构.该结构是一种有序的数据结构,可以按照指定的列进行排序存储.在这种数据结构上,已排序列作为条件查找会非常的高效
在aggregate uniq和duplicate三种数据模型中底层数据存储是按照各自建表语句中,aggregate,uniq key和duplicate key中指定的列进行排序存储的.而前缀即在排序的基础上,实现的一种根据前缀列,快速查询数据索引方式
我们将一行数据的前36个字节作为这行数据的前缀索引,遇到varchar类型会直接截断
rollup可以调整前缀索引
建表时指定了列顺序,所以一个表只有一种前缀索引.这对于使用其他不能命中前缀索引的列作为条件进行查询来说效率上可能无法满足需求,因此我们可以通过创建rollup来人为调增列顺序
注意
rollup是附属于base表的,可以看做是base表的一种辅助数据结构用户可以在base表的基础上,创建或删除ROLLUP但是不能再查询中显式指定某rollup是否命中ROLLUP完全由doris系统自动决定
rollup的数据是独立物理存储的.因此创建的rollup越多,占用磁盘的空间越大.同事对导入速度也会有影响
(导入的elt阶段会自动产生所有rollup的数据),但是不会降低查询效率
rollup与base表是完全同步的
rollup中列的聚合方式,与base表完全相同,再创建rollup无需指定,也不能修改
是否能命中rollup的必要条件是查询设计的所有列是否都存在该rollup列中
物化视图
物化视图就是包含了查询结果的数据库对象,也能是对远程数据的本地copy,也可能是一个表或多表join后结果的行或列的子集,也可能是聚合后的结果,说白了就是预先存储查询结果的一种数据库对象
再doris中的物化视图,就是查询结果预先存储起来的特殊的表
物化视图的出现主要是为了满足用户,技能对原始明细数据任意维度分析,也能快速的对固定维度进行分析查询
适用场景
- 分析需求覆盖明细数据查询以及固定维度查询俩方面
- 查询仅设计表中很小一部分列或行
- 查询包含一些耗时处理操作,比如:事件很久的聚合操作等
- 查询需要匹配不同前缀索引
优势
对于那些经常重复使用相同子查询结果的查询性能大幅提升
doris自动维护物化视图的数据,无论是新的导入,还是删除都能保证base表和物化视图表的数据一致性.无需任何额外的人工维护成本
查询时,会自动匹配到最优物化视图,并直接从物化视图中读取数据
自动物化视图的数据会造成一些维护开销
物化视图VS Rollup
物化视图在覆盖了Rolluup功能的同时,还能支持更丰富的聚合函数.所以物化视图其实是Rollup的一个超集
也就是说之前alter table add rollup 的语法现在均可以通过create materialized biew实现
原理
Doris系统提供了一套对于物化视图的DDL语法,包括创建查看,删除,DDL的语法和Pgsql,oracle都是一致的.但是Doris目前创建物化视图只能在单表操作,不支持join
创建物化视图要根据查询语句的特点来确定创建一个什么样的物化视图.并不是说物化视图和某个查询语句一模一样就好
- 从查询语句中,抽象出,多个查询共有的分组和聚合方式作为物化视图的定义
- 不需要给所有维度都创建物化视图
物化视图也是自动完成优化的分为俩个步骤
- 根据查询条件筛选出一个最优的物化视图:这一部的输入是所有物化视图表的元数据,根据查询的条件从候选中输出一个最优的物化视图
- 根据选出的物化视图对查询进行改写,最终达到直接查询物化视图的目的
限制
- 目前支持的聚合函数包括sum,min,max,count,以及计算pv,uv,留存率等常用的去重算法hll_union,count(distinct)的算法bitmap_union
- 物化视图聚合函数的参数不支持表达式仅支持单列sum(a+b)不支持
适用物化视图后由于是实际上是损失了部分维度数据的.所以对表的dml类型操作会有限制
- 如果表的物化视图key中不包含输出语句中的条件列,则删除语句不能执行
- 比如想要删除渠道为app端的数据,由于存在一个物化视图并不包含渠道这个字段,则这个删除不能执行,因为这个删除无法被执行,这时候你只能把物化视图删除,然后删除完数据后,重新构建一个新的物化视图.
- 单表上过多的物化视图会影响导入的效率
- 系统列不同聚合函数,不能同时出现在一张物化视图中
删除数据
delete from语句类似标准的delete语法
delete from student_kafka where id=1
该语句只能针对partition级别进行删除.如果一个表有多个partition含有需要删除的数据,则需要执行多次针对不同partition的delete语句.而如果是没有使用partition的表,partition的名称及表名
where后面的条件针对此只能针对key列,并且谓词之间只能通过and连接.如果想实现or语义,要执行多条delete
delete是一个同步命令,命令返回代表执行成功
从代码实现的角度,delete是一种特殊的导入操作,该命令锁导入的内容,也是一个新的数据版本,只是该版本中只包含命中指定的删除条件.在实际执行查询时,会根据这些条件进行查询过滤,所以不建议大量使用delete命令,会导致查询效率降低
数据真正删除是在be进行数据comnaction时进行的,所以delete完毕之后不会立刻释放空间
delete一个较强的限制条件是,再执行该命令时,对应的表不能有正在进行导入任务.
deletee也有一个类似quorum_finished的状态.如果delete只在多数副本上完成了,也会返回用户成功.但是会在后台生成一个异步的delete job(async delete job)老继续完成对剩余副本的删除操作.
删除分区
该命令可以直接删除指定的分区.因为partion是逻辑上最小的数据管理单元,所以用drop partition命令可以很轻量完成数据删除工作.并且该命令不受load以及任何其他操作的限制,同时不会影响查询的效率,是比较推荐的数据删除方式
该命令是同步命令,执行成功即生效,后台真正删除数据可能会延迟10分钟