赞
踩
SQL语句可能(有意无意地)要求在同一表达式里混合不同的数据类型。 PostgreSQL在计算混合类型表达式方面有许多功能。
在大多数情况下,用户不需要明白类型转换机制的细节。但是,由PostgreSQL进行的隐式类型转换会对查询的结果产生影响。必要时这些结果可以被使用显式类型转换来调整。
本章介绍PostgreSQL类型转换的机制和习惯。
SQL是一种强类型语言。也就是说,每个数据项都有一个相关的数据类型,数据类型决定其行为和允许的用法。 PostgreSQL有一个可扩展的类型系统,该系统比其它SQL实现更具通用和灵活。因而,PostgreSQL中大多数类型转换行为是由通用规则来管理的,而不是ad hoc启发式规则。这种做法允许使用混合类型表达式,即便是其中包含用户定义的类型。
PostgreSQL扫描器/解析器只将词法元素分解成五个基本种类:整数、非整数数字、字符串、标识符、关键字。大多数非数字类型常量首先被分类为字符串。SQL语言定义允许将类型名指定为字符串, 这个机制被PostgreSQL用于保证解析器沿着正确的方向运行。例如,查询:
- SELECT text 'Origin' AS "label", point '(0,0)' AS "value";
-
- label | value
- --------+-------
- Origin | (0,0)
- (1 row)
有两个文字常量,类型分别为text
和point
。如果一个串文字没有指定类型,初始将被分配一个占位符类型unknown
,该类型将在下文描述的后续阶段被解析。
在SQL解析器里,有四种基本的SQL结构要求独立的类型转换规则:
函数调用
PostgreSQL类型系统的大部分建立在一套丰富的函数上。 函数可以有一个或多个参数。由于PostgreSQL允许函数重载, 所以函数名自身并不唯一地标识将要被调用的函数,解析器必须根据提供的参数类型选择正确的函数。
操作符
PostgreSQL允许带有前缀和后缀一元(单目)操作符的表达式,也允许二元(两个参数)操作符。像函数一样,操作符也可以被重载,因此操作符的选择也有同样的问题。
值存储
SQL INSERT
和UPDATE
语句将表达式的结果放 入表中。语句中的表达式类型必须和目标列的类型一致(或者可以被转换为一致)。
UNION
、CASE
和相关结构
因为来自一个联合的SELECT
语句中的所有查询结果必须在一个列集中显示,所以每个 SELECT
子句的结果类型必须能相互匹配并被转换成一个统一的集合。类似地,一个 CASE
结构的结果表达式必须被转换成一种公共的类型,这样CASE
表达式作为整体才 有一种已知的输出类型。同样的要求也存在于ARRAY
结构以及GREATEST
和LEAST
函数中。
系统目录存储有关哪些数据类型之间存在哪种转换(或造型)以及如何执行这些转换的相关信息。额外的造型可以由用户通过CREATE CAST命令增加(这个通常和定义一种新的数据类型一起完成。 内建的类型转换集已经经过了仔细的雕琢,最好不要去更改它们)。
解析器提供了一种额外的启发式规则,它允许在具有隐式造型的类型组中恰当造型行为的改进决定。 数据类型被分为几个基本的类型分类,包括boolean
、numeric
、string
、bitstring
、datetime
、timespan
、geometric
、network
和用户自定义。在每个分类中,可以有一个或多个首选类型, 当存在类型选择时,这个是更好的选择。利用精心选择的首选类型和可用的隐式造型, 我们可以确保有歧义的表达式(那些有多个候选解析方案的表达式)可以用一种有用的方式来处理。
所有类型转换规则都是建立在下面几个基本原则上的:
隐式转换决不能有意外的或不可预见的输出。
如果一个查询不需要隐式类型转换,解析器或执行器不应该有额外的开销。也就是说,如果一个查询是结构良好的并且类型已经匹配,则查询不应该在解析器里耗费额外的时间执行,也不会在查询中引入不必要的隐式类型转换调用。
另外,如果一个查询通常要求为某个函数进行隐式类型转换,而用户定义了一个有正确参数类型的新函数, 解析器应该使用新函数并不再做隐式转换来使用旧函数。
被一个操作符表达式引用的特定操作符由下列过程决定。注意这个过程会被所涉及的操作符的优先级间接地影响,因为这将决定哪些子表达式被用作哪个操作符的输入。
操作符类型决定
从系统目录pg_operator
中选出要考虑的操作符。如果使用了一个不带模式限定的操作符 名(常见的情况),那么操作符被认为是那些在当前搜索路径中可见并有匹配的名字和参数个数的操作符。如果给出一个被限定的操作符名,那么只考虑指定模式中的操作符。
查找一个正好接受输入参数类型的操作符。如果找到一个(在一组被考虑的操作符中,可能只存在一个正好匹配的),则使用之。在通过限定名称(非典型)调用在一个允许不可信用户创建对象的方案中找到的任意操作符时,精确匹配的缺失会导致安全性危害 [8] 。在这样的情况下,应该造型参数以便强制一次精确匹配。
a. 如果一个二元操作符调用中的一个参数是unknown
类型,则在本次检查中假设它与另一个参数类型相同。 对于涉及两个unknown
输入的调用或者带有一个unknown
输入的一元操作符,在这一步将永远找不到一个匹配。unknown
类型 而另一个是一种域类型,下一次检查会看看是否有一个操作符正好在两边都 接受该域的基类型,如果有就使用它。寻找最优匹配。
a. 抛弃那些输入类型不匹配并且也不能被转换成匹配的候选操作符。unknown
文字被假定为可以为这个目的被转换为 任何东西。如果只剩下一个候选操作符,则使用之,否则继续下一 步。unknown
类型,检查被剩余候选操作符在那些参数位置上接受的类型分类。 在每一个位置,如果任何候选接受该分类,则选择string
分类(这种对字符串的偏爱是合适的, 因为未知类型的文本确实像字符串)。否则,如果所有剩下的候选操作符都接受相同的类型 分类,则选择该分类;否则抛出一个错误(因为在没有更多线索的条件下无法作出正确 的推断)。现在抛弃不接受选定的类型分类的候选操作符。然后,如果任意候选操作符接受那个分类中的首选类型, 则抛弃那些在该参数位置接受非首选类型的候选操作符。如果没有候选操作符能通过这些测试则保留全部候选者。如果只剩下一个候选者,则使用之;否则继续下一步。unknown
参数也有已知类型的参数,并且所有已知类型参数具有相同的类型,则假定该unknown
参数也是那种类型的,并且检查哪些候选操作符可以在该unknown
参数的位置上接受那个类型。如果正好有一个候选者通过了这个测试,则使用之;否则失败。下面是一些例子。
例 1. 阶乘操作符类型决定
在标准目录中只有一个被定义的阶乘操作符(后缀!
),它接受一个类型为bigint
的参数。在下面这个查询表达式中,扫描器会为该参数分配一个初始类型integer
:
- SELECT 40 ! AS "40 factorial";
-
- 40 factorial
- --------------------------------------------------
- 815915283247897734345611269596115894272000000000
- (1 row)
因此,解析器在操作数上做了一个类型转换,该查询等价于:
SELECT CAST(40 AS bigint) ! AS "40 factorial";
例 2. 字符串连接操作符类型决定
一个类字符串的语法被用来处理字符串类型和处理复杂的扩展类型。未指定类型的字符串与可能的候选操作符匹配。
一个未指定参数的例子:
- 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
)会被用作解决未知类型文字的指定类型。
例 3. 绝对值与否定操作符类型决定
PostgreSQL操作符目录中有几个对于前缀操作符@
的条目, 这些都现实了针对各种数字数据类型的绝对值操作。其中之一用于float8
类型,它是在数字分类中的首选类型。 因此,PostgreSQL将在遇到一个unknown
输入时使用它:
- 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)
例 4. 数组包含操作符类型决定
这里是另一个决定带有一个已知和一个未知输入的操作符的例子:
- SELECT array[1,2] <@ '{1,2,3}' as "is subset";
-
- is subset
- -----------
- t
- (1 row)
PostgreSQL操作符目录有一些条目用于中缀操作符<@
,但是仅有的两个可以在左手边接受一个整数数组的是数组包含(anyarray
<@
anyarray
)和范围包含(anyelement
<@
anyrange
)。因为这些多态伪类型中没有一个被认为是首选的,解析器不能以此为基础来解决歧义。不过,上述3.f告诉它假定位置类型的文字和其他输入的类型相同,即整数数组。现在这两个操作符中只有一个可以匹配,因此数组包含被选择(如果选择范围包含,我们将得到一个错误,因为该字符串没有成为一个范围文字的正确格式)。
例 5. 域类型上的自定义操作符
用户有时会尝试声明只适用于一种域类型的操作符。这是可能的, 但是远非它看起来那么有用,因为操作符解析规则被设计为选择 适用于域的基类型的操作符。考虑这个例子:
- 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
操作符( 上述2.a),当然这里没有; 然后它将会考虑该域的基类型text
,并且看看是否有一 个text
=
text
操作符(上述2.a),这里也没有;因 此它会把unknown
-类型文字解析为text
并使用text
=
text
操作符。 让自定义操作符能被使用的唯一方法是显式地转换改文字:
SELECT * FROM mytable WHERE val = text 'foo';
这样根据准确匹配规则会立即找到 mytext
=
text
操作符。如果 到达最佳匹配规则,它们会积极地排斥域类型上的操作符。如果它 们没有,这样一个操作符将创建太多歧义操作符失败,因为转换规 则总是认为一个域可以和它的基类型相互转换,并且因此该域操作 符在所有与该基类型上的一个类似命名的操作符相同的情况中都被 认为可用。
被一个函数调用引用的特定函数使用下面的过程来决定。
函数类型决定
从pg_proc
系统目录中选择要被考虑的函数。 如果使用一个非模式限定的函数名称,那么函数被认为是那些在当前搜索路径中可见并有匹配的名字和参数个数的函数。如果给出一个被限定的函数名,那么只考虑指定模式中的函数。
VARIADIC
数组参数声明一个函数,并且调用不使用关键字VARIADIC
, 那么该函数就好像其数组参数被它的元素类型的一次或多次出现所替换,根据需要去匹配调用。 这样的扩展之后,函数可能会有和非可变函数相同的参数类型。在这种情况下,在搜索路径中出现比较早的函数将被使用,或者如果两个函数在相同的模式中时首选非可变的那一个。VARIADIC
关键词的调用替换掉就可以绕过这种危害。涉及到VARIADIC "any"
参数的调用通常没有等效的包含VARIADIC
关键词的形式。为了安全地发出那些调用,函数的方案必须只允许可信用户创建对象。检查一个函数正好接受输入参数类型。如果存在一个(在所考虑的一组函数中只能有一个准确匹配),则使用之。在通过限定名称调用在一个允许不可信用户创建对象的方案中找到的函数时,精确匹配的缺失会导致安全性危害。在这样的情况下,应该造型参数以便强制一次精确匹配(在该步骤中,涉及unknown
的情况将永远找不到一个匹配)。
如果没有发现准确匹配,那么查看函数调用是否作为一个特定的类型转换请求出现。 如果函数调用仅有一个参数并且函数名和一些数据类型的(内部)名称相同,那么该情况将会发生。 并且,该函数参数必须是一个未知类型的文字,或者是一个可以被二进制强制转换到命名数据类型的类型, 或者是一个可以通过应用其I/O函数被转换为命名数据类型的类型(也就是,转换是转到标准字符串类型或者从标准字符串类型转来)。当满足这些条件的时候,函数调用被当做CAST
声明的一种形式来对待。
查找最佳匹配。
a. 如果候选函数的输入类型不匹配并且不能通过转换(使用一个隐式转换)达到匹配,则丢弃它。为了这个目的,unknown
文字被假定可被转换成任何东西。如果仅有一个候选项,则使用之;否则继续下一步。unknown
,那么检查那些被剩余候选项在那些参数位置上接受的类型分类。在每一个位置上,如果任何候选项接受该分类则选择string
分类 (这个偏向于字符串是恰当的,因为一个未知类型文字看起来像字符)。 否则,如果所有剩余的候选项接受相同的类型分类,那么选择那个分类; 否则将失败,因为缺乏更多线索来推断出正确的选择。现在,丢弃不接受被选中类型分类的候选项。此外,如果任何候选项接受那个分类中的一个首选类型,则丢弃对该参数接受非首选类型的候选项。如果没有候选项能通过这些测试,则保留所有候选项。如果只剩下一个候选项,则使用之;否则继续下一步。unknown
参数也有已知类型的参数,并且所有已知类型参数具有相同的类型,则假定该unknown
参数也是那种类型的,并且检查哪些候选函数可以在该unknown
参数的位置上接受那个类型。如果正好有一个候选者通过了这个测试,则使用之;否则失败。注意,对于操作符和函数类型决定来说“最优匹配”规则是完全相同的。下面是一些例子。
例 6. 圆整函数参数类型决定
只有一个带有两个参数的圆整
函数; 它采用第一个参数类型为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);
例 7. 可变函数决定
- CREATE FUNCTION public.variadic_example(VARIADIC numeric[]) RETURNS int
- LANGUAGE sql AS 'SELECT 1';
- CREATE FUNCTION
这个函数接受(但不要求)VARIADIC关键词。它能同时容忍integer以numeric参数:
- 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
关键词,第三个调用是安全的。
例 8. 子串函数类型决定
有几个substr
函数,其中一个用于text
和integer
类型。如果使用一个未指定类型的字符常量调用,那么系统选择接受一个首选分类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);
注意:
解析器从pg_cast
目录中知道text
和varchar
是二进制可兼容的, 意思是其中一个可以被传递给接受另一种类型的函数而不需要做任何物理转换。因此,在这种情况下不会真正使用类型转换调用。
并且,如果该函数使用一个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)
将被插入到一个表的值会按照下列步骤被转换到目标列的数据类型。
值存储类型转换
检查一个与目标的准确匹配。
否则,尝试转换表达式为目标类型。如果在两种类型之间的一个 赋值造型已经被注册在pg_cast
目录中, 这是可能的。或者,如果该表达式是一个未知类型的文字, 则该文字串的内容将被提供给目标类型的输入转换例程。
检查是否有一个用于目标类型的尺寸调整造型。尺寸调整造型是一个从该类型到其自身的造型。 如果在pg_cast
目录中找到一个,那么把表达式存储到目标列中之前把它应用到表达式。这样一个造型的实现函数总是采用一个额外的integer
类型的参数,它接收目标列的atttypmod
值(通常是它被声明的长度,尽管对于不同数据类型atttypmod
有不同的解释),并且它可能采用第三个boolean
参数来说明造型是显式的还是隐式的。该造型函数负责应用任何长度相关的语义,例如尺寸检查或截断。
例 9. 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
,允许||
操作符被决定为text
连接。 然后操作符的text
结果被转换成bpchar
(“空白填充字符”,character
数据类型的内部名称)来匹配目标列类型(由于从text
到bpchar
的转换是二进制强制的,这个转换不会插入任何实际的函数调用)。最后,尺寸调整函数bpchar(bpchar, integer, boolean)
被从系统目录中找到并应用到操作符的结果和存储的列长度上。这个类型相关的函数执行必要的长度检查并增加填充的空间。
SQL UNION
结构必须使可能不相似的类型匹配成为一个单一的结果集。该决定算法被独立地应用到一个联合查询的每个输出列。INTERSECT
和EXCEPT
采用和UNION
相同的方法来决定不相似的类型。CASE
、ARRAY
、VALUES
、GREATEST
和LEAST
结构使用相同的算法来使它们的组成表达式匹配并选择一种结果数据类型。
UNION
、CASE
和相关结构的类型决定
如果所有的输入为相同类型,并且不是unknown
,那么就决定是该类型。
如果任何输入是一种域类型,在所有后续步骤中都把它当做 该域的基类型。
如果所有的输入为unknown
类型,则决定为text
(字符串分类的首选类型)类型。否则,为了剩余规则,unknown
输入会被忽略。
如果非未知输入不全是相同的类型分类,则失败。
如果有的话,选择第一个在其分类中作为首选类型的非未知输入类型。
否则,选择最后的非未知输入类型,它允许所有在前面的非未知输入被隐式地转换为它(总有这样的一种类型,因为至少在列表中的第一个类型必须满足这个条件)。
转换所有的输入为选定的类型。如果没有一个从给定输入到选定类型的转换将会失败。
下面是一些例子。
例 10. 联合中未指定类型的类型决定
- SELECT text 'a' AS "text" UNION SELECT 'b';
-
- text
- ------
- a
- b
- (2 rows)
这里,未知类型文字'b'
将被决定为类型text
。
例 11. 简单联合中的类型决定
- SELECT 1.2 AS "numeric" UNION SELECT 1;
-
- numeric
- ---------
- 1
- 1.2
- (2 rows)
文字1.2
是numeric
类型,且integer
值1
可以被隐式地造型为numeric
,因此使用numeric
类型。
例 12. 可换位联合中的类型决定
- SELECT 1 AS "real" UNION SELECT CAST('2.2' AS REAL);
-
- real
- ------
- 1
- 2.2
- (2 rows)
这里,由于类型real
被能被隐式地造型为integer
,而integer
可以被隐式地造型为real
,联合结果类型被决定为real
。
例 13. 嵌套合并中的类型决定
- SELECT NULL UNION SELECT NULL UNION SELECT 1;
-
- ERROR: UNION types text and integer cannot be matched
这个失败发生的原因是PostgreSQL把多个UNION
当作是成对操作的嵌套,也就是说上面的输入等同于:
(SELECT NULL UNION SELECT NULL) UNION SELECT 1;
根据上面给定的规则,内层的UNION
被确定为类型text
。然后外层的UNION
的输入是类型text
和integer
,这就导致了上面看到的错误。通过确保最左边的UNION
至少有一个输入类型为想要的结果类型,就可以修正这个问题。
INTERSECT
和EXCEPT
操作也被当作成对操作。不过,这一节中描述的其他结构会在一个决定步骤中考虑所有的输入。
多少有些类似于对待用于操作符和函数的域输入的方式,这种行为允许 一种域类型能通过一个UNION
或相似的结构保留下来, 只要用户小心地确保所有的输入都是(显式地或隐式地)准确类型。否 则会优先选择该域的基类型。
前面的小节中给出的规则将会导致对SQL查询中的所有表达式分配非unknown
数据类型,不过作为SELECT
命令的简单输出列出现的未指定类型的文本除外。例如,在
SELECT 'Hello World';
中没有标识该字符串应该取何种类型。在这种情况下,PostgreSQL将会退而求其次将其类型决定为text
。
当SELECT
处于UNION
(或者INTERSECT
,或者EXCEPT
)结构的一边或者出现在INSERT ... SELECT
中时,这条规则就不适用了,因为在前面小节中给出的规则会优先。在第一种情况下未指定类型文本的类型将从UNION
的另一边取得,而在第二种情况下未指定类型文本的类型将从目标列取得。
出于这样的目的,RETURNING
列表采用和SELECT
输出列表同样的方式对待。
注意:
在PostgreSQL 10之前,这条规则还不存在,SELECT
输出列表中未指定类型的文本的类型会被留成unknown
。这样做会导致各种不好的后果,因此新版本中做出了改变。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。