Partial Result Cache(PTRC)

您可以使用PolarDB for MySQL提供的Partial Result Cache(简称PTRC)功能来缓存查询语句中算子的中间结果集,来减少这些复杂算子的重复计算,以此来提升查询性能。本文介绍了PTRC的概念、工作原理、如何基于代价选择PTRC以及PTRC的动态反馈机制等内容。

PTRC概念

PTRC是针对一个查询内部算子级别粒度的结果集缓存特性,比如缓存Correlated subquery或NestLoopJoin等算子执行的临时结果,后续再次执行该算子时,如果结果集缓存中已存在算子的计算结果,则无需再重复执行该算子。

PTRC中的Partial有两层含义:

  • PTRC缓存的不是整个查询的结果集,而是查询内部某个或某几个算子执行的中间结果集。

  • 并不一定缓存算子所有的中间结果集,而是根据内存限制可能只缓存部分中间结果集。

相较于传统的Query Cache,PTRC的缓存粒度更小,是对查询内部某些算子进行加速,并且可以看出PTRC的生命周期也是跟随查询一起开始和结束。基于此,PTRC的适用范围会更广,因为只针对查询内部算子的优化,不存在跨节点查询时的数据一致性问题,只要一个算子符合要求(即算子执行时依赖的参数不变,执行结果也不会变)就可以使用PTRC,在选择PTRC时不仅需要遵循规则,还会基于代价来决策是否使用PTRC。

PTRC工作原理

PTRC的核心思想是缓存查询语句中算子的中间结果集来避免某些算子的重复执行,可以被PTRC加速的算子需要满足如下条件:

  • 算子执行时依赖相关性参数(Correlated Parameters),并且会被反复执行多次。如NestLoopJoin和相关子查询;

  • 算子的相关性参数保持不变,无论执行多少次,算子执行的结果是固定的。比如算子中不能有影响算子重复执行结果的函数Random、NOW和UDF等,否则缓存中的数据会影响最终结果的正确性。

算子执行时依赖的相关性参数(Correlated Parameters),即算子执行时所依赖的外部参数。例如t1 join t2 on t1.a = t2.at1表作为驱动表,t1表的每一行都要和t2表完成一次join操作,而 t1.a 则被认为是该NestLoopJoin算子执行时依赖的相关性参数。如果t1.at1表中存在较多的重复值,那PTRC将会减少这部分的重复计算。再比如Correlated subquery算子,每执行一次子查询,都依赖于父查询的一次扫描结果作为驱动参数。

此处以TPCH-Q17为例来说明PTRC的基本工作原理。查询语句示例如下:

SELECT
        sum(l_extendedprice) / 7.0 AS avg_yearly
FROM
        lineitem,
        part
WHERE
        p_partkey = l_partkey
        AND p_brand = 'Brand#34'
        AND p_container = 'MED BOX'
        AND l_quantity < (
                SELECT
                        0.2 * avg(l_quantity)
                FROM
                        lineitem
                WHERE
                        l_partkey = p_partkey
        );

PTRC以算子的相关性参数作为key,算子的执行结果作为value存储在缓存中。故TPCH-Q17中PTRC的缓存存储格式为:key= p_partkey, value = [true/false] 。

TPCH-Q17中相关子查询的PTRC的主要执行流程如下图所示:image

每次对相关子查询求值时,根据p_partkey的值在PTRC的缓存中查找结果:

  • 如果未命中,则需要执行子查询进行求值,并将求值结果记录到PTRC的缓存中;

  • 如果命中缓存中的结果,则直接将结果返回,从而避免重复执行一次子查询。

因为TPCH-Q17是partjoin lineitem表后再执行子查询,join之后的结果中p_partkey重复项非常多,而p_partkey又是子查询的相关性参数,所以TPCH-Q17的PTRC命中率会很高,性能提升会非常显著。

使用EXPLAIN命令可以查看执行计划,在子查询执行前添加了一个Partial Result Cache算子,则说明该子查询中引入了PTRC,如下图所示:PTRC执行计划

前提条件

PolarDB for MySQL集群需为8.0版本且Revision version为8.0.2.2.9或以上,您可以通过查询版本号来确认集群版本。

参数说明

参数

级别

说明

partial_result_cache_enabled

Global/Session

Partial Result Cache功能开关。取值范围如下:

  • ON(默认):启用Partial Result Cache功能。

  • OFF:禁用Partial Result Cache功能。

partial_result_cache_cost_threshold

Global/Session

PTRC的代价阈值。当单个查询的整体cost超过该阈值时,才使用PTRC。

取值范围:0~18446744073709551615。默认值:10000。

partial_result_cache_check_frequency

Global/Session

触发PTRC动态反馈检测的频率,当累计cache miss的次数达到该值时触发一次检测。

取值范围:0~18446744073709551615。默认值:200。

partial_result_cache_low_hit_rate

Global/Session

PTRC命中率的低水位阈值。当优化器估算的命中率高于此值时才考虑使用PTRC,如果已使用PTRC,动态反馈机制中发现真实命中率低于该值时,将直接放弃继续使用PTRC。

取值范围:0~100。默认值:20。

partial_result_cache_high_hit_rate

Global/Session

