PostgreSQL技术问答20 - SQL语法

本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:

《PostgreSQL技术问答00 - Why Postgres》

文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。逻辑上每篇文章都是独立的,读者进行阅读时,不用太关注这个方面。

本文主要的内容是笔者想要以PostgreSQL为例,讨论和表达对SQL语言和语法的理解。

什么是SQL

SQL的完整意思是Structure Query Language,结构化查询语言。也就是说,它实际上也是一种计算机编程语言和体系,是主要用于数据库系统的数据查询(其实也包括数据操作和其他管理功能)的。但和一般人类使用的自然语言的灵活性不同,受到一些技术方面的限制,它需要使用一种事先约定好的标准结构化的范式,才能让数据库系统能够正确的理解和执行。这个名称是非常精到的,就是数据库系统使用的结构化-查询(操作)-编程语言。

但其实,这个名称,并没有体现SQL作为一种编程语言,和普通的计算机编程语言的区别。就是虽然是结构化的,但SQL语言的语法基础和框架,其实是模拟人类使用的自然语言,更准确的说,就是英语。

和一般编程语言相比,SQL的主要问题是缺乏变量的管理,以及流程控制和结构。虽然各个数据库系统也都提供了相应的Produce Language,就是存储过程语言。但笔者使用的经验而言,相对于常规的编程语言,它们的编写和调试其实不是很方便,语法也比较古怪繁琐,过程和环境也不是很友好。归根结底的原因,可能还是作为针对数据集合的偏向自然语言语法的指令性语言,和需要精确控制过程的计算机语言之间的结构性冲突造成的,这么一个有点尴尬的状态。

笔者如何理解SQL语法

作为一个半路出家的开发者,笔者虽然觉得自己对SQL已经非常熟悉和了解,日常也经常使用。但如果想到,如果想要把这些理解和经验分享出来,却突然发现,好像并没有认真思考关于SQL的相关理论的问题。因此,笔者稍微研究了一下Postgres的官方文档的相关章节,发现这是一个非常好的理解的框架。就是下面这个链接:

《Part II. The SQL Language》

在这个目录和结构中,关于SQL语法相关的内容和表述,主要分为三个部分:

  • Lexical Structure 词汇结构
  • Value Expressions,值表达式
  • Calling Functions 调用函数

除了语法之外,其他相关的功能和特性模块,主要包括以下部分:

  • Data Define 数据定义
  • Data Manipulaction 数据操作
  • Query 查询
  • Data Types 数据类型
  • Operators And Functions 操作符和函数
  • Type Conversion 类型转换
  • Index 索引
  • Full Text Search 全文搜索
  • Concurrency Control 并发控制
  • Performance Tips 性能考量
  • Parallel Query 并行查询

其实SQL的主要内容和结构,就是这些内容。本文的主要内容还是SQL语言和语法本身相关的,所以主要关注在第一部分。下面我们分别展开讨论。

如何理解Lexical Structure

SQL的词汇结构,包括标识和关键字,常数,操作符,特殊字符,操作符优先等方面的内容。从结构上而言,一个完整的SQL指令,可以由一个或者多个SQL语句构成,语句之间使用逗号分隔。每个SQL语句,构成了可以系统被解析和运行的最小程序化单元。SQL语句基本上是结构化的人类自然语言的语句,基本上就是英语的语句。但是这些语句必须满足一定的结构和语法规范,让系统能够正确的解析和执行。这些结构和规范,包括了正确的关键字、标识、表达式和顺序结构,来构成完整的执行指令。SQL中数据查询和操作的功能,就是基于这些指令来提供和实现的。

关键字 Keyword

SQL约定了一套关键字,来标识当前指令的类别和功能。这些关键字是保留的,只能为系统所使用,用户需要使用的参数和变量,不能和这些关键字冲突。常见的SQL关键字构成了SQL语句的基本框架,如select ... from ... where ... order by ..., 就构成了基本的查询语句的基本结构。然后,通过一些约定和固定的结构组合,来组合成为更复杂的SQL语句,来提供更复杂的功能。

一般情况下,在一个SQL语句中,关键字有一定的组合关系,包括它们之间的先后顺序,这样的设计,也是为了保证解析程序能够正确解析,同时满足一般人类表达的逻辑和习惯。

Postgres中,关键字不分大小写,它们是等效的,所以是完全保留的。

