前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、查询缓存

DorisSQL Cache 是一种通过缓存查询结果来加速重复查询的机制,其核心设计目标是减少重复计算降低网络传输开销。它通过FEFrontend)与 BEBackend)协同工作,结合一致性哈希元数据版本校验实现高效缓存。以下从BE 实现原理FE 实现原理两方面展开说明。

1、原理

1)FEFrontend)实现原理

FE 是查询的入口,负责缓存的元数据管理路由控制,其核心逻辑是通过元数据版本校验决定是否复用缓存

a、 缓存元数据管理

  • 元数据存储:FE 内存中维护一个 Map<SQLHash, QueryMeta> 结构,记录已执行查询的元信息:
    • SQLHashSQL 字符串的哈希值(用于快速匹配)。
    • QueryMeta:包含查询涉及的表列表、表版本号、分区版本号、参数列表等。
  • 元数据同步FE 会定期与 BE 同步表版本信息,确保缓存有效性。

b、 查询路由流程

步骤1:SQL 匹配与元数据校验

  1. 收到查询请求后,计算 SQLSQLHash
  2. FE 内存的 Map中查找 SQLHash
    • 未找到:执行完整查询流程(解析、优化、下发到 BE)。
    • 找到:检查 QueryMeta中的表版本和分区版本是否与当前一致:
      • 不一致:说明数据已变更,执行完整查询流程。
      • 一致:进入步骤 2(缓存路由)。可跳过解析和优化

步骤2:缓存路由与结果返回

  1. 根据 QueryMeta 中的 CacheKey 通过一致性哈希定位到目标 BE
  2. 向目标 BE 发送缓存检索请求:
    • BE 命中缓存:直接返回 CachedResult,FE 封装后响应客户端。
    • BE 未命中缓存:执行完整查询流程,并将结果写入 BE 缓存。

2)BEBackend)实现原理

BE 是实际执行查询并存储缓存结果的节点,其缓存机制的核心是内存中的 HashMap 结构,结合一致性哈希实现缓存的分布式存储与快速检索。

a、 缓存存储结构

  • 数据结构:BE 使用 HashMap<CacheKey, CachedResult>存储缓存结果,其中:
    • CacheKey:由查询的 SQL 字符串、参数(如 LIMIT 值)、表版本号、分区版本号等元数据通过哈希算法生成。
    • CachedResult:存储查询结果数据(如 ResultSet)及结果元信息(如数据大小、过期时间)。
  • 存储位置:缓存数据存放在 BE堆内存中,受 mem_limit 参数限制(默认无上限,需监控避免 OOM)。

b、 一致性哈希定位

  • 哈希算法:当 FE 发起查询时,会根据 SQLCacheKey通过一致性哈希算法选择一个 BE 节点存储或检索缓存。

    • 目的:确保相同查询的缓存始终落在同一 BE,避免分布式缓存一致性问题。

    • 示例:

      // 伪代码:一致性哈希选择BE
      int beId = consistentHash(cacheKey) % beNodeCount;
      

c、 缓存读写流程

写入缓存(Query Execution Aftermath

  1. BE 执行查询后,将结果封装为 CachedResult
  2. 计算当前查询的 CacheKey(包含表版本、分区版本等)。
  3. 通过一致性哈希定位到目标 BE(可能是自身或其他节点)。
  4. (CacheKey, CachedResult) 存入目标 BEHashMap

读取缓存(Query Execution Beginning

  1. 收到查询请求后,计算 CacheKey
  2. 通过一致性哈希定位到目标 BE
  3. 在目标 BEHashMap中查找 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,当查询计划树中出现LogicalSqlCachePhysicalSqlCache节点时,即表明查询已命中 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)排查缓存失效原因

  1. 表/视图的结构发生了变化,例如执行了 drop tablereplace tablealter tablealter view 等操作。
  2. 表数据发生了变化,例如执行了 insertdeleteupdatetruncate 等操作。
  3. 用户权限被移除,例如执行了 revoke 操作。
  4. 使用了非确定函数,并且函数的评估值发生了变化,例如执行了 select random()
  5. 使用了变量,并且变量的值发生了变化,例如执行了 select * from tbl where dt = @dt_var
  6. Row Policy 或 Data Masking 发生了变化,例如设置了用户对某些表的部分数据不可见。
  7. 结果行数超过了 FE 配置的 cache_result_max_row_count,默认值为 3000 行。
  8. 结果大小超过了 FE 配置的 cache_result_max_data_size,默认值为 30MB。

3、关键特性

a、缓存粒度控制

  • 细粒度缓存:缓存基于完整的 SQL 字符串、参数和元数据版本,确保相同语义的查询复用缓存。
  • 避免缓存雪崩:通过一致性哈希分散缓存压力,避免单个 BE 内存过载。

b、 缓存一致性保障

  • 强一致性:通过表版本和分区版本校验,确保缓存与数据实时一致。
  • 自动失效:数据变更时自动淘汰旧缓存,无需手动干预。

c、性能优化

  • 减少解析开销FE 跳过 SQL 解析和优化阶段,直接路由到 BE
  • 降低网络传输BE 本地缓存结果,避免重复计算和数据传输。

d、适用场景与限制

  • 适用场景:
    • 重复查询频繁(如仪表盘、定时报表)。
    • 查询结果数据量小(缓存内存占用低)。
    • 数据变更不频繁(缓存命中率高)。
  • 限制:
    • 高并发写入场景:频繁的数据变更会导致缓存频繁失效,降低命中率。
    • 超大结果集:缓存会占用大量 BE 内存,可能引发 OOM。
    • 复杂查询:包含 UDFSUBQUERY 的查询可能无法命中缓存(因 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 文本顺序

  • 作用:作为 Leading Hint 的特例,强制 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 操作的数据分发策略(ShuffleBroadcast),优化网络传输和计算负载。

  • 语法:

    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 输出中会显示 DistributionSpecReplicatedBroadcast)或 DistributionSpecHashShuffle)。