PTRC命中率的高水位阈值。当内存使用达到上限并且命中率高于此值时,内存缓存变更为文件存储缓存,已缓存的数据也会转存至文件中。

取值范围:0~100。默认值:70。

partial_result_cache_max_mem_size

Global/Session

单个查询中PTRC累积内存使用量。一个查询内部可能有多个PTRC,多个PTRC累计使用的内存不能超过该值。

取值范围:0~18446744073709551615。单位:Byte。默认值:67108864。

如何基于代价选择PTRC

根据PTRC执行流程可以看出,引入PTRC并不是越多越好,还取决于PTRC的命中率,如果命中率不高,引入PTRC会带来额外的性能开销,比如每一次查询都需要Check Cache,以及占用额外的内存等。

为了减少不必要的开销,优化器会基于代价来选择是否使用PTRC,主要从以下两个维度来进行判断:

  • 当查询的整体cost高于partial_result_cache_cost_threshold参数值时,才考虑引入PTRC;

  • 评估算子使用PTRC的命中率,当命中率高于partial_result_cache_low_hit_rate参数值时,才考虑引入PTRC。

基于代价选择PTRC时,优化器会优先检查partial_result_cache_cost_threshold参数值。

  • 当查询的整体cost小于该阈值时,该查询的执行代价比较低,本身应该就是一个短查询,即便引入了PTRC,性能提升也比较有限,且额外的Check Cache等操作可能还会对高并发下的短事务类查询的latency造成负面影响,所以提供了一个总的代价阈值来控制是否完全忽略PTRC。且该阈值不满足使用PTRC的条件时,减少了优化器阶段枚举所有表达式来判断是否使用PTRC的开销。

  • 当查询的整体cost大于或等于partial_result_cache_cost_threshold参数值时,优化器会遍历检查所有可能满足PTRC规则的算子,并估算满足规则的算子的PTRC命中率,计算公式为:

    hit_rate = (fanout - ndv)/ fanout

    其中,fanout表示一个算子需要被重复执行的总次数。ndv表示PTRC的key的唯一值的个数,即所有相关性参数组合值的唯一值个数。

当估算的hit_rate小于partial_result_cache_low_hit_rate参数值时,该算子就不会考虑使用PTRC。但在MySQL已有的代价模型中,它的统计信息依赖于表的索引或直方图,如果相关性参数所在的列没有索引或直方图,则很难评估出非常准确的ndv值,这种情况下查询时会尽量引入PTRC,然后交给PTRC执行阶段的动态反馈机制来动态检查是否有必要继续使用PTRC。

PTRC的动态反馈机制

在PTRC执行阶段,每次Cache是否命中都会记录到统计信息中,动态反馈机制负责对PTRC的真实命中率进行计算,如果发现实际命中率低于partial_result_cache_low_hit_rate参数值时,则会直接在执行阶段禁掉PTRC,恢复到没有引入PTRC前的执行状态,以减少低效的PTRC带来的额外开销。

参数partial_result_cache_check_frequency用于控制PTRC动态检测命中率的频率,即累计出现cache miss的次数,比如默认值为200,则当cache miss的次数累计达到200次后,就会触发一次动态反馈机制。

由于结果集缓存在内存中,当PTRC的内存使用量达到上限时,同样也会触发动态反馈机制,但此时的动态反馈不仅要检查命中率是否过低,还会决策是否进行数据淘汰或者是否需要将结果集转储到Disk。

当PTRC的内存使用量达到上限时,动态反馈策略如下:

  1. hit_rate低于partial_result_cache_low_hit_rate参数值时,认为命中率过低,直接禁用PTRC;

  2. hit_rate高于partial_result_cache_high_hit_rate参数值时,命中率大于高水位,会将内存中的缓存数据转存至Disk存储,即便转储至Disk,依然会有可预期的性能提升;

  3. hit_rate位于低水位和高水位之间时,则会触发LRU数据淘汰,将命中率不符合预期的数据清理掉,后续重新缓存新的数据,若新缓存数据后再次触发内存不足,则重复执行上述步骤1~步骤3。

参数partial_result_cache_max_mem_size限制单个查询中PTRC累积内存使用量,当查询中PTRC累积内存超过该限制,则所有的PTRC都将会触发动态反馈机制。

PTRC性能测试

通过基于代价选择PTRC的内容可以看出,影响PTRC加速效果的主要因素如下:

  • 使用PTRC加速的算子的执行代价要足够大。如果算子本身执行代价不高,则缓存加速的提升空间有限;

  • 缓存命中率。PTRC的缓存命中率越高,加速效果越明显。

以TPCH-Q17测试为例:image

上图中的子查询会重复执行很多次,在子查询中引入PTRC后的查询计划如下:image

经过测试统计信息计算,在TPCH-Q17子查询中引入PTRC后的缓存命中率高达96%,缓存加速效果非常明显。测试数据如下图:image

总结

PTRC是针对单个查询内部具有相关性参数依赖的复杂算子,通过缓存算子执行的中间结果集来减少这些复杂算子的重复计算,只要命中率足够高,就可以获得非常可观的加速效果。目前查询中Correlated Subquery、Nested Loop Join(包含Inner join、Outer join、Semi join以及Anti join)等多种算子均可以使用PTRC进行加速。