在所有的编程语言中,作为某种形式表述系统,都具有关键字的存在。差异就是保留关键字集合的大小和是否易于理解和记忆。因为关键字作为保留字,容易和变量名称产生冲突。SQL作为指令性的语言,相比一般编程语言的关键字集合显然更大一点,再加上Postgres不区分大小写,这些都需要开发者理解和适应。

标识 Identifier

关键字的组合,只能构成SQL的语法框架,但如果要使这个框架来适应各种应用场景,就需要结合实际的数据结构和存储要素,比如特定的数据表和字段表达等等。这些元素,是属于当前数据库的定义的,在不同的应用和数据库中,是不同的,通常称为标识。常见的标识就是很多数据库系统中的对象,比如架构、表、字段等等。

常量 Constant

在PostgreSQL中,常量就是以自己的值表示的不可修改的"变量"。PG包括字符串、位字符串和数字等三种隐式常量。下面我们结合一些示例进行说明:

sql 复制代码
// 一般数字常量
42
3.5
4..001
5e2
1.925e
-3

// 非十进制 
0b100101
0B10011001
0o273
0O755
0x42f
0XFFFF

// 视觉分组
1_500_000_000
0b10001000_00000000
0o_1_755
0xFFFF_FFFF
1.618_034

显式常量

除了上面讨论的隐式常量之外,对于一些非标量的常量,和需要特别指定类型的常量,可以使用显式常量或者类型转换的方式来定义。这些通常使用一个字符串表示和相关的转换方法类实现,如下面的一些例子:

sql 复制代码
-- 声明样式
REAL '1.23' 

-- PostgreSQL(历史)风格
1.23::REAL 

-- 标准Cast转换函数
cast('1.23' as REAL); 

-- 类型(转换)函数,注意使用限制,需要指定具体类型

float8('2.5'); 

转义符

Postgres中的字符串常量,支持C语言风格的转义符。PostgreSQL中,常见的转义符包括:

  • \b 退格符
  • \f 换页符
  • \n 新行符
  • \r 回车符
  • \t 制表符
  • \o... 八进制字节
  • \x... 十六进制字节
  • \u... Unicode编码字符值
  • \\ 反斜杠自己的转义

操作符 Operator

在Postgres中,操作符可以是以下这些字符的有序组合:

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

我们通常很熟悉标准的编程语言操作符,Postgres也支持这些常规项目。如+-等等。但PG的强大之处在于,它支持更多的内置组合式操作符,来提高更丰富的功能特性,比如 ->> 可以作为JSON对象熟悉访问操作符。另外,Postgres还提供了自定义操作符,让开发者可以扩展操作符的使用方式。

操作符的定义有以下一些限制和例外:

  • 不能以"--"开头,因为那表示注释信息
  • +和-开头或者结尾的操作符,必须包括 ~!@#%^&| `?中的最少一个字符,如@-是有效的,而*-不是
  • 使用非SQL标准操作符时,通常需要用空格分隔相邻的运算符以避免歧义

笔者认为,操作符的本质是一个"语法糖",因为所有的操作符,它都用于连接其左右的元素来进行一定的操作,所以所有操作符从理论上都可以表述称为一个有一个或者两个参数的函数调用。如 a + b,改写称为函数的形式就是 add(a,b)。这应该就是Postgres可以支持自定义操作符的理论基础。就是可以先定义一个函数,包括输入参数、处理过程和输出结果,然后定义一个特点的操作符组合来替换它,就可以在系统中使用操作符来进行操作了。

操作符的使用,可以大大简化函数调用需要编写的代码。虽然过多的定义和规则,提高了一些学习和理解的门槛。但比如像 || 来连接字符串,显然就比contact() 函数的实现要更简洁优雅。

函数 Function

Postgres的SQL语句中,支持函数的调用。函数的标准形式和普通编程语言类似,就是 函数名(参数...)。Postgres系统内置了很多功能性的函数,也提供了自定义函数的扩展方法。使用的时候,就需要特别注意这些函数的名称,最好不要和已有的关键字、标识符等冲突,以避免不必要的麻烦。

特殊字符

在PG中,还包括一类特殊字符,它们都是非字母和数字字符,并且不同于操作符,主要用于完整和丰富SQL的语义。常见的特殊字符包括:

  • 分号 ";"

用于终止SQL指令。除了作为字符串常量或使用带引号的表示,它自身不能出现在SQL语句的中间。

  • 逗号 ","

