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

什么是SQL?

SQL(Structured Query Language,结构化查询语言)是一种用于定义、查询和管理关系型数据库的一种声明式语言

声明式语言指的是:只需要描述想要什么结果(例如要哪些数据、满足什么条件),而不必编写具体怎么一步步实现的过程。具体的执行方式由数据库的查询优化器与执行引擎决定。

它主要用来做这些事:

  • 查询数据(DQL):比如 SELECT,从表里查数据、过滤、排序、分组等
  • 定义数据库结构(DDL):比如 CREATE / ALTER / DROP,创建/修改/删除表、索引、视图等
  • 操作数据(DML):比如 INSERT / UPDATE / DELETE,新增、修改、删除表中的记录
  • 权限控制(DCL):比如 GRANT / REVOKE,给用户授权或收回权限
  • 事务控制(TCL):比如 COMMIT / ROLLBACK / SAVEPOINT,控制事务提交、回滚等

基础

一般的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:降序

默认是升序(ASC)。

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

OFFSET

LIMIT类似,只不过是查看从第几条开始之后的结果。

配合LIMIT一起用可以直接锁定某个排名的人:

1
2
3
4
5
SELECT Name, Semester
FROM students
ORDER BY Semester DESC
OFFSET 0
LIMIT 1

image-20260219093420166

注意,offset是从0开始的。

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

GROUP BY 查询里,SELECT 里出现的每个表达式必须满足二选一:

  1. 是分组键(出现在 GROUP BY 里,或能由分组键唯一决定),或者
  2. 是聚合表达式(被 SUM/AVG/COUNT/MIN/MAX/... 包起来)

否则:

image-20251229224844458

但是通过GROUP BY a, b, c这种只会得到最细粒度的分组,即每个(a,b,c)一行。

那我们同时还想得到更粗颗粒度的分组结果(比如按照(a,b),(a),(a,c)这些分组)该怎么办呢?

最普通的方法就是用UNION ALL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SELECT
a, NULL AS b, c,
COUNT(*) AS cnt
FROM t
GROUP BY a, c

UNION ALL
SELECT
NULL AS a, b, c,
COUNT(*) AS cnt
FROM t
GROUP BY b, c

UNION ALL
SELECT
a, NULL AS b, NULL AS c,
COUNT(*) AS cnt
FROM t
GROUP BY a

当然,还有很多更加便捷的方法:

GROUPING SETS

可以用GROUPING SETS来定义多个想要的分组:

1
2
3
4
5
6
7
8
GROUP BY GROUPING SETS (
(a, b, c),
(a, c),
(b, c),
(a),
(c),
(),
)

其中 () 是全表总计(grand total),只按“空分组”汇总一次。

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

这样会高效很多。

LATERAL

LATERAL允许FROM子句中的派生表/子查询/表函数引用其左侧 FROM 项的列,从而形成相关(correlated)的行源,并对左侧输入的每一行进行求值。

例子:

1
2
3
SELECT * FROM
(SELECT 1 AS x) AS t1,
LATERAL (SELECT t1.x+1 AS y) AS t2

这段等价于:

1
2
3
for x in [1]:
for y in [x+1]:
print(x,y)

集合比较与存在性

IN

判断是否属于集合。

比如说我们想知道哪些学生参加了5001这门课,并且最后输出它们的名字,那么我们就可以用IN来进行判断:

1
2
3
4
5
6
7
SELECT s.Name
FROM Students s
WHERE s.StudNr IN (
SELECT a.StudNr
FROM attend a
WHERE a.LectureNr = 5001
);

ANY

对集合里至少一个成立。

比如说我们想找出房间号比某个 C3 教授房间更大的教授:

1
2
3
4
5
6
7
SELECT p.Name, p.Room
FROM Professors p
WHERE p.Room > ANY (
SELECT Room
FROM Professors
WHERE Level = 'C3'
);

ALL

比如说我们想找出房间号比所有 C3 教授房间都小的教授和房间:

1
2
3
4
5
6
7
SELECT p.Name, p.Room
FROM Professors p
WHERE p.Room < ALL (
SELECT Room
FROM Professors
WHERE Level = 'C3'
);

EXISTS

比如说我们想找出至少开了一门课的教授:

