词法结构#

SQL输入由一个命令序列组成。一个命令由一个记号序列构成,并由一个分号(“;”)终结。输入流的末端也标志着一个命令的结束。具体哪些记号是合法的与具体命令的语法有关。

一个记号可以是一个关键词、一个标识符、一个带引号的标识符构成,一个literal(或常量),或一个特殊字符符号。记号通常以空白(空格、制表符、新行)来分隔,但在无歧义时并不强制要求如此(唯一的例子是一个特殊字符紧挨着其他记号)。

例如,下面是一个(语法上)合法的SQL输入:

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

这里是三条命令的序列,每条一行(尽管并不要求这么做:多条命令可以在一行里, 单条命令也可以合理地分成多行)。

如果考虑哪些记号标识命令,哪些参数是操作数或参数, SQL的语法并不是十分一致的。通常前几个记号是命令名,因此在上个例子中我们通常说一个“SELECT”,一个”UPDATE”,和一个”INSERT”命令是命令名。然而,UPDATE操作总是要求在特定位置出现一个SET记号,而且INSERT命令则需要一个VALUES记号来完成。

备注

ROW/ORC/Hudi/MAGMA格式的表支持UPDATE操作,OushuDB 暂时不支持分布键和主键的UPDATE操作;详情参阅 更新数据

标识符和关键字#

上例中给出的如SELECT, UPDATE, 或 VALUES都是在SQL语句中有特定含义的记号,即关键字。记号MY_TABLE和A是标识符,根据使用它们的命令不同,它们标识表名,列名或者其他的数据库元素。因此,它们有时候被称为”名字”。关键字和标识符有同样的词法结构,意味着在不懂这种语言时很难判断一个记号是标识符或关键字。

SQL的标识符和关键字必须以一个字母(a-z,以及带变音符的字母和非拉丁字母)或下划线(_)开头,随后的字符可以是字母、下划线、数字(0-9)、美元符号($)。需要注意的是,根据SQL标准,美元符号不允许出现在标识符中,因此使用美元符号将不易移植。SQL 标准不会定义包含数字或者以下划线开头或结尾的关键字, 因此这种格式的标识符是安全的,不会和将来标准的扩展特性冲突。

关键字和未被引号包围的标识符都是大小写无关的。因此:

UPDATE MY_TABLE SET A = 5;

也可以等效地写成:

uPDaTE my_TabLE SeT a = 5;

一种好习惯是把关键字写成大写,而名字等用小写:

UPDATE my_table SET a = 5;

还有第二种标识符:分隔标识符或 引号包围的标识符。 它是在双引号(“)中包围任意字符序列形成的。分割标识符总是一个标识符,而不是关键字。因此你可以用“select”表示一个列名或者表名,然而一个没有引号的select将会被视为一个关键词,如果将它当作表名或列名时就会导致一个解析错误。上面的例子可以用引号包围的标识符这么写:

UPDATE "my_table" SET "a" = 5;

引号包围的标识符可以包含编码不等于零的任意字符(如要包含一个双引号,需要写两个相连的双引号)。这样我们就可以构造那些原本是不允许的表名或者字段名,比如那些包含空白或与号(&)的名字。但长度限制依旧。

虽然不带引号的标识符总是会被折叠为小写,但是一个带引号的标识符是大小写敏感的。例如,标识符FOO, foo和”foo”被 OushuDB 视为相同的,但是 “Foo”和”FOO”与这三者不同。(OushuDB 将不带引号的标识符折叠为小写与SQL标准不相容,SQL标准将不带引号的标识符折叠为大写。因此,foo 应该等价于SQL标准的”FOO” 而不是”foo”。如果你希望程序有较高的移植性,你应该一直使用带引号的标识符或一直使用不带引号的标识符。)

常量#

在 OushuDB 里有三种隐含类型的常量: 字符串、位串、数值。常量也可以声明为明确的类型, 这样就可以使用更准确的表现形式以及可以被系统更有效地处理。这些将在后面的小节描述。

字符串常量#

SQL 里的一个文本常量是用单引号(‘)包围的任意字符序列, 比如’This is a string’ 。 在这种类型的字符串常量里嵌入单引号的标准兼容的做法是敲入两个连续的单引号, 比如’Dianne’’s horse’。注意:两个连续的单引号不是双引号(“)。

两个只是通过至少一个换行符的空白分隔的字符串常量会被连接在一起, 并当做它们是写成一个常量处理。比如:

SELECT 'foo'
'bar';

等价于

SELECT 'foobar';

但:

SELECT 'foo'      'bar';

是非法的语句。(这个怪异的行为是SQL声明的,OushuDB 下仍遵循标准。)

C风格的转义字符串常量#

