华为云专家详解 GaussDB(for MySQL) 新特性
本文分享自华为云社区《GaussDB创新特性解读:Partial Result Cache,通过缓存中间结果对算子进行加速》,作者:GaussDB 数据库 。
为了加速查询性能,传统的关系型数据库,比如 Oracle、DB2,都有结果集缓存的特性,用来缓存一条查询语句的结果集。如果后续同样的语句被查询,数据库将直接从结果集缓存中获取结果,而不用再重新执行该查询。MySQL 在 4.0 版本中也引入了结果集缓存 Query cache,但是在设计上有局限性,具体如下:
Query cache 针对单个查询,任何一个表做了修改,如果影响到结果集就需要刷新或者失效。
Query cache 对隔离级别有依赖,不同的隔离级别产生的结果集不一样。
Query cache 需要对所有数据进行缓存,如果表结果比较大的话,缓存需要占据较大的内存或者写入磁盘。
这也导致了该特性在 MySQL 8.0 版本被移除。
鉴于结果集缓存对查询性能的增益,我们在 GaussDB(for MySQL)引入 Partial result cache 这一新特性,简称 PTRC。顾名思义,这也是一个结果集缓存特性。不同于传统的结果集缓存,PTRC 是用来辅助单个查询的内部算子的执行。也就是说 PTRC 粒度更小,是对查询内部的某个算子的中间结果进行缓存,从而起到算子加速的作用。
这里的 Partial 有两层概念:
从这两点可以看出,PTRC 是与单个查询相关的,生命周期从查询开始到查询结束,自动终止。由于它是对算子进行加速,所以一个查询内部可以有多个 PTRC。只要优化器根据代价计算,认为该算子适合 PTRC,那么优化器就会为该算子引入 PTRC。
PTRC 如何确定对算子并加速?
这里我们引入一个新概念:参数化的重复扫描,指的是扫描算子根据参数的不同进行算子扫描。比如 Nested Loop Join,对于外表扫描的每一条数据,内表会根据 JOIN 条件进行扫描,那么对于内表来说就是一次“参数化的重复扫描”。再比如相关子查询,对于父查询的每一次扫描都会根据父查询的结果集调用子查询执行,然后返回子查询的结果集。
PTRC 是如何工作的?
如前所述,PTRC 是缓存算子的中间结果集,那么也和其他 cache 一样,将数据以 key ,value 的方式缓存到 cache 中,通过 key 来命中,得到 value。那么 PTRC 的相关 key 和 value 是如何获取的?
下面我们以 Correlated subquery 为例做简单分析,查询语句如下:
上图是子查询使用 EXISTS 策略执行的流程图。可以看出:对于数据表 t1 中的每一条数据,都会驱动子查询执行,直到数据表 t1 中的所有记录都循环结束。对于数据表 t1 中的每一条记录对应的 t1.a,都需要根据该列值重新扫描子查询,进而判断子查询的返回值。
我们通过 EXPLAIN 来对比引入 PTRC 前后执行计划的差异:
可以看出引入 PTRC 后,多了一个算子 Result cache(标红部分),表明该算子当前的子查询引入了 PTRC,引入后的执行流程变更为:
引入 PTRC 后,对于数据表 t1 中的每一条数据对应的 t1.a 列值,优先查看 PTRC,如果命中,直接从 PTRC 中获取结果集,而不需要执行子查询。如果未命中,需要按原来的方式继续执行子查询,子查询执行的结果会储存到 PTRC 中。如果下一次同样的列值来驱动执行子查询,可以直接从 PTRC 获取。
优化器如何选择 PTRC?
优化器在为算子选择 PTRC 的时候会依赖代价估算,主要是看命中率(命中率 = 不同键值的行数/键值的总行数), 如果命中率大于 rds_partial_result_cache_cost_threshold(具体含义参考下文的系统变量介绍)变量定义的最小代价,PTRC 将会被选择,反之则不会被选择。是否选择了 PTRC,可以通过 Explain format=tree 或者 Explain analyze 来观察实际的命中情况。我们通过一个例子来说明:
从 Result cache 这个算子后面可以看到:
Cache Hits: 0,表示命中的次数为 0
Cache Misses:1,表示没有命中的次数为 1
Cache Evictions: 0,表示使用 LRU 淘汰的记录数
Cache Overflows: 0,表示内存 overflow 的次数
Memory Usage: 40960,表示当前查询使用的内存量
由于优化器使用代价估算来计算是否使用 PTRC,如果估算错误的话,PTRC 还是有额外的代价,比如创建自身的一些数据结构,以及记录的拷贝。为了尽可能的保证查询的性能,PTRC 采取了动态反馈的方式来查看 PTRC 在实际执行的过程中是否继续使用。PTRC 会自动判断命中率是否适合保留 PTRC,优化器根据没有命中的次数,每隔 rds_partial_result_cache_hit_ratio_frequency 会检查命中率是否低于 rds_partial_result_cache_min_hit_ratio。如果低于该值,优化器会自动禁止继续使用 PTRC。
优化器如何限制 PTRC 的内存使用
由于单个查询可以有多个 PTRC 算子,每个算子都会使用内存来存储缓存数据,那么控制 PTRC 内存使用就非常有必要,以防止内存 OOM。
通过系统变量 rds_partial_result_cache_max_mem_size 来定义每个查询所使用的所有 PTRC 算子使用的最大内存。如果 PTRC 使用的内存总数超过该值,优化器会根据 LRU 算法来进行淘汰。如果通过 LRU 算法可以找到适合当前存储记录的大小的记录进行淘汰,当前记录可以进行缓存,否则当前记录将不被缓存。
PTRC 如何配置?
首先,PTRC 默认是开启的,可以通过 Optimizer_switch 中的 partial_result_cache 选项更改设置:设置为 ON,启用 PTRC,否则就关闭。通过下表中的 4 个系统变量,对 PTRC 进行具体设置。
PTRC 性能测试
下面是我们使用 TPCH 的 Q17 来测试不同数据量下启用 PTRC 前后的性能变化。
由于 PTRC 是一个 cache,所以命中率越高性能提升就会越高。当然如果 PTRC 相关的算子执行代价越高的话,那么 PTRC 获取的性能提升也是越高的。
MariaDB 的 subquery cache 是对重复扫描算子相关性子查询进行加速引入的一个特性,我们参照 MariaDB 的 subquery cache 测试样例,同样基于 dbt-3 scale 1 数据集,测试 PTRC 对于 correlated subquery 的加速效果。
由于 MySQL 和 MariaDB 索引创建的不同,执行时间与 MariaDB 不同,这里只需要关注相对时间即可。测试结果如下表所示:
可以看出:上表中最后一行命中率为 0 的情况下,PTRC 默认值检查如果 miss 了 200 条之后,会触发检查命中率,发现命中率太低了,所以 PTRC 自动失效了,所以可以看到 miss 列里只有 200 条。
MariaDB 的测试样例和结果请参考:https://mariadb.com/kb/en/subquery-cache/#performance-impact。
PTRC 对于参数化的重复扫描都可以进行适配,只要命中率足够,就可以加速执行。对于查询中的多种算子包括相关子, Nested Loop Join, Semijoin, Antijoin 都有加速作用。PTRC 已经正式上线,欢迎大家使用。
版权声明: 本文为 InfoQ 作者【华为云开发者联盟】的原创文章。
原文链接:【http://xie.infoq.cn/article/0f34a42aaee8ee8d366b732c1】。文章转载请联系作者。
评论