Doris-计算特性

思路

梳理Doris的计算特性,重点关注其Join能力、去重能力、物化视图能力、资源隔离的机制,梳理其在各种场景复杂计算场景下的优化机制/优化参数,了解各种优化手段的技术原理及优缺点。

1 全新优化器

1.1 如何开启

# 新优化器可以更好的支持诸如多层子查询嵌套等更为复杂的查询语句;
# 新优化器在无需手工优化查询的情况下,总体查询时间与旧优化器手工优化后的查询时间相近;
# 新优化器的所有优化规则,均在逻辑执行计划树上完成
# 为了能够充分利用新优化器的 CBO 能力,强烈建议对查询延迟敏感的表,执行analyze 语句,以收集列统计信息

# 在SQL会话时首先执行开启命令,如下
SET enable_nereids_planner=true;

# 验证新优化器是否生效,可以使用EXPLAN命令(查询执行计划的)
EXPLAN SELECT * FROM tbname LIMIT 10;

# 若要退回到旧优化器,则执行
SET enable_fallback_to_original_planner=true;

1.2 统计信息

# 相较于旧优化器新增的点:
通过收集统计信息有助于优化器了解数据分布特性,在进行CBO(基于成本优化)时优化器会利用这些统计信息来计算谓词的选择性,并估算每个执行计划的成本。从而选择更优的计划以大幅提升查询效率。

# 默认情况下(不指定WITH SAMPLE),会对一张表全量采样。 对于比较大的表(5GiB以上),从集群资源的角度出发,一般情况下建议采样收集,采样的行数建议不低于400万行。

1.2.1 使用ANALYZE语句手动收集

# 对一张表全量收集统计信息:
ANALYZE TABLE lineitem;

# 对一张表按照10%的比例采样收集统计数据:
ANALYZE TABLE lineitem WITH SAMPLE PERCENT 10;

# 对一张表按采样10万行收集统计数据
ANALYZE TABLE lineitem WITH SAMPLE ROWS 100000;

1.2.1 自动收集

# 此功能从2.0.3开始正式支持,默认为全天开启状态。

# 打开自动收集
ALTER CATALOG external_catalog SET PROPERTIES ('enable.auto.analyze'='true'); 

# 关闭自动收集
ALTER CATALOG external_catalog SET PROPERTIES ('enable.auto.analyze'='false'); 

1.2.3 作业管理

列名
说明
job_id
统计作业 ID
catalog_name
catalog 名称
db_name
数据库名称
tbl_name
表名称
col_name
列名称列表
job_type
作业类型
analysis_type
统计类型
message
作业信息
last_exec_time_in_ms
上次执行时间
state
作业状态
schedule_type
调度方式

# 查看所有的统计作业
MySQL [ssb]> SHOW ANALYZE\G;

# 根据表名查询统计作业
MySQL [ssb]> SHOW ANALYZE tbName\G;

# 查看每列统计信息收集情况
MySQL [ssb]> MySQL [ssb]> SHOW ANALYZE TASK STATUS [job_id]

# 查看列统计信息
MySQL [ssb]> SHOW COLUMN [cached] STATS table_name [ (column_name [, ...]) ];  //例: show column stats customer(c_phone,c_name);

# 表收集概况
MySQL [ssb]> SHOW TABLE STATS table_name;

# 终止统计作业
MySQL [ssb]> KILL ANALYZE job_id;  //例: KILL ANALYZE 52357;

1.3 会话变量及配置项调优参数

会话变量说明默认值
auto_analyze_start_time自动统计信息收集开始时间00:00:00
auto_analyze_end_time自动统计信息收集结束时间23:59:59
enable_auto_analyze开启自动收集功能true
huge_table_default_sample_rows对大表的采样行数4194304
huge_table_lower_bound_size_in_bytes大小超过该值的的表,在自动收集时将会自动通过采样收集统计信息0
huge_table_auto_analyze_interval_in_millis控制对大表的自动ANALYZE的最小时间间隔,在该时间间隔内大小超过huge_table_lower_bound_size_in_bytes * 5的表仅ANALYZE一次0
table_stats_health_threshold取值在0-100之间,当自上次统计信息收集操作之后,数据更新量达到 (100 - table_stats_health_threshold)% ,认为该表的统计信息已过时60
analyze_timeout控制ANALYZE超时时间,单位为秒43200
auto_analyze_table_width_threshold控制自动统计信息收集处理的最大表宽度,列数大于该值的表不会参与自动统计信息收集100

FE配置项说明默认值
analyze_record_limit控制统计信息作业执行记录的持久化行数20000
stats_cache_sizeFE侧统计信息缓存条数500000
statistics_simultaneously_running_task_num可同时执行的异步作业数量3
statistics_sql_mem_limit_in_bytes控制每个统计信息SQL可占用的BE内存2L * 1024 * 1024 * 1024 (2GiB)

2 Join相关

2.1 支持的Join算子

Doris 支持两种物理算子,一类是 Hash Join,另一类是 Nest Loop Join

  • Hash Join:在右表上根据等值 Join 列建立哈希表,左表流式的利用哈希表进行 Join 计算,它的限制是只能适用于等值 Join。

  • Nest Loop Join:通过两个 for 循环,很直观。然后它适用的场景就是不等值的 Join,例如:大于小于或者是需要求笛卡尔积的场景。它是一个通用的 Join 算子,但是性能表现差。

作为分布式的 MPP 数据库,在 Join 的过程中是需要进行数据的 Shuffle。数据需要进行拆分调度,才能保证最终的 Join 结果是正确的。举个简单的例子,假设关系 S 和 R 进行 Join,N 表示参与 Join 计算的节点的数量;T 则表示关系的 Tuple 数目。

2.2 支持的shuffle方式

2.2.1 Broadcast Join

它要求把右表全量的数据都发送到左表上,即每一个参与 Join 的节点,它都拥有右表全量的数据,也就是 T(R)。

它适用的场景是比较通用的,同时能够支持 Hash Join 和 Nest loop Join,它的网络开销 N * T(R)。

左表数据不移动,右表数据发送到左表数据的扫描节点 。----默认 左表数据量>右边数据量   

2.2.2 Shuffle Join

当进行 Hash Join 时候,可以通过 Join 列计算对应的 Hash 值,并进行 Hash 分桶。

它的网络开销则是:T(S) + T(R),但它只能支持 Hash Join,因为它是根据 Join 的条件也去做计算分桶的。

左右表数据根据分区,计算的结果发送到不同的分区节点上。

2.2.3 Bucket Shuffle Join

Doris 的表数据本身是通过 Hash 计算分桶的,所以就可以利用表本身的分桶列的性质来进行 Join 数据的 Shuffle。

假如两张表需要做 Join,并且 Join 列是左表的分桶列,那么左表的数据其实可以不用去移动,右表通过左表的数据分桶发送数据就可以完成 Join 的计算。

它的网络开销则是:T(R)相当于只 Shuffle 右表的数据就可以了。

左表数据不移动,右表数据根据分区计算的结果发送到左表扫表的节点。

2.2.3.1 原理

Doris 支持的常规分布式 Join 方式包括了 shuffle join 和 broadcast join。这两种 join 都会导致不小的网络开销:

举个例子,当前存在 A 表与 B 表的 Join 查询,它的 Join 方式为 HashJoin,不同 Join 类型的开销如下:

  • Broadcast Join: 如果根据数据分布,查询规划出 A 表有 3 个执行的 HashJoinNode,那么需要将 B 表全量的发送到 3 个 HashJoinNode,那么它的网络开销是3B,它的内存开销也是3B

  • Shuffle Join: Shuffle Join 会将 A,B 两张表的数据根据哈希计算分散到集群的节点之中,所以它的网络开销为 A + B,内存开销为B

在 FE 之中保存了 Doris 每个表的数据分布信息,如果 join 语句命中了表的数据分布列,我们应该使用数据分布信息来减少 join 语句的网络与内存开销,这就是 Bucket Shuffle Join 的思路来源。

上面的图片展示了 Bucket Shuffle Join 的工作原理。SQL 语句为 A 表 join B 表,并且 join 的等值表达式命中了 A 的数据分布列。而 Bucket Shuffle Join 会根据 A 表的数据分布信息,将 B 表的数据发送到对应的 A 表的数据存储计算节点。Bucket Shuffle Join 开销如下:

  • 网络开销: B < min(3B, A + B)

  • 内存开销: B <= min(3B, B)

可见,相比于 Broadcast Join 与 Shuffle Join,Bucket Shuffle Join 有着较为明显的性能优势。减少数据在节点间的传输耗时和 Join 时的内存开销。相对于 Doris 原有的 Join 方式,它有着下面的优点

  • 首先,Bucket-Shuffle-Join 降低了网络与内存开销,使一些 Join 查询具有了更好的性能。尤其是当 FE 能够执行左表的分区裁剪与桶裁剪时。

  • 其次,同时与 Colocate Join 不同,它对于表的数据分布方式并没有侵入性,这对于用户来说是透明的。对于表的数据分布没有强制性的要求,不容易导致数据倾斜的问题。

  • 最后,它可以为 Join Reorder 提供更多可能的优化空间。

2.2.3.2 使用方式

设置 Session 变量:

将 session 变量enable_bucket_shuffle_join设置为true,则 FE 在进行查询规划时就会默认将能够转换为 Bucket Shuffle Join 的查询自动规划为 Bucket Shuffle Join。

set enable_bucket_shuffle_join = true;

在 FE 进行分布式查询规划时,优先选择的顺序为 Colocate Join -> Bucket Shuffle Join -> Broadcast Join -> Shuffle Join。但是如果用户显式 hint 了 Join 的类型,如:

select * from test join [shuffle] baseall on test.k1 = baseall.k1;

则上述的选择优先顺序则不生效。

查看 Join 的类型:

可以通过explain命令来查看 Join 是否为 Bucket Shuffle Join:

在 Join 类型之中会指明使用的 Join 方式为:BUCKET_SHUFFLE
2.2.3.3 Bucket Shuffle Join 的规划规则

在绝大多数场景之中,用户只需要默认打开 session 变量的开关就可以透明的使用这种 Join 方式带来的性能提升,但是如果了解 Bucket Shuffle Join 的规划规则,可以帮助我们利用它写出更加高效的 SQL。

  • Bucket Shuffle Join 只生效于 Join 条件为等值的场景,原因与 Colocate Join 类似,它们都依赖 hash 来计算确定的数据分布。

  • 在等值 Join 条件之中包含两张表的分桶列,当左表的分桶列为等值的 Join 条件时,它有很大概率会被规划为 Bucket Shuffle Join。

  • 由于不同的数据类型的 hash 值计算结果不同,所以 Bucket Shuffle Join 要求左表的分桶列的类型与右表等值 join 列的类型需要保持一致,否则无法进行对应的规划。

  • Bucket Shuffle Join 只作用于 Doris 原生的 OLAP 表,对于 ODBC,MySQL,ES 等外表,当其作为左表时是无法规划生效的。

  • 对于分区表,由于每一个分区的数据分布规则可能不同,所以 Bucket Shuffle Join 只能保证左表为单分区时生效。所以在 SQL 执行之中,需要尽量使用where条件使分区裁剪的策略能够生效。

  • 假如左表为 Colocate 的表,那么它每个分区的数据分布规则是确定的,Bucket Shuffle Join 能在 Colocate 表上表现更好。

2.2.4 Colocate Join

它与 Bucket Shuffle Join 相似,相当于在数据导入的时候,根据预设的 Join 列的场景已经做好了数据的 Shuffle。那么实际查询的时候就可以直接进行 Join 计算而不需要考虑数据的 Shuffle 问题了。

数据已经预先分区,直接在本地进行 Join 计算。

2.2.4.1 原理

Colocation Join 功能,是将一组拥有相同 CGS 的 Table 组成一个 CG。并保证这些 Table 对应的数据分片会落在同一个 BE 节点上。使得当 CG 内的表进行分桶列上的 Join 操作时,可以通过直接进行本地数据 Join,减少数据在节点间的传输耗时。