OushuDB 还允许”转义”字符串中的内容, 这是一个 OushuDB 对SQL标准的扩展。转义字符串语法是通过在字符串前写字母E (大写或者小写)的方法声明的。比如E’foo’。(当需要续行包含转义字符的字符串时, 仅需要在第一行的开始引号前写上E就可以了)。在转义字符串中, 通过一个反斜杠(\)开始C风格的反斜杠转义序列,在该转义中, 反斜杠与其之后字符的组合代表一个特殊的字节值。

反斜杠转义序列

解释

\b

退格

\f

进纸符,将光标位置移到下一页开头

\n

换行

\r

回车

\t

水平制表符

\o,\oo,\ooo(o=0-7)

八进制字节值

\xh,\xhh(h=0-9,A-F)

十六进制字节值

\uxxxx,\uxxxxxxxx(x=0-9,A-F)

16或32位十六进制Unicode字节值

小心

如果配置参数standard_conforming_strings的值是off,那么 OushuDB 将能够识别常规和转义字符串常量中的反斜杠转义,这是为了与历史行为相兼容。虽然现在参数standard_conforming_strings默认值为off,为了提升标准的兼容性,以后发布的版本可以会将其默认值修改为on。因此程序建议不使用反斜杠转义。如果你需要使用反斜杠转义来表示特殊的字符,那么请在字符串常量前加上E。

编码为零的字符不允许出现在字符串常量中。

美元符引用字符串常量#

尽管声明字符串常量的标准方法通常都很方便,但是如果字符串中包含很多单引号或者反斜杠,那么理解字符串的内容可能就会变得困难,因为此时需要将每个单引号都加倍用来表示一个单引号。 为了让这种场合下的查询更具可读性,OushuDB 允许另外一种称作 “美元符引用”的字符串常量书写办法。一个美元符引用字符串通常由一个美元符号($)、零个或多个字符组成的可选“标签”、另一个美元符号、一个组成字符串常量的任意字符序列、一个美元符号、与前面相同的标签、一个美元符号组成。比如,下面是两个不同的用美元符引用的方法声明“Dianne’s hors”的例子:

$$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\ \ ],在函数体被 OushuDB 执行的时候,它将被识别出来。 但是因为这个序列不匹配外层的美元符引用分隔符$function$ ,所以只要考虑了外层字符串, 它就只是常量里面的普通字符而已。

一个美元符引用字符串的标签(如果有标签的话),遵循和无引号包围的标识符相同的规则, 只是它不能包含美元符。标签是大小写敏感的,因此$tag$String content$tag$ 是正确的,而$TAG$String content$tag$则是错误的。

一个后面紧跟着关键字或者标识符的美元符引用字符串必须用空格与其后的关键字或者标识符隔开; 否则美元符引用分隔符将会被当作标识符的开头部分。

美元符引用不属于SQL 标准,但是在写复杂的字符串文本的时候,它通常比标准的单引号语法更方便。尤其是在其它常量里表现字符串常量的时候更有用,比如在过程函数定义里。如果用单引号语法, 每个上面例子里的每个反斜杠都必须写四个,它们在作为字符串文本分析的时候会减少为两个, 然后在函数执行的时候在内层字符串常量里会再次被解析为一个。

位串常量#

位串常量看起来很像在开引号前面有一个B(大写或小写)的普通字符串 (它们之间没有空白),比如B’1001’。位串常量里可以用的字符只有0 和1。

另外,位串常量可以通过使用前缀X(大写或者小写)实现十六进制声明,比如X’1FF’,其中的每个十六进制位等效于四个二进制位。

两种形式的位串常量都可以像普通字符串常量那样跨行连续。但美元符不能用于位串常量。

数值常量#

数值常量的通用形式如下:

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类型中(32位),则认为它是integer类型; 如果它的数值可以放在bigint中(64位),则认为它是bigint, 否则认为它是numeric类型。包含小数点或指数的常量总是被认为是 numeric类型。

给一个数值常量赋予初始数据类型只是类型解析算法的开端。 在大多数情况下该常量会根据上下文被自动强制转换成最合适的类型。 必要时,你可以通过强制类型转换把一个数值解析成特定的数据类型。 比如,你可以强制要求把一个数值当作real (float4)类型来看,方法是这么写:

REAL '1.23'  -- 字符串风格
1.23::REAL   -- OushuDB (历史的) 风格

这些实际上只是下面要讨论的通用转换的特例。

其他类型的常量#

任意类型的常量都可以使用下列的任意一种表示法:

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

其中字符串常量的文本将会被代入到类型type 的输入转换过程。其结果是一个该类型的常量。 如果不存在该常量所属类型的歧义,那么可以省略明确的类型转换(比如,当你把它直接赋予一个表列的时候), 这种情况下它会自动转换。

其中的字符串常量可以用普通 SQL 表示法或者美元符引用来书写。

我们还可以用函数风格的语法来声明类型转换:

typename ( 'string' )

不过并非所有类型名都可以这样使用;详见 类型转换

