服务器编程
1.扩展SQL
1.1.扩展性如何工作
UXDB是可扩展的,因为它的操作是基于目录的。如果您熟悉标准关系数据库系统,您就知道它们将有关数据库、表、列等的信息存储在通常称为系统目录的地方。(有些系统称其为数据字典。)目录对用户显示为像任何其他表格一样的表格,但是DBMS将其内部簿记存储在其中。UXDB和标准关系数据库系统之间的一个关键区别是,UXDB在其目录中存储了更多的信息:不仅包括有关表和列的信息,还包括有关数据类型、函数、访问方法等的信息。这些表格可以由用户修改,由于UXDB的操作基于这些表格,这意味着用户可以扩展UXDB。相比之下,传统的数据库系统只能通过更改源代码中的硬编码过程或加载由DBMS供应商特别编写的模块来扩展。
此外,UXDB服务器可以通过动态加载将用户编写的代码合并到自身中。也就是说,用户可以指定实现新类型或函数的对象代码文件(例如共享库),UXDB将根据需要加载它。编写的SQL代码更容易添加到服务器中。这种修改其操作的能力“在飞行中”使得UXDB非常适合快速原型开发新应用程序和存储结构。
1.2.UXDB 类型系统
UXDB 数据类型可以分为基本类型、容器类型、域和伪类型。
1.2.1.基本类型
基本类型是那些在 SQL 语言层面之下实现的类型,比如整数类型,通常是用低级语言(如 C 语言)实现的抽象数据类型。UXDB只能通过用户提供的函数操作这些类型,并且只能理解用户描述这些类型的行为的程度。
枚举(enum)类型可以被视为基本类型的一个子类。它们的主要区别在于,它们可以使用仅使用 SQL 命令创建,而不需要任何低级编程。
1.2.2.容器类型
UXDB 有三种容器类型:数组、范围和复合类型。
“容器”类型是包含其他类型多个值的类型。这些类型包括数组、组合类型和范围类型。数组可以容纳相同类型的多个值。每个基本类型、组合类型、范围类型和域类型都会自动创建一个数组类型。但是没有数组的数组。就类型系统而言,多维数组与一维数组相同。
组合类型或行类型是用户创建表时创建的。还可以使用CREATE TYPE定义一个“独立”的组合类型,没有关联的表。组合类型只是具有关联字段名称的类型列表。组合类型的值是字段值的行或记录。
范围类型可以容纳两个相同类型的值,即范围的下限和上限。范围类型是用户创建的,尽管存在一些内置类型。
1.2.3.域
域基于特定的基础类型,对于许多目的而言,可以与其基础类型互换使用。但是,域可以具有约束,限制其有效值为基础类型允许的子集。使用SQL命令CREATE DOMAIN创建域。
1.2.4.伪类型
还有一些用于特殊目的的“伪类型”。伪类型不能作为表的列或容器类型的组件出现,但它们可以用于声明函数的参数和结果类型。这提供了在类型系统中识别特殊类别的函数的机制。
1.2.5.多态类型
一些特别感兴趣的伪类型是“多态类型”,用于声明“多态函数”。这个强大的功能允许单个函数定义在许多不同的数据类型上操作,具体的数据类型由在特定调用中实际传递给它的数据类型确定。
多态类型
| 名称 | 族 | 描述 |
|---|---|---|
anyelement | 简单 | 表示函数接受任何数据类型 |
anyarray | 简单 | 表示函数接受任何数组数据类型 |
anynonarray | 简单 | 表示函数接受任何非数组数据类型 |
anyenum | 简单 | 表示函数接受任何枚举数据类型 |
anyrange | 简单 | 表示函数接受任何范围数据类型 |
anymultirange | 简单 | 表示函数接受任何多范围数据类型 |
anycompatible | 常见 | 表示函数接受任何数据类型,并自动将多个参数提升为公共数据类型 |
anycompatiblearray | 常见 | 表示函数接受任何数组数据类型,并自动将多个参数提升为公共数据类型 |
anycompatiblenonarray | 常见 | 表示函数接受任何非数组数据类型,并自动将多个参数提升为公共数据类型 |
anycompatiblerange | 常见 | 表示函数接受任何范围数据类型,并自动将多个参数提升为公共数据类型 |
anycompatiblemultirange | 常见 | 表示函数接受任何多范围数据类型,并自动将多个参数提升为公共数据类型 |
多态参数和结果是相互关联的,并且在解析调用多态函数的查询时会解析为特定的数据类型。当存在多个多态参数时,输入值的实际数据类型必须匹配,如下所述。如果函数的结果类型是多态的,或者它具有多态类型的输出参数,则这些结果的类型将从多态输入的实际类型中推断出来,如下所述。
对于“简单”多态类型族,匹配和推断规则如下:
每个声明为anyelement的位置(参数或返回值)都允许具有任何特定的实际数据类型,但在任何给定的调用中,它们必须都是相同的实际类型。每个声明为anyarray的位置都可以具有任何数组数据类型,但同样它们必须都是相同的类型。同样,声明为anyrange的位置必须都是相同的范围类型。对于anymultirange也是如此。
此外,如果存在声明为anyarray和其他声明为anyelement的位置,则anyarray位置中的实际数组类型必须是一个其元素与anyelement位置中出现的相同类型的数组。anynonarray与anyelement的处理方式完全相同,但增加了一个附加约束条件,即实际类型不能是数组类型。anyenum与anyelement的处理方式完全相同,但增加了一个附加约束条件,即实际类型必须是枚举类型。
同样,如果有声明为anyrange的位置,以及其他声明为anyelement或anyarray的位置,则anyrange位置的实际范围类型必须是一个子类型与anyelement位置相同的类型,并且与anyarray位置的元素类型相同。如果有声明为anymultirange的位置,它们的实际多范围类型必须包含与声明为anyrange的参数匹配的范围,以及与声明为anyelement和anyarray的参数匹配的基本元素。
因此,当声明多个参数位置具有多态类型时,净效果是只允许某些实际参数类型的组合。例如,声明为equal(anyelement,anyelement)的函数将接受任何两个输入值,只要它们是相同的数据类型。
当将函数的返回值声明为多态类型时,必须至少有一个参数位置也是多态的,并且提供给多态参数的实际数据类型确定该调用的实际结果类型。例如,如果没有数组下标机制,可以定义一个实现下标的函数,如subscript(anyarray,integer) returns anyelement。此声明将约束实际第一个参数为数组类型,并允许解析器从实际第一个参数的类型推断出正确的结果类型。另一个例子是,声明为f(anyarray) returns anyenum的函数只接受枚举类型的数组。
在大多数情况下,解析器可以从同一族中不同多态类型的参数中推断出多态结果类型的实际数据类型;例如,anyarray可以从anyelement或反之推断出。一个例外是,多态结果类型anyrange需要一个类型为anyrange的参数;它不能从anyarray或anyelement参数中推断出。这是因为可能有多个具有相同子类型的范围类型。
请注意,anynonarray和anyenum不表示单独的类型变量; 它们与anyelement相同,只是增加了一个附加约束条件。例如,将函数声明为f(anyelement, anyenum)等同于将其声明为f(anyenum, anyenum):两个实际参数都必须是相同的枚举类型。
对于“常见”族的多态类型,匹配和推断规则与“简单”族的规则大致相同,但有一个主要区别:实际参数的类型不需要完全相同,只要它们可以隐式转换为单个公共类型即可。选择公共类型遵循与UNION和相关构造相同的规则。选择公共类型考虑实际类型。对于anycompatible和anycompatiblenonarray输入,它们的公共类型为anycompatiblearray输入的数组元素类型,anycompatiblerange输入的范围子类型,以及anycompatiblemultirange输入的多范围子类型。如果存在anycompatiblenonarray,则要求公共类型为非数组类型。一旦确定了公共类型,anycompatible和anycompatiblenonarray位置的参数会自动转换该类型,anycompatiblearray位置的参数会自动转换为该类型的数组类型。
由于没有办法仅凭子类型选择范围类型,因此使用anycompatiblerange和/或anycompatiblemultirange需要声明为该类型的所有参数具有相同的实际范围和/或多范围类型,并且该类型的子类型与所选公共类型一致,因此不需要对范围值进行转换。与anyrange和anymultirange一样,将anycompatiblerange和anymultirange用作函数结果类型需要存在anycompatiblerange或anycompatiblemultirange参数。
请注意,没有anycompatibleenum类型。这种类型并不是很有用,因为通常没有任何隐式转换到枚举类型,这意味着没有办法解决不同的枚举输入的公共类型。
“simple”和“common”多态家族表示两组独立的类型变量。例如,考虑以下函数:
CREATE FUNCTION myfunc(a anyelement, b anyelement,
c anycompatible, d anycompatible)
RETURNS anycompatible AS ...
在实际调用此函数时,前两个输入必须具有完全相同的类型。最后两个输入必须能够提升为公共类型,但此类型不必与前两个输入的类型有任何关系。结果将具有最后两个输入的公共类型。
可变函数(接受可变数量的参数)可以是多态的:这是通过将其最后一个参数声明为VARIADIC anyarray或VARIADIC anycompatiblearray来实现的。对于参数匹配和确定实际结果类型的目的,这样的函数的行为与您编写适当数量的anynonarray或anycompatiblenonarray参数相同。
1.3.用户定义函数
UXDB 提供了四种类型的函数:
-
查询语言函数(用 SQL 编写的函数)(查询语言(SQL)函数)
-
过程语言函数(用例如 PL/uxSQL 或 PL/Tcl 编写的函数)(过程语言函数)
-
内部函数(内部函数)
-
C语言函数(C语言函数)
每种类型的函数都可以接受基本类型、复合类型或这些类型的组合作为参数。此外,每种类型的函数都可以返回基本类型或复合类型。函数还可以定义为返回基本或复合值的集合。
许多类型的函数可以接受或返回某些伪类型(例如多态类型),但可用的功能因函数类型而异。有关详细信息,请参阅每种类型的函数的描述。
最容易定义的是 SQL 函数,因此我们将从讨论这些函数开始。大多数介绍 SQL 函数的概念也适用于其他类型的函数。
在本章中,查看CREATE FUNCTION命令的参考页面可以更好地理解示例。本章的一些示例可以在 UXDB 源代码分发中的 src/tutorial 目录下的funcs.sql 和 funcs.c 中找到。
1.4.用户定义的过程
过程是类似于函数的数据库对象。主要区别在于:
-
过程使用
CREATE PROCEDURE命令定义,而不是CREATE FUNCTION。 -
过程不返回函数值;因此,
CREATE PROCEDURE没有RETURNS子句。但是,过程可以通过输出参数将数据返回给调用者。 -
函数作为查询或DML命令的一部分调用,而过程使用
CALL命令隔离调用。 -
过程可以在执行过程中提交或回滚事务(然后自动开始新事务),只要调用的
CALL命令不是显式事务块的一部分。函数无法做到这一点。 -
某些函数属性,如严格性,不适用于过程。这些属性控制函数在查询中的使用方式,对过程不相关。
以下各节中关于如何定义用户定义函数的解释也适用于过程,除了上述要点。
函数和过程统称为例程。有一些命令,如ALTERROUTINE和 DROP ROUTINE,可以操作函数和过程,而不必知道它是哪种类型。但是请注意,没有CREATE ROUTINE 命令。
1.5.查询语言(SQL)函数
SQL函数执行任意数量的SQL语句,返回列表中最后一个查询的结果。在简单(非集合)情况下,将返回最后一个查询结果的第一行。请记住,如果不使用ORDER BY,则多行结果的“第一行”未定义。如果最后一个查询返回零行,则将返回null值。
另外,可以声明一个SQL函数返回一个集合(即多行),方法是将函数的返回类型指定为 SETOF sometype,或者等效地声明为RETURNS TABLE(columns)。在这种情况下,将返回最后一个查询结果的所有行。更多细节请参见下文。
SQL 函数的主体必须是由分号分隔的 SQL 语句列表。最后一个语句必须是一个 SELECT,或者是一个带有 RETURNING 子句的INSERT、UPDATE 或 DELETE,除非函数被声明为返回 void。
任何 SQL 语言中的命令集合都可以打包在一起并定义为函数。除了 SELECT查询外,这些命令还可以包括数据修改查询(INSERT、UPDATE和 DELETE),以及其他 SQL 命令(不能在 SQL 函数中使用事务控制命令,例如 COMMIT、SAVEPOINT和一些实用程序命令,例如 VACUUM)。但是,最后一个命令必须是一个 SELECT,或者具有返回函数返回类型的RETURNING 子句。或者,如果要定义一个执行操作但没有有用返回值的 SQL 函数,则可以将其定义为返回 void。例如,此函数从emp 表中删除负薪水的行:
CREATE FUNCTION clean_emp() RETURNS void AS '
DELETE FROM emp
WHERE salary < 0;
' LANGUAGE SQL;
SELECT clean_emp();
clean_emp
-----------
(1 row)
也可以将其编写为存储过程,从而避免返回类型的问题。例如:
CREATE PROCEDURE clean_emp() AS '
DELETE FROM emp
WHERE salary < 0;
' LANGUAGE SQL;
CALL clean_emp();
在这种简单情况下,返回 void的函数和存储过程之间的区别主要是风格上的。但是,存储过程提供了额外的功能,例如事务控制,这在函数中不可用。此外,存储过程是SQL标准,而返回 void 是 UXDB 的扩展。
注意
SQL 函数的整个主体在执行任何部分之前都会被解析。虽然 SQL 函数可以包含更改系统目录的命令(例如 CREATE TABLE),但是这些命令的效果在函数中后续命令的解析分析期间不可见。因此,例如 CREATE TABLE foo (...); INSERT INTO foo VALUES(...); 如果打包到单个 SQL 函数中,则无法按预期工作,因为在解析INSERT 命令时,foo 还不存在。在这种情况下,建议使用 PL/uxSQL 而不是 SQL 函数。
CREATE FUNCTION命令的语法要求将函数主体编写为字符串常量。通常,可以使用单引号括起来,但是如果函数主体本身包含单引号,则必须使用两个单引号来转义。
最方便的方法是使用美元引用来表示字符串常量。如果选择使用常规的单引号字符串常量语法,则必须在函数体中加倍单引号标记(')和反斜杠(\)(假设使用转义字符串语法)。
1.5.1.SQL函数的参数
SQL函数的参数可以使用名称或数字在函数体中引用。下面分别给出了两种方法的示例。
要使用名称,请声明函数参数具有名称,然后在函数体中只需编写该名称。如果参数名称与函数内当前SQL命令中的任何列名称相同,则列名称将优先。要覆盖此,请使用函数本身的名称限定参数名称,即function_name.argument_name。(如果这会与限定的列名冲突,则再次列名获胜。您可以通过为SQL命令中的表选择不同的别名来避免歧义。)
在旧的数字方法中,使用语法$n引用参数:$ 1表示第一个输入参数,$ 2表示第二个参数,依此类推。无论特定参数是否已声明名称,这都将起作用。
如果参数是复合类型,则可以使用点表示法,例如argname.fieldname或$1.fieldname,以访问参数的属性。同样,您可能需要使用函数名称限定参数名称,以使具有参数名称的形式不含糊。
SQL函数参数只能用作数据值,而不能用作标识符。因此,例如,这是合理的:
INSERT INTO mytable VALUES($ 1);
但这将不起作用:
INSERT INTO $ 1 VALUES(42);
注意
UXDB添加了使用名称引用SQL函数参数的功能。要在旧服务器中使用的函数必须使用$n表示法。
1.5.2.基本类型的SQL函数
最简单的SQL函数没有参数,只返回基本类型,例如integer:
CREATE FUNCTION one() RETURNS integer AS $$
SELECT 1 AS result;
$$ LANGUAGE SQL;
-- Alternative syntax for string literal:
CREATE FUNCTION one() RETURNS integer AS '
SELECT 1 AS result;
' LANGUAGE SQL;
SELECT one();
one
-----
1
请注意,我们在函数体内为函数结果定义了一个列别名(名称为result),但是此列别名在函数外部不可见。因此,结果标记为one而不是result。
定义接受基本类型参数的SQL函数几乎和定义其他函数一样简单:
CREATE FUNCTION add_em(x integer, y integer) RETURNS integer AS $$
SELECT x + y;
$$ LANGUAGE SQL;
SELECT add_em(1, 2) AS answer;
answer
--------
3
或者我们可以不使用参数名称,而是使用数字:
CREATE FUNCTION add_em(integer, integer) RETURNS integer AS $$
SELECT $1 + $2;
$$ LANGUAGE SQL;
SELECT add_em(1, 2) AS answer;
answer
--------
3
下面是一个更有用的函数,用于从银行账户中扣款:
CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
UPDATE bank
SET balance = balance - debit
WHERE accountno = tf1.accountno;
SELECT 1;
$$ LANGUAGE SQL;
用户可以执行此函数来从账户17中扣除100.00美元:
SELECT tf1(17, 100.0);
在此示例中,我们选择了第一个参数的名称accountno,但这与bank表中的列名相同。在UPDATE命令中,accountno指的是列bank.accountno,因此必须使用tf1.accountno来引用参数。当然,我们可以通过为参数使用不同的名称来避免这种情况。
实际上,我们可能希望从函数中得到比常数1更有用的结果,因此更可能的定义是:
CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
UPDATE bank
SET balance = balance - debit
WHERE accountno = tf1.accountno;
SELECT balance FROM bank WHERE accountno = tf1.accountno;
$$ LANGUAGE SQL;
它调整余额并返回新余额。可以使用RETURNING在一个命令中完成相同的操作:
CREATE FUNCTION tf1 (accountno integer, debit numeric) RETURNS numeric AS $$
UPDATE bank
SET balance = balance - debit
WHERE accountno = tf1.accountno
RETURNING balance;
$$ LANGUAGE SQL;
如果SQL函数中的最后一个SELECT或RETURNING子句没有返回与函数声明的结果类型完全相同的结果,则UXDB将自动将该值转换为所需的类型(如果可以使用隐式或赋值转换)。否则,您必须编写显式转换。例如,假设我们希望前面的add_em函数返回类型float8。只需编写以下内容即可:
CREATE FUNCTION add_em(integer, integer) RETURNS float8 AS $$
SELECT $1 + $2;
$$ LANGUAGE SQL;
因为integer总和可以隐式转换为float8。
1.5.3.复合类型的SQL函数
在编写具有复合类型参数的函数时,我们不仅必须指定要使用的参数,还必须指定该参数的所需属性(字段)。例如,假设emp是包含员工数据的表,因此也是表的每行的复合类型的名称。下面是一个函数double_salary,它计算如果工资翻倍会是多少:
CREATE TABLE emp (
name text,
salary numeric,
age integer,
cubicle point
);
INSERT INTO emp VALUES ('Bill', 4200, 45, '(2,1)');
CREATE FUNCTION double_salary(emp) RETURNS numeric AS $$
SELECT $1.salary * 2 AS salary;
$$ LANGUAGE SQL;
SELECT name, double_salary(emp.*) AS dream
FROM emp
WHERE emp.cubicle ~= point '(2,1)';
name | dream
------+-------
Bill | 8400
注意使用语法$1.salary来选择参数行值的一个字段。还要注意调用SELECT命令使用table_name.*来选择表的整个当前行作为复合值。表行也可以使用表名来引用,如下所示:
SELECT name, double_salary(emp) AS dream
FROM emp
WHERE emp.cubicle ~= point '(2,1)';
但是这种用法已经过时,因为很容易混淆。
有时构造临时复合参数值很方便。这可以使用ROW构造来完成。例如,我们可以调整传递给函数的数据:
SELECT name, double_salary(ROW(name, salary*1.1, age, cubicle)) AS dream
FROM emp;
还可以构建一个返回复合类型的函数。这是一个返回单个emp行的函数示例:
CREATE FUNCTION new_emp() RETURNS emp AS $$
SELECT text 'None' AS name,
1000.0 AS salary,
25 AS age,
point '(2,2)' AS cubicle;
$$ LANGUAGE SQL;
在此示例中,我们已经指定了每个属性的常量值,但是可以用任何计算替换这些常量。
请注意定义函数的两个重要事项:
-
查询中的选择列表顺序必须与复合类型中列出现的顺序完全相同。(如上所述,对系统来说,列的命名是无关紧要的。)
-
我们必须确保每个表达式的类型可以转换为复合类型的相应列的类型。否则,我们将收到以下错误:
ERROR: return type mismatch in function declared to return emp DETAIL: Final statement returns text instead of point at column 4.与基本类型情况一样,系统不会自动插入显式转换,只会插入隐式或赋值转换。
定义相同函数的另一种方法是:
CREATE FUNCTION new_emp() RETURNS emp AS $$
SELECT ROW('None', 1000.0, 25, '(2,2)')::emp;
$$ LANGUAGE SQL;
在这里,我们编写了一个只返回正确复合类型的单个列的SELECT。在这种情况下,这并不是更好的选择,但在某些情况下,这是一个方便的替代方案,例如,如果我们需要通过调用返回所需复合值的另一个函数来计算结果。另一个例子是,如果我们试图编写一个返回复合域而不是普通复合类型的函数,那么始终需要将其编写为返回单个列,因为没有办法使整个行结果强制转换。
我们可以通过在值表达式中使用它来直接调用此函数:
SELECT new_emp();
new_emp
--------------------------
(None,1000.0,25,"(2,2)")
或通过调用它作为表函数来调用它:
SELECT * FROM new_emp();
name | salary | age | cubicle
------+--------+-----+---------
None | 1000.0 | 25 | (2,2)
第二种方法在将 SQL 函数用作表源中有更详细的描述。
当您使用返回复合类型的函数时,您可能只想要其结果中的一个字段(属性)。您可以使用以下语法实现:
SELECT (new_emp()).name;
name
------
None
需要额外的括号来防止解析器混淆。如果您尝试在没有它们的情况下执行此操作,您将得到类似于以下内容的内容:
SELECT new_emp().name;
ERROR: syntax error at or near "."
LINE 1: SELECT new_emp().name;
^
另一种选择是使用函数符号来提取属性:
SELECT name(new_emp());
name
------
None
字段符号和函数符号是等效的。
使用返回复合类型的函数的另一种方法是将结果传递给接受正确行类型作为输入的另一个函数:
CREATE FUNCTION getname(emp) RETURNS text AS $$
SELECT $1.name;
$$ LANGUAGE SQL;
SELECT getname(new_emp());
getname
---------
None
(1 row)
1.5.4.SQL函数带有输出参数
描述函数结果的另一种方法是使用输出参数定义它,例如:
CREATE FUNCTION add_em (IN x int, IN y int, OUT sum int)
AS 'SELECT x + y'
LANGUAGE SQL;
SELECT add_em(3,7);
add_em
--------
10
(1 row)
这与基本类型的SQL函数中显示的add_em版本没有本质区别。输出参数的真正价值在于它们提供了一种方便的方式来定义返回多个列的函数。例如,
CREATE FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int)
AS 'SELECT x + y, x * y'
LANGUAGE SQL;
SELECT * FROM sum_n_product(11,42);
sum | product
-----+---------
53 | 462
(1 row)
在这里实质上发生的是我们为函数的结果创建了一个匿名复合类型。上面的示例具有与以下示例相同的最终结果:
CREATE TYPE sum_prod AS (sum int, product int);
CREATE FUNCTION sum_n_product (int, int) RETURNS sum_prod
AS 'SELECT $1 + $2, $1 * $2'
LANGUAGE SQL;
但是,不必费心地处理单独的复合类型定义通常很方便。请注意,附加到输出参数的名称不仅仅是装饰,而是确定匿名复合类型的列名。(如果省略输出参数的名称,则系统将自行选择名称。)
请注意,在从SQL调用此类函数时,输出参数不包括在调用参数列表中。这是因为UXDB仅考虑输入参数来定义函数的调用签名。这也意味着仅输入参数在引用函数时很重要,例如删除它。我们可以使用以下任一方法删除上述函数:
DROP FUNCTION sum_n_product (x int, y int, OUT sum int, OUT product int);
DROP FUNCTION sum_n_product (int, int);
参数可以标记为IN(默认),OUT,INOUT或VARIADIC。一个 INOUT参数既可以作为输入参数(调用参数列表的一部分),也可以作为输出参数(结果记录类型的一部分)。VARIADIC参数是输入参数,但是会按照下面描述的特殊方式进行处理。输出参数也可以在过程中使用,但是与函数有些不同。在 CALL命令中,输出参数必须包含在参数列表中。例如,之前的银行账户扣款程序可以这样编写:
1.5.5.带输出参数的SQL过程
过程中也支持输出参数,但它们的工作方式与函数略有不同。在CALL命令中,输出参数必须包含在参数列表中。例如,之前的银行账户借记例程可以写成这样:
CREATE PROCEDURE tp1 (accountno integer, debit numeric, OUT new_balance numeric) AS $$
UPDATE bank
SET balance = balance - debit
WHERE accountno = tp1.accountno
RETURNING balance;
$$ LANGUAGE SQL;
要调用此过程,必须包含与 OUT 参数匹配的参数。通常写成 NULL:
CALL tp1(17, 100.0, NULL);
如果写其他内容,则必须是可以隐式转换为参数声明类型的表达式,就像输入参数一样。但是请注意,这样的表达式不会被评估。在从 PL/uxSQL中调用过程时,必须写一个变量来接收过程的输出,而不是写 NULL。
1.5.6.SQL 函数可以声明接受可变数量的参数
只要所有“可选”参数都是相同的数据类型。可选参数将作为数组传递给函数。通过将最后一个参数标记为VARIADIC 来声明函数;此参数必须声明为数组类型。例如:
CREATE FUNCTION mleast(VARIADIC arr numeric[]) RETURNS numeric AS $$
SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;
SELECT mleast(10, -1, 5, 4.4);
mleast
--------
-1
(1 row)
实际上,在 VARIADIC 位置及其之后的所有实际参数都被收集到一个一维数组中,就好像您写了:
SELECT mleast(ARRAY[10, -1, 5, 4.4]); -- doesn't work
不过,您实际上不能写这个,或者至少它不会匹配此函数定义。标记为VARIADIC的参数匹配其元素类型的一个或多个出现,而不是其自身类型的出现。
有时,能够将已构造的数组传递给可变参数函数是很有用的;当一个可变参数函数想要将其数组参数传递给另一个函数时,这特别方便。此外,这是调用在允许不受信任的用户创建对象的模式中找到的可变参数函数的唯一安全方法。您可以通过在调用中指定VARIADIC 来实现这一点:
SELECT mleast(VARIADIC ARRAY[10, -1, 5, 4.4]);
这可以防止将函数的可变参数扩展为其元素类型的一个或多个出现。元素类型,从而允许数组参数值正常匹配。VARIADIC只能附加到函数调用的最后一个实际参数上。
在调用中指定 VARIADIC 也是将空数组传递给可变函数的唯一方法,例如:
SELECT mleast(VARIADIC ARRAY[]::numeric[]);
仅写 SELECT mleast() 是无效的,因为可变参数必须至少匹配一个实际参数。(如果您想允许这样的调用,可以定义一个没有参数的第二个名为mleast 的函数。)
从可变参数生成的数组元素参数被视为没有自己的名称。这意味着除非指定VARIADIC,否则无法使用命名参数调用可变函数。例如,这将起作用:
SELECT mleast(VARIADIC arr => ARRAY[10, -1, 5, 4.4]);
但这些不起作用:
SELECT mleast(arr => 10);
SELECT mleast(arr => ARRAY[10, -1, 5, 4.4]);
1.5.7.带有参数默认值的 SQL 函数
函数可以声明一些或所有输入参数的默认值。每当函数使用不足够多的实际参数调用时,都会插入默认值。由于参数只能从实际参数列表的末尾省略,因此在具有默认值的参数之后的所有参数也必须具有默认值。(尽管使用命名参数表示法可以允许放宽此限制,但仍然强制执行,以使位置参数表示法起作用。)无论您是否使用它,这种能力都会在调用函数时需要注意预防措施,在某些用户不信任其他用户的数据库中。
例如:
CREATE FUNCTION foo(a int, b int DEFAULT 2, c int DEFAULT 3)
RETURNS int
LANGUAGE SQL
AS $$
SELECT $1 + $2 + $3;
$$;
SELECT foo(10, 20, 30);
foo
-----
60
(1 row)
SELECT foo(10, 20);
foo
-----
33
(1 row)
SELECT foo(10);
foo
-----
15
(1 row)
SELECT foo(); -- fails since there is no default for the first argument
ERROR: function foo() does not exist
等号 = 也可以用于代替关键字 DEFAULT。
1.5.8.将 SQL 函数用作表源
所有 SQL 函数都可以在查询的 FROM子句中使用,但对于返回复合类型的函数特别有用。如果函数被定义为返回基本类型,则表函数生成一个一列表。如果函数被定义为返回复合类型,则表函数为复合类型的每个属性生成一列。
以下是一个示例:
CREATE TABLE foo (fooid int, foosubid int, fooname text);
INSERT INTO foo VALUES (1, 1, 'Joe');
INSERT INTO foo VALUES (1, 2, 'Ed');
INSERT INTO foo VALUES (2, 1, 'Mary');
CREATE FUNCTION getfoo(int) RETURNS foo AS $$
SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;
SELECT *, upper(fooname) FROM getfoo(1) AS t1;
fooid | foosubid | fooname | upper
-------+----------+---------+-------
1 | 1 | Joe | JOE
(1 row)
正如示例所示,我们可以使用函数的列。结果与普通表的列一样。
注意,我们从函数中只得到了一行。这是因为我们没有使用SETOF。这在下一节中描述。
1.5.9.SQL 返回集合的函数
当一个 SQL 函数被声明为返回 SETOF sometype 时,函数的最终查询被完全执行,它输出的每一行都作为结果集的一个元素返回。
这个特性通常在调用函数时在 FROM 子句中使用。在这种情况下,函数返回的每一行都成为查询所看到的表的一行。例如,假设表 foo的内容与上面相同,我们说:
CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$
SELECT * FROM foo WHERE fooid = $1;
$$ LANGUAGE SQL;
SELECT * FROM getfoo(1) AS t1;
然后我们会得到:
fooid | foosubid | fooname
-------+----------+---------
1 | 1 | Joe
1 | 2 | Ed
(2 rows)
也可以返回由输出参数定义的多行多列,如下所示:
CREATE TABLE tab (y int, z int);
INSERT INTO tab VALUES (1, 2), (3, 4), (5, 6), (7, 8);
CREATE FUNCTION sum_n_product_with_tab (x int, OUT sum int, OUT product int)
RETURNS SETOF record
AS $$
SELECT $1 + tab.y, $1 * tab.y FROM tab;
$$ LANGUAGE SQL;
SELECT * FROM sum_n_product_with_tab(10);
sum | product
-----+---------
11 | 10
13 | 30
15 | 50
17 | 70
(4 rows)
关键点在于必须写 RETURNS SETOF record 来表示函数返回多行而不仅仅是一行。如果只有一个输出参数,请写该参数的类型而不是record。
经常有用的是通过多次调用返回集合的函数来构造查询结果,每次调用的参数来自一个表或子查询的连续行。这样做的首选方法是使用 LATERAL关键字。下面是一个使用返回集合的函数枚举树结构元素的示例:
SELECT * FROM nodes;
name | parent
-----------+--------
Top |
Child1 | Top
Child2 | Top
Child3 | Top
SubChild1 | Child1
SubChild2 | Child1
(6 rows)
CREATE FUNCTION listchildren(text) RETURNS SETOF text AS $$
SELECT name FROM nodes WHERE parent = $1
$$ LANGUAGE SQL STABLE;
SELECT * FROM listchildren('Top');
listchildren
--------------
Child1
Child2
Child3
(3 rows)
SELECT name, child FROM nodes, LATERAL listchildren(name) AS child;
name | child
--------+-----------
Top | Child1
Top | Child2
Top | Child3
Child1 | SubChild1
Child1 | SubChild2
(5 rows)
这个例子并没有做什么我们不能用简单的连接做到的事情,但在更复杂的计算中,将一些工作放到函数中的选项可能会非常方便。
返回集合的函数也可以在查询的选择列表中调用。对于查询本身生成的每一行,都会调用返回集合的函数,并为函数结果集的每个元素生成一个输出行。前面的示例也可以使用以下查询完成:
SELECT listchildren('Top');
listchildren
--------------
Child1
Child2
Child3
(3 rows)
SELECT name, listchildren(name) FROM nodes;
name | listchildren
--------+--------------
Top | Child1
Top | Child2
Top | Child3
Child1 | SubChild1
Child1 | SubChild2
(5 rows)
在最后一个SELECT中,注意到对于Child2、Child3等没有输出行。这是因为listchildren对这些参数返回了一个空集,因此没有生成结果行。这与使用LATERAL语法对函数结果进行内部连接时得到的行为相同。
UXDB中,查询选择列表中的返回集函数的行为几乎与将返回集函数写入LATERAL FROM子句项中完全相同。例如,
SELECT x, generate_series(1,5) AS g FROM tab;
几乎等同于
SELECT x, g FROM tab, LATERAL generate_series(1,5) AS g;
它几乎是相同的,除了在这个特定的例子中,规划器可以选择将g放在嵌套循环连接的外部,因为g对tab没有实际的依赖关系。这将导致不同的输出行顺序。选择列表中的返回集函数总是被评估为它们在嵌套循环连接的内部,与FROM子句的其余部分一起运行,因此在考虑FROM子句的下一行之前,函数将被运行到完成。
如果查询选择列表中有多个返回集函数,则行为类似于将函数放入单个LATERAL ROWS FROM(…) FROM子句项中。对于底层查询的每一行,使用第一个函数的第一个结果输出一行,然后使用第二个结果输出一行,依此类推。如果某些返回集函数产生的输出比其他函数少,则用空值替换缺失的数据,以使对于产生最多输出的返回集函数而言,为一个底层行发出的总行数相同。因此,返回集函数运行“同步”,直到它们全部用尽,然后执行继续下一个底层行。
返回集函数可以嵌套在选择列表中,尽管在FROM子句项中不允许这样做。在这种情况下,每个嵌套级别都被单独处理,就像它是一个单独的LATERAL ROWS FROM(…)项一样。例如,在
SELECT srf1(srf2(x), srf3(y)), srf4(srf5(z)) FROM tab;
对于tab的每一行,返回集函数srf2、srf3和srf5将被同步运行,然后srf1和srf4将被应用于下面函数产生的每一行。
返回集函数不能在条件评估结构中使用,例如CASE或COALESCE。例如,考虑
SELECT x, CASE WHEN x > 0 THEN generate_series(1, 5) ELSE 0 END FROM tab;
它可能看起来应该产生x>0的五个输入行的重复,以及那些不是的单个重复;但实际上,因为generate_series(1,5)将在CASE表达式被评估之前在隐式的LATERAL FROM项中运行,它将产生五个输出行。每个输入行的重复。为了减少混淆,这些情况会产生解析时错误。
注意
如果函数的最后一个命令是 INSERT、UPDATE 或 DELETE,并带有RETURNING,那么该命令将始终被执行完成,即使函数未声明为 SETOF 或调用查询未获取所有结果行。由 RETURNING 子句产生的任何额外行都会被静默丢弃,但是所命令的表修改仍然发生(并且在从函数返回之前全部完成)。
注意
在UXDB之前版本中,在同一选择列表中放置多个集合返回函数,除非它们始终产生相等数量的行,否则不会表现得非常明智。否则,您得到的是由集合返回函数产生的行数的最小公倍数的输出行数。此外,嵌套的集合返回函数不起作用,如上所述;相反,一个集合返回函数最多只能有一个集合返回参数,并且每个集合返回函数嵌套都是独立运行的。此外,以前允许条件执行(在 CASE 等中使用集合返回函数),更加复杂。
建议在编写需要在旧版 UXDB 中工作的查询时使用 LATERAL 语法,因为这将在不同版本之间提供一致的结果。如果您有一个查询依赖于集合返回函数的条件执行,则可以通过将条件测试移动到自定义集合返回函数中来修复它。例如,
SELECT x, CASE WHEN y > 0 THEN generate_series(1, z) ELSE 5 END FROM tab;
可以变成
CREATE FUNCTION case_generate_series(cond bool, start int, fin int, els int)
RETURNS SETOF int AS $$
BEGIN
IF cond THEN
RETURN QUERY SELECT generate_series(start, fin);
ELSE
RETURN QUERY SELECT els;
END IF;
END$$ LANGUAGE pluxsql;
SELECT x, case_generate_series(y > 0, 1, z, 5) FROM tab;
这种表述在所有版本的 UXDB 中都是相同的。
1.5.10.SQL 函数返回 TABLE
还有一种将函数声明为返回集合的方法,即使用语法 RETURNS TABLE(columns)。这相当于使用一个或多个 OUT参数加上将函数标记为返回 SETOF record(或适当的单个输出参数类型的 SETOF)。这种符号是在SQL 标准的最新版本中指定的,因此可能比使用 SETOF 更具可移植性。
例如,前面的求和乘积示例也可以这样做:
CREATE FUNCTION sum_n_product_with_tab (x int)
RETURNS TABLE(sum int, product int) AS $$
SELECT $1 + tab.y, $1 * tab.y FROM tab;
$$ LANGUAGE SQL;
不允许在 RETURNS TABLE 符号中使用显式的 OUT 或 INOUT 参数,必须将所有输出列放在 TABLE 列表中。
1.5.11.多态 SQL 函数
可以声明 SQL 函数接受和返回在多态类型中描述的多态类型。这里有一个多态函数make_array,它从两个任意数据类型元素构建一个数组:
CREATE FUNCTION make_array(anyelement, anyelement) RETURNS anyarray AS $$
SELECT ARRAY[$1, $2];
$$ LANGUAGE SQL;
SELECT make_array(1, 2) AS intarray, make_array('a'::text, 'b') AS textarray;
intarray | textarray
----------+-----------
{1,2} | {a,b}
(1 row)
注意使用类型转换 'a'::text 来指定参数的类型为 text。如果参数只是一个字符串字面量,则需要这样做,否则它将被视为类型unknown,而 unknown数组不是一个有效的类型。如果没有类型转换,您将会得到这样的错误:
ERROR: could not determine polymorphic type because input has type unknown
使用上述方式声明的 make_array,必须提供两个完全相同数据类型的参数;系统不会尝试解决任何类型差异。例如,下面的语句不起作用:
SELECT make_array(1, 2.5) AS numericarray;
ERROR: function make_array(integer, numeric) does not exist
另一种方法是使用“通用”多态类型系列,它允许系统尝试识别一个合适的通用类型:
CREATE FUNCTION make_array2(anycompatible, anycompatible)
RETURNS anycompatiblearray AS $$
SELECT ARRAY[$1, $2];
$$ LANGUAGE SQL;
SELECT make_array2(1, 2.5) AS numericarray;
numericarray
--------------
{1,2.5}
(1 row)
由于通用类型解析规则默认选择类型 text,当所有输入都是未知类型时,这也可以工作:
SELECT make_array2('a', 'b') AS textarray;
textarray
-----------
{a,b}
(1 row)
允许具有固定返回类型的多态参数,但反之则不行。例如:
CREATE FUNCTION is_greater(anyelement, anyelement) RETURNS boolean AS $$
SELECT $1 > $2;
$$ LANGUAGE SQL;
SELECT is_greater(1, 2);
is_greater
------------
f
(1 row)
CREATE FUNCTION invalid_func() RETURNS anyelement AS $$
SELECT 1;
$$ LANGUAGE SQL;
ERROR: cannot determine result data type
DETAIL: A result of type anyelement requires at least one input of type anyelement, anyarray, anynonarray, anyenum, or anyrange.
多态性可用于具有输出参数的函数。例如:
CREATE FUNCTION dup (f1 anyelement, OUT f2 anyelement, OUT f3 anyarray)
AS 'select $1, array[$1,$1]' LANGUAGE SQL;
SELECT * FROM dup(22);
f2 | f3
----+---------
22 | {22,22}
(1 row)
多态性也可用于可变函数。例如:
CREATE FUNCTION anyleast (VARIADIC anyarray) RETURNS anyelement AS $$
SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;
SELECT anyleast(10, -1, 5, 4);
anyleast
----------
-1
(1 row)
SELECT anyleast('abc'::text, 'def');
anyleast
----------
abc
(1 row)
CREATE FUNCTION concat_values(text, VARIADIC anyarray) RETURNS text AS $$
SELECT array_to_string($2, $1);
$$ LANGUAGE SQL;
SELECT concat_values('|', 1, 4, 2);
concat_values
---------------
1|4|2
(1 row)
1.5.12.SQL 函数与排序规则
当 SQL 函数有一个或多个可排序数据类型的参数时,每次函数调用都会为每个参数分配一个排序规则,具体取决于实际参数分配的排序规则。如果成功确定了排序规则(即参数之间没有隐式排序规则冲突),则所有可排序参数都被视为隐式具有该排序规则。这将影响函数内部的排序规则敏感操作的行为。例如,使用上面描述的anyleast 函数,以下查询结果将取决于数据库的默认排序规则:
SELECT anyleast('abc'::text, 'ABC');
在 C 区域设置中,结果将是 ABC,但在许多其他区域设置中,结果将是 abc。可以通过在任何参数中添加 COLLATE子句来强制使用排序规则,例如:
SELECT anyleast('abc'::text, 'ABC' COLLATE "C");
或者,如果希望函数在调用时始终使用特定的排序规则,则根据需要在函数定义中插入 COLLATE 子句。以下版本的 anyleast将始终使用 en_US 区域设置来比较字符串:
CREATE FUNCTION anyleast (VARIADIC anyarray) RETURNS anyelement AS $$
SELECT min($1[i] COLLATE "en_US") FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;
但请注意,如果应用于不可排序的数据类型,则会引发错误。
如果实际参数之间无法确定公共排序规则,则 SQL函数将其参数视为其数据类型的默认排序规则(通常是数据库的默认排序规则,但对于域类型的参数可能不同)。
可排序参数的行为可以被视为一种有限的多态形式,仅适用于文本数据类型。
1.6.函数重载
可以定义多个具有相同 SQL名称的函数,只要它们所接受的参数不同。换句话说,函数名称可以进行重载。无论是否使用它,这种能力都需要在调用数据库中的函数时采取安全预防措施,因为某些用户不信任其他用户。当执行查询时,服务器将根据提供的参数的数据类型和数量确定要调用哪个函数。重载还可以用于模拟具有可变数量的参数的函数,最多可以达到有限的最大数量。
在创建一组重载函数时,应该小心不要创建歧义。例如,给定以下函数:
CREATE FUNCTION test(int, real) RETURNS ...
CREATE FUNCTION test(smallint, double precision) RETURNS ...
对于一些简单的输入,如test(1, 1.5),不清楚将调用哪个函数。当前实现的解析规则在类型转换中描述,但是设计一个微妙依赖于此行为的系统是不明智的。
一个接受复合类型的单个参数的函数通常不应该与该类型的任何属性(字段)具有相同的名称。请记住,attribute(table)被视为等同于table.attribute。在复合类型的函数和复合类型的属性之间存在歧义的情况下,将始终使用属性。可以通过模式限定函数名称(即schema.func(table) )来覆盖该选择,但最好通过避免选择冲突的名称来避免问题。
另一个可能的冲突是可变参数和非可变参数函数之间的冲突。例如,可以创建foo(numeric)和foo(VARIADIC numeric[])。在这种情况下,不清楚应该将哪一个匹配到提供单个数值参数的调用,例如foo(10.1)。规则是使用搜索路径中较早出现的函数,或者如果这两个函数在同一个模式中,则优先选择非可变参数函数。
在重载 C 语言函数时,还有一个额外的约束条件:重载函数族中每个函数的 C 名称必须与所有其他函数(内部或动态加载的函数)的 C名称不同。如果违反此规则,则行为不可移植。您可能会得到运行时链接器错误,或者其中一个函数将被调用(通常是内部函数)。SQLCREATE FUNCTION 命令的另一种形式的 AS 子句将 SQL 函数名称与 C 源代码中的函数名称分离。例如:
CREATE FUNCTION test(int) RETURNS int
AS 'filename', 'test_1arg'
LANGUAGE C;
CREATE FUNCTION test(int, int) RETURNS int
AS 'filename', 'test_2arg'
LANGUAGE C;
这里的 C 函数名称反映了许多可能的约定之一。
1.7.函数波动性分类
每个函数都有一个波动性分类,可能是VOLATILE、STABLE或IMMUTABLE。如果CREATE FUNCTION命令没有指定类别,则默认为VOLATILE。波动性类别是对优化器函数行为的承诺:
-
VOLATILE函数可以执行任何操作,包括修改数据库。它可以在具有相同参数的连续调用中返回不同的结果。优化器对这种函数的行为不做任何假设。使用易失性函数的查询将在需要其值的每一行重新评估该函数。 -
STABLE函数不能修改数据库,并保证在单个语句中给定相同参数的所有行中返回相同的结果。此类别允许优化器将多个函数调用优化为单个调用。特别是,在索引扫描条件中使用包含此类函数的表达式是安全的。(由于索引扫描仅在比较值上评估一次,而不是在每一行上评估一次,因此在索引扫描条件中使用VOLATILE函数是无效的。) -
IMMUTABLE函数不能修改数据库,并保证永远在给定相同参数的情况下返回相同的结果。此类别允许优化器在查询使用常量参数调用函数时预先评估该函数。例如,像SELECT... WHERE x = 2 + 2这样的查询可以立即简化为SELECT ... WHERE x= 4,因为整数加法运算符的基础函数被标记为IMMUTABLE。
为获得最佳的优化结果,您应该使用最严格的适用于函数的易变性类别进行标记。
任何具有副作用的函数必须被标记为VOLATILE,以便调用它的语句不能被优化掉。即使一个没有副作用的函数,如果它的值在单个查询中可以改变,也需要被标记为VOLATILE,例如random()、currval()、timeofday()等。
另一个重要的例子是current_timestamp函数族被归类为STABLE,因为它们的值在一个事务中不会改变。
在考虑简单的交互式查询时,STABLE和IMMUTABLE类别之间的差异相对较小,因为这些查询是计划并立即执行的:一个函数在计划期间执行一次或在查询执行启动期间执行一次并没有太大的影响。但是,如果计划被保存并在以后被重用,就会有很大的差异。如果将一个函数标记为IMMUTABLE,而它实际上并不是,可能会导致它在计划期间被过早地折叠为常量,从而在后续使用计划时重复使用过时的值。当使用预处理语句或使用缓存计划的函数语言(如PL/uxSQL)时,这是一个危险。
对于使用SQL或任何标准过程语言编写的函数,易变性类别确定的第二个重要属性是调用函数的SQL命令所做的任何数据更改的可见性。VOLATILE函数将看到这些更改,STABLE或IMMUTABLE函数则不会。这种行为是使用MVCC的快照行为实现的:STABLE和IMMUTABLE函数使用在调用查询开始时建立的快照,而VOLATILE函数在执行每个查询时获取一个新的快照。
注意
使用C编写的函数可以以任何他们想要的方式管理快照,但是让C函数也按照这种方式工作通常是一个好主意。
由于这种快照行为,仅包含SELECT命令的函数可以安全地标记为STABLE,即使它们从可能正在被并发查询修改的表中进行选择。UXDB将使用为调用查询建立的快照执行STABLE函数的所有命令,因此它将在整个查询期间看到数据库的固定视图。
在IMMUTABLE函数中使用SELECT命令时,也使用相同的快照行为。通常不明智的是在IMMUTABLE函数中从数据库表中进行选择,因为如果表内容发生更改,则不可变性将被破坏。但是,UXDB不强制执行这一点。
一个常见的错误是将一个结果依赖于配置参数的函数标记为IMMUTABLE。例如,操作时间戳的函数可能会依赖于TimeZone设置。为了安全起见,这样的函数应该标记为STABLE。
注意
UXDB 要求 STABLE 和 IMMUTABLE 函数除了 SELECT 之外不包含任何 SQL命令,以防止数据修改。(这不是完全可靠的测试,因为这样的函数仍然可以调用修改数据库的VOLATILE 函数。如果这样做,你会发现 STABLE 或 IMMUTABLE函数没有注意到被调用函数应用的数据库更改,因为它们被隐藏在它的快照中。)
1.8.过程语言函数
UXDB允许用户在SQL和C以外的其他语言中编写自定义函数。这些其他语言通称为过程语言(PL)。过程语言并未内置于UXDB服务器中,而是由可加载模块提供。
1.9.内部函数
内部函数是以 C 语言编写的函数,已经静态链接到 UXDB 服务器中。函数定义的“主体”指定了函数的 C语言名称,这个名称不一定与用于 SQL 的名称相同。(出于向后兼容的原因,空主体被接受为C 语言函数名称与 SQL名称相同。)
通常,在数据库集群初始化期间,所有存在于服务器中的内部函数都会被声明,但用户可以使用 CREATE FUNCTION 为内部函数创建其他别名。内部函数在 CREATE FUNCTION 中以语言名称internal 声明。例如,要为 sqrt 函数创建别名:
CREATE FUNCTION square_root(double precision) RETURNS double precision
AS 'dsqrt'
LANGUAGE internal
STRICT;
(大多数内部函数都期望被声明为“严格”。)
注意
并非所有“预定义”函数都具有上述“内部”的含义。有些预定义函数是用SQL编写的。
1.10.C语言函数
用户可以使用 C(或与 C 兼容的语言,如C++)编写自定义函数。这些函数编译为动态可加载对象(也称为共享库),并在需要时由服务器加载。动态加载功能是区分“C语言”函数和“内部”函数的特征,实际编码约定基本相同。因此,标准的内部函数库是用户定义 C 函数的丰富编码示例来源。
目前,C 函数仅使用一种调用约定(“版本 1”)。通过为函数编写 UX_FUNCTION_INFO_V1()宏调用来指示对该调用约定的支持,如下所示。
1.10.1.动态加载
在会话中第一次调用特定可加载对象文件中的用户定义函数时,动态加载器会将该对象文件加载到内存中,以便可以调用该函数。因此,用户定义的C函数的 CREATE FUNCTION 必须为该函数指定两个信息:可加载对象文件的名称和要调用其中特定函数的C名称(链接符号)。如果未显式指定 C 名称,则假定与 SQL 函数名称相同。
根据 CREATE FUNCTION 命令中给定的名称,使用以下算法来定位共享对象文件:
-
如果名称是绝对路径,则加载给定文件。
-
如果名称以字符串
$libdir开头,则该部分将被构建时确定的 UXDB包库目录名称替换。 -
如果名称不包含目录部分,则在由配置变量
dynamic_library_path指定的路径中搜索文件。 -
否则(在路径中未找到文件,或者它包含非绝对目录部分),动态加载器将尝试按原样使用名称,这很可能会失败。(依赖于当前工作目录是不可靠的。)
如果此序列不起作用,则会将特定于平台的共享库文件名扩展名(通常为 .so)附加到给定名称,并再次尝试此序列。如果仍然失败,则加载将失败。
建议将共享库定位在相对于 $libdir 或通过动态库路径。如果新安装位于不同位置,则这简化了版本升级。实际目录可以使用命令ux_config --pkglibdir 找到 $libdir 的位置。
运行 UXDB 服务器的用户 ID 必须能够遍历到要加载的文件的路径。将文件或更高级别的目录设置为不可读和/或不可执行的uxdb 用户是常见的错误。
无论如何,在 CREATE FUNCTION 命令中给定的文件名都会被记录在系统目录中,因此如果需要重新加载文件,则应用相同的过程。
注意
UXDB 不会自动编译 C 函数。必须在引用 CREATE FUNCTION命令中的函数之前编译对象文件。有关更多信息,请参见编译和链接动态加载函数。
为确保动态加载的对象文件不会加载到不兼容的服务器中,UXDB 检查文件是否包含具有适当内容的“魔术块”。这使服务器可以检测明显的不兼容性,例如为不同主要版本的 UXDB 编译的代码。要包含魔术块,请在一个(且仅一个)模块源文件中编写此内容,然后进行编译。包含头文件fmgr.h:
UX_MODULE_MAGIC;
第一次使用后,动态加载的对象文件将保留在内存中。在同一会话中对该文件中的函数的未来调用只会产生符号表查找的小开销。如果需要强制重新加载对象文件,例如在重新编译后,请开始一个新的会话。
可选地,动态加载的文件可以包含一个初始化函数。如果文件包含一个名为 _UX_init的函数,则该函数将在加载文件后立即调用。该函数不接收任何参数,应返回void。目前没有办法卸载动态加载的文件。
1.10.2.C 语言函数中的基本类型
要知道如何编写 C 语言函数,您需要了解 UXDB内部表示基本数据类型的方式以及它们如何在函数之间传递。在内部,UXDB将基本类型视为“内存块”。您定义的针对类型的用户定义函数定义了 UXDB 操作它的方式。也就是说,UXDB只会将数据存储和检索到磁盘,并使用您的用户定义函数来输入、处理和输出数据。
基本类型可以有以下三种内部格式:
-
按值传递,固定长度
-
按引用传递,固定长度
-
按引用传递,可变长度
按值传递的类型只能是长度为 1、2 或 4 个字节(如果您的机器上 sizeof(Datum) 为 8,则也可以是 8个字节)。您应该小心地定义自己的类型,使它们在所有架构上的大小(以字节为单位)相同。例如,long类型是危险的,因为在某些机器上它是 4 个字节,在其他机器上是 8 个字节,而 int 类型在大多数 Unix 机器上是 4 个字节。在Unix 机器上,int4 类型的合理实现可能是:
/* 4-byte integer, passed by value */
typedef int int4;
(实际的 UXDB C 代码将此类型称为 int32,因为在 C 中,intXX 的约定是 XX 位。因此,C 类型int8 的大小为 1 个字节。SQL 类型 int8 在 C 中称为 int64。另请参见内置SQL类型的等效C类型。)
另一方面,任何大小的固定长度类型都可以按引用传递。例如,这是一个示例 UXDB 类型的实现:
/* 16-byte structure, passed by reference */
typedef struct
{
double x, y;
} Point;
只有指向这些类型的指针可以在将它们传递到和从 UXDB 函数中传递它们时使用。要返回这种类型的值,请使用 palloc分配正确数量的内存,填充分配的内存,然后返回指向该内存的指针。并返回一个指向它的指针。(另外,如果你只想返回与一个输入参数相同数据类型的相同值,你可以跳过额外的palloc,直接返回指向输入值的指针。)
最后,所有可变长度类型也必须通过引用传递。所有可变长度类型必须以一个不超过 4 个字节的不透明长度字段开头,该字段将由SET_VARSIZE设置;永远不要直接设置此字段!要存储在该类型中的所有数据必须位于该长度字段紧随其后的内存中。长度字段包含结构的总长度,即它包括长度字段本身的大小。
另一个重要的点是避免在数据类型值中留下任何未初始化的位;例如,要小心地清零可能存在于结构中的任何对齐填充字节。如果没有这样做,逻辑上等价的数据类型常量可能被计划器视为不相等,导致低效(但不是错误的)计划。
警告
永远不要修改传递的引用输入值的内容。如果这样做,您可能会破坏磁盘上的数据,因为您得到的指针可能直接指向磁盘缓冲区。唯一的例外是在用户定义的聚合函数中解释的。
例如,我们可以将类型 text 定义为:
typedef struct {
int32 length;
char data[FLEXIBLE_ARRAY_MEMBER];
} text;
[FLEXIBLE_ARRAY_MEMBER] 表示数据部分的实际长度未在此声明中指定。
在操作可变长度类型时,我们必须小心地分配正确数量的内存并正确设置长度字段。例如,如果我们想在 text 结构中存储 40个字节,我们可以使用以下代码片段:
#include "uxdb.h"
...
char buffer[40]; /* our source data */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
SET_VARSIZE(destination, VARHDRSZ + 40);
memcpy(destination->data, buffer, 40);
...
VARHDRSZ 与 sizeof(int32) 相同,但使用宏 VARHDRSZ引用可变长度类型的开销大小被认为是良好的风格。此外,长度字段必须使用SET_VARSIZE 宏设置,而不是简单的赋值。
内置SQL类型的等效C类型显示了许多内置 SQL 数据类型的 C 类型对应关系。“Defined In”列给出了需要包含以获取类型定义的头文件。(实际定义可能在由列出的文件包含的不同文件中。建议用户坚持使用定义的接口。)请注意,在任何服务器代码源文件中,您应始终首先包含uxdb.h,因为它声明了您需要的许多内容,并且因为先包含其他头文件可能会导致可移植性问题。
内置SQL类型的等效C类型
| SQL 类型 | C 类型 | 定义文件 |
|---|---|---|
boolean | bool | uxdb.h(可能是编译器内建) |
box | BOX* | utils/geo_decls.h |
bytea | bytea* | uxdb.h |
"char" | char | (编译器内建) |
character | BpChar* | uxdb.h |
cid | CommandId | uxdb.h |
date | DateADT | utils/date.h |
smallint (int2) | int16 | uxdb.h |
int2vector | int2vector* | uxdb.h |
integer (int4) | int32 | uxdb.h |
real (float4) | float4* | uxdb.h |
double precision (float8) | float8* | uxdb.h |
interval | Interval* | datatype/timestamp.h |
lseg | LSEG* | utils/geo_decls.h |
name | Name | uxdb.h |
oid | oid | uxdb.h |
oidvector | oidvector* | uxdb.h |
path | PATH* | utils/geo_decls.h |
point | POINT* | utils/geo_decls.h |
regproc | regproc | uxdb.h |
text | text* | uxdb.h |
tid | ItemPointer | storage/itemptr.h |
time | TimeADT | utils/date.h |
time with time zone | TimeTzADT | utils/date.h |
timestamp | Timestamp* | datatype/timestamp.h |
varchar | VarChar* | uxdb.h |
xid | TransactionId | uxdb.h |
现在我们已经讨论了所有基本类型的可能结构,我们可以展示一些真实函数的例子。
1.10.3.版本1调用约定
版本1调用约定依赖于宏来抑制大多数传递参数和结果的复杂性。版本1函数的C声明始终为:
Datum funcname(UX_FUNCTION_ARGS)
此外,宏调用:
UX_FUNCTION_INFO_V1(funcname);
必须出现在同一源文件中。(惯例上,它写在函数本身之前。)对于内部语言函数,不需要此宏调用,因为UXDB假定所有内部函数都使用版本1约定。但是,对于动态加载的函数,它是必需的。
在版本1函数中,每个实际参数都使用与参数数据类型对应的UX_GETARG_xxx()宏获取。(在非严格函数中,需要先使用UX_ARGISNULL()检查参数是否为空;请参见下文。)使用UX_RETURN_xxx()宏返回结果类型的结果。UX_GETARG_xxx()以其参数为要获取的函数参数的编号,其中计数从0开始。UX_RETURN_xxx()以其参数为要返回的实际值。
以下是使用版本1调用约定的一些示例:
#include "uxdb.h"
#include <string.h>
#include "fmgr.h"
#include "utils/geo_decls.h"
UX_MODULE_MAGIC;
/* by value */
UX_FUNCTION_INFO_V1(add_one);
Datum
add_one(UX_FUNCTION_ARGS)
{
int32 arg = UX_GETARG_INT32(0);
UX_RETURN_INT32(arg + 1);
}
/* by reference, fixed length */
UX_FUNCTION_INFO_V1(add_one_float8);
Datum
add_one_float8(UX_FUNCTION_ARGS)
{
/* The macros for FLOAT8 hide its pass-by-reference nature. */
float8 arg = UX_GETARG_FLOAT8(0);
UX_RETURN_FLOAT8(arg + 1.0);
}
UX_FUNCTION_INFO_V1(makepoint);
Datum
makepoint(UX_FUNCTION_ARGS)
{
/* Here, the pass-by-reference nature of Point is not hidden. */
Point *pointx = UX_GETARG_POINT_P(0);
Point *pointy = UX_GETARG_POINT_P(1);
Point *new_point = (Point *) palloc(sizeof(Point));
new_point->x = pointx->x;
new_point->y = pointy->y;
UX_RETURN_POINT_P(new_point);
}
/* by reference, variable length */
UX_FUNCTION_INFO_V1(copytext);
Datum
copytext(UX_FUNCTION_ARGS)
{
text *t = UX_GETARG_TEXT_PP(0);
/*
* VARSIZE_ANY_EXHDR is the size of the struct in bytes, minus the
* VARHDRSZ or VARHDRSZ_SHORT of its header. Construct the copy with a
* full-length header.
*/
text *new_t = (text *) palloc(VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
SET_VARSIZE(new_t, VARSIZE_ANY_EXHDR(t) + VARHDRSZ);
/*
* VARDATA is a pointer to the data region of the new struct. The source
* could be a short datum, so retrieve its data through VARDATA_ANY.
*/
memcpy((void *) VARDATA(new_t), /* destination */
(void *) VARDATA_ANY(t), /* source */
VARSIZE_ANY_EXHDR(t)); /* how many bytes */
UX_RETURN_TEXT_P(new_t);
}
UX_FUNCTION_INFO_V1(concat_text);
Datum
concat_text(UX_FUNCTION_ARGS)
{
text *arg1 = UX_GETARG_TEXT_PP(0);
text *arg2 = UX_GETARG_TEXT_PP(1);
int32 arg1_size = VARSIZE_ANY_EXHDR(arg1);
int32 arg2_size = VARSIZE_ANY_EXHDR(arg2);
int32 new_text_size = arg1_size + arg2_size + VARHDRSZ;
text *new_text = (text *) palloc(new_text_size);
SET_VARSIZE(new_text, new_text_size);
memcpy(VARDATA(new_text), VARDATA_ANY(arg1), arg1_size);
memcpy(VARDATA(new_text) + arg1_size, VARDATA_ANY(arg2), arg2_size);
UX_RETURN_TEXT_P(new_text);
}
假设上述代码已经准备好并编译成共享对象文件 funcs.c,我们可以使用以下命令将函数定义为UXDB:
CREATE FUNCTION add_one(integer) RETURNS integer
AS 'DIRECTORY/funcs', 'add_one'
LANGUAGE C STRICT;
-- note overloading of SQL function name "add_one"
CREATE FUNCTION add_one(double precision) RETURNS double precision
AS 'DIRECTORY/funcs', 'add_one_float8'
LANGUAGE C STRICT;
CREATE FUNCTION makepoint(point, point) RETURNS point
AS 'DIRECTORY/funcs', 'makepoint'
LANGUAGE C STRICT;
CREATE FUNCTION copytext(text) RETURNS text
AS 'DIRECTORY/funcs', 'copytext'
LANGUAGE C STRICT;
CREATE FUNCTION concat_text(text, text) RETURNS text
AS 'DIRECTORY/funcs', 'concat_text'
LANGUAGE C STRICT;
这里,DIRECTORY代表共享库文件的目录(例如 UXDB教程目录,其中包含本节中使用的示例代码)。(更好的风格是在AS子句中仅使用'funcs',在将DIRECTORY添加到搜索路径后。无论如何,我们可以省略共享库的系统特定扩展名,通常为.so。)
请注意,我们已将函数指定为“strict”,这意味着系统应自动假定任何输入值为null时结果也为null。通过这样做,我们避免了在函数代码中检查null输入的必要性。否则,我们必须明确检查null值,使用UX_ARGISNULL()。
宏UX_ARGISNULL(n)允许函数测试每个输入是否为null。(当然,这只在未声明为“strict”的函数中才是必要的。)与UX_GETARG_xxx()宏一样,输入参数从零开始计数。请注意,在验证参数不为null之前,应避免执行UX_GETARG_xxx()。要返回null结果,请执行UX_RETURN_NULL();这适用于严格和非严格函数。
乍一看,版本1的编码约定可能似乎只是与使用普通的C调用约定相比的无意义的晦涩。但是,它们允许我们处理可为NULL的参数/返回值和“toasted”(压缩或离线)值。
版本1接口提供的其他选项是两个变体 UX_GETARG_xxx()宏。这其中的第一个,UX_GETARG_xxx_COPY(),保证返回指定参数的副本,该副本是安全写入。(通常的宏有时会返回指向存储在表中的值的指针,这些值不能被写入。使用UX_GETARG_xxx_COPY()宏可以保证可写的结果。)第二种变体由UX_GETARG_xxx_SLICE()宏组成,它们有三个参数。第一个是函数参数的编号(如上所述)。第二个和第三个是要返回的段的偏移量和长度。偏移量从零开始计数,负长度请求返回值的剩余部分。这些宏提供了更有效的访问大值的部分的方法,如果它们具有存储类型“external”。(列的存储类型可以使用ALTER TABLE tablename ALTER COLUMN colname SET STORAGE storagetype. storagetype指定。storagetype是plain、external、extended或main之一。)
最后,版本1的函数调用约定使得可以返回集合结果(请参见返回集合)并实现触发器函数(请参见触发器)和过程语言调用处理程序。有关详细信息,请参见源分发中的src/backend/utils/fmgr/README。
1.10.4.编写代码
在我们转向更高级的主题之前,我们应该讨论一些用于UXDBC语言函数的编码规则。虽然可能可以将用其他语言编写的函数加载到UXDB中,但这通常很困难(如果可能的话),因为其他语言(如C++、FORTRAN或Pascal)通常不遵循与C相同的调用约定。也就是说,其他语言不会以相同的方式在函数之间传递参数和返回值。因此,我们将假设您的C语言函数实际上是用C编写的。
编写和构建C函数的基本规则如下:
-
使用
ux_config --includedir-server查找您的系统上(或您的用户将要运行的系统上)安装了UXDB服务器头文件的位置。 -
编译和链接您的代码,以便可以将其动态加载到UXDB中,总是需要特殊的标志。有关如何针对您特定的操作系统执行此操作的详细说明,请参见编译和链接动态加载函数。
-
记住为您的共享库定义一个“魔术块”,如动态加载所述。
-
在分配内存时,请使用UXDB函数
palloc和pfree代替相应的C库函数malloc和free。palloc分配的内存将在每个事务结束时自动释放,防止内存泄漏。 -
始终使用
memset将结构体的字节清零(或者一开始就使用palloc0分配)。即使你为结构体的每个字段赋值,也可能存在对齐填充(结构体中的空洞)包含垃圾值的情况。如果没有这个操作,就很难支持哈希索引或哈希连接,因为你必须挑选出数据结构中的有效位来计算哈希。查询规划器有时还依赖通过按位相等比较常量,因此如果逻辑上等价的值不是按位相等的,则可能会得到不良的规划结果。 -
大多数内部UXDB类型都在
uxdb.h中声明,而函数管理器接口(UX_FUNCTION_ARGS等)在fmgr.h中,因此你至少需要包含这两个文件。出于可移植性的原因,最好首先包含uxdb.h,然后再包含任何其他系统或用户头文件。包含uxdb.h还将为你包含elog.h和palloc.h。 -
对象文件中定义的符号名称不能相互冲突,也不能与UXDB服务器可执行文件中定义的符号冲突。如果出现此类错误消息,则必须重命名函数或变量。
1.10.5.编译和链接动态加载函数
在能够使用用C编写的UXDB扩展函数之前,必须以特殊方式编译和链接它们,以生成可以由服务器动态加载的文件。确切地说,需要创建一个共享库。
有关此部分以外的信息,您应该阅读操作系统的文档,特别是C编译器cc和链接编辑器ld的手册页。此外,UXDB源代码包含contrib目录中的几个工作示例。如果您依赖这些示例,您的模块将依赖于UXDB源代码的可用性。
创建共享库通常类似于链接可执行文件:首先将源文件编译为对象文件,然后将对象文件链接在一起。对象文件需要作为位置无关代码(PIC)创建,这在概念上意味着它们可以在加载时放置在内存中的任意位置。(为可执行文件编译的对象文件通常不是这样编译的。)链接共享库的命令包含特殊标志,以区别于链接可执行文件(至少在理论上是这样——在某些系统上,实践要麻烦得多)。
在以下示例中,我们假设您的源代码位于文件foo.c中,并将创建一个共享库。
foo.so。中间的目标文件将被称为foo.o,除非另有说明。共享库可以包含多个目标文件,但我们这里只使用一个。
-
FreeBSD 创建PIC的编译器标志是
-fPIC。要创建共享库,编译器标志是-shared。gcc -fPIC -c foo.c gcc -shared -o foo.so foo.o这适用于FreeBSD的3.0版本。
-
HP-UX
系统编译器创建PIC的编译器标志是
+z。使用GCC时,它是-fPIC。共享库的链接器标志是-b。所以:cc +z -c foo.c或者:
gcc -fPIC -c foo.c然后:
ld -b -o foo.sl foo.oHP-UX使用扩展名
.sl来表示共享库,与大多数其他系统不同。 -
Linux
创建PIC的编译器标志是
-fPIC。创建共享库的编译器标志是-shared。完整的示例如下:cc -fPIC -c foo.c cc -shared -o foo.so foo.o -
macOS
这是一个示例。它假设开发人员工具已安装。
cc -c foo.c cc -bundle -flat_namespace -undefined suppress -o foo.so foo.o -
NetBSD
创建PIC的编译器标志是
-fPIC。对于ELF系统,使用带有标志-shared的编译器来链接共享库。在旧的非ELF系统上,使用ld-Bshareable。gcc -fPIC -c foo.c gcc -shared -o foo.so foo.o -
OpenBSD
创建PIC的编译器标志是
-fPIC。ld -Bshareable用于链接共享库。gcc -fPIC -c foo.c ld -Bshareable -o foo.so foo.o -
Solaris
使用Sun编译器创建PIC的编译器标志是
-KPIC,使用GCC是-fPIC。要链接共享库,编译器选项是-G,任何编译器都可以使用,或者使用GCC的-shared。cc -KPIC -c foo.c cc -G -o foo.so foo.o或者
gcc -fPIC -c foo.c gcc -G -o foo.so foo.o提示
如果这对您来说太复杂了,您应该考虑使用
GNU Libtool,它将平台差异隐藏在统一的接口后面。
然后,生成的共享库文件可以加载到 UXDB 中。在指定文件名时,CREATE FUNCTION 命令必须给出共享库文件的名称,而不是中间对象文件的名称。请注意,系统的标准共享库扩展名(通常为 .so 或 .sl)可以省略 CREATE FUNCTION命令,并且通常应该省略以获得最佳可移植性。
请参阅关于服务器期望找到共享库文件的位置的动态加载。
1.10.6.复合类型参数
复合类型没有像 C 结构一样的固定布局。复合类型的实例可以包含空字段。此外,作为继承层次结构的一部分的复合类型可能与同一继承层次结构的其他成员具有不同的字段。因此,UXDB 提供了一种从 C 中访问复合类型字段的函数接口。
假设我们想编写一个函数来回答查询:
SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';
使用版本 1 的调用约定,我们可以将 c_overpaid 定义为:
#include "uxdb.h"
#include "executor/executor.h" /* for GetAttributeByName() */
UX_MODULE_MAGIC;
UX_FUNCTION_INFO_V1(c_overpaid);
Datum
c_overpaid(UX_FUNCTION_ARGS)
{
HeapTupleHeader t = UX_GETARG_HEAPTUPLEHEADER(0);
int32 limit = UX_GETARG_INT32(1);
bool isnull;
Datum salary;
salary = GetAttributeByName(t, "salary", &isnull);
if (isnull)
UX_RETURN_BOOL(false);
/* Alternatively, we might prefer to do UX_RETURN_NULL() for null salary. */
UX_RETURN_BOOL(DatumGetInt32(salary) > limit);
}
GetAttributeByName 是 UXDB 系统函数,用于从指定行中返回属性。它有三个参数:传递到函数的类型为HeapTupleHeader 的参数、所需属性的名称以及一个返回参数,指示属性是否为空。GetAttributeByName返回一个 Datum 值,您可以使用相应的 DatumGetXXX() 宏将其转换为适当的数据类型。请注意,如果设置了null 标志,则返回值是无意义的;在尝试对结果进行任何操作之前,请始终检查 null 标志。
还有 GetAttributeByNum,它通过列号而不是名称选择目标属性。
以下命令在 SQL 中声明了函数 c_overpaid:
CREATE FUNCTION c_overpaid(emp, integer) RETURNS boolean
AS 'DIRECTORY/funcs', 'c_overpaid'
LANGUAGE C STRICT;
请注意,我们使用了 STRICT,因此不必检查输入参数是否为 NULL。
1.10.7.返回行(复合类型)
要从 C 语言函数返回行或复合类型值,可以使用一个特殊的 API,该 API 提供了宏和函数来隐藏构建复合数据类型的大部分复杂性。要使用此API,源文件必须包括:
#include "funcapi.h"
有两种方法可以构建复合数据值(以下简称“元组”):可以从Datum值的数组构建,也可以从可以传递给元组列数据类型的输入转换函数的 C字符串数组构建。在任一情况下,您首先需要获取或构造一个 TupleDesc 描述符,用于元组结构。当使用 Datum 时,将TupleDesc传递给 BlessTupleDesc,然后为每行调用heap_form_tuple。当使用 C 字符串时,将TupleDesc传递给TupleDescGetAttInMetadata,然后为每行调用BuildTupleFromCStrings。对于返回元组集的函数,设置步骤可以在第一次调用函数时全部完成。
有几个辅助函数可用于设置所需的TupleDesc。在大多数返回复合值的函数中,建议使用以下方式调用:
TypeFuncClass get_call_result_type(FunctionCallInfo fcinfo,
Oid *resultTypeId,
TupleDesc *resultTupleDesc)
传递与调用函数本身传递的相同的fcinfo结构。(这当然要求您使用版本 1 的调用约定。)resultTypeId 可以指定为 NULL或指定为本地变量的地址,以接收函数的结果类型 OID。resultTupleDesc应该是本地 TupleDesc变量的地址。检查结果是否为TYPEFUNC_COMPOSITE;如果是,则resultTupleDesc已填充所需的TupleDesc。(如果不是,则可以报告类似于“在不能接受类型记录的上下文中调用返回记录的函数”这样的错误。)
提示
get_call_result_type可以解析多态函数结果的实际类型;因此,它对于返回标量多态结果的函数非常有用,而不仅仅是对于返回复合类型的函数。resultTypeId输出主要用于返回多态标量的函数。
注意
get_call_result_type有一个类似的函数get_expr_result_type,可以用于解析由表达式树表示的函数调用的预期输出类型。当尝试从函数本身外部确定结果类型时,可以使用此函数。还有一个函数get_func_result_type,当仅可用函数的 OID 时可以使用它。但是,这些函数无法处理声明返回记录的函数,并且get_func_result_type 无法解析多态类型,因此您应优先使用get_call_result_type。
获取 TupleDesc 的旧函数(现已弃用)为:
TupleDesc RelationNameGetTupleDesc(const char *relname)
用于获取命名关系的行类型的 TupleDesc,以及:
TupleDesc TypeGetTupleDesc(Oid typeoid, List *colaliases)
获取基于类型OID的TupleDesc。这可用于获取基本或复合类型的TupleDesc。但是,它不适用于返回record的函数,并且无法解析多态类型。
一旦您拥有TupleDesc,请调用:
TupleDesc BlessTupleDesc(TupleDesc tupdesc)
如果您计划使用Datums,或者:
AttInMetadata *TupleDescGetAttInMetadata(TupleDesc tupdesc)
如果您计划使用C字符串。如果您正在编写返回集的函数,则可以将这些函数的结果保存在FuncCallContext结构中-分别使用tuple_desc或attinmeta字段。
在使用Datums时,请使用:
HeapTuple heap_form_tuple(TupleDesc tupdesc, Datum *values, bool *isnull)
以Datum形式构建HeapTuple,给定用户数据。
在使用C字符串时,请使用:
HeapTuple BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
以C字符串形式构建HeapTuple,给定用户数据。values是C字符串数组,每个返回行的属性都有一个。每个C字符串应以属性数据类型的输入函数所期望的形式出现。为了返回一个属性的空值,应将values数组中的相应指针设置为NULL。需要为每个返回的行再次调用此函数。
一旦您构建了要从函数返回的元组,它必须转换为Datum。使用:
HeapTupleGetDatum(HeapTuple tuple)
将HeapTuple转换为有效的Datum。如果您打算返回单个行,则可以直接返回此Datum,或者它可以用作集返回函数中的当前返回值。
下一节中出现了一个示例。
1.10.8.返回集合
C语言函数有两种返回集(多行)的选项。在称为ValuePerCall模式的一种方法中,将重复调用返回集函数(每次传递相同的参数),并且它在每次调用时返回一个新行,直到它没有更多行可返回并通过返回NULL来发出信号。因此,集返回函数(SRF)必须跨调用保存足够的状态以记住它正在做什么,并在每次调用时返回正确的下一个项目。在称为Materialize模式的另一种方法中,SRF填充并返回包含其整个结果的tuplestore对象;然后仅为整个结果发生一次调用,不需要任何调用状态。
在使用ValuePerCall模式时,重要的是要记住查询不能保证完全运行;也就是说,由于选项(如LIMIT),执行器可能在获取所有行之前停止调用集返回函数。这意味着不能在最后一次调用中执行清理活动,因为可能永远不会发生。建议对需要访问外部资源(例如文件描述符)的函数使用Materialize模式。
本节的其余部分记录了一组常用的(但不需要使用)SRF的帮助程序宏。ValuePerCall 模式。有关 Materialize 模式的更多详细信息,请参见 src/backend/utils/fmgr/README。此外,UXDB 源代码分发中的 contrib 模块包含许多使用 ValuePerCall 和 Materialize 模式的SRF示例。
要使用此处描述的 ValuePerCall 支持宏,请包含 funcapi.h。这些宏与包含需要在调用之间保存的状态的结构 FuncCallContext 一起工作。在调用的 SRF 中,fcinfo->flinfo->fn_extra 用于在调用之间保存指向 FuncCallContext 的指针。宏在第一次使用时自动填充该字段,并期望在后续使用中在那里找到相同的指针。
typedef struct FuncCallContext
{
/*
* 我们之前被调用的次数
*
*
* call_cntr 由 SRF_FIRSTCALL_INIT() 为您初始化为 0,并且每次调用 SRF_RETURN_NEXT() 时为您递增。
*
*/
uint64 call_cntr;
/*
* 可选的最大调用次数
*
* max_calls 仅用于方便起见,设置它是可选的。如果未设置,则必须提供替代方法来知道函数何时完成。
*/
uint64 max_calls;
/*
* 可选的指向杂项用户提供的上下文信息的指针
*
* user_fctx 用作指向自己的数据的指针,以在函数的调用之间保留任意上下文信息。
*/
void *user_fctx;
/*
* 可选的指向包含属性类型输入元数据的结构的指针
*
* attinmeta 用于返回元组(即,复合数据类型),并且在返回基本数据类型时不使用。仅当您打算使用 BuildTupleFromCStrings() 创建返回元组时才需要它。
*/
AttInMetadata *attinmeta;
/*
* 用于必须存在多个调用的结构的内存上下文
*
* multi_call_memory_ctx 由 SRF_FIRSTCALL_INIT() 为您设置,并由 SRF_RETURN_DONE() 用于清理。它是任何要在 SRF 的多个调用之间重复使用的内存的最适当的内存上下文。
*/
MemoryContext multi_call_memory_ctx;
/*
* 可选的指向包含元组描述的结构的指针
*
* tuple_desc 用于返回元组(即,复合数据类型),仅当您打算使用 heap_form_tuple() 而不是 BuildTupleFromCStrings() 构建元组时才需要它。请注意,存储在此处的 TupleDesc 指针通常应首先通过 BlessTupleDesc() 运行。
*/
TupleDesc tuple_desc;
} FuncCallContext;
使用此基础设施的 SRF 要使用的宏是:
SRF_IS_FIRSTCALL()
使用此来确定您的函数是第一次还是后续调用。在第一次调用(仅限)时,调用:
SRF_FIRSTCALL_INIT()
来初始化 FuncCallContext。在每次函数调用中,包括第一次,调用:
SRF_PERCALL_SETUP()
以设置使用 FuncCallContext。
如果您的函数有数据要在当前调用中返回,请使用:
SRF_RETURN_NEXT(funcctx, result)
将其返回给调用者。(result 必须是类型为 Datum 的单个值或按上述方式准备的元组。) 最后,当您的函数完成返回数据时,请使用:
SRF_RETURN_DONE(funcctx)
清理并结束 SRF。
在调用 SRF 时当前的内存上下文是一个瞬态上下文,将在调用之间清除。这意味着您不需要在每个调用上调用 pfree 来释放内存。你使用palloc 分配的内存将会在函数结束时自动释放。但是,如果你想要分配的数据结构在多次调用之间保持存在,你需要将它们放在其他地方。被multi_call_memory_ctx 引用的内存上下文是任何需要在 SRF运行结束之前保留的数据的合适位置。在大多数情况下,这意味着你应该在进行第一次调用设置时切换到multi_call_memory_ctx。使用 funcctx->user_fctx 来保存任何这样的跨调用数据结构的指针。(你在multi_call_memory_ctx 中分配的数据将在查询结束时自动释放,因此不需要手动释放该数据。)
警告
虽然函数的实际参数在每次调用之间保持不变,但是如果你在短暂的上下文中解压缩参数值(通常由 UX_GETARG_xxx宏透明地完成),则解压缩的副本将在每个周期上被释放。因此,如果你在 user_fctx中保留对这些值的引用,你必须在解压缩后将它们复制到 multi_call_memory_ctx中,或者确保你只在该上下文中解压缩这些值。
一个完整的伪代码示例如下:
Datum
my_set_returning_function(UX_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
Datum result;
further declarations as needed
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* One-time setup code appears here: */
user code
if returning composite
build TupleDesc, and perhaps AttInMetadata
endif returning composite
user code
MemoryContextSwitchTo(oldcontext);
}
/* Each-time setup code appears here: */
user code
funcctx = SRF_PERCALL_SETUP();
user code
/* this is just one way we might test whether we are done: */
if (funcctx->call_cntr < funcctx->max_calls)
{
/* Here we want to return another item: */
user code
obtain result Datum
SRF_RETURN_NEXT(funcctx, result);
}
else
{
/* Here we are done returning items, so just report that fact. */
/* (Resist the temptation to put cleanup code here.) */
SRF_RETURN_DONE(funcctx);
}
}
一个返回复合类型的简单 SRF 的完整示例如下:
UX_FUNCTION_INFO_V1(retcomposite);
Datum
retcomposite(UX_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
int call_cntr;
int max_calls;
TupleDesc tupdesc;
AttInMetadata *attinmeta;
/* 仅在函数的第一次调用时执行的操作 */
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
/* 为跨调用持久性创建一个函数上下文 */
funcctx = SRF_FIRSTCALL_INIT();
/* 切换到适合多次函数调用的内存上下文 */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* 要返回的元组的总数 */
funcctx->max_calls = UX_GETARG_INT32(0);
/* 为我们的结果类型构建一个元组描述符 */
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("function returning record called in context "
"that cannot accept type record")));
/*
*
*生成元组描述所需的属性元数据,以便稍后从原始C字符串生成元组
*
*/
attinmeta = TupleDescGetAttInMetadata(tupdesc);
funcctx->attinmeta = attinmeta;
MemoryContextSwitchTo(oldcontext);
}
/* 每次调用函数时执行的操作 */
funcctx = SRF_PERCALL_SETUP();
call_cntr = funcctx->call_cntr;
max_calls = funcctx->max_calls;
attinmeta = funcctx->attinmeta;
if (call_cntr < max_calls) /* 还有更多数据要发送时执行*/
{
char **values;
HeapTuple tuple;
Datum result;
/* 为构建返回的元组准备一个值数组。
这应该是一个C字符串数组,稍后将由类型输入函数处理。*/
values = (char **) palloc(3 * sizeof(char *));
values[0] = (char *) palloc(16 * sizeof(char));
values[1] = (char *) palloc(16 * sizeof(char));
values[2] = (char *) palloc(16 * sizeof(char));
snprintf(values[0], 16, "%d", 1 * UX_GETARG_INT32(1));
snprintf(values[1], 16, "%d", 2 * UX_GETARG_INT32(1));
snprintf(values[2], 16, "%d", 3 * UX_GETARG_INT32(1));
/* 构建元组 */
tuple = BuildTupleFromCStrings(attinmeta, values);
/* 将元组转换为数据 */
result = HeapTupleGetDatum(tuple);
/* 清理(这不是必需的)*/
pfree(values[0]);
pfree(values[1]);
pfree(values[2]);
pfree(values);
SRF_RETURN_NEXT(funcctx, result);
}
else /* 没有更多数据时执行*/
{
SRF_RETURN_DONE(funcctx);
}
}
在 SQL 中声明此函数的一种方法是:
CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);
CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
RETURNS SETOF __retcomposite
AS 'filename', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
另一种方法是使用 OUT 参数:
CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
OUT f1 integer, OUT f2 integer, OUT f3 integer)
RETURNS SETOF record
AS 'filename', 'retcomposite'
LANGUAGE C IMMUTABLE STRICT;
请注意,在此方法中,函数的输出类型在形式上是匿名的 record 类型。
1.10.9.多态参数和返回类型
C 语言函数可以声明为接受和返回多态类型中描述的多态类型。当函数的参数或返回类型被定义为多态类型时,函数作者无法预先知道它将被调用的数据类型,或需要返回的类型。fmgr.h 中提供了两个例程,允许版本 1 的 C 函数发现其参数的实际数据类型和它期望返回的类型。这些例程称为get_fn_expr_rettype(FmgrInfo *flinfo) 和 get_fn_expr_argtype(FmgrInfo*flinfo, int argnum)。它们返回结果或参数类型 OID,如果信息不可用,则返回 InvalidOid。结构体flinfo 通常作为 fcinfo->flinfo 访问。参数 argnum 是从零开始的。get_call_result_type 也可以用作 get_fn_expr_rettype 的替代方法。还有get_fn_expr_variadic,它可以用于查找变长参数是否已合并为数组。这主要对于 VARIADIC "any"函数很有用,因为对于采用普通数组类型的变长函数,这种合并总是会发生。
例如,假设我们想编写一个函数来接受任何类型的单个元素,并返回该类型的一维数组:
UX_FUNCTION_INFO_V1(make_array);
Datum
make_array(UX_FUNCTION_ARGS)
{
ArrayType *result;
Oid element_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
Datum element;
bool isnull;
int16 typlen;
bool typbyval;
char typalign;
int ndims;
int dims[MAXDIM];
int lbs[MAXDIM];
if (!OidIsValid(element_type))
elog(ERROR, "could not determine data type of input");
/* get the provided element, being careful in case it's NULL */
isnull = UX_ARGISNULL(0);
if (isnull)
element = (Datum) 0;
else
element = UX_GETARG_DATUM(0);
/* we have one dimension */
ndims = 1;
/* and one element */
dims[0] = 1;
/* and lower bound is 1 */
lbs[0] = 1;
/* get required info about the element type */
get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);
/* now build the array */
result = construct_md_array(&element, &isnull, ndims, dims, lbs,
element_type, typlen, typbyval, typalign);
UX_RETURN_ARRAYTYPE_P(result);
}
以下命令在 SQL 中声明函数 make_array:
CREATE FUNCTION make_array(anyelement) RETURNS anyarray
AS 'DIRECTORY/funcs', 'make_array'
LANGUAGE C IMMUTABLE;
有一种多态性的变体仅适用于 C 语言函数:它们可以声明为接受类型为 "any" 的参数。(请注意,由于它也是 SQL保留字,因此必须将此类型名称双引号括起来。)这类似于anyelement,但它不会约束不同的 "any"参数为相同的类型,也不会帮助确定函数的结果类型。C语言函数还可以将其最后一个参数声明为 VARIADIC"any"。这将匹配一个或多个任意类型(不一定是相同类型)的实际参数。这些参数将不会像普通的可变参数函数那样被收集到一个数组中;它们将只是单独传递给函数。当使用此功能时,必须使用UX_NARGS() 宏和上述方法来确定实际参数的数量和类型。此类函数的用户可能希望在其函数调用中使用 VARIADIC关键字,并期望函数将数组元素视为单独的参数。如果需要,函数本身必须实现该行为,之后使用get_fn_expr_variadic 来检测实际参数是否标记为 VARIADIC。
1.10.10.共享内存和 LWLocks
插件可以在服务器启动时保留 LWLocks 和共享内存的分配。必须通过在shared_preload_libraries中指定其共享库来预加载插件。共享库应在其 _UX_init 函数中注册一个shmem_request_hook。此shmem_request_hook 可以保留 LWLocks 或共享内存。通过从您的 shmem_request_hook调用以下方法来保留共享内存:
void RequestAddinShmemSpace(int size)
通过从您的 shmem_request_hook 调用以下方法来保留LWLocks:
void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)
这将确保在服务器启动时分配一个数组。num_lwlocks LWLocks 可以使用名称 tranche_name 获得。使用GetNamedLWLockTranche 获取指向此数组的指针。
在 UXDB 源代码树中的 contrib/ux_stat_statements/ux_stat_statements.c中可以找到 shmem_request_hook 的示例。
为避免可能的竞争条件,每个后端在连接和初始化共享内存分配时都应使用 LWLock AddinShmemInitLock,如下所示:
static mystruct *ptr = NULL;
if (!ptr)
{
bool found;
LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
ptr = ShmemInitStruct("my struct name", size, &found);
if (!found)
{
initialize contents of shmem area;
acquire any requested LWLocks using:
ptr->locks = GetNamedLWLockTranche("my tranche name");
}
LWLockRelease(AddinShmemInitLock);
}
1.10.11.使用 C++ 扩展
虽然 UXDB 后端是用 C 写的,但是如果遵循以下准则,可以使用 C++ 编写扩展:
-
所有后端访问的函数必须向后端呈现 C 接口;然后这些 C 函数可以调用 C++ 函数。例如,后端访问的函数需要
extern C链接。对于在后端和 C++ 代码之间传递为指针的任何函数,这也是必需的。 -
使用适当的释放方法释放内存。例如,大多数后端内存使用
palloc()分配,因此使用pfree()进行释放。在这种情况下使用 C++ 的delete将失败。 -
防止异常传播到 C 代码中(在所有
extern C函数的顶层使用 catch-all 块)。即使 C++代码没有显式抛出任何异常,这也是必需的,因为诸如内存不足之类的事件仍然可能引发异常。必须捕获任何异常并将适当的错误传递回C 接口。如果可能,请使用-fno-exceptions编译 C++ 以完全消除异常;在这种情况下,您必须检查您的 C++代码中的失败,例如检查new()返回的 NULL。 -
如果从 C++ 代码调用后端函数,请确保 C++ 调用堆栈仅包含普通数据结构(POD)。这是必需的,因为后端错误会生成远程
longjmp(),它不会正确展开具有非-POD 对象的 C++ 调用堆栈。
总之,最好将 C++ 代码放在一堵与后端接口的 extern C 函数墙后面,并避免异常、内存和调用堆栈泄漏。
1.11.函数优化信息
默认情况下,函数只是数据库系统对其行为知之甚少的“黑盒子”。然而,这意味着使用该函数的查询可能比它们本可以更有效地执行。可以提供额外的知识来帮助规划器优化函数调用。
可以通过在CREATE FUNCTION命令中提供的声明性注释来提供一些基本的信息。其中最重要的是函数的函数波动性分类(IMMUTABLE、STABLE或VOLATILE);在定义函数时,应该始终小心地正确指定这一点。如果希望在并行查询中使用该函数,则还必须指定并行安全属性(PARALLELUNSAFE、PARALLEL RESTRICTED或PARALLEL SAFE)。还可以指定函数的预估执行成本和/或估计返回的行数,但是声明性地指定这两个事实只允许指定一个常量值,这通常是不够的。
还可以将一个规划器支持函数附加到一个 SQL可调用函数(称为其目标函数)上,从而提供关于目标函数的太复杂而无法以声明方式表示的知识。规划器支持函数必须用C 编写(尽管它们的目标函数可能不是),因此这是一个相对较少人使用的高级功能。
规划器支持函数必须具有SQL签名。
supportfn(internal) returns internal
它通过在创建目标函数时指定SUPPORT子句来附加到其目标函数上。
规划器支持函数的 API的详细信息可以在文件src/include/nodes/supportnodes.h中找到。UXDB源代码。在这里,我们仅提供了规划器支持函数可以做什么的概述。可能请求的支持函数集是可扩展的,因此在未来的版本中可能会有更多的功能。
一些函数调用可以根据特定于函数的属性在规划期间简化。例如,int4mul(n, 1)可以简化为n。规划器支持函数可以执行这种类型的转换,方法是实现SupportRequestSimplify请求类型。支持函数将为在查询解析树中找到的其目标函数的每个实例调用。如果它发现特定的调用可以简化为其他形式,它可以构建并返回表示该表达式的解析树。这也会自动适用于基于该函数的运算符 - 在刚才给出的示例中,n * 1也将简化为n。(但请注意,这只是一个例子;标准的UXDB实际上不执行这种特定优化。)我们不能保证UXDB永远不会在支持函数可以简化的情况下调用目标函数。请确保简化表达式与目标函数的实际执行之间具有严格的等价性。
对于返回boolean的目标函数,通常有用的是估计使用该函数的WHERE子句选择的行的分数。这可以通过实现SupportRequestSelectivity请求类型的支持函数来完成。
如果目标函数的运行时间高度依赖于其输入,则为其提供非常数成本估计可能很有用。这可以通过实现SupportRequestCost请求类型的支持函数来完成。
对于返回集的目标函数,通常有用的是提供将返回的行数的非常数估计。这可以通过实现SupportRequestRows请求类型的支持函数来完成。
对于返回boolean的目标函数,可能可以将出现在WHERE中的函数调用转换为可索引的运算符子句或子句。转换后的子句可能完全等价于函数的条件,也可能略弱(即,它们可能接受函数条件不接受的某些值)。在后一种情况下,索引条件被称为有损失的;它仍然可以用于扫描索引,但必须对索引返回的每一行执行函数调用,以查看它是否真正通过了WHERE条件。为创建这样的条件,支持函数必须实现SupportRequestIndexCondition请求类型。
1.12.用户定义的聚合函数
在UXDB中,聚合函数是通过状态值和状态转换函数来定义的。也就是说,聚合函数使用一个状态值,每处理一行输入数据就更新一次该状态值。要定义一个新的聚合函数,需要选择一个数据类型作为状态值,一个状态的初始值,以及一个状态转换函数。状态转换函数接收前一个状态值和当前行的输入值,并返回一个新的状态值。此外,还可以指定一个最终函数,以便在聚合的结果与运行状态值不同时使用。最终函数接收结束状态值,并返回所需的聚合结果。原则上,转换和最终函数只是普通函数,也可以在聚合的上下文之外使用。(实际上,为了性能原因,通常会创建专门的转换函数,只能在作为聚合的一部分调用时才能工作。)
因此,除了聚合的用户可见的参数和结果数据类型之外,还有一个内部状态值数据类型,可能与参数和结果类型都不同。
如果定义一个不使用最终函数的聚合函数,那么就会得到一个计算每行列值的运行函数的聚合函数。例如,sum就是这种聚合函数的一个例子。sum从零开始,每次将当前行的值添加到状态值中。它是一个累加的总和。例如,如果我们想要创建一个针对复数数据类型的sum聚合函数,我们只需要该数据类型的加法函数。聚合定义如下:
CREATE AGGREGATE sum (complex)
(
sfunc = complex_add,
stype = complex,
initcond = '(0,0)'
);
我们可以像这样使用它:
SELECT sum(a) FROM test_complex;
sum
-----------
(34,53.9)
(请注意,我们依赖于函数重载:有多个名为sum的聚合函数,但是UXDB可以确定哪种类型的sum适用于complex类型的列。)
如果没有非空输入值,则sum的上述定义将返回零(初始状态值)。也许在这种情况下我们想返回null——SQL标准期望sum的行为方式是这样的。我们可以通过省略initcond短语来实现这一点,使初始状态值为null。通常情况下,这意味着sfunc需要检查是否有null状态值输入。但是对于sum和其他一些简单的聚合函数,如max和min,如果初始状态值为null并且转换函数被标记为“strict”(即不会为null输入调用),则将第一个非null输入值插入状态变量,然后从第二个非null输入值开始应用转换函数。如果需要在null输入的情况下进行其他操作,请不要将转换函数声明为严格的;相反,编写代码以测试null输入并执行所需操作。
avg(平均值)是聚合函数的一个更复杂的示例。它需要两个运行状态:输入的总和和输入的数量计数。最终结果是通过将这些量相除获得的。平均值通常通过使用数组作为状态值来实现。例如,avg(float8)的内置实现如下所示:
CREATE AGGREGATE avg (float8)
(
sfunc = float8_accum,
stype = float8[],
finalfunc = float8_avg,
initcond = '{0,0,0}'
);
注意
float8_accum需要一个三元素数组,而不仅仅是两个元素,因为它累加了平方和以及输入的总和和计数。这是为了它可以用于一些其他的聚合函数以及avg。
SQL中的聚合函数调用允许DISTINCT和ORDER BY选项,这些选项控制将哪些行提供给聚合的转换函数以及以什么顺序提供。这些选项在幕后实现,不是聚合支持函数的关注点。
1.12.1.移动聚合模式
聚合函数可以选择支持移动聚合模式。移动聚合模式允许在移动窗口起始点内执行聚合函数,从而大大加快执行速度。基本思想是除了正常的“前向”转换函数外,聚合还提供了一个“反向转换函数”,当它们退出窗口框架时,可以从聚合的运行状态值中删除行。例如,使用加法作为前向转换函数的sum聚合将使用减法作为反向转换函数。如果没有反向转换函数,则窗口函数机制必须每次框架起始点移动时从头重新计算聚合,导致运行时间与输入行数乘以平均框架长度成比例。有了反向转换函数,运行时间仅与输入行数成比例。
反向转换函数传递当前状态值和当前状态中包含的最早行的聚合输入值。它必须重建状态值,如果给定的输入行从未被聚合,但仅聚合其后的行。这有时需要前向转换函数保留比普通聚合模式所需的更多状态。因此,移动聚合模式使用与普通模式完全不同的实现:它具有自己的状态数据类型,自己的前向转换函数和自己的最终函数(如果需要)。如果不需要额外的状态,则可以与普通模式的数据类型和函数相同。
例如,我们可以扩展上面给出的sum聚合以支持移动聚合模式:
CREATE AGGREGATE sum (complex)
(
sfunc = complex_add,
stype = complex,
initcond = '(0,0)',
msfunc = complex_add,
minvfunc = complex_sub,
mstype = complex,
minitcond = '(0,0)'
);
以m开头的参数定义了移动聚合实现。除了反向转换函数minvfunc外,它们对应于没有m的普通聚合参数。
移动聚合模式的前向转换函数不允许返回null作为新状态值。如果反向转换函数返回null,则将其视为指示该反向函数无法反转此特定输入的状态计算,因此将从当前框架起始位置重新计算聚合计算。这个约定允许在一些不经常的情况下使用移动聚合模式,这些情况无法从运行状态值中反转出来。反向转换函数可以在这些情况下“放弃”,但只要它可以处理大多数情况,仍然可以获得好处。例如,使用浮点数的聚合可能会选择在必须从运行状态值中删除NaN(不是数字)输入时放弃。
编写移动聚合支持函数时,重要的是要确保反向转换函数可以精确地重建正确的状态值。否则,根据是否使用移动聚合模式,可能会出现用户可见的结果差异。一个聚合的例子,最初似乎很容易添加反向转换函数,但在这种要求无法满足的情况下。sum是对 float4 或 float8 输入进行求和的函数。一个简单的声明 sum(float8) 可以是这样的:
CREATE AGGREGATE unsafe_sum (float8)
(
stype = float8,
sfunc = float8pl,
mstype = float8,
msfunc = float8pl,
minvfunc = float8mi
);
然而,这个聚合函数可能会与没有逆转换函数的情况下产生截然不同的结果。例如,考虑以下查询:
SELECT
unsafe_sum(x) OVER (ORDER BY n ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
FROM (VALUES (1, 1.0e20::float8),
(2, 1.0::float8)) AS v (n,x);
这个查询返回的第二个结果是 0,而不是预期的 1。原因是浮点数值的精度有限:将 1 加到 1e20 上会得到1e20,因此从中减去 1e20 得到的是 0,而不是 1。请注意,这是浮点算术的一般限制,而不是UXDB 的限制。
1.12.2.多态和可变参数聚合函数
聚合函数可以使用多态状态转换函数或最终函数,以便可以使用相同的函数实现多个聚合函数。有关多态函数的说明,请参见多态类型。更进一步,聚合函数本身可以指定多态输入类型和状态类型,以便单个聚合定义可用于多个输入数据类型。以下是多态聚合函数的示例:
CREATE AGGREGATE array_accum (anycompatible)
(
sfunc = array_append,
stype = anycompatiblearray,
initcond = '{}'
);
在这里,任何给定聚合调用的实际状态类型都是具有实际输入类型作为元素的数组类型。聚合的行为是将所有输入连接成该类型的数组。(注意:内置聚合函数array_agg 提供了类似的功能,比这个定义具有更好的性能。)
以下是使用两种不同的实际数据类型作为参数的输出:
SELECT attrelid::regclass, array_accum(attname)
FROM ux_attribute
WHERE attnum > 0 AND attrelid = 'ux_tablespace'::regclass
GROUP BY attrelid;
attrelid | array_accum
---------------+---------------------------------------
ux_tablespace | {spcname,spcowner,spcacl,spcoptions}
(1 row)
SELECT attrelid::regclass, array_accum(atttypid::regtype)
FROM ux_attribute
WHERE attnum > 0 AND attrelid = 'ux_tablespace'::regclass
GROUP BY attrelid;
attrelid | array_accum
---------------+---------------------------
ux_tablespace | {name,oid,aclitem[],text[]}
(1 row)
通常,具有多态结果类型的聚合函数具有多态状态类型,如上例所示。这是必要的,因为否则无法合理地声明最终函数:它需要具有多态结果类型但没有多态参数类型,CREATE FUNCTION将拒绝这样做,理由是无法从调用中推断出结果类型。但有时使用多态状态类型是不方便的。最常见的情况是聚合支持函数要在C中编写,而且状态类型应声明为internal,因为它没有SQL 级别的等效项。为了解决这种情况,可以声明最终函数为带有额外“dummy”参数的函数,这些参数与聚合的输入参数匹配。这些虚拟参数始终作为null值传递,因为在调用最终函数时没有特定的值可用。它们的唯一用途是允许将多态最终函数的结果类型连接到聚合的输入类型。例如,内置聚合array_agg的定义等效于:
CREATE FUNCTION array_agg_transfn(internal, anynonarray)
RETURNS internal ...;
CREATE FUNCTION array_agg_finalfn(internal, anynonarray)
RETURNS anyarray ...;
CREATE AGGREGATE array_agg (anynonarray)
(
sfunc = array_agg_transfn,
stype = internal,
finalfunc = array_agg_finalfn,
finalfunc_extra
);
这里,finalfunc_extra选项指定最终函数除了状态值之外,还接收与聚合的输入参数相对应的额外虚拟参数。额外的anynonarray参数允许声明array_agg_finalfn有效。
通过将最后一个参数声明为VARIADIC数组,聚合函数可以接受可变数量的参数,这与常规函数的方式非常相似;参见函数可以声明接受可变数量的参数。聚合的转换函数必须具有与它们的最后一个参数相同的数组类型。转换函数通常也会被标记为VARIADIC,但这并不是严格要求。
注意
可变聚合在与ORDER BY选项一起使用时容易被误用,因为解析器无法确定在这种组合中是否给出了错误数量的实际参数。请记住,ORDER BY右侧的所有内容都是排序键,而不是聚合的参数。例如,在
SELECT myaggregate(a ORDER BY a, b, c) FROM ...
解析器将其视为单个聚合函数参数和三个排序键。然而,用户可能想要
SELECT myaggregate(a, b, c ORDER BY a) FROM ...
如果myaggregate是可变的,那么这两个调用都可能是完全有效的。
出于同样的原因,在创建具有相同名称和不同数量的常规参数的聚合函数之前,最好三思而后行。
1.12.3.有序集聚合
到目前为止,我们描述的聚合是“正常”聚合。UXDB还支持有序集聚合,它与正常聚合有两个关键区别。首先,除了每个输入行计算一次的普通聚合参数外,有序集聚合可以具有仅在每个聚合操作中计算一次的“直接”参数。其次,普通聚合参数的语法明确指定了它们的排序顺序。有序集聚合通常用于实现依赖于特定行顺序的计算。例如,排序或百分位数等排序方式,因此排序顺序是任何调用的必要方面。例如,内置的percentile_disc定义等效于:
CREATE FUNCTION ordered_set_transition(internal, anyelement)
RETURNS internal ...;
CREATE FUNCTION percentile_disc_final(internal, float8, anyelement)
RETURNS anyelement ...;
CREATE AGGREGATE percentile_disc (float8 ORDER BY anyelement)
(
sfunc = ordered_set_transition,
stype = internal,
finalfunc = percentile_disc_final,
finalfunc_extra
);
此聚合函数接受一个float8直接参数(百分位数分数)和一个可以是任何可排序数据类型的聚合输入。它可以用于像这样获取家庭收入中位数:
SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY income) FROM households;
percentile_disc
-----------------
50489
在这里,0.5是一个直接参数;对于百分位数分数是变化的值,是没有意义的。
与普通聚合不同,有序集聚合的输入行的排序不是在幕后完成,而是由聚合的支持函数负责。典型的实现方法是在聚合的状态值中保留对“元组排序”对象的引用,将传入的行输入到该对象中,然后完成排序并在最终函数中读取数据。此设计允许最终函数执行特殊操作,例如将其他“假设”的行注入要排序的数据中。虽然通常可以使用在PL/uxSQL或其他PL语言中编写的支持函数来实现普通聚合,但有序集聚合通常必须用C编写,因为它们的状态值不能定义为任何SQL数据类型。(在上面的示例中,请注意状态值被声明为类型internal——这是典型的。)此外,由于最终函数执行排序,因此不可能通过再次执行转换函数来继续添加输入行。这意味着最终函数不是READ_ONLY;它必须在CREATE AGGREGATE中声明为READ_WRITE,或者如果其他最终函数调用可以利用已排序的状态,则声明为SHAREABLE。
有序集聚合的状态转换函数接收当前状态值以及每行的聚合输入值,并返回更新后的状态值。这与普通聚合的定义相同,但请注意,不提供直接参数(如果有)。最终函数接收最后的状态值,直接参数的值(如果有),以及(如果指定了finalfunc_extra)对应于聚合输入的空值。与普通聚合一样,finalfunc_extra只有在聚合是多态的情况下才真正有用;然后需要额外的虚拟参数将最终函数的结果类型连接到聚合的输入类型。
目前,有序集聚合不能用作窗口函数,因此它们不需要支持移动聚合模式。
1.12.4.部分聚合
可选地,聚合函数可以支持部分聚合。部分聚合的想法是运行聚合的状态转移函数可以独立地作用于输入数据的不同子集,然后将这些子集产生的状态值组合起来,以产生与在单个操作中扫描所有输入时产生的相同状态值。这种模式可用于并行聚合,通过使不同的工作进程扫描表的不同部分。每个工作进程产生一个部分状态值,最终这些状态值被组合起来产生最终状态值。(将来,该模式也可能用于将本地和远程表的聚合组合起来;但目前尚未实现。)
为了支持部分聚合,聚合定义必须提供一个“组合函数”,它接受聚合状态类型的两个值(表示聚合两个输入行子集的结果),并产生一个新的状态类型值,表示在组合这些行集后聚合后的状态。未指定这两个集合中输入行的相对顺序。这意味着通常无法为对输入行顺序敏感的聚合定义有用地定义组合函数。
作为简单的例子,可以通过将组合函数指定为相同的两个比较函数(用作它们的转换函数)来使MAX和MIN聚合支持部分聚合。SUM聚合只需要一个加法函数作为组合函数。(同样,这与它们的转换函数相同,除非状态值比输入数据类型宽。)
组合函数的处理方式类似于一个转换函数,只是它以状态类型值而不是底层输入类型值作为第二个参数。特别是,处理空值和严格函数的规则是相似的。此外,如果聚合定义指定了非空的initcond,请记住,它将不仅用作每个部分聚合运行的初始状态,而且还用作组合函数的初始状态,该函数将被调用以将每个部分结果组合成该状态。
如果聚合的状态类型声明为“internal”,则组合函数有责任在聚合状态值的正确内存上下文中分配其结果。这意味着特别是当第一个输入为NULL时,简单地返回第二个输入是无效的,因为该值将位于错误的上下文中,并且寿命不足。
当聚合的状态类型声明为“internal”时,通常还适合聚合定义提供“序列化函数”和“反序列化函数”,以允许将这种状态值从一个进程复制到另一个进程。如果没有这些函数,则无法执行并行聚合,并且将来的应用程序,例如本地/远程聚合,可能也无法正常工作。
序列化函数必须接受一个类型为“internal”的单个参数,并返回一个类型为“bytea”的结果,该结果表示将状态值打包成一块平面字节。反之,反序列化函数将反转该转换。它必须接受两个类型为“bytea”和“internal”的参数,并返回类型为“internal”的结果。(第二个参数未使用,始终为零,但出于类型安全原因,它是必需的。)反序列化函数的结果应该只是在当前内存上下文中分配,因为与组合函数的结果不同,它不是长期存在的。
值得注意的是,要使聚合函数并行执行,聚合函数本身必须标记为PARALLEL SAFE。其支持函数上的并行安全标记不会被考虑。
1.12.5.聚合函数的支持函数
C语言编写的函数可以通过调用AggCheckCallContext来检测它是否被调用为聚合函数的支持函数,例如:
if (AggCheckCallContext(fcinfo, NULL))
检查的一个原因是,当它为真时,第一个输入必须是临时状态值,因此可以安全地在原地修改它,而不是分配一个新副本。请参见int8inc()的示例。(虽然聚合过渡函数始终允许修改过渡值,但通常不建议聚合最终函数这样做;如果它们这样做,必须在创建聚合时声明行为。
AggCheckCallContext的第二个参数可用于检索聚合状态值所保存的内存上下文。这对于希望使用“扩展”对象(参见TOAST 考量)作为其状态值的过渡函数非常有用。在第一次调用时,过渡函数应返回一个扩展对象,其内存上下文是聚合状态上下文的子级,然后在后续调用中继续返回相同的扩展对象。请参见array_append()的示例。(array_append()不是任何内置聚合的过渡函数,但它被编写为在作为自定义聚合的过渡函数时行为高效。)
C语言编写的聚合函数还可以使用AggGetAggref支持程序,该程序返回定义聚合调用的Aggref解析节点。这对于有序集聚合非常有用,它们可以检查Aggref节点的子结构,以找出它们应该实现的排序方式。示例可以在UXDB源代码中的orderedsetaggs.c中找到。
1.13.用户定义类型
如UXDB 类型系统所述,UXDB可以扩展以支持新的数据类型。本节介绍如何定义新的基础类型,这些类型是在 SQL 语言层次下定义的数据类型。创建新的基础类型需要使用低级语言(通常是 C)实现操作该类型的函数。
本节中的示例可以在源代码分发包的 src/tutorial 目录下的 complex.sql 和 complex.c 中找到。请参阅该目录中的 README 文件以获取有关运行示例的说明。
用户定义类型必须始终具有输入和输出函数。这些函数确定类型在字符串中的表示方式(用于用户输入和输出)以及类型在内存中的组织方式。输入函数以以空字符结尾的字符串作为其参数,并返回类型的内部(在内存中)表示形式。输出函数以类型的内部表示形式作为参数,并返回以空字符结尾的字符串。如果我们想对该类型进行更多操作而不仅仅是存储它,我们必须提供其他函数来实现我们想要的任何操作。
假设我们想定义一个表示复数的类型 complex。在内存中表示复数的一种自然方式是以下 C 结构体:
typedef struct Complex {
double x;
double y;
} Complex;
我们需要将其设置为按引用传递类型,因为它太大而无法适合单个 Datum 值。
作为类型的外部字符串表示形式,我们选择形如 (x,y) 的字符串。
输入输出函数通常不难写,特别是输出函数。但是在定义类型的外部字符串表示形式时,请记住您最终必须为该表示形式编写一个完整的、健壮的解析器作为您的输入函数。例如:
UX_FUNCTION_INFO_V1(complex_in);
Datum
complex_in(UX_FUNCTION_ARGS)
{
char *str = UX_GETARG_CSTRING(0);
Complex *result;
result = (Complex *) palloc(sizeof(Complex));
if (sscanf(str, " (%lf , %lf )", &result->x, &result->y) != 2)
ereport(ERROR,
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
errmsg("invalid input syntax for complex: \"%s\"",
str)));
UX_RETURN_POINTER(result);
}
以下是输出函数的示例:
UX_FUNCTION_INFO_V1(complex_out);
Datum
complex_out(UX_FUNCTION_ARGS)
{
Complex *complex = (Complex *) UX_GETARG_POINTER(0);
char *result;
result = psprintf("(%g,%g)", complex->x, complex->y);
UX_RETURN_CSTRING(result);
}
你应该小心地使输入和输出函数成为彼此的反函数。如果不这样做,当你需要将数据转储到文件中然后重新读取时,你将遇到严重的问题。当涉及到浮点数时,这是一个特别常见的问题。
可选地,用户定义的类型可以提供二进制输入和输出例程。二进制 I/O 通常比文本 I/O 更快,但可移植性较差。与文本 I/O一样,你需要定义外部二进制表示的确切内容。大多数内置数据类型都试图提供机器无关的二进制表示。对于complex,我们将借助于类型 float8 的二进制 I/O 转换器:
UX_FUNCTION_INFO_V1(complex_recv);
Datum
complex_recv(UX_FUNCTION_ARGS)
{
StringInfo buf = (StringInfo) UX_GETARG_POINTER(0);
Complex *result;
result = (Complex *) palloc(sizeof(Complex));
result->x = uxsql_getmsgfloat8(buf);
result->y = uxsql_getmsgfloat8(buf);
UX_RETURN_POINTER(result);
}
UX_FUNCTION_INFO_V1(complex_send);
Datum
complex_send(UX_FUNCTION_ARGS)
{
Complex *complex = (Complex *) UX_GETARG_POINTER(0);
StringInfoData buf;
uxsql_begintypsend(&buf);
uxsql_sendfloat8(&buf, complex->x);
uxsql_sendfloat8(&buf, complex->y);
UX_RETURN_BYTEA_P(uxsql_endtypsend(&buf));
}
一旦我们编写了 I/O 函数并将它们编译到共享库中,我们就可以在 SQL 中定义 complex 类型。首先,我们将其声明为一个 shell类型:
CREATE TYPE complex;
这充当了一个占位符,允许我们在定义其 I/O 函数时引用该类型。现在我们可以定义 I/O 函数:
CREATE FUNCTION complex_in(cstring)
RETURNS complex
AS 'filename'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION complex_out(complex)
RETURNS cstring
AS 'filename'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION complex_recv(internal)
RETURNS complex
AS 'filename'
LANGUAGE C IMMUTABLE STRICT;
CREATE FUNCTION complex_send(complex)
RETURNS bytea
AS 'filename'
LANGUAGE C IMMUTABLE STRICT;
最后,我们可以提供数据类型的完整定义:
CREATE TYPE complex (
internallength = 16,
input = complex_in,
output = complex_out,
receive = complex_recv,
send = complex_send,
alignment = double
);
当你定义一个新的基本类型时,UXDB会自动提供该类型的数组支持。数组类型通常与基本类型具有相同的名称,前面加下划线字符(_)。
一旦数据类型存在,我们就可以声明其他函数来操作该类型。提供数据类型的有用操作。然后可以在函数之上定义运算符,如果需要,可以创建运算符类来支持数据类型的索引。这些额外的层在以下部分中讨论。如果数据类型的内部表示是可变长度的,则内部表示必须遵循可变长度数据的标准布局:前四个字节必须是一个char[4]字段,该字段永远不会直接访问(通常命名为vl_len_)。您必须使用SET_VARSIZE()宏将数据的总大小(包括长度字段本身)存储在此字段中,并使用VARSIZE()检索它。(这些宏存在,因为长度字段可能根据平台进行编码。)
1.13.1.TOAST 考量
如果您的数据类型的值在大小(内部形式)上有所不同,则通常希望使数据类型可TOAST。即使值始终太小而无法压缩或存储在外部,也应该这样做,因为TOAST也可以通过减少标题开销来节省小数据的空间。
为了支持TOAST存储,操作数据类型的C函数必须始终小心地使用UX_DETOAST_DATUM解包它们所接收到的任何已压缩的值。(通过定义特定于类型的GETARG_DATATYPE_P宏来隐藏此细节。)然后,在运行CREATE TYPE命令时,将内部长度指定为variable,并选择一些适当的存储选项,而不是plain。
如果数据对齐不重要(仅适用于特定函数或因为数据类型已经指定了字节对齐),则可以避免UX_DETOAST_DATUM的一些开销。您可以使用UX_DETOAST_DATUM_PACKED(通过定义GETARG_DATATYPE_PP宏来隐藏)并使用宏VARSIZE_ANY_EXHDR和VARDATA_ANY来访问可能已打包的数据。同样,这些宏返回的数据即使数据类型定义指定对齐方式也不对齐。如果对齐很重要,则必须通过常规的UX_DETOAST_DATUM接口进行操作。
注意
旧代码经常将vl_len_声明为int32字段,而不是 char [4]。只要结构定义的其他字段至少与 int32对齐,就可以。但是,在处理可能未对齐的数据时,使用这样的 struct 定义是危险的; 编译器可能将其作为假定数据实际对齐的许可证,从而导致对对齐严格的体系结构的核心转储。
另一个由TOAST支持的功能是具有比存储在磁盘上的格式更方便的扩展内存数据表示的可能性。常规或“平坦”的varlena存储格式最终只是一堆字节;例如,它不能包含指针,因为它可能会被复制到内存中的其他位置。对于复杂的数据类型,扁平格式可能相当昂贵,因此UXDB提供了一种方法来将扁平格式“扩展”成更适合计算的表示形式,然后在数据类型的函数之间传递该格式的内存。
要使用扩展存储,数据类型必须定义一个遵循src/include/utils/expandeddatum.h中给出的规则的扩展格式,并提供函数将扁平varlena值“扩展”为扩展格式,并将扩展格式“压缩”回常规varlena表示形式。然后确保所有数据类型的C函数都可以接受任一表示形式,可能会在接收后立即将一个转换为另一个。这不需要立即修复所有现有的数据类型函数,因为标准的UX_DETOAST_DATUM宏被定义为将扩展输入转换为常规扁平格式。因此,使用扁平varlena格式工作的现有函数将继续工作,尽管在使用扩展输入时会稍微低效;除非更好的性能很重要,否则不需要将它们转换。
知道如何使用扩展表示的C函数通常分为两类:只能处理扩展格式的函数和可以处理扩展或扁平varlena输入的函数。前者更容易编写,但总体效率可能较低,因为将扁平输入转换为扩展形式以供单个函数使用的成本可能比在扩展格式上操作所节省的成本更高。当只需要处理扩展格式时,可以将扁平输入转换为扩展形式隐藏在参数获取宏中,以便函数看起来不比使用传统varlena输入的函数更复杂。要处理两种类型的输入,请编写一个参数获取函数,该函数将detoast外部、短标题和压缩的varlena输入,但不会扩展输入。这样的函数可以被定义为返回指向扁平varlena格式和扩展格式的联合体的指针。调用者可以使用VARATT_IS_EXPANDED_HEADER()宏来确定它们接收到的格式。
TOAST基础设施不仅允许将常规varlena值与扩展值区分开来,还区分了对扩展值的“读写”和“只读”指针。只需要检查扩展值或仅以安全且非语义可见的方式更改扩展值的C函数不需要关心它们接收到的指针类型。允许生成修改后的输入值的C函数在接收到读写指针时可以原地修改扩展输入值,但如果接收到只读指针,则不能修改输入;在这种情况下,它们必须首先复制该值,生成一个新值以进行修改。构造新的扩展值的C函数应始终返回一个读写指针。此外,正在原地修改读写扩展值的C函数应注意,如果在中途失败,则将该值保持在一个合理的状态。
有关使用扩展值的示例,请参见标准数组基础设施,特别是src/backend/utils/adt/array_expanded.c。
1.14.用户定义的运算符
每个运算符都是对执行实际工作的底层函数的调用的“语法糖”,因此您必须先创建底层函数,然后才能创建运算符。但是,运算符不仅仅是语法糖,因为它携带了额外的信息,有助于查询规划器优化使用运算符的查询。下一节将专门解释这些额外的信息。
UXDB支持前缀和中缀运算符。运算符可以被重载;也就是说,相同的运算符名称可以用于具有不同操作数数量和类型的不同运算符。当执行查询时,系统根据提供的操作数的数量和类型确定要调用的运算符。
以下是创建用于添加两个复数的运算符的示例。我们假设我们已经创建了类型complex的定义(请参见用户定义类型)。首先,我们需要一个执行工作的函数,然后我们可以定义运算符:
CREATE FUNCTION complex_add(complex, complex)
RETURNS complex
AS 'filename', 'complex_add'
LANGUAGE C IMMUTABLE STRICT;
CREATE OPERATOR + (
leftarg = complex,
rightarg = complex,
function = complex_add,
commutator = +
);
现在我们可以执行这样的查询:
SELECT (a + b) AS c FROM test_complex;
c
-----------------
(5.2,6.05)
(133.42,144.95)
我们在这里展示了如何创建二元运算符。要创建前缀运算符,只需省略leftarg。在CREATE OPERATOR中,function子句和参数子句是唯一必需的项目。示例中显示的commutator子句是查询优化器的可选提示。有关commutator和其他优化器提示的更多详细信息,请参见下一节。
1.15.运算符优化信息
一个UXDB运算符定义可以包含多个可选子句,这些子句可以告诉系统有关运算符行为的有用信息。只要适当,就应该提供这些子句,因为它们可以大大加快使用运算符的查询的执行速度。但是,如果您提供了这些子句,必须确保它们是正确的!错误使用优化子句可能导致查询变慢、输出微妙错误或其他不良后果。如果您不确定,可以随时省略优化子句;唯一的后果是查询可能比必要的运行得更慢。
未来版本的UXDB可能会添加其他优化子句。这里描述的是2.1.2.2版本所理解的所有子句。
还可以将一个规划器支持函数附加到支持运算符的函数上,提供另一种告诉系统运算符行为的方法。有关更多信息,请参见函数优化信息。
1.15.1.COMMUTATOR
如果提供COMMUTATOR子句,则指定一个运算符,该运算符是正在定义的运算符的交换子。我们说操作符 A 是操作符 B 的换位子,如果(xAy)对所有可能的输入值 x,y 等于(yBx)。注意,B 也是 A 的换位子。例如,对于一个特定的数据类型,操作符 < 和 > 通常是彼此的换位子,而操作符 + 通常与它自己交换。但是算符-通常不与任何东西交换。
可交换运算符的左操作数类型与其交换子的右操作数类型相同,反之亦然。因此,交换子运算符的名称是UXDB需要查找交换子的全部内容,并且在COMMUTATOR子句中提供的全部内容。
为将用于索引和连接子句的运算符提供交换子信息至关重要,因为这允许查询优化器将这样的子句翻转为不同计划类型所需的形式。例如,考虑具有WHERE子句的查询,例如tab1.x= tab2.y,其中tab1.x和tab2.y是用户定义的类型,并且假设tab2.y已索引。除非它可以确定如何将子句翻转为tab2.y =tab1.x,否则优化器无法生成索引扫描,因为索引扫描机制期望在给定的运算符左侧看到索引列。UXDB不会简单地假设这是有效的转换-运算符的创建者必须指定它是有效的,方法是使用交换子信息标记运算符。
当您定义自交换运算符时,只需这样做。当您定义一对可交换运算符时,情况会有些棘手:第一个定义如何引用另一个尚未定义的运算符?有两种解决方案:
-
一种方法是在您定义的第一个运算符中省略COMMUTATOR子句,然后在第二个运算符的定义中提供一个。由于UXDB知道可交换的运算符成对出现,因此当它看到第二个定义时,它将自动返回并填充第一个定义中缺失COMMUTATOR子句。
-
另一种更直接的方法是在两个定义中都包括COMMUTATOR子句。当UXDB处理第一个定义并意识到COMMUTATOR引用一个不存在的运算符时,系统将在系统目录中创建一个虚拟条目。该虚拟条目仅具有运算符名称、左操作数类型和右操作数类型以及结果类型的有效数据,因为这是UXDB此时可以推断出的全部内容。第一个运算符的目录条目将链接到此虚拟条目。稍后,当您定义第二个运算符时,系统将使用第二个定义的其他信息更新虚拟条目。如果在填充之前尝试使用虚拟运算符,则只会收到错误消息。
1.15.2.NEGATOR
NEGATOR子句(如果提供)命名了作为正在定义的运算符的否定运算符的运算符。我们说运算符A是运算符B的否定运算符,如果两者都返回布尔结果并且(xA y)对于所有可能的输入x,y都等于NOT(x B y)。注意,B也是A的否定词。例如,对于大多数数据类型,< 和 >= 是一个否定词对。运算符永远不能是自己的否定词。
与交换器不同,一对一元运算符可以被标记为彼此的否定词;这意味着(Ax)等于NOT(Bx)对于所有x。
运算符的否定词必须具有与定义运算符相同的左和/或右操作数类型,因此与COMMUTATOR一样,只需在NEGATOR子句中给出运算符名称即可。
提供否定词对查询优化器非常有帮助,因为它允许将NOT(x = y)之类的表达式简化为 x <> y。这比你想象的更常见,因为NOT操作可以作为其他重排的结果插入。
可以使用与上面解释的交换器对相同的方法定义否定词对。
1.15.3.RESTRICT
RESTRICT子句(如果提供)为运算符命名了一个限制选择性估计函数。(请注意,这是一个函数名称,而不是运算符名称。)RESTRICT子句仅对返回布尔值的二元运算符有意义。限制选择性估计器的背后思想是猜测表中有多少行将满足当前运算符和特定常量值的WHERE子句条件:
column OP constant
这通过为优化器提供一些关于将通过这种形式的WHERE子句消除多少行的想法来帮助优化器。(你可能会想知道如果常量在左边会发生什么?好吧,这就是COMMUTATOR的作用之一...)
编写新的限制选择性估计函数远远超出了本章的范围,但幸运的是,您通常可以为自己的许多运算符使用系统的标准估计器之一。这些是标准限制估计器:
eqsel for =
neqsel for <>
scalarltsel for <
scalarlesel for <=
scalargtsel for >
scalargesel for >=
即使它们实际上不是相等或不等,您通常也可以使用eqsel或neqsel来处理具有非常高或非常低选择性的运算符。例如,近似相等的几何运算符使用eqsel,假设它们通常只匹配表中的一小部分条目。
您可以使用scalarltsel,scalarlesel,scalargtsel和scalargesel进行比较,用于可以转换为数字标量以进行范围比较的数据类型。如果可能,请将数据类型添加到函数convert_to_scalar()中理解的那些数据类型中。src/backend/utils/adt/selfuncs.c。(最终,此函数应该被通过ux_type系统目录的一列识别出的每个数据类型函数所替换;但这还没有发生。)如果您不这样做,事情仍然可以工作,但优化器的估计不会像它们本来可以的那样好。
另一个有用的内置选择性估计函数是matchingsel,它将适用于几乎任何二元运算符,如果为输入数据类型收集了标准MCV和/或直方图统计信息。它的默认估计值设置为eqsel中使用的默认估计值的两倍,使其最适合比等式宽松一些的比较运算符。(或者您可以调用底层的generic_restriction_selectivity函数,提供不同的默认估计值。)
在src/backend/utils/adt/geo_selfuncs.c中,还有专为几何运算符设计的其他选择性估计函数:areasel、positionsel和contsel。在撰写本文时,这些只是存根,但您可能仍然想要使用它们(或者更好的是,改进它们)。
1.15.4.JOIN
JOIN子句(如果提供)为运算符命名了一个连接选择性估计函数。(请注意,这是一个函数名称,而不是运算符名称。)JOIN子句仅对返回boolean的二元运算符有意义。连接选择性估计器的想法是猜测一对表中有多少行将满足形如:
table1.column1 OP table2.column2
的WHERE子句条件,对于当前运算符。与RESTRICT子句一样,这通过让优化器找出可能需要最少工作的几个可能的连接序列,从而极大地帮助了优化器。
与之前一样,本章不会尝试解释如何编写连接选择性估计器函数,而只会建议您在适用的情况下使用标准估计器之一:
| 函数 | 对应运算符/用途 |
|---|---|
eqjoinsel | 用于 = (等值连接) |
neqjoinsel | 用于 <> (不等值连接) |
scalarltjoinsel | 用于 < (标量小于) |
scalarlejoinsel | 用于 <= (标量小于等于) |
scalargtjoinsel | 用于 > (标量大于) |
scalargejoinsel | 用于 >= (标量大于等于) |
matchingjoinsel | 用于通用匹配运算符 |
areajoinsel | 用于 2D 基于面积的比较 |
positionjoinsel | 用于 2D 基于位置的比较 |
contjoinsel | 用于 2D 基于包含的比较 |
1.15.5.HASHES
HASHES子句(如果存在)告诉系统可以使用哈希连接方法基于此运算符进行连接。HASHES仅对返回boolean的二元运算符有意义,在实践中,运算符必须表示某些数据类型或数据类型对的相等性。
哈希连接的假设是,连接运算符只能对哈希到相同哈希码的左右值对返回true。如果两个值被放在不同的哈希桶中,连接将根本不会比较它们,隐含地假设连接运算符的结果必须为false。因此,对于不表示某种形式的相等性的运算符,指定 HASHES是没有意义的。在大多数情况下,只有在两侧采用相同数据类型的运算符才实际支持哈希。然而,有时可以为两个或多个数据类型设计兼容的哈希函数;也就是说,生成相同哈希码的“相等”值的函数,即使这些值具有不同的表示形式。例如,当哈希不同宽度的整数时,很容易安排这个属性。
要标记为HASHES,连接运算符必须出现在哈希索引运算符族中。当然,在创建运算符时不会强制执行这一点,因为引用运算符族当然还不存在。但是,如果不存在这样的运算符族,尝试在哈希连接中使用运算符将在运行时失败。系统需要运算符族来查找运算符输入数据类型的数据类型特定哈希函数。当然,在创建运算符族之前,您还必须创建适当的哈希函数。
在准备哈希函数时应该小心,因为有机器相关的方式可能会导致它不能做正确的事情。例如,如果您的数据类型是一个结构,其中可能有不重要的填充位,您不能简单地将整个结构传递给hash_any(除非您编写其他运算符和函数以确保未使用的位始终为零,这是推荐的策略。)另一个例子是,在符合 IEEE浮点标准的机器上,负零和正零是不同的值(不同的位模式),但它们被定义为相等。如果浮点值可能包含负零,则需要额外的步骤来确保它生成与正零相同的哈希值。
哈希连接运算符必须具有可交换性(如果两个操作数数据类型相同,则为其本身,否则为相关的相等运算符),该运算符出现在同一运算符族中。如果不是这种情况,则在使用运算符时可能会出现计划程序错误。此外,对于支持多个数据类型的哈希运算符族,最好(但不是严格要求)为每种数据类型的组合提供相等运算符;这允许更好的优化。
注意
哈希连接运算符的基础函数必须标记为不可变或稳定。如果它是易失性的,则系统永远不会尝试使用该运算符进行哈希连接。
注意
如果哈希连接运算符具有标记为严格的基础函数,则该函数也必须是完整的:也就是说,它应该为任何两个非空输入返回true 或 false,而不是 null。如果不遵循此规则,则 IN 操作的哈希优化可能会生成错误的结果。(具体而言,IN 可能返回false,其中根据标准的正确答案应为 null;或者它可能产生错误,抱怨它没有准备好一个空结果。)
1.15.6.MERGES
MERGES 操作符只对返回boolean的二元操作符有意义,实际上操作符必须表示某种数据类型或一对数据类型的相等性。
合并连接基于将左右表排序的想法,然后并行扫描它们。因此,两种数据类型都必须能够被完全排序,并且连接操作符必须是仅在排序顺序中处于“相同位置”的值对才能成功的操作符。实际上,这意味着连接操作符必须像相等性一样运作。但是,只要它们在逻辑上兼容,就可以合并连接两种不同的数据类型。例如,smallint对 integer 的相等性操作符是可合并连接的。我们只需要排序操作符,将两种数据类型带入逻辑兼容的序列中。
要标记为 MERGES,连接操作符必须出现在 btree索引操作符族的相等成员中。在创建操作符时不强制执行此操作,因为引用操作符族当然还不存在。但是,除非找到匹配的操作符族,否则实际上不会使用操作符进行合并连接。因此,MERGES标志作为提示,告诉规划器值得寻找匹配的操作符族。
可合并连接的操作符必须具有交换律(如果两个操作数数据类型相同,则为其本身,否则为相关的相等性操作符),该操作符出现在同一操作符族中。如果不是这种情况,则在使用操作符时可能会出现规划器错误。此外,对于支持多种数据类型的btree 操作符族,为每种数据类型的组合提供相等性操作符是一个好主意(但不是严格要求);这可以实现更好的优化。
注意
可合并连接操作符的基础函数必须标记为不可变或稳定。如果它是易变的,则系统永远不会尝试使用该操作符进行合并连接。
1.16.将扩展接口连接到索引
到目前为止,所描述的过程让您定义了新类型、新函数和新操作符。但是,我们还不能在新数据类型的列上定义索引。为此,我们必须为新数据类型定义一个操作符类。稍后在本节中,我们将通过一个例子来说明这个概念:一个新的B-tree索引方法的操作符类,它按升序绝对值顺序存储和排序复数。
操作符类可以分组为操作符族,以显示语义兼容类之间的关系。当只涉及单个数据类型时,操作符类就足够了,因此我们将首先关注这种情况,然后再回到操作符族。
1.16.1.索引方法和操作符类
ux_am表包含每个索引方法(在内部称为访问方法)的一行。支持对表的常规访问内置于UXDB中,但所有索引方法都在ux_am中描述。可以向ux_am中添加新的索引方法,但这是高级主题,本节不再讨论。
索引方法的例程不直接了解索引方法将操作的数据类型。相反,一个操作符类别确定索引方法需要使用的操作集,以便使用特定的数据类型进行操作。操作符类别之所以被称为操作符类别,是因为它们指定了可以与索引一起使用的WHERE子句运算符的集合(即可以转换为索引扫描限定符的运算符)。操作符类别还可以指定一些支持函数,这些函数是索引方法的内部操作所需的,但不直接对应于可以与索引一起使用的任何WHERE子句运算符。
可以为同一数据类型和索引方法定义多个操作符类别。通过这样做,可以为单个数据类型定义多组索引语义。例如,B-tree索引需要为其作用的每种数据类型定义排序顺序。对于复数数据类型,可能有一个按复数绝对值排序的B-tree操作符类别,另一个按实部排序的操作符类别,等等。通常,其中一个操作符类别将被认为是最常用的,并被标记为该数据类型和索引方法的默认操作符类别。
相同的操作符类别名称可以用于几种不同的索引方法(例如,B-tree和哈希索引方法都有名为int4_ops的操作符类别),但每个类别都是独立的实体,必须单独定义。
1.16.2.索引方法策略
与操作符类别相关联的操作由“策略号”标识,这些策略号用于在其操作符类别的上下文中标识每个操作的语义。例如,B-tree对键施加严格的排序,从小到大,因此像“小于”和“大于等于”这样的操作符与 B-tree相关。由于 UXDB 允许用户定义操作符,因此 UXDB不能查看操作符的名称(例如,<或>=)并告诉它是什么类型的比较。相反,索引方法定义了一组“策略”,可以将其视为广义操作符。每个操作符类别指定了每个策略对于特定数据类型和索引语义的实际操作符是什么。
B-tree 索引方法定义了五种策略,如B-树策略所示。
B-树策略
| 操作 | 策略号 |
|---|---|
| 小于 | 1 |
| 小于等于 | 2 |
| 等于 | 3 |
| 大于等于 | 4 |
| 大于 | 5 |
哈希索引仅支持相等比较,因此它们仅使用一个策略,如哈希策略所示。
哈希策略
| 操作 | 策略号 |
|---|---|
| 等于 | 1 |
GiST索引更加灵活:它们根本没有固定的策略集。相反,每个特定的GiST操作符类的“一致性”支持例程根据自己的喜好解释策略编号。例如,几个内置的GiST索引操作符类索引二维几何对象,提供了GiST 二维“R-树”策略中所示的“R树”策略。其中四个是真正的二维测试(重叠、相同、包含、被包含);另外四个只考虑X方向;另外四个在Y方向上提供相同的测试。
GiST 二维“R-树”策略
| 操作 | 策略号 |
|---|---|
| 左参数严格地位于右参数的左边 | 1 |
| 左参数不会延伸到右参数的右边 | 2 |
| 重叠 | 3 |
| 左参数不会延伸到右参数的左边 | 4 |
| 左参数严格地位于右参数的右边 | 5 |
| 相同 | 6 |
| 包含 | 7 |
| 被包含 | 8 |
| 不会延伸到高于 | 9 |
| 严格低于 | 10 |
| 严格高于 | 11 |
| 不会延伸到低于 | 12 |
SP-GiST索引与GiST索引类似,具有灵活性:它们没有固定的策略集。相反,每个操作符类的支持例程根据操作符类的定义解释策略编号。例如,内置的点操作符类使用的策略编号如SP-GiST点策略所示。
SP-GiST点策略
| 操作 | 策略号 |
|---|---|
| 左参数严格地位于右参数的左边 | 1 |
| 左参数严格地位于右参数的右边 | 5 |
| 相同 | 6 |
| 被包含 | 8 |
| 严格地低于 | 10 |
| 严格地高于 | 11 |
GIN索引与GiST和SP-GiST索引类似,因为它们也没有固定的策略集。相反,每个操作符类的支持例程根据操作符类的定义解释策略编号。例如,内置的数组操作符类使用的策略编号如GIN 数组策略所示。
GIN 数组策略
| 操作 | 策略编号 |
|---|---|
| overlap | 1 |
| contains | 2 |
| is contained by | 3 |
| equal | 4 |
BRIN 索引与 GiST、SP-GiST 和 GIN索引类似,也没有固定的策略集。相反,每个操作符类的支持例程根据操作符类的定义解释策略编号。例如,内置的Minmax 操作符类使用的策略编号如表BRIN Minmax 策略所示。
BRIN Minmax 策略
| 操作 | 策略编号 |
|---|---|
| 小于 | 1 |
| 小于等于 | 2 |
| 等于 | 3 |
| 大于等于 | 4 |
| 大于 | 5 |
注意,上面列出的所有操作符都返回布尔值。实际上,所有定义为索引方法搜索操作符的操作符必须返回类型为 boolean,因为它们必须出现在WHERE 子句的顶层,以便与索引一起使用。(一些索引访问方法还支持排序操作符,通常不返回布尔值;该功能在排序操作符中讨论。)
1.16.3.索引方法支持例程
策略通常不足以让系统确定如何使用索引。实际上,索引方法需要其他支持例程才能工作。例如,B树索引方法必须能够比较两个键,并确定一个键是否大于、等于或小于另一个键。同样,哈希索引方法必须能够为键值计算哈希码。这些操作不对应于SQL命令中用于限定条件的操作符;它们是索引方法内部使用的管理例程。
与策略一样,操作符类标识了对于给定的数据类型和语义解释,哪些特定函数应该扮演每个角色。索引方法定义了它需要的函数集,操作符类通过将它们分配给索引方法指定的“支持函数编号”来标识要使用的正确函数。
此外,一些操作符类允许用户指定控制它们行为的参数。每个内置索引访问方法都有一个可选的 options支持函数,它定义了一组特定于操作符类的参数。
B 树需要一个比较支持函数,并允许在操作符类作者的选择下提供四个额外的支持函数,如B-Tree支持函数所示。
B-Tree支持函数
| 函数描述 | 支持编号 |
|---|---|
| 比较两个键并返回一个小于零、零或大于零的整数,指示第一个键是否小于、等于或大于第二个键 | 1 |
| 返回 C 可调用排序支持函数的地址(可选) | 2 |
| 将测试值与基值加/减偏移量进行比较,并根据比较结果返回 true 或 false(可选) | 3 |
| 确定是否安全地应用 btree 去重优化的索引使用运算符类(可选) | 4 |
| 定义特定于此运算符类的选项(可选) | 5 |
哈希索引需要一个支持函数,并允许在运算符类作者的选择下提供两个额外的支持函数,如哈希支持函数所示。
哈希支持函数
| 函数描述 | 支持编号 |
|---|---|
| 计算键的 32 位哈希值 | 1 |
| 计算给定 64 位盐的键的 64 位哈希值;如果盐为 0,则结果的低 32 位必须与函数 1 计算的值匹配(可选) | 2 |
| 定义特定于此运算符类的选项(可选) | 3 |
GiST 索引有十一个支持函数,其中六个是可选的,如GiST支持函数所示。
GiST支持函数
| 函数 | 描述 | 支持编号 |
|---|---|---|
consistent | 确定键是否满足查询限定符 | 1 |
union | 计算一组键的并集 | 2 |
compress | 计算要索引的键或值的压缩表示(可选) | 3 |
decompress | 计算压缩键的解压表示(可选) | 4 |
penalty | 计算将新键插入具有给定子树键的子树的惩罚 | 5 |
picksplit | 确定要移动到新页面的页面条目,并计算结果页面的联合键 | 6 |
same | 比较两个键,如果相等则返回 true | 7 |
distance | 确定从键到查询值的距离(可选) | 8 |
fetch | 计算压缩键的原始表示形式,以便进行查询(可选) | 9 |
penalty_reduce | 计算将新键插入具有给定子树键的子树的惩罚的减少量(可选) | 10 |
consistent_multi | 确定键是否满足查询限定符(多个限定符版本)(可选) | 11 |
SP-GiST索引有六个支持函数,其中一个是可选的,如SP-GiST支持函数所示。
SP-GiST支持函数
| 函数 | 描述 | 支持编号 |
|---|---|---|
config | 提供有关操作符类的基本信息 | 1 |
choose | 确定如何将新值插入到内部元组中 | 2 |
picksplit | 确定如何分割一组值 | 3 |
inner_consistent | 确定需要搜索哪些子分区以进行查询 | 4 |
leaf_consistent | 确定键是否满足查询限定符 | 5 |
options | 定义特定于此操作符类的选项(可选) | 6 |
GIN索引有七个支持函数,其中四个是可选的,如GIN支持函数所示。
GIN支持函数
| 函数 | 描述 | 支持编号 |
|---|---|---|
compare | 比较两个键并返回小于零、零或大于零的整数,表示第一个键小于、等于或大于第二个键 | 1 |
extractValue | 从要索引的值中提取键 | 2 |
extractQuery | 从查询条件中提取键 | 3 |
consistent | 确定值是否与查询条件匹配(布尔变量)(如果支持函数 6 存在,则为可选) | 4 |
comparePartial | 比较来自查询的部分键和来自索引的键,并返回小于零、零或大于零的整数(可选) | 5 |
triConsistent | 确定值是否与查询条件匹配(三元变量)(如果支持函数 4 存在,则为可选) | 6 |
options | 定义特定于此操作符类的选项(可选) | 7 |
BRIN索引有五个基本支持函数,其中一个是可选的,如BRIN 支持函数所示。
BRIN 支持函数
| 函数 | 描述 | 支持编号 |
|---|---|---|
opcInfo | 返回描述索引列摘要数据的内部信息 | 1 |
add_value | 向现有摘要索引元组添加新值 | 2 |
consistent | 确定值是否匹配查询条件 | 3 |
union | 计算两个汇总元组的并集 | 4 |
options | 定义特定于此运算符类的选项(可选) | 5 |
与搜索运算符不同,支持函数返回特定索引方法期望的任何数据类型; 例如,对于 B 树的比较函数,返回一个有符号整数。每个支持函数的参数的数量和类型同样依赖于 index 方法。对于 B 树和 hash,比较和散列支持函数采用与操作符类中包含的操作符相同的输入数据类型,但是对于大多数 GiST、 SP-GiST、 GIN 和 BRIN 支持函数来说并非如此。
1.16.4.示例
现在我们已经看到了这些想法,下面是创建一个新的操作符类的例子。(您可以在源代码发行版中的 src/lessons/Complex.c 和 src/lessons/Complex.sql 中找到这个示例的工作副本。)运算符类封装了按绝对值顺序对复杂数字排序的运算符,因此我们选择名称 plex_abs_ops。首先,我们需要一组运算符。定义操作符的过程在用户定义的运算符中讨论过。对于B树上的算子类,我们需要的运算符是:
-
绝对值小于(策略1)
-
绝对值小于或等于(策略2)
-
绝对值相等(策略3)
-
绝对值大于或等于(策略4)
-
绝对值大于(策略5)
定义相关比较运算符的最少出错方式是首先编写B树比较支持函数,然后编写其他函数作为支持函数周围的一行包装器。这降低了在边角情况下获得不一致结果的可能性。按照这种方法,我们首先编写:
#define Mag(c) ((c)->x*(c)->x + (c)->y*(c)->y)
static int
complex_abs_cmp_internal(Complex *a, Complex *b)
{
double amag = Mag(a),
bmag = Mag(b);
if (amag < bmag)
return -1;
if (amag > bmag)
return 1;
return 0;
}
现在小于函数如下所示:
UX_FUNCTION_INFO_V1(complex_abs_lt);
Datum
complex_abs_lt(UX_FUNCTION_ARGS)
{
Complex *a = (Complex *) UX_GETARG_POINTER(0);
Complex *b = (Complex *) UX_GETARG_POINTER(1);
UX_RETURN_BOOL(complex_abs_cmp_internal(a, b) < 0);
}
其他四个函数的区别在于它们如何将内部函数的结果与零进行比较。
接下来,我们根据这些函数声明函数和运算符到 SQL:
CREATE FUNCTION complex_abs_lt(complex, complex) RETURNS bool
AS 'filename', 'complex_abs_lt'
LANGUAGE C IMMUTABLE STRICT;
CREATE OPERATOR < (
leftarg = complex, rightarg = complex, procedure = complex_abs_lt,
commutator = > , negator = >= ,
restrict = scalarltsel, join = scalarltjoinsel
);
正确指定交换律和否定运算符,以及适当的限制和连接选择性函数非常重要,否则优化器将无法有效地使用索引。
还有其他值得注意的事情:
-
只能有一个名为“=”且采用类型“complex”的运算符。在这种情况下,我们没有其他类型为“complex”的运算符“=”,但如果我们构建实际的数据类型,我们可能希望“=”是复数的普通相等操作(而不是绝对值的相等)。在这种情况下,我们需要为complex_abs_eq使用其他运算符名称。
-
虽然UXDB可以处理具有相同SQL名称的函数,只要它们具有不同的参数数据类型,但C只能处理具有给定名称的一个全局函数。因此,我们不应将C函数命名为简单的abs_eq。通常最好在C函数名称中包含数据类型名称,以避免与其他数据类型的函数冲突。
-
我们可以使函数的SQL名称为abs_eq,依靠UXDB通过参数数据类型区分它与同名的任何其他SQL函数。为了使示例简单,我们使函数在C级别和SQL级别具有相同的名称。
下一步是注册B树所需的支持例程。实现此功能的示例C代码位于包含运算符函数的同一文件中。
这是我们声明函数的方式:
CREATE FUNCTION complex_abs_cmp(complex, complex)
RETURNS integer
AS 'filename'
LANGUAGE C IMMUTABLE STRICT;
现在,我们拥有所需的运算符和支持例程,最终可以创建运算符类:
CREATE OPERATOR CLASS complex_abs_ops
DEFAULT FOR TYPE complex USING btree AS
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 complex_abs_cmp(complex, complex);
完成了!现在应该可以在complex列上创建和使用B树索引。
我们可以更详细地编写运算符条目,如下所示:
OPERATOR 1 < (complex, complex) ,
但是,当运算符采用我们正在为其定义运算符类的数据类型时,没有必要这样做。
上面的示例假定您希望将此新运算符类设置为complex数据类型的默认B树运算符类。如果不需要,可以省略单词 DEFAULT。
1.16.5.操作符类和操作符族
到目前为止,我们默认操作符类只处理一种数据类型。虽然在特定的索引列中只能有一种数据类型,但通常有必要对将索引列与不同数据类型的值进行比较的操作进行索引。此外,如果与操作符类相关的交叉数据类型操作符有用,那么通常情况下,其他数据类型也有自己的相关操作符类。明确相关类之间的联系是有帮助的,因为这可以帮助规划器优化SQL 查询(特别是对于 B 树操作符类,因为规划器包含了大量关于如何处理它们的知识)。
为了处理这些需求,UXDB使用了“操作符族”的概念。操作符族包含一个或多个操作符类,还可以包含属于整个族而不属于任何单个类的可索引操作符和相应的支持函数。我们说这样的操作符和函数在族中是“松散的”,而不是绑定到特定的类中。通常,每个操作符类包含单数据类型操作符,而交叉数据类型操作符在族中是松散的。
操作符族中的所有操作符和函数必须具有兼容的语义,兼容性要求由索引方法设置。因此,您可能会想知道为什么要单独指定操作符族的特定子集作为操作符类;实际上,对于许多目的来说,类的划分是无关紧要的,而族是唯一有趣的分组。定义操作符类的原因是它们指定了支持任何特定索引所需的族的数量。如果有一个使用操作符类的索引,则不能删除该操作符类而不删除索引,但是可以删除操作符族的其他部分,即其他操作符类和松散操作符。因此,应指定操作符类以包含最小的一组操作符和函数,这些操作符和函数合理地需要使用特定数据类型的索引,并且可以将相关但非必要的操作符作为操作符族的松散成员添加。
例如,UXDB 有一个内置的 B 树操作符族 integer_ops,其中包括用于 bigint (int8)、integer (int4) 和 smallint (int2) 列的索引的操作符类 int8_ops、int4_ops 和int2_ops。该族还包含交叉数据类型比较操作符,允许比较这些类型中的任意两个,以便可以使用另一种类型的比较值搜索其中一种类型的索引。可以通过以下定义复制该族:
CREATE OPERATOR FAMILY integer_ops USING btree;
CREATE OPERATOR CLASS int8_ops
DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
-- standard int8 comparisons
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 btint8cmp(int8, int8) ,
FUNCTION 2 btint8sortsupport(internal) ,
FUNCTION 3 in_range(int8, int8, int8, boolean, boolean) ,
FUNCTION 4 btequalimage(oid) ;
CREATE OPERATOR CLASS int4_ops
DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
-- standard int4 comparisons
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 btint4cmp(int4, int4) ,
FUNCTION 2 btint4sortsupport(internal) ,
FUNCTION 3 in_range(int4, int4, int4, boolean, boolean) ,
FUNCTION 4 btequalimage(oid) ;
CREATE OPERATOR CLASS int2_ops
DEFAULT FOR TYPE int2 USING btree FAMILY integer_ops AS
-- standard int2 comparisons
OPERATOR 1 < ,
OPERATOR 2 <= ,
OPERATOR 3 = ,
OPERATOR 4 >= ,
OPERATOR 5 > ,
FUNCTION 1 btint2cmp(int2, int2) ,
FUNCTION 2 btint2sortsupport(internal) ,
FUNCTION 3 in_range(int2, int2, int2, boolean, boolean) ,
FUNCTION 4 btequalimage(oid) ;
ALTER OPERATOR FAMILY integer_ops USING btree ADD
-- cross-type comparisons int8 vs int2
OPERATOR 1 < (int8, int2) ,
OPERATOR 2 <= (int8, int2) ,
OPERATOR 3 = (int8, int2) ,
OPERATOR 4 >= (int8, int2) ,
OPERATOR 5 > (int8, int2) ,
FUNCTION 1 btint82cmp(int8, int2) ,
-- cross-type comparisons int8 vs int4
OPERATOR 1 < (int8, int4) ,
OPERATOR 2 <= (int8, int4) ,
OPERATOR 3 = (int8, int4) ,
OPERATOR 4 >= (int8, int4) ,
OPERATOR 5 > (int8, int4) ,
FUNCTION 1 btint84cmp(int8, int4) ,
-- cross-type comparisons int4 vs int2
OPERATOR 1 < (int4, int2) ,
OPERATOR 2 <= (int4, int2) ,
OPERATOR 3 = (int4, int2) ,
OPERATOR 4 >= (int4, int2) ,
OPERATOR 5 > (int4, int2) ,
FUNCTION 1 btint42cmp(int4, int2) ,
-- cross-type comparisons int4 vs int8
OPERATOR 1 < (int4, int8) ,
OPERATOR 2 <= (int4, int8) ,
OPERATOR 3 = (int4, int8) ,
OPERATOR 4 >= (int4, int8) ,
OPERATOR 5 > (int4, int8) ,
FUNCTION 1 btint48cmp(int4, int8) ,
-- cross-type comparisons int2 vs int8
OPERATOR 1 < (int2, int8) ,
OPERATOR 2 <= (int2, int8) ,
OPERATOR 3 = (int2, int8) ,
OPERATOR 4 >= (int2, int8) ,
OPERATOR 5 > (int2, int8) ,
FUNCTION 1 btint28cmp(int2, int8) ,
-- cross-type comparisons int2 vs int4
OPERATOR 1 < (int2, int4) ,
OPERATOR 2 <= (int2, int4) ,
OPERATOR 3 = (int2, int4) ,
OPERATOR 4 >= (int2, int4) ,
OPERATOR 5 > (int2, int4) ,
FUNCTION 1 btint24cmp(int2, int4) ,
-- cross-type in_range functions
FUNCTION 3 in_range(int4, int4, int8, boolean, boolean) ,
FUNCTION 3 in_range(int4, int4, int2, boolean, boolean) ,
FUNCTION 3 in_range(int2, int2, int8, boolean, boolean) ,
FUNCTION 3 in_range(int2, int2, int4, boolean, boolean) ;
请注意,此定义“重载”运算符策略和支持函数编号:每个编号在家族中出现多次。只要每个特定编号的实例具有不同的输入数据类型,就允许这样做。具有两个输入类型都等于运算符类的输入类型的实例是该运算符类的主要运算符和支持函数,并且在大多数情况下应作为运算符类的一部分而不是家族的松散成员声明。
在B树运算符族中,家族中的所有运算符必须具有兼容的排序方式。对于家族中的每个运算符,必须有一个具有与运算符相同的两个输入数据类型的支持函数。建议家族是完整的,即对于每种数据类型的组合,都包括所有运算符。每个运算符类应仅包括其数据类型的非交叉类型运算符和支持函数。
要构建多数据类型哈希运算符族,必须为家族支持的每种数据类型创建兼容的哈希支持函数。在这里,兼容性意味着保证函数对于家族的等式运算符认为相等的任何两个值都返回相同的哈希码,即使这些值是不同类型的。当类型具有不同的物理表示时,通常很难实现这一点,但在某些情况下可以实现。此外,通过在运算符家族中表示的另一个数据类型将值从一个数据类型转换为另一个数据类型,也可以实现兼容性。隐式或二进制强制转换不能改变计算出的哈希值。请注意,每种数据类型只有一个支持函数,而不是每个等式运算符一个。建议一个族完整,即为每种数据类型的每种组合提供一个等式运算符。每个运算符类应该只包括非交叉类型等式运算符和其数据类型的支持函数。
GiST、SP-GiST和GIN索引没有任何明确的跨数据类型操作概念。支持的运算符集只是给定运算符类的主要支持函数能够处理的内容。
在BRIN中,要求取决于提供运算符类的框架。对于基于minmax的运算符类,所需行为与B树运算符族相同:族中的所有运算符必须兼容排序,并且强制转换不能改变相关的排序顺序。
注意
在UXDB之前版本中,没有运算符族的概念,因此任何用于索引的跨数据类型运算符都必须直接绑定到索引的运算符类中。虽然这种方法仍然有效,但已被弃用,因为它使索引的依赖关系过于广泛,并且当两种数据类型具有相同运算符族中的运算符时,规划器可以更有效地处理跨数据类型比较。
1.16.6.操作符类上的系统依赖
UXDB使用运算符类来推断运算符的属性,而不仅仅是它们是否可以与索引一起使用。因此,即使您没有打算为数据类型的任何列创建索引,您可能仍希望创建运算符类。
特别是,有一些SQL功能,如ORDER BY和DISTINCT,需要比较和排序值。要在用户定义的数据类型上实现这些功能,UXDB会查找数据类型的默认B树运算符类。该运算符类的“equals”成员定义了系统对GROUP BY和DISTINCT值的相等性概念,而由运算符类强加的排序顺序定义了默认的ORDER BY排序顺序。
如果没有数据类型的默认B树运算符类,则系统将查找默认哈希运算符类。但是,由于这种类型的运算符类仅提供相等性,因此它只能支持分组而不能支持排序。
当没有数据类型的默认运算符类时,如果尝试使用这些SQL功能与数据类型,则会出现“无法识别排序运算符”的错误。
注意
在之前的UXDB版本中,排序和分组操作将隐式地使用名为=、<和>的运算符。依赖默认运算符类的新行为避免了对具有特定名称的运算符的行为做任何假设。
通过在USING选项中指定类的小于运算符,可以按非默认B树运算符类进行排序,例如:
SELECT * FROM mytable ORDER BY somecol USING ~<~;
或者,通过在USING中指定类的大于运算符,选择降序排序。
用户定义类型的数组比较也依赖于类型默认的B-tree操作符类定义的语义。如果没有默认的B-tree操作符类,但有默认的哈希操作符类,则支持数组相等性,但不支持排序比较。
另一个需要更多数据类型特定知识的SQL特性是窗口函数的RANGE offset PRECEDING/FOLLOWING framing选项。对于如下查询:
SELECT sum(x) OVER (ORDER BY x RANGE BETWEEN 5 PRECEDING AND 10 FOLLOWING)
FROM mytable;
仅仅知道如何按 x 排序是不够的; 数据库还必须了解如何对当前行的 x 值“减5”或“加10”,以确定当前窗口框架的边界。使用定义 ORDER BY排序的 B-tree 操作符类提供的比较操作符,可以将结果边界与其他行的 x 值进行比较--但是加法和减法操作符不属于操作符类,因此应该使用哪些操作符?硬连接这种选择是不可取的,因为不同的排序顺序(不同的 B 树操作符类)可能需要不同的行为。因此,B 树操作符类可以指定一个 in_range支持函数,该函数封装对其排序顺序有意义的加法和减法行为。它甚至可以提供多个in_RANGE支持函数,以防有多个数据类型可以用作 RANGE 子句中的偏移量。如果与窗口的 ORDER BY 子句关联的 B 树运算符类没有匹配的in_RANGE支持函数,则不支持 RANGE 偏移量 PRECEDD/FOLLOWING 选项。
另一个重要的点是,出现在哈希操作符族中的相等运算符是哈希连接、哈希聚合和相关优化的候选项。哈希操作符族在这里是必不可少的,因为它确定要使用的哈希函数。
1.16.7.排序操作符
一些索引访问方法(目前仅有GiST和SP-GiST)支持排序操作符的概念。到目前为止,我们讨论的是搜索操作符。搜索操作符是指可以搜索索引以找到满足WHERE indexed_column operator constant的所有行的操作符。请注意,没有承诺匹配行将以何种顺序返回。相反,排序操作符不限制可以返回的行集,而是确定它们的顺序。排序操作符是指可以扫描索引以按ORDER BY indexed_column operator constant表示的顺序返回行的操作符。定义排序操作符的原因是,如果操作符是测量距离的操作符,则支持最近邻搜索。例如,像这样的查询:
SELECT * FROM places ORDER BY location <-> point '(101,456)' LIMIT 10;
查找距离给定目标点最近的十个地点。由于<->是一个排序运算符,因此在location列上使用GiST索引可以高效地完成此操作。
虽然搜索运算符必须返回布尔结果,但排序运算符通常返回其他类型,例如距离的浮点数或数字类型。此类型通常与正在索引的数据类型不同。为了避免硬编码对不同数据类型行为的假设,需要定义排序运算符来命名指定结果数据类型的排序顺序的B-tree运算符族。如前一节所述,B-tree运算符族定义了UXDB的排序概念,因此这是一种自然的表示。由于点<->运算符返回float8,因此可以在操作符类创建命令中指定它,如下所示:
OPERATOR 15 <-> (point, point) FOR ORDER BY float_ops
其中float_ops是包括float8操作的内置运算符族。此声明说明索引能够按照<->运算符的递增值返回行。
1.16.8.操作符类的特殊功能
有两个操作符类的特殊功能我们还没有讨论,主要是因为它们在最常用的索引方法中没有用处。
通常,将操作符声明为操作符类(或族)的成员意味着索引方法可以使用该操作符检索确切满足WHERE条件的行集。例如:
SELECT * FROM table WHERE integer_column < 4;
可以通过整数列上的B-tree索引确切满足。但是,存在索引作为匹配行的不精确指南的情况。例如,如果GiST索引仅存储几何对象的边界框,则无法精确满足测试非矩形对象(如多边形)之间重叠的WHERE条件。但是,我们可以使用索引查找其边界框与目标对象的边界框重叠的对象,然后仅在索引找到的对象上执行精确重叠测试。如果适用此场景,则称索引对该运算符是“有损”的。有损索引搜索通过使索引方法在可能或可能不真正满足查询条件的行上返回一个recheck标志来实现。然后,核心系统将在检索到的行上测试原始查询条件,以查看是否应将其作为有效匹配返回。如果索引保证返回所有所需行,以及可能的一些其他行,则可以通过执行原始运算符调用来消除这些行。支持有损搜索的索引方法(当前为GiST、SP-GiST和GIN)允许各个操作符类的支持函数设置重新检查标志,因此这基本上是一种操作符类功能。
再次考虑仅在索引中存储复杂对象(如多边形)的边界框的情况。在这种情况下,存储整个多边形在索引条目中没有太多价值,我们可以存储类型为box的简单对象。这种情况在CREATEOPERATOR CLASS中的STORAGE选项中表示:我们可以编写如下内容:
CREATE OPERATOR CLASS polygon_ops
DEFAULT FOR TYPE polygon USING gist AS
...
STORAGE box;
目前,只有GST、SP-GST、GIN和BRIN索引方法支持不同于列数据类型的STORAGE类型。当使用STORAGE时,GST压缩和解压缩支持例程必须处理数据类型转换。当存储类型不同时,SP-GiST同样需要一个压缩支持函数来转换为存储类型; 如果SP-GiST 操作类也支持检索数据,那么反向转换必须由一致函数来处理。在GIN中,STORAGE类型标识“键”值的类型,这种类型通常不同于索引列的类型-例如,整数数组列的运算符类可能有只是整数的键。GIN 提取值和提取查询支持例程负责从索引值中提取键。BRIN 类似于GIN: STORAGE 类型标识存储的汇总值的类型,操作符类的支持过程负责正确解释汇总值。
1.17.将相关对象打包成扩展
一个有用的UXDB扩展通常包括多个 SQL对象;例如,一个新的数据类型将需要新的函数、新的运算符,以及可能的新的索引运算符类。将所有这些对象收集到一个单独的包中可以简化数据库管理。UXDB 将这样的包称为扩展。要定义一个扩展,您至少需要一个包含创建扩展对象的 SQL 命令的脚本文件,以及一个指定扩展本身的一些基本属性的控制文件。如果扩展包括 C代码,则通常还会有一个共享库文件,其中已经构建了 C 代码。一旦您拥有这些文件,一个简单的CREATE EXTENSION命令就可以将对象加载到您的数据库中。
使用扩展的主要优点是,它可以将一堆“松散”的对象打包成一个整体,而不仅仅是运行 SQL 脚本来加载这些对象。将以下文本翻译成中文:将扩展程序的对象加载到您的数据库中后,UXDB 将会理解这些对象是一起的。您可以使用单个DROP EXTENSION命令删除所有对象(无需维护单独的“卸载”脚本)。更有用的是,ux_dump知道它不应该转储扩展程序的单个成员对象,而是只需在转储中包含一个CREATE EXTENSION命令。这大大简化了迁移到可能包含比旧版本更多或不同对象的新版本扩展程序的过程。但是请注意,当将这样的转储加载到新数据库中时,必须可用扩展程序的控制、脚本和其他文件。
UXDB不允许您删除扩展程序中包含的单个对象,除非删除整个扩展程序。此外,虽然您可以更改扩展成员对象的定义(例如,通过CREATE OR REPLACE FUNCTION更改函数),但请记住,ux_dump不会转储修改后的定义。如果要进行此类更改,通常只有在扩展程序的脚本文件中同时进行相同的更改才有意义。(但是,对于包含配置数据的表,有特殊规定,请参见扩展配置表。)在生产环境中,通常最好创建扩展程序更新脚本以执行对扩展程序成员对象的更改。
扩展程序脚本可以使用GRANT和REVOKE语句为扩展程序的对象设置权限。每个对象的最终权限集(如果有设置)将存储在ux_init_privs系统目录中。使用ux_dump时,将在转储中包含CREATE EXTENSION命令,然后是一组GRANT和REVOKE语句,以将对象的权限设置为在转储时的权限。
UXDB 目前不支持扩展程序脚本发出CREATE POLICY或SECURITY LABEL语句。这些语句预计在创建扩展程序后设置。所有扩展程序对象上的RLS策略和安全标签都将包含在ux_dump创建的转储中。
扩展机制还提供了打包修改脚本的规定,这些脚本调整扩展程序中包含的 SQL 对象的定义。例如,如果扩展程序的版本 1.1添加了一个函数并更改了另一个函数的主体,与版本 1.0相比,扩展程序作者可以提供一个更新脚本,仅进行这两个更改。然后可以使用ALTER EXTENSION UPDATE命令应用这些更改,并跟踪实际安装在给定数据库中的扩展程序的版本。
可以成为扩展程序成员的 SQL 对象类型在ALTER EXTENSION的描述中显示。值得注意的是,数据库集群范围的对象,例如数据库、角色和表空间,不能成为扩展程序成员,因为扩展程序仅在一个数据库中可用。(虽然扩展程序脚本不禁止创建这些对象。)
如果这样做,这些对象将不会被跟踪为扩展的一部分。此外,请注意,虽然表可以是扩展的成员,但其索引等子对象不直接被视为扩展的成员。另一个重要的点是,模式可以属于扩展,但反之则不行:扩展本身具有无资格名称,并且不存在于任何模式中。然而,扩展的成员对象将根据其对象类型在适当的情况下属于模式。扩展是否拥有其成员对象所在的模式可能合适,也可能不合适。
如果扩展的脚本创建任何临时对象(例如临时表),那么这些对象将在当前会话的剩余时间内被视为扩展成员,但在会话结束时将自动删除,就像任何临时对象一样。这是扩展成员对象不能被删除而不删除整个扩展的规则的例外。
1.17.1.扩展文件
CREATE EXTENSION命令依赖于每个扩展的控制文件,该文件必须与扩展同名,后缀为.control,并放置在安装的SHAREDIR/extension目录中。还必须至少有一个SQL脚本文件,其遵循以下命名模式--.sql(例如,版本1.0的扩展foo的脚本文件名为foo--1.0.sql)。默认情况下,脚本文件也放置在SHAREDIR/extension目录中;但是控制文件可以指定脚本文件的不同目录。
扩展控制文件的文件格式与UXDB.conf文件相同,即每行一个parameter_name=value赋值的列表。允许空行和以#引入的注释。请确保引用不是单个单词或数字的任何值。
控制文件可以设置以下参数:
directory (string)
包含扩展SQL脚本文件的目录。除非给出绝对路径,否则名称相对于安装的SHAREDIR目录。默认行为相当于指定directory = 'extension'。
default_version (string)
扩展的默认版本(如果未在CREATE EXTENSION中指定版本,则将安装该版本)。虽然可以省略此项,但如果没有VERSION选项,则会导致CREATE EXTENSION失败,因此通常不希望这样做。
comment (string)
关于扩展的注释(任何字符串)。在最初创建扩展时应用注释,但在扩展更新期间不应用注释(因为这可能会覆盖用户添加的注释)。或者,可以通过编写扩展的注释来设置扩展的注释。脚本文件中的COMMENT命令。
encoding (string)
脚本文件使用的字符集编码。如果脚本文件包含任何非ASCII字符,则应指定此参数。否则,文件将被认为是在数据库编码中。
module_pathname (string)
此参数的值将替换脚本文件中每个出现的MODULE_PATHNAME。如果未设置,则不进行替换。通常,将其设置为$libdir/shared_library_name,然后在C语言函数的CREATE FUNCTION命令中使用MODULE_PATHNAME,以便脚本文件不需要硬编码共享库的名称。
requires (string)
此扩展依赖的扩展名称列表,例如requires = 'foo, bar'。这些扩展必须在此扩展安装之前安装。
superuser(boolean)
如果此参数为true(默认值),则只有超级用户才能创建扩展或将其更新到新版本(但请参见下面的trusted)。如果设置为false,则只需要执行安装或更新脚本中的命令所需的权限。如果脚本命令需要超级用户权限,则通常应将其设置为true。(这样的命令无论如何都会失败,但提前给出错误信息更加用户友好。)
trusted(boolean)
如果将此参数设置为true(不是默认值),则允许一些非超级用户安装将superuser设置为true的扩展。具体来说,任何具有当前数据库上CREATE权限的人都将被允许安装。当执行CREATEEXTENSION的用户不是超级用户,但由于此参数而被允许安装时,则安装或更新脚本将作为引导超级用户而不是调用用户运行。如果superuser为false,则此参数无关紧要。通常,不应将此参数设置为扩展,因为这可能允许访问其他仅限超级用户的功能,例如文件系统访问。此外,将扩展标记为可信需要额外的努力来安全地编写扩展的安装和更新脚本;请参见扩展的安全考虑。
relocatable (boolean)
如果可以在扩展初始创建后将其包含的对象移动到不同的模式中,则扩展是可重定位的。默认值为false,即扩展不可重定位。有关更多信息,请参见扩展可重定位性。
schema (string)
该参数仅适用于非可重定位扩展。它强制将扩展加载到指定的模式中,而不是其他任何模式。schema参数仅在初始创建扩展时进行查询,而不是在扩展更新期间进行查询。有关更多信息,请参见扩展可重定位性。
除了主控制文件extension.control之外,扩展还可以具有以extension--version.control命名的辅助控制文件。如果提供了这些文件,则必须将它们放置在脚本文件目录中。辅助控制文件遵循与主控制文件相同的格式。在安装或更新到该扩展的版本时,任何在辅助控制文件中设置的参数都会覆盖主控制文件。但是,directory和default_version参数不能在辅助控制文件中设置。
扩展的SQL脚本文件可以包含任何SQL命令,但不能包含事务控制命令(如BEGIN、COMMIT等)和无法在事务块内执行的命令(如VACUUM)。这是因为脚本文件隐式地在事务块中执行。
扩展的SQL脚本文件还可以包含以\echo开头的行,这些行将被扩展机制忽略(视为注释)。这通常用于在脚本文件被馈送到uxsql而不是通过CREATE EXTENSION加载时抛出错误(请参见扩展示例中的示例脚本)。如果没有这样做,用户可能会意外地将扩展的内容作为“松散”的对象而不是作为扩展来加载,这种情况有点繁琐。
如果扩展脚本包含字符串@extowner@,则该字符串将被替换为调用CREATE EXTENSION或ALTER EXTENSION的用户的名称(适当引用)。通常,此功能由标记为可信的扩展使用,以将所选对象的所有权分配给调用用户而不是引导超级用户。但是,应该小心这样做。例如,将C语言函数的所有权分配给非超级用户将为该用户创建特权升级路径。
虽然脚本文件可以包含指定编码允许的任何字符,但控制文件应仅包含纯ASCII,因为UXDB无法知道控制文件的编码。实际上,只有在扩展的注释中想要使用非ASCII字符时,才会出现这种问题。在这种情况下,建议不要使用控制文件comment参数,而是在脚本文件中使用COMMENT ON EXTENSION设置注释。
1.17.2.扩展可重定位性
用户经常希望将扩展中包含的对象加载到与扩展作者所想的不同模式中。有三个支持的可重定位级别:
-
完全可重定位的扩展可以在加载到数据库后的任何时候移动到另一个模式中。这可以通过
ALTER EXTENSION SET SCHEMA命令完成,该命令会自动将所有成员对象重命名为新模式中的对象。通常,只有在扩展不包含任何关于其对象所在模式的内部假设时,才能实现这一点。此外,扩展的所有对象必须一开始就在一个模式中(忽略不属于任何模式的对象,如过程语言)。通过在其控制文件中设置relocatable = true来标记完全可重定位的扩展。 -
安装期间可能是可重定位的扩展,但之后不是。如果扩展的脚本文件需要明确引用目标模式,例如在为SQL函数设置
search_path属性时,则通常情况下会出现这种情况。对于这样的扩展,在其控制文件中设置relocatable=false,并使用@extschema@来引用脚本文件中的目标模式。在执行脚本之前,所有此字符串的出现都将被实际目标模式的名称替换。用户可以使用CREATE EXTENSION的SCHEMA选项设置目标模式。 -
如果扩展根本不支持重定位,请在其控制文件中设置
relocatable = false,并将schema设置为预期目标模式的名称。这将防止使用CREATE EXTENSION的SCHEMA选项,除非它指定与控制文件中命名的相同模式。如果扩展包含无法通过@extschema@使用替换的模式名称的内部假设,则通常需要进行此选择。在这种情况下,@extschema@替换机制也可用,尽管由控制文件确定模式名称,因此其使用受到限制。
在所有情况下,脚本文件将使用search_path最初设置为指向目标模式;也就是说,CREATE EXTENSION相当于这样做:
SET LOCAL search_path TO @extschema@, ux_temp;
这允许脚本文件创建的对象进入目标模式。如果希望,脚本文件可以更改search_path,但这通常是不可取的。search_path在完成CREATE EXTENSION后恢复为其先前的设置。
如果控制文件中列出了任何先决扩展名,则在search_path的初始设置中添加它们的目标模式,跟随新扩展的目标模式。这允许它们的对象对新扩展的脚本文件可见。为了安全起见,在所有情况下,search_path的末尾都会自动添加ux_temp。
虽然非可重定位扩展可以包含分布在多个模式中的对象,但通常最好将所有用于外部使用的对象放入单个模式中,该模式被视为扩展的目标模式。这种安排与创建依赖扩展时search_path的默认设置方便地配合使用。
1.17.3.扩展配置表
一些扩展包括配置表,其中包含用户在安装扩展后可能添加或更改的数据。通常,如果表是扩展的一部分,则表的定义和内容都不会被ux_dump转储。但是,这种行为对于配置表是不可取的;用户进行的任何数据更改都需要包含在转储中,否则在转储和还原后,扩展的行为将发生变化。
为解决这个问题,扩展的脚本文件可以将其创建的表或序列标记为配置关系,这将导致ux_dump在转储中包括表或序列的内容(而不是其定义)。为此,请在创建表或序列后调用函数ux_extension_config_dump(regclass,text),例如:
CREATE TABLE my_config (key text, value text);
CREATE SEQUENCE my_config_seq;
SELECT ux_catalog.ux_extension_config_dump('my_config', '');
SELECT ux_catalog.ux_extension_config_dump('my_config_seq', '');
可以用这种方式标记任意数量的表或序列。与serial或bigserial列相关联的序列也可以标记。
当ux_extension_config_dump的第二个参数是空字符串时,ux_dump会转储表的全部内容。这通常仅在表最初为空时由扩展脚本创建时才是正确的。如果表中有初始数据和用户提供的数据混合在一起,则ux_extension_config_dump的第二个参数提供了一个WHERE条件,用于选择要转储的数据。例如,您可以执行以下操作:
CREATE TABLE my_config (key text, value text, standard_entry boolean);
SELECT ux_catalog.ux_extension_config_dump('my_config', 'WHERE NOT standard_entry');
然后确保standard_entry仅在扩展脚本创建的行中为true。
对于序列,ux_extension_config_dump的第二个参数没有影响。
更复杂的情况,例如用户可能修改的初始提供的行,可以通过在配置表上创建触发器来处理,以确保修改后的行被正确标记。
您可以通过再次调用ux_extension_config_dump来更改与配置表关联的过滤条件(这通常在扩展更新脚本中很有用)。将表标记为不再是配置表的唯一方法是使用ALTER EXTENSION ... DROP TABLE将其与扩展分离。
请注意,这些表之间的外键关系将决定ux_dump转储表的顺序。具体而言,ux_dump将尝试在引用表之前转储被引用表。由于外键关系是在CREATE EXTENSION时设置的(在引用表之前),因此如果您需要在引用表之前转储引用表,则必须手动更改外键关系。不支持循环依赖关系(在数据加载到表中之前)。当存在循环依赖关系时,数据仍将被转储,但无法直接恢复转储,需要用户进行干预。
与 serial 或 bigserial 列相关联的序列需要直接标记以转储其状态。仅标记其父关系不足以实现此目的。
1.17.4.扩展更新
扩展机制的一个优点是它提供了方便的方法来管理定义扩展对象的 SQL命令的更新。这是通过将版本名称或号码与每个发布的扩展安装脚本的版本相关联来完成的。此外,如果您希望用户能够动态地从一个版本更新到下一个版本,您应该提供更新脚本,以进行必要的更改。更新脚本的名称遵循以下模式:extension--old_version--target_version.sql(例如,foo--1.0--1.1.sql包含将扩展 foo 的版本 1.0 修改为版本 1.1 的命令)。
如果有合适的更新脚本可用,则 ALTER EXTENSION UPDATE 命令将更新已安装的扩展到指定的新版本。更新脚本在与CREATE EXTENSION 提供的安装脚本相同的环境中运行:特别是,search_path以相同的方式设置,并且脚本创建的任何新对象都会自动添加到扩展中。此外,如果脚本选择删除扩展成员对象,则这些对象将自动与扩展分离。
如果扩展具有辅助控制文件,则用于更新脚本的控制参数是与脚本的目标(新)版本相关联的参数。
ALTER EXTENSION 能够执行一系列更新脚本文件以实现所请求的更新。例如,如果只有 foo--1.0--1.1.sql 和foo--1.1--2.0.sql 可用,则在当前安装了版本 1.0 时,如果请求更新到版本 2.0,则 ALTER EXTENSION 将按顺序应用它们。
UXDB 不会假设版本名称的任何属性:例如,它不知道 1.1 是否跟随1.0。它只是匹配可用的版本名称,并遵循需要应用最少更新脚本的路径。(实际上,版本名称可以是不包含-- 或前导或尾随 - 的任何字符串。)
有时提供“降级”脚本很有用,例如 foo--1.1--1.0.sql,以允许还原与版本 1.1相关联的更改。如果这样做,请注意可能会意外地应用降级脚本,因为它会产生更短的路径。风险的情况是存在一个“快速路径”更新脚本,它跳过了几个版本,以及一个降级脚本到快速路径的起点。应用降级脚本然后应用快速路径可能需要更少的步骤。
若要检查意外的更新路径,请使用以下命令:
SELECT * FROM ux_extension_update_paths('extension_name');
这将显示指定扩展的每个已知版本名称对,以及从源版本到目标版本所采取的升级路径序列,如果没有可用的升级路径,则显示NULL。路径以文本形式显示,使用“--”分隔符。如果您喜欢数组格式,可以使用regexp_split_to_array(path,'--')。
1.17.5.使用更新脚本安装扩展
已经存在一段时间的扩展可能会存在多个版本,作者需要为其编写更新脚本。例如,如果您foo在版本1.0、1.1和中发布了扩展1.2,则应该有更新脚本foo--1.0--1.1.sql 和foo--1.1--1.2.sql。在UXDB 10之前,还需要创建新的脚本文件foo--1.1.sql并foo--1.2.sql 直接构建较新的扩展版本,否则无法直接安装较新的版本,只能通过安装1.0然后更新。那是乏味且重复的,但现在没有必要了,因为CREATE EXTENSION可以自动遵循更新链。例如,如果只有脚本文件foo--1.0.sql、foo--1.0--1.1.sql和 可用,则通过按顺序运行这三个脚本来满足foo--1.1--1.2.sql安装版本的请求。1.2处理过程与您首先安装1.0然后更新到1.2. (与 一样ALTER EXTENSION UPDATE,如果有多个路径可用,则首选最短的路径。)以这种方式排列扩展的脚本文件可以减少生成小型更新所需的维护工作量。
如果您使用具有以此样式维护的扩展名的辅助(特定于版本的)控制文件,请记住每个版本都需要一个控制文件,即使它没有独立的安装脚本,因为该控制文件将确定如何执行对该版本的隐式更新。例如,如果foo--1.0.control指定requires = 'bar'但foo其他控制文件没有指定,则当更新到另一个版本 时,扩展的依赖关系bar将被删除1.0。
1.17.6.扩展的安全考虑
广泛分布的扩展应该很少考虑它们占用的数据库。因此,以安全的方式编写扩展提供的函数是适当的,这样就不会受到基于搜索路径的攻击的影响。
该superuser属性设置为 true 的扩展还必须考虑在其安装和更新脚本中执行的操作的安全隐患。对于恶意用户来说,创建特洛伊木马对象并不是非常困难,这些对象会危及稍后执行不小心编写的扩展脚本,从而允许该用户获得超级用户权限。
如果一个扩展被标记为trusted,那么它的安装模式可以由安装用户选择,该用户可能会故意使用不安全的模式,以期获得超级用户权限。因此,一个受信任的扩展在安全方面极为暴露,必须仔细检查其所有脚本命令,以确保没有妥协的可能性。
关于安全编写函数的建议在下面的Extension函数的安全考虑中提供,关于安全编写安装脚本的建议在下面的Extension脚本的安全考虑中提供。
1.17.6.1.Extension函数的安全考虑
由扩展提供的SQL语言和PL语言函数在执行时存在基于搜索路径的攻击风险,因为这些函数的解析发生在执行时间而不是创建时间。
CREATE FUNCTION参考页面包含有关安全编写SECURITYDEFINER函数的建议。对于扩展提供的任何函数,应该应用这些技术,因为该函数可能被高权限用户调用。
如果无法将search_path设置为仅包含安全模式,则假定每个未限定的名称都可能解析为恶意用户定义的对象。注意依赖search_path的结构,例如,IN和CASE expression WHEN总是使用搜索路径选择运算符。在它们的位置上,使用OPERATOR(schema.=) ANY和CASE WHEN expression。
通用扩展通常不应假定它已安装到安全模式中,这意味着即使是对其自己对象的模式限定引用也不是完全无风险的。例如,如果扩展定义了一个函数myschema.myfunc(bigint),那么调用myschema.myfunc(42)可能会被恶意函数myschema.myfunc(integer)捕获。注意函数和运算符参数的数据类型与声明的参数类型完全匹配,必要时使用显式转换。
1.17.6.2.Extension脚本的安全考虑
扩展安装或更新脚本应编写以防止在脚本执行时发生基于搜索路径的攻击。如果脚本中的对象引用可以被解析为脚本作者意图之外的其他对象,则可能会立即发生妥协,或者在使用错误定义的扩展对象时发生。
DDL命令,例如CREATE FUNCTION和CREATE OPERATOR CLASS通常是安全的,但要注意任何具有通用表达式作为组件的命令。例如,CREATE VIEW需要经过审核,DEFAULT表达式也需要。在CREATE FUNCTION中。
有时扩展脚本可能需要执行通用 SQL,例如进行目录调整,这些调整无法通过 DDL实现。请注意使用安全的search_path执行此类命令;不要相信由CREATE/ALTEREXTENSION提供的路径是安全的。最佳实践是将search_path暂时设置为'ux_catalog, ux_temp',并在需要时显式插入对扩展安装模式的引用。(这种做法也可能有助于创建视图。)示例可以在UXDB源代码分发中的contrib模块中找到。
跨扩展引用非常难以完全保证安全,部分原因是不确定其他扩展位于哪个模式中。如果两个扩展都安装在同一个模式中,则危险会降低,因为恶意对象无法在安装时的search_path中放在被引用的扩展之前。但是,目前不存在要求这样做的机制。因此,最佳实践是,如果一个扩展依赖于另一个扩展,则不要将其标记为可信,除非另一个扩展始终安装在ux_catalog中。
1.17.7.扩展示例
下面是一个完整的仅SQL扩展示例,它是一个可以在其插槽中存储任何类型值的两个元素复合类型,这些插槽的名称分别为“k”和“v”。非文本值会自动强制转换为文本进行存储。
脚本文件pair--1.0.sql如下所示:
-- complain if script is sourced in uxsql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION pair" to load this file. \quit
CREATE TYPE pair AS ( k text, v text );
CREATE FUNCTION pair(text, text)
RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::@extschema@.pair;';
CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, FUNCTION = pair);
-- "SET search_path" is easy to get right, but qualified names perform better.
CREATE FUNCTION lower(pair)
RETURNS pair LANGUAGE SQL
AS 'SELECT ROW(lower($1.k), lower($1.v))::@extschema@.pair;'
SET search_path = ux_temp;
CREATE FUNCTION pair_concat(pair, pair)
RETURNS pair LANGUAGE SQL
AS 'SELECT ROW($1.k OPERATOR(ux_catalog.||) $2.k,
$1.v OPERATOR(ux_catalog.||) $2.v)::@extschema@.pair;';
控制文件pair.control如下所示:
# pair extension
comment = 'A key/value pair data type'
default_version = '1.0'
# cannot be relocatable because of use of @extschema@
relocatable = false
虽然您几乎不需要使用makefile将这两个文件安装到正确的目录中,但您可以使用包含以下内容的Makefile:
EXTENSION = pair
DATA = pair--1.0.sql
UX_CONFIG = ux_config
UXXS := $(shell $(UX_CONFIG) --uxxs)
include $(UXXS)
此makefile依赖于UXXS,该工具在扩展的构建基础设施中进行了描述。命令make install将控制和脚本文件安装到由ux_config报告的正确目录中。
安装文件后,使用CREATE EXTENSION命令将对象加载到任何特定的数据库中。
1.18.扩展的构建基础设施
如果您正在考虑分发您的UXDB扩展模块,则为它们设置可移植的构建系统可能相当困难。因此,UXDB安装提供了一个名为UXXS的扩展构建基础设施,以便可以简单地构建简单的扩展模块,这些模块已经安装在服务器上。UXXS主要用于包含C代码的扩展,尽管它也可以用于纯SQL扩展。请注意,UXXS不旨在成为通用的构建系统框架,可用于构建任何与UXDB接口的软件;它只是自动化了简单服务器扩展模块的常见构建规则。对于更复杂的软件包,您可能需要编写自己的构建系统。
要为您的扩展使用UXXS基础设施,您必须编写一个简单的makefile。在makefile中,您需要设置一些变量并包含全局UXXS makefile。以下是一个示例,用于构建名为isbn_issn的扩展模块,该模块由包含一些C代码的共享库、扩展控制文件、SQL脚本、包含文件(仅在其他模块可能需要访问扩展函数而不通过SQL时需要)和文档文本文件组成:
MODULES = isbn_issn
EXTENSION = isbn_issn
DATA = isbn_issn--1.0.sql
DOCS = README.isbn_issn
HEADERS_isbn_issn = isbn_issn.h
UX_CONFIG = ux_config
UXXS := $(shell $(UX_CONFIG) --uxxs)
include $(UXXS)
最后三行应始终相同。在文件的早期,您可以分配变量或添加自定义make规则。
设置以下三个变量之一以指定要构建的内容:
MODULES
要从具有相同词干的源文件构建的共享库对象列表(不要在此列表中包含库后缀)
MODULE_big
一个共享库,用于从多个源文件构建(在OBJS中列出对象文件)
PROGRAM
一个可执行程序,用于构建(在OBJS中列出对象文件)
还可以设置以下变量:
EXTENSION
扩展名;对于每个名称,必须提供一个 extension.control文件,它将被安装到prefix/share/extension。
MODULEDIR
应将DATA和DOCS文件安装到的prefix/share的子目录(如果未设置,则默认为extension(如果设置了EXTENSION),否则为contrib)。
DATA
要安装到prefix/share/$MODULEDIR的随机文件。
DATA_built
要安装到prefix/share/$MODULEDIR的随机文件,需要先构建。
DATA_TSEARCH
要安装到prefix/share/tsearch_data下的随机文件。
DOCS
要安装到prefix/doc/$MODULEDIR下的随机文件。
HEADERS
HEADERS_built
(可选构建和)安装到prefix/include/server/$MODULEDIR/$MODULE_big下的文件。
与DATA_built不同,HEADERS_built中的文件 不会被clean目标删除;如果要删除它们,还要将它们添加到EXTRA_CLEAN或添加自己的规则来执行。
HEADERS_$MODULE
HEADERS_built_$MODULE
安装(如果指定了构建)到prefix/include/server/$MODULEDIR/$MODULE下的文件,其中$MODULE必须是在MODULES或MODULE_big中使用的模块名称。
与DATA_built不同,HEADERS_built_$MODULE中的文件 不会被clean目标删除;如果要删除它们,还要将它们添加到EXTRA_CLEAN或添加自己的规则来执行。
对于同一模块,可以同时使用两个变量或任何组合,除非MODULES列表中有两个模块名称,它们仅因存在前缀built_而不同,这将导致歧义。在那种(希望不太可能的)情况下,您应该只使用HEADERS_built_$MODULE变量。
SCRIPTS
要安装到prefix/bin。
SCRIPTS_built
脚本文件(非二进制文件)需要安装到 prefix/bin,需要先构建。
REGRESS
回归测试用例列表(不带后缀),详见下文。
REGRESS_OPTS
传递给ux_regress 的额外开关。
ISOLATION
隔离测试用例列表,详见下文。
ISOLATION_OPTS
传递给 ux_isolation_regress的额外开关。
TAP_TESTS
定义是否需要运行 TAP 测试,详见下文。
NO_INSTALL
不定义 install 目标,适用于不需要安装构建产品的测试模块。
NO_INSTALLCHECK
不定义 installcheck 目标,例如测试需要特殊配置或不使用 ux_regress。
EXTRA_CLEAN
在 make clean 中需要删除的额外文件。
UX_CPPFLAGS
将被添加到 CPPFLAGS 的前面。
UX_CFLAGS
将被添加到 CFLAGS 的后面。
UX_CXXFLAGS
将被添加到 CXXFLAGS 的后面。
UX_LDFLAGS
将被添加到 LDFLAGS 的前面。
UX_LIBS
将被添加到 PROGRAM 链接行中。
SHLIB_LINK
将被添加到 MODULE_big 链接行中。
UX_CONFIG
用于构建的 UXDB 安装的 ux_config 程序的路径 (通常只需使用您的 PATH 中的第一个ux_config)。
将此 makefile 作为 Makefile 放置在扩展名所在的目录中。然后可以使用 make 进行编译,然后使用 make install 安装模块。默认情况下,扩展名会为 与您的 PATH 中找到的第一个 ux_config 程序对应的UXDB 安装进行编译和安装。您可以通过将 UX_CONFIG 设置为指向其 ux_config程序来使用不同的安装,无论是在 makefile 中还是在 make 命令行上。
如果要将构建目录保持分离,也可以在扩展名源树之外的目录中运行 make。这个过程也称为 VPATH 构建。具体操作如下:
mkdir build_dir
cd build_dir
make -f /path/to/extension/source/tree/Makefile
make -f /path/to/extension/source/tree/Makefile install
或者,您可以像核心代码一样设置VPATH构建目录。一种方法是使用核心脚本config/prep_buildtree。完成后,您可以通过设置VPATH变量来构建:
make VPATH=/path/to/extension/source/tree
make VPATH=/path/to/extension/source/tree install
此过程可以适用于更多种类的目录布局。
REGRESS变量中列出的脚本用于模块的回归测试,可以在make install后通过make installcheck调用。为了使其工作,您必须有一个运行中的UXDB服务器。列在REGRESS中的脚本文件必须出现在扩展目录中名为sql/的子目录中。这些文件必须具有扩展名.sql,不能包含在makefile的REGRESS列表中。对于每个测试,还应该有一个包含期望输出的文件,位于名为expected/的子目录中,具有相同的词干和扩展名.out。make installcheck使用uxsql执行每个测试脚本,并将结果输出与匹配的期望文件进行比较。任何差异都将以diff -c格式写入文件regression.diffs中。请注意,尝试运行缺少其期望文件的测试将报告为“麻烦”,因此请确保您拥有所有期望的文件。
ISOLATION变量中列出的脚本用于测试并发会话与您的模块的行为,可以在make install后通过make installcheck调用。为了使其工作,您必须有一个运行中的UXDB服务器。列在ISOLATION中的脚本文件必须出现在扩展目录中名为specs/的子目录中。这些文件必须具有扩展名.spec,不能包含在makefile的ISOLATION列表中。对于每个测试,还应该有一个包含期望输出的文件,位于名为expected/的子目录中,具有相同的词干和扩展名.out。make installcheck执行每个测试脚本,并将结果输出与匹配的期望文件进行比较。任何差异都将以diff -c格式写入文件output_iso/regression.diffs中。请注意,尝试运行缺少其期望文件的测试将报告为“麻烦”,因此请确保您拥有所有期望的文件。
TAP_TESTS启用TAP测试的使用。每次运行的数据都在名为tmp_check/的子目录中。
提示
创建期望文件的最简单方法是创建空文件,然后进行测试运行(当然会报告差异)。检查在results/中找到的实际结果文件。
目录(用于REGRESS中的测试),或output_iso/results/目录(用于ISOLATION中的测试),然后将它们复制到expected/中,如果它们与您从测试中期望的匹配。
2.触发器
本章提供编写触发器函数的一般信息。触发器函数可以用大多数可用的过程性语言编写,包括 PL/uxSQL, PL/Tcl, PL/Perl和 PL/Python。阅读本章后,您应该查阅您喜欢的过程性语言的章节,以了解在其中编写触发器的语言特定细节。
也可以用C编写触发器函数,但大多数人发现使用其中一种过程性语言更容易。目前不可能用纯SQL函数语言编写触发器函数。
2.1.触发器行为概述
触发器是指数据库在执行特定类型的操作时自动执行特定函数的规范。触发器可以附加到表(分区或非分区)、视图和外部表。
在表和外部表上,触发器可以定义为在任何INSERT、UPDATE或DELETE操作之前或之后执行,可以每修改一行执行一次,也可以每个SQL语句执行一次。UPDATE触发器还可以设置为仅在UPDATE语句的SET子句中提到某些列时才触发。触发器也可以对TRUNCATE语句触发。如果触发器事件发生,则在适当的时间调用触发器的函数来处理事件。
在视图上,触发器可以定义为代替INSERT、UPDATE或DELETE操作执行。这种INSTEADOF触发器为需要在视图中修改的每一行触发一次。触发器的函数有责任对视图的基础表进行必要的修改,并在适当的情况下将修改后的行作为它在视图中的样子返回。视图上的触发器也可以定义为在每个SQL语句之前或之后执行一次INSERT、UPDATE或DELETE操作。但是,只有在视图上也有INSTEAD OF触发器时,才会触发这些触发器。否则,针对视图的任何语句都必须被重写为影响其基础表的语句,然后将触发器附加到基础表。
必须在创建触发器之前定义触发器函数。触发器函数必须声明为不带参数并返回类型为trigger的函数。(触发器函数通过一个特殊传递的TriggerData结构接收其输入,而不是以普通函数参数的形式接收。)
一旦创建了适当的触发器函数,就可以使用CREATE TRIGGER建立触发器。同一个触发器函数可以用于多个触发器。
UXDB提供了每行触发器和每语句触发器。使用每行触发器时,触发器函数会针对语句影响的每一行分别调用一次。相反,每语句触发器仅在执行适当的语句时调用一次,而不管该语句影响的行数。特别地,影响零行的语句仍将导致执行任何适用的每语句触发器。这两种类型的触发器有时被称为行级触发器和语句级触发器。只能在语句级别上定义TRUNCATE的触发器,而不能在每行上定义。
触发器还根据它们触发的操作是之前、之后还是代替操作进行分类。这些分别称为BEFORE触发器、AFTER触发器和INSTEAD OF触发器。语句级BEFORE触发器自然会在语句开始执行之前触发,而语句级AFTER触发器则在语句结束时触发。这些类型的触发器可以在表、视图或外部表上定义。行级BEFORE触发器会在操作特定行之前立即触发,而行级AFTER触发器会在语句结束时触发(但在任何语句级AFTER触发器之前触发)。这些类型的触发器只能在表和外部表上定义,而不能在视图上定义。INSTEAD OF触发器只能在视图上定义,并且只能在行级别上定义;它们会在识别需要操作的视图中的每一行时立即触发。
如果将AFTER触发器定义为约束触发器,则可以将其执行推迟到事务结束,而不是语句结束。在所有情况下,触发器作为触发它的语句的一部分执行,因此如果语句或触发器引发错误,则两者的影响都将被回滚。
针对继承或分区层次结构中的父表的语句不会导致受影响的子表的语句级触发器被触发;只会触发父表的语句级触发器。但是,任何受影响的子表的行级触发器都将被触发。
如果INSERT包含ON CONFLICT DO UPDATE子句,则可能会以一种从更新后的行的最终状态中明显的方式应用每行BEFORE INSERT触发器和每行BEFORE UPDATE触发器的影响,如果引用了EXCLUDED列,则会出现这种情况。虽然不需要为两组行级BEFORE触发器都执行EXCLUDED列引用,但应考虑到出现意外结果的可能性,特别是当同时存在更改正在插入/更新的行的BEFORE INSERT和BEFORE UPDATE行级触发器时(这可以是即使修改更或多或少等效,如果它们不是幂等的,那么就会出现问题。请注意,当指定ON CONFLICT DO UPDATE时,将执行语句级别的UPDATE触发器,无论是否受到UPDATE的影响(无论是否采取了替代的UPDATE路径)。带有ON CONFLICT DO UPDATE子句的INSERT将首先执行语句级别的BEFORE INSERT触发器,然后执行语句级别的BEFORE UPDATE触发器,接着是语句级别的AFTER UPDATE触发器,最后是语句级别的AFTER INSERT触发器。
如果对分区表进行UPDATE导致一行移动到另一个分区,则会在原始分区中执行DELETE,然后在新分区中执行INSERT。在这种情况下,所有行级别的BEFORE UPDATE触发器和所有行级别的BEFORE DELETE触发器都会在原始分区上触发。然后,在目标分区上触发所有行级别的BEFORE INSERT触发器。当所有这些触发器都影响到正在移动的行时,应考虑出现意外结果的可能性。就AFTER ROW触发器而言,将应用AFTER DELETE和AFTER INSERT触发器;但是,不会应用AFTER UPDATE触发器,因为UPDATE已转换为DELETE和INSERT。就语句级别触发器而言,不会触发任何DELETE或INSERT触发器,即使发生行移动;只会触发在UPDATE语句中使用的目标表上定义的UPDATE触发器。没有为MERGE定义单独的触发器。相反,根据MERGE查询中指定的操作(对于语句级别触发器)和执行的操作(对于行级别触发器),将触发语句级别或行级别的UPDATE、DELETE和INSERT触发器。
在运行MERGE命令时,无论操作是否最终执行,都会为MERGE命令中指定的事件触发语句级别的BEFORE和AFTER触发器。这与更新零行的UPDATE语句相同,但会触发语句级别的触发器。只有在实际更新、插入或删除行时才会触发行级别触发器。因此,虽然对于某些类型的操作会触发语句级别触发器,但对于相同类型的操作可能不会触发行级别触发器,这是完全合法的。
由每个语句触发器调用的触发器函数应始终返回NULL。如果选择,由每行触发器调用的触发器函数可以向调用执行器返回表行(类型为HeapTuple的值)。在执行操作之前触发行级别触发器。以下选择:
-
它可以返回
NULL以跳过当前行的操作。这指示执行器不执行触发器调用的行级操作(特定表行的插入、修改或删除)。 -
仅适用于行级
INSERT和UPDATE触发器,返回的行成为将要插入或将要替换正在更新的行。这允许触发器函数修改正在插入或更新的行。
一个不打算引起这些行为的行级BEFORE触发器必须小心地将其结果返回为传递的相同行(即NEW行对于INSERT和UPDATE触发器,OLD行对于DELETE触发器)。
行级INSTEAD OF触发器应返回NULL以指示它没有修改视图的基础表的任何数据,或者它应返回传递的视图行(INSERT和UPDATE操作的NEW行,或DELETE操作的OLD行)。非空返回值用于表示触发器在视图中执行了必要的数据修改。这将导致命令受影响的行数计数增加。仅适用于INSERT和UPDATE操作,触发器可以在返回之前修改NEW行。这将更改由INSERT RETURNING或UPDATE RETURNING返回的数据,并且在视图不显示提供的完全相同的数据时非常有用。
行级触发器在操作后触发时将忽略返回值,因此它们可以返回NULL。
一些考虑适用于生成的列。存储的生成列在BEFORE触发器之后和AFTER触发器之前计算。因此,在AFTER触发器中可以检查生成的值。在BEFORE触发器中,OLD行包含旧的生成值,正如人们所期望的那样,但是NEW行尚未包含新的生成值,不应访问。在C语言接口中,此时列的内容未定义;高级编程语言应防止在BEFORE触发器中访问存储的生成列中的NEW行。在BEFORE触发器中对生成列值的更改将被忽略并将被覆盖。
如果为同一关系的同一事件定义了多个触发器,则触发器将按触发器名称的字母顺序触发。在BEFORE和INSTEAD OF触发器的情况下,每个触发器返回的可能修改的行成为下一个触发器的输入。如果任何BEFORE或INSTEAD OF触发器返回NULL,则该行的操作将被放弃,并且不会触发后续触发器(对于该行)。
触发器定义还可以指定布尔WHEN。条件将被测试以确定触发器是否应该被触发。在行级触发器中,WHEN条件可以检查行的列的旧值和/或新值。(尽管对于语句级触发器也可以有WHEN条件,但该功能对它们不是很有用。)在BEFORE触发器中,WHEN条件在函数执行之前或将要执行之前进行评估,因此使用WHEN与在触发器函数开头测试相同的条件没有实质性的区别。然而,在AFTER触发器中,WHEN条件在行更新发生后立即进行评估,并确定是否在语句结束时排队事件以触发触发器。因此,当AFTER触发器的WHEN条件不返回true时,不需要排队事件,也不需要在语句结束时重新获取行。如果触发器只需要为少数行触发,这可以在修改许多行的语句中导致显着的加速。INSTEAD OF触发器不支持WHEN条件。
如果触发器函数执行 SQL 命令,那么这些命令可能会再次触发触发器。这被称为级联触发器。对级联水平的数量没有直接的限制。级联可能导致对同一触发器的递归调用; 例如,INSERT触发器可能执行向同一表中插入额外行的命令,从而导致再次触发INSERT触发器。在这种情况下,触发器程序员有责任避免无限递归。
在定义触发器时,可以为其指定参数。在触发器定义中包含参数的目的是允许具有相似需求的不同触发器调用相同的函数。例如,可以有一个通用触发器函数,它以两个列名作为参数,将当前用户放在一个列名中,将当前时间戳放在另一个列名中。如果编写正确,这个触发器函数将独立于它触发的特定表。因此,同样的函数可以用于任何具有合适列的表上的INSERT事件,例如,可以自动跟踪事务表中记录的创建。如果定义为 UPDATE 触发器,它还可以用于跟踪最后更新事件。
每种支持触发器的编程语言都有自己的方法,用于使触发器输入数据可用于触发器函数。此输入数据包括触发器事件的类型(例如,INSERT或UPDATE)以及CREATE TRIGGER中列出的任何参数。对于行级触发器,输入数据还包括用于INSERT和UPDATE触发器的 NEW 行,以及/或用于 UPDATE 和 DELETE 触发器的OLD 行。
默认情况下,语句级触发器没有任何方法来检查语句修改的单个行。但是,AFTER STATEMENT触发器可以请求创建转换表,以使受影响的行集可用于触发器。AFTER ROW触发器也可以请求转换表,以便它们可以查看表中的总更改以及它们当前正在触发的单个行的更改。再次检查转换表的方法取决于使用的编程语言,但是典型的方法是使转换表像只读临时表一样,可以通过触发器函数中发出的SQL命令访问转换表。
2.2.数据更改的可见性
如果您在触发器函数中执行 SQL 命令,并且这些命令访问触发器所针对的表,则需要注意数据可见性规则,因为它们决定这些 SQL命令是否会看到触发器触发的数据更改。简而言之:
-
语句级触发器遵循简单的可见性规则:语句级
BEFORE触发器不会看到语句所做的任何更改,而所有修改都会被语句级AFTER触发器看到。 -
触发器触发的数据更改(插入、更新或删除)自然而然地对在行级
BEFORE触发器中执行的 SQL 命令不可见,因为它尚未发生。 -
但是,在行级
BEFORE触发器中执行的 SQL命令将看到在同一外部命令中先前处理的行的数据更改的影响。这需要谨慎,因为这些更改事件的顺序通常是不可预测的;影响多行的SQL 命令可以以任何顺序访问这些行。 -
同样,行级
INSTEAD OF触发器将看到在同一外部命令中先前触发的INSTEAD OF触发器所做的数据更改的影响。 -
当触发行级
AFTER触发器时,外部命令所做的所有数据更改已经完成,并且对调用的触发器函数可见。
如果您的触发器函数是用任何标准过程语言编写的,则上述语句仅适用于函数声明为 VOLATILE 的情况。声明为 STABLE 或IMMUTABLE 的函数在任何情况下都不会看到调用命令所做的更改。
一个完整的触发器示例中的示例演示了这些规则。
2.3.在 C 中编写触发器函数
本节介绍触发器函数接口的低级细节。只有在使用 C编写触发器函数时才需要这些信息。如果您使用的是更高级别的语言,则这些细节已经为您处理。在大多数情况下,您应该在使用C编写触发器之前考虑使用过程语言。每种过程语言的文档都解释了如何在该语言中编写触发器。
触发器函数必须使用“版本 1”函数管理器接口。
当触发器管理器调用函数时,它不会传递任何普通参数,但它会传递一个指向 TriggerData 结构的“上下文”指针。C函数可以通过执行宏来检查它们是否是从触发器管理器调用的:
CALLED_AS_TRIGGER(fcinfo)
它会扩展为:
((fcinfo)->context != NULL && IsA((fcinfo)->context, TriggerData))
如果返回 true,则可以安全地将fcinfo->context强制转换为类型TriggerData *,并使用指向的TriggerData 结构。函数必须不更改TriggerData结构或任何它所指向的数据。
struct TriggerData 在 commands/trigger.h 中定义:
typedef struct TriggerData
{
NodeTag type;
TriggerEvent tg_event;
Relation tg_relation;
HeapTuple tg_trigtuple;
HeapTuple tg_newtuple;
Trigger *tg_trigger;
TupleTableSlot *tg_trigslot;
TupleTableSlot *tg_newslot;
Tuplestorestate *tg_oldtable;
Tuplestorestate *tg_newtable;
const Bitmapset *tg_updatedcols;
} TriggerData;
其中成员定义如下:
type
始终为T_TriggerData。
tg_event
描述调用函数的事件。您可以使用以下宏来检查tg_event:
TRIGGER_FIRED_BEFORE(tg_event)
如果触发器在操作之前触发,则返回true。
TRIGGER_FIRED_AFTER(tg_event)
如果触发器在操作之后触发,则返回true。
TRIGGER_FIRED_INSTEAD(tg_event)
如果触发器代替操作触发,则返回true。
TRIGGER_FIRED_FOR_ROW(tg_event)
如果触发器针对行级事件触发,则返回true。
TRIGGER_FIRED_FOR_STATEMENT(tg_event)
如果触发器针对语句级事件触发,则返回true。
TRIGGER_FIRED_BY_INSERT(tg_event)
如果触发器由INSERT命令触发,则返回true。
TRIGGER_FIRED_BY_UPDATE(tg_event)
如果触发器由UPDATE命令触发,则返回true。
TRIGGER_FIRED_BY_DELETE(tg_event)
如果触发器由DELETE命令触发,则返回true。
TRIGGER_FIRED_BY_TRUNCATE(tg_event)
如果触发器由TRUNCATE命令触发,则返回true。
tg_relation
指向描述触发器触发的关系的结构的指针。有关此结构的详细信息,请参见utils/rel.h。最有趣的是tg_relation->rd_att(关系元组的描述符)和tg_relation->rd_rel->relname(关系名称;类型不是char*,而是NameData;如果需要名称的副本,请使用SPI_getrelname(tg_relation)获取char*)。
tg_trigtuple
指向触发器触发的行的指针。这是正在插入、更新或删除的行。如果此触发器是为INSERT或DELETE触发的,则如果您不想用不同的行替换该行(在INSERT的情况下)或跳过操作,则应从函数返回此行。对于外部表的触发器,此处的系统列值未指定。
tg_newtuple
如果触发器是为UPDATE触发的,则指向行的新版本;如果为INSERT或DELETE触发,则为NULL。如果事件是UPDATE,并且您不想用不同的行替换此行或跳过操作,则必须从函数返回此值。对于外部表的触发器,此处的系统列值未指定。
tg_trigger
指向类型为Trigger的结构的指针,定义在utils/reltrigger.h中:
typedef struct Trigger
{
Oid tgoid;
char *tgname;
Oid tgfoid;
int16 tgtype;
char tgenabled;
bool tgisinternal;
bool tgisclone;
Oid tgconstrrelid;
Oid tgconstrindid;
Oid tgconstraint;
bool tgdeferrable;
bool tginitdeferred;
int16 tgnargs;
int16 tgnattr;
int16 *tgattr;
char **tgargs;
char *tgqual;
char *tgoldtable;
char *tgnewtable;
} Trigger;
其中tgname是触发器的名称,tgnargs是tgargs中参数的数量,tgargs是一个指向CREATE TRIGGER语句中指定的参数的指针数组。其他成员仅供内部使用。
tg_trigslot
包含tg_trigtuple的槽,如果没有这样的元组,则为NULL指针。
tg_newslot
包含tg_newtuple的槽,如果没有这样的元组,则为NULL指针。
tg_oldtable
指向类型为Tuplestorestate的结构的指针,其中包含零个或多个以tg_relation指定的格式的行,如果没有OLD TABLE转换关系,则为NULL指针。
tg_newtable
指向类型为Tuplestorestate的结构的指针,其中包含零个或多个以tg_relation指定的格式的行,如果没有NEW TABLE转换关系,则为NULL指针。
tg_updatedcols
对于UPDATE触发器,指示由触发命令更新的列的位图集。通用触发器函数可以使用此功能通过不必处理未更改的列来优化操作。
例如,要确定属性编号为attnum(从1开始)是否是此位图集的成员,请调用bms_is_member(attnum -FirstLowInvalidHeapAttributeNumber,trigdata->tg_updatedcols))。
对于UPDATE触发器以外的触发器,这将为NULL。
为了允许通过SPI发出的查询引用转换表,请参见SPI_register_trigger_data。
触发器函数必须返回HeapTuple指针或NULL指针(不是SQL空值,即不要将isNull设置为true)。如果不想修改正在操作的行,请小心返回tg_trigtuple或tg_newtuple,具体取决于情况。
2.4.一个完整的触发器示例
这是一个非常简单的用 C 编写的触发器函数示例。(有关使用过程性语言编写的触发器示例,请参见过程性语言的文档。)
函数 trigf 报告表 ttest 中的行数,并在尝试将空值插入列 x 时跳过实际操作。(因此,触发器充当非空约束,但不会中止事务。)
首先,是表定义:
CREATE TABLE ttest (
x integer
);
这是触发器函数的源代码:
#include "uxdb.h"
#include "fmgr.h"
#include "executor/spi.h" /* 这是您需要使用 SPI 的内容 */
#include "commands/trigger.h" /* ... 触发器 ... */
#include "utils/rel.h" /* ... 和关系 */
UX_MODULE_MAGIC;
UX_FUNCTION_INFO_V1(trigf);
Datum
trigf(UX_FUNCTION_ARGS)
{
TriggerData *trigdata = (TriggerData *) fcinfo->context;
TupleDesc tupdesc;
HeapTuple rettuple;
char *when;
bool checknull = false;
bool isnull;
int ret, i;
/* 确保它被称为触发器 */
if (!CALLED_AS_TRIGGER(fcinfo))
elog(ERROR, "trigf: not called by trigger manager");
/* 返回给执行器的元组 */
if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
rettuple = trigdata->tg_newtuple;
else
rettuple = trigdata->tg_trigtuple;
/* 检查空值 */
if (!TRIGGER_FIRED_BY_DELETE(trigdata->tg_event)
&& TRIGGER_FIRED_BEFORE(trigdata->tg_event))
checknull = true;
if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
when = "before";
else
when = "after ";
tupdesc = trigdata->tg_relation->rd_att;
/* 连接到 SPI 管理器 */
if ((ret = SPI_connect()) < 0)
elog(ERROR, "trigf (fired %s): SPI_connect returned %d", when, ret);
/* 获取表中的行数 */
ret = SPI_exec("SELECT count(*) FROM ttest", 0);
if (ret < 0)
elog(ERROR, "trigf (fired %s): SPI_exec returned %d", when, ret);
/* count(*) 返回 int8,因此要小心转换 */
i = DatumGetInt64(SPI_getbinval(SPI_tuptable->vals[0],
SPI_tuptable->tupdesc,
1,
&isnull));
elog (INFO, "trigf (fired %s): there are %d rows in ttest", when, i);
SPI_finish();
if (checknull)
{
SPI_getbinval(rettuple, tupdesc, 1, &isnull);
if (isnull)
rettuple = NULL;
}
return PointerGetDatum(rettuple);
}
在编译源代码之后(参见编译和链接动态加载函数),声明函数和触发器:
CREATE FUNCTION trigf() RETURNS trigger
AS 'filename'
LANGUAGE C;
CREATE TRIGGER tbefore BEFORE INSERT OR UPDATE OR DELETE ON ttest
FOR EACH ROW EXECUTE FUNCTION trigf();
CREATE TRIGGER tafter AFTER INSERT OR UPDATE OR DELETE ON ttest
FOR EACH ROW EXECUTE FUNCTION trigf();
现在可以测试触发器的操作:
=> INSERT INTO ttest VALUES (NULL);
INFO: trigf (fired before): there are 0 rows in ttest
INSERT 0 0
-- Insertion skipped and AFTER trigger is not fired
=> SELECT * FROM ttest;
x
---
(0 rows)
=> INSERT INTO ttest VALUES (1);
INFO: trigf (fired before): there are 0 rows in ttest
INFO: trigf (fired after ): there are 1 rows in ttest
^^^^^^^^
remember what we said about visibility.
INSERT 167793 1
vac=> SELECT * FROM ttest;
x
---
1
(1 row)
=> INSERT INTO ttest SELECT x * 2 FROM ttest;
INFO: trigf (fired before): there are 1 rows in ttest
INFO: trigf (fired after ): there are 2 rows in ttest
^^^^^^
remember what we said about visibility.
INSERT 167794 1
=> SELECT * FROM ttest;
x
---
1
2
(2 rows)
=> UPDATE ttest SET x = NULL WHERE x = 2;
INFO: trigf (fired before): there are 2 rows in ttest
UPDATE 0
=> UPDATE ttest SET x = 4 WHERE x = 2;
INFO: trigf (fired before): there are 2 rows in ttest
INFO: trigf (fired after ): there are 2 rows in ttest
UPDATE 1
vac=> SELECT * FROM ttest;
x
---
1
4
(2 rows)
=> DELETE FROM ttest;
INFO: trigf (fired before): there are 2 rows in ttest
INFO: trigf (fired before): there are 1 rows in ttest
INFO: trigf (fired after ): there are 0 rows in ttest
INFO: trigf (fired after ): there are 0 rows in ttest
^^^^^^
remember what we said about visibility.
DELETE 2
=> SELECT * FROM ttest;
x
---
(0 rows)
在src/test/regress/regress.c和spi中有更复杂的示例。
3.事件触发器
除了在触发器中讨论的触发器机制外,UXDB还提供了事件触发器。与常规触发器不同,常规触发器仅附加到单个表并捕获DML事件,而事件触发器是针对特定数据库全局的,并且能够捕获DDL事件。
与常规触发器一样,事件触发器可以使用包括事件触发器支持的任何过程语言或C编写,但不能使用纯SQL。
3.1.事件触发器行为概述
事件触发器在其定义的数据库中发生与其关联的事件时触发。目前,仅支持以下事件:ddl_command_start、ddl_command_end、table_rewrite和sql_drop。未来版本可能会添加对其他事件的支持。
ddl_command_start事件在执行CREATE、ALTER、DROP、SECURITY LABEL、COMMENT、GRANT或REVOKE命令之前发生。在事件触发器触发之前,不会检查受影响的对象是否存在或不存在。但是,对于针对共享对象(数据库、角色和表空间)或针对事件触发器本身的DDL命令,此事件不会发生。事件触发器机制不支持这些对象类型。ddl_command_start事件还在执行SELECT INTO命令之前发生,因为这等效于CREATE TABLE AS。
ddl_command_end事件在执行相同的命令集之后立即发生。要获取有关执行的DDL操作的更多详细信息,请使用来自ddl_command_end事件触发器代码的集合返回函数ux_event_trigger_ddl_commands()。请注意,触发器在操作发生后(但在事务提交之前)触发,因此可以读取已更改的系统目录。
sql_drop事件在删除数据库对象的任何操作的ddl_command_end事件触发器之前发生。要列出已删除的对象,请使用集合返回函数ux_event_trigger_dropped_objects()从sql_drop事件触发器代码。请注意,触发器在对象从系统目录中删除后执行,因此无法再查找它们。
table_rewrite 事件发生在表通过ALTER TABLE和ALTER TYPE命令的某些操作重写之前。虽然其他控制语句也可用于重写表,例如CLUSTER和VACUUM,但不会触发table_rewrite事件。
事件触发器(如其他函数)不能在已中止的事务中执行。因此,如果DDL命令失败并出现错误,则不会执行任何相关的ddl_command_end触发器。反之,如果ddl_command_start触发器失败并出现错误,则不会触发任何其他事件触发器,并且不会尝试执行命令本身。同样,如果ddl_command_end触发器失败并出现错误,则DDL语句的效果将被回滚,就像包含事务中止的任何其他情况一样。
有关事件触发器机制支持的命令的完整列表,请参见事件触发器触发矩阵。
使用命令CREATE EVENT TRIGGER创建事件触发器。要创建事件触发器,必须首先创建一个带有特殊返回类型event_trigger的函数。此函数不需要(也不能)返回值;返回类型仅作为函数应作为事件触发器调用的信号。
如果为特定事件定义了多个事件触发器,则它们将按触发器名称的字母顺序触发。
触发器定义还可以指定WHEN条件,以便例如,仅针对用户希望拦截的特定命令触发ddl_command_start触发器。这些触发器的常见用途是限制用户可以执行的DDL操作范围。
3.2.事件触发器触发矩阵
表 按命令标记支持的事件触发器列出了支持事件触发器的所有命令。
表 按命令标记支持的事件触发器
| 命令标签 | ddl_command_start | ddl_command_end | sql_drop | table_rewrite | 备注 |
|---|---|---|---|---|---|
| ALTER AGGREGATE | X | X | - | - | |
| ALTER COLLATION | X | X | - | - | |
| ALTER CONVERSION | X | X | - | - | |
| ALTER DOMAIN | X | X | - | - | |
| ALTER DEFAULT PRIVILEGES | X | X | - | - | |
| ALTER EXTENSION | X | X | - | - | |
| ALTER FOREIGN DATA WRAPPER | X | X | - | - | |
| ALTER FOREIGN TABLE | X | X | X | - | |
| ALTER FUNCTION | X | X | - | - | |
| ALTER LANGUAGE | X | X | - | - | |
| ALTER LARGE OBJECT | X | X | - | - | |
| ALTER MATERIALIZED VIEW | X | X | - | X | |
| ALTER OPERATOR | X | X | - | - | |
| ALTER OPERATOR CLASS | X | X | - | - | |
| ALTER OPERATOR FAMILY | X | X | - | - | |
| ALTER POLICY | X | X | - | - | |
| ALTER PROCEDURE | X | X | - | - | |
| ALTER PUBLICATION | X | X | - | - | |
| ALTER ROUTINE | X | X | - | - | |
| ALTER SCHEMA | X | X | - | - | |
| ALTER SEQUENCE | X | X | - | - | |
| ALTER SERVER | X | X | - | - | |
| ALTER STATISTICS | X | X | - | - | |
| ALTER SUBSCRIPTION | X | X | - | - | |
| ALTER TABLE | X | X | X | X | |
| ALTER TEXT SEARCH CONFIGURATION | X | X | - | - | |
| ALTER TEXT SEARCH DICTIONARY | X | X | - | - | |
| ALTER TEXT SEARCH PARSER | X | X | - | - | |
| ALTER TEXT SEARCH TEMPLATE | X | X | - | - | |
| ALTER TRIGGER | X | X | - | - | |
| ALTER TYPE | X | X | - | X | |
| ALTER USER MAPPING | X | X | - | - | |
| ALTER VIEW | X | X | - | - | |
| COMMENT | X | X | - | - | 只对本地对象 |
| CREATE ACCESS METHOD | X | X | - | - | |
| CREATE AGGREGATE | X | X | - | - | |
| CREATE CAST | X | X | - | - | |
| CREATE COLLATION | X | X | - | - | |
| CREATE CONVERSION | X | X | - | - | |
| CREATE DOMAIN | X | X | - | - | |
| CREATE EXTENSION | X | X | - | - | |
| CREATE FOREIGN DATA WRAPPER | X | X | - | - | |
| CREATE FOREIGN TABLE | X | X | - | - | |
| CREATE FUNCTION | X | X | - | - | |
| CREATE INDEX | X | X | - | - | |
| CREATE LANGUAGE | X | X | - | - | |
| CREATE MATERIALIZED VIEW | X | X | - | - | |
| CREATE OPERATOR | X | X | - | - | |
| CREATE OPERATOR CLASS | X | X | - | - | |
| CREATE OPERATOR FAMILY | X | X | - | - | |
| CREATE POLICY | X | X | - | - | |
| CREATE PROCEDURE | X | X | - | - | |
| CREATE PUBLICATION | X | X | - | - | |
| CREATE RULE | X | X | - | - | |
| CREATE SCHEMA | X | X | - | - | |
| CREATE SEQUENCE | X | X | - | - | |
| CREATE SERVER | X | X | - | - | |
| CREATE STATISTICS | X | X | - | - | |
| CREATE SUBSCRIPTION | X | X | - | - | |
| CREATE TABLE | X | X | - | - | |
| CREATE TABLE AS | X | X | - | - | |
| CREATE TEXT SEARCH CONFIGURATION | X | X | - | - | |
| CREATE TEXT SEARCH DICTIONARY | X | X | - | - | |
| CREATE TEXT SEARCH PARSER | X | X | - | - | |
| CREATE TEXT SEARCH TEMPLATE | X | X | - | - | |
| CREATE TRIGGER | X | X | - | - | |
| CREATE TYPE | X | X | - | - | |
| CREATE USER MAPPING | X | X | - | - | |
| CREATE VIEW | X | X | - | - | |
| DROP ACCESS METHOD | X | X | X | - | |
| DROP AGGREGATE | X | X | X | - | |
| DROP CAST | X | X | X | - | |
| DROP COLLATION | X | X | X | - | |
| DROP CONVERSION | X | X | X | - | |
| DROP DOMAIN | X | X | X | - | |
| DROP EXTENSION | X | X | X | - | |
| DROP FOREIGN DATA WRAPPER | X | X | X | - | |
| DROP FOREIGN TABLE | X | X | X | - | |
| DROP FUNCTION | X | X | X | - | |
| DROP INDEX | X | X | X | - | |
| DROP LANGUAGE | X | X | X | - | |
| DROP MATERIALIZED VIEW | X | X | X | - | |
| DROP OPERATOR | X | X | X | - | |
| DROP OPERATOR CLASS | X | X | X | - | |
| DROP OPERATOR FAMILY | X | X | X | - | |
| DROP OWNED | X | X | X | - | |
| DROP POLICY | X | X | X | - | |
| DROP PROCEDURE | X | X | X | - | |
| DROP PUBLICATION | X | X | X | - | |
| DROP ROUTINE | X | X | X | - | |
| DROP RULE | X | X | X | - | |
| DROP SCHEMA | X | X | X | - | |
| DROP SEQUENCE | X | X | X | - | |
| DROP SERVER | X | X | X | - | |
| DROP STATISTICS | X | X | X | - | |
| DROP SUBSCRIPTION | X | X | X | - | |
| DROP TABLE | X | X | X | - | |
| DROP TEXT SEARCH CONFIGURATION | X | X | X | - | |
| DROP TEXT SEARCH DICTIONARY | X | X | X | - | |
| DROP TEXT SEARCH PARSER | X | X | X | - | |
| DROP TEXT SEARCH TEMPLATE | X | X | X | - | |
| DROP TRIGGER | X | X | X | - | |
| DROP TYPE | X | X | X | - | |
| DROP USER MAPPING | X | X | X | - | |
| DROP VIEW | X | X | X | - | |
| GRANT | X | X | - | - | 只对本地对象 |
| IMPORT FOREIGN SCHEMA | X | X | - | - | |
| REFRESH MATERIALIZED VIEW | X | X | - | - | |
| REVOKE | X | X | - | - | 只对本地对象 |
| SECURITY LABEL | X | X | - | - | 只对本地对象 |
| SELECT INTO | X | X | - | - |
3.3.用 C 编写事件触发器函数
本节介绍事件触发器函数接口的低级细节。这些信息仅在使用 C编写事件触发器函数时需要。如果您使用的是更高级别的语言,则这些细节已经为您处理了。在大多数情况下,您应该在使用C 编写事件触发器之前考虑使用过程语言。每种过程语言的文档都解释了如何在该语言中编写事件触发器。
事件触发器函数必须使用“版本 1”函数管理器接口。
当事件触发器管理器调用函数时,它不会传递任何普通参数,但它会传递一个指向EventTriggerData结构的“上下文”指针。C函数可以通过执行宏来检查它们是否是从事件触发器管理器调用的:
CALLED_AS_EVENT_TRIGGER(fcinfo)
它会展开为:
((fcinfo)->context != NULL && IsA((fcinfo)->context, EventTriggerData))
如果返回 true,则可以安全地将fcinfo->context强制转换为类型EventTriggerData*并使用指向的EventTriggerData结构。该函数必须不更改EventTriggerData结构或其指向的任何数据。
struct EventTriggerData在commands/event_trigger.h中定义:
typedef struct EventTriggerData
{
NodeTag type;
const char *event; /* 事件名称 */
Node *parsetree; /* 解析树*/
CommandTag tag; /* 命令标记 */
} EventTriggerData;
其中成员定义如下:
type
始终为T_EventTriggerData。
event
描述调用函数的事件,其中之一是"ddl_command_start"、"ddl_command_end"、"sql_drop"、"table_rewrite"。有关这些事件的含义,请参见事件触发器行为概述。
parsetree
指向命令的解析树的指针。有关详细信息,请检查UXDB源代码。解析树结构可能会随时更改,恕不另行通知。
tag
与运行事件触发器的事件相关联的命令标记,例如"CREATE FUNCTION"。
事件触发器函数必须返回NULL指针(不是SQL空值,即不要将isNull设置为true)。
3.4.一个完整的事件触发器示例
这是一个非常简单的用C编写的事件触发器函数示例。(有关用过程性语言编写的触发器示例,请参见过程性语言的文档。)
函数noddl每次被调用时都会引发异常。事件触发器定义将该函数与ddl_command_start事件相关联。其效果是防止所有DDL命令(除事件触发器行为概述提到的例外情况外)运行。
这是触发器函数的源代码:
#include "uxdb.h"
#include "commands/event_trigger.h"
UX_MODULE_MAGIC;
UX_FUNCTION_INFO_V1(noddl);
Datum
noddl(UX_FUNCTION_ARGS)
{
EventTriggerData *trigdata;
if (!CALLED_AS_EVENT_TRIGGER(fcinfo)) /* internal error */
elog(ERROR, "not fired by event trigger manager");
trigdata = (EventTriggerData *) fcinfo->context;
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("command \"%s\" denied", trigdata->tag)));
UX_RETURN_NULL();
}
编译源代码后(请参见编译和链接动态加载函数),声明函数和触发器:
CREATE FUNCTION noddl() RETURNS event_trigger
AS 'noddl' LANGUAGE C;
CREATE EVENT TRIGGER noddl ON ddl_command_start
EXECUTE FUNCTION noddl();
现在,您可以测试触发器的操作:
=# \dy
List of event triggers
Name | Event | Owner | Enabled | Function | Tags
-------+-------------------+-------+---------+----------+------
noddl | ddl_command_start | dim | enabled | noddl |
(1 row)
=# CREATE TABLE foo(id serial);
ERROR: command "CREATE TABLE" denied
在这种情况下,为了能够在需要时运行某些DDL命令,您必须删除事件触发器或禁用它。禁用触发器只需在事务期间进行即可:
BEGIN;
ALTER EVENT TRIGGER noddl DISABLE;
CREATE TABLE foo (id serial);
ALTER EVENT TRIGGER noddl ENABLE;
COMMIT;
(请注意,事件触发器本身的DDL命令不受事件触发器的影响。)
3.5.表重写事件触发器示例
通过table_rewrite事件,可以实现仅允许在维护窗口中重写表的表重写策略。
以下是实现这种策略的示例。
CREATE OR REPLACE FUNCTION no_rewrite()
RETURNS event_trigger
LANGUAGE pluxsql AS
$$
---
--- 实现本地表重写策略:
--- public.foo 永远不允许重写
--- 其他表只允许在凌晨1点至6点之间重写
--- 除非它们有超过100个块
---
DECLARE
table_oid oid := ux_event_trigger_table_rewrite_oid();
current_hour integer := extract('hour' from current_time);
pages integer;
max_pages integer := 100;
BEGIN
IF ux_event_trigger_table_rewrite_oid() = 'public.foo'::regclass
THEN
RAISE EXCEPTION 'you''re not allowed to rewrite the table %',
table_oid::regclass;
END IF;
SELECT INTO pages relpages FROM ux_class WHERE oid = table_oid;
IF pages > max_pages
THEN
RAISE EXCEPTION 'rewrites only allowed for table with less than % pages',
max_pages;
END IF;
IF current_hour NOT BETWEEN 1 AND 6
THEN
RAISE EXCEPTION 'rewrites only allowed between 1am and 6am';
END IF;
END;
$$;
CREATE EVENT TRIGGER no_rewrite_allowed
ON table_rewrite
EXECUTE FUNCTION no_rewrite();
4.规则系统
本章讨论了UXDB中的规则系统。生产规则系统在概念上很简单,但在实际使用中涉及许多微妙的问题。
其他一些数据库系统定义了活动数据库规则,通常是存储过程和触发器。在UXDB中,这些也可以使用函数和触发器来实现。
规则系统(更准确地说,是查询重写规则系统)与存储过程和触发器完全不同。它修改查询以考虑规则,然后将修改后的查询传递给查询计划器进行规划和执行。执行。它非常强大,可以用于许多事情,例如查询语言过程、视图和版本。这个规则系统的理论基础和能力也在[ston90b]和[ong90]中讨论。
4.1.查询树
要理解规则系统的工作原理,需要知道它何时被调用以及其输入和输出是什么。
规则系统位于解析器和计划器之间。它接收解析器的输出,即一个查询树,以及用户定义的重写规则,这些规则也是带有一些额外信息的查询树,并创建零个或多个查询树作为结果。因此,它的输入和输出始终是解析器本身可以生成的内容,因此,它看到的任何内容基本上都可以表示为一个SQL 语句。
那么什么是查询树?它是 SQL 语句的内部表示,其中构建它的单个部分被分别存储。如果设置配置参数debug_print_parse、debug_print_rewritten或debug_print_plan,则可以在服务器日志中显示这些查询树。规则操作也被存储为查询树,在系统目录ux_rewrite中。它们的格式不像日志输出那样,但它们包含完全相同的信息。
阅读原始查询树需要一些经验。但是,由于查询树的 SQL 表示足以理解规则系统,因此本章不会教授如何阅读它们。
在阅读本章中查询树的 SQL 表示时,需要能够识别语句在查询树结构中被分解成的部分。查询树的部分包括:
-
命令类型
这是一个简单的值,告诉我们哪个命令(
SELECT、INSERT、UPDATE、DELETE)生成了查询树。 -
范围表
范围表是在查询中使用的关系列表。在
SELECT语句中,这些是在 FROM关键字之后给出的关系。每个范围表条目标识一个表或视图,并告诉我们如何访问它们。在查询的其他部分中它被称为什么名字并不重要。在查询树中,范围表条目是通过编号而不是名称引用的,因此如果存在重复名称,就像在SQL语句中一样,这并不重要。这可能发生在规则的范围表合并后。本章的示例不会出现这种情况。
-
结果关系
结果关系是范围表中的索引,用于标识查询结果所在的关系。
SELECT查询没有结果关系。(SELECT INTO的特殊情况与CREATE TABLE后跟INSERT ... SELECT基本相同,这里不单独讨论。)对于
INSERT、UPDATE和DELETE命令,结果关系是要进行更改的表(或视图!)。 -
目标列表
是定义查询结果的表达式列表。在SELECT的情况下,这些表达式是构建查询的最终输出的表达式。它们对应于SELECT和FROM关键字之间的表达式。(*只是关系的所有列名的缩写。它由解析器扩展为单个列,因此规则系统从未看到它。)
DELETE命令不需要普通目标列表,因为它们不产生任何结果。相反,规划器将一个特殊的CTID条目添加到空目标列表中,以允许执行器找到要删除的行。(当结果关系是普通表时,添加CTID。如果是视图,则由规则系统添加整行变量。)对于
INSERT命令,目标列表描述应该进入结果关系的新行。它由VALUES子句中的表达式或INSERT ...SELECT中的表达式组成。重写过程的第一步为任何未被原始命令分配但具有默认值的列添加目标列表条目。任何剩余的列(既没有给定值也没有默认值)将由规划器用常量空表达式填充。对于
UPDATE命令,目标列表描述应替换旧行的新行。在规则系统中,它仅包含命令的SET column = expression部分中的表达式。规划器将通过插入将旧行的值复制到新行的表达式来处理缺少的列。就像DELETE一样,添加CTID或整行变量,以便执行器可以标识要更新的旧行。目标列表中的每个条目都包含一个表达式,可以是常量值、指向范围表中一个关系的列的变量、参数或由函数调用、常量、变量、运算符等组成的表达式树。
-
条件
查询的限定是一个类似于目标列表条目中包含的表达式的表达式。该表达式的结果值是一个布尔值,指示操作是否符合条件。最终结果行的(
INSERT、UPDATE、DELETE或SELECT) 是否应该被执行取决于它对应的 SQL 语句的WHERE子句。 -
连接树
查询的连接树显示了
FROM子句的结构。对于像SELECT ... FROM a, b, c这样的简单查询,连接树只是FROM项的列表,因为我们可以以任何顺序连接它们。但是当使用JOIN表达式,特别是外连接时,我们必须按照连接的顺序进行连接。在这种情况下,连接树显示了JOIN表达式的结构。与特定JOIN子句(来自ON或USING表达式)相关联的限制被存储为附加到这些连接树节点的限定表达式。事实证明,将顶层WHERE表达式存储为附加到顶层连接树项的限定表达式也很方便。因此,连接树实际上代表了SELECT的FROM和WHERE子句。 -
其他
查询树的其他部分,如
ORDER BY子句,在这里不是很重要。规则系统在应用规则时会替换其中的一些条目,但这与规则系统的基本原理没有太大关系。
4.2.视图和规则系统
在UXDB中,视图是使用规则系统实现的。实际上,以下两个命令:
CREATE VIEW myview AS SELECT * FROM mytab;
与以下两个命令:
CREATE TABLE myview (same column list as mytab);
CREATE RULE "_RETURN" AS ON SELECT TO myview DO INSTEAD
SELECT * FROM mytab;
之间基本没有区别,因为这正是CREATE VIEW命令在内部执行的操作。这会产生一些副作用。其中之一是,在UXDB系统目录中,视图的信息与表完全相同。因此,对于解析器来说,表和视图没有任何区别。它们是相同的:关系。
4.2.1.SELECT规则的工作原理
ON SELECT规则会作为最后一步应用于所有查询,即使给定的命令是INSERT、UPDATE或DELETE。与其他命令类型的规则不同,它们修改原始查询树而不是创建新的查询树。因此,首先介绍SELECT规则。
目前,ON SELECT规则中只能有一个操作,并且它必须是SELECT。应该有一个无条件的SELECT操作,它是INSTEAD。这个限制是为了使规则足够安全,以便将它们开放给普通用户,并且它限制了ON SELECT规则的行为类似于视图。
本章的示例是两个连接视图,它们进行一些计算,以及一些使用它们的视图。两个第一视图之一稍后通过添加INSERT、UPDATE和DELETE操作的规则进行自定义,以便最终结果将是一个表现为具有一些魔术功能的真实表格的视图。这不是一个简单的例子,这使得事情变得更加困难。但是最好有一个例子,逐步涵盖所有讨论的要点,而不是有许多不同的例子,可能会混淆思路。
我们在前两个规则系统描述中需要的真实表格如下:
CREATE TABLE shoe_data (
shoename text, -- primary key
sh_avail integer, -- available number of pairs
slcolor text, -- preferred shoelace color
slminlen real, -- minimum shoelace length
slmaxlen real, -- maximum shoelace length
slunit text -- length unit
);
CREATE TABLE shoelace_data (
sl_name text, -- primary key
sl_avail integer, -- available number of pairs
sl_color text, -- shoelace color
sl_len real, -- shoelace length
sl_unit text -- length unit
);
CREATE TABLE unit (
un_name text, -- primary key
un_fact real -- factor to transform to cm
);
正如您所看到的,它们代表鞋店数据。
视图的创建如下:
CREATE VIEW shoe AS
SELECT sh.shoename,
sh.sh_avail,
sh.slcolor,
sh.slminlen,
sh.slminlen * un.un_fact AS slminlen_cm,
sh.slmaxlen,
sh.slmaxlen * un.un_fact AS slmaxlen_cm,
sh.slunit
FROM shoe_data sh, unit un
WHERE sh.slunit = un.un_name;
CREATE VIEW shoelace AS
SELECT s.sl_name,
s.sl_avail,
s.sl_color,
s.sl_len,
s.sl_unit,
s.sl_len * u.un_fact AS sl_len_cm
FROM shoelace_data s, unit u
WHERE s.sl_unit = u.un_name;
CREATE VIEW shoe_ready AS
SELECT rsh.shoename,
rsh.sh_avail,
rsl.sl_name,
rsl.sl_avail,
least(rsh.sh_avail, rsl.sl_avail) AS total_avail
FROM shoe rsh, shoelace rsl
WHERE rsl.sl_color = rsh.slcolor
AND rsl.sl_len_cm >= rsh.slminlen_cm
AND rsl.sl_len_cm <= rsh.slmaxlen_cm;
CREATE VIEW命令用于创建shoelace视图(它是我们拥有的最简单的视图之一),它将创建一个关系shoelace和一个条目ux_rewrite,告诉我们每当在查询的范围表中引用关系shoelace时,必须应用一个重写规则。该规则没有规则限定(稍后将讨论非SELECT规则,因为当前SELECT规则不能具有它们),并且它是INSTEAD。请注意,规则限定与查询限定不同。我们的规则操作具有查询限定条件。该规则的操作是一个查询树,它是视图创建命令中SELECT语句的副本。
注意
在ux_rewrite条目中可以看到的两个用于NEW和OLD的额外范围表条目与SELECT规则无关。
现在,我们填充unit、shoe_data和shoelace_data,并在视图上运行一个简单的查询:
INSERT INTO unit VALUES ('cm', 1.0);
INSERT INTO unit VALUES ('m', 100.0);
INSERT INTO unit VALUES ('inch', 2.54);
INSERT INTO shoe_data VALUES ('sh1', 2, 'black', 70.0, 90.0, 'cm');
INSERT INTO shoe_data VALUES ('sh2', 0, 'black', 30.0, 40.0, 'inch');
INSERT INTO shoe_data VALUES ('sh3', 4, 'brown', 50.0, 65.0, 'cm');
INSERT INTO shoe_data VALUES ('sh4', 3, 'brown', 40.0, 50.0, 'inch');
INSERT INTO shoelace_data VALUES ('sl1', 5, 'black', 80.0, 'cm');
INSERT INTO shoelace_data VALUES ('sl2', 6, 'black', 100.0, 'cm');
INSERT INTO shoelace_data VALUES ('sl3', 0, 'black', 35.0 , 'inch');
INSERT INTO shoelace_data VALUES ('sl4', 8, 'black', 40.0 , 'inch');
INSERT INTO shoelace_data VALUES ('sl5', 4, 'brown', 1.0 , 'm');
INSERT INTO shoelace_data VALUES ('sl6', 0, 'brown', 0.9 , 'm');
INSERT INTO shoelace_data VALUES ('sl7', 7, 'brown', 60 , 'cm');
INSERT INTO shoelace_data VALUES ('sl8', 1, 'brown', 40 , 'inch');
SELECT * FROM shoelace;
sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
-----------+----------+----------+--------+---------+-----------
sl1 | 5 | black | 80 | cm | 80
sl2 | 6 | black | 100 | cm | 100
sl7 | 7 | brown | 60 | cm | 60
sl3 | 0 | black | 35 | inch | 88.9
sl4 | 8 | black | 40 | inch | 101.6
sl8 | 1 | brown | 40 | inch | 101.6
sl5 | 4 | brown | 1 | m | 100
sl6 | 0 | brown | 0.9 | m | 90
(8 rows)
这是您在我们的视图中可以做的最简单的 SELECT,因此我们借此机会解释视图规则的基础知识。SELECT * FROM 鞋带由解析器解释并生成查询树:
SELECT shoelace.sl_name, shoelace.sl_avail,
shoelace.sl_color, shoelace.sl_len,
shoelace.sl_unit, shoelace.sl_len_cm
FROM shoelace shoelace;
这是给规则系统的。规则系统遍历范围表并检查是否存在针对任何关系的规则。在处理鞋带的范围表条目(目前为止唯一的一个)时,它会找到具有查询树的_RETURN 规则:
SELECT s.sl_name, s.sl_avail,
s.sl_color, s.sl_len, s.sl_unit,
s.sl_len * u.un_fact AS sl_len_cm
FROM shoelace old, shoelace new,
shoelace_data s, unit u
WHERE s.sl_unit = u.un_name;
要展开视图,重写器只需创建一个包含规则的操作查询树的子查询范围表条目,并用这个范围表条目替换引用视图的原始条目。得到的重写查询树几乎与输入:
SELECT shoelace.sl_name, shoelace.sl_avail,
shoelace.sl_color, shoelace.sl_len,
shoelace.sl_unit, shoelace.sl_len_cm
FROM (SELECT s.sl_name,
s.sl_avail,
s.sl_color,
s.sl_len,
s.sl_unit,
s.sl_len * u.un_fact AS sl_len_cm
FROM shoelace_data s, unit u
WHERE s.sl_unit = u.un_name) shoelace;
但是有一点不同: 子查询的 range 表有两个额外的条目,即shoelace old和shoelace new。这些条目不直接参与查询,因为它们不被子查询的连接树或目标列表引用。重写器使用它们来存储访问特权检查信息,这些信息最初出现在引用视图的范围表条目中。通过这种方式,执行程序仍然会检查用户是否拥有访问视图的适当权限,即使在重写的查询中没有直接使用视图。
这是适用的第一条规则。规则系统将继续检查顶部查询中剩余的范围表条目(在本例中没有更多) ,并将递归检查添加的子查询中的范围表条目,以查看它们中是否有任何引用视图。(但是它不会展开旧的或者新的,否则我们将会有无限的递归!)在这个示例中,没有关于 shoelace_data 或 unit 的重写规则,因此重写已经完成,上面的结果是给计划者的最终结果。
现在我们想要编写一个查询,找出当前商店中哪些鞋子有与之匹配的鞋带(颜色和长度),并且完全匹配的鞋带对数大于或等于两个。
SELECT * FROM shoe_ready WHERE total_avail >= 2;
shoename | sh_avail | sl_name | sl_avail | total_avail
----------+----------+---------+----------+-------------
sh1 | 2 | sl1 | 5 | 2
sh3 | 4 | sl7 | 7 | 4
(2 rows)
解析器的输出是查询树:
SELECT shoe_ready.shoename, shoe_ready.sh_avail,
shoe_ready.sl_name, shoe_ready.sl_avail,
shoe_ready.total_avail
FROM shoe_ready shoe_ready
WHERE shoe_ready.total_avail >= 2;
首先应用的规则是shoe_ready视图的规则,它导致查询树如下:
SELECT shoe_ready.shoename, shoe_ready.sh_avail,
shoe_ready.sl_name, shoe_ready.sl_avail,
shoe_ready.total_avail
FROM (SELECT rsh.shoename,
rsh.sh_avail,
rsl.sl_name,
rsl.sl_avail,
least(rsh.sh_avail, rsl.sl_avail) AS total_avail
FROM shoe rsh, shoelace rsl
WHERE rsl.sl_color = rsh.slcolor
AND rsl.sl_len_cm >= rsh.slminlen_cm
AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready
WHERE shoe_ready.total_avail >= 2;
类似地,将shoe和shoelace的规则替换到子查询的范围表中,得到一个三层的最终查询树:
SELECT shoe_ready.shoename, shoe_ready.sh_avail,
shoe_ready.sl_name, shoe_ready.sl_avail,
shoe_ready.total_avail
FROM (SELECT rsh.shoename,
rsh.sh_avail,
rsl.sl_name,
rsl.sl_avail,
least(rsh.sh_avail, rsl.sl_avail) AS total_avail
FROM (SELECT sh.shoename,
sh.sh_avail,
sh.slcolor,
sh.slminlen,
sh.slminlen * un.un_fact AS slminlen_cm,
sh.slmaxlen,
sh.slmaxlen * un.un_fact AS slmaxlen_cm,
sh.slunit
FROM shoe_data sh, unit un
WHERE sh.slunit = un.un_name) rsh,
(SELECT s.sl_name,
s.sl_avail,
s.sl_color,
s.sl_len,
s.sl_unit,
s.sl_len * u.un_fact AS sl_len_cm
FROM shoelace_data s, unit u
WHERE s.sl_unit = u.un_name) rsl
WHERE rsl.sl_color = rsh.slcolor
AND rsl.sl_len_cm >= rsh.slminlen_cm
AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready
WHERE shoe_ready.total_avail > 2;
这看起来可能效率不高,但规划器将把它折叠成一个单层的查询树,通过“拉起”子查询,然后像手动编写一样计划连接。因此,折叠查询树是一个优化,重写系统不需要考虑它。
4.2.2.非SELECT语句中的视图规则
在非SELECT语句中的视图规则有两个细节没有在上面的描述中涉及。这些是命令类型和结果关系。实际上,视图规则不需要命令类型,但结果关系可能会影响查询重写器的工作方式,因为如果结果关系是视图,则需要特别注意。
查询树对于SELECT和其他命令的查询树之间只有很少的区别。显然,它们具有不同的命令类型,对于除SELECT之外的命令,结果关系关系指向范围表条目,其中结果应该放置。其他所有内容都完全相同。因此,具有列a和b的两个表t1和t2的查询语句树:
SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a;
UPDATE t1 SET b = t2.b FROM t2 WHERE t1.a = t2.a;
几乎相同。特别是:
-
范围表包含表t1和t2的条目。
-
目标列表包含一个变量,指向表t2的列b的范围表条目。
-
资格表达式比较两个范围表条目的列a是否相等。
-
连接树显示t1和t2之间的简单连接。
结果是,这两个查询树都会产生类似的执行计划:它们都是两个表的连接。对于UPDATE,规划器将从t1添加缺少的列到目标列表中,最终的查询树将读取为:
UPDATE t1 SET a = t1.a, b = t2.b FROM t2 WHERE t1.a = t2.a;
因此,在连接上运行的执行程序将生成与以下结果集完全相同的结果:
SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a = t2.a;
但是,在UPDATE中存在一个小问题:执行连接的执行程序计划部分不关心连接的结果用于什么。它只产生一行结果集。这是一个SELECT命令,另一个是UPDATE,由执行程序更高层次的处理,它知道这是一个UPDATE,并且知道此结果应该进入表t1。但是,哪一行需要用新行替换?
为了解决这个问题,在UPDATE(以及DELETE)语句中添加了另一个目标列表条目:当前元组ID(CTID)。这是一个包含行的文件块号和块中位置的系统列。知道表格后,可以使用CTID检索要更新的t1的原始行。在将CTID添加到目标列表后,查询实际上看起来像:
SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;
现在,UXDB的另一个细节进入了舞台。旧表行不会被覆盖,这就是为什么ROLLBACK很快的原因。在UPDATE中,将新的结果行插入表中(在剥离CTID之后),并在CTID指向的旧行的行头中,将cmax和xmax条目设置为当前命令计数器和当前事务ID。因此,旧行被隐藏,事务提交后,吸尘器最终可以删除死行。
了解所有这些,我们可以以完全相同的方式将视图规则应用于任何命令。没有区别。
4.2.3.UXDB 中视图的强大功能
上面的示例演示了规则系统如何将视图定义合并到原始查询树中。在第二个示例中,从一个视图简单地 SELECT 创建了一个最终查询树,该查询树是4个表的连接(unit两次使用了不同的名称)。
使用规则系统实现视图的好处在于,规划器具有关于必须扫描哪些表以及这些表之间的关系以及来自视图的限制性限定符以及来自原始查询的限定符的所有信息,这些信息都包含在一个单一的查询树中。即使原始查询已经是视图的连接,这仍然是情况。规划器必须决定执行查询的最佳路径,规划器拥有的信息越多,决策就越好。而且,UXDB中实现的规则系统确保这是查询到目前为止的所有可用信息。
4.2.4.更新视图
如果将视图命名为 INSERT、UPDATE 或 DELETE的目标关系,会发生什么?执行上述替换将给出一个查询树,其中结果关系指向一个子查询范围表条目,这将无法工作。然而,UXDB可以通过几种方式支持更新视图的外观。按用户体验复杂性的顺序,它们是:自动替换视图中的基础表、执行用户定义的触发器或根据用户定义的规则重写查询。下面讨论这些选项。
如果子查询从单个基本关系中选择并且足够简单,则重写器可以自动将子查询替换为基础关系,以便以适当的方式将 INSERT、UPDATE 或DELETE 应用于基础关系。
或者,操作可以由视图上的用户提供的 INSTEAD OF 触发器处理。在这种情况下,重写的工作方式略有不同。对于INSERT,重写器根本不处理视图,将其保留为查询的结果关系。对于 UPDATE 和DELETE,仍然需要扩展视图查询以生成命令将尝试更新或删除的“旧” 行。因此,视图像往常一样扩展,但是在查询中添加另一个未扩展的范围表条目,以表示视图作为结果关系的能力。
现在出现的问题是如何识别要在视图中更新的行。请记住,当结果关系是表时,会将一个特殊的 CTID条目添加到目标列表中,以标识要更新或删除的行的物理位置。如果结果关系是视图,则这不起作用,因为视图没有任何CTID,因为其行没有物理位置。对于视图的 UPDATE 或 DELETE操作,不会直接更新实际物理位置的行。相反,目标列表中添加了一个特殊的wholerow 条目,该条目会扩展以包括视图中的所有列。执行器使用此值来向 INSTEAD OF触发器提供“旧”行。触发器根据旧行和新行的值来确定要更新的内容。另一种可能性是用户定义 INSTEAD 规则,指定视图上的 INSERT、UPDATE 和 DELETE命令的替代操作。这些规则将重写命令,通常是将其转换为更新一个或多个表的命令,而不是视图。
请注意,规则首先进行评估,重写原始查询,然后再进行计划和执行。因此,如果视图具有 INSTEAD OF触发器以及 INSERT、UPDATE 或 DELETE的规则,则规则将首先进行评估,根据结果,可能根本不使用触发器。
对于简单视图的 INSERT、UPDATE 或 DELETE查询的自动重写始终是最后尝试的。因此,如果视图具有规则或触发器,则它们将覆盖自动可更新视图的默认行为。
如果视图没有 INSTEAD 规则或 INSTEAD OF触发器,并且重写程序无法自动将查询重写为基础基本关系的更新,则会抛出错误,因为执行器无法更新视图。
4.3.物化视图
在 UXDB 中,物化视图使用与视图相同的规则系统,但将结果以类似表的形式持久化。以下是物化视图和表之间的主要区别:
CREATE MATERIALIZED VIEW mymatview AS SELECT * FROM mytab;
和:
CREATE TABLE mymatview AS SELECT * FROM mytab;
物化视图不能直接进行后续更新,而用于创建物化视图的查询与视图的查询存储方式完全相同,因此可以使用以下命令生成物化视图的新数据:
REFRESH MATERIALIZED VIEW mymatview;
在 UXDB系统目录中,物化视图的信息与表或视图完全相同。因此,对于解析器而言,物化视图就像表或视图一样是一个关系。当在查询中引用物化视图时,数据直接从物化视图返回,就像从表中返回一样;规则仅用于填充物化视图。
虽然访问存储在物化视图中的数据通常比直接访问基础表或通过视图访问要快得多,但数据并不总是最新的;但有时不需要最新数据。考虑记录销售的表:
CREATE TABLE invoice (
invoice_no integer PRIMARY KEY,
seller_no integer, -- 销售员 ID
invoice_date date, -- 销售日期
invoice_amt numeric(13,2) -- 销售金额
);
如果人们想要快速绘制历史销售数据的图表,他们可能想要进行汇总,并且可能不关心当前日期的不完整数据:
CREATE MATERIALIZED VIEW sales_summary AS
SELECT
seller_no,
invoice_date,
sum(invoice_amt)::numeric(13,2) as sales_amt
FROM invoice
WHERE invoice_date < CURRENT_DATE
GROUP BY
seller_no,
invoice_date;
CREATE UNIQUE INDEX sales_summary_seller
ON sales_summary (seller_no, invoice_date);
这个物化视图可能对于在为销售人员创建的仪表板中显示图表非常有用。可以安排一个作业来更新物化视图。每晚使用以下SQL语句更新统计数据:
REFRESH MATERIALIZED VIEW sales_summary;
材料化视图的另一个用途是通过外部数据包装器从远程系统带来的数据,以便更快地访问。下面是一个使用file_fdw的简单示例,附带计时,但由于这是在本地系统上使用缓存,因此与访问远程系统相比,性能差异通常比此处显示的更大。请注意,我们还利用了在材料化视图上放置索引的能力,而file_fdw不支持索引;对于其他类型的外部数据访问,这种优势可能不适用。
设置:
CREATE EXTENSION file_fdw;
CREATE SERVER local_file FOREIGN DATA WRAPPER file_fdw;
CREATE FOREIGN TABLE words (word text NOT NULL)
SERVER local_file
OPTIONS (filename '/usr/share/dict/words');
CREATE MATERIALIZED VIEW wrd AS SELECT * FROM words;
CREATE UNIQUE INDEX wrd_word ON wrd (word);
CREATE EXTENSION ux_trgm;
CREATE INDEX wrd_trgm ON wrd USING gist (word gist_trgm_ops);
VACUUM ANALYZE wrd;
现在让我们拼写检查一个单词。直接使用file_fdw:
SELECT count(*) FROM words WHERE word = 'caterpiler';
count
-------
0
(1 row)
使用EXPLAIN ANALYZE,我们看到:
Aggregate (cost=21763.99..21764.00 rows=1 width=0) (actual time=188.180..188.181 rows=1 loops=1)
-> Foreign Scan on words (cost=0.00..21761.41 rows=1032 width=0) (actual time=188.177..188.177 rows=0 loops=1)
Filter: (word = 'caterpiler'::text)
Rows Removed by Filter: 479829
Foreign File: /usr/share/dict/words
Foreign File Size: 4953699
Planning time: 0.118 ms
Execution time: 188.273 ms
如果改用材料化视图,则查询速度会更快:
Aggregate (cost=4.44..4.45 rows=1 width=0) (actual time=0.042..0.042 rows=1 loops=1)
-> Index Only Scan using wrd_word on wrd (cost=0.42..4.44 rows=1 width=0) (actual time=0.039..0.039 rows=0 loops=1)
Index Cond: (word = 'caterpiler'::text)
Heap Fetches: 0
Planning time: 0.164 ms
Execution time: 0.117 ms
无论哪种方式,单词都拼错了,所以让我们看看我们可能想要的内容。再次使用file_fdw和ux_trgm:
SELECT word FROM words ORDER BY word <-> 'caterpiler' LIMIT 10;
word
---------------
cater
caterpillar
Caterpillar
caterpillars
caterpillar's
Caterpillar's
caterer
caterer's
caters
catered
(10 rows)
Limit (cost=11583.61..11583.64 rows=10 width=32) (actual time=1431.591..1431.594 rows=10 loops=1)
-> Sort (cost=11583.61..11804.76 rows=88459 width=32) (actual time=1431.589..1431.591 rows=10 loops=1)
Sort Key: ((word <-> 'caterpiler'::text))
Sort Method: top-N heapsort Memory: 25kB
-> Foreign Scan on words (cost=0.00..9672.05 rows=88459 width=32) (actual time=0.057..1286.455 rows=479829 loops=1)
Foreign File: /usr/share/dict/words
Foreign File Size: 4953699
Planning time: 0.128 ms
Execution time: 1431.679 ms
使用材料化视图:
Limit (cost=0.29..1.06 rows=10 width=10) (actual time=187.222..188.257 rows=10 loops=1)
-> Index Scan using wrd_trgm on wrd (cost=0.29..37020.87 rows=479829 width=10) (actual time=187.219..188.252 rows=10 loops=1)
Order By: (word <-> 'caterpiler'::text)
Planning time: 0.196 ms
Execution time: 198.640 ms
如果您可以容忍定期更新远程数据到本地数据库,则性能优势可能会很大。
4.4.INSERT、UPDATE 和 DELETE 规则
在 INSERT、UPDATE 和 DELETE 上定义的规则与前一节中描述的视图规则有很大的不同。首先,它们的 CREATE RULE 命令允许更多:
-
它们可以没有动作。
-
它们可以有多个动作。
-
它们可以是
INSTEAD或ALSO(默认)。 -
伪关系
NEW和OLD变得有用。 -
它们可以有规则限定。
其次,它们不会直接修改查询树。相反,它们会创建零个或多个新的查询树,并且可以丢弃原始查询树。
注意
在许多情况下,可以通过触发器来执行 INSERT/UPDATE/DELETE的任务。触发器的符号表示稍微复杂一些,但它们的语义要简单得多。当原始查询包含易变函数时,规则往往会产生令人惊讶的结果:易变函数可能会在执行规则的过程中被执行多次。
此外,这些类型的规则不支持某些情况,特别是原始查询中包含 WITH 子句和在查询中使用的临时表。
4.4.1.更新规则如何工作
语法结构
CREATE [ OR REPLACE ] RULE name AS ON event
TO table [ WHERE condition ]
DO [ ALSO | INSTEAD ] { NOTHING | command | ( command ; command ... ) }
下面的更新规则是指在INSERT、UPDATE或DELETE上定义的规则。
当查询树的结果关系和命令类型与CREATE RULE命令中给出的对象和事件相同时,规则系统将应用更新规则。对于更新规则,规则系统创建一个查询树列表。最初,查询树列表为空。可以有零个(使用NOTHING关键字)、一个或多个操作。为简化起见,我们将查看一个具有一个操作的规则。此规则可以有资格或没有资格,并且可以是INSTEAD或ALSO(默认)。
什么是规则资格?它是一个限制,告诉规则的操作应该何时执行和何时不执行。此资格只能引用伪关系NEW和/或OLD,它们基本上表示作为对象给出的关系(但具有特殊含义)。
因此,对于一个操作规则,会产生以下三种情况的查询树。
-
没有资格,有
ALSO或INSTEAD的情况下使用规则操作的查询树和原始查询树的资格。
-
给定资格和
ALSO的情况下使用规则操作的查询树和规则资格以及原始查询树的资格。
-
给定资格和
INSTEAD的情况下使用规则操作的查询树和规则资格以及原始查询树的资格;并添加具有否定规则资格的原始查询树。
最后,如果规则是ALSO,则未更改的原始查询树将添加到列表中。由于只有有资格的INSTEAD规则已经添加了原始查询树,因此对于具有一个操作的规则,我们最终会得到一个或两个输出查询树。
对于ON INSERT规则,原始查询(如果没有被INSTEAD抑制)将在任何规则添加的操作之前执行。这允许操作查看已插入的行。但是对于ON UPDATE和ON DELETE规则,原始查询将在规则添加的操作之后执行。这确保操作可以查看要更新或要删除的行;否则,操作可能不执行任何操作,因为它们找不到与其资格匹配的行。
从规则操作生成的查询树再次投入重写系统中,可能会应用更多规则,从而产生更多的查询树。在更多或更少的查询树中。因此,规则的操作必须具有与规则本身不同的命令类型或不同的结果关系,否则这个递归过程将陷入无限循环。(规则的递归扩展将被检测并报告为错误。)
在ux_rewrite系统目录中发现的查询树仅是模板。由于它们可以引用NEW和OLD的范围表条目,因此在使用之前必须进行一些替换。对于对NEW的任何引用,将搜索原始查询的目标列表以查找相应的条目。如果找到,该条目的表达式将替换引用。否则,NEW对于UPDATE与OLD相同,或者被替换为null值(对于INSERT)。对于OLD的任何引用都将被替换为是结果关系的范围表条目的引用。
在系统完成应用更新规则之后,它将应用视图规则到生成的查询树中。视图无法插入新的更新操作,因此无需将更新规则应用于视图重写的输出。
4.4.1.1.逐步进行第一条规则
假设我们要跟踪shoelace_data关系中的sl_avail列的更改。因此,我们设置了一个日志表和一个规则,当在shoelace_data上执行UPDATE时,有条件地写入日志条目。
CREATE TABLE shoelace_log (
sl_name text, -- shoelace changed
sl_avail integer, -- new available value
log_who text, -- who did it
log_when timestamp -- when
);
CREATE RULE log_shoelace AS ON UPDATE TO shoelace_data
WHERE NEW.sl_avail <> OLD.sl_avail
DO INSERT INTO shoelace_log VALUES (
NEW.sl_name,
NEW.sl_avail,
current_user,
current_timestamp
);
现在有人这样做:
UPDATE shoelace_data SET sl_avail = 6 WHERE sl_name = 'sl7';
然后我们查看日志表:
SELECT * FROM shoelace_log;
sl_name | sl_avail | log_who | log_when
---------+----------+---------+----------------------------------
sl7 | 6 | Al | Tue Oct 20 16:14:45 1998 MET DST
(1 row)
这是我们预期的。在后台发生的是以下情况。解析器创建了查询树:
UPDATE shoelace_data SET sl_avail = 6
FROM shoelace_data shoelace_data
WHERE shoelace_data.sl_name = 'sl7';
有一个规则log_shoelace,它是ON UPDATE,具有规则限定表达式:
NEW.sl_avail <> OLD.sl_avail
和操作:
INSERT INTO shoelace_log VALUES (
new.sl_name, new.sl_avail,
current_user, current_timestamp )
FROM shoelace_data new, shoelace_data old;
(这看起来有点奇怪,因为通常无法编写INSERT ... VALUES ... FROM。这里的FROM子句只是指示查询树中存在范围表条目new和old。这些是必需的,以便它们可以被INSERT命令的查询树中的变量引用。)
该规则是有资格的ALSO规则,因此规则系统必须返回两个查询树:修改后的规则操作和原始查询树。
第1步,原始查询的范围表是将以下文本翻译成中文:并将其合并到规则的操作查询树中。结果如下:
INSERT INTO shoelace_log VALUES (
new.sl_name, new.sl_avail,
current_user, current_timestamp )
FROM shoelace_data new, shoelace_data old,
shoelace_data shoelace_data;
第2步,将规则限定条件添加到其中,因此结果集 仅限于sl_avail更改的行:
INSERT INTO shoelace_log VALUES (
new.sl_name, new.sl_avail,
current_user, current_timestamp )
FROM shoelace_data new, shoelace_data old,
shoelace_data shoelace_data
WHERE new.sl_avail <> old.sl_avail;
(这看起来更奇怪,因为INSERT ... VALUES也没有 WHERE子句,但计划程序和执行器将不会有困难。无论如何,他们需要支持相同的功能 INSERT ... SELECT。)
第3步,添加了原始查询树的限定条件,进一步将结果集限制为仅限于原始查询可能触及的行:
INSERT INTO shoelace_log VALUES (
new.sl_name, new.sl_avail,
current_user, current_timestamp )
FROM shoelace_data new, shoelace_data old,
shoelace_data shoelace_data
WHERE new.sl_avail <> old.sl_avail
AND shoelace_data.sl_name = 'sl7';
第4步,将对NEW的引用替换为原始查询树中的目标列表条目或匹配变量引用来自结果关系:
INSERT INTO shoelace_log VALUES (
shoelace_data.sl_name, 6,
current_user, current_timestamp )
FROM shoelace_data new, shoelace_data old,
shoelace_data shoelace_data
WHERE 6 <> old.sl_avail
AND shoelace_data.sl_name = 'sl7';
第5步,将OLD引用更改为结果关系引用:
INSERT INTO shoelace_log VALUES (
shoelace_data.sl_name, 6,
current_user, current_timestamp )
FROM shoelace_data new, shoelace_data old,
shoelace_data shoelace_data
WHERE 6 <> shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
由于规则是ALSO,因此我们还输出了 原始查询树。简而言之,规则系统的输出 是与这些语句对应的两个查询树:
INSERT INTO shoelace_log VALUES (
shoelace_data.sl_name, 6,
current_user, current_timestamp )
FROM shoelace_data
WHERE 6 <> shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
UPDATE shoelace_data SET sl_avail = 6
WHERE sl_name = 'sl7';
这些按照此顺序执行,这正是规则的目的。
替换和添加的限定条件确保,如果原始查询是:
UPDATE shoelace_data SET sl_color = 'green'
WHERE sl_name = 'sl7';
不会写入日志条目。在这种情况下,原始查询树不包含sl_avail的目标列表条目,因此NEW.sl_avail将获取替换为shoelace_data.sl_avail。因此,额外的由规则生成的命令是:
INSERT INTO shoelace_log VALUES (
shoelace_data.sl_name, shoelace_data.sl_avail,
current_user, current_timestamp )
FROM shoelace_data
WHERE shoelace_data.sl_avail <> shoelace_data.sl_avail
AND shoelace_data.sl_name = 'sl7';
这种限定条件永远不会成立。
如果原始查询修改了多行,它也会起作用。因此,如果有人发出命令:
UPDATE shoelace_data SET sl_avail = 0
WHERE sl_color = 'black';
实际上会更新四行(sl1、sl2、sl3 和 sl4)。但是 sl3 已经有 sl_avail = 0。在这种情况下,原始查询树的限定条件不同,结果会生成额外的查询树:
INSERT INTO shoelace_log
SELECT shoelace_data.sl_name, 0,
current_user, current_timestamp
FROM shoelace_data
WHERE 0 <> shoelace_data.sl_avail
AND shoelace_data.sl_color = 'black';
这个查询树肯定会插入三个新的日志条目。这是绝对正确的。
在这里,我们可以看到为什么重要的是最后执行原始查询树。如果先执行 UPDATE,所有行都已经被设置为零,因此记录插入将找不到任何行,其中 0 <> shoelace_data.sl_avail。
4.4.2.与视图协作
保护视图关系免受某些人尝试在其上运行INSERT、UPDATE或DELETE的可能性的简单方法是让这些查询树被丢弃。因此,我们可以创建规则:
CREATE RULE shoe_ins_protect AS ON INSERT TO shoe
DO INSTEAD NOTHING;
CREATE RULE shoe_upd_protect AS ON UPDATE TO shoe
DO INSTEAD NOTHING;
CREATE RULE shoe_del_protect AS ON DELETE TO shoe
DO INSTEAD NOTHING;
如果现在有人尝试在视图关系 shoe 上执行任何这些操作,规则系统将应用这些规则。由于规则没有动作并且是 INSTEAD,所以查询树的结果列表将为空,整个查询将变为空,因为在规则系统完成后没有剩余的优化或执行。
更复杂的使用规则系统的方法是创建规则,将查询树重写为在实际表上执行正确操作的查询树。为了在 shoelace 视图上执行此操作,我们创建以下规则:
CREATE RULE shoelace_ins AS ON INSERT TO shoelace
DO INSTEAD
INSERT INTO shoelace_data VALUES (
NEW.sl_name,
NEW.sl_avail,
NEW.sl_color,
NEW.sl_len,
NEW.sl_unit
);
CREATE RULE shoelace_upd AS ON UPDATE TO shoelace
DO INSTEAD
UPDATE shoelace_data
SET sl_name = NEW.sl_name,
sl_avail = NEW.sl_avail,
sl_color = NEW.sl_color,
sl_len = NEW.sl_len,
sl_unit = NEW.sl_unit
WHERE sl_name = OLD.sl_name;
CREATE RULE shoelace_del AS ON DELETE TO shoelace
DO INSTEAD
DELETE FROM shoelace_data
WHERE sl_name = OLD.sl_name;
如果希望在视图上支持 RETURING 查询,则需要使规则包括计算视图行的 RETURING 子句。对于单个表上的视图来说,这通常是非常琐碎的,但是对于像鞋带这样的连接视图来说,就有点乏味了。插入用例的一个示例是:
CREATE RULE shoelace_ins AS ON INSERT TO shoelace
DO INSTEAD
INSERT INTO shoelace_data VALUES (
NEW.sl_name,
NEW.sl_avail,
NEW.sl_color,
NEW.sl_len,
NEW.sl_unit
)
RETURNING
shoelace_data.*,
(SELECT shoelace_data.sl_len * u.un_fact
FROM unit u WHERE shoelace_data.sl_unit = u.un_name);
请注意,此规则支持视图上的INSERT和INSERT RETURNING查询 - 对于INSERT,RETURNING子句将被忽略。
现在假设偶尔有一批鞋带到达商店,还有一个大的零件清单。但是您不想每次手动更新shoelace视图。相反,我们设置了两个小表:一个可以将零件清单中的项目插入其中,另一个具有特殊技巧。这些创建命令为:
CREATE TABLE shoelace_arrive (
arr_name text,
arr_quant integer
);
CREATE TABLE shoelace_ok (
ok_name text,
ok_quant integer
);
CREATE RULE shoelace_ok_ins AS ON INSERT TO shoelace_ok
DO INSTEAD
UPDATE shoelace
SET sl_avail = sl_avail + NEW.ok_quant
WHERE sl_name = NEW.ok_name;
现在,您可以使用来自零件清单的数据填充表shoelace_arrive:
SELECT * FROM shoelace_arrive;
arr_name | arr_quant
----------+-----------
sl3 | 10
sl6 | 20
sl8 | 20
(3 rows)
快速查看当前数据:
SELECT * FROM shoelace;
sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
----------+----------+----------+--------+---------+-----------
sl1 | 5 | black | 80 | cm | 80
sl2 | 6 | black | 100 | cm | 100
sl7 | 6 | brown | 60 | cm | 60
sl3 | 0 | black | 35 | inch | 88.9
sl4 | 8 | black | 40 | inch | 101.6
sl8 | 1 | brown | 40 | inch | 101.6
sl5 | 4 | brown | 1 | m | 100
sl6 | 0 | brown | 0.9 | m | 90
(8 rows)
现在将到达的鞋带移入:
INSERT INTO shoelace_ok SELECT * FROM shoelace_arrive;
并检查结果:
SELECT * FROM shoelace ORDER BY sl_name;
sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
----------+----------+----------+--------+---------+-----------
sl1 | 5 | black | 80 | cm | 80
sl2 | 6 | black | 100 | cm | 100
sl7 | 6 | brown | 60 | cm | 60
sl4 | 8 | black | 40 | inch | 101.6
sl3 | 10 | black | 35 | inch | 88.9
sl8 | 21 | brown | 40 | inch | 101.6
sl5 | 4 | brown | 1 | m | 100
sl6 | 20 | brown | 0.9 | m | 90
(8 rows)
SELECT * FROM shoelace_log;
sl_name | sl_avail | log_who| log_when
---------+----------+--------+----------------------------------
sl7 | 6 | Al | Tue Oct 20 19:14:45 1998 MET DST
sl3 | 10 | Al | Tue Oct 20 19:25:16 1998 MET DST
sl6 | 20 | Al | Tue Oct 20 19:25:16 1998 MET DST
sl8 | 21 | Al | Tue Oct 20 19:25:16 1998 MET DST
(4 rows)
从一个INSERT ... SELECT到这些结果还有很长的路要走。查询树转换的描述将是本章的最后一部分。首先,是解析器的输出:
INSERT INTO shoelace_ok
SELECT shoelace_arrive.arr_name, shoelace_arrive.arr_quant
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok;
现在应用第一个规则shoelace_ok_ins,并将其转换为:
UPDATE shoelace
SET sl_avail = shoelace.sl_avail + shoelace_arrive.arr_quant
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok old, shoelace_ok new,
shoelace shoelace
WHERE shoelace.sl_name = shoelace_arrive.arr_name;
shoelace shoelace当 shoelace.sl_name = shoelace_arrive.arr_name 时,将数据从 shoelace_arrive 插入到 shoelace_ok 中。这是一个 INSTEAD 规则,它将原始的 shoelace_ok 插入语句丢弃。这个重写后的查询再次传递给规则系统,第二个应用的规则 shoelace_upd 产生:
UPDATE shoelace_data
SET sl_name = shoelace.sl_name,
sl_avail = shoelace.sl_avail + shoelace_arrive.arr_quant,
sl_color = shoelace.sl_color,
sl_len = shoelace.sl_len,
sl_unit = shoelace.sl_unit
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok old, shoelace_ok new,
shoelace shoelace, shoelace old,
shoelace new, shoelace_data shoelace_data
WHERE shoelace.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = shoelace.sl_name;
再次是一个 INSTEAD 规则,之前的查询树被丢弃。请注意,此查询仍使用视图 shoelace。但是规则系统还没有完成这一步,因此它继续并应用其上的 _RETURN 规则,我们得到:
UPDATE shoelace_data
SET sl_name = s.sl_name,
sl_avail = s.sl_avail + shoelace_arrive.arr_quant,
sl_color = s.sl_color,
sl_len = s.sl_len,
sl_unit = s.sl_unit
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok old, shoelace_ok new,
shoelace shoelace, shoelace old,
shoelace new, shoelace_data shoelace_data,
shoelace old, shoelace new,
shoelace_data s, unit u
WHERE s.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = s.sl_name;
最后,规则 log_shoelace 被应用,产生额外的查询树:
INSERT INTO shoelace_log
SELECT s.sl_name,
s.sl_avail + shoelace_arrive.arr_quant,
current_user,
current_timestamp
FROM shoelace_arrive shoelace_arrive, shoelace_ok shoelace_ok,
shoelace_ok old, shoelace_ok new,
shoelace shoelace, shoelace old,
shoelace new, shoelace_data shoelace_data,
shoelace old, shoelace new,
shoelace_data s, unit u,
shoelace_data old, shoelace_data new
shoelace_log shoelace_log
WHERE s.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = s.sl_name
AND (s.sl_avail + shoelace_arrive.arr_quant) <> s.sl_avail;
之后,规则系统用尽了规则并返回生成的查询树。因此,我们最终得到了两个等效于 SQL 语句的最终查询树:
INSERT INTO shoelace_log
SELECT s.sl_name,
s.sl_avail + shoelace_arrive.arr_quant,
current_user,
current_timestamp
FROM shoelace_arrive shoelace_arrive, shoelace_data shoelace_data,
shoelace_data s
WHERE s.sl_name = shoelace_arrive.arr_name
AND shoelace_data.sl_name = s.sl_name
AND s.sl_avail + shoelace_arrive.arr_quant <> s.sl_avail;
UPDATE shoelace_data
SET sl_avail = shoelace_data.sl_avail + shoelace_arrive.arr_quant
FROM shoelace_arrive shoelace_arrive,
shoelace_data shoelace_data,
shoelace_data s
WHERE s.sl_name = shoelace_arrive.sl_name
AND shoelace_data.sl_name = s.sl_name;
结果是,来自一个关系的数据插入到另一个关系中,变成对第三个关系的更新,变成对第四个关系的更新以及在第五个关系中记录最终更新,最终被简化为两个查询。
有一个小细节有点丑陋。查看这两个查询,发现 shoelace_data 关系在范围表中出现了两次,它可以被缩减为一次。规划器无法处理它,因此规则系统的输出执行计划将是:
Nested Loop
-> Merge Join
-> Seq Scan
-> Sort
-> Seq Scan on s
-> Seq Scan
-> Sort
-> Seq Scan on shoelace_arrive
-> Seq Scan on shoelace_data
而省略额外的范围表条目将导致:
Merge Join
-> Seq Scan
-> Sort
-> Seq Scan on s
-> Seq Scan
-> Sort
-> Seq Scan on shoelace_arrive
这将在日志表中产生完全相同的条目。因此,规则系统在表shoelace_data上多执行了一次不必要的扫描。而且在UPDATE中也会再次执行同样的冗余扫描。但是,使所有这些成为可能确实是一项非常艰巨的工作。
现在,我们展示一下UXDB规则系统及其功能的最终演示。假设您向数据库中添加了一些颜色非常特别的鞋带:
INSERT INTO shoelace VALUES ('sl9', 0, 'pink', 35.0, 'inch', 0.0);
INSERT INTO shoelace VALUES ('sl10', 1000, 'magenta', 40.0, 'inch', 0.0);
我们想要创建一个视图来检查哪些shoelace条目的颜色与任何鞋子不匹配。此视图为:
CREATE VIEW shoelace_mismatch AS
SELECT * FROM shoelace WHERE NOT EXISTS
(SELECT shoename FROM shoe WHERE slcolor = sl_color);
它的输出是:
SELECT * FROM shoelace_mismatch;
sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
---------+----------+----------+--------+---------+-----------
sl9 | 0 | pink | 35 | inch | 88.9
sl10 | 1000 | magenta | 40 | inch | 101.6
现在,我们希望设置这样一个功能,即从数据库中删除不匹配且缺货的鞋带。为了让UXDB变得更难一些,我们不会直接删除它。相反,我们创建了一个视图:
CREATE VIEW shoelace_can_delete AS
SELECT * FROM shoelace_mismatch WHERE sl_avail = 0;
并且这样做:
DELETE FROM shoelace WHERE EXISTS
(SELECT * FROM shoelace_can_delete
WHERE sl_name = shoelace.sl_name);
结果如下:
SELECT * FROM shoelace;
sl_name | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
---------+----------+----------+--------+---------+-----------
sl1 | 5 | black | 80 | cm | 80
sl2 | 6 | black | 100 | cm | 100
sl7 | 6 | brown | 60 | cm | 60
sl4 | 8 | black | 40 | inch | 101.6
sl3 | 10 | black | 35 | inch | 88.9
sl8 | 21 | brown | 40 | inch | 101.6
sl10 | 1000 | magenta | 40 | inch | 101.6
sl5 | 4 | brown | 1 | m | 100
sl6 | 20 | brown | 0.9 | m | 90
(9 rows)
对一个视图进行DELETE,其中包含4个嵌套/连接视图的子查询限定条件,其中一个子查询限定条件本身包含一个视图,并且使用计算的视图列,将被重写为一个单一的查询树,该查询树从实际表中删除所请求的数据。
在现实世界中,可能只有很少的情况需要这样的构造。但是,它让您感到舒适,因为它可以正常工作。
4.5.规则和权限
由于 UXDB 规则系统重写查询,因此会访问原始查询中未使用的其他表/视图。当使用更新规则时,这可能包括对表的写访问权限。
重写规则没有单独的所有者。关系(表或视图)的所有者自动成为为其定义的重写规则的所有者。除了与安全调用者视图相关的 SELECT规则,由于规则而使用的所有关系都会根据规则所有者的权限进行检查,而不是调用规则的用户的权限。这意味着,除了安全调用者视图外,用户只需要对其查询中明确命名的表/视图具有所需的权限。
例如:用户有一个电话号码列表,其中一些是私人的,其他的对办公室助手很有用。用户可以构建以下内容:
CREATE TABLE phone_data (person text, phone text, private boolean);
CREATE VIEW phone_number AS
SELECT person, CASE WHEN NOT private THEN phone END AS phone
FROM phone_data;
GRANT SELECT ON phone_number TO assistant;
除该用户和数据库超级用户外,没有人可以访问 phone_data 表。但是由于 GRANT,助手可以在 phone_number视图上运行 SELECT。规则系统将从 phone_number 重写 SELECT 为从 phone_data选择。由于用户是 phone_number 的所有者,因此也是规则的所有者,对 phone_data的读取访问现在会根据用户的权限进行检查,因此查询被允许。对于访问 phone_number的检查也会执行,但是这是针对调用用户进行的,因此除了用户和助手外,没有人可以访问phone_number。
特权是逐条检查的。因此,助手是目前唯一能够查看公共电话号码的人。但是助手可以设置另一个视图,并将其授权给公众。然后,任何人都可以通过助手的视图查看phone_number数据。助手不能直接访问phone_data。实际上,助手可以这样做,但是在权限检查期间,每次访问都会被拒绝。一旦用户注意到助手打开了他们的phone_number视图,用户可以撤销助手的访问权限。立即,对助手视图的任何访问都将失败。
有人可能认为逐条检查是一个安全漏洞,但实际上并不是这样。如果不是这样,助手可以设置一个与phone_number具有相同列的表,并将数据每天复制到那里。然后,这是助手自己的数据,助手可以授权给任何他们想要的人。GRANT命令的意思是“我信任你”。如果你信任的人做了上述事情,那么就是时候重新考虑并使用REVOKE了。
请注意,虽然视图可以使用上述技术隐藏某些列的内容,但它们不能可靠地隐藏未见行中的数据,除非设置了security_barrier标志。例如,以下视图是不安全的:
CREATE VIEW phone_number AS
SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%';
这个视图可能看起来很安全,因为规则系统会将任何从phone_number的SELECT重写为从phone_data的SELECT,并添加只有在phone不以412开头的条目才是需要的限定条件。但是,如果用户可以创建自己的函数,那么很容易说服规划器在NOT LIKE表达式之前执行用户定义的函数。例如:
CREATE FUNCTION tricky(text, text) RETURNS bool AS $$
BEGIN
RAISE NOTICE '% => %', $1, $2;
RETURN true;
END;
$$ LANGUAGE pluxsql COST 0.0000000000000000000001;
SELECT * FROM phone_number WHERE tricky(person, phone);
phone_data表中的每个人和电话号码都将被打印为NOTICE,因为规划器将选择在执行更昂贵的NOTLIKE之前执行廉价的tricky函数。即使防止用户定义新函数,内置函数也可以用于类似的攻击。(例如,大多数转换函数在它们生成的错误消息中包含它们的输入值。)
更新规则也有类似的考虑。在上一节的示例中,示例数据库中表的所有者可以将shoelace视图上的SELECT、INSERT、UPDATE和DELETE权限授予其他人,但只能在shoelace_log上授予SELECT权限。写入日志条目的规则操作仍将成功执行,那个用户可以看到日志条目。但是他们不能创建虚假条目,也不能操纵或删除现有条目。在这种情况下,没有可能通过说服规划器改变操作顺序来破坏规则,因为唯一引用shoelace_log的规则是一个未限定的规则。INSERT。在更复杂的情况下可能不是这样的。
当需要视图提供行级安全性时,应将security_barrier属性应用于视图。这可以防止恶意选择的函数和运算符在视图完成工作之前从行中传递值。例如,如果上面显示的视图是这样创建的,则是安全的:
CREATE VIEW phone_number WITH (security_barrier) AS
SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%';
使用security_barrier创建的视图可能比没有此选项创建的视图性能差得多。一般来说,无法避免这种情况:如果可能危及安全,则必须拒绝最快的可能计划。因此,默认情况下不启用此选项。
查询规划器在处理没有副作用的函数时具有更大的灵活性。这些函数称为LEAKPROOF,包括许多简单的常用运算符,例如许多等式运算符。查询规划器可以安全地允许这些函数在查询执行过程的任何时刻进行评估,因为在用户看不见的行上调用它们不会泄漏有关未看到的行的任何信息。此外,不需要将不接受参数或未从安全屏障视图传递任何参数的函数标记为LEAKPROOF即可向下推送,因为它们从未从视图接收数据。相反,可能根据接收到的参数值抛出错误的函数(例如在溢出或除以零的情况下抛出错误的函数)不是泄漏证明,并且如果在安全视图的行过滤器之前应用,则可能提供有关未看到的行的重要信息。
重要的是要理解,即使使用security_barrier选项创建的视图也仅旨在在有限的意义上保护不可见元组的内容不会传递给可能不安全的函数。用户可能有其他方法推断未看到的数据;例如,他们可以使用EXPLAIN查看查询计划,或测量针对视图的查询的运行时间。恶意攻击者可能能够推断有关未看到的数据量的某些信息,甚至获得有关数据分布或最常见值的一些信息(因为这些事情可能会影响计划的运行时间;或者,由于它们也反映在优化器统计信息中,计划的选择)。如果这些类型的“隐蔽通道”攻击是值得关注的,则可能不明智地授予任何对数据的访问权限。
4.6.规则和命令状态
UXDB 服务器会为每个接收到的命令返回一个命令状态字符串,例如INSERT 149592 1。当没有规则参与时,这很简单,但是当查询被规则重写时会发生什么呢?
规则对命令状态的影响如下:
-
如果查询没有无条件的
INSTEAD规则,则将执行最初给定的查询,并像往常一样返回其命令状态。(但请注意,如果有任何条件的INSTEAD规则,则它们的限制的否定将被添加到原始查询中。这可能会减少它处理的行数,如果是这样,报告的状态将受到影响。) -
如果查询有任何无条件的
INSTEAD规则,则原始查询将根本不会被执行。在这种情况下,服务器将返回由INSTEAD规则(条件或无条件)插入的最后一个与原始查询相同的命令类型(INSERT、UPDATE或DELETE)的查询的命令状态。如果没有任何规则添加符合这些要求的查询,则返回的命令状态显示原始查询类型以及行计数和 OID字段的零值。
程序员可以通过将其命名为当前活动规则中字母表顺序最后一个规则来确保任何所需的 INSTEAD规则是设置第二种情况下的命令状态的规则,以便它最后应用。
4.7.规则与触发器
许多可以使用触发器完成的事情也可以使用UXDB规则系统实现。规则无法实现的一些约束条件,特别是外键。可以放置一个有资格的规则,将命令重写为NOTHING,如果一列的值未出现在另一个表中。但是,数据会被静默丢弃,这不是一个好主意。如果需要检查有效值,并在出现无效值的情况下生成错误消息,则必须使用触发器。
在本章中,我们专注于使用规则更新视图。本章中的所有更新规则示例也可以使用视图上的INSTEAD OF触发器实现。编写这样的触发器通常比编写规则更容易,特别是如果需要复杂的逻辑来执行更新。
对于两者都可以实现的事情,哪个更好取决于数据库的使用情况。触发器对于每个受影响的行触发一次。规则修改查询或生成附加查询。因此,如果在一个语句中影响了许多行,则发出一条额外命令的规则可能比为每个单独的行调用的触发器更快,并且必须多次重新确定要执行的操作。但是,触发器方法在概念上比规则方法简单得多,并且更容易让新手正确使用。
这里我们展示一个例子,说明规则与触发器的选择在某些情况下的作用。有两个表:
CREATE TABLE computer (
hostname text, -- indexed
manufacturer text -- indexed
);
CREATE TABLE software (
software text, -- indexed
hostname text -- indexed
);
两个表都有成千上万行,hostname上的索引是唯一的。规则或触发器应实现一个约束条件,即删除引用已删除计算机的software行。触发器将使用此命令:
DELETE FROM software WHERE hostname = $1;
由于触发器针对从computer表中删除的每个单独行调用,因此它的性能可能会受到影响。相反,规则可以使用以下命令:
CREATE RULE computer_del AS ON DELETE TO computer
DO DELETE FROM software WHERE hostname = OLD.hostname;
现在我们来看看不同类型的删除:
DELETE FROM computer WHERE hostname = 'mypc.local.net';
表computer通过索引扫描(快速),触发器发出的命令也将使用索引扫描(同样快速)。规则中的额外命令将是:
DELETE FROM software WHERE computer.hostname = 'mypc.local.net'
AND software.hostname = computer.hostname;
由于已经设置了适当的索引,因此规划器将创建以下计划:
Nestloop
-> Index Scan using comp_hostidx on computer
-> Index Scan using soft_hostidx on software
因此,在触发器和规则实现之间的速度差异不会太大。
接下来,我们想要删除所有以old开头的hostname的2000台计算机。有两个可能的命令可以执行。一个是:
DELETE FROM computer WHERE hostname >= 'old'
AND hostname < 'ole'
规则添加的命令将是:
DELETE FROM software WHERE computer.hostname >= 'old' AND computer.hostname < 'ole'
AND software.hostname = computer.hostname;
计划如下:
Hash Join
-> Seq Scan on software
-> Hash
-> Index Scan using comp_hostidx on computer
另一个可能的命令是:
DELETE FROM computer WHERE hostname ~ '^old';
这将导致规则添加的命令执行以下执行计划:
Nestloop
-> Index Scan using comp_hostidx on computer
-> Index Scan using soft_hostidx on software
这表明,规划器没有意识到computer中hostname的限定条件也可以用于在software上进行索引扫描,当多个限定表达式与AND组合时,这就是命令的正则表达式版本所做的。触发器将为每个要删除的2000个旧计算机之一调用一次,并导致对computer进行一次索引扫描和2000次对software进行索引扫描。规则实现将使用两个使用索引的命令来完成。这取决于表software的总体大小,规则是否仍然比顺序扫描情况下更快。即使所有索引块很快就会在缓存中,来自SPI管理器的2000个命令执行也需要一些时间。
我们要查看的最后一个命令是:
DELETE FROM computer WHERE manufacturer = 'bim';
同样,这可能会导致从computer中删除许多行。因此,触发器将再次通过执行器运行许多命令。规则生成的命令将是:
DELETE FROM software WHERE computer.manufacturer = 'bim'
AND software.hostname = computer.hostname;
该命令的计划将再次是两个索引扫描的嵌套循环,只是在computer上使用了不同的索引:
Nestloop
-> Index Scan using comp_manufidx on computer
-> Index Scan using soft_hostidx on software
在任何这些情况下,规则系统中的额外命令将与命令中受影响的行数或多或少独立。总之,只有当规则的操作导致大量且质量差的连接时,规则才会比触发器慢得多,这是规划器失败的情况。
5.服务器编程接口
服务器编程接口(SPI)允许用户定义的C函数在其函数或过程中运行SQL命令。SPI是一组接口函数,用于简化对解析器、计划器和执行器的访问。SPI还执行一些内存管理。
注意
可用的过程语言提供了各种方法来从函数中执行SQL命令。其中大多数设施都基于SPI,因此这份文档也可能对使用这些语言的用户有用。
请注意,如果通过SPI调用的命令失败,则不会将控制返回到您的C函数。相反,将回滚执行您的C函数的事务或子事务。(这可能看起来令人惊讶,因为SPI函数大多具有记录的错误返回约定。但是,这些约定仅适用于SPI函数本身检测到的错误。)可以通过建立自己的子事务来恢复在错误后的控制,该子事务包围可能失败的SPI调用。
SPI函数在成功时返回非负结果(通过返回的整数值或全局变量SPI_result,如下所述)。出错时,将返回负结果或NULL。
使用SPI的源代码文件必须包括头文件executor/spi.h。
5.1.接口函数
5.1.1.SPI_connect
SPI_connect,SPI_connect_ext - 将 C 函数连接到 SPI 管理器
5.1.1.1.概要
int SPI_connect(void)
int SPI_connect_ext(int options)
5.1.1.2.描述
SPI_connect 打开从 C 函数调用到 SPI 管理器的连接。如果要通过 SPI 执行命令,则必须调用此函数。一些实用程序 SPI函数可以从未连接的 C 函数调用。
SPI_connect_ext 执行相同的操作,但具有允许传递选项标志的参数。目前,以下选项值可用:
SPI_OPT_NONATOMIC
将 SPI 连接设置为非原子性,这意味着允许事务控制调用(SPI_commit,SPI_rollback)。否则,调用这些函数将导致立即出错。
SPI_connect() 等同于 SPI_connect_ext(0)。
5.1.1.3.返回值
SPI_OK_CONNECT
成功
SPI_ERROR_CONNECT
出错
5.1.2.SPI_finish
SPI_finish — 断开 C 函数与 SPI 管理器的连接
5.1.2.1.概要
int SPI_finish(void)
5.1.2.2.描述
SPI_finish 关闭与 SPI 管理器的现有连接。在当前调用 C 函数期间完成所需的 SPI 操作后,必须调用此函数。但是,如果通过elog(ERROR) 中止事务,则无需担心此问题。在这种情况下,SPI 将自动清理自身。
5.1.2.3.返回值
SPI_OK_FINISH
如果成功断开连接
SPI_ERROR_UNCONNECTED
如果从未连接的 C 函数中调用
5.1.3.SPI_execute
SPI_execute — 执行命令
5.1.3.1.概要
int SPI_execute(const char * command, bool read_only, long count)
5.1.3.2.描述
SPI_execute 执行指定的 SQL 命令 对于 count 行。如果 read_only 为true,则命令必须是只读的,并且执行开销 会稍微减少。
此函数只能从已连接的 C 函数中调用。
如果count为零,则执行命令 适用于所有行。如果 count 大于零,则最多检索不超过 count 行;当达到计数时,执行停止,就像 将 LIMIT 子句添加到查询中一样。例如,
SPI_execute("SELECT * FROM foo", true, 5);
将从表中最多检索 5 行。请注意,这样的限制 仅在命令实际返回行时才有效。例如,
SPI_execute("INSERT INTO foo SELECT * FROM bar", false, 5);
插入来自 bar 的所有行,忽略 count 参数。但是,使用
SPI_execute("INSERT INTO foo SELECT * FROM bar RETURNING *", false, 5);
最多插入 5 行,因为在检索到第五个 RETURNING 结果行后,执行将停止。
您可以在一个字符串中传递多个命令; SPI_execute 返回 执行的最后一个命令的结果。 count 限制分别适用于每个命令(即使只有最后一个 结果实际上将被返回)。该限制不适用于任何
由规则生成的隐藏命令。
当 read_only为 false 时,SPI_execute 增加命令 计数器并在执行每个命令之前计算新的快照。如果当前事务隔离级别为SERIALIZABLE或REPEATABLE READ,则快照实际上不会更改,但在READ COMMITTED模式下,快照更新允许每个命令查看其他会话中新提交的事务的结果。当命令修改数据库时,这对于一致的行为至关重要。
当read_only为true时,SPI_execute不会更新快照或命令计数器,并且只允许在命令字符串中出现普通的SELECT命令。命令使用先前为周围查询建立的快照执行。由于消除了每个命令的开销,这种执行模式有点更快。它还允许构建真正的稳定函数:由于连续的执行将使用相同的快照,因此结果不会发生变化。
通常不明智的是,在使用SPI的单个函数中混合使用只读和读写命令,这可能导致非常混乱的行为,因为只读查询不会看到读写查询执行的任何数据库更新的结果。在全局变量SPI_processed中返回执行(最后)命令的实际行数。如果函数的返回值为SPI_OK_SELECT、SPI_OK_INSERT_RETURNING、SPI_OK_DELETE_RETURNING或SPI_OK_UPDATE_RETURNING,则可以使用全局指针SPITupleTable *SPI_tuptable访问结果行。一些实用命令(如EXPLAIN)也返回行集,SPI_tuptable也将在这些情况下包含结果。一些实用命令(COPY、CREATE TABLE AS)不返回行集,因此SPI_tuptable为NULL,但它们仍然返回在SPI_processed中处理的行数。结构SPITupleTable定义如下:
typedef struct SPITupleTable
{
/* Public members */
TupleDesc tupdesc; /* tuple descriptor */
HeapTuple *vals; /* array of tuples */
uint64 numvals; /* number of valid tuples */
/* Private members, not intended for external callers */
uint64 alloced; /* allocated length of vals array */
MemoryContext tuptabcxt; /* memory context of result table */
slist_node next; /* link for internal bookkeeping */
SubTransactionId subid; /* subxact in which tuptable was created */
} SPITupleTable;
字段tupdesc、vals和numvals可以被SPI调用者使用;其余字段是内部的。vals是一个指向行的指针数组。行数由numvals给出(由于某种历史原因,这个计数也返回在SPI_processed中)。tupdesc是一个行描述符,您可以将其传递给处理行的SPI函数。SPI_finish释放当前C函数期间分配的所有SPITupleTable。如果您已经完成了特定的结果表,则可以通过调用SPI_freetuptable来更早地释放它。
5.1.3.3.参数
const char * command
包含要执行的命令的字符串
bool read_only
true 表示只读执行
long count
要返回的最大行数,或者 0 表示没有限制
5.1.3.4.返回值
如果命令执行成功,则返回以下(非负)值之一:
SPI_OK_SELECT
如果执行了 SELECT(但不是 SELECT INTO)
SPI_OK_SELINTO
如果执行了 SELECT INTO
SPI_OK_INSERT
如果执行了 INSERT
SPI_OK_DELETE
如果执行了 DELETE
SPI_OK_UPDATE
如果执行了 UPDATE
SPI_OK_INSERT_RETURNING
如果执行了 INSERT RETURNING
SPI_OK_DELETE_RETURNING
如果执行了 DELETE RETURNING
SPI_OK_UPDATE_RETURNING
如果执行了 UPDATE RETURNING
SPI_OK_UTILITY
如果执行了实用程序命令(例如 CREATE TABLE)
SPI_OK_REWRITTEN
如果命令被规则重写为另一种命令(例如,UPDATE 变成了 INSERT)
出现错误时,将返回以下负值之一:
SPI_ERROR_ARGUMENT
如果 command 是 NULL 或 count 小于 0
SPI_ERROR_COPY
如果尝试执行 COPY TO stdout 或 COPY FROM stdin
SPI_ERROR_TRANSACTION
如果尝试事务操作命令 (BEGIN、 COMMIT、 ROLLBACK、 SAVEPOINT、 PREPARE TRANSACTION、 COMMIT PREPARED、 ROLLBACK PREPARED,或任何其变体)
SPI_ERROR_OPUNKNOWN
如果命令类型未知(不应该发生)
SPI_ERROR_UNCONNECTED
如果从未连接的 C 函数调用
5.1.3.5.注意事项
所有 SPI 查询执行函数都会设置 SPI_processed 和SPI_tuptable(仅指针,而不是结构的内容)。如果您需要在后续调用中跨函数访问SPI_execute 或另一个查询执行函数的结果表,则将这两个全局变量保存到本地 C 函数变量中。
5.1.4.SPI_exec
SPI_exec - 执行读写命令
5.1.4.1.概要
int SPI_exec(const char * command, long count)
5.1.4.2.描述
SPI_exec 与 SPI_execute 相同,只是后者的 read_only 参数始终为 false。
5.1.4.3.参数
const char * command
包含要执行的命令的字符串
long count
返回的最大行数,或者 0 表示没有限制
5.1.4.4.返回值
参见 SPI_execute。
5.1.5.SPI_execute_extended
SPI_execute_extended — 执行带有外部参数引用的命令
5.1.5.1.概要
int SPI_execute_extended(const char *command,const SPIExecuteOptions * options)
5.1.5.2.描述
SPI_execute_extended 执行可能包含对外部提供的参数的引用的命令。命令文本将参数表示为 $n,如果提供了 options->params 对象,则该对象提供每个这样的符号的值和类型信息。还可以在 options 结构中指定各种执行选项。
options->params 对象通常应将每个参数标记为 PARAM_FLAG_CONST 标志,因为查询始终使用一次性计划。
如果 options->dest 不为 NULL,则结果元组将在执行器生成时传递给该对象,而不是在 SPI_tuptable中累积。对于可能生成许多元组的查询,使用调用者提供的 DestReceiver对象特别有帮助,因为数据可以即时处理,而不是在内存中累积。
5.1.5.3.参数
const char * command
命令字符串
const SPIExecuteOptions * options
包含可选参数的结构体
调用者应始终将整个 options 结构清零,然后填写他们想要设置的任何字段。这确保了代码的向前兼容性,因为在将来向结构体添加的任何字段都将被定义为向后兼容地行为,如果它们为零。目前可用 options 字段如下:
ParamListInfo params
包含查询参数类型和值的数据结构;如果没有,则为NULL
bool read_only
true表示只读执行
bool allow_nonatomic
true允许非原子执行CALL和DO语句
bool must_return_tuples
如果true,则如果查询不是返回元组的类型,则引发错误(这不禁止它恰好返回零个元组的情况)
uint64 tcount
要返回的最大行数,或0表示没有限制
DestReceiver *dest
DestReceiver对象将接收查询发出的任何元组;如果为NULL,则结果元组将累积到SPI_tuptable结构中,就像在SPI_execute中一样
ResourceOwner owner
此字段出于一致性而存在SPI_execute_plan_extended,但它被忽略,因为SPI_execute_extended使用的计划从未保存。
5.1.5.4.返回值
返回值与SPI_execute相同。
当 options->dest 为NULL时,SPI_processed和SPI_tuptable设置为SPI_execute中的设置。
当 options->dest 不为NULL时,SPI_processed设置为零,SPI_tuptable设置为NULL。如果需要元组计数,则调用者的DestReceiver对象必须计算它。
5.1.6.SPI_execute_with_args
SPI_execute_with_args — 执行带有外部参数的命令
5.1.6.1.概要
int SPI_execute_with_args(const char *command,
int nargs, Oid *argtypes,
Datum *values, const char *nulls,
bool read_only, long count)
5.1.6.2.描述
SPI_execute_with_args 执行一个可能包含对外部提供的参数的引用的命令。命令文本将参数引用为$n,并且调用指定了每个这样的符号的数据类型和值。read_only 和 count 的解释与 SPI_execute 中相同。
与 SPI_execute 相比,此例程的主要优点在于可以插入数据值到命令中,而不需要繁琐的引用/转义,因此 SQL注入攻击的风险大大降低。
可以通过 SPI_prepare 后跟 SPI_execute_plan来实现类似的结果;但是,使用此函数时,查询计划总是针对提供的特定参数值进行定制。对于一次性查询执行,应优先考虑使用此函数。如果要使用许多不同的参数执行相同的命令,则任一方法可能更快,具体取决于重新计划的成本与定制计划的效益。
5.1.6.3.参数
const char * command
命令字符串
int nargs
输入参数的数量($1、$2 等)
Oid * argtypes
一个长度为 nargs 的数组,包含参数的数据类型的OID
Datum * values
一个长度为 nargs 的数组,包含实际的参数值
const char * nulls
一个长度为 nargs 的数组,描述哪些参数为null
如果nulls是NULL,则SPI_execute_with_args假定没有参数为null。否则,nulls数组的每个条目应为' ',如果相应的参数值为非null,则为'n'(在后一种情况下,相应的values条目中的实际值并不重要)。请注意,nulls不是文本字符串,只是一个数组:它不需要'\0'终止符。
bool read_only
true表示只读执行
long count
返回的最大行数,或0表示没有限制
5.1.6.4.返回值
返回值与SPI_execute相同。
如果成功,SPI_processed和SPI_tuptable将设置为与SPI_execute相同。
5.1.7.SPI_prepare
SPI_prepare - 准备语句,但尚未执行
5.1.7.1.概要
SPIPlanPtr SPI_prepare(const char * command, int nargs, Oid * argtypes)
5.1.7.2.描述
SPI_prepare 创建并返回指定命令的准备语句,但不执行该命令。准备语句可以使用SPI_execute_plan重复执行。
当需要重复执行相同或类似的命令时,通常最好只进行一次解析分析,并且可能还有利于重用命令的执行计划。SPI_prepare将命令字符串转换为封装解析分析结果的准备语句。如果发现为每个执行生成自定义计划不是有益的,则准备语句还提供了一个缓存执行计划的位置。
可以通过在常规命令中写入参数($1、$2等)来概括准备命令。然后在调用SPI_execute_plan时指定参数的实际值。这允许在不使用参数的情况下使用准备命令的范围更广。
SPI_prepare返回的语句只能在当前C函数的调用中使用,因为SPI_finish释放为此类语句分配的内存。但是可以使用SPI_keepplan或SPI_saveplan函数保存语句更长时间。
5.1.7.3.参数
const char * command
命令字符串
int nargs
输入参数的数量($1,$2等)。
Oid *argtypes
指向包含参数数据类型的OID数组的指针。
5.1.7.4.返回值
SPI_prepare返回一个非空指针,指向表示预处理语句的不透明结构SPIPlan。如果出现错误,则返回NULL,并将SPI_result设置为与SPI_execute使用的相同的错误代码之一,除非 command 为NULL,或者 nargs 小于0,或者 nargs 大于0且 argtypes 为NULL。
5.1.7.5.注意事项
如果没有定义参数,则在第一次使用SPI_execute_plan时将创建一个通用计划,并用于所有后续执行。如果有参数,则前几次使用SPI_execute_plan将生成特定于提供的参数值的自定义计划。在使用相同的预处理语句足够多次后,SPI_execute_plan将构建一个通用计划,如果通用计划的成本不比自定义计划高太多,则开始使用通用计划而不是每次重新计划。如果此默认行为不适用,则可以通过将CURSOR_OPT_GENERIC_PLAN或CURSOR_OPT_CUSTOM_PLAN标志传递给SPI_prepare_cursor来更改它,以强制使用通用或自定义计划。
尽管预处理语句的主要目的是避免重复解析分析和计划语句,但是每当用于语句的数据库对象在上一次使用预处理语句之后发生定义(DDL)更改时,UXDB都会强制重新分析和重新计划语句。此外,如果search_path的值从一次使用到下一次使用发生更改,则将使用新的search_path重新解析语句。
此函数应仅从已连接的C函数中调用。
SPIPlanPtr在spi.h中声明为指向不透明结构类型的指针。直接访问其内容是不明智的,因为这会使您的代码在将来的UXDB版本中更容易出现故障。
名称SPIPlanPtr在某种程度上是历史性的,因为数据结构不再必然包含执行计划。
5.1.8.SPI_prepare_cursor
SPI_prepare_cursor — 准备语句,但不立即执行
5.1.8.1.概要
SPIPlanPtr SPI_prepare_cursor(const char * command, int nargs,
Oid * argtypes, int cursorOptions)
5.1.8.2.描述
SPI_prepare_cursor 与 SPI_prepare 相同,但它还允许指定规划器的“游标选项”参数。这是一个位掩码,其值在nodes/parsenodes.h 中显示为 DeclareCursorStmt 的 options 字段。SPI_prepare 总是将游标选项设置为零。
现在已经不推荐使用此函数,而应使用 SPI_prepare_extended。
5.1.8.3.参数
const char * command
命令字符串
int nargs
输入参数的数量($1、$2 等)
Oid * argtypes
指向包含参数数据类型的 OID 数组的指针
int cursorOptions
游标选项的整数位掩码;零表示默认行为
5.1.8.4.返回值
SPI_prepare_cursor 具有与 SPI_prepare 相同的返回约定。
5.1.8.5.注意事项
在 cursorOptions 中设置有用的位包括:CURSOR_OPT_SCROLL、CURSOR_OPT_NO_SCROLL、 CURSOR_OPT_FAST_PLAN、CURSOR_OPT_GENERIC_PLAN和 CURSOR_OPT_CUSTOM_PLAN。特别注意,CURSOR_OPT_HOLD被忽略。
5.1.9.SPI_prepare_extended
SPI_prepare_extended — 准备语句,但尚未执行
5.1.9.1.概要
SPIPlanPtr SPI_prepare_extended(const char * command,
const SPIPrepareOptions * options)
5.1.9.2.描述
SPI_prepare_extended 创建并返回指定命令的准备语句,但不执行该命令。此函数等同于SPI_prepare,但调用者可以指定选项来控制外部参数引用的解析,以及查询解析和计划的其他方面。
5.1.9.3.参数
const char * command
命令字符串
const SPIPrepareOptions * options
包含可选参数的结构体
调用者应始终将整个 options 结构体清零,然后填充他们想要设置的任何字段。这确保了代码的向前兼容性,因为将来添加到结构体中的任何字段,如果它们为零,则被定义为向后兼容。当前可用的 options 字段为:
ParserSetupHook parserSetup
解析器钩子设置函数
void * parserSetupArg
传递给 parserSetup 的参数
RawParseMode parseMode
原始解析模式;RAW_PARSE_DEFAULT(零)产生默认行为
int cursorOptions
游标选项的整数位掩码;零会产生默认行为
5.1.9.4.返回值
SPI_prepare_extended 的返回值约定与 SPI_prepare 相同。
5.1.10.SPI_prepare_params
SPI_prepare_params — 准备语句,但不立即执行
5.1.10.1.概要
SPIPlanPtr SPI_prepare_params(const char * command,
ParserSetupHook parserSetup,
void * parserSetupArg,
int cursorOptions)
5.1.10.2.描述
SPI_prepare_params 创建并返回指定命令的准备语句,但不执行该命令。此函数等效于SPI_prepare_cursor,但调用者可以指定解析器钩子函数以控制外部参数引用的解析。
此函数现在已被弃用,推荐使用 SPI_prepare_extended。
5.1.10.3.参数
const char * command
命令字符串
ParserSetupHook parserSetup
解析器钩子设置函数
void * parserSetupArg
传递给 parserSetup 的参数
int cursorOptions
游标选项的整数位掩码;零表示默认行为
5.1.10.4.返回值
SPI_prepare_params 具有与 SPI_prepare 相同的返回约定。
5.1.11.SPI_getargcount
SPI_getargcount - 返回由SPI_prepare准备的语句所需的参数数量
5.1.11.1.概要
int SPI_getargcount(SPIPlanPtr plan)
5.1.11.2.描述
SPI_getargcount返回由SPI_prepare准备的语句执行所需的参数数量。
5.1.11.3.参数
SPIPlanPtr plan
准备的语句(由SPI_prepare返回)
5.1.11.4.返回值
plan 所需的预期参数计数。如果 plan 为NULL或无效,SPI_result将设置为SPI_ERROR_ARGUMENT并返回-1。
5.1.12.SPI_getargtypeid
SPI_getargtypeid — 返回由SPI_prepare准备的语句的参数的数据类型OID
5.1.12.1.概要
Oid SPI_getargtypeid(SPIPlanPtr plan, int argIndex)
5.1.12.2.描述
SPI_getargtypeid返回表示由SPI_prepare准备的语句的第argIndex个参数的OID。第一个参数的索引为零。
5.1.12.3.参数
SPIPlanPtr plan
准备好的语句(由SPI_prepare返回)
int argIndex
参数的零基索引
5.1.12.4.返回值
给定索引处的参数的类型OID。如果plan为NULL或无效,或argIndex小于0或不小于为plan声明的参数数,SPI_result将设置为SPI_ERROR_ARGUMENT 并返回InvalidOid。
5.1.13.SPI_is_cursor_plan
SPI_is_cursor_plan — 如果由 SPI_prepare 准备的语句可以与 SPI_cursor_open一起使用,则返回 true。
5.1.13.1.概要
bool SPI_is_cursor_plan(SPIPlanPtr plan)
5.1.13.2.描述
SPI_is_cursor_plan 返回 true,如果由 SPI_prepare 准备的语句可以作为参数传递给SPI_cursor_open,否则返回 false。判断标准是 plan 表示一个单一的命令,并且该命令将元组返回给调用者;例如,SELECT是允许的,除非它包含一个 INTO 子句,而 UPDATE 只有在包含 RETURNING 子句时才允许。
5.1.13.3.参数
SPIPlanPtr plan
准备好的语句(由 SPI_prepare 返回)
5.1.13.4.返回值
true 或 false,表示 plan 是否能够生成游标,SPI_result 设置为零。如果无法确定答案(例如,plan为NULL或无效,或在未连接到SPI时调用),则将SPI_result设置为适当的错误代码,并返回false。
5.1.14.SPI_execute_plan
SPI_execute_plan — 执行由 SPI_prepare准备的语句
5.1.14.1.概要
int SPI_execute_plan(SPIPlanPtr plan, Datum * values, const char * nulls,
bool read_only, long count)
5.1.14.2.描述
SPI_execute_plan 执行由 SPI_prepare 或其兄弟函数准备的语句。 read_only 和 count 的解释与 SPI_execute 相同。
5.1.14.3.参数
SPIPlanPtr plan
准备好的语句(由 SPI_prepare 返回)
Datum * values
实际参数值的数组。必须与语句的参数数量相同。
const char * nulls
描述哪些参数为 null 的数组。必须与语句的参数数量相同。
如果 nulls 为 NULL,则 SPI_execute_plan 假定没有参数为 null。否则,nulls数组的每个条目应为 ' ',如果相应的参数值为非 null,则为 'n'。(在后一种情况下,相应的values条目中的实际值无关紧要。)请注意,nulls不是文本字符串,只是一个数组:它不需要 '\0' 终止符。
bool read_only
true 表示只读执行
long count
返回的最大行数,或者 0 表示没有限制
5.1.14.4.返回值
返回值与 SPI_execute 相同,还可能有以下额外的错误(负数)结果:
SPI_ERROR_ARGUMENT
如果 plan 是 NULL 或无效,或者 count 小于 0
SPI_ERROR_PARAM
如果 values 是 NULL,并且 plan 是使用某些参数准备的
如果成功,则 SPI_processed 和 SPI_tuptable 的设置与 SPI_execute 相同。
5.1.15.SPI_execute_plan_extended
SPI_execute_plan_extended — 执行由 SPI_prepare 准备的语句
5.1.15.1.概要
int SPI_execute_plan_extended(SPIPlanPtr plan,
const SPIExecuteOptions * options)
5.1.15.2.描述
SPI_execute_plan_extended 执行由 SPI_prepare 或其兄弟函数准备的语句。该函数等同于SPI_execute_plan,但是参数值的信息以不同的方式呈现,并且可以传递其他执行控制选项。
查询参数值由 ParamListInfo 结构表示,这对于传递已经以该格式可用的值非常方便。也可以使用动态参数集,通过在ParamListInfo 中指定的钩子函数。
此外,不必总是将结果元组累积到 SPI_tuptable 结构中,可以将元组传递给调用方提供的 DestReceiver对象,因为它们由执行器生成。这对于可能生成许多元组的查询特别有帮助,因为数据可以实时处理,而不是在内存中累积。
5.1.15.3.参数
SPIPlanPtr plan
准备的语句(由 SPI_prepare 返回)
const SPIExecuteOptions * options
包含可选参数的结构体
调用者应该始终将整个 options 结构体清零,然后填写他们想要设置的任何字段。这确保了代码的向前兼容性,因为在结构体中添加的任何字段都可以在旧代码中忽略。如果它们为零,未来将被定义为向后兼容。当前可用的 options 字段如下:
ParamListInfo params
包含查询参数类型和值的数据结构;如果没有,则为NULL
bool read_only
执行只读操作的true
bool allow_nonatomic
true允许执行CALL和DO语句的非原子操作
bool must_return_tuples
如果true,则如果查询不是返回元组的类型,将引发错误(这不禁止它恰好返回零个元组的情况)
uint64 tcount
要返回的最大行数,或0表示没有限制
DestReceiver * dest
DestReceiver对象将接收查询发出的任何元组;如果为NULL,则结果元组将累积到SPI_tuptable结构中,就像在SPI_execute_plan中一样
ResourceOwner owner
执行计划时将持有引用计数的资源所有者。如果为NULL,则使用CurrentResourceOwner。对于未保存的计划,它被忽略,因为SPI不会在这些计划上获取引用计数。
5.1.15.4.返回值
返回值与SPI_execute_plan相同。
当 options->dest 为NULL时,SPI_processed和SPI_tuptable的设置与SPI_execute_plan相同。当 options->dest 不为NULL时,SPI_processed设置为零,SPI_tuptable设置为NULL。如果需要元组计数,则调用者的DestReceiver对象必须计算它。
5.1.16.SPI_execute_plan_with_paramlist
SPI_execute_plan_with_paramlist — 执行由 SPI_prepare 准备的语句
5.1.16.1.概述
int SPI_execute_plan_with_paramlist(SPIPlanPtr plan,
ParamListInfo params,
bool read_only,
long count)
5.1.16.2.描述
SPI_execute_plan_with_paramlist 执行由 SPI_prepare 准备的语句。该函数与SPI_execute_plan 相同,只是参数值的信息传递方式不同。ParamListInfo表示法可以方便地传递已经以该格式可用的值。它还支持通过ParamListInfo 中指定的钩子函数使用动态参数集。
该函数现在已被弃用,推荐使用 SPI_execute_plan_extended。
5.1.16.3.参数
SPIPlanPtr plan
准备的语句(由 SPI_prepare 返回)
ParamListInfo params
包含参数类型和值的数据结构;如果没有,则为 NULL
bool read_only
true 表示只读执行
long count
返回的最大行数,或者 0 表示没有限制
5.1.16.4.返回值
返回值与 SPI_execute_plan 相同。
如果成功,SPI_processed和SPI_tuptable将设置为SPI_execute_plan中的值。
5.1.17.SPI_execp
SPI_execp —在读写模式下执行语句
5.1.17.1.概要
int SPI_execp(SPIPlanPtr plan, Datum * values, const char * nulls, long count)
5.1.17.2.描述
SPI_execp 与 SPI_execute_plan 相同,只是前者的 read_only 参数始终为 false。
5.1.17.3.参数
SPIPlanPtr plan
准备好的语句(由 SPI_prepare 返回)
Datum * values
实际参数值的数组。必须与语句的参数数量相同。
const char * nulls
描述哪些参数为 null 的数组。必须与语句的参数数量相同。
如果 nulls 为 NULL,则 SPI_execp 假定没有参数为 null。否则,nulls 数组的每个条目应为 ' '(如果相应的参数值为非 null)或 'n'(如果相应的参数值为null)。(在后一种情况下,相应的 values 条目中的实际值无关紧要。)请注意,nulls不是文本字符串,只是一个数组:它不需要 '\0' 终止符。
long count
返回的最大行数,或 0 表示没有限制。
5.1.17.4.返回值
请参见SPI_execute_plan。
如果成功,SPI_processed和SPI_tuptable将被设置,就像SPI_execute一样。
5.1.18.SPI_cursor_open
SPI_cursor_open — 使用由SPI_prepare创建的语句设置游标
5.1.18.1.概要
Portal SPI_cursor_open(const char * name, SPIPlanPtr plan,
Datum * values, const char * nulls,
bool read_only)
5.1.18.2.描述
SPI_cursor_open设置一个游标(内部为portal),该游标将执行由SPI_prepare准备的语句。参数的含义与SPI_execute_plan的相应参数相同。
使用游标而不是直接执行语句有两个好处。首先,结果行可以一次检索几行,避免查询返回许多行时的内存溢出。其次,portal可以超出当前的C函数(事实上,它可以存活到当前事务的结束)。将portal名称返回给C函数的调用者提供了一种返回行集作为结果的方法。
传入的参数数据将被复制到游标的portal中,因此它可以在游标仍然存在时被释放。
5.1.18.2.参数
const char * name
portal的名称,或NULL以让系统选择一个名称
SPIPlanPtr plan
准备的语句(由SPI_prepare返回)
Datum * values
实际参数值的数组。必须与语句的参数数量相同。
const char * nulls
描述哪些参数为null的数组。必须与语句的参数数量相同。
如果nulls为NULL,则SPI_cursor_open假定没有参数为null。否则,nulls数组的每个条目应为' ',如果相应的参数值为非null,则应为'n'。(在后一种情况下,相应的values条目中的实际值并不重要。)请注意,nulls不是文本字符串,只是一个数组:它不需要'\0'终止符。
bool read_only
true表示只读执行
5.1.18.3.返回值
指向包含游标的portal的指针。请注意,没有错误返回约定;任何错误都将通过elog报告。
5.1.19.SPI_cursor_open_with_args
SPI_cursor_open_with_args — 使用查询和参数设置游标
5.1.19.1.概要
Portal SPI_cursor_open_with_args(const char *name,
const char *command,
int nargs, Oid *argtypes,
Datum *values, const char *nulls,
bool read_only, int cursorOptions)
5.1.19.2.描述
SPI_cursor_open_with_args 设置一个游标(内部是一个 portal)来执行指定的查询。大多数参数的含义与相应的SPI_prepare_cursor 和 SPI_cursor_open 参数相同。
对于一次性查询执行,应该优先使用此函数,而不是使用 SPI_prepare_cursor 后跟 SPI_cursor_open。如果要使用许多不同的参数执行相同的命令,则根据重新规划的成本与自定义计划的效益,任一方法都可能更快。
传入的参数数据将被复制到游标的 portal 中,因此它可以在游标仍然存在时被释放。
现在已经弃用此函数,而改用 SPI_cursor_parse_open,它使用更现代的 API 处理查询参数并提供等效功能。
5.1.19.3.参数
const char * name
portal 的名称,或 NULL 以让系统选择名称
const char * command
命令字符串
int nargs
输入参数的数量($1,$2等)
Oid * argtypes
长度为nargs的数组,包含参数的数据类型的OID
Datum * values
长度为 nargs 的数组,包含实际参数值
const char * nulls
长度为 nargs 的数组,描述哪些参数为null
如果nulls为NULL,则SPI_cursor_open_with_args假定没有参数为null。否则,nulls数组的每个条目应为' ',如果相应的参数值为非null,则为'n'(在后一种情况下,相应的values条目中的实际值无关紧要)。请注意,nulls不是文本字符串,只是一个数组:它不需要'\0'终止符。
bool read_only
true表示只读执行
int cursorOptions
游标选项的整数位掩码;零产生默认行为
5.1.19.4.返回值
指向包含游标的portal的指针。请注意,没有错误返回约定;任何错误都将通过elog报告。
5.1.20.SPI_cursor_open_with_paramlist
SPI_cursor_open_with_paramlist — 使用参数设置游标
5.1.20.1.概述
Portal SPI_cursor_open_with_paramlist(const char *name,
SPIPlanPtr plan,
ParamListInfo params,
bool read_only)
5.1.20.2.描述
SPI_cursor_open_with_paramlist设置一个游标(内部是一个portal),该游标将执行由SPI_prepare准备的语句。此函数与SPI_cursor_open相同,只是有关要传递给查询的参数值的信息以不同的方式呈现。ParamListInfo表示法可以方便地传递已经以该格式可用的值。它还支持通过在ParamListInfo中指定的钩子函数使用动态参数集。
传入的参数数据将被复制到游标的portal中,因此可以在游标仍然存在时释放它。
5.1.20.3.参数
const char * name
portal的名称,或NULL以让系统选择名称
SPIPlanPtr plan
准备的语句(由SPI_prepare返回)
ParamListInfo params
包含参数类型和值的数据结构;如果没有,则为NULL
bool read_only
true表示只读执行
5.1.20.4.返回值
指向包含光标的门户。请注意,没有错误返回约定;任何错误都将通过 elog 报告。
5.1.21.SPI_cursor_parse_open
SPI_cursor_parse_open — 使用查询字符串和参数设置游标
5.1.21.1.概要
Portal SPI_cursor_parse_open(const char *name,
const char *command,
const SPIParseOpenOptions * options)
5.1.21.2.描述
SPI_cursor_parse_open 设置一个游标(内部是一个 portal),用于执行指定的查询字符串。这类似于先使用SPI_prepare_cursor,然后使用 SPI_cursor_open_with_paramlist,但是查询字符串中的参数引用完全由提供的 ParamListInfo 对象处理。
对于一次性查询执行,应该优先使用此函数,而不是先使用 SPI_prepare_cursor,然后使用SPI_cursor_open_with_paramlist。如果要使用许多不同的参数执行相同的命令,则根据重新规划的成本与自定义计划的收益,任一方法都可能更快。
传入的参数数据将被复制到游标的 portal 中,因此可以在游标仍然存在时释放它。
5.1.21.3.参数
const char * name
portal 的名称,或者为 NULL 以让系统选择一个名称
const char * command
命令字符串
const SPIParseOpenOptions * options
包含可选参数的结构体
调用者应始终将整个options结构体清零,然后填写他们想要设置的任何字段。这确保了代码的向前兼容性,因为在将来添加到结构体中的任何字段都将被定义为在零时向后兼容。当前可用的options字段为:
ParamListInfo params
包含查询参数类型和值的数据结构;如果没有,则为NULL
int cursorOptions
游标选项的整数位掩码;零产生默认行为
bool read_only
true表示只读执行
5.1.21.4.返回值
指向包含游标的portal的指针。请注意,没有错误返回约定;任何错误都将通过elog报告。
5.1.22.SPI_cursor_find
SPI_cursor_find — 按名称查找现有游标
5.1.22.1.概要
Portal SPI_cursor_find(const char * name)
5.1.22.2.描述
SPI_cursor_find 按名称查找现有的 portal。这主要用于解析由其他函数返回的游标名称。
5.1.22.3.参数
const char * name
portal 的名称
5.1.22.4.返回值
指向指定名称的 portal 的指针,如果没有找到则为 NULL。
5.1.23.SPI_cursor_fetch
SPI_cursor_fetch - 从游标中获取一些行
5.1.23.1.概要
void SPI_cursor_fetch(Portal portal, bool forward, long count)
5.1.23.2.描述
SPI_cursor_fetch 从游标中获取一些行。这相当于 SQL 命令 FETCH 的子集(有关更多功能,请参见SPI_scroll_cursor_fetch)。
5.1.23.3.参数
Portal portal
包含游标的门户
bool forward
true 表示向前获取,false 表示向后获取
long count
要获取的最大行数
5.1.23.4.返回值
如果成功,则设置 SPI_processed 和 SPI_tuptable,就像在 SPI_execute 中一样。
5.1.23.5.注意事项
如果游标的计划未使用 CURSOR_OPT_SCROLL 选项创建,则向后获取可能会失败。
5.1.24.SPI_cursor_move
SPI_cursor_move — 移动游标
5.1.24.1.概要
void SPI_cursor_move(Portal portal, bool forward, long count)
5.1.24.2.描述
SPI_cursor_move 跳过游标中的一些行。这相当于 SQL 命令 MOVE 的一个子集(有关更多功能,请参见SPI_scroll_cursor_move)。
5.1.24.3.参数
Portal portal
包含游标的门户
bool forward
true 表示向前移动,false 表示向后移动
long count
要移动的最大行数
5.1.24.4.注意事项
如果游标的计划未使用 CURSOR_OPT_SCROLL 选项创建,则向后移动可能会失败。
5.1.25.SPI_scroll_cursor_fetch
SPI_scroll_cursor_fetch — 从游标中获取一些行
5.1.25.1.概要
void SPI_scroll_cursor_fetch(Portal portal, FetchDirection direction,
long count)
5.1.25.2.描述
SPI_scroll_cursor_fetch 从游标中获取一些行。这相当于 SQL 命令 FETCH。
5.1.25.3.参数
Portal portal
包含游标的门户
FetchDirection direction
FETCH_FORWARD、 FETCH_BACKWARD、 FETCH_ABSOLUTE 或FETCH_RELATIVE 中的一个
long count
对于 FETCH_FORWARD 或 FETCH_BACKWARD,表示要获取的行数;对于FETCH_ABSOLUTE,表示要获取的绝对行数;对于 FETCH_RELATIVE,表示要获取的相对行数
5.1.25.4.返回值
如果成功,则设置 SPI_processed 和 SPI_tuptable,就像在 SPI_execute 中一样。
5.1.25.5.注意事项
有关 direction 和 count 参数的解释,请参见FETCH命令。
如果游标的计划未使用 CURSOR_OPT_SCROLL 选项创建,则除了 FETCH_FORWARD 之外的方向值可能会失败。
5.1.26.SPI_scroll_cursor_move
SPI_scroll_cursor_move — 移动游标
5.1.26.1.概要
void SPI_scroll_cursor_move(Portal portal, FetchDirection direction,
long count)
5.1.26.2.描述
SPI_scroll_cursor_move 跳过游标中的一些行。这相当于 SQL 命令 MOVE。
5.1.26.3.参数
Portal portal
包含游标的 portal
FetchDirection direction
FETCH_FORWARD、FETCH_BACKWARD、FETCH_ABSOLUTE 或 FETCH_RELATIVE中的一个
long count
对于 FETCH_FORWARD 或 FETCH_BACKWARD,表示要移动的行数;对于FETCH_ABSOLUTE,表示要移动到的绝对行数;对于 FETCH_RELATIVE,表示要移动到的相对行数
5.1.26.4.返回值
如果成功,SPI_processed 将被设置,就像在 SPI_execute 中一样。由于此函数不返回任何行,因此SPI_tuptable 将被设置为 NULL。
5.1.26.5.注意事项
有关 direction 和 count 参数的解释,请参见FETCH命令。
如果游标的计划未创建,则除 FETCH_FORWARD 之外的方向值可能会失败。使用 CURSOR_OPT_SCROLL 选项。
5.1.27.SPI_cursor_close
SPI_cursor_close — 关闭游标
5.1.27.1.概要
void SPI_cursor_close(Portal portal)
5.1.27.2.描述
SPI_cursor_close 关闭之前创建的游标并释放其门户存储。
所有打开的游标都会在事务结束时自动关闭。只有在希望更早释放资源时才需要调用 SPI_cursor_close。
5.1.27.3.参数
Portal portal
包含游标的门户
5.1.28.SPI_keepplan
SPI_keepplan — 保存一个预处理语句
5.1.28.1.概要
int SPI_keepplan(SPIPlanPtr plan)
5.1.28.2.描述
SPI_keepplan 保存一个传递的语句(由 SPI_prepare 预处理)以便它不会被 SPI_finish或事务管理器释放。这使您能够在当前会话的后续调用中重用预处理语句。
5.1.28.3.参数
SPIPlanPtr plan
要保存的预处理语句
5.1.28.4.返回值
成功返回 0; 如果 plan 为 NULL 或无效,则返回 SPI_ERROR_ARGUMENT。
5.1.28.5.注意事项
通过指针调整将传入的语句重定位到永久存储中(无需复制数据)。如果您以后想要删除它,请使用 SPI_freeplan。
5.1.29.SPI_saveplan
SPI_saveplan — 保存一个预处理语句
5.1.29.1.概要
SPIPlanPtr SPI_saveplan(SPIPlanPtr plan)
5.1.29.2.描述
SPI_saveplan 将一个传递的语句(由 SPI_prepare 准备)复制到不会被 SPI_finish或事务管理器释放的内存中,并返回指向复制语句的指针。这使您能够在当前会话的后续调用中重用预处理语句。
5.1.29.3.参数
SPIPlanPtr plan
要保存的预处理语句
5.1.29.4.返回值
指向复制语句的指针;如果不成功,则为 NULL。出错时,SPI_result 被设置为:
SPI_ERROR_ARGUMENT
如果 plan 是 NULL 或无效
SPI_ERROR_UNCONNECTED
如果从未连接的 C 函数中调用
5.1.29.5.注意事项
最初传入的语句不会被释放,因此您可能希望对其执行 SPI_freeplan 以避免在 SPI_finish 之前泄漏内存。
在大多数情况下,SPI_keepplan 比此函数更受欢迎,因为它可以在不需要物理复制预处理语句的数据结构的情况下实现基本相同的结果。
5.1.30.SPI_register_relation
SPI_register_relation — 通过名称在 SPI 查询中使临时命名关系可用
5.1.30.1.概要
int SPI_register_relation(EphemeralNamedRelation enr)
5.1.30.2.描述
SPI_register_relation 使一个带有关联信息的临时命名关系在当前 SPI 连接中计划和执行的查询中可用。
5.1.30.3.参数
EphemeralNamedRelation enr
临时命名关系注册表条目
5.1.30.4.返回值
如果命令执行成功,则返回以下(非负)值:
SPI_OK_REL_REGISTER
如果关系已成功按名称注册,则返回此值
出现错误时,将返回以下负值之一:
SPI_ERROR_ARGUMENT
如果 enr 为 NULL 或其 name 字段为 NULL
SPI_ERROR_UNCONNECTED
如果从未连接的 C 函数调用
SPI_ERROR_REL_DUPLICATE
如果在 enr 的 name 字段中指定的名称已在此连接中注册
5.1.31.SPI_unregister_relation
SPI_unregister_relation — 从注册表中删除一个临时命名关系
5.1.31.1.概要
int SPI_unregister_relation(const char * name)
5.1.31.2.描述
SPI_unregister_relation 从当前连接的注册表中删除一个临时命名关系。
5.1.31.3.参数
const char * name
关系注册表条目名称
5.1.31.4.返回值
如果命令执行成功,则返回以下(非负)值:
SPI_OK_REL_UNREGISTER
如果元组存储已成功从注册表中删除
发生错误时,将返回以下负值之一:
SPI_ERROR_ARGUMENT
如果 name 为 NULL
SPI_ERROR_UNCONNECTED
如果从未连接的 C 函数调用
SPI_ERROR_REL_NOT_FOUND
如果在当前连接的注册表中未找到 name
5.1.32.SPI_register_trigger_data
SPI_register_trigger_data - 使临时触发器数据在 SPI 查询中可用
5.1.32.1 概要
int SPI_register_trigger_data(TriggerData *tdata)
5.1.32.2.描述
SPI_register_trigger_data 使触发器捕获的任何临时关系在通过当前 SPI连接计划和执行的查询中可用。目前,这意味着使用REFERENCING OLD/NEW TABLE AS ... 子句定义的 AFTER 触发器捕获的转换表。此函数应在连接后由 PL触发器处理程序函数调用。
5.1.32.3.参数
TriggerData *tdata
传递给触发器处理程序函数的 fcinfo->context 的 TriggerData 对象
5.1.32.4.返回值
如果命令的执行成功,则将返回以下(非负)值:
SPI_OK_TD_REGISTER
如果已成功注册捕获的触发器数据(如果有)
出现错误时,将返回以下负值之一:
SPI_ERROR_ARGUMENT
如果 tdata 是 NULL
SPI_ERROR_UNCONNECTED
如果从未连接的 C 函数中调用
SPI_ERROR_REL_DUPLICATE
如果任何触发器数据瞬态关系的名称已为此连接注册
5.2.接口支持函数
本节介绍的函数提供了从SPI_execute和其他SPI函数返回的结果集中提取信息的接口。
本节中描述的所有函数都可以由连接和未连接的C函数使用。
5.2.1.SPI_fname
SPI_fname — 确定指定列号的列名
5.2.1.1.概要
char * SPI_fname(TupleDesc rowdesc, int colnumber)
5.2.1.2.描述
SPI_fname 返回指定列的列名的副本。(当您不再需要它时,可以使用pfree释放名称的副本。)
5.2.1.3.参数
TupleDesc rowdesc
输入行描述
int colnumber
列号(计数从1开始)
5.2.1.4.返回值
列名;如果colnumber超出范围,则为NULL。SPI_result设置为SPI_ERROR_NOATTRIBUTE表示错误。
5.2.2.SPI_fnumber
SPI_fnumber - 确定指定列名的列号
5.2.2.1.概要
int SPI_fnumber(TupleDesc rowdesc, const char * colname)
5.2.2.2.描述
SPI_fnumber 返回具有指定名称的列的列号。
如果 colname 引用系统列(例如 ctid),则将返回适当的负列号。调用者应小心地测试返回值,以确保精确等于SPI_ERROR_NOATTRIBUTE 以检测错误;测试结果小于或等于 0 是不正确的,除非系统列应该被拒绝。
5.2.2.3.参数
TupleDesc rowdesc
输入行描述
const char * colname
列名
5.2.2.4.返回值
列号(从 1 开始计数,用于用户定义的列),如果未找到命名列,则为 SPI_ERROR_NOATTRIBUTE。
5.2.3.SPI_getvalue
SPI_getvalue — 返回指定列的字符串值
5.2.3.1.概要
char * SPI_getvalue(HeapTuple row, TupleDesc rowdesc, int colnumber)
5.2.3.2.描述
SPI_getvalue 返回指定列的字符串表示形式的值。
结果以使用palloc分配的内存返回。(您可以使用pfree在不再需要时释放内存。)
5.2.3.3.参数
HeapTuple row
要检查的输入行
TupleDesc rowdesc
输入行描述
int colnumber
列号(计数从1开始)
5.2.3.4.返回值
列值,如果列为空,则为NULL,colnumber超出范围(SPI_result设置为SPI_ERROR_NOATTRIBUTE),或没有输出函数可用(SPI_result设置为SPI_ERROR_NOOUTFUNC)。
5.2.4.SPI_getbinval
SPI_getbinval — 返回指定列的二进制值
5.2.4.1.概要
Datum SPI_getbinval(HeapTuple row, TupleDesc rowdesc, int colnumber,
bool * isnull)
5.2.4.2.描述
SPI_getbinval 返回指定列的内部形式值(作为类型 Datum)。
此函数不为数据分配新空间。对于按引用传递的数据类型,返回值将是传递行的指针。
5.2.4.3.参数
HeapTuple row
要检查的输入行
TupleDesc rowdesc
输入行描述
int colnumber
列号(从 1 开始计数)
bool * isnull
列中空值的标志
5.2.4.4.返回值
返回列的二进制值。如果列为空,则指向 isnull 的变量将设置为 true,否则为 false。
SPI_result 在出现错误时设置为 SPI_ERROR_NOATTRIBUTE。
5.2.5.SPI_gettype
SPI_gettype — 返回指定列的数据类型名称
5.2.5.1.概要
char * SPI_gettype(TupleDesc rowdesc, int colnumber)
5.2.5.2.描述
SPI_gettype 返回指定列的数据类型名称的副本。(当您不再需要它时,可以使用pfree释放名称的副本。)
5.2.5.3.参数
TupleDesc rowdesc
输入行描述
int colnumber
列号(从1开始计数)
5.2.5.4.返回值
指定列的数据类型名称,或错误时为NULL。错误时,SPI_result设置为SPI_ERROR_NOATTRIBUTE。
5.2.6.SPI_gettypeid
SPI_gettypeid — 返回指定列的数据类型 OID
5.2.6.1.概要
Oid SPI_gettypeid(TupleDesc rowdesc, int colnumber)
5.2.6.2.描述
SPI_gettypeid 返回指定列的数据类型 OID。
5.2.6.3.参数
TupleDesc rowdesc
输入行描述
int colnumber
列号(从 1 开始计数)
5.2.6.4.返回值
指定列的数据类型 OID 或错误时为 InvalidOid。出错时,SPI_result 被设置为SPI_ERROR_NOATTRIBUTE。
5.2.7.SPI_getrelname
SPI_getrelname — 返回指定关系的名称
5.2.7.1.概要
char * SPI_getrelname(Relation rel)
5.2.7.2.描述
SPI_getrelname 返回指定关系的名称的副本。(当您不再需要它时,可以使用 pfree 释放名称的副本。)
5.2.7.3.参数
Relation rel
输入关系
5.2.7.4.返回值
指定关系的名称。
5.2.8.SPI_getnspname
SPI_getnspname — 返回指定关系的命名空间
5.2.8.1.概要
char * SPI_getnspname(Relation rel)
5.2.8.2.描述
SPI_getnspname 返回指定的 Relation所属的命名空间的名称的副本。这相当于关系的模式。使用完此函数的返回值后,应该使用pfree 释放它。
5.2.8.3.参数
Relation rel
输入关系
5.2.8.4.返回值
指定关系的命名空间的名称。
5.2.9.SPI_result_code_string
SPI_result_code_string — 返回错误代码字符串
5.2.9.1.概要
const char * SPI_result_code_string(int code);
5.2.9.2.描述
SPI_result_code_string 返回由各种 SPI 函数返回或存储在 SPI_result 中的结果代码的字符串表示形式。
5.2.9.3.参数
int code
结果代码
5.2.9.4.返回值
结果代码的字符串表示形式。
5.3.内存管理
UXDB在内存上下文中分配内存,这提供了一种方便的方法来管理在许多不同地方进行的分配,这些分配需要存活的时间不同。销毁上下文会释放在其中分配的所有内存。因此,不需要跟踪单个对象以避免内存泄漏;相反,只需要管理相对较少的上下文。palloc 和相关函数从“当前”上下文中分配内存。
SPI_connect创建一个新的内存上下文,并且在该上下文中进行所有后续内存分配。在使用完后,可以通过调用SPI_finish来销毁该上下文。
SPI_finish 会恢复先前的内存上下文并销毁由 SPI_connect 创建的上下文。这些操作确保在 C函数退出时回收在其中进行的临时内存分配,避免内存泄漏。然而,如果您的 C函数需要返回分配的内存中的对象(例如,通过引用传递的数据类型的值),则不能在连接到 SPI 时使用palloc 分配该内存。如果尝试这样做,则对象将由 SPI_finish 释放,您的 C 函数将无法可靠地工作。为解决此问题,请使用SPI_palloc 为返回对象分配内存。SPI_palloc 在“上层执行器上下文”中分配内存,即在调用 SPI_connect时的内存上下文,这正是从您的 C 函数返回的值的正确上下文。本节中描述的其他几个实用程序函数也返回在上层执行器上下文中创建的对象。
调用SPI_connect 时,由 SPI_connect 创建的 C 函数的私有上下文成为当前上下文。所有由palloc、repalloc 或 SPI 实用程序函数(除本节所述之外)进行的分配都在此上下文中进行。当 C 函数通过SPI_finish 与 SPI 管理器断开连接时,当前上下文将恢复为上层执行器上下文,并且在 C函数内存上下文中进行的所有分配都将被释放并且不能再使用。
5.3.1.SPI_palloc
SPI_palloc - 在上层执行器上下文中分配内存
5.3.1.1.概要
void * SPI_palloc(Size size)
5.3.1.2.描述
SPI_palloc 在上层执行器上下文中分配内存。
此函数只能在连接到 SPI 时使用。否则,它会抛出错误。
5.3.1.3.参数
Size size
要分配的存储空间的字节数
5.3.1.4.返回值
指向指定大小的新存储空间的指针
5.3.2.SPI_repalloc
SPI_repalloc — 重新分配上层执行器上下文中的内存
5.3.2.1.概要
void * SPI_repalloc(void * pointer, Size size)
5.3.2.2.描述
SPI_repalloc 更改先前使用 SPI_palloc 分配的内存段的大小。
此函数与普通的 repalloc 不再有区别。它仅用于现有代码的向后兼容性。
5.3.2.3.参数
void * pointer
要更改的现有存储的指针
Size size
要分配的存储空间的字节数
5.3.2.4.返回值
指向新指定大小的存储空间的指针,其内容从现有区域复制而来
5.3.3.SPI_pfree
SPI_pfree — 在上级执行器上下文中释放内存
5.3.3.1.概要
void SPI_pfree(void * pointer)
5.3.3.2.描述
SPI_pfree 释放之前使用 SPI_palloc 或 SPI_repalloc 分配的内存。此函数与普通的 pfree 不再有区别。它仅用于现有代码的向后兼容性。
5.3.3.3.参数
void * pointer
指向要释放的现有存储的指针
5.3.4.SPI_copytuple
SPI_copytuple — 在上层执行器上下文中复制一行
5.3.4.1.概要
HeapTuple SPI_copytuple(HeapTuple row)
5.3.4.2.描述
SPI_copytuple 在上层执行器上下文中复制一行。通常用于从触发器返回修改后的行。在声明返回复合类型的函数中,请改用SPI_returntuple。
此函数只能在连接到 SPI 时使用。否则,它将返回 NULL 并将 SPI_result 设置为SPI_ERROR_UNCONNECTED。
5.3.4.3.参数
HeapTuple row
要复制的行
5.3.4.4.返回值
复制的行,或错误时为 NULL(请参阅 SPI_result 获取错误指示)。
5.3.5.SPI_returntuple
SPI_returntuple — 准备将元组作为 Datum 返回
5.3.5.1.概要
HeapTupleHeader SPI_returntuple(HeapTuple row, TupleDesc rowdesc)
5.3.5.2.描述
SPI_returntuple 在上层执行器上下文中复制一行,以行类型 Datum 的形式返回。返回的指针只需通过PointerGetDatum 转换为 Datum,然后返回即可。
此函数只能在连接到 SPI 时使用。否则,它将返回 NULL,并将 SPI_result 设置为SPI_ERROR_UNCONNECTED。
请注意,这应该用于声明返回复合类型的函数。它不用于触发器; 在触发器中返回修改后的行,请使用 SPI_copytuple。
5.3.5.3.参数
HeapTuple row
要复制的行
TupleDesc rowdesc
行的描述符(对于大多数有效的缓存,请每次传递相同的描述符)
5.3.5.4.返回值
指向复制行的 HeapTupleHeader,或者在出现错误时为 NULL。
5.3.6.SPI_modifytuple
SPI_modifytuple — 通过替换给定行的选定字段创建一行
5.3.6.1.概要
HeapTuple SPI_modifytuple(Relation rel, HeapTuple row, int ncols,
int * colnum, Datum * values, const char * nulls)
5.3.6.2.描述
SPI_modifytuple通过替换选定列的新值,复制原始行的其他位置的列,创建一个新行。输入行不会被修改。新行在上层执行器上下文中返回。
此函数只能在连接到 SPI 时使用。否则,它将返回 NULL,并将 SPI_result 设置为SPI_ERROR_UNCONNECTED。
5.3.6.3.参数
Relation rel
仅用作行描述符的源。(传递关系而不是行描述符是一个缺陷。)
HeapTuple row
要修改的行
int ncols
要更改的列数
int * colnum
一个长度为 ncols 的数组,包含要更改的列的编号 (列编号从 1 开始)
Datum * values
一个长度为 ncols 的数组,包含指定列的新值
const char * nulls
一个长度为 ncols 的数组,描述哪些列的值为 NULL。
新值为 null。如果nulls是 NULL,那么 SPI_modifytuple 假定没有新值为null。否则,nulls数组的每个条目应为 ' ',如果相应的新值为非 null,则为'n'。在后一种情况下,相应的 values条目中的实际值并不重要。请注意,nulls不是文本字符串,只是一个数组:它不需要 '\0' 终止符。
5.3.6.4.返回值
返回值为在上层执行器上下文中分配的带有修改的新行,如果出错则为NULL(请参阅 SPI_result 获取错误指示)。
在出错的情况下,SPI_result 设置如下:
SPI_ERROR_ARGUMEN
如果 rel 是 NULL,或者 row 是 NULL,或者 ncols 小于或等于 0,或者 colnum 是NULL,或者 values 是 NULL,则为 SPI_ERROR_ARGUMENT。
SPI_ERROR_NOATTRIBUTE
如果 colnum 包含无效的列号(小于或等于0或大于行中的列数)。
SPI_ERROR_UNCONNECTED
如果SPI未激活。
5.3.7.SPI_freetuple
SPI_freetuple — 释放在上层执行器上下文中分配的行
5.3.7.1.概要
void SPI_freetuple(HeapTuple row)
5.3.7.2.描述
SPI_freetuple 释放先前在上层执行器上下文中分配的行。
此函数与普通的heap_freetuple不再有区别。它仅用于现有代码的向后兼容性。
5.3.7.3.参数
HeapTuple row
要释放的行
5.3.8.SPI_freetuptable
SPI_freetuptable — 释放由 SPI_execute 或类似函数创建的行集
5.3.8.1.概要
void SPI_freetuptable(SPITupleTable * tuptable)
5.3.8.2.描述
SPI_freetuptable 释放由之前的 SPI 命令执行函数(如SPI_execute)创建的行集。因此,该函数通常使用全局变量SPI_tuptable 作为参数。
如果一个使用 SPI 的 C 函数需要执行多个命令,并且不想保留早期命令的结果,那么该函数就很有用。请注意,任何未释放的行集最终都会在SPI_finish 中被释放。此外,如果在使用 SPI 的 C 函数执行期间启动并中止了子事务,则 SPI会自动释放在子事务运行时创建的任何行集。
SPI_freetuptable包含保护逻辑,以防止对同一行集进行重复删除请求。在之前的版本中,重复删除会导致崩溃。
5.3.8.3.参数
SPITupleTable * tuptable
要释放的行集的指针,或者为 NULL 以不执行任何操作
5.3.9.SPI_freeplan
SPI_freeplan — 释放之前保存的预处理语句
5.3.9.1.概要
int SPI_freeplan(SPIPlanPtr plan)
5.3.9.2.描述
SPI_freeplan 释放之前由 SPI_prepare 返回或由 SPI_keepplan 或 SPI_saveplan保存的预处理语句。
5.3.9.3.参数
SPIPlanPtr plan
指向要释放的语句的指针
5.3.9.4.返回值
成功返回0; 如果 plan 为 NULL 或无效,则返回 SPI_ERROR_ARGUMENT。
5.4.事务管理
不可能通过SPI函数(如SPI_execute)运行事务控制命令,如COMMIT和ROLLBACK。但是,有单独的接口函数允许通过SPI进行事务控制。
在任意用户定义的可调用SQL函数中启动和结束事务而不考虑它们被调用的上下文通常是不安全和明智的。例如,在某个SQL命令的一部分的复杂SQL表达式中的函数中的事务边界可能会导致晦涩的内部错误或崩溃。这里介绍的接口函数主要是为了由CALL命令调用的SQL级过程中的过程语言实现支持事务管理而设计的,考虑到CALL调用的上下文。使用C实现的使用SPI的过程可以实现相同的逻辑,但是这些细节超出了本文档的范围。
5.4.1.SPI_commit
SPI_commit,SPI_commit_and_chain — 提交当前事务
5.4.1.1.概要
void SPI_commit(void)
void SPI_commit_and_chain(void)
5.4.1.2.描述
SPI_commit 提交当前事务。它大致相当于运行 SQL 命令COMMIT。事务提交后,将自动使用默认事务特性启动新事务,以便调用者可以继续使用SPI 工具。如果在提交过程中发生故障,则当前事务将被回滚并启动新事务,之后错误将以通常的方式抛出。
SPI_commit_and_chain 与之相同,但新事务将使用与刚刚完成的事务相同的事务特性启动,就像 SQL 命令 COMMIT AND CHAIN 一样。
这些函数只能在调用 SPI_connect_ext 时将 SPI 连接设置为非原子性时执行。
5.4.2.SPI_rollback
SPI_rollback,SPI_rollback_and_chain — 中止当前事务
5.4.2.1.概要
void SPI_rollback(void)
void SPI_rollback_and_chain(void)
5.4.2.2.描述
SPI_rollback 中止当前事务。它大致相当于运行 SQL 命令ROLLBACK。在事务回滚后,自动使用默认事务特性启动新事务,以便调用者可以继续使用SPI 工具。
SPI_rollback_and_chain 与之相同,但使用与刚刚完成的事务相同的事务特性启动新事务,就像使用 SQL 命令ROLLBACK AND CHAIN 一样。
这些函数只能在调用 SPI_connect_ext 时将 SPI 连接设置为非原子性时执行。
5.4.3.SPI_start_transaction
SPI_start_transaction — 废弃函数
5.4.3.1.概要
void SPI_start_transaction(void)
5.4.3.2.描述
SPI_start_transaction 无作用,仅为与早期 UXDB 版本的代码兼容而存在。它曾经在调用SPI_commit 或 SPI_rollback 后必须调用,但现在这些函数会自动开始一个新的事务。
5.5.数据更改的可见性
以下规则控制使用 SPI(或任何其他 C 函数)的函数中数据更改的可见性:
-
在执行 SQL 命令期间,命令所做的任何数据更改对命令本身是不可见的。例如,在:
INSERT INTO a SELECT * FROM a;插入的行对于
SELECT部分是不可见的。 -
由命令 C 进行的更改对于在 C 之后启动的所有命令都是可见的,无论它们是在 C 内部(在执行 C 期间)还是在 C 完成后启动的。
-
在由 SQL 命令调用的函数(普通函数或触发器)中执行的通过 SPI 执行的命令,根据传递给 SPI的读/写标志遵循上述两个规则之一。在只读模式下执行的命令遵循第一个规则:它们无法看到调用命令的更改。在读写模式下执行的命令遵循第二个规则:它们可以看到到目前为止所做的所有更改。
-
所有标准过程语言都根据函数的不稳定性属性设置 SPI 读写模式。
STABLE和IMMUTABLE函数的命令以只读模式执行,而VOLATILE函数的命令以读写模式执行。虽然C 函数的作者能够违反这个约定,但不建议这么做。
下一节包含一个示例,说明了这些规则的应用。
5.6.示例
本节包含了一个非常简单的 SPI 使用示例。C 函数 execq 将 SQL 命令作为其第一个参数,行数作为其第二个参数,使用SPI_exec 执行该命令,并返回被该命令处理的行数。您可以在源代码树中的 src/test/regress/regress.c 和spi模块中找到更复杂的SPI示例。
#include "uxdb.h"
#include "executor/spi.h"
#include "utils/builtins.h"
UX_MODULE_MAGIC;
UX_FUNCTION_INFO_V1(execq);
Datum
execq(UX_FUNCTION_ARGS)
{
char *command;
int cnt;
int ret;
uint64 proc;
/* Convert given text object to a C string */
command = text_to_cstring(UX_GETARG_TEXT_PP(0));
cnt = UX_GETARG_INT32(1);
SPI_connect();
ret = SPI_exec(command, cnt);
proc = SPI_processed;
/*
* If some rows were fetched, print them via elog(INFO).
*/
if (ret > 0 && SPI_tuptable != NULL)
{
SPITupleTable *tuptable = SPI_tuptable;
TupleDesc tupdesc = tuptable->tupdesc;
char buf[8192];
uint64 j;
for (j = 0; j < tuptable->numvals; j++)
{
HeapTuple tuple = tuptable->vals[j];
int i;
for (i = 1, buf[0] = 0; i <= tupdesc->natts; i++)
snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf), " %s%s",
SPI_getvalue(tuple, tupdesc, i),
(i == tupdesc->natts) ? " " : " |");
elog(INFO, "EXECQ: %s", buf);
}
}
SPI_finish();
pfree(command);
UX_RETURN_INT64(proc);
}
这是在将其编译为共享库后声明该函数的方法(详细信息请参见编译和链接动态加载函数):
CREATE FUNCTION execq(text, integer) RETURNS int8
AS 'filename'
LANGUAGE C STRICT;
这是一个示例会话:
=> SELECT execq('CREATE TABLE a (x integer)', 0);
execq
-------
0
(1 row)
=> INSERT INTO a VALUES (execq('INSERT INTO a VALUES (0)', 0));
INSERT 0 1
=> SELECT execq('SELECT * FROM a', 0);
INFO: EXECQ: 0 -- inserted by execq
INFO: EXECQ: 1 -- returned by execq and inserted by upper INSERT
execq
-------
2
(1 row)
=> SELECT execq('INSERT INTO a SELECT x + 2 FROM a', 1);
execq
-------
1
(1 row)
=> SELECT execq('SELECT * FROM a', 10);
INFO: EXECQ: 0
INFO: EXECQ: 1
INFO: EXECQ: 2 -- 0 + 2, only one row inserted - as specified
execq
-------
3 -- 10 is the max value only, 3 is the real number of rows
(1 row)
=> DELETE FROM a;
DELETE 3
=> INSERT INTO a VALUES (execq('SELECT * FROM a', 0) + 1);
INSERT 0 1
=> SELECT * FROM a;
x
---
1 -- no rows in a (0) + 1
(1 row)
=> INSERT INTO a VALUES (execq('SELECT * FROM a', 0) + 1);
INFO: EXECQ: 1
INSERT 0 1
=> SELECT * FROM a;
x
---
1
2 -- there was one row in a + 1
(2 rows)
-- This demonstrates the data changes visibility rule:
=> INSERT INTO a SELECT execq('SELECT * FROM a', 0) * x FROM a;
INFO: EXECQ: 1
INFO: EXECQ: 2
INFO: EXECQ: 1
INFO: EXECQ: 2
INFO: EXECQ: 2
INSERT 0 2
=> SELECT * FROM a;
x
---
1
2
2 -- 2 rows * 1 (x in first row)
6 -- 3 rows (2 + 1 just inserted) * 2 (x in second row)
(4 rows) ^^^^^^
rows visible to execq() in different invocations
6.后台工作进程
UXDB 可以扩展以在单独的进程中运行用户提供的代码。这些进程由 uxdb 启动、停止和监视,允许它们的生命周期与服务器状态密切相关。这些进程附加到 UXDB 的共享内存区域,并具有内部连接到数据库的选项;它们还可以串行运行多个事务,就像常规的客户端连接服务器进程一样。此外,通过链接到libuxsql,它们可以连接到服务器并像常规客户端应用程序一样运行。
警告
使用后台工作进程存在相当大的健壮性和安全风险,因为它们是用 C 语言编写的,具有对数据的无限制访问权限。希望启用包含后台工作进程的模块的管理员应该极度谨慎。只有经过仔细审计的模块才应被允许运行后台工作进程。
可以通过在 shared_preload_libraries 中包含模块名称来初始化后台工作进程。希望运行后台工作进程的模块可以通过调用RegisterBackgroundWorker(BackgroundWorker *worker) 从其_UX_init() 函数中注册它。后台工作进程也可以在系统启动后通过调用RegisterDynamicBackgroundWorker(BackgroundWorker *worker, BackgroundWorkerHandle **handle)来启动。与只能从uxmaster进程中调用的RegisterBackgroundWorker 不同,RegisterDynamicBackgroundWorker必须从常规后端或另一个后台工作进程中调用。
结构体 BackgroundWorker 定义如下:
typedef void (*bgworker_main_type)(Datum main_arg);
typedef struct BackgroundWorker
{
char bgw_name[BGW_MAXLEN];
char bgw_type[BGW_MAXLEN];
int bgw_flags;
BgWorkerStartTime bgw_start_time;
int bgw_restart_time; /* in seconds, or BGW_NEVER_RESTART */
char bgw_library_name[BGW_MAXLEN];
char bgw_function_name[BGW_MAXLEN];
Datum bgw_main_arg;
char bgw_extra[BGW_EXTRALEN];
int bgw_notify_pid;
} BackgroundWorker;
bgw_name 和 bgw_type 是用于日志消息、进程列表和类似上下文的字符串。bgw_type 应该对于同一类型的所有后台工作者都是相同的,以便可以在进程列表中对这些工作者进行分组。另一方面,bgw_name 可以包含有关特定进程的其他信息。(通常,bgw_name的字符串将以某种方式包含类型,但这并不是严格要求的。)
bgw_flags 是按位或的位掩码,指示模块想要的功能。可能的值包括:
BGWORKER_SHMEM_ACCESS
请求共享内存访问。此标志是必需的。
BGWORKER_BACKEND_DATABASE_CONNECTION
请求建立数据库连接的能力,以便稍后可以通过该连接运行事务和查询。使用 BGWORKER_BACKEND_DATABASE_CONNECTION 连接到数据库的后台工作者还必须使用 BGWORKER_SHMEM_ACCESS 附加共享内存,否则工作者启动将失败。
bgw_start_time 是 uxdb 应该启动进程的服务器状态;它可以是 BgWorkerStart_UxmasterStart(在 uxdb 自身完成自己的初始化后立即启动;请求此类进程不适用于数据库连接)、BgWorkerStart_ConsistentState(在热备机制中达到一致状态后立即启动,允许进程连接到数据库并运行只读查询)和 BgWorkerStart_RecoveryFinished(在系统进入正常的读写状态后立即启动)。请注意,最后两个值在不是热备机制的服务器中是等效的。请注意,此设置仅指示何时启动进程;它们不会在达到不同状态时停止。
bgw_restart_time 是 uxdb 在进程崩溃时等待重启进程的时间间隔(以秒为单位)。它可以是任何正值,或 BGW_NEVER_RESTART,表示在进程崩溃时不重新启动进程。
bgw_library_name 是要查找后台工作者初始入口点的库的名称。工作者进程将动态加载命名库,并使用 bgw_function_name 来标识要调用的函数。如果从核心代码加载函数,则必须将其设置为 "uxdb"。
bgw_function_name 是动态加载库中要用作新后台工作者初始入口点的函数的名称。
bgw_main_arg 是后台工作者主函数的 Datum 参数。此主函数应接受一个指向 BackgroundWorker 结构的指针作为其唯一参数。单个类型为Datum的参数,并返回void。bgw_main_arg将作为参数传递。此外,全局变量MyBgworkerEntry指向在注册时传递的BackgroundWorker结构的副本;工作进程可能会发现检查此结构有帮助。
在Windows(以及任何其他定义了EXEC_BACKEND的地方)或动态后台工作进程中,通过引用传递Datum是不安全的,只能通过值传递。如果需要参数,则最安全的方法是传递int32或其他小值,并将其用作在共享内存中分配的数组的索引。如果传递像cstring或text这样的值,则指针将无法从新的后台工作进程中有效。
bgw_extra可以包含要传递给后台工作进程的额外数据。与bgw_main_arg不同,此数据不作为参数传递给工作进程的主函数,但可以通过上面讨论的MyBgworkerEntry访问。
bgw_notify_pid是UXDB后端进程的PID,当进程启动或退出时,uxmaster应向其发送SIGUSR1。对于在uxmaster启动时注册的工作进程,应将其设置为0,或者当注册工作进程的后端不希望等待工作进程启动时。否则,它应初始化为MyProcPid。
一旦运行,进程可以通过调用BackgroundWorkerInitializeConnection(char *dbname, char *username, uint32 flags)或BackgroundWorkerInitializeConnectionByOid(Oid dboid, Oid useroid, uint32 flags)连接到数据库。这允许进程使用SPI接口运行事务和查询。如果dbname为NULL或dboid为InvalidOid,则会话未连接到任何特定数据库,但可以访问共享目录。如果username为NULL或useroid为InvalidOid,则进程将作为在initdb期间创建的超级用户运行。如果指定了flags为BGWORKER_BYPASS_ALLOWCONN,则可以绕过连接到不允许用户连接的数据库的限制。后台工作进程只能调用这两个函数之一,并且只能调用一次。无法切换数据库。
当控制到达后台工作进程的主函数时,信号最初被阻塞,并且必须由其解除阻塞;这是为了允许进程自定义其信号处理程序(如果必要)。可以通过调用BackgroundWorkerUnblockSignals在新进程中取消阻止信号,并通过调用BackgroundWorkerBlockSignals阻止信号。
如果后台工作进程的bgw_restart_time配置为BGW_NEVER_RESTART,或者退出时的退出代码为0或被TerminateBackgroundWorker终止,则它将在退出时由uxmaster自动注销。否则,将在通过bgw_restart_time配置的时间段后重新启动,或者如果uxmaster在重新启动之前退出,则立即重新启动。由于后端故障,重新初始化集群。需要暂时挂起执行的后端应该使用可中断的睡眠而不是退出;这可以通过调用WaitLatch()来实现。在调用该函数时,请确保设置了WL_UXMASTER_DEATH标志,并验证返回代码以在紧急情况下及时退出uxdb本身。
当使用RegisterDynamicBackgroundWorker函数注册后台工作进程时,执行注册的后端可以获取有关工作进程状态的信息。希望这样做的后端应该将BackgroundWorkerHandle *的地址作为第二个参数传递给RegisterDynamicBackgroundWorker。如果成功注册了工作进程,这个指针将被初始化为一个不透明的句柄,随后可以将其传递给GetBackgroundWorkerPid(BackgroundWorkerHandle *, pid_t *)或TerminateBackgroundWorker(BackgroundWorkerHandle *)。
GetBackgroundWorkerPid可用于轮询工作进程的状态:返回值BGWH_NOT_YET_STARTED表示uxmaster尚未启动工作进程;BGWH_STOPPED表示它已经启动但不再运行;BGWH_STARTED表示它正在运行。在最后一种情况下,PID也将通过第二个参数返回。
TerminateBackgroundWorker会导致uxmaster向工作进程发送SIGTERM,如果它正在运行,并在它不再运行时注销它。
在某些情况下,注册后台工作进程的进程可能希望等待工作进程启动。这可以通过将bgw_notify_pid初始化为MyProcPid,然后将在注册时获取的BackgroundWorkerHandle *传递WaitForBackgroundWorkerStartup(BackgroundWorkerHandle *handle, pid_t *)函数来实现。此函数将阻塞,直到uxmaster尝试启动后台工作进程,或者直到uxmaster死亡。如果后台工作进程正在运行,则返回值将是BGWH_STARTED,PID将写入提供的地址。否则,返回值将是BGWH_STOPPED或BGWH_UXMASTER_DIED。
进程还可以通过使用WaitForBackgroundWorkerShutdown(BackgroundWorkerHandle *handle)函数并传递在注册时获取的BackgroundWorkerHandle *来等待后台工作进程关闭。此函数将阻塞,直到后台工作进程退出或uxmaster死亡。当后台工作进程退出时,返回值为BGWH_STOPPED,如果uxmaster死亡,则返回BGWH_UXMASTER_DIED。
后台工作进程可以发送异步通知消息,可以通过使用NOTIFY命令通过SPI或直接通过Async_Notify()来实现。这样的通知将在事务提交时发送。后台工作进程不应使用LISTEN命令注册接收异步通知,因为没有基础设施来消耗这样的通知。src/test/modules/worker_spi模块包含一个工作示例,演示了一些有用的技术。
注册后台工作进程的最大数量受max_worker_processes限制。
7.逻辑解码
UXDB 提供了基础设施,将通过 SQL 执行的修改流式传输到外部消费者。这个功能可以用于各种目的,包括复制解决方案和审计。
更改通过逻辑复制插槽标识的流发送出去。
这些更改的流式传输格式由使用的输出插件确定。UXDB分发中提供了一个示例插件。可以编写其他插件来扩展可用格式的选择,而无需修改任何核心代码。每个输出插件都可以访问由 INSERT 产生的每个单独的新行和由 UPDATE 创建的新行版本。对于 UPDATE 和DELETE 的旧行版本的可用性取决于配置的副本标识。
更改可以使用流式复制协议和流复制协议接口或通过 SQL调用函数(请参见逻辑解码 SQL 接口)来消耗。还可以编写其他方法来消耗复制插槽的输出,而无需修改核心代码(请参见逻辑解码输出写入器)。
7.1.逻辑解码示例
以下示例演示如何使用 SQL 接口控制逻辑解码。
在使用逻辑解码之前,必须将wal_level设置为logical,并将max_replication_slots设置为至少 1。然后,您应该以超级用户身份连接到目标数据库(在下面的示例中为 uxdb)。
uxdb=# -- 使用输出插件 'test_decoding' 创建名为 'regression_slot' 的插槽
uxdb=# SELECT * FROM ux_create_logical_replication_slot('regression_slot', 'test_decoding', false, true);
slot_name | lsn
-----------------+-----------
regression_slot | 0/16B1970
(1 行记录)
uxdb=# SELECT slot_name, plugin, slot_type, database, active, restart_lsn, confirmed_flush_lsn FROM ux_replication_slots;
slot_name | plugin | slot_type | database | active | restart_lsn | confirmed_flush_lsn
-----------------+---------------+-----------+----------+--------+-------------+-----------------
regression_slot | test_decoding | logical | uxdb | f | 0/16A4408 | 0/16A4440
(1 行记录)
uxdb=# -- 目前没有变化可见
uxdb=# SELECT * FROM ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----+-----+------
(0 行记录)
uxdb=# CREATE TABLE data(id serial primary key, data text);
CREATE TABLE
uxdb=# -- DDL 不会被复制,所以您只会看到事务
uxdb=# SELECT * FROM ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-------+--------------
0/BA2DA58 | 10297 | BEGIN 10297
0/BA5A5A0 | 10297 | COMMIT 10297
(2 行记录)
uxdb=# -- 一旦读取变化,它们就会被消耗并在随后的调用中不会被发出:
uxdb=# SELECT * FROM ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----+-----+------
(0 行记录)
uxdb=# BEGIN;
uxdb=*# INSERT INTO data(data) VALUES('1');
uxdb=*# INSERT INTO data(data) VALUES('2');
uxdb=*# COMMIT;
uxdb=# SELECT * FROM ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-------+---------------------------------------------------------0/BA5A688 | 10298 | BEGIN 10298
0/BA5A6F0 | 10298 | 表 public.data:插入:id[integer]:1 data[text]:'1'
0/BA5A7F8 | 10298 | 表 public.data:插入:id[integer]:2 data[text]:'2'
0/BA5A8A8 | 10298 | COMMIT 10298
(4 行记录)
uxdb=# INSERT INTO data(data) VALUES('3');
uxdb=# -- 您还可以在不消耗更改的情况下预览更改流
uxdb=# SELECT * FROM ux_logical_slot_peek_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-------+---------------------------------------------------------
0/BA5A8E0 | 10299 | BEGIN 10299
0/BA5A8E0 | 10299 | 表 public.data:插入:id[integer]:3 data[text]:'3'
0/BA5A990 | 10299 | COMMIT 10299
(3 行记录)
uxdb=# -- 下一次调用 ux_logical_slot_peek_changes() 将再次返回相同的更改
uxdb=# SELECT * FROM ux_logical_slot_peek_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-------+---------------------------------------------------------
0/BA5A8E0 | 10299 | BEGIN 10299
0/BA5A8E0 | 10299 | 表 public.data:插入:id[integer]:3 data[text]:'3'
0/BA5A990 | 10299 | COMMIT 10299
(3 行记录)
uxdb=# -- 可以向输出插件传递选项,以影响格式
uxdb=# SELECT * FROM ux_logical_slot_peek_changes('regression_slot', NULL, NULL, 'include-timestamp', 'on');
lsn | xid | data
-----------+-------+---------------------------------------------------------
0/BA5A8E0 | 10299 | BEGIN 10299
0/BA5A8E0 | 10299 | 表 public.data:插入:id[integer]:3 data[text]:'3'
0/BA5A990 | 10299 | COMMIT 10299 (at 2017-05-10 12:07:21.272494-04)
(3 行记录)
uxdb=# -- 记得销毁您不再需要的插槽,以停止消耗服务器资源:
uxdb=# -- SELECT ux_drop_replication_slot('regression_slot');
ux_drop_replication_slot
-----------------------
(1 行记录)
以下示例显示了如何通过流复制协议控制逻辑解码,使用 UXDB 发行版中包含的程序 ux_recvlogical。这要求客户端身份验证设置为允许复制连接,并且必须将 max_wal_senders 设置足够高以允许额外的连接。第二个示例显示了如何流式传输两阶段事务。在使用两阶段命令之前,必须将 max_prepared_transactions 设置为至少 1。
示例 1:
$ ux_recvlogical -d uxdb --slot=test --create-slot
$ ux_recvlogical -d uxdb --slot=test --start -f -
Control+Z
$ uxsql -d uxdb -c "INSERT INTO data(data) VALUES('4');"
$ fg
BEGIN 693
table public.data: INSERT: id[integer]:4 data[text]:'4'
COMMIT 693
Control+C
$ ux_recvlogical -d uxdb --slot=test --drop-slot
示例 2:
$ ux_recvlogical -d uxdb --slot=test --create-slot --two-phase
$ ux_recvlogical -d uxdb --slot=test --start -f -
Control+Z
$ uxsql -d uxdb -c "BEGIN;INSERT INTO data(data) VALUES('5');PREPARE TRANSACTION 'test';"
$ fg
BEGIN 694
table public.data: INSERT: id[integer]:5 data[text]:'5'
PREPARE TRANSACTION 'test', txid 694
Control+Z
$ uxsql -d uxdb -c "COMMIT PREPARED 'test';"
$ fg
COMMIT PREPARED 'test', txid 694
Control+C$ ux_recvlogical -d uxdb --slot=test --drop-slot
以下示例展示了可用于解码已准备事务的 SQL 接口。在使用两阶段提交命令之前,必须将 max_prepared_transactions设置为至少 1。您还必须在使用 ux_create_logical_replication_slot创建插槽时将两阶段参数设置为“true”。请注意,如果尚未解码,则在提交后将流式传输整个事务。
uxdb=# BEGIN;
uxdb=*# INSERT INTO data(data) VALUES('5');
uxdb=*# PREPARE TRANSACTION 'test_prepared1';
uxdb=# SELECT * FROM ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-----+---------------------------------------------------------
0/1689DC0 | 529 | BEGIN 529
0/1689DC0 | 529 | table public.data: INSERT: id[integer]:3 data[text]:'5'
0/1689FC0 | 529 | PREPARE TRANSACTION 'test_prepared1', txid 529
(3 rows)
uxdb=# COMMIT PREPARED 'test_prepared1';
uxdb=# select * from ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-----+--------------------------------------------
0/168A060 | 529 | COMMIT PREPARED 'test_prepared1', txid 529
(4 row)
uxdb=#-- 您还可以回滚已准备的事务
uxdb=# BEGIN;
uxdb=*# INSERT INTO data(data) VALUES('6');
uxdb=*# PREPARE TRANSACTION 'test_prepared2';
uxdb=# select * from ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-----+---------------------------------------------------------
0/168A180 | 530 | BEGIN 530
0/168A1E8 | 530 | table public.data: INSERT: id[integer]:4 data[text]:'6'
0/168A430 | 530 | PREPARE TRANSACTION 'test_prepared2', txid 530
(3 rows)
uxdb=# ROLLBACK PREPARED 'test_prepared2';
uxdb=# select * from ux_logical_slot_get_changes('regression_slot', NULL, NULL);
lsn | xid | data
-----------+-----+----------------------------------------------
0/168A4B8 | 530 | ROLLBACK PREPARED 'test_prepared2', txid 530
(1 row)
7.2.逻辑解码概念
7.2.1.逻辑解码
逻辑解码是将数据库表的所有持久性更改提取到一个连贯、易于理解的格式中的过程,这个格式可以在不了解数据库内部状态的详细知识的情况下进行解释。
在UXDB中,逻辑解码是通过解码预写式日志的内容(描述存储级别的更改)来实现的,将其转换为应用程序特定的形式,例如元组流或SQL语句。
7.2.2.复制槽
在逻辑复制的上下文中,槽表示可以按照它们在源服务器上的顺序重放给客户端的更改流。每个槽从单个数据库中流式传输一系列更改。
注意
UXDB还具有流复制槽,但它们在那里的使用方式略有不同。
复制槽具有在UXDB集群中所有数据库中唯一的标识符。槽独立于使用它们的连接并具有崩溃安全性。
在正常操作中,逻辑槽将仅发出每个更改一次。每个插槽的当前位置仅在检查点时持久化,因此在发生崩溃的情况下,插槽可能会返回到较早的LSN,这将导致在服务器重新启动时再次发送最近的更改。逻辑解码客户端负责避免处理相同的消息多次造成的不良影响。客户端可能希望记录它们解码时看到的最后一个LSN,并跳过任何重复的数据或(在使用复制协议时)请求从该LSN开始解码,而不是让服务器确定起始点。复制进度跟踪功能就是为此目的而设计的,请参见复制进度跟踪。
单个数据库可能存在多个独立的插槽。每个插槽都有自己的状态,允许不同的消费者从数据库更改流的不同点接收更改。对于大多数应用程序,每个消费者都需要一个单独的插槽。
逻辑复制插槽对接收者的状态一无所知。甚至可能有多个不同的接收者在不同的时间使用同一个插槽;它们只会得到从上一个接收者停止消费后的更改。任何时候只有一个接收者可以从插槽中消费更改。
注意
复制插槽在崩溃后持久存在,并且对其使用者的状态一无所知。即使没有使用它们的连接,它们也会防止所需资源的删除。这会消耗存储空间,因为只要复制插槽需要,就不能通过VACUUM删除所需的WAL或系统目录中的所需行。在极端情况下,这可能会导致数据库关闭以防止事务ID环绕。因此,如果不再需要插槽,则应将其删除。
7.2.3.输出插件
输出插件将写入前日志的内部表示形式转换为复制插槽使用者所需的格式。
7.2.4.输出插件
使用流复制接口(请参见CREATE_REPLICATION_SLOT)创建新的复制插槽时,将导出快照,该快照将显示所有更改将包含在更改流中之后的数据库状态。这可用于通过使用SET TRANSACTION SNAPSHOT读取插槽创建时数据库的状态来创建新的副本。
然后可以使用插槽的内容更新该时间点的数据库状态,而不会丢失任何更改。并非总是可能创建快照。特别是在连接到热备时,将失败。不需要快照导出的应用程序可以使用NOEXPORT_SNAPSHOT选项来禁止它。
7.3.流复制协议接口
使用以下命令:
-
CREATE_REPLICATION_SLOT slot_nameLOGICALoutput_plugin -
DROP_REPLICATION_SLOT slot_name[WAIT] -
START_REPLICATION SLOT slot_nameLOGICAL ...
分别用于创建、删除和流式传输复制槽中的更改。这些命令仅在复制连接上可用;它们不能通过 SQL 使用。
命令ux_recvlogical可用于控制流复制连接上的逻辑解码。(它在内部使用这些命令。)
7.4.逻辑解码 SQL 接口
了解与逻辑解码交互的SQL 级别 API的详细文档。
同步复制仅支持在流复制接口上使用的复制槽。函数接口和其他非核心接口不支持同步复制。
7.5.与逻辑解码相关的系统目录
ux_replication_slots视图和ux_stat_replication视图提供有关复制槽和流复制连接的当前状态信息。这些视图适用于物理和逻辑复制。ux_stat_replication_slots视图提供有关逻辑复制槽的统计信息。
7.6.逻辑解码输出插件
子目录中可以找到一个示例输出插件。
7.6.1.初始化函数
通过动态加载输出插件名称作为库基本名称的共享库来加载输出插件。使用正常的库搜索路径来定位库。为了提供所需的输出插件回调函数并指示库实际上是一个输出插件,它需要提供一个名为_UX_output_plugin_init 的函数。此函数接收一个结构体,需要填充各个操作的回调函数指针。
typedef struct OutputPluginCallbacks
{
LogicalDecodeStartupCB startup_cb;
LogicalDecodeBeginCB begin_cb;
LogicalDecodeChangeCB change_cb;
LogicalDecodeTruncateCB truncate_cb;
LogicalDecodeCommitCB commit_cb;
LogicalDecodeMessageCB message_cb;
LogicalDecodeFilterByOriginCB filter_by_origin_cb;
LogicalDecodeShutdownCB shutdown_cb;
LogicalDecodeFilterPrepareCB filter_prepare_cb;
LogicalDecodeBeginPrepareCB begin_prepare_cb;
LogicalDecodePrepareCB prepare_cb;
LogicalDecodeCommitPreparedCB commit_prepared_cb;
LogicalDecodeRollbackPreparedCB rollback_prepared_cb;
LogicalDecodeStreamStartCB stream_start_cb;
LogicalDecodeStreamStopCB stream_stop_cb;
LogicalDecodeStreamAbortCB stream_abort_cb;
LogicalDecodeStreamPrepareCB stream_prepare_cb;
LogicalDecodeStreamCommitCB stream_commit_cb;
LogicalDecodeStreamChangeCB stream_change_cb;
LogicalDecodeStreamMessageCB stream_message_cb;
LogicalDecodeStreamTruncateCB stream_truncate_cb;
} OutputPluginCallbacks;
typedef void (*LogicalOutputPluginInit) (struct OutputPluginCallbacks *cb);
输出插件回调函数begin_cb,change_cb和commit_cb是必需的,而startup_cb,filter_by_origin_cb,truncate_cb和shutdown_cb是可选的。如果未设置truncate_cb但需要解码TRUNCATE,则该操作将被忽略。
输出插件还可以定义函数以支持大型正在进行的事务的流式传输。stream_start_cb,stream_stop_cb,stream_abort_cb,stream_commit_cb,stream_change_cb和stream_prepare_cb是必需的,而stream_message_cb和stream_truncate_cb是可选的。
输出插件还可以定义函数以支持两阶段提交,这允许在PREPARETRANSACTION上解码操作。begin_prepare_cb,prepare_cb,stream_prepare_cb,commit_prepared_cb和rollback_prepared_cb回调是必需的,而filter_prepare_cb是可选的。
7.6.2.功能
为了解码,格式化和输出更改,输出插件可以使用大多数后端的正常基础设施,包括调用输出函数。只允许对关系进行只读访问,
ALTER TABLE user_catalog_table SET (user_catalog_table = true);
CREATE TABLE another_catalog_table(data text) WITH (user_catalog_table = true);
请注意,输出插件中对用户目录表或常规系统目录表的访问必须仅通过systable_*扫描API进行。通过heap_*扫描API的访问将出错。此外,禁止任何导致事务ID分配的操作。其中包括写入表,执行DDL更改和调用ux_current_xact_id()等。
7.6.3.输出模式
输出插件回调可以以几乎任意格式向消费者传递数据。对于某些用例,例如通过SQL查看更改,以包含任意数据的数据类型(例如bytea)返回数据是很麻烦的。如果输出插件仅以服务器编码中的文本数据输出数据,则可以通过将OutputPluginOptions.output_type设置为OUTPUT_PLUGIN_TEXTUAL_OUTPUT而不是OUTPUT_PLUGIN_BINARY_OUTPUT来声明。启动回调。在这种情况下,所有数据都必须在服务器的编码中,以便text数据可以包含它。这在启用断言的构建中进行检查。
7.6.4.输出插件回调
输出插件通过各种回调被通知正在发生的更改。
并发事务按提交顺序解码,只有属于特定事务的更改在begin和commit回调之间解码。显式或隐式回滚的事务永远不会被解码。成功的保存点按它们在该事务中执行的顺序折叠到包含它们的事务中。使用PREPARE TRANSACTION进行两阶段提交的事务也将被解码,如果提供了用于解码它们的输出插件回调。当前准备的事务可能会通过ROLLBACK PREPARED命令并发地中止解码。在这种情况下,此事务的逻辑解码也将被中止。一旦检测到中止并调用prepare_cb回调,此类事务的所有更改都将被跳过。因此,即使在并发中止的情况下,输出插件也提供了足够的信息,以便在解码ROLLBACK PREPARED时正确处理它。
注意
只有已经安全刷新到磁盘的事务才会被解码。这可能导致在将synchronous_commit设置为off的情况下,COMMIT不会立即在直接后续的ux_logical_slot_get_changes()中被解码。
7.6.4.1.启动回调
每当创建或要求流更改时,都会调用可选的startup_cb回调,与准备放出的更改数量无关。
typedef void (*LogicalDecodeStartupCB) (struct LogicalDecodingContext *ctx,
OutputPluginOptions *options,
bool is_init);
is_init参数在创建复制槽时为true,在其他情况下为false。options指向一个选项结构,输出插件可以设置:
typedef struct OutputPluginOptions
{
OutputPluginOutputType output_type;
bool receive_rewrites;
} OutputPluginOptions;
output_type必须设置为OUTPUT_PLUGIN_TEXTUAL_OUTPUT或OUTPUT_PLUGIN_BINARY_OUTPUT。另请参见输出模式。如果receive_rewrites为true,则输出插件还将为某些DDL操作期间进行的堆重写所做的更改调用。这些对于处理DDL复制的插件很有用,但需要特殊处理。
启动回调应验证ctx->output_plugin_options中存在的选项。如果输出插件需要状态,则可以使用ctx->output_plugin_private来存储它。
7.6.4.2.关闭回调函数
可选的 shutdown_cb回调函数在以前活跃的复制槽不再使用时被调用,可以用于释放输出插件私有的资源。槽不一定被删除,只是停止流式传输。
typedef void (*LogicalDecodeShutdownCB) (struct LogicalDecodingContext *ctx);
7.6.4.3.事务开始回调函数
必需的 begin_cb回调函数在已提交事务的开始被解码时被调用。已中止的事务及其内容永远不会被解码。
typedef void (*LogicalDecodeBeginCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn);
txn 参数包含有关事务的元信息,例如其提交时间戳和 XID。
7.6.4.4.事务结束回调函数
必需的 commit_cb 回调函数在已解码事务提交时被调用。如果有任何修改的行,则所有修改的行的 change_cb回调函数将在此之前被调用。
typedef void (*LogicalDecodeCommitCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr commit_lsn);
7.6.4.5.修改回调函数
必需的 change_cb 回调函数在事务内的每个单独的行修改中被调用,无论是 INSERT、UPDATE 还是DELETE。即使原始命令一次修改了多行,回调函数也会为每行单独调用。change_cb回调函数可以访问系统或用户目录表,以帮助输出行修改的详细信息。在解码准备好的(但尚未提交的)事务或解码未提交的事务的情况下,此更改回调函数也可能由于此同一事务的同时回滚而出错。在这种情况下,已中止事务的逻辑解码会优雅地停止。
typedef void (*LogicalDecodeChangeCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
Relation relation,
ReorderBufferChange *change);
ctx 和 txn 参数与 begin_cb 和 commit_cb 回调函数的内容相同,但此外,关系描述符 relation 指向行所属的关系,并传递了描述行修改的结构体change。
注意
只有用户定义的表中的更改,这些表不是未记录的且不是临时的,才能使用逻辑解码提取。
7.6.4.6.截断回调
truncate_cb 回调函数会在执行 TRUNCATE命令时被调用。
typedef void (*LogicalDecodeTruncateCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
int nrelations,
Relation relations[],
ReorderBufferChange *change);
这些参数与 change_cb 回调函数类似。但是,由于连接外键的表上的 TRUNCATE操作需要一起执行,因此此回调函数接收一个关系数组而不是单个关系。
7.6.4.7.源过滤器回调
可选的 filter_by_origin_cb 回调函数用于确定从 origin_id 回放的数据是否对输出插件有用。
typedef bool (*LogicalDecodeFilterByOriginCB) (struct LogicalDecodingContext *ctx,
RepOriginId origin_id);
ctx 参数与其他回调函数的内容相同。除了源之外,没有其他信息可用。如果要表示传入节点上产生的更改是无关紧要的,请返回true,导致它们被过滤掉;否则返回false。对于已被过滤的事务和更改,其他回调函数将不会被调用。
这在实现级联或多向复制解决方案时非常有用。通过源进行过滤可以防止在这些设置中来回复制相同的更改。虽然事务和更改也携带有关源的信息,但通过此回调进行过滤明显更有效。
7.6.4.8.通用消息回调
可选的 message_cb回调函数会在解码逻辑解码消息后被调用。
typedef void (*LogicalDecodeMessageCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr message_lsn,
bool transactional,
const char *prefix,
Size message_size,
const char *message);
txn 参数包含有关事务的元信息,例如其提交的时间戳和 XID。但是请注意,当消息是非事务性的且在记录消息的事务中尚未分配 XID时,它可能为 NULL。lsn是消息的 WAL 位置。transactional表示消息是否作为事务性消息发送。与更改回调类似,在解码准备(但尚未提交)事务或解码未提交事务的情况下,此消息回调也可能因此同步回滚而出错。在这种情况下,将优雅地停止中止事务的逻辑解码。prefix是任意的以 null 结尾的前缀,可用于标识当前插件的有趣消息。最后,message参数包含大小为message_size的实际消息。应特别注意确保输出插件使用的前缀是唯一的,以避免与其他插件冲突。认为有趣的是独特的。使用扩展名或输出插件本身的名称通常是一个不错的选择。
7.6.4.9.准备过滤回调
可选的filter_prepare_cb回调被调用以确定当前两阶段提交事务的一部分数据是否应在此准备阶段或稍后作为常规单阶段事务在COMMIT PREPARED时间。要发出跳过解码的信号,请返回true;否则返回false。当未定义回调时,假定为false(即无过滤,所有使用两阶段提交的事务也将以两个阶段解码)。
typedef bool (*LogicalDecodeFilterPrepareCB) (struct LogicalDecodingContext *ctx,
TransactionId xid,
const char *gid);
ctx 参数的内容与其他回调相同。参数 xid 和 gid 提供了两种不同的方法来标识事务。稍后的COMMIT PREPARED或ROLLBACK PREPARED携带两个标识符,为输出插件提供了选择使用哪个的选择。
回调可能会多次调用以解码,并且必须为给定的一对 xid 和 gid 提供相同的静态答案每次调用它时。
7.6.4.10.事务开始准备回调
每当已解码准备事务的开始时,都会调用必需的begin_prepare_cb回调。在此回调中,可以使用gid字段(它是txn参数的一部分)来检查插件是否已收到此PREPARE,在这种情况下,它可以出错或跳过事务的其余更改。
typedef void (*LogicalDecodeBeginPrepareCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn);
7.6.4.11.事务准备回调
每当已解码为两阶段提交准备的事务时,都会调用必需的prepare_cb回调。如果有任何修改的行,则所有修改的行的change_cb回调将在此之前被调用。可以在此回调中使用gid字段(它是txn参数的一部分)。
typedef void (*LogicalDecodePrepareCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr prepare_lsn);
7.6.4.12.事务提交准备回调
每当已解码COMMIT PREPARED事务时,都会调用必需的commit_prepared_cb回调。可以在此回调中使用gid字段(它是 txn参数的一部分)。
typedef void (*LogicalDecodeCommitPreparedCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr commit_lsn);
7.6.4.13.事务回滚准备回调
必需的rollback_prepared_cb回调在解码事务ROLLBACK PREPARED时调用。gid字段是txn参数的一部分,可以在此回调中使用。参数prepare_end_lsn和prepare_time可用于检查插件是否已接收此PREPARE TRANSACTION,在这种情况下,它可以应用回滚,否则,它可以跳过回滚操作。仅gid是不足够的,因为下游节点可能具有相同标识符的准备事务。
typedef void (*LogicalDecodeRollbackPreparedCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr prepare_end_lsn,
TimestampTz prepare_time);
7.6.4.14.流开始回调
stream_start_cb回调在打开从正在进行的事务中流式传输的更改块时调用。
typedef void (*LogicalDecodeStreamStartCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn);
7.6.4.15.流停止回调
stream_stop_cb回调在关闭从正在进行的事务中流式传输的更改块时调用。
typedef void (*LogicalDecodeStreamStopCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn);
7.6.4.16.流终止回调
stream_abort_cb回调用于中止先前流式传输的事务。
typedef void (*LogicalDecodeStreamAbortCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr abort_lsn);
7.6.4.17.流准备回调
stream_prepare_cb回调用于准备先前流式传输的事务作为两阶段提交的一部分。
typedef void (*LogicalDecodeStreamPrepareCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr prepare_lsn);
7.6.4.18.流提交回调
stream_commit_cb回调用于提交先前流式传输的事务。
typedef void (*LogicalDecodeStreamCommitCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr commit_lsn);
7.6.4.19.流变更回调
stream_change_cb回调在发送流式传输更改块中的更改时调用(由stream_change_cb和stream_truncate_cb回调函数在流式更改块中调用(由stream_start_cb和stream_stop_cb调用)。实际更改不会显示,因为事务可能在稍后的时间点中止,我们不会解码已中止事务的更改。
typedef void (*LogicalDecodeStreamChangeCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
Relation relation,
ReorderBufferChange *change);
7.6.4.20.流消息回调
在流式更改块中发送通用消息时(由stream_start_cb和stream_stop_cb调用),将调用stream_message_cb回调。事务消息的消息内容不会显示,因为事务可能在稍后的时间点中止,我们不会解码已中止事务的更改。
typedef void (*LogicalDecodeStreamMessageCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
XLogRecPtr message_lsn,
bool transactional,
const char *prefix,
Size message_size,
const char *message);
7.6.4.21.流截断回调
在流式更改块中的TRUNCATE命令中(由stream_start_cb和stream_stop_cb调用),将调用stream_truncate_cb回调。
typedef void (*LogicalDecodeStreamTruncateCB) (struct LogicalDecodingContext *ctx,
ReorderBufferTXN *txn,
int nrelations,
Relation relations[],
ReorderBufferChange *change);
这些参数类似于stream_change_cb回调。但是,由于连接外键的表上的TRUNCATE操作需要一起执行,因此此回调接收关系数组而不仅仅是单个关系。
7.6.5.生成输出的函数
要实际生成输出,输出插件可以在begin_cb、commit_cb或change_cb回调中写入数据到ctx->out的StringInfo输出缓冲区中。在写入输出缓冲区之前,必须调用OutputPluginPrepareWrite(ctx,last_write),在完成写入缓冲区后,必须调用OutputPluginWrite(ctx, last_write)执行写入。 last_write 指示特定写入是否为回调的最后一次写入。
以下示例显示如何向输出插件的消费者输出数据:
OutputPluginPrepareWrite(ctx, true);
appendStringInfo(ctx->out, "BEGIN %u", txn->xid);
OutputPluginWrite(ctx, true);
7.7.逻辑解码输出写入器
可以为逻辑解码添加更多的输出方法。详情请参见src/backend/replication/logical/logicalfuncs.c。基本上需要提供三个函数:一个用于读取WAL,一个用于准备写入输出,一个用于写入输出(参见生成输出的函数)。
7.8.逻辑解码的同步复制支持
7.8.1.概述
逻辑解码可用于构建与流式复制相同用户界面的同步复制解决方案。为此,必须使用流式复制接口(参见流复制协议接口)来流式传输数据。客户端必须发送Standby status update(F)消息,就像流式复制客户端一样。
注意
通过逻辑解码接收更改的同步副本将在单个数据库范围内工作。由于与此相反, synchronous_standby_names 目前是服务器范围的,这意味着如果有多个数据库处于活动状态,则此技术将无法正常工作。
7.8.2.注意事项
在同步复制设置中,如果事务已独占锁定[user]目录表,则可能会发生死锁。有关用户目录表的信息,请参见功能。这是因为事务的逻辑解码可以锁定目录表以访问它们。为避免这种情况,用户必须避免在目录表上执行独占锁定操作。对[user] 目录表进行独占锁可能会以以下方式发生:
-
发出事务中的显式
LOCK命令,锁定ux_class。 -
在事务中执行
CLUSTER命令,对ux_class进行聚集。 -
在对
ux_class执行LOCK命令后,执行PREPARE TRANSACTION命令,并允许两阶段事务的逻辑解码。 -
在对
ux_trigger执行CLUSTER命令后,执行PREPARE TRANSACTION命令,并允许两阶段事务的逻辑解码。只有当发布的表具有触发器时,才会导致死锁。 -
在事务中执行
TRUNCATE命令,截断[user]目录表。
请注意,这些可能导致死锁的命令不仅适用于上述明确指定的系统目录表,还适用于任何其他[user]目录表。
7.9.逻辑解码的大事务流式传输
基本的输出插件回调(例如begin_cb、change_cb、commit_cb和message_cb)仅在事务实际提交时调用。更改仍从事务日志中解码,但仅在提交时传递给输出插件(如果事务中止,则被丢弃)。
这意味着虽然解码是逐步进行的,可能会溢出到磁盘以控制内存使用,但所有解码的更改都必须在事务最终提交时传输(或更准确地说,在从事务日志中解码提交时)。根据事务的大小和网络带宽,传输时间可能会显着增加应用延迟。
为了减少大事务引起的应用延迟,输出插件可以提供额外的回调以支持正在进行的事务的增量流式传输。有多个必需的流式传输回调(stream_start_cb、stream_stop_cb、stream_abort_cb、stream_commit_cb和stream_change_cb),以及两个可选回调(stream_message_cb和stream_truncate_cb)。此外,如果要支持两阶段命令的流式传输,则必须提供其他回调。(有关详细信息,请参见逻辑解码的两阶段提交支持)。
在流式传输正在进行的事务时,更改(和消息)以stream_start_cb和stream_stop_cb回调所标记的块进行流式传输。一旦传输了所有解码的更改,就可以使用stream_commit_cb回调提交事务(或可能使用stream_abort_cb回调中止事务)。
如果支持两阶段提交,则可以使用stream_prepare_cb回调准备事务,使用commit_prepared_cb回调COMMIT PREPARED或使用rollback_prepared_cb回调中止事务。
一个事务的流式回调调用序列示例如下:
stream_start_cb(...); <-- 开始第一个更改块
stream_change_cb(...);
stream_change_cb(...);
stream_message_cb(...);
stream_change_cb(...);
...
stream_change_cb(...);
stream_stop_cb(...); <-- 结束第一个更改块
stream_start_cb(...); <-- 开始第二个更改块
stream_change_cb(...);
stream_change_cb(...);
stream_change_cb(...);
...
stream_message_cb(...);
stream_change_cb(...);
stream_stop_cb(...); <-- 结束第二个更改块
[a. when using normal commit]
stream_commit_cb(...); <-- 提交流式事务
[b. when using two-phase commit]
stream_prepare_cb(...); <-- 准备流式事务
commit_prepared_cb(...); <-- 提交准备好的事务
当然,实际的回调调用序列可能更加复杂。可能会有多个流式事务的块,一些事务可能会被中止等。
与溢出到磁盘行为类似,当从WAL中解码的更改总量(对于所有正在进行的事务)超过logical_decoding_work_mem设置定义的限制时,就会触发流式传输。此时,选择最大的顶级事务(通过当前用于解码更改的内存量来衡量)并进行流式传输。但是,在某些情况下,即使启用了流式传输,我们仍然必须溢出到磁盘,因为我们超过了内存阈值,但仍然没有解码完整的元组,例如,仅解码了toast表插入而没有主表插入。
即使在流式传输大型事务时,更改仍按提交顺序应用,保留与非流式模式相同的保证。
7.10.逻辑解码的两阶段提交支持
使用基本的输出插件回调(例如,begin_cb、change_cb、commit_cb 和message_cb),两阶段提交命令(如 PREPARE TRANSACTION、COMMIT PREPARED 和 ROLLBACK PREPARED)不会被解码。虽然会忽略 PREPARE TRANSACTION,但COMMIT PREPARED 会被解码为 COMMIT,ROLLBACK PREPARED 会被解码为ROLLBACK。
为了支持两阶段命令的流式传输,输出插件需要提供额外的回调。需要多个两阶段提交回调(begin_prepare_cb、prepare_cb、commit_prepared_cb、rollback_prepared_cb和 stream_prepare_cb),以及一个可选的回调(filter_prepare_cb)。
如果提供了用于解码两阶段提交命令的输出插件回调,则在 PREPARE TRANSACTION上,该事务的更改会被解码,传递给输出插件,并调用prepare_cb 回调。这与基本的解码设置不同,基本的解码设置只在提交事务时将更改传递给输出插件。准备事务的开始由begin_prepare_cb 回调表示。
当使用 ROLLBACK PREPARED 回滚准备好的事务时,会调用 rollback_prepared_cb 回调,当使用COMMIT PREPARED 提交准备好的事务时,会调用 commit_prepared_cb 回调。
可选地,输出插件可以通过 filter_prepare_cb定义过滤规则,以仅解码特定的事务。分为两个阶段。这可以通过对 gid 进行模式匹配或通过使用 xid 进行查找来实现。
想要解码准备事务的用户需要注意以下几点:
-
如果准备事务已经独占锁定了[user]目录表,则解码准备可能会一直阻塞,直到主事务提交。
-
使用此功能构建分布式两阶段提交的逻辑复制解决方案可能会死锁,如果准备事务已经独占锁定了[user]目录表。为了避免这种情况,用户必须避免在这种事务中锁定目录表(例如显式的
LOCK命令)。有关详细信息,请参见注意事项。
8.复制进度跟踪
复制源旨在使在逻辑解码之上实现逻辑复制解决方案更加容易。它们提供了解决两个常见问题的方法:
-
如何安全地跟踪复制进度
-
如何根据行的来源更改复制行为;例如,防止双向复制设置中的循环
复制源只有两个属性,名称和ID。名称是应该用于跨系统引用源的内容,是自由格式的文本。它应该以一种使不同复制解决方案创建的复制源之间的冲突不太可能的方式使用;例如,通过在其前缀中添加复制解决方案的名称。ID仅用于避免在空间效率重要的情况下存储长版本。它永远不应该在系统之间共享。
可以使用函数ux_replication_origin_create()创建复制源;使用ux_replication_origin_drop()删除复制源;并在ux_replication_origin系统目录中查看复制源。
构建复制解决方案的一个不平凡的部分是以安全的方式跟踪重放进度。当应用进程或整个集群死亡时,需要能够找到成功复制数据的位置。这样的天真解决方案,例如为每个重放的事务更新表中的一行,存在运行时开销和数据库膨胀等问题。
使用复制源基础设施,可以将会话标记为从远程节点重放(使用ux_replication_origin_session_setup()函数可以用来设置复制源事务的LSN和提交时间戳,而且可以在每个事务基础上进行配置。如果这样做,复制进度将以崩溃安全的方式持久化。可以在ux_replication_origin_status视图中查看所有复制源的重放进度。可以使用ux_replication_origin_progress()获取单个源的进度,例如在恢复复制时,或者使用ux_replication_origin_session_progress()获取当前会话中配置的源的进度。
在比从一个系统复制到另一个系统更复杂的复制拓扑中,另一个问题是很难避免再次复制重放的行。这可能导致复制中的循环和低效。复制源提供了一个可选机制来识别和防止这种情况。当使用前面段落中引用的函数进行配置时,由会话生成的每个更改和事务传递到输出插件回调(参见逻辑解码输出插件)都会带有生成会话的复制源标记。这允许在输出插件中以不同的方式处理它们,例如忽略除本地源行之外的所有行。此外,可以使用filter_by_origin_cb回调基于源过滤逻辑解码更改流。虽然不太灵活,但通过该回调进行过滤比在输出插件中进行过滤要高效得多。