一个表的数据,最终会根据分桶列值 Hash、对桶数取模的后落在某一个分桶内。假设一个 Table 的分桶数为 8,则共有 [0, 1, 2, 3, 4, 5, 6, 7] 8 个分桶(Bucket),我们称这样一个序列为一个 BucketsSequence。每个 Bucket 内会有一个或多个数据分片(Tablet)。当表为单分区表时,一个 Bucket 内仅有一个 Tablet。如果是多分区表,则会有多个。

为了使得 Table 能够有相同的数据分布,同一 CG 内的 Table 必须保证以下属性相同:

  1. 分桶列和分桶数

    分桶列,即在建表语句中 DISTRIBUTED BY HASH(col1, col2, ...) 中指定的列。分桶列决定了一张表的数据通过哪些列的值进行 Hash 划分到不同的 Tablet 中。同一 CG 内的 Table 必须保证分桶列的类型和数量完全一致,并且桶数一致,才能保证多张表的数据分片能够一一对应的进行分布控制。

  2. 副本数

    同一个 CG 内所有表的所有分区(Partition)的副本数必须一致。如果不一致,可能出现某一个 Tablet 的某一个副本,在同一个 BE 上没有其他的表分片的副本对应。

同一个 CG 内的表,分区的个数、范围以及分区列的类型不要求一致。

在固定了分桶列和分桶数后,同一个 CG 内的表会拥有相同的 BucketsSequence。而副本数决定了每个分桶内的 Tablet 的多个副本,存放在哪些 BE 上。假设 BucketsSequence 为 [0, 1, 2, 3, 4, 5, 6, 7],BE 节点有 [A, B, C, D] 4 个。则一个可能的数据分布如下:

+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
| 0 | | 1 | | 2 | | 3 | | 4 | | 5 | | 6 | | 7 |
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
| A | | B | | C | | D | | A | | B | | C | | D |
| | | | | | | | | | | | | | | |
| B | | C | | D | | A | | B | | C | | D | | A |
| | | | | | | | | | | | | | | |
| C | | D | | A | | B | | C | | D | | A | | B |
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+

CG 内所有表的数据都会按照上面的规则进行统一分布,这样就保证了,分桶列值相同的数据都在同一个 BE 节点上,可以进行本地数据 Join。

2.2.4.2 使用方式

建表:

建表时,可以在 PROPERTIES 中指定属性 "colocate_with" = "group_name",表示这个表是一个 Colocation Join 表,并且归属于一个指定的 Colocation Group。

示例:

CREATE TABLE tbl (k1 int, v1 int sum)
DISTRIBUTED BY HASH(k1)
BUCKETS 8
PROPERTIES(
"colocate_with" = "group1"
);

如果指定的 Group 不存在,则 Doris 会自动创建一个只包含当前这张表的 Group。如果 Group 已存在,则 Doris 会检查当前表是否满足 Colocation Group Schema。如果满足,则会创建该表,并将该表加入 Group。同时,表会根据已存在的 Group 中的数据分布规则创建分片和副本。Group 归属于一个 Database,Group 的名字在一个 Database 内唯一。在内部存储是 Group 的全名为 dbId_groupName,但用户只感知 groupName。

提示:2.0 版本中,Doris 支持了跨 Database 的 Group。

在建表时,需使用关键词 __global__ 作为 Group 名称的前缀。如:

CREATE TABLE tbl (k1 int, v1 int sum)
DISTRIBUTED BY HASH(k1)
BUCKETS 8
PROPERTIES(
"colocate_with" = "__global__group1"
);

__global__ 前缀的 Group 不再归属于一个 Database,其名称也是全局唯一的。

通过创建 Global Group,可以实现跨 Database 的 Colocate Join。

删表:

当 Group 中最后一张表彻底删除后(彻底删除是指从回收站中删除。通常,一张表通过 DROP TABLE 命令删除后,会在回收站默认停留一天的时间后,再删除),该 Group 也会被自动删除。

查看 Group:

以下命令需要 ADMIN 权限。暂不支持普通用户查看。

查看集群内已存在的 Group 信息。

SHOW PROC '/colocation_group';

+-------------+--------------+--------------+------------+----------------+----------+----------+
| GroupId | GroupName | TableIds | BucketsNum | ReplicationNum | DistCols | IsStable |
+-------------+--------------+--------------+------------+----------------+----------+----------+
| 10005.10008 | 10005_group1 | 10007, 10040 | 10 | 3 | int(11) | true |
+-------------+--------------+--------------+------------+----------------+----------+----------+

  • GroupId:一个 Group 的全集群唯一标识,前半部分为 db id,后半部分为 group id。

  • GroupName:Group 的全名。

  • TabletIds:该 Group 包含的 Table 的 id 列表。

  • BucketsNum:分桶数。

  • ReplicationNum:副本数。

  • DistCols:Distribution columns,即分桶列类型。

  • IsStable:该 Group 是否稳定(稳定的定义,见 Colocation 副本均衡和修复 一节)。

通过以下命令可以进一步查看一个 Group 的数据分布情况:

SHOW PROC '/colocation_group/10005.10008';

+-------------+---------------------+
| BucketIndex | BackendIds |
+-------------+---------------------+
| 0 | 10004, 10002, 10001 |
| 1 | 10003, 10002, 10004 |
| 2 | 10002, 10004, 10001 |
| 3 | 10003, 10002, 10004 |
| 4 | 10002, 10004, 10003 |
| 5 | 10003, 10002, 10001 |
| 6 | 10003, 10004, 10001 |
| 7 | 10003, 10004, 10002 |
+-------------+---------------------+

  • BucketIndex:分桶序列的下标。

  • BackendIds:分桶中数据分片所在的 BE 节点 id 列表。

修改表 Colocate Group 属性:

可以对一个已经创建的表,修改其 Colocation Group 属性。示例:

ALTER TABLE tbl SET ("colocate_with" = "group2");

  • 如果该表之前没有指定过 Group,则该命令检查 Schema,并将该表加入到该 Group(Group 不存在则会创建)。

  • 如果该表之前有指定其他 Group,则该命令会先将该表从原有 Group 中移除,并加入新 Group(Group 不存在则会创建)。

也可以通过以下命令,删除一个表的 Colocation 属性:

ALTER TABLE tbl SET ("colocate_with" = "");

其他相关操作:

当对一个具有 Colocation 属性的表进行增加分区(ADD PARTITION)、修改副本数时,Doris 会检查修改是否会违反 Colocation Group Schema,如果违反则会拒绝。

2.2.4.3 高级操作

FE 配置项:

  • disable_colocate_relocate

    是否关闭 Doris 的自动 Colocation 副本修复。默认为 false,即不关闭。该参数只影响 Colocation 表的副本修复,不影响普通表。

  • disable_colocate_balance

    是否关闭 Doris 的自动 Colocation 副本均衡。默认为 false,即不关闭。该参数只影响 Colocation 表的副本均衡,不影响普通表。

以上参数可以动态修改,设置方式请参阅 HELP SHOW CONFIG; 和 HELP SET CONFIG;

HTTP Restful API:

Doris 提供了几个和 Colocation Join 有关的 HTTP Restful API,用于查看和修改 Colocation Group。

该 API 实现在 FE 端,使用 fe_host:fe_http_port 进行访问。需要 ADMIN 权限。

  1. 查看集群的全部 Colocation 信息

    GET /api/colocate

    返回以 Json 格式表示内部 Colocation 信息。

    {
    "msg": "success",
    "code": 0,
    "data": {
    "infos": [
    ["10003.12002", "10003_group1", "10037, 10043", "1", "1", "int(11)", "true"]
    ],
    "unstableGroupIds": [],
    "allGroupIds": [{
    "dbId": 10003,
    "grpId": 12002
    }]
    },
    "count": 0
    }

  2. 将 Group 标记为 Stable 或 Unstable

    • 标记为 Stable

      DELETE /api/colocate/group_stable?db_id=10005&group_id=10008

      返回:200

    • 标记为 Unstable

      POST /api/colocate/group_stable?db_id=10005&group_id=10008

      返回:200

  3. 设置 Group 的数据分布

    该接口可以强制设置某一 Group 的数分布。

    POST /api/colocate/bucketseq?db_id=10005&group_id=10008

    Body:
    [[10004,10002],[10003,10002],[10002,10004],[10003,10002],[10002,10004],[10003,10002],[10003,10004],[10003,10004],[10003,10004],[10002,10004]]

    返回 200

    其中 Body 是以嵌套数组表示的 BucketsSequence 以及每个 Bucket 中分片分布所在 BE 的 id。

    注意,使用该命令,可能需要将 FE 的配置 disable_colocate_relocate 和 disable_colocate_balance 设为 true。即关闭系统自动的 Colocation 副本修复和均衡。否则可能在修改后,会被系统自动重置。

2.3 四种shuffle方式对比

Shuffle 方式网络开销物理算子适用场景
BroadCastN * T(R)Hash Join / Nest Loop Join通用
ShuffleT(S) + T(R)Hash Join通用
Bucket ShuffleT(R)Hash JoinJoin 条件中存在左表的分布式列,且左表执行时为单分区
Colocate0Hash JoinJoin 条件中存在左表的分布式列,且左右表同属于一个 Colocate Group
N:参与 Join 计算的 Instance 个数
T(关系) : 关系的 Tuple 数目
上面这 4 种方式灵活度是从高到低的,它对这个数据分布的要求是越来越严格,但 Join 计算的性能也是越来越好的。

2.4 join优化建议

下面的 4 步基本上完成了一个标准的 Join 调优流程,接着就是实际去查询验证它,看看效果到底怎么样。

如果 4 种方式串联起来之后,还是不奏效。这时候可能就需要去做 Join 语句的改写,或者是数据分布的调整、需要重新去 Recheck 整个数据分布是否合理,包括查询 Join 语句,可能需要做一些手动的调整。

当然这种方式是心智成本是比较高的,也就是说要在尝试前面方式不奏效的情况下,才需要去做进一步的分析。

1. 利用 Doris 本身提供的 Profile,去定位查询的瓶颈。Profile 会记录 Doris 整个查询当中各种信息,这是进行性能调优的一手资料。  
mysql> set enable_profile=true;  //开启profile
mysql> show variables like '%profile%';

2. 了解 Doris 的 Join 机制,这也是第二部分跟大家分享的内容。知其然知其所以然、了解它的机制,才能分析它为什么比较慢。
3. 利用 Session 变量去改变 Join 的一些行为,从而实现 Join 的调优。
4. 查看 Query Plan 去分析这个调优是否生效。

# 调优Runtime Filter

  • 第一个查询选项是调整使用的 Runtime Filter 类型,大多数情况下,您只需要调整这一个选项,其他选项保持默认即可。

    • runtime_filter_type: 包括 Bloom Filter、MinMax Filter、IN predicate、IN Or Bloom Filter、Bitmap Filter,默认会使用 IN Or Bloom Filter,部分情况下同时使用 Bloom Filter、MinMax Filter、IN predicate 时性能更高。
  • 其他查询选项通常仅在某些特定场景下,才需进一步调整以达到最优效果。通常只在性能测试后,针对资源密集型、运行耗时足够长且频率足够高的查询进行优化。

    • runtime_filter_mode: 用于调整 Runtime Filter 的下推策略,包括 OFF、LOCAL、GLOBAL 三种策略,默认设置为 GLOBAL 策略

    • runtime_filter_wait_time_ms: 左表的 ScanNode 等待每个 Runtime Filter 的时间,默认 1000ms

    • runtime_filters_max_num: 每个查询可应用的 Runtime Filter 中 Bloom Filter 的最大数量,默认 10

    • runtime_bloom_filter_min_size: Runtime Filter 中 Bloom Filter 的最小长度,默认 1048576(1M)

    • runtime_bloom_filter_max_size: Runtime Filter 中 Bloom Filter 的最大长度,默认 16777216(16M)

    • runtime_bloom_filter_size: Runtime Filter 中 Bloom Filter 的默认长度,默认 2097152(2M)

    • runtime_filter_max_in_num: 如果 join 右表数据行数大于这个值,我们将不生成 IN predicate,默认 1024

    • runtime_filter_wait_infinitely: 如果参数为 true,那么左表的 scan 节点将会一直等待直到接收到 runtime filer 或者查询超超时,默认为 false