::,CAST() 和函数调用语法也可以用于声明任意表达式的运行时类型转换(如 类型转换 中讨论的那样)。但 type ‘string’ 的形式只能用于声明字面常量的类型。 type ‘string’ 的另一个限制是它不能用于数组类型(可用::或 CAST()声明一个数组常量的类型)。

CAST()语法遵循 SQL 标准。 type ‘string’ 语法是标准的一个推广:SQL 只是给少数几种数据类型声明了这个语法,但 OushuDB 允许将其用于所有类型。::和函数调用的语法是 OushuDB 的历史用法。

操作符#

操作符是一个最多长度(默认为63)个下列字符的序列:

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

不过,有几个限制:

  • - - 和 /* 不能出现在操作符中的任何地方, 因为它们会被当做注释开始对待。

  • 多字符操作符不能以+ 或 -结束,除非其中至少还包含下列操作符之一: ~ ! @ # % ^ & | ` ?

    比如,@-是允许的操作符,但*-不是。 这个限制允许 OushuDB 在不要求记号之间有空白的情况下分析 SQL 兼容的查询。

当你使用非 SQL 标准的操作符的时候,你通常需要用空白分隔相邻的操作符以避免歧义。 比如,如果你定义了一个叫@的左单目操作符,那么你就不能写成X*@Y;而是要写成X* @Y以确保 OushuDB 把它读成两个操作符,而不是一个。

特殊字符#

有些非字母数字字符有一些不同于操作符的特殊含义。它们的用法细节可以在相应的描述语法元素的地方找到。 本节只是描述它们的存在和概括一下这些字符的目的。

  • 美元符号($)后面跟着数字用于在一个函数体定义或者预备语句中表示参数的位置。 在其它上下文中美元符号可能是一个标识符名字或者是一个美元符引用的字符串常量的一部分。

  • 圆括弧(())通常用于分组和强制优先级。在有些场合里,圆括号是作为一个特定的SQL 命令的固定语法的一部分。

  • 方括弧([])用于选取数组元素。

  • 逗号(,)在一些语法构造里用于分隔一个列表的元素。

  • 分号(;)结束一条 SQL 命令。它不能出现在一条命令里的任何地方,除了在引号包围的字符串常量或者标识符中。

  • 冒号(:)用于从数组中选取”片段”。在一些 SQL 方言里(比如嵌入 SQL),冒号用于前缀变量名。

  • 星号(*)在某些上下文里表示一个表的全部字段或者一个复合类型的值。 在用作聚合函数的参数时,表示该聚合并不需要明确的参数。

  • 句点(.)用在数字常量里,并用于分隔模式、表、列名称。

注释#

注释是以双划线开头并延伸到行尾的任意字符序列,比如:

-- 这是标准的 SQL 注释

另外,还可以使用C风格的块注释:

/* 多行注释
* 可以嵌套: /* 被嵌套的块注释 */
*/

这里注释以/*开头到对应*/的结束。这些块注释可以嵌套,就像 SQL标准里说的那样(但和 C 不一样), 因此我们可以注释掉一大块已经包含块注释的代码。

注释在进一步的语法分析之前被从输入流中删除并用空白代替。

操作符优先级#

下表显示了 OushuDB 里面的操作符的优先级和关联性。大多数操作符都有相同的优先级并且都是左关联的。操作符的优先级和关联性是硬连接到解析器的。这种情况可能会有不那么直观的行为; 比如,布尔操作符<和>与布尔操作符<=和 >=之间有着不同的优先级。同样,当你把双目和单目操作符组合使用的时候, 有时候也需要加圆括弧。这是我们为扩展性付出的代价。

表.操作符优先级(递减)

操作符元素

关联性

描述

.

表/列名分隔符

::

OushuDB 特有的类型转换操作符

[]

数组元素选择

+-

单目正号,单目负号

^

*/%

乘、除、模

+-

加、减

IS

IS TURE, IS FALSE, IS NULL, etc

ISNULL

测试是否为NULL

NOTNULL

测试是否不为NULL

(任何其他的)

所有其他的本地和用户定义操作符

IN

集合成员

BETWEEN

范围包含

OVERLAPS

时间间隔重叠

LIKE ILIKE SIMILAR

字符串模式匹配

< >

小于,大于

=

等于,赋值

NOT

逻辑非

AND

逻辑与

OR

逻辑或

请注意操作符优先级也适用于和上面提到的内置操作符同名的用户定义操作符。比如,如果你为一些客户数据类型定义一个”+”操作符, 那么它和内置的”+”操作符有同样的优先级,不管用它来干什么。

如果在OPERATOR语法里使用了模式修饰的操作符名,比如:

SELECT 3 OPERATOR(pg_catalog.+) 4;

那么OPERATOR构造就会有操作优先级表里面为”任何其它的” 操作符显示的缺省优先级。不管什么特定的操作符出现在 OPERATOR()里都是这样。