SQL(Structured Query Language,结构化查询语言)是一种声明式语言,用于在关系数据库管理系统中创建、查询、修改和控制数据。用户只需说明想要得到的结果,由数据库引擎决定如何计算。其正式名称为 ISO/IEC 9075《数据库语言 SQL》(Database Language SQL),当前版本为 SQL:2023。它源于 IBM 在20世纪70年代对关系理论的研究,如今已成为计算领域应用最广泛的语言之一。几乎所有存储结构化数据的软件中都嵌入了 SQL,从银行和企业系统到网络应用、科学数据分析以及数据仓库,无所不包。
SQL 之所以重要,有两个原因。其一,它让关系模型变得切实可用,把集合论和逻辑学中的抽象概念变成了可以大规模存储和检索数据的实用工具。其二,它的标准化加上各厂商的方言,形成了一个生态系统:技能和许多应用可以在不同产品之间迁移,尽管可移植性从来都不是完美的。该语言也长期受到批评,主要是因为它在重复行和缺失值等方面偏离了纯粹的关系模型,这种张力塑造了它的大部分设计,也影响了后来的各种替代方案。
本词条首先介绍 SQL 的起源及其所实现的关系概念,然后讨论其主要语言组成部分、查询语义、数据类型与 NULL 处理、约束、事务以及查询优化。随后介绍标准化与各厂商方言、过程化扩展、高级分析功能、半结构化数据、模式设计、安全性、应用程序接口、大数据与分析场景的应用,以及批评与替代方案,最后讨论 SQL 在人工智能辅助查询中日益重要的角色。
起源:从关系理论到SEQUEL
1970年之前,主要的数据库系统基于层次模型和网状模型,程序通常必须沿着存储的链接访问数据,因此依赖于数据的物理布局。1970年,关于如何组织数据库存在两种思路:层次模型和网状模型。层次模型被IBM的信息管理系统(IMS)采用,这是当时占主导地位的数据库系统。1970年6月,就职于IBM圣何塞研究实验室的埃德加·F·科德(Edgar F. Codd)在《ACM通讯》(Communications of the ACM)上发表了论文《大型共享数据库的数据关系模型》("A Relational Model of Data for Large Shared Data Banks")。该论文建立在n元关系、关系范式以及通用数据子语言的基础之上,旨在使用户无需了解机器的内部表示。其关键思路是把数据的逻辑描述(数据的含义以及可以对其提出什么问题)与物理存储(布局、访问路径以及在磁盘上的位置)分离开来。科德将这一性质称为数据独立性:存储方式发生变化时,应用程序应当仍能正常工作。该模型以集合论和一阶逻辑为数学基础,这使它在理论界颇具吸引力,尽管商业系统采用它的速度较慢。
由于现有的层次型产品非常成功,IBM最初并不愿意探索这一想法。直到1973年,IBM才启动了System R项目,用以探索科德关于关系数据模型的思想。在System R项目开始之前,唐纳德·D·张伯伦(Donald D. Chamberlin)和雷蒙德·F·博伊斯(Raymond F. Boyce)曾设计了一种名为SQUARE的语言,它用数学符号表达关系查询,其中的下标很难输入。张伯伦和博伊斯决定对其加以改造,使其结构更接近英语句子,也更易于输入。结果便是SEQUEL(Structured English Query Language,结构化英语查询语言)。1974年,张伯伦和博伊斯发表了《SEQUEL:一种结构化英语查询语言》("SEQUEL: A Structured English Query Language"),详细介绍了他们对SQUARE的改进,并引入了SEQUEL的数据检索部分。SEQUEL是为配合System R而设计的,System R是IBM的一个研究原型,被描述为准关系系统,因为它并未实现完整的关系模型。
名称因法律原因而改变。缩写SEQUEL后来改为SQL,因为"SEQUEL"是英国公司Hawker Siddeley Dynamics Engineering Limited的商标。随后出现了商业产品。关系软件公司(Relational Software, Inc.)后来更名为甲骨文公司(Oracle Corporation),它推出了首个商用SQL实现——适用于VAX计算机的Oracle V2(Version2)。IBM自身的产品出现得更晚:IBM开始基于其System R原型开发商业产品,包括System/38、SQL/DS和DB2,分别于1979年、1981年和1983年投入商用。一些流行的历史叙述将1981年视为IBM首个商用SQL产品的年份,因此早期版本的确切先后顺序取决于如何界定"SQL产品"。甲骨文和IBM公司的产品通常被认为是最早的两大商业产品线。
20世纪70年代的一个核心问题是:声明式语言(用户只说明所需结果,而不说明获取结果的过程)能否运行得足够快,从而与手工导航的程序竞争。System R项目在一定程度上正是对这一主张的检验。其答案是一个查询优化器:它会考虑执行查询的多种方式(例如使用哪个索引、如何连接表),并利用存储数据的统计信息选出代价最低的方案。帕特丽夏·塞林格(Patricia Selinger)及其同事于1979年发表的论文描述了该优化器的工作,论文表明用户无需指定访问策略。查询"要什么"与系统"如何执行"之间的这种分离,是SQL成为关系数据库主流接口、而不仅仅是一种研究性记号的主要原因之一。
关系理论基础及 SQL 对其的偏离
从关系到表
埃德加·科德(Edgar Codd)1970 年的论文写于他在 IBM 工作期间,它将数据视为集合论意义上的关系:由若干元组构成的集合,每个元组对每个具名属性都有一个取值。在日常理解中,关系就是一张表:每一行是一个元组,每一列是一个属性。这种对应关系大体成立,但并不精确。SQL 表有一组固定的具名列,其主体是若干行的集合,而标准并不要求这些行构成集合。科德还主张属性应通过名称而非位置来标识,并认为列的顺序无关紧要。SQL 保留了具名列,但在其系统目录以及 SELECT * 的输出中确实规定了列的顺序。
科德的核心目标是将数据的逻辑描述与物理存储分离,使用户只需说明需要哪些行,而无需指定它们在磁盘上如何排列。正是这种分离,使表而非早期导航式系统中由指针相连的记录,成为数据描述的基本单位。这一分离对 SQL 自身设计的影响,将在"起源"一节中讨论。
键与行标识
在关系模型中,超键是能够唯一确定一个元组的任意属性集合。候选键是极小的超键,即去掉其中任何一个属性都会破坏唯一性。主键是被选定用来标识关系中各行的候选键。科德 1970 年的论文以主键作为关联关系的机制:外键是这样一组属性,其取值必须与另一关系的主键取值相匹配。实体完整性要求主键值永远不能为未定义。
SQL 通过 PRIMARY KEY、UNIQUE 和 FOREIGN KEY 约束来表达这些概念,但执行方式并不完全相同。表可以完全没有键,这时其各行除内容外没有任何标识,两条完全相同的行也无法被任何查询区分开。当涉及空值时,UNIQUE 约束同样偏离了理论。依据标准,在判断唯一性时,一个空值不等于另一个空值,因此在受约束的列上含空值的多行可以同时存在。这样的列因此并不严格地充当候选键。各数据库厂商在其唯一索引允许多少个空值方面也存在差异。
SQL 偏离之处:重复与顺序
科德的关系是集合,因此任意两个元组都不相同,元组本身也没有固有顺序。而 SQL 表则是多重集(bag)。在纯集合论中,集合不含重复元素;然而 SQL 的实际设计默认采用多重集语义,这意味着查询结果中可以出现重复行。例如,从客户表中只选取城市列的投影,可能会多次返回同一城市;表中也可以存储多条完全相同的行。
设计者出于性能考虑做出了这一选择。IBM 研究人员 Chamberlin 与 Boyce 在创建 SEQUEL(后来的 SQL)时,基于性能原因选择了多重集语义,并增加了 DISTINCT 作为显式的去重运算符。去重需要对整个结果进行排序或哈希,会消耗时间和内存,因此除非查询明确要求删除,SQL 都会保留重复项。DISTINCT 关键字,以及 UNION 与 UNION ALL 之间的区别,是请求集合行为的常用方式。
行的顺序也遵循类似的模式。表本身没有固有的次序,因此除非查询包含 ORDER BY 子句,结果中行的顺序是未定义的。只有当查询提出要求时,SQL 才依赖顺序。
对正确性的影响
偏离集合语义改变了哪些代数定律成立。对集合而言, 是恒等式。在多重集上它不再成立,因为一个多重集与自身求并会使每个重数翻倍,而 SQL 自身的多重集运算符正是遵循这一规则。许多常用技巧依赖于集合定律,因此在多重集上可能出错。
连接是最常见的出错来源。连接会使重数相乘:若一张表中某行出现两次,另一张表中与之匹配的行也出现两次,连接结果就会得到四份副本。聚合函数随之悄然膨胀。将父表与多张子表连接之后再计算 SUM,会把父表的每个值按子行数重复计算;COUNT(*) 可能计入重复行,而 COUNT(DISTINCT ...) 则不会。这类错误往往难以察觉,因为查询仍会返回看似合理的数字。
理论本身同样受到影响。科德的理论也未考虑 SQL 的空值及其所蕴含的三值逻辑;关于空值的逻辑处理至今仍存在争议。此外,SQL 允许重复行的多重集语义,也受到关系派批评者的反对,他们认为应当禁止重复行,因为允许重复会使建模错误被忽视。如果源数据中含有完全相同的行,一种解读是某个现实实体从未被赋予自己的键。因此,正确的 SQL 依赖于作者是否清楚某张表或某个中间结果是否本应是集合。实践中,这意味着:凡一行代表一个唯一实体之处都应声明键;有意识地使用 DISTINCT,而不是出于习惯;当一对多关系会使数值重复时,先聚合后连接。
语言组成:DDL、DML、DQL、DCL 与 TCL
SQL 语句通常按用途分为五类,大多数教程和认证大纲都使用相同的名称。这种分类是一种教学约定,而不是语言的正式划分。标准有自己的语句组织方式,数据库引擎也不会把这几类当作相互独立的模式来强制执行。不过,这些名称依然有助于判断某条语句在关系型数据库中可能改变什么。
数据定义(DDL)
数据定义语句用于创建、修改和删除模式对象。CREATE 用于新增表、视图、索引和例程等对象。ALTER 用于修改已有对象的结构,例如添加列或约束。DROP 用于删除对象及其定义。TRUNCATE 通常也归在这一类,尽管它只清空表而不删除表,而且不接受 WHERE 子句。因此,DDL 语句既会改变存储的数据,也会改变描述数据库的系统目录。删除一张表时,它的行和定义会在一步之内同时被移除。
不同产品中 DDL 的事务行为并不相同。在 MySQL 中,CREATE、ALTER 和 DROP 无法回滚,而 PostgreSQL 允许在事务中执行大多数 DDL。由于存在这些差异,在一种引擎中可以正常运行的迁移脚本,在另一种引擎中可能只完成了部分模式变更。
数据操作(DML)
数据操作语句用于修改已有表中的行。INSERT 添加行,UPDATE 重写满足条件的行的值,DELETE 删除满足条件的行。INSERT 会使表增长,而 UPDATE 和 DELETE 只影响其谓词所选中的行,并保持表的结构不变。如果没有 WHERE 子句,UPDATE 和 DELETE 会作用于所有行,这是常见的事故来源。MERGE 可以在一条语句中有条件地插入、更新或删除行,通常也归入 DML。约束同样适用于 DML:违反约束的 INSERT 或 UPDATE 会被拒绝,其影响的行也不会被保存。
数据查询(DQL)
SELECT 是 SQL 中使用最频繁的语句,在大多数讨论中它是 DQL 中唯一的成员。它读取行并返回结果集,而不修改存储的数据。其子句在关于查询语义的章节中介绍。一些教材会把 SELECT 归入 DML,从而划分出四类。单独列出 SELECT,是因为日常 SQL 中有很大一部分是只读的,而只读操作会带来不同的问题,例如权限、缓存和查询调优。
访问控制(DCL)
数据控制语句用于管理权限。GRANT 将表、视图、模式或例程上的特定权限授予用户或角色,REVOKE 则收回这些权限。权限还可以附带转授选项,允许接收者再将其授予他人。这些语句是本词条后文所述访问控制在 SQL 层面的基础,通常由数据库管理员而非应用程序代码执行。
事务控制(TCL)
COMMIT 使当前事务的修改永久生效,并对其他会话可见。ROLLBACK 则丢弃这些修改,并将数据库恢复到事务开始时的状态。SAVEPOINT 在事务内创建一个命名标记,ROLLBACK TO SAVEPOINT 可以在事务保持打开的情况下,只撤销该标记之后执行的操作。RELEASE SAVEPOINT 用于删除不再需要的标记。标准要求 ROLLBACK TO 中必须写出 SAVEPOINT 关键字,但 PostgreSQL 和 Oracle 允许省略。各引擎的支持程度也不一致:在 MySQL 中,只有 InnoDB 存储引擎支持这三条语句。
界限模糊之处
这五类在教科书式的示例中界限清晰,但在实际使用中相互重叠。SELECT ... FOR UPDATE 虽然是读取行,但同时会加锁,因此在一定程度上具有写操作的性质。某些方言中的 SELECT INTO,以及另一些方言中的 CREATE TABLE ... AS SELECT,会通过查询创建新对象,使 DQL 与 DDL 混合。INSERT ... SELECT 和 MERGE 则在单条语句中同时完成读取与写入。TRUNCATE 被归为 DDL,但其作用对象是数据。各厂商还添加了一些实用语句,例如用于会话设置、统计信息收集或维护的语句,它们都无法清楚地归入这五类。因此,这几类更适合作为讨论风险与职责的实用词汇来理解,例如哪些语句应当是只读账户无法执行的,而不宜被看作严格的分类体系。
查询语义:子句、连接与集合运算
逻辑求值顺序
SQL 查询虽然以 SELECT 开头书写,但数据库理解它的顺序却不同。对于单个查询块,逻辑顺序依次为 FROM 与 JOIN、WHERE、GROUP BY、HAVING、SELECT(包括窗口函数)、DISTINCT、ORDER BY,最后是 LIMIT 或 OFFSET。这只是一个概念模型,并不保证实际执行方式。优化器可以在内部重新安排操作的执行次序,但其结果必须与该顺序所定义的结果一致。
这一顺序可以解释若干常见规则。在 SELECT 中定义的列别名,在 WHERE 运行时尚不存在,因此不能在 WHERE 中引用,但可以用于稍后运行的 ORDER BY。COUNT、SUM 等聚合函数只有在 GROUP BY 之后才存在,这也是为什么行级过滤条件应放在 WHERE 中,而分组后的过滤条件应放在 HAVING 中。由于连接最先发生,它决定了之后每个子句所操作的行集合,因此如果连接条件放错位置,可能会在不知不觉中改变聚合结果。
连接
连接根据某个条件将两个或多个表中的行组合在一起。以两张小表为例:customers 表(1 Ann、2 Ben、3 Cy)和 orders 表(101 对应客户 1,102 对应客户 1,103 对应客户 2,104 对应客户 9,而该客户并不存在)。
- 内连接只保留键值匹配的行对。查询
SELECT c.name, o.id FROM customers c INNER JOIN orders o ON o.customer_id = c.id返回三行:Ann–101、Ann–102 和 Ben–103。 - 左外连接保留左表的每一行。改用
LEFT JOIN后,结果为相同的三行,再加上订单列为 NULL 的 Cy,共四行。 - 右外连接保留右表的每一行,因此订单 104 会出现,而客户姓名为 NULL。
- 全外连接保留两侧所有未匹配的行,共得到五行:三条匹配行,加上 Cy 和订单 104,二者各自用 NULL 补齐缺失的部分。
- 交叉连接将一个表的每一行与另一个表的每一行两两配对,产生笛卡尔积。此例中返回 3 × 4 = 12 行。通常只有在确实需要这种结果时才会显式使用交叉连接,因为普通连接中若遗漏 ON 条件,也会意外得到这一结果。
- 自连接是将一张表以不同别名与自身连接。例如,一张带有 manager_id 列的 employees 表可以与自身连接,使每位员工的行与其经理的行配对。
外连接中会出现 NULL 补齐的情况。若之后的 WHERE 条件作用于被补齐的一侧,就可能悄然把左连接变回内连接,这是常见的错误来源。
集合运算
UNION、INTERSECT 和 EXCEPT 以纵向方式合并两个查询的结果,即增加、保留或删除整行,而不像连接那样增加列。参与运算的两个查询必须具有相同数量的列,且类型相互兼容;列按位置而非名称进行匹配,结果的列名取自第一个查询。
默认情况下,这些运算符遵循集合语义:重复行会被消除,效果等同于应用了 DISTINCT。每个运算符还有一个 ALL 形式,遵循包(bag)语义,即多重集合语义,会保留重复行。若某行在一个输入中出现 m 次,在另一个输入中出现 n 次,则 UNION ALL 产生 m + n 个副本,INTERSECT ALL 产生两者中较小的次数,EXCEPT ALL 产生两者之差,且下限为零。Oracle 使用 MINUS 作为 EXCEPT 的同义词。ALL 形式通常开销更小,因为它省去了去重所需的排序或哈希步骤,因此常见做法是,除非确实需要去重,否则使用 UNION ALL。
一个容易被忽视的要点是 NULL 的处理。在普通比较中,NULL = NULL 的结果是未知(unknown),但在集合运算中,进行重复检测时会将同一列中的两个 NULL 视为相等。因此,含有 NULL 值的行会与完全相同的行进行去重,即使 WHERE 子句永远不会将它们匹配起来。
子查询
子查询是嵌套在另一条语句中的查询。非相关子查询不引用外层查询,可以独立于外层的每一行只求值一次。相关子查询则引用外层查询的列,因此其结果取决于外层的当前行,从概念上讲,它会针对每一行重新求值。若子查询至少产生一行,EXISTS 返回真,常用于检测是否存在相关的行,而不必通过连接使结果成倍增加。IN 用于检测某个值是否出现在一组值中,这组值可以来自子查询。
NOT IN 有一个众所周知的陷阱。如果子查询的结果中包含 NULL,且没有其他值匹配,比较结果就是未知而非真,因此该行会被过滤掉。例如,查询 id 不在 orders 某列中的客户时,只要有一个订单的客户 id 为 NULL,查询就可能完全不返回任何行。NOT EXISTS 不会出现这种情况,这也是它常被优先采用的原因之一。
数据类型与 NULL 语义
类型族与隐式转换
SQL 的列都带有类型,标准和主流产品把这些类型归为几个大类。精确数值类型(如 INTEGER 和 DECIMAL(NUMERIC))与近似数值类型(如 REAL 和 DOUBLE PRECISION)不同,后者继承了浮点运算的舍入行为。字符类型存储定长(CHAR)或变长(VARCHAR)文本,其比较和排序依赖于排序规则(collation)。日期和时间类型包括 DATE、TIME 和 TIMESTAMP,可带或不带时区,另外还有用于表示时间段的 INTERVAL。BOOLEAN 类型在 SQL:1999 中才进入标准,但一些广泛使用的产品过去并没有原生的布尔列,而是用比特或小型整数代替。二进制类型(BINARY、VARBINARY,以及 BLOB 等大对象类型)用于存储原始字节串。
当表达式混合使用不同类型时,引擎会在用户未作要求的情况下自动转换操作数。在混合运算中,精确数值会被提升为更宽的类型;文本字面量与数字比较时,通常会被转换为数字,但具体转换哪一边、如何处理非数字文本,不同引擎的做法各不相同。这些自动转换虽然方便,却可能使谓词的行为与编写者的本意不一致。对索引列施加的类型转换还可能导致优化器无法使用该索引。
三值逻辑与 NULL 比较
Codd 针对含缺失值的关系数据库提出的方案,在布尔推理的基础上增加了第三种结果:未知(unknown)。涉及 NULL 的比较运算,结果为未知。逻辑连接词遵循此类逻辑的常见规律:FALSE AND UNKNOWN 为 FALSE,TRUE OR UNKNOWN 为 TRUE,其他组合仍为未知。对未知取反仍为未知,而对真或假取反则会翻转其值。(更深入的讨论可参见真值表和一阶逻辑。)
实际后果是,WHERE 或 HAVING 子句只保留条件结果为真的行,结果为未知的行与结果为假的行一样会被丢弃。两个 NULL 之间的比较,或 NULL 与任何其他值之间的比较,都返回未知,因为每个 NULL 的值都是未知的。因此,IS NULL 和 IS NOT NULL 是唯一总能返回真或假的检测,因为它们检查的是值是否缺失,而不是比较值的大小。
分组和去重对 NULL 的处理与比较运算不同。GROUP BY 列中含 NULL 的所有行会被归入同一组,这与比较运算的行为恰好相反。
聚合函数与 COALESCE
聚合函数有自己的 NULL 处理规则。除按行计数的形式外,其他聚合函数都会把 NULL 排除在计算之外,因此 COUNT() 会统计每一行。例如,在一张有 100 行的订单表中,若有 25 行缺少发货日期,COUNT() 返回 100,而 COUNT(shipped_at) 返回 75。同样的规则也会使平均值失真:若十行数据中有两个 NULL,AVG 的除数是八而不是十。在输入为空时,COUNT 返回 0,但其他常见聚合函数返回 NULL,因此对没有匹配行的数据求和得到的是未知值,而非零。
COALESCE 返回其第一个非 NULL 参数,是替换默认值的标准工具。使用时需要小心:如 AVG(COALESCE(x, 0)) 那样,在调用 AVG 之前把缺失值替换为零,会改变结果,因为替换进来的零会计入分母。
对 NULL 的批评
引入 NULL 是为了满足实际需求。Codd 提出这一概念,是为了让关系系统能够表示缺失信息和不适用的信息。后来 Codd 又将其区分为两种空值:一种表示存在但尚未知道的值,另一种表示对该实体不适用的属性。SQL 的设计早于这一区分,因此只有一种 NULL 同时承担两种用途。Chris Date 认为这会导致语义不一致,而这一批评并不只停留在理论层面。Codd 和 SQL 的共同设计者 Donald Chamberlin 都指出,空值会在数据库引擎和应用程序代码中造成许多缺陷。
这些缺陷是切实存在的。当子查询结果的某一列中含有 NULL 时,NOT IN 测试会返回空集:只要该列中有任何一行为 NULL,成员判断就变为未知,过滤器便会去掉该行。CHECK 约束的情况更为微妙:当约束针对插入的 NULL 求值时,比较结果为未知,而约束会把未知视为满足。各厂商的实现又带来更多差异,商业引擎只部分实现了标准的逻辑,并且各自还有不同的变体。有人提出用更丰富的多值逻辑(其中一些是六值逻辑)取代三值逻辑,但由于这些逻辑更难推理,至今未能广泛采用。
完整性约束
完整性约束是存储在模式中的声明式规则,数据库引擎在数据发生变化时对其进行检查。约束只需在表或列上声明一次,此后所有触及该数据的语句都要受其约束,无论语句来自哪个应用程序。标准约束类型包括 PRIMARY KEY、FOREIGN KEY、UNIQUE、NOT NULL 和 CHECK。
PRIMARY KEY 指定用于标识每一行的列或列集合。一张表至多只能有一个主键,且其列不得包含 NULL,因此该约束同时兼具唯一性与非空性。UNIQUE 禁止列或列集合中出现重复值,但与主键不同,一张表可以同时存在多个唯一约束。它对 NULL 的处理有所不同。标准及大多数产品将 NULL 视为互不相等,因此多行可以在唯一列中同时取 NULL,不过部分实现的做法有所差异。NOT NULL 会拒绝任何会使列为空的语句。CHECK 为行附加一个布尔条件,例如要求数量为正数。只有当条件求值为假时,CHECK 约束才会被违反;结果为 UNKNOWN(可能由 NULL 引起)的情况会被接受。这种不对称性常常令人意外。
FOREIGN KEY 表达的是参照完整性:引用列中每个非空值都必须与另一张表中某个被引用的键值相匹配,通常是该表的主键或某个唯一列。按照默认的匹配规则,只要外键的任一列含有 NULL,该外键就不会被检查。因此,该约束能够防止“孤立”行的出现,例如指向不存在客户的订单,但它本身并不能说明当被引用的行之后被删除或修改时应当如何处理。这正是引用动作的作用。标准定义了 NO ACTION、RESTRICT、CASCADE、SET NULL 和 SET DEFAULT,可以分别附加到 ON DELETE 和 ON UPDATE 上。NO ACTION 是通常的默认值,只要仍有依赖行存在,就拒绝该变更,检查在语句结束时进行。RESTRICT 则立即拒绝。CASCADE 会将删除或键值变更传播到依赖行,SET NULL 将外键值替换为 NULL,SET DEFAULT 则将其替换为列的默认值。选择哪一种是建模上的决策。CASCADE 适用于脱离父记录便没有意义的子记录,例如订单的明细行;RESTRICT 则适用于必须为历史留存的记录,例如财务交易。级联动作可能跨越多张表连锁发生,在某些产品中,当同一张表受到多条路径的影响时,还会产生执行顺序方面的问题。
约束检查通常是即时的:每条语句在执行时即被验证,一旦违反,该语句即被中止。许多系统还允许将约束声明为 DEFERRABLE,其初始模式可以是 IMMEDIATE 或 DEFERRED。延迟约束只在 COMMIT 时检查,这使得事务可以暂时经过不一致的状态,例如插入两行且它们相互引用时。会话级语句可以为某个事务切换这一模式。支持情况各不相同:PostgreSQL 和 Oracle 提供可延迟约束,而某些引擎(如 MySQL 的 InnoDB)则不提供外键的延迟检查。在不支持的情况下,开发者必须谨慎安排语句顺序,或暂时禁用检查,而这本身就是一种风险。
约束把验证工作从应用代码转移到了数据库中。在模式中只写一次的规则会适用于每一个客户端、批处理作业和临时会话,因此不会因为某个服务中遗漏了一次检查而被绕过。这种集中化是在模式中声明规则的主要理由,也是关系模型的设计者强调声明式完整性的原因之一。其权衡则是实际的:每个约束都会增加每次写入的开销,而外键往往需要在引用列上建立索引才能高效地检查。大批量加载可能因此变慢,或需要暂时禁用约束。错误信息由引擎生成,对终端用户来说可能不如应用层的验证友好。约束还只能覆盖模式所能表达的内容。跨多张表的规则,例如要求订单总额等于其各明细行之和,不在标准的 CREATE ASSERTION 结构的覆盖范围之内(很少有产品实现该结构),只能改用触发器或应用程序逻辑来实施。正因如此,成熟的设计通常会结合使用声明式约束来表达结构性规则,并用应用层检查来实现更丰富的业务逻辑。
事务、ACID与并发控制
一个数据库事务将若干操作组合为一个单元,这些操作要么全部生效,要么全部不生效。通常概括为ACID特性的四项保证分别是:原子性(事务的所有变更要么全部应用,要么全部不应用);一致性(事务将数据库从一个满足其完整性规则的状态转换到另一个同样满足规则的状态);隔离性(并发事务不会暴露彼此未完成的中间工作,除非所选的隔离级别允许);持久性(已提交的变更在崩溃后依然保留)。原子性和持久性主要依靠日志与恢复机制来保证。隔离性则是真正权衡取舍的地方,因为实现隔离就必须限制并发操作的交错方式,而这正是并发计算的核心问题。
隔离级别与异常现象
SQL-92标准借助三种现象定义了四种隔离级别。脏读是指一个事务读取了另一个尚未提交、之后可能回滚的事务所写入的数据。不可重复读是指同一事务中两次读取同一行,结果却不同,原因是两次读取之间另一事务提交了对该行的修改。幻读是指重复执行同一查询时,返回了之前不存在的行,原因是另一事务插入或修改了符合条件的行。隔离级别从弱到强依次为:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 被防止 | 可能 | 可能 |
| 可重复读 | 被防止 | 被防止 | 可能 |
| 可串行化 | 被防止 | 被防止 | 被防止 |
该表描述的是最低限度的保证。实际系统各不相同:某一级别可能防止了比标准要求更多的现象,而且标准的措辞是以基于锁的系统为背景写成的。1995年,Berenson等人证明,快照隔离不会出现SQL-92所禁止的任何异常现象,但它并非可串行化的,因为它允许写偏斜:两个事务读取重叠的数据,却写入互不相交的行,结果合在一起违反了某条规则,而单独来看两者都无法察觉。不同产品的默认隔离级别也不一样:PostgreSQL和Oracle默认为读已提交,而MySQL的InnoDB引擎默认为可重复读。因此,开发者需要核实某个引擎实际提供的保证,而不能想当然地认为它与隔离级别的名称相符。
加锁与两阶段锁定
基于锁的并发控制对读操作授予共享锁,对写操作授予排他锁。共享锁之间彼此兼容,但排他锁会排斥同一数据项上的所有其他锁。两阶段锁定(2PL)约束了加锁与解锁的时机:在增长阶段,事务获取锁;一旦它释放了任何一把锁,就进入收缩阶段,此后不得再获取新锁。这一规则保证了冲突可串行化的调度。普通的2PL仍可能出现脏读和级联回滚,因此实际系统通常采用严格2PL(strict 2PL)或严谨2PL(rigorous 2PL),即一直持有排他锁直到事务提交或中止。
两阶段锁定并不能防止死锁。死锁是指一组事务形成循环,每个事务都在等待下一个事务所持有的锁。大多数系统采用死锁检测:引擎维护一张等待图(waits-for graph),其中每个事务是一个节点,当Ti等待Tj时,就从Ti到Tj画一条边;引擎通常借助后台线程定期在图中搜索环。发现环之后,引擎依据事务年龄、进度、所持锁的数量或之前的重启次数等标准选出牺牲者,并将其中止或重启。检测越频繁,消耗的CPU越多;检测越少,等待时间就越长。另一类做法是死锁预防方案,如wound-wait和wait-die,以及保守型2PL:后者在执行前就获取所有锁,因此永远不会死锁,但可能比必要时间更长地持有锁。
多版本并发控制
多版本并发控制(MVCC)从不原地覆盖行,从而避免了大部分阻塞。更新会创建一个新版本,每个事务读取其快照时点已提交的版本。由于读操作看到的是较旧的版本,而无需等待写者持有的锁,因此读不阻塞写,写也不阻塞读。快照隔离是基于MVCC构建的最常见隔离级别,它通常在提交时检测写-写冲突,因此先提交者获胜,另一个事务被中止。
MVCC并不能取代加锁。写者仍然需要一些锁和提交时的检查,而且这种方法也有代价。旧版本会占用存储空间,最终必须回收;例如PostgreSQL依赖定期执行的vacuum来清除死元组,而长时间运行的事务可能使这种清理无法回收空间。这种方法实际上是用存储管理和版本维护的开销,换取了锁等待的减少,这也是它在服务大量并发读取的事务系统中得到广泛应用的原因。
索引、查询规划与优化
索引是一种辅助性的数据结构,它把列值映射到匹配行的位置,使引擎无需读取整张表。关系型系统中最常见的结构是 B 树,这是一种平衡树,其查找、插入和删除的时间与行数成对数关系。由于叶节点条目按键值有序保存,B 树可以支持等值谓词、范围谓词(如 BETWEEN 或 <),以及对其前导列的 ORDER BY。哈希索引则对键值进行哈希运算,等值查找接近常数时间,但无法回答范围查询,也不能返回有序结果。许多系统还为特定工作负载提供位图索引、GiST、GIN 和全文索引。
索引与存储行之间的关系各不相同。聚簇索引决定表中行的物理存储顺序,因此表实际上是围绕该键组织的;一张表只能有一个这样的顺序。非聚簇索引是一个独立的结构,其条目包含索引键以及指向行的指针,例如物理地址或主键值。通过非聚簇索引查找时,可能需要再访问一次基表,除非该索引是覆盖索引,即包含查询所需的全部列。各种设计的权衡是一致的:索引能加速与之匹配的读取,但每一次修改了索引列的 INSERT、UPDATE 和 DELETE 都必须同步更新该索引。这会带来额外的 I/O、页分裂、锁竞争和存储开销。在低选择性列(例如布尔标志)上建立索引往往收效甚微。
SQL 语句是声明式的:它指定想要的结果,而不是计算结果的过程。引擎先解析并重写查询,然后由查询优化器枚举候选执行计划。这些选择包括每张表的访问路径(全表扫描还是使用某个特定索引)、连接顺序、每次连接所用的算法,以及是否需要排序或聚合。每个候选计划都会得到一个估算代价。早期的关系数据库研究部分依赖规则,但基于代价的方法是在 System R 项目中确立的;Selinger 等人在 1979 年对其作了描述,其中包括对连接顺序进行动态规划。代价估算依赖于基数估计,即预测每个谓词和连接会产生多少行。这些估计来自系统目录中保存的统计信息:行数、不同值的数量、空值比例,以及值分布的直方图。优化器通常假设谓词彼此独立、值均匀分布,而这些假设常常不成立。由于误差会在多次连接中累积,相差几个数量级的估计可能使本应只需几秒的查询变成耗时数小时。因此,统计信息必须随数据变化而定期刷新,例如 PostgreSQL 中的 ANALYZE 或 SQL Server 中的 UPDATE STATISTICS 命令。
主要的连接算法适用于不同的输入情形。嵌套循环连接扫描外层输入,并对每一行去探测内层输入,通常通过索引完成。当外层输入较小且内层有索引时效率很高,但当两侧都很大时性能很差。哈希连接从较小的输入构建内存中的哈希表,然后用另一侧的行去探测它。它只能处理等值条件,并且需要足够的内存;内存不足时,分区会溢出到磁盘。排序-归并连接会按连接键对两侧输入排序(或利用已有索引的顺序),然后一趟完成归并。其代价主要来自排序,约为 ,但当输入已经有序、内存有限或结果必须有序时表现良好。优化器会根据估计的规模、可用索引、排序顺序和内存设置在这些算法之间做出选择。
可以用 EXPLAIN 查看优化器选定的计划,它会以算子树的形式显示,并标注估计的行数和代价。其他系统也提供类似工具:Oracle 提供 EXPLAIN PLAN,SQL Server 则提供实际执行计划。PostgreSQL 和 MySQL 中的 EXPLAIN ANALYZE 会实际执行该语句,并在估计值旁边报告实际行数和耗时,因此估计行数与实际行数之间的差距往往是最有用的诊断信息。开发人员通常会留意:本应使用索引却进行了全表扫描、在大输入上进行嵌套循环,以及溢出到磁盘的排序或哈希操作。常见的补救措施包括添加或重新设计索引、改写谓词以避免对索引列套用函数、刷新统计信息以及重构查询。优化器提示(如 Oracle 的索引提示)可以强制使用某个计划,但它们固化了可能随数据变化而不再成立的假设。统计信息更新或参数值变化后,计划也可能出现意外变化,这正是计划检查始终是日常调优一部分的原因。
标准化:过程与版本
源于 ANSI 与 ISO
SQL 于 1986 年在美国正式标准化。1986 年 10 月,ANSI 发布文档 X3.135-1986《数据库语言 SQL》,该语言由此成为正式标准。这一名称多少有些容易引起误解:ANSI 本身并不制定标准,而是认可美国境内的标准制定组织(SDO),实际编写标准文件的是这些组织下属的委员会。负责此事的委员会为 ASC X3,该机构后来更名为 INCITS,是经 ANSI 认可的机构。国际版本随后很快出现。国际标准化组织(ISO)是各国标准化机构组成的全球性联合会,于 1987 年采纳了这份 ANSI 文档。如今,国际文本由 ISO/IEC JTC 1 制定,标准由 ISO/IEC JTC 1/SC 32 负责维护,而 INCITS 则将国际版本采纳为美国国家标准。
从 SQL-86 到 SQL:2023
第一版通常被称为 SQL-86,是一份基础规范。其第一次修订版 SQL-89 于 1989 年发布,题为《带完整性增强的 SQL》(SQL with Integrity Enhancement),增加了完整性约束。SQL-92 则是一次幅度大得多的升级。标准规范本身的篇幅相比 SQL-89 增长了约五倍,不过其中大部分是对既有功能的描述更加精确所致。其新增功能包括显式外连接、集合运算以及 CAST 运算符(即带外连接的显式 JOIN 语法,外加 NATURAL JOIN 和 CROSS JOIN,以及集合运算)。
SQL:1999 改变了一致性的组织方式。从 SQL:1999 开始,标准不再像 SQL-92 那样划分为三个级别,而是定义了大量独立的功能特性。其中一大部分构成“核心”(Core)功能,每个符合标准的 SQL 实现都必须提供这些功能。该版本还带来了一些重大新增内容,例如递归查询、触发器和数组类型。从 SQL:2003 起,标准改以多个编号部分的形式发布。SQL:2006 新增了关于 XML 的第 14 部分,SQL:2019 新增了关于多维数组的第 15 部分。最新版本 SQL:2023 是 ISO 和 ANSI 标准的第九版,于 2023 年 6 月正式通过。其最突出的新增内容是属性图查询,这是一种建立在 SQL 之上的图查询语言,同时还新增了若干 JSON 相关特性。
ISO/IEC 9075 的各个部分
该标准的名称为《信息技术 数据库语言 SQL》(Information technology – Database languages – SQL),编号为 ISO/IEC 9075。自 SQL:1999 起,该标准被划分为若干部分,编号从 1 到 16(截至 SQL:2023)。并非所有编号都在使用:缺失的部分从未发布。主要现行部分包括:
- 第 1 部分,框架(Framework),提供概览,并对常用术语作出一些定义。
- 第 2 部分,基础(Foundation),定义了 SQL 语言的绝大部分内容。
- 第 3 部分,调用级接口(SQL/CLI),描述用于访问数据库的 C 和 COBOL API。
- 第 4 部分,持久化存储模块(SQL/PSM),定义服务器端的过程式语言。
- 第 9 部分,外部数据管理(SQL/MED),用于访问存储在数据库之外的数据。
- 第 10 部分,对象语言绑定(SQL/OLB),用于将 SQL 嵌入 Java 中。这与 JDBC 不同,JDBC 是将 SQL 语句当作字符串处理。
- 第 11 部分,信息与定义模式(SQL/Schemata),涵盖信息模式(information schema)。
- 第 14 部分,XML 相关规范,以及第 15 部分,多维数组。
- 第 16 部分,属性图查询(SQL/PGQ),定义了 SQL 语言表示属性图并与之交互的方式。
各方对于当前有多少个现行部分的说法并不一致。一种说法认为 SQL 目前有 11 个现行部分,其中大多数人所熟知的核心语言部分是第 2 部分。另一种说法将该标准描述为 10 个部分:第 1–4 部分、第 9–11 部分以及第 13–16 部分。第三种说法则认为当前版本包含 9 个部分。造成差异的原因在于统计时计入哪些部分,因为未发布或已合并的部分会被区别对待。只有少数部分得到广泛实现。第 2 部分、第 11 部分和第 14 部分是在实践中较为常见的几个部分。
一致性与厂商采用
该标准规定的是最低要求。第 1、2、11 部分涵盖了该语言的最低要求,其他部分则定义扩展功能。由于没有任何单一实现能够支持全部功能,厂商通常只声明符合其中的某个子集。在实践中,声称符合 SQL 标准的大多数数据库管理系统,实际上只达到入门级(Entry)一致性。例如,Oracle 完全符合入门级标准,而在更高的 SQL-92 级别上只是部分符合。在基于功能的模型下,SQL 标准中所有新增功能都以可选特性的形式存在,每项特性都有一个特性代码,因此一项一致性声明实际上是一份所支持功能的清单,而不是一个单一的标签。
一致性声明是以最新版本为准来判定的。每个新版本都会取代之前的版本,因此声称符合早期版本的说法在官方层面并不具有效力。例如,PostgreSQL 表示,在不与传统功能或常识相抵触的前提下,其开发目标是符合标准的最新正式版本。厂商也往往会以某些对可移植性有影响的方式偏离标准,而可移植性正是制定该标准的初衷之一。引入标准的目的,就是使 SQL 应用程序能够在符合标准的各产品之间实现可移植。
方言、可移植性与厂商扩展
SQL 的核心部分在各产品之间是通用的,但每个主流数据库系统都实现了自己的方言,差异体现在日常语句中。每个主流数据库系统都会对标准做出各自的变体,加入独有的功能、语法特性和扩展。主要的体系包括 Oracle Database、Microsoft SQL Server、PostgreSQL、MySQL(及其分支 MariaDB)和 SQLite。开发者最常接触的这几个系统,差异远不止表面形式。
实现分歧与语法差异
最明显的差异出现在行数限制、标识列生成和内置函数上。最常用于 Web 应用的三个嵌入式或开源引擎使用末尾的 LIMIT 子句;SQL Server 则把 TOP 放在 SELECT 之后,或使用更冗长但符合 ANSI 标准的 OFFSET...FETCH 语法。Oracle 历来依赖 ROWNUM,近来则采用了更现代的 FETCH FIRST 语法。代理键也呈现同样的格局:Oracle 使用序列对象,SQL Server 使用 IDENTITY(1,1),PostgreSQL 使用 SERIAL,MySQL 使用 AUTO_INCREMENT。标量函数同样各不相同:SQL Server 用 + 连接字符串,PostgreSQL 用 ||,而大多数系统则倾向于使用 CONCAT()。
架构的影响可能不亚于语法。SQLite 并不以服务器方式运行,而是把整个数据库存储在磁盘上的单个文件中。Oracle 的特别之处还在于:在没有表引用的 SELECT 中,它要求必须写明表,因此在不查询任何表的 SELECT 查询中需要使用 "FROM DUAL"。
厂商过程式语言
每个主流厂商都会在 SQL 之上扩展一种过程式语言,加入变量、循环、条件判断和异常处理。Oracle 的 PL/SQL 是最著名的例子,专为在 Oracle 数据库上对 SQL 进行过程式扩展而设计。微软的 Transact-SQL(T-SQL)是 SQL Server 上的对应语言。PostgreSQL 的 PL/pgSQL 最早出现在 1998 年 10 月 30 日发布的 PostgreSQL 6.4 中,其语法与 Oracle 的 PL/SQL 十分相似。
这些语言在细节上各有不同。例如,在 PostgreSQL 中,事务管理命令只能在过程中使用,不能在函数中使用,而 PL/SQL 则允许这些命令出现在两种上下文中。声明规则也有差异:PL/pgSQL 要求所有变量都集中声明在 BEGIN 之前的 DECLARE 段中,而 T-SQL 则允许在批处理中的其他位置进行声明。
可移植性问题
可移植性问题通常集中在过程式层面,而非普通查询。一份比较指南指出,查询层面的语法已经足够接近,大多数语句只需稍作修改即可迁移;真正出问题的地方是变量、控制流、错误处理和内置函数。类型名称也常常带来麻烦,常见差异包括 BOOL 与 BIT、timestamp 与 datetime、now() 与 getdate(),以及 SERIAL 与 INT IDENTITY(1,1)。
分页和行数限制是最容易暴露问题的地方之一。SQL Server 的 OFFSET...FETCH 必须带有 ORDER BY 子句才合法,而只要出现 FETCH,就必须同时出现 OFFSET 子句。查询计划也可能出现分歧:一条在某个引擎上性能良好的语句,换到另一个引擎上可能变慢,原因不在于 SQL 写得不好,而在于优化器不同。标识列的处理方式从序列到自增关键字各不相同,这也使批量加载和数据迁移变得更加复杂。
策略与权衡
通常的策略是:只要标准足够用,就写标准语法。OFFSET...FETCH、CASE 表达式和 ANSI 连接等标准结构通常能被多个引擎接受,不过即便如此,它们也存在特定版本的限制。在无法避免方言的情况下,团队通常会将其隔离在数据访问层、查询构建器或迁移工具中,或放在数据库视图与存储例程之后,使应用代码不必根据数据库类型做分支。若应用代码通过判断引擎名称来选择不同的 LIMIT 或日期函数,则被普遍认为是一种脆弱的做法。
权衡在于,厂商特有的功能往往带来实际收益:靠近数据运行的过程式逻辑、优化器提示或专用数据类型,可以减少网络往返并简化业务规则。但这些收益的代价是被厂商锁定,用 PL/SQL 或 T-SQL 编写的代码无法原样运行在其他系统上,在任何迁移过程中都必须重新编写。因此,项目团队通常会审慎决定哪些厂商特性值得付出可移植性的代价,并针对实际的目标引擎进行测试,而不是想当然地认为为某一产品编写的查询在另一产品上会表现完全一致。
存储例程、触发器与视图
存储例程是保存在数据库内部、通过名称调用的过程式代码。SQL 标准的过程化部分在 ISO/IEC 9075-4《持久存储模块》(SQL/PSM)中规定,为该语言增加了流程控制、变量、赋值、异常处理和游标。过程以语句的形式调用,而用户自定义函数则用于表达式之中并返回一个值。目前的版本为 ISO/IEC 9075-4:2023,但大多数产品只实现了该模型的一部分,并朝各自的方向扩展,因此过程式代码是 SQL 中可移植性最差的部分之一。Oracle 的 PL/SQL、Microsoft 的 Transact-SQL 和 PostgreSQL 的 PL/pgSQL 大致都遵循 SQL/PSM 的总体结构,但语法和运行时行为各不相同。
触发器是附加在表或视图上的例程,会在行被插入、更新或删除时自动运行。触发器最早出现在 SQL:1999 中。触发器由其触发时机(BEFORE、AFTER 或 INSTEAD OF)、触发事件和粒度来定义:行级触发器对每个受影响的行运行一次,语句级触发器则每条语句运行一次。行级触发器可以引用每一行的旧值和新值,并可通过可选条件限定动作何时执行。标准对 BEFORE 触发器可以修改的内容有所限制,但各实现并不一致:Oracle 允许 BEFORE 行级触发器修改传入的新值,PostgreSQL 则允许 BEFORE 触发器跳过该操作或修改该行。INSTEAD OF 触发器使视图具备写入行为,下文将作说明。典型用途包括强制执行业务规则,例如拒绝超过设定比例的加薪,或阻止选课人数超过课程容量;也包括审计,即把每次变更连同旧值、新值和执行操作的用户一并写入日志表。由于一个触发器的动作可能再触发其他触发器,触发器链可能变得难以预测,而递归的触发器链必须由实现加以限制。
视图是表现为虚拟表的命名查询。对单个基表的简单视图(不含聚合、分组、DISTINCT 或集合运算)通常可以更新,更新会传递到底层表。更复杂的视图,例如由连接或聚合构成的视图,通常是只读的,除非有 INSTEAD OF 触发器提供写入逻辑。WITH CHECK OPTION 子句会拒绝那些会使行脱离视图自身过滤条件的写入。视图通过隐藏列、重命名字段和限制行来抽象数据访问,因此也常用于访问控制。物化视图则有所不同,它会将结果物理存储下来。其内容不会自动跟随基表变化:物化视图不会自动更新。各系统提供不同的刷新策略。Oracle 可以选择按需(ON DEMAND)或提交时(ON COMMIT)刷新,ON COMMIT 视图会随事务提交而更新,从而保持数据最新,但代价是降低写入性能。完全刷新会重新执行定义查询,在大数据量下可能很慢;增量(快速)刷新则只应用自上次刷新以来的变更,前提是定义查询满足系统的限制条件。在 PostgreSQL 中,标准刷新可能会阻塞其他试图读取该物化视图的连接,而只有当视图具有合适的唯一索引时,CONCURRENTLY 选项才能避免这种阻塞。
把逻辑放在数据库内部会带来一些反复出现的风险。业务规则若分散在应用代码与触发器或过程之间,就更难查找、测试和进行版本控制;触发器还可能产生副作用,而调用 INSERT 的开发者可能根本看不到这些副作用。逐行的过程式循环和逐行触发器在批量加载时可能导致严重的性能下降,并且查询优化器无法像对待声明式查询那样对过程式代码进行推理。依赖特定方言的例程也会提高迁移成本,因为把模式迁移到另一引擎通常意味着重写其过程、触发器,有时还包括视图。物化视图还带来结果过时的风险,可能让以为数据是实时的用户感到意外。由于这些成本,许多组织会把例程限制在完整性约束、审计或对性能要求严格的操作上,而将大部分业务逻辑保留在应用或服务层中。
高级查询特性:窗口、递归与分组
窗口函数
窗口函数是在 SQL:2003 中加入标准的,与 XML 相关的功能和序列生成器一同引入。到 2010 年前后,大多数现代数据库都已支持 SQL:2003 的核心特性,其中窗口函数几乎已被普遍支持。与聚合函数不同,窗口函数为每一行基于一组相关行计算一个值,且不会把这些行合并。GROUP BY 用汇总结果替代原有的行,而窗口函数则是在已有的行上附加汇总值。
OVER 子句定义窗口。所谓"窗口"就是这组相关行,由 OVER() 子句内的 PARTITION BY 和 ORDER BY 定义。PARTITION BY 会为每个分组重新开始计算,因此它把窗口函数限定在具有相同取值的行组内。空的 OVER() 会将函数应用于整个结果集,而 OVER(ORDER BY) 则按指定顺序生成累积计算。对于有序窗口,默认帧是从分区起始行到当前行,这就是为什么普通的 SUM 会产生累计和:有序窗口的默认帧是"从分区开始到当前行的所有行",因此得到的是累计和,而不是每一行上都相同的总和。
排名函数和偏移函数是另一类常用函数。使用 RANK(),可以按薪资为员工排名,薪资相同时排名会出现跳号。ROW_NUMBER 分配唯一的连续序号,不会出现此类跳号;DENSE_RANK 同样不会出现跳号。LAG 和 LEAD 读取相邻行的值;在分区查询中,每个类别的 LAG 只回看该类别的上一个月,而不会跨越所有类别。一个常见用法是选取每个实体的最新记录,例如每个用户的最新状态:在每个分区内为行编号,然后保留第一行。
递归查询
这一演进的顶点是 SQL:1999 标准,它正式引入了 WITH RECURSIVE 公用表表达式。递归 CTE 由一个非递归的基础项和一个引用该 CTE 自身名称的递归项组成,其中基础项不引用 R,递归项引用 R。两部分通过 UNION 或 UNION ALL 组合。引擎先计算基础项,然后反复将递归项作用于最新产生的行,直到达到不动点为止。
标准对这一过程施加了限制。标准要求递归为线性递归,并限制递归引用可以出现的位置,因此每个分支中的自引用至多出现一次。SQL:1999 还允许相互递归的定义。UNION 与 UNION ALL 的选择很重要:使用 UNION 时,过程会在继续之前去除当前表中的重复行。正是这种去重使图遍历通常采用 UNION:一旦某个节点被再次访问,就不会产生新行,迭代随之停止,从而防止循环。一些引擎要求显式写出关键字,例如只要 WITH 子句中有任何一个 CTE 是递归的,就必须包含 RECURSIVE 关键字。
典型应用包括组织层级结构或物料清单(BOM),其深度事先未知。由于无界递归可能无限运行,引擎会设置限制;例如 Databricks 在确实需要超过 100 步时,可以使用 MAX RECURSION LEVEL 提示来覆盖最大层级。
分组集、ROLLUP 与 CUBE
这些结构是 GROUP BY 子句的扩展。它们可以生成与使用 UNION ALL 合并多个单独分组查询相同的结果集,不过使用其中某个 GROUP BY 运算符通常更高效。GROUPING SETS 是最通用的形式:当完整的 ROLLUP 或 CUBE 所生成的分组并非全部需要时,可以使用 GROUPING SETS 只指定所需的分组。
ROLLUP 专为层级结构设计。除常规分组结果外,它还会生成包含小计和总计的结果集。ROLLUP 假定维度列之间存在层级关系,并且只根据该层级生成分组集。因此,ROLLUP (A, B) 与 ROLLUP (B, A) 的结果并不相同。相比之下,CUBE 会为指定列生成所有可能的聚合组合。输出规模呈指数增长:10 列的 CUBE 会产生 1,024 个分组集,这几乎从来不是人们想要的结果。
小计行在输出中以 NULL 表示,这与数据中本身已有的 NULL 产生冲突。这种设计有些出人意料,因为它除了表示"未知"之外,又为 NULL 引入了第二种含义"全部"。为解决此问题,SQL Server 提供了 GROUPING() / GROUPING_ID() 等函数,用以区分数据本身的 NULL 与小计产生的 NULL。就标准的历史而言,这些特性自 1999 年起已成为 ANSI SQL 标准的一部分,不过 WITH CUBE 和 WITH ROLLUP 最初是由微软引入的。
横向连接与行模式匹配
FROM 子句中的 LATERAL 派生表可以引用同一 FROM 子句中排在前面的表的列,这使子查询能够为每个外层行运行一次,并返回多行结果。它常用于分组取前 N 条的查询,否则这类查询就需要笨拙的相关子查询。行模式匹配在 SQL:2016 中实现标准化,并在 Oracle 等产品中通过 MATCH_RECOGNIZE 子句实现。它允许查询使用类似正则表达式的模式来描述一系列行,例如在按时间排序的数据中查找先下降后上升的一段数值。这些都是专门化的结构:大多数分析工作依赖的是窗口函数、分组集和递归 CTE。
半结构化数据:XML、JSON与数组
关系表默认每一列存放原子值,并且每一行都遵循同一套固定的模式。半结构化数据中字段和嵌套结构因记录而异,与这一模型并不契合。标准的应对方式是为这些格式增加专用的数据类型、函数和构造器,而不是改动表模型本身。主要的分支包括用于XML的SQL/XML、用于JSON的SQL/JSON扩展,以及用于数组的集合类型。
SQL/XML
SQL/XML,即XML相关规范,是SQL规范的第14部分。它引入了预定义的XML数据类型,并配有构造器、若干例程、函数,以及XML与SQL类型之间的映射,以支持在SQL数据库中操作和存储XML。SQL:2003定义了基本数据类型、映射、谓词和函数。SQL:2006、SQL:2008和SQL:2011版本扩展了这些早期部分,使其与XQuery的配合更加紧密。XQuery是由W3C的XML查询工作组制定的查询语言,其数据模型是对XML文档的抽象,这些文档可以是有类型的,也可以是无类型的。
各实现展示了该标准在实践中的应用情况。Microsoft SQL Server 2005实现了ISO SQL-2003中定义的XML数据类型,其列类型既可接受完整文档,也可接受带有多个顶层元素的内容片段,前提是格式良好。列还可以绑定到XML模式集合,由此得到的有类型XML会在插入时进行验证,并在查询编译时得到更精确的检查。查询访问通过方法实现:query()、value()、exist()和nodes(),每个方法都以XQuery表达式作为参数,此外还有用于修改的modify()。
JSON
JSON的支持出现得较晚,且起步形式较为保守。SQL:2016仅支持对存储在字符串字段中的JSON数据进行JSON操作。该版本引入了JSON_VALUE、JSON_QUERY、JSON_OBJECT、JSON_OBJECTAGG和JSON_TABLE等函数,其中JSON_TABLE可将JSON内容转换为行和列。它的路径语言基于JavaScript语法,也是在这一版本中引入的。由于数据本质上只是文本,数据库无法按类型对其进行检查。部分系统对此进行了弥补:例如,SQL Server 2016没有专门针对JSON的数据类型,仍将JSON存储在NVARCHAR列中。它的ISJSON函数可用于在包含JSON文本的NVARCHAR列上创建检查约束。
SQL:2023是该标准的第九版,于2023年6月正式通过。其中的JSON相关改动以可选特性的形式组织。原生JSON数据类型被标识为T801,并已加入标准,许多实现此前就已提供该类型。T802增加了对唯一键的可选检查,T803则保留了SQL:2016中基于字符串的形式。实现可以只支持较早的基于字符串的方式、原生类型,或两者兼而有之。该版本还新增了JSON_SERIALIZE、JSON_SCALAR和IS JSON谓词,并引入了用于访问JSON成员的点号表示法,相比JSON_QUERY和JSON_VALUE函数,它能写出更简短的查询。根据T840,路径语言中允许使用十六进制整数字面量。各厂商跟进这些变化的速度不一;据2023年的一篇评论称,SQL:2023中提出的大多数函数当时已在PostgreSQL中可用。
数组
标准还定义了ARRAY集合类型,它于SQL:1999中引入,允许列、变量或例程参数保存一个由单一声明类型元素组成的有序序列。数组元素通过位置寻址,声明时需设定最大基数。标准数组是一维的。PostgreSQL支持的多维数组通常属于厂商扩展,而不属于标准的核心模型。这一区别关系到可移植性,因为依赖嵌套数组语义的代码无法干净地在不同系统之间迁移。
数组与关系表在几个方面有所不同。单个数组类型的列会把整个集合存放在一个字段中,这违背了关系设计所追求的第一范式。数组元素按位置而非键来标识,其顺序本身也是值的一部分。若要回到面向行的处理方式,查询必须把数组展开为行,之后得到的表便与其他关系一样被处理。因此,半结构化类型在存储上提供了灵活性,而数据的查询与约束方式仍由关系模型统一管理。
模式设计与规范化
模式设计决定存在哪些表、每张表包含哪些列,以及一张表中的行如何指向另一张表中的行。常见的起点是实体关系模型:现实世界中的事物成为实体,其性质成为属性,其关联成为关系。在转换为关系模型时,每个实体通常对应一张带主键的表。一对多关系通过"多"的一端上的外键引用"一"的一端来表示。多对多关系则需要一张单独的连接表,其复合主键由两个参与方的键组合而成。这样便得到了表结构的初稿,之后再由规范化对其进行检验和完善。
规范化建立在函数依赖之上。当某个属性集合能够决定另一个属性时,后者就依赖于前者。各个范式就是对这些依赖关系的一系列检验。第一范式(1NF)要求每个单元格只能包含一个原子值,因此不允许把多个值打包到同一列中。第二范式(2NF)消除部分依赖,即非键列只依赖于复合键的一部分。第三范式(3NF)消除传递依赖,即非键列依赖于另一个非键列。简言之,1NF 要求每个单元格都是原子(单一)值,不能是逗号分隔的列表;3NF 则消除传递依赖,非键列不得依赖于其他非键列。博伊斯-科德范式(BCNF)是第三范式的更严格版本。若一张表满足第三范式,且对于每一个函数依赖 (X → Y),X 都是超键,则该表属于 BCNF。在 BCNF 下,每个决定因素都必须能够充当候选键,这弥补了当表中存在多个相互重叠的候选键时第三范式可能留下的漏洞。
规范化的实际动机在于冗余数据所导致的各种异常。更新异常出现在同一事实被复制到多行时:例如每条订单记录中都保存了客户的邮箱,修改时就必须逐行更改,一旦漏掉某一行,数据就会相互矛盾。插入异常则会阻碍在没有无关事实的情况下记录某一事实,例如某个商品的价格只有在它被订购之后才会存在。删除异常会在不经意间丢失事实,例如删除某位客户的最后一笔订单时,该客户的资料也可能一并被删除。将客户数据拆分到独立的表中,并通过键相互关联,就能消除上述问题,因为每项事实只保存在一个地方。
规范化也有其代价。数据被拆分到多张表中后,读取操作就要依赖连接(join),设计的复杂度也随之增加。虽然表的数量可能会增加,但它能带来逻辑上更为严谨、更可靠的数据库结构。正因如此,许多事务型系统把第三范式视为实际的基线,只有在某个具体异常确实需要时,才会进一步分解至 BCNF。
反规范化则是刻意的逆向操作:设计者重新引入冗余,例如存储预先计算好的订单总额、把客户姓名复制到订单表中,或者将多张表合并为一张宽表,从而让常见查询需要的连接更少。在用于提升读取查询性能的 OLAP 数据仓库中,反规范化是有意为之的做法。其代价在于,每一条写入路径都必须保证重复数据的一致性。这项工作通常会转移到应用代码、触发器或定时任务中,而这些地方很容易出错。
因此,模式的选择决定了数据库的长期维护成本。规范化的设计使更新操作规模小且行为可预测,但会让某些查询变得更长,查询计划也更昂贵。反规范化的设计简化了读取和报表,但会增加数据不一致的风险,并且每当业务规则发生变化时,维护成本都会上升。主键、外键和唯一约束等 SQL 特性,使数据库本身能够强制执行所选的设计,而不必依赖每个写入表的应用程序各自去维护这些约束。
安全与访问控制
SQL 的访问控制模型基于一个原则:数据库账户应当只能执行其工作所需的操作。其基本单位是权限,即允许某个用户、某个角色或特殊组 PUBLIC 对特定对象执行特定操作的授权。表级权限包括 SELECT、INSERT、UPDATE 和 DELETE,并且可以细化到单个列。例如,用户可能被允许更新薪资列,但不能在其他地方读取该列。例程有各自的 EXECUTE 权限,视图也可以独立于其底层表进行授权。撤销操作会删除某项授权,但该模型并不总是那么简单。同一权限可能由多个授权者授予某用户,因此撤销其中一项授权未必能移除访问权限。正是出于这一原因,1976 年有人详细研究了 System R 原型的撤销语义。GRANT 和 REVOKE 语句本身在语言组成部分一节中介绍。
权限与基于角色的访问控制
在大型系统中逐个用户授予权限难以管理,因此标准以及大多数产品都提供了角色,即具名的权限集合。角色被授予给用户,修改角色即会改变所有持有该角色者的访问权限。这是基于角色的访问控制(RBAC)的基础,即将权限附着于“分析师”或“应用程序开发者”等工作职能,而非具体个人。WITH GRANT OPTION 子句允许接受者将权限再转授给他人,这很方便,但也使撤销更难推断。一些厂商在标准之上增加了功能。例如,SQL Server 提供 DENY,可显式阻止访问,并覆盖通过其他角色获得的授权。视图和存储例程也被用作访问边界:用户可以被允许查询只暴露特定列和行的视图,例程也可以被定义为以其所有者而非调用者的权限运行。遵循最小权限原则,即每个账户只拥有其所需的权限,可以限制由错误和攻击造成的损害。
SQL注入
SQL 注入是一种漏洞,当应用程序通过将不可信输入拼接进 SQL 文本来构造查询时便会出现。如果登录查询由用户输入的用户名拼接而成,精心构造的输入可以闭合字符串字面量并添加自己的子句,从而改变数据库执行的内容。结果可能是未经授权的数据读取、修改或删除,在某些配置下甚至可能访问宿主系统。该弱点被归类为 CWE-89,数十年来一直是应用安全指南中的常见条目。
主要防御手段是参数化查询,也称预处理语句。应用程序发送带有占位符的 SQL 文本,然后单独提供参数值,因此数据库在接收到任何数据之前就已解析了命令的结构。由于值是作为数据绑定的,它们无法引入新的语法。OWASP 将预处理语句列为首要防御措施,其次是正确构造的存储过程和白名单输入验证,并且强烈反对依赖对用户输入进行转义。参数化也有局限:占位符只能传递值,不能传递表名或列名等标识符,因此这些内容必须来自固定的白名单。如果 ORM 或存储过程内部构造动态 SQL,它们也并非自动安全。输入验证和最小权限账户是辅助性控制措施,不能替代参数化。
行级安全
权限控制的是对整张表的访问,但许多应用需要将访问限制在部分行上。行级安全(RLS)通过为表附加策略来解决这一问题,策略用于过滤用户可以读取或修改的行。PostgreSQL 在 9.5 版本中加入了该功能。管理员先在表上启用它,然后创建策略,指定策略适用的操作以及每行必须满足的条件。USING 子句决定查询和修改可见哪些已有行,WITH CHECK 则约束被插入或更新的行。启用 RLS 后,普通访问必须经由某条策略允许,因此没有适用策略的表将返回零行。除非表被设置为强制启用 RLS,否则表所有者和超级用户默认会绕过策略。
一个常见用途是多租户数据隔离。策略可以将租户标识列与会话设置或当前数据库角色进行比较,这样无论哪段应用代码发出查询,每个租户都只能看到自己的行。在数据库中强制执行该规则意味着应用中 WHERE 子句的缺陷无法导致数据跨租户泄露。视图提供了一种相关但较弱的机制,因为它依赖于视图定义的正确性,以及底层对象不会通过函数泄露数据。RLS 的代价在于:随着策略增多,复杂度随之上升;并且需要保证策略表达式的效率,因为它们会针对每个候选行求值。
审计、日志与加密
访问控制决定谁可以执行操作,而审计则记录实际发生了什么。数据库的审计设施和日志会记录登录、权限变更以及修改数据的语句,这有助于事件调查和合规审查。当日志存放在数据库自身管理员无法悄悄篡改的地方时,它们最有价值;基于触发器的审计轨迹则可能被拥有足够权限的用户绕过。加密解决的是不同的威胁。静态加密保护存储的文件和备份,列级加密可以保护特别敏感的字段,但会使查询和索引变得复杂。传输中加密通常通过客户端与服务器之间的 TLS 实现,用于防止网络上的窃听。密钥管理是一个独立且往往十分困难的问题,因为加密数据的安全程度取决于解密密钥的安全程度。这些措施是对权限、角色和行级策略的补充,而非替代,它们共同构成了数据库安全实践通常推荐的分层防护方法。关于个人数据处理的更广泛问题,见网络安全和数据隐私词条。
连接应用程序:接口与驱动程序
大多数 SQL 语句是从程序内部访问数据库的,而不是从交互式终端执行的。随着时间推移,逐渐形成了三类主要方式:直接嵌入宿主编程语言中的语句、通过一组固定函数传递 SQL 文本的调用级接口,以及代替应用程序生成 SQL 的高级抽象层。
嵌入式 SQL
最早的方式是嵌入式 SQL,即把语句直接写在 C、COBOL 或 Fortran 等宿主语言的代码中。由厂商提供的预编译器扫描源代码,把每条语句替换为对运行时库的调用,再交给普通编译器处理。标准为多种宿主语言定义了嵌入式 SQL,但静态形式在编译时就固定了语句文本,数据则通过宿主变量在程序与数据库之间传递。动态 SQL 覆盖的则是语句文本在运行时才拼装的情况。由于各厂商的预编译器互不相同,嵌入式程序很难在不同数据库之间迁移,这促使人们转向调用级接口。
SQL/CLI 与调用级模型
调用级接口(CLI)完全不需要预编译器:应用程序把 SQL 作为字符串传给函数,再通过同一套 API 读取结果和错误码。SQL 访问组(SQL Access Group)是由数据库厂商组成的联盟,于 20 世纪 90 年代初开始制定这类接口。规范后来转交给 X/Open,X/Open 于 1994 年后期对其作了扩展,之后被纳入 ISO/IEC 9075 系列,成为第 3 部分 SQL/CLI。该标准定义了环境、连接和语句的句柄,一套诊断机制,以及 SQL 数据类型与宿主语言类型之间的映射规则。其现行发布版本为 ISO/IEC 9075-3:2023,截至 2026 年仍有修订版在制定中。实际应用中,很少有程序直接基于 SQL/CLI 编程,更多是使用 ODBC 和 JDBC 等遵循相同设计的库。
ODBC 与 JDBC
开放数据库互连(ODBC)由 SQL 访问组创建,1992 年 9 月首次发布,同年微软为 Windows 推出了早期实现。其架构包括四个部分:应用程序、代为加载驱动程序的驱动程序管理器、驱动程序本身以及数据源。驱动程序接收 ODBC 调用,把 SQL 提交给特定数据库,再返回结果,因此基于通用 API 编写的应用程序只需更换驱动程序,即可对接不同的产品。ODBC 此后经过多次修订,目前在 Windows、类 Unix 系统和 macOS 上仍得到广泛支持。
Java 数据库连接(JDBC)为 Java 平台采用了同样的模式。Sun Microsystems 于 1996 年 1 月启动该项目,同年 6 月在征询数据库厂商意见后冻结了规范,并于 1997 年 2 月随 JDK 1.1 发布了第一个版本。其驱动程序管理器可以把调用分发给多个驱动程序。JDBC 驱动程序分为四种类型:第 1 类桥接到 ODBC,开销较大,且每台客户端机器都需要本地库;第 2 类把调用转换为厂商的原生客户端 API;第 3 类把调用发送给与数据库通信的中间件服务器;第 4 类是纯 Java 驱动程序,直接使用数据库的网络协议通信。如今第 4 类驱动程序是常见选择,因为它不需要本地代码,部署也很简单。
无论采用哪种接口,驱动程序的核心工作都是转换:把 API 调用转换为特定服务器能够理解的命令和网络协议消息。这也是为什么当驱动程序的质量或功能支持存在差异时,同一份应用代码的表现可能不同。
对象关系映射与阻抗失配
应用代码通常处理的是对象,包括标识、继承、引用和嵌套集合,而关系型数据库则将行存储在通过键相互关联的扁平表中。这两种模型之间的差距通常被称为对象关系阻抗失配。手动加载对象图可能需要大量连接或重复查询,而“N+1 查询”模式(先用一条查询获取列表,再为每个条目各执行一次查询来获取关联数据)是常见的性能问题。
对象关系映射(ORM)框架(如 Java 的 Hibernate、.NET 的 Entity Framework 和 Python 的 SQLAlchemy)通过将类映射到表、跟踪对象变化并为读写操作生成 SQL 来弥合这一差距。它们减少了样板代码,但生成的 SQL 并不总是高效,延迟加载可能触发隐式查询,而忽视所生成语句的开发者可能察觉不到执行计划低效或往返次数过多的问题。因此,许多团队会在使用 ORM 的同时,对关键查询手写 SQL。
连接池与预处理语句
建立数据库连接涉及网络设置、身份验证和会话初始化,因此应用程序很少为每个请求都新建一个连接。连接池会保存一组活动连接,并根据需要将其分配给线程或请求。池的大小很关键:连接过多可能使服务器不堪重负,而且归还连接池的会话必须重置状态,以免设置或临时对象在不同用户之间泄露。
预处理语句将 SQL 文本与参数值分开。服务器只需解析和生成一次执行计划,之后即可在使用不同值的多次执行中复用该计划,因此对于重复执行的查询,性能会有所提升。参数绑定还能使用户提供的数据不进入 SQL 文本,这是防范 SQL 注入的主要手段。只有当驱动程序确实单独发送参数时,这一保护才成立;有些驱动程序在客户端模拟预处理,因此其安全效果取决于驱动程序的行为和配置。连接池与预处理语句也会相互影响,因为许多连接池会按连接缓存预处理语句。
分析、数据仓库与大数据
SQL 在分析领域的应用源于两类工作负载的分工。事务型系统(OLTP)针对当前数据处理大量小规模的读写操作。分析型处理(OLAP)则要回答聚合类问题,例如多年来各地区的营收情况,这类查询需要扫描海量历史数据。因此,企业通常会把业务数据复制到一个独立的存储中,即数据仓库,并按固定计划加载,同时针对报表进行优化。数据仓库一般采用维度模型组织。中心的事实表记录可度量的事件(例如销售明细),包含数值型度量以及指向周围维度表的外键,这些维度表描述了“谁、什么、哪里、何时”等信息。在星型模型中,每个维度都是一张单独的非规范化表,因此大多数查询每个维度只需一次连接。雪花模型则把维度进一步规范化为子表,减少了冗余和存储空间,但增加了连接操作。许多生产环境中的数据仓库会混合使用两种模型,只对体量大或变化频繁的维度进行规范化。
执行这些查询的引擎高度依赖物理存储布局。列式存储将每一列的值集中存放,而不是将每一行的字段集中存放。一个只需要五十列中三列的查询,只需读取这三列;同一类型的值相邻存放,因此压缩效果好,也便于紧凑编码。其代价是读取或更新一整行的开销相对较高,这也是列式系统不适合事务处理的原因。大规模并行处理(MPP)是对这一设计的补充。表被划分到多个节点上,节点之间通常不共享内存或磁盘。规划器将每个查询拆分为若干片段,在持有相关分区的节点上并发执行,然后合并各部分的结果。Vertica 和 MariaDB ColumnStore 等产品同时采用了这两种技术,包括 Amazon Redshift 在内的许多云数据仓库也是如此。
在这类引擎成熟之前,SQL 主要通过 Hadoop 生态系统应用于超大规模数据集。Apache Hive 于 2010 年前后由 Facebook 开发,它在 Hadoop 分布式文件系统中的文件之上提供了一种类 SQL 语言 HiveQL,使分析人员无需手写 MapReduce 程序。Hive 将查询编译为批处理作业,早期基于 MapReduce,后来改用 Tez 或 Spark。其早期版本不支持完整的事务,子查询支持也很有限。直到 0.14 版本,Hive 才提供具有 ACID 保证的完整插入、更新和删除语义。Spark SQL 于 2014 年首次发布,采取了另一种路线:它运行在 Spark 引擎内部,允许开发者将 SQL 与编程式的 DataFrame 代码混合使用,并可以读取注册在 Hive 元存储中的表。两个系统都采用读时模式,并使用 Parquet、ORC 等列式文件格式。它们都以牺牲交互式延迟为代价,换取了扩展能力和容错性。
云数据仓库通常将存储与计算分离,因此查询能力可以独立于所存数据进行伸缩,用户按使用量付费,而非为固定硬件买单。它们的 SQL 方言通常接近标准,但在数据加载、时间旅行、半结构化数据和工作负载管理等功能上各不相同,这会影响可移植性。流式 SQL 则应对另一类需求:对无界事件流(如点击、传感器读数或支付记录)执行持续查询。流式查询不是运行一次并返回结果集,而是持续运行,并随着事件到达不断输出更新后的结果。为了在无穷输入上完成聚合,流式方言引入了时间窗口(滚动窗口、滑动窗口和会话窗口),并提供了处理迟到事件的显式规则。Apache Flink 等流处理器提供了此类 SQL 接口。流式引擎与批处理引擎正日益共用同一套查询语汇,使同一套逻辑既能作用于历史数据,也能作用于实时数据。
批评、NoSQL与其他替代方案
对SQL偏离关系模型之处的批评
关系模型的纯粹主义者反对SQL继承或新增的若干特性。科德(Codd)的模型将关系视为集合,因此不允许重复元组,属性也没有有意义的顺序。SQL表中可以包含完全相同的行,其列有固定的从左到右顺序,这一点体现在SELECT *和按位置书写的INSERT语句中。NULL又带来了更多麻烦。它引入了第三种真值,从而偏离了关系代数所依赖的二值逻辑,并产生了许多用户觉得违反直觉的结果,例如比较运算永远不会得出真,聚合函数会悄悄跳过缺失值。包括C. J. Date(C. J. 戴特)在内的批评者认为,这些偏离使查询更难推理,也更难保证正确性。辩护者则回应说,保留重复行的代价很小,而且有时正是正确计算聚合结果所必需的;NULL则是表示缺失信息的一种实用方式。
NoSQL:起源与类别
“NoSQL数据库”一词自2009年起被用来指代非关系型数据库倡导者的聚会。与关系型数据库管理系统相比,大多数NoSQL数据库的设计目标是良好地进行水平扩展,且不依赖高可用的专用硬件。后来,“不仅是SQL”(not only SQL)这一解释逐渐流行起来,因为许多NoSQL数据库在其原生查询模型之外,也支持类似SQL的查询语法。
通常可以区分为四大类。文档存储将数据保存为自包含的文档,通常采用JSON或BSON格式,结构灵活,且因文档而异。键值存储以键值对的形式保存数据,即一个唯一的键及其对应的值。这种简单的结构使得在已知键的情况下可以进行极快的查找。列族(宽列)存储以行键标识各行,每行附带大量列;需要注意的是,宽列数据存储不应与用于分析的列式数据库混为一谈。图数据库则关注数据元素之间的关系:每个元素作为一个节点存储,而连接本身是数据库中的一等公民,被直接存储。
主要的权衡在于横向扩展与严格事务之间。大多数NoSQL数据库优先保证可用性和分区容错性,而非强一致性(参见CAP定理)。它们还倾向于舍弃那些难以分布式实现的功能。例如,Cassandra不支持连接(join)和子查询。因此,设计者常对数据进行反规范化,使每个查询都能由单张表完成。灵活的模式加快了早期开发速度,但也把完整性检查从数据库转移到了应用程序代码之中。
NewSQL
“NewSQL”一词由451 Research分析师Matt Aslett于2011年提出。NewSQL系统旨在保留关系模型和SQL,同时获得水平可扩展性。它们既提供传统关系型数据库管理系统所熟悉的SQL接口和ACID保证,又具备NoSQL系统的水平扩展能力和容错能力。为此,它们采用同步复制和分片等技术,以确保跨节点的数据一致性。许多系统还依赖Raft或Paxos等分布式共识协议来保持副本之间的一致。CockroachDB和TiDB是常被提及的例子。也有批评者指出,许多NewSQL产品仍建立在传统关系型引擎的基础之上,这可能限制它们在大型、真正分布式部署中的表现。
其他系统中的类SQL语言
多个NoSQL产品提供了类似SQL的自有查询语言。Cassandra查询语言(CQL)是Apache Cassandra的原生接口。它与SQL使用相同的抽象概念,即由行和列组成的表,但语义有所不同。查询通常需要指定分区键,且该语言不支持连接和子查询。Couchbase的N1QL是一种面向JSON文档的查询语言,它将SELECT风格的语法扩展到嵌套的文档结构。这些语言降低了已熟悉SQL的开发者的学习门槛,同时也暴露了底层存储模型所带来的约束。
在企业应用中的持续主导地位
尽管替代方案不断增多,SQL仍是大量企业数据的默认选择。关系型和NewSQL系统通常被用于银行、会计和ERP等要求事务一致性有保障的业务场景。关系型数据库使用SQL,这是一种用于查询结构化数据的标准化语言。标准化、成熟的工具链以及庞大的熟练开发者群体,也使得许多NoSQL和NewSQL厂商同样提供SQL或类SQL接口。在实践中,组织往往是组合使用多种类型的数据库,而不是彻底取代关系型系统。
SQL与大语言模型
文本到SQL(Text-to-SQL)是指将自然语言问题转换为针对已知数据库的可执行SQL查询的任务。这项研究早于当前一代的大语言模型,但大语言模型使其足以应用于日常分析,如今该任务也主导了基于大语言模型的数据访问评估。
基准测试与评估
大多数已发表的结果都来自少数几个公开基准。Spider包含200个数据库上的10,181个问题和5,693条不同的查询,每个数据库都包含多张表。BIRD是一个大规模、跨领域的基准,包含95个大型真实数据库上的12,751个唯一问题-SQL对。BIRD的难度更高,部分原因在于它需要借助外部知识进行推理,例如同义词知识和取值示例。
主要指标是执行准确率,即检验预测查询与参考查询是否返回相同的结果。该指标有一个已知缺陷:错误的预测可能恰好产生与正确查询相同的输出。测试集准确率(test-suite accuracy)通过要求查询在多个数据库实例上都能通过来解决这一问题,这些实例由自动化的数据库增强方法生成。BIRD还报告了有效效率得分(valid efficiency score),用于比较查询与参考查询的运行时间。鲁棒性变体则直接检验泛化能力:Spider-Syn用同义词替换与模式相关的词语,Spider-Realistic则从问题中删除明确提及的列名。即便如此,基于执行的评分仍有局限。有一项研究得出结论,基于执行的准确率无法涵盖所有正确的SQL查询,因此人工审核仍是严肃评估的一部分。
典型错误与缓解措施
最常见的失败发生在模式链接(schema linking)环节,即识别问题所指的表和列。在一项针对开源模型的错误分析中,最常见的错误正是模式链接错误:模型会改动部分列名,或凭空编造列以及表与列之间的关联。一项对四个系统在Spider和BIRD上进行的更大规模研究发现,模式错误导致了超过80%的执行失败。从结构上看,这属于人工智能幻觉:模型虚构出看似合理、但在数据库中并不存在的引用。连接(join)错误也遵循类似规律,随着提示中加入的表越来越多,这类错误也越来越频繁,因为模型缺乏解析名称相近的表之间隐式连接路径的优先规则。
最危险的是那些无声无息的错误。使用错误列的查询可以毫无报错地运行,并返回看似合理的数字。过滤值是另一类隐蔽失败的来源:查询可能猜错了分类值的大小写,于是返回空结果集,却不解释原因。
一些缓解措施已成为标准做法。第一是模式检索:对于大型数据库,将所有表的描述都放入大语言模型的提示中并不现实,因此系统只传入相关的子集。一份实践报告发现,将模式限制在相关列上,是其流程中单项提升最大的改进。第二是基于执行的验证:候选查询在数据库上运行,结果随后被比较或投票。一种表现优异的方法通过多数投票,选择执行结果一致性最高的SQL。生成的查询还可以检查语法有效性,失败的查询则反馈给模型进行修复后重试。
自然语言接口与审核
自然语言接口让业务用户无需了解SQL即可查询数据,但也让人很容易信任错误的答案。准确性在很大程度上取决于文档。如果没有语义元数据层将晦涩的列名和编码的状态值映射到业务概念,模型就只能猜测不透明的列名、编造业务逻辑,并误读编码的状态值。因此,实践指南通常主张对高风险查询进行人工签核,并保持模式和术语表元数据的更新,而不是把一次性的模式导出视为足够。
生成SQL的安全性
生成的SQL应被视为不可信的输入,而非可信的产物。威胁不仅来自模型的困惑:攻击者可以精心构造输入,诱导生成并随后执行恶意SQL,这使其成为注入攻击的一种变体。配置也会加剧风险。在实践中,许多大语言模型到SQL的集成都授予了广泛的读取权限,有些甚至授予了写入权限。
标准的防御措施是分层的。语句过滤器可以在执行前只接受SELECT语句,并拒绝修改数据或包含多条语句的输入。应用程序应通过专用的只读数据库账户连接,而不是管理员账户,这样即使注入成功,也无法修改数据。行级安全和视图可以限制该账户能够访问的行和列。OWASP的指导同样建议在可能的情况下使用只读数据库账户。只读沙箱或副本则提供了另一层边界,使生成的查询永远不会在生产主库上运行。这些措施都无法让生成的SQL变得正确,它们只是在查询出错或具有恶意时,限制损害的范围。
参考来源
- Important Papers: Codd and the Relational Modeltwobithistory.org
- The History of SQLlearnsql.com
- Raymond F. Boyceen.wikipedia.org
- SQL his. SQL was initially developed at IBM by…medium.com
- History of SQL: The Evolution of a Database Languagemedium.com
- Donald Chamberlin & Raymond Boycehistoryofinformation.com
- SQLen.wikipedia.org
- SQL History and Evolution: From Origins to Modern Variantssoftwarepatternslexicon.com
- SQL History - W3Schoolsw3schools.in
- [PDF] A relational model of data for large shared data bankssemanticscholar.org
- The Evolution of the Computerized Databasearxiv.org
- DISTINCT — Use DISTINCT and understand when it changes...varsitytutors.com
- Codd's theoremen.wikipedia.org
- Codd's Theorem for Databases over Semiringsarxiv.org
- A Compiler for Operations on Relations with Bag Semanticsarxiv.org
- Information Retrieval P. BAXENDALE, Editor A Relational Model of Data forengineering.upenn.edu
- 1 Relational Database Model Introduced by E.F. Codd (1970)ovid.cs.depaul.edu
- Relational Model: Keys, Constraints & Integrity (2026)perfectnotes.org
- • CHAPTER 23 • Serious Flaws in SQLdl.acm.org
- Projection Operation: Relational Algebra Theory vs. The Reality of Queries - Informaticsemramadhan.com
- SQL Commands: DQL, DDL, DML, DCL & TCL with Examplesguru99.com
- SQL Sub-Languages: DDL DML DCL TCL DQL Explained with Examplescodersnexus.com
- SQL Commands: DDL, DML, DCL, TCL and DQL Explainedcodewithsql.com
- SQL Commands (DDL, DML, DQL, DCL, TCL) with Examplesmygreatlearning.com
- Commit, Rollback, Savepoint in MySQL Baeldung on SQLbaeldung.com
- PostgreSQL: Documentation: 18: ROLLBACK TO SAVEPOINTpostgresql.org
- SQL - Transactionstutorialspoint.com
- Understanding SQL Query Order of Executionbuiltin.com
- SQL Order of Operations: The Logical Execution Order Behind Every Query Trapprachub.com
- SQL Execution Order Explained: Why WHERE Runs Before SELECTsqlism.com
- PostgreSQL UNION, INTERSECT and EXCEPT Explainedchat2db.ai
- UNION EXCEPT INTERSECT keywordsquestdb.com
- The Set Operatorsdocs.oracle.com
- INTERSECT, UNION, EXCEPT, and MINUS - Alibaba Cloudalibabacloud.com
- UNION, INTERSECT, and EXCEPT clausesdocs.cloudera.com
- Set operations (SQL)en.wikipedia.org
- Achieving Data Privacy through Secrecy Views and Null-Based Virtual Updatesarxiv.org
- T-SQL three-valued logic and ANSI_NULLS setting!medium.com
- Issue #011: NULL and Three-Valued Logic: The SQL Traps That Cost You Silentlybytesbtrees.substack.com
- NULL and UNKNOWN (Transact-SQL) - SQL Serverlearn.microsoft.com
- SQL Aggregate Functions: COUNT, SUM, AVG, MIN, and MAXboot.dev
- Null (SQL)en.wikipedia.org
- Codd's Twelve Rulesred-gate.com
- No More Nulls!arxiv.org
- Database Research needs an Abstract Relational Query Languagearxiv.org
- Comprehending nullsarxiv.org
- Relational Algebra and Calculus with SQL Null Valuesarxiv.org
- Do NULL markers in SQL cause any harm?lemire.me
- Cascading Referential Integrity Constraintslearn.microsoft.com
- CREATEdocs.snowflake.com
- Referential Integrity: Definition and Examplesseedfa.st
- Transaction Isolation Levels (ODBC) - ODBC API Referencelearn.microsoft.com
- Multiversion concurrency controlen.wikipedia.org
- Snapshot isolationen.wikipedia.org
- Two-phase lockingen.wikipedia.org
- Lecture #17: Two-Phase Locking 15-445/645 Database Systems (Spring 2024)15445.courses.cs.cmu.edu
- Two-Phase Lockingmaxnilz.com
- All about Two-Phase Locking and a little bit MVCCdzone.com
- Multiversion Concurrency Control (MVCC)dev.to
- Deep Dive into Postgres Multi-Version Concurrency Control: Architecture, Snapshot Isolation, and Performance Optimizationmartinuke0.github.io
- Chapter 1sql-99.readthedocs.io
- SQL: American National Standard Adoptions by INCITS - The ANSI Blogblog.ansi.org
- IEC 9075en.wikipedia.org
- SQL-92en.wikipedia.org
- The History of SQL Standardslearnsql.com
- PostgreSQL: Documentation: 13: Appendix D. SQL Conformancepostgresql.org
- SQL:2023en.wikipedia.org
- The 16 Parts of the SQL Standard ISO/IEC 9075modern-sql.com
- Announcing the General Availability of the SQL:2023 Standardblogs.oracle.com
- SQL:2023 is finished: Here is what’s newpeter.eisentraut.org
- ISO/IEC 9075-1:2008 - Information technology — Database languages — SQL — Part 1: Framework (SQL/Framework)iso.org
- Conformance Levels in SQL-92modern-sql.com
- Oracle and Standard SQLdocs.oracle.com
- The SQL Standard - ISO/IEC 9075:2023 (ANSI X3.135) - The ANSI Blogblog.ansi.org
- Standard ANSI SQL: What It Is and Why It Mattersdbvis.com
- SQL Dialects: MySQL vs PostgreSQL vs SQL Server vs SQLitetutorialreference.com
- SQL Dialectsstructuredanalytic.com
- Understanding SQL Dialects: How Different Databases Speak Their Own SQL Languageblog.incubyte.co
- SQL Dialects Explained: How to Translate Between Different Database Systems - Data With Sarahdatawithsarah.com
- 0% found this document useful (0 votes)scribd.com
- Difference between T-SQL and PL-SQL - GeeksforGeeksgeeksforgeeks.org
- pgSQLen.wikipedia.org
- Transitioning from Oracle to Postgres: PL/SQL vs PL/pgSQLstormatics.tech
- T-SQL vs PL/pgSQL — Reference for SQL Server → PostgreSQLjamsql.com
- What is TSQL and how is it different from SQL, PL/SQL, and PL/pgSQL?tableplus.com
- Effective SQL Server Paging Strategies Beyond Basic OFFSET FETCHsqlpey.com
- SQL Dialects Differences — Standards vs Extensionsadhdecode.com
- SQL Server, PostgreSQL, MySQL: What's the Difference?datacamp.com
- SQL Dialect Variations (Syntax, Data Types, Procedural Extensions)relationaldbdesign.com
- SQL vs PL-SQL vs T-SQLguru99.com
- kindatechnical()kindatechnical.com
- 7 Refreshing Materialized Viewsdocs.oracle.com
- PostgreSQL: Documentation: 18: REFRESH MATERIALIZED VIEWpostgresql.org
- PostgreSQL: Documentation: 18: CREATE TRIGGERpostgresql.org
- Oracle Row-level Triggersoracletutorial.com
- Triggers Overviewjcsites.juniata.edu
- Chapter 10. Triggershsqldb.org
- ISO/IEC 9075-4:2011 - Information technology — Database languages — SQL — Part 4: Persistent Stored Modules (SQL/PSM)iso.org
- ISO/IEC 9075-4:2008 - Information technology — Database languages — SQL — Part 4: Persistent Stored Modules (SQL/PSM)iso.org
- Triggers Exercisescs.unibg.it
- Refreshing a materialized view - Amazon Redshiftdocs.aws.amazon.com
- SQL:2003en.wikipedia.org
- SQL:2003sqlpedia.org
- SQL Window Functions Explained: The Examples That Actually Made Them Clickmedium.com
- SQL Window Functionsthoughtspot.com
- SQL Window Functions and PARTITION BY in Practicemarmelab.com
- Window Functions in SQL - GeeksforGeeksgeeksforgeeks.org
- Recursive SQL: A Complete Guidepuppygraph.com
- Compiling Linear Datalog to SQL for Program Analysisarxiv.org
- Recursive aggregatesimage-ppubs.uspto.gov
- MySQL :: MySQL 8.4 Reference Manual :: 15.2.20 WITH (Common Table Expressions)dev.mysql.com
- Introducing Recursive Common Table Expressions: Making Databricks SQL Turing Completedatabricks.com
- Using GROUP BY with ROLLUP, CUBE, and GROUPING SETSlearn.microsoft.com
- Grouping Sets, Rollup and Cube in SQL - by Avi Chawlablog.dailydoseofds.com
- SQL Server ROLLUPsqlservertutorial.net
- GROUPING SETS & CUBE · Intermediate & Advanced SQLdatavidhya.com
- Questions About CUBE, ROLLUP and GROUPING SETs That You Were Too Shy to Askred-gate.com
- SQL Grouping Sets, ROLLUP & CUBE: Multi-Dimensional Aggregation Deep Divepipecode.ai
- Window Functions in SQL Serverred-gate.com
- 3. Recursive Common Table Expressionssqlite.org
- Group By in SQL Server with CUBE, ROLLUP and GROUPING SETS Examplesmssqltips.com
- XMLen.wikipedia.org
- Introduction to XQuery in SQL Server 2005learn.microsoft.com
- XML Support in Microsoft SQL Server 2005learn.microsoft.com
- JSON support in SQL Server 2016red-gate.com
- SQL Server 2016: JSON integrationlearn.microsoft.com
- The SQL:2023 standard has already been published, get to know itblog.desdelinux.net
- SQL/JSON Data Model and JSON in SQLtigerdata.com
- Getting Started with JSON Support in SQL Server 2016databasejournal.com
- Database Normalization Explained: 1NF, 2NF, 3NF, BCNF ...perfectnotes.org
- Normalization (1NF, 2NF, 3NF, BCNF)theintactone.com
- Normalization in DBMS: 1NF, 2NF, 3NF, BCNF Explainedtutorsbot.com
- DBMS Normalization: 1NF, 2NF, 3NF Database Exampleguru99.com
- Database Normalization: 1NF, 2NF, 3NF & BCNF Explained (2026)generalistprogrammer.com
- PostgreSQL: Documentation: 18: 5.9. Row Security Policiespostgresql.org
- SQL Injection Prevention - OWASP Cheat Sheet Seriescheatsheetseries.owasp.org
- Top 5 Best Practices for Preventing SQL Injection Attackskiuwan.com
- PostgreSQL Row Level Security (RLS): Basics and Examplessatoricyber.com
- Multi-tenant data isolation with PostgreSQL Row Level Securityaws.amazon.com
- SQL GRANT and REVOKE Permissionstutorialreference.com
- Revoke Grant SQLperlego.com
- ODBC on Linux and UNIX platformseasysoft.com
- What is Open Database Connectivity (ODBC)?techtarget.com
- Call Level Interfaceen.wikipedia.org
- SP 96: SQL Access Group's Call-Level Interfacejacobfilipp.com
- ISO/IEC 9075-3:2023 - Information technology — Database languages SQL — Part 3: Call-Level Interface (SQL/CLI)iso.org
- ISO/IEC CD 9075-3 - Information technology — Database languages SQL — Part 3: Call-Level Interface (SQL/CLI)iso.org
- Introduction to JDBCcondor.depaul.edu
- Main Components : 1. The JDBC APIpvpsiddhartha.ac.in
- JDBC Drivers (About Database Adapters)docs.oracle.com
- Data warehouse with a domain fact tableimage-ppubs.uspto.gov
- Star Schema vs Snowflake Schema: 10 Key Differencesintegrate.io
- Star and Snowflake Schemasoracle.com
- Verticaen.wikipedia.org
- ColumnStore Storage Architecturemariadb.com
- Columnar Databases: Why They're Faster for Analyticshackernoon.com
- 7 best columnar database tools for 2026 - Guideflow Blogguideflow.com
- Apache Hiveen.wikipedia.org
- Apache Hive : Hive on Sparkhive.apache.org
- Comparing Foundational Features of Trino, Hive & Sparkstarburst.io
- Apache Hive vs Spark SQL: Feature wise comparison - DataFlairdata-flair.training
- Intensional RDB Manifesto: a Unifying NewSQL Model for Flexible Big Dataarxiv.org
- Graph Database vs NoSQL: Where Graph Fitstigergraph.com
- NoSQL database types (document, key-value, column, graph) in DBMS Theory - Explained with Diagrams & Examplesleyaa.ai
- 4 Types of NoSQL Databasesblazeclan.com
- Types Of Databasesmongodb.com
- Distributed SQL vs. NewSQL: Navigating the Evolution of Scalable Database - Liam ERDliambx.com
- About Apache Cassandra - DataStax Docsdocs.datastax.com
- What is NewSQL? |NewSQL Defineddremio.com
- NewSQL Databases: Distributed SQL for Real-Time Applicationsconduktor.io
- 0% found this document useful (0 votes)scribd.com
- Data Modeling In NoSQLmeegle.com
- CQLdocs.datastax.com
- SEC-NoSQL: Towards Implementing High Performance Security-as-a-Service for NoSQL Databasesarxiv.org
- MCS-SQL: Leveraging Multiple Prompts and Multiple-Choice Selection For Text-to-SQL Generationarxiv.org
- CodeS: Towards Building Open-source Language Models for Text-to-SQLarxiv.org
- ReEx-SQL: Reasoning with Execution-Aware Reinforcement Learning for Text-to-SQLarxiv.org
- Evaluating Cross-Domain Text-to-SQL Models and Benchmarks Mohammadreza Pourrezaarxiv.org
- Blar-SQL: Faster, Stronger, Smaller NL2SQLarxiv.org
- Semantic Layers for Reliable LLM-Powered Data Analytics: A Paired Benchmark of Accuracy and Hallucination Across Three Frontier Modelsarxiv.org
- Text-to-SQL AI: How the Pipeline Works and Breaks [2026]atlan.com
- A Survey on Employing Large Language Models for Text-to-SQL Tasksarxiv.org
- Enhancing Text-to-SQL through SQL Parsing and Reasoningaclanthology.org
- Why Text-to-SQL Fails on Real Enterprise Schemas (A 10% Accuracy Case Study)medium.com
- (PDF) SQL Injection in LLM-Generated Queries: Systematic Analysis of Detection Gaps and Security Risksresearchgate.net
- LLM Prompt Injection Prevention - OWASP Cheat Sheet Seriescheatsheetseries.owasp.org
- From 68% to ~100%: How We Built a Text-to-SQL System That Gets Smarter Every Day - DEV Communitydev.to
- When Text Becomes Code: Defending LLM–Database Integrations from Prompt Injection - DEV Communitydev.to
- Rethinking Schema Linking: A Context-Aware Bidirectional Retrieval Approach for Text-to-SQLarxiv.org