下面对查询选项做进一步说明。

1. runtime_filter_type

使用的 Runtime Filter 类型。

类型: 数字 (1, 2, 4, 8, 16) 或者相对应的助记符字符串 (IN, BLOOM_FILTER, MIN_MAX, IN_OR_BLOOM_FILTER, BITMAP_FILTER),默认 12(MIN_MAX,IN_OR_BLOOM_FILTER),使用多个时用逗号分隔,注意需要加引号,或者将任意多个类型的数字相加,例如:

set runtime_filter_type="BLOOM_FILTER,IN,MIN_MAX";

等价于:

set runtime_filter_type=7;

使用注意事项

  • IN or Bloom Filter: 根据右表在执行过程中的真实行数,由系统自动判断使用 IN predicate 还是 Bloom Filter

    • 默认在右表数据行数少于 102400 时会使用 IN predicate(可通过 session 变量中的runtime_filter_max_in_num调整),否则使用 Bloom filter。
  • Bloom Filter: 有一定的误判率,导致过滤的数据比预期少一点,但不会导致最终结果不准确,在大部分情况下 Bloom Filter 都可以提升性能或对性能没有显著影响,但在部分情况下会导致性能降低。

    • Bloom Filter 构建和应用的开销较高,所以当过滤率较低时,或者左表数据量较少时,Bloom Filter 可能会导致性能降低。

    • 目前只有左表的 Key 列应用 Bloom Filter 才能下推到存储引擎,而测试结果显示 Bloom Filter 不下推到存储引擎时往往会导致性能降低。

    • 目前 Bloom Filter 仅在 ScanNode 上使用表达式过滤时有短路 (short-circuit) 逻辑,即当假阳性率过高时,不继续使用 Bloom Filter,但当 Bloom Filter 下推到存储引擎后没有短路逻辑,所以当过滤率较低时可能导致性能降低。

  • MinMax Filter: 包含最大值和最小值,从而过滤小于最小值和大于最大值的数据,MinMax Filter 的过滤效果与 join on clause 中 Key 列的类型和左右表数据分布有关。

    • 当 join on clause 中 Key 列的类型为 int/bigint/double 等时,极端情况下,如果左右表的最大最小值相同则没有效果,反之右表最大值小于左表最小值,或右表最小值大于左表最大值,则效果最好。

    • 当 join on clause 中 Key 列的类型为 varchar 等时,应用 MinMax Filter 往往会导致性能降低。

  • IN predicate: 根据 join on clause 中 Key 列在右表上的所有值构建 IN predicate,使用构建的 IN predicate 在左表上过滤,相比 Bloom Filter 构建和应用的开销更低,在右表数据量较少时往往性能更高。

    • 目前 IN predicate 已实现合并方法。

    • 当同时指定 In predicate 和其他 filter,并且 in 的过滤数值没达到 runtime_filter_max_in_num 时,会尝试把其他 filter 去除掉。原因是 In predicate 是精确的过滤条件,即使没有其他 filter 也可以高效过滤,如果同时使用则其他 filter 会做无用功。目前仅在 Runtime filter 的生产者和消费者处于同一个 fragment 时才会有去除非 in filter 的逻辑。

  • Bitmap Filter:

    • 当前仅当in subquery操作中的子查询返回 bitmap 列时会使用 bitmap filter.

    • 当前仅在向量化引擎中支持 bitmap filter.

2. runtime_filter_mode

用于控制 Runtime Filter 在 instance 之间传输的范围。

类型: 数字 (0, 1, 2) 或者相对应的助记符字符串 (OFF, LOCAL, GLOBAL),默认 2(GLOBAL)。

使用注意事项

LOCAL:相对保守,构建的 Runtime Filter 只能在同一个 instance(查询执行的最小单元)上同一个 Fragment 中使用,即 Runtime Filter 生产者(构建 Filter 的 HashJoinNode)和消费者(使用 RuntimeFilter 的 ScanNode)在同一个 Fragment,比如 broadcast join 的一般场景;

GLOBAL:相对激进,除满足 LOCAL 策略的场景外,还可以将 Runtime Filter 合并后通过网络传输到不同 instance 上的不同 Fragment 中使用,比如 Runtime Filter 生产者和消费者在不同 Fragment,比如 shuffle join。

大多数情况下 GLOBAL 策略可以在更广泛的场景对查询进行优化,但在有些 shuffle join 中生成和合并 Runtime Filter 的开销超过给查询带来的性能优势,可以考虑更改为 LOCAL 策略。

如果集群中涉及的 join 查询不会因为 Runtime Filter 而提高性能,您可以将设置更改为 OFF,从而完全关闭该功能。

在不同 Fragment 上构建和应用 Runtime Filter 时,需要合并 Runtime Filter 的原因和策略可参阅 ISSUE 6116(opens new window)

3. runtime_filter_wait_time_ms

Runtime Filter 的等待耗时。

类型: 整数,默认 1000,单位 ms

使用注意事项

在开启 Runtime Filter 后,左表的 ScanNode 会为每一个分配给自己的 Runtime Filter 等待一段时间再扫描数据,即如果 ScanNode 被分配了 3 个 Runtime Filter,那么它最多会等待 3000ms。

因为 Runtime Filter 的构建和合并均需要时间,ScanNode 会尝试将等待时间内到达的 Runtime Filter 下推到存储引擎,如果超过等待时间后,ScanNode 会使用已经到达的 Runtime Filter 直接开始扫描数据。

如果 Runtime Filter 在 ScanNode 开始扫描之后到达,则 ScanNode 不会将该 Runtime Filter 下推到存储引擎,而是对已经从存储引擎扫描上来的数据,在 ScanNode 上基于该 Runtime Filter 使用表达式过滤,之前已经扫描的数据则不会应用该 Runtime Filter,这样得到的中间数据规模会大于最优解,但可以避免严重的裂化。

如果集群比较繁忙,并且集群上有许多资源密集型或长耗时的查询,可以考虑增加等待时间,以避免复杂查询错过优化机会。如果集群负载较轻,并且集群上有许多只需要几秒的小查询,可以考虑减少等待时间,以避免每个查询增加 1s 的延迟。

4. runtime_filters_max_num

每个查询生成的 Runtime Filter 中 Bloom Filter 数量的上限。

类型: 整数,默认 10

使用注意事项 目前仅对 Bloom Filter 的数量进行限制,因为相比 MinMax Filter 和 IN predicate,Bloom Filter 构建和应用的代价更高。

如果生成的 Bloom Filter 超过允许的最大数量,则保留选择性大的 Bloom Filter,选择性大意味着预期可以过滤更多的行。这个设置可以防止 Bloom Filter 耗费过多的内存开销而导致潜在的问题。

选择性=(HashJoinNode Cardinality / HashJoinNode left child Cardinality)
-- 因为目前 FE 拿到 Cardinality 不准,所以这里 Bloom Filter 计算的选择性与实际不准,因此最终可能只是随机保留了部分 Bloom Filter。

仅在对涉及大表间 join 的某些长耗时查询进行调优时,才需要调整此查询选项。

5. Bloom Filter 长度相关参数

包括runtime_bloom_filter_min_sizeruntime_bloom_filter_max_sizeruntime_bloom_filter_size,用于确定 Runtime Filter 使用的 Bloom Filter 数据结构的大小(以字节为单位)。

类型: 整数

使用注意事项 因为需要保证每个 HashJoinNode 构建的 Bloom Filter 长度相同才能合并,所以目前在 FE 查询规划时计算 Bloom Filter 的长度。

如果能拿到 join 右表统计信息中的数据行数 (Cardinality),会尝试根据 Cardinality 估计 Bloom Filter 的最佳大小,并四舍五入到最接近的 2 的幂 (以 2 为底的 log 值)。如果无法拿到右表的 Cardinality,则会使用默认的 Bloom Filter 长度runtime_bloom_filter_sizeruntime_bloom_filter_min_sizeruntime_bloom_filter_max_size用于限制最终使用的 Bloom Filter 长度最小和最大值。

更大的 Bloom Filter 在处理高基数的输入集时更有效,但需要消耗更多的内存。假如查询中需要过滤高基数列(比如含有数百万个不同的取值),可以考虑增加runtime_bloom_filter_size的值进行一些基准测试,这有助于使 Bloom Filter 过滤的更加精准,从而获得预期的性能提升。

Bloom Filter 的有效性取决于查询的数据分布,因此通常仅对一些特定查询额外调整其 Bloom Filter 长度,而不是全局修改,一般仅在对涉及大表间 join 的某些长耗时查询进行调优时,才需要调整此查询选项。

2.5 调优案例实战

案例一

一个四张表 Join 的查询,通过 Profile 的时候发现第二个 Join 耗时很高,耗时 14 秒。

进一步分析 Profile 之后,发现 BuildRows,就是右表的数据量是大概 2500 万。而 ProbeRows(ProbeRows 是左表的数据量)只有 1 万多。这种场景下右表是远远大于左表,这显然是个不合理的情况。这显然说明 Join 的顺序出现了一些问题。这时候尝试改变 Session 变量,开启 Join Reorder。

set enable_cost_based_join_reorder = true

这次耗时从 14 秒降到了 4 秒,性能提升了 3 倍多。

此时再 Check Profile 的时候,左右表的顺序已经调整正确,即右表是小表,左表是大表。基于小表去构建哈希表,开销是很小的,这就是典型的一个利用 Join Reorder 去提升 Join 性能的一个场景

案例二

存在一个慢查询,查看 Profile 之后,整个 Join 节点耗时大概 44 秒。它的右表有 1000 万,左表有 6000 万,最终返回的结果也只有 6000 万。

这里可以大致的估算出过滤率是很高的,那为什么 Runtime Filter 没有生效呢?通过 Query Plan 去查看它,发现它只开启了 IN 的 Runtime Filter。

当右表超过 1024 行的话,IN 是不生效的,所以根本起不到什么过滤的效果,所以尝试调整 RuntimeFilter 的类型。

这里改为了 BloomFilter,左表的 6000 万条数据过滤了 5900 万条。基本上 99% 的数据都被过滤掉了,这个效果是很显著的。查询也从原来的 44 秒降到了 13 秒,性能提升了大概也是三倍多。

案例三

下面是一个比较极端的 Case,通过一些环境变量调优也没有办法解决,因为它涉及到 SQL Rewrite,所以这里列出来了原始的 SQL。

select 100.00 * sum (case
               when P_type like 'PROMOS'
               then 1 extendedprice * (1 - 1 discount)
               else 0
               end ) / sum(1 extendedprice * (1 - 1 discount)) as promo revenue
from lineitem, part
where
    1_partkey = p_partkey
    and 1_shipdate >= date '1997-06-01'
    and 1 shipdate < date '1997-06-01' + interval '1' month

这个 Join 查询是很简单的,单纯的一个左右表的 Join。当然它上面有一些过滤条件,打开 Profile 的时候,发现整个查询 Hash Join 执行了三分多钟,它是一个 BroadCast 的 Join,它的右表有 2 亿条,左表只有 70 万。在这种情况下选择了 Broadcast Join 是不合理的,这相当于要把 2 亿条做一个 Hash Table,然后用 70 万条遍历两亿条的 Hash Table,这显然是不合理的。

为什么会产生不合理的 Join 顺序呢?其实这个左表是一个 10 亿条级别的大表,它上面加了两个过滤条件,加完这两个过滤条件之后,10 亿条的数据就剩 70 万条了。但 Doris 目前没有一个好的统计信息收集的框架,所以它不知道这个过滤条件的过滤率到底怎么样。所以这个 Join 顺序安排的时候,就选择了错误的 Join 的左右表顺序,导致它的性能是极其低下的。