在语法结构中用于分隔列表的元素。这些列表可能包括标识符,变量等等

  • 美元符号 "$" (其实是位置符号)

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

  • 小括号 "()"

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

  • 中括号 "[]"

用于表示和选择数组的元素。

  • 冒号 ":"

用于从数组中通过定义起始位置,来选择"切片"(子数组) 。

  • 星号 "*"

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

  • 句点 "."

SQL语句中的句点,用于分隔架构、表和列名称。

  • 下划线 "_"

可以作为普通字符用在标识符中,也可以用作数字常量中,通常作为视觉分组使用。

操作符和运算符优先级

算符/元素 结合性 描述
. 表/列名称分隔符
:: PostgreSQL风格的类型转换
[ ] 数组元素选择
+ - 一元加减
COLLATE 排序规则选择表
AT AT TIME ZONE
AT 求幂
* / % 乘除模
+ - 加减
其他运算符 所有其他本地和用户定义的运算符
BETWEEN IN LIKE ILIKE SIMILAR 范围包含、集合包含、字符串匹配
< > = <= >= <> 比较运算符
IS ISNULL NOTNULL 逻辑检查
NOT 逻辑非
AND 逻辑与
OR 逻辑或

注释

在标准SQL中,注释信息的语句,应当由 "--" 引领开头,并以分号或者换行符结束。实际上Postgres支持C语言风格的注释语法。即使用 /* ... */ 来包围注释信息。

什么是值表达式(Value Expressions)

值表达式是SQL中一个非常重要的概念和组成部分。它被用于多种上下文中,例如命令结果字段列表、搜索匹配条件、查询数据源等等。值表达式的结果有时称为标量(Scalar),以将其与表表达式(即记录集)的结果区分开。因此,值表达式也称为标量表达式(甚至简称为表达式)。

在PostgreSQL中,有很多中值表达式的实现方式,包括:

  • 常量或字面值
  • 列和字段引用
  • 函数引用
  • 预备语句中的位置引用
  • 带下标的表达式,比如数组类型的字段和下表
  • 运算符调用
  • 函数调用
  • 聚合表达式
  • 窗口函数调用
  • 类型转换
  • 排序表达式
  • 数组构造
  • 行构造
  • 括号中的另一个值表达式(用于对子表达式进行分组并提高优先级)

关于这些方式的具体内容在本文中不做进一步讨论,而是在系列文章中,作为相关专题进行研究。

什么是表表达式

以Select语句为例,我们可以看到,在Select之后,和From之前,使用了值表达式如字段应用等,来构成一个结果记录集结构,但是在From之后,好像这个结构,并不是值表达式,而是一个表表达式,用来构造查询的数据源。同样类似的结构,包括Update、Insert Into和Delete From后面的数据表标识,也是类似的。这种构造,我们可以称为表表达式。

表表达式,在数据操作语句中,比较简单,就是所要操作的数据表。而在查询操作中,它的形态是比较多的。除了表之外,任何可以构造成为表形式的数据集合操作,都是可以的。所以,和值表达式类似,在Postgres常见的表表达式也有多种形态,它们包括:

  • 数据表引用
  • 视图和实体化视图
  • CTE
  • 子查询(非标量)
  • Returing的结果
  • 自定义函数,结果为Query和Result Set

除此之外,还包括一些表生成函数(Table Functions),它们的输出结果是一个记录集,如

  • unnest: 数组展开为一组数据行
  • string_to_table: 可以将一个字符串转换为一个单列记录集
  • regexp_split_to_table:基于正则表达式分拆字符串到数据集
  • generate_series:生成一个数列,从start开始,到stop结束,步进为step(默认1)
  • generate_subscript:生成一个包含数组下标的表
  • json_each(json):将JSON 对象展开为键/值对
  • json_array_elements(json):将 JSON 数组展开为一组行。
  • xmltable(xpath, columns):从 XML 文档中提取数据,并将其展示为表格形式。

这些表表达式的结果,都可以作为查询使用的数据源,并作为一个普通的虚拟的表来对待,可以进行如数据关联、字段计算等操作。

如何理解调用函数的规则

就是在Postgres的SQL语句中,调用和执行函数,特别是自定义和带有参数的函数,是有其相应的规则和方式的。并且这个规则,和函数的定义方式有一定的关联。下面借用官方技术文档中的例子来解释一下:

sql 复制代码
// 函数定义
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;

// 参数位置
SELECT concat_lower_or_upper('Hello', 'World', true);


// 参数位置和可选项

// 命名参数
SELECT concat_lower_or_upper(a => 'Hello', b => 'World');
SELECT concat_lower_or_upper(a => 'Hello', uppercase => true, b => 'World');


// 旧语法
SELECT concat_lower_or_upper(a := 'Hello', uppercase := true, b := 'World');


// 混合形式
SELECT concat_lower_or_upper('Hello', 'World', uppercase => true);

从这个例子中,我们看到,在PG中调用函数和使用参数是非常灵活的,并可以总结出以下要点:

  • 函数的定义,是可选参数和默认值的
  • 可以定义返回类型和值,使函数可以被用于SQL表达式中
  • 参数标记可以使用位置方式、命名参数和混合形式
  • 位置方式需要参数严格按照定义顺序提供
  • 命名方式可以不需要按照定义顺序,但需要指定参数名称
  • 可以混合使用位置和命名方式

举个简单的例子

下面我们简单的总结一下,在一个普通的SQL语句中,通常的语法结构和构成是怎样的:

sql 复制代码
select -- 查询操作,关键字
department,  - 字段标识符
count(1) ecount - 聚合函数表达式
from -- 配合Select,关键字
users -- 数据源表
where salary > 1000 -- 条件子句关键字, 表达式,标识符 操作符 常量 
group by deparment -- 聚合计算子句, 字段表达式
order by ecount desc -- 排序子句, 字段表达式, 倒序关键字

理解SQL语句的结构,对于我们理解数据库系统如何解析和执行SQL,包括对于我们编写合理、清晰、高效的SQL操作语句,都有很大的帮助。

Postgres如何处理SQL语句

Postgres系统也是一个典型的客户端/服务器系统。Postgers服务器,在和客户端连之后,���户端可以向服务器发送网络请求,请求的内容,就是这些SQL语句和指令。当然,不同的数据库系统,可能还有自己有特色的系统级命令,我们这里主要粗略的探讨一下标准SQL语句的解析和执行过程。

由于SQL语句是一种规范的结构化语句。虽然其语义是一种自然语言,但对计算机系统而言,它其实非常像一种脚本语言,可以完全按照脚本语言的解析方式,来进行语法分析和结构。所以,数据库系统对其进行解析和执行,就可以做得比较标准和模式化。

下面是一个简单的例子(图),当数据库系统接收到一个Selece语句后,它可以按照标准的Select语句结构,将语句分为不同的部分比如字段选择子句、条件子句、关联子句、排序子句等等来进行解析,最后将其拆解成为一系列更加细化的数据库对象,和需要对其进行的操作,然后生成一系列像普通计算机程序一样的操作程序,来最终完成数据存取和计算操作。

当然,实际的解析和执行,要考虑更多更复杂的情况,那是数据库系统的开发者需要考虑的问题。这里作为应用开发者我们只需要理解其最基本的原理和概念,从而帮助我们可以编写更加合理的SQL指令,就可以了。

小结

本文主要从理论和框架的角度,探讨了SQL语言,作为一种编程语言在Postgres中的体现和实现。包括了SQL的基本概念,语法特点,主要组成,解析和工作原理等。并且明确和辨析了其中关于开发和应用的一些重要的概念和问题,希望对后端开发者提升认知和能力有所助益。

相关推荐
David爱编程3 分钟前
Java 守护线程 vs 用户线程:一文彻底讲透区别与应用
java·后端
用手编织世界9 分钟前
redis-缓存-双写一致性
数据库·redis·缓存
小奏技术21 分钟前
国内APP的隐私进步,从一个“营销授权”弹窗说起
后端·产品
小研说技术39 分钟前
Spring AI存储向量数据
后端
苏三的开发日记39 分钟前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台处于同一台服务器)
后端
苏三的开发日记40 分钟前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台不在同一服务器)
后端
陈三一1 小时前
MyBatis OGNL 表达式避坑指南
后端·mybatis
whitepure1 小时前
万字详解JVM
java·jvm·后端
我崽不熬夜1 小时前
Java的条件语句与循环语句:如何高效编写你的程序逻辑?
java·后端·java ee
smilejingwei1 小时前
数据分析编程第二步: 最简单的数据分析尝试
数据库·算法·数据分析·esprocspl