向量化最佳实践

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

1 场景

首先,对于不同的场景,向量化执行引擎相比传统的面向行的执行引擎,性能的提升是不同的,对此,你可以通过概述部分有一个基本的认识和理解。

例如,能够完全顺序操作的算子,比如一般地表达式计算、选择算子和聚集算子,能够最大程度得到性能提升。而对于引入了随机性的算子,比如排序算子(Sort)、哈希算子(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 且关闭 matrix.enable_mxv_hash_aggregate,那么强制使用 Group Agg 聚集实现;
  • 开启 matrix.enable_mxv_hash_aggregate 且关闭 matrix.enable_mxv_aggregate,那么强制使用 Hash Agg 聚集实现。

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

3.3.1 MARS2、MARS3 排序键的选择

在 MARS2 和 MARS3 中,数据都是有序存储的。为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键(即排序顺序涉及的字段)。比如设备监控表,可以采用事件时间戳和设备 ID 作为排序键。

MARS2 和 MARS3 的排序键均只能指定一次,不能修改,不能新增,不能删除

不同的是,MARS2 在创建表时:

  • 需要通过创建索引的方式制定排序的顺序。

而 MARS3 在创建表时:

  • 需要通过指定排序列(可以多列)的方式制定排序的顺序。
  • 如果排序键是文本类型,且能接受按照字节顺序排序,那么在这个列采用 COLLATE C 能够加速排序。
  • 指定排序键的 SQL 关键词:ORDER BY

MARS2 可以通过如下 SQL 指定一组排序键:

=# CREATE TABLE t (
    tag int,
    ts timestamp,
    name text,
    region int
   ) USING MARS2;
=# CREATE INDEX ON t USING mars2_btree (ts, tag);

MARS3 可以通过如下 SQL 指定一组排序键:

=# CREATE TABLE t (
    tag int,
    ts timestamp,
    name text,
    region int
    ) USING MARS3
    ORDER BY (ts, tag);

对于 MARS2 和 MARS3 而言,排序键的选择对于查询的性能都至关重要,数据有序不仅能够加速数据的扫描和存储引擎的块筛选,而且对于排序、聚集也有所帮助。

例如,我们指定某表的排序键为 (c1,c2),那么其内部数据按照 (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 MARS2 的 minmax 索引与 MARS3 的 Brin 索引

根据排序键,MARS2、MARS3 中存储的数据都是有序的,一段连续有序的数据称为 Run。Run 的元信息存储了最小最大值,用于在查询时进行过滤。因为是通过元信息进行过滤,不需要加载数据本身,相比于顺序扫描(先访问数据本身,再进行过滤)其 I/O 开销更低,能更快的进行过滤。

  • MARS2 通过建表时启用 minmax 索引的方式实现查询过滤。其建表时不会默认记录最小最大值,需要显式声明。
  • MARS3 则通过额外创建 Brin 索引来实现过滤。

下面分别举例:

对于 MARS2 表,示例 DDL 的所有列上都建立了 minmax 索引(实际工作中可以根据需要建立)。

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

对于 MARS3 表,创建 Brin 索引示例 DDL 如下:

=# CREATE INDEX idx_mars3 ON t USING mars3_brin(ts, tag);

我们想要从上述 MARS2(或 MARS3)表查询今年某个区域的数据:

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

实际上,minmax 索引(Brin 索引)不仅能够使得存储引擎进行块筛选,还能够加速向量化执行引擎的计算。

从存储引擎的角度看,如果一个块(包含某列数据的若干行)的 ts 上最大值小于 2022-01-01,那么存储引擎不再读取这一块,节省 I/O 资源,同时,也减少了引擎的计算量。 向量化执行引擎会利用 minmax (Brin)信息加速计算。比如某块 Region 的最大值和最小值都是 2,那么执行引擎只会在该块应用谓词 ts > '2022-01-01' 过滤出符合条件的数据,而不会进行任何和 Region 有关的运算,提高性能。

minmax 索引(Brin 索引)和排序键是独立设置的,前者可以用于所有列,而后者往往只会选择几列。对于过滤、排序等算子的影响不尽相同,但也有交叉,所以可以综合考虑使用这两者以发挥各自长处。

3.3.3 禁用索引扫描

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

$ SET enable_indexscan TO off;

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

3.4 向量化对接 ORCA 优化器

注意!
目前 ORCA 优化器仅支持直接对接 HEAP,AO 表,不支持 MARS2/3。 与向量化配合使用,仅支持 AOCO 表。

首先,手动开启 ORCA 优化器,optimizer 是会话级别参数,如重进/更换会话需要再次设置才能生效。

=# SET optimizer TO on;

开启 optimizer 参数并正确收集表的统计信息后,向量化即可自动对接 ORCA 优化器生成相应查询计划,例如:

=# EXPLAIN SELECT * FROM t;
                QUERY PLAN
-------------------------------------------
 Result  (cost=0.00..0.00 rows=0 width=20)
   One-Time Filter: false
 Optimizer: Pivotal Optimizer (GPORCA)
(3 rows)

注意!
更多 ORCA 参数请见Greenplum ORCA 优化器配置参数