下图是改写完成之后的一个 SQL 语句,在 Join 后面添加了一个 Join Hint,在 Join 后面加一个方括号,然后把需要的 Join 方式写入。这里选择了 Shuffle Join,可以看到右边它实际查询计划里面看到这个数据确实是做了 Partition,原先 3 分钟的耗时通过这样的改写完之后只剩下 7 秒,性能提升明显

3 去重相关

3.1 bitmap精准去重

背景

Doris 原有的 Bitmap 聚合函数设计比较通用,但对亿级别以上 bitmap 大基数的交并集计算性能较差。排查后端 be 的 bitmap 聚合函数逻辑,发现主要有两个原因。一是当 bitmap 基数较大时,如 bitmap 大小超过 1g,网络/磁盘 IO 处理时间比较长;二是后端 be 实例在 scan 数据后全部传输到顶层节点进行求交和并运算,给顶层单节点带来压力,成为处理瓶颈。

解决思路是将 bitmap 列的值按照 range 划分,不同 range 的值存储在不同的分桶中,保证了不同分桶的 bitmap 值是正交的。当查询时,先分别对不同分桶中的正交 bitmap 进行聚合计算,然后顶层节点直接将聚合计算后的值合并汇总,并输出。如此会大大提高计算效率,解决了顶层单节点计算瓶颈问题。

使用指南

建表,增加 hid 列,表示 bitmap 列值 id 范围,作为 hash 分桶列

使用场景:

1.Create table

建表时需要使用聚合模型,数据类型是 bitmap , 聚合函数是 bitmap_union

CREATE TABLE `user_tag_bitmap` (
`tag` bigint(20) NULL COMMENT "用户标签",
`hid` smallint(6) NULL COMMENT "分桶id",
`user_id` bitmap BITMAP_UNION NULL COMMENT ""
) ENGINE=OLAP
AGGREGATE KEY(`tag`, `hid`)
COMMENT "OLAP"
DISTRIBUTED BY HASH(`hid`) BUCKETS 3

表 schema 增加 hid 列,表示 id 范围,作为 hash 分桶列。

注:hid 数和 BUCKETS 要设置合理,hid 数设置至少是 BUCKETS 的 5 倍以上,以使数据 hash 分桶尽量均衡

2. Data Load

LOAD LABEL user_tag_bitmap_test
(
DATA INFILE('hdfs://abc')
INTO TABLE user_tag_bitmap
COLUMNS TERMINATED BY ','
(tmp_tag, tmp_user_id)
SET (
tag = tmp_tag,
hid = ceil(tmp_user_id/5000000),
user_id = to_bitmap(tmp_user_id)
)
)
注意:5000000这个数不固定,可按需调整
...

数据格式:

11111111,1
11111112,2
11111113,3
11111114,4
...

注:第一列代表用户标签,由中文转换成数字

load 数据时,对用户 bitmap 值 range 范围纵向切割,例如,用户 id 在 1-5000000 范围内的 hid 值相同,hid 值相同的行会分配到一个分桶内,如此每个分桶内到的 bitmap 都是正交的。可以利用桶内 bitmap 值正交特性,进行交并集计算,计算结果会被 shuffle 至 top 节点聚合。
备注

注:正交 bitmap 函数不能用在分区表,因为分区表分区内正交,分区之间的数据是无法保证正交的,则计算结果也是无法预估的。

orthogonal_bitmap_intersect

求 bitmap 交集函数

  • 语法:orthogonal_bitmap_intersect(bitmap_column, column_to_filter, filter_values)
  • 参数:第一个参数是 Bitmap 列,第二个参数是用来过滤的维度列,第三个参数是变长参数,含义是过滤维度列的不同取值
  • 说明:查询规划上聚合分 2 层,在第一层 be 节点(update、serialize)先按 filter_values 为 key 进行 hash 聚合,然后对所有 key 的 bitmap 求交集,结果序列化后发送至第二层 be 节点 (merge、finalize),在第二层 be 节点对所有来源于第一层节点的 bitmap 值循环求并集

样例:

select BITMAP_COUNT(orthogonal_bitmap_intersect(user_id, tag, 13080800, 11110200)) from user_tag_bitmap where tag in (13080800, 11110200);

orthogonal_bitmap_intersect_count

求 bitmap 交集 count 函数,语法同原版 intersect_count,但实现不同

  • 语法:orthogonal_bitmap_intersect_count(bitmap_column, column_to_filter, filter_values)
  • 参数:第一个参数是 Bitmap 列,第二个参数是用来过滤的维度列,第三个参数开始是变长参数,含义是过滤维度列的不同取值
  • 说明:查询规划聚合上分 2 层,在第一层 be 节点(update、serialize)先按 filter_values 为 key 进行 hash 聚合,然后对所有 key 的 bitmap 求交集,再对交集结果求 count,count 值序列化后发送至第二层 be 节点(merge、finalize),在第二层 be 节点对所有来源于第一层节点的 count 值循环求 sum

orthogonal_bitmap_union_count

求 bitmap 并集 count 函数,语法同原版 bitmap_union_count,但实现不同。

  • 语法:orthogonal_bitmap_union_count(bitmap_column)
  • 参数:参数类型是 bitmap,是待求并集 count 的列
  • 说明:查询规划上分 2 层,在第一层 be 节点(update、serialize)对所有 bitmap 求并集,再对并集的结果 bitmap 求 count,count 值序列化后发送至第二层 be 节点(merge、finalize),在第二层 be 节点对所有来源于第一层节点的 count 值循环求 sum

orthogonal_bitmap_expr_calculate

求表达式 bitmap 交并差集合计算函数。

  • 语法:orthogonal_bitmap_expr_calculate(bitmap_column, filter_column, input_string)
  • 参数:第一个参数是 Bitmap 列,第二个参数是用来过滤的维度列,即计算的 key 列,第三个参数是计算表达式字符串,含义是依据 key 列进行 bitmap 交并差集表达式计算

                     表达式支持的计算符:& 代表交集计算,| 代表并集计算,- 代表差集计算,^ 代表异或计算,\ 代表转义字符

  • 说明:查询规划上聚合分 2 层,第一层 be 聚合节点计算包括 init、update、serialize 步骤,第二层 be 聚合节点计算包括 merge、finalize 步骤。在第一层 be 节点,init 阶段解析 input_string 字符串,转换为后缀表达式(逆波兰式),解析出计算 key 值,并在 map<key, bitmap>结构中初始化;update 阶段,底层内核 scan 维度列(filter_column)数据后回调 update 函数,然后以计算 key 为单位对上一步的 map 结构中的 bitmap 进行聚合;serialize 阶段,根据后缀表达式,解析出计算 key 列的 bitmap,利用栈结构先进后出原则,进行 bitmap 交并差集合计算,然后对最终的结果 bitmap 序列化后发送至第二层聚合 be 节点。在第二层聚合 be 节点,对所有来源于第一层节点的 bitmap 值求并集,并返回最终 bitmap 结果

orthogonal_bitmap_expr_calculate_count

求表达式 bitmap 交并差集合计算 count 函数,语法和参数同 orthogonal_bitmap_expr_calculate。

  • 语法:orthogonal_bitmap_expr_calculate_count(bitmap_column, filter_column, input_string)
  • 说明:查询规划上聚合分 2 层,第一层 be 聚合节点计算包括 init、update、serialize 步骤,第二层 be 聚合节点计算包括 merge、finalize 步骤。在第一层 be 节点,init 阶段解析 input_string 字符串,转换为后缀表达式(逆波兰式),解析出计算 key 值,并在 map<key, bitmap>结构中初始化;update 阶段,底层内核 scan 维度列(filter_column)数据后回调 update 函数,然后以计算 key 为单位对上一步的 map 结构中的 bitmap 进行聚合;serialize 阶段,根据后缀表达式,解析出计算 key 列的 bitmap,利用栈结构先进后出原则,进行 bitmap 交并差集合计算,然后对最终的结果 bitmap 的 count 值序列化后发送至第二层聚合 be 节点。在第二层聚合 be 节点,对所有来源于第一层节点的 count 值求加和,并返回最终 count 结果。

使用场景实例

符合对 bitmap 进行正交计算的场景,如在用户行为分析中,计算留存,漏斗,用户画像等。

人群圈选:

select orthogonal_bitmap_intersect_count(user_id, tag, 13080800, 11110200) from user_tag_bitmap where tag in (13080800, 11110200);
注:13080800、11110200代表用户标签

计算 user_id 的去重值:

select orthogonal_bitmap_union_count(user_id) from user_tag_bitmap where tag in (13080800, 11110200);

bitmap 交并差集合混合计算:

select orthogonal_bitmap_expr_calculate_count(user_id, tag, '(833736|999777)&(1308083|231207)&(1000|20000-30000)') from user_tag_bitmap where tag in (833736,999777,130808,231207,1000,20000,30000);
注:1000、20000、30000等整形tag,代表用户不同标签

select orthogonal_bitmap_expr_calculate_count(user_id, tag, '(A:a/b|B:2\\-4)&(C:1-D:12)&E:23') from user_str_tag_bitmap where tag in ('A:a/b', 'B:2-4', 'C:1', 'D:12', 'E:23');
注:'A:a/b', 'B:2-4'等是字符串类型tag,代表用户不同标签, 其中'B:2-4'需要转义成'B:2\\-4'

3.2 hll近似去重

背景

在实际的业务场景中,随着业务数据量越来越大,对数据去重的压力也越来越大,当数据达到一定规模之后,使用精准去重的成本也越来越高,在业务可以接受的情况下,通过近似算法来实现快速去重降低计算压力是一个非常好的方式,本文主要介绍 Doris 提供的 HyperLogLog(简称 HLL)是一种近似去重算法。

HLL 的特点是具有非常优异的空间复杂度 O(mloglogn) , 时间复杂度为 O(n), 并且计算结果的误差可控制在 1%—2% 左右,误差与数据集大小以及所采用的哈希函数有关。

HLL 是基于 HyperLogLog 算法的工程实现,用于保存 HyperLogLog 计算过程的中间结果,它只能作为表的 value 列类型、通过聚合来不断的减少数据量,以此

来实现加快查询的目的,基于它得到的是一个估算结果,误差大概在 1% 左右,hll 列是通过其它列或者导入数据里面的数据生成的,导入的时候通过 hll_hash 函数

来指定数据中哪一列用于生成 hll 列,它常用于替代 count distinct,通过结合 rollup 在业务上用于快速计算 uv 等

HLL_UNION_AGG(hll)

此函数为聚合函数,用于计算满足条件的所有数据的基数估算。

HLL_CARDINALITY(hll)

此函数用于计算单条 hll 列的基数估算

HLL_HASH(column_name)

生成 HLL 列类型,用于 insert 或导入的时候,导入的使用见相关说明

1.如何使用 Doris HLL

  1. 使用 HLL 去重的时候,需要在建表语句中将目标列类型设置成 HLL,聚合函数设置成 HLL_UNION

  2. HLL 类型的列不能作为 Key 列使用

  3. 用户不需要指定长度及默认值,长度根据数据聚合程度系统内控制

1.1 创建一张含有 hll 列的表

create table test_hll(
dt date,
id int,
name char(10),
province char(10),
os char(10),
pv hll hll_union
)
Aggregate KEY (dt,id,name,province,os)
distributed by hash(id) buckets 10
PROPERTIES(
"replication_num" = "1",
"in_memory"="false"
);

