关系型语言:用户只需使用一种声明式语言(即 SQL)来指定他们想要的结果。而 DBMS 则负责确定产生该答案的最有效执行计划。
关系代数是基于集合(Set)的,这意味着它是无序且不允许存在重复项的。而SQL是基于多重集(Bags)的,这意味着它是无序且允许存在重复项的。之所以这样做是为了性能,去重(Distinct)是一个非常昂贵的操作,因为需要排序或哈希。
SQL历史:SQL 是一种用于关系型数据库的声明式查询语言 。它最初在 20 世纪 70 年代作为 IBM System R 项目的一部分被开发出来。IBM 最初将其命名为 "SEQUEL" (结构化英语查询语言)。到了 80 年代,名称更改为现在的 "SQL"(结构化查询语言)。
该语言由不同类别的命令组成:
-
数据操纵语言 (DML) :包括
SELECT(查询)、INSERT(插入)、UPDATE(更新)和DELETE(删除)语句。 -
数据定义语言 (DDL):用于定义表、索引、视图及其他对象的模式(Schema)。
-
数据控制语言 (DCL):涉及安全性、访问权限控制。
SQL 并不是一种死语言。它每隔几年就会更新一些新特性。SQL-92 是数据库管理系统(DBMS)声称支持 SQL 的最低标准。每个供应商都在一定程度上遵循该标准,但同时也拥有许多专有的扩展功能。
以下是 SQL 标准各版本发布的一些主要更新:
-
SQL:1999:正则表达式、触发器。
-
SQL:2003 :XML 支持、窗口函数 (Windows)、序列 (Sequences)。
-
SQL:2008:截断 (Truncation)、高级排序 (Fancy sorting)。
-
SQL:2011:时态数据库 (Temporal DBs)、流水线式 DML。
-
SQL:2016 :JSON 支持、多态表 (Polymorphic tables)。
关键字和子句:关键字指SELECT,FROM,WHERE这些单词本身。子句指关键字加上后面跟着的代码片段,构成一个完整的逻辑单元,例如SELECT name。
FROM:FROM是SQL执行逻辑的第一步,其作用是定义查询的初始数据集,这个空间可以是一张简单的表,也可以是多个表的组合。
WHERE:WHERE的本质是一个布尔表达式求值器,它对FROM搬过来的每一行数据进行一次判断。我们向WHERE输入一个完整的元组,然后将元组中的数据带入WHERE后的公式中,TRUE则留库,FALSE则丢弃。
SELECT:虽然字面意思上SELECT含义为挑选,并且我们也确实可以通过SELECT 列 FROM student; 来筛选出对应的列。但更准确来讲SELECT的作用是输出。
例如SELECT 1 FROM student WHERE id=222。便是对于student中的每一行,如果满足id=222,便输出一个1。
-
SELECT 常量 :
SELECT 1 FROM student;- 每一行都执行一次"产生数字 1"的动作。
-
SELECT 列名 :
SELECT name FROM student;- 每一行都执行一次"从当前行提取 name 字段内容"的动作。
-
SELECT 函数 :
SELECT UPPER(name) FROM student;- 每一行都执行一次"把当前行的 name 转大写"的函数调用。
-
SELECT 标量子查询 :
SELECT (SELECT MAX(gpa) FROM student) FROM student;- 虽然内层结果是不变的,但逻辑上,外层每扫描到一个人,都要去问一下内层:"现在的最高 GPA 是多少?"
连接(Join):Join将来自一个或多个表的列组合起来,并产生一个新表。它用于表达涉及跨越多个表的数据的查询。
示例:哪些学生在15-721中获得了A。
sql
CREATE TABLE student (
sid INT PRIMARY KEY,
name VARCHAR(16),
login VARCHAR(32) UNIQUE,
age SMALLINT,
gpa FLOAT
);
CREATE TABLE course (
cid VARCHAR(32) PRIMARY KEY,
name VARCHAR(32) NOT NULL
);
CREATE TABLE enrolled (
sid INT REFERENCES student (sid),
cid VARCHAR(32) REFERENCES course (cid),
grade CHAR(1)
);
SELECT s.name
FROM enrolled AS e, student AS s
WHERE e.grade = 'A' AND e.cid = '15-721'
AND e.sid = s.sid;
FROM后是数据的来源:enrolled AS e,student AS s,用逗号连接两个表表示做笛卡尔积,产生所有的学生和选课记录组合。
WHERE ... AND e.sid=s.sid:这行代码是关联条件。
WHERE e.grade='A':这是过滤谓词。
聚合函数(Aggregates):聚合函数接收一组元组作为输入,并输出一个单一的标量值。
常见聚合函数:
-
AVG(COL): 计算指定列的平均值。 -
MIN(COL): 计算指定列的最小值。 -
MAX(COL): 计算指定列的最大值。 -
COUNT(COL): 计算关系中的元组数量(行数)。
示例:获取登录名包含"@cs"的学生人数,以下三条查询语句等价:
sql
SELECT COUNT(*) FROM student WHERE login LIKE '%@cs';
SELECT COUNT(login) FROM student WHERE login LIKE '%@cs';
SELECT COUNT(1) FROM student WHERE login LIKE '%@cs';
注:COUNT(*)统计结果集中的总行数,COUNT(列名)统计指定列中非NULL值的行数,如果某一行在该列的值是 NULL,这一行就会被跳过,不计入总数。COUNT(1),为结果集的每一行分配一个常量,然后统计这个常量出现了多少次,它和COUNT(*)一样快。
一个SELECT语句可以包含多个聚合函数,例如获取登录名包含"@cs"的学生人数及其平均GPA。
sql
SELECT AVG(gpa), COUNT(sid)
FROM student WHERE login LIKE '%@cs';
DISTINCT关键字:部分聚合函数如 COUNT, SUM, AVG)支持 DISTINCT 关键字,其作用为过滤掉重复的值,确保每个唯一值只被计算一次。示例:获取登录名包含 "@cs" 的唯一学生人数及其平均 GPA。
sql
SELECT COUNT(DISTINCT login)
FROM student WHERE login LIKE '%@cs';
GROUP BY子句:如果在聚合函数之外输出其他列,该行为是未定义的。正如下面的错误示例那样,我们得到了一个平均分数字和一列课程id,而我们最后得到的平均分数字是所有人所有课程的平均绩点,这无法和任何一门课程对应。
sql
SELECT AVG(s.gpa), e.cid
FROM enrolled AS e, student AS s
WHERE e.sid = s.sid;
因此,SELECT子句中,凡是没有被聚合函数包裹的列,必须全数出现在GROUP BY子句中。
sql
SELECT AVG(s.gpa), e.cid
FROM enrolled AS e, student AS s
WHERE e.sid = s.sid
GROUP BY e.cid;
在生成表后,数据库会扫描表并根据e.cid进行物理或逻辑上的分类,数据库为每一个唯一的cid创建一个桶,一旦分组完成,数据库在每个桶的内部独立运行聚合函数。
HAVING子句:WHERE在数据分组前进行过滤,作用于原始行。HAVING在数据分组后进行过滤,作用于聚合后的结果。HAVING会检查计算出的聚合值,把不符合条件的桶舍弃。示例:获取平均 GPA 大于 3.9 的课程集合。
sql
SELECT AVG(s.gpa) AS avg_gpa, e.cid
FROM enrolled AS e, student AS s
WHERE e.sid = s.sid
GROUP BY e.cid
HAVING avg_gpa > 3.9;
注意:上述查询语法被许多主流数据库系统支持,但不符合 SQL 标准。为了符合标准,我们必须在 HAVING 子句的主体中重复使用 AVG(s.gpa):
sql
SELECT AVG(s.gpa), e.cid
FROM enrolled AS e, student AS s
WHERE e.sid = s.sid
GROUP BY e.cid
HAVING AVG(s.gpa) > 3.9;
字符串操作:SQL标准规定字符串是大小写敏感 的,且必须使用单引号括起来。SQL 提供了一些函数,可以在查询的任何部分对字符串进行操作。
模式匹配:LIKE关键字用于谓词(即WHERE子句)中的字符串匹配。
-
"%":匹配任何子字符串(包括空字符串)。
-
"_" :匹配任何单个字符。
字符串函数:比如SUBSTRING(S,B,E),从字符串S中,从位置B开始提取到位置E的子串。UPPER(S),将S转换为全大写。
字符串拼接:使用||可以将两个或多个字符串拼接成一个单一的字符串。
日期和时间:用于操作日期和时间属性的操作,这些操作既可以用于输出列表,也可以用于谓词,即WHERE子句。
注:处理日期和时间操作的具体语法在不同的数据库系统(如 MySQL, PostgreSQL, Oracle)之间差异极大。
输出重定向:输出重定向可以使DBMS将查询结果存储到另一张表中,而不是将结果返回给客户端。之后,可以在后续的查询中访问这些数据。
存入新表:将查询结果存储到一个新创建的表中。SELECT...INTO...表示创建并插入。
sql
SELECT DISTINCT cid INTO CourseIds FROM enrolled;
-- 这条语句会自动创建一个名为 CourseIds 的表,
-- 并将从 enrolled 表中查到的唯一课程 ID 插入其中。
存入已有表:将查询结果存储到数据库中已经存在的表中。目标表必须拥有与查询结果相同数量和类型的列,但查询结果中的列名不需要与目标表一致。INSERT INTO ... (SELECT ...)表示仅插入。
sql
INSERT INTO CourseIds (SELECT DISTINCT cid FROM enrolled);
-- 这里的 CourseIds 表必须是之前已经创建好的。
输出控制:在数据库中,除非显式要求,否则结果的顺序是不可预测的。我们可以使用ORDER BY子句强行对元组进行排序。
sql
-- 【基础排序】
-- 默认排序方式为升序 (ASC)
SELECT sid, grade FROM enrolled
WHERE cid = '15-721'
ORDER BY grade;
-- 【降序排序】
-- 我们可以手动指定 DESC 来反转顺序
SELECT sid, grade FROM enrolled
WHERE cid = '15-721'
ORDER BY grade DESC;
-- 【多列排序】
-- 我们可以使用多个 ORDER BY 子句来处理"平局"情况(即第一列相同时的次级排序)
SELECT sid, grade FROM enrolled
WHERE cid = '15-721'
ORDER BY grade DESC, sid ASC; -- 先按成绩降序,成绩相同的按学号升序
-- 【表达式排序】
-- 在 ORDER BY 子句中可以使用任何任意表达式
SELECT sid FROM enrolled
WHERE cid = '15-721'
ORDER BY UPPER(grade) DESC, sid + 1 ASC;
结果集限制:默认情况下,DBMS会返回查询产生的所有元组,我们可以使用LIMIT子句来限制返回结果的数量。
sql
-- 【限制行数】
-- 只获取满足条件的前 10 个学生
SELECT sid, name FROM student
WHERE login LIKE '%@cs'
LIMIT 10;
-- 【偏移量限制】
-- 提供一个 OFFSET(偏移量)来返回结果中的某个区间
-- 下面的语句表示:跳过前 10 个,取接下来的 20 个(即第 11 到 30 名)
SELECT sid, name FROM student
WHERE login LIKE '%@cs'
LIMIT 20 OFFSET 10;
注:LIMIT和ORDER BY配合使用,否则返回的是随机读取的结果。
嵌套查询:也被称为子查询,允许像写程序嵌套函数一样,在一个查询中调用另一个查询。嵌套查询通常难以优化。
作用域规则:内部查询可以访问外部查询的作用域(即子查询可以读取外部表的属性),但外部查询无法访问内部查询的作用域。
1.SELECT输出目标。
sql
SELECT (SELECT 1) AS one FROM student;
2.FROM子句
sql
SELECT name
FROM student AS s, (SELECT sid FROM enrolled) AS e
WHERE s.sid = e.sid;
3.WHERE子句
sql
SELECT name FROM student
WHERE sid IN ( SELECT sid FROM enrolled );
示例1:获取选修了15-445课程的学生姓名。
sql
SELECT name FROM student
WHERE sid IN (
SELECT sid FROM enrolled
WHERE cid = '15-445'
);
示例2:查找至少选修了一门课程且ID最大的学生记录。
sql
SELECT student.sid, name
FROM student
JOIN (SELECT MAX(sid) AS sid
FROM enrolled) AS max_e
ON student.sid = max_e.sid;
注:这里的JOIN其实没有起到连接的作用,表max_e的唯一列sid在左表student中已经有了,它更像是在进行筛选。
示例3:查找所有没有学生选修的课程。EXISTS运算符只有两种状态,True则子查询中至少有一行记录,False则子查询中一行记录都没有。NOT EXISTS则相反。
sql
SELECT * FROM course
WHERE NOT EXISTS (
SELECT * FROM enrolled
WHERE course.cid = enrolled.cid
);
窗口函数:窗口函数在一组相关的元组上执行滑动计算,它类似于聚合操作,但和普通聚合不同的是,这些元组不会被压缩成单个输出行。
窗口函数可以是之前讨论过的任何聚合函数(如 SUM, AVG 等)。此外,还有一些特殊的窗口函数,比如ROW_NUMBER,当前行的序列号,RANK,当前行在排序中的排名。
分组:OVER 子句指定了在计算窗口函数时如何将元组组合在一起。使用 PARTITION BY 来指定分组依据。示例中,分组后,会在各自组内进行行号排序,最后再依据ORDER BY cid将各个组排好序并合并。
sql
-- 示例:按课程 ID 分组,并为每门课的学生生成行号
SELECT cid, sid, ROW_NUMBER() OVER (PARTITION BY cid)
FROM enrolled ORDER BY cid;
注:普通的GROUP BY会把10行数据变成一行(比如只给一个平均值),而窗口函数OVER(PARTITION BY ...)依然会输出10行,只是在每一行旁边多了一列计算出的汇总或排名信息。
我们也可以在 OVER 子句中使用 ORDER BY。这样即使数据库内部发生变化,也能确保结果的排序是确定的。
sql
SELECT *, ROW_NUMBER() OVER (ORDER BY cid)
FROM enrolled ORDER BY cid;
示例:查找每门课程中成绩排名第二的学生。
sql
SELECT * FROM (
SELECT *, RANK() OVER (PARTITION BY cid ORDER BY grade ASC) AS rank
FROM enrolled
) AS ranking
WHERE ranking.rank = 2;
公共表表达式(CTE):在编写复杂查询时,公共表表达式(CTE) 是窗口函数或嵌套查询(子查询)的一种替代方案。它们提供了一种为大型查询编写辅助语句的方法。CTE 可以被看作是一个仅限于单个查询作用域内的临时表。
WITH子句将内部查询的输出绑定到一个具有该名称的临时结果中。
示例:生成一个名为 cteName 的 CTE,它包含一个单一元组(行),且该元组有一个属性设为"1"。然后从这个 CTE 中选择所有属性。
sql
WITH cteName AS (
SELECT 1
)
SELECT * FROM cteName;
我们可以在AS关键字之前将输出列绑定到特定名称:
sql
WITH cteName (col1, col2) AS (
SELECT 1, 2
)
SELECT col1 + col2 FROM cteName;
单个查询可以包含多个 CTE 声明:
sql
WITH cte1 (col1) AS (SELECT 1),
cte2 (col2) AS (SELECT 2)
SELECT * FROM cte1, cte2;
在 WITH 之后添加 RECURSIVE 关键字可以允许 CTE 引用自身。这使得在 SQL 查询中实现递归 成为可能。通过递归 CTE,SQL 被证明是 图灵完备的(Turing-complete),这意味着它的计算表达能力与更通用的编程语言一样强大(尽管写起来可能稍微繁琐一些)。
示例:打印从 1 到 10 的数字序列。当某一轮计算后,SELECT语句返回的结果集是空,那么下一轮就没与输入数据了,没有输入递归引擎就会认为任务终止,自动停止并合并所有结果。
sql
WITH RECURSIVE cteSource (counter) AS (
( SELECT 1 ) -- 锚点部分:初始化
UNION
( SELECT counter + 1 FROM cteSource -- 递归部分:引用自身
WHERE counter < 10 ) -- 终止条件
)
SELECT * FROM cteSource;
UNION在每一轮递归产生新行后,会检查这些行是否已经存在于之前的结果集中,如果重复,该行被丢弃。
SQL中递归和通常的递归有所不同。在WITH RECURSIVE运行时,数据库在内存中维护这两个逻辑表。
-
工作表 (Working Table / Intermediate Table):
-
临时性 :它只保存上一轮刚产生的"新鲜"数据。
-
动力源:它是下一轮递归的唯一输入。一旦某轮计算完成,旧的工作表会被清空,填入这一轮产生的新数据。
-
-
结果表 (Result Table / Accumulator):
-
累积性:它是最终我们要看到的那个完整清单。
-
只增不减:每一轮新产生的数据都会被"复制"一份丢进结果表里,直到递归彻底结束。
-
FROM cteSource中我们引用的不是当前的结果表,而是工作表。可以看到,SQL中递归的逻辑不是基于栈,它没有压栈和回溯这样的操作。