Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

什么是SQL?

基础

一般的SQL查询语句的语法顺序/结构如下:

1
2
3
4
5
6
7
SELECT [DISTINCT] <select_list>
FROM <table_or_subquery> [JOIN ... ON ...]
WHERE <row_conditions>
GROUP BY <group_cols>
HAVING <group_conditions>
ORDER BY <sort_exprs>
LIMIT <n> [OFFSET <m>];
  • 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

image-20251229014957843

如果我们想知道一共有哪些教授,那么我们只需要:

  • 列出Professors这张表里的
  • Name这一列的所有内容

对应的命令:

  • SELECT后跟列名
  • FROM后跟表名
1
2
SELECT Name
FROM Professors

image-20251229015523442

DISTINCT

假如我们只关注Professors表里一共有多少种不同的Level:

如果用之前的方法它会重复输出:

1
2
SELECT Level
FROM Professors

image-20251229174125626

所以我们需要用DISTINCT来去重:

1
2
SELECT DISTINCT Level
FROM Professors

image-20251229174138624

ORDER BY

在结果较多时我们往往希望将结果排序以便我们后续操作,这是需要用到ORDER BY

  • ASC:升序
  • DESC:降序
1
2
3
SELECT Name, Level
FROM Professors
ORDER BY Level ASC;

image-20251229203421452

LIMIT

也可以只查看前几条结果:

1
2
3
SELECT Name
FROM Professors
LIMIT 3

image-20251229203523308

WHERE

如果现在想要加上一点条件,比如说只显示Level等于C4的教授,那么就是:

  • WHERE后面跟筛选条件
1
2
3
SELECT Name
FROM Professors
WHERE Level = 'C4'

image-20251229015504737

命名

有时我们希望自己命名查询结果的列名,这时候就需要AS

1
2
SELECT Name AS Prof_name
FROM Professors

image-20251229203210254