1.2 导入数据
  1. Stream load 导入

    curl --location-trusted -u root: -H "label:label_test_hll_load" \
    -H "column_separator:," \
    -H "columns:dt,id,name,province,os, pv=hll_hash(id)" -T test_hll.csv http://fe_IP:8030/api/demo/test_hll/_stream_load

    示例数据如下(test_hll.csv):

    2022-05-05,10001,测试 01,北京,windows
    2022-05-05,10002,测试 01,北京,linux
    2022-05-05,10003,测试 01,北京,macos
    2022-05-05,10004,测试 01,河北,windows
    2022-05-06,10001,测试 01,上海,windows
    2022-05-06,10002,测试 01,上海,linux
    2022-05-06,10003,测试 01,江苏,macos
    2022-05-06,10004,测试 01,陕西,windows

    导入结果如下

    # curl --location-trusted -u root: -H "label:label_test_hll_load" -H "column_separator:," -H "columns:dt,id,name,province,os, pv=hll_hash(id)" -T test_hll.csv http://127.0.0.1:8030/api/demo/test_hll/_stream_load

    {
    "TxnId": 693,
    "Label": "label_test_hll_load",
    "TwoPhaseCommit": "false",
    "Status": "Success",
    "Message": "OK",
    "NumberTotalRows": 8,
    "NumberLoadedRows": 8,
    "NumberFilteredRows": 0,
    "NumberUnselectedRows": 0,
    "LoadBytes": 320,
    "LoadTimeMs": 23,
    "BeginTxnTimeMs": 0,
    "StreamLoadPutTimeMs": 1,
    "ReadDataTimeMs": 0,
    "WriteDataTimeMs": 9,
    "CommitAndPublishTimeMs": 11
    }

  2. Broker Load

LOAD LABEL demo.test_hlllabel
(
DATA INFILE("hdfs://hdfs_host:hdfs_port/user/doris_test_hll/data/input/file")
INTO TABLE `test_hll`
COLUMNS TERMINATED BY ","
(dt,id,name,province,os)
SET (
pv = HLL_HASH(id)
)
);

1.3 查询数据

HLL 列不允许直接查询原始值,只能通过 HLL 的聚合函数进行查询。

  1. 求总的 PV

    mysql> select HLL_UNION_AGG(pv) from test_hll;
    +---------------------+
    | hll_union_agg(`pv`) |
    +---------------------+
    | 4 |
    +---------------------+
    1 row in set (0.00 sec)

    等价于:

    mysql> SELECT COUNT(DISTINCT pv) FROM test_hll;
    +----------------------+
    | count(DISTINCT `pv`) |
    +----------------------+
    | 4 |
    +----------------------+
    1 row in set (0.01 sec)

  2. 求每一天的 PV

    mysql> select HLL_UNION_AGG(pv) from test_hll group by dt;
    +---------------------+
    | hll_union_agg(`pv`) |
    +---------------------+
    | 4 |
    | 4 |
    +---------------------+
    2 rows in set (0.01 sec)

3.3 去重优化?配置?

4 视图与物化视图

4.1 逻辑视图

说明

视图(逻辑视图)是封装一个或多个 SELECT 语句的存储查询。视图在执行时动态访问并计算数据库数据。视图是只读的,可以引用表和其他视图的任意组合。

可以使用视图实现以下用途:

  • 出于简化访问或安全访问的目的,让用户看不到复杂的 SELECT 语句。例如,可以创建仅显示用户所需的各表中数据的视图,同时隐藏这些表中的敏感数据。

  • 将可能随时间而改变的表结构的详细信息封装在一致的用户界面后。

与物化视图不同,视图不实体化,也就是说,它们不在磁盘上存储数据。因此,存在以下限制:

  • 当底层表数据发生变更时,Doris 不需要刷新视图数据。但是,访问和计算数据时,视图也会产生一些开销。

  • 视图不支持插入、删除或更新操作。

4.1.1 创建视图

用于创建一个逻辑视图的语法如下:

CREATE VIEW [IF NOT EXISTS]
[db_name.]view_name
(column1[ COMMENT "col comment"][, column2, ...])
AS query_stmt

说明:

  • 视图为逻辑视图,没有物理存储。所有在视图上的查询相当于在视图对应的子查询上进行。

  • query_stmt 为任意支持的 SQL

4.1.2 举例 

  • 在 example_db 上创建视图 example_view

    CREATE VIEW example_db.example_view (k1, k2, k3, v1)
    AS
    SELECT c1 as k1, k2, k3, SUM(v1) FROM example_table
    WHERE k1 = 20160112 GROUP BY k1,k2,k3;

  • 创建一个包含 comment 的 view

    CREATE VIEW example_db.example_view
    (
    k1 COMMENT "first key",
    k2 COMMENT "second key",
    k3 COMMENT "third key",
    v1 COMMENT "first value"
    )
    COMMENT "my first view"
    AS
    SELECT c1 as k1, k2, k3, SUM(v1) FROM example_table
    WHERE k1 = 20160112 GROUP BY k1,k2,k3;

4.2 同步物化视图

说明

物化视图是将预先计算(根据定义好的 SELECT 语句)好的数据集,存储在 Doris 中的一个特殊的表。

物化视图的出现主要是为了满足用户,既能对原始明细数据的任意维度分析,也能快速的对固定维度进行分析查询。

4.2.1 适用场景

  • 分析需求覆盖明细数据查询以及固定维度查询两方面。

  • 查询仅涉及表中的很小一部分列或行。

  • 查询包含一些耗时处理操作,比如:时间很久的聚合操作等。

  • 查询需要匹配不同前缀索引。

4.2.2 优势

  • 对于那些经常重复的使用相同的子查询结果的查询性能大幅提升。

  • Doris 自动维护物化视图的数据,无论是新的导入,还是删除操作都能保证 Base 表和物化视图表的数据一致性,无需任何额外的人工维护成本。

  • 查询时,会自动匹配到最优物化视图,并直接从物化视图中读取数据。

4.2.3 物化视图 VS Rollup

在没有物化视图功能之前,用户一般都是使用 Rollup 功能通过预聚合方式提升查询效率的。但是 Rollup 具有一定的局限性,他不能基于明细模型做预聚合。

物化视图则在覆盖了 Rollup 的功能的同时,还能支持更丰富的聚合函数。所以物化视图其实是 Rollup 的一个超集。

也就是说,之前 ALTER TABLE ADD ROLLUP 语法支持的功能现在均可以通过 CREATE MATERIALIZED VIEW 实现。

4.2.4 使用物化视图

Doris 系统提供了一整套对物化视图的 DDL 语法,包括创建,查看,删除。DDL 的语法和 PostgreSQL, Oracle 都是一致的。

4.2.4.1 创建物化视图

这里首先你要根据你的查询语句的特点来决定创建一个什么样的物化视图。这里并不是说你的物化视图定义和你的某个查询语句一模一样就最好。这里有两个原则:

  1. 从查询语句中抽象出,多个查询共有的分组和聚合方式作为物化视图的定义。

  2. 不需要给所有维度组合都创建物化视图。

  • 首先第一个点,一个物化视图如果抽象出来,并且多个查询都可以匹配到这张物化视图。这种物化视图效果最好。因为物化视图的维护本身也需要消耗资源。如果物化视图只和某个特殊的查询很贴合,而其他查询均用不到这个物化视图。则会导致这张物化视图的性价比不高,既占用了集群的存储资源,还不能为更多的查询服务。所以用户需要结合自己的查询语句,以及数据维度信息去抽象出一些物化视图的定义。
  • 第二点就是,在实际的分析查询中,并不会覆盖到所有的维度分析。所以给常用的维度组合创建物化视图即可,从而到达一个空间和时间上的平衡。创建物化视图是一个异步的操作,也就是说用户成功提交创建任务后,Doris 会在后台对存量的数据进行计算,直到创建成功。具体的语法可查看CREATE MATERIALIZED VIEW 。

提示:

  • 建议用户在正式的生产环境中使用物化视图前,先在测试环境中确认是预期中的查询能否命中想要创建的物化视图。如果不清楚如何验证一个查询是否命中物化视图,可以阅读下面的最佳实践1
  • 不建议用户在同一张表上建多个形态类似的物化视图,这可能会导致多个物化视图之间的冲突使得查询命中失败 (在新优化器中这个问题会有所改善)。
  • 建议用户先在测试环境中验证物化视图和查询是否满足需求并能正常使用。
4.2.4.2 支持聚合函数

目前物化视图创建语句支持的聚合函数有:

  • SUM, MIN, MAX

  • COUNT, BITMAP_UNION, HLL_UNION

  • AGG_STATE :一些不在原有的支持范围内的聚合函数,会被转化为 agg_state 类型来实现预聚合。

4.2.4.3 更新策略

为保证物化视图表和 Base 表的数据一致性,Doris 会将导入,删除等对 Base 表的操作都同步到物化视图表中。并且通过增量更新的方式来提升更新效率。通过事务方式来保证原子性。

比如如果用户通过 INSERT 命令插入数据到 Base 表中,则这条数据会同步插入到物化视图中。当 Base 表和物化视图表均写入成功后,INSERT 命令才会成功返回。

4.2.4.4 查询自动匹配

物化视图创建成功后,用户的查询不需要发生任何改变,也就是还是查询的 Base 表。Doris 会根据当前查询的语句去自动选择一个最优的物化视图,从物化视图中读取数据并计算。

用户可以通过 EXPLAIN 命令来检查当前查询是否使用了物化视图。

物化视图中的聚合和查询中聚合的匹配关系:

物化视图聚合查询中聚合
sumsum
minmin
maxmax
countcount
bitmap_unionbitmap_union, bitmap_union_count, count(distinct)
hll_unionhll_raw_agg, hll_union_agg, ndv, approx_count_distinct

其中 bitmap 和 hll 的聚合函数在查询匹配到物化视图后,查询的聚合算子会根据物化视图的表结构进行改写。详细见实例 2。

4.2.5 查询物化视图

查看当前表都有哪些物化视图,以及他们的表结构都是什么样的。通过下面命令:

MySQL [test]> desc mv_test all;
+-----------+---------------+-----------------+----------+------+-------+---------+--------------+
| IndexName | IndexKeysType | Field | Type | Null | Key | Default | Extra |
+-----------+---------------+-----------------+----------+------+-------+---------+--------------+
| mv_test | DUP_KEYS | k1 | INT | Yes | true | NULL | |
| | | k2 | BIGINT | Yes | true | NULL | |
| | | k3 | LARGEINT | Yes | true | NULL | |
| | | k4 | SMALLINT | Yes | false | NULL | NONE |
| | | | | | | | |
| mv_2 | AGG_KEYS | k2 | BIGINT | Yes | true | NULL | |
| | | k4 | SMALLINT | Yes | false | NULL | MIN |
| | | k1 | INT | Yes | false | NULL | MAX |
| | | | | | | | |
| mv_3 | AGG_KEYS | k1 | INT | Yes | true | NULL | |
| | | to_bitmap(`k2`) | BITMAP | No | false | | BITMAP_UNION |
| | | | | | | | |
| mv_1 | AGG_KEYS | k4 | SMALLINT | Yes | true | NULL | |
| | | k1 | BIGINT | Yes | false | NULL | SUM |
| | | k3 | LARGEINT | Yes | false | NULL | SUM |
| | | k2 | BIGINT | Yes | false | NULL | MIN |
+-----------+---------------+-----------------+----------+------+-------+---------+--------------+

可以看到当前 mv_test 表一共有三张物化视图:mv_1, mv_2 和 mv_3,以及他们的表结构。

4.2.6 删除物化视图

如果用户不再需要物化视图,则可以通过命令删除物化视图。

DROP MATERIALIZED VIEW [IF EXISTS] mv_name ON table_name

4.2.7 查看已创建的物化视图

用户可以通过命令查看已创建的物化视图的

SHOW CREATE MATERIALIZED VIEW mv_name ON table_name

4.2.8 取消创建物化视图

CANCEL ALTER TABLE MATERIALIZED VIEW FROM db_name.table_name

4.2.9 实践

最佳实践 1

使用物化视图一般分为以下几个步骤:

  1. 创建物化视图

  2. 异步检查物化视图是否构建完成

  3. 查询并自动匹配物化视图

首先是第一步:创建物化视图

假设用户有一张销售记录明细表,存储了每个交易的交易 id,销售员,售卖门店,销售时间,以及金额。建表语句和插入数据语句为:

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,1,1,"2020-02-02",1);

