这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

扩展 Vertica

可以扩展 Vertica 以执行新操作或处理新的数据类型。有几种类型的扩展:

  • 外部过程: 在数据库群集中执行主机上安装的外部脚本或程序。

  • 用户定义的 SQL 函数: 常用 SQL 表达式,可帮助您简化和标准化 SQL 脚本。

  • 存储过程: 存储在数据库中的 SQL 过程(相对于外部过程)。存储过程可以直接与您的数据库进行通信和交互,以执行维护、执行查询和更新表。

  • 用户定义的扩展 (UDx): 用 C++、Python、Java 和 R 编程语言编写的函数或数据加载步骤。当使用 SQL 难以完成或需要很长时间才能完成您要执行的数据处理类型时,这些扩展很有用。 用户定义的扩展 解释了如何使用它们,开发用户定义的扩展 (UDx) 解释了如何创建它们。

1 - 外部过程

仅限企业模式

外部过程是您可以从 Vertica 内部调用的数据库群集中主机上的脚本或可执行程序。外部过程无法将数据传回给 Vertica。

要实施外部过程,请执行下列操作:

  1. 创建外部过程可执行文件。请参阅外部过程要求

  2. 为该文件启用 set-user-ID (SUID)、用户执行和组执行属性。该文件必须可由 dbadmin 读取,或者必须使用 管理工具 install_procedure 命令提供文件所有者的密码。

  3. 安装外部过程可执行文件

  4. 在 Vertica 中创建外部过程

在 Vertica 中创建过程后,可以执行删除该过程,但无法更改该过程。

1.1 - 外部过程要求

仅限企业模式

外部过程对其属性(例如,外部过程的存储位置以及对其输出的处理方式)具有相关要求。您还应了解外部过程的资源使用情况。

过程文件属性

过程文件不能由 root 用户拥有。过程文件必须具有 set-user-ID (SUID)、用户执行和组执行属性集。如果过程文件不可由 Linux 数据库管理员用户读取,则必须在安装该过程时指定所有者的密码。

处理过程输出

Vertica 未提供用于处理过程输出的设施。因此,您必须自行安排以处理过程输出,这些安排应包括直接将错误、日志记录信息和程序信息写入到您管理的文件。

处理资源使用情况

Vertica 资源管理器无法识别由外部过程使用的资源。此外,按设计,Vertica 是在系统上运行的唯一一个主要进程。如果外部过程占用大量资源,可能会影响 Vertica 的性能和稳定性。应考虑所创建的外部过程的类型以及何时运行这些外部过程。例如,您可以在非正常工作时间运行资源密集型过程。

示例过程文件

#!/bin/bash
echo "hello planet argument: $1" >> /tmp/myprocedure.log

1.2 - 安装外部过程可执行文件

仅限企业模式

要安装外部过程,请通过菜单或命令行使用管理工具。

菜单

  1. 运行 管理工具

    $ /opt/vertica/bin/adminTools
    
  2. 在管理工具主菜单 (Main Menu) 上,单击配置菜单 (Configuration Menu),再单击确定 (OK)

  3. 配置菜单 (Configuration Menu) 上,单击安装外部过程 (Install External Procedure),再单击确定 (OK)

  4. 选择要在其上安装外部过程的数据库。

  5. 选择要安装的文件或手动键入完整文件路径,然后单击确定 (OK)

  6. 如果不是超级用户,则会提示您输入密码,然后单击确定 (OK)

    管理工具会自动在数据库中的每个节点上创建 database-name/procedures 目录,并在这些目录中为您安装外部过程。

  7. 单击对话框中的确定 (OK),这表示安装已成功完成。

命令行

如果使用命令行,请务必指定过程文件的完整路径以及拥有该过程文件的 Linux 用户的密码。例如:

$ admintools -t install_procedure -d vmartdb -f /scratch/helloworld.sh -p ownerpassword
Installing external procedure...
External procedure installed

安装外部过程之后,则需要使 Vertica 注意到该过程。要执行此操作,可以使用 CREATE PROCEDURE(外部) 语句,但应先回顾创建外部过程

1.3 - 创建外部过程

仅限企业模式

安装外部过程后,必须使用 CREATE PROCEDURE(外部) 告知 Vertica 这一情况。

只有超级用户可以创建外部过程,默认情况下,只有他们具有执行权限。但是,超级用户可以向用户和角色授予对存储过程的 EXECUTE 权限。

创建过程后,其元数据将存储在系统表 USER_PROCEDURES 中。用户只能查看他们已获得执行权限的过程。

示例

以下示例将为 helloplanet.sh 外部过程文件创建名为 helloplanet 的过程。此文件接受一个 VARCHAR 实参。示例代码在 外部过程要求 中提供。

=> CREATE PROCEDURE helloplanet(arg1 VARCHAR) AS 'helloplanet.sh' LANGUAGE 'external'
   USER 'dbadmin';

下一个示例将为脚本 copy_vertica_database.sh 创建名为 proctest 的过程。此脚本可将数据库从一个群集复制到另一个群集;该脚本包含在服务器 RPM(位于目录 /opt/vertica/scripts 中)中。

=> CREATE PROCEDURE proctest(shosts VARCHAR, thosts VARCHAR, dbdir VARCHAR)
   AS 'copy_vertica_database.sh' LANGUAGE 'external' USER 'dbadmin';

使外部过程过载

您可以创建具有相同名称的多个外部过程,但前提是这些外部过程具有不同的签名,即接受不同的实参集。例如,可以重载 helloplanet 外部过程,以便同时接受整数值:

=> CREATE PROCEDURE helloplanet(arg1 INT) AS 'helloplanet.sh' LANGUAGE 'external'
   USER 'dbadmin';

执行此语句后,数据库编录将存储两个名为 helloplanet 的外部过程,其中一个外部过程接受 VARCHAR 实参,另一个接受整数。当您调用外部过程时,Vertica 会评估过程调用中的实参以确定要调用的过程。

另请参阅

1.4 - 执行外部程序

仅限企业模式

使用 CREATE PROCEDURE(外部) 语句定义过程后,可以将其用作 SELECT 语句中的元命令。Vertica 不支持在更复杂的语句或表达式中使用过程。

以下示例将运行名为 helloplanet 的过程:

=> SELECT helloplanet('earthlings');
 helloplanet
-------------
           0
(1 row)

以下示例将运行名为 proctest 的过程。此过程引用 copy_vertica_database.sh 脚本,而该脚本可将数据库从一个群集复制到另一个群集。该脚本由服务器 RPM 安装在 /opt/vertica/scripts 目录中。

=> SELECT proctest(
    '-s qa01',
    '-t rbench1',
    '-D /scratch_b/qa/PROC_TEST' );

过程在启动节点上执行。Vertica 通过将程序分叉并运行程序来运行该过程。每个过程参数作为字符串传递到可执行文件。父分叉进程会等待直至子进程结束。

如果子进程以状态 0 退出,Vertica 会通过返回一行来报告操作发生,如 helloplanet 示例中所示。如果子进程以任何其他状态退出,Vertica 将报告如下错误:

ERROR 7112: Procedure reported: Procedure execution error: exit status = code

要停止执行,请通过从客户端发送取消命令(例如,按 Ctrl+C)来取消该进程。如果过程程序退出并显示错误,将返回带有退出状态的错误消息。

权限

要执行外部过程,用户需要以下权限:

  • 对于过程的 EXECUTE 权限

  • 对于包含该过程的架构的 USAGE 权限

1.5 - 删除外部过程

仅限企业模式

只有超级用户可以删除外部过程。要从 Vertica 中删除外部过程的定义,请使用 DROP PROCEDURE(外部) 语句。只会移除对过程的引用。外部文件会保留在数据库中每个节点上的 <database>/procedures 目录中。

示例

=> DROP PROCEDURE helloplanet(arg1 varchar);

另请参阅

2 - 用户定义的 SQL 函数

用户定义的 SQL 函数允许您以函数形式定义和存储常用的 SQL 表达式。用户定义的 SQL 函数对于执行复杂的查询和组合 Vertica 内置函数很有用。您只要调用在查询中指定的函数的名称。

只要可以在该位置使用普通 SQL 表达式,您就可以在查询中的任意位置使用用户定义的 SQL 函数,但不能在表分区子句或投影分段子句中使用。

有关用于本节中讨论的命令和系统表的语法与参数,请参阅以下主题:

2.1 - 创建用户定义的 SQL 函数

除了不能在表分区子句或投影分段子句中使用以外,用户定义的 SQL 函数可以在能够使用普通 SQL 表达式的任何查询中使用。

要创建 SQL 函数,用户必须拥有架构的 CREATE 权限。要使用 SQL 函数,用户必须拥有架构的 USAGE 权限和定义的函数的 EXECUTE 权限。

下面的语句可创建名为 myzeroifnull 的 SQL 函数,该函数接受 INTEGER 实参并返回 INTEGER 结果。

=> CREATE FUNCTION myzeroifnull(x INT) RETURN INT
   AS BEGIN
     RETURN (CASE WHEN (x IS NOT NULL) THEN x ELSE 0 END);
   END;

只要使用普通 SQL 表达式,便可以使用新的 SQL 函数 (myzeroifnull)。例如,创建一个简单表:

=> CREATE TABLE tabwnulls(col1 INT);
=> INSERT INTO tabwnulls VALUES(1);
=> INSERT INTO tabwnulls VALUES(NULL);
=> INSERT INTO tabwnulls VALUES(0);
=> SELECT * FROM tabwnulls;
 a
---
 1
 0
(3 rows)

使用 myzeroifnull 函数(在 SELECT 语句中),其中函数从表 tabwnulls 中调用 col1

=> SELECT myzeroifnull(col1) FROM tabwnulls;
 myzeroifnull
--------------
          1
          0
          0
(3 rows)

使用 myzeroifnull 函数(在 GROUP BY 子句中):

=> SELECT COUNT(*) FROM tabwnulls GROUP BY myzeroifnull(col1);
 count
-------
     2
     1
(2 rows)

如果要更改用户定义的 SQL 函数的主体,请使用 CREATE OR REPLACE 语法。以下命令修改了 CASE 表达式:

=> CREATE OR REPLACE FUNCTION myzeroifnull(x INT) RETURN INT
   AS BEGIN
     RETURN (CASE WHEN (x IS NULL) THEN 0 ELSE x END);
   END;

要查看此信息在 Vertica 编录中的存储方式,请参阅查看有关 SQL 函数的信息

另请参阅

2.2 - 更改并删除用户定义的 SQL 函数

Vertica 允许具有不同实参类型的多个函数共用相同名称。因此,如果您在未指定参数数据类型的情况下尝试更改或删除 SQL 函数,系统将返回错误消息,以防止删除不正确的函数:

=> DROP FUNCTION myzeroifnull();
ROLLBACK:  Function with specified name and parameters does not exist: myzeroifnull

更改用户定义的 SQL 函数

使用 ALTER FUNCTION(标量) 命令,可以为用户定义的函数分配新名称并将该函数移到其他架构。

在上一个主题中,您创建了名为 myzeroifnull 的 SQL 函数。以下命令可将 myzeroifnull 函数重命名为 zerowhennull

=> ALTER FUNCTION myzeroifnull(x INT) RENAME TO zerowhennull;
ALTER FUNCTION

以下命令可将已重命名的函数移到名为 macros 的新架构中:

=> ALTER FUNCTION zerowhennull(x INT) SET SCHEMA macros;
ALTER FUNCTION

删除 SQL 函数

DROP FUNCTION 命令可从 Vertica 编录中删除 SQL 函数。

与 ALTER FUNCTION 一样,您必须指定参数数据类型,否则系统会返回以下错误消息:

=> DROP FUNCTION zerowhennull();
ROLLBACK:  Function with specified name and parameters does not exist: zerowhennull

指定参数类型:

=> DROP FUNCTION macros.zerowhennull(x INT);
DROP FUNCTION

Vertica 不会检查依赖项,因此,如果删除其他对象(例如视图或其他 SQL 函数)所引用的 SQL 函数,Vertica 会在使用这些对象时(而非删除该函数时)返回错误。

另请参阅

2.3 - 管理对 SQL 函数的访问

用户必须拥有架构的 USAGE 权限和定义的函数的 EXECUTE 权限才能执行用户定义的 SQL 函数。只有超级用户或所有者可以授予/撤销函数的 EXECUTE 权限。

要向用户 Fred 授予 myzeroifnull 函数的 EXECUTE 权限,请执行下列操作:

=> GRANT EXECUTE ON FUNCTION myzeroifnull (x INT) TO Fred;

要从用户 Fred 撤销 myzeroifnull 函数的 EXECUTE 权限,请执行下列操作:

=> REVOKE EXECUTE ON FUNCTION myzeroifnull (x INT) FROM Fred;

另请参阅

2.4 - 查看有关用户定义的 SQL 函数的信息

只要拥有 EXECUTE 权限,您就可以访问有关用户定义的 SQL 函数的信息。可以从系统表 USER_FUNCTIONS 访问此信息,也可以通过 vsql 元命令 \df 访问此信息。

要查看您拥有 EXECUTE 权限的所有用户定义的 SQL 函数,请查询 USER_FUNCTIONS:

=> SELECT * FROM USER_FUNCTIONS;
-[ RECORD 1 ]----------+---------------------------------------------------
schema_name            | public
function_name          | myzeroifnull
function_return_type   | Integer
function_argument_type | x Integer
function_definition    | RETURN CASE WHEN (x IS NOT NULL) THEN x ELSE 0 END
volatility             | immutable
is_strict              | f

如果要更改用户定义的 SQL 函数的主体,请使用 CREATE OR REPLACE 语法。以下命令修改了 CASE 表达式:

=> CREATE OR REPLACE FUNCTION myzeroifnull(x INT) RETURN INT
   AS BEGIN
     RETURN (CASE WHEN (x IS NULL) THEN 0 ELSE x END);
   END;

现在,当您查询 USER_FUNCTIONS 时,可在 function_definition 列中查看所做更改:

=> SELECT * FROM USER_FUNCTIONS;
-[ RECORD 1 ]----------+---------------------------------------------------
schema_name            | public
function_name          | myzeroifnull
function_return_type   | Integer
function_argument_type | x Integer
function_definition    | RETURN CASE WHEN (x IS NULL) THEN 0 ELSE x END
volatility             | immutable
is_strict              | f

如果使用 CREATE OR REPLACE 语法以便仅更改参数名称或参数类型(或两者),系统会维护函数的两个版本。例如,以下命令将指示函数为 myzeroifnull 函数接受并返回数字数据类型而非整数。

=> CREATE OR REPLACE FUNCTION myzeroifnull(z NUMERIC) RETURN NUMERIC
   AS BEGIN
     RETURN (CASE WHEN (z IS NULL) THEN 0 ELSE z END);
   END;

现在,当您查询 USER_FUNCTIONS 表时,可在 Record 2 中查看 myzeroifnull 的第二个实例,还可以在 function_return_typefunction_argument_typefunction_definition 列中查看所做更改。

=> SELECT * FROM USER_FUNCTIONS;
-[ RECORD 1 ]----------+------------------------------------------------------------
schema_name            | public
function_name          | myzeroifnull
function_return_type   | Integer
function_argument_type | x Integer
function_definition    | RETURN CASE WHEN (x IS NULL) THEN 0 ELSE x END
volatility             | immutable
is_strict              | f
-[ RECORD 2 ]----------+------------------------------------------------------------
schema_name            | public
function_name          | myzeroifnull
function_return_type   | Numeric
function_argument_type | z Numeric
function_definition    | RETURN (CASE WHEN (z IS NULL) THEN (0) ELSE z END)::numeric
volatility             | immutable
is_strict              | f

由于 Vertica 允许具有不同实参类型的函数共用相同名称,因此您必须在更改删除函数时指定实参类型。否则,系统将返回错误消息:

=> DROP FUNCTION myzeroifnull();
ROLLBACK:  Function with specified name and parameters does not exist: myzeroifnull

2.5 - 迁移内置 SQL 函数

如果您有来自其他 RDBMS 的内置 SQL 函数,并且这些函数未映射到 Vertica 支持的函数,您可以使用用户定义的 SQL 函数将这些函数迁移到 Vertica 数据库中。

下面的示例脚本显示了如何为以下 DB2 内置函数创建用户定义的函数:

  • UCASE()

  • LCASE()

  • LOCATE()

  • POSSTR()

UCASE()

该脚本为 UCASE() 函数创建了一个用户定义的 SQL 函数:

=> CREATE OR REPLACE FUNCTION UCASE (x VARCHAR)
   RETURN VARCHAR
   AS BEGIN
   RETURN UPPER(x);
   END;

LCASE()

该脚本为 LCASE() 函数创建了一个用户定义的 SQL 函数:

=> CREATE OR REPLACE FUNCTION LCASE (x VARCHAR)
   RETURN VARCHAR
   AS BEGIN
   RETURN LOWER(x);
   END;

LOCATE()

该脚本为 LOCATE() 函数创建了一个用户定义的 SQL 函数:

=> CREATE OR REPLACE FUNCTION LOCATE(a VARCHAR, b VARCHAR)
   RETURN INT
   AS BEGIN
   RETURN POSITION(a IN b);
   END;

POSSTR()

该脚本为 POSSTR() 函数创建了一个用户定义的 SQL 函数:

=> CREATE OR REPLACE FUNCTION POSSTR(a VARCHAR, b VARCHAR)
   RETURN INT
   AS BEGIN
   RETURN POSITION(b IN a);
   END;

3 - 存储过程

您可以将复杂的数据库任务和例程压缩为存储过程。与外部过程不同,存储过程在数据库内部存在且可以从数据库内部执行;这使它们可以直接与您的数据库进行通信和交互,以执行维护、执行查询和更新表。

最佳实践

许多其他数据库针对专重于频繁事务的在线事务处理 (OLTP) 进行了优化。相比之下,Vertica 针对在线分析处理 (OLAP) 进行了优化,该处理专注于存储和分析大量数据,并对针对该数据的最复杂查询提供最快的响应。

这种架构差异意味着 Vertica 中存储过程的推荐用例和最佳实践与其他数据库中的存储过程略有不同。

虽然面向 OLTP 的数据库中的存储过程通常用于执行小型事务,但应使用 Vertica 等面向 OLAP 的数据库中的存储过程来增强分析工作负载。Vertica 可以处理孤立的事务,但频繁的小事务可能会影响性能。

一些推荐的 Vertica 存储过程用例包括信息生命周期管理 (ILM) 活动(例如提取、转换和加载 (ETL))以及用于机器学习等任务的数据准备。例如:

  • 根据生命期交换分区

  • 在生命期结束时导出数据并删除分区

  • 保存机器学习模型的输入、输出和元数据 — 运行模型的人员、模型的版本、模型运行次数以及接收结果的人员

Vertica 中的存储过程还可以对需要比调用者更高权限的对象进行操作。可选参数允许过程使用定义者的权限运行,允许调用者以受控方式执行敏感操作。

已知问题和解决方法

  • 您不能使用 PERFORM CREATE FUNCTION 创建 SQL 宏。

    解决方法
    使用 EXECUTE 在存储过程中创建 SQL 宏

    CREATE PROCEDURE procedure_name()
    LANGUAGE PLvSQL AS $$
    BEGIN
        EXECUTE 'macro';
    end;
    $$;
    

    其中 macro 是 SQL 宏的创建语句。例如,此过程创建 argmax 宏:

    => CREATE PROCEDURE make_argmax() LANGUAGE PLvSQL AS $$
    BEGIN
        EXECUTE
            'CREATE FUNCTION
            argmax(x int) RETURN int AS
            BEGIN
                RETURN (CASE WHEN (x IS NOT NULL) THEN x ELSE 0 END);
            END';
    END;
    $$;
    
  • 嵌入式 SQL 语句中的非错误异常不会被报告。

  • DECIMAL、NUMERIC、NUMBER、MONEY 和 UUID 数据类型尚不能用于实参。

  • 游标应该在声明时捕获变量上下文,但它们目前在打开时捕获变量上下文。

  • 对具有键约束的表的 DML 查询还不能返回值。

    解决方法
    不要使用以下语句:

    DO $$
    DECLARE
        y int;
    BEGIN
        y := UPDATE tbl WHERE col1 = 3 SET col2 = 4;
    END;
    $$
    

    而应使用 SELECT 检查 DML 查询结果:

    DO $$
    DECLARE
        y int;
    BEGIN
        y := SELECT COUNT(*) FROM tbl WHERE col1 = 3;
        PERFORM UPDATE tbl SET col2 = 4 WHERE col1 = 3;
    END;
    $$;
    

3.1 - PL/vSQL

PL/vSQL 是一种功能强大且富有表现力的过程语言,用于创建可重用过程、操作数据和简化其他复杂的数据库例程。

Vertica PL/vSQL 在很大程度上与 PostgreSQL PL/pgSQL 兼容,语义差异很小。有关将 PostgreSQL PL/pgSQL 存储过程迁移到 Vertica 的详细信息,请参阅 PL/pgSQL 到 PL/vSQL 迁移指南

有关 PL/vSQL 用法的实际示例,请参阅存储过程:用例和示例

3.1.1 - 支持的类型

Vertica PL/vSQL 支持非复杂数据类型。以下类型仅作为变量受支持,而不能作为实参:

  • DECIMAL

  • NUMERIC

  • NUMBER

  • MONEY

  • UUID

3.1.2 - 范围和结构

PL/vSQL 使用块范围,其中块具有以下结构:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
    ...
END [ label ];

声明

DECLARE 块中的变量 declarations 结构如下:

variable_name [ CONSTANT ] data_type [ NOT NULL ] [:= { expression | statement } ];

别名

别名是同一变量的备选名称。变量的别名不是副本,对任一引用的任何更改都会影响同一基础变量。

new_name ALIAS FOR variable;

在下方示例中,标识符 y 现在是变量 x 的别名,对 y 的更改反映在 x 中。

DO $$
DECLARE
    x int := 3;
    y ALIAS FOR x;
BEGIN
    y := 5; -- since y refers to x, x = 5
    RAISE INFO 'x = %, y = %', x, y;
END;
$$;

INFO 2005:  x = 5, y = 5

BEGIN 和嵌套块

BEGIN 包含 statementsstatement 定义为 PL/vSQL 的行或块。

内部块中声明的变量遮蔽外部块中声明的变量。要明确指定特定块中的变量,您可以使用 label(不区分大小写)命名块,然后使用以下语句引用该块中声明的变量:

label.variable_name

例如,由于存在遮蔽,在内部块中指定变量 x 会隐式引用 inner_block.x 而不引用 outer_block.x


<<outer_block>>
DECLARE
    x int;
BEGIN
    <<inner_block>>
    DECLARE
        x int;
    BEGIN
        x := 1000; -- implicitly specifies x in inner_block because of shadowing
        OUTER_BLOCK.x := 0; -- specifies x in outer_block; labels are case-insensitive
    END inner_block;
END outer_block;

NULL 语句

NULL 语句不起任何作用。它可以用作占位符语句,或者是一种显示代码块有意为空的方式。例如:

DO $$
BEGIN
    NULL;
END;
$$

备注

注释语法如下。您不能嵌套注释。

-- single-line comment

/* multi-line
comment
*/

3.1.3 - 嵌入式 SQL

您可以在存储过程中嵌入和执行 SQL 语句表达式

赋值

要保存表达式的值或返回的值,您可以将其赋值给变量:

variable_name := expression;
variable_name := statement;

例如,此过程将 3 赋值给 i,将 'message' 赋值给 v

=> CREATE PROCEDURE performless_assignment() LANGUAGE PLvSQL AS $$
DECLARE
    i int;
    v varchar;
BEGIN
    i := SELECT 3;
    v := 'message';
END;
$$;

如果查询未返回任何行或者返回多个行,则此类赋值失败。要返回多个行,请使用 LIMIT截断赋值

=> SELECT * FROM t1;
 b
---
 t
 f
 f
(3 rows)


=> CREATE PROCEDURE more_than_one_row() LANGUAGE PLvSQL as $$
DECLARE
    x boolean;
BEGIN
    x := SELECT * FROM t1;
END;
$$;
CREATE PROCEDURE

=> CALL more_than_one_row();
ERROR 10332:  Query returned multiple rows where 1 was expected

截断赋值

截断赋值将查询返回的第一行存储在变量中。行排序是不确定的,除非您指定了 ORDER BY 子句

variable_name <- expression;
variable_name <- statement;

以下过程获取指定的查询所返回结果的第一行,并将其赋值给 x

=> CREATE PROCEDURE truncating_assignment() LANGUAGE PLvSQL AS $$
DECLARE
    x boolean;
BEGIN
    x <- SELECT * FROM t1 ORDER BY b DESC; -- x is now assigned the first row returned by the SELECT query
END;
$$;

PERFORM

PERFORM 关键字运行 SQL 语句表达式,放弃返回的结果。

PERFORM statement;
PERFORM expression;

例如,此过程将一个值插入表。INSERT 返回插入的行数,因此您必须将其与 PERFORM 配对。

=> DO $$
BEGIN
    PERFORM INSERT INTO coordinates VALUES(1,2,3);
END;
$$;

EXECUTE

EXECUTE 允许您在执行期间动态构造 SQL 查询:

EXECUTE command_expression [ USING expression [, ... ] ];

command_expression 是一个 SQL 表达式,它可以将 PL/vSQL 变量和求值引向字符串字面量。该字符串字面量作为 SQL 语句执行,$1、$2、... 替换为对应的表达式

使用 PL/vSQL 变量构造查询可能有危险,会将您的系统暴露给 SQL 注入,因此请使用 QUOTE_IDENTQUOTE_LITERALQUOTE_NULLABLE 对其进行包装。

以下过程使用 WHERE 子句来构造查询:

DO $$
BEGIN
    EXECUTE 'SELECT * FROM t1 WHERE x = $1' USING 10; -- becomes WHERE x = 10
END;
$$;

以下过程使用 usernamepassword 实参中的密码创建用户。由于构造的 CREATE USER 语句使用变量,请使用函数 QUOTE_IDENT 和 QUOTE_LITERAL,用 || 连接它们。

=> CREATE PROCEDURE create_user(username varchar, password varchar) LANGUAGE PLvSQL AS $$
BEGIN
    EXECUTE 'CREATE USER ' || QUOTE_IDENT(username) || ' IDENTIFIED BY ' || QUOTE_LITERAL(password);
END;
$$;

EXECUTE 是 SQL 语句,因此您可以将其赋值给变量或与 PERFORM 配对:

variable_name:= EXECUTE command_expression;
PERFORM EXECUTE command_expression;

FOUND(特殊变量)

特殊布尔变量 FOUND 初始化为 false,然后根据是否发生以下操作确定是赋值为 true 还是 false:

  • 语句(不是表达式)返回具有非零行数的结果,或者

  • FOR 循环至少迭代一次

您可以使用 FOUND 区分返回 NULL 和返回 0 行。

在过程实参的作用域与其定义的最外层块之间存在特殊变量。这意味着:

  • 特殊变量影射过程实参

  • 在存储过程主体中声明的变量将影射特殊变量

下面的过程演示 FOUND 如何更改。在 SELECT 语句之前,FOUND 为 false;在 SELECT 语句之后,FOUND 为 true。

=> DO $$
BEGIN
    RAISE NOTICE 'Before SELECT, FOUND = %', FOUND;
    PERFORM SELECT 1; -- SELECT returns 1
    RAISE NOTICE 'After SELECT, FOUND = %', FOUND;
END;
$$;

NOTICE 2005:  Before SELECT, FOUND = f
NOTICE 2005:  After SELECT, FOUND = t

同样,UPDATE、DELETE 和 INSERT 返回受影响的行数。在下一示例中,UPDATE 不更改任何行,但是返回 0 值以表示没有任何行受影响,因此 FOUND 设置为 true:

=> SELECT * t1;
  a  |  b
-----+-----
 100 | abc
(1 row)

DO $$
BEGIN
    PERFORM UPDATE t1 SET a=200 WHERE b='efg'; -- no rows affected since b doesn't contain 'efg'
    RAISE INFO 'FOUND = %', FOUND;
END;
$$;

INFO 2005:  FOUND = t

FOUND 开始时设置为 false,如果循环至少迭代一次,则设为 true:

=> DO $$
BEGIN
    RAISE NOTICE 'FOUND = %', FOUND;
    FOR i IN RANGE 1..1 LOOP -- RANGE is inclusive, so iterates once
        RAISE NOTICE 'i = %', i;
    END LOOP;
    RAISE NOTICE 'FOUND = %', FOUND;
END;
$$;

NOTICE 2005:  FOUND = f
NOTICE 2005:  FOUND = t

DO $$
BEGIN
    RAISE NOTICE 'FOUND = %', FOUND;
    FOR i IN RANGE 1..0 LOOP
        RAISE NOTICE 'i = %', i;
    END LOOP;
    RAISE NOTICE 'FOUND = %', FOUND;
END;
$$;

NOTICE 2005:  FOUND = f
NOTICE 2005:  FOUND = f

3.1.4 - 控制流

控制流构造使您可以控制语句块应运行的次数和条件。

条件

IF/ELSIF/ELSE

IF/ELSIF/ELSE 语句允许您根据指定的条件执行不同的操作。

IF condition_1 THEN
  statement_1;
[ ELSIF condition_2 THEN
  statement_2 ]
...
[ ELSE
  statement_n; ]
END IF;

Vertica 将每个条件作为布尔值依次求值,直到找到一个为 true 的条件,然后执行语句块并退出 IF 语句。如果任何条件均不为 true,则执行 ELSE 块(如果存在)。


IF i = 3 THEN...
ELSIF 0 THEN...
ELSIF true THEN...
ELSIF x <= 4 OR x >= 10 THEN...
ELSIF y = 'this' AND z = 'THAT' THEN...

例如,此过程演示简单的 IF...ELSE 分支。由于 b 声明为 true,Vertica 执行第一个分支。

=> DO LANGUAGE PLvSQL $$
DECLARE
    b bool := true;
BEGIN
    IF b THEN
        RAISE NOTICE 'true branch';
    ELSE
        RAISE NOTICE 'false branch';
    END IF;
END;
$$;

NOTICE 2005:  true branch

CASE

CASE 表达式通常比 IF…ELSE 链更可读。在执行 CASE 表达式的分支之后,控制将跳转到封闭 END CASE 后的语句。

PL/vSQL CASE 表达式比 SQL Case 表达式更灵活、更强大,但后者更高效;在均可使用的情况下,您应该更偏好 SQL Case 表达式。

CASE [ search_expression ]
   WHEN expression_1 [, expression_2, ...] THEN
      when_statements
  [ ... ]
  [ ELSE
      else_statements ]
END CASE;

search_expression 求值一次,然后从上到下与每个分支中的 expression_n 进行比较。如果 search_expression 与给定 expression_n 相等,则 Vertica 针对 expression_n 执行 WHEN 块,并退出 CASE 块。如果找不到匹配的表达式,则执行 ELSE 分支(如果存在)。

Case 表达式必须具有匹配的 Case 或 ELSE 分支,否则 Vertica 将引发 Case_NOT_FOUND 错误。

如果忽略 search_expression,则其值默认为 true

例如,此过程进行游戏 FizzBuzz。如果实参可被 3 整除,则输出 Fizz;如果实参可被 5 整除,则输出 Buzz;如果实参可被 3 和 5 整除,则输出 FizzBuzz。

=> CREATE PROCEDURE fizzbuzz(IN x int) LANGUAGE PLvSQL AS $$
DECLARE
    fizz int := x % 3;
    buzz int := x % 5;
BEGIN
    CASE fizz
        WHEN 0 THEN -- if fizz = 0, execute WHEN block
            CASE buzz
                WHEN 0 THEN -- if buzz = 0, execute WHEN block
                    RAISE INFO 'FizzBuzz';
                ELSE -- if buzz != 0, execute WHEN block
                    RAISE INFO 'Fizz';
            END CASE;
        ELSE -- if fizz != 0, execute ELSE block
            CASE buzz
                WHEN 0 THEN
                    RAISE INFO 'Buzz';
                ELSE
                    RAISE INFO '';
            END CASE;
    END CASE;
END;
$$;

=> CALL fizzbuzz(3);
INFO 2005:  Fizz

=> CALL fizzbuzz(5);
INFO 2005:  Buzz

=> CALL fizzbuzz(15);
INFO 2005:  FizzBuzz

循环

循环重复执行代码块,直到满足给定条件。

WHILE

WHILE 循环检查给定条件,如果条件为 true,则执行循环体,然后再次检查条件:如果为 true,循环体再次执行;如果为 false,则控制跳到循环体的末尾。

[ <<label>> ]
WHILE condition LOOP
   statements;
END LOOP;

例如,此过程计算实参的阶乘:

=> CREATE PROCEDURE factorialSP(input int) LANGUAGE PLvSQL AS $$
DECLARE
    i int := 1;
    output int := 1;
BEGIN
    WHILE i <= input loop
        output := output * i;
        i := i + 1;
    END LOOP;
    RAISE INFO '%! = %', input, output;
END;
$$;

=> CALL factorialSP(5);
INFO 2005:  5! = 120

LOOP

此循环类型等同于 WHILE true,仅在遇到 RETURN 或 EXIT 语句或引发异常时终止。

[ <<label>> ]
LOOP
   statements;
END LOOP;

例如,此过程输出从 counterupper_bound(含)的整数:

DO $$
DECLARE
    counter int := 1;
    upper_bound int := 3;
BEGIN
    LOOP
        RAISE INFO '%', counter;
        IF counter >= upper_bound THEN
            RETURN;
        END IF;
        counter := counter + 1;
    END LOOP;
END;
$$;

INFO 2005:  1
INFO 2005:  2
INFO 2005:  3

FOR

FOR 循环在集合上迭代,集合可以是整体范围、查询或游标。

如果 FOR 循环至少迭代一次,则在循环结束后,特殊的 FOUND 变量设置为 true。否则,FOUND 设置为 false。

FOUND 变量可用于区分返回 NULL 和返回 0 行,或者在 LOOP 未运行时创建 IF 分支。

FOR (RANGE)

FOR (RANGE) 循环在表达式 leftright 指定的整数范围内迭代。

[ <<label>> ]
FOR loop_counter IN RANGE [ REVERSE ] left..right [ BY step ] LOOP
    statements
END LOOP [ label ];

loop_counter

  • 不必声明,它使用 left 值进行初始化

  • 仅在 FOR 循环范围内可用

loop_counterleft 迭代到 right(包含),在每次迭代结尾以 step 递增。

相反,REVERSE 选项从 right 迭代到 left(包含),以 step 递减。

例如,下面是一个标准的递增 FOR 循环,其 step = 1:

=> DO $$
BEGIN
    FOR i IN RANGE 1..4 LOOP -- loop_counter i does not have to be declared
        RAISE NOTICE 'i = %', i;
    END LOOP;
    RAISE NOTICE 'after loop: i = %', i; -- fails
END;
$$;

NOTICE 2005:  i = 1
NOTICE 2005:  i = 2
NOTICE 2005:  i = 3
NOTICE 2005:  i = 4
ERROR 2624:  Column "i" does not exist -- loop_counter i is only available inside the FOR loop

在下方示例中,loop_counteri 从 4 开始,在每次迭代结尾以 2 递减:

=> DO $$
BEGIN
    FOR i IN RANGE REVERSE 4..0 BY 2 LOOP
        RAISE NOTICE 'i = %', i;
    END LOOP;
END;
$$;

NOTICE 2005:  i = 4
NOTICE 2005:  i = 2
NOTICE 2005:  i = 0

FOR (query)

FOR (QUERY) 循环在查询结果上进行迭代。

[ <<label>> ]
FOR target IN QUERY statement LOOP
    statements
END LOOP [ label ];

您可以在查询中包含 ORDER BY 子句,以使排序具有确定性。

与 FOR (RANGE) 循环不同,您必须声明 target 变量。这些变量的值在循环结束后保持不变。

例如,假设给定表 tuple

=> SELECT * FROM tuples ORDER BY x ASC;
 x | y | z
---+---+---
 1 | 2 | 3
 4 | 5 | 6
 7 | 8 | 9
(3 rows)

此过程检索每行中的元组,并将它们存储在变量 abc 中,在每次迭代后输出它们:

=>
=> DO $$
DECLARE
    a int; -- target variables must be declared
    b int;
    c int;
    i int := 1;
BEGIN
    FOR a,b,c IN QUERY SELECT * FROM tuples ORDER BY x ASC LOOP
        RAISE NOTICE 'iteration %: a = %, b = %, c = %', i,a,b,c;
        i := i + 1;
    END LOOP;
    RAISE NOTICE 'after loop: a = %, b = %, c = %', a,b,c;
END;
$$;

NOTICE 2005:  iteration 1: a = 1, b = 2, c = 3
NOTICE 2005:  iteration 2: a = 4, b = 5, c = 6
NOTICE 2005:  iteration 3: a = 7, b = 8, c = 9
NOTICE 2005:  after loop: a = 7, b = 8, c = 9

您还可以使用通过 EXECUTE 动态构建的查询:

[ <<label>> ]
FOR target IN EXECUTE 'statement' [ USING expression [, ... ] ] LOOP
    statements
END LOOP [ label ];

下面的过程使用 EXECUTE 构建 FOR (QUERY) 循环,并将 SELECT 语句的结果存储在变量 xy 中。此类语句的结果集只有一行,因此它只迭代一次。

=> SELECT 'first string', 'second string';
   ?column?   |   ?column?
--------------+---------------
 first string | second string
(1 row)

=> DO $$
DECLARE
    x varchar; -- target variables must be declared
    y varchar;
BEGIN
    -- substitute the placeholders $1 and $2 with the strings
    FOR x, y IN EXECUTE 'SELECT $1, $2' USING 'first string', 'second string' LOOP
        RAISE NOTICE '%', x;
        RAISE NOTICE '%', y;
    END LOOP;
END;
$$;

NOTICE 2005:  first string
NOTICE 2005:  second string

FOR (cursor)

FOR (CURSOR) 循环在绑定的、未打开的游标上进行迭代,为每次迭代执行一组 statements

[ <<label>> ]
FOR loop_variable [, ...] IN CURSOR bound_unopened_cursor [ ( [ arg_name := ] arg_value [, ...] ) ] LOOP
    statements
END LOOP [ label ];

这种类型的 FOR 循环在循环开始时打开游标,在循环结束时关闭游标。

例如,此过程创建游标 c。该过程将 6 作为实参传递给游标,因此游标仅检索 y 坐标为 6 的行,将坐标存储在变量 x_y_z_ 中,并在每次迭代结束时输出它们:

=> SELECT * FROM coordinates;
 x  | y | z
----+---+----
 14 | 6 | 19
  1 | 6 |  2
 10 | 6 | 39
 10 | 2 | 1
  7 | 1 | 10
 67 | 1 | 77
(6 rows)

DO $$
DECLARE
    c CURSOR (key int) FOR SELECT * FROM coordinates WHERE y=key;
    x_ int;
    y_ int;
    z_ int;
BEGIN
    FOR x_,y_,z_ IN CURSOR c(6) LOOP
       RAISE NOTICE 'cursor returned %,%,% FOUND=%', x_,y_,z_,FOUND;
    END LOOP;
    RAISE NOTICE 'after loop: %,%,% FOUND=%', x_,y_,z_,FOUND;
END;
$$;

NOTICE 2005:  cursor returned 14,6,19 FOUND=f -- FOUND is only set after the loop ends
NOTICE 2005:  cursor returned 1,6,2 FOUND=f
NOTICE 2005:  after loop: 10,6,39 FOUND=t -- x_, y_, and z_ retain their values, FOUND is now true because the FOR loop iterated at least once

操作循环

RETURN

您可以使用 RETURN 退出整个过程(因此退出循环)。RETURN 是可选语句,可以添加它来向读取器发出过程结束的信号。

RETURN;

EXIT

与其他编程语言中的 break 或带标签 break 类似,EXIT 语句允许您提早退出循环,可以选择指定:

  • loop_label:退出的循环的名称

  • condition:如果 conditiontrue,则执行 EXIT 语句

EXIT [ loop_label ] [ WHEN condition ];

CONTINUE

CONTINUE 跳到循环的下一次迭代,而不执行 CONTINUE 本身之后的语句。您可以指定具有 loop_label 的特定循环:

CONTINUE [loop_label] [ WHEN condition ];

例如,此过程在其前两次迭代期间不输出,因为 CONTINUE 语句将在控制到达 RAISE NOTICE 语句之前执行并移动到循环的下一次迭代:

=> DO $$
BEGIN
    FOR i IN RANGE 1..5 LOOP
        IF i < 3 THEN
            CONTINUE;
        END IF;
        RAISE NOTICE 'i = %', i;
    END LOOP;
END;
$$;

NOTICE 2005:  i = 3
NOTICE 2005:  i = 4
NOTICE 2005:  i = 5

3.1.5 - 错误和诊断

ASSERT

ASSERT 是一个调试功能,它可以检查某个条件是否为 true。如果条件为 false,则 ASSERT 引发 ASSERT_FAILURE 异常,并显示可选错误消息。

要转义 '(单引号)字符,请使用 ''。同样,要转义 "(双引号)字符,请使用 ""

ASSERT condition [ , message ];

例如,此过程检查 products 表中的行数,并使用 ASSERT 检查是否填充了该表。如果该表为空,则 Vertica 引发错误:


=> CREATE TABLE products(id UUID, name VARCHARE, price MONEY);
CREATE TABLE

=> SELECT * FROM products;
 id | name | price
----+------+-------
(0 rows)

DO $$
DECLARE
    prod_count INT;
BEGIN
    prod_count := SELECT count(*) FROM products;
    ASSERT prod_count > 0, 'products table is empty';
END;
$$;

ERROR 2005:  products table is empty

要让 Vertica 停止检查 ASSERT 语句,您可以设置布尔会话级别参数PLpgSQLCheckAsserts

RAISE

RAISE 可以引发错误或输出用户指定的下列错误消息之一:

RAISE [ level ] 'format' [, arg_expression [, ... ]] [ USING option = expression [, ... ] ];
RAISE [ level ] condition_name [ USING option = expression [, ... ] ];
RAISE [ level ] SQLSTATE 'sql-state' [ USING option = expression [, ... ] ];
RAISE [ level ] USING option = expression [, ... ];

此过程演示各种 RAISE 级别:

=> DO $$
DECLARE
    logfile varchar := 'vertica.log';
BEGIN
    RAISE LOG 'this message was sent to %', logfile;
    RAISE INFO 'info';
    RAISE NOTICE 'notice';
    RAISE WARNING 'warning';
    RAISE EXCEPTION 'exception';

    RAISE NOTICE 'exception changes control flow; this is not printed';
END;
$$;

INFO 2005:  info
NOTICE 2005:  notice
WARNING 2005:  warning
ERROR 2005:  exception

$ grep 'this message was sent to vertica.log' v_vmart_node0001_catalog/vertica.log
<LOG> @v_vmart_node0001: V0002/2005: this message is sent to vertica.log

异常

EXCEPTION 块使您可以捕获和处理从 statements 引发的异常

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN exception_condition [ OR exception_condition ... ] THEN
        handler_statements
    [ WHEN exception_condition [ OR exception_condition ... ] THEN
        handler_statements
      ... ]
END [ label ];

exception_condition 具有下列格式之一:

WHEN errcode_division_by_zero THEN ...
WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN ...
WHEN OTHERS THEN ...

OTHERS 是特殊条件,它捕获除 QUERY_CANCELLEDASSERT_FAILUREFEATURE_NOT_SUPPORTED 之外的所有异常。

当引发异常时,Vertica 检查异常列表,从上到下查找匹配的 exception_condition。如果找到匹配,它将执行 handler_statements,然后离开异常块范围。

如果 Vertica 找不到匹配,则会将异常传播到下一个封闭块。您可以使用 RAISE 在异常处理程序中手动执行此操作:

RAISE;

例如,以下过程在 inner_block 中用 3 除以 0,这是一个非法操作,将引发 division_by_zero 异常(SQL 状态 22012)。Vertica 检查内部 EXCEPTION 块以查找匹配的条件:

  1. 第一个条件检查 SQL 状态 42501,因此 Vertica 移动到下一个条件。

  2. WHEN OTHERS THEN 捕获所有异常,因此它执行该块。

  3. 随后,基本 RAISE 将异常传播到 outer_block

  4. 外部 EXCEPTION 块成功捕获异常并输出消息。

=> DO $$
<<outer_block>>
BEGIN
    <<inner_block>>
    DECLARE
        x int;
    BEGIN
        x := 3 / 0; -- throws exception division_by_zero, SQLSTATE 22012
    EXCEPTION -- this block is checked first for matching exceptions
        WHEN SQLSTATE '42501' THEN
            RAISE NOTICE 'caught insufficient_privilege exception';
        WHEN OTHERS THEN -- catches all exceptions
            RAISE; -- manually propagate the exception to the next enclosing block
    END inner_block;
EXCEPTION -- exception is propagated to this block
    WHEN division_by_zero THEN
        RAISE NOTICE 'caught division_by_zero exception';
END outer_block;
$$;

NOTICE 2005:  caught division_by_zero exception

SQLSTATE 和 SQLERRM 变量

当处理异常时,您可以使用以下变量检索错误信息:

  • SQLSTATE 包含 SQL 状态

  • SQLERRM 包含错误消息

有关详细信息,请参阅SQL 状态列表

此过程通过尝试将 NULL 赋值给 NOT NULL 变量捕获引发的异常,并输出 SQL 状态和错误消息:

DO $$
DECLARE
    i int NOT NULL := 1;
BEGIN
    i := NULL; -- illegal, i was declared with NOT NULL
EXCEPTION
    WHEN OTHERS THEN
        RAISE WARNING 'SQL State: %', SQLSTATE;
        RAISE WARNING 'Error message: %', SQLERRM;
END;
$$;

WARNING 2005:  SQLSTATE: 42809
WARNING 2005:  SQLERRM: Cannot assign null into NOT NULL variable

检索异常信息

您可以使用 GET STACKED DIAGNOSTICS 在异常处理程序中检索有关异常的信息:

GET STACKED DIAGNOSTICS variable_name { = | := } item [, ... ];

其中 item 可以是以下任意一项:

例如,此过程有一个 EXCEPTION 块,它捕捉 division_by_zero 错误并输出 SQL 状态、错误消息和异常上下文:

=> DO $$
DECLARE
    message_1 varchar;
    message_2 varchar;
    message_3 varchar;
    x int;
BEGIN
    x := 5 / 0;
EXCEPTION
    WHEN OTHERS THEN -- OTHERS catches all exceptions
    GET STACKED DIAGNOSTICS message_1 = RETURNED_SQLSTATE,
                            message_2 = MESSAGE_TEXT,
                            message_3 = EXCEPTION_CONTEXT;

    RAISE INFO 'SQLSTATE: %', message_1;
    RAISE INFO 'MESSAGE: %', message_2;
    RAISE INFO 'EXCEPTION_CONTEXT: %', message_3;
END;
$$;

INFO 2005:  SQLSTATE: 22012
INFO 2005:  MESSAGE: Division by zero
INFO 2005:  EXCEPTION_CONTEXT: PL/vSQL procedure inline_code_block line 8 at static SQL

3.1.6 - 游标

游标是对查询结果集的引用,允许您一次查看一行结果。游标记住结果集中的位置,可以是以下位置之一:

  • 结果行

  • 第一行之前

  • 最后一行之后

您还可以使用 FOR 循环在未打开的绑定游标上进行迭代。有关详细信息,请参阅控制流

声明游标

绑定游标

要将游标绑定到声明中的语句,请使用 FOR 关键字:

cursor_name CURSOR [ ( arg_name arg_type [, ...] ) ] FOR statement;

游标的实参使您能够更好地控制要处理的行。例如,假设您有下表:

=> SELECT * FROM coordinates_xy;
 x | y
---+----
 1 |  2
 9 |  5
 7 | 13
...
(100000 rows)

如果仅对 y 为 6 的行感兴趣,则可以声明以下游标,然后在打开游标时提供实参 6

c CURSOR (key int) FOR SELECT * FROM coordinates_xy WHERE y=key;

未绑定的游标

要声明未绑定到特定查询的游标,请使用 refcursor 类型:

cursor_name refcursor;

您可以随时使用打开对未绑定的游标进行绑定。

例如,要声明游标 my_unbound_cursor

my_unbound_cursor refcursor;

打开和关闭游标

打开

打开游标将使用给定实参执行查询,并将游标放在结果集的第一行之前。查询结果的排序(因此结果集的开头)是不确定的,除非您指定了 ORDER BY 子句

打开绑定的游标

打开声明期间绑定的游标:

OPEN bound_cursor [ ( [ arg_name := ] arg_value [, ...] ) ];

例如,给定以下声明:

c CURSOR (key int) FOR SELECT * FROM t1 WHERE y=key;

可以使用以下某项操作打开游标:

OPEN c(5);
OPEN c(key := 5);

关闭

当游标离开范围时,打开的游标将自动关闭,但您可以使用“关闭”命令提前关闭游标。关闭的游标可以稍后重新打开,这将重新执行查询并准备新的结果集。

CLOSE cursor;

打开未绑定的游标

对未绑定的游标进行绑定,然后打开游标:

OPEN unbound_cursor FOR statement;

您还可以使用 EXECUTE,因为它是语句:

OPEN unbound_cursor FOR EXECUTE statement_string [ USING expression [, ... ] ];

例如,将游标 c 绑定到表 product_data 的查询:

OPEN c for SELECT * FROM product_data;

提取行

FETCH 语句:

  1. 检索指定游标当前指向的行,并将其存储在某个变量中。

  2. 使游标前进到下一个位置。

variable [, ...] := FETCH opened_cursor;

检索的值存储在变量中。行通常有多个值,因此您可以每行使用一个变量。

如果 FETCH 成功检索值,则特殊变量 FOUND 设置为 true。否则,如果您在游标经过结果集的最后一行时调用 FETCH,它将返回 NULL,且特殊变量 FOUND 设置为 false

下面的过程创建游标 c,将其绑定到 coordinates 表中的 SELECT 查询。该过程将实参 1 传递给游标,因此游标仅检索 y 坐标为 1 的行,将坐标存储在变量 x_y_z_ 中。

只有两行的 y 坐标为 1,因此使用 FETCH 两次后,第三个 FETCH 开始返回 NULL 值,且 FOUND 设置为 false

=> SELECT * FROM coordinates;
 x  | y | z
----+---+----
 14 | 6 | 19
  1 | 6 |  2
 10 | 6 | 39
 10 | 2 |  1
  7 | 1 | 10
 67 | 1 | 77
(6 rows)

DO $$
DECLARE
    c CURSOR (key int) FOR SELECT * FROM coordinates WHERE y=key;
    x_ int;
    y_ int;
    z_ int;
BEGIN
    OPEN c(1); -- only retrieve rows where y=1
    x_,y_,z_ := FETCH c;
    RAISE NOTICE 'cursor returned %, %, %, FOUND=%',x_, y_, z_, FOUND;
    x_,y_,z_ := FETCH c; -- fetches the last set of results and moves to the end of the result set
    RAISE NOTICE 'cursor returned %, %, %, FOUND=%',x_, y_, z_, FOUND;
    x_,y_,z_ := FETCH c; -- cursor has advanced past the final row
    RAISE NOTICE 'cursor returned %, %, %, FOUND=%',x_, y_, z_, FOUND;
END;
$$;

NOTICE 2005:  cursor returned 7, 1, 10, FOUND=t
NOTICE 2005:  cursor returned 67, 1, 77, FOUND=t
NOTICE 2005:  cursor returned <NULL>, <NULL>, <NULL>, FOUND=f

移动游标

MOVE 使打开的游标前进到下一个位置,而不检索该行。如果游标位置(在 MOVE 之前)未经过最后一行,则特殊 FOUND 变量设置为 true — 即,如果调用 FETCH 而不调用 MOVE,则会检索该行。

MOVE bound_cursor;

例如,此游标仅检索 y 坐标为 2 的行。结果集只有一行,因此使用 MOVE 两次会导致前进超过第一行(和最后一行),且将 FOUND 设置为 false:

 => SELECT * FROM coordinates WHERE y=2;
 x  | y | z
----+---+---
 10 | 2 | 1
(1 row)

DO $$
DECLARE
    c CURSOR (key int) FOR SELECT * FROM coordinates WHERE y=key;
BEGIN
    OPEN c(2); -- only retrieve rows where y=2, cursor starts before the first row
    MOVE c; -- cursor advances to the first (and last) row
    RAISE NOTICE 'FOUND=%', FOUND; -- FOUND is true because the cursor points to a row in the result set
    MOVE c; -- cursor advances past the final row
    RAISE NOTICE 'FOUND=%', FOUND; -- FOUND is false because the cursor is past the final row
END;
$$;

NOTICE 2005:  FOUND=t
NOTICE 2005:  FOUND=f

3.1.7 - PL/pgSQL 到 PL/vSQL 迁移指南

虽然 Vertica PL/vSQL 在很大程度上与 PostgreSQL PL/pgSQL 兼容,但从 PostgreSQL PL/pgSQL 迁移时,存在一些易于解决的语义和 SQL 级别差异。

语言级别差异

下面列出了 Vertica PL/vSQL 与 PostgreSQL PL/pgSQL 之间的显著差异。在 Vertica PL/vSQL 中:

  • 必须将 PERFORM 语句用于未返回任何值的 SQL 语句。

  • UPDATE/DELETE WHERE CURRENT OF 不受支持。

  • FOR 循环具有附加关键字:

    • FOR (RANGE) 循环RANGE 关键字

    • FOR (QUERY) 循环:QUERY 关键字

    • FOR (CURSOR) 循环:CURSOR 关键字

  • 默认情况下,NULL 不能强制转换为 FALSE。

解决方法:将 NULL 强制转换为 FALSE

与 PostgreSQL PL/pgSQL 不同,在 Vertica PL/vSQL 中,NULL 不可强制转换为 false。当赋值 NULL 时,预期获得布尔值的表达式会引发异常:

=> DO $$
BEGIN
    IF NULL THEN -- boolean value expected for IF
    END IF;
END;
$$;

ERROR 10268:  Query returned null where a value was expected

要使 NULL 强制转换为 false,请启用配置参数 PLvSQLCoerceNull:

=> ALTER DATABASE DEFAULT SET PLvSQLCoerceNull = 1;

计划的功能

在将来版本中,计划支持以下功能:

  • 完整事务和会话语义。目前,存储过程在执行之前提交事务,每个嵌入式 SQL 语句在其自己的自动提交事务中执行。它具有以下含义: * 您不能回退。 * 会话级别的更改(如创建定向查询或设置会话级别参数)将成功,但是没有作用。

  • OUT/INOUT 参数模式

  • FOREACH (ARRAY) 循环

  • 使用以下类型作为实参: * DECIMAL * NUMERIC * NUMBER * MONEY * UUID

  • 非正向移动游标

  • 用于诊断的 CONTEXT/EXCEPTION_CONTEXT。

  • 特殊变量 ROW_COUNT。

SQL 级别差异

下面说明了 Vertica 和 PostgreSQL 之间在架构和 SQL 级别上的显著差异。在 Vertica 中:

  • 一些数据类型大小不同 — 例如,Vertica 中的标准 INTEGER 为 8 字节,但 PostgreSQL 中为 4 字节。

  • INSERT、UPDATE 和 DELETE 返回受影响的行数。

  • 某些 SQLSTATE 代码不同,这会影响异常处理

3.2 - 形参模式

存储过程支持 IN 形参。OUT 和 INOUT 形参当前不受支持。

如果未指定,则形参的模式默认为 IN。

IN

IN 形参指定实参的名称和类型。这些形参确定过程的签名。当调用过载过程时,Vertica 运行其签名与调用中传递的实参类型匹配的过程。

例如,此过程的调用者必须传入 INT 和 VARCHAR 值。xy 都是 IN 形参:

=> CREATE PROCEDURE raiseXY(IN x INT, y VARCHAR) LANGUAGE PLvSQL AS $$
BEGIN
    RAISE NOTICE 'x = %', x;
    RAISE NOTICE 'y = %', y;
    -- some processing statements
END;
$$;

CALL raiseXY(3, 'some string');
NOTICE 2005:  x = 3
NOTICE 2005:  y = some string

有关 RAISE NOTICE 的详细信息,请参阅错误和诊断

3.3 - 执行存储过程

如果您对存储过程有 EXECUTE 权限,则可以使用指定存储过程及其 IN 实参的 CALL 语句来执行存储过程。

语法

CALL stored_procedure_name();

例如,存储过程 raiseXY() 定义为:

=> CREATE PROCEDURE raiseXY(IN x INT, y VARCHAR) LANGUAGE PLvSQL AS $$
BEGIN
    RAISE NOTICE 'x = %', x;
    RAISE NOTICE 'y = %', y;
    -- some processing statements
END;
$$;

CALL raiseXY(3, 'some string');
NOTICE 2005:  x = 3
NOTICE 2005:  y = some string

有关 RAISE NOTICE 的详细信息,请参阅错误和诊断

您可以使用 DO 执行匿名(未命名)过程。这不需要权限:

=> DO $$
BEGIN
    RAISE NOTICE '% ran an anonymous procedure', current_user();
END;
$$;

NOTICE 2005:  Bob ran an anonymous procedure

限制运行时

可以使用会话参数 RUNTIMECAP 来设置过程的最大运行时。

此示例将会话期间所有存储过程的运行时设置为一秒,并使用无限循环运行匿名过程。Vertica 在过程运行超过一秒后终止过程:

=> SET SESSION RUNTIMECAP '1 SECOND';

=> DO $$
BEGIN
    LOOP
    END LOOP;
END;
$$;

ERROR 0:  Query exceeded maximum runtime
HINT:  Change the maximum runtime using SET SESSION RUNTIMECAP

执行安全性和权限

默认情况下,存储过程以调用者的权限执行,因此,调用者必须对存储过程访问的编录对象具有必要的权限。您可以通过指定 SECURITY 选项的 DEFINER,来允许调用者使用定义者的权限、默认角色用户参数、和用户属性 (RESOURCE_POOL、MEMORY_CAP_KB、TEMP_SPACE_CAP_KB、RUNTIMECAP)。

例如,以下过程将一个值插入表 s1.t1。如果定义者具有所需的权限(对于架构,为 USAGE;对于表,为 INSERT),则此要求不适用于调用者。

=> CREATE PROCEDURE insert_into_s1_t1(IN x int, IN y int)
LANGUAGE PLvSQL
SECURITY DEFINER AS $$
BEGIN
    PERFORM INSERT INTO s1.t1 VALUES(x,y);
END;
$$;

使用 SECURITY DEFINER 的过程以该用户身份有效地执行过程,因此,对数据库的更改似乎是由过程的定义者(而不是其调用者)执行的。

示例

在此示例中,此表:

records(i INT, updated_date TIMESTAMP DEFAULT sysdate, updated_by VARCHAR(128) DEFAULT current_user())

包含以下内容:

=> SELECT * FROM records;
 i |        updated_date        | updated_by
---+----------------------------+------------
 1 | 2021-08-27 15:54:05.709044 | Bob
 2 | 2021-08-27 15:54:07.051154 | Bob
 3 | 2021-08-27 15:54:08.301704 | Bob
(3 rows)

Bob 创建一个过程来更新表,然后使用 SECURITY DEFINER 选项并将过程上的 EXECUTE 授予 Alice。Alice 现在可以使用该过程来更新表,而无需任何额外权限:

=> GRANT EXECUTE ON PROCEDURE update_records(int,int) to Alice;
GRANT PRIVILEGE

=> \c - Alice
You are now connected as user "Alice".

=> CALL update_records(99,1);
 update_records
---------------
             0
(1 row)

由于对 update_records() 的调用以 Bob 身份有效地运行过程,所以 Bob(而不是 Alice)被列为表的更新者:

=> SELECT * FROM records;
 i  |        updated_date        | updated_by
----+----------------------------+------------
 99 | 2021-08-27 15:55:42.936404 | Bob
  2 | 2021-08-27 15:54:07.051154 | Bob
  3 | 2021-08-27 15:54:08.301704 | Bob
(3 rows)

3.4 - 更改存储过程

您可以使用 ALTER PROCEDURE 更改存储过程并保留其授权。

示例

下面的示例使用以下过程:

=> CREATE PROCEDURE echo_integer(IN x int) LANGUAGE PLvSQL AS $$
BEGIN
    RAISE INFO 'x is %', x;
END;
$$;

默认情况下,存储过程以调用者的权限执行,因此,调用者必须对存储过程访问的编录对象具有必要的权限。您可以通过指定 SECURITY 选项的 DEFINER,来允许调用者使用定义者的权限、默认角色用户参数、和用户属性 (RESOURCE_POOL、MEMORY_CAP_KB、TEMP_SPACE_CAP_KB、RUNTIMECAP)。

使用以下对象的权限执行过程...

  • 定义者(所有者):

    => ALTER PROCEDURE echo_integer(int) SECURITY DEFINER;
    
  • 调用者:

    => ALTER PROCEDURE echo_integer(int) SECURITY INVOKER;
    

更改过程的源代码:

=> ALTER PROCEDURE echo_integer(int) SOURCE TO $$
    BEGIN
        RAISE INFO 'the integer is: %', x;
    END;
$$;

更改过程的所有者(定义者):

=> ALTER PROCEDURE echo_integer(int) OWNER TO u1;

更改过程的架构:

=> ALTER PROCEDURE echo_integer(int) SET SCHEMA s1;

重命名过程:

=> ALTER PROCEDURE echo_integer(int) RENAME TO echo_int;

3.5 - 存储过程:用例和示例

Vertica 中的存储过程最适合复杂的分析工作流,而不适合小型、事务繁重的工作流。一些推荐的用例包括信息生命周期管理 (ILM) 活动(例如提取、转换和加载 (ETL))以及更复杂的分析任务(如机器学习)的数据准备。例如:

  • 根据生命期交换分区

  • 在生命期结束时导出数据并删除分区

  • 保存机器学习模型的输入、输出和元数据(例如运行模型的人员、模型的版本、模型运行次数以及接收结果的人员等)以进行审核

搜索值

find_my_value() 过程在给定架构的任何表列中搜索用户指定的值,并将该值实例的位置存储在用户指定的表中:

=> CREATE PROCEDURE find_my_value(p_table_schema VARCHAR(128), p_search_value VARCHAR(1000), p_results_schema VARCHAR(128), p_results_table VARCHAR(128)) AS $$
DECLARE
    sql_cmd VARCHAR(65000);
    sql_cmd_result VARCHAR(65000);
    results VARCHAR(65000);
BEGIN
    IF p_table_schema IS NULL OR p_table_schema = '' OR
        p_search_value IS NULL OR p_search_value = '' OR
        p_results_schema IS NULL OR p_results_schema = '' OR
        p_results_table IS NULL OR p_results_table = '' THEN
        RAISE EXCEPTION 'Please provide a schema to search, a search value, a results table schema, and a results table name.';
        RETURN;
    END IF;

    sql_cmd := 'CREATE TABLE IF NOT EXISTS ' || QUOTE_IDENT(p_results_schema) || '.' || QUOTE_IDENT(p_results_table) ||
        '(found_timestamp TIMESTAMP, found_value VARCHAR(1000), table_name VARCHAR(128), column_name VARCHAR(128));';

    sql_cmd_result := EXECUTE 'SELECT LISTAGG(c USING PARAMETERS max_length=1000000, separator='' '')
        FROM (SELECT ''
        (SELECT '''''' || NOW() || ''''''::TIMESTAMP , ''''' || QUOTE_IDENT(p_search_value) || ''''','''''' || table_name || '''''', '''''' || column_name || ''''''
            FROM '' || table_schema || ''.'' || table_name || ''
            WHERE '' || column_name || ''::'' ||
            CASE
                WHEN data_type_id IN (17, 115, 116, 117) THEN data_type
                ELSE ''VARCHAR('' || LENGTH(''' || QUOTE_IDENT(p_search_value)|| ''') || '')'' END || '' = ''''' || QUOTE_IDENT(p_search_value) || ''''''' || DECODE(LEAD(column_name) OVER(ORDER BY table_schema, table_name, ordinal_position), NULL, '' LIMIT 1);'', '' LIMIT 1)

        UNION ALL '') c
            FROM (SELECT table_schema, table_name, column_name, ordinal_position, data_type_id, data_type
            FROM columns WHERE NOT is_system_table AND table_schema ILIKE ''' || QUOTE_IDENT(p_table_schema) || ''' AND data_type_id < 1000
            ORDER BY table_schema, table_name, ordinal_position) foo) foo;';

    results := EXECUTE 'INSERT INTO ' || QUOTE_IDENT(p_results_schema) || '.' || QUOTE_IDENT(p_results_table) || ' ' || sql_cmd_result;

    RAISE INFO 'Matches Found: %', results;
END;
$$;

例如,在 public 架构中搜索字符串 'dog' 的实例,然后将结果存储在 public.table_list 中:

=> CALL find_my_value('public', 'dog', 'public', 'table_list');
 find_my_value
---------------
             0
(1 row)

=> SELECT * FROM public.table_list;
      found_timestamp       | found_value |  table_name   | column_name
----------------------------+-------------+---------------+-------------
 2021-08-25 22:13:20.147889 | dog         | another_table | b
 2021-08-25 22:13:20.147889 | dog         | some_table    | c
(2 rows)

优化表

您可以使用 create_optimized_table() 过程自动从 Parquet 文件加载数据并优化查询。此过程:

  1. 创建一个外部表,其结构是使用 Vertica INFER_TABLE_DDL 函数从 Parquet 文件构建的。

  2. 创建一个原生 Vertica 表,就像外部表一样,将所有 VARCHAR 列的大小调整为要加载的数据的 MAX 长度。

  3. 使用可选分段/按作为参数传入的列排序创建超投影。

  4. 向作为参数传入的原生表添加可选主键。

  5. 将外部表中的示例数据集(100 万行)加载到原生表中。

  6. 删除外部表。

  7. 在原生表上运行 ANALYZE_STATISTICS 函数。

  8. 运行 DESIGNER_DESIGN_PROJECTION_ENCODINGS 函数以获取原生表的正确编码的超投影。

  9. 截断现在优化的原生表(我们将在单独的脚本/存储过程中加载整个数据集)。


=> CREATE OR REPLACE PROCEDURE create_optimized_table(p_file_path VARCHAR(1000), p_table_schema VARCHAR(128), p_table_name VARCHAR(128), p_seg_columns VARCHAR(1000), p_pk_columns VARCHAR(1000)) LANGUAGE PLvSQL AS $$
DECLARE
    command_sql VARCHAR(1000);
    seg_columns VARCHAR(1000);
    BEGIN

-- First 3 parms are required.
-- Segmented and PK columns names, if present, must be Unquoted Identifiers
    IF p_file_path IS NULL OR p_file_path = '' THEN
        RAISE EXCEPTION 'Please provide a file path.';
    ELSEIF p_table_schema IS NULL OR p_table_schema = '' THEN
        RAISE EXCEPTION 'Please provide a table schema.';
    ELSEIF p_table_name IS NULL OR p_table_name = '' THEN
        RAISE EXCEPTION 'Please provide a table name.';
    END IF;

-- Pass optional segmented columns parameter as null or empty string if not used
    IF p_seg_columns IS NULL OR p_seg_columns = '' THEN
        seg_columns := '';
    ELSE
        seg_columns := 'ORDER BY ' || p_seg_columns || ' SEGMENTED BY HASH(' || p_seg_columns || ') ALL NODES';
    END IF;

-- Add '_external' to end of p_table_name for the external table and drop it if it already exists
    EXECUTE 'DROP TABLE IF EXISTS ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || '_external CASCADE;';

-- Execute INFER_TABLE_DDL to generate CREATE EXTERNAL TABLE from the Parquet files
    command_sql := EXECUTE 'SELECT infer_table_ddl(' || QUOTE_LITERAL(p_file_path) || ' USING PARAMETERS format = ''parquet'', table_schema = ''' || QUOTE_IDENT(p_table_schema) || ''', table_name = ''' || QUOTE_IDENT(p_table_name) || '_external'', table_type = ''external'');';

-- Run the CREATE EXTERNAL TABLE DDL
    EXECUTE command_sql;

-- Generate the Internal/ROS Table DDL and generate column lengths based on maximum column lengths found in external table
    command_sql := EXECUTE 'SELECT LISTAGG(y USING PARAMETERS separator='' '')
        FROM ((SELECT 0 x, ''SELECT ''''CREATE TABLE ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || '('' y
            UNION ALL SELECT ordinal_position, column_name || '' '' ||
            CASE WHEN data_type LIKE ''varchar%''
                THEN ''varchar('''' || (SELECT MAX(LENGTH('' || column_name || ''))
                    FROM '' || table_schema || ''.'' || table_name || '') || '''')'' ELSE data_type END || NVL2(LEAD('' || column_name || '', 1) OVER (ORDER BY ordinal_position), '','', '')'')
                    FROM columns WHERE table_schema = ''' || QUOTE_IDENT(p_table_schema) || ''' AND table_name = ''' || QUOTE_IDENT(p_table_name) || '_external''
                    UNION ALL SELECT 10000, ''' || seg_columns || ''' UNION ALL SELECT 10001, '';'''''') ORDER BY x) foo WHERE y <> '''';';
    command_sql := EXECUTE command_sql;
    EXECUTE command_sql;

-- Alter the Internal/ROS Table if primary key columns were passed as a parameter
    IF p_pk_columns IS NOT NULL AND p_pk_columns <> '' THEN
        EXECUTE 'ALTER TABLE ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || ' ADD CONSTRAINT ' || QUOTE_IDENT(p_table_name) || '_pk PRIMARY KEY (' || p_pk_columns || ') ENABLED;';
    END IF;

-- Insert 1M rows into the Internal/ROS Table, analyze stats, and generate encodings
    EXECUTE 'INSERT INTO ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || ' SELECT * FROM ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || '_external LIMIT 1000000;';

    EXECUTE 'SELECT analyze_statistics(''' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || ''');';

    EXECUTE 'SELECT designer_design_projection_encodings(''' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || ''', ''/tmp/toss.sql'', TRUE, TRUE);';


-- Truncate the Internal/ROS Table and you are now ready to load all rows
-- Drop the external table

    EXECUTE 'TRUNCATE TABLE ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || ';';

    EXECUTE 'DROP TABLE IF EXISTS ' || QUOTE_IDENT(p_table_schema) || '.' || QUOTE_IDENT(p_table_name) || '_external CASCADE;';

  END;

  $$;
=> call create_optimized_table('/home/dbadmin/parquet_example/*','public','parquet_table','c1,c2','c1');

create_optimized_table
------------------------
                      0
(1 row)

=> select export_objects('', 'public.parquet_table');
       export_objects
------------------------------------------
CREATE TABLE public.parquet_table
(
    c1 int NOT NULL,
    c2 varchar(36),
    c3 date,
    CONSTRAINT parquet_table_pk PRIMARY KEY (c1) ENABLED
);


CREATE PROJECTION public.parquet_table_super /*+createtype(D)*/
(
c1 ENCODING COMMONDELTA_COMP,
c2 ENCODING ZSTD_FAST_COMP,
c3 ENCODING COMMONDELTA_COMP
)
AS
SELECT parquet_table.c1,
        parquet_table.c2,
        parquet_table.c3
FROM public.parquet_table
ORDER BY parquet_table.c1,
          parquet_table.c2
SEGMENTED BY hash(parquet_table.c1, parquet_table.c2) ALL NODES OFFSET 0;

SELECT MARK_DESIGN_KSAFE(0);

(1 row)

动态透视表

存储过程 unpivot() 将源表和目标表作为输入。它取消透视源表并将其输出到目标表中。

此示例使用下表:

=> SELECT * FROM make_the_columns_into_rows;
c1  | c2  |                  c3                  |             c4             |    c5    | c6
-----+-----+--------------------------------------+----------------------------+----------+----
123 | ABC | cf470c5b-50e3-492a-8483-b9e4f20d195a | 2021-08-24 18:49:40.835802 |  1.72964 | t
567 | EFG | 25ea7636-d924-4b4f-81b5-1e1c884b06e3 | 2021-08-04 18:49:40.835802 | 41.46100 | f
890 | XYZ | f588935a-35a4-4275-9e7f-ebb3986390e3 | 2021-08-29 19:53:39.465778 |  8.58207 | t
(3 rows)

此表包含以下列:

=> \d make_the_columns_into_rows
                                               List of Fields by Tables
Schema |           Table            | Column |     Type      | Size | Default | Not Null | Primary Key | Foreign Key
--------+----------------------------+--------+---------------+------+---------+----------+-------------+-------------
public | make_the_columns_into_rows | c1     | int           |    8 |         | f        | f           |
public | make_the_columns_into_rows | c2     | varchar(80)   |   80 |         | f        | f           |
public | make_the_columns_into_rows | c3     | uuid          |   16 |         | f        | f           |
public | make_the_columns_into_rows | c4     | timestamp     |    8 |         | f        | f           |
public | make_the_columns_into_rows | c5     | numeric(10,5) |    8 |         | f        | f           |
public | make_the_columns_into_rows | c6     | boolean       |    1 |         | f        | f           |
(6 rows)

目标表具有来自源表的列,这些列作为键/值对转换为行。它还有一个 ROWID 列,用于将键/值对绑定回源表中的原始行:

=> CREATE PROCEDURE unpivot(p_source_table_schema VARCHAR(128), p_source_table_name VARCHAR(128), p_target_table_schema VARCHAR(128), p_target_table_name VARCHAR(128)) AS $$
DECLARE
    explode_command VARCHAR(10000);
BEGIN
    explode_command := EXECUTE 'SELECT ''explode(string_to_array(''''['''' || '' || LISTAGG(''NVL('' || column_name || ''::VARCHAR, '''''''')'' USING PARAMETERS separator='' || '''','''' || '') || '' || '''']'''')) OVER (PARTITION BY rn)'' explode_command FROM (SELECT table_schema, table_name, column_name, ordinal_position FROM columns ORDER BY table_schema, table_name, ordinal_position LIMIT 10000000) foo WHERE table_schema = ''' || QUOTE_IDENT(p_source_table_schema) || ''' AND table_name = ''' || QUOTE_IDENT(p_source_table_name) || ''';';

    EXECUTE 'CREATE TABLE ' || QUOTE_IDENT(p_target_table_schema) || '.' || QUOTE_IDENT(p_target_table_name) || '
        AS SELECT rn rowid, column_name key, value FROM (SELECT (ordinal_position - 1) op, column_name
            FROM columns WHERE table_schema = ''' || QUOTE_IDENT(p_source_table_schema) || '''
            AND table_name = ''' || QUOTE_IDENT(p_source_table_name) || ''') a
            JOIN (SELECT rn, ' || explode_command || '
            FROM (SELECT ROW_NUMBER() OVER() rn, *
            FROM ' || QUOTE_IDENT(p_source_table_schema) || '.' || QUOTE_IDENT(p_source_table_name) || ') foo) b ON b.position = a.op';
END;
$$;

调用过程:

=> CALL unpivot('public', 'make_the_columns_into_rows', 'public', 'columns_into_rows');
 unpivot
---------
       0
(1 row)

=> SELECT * FROM columns_into_rows ORDER BY rowid, key;
rowid | key |                value
-------+-----+--------------------------------------
     1 | c1  | 123
     1 | c2  | ABC
     1 | c3  | cf470c5b-50e3-492a-8483-b9e4f20d195a
     1 | c4  | 2021-08-24 18:49:40.835802
     1 | c5  | 1.72964
     1 | c6  | t
     2 | c1  | 890
     2 | c2  | XYZ
     2 | c3  | f588935a-35a4-4275-9e7f-ebb3986390e3
     2 | c4  | 2021-08-29 19:53:39.465778
     2 | c5  | 8.58207
     2 | c6  | t
     3 | c1  | 567
     3 | c2  | EFG
     3 | c3  | 25ea7636-d924-4b4f-81b5-1e1c884b06e3
     3 | c4  | 2021-08-04 18:49:40.835802
     3 | c5  | 41.46100
     3 | c6  | f
(18 rows)

unpivot() 过程也可以处理源表中的新列。

将新列 z 添加到源表,然后使用相同的过程取消透视表:

=> ALTER TABLE make_the_columns_into_rows ADD COLUMN z VARCHAR;
ALTER TABLE

=> UPDATE make_the_columns_into_rows SET z = 'ZZZ' WHERE c1 IN (123, 890);
OUTPUT
--------
      2
(1 row)

=> CALL unpivot('public', 'make_the_columns_into_rows', 'public', 'columns_into_rows');
unpivot
---------
       0
(1 row)

=> SELECT * FROM columns_into_rows;
rowid | key |                value
-------+-----+--------------------------------------
     1 | c1  | 567
     1 | c2  | EFG
     1 | c3  | 25ea7636-d924-4b4f-81b5-1e1c884b06e3
     1 | c4  | 2021-08-04 18:49:40.835802
     1 | c5  | 41.46100
     1 | c6  | f
     1 | z   |   -- new column
     2 | c1  | 123
     2 | c2  | ABC
     2 | c3  | cf470c5b-50e3-492a-8483-b9e4f20d195a
     2 | c4  | 2021-08-24 18:49:40.835802
     2 | c5  | 1.72964
     2 | c6  | t
     2 | z   | ZZZ   -- new column
     3 | c1  | 890
     3 | c2  | XYZ
     3 | c3  | f588935a-35a4-4275-9e7f-ebb3986390e3
     3 | c4  | 2021-08-29 19:53:39.465778
     3 | c5  | 8.58207
     3 | c6  | t
     3 | z   | ZZZ   -- new column
(21 rows)

机器学习:优化 AUC 估计

ROC 函数可以近似估计 AUC(曲线下面积),其准确度取决于num_bins 参数;num_bins 值越大,提供的近似值越精确,但可能会影响性能。

您可以使用存储过程 accurate_auc() 来近似估计 AUC,它会自动确定给定 epsilon(误差项)的最佳 num_bins 值:

=> CREATE PROCEDURE accurate_auc(relation VARCHAR, observation_col VARCHAR, probability_col VARCHAR, epsilon FLOAT) AS $$
DECLARE
    auc_value FLOAT;
    previous_auc FLOAT;
    nbins INT;
BEGIN
    IF epsilon > 0.25 THEN
        RAISE EXCEPTION 'epsilon must not be bigger than 0.25';
    END IF;
    IF epsilon < 1e-12 THEN
        RAISE EXCEPTION 'epsilon must be bigger than 1e-12';
    END IF;
    auc_value := 0.5;
    previous_auc := 0; -- epsilon and auc should be always less than 1
    nbins := 100;
    WHILE abs(auc_value - previous_auc) > epsilon and nbins < 1000000 LOOP
        RAISE INFO 'auc_value: %', auc_value;
        RAISE INFO 'previous_auc: %', previous_auc;
        RAISE INFO 'nbins: %', nbins;
        previous_auc := auc_value;
        auc_value := EXECUTE 'SELECT auc FROM (select roc(' || QUOTE_IDENT(observation_col) || ',' || QUOTE_IDENT(probability_col) || ' USING parameters num_bins=$1, auc=true) over() FROM ' || QUOTE_IDENT(relation) || ') subq WHERE auc IS NOT NULL' USING nbins;
        nbins := nbins * 2;
    END LOOP;
    RAISE INFO 'Result_auc_value: %', auc_value;
END;
$$;

例如,给定 test_data.csv 中的以下数据:

1,0,0.186
1,1,0.993
1,1,0.9
1,1,0.839
1,0,0.367
1,0,0.362
0,1,0.6
1,1,0.726
...

(完整数据集见 test_data.csv

您可以将数据加载到表 categorical_test_data 中,如下所示:

=> \set datafile '\'/data/test_data.csv\''
=> CREATE TABLE categorical_test_data(obs INT, pred INT, prob FLOAT);
CREATE TABLE

=> COPY categorical_test_data FROM :datafile DELIMITER ',';

调用 accurate_auc()。对于此示例,近似估计的 AUC 将在 epsilon 0.01 内:

=> CALL accurate_auc('categorical_test_data', 'obs', 'prob', 0.01);
INFO 2005:  auc_value: 0.5
INFO 2005:  previous_auc: 0
INFO 2005:  nbins: 100
INFO 2005:  auc_value: 0.749597423510467
INFO 2005:  previous_auc: 0.5
INFO 2005:  nbins: 200
INFO 2005:  Result_auc_value: 0.750402576489533

test_data.csv

1,0,0.186
1,1,0.993
1,1,0.9
1,1,0.839
1,0,0.367
1,0,0.362
0,1,0.6
1,1,0.726
0,0,0.087
0,0,0.004
0,1,0.562
1,0,0.477
0,0,0.258
1,0,0.143
0,0,0.403
1,1,0.978
1,1,0.58
1,1,0.51
0,0,0.424
0,1,0.546
0,1,0.639
0,1,0.676
0,1,0.639
1,1,0.757
1,1,0.883
1,0,0.301
1,1,0.846
1,0,0.129
1,1,0.76
1,0,0.351
1,1,0.803
1,1,0.527
1,1,0.836
1,0,0.417
1,1,0.656
1,1,0.977
1,1,0.815
1,1,0.869
0,0,0.474
0,0,0.346
1,0,0.188
0,1,0.805
1,1,0.872
1,0,0.466
1,1,0.72
0,0,0.163
0,0,0.085
0,0,0.124
1,1,0.876
0,0,0.451
0,0,0.185
1,1,0.937
1,1,0.615
0,0,0.312
1,1,0.924
1,1,0.638
1,1,0.891
0,1,0.621
1,0,0.421
0,0,0.254
0,0,0.225
1,1,0.577
0,1,0.579
0,1,0.628
0,1,0.855
1,1,0.955
0,0,0.331
1,0,0.298
0,0,0.047
0,0,0.173
1,1,0.96
0,0,0.481
0,0,0.39
0,0,0.088
1,0,0.417
0,0,0.12
1,1,0.871
0,1,0.522
0,0,0.312
1,1,0.695
0,0,0.155
0,0,0.352
1,1,0.561
0,0,0.076
0,1,0.923
1,0,0.169
0,0,0.032
1,1,0.63
0,0,0.126
0,0,0.15
1,0,0.348
0,0,0.188
0,1,0.755
1,1,0.813
0,0,0.418
1,0,0.161
1,0,0.316
0,1,0.558
1,1,0.641
1,0,0.305

4 - 用户定义的扩展

用户定义的扩展 (UDx) 是扩展 Vertica 功能的组件,例如,新类型的数据分析以及解析和加载新类型数据的能力。

本节概述如何安装和使用 UDx。如果您使用的是第三方开发的 UDx,请查阅其文档以获取详细的安装和使用说明。

4.1 - 加载 UDx

库中包含用户定义扩展 (UDx)。库可包含多个 UDx。要将 UDx 添加到 Vertica,您必须执行下列操作:

  1. 部署库(每个库一次)。

  2. 创建每个 UDx(每个 UDx 一次)。

如果您使用的是用 Java 编写的 UDx,则还必须设置 Java 运行时环境。请参阅在 Vertica 主机上安装 Java

部署库

要将库部署到 Vertica 数据库,请执行下列操作:

  1. 将包含您的函数的 UDx 共享库文件 (.so)、Python 文件、Java JAR 文件或 R 函数文件复制到 Vertica 群集上的节点。您不需要将其复制到每个节点。

  2. 连接到从中复制库的节点(例如使用 vsql)。

  3. 使用 CREATE LIBRARY 语句将库添加到数据库编录。

    => CREATE LIBRARY libname AS '/path_to_lib/filename'
       LANGUAGE 'language';
    

    libname 是要用来引用库的名称,path_to_lib/filename 是复制到主机的库或 JAR 文件的完全限定路径,language 是实施语言。

    例如,如果创建一个名为 TokenizeStringLib.jar 的 JAR 文件,然后将其复制到数据库管理员帐户的主目录,将使用此命令加载库:

    => CREATE LIBRARY tokenizelib AS '/home/dbadmin/TokenizeStringLib.jar'
       LANGUAGE 'Java';
    

可以将任意数量的库加载到 Vertica。

权限

超级用户可以创建、修改和删除任何库。具有 UDXDEVELOPER 角色或显式授权的用户也可以对库进行操作,如下表所示:

创建 UDx 函数

加载库之后,您可以使用 SQL 语句(例如 CREATE FUNCTIONCREATE SOURCE)定义各个 UDx。这些语句将 SQL 函数名分配给库中的扩展类。它们会将 UDx 添加到数据库编录,并在数据库重新启动后保持可用。

所使用的语句取决于要声明的 UDx 的类型,如下表所示:

如果给定名称的 UDx 已存在,您可以替换它或指示 Vertica 不替换它。要替换它,请使用 OR REPLACE 语法,如下例所示:


=> CREATE OR REPLACE TRANSFORM FUNCTION tokenize
   AS LANGUAGE 'C++' NAME 'TokenFactory' LIBRARY TransformFunctions;
CREATE TRANSFORM FUNCTION

您可能希望替换现有函数以在隔离和非隔离模式之间进行切换。

或者,如果该函数已经存在,您可以使用 IF NOT EXISTS 来防止再次创建该函数。您可能希望在需要并因此加载 UDx 的升级或测试脚本中使用它。通过使用 IF NOT EXISTS,您可以保留包括隔离状态在内的原始定义。以下示例显示了此语法:

--- original creation:
=> CREATE TRANSFORM FUNCTION tokenize
   AS LANGUAGE 'C++' NAME 'TokenFactory' LIBRARY TransformFunctions NOT FENCED;
CREATE TRANSFORM FUNCTION

--- function is not replaced (and is still unfenced):
=> CREATE TRANSFORM FUNCTION IF NOT EXISTS tokenize
   AS LANGUAGE 'C++' NAME 'TokenFactory' LIBRARY TransformFunctions FENCED;
CREATE TRANSFORM FUNCTION

将 UDx 添加到数据库之后,您便可以在 SQL 语句中使用扩展。数据库超级用户能够向用户授予 UDx 访问权限。有关详细信息,请参阅GRANT(用户定义的扩展)

只要您调用 UDx,Vertica 就会在群集中的每个节点上创建 UDx 类的一个实例,并将需要处理的数据提供给该实例。

4.2 - 在 Vertica 主机上安装 Java

如果您使用的是用 Java 编写的 UDx,请按照本节中的说明进行操作。

必须在群集中的每个主机上安装 Java 虚拟机 (Java Virtual Machine, JVM),以使 Vertica 能够执行 Java UDx。

在 Vertica 群集上安装 Java 分为两步:

  1. 在群集中的所有主机上安装 Java 运行时。

  2. 设置 JavaBinaryForUDx 配置参数,让 Vertica 了解 Java 可执行文件的位置。

对于基于 Java 的功能,Vertica 要求使用 64 位 Java 6(Java 版本 1.6)或更高版本 Java 运行时。Vertica 支持 Oracle 或 OpenJDK 中的运行时。您可以选择安装 Java 运行时环境 (JRE) 或者 Java 开发工具包 (JDK),因为 JDK 还包括 JRE。

许多 Linux 分发版都含有一个 OpenJDK 运行时包。有关安装和配置 OpenJDK 的信息,请参阅您的 Linux 分发版的文档。

要安装 Oracle Java 运行时,请参阅 Java Standard Edition (SE) 下载页。您通常需要以 root 身份运行安装包才能安装它。有关说明,请参阅下载页。

如果已经在每台主机上安装了 JVM,请确保 java 命令位于搜索路径中,而且通过运行以下命令来调用正确的 JVM:

$ java -version

此命令的输出类似如下内容:

java version "1.8.0_102"
Java(TM) SE Runtime Environment (build 1.8.0_102-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode)

设置 JavaBinaryForUDx 配置参数

JavaBinaryForUDx 配置参数可以让 Vertica 了解到执行 Java UDx 的 JRE 的位置。在群集中的所有节点上安装 JRE 后,请将此参数设置为 Java 可执行文件的绝对路径。您可以使用 Java 安装程序创建的符号链接(例如 /usr/bin/java)。如果 Java 可执行文件位于 shell 搜索路径中,您可以从 Linux 命令行 shell 中运行以下命令来获得 Java 可执行文件的路径。

$ which java
/usr/bin/java

如果 java 命令不在 shell 搜索路径中,请使用 JRE 安装目录中的 Java 可执行文件路径。假设您已在 /usr/java/default 中安装了 JRE(Oracle 提供的安装包会在此安装 Java 1.6 JRE)。在此情况下,Java 可执行文件为 /usr/java/default/bin/java

要设置配置参数,您可以 数据库超级用户身份执行以下语句:

=> ALTER DATABASE DEFAULT SET PARAMETER JavaBinaryForUDx = '/usr/bin/java';

有关设置配置参数的详细信息,请参阅 ALTER DATABASE

要查看配置参数的当前设置,请查询 CONFIGURATION_PARAMETERS 系统表:

=> \x
Expanded display is on.
=> SELECT * FROM CONFIGURATION_PARAMETERS WHERE parameter_name = 'JavaBinaryForUDx';
-[ RECORD 1 ]-----------------+----------------------------------------------------------
node_name                     | ALL
parameter_name                | JavaBinaryForUDx
current_value                 | /usr/bin/java
default_value                 |
change_under_support_guidance | f
change_requires_restart       | f
description                   | Path to the java binary for executing UDx written in Java

设置配置参数后,Vertica 可以在群集中的每个节点上查找 Java 可执行文件。

4.3 - UDx 限制

您不能对包含复杂类型的输入使用任何 UDx(请参阅复杂类型)。例如,您不能转换或聚合 ROW 列。UDSF(不是其他 UDx)可以返回 ROW。

部分 UDx 类型具有特殊注意事项或限制。

聚合函数

您不能在具有多个聚合函数的查询中使用 DISTINCT 子句。

分析函数

UDAnF 不支持使用 ROWS 搭建窗体框架

使用 Vertica 的内置分析函数时,UDAnF 无法与 MATCH 子句函数 一起使用。

标量函数

如果应用 UDSF 的结果是无效记录,即使 CopyFaultTolerantExpressions 设置为 true,COPY 也会中止加载。

从 UDSF 返回的 ROW 不能用作 COUNT 的实参。

转换函数

包括 UDTF 的查询不能:

加载函数

安装不受信任的 UDL 函数可能会影响服务器安全。UDxs 可以包含任意代码。尤其是用户定义的源函数可以从任意位置读取数据。恰当的安全限制要由函数的开发人员来实施。超级用户不得向不受信任用户授予对 UDx 的访问权限。

不能修改 UDL 函数。

4.4 - 隔离和非隔离模式

用 C++ 编程语言编写的用户定义扩展 (UDx) 可以选择在隔离或非隔离模式下运行。隔离模式在一个单独的 zygote 进程中运行主 Vertica 进程之外的 UDx 代码。使用非隔离模式的 UDx 直接在 Vertica 进程中运行。

隔离模式

您可以在隔离模式下运行大多数 C++ UDx。隔离模式使用单独的 zygote 进程,因此隔离的 UDx 崩溃不会影响核心 Vertica 进程。在隔离模式下运行 UDx 代码时,将造成轻微影响。平均来说,使用隔离模式会使执行时间比非隔离模式增加大约 10%。

目前,所有 C++ UDx(用户定义的聚合除外)都可以使用隔离模式。使用 Python、R 和 Java 编程语言开发的所有 UDx 都必须在隔离模式下运行,因为 Python、R 和 Java 运行时无法直接在 Vertica 进程中运行。

使用隔离模式不会影响 UDx 的开发。对于支持隔离模式的 UDx,默认启用隔离模式。或者,您可以发出带有 NOT FENCED 修饰符的 CREATE FUNCTION 命令来禁用该函数的隔离模式。此外,您可以使用 ALTER FUNCTION 命令在任何支持隔离模式的 C++ UDx 上启用或禁用隔离模式。

非隔离模式

非隔离 UDx 在 Vertica 中运行,因此它们的开销很小,并且执行速度几乎与 Vertica 自有的内置函数一样快。但是,由于它们直接在 Vertica 中运行,因此其代码中的任何缺陷(例如内存泄漏)会导致 Vertica 主进程不稳定,从而关闭一个或多个数据库节点。

关于 zygote 进程

Vertica zygote 进程随 Vertica 一起启动。每个节点具有单个 zygote 进程。从属进程是“按需”创建的。zygote 会侦听请求,并在用户调用 UDx 时生成 UDx 端会话(在隔离模式下运行 UDx)。

关于隔离模式日志记录:

在隔离模式下运行的 UDx 代码记录在 UDxZygote.log 中,并存储在 Vertica 编录目录中的 UDxLogs 目录中。从属进程的日志条目用 UDx 语言(例如 C++)、节点以及 zygote 进程 ID 和 UDxSideProcess ID 表示。

例如,对于以下查询,将返回当前隔离的进程:

=> SELECT * FROM UDX_FENCED_PROCESSES;
    node_name     |   process_type   |            session_id            |  pid  | port  | status
------------------+------------------+----------------------------------+-------+-------+--------
 v_vmart_node0001 | UDxZygoteProcess |                                  | 27468 | 51900 | UP
 v_vmart_node0001 | UDxSideProcess   | localhost.localdoma-27465:0x800b |  5677 | 44123 | UP

以下是上一个查询中返回的隔离进程的相应日志文件:

2016-05-16 11:24:43.990 [C++-localhost.localdoma-27465:0x800b-5677]  0x2b3ff17e7fd0 UDx side process started
 11:24:43.996 [C++-localhost.localdoma-27465:0x800b-5677]  0x2b3ff17e7fd0 Finished setting up signal handlers.
 11:24:43.996 [C++-localhost.localdoma-27465:0x800b-5677]  0x2b3ff17e7fd0 My port: 44123
 11:24:43.996 [C++-localhost.localdoma-27465:0x800b-5677]  0x2b3ff17e7fd0 My address: 0.0.0.0
 11:24:43.996 [C++-localhost.localdoma-27465:0x800b-5677]  0x2b3ff17e7fd0 Vertica port: 51900
 11:24:43.996 [C++-localhost.localdoma-27465:0x800b-5677]  0x2b3ff17e7fd0 Vertica address: 127.0.0.1
 11:25:19.749 [C++-localhost.localdoma-27465:0x800b-5677]  0x41837940 Setting memory resource limit to -1
 11:30:11.523 [C++-localhost.localdoma-27465:0x800b-5677]  0x41837940 Exiting UDx side process

最后一行指示从属进程已终止。在此示例中,该从属进程在用户会话 (vsql) 关闭时终止。

关于隔离模式配置参数

隔离模式支持以下配置参数

  • FencedUDxMemoryLimitMB:用于隔离模式进程的最大内存大小(以 MB 为单位)。默认值为 -1(无限制)。当超过此限制时,将终止从属进程。

  • ForceUDxFencedMode:如果将此参数设置为 1,将强制所有支持隔离模式的 UDx 在隔离模式下运行,即使其定义指定了 NOT FENCED 也如此。默认值为 0(禁用)。

  • UDxFencedBlockTimeout:Vertica 服务器在因 ERROR 3399 中止之前等待 UDx 返回的最长时间(以秒为单位)。默认值为 60

另请参阅

4.5 - 更新 UDx 库

在以下两种情况下,您需要更新已部署的库:

  • 您已将 Vertica 升级到包含 SDK API 更改的新版本。要使库与新的服务器版本正常使用,您需要使用新版本的 SDK 重新编译这些库。有关详细信息,请参阅UDx 库与新服务器版本的兼容性

  • 您已对 UDx 进行了更改,并且想要部署这些更改。更新 UDx 库之前,您需要确定是否更改了库中包含的任何函数的签名。如果是的话,则在更新库之前,您需要从 Vertica 编录中删除这些函数。

4.5.1 - UDx 库与新服务器版本的兼容性

Vertica SDK 定义了一个应用程序编程接口 (Application Programming Interface, API),UDx 可使用该 API 与数据库交互。当开发人员编译其 UDx 代码时,该代码会链接到 SDK 代码以形成一个库。此库仅与支持用于编译该代码的 SDK API 版本的 Vertica 服务器兼容。使用相同 API 版本的库和服务器在二进制级别是兼容的(此兼容性称为“二进制兼容”)。

如果您尝试加载与 Vertica 服务器不具有二进制兼容性的库,服务器会返回错误消息。同样,如果将 Vertica 服务器升级到支持新的 SDK API 的版本,任何依赖新的不兼容库的现有 UDx 会在您向其发出调用时返回错误消息:

ERROR 2858:  Could not find function definition
HINT:
This usually happens due to missing or corrupt libraries, libraries built
with the wrong SDK version, or due to a concurrent session dropping the library
or function. Try recreating the library and function

要解决此问题,您必须安装已使用正确的 SDK 版本重新编译的 UDx 库。

新版本的 Vertica 服务器并不总会更改 SDK API 版本。只要 Micro Focus 更改了构成 SDK 的组件,SDK API 版本就会更改。如果 SDK API 在新版本的服务器中未更改,旧库会继续与新服务器兼容。

当 Micro Focus 扩展 SDK 的功能时,SDK API 几乎总是在 Vertica 版本(主要版本、次要版本、服务包)中发生更改。Vertica 绝不会在修补程序补丁中更改 API。

这些策略意味着您必须在主要版本之间升级时更新 UDx 库。例如,如果从版本 10.0 升级到 10.1,则必须更新 UDx 库。

升级前步骤

升级 Vertica 服务器之前,应考虑是否存在任何与新版本不兼容的 UDx 库。请参阅新的服务器版本的发行说明,以确定 SDK API 在当前已安装的 Vertica 服务器版本和新版本之间是否已更改。如前文所述,只有从前一个主要版本升级或从某个主要版本的初始发行版升级到某个服务包发行版时,才会导致当前已加载的 UDx 库变为与服务器不兼容。

必须重新编译与新版本 Vertica 服务器不兼容的任何 UDx 库。如果 UDx 库之前是从第三方获取的,您必须检查该库是否已发布新版本。如果是这样,请在升级服务器后部署新版本(请参阅部署 UDx 库的新版本)。

如果 UDx 是您自己开发的(或者如果您有源代码),则您必须执行下列操作:

  1. 使用新版本的 Vertica SDK 重新编译 UDx 库。有关详细信息,请参阅编译 C++ 库编译并打包 Java 库

  2. 部署新版本的库。请参阅部署 UDx 库的新版本

4.5.2 - 确定 UDx 签名是否已更改

对包含已部署到 Vertica 数据库的函数的 UDx 库进行更改时,您需要小心操作。部署新版本的 UDx 库时,Vertica 无法确保在该库中定义的函数的签名与已在 Vertica 编录中定义的函数的签名匹配。如果您更改了库中的 UDx 的签名,然后在 Vertica 数据库中更新了该库,则对已更改的 UDx 发出的调用会生成错误。

对 UDx 进行以下任何更改会更改其签名:

  • 更改函数(不包括多态函数)所接受的参数个数,或更改所接受的任何参数的数据类型。

  • 更改任何返回值或输出列的个数或数据类型。

  • 更改 Vertica 用来为函数代码创建实例的工厂类的名称。

  • 更改函数的空值处理行为或可变性行为。

  • 从库中完成移除函数的工厂类。

以下更改不会更改函数的签名,也不要求在更新库之前删除函数。

  • 更改由多态函数处理的参数的个数或类型。Vertica 不会处理用户传递到多态函数的实参。

  • 更改函数所接受的参数的名称、数据类型或个数。函数所接受的参数不由函数签名决定。相反,Vertica 会传递用户包含到函数调用中的所有函数,而函数会在运行时传递这些参数。有关参数的详细信息,请参阅 UDx 参数

  • 更改由函数执行的任何内部处理。

  • 将新的 UDx 添加到库中

删除签名已更改的任何函数之后,您可以加载新的库文件,然后重新创建已更改的函数。如果未对 UDx 的签名进行任何更改,则您只需在 Vertica 数据库中更新库文件即可,而无需删除或更改函数定义。只要 Vertica 编录中的 UDx 定义与库中的函数的签名匹配,函数调用就可以在更新库之后以透明方式工作。请参阅部署 UDx 库的新版本

4.5.3 - 部署 UDx 库的新版本

在下列情况下,您需要部署 UDx 库的新版本:

  • 您对库进行了更改,并且现在想要将这些更改应用到 Vertica 数据库。

  • 您已将 Vertica 升级到新版本,并且该新版本的 SDK 与上一个版本不兼容。

部署库的新版本的过程与初始部署相似。

  1. 如果要部署在 C++ 或 Java 中开发的 UDx 库,您必须使用当前版本的 Vertica SDK 编译该库。

  2. 将 UDx 的库文件(对于在 C++ 中开发的库,此文件是 .so 文件;对于在 Python 中开发的库,此文件是 .py 文件;对于在 Java 中开发的库,此文件是 .jar 文件)或 R 源文件复制到 Vertica 数据库中的主机。

  3. 使用 vsql 连接到主机。

  4. 如果更改了共享库中任何 UDx 的签名,您必须使用 DROP 语句(例如 DROP FUNCTIONDROP SOURCE)删除它们。如果不确定函数的任何签名是否已更改,请参阅确定 UDx 签名是否已更改

  5. 通过 ALTER LIBRARY 语句使用在步骤 1 中复制的文件更新 UDx 库定义。例如,如果要使用 dbadmin 用户主目录中名为 ScalarFunctions-2.0.so 的文件更新名为 ScalarFunctions 的库,请使用以下命令:

    => ALTER LIBRARY ScalarFunctions AS '/home/dbadmin/ScalarFunctions-2.0.so';
    

    更新了 UDx 库定义以使用新版本的共享库之后,使用 UDx 库中的类定义的 UDx 将开始使用新的共享库文件,而不进一步对该文件进行任何更改。

  6. 如果在步骤 4 中必须删除任何函数,请使用由库中的工厂类定义的新签名重新创建这些函数。请参阅CREATE FUNCTION 语句

4.6 - 列出包含在库中的 UDx

使用 CREATE LIBRARY 语句加载了库之后,您可以通过查询 USER_LIBRARY_MANIFEST 系统表来查找 UDx 及其包含的 UDL:

=> CREATE LIBRARY ScalarFunctions AS '/home/dbadmin/ScalarFunctions.so';
CREATE LIBRARY
=> \x
Expanded display is on.
=> SELECT * FROM USER_LIBRARY_MANIFEST WHERE lib_name = 'ScalarFunctions';
-[ RECORD 1 ]-------------------
schema_name | public
lib_name    | ScalarFunctions
lib_oid     | 45035996273792402
obj_name    | RemoveSpaceFactory
obj_type    | Scalar Function
arg_types   | Varchar
return_type | Varchar
-[ RECORD 2 ]-------------------
schema_name | public
lib_name    | ScalarFunctions
lib_oid     | 45035996273792402
obj_name    | Div2intsInfo
obj_type    | Scalar Function
arg_types   | Integer, Integer
return_type | Integer
-[ RECORD 3 ]-------------------
schema_name | public
lib_name    | ScalarFunctions
lib_oid     | 45035996273792402
obj_name    | Add2intsInfo
obj_type    | Scalar Function
arg_types   | Integer, Integer
return_type | Integer

obj_name 列将列出库中包含的工厂类。您可以使用这些名称通过诸如 CREATE FUNCTIONCREATE SOURCE 等语句在数据库编录中定义 UDx 和 UDL。

4.7 - 在您的 UDx 中使用通配符

Vertica 支持在用户定义的函数中使用通配符 * 来替代列名称。

您可以在以下情况下使用通配符:

  • 您的查询在 FROM 子句中包含表

  • 您正在使用支持 Vertica 的开发语言

  • 您的 UDx 正在以隔离模式或非隔离模式运行

支持的 SQL 语句

以下 SQL 语句可以接受通配符:

  • DELETE

  • INSERT

  • SELECT

  • UPDATE

不支持的配置

以下情况不支持通配符:

  • 不能在查询的 OVER 子句中传递通配符

  • 不能在 DROP 语句中使用通配符

  • 不能将通配符与任何其他参数结合使用

示例

以下示例显示了一系列数据处理操作中的通配符和用户定义的函数。

DELETE 语句:

=> DELETE FROM tablename WHERE udf(tablename.*) = 5;

INSERT 语句:

=> INSERT INTO table1 SELECT udf(*) FROM table2;

SELECT 语句:

=> SELECT udf(*) FROM tablename;
=> SELECT udf(tablename.*) FROM tablename;
=> SELECT udf(f.*) FROM table f;
=> SELECT udf(*) FROM table1,table2;
=> SELECT udf1( udf2(*) ) FROM table1,table2;
=> SELECT udf( db.schema.table.*) FROM tablename;
=> SELECT udf(sub.*) FROM (select col1, col2 FROM table) sub;
=> SELECT x FROM tablename WHERE udf(*) = y;
=> WITH sub as (SELECT * FROM tablename) select x, udf(*) FROM sub;
=> SELECT udf( * using parameters x=1) FROM tablename;
=> SELECT udf(table1.*, table2.col2) FROM table1,table2;

UPDATE 语句:

=> UPDATE tablename set col1 = 4 FROM tablename WHERE udf(*) = 3;

5 - 开发用户定义的扩展 (UDx)

用户定义的扩展 (UDx) 是包含在外部库中的函数,它们使用 Vertica SDK 以 C++、Python、Java 或 R 语言开发而成。外部库是在 Vertica 编录中使用 CREATE LIBRARY 语句定义的。它们最适用于难以用 SQL 执行的分析操作或者需要足够频繁地执行但速度成为主要问题的分析操作。

UDx 的主要优势如下:

  • 它们能够在任何可以使用内部函数的地方使用。

  • 它们充分利用 Vertica 的分布式计算功能。扩展通常在群集中的每个节点上并行执行。

  • 它们由 Vertica 分发到所有节点。您只需要将库复制到启动程序节点。

  • 开发分布式分析代码块的所有复杂方面均由 Vertica 为您处理。您的主要编程任务是读入并处理数据,然后使用 Vertica SDK API 写出该数据。

您需要谨记以下有关开发 UDx 的几个要点:

  • 可以采用编程语言开发 UDx:C++、Python、Java 和 R(并非所有 UDx 类型均支持全部语言)。

  • 在 Java 中编写的 UDx 始终在隔离模式下运行,因为执行 Java 程序的 Java 虚拟机无法直接在 Vertica 进程中运行。

  • 以 Python 编写的 UDx 和 R 函数始终在隔离模式下运行。

  • 在 C++ 中开发的 UDx 提供了在非隔离模式下运行的选项,这意味着它们直接加载到 Vertica 数据库进程并在其中运行。此选项可提供最小开销和最高速度。但是,UDx 代码中的任何缺陷会导致数据库不稳定。部署到实时环境之前,您必须全面测试计划在非隔离模式下运行的任何 UDx。应考虑在非隔离模式下运行 C++ UDx 所实现的性能提升是否抵得上有缺陷 UDx 可能会导致的数据库不稳定性。

  • 由于 UDx 在 Vertica 群集上运行,因此它会从数据库进程夺取处理器时间和内存。使用大量计算资源的 UDx 会对数据库性能造成负面影响。

UDx 的类型

Vertica 支持五种类型的用户定义的扩展:

  • 用户定义的标量函数 (UDSF) 接受单个数据行并返回单个值。这些函数能够在任何可以使用原生函数的地方使用,但不能在 CREATE TABLE BY PARTITION 和 SEGMENTED BY 表达式中使用。可以采用 C++、Python、Java 和 R 开发 UDSF。

  • 使用用户定义的聚合函数 (UDAF),可以创建特定于您的需求的自定义 聚合函数。它们读取一个数据列,并返回一个输出列。可以采用 C++ 开发 UDAF。

  • 用户定义的分析函数 (UDAnF) 与 UDSF 相似,它们也读取一个数据行并返回单个行。但是,这种函数可以仅读取输入行而不输出行,以便可以基于多个输入行计算输出值。该函数可以与查询的 OVER() 子句一起使用,以便对行进行分区。可以采用 C++ 和 Java 开发 UDAnF。

  • 用户定义的转换函数 (UDTF) 对表分区进行操作(由查询的 OVER() 子句指定),并返回零个或更多数据行。它们返回的数据可以是与输入表的架构毫无关联的全新表,具有自己的排序和分段表达式。它们可在查询的 SELECT 列表中使用。可以采用 C++、Python、Java 和 R 开发 UDTF。

    要优化查询性能,您可以使用实时聚合投影对 UDTF 返回的数据进行预聚合。有关详细信息,请参阅预聚合 UDTF 结果

  • 用户定义的加载,可以创建用于加载数据的自定义资源筛选器解析器。这些扩展可以用在 COPY 语句中。UDL可以开发 C++、Java 和 Python。

虽然每种 UDx 类型具有唯一的基本类,但它们的开发过程有许多相似之处。不同类型的 UDx 还可以共用同一个库。

结构

每个 UDx 类型包含两个主类。主类执行实际工作(转换和聚合等)。此类通常具有至少三种方法:一种用于设置,一种用于拆解(释放预留资源),另一种用于执行工作。有时还会定义其他方法。

主要处理方法接收 ServerInterface 类的实例作为实参。底层 Vertica SDK 代码使用此对象向 Vertica 进程发出回调,例如分配内存。您可以使用此类在 UDx 执行期间写入服务器日志。

第二个类是个单例工厂。此类定义了一种用于生成第一个类的实例的方法,并且可能定义用于管理参数的其他方法。

实施 UDx 时,您必须将这两个类子类化。

约定

C++、Python 和 Java API 几乎相同。在可能的情况下,本指南在介绍这些接口时不考虑语言。特定于语言的各节涵盖了特定于 C++、Python 或 Java 的文档。

由于部分文档与语言无关,因此无法始终使用基于语言的最合适术语。本文档使用术语“方法”来指 Java 方法或 C++ 成员函数。

另请参阅

加载 UDx

5.1 - 使用 Vertica SDK 进行开发

在编写用户定义的扩展之前,您必须设置一个开发环境。完成此操作后,最好通过下载、构建和运行已发布的示例来进行测试。

除了介绍如何设置您的环境之外,本节还描述了有关使用 Vertica SDK 的一般信息,包括特定于语言的注意事项。

5.1.1 - 设置开发环境

在开始开发 UDx 之前,您需要配置您的开发环境和测试环境。开发测试和测试环境必须使用与生产环境相同的操作系统和 Vertica 版本。

有关其他特定于语言的要求,请参阅以下主题:

开发环境选项

您用于开发 UDx 的语言决定了开发环境的设置选项和要求。C++ 开发人员可以使用 C++ UDx 容器,而所有开发人员都可以使用非生产 Vertica 环境

C++ UDx 容器

C++ 开发人员可以使用 C++ UDx 容器进行开发。UDx 容器的 GitHub 存储库提供了用于构建容器的工具,该容器可对开发 C++ Vertica 扩展所需的二进制文件、库和编译器进行打包。C++ UDx 容器具有以下构建选项:

  • CentOS 或 Ubuntu 基础映像

  • Vertica 10.x 和 11.x 版本

有关要求、构建和测试的详细信息,请参阅存储库自述文件

非生产 Vertica 环境

您可以使用非生产 Vertica 数据库中的节点,也可以使用另一台运行与生产环境相同的操作系统和 Vertica 版本的计算机。有关具体要求和依赖项,请参考操作系统要求语言要求

测试环境选项

要测试 UDx,需要访问非生产 Vertica 数据库。您具有以下选择:

  • 在开发计算机上安装单节点 Vertica 数据库。
  • 下载并构建容器化测试环境。

容器化测试环境

Vertica 提供以下容器化选项来简化您的测试环境设置:

操作系统要求

在用于生产 Vertica 数据库群集的同一 Linux 平台上开发 UDx 代码。基于 Centos 和 Debian 的操作系统都要求您下载其他包。

基于 CentOS 的操作系统

在以下基于 CentOS 的操作系统上安装时需要 devtoolset-7 包

  • CentOS

  • Red Hat Enterprise Linux

  • Oracle Enterprise Linux

有关具体的安装命令,请参阅适用于您所用操作系统的文档。

基于 Debian 的操作系统

在以下基于 Debian 的操作系统上安装时需要 GCC-7 包

  • Debian

  • Ubuntu

  • SUSE

  • OpenSUSE

  • Amazon Linux(GCC 包已预装在 Amazon Linux 上)

有关具体的安装命令,请参阅适用于您所用操作系统的文档。

5.1.2 - 下载并运行 UDx 示例代码

您可以从 Vertica GitHub 存储库下载本文档中显示的所有示例以及更多示例。此存储库包含所有类型的 UDx 的示例。

您可以通过以下两种方式之一下载示例:

  • 下载 ZIP 文件。将文件内容解压缩到目录中。

  • 克隆存储库。使用终端窗口,运行以下命令:

    $ git clone https://github.com/vertica/UDx-Examples.git
    

该存储库包含一个可用于编译 C++ 和 Java 示例的生成文件。它还包含加载和使用示例的 .sql 文件。有关编译和运行示例的说明,请参阅 README 文件。要编译示例,您将需要 g++ 或 JDK 和 make。有关相关信息,请参阅设置开发环境

运行示例不仅有助于了解 UDx 的工作原理,而且有助于确保正确设置开发环境以编译 UDx 库。

另请参阅

5.1.3 - C++ SDK

Vertica SDK 支持在 C++ 11 中编写受保护和未受保护的 UDx。您可以下载、编译和运行示例;请参阅下载并运行 UDx 示例代码。运行示例是验证您的开发环境是否具有所有需要的库的好方法。

如果您无权访问 Vertica 测试环境,则可以在开发计算机上安装 Vertica 并运行单个节点。每次重建 UDx 库时,都需要将其重新安装到 Vertica。下图说明了典型开发周期。

此部分涵盖适用于所有 UDx 类型的 C++ 特定主题。有关适用于所有语言的信息,请参阅实参和返回值UDx 参数错误、警告和日志记录处理取消请求和特定 UDx 类型的章节。如需完整的 API 文档,请参阅 C++ SDK 文档。

5.1.3.1 - 设置 C++ SDK

Vertica C++ 软件开发工具包 (SDK) 将作为服务器安装的一部分分发。它包含创建 UDx 库所需的源文件和头文件。有关可以编译和运行的示例,请参阅下载并运行 UDx 示例代码

要求

至少需要在开发计算机上安装下列资源:

  • devtoolset-7 包 (CentOS) 或 GCC-7 包 (Debian),包括 gcc 版本 7 和最新的 libstdc++ 包。

  • g++ 及其关联的工具链,例如 ld。有些 Linux 分发将 g++gcc 分开打包。

  • Vertica SDK 的副本。

您必须使用标记 -std=c++11 进行编译。Vertica SDK 会使用 C++ 11 的功能。

以下可选软件包可以简化开发:

  • make,或某些其他内部版本管理工具。

  • gdb,或某些其他调试器。

  • Valgrind 或可检测内存泄漏的类似工具。

如果要使用任何第三方库(例如统计分析库),您需要在开发计算机上安装这些库。如果不是静态地将这些库链接到 UDx 库,您必须在群集中的每个节点上安装这些库。有关详细信息,请参阅编译 C++ 库

SDK 文件

SDK 文件位于 Vertica 服务器根目录下的 sdk 子目录中(通常为 /opt/vertica/sdk)。此目录包含子目录 include,其中包含编译 UDx 库所需的头文件和源文件。

include 目录包含两个文件,在编译 UDx 时,需要使用这两个文件:

  • Vertica.h 是 SDK 的主要头文件。UDx 代码需要包含此文件才能查找 SDK 的定义。

  • Vertica.cpp 包含需要编译到 UDx 库中的支持代码。

VerticaUDx.h 头文件(包含在 Vertica.h 文件中)中定义了许多 Vertica SDK API。如果对此感到好奇,您可以检查此文件的内容并阅读 API 文件。

开发 UDx 时,计划使用的 SDK 版本必须与其所在的数据库版本相同。要显示当前安装在系统上的 SDK 版本,请在 vsql 中运行以下命令:

=> SELECT sdk_version();

运行示例

您可以从 GitHub 存储库下载示例(请参阅下载并运行 UDx 示例代码)。编译和运行示例有助于确保正确设置开发环境。

要编译所有示例(包括 Java 示例),请在示例目录下的 Java-and-C++ 目录中执行以下命令:

$ make

5.1.3.2 - 编译 C++ 库

仅支持使用 GNU g++ 编译器来编译 UDx 库。请始终基于 Vertica 群集上使用的相同 Linux 版本来编译 UDx 代码。

编译库时,始终必须执行下列操作:

  • 使用 -std=c++11 标志进行编译。

  • -shared-fPIC 标记传递到链接器。最简单的方法是在编译和链接库时仅将这些标记传递到 g++。

  • 使用 -Wno-unused-value 标记可抑制未使用宏参数时的警告。如果不使用此标记,您可能会收到“left-hand operand of comma has no effect”(逗号的左操作数无效)警告。

  • 编译 sdk/include/Vertica.cpp 并将其链接到库。此文件包含可帮助 UDx 与 Vertica 通信的支持例程。执行此操作的最简单方法是将此文件包含到用于编译库的 g++ 命令中。Vertica 以 C++ 源代码(而非库)的形式提供此文件,以避免库兼容性问题。

  • 使用 g++ -I 标记将 Vertica SDK include 目录添加到 include 搜索路径中。

SDK 示例包括一个工作生成文件。请参阅下载并运行 UDx 示例代码

编译 UDx 的示例

以下命令可将一个 UDx(包含于名为 MyUDx.cpp 的单个源文件中)编译到名为 MyUDx.so 的共享库中:

g++ -I /opt/vertica/sdk/include -Wall -shared -Wno-unused-value \
      -fPIC -o MyUDx.so MyUDx.cpp /opt/vertica/sdk/include/Vertica.cpp

调试 UDx 后,便已准备好部署它。使用 -O3 标记重新编译 UDx,以启用编译器优化。

可以将其他源文件添加到库中,方法是将这些源文件添加到命令行中。还可以分别编译这些源文件,然后将它们链接到一起。

处理外部库

必须将 UDx 库链接到 UDx 代码所依赖的任何支持库。这些库必须是您开发的库或由第三方提供的其他库。可以使用以下两个选项进行链接:

  • 静态地将支持库链接到 UDx。此方法的优点是 UDx 库不依赖外部文件。拥有一个 UDx 库文件可以简化部署,因为您只需将一个文件传输到 Vertica 群集。此方法的主要缺点是增加 UDx 库文件的大小。

  • 动态地将库链接到 UDx。如果第三方库不允许静态链接,则有时必须使用动态链接。在这种情况下,必须将库和 UDx 库文件复制到 Vertica 群集。

5.1.3.3 - 将元数据添加到 C++ 库

您可以将诸如作者姓名、库版本以及库描述之类的元数据添加到您的库。此元数据可以让您跟踪在 Vertica 分析数据库群集上部署的函数的版本并让您函数的第三方用户了解该函数的创建者。库加载到 Vertica 分析数据库编录之后,库的元数据会出现在 USER_LIBRARIES 系统表中。

可通过在 UDx 的一个源文件中调用 RegisterLibrary() 函数为库声明元数据。如果 UDx 的源文件中存在多个函数调用,则使用在 Vertica 分析数据库加载库时最后解释的函数调用来确定库的元数据。

RegisterLibrary() 函数采用八个字符串参数:

RegisterLibrary(author,
                library_build_tag,
                library_version,
                library_sdk_version,
                source_url,
                description,
                licenses_required,
                signature);
  • author 包含要与库创建相关联的名称(例如自己的名称或公司名称)。

  • library_build_tag 为用于代表库的特定版本的字符串(例如 SVN 修订号或编译库的时间戳)。开发库实例时,跟踪这些库实例很有用。

  • library_version 为库的版本。您可以使用想使用的任何编号或命名方案。

  • library_sdk_version 是已为其编译了库的 Vertica 分析数据库 SDK 库的版本。

  • source_url 为函数用户可从中查找函数详细信息的 URL。这可以是您公司的网站、托管库源代码的 GitHub 页面或您喜欢的任何站点。

  • description 为库的简要描述。

  • licenses_required 为许可信息占位符。您必须为此值传递一个空字符串。

  • signature 为对库进行身份验证的签名的占位符。您必须为此值传递一个空字符串。

例如,以下代码演示了向 Add2Ints 示例添加元数据(请参阅 C++ 示例:Add2Ints)。

// Register the factory with Vertica
RegisterFactory(Add2IntsFactory);

// Register the library's metadata.
RegisterLibrary("Whizzo Analytics Ltd.",
                "1234",
                "2.0",
                "7.0.0",
                "http://www.example.com/add2ints",
                "Add 2 Integer Library",
                "",
                "");

加载库并查询 USER_LIBRARIES 系统表将显示调用 RegisterLibrary() 提供的元数据:

=> CREATE LIBRARY add2intslib AS '/home/dbadmin/add2ints.so';
CREATE LIBRARY
=> \x
Expanded display is on.
=> SELECT * FROM USER_LIBRARIES WHERE lib_name = 'add2intslib';
-[ RECORD 1 ]-----+----------------------------------------
schema_name       | public
lib_name          | add2intslib
lib_oid           | 45035996273869808
author            | Whizzo Analytics Ltd.
owner_id          | 45035996273704962
lib_file_name     | public_add2intslib_45035996273869808.so
md5_sum           | 732c9e145d447c8ac6e7304313d3b8a0
sdk_version       | v7.0.0-20131105
revision          | 125200
lib_build_tag     | 1234
lib_version       | 2.0
lib_sdk_version   | 7.0.0
source_url        | http://www.example.com/add2ints
description       | Add 2 Integer Library
licenses_required |
signature         |

5.1.3.4 - C++ SDK 数据类型

Vertica SDK 提供了用于在 UDx 代码中表示 Vertica 数据类型的 typedef 和类。使用这些 typedef,可确保 UDx 所处理和生成的数据与 Vertica 数据库之间的数据类型兼容性。下表介绍了部分可用的 typedef。有关完整列表以及用于转换和处理这些数据类型的 helper 函数的列表,请参阅 C++ SDK 文档。

有关 SDK 支持的复杂数据类型的信息,请参阅作为实参的复杂类型和返回值

注意

  • 在对象上发出某些 Vertica SDK API 调用(例如 VerticaType::getNumericLength())时,请确保这些对象具有正确的数据类型。为了最大程度减少开销并提高性能,大部分 API 不会检查在其上面发出调用的对象的数据类型。对不正确的数据类型调用函数会导致发生错误。

  • 不能独自创建 VString 或 VNumeric 的实例。您可以处理这些类(由 Vertica 传递到 UDx)的现有对象的值,并从这些类提取值。但是,只有 Vertica 可以实例化这些类。

5.1.3.5 - C++ UDx 的资源使用情况

通过将类实例化并创建逻辑变量,UDx 可将自己占用的内存数量减至最少。UDx 的此基本内存使用量相当小,您完全不必关心此内存使用量。

如果 UDx 需要为数据结构分配超过 1 MB 或 2 MB 内存,或者需要访问其他资源(例如文件),您必须向 Vertica 通知其资源使用情况。然后,Vertica 可以先确保 UDx 所需的资源可用,接着再运行使用该 UDx 的查询。如果许多调用该 UDx 的查询同时运行,则即使是中等内存使用量(例如,UDx 的每个调用各 10 MB)也会成为问题。

5.1.3.5.1 - 为 UDx 分配资源

为用户定义的扩展 (UDx) 分配内存和文件句柄时,您可以选择以下两种方法:

  • 使用 Vertica SDK 宏分配资源。这是最佳方法,因为此方法使用 Vertica 自有的资源管理器,并且能够保证 UDx 所使用的资源得到回收。请参阅使用 SDK 宏分配资源

  • 虽然不是推荐的选项,但您可以使用标准 C++ 方法(使用 new 将对象实例化和使用 malloc() 分配内存块等)自己在 UDx 中分配资源。您必须在 UDx 退出之前手动释放这些资源。

无论选择哪种方法,您通常都应在 UDx 类中名为 setup() 的函数中分配资源。在 UDx 函数对象已实例化之后,但在 Vertica 调用该对象以处理数据之前,将调用此函数。

如果手动在 setup() 函数中分配内存,则您必须在名为 destroy() 的相应函数中释放内存。在 UDx 已执行所有处理之后,将调用此函数。如果 UDx 返回错误,也会调用此函数(请参阅处理错误)。

以下代码片段演示了使用 setup()destroy() 函数分配和释放内存。

class MemoryAllocationExample : public ScalarFunction
{
public:
    uint64* myarray;
    // Called before running the UDF to allocate memory used throughout
    // the entire UDF processing.
    virtual void setup(ServerInterface &srvInterface, const SizedColumnTypes
                        &argTypes)
    {
        try
        {
            // Allocate an array. This memory is directly allocated, rather than
            // letting Vertica do it. Remember to properly calculate the amount
            // of memory you need based on the data type you are allocating.
            // This example divides 500MB by 8, since that's the number of
            // bytes in a 64-bit unsigned integer.
            myarray = new uint64[1024 * 1024 * 500 / 8];
        }
        catch (std::bad_alloc &ba)
        {
            // Always check for exceptions caused by failed memory
            // allocations.
            vt_report_error(1, "Couldn't allocate memory :[%s]", ba.what());
        }

    }

    // Called after the UDF has processed all of its information. Use to free
    // any allocated resources.
    virtual void destroy(ServerInterface &srvInterface, const SizedColumnTypes
                          &argTypes)
    {
        // srvInterface.log("RowNumber processed %d records", *count_ptr);
        try
        {
            // Properly dispose of the allocated memory.
            delete[] myarray;
        }
        catch (std::bad_alloc &ba)
        {
            // Always check for exceptions caused by failed memory
            // allocations.
            vt_report_error(1, "Couldn't free memory :[%s]", ba.what());
        }

    }

5.1.3.5.2 - 使用 SDK 宏分配资源

Vertica SDK 提供了三个用于分配内存的宏:

  • vt_alloc 可分配内存块以用于容纳特定数据类型(vint、struct 等)。

  • vt_allocArray 可分配内存块以用于容纳特定数据类型的数组。

  • vt_allocSize 可分配任意大小的内存块。

所有这些宏都可以从 Vertica 管理的内存池分配内存。让 Vertica 管理 UDx 的内存的主要优点是,内存会在 UDx 已完成之后自动回收。这样可确保 UDx 中不会出现内存泄漏。

由于 Vertica 会自动释放该内存,因此请勿尝试释放您通过以上任一宏分配的任何内存。尝试释放该内存将导致发生运行时错误。

5.1.3.5.3 - 向 Vertica 通知资源要求

在隔离模式下运行 UDx 时,Vertica 会监控其内存和文件句柄的使用情况。如果 UDx 使用超过数 MB 内存或使用任意文件句柄,则它应向 Vertica 说明其资源要求。获知 UDx 的资源要求后,Vertica 能够确定是可以立即运行该 UDx,还是需要将请求排入队列直至足够的资源变为可用于运行该 UDx。

在某些情况下,可能难以确定 UDx 查询所需的内存数量。例如,如果 UDx 从数据集提取唯一的数据元素,数据项的数量可能不会受到限制。在这种情况下,您可以在测试环境中运行 UDx 并在节点上监控该 UDx 在处理多个不同大小的查询时的内存使用量,然后基于该 UDx 在生产环境中可能面临的最坏情况推断其内存使用量,这种技术很有用。在所有情况下,通常最好为您向 Vertica 告知的 Udx 内存使用量添加安全余地。

UDx 可通过在其工厂类中实施 getPerInstanceResources() 函数来向 Vertica 通知其资源需求(请参阅 SDK 文档中的 Vertica::UDXFactory::getPerInstanceResources())。如果 UDx 的工厂类实施此函数,Vertica 将调用此函数以确定 UDx 所需的资源。

getPerInstanceResources() 函数将收到 Vertica::VResources 结构的一个实例。此结构包含用于设置 UDx 所需的内存数量和文件句柄数的字段。默认情况下,Vertica 服务器会为 UDx 的每个实例分配零字节内存和 100 个文件句柄。

getPerInstanceResources() 函数的实施基于 UDx 可能使用的最大资源数量为 UDx 函数的每个实例设置 VResources 结构中的字段。因此,如果 UDx 的 processBlock() 函数所创建的数据结构最多使用 100 MB 内存,则 UDx 必须将 VResources.scratchMemory 字段设置为 104857600(100 MB 等于的字节数)或以上。通过将该数字增加至 UDx 在正常情况下应使用的内存数量以上,为您自己保留一个安全边际。在此示例中,合适做法是分配 115000000 字节(刚好低于 110 MB)。

以下 ScalarFunctionFactory 类演示了调用 getPerInstanceResources() 以向 Vertica 通知为 UDx 分配资源中所示的 MemoryAllocationExample 类的内存要求。该类会向 Vertica 说明 UDSF 需要 510 MB 内存(为保持安全大小,此容量比 UDSF 实际分配的容量稍多)。

class MemoryAllocationExampleFactory : public ScalarFunctionFactory
{
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface
                                                            &srvInterface)
    {
        return vt_createFuncObj(srvInterface.allocator, MemoryAllocationExample);
    }
    virtual void getPrototype(Vertica::ServerInterface &srvInterface,
                              Vertica::ColumnTypes &argTypes,
                              Vertica::ColumnTypes &returnType)
    {
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }
    // Tells Vertica the amount of resources that this UDF uses.
    virtual void getPerInstanceResources(ServerInterface &srvInterface,
                                          VResources &res)
    {
        res.scratchMemory += 1024LL * 1024 * 510; // request 510MB of memory
    }
};

5.1.3.5.4 - 为隔离模式 UDx 设置内存限制

Vertica 将调用隔离模式 UDx 的 Vertica::UDXFactory::getPerInstanceResources() 实施,以确定是否有足够的资源可用于运行包含该 UDx 的查询(请参阅向 Vertica 通知资源要求)。由于这些报告并非由实际内存使用生成,因此这些报告可能不准确。Vertica 启动某个 UDx 之后,该 UDx 实际分配的内存或文件句柄可能远远多于其报告的需求量。

使用 FencedUDxMemoryLimitMB 配置参数,您可以为 UDx 创建绝对内存限制。只要 UDx 尝试分配的内存数量超过此限制,就会生成 bad_alloc 异常。有关设置 FencedUDxMemoryLimitMB 的示例,请参阅如何强制执行资源限制

5.1.3.5.5 - 如何强制执行资源限制

运行查询之前,Vertica 会确定该查询运行所需的内存量。如果查询含有在工厂类中实施 getPerInstanceResources() 函数的隔离模式 UDx,Vertica 将调用此函数以确定其所需的内存数量,并将此内存数量添加到查询所需的内存总量中。Vertica 会根据以下要求来确定如何处理查询:

  • 如果所需的内存总量(包括 UDx 报告的所需内存量)超过会话的 MEMORYCAP 或 资源池的 MAXMEMORYSIZE 设置,Vertica 将拒绝该查询。有关资源池的详细信息,请参阅资源池架构

  • 如果内存量低于会话所设置的限制和资源池限制,但目前没有足够的可用内存可用于运行查询,Vertica 会将该查询排入队列,直至足够资源变为可用为止。

  • 如果有足够的可用资源可用于运行查询,Vertica 会执行该查询。

如果执行 UDx 的进程尝试分配比 FencedUDxMemoryLimitMB 配置参数所设置的限制更多的内存,进程将收到 bad_alloc 异常。有关 FencedUDxMemoryLimitMB 的详细信息,请参阅为隔离模式 UDx 设置内存限制

下面是执行以下操作时的输出:加载使用 500 MB 内存的 UDSF,然后更改内存设置而导致发生内存不足错误。以下示例中的 MemoryAllocationExample UDSF 只是按照为 UDx 分配资源向 Vertica 通知资源要求中所示而更改的 Add2Ints UDSF 示例,用于分配 500 MB RAM。

=> CREATE LIBRARY mylib AS '/home/dbadmin/MemoryAllocationExample.so';
CREATE LIBRARY
=> CREATE FUNCTION usemem AS NAME 'MemoryAllocationExampleFactory' LIBRARY mylib
-> FENCED;
CREATE FUNCTION
=> SELECT usemem(1,2);
 usemem
--------
      3
(1 row)

以下语句演示了将会话的 MEMORYCAP 设置为比 UDSF 报告其使用的内存数量更低的值。此操作会导致 Vertica 在执行 UDSF 之前返回错误。

=> SET SESSION MEMORYCAP '100M';
SET
=> SELECT usemem(1,2);
ERROR 3596:  Insufficient resources to execute plan on pool sysquery
[Request exceeds session memory cap: 520328KB > 102400KB]
=> SET SESSION MEMORYCAP = default;
SET

如果 UDx 所需的内存量超过池中的可用容量, 资源池还会阻止该 UDx 运行。以下语句演示了所创建和使用的资源池具有太少内存而导致 UDSF 无法运行的结果。与会话的 MAXMEMORYCAP 限制相似,池的 MAXMEMORYSIZE 设置可防止 Vertica 执行包含该 UDSF 的查询。

=> CREATE RESOURCE POOL small MEMORYSIZE '100M' MAXMEMORYSIZE '100M';
CREATE RESOURCE POOL
=> SET SESSION RESOURCE POOL small;
SET
=> CREATE TABLE ExampleTable(a int, b int);
CREATE TABLE
=> INSERT /*+direct*/ INTO ExampleTable VALUES (1,2);
 OUTPUT
--------
      1
(1 row)
=> SELECT usemem(a, b) FROM ExampleTable;
ERROR 3596:  Insufficient resources to execute plan on pool small
[Request Too Large:Memory(KB) Exceeded: Requested = 523136, Free = 102400 (Limit = 102400, Used = 0)]
=> DROP RESOURCE POOL small; --Dropping the pool resets the session's pool
DROP RESOURCE POOL

最后,将 FencedUDxMemoryLimitMB 配置参数设置为比 UDx 实际分配的内存数量更低的值会导致该 UDx 抛出异常。此示例与前两个示例之一是不同情况,因为实际上会执行该查询。UDx 的代码需要捕获并处理该异常。在此示例中,该代码使用 vt_report_error 宏将错误报告回 Vertica 并退出。

=> ALTER DATABASE DEFAULT SET FencedUDxMemoryLimitMB = 300;

=> SELECT usemem(1,2);
    ERROR 3412:  Failure in UDx RPC call InvokeSetup(): Error calling setup() in
    User Defined Object [usemem] at [MemoryAllocationExample.cpp:32], error code:
     1, message: Couldn't allocate memory :[std::bad_alloc]

=> ALTER DATABASE DEFAULT SET FencedUDxMemoryLimitMB = -1;

=> SELECT usemem(1,2);
 usemem
--------
      3
(1 row)

另请参阅

5.1.4 - Java SDK

Vertica SDK 支持编写除聚合函数之外的所有类型的 Java UDx。所有 Java UDx 均被隔离。

您可以下载、编译和运行示例;请参阅下载并运行 UDx 示例代码。运行示例是验证您的开发环境是否具有所有需要的库的好方法。

如果您无权访问 Vertica 测试环境,则可以在开发计算机上安装 Vertica 并运行单个节点。每次重建 UDx 库时,都需要将其重新安装到 Vertica。下图说明了典型开发周期。

此部分涵盖适用于所有 UDx 类型的特定于 Java 的主题。有关适用于所有语言的信息,请参阅实参和返回值UDx 参数错误、警告和日志记录处理取消请求和特定 UDx 类型的章节。如需完整的 API 文档,请参阅 Java SDK 文档。

5.1.4.1 - 设置 Java SDK

Vertica Java 软件开发工具包 (SDK) 将作为服务器安装的一部分分发。它包含创建 UDx 库所需的源文件和 JAR 文件。有关可以编译和运行的示例,请参阅下载并运行 UDx 示例代码

要求

至少需要在开发计算机上安装下列资源:

  • 与您已在数据库主机上安装的 Java 版本相匹配的 Java 开发工具包 (JDK) 版本(请参阅在 Vertica 主机上安装 Java)。

  • Vertica SDK 的副本。

或者,您可以使用内部版本管理工具(例如 make)来简化开发。

SDK 文件

要使用 SDK,您需要 Java 支持包中的两个文件:

  • /opt/vertica/bin/VerticaSDK.jar 包含 Vertica Java SDK 和其他支持文件。

  • /opt/vertica/sdk/BuildInfo.java 包含有关 SDK 的版本信息。您必须编译此文件并将其包含到 Java UDx JAR 文件中。

如果您不在数据库节点上进行开发,则可以从数据库节点之一将这两个文件复制到开发系统。

用来编译 UDx 的 BuildInfo.javaVerticaSDK.jar 文件必须来自同一个 SDK 版本。这两个文件还必须与 Vertica 主机上的 SDK 文件的版本匹配。只有不在 Vertica 主机上编译 UDx 时,版本控制才会出现问题。如果要在单独的开发系统上进行编译,请始终刷新这两个文件的副本,并在部署之前重新编译 UDx。

开发 UDx 时,计划使用的 SDK 版本必须与其所在的数据库版本相同。要显示当前安装在系统上的 SDK 版本,请在 vsql 中运行以下命令:

=> SELECT sdk_version();

编译 BuildInfo.java

您需要将 BuildInfo.java 文件编译到类文件中,以便可以将该文件包含到 Java UDx JAR 库中。如果要使用 Vertica 节点作为开发系统,您可以执行以下任一操作:

  • BuildInfo.java 文件复制到主机上的其他位置。

  • 如果您没有 root 权限,请在原位置编译 BuildInfo.java 文件。(只有 root 用户拥有将文件写入到 /opt/vertica/sdk 目录的权限。)

使用以下命令编译文件。将 path 替换为文件的路径,并将 output-directory 替换为将在其中编译 UDx 的目录。

$ javac -classpath /opt/vertica/bin/VerticaSDK.jar \
      /path/BuildInfo.java -d output-directory

如果使用诸如 Eclipse 等 IDE,则您可以将 BuildInfo.java 文件包含到项目中,而不必单独编译此文件。您还必须将 VerticaSDK.jar 文件添加到项目的构建路径中。有关如何将文件和库包含到项目中的详细信息,请参阅 IDE 的文档。

运行示例

您可以从 GitHub 存储库下载示例(请参阅下载并运行 UDx 示例代码)。编译和运行示例有助于确保正确设置开发环境。

如果您尚未这样做,请将 JAVA_HOME 环境变量设置为您的 JDK(而非 JRE)目录。

要编译所有示例(包括 Java 示例),请在示例目录下的 Java-and-C++ 目录中执行以下命令:

$ make

要仅编译 Java 示例,请在示例目录下的 Java-and-C++ 目录中执行以下命令:

$ make JavaFunctions

5.1.4.2 - 编译并打包 Java 库

您需要先编译 Java UDx 并将其打包到 JAR 文件中,然后才能开始使用。

SDK 示例包括一个工作生成文件。请参阅下载并运行 UDx 示例代码

编译 Java UDx

编译 Java UDx 源文件时,您需要将 SDK JAR 文件包含到 CLASSPATH 中,以便 Java 编译器可以解析 Vertica API 调用。如果要使用位于数据库群集中的主机上的命令行 Java 编译器,请输入以下命令:

$ javac -classpath /opt/vertica/bin/VerticaSDK.jar factorySource.java \
      [functionSource.java...] -d output-directory

如果所有源文件均位于同一个目录中,则您可以在命令行中使用 *.java,而不必逐个列出文件。

如果要使用 IDE,请验证 VerticaSDK.jar 文件的副本是否位于构建路径中。

UDx 类文件组织

编译 UDx 之后,您必须将其类文件和 BuildInfo.class 文件打包到 JAR 文件中。

要使用打包在 JDK 中的 jar 命令,您必须将 UDx 类文件组织成与类包结构匹配的目录结构。例如,假设 UDx 的工厂类的完全限定名称为 com.mycompany.udfs.Add2ints。在这种情况下,类文件必须位于目录层次结构 com/mycompany/udfs(相对于项目的基本目录)中。此外,您还必须将 BuildInfo.class 文件的副本放到路径 com/vertica/sdk 中,以便可以将该副本包含到 JAR 文件中。此类必须存在于 JAR 文件中,以指示用于编译 Java UDx 的 SDK 版本。

Add2ints UDSF 示例的 JAR 文件在编译后具有以下目录结构:

com/vertica/sdk/BuildInfo.class
com/mycompany/example/Add2intsFactory.class
com/mycompany/example/Add2intsFactory$Add2ints.class

将 UDx 打包到 JAR 文件中

要从命令行创建 JAR 文件,请执行下列操作:

  1. 更改为项目的根目录。

  2. 使用 jar 命令打包 BuildInfo.class 文件和 UDx 中的所有类:

    # jar -cvf libname.jar com/vertica/sdk/BuildInfo.class \
           packagePath/*.class
    

    键入此命令时,libname 表示为 JAR 文件选择的文件名(您可以随意选择任何名称),packagePath 表示包含 UDx 的类文件的目录的路径。

    • 例如,要打包 Add2ints 示例中的文件,请使用以下命令:

      # jar -cvf Add2intsLib.jar com/vertica/sdk/BuildInfo.class \
      com/mycompany/example/*.class
      
    • 更简单来说,如果已将 BuildInfo.class 和类文件打包到同一个根目录中,则您可以使用以下命令:

      # jar -cvf Add2intsLib.jar .
      

    您必须将构成 UDx 的所有类文件包含到 JAR 文件中。UDx 始终包含至少两个类(工厂类和函数类)。即使您已将函数类定义为工厂类的内部类,Java 也会为内部类生成单独的类文件。

将 UDx 打包到 JAR 文件之后,您便已准备好将其部署到 Vertica 数据库。

5.1.4.3 - 处理 Java UDx 依赖项

如果 Java UDx 依赖一个或多个外部库,您可以通过以下三种方法之一处理依赖项:

  • 使用工具(例如,One-JAR 或 Eclipse Runnable JAR Export Wizard)将 JAR 文件捆绑到 UDx JAR 文件中。

  • 将 JAR 文件解包,然后将其内容重新打包到 UDx 的 JAR 文件中。

  • 将库复制到 Vertica 群集和 UDx 库。然后使用 CREATE LIBRARY 语句的 DEPENDS 关键字向 Vertica 说明 UDx 库依赖外部库。此关键字用作特定于库的 CLASSPATH 设置。Vertica 会将支持库分发给群集中的所有节点,并设置 UDx 的类路径以便能够找到这些支持库。

    如果 UDx 依赖本地库(SO 文件),请使用 DEPENDS 关键字指定其路径。调用 UDx 中的 System.loadLibrary(使用本地库之前必须执行此操作)时,此函数使用 DEPENDS 路径来查找这些库。您不需要另外设置 LD_LIBRARY_PATH 环境变量。

外部库示例

以下示例演示了将外部库与 Java UDx 结合使用。

以下示例代码定义了一个名为 VowelRemover 的简单类。此类包含名为 removevowels 的单个方法,该方法将从字符串中移除所有元音字母(字母 aeiouy)。

package com.mycompany.libs;

public class VowelRemover {
    public String removevowels(String input) {
        return input.replaceAll("(?i)[aeiouy]", "");
    }
};

可以使用以下命令编译此类并将其打包到 JAR 文件中:

$ javac -g com/mycompany/libs/VowelRemover.java
$ jar cf mycompanylibs.jar com/mycompany/libs/VowelRemover.class

以下代码定义了一个名为 DeleteVowels Java UDSF,它使用在以上示例代码中定义的库。 DeleteVowels 接受单个 VARCHAR 作为输入,并返回 VARCHAR。

package com.mycompany.udx;
// Import the support class created earlier
import com.mycompany.libs.VowelRemover;
// Import the Vertica SDK
import com.vertica.sdk.*;

public class DeleteVowelsFactory extends ScalarFunctionFactory {

    @Override
    public ScalarFunction createScalarFunction(ServerInterface arg0) {
        return new DeleteVowels();
    }

    @Override
    public void getPrototype(ServerInterface arg0, ColumnTypes argTypes,
            ColumnTypes returnTypes) {
        // Accept a single string and return a single string.
        argTypes.addVarchar();
        returnTypes.addVarchar();
    }

    @Override
    public void getReturnType(ServerInterface srvInterface,
            SizedColumnTypes argTypes,
            SizedColumnTypes returnType){
        returnType.addVarchar(
        // Output will be no larger than the input.
        argTypes.getColumnType(0).getStringLength(), "RemovedVowels");
    }

    public class DeleteVowels extends ScalarFunction
    {
        @Override
        public void processBlock(ServerInterface arg0, BlockReader argReader,
                BlockWriter resWriter) throws UdfException, DestroyInvocation {

            // Create an instance of the  VowelRemover object defined in
            // the library.
            VowelRemover remover = new VowelRemover();

            do {
                String instr = argReader.getString(0);
                // Call the removevowels method defined in the library.
                resWriter.setString(remover.removevowels(instr));
                resWriter.next();
            } while (argReader.next());
        }
    }

}

可以使用以下命令构建示例 UDSF 并将其打包到 JAR 中:

  • 第一个 javac 命令可编译 SDK 的 BuildInfo 类。Vertica 要求所有 UDx 库包含此类。此 javac 命令的 -d 选项可在 UDSF 源的目录结构中输出类文件。

  • 第二个 javac 命令可编译 UDSF 类。此命令可将先前创建的 mycompanylibs.jar 文件添加到类路径,以便编译器能够找到 VowelRemover 类。

  • jar 命令可将 BuildInfo 和 UDx 库的类打包到一起。

$ javac -g -cp /opt/vertica/bin/VerticaSDK.jar\
   /opt/vertica/sdk/com/vertica/sdk/BuildInfo.java -d .
$ javac -g -cp mycompanylibs.jar:/opt/vertica/bin/VerticaSDK.jar\
  com/mycompany/udx/DeleteVowelsFactory.java
$ jar cf DeleteVowelsLib.jar com/mycompany/udx/*.class \
   com/vertica/sdk/*.class

要安装 UDx 库,您必须将两个 JAR 文件同时复制到 Vertica 群集中的节点。然后连接到该节点以执行 CREATE LIBRARY 语句。

以下示例演示了如何在复制 JAR 文件之后将 UDx 库加载到 dbadmin 用户的主目录。DEPENDS 关键字可向 Vertica 说明 UDx 库依赖 mycompanylibs.jar 文件。

=> CREATE LIBRARY DeleteVowelsLib AS
   '/home/dbadmin/DeleteVowelsLib.jar' DEPENDS '/home/dbadmin/mycompanylibs.jar'
   LANGUAGE 'JAVA';
CREATE LIBRARY
=> CREATE FUNCTION deleteVowels AS language 'java' NAME
  'com.mycompany.udx.DeleteVowelsFactory' LIBRARY DeleteVowelsLib;
CREATE FUNCTION
=> SELECT deleteVowels('I hate vowels!');
 deleteVowels
--------------
  ht vwls!
(1 row)

5.1.4.4 - Java 和 Vertica 数据类型

Vertica Java SDK 将Vertica 的原生数据类型转换为相应的 Java 数据类型。下表列出了 Vertica 数据类型及其相应的 Java 数据类型。

设置 BINARY、VARBINARY 和 LONG VARBINARY 值

Vertica BINARY、VARBINARY 和 LONG VARBINARY 数据类型将转换为 Java UDx SDK 的 VString 类。您还可以通过 PartitionWriter.setStringBytes() 方法使用 ByteBuffer 对象(或包装在 ByteBuffer 中的字节数组)来设置具有这些数据类型的列的值。有关详细信息,请参阅 PartitionWriter.setStringBytes() 的 Java API UDx 条目。

时间戳和时区

当 SDK 将 Vertica 时间戳转换为 Java 时间戳时,它使用 JVM 的时区。如果 JVM 运行的时区与 Vertica 使用的时区不同,则结果可能会令人困惑。

Vertica 以 UTC 格式将时间戳存储在数据库中。(如果设置了数据库时区,则在查询时完成转换。)为了防止来自 JVM 时区的错误,请将以下代码添加到 UDx 的处理方法中:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

字符串

Java SDK 包含一个名为 StringUtils 的类,此类可帮助处理字符串数据。getStringBytes() 方法是此类的较有用的功能之一。此方法可以从 String 提取字节并防止创建无效字符串。如果尝试提取会将多字节 UTF-8 字符拆分为多个部分的子字符串,getStringBytes() 会将该子字符串截断为最近似的完整字符。

5.1.4.5 - 处理 NULL 值

UDx 必须准备好处理 NULL 值。这些值通常必须与正则值分开进行处理。

读取 NULL 值

UDx 将从 BlockReader 类或 PartitionReader 类的实例读取数据。如果列的值为 NULL,则用来获取数据的方法(例如 getLong)将返回 Java null 引用。如果您在未检查 NULL 的情况下尝试使用该值,Java 运行时将抛出空指针异常。

您可以在读取列之前使用特定于数据类型的方法(例如,isLongNullisDoubleNullisBooleanNull)测试 null 值。例如,要测试 UDx 输入的第一列的 INTEGER 数据类型是否为 NULL,请使用以下语句:

// See if the Long value in column 0 is a NULL
if (inputReader.isLongNull(0)) {
    // value is null
    . . .

写入 NULL 值

可以使用特定于类型的方法(例如 setLongNullsetStringNull)在 BlockWriter 类和 PartitionWriter 类上输出 NULL 值。这些方法使用列号来接收 NULL 值。此外,PartitionWriter 类具有特定于数据类型的设置值方法(例如 setLongValuesetStringValue)。如果向这些方法传递某个值,这些方法会将输出列设置为该值。如果向这些方法传递 Java null 引用,这些方法会将输出列设置为 NULL。

5.1.4.6 - 将元数据添加到 Java UDx 库

您可以将诸如作者姓名、库版本以及库描述之类的元数据添加到您的库。此元数据可以让您跟踪在 Vertica 分析数据库群集上部署的函数的版本并让您函数的第三方用户了解该函数的创建者。库加载到 Vertica 分析数据库编录之后,库的元数据会出现在 USER_LIBRARIES 系统表中。

要向 Java UDx 库添加元数据,请创建 UDXLibrary 类(包括库的元数据)的子类。然后,您可以将该类包含到 JAR 文件中。使用 CREATE LIBRARY 语句将类加载到 Vertica 分析数据库编录时,请查找 UDXLibrary 的子类以获取库的元数据。

UDXLibrary 的子类中,您需要实施八个 getter,它们将返回包含库的元数据的字符串值。此类中的 getter 如下:

  • getAuthor() 将返回您要用来与创建的库关联的名称(例如,您自己的名字或您的公司的名称)。

  • getLibraryBuildTag() 将返回您要用来表示库的特定内部版本的任何字符串(例如,SVN 修订号或编译该库时的时间戳)。开发库实例时,跟踪这些库实例很有用。

  • getLibraryVersion() 将返回库的版本。您可以使用想使用的任何编号或命名方案。

  • getLibrarySDKVersion() 将返回您为其编译了库的 Vertica 分析数据库 SDK 库的版本。

  • getSourceUrl() 将返回一个 URL,函数用户可以通过此 URL 找到有关该函数的详细信息。这可以是您公司的网站、托管库源代码的 GitHub 页面或您喜欢的任何站点。

  • getDescription() 将返回库的简要描述。

  • getLicensesRequired() 将返回用于许可信息的占位符。您必须为此值传递一个空字符串。

  • getSignature() 将返回用于签名(此签名将对库进行身份验证)的占位符。您必须为此值传递一个空字符串。

例如,以下代码演示了创建 UDXLibrary 子类并将其包含到 Add2Ints UDSF 示例 JAR 文件中(请参阅任何 Vertica 代码上的 /opt/vertica/sdk/examples/JavaUDx/ScalarFunctions)。

// Import the UDXLibrary class to hold the metadata
import com.vertica.sdk.UDXLibrary;

public class Add2IntsLibrary extends UDXLibrary
{
    // Return values for the metadata about this library.

    @Override public String getAuthor() {return "Whizzo Analytics Ltd.";}
    @Override public String getLibraryBuildTag() {return "1234";}
    @Override public String getLibraryVersion() {return "1.0";}
    @Override public String getLibrarySDKVersion() {return "7.0.0";}
    @Override public String getSourceUrl() {
        return "http://example.com/add2ints";
    }
    @Override public String getDescription() {
        return "My Awesome Add 2 Ints Library";
    }
    @Override public String getLicensesRequired() {return "";}
    @Override public String getSignature() {return "";}
}

当加载包含 Add2IntsLibrary 类的库时,元数据将显示在 USER_LIBRARIES 表中:

=> CREATE LIBRARY JavaAdd2IntsLib AS :libfile LANGUAGE 'JAVA';
CREATE LIBRARY
=> CREATE FUNCTION JavaAdd2Ints as LANGUAGE 'JAVA'  name 'com.mycompany.example.Add2IntsFactory' library JavaAdd2IntsLib;
CREATE FUNCTION
=> \x
Expanded display is on.
=> SELECT * FROM USER_LIBRARIES WHERE lib_name = 'JavaAdd2IntsLib';
-[ RECORD 1 ]-----+---------------------------------------------
schema_name       | public
lib_name          | JavaAdd2IntsLib
lib_oid           | 45035996273869844
author            | Whizzo Analytics Ltd.
owner_id          | 45035996273704962
lib_file_name     | public_JavaAdd2IntsLib_45035996273869844.jar
md5_sum           | f3bfc76791daee95e4e2c0f8a8d2737f
sdk_version       | v7.0.0-20131105
revision          | 125200
lib_build_tag     | 1234
lib_version       | 1.0
lib_sdk_version   | 7.0.0
source_url        | http://example.com/add2ints
description       | My Awesome Add 2 Ints Library
licenses_required |
signature         |

5.1.4.7 - Java UDx 资源管理

当启动时,Java 虚拟机 (JVM) 会分配固定数量的内存。此固定内存分配机制会使 Java UDx 的内存管理复杂化,因为 UDx 在处理数据时无法动态分配和释放内存。与此不同的是,C++ UDx 可以动态分配资源。

为了控制 Java UDx 所占用的内存量,Vertica 包含一个名为 jvm 的内存池,它使用该池为 JVM 分配内存。如果该内存池已用完,则在该池中有足够内存可用于启动新的 JVM 之前,调用 Java UDx 的查询会阻塞。

默认情况下,jvm 池存在以下情况:

  • 由于没有分配给自己的内存,它会从 GENERAL 池借用内存。

  • 其 MAXMEMORYSIZE 设置为系统内存的 10% 或 2 GB(以较小者为准)。

  • 其 PLANNEDCONCURRENCY 设置为 AUTO,因此它会继承 GENERAL 池的 PLANNEDCONCURRENCY 设置。

您可以通过查询 RESOURCE_POOLS 表来查看 jvm 池的当前设置:

=> SELECT MAXMEMORYSIZE,PLANNEDCONCURRENCY FROM V_CATALOG.RESOURCE_POOLS WHERE NAME = 'jvm';
 MAXMEMORYSIZE | PLANNEDCONCURRENCY
---------------+--------------------
 10%           | AUTO

当 SQL 语句调用 Java UDx 时,Vertica 会检查 jvm 内存池是否具有足够内存可用于启动新的 JVM 实例以执行函数调用。在启动每个新的 JVM 时,Vertica 会将其堆内存大小设置为大约 jvm 池的 MAXMEMORYSIZE 参数除以其 PLANNEDCONCURRENCY 参数。如果内存池不包含足够内存,则在另一个 JVM 退出并将其内存返回到池中之前,查询会阻塞。

如果 Java UDx 尝试使用的内存多于已分配给 JVM 堆大小的内存,它会退出并显示错误。您可以尝试通过以下方法解决此问题:

  • 增加 jvm 池的 MAXMEMORYSIZE 参数。

  • 减少 jvm 池的 PLANNEDCONCURRENCY 参数。

  • 更改 Java UDx 的代码以使用更少内存。

调整 jvm 池

根据需求调整 jvm 池,您必须考虑以下两个因素:

  • Java UDx 运行所需的 RAM 容量

  • 您希望数据库运行多少个并发 Java UDx 函数

您可以使用几种方法来了解 Java UDx 所需的内存数量。例如,您的代码可以使用 Java 的 Runtime 类来获取已分配的总内存的估算值并使用 ServerInterface.log() 记录该值。(此类的一个实例将传递给您的 UDx。)如果数据库中有多个 Java UDx,请基于使用最多内存的 UDx 设置 jvm 池的内存大小。

需要运行 Java UDx 的并发会话数不能与全局 PLANNEDCONCURRENCY 设置相同。例如,您可能只有一个用户运行 Java UDx,这意味着您可以将 jvm 池的 PLANNEDCONCURRENCY 设置减少至 1。

在获取 RAM 数量的估算值和需要运行 Java UDX 的并发用户会话数之后,您可以将 jvm 池调整为适当大小。将该池的 MAXMEMORYSIZE 设置为需要最多资源的 Java UDx 所需的最大 RAM 容量乘以运行 Java UDx 所需的并发用户会话数。将该池的 PLANNEDCONCURENCY 设置为运行 Java Udx 所需的并发用户会话数。

例如,假设您的 Java UDx 最多需要 4 GB 内存才能运行,并且您希望最多有两个用户会话使用 Java UDx。您应使用以下命令调整 jvm 池:

=> ALTER RESOURCE POOL jvm MAXMEMORYSIZE '8G' PLANNEDCONCURRENCY 2;

MEMORYSIZE 已设置为 8 GB,即 Java UDx 使用的最大内存 (4 GB) 乘以并发用户会话数(2 个)。

有关对 jvm 和其他资源池进行优化的详细信息,请参阅管理工作负载

释放 JVM 内存

当用户在其会话期间第一次调用 Java UDx 时,Vertica 会分配 jvm 池中的内存并启动新的 JVM。只要用户会话处于打开状态,此 JVM 就会保持运行,以便可以处理其他 Java UDx 调用。让 JVM 保持运行可以减少由同一个会话执行多个 Java Udx 所产生的开销。如果 JVM 不保持处于打开状态,则对 Java UDx 的每次调用都会导致 Vertica 需要耗费更多时间来分配资源和启动新的 JVM。但是,让 JVM 保持处于打开状态意味着无论是否会再次使用 JVM 的内存,该内存在会话的有效期内都会保持处于已分配状态。

如果减小 jvm 内存池,则包含 Java UDx 的查询会在内存变为可用之前阻塞,或者最终会由于缺少资源而失败。如果发现查询由于此原因而阻塞或失败,您可以向 jvm 池分配更多内存并增加其 PLANNEDCONCURRENCY。另一个选项是要求用户在他们不再需要运行 Java UDx 时调用 RELEASE_JVM_MEMORY 函数。此函数可关闭属于该用户会话的任何 JVM 并将其已分配的内存恢复到 jvm 内存池中。

以下示例演示了查询 V_MONITOR.SESSIONS 以确定所有会话已分配给 JVM 内存。此示例还演示了如何通过调用 Java UDx 来分配内存以及如何通过调用 RELEASE_JVM_MEMORY 来释放内存。

=> SELECT USER_NAME,EXTERNAL_MEMORY_KB FROM V_MONITOR.SESSIONS;
 user_name | external_memory_kb
-----------+---------------
 dbadmin   |             0
(1 row)

=> -- Call a Java UDx
=> SELECT add2ints(123,456);
 add2ints
----------
      579
(1 row)
=> -- JVM is now running and memory is allocated to it.
=> SELECT USER_NAME,EXTERNAL_MEMORY_KB FROM V_MONITOR.SESSIONS;
 USER_NAME | EXTERNAL_MEMORY_KB
-----------+---------------
 dbadmin   |         79705
(1 row)

=> -- Shut down the JVM and deallocate memory
=> SELECT RELEASE_JVM_MEMORY();
           RELEASE_JVM_MEMORY
-----------------------------------------
 Java process killed and memory released
(1 row)

=> SELECT USER_NAME,EXTERNAL_MEMORY_KB FROM V_MONITOR.SESSIONS;
 USER_NAME | EXTERNAL_MEMORY_KB
-----------+---------------
 dbadmin   |             0
(1 row)

在极少数情况下,您可能需要关闭所有 JVM。例如,您可能需要为重要查询释放内存,或者 Java UDx 的多个实例可能需要太长时间才能完成。您可以使用 RELEASE_ALL_JVM_MEMORY 关闭所有用户会话中的所有 JVM:

=> SELECT USER_NAME,EXTERNAL_MEMORY_KB FROM V_MONITOR.SESSIONS;
  USER_NAME  | EXTERNAL_MEMORY_KB
-------------+---------------
 ExampleUser |         79705
 dbadmin     |         79705
(2 rows)

=> SELECT RELEASE_ALL_JVM_MEMORY();
                           RELEASE_ALL_JVM_MEMORY
-----------------------------------------------------------------------------
 Close all JVM sessions command sent. Check v_monitor.sessions for progress.
(1 row)

=> SELECT USER_NAME,EXTERNAL_MEMORY_KB FROM V_MONITOR.SESSIONS;
 USER_NAME | EXTERNAL_MEMORY_KB
-----------+---------------
 dbadmin   |             0
(1 row)

注意

  • jvm 资源池仅用于为语句中的 Java UDx 函数分配内存。SQL 语句所需的其余资源来自其他内存池。

  • 第一次调用 Java UDx 时,Vertica 会启动 JVM 以执行某些 Java 方法,从而在查询规划阶段获取有关 UDx 的元数据。此 JVM 的内存也是从 jvm 内存池获取的。

5.1.5 - Python SDK

Vertica SDK 支持在 Python 3 中编写某些类型的 UDx。

Python SDK 不需要任何额外的系统配置或头文件。由于开销较低,您可以在短时间内开发新功能并将其部署到您的 Vertica 群集。

以下工作流是 Python SDK 的典型工作流:

由于 Python 含有解释器,因此您不必在 Vertica 中加载 UDx 之前编译您的程序。但是,您应该在创建函数并开始在 Vertica 中测试函数后对代码进行一些调试。

当 Vertica 调用您的 UDx 时,它会启动一个对服务器和 Python 解释器之间的交互进行管理的从属进程。

此部分涵盖适用于所有 UDx 类型且特定于 Python 的主题。有关适用于所有语言的信息,请参阅实参和返回值UDx 参数错误、警告和日志记录处理取消请求和特定 UDx 类型的章节。如需完整的 API 文档,请参阅 Python SDK

5.1.5.1 - Python 库

在可以使用 Python UDx 之前,您需要验证它是否满足以下库要求:

  • 您的 UDx 必须在代码中导入“vertica_sdk”包。您无需下载此包。它是 Vertica 服务器的一部分。
    python import vertica_sdk
  • Vertica Python SDK 包含 Python 标准库。如果您的 UDx 依赖于其他库,则必须使用 CREATE LIBRARY 将它们添加为依赖项。您不能只是简单地导入它们。

5.1.5.2 - Python 和 Vertica 数据类型

Vertica Python SDK 会将原生 Vertica 数据类型转换为相应的 Python 数据类型。下表描述了一些数据类型转换。有关完整列表以及用于转换和处理这些数据类型的 helper 函数的列表,请参阅 Python SDK

有关 SDK 支持的复杂数据类型的信息,请参阅作为实参的复杂类型和返回值

5.1.6 - R SDK

Vertica R SDK 扩展了 Vertica 分析数据库的功能,因此您可以利用其他 R 库。开始在 R 中开发用户定义的扩展 (UDx) 之前,您必须在群集中的每个节点上安装适用于 Vertica 的 R 语言包。R SDK 在隔离模式下支持标量和转换函数。其他 UDx 类型不受支持。

以下工作流是 R SDK 的典型工作流:

您可以在 Vertica R SDK 中找到所有类的详细文档。

5.1.6.1 - 安装/升级 Vertica 的 R 语言包

要在 Vertica 中创建 R UDx,请安装与您的服务器版本匹配的 R 语言包。R 语言包含有用来与 Vertica 交互的 R 运行时和关联库。您必须使用此版本的 R 运行时;不能升级它。

您必须在群集中的每个节点上安装 R 语言包。Vertica R 语言包必须是节点上安装的唯一 R 语言包。

Vertica R 语言包先决条件

R 语言包需要许多包才能进行安装和执行。这些依赖项的名称因 Linux 发行版而异。对于支持 Vertica 的 Linux 平台,这些包为:

  • RHEL/CentOS:libfortranxz-libslibgomp

  • SUSE Linux Enterprise Server:libfortran3liblzma5libgomp1

  • Debian/Ubuntu:libfortran3liblzma5libgomp1

  • Amazon Linux 2.0:compat-gcc-48-libgfortranxz-libslibgomp

Vertica 需要高于 7.1 的 libgfortran4 库版本才能创建 R 扩展。libgfortran 库默认包含在 devtoolgcc 包中。

安装 Vertica R 语言包

如果您使用操作系统包管理器而不是 rpm 或 dpkg 命令进行安装,则无需手动安装 R 语言包。适用于每个受支持的 Linux 版本的本机包管理器为:

  • RHEL/CentOS:yum

  • SUSE Linux Enterprise Server:zypper

  • Debian/Ubuntu:apt-get

  • Amazon Linux 2.0:yum

  1. 通过浏览 Vertica 网站,下载 R 语言包。

  2. 支持 (Support) 选项卡上,选择客户下载 (Customer Downloads)

  3. 在系统出现提示时,使用您的 Micro Focus 凭据登录。

  4. 找到并选择适用于您所用的服务器版本的 vertica-R-lang_version.rpmvertica-R-lang_version.deb 文件。R 语言包版本必须与服务器版本在三个小数点上匹配。

  5. 以 root 身份或使用 sudo 安装包:

    • RHEL/CentOS

      $ yum install vertica-R-lang-<version>.rpm
      
    • SUSE Linux Enterprise Server

      $ zypper install vertica-R-lang-<version>.rpm
      
    • Debian

      $ apt-get install ./vertica-R-lang_<version>.deb
      
    • Amazon Linux 2.0

       $ yum install vertica-R-lang-<version>.AMZN.rpm
      

安装程序会将 R 二进制文件放入 /opt/vertica/R

升级 Vertica R 语言包

升级时,您已手动安装的某些 R 包可能无法正常工作且可能需要重新安装。如果不更新包,R 将在包无法使用时返回错误。升级这些包的说明如下所述。

  1. 在升级 Vertica 之前,您必须卸载 R 语言包。卸载语言包时,已手动安装的所有其他 R 包会一直留在 /opt/vertica/R 中,而不会被移除。

  2. 按照将 Vertica 升级到新版本中的详细说明升级您的服务器包。

  3. 更新服务器包后,在每个主机上安装新的 R 语言包。

如果已在每个节点上安装了其他 R 包:

  1. 以 root 身份运行 /opt/vertica/R/bin/R 并执行以下命令:

    > update.packages(checkBuilt=TRUE)
    
  2. 从显示的列表中选择 CRAN 镜像。

  3. 系统会提示您更新具有可用更新的每个包。您必须更新已手动安装且与 R 语言包中的当前 R 版本不兼容的所有包。
    更新:

    • Rcpp

    • Rinside

    此时即会安装选择进行更新的包。使用以下命令退出 R:

    > quit()
    

使用 R 语言编写的 Vertica UDx 函数无需编译,而且升级后无需重新加载 Vertica-R 库和函数。

5.1.6.2 - R 包

除了与 R 捆绑在一起的默认包之外,Vertica R 语言包还包含以下 R 包:

  • Rcpp

  • RInside

  • IpSolve

  • lpSolveAPI

您可以使用以下两种方法之一来安装不包含在 Vertica R 语言包中的其他 R 包。您必须在所有节点上安装相同的包。

安装 R 包

您可以使用以下两种方法之一安装其他 R 包。

使用 install.packages() R 命令:

$ sudo /opt/vertica/R/bin/R
> install.packages("Zelig");

使用 CMD INSTALL:

/opt/vertica/R/bin/R CMD INSTALL <path-to-package-tgz>

安装的包位于:/opt/vertica/R/library

5.1.6.3 - R 和 Vertica 数据类型

将数据传递到 R UDx 或从中传递数据时,支持以下数据类型:

发送到 R 函数时,Vertica 中的 NULL 值会转换为 R NA 值。当从 R 函数返回到 Vertica 时,R NA 值会转换为 Vertica null 值。

5.1.6.4 - 将元数据添加到 R 库

您可以将诸如作者姓名、库版本以及库描述之类的元数据添加到您的库。此元数据可以让您跟踪在 Vertica 分析数据库群集上部署的函数的版本并让您函数的第三方用户了解该函数的创建者。库加载到 Vertica 分析数据库编录之后,库的元数据会出现在 USER_LIBRARIES 系统表中。

可通过在 UDx 的一个源文件中调用 RegisterLibrary() 函数为库声明元数据。如果 UDx 的源文件中存在多个函数调用,则使用在 Vertica 分析数据库加载库时最后解释的函数调用来确定库的元数据。

RegisterLibrary() 函数采用八个字符串参数:

RegisterLibrary(author,
                library_build_tag,
                library_version,
                library_sdk_version,
                source_url,
                description,
                licenses_required,
                signature);
  • author 包含要与库创建相关联的名称(例如自己的名称或公司名称)。

  • library_build_tag 为用于代表库的特定版本的字符串(例如 SVN 修订号或编译库的时间戳)。开发库实例时,跟踪这些库实例很有用。

  • library_version 为库的版本。您可以使用想使用的任何编号或命名方案。

  • library_sdk_version 是已为其编译了库的 Vertica 分析数据库 SDK 库的版本。

  • source_url 为函数用户可从中查找函数详细信息的 URL。这可以是您公司的网站、托管库源代码的 GitHub 页面或您喜欢的任何站点。

  • description 为库的简要描述。

  • licenses_required 为许可信息占位符。您必须为此值传递一个空字符串。

  • signature 为对库进行身份验证的签名的占位符。您必须为此值传递一个空字符串。

以下示例显示如何将元数据添加到 R UDx。


RegisterLibrary("Speedy Analytics Ltd.",
                "1234",
                "1.0",
                "8.1.0",
                "http://www.example.com/sales_tax_calculator.R",
                "Sales Tax R Library",
                "",
                "")

加载库并查询 USER_LIBRARIES 系统表将显示调用 RegisterLibrary 时提供的元数据:

=> CREATE LIBRARY rLib AS '/home/dbadmin/sales_tax_calculator.R' LANGUAGE 'R';
CREATE LIBRARY
=> SELECT * FROM USER_LIBRARIES WHERE lib_name = 'rLib';
-[ RECORD 1 ]-----+---------------------------------------------------------
schema_name       | public
lib_name          | rLib
lib_oid           | 45035996273708350
author            | Speedy Analytics Ltd.
owner_id          | 45035996273704962
lib_file_name     | rLib_02552872a35d9352b4907d3fcd03cf9700a0000000000d3e.R
md5_sum           | 30da555537c4d93c352775e4f31332d2
sdk_version       |
revision          |
lib_build_tag     | 1234
lib_version       | 1.0
lib_sdk_version   | 8.1.0
source_url        | http://www.example.com/sales_tax_calculator.R
description       | Sales Tax R Library
licenses_required |
signature         |
dependencies      |
is_valid          | t
sal_storage_id    | 02552872a35d9352b4907d3fcd03cf9700a0000000000d3e

5.1.6.5 - 为 R 函数设置 null 输入和可变性行为

Vertica 支持为采用 R 编写的 UDx 定义可变性设置和 null 输入设置。这两项设置都有助于提高 R 函数的性能。

可变性设置

可变性设置向 Vertica 优化器描述了该函数的行为。例如,如果输入数据有相同的行,并且您知道 UDx 是不可变的,则可以将 UDx 定义为 IMMUTABLE。这样即可告知 Vertica 优化器,它可以返回对其调用该函数的后续相同行的缓存值,而不是对每个相同的行都运行该函数。

要指示 UDx 的可变性,请将 R 工厂函数的 volatility 参数设置为以下值之一:

如果未定义可变性,则函数会被视为 VOLATILE。

以下示例会在 multiplyTwoIntsFactory 函数中将可变性设置为 STABLE:

multiplyTwoIntsFactory <- function() {
  list(name                  = multiplyTwoInts,
       udxtype               = c("scalar"),
       intype                = c("float","float"),
       outtype               = c("float"),
       volatility            = c("stable"),
       parametertypecallback = multiplyTwoIntsParameters)
}

Null 输入行为

Null 输入设置决定如何响应包含 null 输入的行。例如,您可以选择在任何输入为 null 时返回 null,而不调用函数并让函数处理 NULL 输入。

要指示 UDx 如何响应 NULL 输入,请将 R 工厂函数的 strictness 参数设置为以下值之一:

如果未定义 null 输入行为,则无论是否存在 NULL 值,系统都会对每行数据调用该函数。

以下示例会在 multiplyTwoIntsFactory 函数中将 NULL 输入行为设置为 STRICT:

multiplyTwoIntsFactory <- function() {
  list(name                  = multiplyTwoInts,
       udxtype               = c("scalar"),
       intype                = c("float","float"),
       outtype               = c("float"),
       strictness            = c("strict"),
       parametertypecallback = multiplyTwoIntsParameters)
}

5.1.7 - 调试提示

以下提示可帮助您在将 UDx 部署到生产环境之前对其进行调试。

使用单个节点进行初始调试

可以使用诸如 gdb 等调试器连接到 Vertica 进程以调试 UDx 代码。但是,在多节点环境中难以执行此调试。因此,请考虑设置单节点 Vertica 测试环境以对 UDx 进行初始调试。

使用日志记录

每个 UDx 都有一个关联的 ServerInterface 实例。ServerInterface 提供了要写入 Vertica 日志和写入系统表(仅在 C++ API 中)的函数。有关详细信息,请参阅日志

5.2 - 实参和返回值

对于除加载 (UDL) 之外的所有 UDx 类型,工厂类会声明关联函数的实参和返回类型。为此,工厂有两种方法:

  • getPrototype() (必需):声明输入和输出类型

  • getReturnType() (有时必需):声明返回类型,包括长度和精度(适用时)

getPrototype() 方法会收到两个 ColumnTypes 参数,一个用于输入,一个用于输出。C++ 示例:字符串分词器 中的工厂接受单个输入字符串并返回字符串:

virtual void getPrototype(ServerInterface &srvInterface,
                          ColumnTypes &argTypes, ColumnTypes &returnType)
{
  argTypes.addVarchar();
  returnType.addVarchar();
}

ColumnTypes 类为每种受支持的类型提供“add”方法,例如 addVarchar()。此类支持具有 addArrayType()addRowType() 方法的复杂类型;请参阅作为实参的复杂类型。如果函数为多态函数,则可以改为调用 addAny()。然后,您负责验证输入和输出。有关实施多态 UDx 的详细信息,请参阅创建多态 UDx

getReturnType() 方法会计算返回值的最大长度。如果 Udx 返回特定大小的列(一种长度可变的返回数据类型,例如 VARCHAR)、需要精度的值或多个值,则需要实施此工厂方法。(某些 UDx 类型要求您实施该方法。)

输入是 SizedColumnTypes,其中包含输入实参类型及其长度。根据输入类型,请将以下值之一添加到输出类型:

  • CHAR、(LONG) VARCHAR、BINARY 和 (LONG) VARBINARY:返回最大长度。

  • NUMERIC 类型:指定精度和小数位数。

  • TIME 和 TIMESTAMP 值(含或不含时区):指定精度。

  • INTERVAL YEAR TO MONTH:指定范围。

  • INTERVAL DAY TO SECOND:指定精度和范围。

  • ARRAY:指定数组元素的最大数量。

使用字符串分词器时,输出为 VARCHAR 且函数可确定其最大长度:

// Tell Vertica what our return string length will be, given the input
// string length
virtual void getReturnType(ServerInterface &srvInterface,
                           const SizedColumnTypes &inputTypes,
                           SizedColumnTypes &outputTypes)
{
  // Error out if we're called with anything but 1 argument
  if (inputTypes.getColumnCount() != 1)
    vt_report_error(0, "Function only accepts 1 argument, but %zu provided", inputTypes.getColumnCount());

  int input_len = inputTypes.getColumnType(0).getStringLength();

  // Our output size will never be more than the input size
  outputTypes.addVarchar(input_len, "words");
}

作为实参的复杂类型和返回值

ColumnTypes 类支持 ARRAYROW 类型。数组包含各种元素,而行包含各种字段,两者均包含您需要描述的类型。要使用复杂类型,您需要为数组或行构建 ColumnTypes 对象,然后将其添加到表示函数输入和输出的 ColumnTypes 对象中。

在以下示例中,转换函数的输入是一个订单数组(即多个行),输出是各个行及其在数组中的位置。订单包含送货地址 (VARCHAR) 和产品 ID 数组 (INT)。

工厂的 getPrototype() 方法会先为数组和行元素创建 ColumnTypes,然后使用它们来调用 addArrayType()addRowType()


void getPrototype(ServerInterface &srv,
            ColumnTypes &argTypes,
            ColumnTypes &retTypes)
    {
        // item ID (int), to be used in an array
        ColumnTypes itemIdProto;
        itemIdProto.addInt();

        // row: order = address (varchar) + array of previously-created item IDs
        ColumnTypes orderProto;
        orderProto.addVarchar();                  /* address */
        orderProto.addArrayType(itemIdProto);     /* array of item ID */

        /* argument (input) is array of orders */
        argTypes.addArrayType(orderProto);

        /* return values: index in the array, order */
        retTypes.addInt();                        /* index of element */
        retTypes.addRowType(orderProto);          /* element return type */
    }

实参包括特定大小的类型 (VARCHAR)。getReturnType() 方法会使用类似的方法,从而使用 Fields 类在订单中构建两个字段。


void getReturnType(ServerInterface &srv,
            const SizedColumnTypes &argTypes,
            SizedColumnTypes &retTypes)
    {
        Fields itemIdElementFields;
        itemIdElementFields.addInt("item_id");

        Fields orderFields;
        orderFields.addVarchar(32, "address");
        orderFields.addArrayType(itemIdElementFields[0], "item_id");
        // optional third arg: max length, default unbounded

        /* declare return type */
        retTypes.addInt("index");
        static_cast<Fields &>(retTypes).addRowType(orderFields, "element");

        /* NOTE: presumably we have verified that the arguments match the prototype, so really we could just do this: */
        retTypes.addInt("index");
        retTypes.addArg(argTypes.getColumnType(0).getElementType(), "element");
    }

要在 UDx 处理方法中访问复杂类型,请使用 ArrayReaderArrayWriterStructReaderStructWriter 类。

请参阅“ C++ 示例:使用复杂类型”,了解使用数组的多态函数。

工厂的 getPrototype() 方法会先使用 makeType()addType() 方法,为行及其元素创建和构造 ColumnTypes。然后,该方法会调用 addType() 方法,以将这些构造的 ColumnTypes 添加到 arg_typesreturn_type 对象:


def getPrototype(self, srv_interface, arg_types, return_type):
    # item ID (int), to be used in an array
    itemIdProto = vertica_sdk.ColumnTypes.makeInt()

    # row (order): address (varchar) + array of previously-created item IDs
    orderProtoFields = vertica_sdk.ColumnTypes.makeEmpty()
    orderProtoFields.addVarchar()  # address
    orderProtoFields.addArrayType(itemIdProto) # array of item ID
    orderProto = vertica_sdk.ColumnTypes.makeRowType(orderProtoFields)

    # argument (input): array of orders
    arg_types.addArrayType(orderProto)

    # return values: index in the array, order
    return_type.addInt();                        # index of element
    return_type.addRowType(orderProto);          # element return type

工厂的 getReturnType() 方法会使用 makeInt()makeEmpty() 方法来创建 SizedColumnTypes,然后使用 addVarchar()addArrayType() 方法构建两个行字段。请注意,addArrayType() 方法会将数组元素的最大数量指定为 1024。 getReturnType() 然后,将这些构造的 SizedColumnTypes 添加到表示返回类型的对象中。


def getReturnType(self, srv_interface, arg_types, return_type):
    itemsIdElementField = vertica_sdk.SizedColumnTypes.makeInt("item_id")

    orderFields = vertica_sdk.SizedColumnTypes.makeEmpty()
    orderFields.addVarchar(32, "address")
    orderFields.addArrayType(itemIdElementField, 1024, "item_ids")

    # declare return type
    return_type.addInt("index")
    return_type.addRowType(orderFields, "element")

    '''
    NOTE: presumably we have verified that the arguments match the prototype, so really we could just do this:
    return_type.addInt("index")
    return_type.addArrayType(argTypes.getColumnType(0).getElementType(), "element")
    '''

要在 UDx 处理方法中访问复杂类型,请使用 ArrayReaderArrayWriterRowReaderRowWriter 类。有关详细信息,请参阅Python SDK

有关使用复杂类型的标量函数,请参阅Python 示例:矩阵乘法

处理不同数量和类型的实参

使用过载或多态性,您可以创建处理多个签名的 UDx,甚至还可以创建接受由用户提供给它们的所有实参的 UDx。

您可以使 UDx 过载,方法是将同一个 SQL 函数名称分配给多个工厂类,每个工厂类会定义一个唯一的函数签名。当用户在查询中使用该函数名称时,Vertica 会尝试将函数调用的签名与工厂的 getPrototype() 方法所声明的签名进行匹配。如果 UDx 需要接受几个不同签名(例如,接受两个必需参数和一个可选参数),则这是可用的最佳技术。

或者,您可以编写一个多态函数,以写入一个工厂方法而非多个工厂方法,并声明它接受任何数量和类型的实参。当用户在查询中使用函数名称时,Vertica 会调用您的函数,而不考虑签名。为换取这种灵活性,UDx 的主要“process”方法必须确定它是否可以接受各种实参并在无法接受时发出错误。

所有 UDx 类型都可以使用多态输入。转换函数和分析函数也可以使用多态输出。这意味着 getPrototype() 可以声明返回类型“any”并在运行时设置实际返回类型。例如,在输入中返回最大值的函数将返回与输入类型相同的类型。

5.2.1 - 重载 UDx

您可能希望 UDx 接受多个不同的签名(参数集)。例如,您可能希望 UDx 接受以下内容:

  • 一个或多个可选参数。

  • 一个或多个参数(可以是多个数据类型之一)。

  • 完全不同的签名(例如,所有 INTEGER 或所有 VARCHAR)。

可以通过以下方法创建具有此行为的函数:创建分别接受不同的签名(参数的个数和数据类型)的多个工厂类。然后,您可以将单个 SQL 函数名称与所有工厂类相关联。只要每个工厂定义的签名都是唯一的,您就可以使用相同的 SQL 函数名称来引用多个工厂类。当用户调用 UDx 时,Vertica 会将用户所提供的实参的数量和类型与函数的每个工厂类所接受的实参进行匹配。如果有一个匹配,Vertica 将使用它来实例化函数类以处理数据。

多个工厂类可以实例化相同的函数类,所以您可以重复使用能够处理多个参数集的一个函数分类,然后为每个函数签名创建工厂类。如果您需要,您还可以创建多个函数类。

请参阅 C++ 示例:重载 UDxJava 示例:重载 UDx 示例。

5.2.1.1 - C++ 示例:重载 UDx

以下示例代码演示了创建一个将两个或三个整数相加的用户定义标量函数 (UDSF)。Add2or3ints 类已准备好处理两个或三个参数。processBlock() 函数将检查已传入的参数个数,并将两个或三个参数全部相加。此外,如果对此函数发出的调用具有少于 2 个参数或具有多于 3 个参数,此函数将退出并显示错误消息。理论上,这不应当发生,因为如果用户的函数调用与您为函数创建的某一个工厂类上的签名相匹配,则 Vertica 仅调用 UDSF。在实践中,如果您的(或其他人的)工厂类不正确地报告函数类无法处理一组参数,最好执行此健全性检查。

#include "Vertica.h"
using namespace Vertica;
using namespace std;
// a ScalarFunction that accepts two or three
// integers and adds them together.
class Add2or3ints : public Vertica::ScalarFunction
{
public:
    virtual void processBlock(Vertica::ServerInterface &srvInterface,
                              Vertica::BlockReader &arg_reader,
                              Vertica::BlockWriter &res_writer)
    {
        const size_t numCols = arg_reader.getNumCols();

        // Ensure that only two or three parameters are passed in
        if ( numCols < 2 || numCols > 3)
            vt_report_error(0, "Function only accept 2 or 3 arguments, "
                                "but %zu provided", arg_reader.getNumCols());
      // Add two integers together
        do {
            const vint a = arg_reader.getIntRef(0);
            const vint b = arg_reader.getIntRef(1);
            vint c = 0;
        // Check for third argument, add it in if it exists.
            if (numCols == 3)
                c = arg_reader.getIntRef(2);
            res_writer.setInt(a+b+c);
            res_writer.next();
        } while (arg_reader.next());
    }
};
// This factory accepts function calls with two integer arguments.
class Add2intsFactory : public Vertica::ScalarFunctionFactory
{
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface
                &srvInterface)
    { return vt_createFuncObj(srvInterface.allocator, Add2or3ints); }
    virtual void getPrototype(Vertica::ServerInterface &srvInterface,
                              Vertica::ColumnTypes &argTypes,
                              Vertica::ColumnTypes &returnType)
    {   // Accept 2 integer values
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }
};
RegisterFactory(Add2intsFactory);
// This factory defines a function that accepts 3 ints.
class Add3intsFactory : public Vertica::ScalarFunctionFactory
{
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface
                &srvInterface)
    { return vt_createFuncObj(srvInterface.allocator, Add2or3ints); }
    virtual void getPrototype(Vertica::ServerInterface &srvInterface,
                              Vertica::ColumnTypes &argTypes,
                              Vertica::ColumnTypes &returnType)
    {   // accept 3 integer values
        argTypes.addInt();
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }
};
RegisterFactory(Add3intsFactory);

此示例具有两个 ScalarFunctionFactory 类,函数所接受的每个签名(两个整数和三个整数)各一个。除了其 ScalarFunctionFactory::createScalarFunction() 实施都将创建 Add2or3ints 对象之外,这两个工厂类没有其他例外情况。

最后一步是将同一个 SQL 函数名称绑定到这两个工厂类。只要每个工厂的 getPrototype() 实施所定义的签名不相同,您就可以将多个工厂分配给同一个 SQL 函数。

=> CREATE LIBRARY add2or3IntsLib AS '/home/dbadmin/Add2or3Ints.so';
CREATE LIBRARY
=> CREATE FUNCTION add2or3Ints as NAME 'Add2intsFactory' LIBRARY add2or3IntsLib FENCED;
CREATE FUNCTION
=> CREATE FUNCTION add2or3Ints as NAME 'Add3intsFactory' LIBRARY add2or3IntsLib FENCED;
CREATE FUNCTION
=> SELECT add2or3Ints(1,2);
 add2or3Ints
-------------
           3
(1 row)
=> SELECT add2or3Ints(1,2,4);
 add2or3Ints
-------------
           7
(1 row)
=> SELECT add2or3Ints(1,2,3,4); -- Will generate an error
ERROR 3467:  Function add2or3Ints(int, int, int, int) does not exist, or
permission is denied for add2or3Ints(int, int, int, int)
HINT:  No function matches the given name and argument types. You may
need to add explicit type casts

Vertica 在对 add2or3Ints 函数的最终调用的响应中生成了错误消息,因为它无法找到与接受了四个整数实参的 add2or3Ints 关联的工厂类。要进一步扩展 add2or3Ints,您可以创建可接受此签名的另一个工厂类,然后更改 Add2or3ints ScalarFunction 类,或者创建完全不同的类以处理更多整数的相加。但是,添加更多类可以快速接受参数中的每个变体,这具有压倒性优势。在这种情况下,您应考虑创建多态 UDx。

5.2.1.2 - Java 示例:重载 UDx

以下示例代码演示了创建一个将两个或三个整数相加的用户定义标量函数 (UDSF)。Add2or3ints 类已准备好处理两个或三个参数。它将检查已传入的参数个数,并将两个或三个参数全部相加。processBlock() 方法将检查对它发出的调用是否具有少于 2 个参数或具有多于 3 个参数。理论上,这不应当发生,因为如果用户的函数调用与您为函数创建的某一个工厂类上的签名相匹配,则 Vertica 仅调用 UDSF。在实践中,如果您的(或其他人的)工厂类报告函数类接受了它实际上不接受的一组参数,最好执行此健全性检查。

// You need to specify the full package when creating functions based on
// the classes in your library.
package com.mycompany.multiparamexample;
// Import the entire Vertica SDK
import com.vertica.sdk.*;
// This ScalarFunction accepts two or three integer arguments. It tests
// the number of input columns to determine whether to read two or three
// arguments as input.
public class Add2or3ints extends ScalarFunction
{
    @Override
    public void processBlock(ServerInterface srvInterface,
                             BlockReader argReader,
                             BlockWriter resWriter)
                throws UdfException, DestroyInvocation
    {
        // See how many arguments were passed in
        int numCols = argReader.getNumCols();

        // Return an error if less than two or more than 3 aerguments
        // were given. This error only occurs if a Factory class that
        // accepts the wrong number of arguments instantiates this
        // class.
        if (numCols < 2 || numCols > 3) {
            throw new UdfException(0,
                "Must supply 2 or 3 integer arguments");
        }

        // Process all of the rows of input.
        do {
            // Get the first two integer arguments from the BlockReader
            long a = argReader.getLong(0);
            long b = argReader.getLong(1);

            // Assume no third argument.
            long c = 0;

            // Get third argument value if it exists
            if (numCols == 3) {
                c = argReader.getLong(2);
            }

            // Process the arguments and come up with a result. For this
            // example, just add the three arguments together.
            long result = a+b+c;

            // Write the integer output value.
            resWriter.setLong(result);

            // Advance the output BlocKWriter to the next row.
            resWriter.next();

            // Continue processing input rows until there are no more.
        } while (argReader.next());
    }
}

Add2ints 类和 Add2or3ints 类之间的主要区别在于是否包含一个通过调用 BlockReader.getNumCols() 来获取参数个数的节。此类还会测试它从 Vertica 收到的列数,以确保该列数处于它准备好处理的范围内。仅当所创建的 ScalarFunctionFactorygetPrototype() 方法定义了接受少于两个参数或接受多于三个参数的签名时,此测试才会失败。在此简单示例中,实在没有必要执行此测试,但对于更复杂的类,最好测试 Vertica 传递到函数类的列数和数据类型。

do 循环中,如果 Vertica 向 Add2or3ints 类发送两个输入列,则此类会使用默认值(零)。否则,此类会检索第三个值,并将该值与另外两个值相加。您自己的类需要对缺失的输入列使用默认值,或者需要将其处理更改为某种其他方法以处理可变列。

您必须在单独的源文件中定义函数类,而不能将函数类定义为工厂类之一的内部类,因为 Java 不允许从内部类的所属类的外部将内部类实例化。您的工厂类必须可由多个工厂类进行实例化。

创建了一个或多个函数类之后,应为您希望函数类处理的每个签名创建工厂类。这些工厂类可以调用不同的函数类,或者也可以全部调用已准备好接受多个参数集的同一个类。

以下示例的 createScalarFunction() 方法实例化 Add2or3ints 类的成员。

// You will need to specify the full package when creating functions based on
// the classes in your library.
package com.mycompany.multiparamexample;
// Import the entire Vertica SDK
import com.vertica.sdk.*;
public class Add2intsFactory extends ScalarFunctionFactory
{
    @Override
    public void getPrototype(ServerInterface srvInterface,
                             ColumnTypes argTypes,
                             ColumnTypes returnType)
    {
        // Accept two integers as input
        argTypes.addInt();
        argTypes.addInt();
        // writes one integer as output
        returnType.addInt();
    }
    @Override
    public ScalarFunction createScalarFunction(ServerInterface srvInterface)
    {
        // Instantiate the class that can handle either 2 or 3 integers.
        return new Add2or3ints();
    }
}

以下 ScalarFunctionFactory 子类接受三个整数作为输入。此外,它还会将 Add2or3ints 类的成员实例化以处理函数调用:

// You will need to specify the full package when creating functions based on
// the classes in your library.
package com.mycompany.multiparamexample;
// Import the entire Vertica SDK
import com.vertica.sdk.*;
public class Add3intsFactory extends ScalarFunctionFactory
{
    @Override
    public void getPrototype(ServerInterface srvInterface,
                             ColumnTypes argTypes,
                             ColumnTypes returnType)
    {
        // Accepts three integers as input
        argTypes.addInt();
        argTypes.addInt();
        argTypes.addInt();
        // Returns a single integer
        returnType.addInt();
    }
    @Override
    public ScalarFunction createScalarFunction(ServerInterface srvInterface)
    {
        // Instantiates the Add2or3ints ScalarFunction class, which is able to
        // handle eitehr 2 or 3 integers as arguments.
        return new Add2or3ints();
    }
}

必须将工厂类及其调用的一个或多个函数类打包到同一个 JAR 文件中(有关详细信息,请参阅编译并打包 Java 库)。如果数据库群集中的主机上安装了 JDK,则您可以使用以下命令来编译和打包该示例:

$ cd pathToJavaProject$ javac -classpath /opt/vertica/bin/VerticaSDK.jar \
> com/mycompany/multiparamexample/*.java
$ jar -cvf Add2or3intslib.jar com/vertica/sdk/BuildInfo.class \
> com/mycompany/multiparamexample/*.class
added manifest
adding: com/vertica/sdk/BuildInfo.class(in = 1202) (out= 689)(deflated 42%)
adding: com/mycompany/multiparamexample/Add2intsFactory.class(in = 677) (out= 366)(deflated 45%)
adding: com/mycompany/multiparamexample/Add2or3ints.class(in = 919) (out= 601)(deflated 34%)
adding: com/mycompany/multiparamexample/Add3intsFactory.class(in = 685) (out= 369)(deflated 46%)

打包了已重载的 UDx 之后,应以常规 UDx 的相同方法部署已重载的 UDx,唯一的例外是应多次(每个工厂类各一次)使用 CREATE FUNCTION 语句来定义函数。

=> CREATE LIBRARY add2or3intslib as '/home/dbadmin/Add2or3intslib.jar'
-> language 'Java';
CREATE LIBRARY
=> CREATE FUNCTION add2or3ints as LANGUAGE 'Java' NAME 'com.mycompany.multiparamexample.Add2intsFactory' LIBRARY add2or3intslib;
CREATE FUNCTION
=> CREATE FUNCTION add2or3ints as LANGUAGE 'Java' NAME 'com.mycompany.multiparamexample.Add3intsFactory' LIBRARY add2or3intslib;
CREATE FUNCTION

调用已重载的函数的方法与调用任何其他函数相同。

=> SELECT add2or3ints(2,3);
 add2or3ints
-------------
           5
(1 row)
=> SELECT add2or3ints(2,3,4);
 add2or3ints
-------------
           9
(1 row)
=> SELECT add2or3ints(2,3,4,5);
ERROR 3457:  Function add2or3ints(int, int, int, int) does not exist, or permission is denied for add2or3ints(int, int, int, int)
HINT:  No function matches the given name and argument types. You may need to add explicit type casts

最后一个错误由 Vertica 生成,而非 UDx 代码。如果无法找到签名与函数调用的签名匹配的工厂类,它会返回错误。

如果您希望函数接受有限的一组潜在参数,则创建已重载的 UDx 很有用。如果要创建更灵活的参数,您可以创建多态函数。

5.2.2 - 创建多态 UDx

多态 UDx 可接受用户提供的任何数量的参数和任何类型的参数。转换函数 (UDTF)、分析函数 (UDAnF) 和聚合函数 (UDAF) 通常可以在运行时基于输入实参来定义输出返回类型。例如,将两个数字相加的 UDTF 可以返回整数或浮点数,具体取决于输入类型。

Vertica 不会检查用户传递到 UDx 的实参数量或类型,而只会向 UDx 传递用户提供的所有实参。多态 UDx 的主要处理函数(例如,用户定义的标量函数中的 processBlock())负责检查收到的实参数量和类型以及确定是否能够处理这些实参。UDx 最多支持 9800 个实参。

多态 UDx 比使用函数的多个工厂类更灵活(请参阅重载 UDx)。使用它们还可以编写更简洁的代码,而不是为每种数据类型编写不同的代码版本。代价是您的多态函数需要执行更多操作来确定它是否可以处理其实参。

多态 UDx 通过对 ColumnTypes 对象(定义其实参)调用 addAny() 函数,在其工厂的 getPrototype() 函数中声明它接受任何数量的实参,如下所示:

    // C++ example
    void getPrototype(ServerInterface &srvInterface,
                      ColumnTypes &argTypes,
                      ColumnTypes &returnType)
    {
        argTypes.addAny(); // Must be only argument type.
        returnType.addInt(); // or whatever the function returns
    }

您的函数只能声明此“any parameter”参数类型。不能定义必需参数,然后调用 addAny() 以声明其余签名作为可选参数。如果您的函数对其接受的实参有所要求,您的 process() 函数必须强制使用这些实参。

前面显示的 getPrototype() 示例接受任何类型并声明它会返回整数。以下示例显示将解析返回类型推迟到运行时的方法版本。您只能将“any”返回类型用于转换函数和分析函数。

    void getPrototype(ServerInterface &srvInterface,
                      ColumnTypes &argTypes,
                      ColumnTypes &returnType)
    {
        argTypes.addAny();
        returnType.addAny(); // type determined at runtime
    }

如果使用多态返回类型,则还必须在工厂中定义 getReturnType()。在运行时调用此函数可确定实际返回类型。有关示例,请参阅 C++ 示例:PolyNthValue

多态 UDx 和架构搜索路径

如果用户在调用 UDx 时未提供架构名称,Vertica 会在架构搜索路径中的每个架构中搜索其名称和签名与函数调用相匹配的函数。有关架构搜索路径的详细信息,请参阅设置搜索路径

由于多态 UDx 没有与其关联的特定签名,Vertica 最初在搜索函数以处理函数调用时会跳过它们。如果搜索路径中没有架构包含其名称和签名与函数调用相匹配的 UDx,Vertica 会再次在架构搜索路径中搜索其名称与函数调用中的函数名称相匹配的多态 UDx。

此行为会为其签名与函数调用完全匹配的 UDx 赋予优先权。它允许您创建“捕获全部”多态 UDx,仅当没有任何同名非多态 UDx 具有匹配的签名时,Vertica 才调用该多态 UDx。

如果用户期望架构搜索路径中的第一个多态函数处理函数调用,此行为可能会造成混乱。为了避免混乱,您应该:

  • 避免不同 UDx 使用相同的名称。应始终唯一命名 UDx,除非您要创建具有多个签名的过载 UDx。

  • 当无法避免在不同架构中使用同名 UDx 时,请始终在函数调用中提供架构名称。使用架构名称可避免产生歧义,并确保 Vertica 使用正确的 Udx 来处理函数调用。

5.2.2.1 - C++ 示例:PolyNthValue

PolyNthValue 示例是一个分析函数,它返回其输入中每个分区的第 N 行中的值。该函数是 FIRST_VALUE [analytic]LAST_VALUE [analytic] 的泛化。

这些值可以是任何基元数据类型。

有关完整的源代码,请参阅示例(位于 /opt/vertica/sdk/examples/AnalyticFunctions/ 中)中的 PolymorphicNthValue.cpp

加载和使用示例

加载库并创建函数,如下所示:

=> CREATE LIBRARY AnalyticFunctions AS '/home/dbadmin/AnalyticFns.so';
CREATE LIBRARY

=> CREATE ANALYTIC FUNCTION poly_nth_value AS LANGUAGE 'C++'
   NAME 'PolyNthValueFactory' LIBRARY AnalyticFunctions;
CREATE ANALYTIC FUNCTION

考虑不同测试组的分数表:

=> SELECT cohort, score FROM trials;
 cohort | score
--------+-------
   1    | 9
   1    | 8
   1    | 7
   3    | 3
   3    | 2
   3    | 1
   2    | 4
   2    | 5
   2    | 6
(9 rows)

在使用 OVER 子句对数据进行分区的查询中调用该函数。此示例返回每个同类群组中的第二高分:

=> SELECT cohort, score, poly_nth_value(score USING PARAMETERS n=2) OVER (PARTITION BY cohort) AS nth_value
FROM trials;
 cohort | score | nth_value
--------+-------+-----------
   1    | 9     |         8
   1    | 8     |         8
   1    | 7     |         8
   3    | 3     |         2
   3    | 2     |         2
   3    | 1     |         2
   2    | 4     |         5
   2    | 5     |         5
   2    | 6     |         5
(9 rows)

工厂实施

工厂先声明类是多态的,然后根据输入类型设置返回类型。两个工厂方法指定实参和返回类型。

使用 getPrototype() 方法声明分析函数接受并返回任何类型:

    void getPrototype(ServerInterface &srvInterface, ColumnTypes &argTypes, ColumnTypes &returnType)
    {
        // This function supports any argument data type
        argTypes.addAny();

        // Output data type will be the same as the argument data type
        // We will specify that in getReturnType()
        returnType.addAny();
    }

在运行时调用 getReturnType() 方法。这是您根据输入类型设置返回类型的地方:

    void getReturnType(ServerInterface &srvInterface, const SizedColumnTypes &inputTypes,
                       SizedColumnTypes &outputTypes)
    {
        // This function accepts only one argument
        // Complain if we find a different number
        std::vector<size_t> argCols;
        inputTypes.getArgumentColumns(argCols); // get argument column indices

        if (argCols.size() != 1)
        {
            vt_report_error(0, "Only one argument is expected but %s provided",
                            argCols.size()? std::to_string(argCols.size()).c_str() : "none");
        }

        // Define output type the same as argument type
        outputTypes.addArg(inputTypes.getColumnType(argCols[0]), inputTypes.getColumnName(argCols[0]));
    }

函数实施

分析函数本身与类型无关:


    void processPartition(ServerInterface &srvInterface, AnalyticPartitionReader &inputReader,
                          AnalyticPartitionWriter &outputWriter)
    {
        try {
            const SizedColumnTypes &inTypes = inputReader.getTypeMetaData();
            std::vector<size_t> argCols; // Argument column indexes.
            inTypes.getArgumentColumns(argCols);

            vint currentRow = 1;
            bool nthRowExists = false;

            // Find the value of the n-th row
            do {
                if (currentRow == this->n) {
                    nthRowExists = true;
                    break;
                } else {
                    currentRow++;
                }
            } while (inputReader.next());

            if (nthRowExists) {
                do {
                    // Return n-th value
                    outputWriter.copyFromInput(0 /*dest column*/, inputReader,
                                               argCols[0] /*source column*/);
                } while (outputWriter.next());
            } else {
                // The partition has less than n rows
                // Return NULL value
                do {
                    outputWriter.setNull(0);
                } while (outputWriter.next());
            }
        } catch(std::exception& e) {
            // Standard exception. Quit.
            vt_report_error(0, "Exception while processing partition: [%s]", e.what());
        }
    }
};

5.2.2.2 - Java 示例:AddAnyInts

以下示例显示了将两个或更多整数相加的 Java ScalarFunction 的实施。

有关完整的源代码,请参阅示例(位于 /opt/vertica/sdk/examples/JavaUDx/ScalarFunctions 中)中的 AddAnyIntsInfo.java

加载和使用示例

加载库并创建函数,如下所示:

=> CREATE LIBRARY JavaScalarFunctions AS '/home/dbadmin/JavaScalarLib.jar' LANGUAGE 'JAVA';
CREATE LIBRARY

=> CREATE FUNCTION addAnyInts AS LANGUAGE 'Java' NAME 'com.vertica.JavaLibs.AddAnyIntsInfo'
   LIBRARY JavaScalarFunctions;
CREATE FUNCTION

使用两个或多个整数实参调用函数:

=> SELECT addAnyInts(1,2);
 addAnyInts
------------
          3
(1 row)

=> SELECT addAnyInts(1,2,3,40,50,60,70,80,900);
 addAnyInts
------------
       1206
(1 row)

如果使用太少的实参或使用非整数实参调用函数,则产生由 processBlock() 方法生成的错误。UDx 负责确保用户向函数提供正确的参数个数和类型,如果无法处理参数,它应退出并显示错误。

函数实施

此示例中的大部分工作由 processBlock() 方法执行。该函数将对通过 BlockReader 对象传入的实参执行两次检查:

  • 是否存在至少两个参数。

  • 是否所有实参的数据类型均为整数。

多态 UDx 负责确定传入的所有输入是否有效。

processBlock() 方法验证其参数之后,它会在所有参数之中循环并将其相加。

        @Override
        public void processBlock(ServerInterface srvInterface,
                                 BlockReader arg_reader,
                                 BlockWriter res_writer)
                    throws UdfException, DestroyInvocation
        {
        SizedColumnTypes inTypes = arg_reader.getTypeMetaData();
        ArrayList<Integer> argCols = new ArrayList<Integer>(); // Argument column indexes.
        inTypes.getArgumentColumns(argCols);
        // While we have inputs to process
            do {
        long sum = 0;
        for (int i = 0; i < argCols.size(); ++i){
            long a = arg_reader.getLong(i);
            sum += a;
        }
                res_writer.setLong(sum);
                res_writer.next();
            } while (arg_reader.next());
        }
    }

工厂实施

工厂在 getPrototype() 函数中声明实参的数量和类型。

    @Override
    public void getPrototype(ServerInterface srvInterface,
                             ColumnTypes argTypes,
                             ColumnTypes returnType)
    {
    argTypes.addAny();
        returnType.addInt();
    }

5.2.2.3 - R 示例:kmeansPoly

以下示例显示了对一个或多个输入列执行 kmeans 聚类的转换函数 (UDTF) 的实施。

kmeansPoly <- function(v.data.frame,v.param.list) {
  # Computes clusters using the kmeans algorithm.
  #
  # Input: A dataframe and a list of parameters.
  # Output: A dataframe with one column that tells the cluster to which each data
  #         point belongs.
  # Args:
  #  v.data.frame: The data from Vertica cast as an R data frame.
  #  v.param.list: List of function parameters.
  #
  # Returns:
  #  The cluster associated with each data point.
  # Ensure k is not null.
  if(!is.null(v.param.list[['k']])) {
     number_of_clusters <- as.numeric(v.param.list[['k']])
  } else {
    stop("k cannot be NULL! Please use a valid value.")
  }
  # Run the kmeans algorithm.
  kmeans_clusters <- kmeans(v.data.frame, number_of_clusters)
  final.output <- data.frame(kmeans_clusters$cluster)
  return(final.output)
}

kmeansFactoryPoly <- function() {
  # This function tells Vertica the name of the R function,
  # and the polymorphic parameters.
  list(name=kmeansPoly, udxtype=c("transform"), intype=c("any"),
       outtype=c("int"), parametertypecallback=kmeansParameters)
}

kmeansParameters <- function() {
  # Callback function for the parameter types.
  function.parameters <- data.frame(datatype=rep(NA, 1), length=rep(NA,1),
                                    scale=rep(NA,1), name=rep(NA,1))
  function.parameters[1,1] = "int"
  function.parameters[1,4] = "k"
  return(function.parameters)
}

多态 R 函数通过将 "any" 指定为 intype 形参的实参和可选的 outtype 形参,在其工厂函数中声明它可接受任何数量的实参。如果为 intypeouttype 定义 "any" 实参,则函数只能为相应的形参声明该类型。您不能先定义必需实参,然后再调用“any”将其余签名声明为可选实参。如果您的函数对其接受的实参有所要求,您的处理函数必须强制使用这些实参。

outtypecallback 方法用于指示与此方法一起调用的实参类型和数量,并且需要指示函数所返回的类型和数量。outtypecallback 方法还可以用于检查不受支持的实参类型和/或数量。例如,函数可能只需要最多 10 个整数:

您使用与将某个 SQL 名称分配给一个非多态 UDx 相同的语句将一个 SQL 名称分配给您的多态 UDx。以下语句显示了如何从示例中加载和调用多态函数。

=> CREATE LIBRARY rlib2 AS '/home/dbadmin/R_UDx/poly_kmeans.R' LANGUAGE 'R';
CREATE LIBRARY
=> CREATE TRANSFORM FUNCTION kmeansPoly AS LANGUAGE 'R' name 'kmeansFactoryPoly' LIBRARY rlib2;
CREATE FUNCTION
=> SELECT spec, kmeansPoly(sl,sw,pl,pw USING PARAMETERS k = 3)
    OVER(PARTITION BY spec) AS Clusters
      FROM iris;
      spec       | Clusters
-----------------+----------
 Iris-setosa     |        1
 Iris-setosa     |        1
 Iris-setosa     |        1
 Iris-setosa     |        1
.
.
.
(150 rows)

5.3 - UDx 参数

形参可让您为 UDx 定义具有以下特性的实参:在由调用 UDx 的 SQL 语句处理的所有行之间保持恒定。通常,您的 UDxs 允许在 SQL 语句中使用来自列的参数。例如,在以下 SQL 语句中,add2ints UDSF 的参数 a 和 b 会在 SELECT 语句处理每行时更改值:

=> SELECT a, b, add2ints(a,b) AS 'sum' FROM example;
a | b  | sum
---+----+-----
1 |  2 |   3
3 |  4 |   7
5 |  6 |  11
7 |  8 |  15
9 | 10 |  19
(5 rows)

UDx 处理的所有行的参数都保持恒定。您还可以将参数变为可选,从而当用户没有提供参数时,UDx 可以使用默认值。例如,以下示例说明了调用名为 add2intsWithConstant 的 UDSF 的过程,其中具有一个名为 constant 的参数值,该参数值会添加到每个输入行提供的每个参数:

=> SELECT a, b, add2intsWithConstant(a, b USING PARAMETERS constant=42)
    AS 'a+b+42' from example;
a | b  | a+b+42
---+----+--------
1 |  2 |     45
3 |  4 |     49
5 |  6 |     53
7 |  8 |     57
9 | 10 |     61
(5 rows)

此部分的主题说明了如何开发接收参数的 UDX。

5.3.1 - 定义 UDx 接受的参数

可以通过实施 getParameterType() 在 UDx 的工厂类(ScalarFunctionFactoryAggregateFunctionFactory 等)中定义其接受的参数。此方法与 getReturnType() 相似:对作为参数传入的 SizedColumnTypes 对象调用特定于数据类型的方法。每个函数调用都会设置参数的名称、数据类型和宽度或精度(如果数据类型需要此设置)。

设置参数属性(仅限 C++)

使用 C++ API 将参数添加到 getParameterType() 函数时,您还可以设置每个参数的属性。例如,您可以在 UDx 需要时定义一个参数。这样做可告知 Vertica 服务器每次调用 UDx 必须提供指定的参数,否则查询将失败。

通过将对象传递至 SizedColumnTypes::Properties 类,可以定义下列四个参数属性:

设置参数属性(仅限 R)

在 R UDx 中使用参数时,您必须在名为 parametertypecallback 的工厂函数中指定一个字段。此字段指向回调函数,而该回调函数定义函数所需的参数。回调函数定义了具有以下属性的四列数据帧:

如果任何列保留为空(或忽略 parametertypecallback 函数),则 Vertica 将使用默认值。

有关详细信息,请参阅Parametertypecallback 函数

5.3.2 - 获取 UDx 中的参数值

在函数类的处理方法(例如 processBlock()processPartition())中,UDx 会使用在其工厂类中声明的参数值(请参阅定义 UDx 接受的参数)。它从 ParamReader 对象(可通过传递到处理方法的 ServerInterface 对象访问此对象)获取参数值。从此对象读取参数类似于从 BlockReaderPartitionReader 对象读取参数值:使用该参数名称调用特定于数据类型的函数以检索其值。例如,在 C++ 中:

// Get the parameter reader from the ServerInterface to see if there are supplied parameters.
ParamReader paramReader = srvInterface.getParamReader();
// Get the value of an int parameter named constant.
const vint constant = paramReader.getIntRef("constant");

使用工厂类中的参数

除了在 UDx 函数类中使用参数之外,您还可以在工厂类中访问参数。您可能希望访问参数,以让用户通过某种方法控制函数的输入值或输出值。例如,UDx 可以具有一个参数,该参数允许用户选择让 UDx 返回单精度值或双精度值。在工厂类中访问参数的过程与在函数类中访问参数的过程相同:从 ServerInterface's getParamReader() 方法获取 ParamReader 对象,然后读取参数值。

测试用户是否提供了参数值

与实参处理不同,当用户的函数调用不包含由 UDx 工厂类定义的参数值时,Vertica 不会立即返回错误。这意味着函数可以尝试读取用户未提供的参数值。如果函数进行此尝试,则在默认情况下,Vertica 将向用户返回参数不存在警告,并且包含该函数调用的查询将继续运行。

如果希望参数是可选的,您可以在尝试访问参数的值之前测试用户是否提供了该参数的值。函数通过使用该参数名称调用 ParamReadercontainsParameter() 方法来确定特定参数的值是否存在。如果此调用返回 true,则函数可以安全地检索值。如果此调用返回 false,则 UDx 可以使用默认值或将其处理更改为某种其他方法以弥补参数值不存在的问题。只要 UDx 不尝试访问不存在的参数值,Vertica 就不会生成有关缺失参数的错误或警告。

有关示例,请参阅 C++ 示例:定义参数

5.3.3 - 使用参数调用 UDx

可以通过在函数调用中的最后一个参数之后添加 USING PARAMETERS 子句来将参数传递到 UDx。

  • 请勿在最后一个参数和 USING PARAMETERS 子句之间插入逗号。

  • 在 USING PARAMETERS 子句之后,使用以下格式添加一个或多个参数定义:

    <parameter name> = <parameter value>
    
  • 用逗号分隔各个参数定义。

参数值可以是常量表达式(例如 1234 + SQRT(5678))。不能在表达式中使用易变函数(例如 RANDOM),因为易变函数不会返回常量值。如果确实提供了易变表达式作为参数值,则在默认情况下,Vertica 将返回参数类型不正确警告。然后,Vertica 会尝试运行不带有参数值的 UDx。如果 UDx 需要参数,则它会返回自己的错误,该错误会导致取消查询。

调用带有单个参数的 UDx

以下示例演示了如何调用 C++ 示例:定义参数 中所示的 Add2intsWithConstant UDSF 示例:

=> SELECT a, b, Add2intsWithConstant(a, b USING PARAMETERS constant=42) AS 'a+b+42' from example;
 a | b  | a+b+42
---+----+--------
 1 |  2 |     45
 3 |  4 |     49
 5 |  6 |     53
 7 |  8 |     57
 9 | 10 |     61
(5 rows)

要移除数字 3 的第一个实例,您可以调用 RemoveSymbol UDSF 示例:

=> SELECT '3re3mo3ve3sy3mb3ol' original_string, RemoveSymbol('3re3mo3ve3sy3mb3ol' USING PARAMETERS symbol='3');
  original_string   |   RemoveSymbol
--------------------+-------------------
 3re3mo3ve3sy3mb3ol | re3mo3ve3sy3mb3ol
(1 row)

调用带有多个参数的 UDx

以下示例显示了如何调用某个版本的 tokenize UDTF。此 UDTF 包含用于限制允许的最短单词的参数和用于强制以大写输出单词的参数。用逗号分隔多个参数。

=> SELECT url, tokenize(description USING PARAMETERS minLength=4, uppercase=true) OVER (partition by url) FROM T;
       url       |   words
-----------------+-----------
 www.amazon.com  | ONLINE
 www.amazon.com  | RETAIL
 www.amazon.com  | MERCHANT
 www.amazon.com  | PROVIDER
 www.amazon.com  | CLOUD
 www.amazon.com  | SERVICES
 www.dell.com    | LEADING
 www.dell.com    | PROVIDER
 www.dell.com    | COMPUTER
 www.dell.com    | HARDWARE
 www.vertica.com | WORLD'S
 www.vertica.com | FASTEST
 www.vertica.com | ANALYTIC
 www.vertica.com | DATABASE
(16 rows)

以下示例将调用 RemoveSymbol UDSF。通过更改可选参数 n 的值,您可以移除数字 3 的所有实例:

=> SELECT '3re3mo3ve3sy3mb3ol' original_string, RemoveSymbol('3re3mo3ve3sy3mb3ol' USING PARAMETERS symbol='3', n=6);
  original_string   | RemoveSymbol
--------------------+--------------
 3re3mo3ve3sy3mb3ol | removesymbol
(1 row)

调用带有可选参数或不正确参数的 UDx

您可以选择添加 Add2intsWithConstant UDSF 的常量参数。在未使用参数的情况下调用此约束不会返回错误或警告:

=> SELECT a,b,Add2intsWithConstant(a, b) AS 'sum' FROM example;
 a | b  | sum
---+----+-----
 1 |  2 |   3
 3 |  4 |   7
 5 |  6 |  11
 7 |  8 |  15
 9 | 10 |  19
(5 rows)

虽然调用带有不正确参数的 UDx 将生成警告,但在默认情况下,查询仍会运行。有关设置 UDx 在您提供不正确参数时的行为的详细信息,请参阅指定传递未注册参数的行为

=> SELECT a, b,  add2intsWithConstant(a, b USING PARAMETERS wrongparam=42) AS 'result' from example;
WARNING 4332:  Parameter wrongparam was not registered by the function and cannot
be coerced to a definite data type
 a | b  | result
---+----+--------
 1 |  2 |      3
 3 |  4 |      7
 5 |  6 |     11
 7 |  8 |     15
 9 | 10 |     19
(5 rows)

5.3.4 - 指定传递未注册参数的行为

默认情况下,Vertica 会在您向 UDx 传递未注册参数时发出警告消息。未注册参数是指未在 getParameterType() 方法中声明的参数。

可以通过更改 StrictUDxParameterChecking 配置参数来控制 UDx 在您向其传递未注册参数时的行为。

未注册参数行为设置

可以指定 UDx 为响应一个或多个未注册参数而做出的行为。要执行此操作,请将 StrictUDxParameterChecking 配置参数设置为以下值之一:

  • 0:允许 UDx 访问未注册参数。ParamReader 类的 getType() 方法决定未注册参数的数据类型。Vertica 不会显示任何警告或错误消息。

  • 1(默认值):忽略未注册参数并允许函数运行。Vertica 将显示警告消息。

  • 2:返回错误并阻止函数运行。

示例

以下示例演示了可以通过对 StrictUDxParameterChecking 参数使用不同的值来指定的行为。

查看 StrictUDxParameterChecking 的当前值

要查看 StrictUDxParameterChecking 配置参数的当前值,请运行以下查询:


=> \x
Expanded display is on.
=> SELECT * FROM configuration_parameters WHERE parameter_name = 'StrictUDxParameterChecking';
-[ RECORD 1 ]-----------------+------------------------------------------------------------------
node_name                     | ALL
parameter_name                | StrictUDxParameterChecking
current_value                 | 1
restart_value                 | 1
database_value                | 1
default_value                 | 1
current_level                 | DATABASE
restart_level                 | DATABASE
is_mismatch                   | f
groups                        |
allowed_levels                | DATABASE
superuser_only                | f
change_under_support_guidance | f
change_requires_restart       | f
description                   | Sets the behavior to deal with undeclared UDx function parameters

更改 StrictUDxParameterChecking 的值

可以在数据库级别、节点级别或会话级别更改 StrictUDxParameterChecking 配置参数的值。例如,可以将值更改为“0”,以指定可以向 UDx 传递未注册参数而不显示警告或错误消息:


=> ALTER DATABASE DEFAULT SET StrictUDxParameterChecking = 0;
ALTER DATABASE

RemoveSymbol 的无效参数行为

以下示例演示了如何调用 RemoveSymbol UDSF 示例。RemoveSymbol UDSF 具有一个必需参数 (symbol) 和一个可选参数 (n)。在此示例中,未使用可选参数。

如果同时传递 symbol 和一个名为 wrongParam 的附加参数(未在 UDx 中声明此参数),UDx 的行为会根据 StrictUDxParameterChecking 的值相应地更改。

如果将 StrictUDxParameterChecking 设置为“0”,UDx 将正常运行而不显示警告。此外,wrongParam 将变为可供 UDx 访问(通过 ServerInterface 对象的 ParamReader 对象):


=> ALTER DATABASE DEFAULT SET StrictUDxParameterChecking = 0;
ALTER DATABASE

=> SELECT '3re3mo3ve3sy3mb3ol' original_string, RemoveSymbol('3re3mo3ve3sy3mb3ol' USING PARAMETERS symbol='3', wrongParam='x');
  original_string   |   RemoveSymbol
--------------------+-------------------
 3re3mo3ve3sy3mb3ol | re3mo3ve3sy3mb3ol
(1 row)

如果将 StrictUDxParameterChecking 设置为“1”,UDx 将忽略 wrongParam 并正常运行。但是,它还会发出警告消息:


=> ALTER DATABASE DEFAULT SET StrictUDxParameterChecking = 1;
ALTER DATABASE

=> SELECT '3re3mo3ve3sy3mb3ol' original_string, RemoveSymbol('3re3mo3ve3sy3mb3ol' USING PARAMETERS symbol='3', wrongParam='x');
WARNING 4320:  Parameter wrongParam was not registered by the function and cannot be coerced to a definite data type
  original_string   |   RemoveSymbol
--------------------+-------------------
 3re3mo3ve3sy3mb3ol | re3mo3ve3sy3mb3ol
(1 row)

如果将 StrictUDxParameterChecking 设置为 '2',UDx 将在尝试调用 wrongParam 时遇到错误并且无法运行。相反,它会生成错误消息:


=> ALTER DATABASE DEFAULT SET StrictUDxParameterChecking = 2;
ALTER DATABASE

=> SELECT '3re3mo3ve3sy3mb3ol' original_string, RemoveSymbol('3re3mo3ve3sy3mb3ol' USING PARAMETERS symbol='3', wrongParam='x');
ERROR 0:  Parameter wrongParam was not registered by the function

5.3.5 - 用户定义的会话参数

使用用户定义的会话参数,可以编写比 Vertica 提供的参数更通用的参数。您可以使用以下方法配置用户定义的会话参数:

  • 从客户端 — 例如,使用 ALTER SESSION

  • 通过 UDx 自身

用户定义的会话参数可以传递到受 Vertica 支持的任何类型的 UDx。您还可以在会话级别为您的 UDx 设置参数。通过指定用户定义的会话参数,您可以持续保存参数状态。甚至当 UDx 在单个会话期间被多次调用时,Vertica 也可以保存参数状态。

RowCount 示例使用用户定义的会话参数。此参数会计算出每次运行时 UDx 处理的行的总数。然后,RowCount 会显示所有执行操作处理的行的总数。有关如何实施,请参阅 C++ 示例:使用会话参数Java 示例:使用会话参数

查看用户定义的会话参数

输入以下命令以查看所有会话参数的值:

=> SHOW SESSION UDPARAMETER all;
schema | library | key | value
--------+---------+-----+-------
(0 rows)

尚未设置任何值,因此该表为空。现在执行 UDx:

=> SELECT RowCount(5,5);
RowCount
----------
10
(1 row)

再次输入命令,查看会话参数的值:

=> SHOW SESSION UDPARAMETER all;
schema |  library  |   key    | value
--------+-----------+----------+-------
public | UDSession | rowcount | 1
(1 row)

库列显示包含 UDx 的库的名称。这是使用 CREATE LIBRARY 设置的名称。因为 UDx 已经处理了一行,所以 rowcount 会话参数的值当前为 1。再运行两次 Udx 应将该值增加 2。

=> SELECT RowCount(10,10);
RowCount
----------
20
(1 row)
=> SELECT RowCount(15,15);
RowCount
----------
30
(1 row)

现在您已经执行了三次 UDx,获取了 5 + 5、10 + 10 和 15 + 15 的总和。现在,检查 rowcount 的值。

=> SHOW SESSION UDPARAMETER all;
schema |  library  |   key    | value
--------+-----------+----------+-------
public | UDSession | rowcount | 3
(1 row)

更改用户定义的会话参数

您还可以手动更改 rowcount 的值。为此,请输入以下命令:

=> ALTER SESSION SET UDPARAMETER FOR UDSession rowcount = 25;
ALTER SESSION

检查 RowCount 的值:

=> SHOW SESSION UDPARAMETER all;
schema |  library  |   key    | value
--------+-----------+----------+-------
public | UDSession | rowcount | 25
(1 row)

清除用户定义的会话参数

从客户端

要清除 rowcount 的当前值,请输入以下命令:

=> ALTER SESSION CLEAR UDPARAMETER FOR UDSession rowcount;
ALTER SESSION

确认 rowcount 已被清除:

=> SHOW SESSION UDPARAMETER all;
schema | library | key | value
--------+---------+-----+-------
(0 rows)

通过 C++ UDx

可以将会话参数通过 UDx 自身进行清除。例如,要在其值达到 10 或更大值时清除 rowcount,请执行下列操作:

  1. 从 RowCount 类的 destroy() 方法中移除以下行:

    udParams.getUDSessionParamWriter("library").getStringRef("rowCount").copy(i_as_string);
    
  2. 将已从 destroy() 方法中移除的行替换为以下代码:

    
    if (rowCount < 10)
    {
    udParams.getUDSessionParamWriter("library").getStringRef("rowCount").copy(i_as_string);
    }
    else
    {
    udParams.getUDSessionParamWriter("library").clearParameter("rowCount");
    }
    
  3. 要查看 Udx 是否已清除会话参数,请将 rowcount 的值设置为 9:

    => ALTER SESSION SET UDPARAMETER FOR UDSession rowcount = 9;
    ALTER SESSION
    
  4. 检查 rowcount 的值:

    => SHOW SESSION UDPARAMETER all;
     schema |  library  |   key    | value
    --------+-----------+----------+-------
     public | UDSession | rowcount | 9
     (1 row)
    
  5. 调用 RowCount,使它的值变成 10:

    => SELECT RowCount(15,15);
    RowCount
    ----------
          30
     (1 row)
    
  6. 再次检查 rowcount 的值。由于值已达到 10(即 UDx 中指定的阈值),因此可认为 rowcount 已被清除:

    => SHOW SESSION UDPARAMETER all;
     schema | library | key | value
    --------+---------+-----+-------
     (0 rows)
    

    正如预计的那样,RowCount 已被清除。

通过 Java UDx

  1. 从 RowCount 类的 destroy() 方法中移除以下行:

    udParams.getUDSessionParamWriter("library").setString("rowCount", Integer.toString(rowCount));
    srvInterface.log("RowNumber processed %d records", count);
    
  2. 将已从 destroy() 方法中移除的行替换为以下代码:

    
    if (rowCount < 10)
    {
    udParams.getUDSessionParamWriter("library").setString("rowCount", Integer.toString(rowCount));
    srvInterface.log("RowNumber processed %d records", count);
    }
    else
    {
    udParams.getUDSessionParamWriter("library").clearParameter("rowCount");
    }
    
  3. 要查看 Udx 是否已清除会话参数,请将 rowcount 的值设置为 9:

    => ALTER SESSION SET UDPARAMETER FOR UDSession rowcount = 9;
    ALTER SESSION
    
  4. 检查 rowcount 的值:

    => SHOW SESSION UDPARAMETER all;
     schema |  library  |   key    | value
    --------+-----------+----------+-------
     public | UDSession | rowcount | 9
     (1 row)
    
  5. 调用 RowCount,使它的值变成 10:

    => SELECT RowCount(15,15);
    RowCount
    ----------
           30
     (1 row)
    
  6. 检查 rowcount 的值。由于值已达到 10(即 UDx 中指定的阈值),因此可认为 rowcount 已被清除:

    => SHOW SESSION UDPARAMETER all;
     schema | library | key | value
    --------+---------+-----+-------
     (0 rows)
    

正如预计的那样,rowcount 已被清除。

只读会话参数和隐藏的会话参数

如果不想在 UDx 之外的其他地方设置参数,您可以将其设为只读。此外,如果不想让参数在客户端内可见,您可以将其设为隐藏。

将参数变为只读,就意味着它不能在客户端中设置,但是可以进行查看。要将参数变为只读,请在参数名称前面添加单下划线。例如,要将 rowCount 变为只读,请将 UDx 中的所有“rowCount”实例更改为“_rowCount”。

将参数设为隐藏,就意味着它不能在客户端中查看,也不能设置。要将参数设为隐藏,请在参数名称前面添加两个下划线。例如,要将 rowCount 变为隐藏,请将 UDx 中的所有“rowCount”实例更改为“__rowCount”。

另请参阅

Kafka 用户定义的会话参数

5.3.6 - C++ 示例:定义参数

以下代码片段演示了如何将单个参数添加到 C++ add2ints UDSF 示例。getParameterType() 函数定义了名为 constant 的单个整数参数。

class Add2intsWithConstantFactory : public ScalarFunctionFactory
{
    // Return an instance of Add2ints to perform the actual addition.
    virtual ScalarFunction *createScalarFunction(ServerInterface &interface)
    {
        // Calls the vt_createFuncObj to create the new Add2ints class instance.
        return vt_createFuncObj(interface.allocator, Add2intsWithConstant);
    }
    // Report the argument and return types to Vertica.
    virtual void getPrototype(ServerInterface &interface,
                              ColumnTypes &argTypes,
                              ColumnTypes &returnType)
    {
        // Takes two ints as inputs, so add ints to the argTypes object.
        argTypes.addInt();
        argTypes.addInt();
        // Returns a single int.
        returnType.addInt();
    }
    // Defines the parameters for this UDSF. Works similarly to defining arguments and return types.
    virtual void getParameterType(ServerInterface &srvInterface,
                                  SizedColumnTypes &parameterTypes)
    {
        // One int parameter named constant.
        parameterTypes.addInt("constant");
    }
};
RegisterFactory(Add2intsWithConstantFactory);

有关定义参数时可调用的特定于数据类型的函数的完整列表,请参阅 SizedColumnTypes 的 Vertica SDK 条目。

以下代码片段演示了使用参数值。Add2intsWithConstant 类定义了一个可将两个整数值相加的函数。如果用户提供了名为 constant 的可选整数参数,则函数还会加上该参数的值。

/**
 * A UDSF that adds two numbers together with a constant value.
 *
 */
class Add2intsWithConstant : public ScalarFunction
{
public:
    // Processes a block of data sent by Vertica.
    virtual void processBlock(ServerInterface &srvInterface,
                              BlockReader &arg_reader,
                              BlockWriter &res_writer)
    {
        try
            {
                // The default value for the constant parameter is 0.
                vint constant = 0;

                // Get the parameter reader from the ServerInterface to see if there are supplied parameters.
                ParamReader paramReader = srvInterface.getParamReader();
                // See if the user supplied the constant parameter.
                if (paramReader.containsParameter("constant"))
                    // There is a parameter, so get its value.
                    constant = paramReader.getIntRef("constant");
                // While we have input to process:
                do
                    {
                        // Read the two integer input parameters by calling the BlockReader.getIntRef class function.
                        const vint a = arg_reader.getIntRef(0);
                        const vint b = arg_reader.getIntRef(1);
                        // Add arguments plus constant.
                        res_writer.setInt(a+b+constant);
                        // Finish writing the row, and advance to the next output row.
                        res_writer.next();
                        // Continue looping until there are no more input rows.
                    }
                while (arg_reader.next());
            }
        catch (exception& e)
            {
                // Standard exception. Quit.
                vt_report_error(0, "Exception while processing partition: %s",
                    e.what());
            }
    }
};

5.3.7 - C++ 示例:使用会话参数

RowCount 示例使用用户定义会话参数,也称为 RowCount。此参数会计算出每次运行时 UDx 处理的行的总数。然后,RowCount 会显示所有执行操作处理的行的总数。

#include <string>
#include <sstream>
#include <iostream>
#include "Vertica.h"
#include "VerticaUDx.h"

using namespace Vertica;

class RowCount : public Vertica::ScalarFunction
{
private:
    int rowCount;
    int count;

public:

    virtual void setup(Vertica::ServerInterface &srvInterface, const Vertica::SizedColumnTypes &argTypes) {
        ParamReader pSessionParams = srvInterface.getUDSessionParamReader("library");
        std::string rCount = pSessionParams.containsParameter("rowCount")?
            pSessionParams.getStringRef("rowCount").str(): "0";
        rowCount=atoi(rCount.c_str());

    }
    virtual void processBlock(Vertica::ServerInterface &srvInterface, Vertica::BlockReader &arg_reader, Vertica::BlockWriter &res_writer) {

        count = 0;
        if(arg_reader.getNumCols() != 2)
            vt_report_error(0, "Function only accepts two arguments, but %zu provided", arg_reader.getNumCols());

        do {
            const Vertica::vint a = arg_reader.getIntRef(0);
            const Vertica::vint b = arg_reader.getIntRef(1);
            res_writer.setInt(a+b);
            count++;
            res_writer.next();
        } while (arg_reader.next());

        srvInterface.log("count %d", count);

        }

        virtual void destroy(ServerInterface &srvInterface, const SizedColumnTypes &argTypes, SessionParamWriterMap &udParams) {
            rowCount = rowCount + count;

            std:ostringstream s;
            s << rowCount;
            const std::string i_as_string(s.str());

            udParams.getUDSessionParamWriter("library").getStringRef("rowCount").copy(i_as_string);

        }
};

class RowCountsInfo : public Vertica::ScalarFunctionFactory {
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface &srvInterface)
    { return Vertica::vt_createFuncObject<RowCount>(srvInterface.allocator);
    }

    virtual void getPrototype(Vertica::ServerInterface &srvInterface, Vertica::ColumnTypes &argTypes, Vertica::ColumnTypes &returnType)
    {
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }
};

RegisterFactory(RowCountsInfo);

5.3.8 - Java 示例:定义参数

以下代码片段演示了将单个参数添加到 Java add2ints UDSF 示例。getParameterType() 函数定义了名为 constant 的单个整数参数。

package com.mycompany.example;
import com.vertica.sdk.*;
public class Add2intsWithConstantFactory extends ScalarFunctionFactory
{
    @Override
    public void getPrototype(ServerInterface srvInterface,
                             ColumnTypes argTypes,
                             ColumnTypes returnType)
    {
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }

    @Override
    public void getReturnType(ServerInterface srvInterface,
                              SizedColumnTypes argTypes,
                              SizedColumnTypes returnType)
    {
        returnType.addInt("sum");
    }

    // Defines the parameters for this UDSF. Works similarly to defining
    // arguments and return types.
    public void getParameterType(ServerInterface srvInterface,
                              SizedColumnTypes parameterTypes)
    {
        // One INTEGER parameter named constant
        parameterTypes.addInt("constant");
    }

    @Override
    public ScalarFunction createScalarFunction(ServerInterface srvInterface)
    {
        return new Add2intsWithConstant();
    }
}

有关定义参数时可调用的特定于数据类型的方法的完整列表,请参阅 SizedColumnTypes 的 Vertica Java SDK 条目。

5.3.9 - Java 示例:使用会话参数

RowCount 示例使用用户定义会话参数,也称为 RowCount。此参数会计算出每次运行时 UDx 处理的行的总数。然后,RowCount 会显示所有执行操作处理的行的总数。


package com.mycompany.example;

import com.vertica.sdk.*;

public class RowCountFactory extends ScalarFunctionFactory {

    @Override
    public void getPrototype(ServerInterface srvInterface, ColumnTypes argTypes, ColumnTypes returnType)
    {
        argTypes.addInt();
        argTypes.addInt();
     returnType.addInt();
    }

public class RowCount extends ScalarFunction {

    private Integer count;
    private Integer rowCount;

    // In the setup method, you look for the rowCount parameter. If it doesn't exist, it is created.
    // Look in the default namespace which is "library," but it could be anything else, most likely "public" if not "library".
    @Override
       public void setup(ServerInterface srvInterface, SizedColumnTypes argTypes) {
    count = new Integer(0);
    ParamReader pSessionParams = srvInterface.getUDSessionParamReader("library");
    String rCount = pSessionParams.containsParameter("rowCount")?
    pSessionParams.getString("rowCount"): "0";
    rowCount = Integer.parseInt(rCount);

    }

    @Override
    public void processBlock(ServerInterface srvInterface, BlockReader arg_reader, BlockWriter res_writer)
        throws UdfException, DestroyInvocation {
        do {
        ++count;
        long a = arg_reader.getLong(0);
        long b = arg_reader.getLong(1);

        res_writer.setLong(a+b);
        res_writer.next();
        } while (arg_reader.next());
    }

    @Override
    public void destroy(ServerInterface srvInterface, SizedColumnTypes argTypes, SessionParamWriterMap udParams){
        rowCount = rowCount+count;
        udParams.getUDSessionParamWriter("library").setString("rowCount", Integer.toString(rowCount));
        srvInterface.log("RowNumber processed %d records", count);
        }
    }

    @Override
    public ScalarFunction createScalarFunction(ServerInterface srvInterface){
        return new RowCount();
    }
}

5.4 - 错误、警告和日志记录

SDK 为 UDx 提供了多种报告错误、警告和其他消息的方法。对于使用 C++ 或 Python 编写的 UDx,请使用发送消息中描述的消息传递 API。如处理错误中所述,所有语言的 UDx 都可能因出错而停止执行。

此外,UDx 还可以将消息写入 Vertica 日志,而使用 C++ 编写的 UDx 可以将消息写入系统表。

5.4.1 - 发送消息

UDx 可以通过报告错误并终止执行来处理问题,但在某些情况下,您可能希望发送警告并继续。例如,UDx 可能会忽略或使用意外输入的默认值,并报告它已经这样做了。C++ 和 Python 消息传递 API 支持报告不同严重性级别的消息。

Udx 对 ServerInterface 实例具有访问权限。此类具有以下按严重性顺序报告消息的方法:

  • reportError (也会终止执行)

  • reportWarning

  • reportNotice

  • reportInfo

每种方法都会生成包含以下组件的消息:

  • ID 代码:标识码,为任意整数。此代码不会与 Vertica 错误代码交互。

  • 消息字符串:对问题的简要描述。

  • 可选详细信息字符串:提供更多上下文信息。

  • 可选提示字符串:提供其他指导。

如果重复的消息具有相同的代码和消息字符串,则即使详细信息和提示字符串不同,它们也会被压缩到一个报告中。

构造消息

UDx 通常应在进程调用期间立即报告错误。对于所有其他消息类型,请在处理期间记录信息并从 UDx 的 destroy 方法调用报告方法。如果在处理期间调用其他报告方法,则不会生成输出。

构造消息的过程特定于语言。

C++

每个 ServerInterface 报告方法均使用 ClientMessage 实参。ClientMessage 类具有以下用于设置代码和消息、详细信息及提示的方法:

  • makeMessage: 设置 ID 代码和消息字符串。

  • setDetail: 设置可选的详细信息字符串。

  • setHint: 设置可选的提示字符串。

这些方法调用可以链接起来以简化消息的创建和传递。

所有字符串都支持 printf 样式的实参和格式。

在以下示例中,函数在 processBlock 中记录问题并在 destroy 中报告它们:


class PositiveIdentity : public Vertica::ScalarFunction
    {
public:
    using ScalarFunction::destroy;
    bool hitNotice = false;

    virtual void processBlock(Vertica::ServerInterface &srvInterface,
                              Vertica::BlockReader &arg_reader,
                              Vertica::BlockWriter &res_writer)
    {
        do {
            const Vertica::vint a = arg_reader.getIntRef(0);
            if (a < 0 && a != vint_null) {
                hitNotice = true;
                res_writer.setInt(null);
            } else {
                res_writer.setInt(a);
            }
            res_writer.next();
        } while (arg_reader.next());
    }

    virtual void destroy(ServerInterface &srvInterface,
                         const SizedColumnTypes &argTypes) override
    {
        if (hitNotice) {
            ClientMessage msg = ClientMessage::makeMessage(100, "Passed negative argument")
                                .setDetail("Value set to null");
            srvInterface.reportNotice(msg);
        }
    }
}

Python

每个 ServerInterface 报告方法都包含以下位置实参和关键字实参:

  • idCode:整数 ID 代码,为位置实参。

  • message:消息文本,为位置实参。

  • hint:可选提示文本,为关键字实参。

  • detail:可选的详细信息文本,为关键字实参。

所有实参都支持 str.format()f-string 格式。

在以下示例中,函数在 processBlock 中记录问题并在 destroy 中报告它们:


class PositiveIdentity(vertica_sdk.ScalarFunction):
    def __init__(self):
        self.hitNotice = False

    def processBlock(self, server_interface, arg_reader, res_writer):
        while True:
            arg = arg_reader.getInt(0)
            if arg < 0 and arg is not None:
                self.hitNotice = True
                res_writer.setNull()
            else:
                res_writer.setInt(arg)
            res_writer.next()
            if not arg_reader.next():
                break

    def destroy(self, srv, argType):
        if self.hitNotice:
            srv.reportNotice(100, "Passed negative arguement", detail="Value set to null")
        return

API

在调用 ServerInterface 报告方法之前,请使用 ClientMessage 类构造并填充消息。

ServerInterface API 会提供以下报告消息的方法:


// ClientMessage methods
template<typename... Argtypes>
static ClientMessage makeMessage(int errorcode, const char *fmt, Argtypes&&... args);

template <typename... Argtypes>
ClientMessage & setDetail(const char *fmt, Argtypes&&... args);

template <typename... Argtypes>
ClientMessage & setHint(const char *fmt, Argtypes&&... args);

// ServerInterface reporting methods
virtual void reportError(ClientMessage msg);

virtual void reportInfo(ClientMessage msg);

virtual void reportNotice(ClientMessage msg);

virtual void reportWarning(ClientMessage msg);

ServerInterface API 会提供以下报告消息的方法:


def reportError(self, code, text, hint='', detail=''):

def reportInfo(self, code, text, hint='', detail=''):

def reportNotice(self, code, text, hint='', detail=''):

def reportWarning(self, code, text, hint='', detail=''):

5.4.2 - 处理错误

如果 UDx 遇到无法恢复的错误,它应报告错误并终止。如何做到这一点取决于编写时所用的语言:

  • C++:考虑使用发送消息中所述的 API,它比本主题中描述的错误处理更具表现力。或者,您可以使用 vt_report_error 宏来报告错误并退出。该宏使用以下两个参数:错误号和错误消息字符串。错误号和消息都会显示在 Vertica 向用户报告的错误中。错误号并不由 Vertica 定义。您可以随意使用任何值。

  • Java:实例化并引发 UdfException,它会将数字代码和消息字符串报告给用户。

  • Python:考虑使用发送消息中所述的 API,它比本主题中描述的错误处理更具表现力。或者,引发内置到 Python 语言中的异常;SDK 不包含特定于 UDx 的异常。

  • R:使用 stop 来停止执行并显示一条消息。

异常或停止会导致包含函数调用的事务被回滚。

以下示例演示了错误处理:

以下函数会将两个整数相除。为了防止除数为零,它会测试第二个参数并在为零时设为失败:

class Div2ints : public ScalarFunction
{
public:
  virtual void processBlock(ServerInterface &srvInterface,
                            BlockReader &arg_reader,
                            BlockWriter &res_writer)
  {
    // While we have inputs to process
    do
      {
        const vint a = arg_reader.getIntRef(0);
        const vint b = arg_reader.getIntRef(1);
        if (b == 0)
          {
            vt_report_error(1,"Attempted divide by zero");
          }
        res_writer.setInt(a/b);
        res_writer.next();
      }
    while (arg_reader.next());
  }
};

加载和调用函数演示了用户所看到的错误。隔离和非隔离模式使用不同的错误号。

=> CREATE LIBRARY Div2IntsLib AS '/home/dbadmin/Div2ints.so';
CREATE LIBRARY
=> CREATE FUNCTION div2ints AS LANGUAGE 'C++' NAME 'Div2intsInfo' LIBRARY Div2IntsLib;
CREATE FUNCTION
=> SELECT div2ints(25, 5);
 div2ints
----------
        5
(1 row)
=> SELECT * FROM MyTable;
 a  | b
----+---
 12 | 6
  7 | 0
 12 | 2
 18 | 9
(4 rows)
=> SELECT * FROM MyTable WHERE div2ints(a, b) > 2;
ERROR 3399:  Error in calling processBlock() for User Defined Scalar Function
div2ints at Div2ints.cpp:21, error code: 1, message: Attempted divide by zero

在以下示例中,如果任一实参为 NULL,则 processBlock() 方法将引发异常:

@Override
public void processBlock(ServerInterface srvInterface,
                         BlockReader argReader,
                         BlockWriter resWriter)
            throws UdfException, DestroyInvocation
{
  do {
      // Test for NULL value. Throw exception if one occurs.
      if (argReader.isLongNull(0) || argReader.isLongNull(1) ) {
          // No nulls allowed. Throw exception
          throw new UdfException(1234, "Cannot add a NULL value");
     }

当 UDx 引发异常时,正在运行 UDx 的从属进程会将错误报告回 Vertica 并退出。Vertica 将向用户显示包含于异常中的错误消息和堆栈跟踪:

=> SELECT add2ints(2, NULL);
ERROR 3399:  Failure in UDx RPC call InvokeProcessBlock(): Error in User Defined Object [add2ints], error code: 1234
com.vertica.sdk.UdfException: Cannot add a NULL value
        at com.example.Add2intsFactory$Add2ints.processBlock(Add2intsFactory.java:37)
        at com.vertica.udxfence.UDxExecContext.processBlock(UDxExecContext.java:700)
        at com.vertica.udxfence.UDxExecContext.run(UDxExecContext.java:173)
        at java.lang.Thread.run(Thread.java:662)

在此示例中,如果其中一个实参小于 100,则 Python UDx 会引发错误:

    while(True):
        # Example of error checking best practices.
        product_id = block_reader.getInt(2)
        if product_id < 100:
            raise ValueError("Invalid Product ID")

错误会生成如下消息:

=> SELECT add2ints(prod_cost, sale_price, product_id) FROM bunch_of_numbers;
ERROR 3399:  Failure in UDx RPC call InvokeProcessBlock(): Error calling processBlock() in User Defined Object [add2ints]
at [/udx/PythonInterface.cpp:168], error code: 0,
message: Error [/udx/PythonInterface.cpp:385] function ['call_method']
(Python error type [<class 'ValueError'>])
Traceback (most recent call last):
  File "/home/dbadmin/py_db/v_py_db_node0001_catalog/Libraries/02fc4af0ace6f91eefa74baecf3ef76000a0000000004fc4/pylib_02fc4af0ace6f91eefa74baecf3ef76000a0000000004fc4.py",
line 13, in processBlock
    raise ValueError("Invalid Product ID")
ValueError: Invalid Product ID

在此示例中,如果数据帧的第三列与指定的产品 ID 不匹配,则 R UDx 会引发错误:


Calculate_Cost_w_Tax <- function(input.data.frame) {
  # Must match the Product ID 11444
  if ( !is.numeric(input.data.frame[, 3]) == 11444 ) {
    stop("Invalid Product ID!")
  } else {
    cost_w_tax <- data.frame(input.data.frame[, 1] * input.data.frame[, 2])
  }
  return(cost_w_tax)
}

Calculate_Cost_w_TaxFactory <- function() {
  list(name=Calculate_Cost_w_Tax,
       udxtype=c("scalar"),
       intype=c("float","float", "float"),
       outtype=c("float"))
}

错误会生成如下消息:

=> SELECT Calculate_Cost_w_Tax(item_price, tax_rate, prod_id) FROM Inventory_Sales_Data;
vsql:sql_test_multiply.sql:21: ERROR 3399:  Failure in UDx RPC call InvokeProcessBlock():
Error calling processBlock() in User Defined Object [mul] at
[/udx/RInterface.cpp:1308],
error code: 0, message: Exception in processBlock :Invalid Product ID!

要报告有关错误的其他诊断信息,您可以在引发异常之前将消息写入日志文件(请参阅日志)。

您的 UDx 不得使用其未引发的异常。拦截服务器异常可能会导致数据库不稳定。

5.4.3 - 日志

每个使用 C++、Java 或 Python 编写的 UDx 都有一个关联的 ServerInterface 实例。ServerInterface 类提供了写入 Vertica 日志的功能,C++ 实施还提供了在系统表中记录事件的功能。

将消息写入 Vertica 日志

可以使用 ServerInterface.log() 函数将消息写入到日志文件。此函数的工作方式与 printf() 相似,它使用已设置格式的字符串和一个可选值集,并将字符串写入到日志文件。将消息写入到的位置取决于函数在隔离模式还是非隔离模式下运行。

  • 在非隔离模式下运行的函数会将其消息写入到编录目录中的 vertica.log 文件。

  • 在隔离模式下运行的函数会将其消息写入到编录目录中名为 UDxLogs/UDxFencedProcesses.log 的日志文件。

为了帮助标识函数的输出,Vertica 会将绑定到 UDx 的 SQL 函数名称添加到日志消息中。

以下示例将记录 UDx 的输入值:

    virtual void processBlock(ServerInterface &srvInterface,
                              BlockReader &argReader,
                              BlockWriter &resWriter)
    {
        try {
            // While we have inputs to process
            do {
                if (argReader.isNull(0) || argReader.isNull(1)) {
                    resWriter.setNull();
                } else {
                    const vint a = argReader.getIntRef(0);
                    const vint b = argReader.getIntRef(1);
                    <span class="code-input">srvInterface.log("got a: %d and b: %d", (int) a, (int) b);</span>
                    resWriter.setInt(a+b);
                }
                resWriter.next();
            } while (argReader.next());
        } catch(std::exception& e) {
            // Standard exception. Quit.
            vt_report_error(0, "Exception while processing block: [%s]", e.what());
        }
    }
        @Override
        public void processBlock(ServerInterface srvInterface,
                                 BlockReader argReader,
                                 BlockWriter resWriter)
                    throws UdfException, DestroyInvocation
        {
            do {
                // Get the two integer arguments from the BlockReader
                long a = argReader.getLong(0);
                long b = argReader.getLong(1);

                // Log the input values
                <span class="code-input">srvInterface.log("Got values a=%d and b=%d", a, b);</span>

                long result = a+b;
                resWriter.setLong(result);
                resWriter.next();
            } while (argReader.next());
        }
    }
    def processBlock(self, server_interface, arg_reader, res_writer):
        server_interface.log("Python UDx - Adding 2 ints!")
        while(True):
            first_int = block_reader.getInt(0)
            second_int = block_reader.getInt(1)
            block_writer.setInt(first_int + second_int)
            <span class="code-input">server_interface.log("Values: first_int is {} second_int is {}".format(first_int, second_int))</span>
            block_writer.next()
            if not block_reader.next():
                break

log() 函数在日志文件中生成条目,如下所示:

$ tail /home/dbadmin/py_db/v_py_db_node0001_catalog/UDxLogs/UDxFencedProcesses.log
 07:52:12.862 [Python-v_py_db_node0001-7524:0x206c-40575]  0x7f70eee2f780 PythonExecContext::processBlock
 07:52:12.862 [Python-v_py_db_node0001-7524:0x206c-40575]  0x7f70eee2f780 [UserMessage] add2ints - Python UDx - Adding 2 ints!
 07:52:12.862 [Python-v_py_db_node0001-7524:0x206c-40575]  0x7f70eee2f780 [UserMessage] add2ints - Values: first_int is 100 second_int is 100

有关查看 Vertica 日志文件的详细信息,请参阅监控日志文件

将消息写入 UDX_EVENTS 表(仅限 C++)

在 C++ API 中,除了写入日志之外,您还可以将消息写入 UDX_EVENTS 系统表。写入系统表之后,您可以将来自所有节点的事件收集至一个位置中。

可以使用 ServerInterface.logEvent() 函数将消息写入此表。该函数将提取一个实参和一个映射。映射作为 Flex VMap 写入表的 RAW 列。以下示例显示 Parquet 导出器如何创建和记录此映射。

// Log exported parquet file details to v_monitor.udx_events
std::map<std::string, std::string> details;
details["file"] = escapedPath;
details["created"] = create_timestamp_;
details["closed"] = close_timestamp_;
details["rows"] = std::to_string(num_rows_in_file);
details["row_groups"] = std::to_string(num_row_groups_in_file);
details["size_mb"] = std::to_string((double)outputStream->Tell()/(1024*1024));
srvInterface.logEvent(details);

您可以从 VMap 中选择单个字段,如下例所示。

=> SELECT __RAW__['file'] FROM UDX_EVENTS;
                                   __RAW__
-----------------------------------------------------------------------------
 /tmp/export_tmpzLkrKq3a/450c4213-v_vmart_node0001-139770732459776-0.parquet
 /tmp/export_tmpzLkrKq3a/9df1c797-v_vmart_node0001-139770860660480-0.parquet
(2 rows)

或者,您可以定义一个视图,以便像列一样更轻松地直接查询字段。有关示例,请参阅 监控导出

5.5 - 处理取消请求

UDx 的用户可能会在运行时取消相应操作。Vertica 如何处理查询和 UDx 的取消取决于 UDx 正在隔离模式还是非隔离模式下运行。

  • 如果 UDx 正在非隔离模式下运行,则 Vertica 会在函数请求新的输入块或输出块时停止该函数,或者会等到该函数运行完成并丢弃结果。

  • 如果 UDx 正在隔离和非隔离模式模式下运行,则当正在运行函数的 zygote 进程在超时后继续处理函数时,Vertica 会终止该进程。

此外,您还可以在任何 UDx 中实施 cancel() 方法来执行任何必要的额外工作。取消查询时,Vertica 会调用您的函数。在 Udx 的生命周期内(从 setup()destroy())的任何时间都可能发生这种取消。

通过调用 isCanceled(),您可以在开始执行代价高昂的操作之前检查是否已取消查询。

5.5.1 - 实施 Cancel 回调

UDx 可以实施 cancel() 回调函数。如果调用 UDx 的查询已被取消,Vertica 将调用此函数。

通常可以实施此函数以对 UDx 已生成的任何附加处理执行有序关闭。例如,您可以让 cancel() 函数关闭 UDx 已生成的线程,或者也可以让该函数向第三方库指示它需要停止处理并退出。cancel() 函数应使 UDx 的函数类准备好进行销毁,因为 Vertica 会在 cancel() 函数已退出之后调用 UDx 的 destroy() 函数。

UDx 的默认 cancel() 行为是什么都不做。

cancel() 的合约为:

  • 对于每个 UDx 实例,Vertica 最多会调用一次 cancel()

  • Vertica 可以与 UDx 对象的任何其他方法(构造函数和析构函数除外)同时调用 cancel()

  • Vertica 可以从另一个线程调用 cancel(),因此实施应当是线程安全的。

  • Vertica 将调用 cancel() 来处理明确的用户取消或查询错误。

  • Vertica 不保证 cancel() 将运行完成。长期取消可能会被中止。

cancel() 的调用不会以任何方式与 UDx 的其他函数同步。如果要求处理函数在 cancel() 函数执行某项操作(例如终止线程)之前退出,您必须让这两个函数同步其操作。

如果调用 setup(),则 Vertica 始终调用 destroy()。取消并不能防止破坏。

有关实施 cancel() 的示例,请参阅 C++ 示例:可取消的 UDSource

5.5.2 - 在执行期间检查是否已取消查询

您可以调用 isCanceled() 方法来检查用户是否已取消查询。通常,在开始执行代价高昂的操作之前,您会使用在 UDx 中进行主要处理的方法来检查是否已取消查询。如果 isCanceled() 返回 true,则表明查询已被取消,您的方法应立即退出以防止浪费 CPU 时间。如果 UDx 未在隔离模式下运行,则 Vertica 无法停止函数,并且必须等待函数完成。如果 UDx 在隔离模式下运行,Vertica 最终会终止运行它的从属进程。

有关使用 isCanceled() 的示例,请参阅 C++ 示例:可取消的 UDSource

5.5.3 - C++ 示例:可取消的 UDSource

在 SDK 示例的 filelib.cpp 中找到的 FifoSource 示例演示了如何使用 cancel()isCanceled()。此源从指定的管道执行读取操作。与从文件读取不同,从管道读取可能会堵塞。因此,我们需要能够取消从此源加载数据。

为了管理取消操作,UDx 使用了管道,它是一种用于进程间通信的数据通道。某个进程可以将数据写入管道的写入端,并在另一个进程从管道的读取端读取数据之前保持可用。此示例不通过该管道传递数据;相反,它使用该管道来管理取消操作,如下面进一步所述。除了管道的两个文件描述符(每端一个)之外,UDx 还会为要从其读取的文件创建文件描述符。setup() 函数将创建管道,然后打开相应文件。

virtual void setup(ServerInterface &srvInterface) {
  // cancelPipe is a pipe used only for checking cancellation
  if (pipe(cancelPipe)) {
    vt_report_error(0, "Error opening control structure");
  }

  // handle to the named pipe from which we read data
  namedPipeFd = open(filename.c_str(), O_RDONLY | O_NONBLOCK);
  if (namedPipeFd < 0) {
    vt_report_error(0, "Error opening fifo [%s]", filename.c_str());
  }
}

现在有三个文件描述符:namedPipeFdcancelPipe[PIPE_READ]cancelPipe[PIPE_WRITE]。上述每个描述符最终都必须关闭。

此 UDx 使用 poll() 系统调用来等待数据从指定的管道到达 (namedPipeFd) 或等待取消查询 (cancelPipe[PIPE_READ])。process() 函数将执行轮询、检查结果、检查是否已取消查询、在需要时写入输出,然后返回结果。

virtual StreamState process(ServerInterface &srvInterface, DataBuffer &output) {
  struct pollfd pollfds[2] = {
    { namedPipeFd,           POLLIN, 0 },
    { cancelPipe[PIPE_READ], POLLIN, 0 }
  };

  if (poll(pollfds, 2, -1) < 0) {
    vt_report_error(1, "Error reading [%s]", filename.c_str());
  }

  if (pollfds[1].revents & (POLLIN | POLLHUP)) {
    /* This can only happen after cancel() has been called */
    VIAssert(isCanceled());
    return DONE;
  }

  VIAssert(pollfds[PIPE_READ].revents & (POLLIN | POLLHUP));

  const ssize_t amount = read(namedPipeFd, output.buf + output.offset, output.size - output.offset);
  if (amount < 0) {
    vt_report_error(1, "Error reading from fifo [%s]", filename.c_str());
  }

  if (amount == 0 || isCanceled()) {
    return DONE;
  } else {
    output.offset += amount;
    return OUTPUT_NEEDED;
  }
}

如果查询被取消,则 cancel() 函数会关闭管道的写入端。process() 下一次轮询输入时,它会在管道的读取端找不到输入时退出。否则,它会继续操作。此外,该函数还会调用 isCanceled() 以在返回 OUTPUT_NEEDED(表示已填满缓冲区且正在等待下游处理的信号)之前检查是否已取消查询。

cancel() 函数仅执行中断对 process() 的调用所需的工作。相反,始终需要执行(而不仅仅是为了取消查询)的清理是在 destroy() 或析构函数中完成的。cancel() 函数会关闭管道的写入端。(稍后将显示 helper 函数。)


virtual void cancel(ServerInterface &srvInterface) {
  closeIfNeeded(cancelPipe[PIPE_WRITE]);
}

cancel() 中关闭指定的管道并不安全,因为如果另一个进程(如另一个查询)要在 UDx 完成之前将文件描述符编号重用于新描述符,则关闭指定的管道可能会产生竞争条件。我们会改为在 destroy() 中关闭指定的管道以及管道的读取端。

virtual void destroy(ServerInterface &srvInterface) {
  closeIfNeeded(namedPipeFd);
  closeIfNeeded(cancelPipe[PIPE_READ]);
}

destroy() 中关闭管道的写入端并不安全,因为 cancel() 会关闭它且可以使用 destroy() 进行并发调用。因此,我们在析构函数中关闭管道的写入端。


~FifoSource() {
  closeIfNeeded(cancelPipe[PIPE_WRITE]);
}

Udx 会使用 helper 函数 closeIfNeeded() 来确保每个文件描述符正好关闭一次。

void closeIfNeeded(int &fd) {
  if (fd >= 0) {
    close(fd);
    fd = -1;
  }
}

5.6 - 聚合函数 (UDAF)

聚合函数可对值集执行操作并返回单个值。Vertica 提供标准内置聚合函数,例如 AVGMAXMIN。用户定义的聚合函数 (UDAF) 提供类似的功能:

  • 支持单个输入列(或一组)值并提供单个输出列。

  • 支持 RLE 解压缩。RLE 输入会在发送到 UDAF 之前进行解压缩。

  • 支持与 GROUP BYHAVING 子句一起使用。只能选择出现在 GROUP BY 子句中的列。

限制

以下限制适用于 UDAF:

  • 仅适用于 C++。

  • 不能在隔离模式下运行。

  • 不能与相关子查询一起使用。

5.6.1 - AggregateFunction 类

AggregateFunction 类可执行聚合。它会计算存储相关数据的每个数据库节点上的值,然后组合各个节点中的结果。您必须实施以下方法:

  • initAggregate() - 初始化类、定义变量以及设置变量的起始值。此函数必须是等幂的。

  • aggregate() - 在每个节点上执行的主要聚合操作。

  • combine() - 如果需要多次调用 aggregate(),Vertica 会调用 combine() 以将所有子聚合合并为最终的聚合。虽然不可能调用此方法,但您必须定义此方法。

  • terminate() - 终止函数并以列的形式返回结果。

AggregateFunction 类还提供了以下两种可选方法,您可以实施这两种方法以分配和释放资源: setup()destroy()。您应使用这些方法来分配和取消分配那些不通过 UDAF API 分配的资源(有关详细信息,请参阅为 UDx 分配资源)。

API

仅 C++ 支持聚合函数。

AggregateFunction API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes);

virtual void initAggregate(ServerInterface &srvInterface, IntermediateAggs &aggs)=0;

void aggregate(ServerInterface &srvInterface, BlockReader &arg_reader,
        IntermediateAggs &aggs);

virtual void combine(ServerInterface &srvInterface, IntermediateAggs &aggs_output,
        MultipleIntermediateAggs &aggs_other)=0;

virtual void terminate(ServerInterface &srvInterface, BlockWriter &res_writer,
        IntermediateAggs &aggs);

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface, const SizedColumnTypes &argTypes);

5.6.2 - AggregateFunctionFactory 类

AggregateFunctionFactory 类可指定元数据信息,例如,聚合函数的参数和返回类型。此类还可将 AggregateFunction 子类实例化。子类必须实施以下方法:

  • getPrototype() - 定义函数所接受的参数数量和数据类型。聚合函数接受单个参数。

  • getIntermediateTypes() - 定义函数所使用的中间变量。这些变量在组合 aggregate() 调用的结果时使用。

  • getReturnType() - 定义输出列的类型。

您的函数还可以实施 getParameterType(),后者定义了此函数使用的参数名称和类型。

当您调用 CREATE AGGREGATE FUNCTION SQL 语句以将函数添加到数据库编录时,Vertica 将使用这些数据。

API

仅 C++ 支持聚合函数。

AggregateFunctionFactory API 提供了以下通过子类扩展的方法:

virtual AggregateFunction *
        createAggregateFunction ServerInterface &srvInterface)=0;

virtual void getPrototype(ServerInterface &srvInterface,
        ColumnTypes &argTypes, ColumnTypes &returnType)=0;

virtual void getIntermediateTypes(ServerInterface &srvInterface,
        const SizedColumnTypes &inputTypes, SizedColumnTypes &intermediateTypeMetaData)=0;

virtual void getReturnType(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes, SizedColumnTypes &returnType)=0;

virtual void getParameterType(ServerInterface &srvInterface,
        SizedColumnTypes &parameterTypes);

5.6.3 - 包含 GROUP BY 子句的语句中的 UDAF 性能

如果调用 UDAF 的 SQL 语句还包含 GROUP BY 子句,该 UDAF 的性能可能会低于预期。例如:

=> SELECT a, MYUDAF(b) FROM sampletable GROUP BY a;

在类似于以上示例的语句中,Vertica 不会在调用 UDAF 的 aggregate() 方法之前将行数据合并到一起。相反,它会为每个数据行调用 aggregate() 一次。通常,让 Vertica 合并行数据的开销超过为每个数据行调用 aggregate() 的开销。但是,如果 UDAF 的 aggregate() 方法具有很大开销,您可能会发现 UDAF 的性能受到影响。

例如,假设内存由 aggregate() 分配。在包含 GROUP BY 子句的语句中调用此方法时,此方法将为每个数据行执行该内存分配。因为内存分配是一个成本相对高昂的过程,所以此分配会影响 UDAF 和查询的总体性能。

有两种方法可用于解决包含 GROUP BY 子句的语句中的 UDAF 性能问题:

  • 减少对 aggregate() 的每个调用的开销。如果可能,将任何分配或其他设置操作移到 UDAF 的 setup() 函数。

  • 声明一个特殊参数,以指示 Vertica 在调用 UDAF 时将行数据分组到一起。下文介绍此技术。

使用 _minimizeCallCount 参数

UDAF 可以指示 Vertica 始终将行数据分组到一起,以减少对其 aggregate() 方法调用的次数。要触发此行为,UDAF 必须声明一个名为 _minimizeCallCount 的整数参数。您不需要在 SQL 语句中为此参数设置值。如果 UDAF 声明此参数,将触发 Vertica 在调用 aggregate() 时将行数据分组到一起。

声明 _minimizeCallCount 参数的方法与声明其他 UDx 参数相同。有关详细信息,请参阅UDx 参数

5.6.4 - C++ 示例:平均值

在此示例中创建的 Average 聚合函数将计算列中各值的平均值。

您可以在 Vertica GitHub 页面上找到此示例中使用的源代码。

加载示例

使用 CREATE LIBRARY 和 CREATE AGGREGATE FUNCTION 声明函数:

=> CREATE LIBRARY AggregateFunctions AS
'/opt/vertica/sdk/examples/build/AggregateFunctions.so';
CREATE LIBRARY
=> CREATE aggregate function ag_avg AS LANGUAGE 'C++'
name 'AverageFactory' library AggregateFunctions;
CREATE AGGREGATE FUNCTION

使用示例

将该函数用作 SELECT 语句的一部分:


=> SELECT * FROM average;
id | count
----+---------
A  |       8
B  |       3
C  |       6
D  |       2
E  |       9
F  |       7
G  |       5
H  |       4
I  |       1
(9 rows)
=> SELECT ag_avg(count) FROM average;
ag_avg
--------
  5
(1 row)

AggregateFunction 实施

此示例会在 aggregate() 方法中添加输入实参值,并保留所添加值数量的计数器。服务器会在每个节点和不同的数据块上运行 aggregate() ,并在 combine() 方法中组合所有单独添加的值和计数器。最后,在 terminate() 方法中通过用总和除以已处理值的总数来计算平均值。

为进行此讨论,假设采用以下环境:

  • 三节点 Vertica 群集

  • 包含九个值的表列,这些值均匀分布在各个节点上。根据示意图,各个节点如下图所示:

该函数使用 sum 和 count 变量。sum 包含值的总和,count 包含值的计数。

首先,initAggregate() 会初始化变量并将其值设为零。

virtual void initAggregate(ServerInterface &srvInterface,
                           IntermediateAggs &aggs)
{
  try {
    VNumeric &sum = aggs.getNumericRef(0);
    sum.setZero();

    vint &count = aggs.getIntRef(1);
    count = 0;
  }
  catch(std::exception &e) {
    vt_ report_ error(0, "Exception while initializing intermediate aggregates: [% s]", e.what());
  }
}

aggregate() 函数会读取每个节点上的数据块并计算部分聚合。

void aggregate(ServerInterface &srvInterface,
               BlockReader &argReader,
               IntermediateAggs &aggs)
{
     try {
          VNumeric &sum = aggs.getNumericRef(0);
          vint     &count = aggs.getIntRef(1);
          do {
              const VNumeric &input = argReader.getNumericRef(0);
              if (!input.isNull()) {
               sum.accumulate(&input);
           count++;
              }
          } while (argReader.next());
    } catch(std::exception &e) {
       vt_ report_ error(0, " Exception while processing aggregate: [% s]", e.what());
    }
}

aggregate() 函数的每个已完成实例都会返回 sum 和 count 的多个部分聚合。下图使用 aggregate() 函数说明了此过程:

combine() 函数会将 average 函数的每个实例计算出的部分聚合组合在一起。

virtual void combine(ServerInterface &srvInterface,
                     IntermediateAggs &aggs,
                     MultipleIntermediateAggs &aggsOther)
{
    try {
        VNumeric       &mySum      = aggs.getNumericRef(0);
        vint           &myCount    = aggs.getIntRef(1);

        // Combine all the other intermediate aggregates
        do {
            const VNumeric &otherSum   = aggsOther.getNumericRef(0);
            const vint     &otherCount = aggsOther.getIntRef(1);

            // Do the actual accumulation
            mySum.accumulate(&otherSum);
            myCount += otherCount;

        } while (aggsOther.next());
    } catch(std::exception &e) {
        // Standard exception. Quit.
        vt_report_error(0, "Exception while combining intermediate aggregates: [%s]", e.what());
    }
}

下图显示了如何组合每个部分聚合:

aggregate() 函数评估完所有输入之后,Vertica 会调用 terminate() 函数。该函数会向调用者返回平均值。

virtual void terminate(ServerInterface &srvInterface,
                       BlockWriter &resWriter,
                       IntermediateAggs &aggs)
{
      try {
           const int32 MAX_INT_PRECISION = 20;
           const int32 prec = Basics::getNumericWordCount(MAX_INT_PRECISION);
           uint64 words[prec];
           VNumeric count(words,prec,0/*scale*/);
           count.copy(aggs.getIntRef(1));
           VNumeric &out = resWriter.getNumericRef();
           if (count.isZero()) {
               out.setNull();
           } else
               const VNumeric &sum = aggs.getNumericRef(0);
               out.div(&sum, &count);
        }
}

下图显示了 terminate() 函数的实施过程:

AggregateFunctionFactory 实施

使用 getPrototype() 函数,可以定义要发送到聚合函数的变量以及在聚合函数运行后返回给 Vertica 的变量。以下示例接受并返回了数值:

virtual void getPrototype(ServerInterface &srvfloaterface,
                          ColumnTypes &argTypes,
                          ColumnTypes &returnType)
    {
        argTypes.addNumeric();
        returnType.addNumeric();
    }

getIntermediateTypes() 函数可定义在聚合函数中使用的任何中间变量。中间变量是用于在多次调用聚合函数期间传递数据的值。它们用于组合结果,直到可以计算出最终结果。在此示例中,存在两个结果 - 总计(数字)和计数(整数)。

 virtual void getIntermediateTypes(ServerInterface &srvInterface,
                                   const SizedColumnTypes &inputTypes,
                                   SizedColumnTypes &intermediateTypeMetaData)
    {
        const VerticaType &inType = inputTypes.getColumnType(0);
        intermediateTypeMetaData.addNumeric(interPrec, inType.getNumericScale());
        intermediateTypeMetaData.addInt();
    }

getReturnType() 函数用于定义输出数据类型:

    virtual void getReturnType(ServerInterface &srvfloaterface,
                               const SizedColumnTypes &inputTypes,
                               SizedColumnTypes &outputTypes)
    {
        const VerticaType &inType = inputTypes.getColumnType(0);
        outputTypes.addNumeric(inType.getNumericPrecision(),
        inType.getNumericScale());
    }

5.7 - 分析函数 (UDAnF)

用户定义的分析函数 (UDAnF) 用于分析。有关 Vertica 内置分析的概述,请参阅 SQL 分析。与用户定义的标量函数 (UDSF) 一样,UDAnF 也必须为读取的每个数据行输出单个值,并且不能超过 9800 个实参。

与 UDSF 不同的是,UDAnF 的输入读取器和输出读取器可以单独前进。使用此功能,可以创建基于多个数据行计算输出值的分析功能。通过使读取器和写入器单独前进,可以创建与内置的分析函数(例如 LAG,此函数使用前面的行中的数据来输出当前行的值)相似的函数。

5.7.1 - AnalyticFunction 类

AnalyticFunction 类可执行分析处理。子类必须定义用于执行操作的 processPartition() 方法。该类可定义多种方法以设置和分解该函数。

执行操作

processPartition() 方法可读取数据分区,执行某种处理,以及为每个输入行输出单个值。

Vertica 将为每个数据分区调用 processPartition() 一次。它使用 AnalyticPartitionReader 对象来提供分区,您可以从该对象读取输入数据。此外,该对象上存在名为 isNewOrderByKey() 的唯一方法,此方法可返回布尔值以指示函数是否已发现具有一个或多个相同 ORDER BY 键的行。此方法对分析函数(例如示例 RANK 函数)很有用,分析函数需要以不同方式处理具有相同 ORDER BY 键的行和具有不同 ORDER BY 键的行。

方法已完成处理数据行之后,您可以通过对 AnalyticPartitionReader 调用 next() 使其前进到下一个输入行。

方法使用 Vertica 作为参数提供给 processPartition()AnalyticPartitionWriter 对象来写入输出值。此对象具有特定于数据类型的方法(例如 setInt())用于写入输出值。设置输出值之后,对 AnalyticPartitionWriter 调用 next() 以前进到下一个输出行。

设置和分解

AnalyticFunction 类定义了两种其他方法,您可以选择性地实施这两种方法以分配和释放资源: setup()destroy()。您应使用这些方法来分配和取消分配那些不通过 UDx API 分配的资源(有关详细信息,请参阅为 UDx 分配资源)。

API

AnalyticFunction API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes);

virtual void processPartition (ServerInterface &srvInterface,
        AnalyticPartitionReader &input_reader,
        AnalyticPartitionWriter &output_writer)=0;

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface, const SizedColumnTypes &argTypes);

AnalyticFunction API 提供了以下通过子类扩展的方法:

public void setup(ServerInterface srvInterface, SizedColumnTypes argTypes);

public abstract void processPartition (ServerInterface srvInterface,
        AnalyticPartitionReader input_reader, AnalyticPartitionWriter output_writer)
        throws UdfException, DestroyInvocation;

protected void cancel(ServerInterface srvInterface);

public void destroy(ServerInterface srvInterface, SizedColumnTypes argTypes);

5.7.2 - AnalyticFunctionFactory 类

AnalyticFunctionFactory 类将向 Vertica 提供有关 UDAnF 的元数据:其参数数量和数据类型及其返回值的数据类型。该类还会实例化 AnalyticFunction 的子类。

AnalyticFunctionFactory 子类必须实施以下方法:

  • getPrototype() 可描述函数的输入参数和输出值。可以通过对传递到方法的两个 ColumnTypes 对象调用函数来设置这些值。

  • createAnalyticFunction() 可提供 AnalyticFunction 的实例,Vertica 可以调用此实例以处理 UDAnF 函数调用。

  • getReturnType() 可提供有关函数输出的详细信息。通过此方法,您可以设置输出值的宽度(如果函数将返回可变宽度值,例如 VARCHAR),或者设置输出值的精度(如果该输出值具有可设置的精度,例如 TIMESTAMP)。

API

AnalyticFunctionFactory API 提供了以下通过子类扩展的方法:

virtual AnalyticFunction * createAnalyticFunction (ServerInterface &srvInterface)=0;

virtual void getPrototype(ServerInterface &srvInterface,
        ColumnTypes &argTypes, ColumnTypes &returnType)=0;

virtual void getReturnType(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes, SizedColumnTypes &returnType)=0;

virtual void getParameterType(ServerInterface &srvInterface,
        SizedColumnTypes &parameterTypes);

AnalyticFunctionFactory API 提供了以下通过子类扩展的方法:

public abstract AnalyticFunction createAnalyticFunction (ServerInterface srvInterface);

public abstract void getPrototype(ServerInterface srvInterface, ColumnTypes argTypes, ColumnTypes returnType);

public abstract void getReturnType(ServerInterface srvInterface, SizedColumnTypes argTypes,
        SizedColumnTypes returnType) throws UdfException;

public void getParameterType(ServerInterface srvInterface, SizedColumnTypes parameterTypes);

5.7.3 - C++ 示例:排名

Rank 分析函数根据行的排序顺序对其进行排序。此 UDx 的 Java 版本包含在 /opt/vertica/sdk/examples 中。

加载和使用示例

以下示例显示了如何将函数加载至 Vertica 中。假设包含该函数的 AnalyticFunctions.so 库已复制到启动程序节点上数据库管理员用户的主目录中。

=> CREATE LIBRARY AnalyticFunctions AS '/home/dbadmin/AnalyticFunctions.so';
CREATE LIBRARY
=> CREATE ANALYTIC FUNCTION an_rank AS LANGUAGE 'C++'
   NAME 'RankFactory' LIBRARY AnalyticFunctions;
CREATE ANALYTIC FUNCTION

以下是使用此 rank 函数(名为 an_rank)的示例:

=> SELECT * FROM hits;
      site       |    date    | num_hits
-----------------+------------+----------
 www.example.com | 2012-01-02 |       97
 www.vertica.com | 2012-01-01 |   343435
 www.example.com | 2012-01-01 |      123
 www.example.com | 2012-01-04 |      112
 www.vertica.com | 2012-01-02 |   503695
 www.vertica.com | 2012-01-03 |   490387
 www.example.com | 2012-01-03 |      123
(7 rows)
=> SELECT site,date,num_hits,an_rank()
   OVER (PARTITION BY site ORDER BY num_hits DESC)
   AS an_rank FROM hits;
      site       |    date    | num_hits | an_rank
-----------------+------------+----------+---------
 www.example.com | 2012-01-03 |      123 |       1
 www.example.com | 2012-01-01 |      123 |       1
 www.example.com | 2012-01-04 |      112 |       3
 www.example.com | 2012-01-02 |       97 |       4
 www.vertica.com | 2012-01-02 |   503695 |       1
 www.vertica.com | 2012-01-03 |   490387 |       2
 www.vertica.com | 2012-01-01 |   343435 |       3
(7 rows)

与内置的 RANK 分析函数一样,在 ORDER BY 列(此示例中的 num_hits)具有相同值的行具有相同排名,但排名会持续增加,以便下一个具有不同 ORDER BY 键的行可基于其前面的行数获得排名值。

AnalyticFunction 实施

以下代码定义了一个名为 RankAnalyticFunction 子类。该子类基于 SDK 示例目录中分发的示例代码。

/**
 * User-defined analytic function: Rank - works mostly the same as SQL-99 rank
 * with the ability to define as many order by columns as desired
 *
 */
class Rank : public AnalyticFunction
{
    virtual void processPartition(ServerInterface &srvInterface,
                                  AnalyticPartitionReader &inputReader,
                                  AnalyticPartitionWriter &outputWriter)
    {
        // Always use a top-level try-catch block to prevent exceptions from
        // leaking back to Vertica or the fenced-mode side process.
        try {
            rank = 1; // The rank to assign a row
            rowCount = 0; // Number of rows processed so far
            do {
                rowCount++;
                // Do we have a new order by row?
                if (inputReader.isNewOrderByKey()) {
                    // Yes, so set rank to the total number of rows that have been
                    // processed. Otherwise, the rank remains the same value as
                    // the previous iteration.
                    rank = rowCount;
                }
                // Write the rank
                outputWriter.setInt(0, rank);
                // Move to the next row of the output
                outputWriter.next();
            } while (inputReader.next()); // Loop until no more input
        } catch(exception& e) {
            // Standard exception. Quit.
            vt_report_error(0, "Exception while processing partition: %s", e.what());
        }
    }
private:
    vint rank, rowCount;
};

在此示例中,processPartition() 方法实际上不读取输入行中的任何数据;只会遍历这些行。该方法不需要读取数据;它只需要计算已读取的行数并确定这些行是否具有与上一行相同的 ORDER BY 键。如果当前行为新的 ORDER BY 键,则排名设置为已处理的总行数。如果当前行与上一行的 ORDER BY 值相同,则排名保持不变。

请注意,此函数包含顶级 try-catch 块。所有 UDx 函数都应始终包含该块,以防止偶然发生的异常传递回 Vertica(如果在非隔离模式下运行函数)或从属进程。

AnalyticFunctionFactory 实施

以下代码定义了与 Rank 分析函数对应的 AnalyticFunctionFactory

class RankFactory : public AnalyticFunctionFactory
{
    virtual void getPrototype(ServerInterface &srvInterface,
                                ColumnTypes &argTypes, ColumnTypes &returnType)
    {
        returnType.addInt();
    }
    virtual void getReturnType(ServerInterface &srvInterface,
                               const SizedColumnTypes &inputTypes,
                               SizedColumnTypes &outputTypes)
    {
        outputTypes.addInt();
    }
    virtual AnalyticFunction *createAnalyticFunction(ServerInterface
                                                        &srvInterface)
    { return vt_createFuncObj(srvInterface.allocator, Rank); }
};

RankFactory 子类定义的第一种方法 getPrototype() 设置了返回值的数据类型。因为 Rank UDAnF 不读取输入内容,因此不会通过对传入 argTypes 参数的 ColumnTypes 对象调用方法定义任何实参。

下一种方法是 getReturnType()。如果函数返回需要定义宽度或精度的数据类型,则 getReturnType() 方法的实施将对作为参数传入的 SizedColumnType 对象调用某个方法,以向 Vertica 说明该宽度或精度。 Rank 将返回固定宽度的数据类型 (INTEGER),因此无需设置其输出的精度或宽度;它只是调用 addInt() 以报告其输出数据类型而已。

最后,RankFactory 定义了 createAnalyticFunction() 方法,该方法会返回一个 Vertica 可以调用的 AnalyticFunction 类的实例。此代码大部分是样板。您只需在对 vt_createFuncObj() 发出的调用中添加分析函数类的名称即可,此子类将为您分配对象。

5.8 - 标量函数 (UDSF)

用户定义的标量函数 (UDSF) 为读取的每个数据行返回单个值。您可以在任何可使用内置 Vertica 函数的地方使用 UDSF。您通常可以开发 UDSF 来执行在使用 SQL 语句和函数时因过于复杂或速度过慢而难以执行的数据处理。UDSF 还可让您在 Vertica 中使用由第三方库提供的分析函数,并同时保持高性能。

UDSF 返回单个列。您可以在 ROW 中自动返回多个值。ROW 是一组属性值对。在以下示例中,div_with_rem 是一个执行除法运算的 UDSF,将商和余数作为整数返回:

=> SELECT div_with_rem(18,5);
        div_with_rem
------------------------------
 {"quotient":3,"remainder":3}
(1 row)

从 UDSF 返回的 ROW 不能用作 COUNT 的实参。

或者,您可以自己构造一个复杂的返回值,如复杂类型作为实参中所述。

UDSF 必须为每个输入行返回值(除非生成错误;有关详细信息,请参阅处理错误)。未能为输入行返回值会导致不正确的结果,如果未在 隔离和非隔离模式 中运行,可能会破坏 Vertica 服务器的稳定性。

一个 UDSF 最多可以有 9800 个实参。

5.8.1 - ScalarFunction 类

ScalarFunction 类是 UDSF 的核心。子类必须定义用于执行标量操作的 processBlock() 方法。该类可定义多种方法以设置和分解该函数。

对于使用 C++ 语言编写的标量函数,您可以提供有助于优化查询的信息。请参阅提高查询性能(仅限 C++)

执行操作

processBlock() 方法可执行您希望 UDSF 执行的所有处理。当用户在 SQL 语句中调用您的函数时,Vertica 会将来自函数参数的数据绑定在一起并将其传递至 processBlock()

processBlock() 方法的输入和输出由 BlockReaderBlockWriter 类的对象提供。这些类定义了用于为 UDSF 读取输入数据和写入输出数据的方法。

开发 UDSF 的大部分工作是编写 processBlock()。对函数的所有处理在此阶段进行。UDSF 应遵循以下基本模式:

  • 使用特定于数据类型的方法从 BlockReader 对象读入一组实参。

  • 以某种方式处理数据。

  • 使用 BlockWriter 类的特定于数据类型的方法之一输出结果值。

  • 通过调用 BlockWriter.next()BlockReader.next() 分别前进到下一个输出行和下一个输入行。

此过程将持续到没有更多数据行可供读取为止(BlockReader.next() 将返回 false)。

您必须确保 processBlock() 读取所有输入行并为每个行输出单个值。如未遵守此规则,将损坏 Vertica 为获取 UDSF 的输出而读取的数据结构。此规则的唯一例外出现在 processBlock() 函数将错误报告回 Vertica 时(请参阅处理错误)。在这种情况下,Vertica 不会尝试读取由 UDSF 生成的不完整结果集。

设置和分解

ScalarFunction 类定义了两种其他方法,您可以选择性地实施这两种方法以分配和释放资源: setup()destroy()。您应使用这些方法来分配和取消分配那些不通过 UDx API 分配的资源(有关详细信息,请参阅为 UDx 分配资源)。

注意

  • 虽然为 ScalarFunction 子类选择的名称不必与稍后为其分配的 SQL 函数名称匹配,但 Vertica 会将名称设为相同视为最佳实践。

  • 请勿假设系统会从将函数实例化的同一个线程调用该函数。

  • 可以调用 ScalarFunction 子类的同一个实例以处理多个数据块。

  • 不保证发送到 processBlock() 的输入行具有特定顺序。

  • 写入太多输出行会导致 Vertica 发出超过边界错误。

API

ScalarFunction API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes);

virtual void processBlock(ServerInterface &srvInterface,
        BlockReader &arg_reader, BlockWriter &res_writer)=0;

virtual void getOutputRange (ServerInterface &srvInterface,
        ValueRangeReader &inRange, ValueRangeWriter &outRange)

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface, const SizedColumnTypes &argTypes);

ScalarFunction API 提供了以下通过子类扩展的方法:

public void setup(ServerInterface srvInterface, SizedColumnTypes argTypes);

public abstract void processBlock(ServerInterface srvInterface, BlockReader arg_reader,
        BlockWriter res_writer) throws UdfException, DestroyInvocation;

protected void cancel(ServerInterface srvInterface);

public void destroy(ServerInterface srvInterface, SizedColumnTypes argTypes);

ScalarFunction API 提供了以下通过子类扩展的方法:


def setup(self, server_interface, col_types)

def processBlock(self, server_interface, block_reader, block_writer)

def destroy(self, server_interface, col_types)

实施 主函数 API 以定义标量函数:

FunctionName <- function(input.data.frame, parameters.data.frame) {
  # Computations

  # The function must return a data frame.
  return(output.data.frame)
}

5.8.2 - ScalarFunctionFactory 类

ScalarFunctionFactory 类将向 Vertica 提供有关 UDSF 的元数据:其参数数量和数据类型及其返回值的数据类型。该类还会实例化 ScalarFunction 的子类。

方法

您必须在 ScalarFunctionFactory 子类中实施以下方法:

  • createScalarFunction() 实例化 ScalarFunction 子类。如果采用 C++ 进行编写,则您可以使用 ScalarFunction 子类的名称调用 vt_createFuncObj 宏。此宏会为您分配类并将该类实例化。

  • getPrototype() 会向 Vertica 提供有关 UDSF 的参数和返回类型。除了一个 ServerInterface 对象之外,此方法还获取另外两个 ColumnTypes 对象。在此函数中唯一需要执行的操作是,对这两个对象调用类函数以构建参数列表和返回值类型。如果返回多个值,则将结果打包为 ROW 类型。

定义工厂类之后,您需要调用 RegisterFactory 宏。此宏可将工厂类的成员实例化,以便 Vertica 可以与该成员交互并提取其中包含的有关 UDSF 的元数据。

声明返回值

如果函数返回特定大小的列(一种长度可变的返回数据类型,例如 VARCHAR)、需要精度的值或多个值,则必须实施 getReturnType()。此方法由 Vertica 调用,以用于查找在每个结果行中返回的数据的长度和精度。此方法的返回值取决于 processBlock() 方法所返回的数据类型:

  • CHAR、(LONG) VARCHAR、BINARY 和 (LONG) VARBINARY 将返回最大长度。

  • NUMERIC 类型可指定精度和小数位数。

  • TIME 和 TIMESTAMP 值可指定精度(无论是否带有时区均可)。

  • INTERVAL YEAR TO MONTH 可指定范围。

  • INTERVAL DAY TO SECOND 可指定精度和范围。

  • ARRAY 类型将指定元素的最大数量。

如果 UDSF 不返回以上数据类型之一,而返回单个值,则它不需要实施 getReturnType() 方法。

传递到 getReturnType() 方法的输入是一个 SizedColumnTypes 对象,其中包含输入参数类型及其长度。此对象将传递到 processBlock() 函数的一个实例。getReturnType() 的实施必须从该输入提取数据类型和长度,并确定输出行的长度或精度,然后将此信息保存在 SizedColumnTypes 类的另一个实例中。

API

ScalarFunctionFactory API 提供了以下通过子类扩展的方法:

virtual ScalarFunction * createScalarFunction(ServerInterface &srvInterface)=0;

virtual void getPrototype(ServerInterface &srvInterface,
        ColumnTypes &argTypes, ColumnTypes &returnType)=0;

virtual void getReturnType(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes, SizedColumnTypes &returnType);

virtual void getParameterType(ServerInterface &srvInterface,
        SizedColumnTypes &parameterTypes);

ScalarFunctionFactory API 提供了以下通过子类扩展的方法:

public abstract ScalarFunction createScalarFunction(ServerInterface srvInterface);

public abstract void getPrototype(ServerInterface srvInterface, ColumnTypes argTypes, ColumnTypes returnType);

public void getReturnType(ServerInterface srvInterface, SizedColumnTypes argTypes,
        SizedColumnTypes returnType) throws UdfException;

public void getParameterType(ServerInterface srvInterface, SizedColumnTypes parameterTypes);

ScalarFunctionFactory API 提供了以下通过子类扩展的方法:

def createScalarFunction(self, srv)

def getPrototype(self, srv_interface, arg_types, return_type)

def getReturnType(self, srv_interface, arg_types, return_type)

实施 工厂函数 API 以定义标量函数工厂:

FunctionNameFactory <- function() {
  list(name    = FunctionName,
       udxtype = c("scalar"),
       intype  = c("int"),
       outtype = c("int"))
}

5.8.3 - 设置 null 输入和波动性行为

一般情况下,Vertica 会为查询中的每个数据行调用 UDSF。在某些情况下,Vertica 可以避免执行您的 UDSF。您可以告知 Vertica 何时能够跳过函数调用,而只需通过更改函数的波动性和严格性设置来提供返回值本身。

  • 函数的波动性 是指在传递相同的参数时,它是否始终返回相同的输出值。根据其行为,Vertica 可以缓存实参和返回值。如果用户使用相同的实参集来调用 UDSF,Vertica 会返回缓存值,而不会调用 UDSF。

  • 函数的严格性 是指它如何对 NULL 实参作出响应。如果“任何”实参为 NULL 时它始终返回 NULL,则 Vertica 可以只返回 NULL,而无需调用函数。此优化还可以节省工作量,因为您无需在 UDSF 代码中测试并处理 Null 参数。

可以通过在 ScalarFunctionFactory 类的构造函数中设置 volstrict 字段来指示函数的可变性和空值处理。

可变性设置

要指示函数的可变性,请将 vol 字段设置为以下值之一:

示例

以下示例显示了使函数不可变的 Add2ints 示例工厂类的某个版本。

class Add2intsImmutableFactory : public Vertica::ScalarFunctionFactory
{
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface &srvInterface)
    { return vt_createFuncObj(srvInterface.allocator, Add2ints); }
    virtual void getPrototype(Vertica::ServerInterface &srvInterface,
                              Vertica::ColumnTypes &argTypes,
                              Vertica::ColumnTypes &returnType)
    {
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }

public:
    Add2intsImmutableFactory() {vol = IMMUTABLE;}
};
RegisterFactory(Add2intsImmutableFactory);

以下示例演示了将 Add2IntsFactoryvol 字段设置为 IMMUTABLE 以向 Vertica 说明可以缓存实参和返回值。

public class Add2IntsFactory extends ScalarFunctionFactory {

    @Override
    public void getPrototype(ServerInterface srvInterface, ColumnTypes argTypes, ColumnTypes returnType){
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }

    @Override
    public ScalarFunction createScalarFunction(ServerInterface srvInterface){
        return new Add2Ints();
    }

    // Class constructor
    public Add2IntsFactory() {
        // Tell Vertica that the same set of arguments will always result in the
        // same return value.
        vol = volatility.IMMUTABLE;
    }
}

Null 输入行为

要指示函数如何响应 Null 输入,请将 strictness 字段设置为以下值之一:

示例

以下 C++ 示例演示了设置 Add2ints 的 null 行为以使 Vertica 不调用带有 NULL 值的函数。

class Add2intsNullOnNullInputFactory : public Vertica::ScalarFunctionFactory
{
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface &srvInterface)
    { return vt_createFuncObj(srvInterface.allocator, Add2ints); }
    virtual void getPrototype(Vertica::ServerInterface &srvInterface,
                              Vertica::ColumnTypes &argTypes,
                              Vertica::ColumnTypes &returnType)
    {
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt();
    }

public:
    Add2intsNullOnNullInputFactory() {strict = RETURN_NULL_ON_NULL_INPUT;}
};
RegisterFactory(Add2intsNullOnNullInputFactory);

5.8.4 - 提高查询性能(仅限 C++)

在评估查询时,Vertica 可以利用有关值范围的可用信息。例如,如果数据已分区并且查询通过分区值限制输出,则 Vertica 可以忽略不可能包含满足查询的数据的分区。同样,对于标量函数,Vertica 可以不处理数据中函数返回的值不可能影响结果的行。

考虑一个表(表中包含数百万行客户订单数据)和一个标量函数(该函数计算为订单中的所有项目支付的总价格)。查询使用 WHERE 子句将结果限制为高于给定值的订单。对数据块调用标量函数;如果该块中没有行可以产生目标值,则跳过该块的处理可以提高查询性能。

用 C++ 编写的标量函数可以实施 getOutputRange 方法。在调用 processBlock 之前,Vertica 调用 getOutputRange 以在给定输入范围的情况下确定此块的最小和最大返回值。然后,它决定是否调用 processBlock 来执行计算。

Add2Ints 示例实施了此函数。最小输出值是两个输入的最小值之和,最大输出值是每个输入的最大值之和。此函数不考虑单个行。考虑以下输入:

   a  |  b
------+------
  21  | 92
 500  | 19
 111  | 11

两个输入的最小值是 21 和 11,因此函数将 32 报告为输出范围的下限。最大输入值为 500 和 92,因此它将 592 报告为输出范围的上限。任何输入行的返回值均大于 32,小于 592。

getOutputRange 的目的是快速消除输出肯定超出范围的调用。例如,如果查询包含 "WHERE Add2Ints(a,b) > 600",则可以跳过该数据块。仍然可能存在调用 getOutputRangeprocessBlock 没有返回结果的情况。如果查询包含 "WHERE Add2Ints(a,b) > 500",则 getOutputRange 不会消除此数据块。

Add2Ints 实施 getOutputRange,如下所示:


    /*
     * This method computes the output range for this scalar function from
     *   the ranges of its inputs in a single invocation.
     *
     * The input ranges are retrieved via inRange
     * The output range is returned via outRange
     */
    virtual void getOutputRange(Vertica::ServerInterface &srvInterface,
                                Vertica::ValueRangeReader &inRange,
                                Vertica::ValueRangeWriter &outRange)
    {
        if (inRange.hasBounds(0) && inRange.hasBounds(1)) {
            // Input ranges have bounds defined
            if (inRange.isNull(0) || inRange.isNull(1)) {
                // At least one range has only NULL values.
                // Output range can only have NULL values.
                outRange.setNull();
                outRange.setHasBounds();
                return;
            } else {
                // Compute output range
                const vint& a1LoBound = inRange.getIntRefLo(0);
                const vint& a2LoBound = inRange.getIntRefLo(1);
                outRange.setIntLo(a1LoBound + a2LoBound);

                const vint& a1UpBound = inRange.getIntRefUp(0);
                const vint& a2UpBound = inRange.getIntRefUp(1);
                outRange.setIntUp(a1UpBound + a2UpBound);
            }
        } else {
            // Input ranges are unbounded. No output range can be defined
            return;
        }

        if (!inRange.canHaveNulls(0) && !inRange.canHaveNulls(1)) {
            // There cannot be NULL values in the output range
            outRange.setCanHaveNulls(false);
        }

        // Let Vertica know that the output range is bounded
        outRange.setHasBounds();
    }

如果 getOutputRange 产生错误,Vertica 会发出警告并且不会为当前查询再次调用该方法。

5.8.5 - C++ 示例:Add2Ints

以下示例显示了基本的 ScalarFunction 子类,它名为 Add2ints。顾名思义,它将两个整数相加,并返回单个整数结果。

有关完整的源代码,请参阅/opt/vertica/sdk/examples/ScalarFunctions/Add2Ints.cpp。此 UDx 的 Java 和 Python 版本包含在 /opt/vertica/sdk/examples 中。

加载和使用示例

使用 CREATE LIBRARY 加载包含函数的库,然后按下例所示使用 CREATE FUNCTION(标量) 声明该函数:

=> CREATE LIBRARY ScalarFunctions AS '/home/dbadmin/examples/ScalarFunctions.so';

=> CREATE FUNCTION add2ints AS LANGUAGE 'C++' NAME 'Add2IntsFactory' LIBRARY ScalarFunctions;

以下示例显示了如何使用该函数:

=> SELECT Add2Ints(27,15);
 Add2ints
----------
       42
(1 row)
=> SELECT * FROM MyTable;
  a  | b
-----+----
   7 |  0
  12 |  2
  12 |  6
  18 |  9
   1 |  1
  58 |  4
 450 | 15
(7 rows)
=> SELECT * FROM MyTable WHERE Add2ints(a, b) > 20;
  a  | b
-----+----
  18 |  9
  58 |  4
 450 | 15
(3 rows)

函数实施

标量函数在 processBlock 方法中进行计算:


class Add2Ints : public ScalarFunction
 {
    public:
   /*
     * This method processes a block of rows in a single invocation.
     *
     * The inputs are retrieved via argReader
     * The outputs are returned via resWriter
     */
    virtual void processBlock(ServerInterface &srvInterface,
                              BlockReader &argReader,
                              BlockWriter &resWriter)
    {
        try {
            // While we have inputs to process
            do {
                if (argReader.isNull(0) || argReader.isNull(1)) {
                    resWriter.setNull();
                } else {
                    const vint a = argReader.getIntRef(0);
                    const vint b = argReader.getIntRef(1);
                    resWriter.setInt(a+b);
                }
                resWriter.next();
            } while (argReader.next());
        } catch(std::exception& e) {
            // Standard exception. Quit.
            vt_report_error(0, "Exception while processing block: [%s]", e.what());
        }
    }

  // ...
};

实施 getOutputRange,这是可选的,允许您的函数跳过结果不在目标范围内的行。例如,如果 WHERE 子句将查询结果限制在某个范围内,则无需为不可能在该范围内的情况调用该函数。


    /*
     * This method computes the output range for this scalar function from
     *   the ranges of its inputs in a single invocation.
     *
     * The input ranges are retrieved via inRange
     * The output range is returned via outRange
     */
    virtual void getOutputRange(Vertica::ServerInterface &srvInterface,
                                Vertica::ValueRangeReader &inRange,
                                Vertica::ValueRangeWriter &outRange)
    {
        if (inRange.hasBounds(0) && inRange.hasBounds(1)) {
            // Input ranges have bounds defined
            if (inRange.isNull(0) || inRange.isNull(1)) {
                // At least one range has only NULL values.
                // Output range can only have NULL values.
                outRange.setNull();
                outRange.setHasBounds();
                return;
            } else {
                // Compute output range
                const vint& a1LoBound = inRange.getIntRefLo(0);
                const vint& a2LoBound = inRange.getIntRefLo(1);
                outRange.setIntLo(a1LoBound + a2LoBound);

                const vint& a1UpBound = inRange.getIntRefUp(0);
                const vint& a2UpBound = inRange.getIntRefUp(1);
                outRange.setIntUp(a1UpBound + a2UpBound);
            }
        } else {
            // Input ranges are unbounded. No output range can be defined
            return;
        }

        if (!inRange.canHaveNulls(0) && !inRange.canHaveNulls(1)) {
            // There cannot be NULL values in the output range
            outRange.setCanHaveNulls(false);
        }

        // Let Vertica know that the output range is bounded
        outRange.setHasBounds();
    }

工厂实施

工厂实例化了类的一个成员 (createScalarFunction),并且还描述了函数的输入和输出 (getPrototype):

class Add2IntsFactory : public ScalarFunctionFactory
{
    // return an instance of Add2Ints to perform the actual addition.
    virtual ScalarFunction *createScalarFunction(ServerInterface &interface)
    { return vt_createFuncObject<Add2Ints>(interface.allocator); }

    // This function returns the description of the input and outputs of the
    // Add2Ints class's processBlock function.  It stores this information in
    // two ColumnTypes objects, one for the input parameters, and one for
    // the return value.
    virtual void getPrototype(ServerInterface &interface,
                              ColumnTypes &argTypes,
                              ColumnTypes &returnType)
    {
        argTypes.addInt();
        argTypes.addInt();

        // Note that ScalarFunctions *always* return a single value.
        returnType.addInt();
    }
};

RegisterFactory 宏

使用 RegisterFactory 宏注册一个 UDx。该宏将对工厂类进行实例化并将其包含的元数据供 Vertica 访问。要调用该宏,请将您的工厂类的名称传递给它。

RegisterFactory(Add2IntsFactory);

5.8.6 - Python 示例:currency_convert

currency_convert 标量函数从表中读取两个值,即币种和价值。然后它将项目的价值转换为美元,返回一个浮点结果。

您可以在 Vertica Github 存储库中找到更多 UDx 示例:https://github.com/vertica/UDx-Examples

UDSF Python 代码

import vertica_sdk
import decimal

rates2USD = {'USD': 1.000,
             'EUR': 0.89977,
             'GBP': 0.68452,
             'INR': 67.0345,
             'AUD': 1.39187,
             'CAD': 1.30335,
             'ZAR': 15.7181,
             'XXX': -1.0000}

class currency_convert(vertica_sdk.ScalarFunction):
    """Converts a money column to another currency

    Returns a value in USD.

    """
    def __init__(self):
        pass

    def setup(self, server_interface, col_types):
        pass

    def processBlock(self, server_interface, block_reader, block_writer):
        while(True):
            currency = block_reader.getString(0)
            try:
                rate = decimal.Decimal(rates2USD[currency])

            except KeyError:
                server_interface.log("ERROR: {} not in dictionary.".format(currency))
                # Scalar functions always need a value to move forward to the
                # next input row. Therefore, we need to assign it a value to
                # move beyond the error.
                currency = 'XXX'
                rate = decimal.Decimal(rates2USD[currency])

            starting_value = block_reader.getNumeric(1)
            converted_value = decimal.Decimal(starting_value  / rate)
            block_writer.setNumeric(converted_value)
            block_writer.next()
            if not block_reader.next():
                break

    def destroy(self, server_interface, col_types):
        pass

class currency_convert_factory(vertica_sdk.ScalarFunctionFactory):

    def createScalarFunction(self, srv):
        return currency_convert()

    def getPrototype(self, srv_interface, arg_types, return_type):
        arg_types.addVarchar()
        arg_types.addNumeric()
        return_type.addNumeric()

    def getReturnType(self, srv_interface, arg_types, return_type):
        return_type.addNumeric(9,4)

加载函数和库

创建库和函数。

=> CREATE LIBRARY pylib AS '/home/dbadmin/python_udx/currency_convert/currency_convert.py' LANGUAGE 'Python';
CREATE LIBRARY
=> CREATE FUNCTION currency_convert AS LANGUAGE 'Python' NAME 'currency_convert_factory' LIBRARY pylib fenced;
CREATE FUNCTION

使用函数查询数据

以下查询显示了如何使用 UDSF 运行查询。

=> SELECT product, currency_convert(currency, value) AS cost_in_usd
    FROM items;
   product    | cost_in_usd
--------------+-------------
 Shoes        |    133.4008
 Soccer Ball  |    110.2817
 Coffee       |     13.5190
 Surfboard    |    176.2593
 Hockey Stick |     76.7177
 Car          |  17000.0000
 Software     |     10.4424
 Hamburger    |      7.5000
 Fish         |    130.4272
 Cattle       |    269.2367
(10 rows)

5.8.7 - Python 示例:validate_url

validate_url 标量函数会从表中读取字符串,即 URL。然后,它会验证该 URL 是否会做出响应,以返回状态代码或指示尝试失败的字符串。

您可以在 Vertica Github 存储库中找到更多 UDx 示例:https://github.com/vertica/UDx-Examples

UDSF Python 代码


import vertica_sdk
import urllib.request
import time

class validate_url(vertica_sdk.ScalarFunction):
    """Validates HTTP requests.

    Returns the status code of a webpage. Pages that cannot be accessed return
    "Failed to load page."

    """

    def __init__(self):
        pass

    def setup(self, server_interface, col_types):
        pass

    def processBlock(self, server_interface, arg_reader, res_writer):
        # Writes a string to the UDx log file.
        server_interface.log("Validating webpage accessibility - UDx")

        while(True):
            url = arg_reader.getString(0)
            try:
                status = urllib.request.urlopen(url).getcode()
                # Avoid overwhelming web servers -- be nice.
                time.sleep(2)
            except (ValueError, urllib.error.HTTPError, urllib.error.URLError):
                status = 'Failed to load page'
            res_writer.setString(str(status))
            res_writer.next()
            if not arg_reader.next():
                # Stop processing when there are no more input rows.
                break

    def destroy(self, server_interface, col_types):
        pass

class validate_url_factory(vertica_sdk.ScalarFunctionFactory):

    def createScalarFunction(self, srv):
        return validate_url()

    def getPrototype(self, srv_interface, arg_types, return_type):
        arg_types.addVarchar()
        return_type.addChar()

    def getReturnType(self, srv_interface, arg_types, return_type):
        return_type.addChar(20)

加载函数和库

创建库和函数。

=> CREATE OR REPLACE LIBRARY pylib AS 'webpage_tester/validate_url.py' LANGUAGE 'Python';
=> CREATE OR REPLACE FUNCTION validate_url AS LANGUAGE 'Python' NAME 'validate_url_factory' LIBRARY pylib fenced;

使用函数查询数据

以下查询显示了如何使用 UDSF 运行查询。

=> SELECT url, validate_url(url) AS url_status FROM webpages;
                     url                       |      url_status
-----------------------------------------------+----------------------
 http://www.vertica.com/documentation/vertica/ | 200
 http://www.google.com/                        | 200
 http://www.mass.gov.com/                      | Failed to load page
 http://www.espn.com                           | 200
 http://blah.blah.blah.blah                    | Failed to load page
 http://www.microfocus.com/                    | 200
(6 rows)

5.8.8 - Python 示例:矩阵乘法

Python UDx 可以接受并返回复杂类型。MatrixMultiply 类会乘以输入矩阵,并返回生成的矩阵乘积。这些矩阵将以二维数组来表示。为了执行矩阵乘法运算,第一个输入矩阵中的列数必须等于第二个输入矩阵中的行数。

完整的源代码位于 /opt/vertica/sdk/examples/python/ScalarFunctions.py 中。

加载和使用示例

加载库并创建函数,如下所示:

=> CREATE OR REPLACE LIBRARY ScalarFunctions AS '/home/dbadmin/examples/python/ScalarFunctions.py' LANGUAGE 'Python';

=> CREATE FUNCTION MatrixMultiply AS LANGUAGE 'Python' NAME 'matrix_multiply_factory' LIBRARY ScalarFunctions;

您可以创建输入矩阵,然后调用诸如以下函数:


=> CREATE TABLE mn (id INTEGER, data ARRAY[ARRAY[INTEGER, 3], 2]);
CREATE TABLE

=> CREATE TABLE np (id INTEGER, data ARRAY[ARRAY[INTEGER, 2], 3]);
CREATE TABLE

=> COPY mn FROM STDIN PARSER fjsonparser();
{"id": 1, "data": [[1, 2, 3], [4, 5, 6]] }
{"id": 2, "data": [[7, 8, 9], [10, 11, 12]] }
\.

=> COPY np FROM STDIN PARSER fjsonparser();
{"id": 1, "data": [[0, 0], [0, 0], [0, 0]] }
{"id": 2, "data": [[1, 1], [1, 1], [1, 1]] }
{"id": 3, "data": [[2, 0], [0, 2], [2, 0]] }
\.

=> SELECT mn.id, np.id, MatrixMultiply(mn.data, np.data) FROM mn CROSS JOIN np ORDER BY 1, 2;
id | id |   MatrixMultiply
---+----+-------------------
1  |  1 | [[0,0],[0,0]]
1  |  2 | [[6,6],[15,15]]
1  |  3 | [[8,4],[20,10]]
2  |  1 | [[0,0],[0,0]]
2  |  2 | [[24,24],[33,33]]
2  |  3 | [[32,16],[44,22]]
(6 rows)

设置

所有 Python UDx 都必须导入 Vertica SDK 库:

import vertica_sdk

工厂实施

getPrototype() 方法会声明函数实参和返回类型都必须为二维数组,以整数数组的数组来表示:


def getPrototype(self, srv_interface, arg_types, return_type):
    array1dtype = vertica_sdk.ColumnTypes.makeArrayType(vertica_sdk.ColumnTypes.makeInt())
    arg_types.addArrayType(array1dtype)
    arg_types.addArrayType(array1dtype)
    return_type.addArrayType(array1dtype)

getReturnType() 验证乘积矩阵的行数是否与第一个输入矩阵相同,以及列数是否与第二个输入矩阵相同:


def getReturnType(self, srv_interface, arg_types, return_type):
    (_, a1type) = arg_types[0]
    (_, a2type) = arg_types[1]
    m = a1type.getArrayBound()
    p = a2type.getElementType().getArrayBound()
    return_type.addArrayType(vertica_sdk.SizedColumnTypes.makeArrayType(vertica_sdk.SizedColumnTypes.makeInt(), p), m)

函数实施

使用名称分别为 arg_readerres_writerBlockReaderBlockWriter 来调用 processBlock() 方法。为了访问输入数组的元素,该方法会使用 ArrayReader 实例。数组是嵌套的,因此必须为外部和内部数组实例化 ArrayReader。列表推导式简化了将输入数组读取到列表中的过程。该方法会执行计算,然后使用 ArrayWriter 实例来构造乘积矩阵。


def processBlock(self, server_interface, arg_reader, res_writer):
    while True:
        lmat = [[cell.getInt(0) for cell in row.getArrayReader(0)] for row in arg_reader.getArrayReader(0)]
        rmat = [[cell.getInt(0) for cell in row.getArrayReader(0)] for row in arg_reader.getArrayReader(1)]
        omat = [[0 for c in range(len(rmat[0]))] for r in range(len(lmat))]

        for i in range(len(lmat)):
            for j in range(len(rmat[0])):
                for k in range(len(rmat)):
                    omat[i][j] += lmat[i][k] * rmat[k][j]

        res_writer.setArray(omat)
        res_writer.next()

        if not arg_reader.next():
            break

5.8.9 - R 示例: SalesTaxCalculator

SalesTaxCalculator 标量函数会从表中读取浮点数和可变长字符串,即商品的价格和州名缩写。然后,它会使用州名缩写从列表中查找销售税率并计算商品的价格(包括所在州的销售税),以返回商品的总成本。

您可以在 Vertica Github 存储库中找到更多 UDx 示例:https://github.com/vertica/UDx-Examples

加载函数和库

创建库和函数。

=> CREATE OR REPLACE LIBRARY rLib AS 'sales_tax_calculator.R' LANGUAGE 'R';
CREATE LIBRARY
=> CREATE OR REPLACE FUNCTION SalesTaxCalculator AS LANGUAGE 'R' NAME 'SalesTaxCalculatorFactory' LIBRARY rLib FENCED;
CREATE FUNCTION

使用函数查询数据

以下查询显示了如何使用 UDSF 运行查询。

=> SELECT item, state_abbreviation,
          price, SalesTaxCalculator(price, state_abbreviation) AS Price_With_Sales_Tax
    FROM inventory;
    item     | state_abbreviation | price | Price_With_Sales_Tax
-------------+--------------------+-------+---------------------
 Scarf       | AZ                 |  6.88 |             7.53016
 Software    | MA                 | 88.31 |           96.655295
 Soccer Ball | MS                 | 12.55 |           13.735975
 Beads       | LA                 |  0.99 |            1.083555
 Baseball    | TN                 | 42.42 |            46.42869
 Cheese      | WI                 | 20.77 |           22.732765
 Coffee Mug  | MA                 |  8.99 |            9.839555
 Shoes       | TN                 | 23.99 |           26.257055
(8 rows)

UDSF R 代码


SalesTaxCalculator <- function(input.data.frame) {
  # Not a complete list of states in the USA, but enough to get the idea.
  state.sales.tax <- list(ma = 0.0625,
                          az = 0.087,
                          la = 0.0891,
                          tn = 0.0945,
                          wi = 0.0543,
                          ms = 0.0707)
  for ( state_abbreviation in input.data.frame[, 2] ) {
    # Ensure state abbreviations are lowercase.
    lower_state <- tolower(state_abbreviation)
    # Check if the state is in our state.sales.tax list.
    if (is.null(state.sales.tax[[lower_state]])) {
      stop("State is not in our small sample!")
    } else {
      sales.tax.rate <- state.sales.tax[[lower_state]]
      item.price <- input.data.frame[, 1]
      # Calculate the price including sales tax.
      price.with.sales.tax <- (item.price) + (item.price * sales.tax.rate)
    }
  }
  return(price.with.sales.tax)
}

SalesTaxCalculatorFactory <- function() {
  list(name    = SalesTaxCalculator,
       udxtype = c("scalar"),
       intype  = c("float", "varchar"),
       outtype = c("float"))
}

5.8.10 - R 示例:kmeans

KMeans_User 标量函数会从表中读取任意数量的列,即观察值。然后,在将 kmeans 群集算法应用于数据时,它会使用观测值和两个参数,以返回与行的群集相关联的整数值。

您可以在 Vertica Github 存储库中找到更多 UDx 示例:https://github.com/vertica/UDx-Examples

加载函数和库

创建库和函数:

=> CREATE OR REPLACE LIBRARY rLib AS 'kmeans.R' LANGUAGE 'R';
CREATE LIBRARY
=> CREATE OR REPLACE FUNCTION KMeans_User AS LANGUAGE 'R' NAME 'KMeans_UserFactory' LIBRARY rLib FENCED;
CREATE FUNCTION

使用函数查询数据

以下查询显示了如何使用 UDSF 运行查询。

=> SELECT spec,
          KMeans_User(sl, sw, pl, pw USING PARAMETERS clusters = 3, nstart = 20)
    FROM iris;
      spec       | KMeans_User
-----------------+-------------
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
 Iris-setosa     |           2
.
.
.
(150 rows)

UDSF R 代码


KMeans_User <- function(input.data.frame, parameters.data.frame) {
  # Take the clusters and nstart parameters passed by the user and assign them
  # to variables in the function.
  if ( is.null(parameters.data.frame[['clusters']]) ) {
    stop("NULL value for clusters! clusters cannot be NULL.")
  } else {
    clusters.value <- parameters.data.frame[['clusters']]
  }
  if ( is.null(parameters.data.frame[['nstart']]) ) {
    stop("NULL value for nstart! nstart cannot be NULL.")
  } else {
    nstart.value <- parameters.data.frame[['nstart']]
  }
  # Apply the algorithm to the data.
  kmeans.clusters <- kmeans(input.data.frame[, 1:length(input.data.frame)],
                           clusters.value, nstart = nstart.value)
  final.output <- data.frame(kmeans.clusters$cluster)
  return(final.output)
}

KMeans_UserFactory <- function() {
  list(name    = KMeans_User,
       udxtype = c("scalar"),
       # Since this is a polymorphic function the intype must be any
       intype  = c("any"),
       outtype = c("int"),
       parametertypecallback=KMeansParameters)
}

KMeansParameters <- function() {
  parameters <- list(datatype = c("int", "int"),
                     length   = c("NA", "NA"),
                     scale    = c("NA", "NA"),
                     name     = c("clusters", "nstart"))
  return(parameters)
}

5.8.11 - C++ 示例:使用复杂类型

UDx 可以接受和返回复杂类型。ArraySlice 示例将一个数组和两个索引作为输入,返回一个仅包含该范围内的值的数组。由于数组元素可以是任何类型,因此函数是多态的。

完整的源代码位于 /opt/vertica/sdk/examples/ScalarFunctions/ArraySlice.cpp 中。

加载和使用示例

加载库并创建函数,如下所示:

=> CREATE OR REPLACE LIBRARY ScalarFunctions AS '/home/dbadmin/examplesUDSF.so';

=> CREATE FUNCTION ArraySlice AS
LANGUAGE 'C++' NAME 'ArraySliceFactory' LIBRARY ScalarFunctions;

创建一些数据并在其上调用函数,如下所示:

=> CREATE TABLE arrays (id INTEGER, aa ARRAY[INTEGER]);
COPY arrays FROM STDIN;
1|[]
2|[1,2,3]
3|[5,4,3,2,1]
\.

=> CREATE TABLE slices (b INTEGER, e INTEGER);
COPY slices FROM STDIN;
0|2
1|3
2|4
\.

=> SELECT id, b, e, ArraySlice(aa, b, e) AS slice FROM arrays, slices;
 id | b | e | slice
----+---+---+-------
  1 | 0 | 2 | []
  1 | 1 | 3 | []
  1 | 2 | 4 | []
  2 | 0 | 2 | [1,2]
  2 | 1 | 3 | [2,3]
  2 | 2 | 4 | [3]
  3 | 0 | 2 | [5,4]
  3 | 1 | 3 | [4,3]
  3 | 2 | 4 | [3,2]
(9 rows)

工厂实施

由于函数是多态的,getPrototype() 声明输入和输出可以是任何类型,类型强制必须在别处完成:

void getPrototype(ServerInterface &srvInterface,
                              ColumnTypes &argTypes,
                              ColumnTypes &returnType) override
{
    /*
     * This is a polymorphic function that accepts any array
     * and returns an array of the same type
     */
    argTypes.addAny();
    returnType.addAny();
}

工厂验证输入类型并确定 getReturnType() 中的返回类型:

void getReturnType(ServerInterface &srvInterface,
                   const SizedColumnTypes &argTypes,
                   SizedColumnTypes &returnType) override
{
    /*
     * Three arguments: (array, slicebegin, sliceend)
     * Validate manually since the prototype accepts any arguments.
     */
    if (argTypes.size() != 3) {
        vt_report_error(0, "Three arguments (array, slicebegin, sliceend) expected");
    } else if (!argTypes[0].getType().isArrayType()) {
        vt_report_error(1, "Argument 1 is not an array");
    } else if (!argTypes[1].getType().isInt()) {
        vt_report_error(2, "Argument 2 (slicebegin) is not an integer)");
    } else if (!argTypes[2].getType().isInt()) {
        vt_report_error(3, "Argument 3 (sliceend) is not an integer)");
    }

    /* return type is the same as the array arg type, copy it over */
    returnType.push_back(argTypes[0]);
}

函数实施

使用 BlockReaderBlockWriter 调用 processBlock() 方法。第一个实参是一个数组。为了访问数组的元素,该方法使用 ArrayReader。同样,它使用 ArrayWriter 来构造输出。

void processBlock(ServerInterface &srvInterface,
        BlockReader &argReader,
        BlockWriter &resWriter) override
{
    do {
        if (argReader.isNull(0) || argReader.isNull(1) || argReader.isNull(2)) {
            resWriter.setNull();
        } else {
            Array::ArrayReader argArray  = argReader.getArrayRef(0);
            const vint slicebegin = argReader.getIntRef(1);
            const vint sliceend   = argReader.getIntRef(2);

            Array::ArrayWriter outArray = resWriter.getArrayRef(0);
            if (slicebegin < sliceend) {
                for (int i = 0; i < slicebegin && argArray->hasData(); i++) {
                    argArray->next();
                }
                for (int i = slicebegin; i < sliceend && argArray->hasData(); i++) {
                    outArray->copyFromInput(*argArray);
                    outArray->next();
                    argArray->next();
                }
            }
            outArray.commit();  /* finalize the written array elements */
        }
        resWriter.next();
    } while (argReader.next());
}

5.8.12 - C++ 示例:返回多个值

编写 UDSF 时,可以指定多个返回值。如果您指定多个值,Vertica 会将它们打包到单个 ROW 作为返回值。您可以查询 ROW 中的字段或整个 ROW。

下面的示例实施一个名为 div(除法)的函数,它返回两个整数,即商和余数。

此示例显示了一种从 UDSF 返回 ROW 的方法。当输入和输出都是基元类型时,返回多个值并让 Vertica 构建 ROW 非常方便。您还可以直接使用复杂类型,如复杂类型作为实参中所述和 C++ 示例:使用复杂类型中所示。

加载和使用示例

加载库并创建函数,如下所示:

=> CREATE OR REPLACE LIBRARY ScalarFunctions AS '/home/dbadmin/examplesUDSF.so';

=> CREATE FUNCTION div AS
LANGUAGE 'C++' NAME 'DivFactory' LIBRARY ScalarFunctions;

创建一些数据并在其上调用函数,如下所示:

=> CREATE TABLE D (a INTEGER, b INTEGER);
COPY D FROM STDIN DELIMITER ',';
10,0
10,1
10,2
10,3
10,4
10,5
\.

=> SELECT a, b, Div(a, b), (Div(a, b)).quotient, (Div(a, b)).remainder FROM D;
 a  | b |                  Div               | quotient | remainder
----+---+------------------------------------+----------+-----------
 10 | 0 | {"quotient":null,"remainder":null} |          |
 10 | 1 | {"quotient":10,"remainder":0}      |       10 |         0
 10 | 2 | {"quotient":5,"remainder":0}       |        5 |         0
 10 | 3 | {"quotient":3,"remainder":1}       |        3 |         1
 10 | 4 | {"quotient":2,"remainder":2}       |        2 |         2
 10 | 5 | {"quotient":2,"remainder":0}       |        2 |         0
(6 rows)

工厂实施

工厂在 getPrototype()getReturnType() 中声明了两个返回值。工厂在其他方面没有作用。


    void getPrototype(ServerInterface &interface,
            ColumnTypes &argTypes,
            ColumnTypes &returnType) override
    {
        argTypes.addInt();
        argTypes.addInt();
        returnType.addInt(); /* quotient  */
        returnType.addInt(); /* remainder */
    }

    void getReturnType(ServerInterface &srvInterface,
            const SizedColumnTypes &argTypes,
            SizedColumnTypes &returnType) override
    {
        returnType.addInt("quotient");
        returnType.addInt("remainder");
    }

函数实施

该函数在 processBlock() 中写入两个输出值。此处值的数量必须与工厂声明相匹配。


class Div : public ScalarFunction {
    void processBlock(Vertica::ServerInterface &srvInterface,
            Vertica::BlockReader &argReader,
            Vertica::BlockWriter &resWriter) override
    {
        do {
            if (argReader.isNull(0) || argReader.isNull(1) || (argReader.getIntRef(1) == 0)) {
                resWriter.setNull(0);
                resWriter.setNull(1);
            } else {
                const vint dividend = argReader.getIntRef(0);
                const vint divisor  = argReader.getIntRef(1);
                resWriter.setInt(0, dividend / divisor);
                resWriter.setInt(1, dividend % divisor);
            }
            resWriter.next();
        } while (argReader.next());
    }
};

5.8.13 - C++ 示例:从检查约束调用 UDSF

此示例显示了创建可由检查约束调用的 UDSF 所需的 C++ 代码。示例函数的名称是 LargestSquareBelow。此示例函数确定其平方小于主题列中的数字的最大数字。例如,如果该列中的数字是 1000,则其平方 (961) 小于 1000 的最大数字是 31。

有关检查约束的信息,请参阅检查约束

加载和使用示例

以下示例显示了如何使用 CREATE LIBRARY 创建和加载名为 MySqLib 的库。将此示例中的库路径调整为绝对路径,并将文件名调整为共享对象 LargestSquareBelow 保存到的位置。

创建库:

=> CREATE OR REPLACE LIBRARY MySqLib AS '/home/dbadmin/LargestSquareBelow.so';
  1. 创建并加载库之后,使用 CREATE FUNCTION(标量) 语句将函数添加到编录中:
=> CREATE OR REPLACE FUNCTION largestSqBelow AS LANGUAGE 'C++' NAME 'LargestSquareBelowInfo' LIBRARY MySqLib;
  1. 下一步,将 UDSF 包含到检查约束中。
=> CREATE TABLE squaretest(
   ceiling INTEGER UNIQUE,
   CONSTRAINT chk_sq CHECK (largestSqBelow(ceiling) < ceiling*ceiling)
);
  1. 将数据添加到表 squaretest 中:
=> COPY squaretest FROM stdin DELIMITER ','NULL'null';
-1
null
0
1
1000
1000000
1000001
\.

根据所使用的数据,输出应类似于以下示例:

SELECT ceiling, largestSqBelow(ceiling)
FROM squaretest ORDER BY ceiling;

ceiling  | largestSqBelow
---------+----------------
         |
      -1 |
       0 |
       1 |              0
    1000 |             31
 1000000 |            999
 1000001 |           1000
(7 rows)

ScalarFunction 实施

ScalarFunction 实施为 UDSF 执行处理工作,即确定其平方小于数字输入的最大数字。


#include "Vertica.h"
/*
 * ScalarFunction implementation for a UDSF that
 * determines the largest number whose square is less than
 * the number input.
*/
class LargestSquareBelow : public Vertica::ScalarFunction
{
public:
 /*
  * This function does all of the actual processing for the UDSF.
  * The inputs are retrieved via arg_reader
  * The outputs are returned via arg_writer
  *
 */
    virtual void processBlock(Vertica::ServerInterface &srvInterface,
                              Vertica::BlockReader &arg_reader,
                              Vertica::BlockWriter &res_writer)
    {
        if (arg_reader.getNumCols() != 1)
            vt_report_error(0, "Function only accept 1 argument, but %zu provided", arg_reader.getNumCols());
// While we have input to process
        do {
            // Read the input parameter by calling the
            // BlockReader.getIntRef class function
            const Vertica::vint a = arg_reader.getIntRef(0);
            Vertica::vint res;
            //Determine the largest square below the number
            if ((a != Vertica::vint_null) && (a > 0))
            {
                res = (Vertica::vint)sqrt(a - 1);
            }
            else
                res = Vertica::vint_null;
            //Call BlockWriter.setInt to store the output value,
            //which is the largest square
            res_writer.setInt(res);
            //Write the row and advance to the next output row
            res_writer.next();
            //Continue looping until there are no more input rows
        } while (arg_reader.next());
    }
};

ScalarFunctionFactory 实施

ScalarFunctionFactory 实施执行对输入和输出的处理工作,并将函数标记为不可变(如果计划在检查约束中使用 UDSF,则必须满足此要求)。


class LargestSquareBelowInfo : public Vertica::ScalarFunctionFactory
{
    //return an instance of LargestSquareBelow to perform the computation.
    virtual Vertica::ScalarFunction *createScalarFunction(Vertica::ServerInterface &srvInterface)
    //Call the vt_createFuncObj to create the new LargestSquareBelow class instance.
    { return Vertica::vt_createFuncObject<LargestSquareBelow>(srvInterface.allocator); }

    /*
     * This function returns the description of the input and outputs of the
     * LargestSquareBelow class's processBlock function.  It stores this information in
     * two ColumnTypes objects, one for the input parameter, and one for
     * the return value.
    */
    virtual void getPrototype(Vertica::ServerInterface &srvInterface,
                              Vertica::ColumnTypes &argTypes,
                              Vertica::ColumnTypes &returnType)
    {
        // Takes one int as input, so adds int to the argTypes object
        argTypes.addInt();
        // Returns a single int, so add a single int to the returnType object.
        // ScalarFunctions always return a single value.
        returnType.addInt();
    }
public:
    // the function cannot be called within a check constraint unless the UDx author
    // certifies that the function is immutable:
    LargestSquareBelowInfo() { vol = Vertica::IMMUTABLE; }
};

RegisterFactory 宏

使用 RegisterFactory 宏注册一个 ScalarFunctionFactory 子类。该宏将对工厂类进行实例化并将其包含的元数据供 Vertica 访问。要调用该宏,请将您的工厂类的名称传递给它。


RegisterFactory(LargestSquareBelowInfo);

5.9 - 转换函数 (UDTF)

用户定义的转换函数 (UDTF) 可用于将数据表转换成其他表。该函数读取一个或多个参数(视为数据行),并返回包含一列或多列的零行或多行数据。UDTF 可以生成任意数量的行作为输出。但是,输出的每个行必须是完整的。在未向每个列添加值的情况下前进到下一行会生成不正确的结果。

输出表的架构并不需要与输入表的架构相对应,它们可以完全不同。UDTF 可为每行输入返回任意行输出。

UDTF 只能用在仅包含 UDTF 调用和所需 OVER 子句的 SELECT 列表中。多阶段 UDTF 可以使用分区列 (PARTITION BY),但其他 UDTF 不能。

在语句中与 GROUP BY 和 ORDER BY 结合使用时,UDTF 在 GROUP BY 之后运行,但在最后一个 ORDER BY 之前运行。ORDER BY 子句可能仅包含窗口分区子句中的列或表达式(请参阅窗口分区)。

UDTF 最多可读取 9800 个参数(输入列)。尝试向 UDTF 传递更多参数会返回错误。

5.9.1 - TransformFunction 类

您可以在 TransformFunction 类中执行数据处理,以及将输入行转换为输出行。子类必须定义 processPartition() 方法。该类可定义多种方法以设置和分解该函数。

执行转换

processPartition() 方法可执行您希望 UDTF 执行的所有处理。当用户在 SQL 语句中调用您的函数时,Vertica 会将来自函数参数的数据绑定在一起并将其传递至 processPartition()

processPartition() 方法的输入和输出由 PartitionReaderPartitionWriter 类的对象提供。这些类定义了用于为 UDTF 读取输入数据和写入输出数据的方法。

UDTF 不一定像 UDSF 那样对单行进行操作。UDTF 可以随时读取任意数量的行并写入输出。

在实施 processPartition() 时,请遵循以下准则:

  • PartitionReader 对象中调用特定于数据类型的函数以提取每个输入参数。这些函数全部使用一个参数:要读取的输入行的列号。函数可能需要处理 NULL 值。

  • 写入输出时,UDTF 必须提供在工厂中定义的所有输出列的值。与读取输入列类似,PartitionWriter 对象具备将每种类型的数据写入输出行的函数。

  • 请使用 PartitionReader.next() 确定是否还有更多输入需要处理,并在输入处理完时退出。

  • 在某些情况下,您可能需要使用 PartitionReadergetNumCols()getTypeMetaData() 函数来确定参数的数量和类型,而非仅对输入行中列的数据类型进行硬编码。如果希望 TransformFunction 能够处理具有不同架构的输入表,则此方法很有用。然后,您可以使用不同的 TransformFunctionFactory 类定义调用同一个 TransformFunction 类的多个函数签名。有关详细信息,请参阅重载 UDx

设置和分解

TransformFunction 类定义了两种其他方法,您可以选择性地实施这两种方法以分配和释放资源: setup()destroy()。您应使用这些方法来分配和取消分配那些不通过 UDx API 分配的资源(有关详细信息,请参阅为 UDx 分配资源)。

API

TransformFunction API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes);

virtual void processPartition(ServerInterface &srvInterface,
        PartitionReader &input_reader, PartitionWriter &output_writer)=0;

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface,
        const SizedColumnTypes &argTypes);

PartitionReaderPartitionWriter 类会为列值提供 getter 和 setter,以及用于遍历分区的 next()。有关详细信息,请参阅 API 参考文档。

TransformFunction API 提供了以下通过子类扩展的方法:

public void setup(ServerInterface srvInterface, SizedColumnTypes argTypes);

public abstract void processPartition(ServerInterface srvInterface,
        PartitionReader input_reader, PartitionWriter input_writer)
    throws UdfException, DestroyInvocation;

protected void cancel(ServerInterface srvInterface);

public void destroy(ServerInterface srvInterface, SizedColumnTypes argTypes);

PartitionReaderPartitionWriter 类会为列值提供 getter 和 setter,以及用于遍历分区的 next()。有关详细信息,请参阅 API 参考文档。

TransformFunction API 提供了以下通过子类扩展的方法:


def setup(self, server_interface, col_types)

def processPartition(self, server_interface, partition_reader, partition_writer)

def destroy(self, server_interface, col_types)

PartitionReaderPartitionWriter 类会为列值提供 getter 和 setter,以及用于遍历分区的 next()。有关详细信息,请参阅 API 参考文档。

实施 主函数 API 以定义转换函数:

FunctionName <- function(input.data.frame, parameters.data.frame) {
  # Computations

  # The function must return a data frame.
  return(output.data.frame)
}

5.9.2 - TransformFunctionFactory 类

TransformFunctionFactory 类将向 Vertica 提供有关 UDTF 的元数据:其参数的数量和数据类型及其返回值的数据类型。该类还会实例化 TransformFunction 的子类。

您必须在 TransformFunctionFactory 中实施以下方法:

  • getPrototype() 将返回两个 ColumnTypes 对象,这两个对象描述 UDTF 将用作输入的列以及作为输出返回的列。

  • getReturnType() 将向 Vertica 提供有关输出值的详细信息:可变大小数据类型(例如 VARCHAR)的宽度和具有可设置精度的数据类型(例如 TIMESTAMP)的精度。您还可以使用此函数设置输出列的名称。虽然此方法对于返回单个值的 UDx 是可选的,但您必须为 UDTF 实施该方法。

  • createTransformFunction() 实例化 TransformFunction 子类。

对于使用 C++ 语言编写的转换函数,您可以提供有助于优化查询的信息。请参阅提高查询性能(仅限 C++)

API

TransformFunctionFactory API 提供了以下通过子类扩展的方法:

virtual TransformFunction *
    createTransformFunction (ServerInterface &srvInterface)=0;

virtual void getPrototype(ServerInterface &srvInterface,
            ColumnTypes &argTypes, ColumnTypes &returnType)=0;

virtual void getReturnType(ServerInterface &srvInterface,
            const SizedColumnTypes &argTypes,
            SizedColumnTypes &returnType)=0;

virtual void getParameterType(ServerInterface &srvInterface,
            SizedColumnTypes &parameterTypes);

TransformFunctionFactory API 提供了以下通过子类扩展的方法:

public abstract TransformFunction createTransformFunction(ServerInterface srvInterface);

public abstract void getPrototype(ServerInterface srvInterface, ColumnTypes argTypes, ColumnTypes returnType);

public abstract void getReturnType(ServerInterface srvInterface, SizedColumnTypes argTypes,
        SizedColumnTypes returnType) throws UdfException;

public void getParameterType(ServerInterface srvInterface, SizedColumnTypes parameterTypes);

TransformFunctionFactory API 提供了以下通过子类扩展的方法:


def createTransformFunction(self, srv)

def getPrototype(self, srv_interface, arg_types, return_type)

def getReturnType(self, srv_interface, arg_types, return_type)

def getParameterType(self, server_interface, parameterTypes)

实施 工厂函数 API 以定义转换函数工厂:

FunctionNameFactory <- function() {
  list(name    = FunctionName,
       udxtype = c("scalar"),
       intype  = c("int"),
       outtype = c("int"))
}

5.9.3 - MultiPhaseTransformFunctionFactory 类

使用多阶段 UDTF,您可以将数据处理分为多个步骤。使用此功能后,您的 UDTF 可以使用类似于 Hadoop 或其他 MapReduce 框架的方式执行处理。您可以使用第一阶段来分解并收集数据,然后使用后续阶段来处理数据。例如,您的 UDTF 的第一阶段可以是从存储在表列的 Web 服务器日志中提取特定类型的用户交互,而且后续阶段可以是对这些交互执行分析。

多阶段 UDTF 还能让您决定在什么地方进行处理:在每个节点本地,还是在整个群集内。如果您的多阶段 UDTF 类似于 MapReduce 流程,您可能希望在多阶段 UDTF 的第一阶段处理存储在本地(运行 UDTF 实例的节点)的数据。这样可防止在 Vertica 群集中复制较大的数据段。根据在后续阶段执行的处理类型,您可以选择对数据进行分段并分布在 Vertica 群集中。

UDTF 的每个阶段都与传统(单阶段)UDTF 相同:它可以接收作为输入的表,然后生成作为输出的表。每个阶段的输出的架构不需要与其输入匹配,而且每个阶段可以根据需要输出大量或少量行。

要定义由每个阶段执行的处理,请创建 TransformFunction 的子类。如果已从单阶段 UDTF(用于执行您希望多阶段 UDTF 的其中一个阶段执行的处理)创建了 TransformFunction,则您可以轻松调整该子类以使其在多阶段 UDTF 中工作。

多阶段 UDTF 和传统 UDTF 不同之处是所使用的工厂类。应使用 MultiPhaseTransformFunctionFactory 的子类定义多阶段 UDTF,而非 TransformFunctionFactory。此特殊工厂类用作多步骤 UDTF 中所有阶段的容器。此工厂类通过 getPrototype() 函数向 Vertica 提供整个多阶段 UDTF 的输入要求和输出要求,以及该 UDTF 中所有阶段的列表。

MultiPhaseTransformFunctionFactory 类的子类中,可以定义 TransformFunctionPhase 的一个或多个子类。对于多阶段 UDTF 中的每个阶段,这些类充当与 TransformFunctionFactory 类相同的角色。这些类定义每个阶段的输入和输出并创建其关联的 TransformFunction 类的实例,以对 UDTF 的每个阶段执行处理。除了这些子类之外,MultiPhaseTransformFunctionFactory 还包含可用于控制每个 TransformFunctionPhase 子类的实例的字段。

API

MultiPhaseTransformFunctionFactory 类扩展了 TransformFunctionFactory API 提供了以下其他方法以通过子类扩展:

virtual void getPhases(ServerInterface &srvInterface,
        std::vector< TransformFunctionPhase * > &phases)=0;

如果使用该工厂,则还必须扩展 TransformFunctionPhase。请参阅 SDK 参考文档。

MultiPhaseTransformFunctionFactory 类扩展了 TransformFunctionFactory。API 提供了以下通过子类扩展的方法:

public abstract void getPhases(ServerInterface srvInterface,
        Vector< TransformFunctionPhase > phases);

如果使用该工厂,则还必须扩展 TransformFunctionPhase。请参阅 SDK 参考文档。

TransformFunctionFactory 类扩展了 TransformFunctionFactory。对于每个阶段,工厂必须定义可扩展 TransformFunctionPhase 的类。

工厂将添加以下方法:


def getPhase(cls, srv)

TransformFunctionPhase 包含以下方法:


def createTransformFunction(cls, srv)

def getReturnType(self, srv_interface, input_types, output_types)

5.9.4 - 提高查询性能(仅限 C++)

评估查询时,Vertica 优化器可能会对其输入进行排序以提高性能。如果函数已返回排序后的数据,这意味着优化器正在执行额外的工作。使用 C++ 语言编写的转换函数可以声明其返回的数据的排序方式,这样优化器就可以利用这些信息。

转换函数会在函数的 processPartition() 方法中进行实际排序。要利用这种优化,排序必须为升序。您不需要对所有列进行排序,但必须先返回已排序的列。

您可以在工厂的 getReturnType() 方法中声明函数对其输出进行排序的方式。

PolyTopKPerPartition 示例会对输入列进行排序并返回给定的行数:

=> SELECT polykSort(14, a, b, c) OVER (ORDER BY a, b, c)
    AS (sort1,sort2,sort3) FROM observations ORDER BY 1,2,3;
 sort1 | sort2 | sort3
-------+-------+-------
     1 |     1 |     1
     1 |     1 |     2
     1 |     1 |     3
     1 |     2 |     1
     1 |     2 |     2
     1 |     3 |     1
     1 |     3 |     2
     1 |     3 |     3
     1 |     3 |     4
     2 |     1 |     1
     2 |     1 |     2
     2 |     2 |     3
     2 |     2 |    34
     2 |     3 |     5
(14 rows)

工厂会通过在每一列上设置 isSortedBy 属性在 getReturnType() 中声明此排序。每个 SizedColumnType 都存在关联的 Properties 对象,您可以在其中设置此值:


virtual void getReturnType(ServerInterface &srvInterface, const SizedColumnTypes &inputTypes, SizedColumnTypes &outputTypes)
{
    vector<size_t> argCols; // Argument column indexes.
    inputTypes.getArgumentColumns(argCols);
    size_t colIdx = 0;

    for (vector<size_t>::iterator i = argCols.begin() + 1; i < argCols.end(); i++)
    {
        SizedColumnTypes::Properties props;
        props.isSortedBy = true;
        std::stringstream cname;
        cname << "col" << colIdx++;
        outputTypes.addArg(inputTypes.getColumnType(*i), cname.str(), props);
    }
}

通过查看包含和不包含此设置的查询的 EXPLAIN 计划,可以查看此优化的效果。以下输出显示了 polyk(未排序的版本)的查询计划。记录排序成本:

=> EXPLAN SELECT polyk(14, a, b, c) OVER (ORDER BY a, b, c)
    FROM observations ORDER BY 1,2,3;

 Access Path:
 +-SORT [Cost: 2K, Rows: 10K] (PATH ID: 1)
 |  Order: col0 ASC, col1 ASC, col2 ASC
 | +---> ANALYTICAL [Cost: 2K, Rows: 10K] (PATH ID: 2)
 | |      Analytic Group
 | |       Functions: polyk()
 | |       Group Sort: observations.a ASC NULLS LAST, observations.b ASC NULLS LAST, observations.c ASC NULLS LAST
 | | +---> STORAGE ACCESS for observations [Cost: 2K, Rows: 10K]
 (PATH ID: 3)
 | | |      Projection: public.observations_super
 | | |      Materialize: observations.a, observations.b, observations.c

已排序版本的查询计划会忽略此步骤(从而节省成本)并从分析步骤开始(上一个计划中的第二个步骤):

=> EXPLAN SELECT polykSort(14, a, b, c) OVER (ORDER BY a, b, c)
    FROM observations ORDER BY 1,2,3;

Access Path:
 +-ANALYTICAL [Cost: 2K, Rows: 10K] (PATH ID: 2)
 |  Analytic Group
 |   Functions: polykSort()
 |   Group Sort: observations.a ASC NULLS LAST, observations.b ASC NULLS LAST, observations.c ASC NULLS LAST
 | +---> STORAGE ACCESS for observations [Cost: 2K, Rows: 10K] (PATH ID: 3)
 | |      Projection: public.observations_super
 | |      Materialize: observations.a, observations.b, observations.c

5.9.5 - 用于处理本地数据的分区选项

UDTF 通常以特定方式处理已分区的数据。例如,处理 Web 服务器日志文件以计算每个合作伙伴网站引用的点击数的 UDTF 需要具有按引用网站列分区的输入。UDTF 的每个实例将查看特定合作伙伴站点引用的点击,以便计算其数量。

在与此类似的情况下,窗口分区子句应使用 PARTITION BY 子句。群集中的每个节点将对其存储的数据进行分区并将部分分区发送给其他节点,然后合并从其他节点收到的分区并运行 UDTF 的一个实例以处理这些分区。

在其他情况下,UDTF 可能不需要以特殊方式对输入数据进行分区,例如,从 Apache 日志文件解析数据的 UDTF。在这种情况下,您可以指定每个 UDTF 实例仅处理由节点(该实例正在此节点上运行)存储在本地的数据。通过消除对数据进行分区的开销,可以大大提高处理效率。

可以使用以下窗口分区选项之一来指示 UDTF 仅处理本地数据:

  • PARTITION BEST:仅对于线程安全的 UDTF,可通过多个节点间的多线程查询来优化性能。

  • PARTITION NODES:优化多个节点间的单线程查询的性能。

查询必须指定将在所有节点之间复制且包含一行的源表(类似于 DUAL 表)。例如,以下语句将调用可解析存储在本地的 Apache 日志文件的 UDTF:

=> CREATE TABLE rep (dummy INTEGER) UNSEGMENTED ALL NODES;
CREATE TABLE
=> INSERT INTO rep VALUES (1);
 OUTPUT
--------
      1
(1 row)
=> SELECT ParseLogFile('/data/apache/log*') OVER (PARTITION BEST) FROM rep;

5.9.6 - C++ 示例:字符串分词器

以下示例显示了 TransformFunction 的子类,它名为 StringTokenizer。此子类定义的 UDTF 可读取包含 INTEGER ID 列和 VARCHAR 列的表。此子类可将 VARCHAR 列中的文本拆分为标记(各个词)。此子类将返回一个表,表中包含每个标记、标记所出现在的行以及标记在字符串中的位置。

加载和使用示例

以下示例显示了如何将函数加载至 Vertica 中。假设包含该函数的 TransformFunctions.so 库已复制到启动程序节点上数据库管理员用户的主目录中。

=> CREATE LIBRARY TransformFunctions AS
   '/home/dbadmin/TransformFunctions.so';
CREATE LIBRARY
=> CREATE TRANSFORM FUNCTION tokenize
   AS LANGUAGE 'C++' NAME 'TokenFactory' LIBRARY TransformFunctions;
CREATE TRANSFORM FUNCTION

然后,您可以通过 SQL 语句使用该函数,例如:


=> CREATE TABLE T (url varchar(30), description varchar(2000));
CREATE TABLE
=> INSERT INTO T VALUES ('www.amazon.com','Online retail merchant and provider of cloud services');
 OUTPUT
--------
      1
(1 row)
=> INSERT INTO T VALUES ('www.vertica.com','World''s fastest analytic database');
 OUTPUT
--------
      1
(1 row)
=> COMMIT;
COMMIT

=> -- Invoke the UDTF
=> SELECT url, tokenize(description) OVER (partition by url) FROM T;
       url       |   words
-----------------+-----------
 www.amazon.com  | Online
 www.amazon.com  | retail
 www.amazon.com  | merchant
 www.amazon.com  | and
 www.amazon.com  | provider
 www.amazon.com  | of
 www.amazon.com  | cloud
 www.amazon.com  | services
 www.vertica.com | World's
 www.vertica.com | fastest
 www.vertica.com | analytic
 www.vertica.com | database
(12 rows)

请注意,结果表中的行数和列数与输入表中不同。这是 UDTF 的优势之一。

TransformFunction 实施

以下代码显示了 StringTokenizer 类。

class StringTokenizer : public TransformFunction
{
  virtual void processPartition(ServerInterface &srvInterface,
                                PartitionReader &inputReader,
                                PartitionWriter &outputWriter)
  {
    try {
      if (inputReader.getNumCols() != 1)
        vt_report_error(0, "Function only accepts 1 argument, but %zu provided", inputReader.getNumCols());

      do {
        const VString &sentence = inputReader.getStringRef(0);

        // If input string is NULL, then output is NULL as well
        if (sentence.isNull())
          {
            VString &word = outputWriter.getStringRef(0);
            word.setNull();
            outputWriter.next();
          }
        else
          {
            // Otherwise, let's tokenize the string and output the words
            std::string tmp = sentence.str();
            std::istringstream ss(tmp);

            do
              {
                std::string buffer;
                ss >> buffer;

                // Copy to output
                if (!buffer.empty()) {
                  VString &word = outputWriter.getStringRef(0);
                  word.copy(buffer);
                  outputWriter.next();
                }
              } while (ss);
          }
      } while (inputReader.next() && !isCanceled());
    } catch(std::exception& e) {
      // Standard exception. Quit.
      vt_report_error(0, "Exception while processing partition: [%s]", e.what());
    }
  }
};

此示例中的 processPartition() 函数将遵循您在自己的 UDTF 中遵循的相同模式:遍历 Vertica 向其发送的表分区中的所有行,以处理每个行并在前进之前检查是否已取消查询。对于 UDTF,您实际上不必处理每个行。即使退出函数而不读取所有输入,也不会出现任何问题。如果 UDTF 在执行某种搜索或其他某项操作后确定其余资源是不需要的,您可以选择执行此操作。

在此示例中,processPartition() 会先从 PartitionReader 对象中提取包含文本的 VStringVString 类代表 Vertica 字符串值(VARCHAR 或 CHAR)。如果存在输入,它会对其进行字符串标记并使用 PartitionWriter 对象将其添加到输出中。

与读取输入列类似,PartitionWriter 类具有将每种类型的数据写入输出行的函数。在这种情况下,该示例会调用 PartitionWriter 对象的 getStringRef() 函数来分配新的 VString 对象以保存第一列的输出标记,然后将标记的值复制到 VString 中。

TransformFunctionFactory 实施

以下代码显示了该工厂类。

class TokenFactory : public TransformFunctionFactory
{
  // Tell Vertica that we take in a row with 1 string, and return a row with 1 string
  virtual void getPrototype(ServerInterface &srvInterface, ColumnTypes &argTypes, ColumnTypes &returnType)
  {
    argTypes.addVarchar();
    returnType.addVarchar();
  }

  // Tell Vertica what our return string length will be, given the input
  // string length
  virtual void getReturnType(ServerInterface &srvInterface,
                             const SizedColumnTypes &inputTypes,
                             SizedColumnTypes &outputTypes)
  {
    // Error out if we're called with anything but 1 argument
    if (inputTypes.getColumnCount() != 1)
      vt_report_error(0, "Function only accepts 1 argument, but %zu provided", inputTypes.getColumnCount());

    int input_len = inputTypes.getColumnType(0).getStringLength();

    // Our output size will never be more than the input size
    outputTypes.addVarchar(input_len, "words");
  }

  virtual TransformFunction *createTransformFunction(ServerInterface &srvInterface)
  { return vt_createFuncObject<StringTokenizer>(srvInterface.allocator); }

};

在此示例中:

  • UDTF 会将 VARCHAR 列作为输入。为了定义输入列,getPrototype() 会在表示输入表的 ColumnTypes 对象上调用 addVarchar()

  • UDTF 将返回 VARCHAR 作为输出。getPrototype() 函数会调用 addVarchar() 以定义输出表。

此示例必须返回 VARCHAR 输出列的最大长度。它会将长度设置为输入字符串的长度。这是一个安全值,因为输出长度永远不会超过输入字符串。此示例还会将 VARCHAR 输出列的名称设置为“words”。

此示例中的 createTransformFunction() 函数实施是样板代码。该实施仅使用与此工厂类关联的 TransformFunction 类的名称来调用 vt_returnFuncObj 宏。此宏负责将 TransformFunction 类的副本实例化,Vertica 可以使用该副本来处理数据。

RegisterFactory 宏

创建 UDTF 的最后一步是调用 RegisterFactory 宏。此宏可确保在 Vertica 加载包含 UDTF 的共享库时,工厂类已实例化。只有将工厂类初始化,Vertica 才能找到 UDTF 并确定其输入和输出,除此之外没有任何其他方法。

RegisterFactory 宏仅使用工厂类的名称:

RegisterFactory(TokenFactory);

5.9.7 - Python 示例:字符串分词器

以下示例显示将输入字符串分解为标记(基于空格)的转换函数。它类似于 C++ 和 Java 的分词器示例。

加载和使用示例

创建库和函数:

=> CREATE LIBRARY pyudtf AS '/home/dbadmin/udx/tokenize.py' LANGUAGE 'Python';
CREATE LIBRARY
=> CREATE TRANSFORM FUNCTION tokenize AS NAME 'StringTokenizerFactory' LIBRARY pyudtf;
CREATE TRANSFORM FUNCTION

然后,您可以在 SQL 语句中使用该函数,例如:

=> CREATE TABLE words (w VARCHAR);
CREATE TABLE
=> COPY words FROM STDIN;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> this is a test of the python udtf
>> \.

=> SELECT tokenize(w) OVER () FROM words;
  token
----------
 this
 is
 a
 test
 of
 the
 python
 udtf
(8 rows)

设置

所有 Python UDx 都必须导入 Vertica SDK。

import vertica_sdk

UDTF Python 代码

以下代码定义了分词器及其工厂。

class StringTokenizer(vertica_sdk.TransformFunction):
    """
    Transform function which tokenizes its inputs.
    For each input string, each of the whitespace-separated tokens of that
    string is produced as output.
    """
    def processPartition(self, server_interface, input, output):
        while True:
            for token in input.getString(0).split():
                output.setString(0, token)
                output.next()
            if not input.next():
                break


class StringTokenizerFactory(vertica_sdk.TransformFunctionFactory):
    def getPrototype(self, server_interface, arg_types, return_type):
        arg_types.addVarchar()
        return_type.addVarchar()
    def getReturnType(self, server_interface, arg_types, return_type):
        return_type.addColumn(arg_types.getColumnType(0), "tokens")
    def createTransformFunction(cls, server_interface):
        return StringTokenizer()

5.9.8 - R 示例:日志分词器

LogTokenizer 转换函数会从表中读取可变长字符串,即日志消息。然后,它会标记每条日志消息的字符串,以返回每个标记。

您可以在 Vertica Github 存储库中找到更多 UDx 示例:https://github.com/vertica/UDx-Examples

加载函数和库

创建库和函数。

=> CREATE OR REPLACE LIBRARY rLib AS 'log_tokenizer.R' LANGUAGE 'R';
CREATE LIBRARY
=> CREATE OR REPLACE TRANSFORM FUNCTION LogTokenizer AS LANGUAGE 'R' NAME 'LogTokenizerFactory' LIBRARY rLib FENCED;
CREATE FUNCTION

使用函数查询数据

以下查询显示了如何使用 UDTF 运行查询。

=> SELECT machine,
          LogTokenizer(error_log USING PARAMETERS spliton = ' ') OVER(PARTITION BY machine)
     FROM error_logs;
 machine |  Token
---------+---------
 node001 | ERROR
 node001 | 345
 node001 | -
 node001 | Broken
 node001 | pipe
 node001 | WARN
 node001 | -
 node001 | Nearly
 node001 | filled
 node001 | disk
 node002 | ERROR
 node002 | 111
 node002 | -
 node002 | Flooded
 node002 | roads
 node003 | ERROR
 node003 | 222
 node003 | -
 node003 | Plain
 node003 | old
 node003 | broken
(21 rows)

UDTF R 代码


LogTokenizer <- function(input.data.frame, parameters.data.frame) {
  # Take the spliton parameter passed by the user and assign it to a variable
  # in the function so we can use that as our tokenizer.
  if ( is.null(parameters.data.frame[['spliton']]) ) {
    stop("NULL value for spliton! Token cannot be NULL.")
  } else {
    split.on <- as.character(parameters.data.frame[['spliton']])
  }
  # Tokenize the string.
  tokens <- vector(length=0)
  for ( string in input.data.frame[, 1] ) {
    tokenized.string <- strsplit(string, split.on)
    for ( token in tokenized.string ) {
      tokens <- append(tokens, token)
    }
  }
  final.output <- data.frame(tokens)
  return(final.output)
}

LogTokenizerFactory <- function() {
  list(name    = LogTokenizer,
       udxtype = c("transform"),
       intype  = c("varchar"),
       outtype = c("varchar"),
       outtypecallback=LogTokenizerReturn,
       parametertypecallback=LogTokenizerParameters)
}


LogTokenizerParameters <- function() {
  parameters <- list(datatype = c("varchar"),
                     length   = c("NA"),
                     scale    = c("NA"),
                     name     = c("spliton"))
  return(parameters)
}

LogTokenizerReturn <- function(arg.data.frame, parm.data.frame) {
  output.return.type <- data.frame(datatype = rep(NA,1),
                                   length   = rep(NA,1),
                                   scale    = rep(NA,1),
                                   name     = rep(NA,1))
  output.return.type$datatype <- c("varchar")
  output.return.type$name <- c("Token")
  return(output.return.type)
}

5.9.9 - C++ 示例:多阶段索引器

以下代码片段来自随 Vertica SDK 一起分发的 InvertedIndex UDTF 示例。此代码片段演示了将 MultiPhaseTransformFunctionFactory 子类化以及添加两个 TransformFunctionPhase 子类(定义此 UDTF 中的两个阶段)。

class InvertedIndexFactory : public MultiPhaseTransformFunctionFactory
{
public:
   /**
    * Extracts terms from documents.
    */
   class ForwardIndexPhase : public TransformFunctionPhase
   {
       virtual void getReturnType(ServerInterface &srvInterface,
                                  const SizedColumnTypes &inputTypes,
                                  SizedColumnTypes &outputTypes)
       {
           // Sanity checks on input we've been given.
           // Expected input: (doc_id INTEGER, text VARCHAR)
           vector<size_t> argCols;
           inputTypes.getArgumentColumns(argCols);
           if (argCols.size() < 2 ||
               !inputTypes.getColumnType(argCols.at(0)).isInt() ||
               !inputTypes.getColumnType(argCols.at(1)).isVarchar())
               vt_report_error(0, "Function only accepts two arguments"
                                "(INTEGER, VARCHAR))");
           // Output of this phase is:
           //   (term_freq INTEGER) OVER(PBY term VARCHAR OBY doc_id INTEGER)
           // Number of times term appears within a document.
           outputTypes.addInt("term_freq");
           // Add analytic clause columns: (PARTITION BY term ORDER BY doc_id).
           // The length of any term is at most the size of the entire document.
           outputTypes.addVarcharPartitionColumn(
                inputTypes.getColumnType(argCols.at(1)).getStringLength(),
                "term");
           // Add order column on the basis of the document id's data type.
           outputTypes.addOrderColumn(inputTypes.getColumnType(argCols.at(0)),
                                      "doc_id");
       }
       virtual TransformFunction *createTransformFunction(ServerInterface
                &srvInterface)
       { return vt_createFuncObj(srvInterface.allocator, ForwardIndexBuilder); }
   };
   /**
    * Constructs terms' posting lists.
    */
   class InvertedIndexPhase : public TransformFunctionPhase
   {
       virtual void getReturnType(ServerInterface &srvInterface,
                                  const SizedColumnTypes &inputTypes,
                                  SizedColumnTypes &outputTypes)
       {
           // Sanity checks on input we've been given.
           // Expected input:
           //   (term_freq INTEGER) OVER(PBY term VARCHAR OBY doc_id INTEGER)
           vector<size_t> argCols;
           inputTypes.getArgumentColumns(argCols);
           vector<size_t> pByCols;
           inputTypes.getPartitionByColumns(pByCols);
           vector<size_t> oByCols;
           inputTypes.getOrderByColumns(oByCols);
           if (argCols.size() != 1 || pByCols.size() != 1 || oByCols.size() != 1 ||
               !inputTypes.getColumnType(argCols.at(0)).isInt() ||
               !inputTypes.getColumnType(pByCols.at(0)).isVarchar() ||
               !inputTypes.getColumnType(oByCols.at(0)).isInt())
               vt_report_error(0, "Function expects an argument (INTEGER) with "
                               "analytic clause OVER(PBY VARCHAR OBY INTEGER)");
           // Output of this phase is:
           //   (term VARCHAR, doc_id INTEGER, term_freq INTEGER, corp_freq INTEGER).
           outputTypes.addVarchar(inputTypes.getColumnType(
                                    pByCols.at(0)).getStringLength(),"term");
           outputTypes.addInt("doc_id");
           // Number of times term appears within the document.
           outputTypes.addInt("term_freq");
           // Number of documents where the term appears in.
           outputTypes.addInt("corp_freq");
       }

       virtual TransformFunction *createTransformFunction(ServerInterface
                &srvInterface)
       { return vt_createFuncObj(srvInterface.allocator, InvertedIndexBuilder); }
   };
   ForwardIndexPhase fwardIdxPh;
   InvertedIndexPhase invIdxPh;
   virtual void getPhases(ServerInterface &srvInterface,
        std::vector<TransformFunctionPhase *> &phases)
   {
       fwardIdxPh.setPrepass(); // Process documents wherever they're originally stored.
       phases.push_back(&fwardIdxPh);
       phases.push_back(&invIdxPh);
   }
   virtual void getPrototype(ServerInterface &srvInterface,
                             ColumnTypes &argTypes,
                             ColumnTypes &returnType)
   {
       // Expected input: (doc_id INTEGER, text VARCHAR).
       argTypes.addInt();
       argTypes.addVarchar();
       // Output is: (term VARCHAR, doc_id INTEGER, term_freq INTEGER, corp_freq INTEGER)
       returnType.addVarchar();
       returnType.addInt();
       returnType.addInt();
       returnType.addInt();
   }
};
RegisterFactory(InvertedIndexFactory);

此示例中的大部分代码与 TransformFunctionFactory 类中的代码相似:

  • 这两个 TransformFunctionPhase 子类都实施 getReturnType() 函数,此函数描述每个阶段的输出。这与 TransformFunctionFactory 类中的 getReturnType() 函数相似。但是,此函数还可让您控制如何在多阶段 UDTF 的每个节点之间对数据进行分区和排序。

    第一个阶段调用 SizedColumnTypes::addVarcharPartitionColumn()(而非仅调用 addVarcharColumn()),以将此阶段的输出表设置为按包含提取的词的列进行分区。此阶段还调用 SizedColumnTypes::addOrderColumn(),以便按文档 ID 列对输出表进行排序。此阶段调用该函数而非特定于数据类型的函数(例如 addIntOrderColumn())之一,以便可以将原始列的数据类型传递到输出列。

  • MultiPhaseTransformFunctionFactory 类实施 getPrototype() 函数,此函数定义多阶段 UDTF 的输入和输出的架构。此函数与 TransformFunctionFactory::getPrototype() 函数相同。

MultiPhaseTransformFunctionFactory 类实施的唯一函数是 getPhases()。此函数可定义各个阶段的执行顺序。表示阶段的字段将以预期执行顺序推入该矢量。

您还可以在 MultiPhaseTransformFunctionFactory.getPhases() 函数中将 UDTF 的第一个阶段标记为对存储在节点本地的数据(而非分区到所有节点的数据)执行操作(称为“预通过”阶段)。使用此选项,您不必在 Vertica 群集中四处移动大量数据,从而提高多阶段 UDTF 的效率。

要将第一个阶段标记为预通过,您可以从 getPhase() 函数中调用第一个阶段的 TransformFunctionPhase 实例的 TransformFunctionPhase::setPrepass() 函数。

注意

  • 您需要确保每个阶段的输出架构与下一个阶段所需的输入架构匹配。在示例代码中,每个 TransformFunctionPhase::getReturnType() 实施将对其输入架构和输出架构执行健全性检查。TransformFunction 子类还可以在其 processPartition() 函数中执行这些检查。

  • 您的多阶段 UDTF 可以拥有的阶段数没有内置限制。但,阶段数越多,使用的资源就越多。在隔离模式下运行时,Vertica 可能会终止使用太多内存的 UDTF。请参阅C++ UDx 的资源使用情况

5.9.10 - Python 示例:多阶段计算

以下示例显示多阶段转换函数,该函数用于计算输入表中数字列的平均值。它会先定义两个转换函数,然后定义使用这些函数创建阶段的工厂。

有关完整的代码,请参阅示例分发中的 AvgMultiPhaseUDT.py

加载和使用示例

创建库和函数:

=> CREATE LIBRARY pylib_avg AS '/home/dbadmin/udx/AvgMultiPhaseUDT.py' LANGUAGE 'Python';
CREATE LIBRARY
=> CREATE TRANSFORM FUNCTION myAvg AS NAME 'MyAvgFactory' LIBRARY pylib_avg;
CREATE TRANSFORM FUNCTION

然后,您可以在 SELECT 语句中使用该函数:

=> CREATE TABLE IF NOT EXISTS numbers(num FLOAT);
CREATE TABLE

=> COPY numbers FROM STDIN delimiter ',';
1
2
3
4
\.

=> SELECT myAvg(num) OVER() FROM numbers;
 average | ignored_rows | total_rows
---------+--------------+------------
     2.5 |            0 |          4
(1 row)

设置

所有 Python UDx 都必须导入 Vertica SDK。此示例还会导入另一个库。

import vertica_sdk
import math

分量转换函数

多阶段转换函数必须定义两个或更多要在阶段中使用的 TransformFunction 子类。此示例会使用两个类:LocalCalculation(用于对本地分区进行计算)以及 GlobalCalculation(用于聚合所有 LocalCalculation 实例的结果以计算最终结果)。

在这两个函数中,计算都是在 processPartition() 函数中完成:

class LocalCalculation(vertica_sdk.TransformFunction):
    """
    This class is the first phase and calculates the local values for sum, ignored_rows and total_rows.
    """

    def setup(self, server_interface, col_types):
        server_interface.log("Setup: Phase0")
        self.local_sum = 0.0
        self.ignored_rows = 0
        self.total_rows = 0

    def processPartition(self, server_interface, input, output):
        server_interface.log("Process Partition: Phase0")

        while True:
            self.total_rows += 1

            if input.isNull(0) or math.isinf(input.getFloat(0)) or math.isnan(input.getFloat(0)):
                # Null, Inf, or Nan is ignored
                self.ignored_rows += 1
            else:
                self.local_sum += input.getFloat(0)

            if not input.next():
                break

        output.setFloat(0, self.local_sum)
        output.setInt(1, self.ignored_rows)
        output.setInt(2, self.total_rows)
        output.next()

class GlobalCalculation(vertica_sdk.TransformFunction):
    """
    This class is the second phase and aggregates the values for sum, ignored_rows and total_rows.
    """

    def setup(self, server_interface, col_types):
        server_interface.log("Setup: Phase1")
        self.global_sum = 0.0
        self.ignored_rows = 0
        self.total_rows = 0

    def processPartition(self, server_interface, input, output):
        server_interface.log("Process Partition: Phase1")

        while True:
            self.global_sum += input.getFloat(0)
            self.ignored_rows += input.getInt(1)
            self.total_rows += input.getInt(2)

            if not input.next():
                break

        average = self.global_sum / (self.total_rows - self.ignored_rows)

        output.setFloat(0, average)
        output.setInt(1, self.ignored_rows)
        output.setInt(2, self.total_rows)
        output.next()

多阶段工厂

MultiPhaseTransformFunctionFactory 会将各个函数作为阶段绑定在一起。工厂会为每个函数定义 TransformFunctionPhase。每个阶段都会定义 createTransformFunction(),用于调用对应的 TransformFunctiongetReturnType() 的构造函数。

下面是第一阶段 LocalPhase


class MyAvgFactory(vertica_sdk.MultiPhaseTransformFunctionFactory):
    """ Factory class """

    class LocalPhase(vertica_sdk.TransformFunctionPhase):
        """ Phase 1 """
        def getReturnType(self, server_interface, input_types, output_types):
            # sanity check
            number_of_cols = input_types.getColumnCount()
            if (number_of_cols != 1 or not input_types.getColumnType(0).isFloat()):
                raise ValueError("Function only accepts one argument (FLOAT))")

            output_types.addFloat("local_sum");
            output_types.addInt("ignored_rows");
            output_types.addInt("total_rows");

        def createTransformFunction(cls, server_interface):
            return LocalCalculation()

第二阶段 GlobalPhase 不会检查其输入,因为第一阶段已经进行了检查。与第一阶段一样,createTransformFunction 只是构造并返回对应的 TransformFunction


    class GlobalPhase(vertica_sdk.TransformFunctionPhase):
        """ Phase 2 """
        def getReturnType(self, server_interface, input_types, output_types):
            output_types.addFloat("average");
            output_types.addInt("ignored_rows");
            output_types.addInt("total_rows");

        def createTransformFunction(cls, server_interface):
            return GlobalCalculation()

在定义 TransformFunctionPhase 子类之后,工厂会将其实例化并在 getPhases() 中将其链接在一起。

    ph0Instance = LocalPhase()
    ph1Instance = GlobalPhase()

    def getPhases(cls, server_interface):
        cls.ph0Instance.setPrepass()
        phases = [cls.ph0Instance, cls.ph1Instance]
        return phases

5.9.11 - Python 示例:计数元素

以下示例会详细说明采用数组分区的 UDTF,计算分区中每个不同数组元素的计数,并将每个元素及其计数输出为行值。您可以对包含多个数组分区的表调用相应函数。

完整的源代码位于 /opt/vertica/sdk/examples/python/TransformFunctions.py 中。

加载和使用示例

加载库并创建转换函数,如下所示:

=> CREATE OR REPLACE LIBRARY TransformFunctions AS '/home/dbadmin/examples/python/TransformFunctions.py' LANGUAGE 'Python';

=> CREATE TRANSFORM FUNCTION CountElements AS LANGUAGE 'Python' NAME 'countElementsUDTFactory' LIBRARY TransformFunctions;

您可以创建一些数据,然后对其调用相应函数,例如:


=> CREATE TABLE orders (storeID int, productIDs array[int]);
CREATE TABLE

=> INSERT INTO orders VALUES
    (1, array[101, 102, 103]),
    (1, array[102, 104]),
    (1, array[101, 102, 102, 201, 203]),
    (2, array[101, 202, 203, 202, 203]),
    (2, array[203]),
    (2, array[]);
OUTPUT
--------
6
(1 row)

=> COMMIT;
COMMIT

=> SELECT storeID, CountElements(productIDs) OVER (PARTITION BY storeID) FROM orders;
storeID |       element_count
--------+---------------------------
      1 | {"element":101,"count":2}
      1 | {"element":102,"count":4}
      1 | {"element":103,"count":1}
      1 | {"element":104,"count":1}
      1 | {"element":201,"count":1}
      1 | {"element":202,"count":1}
      2 | {"element":101,"count":1}
      2 | {"element":202,"count":2}
      2 | {"element":203,"count":3}
(9 rows)

设置

所有 Python UDx 都必须导入 Vertica SDK 库:

import vertica_sdk

工厂实施

getPrototype() 方法会声明输入和输出可以是任何类型,这意味着必须在其他位置执行类型强制:


def getPrototype(self, srv_interface, arg_types, return_type):
    arg_types.addAny()
    return_type.addAny()

getReturnType() 将验证函数的唯一实参是否为数组,以及返回类型是否是具有“element”和“count”字段的行:


def getReturnType(self, srv_interface, arg_types, return_type):

    if arg_types.getColumnCount() != 1:
        srv_interface.reportError(1, 'countElements UDT should take exactly one argument')

    if not arg_types.getColumnType(0).isArrayType():
        srv_interface.reportError(2, 'Argument to countElements UDT should be an ARRAY')

    retRowFields = vertica_sdk.SizedColumnTypes.makeEmpty()
    retRowFields.addColumn(arg_types.getColumnType(0).getElementType(), 'element')
    retRowFields.addInt('count')
    return_type.addRowType(retRowFields, 'element_count')

countElementsUDTFactory 类还包含可实例化并返回转换函数的 createTransformFunction() 方法。

函数实施

使用名称分别为 arg_readerres_writerBlockReaderBlockWriter 来调用 processBlock() 方法。该函数会遍历分区中的所有输入数组,并使用字典来收集每个元素的频率。为了访问每个输入数组的元素,该方法会实例化 ArrayReader。收集元素计数后,该函数会将每个元素及其计数写入某个行中。对每个分区重复此过程。


def processPartition(self, srv_interface, arg_reader, res_writer):

    elemCounts = dict()
    # Collect element counts for entire partition
    while (True):
        if not arg_reader.isNull(0):
            arr = arg_reader.getArray(0)
            for elem in arr:
                elemCounts[elem] = elemCounts.setdefault(elem, 0) + 1

        if not arg_reader.next():
            break

    # Write at least one value for each partition
    if len(elemCounts) == 0:
        elemCounts[None] = 0

    # Write out element counts as (element, count) pairs
    for pair in elemCounts.items():
        res_writer.setRow(0, pair)
        res_writer.next()

5.10 - 用户定义的加载 (UDL)

COPY 提供了用于控制如何加载数据的大量选项和设置。但是,您可能发现这些选项不适合您要执行的数据加载类型。使用用户定义的加载 (UDL) 功能,您可以开发一个或多个用于更改 COPY 语句的工作方式的函数。您可以使用 Vertica SDK 来创建用于处理加载过程中的各个步骤的自定义库。。

您可以在开发期间使用以下三种类型的 UDL 函数,每种类型适用于数据加载过程的每个阶段:

  • 用户自定义的源 (UDSource):控制 COPY 如何获取要加载到数据库中的数据。例如,COPY 获取数据的方法可能是通过 HTTP 或 cURL 提取数据。最多只能有一个 UDSource 从文件或输入流读取数据。UDSource 可以从多个源读取数据,但 COPY 只能调用一个 UDSource。

    API 支持:C++、Java。

  • 用户自定义的筛选器 (UDFilter):预处理数据。例如,过滤器可以解压缩文件或将 UTF-16 转换为 UTF-8。可以将多个用户定义的筛选器链接到一起,例如,先解压缩再转换。

    API 支持:C++、Java、Python。

  • 用户自定义的解析器 (UDParser):最多只能有一个解析器将数据解析为可供插入到表中的元组。例如,解析器可以从类似于 XML 的格式提取数据。您可以选择定义用户定义的块分割器(UDChunker,仅限 C++),以让解析器执行并行解析。

    API 支持:C++、Java、Python。

完成最后一步之后,COPY 会将数据插入到表中,或者在格式不正确时拒绝该数据。

5.10.1 - 用户定义的源

使用用户定义的源,您可以使用未内置在 Vertica 中的方法来处理数据源。例如,您可以编写用户定义的源,以使用 cURL 访问来自 HTTP 源的数据。虽然给定的 COPY 语句只能指定一个用户定义的源语句,但源函数本身可以从多个源中拉取数据。

UDSource 类将从外部源获取数据。此类可从输入流读取数据,并生成输出流以进行过滤和解析。如果实施 UDSource,则还必须实施相应的 SourceFactory

5.10.1.1 - UDSource 类

如果需要从 COPY 已不支持的源类型加载数据,您可以将 UDSource 类子类化。

UDSource 子类的每个实例从单个数据源读取数据。例如,单个数据源可以是单个文件或对 RESTful Web 应用程序执行的单个函数调用的结果。

UDSource 方法

您的 UDSource 子类必须覆盖 process()processWithMetadata()

  • process() 将原始输入流作为一个大文件读取。如果有任何错误或故障,整个加载将失败。

  • processWithMetadata() 当数据源具有以某种结构化格式(与数据有效负载分开)提供的关于记录边界的元数据时很有用。使用此接口,源除了发出数据之外,还会为每个记录发出记录长度。

    通过在每个阶段实施 processWithMetadata() 而不是 process(),您可以在整个加载堆栈中保留此记录长度元数据,从而实施更有效的解析,以在每个消息的基础上而不是每个文件或每个源的基础上从错误中恢复。 KafkaSource 和 Kafka 解析器(KafkaAvroParserKafkaJSONParserKafkaParser)会在各条 Kafka 消息无法解析时使用这种机制为拒绝每条 Kafka 消息提供支持。

此外,还可以覆盖其他 UDSource 类方法。

源执行

以下各节详细说明了每次调用用户定义的源时的执行序列。以下示例覆盖 process() 方法。

设置
COPY 在第一次调用 process() 之前调用了 setup()。使用 setup() 可执行访问数据源所需的任何设置步骤。此方法可以建立网络连接和打开文件,还可以执行类似的必需任务以使 UDSource 能够从数据源读取数据。您的对象可能在使用期间已损坏并重新创建,因此请确保您的对象可以重新启动。

处理源
COPY 将在查询执行期间重复调用 process(),以读取数据并将数据写入到作为参数传递的 DataBuffer。然后,此缓冲区会传递到第一个过滤器。

如果源已用完输入或已填满输出缓冲区,则它必须返回值 StreamState.OUTPUT_NEEDED。如果 Vertica 收到此返回值,它将再次调用该方法。此第二次调用在数据加载过程的下一个阶段已处理输出缓冲区之后进行。如果返回 StreamState.DONE,则指示已读取源中的所有数据。

用户可以取消加载操作,这样会中止读取。

分解
COPY 将在最后一次调用 process() 之后调用 destroy()。此方法可释放由 setup()process() 方法预留的任何资源,例如,setup() 方法分配的文件句柄或网络连接。

取值函数

一个源可以定义两个取值函数,即 getSize()getUri()

调用 process() 之前,COPY 可能会调用 getSize() 以估算要读取的数据的字节数。此值只是一个估算值,用于在 LOAD_STREAMS 表中指示文件大小。由于 Vertica 会在调用 setup() 之前调用该方法,因此 getSize() 不得依赖由 setup() 分配的任何资源。

此方法不应使任何资源处于打开状态。例如,请勿保存由 getSize() 打开以供 process() 方法使用的任何文件句柄,否则会耗尽可用资源,因为 Vertica 将在加载任何数据之前会对 UDSource 子类的所有实例调用 getSize()。如果正在打开许多数据源,这些打开的文件句柄可能会用完系统提供的文件句柄,从而导致没有任何其他文件句柄可用于执行实际的数据加载。

Vertica 将在执行期间调用 getUri(),以更新有关当前正在加载哪些资源的状态信息。它会返回由此 UDSource 读取的数据源的 URI。

API

UDSource API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface);

virtual bool useSideChannel();

virtual StreamState process(ServerInterface &srvInterface, DataBuffer &output)=0;

virtual StreamState processWithMetadata(ServerInterface &srvInterface, DataBuffer &output, LengthBuffer &output_lengths)=0;

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface);

virtual vint getSize();

virtual std::string getUri();

UDSource API 提供了以下通过子类扩展的方法:

public void setup(ServerInterface srvInterface) throws UdfException;

public abstract StreamState process(ServerInterface srvInterface, DataBuffer output) throws UdfException;

protected void cancel(ServerInterface srvInterface);

public void destroy(ServerInterface srvInterface) throws UdfException;

public Integer getSize();

public String getUri();

5.10.1.2 - SourceFactory 类

如果编写源,您还必须编写源工厂。SourceFactory 类的子类负责执行下列任务:

  • 对传递到 UDSource 的参数执行初始验证。

  • 设置 UDSource 实例执行其工作所需的任何数据结构。此信息可能包括有关哪些节点将读取哪个数据源的记录。

  • 为函数在每个主机上读取的每个数据源(或其一部分)创建 UDSource 子类的一个实例。

最简单的源工厂将为每个执行程序节点的每个数据源创建一个 UDSource 实例。您还可以在每个节点上使用多个并发 UDSource 实例。此行为称为并发加载。为了支持这两个选项,SourceFactory 拥有可创建源的方法的两个版本。您必须准确实施其中一个版本。

源工厂是单例。您的子类必须为无状态子类,没有包含数据的字段。该子类还不得修改任何全局变量。

SourceFactory 方法

SourceFactory 类定义了多个方法。您的类必须覆盖 prepareUDSources();它可以覆盖其他方法。

设置

Vertica 将在启动程序节点上调用 plan() 一次,以执行下列任务:

  • 检查用户已向 COPY 语句中的函数调用提供的参数,并提供错误消息(如果出现任何问题)。您可以通过从传递到 plan() 方法的 ServerInterface 的实例获取 ParamReader 对象来读取参数。

  • 确定群集中的哪些主机将读取该数据源。如何拆分工作取决于函数将读取的源。某些源可以跨多个主机进行拆分,例如从多个 URL 读取数据的源。其他源(例如主机文件系统上的单个本地文件)只能由单个特定主机读取。

    可以通过对 NodeSpecifyingPlanContext 对象调用 setTargetNodes() 方法来存储要读取数据源的主机的列表。此对象会传递到 plan() 方法中。

  • 存储各个主机所需的任何信息,以便处理传递到 plan() 方法的 NodeSpecifyingPlanContext 实例中的数据源。例如,您可以存储分配,用于告知每个主机要处理的数据源。plan() 方法仅在启动程序节点上运行,而 prepareUDSources() 方法则在每个读取数据源的主机上运行。因此,该对象是它们之间的唯一通信方式。

    您通过从 getWriter() 方法中获取 ParamWriter 对象在 NodeSpecifyingPlanContext 中存储数据。然后,通过对 ParamWriter 调用方法(比如 setString())写入参数。

创建源

Vertica 会在 plan() 方法已选择将数据加载到的所有主机上调用 prepareUDSources()。此调用会实例化并返回 UDSource 子类实例的列表。如果不使用并发加载,请为分配给主机进行处理的每个源返回一个 UDSource。如果要使用并发加载,请使用将 ExecutorPlanContext 作为参数的方法版本,并尽可能多地返回可供使用的源。您的工厂必须准确实施这些方法之一。

对于并发加载,您可以通过在传入的 ExecutorPlanContext 上调用 getLoadConcurrency() 来了解节点上可供运行 UDSource 实例的线程数。

定义参数

实施 getParameterTypes() 可定义源所使用的参数的名称和类型。Vertica 使用此信息对调用人员发出有关未知或缺失参数的警告。Vertica 会忽略未知参数并为缺失参数使用默认值。当您需要为函数定义类型和参数时,您不需要覆盖此方法。

请求并发加载的线程

当源工厂在执行程序节点上创建源时,默认情况下,它会为每个源创建一个线程。如果您的源可以使用多个线程,请实施 getDesiredThreads()。在调用 prepareUDSources() 之前,Vertica 会先调用此方法,因此您也可以使用它来决定要创建的源数量。返回工厂可用于源的线程数。已传入可用线程的最大数量,因此您可以将其考虑在内。方法返回的值只是一种提示,而不是保证;每个执行程序节点均可确定要分配的线程数。FilePortionSourceFactory 示例将实施此方法;请参阅 C++ 示例:并发加载

您可以允许源控制并行度,这意味着它可以通过实施 isSourceApportionable() 将单个输入拆分成多个加载流。即使此方法返回 true,也不保证源分摊该加载。然而,返回 false 表示解析器不会尝试这么做。有关详细信息,请参阅分摊加载

通常,实施 getDesiredThreads()SourceFactory 也使用分摊加载。但是,使用分摊加载不是必需的。例如,从 Kafka 流中读取的源可以使用多个线程而无需分摊。

API

SourceFactory API 提供了以下通过子类扩展的方法:

virtual void plan(ServerInterface &srvInterface, NodeSpecifyingPlanContext &planCtxt);

// must implement exactly one of prepareUDSources() or prepareUDSourcesExecutor()
virtual std::vector< UDSource * > prepareUDSources(ServerInterface &srvInterface,
            NodeSpecifyingPlanContext &planCtxt);

virtual std::vector< UDSource * > prepareUDSourcesExecutor(ServerInterface &srvInterface,
            ExecutorPlanContext &planCtxt);

virtual void getParameterType(ServerInterface &srvInterface,
            SizedColumnTypes &parameterTypes);

virtual bool isSourceApportionable();

ssize_t getDesiredThreads(ServerInterface &srvInterface,
            ExecutorPlanContext &planContext);

创建 SourceFactory 之后,您必须将其注册到 RegisterFactory 宏。

SourceFactory API 提供了以下通过子类扩展的方法:

public void plan(ServerInterface srvInterface, NodeSpecifyingPlanContext planCtxt)
    throws UdfException;

// must implement one overload of prepareUDSources()
public ArrayList< UDSource > prepareUDSources(ServerInterface srvInterface,
                NodeSpecifyingPlanContext planCtxt)
    throws UdfException;

public ArrayList< UDSource > prepareUDSources(ServerInterface srvInterface,
                ExecutorPlanContext planCtxt)
    throws UdfException;

public void getParameterType(ServerInterface srvInterface, SizedColumnTypes parameterTypes);

public boolean isSourceApportionable();

public int getDesiredThreads(ServerInterface srvInterface,
                ExecutorPlanContext planCtxt)
    throws UdfException;

5.10.1.3 - C++ 示例: CurlSource

使用 CurlSource 示例,您可以使用 cURL 通过 HTTP 打开和读入文件。该示例作为以下内容的一部分提供:/opt/vertica/sdk/examples/SourceFunctions/cURL.cpp

源代码实施

此示例使用位于 /opt/vertica/sdk/examples/HelperLibraries/ 中的 helper 库。

CurlSource 按区块加载数据。如果解析器遇到 EndOfFile 标记,则 process() 方法将返回 DONE。否则,该方法将返回 OUTPUT_NEEDED 并处理其他数据区块。helper 库中包含的函数(例如 url_fread()url_fopen())基于随 libcurl 库附带提供的示例。有关示例,请访问 http://curl.haxx.se/libcurl/c/fopen.html

setup() 函数可打开文件句柄,而 destroy() 函数可关闭文件句柄。它们都使用 helper 库中的函数。

class CurlSource : public UDSource {private:
    URL_FILE *handle;
    std::string url;
    virtual StreamState process(ServerInterface &srvInterface, DataBuffer &output) {
        output.offset = url_fread(output.buf, 1, output.size, handle);
        return url_feof(handle) ? DONE : OUTPUT_NEEDED;
    }
public:
    CurlSource(std::string url) : url(url) {}
    void setup(ServerInterface &srvInterface) {
        handle = url_fopen(url.c_str(),"r");
    }
    void destroy(ServerInterface &srvInterface) {
        url_fclose(handle);
    }
};

工厂实施

CurlSourceFactory 可生成 CurlSource 实例。

class CurlSourceFactory : public SourceFactory {public:
    virtual void plan(ServerInterface &srvInterface,
            NodeSpecifyingPlanContext &planCtxt) {
        std::vector<std::string> args = srvInterface.getParamReader().getParamNames();
       /* Check parameters */
        if (args.size() != 1 || find(args.begin(), args.end(), "url") == args.end()) {
            vt_report_error(0, "You must provide a single URL.");
        }
        /* Populate planData */
        planCtxt.getWriter().getStringRef("url").copy(
                                    srvInterface.getParamReader().getStringRef("url"));

        /* Assign Nodes */
        std::vector<std::string> executionNodes = planCtxt.getClusterNodes();
        while (executionNodes.size() > 1) executionNodes.pop_back();
        // Only run on the first node in the list.
        planCtxt.setTargetNodes(executionNodes);
    }
    virtual std::vector<UDSource*> prepareUDSources(ServerInterface &srvInterface,
            NodeSpecifyingPlanContext &planCtxt) {
        std::vector<UDSource*> retVal;
        retVal.push_back(vt_createFuncObj(srvInterface.allocator, CurlSource,
                planCtxt.getReader().getStringRef("url").str()));
        return retVal;
    }
    virtual void getParameterType(ServerInterface &srvInterface,
                                  SizedColumnTypes &parameterTypes) {
        parameterTypes.addVarchar(65000, "url");
    }
};
RegisterFactory(CurlSourceFactory);

5.10.1.4 - C++ 示例:并发加载

FilePortionSource 示例演示了如何使用并发加载。此示例是对 FileSource 示例进行的改进。每个输入文件都会拆分成多个部分并分发到 FilePortionSource 实例。源接受将输入分成多个部分所依据的偏移量列表;如果未提供偏移量,源将动态拆分输入。

并发加载将在工厂中进行处理,所以本文讨论的重点是 FilePortionSourceFactory。该示例的完整代码位于 /opt/vertica/sdk/examples/ApportionLoadFunctions 中。该分发还包括此示例的 Java 版本。

加载和使用示例

按如下所示加载并使用 FilePortionSource 示例。

=> CREATE LIBRARY FilePortionLib AS '/home/dbadmin/FP.so';

=> CREATE SOURCE FilePortionSource AS LANGUAGE 'C++'
-> NAME 'FilePortionSourceFactory' LIBRARY FilePortionLib;

=> COPY t WITH SOURCE FilePortionSource(file='g1/*.dat', nodes='initiator,e0,e1', offsets = '0,380000,820000');

=> COPY t WITH SOURCE FilePortionSource(file='g2/*.dat', nodes='e0,e1,e2', local_min_portion_size = 2097152);

实施

并发加载会在两个位置影响源工厂:getDesiredThreads()prepareUDSourcesExecutor()

getDesiredThreads()

getDesiredThreads() 成员函数可确定要请求的线程数。Vertica 会在调用 prepareUDSourcesExecutor() 之前在每个执行程序节点上调用此成员函数。

该函数会先开始将输入文件路径(可能为 glob)分成多个单独的路径。本讨论省略了这些细节。如果未使用分摊加载,则该函数为每个文件分配一个源。

virtual ssize_t getDesiredThreads(ServerInterface &srvInterface,
    ExecutorPlanContext &planCtxt) {
  const std::string filename = srvInterface.getParamReader().getStringRef("file").str();

  std::vector<std::string> paths;
  // expand the glob - at least one thread per source.
  ...

  // figure out how to assign files to sources
  const std::string nodeName = srvInterface.getCurrentNodeName();
  const size_t nodeId = planCtxt.getWriter().getIntRef(nodeName);
  const size_t numNodes = planCtxt.getTargetNodes().size();

  if (!planCtxt.canApportionSource()) {
    /* no apportioning, so the number of files is the final number of sources */
    std::vector<std::string> *expanded =
        vt_createFuncObject<std::vector<std::string> >(srvInterface.allocator, paths);
    /* save expanded paths so we don't have to compute expansion again */
    planCtxt.getWriter().setPointer("expanded", expanded);
    return expanded->size();
  }

  // ...

如果可以分摊源,则 getDesiredThreads() 会使用作为实参传递给工厂的偏移量,以将文件拆分成多个部分。然后,它会将这些部分分配给可用节点。此函数实际上不会直接分配源;完成这项工作是为了确定要请求的线程数。

  else if (srvInterface.getParamReader().containsParameter("offsets")) {

    // if the offsets are specified, then we will have a fixed number of portions per file.
    // Round-robin assign offsets to nodes.
    // ...

    /* Construct the portions that this node will actually handle.
     * This isn't changing (since the offset assignments are fixed),
     * so we'll build the Portion objects now and make them available
     * to prepareUDSourcesExecutor() by putting them in the ExecutorContext.
     *
     * We don't know the size of the last portion, since it depends on the file
     * size.  Rather than figure it out here we will indicate it with -1 and
     * defer that to prepareUDSourcesExecutor().
     */
    std::vector<Portion> *portions =
        vt_createFuncObject<std::vector<Portion>>(srvInterface.allocator);

    for (std::vector<size_t>::const_iterator offset = offsets.begin();
            offset != offsets.end(); ++offset) {
        Portion p(*offset);
        p.is_first_portion = (offset == offsets.begin());
        p.size = (offset + 1 == offsets.end() ? -1 : (*(offset + 1) - *offset));

        if ((offset - offsets.begin()) % numNodes == nodeId) {
            portions->push_back(p);
            srvInterface.log("FilePortionSource: assigning portion %ld: [offset = %lld, size = %lld]",
                    offset - offsets.begin(), p.offset, p.size);
        }
      }

该函数现在具有所有部分,因此可以知道部分的数量:

      planCtxt.getWriter().setPointer("portions", portions);

      /* total number of threads we want is the number of portions per file, which is fixed */
      return portions->size() * expanded->size();
    } // end of "offsets" parameter

如果未提供偏移量,则该函数会将文件动态拆分成多个部分,每个线程一个部分。本讨论省略了此计算过程的细节。请求的线程数多于可用线程数没有意义,因此该函数会对用 PlanContext(函数的实参)调用 getMaxAllowedThreads() 来设置上限:

  if (portions->size() >= planCtxt.getMaxAllowedThreads()) {
    return paths.size();
  }

有关此函数如何将文件拆分成多个部分的详细信息,请参阅完整示例。

此函数使用 vt_createFuncObject 模板来创建对象。Vertica 会调用使用此宏创建的返回对象的析构函数,但它不会调用其他对象(如向量)的析构函数。您必须自己调用这些析构函数以避免内存泄漏。在此示例中,这些调用是在 prepareUDSourcesExecutor() 中进行的。

prepareUDSourcesExecutor()

prepareUDSourcesExecutor() 成员函数(如 getDesiredThreads())包含单独的代码块,具体取决于是否提供了偏移量。在这两种情况下,该函数都会将输入拆分成多个部分并为它们创建 UDSource 实例。

如果使用偏移量调用函数,则 prepareUDSourcesExecutor() 会调用 prepareCustomizedPortions()。此函数如下所示。

/* prepare portions as determined via the "offsets" parameter */
void prepareCustomizedPortions(ServerInterface &srvInterface,
                               ExecutorPlanContext &planCtxt,
                               std::vector<UDSource *> &sources,
                               const std::vector<std::string> &expandedPaths,
                               std::vector<Portion> &portions) {
    for (std::vector<std::string>::const_iterator filename = expandedPaths.begin();
            filename != expandedPaths.end(); ++filename) {
        /*
         * the "portions" vector contains the portions which were generated in
         * "getDesiredThreads"
         */
        const size_t fileSize = getFileSize(*filename);
        for (std::vector<Portion>::const_iterator portion = portions.begin();
                portion != portions.end(); ++portion) {
            Portion fportion(*portion);
            if (fportion.size == -1) {
                /* as described above, this means from the offset to the end */
                fportion.size = fileSize - portion->offset;
                sources.push_back(vt_createFuncObject<FilePortionSource>(srvInterface.allocator,
                            *filename, fportion));
            } else if (fportion.size > 0) {
                sources.push_back(vt_createFuncObject<FilePortionSource>(srvInterface.allocator,
                            *filename, fportion));
            }
        }
    }
}

如果未使用偏移量调用 prepareUDSourcesExecutor(),则它必须决定要创建的部分数。

基本规则是每个源使用一个部分。但是,如果有额外的线程可用,该函数会将输入拆分成更多部分,以便源可以同时处理它们。然后,prepareUDSourcesExecutor() 会调用 prepareGeneratedPortions() 以创建这些部分。该函数会先开始在计划上下文中调用 getLoadConcurrency() 以找到可用的线程数。

void prepareGeneratedPortions(ServerInterface &srvInterface,
                              ExecutorPlanContext &planCtxt,
                              std::vector<UDSource *> &sources,
                              std::map<std::string, Portion> initialPortions) {

  if ((ssize_t) initialPortions.size() >= planCtxt.getLoadConcurrency()) {
  /* all threads will be used, don't bother splitting into portions */

  for (std::map<std::string, Portion>::const_iterator file = initialPortions.begin();
       file != initialPortions.end(); ++file) {
    sources.push_back(vt_createFuncObject<FilePortionSource>(srvInterface.allocator,
            file->first, file->second));
       } // for
    return;
  } // if

  // Now we can split files to take advantage of potentially-unused threads.
  // First sort by size (descending), then we will split the largest first.

  // details elided...

}

有关详细信息

有关此示例的完整实施,请参阅源代码。

5.10.1.5 - Java 示例: FileSource

本节中所示的示例是一个名为 FileSource 的简单 UDL 源函数,此函数可加载存储在主机文件系统上的文件中的数据(类似于标准 COPY 语句)。要调用 FileSource,您必须提供一个名为 file 的参数,并且该参数必须包含主机文件系统上的一个或多个文件的绝对路径。您可以使用逗号分隔列表格式指定多个文件。

FileSource 函数还接受一个名为 nodes 的可选参数,此可选参数指示哪些节点应加载文件。如果未提供此参数,则在默认情况下,函数仅在启动程序节点上加载数据。由于此示例是一个简单示例,因此节点仅加载自己的文件系统中的文件。file 参数中的任何文件必须存在于 nodes 参数中的所有主机上。FileSource UDSource 会尝试将 file 参数中的所有文件加载到 nodes 参数中的所有主机上。

生成文件

可以使用以下 Python 脚本生成文件并将这些文件分发给 Vertica 群集中的主机。使用这些文件,您可以试验示例 UDSource 函数。要运行此函数,您必须能够进行无密码 SSH 登录,以将文件复制到其他主机。因此,您必须使用数据库管理员帐户在数据库主机之一上运行该脚本。

#!/usr/bin/python
# Save this file as UDLDataGen.py
import string
import random
import sys
import os

# Read in the dictionary file to provide random words. Assumes the words
# file is located in /usr/share/dict/words
wordFile = open("/usr/share/dict/words")
wordDict = []
for line in wordFile:
    if len(line) > 6:
        wordDict.append(line.strip())

MAXSTR = 4 # Maximum number of words to concatentate
NUMROWS = 1000 # Number of rows of data to generate
#FILEPATH = '/tmp/UDLdata.txt' # Final filename to use for UDL source
TMPFILE = '/tmp/UDLtemp.txt'  # Temporary filename.

# Generate a random string by concatenating several words together. Max
# number of words set by MAXSTR
def randomWords():
    words = [random.choice(wordDict) for n in xrange(random.randint(1, MAXSTR))]
    sentence = " ".join(words)
    return sentence

# Create a temporary data file that will be moved to a node. Number of
# rows for the file is set by NUMROWS. Adds the name of the node which will
# get the file, to show which node loaded the data.
def generateFile(node):
    outFile = open(TMPFILE, 'w')
    for line in xrange(NUMROWS):
        outFile.write('{0}|{1}|{2}\n'.format(line,randomWords(),node))
    outFile.close()

# Copy the temporary file to a node. Only works if passwordless SSH login
# is enabled, which it is for the database administrator account on
# Vertica hosts.
def copyFile(fileName,node):
    os.system('scp "%s" "%s:%s"' % (TMPFILE, node, fileName) )

# Loop through the comma-separated list of nodes given in the first
# parameter, creating and copying data files whose full comma-separated
# paths are passed in the second parameter
for node in [x.strip() for x in sys.argv[1].split(',')]:
    for fileName in [y.strip() for y in sys.argv[2].split(',')]:
        print "generating file", fileName, "for", node
        generateFile(node)
        print "Copying file to",node
        copyFile(fileName,node)

您可以调用该脚本,并为其提供要接收文件的主机的逗号分隔列表和要生成的文件的绝对路径的逗号分隔列表。例如:

python UDLDataGen.py v_vmart_node0001,v_vmart_node0002,v_vmart_node0003 /tmp/UDLdata01.txt,/tmp/UDLdata02.txt,\
UDLdata03.txt

该脚本将生成包含一千个行(各列用管道字符 (|) 分隔)的文件。这些列包含一个索引值、一组随机词以及为其生成了文件的节点,如以下输出示例所示:

0|megabits embanks|v_vmart_node0001
1|unneatly|v_vmart_node0001
2|self-precipitation|v_vmart_node0001
3|antihistamine scalados Vatter|v_vmart_node0001

加载和使用示例

按如下所示加载并使用 FileSource UDSource:

=> --Load library and create the source function
=> CREATE LIBRARY JavaLib AS '/home/dbadmin/JavaUDlLib.jar'
-> LANGUAGE 'JAVA';
CREATE LIBRARY
=> CREATE SOURCE File as LANGUAGE 'JAVA' NAME
-> 'com.mycompany.UDL.FileSourceFactory' LIBRARY JavaLib;
CREATE SOURCE FUNCTION
=> --Create a table to hold the data loaded from files
=> CREATE TABLE t (i integer, text VARCHAR, node VARCHAR);
CREATE TABLE
=> -- Copy a single file from the currently host using the FileSource
=> COPY t SOURCE File(file='/tmp/UDLdata01.txt');
 Rows Loaded
-------------
        1000
(1 row)

=> --See some of what got loaded.
=> SELECT * FROM t WHERE i < 5 ORDER BY i;
 i |             text              |  node
---+-------------------------------+-----------------
 0 | megabits embanks              | v_vmart_node0001
 1 | unneatly                      | v_vmart_node0001
 2 | self-precipitation            | v_vmart_node0001
 3 | antihistamine scalados Vatter | v_vmart_node0001
 4 | fate-menaced toilworn         | v_vmart_node0001
(5 rows)



=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> -- Now load a file from three hosts. All of these hosts must have a file
=> -- named /tmp/UDLdata01.txt, each with different data
=> COPY t SOURCE File(file='/tmp/UDLdata01.txt',
-> nodes='v_vmart_node0001,v_vmart_node0002,v_vmart_node0003');
 Rows Loaded
-------------
        3000
(1 row)

=> --Now see what has been loaded
=> SELECT * FROM t WHERE i < 5 ORDER BY i,node ;
 i |                      text                       |  node
---+-------------------------------------------------+--------
 0 | megabits embanks                                | v_vmart_node0001
 0 | nimble-eyed undupability frowsier               | v_vmart_node0002
 0 | Circean nonrepellence nonnasality               | v_vmart_node0003
 1 | unneatly                                        | v_vmart_node0001
 1 | floatmaker trabacolos hit-in                    | v_vmart_node0002
 1 | revelrous treatableness Halleck                 | v_vmart_node0003
 2 | self-precipitation                              | v_vmart_node0001
 2 | whipcords archipelagic protodonatan copycutter  | v_vmart_node0002
 2 | Paganalian geochemistry short-shucks            | v_vmart_node0003
 3 | antihistamine scalados Vatter                   | v_vmart_node0001
 3 | swordweed touristical subcommanders desalinized | v_vmart_node0002
 3 | batboys                                         | v_vmart_node0003
 4 | fate-menaced toilworn                           | v_vmart_node0001
 4 | twice-wanted cirrocumulous                      | v_vmart_node0002
 4 | doon-head-clock                                 | v_vmart_node0003
(15 rows)

=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> --Now copy from several files on several hosts
=> COPY t SOURCE File(file='/tmp/UDLdata01.txt,/tmp/UDLdata02.txt,/tmp/UDLdata03.txt'
-> ,nodes='v_vmart_node0001,v_vmart_node0002,v_vmart_node0003');
 Rows Loaded
-------------
        9000
(1 row)

=> SELECT * FROM t WHERE i = 0 ORDER BY node ;
 i |                    text                     |  node
---+---------------------------------------------+--------
 0 | Awolowo Mirabilis D'Amboise                 | v_vmart_node0001
 0 | sortieing Divisionism selfhypnotization     | v_vmart_node0001
 0 | megabits embanks                            | v_vmart_node0001
 0 | nimble-eyed undupability frowsier           | v_vmart_node0002
 0 | thiaminase hieroglypher derogated soilborne | v_vmart_node0002
 0 | aurigraphy crocket stenocranial             | v_vmart_node0002
 0 | Khulna pelmets                              | v_vmart_node0003
 0 | Circean nonrepellence nonnasality           | v_vmart_node0003
 0 | matterate protarsal                         | v_vmart_node0003
(9 rows)

解析器实施

以下代码显示了从主机文件系统读取文件的 FileSource 类的源。FileSourceFactory.prepareUDSources() 所调用的构造函数可获取包含要读取的数据的文件的绝对文件。setup() 方法可打开文件,而 destroy() 方法可关闭文件。process() 方法可读取文件中的数据并将数据写入到缓冲区(由作为参数传递的 DataBuffer 类的实例提供)。如果读取操作已将输出缓冲区填满,则该方法将返回 OUTPUT_NEEDED。此值可指示 Vertica 在加载的下一个阶段已处理输出缓冲区之后再次调用该方法。如果读取操作未将输出缓冲区填满,则 process() 将返回 DONE,以指示已完成对数据源的处理。

package com.mycompany.UDL;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;

import com.vertica.sdk.DataBuffer;
import com.vertica.sdk.ServerInterface;
import com.vertica.sdk.State.StreamState;
import com.vertica.sdk.UDSource;
import com.vertica.sdk.UdfException;

public class FileSource extends UDSource {

    private String filename;  // The file for this UDSource to read
    private RandomAccessFile reader;   // handle to read from file


    // The constructor just stores the absolute filename of the file it will
    // read.
    public FileSource(String filename) {
        super();
        this.filename = filename;
    }

    // Called before Vertica starts requesting data from the data source.
    // In this case, setup needs to open the file and save to the reader
    // property.
    @Override
    public void setup(ServerInterface srvInterface ) throws UdfException{
        try {
            reader = new RandomAccessFile(new File(filename), "r");
        } catch (FileNotFoundException e) {
            // In case of any error, throw a UDfException. This will terminate
            // the data load.
             String msg = e.getMessage();
             throw new UdfException(0, msg);
        }
    }

    // Called after data has been loaded. In this case, close the file handle.
    @Override
    public void destroy(ServerInterface srvInterface ) throws UdfException {
        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                String msg = e.getMessage();
                 throw new UdfException(0, msg);
            }
        }
    }

    @Override
    public StreamState process(ServerInterface srvInterface, DataBuffer output)
                                throws UdfException {

        // Read up to the size of the buffer provided in the DataBuffer.buf
        // property. Here we read directly from the file handle into the
        // buffer.
        long offset;
        try {
            offset = reader.read(output.buf,output.offset,
                                 output.buf.length-output.offset);
        } catch (IOException e) {
            // Throw an exception in case of any errors.
            String msg = e.getMessage();
            throw new UdfException(0, msg);
        }

        // Update the number of bytes processed so far by the data buffer.
        output.offset +=offset;

        // See end of data source has been reached, or less data was read
        // than can fit in the buffer
        if(offset == -1 || offset < output.buf.length) {
            // No more data to read.
            return StreamState.DONE;
        }else{
            // Tell Vertica to call again when buffer has been emptied
            return StreamState.OUTPUT_NEEDED;
        }
    }
}

工厂实施

以下代码是 Java UDx 支持包中提供的示例 Java UDsource 函数的修改版本。可以在 /opt/vertica/sdk/examples/JavaUDx/UDLFuctions/com/vertica/JavaLibs/FileSourceFactory.java 中找到完整示例。通过覆盖 plan() 方法,可验证用户是否提供了必需的 file 参数。如果用户还提供了可选的 nodes 参数,该方法还可以验证节点是否存在于 Vertica 群集中。如果任一参数存在问题,该方法将抛出异常并向用户返回错误。如果两个参数都没有问题,则 plan() 方法会将其值存储在计划上下文对象中。

package com.mycompany.UDL;

import java.util.ArrayList;
import java.util.Vector;
import com.vertica.sdk.NodeSpecifyingPlanContext;
import com.vertica.sdk.ParamReader;
import com.vertica.sdk.ParamWriter;
import com.vertica.sdk.ServerInterface;
import com.vertica.sdk.SizedColumnTypes;
import com.vertica.sdk.SourceFactory;
import com.vertica.sdk.UDSource;
import com.vertica.sdk.UdfException;

public class FileSourceFactory extends SourceFactory {

    // Called once on the initiator host to do initial setup. Checks
    // parameters and chooses which nodes will do the work.
    @Override
    public void plan(ServerInterface srvInterface,
            NodeSpecifyingPlanContext planCtxt) throws UdfException {

        String nodes; // stores the list of nodes that will load data

        // Get  copy of the parameters the user supplied to the UDSource
        // function call.
        ParamReader args =  srvInterface.getParamReader();

        // A list of nodes that will perform work. This gets saved as part
        // of the plan context.
        ArrayList<String> executionNodes = new ArrayList<String>();

        // First, ensure the user supplied the file parameter
        if (!args.containsParameter("file")) {
            // Withut a file parameter, we cannot continue. Throw an
            // exception that will be caught by the Java UDx framework.
            throw new UdfException(0, "You must supply a file parameter");
        }

        // If the user specified nodes to read the file, parse the
        // comma-separated list and save. Otherwise, assume just the
        // Initiator node has the file to read.
        if (args.containsParameter("nodes")) {
            nodes = args.getString("nodes");

            // Get list of nodes in cluster, to ensure that the node the
            // user specified actually exists. The list of nodes is available
            // from the planCTxt (plan context) object,
            ArrayList<String> clusterNodes = planCtxt.getClusterNodes();

            // Parse the string parameter "nodes" which
            // is a comma-separated list of node names.
            String[] nodeNames = nodes.split(",");

            for (int i = 0; i < nodeNames.length; i++){
                // See if the node the user gave us actually exists
                if(clusterNodes.contains(nodeNames[i]))
                    // Node exists. Add it to list of nodes.
                    executionNodes.add(nodeNames[i]);
                else{
                    // User supplied node that doesn't exist. Throw an
                    // exception so the user is notified.
                    String msg = String.format("Specified node '%s' but no" +
                        " node by that name is available.  Available nodes "
                        + "are \"%s\".",
                        nodeNames[i], clusterNodes.toString());
                    throw new UdfException(0, msg);
                }
            }
        } else {
            // User did not supply a list of node names. Assume the initiator
            // is the only host that will read the file. The srvInterface
            // instance passed to this method has a getter for the current
            // node.
            executionNodes.add(srvInterface.getCurrentNodeName());
        }

        // Set the target node(s) in the plan context
        planCtxt.setTargetNodes(executionNodes);

        // Set parameters for each node reading data that tells it which
        // files it will read. In this simple example, just tell it to
        // read all of the files the user passed in the file parameter
        String files = args.getString("file");

        // Get object to write parameters into the plan context object.
        ParamWriter nodeParams = planCtxt.getWriter();

        // Loop through list of execution nodes, and add a parameter to plan
        // context named for each node performing the work, which tells it the
        // list of files it will process. Each node will look for a
        // parameter named something like "filesForv_vmart_node0002" in its
        // prepareUDSources() method.
        for (int i = 0; i < executionNodes.size(); i++) {
            nodeParams.setString("filesFor" + executionNodes.get(i), files);
        }
    }

    // Called on each host that is reading data from a source. This method
    // returns an array of UDSource objects that process each source.
    @Override
    public ArrayList<UDSource> prepareUDSources(ServerInterface srvInterface,
            NodeSpecifyingPlanContext planCtxt) throws UdfException {

        // An array to hold the UDSource subclasses that we instaniate
        ArrayList<UDSource> retVal = new ArrayList<UDSource>();

        // Get the list of files this node is supposed to process. This was
        // saved by the plan() method in the plancontext
        String myName = srvInterface.getCurrentNodeName();
        ParamReader params = planCtxt.getReader();
        String fileNames = params.getString("filesFor" + myName);

        // Note that you can also be lazy and directly grab the parameters
        // the user passed to the UDSource functon in the COPY statement directly
        // by getting parameters from the ServerInterface object. I.e.:

        //String fileNames = srvInterface.getParamReader().getString("file");

        // Split comma-separated list into a single list.
        String[] fileList = fileNames.split(",");
        for (int i = 0; i < fileList.length; i++){
            // Instantiate a FileSource object (which is a subclass of UDSource)
            // to read each file. The constructor for FileSource takes the
            // file name of the
            retVal.add(new FileSource(fileList[i]));
        }

        // Return the collection of FileSource objects. They will be called,
        // in turn, to read each of the files.
        return retVal;
    }

    // Declares which parameters that this factory accepts.
    @Override
    public void getParameterType(ServerInterface srvInterface,
                                    SizedColumnTypes parameterTypes) {
        parameterTypes.addVarchar(65000, "file");
        parameterTypes.addVarchar(65000, "nodes");
    }
}

5.10.2 - 用户自定义的筛选器

使用用户定义的筛选器,您可以通过多种方式处理从源获取的数据。例如,筛选器可以执行下列操作:

  • 处理其压缩格式不受 Vertica 原生支持的压缩文件。

  • 接收 UTF-16 编码数据并将其转码为 UTF-8 编码。

  • 对数据执行搜索和替换操作,然后再将数据加载到 Vertica 中。

您还可以通过多个筛选器处理数据,然后再将数据加载到 Vertica 中。例如,您可以解压缩使用 GZip 格式压缩的文件,然后将内容从 UTF-16 转换为 UTF-8,最后搜索并替换某些文本字符串。

如果您实施 UDFilter,还必须实施相应的 FilterFactory

有关 API 详细信息,请参阅 UDFilter 类FilterFactory 类

5.10.2.1 - UDFilter 类

UDFilter 类负责从源读取原始输入数据,以及使数据准备好加载到 Vertica 或由解析器处理。此准备可能涉及解压缩、重新编码或任何其他类型的二进制处理。

UDFilter 由 Vertica 群集中对数据源执行筛选的每个主机上相应的 FilterFactory 实例化。

UDFilter 方法

您的 UDFilter 子类必须覆盖 process()processWithMetadata()

  • process() 将原始输入流作为一个大文件读取。如果有任何错误或故障,整个加载将失败。
    上游源实施 processWithMetadata() 时可以实施 process(),但可能会导致解析错误。

  • processWithMetadata() 当数据源具有以某种结构化格式(与数据有效负载分开)提供的关于记录边界的元数据时很有用。使用此接口,源除了发出数据之外,还会为每个记录发出记录长度。

    通过在每个阶段实施 processWithMetadata() 而不是 process(),您可以在整个加载堆栈中保留此记录长度元数据,从而实施更有效的解析,以在每个消息的基础上而不是每个文件或每个源的基础上从错误中恢复。 KafkaSource 和 Kafka 解析器(KafkaAvroParserKafkaJSONParserKafkaParser)在单个 Kafka 消息损坏时使用这种机制来支持每个 Kafka 消息的拒绝。

    processWithMetadata()UDFilter 子类一起使用,您可以编写一个内部筛选器,该筛选器将源中的记录长度元数据集成到数据流中,生成带边界信息的单字节流以帮助解析器提取和处理单个消息。KafkaInsertDelimetersKafkaInsertLengths 使用这种机制将消息边界信息插入到 Kafka 数据流中。

或者,您可以覆盖其他 UDFilter 类方法。

筛选器执行

以下部分详细说明了每次调用用户定义的筛选器时的执行序列。以下示例覆盖 process() 方法。

正在设置
COPY 在第一次调用 process() 之前调用了 setup()。使用 setup() 可执行任何必要的设置步骤以使过滤器正常工作,例如,初始化数据结构以在过滤期间使用。您的对象可能在使用期间已损坏并重新创建,因此请确保您的对象可以重新启动。

过滤数据
COPY 将在查询执行期间重复调用 process() 以过滤数据。此方法将在其参数中收到 DataBuffer 类的两个实例以及一个输入缓冲区和一个输出缓冲区。您的实施应从输入缓冲区读取数据,并以某种方式(例如解压缩)处理数据,然后将结果写入到输出缓冲区。您的实施所读取的字节数和所写入的字节数之间可能不存在一对一关联。process() 方法应处理数据,直至没有更多数据可读取或输出缓冲区中的空间已用完。当出现这两种情况之一时,该方法应返回以下由 StreamState 定义的值之一:

  • OUTPUT_NEEDED,如果过滤器的输出缓冲区需要更多空间。

  • INPUT_NEEDED,如果过滤器已用完输入数据(但尚未完全处理数据源)。

  • DONE,如果过滤器已处理数据源中的所有数据。

  • KEEP_GOING,如果过滤器在很长一段时间内无法继续执行操作。系统会再次调用该方法,因此请勿无限期阻止该方法,否则会导致用户无法取消查询。

返回之前,process() 方法必须在每个 DataBuffer 中设置 offset 属性。在输入缓冲区中,请将该属性设置为方法成功读取的字节数。在输出缓冲区中,请将该属性设置为方法已写入的字节数。通过设置这些属性,对 process() 发出的下一次调用可以在缓冲区中的正确位置继续读取和写入数据。

process() 方法还需要检查传递给它的 InputState 对象,以确定数据源中是否存在更多数据。如果此对象等于 END_OF_FILE,则输入数据中剩余的数据是数据源中的最后数据。处理完所有剩余的数据后,process() 必须返回 DONE。

分解
COPY 将在最后一次调用 process() 之后调用 destroy()。此方法可释放由 setup()process() 方法预留的任何资源。Vertica 将在 process() 方法指示已完成对数据流中所有数据的筛选之后调用此方法。

如果仍有数据源尚未处理,Vertica 可能会在稍后再次对该对象调用 setup()。在后续发出调用时,Vertica 会指示该方法筛选新的数据流中的数据。因此,destroy() 方法应使 UDFilter 子类的对象处于某种状态,以便 setup() 方法可为重复使用该对象做好准备。

API

UDFilter API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface);

virtual bool useSideChannel();

virtual StreamState process(ServerInterface &srvInterface, DataBuffer &input, InputState input_state, DataBuffer &output)=0;

virtual StreamState processWithMetadata(ServerInterface &srvInterface, DataBuffer &input,
    LengthBuffer &input_lengths, InputState input_state, DataBuffer &output, LengthBuffer &output_lengths)=0;

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface);

UDFilter API 提供了以下通过子类扩展的方法:

public void setup(ServerInterface srvInterface) throws UdfException;

public abstract StreamState process(ServerInterface srvInterface, DataBuffer input,
                InputState input_state, DataBuffer output)
    throws UdfException;

protected void cancel(ServerInterface srvInterface);

public void destroy(ServerInterface srvInterface) throws UdfException;

UDFilter API 提供了以下通过子类扩展的方法:

class PyUDFilter(vertica_sdk.UDFilter):
    def __init__(self):
        pass

    def setup(self, srvInterface):
        pass

    def process(self, srvInterface, inputbuffer, outputbuffer, inputstate):
        # User process data here, and put into outputbuffer.
        return StreamState.DONE

5.10.2.2 - FilterFactory 类

如果编写过滤器,您还必须编写用于生成过滤器实例的过滤器工厂。为此,请对 FilterFactory 类设置子类。

子类将执行函数执行的初始验证和规划工作,并将每个主机(将进行数据过滤)上的 UDFilter 对象实例化。

过滤器工厂是单例。您的子类必须为无状态子类,没有包含数据的字段。该子类还不得修改任何全局变量。

FilterFactory 方法

FilterFactory 类定义了以下方法。您的子类必须覆盖 prepare() 方法。可以覆盖其他方法。

设置

Vertica 将在启动程序节点上调用 plan() 一次,以执行下列任务:

  • 检查已从 COPY 语句中的函数调用传递的任何参数和错误消息(如果出现任何问题)。您通过从传递至 plan() 方法的 ServerInterface 的实例中获得 ParamReader 对象读取参数。

  • 存储各个主机所需的任何信息,以便过滤作为参数传递的 PlanContext 实例中的数据。例如,您可以存储过滤器将读取的输入格式的详细信息,并输出该过滤器应生成的格式。plan() 方法仅在启动程序节点上运行,而 prepare() 方法则在每个读取数据源的主机上运行。因此,该对象是它们之间的唯一通信方式。

    您通过从 getWriter() 方法中获取 ParamWriter 对象在 PlanContext 中存储数据。然后,通过对 ParamWriter 调用方法(比如 setString)写入参数。

创建筛选器

Vertica 将调用 prepare(),以创建并初始化筛选器。它在将执行过滤的每个节点上调用此方法一次。Vertica 根据可用资源自动选择可完成该工作的最佳节点。您无法指定在哪些节点上完成工作。

定义参数

实施 getParameterTypes() 可定义过滤器所使用的参数的名称和类型。Vertica 使用此信息对调用人员发出有关未知或缺失参数的警告。Vertica 会忽略未知参数并为缺失参数使用默认值。当您需要为函数定义类型和参数时,您不需要覆盖此方法。

API

FilterFactory API 提供了以下通过子类扩展的方法:

virtual void plan(ServerInterface &srvInterface, PlanContext &planCtxt);

virtual UDFilter * prepare(ServerInterface &srvInterface, PlanContext &planCtxt)=0;

virtual void getParameterType(ServerInterface &srvInterface, SizedColumnTypes &parameterTypes);

创建 FilterFactory 之后,您必须将其注册到 RegisterFactory 宏。

FilterFactory API 提供了以下通过子类扩展的方法:

public void plan(ServerInterface srvInterface, PlanContext planCtxt)
    throws UdfException;

public abstract UDFilter prepare(ServerInterface srvInterface, PlanContext planCtxt)
    throws UdfException;

public void getParameterType(ServerInterface srvInterface, SizedColumnTypes parameterTypes);

FilterFactory API 提供了以下通过子类扩展的方法:


class PyFilterFactory(vertica_sdk.SourceFactory):
    def __init__(self):
        pass
    def plan(self):
        pass
    def prepare(self, planContext):
        #User implement the function to create PyUDSources.
        pass

5.10.2.3 - Java 示例: ReplaceCharFilter

本节中的示例演示了创建一个 UDFilter,以用于在输出流中将输入流中某个字符的任何实例替换为另一个字符。此示例是高度简化的示例,并假设输入流为 ASCII 数据。

始终应记住的是,UDFilter 中的输入流和输出流实际上是二进制数据。如果要使用 UDFilter 执行字符转换,请将数据流从字节字符串转换为正确编码的字符串。例如,输入流可能包含 UTF-8 编码文本。如果是的话,务必先将要从缓冲区中读取的原始二进制数据转换为 UTF 字符串,然后再进行处理。

加载和使用示例

示例 UDFilter 具有两个必需参数。from_char 参数指定了要替换的字符,而 to_char 参数指定了替换字符。按如下所示加载并使用 ReplaceCharFilter UDFilter:

=> CREATE LIBRARY JavaLib AS '/home/dbadmin/JavaUDlLib.jar'
->LANGUAGE 'JAVA';
CREATE LIBRARY
=> CREATE FILTER ReplaceCharFilter as LANGUAGE 'JAVA'
->name 'com.mycompany.UDL.ReplaceCharFilterFactory' library JavaLib;
CREATE FILTER FUNCTION
=> CREATE TABLE t (text VARCHAR);
CREATE TABLE
=> COPY t FROM STDIN WITH FILTER ReplaceCharFilter(from_char='a', to_char='z');
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> Mary had a little lamb
>> a man, a plan, a canal, Panama
>> \.

=> SELECT * FROM t;
              text
--------------------------------
 Mzry hzd z little lzmb
 z mzn, z plzn, z cznzl, Pznzmz
(2 rows)

=> --Calling the filter with incorrect parameters returns errors
=> COPY t from stdin with filter ReplaceCharFilter();
ERROR 3399:  Failure in UDx RPC call InvokePlanUDL(): Error in User Defined Object [
ReplaceCharFilter], error code: 0
com.vertica.sdk.UdfException: You must supply two parameters to ReplaceChar: 'from_char' and 'to_char'
        at com.vertica.JavaLibs.ReplaceCharFilterFactory.plan(ReplaceCharFilterFactory.java:22)
        at com.vertica.udxfence.UDxExecContext.planUDFilter(UDxExecContext.java:889)
        at com.vertica.udxfence.UDxExecContext.planCurrentUDLType(UDxExecContext.java:865)
        at com.vertica.udxfence.UDxExecContext.planUDL(UDxExecContext.java:821)
        at com.vertica.udxfence.UDxExecContext.run(UDxExecContext.java:242)
        at java.lang.Thread.run(Thread.java:662)

解析器实施

ReplaceCharFilter 类将读取数据流,并将用户指定字符的每个实例替换为另一个字符。

package com.vertica.JavaLibs;

import com.vertica.sdk.DataBuffer;
import com.vertica.sdk.ServerInterface;
import com.vertica.sdk.State.InputState;
import com.vertica.sdk.State.StreamState;
import com.vertica.sdk.UDFilter;

public class ReplaceCharFilter extends UDFilter {

    private byte[] fromChar;
    private byte[] toChar;

    public ReplaceCharFilter(String fromChar, String toChar){
        // Stores the from char and to char as byte arrays. This is
        // not a robust method of doing this, but works for this simple
        // example.
        this.fromChar= fromChar.getBytes();
        this.toChar=toChar.getBytes();
    }
    @Override
    public StreamState process(ServerInterface srvInterface, DataBuffer input,
            InputState input_state, DataBuffer output) {

        // Check if there is no more input and the input buffer has been completely
        // processed. If so, filtering is done.
        if (input_state == InputState.END_OF_FILE && input.buf.length == 0) {
            return StreamState.DONE;
        }

        // Get current position in the input buffer
        int offset = output.offset;

        // Determine how many bytes to process. This is either until input
        // buffer is exhausted or output buffer is filled
        int limit = Math.min((input.buf.length - input.offset),
                (output.buf.length - output.offset));

        for (int i = input.offset; i < limit; i++) {
            // This example just replaces each instance of from_char
            // with to_char. It does not consider things such as multi-byte
            // UTF-8 characters.
            if (input.buf[i] == fromChar[0]) {
                output.buf[i+offset] = toChar[0];
            } else {
                // Did not find from_char, so copy input to the output
                output.buf[i+offset]=input.buf[i];
            }
        }

        input.offset += limit;
        output.offset += input.offset;

        if (input.buf.length - input.offset < output.buf.length - output.offset) {
            return StreamState.INPUT_NEEDED;
        } else {
            return StreamState.OUTPUT_NEEDED;
        }
    }
}

工厂实施

ReplaceCharFilterFactory 需要两个参数(from_charto_char)。plan() 方法可验证这些参数是否存在以及是否为单字符字符串。然后,此方法会将它们存储在计划上下文中。prepare() 方法可获取参数值并将参数值传递到 ReplaceCharFilter 对象,然后将这些对话实例化以执行过滤。

package com.vertica.JavaLibs;

import java.util.ArrayList;
import java.util.Vector;

import com.vertica.sdk.FilterFactory;
import com.vertica.sdk.PlanContext;
import com.vertica.sdk.ServerInterface;
import com.vertica.sdk.SizedColumnTypes;
import com.vertica.sdk.UDFilter;
import com.vertica.sdk.UdfException;

public class ReplaceCharFilterFactory extends FilterFactory {

    // Run on the initiator node to perform varification and basic setup.
    @Override
    public void plan(ServerInterface srvInterface,PlanContext planCtxt)
                     throws UdfException {
        ArrayList<String> args =
                          srvInterface.getParamReader().getParamNames();

        // Ensure user supplied two arguments
        if (!(args.contains("from_char") && args.contains("to_char"))) {
            throw new UdfException(0, "You must supply two parameters" +
                        " to ReplaceChar: 'from_char' and 'to_char'");
        }

        // Verify that the from_char is a single character.
        String fromChar = srvInterface.getParamReader().getString("from_char");
        if (fromChar.length() != 1) {
            String message =  String.format("Replacechar expects a single " +
                "character in the 'from_char' parameter. Got length %d",
                fromChar.length());
            throw new UdfException(0, message);
        }

        // Save the from character in the plan context, to be read by
        // prepare() method.
        planCtxt.getWriter().setString("fromChar",fromChar);

        // Ensure to character parameter is a single characater
        String toChar = srvInterface.getParamReader().getString("to_char");
        if (toChar.length() != 1) {
            String message =  String.format("Replacechar expects a single "
                 + "character in the 'to_char' parameter. Got length %d",
                toChar.length());
            throw new UdfException(0, message);
        }
        // Save the to character in the plan data
        planCtxt.getWriter().setString("toChar",toChar);
    }

    // Called on every host that will filter data. Must instantiate the
    // UDFilter subclass.
    @Override
    public UDFilter prepare(ServerInterface srvInterface, PlanContext planCtxt)
                            throws UdfException {
        // Get data stored in the context by the plan() method.
        String fromChar = planCtxt.getWriter().getString("fromChar");
        String toChar = planCtxt.getWriter().getString("toChar");

        // Instantiate a filter object to perform filtering.
        return new ReplaceCharFilter(fromChar, toChar);
    }

    // Describe the parameters accepted by this filter.
    @Override
    public void getParameterType(ServerInterface srvInterface,
             SizedColumnTypes parameterTypes) {
        parameterTypes.addVarchar(1, "from_char");
        parameterTypes.addVarchar(1, "to_char");
    }
}

5.10.2.4 - C++ 示例:转换编码

以下示例显示了如何通过将 UTF-16 编码数据转换为 UTF-8 来将文件的编码从一种类型转换为另一种类型。您可以在 SDK 的 /opt/vertica/sdk/examples/FilterFunctions/IConverter.cpp 目录中找到此示例。

筛选器实施

class Iconverter : public UDFilter{
private:
    std::string fromEncoding, toEncoding;
    iconv_t cd; // the conversion descriptor opened
    uint converted; // how many characters have been converted
protected:
    virtual StreamState process(ServerInterface &srvInterface, DataBuffer &input,
                                InputState input_state, DataBuffer &output)
    {
        char *input_buf = (char *)input.buf + input.offset;
        char *output_buf = (char *)output.buf + output.offset;
        size_t inBytesLeft = input.size - input.offset, outBytesLeft = output.size - output.offset;
        // end of input
        if (input_state == END_OF_FILE && inBytesLeft == 0)
        {
            // Gnu libc iconv doc says, it is good practice to finalize the
            // outbuffer for stateful encodings (by calling with null inbuffer).
            //
            // http://www.gnu.org/software/libc/manual/html_node/Generic-Conversion-Interface.html
            iconv(cd, NULL, NULL, &output_buf, &outBytesLeft);
            // output buffer can be updated by this operation
            output.offset = output.size - outBytesLeft;
            return DONE;
        }
        size_t ret = iconv(cd, &input_buf, &inBytesLeft, &output_buf, &outBytesLeft);
        // if conversion is successful, we ask for more input, as input has not reached EOF.
        StreamState retStatus = INPUT_NEEDED;
        if (ret == (size_t)(-1))
        {
            // seen an error
            switch (errno)
            {
            case E2BIG:
                // input size too big, not a problem, ask for more output.
                retStatus = OUTPUT_NEEDED;
                break;
            case EINVAL:
                // input stops in the middle of a byte sequence, not a problem, ask for more input
                retStatus = input_state == END_OF_FILE ? DONE : INPUT_NEEDED;
                break;
            case EILSEQ:
                // invalid sequence seen, throw
                // TODO: reporting the wrong byte position
                vt_report_error(1, "Invalid byte sequence when doing %u-th conversion", converted);
            case EBADF:
                // something wrong with descriptor, throw
                vt_report_error(0, "Invalid descriptor");
            default:
                vt_report_error(0, "Uncommon Error");
                break;
            }
        }
        else converted += ret;
        // move position pointer
        input.offset = input.size - inBytesLeft;
        output.offset = output.size - outBytesLeft;
        return retStatus;
    }
public:
    Iconverter(const std::string &from, const std::string &to)
    : fromEncoding(from), toEncoding(to), converted(0)
    {
        // note "to encoding" is first argument to iconv...
        cd = iconv_open(to.c_str(), from.c_str());
        if (cd == (iconv_t)(-1))
        {
            // error when creating converters.
            vt_report_error(0, "Error initializing iconv: %m");
        }
    }
    ~Iconverter()
    {
        // free iconv resources;
        iconv_close(cd);
    }
};

工厂实施

class IconverterFactory : public FilterFactory{
public:
    virtual void plan(ServerInterface &srvInterface,
            PlanContext &planCtxt) {
        std::vector<std::string> args = srvInterface.getParamReader().getParamNames();
        /* Check parameters */
        if (!(args.size() == 0 ||
                (args.size() == 1 && find(args.begin(), args.end(), "from_encoding")
                        != args.end()) || (args.size() == 2
                        && find(args.begin(), args.end(), "from_encoding") != args.end()
                        && find(args.begin(), args.end(), "to_encoding") != args.end()))) {
            vt_report_error(0, "Invalid arguments.  Must specify either no arguments,  or "
                               "'from_encoding' alone, or 'from_encoding' and 'to_encoding'.");
        }
        /* Populate planData */
        // By default, we do UTF16->UTF8, and x->UTF8
        VString from_encoding = planCtxt.getWriter().getStringRef("from_encoding");
        VString to_encoding = planCtxt.getWriter().getStringRef("to_encoding");
        from_encoding.copy("UTF-16");
        to_encoding.copy("UTF-8");
        if (args.size() == 2)
        {
            from_encoding.copy(srvInterface.getParamReader().getStringRef("from_encoding"));
            to_encoding.copy(srvInterface.getParamReader().getStringRef("to_encoding"));
        }
        else if (args.size() == 1)
        {
            from_encoding.copy(srvInterface.getParamReader().getStringRef("from_encoding"));
        }
        if (!from_encoding.length()) {
            vt_report_error(0, "The empty string is not a valid from_encoding value");
        }
        if (!to_encoding.length()) {
            vt_report_error(0, "The empty string is not a valid to_encoding value");
        }
    }
    virtual UDFilter* prepare(ServerInterface &srvInterface,
            PlanContext &planCtxt) {
        return vt_createFuncObj(srvInterface.allocator, Iconverter,
                planCtxt.getReader().getStringRef("from_encoding").str(),
                planCtxt.getReader().getStringRef("to_encoding").str());
    }
    virtual void getParameterType(ServerInterface &srvInterface,
                                  SizedColumnTypes &parameterTypes) {
        parameterTypes.addVarchar(32, "from_encoding");
        parameterTypes.addVarchar(32, "to_encoding");
    }
};
RegisterFactory(IconverterFactory);

5.10.3 - 用户定义的解析器

解析器将接收字节流,并将相应的元组序列传递到 Vertica 加载进程。您可以使用用户定义的解析器函数解析以下数据:

  • Vertica 内置解析器无法理解其格式的数据。

  • 所需的控制比内置解析器提供的控制更精确的数据。

例如,您可以使用特定的 CSV 库加载 CSV 文件。请参阅 Vertica SDK 查看两个 CSV 示例。

COPY 支持单个用户定义的解析器,您可以将其与用户定义的源以及零个或多个用户定义的筛选器实例一起使用。如果您实施 UDParser 类,还必须实施相应的 ParserFactory

有时可以通过添加块分割器来提高解析器的性能。块分割器可拆分输入并使用多个线程来解析输入。块分割器仅在 C++ API 中可用。有关详细信息,请参阅“协作解析”和“UDChunker 类”。在某些特殊情况下,您可以通过使用*分摊加载*进一步提高性能,当使用此方法时,输入由多个 Vertica 节点进行解析。

5.10.3.1 - UDParser 类

如果需要解析 COPY 语句的原生解析器无法处理的数据格式,您可以将 UDParser 类子类化。

在解析器执行期间,Vertica 会始终调用三个方法:setup()process()destroy()。它还可能调用 getRejectedRecord()

UDParser 构造函数

UDParser 类可执行对所有子类来说必不可少的初始化,包括初始化解析器使用的 StreamWriter 对象。因此,构造函数必须调用 super()

UDParser 方法

您的 UDParser 子类必须覆盖 process()processWithMetadata()

  • process() 将原始输入流作为一个大文件读取。如果有任何错误或故障,整个加载将失败。您可以实施具有源或筛选器(可实施 processWithMetadata())的 process(),但这可能会导致解析错误。
    您可以在上游源或筛选器实施 processWithMetadata() 时实施 process(),但可能会导致解析错误。

  • processWithMetadata() 当数据源具有以某种结构化格式(与数据有效负载分开)提供的关于记录边界的元数据时很有用。使用此接口,源除了发出数据之外,还会为每个记录发出记录长度。

    通过在每个阶段实施 processWithMetadata() 而不是 process(),您可以在整个加载堆栈中保留此记录长度元数据,从而实施更有效的解析,以在每个消息的基础上而不是每个文件或每个源的基础上从错误中恢复。 KafkaSource 和 Kafka 解析器(KafkaAvroParserKafkaJSONParserKafkaParser)会在各条 Kafka 消息损坏时使用这种机制为拒绝每条 Kafka 消息提供支持。

此外,您必须覆盖 getRejectedRecord() 以返回有关被拒绝记录的信息。

或者,您可以覆盖其他 UDParser 类方法。

解析器执行

以下各节详细说明了每次调用用户定义的解析器时的执行序列。以下示例覆盖 process() 方法。

设置

COPY 在第一次调用 process() 之前调用了 setup()。使用 setup() 可执行任何初始设置任务,以使解析器能够解析数据。此设置包括从类上下文结构检索参数,或初始化数据结构以在过滤期间使用。在第一次调用 process() 方法之前,Vertica 会先调用此方法。您的对象可能在使用期间已损坏并重新创建,因此请确保您的对象可以重新启动。

解析

COPY 将在查询执行期间重复调用 process()。Vertica 会向此方法传递数据缓冲区以解析为列和行以及由 InputState 定义的以下输入状态之一:

  • OK:当前在流的开头或中间

  • END_OF_FILE:无更多数据可用。

  • END_OF_CHUNK:当前数据在记录边界处结束且解析器应在返回之前用完所有数据。此输入状态仅在使用块分割器时出现。

  • START_OF_PORTION:输入的起始位置不是源的开头。解析器应查找第一个记录结束标记。此输入状态仅在使用分摊加载时出现。您可以使用 getPortion() 方法访问相应部分的偏移量和大小。

  • END_OF_PORTION:源已到达其部分的结尾。解析器应完成处理它开始的最后一条记录且不再前进。此输入状态仅在使用分摊加载时出现。

解析器必须拒绝其无法解析的任何数据,以便 Vertica 可以报告拒绝并将拒绝的数据写入到文件中。

process() 方法必须从输入缓冲区解析尽可能多的数据。输入缓冲区可能不在行边界结束。因此,该方法可能不得不在输入行的中间停止解析并请求更多数据。如果源文件包含 null 字节,则输入可以包含 null 字节,而且不会自动以 null 终止。

解析器具有关联的 StreamWriter 对象,数据写入实际上由此对象执行。当解析器提取列值后,它会对 StreamWriter 使用特定于类型的方法之一以将该值写入到输出流。有关这些方法的详细信息,请参阅写入数据

process() 的一次调用可能会写入多行数据。当解析器结束对数据行的处理后,它必须对 StreamWriter 调用 next() 以使输出流前进到新行。(通常,解析器会完成处理一行,因为它遇到了行尾标记。)

process() 方法到达缓冲区结尾时,它会通过返回由 StreamState 定义的以下值之一来告知 Vertica 其当前状态:

  • INPUT_NEEDED:解析器已到达缓冲区的结尾且需要获取更多可供解析的数据。

  • DONE:解析器已到达输入数据流的结尾。

  • REJECT:解析器已拒绝所读取的最后一行数据(请参阅拒绝行)。

分解

COPY 将在最后一次调用 process() 之后调用 destroy()。此方法可释放由 setup()process() 方法预留的任何资源。

process() 方法指示它已完成对数据源的解析之后,Vertica 将调用此方法。但是,有时可能会剩余尚未处理的数据源。在这种情况下,Vertica 可能会在稍后再次对该对象调用 setup(),并让此方法解析新的数据流中的数据。因此,请编写 destroy() 方法,以使 UDParser 子类的实例处于某种状态,从而可以安全地再次调用 setup()

报告拒绝

如果 process() 拒绝某行,Vertica 将调用 getRejectedRecord() 以报告该拒绝。通常,此方法将返回 RejectedRecord 类的实例和拒绝的行的详细信息。

写入数据

解析器具有关联的 StreamWriter 对象,您可以通过调用 getStreamWriter() 来访问此对象。在实施 process() 时,对 StreamWriter 对象使用 setType() 方法可将行中的值写入到特定的列索引。请验证写入的数据类型是否与架构所需的数据类型匹配。

以下示例显示了如何将类型为 long 的值写入到当前行中的第四列(索引 3):

StreamWriter writer = getStreamWriter();
...
writer.setLongValue(3, 98.6);

StreamWriter 为所有基本类型提供多种方法,例如 setBooleanValue()setStringValue() 等。有关 StreamWriter 方法的完整列表,包括使用基元类型的选项或将条目显式设置为 null 的选项,请参阅 API 文档。

拒绝行

如果解析器发现无法解析的数据,它应按照以下过程拒绝该行:

  1. 保存有关拒绝的行数据的详细信息和拒绝原因。这些信息片段可以直接存储在 RejectedRecord 对象中,或者也可以在需要使用之前将其存储在 UDParser 子类上的字段中。

  2. 通过更新 input.offset 来更新该行在输入缓冲区中的位置,以便可以从下一行继续解析。

  3. 通过返回值 StreamState.REJECT 来指示它已拒绝某个行。

  4. 返回 RejectedRecord 类的实例和有关拒绝的行的详细信息。

拆分大型负载

Vertica 提供了两种拆分大型负载的方法。 分摊加载 允许您在多个数据库节点之间分配负载。 协作解析 (仅限 C++)允许您在一个节点上的多个线程之间分配负载。

API

UDParser API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface, SizedColumnTypes &returnType);

virtual bool useSideChannel();

virtual StreamState process(ServerInterface &srvInterface, DataBuffer &input, InputState input_state)=0;

virtual StreamState processWithMetadata(ServerInterface &srvInterface, DataBuffer &input,
                            LengthBuffer &input_lengths, InputState input_state)=0;

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface, SizedColumnTypes &returnType);

virtual RejectedRecord getRejectedRecord();

UDParser API 提供了以下通过子类扩展的方法:

public void setup(ServerInterface srvInterface, SizedColumnTypes returnType)
    throws UdfException;

public abstract StreamState process(ServerInterface srvInterface,
                DataBuffer input, InputState input_state)
    throws UdfException, DestroyInvocation;

protected void cancel(ServerInterface srvInterface);

public void destroy(ServerInterface srvInterface, SizedColumnTypes returnType)
    throws UdfException;

public RejectedRecord getRejectedRecord() throws UdfException;

UDParser 使用 StreamWriter 写入其输出。 StreamWriter 为所有基本类型提供多种方法,例如 setBooleanValue()setStringValue() 等。在 Java API 中,该类还提供可自动设置数据类型的 setValue() 方法。

前述方法可写入单个列值。 StreamWriter 还提供用于从映射写入完整行的方法。setRowFromMap() 方法使用列名称和值的映射,并将所有值写入到相应的列。此方法不定义新列,而是仅将值写入到现有列。JsonParser 示例使用此方法写入任意 JSON 输入。(请参阅Java 示例:JSON 解析器。)

setRowsFromMap() 还会使用提供的完整映射填充 Flex 表(请参阅 Flex 表)的任何 VMap ('raw') 列。在大多数情况下,setRowsFromMap() 是用于填充弹性表的适当方法。但是,您也可以使用 setVMap()(类似于其他 setValue() 方法)在指定的列中生成 VMap 值。

setRowFromMap() 方法可自动强制将输入值转换为使用关联的 TypeCoercion 为这些列定义的类型。在大多数情况下,使用默认实施 (StandardTypeCoercion) 都是合适的。

TypeCoercion 使用策略来控制其行为。例如,FAIL_INVALID_INPUT_VALUE 策略指示将无效输入视为错误,而非使用 null 值。系统会捕获错误,并将其作为拒绝进行处理(请参阅用户定义的解析器中的“拒绝行”)。策略还能控制是否截断太长的输入。对解析器的 TypeCoercion 使用 setPolicy() 方法可设置策略。有关支持的值,请参阅 API 文档。

除了设置这些策略之外,您可能还需要自定义类型强制转换。要执行此操作,请子类化提供的 TypeCoercion 实施之一,并覆盖 asType() 方法。如果解析器将读取来自第三方库的对象,则有必要进行此自定义。例如,用于处理地理坐标的解析器可以覆盖 asLong,以将诸如“40.4397N”等输入转换为数字。有关实施的列表,请参阅 Vertica API 文档。

UDParser API 提供了以下通过子类扩展的方法:


class PyUDParser(vertica_sdk.UDSource):
    def __init__(self):
        pass
    def setup(srvInterface, returnType):
        pass
    def process(self, srvInterface, inputbuffer, inputstate, streamwriter):
        # User implement process function.
        # User reads data from inputbuffer and parse the data.
        # Rows are emitted via the streamwriter argument
        return StreamState.DONE

在 Python 中,process() 方法需要输入缓冲区和输出缓冲区(请参阅 InputBuffer API 和 OutputBuffer API)。输入缓冲区表示您要解析的信息源。输出缓冲区会将筛选后的信息传递给 Vertica。

如果筛选器拒绝记录,请使用方法 REJECT() 来识别被拒绝的数据和拒绝原因。

5.10.3.2 - UDChunker 类

您可以子类化 UDChunker 类以允许解析器支持 协作解析。此类仅在 C++ API 中可用。

从根本上说,UDChunker 是一个非常简单的解析器。与 UDParser 一样,它包含以下三个方法:setup()process()destroy()。您必须覆盖 process(),而且可以覆盖其他方法。此类包含一个额外的方法 alignPortion(),您必须在要为 UDChunker 启用 分摊加载时实施该方法。

设置和分解

UDParser 一样,您可以为块分割器定义初始化和清理代码。Vertica 会在第一次调用 process() 之前先调用 setup(),并在最后一次调用 process() 之后才调用 destroy()。您的对象可能会在多个加载源之间重用,因此请确保 setup() 会完全初始化所有字段。

分块

Vertica 会调用 process() 以将输入拆分为可以独立解析的块。该方法会采用输入缓冲区和输入状态指示器:

  • OK:输入缓冲区从流的开头或中间位置开始。

  • END_OF_FILE:无更多数据可用。

  • END_OF_PORTION:源已到达其部分的结尾。此状态仅在使用分摊加载时出现。

如果输入状态为 END_OF_FILE,则块分割器应将 input.offset 标记设置为 input.size 并返回 DONE。返回 INPUT_NEEDED 是错误的。

如果输入状态为 OK,则块分割器应从输入缓冲区读取数据并找到记录边界。如果它找到至少一条记录的结尾,它应将 input.offset 标记与缓冲区中最后一条记录结尾之后的字节对齐并返回 CHUNK_ALIGNED。例如,如果输入是“abc~def”并且“~”是记录终止符,则此方法应将 input.offset 设置为 4,即“d”的位置。如果 process() 在没有找到记录边界的情况下到达输入的结尾,则应返回 INPUT_NEEDED

您可以将输入拆分成更小的块,但使用输入中的所有可用记录可以提高性能。例如,块分割器可以从输入的结尾向后扫描以找到记录终止符(它可能是输入中诸多记录的最后一条),并将其作为一个块全部返回,而不扫描剩余输入。

如果输入状态为 END_OF_PORTION,则块分割器的行为应与输入状态 OK 的行为相同,只不过它还应设置一个标记。再次调用时,它应在下一部分中找到第一条记录,并将块与该记录对齐。

输入数据可能包含 null 字节(如果源文件包含这种字节)。输入实参不会自动以 null 值终止。

process() 方法不得被无限期阻止。如果此方法在很长一段时间内无法继续执行,它应返回 KEEP_GOING。未能返回 KEEP_GOING 将导致多种后果,例如导致用户无法取消查询。

有关使用分块的 process() 方法的示例,请参阅 C++ 示例:分隔解析器和块分割器

对齐部分

如果块分割器支持分摊加载,请实施 alignPortion() 方法。在调用 process() 之前,Vertica 会调用此方法一次或多次,以将输入偏移量与该部分中第一个完整块的开头对齐。该方法会采用输入缓冲区和输入状态指示器:

  • START_OF_PORTION:缓冲区的开头对应于该部分的开头。可以使用 getPortion() 方法来访问该部分的偏移量和大小。

  • OK:输入缓冲区在相应部分的中间。

  • END_OF_PORTION:缓冲区的结尾对应于相应部分的结尾或超出相应部分的结尾。

  • END_OF_FILE:无更多数据可用。

该方法应从缓冲区的开头扫描到第一个完整记录的开头。它应该将 input.offset 设置为此位置并返回以下值之一:

  • DONE(如果找到块)。 input.offset 为块的第一个字节。

  • INPUT_NEEDED(如果输入缓冲区不包含任何块的开头)。从 END_OF_FILE 的输入状态返回此值是错误的。

  • REJECT(如果相应部分而非缓冲区不包含任何块的开头)。

API

UDChunker API 提供了以下通过子类扩展的方法:

virtual void setup(ServerInterface &srvInterface,
            SizedColumnTypes &returnType);

virtual StreamState alignPortion(ServerInterface &srvInterface,
            DataBuffer &input, InputState state);

virtual StreamState process(ServerInterface &srvInterface,
            DataBuffer &input, InputState input_state)=0;

virtual void cancel(ServerInterface &srvInterface);

virtual void destroy(ServerInterface &srvInterface,
            SizedColumnTypes &returnType);

5.10.3.3 - ParserFactory 类

如果编写解析器,您还必须编写用于生成解析器实例的工厂。为此,请对 ParserFactory 类设置子类。

解析器工厂是单例。您的子类必须为无状态子类,没有包含数据的字段。子类还不得修改任何全局变量。

ParserFactory 类定义了以下方法。您的子类必须覆盖 prepare() 方法。可以覆盖其他方法。

设置

Vertica 将在启动程序节点上调用 plan() 一次,以执行下列任务:

  • 检查已从 COPY 语句中的函数调用传递的任何参数和错误消息(如果出现任何问题)。您通过从传递至 plan() 方法的 ServerInterface 的实例中获得 ParamReader 对象读取参数。

  • 存储各个主机解析数据所需的任何信息。例如,可以将参数存储在通过 planCtxt 参数传入的 PlanContext 实例中。plan() 方法仅在启动程序节点上运行,而 prepareUDSources() 方法则在每个读取数据源的主机上运行。因此,该对象是它们之间的唯一通信方式。

    您通过从 getWriter() 方法中获取 ParamWriter 对象在 PlanContext 中存储数据。然后,通过对 ParamWriter 调用方法(比如 setString)写入参数。

创建解析器

Vertica 将在每个节点上调用 prepare(),以使用由 plan() 方法存储的数据来创建并初始化解析器。

定义参数

实施 getParameterTypes() 可定义解析器所使用的参数的名称和类型。Vertica 使用此信息对调用人员发出有关未知或缺失参数的警告。Vertica 会忽略未知参数并为缺失参数使用默认值。当您需要为函数定义类型和参数时,您不需要覆盖此方法。

定义解析器输出

实施 getParserReturnType() 可定义解析器输出的表列的数据类型。如果适用,getParserReturnType() 还可以定义数据类型的大小、精度和小数位数。通常,此方法从 argTypeperColumnParamReader 参数读取输出表的数据类型,并验证是否能够输出相应的数据类型。如果 getParserReturnType() 已准备好输出数据类型,则它会对 returnType 参数中传递的 SizedColumnTypes 对象调用方法。除了输出列的数据类型之外,方法还应指定有关列的数据类型的任何附加信息:

  • 对于二进制和字符串数据类型(例如,CHAR、VARCHAR 和 LONG VARBINARY),请指定其最大长度。

  • 对于 NUMERIC 类型,请指定其精度和小数位数。

  • 对于 Time/Timestamp 类型(无论是否带有时区),请指定其精度(-1 表示未指定)。

  • 对于 ARRAY 类型,请指定元素的最大数量。

  • 对于所有其他类型,无需指定长度或精度。

支持协作解析

要支持协作解析,请实施 prepareChunker() 并返回 UDChunker 子类的实例。如果 isChunkerApportionable() 返回 true,则此方法返回 null 是错误的。

目前仅在 C++ API 中支持协作解析。

支持分摊加载

要支持分摊加载,解析器、块分割器或这两者都必须支持分摊。要指示解析器可以分摊加载,请实施 isParserApportionable() 并返回 true。要指示块分割器可以分摊加载,请实施 isChunkerApportionable() 并返回 true

isChunkerApportionable() 方法会将 ServerInterface 作为实参,因此您可以访问 COPY 语句中提供的参数。例如,如果用户可以指定记录分隔符,您可能需要此类信息。当且仅当工厂可以为此输入创建块分割器时,此方法将返回 true

API

ParserFactory API 提供了以下通过子类扩展的方法:

virtual void plan(ServerInterface &srvInterface, PerColumnParamReader &perColumnParamReader, PlanContext &planCtxt);

virtual UDParser * prepare(ServerInterface &srvInterface, PerColumnParamReader &perColumnParamReader,
            PlanContext &planCtxt, const SizedColumnTypes &returnType)=0;

virtual void getParameterType(ServerInterface &srvInterface, SizedColumnTypes &parameterTypes);

virtual void getParserReturnType(ServerInterface &srvInterface, PerColumnParamReader &perColumnParamReader,
            PlanContext &planCtxt, const SizedColumnTypes &argTypes,
            SizedColumnTypes &returnType);

virtual bool isParserApportionable();

// C++ API only:
virtual bool isChunkerApportionable(ServerInterface &srvInterface);

virtual UDChunker * prepareChunker(ServerInterface &srvInterface, PerColumnParamReader &perColumnParamReader,
            PlanContext &planCtxt, const SizedColumnTypes &returnType);

如果要使用 分摊加载 将单个输入拆分成多个加载流,请实施 isParserApportionable() 和/或 isChunkerApportionable() 并返回 true。即使这些方法返回 true,也不能保证 Vertica 分摊加载。然而,如果这两个方法均返回 false,则表示解析器不会尝试这么做。

如果要使用 协作解析,请实施 prepareChunker() 并返回 UDChunker 子类的实例。仅 C++ API 支持协作解析。

Vertica 将为非隔离功能调用 prepareChunker() 方法。在隔离模式下使用函数时,此方法不可用。

如果您希望块分割器可用于分摊加载,请实施 isChunkerApportionable() 并返回 true

创建 ParserFactory 之后,您必须将其注册到 RegisterFactory 宏。

ParserFactory API 提供了以下通过子类扩展的方法:

public void plan(ServerInterface srvInterface, PerColumnParamReader perColumnParamReader, PlanContext planCtxt)
    throws UdfException;

public abstract UDParser prepare(ServerInterface srvInterface, PerColumnParamReader perColumnParamReader,
                PlanContext planCtxt, SizedColumnTypes returnType)
    throws UdfException;

public void getParameterType(ServerInterface srvInterface, SizedColumnTypes parameterTypes);

public void getParserReturnType(ServerInterface srvInterface, PerColumnParamReader perColumnParamReader,
                PlanContext planCtxt, SizedColumnTypes argTypes, SizedColumnTypes returnType)
    throws UdfException;

ParserFactory API 提供了以下通过子类扩展的方法:

class PyParserFactory(vertica_sdk.SourceFactory):
    def __init__(self):
        pass
    def plan(self):
        pass
    def prepareUDSources(self, srvInterface):
        # User implement the function to create PyUDParser.
        pass

5.10.3.4 - C++ 示例: BasicIntegerParser

BasicIntegerParser 示例解析由非数字字符分隔的整数字符串。有关使用持续加载的此解析器的版本,请参阅 C++ 示例: ContinuousIntegerParser

加载和使用示例

按如下所示加载并使用 BasicIntegerParser 示例。

=> CREATE LIBRARY BasicIntegerParserLib AS '/home/dbadmin/BIP.so';

=> CREATE PARSER BasicIntegerParser AS
LANGUAGE 'C++' NAME 'BasicIntegerParserFactory' LIBRARY BasicIntegerParserLib;

=> CREATE TABLE t (i integer);

=> COPY t FROM stdin WITH PARSER BasicIntegerParser();
0
1
2
3
4
5
\.

实施

BasicIntegerParser 类仅实施 API 中的 process() 方法。(它还实施了一个用于类型转换的 helper 方法。)此方法处理每一行输入,查找每一行的数字。当它前进到新行时,它会移动 input.offset 标记并检查输入状态。然后它写入输出。

    virtual StreamState process(ServerInterface &srvInterface, DataBuffer &input,
                InputState input_state) {
        // WARNING: This implementation is not trying for efficiency.
        // It is trying for simplicity, for demonstration purposes.

        size_t start = input.offset;
        const size_t end = input.size;

        do {
            bool found_newline = false;
            size_t numEnd = start;
            for (; numEnd < end; numEnd++) {
                if (input.buf[numEnd] < '0' || input.buf[numEnd] > '9') {
                    found_newline = true;
                    break;
                }
            }

            if (!found_newline) {
                input.offset = start;
                if (input_state == END_OF_FILE) {
                    // If we're at end-of-file,
                    // emit the last integer (if any) and return DONE.
                    if (start != end) {
                        writer->setInt(0, strToInt(input.buf + start, input.buf + numEnd));
                        writer->next();
                    }
                    return DONE;
                } else {
                    // Otherwise, we need more data.
                    return INPUT_NEEDED;
                }
            }

            writer->setInt(0, strToInt(input.buf + start, input.buf + numEnd));
            writer->next();

            start = numEnd + 1;
        } while (true);
    }
};

在工厂中,plan() 方法是无操作的;没有要检查的参数。prepare() 方法使用 SDK 提供的宏实例化解析器:

    virtual UDParser* prepare(ServerInterface &srvInterface,
            PerColumnParamReader &perColumnParamReader,
            PlanContext &planCtxt,
            const SizedColumnTypes &returnType) {

        return vt_createFuncObject<BasicIntegerParser>(srvInterface.allocator);
    }

getParserReturnType() 方法声明了单个输出:

    virtual void getParserReturnType(ServerInterface &srvInterface,
            PerColumnParamReader &perColumnParamReader,
            PlanContext &planCtxt,
            const SizedColumnTypes &argTypes,
            SizedColumnTypes &returnType) {
        // We only and always have a single integer column
        returnType.addInt(argTypes.getColumnName(0));
    }

对于所有用 C++ 编写的 UDx,该示例以注册其工厂结束:

RegisterFactory(BasicIntegerParserFactory);

5.10.3.5 - C++ 示例: ContinuousIntegerParser

ContinuousIntegerParser 示例是 BasicIntegerParser 的变体。这两个示例都从输入字符串中解析整数。 ContinuousIntegerParser 使用 连续加载 读取数据。

加载和使用示例

按如下所示加载 ContinuousIntegerParser 示例。

=> CREATE LIBRARY ContinuousIntegerParserLib AS '/home/dbadmin/CIP.so';

=> CREATE PARSER ContinuousIntegerParser AS
LANGUAGE 'C++' NAME 'ContinuousIntegerParserFactory'
LIBRARY ContinuousIntegerParserLib;

以使用 BasicIntegerParser 的同样方式使用它。请参阅C++ 示例: BasicIntegerParser

实施

ContinuousIntegerParserContinuousUDParser 的子类。ContinuousUDParser 的子类将处理逻辑放在 run() 方法中。

    virtual void run() {

        // This parser assumes a single-column input, and
        // a stream of ASCII integers split by non-numeric characters.
        size_t pos = 0;
        size_t reserved = cr.reserve(pos+1);
        while (!cr.isEof() || reserved == pos + 1) {
            while (reserved == pos + 1 && isdigit(*ptr(pos))) {
                pos++;
                reserved = cr.reserve(pos + 1);
            }

            std::string st(ptr(), pos);
            writer->setInt(0, strToInt(st));
            writer->next();

            while (reserved == pos + 1 && !isdigit(*ptr(pos))) {
                pos++;
                reserved = cr.reserve(pos + 1);
            }
            cr.seek(pos);
            pos = 0;
            reserved = cr.reserve(pos + 1);
        }
    }
};

有关 ContinuousUDParser 的更复杂示例,请参阅示例中的 ExampleDelimitedParser。(请参阅下载并运行 UDx 示例代码。) ExampleDelimitedParser 使用块分割器;请参阅 C++ 示例:分隔解析器和块分割器

5.10.3.6 - Java 示例:数字文本

NumericTextParser 示例可解析以单词而非数字表示的整数值(例如,"one two three" 代表一百二十三)。解析器将执行下列操作:

  • 接受单个参数,以设置用来分隔数据行中各列的字符。分隔符默认设置为管道 (|) 字符。

  • 忽略额外空格和用于表示数字的单词的首字母大写。

  • 使用以下单词标识数字:zero、one、two、three、four、five、six、seven、eight 和 nine。

  • 假设表示整数的单词至少用一个空格分隔。

  • 拒绝无法完全解析为整数的任何数据行。

  • 在输出表包含非整数列时生成错误。

加载和使用示例

按如下所示加载并使用解析器:

=> CREATE LIBRARY JavaLib AS '/home/dbadmin/JavaLib.jar' LANGUAGE 'JAVA';
CREATE LIBRARY

=> CREATE PARSER NumericTextParser AS LANGUAGE 'java'
->    NAME 'com.myCompany.UDParser.NumericTextParserFactory'
->    LIBRARY JavaLib;
CREATE PARSER FUNCTION
=> CREATE TABLE t (i INTEGER);
CREATE TABLE
=> COPY t FROM STDIN WITH PARSER NumericTextParser();
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> One
>> Two
>> One Two Three
>> \.
=> SELECT * FROM t ORDER BY i;
  i
-----
   1
   2
 123
(3 rows)

=> DROP TABLE t;
DROP TABLE
=> -- Parse multi-column input
=> CREATE TABLE t (i INTEGER, j INTEGER);
CREATE TABLE
=> COPY t FROM stdin WITH PARSER NumericTextParser();
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> One | Two
>> Two | Three
>> One Two Three | four Five Six
>> \.
=> SELECT * FROM t ORDER BY i;
  i  |  j
-----+-----
   1 |   2
   2 |   3
 123 | 456
(3 rows)

=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> -- Use alternate separator character
=> COPY t FROM STDIN WITH PARSER NumericTextParser(separator='*');
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> Five * Six
>> seven * eight
>> nine * one zero
>> \.
=> SELECT * FROM t ORDER BY i;
 i | j
---+----
 5 |  6
 7 |  8
 9 | 10
(3 rows)

=> TRUNCATE TABLE t;
TRUNCATE TABLE

=> -- Rows containing data that does not parse into digits is rejected.
=> DROP TABLE t;
DROP TABLE
=> CREATE TABLE t (i INTEGER);
CREATE TABLE
=> COPY t FROM STDIN WITH PARSER NumericTextParser();
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself.
>> One Zero Zero
>> Two Zero Zero
>> Three Zed Zed
>> Four Zero Zero
>> Five Zed Zed
>> \.
SELECT * FROM t ORDER BY i;
  i
-----
 100
 200
 400
(3 rows)

=> -- Generate an error by trying to copy into a table with a non-integer column
=> DROP TABLE t;
DROP TABLE
=> CREATE TABLE t (i INTEGER, j VARCHAR);
CREATE TABLE
=> COPY t FROM STDIN WITH PARSER NumericTextParser();
vsql:UDParse.sql:94: ERROR 3399:  Failure in UDx RPC call
InvokeGetReturnTypeParser(): Error in User Defined Object [NumericTextParser],
error code: 0
com.vertica.sdk.UdfException: Column 2 of output table is not an Int
        at com.myCompany.UDParser.NumericTextParserFactory.getParserReturnType
        (NumericTextParserFactory.java:70)
        at com.vertica.udxfence.UDxExecContext.getReturnTypeParser(
        UDxExecContext.java:1464)
        at com.vertica.udxfence.UDxExecContext.getReturnTypeParser(
        UDxExecContext.java:768)
        at com.vertica.udxfence.UDxExecContext.run(UDxExecContext.java:236)
        at java.lang.Thread.run(Thread.java:662)

解析器实施

以下代码可实施解析器。

package com.myCompany.UDParser;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import com.vertica.sdk.DataBuffer;
import com.vertica.sdk.DestroyInvocation;
import com.vertica.sdk.RejectedRecord;
import com.vertica.sdk.ServerInterface;
import com.vertica.sdk.State.InputState;
import com.vertica.sdk.State.StreamState;
import com.vertica.sdk.StreamWriter;
import com.vertica.sdk.UDParser;
import com.vertica.sdk.UdfException;

public class NumericTextParser extends UDParser {

    private String separator; // Holds column separator character

    // List of strings that we accept as digits.
    private List<String> numbers = Arrays.asList("zero", "one",
            "two", "three", "four", "five", "six", "seven",
            "eight", "nine");

    // Hold information about the last rejected row.
    private String rejectedReason;
    private String rejectedRow;

    // Constructor gets the separator character from the Factory's prepare()
    // method.
    public NumericTextParser(String sepparam) {
        super();
        this.separator = sepparam;
    }

    // Called to perform the actual work of parsing. Gets a buffer of bytes
    // to turn into tuples.
    @Override
    public StreamState process(ServerInterface srvInterface, DataBuffer input,
            InputState input_state) throws UdfException, DestroyInvocation {

        int i=input.offset; // Current position in the input buffer
        // Flag to indicate whether we just found the end of a row.
        boolean lastCharNewline = false;
        // Buffer to hold the row of data being read.
        StringBuffer line = new StringBuffer();

        //Continue reading until end of buffer.
        for(; i < input.buf.length; i++){
            // Loop through input until we find a linebreak: marks end of row
            char inchar = (char) input.buf[i];
            // Note that this isn't a robust way to find rows. It should
            // accept a user-defined row separator. Also, the following
            // assumes ASCII line break metheods, which isn't a good idea
            // in the UTF world. But it is good enough for this simple example.
            if (inchar != '\n' && inchar != '\r') {
                // Keep adding to a line buffer until a full row of data is read
                line.append(inchar);
                lastCharNewline = false; // Last character not a new line
            } else {
                // Found a line break. Process the row.
                lastCharNewline = true; // indicate we got a complete row
                // Update the position in the input buffer. This is updated
                // whether the row is successfully processed or not.
                input.offset = i+1;
                // Call procesRow to extract values and write tuples to the
                // output. Returns false if there was an error.
                if (!processRow(line)) {
                    // Processing row failed. Save bad row to rejectedRow field
                    // and return to caller indicating a rejected row.
                    rejectedRow = line.toString();
                    // Update position where we processed the data.
                    return StreamState.REJECT;
                }
                line.delete(0, line.length()); // clear row buffer
            }
        }

        // At this point, process() has finished processing the input buffer.
        // There are two possibilities: need to get more data
        // from the input stream to finish processing, or there is
        // no more data to process. If at the end of the input stream and
        // the row was not terminated by a linefeed, it may need
        // to process the last row.

        if (input_state == InputState.END_OF_FILE && lastCharNewline) {
            // End of input and it ended on a newline. Nothing more to do
            return StreamState.DONE;
        } else if (input_state == InputState.END_OF_FILE && !lastCharNewline) {
            // At end of input stream but didn't get a final newline. Need to
            // process the final row that was read in, then exit for good.
            if (line.length() == 0) {
                // Nothing to process. Done parsing.
                return StreamState.DONE;
            }
            // Need to parse the last row, not terminated by a linefeed. This
            // can occur if the file being read didn't have a final line break.
            if (processRow(line)) {
                return StreamState.DONE;
            } else {
                // Processing last row failed. Save bad row to rejectedRow field
                // and return to caller indicating a rejected row.
                rejectedRow = line.toString();
                // Tell Vertica the entire buffer was processed so it won't
                // call again to have the line processed.
                input.offset = input.buf.length;
                return StreamState.REJECT;
            }
        } else {
            // Stream is not fully read, so tell Vertica to send more. If
            // process() didn't get a complete row before it hit the end of the
            // input buffer, it will end up re-processing that segment again
            // when more data is added to the buffer.
            return StreamState.INPUT_NEEDED;
        }
    }

    // Breaks a row into columns, then parses the content of the
    // columns. Returns false if there was an error parsing the
    // row, in which case it sets the rejected row to the input
    // line. Returns true if the row was successfully read.
    private boolean processRow(StringBuffer line)
                                throws UdfException, DestroyInvocation {
        String[] columns = line.toString().split(Pattern.quote(separator));
        // Loop through the columns, decoding their contents
        for (int col = 0; col < columns.length; col++) {
            // Call decodeColumn to extract value from this column
            Integer colval = decodeColumn(columns[col]);
            if (colval == null) {
                // Could not parse one of the columns. Indicate row should
                // be rejected.
                return false;
            }
            // Column parsed OK. Write it to the output. writer is a field
            // provided by the parent class. Since this parser only accepts
            // integers, there is no need to verify that data type of the parsed
            // data matches the data type of the column being written. In your
            // UDParsers, you may want to perform this verification.
            writer.setLong(col,colval);
        }
        // Done with the row of data. Advance output to next row.

        // Note that this example does not verify that all of the output columns
        // have values assigned to them. If there are missing values at the
        // end of a row, they get automatically get assigned a default value
        // (0 for integers). This isn't a robust solution. Your UDParser
        // should perform checks here to handle this situation and set values
        // (such as null) when appropriate.
        writer.next();
        return true; // Successfully processed the row.
    }

    // Gets a string with text numerals, i.e. "One Two Five Seven" and turns
    // it into an integer value, i.e. 1257. Returns null if the string could not
    // be parsed completely into numbers.
    private Integer decodeColumn(String text) {
        int value = 0; // Hold the value being parsed.

        // Split string into individual words. Eat extra spaces.
        String[] words = text.toLowerCase().trim().split("\\s+");

        // Loop through the words, matching them against the list of
        // digit strings.
        for (int i = 0; i < words.length; i++) {
            if (numbers.contains(words[i])) {
                // Matched a digit. Add the it to the value.
                int digit = numbers.indexOf(words[i]);
                value = (value * 10) + digit;
            } else {
                // The string didn't match one of the accepted string values
                // for digits. Need to reject the row. Set the rejected
                // reason string here so it can be incorporated into the
                // rejected reason object.
                //
                // Note that this example does not handle null column values.
                // In most cases, you want to differentiate between an
                // unparseable column value and a missing piece of input
                // data. This example just rejects the row if there is a missing
                // column value.
                rejectedReason = String.format(
                        "Could not parse '%s' into a digit",words[i]);
                return null;
            }
        }
        return value;
    }

    // Vertica calls this method if the parser rejected a row of data
    // to find out what went wrong and add to the proper logs. Just gathers
    // information stored in fields and returns it in an object.
    @Override
    public RejectedRecord getRejectedRecord() throws UdfException {
        return new RejectedRecord(rejectedReason,rejectedRow.toCharArray(),
                rejectedRow.length(), "\n");
    }
}

ParserFactory 实施

以下代码可实施解析器工厂。

NumericTextParser 接受名为 separator 的单个可选参数。您可以在 getParameterType() 方法中定义此参数,而 plan() 方法可存储此参数的值。 NumericTextParser 仅输出整数值。因此,如果输出表包含数据类型是非整数的列,getParserReturnType() 方法将抛出异常。

package com.myCompany.UDParser;

import java.util.regex.Pattern;

import com.vertica.sdk.ParamReader;
import com.vertica.sdk.ParamWriter;
import com.vertica.sdk.ParserFactory;
import com.vertica.sdk.PerColumnParamReader;
import com.vertica.sdk.PlanContext;
import com.vertica.sdk.ServerInterface;
import com.vertica.sdk.SizedColumnTypes;
import com.vertica.sdk.UDParser;
import com.vertica.sdk.UdfException;
import com.vertica.sdk.VerticaType;

public class NumericTextParserFactory extends ParserFactory {

    // Called once on the initiator host to check the parameters and set up the
    // context data that hosts performing processing will need later.
    @Override
    public void plan(ServerInterface srvInterface,
                PerColumnParamReader perColumnParamReader,
                PlanContext planCtxt) {

        String separator = "|"; // assume separator is pipe character

        // See if a parameter was given for column separator
        ParamReader args = srvInterface.getParamReader();
        if (args.containsParameter("separator")) {
            separator = args.getString("separator");
            if (separator.length() > 1) {
                throw new UdfException(0,
                        "Separator parameter must be a single character");
            }
            if (Pattern.quote(separator).matches("[a-zA-Z]")) {
                throw new UdfException(0,
                        "Separator parameter cannot be a letter");
            }
        }

        // Save separator character in the Plan Data
        ParamWriter context = planCtxt.getWriter();
        context.setString("separator", separator);
    }

    // Define the data types of the output table that the parser will return.
    // Mainly, this just ensures that all of the columns in the table which
    // is the target of the data load are integer.
    @Override
    public void getParserReturnType(ServerInterface srvInterface,
                PerColumnParamReader perColumnParamReader,
                PlanContext planCtxt,
                SizedColumnTypes argTypes,
                SizedColumnTypes returnType) {

        // Get access to the output table's columns
        for (int i = 0; i < argTypes.getColumnCount(); i++ ) {
            if (argTypes.getColumnType(i).isInt()) {
                // Column is integer... add it to the output
                 returnType.addInt(argTypes.getColumnName(i));
            } else {
                // Column isn't an int, so throw an exception.
                // Technically, not necessary since the
                // UDx framework will automatically error out when it sees a
                // Discrepancy between the type in the target table and the
                // types declared by this method. Throwing this exception will
                // provide a clearer error message to the user.
                String message = String.format(
                    "Column %d of output table is not an Int", i + 1);
                throw new UdfException(0, message);
            }
        }
    }

    // Instantiate the UDParser subclass named NumericTextParser. Passes the
    // separator characetr as a paramter to the constructor.
    @Override
    public UDParser prepare(ServerInterface srvInterface,
            PerColumnParamReader perColumnParamReader, PlanContext planCtxt,
            SizedColumnTypes returnType) throws UdfException {
        // Get the separator character from the context
        String separator = planCtxt.getReader().getString("separator");
        return new NumericTextParser(separator);
    }

    // Describe the parameters accepted by this parser.
    @Override
    public void getParameterType(ServerInterface srvInterface,
             SizedColumnTypes parameterTypes) {
        parameterTypes.addVarchar(1, "separator");
    }
}

5.10.3.7 - Java 示例:JSON 解析器

JSON 解析器会使用 JSON 对象流。每个对象都必须具有正确格式,并且必须位于输入中的一行上。可以使用换行符分隔对象。此解析器使用字段名称作为映射中的键,而这些键将成为表中的列名称。可以在 /opt/vertica/packages/flextable/examples 中找到此示例的代码。此目录还包含一个示例数据文件。

此示例使用 setRowFromMap() 方法写入数据。

加载和使用示例

使用第三方库 (gson-2.2.4.jar) 加载库并定义 JSON 解析器,如下所示。有关下载 URL,请参阅 JsonParser.java 中的注释:

=> CREATE LIBRARY json
-> AS '/opt/vertica/packages/flextable/examples/java/output/json.jar'
-> DEPENDS '/opt/vertica/bin/gson-2.2.4.jar' language 'java';
CREATE LIBRARY

=> CREATE PARSER JsonParser AS LANGUAGE 'java'
-> NAME 'com.vertica.flex.JsonParserFactory' LIBRARY json;
CREATE PARSER FUNCTION

您现在可以定义一个表,然后使用 JSON 解析器将数据加载到其中,如下所示:

=> CREATE TABLE mountains(name varchar(64), type varchar(32), height integer);
CREATE TABLE

=> COPY mountains FROM '/opt/vertica/packages/flextable/examples/mountains.json'
-> WITH PARSER JsonParser();
-[ RECORD 1 ]--
Rows Loaded | 2

=> SELECT * from mountains;
-[ RECORD 1 ]--------
name   | Everest
type   | mountain
height | 29029
-[ RECORD 2 ]--------
name   | Mt St Helens
type   | volcano
height |

该数据文件包含一个未加载的值 (hike_safety),因为表定义不包括该列。该数据文件遵循以下格式:

{ "name": "Everest", "type":"mountain", "height": 29029, "hike_safety": 34.1  }
{ "name": "Mt St Helens", "type": "volcano", "hike_safety": 15.4 }

实施

以下代码显示了 JsonParser.java 中的 process() 方法。解析器会尝试将输入读取到一个 Map. 中。如果读取成功,JSON 解析器将调用 setRowFromMap()

    @Override
    public StreamState process(ServerInterface srvInterface, DataBuffer input,
            InputState inputState) throws UdfException, DestroyInvocation {
        clearReject();
        StreamWriter output = getStreamWriter();

        while (input.offset < input.buf.length) {
            ByteBuffer lineBytes = consumeNextLine(input, inputState);

            if (lineBytes == null) {
                return StreamState.INPUT_NEEDED;
            }

            String lineString = StringUtils.newString(lineBytes);

            try {
                Map map = gson.fromJson(lineString, parseType);

                if (map == null) {
                    continue;
                }

                output.setRowFromMap(map);
                // No overrides needed, so just call next() here.
         output.next();
            } catch (Exception ex) {
                setReject(lineString, ex);
                return StreamState.REJECT;
            }
        }

JsonParserFactory.java 工厂会将解析器实例化并在 prepare() 方法中返回该解析器。您不需要执行其他设置。

5.10.3.8 - C++ 示例:分隔解析器和块分割器

ExampleDelimitedUDChunker 类在分隔符处划分输入。您可以将此块分割器与任何理解分隔输入的解析器一起使用。 ExampleDelimitedParser 是一个使用这个块分割器的 ContinuousUDParser 子类。

加载和使用示例

按如下所示加载并使用示例。

=> CREATE LIBRARY ExampleDelimitedParserLib AS '/home/dbadmin/EDP.so';

=> CREATE PARSER ExampleDelimitedParser AS
    LANGUAGE 'C++' NAME 'DelimitedParserFrameworkExampleFactory'
    LIBRARY ExampleDelimitedParserLib;

=> COPY t FROM stdin WITH PARSER ExampleDelimitedParser();
0
1
2
3
4
5
6
7
8
9
\.

块分割器实施

该块分割器支持分摊加载。alignPortion() 方法在当前部分找到第一个完整记录的开头,并将输入缓冲区与其对齐。记录终止符作为实参传递并在构造函数中设置。

StreamState ExampleDelimitedUDChunker::alignPortion(
            ServerInterface &srvInterface,
            DataBuffer &input, InputState state)
{
    /* find the first record terminator.  Its record belongs to the previous portion */
    void *buf = reinterpret_cast<void *>(input.buf + input.offset);
    void *term = memchr(buf, recordTerminator, input.size - input.offset);

    if (term) {
        /* record boundary found.  Align to the start of the next record */
        const size_t chunkSize = reinterpret_cast<size_t>(term) - reinterpret_cast<size_t>(buf);
        input.offset += chunkSize
            + sizeof(char) /* length of record terminator */;

        /* input.offset points at the start of the first complete record in the portion */
        return DONE;
    } else if (state == END_OF_FILE || state == END_OF_PORTION) {
        return REJECT;
    } else {
        VIAssert(state == START_OF_PORTION || state == OK);
        return INPUT_NEEDED;
    }
}

process() 方法必须考虑跨越部分边界的块。如果之前的调用在某个部分的末尾,则该方法设置一个标志。代码首先检查并处理该条件。逻辑与 alignPortion() 类似,所以示例调用它来做部分除法。

StreamState ExampleDelimitedUDChunker::process(
                ServerInterface &srvInterface,
                DataBuffer &input,
                InputState input_state)
{
    const size_t termLen = 1;
    const char *terminator = &recordTerminator;

    if (pastPortion) {
        /*
         * Previous state was END_OF_PORTION, and the last chunk we will produce
         * extends beyond the portion we started with, into the next portion.
         * To be consistent with alignPortion(), that means finding the first
         * record boundary, and setting the chunk to be at that boundary.
         * Fortunately, this logic is identical to aligning the portion (with
         * some slight accounting for END_OF_FILE)!
         */
        const StreamState findLastTerminator = alignPortion(srvInterface, input);

        switch (findLastTerminator) {
            case DONE:
                return DONE;
            case INPUT_NEEDED:
                if (input_state == END_OF_FILE) {
                    /* there is no more input where we might find a record terminator */
                    input.offset = input.size;
                    return DONE;
                }
                return INPUT_NEEDED;
            default:
                VIAssert("Invalid return state from alignPortion()");
        }
        return findLastTerminator;
    }

现在,该方法查找分隔符。如果输入从一个部分的末尾开始,它会设置标志。

    size_t ret = input.offset, term_index = 0;
    for (size_t index = input.offset; index < input.size; ++index) {
        const char c = input.buf[index];
        if (c == terminator[term_index]) {
            ++term_index;
            if (term_index == termLen) {
                ret = index + 1;
                term_index = 0;
            }
            continue;
        } else if (term_index > 0) {
            index -= term_index;
        }

        term_index = 0;
    }

    if (input_state == END_OF_PORTION) {
        /*
         * Regardless of whether or not a record was found, the next chunk will extend
         * into the next portion.
         */
        pastPortion = true;
    }

最后,process() 移动输入偏移量并返回。

    // if we were able to find some rows, move the offset to point at the start of the next (potential) row, or end of block
    if (ret > input.offset) {
        input.offset = ret;
        return CHUNK_ALIGNED;
    }

    if (input_state == END_OF_FILE) {
        input.offset = input.size;
        return DONE;
    }

    return INPUT_NEEDED;
}

工厂实施

文件 ExampleDelimitedParser.cpp 定义了一个使用此 UDChunker 的工厂。块分割器支持分摊加载,因此工厂实施 isChunkerApportionable()

    virtual bool isChunkerApportionable(ServerInterface &srvInterface) {
        ParamReader params = srvInterface.getParamReader();
        if (params.containsParameter("disable_chunker") && params.getBoolRef("d\
isable_chunker")) {
            return false;
        } else {
            return true;
        }
    }

prepareChunker() 方法创建块分割器:

    virtual UDChunker* prepareChunker(ServerInterface &srvInterface,
                                      PerColumnParamReader &perColumnParamReade\
r,
                                      PlanContext &planCtxt,
                                      const SizedColumnTypes &returnType)
    {
        ParamReader params = srvInterface.getParamReader();
        if (params.containsParameter("disable_chunker") && params.getBoolRef("d\
isable_chunker")) {
            return NULL;
        }

        std::string recordTerminator("\n");

        ParamReader args(srvInterface.getParamReader());
        if (args.containsParameter("record_terminator")) {
            recordTerminator = args.getStringRef("record_terminator").str();
        }

        return vt_createFuncObject<ExampleDelimitedUDChunker>(srvInterface.allo\
cator,
                recordTerminator[0]);
    }

5.10.3.9 - Python 示例:复杂类型的 JSON 解析器

以下示例详细说明了 UDParser,它接受 JSON 对象并将其解析为复杂类型。对于此示例,解析器假设输入数据是具有两个整数字段的行数组。输入记录应使用换行符进行分隔。如果 JSON 输入未指定任何行字段,则函数会将这些字段解析为 NULL。

此 UDParser 的源代码还包含一个工厂方法,用于解析具有整数和整数字段数组的行。解析器的实施与工厂中的返回类型无关,因此您可以创建具有不同返回类型且均指向 prepare() 方法中的 ComplexJsonParser() 类的工厂。完整的源代码位于 /opt/vertica/sdk/examples/python/UDParsers.py 中。

加载和使用示例

加载库并创建解析器,如下所示:


=> CREATE OR REPLACE LIBRARY UDParsers AS '/home/dbadmin/examples/python/UDParsers.py' LANGUAGE 'Python';

=> CREATE PARSER ComplexJsonParser AS LANGUAGE 'Python' NAME 'ArrayJsonParserFactory' LIBRARY UDParsers;

您现在可以定义一个表,然后使用 JSON 解析器将数据加载到其中,例如:


=> CREATE TABLE orders (a bool, arr array[row(a int, b int)]);
CREATE TABLE

=> COPY orders (arr) FROM STDIN WITH PARSER ComplexJsonParser();
[]
[{"a":1, "b":10}]
[{"a":1, "b":10}, {"a":null, "b":10}]
[{"a":1, "b":10},{"a":10, "b":20}]
[{"a":1, "b":10}, {"a":null, "b":null}]
[{"a":1, "b":2}, {"a":3, "b":4}, {"a":5, "b":6}, {"a":7, "b":8}, {"a":9, "b":10}, {"a":11, "b":12}, {"a":13, "b":14}]
\.

=> SELECT * FROM orders;
a |                                  arr
--+--------------------------------------------------------------------------
  | []
  | [{"a":1,"b":10}]
  | [{"a":1,"b":10},{"a":null,"b":10}]
  | [{"a":1,"b":10},{"a":10,"b":20}]
  | [{"a":1,"b":10},{"a":null,"b":null}]
  | [{"a":1,"b":2},{"a":3,"b":4},{"a":5,"b":6},{"a":7,"b":8},{"a":9,"b":10},{"a":11,"b":12},{"a":13,"b":14}]
(6 rows)

设置

所有 Python UDx 都必须导入 Vertica SDK 库。 ComplexJsonParser() 也需要 json 库。

import vertica_sdk
import json

工厂实施

prepare() 方法将实例化并返回解析器:


def prepare(self, srvInterface, perColumnParamReader, planCtxt, returnType):
    return ComplexJsonParser()

getParserReturnType() 声明返回类型必须是行数组,其中每个行都有两个整数字段:


def getParserReturnType(self, rvInterface, perColumnParamReader, planCtxt, argTypes, returnType):
    fieldTypes = vertica_sdk.SizedColumnTypes.makeEmpty()
    fieldTypes.addInt('a')
    fieldTypes.addInt('b')
    returnType.addArrayType(vertica_sdk.SizedColumnTypes.makeRowType(fieldTypes, 'elements'), 64, 'arr')

解析器实施

process() 方法会使用 InputBuffer 读入数据,然后在换行符处拆分该输入数据。随后,该方法会将处理后的数据传递给 writeRows() 方法。 writeRows() 会将每个数据行转换为 JSON 对象,检查该 JSON 对象的类型,然后将相应的值或对象写入输出。


class ComplexJsonParser(vertica_sdk.UDParser):

    leftover = ''

    def process(self, srvInterface, input_buffer, input_state, writer):
        input_buffer.setEncoding('utf-8')

        self.count = 0
        rec = self.leftover + input_buffer.read()
        row_lst = rec.split('\n')
        self.leftover = row_lst[-1]
        self.writeRows(row_lst[:-1], writer)
        if input_state == InputState.END_OF_FILE:
            self.writeRows([self.leftover], writer)
            return StreamState.DONE
        else:
            return StreamState.INPUT_NEEDED

    def writeRows(self, str_lst, writer):
        for s in str_lst:
            stripped = s.strip()
            if len(stripped) == 0:
                return
            elif len(stripped) > 1 and stripped[0:2] == "//":
                continue
            jsonValue = json.loads(stripped)
            if type(jsonValue) is list:
                writer.setArray(0, jsonValue)
            elif jsonValue is None:
                writer.setNull(0)
            else:
                writer.setRow(0, jsonValue)
            writer.next()

5.10.4 - 加载并行度

Vertica 可以拆分加载数据的工作,从而利用并行度来加快操作速度。Vertica 支持多种类型的并行度:

  • 分布式加载:Vertica 会将多文件加载中的文件分配给多个节点以并行加载,而不是在单个节点上加载所有文件。Vertica 会管理分布式加载;您无需在 UDL 中执行任何特殊操作。

  • 协作解析:在单个节点上加载的源会使用多线程来并行执行解析。协作解析会根据线程的调度方式在执行时拆分加载。您必须在解析器中启用协作解析。请参阅协作解析

  • 分摊加载:Vertica 会将单个大型文件或其他单个源拆分成多个段,以将段分配给多个节点进行并行加载。分摊加载会根据每个节点上的可用节点和核心在计划时拆分加载。您必须在源代码和解析器中启用分摊加载。请参阅分摊加载

您可以在同一 UDL 中同时支持协作解析和分摊加载。Vertica 可确定要对每个加载操作使用哪个并行度,也可能同时使用这两者。请参阅组合协作解析和分摊加载

5.10.4.1 - 协作解析

默认情况下,Vertica 会在一个数据库节点上的单个线程中解析数据源。您可以选择使用协作解析,以在节点上使用多个线程来解析源。更具体地说,源中的数据将通过块分割器,块分割器会将源流中的块分组为逻辑单元。可以并行解析这些块。块分割器会将输入拆分为可单独解析的块,然后由解析器同时进行解析。协作解析仅适用于非隔离 UDx。(请参阅隔离和非隔离模式。)

要使用协作解析,块分割器必须能够在输入中找到记录结尾标记。并非在所有输入格式中都能找到这些标记。

块分割器由解析器工厂创建。在加载时,Vertica 会先调用 UDChunker 以将输入拆分为块,然后调用 UDParser 以解析每个块。

您可以单独或同时使用协作解析和分摊加载。请参阅组合协作解析和分摊加载

Vertica 如何拆分加载

当 Vertica 收到来自源的数据时,它会重复调用块分割器的 process() 方法。块分割器本质上是一个轻量级解析器。process() 方法用于将输入拆分为块,而不是进行解析。

块分割器完成将输入拆分为块后,Vertica 会将这些块发送到尽可能多的可用解析器,从而在解析器上调用 process() 方法。

实施协作解析

要实施协作解析,请执行以下操作:

  • 子类化 UDChunker 并实施 process()

  • ParserFactory 中,实施 prepareChunker() 以返回 UDChunker

请参阅 C++ 示例:分隔解析器和块分割器以了解同样支持分摊加载的 UDChunker

5.10.4.2 - 分摊加载

解析器可以使用多个数据库节点来并行加载单个输入源。此方法称为分摊加载。在 Vertica 内置的解析器中,默认(分隔)解析器支持分摊加载。

与协作解析一样,分摊加载也需要可以在记录边界进行拆分的输入。不同之处在于,协作解析会执行顺序扫描以查找记录边界,而分摊加载会先跳(寻找)至给定位置,然后再执行扫描。某些格式(例如通用 XML)不支持寻找。

要使用分摊加载,您必须确保所有参与的数据库节点都可以访问源。分摊加载通常与分布式文件系统结合使用。

解析器可以不直接支持分摊加载,但需要包含支持分摊的块分割器。

您可以单独或同时使用分摊加载和协作解析。请参阅组合协作解析和分摊加载

Vertica 如何分摊加载

如果解析器及其源均支持分摊,则您可以指定将单个输入分发到多个数据库节点进行加载。SourceFactory 可将输入拆分为多个部分,并将它们分配给执行节点。每个 Portion 包含一个输入偏移量和一个大小。Vertica 会将各个部分及其参数分发到执行节点。在每个节点上运行的源工厂将为给定部分生成一个 UDSource

UDParser 首先会确定从何处开始解析:

  • 如果该部分是输入中的第一部分,则解析器会前进至该偏移量并开始解析。

  • 如果不是输入中的第一部分,则解析器会前进该偏移量,然后执行扫描直至找到记录的结尾。由于记录分散在多个部分,因此在到达第一个记录结尾之后开始解析。

解析器必须完成整个记录,这可能要求解析器在该部分的结尾之后继续读取。无论记录在何处结束,解析器都负责处理从已分配的部分开始的所有记录。此工作主要在解析器的 process() 方法中进行。

有时,一个部分不包含可由已分配的节点解析的任何内容。例如,假设某个记录从第 1 部分开始,而且贯穿第 2 部分并在第 3 部分结束。分配给第 1 部分的解析器将解析该记录,而分配给第 3 部分的解析器将在该记录之后开始解析。但是,分配给第 2 部分的解析器没有任何记录从此部分开始。

如果加载也使用 协作解析,则在分摊加载之后和解析之前,Vertica 会将部分拆分成块以进行并行加载。

实施分摊加载

要实施分摊加载,请在源、解析器及其工厂中执行下列操作。

在您的 SourceFactory 子类中:

  • 实施 isSourceApportionable() 并返回 true

  • 实施 plan(),以确定部分大小,指定部分,以及将各个部分分配给执行节点。要将多个部分分配给特定的执行程序,请使用计划上下文 (PlanContext::getWriter()) 上的参数编写器来传递信息。

  • 实施 prepareUDSources()。Vertica 将使用由工厂创建的计划上下文对每个执行节点调用此方法。此方法将返回 UDSource 实例,以用于该节点的已分配部分。

  • 如果源可以利用并行度,您可以实施 getDesiredThreads() 为每个源请求多个线程。有关此方法的详细信息,请参阅 SourceFactory 类

UDSource 子类中,与对任何其他源一样使用已分配的部分实施 process()。可以使用 getPortion() 检索此部分。

在您的 ParserFactory 子类中:

  • 实施 isParserApportionable() 并返回 true

  • 如果解析器使用支持分摊加载的 UDChunker,请实施 isChunkerApportionable()

在您的 UDParser 子类中:

  • 编写 UDParser 子类,使其对各个部分而非整个源执行操作。您可以通过处理流状态 PORTION_STARTPORTION_END 或使用 ContinuousUDParser API 来完成该编写。解析器必须扫描该部分的开头,查找该部分之后的第一个记录边界并解析至从该部分开始的最后一个记录的结尾。请注意,此行为可能会要求解析器在该部分的结尾之后继续读取。

  • 对某个部分不包含任何记录这一特殊情况进行如下处理:返回但不写入任何输出。

UDChunker 子类中,实施 alignPortion()。请参阅对齐部分

示例

SDK 在 ApportionLoadFunctions 目录中提供了分摊加载的 C++ 示例:

  • FilePortionSourceUDSource 的子类。

  • DelimFilePortionParserContinuousUDParser 的子类。

将这些类一起使用。您还可以将 FilePortionSource 与内置的分隔解析器结合使用。

以下示例显示了如何加载库并在数据库中创建函数:


=> CREATE LIBRARY FilePortionSourceLib as '/home/dbadmin/FP.so';

=> CREATE LIBRARY DelimFilePortionParserLib as '/home/dbadmin/Delim.so';

=> CREATE SOURCE FilePortionSource AS
LANGUAGE 'C++' NAME 'FilePortionSourceFactory' LIBRARY FilePortionSourceLib;

=> CREATE PARSER DelimFilePortionParser AS
LANGUAGE 'C++' NAME 'DelimFilePortionParserFactory' LIBRARY DelimFilePortionParserLib;

以下示例显示了如何使用源和解析器来加载数据:


=> COPY t WITH SOURCE FilePortionSource(file='g1/*.dat') PARSER DelimFilePortionParser(delimiter = '|',
    record_terminator = '~');

5.10.4.3 - 组合协作解析和分摊加载

您可以在同一解析器中同时启用协作解析分摊加载,从而允许 Vertica 决定如何加载数据。

决定如何拆分加载

Vertica 在查询规划时会尽可能使用分摊加载。它决定是否在执行时也使用协作解析。

分摊加载需要 SourceFactory 支持。如果指定合适的 UDSource,Vertica 将在规划时对 ParserFactory 调用 isParserApportionable() 方法。如果此方法返回 true,Vertica 将分摊加载。

如果 isParserApportionable() 返回 false,但 isChunkerApportionable() 返回 true,则块分割器可用于协作解析且块分割器支持分摊加载。Vertica 会分摊加载。

如果这些方法均未返回 true,则 Vertica 不会分摊加载。

在执行时,Vertica 会先检查加载是否在非隔离模式下运行,并仅在非隔离模式下运行才继续操作。在隔离模式下,不支持协作解析。

如果未分摊加载且有多个线程可用,Vertica 将使用协作解析。

如果已分摊加载且正好只有一个线程可用,则 Vertica 在当且仅当解析器不可分摊时才使用协作解析。在这种情况下,块分割器是可分摊的,但解析器是不可分摊的。

如果已分摊加载、有多个线程可用且块分割器是可分摊的,则 Vertica 会使用协作解析。

如果 Vertica 使用协作解析,但 prepareChunker() 未返回 UDChunker 实例,则 Vertica 会报告错误。

执行分摊的协作加载

如果加载同时使用分摊加载和协作解析,则 Vertica 会使用 SourceFactory 将输入分解为多个部分。然后,它将这些部分分配给执行节点。请参阅 Vertica 如何分摊加载

在执行节点上,Vertica 会调用块分割器的 alignPortion() 方法以将输入与部分边界对齐。(此步骤在第一部分已跳过,且根据定义已在开始时对齐。)必须执行此步骤,因为使用分摊加载的解析器有时必须在相应部分结尾之后继续读取,因此块分割器需要找到终点。

对齐相应部分后,Vertica 会重复调用块分割器的 process() 方法。请参阅 Vertica 如何拆分加载

然后,将块分割器找到的各个块发送给解析器的 process() 方法以按照常规方式进行处理。

5.10.5 - 连续加载

ContinuousUDSourceContinuousUDFilterContinuousUDParser 类允许您根据需要写入和处理数据,而不必遍历数据。Python API 不支持连续加载。

每个类均包含以下函数:

  • initialize() - 已在 run() 之前调用。您可以选择覆盖此函数以执行设置和初始化。

  • run() - 处理数据。

  • deinitialize() - 已在 run() 返回之后调用。您可以选择覆盖此函数以执行分解和消除。

请勿覆盖从父类继承的 setup()process()destroy() 函数。

您可以使用 yield() 函数在服务器空闲或陷入忙循环期间将控制权交回服务器,以便该服务器可以检查状态变化或取消查询。

这三个类会使用关联的 ContinuousReaderContinuousWriter 类来读取输入数据和写入输出数据。

ContinuousUDSource API (C++)

ContinuousUDSource 类会扩展 UDSource 并添加以下方法以通过子类扩展:

virtual void initialize(ServerInterface &srvInterface);

virtual void run();

virtual void deinitialize(ServerInterface &srvInterface);

ContinuousUDFilter API (C++)

ContinuousUDFilter 类会扩展 UDFilter 并添加以下方法以通过子类扩展:

virtual void initialize(ServerInterface &srvInterface);

virtual void run();

virtual void deinitialize(ServerInterface &srvInterface);

ContinuousUDParser API

ContinuousUDParser 类会扩展 UDParser 并添加以下方法以通过子类扩展:

virtual void initialize(ServerInterface &srvInterface);

virtual void run();

virtual void deinitialize(ServerInterface &srvInterface);

ContinuousUDParser 类会扩展 UDParser 并添加以下方法以通过子类扩展:


public void initialize(ServerInterface srvInterface, SizedColumnTypes returnType);

public abstract void run() throws UdfException;

public void deinitialize(ServerInterface srvInterface, SizedColumnTypes returnType);

有关其他实用程序方法,请参阅 API 文档。

5.10.6 - 缓冲区类

缓冲区类可用作所有 UDL 函数的原始数据流的句柄。C++ 和 Java API 对输入和输出使用单个 DataBuffer 类。Python API 包含两个类:InputBufferOutputBuffer

DataBuffer API(C++、java)

DataBuffer 类具有指向缓冲区和大小的指针,以及指示已使用的流量的偏移量。

/**
* A contiguous in-memory buffer of char *
*/
    struct DataBuffer {
    /// Pointer to the start of the buffer
    char * buf;

    /// Size of the buffer in bytes
    size_t size;

    /// Number of bytes that have been processed by the UDL
    size_t offset;
};

DataBuffer 类具有可指示已使用的流量的偏移量。因为 Java 是一种其字符串需要注意字符编码的语言,所以 UDx 必须对缓冲区进行解码或编码。解析器可以通过直接访问缓冲区来与流交互。

/**
* DataBuffer is a a contiguous in-memory buffer of data.
*/
public class DataBuffer {

/**
* The buffer of data.
*/
public byte[] buf;

/**
* An offset into the buffer that is typically used to track progress
* through the DataBuffer. For example, a UDParser advances the
* offset as it consumes data from the DataBuffer.
*/
public int offset;}

InputBuffer API 和 OutputBuffer API (Python)

Python InputBuffer 和 OutputBuffer 类会取代 C++ 和 Java API 中的 DataBuffer 类。

InputBuffer 类

InputBuffer 类会根据指定的编码来解码和转换原始数据流。Python 原本就支持各种语言和编解码器。InputBuffer 是 UDFilters 和 UDParsers 的 process() 方法的实参。用户会通过调用 InputBuffer 的方法来与 UDL 的数据流进行交互

如果没有为 setEncoding() 指定值,Vertica 会假设值为 NONE。

class InputBuffer:
    def getSize(self):
        ...
    def getOffset(self):
    ...

    def setEncoding(self, encoding):
        """
        Set the encoding of the data contained in the underlying buffer
        """
        pass

    def peek(self, length = None):
        """
        Copy data from the input buffer into Python.
        If no encoding has been specified, returns a Bytes object containing raw data.
        Otherwise, returns data decoded into an object corresponding to the specified encoding
        (for example, 'utf-8' would return a string).
        If length is None, returns all available data.
        If length is not None then the length of the returned object is at most what is requested.
        This method does not advance the buffer offset.
        """
        pass

    def read(self, length = None):
        """
        See peek().
        This method does the same thing as peek(), but it also advances the
        buffer offset by the number of bytes consumed.
        """
        pass

        # Advances the DataBuffer offset by a number of bytes equal to the result
        # of calling "read" with the same arguments.
        def advance(self, length = None):
        """
        Advance the buffer offset by the number of bytes indicated by
        the length and encoding arguments.  See peek().
    Returns the new offset.
        """
        pass

OutputBuffer 类

OutputBuffer 类会对 Python 中的数据进行编码并输出到 Vertica。OutputBuffer 是 UDFilters 和 UDParsers 的 process() 方法的实参。用户会通过调用 OutputBuffer 的方法来操作数据并进行编码,从而与 UDL 的数据流进行交互。

write() 方法会将所有数据从 Python 客户端传输到 Vertica。输出缓冲区可以接受任何大小的对象。如果用户向 OutputBuffer 写入的对象大于 Vertica 可以立即处理的大小,则 Vertica 会存储溢出。在下一次调用 process() 期间,Vertica 会检查剩余数据。如果有剩余数据,Vertica 会先将其复制到 DataBuffer,然后再确定是否需要从 Python UDL 调用 process()

如果没有为 setEncoding() 指定值,Vertica 会假设值为 NONE。

class OutputBuffer:
def setEncoding(self, encoding):
"""
Specify the encoding of the data which will be written to the underlying buffer
"""
pass
def write(self, data):
"""
Transfer bytes from the data object into Vertica.
If an encoding was specified via setEncoding(), the data object will be converted to bytes using the specified encoding.
Otherwise, the data argument is expected to be a Bytes object and is copied to the underlying buffer.
"""
pass