2、Hint Log

Hint Log 主要用于在执行 EXPLAIN 时显示提示是否生效。其显示位置通常位于 EXPLAIN 输出的最下方。用户可以通过 Hint Log 查看生效情况以及未生效原因,便于调整和验证。

Hint Log 分为三个状态:

+---------------------------------+
| Hint log:                       |
| Used:                           |
| UnUsed:                         |
| SyntaxError:                    |
+---------------------------------+
  • Used:表明该提示生效了。
  • UnUsedSyntaxError:都表明该提示未生效。
  • SyntaxError 表示提示语法使用错误或该语法不支持,同时会附加不支持的原因信息。

三、计划调优

1、优化表 Schema

Schema 设计和调优中,表设计是其中重要的一部分,包括表引擎选择、分区分桶列选择、分区分桶大小设置、key 列和字段类型优化等。缺乏 Schema 设计的系统,有可能会导致数据倾斜等问题,不能充分利用系统并行和排序特性,从而影响 Doris 在业务系统中发挥真实的性能优势。

1)表引擎选择

优化建议:当业务无数据更新需求,但对查询性能有较高要求时,推荐使用

Doris 支持 DuplicateUniqueAggregate 三种表模型。其中,Unique 又可以进一步分为 Merge-On-ReadMOR)和 Merge-On-WriteMOW)两种。

这几种表模型的查询性能,由好到差依次为:Duplicate > MOW > MOR == Aggregate。因此,通常情况下,如果没有特殊需求,推荐使用 Duplicate 表,以获得更好的查询性能。

2)分桶列选择

优化建议:检查分桶列是否存在数据倾斜问题,如果存在,则更换为在业务含义上具有充分散列特性的字段作为分桶列。

Doris 支持对数据进行分桶操作,即依据 Schema 中预设的分桶键来分布数据,进而形成数据 Bucket

选取恰当的分桶列,对于原始数据的合理分布至关重要,它能有效防止数据倾斜所引发的性能问题。同时,这也能最大化地利用 Doris 提供的 Colocate JoinBucket 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 c2count(*) 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 的设计及后期优化提供了重要启示:

  1. 在满足业务系统表达和计算需求的前提下,应优先选择定长类型,避免使用变长类型;
  2. 尽量采用低精类型,避免高精类型。具体实践包括:使用 BIGINT 替代 VARCHARSTRING 类型的字段,以及用 FLOAT / INT / BIGINT 替换 DECIMAL 类型的字段等。此类字段类型的合理设计和优化,将极大地提升业务的计算效率,从而增强系统性能。

5)使用分区裁剪优化扫表

Doris 作为一款高性能实时分析数据库,提供了强大的分区裁剪(Partition Pruning)功能,可以显著提升查询性能。分区裁剪是一种查询优化技术,它通过分析查询条件,智能识别与查询相关的分区,并仅扫描这些分区的数据,从而避免了对无关分区的不必要扫描。这种优化方式能够大幅减少 I/O 操作和计算量,进而加速查询执行。

2、优化索引设计和使用

Doris 目前支持两类索引:

  1. 内置索引:包括前缀索引和 ZoneMap 索引等;
  2. 二级索引:包括倒排索引、Bloomfilter 索引、N-Gram Bloomfilter 索引和 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 或聚合场景。然而,数据倾斜(Data Skew(如某些键值的数据量远大于其他键值)会导致 RuntimeFilter 生成缓慢或失效,进而引发查询超时或性能下降。以下是典型案例及调优过程的详细说明:

实际生产场景会遇到因为 RuntimeFilter 等待时间不合理,引起的性能问题的情况。RuntimeFilter 是一种查询优化技术,它通过运行时生成过滤条件,从而避免了对无关数据的扫描。这种优化方式能够大幅减少 I/O 操作和计算量,进而加速查询执行。下面介绍几种常见的案例,帮助在数据倾斜场景下进行调优。。

比如:Join 耗时大概是1sec780ms,所以 RuntimeFilter1s 内并没有等到。于是调整 RuntimeFilter 的等待时间:

set runtime_filter_wait_time_ms = 3000;

3、并行度调优

1)并行度调优的原则

parallel_pipeline_task_num 设定目的是为了充分利用多核资源,降低查询的延迟;但是,为了多核并行执行,通常会引入一些数据 Shuffle 算子,以及多线程之间同步的逻辑,这也会带来一些不必要的资源浪费。

Doris 中默认值为0,即 BECPU 核数目的一半,这个值考虑了单查询和并发的资源利用的情况,通常不需要用户介入调整。当存在性能瓶颈时可以参考下面示例进行必要的调整。Doris 在持续完善自适应的策略,通常建议在特定场景或 SQL 级别进行必要的调整。

假设 BECPU 核数为 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)总结

通常用户不需要介入调整查询并行度,如需要调整,需要注意以下事项:

  1. 建议从 CPU 利用率出发。通过 PROFILE 工具输出观察是否是 CPU 瓶颈,尝试进行并行度的合理修改
  2. SQL 调整比较安全,尽量不要全局做过于激进的修改

ContactAuthor