这张 sales_records 的表结构如下:

MySQL [test]> desc sales_records;
+-----------+--------+------+-------+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------+------+-------+---------+-------+
| record_id | INT | Yes | true | NULL | |
| seller_id | INT | Yes | true | NULL | |
| store_id | INT | Yes | true | NULL | |
| sale_date | DATE | Yes | false | NULL | NONE |
| sale_amt | BIGINT | Yes | false | NULL | NONE |
+-----------+--------+------+-------+---------+-------+

这时候如果用户经常对不同门店的销售量进行一个分析查询,则可以给这个 sales_records 表创建一张以售卖门店分组,对相同售卖门店的销售额求和的一个物化视图。创建语句如下:

MySQL [test]> create materialized view store_amt as select store_id, sum(sale_amt) from sales_records group by store_id;

后端返回下图,则说明创建物化视图任务提交成功。

Query OK, 0 rows affected (0.012 sec)

第二步:检查物化视图是否构建完成

由于创建物化视图是一个异步的操作,用户在提交完创建物化视图任务后,需要异步的通过命令检查物化视图是否构建完成。命令如下:

SHOW ALTER TABLE ROLLUP FROM db_name; (Version 0.12)
SHOW ALTER TABLE MATERIALIZED VIEW FROM db_name; (Version 0.13)

这个命令中 db_name 是一个参数,你需要替换成自己真实的 db 名称。命令的结果是显示这个 db 的所有创建物化视图的任务。结果如下:

+-------+---------------+---------------------+---------------------+---------------+-----------------+----------+---------------+-----------+
| JobId | TableName | CreateTime | FinishedTime | BaseIndexName | RollupIndexName | RollupId | TransactionId | State | Msg | Progress | Timeout |
+-------+---------------+---------------------+---------------------+---------------+-----------------+----------+---------------+-----------+--
| 22036 | sales_records | 2020-07-30 20:04:28 | 2020-07-30 20:04:57 | sales_records | store_amt | 22037 | 5008 | FINISHED | | NULL | 86400 |
+-------+---------------+---------------------+---------------------+---------------+-----------------+----------+---------------+-----------+

其中 TableName 指的是物化视图的数据来自于哪个表,RollupIndexName 指的是物化视图的名称叫什么。其中比较重要的指标是 State。

当创建物化视图任务的 State 已经变成 FINISHED 后,就说明这个物化视图已经创建成功了。这就意味着,查询的时候有可能自动匹配到这张物化视图了。

第三步:查询

当创建完成物化视图后,用户再查询不同门店的销售量时,就会直接从刚才创建的物化视图 store_amt 中读取聚合好的数据。达到提升查询效率的效果。

用户的查询依旧指定查询 sales_records 表,比如:

SELECT store_id, sum(sale_amt) FROM sales_records GROUP BY store_id;

上面查询就能自动匹配到 store_amt。用户可以通过下面命令,检验当前查询是否匹配到了合适的物化视图。

EXPLAIN SELECT store_id, sum(sale_amt) FROM sales_records GROUP BY store_id;
+----------------------------------------------------------------------------------------------+
| Explain String |
+----------------------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS: |
| <slot 4> `default_cluster:test`.`sales_records`.`mv_store_id` |
| <slot 5> sum(`default_cluster:test`.`sales_records`.`mva_SUM__`sale_amt``) |
| PARTITION: UNPARTITIONED |
| |
| VRESULT SINK |
| |
| 4:VEXCHANGE |
| offset: 0 |
| |
| PLAN FRAGMENT 1 |
| |
| PARTITION: HASH_PARTITIONED: <slot 4> `default_cluster:test`.`sales_records`.`mv_store_id` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 04 |
| UNPARTITIONED |
| |
| 3:VAGGREGATE (merge finalize) |
| | output: sum(<slot 5> sum(`default_cluster:test`.`sales_records`.`mva_SUM__`sale_amt``)) |
| | group by: <slot 4> `default_cluster:test`.`sales_records`.`mv_store_id` |
| | cardinality=-1 |
| | |
| 2:VEXCHANGE |
| offset: 0 |
| |
| PLAN FRAGMENT 2 |
| |
| PARTITION: HASH_PARTITIONED: `default_cluster:test`.`sales_records`.`record_id` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 02 |
| HASH_PARTITIONED: <slot 4> `default_cluster:test`.`sales_records`.`mv_store_id` |
| |
| 1:VAGGREGATE (update serialize) |
| | STREAMING |
| | output: sum(`default_cluster:test`.`sales_records`.`mva_SUM__`sale_amt``) |
| | group by: `default_cluster:test`.`sales_records`.`mv_store_id` |
| | cardinality=-1 |
| | |
| 0:VOlapScanNode |
| TABLE: default_cluster:test.sales_records(store_amt), PREAGGREGATION: ON |
| partitions=1/1, tablets=10/10, tabletList=50028,50030,50032 ... |
| cardinality=1, avgRowSize=1520.0, numNodes=1 |
+----------------------------------------------------------------------------------------------+

从最底部的test.sales_records(store_amt)可以表明这个查询命中了store_amt这个物化视图。值得注意的是,如果表中没有数据,那么可能不会命中物化视图。

最佳实践 2(UV,PV)

业务场景:计算广告的 UV,PV。

假设用户的原始广告点击数据存储在 Doris,那么针对广告 PV, UV 查询就可以通过创建 bitmap_union 的物化视图来提升查询速度。

通过下面语句首先创建一个存储广告点击数据明细的表,包含每条点击的点击时间,点击的是什么广告,通过什么渠道点击,以及点击的用户是谁。

create table advertiser_view_record(time date, advertiser varchar(10), channel varchar(10), user_id int) distributed by hash(time) properties("replication_num" = "1");
insert into advertiser_view_record values("2020-02-02",'a','a',1);

原始的广告点击数据表结构为:

MySQL [test]> desc advertiser_view_record;
+------------+-------------+------+-------+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-------------+------+-------+---------+-------+
| time | DATE | Yes | true | NULL | |
| advertiser | VARCHAR(10) | Yes | true | NULL | |
| channel | VARCHAR(10) | Yes | false | NULL | NONE |
| user_id | INT | Yes | false | NULL | NONE |
+------------+-------------+------+-------+---------+-------+
4 rows in set (0.001 sec)

  1. 创建物化视图

    由于用户想要查询的是广告的 UV 值,也就是需要对相同广告的用户进行一个精确去重,则查询一般为:

    SELECT advertiser, channel, count(distinct user_id) FROM advertiser_view_record GROUP BY advertiser, channel;

    针对这种求 UV 的场景,我们就可以创建一个带 bitmap_union 的物化视图从而达到一个预先精确去重的效果。

    在 Doris 中,count(distinct) 聚合的结果和 bitmap_union_count聚合的结果是完全一致的。而bitmap_union_count 等于 bitmap_union 的结果求 count,所以如果查询中涉及到 count(distinct) 则通过创建带 bitmap_union 聚合的物化视图方可加快查询

    针对这个 Case,则可以创建一个根据广告和渠道分组,对 user_id 进行精确去重的物化视图。

    MySQL [test]> create materialized view advertiser_uv as select advertiser, channel, bitmap_union(to_bitmap(user_id)) from advertiser_view_record group by advertiser, channel;
    Query OK, 0 rows affected (0.012 sec)

    警告注意:因为本身 user_id 是一个 INT 类型,所以在 Doris 中需要先将字段通过函数 to_bitmap 转换为 bitmap 类型然后才可以进行 bitmap_union 聚合。

    创建完成后,广告点击明细表和物化视图表的表结构如下:

    MySQL [test]> desc advertiser_view_record all;
    +------------------------+---------------+----------------------+-------------+------+-------+---------+--------------+
    | IndexName | IndexKeysType | Field | Type | Null | Key | Default | Extra |
    +------------------------+---------------+----------------------+-------------+------+-------+---------+--------------+
    | advertiser_view_record | DUP_KEYS | time | DATE | Yes | true | NULL | |
    | | | advertiser | VARCHAR(10) | Yes | true | NULL | |
    | | | channel | VARCHAR(10) | Yes | false | NULL | NONE |
    | | | user_id | INT | Yes | false | NULL | NONE |
    | | | | | | | | |
    | advertiser_uv | AGG_KEYS | advertiser | VARCHAR(10) | Yes | true | NULL | |
    | | | channel | VARCHAR(10) | Yes | true | NULL | |
    | | | to_bitmap(`user_id`) | BITMAP | No | false | | BITMAP_UNION |
    +------------------------+---------------+----------------------+-------------+------+-------+---------+--------------+

  2. 查询自动匹配

    当物化视图表创建完成后,查询广告 UV 时,Doris 就会自动从刚才创建好的物化视图 advertiser_uv 中查询数据。比如原始的查询语句如下:

    SELECT advertiser, channel, count(distinct user_id) FROM advertiser_view_record GROUP BY advertiser, channel;

    在选中物化视图后,实际的查询会转化为:

    SELECT advertiser, channel, bitmap_union_count(to_bitmap(user_id)) FROM advertiser_uv GROUP BY advertiser, channel;

    通过 EXPLAIN 命令可以检验到 Doris 是否匹配到了物化视图:

    mysql [test]>explain SELECT advertiser, channel, count(distinct user_id) FROM advertiser_view_record GROUP BY advertiser, channel;
    +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | Explain String |
    +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
    | PLAN FRAGMENT 0 |
    | OUTPUT EXPRS: |
    | <slot 9> `default_cluster:test`.`advertiser_view_record`.`mv_advertiser` |
    | <slot 10> `default_cluster:test`.`advertiser_view_record`.`mv_channel` |
    | <slot 11> bitmap_union_count(`default_cluster:test`.`advertiser_view_record`.`mva_BITMAP_UNION__to_bitmap_with_check(`user_id`)`) |
    | PARTITION: UNPARTITIONED |
    | |
    | VRESULT SINK |
    | |
    | 4:VEXCHANGE |
    | offset: 0 |
    | |
    | PLAN FRAGMENT 1 |
    | |
    | PARTITION: HASH_PARTITIONED: <slot 6> `default_cluster:test`.`advertiser_view_record`.`mv_advertiser`, <slot 7> `default_cluster:test`.`advertiser_view_record`.`mv_channel` |
    | |
    | STREAM DATA SINK |
    | EXCHANGE ID: 04 |
    | UNPARTITIONED |
    | |
    | 3:VAGGREGATE (merge finalize) |
    | | output: bitmap_union_count(<slot 8> bitmap_union_count(`default_cluster:test`.`advertiser_view_record`.`mva_BITMAP_UNION__to_bitmap_with_check(`user_id`)`)) |
    | | group by: <slot 6> `default_cluster:test`.`advertiser_view_record`.`mv_advertiser`, <slot 7> `default_cluster:test`.`advertiser_view_record`.`mv_channel` |
    | | cardinality=-1 |
    | | |
    | 2:VEXCHANGE |
    | offset: 0 |
    | |
    | PLAN FRAGMENT 2 |
    | |
    | PARTITION: HASH_PARTITIONED: `default_cluster:test`.`advertiser_view_record`.`time` |
    | |
    | STREAM DATA SINK |
    | EXCHANGE ID: 02 |
    | HASH_PARTITIONED: <slot 6> `default_cluster:test`.`advertiser_view_record`.`mv_advertiser`, <slot 7> `default_cluster:test`.`advertiser_view_record`.`mv_channel` |
    | |
    | 1:VAGGREGATE (update serialize) |
    | | STREAMING |
    | | output: bitmap_union_count(`default_cluster:test`.`advertiser_view_record`.`mva_BITMAP_UNION__to_bitmap_with_check(`user_id`)`) |
    | | group by: `default_cluster:test`.`advertiser_view_record`.`mv_advertiser`, `default_cluster:test`.`advertiser_view_record`.`mv_channel` |
    | | cardinality=-1 |
    | | |
    | 0:VOlapScanNode |
    | TABLE: default_cluster:test.advertiser_view_record(advertiser_uv), PREAGGREGATION: ON |
    | partitions=1/1, tablets=10/10, tabletList=50075,50077,50079 ... |
    | cardinality=0, avgRowSize=48.0, numNodes=1 |
    +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

    在 EXPLAIN 的结果中,首先可以看到 VOlapScanNode 命中了 advertiser_uv。也就是说,查询会直接扫描物化视图的数据。说明匹配成功。

    其次对于 user_id 字段求 count(distinct) 被改写为求 bitmap_union_count(to_bitmap)。也就是通过 Bitmap 的方式来达到精确去重的效果。

