SQL语言

1.语法

本章描述了SQL的语法。它是理解以下章节的基础,这些章节将详细介绍如何使用SQL命令来定义和修改数据。

我们还建议已经熟悉SQL的用户仔细阅读本章,因为它包含了一些在SQL数据库中实现不一致或特定于UXDB的规则和概念。

1.1.词法结构

SQL输入由一系列命令组成。命令由一系列标记组成,以分号(“;”)结尾。输入流的末尾也终止命令。哪些标记有效取决于特定命令的语法。

标记可以是关键字、标识符、带引号的标识符、文字(或常量)或特殊字符符号。标记通常由空格(空格、制表符、换行符)分隔,但如果没有歧义则不必如此(通常只有特殊字符与其他标记类型相邻时才会出现这种情况)。

例如,以下是(语法上)有效的SQL输入:

SELECT * FROM MY_TABLE;
UPDATE MY_TABLE SET A = 5;
INSERT INTO MY_TABLE VALUES (3, 'hi there');

这是三个命令的序列,每行一个(尽管这不是必需的;一行上可以有多个命令,并且命令可以有效地跨行分割)。

此外,注释可以出现在SQL输入中。它们不是标记,它们实际上相当于空白。

关于哪些标记标识命令以及哪些是操作数或参数,SQL语法不太一致。前几个标记通常是命令名称,因此在上面的示例中,我们通常会提到SELECTUPDATEINSERT命令。但例如,UPDATE命令总是需要一个SET标记出现在某个位置,并且这个特定的变体INSERT也需要一个VALUES才能完成。

1.1.1.标识符和关键词

上例中 的SELECTUPDATE、或等标记是关键字的示例,即在SQL语言中具有固定含义的单词。标记和是标识符的示例。它们根据所使用的命令来标识表、列或其他数据库对象的名称。因此它们有时简称为“名称”。关键字和标识符具有相同的词汇结构,这意味着如果不了解语言,就无法知道标记是标识符还是关键字。完整的关键词列表可以在SQL关键字中找到。

SQL标识符和关键字必须以字母(a- z,也可以是带变音符号的字母和非拉丁字母)或下划线 ( _)开头。标识符或关键字中的后续字符可以是字母、下划线、数字 (0- 9)或美元符号($)。请注意,根据SQL标准的字母,标识符中不允许使用美元符号,因此它们的使用可能会降低应用程序的可移植性。SQL标准不会定义包含数字或以下划线开头或结尾的关键字,因此这种形式的标识符可以安全地防止与标准的未来扩展可能发生的冲突。

系统使用不超过NAMEDATALEN-1字节的标识符;可以在命令中写入更长的名称,但它们会被截断。默认情况下,NAMEDATALEN为64,因此最大标识符长度为63字节。在uxdb oracle运行模式下,一个标识符的长度不能超过NAMEDATALEN-2字节,因此标识符的长度上限为62字节。NAMEDATALEN如果这个限制有问题,可以通过更改中的常数来提高它src/include/ux_config_manual.h

关键字和未加引号的标识符不区分大小写。所以:

UPDATE MY_TABLE SET A = 5;

可以等效地写为:

UPDATE my_TabLE SeT a = 5;

经常使用的约定是用大写写关键字,用小写写名称,例如:

UPDATE my_table SET a = 5;

还有第二种标识符:分隔标识符或带引号的标识符。它是由双引号(")或反单引号(`)包围的一个任意字符序列,反单引号与双引号同义;函数sys.quote_identifier('string')也可以生成标识符,相当于反单引号。分隔标识符始终是标识符,而不是关键字。因此"select"可用于引用名为“select”的列或表,而未加引号的select将被视为关键字,因此在需要表名或列名的地方使用时会引发解析错误。该示例可以使用带引号的标识符编写,如下所示:

UPDATE "my_table" SET "a" = 5;

带引号的标识符可以包含任何字符,但代码为零的字符除外。(要包含双引号,请编写两个双引号。)这允许构造原本不可能的表名或列名,例如包含空格或与符号的表名或列名。长度限制仍然适用。

引用标识符也使其区分大小写,而未引用的名称始终折叠为小写。例如,标识符FOOfoo、和被UXDB"foo"认为是相同的,根据标准,foo应等同于“FOO”而非“foo”。(在UXDB中将未加引号的名称折叠为小写与SQL 标准不兼容,该标准规定未加引号的名称应折叠为大写。因此,应该相当于不符合标准。如果您想编写可移植的应用程序,您可以建议始终引用特定名称或从不引用它。)

带引号的标识符的变体允许包含由其代码点标识的转义Unicode字符。此变体U&在左双引号之前以(大写或小写 U 后跟 &符号)开头,中间没有任何空格,例如U&"foo"。(请注意,这会导致运算符产生歧义&。在运算符周围使用空格可以避免此问题。)在引号内,可以通过编写反斜杠后跟四位十​​六进制代码点数字或其他方式以转义形式指定Unicode 字符反斜杠后跟一个加号,后跟六位十六进制代码点编号。例如,标识符"data"可以写为

U&"d\0061t\+000061"

下面这个不太简单的例子用西里尔字母 写出俄语单词“slon” (大象):

U&"\0441\043B\043E\043D"

如果需要与反斜杠不同的转义字符,可以使用UESCAPE字符串后面的子句,例如:

U&"d!0061t!+000061" UECAPE '!'

转义字符可以是除十六进制数字、加号、单引号、双引号或空白字符之外的任何单个字符。请注意,转义字符写在单引号中,而不是双引号中UESCAPE

要按字面意思将转义字符包含在标识符中,请写入两次。

4位或6位转义形式可用于指定UTF-16代理项对来组成代码点大于U+FFFF的字符,尽管6位形式的可用性在技术上使这种做法变得不必要。(代理项对不直接存储,而是组合成单个代码点。)

如果服务器编码不是UTF-8,则将这些转义序列之一标识的Unicode代码点转换为实际的服务器编码;如果不可能,则会报告错误。

1.1.2.常量

UXDB中存在三种隐式类型常量:字符串、位字符串和数字。常量还可以用显式类型指定,这可以使系统更准确地表示和更有效地处理。这些替代方案将在以下小节中讨论。

1.1.2.1.字符串常量

SQL 中的字符串常量是由单引号 () 括起来的任意字符序列',例如'This is a string'。要在字符串常量中包含单引号字符,请写入两个相邻的单引号,例'Dianne''s horse'。请注意,这与双引号字符(")不同。

仅由空格分隔且至少有一个换行符的两个字符串常量被连接起来并被有效地处理,就好像该字符串已被写入为一个常量一样。例如:

SELECT 'foo'
'bar';

相当于:

SELECT 'foobar';

但:

SELECT 'foo'      'bar';

不是有效的语法。(这种有点奇怪的行为是由SQL指定的;UXDB遵循该标准。)

1.1.2.2.具有 C 风格转义的字符串常量

UXDB还接受“转义”字符串常量,这是SQL标准的扩展。通过在左单引号之前写入字母E(大写或小写)来指定转义字符串常量,例如E'foo'。(跨行继续转义字符串常量时,E仅在第一个左引号之前写入。)在转义字符串中,反斜杠字符(\) 开始类似 C的反斜杠转义序列,其中反斜杠和后续字符的组合代表一个特殊的字节值,如下表所示。

表 反斜杠转义序列

反斜杠转义序列解释
\b退格键 (Backspace)
\f换页 (Form feed)
\n换行 (New line)
\r回车 (Return)
\t制表符 (Tab)
\o, \oo, \ooo八进制字节值 ($o = 0-7$)
\xh, \xhh十六进制字节值 ($h = 0-9, A-F$)
\uxxxx, \UxxxxxxxxUnicode 字符值(16 或 32 位十六进制,$x = 0-9, A-F$)

反斜杠后面的任何其他字符均按字面意思理解。因此,要包含反斜杠字符,请写入两个反斜杠 ( \\)。\'此外,除了的正常方式之外,还可以通过编写 来将单引号包含在转义字符串中 ''

您有责任确保您创建的字节序列(尤其是在使用八进制或十六进制转义符时)组成服务器字符集编码中的有效字符。一个有用的替代方法是使用Unicode转义或替代的Unicode转义语法,如带有 Unicode 转义的字符串常量 中所述;然后服务器将检查字符转换是否可行。

警告

如果配置参数standard_conforming_stringsoff,则UXDB会识别常规字符串常量和转义字符串常量中的反斜杠转义。然而,从UXDB 2112开始,默认值为on,这意味着反斜杠转义仅在转义字符串常量中被识别。此行为更符合标准,但可能会破坏依赖于历史行为的应用程序,其中反斜杠转义始终被识别。作为解决方法,您可以将此参数设置为off,但最好不要使用反斜杠转义。如果需要使用反斜杠转义来表示特殊字符,请将字符串常量写为E

除了之外standard_conforming_strings,配置参数escape_string_warningbackslash_quote还控制字符串常量中反斜杠的处理。

代码为零的字符不能出现在字符串常量中。

1.1.2.3.带有 Unicode 转义的字符串常量

UXDB还支持另一种类型的字符串转义语法,允许通过代码点指定任意Unicode字符。Unicode转义字符串常量U&在左引号之前以(大写或小写字母U后跟&符号)开头,中间没有任何空格,例如U&'foo'。(请注意,这会导致运算符产生歧义&。在运算符周围使用空格可以避免此问题。)在引号内,可以通过编写反斜杠后跟四位十​​六进制代码点数字或其他方式以转义形式指定Unicode字符反斜杠后跟一个加号,后跟六位十六进制代码点编号。例如,字符串'data'可以写成

U&'d\0061t\+000061'

下面这个不太简单的例子用西里尔字母 写出俄语单词“slon” (大象):

U&'\0441\043B\043E\043D'

如果需要与反斜杠不同的转义字符,可以使用UESCAPE字符串后面的子句,例如:

U&'d!0061t!+000061' UECAPE '!'

转义字符可以是除十六进制数字、加号、单引号、双引号或空白字符之外的任何单个字符。

要按字面意思将转义字符包含在字符串中,请写入两次。

4位或6位转义形式可用于指定UTF-16代理项对来组成代码点大于U+FFFF的字符,尽管6位形式的可用性在技术上使这种做法变得不必要。(代理项对不直接存储,而是组合成单个代码点。)

如果服务器编码不是UTF-8,则将这些转义序列之一标识的Unicode代码点转换为实际的服务器编码;如果不可能,则会报告错误。

此外,字符串常量的Unicode转义语法仅在配置参数standard_conforming_strings打开时才起作用。这是因为,否则此语法可能会使解析SQL语句的客户端感到困惑,从而导致SQL注入和类似的安全问题。如果该参数设置为off,则该语法将被拒绝并显示错误消息。

1.1.2.4.美元引用的字符串常量

虽然指定字符串常量的标准语法通常很方便,但当所需的字符串包含许多单引号或反斜杠时,可能很难理解,因为每个单引号或反斜杠都必须加倍。为了在这种情况下允许更可读的查询,UXDB提供了另一种方式,称为“美元引用”,来编写字符串常量。美元引用的字符串常量由美元符号 ($) 和可选的“标记”组成零个或多个字符、另一个美元符号、组成字符串内容的任意字符序列、一个美元符号、开始此美元报价的相同标记以及一个美元符号。例如,以下是使用美元引用 指定字符串“ Dianne's horse ”的两种不同方法:

$$Dianne's horse$$
$SomeTag$Dianne's horse$SomeTag$

请注意,在美元引号字符串内,可以使用单引号,而无需转义。事实上,美元引用的字符串中的任何字符都不会被转义:字符串内容始终按字面书写。反斜杠并不特殊,美元符号也不是特殊的,除非它们是与开始标记匹配的序列的一部分。

通过在每个嵌套级别选择不同的标签,可以嵌套美元引用的字符串常量。这最常用于编写函数定义。例如:

$function$
BEGIN
    RETURN ($1 ~ $q$[\t\r\n\v\\]$q$);
END;
$function$

这里,序列$q$[\t\r\n\v\\]$q$代表一个用美元引号括起来的文字字符串[\t\r\n\v\\],当函数体被UXDB执行时,它将被识别。但由于序列与外部美元引用分隔符不匹配$function$,因此就外部字符串而言,它只是常量中的一些字符。

带美元引号的字符串的标记(如果有)遵循与不带引号的标识符相同的规则,但它不能包含美元符号。标签区分大小写,因此$tag$Stringcontent$tag$ 正确,但$TAG$String content$tag$错误。

关键字或标识符后面的美元引号字符串必须用空格与其分隔;否则,美元引用分隔符将被视为前面标识符的一部分。

美元引用不是SQL标准的一部分,但它通常是比符合标准的单引号语法更方便的编写复杂字符串文字的方法。当在其他常量中表示字符串常量时,它特别有用,这在过程函数定义中经常需要。使用单引号语法,上例中的每个反斜杠必须写为四个反斜杠,在解析原始字符串常量时将减少为两个反斜杠,然后在函数期间重新解析内部字符串常量时减少为一个反斜杠执行。

1.1.2.5.Oracle风格字符常量

在字符串中输入一些特殊符号(比如单引号、双引号等)时,需要在每个特殊符号前加一个转义符号。当符号较多时,会导致输入非常不直观。使用q'转义字符语法可以在输入特殊符号时不需要增加转义符号,使长文本、多符号的字符串输入更直观。另外其还支持前缀N,该情况下oracle表示该字符串为国家字符集。

UXDB兼容oracle这种风格的字符串。其词法如下:

IMG

q和Q只有一个,两个单引号、两个定界符字段必须有。

q或Q表示将使用替代转义机制。这种机制允许为文本字符串使用多种定界符(quote_delimiter)。

最外面' '是两个单引号,分别在开口定界符之前和闭合定界符之后。

c是用户字符集的任何成员,可以是引号(“),可以是定界符(没有紧随其后的单引号)。

定界符是除单引号、制表符和返回值之外的任何单字节。

如果开口定界符是[,{,<,或(,那么闭合定界符必须是相应的],},>,或)。在所有其他情况下,开口和闭合定界符必须使用相同的字符。

N和n在UXDB 表示该字符串的类型为nvarchar2。

1.1.2.6.位串常量

B位字符串常量看起来像常规字符串常量,在左引号之前带有(大写或小写,没有中间的空格),例如B'1001'。位串常量中允许使用的唯一字符是01

或者,可以使用前导X(大写或小写)以十六进制表示法指定位串常量,例如X'1FF'。此表示法相当于每个十六进制数字有四个二进制数字的位串常量。

两种形式的位串常量都可以按照与常规字符串常量相同的方式跨行继续。美元引用不能用在位串常量中。

1.1.2.7.数字常量

数字常量以下列一般形式被接受:

digits
digits.[digits][e[+-]digits]
[digits].digits[e[+-]digits]
digitse[+-]digits

其中digits是一个或多个十进制数字(0到9)。如果使用一位的话,小数点之前或之后必须至少有一位数字。指数标记后必须至少有一位数字(e如果存在)。常量中不能嵌入任何空格或其他字符。请注意,任何前导的加号或减号实际上并不被视为常量的一部分;它是应用于常量的运算符。

以下是有效数字常量的一些示例:

42
3.5
4.
.001
5e2
1.925e-3

既不包含小数点也不包含指数的数值常量,integer如果其值适合类型integer(32位),则最初被假定为类型;bigint否则,如果它的值符合类型bigint(64位),则假定它是类型;否则它被认为是type numeric。包含小数点和/或指数的常量最初始终被假定为类型numeric

最初分配的数值常量的数据类型只是类型解析算法的起点。在大多数情况下,常量将根据上下文自动强制为最合适的类型。必要时,您可以通过强制转换将数值强制解释为特定数据类型。例如,您可以通过编写以下内容强制将数值视为类型real(float4):

REAL '1.23'  -- string style
1.23::REAL   -- UXDB (historical) style

这些实际上只是接下来讨论的一般铸造符号的特殊情况。

1.1.2.8.其他类型的常量

可以使用以下任一表示法输入任意类型的常量:

type 'string'
'string'::type
CAST ( 'string' AS type )

字符串常量的文本被传递给名为type的类型的输入转换例程。结果是指示类型的常数。如果对常量的类型没有歧义(例如,当它直接分配给表列时),则可以省略显式类型强制转换,在这种情况下,它是自动强制的。

字符串常量可以使用常规SQL表示法或美元引用来编写。

还可以使用类似函数的语法指定类型强制:

typename ( 'string' )

但并非所有类型名称都可以这样使用;详细信息请参见类型转换

CAST()和函数调用语法也可以用来指定任意表达式的运行时类型转换,如类型转换节所述。为了避免语法歧义,类型“string”语法只能用于指定简单文本常量的类型。类型“string”语法的另一个限制是它不适用于数组类型;使用::或CAST()来指定数组常量的类型。

CAST()语法符合 SQL。type 'string'语法是标准语法的一种推广: SQL只为少数数据类型指定了这种语法,但UXDB允许所有类型都使用这种语法。具有::的语法是UXDB的历史用法,函数调用语法也是如此。

1.1.3.运算符

NAMEDATALEN运算符名称是以下列表中最多 -1 个(默认为 63 个)字符的序列:

+ - * / < > = ~ ! @#%^&| `?  

但是,操作员名称有一些限制:

  • --并且/*不能出现在操作员名称中的任何位置,因为它们将被视为注释的开头。

  • 多字符运算符名称不能以+或结尾-,除非该名称还至少包含以下字符之一:

    ~!@#%^&| `? 
    

    例如,@-是允许的操作员名称,但*-不是。此限制允许 UXDB解析符合 SQL 的查询,而不需要标记之间有空格。

使用非SQL标准运算符名称时,通常需要用空格分隔相邻的运算符以避免歧义。例如,如果您定义了名为 的前缀运算符@,则不能编写X*@Y;您必须编写X* @Y以确保UXDB将其读取为两个运算符名称而不是一个。

1.1.4.特殊字符

某些非字母数字字符具有不同于运算符的特殊含义。有关用法的详细信息可以在描述相应语法元素的位置找到。本节的存在只是为了建议这些字符的存在并总结这些字符的用途。

  • 美元符号 ( $)后跟数字用于表示函数定义或准备语句主体中的位置参数。在其他上下文中,美元符号可以是标识符或美元引用的字符串常量的一部分。

  • 括号 ( ()) 具有对表达式进行分组和强制优先级的通常含义。在某些情况下,需要使用括号作为特定 SQL 命令的固定语法的一部分。

  • 中括号 ( []) 用于选择数组的元素。有关数组的更多信息,请参见数组

  • 逗号 ( ,) 在某些语法结构中用于分隔列表的元素。

  • 分号 ( ;) 终止 SQL 命令。它不能出现在命令中的任何位置,除了字符串常量或带引号的标识符中。

  • 冒号 ( :) 用于从数组中选择 “切片”。(参见数组)在某些SQL方言(例如嵌入式 SQL)中,冒号用于为变量名称添加前缀。

  • 星号 ( *) 在某些上下文中用于表示表行或复合值的所有字段。当用作聚合函数的参数时,它还有特殊的含义,即聚合不需要任何显式参数。

  • 句点 ( .) 用于数字常量,用于分隔架构、表和列名称。

1.1.5.评论

注释是以双破折号开头并延伸到行尾的字符序列,例如:

-- This is a standard SQL comment

或者,可以使用 C 风格的块注释:

/* multiline comment
 * with nesting: /* nested block comment */
 */

其中注释以 开始/*并延伸到匹配的*/. 这些块注释嵌套,如SQL标准中所指定,但与C不同,因此可以注释掉可能包含现有块注释的较大代码块。

在进一步语法分析之前,注释会从输入流中删除,并有效地替换为空格。

1.1.6.运算符优先级

表 运算符优先级(从最高到最低)UXDB中运算符的优先级和结合性。大多数运算符具有相同的优先级并且是左结合的。运算符的优先级和结合性被硬连接到解析器中。如果您希望以优先级规则所暗示的其他方式解析具有多个运算符的表达式,请添加括号。

表 运算符优先级(从最高到最低)

运算符/元素关联性描述
.表/列名称分隔符(最高优先级)
::UXDB 风格的类型转换
[ ]数组元素选择
+ -一元加、一元减
^求幂运算
* / %乘法、除法、取模
+ -加法、减法
(任何其他运算符)所有其他本机和用户定义的运算符
BETWEEN IN LIKE ILIKE SIMILAR-范围包含、集合成员资格、字符串匹配
< > = <= >= <>-比较运算符
IS ISNULL NOTNULL-IS TRUEIS NULLIS DISTINCT FROM
NOT逻辑否定
AND逻辑连接(与)
OR逻辑析取(或,最低优先级)

请注意,运算符优先级规则也适用于与上述内置运算符同名的用户定义运算符。例如,如果您为某些自定义数据类型定义“+”运算符,则无论您做什么,它都将具有与内置“+”运算符相同的优先级。

当语法中使用模式限定的运算符名称时OPERATOR,例如:

SELECT 3 OPERATOR(ux_catalog.+) 4;

该结构被认为具有表 运算符优先级(从最高到最低)“任何其他运算符”OPERATOR的默认优先级。无论里面出现哪个具体操作符都是如此。

注意

之前的UXDB版本使用的运算符优先级规则略有不同。特别是,<=``>=and<>曾经被视为通用运算符;IS测试曾经具有更高的优先级;andNOTBETWEEN和相关结构的行为不一致,在某些情况下被视为具有而NOT 不是 的优先级BETWEEN。这些规则进行了更改,以便更好地符合SQL标准,并减少由于对逻辑等效结构的不一致处理而造成的混乱。在大多数情况下,这些变化不会导致行为变化,或者可能导致“没有这样的操作员”可以通过添加括号来解决的故障。但是,在某些极端情况下,查询可能会更改行为而不报告任何解析错误。

1.2.值表达式

值表达式在各种上下文中使用,例如在 SELECT 命令的目标列表中,作为 INSERTUPDATE中的新列值,或在许多命令的搜索条件中。值表达式的结果有时被称为标量,以区别于表达式的结果(即表)。因此,值表达式也被称为标量表达式(或简称“标量”)。甚至简单的表达式。表达式语法允许使用算术、逻辑、集合和其他操作从基本部分计算值。值表达式可以是以下之一: - 常量或文字值 - 列引用 - 位置参数引用,在函数定义或预处理语句的主体中 - 带下标的表达式 - 字段选择表达式- 运算符调用 - 函数调用 - 聚合表达式 - 窗口函数调用 - 类型转换 - 排序表达式 - 标量子查询 - 数组构造器 - 行构造器 -括号中的另一个值表达式(用于分组子表达式和覆盖优先级)除了这个列表,还有许多可以归类为表达式但不遵循任何一般语法规则的结构。一个例子是IS NULL子句。我们已经在上述章节中讨论了常量。以下各节讨论了其余选项。

1.2.1.列引用

列可以以以下形式引用:

correlation.columnname 

correlation是一个表的名称(可能带有模式名称的限定符),或者是通过FROM子句定义的表的别名。如果在当前查询中使用的所有表中列名唯一,则可以省略关联名称和分隔符点。

1.2.2.位置参数

位置参数引用用于指示外部提供给SQL语句的值。参数用于SQL函数定义和准备查询。一些客户端库还支持从SQL命令字符串分别指定数据值的方式,在这种情况下,参数用于引用线外数据值。参数引用的形式为:

$number 

例如,考虑一个函数dept的定义,如下所示:

CREATE FUNCTION dept(text) RETURNS dept
    AS $$ SELECT * FROM dept WHERE name = $1 $$
    LANGUAGE SQL;

这里的$1引用每次调用函数时第一个函数参数的值。

1.2.3.下标

如果一个表达式产生了一个数组类型的值,那么可以通过写

expression[subscript]

来提取数组值的特定元素,或者通过写

expression[lower_subscript:upper_subscript]

来提取多个相邻的元素(一个“数组切片”)。(这里,方括号[ ]是字面意义上的。)每个subscript本身都是一个表达式,它将四舍五入为最接近的整数值。

一般来说,数组expression必须加上括号,但当要被下标的表达式只是一个列引用或位置参数时,可以省略括号。此外,当原始数组是多维的时,可以连接多个下标。例如:

mytable.arraycolumn[4]
mytable.two_d_column[17][34]
$1[10:42]
(arrayfunction(a,b))[42]

最后一个例子中的括号是必需的。有关数组的更多信息,请参见数组

1.2.4.字段选择

如果表达式生成复合类型(行类型)的值,则可以通过写入提取该行的特定字段:

expression.fieldname

一般来说,行expression必须加上括号,但当要从中选择的表达式只是一个表引用或位置参数时,可以省略括号。例如:

mytable.mycolumn
$1.somecolumn
(rowfunction(a,b)).col3

(因此,限定的列引用实际上只是字段选择语法的一个特殊情况。)一个重要的特殊情况是从一个复合类型的表列中提取字段:

(compositecol).somefield
(mytable.compositecol).somefield

这里需要括号来显示compositecol是一个列名而不是表名,在第二种情况中mytable是一个表名而不是模式名。

您可以通过写.*来请求复合值的所有字段:

(compositecol).*

这种表示法在不同的上下文中的行为不同;有关详细信息,请参见在查询中使用复合类型

1.2.5.运算符调用

运算符调用有两种可能的语法:

expression operator expression (binary infix operator)
operator expression (unary prefix operator)

operator 标记遵循运算符的语法规则,或者是关键字ANDORNOT中的一个,或者是形式为以下内容的限定操作符名称:

oPERATOR(schema.operatorname)

存在哪些特定的操作符以及它们是一元的还是二元的,取决于系统或用户定义了哪些操作符。

1.2.6.函数调用

函数调用的语法是函数名称(可能带有模式名称),后跟括号括起来的参数列表:

function_name ([expression [, expression ... ]] )

例如,以下计算2的平方根:

Sqrt(2)

其他函数可以由用户添加。

在查询数据库时,如果某些用户不信任其他用户,请遵循安全预防措施编写函数调用。

参数可以选择附加名称。有关详细信息,请参见调用函数

注意

一个接受复合类型单个参数的函数可以选择使用字段选择语法进行调用,反之亦然。也就是说,符号col(table)table.col是可互换的。这种行为不是SQL标准,但在UXDB中提供,因为它允许使用函数来模拟“计算字段”。有关更多信息,请参见在查询中使用复合类型

1.2.7.聚合表达式

聚合表达式表示在查询所选行中应用聚合函数。聚合函数将多个输入缩减为单个输出值,例如输入的总和或平均值。聚合表达式的语法如下:

aggregate_name (expression [ , ... ] [ order_by_clause ] ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name (ALL expression [ , ... ] [ order_by_clause ] ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name (DISTINCT expression [ , ... ] [ order_by_clause ] ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name ( * ) [ FILTER ( WHERE filter_clause ) ]
aggregate_name ( [ expression [ , ... ] ] ) WITHIN GROUP ( order_by_clause ) [ FILTER ( WHERE filter_clause ) ]

其中aggregate_name是先前定义的聚合(可能带有模式名称),expression是任何不包含聚合表达式或窗口函数调用的值表达式。可选的order_by_clausefilter_clause如下所述。

聚合表达式的第一种形式为每个输入行调用一次聚合。第二种形式与第一种相同,因为ALL是默认值。第三种形式为每个表达式(或多个表达式的不同值集)在输入行中找到的不同值集调用一次聚合。第四种形式为每个输入行调用一次聚合;由于未指定特定的输入值,因此通常仅对count(*)聚合函数有用。最后一种形式用于有序集聚合函数,下面将对其进行描述。大多数聚合函数忽略空输入,因此其中一个或多个表达式产生null的行将被丢弃。除非另有说明,否则可以认为这对所有内置聚合都是正确的。

例如,count(*)产生输入行的总数;count(f1)产生f1非空的输入行数,因为count忽略null;count(distinct f1)产生f1的不同非空值的数量。

通常,输入行以未指定的顺序提供给聚合函数。在许多情况下,这并不重要;例如,min无论以何种顺序接收输入都会产生相同的结果。但是,某些聚合函数(例如array_agg和string_agg)产生依赖于输入行的排序的结果。使用这样的聚合时,可以使用可选的order_by_clause来指定所需的排序。order_by_clause具有与查询级别ORDER BY子句相同的语法,除了其表达式始终只是表达式,不能是输出列名称或数字。例如:

SELECT array_agg(a ORDER BY b DESC) FROM table;

处理多参数聚合函数时,请注意ORDER BY子句在所有聚合参数之后。例如,写成这样:

SELECT string_agg(a ORDER BY a, ',') FROM table;  -- incorrect

后者在语法上是有效的,但它表示调用一个带有两个ORDER BY键的单参数聚合函数(第二个键是相当无用的,因为它是一个常量)。

如果除了 order_by_clause 之外还指定了DISTINCT,则所有ORDER BY表达式都必须与聚合的常规参数匹配;也就是说,不能按未包含在DISTINCT列表中的表达式排序。

注意

在聚合函数中同时指定DISTINCTORDER BY的能力是UXDB的扩展。

按照迄今为止所描述的方式,在聚合的常规参数列表中放置 ORDER BY用于对输入行进行排序,这适用于可选排序的通用和统计聚合。有一类聚合函数称为有序集聚合,其中order_by_clause是必需的,通常是因为聚合的计算仅在其输入行的特定排序方案下才有意义。有序集聚合的典型示例包括排名和百分位数计算。对于有序集聚合,order_by_clause写在WITHIN GROUP (...) 中,如上面的最终语法替代方案所示。在 order_by_clause 中的表达式与常规聚合参数一样,每个输入行只计算一次,按照order_by_clause的要求排序,并作为输入参数提供给聚合函数。(这与非 WITHIN GROUPorder_by_clause不同,后者不被视为聚合函数的参数。)在WITHIN GROUP之前的参数表达式(如果有)称为直接参数,以区别于在 order_by_clause 中列出的聚合参数。与常规聚合参数不同,直接参数仅在每个聚合调用时计算一次,而不是每个输入行计算一次。这意味着,如果这些变量是按GROUP BY分组的,则它们可以包含变量;这个限制与直接参数不在聚合表达式中的情况相同。直接参数通常用于像百分位数分数这样的东西,每个聚合计算只有一个意义。直接参数列表可以为空;在这种情况下,只需写() 而不是 (*)。(UXDB 实际上接受任何一种拼写方式,但只有第一种方式符合SQL标准。)

有序集合聚合调用的一个例子是:

SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY income) FROM households;
 percentile_cont
-----------------
           50489

它从表households获取income列的第50个百分位数或中位数值。这里,0.5是一个直接参数;对于百分位数分数变化的值在行之间变化是没有意义的。

如果指定了FILTER,则只有FILTER_子句计算结果为true的输入行被提供给聚合函数;其他行被丢弃。例如:

SELECT
    count(*) AS unfiltered,
    count(*) FILTER (WHERE i < 5) AS filtered
FROM generate_series(1,10) AS s(i);
 unfiltered | filtered
------------+----------
         10 |        4
(1 row)

用户可以添加其他聚合函数。

聚合表达式只能出现在SELECT命令的结果列表或HAVING子句中。在其他子句中,如WHERE,是禁止的,因为这些子句在聚合结果形成之前逻辑上进行评估。

当聚合表达式出现在子查询中时,通常会在子查询的行上评估聚合。但是,如果聚合的参数(和任何filter_clause)仅包含外部级别变量,则会发生异常:聚合函数属于最近的外部级别,并在该查询的行上进行评估。整个聚合表达式对于它出现在的子查询是一个外部引用,并且在该子查询的任何一次评估中都充当常量。关于仅出现在结果列表或HAVING子句中的限制适用于聚合所属的查询级别。

1.2.8.窗口函数调用

窗口函数调用表示类聚合函数在查询所选行的某些部分上的应用程序。与非窗口聚合调用不同,这与将选定的行分组为单个输出行没有关系-每个行在查询输出中保持独立。但是,窗口函数可以根据窗口函数调用的分组规范(PARTITION BY列表)访问属于当前行组的所有行。窗口函数调用的语法如下:

function_name ([expression [, expression ... ]]) [ FILTER ( WHERE filter_clause ) ] OVER window_name
function_name ([expression [, expression ... ]]) [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition )
function_name ( * ) [ FILTER ( WHERE filter_clause ) ] OVER window_name
function_name ( * ) [ FILTER ( WHERE filter_clause ) ] OVER ( window_definition )

window_definition 语法如下所示。

[ existing_window_name ]
[ PARTITION BY expression [, ...] ]
[ ORDER BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ]
[ frame_clause ]

可选的 frame_clause 可以是以下之一:

{ RANGE | ROWS | GROUPS } frame_start [ frame_exclusion ]
{ RANGE | ROWS | GROUPS } BETWEEN frame_start AND frame_end [ frame_exclusion ]

其中 frame_startframe_end 可以是以下之一:

uNBOUNDED PRECEDING
offset PRECEDING
CURRENT ROW
offset FOLLOWING
UNBOUNDED FOLLOWING

frame_exclusion 可以是以下之一:

eXCLUDE CURRENT ROW
EXCLUDE GROUP
EXCLUDE TIES
EXCLUDE NO OTHERS

这里,expression表示任何不包含窗口函数调用的值表达式。

window_name 是对查询的WINDOW子句中定义的命名窗口规范的引用。或者,可以在括号中给出完整的window_definition,使用与在 WINDOW 子句中定义命名窗口相同的语法。值得指出的是,OVER wname不完全等同于OVER (wname ...);后者意味着复制和修改窗口定义,并且如果引用的窗口规范包括帧子句,则会被拒绝。PARTITION BY子句将查询的行分组为分区,窗口函数单独处理每个分区。

PARTITION BY的工作方式类似于查询级别的GROUP BY 子句,但其表达式始终只是表达式,不能是输出列名称或编号。如果没有PARTITION BY,则查询生成的所有行都将视为单个分区。ORDER BY子句确定窗口函数处理分区行的顺序。它的工作方式类似于查询级别的 ORDER BY子句,但同样不能使用输出列名称或编号。如果没有 ORDER BY,则行将以未指定的顺序处理。

frame_clause指定构成窗口帧的行集,对于那些作用于帧而不是整个分区的窗口函数。帧中的行集可以因当前行是哪一行而变化。帧可以在RANGEROWSGROUPS 模式下指定;在每种情况下,它从 frame_startframe_end 运行。如果省略 frame_end,则默认为 CURRENT ROW

UNBOUNDED PRECEDINGframe_start 表示该框架以分区的第一行开始,同样,UNBOUNDED FOLLOWINGframe_end表示该框架以分区的最后一行结束。

RANGEGROUPS模式下,CURRENT ROWframe_start表示该框架从当前行的第一个peer行开始(窗口的ORDER BY子句将其排序为与当前行等效),而CURRENT ROWframe_end表示该框架以当前行的最后一个peer行结束。在ROWS模式下,CURRENT ROW表示当前行。

offset PRECEDINGoffset FOLLOWING框架选项中,offset必须是不包含任何变量、聚合函数或窗口函数的表达式。偏移量的含义取决于框架模式:

  • ROWS模式下,offset必须产生一个非空的、非负的整数,该选项表示框架在当前行之前或之后指定的行数开始或结束。

  • GROUPS模式下,offset再次必须产生一个非空的、非负的整数,该选项表示框架在当前行的对等组之前或之后指定的peer groups数开始或结束,其中对等组是按ORDER BY排序的一组等效行。(窗口定义中必须有一个ORDER BY子句才能使用GROUPS模式。)

  • RANGE模式下,这些选项要求ORDER BY子句指定恰好一列。偏移量指定当前行中该列的值与框架中前面或后面行中该列的值之间的最大差异。偏移量表达式的数据类型取决于排序列的数据类型。对于数字排序列,它通常与排序列的类型相同,但对于日期时间排序列,它是一个interval。例如,如果排序列的类型是datetimestamp,可以写成RANGE BETWEEN '1 day' PRECEDING AND '10 days' FOLLOWING。偏移量仍然需要是非空的和非负的,尽管“非负的”的含义取决于它的数据类型。

无论如何,到框架结束的距离都受到分区结束距离的限制,因此在分区结束附近的行中,框架可能包含的行数比其他地方少。

请注意,在ROWSGROUPS模式下,0 PRECEDING0 FOLLOWING等效于CURRENT ROW。对于适当的排序列,这通常也适用于RANGE模式。“零”的数据类型特定含义。

frame_exclusion选项允许从帧中排除当前行周围的行,即使根据帧起始和帧结束选项它们将被包括在内。EXCLUDE CURRENT ROW将当前行从帧中排除。EXCLUDE GROUP将当前行及其排序对等项从帧中排除。EXCLUDE TIES将当前行的任何对等项从帧中排除,但不包括当前行本身。EXCLUDE NO OTHERS仅明确指定不排除当前行或其对等项的默认行为。

默认的帧选项是RANGE UNBOUNDED PRECEDING,它与RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW相同。使用ORDER BY,这将将帧设置为从分区开始到当前行的最后一个ORDER BY对等项的所有行。如果没有ORDER BY,则这意味着分区的所有行都包含在窗口帧中,因为所有行都成为当前行的对等项。

限制是frame_start不能是UNBOUNDED FOLLOWINGframe_end不能是UNBOUNDED PRECEDING,并且frame_end选择不能出现在上面的frame_startframe_end选项列表中早于frame_start选择- 例如RANGE BETWEEN CURRENT ROW AND offset PRECEDING不允许。但是,例如,ROWS BETWEEN 7 PRECEDING AND 8 PRECEDING是允许的,即使它永远不会选择任何行。

如果指定了FILTER,则仅将评估filter_clause为true的输入行馈送到窗口函数;其他行将被丢弃。仅聚合的窗口函数接受FILTER子句。

用户可以添加其他窗口函数。此外,任何内置或用户定义的通用或统计聚合都可以用作窗口函数。(有序集和假设集聚合目前不能用作窗口函数。)

使用*的语法用于调用无参数聚合函数作为窗口函数,例如count(*) OVER (PARTITION BY x ORDER BY y)。星号(*)通常不用于特定于窗口的函数。窗口特定函数不允许在函数参数列表中使用DISTINCTORDER BY

仅允许在查询的SELECT列表和ORDER BY子句中使用窗口函数调用。

1.2.9.类型转换

类型转换指从一种数据类型到另一种数据类型的转换。UXDB接受两种等效的语法进行类型转换:

cAST ( expression AS type )
expression::type

CAST 语法符合 SQL 标准;带有::的语法是之前UXDB的用法。

当将类型转换应用于已知类型的值表达式时,它表示运行时类型转换。只有在定义了适当的类型转换操作时,转换才会成功。请注意,这与在常量中使用转换的用法略有不同,如其他类型的常量所示。应用于未装饰的字符串字面量的转换表示将类型分配给文字常量值的初始赋值,因此它将成功地为任何类型执行转换(如果字符串字面量的内容是数据类型的可接受输入语法)。

如果对于值表达式必须产生的类型没有歧义,通常可以省略显式类型转换(例如,当它被分配给表列时);在这种情况下,系统将自动应用类型转换。但是,仅对标记为“可以隐式应用”的转换执行自动转换,这些转换在系统目录中标记为“可以隐式应用”。其他转换必须使用显式转换语法调用。这个限制旨在防止意外的转换被静默地应用。

还可以使用类似函数的语法指定类型转换:

typename ( expression )

但是,这仅适用于名称也是有效函数名称的类型。例如,不能使用 double precision,但可以使用等效的float8。此外,只有在双引号中使用名称 intervaltimetimestamp,因为存在语法冲突,才能以这种方式使用它们。因此,使用函数式转换语法会导致不一致性,应该避免使用。

注意

函数式语法实际上只是一个函数调用。当使用两种标准转换语法之一进行运行时转换时,它将内部调用已注册的函数执行转换。按照惯例,这些转换函数与其输出类型具有相同的名称,因此“函数式语法” 实际上只是底层转换函数的直接调用。显然,这不是可移植应用程序应该依赖的内容。

在兼容mysql数据库模式下,使用CAST(expression AS type)类型造型语法时,当type为date,time,decimal,char,nchar,binary六种类型中一种类型时,所进行的类型转换操作及得到的结果表现与mysql相同。

示例

SELECT CAST('9912312359' AS DATE);
    date    
------------
 12-31-1999
(1 row)

SELECT CAST('99991231235959' AS TIME);
   time   
----------
 23:59:59
(1 row)

1.2.10.排序表达式

COLLATE 子句覆盖表达式的排序。它附加到它应用的表达式上:

expr COLLATE collation

其中,collation 是一个可能带有模式限定符的标识符。COLLATE子句的绑定优先级高于运算符;必要时可以使用括号。

如果没有明确指定排序规则,则数据库系统会从表达式中涉及的列中派生排序规则,或者如果表达式中没有涉及列,则默认为数据库的默认排序规则。

COLLATE子句的两个常见用途是覆盖ORDER BY子句中的排序顺序,例如:

SELECT a, b, c FROM tbl WHERE ... ORDER BY a COLLATE "C";

以及覆盖具有区域设置敏感结果的函数或运算符调用的排序规则,例如:

SELECT * FROM tbl WHERE a > 'foo' COLLATE "C";

请注意,在后一种情况下,COLLATE子句附加到我们希望影响的运算符的输入参数上。无论将COLLATE子句附加到运算符或函数调用的哪个参数上,都没有关系,因为运算符或函数应用的排序规则是通过考虑所有参数来派生的,并且显式的COLLATE子句将覆盖所有其他参数的排序规则。(然而,将不匹配的COLLATE子句附加到多个参数是错误的。)因此,这与前面的示例给出了相同的结果:

SELECT * FROM tbl WHERE a COLLATE "C" > 'foo';

但是这是一个错误:

SELECT * FROM tbl WHERE (a > 'foo') COLLATE "C";

因为它试图将排序规则应用于>运算符的结果,而该结果是不可排序的数据类型boolean。

1.2.11.标量子查询

标量子查询是带有括号的普通SELECT查询,返回一个具有一个列的行。执行SELECT查询,并将返回的单个值用于周围的值表达式。将返回多行或多列的查询用作标量子查询是错误的。(但是,如果在特定执行期间,子查询不返回行,则没有错误;标量结果被视为null。)子查询可以引用来自周围查询的变量,在子查询的任何一次评估期间,它们将充当常量。

例如,以下查询查找每个省人口最多的城市:

SELECT name, (SELECT max(pop) FROM cities WHERE cities.state = states.name)
FROM states;

1.2.12.数组构造函数

数组构造函数是使用成员元素的值构建数组值的表达式。简单的数组构造函数由关键字ARRAY、左方括号和一个逗号分隔的元素列表组成。元素列表可以是任何表达式,包括其他数组构造函数。例如:

SELECT ARRAY[1,2,3+4];
  array
---------
 {1,2,7}
(1 row)

默认情况下,数组元素类型是成员表达式的公共类型,使用与UNIONCASE结构相同的规则确定(参见UNION,CASE和相关结构)。您可以通过将数组构造函数显式转换为所需类型来覆盖此行为,例如:

SELECT ARRAY[1,2,22.7]::integer[];
  array
----------
 {1,2,23}
(1 row)

这与将每个表达式单独转换为数组元素类型具有相同的效果。有关转换的更多信息,请参见类型转换

多维数组值可以通过嵌套数组构造函数来构建。在内部构造函数中,关键字ARRAY可以省略。例如,以下产生相同的结果:

SELECT ARRAY[ARRAY[1,2], ARRAY[3,4]];
     array
---------------
 {{1,2},{3,4}}
(1 row)

SELECT ARRAY[[1,2],[3,4]];
     array
---------------
 {{1,2},{3,4}}
(1 row)

由于多维数组必须是矩形的,因此在同一级别的内部构造函数必须产生具有相同维度的子数组。应用于外部ARRAY构造函数的任何转换都会自动传播到所有内部构造函数。

多维数组构造函数元素可以是任何产生正确类型的数组,而不仅仅是子ARRAY构造。例如:

CREATE TABLE arr(f1 int[], f2 int[]);

INSERT INTO arr VALUES (ARRAY[[1,2],[3,4]], ARRAY[[5,6],[7,8]]);

SELECT ARRAY[f1, f2, '{{9,10},{11,12}}'::int[]] FROM arr;
                     array
------------------------------------------------
 {{{1,2},{3,4}},{{5,6},{7,8}},{{9,10},{11,12}}}
(1 row)

您可以构建一个空数组,但由于不可能有没有类型的数组,因此必须将空数组显式转换为所需的类型。例如:

SELECT ARRAY[]::integer[];
 array
-------
 {}
(1 row)

还可以从子查询的结果构建数组。在此形式中,数组构造函数写为关键字ARRAY后跟括号括起来的子查询(而不是方括号)。例如:

SELECT ARRAY(SELECT oid FROM ux_proc WHERE proname LIKE 'bytea%');
                              array
------------------------------------------------------------------
 {2011,1954,1948,1952,1951,1244,1950,2005,1949,1953,2006,31,2412}
(1 row)

SELECT ARRAY(SELECT ARRAY[i, i*2] FROM generate_series(1,5) AS a(i));
              array
----------------------------------
 {{1,2},{2,4},{3,6},{4,8},{5,10}}
(1 row)

子查询必须返回单个列。如果子查询的输出列是非数组类型,则生成的一维数组将具有与子查询结果中的每行一样的元素,元素类型与子查询的输出列匹配。如果子查询的输出列是数组类型,则结果将是相同类型但维数更高的数组;在这种情况下,所有子查询行必须产生具有相同维度的数组,否则结果将不是矩形的。

使用ARRAY构建的数组值的下标始终从1开始。有关数组的更多信息,请参见数组

1.2.13.行构造器

行构造器是一个表达式,它使用成员字段的值构建行值(也称为复合值)。行构造器由关键字ROW、左括号、零个或多个用逗号分隔的行字段值表达式和最后的右括号组成。例如:

SELECT ROW(1,2.5,'this is a test');

当列表中有多个表达式时,关键字ROW是可选的。

行构造器可以包括语法rowvalue.*,它将扩展为行值的元素列表,就像在SELECT列表的顶层使用.*语法一样(参见在查询中使用复合类型 )。例如,如果表t有列f1f2,则以下两个查询是相同的:

SELECT ROW(t.*, 42) FROM t;
SELECT ROW(t.f1, t.f2, 42) FROM t;

注意

如果需要嵌套行值的旧行为,请不要使用.*编写内部行值,例如ROW(t, 42)

默认情况下,ROW表达式创建的值是匿名记录类型。如果需要,它可以转换为命名复合类型,即表的行类型或使用CREATE TYPEAS创建的复合类型。可能需要显式转换以避免歧义。例如:

CREATE TABLE mytable(f1 int, f2 float, f3 text);

CREATE FUNCTION getf1(mytable) RETURNS int AS 'SELECT $1.f1' LANGUAGE SQL;

-- No cast needed since only one getf1() exists
SELECT getf1(ROW(1,2.5,'this is a test'));
 getf1
-------
     1
(1 row)

CREATE TYPE myrowtype AS (f1 int, f2 text, f3 numeric);

CREATE FUNCTION getf1(myrowtype) RETURNS int AS 'SELECT $1.f1' LANGUAGE SQL;

-- Now we need a cast to indicate which function to call:
SELECT getf1(ROW(1,2.5,'this is a test'));
ERROR:  function getf1(record) is not unique

SELECT getf1(ROW(1,2.5,'this is a test')::mytable);
 getf1
-------
     1
(1 row)

SELECT getf1(CAST(ROW(11,'this is a test',2.5) AS myrowtype));
 getf1
-------
    11
(1 row)

行构造器可用于构建要存储在复合类型表列中的复合值,或传递给接受复合参数的函数。此外,可以比较两个行值或测试行是否为NULLNOT NULL,例如:

SELECT ROW(1,2.5,'this is a test') = ROW(1, 3, 'not the same');
SELECT ROW(table.*) IS NULL FROM table;  -- 检测所有空行

行构造器也可与子查询一起使用。

1.2.14.表达式求值规则

子表达式的求值顺序未定义。特别地,运算符或函数的输入不一定按从左到右或任何其他固定顺序进行求值。

此外,如果一个表达式的结果可以通过仅评估其中的一些部分来确定,则其他子表达式可能根本不会被评估。例如,如果写入:

SELECT true OR somefunc();

那么somefunc()可能根本不会被调用。如果写入:

SELECT somefunc() OR true;

同样的情况也会发生。请注意,这与某些编程语言中发现的布尔运算符的从左到右的“短路”不同。

因此,在复杂表达式的一部分中使用具有副作用的函数是不明智的。在WHEREHAVING子句中依赖副作用或求值顺序尤其危险,因为这些子句作为开发执行计划的一部分而被广泛重新处理。这些子句中的布尔表达式(AND/OR/NOT组合)可以按照布尔代数法则允许的任何方式重新组织。

当必须强制求值顺序时,可以使用CASE结构。例如,在WHERE子句中,这是一种不可靠的尝试避免除以零:

SELECT ... WHERE x > 0 AND y/x > 1.5;

但这是安全的:

SELECT ... WHERE CASE WHEN x > 0 THEN y/x > 1.5 ELSE false END;

以这种方式使用CASE结构将阻止优化尝试,因此只有在必要时才应这样做。(在这个特定的例子中,写y> 1.5*x会更好。)

然而,CASE并不是解决这些问题的万能药。上述技术的一个限制是它不能防止常量子表达式的早期求值。标记为IMMUTABLE的函数和运算符可以在查询计划时进行评估,而不是在执行时进行评估。因此,例如:

SELECT CASE WHEN x > 0 THEN x ELSE 1/0 END FROM tab;

由于规划器试图简化常量子表达式,这可能导致除以零的失败,即使表中的每一行都具有x> 0,以便在运行时永远不会进入ELSE分支。

虽然那个特定的例子可能看起来很傻,但在函数内执行的查询中可能会出现不明显涉及常量的相关情况,因为函数参数和局部变量的值可以作为规划目的的常量插入到查询中。例如,在PL/uxSQL函数中,使用IF-THEN-ELSE语句来保护风险计算比仅将其嵌套在CASE表达式中更安全。

同样的限制是CASE不能防止对其中包含的聚合表达式进行评估,因为聚合表达式在考虑SELECT列表或HAVING子句中的其他表达式之前计算。例如,以下查询可能会导致除以零的错误,尽管看起来已经保护了它:

SELECT CASE WHEN min(employees) > 0
            THEN avg(expenses / employees)
       END
    FROM departments;

min()avg()聚合同时计算所有输入行,因此如果任何一行的employees等于零,则会在测试min()的结果之前发生除以零的错误。相反,使用WHEREFILTER子句防止有问题的输入行首先到达聚合函数。

1.3.调用函数

UXDB允许使用命名参数的函数使用位置表示法或命名表示法进行调用。命名表示法对于具有大量参数的函数特别有用,因为它使参数和实际参数之间的关联更加明确和可靠。在位置表示法中,函数调用的参数值按照它们在函数声明中定义的顺序编写。在命名表示法中,参数按名称与函数参数匹配,并且可以按任意顺序编写。对于每种表示法,还要考虑函数参数类型的影响。

在任何表示法中,函数声明中给出默认值的参数不需要在调用中编写。但是,在命名表示法中,任何组合的参数都可以省略;而在位置表示法中,参数只能从右到左省略。

UXDB还支持混合表示法,它结合了位置表示法和命名表示法。在这种情况下,位置参数首先编写,然后是命名参数。

以下示例将说明使用所有三种表示法的用法,使用以下函数定义:

CREATE FUNCTION concat_lower_or_upper(a text, b text, uppercase boolean DEFAULT false)
RETURNS text
AS
$$
 SELECT CASE
        WHEN $3 THEN UPPER($1 || ' ' || $2)
        ELSE LOWER($1 || ' ' || $2)
        END;
$$
LANGUAGE SQL IMMUTABLE STRICT;

函数concat_lower_or_upper有两个必需参数ab。此外,它还有一个名为uppercase的布尔类型参数,其默认值为false。有一个可选参数uppercase,默认为false。输入的ab将被连接起来,并根据uppercase参数强制转换为大写或小写。此函数定义的其余细节在此处不重要。

1.3.1.使用位置表示法

位置表示法是在UXDB中传递函数参数的传统机制。例如:

SELECT concat_lower_or_upper('Hello', 'World', true);
 concat_lower_or_upper
-----------------------
 HELLO WORLD
(1 row)

所有参数按顺序指定。结果是大写的,因为uppercase被指定为true。另一个例子是:

SELECT concat_lower_or_upper('Hello', 'World');
 concat_lower_or_upper
-----------------------
 hello world
(1 row)

这里省略了uppercase参数,因此它接收其默认值false,导致小写输出。在位置表示法中,可以从右到左省略具有默认值的参数。

1.3.2.使用命名表示法

在命名表示法中,使用=>将每个参数的名称与参数表达式分隔开。例如:

SELECT concat_lower_or_upper(a => 'Hello', b => 'World');
 concat_lower_or_upper
-----------------------
 hello world
(1 row)

同样,省略了参数uppercase,因此它隐含地设置为false。使用命名表示法的一个优点是可以以任何顺序指定参数,例如:

SELECT concat_lower_or_upper(a => 'Hello', b => 'World', uppercase => true);
 concat_lower_or_upper
-----------------------
 HELLO WORLD
(1 row)

SELECT concat_lower_or_upper(a => 'Hello', uppercase => true, b => 'World');
 concat_lower_or_upper
-----------------------
 HELLO WORLD
(1 row)

基于“:=”的旧语法出于向后兼容性而得到支持:

SELECT concat_lower_or_upper(a := 'Hello', uppercase := true, b := 'World');
 concat_lower_or_upper
-----------------------
 HELLO WORLD
(1 row)

1.3.3.使用混合表示法

混合表示法结合了位置表示法和命名表示法。但是,如已经提到的,命名参数不能在位置参数之前。例如:

SELECT concat_lower_or_upper('Hello', 'World', uppercase => true);
 concat_lower_or_upper
-----------------------
 HELLO WORLD
(1 row)

在上面的查询中,参数ab按位置指定,而uppercase按名称指定。在这个例子中,除了文档之外,这增加了一点。对于具有许多具有默认值的参数的更复杂的函数,命名或混合表示法可以节省大量的写作,并减少出错的机会。

注意

目前在调用聚合函数时无法使用命名和混合调用符号(但在将聚合函数用作窗口函数时可以使用)。


2.数据定义

本章介绍如何创建保存数据的数据库结构。在关系型数据库中,原始数据存储在表中,因此本章的大部分内容都是介绍如何创建和修改表以及可用于控制存储在表中的数据的功能。随后,我们将讨论如何将表组织成模式,并如何为表分配权限。最后,我们将简要介绍其他影响数据存储的功能,例如继承、表分区、视图、函数和触发器。

2.1.表

关系数据库中的表格类似于纸上的表格:由行和列组成。列的数量和顺序是固定的,每个列都有一个名称。行的数量是可变的,它反映了在给定时刻存储的数据量。SQL不保证表格行的顺序。除非明确请求排序,否则读取表格时,行将以未指定的顺序出现。此外,SQL不会为行分配唯一标识符,因此在表格中可能有几个完全相同的行。这是 SQL底层的数学模型的结果,但通常不是期望的结果。在本章后面,我们将看到如何处理这个问题。

每个列都有一个数据类型。数据类型约束了可以分配给列的可能值集,并为存储在列中的数据分配语义,以便可以用于计算。例如,声明为数值类型的列不会接受任意文本字符串,存储在这样的列中的数据可以用于数学计算。相比之下,声明为字符字符串类型的列将接受几乎任何类型的数据,但它不适合于数学计算,尽管其他操作,如字符串连接,是可用的。

UXDB包括一组相当大的内置数据类型,适用于许多应用程序。用户还可以定义自己的数据类型。大多数内置数据类型都有明显的名称和语义。一些常用的数据类型是整数(integer)、可能是分数的数字(numeric)、字符字符串(text)、日期(date)、时间(time)和同时包含日期和时间的值(timestamp)。

要创建一个表,您可以使用恰当命名的CREATE TABLE命令。在此命令中,您至少需要指定一个新表的名称、列的名称和每个列的数据类型。例如:

CREATE TABLE my_first_table (
first_column text,
second_column integer
);

这将创建一个名为my_first_table的表,其中包含两列。第一列名为first_column,数据类型为text;第二列名为second_column,类型为integer。表和列名遵循标识符和关键词中解释的标识符语法。类型名称通常也是标识符,但有一些例外。请注意,列列表以逗号分隔,并用括号括起来。

当然,上面的示例是非常人为的。通常,您会为表和列命名,以传达它们存储的数据类型。因此,让我们看一个更现实的例子:

CREATE TABLE products (
product_no integer,
name text,
price numeric
);

numeric类型可以存储小数部分,这是货币金额的典型情况。)

提示

当您创建许多相互关联的表时,选择一致的表和列命名模式是明智的。例如,有使用单数或复数名词作为表名的选择,这两种选择都受到某些理论家的青睐。

表可以包含的列数有限。根据列类型,它在250到1600之间。然而,定义一个具有接近这么多列的表是非常不寻常的,通常是一个可疑的设计。

如果您不再需要一个表,可以使用DROP TABLE命令将其删除。例如:

DROP TABLE my_first_table;
DROP TABLE products;

尝试删除不存在的表是一个错误。然而,在SQL脚本文件中,通常会无条件地尝试在创建表之前删除每个表,忽略任何错误消息,以便脚本可以在表存在与否的情况下工作。(如果您愿意,可以使用DROP TABLE IF EXISTS变体来避免错误消息,但这不是标准SQL。)

如果您需要修改已经存在的表,请参见修改表格

使用到目前为止讨论的工具,您可以创建完全功能的表。本章的其余部分涉及添加表定义的功能,以确保数据完整性、安全性或方便性。如果您现在急于填充您的表,请跳到数据操作,稍后再阅读本章的其余部分。

2.2.默认值

列可以被赋予默认值。当创建新行并且某些列没有指定值时,这些列将被填充为它们各自的默认值。数据操作命令也可以显式地请求将列设置为其默认值,而不必知道该值是什么。(有关数据操作命令的详细信息在数据操作 。)

如果没有显式声明默认值,则默认值为 null 值。这通常是有意义的,因为 null 值可以被认为代表未知数据。

在表定义中,列的默认值列在列数据类型之后。例如:

CREATE TABLE products (
product_no integer,
name text,
price numeric DEFAULT 9.99
);

默认值可以是一个表达式,该表达式将在插入默认值时进行评估(而不是在创建表时)。一个常见的例子是将 timestamp 列的默认值设置为CURRENT_TIMESTAMP,以便它被设置为行插入的时间。另一个常见的例子是为每一行生成一个“序列号”。在UXDB中,通常是通过以下方式完成的:

CREATE TABLE products (
product_no integer DEFAULT nextval('products_product_no_seq'),
...
);

其中 nextval()函数从一个序列对象中提供连续的值。这种安排是足够常见的,以至于有一个特殊的简写方式:

CREATE TABLE products (
product_no SERIAL,
...
);

2.3.生成列

生成列是一种特殊的列,它总是从其他列计算而来。因此,它对于列就像视图对于表一样。有两种生成列:存储和虚拟。存储生成列在写入(插入或更新)时计算并占用存储空间,就像普通列一样。虚拟生成列不占用存储空间,在读取时计算。因此,虚拟生成列类似于视图,而存储生成列类似于材料化视图(除了它总是自动更新)。UXDB目前仅实现了存储生成列。

要创建生成列,请在CREATE TABLE中使用GENERATED ALWAYS AS子句,例如:

CREATE TABLE people (
...,
height_cm numeric,
height_in numeric GENERATED ALWAYS AS (height_cm / 2.54) STORED
);

必须指定关键字STORED来选择存储类型的生成列。

不能直接写入生成列。在INSERTUPDATE命令中,不能为生成列指定值,但可以指定关键字DEFAULT

考虑具有默认值和生成列之间的差异。当行首次插入时,列默认值只计算一次(如果没有提供其他值);生成列在行更改时更新,不能被覆盖。列默认值可能不引用表的其他列;生成表达式通常会这样做。列默认值可以使用易失函数,例如random()或引用当前时间的函数;但是,生成列不允许这样做。

生成列和涉及生成列的表的定义受到几个限制:

  • 生成表达式只能使用不可变函数,不能使用子查询或以任何方式引用除当前行之外的任何内容。

  • 生成表达式不能引用另一个生成列。

  • 生成表达式不能引用系统列,除了tableoid

  • 生成列不能有列默认值或标识定义。

  • 生成列不能是分区键的一部分。

  • 外部表可以有生成列。

  • 对于继承:

    • 如果父列是生成列,则子列也必须是使用相同表达式的生成列。在子列的定义中,省略GENERATED子句,因为它将从父项复制。

    • 在多重继承的情况下,如果一个父列是生成列,则所有父列都必须是生成列,并且使用相同的表达式。

    • 如果父列不是生成列,则可以定义子列为生成列或非生成列。

使用生成列还有其他考虑因素。

  • 生成列与其基础基础列分别维护访问权限。因此,可以安排特定角色可以从生成列中读取,但不能从基础基础列中读取。

  • 生成列在概念上在BEFORE触发器运行后更新。因此,在BEFORE触发器中对基础列进行的更改将反映在生成列中。但是相反,在BEFORE触发器中不允许访问生成列。

2.4.约束

数据类型是限制可以存储在表中的数据类型的一种方式。然而,对于许多应用程序,它们提供的约束太粗略了。例如,包含产品价格的列应该只接受正值。但是没有标准的数据类型只接受正数。另一个问题是,您可能希望将列数据约束与其他列或行相关。例如,在包含产品信息的表中,每个产品编号应该只有一行。

为此,SQL允许您在列和表上定义约束。约束可以让您对表中的数据拥有尽可能多的控制权。如果用户尝试将违反约束的数据存储在列中,则会引发错误。即使该值来自默认值定义也是如此。

2.4.1.检查约束

检查约束是最通用的约束类型。它允许您指定某个列中的值必须满足布尔(真值)表达式。例如,要求正的产品价格,您可以使用:

CREATE TABLE products (
product_no integer,
name text,
price numeric CHECK (price > 0)
);

如您所见,约束定义出现在数据类型之后,就像默认值定义一样。默认值和约束可以以任何顺序列出。检查约束由关键字CHECK后跟括号中的表达式组成。检查约束表达式应涉及受此约束的列,否则约束将没有太多意义。

您还可以为约束指定单独的名称。这可以澄清错误消息并允许您在需要更改约束时引用它。语法如下:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CONSTRAINT positive_price CHECK (price > 0)
);

因此,要指定命名约束,请使用关键字CONSTRAINT后跟标识符,后跟约束定义。(如果您不以这种方式指定约束名称,则系统会为您选择一个名称。)

检查约束还可以引用多个列。假设您存储了常规价格和折扣价格,并且您希望确保折扣价格低于常规价格:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0),
    discounted_price numeric CHECK (discounted_price > 0),
    CHECK (price > discounted_price)
);

前两个约束应该很熟悉。第三个使用了新的语法。它没有附加到特定的列,而是作为逗号分隔的列列表中的单独项出现。列定义和这些约束定义可以以混合顺序列出。

我们说前两个约束是列约束,而第三个是表约束,因为它是单独编写的,不属于任何一个列定义。列约束也可以编写为表约束,反之则不一定可行,因为列约束应仅引用其附加到的列。(UXDB不强制执行该规则,但如果您希望您的表定义与其他数据库系统一起使用,则应遵循该规则。)上面的示例也可以编写为:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    CHECK (price > 0),
    discounted_price numeric,
    CHECK (discounted_price > 0),
    CHECK (price > discounted_price)
);

甚至可以编写为:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0),
    discounted_price numeric,
    CHECK (discounted_price > 0 AND price > discounted_price)
);

这是一种品味问题。可以以与列约束相同的方式为表约束分配名称:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    CHECK (price > 0),
    discounted_price numeric,
    CHECK (discounted_price > 0),
    CONSTRAINT valid_discount CHECK (price > discounted_price)
);

应注意,如果检查表达式计算为true或null值,则满足检查约束。由于大多数表达式如果任何操作数为null,则将计算为null值,因此它们不会阻止受约束列中的null值。为了确保列不包含null值,可以使用下一节中描述的非null约束。

注意

UXDB不支持引用除正在检查的新行或更新行之外的表数据的CHECK约束。虽然违反此规则的CHECK约束在简单测试中可能有效,但它不能保证数据库不会达到无法恢复的状态。其中约束条件为假(由于其他涉及的行的后续更改)。这将导致数据库转储和还原失败。即使完整的数据库状态与约束一致,由于未按满足约束的顺序加载行,还原也可能失败。如果可能,请使用UNIQUEEXCLUDEFOREIGN KEY约束来表示跨行和跨表限制。

如果您想要的是在行插入时进行一次性检查,而不是持续维护的一致性保证,则可以使用自定义触发器来实现。(这种方法避免了转储/还原问题,因为ux_dump在恢复数据之后才重新安装触发器,因此在转储/还原期间不会强制执行检查。)

UXDB假定CHECK约束的条件是不可变的,即它们对于相同的输入行始终会给出相同的结果。这种假设是验证仅在插入或更新行时检查CHECK约束的原因,而不是其他时间。(上面关于不引用其他表数据的警告实际上是这个限制的特殊情况。)

打破这个假设的常见方法是在CHECK表达式中引用用户定义的函数,然后更改该函数的行为。UXDB不会禁止这样做,但它不会注意到表中是否存在现在违反CHECK约束的行。这将导致随后的数据库转储和还原失败。处理这种更改的推荐方法是删除约束(使用ALTER TABLE),调整函数定义,然后重新添加约束,从而针对所有表行重新检查它。

2.4.2.非空约束

只是指定列不能假定空值。语法示例:

CREATE TABLE products (
    product_no integer NOT NULL,
    name text NOT NULL,
    price numeric
);

非空约束始终写为列约束。非空约束在功能上等同于创建CHECK约束CHECK(column_name IS NOT NULL),但在UXDB中创建显式非空约束更有效。缺点是您无法为以这种方式创建的非空约束指定显式名称。

当然,一列可以有多个约束。只需将约束一个接一个地写入:

CREATE TABLE
products ( product_no integer NOT NULL, name text NOT NULL, price
numeric NOT NULL CHECK (price > 0) ); 

顺序无关紧要。它不一定确定约束的检查顺序。

非空约束有一个相反的约束:NULL约束。这并不意味着列必须为空,这肯定是无用的。相反,这只是选择列可能为空的默认行为。NULL约束不存在于SQL标准中,不应在可移植应用程序中使用。(它是仅添加到 UXDB 以兼容其他一些数据库系统。然而,一些用户喜欢它,因为它使在脚本文件中切换约束变得容易。例如,您可以从以下内容开始:

CREATE TABLE products (
    product_no integer NULL,
    name text NULL,
    price numeric NULL
);

然后在所需位置插入NOT关键字。

提示

在大多数数据库设计中,大多数列应标记为非空。

在兼容mysql数据库模式下,非空约束和自增序列组合场景下,自增序列优先级更高,插入NULL时直接产生序列插入。

示例

--创建新表
uxdb=# create table testTable(id serial not null primary key,name varchar(100));
CREATE TABLE

--插入数据
uxdb=# INSERT INTO testTable values(null,'b');
INSERT 0 1

--查询数据
uxdb=# select * from testTable;
 id | name
----+------
  1 | b
(1 row)

--插入数据
uxdb=# INSERT INTO testTable values(null,'b');
INSERT 0 1
uxdb=# INSERT INTO testTable values(null,'b');
INSERT 0 1

--查询数据
uxdb=# select * from testTable;
 id | name
----+------
  1 | b
  2 | b
  3 | b
(3 rows)

2.4.3.唯一约束

唯一约束确保列或一组列中包含的数据在整个表中是唯一的。语法如下:

CREATE TABLE products (
    product_no integer UNIQUE,
    name text,
    price numeric
);

当作为列约束时,写成:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    UNIQUE (product_no)
);

当作为表约束时,如下所示。

要为一组列定义唯一约束,请将其作为表约束编写,其中列名用逗号分隔:

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    UNIQUE (a, c)
);

这指定了指定列中的值的组合在整个表中是唯一的,尽管任何一个列不需要(通常不是)唯一的。

您可以按通常的方式为唯一约束分配自己的名称:

CREATE TABLE products (
    product_no integer CONSTRAINT must_be_different UNIQUE,
    name text,
    price numeric
);

添加唯一约束将自动在列或列组上创建唯一的B-tree索引。仅覆盖某些行的唯一性限制不能写为唯一约束,但可以通过创建唯一的部分索引来强制执行这种限制。

通常,如果表中有多行,其中包含约束中包含的所有列的值相等,则违反了唯一约束。默认情况下,在此比较中,两个空值不被视为相等。这意味着即使存在唯一约束,仍然可以存储包含约束列中至少一个空值的重复行。通过添加NULLS NOT DISTINCT 子句,可以更改此行为,例如:

CREATE TABLE products (
    product_no integer UNIQUE NULLS NOT DISTINCT,
    name text,
    price numeric
);

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    UNIQUE NULLS NOT DISTINCT (product_no)
);

默认行为可以使用 NULLS DISTINCT 明确指定。唯一约束中的默认空值处理是根据SQL标准的实现定义的,其他实现具有不同的行为。因此,在开发旨在具有可移植性的应用程序时要小心。

2.4.4.主键

主键约束表示一个列或一组列可以作为表中行的唯一标识符。这要求值既是唯一的,也不为null。因此,以下两个表定义接受相同的数据:

CREATE TABLE products (
    product_no integer UNIQUE NOT NULL,
    name text,
    price numeric
);
CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

主键可以跨越多个列;语法与唯一约束类似:

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    PRIMARY KEY (a, c)
);

添加主键将自动在主键中列出的列或列组上创建唯一的B树索引,并强制将列标记为NOT NULL

一个表最多只能有一个主键。(可以有任意数量的唯一和非空约束,它们在功能上几乎相同,但只能将一个标识为主键。)关系数据库理论规定每个表必须有一个主键。这个规则没有被UXDB强制执行,但最好遵循它。

主键对于文档和客户端应用程序都很有用。例如,允许修改行值的GUI应用程序可能需要知道表的主键,以便能够唯一地标识行。如果声明了一个主键,数据库系统还可以通过多种方式使用该主键;例如,主键为引用其表的外键定义默认目标列。

2.4.5.外键

外键约束指定一个列(或一组列)中的值必须与另一个表中的某一行中出现的值匹配。我们说这维护了两个相关表之间的引用完整性。

假设您有一个我们已经多次使用过的产品表:

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

我们还假设您有一个存储这些产品订单的表。我们想确保订单表只包含实际存在的产品订单。因此,我们在订单表中定义一个外键约束,引用产品表:

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    product_no integer REFERENCES products (product_no),
    quantity integer
);

现在不可能创建具有非空product_no条目且不出现在产品表中的订单。

在这种情况下,我们说订单表是引用表,产品表是被引用表。同样,有引用和被引用的列。

您还可以将上述命令缩短为:

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    product_no integer REFERENCES products,
    quantity integer
);

因为在没有列列表的情况下,主键的列是默认引用的列。引用表用作引用列。

您可以按照通常的方式为外键约束分配自己的名称。

外键还可以约束和引用一组列。通常,它需要以表约束形式编写。以下是一个人为的语法示例:

CREATE TABLE t1 (
  a integer PRIMARY KEY,
  b integer,
  c integer,
  FOREIGN KEY (b, c) REFERENCES other_table (c1, c2)
);

当然,受约束的列的数量和类型需要与引用列的数量和类型匹配。

有时,外键约束的“其他表”与同一表相同;这称为自引用外键。例如,如果您想要表的行表示树结构的节点,则可以编写以下内容:

CREATE TABLE tree (
    node_id integer PRIMARY KEY,
    parent_id integer REFERENCES tree,
    name text,
    ...
);

顶级节点将具有NULL parent_id,而非NULL parent_id条目将被限制为引用表的有效行。

一个表可以有多个外键约束。这用于在表之间实现多对多关系。假设您有关于产品和订单的表,但现在您想允许一个订单包含可能多个产品(上面的结构不允许)。您可以使用此表结构:

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    shipping_address text,
    ...
);

CREATE TABLE order_items (
    product_no integer REFERENCES products,
    order_id integer REFERENCES orders,
    quantity integer,
    PRIMARY KEY (product_no, order_id)
);

请注意,主键与最后一个表中的外键重叠。

我们知道,外键不允许创建与任何产品无关的订单。但是,如果在创建引用它的订单之后删除了产品怎么办?SQL也允许您处理这个问题。直观地说,我们有几个选择:

  • 禁止删除引用的产品

  • 同时删除订单

  • 其他操作?

为了说明这一点,让我们在上面的多对多关系示例中实现以下策略:当有人想要删除仍然通过order_items引用的产品时,我们禁止它。如果有人删除订单,则也会删除订单项:

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    shipping_address text,
    ...
);

CREATE TABLE order_items (
    product_no integer REFERENCES products ON DELETE RESTRICT,
    order_id integer REFERENCES orders ON DELETE CASCADE,
    quantity integer,
    PRIMARY KEY (product_no, order_id)
);

限制和级联删除是最常见的选项。RESTRICT 防止删除引用行。NO ACTION表示如果在检查约束时仍存在任何引用行,则会引发错误;如果您没有指定任何内容,则这是默认行为。(这两个选择之间的基本区别在于,NO ACTION 允许将检查推迟到将来的某个时间,而 RESTRICT不允许这样做。)在事务的后续阶段,RESTRICT不会执行删除操作。CASCADE指定当引用行被删除时,引用它的行也应该被自动删除。还有两个选项:SET NULLSET DEFAULT。当引用行被删除时,这些选项会导致引用行中的引用列被设置为 null 或它们的默认值。请注意,这些选项并不免除您遵守任何约束条件。例如,如果一个操作指定了SET DEFAULT,但默认值不满足外键约束,则操作将失败。

选择适当的ON DELETE操作取决于相关表所代表的对象类型。当引用表表示被引用表所代表的组件,且不能独立存在时,则CASCADE 可能是合适的。如果两个表表示独立的对象,则RESTRICTNO ACTION更合适;实际上想要删除两个对象的应用程序必须明确地运行两个删除命令。在上面的例子中,订单项是订单的一部分,如果删除订单,则自动删除订单项是很方便的。但是产品和订单是不同的东西,因此使删除产品自动导致删除一些订单项可能被认为是有问题的。如果外键关系表示可选信息,则SET NULLSET DEFAULT操作可能是合适的。例如,如果产品表包含对产品经理的引用,并且产品经理条目被删除,则将产品的产品经理设置为null或默认值可能是有用的。

操作SET NULLSET DEFAULT可以使用列列表指定要设置的列。通常,所有外键约束的列都会被设置;在某些特殊情况下,仅设置子集是有用的。考虑以下示例:

CREATE TABLE tenants (
    tenant_id integer PRIMARY KEY
);

CREATE TABLE users (
    tenant_id integer REFERENCES tenants ON DELETE CASCADE,
    user_id integer NOT NULL,
    PRIMARY KEY (tenant_id, user_id)
);

CREATE TABLE posts (
    tenant_id integer REFERENCES tenants ON DELETE CASCADE,
    post_id integer NOT NULL,
    author_id integer,
    PRIMARY KEY (tenant_id, post_id),
    FOREIGN KEY (tenant_id, author_id) REFERENCES users ON DELETE SET NULL (author_id)
);

如果没有指定列,外键将同时将列 tenant_id 设置为 null,但该列仍然作为主键的一部分是必需的。

类似于 ON DELETE,还有 ON UPDATE,当引用列被更改(更新)时会调用它。可能的操作是相同的,只是不能为 SET NULLSET DEFAULT 指定列列表。在这种情况下,CASCADE 意味着应该将引用列的更新值复制到引用行中。

通常,如果引用行的任何引用列为 null,则引用行不需要满足外键约束。如果在外键声明中添加了MATCH FULL,则只有当所有引用列都为null时,引用行才能逃避满足约束(因此,null和非null值的混合保证失败 MATCH FULL 约束)。如果您不想引用行为了避免满足外键约束,需要将引用列声明为NOT NULL

外键必须引用主键或形成唯一约束的列。这意味着引用列始终具有索引(基础主键或唯一约束下的索引);因此,检查引用行是否有匹配项将是有效的。由于从引用表中删除行或更新引用列将需要扫描引用表以查找与旧值匹配的行,因此通常最好也对引用列建立索引。由于这并不总是需要的,并且有许多可用的索引选择,因此声明外键约束不会自动在引用列上创建索引。

2.4.5.1.兼容mysql外键约束开关

控制外键约束的失效与生效。外键约束开关FOREIGN_KEY_CHECKS默认为on开启外键约束,当开关关闭时跳过对应的外键检查操作,如:UPDATEINSERTDELETEDROPADD CONSTRAINT操作。该开关仅在UXDB数据库mysql运行模式下有效。

控制外键约束失效与生效有如下两种方式。

Set FOREIGN_KEY_CHECKS = on;
或 
set FOREIGN_KEY_CHECKS = off;
ALTER system set FOREIGN_KEY_CHECKS = on; 
或 
alter system set FOREIGN_KEY_CHECKS = off;

2.4.6.排除约束

排除约束确保如果使用指定的运算符在指定的列或表达式上比较任意两行,则这些运算符比较中至少有一个将返回 false 或 null。语法如下:

CREATE TABLE circles (
    c circle,
    EXCLUDE USING gist (c WITH &&)
);

添加排除约束将自动创建指定在约束声明中的索引类型的索引。

2.5.系统列

每个表都有几个由系统隐式定义的系统列。因此,这些名称不能用作用户定义列的名称。(请注意,这些限制与名称是否为关键字无关;引用名称将无法避免这些限制。)您不需要真正关心这些列;只需知道它们存在即可。

  • tableoid
    包含此行的表的OID。此列对于从分区表(参见表分区)或继承层次结构(参见继承)中选择的查询特别有用,因为如果没有它,很难确定行来自哪个单独的表。可以将tableoidux_classoid列连接以获取表名。

  • xmin
    插入此行版本的标识(事务ID)。(行版本是行的一个单独状态;每次更新行都会为同一逻辑行创建一个新的行版本。)

  • cmin
    插入事务中的命令标识符(从零开始)。

  • xmax
    删除事务的标识(事务ID),或未删除行版本的零。在可见行版本中,此列可能为非零值。这通常表示删除事务尚未提交,或尝试删除已回滚。

  • cmax
    删除事务中的命令标识符,或零。

  • ctid
    ctid是行版本在其表中的物理位置。请注意,尽管可以使用ctid快速定位行版本,但如果使用VACUUM FULL更新或移动行,则行的ctid将更改。因此,ctid无法作为长期行标识符使用。应使用主键来标识逻辑行。

事务标识符也是32位数量级。在长期运行的数据库中,事务ID可能会回绕。在适当的维护程序下,这不是致命问题。然而,长期依赖事务ID的唯一性是不明智的(超过十亿个事务)。

命令标识符也是32位数量级。这会创建一个硬限制,即单个事务中最多有 232(40 亿)个 SQL命令。实际上,这个限制不是问题——请注意,限制是在SQL命令数量上,而不是在处理的行数上。此外,只有实际修改数据库内容的命令才会消耗命令标识符。

2.6.修改表格

当您创建一个表格并意识到您犯了一个错误,或者应用程序的要求发生了变化时,您可以删除该表格并重新创建。但是,如果表格已经填充了数据,或者表格被其他数据库对象引用(例如外键约束),这不是一个方便的选项。因此,UXDB提供了一系列命令来修改现有的表格。请注意,这在概念上与修改表格中包含的数据是不同的:在这里,我们感兴趣的是修改表格的定义或结构。

您可以:

  • 添加列

  • 删除列

  • 添加约束

  • 删除约束

  • 更改默认值

  • 更改列数据类型

  • 重命名列

  • 重命名表格

所有这些操作都使用ALTER TABLE命令执行,其参考页面包含超出此处给出的详细信息。

2.6.1.添加列

要添加列,请使用以下命令:

ALTER TABLE products ADD COLUMN description text;

新列最初填充有给定的默认值(如果您没有指定DEFAULT子句,则为null)。

提示

UXDB添加具有恒定默认值的列不再意味着在执行ALTER TABLE语句时需要更新表的每一行。相反,下次访问行时将返回默认值,并在重写表时应用,使得ALTER TABLE即使在大型表上也非常快速。

但是,如果默认值是易变的(例如clock_timestamp()),则每行都需要使用在执行ALTER TABLE时计算的值进行更新。为了避免潜在的冗长更新操作,特别是如果您打算填充列的大部分非默认值,那么最好添加没有默认值的列,使用UPDATE插入正确的值,然后按照下面的说明添加任何所需的默认值。

您还可以在同一时间定义列上的约束,使用通常的语法:

ALTER TABLE products ADD COLUMN description text CHECK (description <> '');

实际上,可以在CREATE TABLE中应用于列描述的所有选项都可以在此处使用。但是请记住,默认值必须满足给定的约束条件,否则ADD将失败。或者,您可以稍后添加约束(请参见下文),在正确填写新列后。

2.6.2.删除列

要删除列,请使用以下命令:

ALTER TABLE products DROP COLUMN description;

该列中的任何数据都会消失。涉及该列的表约束也将被删除。但是,如果该列被另一个表的外键约束引用,UXDB将不会默默删除该约束。您可以通过添加CASCADE来授权删除依赖于该列的所有内容:

ALTER TABLE products DROP COLUMN description CASCADE;

如果有视图依赖于该列,不带cascade选项时该列被直接删除,视图查询报错;带cascade选项时该列被直接删除,对应视图也会被删除。

2.6.3.添加约束

要添加约束,请使用表约束语法。例如:

ALTER TABLE products ADD CHECK (name <> '');
ALTER TABLE products ADD CONSTRAINT some_name UNIQUE (product_no);
ALTER TABLE products ADD FOREIGN KEY (product_group_id) REFERENCES product_groups;

要添加非空约束,无法编写为表约束,请使用以下语法:

ALTER TABLE products ALTER COLUMN product_no SET NOT NULL;

约束将立即进行检查,因此在添加约束之前,表数据必须满足约束。

2.6.4.删除约束

要删除约束,您需要知道它的名称。如果您给它起了一个名称,那就很容易。否则,系统会分配一个生成的名称,您需要找出它的名称。在这里,uxsql 命令\d tablename 可以提供帮助;其他接口也可能提供一种检查表详细信息的方法。然后,命令是:

ALTER TABLE products DROP CONSTRAINT some_name;

(如果您正在处理像$2这样的生成的约束名称,请不要忘记将其加倍引用,以使其成为有效的标识符。)

与删除列一样,如果您想删除其他依赖于某个约束的约束,则需要添加 CASCADE。一个例子是,外键约束依赖于所引用列上的唯一或主键约束。

对于除非空约束之外的所有约束类型,它们的处理方式都相同。要删除非空约束,请使用:

ALTER TABLE products ALTER COLUMN product_no DROP NOT NULL;

(请记住,非空约束没有名称。)

2.6.5.更改列的默认值

要为列设置新的默认值,请使用以下命令:

ALTER TABLE products ALTER COLUMN price SET DEFAULT 7.77;

请注意,这不会影响表中的任何现有行,它只会更改未来的 INSERT 命令的默认值。

要删除任何默认值,请使用:

ALTER TABLE products ALTER COLUMN price DROP DEFAULT;

这实际上与将默认值设置为 null 相同。因此,如果没有定义默认值,则删除默认值不会出错,因为默认值隐式为 null 值。

2.6.6.更改列的数据类型

要将列转换为不同的数据类型,请使用以下命令:

ALTER TABLE products ALTER COLUMN price TYPE numeric(10,2);

只有当可以通过隐式转换将列中的每个现有条目转换为新类型时,此操作才会成功。如果需要更复杂的转换,则可以添加 USING子句,指定如何从旧值计算新值。

UXDB将尝试将列的默认值(如果有)转换为新类型,以及涉及该列的任何约束。但是,这些转换可能会失败,或者可能会产生令人惊讶的结果。通常最好在更改其类型之前删除列上的任何约束,然后在适当修改约束后添加回来。

2.6.7.重命名列

要重命名列:

ALTER TABLE products RENAME COLUMN product_no TO product_number;

2.6.8.重命名表

要重命名表:

ALTER TABLE products RENAME TO items;

2.7.权限

当创建一个对象时,它会被分配一个所有者。通常情况下,所有者是执行创建语句的角色。对于大多数类型的对象,初始状态是只有所有者(或超级用户)可以对对象进行任何操作。要允许其他角色使用它,必须授予权限。

有不同种类的权限:SELECTINSERTUPDATEDELETETRUNCATEREFERENCESTRIGGERCREATECONNECTTEMPORARYEXECUTEUSAGESETALTER SYSTEM。适用于特定对象的权限因对象的类型(表、函数等)而异。有关这些权限含义的更多详细信息,请参见下文。

修改或销毁对象的权利是作为对象所有者的固有权利,本身无法授予或撤销。(但是,像所有权限一样,该权利可以被拥有角色的成员继承。

可以使用适用于对象的相应种类的 ALTER 命令将对象分配给新所有者,例如:

ALTER TABLE table_name OWNER TO new_owner;

超级用户始终可以这样做;普通角色只有在它们既是对象的当前所有者(或拥有角色的成员)又是新拥有角色的成员时才能这样做。

要分配权限,使用GRANT命令。例如,如果joe是现有角色,并且要将对表的SELECT 权限授予给它,则可以使用以下命令:

GRANT SELECT ON table_name TO joe;

要撤销权限,使用REVOKE命令。例如,要从joe中撤销对表的SELECT 权限,则可以使用以下命令:

REVOKE SELECT ON table_name FROM joe;

要查看已分配的权限,请使用GRANT命令的[NO]INHERIT[NO]RECURSIVE选项。要查看当前角色拥有的所有权限,请使用SHOW GRANTS命令。

accounts是一个现有的表,可以使用以下命令授予更新表的权限:

GRANT UPDATE ON accounts TO joe;

在特定权限的位置写入ALL将授予与对象类型相关的所有权限。

特殊的“角色”名称PUBLIC可以用于授予系统上每个角色的权限。此外,当有许多数据库用户时,可以设置“组”角色来帮助管理权限。

要撤销先前授予的权限,请使用名为REVOKE的命令:

REVOKE ALL ON accounts FROM PUBLIC;

通常,只有对象的所有者(或超级用户)可以在对象上授予或撤销权限。但是,可以授予“具有授予权限”的权限,这将使接收者有权将其转授给其他人。如果随后撤销了授予权限,则所有从该接收者(直接或通过授权链)接收该权限的人都将失去该权限。

对象的所有者可以选择撤销自己的普通权限,例如使表对自己和其他人只读。但是,所有者始终被视为持有所有授予权限,因此他们始终可以重新授予自己的权限。

可用的权限包括:

  • SELECT
    允许从表、视图、材料化视图或其他类似表的对象的任何列或特定列中进行SELECT。还允许使用COPY TO。此权限还需要引用UPDATEDELETE中的现有列值。对于序列,此权限还允许使用currval函数。对于大型对象,此权限允许读取对象。

  • INSERT
    允许将新行插入到表、视图等中。可以授予特定列上的权限,此时只能在INSERT命令中分配这些列(因此其他列将接收默认值)。还允许使用COPY FROM

  • UPDATE
    允许更新表、视图等的任何列或特定列。(实际上,任何非平凡的UPDATE命令都需要SELECT权限,因为它必须引用表列以确定要更新的行和/或计算列的新值。)SELECT... FOR UPDATESELECT ... FOR SHARE还需要至少一列的此权限,除了SELECT权限。对于序列,此权限允许使用nextvalsetval函数。对于大型对象,此权限允许编写或截断对象。

  • DELETE
    允许从表、视图等中删除行。(实际上,任何非平凡的DELETE命令都需要SELECT权限,因为它必须引用表列以确定要删除的行。)

  • TRUNCATE
    允许在表上进行TRUNCATE操作。

  • REFERENCES
    允许创建引用表或表的特定列的外键约束。

  • TRIGGER
    允许在表、视图等上创建触发器。

  • CREATE
    对于数据库,允许在数据库中创建新模式和发布,并允许在数据库中安装可信扩展。

    对于模式,允许在模式中创建新对象。要重命名现有对象,您必须拥有该对象并具有包含模式的此权限。

    对于表空间,允许在表空间中创建表、索引和临时文件,并允许创建以该表空间为默认表空间的数据库。

    请注意,撤销此权限不会改变现有对象的存在或位置。

  • CONNECT
    允许授予人连接到数据库。在连接启动时检查此权限(除了检查ux_hba.conf强制执行的任何限制)。

  • TEMPORARY
    允许在使用数据库时创建临时表。

  • EXECUTE
    允许调用函数或过程,包括使用在函数之上实现的任何运算符。这是唯一适用于函数和过程的权限类型。

  • USAGE
    对于过程性语言,允许使用该语言创建函数。这是唯一适用于过程性语言的权限类型。

    对于模式,允许访问包含在模式中的对象(假设对象自己的权限要求也得到满足)。基本上,这允许受让人“查找”模式中的对象。如果没有此权限,仍然可以看到对象名称,例如通过查询系统目录。此外,在撤销此权限之后,现有会话可能具有先前执行此查找的语句,因此这不是完全安全的防止对象访问的方法。

    对于序列,允许使用currval和nextval函数。

    对于类型和域,允许在创建表、函数和其他模式对象时使用类型或域。(请注意,此权限不控制类型的所有“使用”,例如在查询中出现的类型值。它仅防止创建依赖于该类型的对象。此权限的主要目的是控制哪些用户可以创建类型的依赖项,这可能会防止所有者稍后更改类型。)

    对于外部数据包装器,允许使用外部数据包装器创建新服务器。

    对于外部服务器,允许使用服务器创建外部表。受让人还可以创建、更改或删除与该服务器关联的自己的用户映射。

  • SET
    允许在当前会话中将服务器配置参数设置为新值。(虽然可以授予任何参数的此权限,但除了通常需要超级用户权限才能设置的参数外,此权限毫无意义。)

  • ALTER SYSTEM
    允许使用ALTER SYSTEM命令将服务器配置参数配置为新值。

  • ALTER
    允许超级用户或表所有者以外的用户,对表进行alter操作。

其他命令所需的权限列在相应命令的参考页面上。

UXDB在创建某些类型的对象时默认向PUBLIC授予权限。默认情况下,对于表、表列、序列、外部数据包装器、外部服务器、大对象、模式、表空间或配置参数,不会向PUBLIC授予任何权限。对于其他类型的对象,向PUBLIC授予的默认权限如下:CONNECTTEMPORARY(创建临时表)权限适用于数据库;EXECUTE权限适用于函数和过程;USAGE权限适用于语言和数据类型(包括域)。当然,对象所有者可以REVOKE默认和明确授予的权限。(为了最大安全性,在创建对象的同一事务中发出REVOKE;然后就没有其他用户可以使用该对象的窗口了。)此外,可以使用ALTER DEFAULT PRIVILEGES命令覆盖这些默认权限设置。

表 ACL权限缩写显示了在ACL(访问控制列表)值中用于这些权限类型的单个字母缩写。您将在下面列出的uxsql命令的输出中看到这些字母,或者在查看系统目录的ACL列时看到这些字母。

表 ACL权限缩写

权限缩写适用对象类型
SELECTr (读取)LARGE OBJECT, SEQUENCE, TABLE (及类似对象), 表列
INSERTa (追加)TABLE, 表列
UPDATEw (写入)LARGE OBJECT, SEQUENCE, TABLE, 表列
DELETEdTABLE
TRUNCATEDTABLE
REFERENCESxTABLE, 表列
TRIGGERtTABLE
CREATECDATABASE, SCHEMA, TABLESPACE
CONNECTcDATABASE
TEMPORARYTDATABASE
EXECUTEXFUNCTION, PROCEDURE
USAGEUDOMAIN, FDW, FOREIGN SERVER, LANGUAGE, SCHEMA, SEQUENCE, TYPE
SETsPARAMETER
ALTER SYSTEMAPARAMETER
ENCRYPT_DECRYPTETABLE注: 仅在 UXDB 安全模式下可用)
ALTERLTABLE (及类似对象)

表 访问权限总结总结了每种类型的SQL对象可用的权限,使用上述缩写。它还显示了可用于检查每种对象类型的权限设置的uxsql命令。

表 访问权限总结

对象类型所有权限默认 PUBLIC 权限uxsql 命令
DATABASECTcTc\l
DOMAINUU\dD+
FUNCTIONPROCEDUREXX\df+
FOREIGN DATA WRAPPERU\dew+
FOREIGN SERVERU\des+
LANGUAGEUU\dL+
LARGE OBJECTrw\dl+
PARAMETERsA\dconfig+
SCHEMAUC\dn+
SEQUENCErwU\dp
TABLE (及类似对象)arwdDxtL\dp
TABLE COLUMNarwx\dp
TABLESPACEC\db+
TYPEUU\dT+

已授予特定对象的权限显示为 aclitem 条目列表,其中每个 aclitem描述了一个授权者授予给一个受让人的权限。例如,calvin=r*w/hobbes指定角色 calvin 具有由角色 hobbes 授予的可授予权限 SELECTr)和不可授予权限UPDATEw)。如果 calvin 也具有由不同授权者授予的相同对象的某些权限,则这些权限将显示为单独的aclitem 条目。在 aclitem 中,空的受让人字段表示 PUBLIC

例如,假设用户 miriam 创建了表 mytable 并执行了以下操作:

GRANT SELECT ON mytable TO PUBLIC;
GRANT SELECT, UPDATE, INSERT ON mytable TO admin;
GRANT SELECT (col1), UPDATE (col1) ON mytable TO miriam_rw;

然后,uxsql的\dp命令将显示:

=> \dp mytable
                                  Access privileges
 Schema |  Name   | Type  |   Access privileges   |   Column privileges   | Policies
--------+---------+-------+-----------------------+-----------------------+----------
 public | mytable | table | miriam=arwdDxt/miriam+| col1:                +|
        |         |       | =r/miriam            +|   miriam_rw=rw/miriam |
        |         |       | admin=arw/miriam      |                       |
(1 row)

如果给定对象的“访问权限”列为空,则表示该对象具有默认权限(即其相关系统目录中的权限条目为null)。默认权限始终包括所有者的所有权限,并且可以根据对象类型包括一些PUBLIC的权限,如上所述。第一个GRANTREVOKE对象将实例化默认权限(例如,miriam=arwdDxt/miriam),然后根据指定的请求修改它们。同样,仅对具有非默认权限的列显示条目。(注意:对于此目的,“默认权限”始终指对象类型的内置默认权限。受ALTER DEFAULT PRIVILEGES命令影响的对象始终显示具有包括ALTER效果的显式特权条目。)请注意,所有者的隐式授予权限未在访问权限显示中标记。仅当授予选项已明确授予某人时,才会出现*。

注解

ALTER权限对ALTER TABLE命令的不同功能会做不同的权限检查,以下表格中内容只有超级用户或表所有者才能执行成功。

表 ALTER TABLE功能说明

语句功能核心安全限制 / 说明
SET TABLESPACE更改表空间并移动数据文件非表 Owner 禁用。涉及底层物理文件操作。
INHERIT parent_table将目标表增加为指定父表的子表非表 Owner 禁用。影响继承层级关系。
NO INHERIT parent_table从父表的子女列表中移除目标表非表 Owner 禁用
OWNER TO更改表的拥有者非表 Owner 禁用。通常只有超级用户或原 Owner 可执行。
SET SCHEMA将表移动到另一个模式(Schema)非表 Owner 禁用。需同时拥有新 Schema 的 CREATE 权限。
ATTACH PARTITION将已有表挂载为分区非表 Owner 禁用。涉及分区表的元数据合并。
DETACH PARTITION分离指定分区非表 Owner 禁用。分离后该表变为普通独立表。
ADD/SET GENERATED更改或定义标识列(Identity)属性若涉及序列会报错。因过程含 CREATE/ALTER SEQUENCE,而 Sequence 必须由表 Owner 拥有
SET sequence_option RESTART修改标识列底层的序列值非 SEQUENCE 所有者禁用
ADD table_constraint增加约束(如 CHECK, NOT NULL若为 PK/Unique 需新建索引,此时严格检查执行者是否为表 Owner。
ADD ... USING INDEX基于已有索引增加 PK/Unique 约束成功后索引会重命名,并触发 NOTIFY 消息提醒索引更名。
DISABLE/ENABLE RULE配置重写规则(Rule)的触发设置涉及规则修改,需检查执行者是否为 Rule 的 Owner
ADD COLUMN增加新列同时检查 当前执行用户表 Owner 是否都有该数据类型的使用权限。
SET DATA TYPE更改列的数据类型同上,双重检查类型使用权限,且可能触发全表重写。
OF type_name将表链接到一种组合类型(Typed Table)同上,检查类型权限。

2.8.行安全策略

除了通过SQL标准权限系统(通过GRANT提供)外,表还可以具有行安全策略,该策略可以基于每个用户限制正常查询返回的行或可以由数据修改命令插入、更新或删除的行。此功能也称为行级安全性。默认情况下,表没有任何策略,因此如果用户根据SQL权限系统具有访问表的权限,则其中的所有行都可以查询或更新。

当在表上启用行安全性(使用ALTER TABLE ... ENABLE ROW LEVEL SECURITY),必须通过行安全策略允许选择行或修改行的所有正常访问。(但是,表的所有者通常不受行安全策略的限制。)如果表没有策略,则使用默认拒绝策略,这意味着没有行可见或可修改。适用于整个表的操作,例如TRUNCATEREFERENCES,不受行安全性的限制。

行安全策略可以特定于命令、角色或两者。可以指定策略适用于ALL命令、SELECTINSERTUPDATEDELETE。可以将多个角色分配给给定策略,并且正常的角色成员身份和继承规则适用。

要指定哪些行可见或可修改,需要一个返回布尔结果的表达式。在用户的查询中的任何条件或函数之前,将为每行计算此表达式。(唯一的例外是leakproof函数,它保证不会泄漏信息;优化器可能会选择在行安全性检查之前应用此类函数。)表达式不返回true的行将不会被处理。可以指定单独的表达式以提供对可见或可修改行的独立控制。可见的行和允许修改的行是由策略表达式在查询时运行并使用运行查询的用户的权限进行控制的,尽管安全定义函数可以用于访问对调用用户不可用的数据。

超级用户和具有BYPASSRLS属性的角色在访问表时始终会绕过行安全系统。表所有者通常也会绕过行安全,但表所有者可以选择使用ALTER TABLE ... FORCE ROW LEVEL SECURITY来受到行安全的约束。

启用和禁用行安全以及向表添加策略始终只有表所有者才有权限。

使用CREATE POLICY命令创建策略,使用ALTER POLICY命令修改策略,使用DROP POLICY命令删除策略。要为给定的表启用和禁用行安全,请使用ALTER TABLE命令。

每个策略都有一个名称,可以为一个表定义多个策略。由于策略是特定于表的,因此表的每个策略必须具有唯一名称。不同的表可以具有具有相同名称的策略。

当多个策略应用于给定的查询时,它们使用OR(对于宽松策略,这是默认值)或使用AND(对于限制性策略)进行组合。这类似于给定角色具有其所有成员角色的权限的规则。下面将进一步讨论宽松策略与限制性策略。

以下是一个简单的示例,演示如何在account关系上创建一个策略,以仅允许managers角色的成员访问行,且仅访问其账户的行:

CREATE TABLE accounts (manager text, company text, contact_email text);

ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;

CREATE POLICY account_managers ON accounts TO managers
    USING (manager = current_user);

上述策略隐含地提供了一个与其USING子句相同的WITH CHECK子句,以便约束适用于由命令选择的行(因此经理不能选择、更新或删除属于不同经理的现有行)和由命令修改的行(因此不能通过INSERTUPDATE创建属于不同经理的行)。

如果未指定角色或使用特殊用户名PUBLIC,则该策略适用于系统上的所有用户。要允许所有用户仅访问其在users表中的行,可以使用简单的策略:

CREATE POLICY user_policy ON users 
  USING (user_name = current_user);

这与前面的示例类似。

要为将添加到表中的行使用不同的策略,与可见行使用的策略不同,可以组合多个策略。这对策略将允许所有用户查看users表中的所有行,但仅修改自己的行:

CREATE POLICY user_sel_policy ON users
    FOR SELECT
    USING (true);
CREATE POLICY user_mod_policy ON users
    USING (user_name = current_user);

在SELECT命令中,这两个策略将组合起来。使用OR,其净效应是可以选择所有行。在其他命令类型中,仅适用第二个策略,因此效果与以前相同。行安全性也可以使用ALTER TABLE命令禁用。禁用行安全性不会删除在表上定义的任何策略;它们只是被忽略。然后,表中的所有行都是可见和可修改的,受标准SQL权限系统的约束。

以下是如何在生产环境中使用此功能的更大示例。表passwd模拟Unix密码文件:

-- Simple passwd-file based example
CREATE TABLE passwd (
  user_name             text UNIQUE NOT NULL,
  pwhash                text,
  uid                   int  PRIMARY KEY,
  gid                   int  NOT NULL,
  real_name             text NOT NULL,
  home_phone            text,
  extra_info            text,
  home_dir              text NOT NULL,
  shell                 text NOT NULL
);

CREATE ROLE admin;  -- Administrator
CREATE ROLE bob;    -- Normal user
CREATE ROLE alice;  -- Normal user

-- Populate the table
INSERT INTO passwd VALUES
  ('admin','xxx',0,0,'Admin','111-222-3333',null,'/root','/bin/dash');
INSERT INTO passwd VALUES
  ('bob','xxx',1,1,'Bob','123-456-7890',null,'/home/bob','/bin/zsh');
INSERT INTO passwd VALUES
  ('alice','xxx',2,1,'Alice','098-765-4321',null,'/home/alice','/bin/zsh');

-- Be sure to enable row-level security on the table
ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;

-- Create policies
-- Administrator can see all rows and add any rows
CREATE POLICY admin_all ON passwd TO admin USING (true) WITH CHECK (true);
-- Normal users can view all rows
CREATE POLICY all_view ON passwd FOR SELECT USING (true);
-- Normal users can update their own records, but
-- limit which shells a normal user is allowed to set
CREATE POLICY user_mod ON passwd FOR UPDATE
  USING (current_user = user_name)
  WITH CHECK (
    current_user = user_name AND
    shell IN ('/bin/bash','/bin/sh','/bin/dash','/bin/zsh','/bin/tcsh')
  );

-- Allow admin all normal rights
GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin;
-- Users only get select access on public columns
GRANT SELECT
  (user_name, uid, gid, real_name, home_phone, extra_info, home_dir, shell)
  ON passwd TO public;
-- Allow users to update certain columns
GRANT UPDATE
  (pwhash, real_name, home_phone, extra_info, shell)
  ON passwd TO public;

与任何安全设置一样,重要的是测试并确保系统的行为符合预期。使用上面的示例,这证明了权限系统正在正常工作。

-- admin can view all rows and fields
uxdb=> set role admin;
SET
uxdb=> table passwd;
 user_name | pwhash | uid | gid | real_name |  home_phone  | extra_info | home_dir    |   shell
-----------+--------+-----+-----+-----------+--------------+------------+-------------+-----------
 admin     | xxx    |   0 |   0 | Admin     | 111-222-3333 |            | /root       | /bin/dash
 bob       | xxx    |   1 |   1 | Bob       | 123-456-7890 |            | /home/bob   | /bin/zsh
 alice     | xxx    |   2 |   1 | Alice     | 098-765-4321 |            | /home/alice | /bin/zsh
(3 rows)

-- Test what Alice is able to do
uxdb=> set role alice;
SET
uxdb=> table passwd;
ERROR:  permission denied for relation passwd
uxdb=> select user_name,real_name,home_phone,extra_info,home_dir,shell from passwd;
 user_name | real_name |  home_phone  | extra_info | home_dir    |   shell
-----------+-----------+--------------+------------+-------------+-----------
 admin     | Admin     | 111-222-3333 |            | /root       | /bin/dash
 bob       | Bob       | 123-456-7890 |            | /home/bob   | /bin/zsh
 alice     | Alice     | 098-765-4321 |            | /home/alice | /bin/zsh
(3 rows)

uxdb=> update passwd set user_name = 'joe';
ERROR:  permission denied for relation passwd
-- Alice is allowed to change her own real_name, but no others
uxdb=> update passwd set real_name = 'Alice Doe';
UPDATE 1
uxdb=> update passwd set real_name = 'John Doe' where user_name = 'admin';
UPDATE 0
uxdb=> update passwd set shell = '/bin/xx';
ERROR:  new row violates WITH CHECK OPTION for "passwd"
uxdb=> delete from passwd;
ERROR:  permission denied for relation passwd
uxdb=> insert into passwd (user_name) values ('xxx');
ERROR:  permission denied for relation passwd
-- Alice can change her own password; RLS silently prevents updating other rows
uxdb=> update passwd set pwhash = 'abc';
UPDATE 1

到目前为止,所有构建的策略都是允许的策略,这意味着当应用多个策略时,它们使用“OR”布尔运算符进行组合。虽然可以构建允许访问预期情况下的行的允许策略,但将允许策略与限制策略(记录必须通过并使用“AND”布尔运算符组合)结合可能更简单。在上面的示例基础上,我们添加了一个限制策略,要求管理员通过本地Unix套接字连接才能访问passwd表的记录:

CREATE POLICY admin_local_only ON passwd AS RESTRICTIVE TO admin
    USING (ux_catalog.inet_client_addr() IS NULL);

然后,我们可以看到通过网络连接的管理员将看不到任何记录,因为存在限制策略:

=> SELECT current_user;
 current_user
--------------
 admin
(1 row)

=> select inet_client_addr();
 inet_client_addr
------------------
 127.0.0.1
(1 row)

=> TABLE passwd;
 user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell
-----------+--------+-----+-----+-----------+------------+------------+----------+-------
(0 rows)

=> UPDATE passwd set pwhash = NULL;
UPDATE 0

引用完整性检查,例如唯一或主键约束和外键引用,始终绕过行安全以确保数据完整性得到维护。在开发模式和行级策略时,必须注意避免通过这些引用完整性检查泄漏信息的“隐蔽通道”。

在某些情况下,确保不应用行安全非常重要。例如,在备份时,如果行安全默默地导致某些行被省略,则可能会造成灾难性后果。在这种情况下,可以将row_security配置参数设置为off。这本身并不绕过行安全;它的作用是如果任何查询的结果会被策略过滤,则抛出错误。然后可以调查并修复错误的原因。

在上面的示例中,策略表达式仅考虑要访问或更新的行中的当前值。这是最简单和最佳性能的情况;如果可能,最好设计行安全应用程序以按此方式工作。如果必须查询其他行或其他表以做出策略决策,则可以在策略表达式中使用子SELECT或包含SELECT的函数来完成。但是请注意,如果不小心,这样的访问可能会创建竞争条件,从而允许信息泄漏。例如,请考虑以下表设计:

-- 特权组定义
CREATE TABLE groups (group_id int PRIMARY KEY,
                     group_name text NOT NULL);

INSERT INTO groups VALUES
  (1, 'low'),
  (2, 'medium'),
  (5, 'high');

GRANT ALL ON groups TO alice;  -- alice是管理员
GRANT SELECT ON groups TO public;

-- 用户权限级别定义
CREATE TABLE users (user_name text PRIMARY KEY,
                    group_id int NOT NULL REFERENCES groups);

INSERT INTO users VALUES
  ('alice', 5),
  ('bob', 2),
  ('mallory', 2);

GRANT ALL ON users TO alice;
GRANT SELECT ON users TO public;

-- 保存需要保护的信息的表
CREATE TABLE information (info text,
                          group_id int NOT NULL REFERENCES groups);

INSERT INTO information VALUES
  ('barely secret', 1),
  ('slightly secret', 2),
  ('very secret', 5);

ALTER TABLE information ENABLE ROW LEVEL SECURITY;

-- 只有用户的安全组ID大于或等于行的group_id时,才能查看/更新行
CREATE POLICY fp_s ON information FOR SELECT
  USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));
CREATE POLICY fp_u ON information FOR UPDATE
  USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user));

-- 我们只依赖RLS来保护information表
GRANT ALL ON information TO public;

现在假设alice想要更改“稍微保密”信息,但决定不信任mallory,不让她看到该行的新内容,因此她执行了以下操作:

bEGIN;
UPDATE users SET group_id = 1 WHERE user_name = 'mallory';
UPDATE information SET info = 'secret from mallory' WHERE group_id = 2;
COMMIT;

看起来很安全,没有窗口让mallory能够看到“secret from mallory”字符串。然而,这里存在竞态条件。如果mallory同时执行以下操作:

SELECT * FROM information WHERE group_id = 2 FOR UPDATE;

并且她的事务处于READ COMMITTED模式,那么她有可能看到“secret from mallory”。如果她的事务在alice之后到达information行,它会阻塞等待alice的事务提交,然后通过FOR UPDATE子句获取更新的行内容。但是,它没有使用FOR UPDATEusers中获取更新的行,而是使用查询开始时的快照读取users行。因此,策略表达式测试了mallory特权级别的旧值,并允许她查看更新的行。

有几种方法可以解决这个问题。一个简单的答案是在行安全策略的子SELECT中使用SELECT ... FOR SHARE。但是,这需要向受影响的用户授予对引用表(这里是users)的UPDATE权限,这可能是不希望的。(但是可以应用另一个行安全策略来防止他们实际行使该权限;或者将子SELECT嵌入到安全定义函数中。)此外,在引用表的并发使用中,重度使用行共享锁可能会带来性能问题,特别是如果经常更新它。另一种解决方案是,如果更新引用表的频率很低,可以在更新时对引用表进行ACCESS EXCLUSIVE锁定,以便没有并发事务可以检查旧的行值。或者可以等待所有事务完成后再进行更新。并发事务在提交引用表的更新之后,以及在依赖于新安全情况的更改之前结束。

2.9.模式

一个UXDB数据库集群包含一个或多个命名数据库。角色和一些其他对象类型在整个集群中共享。客户端连接到服务器只能访问单个数据库,即连接请求中指定的数据库。

注意

集群的用户不一定有权访问集群中的每个数据库。角色名称的共享意味着在同一集群中的两个数据库中不能有不同的名为joe的角色;但系统可以配置为仅允许joe访问其中一些数据库。

一个数据库包含一个或多个命名的模式,这些模式又包含表。模式还包含其他类型的命名对象,包括数据类型、函数和运算符。同一对象名称可以在不同的模式中使用而不会冲突;例如,schema1myschema都可以包含名为mytable的表。与数据库不同,模式并没有严格分离:如果用户有权限,他们可以访问连接到的数据库中任何模式中的对象。

使用模式的原因有几个:

  • 允许许多用户使用一个数据库而不互相干扰。

  • 将数据库对象组织成逻辑组,使它们更易于管理。

  • 第三方应用程序可以放在单独的模式中,以避免与其他对象名称冲突。

模式类似于操作系统级别的目录,但模式不能嵌套。

2.9.1.创建模式

请使用CREATE SCHEMA命令。给模式一个自己选择的名称。例如:

CREATE SCHEMA  myschema; 

要在模式中创建或访问对象,请编写由模式名称和表名称组成的限定名称,中间用点号分隔:

Schema.table

这适用于任何需要表名称的地方,包括在以下章节中讨论的表修改命令和数据访问命令。(当前只讨论表,但是相同的思想也适用于其他类型的命名对象,例如类型和函数。)

实际上,还可以使用更通用的语法:

database.schema.table

但目前这只是为了符合SQL标准的形式。如果您编写数据库名称,则必须与您连接到的数据库相同。

因此,要在新模式中创建表,请使用:

CREATE TABLE myschema.mytable ( ... );

要删除一个空模式(其中所有对象都已删除),请使用:

dROP SCHEMA myschema;

要删除包含所有包含对象的模式,请使用:

dROP SCHEMA myschema CASCADE;

有关此背后的一般机制的描述,请参见依赖跟踪

通常,您将希望创建由其他人拥有的模式(因为这是将用户活动限制为明确定义的命名空间的方法之一)。其语法如下:

CREATE SCHEMA schema_name AUTHORIZATION user_name;

您甚至可以省略模式名称,在这种情况下,模式名称将与用户名相同。有关此如何有用的详细信息,请参见使用模式

以ux_开头的模式名称保留用于系统目的,不能由用户创建。

2.9.2.公共模式

在前面的部分中,我们创建了没有指定模式名称的表。默认情况下,这些表(以及其他对象)会自动放入名为“public”的模式中。每个新数据库都包含这样的模式。因此,以下内容是等效的:

CREATE TABLE products ( ... ); 

CREATE TABLE public.products ( ... );

2.9.3.模式搜索路径

限定名称写起来很麻烦,而且通常最好不要将特定的模式名称直接写入应用程序中。因此,表通常通过未限定名称来引用,该名称仅由表名称组成。系统通过遵循搜索路径来确定所需的表,该路径是要查找的模式列表。搜索路径中的第一个匹配表被认为是所需的表。如果在搜索路径中没有匹配项,则会报告错误,即使在数据库的其他模式中存在匹配表名称也是如此。

在不同模式中创建同名对象的能力使编写查询以每次精确引用相同对象变得复杂。它还打开了用户更改其他用户查询行为的潜力,无论是恶意还是意外。由于查询中未限定名称的普遍存在以及它们在UXDB内部的使用,将模式添加到search_path实际上信任所有具有该模式上的CREATE权限的用户。当运行普通查询时,能够在搜索路径的模式中创建对象的恶意用户可以控制并执行任意SQL函数,就像您执行它们一样。

搜索路径中命名的第一个模式称为当前模式。除了是第一个搜索的模式之外,如果CREATE TABLE命令没有指定模式名称,则它也是新表将被创建的模式。

要显示当前搜索路径,请使用以下命令:

SHOW search_path;

在默认设置中,返回:

 search_path
--------------
 "$user", public

第一个元素指定要搜索与当前用户同名的模式。如果不存在这样的模式,则忽略该条目。第二个元素是我们已经看到的public模式。

搜索路径中存在的第一个模式是创建新对象的默认位置。这就是默认情况下对象创建在public模式中的原因。当在任何其他上下文中引用对象时,如果没有模式限定符(表修改、数据修改或查询命令),则会遍历搜索路径,直到找到匹配的对象。因此,在默认配置中,任何未限定的访问都只能引用public模式。

要将我们的新模式放入路径中,我们使用:

SET search_path TO myschema,public;

(这里我们省略了 $user,因为我们暂时不需要它。)然后我们可以不带模式限定符访问表:

dROP TABLE mytable;

此外,由于 myschema 是路径中的第一个元素,因此默认情况下新对象将在其中创建。

我们也可以写成:

SET search_path TO myschema;

然后我们不再能够不带限定符访问public模式。除了默认存在之外,public模式没有任何特殊之处。它也可以被删除。

数据类型和函数名称的搜索路径与表名称的搜索路径相同。数据类型和函数名称的名称可以像表名一样进行限定。如果您需要在表达式中编写限定的操作符名称,则有一个特殊规定:您必须编写

oPERATOR(schema.operator)

这是为了避免语法歧义。例如:

SELECT 3 OPERATOR(ux_catalog.+) 4;

实际上,通常依赖于运算符的搜索路径,以避免编写如此丑陋的内容。

2.9.4.模式和权限

默认情况下,用户无法访问他们不拥有的模式中的任何对象。要允许这样做,模式的所有者必须授予模式的USAGE权限。默认情况下,每个人都在模式public上具有该权限。为了允许用户使用模式中的对象,可能需要授予其他适当的权限。

还可以允许用户在其他人的模式中创建对象。要允许这样做,需要授予模式的CREATE权限。在从UXDB 2122或更早版本升级的数据库中,每个人都在模式public上具有该权限。一些使用模式 要求撤销该权限:

REVOKE CREATE ON SCHEMA public FROM PUBLIC;

(第一个“public”是模式,第二个“public”表示“每个用户”。在第一个意义上,它是一个标识符,在第二个意义上,它是一个关键字,因此大小写不同;请回顾词法结构中的指南。)

2.9.5.系统目录模式

除了public和用户创建的模式外,每个数据库都包含一个ux_catalog模式,其中包含系统表和所有内置数据类型、函数和操作符。ux_catalog始终有效地成为搜索路径的一部分。如果在路径中没有明确命名,则在搜索路径的模式之前隐式搜索它。这确保了内置名称始终可以被找到。但是,如果您希望用户定义的名称覆盖内置名称,则可以将ux_catalog明确放置在搜索路径的末尾。

由于系统表名称以ux_开头,最好避免这样的名称,以确保如果将来的某个版本定义了与您的表相同的系统表,则不会发生冲突。(使用默认搜索路径,对于未限定的表名引用,将解析为系统表。)系统表将继续遵循以ux_开头的约定,因此只要用户避免使用ux_前缀,它们就不会与未限定的用户表名发生冲突。

2.9.6.使用模式

模式可以用于以多种方式组织数据。安全模式使用模式可以防止不受信任的用户更改其他用户查询的行为。当数据库不使用安全模式使用模式时,希望安全查询该数据库的用户将在每个会话开始时采取保护措施。具体来说,他们将通过将search_path设置为空字符串或以其他方式从search_path中删除非超级用户可写的模式来开始每个会话。默认配置可以轻松支持一些使用模式:

  • 限制普通用户使用用户专用模式。要实现这一点,首先发出REVOKE CREATE ON SCHEMA public FROM PUBLIC。然后,对于每个需要创建非临时对象的用户,创建一个与该用户名称相同的模式。请记住,默认搜索路径以$user开头,该路径解析为用户名。因此,如果每个用户都有一个单独的模式,则默认情况下他们将访问自己的模式。在已经登录不受信任的用户的数据库中采用此模式后,请考虑审核公共模式中是否存在与模式ux_catalog中的对象名称相似的对象。除非不受信任的用户是数据库所有者或持有CREATEROLE特权,否则此模式是安全的。如果数据库起源于UXDB 2122或更早版本的升级,则REVOKE是必不可少的。否则,默认配置将遵循此模式;普通用户只能创建临时对象,直到特权用户提供模式为止。

  • 通过修改UXDB.conf或发出ALTER ROLE ALL SET search_path ="$user",从默认搜索路径中删除公共模式。然后,授予在公共模式中创建的特权。只有限定名称才会选择公共模式对象。虽然限定表引用是可以的,但是调用公共模式中的函数将是不安全或不可靠的。如果在公共模式中创建函数或扩展名,请改用第一种模式。否则,像第一种模式一样,这是安全的,除非不受信任的用户是数据库所有者或持有CREATEROLE特权。

  • 保持默认搜索路径,并授予在公共模式中创建的特权。所有用户都会隐式访问公共模式。这模拟了模式不可用的情况,为非模式感知的世界提供了平稳的过渡。然而,这永远不是一个安全的模式。只有在数据库具有单个用户或少数相互信任的用户时才是可接受的。在从UXDB 2122或更早版本升级的数据库中,这是默认设置。

对于任何模式,要安装共享应用程序(由所有人使用的表,第三方提供的其他函数等),请将它们放入单独的模式中。记得授予适当的特权,以允许其他用户访问它们。然后,用户可以通过限定名称引用这些附加对象,或者根据需要将其他模式放入其搜索路径中。

2.9.7.可移植性

在SQL标准中,同一模式中的对象归不同用户所有的概念不存在。此外,一些实现不允许您创建名称与其所有者不同的模式。实际上,在仅实现标准中指定的基本模式支持的数据库系统中,模式和用户的概念几乎是等价的。因此,许多用户认为限定名称实际上由user_name.table_name组成。如果为每个用户创建每个用户模式,则UXDB将有效地执行此操作。

此外,在SQL标准中没有公共模式的概念。为了最大限度地符合标准,您不应使用公共模式。

当然,一些SQL数据库系统可能根本不实现模式,或通过允许(可能有限的)跨数据库访问来提供命名空间支持。如果您需要使用这些系统,则最大的可移植性是根本不使用模式。

2.10.继承

UXDB实现了表继承,这是数据库设计师的有用工具。(SQL:1999及以后版本定义了类型继承功能,与此处描述的功能在许多方面不同。)

我们从一个例子开始:假设我们正在构建一个城市的数据模型。每个省都有很多城市,但只有一个省会。我们想要能够快速检索任何特定省的省会城市。这可以通过创建两个表来实现,一个用于省省会,另一个用于不是省会的城市。但是,当我们想要查询有关城市的数据时,无论它是否是省会,会发生什么情况?继承功能可以帮助解决这个问题。我们定义capitals 表,使其继承自 cities

CREATE TABLE cities (
    name            text,
    population      float,
    elevation       int     -- in feet
);

CREATE TABLE capitals (
    state           char(2)
) INHERITS (cities);

在这种情况下,capitals表继承其父表cities的所有列。省会还有一个额外的列state,显示它们所属的省。

在 UXDB中,一个表可以从零个或多个其他表继承,查询可以引用一个表的所有行或一个表的所有行以及其后代表的所有行。后者是默认行为。例如,以下查询查找所有位于海拔超过500英尺的城市的名称,包括省省会:

SELECT name, elevation
    FROM cities
    WHERE elevation > 500;

给定 UXDB 教程中的示例数据,这将返回:

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953
 Madison   |       845

另一方面,下面的查询找到了所有非省首府、海拔超过500英尺的城市:

SELECT name, elevation
    FROM ONLY cities
    WHERE elevation > 500;

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953

这里的ONLY关键字表示查询仅适用于cities,而不适用于继承层次结构中cities下面的任何表。我们已经讨论过的许多命令,如SELECTUPDATEDELETE,都支持ONLY关键字。

您还可以在表名后面加上一个尾随的*,以明确指定包括后代表:

SELECT name, elevation
    FROM cities*
    WHERE elevation > 500;

写上 * 是不必要的,因为这种行为始终是默认的。然而,这种语法仍然支持与旧版本的兼容性,其中默认值可以更改。

在某些情况下,您可能希望知道特定行来自哪个表。每个表中都有一个名为 tableoid 的系统列,可以告诉您源表:

SELECT c.tableoid, c.name, c.elevation
FROM cities c
WHERE c.elevation > 500;

返回:

 tableoid |   name    | elevation
----------+-----------+-----------
   139793 | Las Vegas |      2174
   139793 | Mariposa  |      1953
   139798 | Madison   |       845

(如果您尝试复制此示例,则可能会获得不同的数字 OID。)通过与ux_class进行连接,您可以查看实际的表名:

SELECT p.relname, c.name, c.elevation
FROM cities c, ux_class p
WHERE c.elevation > 500 AND c.tableoid = p.oid;

返回:

 relname  |   name    | elevation
----------+-----------+-----------
 cities   | Las Vegas |      2174
 cities   | Mariposa  |      1953
 capitals | Madison   |       845

获得相同效果的另一种方法是使用regclass别名类型,它将以符号方式打印表OID:

SELECT c.tableoid::regclass, c.name, c.elevation
FROM cities c
WHERE c.elevation > 500;

继承不会自动将INSERTCOPY命令中的数据传播到继承层次结构中的其他表。在我们的示例中,以下INSERT语句将失败:

INSERT INTO cities (name, population, elevation, state)
VALUES ('Albany', NULL, NULL, 'NY');

我们可能希望数据以某种方式路由到capitals表,但这并不会发生:INSERT总是插入到指定的表中。在某些情况下,可以使用规则重定向插入。但是,对于上述情况,这并没有帮助,因为 cities 表不包含列state,因此在应用规则之前,该命令将被拒绝。

父表上的所有检查约束和非空约束都会自动继承到其子表中,除非使用NO INHERIT子句明确指定。其他类型的约束(唯一、主键和外键约束)不会继承。

一个表可以从多个父表继承,此时它具有多个父表。表继承是指子表从一个或多个父表继承其列定义。子表的定义中声明的任何列都将添加到这些列中。如果同一列名出现在多个父表中,或者同时出现在父表和子表的定义中,则这些列将被“合并”,以便子表中只有一个这样的列。要进行合并,列必须具有相同的数据类型,否则会引发错误。可继承的检查约束和非空约束也以类似的方式合并。因此,例如,如果任何一个列定义被标记为非空,则合并的列将被标记为非空。如果检查约束具有相同的名称,则将合并这些约束,如果它们的条件不同,则合并将失败。

通常在创建子表时使用CREATE TABLE语句的INHERITS子句来建立表继承。或者,可以使用ALTER TABLEINHERIT变体来添加新的父关系到已经以兼容方式定义的表中。为此,新的子表必须已经包含与父表的列具有相同名称和类型的列。它还必须包括与父表具有相同名称和检查表达式的检查约束。同样,可以使用ALTER TABLENO INHERIT变体从子表中删除继承链接。动态添加和删除继承链接可以在继承关系用于表分区时非常有用。

创建一个兼容的表,稍后将其作为新的子表的一种方便的方法是在CREATE TABLE中使用LIKE子句。这将创建一个与源表具有相同列的新表。如果在源表上定义了任何CHECK约束,则应指定INCLUDING CONSTRAINTS选项以使新的子表必须具有与父表匹配的约束才能被视为兼容。

只要任何子表存在,就不能删除其任何父表。如果从任何父表继承,则不能删除或更改子表的列或检查约束。如果要删除一个表及其所有后代,一种简单的方法是使用CASCADE选项删除父表,请参见依赖跟踪

ALTER TABLE将在继承层次结构中传播列数据定义和检查约束的任何更改。同样,在使用CASCADE选项时,只有在其他表依赖的列才能删除。ALTER TABLE遵循在CREATE TABLE期间应用的重复列合并和拒绝规则。

继承查询仅对父表执行访问权限检查。因此,例如,授予cities表UPDATE权限意味着在通过cities访问时也可以更新capitals表中的行。这保留了数据也在父表中的外观。但是,不能直接更新capitals表而不进行额外的授权。类似地,父表的行安全策略(参见 行安全策略)应用于继承查询中来自子表的行。如果查询中显式命名了子表,则仅应用子表的策略(如果有),并忽略其父表的任何策略。

外部表外部数据也可以作为继承层次结构的父表或子表,就像常规表一样。如果外部表是继承层次结构的一部分,则不支持外部表的任何操作也不支持整个层次结构。

2.10.1.注意事项

请注意,并非所有SQL命令都能在继承层次结构上工作。用于数据查询、数据修改或模式修改(例如SELECTUPDATEDELETE、大多数ALTER TABLE变体,但不包括INSERTALTER TABLE ...RENAME)的命令通常默认包括子表,并支持ONLY符号来排除它们。用于数据库维护和调整(例如REINDEXVACUUM)的命令通常仅适用于单个物理表,不支持递归继承层次结构。每个单独命令的行为在其参考页面(SQL命令)中有所记录。

继承功能的一个严重限制是索引(包括唯一约束)和外键约束仅适用于单个表,而不适用于其继承子表。这在外键约束的引用和被引用方面都是正确的。因此,在上述示例的术语中:

  • 如果我们声明cities.name为UNIQUEPRIMARY KEY,则这不会阻止capitals表中具有与cities中行重复的名称的行。而且这些重复的行默认情况下会在来自cities的查询中显示。实际上,默认情况下,capitals将没有唯一约束,因此可能包含多个具有相同名称的行。您可以向capitals添加唯一约束,但这不会防止与cities的重复。

  • 同样,如果我们指定cities.nameREFERENCES某个其他表,则此约束不会自动传播到capitals。在这种情况下,您可以通过手动向capitals添加相同的REFERENCES约束来解决它。

  • 指定另一个表的列REFERENCES cities(name)将允许另一个表包含城市名称,但不包含省会名称。这种情况没有很好的解决方法。

对于继承层次结构未实现的某些功能,已实现了声明性分区。在决定是否使用遗留继承进行分区时需要格外小心,以确保其对您的应用程序有用。

2.11.表分区

UXDB支持基本的表分区。本节介绍为什么以及如何将分区作为数据库设计的一部分实现。

2.11.1.概述

分区是指将逻辑上的一个大表拆分成较小的物理部分。分区可以提供多种好处:

  • 在某些情况下查询性能能够显著提升,特别是当那些访问压力大的行在一个分区或者少数几个分区时。划分可以取代索引的主导列、减小索引尺寸以及使索引中访问压力大的部分更有可能被放在内存中。

  • 当查询或更新访问一个分区的大部分行时,可以通过该分区上的一个顺序扫描来取代分散到整个表上的索引和随机访问,这样可以改善性能。

  • 如果批量操作的需求是在分区设计时就规划好的,则批量装载和删除可以通过增加或者去除分区来完成。执行ALTER TABLE DETACH PARTITION或者使用DROP TABLE删除一个分区远快于批量操作。这些命令也完全避免了批量DELETE导致的VACUUM开销。

  • 很少使用的数据可以被迁移到便宜且较慢的存储介质上。

当一个表非常大时,划分所带来的好处是非常值得的。一个表何种情况下会从划分获益取决于应用,一个经验法则是当表的尺寸超过了数据库服务器物理内存时,划分会为表带来好处。

UXDB对下列分区形式提供了内建支持:

范围划分

表被根据一个关键列或一组列划分为“范围”,不同的分区的范围之间没有重叠。例如,我们可以根据日期范围划分,或者根据特定业务对象的标识符划分。

列表划分

通过显式地列出每一个分区中出现的键值来划分表。

哈希分区

通过为每个分区指定模数和余数来对表进行分区。每个分区所持有的行都满足:分区键的值除以为其指定的模数将产生为其指定的余数。

如果应用需要使用上面所列之外的分区形式,可以使用诸如继承和UNION ALL视图之类的替代方法。这些方法很灵活,但是却缺少内建声明式分区的一些性能优势。

2.11.2.声明式分区

UXDB提供了一种方法指定如何把一个表划分成称为分区的片段。被划分的表被称作分区表。这种说明由分区方法以及要被用作分区键的列或者表达式列表组成。

分区表本身是一个“虚拟”表,没有自己的存储空间。相反,存储属于分区,这些分区是与已分区表关联的普通表。每个分区存储由其分区边界定义的数据子集。根据分区键列的值,插入到分区表中的所有行都将被路由到适当的分区之一。如果一行的分区键不再满足其原始分区的分区界限,则更新该行的分区键将导致将其移动到另一个分区中。

分区本身也可能被定义为分区表,这种用法被称为子分区。分区可以有自己的与其他分区不同的索引、约束以及默认值。创建分区表及分区的更多细节请见《优炫数据库管理系统SQL命令手册》中的CREATE TABLE

无法把一个常规表转换成分区表,反之亦然。不过,可以把一个包含数据的常规表或者分区表作为分区加入到另一个分区表,或者从分区表中移走一个分区并且把它变成一个独立的表。有关ATTACH PARTITIONDETACH PARTITION子命令的内容请见《优炫数据库管理系统SQL命令手册》中的ALTER TABLE

分区也可以是外部表,不过需要特别注意,因为外部表的内容满足分区规则是用户的责任。还有一些其他的限制。有关更多信息,请参见《优炫数据库管理系统SQL命令手册》中的CREATE FOREIGN TABLE

2.11.2.1.示例

假设我们正在为一家大型冰淇淋公司建立一个数据库。该公司每天测量最高气温以及每个地区的冰淇淋销售情况。从概念上讲,我们想要的表格类似于:

CREATE TABLE measurement (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
);

我们知道大部分查询只会访问上周的、上月的或者上季度的数据,因为这个表的主要用途是为管理层准备在线报告。为了减少需要被存放的旧数据量,我们决定只保留最近3年的数据。在每个月的开始我们将去除掉最早的那个月的数据。在这种情况下我们可以使用分区技术来帮助我们满足对measurement表的所有不同需求。

要在这种情况下使用声明式分区,可采用下面的步骤:

  1. 通过指定PARTITION BY子句将度量表创建为分区表,该子句包括分区方法(本例中为RANGE)和用作分区键的列列表。

    CREATE TABLE measurement (
      city_id         int not null,
      logdate         date not null,
      peaktemp        int,
      unitsales       int
    ) PARTITION BY RANGE (logdate);
    
  2. 创建分区。每个分区的定义必须指定与父分区方法和分区键对应的边界。注意,指定边界使新分区的值与一个或多个现有分区中的值重叠将导致错误。

    这样创建的分区在各个方面都是普通的UXDB表(或者可能是外部表)。可以分别为每个分区指定表空间和存储参数。

    对于我们的示例,每个分区应该保存一个月的数据,以满足一次删除一个月的数据的要求。所以这些命令可能看起来像

    CREATE TABLE measurement_y2006m02 PARTITION OF measurement
      FOR VALUES FROM ('2006-02-01') TO ('2006-03-01');
    
    CREATE TABLE measurement_y2006m03 PARTITION OF measurement
      FOR VALUES FROM ('2006-03-01') TO ('2006-04-01');
    
    ...
    CREATE TABLE measurement_y2007m11 PARTITION OF measurement
      FOR VALUES FROM ('2007-11-01') TO ('2007-12-01');
    
    CREATE TABLE measurement_y2007m12 PARTITION OF measurement
      FOR VALUES FROM ('2007-12-01') TO ('2008-01-01')
      TABLESPACE fasttablespace;
    
    CREATE TABLE measurement_y2008m01 PARTITION OF measurement
      FOR VALUES FROM ('2008-01-01') TO ('2008-02-01')
      WITH (parallel_workers = 4)
      TABLESPACE fasttablespace;
    

    (回想一下,相邻分区可以共享一个绑定值,因为范围上限被视为独占边界。)

    为了实现子分区,在创建分区的命令中指定PARTITION BY子句,例如:

    CREATE TABLE measurement_y2006m02 PARTITION OF measurement
      FOR VALUES FROM ('2006-02-01') TO ('2006-03-01')
      PARTITION BY RANGE (peaktemp);
    

    在创建了measurement_y2006m02的分区之后,任何被插入到measurement中且被映射到measurement_y2006m02的数据(或者直接被插入到measurement_y2006m02的数据,假定它满足这个分区的分区约束)将被基于peaktemp列进一步重定向到measurement_y2006m02的一个分区。指定的分区键可以与父亲的分区键重叠,不过在指定子分区的边界时要注意它接受的数据集合是分区自身边界允许的数据集合的一个子集,系统不会尝试检查事情情况是否如此。

    在父表中插入未映射到某个现有分区的数据将导致错误;必须手动添加适当的分区。

    不需要手动创建描述分区的分区边界条件的表约束。这些约束将自动创建。

  3. 在已分区表的键列上创建索引,以及可能需要的任何其他索引。(从严格意义上讲,关键索引并非必要,但在大多数情况下,它是有帮助的。)这会自动在每个分区上创建一个匹配的索引,以后创建或附加的任何分区也将具有这样的索引。在已分区表上声明的索引或唯一约束是“虚拟的”,其方式与已分区表相同: 实际数据位于各个分区表的子索引中。

    CREATE INDEX ON measurement (logdate);
    
  4. 确保enable_partition_pruning配置参数在UXDB.conf中没有被禁用。如果被禁用,查询将不会按照想要的方式被优化。

在上面的例子中,我们每个月都要创建一个新的分区,因此编写一个自动生成所需DDL的脚本可能是明智的。

2.11.2.2.分区维护

通常在初始定义分区表时建立的分区并非保持静态不变。移除旧分区的数据并且为新数据周期性地增加新分区的需求比比皆是。分区的最大好处之一就是可以通过操纵分区结构来近乎瞬时地执行这类让人头痛的任务,而不是物理地去除大量数据。

移除旧数据最简单的选择是删除掉不再需要的分区:

DROP TABLE measurement_y2006m02;

这可以非常快地删除数百万行记录,因为它不需要逐个删除每个记录。不过要注意上面的命令需要在父表上拿到ACCESS EXCLUSIVE锁。

另一个通常更可取的选择是从已分区表中删除分区,但保留对其作为表的访问权限。这有两种形式:

ALTER TABLE measurement DETACH PARTITION measurement_y2006m02;
ALTER TABLE measurement DETACH PARTITION measurement_y2006m02 CONCURRENTLY;

这些操作允许在删除数据之前对数据执行进一步的操作。例如,这通常是使用COPY、 ux_dump或类似工具备份数据的有用时间。将数据聚合成更小的格式、执行其他数据操作或运行报告也可能是一个有用的时间。命令的第一种形式要求在父表上使用ACCESSEXclusIVE锁。在第二个表单中添加CONCURRENTLY 限定符允许分离操作只需要父表上的SHARE UPDATE EXclusIVE锁,但是有关限制的详细信息,请参见《优炫数据库管理系统SQL命令手册》中的ALTER TABLE... DETACH PARTITION

类似地,我们可以增加一个新分区来处理新数据。我们可以在分区表中创建一个空分区,就像上面创建的初始分区那样:

CREATE TABLE measurement_y2008m02 PARTITION OF measurement
    FOR VALUES FROM ('2008-02-01') TO ('2008-03-01')
    TABLESPACE fasttablespace;

作为一种替代方法,有时在分区结构之外创建新表,并在以后使其成为适当的分区更为方便。这允许在新数据出现在分区表中之前加载、检查和转换它。CREATE TABLE... LIKE选项有助于避免冗长地重复父表的定义:

  CREATE TABLE measurement_y2008m02
    (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS)
    TABLESPACE fasttablespace;

  ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
    CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );

  \copy measurement_y2008m02 from 'measurement_y2008m02'
  -- possibly some other data preparation work

  ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
      FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );
  
  CREATE TABLE measurement_y2008m02
    (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS)
    TABLESPACE fasttablespace;

  ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
    CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );

  \copy measurement_y2008m02 from 'measurement_y2008m02'
  -− possibly some other data preparation work

  ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02
      FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );

ATTACH PARTITION命令要求在分区表上接受SHARE UPDATE EXCLUSIVE锁。

在运行ATTACH PARTITION命令之前,建议对要附加的表创建与预期分区约束匹配的CHECK约束,如上所示。这样,系统就可以跳过扫描,否则就需要扫描来验证隐式分区约束。如果没有CHECK约束,则将扫描该表以验证分区约束,同时在该分区上持有ACCESSEXclusIVE锁。建议在ATTACH PARTITION完成之后删除现在已经冗余的CHECK约束。如果附加的表本身是一个分区表,那么它的每个子分区将被递归地锁定和扫描,直到遇到合适的CHECK约束或到达叶分区。

类似地,如果已分区表具有DEFAULT分区,则建议创建一个CHECK约束,该约束排除要附加的分区的约束。如果没有这样做,那么将扫描DEFAULT分区,以验证它是否包含应该位于所附加的分区中的任何记录。这个操作将在持有DEFAULT分区上的ACCESSEXclusIVE锁的同时执行。如果DEFAULT分区本身是一个分区表,那么它的每个分区都将以与所附加的表相同的方式递归检查,如上所述。

如上所述,可以在分区的表上创建索引,并自动将其应用于整个层次结构。这非常便利,因为不仅现有分区将变为索引,而且将来创建的任何分区都将变为索引。一个限制是,在创建这样一个分区索引时,不可能同时使用CONCURRENTLY限定符。为了克服长时间锁,可以对分区表使用CREATE INDEX ON ONLY;这样的索引被标记为无效,并且分区不会自动应用该索引。分区上的索引可以使用CONCURRENTLY分别的创建。然后使用ALTER INDEX .. ATTACH PARTITIONattached到父索引。一旦所有分区的索引附加到父索引,父索引将自动标记为有效。例如:

CREATE INDEX measurement_usls_idx ON ONLY measurement (unitsales);

CREATE INDEX measurement_usls_200602_idx
    ON measurement_y2006m02 (unitsales);
ALTER INDEX measurement_usls_idx
    ATTACH PARTITION measurement_usls_200602_idx;
...

该技术也可以与UNIQUEPRIMARY KEY约束一起试用;当创建约束时隐式创建索引。例如:

ALTER TABLE ONLY measurement ADD UNIQUE (city_id, logdate);

ALTER TABLE measurement_y2006m02 ADD UNIQUE (city_id, logdate);
ALTER INDEX measurement_city_id_logdate_key
    ATTACH PARTITION measurement_y2006m02_city_id_logdate_key;
...
2.11.2.3.限制

分区表有以下限制:

  • 要在分区表上创建唯一或主键约束,分区键不能包括任何表达式或函数调用,约束的列必须包括所有分区键列。这个限制存在是因为构成约束的单个索引只能在其自己的分区内直接强制执行唯一性;因此,分区结构本身必须保证不会在不同分区中出现重复。

  • 无法创建跨整个分区表的排除约束。只能在每个叶子分区上放置这样的约束。同样,这个限制源于无法强制执行跨分区限制。

  • INSERT上的BEFORE ROW触发器无法更改新行的最终目标分区。

  • 不允许在同一分区树中混合临时和永久关系。因此,如果分区表是永久的,则其分区也必须是永久的,反之亦然。使用临时关系时,分区树的所有成员都必须来自同一个会话。

各个分区通过使用分区表链接到它们。继承背后的细节。然而,使用声明性分区表或其分区时,不能使用继承的所有通用特性,如下所述。特别地,分区不能有除其所属的分区表之外的任何父级,也不能从分区表和常规表都继承。这意味着分区表及其分区永远不会与常规表共享继承层次结构。

由于由分区表及其分区组成的分区层次结构仍然是继承层次结构,tableoid和所有正常的继承规则都适用,如继承所述,但有一些例外:

  • 分区不能有父级中不存在的列。不能在创建分区时指定列,也不能使用ALTER TABLE在事后向分区添加列。只有当表的列与父级完全匹配时,才能使用ALTER TABLE ... ATTACH PARTITION将表添加为分区。

  • 分区表的CHECKNOT NULL约束始终被其所有分区继承。标记为NO INHERITCHECK约束不允许在分区表上创建。如果分区表中存在相同的约束,则无法删除分区的列上的NOT NULL约束。

  • 只要没有分区存在,就支持仅在分区表上添加或删除约束。一旦存在分区,使用ONLY将导致错误。相反,可以在分区本身上添加约束(如果它们不存在于父级表中)并删除它们。

  • 由于分区表本身没有任何数据,因此尝试在分区表上使用TRUNCATE ONLY将始终返回错误。

2.11.3.使用继承进行分区

虽然内置的声明性分区适用于大多数常见用例,但在某些情况下,更灵活的方法可能会有用。可以使用表继承来实现分区,这允许使用声明性分区不支持的几个特性,例如:

  • 对于声明性分区,分区必须具有与分区表完全相同的列集,而使用表继承,子表可以具有父表中不存在的额外列。

  • 表继承允许多重继承。

  • 声明性分区仅支持范围、列表和哈希分区,而表继承允许按用户选择的方式划分数据。(但是,请注意,如果约束排除无法有效地修剪子表,则查询性能可能很差。)

2.11.3.1.例子

此示例构建了一个等效于上面声明性分区示例的分区结构。请按以下步骤操作:

  1. 创建“root”表,所有“child”表都将从中继承。该表不包含任何数据。除非您希望将其应用于所有子表,否则不要在此表上定义任何检查约束。也没有必要在其上定义任何索引或唯一约束。对于我们的示例,根表是最初定义的measurement表:

    CREATE TABLE measurement (
        city_id         int not null,
        logdate         date not null,
        peaktemp        int,
        unitsales       int
    );
    
  2. 创建几个“child”表,每个表都从根表继承。通常,这些表不会向从根表继承的集合中添加任何列。与声明性分区一样,这些表在每个方面都是普通的UXDB表(或外部表)。

    CREATE TABLE measurement_y2006m02 () INHERITS (measurement);
    CREATE TABLE measurement_y2006m03 () INHERITS (measurement);
    ...
    CREATE TABLE measurement_y2007m11 () INHERITS (measurement);
    CREATE TABLE measurement_y2007m12 () INHERITS (measurement);
    CREATE TABLE measurement_y2008m01 () INHERITS (measurement);
    
  3. 向子表添加不重叠的表约束,以定义每个表中允许的键值。

    典型的例子包括:

    CHECK ( x = 1 )
    CHECK ( county IN ( 'Oxfordshire', 'Buckinghamshire', 'Warwickshire' ))
    CHECK ( outletID >= 100 AND outletID < 200 )
    

    确保约束保证在不同的子表中允许的键值之间没有重叠。一个常见的错误是设置如下的范围约束:

    CHECK ( outletID BETWEEN 100 AND 200 )
    CHECK ( outletID BETWEEN 200 AND 300 )
    

    这是错误的,因为不清楚键值200属于哪个子表。相反,应以以下方式定义范围:

    CREATE TABLE measurement_y2006m02 (
    CHECK ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2006m03 (
    CHECK ( logdate >= DATE '2006-03-01' AND logdate < DATE '2006-04-01' )
    ) INHERITS (measurement);
    
    ...
    CREATE TABLE measurement_y2007m11 (
    CHECK ( logdate >= DATE '2007-11-01' AND logdate < DATE '2007-12-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2007m12 (
    CHECK ( logdate >= DATE '2007-12-01' AND logdate < DATE '2008-01-01' )
    ) INHERITS (measurement);
    
    CREATE TABLE measurement_y2008m01 (
    CHECK ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
    ) INHERITS (measurement);
    
  4. 对于每个子表,创建键列的索引,以及您可能想要的任何其他索引。

    CREATE INDEX measurement_y2006m02_logdate ON measurement_y2006m02 (logdate);
    CREATE INDEX measurement_y2006m03_logdate ON measurement_y2006m03 (logdate);
    CREATE INDEX measurement_y2007m11_logdate ON measurement_y2007m11 (logdate);
    CREATE INDEX measurement_y2007m12_logdate ON measurement_y2007m12 (logdate);
    CREATE INDEX measurement_y2008m01_logdate ON measurement_y2008m01 (logdate);
    
  5. 我们希望我们的应用程序能够说INSERT INTO measurement...,并将数据重定向到适当的子表中。我们可以通过将适当的触发器函数附加到根表来安排这样做。如果数据仅添加到最新的子表中,我们可以使用非常简单的触发器函数:

    CREATE OR REPLACE FUNCTION measurement_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
      INSERT INTO measurement_y2008m01 VALUES (NEW.*);
        RETURN NULL;
    END;
    $$
    LANGUAGE pluxsql;
    

    创建函数后,我们创建一个触发器,该触发器调用触发器函数:

    CREATE TRIGGER insert_measurement_trigger
        BEFORE INSERT ON measurement
        FOR EACH ROW EXECUTE FUNCTION measurement_insert_trigger();
    

    我们必须每个月重新定义触发器函数,以便始终插入到当前子表中。但是,触发器定义不需要更新。

    我们可能希望插入数据并使服务器自动定位应添加行的子表。我们可以使用更复杂的触发器函数,例如:

    CREATE OR REPLACE FUNCTION measurement_insert_trigger()
    RETURNS TRIGGER AS $$
    BEGIN
        IF ( NEW.logdate >= DATE '2006-02-01' AND
            NEW.logdate < DATE '2006-03-01' ) THEN
          INSERT INTO measurement_y2006m02 VALUES (NEW.*);
        ELSIF ( NEW.logdate >= DATE '2006-03-01' AND
                NEW.logdate < DATE '2006-04-01' ) THEN
          INSERT INTO measurement_y2006m03 VALUES (NEW.*);
        ...
        ELSIF ( NEW.logdate >= DATE '2008-01-01' AND
                NEW.logdate < DATE '2008-02-01' ) THEN
          INSERT INTO measurement_y2008m01 VALUES (NEW.*);
        ELSE
            RAISE EXCEPTION 'Date out of range.  Fix the measurement_insert_trigger() function!';
        END IF;
        RETURN NULL;
      END;
      $$
      LANGUAGE pluxsql;
    

    触发器定义与前面相同。注意,每个 IF 测试必须与其子表的CHECK约束完全匹配。

    虽然这个函数比单月的情况更复杂,但它不需要经常更新,因为可以在需要之前添加分支。

    注意

    实际上,如果大多数插入都进入最新的子项,最好先检查该子项。为简单起见,我们按照与本示例其他部分相同的顺序显示了触发器的测试。

    将插入重定向到适当的子表的另一种方法是在根表上设置规则而不是触发器。例如:

      CREATE RULE measurement_insert_y2006m02 AS
      ON INSERT TO measurement WHERE
          ( logdate >= DATE '2006-02-01' AND logdate < DATE '2006-03-01' )
      DO INSTEAD
        INSERT INTO measurement_y2006m02 VALUES (NEW.*);
      ...
      CREATE RULE measurement_insert_y2008m01 AS
      ON INSERT TO measurement WHERE
          ( logdate >= DATE '2008-01-01' AND logdate < DATE '2008-02-01' )
      DO INSTEAD
        INSERT INTO measurement_y2008m01 VALUES (NEW.*);
    

    规则的开销比触发器大得多,但开销是每个查询支付一次而不是每行一次,因此此方法对于批量插入情况可能是有利的。然而,在大多数情况下,触发方法将提供更好的性能。

    请注意,COPY忽略规则。如果要用于COPY插入数据,则需要复制到正确的子表中,而不是直接复制到根表中。COPY会触发触发器,所以如果你使用触发器方法就可以正常使用它。

    规则方法的另一个缺点是,如果规则集未涵盖插入日期,则没有简单的方法可以强制出错。数据将默默地进入根表。

  6. 确保constraint_exclusion配置参数在 中没有被禁用 uxsql.conf;否则可能会不必要地访问子表。

    正如我们所看到的,复杂的表层次结构可能需要大量的DDL。在上面的示例中,我们每个月都会创建一个新的子表,因此编写一个自动生成所需DDL的脚本可能是明智的做法。

2.11.3.2.继承分区的维护

要快速删除旧数据,只需删除不再需要的子表:

dROP TABLE measurement_y2006m02;

要从继承层次结构表中删除子表但保留对其作为独立表的访问权限:

ALTER TABLE measurement_y2006m02 NO INHERIT measurement;

要添加一个新的子表以处理新数据,请创建一个空的子表,就像上面创建原始子表一样:

CREATE TABLE measurement_y2008m02 (
    CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' )
) INHERITS (measurement);

或者,您可能希望在将其添加到表层次结构之前创建和填充新的子表。这可以允许在将其对父表的查询可见之前加载、检查和转换数据。

CREATE TABLE measurement_y2008m02
  (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS);
ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02
   CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' );
\copy measurement_y2008m02 from 'measurement_y2008m02'
-- possibly some other data preparation work
ALTER TABLE measurement_y2008m02 INHERIT measurement;
2.11.3.3.注意事项

使用继承实现分区的以下注意事项适用:

  • 没有自动验证所有CHECK约束是否相互排斥的方法。创建生成子表并创建和/或修改相关对象的代码比手动编写更安全。

  • 索引和外键约束适用于单个表而不是其继承子表,因此它们有一些需要注意的注意事项

  • 这里显示的方案假定行的键列的值永远不会更改,或者至少不会更改到需要将其移动到另一个分区。尝试执行这样的UPDATE将失败,因为CHECK约束。如果需要处理这种情况,可以在子表上放置适当的更新触发器,但这会使结构的管理变得更加复杂。

  • 如果使用手动的VACUUMANALYZE命令,请不要忘记需要单独在每个子表上运行它们。像这样的命令:

    ANALYZE measurement;
    

    只会处理根表。

  • 带有ON CONFLICT子句的INSERT语句不太可能按预期工作,因为ON CONFLICT操作仅在指定目标关系的唯一违规时才会执行,而不是其子关系。

  • 除非应用程序明确知道分区方案,否则需要触发器或规则将行路由到所需的子表。编写触发器可能很复杂,并且比声明性分区内部执行的元组路由要慢得多。

2.11.4.分区剪枝

分区剪枝是一种查询优化技术,可提高声明性分区表的性能。例如:

SET enable_partition_pruning = on;                 -- 默认设置
SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';

如果没有分区剪枝,上述查询将扫描measurement表的每个分区。启用分区剪枝后,规划器将检查每个分区的定义,并证明该分区不需要被扫描,因为它不可能包含任何满足查询WHERE子句的行。当规划器可以证明这一点时,它将从查询计划中排除(剪枝)该分区。

通过使用EXPLAIN命令和enable_partition_pruning配置参数,可以显示已剪枝和未剪枝分区的计划之间的差异。这种类型表的典型未优化计划如下所示:

SET enable_partition_pruning = off;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                    QUERY PLAN
-------------------------------------------------------------------​----------------
 Aggregate  (cost=188.76..188.77 rows=1 width=8)
   ->  Append  (cost=0.00..181.05 rows=3085 width=0)
         ->  Seq Scan on measurement_y2006m02  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2006m03  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
...
         ->  Seq Scan on measurement_y2007m11  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2007m12  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)
         ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
               Filter: (logdate >= '2008-01-01'::date)

一些或所有分区可能使用索引扫描而不是全表顺序扫描,但这里的重点是根本不需要扫描旧分区来回答此查询。当启用分区剪枝时,我们可以获得一个显着更便宜的计划,该计划将提供相同的答案:

SET enable_partition_pruning = on;
EXPLAIN SELECT count(*) FROM measurement WHERE logdate >= DATE '2008-01-01';
                                    QUERY PLAN
-------------------------------------------------------------------​----------------
 Aggregate  (cost=37.75..37.76 rows=1 width=8)
   ->  Seq Scan on measurement_y2008m01  (cost=0.00..33.12 rows=617 width=0)
         Filter: (logdate >= '2008-01-01'::date)

请注意,分区剪枝仅由分区键隐式定义的约束驱动,而不是由索引的存在驱动。因此,在关键列上定义索引并不是必需的。是否需要为给定分区创建索引取决于您是否预计扫描分区的查询通常会扫描分区的大部分还是只扫描一小部分。在后一种情况下,索引将有所帮助,但在前一种情况下则不会。

分区剪枝不仅可以在给定查询的规划期间执行,还可以在其执行期间执行。这很有用,因为它可以允许在子句包含表达式的情况下剪枝更多的分区,这些表达式的值在查询规划时未知,例如在PREPARE语句中定义的参数,使用从子查询获得的值,或在嵌套循环连接的内部使用参数化值。执行期间的分区剪枝可以在以下任何时间执行:

  • 在查询计划初始化期间。可以在此处执行分区剪枝,以针对在执行初始化阶段已知的参数值。在此阶段剪枝的分区将不会出现在查询的EXPLAINEXPLAIN ANALYZE中。可以通过观察EXPLAIN输出中的“Subplans Removed”属性来确定在此阶段删除的分区数。

  • 在查询计划的实际执行期间。也可以在此处执行分区剪枝,以使用仅在实际查询执行期间已知的值来删除分区。这包括来自子查询和执行时间参数的值,例如来自参数化嵌套循环连接的值。由于这些参数的值在查询执行期间可能会多次更改,因此每次执行参数之一被用于分区剪枝时都会执行分区剪枝。确定在此阶段是否剪枝了分区需要仔细检查EXPLAIN ANALYZE输出中的loops属性。对应于不同分区的子计划可能具有不同的值,具体取决于每个子计划在执行过程中被剪枝的次数。如果每次都被剪枝,则有些子计划可能显示为(never executed)

可以使用enable_partition_pruning设置禁用分区剪枝。

2.11.5.分区和约束排除

约束排除是一种类似于分区剪枝的查询优化技术。虽然它主要用于使用传统继承方法实现的分区,但它也可以用于其他目的,包括使用声明性分区。

约束排除的工作方式与分区剪枝非常相似,只是它使用每个表的CHECK约束——这就是它的名称——而分区剪枝使用表的分区边界,这仅在声明性分区的情况下存在。另一个区别是,约束排除仅在计划时间应用;没有尝试在执行时间删除分区。

约束排除使用CHECK约束的事实,使它与分区剪枝相比较慢,有时可以用作优势:因为约束可以定义甚至在声明性分区表上,除了它们的内部分区边界,约束排除可能能够从查询计划中省略其他分区。

constraint_exclusion的默认(和推荐)设置既不是on也不是off,而是一个中间设置称为partition,它仅在可能在继承分区表上工作的查询中应用该技术。on设置会导致规划器检查所有查询中的CHECK约束,即使是不太可能受益的简单查询。

以下注意事项适用于约束排除:

  • 约束排除仅在查询规划期间应用,而分区修剪可以在查询执行期间应用。

  • 约束排除仅在查询的WHERE子句包含常量(或外部提供的参数)时起作用。例如,与非不变函数(如CURRENT_TIMESTAMP)进行比较无法进行优化,因为规划器无法知道函数值可能在运行时落入哪个子表中。

  • 保持分区约束简单,否则规划器可能无法证明不需要访问子表。对于列表分区,请使用简单的等式条件,对于范围分区,请使用简单的范围测试,如前面的示例所示。一个好的经验法则是,分区约束应仅包含使用B树可索引运算符将分区列与常量进行比较,因为只允许在分区键中使用B树可索引列。

  • 在约束排除期间检查父表的所有子表上的所有约束,因此大量的子表可能会显著增加查询规划时间。因此,遗留的基于继承的分区将与最多一百个子表一起很好地工作;不要尝试使用数千个子表。

2.11.6.声明式分区的最佳实践

选择如何分区表应该仔细考虑,因为不良设计可能会对查询规划和执行的性能产生负面影响。

最关键的设计决策之一将是按哪个列或列分区数据。通常,最佳选择将是按最常出现在对分区表执行的查询的WHERE子句中的列或列集分区。与分区边界约束兼容的WHERE子句可用于修剪不需要的分区。但是,您可能会被迫做出其他决策,以满足PRIMARY KEYUNIQUE约束的要求。在计划分区策略时,还要考虑删除不需要的数据的因素。整个分区可以相当快速地分离,因此设计分区策略的方式可能有益,以便所有要一次删除的数据都位于单个分区中。

选择将表分成的目标分区数也是一个关键决策。分区不足可能意味着索引仍然太大,数据局部性仍然很差,这可能导致低缓存命中率。然而,将表分成太多分区也可能会引起问题。太多的分区可能意味着更长的查询规划时间和更高的内存消耗,无论是在查询规划还是执行期间,如下所述。在选择如何分区表时,还重要考虑未来可能发生的变化。例如,如果您选择每个客户一个分区,而您目前只有少量大客户,请考虑如果在几年后您发现自己有大量小客户的影响。在这种情况下,最好选择按哈希分区,并选择合理数量的分区,而不是尝试按列表分区,并希望客户数量不会增加到超出实际可分区数据的范围。

子分区可以用于进一步划分预计比其他分区更大的分区。另一个选择是使用多列分区键的范围分区。任何一种方法都很容易导致过多的分区,因此建议要有所节制。

在查询规划和执行期间考虑分区的开销非常重要。查询规划器通常能够很好地处理具有数千个分区的分区层次结构,前提是典型查询允许查询规划器修剪除少数分区之外的所有分区。当规划器执行分区修剪后,如果仍有更多的分区,规划时间会变长,内存消耗会增加。担心分区数量过多的另一个原因是,如果许多会话触及大量分区,服务器的内存消耗可能会显著增长,因为每个分区都需要将其元数据加载到触及它的每个会话的本地内存中。

对于数据仓库类型的工作负载,使用比 OLTP类型工作负载更多的分区可能是有意义的。通常,在数据仓库中,查询规划时间不是很重要,因为大部分处理时间都花费在查询执行期间。对于这两种类型的工作负载,重要的是要及早做出正确的决策,因为重新分区大量数据可能非常缓慢。模拟预期工作负载通常有助于优化分区策略。永远不要假设分区越多越好,也不要假设分区越少越好。

2.12.外部数据

UXDB 实现了 SQL/MED 规范的部分内容,允许您使用常规 SQL 查询访问存储在 UXDB之外的数据。这样的数据被称为外部数据。(请注意,这种用法不应与数据库中的外键混淆,外键是数据库中的一种约束类型。)

外部数据是通过外部数据包装器来访问的。外部数据包装器是一个库,可以与外部数据源通信,隐藏连接到数据源和从数据源获取数据的细节。有一些外部数据包装器作为contrib 模块提供。其他类型的外部数据包装器可能作为第三方产品提供。如果没有现有的外部数据包装器符合您的需求,您可以编写自己的外部数据包装器。

要访问外部数据,您需要创建一个外部服务器对象,该对象定义了如何根据其支持的外部数据包装器使用的选项集连接到特定的外部数据源。然后,您需要创建一个或多个外部表,它们定义了远程数据的结构。外部表可以像普通表一样在查询中使用,但是外部表在 UXDB服务器中没有存储。每当使用它时,UXDB都会要求外部数据包装器从外部源获取数据,或在更新命令的情况下将数据传输到外部源。

访问远程数据可能需要对外部数据源进行身份验证。这些信息可以由用户映射提供,它可以根据当前的UXDB角色提供额外的数据,例如用户名和密码。

2.13.其他数据库对象

表是关系数据库结构中的核心对象,因为它们保存数据。但是,在数据库中还存在许多其他类型的对象,可以创建这些对象以使数据的使用和管理更加高效或方便。虽然本章不讨论这些对象,但我们在此列出它们,以便您了解可能的情况:

  • 视图

  • 函数、过程和运算符

  • 数据类型和域

  • 触发器和重写规则

2.14.依赖跟踪

当您创建涉及许多带有外键约束、视图、触发器、函数等的复杂数据库结构时,您会隐式地创建对象之间的依赖关系网络。例如,具有外键约束的表依赖于它引用的表。

为了确保整个数据库结构的完整性,UXDB确保您不能删除其他对象仍然依赖的对象。例如,尝试删除我们在外键中考虑的产品表,而订单表依赖于它,将导致像这样的错误消息:

DROP TABLE products;

ERROR:  cannot drop table products because other objects depend on it
DETAIL:  constraint orders_product_no_fkey on table orders depends on table products
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

错误消息包含一个有用的提示:如果您不想麻烦地逐个删除所有相关对象,可以运行:

DROP TABLE products CASCADE;

所有相关对象都将被删除,以及任何依赖于它们的对象,递归地。在这种情况下,它不会删除订单表,它只会删除外键约束。它停在那里,因为没有任何对象依赖于外键约束。(如果您想检查 DROP ... CASCADE 将执行什么操作,请运行不带 CASCADEDROP 并阅读 DETAIL输出。)

几乎所有的 DROP 命令在 UXDB 中都支持指定 CASCADE。当然,可能的依赖关系的性质因对象类型而异。您还可以写 RESTRICT 而不是 CASCADE 来获得默认行为,即防止删除任何其他对象依赖的对象。

注意

根据 SQL 标准,DROP命令需要指定RESTRICTCASCADE。没有数据库系统实际上强制执行该规则,但是默认行为是RESTRICT还是CASCADE在不同的系统中有所不同。

如果DROP命令列出多个对象,则只有在指定组之外存在依赖项时才需要CASCADE。例如,当说到 DROP TABLE tab1,tab2时,从 tab2引用 tab1的外键的存在并不意味着需要CASCADE才能成功。

对于用户定义的函数,UXDB 跟踪与函数的外部可见属性相关联的依赖项,例如它的参数和结果类型,但不跟踪只能通过检查函数体才能知道的依赖项。举个例子,考虑一下这种情况:

CREATE TYPE rainbow AS ENUM ('red', 'orange', 'yellow',
                             'green', 'blue', 'purple');

CREATE TABLE my_colors (color rainbow, note text);

CREATE FUNCTION get_color_note (rainbow) RETURNS text AS
  'SELECT note FROM my_colors WHERE color = $1'
  LANGUAGE SQL;

UXDB将意识到get_color_note函数依赖于rainbow类型:删除该类型将强制删除该函数,因为其参数类型将不再定义。但是,UXDB不会认为get_color_note依赖于my_colors表,因此如果删除表,则不会删除函数。虽然这种方法存在缺点,但也有好处。如果表丢失,则该函数在某种意义上仍然有效,尽管执行它会导致错误;创建一个同名的新表将使函数再次工作。

2.15.隐藏列

2.15.1.rowid

UXDB数据库的rowid是为了兼容达梦rowid伪列所做功能适配,用来标识数据库基表中每一条记录的唯一键值。本次实现,还新增了数据类型:rowid。该类型是一个大范围整数的数字类型,其范围和功能等同bigint类型。

新增GUC参数:create_as_parallel。该参数的作用:控制在执行create ... as或者select ... into命令时,数据库可以并行的条件下,是否走并行。如果设置为off,表示为不并行,此时rowid功能正常;设置为on时,表示为走并行,此时无法保证rowid功能正常。该参数默认值为off

示例

---创建字段为rowid类型的表并插入数据查询
CREATE TABLE TEST(ID ROWID);
INSERT INTO TEST SELECT GENERATE_SERIES(1, 1000000);
SELECT * FROM TEST WHERE ID > 999995;
   ID    
---------
  999996
  999997
  999998
  999999
 1000000
(5 rows)

---创建普通表,查询rowid隐藏列
CREATE TABLE TEST2(ID INT);
INSERT INTO TEST2 SELECT GENERATE_SERIES(25, 30);
SELECT ROWID, * FROM TEST2;
 ROWID | ID 
-------+----
     1 | 25
     2 | 26
     3 | 27
     4 | 28
     5 | 29
     6 | 30
(6 rows)

---利用rowid隐藏列进行数据更新或删除
UPDATE TEST2 SET ID = 31 WHERE ROWID = 1;
DELETE FROM TEST2 WHERE ROWID = 6;
SELECT ROWID, * FROM TEST2;
 ROWID | ID 
-------+----
     2 | 26
     3 | 27
     4 | 28
     5 | 29
     1 | 31
(5 rows)

---利用rowid隐藏列进行数据查询
SELECT * FROM TEST2 WHERE ROWID BETWEEN 2 AND 5;
 ID 
----
 26
 27
 28
 29
(4 rows)
SELECT * FROM TEST2 WHERE ROWID >= 3;
 ID 
----
 27
 28
 29
(3 rows)

---创建分区表,查询rowid隐藏列
CREATE TABLE PATEST(DA DATE NOT NULL) PARTITION BY RANGE (DA);
CREATE TABLE PATEST_CH1 PARTITION OF PATEST FOR VALUES FROM ('2022/03/01') TO ('2022/04/01');
CREATE TABLE PATEST_CH2 PARTITION OF PATEST FOR VALUES FROM ('2022/04/01') TO ('2022/05/01');
INSERT INTO PATEST SELECT GENERATE_SERIES(TIMESTAMP'2022/03/01', TIMESTAMP'2022/04/30', '1 day');

SELECT ROWID, * FROM PATEST_CH1 LIMIT 5;
 ROWID |     DA     
-------+------------
     1 | 2022-03-01
     2 | 2022-03-02
     3 | 2022-03-03
     4 | 2022-03-04
     5 | 2022-03-05
(5 rows)
SELECT ROWID, * FROM PATEST_CH2 LIMIT 5;
 ROWID |     DA     
-------+------------
    32 | 2022-04-01
    33 | 2022-04-02
    34 | 2022-04-03
    35 | 2022-04-04
    36 | 2022-04-05
(5 rows)

---开启强制并行,create_as_parallel为默认值
set force_parallel_mode to on;
create table test(id int);
insert into test select generate_series(1, 5);
create table test1 as select * from test;

select rowid, * from test1;
 ROWID | ID 
-------+----
     1 |  1
     2 |  2
     3 |  3
     4 |  4
     5 |  5
(5 rows)

---开启强制并行,设置create_as_parallel为on
set force_parallel_mode to on;
set create_as_parallel to on;
create table test(id int);
insert into test select generate_series(1, 5);
select * into test1 from test;

select rowid, * from test1;
 ROWID | ID 
-------+----
     0 |  1
     0 |  2
     0 |  3
     0 |  4
     0 |  5
(5 rows)

3.数据操作

上一章讨论了如何创建表和其他结构来保存数据。现在是时候填充表格了。本章介绍如何插入、更新和删除表格数据。下一章将最终解释如何从数据库中提取您丢失的数据。

3.1.插入数据

当创建表时,它不包含任何数据。在数据库有用之前,首先要做的事情是插入数据。数据逐行插入。您也可以在单个命令中插入多行,但无法插入不完整的行。即使您只知道某些列值,也必须创建完整的行。

要创建新行,请使用INSERT命令。该命令需要表名和列值。例如,考虑数据定义中的产品表:

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric
);

插入行的示例命令如下:

INSERT INTO products VALUES (1, 'Cheese', 9.99);

数据值按表中列出现的顺序列出,用逗号分隔。通常,数据值将是文字(常量),但也允许标量表达式。

上述语法的缺点是您需要知道表中列的顺序。为避免这种情况,您还可以显式列出列。例如,以下两个命令与上面的命令具有相同的效果:

INSERT INTO products (product_no, name, price) VALUES (1, 'Cheese', 9.99);
INSERT INTO products (name, price, product_no) VALUES ('Cheese', 9.99, 1);

许多用户认为始终列出列名是一种良好的实践。

如果您没有所有列的值,可以省略其中一些列。在这种情况下,列将填充其默认值。例如:

INSERT INTO products (product_no, name) VALUES (1, 'Cheese');
INSERT INTO products VALUES (1, 'Cheese');

第二种形式是UXDB的扩展。它从左边开始用尽可能多的值填充列,其余的将被默认值填充。

为了清晰起见,您还可以显式请求默认值,用于单个列或整行:

INSERT INTO products (product_no, name, price) VALUES (1, 'Cheese', DEFAULT);
INSERT INTO products DEFAULT VALUES;

您可以在单个命令中插入多行:

INSERT INTO products (product_no, name, price) VALUES
(1, 'Cheese', 9.99),
(2, 'Bread', 1.99),
(3, 'Milk', 2.99);

还可以插入查询结果(可能是零行、一行或多行):

INSERT INTO products (product_no, name, price)
SELECT product_no, name, price FROM new_products
WHERE release_date = 'today';

这提供了 SQL 查询机制(查询)的全部功能,用于计算要插入的行。

提示

当同时插入大量数据时,请考虑使用COPY命令。它不像INSERT命令那样灵活,但更高效。

3.2.更新数据

已经存在于数据库中的数据的修改被称为更新。您可以更新单个行、表中的所有行或所有行的子集。每个列可以单独更新;其他列不受影响。

要更新现有行,请使用UPDATE命令。这需要三个信息:

  1. 要更新的表和列的名称

  2. 列的新值

  3. 要更新的行

回想一下数据定义中提到的,SQL通常不为行提供唯一标识符。因此,不总是可能直接指定要更新的行。相反,您指定行必须满足的条件才能进行更新。只有在表中有主键(无论您是否声明它)时,您才能通过选择与主键匹配的条件可靠地寻址单个行。图形数据库访问工具依赖于此事实,以允许您单独更新行。

例如,此命令将所有价格为5的产品更新为价格为10:

UPDATE products SET price = 10 WHERE price = 5;

这可能导致零个、一个或多个行被更新。尝试不匹配任何行的更新不是错误。

让我们详细看看该命令。首先是关键字UPDATE,后跟表名。像往常一样,表名可以是模式限定的,否则将在路径中查找。接下来是关键字SET,后跟列名、等号和新列值。新列值可以是任何标量表达式,而不仅仅是常量。例如,如果您想将所有产品的价格提高10%,可以使用:

UPDATE products SET price = price * 1.10;

如您所见,新值的表达式可以引用行中的现有值。我们还省略了WHERE子句。如果省略它,这意味着更新表中的所有行。如果存在,只有与WHERE条件匹配的行才会被更新。请注意,SET子句中的等号是赋值,而WHERE子句中的等号是比较,但这不会产生任何歧义。当然,WHERE条件不必是相等测试。许多其他运算符可用,但表达式需要评估为布尔结果。

您可以通过在SET子句中列出多个赋值来在UPDATE命令中更新多个列。例如:

UPDATE mytable SET a = 5, b = 3, c = 1 WHERE a > 0;

3.3.删除数据

到目前为止,我们已经解释了如何向表中添加数据以及如何更改数据。现在需要讨论如何删除不再需要的数据。与添加数据一样,只能整行添加数据,因此只能从表中删除整行。在前一节中,我们解释了SQL没有提供直接访问单个行的方法。因此,只能通过指定要删除的行必须匹配的条件来删除行。如果表中有主键,则可以指定确切的行。但是,您也可以删除与条件匹配的行组,或者一次删除表中的所有行。

使用DELETE命令删除行;语法与UPDATE命令非常相似。例如,要从产品表中删除所有价格为10 的行,请使用:

DELETE FROM products WHERE price = 10;

如果只写:

DELETE FROM products;

那么表中的所有行都将被删除!程序员要小心。

3.4.从修改的行返回数据

有时在操作修改的行时获取数据是很有用的。INSERTUPDATEDELETE命令都有一个可选的RETURNING子句来支持这一点。使用RETURNING避免执行额外的数据库查询来收集数据,特别是当否则很难可靠地识别修改的行时,这是非常有价值的。

RETURNING子句的允许内容与SELECT命令的输出列表相同(参见选择列表)。它可以包含命令目标表的列名,或使用这些列的值表达式。一个常见的简写是RETURNING*,它按顺序选择目标表的所有列。

INSERT中,RETURNING可用的数据是插入时的行。在简单的插入中,这并不是很有用,因为它只会重复客户端提供的数据。但是,在依赖计算默认值时,它非常方便。例如,当使用serial列提供唯一标识符时,RETURNING可以返回分配给新行的ID:

CREATE TABLE users (firstname text, lastname text, id serial primary key);

INSERT INTO users (firstname, lastname) VALUES ('Joe', 'Cool') RETURNING id;

RETURNING子句在INSERT ... SELECT中也非常有用。

UPDATE中,RETURNING可用的数据是修改后的行内容。例如:

UPDATE products SET price = price * 1.10
  WHERE price <= 99.99
  RETURNING name, price AS new_price;

DELETE中,RETURNING可用的数据是删除行的内容。例如:

dELETE FROM products
  WHERE obsoletion_date = 'today'
  RETURNING *;

如果目标表上有触发器,RETURNING可用的数据是由触发器修改后的行。因此,检查由触发器计算的列是RETURNING 的另一个常见用例。


4.查询

前面的章节介绍了如何创建表格,如何填充数据以及如何操作这些数据。现在我们终于讨论如何从数据库中检索数据。

4.1.概述

从数据库中检索数据或检索命令称为查询。在 SQL中,使用SELECT命令来指定查询。SELECT命令的一般语法为

[WITH with_queries] SELECT select_list FROM table_expression [sort_specification]

以下各节描述了选择列表、表达式和排序规范的详细信息。由于WITH查询是高级功能,因此最后处理它们。

简单的查询形式为:

SELECT * FROM table1;

假设有一个名为table1的表,此命令将从table1检索所有行和所有用户定义的列。(检索方法取决于客户端应用程序。例如,uxsql程序将在屏幕上显示ASCII艺术表,而客户端库将提供从查询结果中提取单个值的函数。)选择列表规范*表示表达式提供的所有列。选择列表还可以选择可用列的子集或使用列进行计算。例如,如果table1具有名为abc(以及其他可能的列),则可以进行以下查询:

SELECT a, b + c FROM table1;

(假设bc是数值数据类型)。有关更多详细信息,请参见选择列表

FROM table1是一个简单的表达式:它只读取一个表。通常,表达式可以是基本表、连接和子查询的复杂构造。但是,您也可以完全省略表达式并将SELECT命令用作计算器:

SELECT 3 * 4;

如果选择列表中的表达式返回不同的结果,则此方法更有用。例如,您可以这样调用函数:

SELECT random();

4.2.表表达式

表达式计算出一个表。表达式包含一个FROM子句,该子句可选择后跟WHEREGROUP BYHAVING子句。简单的表达式只是引用磁盘上的表,即所谓的基本表,但更复杂的表达式可以用于以各种方式修改或组合基本表。

表达式中的可选WHEREGROUP BYHAVING子句指定了在FROM子句中派生的表上执行的一系列连续转换。所有这些转换都产生一个虚拟表,该表提供传递给选择列表以计算查询输出行的行。

4.2.1.FROM 子句

FROM子句从以逗号分隔的表引用列表中给出的一个或多个其他表中派生一个表。

FROM table_reference [, table_reference [, ...]]

表引用可以是表名(可能带有模式限定符),也可以是子查询,或者是连接表达式(JOIN)。或者派生表,如子查询、JOIN构造或这些的复杂组合。如果在FROM子句中列出了多个表引用,则这些表会进行交叉连接(即形成它们的行的笛卡尔积;见下文)。FROM列表的结果是一个中间虚拟表,可以通过WHEREGROUP BYHAVING子句进行转换,最终是整个表达式的结果。

当表引用命名了一个表继承层次结构的父表时,该表引用会生成不仅是该表的行,还包括所有子孙表的行,除非关键字ONLY在表名之前。但是,该引用仅生成出现在命名表中的列,任何添加在子表中的列都会被忽略。

您可以在表名之前写ONLY,也可以在表名之后写*,以明确指定包括子孙表。现在,搜索子孙表始终是默认行为,因此没有真正的理由再使用此语法,但是为了与旧版本兼容而支持它。

4.2.1.1.连接表

连接表是根据特定连接类型的规则从两个其他(真实或派生)表派生出来的表。内部连接、外部连接和交叉连接都是可用的。连接表的一般语法是:

T1 join_type T2 [ join_condition ]

所有类型的连接都可以链接在一起或嵌套:T1和T2都可以是连接表。可以使用括号来控制连接顺序。在没有括号的情况下,JOIN子句从左到右嵌套。

连接类型

交叉连接

T1 CROSS JOIN T2

交叉连接对于T1和T2的每个可能的行组合(即笛卡尔积),连接表将包含由T1中的所有列后跟T2中的所有列组成的行。如果表分别具有N和M行,则连接表将具有N*M行。

FROM T1 CROSS JOIN T2等同于FROM T1 INNER JOIN T2 ON TRUE(见下文)。它也等同于FROM T1,T2。

注意

当出现两个以上的表时,这种后者的等价性不完全成立,因为JOIN的绑定比逗号更紧密。例如:FROM T1 CROSS JOIN T2 INNER JOIN T3 ON condition 不同于 FROM T1, T2 INNER JOIN T3 ON condition 因为在第一种情况下,condition可以引用 T1,但在第二种情况下则不行。

限定连接

T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON boolean_expression
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 USING ( join column list )
T1 NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2

所有形式中,INNEROUTER 是可选的。默认为 INNERLEFTRIGHTFULL 意味着外连接。

连接条件在 ONUSING 子句中指定,或者通过单词 NATURAL隐式指定。连接条件决定了来自两个源表的哪些行被认为是“匹配的”,详见下文。

限定连接的可能类型有:

  • INNER JOIN
    对于 T1 的每一行 R1,连接表中都有一行,该行与 T2 中满足与 R1 的连接条件的每一行相对应。

  • LEFT OUTER JOIN 首先执行内连接。然后,对于 T1 中与 T2 中任何行都不满足连接条件的每一行,都会添加一个带有 T2列中空值的连接行。因此,连接表对于 T1 中的每一行都至少有一行。

  • RIGHT OUTER JOIN 首先执行内连接。然后,对于 T2 中与 T1 中任何行都不满足连接条件的每一行,都会添加一个带有 T1列中空值的连接行。这是左连接的相反情况:结果表中将始终有 T2中的每一行。

  • FULL OUTER JOIN
    首先执行内连接。然后,对于 T1 中与 T2 中任何行都不满足连接条件的每一行,都会添加一个带有 T2 列中空值的连接行。此外,对于T2 中与 T1 中任何行都不满足连接条件的每一行,都会添加一个带有 T1列中空值的连接行。

ON子句是最常见的连接条件:它采用与WHERE子句相同的布尔值表达式。如果来自T1T2的一对行匹配,则ON表达式的计算结果为true。

USING子句是一种简写方式,允许您利用连接的两侧使用相同名称的连接列的特定情况。它采用逗号分隔的共享列名称列表,并形成一个连接条件,其中包括每个列的等式比较。例如,使用(a,b)连接T1T2会产生连接条件ON T1.a = T2.a AND T1.b = T2.b。

此外,JOIN USING的输出会抑制冗余列:没有必要打印匹配的两个列,因为它们必须具有相等的值。虽然JOIN ON会产生来自T1的所有列,然后是来自T2的所有列,但JOIN USING会为列对列表中的每个列对(按列出现的顺序)生成一个输出列,然后是来自T1的任何剩余列,最后是来自T2的任何剩余列。

最后,NATURALUSING的简写形式:它形成一个USING列表,其中包含两个输入表中出现的所有列名。与USING一样,这些列仅在输出表中出现一次。如果没有公共列名,则NATURAL JOIN的行为类似于JOIN ... ON TRUE,产生一个交叉产品连接。

注意

USING相对于连接的关系中的列更改是相对安全的,因为只有列出的列被组合。NATURAL要冒更大的风险,因为任何导致新匹配列名存在的关系模式更改都会导致连接将该新列合并。

为了将其组合在一起,假设我们有表t1

 num | name
-----+------
   1 | a
   2 | b
   3 | c

t2

 num | value
-----+-------
   1 | xxx
   3 | yyy
   5 | zzz

然后我们得到各种连接的以下结果:

=> SELECT * FROM t1 CROSS JOIN t2;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   1 | a    |   3 | yyy
   1 | a    |   5 | zzz
   2 | b    |   1 | xxx
   2 | b    |   3 | yyy
   2 | b    |   5 | zzz
   3 | c    |   1 | xxx
   3 | c    |   3 | yyy
   3 | c    |   5 | zzz
(9 rows)

=> SELECT * FROM t1 INNER JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   3 | c    |   3 | yyy
(2 rows)

=> SELECT * FROM t1 INNER JOIN t2 USING (num);
 num | name | value
-----+------+-------
   1 | a    | xxx
   3 | c    | yyy
(2 rows)

=> SELECT * FROM t1 NATURAL INNER JOIN t2;
 num | name | value
-----+------+-------
   1 | a    | xxx
   3 | c    | yyy
(2 rows)

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |   3 | yyy
(3 rows)

=> SELECT * FROM t1 LEFT JOIN t2 USING (num);
 num | name | value
-----+------+-------
   1 | a    | xxx
   2 | b    |
   3 | c    | yyy
(3 rows)

=> SELECT * FROM t1 RIGHT JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   3 | c    |   3 | yyy
     |      |   5 | zzz
(3 rows)

=> SELECT * FROM t1 FULL JOIN t2 ON t1.num = t2.num;
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |   3 | yyy
     |      |   5 | zzz
(4 rows)

使用 ON指定的连接条件也可以包含与连接无直接关系的条件。这对于某些查询可能很有用,但需要仔细考虑。例如:

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num AND t2.value = 'xxx';
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
   2 | b    |     |
   3 | c    |     |
(3 rows)

请注意,将限制条件放在 WHERE子句中会产生不同的结果:

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num WHERE t2.value = 'xxx';
 num | name | num | value
-----+------+-----+-------
   1 | a    |   1 | xxx
(1 row)

这是因为放置在 ON 子句中的限制在连接之前处理,而放置在 WHERE子句中的限制在连接之后处理。这对于内连接没有关系,但对于外连接非常重要。

4.2.1.2.表和列别名

可以为表和复杂表引用指定临时名称,以便在查询的其余部分中用于引用派生表。这称为表别名。

要创建表别名,请编写

FROM table_reference AS alias

FROM table_reference alias

AS 关键字是可选的噪声。alias可以是任何标识符。

表别名的典型应用是为长表名分配短标识符,以保持连接子句的可读性。例如:

SELECT * FROM some_very_long_table_name s JOIN another_fairly_long_name a ON s.id = a.num;

别名成为表引用的新名称,因为当前查询而言,不允许在查询的其他地方使用原始名称引用表。因此,这是无效的:

SELECT * FROM my_table AS m WHERE my_table.a > 5;    -- 错误

表别名主要是为了方便表示,但在将表连接到自身时必须使用它们,例如:

SELECT * FROM people AS mother JOIN people AS child ON mother.id = child.mother_id;

此外,如果表引用是子查询(参见子查询),则需要使用别名。

括号用于解决歧义。在以下示例中,第一条语句将别名b分配给my_table的第二个实例,但第二条语句将别名分配给连接的结果:

SELECT * FROM my_table AS a CROSS JOIN my_table AS b ...
SELECT * FROM (my_table AS a CROSS JOIN my_table) AS b ...

另一种形式的表别名为表的列以及表本身提供临时名称:

FROM table_reference [AS] alias ( column1 [, column2 [, ...]] )

如果指定的列别名少于实际表的列,则不会重命名剩余的列。此语法对于自连接或子查询特别有用。

当将别名应用于JOIN子句的输出时,别名会隐藏JOIN中原始名称。例如:

SELECT a.* FROM my_table AS a JOIN your_table AS b ON ...

是有效的SQL,但是:

SELECT a.* FROM (my_table AS a JOIN your_table AS b ON ...) AS c

不是有效的;表别名a在别名c之外不可见。

4.2.1.2.1.PARTITION
  • 功能

    在查询时使用partition关键字时,可以查询出指定分区名的分区表的内容,括号内查询的表为from中查询表的分区表,若两张表无关联则报错。

  • 函数

    SELECT * from table partition(partition_table_name)
    
  • 参数

    PARTITION参数说明

    名称描述
    表名 (partition_table_name) 或 表名字符串 ("partition_table_name")语法识别的分区表子表名称。注意: 仅支持对单个子表进行查询。
  • 支持模式

    该功能仅支持兼容模式。

4.2.1.3.子查询

指定派生表的子查询必须用括号括起来,并且必须分配一个表别名(如表和列别名所示)。例如:

FROM (SELECT * FROM table1) AS alias_name

此示例等效于FROM table1 AS alias_name。更有趣的情况是,当子查询涉及分组或聚合时,它们不能简化为普通的连接。

子查询也可以是VALUES列表:

FROM (VALUES ('anne', 'smith'), ('bob', 'jones'), ('joe', 'blow'))
     AS names(first, last)

同样,需要一个表别名。将别名分配给VALUES列表的列是可选的,但是这是一个好习惯。有关更多信息,请参见VALUES列表

4.2.1.4.表函数

表函数是生成由基本数据类型(标量类型)或复合数据类型(表行)组成的行集的函数。它们像表、视图或子查询一样在查询的FROM子句中使用。表函数返回的列可以以相同的方式包含在SELECTJOINWHERE子句中。作为表、视图或子查询的列。

表函数也可以使用 ROWS FROM 语法组合,结果以并行列返回;在这种情况下,结果行数为最大函数结果的行数,较小的结果用 null值填充以匹配。

function_call [WITH ORDINALITY] [[AS] table_alias [(column_alias [, ... ])]]
ROWS FROM( function_call [, ... ] ) [WITH ORDINALITY] [[AS] table_alias [(column_alias [, ... ])]]

如果指定了 WITH ORDINALITY 子句,则会添加一个额外的 bigint类型的列到函数结果列中。该列对函数结果集的行进行编号,从1 开始。(这是 UNNEST ... WITH ORDINALITY 的 SQL 标准语法的概括。)默认情况下,序号列称为ordinality,但可以使用 AS 子句将其分配给不同的列名。

特殊的表函数 UNNEST 可以使用任意数量的数组参数进行调用,并返回相应数量的列,就像分别对每个参数调用 UNNEST并使用 ROWS FROM结构组合一样。

uNNEST( array_expression [, ... ] ) [WITH ORDINALITY] [[AS] table_alias [(column_alias [, ... ])]]

如果未指定table_alias,则使用函数名作为表名;在 ROWS FROM() 结构的情况下,使用第一个函数的名称。

如果未提供列别名,则对于返回基本数据类型的函数,列名也与函数名相同。对于返回复合类型的函数,结果列获得类型的各个属性的名称。

一些例子:

CREATE TABLE foo (fooid int, foosubid int, fooname text);

CREATE FUNCTION getfoo(int) RETURNS SETOF foo AS $$
    SELECT * FROM foo WHERE fooid = $1;
 $$ LANGUAGE SQL;

SELECT * FROM getfoo(1) AS t1;

SELECT * FROM foo
    WHERE foosubid IN (
                        SELECT foosubid
                        FROM getfoo(foo.fooid) z
                        WHERE z.fooid = foo.fooid
                      );

CREATE VIEW vw_getfoo AS SELECT * FROM getfoo(1);

SELECT * FROM vw_getfoo;

在某些情况下,定义可以根据调用方式返回不同列集的表函数很有用。为了支持这一点,可以将表函数声明为返回没有 OUT 参数的伪类型record。当这样的函数在查询中使用时,必须在查询本身中指定预期的行结构,以便系统知道如何解析和计划查询。这个语法看起来像:

function_call [AS] alias (column_definition [, ... ])
function_call AS [alias] (column_definition [, ... ])
ROWS FROM( ... function_call AS (column_definition [, ... ]) [, ... ] )

当不使用ROWS FROM()语法时,column_definition列表替换了可以附加到FROM项的列别名列表;列定义中的名称用作列别名。当使用ROWS FROM()语法时,可以column_definition列表分别附加到每个成员函数;或者如果只有一个成员函数且没有WITH ORDINALITY子句,则可以在ROWS FROM()后写入column_definition列表,以代替列别名列表。

考虑以下示例:

SELECT *
  FROM dblink('dbname=mydb', 'SELECT proname, prosrc FROM ux_proc')
    AS t1(proname name, prosrc text)
WHERE proname LIKE 'bytea%';

dblink函数(dblink模块的一部分)执行远程查询。它声明返回record,因为它可能用于任何类型的查询。必须在调用查询中指定实际的列集,以便解析器知道例如*应该扩展到什么。

此示例使用ROWS FROM

SELECT *
FROM ROWS FROM
    (
        json_to_recordset('[{"a":40,"b":"foo"},{"a":"100","b":"bar"}]')
            AS (a INTEGER, b TEXT),
        generate_series(1, 3)
    ) AS x (p, q, s)
ORDER BY p;

  p  |  q  | s
-----+-----+---
  40 | foo | 1
 100 | bar | 2
     |     | 3

它将两个函数连接成单个FROM目标。json_to_recordset()被指示返回两个列,第一个是integer,第二个是text。直接使用generate_series()的结果。ORDER BY子句将列值按整数排序。

4.2.1.5.LATERAL子查询

FROM中出现的子查询可以在前面加上关键字LATERAL。这允许它们引用由前面的FROM项提供的列。(没有LATERAL,每个子查询都是独立评估的,因此不能交叉引用任何其他FROM项。)

FROM中出现的表函数也可以在前面加上关键字LATERAL,但对于函数来说,关键字是可选的;函数的参数在任何情况下都可以包含对前面的FROM项提供的列的引用。

LATERAL项可以出现在FROM列表的顶层,或者在JOIN树中。在后者中,LATERAL项必须出现在JOIN项之后,以便引用前面的FROM项。

以下示例使用LATERAL

SELECT * FROM foo, LATERAL (SELECT * FROM bar WHERE bar.id = foo.bar_id) ss;

这并不特别有用,因为它的结果与更常规的结果完全相同。

SELECT * FROM foo, bar WHERE bar.id = foo.bar_id;

当交叉引用列对于计算要连接的行是必要的时,LATERAL主要是有用的。一个常见的应用是为返回集提供参数值的函数。例如,假设vertices(polygon)返回多边形的顶点集,我们可以使用以下查询来识别存储在表中的多边形的紧密相邻的顶点:

SELECT p1.id, p2.id, v1, v2
FROM polygons p1, polygons p2,
     LATERAL vertices(p1.poly) v1,
     LATERAL vertices(p2.poly) v2
WHERE (v1 <-> v2) < 10 AND p1.id != p2.id;

这个查询也可以写成

SELECT p1.id, p2.id, v1, v2
FROM polygons p1 CROSS JOIN LATERAL vertices(p1.poly) v1,
     polygons p2 CROSS JOIN LATERAL vertices(p2.poly) v2
WHERE (v1 <-> v2) < 10 AND p1.id != p2.id;

或者其他几个等价的表达式。(如前所述,这个例子中LATERAL关键字是不必要的,但为了清晰起见,我们使用它。)

LATERAL子查询中LEFT JOIN通常特别方便,这样即使LATERAL子查询对它们不产生任何行,源行也会出现在结果中。例如,如果get_product_names()返回由制造商制造的产品的名称,但我们的表中有些制造商目前没有生产任何产品,我们可以像这样找出它们:

SELECT m.name
FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON true
WHERE pname IS NULL;

4.2.2.WHERE子句

WHERE子句的语法是

WHERE search_condition

其中search_condition是任何返回类型为boolean的值表达式(参见表达式)。

在处理FROM子句之后,每个派生虚拟表的行都会与搜索条件进行比较。如果条件的结果为true,则保留该行在输出表中,否则(即如果结果为false或null)则丢弃该行。搜索条件通常引用FROM子句中生成的表的至少一列;这不是必需的,但否则将生成笛卡尔积。WHERE子句将会相当无用。

注意

内连接的连接条件可以写在WHERE子句或JOIN子句中。例如,这些表达式是等价的:

FROM a, b WHERE a.id = b.id AND b.val > 5

和:

FROM a INNER JOIN b ON (a.id = b.id) WHERE b.val > 5

或者甚至:

FROM a NATURAL JOIN b WHERE b.val > 5

你使用哪个取决于个人风格。尽管在SQL标准中,但FROM子句中的JOIN语法可能在其他SQL数据库管理系统中不太可移植。对于外连接,没有选择:必须在FROM子句中完成。外连接的ONUSING子句与WHERE条件不等价,因为它会导致行的添加(对于不匹配的输入行)以及最终结果中行的删除。

以下是一些WHERE子句的示例:

SELECT ... FROM fdt WHERE c1 > 5

SELECT ... FROM fdt WHERE c1 IN (1, 2, 3)

SELECT ... FROM fdt WHERE c1 IN (SELECT c1 FROM t2)

SELECT ... FROM fdt WHERE c1 IN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10)

SELECT ... FROM fdt WHERE c1 BETWEEN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10) AND 100

SELECT ... FROM fdt WHERE EXISTS (SELECT c1 FROM t2 WHERE c2 > fdt.c1)

fdt是在FROM子句中派生的表。不满足WHERE子句的搜索条件的行将从fdt中删除。请注意标量子查询作为值表达式的使用。与任何其他查询一样,子查询可以使用复杂的表达式。还要注意如何在子查询中引用fdt。只有在c1也是子查询的派生输入表中的列名时,才需要将c1限定为fdt.c1。但是,即使不需要,限定列名也可以增加清晰度。此示例显示了外部查询的列命名范围如何延伸到其内部查询。

4.2.3.GROUP BYHAVING子句

在通过WHERE过滤后,派生输入表可能会被分组,使用GROUP BY子句,并使用HAVING子句消除组行。

SELECT select_list
    FROM ...
    [WHERE ...]
    GROUP BY grouping_column_reference [, grouping_column_reference]...

GROUP BY子句用于将表中具有相同值的所有列列在一起的行分组。列的列出顺序无关紧要。其效果是将具有相同值的每组行合并为一个组行,该组行表示组中的所有行。这样做是为了消除输出中的冗余和/或计算适用于这些组的聚合。例如:

=> SELECT * FROM test1;
 x | y
---+---
 a | 3
 c | 2
 b | 5
 a | 1
(4 rows)

=> SELECT x FROM test1 GROUP BY x;
 x
---
 a
 b
 c
(3 rows)

在第二个查询中,我们不能写成SELECT * FROM test1 GROUP BY x,因为没有单个值可以与每个组关联的列y。由于分组列在每个组中只有一个值,因此可以在选择列表中引用它们。

通常,如果表被分组,则未在GROUP BY中列出的列除了在聚合表达式中不能被引用。带有聚合表达式的示例如下:

=> SELECT x, sum(y) FROM test1 GROUP BY x;
 x | sum
---+-----
 a |   4
 b |   5
 c |   2
(3 rows)

这里sum是一个聚合函数,它在整个组中计算单个值。

提示

没有聚合表达式的分组实际上计算了列中的不同值集。这也可以使用DISTINCT子句实现(请参见DISTINCT)。

这是另一个例子:它计算每个产品的总销售额(而不是所有产品的总销售额):

SELECT product_id, p.name, (sum(s.units) * p.price) AS sales
    FROM products p LEFT JOIN sales s USING (product_id)
    GROUP BY product_id, p.name, p.price;

在此示例中,列product_idp.namep.price必须在GROUP BY子句中,因为它们在查询选择列表中被引用(请参见下文)。列s.units不必在GROUP BY列表中,因为它仅在聚合表达式(sum(...))中使用,该表达式表示产品的销售额。对于每个产品,查询返回有关产品的所有销售的摘要行。

如果产品表设置为,例如,product_id是主键,则在上面的示例中仅按product_id分组就足够了,因为名称和价格将是函数依赖于产品ID,因此对于每个产品ID组,将没有关于返回哪个名称和价格值的歧义。

在严格的SQL中,GROUP BY只能按源表的列分组,但UXDB将其扩展为还允许GROUP BY按选择列表中的列分组。也允许按值表达式而不是简单列名进行分组。

如果使用GROUP BY对表进行了分组,但只有某些组是感兴趣的,则可以使用HAVING子句,就像WHERE子句一样,从结果中消除组。语法如下:

SELECT select_list FROM ... [WHERE ...] GROUP BY ... HAVING boolean_expression

HAVING 子句中的表达式既可以引用分组表达式,也可以引用未分组表达式(这必然涉及聚合函数)。

例如:

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING sum(y) > 3;
 x | sum
---+-----
 a |   4
 b |   5
(2 rows)

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING x < 'c';
 x | sum
---+-----
 a |   4
 b |   5
(2 rows)

再举一个更现实的例子:

SELECT product_id, p.name, (sum(s.units) * (p.price - p.cost)) AS profit
    FROM products p LEFT JOIN sales s USING (product_id)
    WHERE s.date > CURRENT_DATE - INTERVAL '4 weeks'
    GROUP BY product_id, p.name, p.price, p.cost
    HAVING sum(p.price * s.units) > 5000;

在上面的例子中,WHERE 子句通过一个未分组的列选择行(该表达式仅对最近四周的销售有效),而 HAVING子句将输出限制为总销售额超过5000的组。请注意,聚合表达式不一定在查询的所有部分中都需要相同。

如果查询包含聚合函数调用,但没有 GROUP BY 子句,则仍会进行分组:结果是单个组行(或者可能根本没有行,如果单行然后被HAVING 消除)。如果它包含一个 HAVING 子句,即使没有任何聚合函数调用或 GROUP BY 子句,也是如此。

4.2.4.GROUPING SETSCUBEROLLUP

使用分组集的概念可以进行比上述更复杂的分组操作。由 FROMWHERE子句选择的数据将按每个指定的分组集分别分组,为每个组计算聚合,就像简单的GROUP BY 子句一样,然后返回结果。例如:

=> SELECT * FROM items_sold;
 brand | size | sales
-------+------+-------
 Foo   | L    |  10
 Foo   | M    |  20
 Bar   | M    |  15
 Bar   | L    |  5
(4 rows)

=> SELECT brand, size, sum(sales) FROM items_sold GROUP BY GROUPING SETS ((brand), (size), ());
 brand | size | sum
-------+------+-----
 Foo   |      |  30
 Bar   |      |  20
       | L    |  15
       | M    |  35
       |      |  50
(5 rows)

GROUPING SETS 的每个子列表可以指定零个或多个列或表达式,并且与直接在 GROUP BY子句中一样解释。空分组集意味着所有行都被聚合到单个组中(即使没有输入行也会输出该组),如上述聚合函数没有GROUP BY 子句的情况所述。

在不出现这些列的分组集中,对分组列或表达式的引用在结果行中被替换为null值。

提供了一种速记符号来指定两种常见的分组集类型。形如:

rOLLUP ( e1, e2, e3, ... )

表示给定的表达式列表和列表的所有前缀,包括空列表;因此它等效于

gROUPING SETS (
    ( e1, e2, e3, ... ),
    ...
    ( e1, e2 ),
    ( e1 ),
    ( )
)

这通常用于分析分层数据;例如,按部门、部门和公司总体工资总额。

形如

cUBE ( e1, e2, ... )

的子句表示给定的列表及其所有可能的子集(即幂集)。因此

cUBE ( a, b, c )

等价于

gROUPING SETS (
    ( a, b, c ),
    ( a, b    ),
    ( a,    c ),
    ( a       ),
    (    b, c ),
    (    b    ),
    (       c ),
    (         )
)

CUBEROLLUP子句的各个元素可以是单个表达式,也可以是括在括号中的元素子列表。在后一种情况下,为了生成各个分组集,子列表被视为单个单位。例如:

cUBE ( (a, b), (c, d) )

等价于

gROUPING SETS (
    ( a, b, c, d ),
    ( a, b       ),
    (       c, d ),
    (            )
)

rOLLUP ( a, (b, c), d )

等价于

gROUPING SETS (
    ( a, b, c, d ),
    ( a, b, c    ),
    ( a          ),
    (            )
)

CUBEROLLUP 结构可以直接用于 GROUP BY 子句中,也可以嵌套在 GROUPING SETS子句中。如果一个 GROUPING SETS子句嵌套在另一个子句中,则其效果与内部子句的所有元素直接写在外部子句中相同。

如果在单个 GROUP BY 子句中指定了多个分组项,则最终的分组集列表是各个项的笛卡尔积。例如:

gROUP BY a, CUBE (b, c), GROUPING SETS ((d), (e))

等价于

gROUP BY GROUPING SETS (
    (a, b, c, d), (a, b, c, e),
    (a, b, d),    (a, b, e),
    (a, c, d),    (a, c, e),
    (a, d),       (a, e)
)

当一起指定多个分组项时,最终的分组集可能包含重复项。例如:

gROUP BY ROLLUP (a, b), ROLLUP (a, c)

等价于

gROUP BY GROUPING SETS (
    (a, b, c),
    (a, b),
    (a, b),
    (a, c),
    (a),
    (a),
    (a, c),
    (a),
    ()
)

如果这些重复项不需要,则可以直接在 GROUP BY 上使用 DISTINCT 子句来删除它们。因此:

gROUP BY DISTINCT ROLLUP (a, b), ROLLUP (a, c)

等价于

gROUP BY GROUPING SETS (
    (a, b, c),
    (a, b),
    (a, c),
    (a),
    ()
)

这与使用 SELECT DISTINCT 不同,因为输出的结果集不同。行可能仍然包含重复项。如果任何未分组的列包含NULL,则它将无法与在同一列分组时使用的 NULL 区分开来。

注意

构造 (a, b)通常在表达式中被识别为行构造函数。在GROUP BY 子句中,这不适用于表达式的顶层级别,(a, b)被解析为如上所述的表达式列表。如果出于某种原因您需要在分组表达式中使用行构造函数,请使用ROW(a,b)

4.2.5.窗口函数处理

如果查询包含任何窗口函数,则这些函数在执行任何分组、聚合和HAVING 过滤之后进行评估。也就是说,如果查询使用任何聚合、GROUP BYHAVING,则窗口函数看到的行是分组行,而不是来自FROM/WHERE 的原始表行。

当使用多个窗口函数时,所有具有在其窗口定义中具有语法等效的 PARTITION BYORDER BY子句的窗口函数都保证在数据的单次传递中进行评估。因此,它们将看到相同的排序顺序,即使 ORDER BY 不能唯一确定排序顺序。但是,对于具有不同 PARTITION BYORDER BY规范的函数,不保证评估。在这种情况下,通常需要在窗口函数评估的传递之间进行排序步骤,并且不能保证排序会保留其ORDER BY 视为等效的行的顺序。

目前,窗口函数始终需要预排序的数据,因此查询输出将根据一个或多个窗口函数的 PARTITION BY/ORDER BY子句进行排序。但是,不建议依赖此功能。如果要确保结果以特定方式排序,请使用显式的顶层 ORDER BY子句。

4.3.选择列表

如前一节所示,SELECT 命令中的表达式通过可能的组合表、视图、消除行、分组等方式构建一个中间虚拟表。最终,该表传递给选择列表进行处理。选择列表确定实际输出中间表的哪些列。

4.3.1.选择列表项

最简单的选择列表是 *,它会输出表达式生成的所有列。否则,选择列表是一个由逗号分隔的值表达式列表。例如,它可以是一列列名:

SELECT a, b, c FROM ...

列名 abc 可能是在 FROM 子句中引用的表的列的实际名称,也可能是在表和列别名中给它们的别名。选择列表中可用的名称空间与 WHERE 子句中的相同,除非使用分组,在这种情况下,它与 HAVING子句中的相同。

如果多个表具有相同名称的列,则必须同时给出表名,如下所示:

SELECT tbl1.a, tbl2.a, tbl1.b FROM ...

在使用多个表时,请求特定表的所有列也很有用:

SELECT tbl1.*, tbl2.a FROM ...

如果在选择列表中使用任意值表达式,则概念上将向返回的表添加一个新的虚拟列。该值表达式对于每个结果行都会计算一次,并用该行的值替换任何列引用。但是,选择列表中的表达式不必引用FROM子句的表达式中的任何列;它们可以是常量算术表达式,例如。

4.3.2.列标签

选择列表中的条目可以分配名称以供后续处理,例如用于ORDER BY子句或由客户端应用程序显示。例如:

SELECT a AS value, b + c AS sum FROM ...

对于指定的列标签,均可以在having子句、group by子句、where子句以及casewhen语法中使用。

如果没有使用AS指定输出列名,则系统会分配默认列名。对于简单的列引用,这是引用列的名称。对于函数调用,这是函数的名称。对于复杂的表达式,系统将生成一个通用名称。

AS关键字通常是可选的,但在某些情况下,如果所需的列名与UXDB关键字匹配,则必须编写AS或将列名加上双引号以避免歧义。(SQL关键字显示哪些关键字需要使用AS作为列标签。)例如,FROM就是这样一个关键字,因此这样做行不通:

SELECT a from, b + c AS sum FROM ...

但是以下任何一个都可以:

SELECT a AS from, b + c AS sum FROM ...
SELECT a "from", b + c AS sum FROM ...

为了最大限度地提高对可能的未来关键字添加的安全性,建议您始终编写AS或将输出列名加上双引号。

当指定的列标签与UXDB关键字匹配时,对于指定的的列标签,在having子句、group by子句、where子句以及casewhen语法中无法使用。

例如,当使用关键词FROM时,以下写法输出如下:

SELECT a as from,c as t1 from a1 where from>1;
ERROR:  syntax error at or near "from"
LINE 1: select a as from,c as t1 from a1 where from>1;
                                     ^
select a as from,c from a1 group by from;
ERROR:  syntax error at or near "from"
LINE 1: select a as from,c from a1 group by from;
                                   ^
select a as from,c from a1 having from>1;
ERROR:  syntax error at or near "from"
LINE 1: select a as from,c from a1 having from>1;
                                 ^
select a as from,c from a1 ORDER BY CASE WHEN from>1 THEN a;
ERROR:  syntax error at or near "from"
LINE 1: select a as from,c from a1 ORDER BY CASE WHEN from>1 THEN a;
                                              ^

注意

这里输出列的命名与FROM子句中所做的命名不同(请参见表和列别名)。可以重命名同一列两次,但在选择列表中分配的名称是将传递的名称。当同一表中使用的列标签和列名一致时,将会优先匹配表中的列名。

4.3.3.DISTINCT

在处理选择列表后,结果表可以选择消除重复行。可以直接在SELECT之后写入DISTINCT来指定此操作:

SELECT DISTINCT select_list ...

(可以使用ALL关键字代替DISTINCT来指定保留所有行的默认行为。)

显然,如果两行在至少一个列值上不同,则认为它们是不同的。在此比较中,空值被视为相等。

或者,任意表达式可以确定哪些行被视为不同的行:

SELECT DISTINCT ON (expression [, expression ...]) select_list ...

这里的expression是一个任意的值表达式,对所有行进行评估。所有表达式相等的一组行被视为重复项,只有该组的第一行保留在输出中。请注意,除非查询在足够的列上排序以保证到达DISTINCT过滤器的行的唯一排序,否则集合的“第一行”是不可预测的。(DISTINCT ON处理发生在ORDER BY排序之后。)

DISTINCT ON子句不是SQL标准的一部分,有时被认为是不好的风格,因为其结果可能是不确定的。通过在FROM中使用GROUP BY和子查询,可以避免使用此结构,但它通常是最方便的选择。

4.4.合并查询(UNIONINTERSECTEXCEPT

使用集合运算符 union、intersection 和 difference 可以将两个查询的结果合并。语法如下:

query1 UNION [ALL] query2
query1 INTERSECT [ALL] query2
query1 EXCEPT [ALL] query2

其中 query1query2 是查询,可以使用本章之前讨论的任何特性。

UNIONquery2 的结果有效地附加到 query1的结果中(虽然不能保证这是实际返回行的顺序)。此外,它从其结果中消除重复行,方式与DISTINCT 相同,除非使用 UNION ALL

INTERSECT 返回既在 query1 的结果中,也在 query2 的结果中的所有行。除非使用 INTERSECT ALL,否则将消除重复行。

EXCEPT 返回在 query1 的结果中,但不在 query2的结果中的所有行。(有时称为两个查询之间的差异。)除非使用EXCEPT ALL,否则将消除重复行。

为了计算两个查询的并集、交集或差集,这两个查询必须是“联合兼容”的,这意味着它们返回相同数量的列,并且相应的列具有兼容的数据类型。

集合操作可以组合,例如:

query1 UNION query2 EXCEPT query3 

这等同于

(query1 UNION query2) EXCEPT query3

如此所示,您可以使用括号来控制计算顺序。没有括号,UNION和EXCEPT从左到右关联,但INTERSECT的绑定比这两个运算符更紧密。因此,

query1 UNION query2 INTERSECT query3 

意味着

query1 UNION (query2 INTERSECT query3)

您还可以用括号括起一个单独的查询。如果查询需要使用以下各节中讨论的任何子句(如LIMIT),则这很重要。没有括号,您将得到语法错误,否则该子句将被理解为应用于集合操作的输出而不是其输入之一。例如,

SELECT a FROM b UNION SELECT x FROM y LIMIT 10 

是被接受的,但它意味着

(SELECT a FROM b UNION SELECT x FROM y) LIMIT 10 

而不是

SELECT a FROM b UNION (SELECT x FROM y LIMIT 10)

4.5.排序行(ORDER BY

查询生成输出表后(处理选择列表后),可以选择对其进行排序。如果不选择排序,则行将以未指定的顺序返回。在这种情况下,实际顺序将取决于扫描和连接计划类型以及磁盘上的顺序,但不能依赖于它。只有明确选择排序步骤才能保证特定的输出排序。

ORDER BY 子句指定排序顺序:

SELECT select_list
    FROM table_expression
    ORDER BY sort_expression1 [ASC | DESC] [NULLS { FIRST | LAST }]
             [, sort_expression2 [ASC | DESC] [NULLS { FIRST | LAST }] ...]

排序表达式可以是查询选择列表中有效的任何表达式。例如:

SELECT a, b FROM table1 ORDER BY a + b, c;

当指定多个表达式时,后面的值用于对根据前面的值相等的行进行排序。每个表达式都可以后跟一个可选的 ASCDESC关键字,以将排序方向设置为升序或降序。升序是默认值。升序将较小的值放在前面,其中“较小”是根据 <运算符定义的。类似地,降序是由 > 运算符确定的。

可以使用 NULLS FIRSTNULLS LAST 选项来确定在排序顺序中 null 值出现在非 null值之前还是之后。默认情况下,null 值按照大于任何非 null值的方式排序;也就是说,NULLS FIRST是默认值。DESC顺序,否则为NULLS LAST。请注意,排序选项是针对每个排序列独立考虑的。例如,ORDER BY x,y DESC表示ORDER BY x ASC,y DESC,这与ORDER BY x DESC,y DESC不同。 sort_expression 也可以是输出列的列标签或编号,例如:

SELECT a + b AS sum, c FROM table1 ORDER BY sum;
SELECT a, max(b) FROM table1 GROUP BY a ORDER BY 1;

这两个示例都按第一个输出列排序。请注意,输出列名称必须独立存在,即不能在表达式中使用。例如,以下示例是错误的:

SELECT a + b AS sum, c FROM table1 ORDER BY sum + c;          -- wrong

这个限制是为了减少歧义。如果ORDER BY项是一个简单的名称,既可以匹配输出列名称,也可以匹配表达式中的列,则使用输出列。只有在使用AS将输出列重命名为与其他表列的名称相匹配时,才会引起混淆。

ORDER BY可以应用于UNIONINTERSECTEXCEPT组合的结果,但在这种情况下,只允许按输出列名称或编号排序,而不是按表达式排序。

实际上,UXDB使用表达式数据类型的默认B-tree运算符类来确定ASCDESC的排序顺序。传统上,数据类型将被设置为<>运算符对应于此排序顺序,但用户定义的数据类型的设计者可以选择做一些不同的事情。

4.6.LIMITOFFSET

LIMITOFFSET 允许您仅检索查询其余部分生成的一部分行:

SELECT select_list
    FROM table_expression
    [ ORDER BY ... ]
    [ LIMIT { number | ALL } ] [ OFFSET number ]

如果给出了限制计数,则最多返回那么多行(但如果查询本身生成的行较少,则可能返回较少的行)。LIMIT ALL 与省略 LIMIT子句相同,LIMIT 带有 NULL 参数也是如此。

OFFSET 表示在开始返回行之前跳过那么多行。OFFSET 0 与省略 OFFSET 子句相同,OFFSET 带有 NULL参数也是如此。

如果同时出现 OFFSETLIMIT,则在开始计算返回的 LIMIT 行之前跳过 OFFSET 行。

使用 LIMIT 时,重要的是使用 ORDER BY子句将结果行约束为唯一顺序。否则,您将获得查询行的不可预测子集。您可能正在请求第十到第二十行,但第十到第二十行按什么顺序排列?除非指定了ORDER BY,否则顺序是未知的。

查询优化器在生成查询计划时考虑 LIMIT,因此根据您为 LIMITOFFSET提供的内容,很可能会得到不同的计划(产生不同的行顺序)。因此,使用不同的LIMIT/OFFSET 值选择查询结果的不同子集将给出不同的结果。

如果不使用 ORDER BY 强制规定结果排序顺序,则会出现不一致的结果。这不是一个错误;这是 SQL不保证以任何特定顺序提供查询结果的固有后果,除非使用 ORDER BY 来限制顺序。

OFFSET 子句跳过的行仍然必须在服务器内计算;因此,大的 OFFSET 可能效率低下。

分页查询语法在不同场景下查询数据。

  • 场景一:LIMIT...OFFSET....

    SELECT id, name, gender, score FROM students ORDER BY score DESC LIMIT 3 OFFSET 2;
    id | name | gender | score 
    ----+------+--------+-------
    1 | 小明 | M      |    90
    9 | 小王 | M      |    89
    3 | 小军 | M      |    88
    (3 rows)
    ```sql
    
  • 场景二:LIMIT...,.....

    SELECT id, name, gender, score FROM students ORDER BY score DESC LIMIT 2, 3;
    id | name | gender | score 
    ----+------+--------+-------
    1 | 小明 | M      |    90
    9 | 小王 | M      |    89
    3 | 小军 | M      |    88
    (3 rows)
    ```sql
    
  • 场景三:OFFSET...LIMIT.....

    SELECT id, name, gender, score FROM students ORDER BY score DESC OFFSET 2 LIMIT 3;
    id | name | gender | score 
    ----+------+--------+-------
    1 | 小明 | M      |    90
    9 | 小王 | M      |    89
    3 | 小军 | M      |    88
    (3 rows)
    
  • 场景四:LIMIT 表达式

    SELECT id, name, gender, score FROM students ORDER BY score DESC LIMIT 2+2 OFFSET 2;
    id | name | gender | score 
    ----+------+--------+-------
    1 | 小明 | M      |    90
    9 | 小王 | M      |    89
    3 | 小军 | M      |    88
    10 | 小丽 | F      |    88
    (4 rows)
    ```sql
    
  • 场景五:OFFSET...

    SELECT id, name, gender, score FROM students ORDER BY score DESC OFFSET 2;
    id | name | gender | score 
    ----+------+--------+-------
    1 | 小明 | M      |    90
    9 | 小王 | M      |    89
    3 | 小军 | M      |    88
    10 | 小丽 | F      |    88
    7 | 小林 | M      |    85
    5 | 小白 | F      |    81
    4 | 小米 | F      |    73
    6 | 小冰 | M      |    55
    (8 rows)
    

4.7.VALUES 列表

VALUES提供了一种生成“常量表”的方法,可以在查询中使用,而无需实际创建和填充磁盘上的表。语法如下:

VALUES ( expression [, ...] ) [, ...]

每个括号括起来的表达式列表生成表中的一行。所有列表必须具有相同数量的元素(即表中的列数),并且每个列表中的对应条目必须具有兼容的数据类型。分配给结果的每个列的实际数据类型使用与UNION 相同的规则确定(参见UNION、CASE 和相关结构)。

例如:

VALUES (1, 'one'), (2, 'two'), (3, 'three');

将返回一个两列三行的表。它实际上相当于:

SELECT 1 AS column1, 'one' AS column2
UNION ALL
SELECT 2, 'two'
UNION ALL
SELECT 3, 'three';

默认情况下,UXDB 为 VALUES 表的列分配名称 column1column2 等。列名未在 SQL标准中指定,不同的数据库系统处理方式不同,因此通常最好使用表别名列表覆盖默认名称,例如:

=> SELECT * FROM (VALUES (1, 'one'), (2, 'two'), (3, 'three')) AS t (num,letter);
 num | letter
-----+--------
   1 | one
   2 | two
   3 | three
(3 rows)

从语法上讲,后跟表达式列表的 VALUES 被视为等同于:

SELECT select_list FROM table_expression

并且可以出现在 SELECT 可以出现的任何地方。例如,您可以将其作为 UNION 的一部分使用,或者向其附加一个sort_speticf (ORDERBY、 LIMIT 和/或 OffSET)。VALUES最常用作INSERT命令中的数据源,其次最常用作子查询。

4.8.WITH 查询(公共表达式)

WITH 提供了一种编写辅助语句以在更大的查询中使用的方法。这些语句通常被称为公共表达式或CTE,可以将其视为仅为一个查询而存在的临时表。在WITH 子句中的每个辅助语句都可以是 SELECTINSERTUPDATEDELETE;而 WITH子句本身则附加在一个可以是 SELECTINSERTUPDATEDELETE 的主语句上。

4.8.1.SELECT 中的 WITH

WITHSELECT 的基本价值在于将复杂的查询分解为更简单的部分。例如:

WITH regional_sales AS (
    SELECT region, SUM(amount) AS total_sales
    FROM orders
    GROUP BY region
), top_regions AS (
    SELECT region
    FROM regional_sales
    WHERE total_sales > (SELECT SUM(total_sales)/10 FROM regional_sales)
)
SELECT region,
       product,
       SUM(quantity) AS product_units,
       SUM(amount) AS product_sales
FROM orders
WHERE region IN (SELECT region FROM top_regions)
GROUP BY region, product;

该查询仅显示顶级销售区域中每个产品的销售总额。由于未使用流式响应导致超时,请务必使用流式响应以避免再次出现这种情况!如有疑问,请联系我们。

4.8.2.递归查询

使用递归查询计算树遍历时,您可能希望按深度优先或广度优先顺序对结果进行排序。这可以通过在其他数据列旁边计算一个排序列来完成,并在最后使用它来对结果进行排序。请注意,这实际上并不能控制查询评估访问行的顺序;这始终取决于SQL实现。这种方法仅仅提供了一种方便的方法来在之后对结果进行排序。

要创建深度优先顺序,我们为每个结果行计算一个已访问的行数组。例如,考虑以下查询,它使用link字段搜索表tree

WITH RECURSIVE t(n) AS (
    VALUES (1)
  UNION ALL
    SELECT n+1 FROM t WHERE n < 100
)
SELECT sum(n) FROM t;

过程 递归查询求值

  1. 计算非递归项。对UNION(但不对UNION ALL),抛弃重复行。把所有剩余的行包括在递归查询的结果中,并且也把它们放在一个临时的工作表中。

  2. 只要工作表不为空,重复下列步骤:

    a. 计算递归项,用当前工作表的内容替换递归自引用。对UNION(不是UNION ALL),抛弃重复行以及那些与之前结果行重复的行。将剩下的所有行包括在递归查询的结果中,并且也把它们放在一个临时的中间表中。

    b. 用中间表的内容替换工作表的内容,然后清空中间表。

    提示

    在需要跟踪一个字段的常见情况下,省略ROW()语法。这允许使用简单的数组而不是复合类型数组,从而提高效率。

在上面的示例中,工作表在每个步骤中只有一行,并且在后续步骤中接受从1到100的值。在第100步中,由于WHERE子句,没有输出,因此查询将终止。

递归查询通常用于处理分层数据或树结构数据。一个有用的例子是这个查询,它查找产品的所有直接和间接子部件,只给出一个显示直接包含的表:

WITH RECURSIVE included_parts(sub_part, part, quantity) AS (
    SELECT sub_part, part, quantity FROM parts WHERE part = 'our_product'
  UNION ALL
    SELECT p.sub_part, p.part, p.quantity * pr.quantity
    FROM included_parts pr, parts p
    WHERE p.part = pr.sub_part
)
SELECT sub_part, SUM(quantity) as total_quantity
FROM included_parts
GROUP BY sub_part
4.8.2.1.顺序查询

当使用递归查询计算树遍历时,您可能希望按深度优先或广度优先顺序排列结果。这可以通过计算一个排序列以及其他数据列来完成,并使用这些数据列对最后的结果进行排序。请注意,这实际上并不控制查询计算访问行的顺序;这总是依赖于SQL实现。这种方法只是提供了一种方便的方法,以便事后对结果进行排序。

为了创建深度优先顺序,我们为每个结果行计算到目前为止访问过的行数组。例如,考虑使用链接字段搜索表树的以下查询:

WITH RECURSIVE search_tree(id, link, data) AS (
    SELECT t.id, t.link, t.data
    FROM tree t
  UNION ALL
    SELECT t.id, t.link, t.data
    FROM tree t, search_tree st
    WHERE t.id = st.link
)
SELECT * FROM search_tree;

要添加深度优先排序信息,可以编写如下代码:

WITH RECURSIVE search_tree(id, link, data, path) AS (
    SELECT t.id, t.link, t.data, ARRAY[t.id]
    FROM tree t
  UNION ALL
    SELECT t.id, t.link, t.data, path || t.id
    FROM tree t, search_tree st
    WHERE t.id = st.link
)
SELECT * FROM search_tree ORDER BY path;

在一般情况下,如果需要使用多个字段来标识一行,则使用行数组。例如,如果我们需要跟踪字段 f1和 f2:

WITH RECURSIVE search_tree(id, link, data, path) AS (
    SELECT t.id, t.link, t.data, ARRAY[ROW(t.f1, t.f2)]
    FROM tree t
  UNION ALL
    SELECT t.id, t.link, t.data, path || ROW(t.f1, t.f2)
    FROM tree t, search_tree st
    WHERE t.id = st.link
)
SELECT * FROM search_tree ORDER BY path;

提示

在只需要跟踪一个字段的常见情况下,省略 ROW()语法。这允许使用简单的数组而不是复合类型的数组,从而提高效率。

若要创建宽度优先的顺序,可以添加一个跟踪搜索深度的列,例如:

WITH RECURSIVE search_tree(id, link, data, depth) AS (
    SELECT t.id, t.link, t.data, 0
    FROM tree t
  UNION ALL
    SELECT t.id, t.link, t.data, depth + 1
    FROM tree t, search_tree st
    WHERE t.id = st.link
)
SELECT * FROM search_tree ORDER BY depth;

若要获得稳定排序,请将数据列添加为辅助排序列。

递归查询评估算法按广度优先搜索顺序生成输出。但是,这是一个实现细节,依赖它可能是不合理的。每个级别中的行的顺序肯定是未定义的,因此在任何情况下都可能需要一些显式的排序。

内置语法可以计算深度或广度优先排序列,例如:

WITH RECURSIVE search_tree(id, link, data) AS (
    SELECT t.id, t.link, t.data
    FROM tree t
  UNION ALL
    SELECT t.id, t.link, t.data
    FROM tree t, search_tree st
    WHERE t.id = st.link
) SEARCH DEPTH FIRST BY id SET ordercol
SELECT * FROM search_tree ORDER BY ordercol;

WITH RECURSIVE search_tree(id, link, data) AS (
    SELECT t.id, t.link, t.data
    FROM tree t
  UNION ALL
    SELECT t.id, t.link, t.data
    FROM tree t, search_tree st
    WHERE t.id = st.link
) SEARCH BREADTH FIRST BY id SET ordercol
SELECT * FROM search_tree ORDER BY ordercol;

它将被内部重写为上面的形式。CYCLE子句首先指定要跟踪的列的列表以进行循环检测,然后是一个列名,该列将显示是否检测到循环,最后是将跟踪路径的另一个列的名称。循环和路径列将隐式添加到CTE的输出行中。

4.8.2.2.循环路径

在使用递归查询时,重要的是要确保递归部分的查询最终将不返回任何元组,否则查询将无限循环。有时,使用UNION而不是UNION ALL可以通过丢弃与先前输出行完全重复的行来实现这一点。然而,往往一个循环不涉及完全重复的输出行:可能需要检查一个或几个字段,以查看是否已经到达了相同的点。处理这种情况的标准方法是计算已访问值的数组。例如,再次考虑使用link字段搜索表格graph的以下查询:

WITH RECURSIVE search_graph(id, link, data, depth) AS (
    SELECT g.id, g.link, g.data, 0
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1
    FROM graph g, search_graph sg
    WHERE g.id = sg.link
)
SELECT * FROM search_graph;

如果link关系包含循环,则此查询将循环。因为我们需要一个“深度”输出,只是将UNION ALL更改为UNION不会消除循环。相反,我们需要识别是否在遵循特定链接路径时再次到达了相同的行。我们将两个列is_cycle和path添加到易于循环的查询中:

WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS (
    SELECT g.id, g.link, g.data, 0,
      false,
      ARRAY[g.id]
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1,
      g.id = ANY(path),
      path || g.id
    FROM graph g, search_graph sg
    WHERE g.id = sg.link AND NOT is_cycle
)
SELECT * FROM search_graph;

除了防止循环外,数组值本身通常也很有用,表示到达任何特定行的“路径”。

在需要检查多个字段以识别循环的一般情况下,使用行数组。例如,如果我们需要比较字段f1和f2:

WITH RECURSIVE search_graph(id, link, data, depth, is_cycle, path) AS (
    SELECT g.id, g.link, g.data, 0,
      false,
      ARRAY[ROW(g.f1, g.f2)]
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1,
      ROW(g.f1, g.f2) = ANY(path),
      path || ROW(g.f1, g.f2)
    FROM graph g, search_graph sg
    WHERE g.id = sg.link AND NOT is_cycle
)
SELECT * FROM search_graph;

提示

在只需要检查一个字段来识别循环的常见情况下,可以省略 ROW() 语法。这样可以使用简单的数组而不是复合类型数组,提高效率。

有内置的语法来简化循环检测。上面的查询也可以写成这样:

WITH RECURSIVE search_graph(id, link, data, depth) AS (
    SELECT g.id, g.link, g.data, 1
    FROM graph g
  UNION ALL
    SELECT g.id, g.link, g.data, sg.depth + 1
    FROM graph g, search_graph sg
    WHERE g.id = sg.link
) CYCLE id SET is_cycle USING path
SELECT * FROM search_graph;

它将被内部重写为上面的形式。CYCLE子句首先指定要跟踪的列的列表以进行循环检测,然后是一个列名,该列将显示是否检测到循环,最后是将跟踪路径的另一个列的名称。循环和路径列将隐式添加到CTE 的输出行中。

提示

循环路径列的计算方式与上一节中显示深度优先排序列的计算方式相同。查询可以同时具有 SEARCHCYCLE子句,但是深度优先搜索规范和循环检测规范将创建冗余计算,因此仅使用 CYCLE子句并按路径列排序更有效率。如果需要广度优先排序,则同时指定 SEARCHCYCLE 可以很有用。

在您不确定查询是否可能循环时,测试查询的有用技巧是在父查询中放置 LIMIT。例如,如果没有 LIMIT,此查询将无限循环:

WITH RECURSIVE t(n) AS (
    SELECT 1
  UNION ALL
    SELECT n+1 FROM t
)
SELECT n FROM t LIMIT 100;

这是因为 UXDB 的实现仅计算父查询实际获取的 WITH查询的行数。在生产中使用此技巧不建议,因为其他系统可能会有不同的工作方式。此外,如果您使外部查询对递归查询的结果进行排序或将其与其他表连接,则通常不起作用,因为在这种情况下,外部查询通常会尝试获取所有WITH 查询的输出。

4.8.3.公共表达式物化

WITH 查询的一个有用属性是,它们通常仅在父查询的执行期间评估一次,即使它们被父查询或兄弟 WITH 查询多次引用。因此,可以将需要在多个位置使用的昂贵计算放置在 WITH查询中,以避免冗余工作。另一个可能的应用是防止具有副作用的函数的不必要多次评估。然而,这个硬币的另一面是,优化器无法将限制从父查询推入多次引用的 WITH 查询中,因为这可能会影响所有使用 WITH查询的输出,而应该只影响一个。多次引用的 WITH 查询将按原样评估,而不会抑制父查询可能之后会丢弃。(但是,如上所述,如果对查询的引用仅需要有限数量的行,则评估可能会提前停止。)

但是,如果WITH查询是非递归的且没有副作用(即,它是一个包含没有易失性函数的SELECT),则可以将其折叠到父查询中,从而允许两个查询级别的联合优化。默认情况下,如果父查询仅引用一次WITH查询,则会发生这种情况,但如果父查询引用WITH查询多次,则不会发生这种情况。您可以通过指定MATERIALIZED来覆盖该决策,以强制单独计算WITH查询,或者通过指定NOT MATERIALIZED来强制将其合并到父查询中。后一种选择会冒重复计算WITH查询的风险,但如果每个WITH查询的使用仅需要WITH查询的部分输出,则仍然可以节省净值。

这些规则的一个简单示例是

WITH w AS (
    SELECT * FROM big_table
)
SELECT * FROM w WHERE key = 123;

这个WITH查询将被折叠,产生与以下执行计划相同的结果

SELECT * FROM big_table WHERE key = 123;

特别是,如果key上有索引,则可能会使用它来仅获取具有key = 123的行。另一方面,在

WITH w AS (
    SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.key = w2.ref
WHERE w2.key = 123;

WITH查询将被材料化,产生big_table的临时副本,然后将其与自身连接-没有任何索引的好处。如果写成这个查询,它将执行得更有效率

WITH w AS NOT MATERIALIZED (
    SELECT * FROM big_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.key = w2.ref
WHERE w2.key = 123;

这样,父查询的限制可以直接应用于big_table的扫描。

NOT MATERIALIZED可能不理想的一个例子是

WITH w AS (
    SELECT key, very_expensive_function(val) as f FROM some_table
)
SELECT * FROM w AS w1 JOIN w AS w2 ON w1.f = w2.f;

在这里,WITH查询的材料化确保very_expensive_function仅对每个表行评估一次,而不是两次。

上面的示例仅显示了WITHSELECT一起使用,但它可以以相同的方式附加到INSERTUPDATEDELETE。在每种情况下,它实际上提供了可以在主命令中引用的临时表。

4.8.4.在WITH中修改数据的语句

您可以在WITH中使用数据修改语句(INSERTUPDATEDELETE)。这使您可以在同一查询中执行多个不同的操作。一个例子是:

WITH moved_rows AS (
    DELETE FROM products
    WHERE
        "date" >= '2010-10-01' AND
        "date" < '2010-11-01'
    RETURNING *
)
INSERT INTO products_log
SELECT * FROM moved_rows;

这个查询有效地将行从products移到products_log中。products_log。在WITH中的DELETEproducts中删除指定的行,并通过其RETURNING子句返回它们的内容;然后主查询读取该输出并将其插入products_log中。

上面示例的一个细节是,WITH子句附加到INSERT而不是INSERT中的子SELECT。这是必要的,因为只允许在附加到顶级语句的WITH子句中使用修改数据的语句。但是,正常的WITH可见性规则适用,因此可以从子SELECT中引用WITH语句的输出。

WITH中的修改数据的语句通常具有RETURNING子句,如上例所示。是RETURNING子句的输出,而不是修改数据语句的目标表,形成可以被查询的临时表。如果WITH中的修改数据的语句缺少RETURNING子句,则不会形成临时表,并且无法在查询的其余部分中引用。这样的语句仍将被执行。一个不是特别有用的例子是:

WITH t AS (
    DELETE FROM foo
)
DELETE FROM bar;

此示例将从表foobar中删除所有行。向客户端报告的受影响行数仅包括从bar中删除的行。

不允许在修改数据的语句中进行递归自引用。在某些情况下,可以通过引用递归WITH的输出来解决此限制,例如:

WITH RECURSIVE included_parts(sub_part, part) AS (
    SELECT sub_part, part FROM parts WHERE part = 'our_product'
  UNION ALL
    SELECT p.sub_part, p.part
    FROM included_parts pr, parts p
    WHERE p.part = pr.sub_part
)
DELETE FROM parts
  WHERE part IN (SELECT part FROM included_parts);

此查询将删除产品的所有直接和间接子部件。

WITH中的修改数据的语句仅执行一次,并始终完成,无论主查询是否读取它们的所有(或任何)输出。请注意,这与WITHSELECT的规则不同:如前一节所述,SELECT的执行仅在主查询需要其输出时进行。

WITH中的子语句彼此并发执行,并与主查询并发执行。因此,在WITH中使用数据修改语句时,指定的更新实际发生的顺序是不可预测的。所有语句都使用相同的快照执行,因此它们无法“看到”目标表上彼此的效果。这减轻了实际行更新顺序不可预测的影响,并意味着RETURING数据是在不同的WITH子语句和主查询之间通信更改的唯一方法。这方面的一个例子是

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM products;

外部的SELECT将返回更新操作之前的原始价格,而在

WITH t AS (
    UPDATE products SET price = price * 1.05
    RETURNING *
)
SELECT * FROM t;

外部SELECT将返回更新后的数据。

不支持在一条语句中两次更新同一行。只有一个修改发生,但不容易(有时不可能)可靠地预测哪一个。这也适用于删除同一语句中已更新的行: 只执行更新。因此,通常应该避免在单个语句中两次修改单个行。尤其要避免编写可能影响主语句或同级子语句所更改的相同行的WITH子语句。这种说法的效果是不可预测的。

目前,WITH- 中用作数据修改语句目标的任何表都不能有条件规则、ALSO规则或扩展到多个语句的INSTEAD规则。

4.8.5.WITH中的PROCEDUREFUNCTION

  • 描述

    WITH [PROCEDURE] FUNCTION子句用于在SQL语句中临时声明并定义函数或存储过程,这些函数或存储过程,可以在with子句内被引用。相比模式级别定义的函数后存储过程,通过WITH [PROCEDURE] FUNCTION定义的函数在解析时拥有更高的优先级。

    和公用表表达式CTE类似,通过WITH [PROCEDURE] FUNCTION定义的函数或存储过程对象不会存入系统表中,且仅在当前with语句中有效。

  • 语法

    [ WITH [ RECURSIVE ] with_query [, ...] ]
    | [ WITH [ { PROCEDURE | FUNCTION } with_func_query [, ...] ] ]
    SELECT [ ALL | DISTINCT [ ON ( expression [, ...] ) ] ]
        [ TOP n [, m | ( m ) ] ]
        | [ TOP ( n ) [, m | ( m ) ] ]
        [ * | expression [ [ AS ] output_name ] [, ...] ]
        [ FROM from_item [, ...] ]
        [ SAMPLE [ BLOCK ] ( sample_count ) ] [ SEED ( seed_count ) ]
        [ WHERE condition ]
        [ START WITH condition CONNECT BY [ NOCYCLE ] condition ]
        | [ CONNECT BY [ NOCYCLE ] condition [ START WITH condition ] ]
        [ GROUP BY grouping_element [, ...] ]
        [ HAVING condition [, ...] ]
        [ WINDOW window_name AS ( window_definition ) [, ...] ]
        [ { UNION | INTERSECT | EXCEPT | MINUS } [ ALL | DISTINCT ] select ]
        [ ORDER [ SIBLINGS ] BY expression [ ASC | DESC | USING operator ] [ NULLS { FIRST | LAST } ] [, ...] ] [ LIMIT { count | ALL } ]
        [ OFFSET start [ ROW | ROWS ] ]
        [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ]
        [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } [ OF table_name [, ...] ] [ NOWAIT | SKIP LOCKED | WAIT wait_time] [...] ]
    
    其中from_item是以下之一:
    
      [ ONLY ] table_name [ * ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
                  [ TABLESAMPLE sampling_method ( argument [, ...] ) [ REPEATABLE ( seed ) ] ]
      [ LATERAL ] ( select ) [ AS ] alias [ ( column_alias [, ...] ) ]
      with_query_name [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
      [ LATERAL ] function_name ( [ argument [, ...] ] )
                  [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
      [ LATERAL ] function_name ( [ argument [, ...] ] ) [ AS ] alias ( column_definition [, ...] )
      [ LATERAL ] function_name ( [ argument [, ...] ] ) AS ( column_definition [, ...] )
      [ LATERAL ] ROWS FROM( function_name ( [ argument [, ...] ] ) [ AS ( column_definition [, ...] ) ] [, ...] )
                  [ WITH ORDINALITY ] [ [ AS ] alias [ ( column_alias [, ...] ) ] ]
      [ from_item PIVOT ( pivot_clause pivot_for_clause pivot_in_clause ) [ alias ] ]
      [ from_item UNPIVOT [ { INCLUDE | EXCLUDE } NULLS ] ( unpivot_value_clause unpivot_for_clause unpivot_in_clause) [ [ AS ] alias [ ( column_alias [, ...] ) ] ] ]
      from_item [ NATURAL ] join_type from_item [ ON join_condition | USING ( join_column [, ...] ) ]
    
    其中pivot_clause是:
      agg_func_expression ( expr ) [ [ AS ] output_name ] [, ...]
    其中pivot_for_clause是:
      FOR { pivot_column | ( pivot_column [, ...] ) }
    其中pivot_in_clause是:
      IN ( [ const_expression [ [ AS ] output_name ] [, ...] ] )
    其中unpivot_value_clause是:
      { column_name | ( column_name [, ...] ) }
    其中unpivot_for_clause是:
      FOR { output_column | ( output_column [, ...] ) }
    其中unpivot_in_clause是:
      IN ( input_data_column [ AS constant ] [, ...] )
    其中grouping_element是以下之一:
    
      ( )
      expression
      ( expression [, ...] )
      ROLLUP ( { expression | ( expression [, ...] ) } [, ...] )
      CUBE ( { expression | ( expression [, ...] ) } [, ...] )
      GROUPING SETS ( grouping_element [, ...] )
    
    其中with_func_query是:
    
      func_name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
      [ RETURNS rettype
        | RETURNS TABLE ( column_name column_type [, ...] ) ]
      { LANGUAGE lang_name
          | TRANSFORM { FOR TYPE type_name } [, ... ]
          | WINDOW
          | IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
          | CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT
          | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
          | PARALLEL { UNSAFE | RESTRICTED | SAFE }
          | COST execution_cost
          | ROWS result_rows
          | SUPPORT support_function
          | SET configuration_parameter { TO value | = value | FROM CURRENT }
          | { AS | IS } 'definition'
          | { AS | IS } 'obj_file', 'link_symbol'
      }
    
    其中with_query是:
    
      with_query_name [ ( column_name [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( select | values | insert | update | delete )
          [ SEARCH { BREADTH | DEPTH } FIRST BY column_name [, ...] SET search_seq_col_name ]
          [ CYCLE column_name [, ...] SET cycle_mark_col_name [ TO cycle_mark_value DEFAULT cycle_mark_default ] USING cycle_path_col_name ]
    
    TABLE [ ONLY ] table_name [ * ]
    
  • 参数

    • [PROCEDURE] FUNCTION

      指明需要定义的是函数还是存储过程。

    • func_name、[argmode]等参数

      请参见《优炫数据库管理系统SQL命令手册》中的CREATE FUNCTIONCREATE PROCEDURE

  • 注解

    1. 同一with子句支持同时创建多个function、procedure,中间以分号(;)分隔,末尾以斜杠符号(/)结束输入。

    2. procedure支持嵌套使用,但必须由function调用,不可单独定义使用。

  • 支持模式

    该功能支持的模式:标准模式、兼容(oracle)模式、MYSQL模式、安全模式。

  • 示例

    定义并使用function,如下所示。

    with function with_func1(aa numeric) returns numeric
    as $$
    begin
    return aa + 20;
    end;
    $$ language pluxsql;
    select with_func1(3::numeric);
    /
     with_func1
    ------------
             23
    (1 row)
    

    定义procedure并在function中调用,如下所示。

    CREAte table with_tb1 (id int);
    
    with
    procedure with_proce1(aa int)
    AS $$
    insert into with_tb1 values (aa);
    $$ language SQL;
    function with_func1(cc int) returns int
    as $$
    begin
    call with_proce1(cc);
    return cc;
    end;
    \$$ language pluxsql;
    select with_func1(1);
    /
    
     with_func1
    ------------
              1
    (1 row)
    
    select * from with_tb1;
     id
    ----
      1
    (1 row)
    
  • 兼容性

    oracle与达梦均支持with function用法,达梦不支持存储过程(procedure)定义使用。

4.9.pivot行列转换

  • 概述

    PIVOT查询为将行转换为列,以交叉表格式生成结果。在转换过程中聚合数据,新的列表示不同范围的聚合数据,PIVOT操作的输出通常比初始数据行包含更多的列和更少的行。

  • 语法

    SELECT column_clause 
    FROM <table-expr> 
    PIVOT
    (
       pivot_clause 
       pivot_for_clause 
       pivot_in_clause 
    )
    
    其中 column_clause 是:
    column1, column2(,... ...)或者*
    为*或者列名,若为列名,则必须为table-expr中子列
    
    其中 table-expr 是:
    table-expr 可以是基本表或者子查询
    
    其中 pivot_clause 是:
    aggregate_function ( expr) [[AS] alias ]                      
    [, aggregate_function ( expr ) [[AS] alias ] ... ... ]
    aggregate_function为聚集函数比如sum,max,可为单列/多列聚合,支持as 重命名,as重命名子句可缺省
    
    其中 pivot_for_clause 是:
    for ( column1  [,  column2 , ... ...] )
    
    其中 pivot_in_clause 是:
    in (  value1  [as alias ] [, value2  [as alias ] , ... ...])  
    指定的value值,行转列之后的新列的列名
    
  • 参数

    • pivot_clause

      单列/多列的聚集函数。

    • pivot_for_clause

      列名,且该列名必须要在table-expr中出现。

    • pivot_in_clause

      常量表达式,为字符串/数值,非常量表达式不可用。

  • 返回值

    返回行转列之后的视图。

  • 支持模式

    支持标准模式、兼容模式、安全模式,Linux平台和Windows平台可用。

  • 示例

    1. 创建表,并插入数据。

      create table sales(product VARCHAR(32) not null,id int,country VARCHAR(20),channel int,
      quarter  int,amount_sold int, quantity_sold int);
      
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('noodles',1,'China',3,01,7,100);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('noodles',2,'China',3,02,7,150);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('noodles',2,'China',4,02,7,200);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('noodles',2,'China',5,03,7,300);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('noodles',3,'China',9,04,7,400);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('KFC',1,'America',3,01,20,100);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('KFC',2,'America',4,02,20,200);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('KFC',3,'America',5,03,20,300);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('KFC',3,'America',9,04,20,400);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('pizza',1,'japan',3,01,15,100);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('pizza',1,'japan',4,02,15,200);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('pizza',1,'japan',5,03,15,300);
      insert into sales (product,id,country,channel,quarter,amount_sold,quantity_sold)values('pizza',4,'japan',9,04,15,400);
      
      create table uxdbc_oracle_pivot_0001_table01(id1 int not null,name varchar2(15) not null, sex char(10), phone varchar2(15), salary float);
      
      create table uxdbc_oracle_pivot_0001_table02(id2 int not null,name varchar2(15) not null, depart varchar2(15),entime int);
      
      insert into uxdbc_oracle_pivot_0001_table01 values(0001,'张三','man','0123-54589521',2500);
      insert into uxdbc_oracle_pivot_0001_table02 values(0001,'张三','A3001',1);
      insert into uxdbc_oracle_pivot_0001_table01 values(0002,'李明','woman','0123-54589521',2500);
      insert into uxdbc_oracle_pivot_0001_table02 values(0002,'李明','A3001',2);
      insert into uxdbc_oracle_pivot_0001_table01 values(0003,'李四','man','',3000);
      insert into uxdbc_oracle_pivot_0001_table02 values(0003,'李四','A3002',2);
      
    2. 验证单列聚合函数。

      SELECT *
      FROM sales PIVOT(SUM(amount_sold)
      FOR CHANNEL IN(3,4,5,9)) 
      ORDER BY product,id,quarter,quantity_sold;
      
      PRODUCT | ID | COUNTRY | QUARTER | QUANTITY_SOLD | 3  | 4  | 5  | 9  
      ---------+----+---------+---------+---------------+----+----+----+----
       KFC     |  1 | America |       1 |           100 | 20 |    |    |    
       KFC     |  2 | America |       2 |           200 |    | 20 |    |    
       KFC     |  3 | America |       3 |           300 |    |    | 20 |    
       KFC     |  3 | America |       4 |           400 |    |    |    | 20 
       noodles |  1 | China   |       1 |           100 |  7 |    |    |    
       noodles |  2 | China   |       2 |           150 |  7 |    |    |    
       noodles |  2 | China   |       2 |           200 |    |  7 |    |    
       noodles |  2 | China   |       3 |           300 |    |    |  7 |    
       noodles |  3 | China   |       4 |           400 |    |    |    |  7 
       pizza   |  1 | japan   |       1 |           100 | 15 |    |    |    
       pizza   |  1 | japan   |       2 |           200 |    | 15 |    |    
       pizza   |  1 | japan   |       3 |           300 |    |    | 15 |    
       pizza   |  4 | japan   |       4 |           400 |    |    |    | 15 
      (13 rows)
      
    3. 验证多列聚合函数与聚合函数重命名。

      SELECT *
      FROM (SELECT product, channel, amount_sold, quantity_sold FROM sales) S 
      PIVOT
      (SUM(amount_sold) AS sums, SUM(quantity_sold) AS sumq
      FOR channel IN(5, 4, 3, 9)) ORDER BY product;
      
      PRODUCT | 5_SUMS | 5_SUMQ | 4_SUMS | 4_SUMQ | 3_SUMS | 3_SUMQ | 9_SUMS | 9_SUMQ 
      ---------+--------+--------+--------+--------+--------+--------+--------+--------
       KFC     |     20 |    300 |     20 |    200 |     20 |    100 |     20 |    400
       noodles |      7 |    300 |      7 |    200 |     14 |    250 |      7 |    400
       pizza   |     15 |    300 |     15 |    200 |     15 |    100 |     15 |    400
      (3 rows)
      
    4. 验证for-in多列与as重命名。

      SELECT *
         FROM (SELECT product, channel, quarter, quantity_sold FROM 	sales) S
      PIVOT(SUM(quantity_sold) AS sums, SUM(quantity_sold)
         FOR (CHANNEL, quarter) 
      IN((5,3) as col1,(4,2),(3,1) col3,(3,2),(9,4)))
      ORDER BY product;
      
      PRODUCT | COL1_SUMS | COL1 | 4_2_SUMS | 4_2 | COL3_SUMS | COL3 | 3_2_SUMS | 3_2 | 9_4_SUMS | 9_4 
      ---------+-----------+------+----------+-----+-----------+------+----------+-----+----------+-----
       KFC     |       300 |  300 |      200 | 200 |       100 |  100 |          |     |      400 | 400
       noodles |       300 |  300 |      200 | 200 |       100 |  100 |      150 | 150 |      400 | 400
       pizza   |       300 |  300 |      200 | 200 |       100 |  100 |          |     |      400 | 400
      (3 rows)
      
    5. 验证外连接时使用pivot的结合性-1。

      SELECT * from 
      (
      uxdbc_oracle_pivot_0001_table01 
      LEFT JOIN 
      uxdbc_oracle_pivot_0001_table02 
      pivot(sum(entime) FOR depart in ('A3001')) s
      on s.id2 = uxdbc_oracle_pivot_0001_table01.id1
      ) ORDER BY id1, id2;
      
      ID1 | NAME |    SEX     |     PHONE     | SALARY | ID2 | NAME | 'A3001'
      -----+------+------------+---------------+--------+-----+------+---------
         1 | 张三 | man        | 0123-54589521 |   2500 |   1 | 张三 |       1
         2 | 李明 | woman      | 0123-54589521 |   2500 |   2 | 李明 |       2
         3 | 李四 | man        |               |   3000 |   3 | 李四 |
      (3 rows)
      
    6. 验证外连接时使用pivot的结合性-2。

      SELECT * from 
      (
      uxdbc_oracle_pivot_0001_table01 
      LEFT JOIN 
      uxdbc_oracle_pivot_0001_table02 
      pivot(sum(entime) FOR name in ('张三','李四','李明')) s
      on s.id2 = uxdbc_oracle_pivot_0001_table01.id1
      )pivot (sum(salary) FOR sex IN ('man')) ORDER BY id1, id2;
      
      ID1 | NAME |     PHONE     | ID2 | DEPART | '张三' | '李四' | '李明' | 'man'
      -----+------+---------------+-----+--------+--------+--------+--------+-------
         1 | 张三 | 0123-54589521 |   1 | A3001  |      1 |        |        |  2500
         2 | 李明 | 0123-54589521 |   2 | A3001  |        |        |      2 |
         3 | 李四 |               |   3 | A3002  |        |      2 |        |  3000
      (3 rows)
      
    7. 验证外连接时使用pivot的结合性-3。

      SELECT * from 
      (
      uxdbc_oracle_pivot_0001_table01
      pivot(sum(salary) FOR sex in ('man')) s
      pivot(max(name) FOR phone IN ('0123-54589521')) s1
      LEFT JOIN 
      uxdbc_oracle_pivot_0001_table02 
      on s1.id1 = uxdbc_oracle_pivot_0001_table02.id2
      ) ORDER BY id1, id2;
      
      ID1 | 'man' | '0123-54589521' | ID2 | NAME | DEPART | ENTIME
      -----+-------+-----------------+-----+------+--------+--------
         1 |  2500 | 张三            |   1 | 张三 | A3001  |      1
         2 |       | 李明            |   2 | 李明 | A3001  |      2
         3 |  3000 |                 |   3 | 李四 | A3002  |      2
      (3 rows)
      
    8. 验证pivot作子查询。

      SELECT * from uxdbc_oracle_pivot_0001_table02 pivot(sum(id2) 
      FOR depart in ('A3001','A3002')) pivot(sum(entime)
      FOR name in ('张三','李四')) ORDER BY "'张三'";
      
      'A3001' | 'A3002' | '张三' | '李四'
      ---------+---------+--------+--------
           1 |         |      1 |
               |       3 |        |      2
           2 |         |        |
      (3 rows)
      
    9. 验证pivot在多个table-expr成员存在的情况下的优先结合性。

      SELECT * from 
      uxdbc_oracle_pivot_0001_table01 
      pivot(sum(salary) FOR sex in ('man')),
      uxdbc_oracle_pivot_0001_table02
      ORDER BY id1,id2;
      
      ID1 | NAME |     PHONE     | 'man' | ID2 | NAME | DEPART | ENTIME
      -----+------+---------------+-------+-----+------+--------+--------
         1 | 张三 | 0123-54589521 |  2500 |   1 | 张三 | A3001  |      1
         1 | 张三 | 0123-54589521 |  2500 |   2 | 李明 | A3001  |      2
         1 | 张三 | 0123-54589521 |  2500 |   3 | 李四 | A3002  |      2
         2 | 李明 | 0123-54589521 |       |   1 | 张三 | A3001  |      1
         2 | 李明 | 0123-54589521 |       |   2 | 李明 | A3001  |      2
         2 | 李明 | 0123-54589521 |       |   3 | 李四 | A3002  |      2
         3 | 李四 |               |  3000 |   1 | 张三 | A3001  |      1
         3 | 李四 |               |  3000 |   2 | 李明 | A3001  |      2
         3 | 李四 |               |  3000 |   3 | 李四 | A3002  |      2
      (9 rows)
      

4.10.unpivot列转行

  • 概述

    UNPIVOT将一个表的列转换为行,常用于数据透视或数据汇总操作。UNPIVOT函数将列名旋转为行值,同时将列值转换为另一个列,以展示更加方便的数据结构。

  • 语法

    select  * | column1,column2,column3[,...] from table_expr
     UNPIVOT [include | exclude nulls]
     (
     unpivot_value_clause
     unpivot_for_clause
     unpivot_in_clause
     )
     [ order by column1, column2, column3 [,...] ]
    
     其中 unpivot_value_clause 是: 
     value-column | (value-column, value-column, ...) 
    
     其中 unpivot_for_clause是: 
     For output-column | (output-column1, output-column2, ...)
    
     其中 unpivot_in_clause是: 
     in input-data-column [as literal] [, input-data-column [as literal] ...]        
     | in ((input-data-column, input-data-column...) as literal 
     [,(input-data-column, input-data-column...) as literal ...]) 
    ```sql
    
  • 描述

    1. include | exclude nulls: 控制最后结果集是否含有null,include表示含有,exclude去除含有null的行,默认是exclude。该语句只控制unpivot_value_clause的输出结果,当unpivot_value_clause为多列时候,当且仅当所有列都为null,exclude nulls才会发生作用。

    2. unpivot_value_clausevalue-column即结果列,列名由用户定义,必须为列。可单列,可多列,但列数必须与unpivot_in_clauseinput-data-column列数对应相同,否则报错。

    3. unpivot_for_clauseoutput-column即行转列之后的聚合列,列名由用户定义,必须为列。可以单列,可以多列。output-column的列数不必与unpivot_in_clauseunpivot_value_clause保持一致。

    4. unpivot_in_clause由多个input-data-column组成,input-data-column必须是from子句中table-expr中存在的列。input-data-column的列名或者as重命名子句是unpivot_for_clauseoutput-column的值。input-data-column的行值是unpivot_value_clausevalue-column的行值。

  • 参数

    • unpivot_value_clause

      unpivot_value_clause的行值是unpivot_in_clauseinput-data-column的行值,所以其类型由input-data-column的类型确定,unpivot_value_clause只可以是列,不支持为表达式,常量,函数等。

    • unpivot_for_clause

      output-column的值是unpivot_in_clauseinput-data-column的列名,其类型只可以为数字类型或者字符类型,并且unpivot_in_clause每个圆括号中对应项input-data-column的数据类型要属于同一大类,否则报错。默认类型为字符类型,默认值是由单个圆括号中,input-data-column的列名加'_'拼接而成,或者可使用as 子句进行重命名。

    • unpivot_in_clause

      unpivot_in_clause由多个input-data-column组成,只可以为列,且必须在table-expr中存在,不支持为常量,表达式,函数等。在unpivot_in_clause中调用to_char等类型转换函数,或者进行类型转换都是不允许的。

    • 列数限制

      unpivot的列数没有明确限制,因为oracle的视图和表的列数限制为1000列,所以只需要确保output-column + value-column + 原表未用于列转行的列 <=视图/表的列数限制,即可

    • unpivot函数的结合性

      unpivot的函数的结合性和pivot函数的结合性相同,即就近优先。unpivot函数先与from子句中位置就近的成员优先结合,进行行转列操作,然后再返回一张已经进行行转列的视图。如下所示。

      SELECT * from table-expr-1 unpivot_value_clause, table-expr-2 unpivot_value_clause;
      

      先对table-expr-1 进行列转行操作得到v1视图,再对table-expr-2 进行列转行操作得到v2视图,然后v1与v2进行join操作得到新的视图v3,最后执行select * from v3;

    • as重命名

      unpivot_in_clause子句支持as重命名语法,重命名的数据类型要求,与unpivot_in_clause的数据类型要求保持一致,否则报错。

      as重命名子句中,可以调用函数。不支持聚集函数,不支持窗口函数,返回结果集的函数;可以为数学函数、字符串函数、数据类型格式化函数、时间/日期函数和操作符,支持自定义函数和过程,但必须为稳定的函数/过程。且这些函数的入参需要是常量或者表达式,不可以为列,否则报错。

  • 返回值

    返回列转行之后的结果集,即列转行之后的表。

  • 示例

    Sales表记录了商品的销售情况,现在要将"amount_sold"和"quantity_sold"转为行,如下所示。

    SELECT * FROM Sales;
    PRODUCT|ID|COUNTRY|CHANNEL|QUARTER|AMOUNT_SOLD|QUANTITY_SOLD|
    -------+--+-------+-------+-------+-----------+-------------+
    noodles| 1|China  |      3|      1|          7|          100|
    noodles| 2|China  |      3|      2|          7|          150|
    noodles| 2|China  |      4|      2|          7|          200|
    noodles| 2|China  |      5|      3|          7|          300|
    noodles| 3|China  |      9|      4|          7|          400|
    KFC    | 1|America|      3|      1|         20|          100|
    KFC    | 2|America|      4|      2|         20|          200|
    KFC    | 3|America|      5|      3|         20|          300|
    KFC    | 3|America|      9|      4|         20|          400|
    pizza  | 1|japan  |      3|      1|         15|          100|
    pizza  | 1|japan  |      4|      2|         15|          200|
    pizza  | 1|japan  |      5|      3|         15|          300|
    pizza  | 4|japan  |      9|      4|         15|          400|
    (13 rows)
    

    列转行操作,如下所示。

    SELECT * from sales unpivot (sold for sold_category in (AMOUNT_SOLD, QUANTITY_SOLD));
    |PRODUCT|ID |COUNTRY|CHANNEL|QUARTER|SOLD_CATEGORY|SOLD|
    |-------|---|-------|-------|-------|-------------|----|
    |noodles|1  |China  |3      |1         |AMOUNT_SOLD  |7   |
    |noodles|1  |China  |3      |1         |QUANTITY_SOLD|100 |
    |noodles|2  |China  |3      |2         |AMOUNT_SOLD  |7   |
    |noodles|2  |China  |3      |2         |QUANTITY_SOLD|150 |
    |noodles|2  |China  |4      |2         |AMOUNT_SOLD  |7   |
    ... ...
    |pizza  |1  |japan  |3      |1         |AMOUNT_SOLD  |15  |
    |pizza  |1  |japan  |3      |1         |QUANTITY_SOLD|100 |
    |pizza  |1  |japan  |4      |2         |AMOUNT_SOLD  |15  |
    |pizza  |1  |japan  |4      |2         |QUANTITY_SOLD|200 |
    |pizza  |1  |japan  |5      |3         |AMOUNT_SOLD  |15  |
    |pizza  |1  |japan  |5      |3         |QUANTITY_SOLD|300 |
    |pizza  |4  |japan  |9      |4         |AMOUNT_SOLD  |15  |
    |pizza  |4  |japan  |9      |4         |QUANTITY_SOLD|400 |
    (26 rows)
    

    从返回的结果可以看出,原表的amount_soldquantity_sold进行了列转行的操作,新增了两个新列soldsold_category

    amount_soldquantity_sold的列名,变成了sold_category列的行值,而原来的amount_soldquantity_sold的行值,被填充到了sold列中。


5.数据类型

UXDB 提供了丰富的原生数据类型供用户使用。用户可以使用CREATE TYPE命令向UXDB添加新类型。

表 数据类型显示了所有内置的通用数据类型。大多数“别名”列中列出的替代名称是由于历史原因而在UXDB 内部使用的名称。此外,一些内部使用或已弃用的类型是可用的,但未在此处列出。

表 数据类型

名称 别名 描述
bigint int8 有符号的八字节整数
bigserial serial8 自增的八字节整数
bit [ (n) ] 固定长度的位字符串
bit varying [ (n) ] varbit [ (n) ] 可变长度的位字符串
boolean bool 逻辑布尔值(真/假)
box 平面上的矩形框
bytea 二进制数据(“字节数组”)
character [ (n) ] char [ (n) ] 固定长度的字符字符串
character varying [ (n) ] varchar [ (n) ] 可变长度的字符字符串
cidr IPv4或IPv6网络地址
circle 平面上的圆
date 日历日期(年、月、日)
double precision float8 双精度浮点数(8字节)
inet IPv4或IPv6主机地址
integer intint4 有符号的四字节整数
interval [ fields ] [ (p) ] 时间跨度
json 文本JSON数据
jsonb 二进制JSON数据,分解的
line 平面上的无限线
lseg 平面上的线段
macaddr MAC(媒体访问控制)地址
macaddr8 MAC(媒体访问控制)地址(EUI-64格式)
money 货币金额
numeric [ (p, s) ] decimal [ (p, s) ] 可选择精度的精确数值
path 平面上的几何路径
ux_lsn UXDB日志序列号
ux_snapshot 用户级事务ID快照
point 平面上的几何点
polygon 平面上的封闭几何路径
real float4 单精度浮点数(4字节)
smallint int2 带符号的两字节整数
smallserial serial2 自增的两字节整数
serial serial4 自增的四字节整数
set('label', ...) 存储集合的数据类型,标签数量支持1到64个
text 可变长度的字符字符串
time [ (p) ] [ without time zone ] 一天中的时间(没有时区)
time [ (p) ] with time zone timetz 一天中的时间,包括时区
timestamp [ (p) ] [ without time zone ] 日期和时间(没有时区)
timestamp [ (p) ] with time zone timestamptz 日期和时间,包括时区
tsquery 文本搜索查询
tsvector 文本搜索文档
txid_snapshot 用户级事务ID快照(已弃用;请参见ux_snapshot
uuid 通用唯一标识符
xml XML数据

兼容性

以下类型(或其拼写)由SQL指定:bigintbitbit varyingbooleancharcharacter varyingcharactervarchardatedouble precisionintegerintervalnumericdecimalrealsmallintdecnumbertime(带或不带时区)、timestamp(带或不带时区)、xml

每种数据类型都有由其输入和输出函数确定的外部表示形式。许多内置类型具有明显的外部格式。然而,一些类型要么是UXDB独有的,例如几何路径,要么有几种可能的格式,例如日期和时间类型。一些输入和输出函数不可逆,即输出函数的结果与原始输入相比可能会失去精度。

MySQL模式下,支持 decimal(0),decimal(0,0)数据类型,等价于decimal(10,0)。

允许插入空串基础数据类型仅支持在oracle模式下使用。

表 允许插入空串基础数据类型

类型大类具体数据类型允许空串插入展现形式
数值类型 (N)int2, int4, int8, numeric, float4, float8允许NULL 值
字符类型 (S)char, varchar, varchar2, nvarchar2, text, name允许NULL 值
日期/时间 (D/T)timestamp, timestamptz, date, time, timetz, interval允许NULL 值
位串类型 (V)bit(n), varbit(n)允许NULL 值
二进制 (B)binary, varbinary, raw, bytea允许NULL 值
JSON (A)json, jsonb允许NULL 值
其他/大对象oid, blob, clob允许NULL 值
布尔类型 (B)boolean允许NULL 值

oracle模式下,空字符串或者NULL与其他值拼接时,忽略空字符串或者NULL,返回其他值;拼接的值中只有NULL或空串时,返回NULL。

oralce模式下,函数入参中存在空字符串或者NULL时:

  • 函数中的可选参数为空字符串或者NULL时,与没有该参数的执行结果一致,包含可选参数的函数有:regexp_replac、regexp_split_to_table、regexp_split_to_array、string_to_array、​array_to_string以及文本搜索类函数。

  • 必选参数均按照空字符串处理的函数有:find_in_set、regexp_split_to_table、regexp_split_to_array、replace。

  • 必选参数均按照NULL处理的函数有:文本搜索类函数、Json函数。

  • 必选参数中既有按照NULL处理又有按照空串处理的函数有:regexp_replace、string_to_array、array_to_string、xml函数。

表 参数为NULL或空串时函数返回值表

函数类型代表函数处理逻辑(空串或 NULL)
字符串函数find_in_set参数 1、2 均按空串处理,返回具体位置。
regexp_replace参数 1 为 NULL/空串则返回 NULL;参数 2 为 NULL/空串则返回参数 1;参数 3 按空串处理。
replace参数 1、2、3 均按空串处理。
数组函数string_to_array参数 1NULL 处理,返回 NULL;参数 2 按空串处理。
array_to_string参数 1(数组本身)按 NULL 处理会报错;分隔符(参数 2)按空串处理。
JSON 函数json_object, jsonb_build_object统一按 NULL 处理(即结果通常为 NULL 或跳过)。
文本搜索to_tsvector, to_tsquery非可选参数为空串或 NULL 时,按 NULL 处理,直接返回 NULL。
XML 函数table_to_xml, query_to_xml参数 1, 2, 3 为 NULL/空串时返回 NULL;参数 4(命名空间)按空串处理。

oracle模式下,字符函数的返回值由空字符串改成NULL,涉及的函数包括:concat、concat_ws、substring、substr、trim、ltrim、rtrim、left、lpad、rpad、repeat、​right。

数组、范围类型、组合类型中空串兼容。将类型按照大类区分,数组元素类型为数值类型、日期时间类型、bool类型、大对象及OID类型时,空串当成NULL;数组元素类型为字符类型、位串类型、JSON类型时,按照空串处理,特殊表现的类型有bit(n)、bytea。组合类型中使用使用表达式插入时,row(null)与row('')中空串与NULL均当成NULL处理。

5.1.数值类型

数值类型包括两字节、四字节和八字节的整数、四字节和八字节的浮点数以及可选精度的十进制数。表 数值类型列出了可用的类型。

表 数值类型

名称存储大小描述范围 / 精度
smallint2 字节小范围整数-32,768 到 +32,767
integer4 字节整数的典型选择约 -21 亿 到 +21 亿
bigint8 字节大范围整数约 $\pm 9.22 \times 10^{18}$
decimal(p,s)可变用户指定的精度,精确$p \in [1, 1000], s \in [-1000, 1000], p \ge s$
numeric(p,s)可变用户指定的精度,精确同上
real4 字节可变精度,不精确6 位小数精度
double precision8 字节可变精度,不精确15 位小数精度
smallserial2 字节小自增整数1 到 32,767
serial4 字节自增整数1 到 2,147,483,647
bigserial8 字节大自增整数1 到 $9,223,372,036,854,775,807$

数值类型的常量语法在词法结构中描述。数值类型具有完整的相应算术运算符和函数。

5.1.1.整数类型

类型 smallintintegerbigint存储各种范围的整数,即没有小数部分的数字。尝试存储超出允许范围的值将导致错误。

类型 integer 是常见选择,因为它在范围、存储大小和性能之间提供了最佳平衡。如果磁盘空间很紧张,通常只使用 smallint类型。当 integer 类型的范围不足时,设计使用 bigint 类型。

SQL 仅指定整数类型 integer(或 int)、smallintbigint。类型名称 int2int4int8 是扩展名,也被一些其他 SQL 数据库系统使用。

5.1.2.任意精度数字

numeric 类型可以存储具有非常多位数字的数字。它特别适用于存储需要精确度的货币金额和其他数量。对于 numeric值的计算在可能的情况下会产生精确的结果,例如加法、减法、乘法。但是,与整数类型或下一节中描述的浮点类型相比,numeric值的计算非常缓慢。

我们在下面使用以下术语: numeric 的精度是整数部分和小数部分中所有有效数字的总数,即小数点两侧的数字的数量。numeric标度 是小数点右侧的小数位数。因此,数字 23.5141 的精度为 6,标度为 4。整数可以视为标度为零。

numeric 列的最大精度和最大标度都可以进行配置。要声明类型为 numeric 的列,请使用以下语法:

NUMERIC(precision, scale)

精度必须为正数,而标度可以为正数或负数(见下文),取值范围为-1000到8191。或者:

NUMERIC(precision)

选择标度为 0。指定:

NUMERIC

没有任何精度或标度会创建一个 “无限制的 numeric”列,其中可以存储任意长度的数字值,最多达到实现限制。这种类型的列不会强制将输入值转换为任何特定的标度,而具有声明标度的numeric 列将强制将输入值转换为该标度。(SQL 标准要求默认标度为0,即强制转换为整数精度。如果您关心可移植性,请始终明确指定精度和标度。)

注意

numeric 类型声明中可以明确指定的最大精度为 1000。无限制的 numeric 列受到表 数值类型中描述的限制。

如果要存储的值的标度大于列的声明标度,则系统将将该值舍入到指定的小数位数。然后,如果小数点左侧的数字的数量超过声明精度减去声明标度,则会引发错误。例如,声明为

NUMERIC(3, 1)

将值舍入到 1 个小数位,并且可以存储介于 -99.9 和 99.9 之间的值。

从 UXDB 2122开始,允许声明具有负标度的 numeric列。然后,值将舍入到小数点左侧。精度仍然表示最大非舍入数字的数量。因此,声明为

NUMERIC(2, -3)

将值舍入到最接近的千位,并且可以存储介于 -99000 和 99000之间的值。还允许声明标度大于声明精度的列。精度。这样的列只能保存小数值,并且需要小数点右侧的零位数至少为声明的精度减去声明的小数位数。例如,声明为

NUMERIC(3, 5)

的列将四舍五入到5个小数位,并且可以存储-0.00999到0.00999之间的值,包括这两个值。

注意

UXDB允许numeric类型声明中的小数位数为-1000到1000的任何值。然而,SQL标准要求小数位数在0到precision的范围内。使用超出该范围的小数位数可能无法在其他数据库系统中移植。

数值在物理上存储时没有任何额外的前导或尾随零。因此,列的声明精度和小数位数是最大值,而不是固定的分配。(在这个意义上,numeric类型更类似于varchar(n)而不是char(n)。)实际存储要求是每四个小数位组的两个字节,加上三到八个字节的开销。

除了普通的数值之外,numeric类型还有几个特殊值:

Infinity
-Infinity
NaN

这些值来自IEEE 754标准,分别表示“无穷大”、“负无穷大”和“非数字”。在SQL命令中将这些值作为常量写入时,必须在它们周围加上引号,例如UPDATE table SET x = '-Infinity'。在输入时,这些字符串以不区分大小写的方式被识别。无穷大的值也可以拼写为inf-inf

无穷大的值的行为符合数学期望。例如,Infinity加上任何有限值等于InfinityInfinity加上Infinity也是如此;但是Infinity减去Infinity产生NaN(不是一个数字),因为它没有明确定义的解释。请注意,只有在一个无约束的numeric列中才能存储无穷大,因为它在概念上超过了任何有限精度限制。

NaN(不是一个数字)的值用于表示未定义的计算结果。通常,任何带有NaN输入的操作都会产生另一个NaN。唯一的例外是当操作的其他输入是这样的,即如果将NaN替换为任何有限或无限数值,则会得到相同的输出;然后,该输出值也用于NaN。(这个原则的一个例子是,NaN的零次幂等于一。)

注意

在大多数实现中,“不是一个数字”的概念中,NaN被认为不等于任何其他数值(包括NaN)。为了允许要排序和用于基于树的索引的numeric值,UXDB将NaN值视为相等,并大于所有非NaN值。

类型decimalnumeric是等效的。这两种类型都是SQL标准的一部分。

在舍入值时,numeric类型将平局舍入到零的一侧,而(在大多数机器上)realdouble precision类型将平局舍入到最近的偶数。例如:

SELECT x,
  round(x::numeric) AS num_round,
  round(x::double precision) AS dbl_round
FROM generate_series(-3.5, 3.5, 1) as x;
  x   | num_round | dbl_round
------+-----------+-----------
 -3.5 |        -4 |        -4
 -2.5 |        -3 |        -2
 -1.5 |        -2 |        -2
 -0.5 |        -1 |        -0
  0.5 |         1 |         0
  1.5 |         2 |         2
  2.5 |         3 |         2
  3.5 |         4 |         4
(8 rows)

5.1.3.浮点数类型

realdouble precision是不精确的、可变精度的数值类型,double precision与double类型相同。在当前支持的所有平台上,这些类型都是IEEE标准754的实现,用于二进制浮点算术(分别为单精度和双精度),只要底层处理器、操作系统和编译器支持它。

不精确意味着有些值无法精确转换为内部格式,并存储为近似值,因此存储和检索值可能会显示轻微的差异。管理这些错误以及它们如何通过计算传播是数学和计算机科学的一个完整分支,这里不会讨论,除了以下几点:

  • 如果需要精确的存储和计算(例如货币金额),请改用numeric类型。

  • 如果要对这些类型进行重要的复杂计算,特别是如果您依赖于某些边界情况(无穷大、下溢),则应仔细评估实现。

  • 比较两个浮点值是否相等可能不总是按预期工作。

在当前支持的所有平台上,real类型的范围约为1E-37到1E+37,精度至少为6个小数位。double precision类型的范围约为1E-307到1E+308,精度至少为15位数字。太大或太小的值会导致错误。如果输入数字的精度太高,则可能会进行舍入。太接近零的数字如果与零不可区分,则会导致下溢错误。

默认情况下,浮点值以文本形式输出,以其最短精确的十进制表示形式表示;产生的十进制值比任何其他在相同二进制精度中可表示的值更接近于真实存储的二进制值。(但是,为了避免广泛存在的输入例程不正确地遵守四舍五入到最近偶数规则的错误,输出值当前从不完全处于两个可表示值之间。)这个值将对于float8 值,最多使用 17 个有效十进制数字;对于 float4 值,最多使用 9 个有效数字。

注意

这种最短精确输出格式比历史上的四舍五入格式生成速度快得多。

为了与旧版本的 UXDB 生成的输出兼容,并允许输出精度降低,可以使用extra_float_digits参数选择四舍五入的十进制输出。将值设置为 0 将恢复先前的默认值,即将值舍入为 6(对于 float4)或 15(对于float8)个有效十进制数字。将值设置为负数将进一步减少数字的数量;例如,-2 将将输出舍入为 4 或 13 个数字。

任何大于 0 的extra_float_digits值都选择最短精确格式。

注意

历史上,希望获得精确值的应用程序必须将extra_float_digits设置为3才能获得它们。为了在版本之间实现最大兼容性,它们应继续这样做。

除了普通的数字值外,浮点类型还有几个特殊值:

Infinity
-Infinity
NaN

它们分别表示 IEEE 754 的特殊值“无穷大”、“负无穷大”和“非数字”。在 SQL命令中将这些值作为常量写入时,必须在它们周围加上引号,例如UPDATE table SET x = '-Infinity'。在输入时,这些字符串以不区分大小写的方式被识别。无穷大的值也可以拼写为inf-inf

注意

IEEE 754 规定 NaN 不应等于任何其他浮点值(包括 NaN)。为了允许浮点值排序并在基于树的索引中使用,UXDB将 NaN 值视为相等,并大于所有非 NaN 值。

UXDB 还支持 SQL 标准符号 floatfloat(p) 来指定不精确的数字类型。这里,p指定在二进制位中的最小可接受精度。UXDB 接受 float(1)float(24)作为选择 real 类型,而 float(25)float(53) 则选择 doubleprecision。超出允许范围的p值会引发错误。未指定精度的 float 被视为 double precision

5.1.4.串行类型

注意

本节描述了一种 UXDB 特有的创建自增列的方式。另一种方式是使用 SQL 标准的 identity 列特性。

数据类型 smallserialserialbigserial不是真正的类型,而只是一种方便的表示方式,用于创建唯一标识符列(类似于其他一些数据库支持的AUTO_INCREMENT 属性)。在当前实现中,指定:

CREATE TABLE tablename (
colname SERIAL
);

等同于指定:

CREATE SEQUENCE tablename_colname_seq AS integer;
CREATE TABLE tablename (
    colname integer NOT NULL DEFAULT nextval('tablename_colname_seq')
);
ALTER SEQUENCE tablename_colname_seq OWNED BY tablename.colname;

因此,我们创建了一个整数列,并安排其默认值从序列生成器中分配。应用了 NOT NULL约束以确保不能插入空值。(在大多数情况下,您还需要附加一个UNIQUEPRIMARY KEY约束,以防止意外插入重复值,但这不是自动的。)最后,将序列标记为该列的所有者,以便在删除列或表时删除该序列。

注意

因为 smallserialserialbigserial使用序列实现,即使没有删除任何行,序列中的值序列中可能会有“空洞”或间隔。即使包含该值的行未成功插入到表列中,从序列分配的值仍然会被“使用”。例如,如果插入事务回滚,则可能会发生这种情况。

要将序列的下一个值插入到 serial 列中,请指定应将 serial 列分配其默认值。这可以通过在 INSERT语句中排除列的列表或使用 DEFAULT 关键字来完成。

类型名称 serialserial4 是等效的:两者都创建 integer 列。类型名称 bigserialserial8 可以用来创建更大的整数列。相同的方式,只是它们创建一个 bigint 列。如果您预计表的生命周期内会使用超过231 个标识符,则应使用 bigserial。类型名称 smallserialserial2也以相同的方式工作,只是它们创建一个 smallint 列。

serial 列创建的序列在拥有列被删除时会自动删除。您可以在不删除列的情况下删除序列,但这将强制删除列默认表达式。

5.2.货币类型

money类型存储具有固定小数精度的货币金额;请参见表 货币类型。小数精度由数据库的lc_monetary设置确定。表中显示的范围假定有两个小数位。接受各种格式的输入,包括整数和浮点字面量,以及典型的货币格式,例如'$1,000.00'。输出通常采用后一种形式,但取决于语言环境。

表 货币类型

名称存储大小描述范围
money8 字节货币金额-92233720368547758.08 到 +92233720368547758.07

由于此数据类型的输出与语言环境有关,因此将 money 数据加载到具有不同 lc_monetary设置的数据库中可能无法正常工作。为避免问题,在将转储恢复到新数据库之前,请确保lc_monetary 具有与转储数据库相同或等效的值。

numericintbigint 数据类型的值可以转换为 money。可以通过先将其转换为numeric,然后再进行转换,例如:

SELECT '12.34'::float8::numeric::money;

但是,不建议这样做。由于可能存在舍入误差,不应使用浮点数处理货币。

money 值可以转换为 numeric 而不会失去精度。转换为其他类型可能会丢失精度,并且还必须分两个阶段进行:

SELECT '52093.89'::money::numeric::float8;

将一个money值除以一个整数值时,会将小数部分截断为零。要获得四舍五入的结果,请除以浮点值,或在除法之前将money值转换为numeric,然后再转换回money。后者比较可取,以避免出现精度损失。当一个 money值被另一个money值除时,结果是 double precision(即一个纯数值,而不是货币);货币单位在除法中相互抵消。

5.3.字符类型

表  字符类型

名称描述
character varying(n), varchar(n)可变长度,受长度限制 $n$ 约束。
character(n), char(n)固定长度,不足部分以空格填充。
text可变长度,无预定义长度限制。
long存储可变长字符串,功能等同于 text 类型。

表 字符类型显示了 UXDB 中可用的通用字符类型。

long类型与text类型相同,属于字符类型。存储可变长字符串,能被存储的最长的字符串是1GB。

SQL 定义了两种主要的字符类型: character varying(n)character(n),其中 n是一个正整数。这两种类型都可以存储长度不超过 n个字符(而不是字节)的字符串。如果试图将一个更长的字符串存储到这些类型的列中,除非多余的字符都是空格,否则会导致错误,此时字符串将被截断为最大长度。(这种有点奇怪的例外是SQL 标准所要求的。)如果要存储的字符串比声明的长度短,类型为 character 的值将会填充空格;类型为 character varying 的值将简单地存储较短的字符串。

如果将一个值显式地转换为 character varying(n)或如果数据类型为character(n),则超长值将被截断为n个字符而不会引发错误。(这也是SQL标准要求的。)

符号varchar(n)char(n)character varying(n)character(n)的别名。如果指定了长度,则长度必须大于零且不能超过10485760。没有长度说明符的character等同于character(1)。如果使用character varying而没有长度说明符,则该类型接受任何大小的字符串。后者是UXDB的扩展。

此外,UXDB提供了text类型,它可以存储任意长度的字符串。虽然text类型不在SQL标准中,但其他几个SQL数据库管理系统也有它。

类型为character的值在物理上用空格填充到指定的宽度n,并以此方式存储和显示。但是,在比较两个character类型的值时,尾随空格被视为语义上不重要的,并被忽略。在空格有意义的排序规则中,这种行为可能会产生意外的结果;例如,SELECT 'a '::CHAR(2) collate "C" < E'a\n'::CHAR(2)返回true,即使C语言环境认为空格大于换行符。在将character值转换为其他字符串类型时,尾随空格将被删除。请注意,在character varyingtext值以及使用模式匹配(即LIKE和正则表达式)时,尾随空格是语义上重要的。

可以存储在这些数据类型中的字符由数据库字符集确定,该字符集在创建数据库时选择。无论特定的字符集如何,代码为零的字符(有时称为NUL)都无法存储。

短字符串(最多126个字节)的存储要求为1个字节加上实际字符串,包括在character的情况下的空格填充。长字符串的开销为4个字节,而不是1个字节。系统会自动压缩长字符串,因此磁盘上的物理需求可能会更少。非常长的值也存储在后台表中,以便它们不会干扰对较短列值的快速访问。在任何情况下,可以存储的最长可能字符字符串约为1GB。(在数据类型声明中允许的n的最大值小于该值。更改这个值是没有用的,因为使用多字节字符编码时,字符数和字节数可能非常不同。如果希望存储没有特定上限的长字符串,请使用没有长度说明符的textcharacter varying,而不是制定任意长度限制。)

提示

这三种类型之间没有性能差异,除了在使用填充空格的character时增加了存储空间。类型,以及在存储到长度受限列时检查长度所需的一些额外CPU周期。虽然在其他一些数据库系统中,character(n) 有性能优势,但在UXDB中没有这样的优势;实际上,由于其额外的存储成本,character(n)通常是三种类型中最慢的。在大多数情况下,应使用textcharacter varying代替。

有关字符串字面值语法的信息,请参见字符串常量

示例 使用字符类型

CREATE TABLE test1 (a character(4));
INSERT INTO test1 VALUES ('ok');
SELECT a, char_length(a) FROM test1; -- (1)

a   | char_length
------+-------------
ok   |           2


CREATE TABLE test2 (b varchar(5));
INSERT INTO test2 VALUES ('ok');
INSERT INTO test2 VALUES ('good      ');
INSERT INTO test2 VALUES ('too long');
ERROR:  value too long for type character varying(5)
INSERT INTO test2 VALUES ('too long'::varchar(5)); -- 显式截断
SELECT b, char_length(b) FROM test2;

b   | char_length
-------+-------------
ok    |           2
good  |           5
too l |           5

UXDB中还有另外两种固定长度的字符类型,如表 特殊字符类型所示。这些类型不适用于通用用途,仅适用于内部系统目录中的使用。类型name用于存储标识符。它的长度当前定义为64字节(63个可用字符加终止符),但应在C源代码中使用常量NAMEDATALEN进行引用。长度在编译时设置(因此可调整用于特殊用途);默认的最大长度可能会在将来的版本中更改。类型"char"(注意引号)与char(1)不同,它只使用一个字节的存储空间,因此只能存储单个ASCII 字符。它在系统目录中用作简单的枚举类型。

表 特殊字符类型

名称存储大小描述
"char"1 字节单字节内部类型:通常用于系统表中存储各种状态位、标志符或类别码(注意必须带双引号以区别于标准 char)。
name64 字节对象名称的内部类型:专门用于存储表名、索引名、角色名等系统标识符。其长度由编译参数 NAMEDATALEN 决定(默认 64 字节,包含结尾空字符即实际可用 63 字节)。

5.3.1.SET

用于存储集合的数据类型。

SET数据类型支持最多64个成员值,可以使用逗号分隔的字符串来表示。SET类型的存储空间不会受到成员值数量的影响,只与集合大小有关。

  • 示例

    1. set类型增删改查、比较、计算

      uxdb=# create table t1(v1 set('a', 'b', 'c'));
      CREATE TABLE
      uxdb=# insert into t1 values (1), ('b'), (3);
      INSERT 0 3
      uxdb=# select * from t1;
      v1  
      -----
      a
      b
      a,b
      (3 rows)
      
      uxdb=# select v1+0 from t1;
      ?column? 
      ----------
             1
             2
             3
      (3 rows)
      
      uxdb=# select v1 from t1 where v1 > 1;
      v1  
      -----
      b
      a,b
      (2 rows)
      
      uxdb=# select v1 from t1 where v1 > 'a';
      v1  
      -----
      b
      a,b
      (2 rows)
      
      uxdb=# delete from t1 where v1 = 2;
      DELETE 1
      uxdb=# 
      uxdb=# select v1 from t1;
      v1  
      -----
      a
      a,b
      (2 rows)
      
      uxdb=# update t1 SET v1 = 'a,b,c' where v1 = 1;
      UPDATE 1
      uxdb=# select v1 from t1;
      v1   
      -------
      a,b
      a,b,c
      (2 rows)
      
    2. set类型修改

      uxdb=# delete from t1;
      DELETE 2
      uxdb=# insert into t1 values (1), (2), (3);
      INSERT 0 3
      uxdb=# select * from t1;
      v1  
      -----
      a
      b
      a,b
      (3 rows)
      
      uxdb=# alter table t1 alter column v1 type int;
      ALTER TABLE
      uxdb=# select * from t1;
      v1 
      ----
      1
      2
      3
      (3 rows)
      
      uxdb=# alter table t1 alter column v1 type set('a', 'b');
      ALTER TABLE
      uxdb=# select * from t1;
      v1  
      -----
      a
      b
      a,b
      (3 rows)
      
      uxdb=# alter table t1 alter column v1 type set('a', 'b', 'd');
      ALTER TABLE
      uxdb=# insert into t1 values (7);
      INSERT 0 1
      uxdb=# select * from t1;
      v1   
      -------
      a
      b
      a,b
      a,b,d
      (4 rows)
      
      uxdb=# alter table t1 alter column v1 type text;
      ALTER TABLE
      uxdb=# select * from t1;
      v1   
      -------
      a
      b
      a,b
      a,b,d
      (4 rows)
      
      uxdb=# alter table t1 alter column v1 type set('a', 'b', 'c', 'd');
      ALTER TABLE
      uxdb=# select * from t1;
      v1   
      -------
      a
      b
      a,b
      a,b,d
      (4 rows)
      
      uxdb=# select v1+0 from t1;
      ?column? 
      ----------
             1
             2
             3
            11
      (4 rows)
      
  • 使用限制

    1. set类型根据成员数量,最大可存储UINT64范围的最大值,但是在与数字比较、计算时如果set内部数字范围超过另一数字操作数的类型范围将报错。

    2. SET类型插入数字、SET与SET数学计算时,最大支持INT8范围,超限报错。

    3. numeric类型转为SET类型时超过INT8范围报错。

    4. SET类型列现阶段不支持建立索引,不支持作主键、外键等。

    5. SET列现阶段不支持直接排序,但可强转为数字或字符类型排序。

5.4.二进制数据类型

bytea 数据类型允许存储二进制字符串;请参见表 二进制数据类型

表 二进制数据类型

名字 存储尺寸 描述
bytea 1 或 4 个字节加上实际的二进制字符串 可变长度的二进制字符串
raw 可变长度的二进制数据或十六进制字节字符串
varbinary 可变长度二进制数据类型
longvarbinary 加长版可变二进制类型数据

二进制字符串是八位字节(或字节)的序列。二进制字符串与字符字符串有两个区别。首先,二进制字符串特别允许存储值为零的八位字节和其他“不可打印”的八位字节(通常是十进制范围32 到 126之外的八位字节)。字符字符串不允许零八位字节,也不允许任何其他的八位字节值和八位字节值序列,这些值和序列在数据库选择的字符集编码方案中是无效的。其次,对二进制字符串的操作处理实际的字节,而对字符字符串的处理取决于区域设置。简而言之,二进制字符串适用于存储程序员认为是“原始字节”的数据,而字符字符串适用于存储文本。

bytea 类型支持两种输入和输出格式:“十六进制”格式和 UXDB的历史“转义”格式。这两种格式都始终被接受作为输入。输出格式取决于应用程序或工具的设置。

5.4.1.bytea十六进制格式

“hex”格式将每个字节的二进制数据编码为2个十六进制数字,最高有效位先。整个字符串以序列\x开头(以区别于转义格式)。在某些情况下,初始反斜杠可能需要通过加倍来转义(参见字符串常量)。对于输入,十六进制数字可以是大写或小写,并且在数字对之间允许使用空格(但不允许在数字对内部或在起始\x序列中使用空格)。十六进制格式与广泛的外部应用程序和协议兼容,并且转换速度比转义格式快,因此建议使用它。

示例:

SELECT '\xDEADBEEF';

5.4.2.bytea转义格式

“escape”格式是bytea类型的传统UXDB格式。它采用将二进制字符串表示为ASCII字符序列的方法,同时将那些无法表示为ASCII字符的字节转换为特殊的转义序列。如果从应用程序的角度来看,将字节表示为字符是有意义的,则此表示法可能很方便。但实际上,它通常会混淆二进制字符串和字符字符串之间的区别,而且所选择的特定转义机制有些笨重。因此,对于大多数新应用程序,应避免使用此格式。

在转义格式中输入bytea值时,必须转义某些值的八位字节,而所有八位字节都可以转义。通常,要转义一个八位字节,请将其转换为其三位八进制值,并在其前面加上反斜杠。反斜杠本身(八位字节十进制值92)也可以用双反斜杠表示。bytea 字面量转义八进制数显示了必须转义的字符,并在适用的情况下给出了替代转义序列。

bytea 字面量转义八进制数

十进制/八进制数值描述转义输入表示示例 (SQL)十六进制表示
0零字节 (Null)'\000''\000'::bytea\x00
39单引号'''''\047'''''::bytea\x27
92反斜杠'\\''\134''\\'::bytea\x5c
0-31 & 127-255“不可打印”字符'\xxx' (八进制值)'\001'::bytea\x01

需要转义不可打印八进制数的要求因语言环境设置而异。在某些情况下,您可以不转义它们。

须将单引号加倍的原因是这对于 SQL命令中的任何字符串字面量都是正确的。通用字符串字面量解析器会消耗最外层的单引号,并将任何一对单引号减少为一个数据字符。bytea输入函数看到的只是一个单引号,它将其视为普通数据字符。但是,bytea 输入函数将反斜杠视为特殊字符,其他行为是由该函数实现的。

在某些情况下,与上面所示的相比,反斜杠必须加倍,因为通用字符串字面量解析器还会将一对反斜杠减少为一个数据字符;请参见字符串常量

默认情况下,bytea 八进制数以 hex 格式输出。如果将bytea_output更改为escape,则“不可打印”八进制数将转换为它们的等效三位八进制值,并在前面加上一个反斜杠。大多数“可打印”八进制数以客户端字符集中的标准表示输出,例如:

SET bytea_output = 'escape';

SELECT 'abc \153\154\155 \052\251\124'::bytea;
     bytea
----------------
 abc klm *\251T

十进制值为92(反斜杠)的八进制数在输出中加倍。详细信息请参见表 bytea 转义八进制数

表 bytea 转义八进制数

十进制/八进制数值描述转义输出表示示例输出结果
92反斜杠\\'\134'::bytea\\
0-31 & 127-255“不可打印”字节\xxx (八进制值)'\001'::bytea\001
32-126“可打印”字节客户端字符集表示'\176'::bytea~

根据您使用的 UXDB 前端,您可能需要进行其他转义和反转义 bytea字符串的工作。例如,如果您的界面自动翻译这些内容,您可能还需要转义换行符和回车符。

5.4.3.raw

可变长度二进制数据或字节字符串。支持插入十六进制字符(0123456789abcdefABCDEF),将十六进制字符串转换成字节存储。在定义字段时必须指明最大长度n(n以字节为单位),如:RAW(n),n为长度;n最小值1,最大32767,n不能使用一个符号常量或变量来代替,n必须使用1..32767中的任一整数;raw(n)最多存储n*2 个十六进制字符,当输入字符个数大于n*2 时报错。

  • 示例

    CREATE table raw_tb (r raw(4));
    

    一、插入数据,并查看结果。

    uxdb=# insert into raw_tb values('1234');
    INSERT 0 1
    uxdb=# insert into raw_tb values('abcd');
    INSERT 0 1
    uxdb=# insert into raw_tb values('');
    INSERT 0 1
    uxdb=# select * from raw_tb;
    r   
    ------
    1234
    ABCD
    (3 rows)
    

    二、不支持插入非十六进制字符:

    uxdb=# insert into raw_tb values('xyz'); --error
    ERROR:  invalid hexadecimal digit: "x"
    LINE 1: insert into raw_tb values('xyz');
                                      ^
    uxdb=# insert into raw_tb values('0x1234');-- error
    ERROR:  invalid hexadecimal digit: "x"
    LINE 1: insert into raw_tb values('0x1234');
                                      ^
    uxdb=# insert into raw_tb values('\x6162');-- error
    ERROR:  invalid hexadecimal digit: "\"
    LINE 1: insert into raw_tb values('\x6162');
                                      ^
    uxdb=# insert into raw_tb values(' ');-- error
    ERROR:  invalid hexadecimal digit: " "
    LINE 1: insert into raw_tb values(' ');
                                      ^
    

表 raw类型转换函数

函数输入参数类型返回类型功能描述核心逻辑示例
hextoraw各种字符类型 (text/varchar2/clob等)raw将十六进制表示的字符串转换为二进制数据。输入 '1ac' $\rightarrow$ 输出 01AC (自动补齐前导0以凑整字节)uxdb=# select hextoraw('1ac'::TEXT);
hextoraw
----------
01AC
(1 row)
rawtohexrawvarchar2将二进制原始数据转换为十六进制字符串表示。输入 'acd'::raw $\rightarrow$ 输出 0ACDuxdb=# select rawtohex('acd'::raw);
rawtohex
----------
0ACD
(1 row)

5.4.4.binary

可变长度二进制数据类型,长度限制为[0, 255]。

  • 示例

    CREATE table t1 (col1 binary, col2 binary(10));
    

    binary(n)binary均可声明数据类型,n为非负整数,当不声明长度时,默认分配1字节。

    CREATE TABLE test_binary(PW BINARY(10));
    INSERT INTO test_binary VALUES('123');
    INSERT INTO test_binary VALUES(0x123);
    INSERT INTO test_binary VALUES('0x123');
    INSERT INTO test_binary VALUES(NULL);
    INSERT INTO test_binary VALUES('');
    INSERT INTO test_binary VALUES(123);
    INSERT INTO test_binary VALUES('MANAGER');
    
    SELECT * FROM test_binary;
            pw           
    ------------------------
    0x31323300000000000000
    0x01230000000000000000
    0x30783132330000000000
    
    0x00000000000000000000
    0x31323300000000000000
    0x4d414e41474552000000
    (7 rows)
    
    SELECT * FROM test_binary WHERE PW = 'MANAGER'::binary(10);
            pw           
    ------------------------
    0x4d414e41474552000000
    (1 row)
    
    SELECT * FROM test_binary WHERE PW = 0x4d414e41474552000000;
            pw           
    ------------------------
    0x4d414e41474552000000
    (1 row)
    
  • binary(n)和varbinary(n)区别说明

    参见varbinary

  • varbinary和binary类型操作符

    参见varbinary

5.4.5.varbinary

可变长度二进制数据类型。长度限制为[0, 65532]。

  • 示例

    CREATE table  t2 (col1 varbinary(10));
    

    varbinary(n)为可声明数据类型,n为非负整数,当不声明长度时,会报错。

    CREATE TABLE test_varbinary(PW VARBINARY(10));
    INSERT INTO test_varbinary VALUES('123');
    INSERT INTO test_varbinary VALUES(0x123);
    INSERT INTO test_varbinary VALUES('0x123');
    INSERT INTO test_varbinary VALUES(NULL);
    INSERT INTO test_varbinary VALUES('');
    INSERT INTO test_varbinary VALUES(123);
    INSERT INTO test_varbinary VALUES('MANAGER');
    
    SELECT * FROM test_varbinary;
            pw        
    ------------------
    0x313233
    0x0123
    0x3078313233
    
    0x
    0x313233
    0x4d414e41474552
    (7 rows)
    
    SELECT * FROM test_varbinary WHERE PW = 'MANAGER';
            pw        
    ------------------
    0x4d414e41474552
    (1 row)
    
    SELECT * FROM test_varbinary WHERE PW = '0X6D616E61676572';
    pw 
    ----
    (0 rows)
    
  • binary(n)和varbinary(n)区别说明

    对于binary类型,如果声明了长度n,那么在插入数据并存储时,会分配固定长度n。比如binary(10),如果插入数据为’123’(3字节),但是存储时依然会分配10字节存储空间,多出的字节用’\0’进行填充补齐,所以使用uxsql工具连接数据库时,binary类型会有末尾补零的情况发生。并且如果插入的16进制数据为奇数位,比如0x123,那么会在最高位补0,变为偶数位,如下所示。

    SELECT 0x123::binary;
    binary 
    --------
    0x0123
    (1 row)
    

    而对于varbinary类型,并不会分配固定长度n。如varbinary(10),插入数据为’123’(3字节),那么存储时只分配3字节长度。

  • 隐式转换规则

    表 隐式转换规则

    类型转换规则
    数值类型smallint、integer、bigint、numeric、real、double precision
    字符类型character varying(n), varchar(n)、character(n), char(n)、text
    时间类型timestamp、timestamptz、date、time、timetz
  • varbinary和Binary类型操作符

    支持操作符有!=,=,>=,<=,>,<,like。

    示例如下所示。

     CREATE TABLE test_varbinary (a VARBINARY(100), b VARBINARY(100));
     COPY test_varbinary FROM stdin;
     X0F	X10
     X1F	X11
     X2F	X12
     X3F	X13
     X8F	X04
     X000F	X0010
     X0123	XFFFF
     X2468	X2468
     XFA50	X05AF
     X1234	XFFF5
     \.
    
     SELECT a, b, a<b AS "a<b", a<=b AS "a<=b", a=b AS "a=b",
             a>=b AS "a>=b", a>b AS "a>b", a<>b AS "a<>b" FROM test_varbinary;
     SELECT a, b, a~~b AS "a~~b" FROM test_varbinary;
     DROP TABLE test_varbinary;
         a       |      b       | a<b | a<=b | a=b | a>=b | a>b | a<>b 
    --------------+--------------+-----+------+-----+------+-----+------
     0x583046     | 0x583130     | t   | t    | f   | f    | f   | t
     0x583146     | 0x583131     | f   | f    | f   | t    | t   | t
     0x583246     | 0x583132     | f   | f    | f   | t    | t   | t
     0x583346     | 0x583133     | f   | f    | f   | t    | t   | t
     0x583846     | 0x583034     | f   | f    | f   | t    | t   | t
     0x5830303046 | 0x5830303130 | t   | t    | f   | f    | f   | t
     0x5830313233 | 0x5846464646 | t   | t    | f   | f    | f   | t
     0x5832343638 | 0x5832343638 | f   | t    | t   | t    | f   | f
     0x5846413530 | 0x5830354146 | f   | f    | f   | t    | t   | t
     0x5831323334 | 0x5846464635 | t   | t    | f   | f    | f   | t
    
     (10 rows) 
    
     SELECT a, b, a~~b AS "a~~b" FROM test_varbinary;
         a       |      b       | a~~b 
    --------------+--------------+------
     0x583046     | 0x583130     | f
     0x583146     | 0x583131     | f
     0x583246     | 0x583132     | f
     0x583346     | 0x583133     | f
     0x583846     | 0x583034     | f
     0x5830303046 | 0x5830303130 | f
     0x5830313233 | 0x5846464646 | f
     0x5832343638 | 0x5832343638 | t
     0x5846413530 | 0x5830354146 | f
     0x5831323334 | 0x5846464635 | f
    
     (10 rows)
    
  • uxsql控制参数

    uxsql工具增加控制参数binary-as-hex,该参数只接受false/true,默认为true。该参数控制varbinary/binary二进制数据类型的显示。启动如下所示。

    uxsql  -U  uxdb  -d  uxdb  --binary-as-hex=[true/false]
    

    该参数设置为true时,binaryvarbinary表现为16进制,该参数为false时,binaryvarbinary的二进制字符串会根据当前数据库的编码规范做解码操作,转换为字符串。比如binary-as-hex = true时(默认为true),二进制数据类型为0x31323334,而binary-as-hex=false时,二进制数据类型为字符串'1234'。

  • 0x十六进制数说明

    目前uxdb支持0x 十六进制数的使用,输入形式为0xval,语法要求0x只能是小写,而val必须要是16进制数,即'0-9、A-F、a-f',否则报错。

    正确输入如下所示。

    0x01AF 
    0x01af
    

    不合法输入如下所示。

    0x0G (G是非法的16进制数字) 
    0X01AF (0X要写作0x)
    
  • 0x十六进制数的表现

    0xval默认返回是二进制类型varbinary,示例如下。

    uxdb=# select 0x123fd;
    ?column? 
    ----------
    0x0123FD
    (1 row)
    
    uxdb=# select ux_typeof(0x123fd);
    ux_typeof 
    -----------
    varbinary
    (1 row)
    

    支持0x 十六进制数作为varbinary/binary类型的输入,示例如下。

    uxdb=# create table varbinary_t1 (col1 varbinary(10));
    CREATE TABLE
    uxdb=# create table binary_t1 (col1 binary(10));
    CREATE TABLE
    uxdb=# insert into varbinary_t1 values (0x1234);
    INSERT 0 1
    uxdb=# insert into binary_t1 values (0x1234);
    INSERT 0 1
    uxdb=# select * from varbinary_t1;
      col1  
    --------
    0x1234
    (1 row)
    
    uxdb=# select * from binary_t1;
              col1          
    ------------------------
    0x12340000000000000000
    (1 row)
    
  • 0x十六进制数的计算

    如果对0x十六进制数进行加减乘除的数学运算或者逻辑运算,此时会将0x十六进制数切换到数值上下文,即0x十六进制数被作为数字被处理(目前暂不支持0x 十六进制的位运算)。

    uxdb=# select 0x123 + 0;
    ?column? 
    ----------
          291
    (1 row)
    
    uxdb=# select 0x123 & 0;
    ?column? 
    ----------
            0
    (1 row)
    
    uxdb=# select 0x123 & 1;
    ?column? 
    ----------
            1
    (1 row)
    

    数值上下文的0x 十六进制数的范围为-9223372036854775808 ~ +9223372036854775807。此时的0x十六进制数在数据库内部作为bigint类型存在,其计算特性,隐式转换规则等都与bigint类型相同。

  • 在uxdb数据库MySQL模式下使用

    输入如下所示。

    Select b’10’;
    Select B’10’;
    Select 0b10;
    

    其中的10可以为任意的10序列,其数据类型为varbinary类型。

    1. 非法输入非01的数字会报错,注意0b形式的输入没有单引号。

    2. 输入的01序列长度限制为varbinary类型的最大长度65532,故01序列长度限制在65532*8以内。

  • B二进制形式输入的表现

    默认返回类型为varbinary。

    uxdb=# create table test_B_input (col varbinary(2));
    CREATE TABLE
    uxdb=# insert into test_B_input values (b'10100011');
    INSERT 0 1
    uxdb=# select * from test_B_input;
     col
     ------
     0xA3
     (1 row)
    
  • B二进制形式输入的计算

    uxdb=# select b'1010' + 3;
     ?column?
     ----------
          13
     (1 row)
    uxdb=# select b'1010' & 0;
     ?column?
     ----------
             0
     (1 row)
    

    数值上下文的B二进制数的范围为-9223372036854775808 ~ +9223372036854775807。此时的B二进制输入在数据库内部作为bigint类型存在,其计算特性,隐式转换规则等都与bigint类型相同。

5.4.6.longvarbinary

加长版可变二进制类型数据,使用方法参见varbinary

5.5.日期/时间类型

UXDB 支持完整的 SQL日期和时间类型,如表 日期/时间类型所示。日期按照公历计算,即使在该日历引入之前的年份也是如此。

表 日期/时间类型

名称存储大小描述最小值最大值分辨率
timestamp [ (p) ] [ without time zone ]8 字节日期和时间(无时区)公元前 4713 年公元 294276 年1 微秒
timestamp [ (p) ] with time zone8 字节日期和时间,带时区公元前 4713 年公元 294276 年1 微秒
date4 字节日期(无时间)公元前 4713 年公元 5874897 年1 天
time [ (p) ] [ without time zone ]8 字节时间(无日期)00:00:0024:00:001 微秒
time [ (p) ] with time zone12 字节时间(无日期),带时区00:00:00+155924:00:00-15591 微秒
interval [ fields ] [ (p) ]16 字节时间间隔前 178000000 年后 178000000 年1 微秒

注意

SQL 标准要求仅写 timestamp 与写 timestamp without time zone 等效,UXDB遵循该行为。timestamptz 被接受为 timestamp with time zone 的缩写;这是一个UXDB 扩展。

timetimestampinterval 可以接受一个可选的精度值p,指定保留秒字段中的小数位数。默认情况下,精度没有明确的限制。p 的允许范围是 0 到 6。

timestamp类型支持插入0::int

interval 类型有一个额外的选项,即通过写入以下短语之一来限制存储字段的集合:

YEAR
MONTH
DAY
HOUR
MINUTE
SECOND
YEAR TO MONTH
DAY TO HOUR
DAY TO MINUTE
DAY TO SECOND
HOUR TO MINUTE
HOUR TO SECOND
MINUTE TO SECOND

请注意,如果同时指定了fieldsp,则 fields 必须包括 SECOND,因为精度仅适用于秒。

SQL 标准定义了 time with time zone类型,但该定义具有引起问题的有用性属性。在大多数情况下,datetimetimestamp without time zonetimestamp with time zone的组合应该提供任何应用程序所需的完整的日期/时间功能范围。

5.5.1.日期/时间输入

几乎可以接受任何合理的格式的日期和时间输入,包括 ISO 8601、SQL兼容、传统的UXDB等格式中,日期输入中的日、月、年的顺序是不明确的,因此支持指定这些字段的预期顺序。将DateStyle参数设置为MDY以选择月-日-年解释,DMY以选择日-月-年解释,或YMD以选择年-月-日解释。

UXDB在处理日期/时间输入方面比SQL标准要灵活得多。了解日期/时间输入的确切解析规则以及被识别的文本字段,包括月份、星期几和时区。

请记住,任何日期或时间文字输入都需要用单引号括起来,就像文本字符串一样。有关更多信息,请参阅其他类型的常量。SQL需要以下语法

type [ (p) ] 'value'

其中p是可选的精度规范,给出秒字段中小数位的数量。精度可以为timetimestampinterval类型指定,并且可以从0到6。如果在常量规范中未指定精度,则默认为文字值的精度(但不超过6位数)。

5.5.1.1.日期

表 日期输入显示了date类型的一些可能的输入。在uxdb数据库mysql运行模式下,date使用宽松语法,任何标点符号都可以用作日期部分或时间部分之间的分隔符,兼容mysql数据库。

表 日期输入

示例描述
1999-01-08ISO 8601;在任何模式下的 1 月 8 日(推荐格式)
January 8, 1999在任何 datestyle 输入模式下都是明确的
1/8/1999MDY 模式下是 1 月 8 日;在 DMY 模式下是 8 月 1 日
1/18/1999MDY 模式下是 1 月 18 日;在其他模式下被拒绝
01/02/03MDY 模式下是 2003 年 1 月 2 日;在 DMY 模式下是 2003 年 2 月 1 日;在 YMD 模式下是 2001 年 2 月 3 日
1999年1月8日任何模式下的 1 月 8 日
Jan-08-1999任何模式下的 1 月 8 日
08-Jan-1999任何模式下的 1 月 8 日
99-Jan-08YMD 模式下的 1 月 8 日,否则出错
08-Jan-991 月 8 日,除了在 YMD 模式下出错
Jan-08-991 月 8 日,除了在 YMD 模式下出错
19990108ISO 8601; 任何模式下的 1999 年 1 月 8 日
990108ISO 8601; 任何模式下的 1999 年 1 月 8 日
1999.008年和年中天数
J2451187儒略日
公元前99年1月8日公元前 99 年
5.5.1.2.时间

时间类型是time [ (p) ] without time zonetime [ (p) ] with time zone. time等同于time without timezone

这些类型的有效输入包括一天中的时间,后面跟着一个可选的时区。(参见表 时间输入表 时区输入。)如果在time without time zone的输入中指定了时区,则会被忽略。您也可以指定日期,但它将被忽略,除非您使用涉及夏令时规则的时区名称,例如America/New_York。在这种情况下,指定日期是必需的,以确定是使用标准时间还是夏令时。适当的时区偏移量记录在time with time zone值中。

表 时间输入

示例描述
04:05:06.789ISO 8601
04:05:06ISO 8601
04:05ISO 8601
040506ISO 8601
04:05 AM04:05 相同;AM 不影响值
04:05 PM16:05 相同;输入小时必须小于等于 12
04:05:06.789-8ISO 8601,带有 UTC 偏移的时区
04:05:06-08:00ISO 8601,带有 UTC 偏移的时区
04:05-08:00ISO 8601,带有 UTC 偏移的时区
040506-08ISO 8601,带有 UTC 偏移的时区
040506+0730ISO 8601,带有小数小时的 UTC 偏移作为时区
040506+07:30:00指定到秒的 UTC 偏移(注: ISO 8601 标准本身不允许此格式)
04:05:06 PST由缩写指定的时区
2003-04-12 04:05:06 America/New_York由全名(IANA 时区名)指定的时区

表 时区输入

示例描述
PST缩写(太平洋标准时间)
America/New_York完整的时区名称(推荐,包含夏令时逻辑)
PST8PDTPOSIX 样式的时区规范
-8:00:00PST 的 UTC 偏移
-8:00PST 的 UTC 偏移(ISO 8601 扩展格式)
-800PST 的 UTC 偏移(ISO 8601 基本格式)
-8PST 的 UTC 偏移(ISO 8601 基本格式)
zuluUTC 的军事缩写
zzulu 的简写形式(同样符合 ISO 8601 标准)
5.5.1.3.时间戳

时间戳类型的有效输入由日期和时间的串联组成,后跟可选的时区,后跟可选的 ADBC。(或者,AD/BC可以出现在时区之前,但这不是首选的顺序。)因此:

1999-01-08 04:05:06

和:

1999-01-08 04:05:06 -8:00

和:

199901080405

是有效的值,遵循 ISO 8601 的规定。标准。此外,支持常见格式:

1999年1月8日04:05:06 PST

空字符也被支持。

SQL标准通过时间后的“+”或“-”符号和时区偏移来区分timestamp without time zonetimestamp with time zone字面值。因此,根据标准:

TIMESTAMP '2004-10-19 10:23:54'

是一个timestamp without time zone,而

TIMESTAMP '2004-10-19 10:23:54+02'

是一个timestamp with time zone

UXDB在确定其类型之前从不检查文字字符串的内容,因此将以上两者都视为timestamp without time zone。为确保文字字符串被视为timestamp with time zone,请给它正确的显式类型:

TIMESTAMP WITH TIME ZONE '2004-10-19 10:23:54+02'

在确定为timestamp without time zone的文字字符串中,UXDB将默默地忽略任何时区指示。也就是说,生成的值是从输入值中的日期/时间字段派生的,并且不会根据时区进行调整。

对于timestamp with time zone,内部存储的值始终为UTC(协调世界时,传统上称为格林威治标准时间,GMT)。具有显式时区的输入值将使用该时区的适当偏移量转换为UTC。如果输入字符串中未指定时区,则假定它在系统的TimeZone参数指示的时区中,并使用timezone区的偏移量将其转换为UTC。

输出timestamp with time zone值时,它始终从UTC转换为当前timezone区,并以该区域的本地时间显示。要查看另一个时区的时间,请更改timezone或使用AT TIME ZONE结构。

timestamp without time zonetimestamp with time zone之间的转换通常假定timestamp without time zone值应作为timezone本地时间。可以使用AT TIME ZONE指定不同的时区进行转换。

在uxdb数据库mysql运行模式下,timestamptimestamptz使用宽松语法,任何标点符号都可以用作日期部分或时间部分之间的分隔符,兼容mysql数据库datetime和timestamp。

5.5.1.4.特殊值

为方便起见,UXDB支持几个特殊的日期/时间输入值,如表 特殊日期/时间输入所示。值infinity-infinity在系统内部特别表示,并将不改变地显示;但其他值只是简写符号,在读取时将转换为普通的日期/时间值。(特别是,now和相关字符串在读取时将被转换为特定的时间值。)所有这些值在SQL命令中用作常量时都需要用单引号括起来。

表 特殊日期/时间输入列出了一些特殊的日期/时间输入字符串,它们可以用于日期/时间类型的常量。这些值也需要用单引号括起来。

表 特殊日期/时间输入

输入串合法类型描述
epochdate, timestamp1970-01-01 00:00:00+00(Unix 系统时间 0)
infinitydate, timestamp比任何其他时间戳都晚
-infinitydate, timestamp比任何其他时间戳都早
nowdate, time, timestamp当前事务的开始时间
todaydate, timestamp今日午夜 (00:00)
tomorrowdate, timestamp明日午夜 (00:00)
yesterdaydate, timestamp昨日午夜 (00:00)
allballstime00:00:00.00 UTC

此外,还可以使用以下与SQL兼容的函数来获取相应数据类型的当前时间值:CURRENT_DATECURRENT_TIMECURRENT_TIMESTAMPLOCALTIMELOCALTIMESTAMP。请注意,这些是SQL函数,不会被识别为数据输入字符串。

注意

需要注意的是,虽然在交互式SQL命令中使用now、today、tomorrow和yesterday等输入字符串是可以的,但是当命令被保存以便以后执行时(例如在准备好的语句、视图和函数定义中),它们可能会产生意想不到的行为。在这种情况下,最好使用SQL函数。例如,CURRENT_DATE+ 1比'tomorrow'::date更安全。

5.5.2.日期/时间输出

日期/时间类型的输出格式可以设置为ISO 8601、SQL(Ingres)、传统的UXDB(Unix date格式)或德国格式中的其中一种。默认格式是ISO格式。SQL标准要求使用ISO 8601格式。SQL输出格式的名称是一个历史遗留问题。历史偶然事件。表 日期/时间输出样式显示了每种输出样式的示例。根据给定的示例,datetime类型的输出通常仅为日期或时间部分。但是,UXDB样式以ISO格式输出仅日期值。

表 日期/时间输出样式

样式规范描述示例
ISOISO 8601,SQL 标准1997-12-17 07:37:16-08
SQL传统样式12/17/1997 07:37:16.00 PST
Uxdb原始样式(传统 Unix 风格)Wed Dec 17 07:37:16 1997 PST
German区域样式17.12.1997 07:37:16.00 PST

注意

ISO 8601规定使用大写字母T分隔日期和时间。UXDB接受该格式的输入,但在输出上使用空格而不是T,如上所示。这是为了可读性和与RFC 3339以及其他一些数据库系统的一致性。

在SQL和UXDB样式中,如果指定了DMY字段顺序,则日出现在月份之前,否则月份出现在日之前。(有关此设置如何影响输入值的解释,请参见日期/时间输入。)表 日期顺序约定显示了示例。

表 日期顺序约定

datestyle 设置输入顺序示例输出
SQL, DMYday/month/year17/12/1997 15:37:16.00 CET
SQL, MDYmonth/day/year12/17/1997 07:37:16.00 PST
Uxdb, DMYday/month/yearWed 17 Dec 07:37:16 1997 PST

在ISO样式中,时区始终显示为与UTC的有符号数值偏移量,正号用于格林威治以东的区域。偏移将显示如果时间是整数小时,则以hh(仅小时)表示,否则以hh:mm(小时和分钟)表示,如果时间是整数分钟,则以此方式表示,否则以hh:mm:ss(小时、分钟和秒)表示。(第三种情况在任何现代时区标准中都不可能出现,但在处理早于标准化时区采用的时间戳时可能会出现。)在其他日期样式中,如果当前区域中有常用的字母缩写,则将时区显示为字母缩写。否则,它将以ISO 8601基本格式的带符号数字偏移量(hhhhmm)表示。

用户可以使用SET datestyle命令、UXDB.conf配置文件中的DateStyle参数或服务器或客户端上的UXDATESTYLE环境变量选择日期/时间样式。

格式化函数to_char也可用作格式化日期/时间输出的更灵活的方法。

在uxdb数据库Oracle模式下,由于1582/10/5前儒略历计时,1582/10/5后格里高利历计时,当日期输入1582/10/5——1582/10/14时,均返回1582/10/15。

5.5.3.时区

时区和时区约定受政治决策的影响,而不仅仅是地球几何。世界各地的时区在20世纪逐渐标准化,但仍然容易受到任意更改的影响,特别是在夏令时规则方面。UXDB使用广泛使用的IANA(Olson)时区数据库来获取有关历史时区规则的信息。对于未来的时间,假设给定时区的最新已知规则将一直被遵守,直到遥远的未来。

UXDB努力与典型用法的SQL标准定义兼容。然而,SQL标准具有奇怪的日期和时间类型和功能的混合。两个明显的问题是:

  • 虽然date类型不能有关联的时区,但time类型可以。在现实世界中,时区没有太大意义,除非与日期以及时间相关联,因为偏移量可以随着夏令时边界的变化而在一年中变化。

  • 默认时区被指定为与UTC的恒定数字偏移量。因此,在跨越夏令时边界进行日期/时间算术时,无法适应夏令时。

为解决这些困难,我们建议在使用时区时使用同时包含日期和时间的日期/时间类型。我们不建议使用time with time zone类型(尽管它受到UXDB的支持,用于遗留应用程序和符合SQL标准)。UXDB假定任何仅包含日期或时间的类型都使用本地时区。

所有有时区意识的日期和时间在内部都以UTC存储。它们会转换为本地时间在被显示给客户端之前,会根据配置参数TimeZone指定的时区进行转换。

UXDB允许您以三种不同的形式指定时区:

  • 完整的时区名称,例如America/New_York。认可的时区名称列在ux_timezone_names视图中。UXDB使用广泛使用的IANA时区数据来实现,因此其他软件也会识别相同的时区名称。

  • 时区缩写,例如PST。这种规范仅定义与UTC的特定偏移量,与可以暗示一组夏令时转换规则的完整时区名称形成对比。认可的缩写列在ux_timezone_abbrevs视图中。您不能将配置参数TimeZonelog_timezone设置为时区缩写,但可以在日期/时间输入值和使用AT TIME ZONE运算符时使用缩写。

  • 除了时区名称和缩写之外,UXDB还接受POSIX样式的时区规范。通常情况下,这种选项不如使用命名时区方便,但如果没有合适的IANA时区条目可用,则可能需要使用它。

简而言之,这是缩写和完整名称之间的区别:缩写表示与UTC的特定偏移量,而许多完整名称则暗示本地夏令时规则,因此具有两个可能的UTC偏移量。例如,2014-06-04 12:00 America/New_York表示纽约当地时间中午,对于这个特定日期来说是东部夏令时(UTC-4)。因此,2014-06-04 12:00 EDT指定了相同的时间点。但是,2014-06-04 12:00 EST指定了中午的东部标准时间(UTC-5),无论当天是否名义上实行夏令时。

为了使事情更加复杂,一些司法管辖区在不同的时间使用相同的时区缩写来表示不同的UTC偏移量;例如,在莫斯科,MSK在某些年份表示UTC+3,在其他年份表示UTC+4。UXDB根据指定日期的含义(或最近的含义)解释这样的缩写;但是,与上面的EST示例一样,这不一定与当天的当地民用时间相同。

在所有情况下,时区名称和缩写都是不区分大小写的。

时区名称和缩写都没有硬编码到服务器中;它们是从安装目录下的.../share/timezone/.../share/timezonesets/配置文件中获取的。

配置参数TimeZone可以在文件UXDB.conf中设置,或者使用第20章 服务器配置中描述的任何其他标准方式进行设置。还有一些特殊的设置方式:

  • SQL命令SET TIME ZONE设置会话的时区。这是使用更符合SQL规范的语法的SET TIMEZONE TO的另一种拼写方式。

  • UXTZ环境变量由libuxsql客户端使用,在连接时向服务器发送SET TIME ZONE命令。

5.5.4.间隔输入

可以使用以下冗长的语法编写interval值:

[@] quantity unit [quantity unit...] [direction]

其中quantity是一个数字(可能带符号);unitmicrosecondmillisecondsecondminutehourdayweekmonthyeardecadecenturymillennium或这些单位的缩写或复数形式;direction可以是ago或空。at符号(@)是可选的噪声。不同单位的数量会隐式地添加,并进行适当的符号计算。ago会否定所有字段。如果将IntervalStyle设置为uxdb_verbose,则此语法也用于间隔输出。

可以指定天、小时、分钟和秒的数量,而无需明确的单位标记。例如,'1 12:59:10''1 day 12 hours 59 min 10 sec'相同。此外,可以使用破折号指定年和月的组合;例如,'200-10''200 years 10 months'相同。(这些较短的形式实际上是SQL标准允许的唯一形式,并且在将IntervalStyle设置为sql_standard时用于输出。)

间隔值也可以编写为ISO 8601时间间隔,使用标准的“带有设计ator的格式”或“替代格式”。设计器格式如下:

p quantity unit [ quantity unit ...] [ T [ quantity unit ...]]

该字符串必须以 P 开头,可以包含一个引入时间单位的 T。单位可以省略,并且可以以任何顺序指定,但是小于一天的单位必须出现在 T 之后。特别地,M 的含义取决于它是在 T 之前还是之后。

表 ISO 8601 间隔单位缩写

缩写含义
Y
M月(在日期部分中)
W
D
H小时
M分钟(在时间部分中)
S

在备选格式中:

p [ years-months-days ] [ T hours:minutes:seconds ]

该字符串必须以 P 开头,T 分隔间隔的日期和时间部分。值以类似于 ISO 8601 日期的数字形式给出。

在使用 fields 规范编写间隔常量或将字符串分配给使用 fields 规范定义的间隔列时,未标记数量的解释取决于 fields。例如,INTERVAL '1' YEAR 读作 1 年,而 INTERVAL '1' 表示 1 秒。此外,fields 规范允许的最低有效字段右侧的字段值将被静默丢弃。例如,编写 INTERVAL '1 day 2:03:04' HOUR TO MINUTE 将导致删除秒字段,但不删除天字段。

根据 SQL 标准,间隔值的所有字段必须具有相同的符号,因此前导负号适用于所有字段;例如,间隔文字 '-1 2:03:04' 中的负号适用于天数和小时/分钟/秒部分。UXDB 允许字段具有不同的符号,并且传统上将文本表示中的每个字段视为独立符号,因此在此示例中,小时/分钟/秒部分被视为正数。如果将 IntervalStyle 设置为 sql_standard,则认为前导符号适用于所有字段(但仅在没有其他符号出现时)。否则将使用传统的 UXDB 解释方式。为避免歧义,建议为每个字段附加一个明确的符号,如果任何字段为负数。

字段值可以有小数部分:例如,'1.5周' 或 '01:02:03.45'。然而,因为间隔内部仅存储三个整数单位(月、日、微秒),所以小数单位必须溢出到更小的单位。大于月份的单位的小数部分将四舍五入为整数个月,例如,'1.5年' 变成 '1年6个月'。周和天的小数部分被计算为整数天和微秒数,假设每月30天,每天24小时,例如,'1.75个月' 变成 1个月22天12:00:00。只有秒会在输出时显示为小数。

表 间隔输入示例显示了一些有效的 interval 输入示例。

表 间隔输入示例

例子描述
1-2SQL 标准格式:1 年 2 个月
3 4:05:06SQL 标准格式:3 日 4 小时 5 分钟 6 秒
1 year 2 months 3 days 4 hours 5 minutes 6 secondsUXDB 格式:1 年 2 个月 3 日 4 小时 5 分钟 6 秒钟
P1Y2M3DT4H5M6S“带标志符的” ISO 8601 格式:含义同上
P0001-02-03T04:05:06ISO 8601 的“替代格式”:含义同上

在内部,interval值以月、日和微秒存储。这是因为一个月的天数是不固定的,如果涉及到夏令时调整,一天可能有23或25个小时。月份和天数字段是整数,而微秒字段可以存储小数秒。因为间隔通常是从常量字符串或timestamp 减法创建的,所以这种存储方法在大多数情况下都很好用,但可能会导致意外的结果:

SELECT EXTRACT(hours from '80 minutes'::interval);
 date_part
-----------
         1

SELECT EXTRACT(days from '80 hours'::interval);
 date_part
-----------
         0

函数 justify_daysjustify_hours 可用于调整超出其正常范围的天数和小时数。

5.5.5.间隔输出

间隔类型的输出格式可以使用命令 SET intervalstyle设置为四种样式之一:sql_standarduxdbuxdb_verboseiso_8601。默认格式为 uxdb。[表 间隔输出样式示例](# intervalstyleoutputtable)显示了每种输出样式的示例。

sql_standard 样式生成的输出符合 SQL标准对间隔文字字符串的规范,如果间隔值符合标准的限制(仅年-月或仅日-时间,没有混合正负分量)。否则,输出看起来像标准的年-月文字字符串,后跟一个日-时间文字字符串,添加显式符号以消除混合符号间隔的歧义。

uxdb 样式的输出与 UXDB之前的版本的输出匹配,当DateStyle参数设置为 ISO 时。

uxdb_verbose 样式的输出与 UXDB之前的版本的输出匹配,当 DateStyle 参数设置为非ISO 输出时。

iso_8601 样式的输出与 ISO 8601 标准中描述的“带有指示符的格式”相匹配。

表 间隔输出样式示例

样式规范年-月间隔日-时间间隔混合间隔
sql_standard1-23 4:05:06-1-2 +3 -4:05:06
uxdb1 year 2 mons3 days 04:05:06-1 year -2 mons +3 days -04:05:06
uxdb_verbose@ 1 year 2 mons@ 3 days 4 hours 5 mins 6 secs@ 1 year 2 mons -3 days 4 hours 5 mins 6 secs ago
iso_8601P1Y2MP3DT4H5M6SP-1Y-2M3DT-4H-5M-6S

5.6.布尔类型

UXDB 提供了标准的 SQL 类型 boolean;参见表 布尔数据类型boolean 类型可以有几种状态: “true”、“false” 和第三种状态 “unknown”,它由 SQL 的 null 值表示。

表 布尔数据类型

名称存储大小描述
boolean1 字节真或假的状态

布尔常量可以用 SQL 查询中的 SQL 关键字 TRUEFALSENULL 表示。

类型为 boolean 的数据输入函数接受以下字符串表示形式作为 “true” 状态:

true
yes
on
1

以下是 “false” 状态的表示形式:

false
no
off
0

这些字符串的唯一前缀也被接受,例如 tn。前导或尾随空格被忽略,大小写不敏感。

类型为boolean的数据类型输出函数始终会发出tf,如示例 使用boolean类型所示。

示例 使用boolean类型

CREATE TABLE test1 (a boolean, b text);
INSERT INTO test1 VALUES (TRUE, 'sic est');
INSERT INTO test1 VALUES (FALSE, 'non est');
SELECT * FROM test1;
 a |    b
---+---------
 t | sic est
 f | non est

SELECT * FROM test1 WHERE a;
 a |    b
---+---------
 t | sic est

在SQL查询中,关键字TRUEFALSE是编写布尔常量的首选(符合SQL标准)方法。但是,您也可以使用字符串表示形式,方法是遵循词法结构中描述的通用字符串文字常量语法,例如'yes'::boolean

请注意,解析器自动理解TRUEFALSEboolean类型,但对于NULL来说,情况并非如此,因为它可以具有任何类型。因此,在某些情况下,您可能需要显式地将NULL转换为boolean,例如NULL::boolean。相反,在解析器可以推断出文字必须是boolean类型的上下文中,可以省略字符串文字布尔值的转换。

5.7.枚举类型

枚举(enum)类型是由静态、有序的值集合组成的数据类型。它们相当于许多编程语言中支持的enum类型。枚举类型的一个示例可能是一周中的天数,或者是一些数据的状态值集合。

5.7.1.声明枚举类型

可以使用CREATE TYPE命令创建枚举类型,例如:

CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');

创建后,枚举类型可以像任何其他类型一样在表和函数定义中使用:

CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
CREATE TABLE person (
    name text,
    current_mood mood
);
INSERT INTO person VALUES ('Moe', 'happy');
SELECT * FROM person WHERE current_mood = 'happy';
 name | current_mood
------+--------------
 Moe  | happy
(1 row)

5.7.2.排序

枚举类型中值的排序是创建类型时列出的值的顺序。所有标准比较运算符和相关的聚合函数都支持枚举类型。例如:

INSERT INTO person VALUES ('Larry', 'sad');
INSERT INTO person VALUES ('Curly', 'ok');
SELECT * FROM person WHERE current_mood > 'sad';
 name  | current_mood
-------+--------------
 Moe   | happy
 Curly | ok
(2 rows)

SELECT * FROM person WHERE current_mood > 'sad' ORDER BY current_mood;
 name  | current_mood
-------+--------------
 Curly | ok
 Moe   | happy
(2 rows)

SELECT name
FROM person
WHERE current_mood = (SELECT MIN(current_mood) FROM person);
 name
-------
 Larry
(1 row)

5.7.3.类型安全

每个枚举数据类型都是独立的,不能与其他枚举类型进行比较。请参见以下示例:

CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic');
CREATE TABLE holidays (
    num_weeks integer,
    happiness happiness
);
INSERT INTO holidays(num_weeks,happiness) VALUES (4, 'happy');
INSERT INTO holidays(num_weeks,happiness) VALUES (6, 'very happy');
INSERT INTO holidays(num_weeks,happiness) VALUES (8, 'ecstatic');
INSERT INTO holidays(num_weeks,happiness) VALUES (2, 'sad');
ERROR:  invalid input value for enum happiness: "sad"
SELECT person.name, holidays.num_weeks FROM person, holidays
  WHERE person.current_mood = holidays.happiness;
ERROR:  operator does not exist: mood = happiness

如果您确实需要执行此类操作,则可以编写自定义运算符或在查询中添加显式转换:

SELECT person.name, holidays.num_weeks FROM person, holidays
  WHERE person.current_mood::text = holidays.happiness::text;
 name | num_weeks
------+-----------
 Moe  |         4
(1 row)

5.7.4.实现细节

枚举标签区分大小写,因此'happy''HAPPY'不同。标签中的空格也很重要。

虽然枚举类型主要用于静态值集,但支持向现有枚举类型添加新值以及重命名值。无法从枚举类型中删除现有值,也无法更改这些值的排序顺序,除非删除并重新创建枚举类型。

枚举值在磁盘上占用四个字节。枚举值的文本标签的长度由编译到UXDB中的NAMEDATALEN设置限制;在标准构建中,这意味着最多为63个字节。

从内部枚举值到文本标签的翻译保存在系统目录ux_enum中。直接查询此目录可能很有用。

5.8.几何类型

几何数据类型表示二维空间对象。在UXDB中,表 几何类型列出了可用的几何类型。

表 几何类型

名称存储大小描述表示
point16 字节平面上的点(x,y)
line32 字节无限直线{A,B,C}
lseg32 字节有限线段((x1,y1),(x2,y2))
box32 字节矩形框((x1,y1),(x2,y2))
path16+16n 字节闭合路径(类似于多边形)((x1,y1),...)
path16+16n 字节开放路径[(x1,y1),...]
polygon40+16n 字节多边形(类似于闭合路径)((x1,y1),...)
circle24 字节<(x,y),r>(中心点和半径)

提供了丰富的函数和运算符来执行各种几何操作,如缩放、平移、旋转和确定交点。

5.8.1.点

点是几何类型的基本二维构建块。类型为point的值使用以下语法之一指定:

( x , y )
  x , y

其中xy是浮点数,分别是相应的坐标。

5.8.2.直线

线由线性方程Ax + By + C =0表示,其中AB不同时为零。类型为line的值以以下形式输入和输出:

{ A, B, C }

或者,可以使用以下任何形式进行输入:

[ ( x1 , y1 ) , ( x2 , y2 ) ]
( ( x1 , y1 ) , ( x2 , y2 ) )
  ( x1 , y1 ) , ( x2 , y2 )
    x1 , y1   ,   x2 , y2

其中(x1,y1)和(x2,y2)是线上的两个不同点。

5.8.3.线段

线段由成对的点表示,这些点是线段的端点。类型为lseg的值使用以下任何语法之一指定:

[ ( x1 , y1 ) , ( x2 , y2 ) ]
( ( x1 , y1 ) , ( x2 , y2 ) )
  ( x1 , y1 ) , ( x2 , y2 )
    x1 , y1   ,   x2 , y2

其中(x1y1)和(x2y2)是线段的端点。

线段使用第一种语法输出。

5.8.4.矩形框

矩形框由对角线的两个端点表示。类型为box的值可以使用以下任何语法指定:

( ( x1 , y1 ) , ( x2 , y2 ) )
  ( x1 , y1 ) , ( x2 , y2 )
    x1 , y1   ,   x2 , y2

其中(x1y1)和(x2y2)是矩形框的对角线的任意两个端点。

矩形框使用第二种语法输出。

可以提供任意两个对角线的端点作为输入,但是值将被重新排序以存储右上角和左下角,以此顺序。

5.8.5.路径

路径由连接点的列表表示。路径可以是开放的,其中列表中的第一个和最后一个点被认为是未连接的,或者是封闭的,其中第一个和最后一个点被认为是连接的。

类型为path的值可以使用以下任何语法指定:

[ ( x1 , y1 ) , ... , ( xn , yn ) ]
( ( x1 , y1 ) , ... , ( xn , yn ) )
  ( x1 , y1 ) , ... , ( xn , yn )
  ( x1 , y1   , ... ,   xn , yn )
    x1 , y1   , ... ,   xn , yn

其中点是组成路径的线段的端点。方括号([])表示开放路径,而括号(())表示封闭路径。当省略最外层括号时,如第三到第五种语法,将假定为封闭路径。路径将使用适当的第一或第二语法输出。

5.8.6.多边形

多边形由点列表(多边形的顶点)表示。多边形与封闭路径非常相似;本质区别在于多边形被认为包括其中的区域,而路径不包括其中的区域。

使用以下任何语法指定类型为polygon的值:

( ( x1 , y1 ) , ... , ( xn , yn ) )
  ( x1 , y1 ) , ... , ( xn , yn )
  ( x1 , y1   , ... ,   xn , yn )
    x1 , y1   , ... ,   xn , yn

其中点是组成多边形边界的线段的端点。

多边形将使用第一语法输出。

5.8.7.圆

圆由中心点和半径表示。使用以下任何语法指定类型为circle的值:

< ( x , y ) , r >
( ( x , y ) , r )
  ( x , y ) , r
    x , y   , r

其中 (x,y) 是圆的中心点,r是半径。

圆将使用第一语法输出。

5.9.网络地址类型

UXDB 提供了存储 IPv4、IPv6 和 MAC 地址的数据类型,如表 网络地址类型所示。使用这些类型存储网络地址比使用纯文本类型更好,因为这些类型提供了输入错误检查和专门的运算符和函数。

表 网络地址类型

名称存储大小描述
cidr7 或 19 字节IPv4 和 IPv6 网络(无类域间路由)
inet7 或 19 字节IPv4 和 IPv6 主机和网络
macaddr6 字节MAC 地址
macaddr88 字节MAC 地址(EUI-64 格式)

在对 inetcidr 数据类型进行排序时,IPv4 地址始终排在 IPv6 地址之前,包括封装或映射到 IPv6 地址的IPv4 地址,例如 ::10.2.3.4 或 ::ffff:10.4.3.2。

5.9.1.inet

inet 类型保存 IPv4 或 IPv6 主机地址,以及可选的子网,全部保存在一个字段中。子网由主机地址中存在的网络地址位数(即“掩码”)表示。如果掩码为32 且地址为 IPv4,则该值不表示子网,只表示单个主机。在 IPv6 中,地址长度为 128 位,因此 128位指定了唯一的主机地址。请注意,如果您只想接受网络,则应使用 cidr 类型而不是inet

此类型的输入格式为address/y,其中address 是 IPv4 或 IPv6 地址,y是掩码中的位数。如果省略了/y 部分,则对于 IPv4,掩码被视为 32,对于IPv6,掩码被视为 128,因此该值仅表示单个主机。在显示时,如果掩码指定单个主机,则会省略 /y 部分。

5.9.2.cidr

cidr 类型保存 IPv4 或 IPv6 网络规范。输入和输出格式遵循无类域间路由选择(CIDR)约定。指定网络的格式为address/y,其中 address 是表示网络最低地址的 IPv4 或 IPv6 地址,y是掩码中的位数。如果省略了y,则使用旧的类别网络编号系统的假设进行计算,但至少要足够大以包括输入中写入的所有八位组。指定右侧具有位设置的网络地址是错误的。

表 cidr 类型输入示例显示了一些示例。

表 cidr 类型输入示例

cidr 输入cidr 输出abbrev(cidr) (缩写函数结果)
192.168.100.128/25192.168.100.128/25192.168.100.128/25
192.168/24192.168.0.0/24192.168.0/24
192.168/25192.168.0.0/25192.168.0.0/25
192.168.1192.168.1.0/24192.168.1/24
192.168192.168.0.0/24192.168.0/24
128.1128.1.0.0/16128.1/16
128128.0.0.0/16128.0/16
128.1.2128.1.2.0/24128.1.2/24
10.1.210.1.2.0/2410.1.2/24
10.110.1.0.0/1610.1/16
1010.0.0.0/810/8
10.1.2.3/3210.1.2.3/3210.1.2.3/32
2001:4f8:3:ba::/642001:4f8:3:ba::/642001:4f8:3:ba/64
2001:4f8:3:ba:2e0:81ff:fe22:d1f1/1282001:4f8:3:ba:2e0:81ff:fe22:d1f1/1282001:4f8:3:ba:2e0:81ff:fe22:d1f1/128
::ffff:1.2.3.0/120::ffff:1.2.3.0/120::ffff:1.2.3/120
::ffff:1.2.3.0/128::ffff:1.2.3.0/128::ffff:1.2.3.0/128

5.9.3.inetcidr

inetcidr 数据类型之间的本质区别在于,inet 接受带有子网掩码右侧的非零位的值,而 cidr不接受。例如,192.168.0.1/24 对于 inet 是有效的,但对于 cidr 是无效的。

提示

如果您不喜欢 inetcidr 值的输出格式,请尝试使用 hosttextabbrev 函数。

5.9.4.macaddr

macaddr 类型存储 MAC 地址,例如以太网卡硬件地址(尽管 MAC 地址也用于其他目的)。输入可采用以下格式:

'08:00:2b:01:02:03'
'08-00-2b-01-02-03'
'08002b:010203'
'08002b-010203'
'0800.2b01.0203'
'0800-2b01-0203'
'08002b010203'

这些示例都指定了相同的地址。数字a到f的大小写均可接受。输出始终采用第一种形式。

IEEE 802-2001标准规定了第二种形式(带连字符)作为 MAC 地址的规范形式,并规定了第一种形式(带冒号)与位反转的 MSB 第一符号一起使用,因此08-00-2b-01-02-03 = 10:00: D4:80:40: C0。这个约定现在被广泛忽视,它只与过时的网络协议(如令牌环)有关。UXDB 没有提供位反转;所有可接受的格式都使用规范的 LSB 顺序。

其余五种输入格式不属于任何标准。

5.9.5.macaddr8

macaddr类型存储MAC地址,采用标准的EUI-48格式,例如以太网卡硬件地址(尽管MAC地址也用于其他目的)。此类型可以接受6字节和8字节长度的MAC地址,并以8字节长度格式存储它们。以6字节格式给出的MAC地址将以第4和第5个字节分别设置为FF和FE的8字节长度格式存储。请注意,IPv6使用修改后的EUI-64格式,在从EUI-48进行转换后,第7位应设置为1。提供了函数macaddr8_set7bit来进行此更改。一般来说,任何由一对十六进制数字(在字节边界上),可选地由':''-''.'之一一致分隔的输入都将被接受。十六进制数字的数量必须是16(8字节)或12(6字节)。前导和尾随空格将被忽略。以下是接受的输入格式示例:

'08:00:2b:01:02:03:04:05'
'08-00-2b-01-02-03-04-05'
'08002b:0102030405'
'08002b-0102030405'
'0800.2b01.0203.0405'
'0800-2b01-0203-0405'
'08002b01:02030405'
'08002b0102030405'

这些示例都指定相同的地址。十六进制数字af的大小写均可接受。输出始终以第一种形式显示。

上述最后六种输入格式均不属于任何标准。

要将以传统的EUI-48格式表示的48位MAC地址转换为修改后的EUI-64格式,以包含在IPv6地址的主机部分中,请使用macaddr8_set7bit,如下所示:

SELECT macaddr8_set7bit('08:00:2b:01:02:03');

macaddr8_set7bit
-------------------------
0a:00:2b:ff:fe:01:02:03
(1 row)

5.10.位串类型

位串是由1和0组成的字符串。它们可以用于存储或可视化位掩码。有两种SQL位类型:bit(n)bitbit varying(n),其中n是正整数。

bit类型的数据必须完全匹配长度n;尝试存储较短或较长的位串将导致错误。bit varying数据的长度可变,最大长度为n;较长的字符串将被拒绝。写bit而不带长度等效于bit(1),而不带长度规定的bit varying表示无限长度。

注意

如果将位串值显式转换为bit(n),它将被截断或在右侧填充零以确切地达到n位,而不会引发错误。同样,如果将位串值显式转换为bitbit varying(n),如果它超过n位,则会在右侧被截断。

有关位串常量语法的信息,请参见词法结构

示例 使用位串类型

CREATE TABLE test (a BIT(3), b BIT VARYING(5));
INSERT INTO test VALUES (B'101', B'00');
INSERT INTO test VALUES (B'10', B'101');

ERROR:  bit string length 2 does not match type bit(3)

INSERT INTO test VALUES (B'10'::bit(3), B'101');
SELECT * FROM test;

  a  |  b
-----+-----
 101 | 00
 100 | 101

一个位串值需要每8位组的1个字节,加上5或8个字节的开销,具体取决于字符串的长度(但长值可以压缩或移出行)。

5.11.文本搜索类型

UXDB 提供了两种数据类型,旨在支持全文搜索,即在自然语言文档集合中搜索最佳匹配查询的文档。tsvector类型表示以优化文本搜索的形式表示的文档;tsquery 类型类似地表示文本查询。

5.11.1.tsvector

tsvector 值是一个排序的不同词元列表,这些词元是已经规范化的单词,用于合并同一个单词的不同变体。输入时会自动进行排序和去重,如下面的示例所示:

SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector;
                      tsvector
----------------------------------------------------
 'a' 'and' 'ate' 'cat' 'fat' 'mat' 'on' 'rat' 'sat'

要表示包含空格或标点符号的词元,请用引号将它们括起来:

SELECT $$the lexeme '    ' contains spaces$$::tsvector;
                 tsvector
-------------------------------------------
 '    ' 'contains' 'lexeme' 'spaces' 'the'

(在此示例和下一个示例中,我们使用美元引用的字符串字面量,以避免在字面量内部需要双引号的混淆。)嵌入的引号和反斜杠必须加倍:

SELECT $$the lexeme 'Joe''s' contains a quote$$::tsvector;
                    tsvector
------------------------------------------------
 'Joe''s' 'a' 'contains' 'lexeme' 'quote' 'the'

可选地,整数位置可以附加到词元上:

SELECT 'a:1 fat:2 cat:3 sat:4 on:5 a:6 mat:7 and:8 ate:9 a:10 fat:11 rat:12'::tsvector;
                                  tsvector
-------------------------------------------------------------------​------------
 'a':1,6,10 'and':8 'ate':9 'cat':3 'fat':2,11 'mat':7 'on':5 'rat':12 'sat':4

位置通常表示源单词在文档中的位置。位置信息可用于接近排名。位置值可以从1到16383;较大的数字被默默地设置为16383。相同词元的重复位置将被丢弃。

具有位置的词元可以进一步用权重标记,可以是ABCDD是默认值,因此不会在输出中显示:

SELECT 'a:1A fat:2B,4C cat:5D'::tsvector;
          tsvector
----------------------------
 'a':1A 'cat':5 'fat':2B,4C

权重通常用于反映文档结构,例如通过将标题词与正文词标记不同。文本搜索排名函数可以为不同的权重标记分配不同的优先级。

重要的是要理解,tsvector类型本身不执行任何单词规范化;它假定它所给出的单词已经适当地规范化了应用程序。例如,

SELECT 'The Fat Rats'::tsvector;
      tsvector
--------------------
 'Fat' 'Rats' 'The'

对于大多数英文文本搜索应用程序,上述单词将被视为非规范化,但是tsvector不关心。原始文档文本通常应通过to_tsvector进行传递,以适当地规范化单词以进行搜索:

SELECT to_tsvector('english', 'The Fat Rats');
   to_tsvector
-----------------
 'fat':2 'rat':3

5.11.2.tsquery

tsquery值存储要搜索的词元,并可以使用布尔运算符&(AND)、|(OR)和!(NOT),以及短语搜索运算符<->(FOLLOWED BY)将它们组合起来。还有一个变体<N> FOLLOWED BY运算符,其中N是一个整数常量,指定要搜索的两个词元之间的距离。<->等同于<1>

可以使用括号来强制执行这些运算符的分组。在没有括号的情况下,!(NOT)最紧密地绑定,<->(FOLLOWED BY)次之,然后是&(AND),最后是|(OR)。

以下是一些示例:

SELECT 'fat & rat'::tsquery;
    tsquery
---------------
 'fat' & 'rat'

SELECT 'fat & (rat | cat)'::tsquery;
          tsquery
---------------------------
 'fat' & ( 'rat' | 'cat' )

SELECT 'fat & rat & ! cat'::tsquery;
        tsquery
------------------------
 'fat' & 'rat' & !'cat'

可选地,tsquery中的词元可以带有一个或多个权重字母,这将限制它们仅匹配具有其中一个权重的tsvector词元:

SELECT 'fat:ab & cat'::tsquery;
tsquery
------------------
'fat':AB & 'cat'

此外,tsquery中的词元可以带有*以指定前缀匹配:

SELECT 'super:*'::tsquery;
tsquery
-----------
'super':*

此查询将匹配以“super”开头的tsvector中的任何单词。

词元的引用规则与先前描述的tsvector中的词元相同;并且与tsvector一样,在转换为tsquery类型之前必须执行任何所需的单词规范化。to_tsquery函数方便执行此类规范化:

SELECT to_tsquery('Fat:ab & Cats');
to_tsquery
------------------
'fat':AB & 'cat'

请注意,to_tsquery将以与其他单词相同的方式处理前缀,这意味着此比较返回true:

SELECT to_tsvector( 'postgraduate' ) @@ to_tsquery( 'uxdb:*' );
?column?
----------
t

因为uxdb被词干提取为postgr:

SELECT to_tsvector( 'postgraduate' ), to_tsquery( 'uxdb:*' );
to_tsvector  | to_tsquery
---------------+------------
'postgradu':1 | 'postgr':*

这将匹配postgraduate的词干形式。

5.12.UUID类型

数据类型uuid将根据RFC 4122、ISO/IEC 9834-8:2005和相关标准定义的通用唯一标识符(UUID)存储。(有些系统将此数据类型称为全局唯一标识符或GUID。)此标识符是一个128位的数量,由选择的算法生成,使得使用相同算法的任何其他人在已知的宇宙中生成相同的标识符的可能性非常小。因此,对于分布式系统,这些标识符提供了比序列生成器更好的唯一性保证,后者仅在单个数据库内唯一。

UUID写为一系列小写十六进制数字,由连字符分隔的几个组组成,具体来说是一个由8个数字组成的组,后跟三个由4个数字组成的组,后跟一个由12个数字组成的组,总共32个数字表示128位。这种标准形式的UUID示例为:

a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11

UXDB还接受以下输入的替代形式: 使用大写数字,标准格式用大括号括起来,省略一些或所有连字符,在任何四个数字组后添加连字符。例如:

A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11
{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}
a0eebc999c0b4ef8bb6d6bb9bd380a11
a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11
{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}

使用uuid函数,可以生成一个uuid值,如下所示。

SELECT uuid();
                 uuid                 
--------------------------------------
 5d5794ea-a9c4-4b6d-9537-bc6d1c5e4a18

输出始终以标准形式呈现。

5.13.XML类型

xml数据类型可用于存储XML数据。与在text字段中存储XML数据相比,它的优点在于它检查输入值的格式是否正确,并且有支持函数对其执行类型安全操作。使用此数据类型需要安装时使用configure --with-libxml进行构建。

xml类型可以存储符合XML标准定义的“文档”,以及通过引用XQuery和XPath数据模型的更宽松的“文档节点”定义的“内容片段”。大致上,这意味着内容片段可以有多个顶级元素或字符节点。表达式xmlvalue IS DOCUMENT可用于评估特定的xml值是完整文档还是仅内容片段。

5.13.1.创建XML值

要从字符数据生成xml类型的值,请使用函数xmlparse:

XMLPARSE ( { DOCUMENT | CONTENT } value)

例如:

XMLPARSE(DOCUMENT '<?xml version="1.0"?><book><title>Manual</title><chapter>...</chapter></book>')
XMLPARSE(CONTENT 'abc<foo>bar</foo><bar>foo</bar>')

虽然这是根据SQL标准将字符字符串转换为XML值的唯一方法,但UXDB特定的语法:

xml '<foo>bar</foo>'
'<foo>bar</foo>' :: xml

也可以使用。

xml类型不会根据文档类型声明(DTD)验证输入值,即使输入值指定了DTD。目前也没有内置支持用于验证其他XML模式语言(如XML Schema)的功能。

反向操作,从xml生成字符字符串值,使用函数xmlserialize

XMLSERIALIZE({DOCUMENT | CONTENT}  value   AS  type  )

type可以是charactercharacter varyingtext(或其别名)。同样,根据SQL标准,这是在类型xml和字符类型之间转换的唯一方法,但UXDB还允许您简单地转换该值。

当将字符字符串值转换为xml类型或从xml类型转换为字符字符串值时,如果不经过XMLPARSEXMLSERIALIZE,则DOCUMENTCONTENT的选择由“XML选项”会话配置参数确定,可以使用标准命令设置该参数:

SET XML OPTION {DOCUMENT | CONTENT};

或更类似于UXDB的语法

SET xmloption TO {DOCUMENT | CONTENT};

默认值为CONTENT,因此允许使用所有形式的XML数据。

5.13.2.编码处理

在客户端、服务器和通过它们传递的XML数据上处理多个字符编码时必须小心。当使用文本模式将查询传递到服务器并将查询结果传递到客户端(这是正常模式)时,UXDB将在客户端和服务器之间传递的所有字符数据以及反之互相转换为各自端的字符编码。这包括XML值的字符串表示,例如上面的示例。这通常意味着在XML数据中包含的编码声明可能会变得无效,因为在客户端和服务器之间传输字符数据时,嵌入的编码声明不会更改。为了应对这种行为,输入到xml类型的字符字符串中包含的编码声明被忽略,并且假定内容在当前服务器编码中。因此,为了正确处理,XML数据的字符字符串必须从客户端以当前客户端编码发送。客户端有责任在将文档发送到服务器之前将文档转换为当前客户端编码,或者适当调整客户端编码。在输出时,xml类型的值将不具有编码声明,客户端应假定所有数据都在当前客户端编码中。

当使用二进制模式将查询参数传递到服务器并将查询结果传递回客户端时,不执行编码转换,因此情况不同。在这种情况下,将观察XML数据中的编码声明,如果不存在,则假定数据为UTF-8(根据XML标准所需;请注意,UXDB不支持UTF-16)。在输出时,数据将具有指定客户端编码的编码声明,除非客户端编码为UTF-8,在这种情况下,它将被省略。

不用说,如果XML数据编码、客户端编码和服务器编码相同,使用UXDB处理XML数据将更少出错且更有效率。由于XML数据在内部以UTF-8处理,因此如果服务器编码也是UTF-8,则计算效率最高。

注意

当服务器编码不是UTF-8时,某些与XML相关的函数可能根本无法处理非ASCII数据。这已知是xmltable()xpath()的问题。

5.13.3.访问XML值

xml数据类型不同寻常,因为它不提供任何比较运算符。这是因为XML数据没有明确定义且普遍有用的比较算法。这样做的一个后果是,您不能通过将xml列与搜索值进行比较来检索行。因此,XML值通常应该附带一个单独的键字段,例如ID。将XML值转换为字符字符串进行比较的另一种解决方案是,但请注意,字符字符串比较与有用的XML比较方法几乎没有关系。

由于xml数据类型没有比较运算符,因此不可能直接在此类型的列上创建索引。如果需要快速搜索XML数据,则可能的解决方法包括将表达式转换为字符字符串类型并对其进行索引,或对XPath表达式进行索引。当然,实际查询必须通过索引表达式进行搜索。

UXDB中的文本搜索功能也可用于加速XML数据的全文档搜索。然而,必要的预处理支持尚未在UXDB发行版中提供。

5.14.JSON 类型

JSON 数据类型用于存储 JSON(JavaScript 对象表示法)数据,如RFC 7159 中所述。这些数据也可以存储为 text,但JSON 数据类型的优点在于强制每个存储的值都符合 JSON 规则。此外,还有一些针对这些数据类型存储的 JSON 特定函数和运算符。

UXDB 提供了两种类型来存储 JSON 数据:jsonjsonb。为了实现这些数据类型的高效查询机制,UXDB还提供了jsonpath` 数据类型。

jsonjsonb 数据类型接受几乎相同的输入值集。主要的实际区别在于效率。json数据类型存储输入文本的精确副本,处理函数必须在每次执行时重新解析;而jsonb 数据以分解的二进制格式存储,这使得处理函数可以更快地访问数据。

由于增加了转换开销,使输入略微变慢,但由于不需要重新解析,因此处理速度显着提高。jsonb 还支持索引,这可能是一个重要的优势。

因为 json 类型存储输入文本的精确副本,所以它将保留标记之间的语义上不重要的空格,以及 JSON 对象内键的顺序。此外,如果值中的JSON对象包含多个相同的键,则会保留所有键/值对。(处理函数将最后一个值视为操作值。)相比之下,jsonb不保留空格,不保留对象键的顺序,也不保留重复的对象键。如果输入中指定了重复的键,则只保留最后一个值。

一般来说,大多数应用程序应该优先将 JSON 数据存储为jsonb,除非存在非常专业化的需求,例如关于对象键排序的传统假设。

RFC 7159 指定 JSON 字符串应使用 UTF8 编码。因此,除非数据库编码为 UTF8,否则 JSON 类型无法严格符合 JSON规范。直接包含无法在数据库编码中表示的字符的尝试将失败;反之,将允许在数据库编码中可以表示但不能在 UTF8 中表示的字符。

RFC 7159 允许 JSON 字符串包含由 \uXXXX 表示的 Unicode 转义序列。在 json类型的输入函数中,无论数据库编码如何,都允许使用 Unicode转义序列,并且仅检查语法正确性(即四个十六进制数字跟随 \u)。然而,jsonb的输入函数更严格:它不允许使用不能在数据库编码中表示的字符的 Unicode 转义。jsonb 类型还拒绝\u0000(因为它无法在 UXDB 的 text 类型中表示),并坚持任何用于指定 Unicode基本多语言平面之外的字符的 Unicode 代理对必须正确。有效的 Unicode转义将转换为等效的单个字符进行存储;这包括将代理对折叠成单个字符。

注意

许多 JSON 处理函数将 Unicode 转义转换为常规字符,因此,即使它们的输入类型为 json 而不是jsonb,它们也会抛出相同类型的错误。json输入函数不进行这些检查的事实可能被认为是历史遗留问题,尽管它允许在不支持所表示字符的数据库编码中简单存储(而不进行处理)JSON Unicode 转义。

将文本 JSON 输入转换为 jsonb 时,RFC 7159 描述的原始类型被有效地映射到本机 UXDB 类型,如表 JSON 原始类型和相应的 UXDB 类型所示。因此,对于构成有效jsonb 数据的一些次要附加约束,这些约束不适用于 json 类型,也不适用于抽象的JSON,对应于可以由基础数据类型表示的内容的限制。值得注意的是,jsonb将拒绝超出 UXDB numeric 数据类型范围的数字,而 json 则不会。这种实现定义的限制是由 RFC 7159允许的。然而,在实践中,这种问题更有可能在其他实现中出现,因为通常将 JSON 的 number 原始类型表示为 IEEE 754双精度浮点数(RFC 7159 明确预见并允许这种情况)。在与这些系统交换格式时,应考虑与由 UXDB最初存储的数据相比丢失数字精度的风险。

相反,如表中所示,对于 JSON 原始类型的输入格式,存在一些不适用于相应的 UXDB 类型的小限制。

表 JSON 原始类型和相应的 UXDB 类型

JSON 原始类型UXDB 类型注释
stringtext禁止使用 \u0000 以及表示数据库编码中不可用字符的 Unicode 转义
numbernumeric禁止使用 NaNinfinity
booleanboolean只接受小写的 truefalse 拼写
null(无)SQL NULL 是一个完全不同的概念

5.14.1.JSON 输入和输出语法

JSON 数据类型的输入/输出语法如 RFC 7159 中所指定。

以下是所有有效的 json(或 jsonb)表达式:

-- 简单的标量/原始值
-- 原始值可以是数字、带引号的字符串、true、false 或 null
SELECT '5'::json;

-- 零个或多个元素的数组(元素不必是相同的类型)
SELECT '[1, 2, "foo", null]'::json;

-- 包含键值对的对象
-- 请注意,对象键必须始终是带引号的字符串
SELECT '{"bar": "baz", "balance": 7.77, "active": false}'::json;

-- 数组和对象可以任意嵌套
SELECT '{"foo": [true, "bar"], "tags": {"a": 1, "b": null}}'::json;

如前所述,当输入 JSON 值并且没有进行任何其他处理时,json 输出与输入的文本相同,而 jsonb不保留语义上不重要的细节,例如空格。例如,请注意以下差异:

SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::json;
                      json
-------------------------------------------------
 {"bar": "baz", "balance": 7.77, "active":false}
(1 row)

SELECT '{"bar": "baz", "balance": 7.77, "active":false}'::jsonb;
                      jsonb
--------------------------------------------------
 {"bar": "baz", "active": false, "balance": 7.77}
(1 row)

值得注意的一个语义上不重要的细节是,在jsonb中,数字将根据底层numeric类型的行为进行打印。实际上,这意味着使用E表示法输入的数字将被打印为没有它的数字,例如:

SELECT '{"reading": 1.230e-5}'::json, '{"reading": 1.230e-5}'::jsonb;
         json          |          jsonb
-----------------------+-------------------------
 {"reading": 1.230e-5} | {"reading": 0.00001230}
(1 row)

但是,jsonb将保留尾数的小数零,如此例所示,即使对于等式检查等目的来说,这些零是语义上不重要的。

5.14.2.设计 JSON 文档

将数据表示为JSON可以比传统的关系数据模型更加灵活,这在需求不断变化的环境中非常有吸引力。两种方法很可能在同一个应用程序中共存并相互补充。然而,即使对于需要最大灵活性的应用程序,仍建议JSON文档具有某种固定的结构。该结构通常是不强制执行的(虽然可以通过声明性地强制执行一些业务规则),但是具有可预测的结构使得更容易编写查询,这些查询有用地总结了表中一组“文档”(数据)。

当存储在表中时,JSON数据受到与任何其他数据类型相同的并发控制考虑。虽然存储大型文档是可行的,但请记住,任何更新都会在整个行上获取行级锁定。考虑将JSON文档限制为可管理的大小,以减少更新事务之间的锁争用。理想情况下,JSON文档应该每个表示一个原子数据,业务规则规定不能合理地将其进一步细分为可以独立修改的较小数据。

5.14.3.jsonb 包含和存在

测试包含是jsonb的一个重要功能。json类型没有类似的设施。包含测试是否一个jsonb文档中包含另一个文档。这些示例返回true,除非另有说明:

-- Simple scalar/primitive values contain only the identical value:
SELECT '"foo"'::jsonb @> '"foo"'::jsonb;

-- The array on the right side is contained within the one on the left:
SELECT '[1, 2, 3]'::jsonb @> '[1, 3]'::jsonb;

-- Order of array elements is not significant, so this is also true:
SELECT '[1, 2, 3]'::jsonb @> '[3, 1]'::jsonb;

-- Duplicate array elements don't matter either:
SELECT '[1, 2, 3]'::jsonb @> '[1, 2, 2]'::jsonb;

-- The object with a single pair on the right side is contained
-- within the object on the left side:
SELECT '{"product": "UXDB", "version": 9.4, "jsonb": true}'::jsonb @> '{"version": 9.4}'::jsonb;

-- The array on the right side is not considered contained within the
-- array on the left, even though a similar array is nested within it:
SELECT '[1, 2, [1, 3]]'::jsonb @> '[1, 3]'::jsonb;  -- yields false

-- But with a layer of nesting, it is contained:
SELECT '[1, 2, [1, 3]]'::jsonb @> '[[1, 3]]'::jsonb;

-- Similarly, containment is not reported here:
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"bar": "baz"}'::jsonb;  -- yields false

-- A top-level key and an empty object is contained:
SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"foo": {}}'::jsonb;

一般原则是,包含的对象必须与包含对象在结构和数据内容上匹配,可能在从包含对象中丢弃一些不匹配的数组元素或对象键/值对后匹配。但请记住,在进行包含匹配时,数组元素的顺序不重要,并且重复的数组元素实际上只被视为一次。

作为一种特殊情况,数组可以包含原始值:

-- String exists as array element:
SELECT '["foo", "bar", "baz"]'::jsonb ? 'bar';

-- String exists as object key:
SELECT '{"foo": "bar"}'::jsonb ? 'foo';

-- Object values are not considered:
SELECT '{"foo": "bar"}'::jsonb ? 'bar';  -- yields false

-- As with containment, existence must match at the top level:
SELECT '{"foo": {"bar": "baz"}}'::jsonb ? 'bar'; -- yields false

-- A string is considered to exist if it matches a primitive JSON string:
SELECT '"foo"'::jsonb ? 'foo';

当涉及许多键或元素时,JSON对象比数组更适合测试包含或存在,因为与数组不同,它们在内部优化了搜索,并且不需要线性搜索。

提示

由于JSON包含是嵌套的,因此适当的查询可以跳过子对象的显式选择。例如,假设我们有一个包含顶级对象的 doc列,大多数对象都包含包含子对象数组的标签字段。此查询查找包含同时包含"term":"paris" 和 "term":"food" 的子对象的条目,同时忽略标签数组之外的任何这样的键:

SELECT doc->'site_name' FROM websites
  WHERE doc @> '{"tags":[{"term":"paris"}, {"term":"food"}]}';

也可以使用以下方法完成相同的操作:

SELECT doc->'site_name' FROM websites
  WHERE doc->'tags' @> '[{"term":"paris"}, {"term":"food"}]';

但是这种方法缺乏灵活性,而且通常效率也不高。

另一方面,JSON 存在运算符不是嵌套的:它只会在 JSON 值的顶层查找指定的键或数组元素。

5.14.4.jsonb 索引

GIN 索引可用于有效地搜索大量 jsonb 文档(数据)中出现的键或键/值对。提供了两个 GIN “操作符类”,提供不同的性能和灵活性权衡。

jsonb 的默认 GIN 操作符类支持具有键存在操作符 ??|?&,包含操作符 @>,以及 jsonpath匹配操作符 @?@@。创建具有此操作符类的索引的示例是:

CREATE INDEX idxgin ON api USING GIN (jdoc);

非默认的 GIN 操作符类 jsonb_path_ops 不支持键存在操作符,但支持 @>@?@@。创建具有此操作符类的索引的示例是:

CREATE INDEX idxginp ON api USING GIN (jdoc jsonb_path_ops);

考虑一个存储从第三方 Web 服务检索的具有文档模式定义的 JSON 文档的表的示例。一个典型的文档是:

{
    "guid": "9c36adc1-7fb5-4d5b-83b4-90356a46061a",
    "name": "Angela Barton",
    "is_active": true,
    "company": "Magnafone",
    "address": "178 Howard Place, Gulf, Washington, 702",
    "registered": "2009-11-07T08:53:22 +08:00",
    "latitude": 19.793713,
    "longitude": 86.513373,
    "tags": [
        "enim",
        "aliquip",
        "qui"
    ]
}

我们将这些文档存储在名为 api 的表中,其中有一个 jsonb 列名为 jdoc。如果在此列上创建了 GINSS索引,则可以使用以下查询利用该索引:

-- 查找键 "company" 的值为 "Magnafone" 的文档
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"company": "Magnafone"}';

然而,索引不能用于以下查询,因为虽然操作符 ? 是可索引的,但它不直接应用于索引列 jdoc

-- 查找键 "tags" 包含键或数组元素 "qui" 的文档
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc -> 'tags' ? 'qui';

但是,通过适当使用表达式索引,上述查询可以使用索引。如果经常查询 "tags" 键中的特定项目,则定义如下的索引可能是值得的:

CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags'));

现在,WHERE 子句 jdoc -> 'tags' ? 'qui' 将被识别为对索引化操作符 ? 应用于索引表达式 jdoc-> 'tags'。查询的另一种方法是利用包含关系,例如:

-- 查找键 "tags" 包含数组元素 "qui" 的文档
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qui"]}';

jdoc 列上的简单 GIN 索引可以支持此查询。但请注意,这样的索引将存储 jdoc列中每个键和值的副本,而前面示例中的表达式索引仅存储在"tags"键下找到的数据。tags键。虽然简单索引方法更加灵活(因为它支持任何键的查询),但是有针对性的表达式索引可能比简单索引更小且搜索速度更快。

GIN索引还支持@?@@运算符,用于执行jsonpath匹配。例如:

SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @? '$.tags[*] ? (@ == "qui")';
SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"';

对于这些运算符,GIN索引从jsonpath模式中提取形如accessors_chain= constant 的子句,并基于这些子句中提到的键和值进行索引搜索。访问器链可以包括.key[*][index]访问器。jsonb_ops操作符类还支持.*.**访问器,但jsonb_path_ops操作符类不支持。

尽管jsonb_path_ops操作符类仅支持使用@>@?@@运算符的查询,但它具有明显的性能优势,特别是与默认操作符类jsonb_ops相比。在相同的数据上,jsonb_path_ops索引通常比jsonb_ops索引小得多,并且搜索的特定性更好,特别是当查询包含在数据中频繁出现的键时。因此,搜索操作通常比默认操作符类更快。

jsonb_opsjsonb_path_opsGIN索引之间的技术差异在于前者为数据中的每个键和值创建独立的索引项,而后者仅为数据中的每个值创建索引项。基本上,每个jsonb_path_ops索引项是值和导致它的键的哈希值;例如,为了索引{"foo":{"bar":"baz"}},将创建一个单独的索引项,将foobarbaz三者合并到哈希值中。因此,寻找这种结构的包含查询将导致非常具体的索引搜索;但是,根本没有办法找出foo是否出现为键。另一方面,jsonb_ops索引将分别创建代表foobarbaz的三个索引项;然后,为了进行包含查询,它将查找包含这三个项的行。虽然GIN索引可以相当有效地执行这样的AND搜索,但它仍然比等效的jsonb_path_ops搜索不够具体和慢,特别是如果有大量行包含其中任何一个三个索引项。

jsonb_path_ops方法的一个缺点是,它不为不包含任何值的JSON结构(例如{"a":{}})生成索引条目。如果搜索这样的结构,将返回空结果。因此,如果需要搜索这样的结构,则必须使用jsonb_ops索引。

如果请求包含这样一种结构的文档,则需要进行全索引扫描,这相当慢。因此,jsonb_path_ops 不适用于经常执行此类搜索的应用程序。

jsonb 还支持 btreehash 索引。这些通常仅在检查完整 JSON 文档的相等性很重要时才有用。对于 jsonb数据,btree 排序很少具有很大的兴趣,但为了完整起见,它是:

Object > Array > Boolean > Number > String > Null

Object with n pairs > object with n - 1 pairs

Array with n elements > array with n - 1 elements

具有相等对数的对象按以下顺序进行比较:

key-1, value-1, key-2 ...

请注意,对象键按其存储顺序进行比较;特别是,由于较短的键在较长的键之前存储,这可能会导致结果可能是不直观的,例如:

{ "aa": 1, "c": 1} > {"b": 1, "d": 1}

类似地,具有相等元素数量的数组按以下顺序进行比较:

element-1, element-2 ...

原始的 JSON 值使用与底层 UXDB 数据类型相同的比较规则进行比较。字符串使用默认数据库排序进行比较。

5.14.5.jsonb 下标

jsonb 数据类型支持数组样式的下标表达式来提取和修改元素。可以通过链接下标表达式来指示嵌套值,遵循与 jsonb_set 函数中的path 参数相同的规则。如果 jsonb值是数组,则数字下标从零开始,负整数从数组的最后一个元素向后计数。不支持切片表达式。下标表达式的结果始终是jsonb数据类型。

UPDATE 语句可以在 SET 子句中使用下标来修改 jsonb 值。就所涉及的所有值而言,下标路径必须是可遍历的。例如,如果每个valval['a']val['a']['b'] 都是对象,则路径 val['a']['b']['c'] 可以遍历到c。如果未定义任何 val['a']val['a']['b'],则它将被创建为空对象,并根据需要填充。但是,如果任何val 本身或其中间值之一被定义为非对象,例如字符串、数字或 jsonb null,则遍历无法继续,因此会引发错误并中止事务。

下标语法示例:

-- 按键提取对象值选择以下文本:

SELECT ('{"a": 1}'::jsonb)['a'];

-- 通过键路径提取嵌套对象值
SELECT ('{"a": {"b": {"c": 1}}}'::jsonb)['a']['b']['c'];

-- 通过索引提取数组元素
SELECT ('[1, "2", null]'::jsonb)[1];

-- 通过键更新对象值。请注意,'1'周围的引号:分配的值也必须是jsonb类型
UPDATE table_name SET jsonb_field['key'] = '1';

-- 如果任何记录的jsonb_field ['a'] ['b']不是对象,则会引发错误。例如,键'a'的值为{"a": 1}。
UPDATE table_name SET jsonb_field['a']['b']['c'] = '1';

-- 使用下标运算符的WHERE子句过滤记录。由于下标运算的结果是jsonb,因此我们将其与jsonb类型的值进行比较。
-- 双引号使"value"也成为有效的jsonb字符串。
SELECT * FROM table_name WHERE jsonb_field['key'] = '"value"';

通过下标运算符进行jsonb分配处理一些边缘情况 与jsonb_set不同。当源jsonb值为NULL时,通过下标分配将继续好像它是由子脚本键隐含的类型(对象或数组)的空JSON值:

-- 当jsonb_field为NULL时,它现在为{"a": 1}
UPDATE table_name SET jsonb_field['a'] = '1';

-- 当jsonb_field为NULL时,它现在为[1]
UPDATE table_name SET jsonb_field[0] = '1';

如果为包含太少元素的数组指定了索引,将附加NULL元素,直到可以到达索引并且可以设置值。

-- 当jsonb_field为[]时,它现在为[null,null,2];
-- 当jsonb_field为[0]时,它现在为[0,null,2]
UPDATE table_name SET jsonb_field[2] = '2';

jsonb值将接受对不存在的下标路径的分配,只要要遍历的最后一个现有元素是对象或数组,就像子脚本键隐含的那样(由路径中的最后一个下标指示的元素不会被遍历,可以是任何内容)。嵌套数组和对象结构将被创建,在前者中 null填充,由子脚本路径指定,直到可以放置分配的值。

-- 当jsonb_field为{}时,它现在为{"a": [{"b": 1}]}
UPDATE table_name SET jsonb_field['a'][0]['b'] = '1';

-- 当jsonb_field为[]时,它现在为[null,{"a": 1}]
UPDATE table_name SET jsonb_field[1]['a'] = '1';

5.14.6.转换

可用的附加扩展实现了不同过程语言的jsonb类型的转换。

PL/Perl的扩展名为jsonb_plperljsonb_plperlu。如果您使用它们,jsonb值将映射到适当的Perl数组,哈希和标量。

PL/Python的扩展名为jsonb_plpython3u。如果您使用它,jsonb值将映射到Python字典,列表和标量,视情况而定。

这些扩展中,jsonb_plperl被认为是“可信”的,也就是说,它可以被在当前数据库上具有CREATE特权的非超级用户安装。其余的需要超级用户特权才能安装。

5.14.7.jsonpath类型

jsonpath类型实现了对SQL/JSON路径语言的支持在UXDB中高效查询JSON数据。它提供了解析的SQL/JSON路径表达式的二进制表示,该表达式指定了从JSON数据中由路径引擎检索的项,以便使用SQL/JSON查询函数进行进一步处理。

SQL/JSON路径谓词和运算符的语义通常遵循SQL。同时,为了提供一种自然的处理JSON数据的方式,SQL/JSON路径语法使用了一些JavaScript约定:

  • 点(.)用于成员访问。

  • 方括号([])用于数组访问。

  • SQL/JSON数组是从0开始的,不像常规的SQL数组从1开始。

SQL/JSON路径表达式通常在SQL查询中作为SQL字符字符串字面量编写,因此必须用单引号括起来,并且任何所需的单引号必须加倍(参见词法结构 )。某些形式的路径表达式需要其中的字符串字面量。这些嵌入式字符串字面量遵循JavaScript/ECMAScript约定:它们必须用双引号括起来,并且可以在其中使用反斜杠转义表示其他难以输入的字符。特别是,在嵌入式字符串字面量中写入双引号的方法是\",而要写入反斜杠本身,则必须写\\。其他特殊的反斜杠序列包括JSON字符串中识别的那些:\b\f\n\r\t\v,用于各种ASCII控制字符,以及\uNNNN,用于由其4位十六进制代码点标识的Unicode字符。反斜杠语法还包括JSON不允许的两种情况:\xNN,用于仅用两个十六进制数字编写的字符代码,以及\u{N...},用于用1到6个十六进制数字编写的字符代码。

路径表达式由一系列路径元素组成,可以是以下任何一种:

  • JSON原始类型的路径字面量:Unicode文本、数字、true、false或null。

  • 表 jsonpath 变量中列出的路径变量。

  • jsonpath 访问器中列出的访问器运算符。

  • 列出的jsonpath运算符和方法。

  • 括号,可用于提供过滤器表达式或定义路径评估的顺序。

表 jsonpath 变量

变量描述
$表示正在查询的 JSON 原始值 的变量(即“上下文项”,通常指整个 JSON 对象)。
$varname命名变量。它的值可以通过多个 JSON 处理函数的 vars 参数进行外部设置(类似于占位符)。
@表示在过滤表达式中,当前路径评估结果的变量(通常用于 ?() 过滤条件内)。

jsonpath 访问器

访问器运算符描述
.key ."$varname"成员访问器:返回指定键的对象成员。若键名以 $ 开头或不符合标识符规则,必须用双引号括起来。
.*通配符成员访问器:返回当前对象顶层(第一层)的所有成员值。
.**递归通配符访问器(UXDB 扩展):递归处理所有嵌套级别,返回所有成员值。
.**{level} .**{start to end}带层级的递归访问器(UXDB 扩展):仅选择指定的嵌套级别。0 代表当前层,可以使用 last 关键字指向最底层。
[subscript, ...]数组访问器:可以使用单个索引(如 [0])或范围(如 [start to end])获取切片。支持 last 关键字和数字表达式。
[*]通配符数组访问器:返回数组中的所有元素。

术语“值”包括数组元素,尽管JSON术语有时将数组元素视为与对象内的值不同的元素。

5.15.数组

UXDB 允许将表的列定义为可变长度的多维数组。可以创建任何内置或用户定义的基本类型、枚举类型、复合类型、范围类型或域的数组。

5.15.1.声明数组类型

为了说明数组类型的使用,我们创建这个表:

CREATE TABLE sal_emp (
    name            text,
    pay_by_quarter  integer[],
    schedule        text[][]
);

如上所示,数组数据类型是通过在数组元素的数据类型名称后附加方括号 ([]) 来命名的。上述命令将创建一个名为 sal_emp的表,其中包含一个类型为 text 的列 (name),一个类型为 integer 的一维数组(pay_by_quarter),它表示员工按季度的工资,以及一个类型为 text 的二维数组(schedule),它表示员工的每周工作计划。

CREATE TABLE 的语法允许指定数组的确切大小,例如:

CREATE TABLE tictactoe (
    squares   integer[3][3]
);

然而,当前的实现忽略了任何提供的数组大小限制,即行为与未指定长度的数组相同。

当前的实现也不强制执行声明的维数。特定元素类型的数组可以具有任意数量的维数,但是在任何给定时刻,所有数组元素的维数必须相同。

所有的数组都被认为是相同类型的,无论大小或维数。因此,在CREATE TABLE中声明数组大小或维数只是文档说明;它不会影响运行时行为。

一种符合SQL标准的替代语法可以用于一维数组,使用关键字ARRAYpay_by_quarter可以被定义为:

pay_by_quarter  integer ARRAY[4],

或者,如果不指定数组大小:

pay_by_quarter  integer ARRAY,

然而,与以前一样,UXDB不会在任何情况下强制执行大小限制。

5.15.2.数组值输入

要将数组值写为文字常量,请在花括号内将元素值括起来,并用逗号分隔它们。(如果您了解C语言,这与初始化结构的C语法类似。)您可以在任何元素值周围放置双引号,如果它包含逗号或花括号,则必须这样做。(下面会有更多详细信息。)因此,数组常量的一般格式如下:

'{ val1 delim val2 delim ... }'

其中delim是类型的分隔符字符,记录在其ux_type条目中。在UXDB分发的所有标准数据类型中,除了类型box使用分号(;)外,所有类型都使用逗号()。每个val都是数组元素类型的常量或子数组。数组常量的示例是:

'{{1,2,3},{4,5,6},{7,8,9}}'

此常量是一个二维的3x3数组,由三个整数子数组组成。

要将数组常量的元素设置为NULL,请为元素值写入NULL。(任何大小写变体的NULL都可以。)如果要实际的字符串值“NULL”,则必须在其周围放置双引号。

(这些类型的数组常量实际上只是讨论其他类型的常量中的通用类型常量的特例。常量最初被视为字符串,并传递给数组输入转换例程。可能需要显式类型说明。)

现在我们可以展示一些INSERT语句:

INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {"training", "presentation"}}');

INSERT INTO sal_emp
    VALUES ('Carol',
    '{20000, 25000, 25000, 25000}',
    '{{"breakfast", "consulting"}, {"meeting", "lunch"}}');

前两个插入的结果如下所示:

SELECT * FROM sal_emp;
 name  |      pay_by_quarter       |                 schedule
-------+---------------------------+-------------------------------------------
 Bill  | {10000,10000,10000,10000} | {{meeting,lunch},{training,presentation}}
 Carol | {20000,25000,25000,25000} | {{breakfast,consulting},{meeting,lunch}}
(2 rows)

多维数组必须具有每个维度的匹配范围。不匹配会导致错误,例如:

INSERT INTO sal_emp
    VALUES ('Bill',
    '{10000, 10000, 10000, 10000}',
    '{{"meeting", "lunch"}, {"meeting"}}');
ERROR:  multidimensional arrays must have array expressions with matching dimensions

也可以使用ARRAY构造函数语法:

INSERT INTO sal_emp
    VALUES ('Bill',
    ARRAY[10000, 10000, 10000, 10000],
    ARRAY[['meeting', 'lunch'], ['training', 'presentation']]);

INSERT INTO sal_emp
    VALUES ('Carol',
    ARRAY[20000, 25000, 25000, 25000],
    ARRAY[['breakfast', 'consulting'], ['meeting', 'lunch']]);

请注意,数组元素是普通的SQL常量或表达式;例如,字符串文字是单引号,而不是数组文字中的双引号。ARRAY构造函数语法在数组构造函数中详细讨论。

5.15.3.访问数组

现在,我们可以在表格上运行一些查询。首先,我们展示如何访问数组的单个元素。此查询检索在第二季度薪水发生变化的员工的名称:

SELECT name FROM sal_emp WHERE pay_by_quarter[1] <> pay_by_quarter[2];

 name
-------
 Carol
(1 row)

数组下标数字写在方括号内。默认情况下,UXDB使用基于一的编号约定进行数组,即,n个元素的数组以array[1]开头,并以array[n]结尾。

此查询检索所有员工的第三季度薪水:

SELECT pay_by_quarter[3] FROM sal_emp;

 pay_by_quarter
----------------
          10000
          25000
(2 rows)

我们还可以访问数组的任意矩形切片或子数组。数组切片的表示方法是为一个或多个数组维度写入lower-bound:upper-bound。例如,此查询检索Bill在本周前两天的第一项日程安排:

SELECT schedule[1:2][1:1] FROM sal_emp WHERE name = 'Bill';

        schedule
------------------------
 {{meeting},{training}}
(1 row)

如果任何维度都写为切片,即包含冒号,则所有维度都被视为切片。任何仅具有单个数字(无冒号)的维度都被视为从1到指定数字。例如,[2]被视为[1:2],如下例所示:

SELECT schedule[1:2][2] FROM sal_emp WHERE name = 'Bill';

                 schedule
-------------------------------------------
 {{meeting,lunch},{training,presentation}}
(1 row)

为避免与非切片情况混淆,最好对所有维度使用切片语法,例如[1:2][1:1],而不是[2][1:1]

可以省略切片指定符的lower-bound和/或upper-bound;缺失的边界将被替换为数组下标的下限或上限。例如:

SELECT schedule[:2][2:] FROM sal_emp WHERE name = 'Bill';

        schedule
------------------------
 {{lunch},{presentation}}
(1 row)

SELECT schedule[:][1:1] FROM sal_emp WHERE name = 'Bill';

        schedule
------------------------
 {{meeting},{training}}
(1 row)

如果数组本身或数组元素为null,则数组下标表达式将返回null。如果任何下标表达式为空,则返回 null。此外,如果下标超出数组边界,则返回null(此情况不会引发错误)。例如,如果 schedule 当前具有维度 [1:3][1:2],则引用schedule[3][3] 会产生 NULL。同样,如果数组引用的下标数量不正确,则返回 null 而不是错误。

如果数组本身或任何下标表达式为空,则数组切片表达式同样返回null。但是,在其他情况下,例如选择完全超出当前数组边界的数组切片,则切片表达式返回一个空(零维)数组而不是null。(这与非切片行为不匹配,出于历史原因而这样做。)如果请求的切片部分重叠数组边界,则将其静默缩小为仅重叠区域,而不是返回 null。

可以使用 array_dims 函数检索任何数组值的当前维度:

SELECT array_dims(schedule) FROM sal_emp WHERE name = 'Carol';

 array_dims
------------
 [1:2][1:2]
(1 row)

array_dims 生成一个 text 结果,这对人们来说很方便,但对程序来说可能不方便。也可以使用 array_upperarray_lower 检索维度,它们分别返回指定数组维度的上限和下限:

SELECT array_upper(schedule, 1) FROM sal_emp WHERE name = 'Carol';

 array_upper
-------------
           2
(1 row)

array_length 将返回指定数组维度的长度:

SELECT array_length(schedule, 1) FROM sal_emp WHERE name = 'Carol';

 array_length
--------------
            2
(1 row)

cardinality 返回跨所有维度的数组中元素的总数。它实际上是调用 unnest 将产生的行数:

SELECT cardinality(schedule) FROM sal_emp WHERE name = 'Carol';

 cardinality
-------------
           4
(1 row)

5.15.4.修改数组

可以完全替换数组值:

UPDATE sal_emp SET pay_by_quarter = '{25000,25000,27000,27000}'
    WHERE name = 'Carol';

或使用 ARRAY 表达式语法:

UPDATE sal_emp SET pay_by_quarter = ARRAY[25000,25000,27000,27000]
    WHERE name = 'Carol';

也可以在单个元素中更新数组:

UPDATE sal_emp SET pay_by_quarter[4] = 15000
    WHERE name = 'Bill';

或在切片中更新:

UPDATE sal_emp SET pay_by_quarter[1:2] = '{27000,27000}'
    WHERE name = 'Carol';

也可以使用省略了下限和/或上限的切片语法,但仅在更新不为 NULL 或零维的数组值时才能使用(否则,没有现有的下标限制可替换)。

存储的数组值可以通过分配尚未存在的元素来扩大。介于先前存在的位置和新分配的元素之间的任何位置都将填充为 null。例如,如果数组myarray 当前具有 4 个元素,则在分配给 myarray[6] 后,它将具有六个元素;myarray[5] 将包含null。目前,这种方式的扩展仅允许一维数组,而不是多维数组。

下标赋值允许创建不使用基于1的下标的数组。例如,可以分配给myarray[-2:7]来创建具有下标值从-2到7的数组。

也可以使用连接运算符||构建新的数组值:

SELECT ARRAY[1,2] || ARRAY[3,4];
 ?column?
-----------
 {1,2,3,4}
(1 row)

SELECT ARRAY[5,6] || ARRAY[[1,2],[3,4]];
      ?column?
---------------------
 {{5,6},{1,2},{3,4}}
(1 row)

连接运算符允许将单个元素推送到一维数组的开头或结尾。它还接受两个N维数组或一个N维和一个N+1维数组。

当将单个元素推送到一维数组的开头或结尾时,结果是具有与数组操作数相同的下限下标的数组。例如:

SELECT array_dims(1 || '[0:1]={2,3}'::int[]);
 array_dims
------------
 [0:2]
(1 row)

SELECT array_dims(ARRAY[1,2] || 3);
 array_dims
------------
 [1:3]
(1 row)

当连接具有相等维数的两个数组时,结果保留左操作数的外部维度的下限下标。结果是由左操作数的每个元素后跟右操作数的每个元素组成的数组。例如:

SELECT array_dims(ARRAY[1,2] || ARRAY[3,4,5]);
 array_dims
------------
 [1:5]
(1 row)

SELECT array_dims(ARRAY[[1,2],[3,4]] || ARRAY[[5,6],[7,8],[9,0]]);
 array_dims
------------
 [1:5][1:2]
(1 row)

当将N维数组推送到N+1维数组的开头或结尾时,结果类似于元素数组情况。每个N维子数组本质上是N+1维数组的外部维度的元素。例如:

SELECT array_dims(ARRAY[1,2] || ARRAY[[3,4],[5,6]]);
 array_dims
------------
 [1:3][1:2]
(1 row)

也可以使用函数array_prependarray_appendarray_cat构建数组。前两个仅支持一维数组,但array_cat支持多维数组。一些例子:

SELECT array_prepend(1, ARRAY[2,3]);
 array_prepend
---------------
 {1,2,3}
(1 row)

SELECT array_append(ARRAY[1,2], 3);
 array_append
--------------
 {1,2,3}
(1 row)

SELECT array_cat(ARRAY[1,2], ARRAY[3,4]);
 array_cat
-----------
 {1,2,3,4}
(1 row)

SELECT array_cat(ARRAY[[1,2],[3,4]], ARRAY[5,6]);
      array_cat
---------------------
 {{1,2},{3,4},{5,6}}
(1 row)

SELECT array_cat(ARRAY[5,6], ARRAY[[1,2],[3,4]]);
      array_cat
---------------------
 {{5,6},{1,2},{3,4}}

在简单情况下,上面讨论的连接运算符优于直接使用这些函数。但是,由于连接运算符被重载为服务于所有三种情况,因此在某些情况下,使用其中一个函数有助于避免歧义。例如考虑:

SELECT ARRAY[1, 2] || '{3, 4}';  -- 未打类型的文字被视为数组
 ?column?
-----------
 {1,2,3,4}

SELECT ARRAY[1, 2] || '7';                 -- 这个也是
ERROR:  malformed array literal: "7"

SELECT ARRAY[1, 2] || NULL;                -- 这是一个未装饰NULL
 ?column?
----------
 {1,2}
(1 row)

SELECT array_append(ARRAY[1, 2], NULL);    -- 这可能是想要的
 array_append
--------------
 {1,2,NULL}

在上面的示例中,解析器在连接运算符的一侧看到一个整数数组,在另一侧看到一个未确定类型的常量。它用于解析常量类型的启发式方法是假定它与运算符的其他输入具有相同的类型-在这种情况下,整数数组。因此,连接运算符被认为代表array_cat,而不是array_append。当这是错误的选择时,可以通过将常量转换为数组的元素类型来修复它;但是,显式使用array_append可能是更可取的解决方案。

5.15.5.在数组中搜索

要在数组中搜索值,必须检查每个值。如果您知道数组的大小,则可以手动执行此操作。例如:

SELECT * FROM sal_emp WHERE pay_by_quarter[1] = 10000 OR
                            pay_by_quarter[2] = 10000 OR
                            pay_by_quarter[3] = 10000 OR
                            pay_by_quarter[4] = 10000;

但是,对于大型数组,这很快变得乏味,并且如果数组的大小未知,则没有帮助。上面的查询可以替换为:

SELECT * FROM sal_emp WHERE 10000 = ANY (pay_by_quarter);

此外,您可以找到数组的所有值都等于10000的行:

SELECT * FROM sal_emp WHERE 10000 = ALL (pay_by_quarter);

或者,可以使用generate_subscripts函数。例如:

SELECT * FROM
   (SELECT pay_by_quarter,
           generate_subscripts(pay_by_quarter, 1) AS s
      FROM sal_emp) AS foo
 WHERE pay_by_quarter[s] = 10000;

您还可以使用&&运算符搜索数组,该运算符检查左操作数是否与右操作数重叠。例如:

SELECT * FROM sal_emp WHERE pay_by_quarter && ARRAY[10000];

可以通过适当的索引加速它。

您还可以使用array_position和array_positions函数在数组中搜索特定值。前者返回数组中第一个值的下标,后者返回包含数组中所有出现值的下标的数组。例如:

SELECT array_position(ARRAY['sun','mon','tue','wed','thu','fri','sat'], 'mon');
 array_position
----------------
              2
(1 row)

SELECT array_positions(ARRAY[1, 4, 3, 1, 3, 4, 2, 1], 1);
 array_positions
-----------------
 {1,4,8}
(1 row)

提示

数组不是集合;搜索特定数组元素可能是数据库设计不良的迹象。考虑使用单独的表,每个项目都是数组元素的一行。这将更容易搜索,并且对于大量元素,可能会更好地扩展。

5.15.6.数组输入和输出语法

数组值的外部文本表示由根据数组元素类型的 I/O 转换规则解释的项组成,以及指示数组结构的修饰符。修饰符由花括号 ({})包围数组值,以及相邻项之间的分隔符字符组成。分隔符字符通常是逗号 (,),但也可以是其他字符:它由数组元素类型的typdelim 设置确定。在 UXDB 发行版提供的所有标准数据类型中,除了类型 box 使用分号(;),其余都使用逗号。在多维数组中,每个维度(行、平面、立方体等)都有自己的一级花括号,并且必须在同一级别的花括号实体之间写入分隔符。

如果数组元素值为空字符串、包含花括号、分隔符字符、双引号、反斜杠或空格,或与单词 NULL匹配,则数组输出例程将在元素值周围放置双引号。嵌入元素值的双引号和反斜杠将被反斜杠转义。对于数值数据类型,可以安全地假定双引号永远不会出现,但对于文本数据类型,应准备好处理引号的存在或不存在。

默认情况下,数组维度的下限索引值设置为 1。要表示具有其他下限的数组,可以在写入数组内容之前显式指定数组下标范围。此修饰符由方括号 ([])包围每个数组维度的下限和上限,中间用冒号 (:) 分隔符字符分隔。数组维度修饰符后跟等号 (=)。例如:

SELECT f1[1][-2][3] AS e1, f1[1][-1][5] AS e2
 FROM (SELECT '[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}'::int[] AS f1) AS ss;

 e1 | e2
----+----
  1 |  6
(1 row)

只有当一个或多个下限不等于 1 时,数组输出例程才会在其结果中包含显式维度。

如果为元素写入的值为 NULL(任何大小写变体),则该元素被视为NULL。任何引号或反斜杠的存在都会禁用此功能,并允许输入文本字符串值“NULL”。

如前所示,在写入数组值时,可以在任何单个数组元素周围使用双引号。如果元素值否则会使数组值解析器混淆,则必须这样做。例如,包含花括号、逗号(或数据类型的分隔符字符)、双引号、反斜杠或前导或尾随空格的元素必须用双引号括起来。空字符串和与单词NULL匹配的字符串也必须用引号括起来。要在带引号的数组元素值中放置双引号或反斜杠,必须在其前面加上反斜杠。或者,可以避免使用引号,并使用反斜杠转义来保护所有否则将被视为数组语法的数据字符。

您可以在左括号之前或右括号之后添加空格。支架。您还可以在任何单个项字符串之前或之后添加空格。在所有这些情况下,空格将被忽略。但是,在双引号元素内部或由元素的非空格字符包围的两侧的空格不会被忽略。

提示

在编写SQL命令中的数组值时,使用ARRAY构造函数语法(请参见数组构造函数)通常比数组文字语法更容易使用。在ARRAY中,单个元素值的编写方式与它们不是数组成员时的编写方式相同。

5.16.复合类型

复合类型表示行或记录的结构;它本质上只是一个字段名称和它们的数据类型的列表。UXDB允许像简单类型一样使用复合类型。例如,可以声明一个表的列为复合类型。

5.16.1.声明复合类型

以下是定义复合类型的两个简单示例:

CREATE TYPE complex AS (
    r       double precision,
    i       double precision
);

CREATE TYPE inventory_item AS (
    name            text,
    supplier_id     integer,
    price           numeric
);

语法类似于 CREATE TABLE,只能指定字段名称和类型;目前不能包括约束(如 NOT NULL)。注意,关键字 AS是必需的;如果没有它,系统将认为是另一种类型的 CREATE TYPE 命令,你将得到奇怪的语法错误。

定义类型后,可以使用它们创建表:

CREATE TABLE on_hand (
    item      inventory_item,
    count     integer
);

INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000);

或函数:

CREATE FUNCTION price_extension(inventory_item, integer) RETURNS numeric
AS 'SELECT $1.price * $2' LANGUAGE SQL;

SELECT price_extension(item, 10) FROM on_hand;

每当您创建一个表时,还会自动创建一个复合类型,其名称与表相同,用于表示表的行类型。例如,如果我们说:

CREATE TABLE inventory_item (
    name            text,
    supplier_id     integer REFERENCES suppliers,
    price           numeric CHECK (price > 0)
);

那么与上面显示的相同的inventory_item复合类型将会成为副产品,并且可以像上面那样使用。但是请注意当前实现的一个重要限制:由于没有将约束与复合类型关联,因此表定义中显示的约束不适用于表外的复合类型值。(要解决这个问题,请在复合类型上创建一个域,并将所需的约束作为域的CHECK约束应用。)

5.16.2.构造复合值

要将复合值写为文字常量,请在括号内放置字段值,并用逗号分隔。您可以在任何字段值周围放置双引号,如果它包含逗号或括号,则必须这样做。因此,复合常量的一般格式如下:

'( val1 , val2 , ... )'

例如:

'("fuzzy dice",42,1.99)'

这将是上面定义的inventory_item类型的有效值。要使字段为空,请在列表中不写任何字符。例如,此常量指定了第三个字段为空:

'("fuzzy dice",42,)'

如果您想要一个空字符串而不是NULL,请写双引号:

'("",42,)'

这里第一个字段是非NULL空字符串,第三个字段是NULL。

(这些常量实际上只是其他类型的常量中讨论的通用类型常量的特殊情况。常量最初被视为字符串并传递给复合类型输入转换例程。可能需要明确的类型说明来告诉将常量转换为哪种类型。)

ROW表达式语法也可用于构造复合值。在大多数情况下,这比字符串文字语法要简单得多,因为您不必担心多层引用。我们已经在上面使用了这种方法:

rOW('fuzzy dice', 42, 1.99)

ROW('', 42, NULL)

只要表达式中有多个字段,ROW关键字实际上是可选的,因此这些可以简化为:

('fuzzy dice', 42, 1.99)
('', 42, NULL)

ROW表达式语法在行构造器

5.16.3.访问复合类型

要访问复合列的字段,可以编写一个点和字段名称,就像从表名选择字段一样。实际上,它非常就像从表名中选择时,您经常需要使用括号来避免混淆解析器一样。例如,您可能会尝试从我们的on_hand示例表中选择一些子字段,如下所示:

SELECT item.name FROM on_hand WHERE item.price > 9.99;

这不起作用,因为根据SQL语法规则,名称item被视为表名,而不是on_hand的列名。您必须像这样编写它:

SELECT (item).name FROM on_hand WHERE (item).price > 9.99;

或者如果您还需要使用表名(例如在多表查询中),则像这样:

SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9.99;

现在,括号中的对象被正确解释为对item列的引用,然后可以从中选择子字段。

类似的语法问题也适用于每当您从复合值中选择字段时。例如,要仅从返回复合值的函数的结果中选择一个字段,您需要编写类似于以下内容的内容:

SELECT (my_func(...)).field FROM ...

没有额外的括号,这将生成语法错误。

5.16.4.修改复合类型

以下是插入和更新复合列的正确语法示例。首先,插入或更新整个列:

INSERT INTO mytab (complex_col) VALUES((1.1,2.2));

UPDATE mytab SET complex_col = ROW(1.1,2.2) WHERE ...;

第一个示例省略了ROW,第二个使用了它;我们可以任选一种方式。

我们可以更新复合列的单个子字段:

UPDATE mytab SET complex_col.r = (complex_col).r + 1 WHERE ...;

请注意,这里我们不需要(实际上不能)在SET后面出现的列名周围放括号,但是在等号右侧的表达式中引用同一列时需要括号。

我们也可以将子字段指定为INSERT的目标:

INSERT INTO mytab (complex_col.r, complex_col.i) VALUES(1.1, 2.2);

如果我们没有为列的所有子字段提供值,则其余子字段将填充为null值。

5.16.5.在查询中使用复合类型

在查询中,与复合类型相关的有各种特殊的语法规则和行为。这些规则提供了有用的快捷方式,但如果您不知道背后的逻辑,可能会感到困惑。

在UXDB中,查询中对表名(或别名)的引用实际上是对表当前行的复合值的引用。例如,如果我们有一个如上所示的inventory_item表,我们可以编写:

SELECT c FROM inventory_item c;

此查询生成一个单个的复合值列,因此我们可能会得到如下输出:

           c
------------------------
 ("fuzzy dice",42,1.99)
(1 row)

请注意,简单名称在匹配表名之前会与列名匹配,因此此示例仅适用于查询的表中没有名为c的列的情况。

普通的限定列名语法table_name.column_name可以理解为将字段选择应用于表的当前行的复合值。(出于效率原因,实际上并不是这样实现的。)

当我们写下:

SELECT c.* FROM inventory_item c;

然后,根据SQL标准,我们应该将表的内容展开为单独的列:

    name    | supplier_id | price
------------+-------------+-------
 fuzzy dice |          42 |  1.99
(1 row)

就好像查询是这样的:

SELECT c.name, c.supplier_id, c.price FROM inventory_item c;

UXDB将对任何复合值表达式应用此扩展行为,尽管如上所示,每当它不是一个简单的表名时,您需要在应用.*的值周围写括号。例如,如果myfunc()是一个返回具有列abc的复合类型的函数,则这两个查询具有相同的结果:

SELECT (myfunc(x)).* FROM some_table;
SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;

提示

UXDB通过将第一种形式转换为第二种形式来处理列扩展。因此,在此示例中,每行将使用任一语法调用三次myfunc()。如果它是一个昂贵的函数,您可能希望避免这种情况,您可以使用以下查询:

SELECT m.* FROM some_table, LATERAL myfunc(x) AS m;

将函数放在LATERAL``FROM项中可防止它在每行中被调用多次。m.*仍然扩展为m.a,m.b,m.c,但现在这些变量只是对FROM项的输出的引用。(此处的LATERAL关键字是可选的,但我们显示它以澄清函数正在从some_table获取x。)

当它出现在选择列表INSERT/UPDATE/DELETE中的RETURNING列表、VALUES 列表行构造器的顶层时,composite_value.*语法会导致这种类型的列扩展。在所有其他上下文中(包括嵌套在这些构造之一中时),将.*附加到复合值不会更改该值,因为它意味着“所有列”,因此如果somefunc()接受一个复合值参数,那么这些查询是相同的:

SELECT somefunc(c.*) FROM inventory_item c;
SELECT somefunc(c) FROM inventory_item c;

在这两种情况下,当前的inventory_item行作为单个复合值参数传递给函数。即使.*在这种情况下没有任何作用,使用它也是好的风格,因为它清楚地表明了意图使用复合值。特别地,解析器将认为c.*中的c是一个表名或别名,而不是列名,因此没有歧义;而没有.*,则不清楚c是指表名还是列名,实际上,如果有一个名为c的列,则会优先选择列名解释。

演示这些概念的另一个例子是,所有这些查询都是相同的:

SELECT * FROM inventory_item c ORDER BY c;
SELECT * FROM inventory_item c ORDER BY c.*;
SELECT * FROM inventory_item c ORDER BY ROW(c.*);

所有这些ORDER BY子句都指定了行的复合值,导致按照规则对行进行排序。但是,如果inventory_item包含一个名为c的列,则第一种情况将与其他情况不同,因为它将意味着仅按该列排序。鉴于先前显示的列名,这些查询也等同于上面的查询:

SELECT * FROM inventory_item c ORDER BY ROW(c.name, c.supplier_id, c.price);
SELECT * FROM inventory_item c ORDER BY (c.name, c.supplier_id, c.price);

(最后一种情况省略了关键字ROW的行构造器。)

与复合值相关的另一个特殊语法行为是,我们可以使用函数表示法来提取复合值的字段。简单的解释是,符号field(table)table.*field*是可以互换的。例如,这些查询是等价的:

SELECT c.name FROM inventory_item c WHERE c.price > 1000;
SELECT name(c) FROM inventory_item c WHERE price(c) > 1000;

此外,如果我们有一个接受单个复合类型参数的函数,我们可以使用任一表示法来调用它。这些查询都是等价的:

SELECT somefunc(c) FROM inventory_item c;
SELECT somefunc(c.*) FROM inventory_item c;
SELECT c.somefunc FROM inventory_item c;

函数表示法和字段表示法之间的这种等价性使得可以使用复合类型上的函数来实现“计算字段”。使用上面的最后一个查询的应用程序不需要直接意识到somefunc不是表的真实列。

提示

由于这种行为,给一个接受单个复合类型参数的函数赋予与该复合类型的任何字段相同的名称是不明智的。如果存在歧义,则如果使用字段名称语法,则将选择字段名称解释,而如果使用函数表示法,则将选择函数名称解释。如果使用函数调用语法,则将选择函数。然而,在11版本之前的UXDB中,除非调用语法要求是函数调用,否则始终选择字段名称解释。在旧版本中强制使用函数解释的一种方法是对函数名称进行模式限定,即写成schema.func(compositevalue).

5.16.6.复合类型输入和输出语法

复合类型的外部文本表示由根据各个字段类型的I/O转换规则进行解释的项目组成,以及指示复合结构的装饰。装饰包括整个值周围的括号(()),以及相邻项目之间的逗号(,)。括号外的空格将被忽略,但在括号内部,它被视为字段值的一部分,具体取决于字段数据类型的输入转换规则,可能是重要的也可能不是重要的。例如,在:

'(  42)'

如果字段类型是整数,则空格将被忽略,但如果是文本,则不会被忽略。

如前所述,当编写复合值时,您可以在任何单个字段值周围写双引号。如果字段值否则会混淆复合值解析器,则必须这样做。特别是,包含括号、逗号、双引号或反斜杠的字段必须用双引号括起来。要在带引号的复合字段值中放置双引号或反斜杠,请在其前面加上反斜杠。(此外,在SQL文字字符串中的单引号的规则类似于双引号中的规则,一对双引号在双引号引用的字段值中表示一个双引号字符。)或者,您可以避免引用并使用反斜杠转义来保护所有数据字符,否则将被视为复合语法。

完全空的字段值(逗号或括号之间没有任何字符)表示NULL。要写一个空字符串而不是NULL的值,请写成""

如果字段值是空字符串或包含括号、逗号、双引号、反斜杠或空格,则复合输出例程将在其周围放置双引号。(这样做对于空格并不重要,但有助于可读性。)嵌入字段值中的双引号和反斜杠将加倍。

注意

请记住,在SQL命令中编写的内容首先将被解释为字符串文字,然后再解释为复合。这将使您需要双倍的反斜杠(假设使用转义字符串语法)。例如,要在复合值中插入包含双引号和反斜杠的text字段,您需要编写:

INSERT ... VALUES ('("\"\\")');

字符串文字处理器会删除一个反斜杠级别,因此到达复合值解析器的内容看起来像("\"\\")。反过来,传递给text数据类型的输入例程的字符串变成"\。(如果我们正在使用一个输入例程也特别处理反斜杠的数据类型,例如bytea,则可能需要在命令中使用多达八个反斜杠才能将一个反斜杠放入存储的复合字段中。)可以使用美元引用(请参见词法结构)来避免需要双倍反斜杠的需要。

提示

在编写 SQL 命令中的复合值时,ROW 构造函数语法通常比复合字面量语法更易于使用。在 ROW中,单个字段值的编写方式与它们不是复合成员时的编写方式相同。

5.17.范围类型

范围类型是表示某个元素类型(称为范围的子类型)值范围的数据类型。例如,timestamp的范围可以用来表示会议室预订的时间范围。在这种情况下,数据类型是tsrange(缩写为“时间戳范围”),而timestamp是子类型。子类型必须具有完全顺序,以便明确定义元素值是否在值范围内、之前或之后。

范围类型很有用,因为它们可以用单个范围值表示许多元素值,并且可以清晰地表达重叠范围等概念。时间和日期范围用于调度目的是最清晰的例子;但价格范围、来自仪器的测量范围等也可以很有用。

每个范围类型都有相应的多范围类型。多范围是非连续、非空、非空值范围的有序列表。大多数范围运算符也适用于多范围,它们还有一些自己的函数。

5.17.1.内置范围和多范围类型

UXDB带有以下内置范围类型:

  • int4range — 整数范围,int4multirange — 对应的多范围

  • int8range — 大整数范围,int8multirange — 对应的多范围

  • numrange — 数字范围,nummultirange — 对应的多范围

  • tsrange — 不带时区的时间戳范围,tsmultirange — 对应的多范围

  • tstzrange — 带时区的时间戳范围,tstzmultirange — 对应的多范围

  • daterange — 日期范围,datemultirange — 对应的多范围

此外,您可以定义自己的范围类型。

5.17.2.示例

CREATE TABLE reservation (room int, during tsrange);
INSERT INTO reservation VALUES
    (1108, '[2010-01-01 14:30, 2010-01-01 15:30)');

-- 包含
SELECT int4range(10, 20) @> 3;

-- 重叠
SELECT numrange(11.1, 22.2) && numrange(20.0, 30.0);

-- 提取上限
SELECT upper(int8range(15, 25));

-- 计算交集
SELECT int4range(10, 20) * int4range(15, 25);

-- 范围是否为空?
SELECT isempty(numrange(1, 5));

5.17.3.包含和排除边界

每个非空范围都有两个边界,下限和上限。这些值之间的所有点都包含在范围内。包含边界意味着边界点本身也包含在范围内,而排除边界意味着边界点不包含在范围内。

在范围的文本形式中,包含下限由“[”表示,而排除下限由“(”表示。同样,包含上限由“]”表示,而排除上限由“)”表示。

函数lower_incupper_inc分别测试范围值的下限和上限的包含性。

5.17.4.无限(无界)范围

范围的下限可以省略,这意味着所有小于上限的值都包含在范围内,例如(,3]。同样,如果省略范围的上限,则所有大于下限的值都包含在范围内。如果下限和上限都省略,则认为元素类型的所有值都在范围内。将缺失的边界指定为包含的会自动转换为排除的,例如[,]转换为(,)。您可以将这些缺失的值视为+/-无穷大,但它们是特殊的范围类型值,并被认为超出任何范围元素类型的+/-无穷大值。

具有“无穷大”概念的元素类型可以将其用作显式边界值。例如,对于时间戳范围,[today,infinity)排除特殊的timestampinfinity,而[today,infinity]包括它,[today,)[today,]也是如此。

函数lower_infupper_inf分别测试范围的无限下限和上限。

5.17.5.范围输入/输出

范围值的输入必须遵循以下模式之一:

(lower-bound,upper-bound)
(lower-bound,upper-bound]
[lower-bound,upper-bound)
[lower-bound,upper-bound]
empty

括号或方括号表示下限和上限是排除还是包含关系,如前所述。请注意,最后一个模式是empty,表示空范围(不包含任何点的范围)。

lower-bound可以是有效输入子类型的字符串,也可以为空以表示没有下限。同样,upper-bound可以是有效输入子类型的字符串,也可以为空以表示没有上限。

每个边界值都可以使用"(双引号)字符进行引用。如果边界值包含括号、方括号、逗号、双引号或反斜杠,则必须这样做,否则这些字符将被视为范围语法的一部分。要在引用的边界值中放置双引号或反斜杠,请在其前面加上反斜杠。(此外,在双引号中的一对双引号被视为表示双引号字符,类似于SQL文字字符串中单引号的规则。)或者,您可以避免引用并使用反斜杠转义来保护所有数据字符,否则将被视为范围语法。此外,要编写一个空字符串的边界值,请写"",因为什么也不写意味着无限制。

在范围值之前和之后允许空格,但是括号或方括号之间的任何空格都将视为下限或上限值的一部分。(根据元素类型,它可能或可能不是重要的。)

注意

这些规则与在SQL中编写字段值的规则非常相似。复合类型字面量。有关详细说明,请参见复合类型输入和输出语法

示例:

-- 包括3,不包括7,并包括其间所有点
SELECT '[3,7)'::int4range;

-- 不包括3或7,但包括其间所有点
SELECT '(3,7)'::int4range;

-- 仅包括单个点4
SELECT '[4,4]'::int4range;

-- 不包括任何点(将被规范化为“empty”)
SELECT '[4,4)'::int4range;

多范围的输入为花括号({}),其中包含零个或多个有效范围,用逗号分隔。括号和逗号周围允许有空格。这意味着它类似于数组语法,尽管多范围要简单得多:它们只有一个维度,不需要引用其内容。(但是,它们的范围边界可能像上面一样被引用。)

示例:

SELECT '{}'::int4multirange;
SELECT '{[3,7)}'::int4multirange;
SELECT '{[3,7), [8,9)}'::int4multirange;

5.17.6.构造范围和多范围

每个范围类型都有一个与范围类型名称相同的构造函数。使用构造函数通常比编写范围字面常量更方便,因为它避免了需要额外引用边界值的需要。构造函数接受两个或三个参数。两个参数的形式构造标准形式的范围(下限包含,上限不包含),而三个参数的形式构造具有由第三个参数指定的形式的边界的范围。第三个参数必须是以下字符串之一:““()””,““(]””,““[)””或““[]””。例如:

-- 完整形式为:下限,上限和指示边界包含/排除的文本参数。
SELECT numrange(1.0, 14.0, '(]');

-- 如果省略第三个参数,则假定为“[)”。
SELECT numrange(1.0, 14.0);

-- 尽管此处指定了“(]”,但在显示时,该值将被转换为规范形式,因为int8range是离散范围类型(见下文)。
SELECT int8range(1, 14, '(]');

-- 对任一边界使用NULL会导致该范围在该侧无界。
SELECT numrange(NULL, 2.2);

每个范围类型还具有与多范围类型名称相同的多范围构造函数。构造函数接受零个或多个参数,这些参数都是适当类型的范围。例如:

SELECT nummultirange();
SELECT nummultirange(numrange(1.0, 14.0));
SELECT nummultirange(numrange(1.0, 14.0), numrange(20.0, 25.0));

5.17.7.离散范围类型

离散范围是具有明确定义的“步长”的元素类型的范围,例如整数或日期。在这些类型中,当两个元素之间没有有效值时,可以说它们是相邻的。这与连续范围形成对比,在连续范围中,通常(或几乎总是)可以确定两个给定值之间的其他元素值。例如,对于numeric类型的范围和timestamp类型的范围,它们都是连续的。即使timestamp 的精度有限,因此在理论上可以将其视为离散的,但最好将其视为连续的,因为步长通常不是感兴趣的内容。

另一种思考离散范围类型的方法是,对于每个元素值,都有一个明确的“下一个”或“上一个”值的概念。在知道这一点的情况下,可以通过选择下一个或上一个元素值而不是最初给定的元素值来在包含和排除表示法之间进行转换。例如,在整数范围类型中,[4,8](3,9) 表示相同的值集;但对于数值范围,情况并非如此。

离散范围类型应该有一个规范化函数,该函数了解元素类型的所需步长。规范化函数负责将等价的范围类型值转换为具有相同表示的值,特别是一致的包含或排除边界。如果未指定规范化函数,则具有不同格式的范围将始终被视为不相等,即使它们在现实中可能表示相同的值集。

内置的范围类型 int4rangeint8rangedaterange 都使用包括下限和排除上限的规范形式,即[)。然而,用户定义的范围类型可以使用其他约定。

5.17.8.定义新的范围类型

用户可以定义自己的范围类型。这样做的最常见原因是使用不在内置范围类型中提供的子类型的范围。例如,要定义一个子类型为float8 的新范围类型:

CREATE TYPE floatrange AS RANGE (
    subtype = float8,
    subtype_diff = float8mi
);

SELECT '[1.234, 5.678]'::floatrange;

因为float8 没有有意义的“步长”,所以在此示例中不定义规范化函数。

当您定义自己的范围时,还会自动获得相应的多范围类型。

定义自己的范围类型还允许您指定不同的子类型 B-tree操作符类或排序规则,以更改确定哪些值落入给定范围的排序顺序。

如果认为子类型具有离散而不是连续的值,则 CREATE TYPE命令应指定一个规范化函数。规范化函数接受输入范围值,并必须返回一个等价的范围值,该值可能具有不同的边界和格式。例如,表示相同值集的两个范围,例如整数范围[1, 7][1, 8),其规范输出必须相同。无论选择哪种表示形式作为规范形式,只要具有不同格式的两个等价值始终被映射到具有相同格式的相同值,就可以了。除了调整包含/排除边界格式外,规范化函数还可以舍入边界值,以防所需步长大于子类型能够存储的步长。例如,可以定义一个基于timestamp 的范围类型,其步长为一小时,在这种情况下,规范化函数需要舍入不是小时的倍数的边界,或者可能抛出错误。

此外,任何旨在与GiST 或 SP-GiST 一起使用的范围类型都应该有一个支持 GiST操作符类的函数。索引应该定义一个子类型差异或subtype_diff函数。(即使没有subtype_diff,索引仍然可以工作,但是如果提供了差异函数,它可能会比没有提供的情况下更加高效。)子类型差异函数接受子类型的两个输入值,并将它们的差异返回为float8值(即 X 减去 Y )。在上面的示例中,可以使用支持常规float8减法运算符的函数float8mi,但是对于任何其他子类型,都需要进行一些类型转换。还需要一些有关如何将差异表示为数字的创造性思考。在最大程度上,subtype_diff函数应该与所选操作符类和排序规则所暗示的排序顺序一致;也就是说,它的结果应该在根据排序顺序第一个参数大于第二个参数时为正数。

一个不那么简化的subtype_diff函数示例是

CREATE FUNCTION time_subtype_diff(x time, y time) RETURNS float8 AS
'SELECT EXTRACT(EPOCH FROM (x - y))' LANGUAGE sql STRICT IMMUTABLE;

CREATE TYPE timerange AS RANGE (
    subtype = time,
    subtype_diff = time_subtype_diff
);

SELECT '[11:10, 23:00]'::timerange;

5.17.9.索引

可以为范围类型的表列创建GiST和SP-GiST索引。还可以为多范围类型的表列创建GiST索引。例如,要创建GiST索引:

CREATE INDEX reservation_idx ON reservation USING GIST (during);

范围上的GiST或SP-GiST索引可以加速涉及以下范围运算符的查询:=&&<@@><<>>-|-&<&>。多范围的GiST索引可以加速涉及相同一组多范围运算符的查询。范围的GiST索引和多范围的GiST索引还可以分别加速涉及这些跨类型范围到多范围和多范围到范围运算符的查询:&&<@@><<>>-|-&<&>

此外,可以为范围类型的表列创建B-tree和哈希索引。对于这些索引类型,基本上唯一有用的范围操作是相等性。为范围值定义了B-tree排序顺序,具有相应的<>运算符,但排序是相当任意的,通常在实际世界中不太有用。范围类型的B-tree和哈希支持主要是为了允许在查询内部进行排序和哈希,而不是创建实际索引。

5.17.10.范围约束

虽然UNIQUE是标量值的自然约束,但通常不适用于范围类型。相反,排除约束通常更合适。排除约束允许在范围类型上指定诸如“不重叠”之类的约束。例如:

CREATE TABLE reservation (
    during tsrange,
    EXCLUDE USING GIST (during WITH &&)
);

该约束将防止任何重叠的值同时存在于表中:

INSERT INTO reservation VALUES
    ('[2010-01-01 11:30, 2010-01-01 15:00)');
INSERT 0 1

INSERT INTO reservation VALUES
    ('[2010-01-01 14:45, 2010-01-01 15:45)');
ERROR:  conflicting key value violates exclusion constraint "reservation_during_excl"
DETAIL:  Key (during)=(["2010-01-01 14:45:00","2010-01-01 15:45:00")) conflicts
with existing key (during)=(["2010-01-01 11:30:00","2010-01-01 15:00:00")).

您可以使用btree_gist扩展来定义纯标量数据类型的排除约束,然后与范围排除组合以实现最大的灵活性。例如,在安装了btree_gist之后,以下约束将仅拒绝重叠范围,如果会议室编号相等:

CREATE EXTENSION btree_gist;
CREATE TABLE room_reservation (
    room text,
    during tsrange,
    EXCLUDE USING GIST (room WITH =, during WITH &&)
);

INSERT INTO room_reservation VALUES
    ('123A', '[2010-01-01 14:00, 2010-01-01 15:00)');
INSERT 0 1

INSERT INTO room_reservation VALUES
    ('123A', '[2010-01-01 14:30, 2010-01-01 15:30)');
ERROR:  conflicting key value violates exclusion constraint "room_reservation_room_during_excl"
DETAIL:  Key (room, during)=(123A, ["2010-01-01 14:30:00","2010-01-01 15:30:00")) conflicts
with existing key (room, during)=(123A, ["2010-01-01 14:00:00","2010-01-01 15:00:00")).

INSERT INTO room_reservation VALUES
    ('123B', '[2010-01-01 14:30, 2010-01-01 15:30)');
INSERT 0 1

5.18.域类型

是基于另一种基础类型的用户定义数据类型。它可以选择性地具有约束条件,将其有效值限制为基础类型允许的子集。否则,它的行为类似于基础类型,例如,可以应用于基础类型的任何运算符或函数将在域类型上工作。基础类型可以是任何内置或用户定义的基本类型、枚举类型、数组类型、复合类型、范围类型或另一个域。

例如,我们可以创建一个仅接受正整数的整数域:

CREATE DOMAIN posint AS integer CHECK (VALUE > 0);
CREATE TABLE mytable (id posint);
INSERT INTO mytable VALUES(1);   -- 成功
INSERT INTO mytable VALUES(-1);  -- 失败

当对域值应用基础类型的运算符或函数时,域会自动向下转换为基础类型。因此,例如,mytable.id- 1的结果被认为是integer类型而不是posint类型。我们可以编写(mytable.id - 1)::posint将结果转换回posint,导致重新检查域的约束条件。在这种情况下,如果将表达式应用于id值为1的情况下,将会导致错误。允许将基础类型的值分配给域类型的字段或变量,而不需要编写显式转换,但将检查域的约束条件。

5.19.对象标识符类型

对象标识符(OID)在UXDB内部用作各种系统表的主键。类型oid表示对象标识符。此外,还有几个别名类型用于oid,每个别名类型都命名为regsomething表 对象标识符类型显示了一个概述。

oid类型当前实现为无符号四字节整数。因此,在大型数据库或大型单个表中,它不足以提供数据库范围内的唯一性。

oid类型本身除了比较之外几乎没有操作。但是,它可以转换为整数,然后使用标准整数运算符进行操作。(如果这样做,请注意可能存在有符号与无符号混淆。)

OID别名类型本身没有任何操作,除了专门的输入和输出例程。这些例程能够接受和显示系统对象的符号名称,而不是oid类型将使用的原始数字值。别名类型允许简化对象的OID值的查找。例如,要查看与表mytable相关的ux_attribute行,可以编写:

SELECT * FROM ux_attribute WHERE attrelid = 'mytable'::regclass;

而不是:

SELECT * FROM ux_attribute
  WHERE attrelid = (SELECT oid FROM ux_class WHERE relname = 'mytable');

虽然这本身看起来并不那么糟糕,但仍然过于简化了。如果存在不同模式下命名为mytable的多个表,则需要更复杂的子选择才能选择正确的OID。regclass输入转换器根据模式路径设置处理表查找,因此它会自动执行“正确的操作”。同样,将表的OID转换为regclass对于数字OID的符号显示非常方便。

表 对象标识符类型

名字引用表描述值示例
oid任意数字形式的对象标识符564182
regprocux_proc函数名字sum
regprocedureux_proc带参数类型的函数sum(int4)
regoperux_operator操作符名字+
regoperatorux_operator带参数类型的操作符*(integer,integer)-(NONE,integer)
regclassux_class关系名字(表、视图、索引等)ux_type
regtypeux_type数据类型名字integer
regroleux_authid角色名smithee
regnamespaceux_namespace名字空间名称(Schema)ux_catalog
regconfigux_ts_config文本搜索配置english
regdictionaryux_ts_dict文本搜索字典simple

所有OID别名类型都接受模式限定名称,并且如果对象没有被限定,则在输出中显示模式限定名称。例如,如果存在这样的表,则myschema.mytableregclass的可接受输入。该值可能作为myschema.mytablemytable输出,具体取决于当前搜索路径。regprocregoper别名类型仅接受唯一的输入名称(不重载),因此它们的使用受到限制;对于大多数用途,regprocedureregoperator更为合适。对于regoperator,输入名称必须是唯一的。一元运算符通过在未使用的操作数中写入NONE来标识。

这些类型的输入函数允许在标记之间使用空格,并将大写字母折叠为小写字母,除非在双引号内;这样做是为了使语法规则类似于在SQL中编写对象名称的方式。相反,输出函数将使用双引号(如果需要)使输出成为有效的SQL标识符。例如,名为Foo(带有大写F)的函数的OID,该函数接受两个整数参数,可以输入为'"Foo" ( int, integer )'::regprocedure。输出将看起来像"Foo"(integer,integer)。函数名称和参数类型名称也可以是模式限定的。

许多内置的UXDB函数接受表的OID或另一种数据库对象的OID,并为方便起见声明为接受regclass(或适当的OID别名类型)。这意味着您不必手动查找对象的OID,而只需将其名称输入为字符串文字。例如,nextval(regclass)函数接受序列关系的OID,因此您可以像这样调用它:

nextval('foo')              operates on sequence foo
nextval('FOO')              same as above
nextval('"Foo"')            operates on sequence Foo
nextval('myschema.foo')     operates on myschema.foo
nextval('"myschema".foo')   same as above
nextval('foo')              searches search path for foo

注意

当您将此类函数的参数写为未装饰的文字字符串时,它将成为regclass(或适当类型)的常量。由于这实际上只是一个OID,因此它将跟踪最初标识的对象,尽管稍后重命名,模式重新分配等。这种“早期绑定”行为通常适用于列默认值和视图中的对象引用。但有时您可能希望“延迟绑定”,其中对象引用在运行时解析。要获取延迟绑定行为,请将常量强制存储为text常量,而不是regclass

nextval('foo'::text)      foo is looked up at runtime

to_regclass()函数及其兄弟也可用于执行运行时查找。

regclass的另一个实际示例是查找在information_schema视图中列出的表的OID,这些视图不直接提供此类OID。例如,可能希望调用ux_relation_size()函数,该函数需要表OID。考虑到上述规则,正确的方法是:

SELECT table_schema, table_name,
       ux_relation_size((quote_ident(table_schema) || '.' ||
                         quote_ident(table_name))::regclass)
FROM information_schema.tables
WHERE ...

quote_ident() 函数将在需要时处理标识符的双引号。看似更简单的

SELECT ux_relation_size(table_name)
FROM information_schema.tables
WHERE ...

不建议使用,因为它会对超出搜索路径或需要引用的名称的表失败。

大多数 OID 别名类型的另一个属性是创建依赖项。如果这些类型之一的常量出现在存储的表达式中(例如列默认表达式或视图),它将创建对引用对象的依赖关系。例如,如果列具有默认值 表达式 nextval('my_seq'::regclass),UXDB 了解到默认表达式取决于序列my_seq,因此系统不会让序列 在删除默认表达式之前。nextval('my_seq'::text) 的替代方法不会 创建依赖关系。(regrole 是此属性的例外。此类型的常量 不允许在存储的表达式中使用。)

系统使用的另一种标识符类型是 xid 或事务(缩写为 xact)标识符。这是系统列的数据类型 xminxmax。事务标识符是 32 位数量。在某些情况下,使用 64 位变体 xid8。与 xid 值不同,xid8值严格增加 单调,不能在数据库群集的生命周期内重复使用。

系统使用的第三种标识符类型是 cid 或 命令标识符。这是系统列的数据类型 cmincmax。命令标识符也是 32位数量。

系统使用的最后一种标识符类型是 tid 或元组 标识符(行标识符)。这是系统列的数据类型 ctid。元组 ID 是一对(块编号,块内元组索引),用于标识 行在其表中的物理位置。

5.20.ux_lsn 类型

ux_lsn 数据类型可用于存储 LSN(日志序列号)数据,它是指向 WAL 中位置的指针。该类型是 XLogRecPtr的一种表示形式,是 UXDB 的内部系统类型。

在内部,LSN 是一个 64 位整数,表示写前日志流中的字节位置。它被打印为两个十六进制数字,每个数字最多有 8位,用斜杠分隔;例如,16/B374D848ux_lsn 类型支持标准比较运算符,如=>。可以使用 - 运算符对两个 LSN 进行减法运算;结果是这些写前日志位置之间的字节数。还可以使用+(ux_lsn,numeric)-(ux_lsn,numeric) 运算符将字节数添加到 LSN 中或从 LSN中减去字节数。请注意,计算出的 LSN 应该在 ux_lsn 类型的范围内,即在 0/0FFFFFFFF/FFFFFFFF 之间。

5.21.伪类型

UXDB 类型系统包含一些特殊用途的条目,统称为伪类型。伪类型不能用作列数据类型,但可以用于声明函数的参数或结果类型。每个可用的伪类型在函数的行为不对应于简单地获取或返回特定SQL数据类型的值的情况下都是有用的。表 伪类型列出了现有的伪类型。

表 伪类型

名称描述
any表示函数接受任何输入数据类型。
anyelement表示函数接受任何数据类型(通常用于多态函数中锁定同一类型)。
anyarray表示函数接受任何数组数据类型。
anynonarray表示函数接受任何非数组数据类型。
anyenum表示函数接受任何枚举数据类型。
anyrange表示函数接受任何范围数据类型。
anymultirange表示函数接受任何多范围数据类型。
anycompatible表示函数接受任何数据类型,并自动将多个参数提升为公共兼容数据类型。
anycompatiblearray接受任何数组,并自动提升为公共兼容数组类型。
anycompatiblenonarray接受任何非数组,并自动提升为公共兼容类型。
anycompatiblerange接受任何范围类型,并自动提升为公共兼容范围类型。
anycompatiblemultirange接受任何多范围类型,并自动提升为公共兼容多范围类型。
cstring表示函数接受或返回以空字符结尾的 C 字符串(主要用于底层 I/O 函数)。
internal表示函数接受或返回服务器内部数据类型(外部无法直接调用)。
fdw_handler声明一个外部数据包装器 (FDW) 处理程序的返回类型。
table_am_handler声明一个表访问方法处理程序的返回类型。
index_am_handler声明一个索引访问方法处理程序的返回类型。
tsm_handler声明一个表样本方法 (TABLESAMPLE) 处理程序的返回类型。
record标识一个函数接受或返回未指定的行类型(动态结果集)。
trigger触发器函数专属的返回类型。
event_trigger事件触发器函数专属的返回类型。
ux_ddl_command标识 DDL 命令的内部表示形式(常用于事件触发器)。
void表示函数不返回任何值
unknown标识尚未解析的类型(例如带单引号但未指定类型的字符串字面量)。

用C编写的函数(无论是内置的还是动态加载的)可以声明接受或返回任何这些伪类型。函数作者需要确保在使用伪类型作为参数类型时,函数将安全地运行。

使用过程语言编写的函数只能按照其实现语言允许的方式使用伪类型。目前,大多数过程语言禁止将伪类型用作参数类型,并仅允许voidrecord作为结果类型(加上triggerevent_trigger当函数用作触发器或事件触发器时)。一些还支持使用多态伪类型的多态函数,这些伪类型如上所示。

internal伪类型用于声明仅由数据库系统内部调用的函数,而不是通过SQL查询直接调用。如果一个函数至少有一个internal类型参数,则无法从SQL调用该函数。为了保持此限制的类型安全性,遵循以下编码规则很重要:不要创建声明返回internal的任何函数,除非它至少有一个internal参数。

5.22.集合类型

UXDB允许用户创建两种不同的集合类型:可变数组和嵌套表。

5.22.1.集合类型的定义

集合类型可以使用CREATE TYPE命令创建。

定义可变数组类型,如下所示。

cREATE TYPE varray_type IS VARRAY(10) OF INT;

定义嵌套表类型,如下所示。

cREATE TYPE nesttable_type AS TABLE OF INT;

在定义可变数组类型时,虽然可以指定可变数组元素个数的大小,但是实际上,UXDB不会对其进行限制。

5.22.2.集合类型的使用

当前集合类型无法作为一个表的列的数据类型使用,也无法使用ALTER TYPE 命令来修改已有的集合类型。

集合类型主要是在存储过程中使用,比如作为函数的入参或者是出参类型,亦或者是作为变量的数据类型使用。

cREATE OR REPLACE FUNCTION test_func1 (a INT) RETURNS varray_type
AS $$
DECLARE
var varray_type := varray_type(a, a + 1);
BEGIN
RETURN var;
END; $$;

当使用集合类型作为类型名声明变量时,可以在变量声明或者声明后使用集合类型构造函数对变量进行初始化。如果没有初始化,变量的值为空。上面的例子,就是使用集合类型构造函数,为var变量进行初始化操作,执行之后,var变量中元素的值就分别为a的值和a+1的值。

UXDB中,没有初始化的集合变量也可以直接进行操作;集合类型的构造函数,目前只能在函数、存储过程及匿名块中使用。

注意

集合类型在函数、存储过程及匿名块中具体使用方法,可以参考PL/UXSQL或PL/SQL中集合类型的使用说明。

5.22.3.集合类型元素操作

集合变量声明后,可通过下标访问集合元素,或者对集合元素进行赋值修改。比如下面的例子输出集合变量下标为1的元素,并修改它的值。

dO $$
DECLARE
a nesttable_type := nesttable_type(1, 2, 3);
BEGIN
RAISE NOTICE '%', a(1);
a(1) := 5;
RAISE NOTICE '%', a(1);
END; $$;

5.22.4.集合类型输出语法

一个集合类型值的外部文本表现由根据集合元素类型的I/O转换规则解释的项构成,并在其上加上修饰用于指示集合类型结构。修饰包括集合类型值周围的括号‘(’和‘)’以及相邻项之间的定界字符‘,’。如下所示。

SELECT test_func1(2);
    TEST_FUNC1     
-------------------
 VARRAY_TYPE(2, 3)
(1 row)

如果元素值是空字符串、包含花括号、包含定界字符、包含双引号、包含反斜线、包含空白或者匹配词NULL,数组输出例程将在元素值周围放上双引号。嵌在元素值中的双引号以及反斜线将被反斜线转义。对于数字数据类型可以安全地假设双引号绝不会出现,但是对于文本数据类型我们必须准备好处理可能出现亦可能不出现的引号。

6.类型转换

SQL语句可以有意或无意地要求在同一表达式中混合不同的数据类型。UXDB具有广泛的功能,用于评估混合类型表达式。

在许多情况下,用户不需要理解类型转换机制的细节。但是,UXDB执行的隐式转换可能会影响查询结果。必要时,可以使用显式类型转换来调整这些结果。

本章介绍了UXDB的类型转换机制和约定。有关特定数据类型和允许的函数和运算符的更多信息,请参见数据类型

6.1.概述

SQL 是一种强类型语言。也就是说,每个数据项都有一个关联的数据类型,它决定了它的行为和允许的使用方式。UXDB具有一个可扩展的类型系统,比其他 SQL 实现更通用和灵活。因此,UXDB中的大多数类型转换行为都受到通用规则的控制,而不是由特定的启发式算法控制。这允许使用混合类型表达式,即使使用了用户定义的类型也是如此。

UXDB的扫描器/解析器将词法元素分为五个基本类别:整数、非整数数字、字符串、标识符和关键字。大多数非数字类型的常量首先被分类为字符串。SQL 语言定义允许使用字符串指定类型名称,这种机制可以在 UXDB 中用于启动解析器沿着正确的路径进行解析。例如,查询:

SELECT text 'Origin' AS "label", point '(0,0)' AS "value";
label  | value
--------+-------
Origin | (0,0)

(1 row)

有两个文字常量,类型分别为 textpoint。如果没有为字符串文字指定类型,则初始分配占位符类型unknown,稍后将在后续阶段解决,如下所述。

在 UXDB 解析器中,有四个基本的 SQL 构造需要不同的类型转换规则:

  • 函数调用
    UXDB 的大部分类型系统都建立在丰富的函数集合之上。函数可以有一个或多个参数。由于 UXDB允许函数重载,因此仅凭函数名无法唯一标识要调用的函数;解析器必须根据提供的参数的数据类型选择正确的函数。

  • 运算符
    UXDB允许使用前缀(单参数)运算符和中缀(双参数)运算符的表达式。与函数一样,运算符也可以重载,因此存在选择正确运算符的相同问题。

  • 值存储
    SQL INSERTUPDATE 语句将表达式的结果存储到表中。语句中的表达式必须与目标列的类型匹配,并可能进行转换。

  • UNIONCASE 和相关结构
    由于联合的 SELECT 语句的所有查询结果必须出现在单个列集中,因此每个 SELECT子句的结果类型必须匹配并转换为统一的集合。类似地,CASE结构的结果表达式必须转换为公共类型,以便整个 CASE 表达式具有已知的输出类型。一些其他结构,例如 ARRAY[]GREATESTLEAST 函数,同样需要确定几个子表达式的公共类型。

系统目录存储有关哪些数据类型之间存在哪些转换或强制类型转换以及如何执行这些转换的信息。用户可以使用CREATE CAST命令添加其他强制类型转换。(通常与定义新数据类型一起完成。内置类型之间的强制类型转换集已经经过精心制作,最好不要更改。)

解析器提供的另一个启发式方法允许在具有隐式转换的类型组之间改进适当的转换行为的确定。数据类型分为几个基本的类型类别,包括booleannumericstringbitstringdatetimetimespangeometricnetwork和用户定义的类型。但请注意,还可以创建自定义类型类别。)在每个类别中,可以有一个或多个首选类型,当有多个可能的类型选择时,它们是首选的。通过精心选择首选类型和可用的隐式转换,可以确保可以以有用的方式解决模糊的表达式(具有多个候选解析解决方案)。

所有类型转换规则都考虑了几个原则:

  • 隐式转换不应具有令人惊讶或不可预测的结果。

  • 如果查询不需要隐式类型转换,则解析器或执行器中不应有额外的开销。也就是说,如果查询格式良好且类型已经匹配,则查询应该在解析器中不花费额外的时间,并且在查询中不引入不必要的隐式转换调用。

  • 此外,如果查询通常需要函数的隐式转换,并且如果用户定义了具有正确参数类型的新函数,则解析器应使用此新函数,不再进行隐式转换以使用旧函数。

6.2.运算符

通过以下过程确定操作符表达式引用的特定操作符。请注意,此过程受操作符优先级的间接影响,因为这将确定哪些子表达式被视为哪些操作符的输入。有关更多信息,请参见词法结构

操作符类型解析

  1. ux_operator系统目录中选择要考虑的操作符。如果使用了非模式限定的操作符名称(通常情况下),则考虑的操作符是在当前搜索路径中可见的具有匹配名称和参数计数的操作符。如果给出了限定的操作符名称,则仅考虑指定模式中的操作符。

    a. 如果搜索路径找到具有相同参数类型的多个操作符,则仅考虑路径中最早出现的操作符。无论搜索路径位置如何,具有不同参数类型的操作符都被视为平等。

  2. 检查是否存在接受精确输入参数类型的操作符。如果存在(在考虑的操作符集中只能有一个精确匹配),则使用它。缺少精确匹配会在通过限定名称(不典型)调用任何在允许不受信任的用户创建对象的模式中创建安全隐患。在这种情况下,强制转换参数以强制进行精确匹配。

    a. 如果二元操作符调用的一个参数是unknown类型,则假定它与另一个参数具有相同的类型进行此检查。涉及两个unknown输入或具有unknown输入的前缀操作符将永远无法在此步骤中找到匹配项。

    b. 如果二元运算符调用的一个参数是unknown类型,另一个参数是域类型,则接下来检查是否有一个运算符在两侧都接受域的基本类型;如果有,则使用它。

  3. 寻找最佳匹配。

    a. 丢弃输入类型不匹配且不能转换(使用隐式转换)以匹配的候选运算符。对于此目的,unknown字面量被认为是可转换为任何类型的。如果只剩下一个候选项,则使用它;否则继续下一步。

    b. 如果任何输入参数是域类型,则将其视为所有后续步骤中的域基本类型。这确保了域在模糊运算符解析的目的下像其基本类型一样。

    c. 运行所有候选项并保留那些在输入类型上具有最精确匹配的候选项。如果没有候选项具有精确匹配,则保留所有候选项。如果只剩下一个候选项,则使用它;否则继续下一步。

    d. 运行所有候选项并保留那些在需要类型转换的位置上接受首选类型(输入数据类型的类型类别)的候选项。如果没有候选项接受首选类型,则保留所有候选项。如果只剩下一个候选项,则使用它;否则继续下一步。

    e. 如果任何输入参数是unknown,则检查剩余候选项在这些参数位置接受的类型类别。在每个位置上,如果任何候选项接受该类别,则选择string类别(这种偏向字符串是适当的,因为未知类型字面量看起来像字符串)。否则,如果所有剩余候选项接受相同的类型类别,则选择该类别;否则失败,因为在没有更多线索的情况下无法推断正确的选择。现在丢弃不接受所选类型类别的候选项。此外,如果任何候选项在该类别中接受首选类型,则丢弃接受该参数的非首选类型的候选项。如果没有候选项通过这些测试,则保留所有候选项。如果只剩下一个候选项,则使用它;否则继续下一步。

    f. 如果既有unknown又有已知类型的参数,并且所有已知类型的参数具有相同的类型,则假定unknown参数也是该类型,并检查哪些候选项可以在unknown参数位置接受该类型。如果只有一个候选项通过了此测试,则使用它。否则失败。

以下是一些示例。

示例 平方根运算符类型解析

标准目录中只定义了一个平方根运算符(前缀|/),它接受double precision类型的参数。在此查询表达式中,扫描器将参数的初始类型设置为integer

SELECT | / 40 AS "square root of 40";

square root of 40
-------------------
6.324555320336759
(1 row)

因此,解析器对操作数进行了类型转换,查询等效于:

SELECT |/ CAST(40 AS double precision) AS "square root of 40";

示例  字符串连接运算符类型解析

使用类似字符串的语法处理字符串类型和复杂扩展类型。未指定类型的字符串与可能的运算符候选项匹配。

一个带有一个未指定参数的例子:

SELECT text 'abc' || 'def' AS "text and unknown";

text and unknown
------------------
abcdef
(1 row)

在这种情况下,解析器会查看是否有一个操作符接受text作为两个参数。由于有这样的操作符,它假定第二个参数应该被解释为text类型。

这是两个未指定类型的值的连接:

SELECT 'abc' || 'def' AS "unspecified";

unspecified
-------------
abcdef
(1 row)

在这种情况下,没有初始提示要使用哪种类型,因为查询中没有指定任何类型。因此,解析器查找所有候选操作符,并发现有候选操作符接受字符串类别和位字符串类别的输入。由于字符串类别可用时优先选择该类别,因此选择该类别,然后选择字符串的首选类型text作为特定类型来解析未知类型的字面量。

示例绝对值和否定操作符类型解析

UXDB操作符目录中有几个前缀操作符@的条目,它们都实现了各种数值数据类型的绝对值操作。其中之一是float8类型的条目,这是数值类别中的首选类型。因此,当面对未知输入时,UXDB将使用该条目:

SELECT @ '-4.5' AS "abs";

abs
-----
4.5
(1 row)

在这里,系统在应用所选操作符之前隐式地将未知类型的字面量解析为float8类型。我们可以验证使用的是float8而不是其他类型:

SELECT @ '-4.5e500' AS "abs";

ERROR:  "-4.5e500" is out of range for type double precision

另一方面,前缀操作符~(按位否定)仅针对整数数据类型定义,而不针对float8定义。因此,如果我们尝试使用类似的情况进行~操作,我们会得到:

SELECT ~ '20' AS "negation";
ERROR:  operator is not unique: ~ "unknown"
HINT:  Could not choose a best candidate operator. You might need to add
explicit type casts.

这是因为系统无法决定应该优先选择哪个多个可能的~操作符之一。我们可以通过显式转换来帮助它:

SELECT ~ CAST('20' AS int8) AS "negation";
negation
----------
-21
(1 row)

示例 数组包含操作符类型解析

这是另一个解析一个已知和一个未知输入的操作符的例子:

SELECT array[1,2] <@ '{1,2,3}' as "is subset";
is subset
-----------
t
(1 row)
```sql
uXDB操作符目录中有几个中缀操作符`<@`的条目,但唯一可能接受左侧为整数数组的两个条目是数组包含(`anyarray` `<@``anyarray`)和范围包含(`anyelement` `<@``anyrange`)。由于这些多态伪类型都不被认为是首选类型,因此解析器无法确定哪个操作符应该被优先选择。解决这种歧义的基础是告诉它假设未知类型的文字与其他输入的类型相同,即整数数组。现在只有两个运算符中的一个可以匹配,因此选择数组包含。(如果选择范围包含,我们将会得到一个错误,因为字符串没有正确的格式成为范围文字。)

**示例 自定义域类型上的运算符**

用户有时会尝试声明仅适用于域类型的运算符。这是可能的,但并不像看起来那么有用,因为运算符解析规则旨在选择适用于域的基本类型的运算符。例如,请考虑
```sql
CREATE DOMAIN mytext AS text CHECK(...);
CREATE FUNCTION mytext_eq_text (mytext, text) RETURNS boolean AS ...;
CREATE OPERATOR = (procedure=mytext_eq_text, leftarg=mytext, rightarg=text);
CREATE TABLE mytable (val mytext);

SELECT * FROM mytable WHERE val = 'foo';

此查询将不使用自定义运算符。解析器首先会查看是否存在 mytext = mytext 运算符 ,但实际上不存在;然后它将考虑域的基本类型 text,并查看是否存在 text = text 运算符 ,实际上是存在的; 因此,它将unknown 类型的文字解析为 text,并使用 text = text 运算符。唯一使用自定义运算符的方法是显式转换文字:

SELECT * FROM mytable WHERE val = text 'foo';

因此,根据精确匹配规则,立即找到 mytext = text运算符。如果达到了最佳匹配规则,则它们会积极歧视域类型上的运算符。如果不这样做,这样的运算符将会创建太多的模糊运算符失败,因为转换规则始终将域视为可转换为其基本类型或从其基本类型转换,因此域运算符将被认为可在与基本类型上同名的运算符中使用的所有情况下使用。

6.3.函数

通过以下过程确定函数调用引用的特定函数。

函数类型解析

  1. ux_proc系统目录中选择要考虑的函数。如果使用非模式限定的函数名称,则考虑与当前搜索路径中可见的具有匹配名称和参数计数的函数。如果给出了限定函数名称,则仅考虑指定模式中的函数。

    a. 如果搜索路径找到多个具有相同参数类型的函数,则仅考虑路径中最早出现的函数。不考虑搜索路径位置,不同参数类型的函数被视为平等。

    b. 如果一个函数声明了一个VARIADIC数组参数,并且调用没有使用VARIADIC关键字,则该函数被视为数组参数被替换为其元素类型的一个或多个出现,以便匹配调用。在这种扩展后,函数可能具有与某些非可变函数相同的有效参数类型。在这种情况下,使用搜索路径中出现的函数,或者如果两个函数在同一个模式中,则首选非可变函数。

    当通过限定名称调用在允许不受信任的用户创建对象的模式中找到的可变函数时,会产生安全隐患。恶意用户可以控制并执行任意SQL函数,就像您执行它们一样。替换一个带有VARIADIC关键字的调用,可以绕过此隐患。填充VARIADIC "any"参数的调用通常没有包含VARIADIC关键字的等效公式。为了安全地发出这些调用,函数的模式必须只允许受信任的用户创建对象。

    c. 具有参数默认值的函数被认为与省略一个或多个可默认参数位置的任何调用匹配。如果有多个这样的函数匹配一个调用,则使用搜索路径中最早出现的函数。如果在相同模式中有两个或更多具有非默认位置中相同参数类型的这样的函数(如果它们具有不同的可默认参数集,则可能出现这种情况),则系统将无法确定哪个函数更好,因此如果找不到更好的匹配项,则会导致“模棱两可的函数调用”错误。

    这在通过限定名称调用任何在允许不受信任的用户创建对象的模式中找到的函数时会创建可用性风险。恶意用户可以创建一个具有现有函数名称的函数,复制该函数的参数并附加具有默认值的新参数。这将排除对原始函数的新调用。为了防止这种风险,将函数放置在仅允许受信任的用户创建对象的模式中。

  2. 检查是否存在接受恰好输入参数类型的函数。如果存在(在考虑的函数集中只能有一个完全匹配),则使用它。缺少精确匹配会在通过限定名称调用在允许不受信任的用户创建对象的模式中找到的函数时创建安全风险。在这种情况下,将参数强制转换以强制进行精确匹配。(涉及unknown的情况将永远不会在此步骤中找到匹配项。)

  3. 如果没有找到精确匹配,请查看函数调用是否为特殊类型转换请求。如果函数调用只有一个参数,并且函数名称与某个数据类型的(内部)名称相同,则会发生这种情况。此外,函数参数必须是未知类型文字,或者是可以通过二进制强制转换为命名数据类型的类型,或者是可以通过应用该类型的I/O函数将其转换为命名数据类型的类型(即,转换是标准字符串类型之一)。当满足这些条件时,函数调用将被视为CAST规范的形式。

  4. 寻找最佳匹配项。

    a. 丢弃输入类型不匹配且无法转换(使用隐式转换)以匹配的候选函数。对于此目的,假定unknown文字可转换为任何内容。如果只剩下一个候选项,请使用它;否则继续下一步。

    b. 如果任何输入参数是域类型,请将其视为所有后续步骤的域基本类型。这确保域在模糊函数解析的目的上像其基本类型一样。

    c. 运行所有候选项并保留在输入类型上具有最精确匹配的候选项。如果没有精确匹配,则保留所有候选项。如果只剩下一个候选项,请使用它;否则继续下一步。

    d. 运行所有候选项并保留在需要进行类型转换的大多数位置上接受首选类型(输入数据类型的类型类别)的候选项。如果没有接受首选类型的候选项,则保留所有候选项。如果只剩下一个候选项,请使用它;否则继续下一步。

    e. 如果任何输入参数是unknown,请检查接受的类型类别在剩余的候选人中,对于每个参数位置,如果有任何候选人接受该类别,则选择string类别。(这种偏向字符串的方式是合适的,因为未知类型的文字看起来像字符串。)否则,如果所有剩余的候选人都接受相同的类型类别,则选择该类别;否则失败,因为没有更多线索无法推断出正确的选择。现在丢弃不接受所选类型类别的候选人。此外,如果任何候选人接受该类别中的首选类型,则丢弃接受该类别中的非首选类型的参数的候选人。如果没有候选人通过这些测试,则保留所有候选人。如果只剩下一个候选人,则使用它;否则继续下一步。

    f. 如果既有 unknown 类型的参数又有已知类型的参数,并且所有已知类型的参数都具有相同的类型,则假定 unknown参数也是该类型,并检查哪些候选人可以在 unknown参数位置接受该类型。如果只有一个候选人通过了此测试,则使用它。否则失败。

请注意,运算符和函数类型解析的“最佳匹配”规则是相同的。以下是一些示例。

示例 四舍五入函数参数类型解析

只有一个 round 函数接受两个参数;它接受一个类型为 numeric 的第一个参数和一个类型为 integer的第二个参数。因此,以下查询会自动将类型为 integer 的第一个参数转换为 numeric

SELECT round(4, 4);
round
--------
4.0000
(1 row)

该查询实际上被解析器转换为:

SELECT round(CAST (4 AS numeric), 4);

由于带小数点的数字常量最初被分配为类型 numeric,因此以下查询不需要类型转换,因此可能稍微更有效:

SELECT round(4.0, 4);

示例 可变参数函数解析

CREATE FUNCTION public.variadic_example(VARIADIC numeric[]) RETURNS int
  LANGUAGE sql AS 'SELECT 1';
CREATE FUNCTION

该函数接受但不需要 VARIADIC 关键字。它容忍整数和数字参数:

SELECT public.variadic_example(0),
       public.variadic_example(0.0),
       public.variadic_example(VARIADIC array[0.0]);
 variadic_example | variadic_example | variadic_example
------------------+------------------+------------------
                1 |                1 |                1
(1 row)

但是,如果可用,第一个和第二个调用将更喜欢更具体的函数:

CREATE FUNCTION public.variadic_example(numeric) RETURNS int
  LANGUAGE sql AS 'SELECT 2';
CREATE FUNCTION

CREATE FUNCTION public.variadic_example(int) RETURNS int
  LANGUAGE sql AS 'SELECT 3';
CREATE FUNCTION

SELECT public.variadic_example(0),
       public.variadic_example(0.0),
       public.variadic_example(VARIADIC array[0.0]);
 variadic_example | variadic_example | variadic_example
------------------+------------------+------------------
                3 |                2 |                1
(1 row)

在默认配置下,只有第一个函数存在时,第一个和第二个调用是不安全的。任何用户都可以通过创建第二个或第三个函数来拦截它们。通过精确匹配参数类型,可以避免这种情况。并且使用VARIADIC关键字,第三个调用是安全的。

示例 子字符串函数类型解析

有几个substr函数,其中一个 采用类型textinteger。如果调用使用未指定类型的字符串常量,则系统选择接受首选类别参数的候选函数 string(即类型text)。

SELECT substr('1234', 3);

 substr
--------
     34
(1 row)

如果字符串声明为varchar类型,这可能是情况 如果它来自表,则解析器将尝试将其转换为text

SELECT substr(varchar '1234', 3);

 substr
--------
     34
(1 row)

这由解析器转换为有效地变为:

SELECT substr(CAST (varchar '1234' AS text), 3);

注意

解析器从ux_cast目录中学习到 textvarchar 是二进制兼容的,这意味着可以将一个传递给函数接受另一个而不进行任何物理转换。因此,没有 在这种情况下真正插入类型转换调用。

如果使用integer类型的参数调用函数,解析器将尝试将其转换为text

SELECT substr(1234, 3);
ERROR:  function substr(integer, integer) does not exist
HINT:  No function matches the given name and argument types. You might need
to add explicit type casts.

这不起作用,因为integer没有隐式转换 到text。但是,显式转换将起作用:

SELECT substr(CAST (1234 AS text), 3);

 substr
--------
     34
(1 row)

如果使用非模式限定名称,则不会出现危险,包含允许不受信任的用户创建的模式的搜索路径对象不是安全模式使用模式。

这一步的原因是支持函数样式的转换规范在没有实际的转换函数的情况下。如果有一个铸造函数,它的命名惯例是以其输出类型命名的,因此没有必要有特殊情况。看看CREATE CAST有关其他评论。

6.4.值存储

要插入表中的值将根据以下步骤转换为目标列的数据类型。

值存储类型转换

  1. 检查是否与目标完全匹配。

  2. 否则,尝试将表达式转换为目标类型。如果在 ux_cast 目录中注册了两种类型之间的赋值转换,则可以进行转换。或者,如果表达式是未知类型的文字,将文字字符串的内容提供给目标类型的输入转换例程。

  3. 检查目标类型是否有大小调整转换。大小调整转换是从该类型到它本身的转换。如果在 ux_cast目录中找到一个,将其应用于存储到目标列之前的表达式。这种转换的实现函数始终带有一个额外的类型为integer 的参数,该参数接收目标列的 atttypmod值(通常是它声明的长度,尽管对于不同的数据类型,atttypmod的解释有所不同),它可能带有第三个 boolean参数,指示转换是显式还是隐式的。转换函数负责应用任何长度相关的语义,例如大小检查或截断。

示例 character 存储类型转换

对于声明为 character(20) 的目标列,以下语句显示存储的值已正确调整大小:

CREATE TABLE vv (v character(20));
INSERT INTO vv SELECT 'abc' || 'def';
SELECT v, octet_length(v) FROM vv;

          v           | octet_length
----------------------+--------------
 abcdef               |           20
(1 row)

默认情况下,text类型的数据会被转换为bpchar类型的数据(“blank-padded char”,即character数据类型的内部名称),以匹配目标列的数据类型。这样做是为了让||操作符能够被解析为text的连接操作。由于从textbpchar的转换是二进制可强制转换的,因此这种转换不会插入任何真正的函数调用。最后,在系统目录中找到并应用大小调整函数bpchar(bpchar, integer, boolean),将其应用于操作符的结果和存储的列长度。这种类型特定的函数执行所需的长度检查和填充空格的操作。

6.5.UNION, CASE和相关结构

SQL 的 UNION 结构必须将可能不同的类型匹配为单个结果集。解析算法分别应用于联合查询的每个输出列。INTERSECTEXCEPT 结构以与 UNION 相同的方式解析不同的类型。一些其他结构,包括 CASEARRAYVALUESGREATESTLEAST 函数,使用相同的算法来匹配它们的组成表达式并选择结果数据类型。

UNIONCASE 和相关结构的类型解析

  1. 如果所有的输入为相同类型,并且不是unknown类型,那么就决定是该类型。

  2. 如果任何输入是一种域类型,在所有后续步骤中都把它当做该域的基类型。

  3. 如果所有的输入为unknown类型,则决定为text(字符串分类的首选类型)类型。

  4. 如果出现非unknown类型的话,选择第一个在其分类中作为首选类型的非unknown类型。

  5. 对于union中的特殊值NULLunknown类型的处理。

    exp2 exp1 unknown null::type(对null进行进行类型转换) null
    null text type text
    unknown text character varying text
    null::type(对null进行进行类型转换) varchar(当type为blob、clob、text类型为text) 随转换规则转化 type(当type为blob、clob、text类型为text)
  6. 可以实现不同的类型连接,在连接时内部进行对应的转换,返回类型为最终转换得到的类型。

  7. 转换所有的输入为最终转换的类型。如果没有一个从给定输入到选定类型的转换将会失败,返回无法转换的报错处理。

以下是一些示例。

示例 在联合中解析未指定类型的类型

SELECT text 'a' AS "text" UNION SELECT 'b';
 text
------
 a
 b
(2 rows)

在这里,未知类型的字面值'b'将被解析为类型text

示例 在简单联合中解析类型

SELECT 1.2 AS "numeric" UNION SELECT 1;

 numeric
---------
       1
     1.2
(2 rows)

文字1.2numeric类型,且integer1可以被造型为numeric,因此使用numeric类型。

示例 在转置联合中解析类型

SELECT 1 AS "real" UNION SELECT CAST('2.2' AS REAL);
 real
------
    1
  2.2
(2 rows)

这里,由于类型real被能被造型为integer,而integer可以被造型为real,联合结果类型被决定为real

示例 在嵌套联合中解析类型

SELECT NULL AS TEST UNION SELECT NULL UNION SELECT 1;
 test
------
 1

(2 rows)

这里因为UXDB将多个union当作是成对操作的嵌套,也就是说上面的输入等同于:(SELECT NULL UNION SELECT NULL) UNION SELECT 1; 根据上面给定的规则,内层的UNION被确定为类型text。然后外层的UNION的输入是类型text和integer,UXDB中包含对应的转换规则,所以可以正常进行返回操作。

但是当不存在对应的转换规则时,就会转换失败返回无法转换的报错。

就比如:

SELECT '22:02:20'::TIME AS TEST UNION SELECT '2022-2-2'::DATE UNION SELECT 1;
ERROR:  UNION/INTERSECT/EXCEPT could not convert type time without time zone to date

这个失败的原因就是因为不存在日期类型和不包含时区的时间类型之间的转换规则。

有点像操作符和函数的域输入的处理方式,这种行为允许保留域类型通过UNION或类似的结构,只要用户小心确保所有输入隐式或显式地是该确切类型。否则将使用域的基本类型。

由于历史原因,CASE 将其(如果有的话)ELSE子句视为第一个输入,然后考虑 THEN 子句。在所有其他情况下,“从左到右”表示表达式在查询文本中出现的顺序。

6.6.SELECT 输出列

在前面的章节中给出的规则将为 SQL 查询中的所有表达式分配非 unknown数据类型,但对于作为简单输出列的未指定类型字面量,SELECT命令将会采用回退方式来解析字面量的类型为 text。例如,在以下查询中:

SELECT 'Hello World';

没有任何东西可以确定字符串字面量应该被视为什么类型。在这种情况下,UXDB 将采用回退方式将字面量的类型解析为 text

SELECTUNION(或 INTERSECTEXCEPT)结构的一部分,或者出现在 INSERT ...SELECT 中时,不会应用此规则,因为前面章节中给出的规则优先。在第一种情况下,未指定类型字面量的类型可以从另一个 UNION分支中获取,在第二种情况下,可以从目标列中获取。

对于此目的,RETURNING 列表与 SELECT 输出列表的处理方式相同。

注解

SELECT 输出列表中的未指定类型字面量被保留为类型unknown。这会带来各种不良后果,因此已经进行了更改。

7.大对象

UXDB 提供了一种大对象设施,可为存储在特殊大对象结构中的用户数据提供流式访问。流式访问在处理无法方便地作为整体操作的数据值时非常有用。

本章介绍了 UXDB 大对象数据的实现、编程和查询语言接口。本章的示例使用 C 库 libuxsql,但大多数编程语言都提供了相应的接口。

与UXDB原生接口支持相同的功能。其他接口可能会在内部使用大对象接口来提供大值的通用支持。这里不进行描述。

7.1.介绍

所有大对象都存储在一个名为ux_largeobject的系统表中。每个大对象还在系统表ux_largeobject_metadata中有一个条目。可以使用类似于对文件的标准操作的读/写 API 创建、修改和删除大对象。

UXDB还支持一种称为“TOAST”的存储系统,它会自动将大于单个数据库页面的值存储到每个表的二级存储区域中。这使得大对象设施部分过时。大对象设施的一个剩余优势是它允许值达到4TB的大小,而TOAST字段最多只能为1 GB。此外,可以有效地读取和更新大对象的部分,而对TOAST字段的大多数操作将作为一个单元读取或写入整个值。

7.2.实现特性

大对象实现将大对象分成“块”,并将这些块存储在数据库的行中。B树索引保证了在进行随机访问读写时快速搜索正确的块号。

存储大对象的块不必是连续的。例如,如果应用程序打开一个新的大对象,寻找偏移量1000000,并在那里写入一些字节,这不会导致分配1000000字节的存储空间;只有覆盖实际写入的数据字节范围的块才会被分配。然而,读取操作将为任何未分配的位置读取零,这对应于Unix文件系统中“稀疏分配”的常见行为。

大对象具有所有者和一组访问权限,可以使用GRANTREVOKE进行管理。读取大对象需要SELECT权限,写入或截断大对象需要UPDATE权限。只有大对象的所有者(或数据库超级用户)才能删除、评论或更改大对象的所有者。要调整此行为以与先前的版本兼容,请参见lo_compat_privileges运行时参数。

7.3.客户端接口

本节介绍了UXDB的libuxsql客户端接口库提供的访问大对象的功能。UXDB大对象接口是基于Unix文件系统接口建模的,具有openreadwritelseek等类似函数。

使用这些函数进行所有大对象操作必须在SQL事务块内进行,因为大对象文件描述符仅在事务的持续时间内有效。

如果在执行任何一个这些函数时发生错误,则该函数将返回一个通常为0或-1的不可能值。描述错误的消息存储在连接对象中,并可使用UXSQLerrorMessage检索。

使用这些函数的客户端应用程序应该包含头文件libuxsql/libuxsql-fs.h并链接libuxsql库。

当libuxsql连接处于管道模式时,客户端应用程序不能使用这些函数。

7.3.1.创建大型对象

函数

Oid lo_create(UXconn *conn, Oid lobjId);

创建一个新的大型对象。可以通过lobjId指定要分配的OID;如果指定了,则如果该OID已经用于某个大型对象,则失败。如果lobjIdInvalidOid(零),则lo_create分配一个未使用的OID。返回值是分配给新大型对象的OID,如果失败则为InvalidOid(零)。

例如:

inv_oid = lo_create(conn, desired_oid);

旧的函数

Oid lo_creat(UXconn *conn, int mode);

也创建一个新的大型对象,总是分配一个未使用的OID。返回值是分配给新大型对象的OID,如果失败则为InvalidOid(零)。

在UXDB中,忽略mode,因此lo_creat与零第二个参数的lo_create完全等效。要使用这样旧的服务器,必须使用lo_creat而不是lo_create,并且必须将mode设置为INV_READINV_WRITEINV_READ | INV_WRITE之一。(这些符号常量在头文件libuxsql/libuxsql-fs.h中定义。)

例如:

inv_oid = lo_creat(conn, INV_READ|INV_WRITE);

7.3.2.导入大型对象

要将操作系统文件导入为大型对象,请调用

Oid lo_import(UXconn *conn, const char *filename);

filename指定要导入为大型对象的操作系统文件名。返回值是分配给新大型对象的OID,如果失败则为InvalidOid(零)。请注意,文件由客户端接口库读取,而不是由服务器读取;因此,它必须存在于客户端文件系统中,并且客户端应用程序可以读取它。

函数

Oid lo_import_with_oid(UXconn *conn, const char *filename, Oid lobjId);

也导入一个新的大型对象。可以通过lobjId指定要分配的OID;如果指定了,则如果该OID已经用于某个大型对象,则失败。如果lobjIdInvalidOid(零),则lo_import_with_oid分配一个未使用的OID(这与lo_import的行为相同)。返回值是分配给新大型对象的OID,如果失败则为InvalidOid(零)。

7.3.3.导出大型对象

要将大型对象导出到操作系统文件中,请调用

int lo_export(UXconn *conn, Oid lobjId, const char *filename);

lobjId 参数指定要导出的大型对象的 OID,filename参数指定操作系统文件的名称。请注意,该文件是由客户端接口库而不是服务器编写的。成功时返回1,失败时返回 -1。

7.3.4.打开现有的大型对象

要打开现有的大型对象以进行读取或写入,请调用

int lo_open(UXconn *conn, Oid lobjId, int mode);

lobjId 参数指定要打开的大型对象的 OID。mode位控制对象是以读取(INV_READ)、写入(INV_WRITE)还是两者都打开。(这些符号常量在头文件libuxsql/libuxsql-fs.h 中定义。)lo_open 返回一个(非负的)大型对象描述符,以供稍后使用lo_readlo_writelo_lseeklo_lseek64lo_telllo_tell64lo_truncatelo_truncate64lo_close。该描述符仅在当前事务的持续时间内有效。失败时返回 -1。

服务器目前不区分模式 INV_WRITEINV_READ | INV_WRITE:您可以在任一情况下从描述符中读取。但是,这些模式和仅使用INV_READ 之间存在显着差异:使用 INV_READ,您无法在描述符上写入,并且从中读取的数据将反映在执行 lo_open时处于事务快照的大型对象的内容,而不管此后该事务或其他事务的写入。使用 INV_WRITE打开的描述符读取的数据将反映所有已提交事务的写入以及当前事务的写入。这类似于普通SQL SELECT 命令的 REPEATABLE READREAD COMMITTED 事务模式的行为。

如果大型对象没有 SELECT 权限,或者指定了 INV_WRITE 且没有 UPDATE 权限,则 lo_open将失败。(在 UXDB 2122之前,这些权限检查是在使用描述符的第一个实际读取或写入调用时执行的。)这些权限检查可以通过lo_compat_privileges运行时参数禁用。

一个例子:

inv_fd = lo_open(conn, inv_oid, INV_READ|INV_WRITE);

7.3.5.向大对象写入数据

函数

int lo_write(UXconn *conn, int fd, const char *buf, size_t len);

buf(必须是len大小)中写入len字节到大对象描述符fd。参数fd必须是之前lo_open返回的。实际写入的字节数将被返回(在当前实现中,这将始终等于len,除非出现错误)。如果出现错误,则返回值为-1。

虽然len参数声明为size_t,但此函数将拒绝大于INT_MAX的长度值。实际上,最好将数据分块传输,每个块最多几兆字节。

7.3.6.从大对象读取数据

函数

int lo_read(UXconn *conn, int fd, char *buf, size_t len);

从大对象描述符fd中读取最多len字节到buf(必须是len大小)。参数fd必须是之前lo_open返回的。实际读取的字节数将被返回;如果先到达大对象的末尾,则这将小于len。如果出现错误,则返回值为-1。

虽然len参数声明为size_t,但此函数将拒绝大于INT_MAX的长度值。实际上,最好将数据分块传输,每个块最多几兆字节。

7.3.7.在大对象中寻找

要更改与大对象描述符关联的当前读取或写入位置,请调用

int lo_lseek(UXconn *conn, int fd, int offset, int whence);

此函数将大对象描述符的当前位置指针移动到由offset指定的新位置。参数whence的有效值为SEEK_SET(从对象开头寻找),SEEK_CUR(从当前位置寻找)和SEEK_END(从对象末尾寻找)。返回值是新的位置指针,或者在出现错误时为-1。

当处理可能超过2GB大小的大对象时,应使用

ux_int64 lo_lseek64(UXconn *conn, int fd, ux_int64 offset, int whence);

此函数具有与lo_lseek相同的行为,但它可以接受大于2GB的 offset 和/或提供大于2GB的结果。请注意,如果新位置指针大于2GB,则lo_lseek将失败。

lo_lseek64 是新增的。如果该函数在旧版本的服务器上运行,它将会失败并返回 -1。

7.3.8.获取大物体的搜索位置

要获取大型对象描述符的当前读取或写入位置,请调用:

int lo_tell(UXconn *conn, int fd);

如果出现错误,则返回值为 -1。当处理可能超过 2GB 大小的大型对象时,请改用:

ux_int64 lo_tell64(UXconn *conn, int fd);

此函数与 lo_tell 具有相同的行为,但它可以提供大于 2GB 的结果。请注意,如果当前的读/写位置大于 2GB,则 lo_tell将失败。

7.3.9.截断大对象

要将大型对象截断为给定长度,请调用:

int lo_truncate(UXconn *conn, int fd, size_t len);

此函数将大型对象描述符 fd 截断为长度 lenfd 参数必须由先前的 lo_open 返回。如果len 大于大型对象的当前长度,则使用空字节('\0')将大型对象扩展到指定的长度。成功时,lo_truncate返回零。如果出现错误,则返回值为 -1。与描述符 fd 关联的读/写位置不会更改。尽管len 参数声明为 size_t,但 lo_truncate 将拒绝大于 INT_MAX 的长度值。当处理可能超过 2GB大小的大型对象时,请改用:

int lo_truncate64(UXconn *conn, int fd, ux_int64 len);

此函数与 lo_truncate 具有相同的行为,但它可以接受超过 2GB 的len 值。

7.3.10.关闭大对象描述符

可以通过调用以下函数来关闭大型对象描述符:

int lo_close(UXconn *conn, int fd);

其中fd是由 lo_open 返回的大型对象描述符。成功时,lo_close 返回零。如果出现错误,则返回值为 -1。

在事务结束时,任何仍然打开的大型对象描述符都将自动关闭。

7.3.11.删除大对象

要从数据库中删除大对象,请调用

 int lo_unlink(UXconn *conn, Oid lobjId); 

lobjId参数指定要删除的大对象的OID。如果成功,则返回1;如果失败,则返回-1。

7.4.服务器端函数

列在表 面向SQL的大对象函数中的是专门用于从SQL操作大对象的服务器端函数。

表 面向SQL的大对象函数

函数与参数描述示例
lo_from_bytea(loid oid, data bytea)创建并存储:创建一个大对象并将 data 存储其中。若 loid0,系统自动分配 OID;若指定 OID 且已存在则报错。返回大对象的 OID。lo_from_bytea(0, '\xffffff00')24528
lo_put(loid oid, offset bigint, data bytea)按位写入:从指定的 offset 偏移量开始,将 data 写入大对象。若写入位置超过当前长度,将自动扩大大对象。lo_put(24528, 1, '\xaa')void
lo_get(loid oid [, offset bigint, length int])提取内容:提取大对象完整内容或指定长度的子字符串。lo_get(24528, 0, 3)\xffaaff

每个客户端函数都有相应的服务器端函数;实际上,大多数客户端函数只是等效的服务器端函数的接口。其中一些通过SQL命令调用同样方便,如lo_creatlo_createlo_unlinklo_importlo_export。以下是它们的使用示例:

CREATE TABLE image (
name            text,
raster          oid
);

SELECT lo_creat(-1);       -- 返回新的空大对象的OID

SELECT lo_create(43213);   -- 尝试创建OID为43213的大对象

SELECT lo_unlink(173454);  -- 删除OID为173454的大对象

INSERT INTO image (name, raster)
VALUES ('beautiful image', lo_import('/etc/motd'));

INSERT INTO image (name, raster)  -- 与上面相同,但指定要使用的OID
VALUES ('beautiful image', lo_import('/etc/motd', 68583));

SELECT lo_export(image.raster, '/tmp/motd') FROM image
WHERE name = 'beautiful image';

服务器端的lo_importlo_export函数的行为与其客户端模拟函数有很大不同。这两个函数在服务器文件系统中读取和写入文件,使用数据库所属用户的权限。因此,默认情况下,它们的使用仅限于超级用户。相比之下,客户端的导入和导出函数在客户端文件系统中读取和写入文件,使用客户端程序的权限。客户端函数不需要任何数据库权限,除了读取或写入相关大对象的权限。

注意

可以将服务器端的lo_importlo_export函数的使用授权给非超级用户,但需要仔细考虑安全性影响。这些权限的恶意用户可以轻松地将其转化为超级用户(例如通过重写服务器配置文件),或者可以攻击服务器文件系统的其余部分,而无需获得数据库超级用户权限。因此,拥有此类权限的角色的访问权限必须像超级用户角色的访问权限一样受到严格保护。尽管如此,如果某个常规任务需要使用服务器端的lo_importlo_export,则使用具有此类权限的角色比使用具有完整超级用户权限的角色更安全,因为这有助于减少意外错误造成的风险。

lo_readlo_write的功能也可以通过服务器端调用实现,但是服务器端函数的名称与客户端接口不同,因为它们不包含下划线。您必须调用这些函数作为loreadlowrite

7.5.示例程序

示例 使用libuxsql的大型对象示例程序是一个演示如何使用libuxsql中的大型对象接口的示例程序。该程序的某些部分被注释掉,但为了读者的方便,源代码中保留了这些部分。该程序也可以在源代码分发中的src/test/examples/testlo.c中找到。

示例 使用libuxsql的大型对象示例程序

/*-----------------------------------------------------------------
*
* testlo.c
*    test using large objects with libuxsql
*
* Portions Copyright (c) 1996-2022, UXDB Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
*
* IDENTIFICATION
*    src/test/examples/testlo.c
*
*-----------------------------------------------------------------
*/
#include <stdio.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include "libuxsql-fe.h"
#include "libuxsql/libuxsql-fs.h"

#define BUFSIZE         1024

/*
* importFile -
*    import file "in_filename" into database as large object "lobjOid"
*
*/
static Oid
importFile(UXconn *conn, char *filename)
{
Oid         lobjId;
int         lobj_fd;
char        buf[BUFSIZE];
int         nbytes,
tmp;
int         fd;

/*
* open the file to be read in
*/
fd = open(filename, O_RDONLY, 0666);
if (fd < 0)
{                           /* error */
fprintf(stderr, "cannot open unix file\"%s\"\n", filename);
}

/*
* create the large object
*/
lobjId = lo_creat(conn, INV_READ | INV_WRITE);
if (lobjId == 0)
fprintf(stderr, "cannot create large object");

lobj_fd = lo_open(conn, lobjId, INV_WRITE);

/*
* read in from the Unix file and write to the inversion file
*/
while ((nbytes = read(fd, buf, BUFSIZE)) > 0)
{
tmp = lo_write(conn, lobj_fd, buf, nbytes);
if (tmp < nbytes)
fprintf(stderr, "error while reading \"%s\"", filename);
}

close(fd);
lo_close(conn, lobj_fd);

return lobjId;
}

static void
pickout(UXconn *conn, Oid lobjId, int start, int len)
{
int         lobj_fd;
char       *buf;
int         nbytes;
int         nread;

lobj_fd = lo_open(conn, lobjId, INV_READ);
if (lobj_fd < 0)
fprintf(stderr, "无法打开大对象 %u", lobjId);

lo_lseek(conn, lobj_fd, start, SEEK_SET);
buf = malloc(len + 1);

nread = 0;
while (len - nread > 0)
{
nbytes = lo_read(conn, lobj_fd, buf, len - nread);
buf[nbytes] = '\0';
fprintf(stderr, ">>> %s", buf);
nread += nbytes;
if (nbytes <= 0)
break;              /* 没有更多数据? */
}
free(buf);
fprintf(stderr, "\n");
lo_close(conn, lobj_fd);
}

static void
overwrite(UXconn *conn, Oid lobjId, int start, int len)
{
int         lobj_fd;
char       *buf;
int         nbytes;
int         nwritten;
int         i;

lobj_fd = lo_open(conn, lobjId, INV_WRITE);
if (lobj_fd < 0)
fprintf(stderr, "无法打开大对象 %u", lobjId);

lo_lseek(conn, lobj_fd, start, SEEK_SET);
buf = malloc(len + 1);

for (i = 0; i < len; i++)
buf[i] = 'X';
buf[i] = '\0';

nwritten = 0;
while (len - nwritten > 0)
{
nbytes = lo_write(conn, lobj_fd, buf + nwritten, len - nwritten);
nwritten += nbytes;
if (nbytes <= 0)
{
fprintf(stderr, "\n写入失败!\n");
break;
}
}
free(buf);
fprintf(stderr, "\n");
lo_close(conn, lobj_fd);
}


/*
* exportFile -
*    将大对象“lobjOid”导出到文件“out_filename”
*
*/
static void
exportFile(UXconn *conn, Oid lobjId, char *filename)
{
int         lobj_fd;
char        buf[BUFSIZE];
int         nbytes,
tmp;
int         fd;

/*
* 打开大对象
*/
lobj_fd = lo_open(conn, lobjId, INV_READ);
if (lobj_fd < 0)
fprintf(stderr, "无法打开大对象 %u", lobjId);

/*
* 打开要写入的文件
*/
fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{                           /* 错误 */
fprintf(stderr, "无法打开Unix文件\"%s\"",
filename);
}

/*
* 从反转文件中读取并写入Unix文件
*/
while ((nbytes = lo_read(conn, lobj_fd, buf, BUFSIZE)) > 0)
{
tmp = write(fd, buf, nbytes);
if (tmp < nbytes)
{
fprintf(stderr, "写入\"%s\"时出错",
filename);
}
}

lo_close(conn, lobj_fd);
close(fd);
}

static void
exit_nicely(UXconn *conn)
{
UXSQLfinish(conn);
exit(1);
}

int
main(int argc, char **argv)
{
char       *in_filename,
*out_filename;
char       *database;
Oid         lobjOid;
UXconn     *conn;
UXresult   *res;

if (argc != 4)
{
fprintf(stderr, "用法:%s 数据库名称 in_filename out_filename\n",
argv[0]);
exit(1);
}

database = argv[1];
in_filename = argv[2];
out_filename = argv[3];

/*
* 设置连接
*/
conn = UXSQLsetdb(NULL, NULL, NULL, NULL, database);

/* 检查是否成功建立后端连接 */
if (UXSQLstatus(conn) != CONNECTION_OK)
{
fprintf(stderr, "%s", UXSQLerrorMessage(conn));
exit_nicely(conn);
}

/* 设置始终安全的搜索路径,以防恶意用户控制。*/
res = UXSQLexec(conn,
"SELECT ux_catalog.set_config('search_path', '', false)");
if (UXSQLresultStatus(res) != UXRES_TUPLES_OK)
{
fprintf(stderr, "设置失败:%s", UXSQLerrorMessage(conn));
UXSQLclear(res);
exit_nicely(conn);
}
UXSQLclear(res);

res = UXSQLexec(conn, "begin");
UXSQLclear(res);
printf("正在导入文件\"%s\"...\n", in_filename);
/*  lobjOid = importFile(conn, in_filename); */
lobjOid = lo_import(conn, in_filename);
if (lobjOid == 0)
fprintf(stderr, "%s\n", UXSQLerrorMessage(conn));
else
{
printf("\t作为大对象 %u。\n", lobjOid);

printf("挑选大对象的1000-2000字节\n");
pickout(conn, lobjOid, 1000, 1000);

printf("用X覆盖大对象的1000-2000字节\n");
overwrite(conn, lobjOid, 1000, 1000);

printf("将大对象导出到文件\"%s\"...\n", out_filename);
/*      exportFile(conn, lobjOid, out_filename); */
if (lo_export(conn, lobjOid, out_filename) < 0)
fprintf(stderr, "%s\n", UXSQLerrorMessage(conn));
}

res = UXSQLexec(conn, "end");
UXSQLclear(res);
UXSQLfinish(conn);
return 0;
}

7.6.相关插件

7.6.1.lo

lo模块提供管理大对象(也被称为LO或BLOB)的支持。这包括一种数据类型lo以及一个触发器lo_manage。

原理

JDBC(java数据库连接)驱动的问题之一是规范假定对BLOB(二进制大对象)的引用被存储在一个表中,如果该表被改变,相关的BLOB会被从数据库删除;ODBC(开放数据库连接)驱动也存在这样的问题。

但对于UXDB来说这并不会发生。大对象被当做自主的对象,表对象通过OID引用大对象,可以有多个表对象通过OID引用同一个大对象,因此系统不会因为表对象的改变或者删除而删除大对象。但是由于使用JDBC或ODBC的标准代码不会删除大对象,从而导致孤立大对象—不被任何表引用的大对象,会始终占据磁盘空间。

lo模块允许通过附加一个触发器到包含LO引用列的表来修复这种问题。该触发器本质上只是在删除或修改一个引用大对象的值时做lo_unlink。使用这个触发器时,触发器控制的列中的大对象必须只有一个数据库引用。

这个模块也提供了一种数据类型lo,是oid类型的一个域。有助于区分包含大对象引用的数据库列和包含其他类的数据库列。并非必须使用lo类型来使用触发器,但是用它来追踪数据库中哪些列表示用触发器管理的大对象非常方便。

示例

CREATE EXTENSION lo;
CREATE TABLE image (title TEXT, raster lo);

CREATE TRIGGER t_raster BEFORE UPDATE OR DELETE ON image
 FOR EACH ROW EXECUTE PROCEDURE lo_manage(raster);

对每一个将包含大对象唯一引用的列,创建一个BEFORE UPDATE OR DELETE触发器,并且将该列名作为唯一的触发器参数。也可以用BEFORE UPDATE OF column_name来限制该触发器只对该列上的更新事件执行。如果需要在同一个表中有多个lo列,为每一个lo列创建一个独立的触发器,记住为同一个表上的每个触发器指定不同的名称。

限制

清空或删除表仍将让表中包含的大对象变成孤立的,因为触发器在这种情况下不会被执行。可以在TRUNCATE TABLE或DROP TABLE之前使用DELETE FROM table来避免这种问题。如果已经有或者怀疑有孤立的大对象,使用客户端应用vacuumlo模块可以进行清理。

7.6.2.ux_lob_operate

ux_lob_operate提供了对大对象进行操作的接口,包括对大对象的追加、拷贝、删除、截断等操作,及临时大对象的功能。

为了方便演示设置set bytea_output TO escape;以字符形式显示lob的内容。

7.6.2.1.追加一个大对象

功能

通过fd向一个大对象尾追加一个大对象。

通过OID向一个大对象尾追加一个大对象。

函数

lo_append(fd_dest int4, fd_src int4)
lo_append(dest_oid oid, src_oid oid)

参数

表 lo_append 参数说明

名称描述说明
fd_dest目标大对象描述符 (fd)需要事先以写方式打开。
fd_src源大对象描述符 (fd)需要事先以读方式打开。
dest_oid目标大对象 OID无需提前打开大对象,函数内部会自动处理。
src_oid源大对象 OID无需提前打开大对象,函数内部会自动处理。

示例

通过fd追加大对象,如下所示。

uxdb=# select lo_create(16788); --创建大对象
 lo_create 
-----------
     16788
(1 row)

uxdb=# select lo_put(16788, 0, 'aaaaaa');
 lo_put 
--------
 
(1 row)

uxdb=# select lo_create(16789);
 lo_create 
-----------
     16789
(1 row)

uxdb=# select lo_put(16789, 0, 'bbbbbb');
 lo_put 
--------
 
(1 row)

uxdb=# select lo_get(16788); --获取大对象内容
 lo_get 
--------
 aaaaaa
(1 row)

uxdb=# select lo_get(16789);
 lo_get 
--------
 bbbbbb
(1 row)

uxdb=# select lo_open(16788, set_mode('read')); --以读方式打开
 lo_open 
---------
       0
(1 row)

uxdb=# select lo_open(16789, set_mode('write')); --以写方式打开
 lo_open 
---------
       1
(1 row)

uxdb=# select lo_append(1,0); --通过文件描述符操作追加
 lo_append 
-----------
         0
(1 row)

uxdb=# select lo_get(16789);
    lo_get    
--------------
 bbbbbbaaaaaa
(1 row)

通过OID追加大对象,如下所示。

uxdb=# select lo_get(16789);
    lo_get    
--------------
 bbbbbbaaaaaa
(1 row)

uxdb=# select lo_get(16788);
 lo_get 
--------
 aaaaaa
(1 row)

uxdb=# select lo_append(16789::oid, 16788::oid);--通过oid进行追加
 lo_append 
-----------
         0
(1 row)

uxdb=# select lo_get(16789);
       lo_get       
--------------------
 bbbbbbaaaaaaaaaaaa
(1 row)
7.6.2.2.删除大对象的固定内容

功能

通过fd删除大对象的固定内容。

通过oid删除大对象的固定内容。

函数

lo_erase(lob_fd int4, amount int8, lob_offset int8, fill bytea)
lo_erase(lob_oid oid, amount int8, lob_offset int8, fill bytea)

参数

表 lo_erase 参数说明

名称描述核心说明
lob_fd大对象描述符 (fd)类似于文件句柄。调用前必须先以写方式(如 inv_write)打开大对象。
amount操作字节数指定要删除或处理的长度。若 偏移量 + amount 超过对象实际长度,则处理到末尾为止,超出部分自动忽略。
lob_offset偏移量 (Offset)规定操作的起始位置(从 0 开始计数)。
fill填充字节用于覆盖被删除区域的字节: • CLOB: 通常填充空格(Space)。 • BLOB: 通常填充零字节 (\0)。 注意:仅支持单字节填充。
lob_oid大对象的 OID对象的唯一标识符。使用此参数的函数通常内部封装了打开/关闭逻辑,不需要手动提前打开。

示例

通过fd删除大对象的固定内容,如下所示。

uxdb=# select lo_get(16789);
    lo_get    
--------------
 bbbbbbaaaaaa
(1 row)

uxdb=# select lo_erase(1, 3, 7, ' '); --从第7个位置删除3个字节,以空格填充
 lo_erase 
----------
        3
(1 row)

uxdb=# select lo_get(16789);
    lo_get    
--------------
 bbbbbba   aa
(1 row)

通过OID删除大对象的固定内容,如下所示。

uxdb=# select lo_get(16789);
       lo_get       
--------------------
 bbbbbbaaaaaaaaaaaa
(1 row)

uxdb=# select lo_erase(16789::oid, 6, 14, ' ');--从第14个位置开始删除6个字节,以--空格填充
 lo_erase 
----------
        4
(1 row)

uxdb=# select lo_get(16789);
       lo_get       
--------------------
 bbbbbbaaaaaaaa    
(1 row)
7.6.2.3.拷贝大对象内容到另一个大对象

功能

通过fd拷贝大对象内容到另一个大对象。

通过oid拷贝大对象内容到另一个大对象。

函数

lo_copy(fd_dest int4, fd_src int4, amount int8, dest_offset int8, src_offset int8)

lo_copy(dest_oid oid, src_oid oid, amount int8, dst_offset int8, src_offset int8)

参数

表 lo_copy 参数说明

名称描述核心约束与说明
fd_dest目标大对象描述符必须事先以写方式(Write Mode)打开。
fd_src源大对象描述符必须事先以读方式(Read Mode)打开。
amount拷贝字节数偏移量 + amount 超出源对象实际长度,则拷贝至末尾,超出部分自动忽略。
dest_offset目标对象偏移量拷贝的起始写入位置,且偏移量不得超过目标对象当前长度
src_offset源对象偏移量拷贝的起始读取位置,且偏移量不得超过源对象当前长度
dest_oid目标对象 OID封装了打开逻辑,无需手动通过 fd 操作。
src_oid源对象 OID封装了读取逻辑,无需手动通过 fd 操作。

示例

通过fd拷贝大对象内容到另一个大对象,如下所示。

uxdb=# select lo_copy(1, 0, 6, 7, 0); --从src起始位置拷贝7个字节到dest第6位
 lo_copy 
---------
       0
(1 row)

uxdb=# select lo_get(16789);
    lo_get     
---------------
 bbbbbbaaaaaaa
(1 row)

通过oid拷贝大对象内容到另一个大对象,如下所示。

uxdb=# select lo_get(16789);
       lo_get       
--------------------
 bbbbbbaaaaaaaa    
(1 row)

uxdb=# select lo_copy(16789::oid, 16788::oid, 6, 0, 0);
 lo_copy 
---------
       0
(1 row)

uxdb=# select lo_get(16789);
       lo_get       
--------------------
 aaaaaaaaaaaaaa    
(1 row)
7.6.2.4.获取大对象的查找位置

功能

获取大对象的查找位置。

函数

lo_try_lseek64(lob_fd int4, lob_offset int8)

参数

表 lo_try_lseek64 参数说明

字段解析核心约束与说明
lob_fd大对象描述符 (fd)调用任何基于句柄的操作函数前,必须先使用 lo_openinv_open 事先打开该对象。
lob_offset偏移量 (Offset)指向大对象内部的具体字节位置。偏移量不能超过大对象的实际长度,否则会导致寻址错误或越界。

示例

uxdb=# select lo_try_lseek64(1, 5);
 lo_try_lseek64 
----------------
              5
(1 row)
7.6.2.5.获取当前服务端的PID

功能

获取当前服务端的PID。

函数

ux_getpid()

示例

uxdb=# select ux_getpid();
 ux_getpid 
-----------
    105726
(1 row)
7.6.2.6.获取打开模式的整型值

功能

获取打开模式的整型值。

函数

open_mode(text)

参数

当传入的字符串是'write'的时候返回131072,代表写方式的整型值;当传入的字符串是'read'的时候返回262144,代表读方式的整型值;传入其他字符串的时候返回0。

示例

uxdb=# select lo_open(16788, open_mode('read')); --以读的方式打开
 lo_open 
---------
       0
(1 row)

uxdb=# select lo_open(16789, open_mode('write'));--以写的方式打开
 lo_open 
---------
       1
(1 row)
7.6.2.7.获取位置的整型值

功能

获取位置的整型值。

函数

Set_mode(text)

参数

传入的字符串是'set'时返回0,代表设置模式的整型值;传入字符串是'cur'时返回1,代表当前位置。传入字符串是'end'时返回2,代表设置到最后位置。传入其他字符串是返回-1。

示例

uxdb=# select lo_open(16789, open_mode('write')); --以写的方式打开
 lo_open 
---------
       0
(1 row)

uxdb=# select lo_lseek64(0, 0, set_mode('set')); --使用绝对偏移
 lo_lseek64 
------------
          0
(1 row)

uxdb=# select lo_lseek64(0, 0, set_mode('cur'));--使用当前位置的相对偏移
 lo_lseek64 
------------
          0
(1 row)

uxdb=# select lo_lseek64(0, 0, set_mode('end'));--使用最后位置的相对偏移
 lo_lseek64 
------------
          5
(1 row)
7.6.2.8.获取大对象的长度

功能

获取大对象的长度(字节数)。

函数

lo_get_length(oid)

参数

传入大对象的oid。

示例

uxdb=# select lo_get(16789);
    lo_get    
--------------
 bbbbbbaaaaaa
(1 row)

uxdb=# select lo_get_length(16789);
 lo_get_length 
---------------
            12
(1 row)
7.6.2.9.截断大对象

功能

通过fd截断大对象。

通过oid截断大对象。

函数

lo_trim(fd int4, newlen int8)

lo_trim(lob_oid oid, newlen int8)

参数

表 lo_trim 参数说明

名称描述说明
fd大对象描述符执行操作前,需要事先以写方式打开大对象。
newlen大对象的剩余长度指定截断或调整后的目标长度,该值不能大于大对象的当前实际长度。
lob_oid大对象的 OID针对该参数的操作不需要提前打开大对象,系统会自动处理。

示例

通过fd截断大对象,如下所示。

uxdb=# select lo_trim(0, 5);
 lo_trim 
---------
       0
(1 row)

uxdb=# select lo_get(16789);
 lo_get 
--------
 aaaaa
(1 row)

通过oid截断大对象,如下所示。

uxdb=# select lo_trim(16789::oid, 5);
 lo_trim 
---------
       0
(1 row)

uxdb=# select lo_get(16789);
 lo_get 
--------
 aaaaa
(1 row)
7.6.2.10.临时大对象

临时大对象被记录在表ux_temp_lob中。

表ux_temp_lob 列说明

名称类型描述
oidoid大对象的唯一标识符(OID)
typeduration大对象的类型(通常指生命周期或存储属性)
pidint4创建该大对象的后台服务进程 ID

临时大对象被分成两类,一类是连接级别的临时大对象用0表示,可以调用free_session_temp函数一次释放当前连接创建的连接级别大对象,也可以调用free_temp_lob逐个释放;另一类是调用级别的临时大对象用1表示,只能调用free_temp_lob逐个释放。另外临时大对象的类型被指定为新的类型duration。

7.6.2.10.1.创建临时大对象

功能

创建临时大对象。

参数

表 create_temp_lob 参数说明

名称描述
oid大对象 OID:如果输入 0 则由系统自动分配一个大对象 OID;否则按照给定的 OID 进行创建。
duration大对象类型:用于指定大对象的生命周期或存储类型。

返回值

大对象oid。

示例

uxdb=# select create_temp_lob(0,0::duration);
 create_temp_lob 
-----------------
           40985
(1 row)
7.6.2.10.2.删除临时大对象

功能

删除临时大对象。

参数

表 free_temp_lob 参数说明

名称描述
oid临时大对象 OID:用于标识在当前会话或事务中生成的临时大对象。

返回值

临时大对象存在返回1,否则返回空。

示例

uxdb=# select free_temp_lob(40985);
 free_temp_lob 
---------------
             1
(1 row)
7.6.2.10.3.清除连接级别临时大对象

功能

清除连接级别临时大对象。

返回值

临时大对象存在返回1,否则返回空。

示例

uxdb=# select free_temp_session_lob();
 free_temp_session_lob 
-----------------------
                     1
(1 row)
7.6.2.10.4.判断大对象是不是连接级别临时大对象

功能

判断大对象是不是连接级别临时大对象。

参数

表 lob_is_temp 参数说明

名称描述
oid大对象 OID:数据库中用于唯一标识该大对象的对象标识符(Object Identifier)。

返回值

所给定oid是连接级别临时大对象返回1,否则返回0。

示例

uxdb=# select lob_is_temp(40988);
 lob_is_temp 
-------------
           1
(1 row)

7.7.工具

7.7.1.vacuumlo

功能

vacuumlo是一个从UXDB数据库中移除“孤立”大对象的简单使用程序。一个孤立的大对象(LO)是指其OID不出现在数据中任何oid或lo数据列中的LO。

如果使用该程序,也许还会对lo模块中的lo_manage触发器感兴趣。lo_manage对于避免创建孤立LO有用处。

在命令行中提到的所有数据库都将被处理。

语法

Vacuumlo [option...] dbname...

选项

vacuumlo接受下列命令行参数:

  • -l limit
    --limit= limit
    在每一个事务中移除不超过limit个大对象(默认值为1000)。因为移除每一个LO时服务器都将要求一个锁,所以在一个事务中移除过多的LO会有超过max_locks_per_transaction的风险。如果想在一个事务中就完成所有的移除工作,请将这个限制设置为0。

  • -n
    --dry-run
    不移除任何东西,只是显示下一步操作。

  • -v
    --verbose
    显示进度消息。

  • -V
    --version
    打印vacuumlo的版本并退出。

  • -?
    --help
    显示关于vacuumlo的命令行参数,并且退出。

vacuumlo也接受下列命令行参数用于连接:

  • -h host --host=host
    数据库服务器的主机名。

  • -p port
    --port=port
    数据库服务器的端口。

  • -U username
    --username=username
    用于连接的用户名。

  • -w
    --no-password
    不要发出一个口令提示。如果服务器要求口令认证并且没有其他方式可以提供一个口令(例如一个.uxpass文件),连接尝试将会失败。这个选项可用于批处理任务以及脚本,因为在这些情况下不会有用户输入口令。

  • -W
    --password
    强制vacuumlo在连接到数据库之前提示要求一个口令。

    这个选项不是必需的,因为如果服务器要求口令认证,vacuumlo会自动提示要求一个口令。但是,vacuumlo将会浪费一次连接尝试来了解到服务器需要口令。在某些情况,可以用-W来避免这种额外的连接尝试。

环境变量

  • UXHOST
    UXPORT
    UXUSER
    默认连接参数。

和大部分其他UXDB工具相似,这个工具也使用libuxsql支持的环境变量。

说明

vacuumlo按照下列方法工作:首先vacuumlo建立一个临时表,其中包含所选择数据库中所有大对象的OID。然后它会扫描数据库中所有类型为oid或lo的列,并且从临时表中移除在这些列中出现过的OID(注意:只有类型为这些名字的才被考虑,特别的,在这些类型之上的域是不被考虑的)。临时表中剩下的项就标识了孤立LO。它们将被移除。

7.8.大对象类型

当需要存储较大的内容时,可以使用大对象;根据其作用的不同大对象可以分为clob,blob。nclob其行为和clob一致,nclob是clob的同义词。

表 大对象类型

类型名作用输入输出
CLOB存储长文本接受任意字符,按输入字符原样存储。显示文本内容
BLOB存储大文件仅接受十六进制格式的数字和字符(0-9, A-F)。显示文件的十六进制内容

注意

clob、blob存储上限要大的多(超过了1TB),其实质是存储大对象OID,在输出时通过OID读取对应内容,再分别以clob、blob的输出形式进行输出;也因此使用旧的clob、blob赋值时不会产生新的大对象,如果直接赋值后进行修改会造成旧的clob、blob也会被一并修改。如若需要产生新的大对象那么可以先调用empty_clob\empty_blob产生新的对象,然后调用lo_append函数(需要注意先创建ux_lob_operate插件)追加内容。如下所示。

有表 t1(c1 clob), t2(c2 clob)
insert into t1 values(empty_blob());
select lo_append(c1::oid,c2) from t2 where ...;

表 大对象操作符

操作符左操作数右操作数作用
<、<=、=、>=、>、<>CLOB | BLOB | OIDCLOB | BLOB | OID比较大对象 OID 的值
<、<=、=、>=、>、<>CLOB | BLOB字符类型将 CLOB、BLOB 类型转为字符,按字符进行比较
<、<=、=、>=、>、<>字符类型CLOB | BLOB将 CLOB、BLOB 类型转为字符,按字符进行比较
||CLOB | BLOB | 字符类型CLOB | BLOB | 字符类型将 CLOB、BLOB 类型转为字符,按字符进行拼接
LIKECLOB | BLOB | 字符类型CLOB | BLOB | 字符类型将 CLOB、BLOB 类型转为字符,按字符进行查找

表 大对象函数

函数作用示例
empty_blob()生成内容为空的 BLOB 大对象empty_blob()
empty_clob()生成内容为空的 CLOB 大对象empty_clob()
clob_import(text)导入 CLOB 大对象,参数为其路径clob_import('/home/uxdb/test.txt')
clob_import(text, oid)导入 CLOB 大对象,参数 1 为其路径,参数 2 为生成的大对象 OIDclob_import('/home/uxdb/test.txt', 10086)
blob_import(text)导入 BLOB 大对象,参数为其路径blob_import('/home/uxdb/test.jpg')
blob_import(text, oid)导入 BLOB 大对象,参数 1 为其路径,参数 2 为生成的大对象 OIDblob_import('/home/uxdb/test.txt', 10086)
clob_export(clob, text)导出 CLOB 大对象为文件,参数 1 为 CLOB 大对象,参数 2 为导出后的文件路径SELECT clob_export(c1, '/home/uxdb/test.txt') FROM tclob;
blob_export(blob, text)导出 BLOB 大对象为文件,参数 1 为 BLOB 大对象,参数 2 为导出后的文件路径SELECT blob_export(c1, '/home/uxdb/test.jpg') FROM tblob;

表 类型转换

源类型目标类型转换规则
CLOBCLOB、OID存储不变,只是以不同的形式读出
BLOBCLOB、OID存储不变,只是以不同的形式读出
OIDCLOB、BLOB存储不变,只是以不同的形式读出
CLOB字符类型直接按照输出转换为字符
BLOB字符类型按照对应的编码转换为字符
字符类型CLOB、BLOB转换规则和输入规则相同
数值类型CLOB将数值转为字符后存入
时间类型CLOB将时间转为字符后存入

附录

附录 A.SQL关键字

表 SQL关键字列出了在SQL 标准和 UXDB 2122中作为关键字的所有标记。背景信息可以在标识符和关键词节中找到。(由于空间原因,仅包括 SQL 标准的最新两个版本和 SQL-92 进行历史比较。这些版本与其他中间标准版本之间的差异很小。)

SQL 区分保留和非保留关键词。根据标准,保留关键词是真正的关键词;它们永远不能作为标识符使用。非保留关键词只在特定上下文中具有特殊含义,并且可以在其他上下文中用作标识符。大多数非保留关键词实际上是由 SQL 指定的内置表和函数的名称。非保留关键词的概念实际上只是声明在某些上下文中某个预定义的含义与某个词相关联。

在 UXDB 解析器中,情况要复杂一些。有几个不同的令牌类别,从那些永远不能用作标识符的令牌到在解析器中没有特殊状态但被视为普通标识符的令牌。(这通常适用于由 SQL 指定的函数。) 即使保留关键词在 UXDB 中也不完全保留,可以用作列标签(例如,SELECT 55 AS CHECK,即使 CHECK 是保留关键词)。

在UXDB中支持关键字重命名功能,重命名后原有关键字功能被重命名的关键字取代。UXDB通过guc变量ux_keywords_fallback以关键字对的方式对关键字进行重命名,关键字名不能超过63位,每个关键字对用分号隔开,小括号括起来;多个关键字对用逗号隔开。如:'(key1:value1),(key2:value2)' 其中key1、key2为uxdb内部关键字,value1、value2为自定义的关键字名。其默认值为'(ux_prior:prior),(ux_encrypted:encrypted)',且如果ux_priorux_encrypted如果在重命名列表中未进行指定,则它们的重命名会被指定为对应的默认值。

表 SQL关键字l列中,对于UXDB,我们将那些明确为解析器所知,但允许作为列名或表名使用的关键字分类为“非保留”关键字。一些在其他情况下为非保留的关键字不能用作函数或数据类型的名称,并相应地进行了标记。(其中大多数词表示具有特殊语法的内置函数或数据类型。该函数或类型仍然可用,但用户无法重新定义它们。)被标记为“保留”的关键字不允许用作列名或表名。一些保留关键字可以用作函数或数据类型的名称;这在表中也有显示。如果没有特殊标记,保留关键字仅允许用作列标签。在此列中,空白条目意味着在UXDB中将该单词视为普通标识符。此外,大多数关键字可以作为“裸露的”列标签使用,而不必在它们之前写上AS,但有一些关键字需要在前面加上AS以避免歧义。这些在表中标记为“需要AS”。作为一般规则,如果你对使用任何列出的关键字作为标识符的命令出现错误,请尝试将标识符加引号,看看问题是否消失。在研究表表 SQL关键字 之前,重要的是要明白在UXDB中,关键字不被保留并不意味着与该关键字相关的功能未被实现。相反,关键字的存在并不表示该功能存在。

1. 保留关键词 (Reserved Words)

这些词在 UXDB 中具有特殊含义,通常不能直接用作表名或列名。

关键词UXDB 状态SQL-92
after保留
all保留保留
analyse / analyze保留
and / any / as / asc保留保留
asymmetric保留
both / case / cast保留保留
check / collate / column保留保留
connect_by_root保留
constraint / create保留保留
current_catalog / current_date / current_role保留
current_time / current_timestamp / current_user保留保留
decrypted / default / deferrable / desc / distinct保留保留
do / else / end / except / false / fetch / first保留保留
for / foreign / from / GRANT / group / having保留保留
in / initially / intersect / into / lateral / leading保留保留
limit / localtime / localtimestamp / minus保留
not / nocycle / null / offset / on / only / or / order保留保留
pivot / placing / plcols / primary / references保留保留
returning / rownum / select / some / symmetric保留保留
table / then / to / trailing / true / union / unique保留保留
unpivot / user / using / ux_encrypted / ux_prior保留保留
variadic / when / where / window / with保留保留

2. 类型或列名关键字 (Column Name Keywords)

在某些上下文中可以作为列名,但在定义类型或特定语法时受限。

关键词UXDB 状态SQL-92
between / bit / char / character列名关键字保留
bigint / boolean / dec / decimal / float列名关键字保留
int / integer / interval / number / numeric列名关键字保留
precision / smallint / time / timestamp / varchar列名关键字保留
decode / exists / extract / greatest / ifnull列名关键字
grouping / grouping_id / inout / least / locate列名关键字
national / nchar / none / normalize / nullif / nvl列名关键字
out / overlay / position / real / row / setof列名关键字
substring / sys_connect_by_path / to_date / treat / trim列名关键字
values / varying / xmlattributes / xmlconcat / xmlelement列名关键字

3. 函数或操作符关键字 (Function/Type Keywords)

关键词UXDB 状态SQL-92
authorization / collation / convert函数关键字保留
binary / concurrently / cross / freeze / full函数关键字
ilike / inner / isnull / is / join / left / like函数关键字保留
natural / notnull / outer / overlaps / regexp函数关键字保留
right / rlike / sample / similar / tablesample / verbose函数关键字

4. 非保留关键词 (Non-reserved Words)

这些词在 UXDB 中通常可以自由用作标识符。

关键词(部分列举)UXDB 状态SQL-92
abort / access / account / action / add / admin非保留部分保留
begin / commit / rollback / savepoint / release非保留部分保留
copy / database / index / schema / view / trigger非保留
password / role / session / storage / temp / text非保留

附录 B.兼容功能

UXDB 兼容性功能列表(Oracle / MySQL / 达梦)

1. Oracle 兼容性

功能版本说明
数据压缩比功能(cstore 实现)
闪回 (Flashback)
INSERT ALL / FIRST 语法
MERGE INTO 语法
(+) 外连接操作符语法
q' ' 转义符语法
同义词 (Synonym) 功能
正则表达式2.1.1.3
时间格式 YYYYMMDDHH24MISS 及空字符插入查询
DISTINCTORDER BY 子查询逆序排序2.1.1.4
大对象操作函数:EMPTY_BLOB/CLOB, IMPORT/EXPORT 涵盖 BLOB/CLOB
MINUS 关键字
空字符串转换为 NULL
主键、外键 ENABLE/DISABLE 语法
同义词语法、对齐语法结构
ROWID 支持
LONG / DOUBLE 类型2.1.1.5A
TRUNC 支持 VARCHAR 等类型
REGEXP_LIKE 函数
SUBSTR / TO_CHAR
层次查询 (Hierarchical Queries)2.1.1.5BCONNECT BY
ROUND / USERENV 函数
TO_NUMBER 支持 fmtXXX 格式
DBLINK / LISTAGG / SAMPLE
V$SYSSTAT 等 13 个系统视图
WITH FUNCTION / PROCEDURE
LAST_DAY / MONTHS_BETWEEN / ADD_MONTH
FORALL 循环
SELECT FOR UPDATE NOWAIT/WAIT/SKIP LOCKED2.1.1.5C
闰年计算支持
TO_TIMESTAMP_TZ / SYS_EXTRACT_UTC
BLOB / CLOB 原生支持
REGEXP_SUBSTR / REMAINDER / RATIO_TO_REPORT
UNPIVOT(列转行)支持
ANY / ALL(参数列表)语法支持
WITH AS 语法
多语言 (PL/SQL) 框架支持
集合类型兼容
PL/SQL 中支持 Oracle 的 IS 关键字2.1.1.5D
NULL 和空串行为兼容
缺少 LIKE (~~) 操作符的相关类型隐式转换
Oracle 虚拟列分区特性支持
SYS_GUID 函数2.1.2.2基于 uuid-ossp 实现

2. MySQL 兼容性

功能版本说明
DATE_ADDDATE_DIFF 函数
ISNULL
分页语法 LIMIT x, y
对表添加约束时约束名可省略
GROUP BY 语法兼容
CASE WHEN 返回值类型兼容2.1.1.4
系统函数 DATE_FORMAT2.1.1.5B调用 TO_CHAR 按照格式转换
UPDATE...JOIN 语法
FIND_IN_SET()
外键暂时失效2.1.1.5C
CHANGE(字段修改)
IF 函数
SHOW INDEX / SHOW COLUMNS 查看索引与列信息
支持反单引号 (```)
ELT() / FIELD() / FORMAT() / HEX()
LOCATE() / REGEXP / STRCMP() / RAND()
CONVERT() / STR_TO_DATE()
UPDATE / DELETE 带有 LIMIT 用法
TIMESTAMPDIFF() / UUID()
HOUR() / QUARTER() / NULLIF()
FIRST / AFTER 关键字用法 用于字段位置调整
AES_ENCRYPTAES_DECRYPT 函数
兼容 ALTER VIEW DROP CONSTRAINT 语法
日期操作函数 TO_DSINTERVAL
支持关键字 COMMENT 指定备注2.1.1.5D

3. 达梦 (DM) 兼容性

功能版本说明
分页语法 LIMIT x, y
NUMBER 数据类型兼容 实质是 NUMERIC 的别名
LONGVARBINARY 数据类型
时间和数字类型运算返回值为数字型
NUMERIC / INT 类型插入空字符
时间类型 (TIMESTAMP) 插入 0 (int) 场景
TO_CHAR(text, text) / TO_DATE
SYSDATE 关键字兼容
DISTINCT 未与 ORDER BY 一起使用的排序问题
ROWNUM 语法功能支持
子查询支持无别名语法
DELETE 不加 FROM 语法
UPDATE 语句使用别名语法
STORAGE 语法支持 包括默认表空间 MAIN
ALTER TABLE CONSTRAINT PRIMARY KEY 语法
创建表指定 CLUSTER / NOT CLUSTER 语法
TOP 语法
序列.NEXTVAL 语法
Varbinary, Clob, Blob, Raw, Longvarbinary 类型
LIKE (~~) 操作符转换与扩展2.1.1.4支持 int LIKE '%1%' 等场景
TO_DATE 支持 TIMESTAMP WITHOUT TIME ZONE2.1.1.5B