什么是SQL?
基础
一般的SQL查询语句的语法顺序/结构如下:
1 | SELECT [DISTINCT] <select_list> |
SELECT:选择列DISTINCT:去重FROM:选择表格WHERE:筛选条件GROUP BY:分组HAVING:对分组后的结果过滤ORDER BY:排序ASC:升序DESC:降序
LIMIT:截断返回行数
而它们的执行顺序是:FROM → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
入门
以这个数据库为例( https://hyper-db.de/interface.html )

如果我们想知道一共有哪些教授,那么我们只需要:
- 列出Professors这张表里的
- Name这一列的所有内容
对应的命令:
SELECT后跟列名FROM后跟表名
1 | SELECT Name |

DISTINCT
假如我们只关注Professors表里一共有多少种不同的Level:
如果用之前的方法它会重复输出:
1 | SELECT Level |

所以我们需要用DISTINCT来去重:
1 | SELECT DISTINCT Level |

ORDER BY
在结果较多时我们往往希望将结果排序以便我们后续操作,这是需要用到ORDER BY:
ASC:升序DESC:降序
1 | SELECT Name, Level |

LIMIT
也可以只查看前几条结果:
1 | SELECT Name |

WHERE
如果现在想要加上一点条件,比如说只显示Level等于C4的教授,那么就是:
WHERE后面跟筛选条件
1 | SELECT Name |

命名
有时我们希望自己命名查询结果的列名,这时候就需要AS:
1 | SELECT Name AS Prof_name |

除此之外,有些时候筛选会遇到2个表里有相同的列名,这个时候就需要命名每个表以此来区分:
- 在每个表后加上空格以及新的名字(例如
Students s) - 在之后的部分就可以用s指代这张表
比如说我们想知道学生”Fichte”上了哪些课:
1 | SELECT s.Name, a.LectureNr |

COUNT
如果我们想知道一共有多少名教授,就需要COUNT来帮助我们数数:1
2SELECT COUNT(*)
FROM Professors

GROUP BY
如果我们想知道每个Level各有多少教授,就需要GROUP BY来帮助我们进行分组:
1 | SELECT Level, COUNT(*) |

需要注意的一点是,SELECT里出现的列必须是在GROUP BY里出现过的列。否则:

但是通过GROUP BY a, b, c这种只会得到最细粒度的分组,即每个(a,b,c)一行。
那我们同时还想得到更粗颗粒度的分组结果(比如按照(a,b),(a),(a,c)这些分组)该怎么办呢?
CUBE
1 | GROUP BY CUBE(a,b,c) |
CUBE会处理所有子集分组。即当维度(CUBE括号里的列个数)为n时,会产生$2^n$种分组。
例如n=3时,一共8种:
- (a,b,c)
- (a,b)
- (a,c)
- (b,c)
- (a)
- (b)
- (c)
- () 全表总计
CUBE的输出里某些维度列会出现NULL,它代表这一行在该维度上“被汇总掉了”(对所有该维度值求和/求平均等)。比如说在按照(a,b)分组的部分内容里,那么c栏内容就会是NULL。
因为CUBE会显示/返回所有分组层级的结果,所以通常要用WHERE ... IS NULL来挑某一层。
例子:只要按(a,b)的汇总,即c IS NULL:
1 | SELECT a, b, s |
ROLLUP
与CUBE不同,ROLLUP会沿着维度列表“逐层向上汇总。
1 | GROUP BY ROLLUP(a,b,c) |
例如n=3的情况,ROLLUP会计算这几个分组:
(a,b,c)
(a,b)
(a)
()
不会处理(a,c)之类的分组结果。
HAVING
在分组的基础上我们往往希望再加点筛选条件,这个时候就需要HAVING来对分组后的结果过滤。
比如说我们想知道哪些Level的人数超过了3:
1 | SELECT Level, COUNT(*) |

子查询/嵌套
假如我们想知道哪些学生的学期数在平均学期数以下,就需要拆成2步来完成
计算平均学期数(先生成一个子列表)
1
2SELECT AVG(Semester)
FROM Students
筛选
1
2
3SELECT Name
FROM Students
WHERE Semester <
拼起来就是:
1 | SELECT Name |

或者也可以:
1 | SELECT Name |

这样会高效很多。
WITH AS
WITH AS的功能是在一条 SQL 里给一段子查询/临时结果起名字,后面就能像用表一样用它。
注意,WITH AS和子查询是等价的,但是用WITH AS会更容易阅读和理解。
基本语法:
1 | WITH 名字(可选列名列表) AS ( |
例子:查询C4教授的房间(先找C4教授,再查询他们的房间)
1 | WITH C4Prof AS ( |
也可以用VALUES临时造数据:
1 | WITH Professors ( persnr , name , paygrade , room , salary , taxclass ) as |

又或者是之前这个例子:
1 | SELECT Name |
改成用WITH AS就是:
1 | WITH avg AS (SELECT AVG(Semester) AS avg_semester |
同时命名多个临时结果时只需要写一个WITH:
1 | WITH examination ( MatrNr , CourseNr , PersNr , Grade ) as ( |
建表
基础
1 | CREATE TABLE Students ( |
含义:
CREATE TABLE 表名 (...):创建表- 括号里每行:
列名 数据类型
常见数据类型(不同库略有差别):
- 整数:
INT/BIGINT - 小数:
DECIMAL(p,s)(精确)/DOUBLE(浮点) - 字符串:
VARCHAR(n)/TEXT - 日期时间:
DATE/TIMESTAMP - 布尔:
BOOLEAN
约束
但我们往往需要加上些条件,比如说非空,指定主键:
主键
PRIMARY KEY(唯一 + 非空)NOT NULL(不允许空)UNIQUE(不允许重复)DEFAULT(缺省值)CHECK(自定义规则)1
Grade DECIMAL(2,1) CHECK (Grade >= 1.0 AND Grade <= 5.0)
例子:
1 | CREATE TABLE Professors ( |
CTAS
CREATE TABLE AS SELECT(简称 CTAS),就是用查询结果建表。
例子:
1 | CREATE TABLE C4_Professors AS |
特点:
- 新表会包含查询出来的列
- 很多数据库会把类型推导出来
- 约束(主键/外键/NOT NULL 等)通常不会自动带过去(不同库细节不同),需要另加
临时表(TEMP / TEMPORARY)
只在当前会话存在,断开就没了:
1 | CREATE TEMP TABLE tmp_top_students AS |
窗口函数
窗口函数(window function)可以把它理解成:“对每一行,都能基于‘一组相关的行’计算出一个值,但又不会把行合并掉”。
和之前的分组(GROUP BY)的区别:
- 普通聚合 + GROUP BY:会把多行“压成一行”(一组只剩一行)。
- 窗口函数:每一行都还在,但可以在每一行旁边附加一个聚合/排名/累计值。
窗口函数的典型形式是:
1 | <窗口函数>(...) OVER ( |
PARTITION BY ...:按什么分组(每组独立算)ORDER BY ...:组内顺序(排名/累计/相邻必备)Frame:ROWS .../RANGE ...ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING(按行数滑窗)RANGE BETWEEN 5000 PRECEDING AND 5000 FOLLOWING(按值范围滑窗)
例子1:比如说我们同时想知道每个学生上的课的总数以及具体是哪些课的话,就需要:
1 | SELECT s.name, |

或者
1 | SELECT s.name, |
也是一样的效果。
例子2:假如我们现在只关注现有的test表,想知道每个学生的排名:
1 | SELECT s.name, t.Grade, rank()OVER ( ORDER BY t.Grade ASC) |

这里的rank会自动实现跳号的功能,即如果A,B的值为1,C的值为2,那么A,B的排名都是1,C的排名会自动变成3,而不是2。
常用的窗口函数
1)排名类:
用来给每行一个名次/序号。
ROW_NUMBER():每行一个唯一序号(不管并列)(1,2,3,4…)RANK():并列同名次,会跳号(1,1,3…)DENSE_RANK():并列同名次,不跳号(1,1,2…)PERCENT_RANK():把排名映射到 0~1(相对排名)CUME_DIST():累计分布(<= 当前行的比例)NTILE(n):分桶,把排序后的行分成 n 份(比如分成 4 桶做四分位)
2)聚合类(在GROUP BY里用的聚合函数也可以用在窗口上):
SUM(...) OVER (...):组内总和 / 累计和AVG(...) OVER (...):组内平均 / 滑动平均COUNT(...) OVER (...):组内计数 / 累计计数MIN/MAX(...) OVER (...):组内最小/最大(也可做滚动最小/最大)
3)取相邻对象(分析趋势、做差分)
LAG(x, k, default):往前第 k 行的 x(默认 k=1)LEAD(x, k, default):往后第 k 行的 xFIRST_VALUE(x):窗口内第一行的 xLAST_VALUE(x):窗口内最后一行的 xNTH_VALUE(x, n):窗口内第 n 行的 x(不是所有库都有)
递归/Recursion
递归的基本框架:
1 | WITH RECURSIVE 名字(列...) AS ( |
SQL里递归的写法和Induction/Iteration一样。先定义base case,再一层一层递进。
或者说是先给出起始集合,再反复应用递推规则,不断扩张结果集合,直到没有新东西为止。
而不是像其他编程语言里一样一直调用自己,直到碰到base case,然后停止。
在此基础上我们还可以通过定义一列新的内容(次数,长度等)以及增加基于这列信息的判断条件,来实现控制循环次数(上限)。(见例子3)
例子1
生成1-5
1 | WITH RECURSIVE nums(n) AS ( |

例子2
假设我们现在希望查询“Der Wiener Kreis”(LectureNr=5259) 这门课的的所有前置课程(包括直接和间接)。
普通的查找直接前置课程:
1 | SELECT Title |
查询第n轮前置就是:
1 | select v1.Predecessor |
我们把这个完整查询拆成2步:
利用递归创建子查询(临时表),在原来require表的基础上添加所有间接前置课程
1
2
3
4
5
6
7
8
9
10
11
12
13
14WITH RECURSIVE TransPrereq (Prereq, Successor) AS (
-- 递归的基础查询
SELECT Predecessor, Successor
FROM require
UNION ALL
-- 递归部分 (Prereq -> ... -> Successor)
SELECT t.Prereq, r.Successor
FROM TransPrereq t, require r
WHERE t.Successor = r.Predecessor
)
SELECT *
FROM TransPrereq每次递归时相当于有这样一个链:
1
t.Prereq -> t.Successor = r.Predecessor -> r.Successor
我们将一头一尾加进结果里。

在里面找’Der Wiener Kreis’的所有前置课程
合并起来:
1 | WITH RECURSIVE TransPrereq (Prereq, Successor) AS ( |
例子3
考虑下面这个图:

用SQL表示是这样的:
1 | WITH recursive singleDirection (a ,b ) as ( |
如果我们现在想判断从1出发是否能到6,我们就可以这样:
- 利用递归生成一张查询结果表,列出所有可到达的pair
- 然后找(1,6)是否在里面
1 | WITH recursive singleDirection (a ,b ) as ( |
为了避免无穷展开,我们现在加个限制:图的直径为4,即不考虑长度超过4的路径,那么就可以加一列来记录路径长度并且限制长度:
1 | WITH recursive singleDirection (a , b) as ( |