向量化最佳实践

这一小节会讨论如何高效地使用向量化执行引擎。

1 场景

首先,对于不同的场景,向量化执行引擎相比传统的面向行的执行引擎,性能的提升是不同的,对此,你可以通过概述部分有一个基本的认识和理解。
例如,能够完全顺序操作的算子,比如一般地表达式计算、选择算子和聚集算子,能够最大程度得到性能提升。而对于引入了随机性的算子,比如排序算子、Hash-算子,但是性能提升空间有限。

2 数据类型

2.1 尽可能使用原始类型

你需要尽可能使用原始类型(Primitive type),如 int32, int64 等,来提高向量化的性能提升效率。
一般枚举类型字段往往用于过滤操作,比如国家字段或中国省份字段,可以使用字符串表示,如"China"、"USA",或 "Beijing"、"Xinjiang" 等,但是如果预处理一下,使用 smallint 表示,那么对于过滤操作将会有巨大的性能提升。

2.2 尽可能使用能够覆盖数据值域范围的长度最小的类型

如果数据中的年龄字段用 smallint 表示就足够了,那么就不需要使用 int32 甚至 int64 来表示。 对于传统的执行引擎,不管数值类型是什么,最后结果中的元组(Tuple)中往往是用 Datum 来表示处理,占用 64bits。Datum 可以表示任何 SQL 数据类型的单个值,但其本身不包含任何有关数据类型的信息。所以使用不同类型对于处理速度没有影响。 但是对于向量化执行引擎,数据是连续紧密布局的。数值类型越短,对于 CPU 缓存更友好,SIMD 寄存器也能存储更多数据。所以对某一列进行过滤或者聚集,类型使用 int64 会比 int16 慢四倍。

3 性能分析与调优

3.1 仅用向量化执行引擎

注意!
由于时间和资源受限,我们并没有实现所有数据库标准支持的数据类型、表达式和查询,但为了保证可用性,当遇到无法支持的情况时,我们会部分或者全部使用传统的执行引擎进行运算,现象体现为性能提升不明显或者没有提升。

你可以手动关闭 matrix.enable_mxv_fallback_expression 参数以确定查询是否使用了传统的执行引擎,此参数默认开启。

参数 描述
matrix.enable_mxv_fallback_expression 对于当前向量化执行引擎不能处理的表达式,是否要回退到传统执行引擎执行,默认开启
set matrix.enable_mxv_fallback_expression to off;

当 matrix.enable_mxv_fallback_expression 处于关闭状态且遇到如下错误,那么说明查询中包含当前版本向量化引擎无法处理的数据类型、表达式或者查询。

not implemented yet

3.2 Hash Agg vs. Group Agg

Hash Agg 和 Group Agg 是聚集算子的两种具体实现,有各自的适用场景。Hash Agg 使用哈希表实现聚合,Group Agg 对分组后数据进行聚合。通常情况下,优化器根据统计信息能够针对查询选择合适的算子。但是如果统计信息不准确或者对查询优化不足,可能会选择一个非最优的路径。 这里介绍两个 GUC 帮助分析优化查询。

参数 描述
matrix.enable_mxv_aggregate 用于控制是否开启 Group Agg 功能,默认开启
matrix.enable_mxv_hash_aggregate 用于控制是否开启 Hash Agg 功能,默认开启

两两组合共计四种可能性。

  • 都关闭,使用传统执行引擎的聚集功能;
  • 都开启,由优化器选择最佳的路径;
  • 开启 matrix.enable_mxv_aggregate,那么强制使用 Group Agg 聚集实现;
  • 开启 matrix.enable_mxv_hash_aggregate,那么强制使用 Hash Agg 聚集实现。

3.3 向量化与 MARS2 存储引擎结合

3.3.1 MARS2 排序键的选择

MARS2 中,数据是有序存储的。在创建表时,需要通过创建索引的方式制定排序的顺序。 这个排序顺序涉及的字段,称为排序键。排序键只能指定一次,不能修改,不能删除。为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键。
MARS2 可以通过如下 SQL 指定一组排序键。

CREATE INDEX ON t USING mars2_btree (ci, cj, ck...);

排序键的选择对于查询的性能至关重要:

  • 数据有序能够加速数据的扫描和存储引擎的块筛选。
  • 对于排序、聚集也有所帮助。 比如存储按照 c1, c2 有序。

只涉及排序的查询

SELECT * FROM t ORDER BY c1, c2;
SELECT * FROM t ORDER BY c1, c3;

第一个查询不再需要排序,向量化执行引擎会优化掉排序算子。对于第二个查询,c1 已经有序了,那么向量化执行引擎会对这种情况有特殊优化,仅对相同 c1 的不同 c3 进行排序。

只涉及聚集的查询

SELECT sum(c1), sum(c2) FROM t GROUP BY c1, c2;
SELECT sum(c1), sum(c3) FROM t GROUP BY c1, c3;

如果使用 Group Agg,那么类似于排序,能够大幅提升性能,只需要顺序扫描数据进行聚集操作即可。对于 Hash Agg,数据是否有序对性能几乎没有影响。如前所述,对于顺序算子向量化执行引擎能最大程度发挥其性能,所以针对上面两个查询,尤其是第一个,使用 Group Agg 往往耗时更低。
综上,需要分析和结合业务,根据查询的特征(需要排序、聚集、过滤的列)来选择排序键。

3.3.2 minmax 索引

根据排序键,MARS2 中存储的数据是有序的,一段连续有序的数据称为 Run。 Run 的元信息存储了最小最大值,用于在查询时进行过滤。
因为是通过元信息进行过滤,不需要加载数据本身,相比于顺序扫描(先访问数据本身,再进行过滤)其 I/O 开销更低,能更快的进行过滤。 使用 MARS2 建表时不会默认记录最小最大值,需要显式声明。
minmax 索引不仅能够使得存储引擎进行块筛选,还能够加速向量化执行引擎的计算。下面举例说明其作用: 首先,在 MARS2 表的所有列上都建立了 minmax 索引(实际工作中可以根据需要建立)。

CREATE TABLE fast_filter (
    tag int encoding (minmax),
    ts timestamp encoding (minmax),
    name text encoding (minmax),
    region int encoding (minmax)
    ) USING MARS2;
CREATE INDEX ON fast_filter USING mars2_btree(tag);

我们想要查询今年某个区域的数据

SELECT * FROM fast_filter WHERE ts > '2022-01-01' AND region = 2;

从存储引擎的角度看,如果一个块(包含某列数据的若干行)的 ts 上最大值小于 2022-01-01,那么存储引擎不再读取这一块,节省 I/O 资源,同时,也减少了引擎的计算量。 向量化执行引擎会利用 minmax 信息加速计算。比如某块 Region 的最大值和最小值都是 2,那么执行引擎只会在该块应用谓词 ts > '2022-01-01' 过滤出符合条件的数据,而不会进行任何和 Region 有关的运算,提高性能。
minmax 索引和排序键是独立设置的,前者可以用于所有列,而后者往往只会选择几列。对于过滤、排序等算子的影响不尽相同,但也有交叉,所以可以综合考虑使用这两者以发挥各自长处。

3.3.3 禁用索引扫描

MARS2 提供了索引扫描,但是当前版本的向量化执行引擎只对接了 minmax 索引及顺序扫描,没有和索引扫描对接,那么对于一些查询,虽然开启了向量化执行引擎的开关,但是没有使用向量化执行引擎。计划中会出现 Custom Scan (sortheapscan) 字样。 此时,可以通过禁用索引扫描以使用向量化执行引擎。

set enable_indexscan to off;

MARS2 更多详细信息请见 存储引擎