最佳实践 3

业务场景:匹配更丰富的前缀索引

用户的原始表有(k1, k2, k3)三列。其中 k1, k2 为前缀索引列。这时候如果用户查询条件中包含 where k1=1 and k2=2 就能通过索引加速查询。

但是有些情况下,用户的过滤条件无法匹配到前缀索引,比如 where k3=3。则无法通过索引提升查询速度。

创建以 k3 作为第一列的物化视图就可以解决这个问题。

  1. 创建物化视图

    CREATE MATERIALIZED VIEW mv_1 as SELECT k3, k2, k1 FROM tableA ORDER BY k3;

    通过上面语法创建完成后,物化视图中既保留了完整的明细数据,且物化视图的前缀索引为 k3 列。表结构如下:

    MySQL [test]> desc tableA all;
    +-----------+---------------+-------+------+------+-------+---------+-------+
    | IndexName | IndexKeysType | Field | Type | Null | Key | Default | Extra |
    +-----------+---------------+-------+------+------+-------+---------+-------+
    | tableA | DUP_KEYS | k1 | INT | Yes | true | NULL | |
    | | | k2 | INT | Yes | true | NULL | |
    | | | k3 | INT | Yes | true | NULL | |
    | | | | | | | | |
    | mv_1 | DUP_KEYS | k3 | INT | Yes | true | NULL | |
    | | | k2 | INT | Yes | false | NULL | NONE |
    | | | k1 | INT | Yes | false | NULL | NONE |
    +-----------+---------------+-------+------+------+-------+---------+-------+

  2. 查询匹配

    这时候如果用户的查询存在 k3 列的过滤条件是,比如:

    select k1, k2, k3 from table A where k3=3;

    这时候查询就会直接从刚才创建的 mv_1 物化视图中读取数据。物化视图对 k3 是存在前缀索引的,查询效率也会提升。

最佳实践 4

本示例将主要体现新版本物化视图对各种表达式的支持和提前过滤。

  1. 创建一个 Base 表并插入一些数据。

create table d_table (
k1 int null,
k2 int not null,
k3 bigint null,
k4 date null
)
duplicate key (k1,k2,k3)
distributed BY hash(k1) buckets 3
properties("replication_num" = "1");

insert into d_table select 1,1,1,'2020-02-20';
insert into d_table select 2,2,2,'2021-02-20';
insert into d_table select 3,-3,null,'2022-02-20';

  1. 创建一些物化视图。

create materialized view k1a2p2ap3ps as select abs(k1)+k2+1,sum(abs(k2+2)+k3+3) from d_table group by abs(k1)+k2+1;
create materialized view kymd as select year(k4),month(k4) from d_table where year(k4) = 2020; // 提前用where表达式过滤以减少物化视图中的数据量。

  1. 用一些查询测试是否成功命中物化视图。

select abs(k1)+k2+1,sum(abs(k2+2)+k3+3) from d_table group by abs(k1)+k2+1; // 命中k1a2p2ap3ps
select bin(abs(k1)+k2+1),sum(abs(k2+2)+k3+3) from d_table group by bin(abs(k1)+k2+1); // 命中k1a2p2ap3ps
select year(k4),month(k4) from d_table; // 无法命中物化视图,因为where条件不匹配
select year(k4)+month(k4) from d_table where year(k4) = 2020; // 命中kymd

4.2.10 局限性

  1. 如果删除语句的条件列,在物化视图中不存在,则不能进行删除操作。如果一定要删除数据,则需要先将物化视图删除,然后方可删除数据。

  2. 单表上过多的物化视图会影响导入的效率:导入数据时,物化视图和 Base 表数据是同步更新的,如果一张表的物化视图表超过 10 张,则有可能导致导入速度很慢。这就像单次导入需要同时导入 10 张表数据是一样的。

  3. 物化视图针对 Unique Key 数据模型,只能改变列顺序,不能起到聚合的作用,所以在 Unique Key 模型上不能通过创建物化视图的方式对数据进行粗粒度聚合操作

  4. 目前一些优化器对 sql 的改写行为可能会导致物化视图无法被命中,例如 k1+1-1 被改写成 k1,between 被改写成<=和>=,day 被改写成 dayofmonth,遇到这种情况需要手动调整下查询和物化视图的语句。

4.3 异步物化视图

4.3.1 物化视图的构建和维护 

4.3.1.1 创建物化视图

准备两张表和数据

use tpch;

# 创建orders
CREATE TABLE IF NOT EXISTS orders (
o_orderkey integer not null,
o_custkey integer not null,
o_orderstatus char(1) not null,
o_totalprice decimalv3(15,2) not null,
o_orderdate date not null,
o_orderpriority char(15) not null,
o_clerk char(15) not null,
o_shippriority integer not null,
o_comment varchar(79) not null
)
DUPLICATE KEY(o_orderkey, o_custkey)
PARTITION BY RANGE(o_orderdate)(
FROM ('2023-10-17') TO ('2023-10-20') INTERVAL 1 DAY)
DISTRIBUTED BY HASH(o_orderkey) BUCKETS 3
PROPERTIES ("replication_num" = "1");

insert into orders values
(1, 1, 'o', 99.5, '2023-10-17', 'a', 'b', 1, 'yy'),
(2, 2, 'o', 109.2, '2023-10-18', 'c','d',2, 'mm'),
(3, 3, 'o', 99.5, '2023-10-19', 'a', 'b', 1, 'yy');

# 创建lineitem
CREATE TABLE IF NOT EXISTS lineitem (
l_orderkey integer not null,
l_partkey integer not null,
l_suppkey integer not null,
l_linenumber integer not null,
l_quantity decimalv3(15,2) not null,
l_extendedprice decimalv3(15,2) not null,
l_discount decimalv3(15,2) not null,
l_tax decimalv3(15,2) not null,
l_returnflag char(1) not null,
l_linestatus char(1) not null,
l_shipdate date not null,
l_commitdate date not null,
l_receiptdate date not null,
l_shipinstruct char(25) not null,
l_shipmode char(10) not null,
l_comment varchar(44) not null
)
DUPLICATE KEY(l_orderkey, l_partkey, l_suppkey, l_linenumber)
PARTITION BY RANGE(l_shipdate)
(FROM ('2023-10-17') TO ('2023-10-20') INTERVAL 1 DAY)
DISTRIBUTED BY HASH(l_orderkey) BUCKETS 3
PROPERTIES ("replication_num" = "1");

# 插入数据
insert into lineitem values
(1, 2, 3, 4, 5.5, 6.5, 7.5, 8.5, 'o', 'k', '2023-10-17', '2023-10-17', '2023-10-17', 'a', 'b', 'yyyyyyyyy'),
(2, 2, 3, 4, 5.5, 6.5, 7.5, 8.5, 'o', 'k', '2023-10-18', '2023-10-18', '2023-10-18', 'a', 'b', 'yyyyyyyyy'),
(3, 2, 3, 6, 7.5, 8.5, 9.5, 10.5, 'k', 'o', '2023-10-19', '2023-10-19', '2023-10-19', 'c', 'd', 'xxxxxxxxx');

创建物化视图mv1

CREATE MATERIALIZED VIEW mv1
BUILD DEFERRED REFRESH AUTO ON MANUAL
partition by(l_shipdate)
DISTRIBUTED BY RANDOM BUCKETS 2
PROPERTIES ('replication_num' = '1')
AS
select l_shipdate, o_orderdate, l_partkey, l_suppkey, sum(o_totalprice) as sum_total
from lineitem
left join orders on lineitem.l_orderkey = orders.o_orderkey and l_shipdate = o_orderdate
group by
l_shipdate,
o_orderdate,
l_partkey,
l_suppkey;

4.3.1.2 查看物化视图元信息

select * from mv_infos("database"="tpch") where Name="mv1";

物化视图独有的特性可以通过mv_infos()查看  //使用 desc function mv_infos("database"="tpch"); 可以查看表结构  和 table 相关的属性,仍通过 show tables;  show tables like '%cm%';来查看

4.3.1.3 刷新物化视图

物化视图支持不同刷新策略,如定时刷新和手动刷新。也支持不同的刷新粒度,如全量刷新,分区粒度的增量刷新等。这里我们以手动刷新物化视图的部分分区为例。

首先查看物化视图分区列表

SHOW PARTITIONS FROM mv1;

刷新名字为p_20231017_20231018的分区

REFRESH MATERIALIZED VIEW mv1 partitions(p_20231017_20231018);

4.3.1.4 任务管理

每个物化视图都会默认有一个 job 负责刷新数据,job 用来描述物化视图的刷新策略等信息,每次触发刷新,都会产生一个 task, task 用来描述具体的一次刷新信息,例如刷新用的时间,刷新了哪些分区等

4.3.1.4.1 查看物化视图的 job

select * from jobs("type"="mv") order by CreateTime;

具体的语法可查看jobs("type"="mv")

4.3.1.4.2 暂停物化视图 job 定时调度

PAUSE MATERIALIZED VIEW JOB ON mv1;

可以暂停物化视图的定时调度,具体的语法可查看PAUSE MATERIALIZED VIEW JOB

4.3.1.4.3 恢复物化视图 job 定时调度

RESUME MATERIALIZED VIEW JOB ON mv1;

可以恢复物化视图的定时调度,具体的语法可查看RESUME MATERIALIZED VIEW JOB

4.3.1.4.4 查看物化视图的 task

select * from tasks("type"="mv");

具体的语法可查看tasks("type"="mv")

4.3.1.4.5 取消物化视图的 task

CANCEL MATERIALIZED VIEW TASK realTaskId on mv1;

可以取消本次 task 的运行,具体的语法可查看CANCEL MATERIALIZED VIEW TASK

4.3.1.5 修改物化视图

修改物化视图的属性

ALTER MATERIALIZED VIEW mv1 set("grace_period"="3333");

修改物化视图的名字,物化视图的刷新方式及物化视图特有的 property 可通过ALTER ASYNC MATERIALIZED VIEW来修改

物化视图本身也是一个 Table,所以 Table 相关的属性,例如副本数,仍通过ALTER TABLE相关的语法来修改。

4.3.1.6 删除物化视图

DROP MATERIALIZED VIEW mv1;

物化视图有专门的删除语法,不能通过 drop table 来删除,具体的语法可查看DROP ASYNC MATERIALIZED VIEW

4.3.2 最佳实践​

# 基表分区过多,物化视图只关注最近一段时间的数据​

# 创建基表,有三个分区

CREATE TABLE t1 (
    `k1` INT,
    `k2` DATE NOT NULL
) ENGINE=OLAP
DUPLICATE KEY(`k1`)
COMMENT 'OLAP'
PARTITION BY range(`k2`)
(
PARTITION p26 VALUES [("2024-03-26"),("2024-03-27")),
PARTITION p27 VALUES [("2024-03-27"),("2024-03-28")),
PARTITION p28 VALUES [("2024-03-28"),("2024-03-29"))
)
DISTRIBUTED BY HASH(`k1`) BUCKETS 2
PROPERTIES (
'replication_num' = '1'
);


# 创建物化视图,只关注最近一天的数据,如果当前时间为 2024-03-28 xx:xx:xx,这样物化视图会仅有一个分区[("2024-03-28"),("2024-03-29"))

