Partial Result Cache(PTRC)
操作场景
鉴于结果集缓存对查询性能的增益,我们在TaurusDB引入Partial result cache这一新特性,简称PTRC。顾名思义,这也是一个结果集缓存特性。不同于传统的结果集缓存,PTRC是用来辅助单个查询的内部算子的执行。也就是说PTRC粒度更小,是对查询内部的某个算子的中间结果进行缓存,从而起到算子加速的作用。
PTRC中的Partial有两层含义:
- PTRC缓存的不是查询的全部数据,而是某个算子(比如Nested loop join, Correlated subquery等)的中间结果集的缓存。
- 缓存并不一定需要缓存该算子的所有中间结果,而是根据内存大小限制缓存部分数据。
从这两点可以看出,PTRC 与单个查询相关,其生命周期从查询开始至查询结束,并自动终止。由于它用于加速算子,因此一个查询内部可以包含多个 PTRC。只要优化器通过代价计算判断某个算子适合使用 PTRC,则优化器就会为该算子引入PTRC。
工作原理
PTRC的核心原理是针对“参数化的重复扫描”类算子进行加速。此类算子的特点是需要根据外部输入的不同参数值,反复执行相同的扫描或计算逻辑,例如:
- Nested Loop Join:对于外表扫描的每一行,内表都会根据连接条件进行一次扫描。
- Correlated Subquery:对于父查询的每一行结果,都会驱动执行一次子查询。
PTRC通过创建以参数值为键(Key)、以对应计算结果为值(Value)的缓存结构来工作。当算子再次执行时,首先尝试根据当前参数值从PTRC中获取结果。若命中,则直接返回缓存值,跳过实际计算;若未命中,则执行原计算逻辑,并将结果存入缓存以供后续使用。以相关子查询为例,其工作流程如下:
控制机制与执行策略
根据PTRC执行流程,引入PTRC并非越多越好,其效果取决于缓存命中率。如果命中率不高,PTRC反而会引入额外的性能开销,例如每次查询都需要检查缓存,以及占用额外的内存空间。为此,我们引入了部分PTRC参数,用于在优化阶段和执行阶段对PTRC的行为进行控制。
引入的PTRC参数,建议保持系统默认值,且不支持在管理控制台修改。如需修改,请提交工单。
参数的详细介绍请参考后文详细内容。
- rds_partial_result_cache_cost_threshold
- rds_partial_result_cache_max_mem_size
- rds_partial_result_cache_min_hit_ratio
- rds_partial_result_cache_hit_ratio_frequency
前提条件
TaurusDB内核版本为2.0.63.250300或以上的版本支持PTRC。内核版本的查询方法请参见如何查看云数据库 TaurusDB实例的版本号。
设置PTRC
可以通过参数开关来控制PTRC特性,也可以使用HINT来实现。
- 通过参数设置
参数名称
级别
描述
optimizer_switch : partial_result_cache
Global,Session
Partial Result Cache功能开关,默认为ON。
取值范围如下:
ON :启用Partial Result Cache功能。
OFF:禁用Partial Result Cache功能。
- 通过Hint设置
用户可以通过Hint(提示)在语句级别更精细地控制PTRC(Partial Result Cache)的使用。语句内的Hint优先级高于optimizer_switch系统变量标志,即使全局或会话开关未开启,通过Hint也可以为特定查询启用PTRC。
- 强制使用PTRC
- 在查询块中使用,强制该查询块中的所有NLJ算子使用PTRC。
/*+ PRC_JOIN() */
- 当嵌套循环连接(NLJ)涉及的所有表(如t1, t2)都包含在Hint列表中时,强制该NLJ算子使用PTRC。
/*+ PRC_JOIN(t1, t2) */
- 在查询块中使用,强制该查询块中的所有NLJ算子使用PTRC。
- 强制不使用PTRC
- 在查询块中使用,强制该查询块中的所有NLJ算子不使用PTRC。
/*+ NO_PRC_JOIN() */
- 当嵌套循环连接(NLJ)涉及的所有表(如t1, t2)都包含在Hint列表中时,则强制该NLJ算子不使用PTRC。
/*+ NO_PRC_JOIN(t1, t2) */
- 在查询块中使用,强制该查询块中的所有NLJ算子不使用PTRC。
使用示例
- 使用如下任意方式开启PTRC优化特性。
- 通过SET命令设置此开关值。
SET optimizer_switch='partial_result_cache=on'; Query OK, 0 rows affected (0.00 sec)
SET optimizer_switch='partial_result_cache=off'; Query OK, 0 rows affected (0.00 sec)
- 通过HINT方式在SQL语句中设置开关值。
EXPLAIN FORMAT=TREE SELECT /*+ set_var(optimizer_switch='partial_result_cache=on') */ t1.i AS a, (SELECT t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1;
EXPLAIN FORMAT=TREE SELECT /*+ set_var(optimizer_switch='partial_result_cache=off') */ t1.i AS a, (SELECT t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1;
- 嵌套循环联接(NLJ)场景通过HINT方式在SQL语句中设置强制使用PTRC。
EXPLAIN FORMAT=TREE SELECT t1.*, (SELECT /*+ PRC_JOIN() */ count(*) FROM (t t2 JOIN t t3) LEFT JOIN (t t4 LEFT JOIN (t t5 JOIN t t6) ON t4.a>t5.a-t1.a) ON t3.a>t2.a-t1.a) FROM t t1;
EXPLAIN FORMAT=TREE SELECT t1.*, (SELECT /*+ PRC_JOIN(t1,t2,t3) */ count(*) FROM (t t2 JOIN t t3) LEFT JOIN (t t4 LEFT JOIN (t t5 JOIN t t6) ON t4.a>t5.a-t1.a) ON t3.a>t2.a-t1.a) FROM t t1;
当前语句中包含了多个NLJ算子。若使用提示/*+ PRC_JOIN() */,所有NLJ算子均会被强制使用PTRC。而若使用/*+ PRC_JOIN(t1,t2,t3) */,则仅作用于包含t1、t2、t3表的NLJ算子,这些算子会强制使用PTRC。
- 嵌套循环联接(NLJ)场景通过HINT方式在SQL语句中设置强制不使用PTRC。
EXPLAIN FORMAT=TREE SELECT t1.*, (SELECT /*+ NO_PRC_JOIN() */ count(*) FROM (t t2 JOIN t t3) LEFT JOIN (t t4 LEFT JOIN (t t5 JOIN t t6) ON t4.a>t5.a-t1.a) ON t3.a>t2.a-t1.a) FROM t t1;
EXPLAIN FORMAT=TREE SELECT t1.*, (SELECT /*+ NO_PRC_JOIN(t1,t2,t3) */ count(*) FROM (t t2 JOIN t t3) LEFT JOIN (t t4 LEFT JOIN (t t5 JOIN t t6) ON t4.a>t5.a-t1.a) ON t3.a>t2.a-t1.a) FROM t t1;
当前语句中包含了多个NLJ算子。当使用提示/*+ NO_PRC_JOIN() */时,所有NLJ算子均被禁止使用PTRC。而若使用/*+ NO_PRC_JOIN(t1,t2,t3) */,则仅作用于包含t1、t2、t3表的NLJ算子,这些算子会被禁止使用PTRC。
- 相关子查询场景通过HINT方式在SQL语句中设置强制使用PTRC。
EXPLAIN FORMAT=TREE SELECT t1.i AS a, (SELECT /*+PRC_SUBQUERY() */ t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1;
- 相关子查询场景通过HINT方式在SQL语句中设置强制不使用PTRC。
EXPLAIN FORMAT=TREE SELECT t1.i AS a, (SELECT /*+NO_PRC_SUBQUERY() */ t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1;
- 通过SET命令设置此开关值。
- 查看控制效果。
通过执行Explain Analyze/Explain format=tree语句可以确认优化是否生效,执行计划出现'Result cache'关键字时,说明优化生效。
具体步骤如下:
- 准备数据
CREATE TABLE t (i INTEGER PRIMARY KEY AUTO_INCREMENT, j INTEGER, KEY(j));
INSERT INTO t VALUES (NULL, NULL); INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t; INSERT INTO t SELECT NULL, NULL FROM t;
ANALYZE TABLE t;
- 关闭特性,执行以下SQL语句,优化器选择默认执行计划。
SET optimizer_switch='partial_result_cache=off'; Query OK, 0 rows affected (0.00 sec)
EXPLAIN FORMAT=TREE SELECT t1.i AS a, (SELECT t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1\G *************************** 1. row *************************** EXPLAIN: -> Index scan on t1 using j (cost=411.60 rows=4096) -> Select #2 (subquery in projection; dependent) -> Limit: 1 row(s) (cost=410.97 rows=1) -> Index lookup on t2 using j (j=t1.j; iterate backwards) (cost=410.97 rows=4096) - 在启用特性后,执行以下 SQL 语句,从执行计划中可以看到出现“Result cache”关键字,说明该优化已经生效。cache keys 部分展示了用于构建缓存键的列:
SET optimizer_switch='partial_result_cache=on'; Query OK, 0 rows affected (0.00 sec)
EXPLAIN FORMAT=TREE SELECT t1.i AS a, (SELECT t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1\G *************************** 1. row *************************** EXPLAIN: -> Index scan on t1 using j (cost=411.60 rows=4096) -> Select #2 (subquery in projection; dependent) -> Result cache : cache keys(t1.j) -> Limit: 1 row(s) (cost=410.97 rows=1) -> Index lookup on t2 using j (j=t1.j; iterate backwards) (cost=410.97 rows=4096)当执行 EXPLAIN ANALYZE 时,Result cache 部分除了显示作为缓存键的列信息之外,还会输出执行阶段的详细统计信息:
EXPLAIN ANALYZE SELECT t1.i AS a, (SELECT t2.i FROM t t2 WHERE t1.j = t2.j ORDER BY j DESC, i DESC LIMIT 1) AS b FROM t t1\G *************************** 1. row *************************** EXPLAIN: -> Index scan on t1 using j (cost=411.60 rows=4096) (actual time=0.026..0.651 rows=4096 loops=1) -> Select #2 (subquery in projection; dependent) -> Result cache : cache keys(t1.j) (Cache Hits: 4095, Cache Misses:1, Cache Evictions: 0, Cache Overflows: 0, Memory Usage: 40960 ) (actual time=0.000..0.000 rows=0 loops=4096) -> Limit: 1 row(s) (cost=410.97 rows=1) (actual time=0.000..0.000 rows=0 loops=4096) -> Index lookup on t2 using j (j=t1.j; iterate backwards) (cost=410.97 rows=4096) (actual time=0.000..0.000 rows=0 loops=4096)表1 Result cache指标含义 指标
含义
Cache Hits
表示缓存命中的次数。
Cache Misses
表示缓存未命中的次数。
Cache Evictions
表示使用LRU淘汰的记录数。
Cache Overflows
表示内存overflow的次数。
Memory Usage
表示当前查询使用的内存量。
- 准备数据
性能测试
我们采用TPCH中的Q17查询对不同数据规模下PTRC启用前后的性能进行了对比测试。结果表明,引入PTRC后,Q17的查询性能得到显著提升。
以SF 100下的Q17执行计划为例,查询语句中包含相关子查询,该子查询使用了PTRC机制,从执行统计中可见有96.7%(计算公式:Cache Hits/(Cache Hits+Cache Misses))的数据成功命中缓存。由于PTRC本质上是一种缓存结构,命中率越高,带来的性能改善越显著。
EXPLAIN ANALYZE SELECT sum(l_extendedprice) / 7.0 AS avg_yearly FROM LINEITEM, PART WHERE p_PARTkey = l_PARTkey AND p_brand = 'Brand#14' AND p_container = 'SM PKG' AND l_quantity < ( SELECT /*+ PRC_SUBQUERY() */ 0.2 * avg(l_quantity) FROM LINEITEM WHERE l_PARTkey = p_PARTkey ) LIMIT 1;
| EXPLAIN |
| -> Limit: 1 row(s) (actual time=13338.942..13338.942 rows=1 loops=1)
-> Aggregate: sum(lineitem.L_EXTENDEDPRICE) (actual time=13338.941..13338.941 rows=1 loops=1)
-> Nested loop inner join (cost=7602572.34 rows=5448536) (actual time=0.369..13331.440 rows=54130 loops=1)
-> Filter: ((part.P_BRAND = 'Brand#14') and (part.P_CONTAINER = 'SM PKG')) (cost=2029081.97 rows=197905) (actual time=0.094..8830.363 rows=20004 loops=1)
-> Table scan on PART (cost=2029081.97 rows=19790487) (actual time=0.023..7105.163 rows=20000000 loops=1)
-> Filter: (lineitem.L_QUANTITY < (select #2)) (cost=25.41 rows=28) (actual time=0.198..0.225 rows=3 loops=20004)
-> Index lookup on LINEITEM using LINEITEM_FK2 (L_PARTKEY=part.P_PARTKEY) (cost=25.41 rows=28) (actual time=0.117..0.124 rows=30 loops=20004)
-> Select #2 (subquery in condition; dependent)
-> Result cache : cache keys(PART.P_PARTKEY) (Cache Hits: 580430, Cache Misses:20004, Cache Evictions: 0, Cache Overflows: 0, Memory Usage: 4218760 ) (actual time=0.003..0.003 rows=1 loops=600434)
-> Aggregate: avg(lineitem.L_QUANTITY) (actual time=0.003..0.003 rows=0 loops=600434)
-> Index lookup on LINEITEM using LINEITEM_FK2 (L_PARTKEY=part.P_PARTKEY) (cost=28.16 rows=28) (actual time=0.002..0.002 rows=1 loops=600434)
|
此外,如果PTRC所关联的算子执行代价较高,则通过PTRC所能实现的性能增益也会相应更大。