除此之外,有些时候筛选会遇到2个表里有相同的列名,这个时候就需要命名每个表以此来区分:

  • 在每个表后加上空格以及新的名字(例如Students s
  • 在之后的部分就可以用s指代这张表

比如说我们想知道学生”Fichte”上了哪些课:

1
2
3
4
SELECT s.Name, a.LectureNr
FROM Students s, attend a
WHERE s.StudNr = a.StudNr AND
s.Name = 'Fichte'

image-20251229204255579

COUNT

如果我们想知道一共有多少名教授,就需要COUNT来帮助我们数数:

1
2
SELECT COUNT(*)
FROM Professors

image-20251229202539482

GROUP BY

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

1
2
3
SELECT Level, COUNT(*)
FROM Professors
GROUP BY Level;

image-20251229202747291

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

image-20251229224844458

但是通过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种:

  1. (a,b,c)
  2. (a,b)
  3. (a,c)
  4. (b,c)
  5. (a)
  6. (b)
  7. (c)
  8. () 全表总计

CUBE的输出里某些维度列会出现NULL,它代表这一行在该维度上“被汇总掉了”(对所有该维度值求和/求平均等)。比如说在按照(a,b)分组的部分内容里,那么c栏内容就会是NULL

因为CUBE会显示/返回所有分组层级的结果,所以通常要用WHERE ... IS NULL来挑某一层。

例子:只要按(a,b)的汇总,即c IS NULL

1
2
3
SELECT a, b, s
FROM cube_table
WHERE c IS NULL;

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
2
3
4
SELECT Level, COUNT(*)
FROM Professors
GROUP BY Level
HAVING COUNT(*) > 3;

image-20251229203038546

子查询/嵌套

假如我们想知道哪些学生的学期数在平均学期数以下,就需要拆成2步来完成

  • 计算平均学期数(先生成一个子列表)

    1
    2
    SELECT AVG(Semester)
    FROM Students

    image-20251229223305184

  • 筛选

    1
    2
    3
    SELECT Name
    FROM Students
    WHERE Semester <

拼起来就是:

1
2
3
4
SELECT Name
FROM Students
WHERE Semester < (SELECT AVG(Semester)
FROM Students)

image-20251229223655076

或者也可以:

1
2
3
4
SELECT Name
FROM Students, (SELECT AVG(Semester) AS avg_semester
FROM Students) avg
WHERE Semester < avg_semester

image-20251229223919452

这样会高效很多。

WITH AS

WITH AS的功能是在一条 SQL 里给一段子查询/临时结果起名字,后面就能像用表一样用它。

注意,WITH AS和子查询是等价的,但是用WITH AS会更容易阅读和理解。

基本语法:

1
2
3
4
5
WITH 名字(可选列名列表) AS (
一条查询
)
SELECT ...
FROM 名字;


例子:查询C4教授的房间(先找C4教授,再查询他们的房间)

1
2
3
4
5
6
7
WITH C4Prof AS (
SELECT *
FROM Professors
WHERE Level = 'C4'
)
SELECT Name, Room
FROM C4Prof;


也可以用VALUES临时造数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
WITH Professors ( persnr , name , paygrade , room , salary , taxclass ) as
(
VALUES (2125 , ' Sokrates ' , ' C4 ' ,226 ,85000 ,1) ,
(2126 , ' Russel ' , ' C4 ' ,232 ,80000 ,3) ,
(2127 , ' Kopernikus ' , ' C3 ' ,310 ,65000 ,5) ,
(2128 , ' Aristoteles ' , ' C4 ' ,250 ,85000 ,1) ,
(2133 , ' Popper ' , ' C3 ' ,52 ,68000 ,1) ,
(2134 , ' Augustinus ' , ' C3 ' ,309 ,55000 ,5) ,
(2136 , ' Curie ' , ' C4 ' ,36 ,95000 ,3) ,
(2137 , ' Kant ' , ' C4 ' ,7 ,98000 ,1)
)
SELECT persnr , name , paygrade , room , salary , taxclass
FROM Professors;

image-20251230120003507


又或者是之前这个例子:

1
2
3
4
SELECT Name
FROM Students, (SELECT AVG(Semester) AS avg_semester
FROM Students) avg
WHERE Semester < avg_semester

改成用WITH AS就是:

1
2
3
4
5
WITH avg AS (SELECT AVG(Semester) AS avg_semester
FROM Students)
SELECT Name
FROM Students, avg
WHERE Semester < avg_semester


同时命名多个临时结果时只需要写一个WITH:

1
2
3
4
5
6
7
8
9
10
11
12
WITH examination ( MatrNr , CourseNr , PersNr , Grade ) as (
SELECT * FROM pruefen
UNION
VALUES (29120 ,0 ,0 ,3.0) , (29555 ,0 ,0 ,2.0) ,
(29555 ,0 ,0 ,1.3) , (29555 ,0 ,0 ,1.0)
) ,
grades ( Name , MatrNr , Semester , Grade ) as (
SELECT s . name , s . matrnr , semester , avg ( Grade )
FROM studenten s , examination p
WHERE s . matrnr = p. matrnr
GROUP BY s. name , s. matrnr , semester
)

建表

基础

1
2
3
4
5
CREATE TABLE Students (
StudNr INT,
Name VARCHAR(50),
Semester INT
);

含义:

  • 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
2
3
4
5
6
CREATE TABLE Professors (
PersNr INT PRIMARY KEY,
Name VARCHAR(50) NOT NULL,
Level VARCHAR(2),
Room INT
);

CTAS

CREATE TABLE AS SELECT(简称 CTAS),就是用查询结果建表。

例子:

1
2
3
4
CREATE TABLE C4_Professors AS
SELECT PersNr, Name, Room
FROM Professors
WHERE Level = 'C4';

特点:

  • 新表会包含查询出来的列
  • 很多数据库会把类型推导出来
  • 约束(主键/外键/NOT NULL 等)通常不会自动带过去(不同库细节不同),需要另加

临时表(TEMP / TEMPORARY)

只在当前会话存在,断开就没了:

1
2
3
4
CREATE TEMP TABLE tmp_top_students AS
SELECT StudNr, AVG(Grade) AS avg_grade
FROM test
GROUP BY StudNr;

窗口函数

窗口函数(window function)可以把它理解成:“对每一行,都能基于‘一组相关的行’计算出一个值,但又不会把行合并掉”

和之前的分组(GROUP BY)的区别:

  • 普通聚合 + GROUP BY:会把多行“压成一行”(一组只剩一行)。
  • 窗口函数:每一行都还在,但可以在每一行旁边附加一个聚合/排名/累计值。

窗口函数的典型形式是:

1
2
3
4
5
<窗口函数>(...) OVER (
PARTITION BY ...
ORDER BY ...
<frame>
)
  • PARTITION BY ...:按什么分组(每组独立算)
  • ORDER BY ...:组内顺序(排名/累计/相邻必备)
  • FrameROWS ... / RANGE ...
    • ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING(按行数滑窗)
    • RANGE BETWEEN 5000 PRECEDING AND 5000 FOLLOWING(按值范围滑窗)


例子1:比如说我们同时想知道每个学生上的课的总数以及具体是哪些课的话,就需要:

1
2
3
4
5
6
SELECT s.name,
l.Title,
COUNT(*) OVER (PARTITION BY a.StudNr) AS total_attended_lectures
FROM Students s, Lectures l, attend a
WHERE s.StudNr = a.StudNr
AND l.LectureNr = a.LectureNr;

image-20251230112006687

或者

1
2
3
4
5
6
SELECT s.name,
l.Title,
SUM(1) OVER (PARTITION BY a.StudNr) AS total_attended_lectures
FROM Students s, Lectures l, attend a
WHERE s.StudNr = a.StudNr
AND l.LectureNr = a.LectureNr;

也是一样的效果。


例子2:假如我们现在只关注现有的test表,想知道每个学生的排名:

1
2
3
SELECT s.name, t.Grade, rank()OVER ( ORDER BY t.Grade ASC)
FROM Students s, test t
WHERE s.StudNr = t.StudNr

image-20251230110235179

这里的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 行的 x
  • FIRST_VALUE(x):窗口内第一行的 x
  • LAST_VALUE(x):窗口内最后一行的 x
  • NTH_VALUE(x, n):窗口内第 n 行的 x(不是所有库都有)

递归/Recursion

递归的基本框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
WITH RECURSIVE 名字(列...) AS (
-- 1. 基础部分(base case / anchor)
SELECT ...

UNION [ALL]

-- 2. 递归部分(recursive step)
SELECT ...
FROM 名字 ... -- 这里引用自己
...
)
SELECT ...
FROM 名字;

SQL里递归的写法和Induction/Iteration一样。先定义base case,再一层一层递进。

或者说是先给出起始集合,再反复应用递推规则,不断扩张结果集合,直到没有新东西为止。

而不是像其他编程语言里一样一直调用自己,直到碰到base case,然后停止。

在此基础上我们还可以通过定义一列新的内容(次数,长度等)以及增加基于这列信息的判断条件,来实现控制循环次数(上限)。(见例子3)


例子1

生成1-5

1
2
3
4
5
6
7
8
9
10
11
WITH RECURSIVE nums(n) AS (
SELECT 1

UNION ALL

SELECT n + 1
FROM nums
WHERE n < 5
)
SELECT *
FROM nums;

image-20251231111821756

例子2

假设我们现在希望查询“Der Wiener Kreis”(LectureNr=5259) 这门课的的所有前置课程(包括直接和间接)。

普通的查找直接前置课程:

1
2
3
4
5
6
7
8
SELECT Title
FROM Lectures
WHERE LectureNr IN (
SELECT Predecessor
FROM require r, Lectures l
WHERE r.Successor = l.LectureNr
AND l.Title = 'Der Wiener Kreis'
)

查询第n轮前置就是:

1
2
3
4
5
6
7
8
9
10
11
select v1.Predecessor
from require v1,
...
require vn_minus_1,
require vn,
require v
where v1.Predecessor = v2.Successor and
...
vn_minus_1.Predecessor = vn.Successor and
vn.Predecessor = v.LectureNr and
v.Title = 'Der Wiener Kreis';

我们把这个完整查询拆成2步:

  • 利用递归创建子查询(临时表),在原来require表的基础上添加所有间接前置课程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    WITH 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

    我们将一头一尾加进结果里。

    image-20251231110956890

  • 在里面找’Der Wiener Kreis’的所有前置课程

合并起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
WITH 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 Title
FROM Lectures
WHERE LectureNr IN (
SELECT Prereq
FROM TransPrereq t, Lectures l
WHERE t.Successor = l.LectureNr
AND l.Title = 'Der Wiener Kreis'
)

例子3

考虑下面这个图:

image-20251231154645072

用SQL表示是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
WITH recursive singleDirection (a ,b ) as (
SELECT *
FROM ( VALUES (1 ,2) , (2 ,4) , (1 ,3) , (3 ,4) , (3 ,5) , (2 ,5) ,
(5 ,6) , (4 ,6) , (6 ,7) ) as graph
) ,
undirectedGraph as (
SELECT *
FROM singleDirection
UNION ALL
SELECT b , a
FROM singleDirection
)

如果我们现在想判断从1出发是否能到6,我们就可以这样:

  • 利用递归生成一张查询结果表,列出所有可到达的pair
  • 然后找(1,6)是否在里面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WITH recursive singleDirection (a ,b ) as (
SELECT *
FROM ( VALUES (1 ,2) , (2 ,4) , (1 ,3) , (3 ,4) , (3 ,5) , (2 ,5) ,
(5 ,6) , (4 ,6) , (6 ,7) ) as graph
) ,
undirectedGraph as (
SELECT *
FROM singleDirection
UNION ALL
SELECT b , a
FROM singleDirection
) ,
hull (a , b) as (
SELECT * FROM undirectedGraph
UNION
SELECT fst .a , snd . b
FROM hull fst , undirectedGraph snd
WHERE fst . b = snd .a
)
SELECT *
FROM hull
WHERE a = 6
and b = 1;

为了避免无穷展开,我们现在加个限制:图的直径为4,即不考虑长度超过4的路径,那么就可以加一列来记录路径长度并且限制长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
WITH recursive singleDirection (a , b) as (
SELECT *
FROM ( VALUES (1 ,2) , (2 ,4) , (1 ,3) , (3 ,4) , (3 ,5) , (2 ,5) ,
(5 ,6) , (4 ,6) , (6 ,7) ) as graph
) ,
undirectedGraph (a , b) as (
SELECT *
FROM singleDirection
UNION ALL
SELECT b , a
FROM singleDirection
) ,
hull (a , b , dist ) as (
SELECT a , b , 1
FROM undirectedGraph
UNION ALL
SELECT fst .a , snd .b , dist + 1
FROM hull fst , undirectedGraph snd
WHERE fst . b = snd .a
and dist <= 4
)
SELECT min ( dist )
FROM hull
WHERE a = 6
and b = 1;