CREATE MATERIALIZED VIEW mv1
BUILD DEFERRED REFRESH AUTO ON MANUAL
partition by(`k2`)
DISTRIBUTED BY RANDOM BUCKETS 2
PROPERTIES (
'replication_num' = '1',
'partition_sync_limit'='1',
'partition_sync_time_unit'='DAY'
)
AS
SELECT * FROM t1;


# 时间又过了一天,当前时间为 2024-03-29 xx:xx:xx,t1新增一个分区 [("2024-03-29"),("2024-03-30")),如果此时刷新物化视图,刷新完成后,物化视图会仅有一个分区 [("2024-03-29"),("2024-03-30"))

4.4 查询异步物化视图

4.4.1 直查物化视图

# 物化视图可以看作是表,可以像正常的表一样直接查询。
# 可以对物化视图添加过滤条件和聚合等,进行直接查询。
# 例: 视图名称:mv1
SELECT l_linenumber,
       o_custkey
FROM mv1 
WHERE l_linenumber > 1 and o_orderdate = '2023-10-18';

4.4.2 透明改写能力 

4.4.2.1 Join改写

Join 改写指的是查询和物化使用的表相同,可以在物化视图和查询 Join 的输入或者 Join 的外层写 where,优化器对此 pattern 的查询会尝试进行透明改写。

支持多表 Join,支持 Join 的类型为:

  • INNER JOIN
  • LEFT OUTER JOIN
  • RIGHT OUTER JOIN
  • FULL OUTER JOIN
  • LEFT SEMI JOIN
  • RIGHT SEMI JOIN
  • LEFT ANTI JOIN
  • RIGHT ANTI JOIN

例: 

mv 定义:

CREATE MATERIALIZED VIEW mv3
BUILD IMMEDIATE REFRESH AUTO ON SCHEDULE EVERY 1 hour
DISTRIBUTED BY RANDOM BUCKETS 3
PROPERTIES ('replication_num' = '1')
AS
SELECT
    l_shipdate, l_suppkey, o_orderdate,
    sum(o_totalprice) AS sum_total,
    max(o_totalprice) AS max_total,
    min(o_totalprice) AS min_total,
    count(*) AS count_all,
    count(distinct CASE WHEN o_shippriority > 1 AND o_orderkey IN (1, 3) THEN o_custkey ELSE null END) AS bitmap_union_basic
FROM lineitem
LEFT OUTER JOIN orders ON lineitem.l_orderkey = orders.o_orderkey AND l_shipdate = o_orderdate
GROUP BY
l_shipdate,
l_suppkey,
o_orderdate;

查询语句:

SELECT
    l_shipdate, l_suppkey, o_orderdate,
    sum(o_totalprice) AS sum_total,
    max(o_totalprice) AS max_total,
    min(o_totalprice) AS min_total,
    count(*) AS count_all,
    count(distinct CASE WHEN o_shippriority > 1 AND o_orderkey IN (1, 3) THEN o_custkey ELSE null END) AS bitmap_union_basic
FROM lineitem
INNER JOIN orders ON lineitem.l_orderkey = orders.o_orderkey AND l_shipdate = o_orderdate
WHERE o_orderdate = '2023-10-18' AND l_suppkey = 3
GROUP BY
l_shipdate,
l_suppkey,
o_orderdate;

4.4.2.2 聚合改写

查询和物化视图定义中,聚合的维度可以一致或者不一致,可以使用维度中的字段写 WHERE 对结果进行过滤。

物化视图使用的维度需要包含查询的维度,并且查询使用的指标可以使用物化视图的指标来表示。

例:如下查询可以进行透明改写,查询和物化使用聚合的维度不一致,物化视图使用的维度包含查询的维度。查询可以使用维度中的字段对结果进行过滤,

查询会尝试使用物化视图 SELECT 后的函数进行上卷,如物化视图的 bitmap_union 最后会上卷成 bitmap_union_count,和查询中 count(distinct) 的语义保持一致。

mv 定义:

CREATE MATERIALIZED VIEW mv5
BUILD IMMEDIATE REFRESH AUTO ON SCHEDULE EVERY 1 hour
DISTRIBUTED BY RANDOM BUCKETS 3
PROPERTIES ('replication_num' = '1')
AS
SELECT
l_shipdate, o_orderdate, l_partkey, l_suppkey,
sum(o_totalprice) AS sum_total,
max(o_totalprice) AS max_total,
min(o_totalprice) AS min_total,
count(*) AS count_all,
bitmap_union(to_bitmap(CASE WHEN o_shippriority > 1 AND o_orderkey IN (1, 3) THEN o_custkey ELSE null END)) AS bitmap_union_basic
FROM lineitem
LEFT OUTER JOIN orders ON lineitem.l_orderkey = orders.o_orderkey AND l_shipdate = o_orderdate
GROUP BY
l_shipdate,
o_orderdate,
l_partkey,
l_suppkey;

查询语句:

SELECT
l_shipdate, l_suppkey,
sum(o_totalprice) AS sum_total,
max(o_totalprice) AS max_total,
min(o_totalprice) AS min_total,
count(*) AS count_all,
count(distinct CASE WHEN o_shippriority > 1 AND o_orderkey IN (1, 3) THEN o_custkey ELSE null END) AS bitmap_union_basic
FROM lineitem
LEFT OUTER JOIN orders ON lineitem.l_orderkey = orders.o_orderkey AND l_shipdate = o_orderdate
WHERE o_orderdate = '2023-10-18' AND l_partkey = 3
GROUP BY
l_shipdate,
l_suppkey;

4.4.3 视图与源表数据同步设置

 grace_period 的单位是秒,指的是容许物化视图和所用基表数据不一致的时间。 比如 grace_period 设置成 0,意味要求物化视图和基表数据保持一致,此物化视图才可用于透明改写;

对于外表,因为无法感知数据变更,所以物化视图使用了外表,

无论外表的数据是不是最新的,都可以使用此物化视图用于透明改写,如果外表配置了 HMS 元数据源,是可以感知数据变更的,配置数据源和感知数据变更的功能会在后面迭代支持。

如果设置成 10,意味物化视图和基表数据允许 10s 的延迟,如果物化视图的数据和基表的数据有延迟,如果在 10s 内,此物化视图都可以用于透明改写。

对于物化视图中的内表,可以通过设定 grace_period 属性来控制透明改写使用的物化视图所允许数据最大的延迟时间。

ALTER MATERIALIZED VIEW mv1 set("grace_period"="0")

如果想知道物化视图候选,改写和最终选择情况的过程详细信息,可以执行如下语句,会展示透明改写过程详细的信息。

explain memo plan <query_sql>

4.4.4 部分调参

开关说明
SET enable_nereids_planner = true;异步物化视图只有在新优化器下才支持,所以需要开启新优化器
SET enable_materialized_view_rewrite = true;开启或者关闭查询透明改写,默认关闭
SET materialized_view_rewrite_enable_contain_external_table = true;参与透明改写的物化视图是否允许包含外表,默认不允许
SET materialized_view_rewrite_success_candidate_num = 3;透明改写成功的结果集合,允许参与到 CBO 候选的最大数量,默认是 3 个

4.4.5 限制

  • 物化视图定义语句中只允许包含 SELECT、FROM、WHERE、JOIN、GROUP BY 语句,JOIN 的输入可以包含简单的 GROUP BY(单表聚合),其中 JOIN 的支持的类型为 INNER 和 LEFT OUTER JOIN 其他类型的 JOIN 操作逐步支持。
  • 基于 External Table 的物化视图不保证查询结果强一致。
  • 不支持使用非确定性函数来构建物化视图,包括 rand、now、current_time、current_date、random、uuid 等。
  • 不支持窗口函数的透明改写。
  • 查询和物化视图中有 LIMIT,暂时不支持透明改写。
  • 物化视图的定义暂时不能使用视图和物化视图。
  • 当查询或者物化视图没有数据时,不支持透明改写。
  • 目前 WHERE 条件补偿,支持物化视图没有 WHERE,查询有 WHERE 情况的条件补偿;或者物化视图有 WHERE 且查询的 WHERE 条件是物化视图的超集。 目前暂时还不支持范围的条件补偿,比如物化视图定义是 a > 5,查询是 a > 10

5 资源隔离机制

5.1 队列设置

在 Doris 中设置队列通常是与资源管理相关的,Doris 允许用户通过 BE(Backend)的配置文件来设置队列,以此来控制不同用户的查询请求的优先级和资源分配。以下是一些基本的步骤来设置 Doris 的队列:

1. 编辑 BE 配置文件:Doris 的 BE(Backend)服务有一个配置文件,通常是 `be.conf`。需要编辑这个文件来设置队列相关的参数。

2. 设置队列参数:在 `be.conf` 文件中,可以设置如下参数来控制队列的行为:
   - `queue_type`: 设置队列的类型,比如 `stateful` 或 `stateless`。
   - `min_cluster_capacity`: 设置集群的最小容量。
   - `available_capacity`: 设置集群的可用容量。

3. 配置队列规则:Doris 允许根据不同的条件将查询请求分配到不同的队列。可以在 BE 配置文件中设置队列选择器(queue selector)的参数,以定义如何根据查询的特性(如数据量、查询成本等)来分配队列。

4. 设置队列数量:可以定义多个队列,并为每个队列分配不同的资源和优先级。

5. 调整查询的优先级:在某些情况下,可能希望某些查询比其他查询有更高的优先级。Doris 允许通过设置查询的优先级参数来实现这一点。

6. 重启 BE 服务:修改配置文件后,需要重启 BE 服务以使更改生效。

5.2 具体样例

1. 配置文件定位:找到Doris BE 服务的配置文件 `be.conf`。

2. 编辑配置文件:使用文本编辑器打开 `be.conf` 文件,并找到资源相关的配置部分。

3. 设置队列相关参数:以下是一个配置队列的示例:

# 在 be.conf 中设置队列参数的示例

# 启用队列管理功能
enable_queue=true

# 设置队列的类型,例如:stateful 或 stateless
queue_type=stateful

# 定义队列的容量和资源
queue_capacity=10GB

# 设置不同队列的资源分配和优先级
queue_list=(
    (queue_name=high_priority min_memory=4GB max_memory_proportion=0.7),
    (queue_name=normal_priority min_memory=2GB max_memory_proportion=0.5),
    (queue_name=low_priority min_memory=1GB max_memory_proportion=0.3)
)

# 设置队列选择器,决定查询应该放入哪个队列
queue_selector=cost_based

# 设置队列选择器的成本阈值
queue_cost_based_threshold=10000

4. 队列选择器:在上面的配置中,`queue_selector` 设置为 `cost_based`,这意味着 BE 会根据查询的成本(如扫描的数据量)来决定将查询放入哪个队列。

5. 资源分配:每个队列都有最小内存和最大内存比例的设置,这决定了该队列可以分配的资源量。

6. 重启服务:配置完成后,保存文件并重启 BE 服务以使配置生效。

5.3 参数说明

参数名称说明
Stateless(无状态)在无状态队列中,查询的调度与执行不依赖于之前的状态。每个查询都是独立调度的,与其它查询无关。这种队列类型适合于那些不需要考虑查询之间依赖关系的场景。
Stateful(有状态)有状态队列会考虑查询之间的依赖关系。
例如,如果一个查询正在等待某个资源,而这个资源被另一个查询占用,那么有状态队列会跟踪这种状态,并在资源可用时调度查询。这种队列类型适合于需要考虑资源锁定和查询依赖关系的场景。
queue_selector类型说明
cost_based基于成本的队列选择器,根据查询预计的成本(如资源消耗、执行时间等)来分配队列。
fair公平队列选择器,旨在为所有用户或查询提供相对公平的资源分配。
random随机队列选择器,根据随机算法将查询分配到不同的队列。
user_defined用户自定义队列选择器,允许用户根据自定义的逻辑来分配查询到特定的队列。
legacy遗留队列选择器,可能用于保持向后兼容性,按照旧版本的策略分配查询。
resource_broker资源经纪人队列选择器,可能根据当前的资源使用情况动态地分配查询。
  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卢子墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值