大数据Doris之_12_查询加速
前言
Github:https://github.com/HealerJean
一、查询缓存
Doris的SQLCache是一种通过缓存查询结果来加速重复查询的机制,其核心设计目标是减少重复计算和降低网络传输开销。它通过FE(Frontend)与BE(Backend)协同工作,结合一致性哈希和元数据版本校验实现高效缓存。以下从BE 实现原理和FE 实现原理两方面展开说明。
1、原理
1)FE(Frontend)实现原理
FE是查询的入口,负责缓存的元数据管理和路由控制,其核心逻辑是通过元数据版本校验决定是否复用缓存。
a、 缓存元数据管理
- 元数据存储:FE 内存中维护一个
Map<SQLHash, QueryMeta>结构,记录已执行查询的元信息:SQLHash:SQL字符串的哈希值(用于快速匹配)。QueryMeta:包含查询涉及的表列表、表版本号、分区版本号、参数列表等。
- 元数据同步:
FE会定期与BE同步表版本信息,确保缓存有效性。
b、 查询路由流程
步骤1:SQL 匹配与元数据校验
- 收到查询请求后,计算
SQL的SQLHash。 - 在
FE内存的Map中查找SQLHash:- 未找到:执行完整查询流程(解析、优化、下发到 BE)。
- 找到:检查
QueryMeta中的表版本和分区版本是否与当前一致:- 不一致:说明数据已变更,执行完整查询流程。
- 一致:进入步骤 2(缓存路由)。可跳过解析和优化
步骤2:缓存路由与结果返回
- 根据
QueryMeta中的CacheKey通过一致性哈希定位到目标BE。 - 向目标
BE发送缓存检索请求:BE命中缓存:直接返回CachedResult,FE 封装后响应客户端。BE未命中缓存:执行完整查询流程,并将结果写入BE缓存。
2)BE(Backend)实现原理
BE是实际执行查询并存储缓存结果的节点,其缓存机制的核心是内存中的HashMap结构,结合一致性哈希实现缓存的分布式存储与快速检索。
a、 缓存存储结构
- 数据结构:
BE使用HashMap<CacheKey, CachedResult>存储缓存结果,其中:CacheKey:由查询的SQL字符串、参数(如LIMIT值)、表版本号、分区版本号等元数据通过哈希算法生成。CachedResult:存储查询结果数据(如ResultSet)及结果元信息(如数据大小、过期时间)。
- 存储位置:缓存数据存放在
BE的堆内存中,受mem_limit参数限制(默认无上限,需监控避免OOM)。
b、 一致性哈希定位
-
哈希算法:当
FE发起查询时,会根据SQL的CacheKey通过一致性哈希算法选择一个BE节点存储或检索缓存。-
目的:确保相同查询的缓存始终落在同一
BE,避免分布式缓存一致性问题。 -
示例:
// 伪代码:一致性哈希选择BE int beId = consistentHash(cacheKey) % beNodeCount;
-
c、 缓存读写流程
写入缓存(Query Execution Aftermath)
BE执行查询后,将结果封装为CachedResult。- 计算当前查询的
CacheKey(包含表版本、分区版本等)。 - 通过一致性哈希定位到目标
BE(可能是自身或其他节点)。 - 将
(CacheKey, CachedResult)存入目标BE的HashMap。
读取缓存(Query Execution Beginning)
- 收到查询请求后,计算
CacheKey。 - 通过一致性哈希定位到目标
BE。 - 在目标
BE的HashMap中查找CacheKey:- 命中:直接返回
CachedResult,跳过执行计划。 - 未命中:执行完整查询流程,并将结果写入缓存。
- 命中:直接返回
d、 缓存失效机制
-
表/分区变更
:当表或分区的数据变更时,其版本号会递增。此时:
- 旧版本的
CacheKey会失效(因包含旧版本号)。 - 后续查询会重新计算并写入新缓存。
- 旧版本的
-
手动清理:通过
ADMIN CLEAR CACHE命令强制清理缓存:ADMIN CLEAR CACHE FOR TABLE db_name.table_name;
2、缓存管理
1)开启和关闭 SQL Cache
-- 在当前 Session 打开 SQL Cache, 默认是关闭状态
set enable_sql_cache=true;
-- 在当前 Session 关闭 SQL Cache
set enable_sql_cache=false;
-- 全局打开 SQL Cache, 默认是关闭状态
set global enable_sql_cache=true;
-- 全局关闭 SQL Cache
set global enable_sql_cache=false;
2)检查查询是否命中 SQL Cache
用户能够通过执行explain plan语句检查当前查询是否能够成功命中 SQL Cache,当查询计划树中出现LogicalSqlCache或PhysicalSqlCache节点时,即表明查询已命中 SQL Cache
3)内存控制
a、FE 内存控制
在
FE中,Cache的元数据信息被设置为弱引用。当FE内存不足时,系统会自动释放最近最久未使用的Cache元数据。此外,用户还可以通过执行以下SQL语句,进一步限制FE内存的使用量。此配置实时生效,且每个FE都需要进行配置。若需持久化配置,则需将其保存在fe.conf文件中。
-- 最多存放 100 个 Cache 元数据,超过时自动释放最近最久未使用的元数据。默认值为 100。
ADMIN SET FRONTEND CONFIG ('sql_cache_manage_num'='100');
-- 当 300 秒未访问该 Cache 元数据后,自动进行释放。默认值为 300。
ADMIN SET FRONTEND CONFIG ('expire_sql_cache_in_fe_second'='300');
另外还可以在 FE 中配置,当结果行数或大小超过某个阈值时,不创建 SQL Cache:
-- 默认超过 3000 行结果时,不创建 SQL Cache。
ADMIN SET FRONTEND CONFIG ('cache_result_max_row_count'='3000');
-- 默认超过 30M 时,不创建 SQL Cache。
ADMIN SET FRONTEND CONFIG ('cache_result_max_data_size'='31457280');
b、BE 内存控制
在
be.conf文件中进行以下配置更改,重启BE后生效:
-- 当 Cache 的内存空间超过 query_cache_max_size_mb + query_cache_elasticity_size_mb 时,
-- 释放最近最久未使用的 Cache,直至占用内存低于 query_cache_max_size_mb。
query_cache_max_size_mb = 256
query_cache_elasticity_size_mb = 128
4)排查缓存失效原因
- 表/视图的结构发生了变化,例如执行了
drop table、replace table、alter table或alter view等操作。 - 表数据发生了变化,例如执行了
insert、delete、update或truncate等操作。 - 用户权限被移除,例如执行了
revoke操作。 - 使用了非确定函数,并且函数的评估值发生了变化,例如执行了
select random()。 - 使用了变量,并且变量的值发生了变化,例如执行了
select * from tbl where dt = @dt_var。 - Row Policy 或 Data Masking 发生了变化,例如设置了用户对某些表的部分数据不可见。
- 结果行数超过了 FE 配置的
cache_result_max_row_count,默认值为 3000 行。 - 结果大小超过了 FE 配置的
cache_result_max_data_size,默认值为 30MB。
3、关键特性
a、缓存粒度控制
- 细粒度缓存:缓存基于完整的
SQL字符串、参数和元数据版本,确保相同语义的查询复用缓存。 - 避免缓存雪崩:通过一致性哈希分散缓存压力,避免单个
BE内存过载。
b、 缓存一致性保障
- 强一致性:通过表版本和分区版本校验,确保缓存与数据实时一致。
- 自动失效:数据变更时自动淘汰旧缓存,无需手动干预。
c、性能优化
- 减少解析开销:
FE跳过SQL解析和优化阶段,直接路由到BE。 - 降低网络传输:
BE本地缓存结果,避免重复计算和数据传输。
d、适用场景与限制
- 适用场景:
- 重复查询频繁(如仪表盘、定时报表)。
- 查询结果数据量小(缓存内存占用低)。
- 数据变更不频繁(缓存命中率高)。
- 限制:
- 高并发写入场景:频繁的数据变更会导致缓存频繁失效,降低命中率。
- 超大结果集:缓存会占用大量
BE内存,可能引发 OOM。 - 复杂查询:包含
UDF、SUBQUERY的查询可能无法命中缓存(因CacheKey难以精确匹配)。
二、Hint
Hint(提示) 是一种显式指导查询优化器生成执行计划的机制,,用于指导数据库查询优化器如何生成指定的计划。通过提供Hint,用户可以对查询优化器的默认行为进行微调,以期望获得更好的性能或满足特定需求。
1、Hint 分类
Hint 类型 |
作用 | 典型场景 | 关键语法 |
|---|---|---|---|
Leading Hint |
指定 Join 顺序 |
数据倾斜、复杂查询优化 | /*+ LEADING(t2 t1) */ |
Ordered Hint |
保持原始 SQL 文本顺序 |
快速验证、教学演示 | /*+ ORDERED */ |
Distribute Hint |
控制数据分发方式(Shuffle/Broadcast) |
大表 Join 优化、资源控制 |
/*+ DISTRIBUTE(t1 BROADCAST) */ |
1)Leading Hint:控制表连接顺序
-
作用:强制指定多表
Join的顺序,覆盖优化器的默认决策。 -
语法:
SELECT /*+ LEADING(t2 t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -
适用场景:
- 数据倾斜:当小表与大表
Join时,将小表作为驱动表(如LEADING(t2 t1)中t2为小表),避免大表广播导致的内存压力。 - 复杂查询:在多表
Join中,通过调整顺序减少中间结果集大小(如先过滤条件严格的表)。
- 数据倾斜:当小表与大表
-
示例:
-- 原始计划:优化器默认顺序可能导致大表先参与计算 EXPLAIN SELECT * FROM large_table t1 JOIN small_table t2 ON t1.id = t2.id; -- 优化后:强制小表驱动,减少数据移动 EXPLAIN SELECT /*+ LEADING(t2 t1) */ * FROM t1 JOIN t2 ON t1.id = t2.id;通过
EXPLAIN可观察到 Join 顺序变化,若Hint生效,输出中会显示Used: leading(t2 t1)
2)Ordered Hint:保持原始 SQL 文本顺序
-
作用:作为
LeadingHint的特例,强制Join顺序与SQL文本中的表顺序一致。 -
语法:
SELECT /*+ ORDERED */ * FROM t1 JOIN t2 ON t1.id = t2.id JOIN t3 ON t2.id = t3.id; -
适用场景:
- 快速验证:当怀疑优化器对简单查询的顺序判断错误时,快速测试原始顺序的性能。
- 教学演示:展示不同
Join顺序对性能的影响。
-
注意:仅适用于确认原始顺序更优的场景,盲目使用可能导致性能下降。
3)Distribute Hint:控制数据分发方式
-
作用:指定
Join操作的数据分发策略(Shuffle或Broadcast),优化网络传输和计算负载。 -
语法:
SELECT /*+ DISTRIBUTE(t1 BROADCAST) */ * FROM t1 JOIN t2 ON t1.id = t2.id; -
分发类型:
BROADCAST:将小表复制到所有节点,与大表本地Join(适合小表 <100MB)。SHUFFLE:按Hash重新分区数据,适合大表与大表Join。
-
适用场景:
- 数据倾斜:通过
Broadcast避免Shuffle导致的热点节点。 - 资源控制:限制大表的
Shuffle范围,减少网络开销。
- 数据倾斜:通过
-
示例:
-- 原始计划:优化器默认 Shuffle 导致数据倾斜 EXPLAIN SELECT * FROM large_table t1 JOIN large_table t2 ON t1.id = t2.id; -- 优化后:强制 Broadcast 小表(需确保 t2 足够小) EXPLAIN SELECT /*+ DISTRIBUTE(t2 BROADCAST) */ * FROM t1 JOIN t2 ON t1.id = t2.id;若
Hint生效,EXPLAIN输出中会显示DistributionSpecReplicated(Broadcast)或DistributionSpecHash(Shuffle)。
2、Hint Log
HintLog主要用于在执行EXPLAIN时显示提示是否生效。其显示位置通常位于EXPLAIN输出的最下方。用户可以通过HintLog查看生效情况以及未生效原因,便于调整和验证。
Hint Log 分为三个状态:
+---------------------------------+
| Hint log: |
| Used: |
| UnUsed: |
| SyntaxError: |
+---------------------------------+
Used:表明该提示生效了。UnUsed和SyntaxError:都表明该提示未生效。SyntaxError表示提示语法使用错误或该语法不支持,同时会附加不支持的原因信息。
三、计划调优
1、优化表 Schema
Schema设计和调优中,表设计是其中重要的一部分,包括表引擎选择、分区分桶列选择、分区分桶大小设置、key列和字段类型优化等。缺乏Schema设计的系统,有可能会导致数据倾斜等问题,不能充分利用系统并行和排序特性,从而影响Doris在业务系统中发挥真实的性能优势。
1)表引擎选择
优化建议:当业务无数据更新需求,但对查询性能有较高要求时,推荐使用
Doris 支持 Duplicate、Unique、Aggregate 三种表模型。其中,Unique 又可以进一步分为 Merge-On-Read(MOR)和 Merge-On-Write(MOW)两种。
这几种表模型的查询性能,由好到差依次为:Duplicate > MOW > MOR == Aggregate。因此,通常情况下,如果没有特殊需求,推荐使用 Duplicate 表,以获得更好的查询性能。
2)分桶列选择
优化建议:检查分桶列是否存在数据倾斜问题,如果存在,则更换为在业务含义上具有充分散列特性的字段作为分桶列。
Doris 支持对数据进行分桶操作,即依据 Schema 中预设的分桶键来分布数据,进而形成数据 Bucket。
选取恰当的分桶列,对于原始数据的合理分布至关重要,它能有效防止数据倾斜所引发的性能问题。同时,这也能最大化地利用 Doris 提供的 Colocate Join 和 Bucket Shuffle Join 特性,从而显著提升 Join 操作的性能。
以下面 t1 表的建表语句为例,当前分桶列选定为 c2。然而,在实际数据导入过程中,若 c2 列的值全部默认为 null,那么即便设定了 64 个分桶,实际上也只有一个分桶会包含所有数据。这种极端情况会导致严重的数据倾斜,进而产生性能瓶颈。
CREATE TABLE `t1` (
`c1` INT NULL,
`c2` INT NULL
) ENGINE=OLAP
DUPLICATE KEY(`c1`)
DISTRIBUTED BY HASH(`c2`) BUCKETS 64
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
insert into t1 select number, null from numbers ('number'='10000000');
针对上述情况,我们可以将分桶列从 c2 改为 c1,以实现数据的充分散列,并最大化地利用系统的并行处理能力,从而达到调优的目的。因此,在 Schema 设计阶段,业务人员需要根据业务特性,提前进行合理的分桶列设计。
例如,如果预先了解到 c2 列的业务含义中可能包含大量倾斜的值,如 null 或某些特定的值,那么就应该避免选择这些字段作为分桶列。相反,应该选择那些在业务含义上具有充分散列特性的字段,如用户 ID,作为分桶列。在性能问题排查阶段,可以使用以下 SQL 语句来确认分桶字段是否存在数据倾斜,并据此进行后续的优化调整。
select c2,count(*) cnt from t1 group by c2 order by cnt desc limit 10;
可以明确的是,良好的事前设计能够显著降低事后问题发生时的定位和修正成本。因此,强烈推荐业务人员在 Schema 设计阶段进行严格的设计和检查,以避免引入不必要的成本。
3)Key 列优化
优化建议:将业务查询中频繁使用的列设定为 Key 列,以加速查询过程。
Doris在存储层面确保数据依据Key列进行排序。这一特性为数据查询的性能优化提供了新的思路。具体来说,在Schema设计阶段,若能将业务查询中频繁使用的等值或范围查询列定义为Key列,将会显著提升这类查询的执行速度,进而提升整体性能。
以下是一组业务查询需求的示例:
select * from t1 where t1.c1 = 1;
select * from t1 where t1.c1 > 1 and t1.c1 < 10;
select * from t1 where t1.c1 in (1, 2, 3);
针对上述业务需求和 t1 表的 Schema 设计与后期优化,可以考虑将 c1 列作为 Key 列,以加速查询过程。以下是一个示例:
CREATE TABLE `t1` (
`c1` INT NULL,
`c2` INT NULL
) ENGINE=OLAP
DUPLICATE KEY(`c1`)
DISTRIBUTED BY HASH(`c2`) BUCKETS 10
PROPERTIES (
"replication_allocation" = "tag.location.default: 1"
);
4)字段类型优化
优化建议:在定义 Schema 类型时,应遵循定长和低精优先的原则。
在数据库系统中,不同类型的数据其处理复杂程度可能存在显著差异。例如,变长类型的数据处理相较于定长类型而言,其复杂性要高得多;同样,高精类型的数据处理也比低精类型更为复杂。
这一特性对业务系统 Schema 的设计及后期优化提供了重要启示:
- 在满足业务系统表达和计算需求的前提下,应优先选择定长类型,避免使用变长类型;
- 尽量采用低精类型,避免高精类型。具体实践包括:使用
BIGINT替代VARCHAR或STRING类型的字段,以及用FLOAT/INT/BIGINT替换DECIMAL类型的字段等。此类字段类型的合理设计和优化,将极大地提升业务的计算效率,从而增强系统性能。
5)使用分区裁剪优化扫表
Doris 作为一款高性能实时分析数据库,提供了强大的分区裁剪(Partition Pruning)功能,可以显著提升查询性能。分区裁剪是一种查询优化技术,它通过分析查询条件,智能识别与查询相关的分区,并仅扫描这些分区的数据,从而避免了对无关分区的不必要扫描。这种优化方式能够大幅减少 I/O 操作和计算量,进而加速查询执行。
2、优化索引设计和使用
Doris 目前支持两类索引:
- 内置索引:包括前缀索引和
ZoneMap索引等; - 二级索引:包括倒排索引、
Bloomfilter索引、N-GramBloomfilter索引和Bitmap索引等
1)优化 Key 列顺序利用前缀索引加速查询
优化提示:在定义 schema 列顺序时,应参考业务查询过滤中的高频高优列,以便充分利用 Doris 的前缀索引加速功能。
由于 Doris 内置了前缀索引功能,它会在建表时自动取表 Key 的前 36 字节作为前缀索引。当查询条件与前缀索引的前缀相匹配时,可以显著加快查询速度
2)使用倒排索引加速查询
优化建议:对于文本类型的全文检索,以及字符串、数值、日期时间类型字段上的等值或范围查询,均可利用倒排索引来加速查询。特别是在某些情况下,如原始表结构和 Key 定义不便优化,或重新导入表数据的成本较高时,倒排索引提供了一种灵活的加速方案,以优化业务执行性能。
Doris 支持倒排索引作为二级索引,以加速等值、范围及文本类型的全文检索等业务场景。倒排索引的创建和管理是独立的,它能够在不影响原始表 Schema 和无需重新导入表数据的情况下,便捷地进行业务性能优化。
3)使用物化视图
五、执行调优
1、常见调优参数
| 参数 | 说明 | 默认值 | 使用场景 |
|---|---|---|---|
| enable_nereids_planner | 是否打开新优化器 | TRUE | 低版本升级等场景,此开关初始为 false;升级后,可设置为 true |
| enable_nereids_dml | 是否启用新优化器的 DML 支持 | TRUE | 低版本升级等场景,此开关初始为 false;升级后,可设置为 true |
| parallel_pipeline_task_num | Pipeline 并行度 |
0 | 低版本升级等场景,此值为之前设置的固定值;升级后,可设置为 0,表示由系统自适应策略决定并行度 |
| runtime_filter_mode | Runtime Filter 类型 | GLOBAL | 低版本升级等场景,此值为 NONE,表示不启用 Runtime Filter;升级后,可设置为 GLOBAL,表示默认启用 Runtime Filter |
2、RuntimeFilter
RuntimeFilter(运行时过滤) 是一种通过动态生成过滤条件来减少数据扫描量的优化技术,尤其适用于大表Join或聚合场景。然而,数据倾斜(DataSkew)(如某些键值的数据量远大于其他键值)会导致RuntimeFilter生成缓慢或失效,进而引发查询超时或性能下降。以下是典型案例及调优过程的详细说明:实际生产场景会遇到因为
RuntimeFilter等待时间不合理,引起的性能问题的情况。RuntimeFilter是一种查询优化技术,它通过运行时生成过滤条件,从而避免了对无关数据的扫描。这种优化方式能够大幅减少I/O操作和计算量,进而加速查询执行。下面介绍几种常见的案例,帮助在数据倾斜场景下进行调优。。
比如:Join 耗时大概是1sec780ms,所以 RuntimeFilter 在 1s 内并没有等到。于是调整 RuntimeFilter 的等待时间:
set runtime_filter_wait_time_ms = 3000;
3、并行度调优
1)并行度调优的原则
parallel_pipeline_task_num 设定目的是为了充分利用多核资源,降低查询的延迟;但是,为了多核并行执行,通常会引入一些数据 Shuffle 算子,以及多线程之间同步的逻辑,这也会带来一些不必要的资源浪费。
Doris 中默认值为0,即 BE 的 CPU 核数目的一半,这个值考虑了单查询和并发的资源利用的情况,通常不需要用户介入调整。当存在性能瓶颈时可以参考下面示例进行必要的调整。Doris 在持续完善自适应的策略,通常建议在特定场景或 SQL 级别进行必要的调整。
假设 BE 的 CPU 核数为 16:
1.对于单表的简单操作(如单表点差、where 扫描获取少量数据,limit 少量数据,命中物化视图) 并行度可设置为 1
说明:单表的简单操作,只有一个 Fragment,查询的瓶颈通常在数据扫描处理上,数据扫描线程和查询执行的线程是分开的,数据扫描线程会自适应的做并行的扫描,这里的瓶颈不是查询线程,并行度可以直接设置为 1。
2.对于两表 JOIN 的查询/聚合查询,如果数据量很大,确认是 CPU 瓶颈型查询,并行度可设置为 16。
说明:对于两表 JOIN/聚合查询,这类数据计算密集型的查询,如果观察 CPU 没有打满,可以考虑在默认值的基础上,继续调大并行度,利用 Pipeline 执行引擎的并行能力,充分利用 CPU 资源参与计算。并不能保证每个 PipelineTask 都能将分配给它的 CPU 资源使用到极限。因此,可以适当调整并行度,比如设为 16,以更充分地利用 CPU。然而,不应无限制地增加并行度,设置为 48 根本不会带来实质性的收益,反而会增加线程调度开销和框架调度开销。
3.对于压力测试场景,压测的多个查询的任务本身就能够充分利用 CPU,可以考虑并行度设置为 1。
说明:对于压力测试场景,压测的查询的任务足够多。过大的并行度同样带来了线程调度开销和框架调度开销,这里需要设置为 1 是比较合理的。
4.复杂查询的情况要根据 Profile 和机器负载,灵活调整,这里建议使用默认值,如果不合适可以尝试 4-2-1 的阶梯方式调整,观察查询表现和机器负载。
2)调整方法
Doris 可以手动指定查询的并行度,以调整查询执行时并行执行的效率。
a、SQL 级别调整
通过 SQL ``HINT 来指定单个 SQL 的并行度,这样可以灵活控制不同 SQL` 的并行度来取得最佳的执行效果
select /*+SET_VAR("parallel_pipeline_task_num=8")*/ * from nation, lineitem where lineitem.l_suppkey = nation.n_nationkey
select /*+SET_VAR("parallel_pipeline_task_num=8,runtime_filter_mode=global")*/ * from nation, lineitem where lineitem.l_suppkey = nation.n_nationkey
b、会话级别调整
通过 session variables 来调整会话级别的并行度,session 中的所有查询语句都将以指定的并行度执行。请注意,即使是单行查询的 SQL,也会使用该并行度,可能导致性能下降。
set parallel_pipeline_task_num = 8;
c、全局调整:
如果需要全局调整,通常涉及 cpu 利用率的调整,可以 global 设置并行度
set global parallel_pipeline_task_num = 8;
3)总结
通常用户不需要介入调整查询并行度,如需要调整,需要注意以下事项:
- 建议从
CPU利用率出发。通过PROFILE工具输出观察是否是CPU瓶颈,尝试进行并行度的合理修改 - 单
SQL调整比较安全,尽量不要全局做过于激进的修改