1
2
3
4
5
6
7
SELECT p.Name
FROM Professors p
WHERE EXISTS (
SELECT 1
FROM Lectures l
WHERE l.Given_by = p.PersNr
);

NOT EXISTS

找出没有学生选的课程:

1
2
3
4
5
6
7
SELECT l.LectureNr, l.Title
FROM Lectures l
WHERE NOT EXISTS (
SELECT 1
FROM attend a
WHERE a.LectureNr = l.LectureNr
);

Common Table Expression (CTE) / WITH AS

CTE的核心动机:

  1. 可读性/结构化

    • 多层嵌套查询堆起来很难读

    • CTE 可以“给每一步命名”,让外层查询更清爽

  2. 可复用

    • CTE 的结果可以在同一个查询里被引用多次(比重复写子查询好)

CTE具体用到的命令就是WITH AS

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

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

基本语法:

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

一条SQL也可以写多个 CTE,用逗号隔开:

1
2
3
WITH cte1 AS (...),
cte2 AS (...)
SELECT ...


例子:查询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
)

COALESCE

COALESCE 是 SQL 里的空值处理函数
按从左到右的顺序,返回第一个不是 NULL 的值;如果全是 NULL,就返回 NULL。

1
COALESCE(a, b, c, 0)

等价于:如果 a 不是 NULL 返回 a,否则看 b,否则看 c,否则返回 0

并且

1
2
SELECT COALESCE(description, 'None')
FROM sometable

等价于

1
2
3
4
SELECT CASE WHEN description is NOT NULL
THEN description
ELSE 'NONE' END
FROM sometable

处理字符串

字符串匹配:

  • Like% 任意长度(可空),_ 单字符

常见字符串函数(SQL-92/常见实现):

  • SUBSTRING(str, start, len)
  • UPPER(str)(也常见 LOWER

字符串拼接:

  • 标准:||
  • 也提到某些系统有 +CONCAT(a,b)(方言差异)

建表

基础

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 functions)

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

窗口函数一般写在 SELECT部分(也可用于 ORDER BY)。并且由于窗口函数的计算发生在 WHERE/GROUP BY/HAVING 之后,因此通常不能直接出现在这些子句(以及 JOIN ON)里。

和之前的分组(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;

PostgreSQL

pgvector

pgvector是PostgreSQL的一个拓展,用于直接做向量存储与相似检索。

定义向量列

比如说定义10维的vector列:

1
2
3
4
CREATE TABLE items (
id bigserial PRIMARY KEY,
embedding vector(10)
);

embedding只是列名)

插入向量

1
2
INSERT INTO items (embedding)
VALUES ('[1,2,3,4,5,6,7,8,9,10]');

查看向量维度

1
2
3
SELECT embedding.dim 
FROM items
LIMIT 1;

运算符与距离度量

运算符 含义
<-> L2(欧氏距离)
<#> negative inner product(负内积)
<=> cosine distance(余弦距离)
<+> L1(曼哈顿距离)
<~> Hamming distance(二值向量)
<%> Jaccard distance(二值向量)

例子:计算距离并输出一个dist

1
2
3
4
5
SELECT
id,
embedding <-> '[1,2,3,4,5,6,7,8,9,10]'::vector AS dist
FROM items
ORDER BY dist;

会计算embedding列的所有vector到[1,2,3,4,5,6,7,8,9,10]的L2距离,然后按照或者距离进行排序。(注意,ORDER BY默认是升序)

例子2:找某条记录的近邻

1
2
3
4
5
6
SELECT id
FROM items
ORDER BY embedding <-> (
SELECT embedding FROM items WHERE id = 123
)
LIMIT 5;

例子3:给每个找近邻

假设有一个表wiki(word TEXT, embedding vector(50))

1
2
3
4
5
6
7
8
9
SELECT w1.word, w2.word
FROM wiki w1
CROSS JOIN LATERAL (
SELECT word
FROM wiki
WHERE w1.word <> word
ORDER BY w1.embedding <=> embedding
LIMIT 3
) AS w2;
  • CROSS JOIN:笛卡尔连接(不写 ON 条件)
  • LATERAL (subquery):这个 subquery 会根据当前 w1 变化而变化

所以对外层每一行 w1,运行一次括号里的子查询,得到若干行结果,然后把这些结果行和当前 w1 拼在一起输出。