PostgreSQL · Interactive Course

从第一条 SQL,
到向量检索与高可用。

一份循序渐进的 PostgreSQL 完整学习手册:基础语法 → 数据建模 → 查询进阶 → 性能调优 → 事务并发 → 权限安全 → 备份恢复,最后深入向量插件 pgvector,覆盖 AI 时代的相似度检索实战。 每章配有测试题,学完一章就打卡,进度自动保存。

9 章
核心模块
45 题
章末测试
100+ 例
可运行代码
v18
面向现代版本
CHAPTER 01

基础语法入门

PostgreSQL 是世界上最先进的开源关系型数据库。本章从零开始:了解它的核心概念、用 psql 连接数据库、 掌握 SQL 四大语言分类和最基础的增删改查语句。

1.1 认识 PostgreSQL

PostgreSQL(常简写为 Postgres)诞生于加州大学伯克利分校的 Ingres 项目,自 1996 年开源以来, 以「正确性优先」的设计哲学著称。它是完全的事务型关系数据库,同时具备传统商用数据库的绝大部分能力:

  • 标准兼容:SQL 标准符合度极高,同时大量扩展(窗口函数、CTE、JSON、数组、全文检索一应俱全)
  • 可靠:ACID 事务、MVCC 并发控制、崩溃后自动恢复(WAL)
  • 可扩展:自定义类型、函数、操作符、外部数据包装器(FDW)、插件生态(如 pgvector)
  • 免费开源:PostgreSQL 许可证,无商业授权费用
约定 本教程面向 PostgreSQL 12 及以上版本(示例在 14–18 上验证通过)。安装方式各平台不同,推荐: Windows 用 EDB 安装包 或 winget install PostgreSQL.PostgreSQL, Linux 用各发行版仓库或官方 PGDG 源,macOS 用 brew install postgresql@17。

1.2 使用 psql 连接数据库

psql 是 PostgreSQL 自带的命令行客户端,也是学习阶段最好的工具——它稳定、纯粹、所有教学资料通用。 连接数据库的基本语法:

bash
# 基本连接格式
psql -h 主机地址 -p 端口 -U 用户名 -d 数据库名

# 本机连接(默认端口 5432)
psql -U postgres -d postgres

# 指定端口(例如 5433)并要求输入密码
psql -h localhost -p 5433 -U postgres -d postgres

连上之后,最有用的几个「反斜杠命令」:

命令作用
\l列出所有数据库
\c dbname切换到指定数据库
\dt列出当前库中的表
\d 表名查看表结构(字段、类型、索引、约束)
\du列出所有角色(用户)
\x切换竖排显示(宽表神器)
\timing显示每条语句的执行耗时
\q退出 psql

1.3 SQL 的四大语言分类

SQL 不是一种语言,而是四类语言的集合。理解分类,你就能把任何教程里的语句「对号入座」:

分类全称负责什么代表语句
DQLData Query Language查询数据SELECT
DMLData Manipulation Language增删改数据INSERT UPDATE DELETE
DDLData Definition Language定义结构CREATE ALTER DROP TRUNCATE
DCLData Control Language权限控制GRANT REVOKE

1.4 第一批 SQL 语句

建一张练习表

sql
CREATE TABLE student (
    id      integer PRIMARY KEY,
    name    text NOT NULL,
    major   text,
    score   numeric(5, 2),
    enrolled date DEFAULT CURRENT_DATE
);

插入数据(INSERT)

sql
-- 单条插入
INSERT INTO student (id, name, major, score)
VALUES (1, '张三', '计算机', 88.5);

-- 多条批量插入
INSERT INTO student (id, name, major, score) VALUES
    (2, '李四', '软件工程', 92.0),
    (3, '王五', '计算机', 76.5),
    (4, '赵六', '数学', NULL);

查询数据(SELECT)

sql
-- 查询所有列、所有行
SELECT * FROM student;

-- 投影:只取需要的列;WHERE 过滤行
SELECT name, score FROM student WHERE major = '计算机';

-- DISTINCT 去重:有哪些专业
SELECT DISTINCT major FROM student;

-- 排序 + 分页:按分数降序,取前 2 条
SELECT name, score FROM student
ORDER BY score DESC
LIMIT 2 OFFSET 1;   -- 跳过 1 行后取 2 行

更新与删除(UPDATE / DELETE)

sql
-- 更新:一定要带 WHERE,否则全表更新!
UPDATE student SET score = 80.0 WHERE id = 4;

-- 删除:同样一定要带 WHERE
DELETE FROM student WHERE name = '赵六';
危险操作 UPDATE / DELETE 不带 WHERE 会作用于全表所有行且不可撤销(除非在事务里回滚, 见第 6 章)。执行前先用同条件的 SELECT 确认影响范围,是最重要的安全习惯。

1.5 NULL 与三值逻辑

NULL 表示「未知 / 不存在」,它不等于任何值,包括它自己。任何值与 NULL 用 = 比较结果都是 UNKNOWN 而非 TRUE,所以判断 NULL 必须用 IS NULL / IS NOT NULL:

sql
-- 错误:永远查不到 score 为 NULL 的行
SELECT * FROM student WHERE score = NULL;

-- 正确
SELECT * FROM student WHERE score IS NULL;

-- 空值合并:NULL 时取默认值(COALESCE 从左到右取第一个非 NULL)
SELECT name, COALESCE(score, 0) AS score FROM student;

-- NULLIF(a, b):a = b 时返回 NULL,常用于避免除零
SELECT score / NULLIF(score, 0) FROM student;

1.6 常用运算符速查

类别运算符 / 关键字说明
比较= <> != < > <= >=注意 SQL 的相等判断只有一个等号
逻辑AND OR NOT优先级 NOT > AND > OR,复杂条件建议加括号
范围BETWEEN a AND b闭区间 [a, b]
集合IN (v1, v2)等于列表中任意一个值
模糊LIKE / ILIKE% 任意串 _ 单字符;ILIKE 忽略大小写
注释-- 单行 /* 多行 */—
学完本章了?标记完成,进度会自动同步到目录。
CHAPTER 02

数据库与表操作

数据库、模式、表是 PostgreSQL 的三级存储结构。本章掌握数据库与模式的组织方式、 建表的完整姿势、五大约束、以及表结构的日常维护操作。

2.1 数据库与模式(Schema)

PostgreSQL 的层级结构是:实例(集群)→ 数据库 → 模式 → 表。

  • 一个实例可以承载多个数据库,数据库之间完全隔离;
  • 每个数据库内有多个模式(schema),默认都有一个 public 模式;
  • 表必须属于某个模式,全限定名写作 schema.table,省略时按 search_path 查找。
sql
-- 创建 / 删除数据库(数据库级别操作,不能在事务里执行)
CREATE DATABASE shop ENCODING 'UTF8';
DROP DATABASE IF EXISTS shop;

-- 模式:给多租户、多业务模块做命名空间隔离
CREATE SCHEMA analytics;
CREATE TABLE analytics.daily_stats (id integer);  -- 全限定名
SET search_path TO analytics, public;             -- 切换默认查找路径
提示 PG 15 起 public 模式不再对所有人开放 CREATE 权限(安全加固), 普通用户建表前可能需要 GRANT CREATE ON SCHEMA public TO 用户。

2.2 CREATE TABLE 与五大约束

建表的本质是定义「列 + 类型 + 约束」。约束是数据库替你把关数据质量的第一道防线:

sql
CREATE TABLE orders (
    order_id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 主键:非空 + 唯一
    user_id     bigint NOT NULL,                                 -- 非空约束
    status      text NOT NULL DEFAULT 'pending',                 -- 默认值
    amount      numeric(10,2) CHECK (amount >= 0),              -- 检查约束
    email       text UNIQUE,                                     -- 唯一约束
    created_at  timestamptz NOT NULL DEFAULT now(),
    -- 外键:引用另一张表,级联删除
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
约束作用语法关键字
主键行的唯一标识,自动建唯一索引PRIMARY KEY
外键保证引用完整性,数据一致FOREIGN KEY ... REFERENCES
非空禁止 NULLNOT NULL
唯一列值不重复(NULL 除外)UNIQUE
检查自定义布尔规则CHECK (表达式)

自增主键:IDENTITY vs 序列

sql
-- 现代推荐:SQL 标准的 IDENTITY 列(PG 10+)
id bigint GENERATED ALWAYS AS IDENTITY
    -- ALWAYS:手动插 id 会被拒绝,更安全;BY DEFAULT 则允许手动指定

-- 传统方式:serial 伪类型(内部就是一个序列对象)
id bigserial PRIMARY KEY

-- 序列也可单独创建、手动调用
CREATE SEQUENCE order_seq START 1000;
SELECT nextval('order_seq');

2.3 ALTER TABLE:修改表结构

sql
-- 加列(PG 11+ 加带常量默认值的列不重写全表,秒级完成)
ALTER TABLE orders ADD COLUMN remark text DEFAULT '';

-- 删列、重命名列
ALTER TABLE orders DROP COLUMN remark;
ALTER TABLE orders RENAME COLUMN status TO state;

-- 修改类型(需要有隐式转换路径,大表会锁表重写,慎用)
ALTER TABLE orders ALTER COLUMN amount TYPE numeric(12,2);

-- 追加 / 删除约束(命名便于日后管理)
ALTER TABLE orders ADD CONSTRAINT chk_amount CHECK (amount <= 999999);
ALTER TABLE orders DROP CONSTRAINT chk_amount;

2.4 DROP 与 TRUNCATE 的区别

sql
-- DROP:连表带结构、索引、约束一起删除
DROP TABLE IF EXISTS orders;

-- 若有别的对象依赖它(如视图),会报错;CASCADE 连依赖一起删(危险)
DROP TABLE orders CASCADE;

-- TRUNCATE:只清空数据、保留结构,比 DELETE 快得多(直接重写文件块)
TRUNCATE TABLE logs;

-- 多表一起清,并重置序列
TRUNCATE logs, audit RESTART IDENTITY CASCADE;
DELETETRUNCATEDROP
删除内容行(可带 WHERE)全部行表 + 结构
速度逐行删除,慢极快快
可回滚可以可以(PG 中是事务性的)可以
触发 DELETE 触发器是否—
回收磁盘空间需 VACUUM立即立即

2.5 视图与物化视图

sql
-- 普通视图:存储的是查询语句,每次查询实时计算
CREATE VIEW v_active_orders AS
SELECT order_id, user_id, amount FROM orders WHERE status = 'paid';

-- 物化视图:把结果落盘,查询快,但数据不自动更新
CREATE MATERIALIZED VIEW mv_daily_sales AS
SELECT date_trunc('day', created_at) AS day, SUM(amount) AS total
FROM orders GROUP BY 1;

REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_sales;  -- 刷新但不锁读

2.6 临时表

sql
-- 会话结束自动销毁,常用于中间结果
CREATE TEMP TABLE tmp_calc AS
SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id;
掌握库表操作了吗?标记完成这一章。
CHAPTER 03

数据类型详解

PostgreSQL 拥有业界最丰富的类型系统。选对类型 = 更好的性能 + 更少的应用层校验。 本章覆盖数值、文本、时间、布尔四大基础,以及数组、JSON/JSONB、枚举、UUID、范围类型等 PG 特色。

3.1 数值类型

类型范围 / 精度适用场景
smallint16 位整数 ±3.2 万省空间的枚举数值
integer32 位整数 ±21 亿常规整数首选
bigint64 位整数主键、计数器、时间戳
numeric(p,s)任意精度,精确金额必选,无浮点误差
real / double precision浮点,有误差科学计算、坐标(不要求精确)
serial / bigserial自增整数旧式自增主键
金额警告 永远不要用 float / double 存钱:0.1 + 0.2 ≠ 0.3 的浮点误差会让你对账时痛苦万分。 金额一律 numeric(10,2)(或以「分」为单位存 bigint)。

3.2 文本类型

sql
-- varchar(n):限长,超长报错;text:不限长。性能完全相同!
-- 在 PG 里没有理由为了性能选 varchar,text 生态最好
name  varchar(50),
intro text,

-- 常用字符串函数与操作符
SELECT 'Post' || 'greSQL';               -- 拼接:PostgreSQL
SELECT upper(name), length(name) FROM student;
SELECT substr('PostgreSQL', 1, 4);       -- Post
SELECT '  hi  '::text AS trimmed;          -- 类型转换写法:值::类型
SELECT trim('  hi  ');                    -- hi
SELECT format('%s 有 %s 元', '张三', 100); -- 格式化

3.3 时间与日期

sql
-- timestamp:不带时区;timestamptz:带时区(全球业务强烈推荐)
-- date:纯日期;time:纯时间;interval:时间间隔
created_at timestamptz DEFAULT now(),

SELECT now();                              -- 当前事务开始时间(事务内恒定)
SELECT clock_timestamp();                  -- 真实当前时刻
SELECT now() + INTERVAL '7 days';          -- 一周后
SELECT date_trunc('month', now());         -- 截断到月初
SELECT extract(year FROM now());           -- 提取年份

-- 时区转换
SELECT now() AT TIME ZONE 'Asia/Shanghai';
最佳实践 now() 返回事务开始时刻——同一事务里多次调用结果一致,适合做业务一致性时间戳。 另外存储用 timestamptz(实际以 UTC 落盘),展示层再按用户时区转换。

3.4 布尔类型

sql
is_active boolean DEFAULT true,
-- 可取 true/false、't'/'f'、'yes'/'no'、1/0

SELECT * FROM users WHERE is_active;     -- 直接当条件用
SELECT NOT is_active FROM users;

3.5 枚举类型(ENUM)

sql
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'done', 'closed');

ALTER TABLE orders ALTER COLUMN status TYPE order_status;

-- 好处:天然有序(可 ORDER BY / 范围比较)、非法值插不进来
-- 代价:加值需 ALTER TYPE,删值麻烦;要求灵活时可用 text + CHECK 约束替代
ALTER TYPE order_status ADD VALUE 'refunded';  -- 事务外执行

3.6 UUID

sql
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),  -- PG13+ 内置函数

-- 优点:分布式生成不冲突、暴露在 URL 不泄露业务量级
-- 缺点:16 字节大于 bigint 的 8 字节,索引更大、无序写入更碎页

3.7 数组类型

sql
CREATE TABLE article (
    id    integer PRIMARY KEY,
    title text,
    tags  text[]           -- 一维数组;也可 text[][] 多维
);

INSERT INTO article VALUES (1, 'PG 入门', ARRAY['db', 'sql', 'tutorial']);
INSERT INTO article VALUES (2, '数组玩法', '{"db", "advanced"}');

SELECT title, tags[1] FROM article;            -- 下标从 1 开始!
SELECT * FROM article WHERE 'sql' = ANY(tags); -- 数组中任一元素等于值
SELECT * FROM article WHERE tags @> ARRAY['db']; -- 包含关系

-- 数组可以建 GIN 索引,ANY 查询能走索引(第 5 章)

3.8 JSON 与 JSONB(重点)

JSON 数据在 PG 里有两个存储形态:json 原样保存文本(保留空格、键顺序,每次查询都要重新解析); jsonb 解析为二进制(解析一次,可比较、可索引、可高效操作)。99% 的场景选 jsonb。

sql
CREATE TABLE events (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    data  jsonb
);

INSERT INTO events (data) VALUES
('{"user": {"id": 1, "name": "张三"}, "action": "click", "tags": ["home", "banner"]}');

-- 取值:-> 返回 jsonb,->> 返回 text
SELECT data->'user'->>'name' FROM events;              -- 张三
SELECT data->'tags'->0 FROM events;                    -- "home"

-- jsonb 专属操作符
SELECT data ? 'action' FROM events;                      -- 是否存在顶层键
SELECT data['tags'] @> '["home"]' FROM events;           -- 包含
SELECT data || '{"ip": "1.2.3.4"}'::jsonb FROM events;   -- 合并
UPDATE events SET data = data - 'ip';                     -- 删键

-- 建索引:GIN 让包含查询飞起来
CREATE INDEX idx_events_data ON events USING GIN (data);
SELECT * FROM events WHERE data @> '{"action": "click"}';

3.9 其他值得知道的类型

类型说明
inet / cidr / macaddrIP 地址与网段,支持子网运算
int4range / tstzrange范围类型(如排班时间段),支持重叠、包含运算
bytea二进制大对象(图片、文件)
bit(n) / bit varying(n)位串,支持位运算
tsvector全文检索类型,配合 GIN 索引
vector(n)pgvector 提供的向量类型(第 9 章专讲)
复合类型CREATE TYPE ... AS (...) 自定义行结构
类型系统心里有数了吗?标记完成。
CHAPTER 04

常用 SQL 查询

查询是 SQL 的灵魂。本章从连接查询、聚合分组一路讲到 PostgreSQL 的杀手锏: 窗口函数、CTE(含递归)、DISTINCT ON、UPSERT——写出真正解决业务问题的查询。

4.1 示例数据模型

sql
CREATE TABLE users (
    id   integer PRIMARY KEY,
    name text NOT NULL,
    city text NOT NULL
);

CREATE TABLE orders (
    id      integer PRIMARY KEY,
    user_id integer REFERENCES users(id),
    amount  numeric(10,2) NOT NULL,
    status  text NOT NULL,
    created_at date NOT NULL
);

INSERT INTO users VALUES
 (1,'张三','北京'),(2,'李四','上海'),(3,'王五','北京'),(4,'赵六','广州');

INSERT INTO orders VALUES
 (101,1, 250.00,'paid',  '2026-09-01'),
 (102,1,  80.50,'pending','2026-09-02'),
 (103,2, 999.00,'paid',  '2026-09-02'),
 (104,3, 120.00,'paid',  '2026-09-03'),
 (105,3, 45.00,'closed','2026-09-04'),
 (106,4, 300.00,'paid',  '2026-09-05');

4.2 WHERE 进阶与 EXISTS

sql
-- 模式匹配:LIKE / ILIKE;或更强大的正则(PG 特色)
SELECT * FROM users WHERE name LIKE '张%';
SELECT * FROM users WHERE name ~ '^张.{1,2}$';   -- 正则匹配

-- EXISTS 半连接:判断「存在与否」比 IN 更直观高效(尤其子查询大表时)
SELECT u.* FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid');

-- 找「没有任何订单」的用户(反半连接)
SELECT u.* FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

4.3 聚合与分组

sql
-- 常用聚合函数:COUNT SUM AVG MIN MAX
SELECT COUNT(*) AS 总单数, SUM(amount) AS 总金额, AVG(amount)::numeric(10,2) AS 均价
FROM orders;

-- GROUP BY:先分组再聚合;SELECT 只能出现分组列和聚合结果
SELECT u.city, COUNT(*) AS order_cnt, SUM(o.amount) AS total
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.city
ORDER BY total DESC;

-- HAVING:对聚合结果过滤(WHERE 是分组前过滤,HAVING 是分组后)
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE status = 'paid'                 -- 先筛掉非已支付
GROUP BY user_id
HAVING SUM(amount) > 200              -- 再筛总额大于 200 的
ORDER BY total DESC;

-- FILTER 子句:一个查询里多种口径(PG 特色,比反复 CASE WHEN 干净)
SELECT
    COUNT(*) AS all_cnt,
    COUNT(*) FILTER (WHERE status = 'paid')    AS paid_cnt,
    SUM(amount) FILTER (WHERE status = 'pending') AS pending_amt
FROM orders;

4.4 连接查询(JOIN)全家桶

写法语义
INNER JOIN两表都匹配的行
LEFT JOIN左表全保留,右表无匹配补 NULL
RIGHT JOIN右表全保留
FULL JOIN两边都全保留
CROSS JOIN笛卡尔积(N × M 行)
SELF JOIN表连自己(如查「员工-上级」)
sql
-- LEFT JOIN + IS NULL 找「没有已支付订单」的用户(经典反连接模式)
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid'  -- 连接条件放 ON
WHERE o.id IS NULL;

-- 注意:LEFT JOIN 时「右表过滤条件」必须写在 ON 里;
-- 写到 WHERE 里会退化成 INNER JOIN(NULL 被 WHERE 过滤掉了)
SELECT u.name, o.id AS order_id
FROM users u LEFT JOIN orders o ON o.user_id = u.id AND o.amount > 100;

4.5 集合运算

sql
SELECT city FROM users WHERE id < 3
UNION                     -- 去重合并(UNION ALL 不去重、更快,能不用去重就 ALL)
SELECT city FROM users WHERE id > 2;

SELECT city FROM users
INTERSECT                -- 交集
SELECT u.city FROM users u JOIN orders o ON o.user_id = u.id;

SELECT city FROM users
EXCEPT                   -- 差集
SELECT '广州';

4.6 子查询

sql
-- 标量子查询:返回单值
SELECT name FROM users
WHERE id = (SELECT user_id FROM orders ORDER BY amount DESC LIMIT 1);

-- 派生表:子查询当表用
SELECT t.city, t.total
FROM (SELECT u.city, SUM(o.amount) AS total
      FROM users u JOIN orders o ON o.user_id = u.id
      GROUP BY u.city) t
WHERE t.total > 100;

-- LATERAL:子查询引用外层列,逐行驱动(对每行用户取其金额最高的订单)
SELECT u.name, top_order.amount
FROM users u,
LATERAL (SELECT amount FROM orders o
         WHERE o.user_id = u.id
         ORDER BY amount DESC LIMIT 1) top_order;

4.7 窗口函数(重中之重)

聚合函数把 N 行压成 1 行;窗口函数在保留每一行的同时计算跨行统计。 「排名、累计、同比环比、Top-N per group」全都靠它。

sql
-- 语法:函数(...) OVER (PARTITION BY 分组列 ORDER BY 排序列 frame)
SELECT
    user_id, amount, created_at,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn,
    RANK()       OVER (ORDER BY amount DESC)  AS rnk,   -- 并列同名次,跳号
    DENSE_RANK() OVER (ORDER BY amount DESC)  AS drnk,  -- 并列同名次,不跳号
    SUM(amount)  OVER (PARTITION BY user_id)  AS user_total,
    AVG(amount)  OVER ()                        AS global_avg,
    amount - AVG(amount) OVER (PARTITION BY user_id) AS diff_from_user_avg
FROM orders;

经典应用:每组 Top-N

sql
-- 每个用户金额最高的 2 单:GROUP BY 做不到,窗口函数轻松拿捏
SELECT * FROM (
    SELECT o.*,
           ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn
    FROM orders o
) t
WHERE rn <= 2;

偏移分析:LAG / LEAD

sql
-- 计算每个用户订单金额的环比变化(当前行 vs 上一单)
SELECT
    user_id, created_at, amount,
    LAG(amount)  OVER (PARTITION BY user_id ORDER BY created_at) AS prev_amt,
    amount - LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS delta
FROM orders;

-- 累计求和(默认帧:分区起点到当前行)
SELECT created_at, amount,
       SUM(amount) OVER (ORDER BY created_at) AS running_total
FROM orders;

4.8 CTE:WITH 子句与递归

sql
-- 普通 CTE:让复杂查询可读
WITH paid AS (
    SELECT * FROM orders WHERE status = 'paid'
),
city_total AS (
    SELECT u.city, SUM(p.amount) AS total
    FROM users u JOIN paid p ON p.user_id = u.id
    GROUP BY u.city
)
SELECT * FROM city_total WHERE total > 100 ORDER BY total DESC;

-- 递归 CTE:遍历树形结构(组织架构、评论楼层、目录树)
WITH RECURSIVE org_tree AS (
    -- 种子:根节点
    SELECT id, name, 1 AS depth
    FROM employees WHERE manager_id IS NULL
    UNION ALL
    -- 递归:找下一层
    SELECT e.id, e.name, t.depth + 1
    FROM employees e
    JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree ORDER BY depth;

-- 生成时间序列也常用递归 CTE(报表补零点)
WITH RECURSIVE days AS (
    SELECT DATE '2026-09-01' AS d
    UNION ALL
    SELECT d + 1 FROM days WHERE d < DATE '2026-09-30'
)
SELECT * FROM days;

4.9 PG 特色:DISTINCT ON

sql
-- 「每个用户金额最高的那一单」一行代码(其他数据库要羡慕哭)
SELECT DISTINCT ON (user_id)
       user_id, id, amount
FROM orders
ORDER BY user_id, amount DESC;

4.10 UPSERT 与 RETURNING

sql
-- UPSERT:存在则更新,不存在则插入(PG 的 ON CONFLICT 扩展)
INSERT INTO orders (id, user_id, amount, status, created_at)
VALUES (101, 1, 260.00, 'paid', '2026-09-01')
ON CONFLICT (id) DO UPDATE
SET amount = EXCLUDED.amount, status = EXCLUDED.status;

-- 只在冲突时什么都不做
INSERT INTO users (id, name, city) VALUES (4, '赵六', '广州')
ON CONFLICT (id) DO NOTHING;

-- RETURNING:DML 直接返回受影响的行,省一次回查
UPDATE orders SET status = 'paid' WHERE status = 'pending'
RETURNING id, amount;

DELETE FROM orders WHERE status = 'closed'
RETURNING id;

-- 删除旧数据的同时 INSERT 归档表(同一语句原子完成)
WITH moved AS (
    DELETE FROM orders WHERE created_at < '2026-01-01' RETURNING *
)
INSERT INTO orders_archive SELECT * FROM moved;
窗口函数和 CTE 能信手拈来了吗?标记完成。
CHAPTER 05

索引与性能优化

90% 的「数据库好慢」都是索引问题。本章吃透六种索引类型及其适用场景, 学会用 EXPLAIN ANALYZE 读懂执行计划,掌握一套系统的慢查询排查方法。

5.1 索引的工作原理

没有索引时,查找一行数据只能顺序扫描(Sequential Scan)全表; 有了索引,数据库通过 B-tree 等结构把查找复杂度降到对数级别——就像书的目录, 从「翻遍整本书」变成「先查目录再翻页」。

sql
-- 最基本的创建与查看
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_created  ON orders (created_at DESC);

-- 唯一索引(等于唯一约束的底层实现)
CREATE UNIQUE INDEX idx_users_email ON users (email);

-- 部分索引:只索引满足条件的行,写入更频繁的状态时省空间提速度
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';

-- 表达式索引:查询里用了函数/表达式时,普通索引用不上!
CREATE INDEX idx_users_email_lower ON users (lower(email));
SELECT * FROM users WHERE lower(email) = 'zhang@ex.com';  -- 现在能走索引

-- 覆盖索引:把常一起查的列直接带上,免回表
CREATE INDEX idx_orders_user_amount ON orders (user_id) INCLUDE (amount);

-- 删除 / 重建(重建可回收碎片)
DROP INDEX idx_orders_user_id;
REINDEX INDEX idx_orders_user_amount;
何时建索引 主键和唯一约束自动建索引;外键列、WHERE / JOIN / ORDER BY 高频列优先。 索引不是越多越好——每个索引都会拖慢 INSERT/UPDATE/DELETE 并占用磁盘。

5.2 六种索引类型

类型适用场景典型示例
B-tree默认;等值、范围、排序(< > BETWEEN ORDER BY)id、时间、状态列
Hash纯等值判断(=),不支持范围大表精确匹配
GIN「包含」语义:jsonb、数组、全文检索data @> '...'、tags @> ARRAY[...]
GiST几何、范围类型、地理(PostGIS)、KNN时间段重叠、空间检索
SP-GiST非平衡结构(基数树):IP 前缀、点网段匹配
BRIN超大有物理顺序的大表(时序),块级范围索引,极小日志表的时间列
sql
-- GIN:jsonb / 数组 / 全文
CREATE INDEX idx_events_data ON events USING GIN (data);
CREATE INDEX idx_article_tags ON article USING GIN (tags);

-- BRIN:每天千万行的日志表,时间列自然递增 —— 索引只有几 MB
CREATE INDEX idx_logs_ts ON logs USING BRIN (ts);

-- B-tree 联合索引:最左前缀原则
-- 生效:WHERE a=?  / WHERE a=? AND b=? ;不生效:WHERE b=?
CREATE INDEX idx_multi ON orders (user_id, status, created_at);

5.3 EXPLAIN:读懂执行计划

sql
EXPLAIN ANALYZE
SELECT u.name, SUM(o.amount)
FROM users u JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.name;

输出解读——每个节点代表一步操作,缩进表示先后(内层先执行):

text
HashAggregate  (cost=.. rows=3) (actual time=0.18..0.19 rows=3 loops=1)
  ->  Hash Join  (actual time=0.11..0.15 rows=4 loops=1)
        Hash Cond: (o.user_id = u.id)
        ->  Seq Scan on orders o  (actual time=0.02..0.06 rows=4 loops=1)
              Filter: (status = 'paid')
              Rows Removed by Filter: 2
        ->  Hash  (rows=4)
              ->  Seq Scan on users u  (rows=4)
节点含义好坏
Seq Scan顺序扫全表小表正常;大表 + 高选择性条件 = 缺索引
Index Scan走索引并回表好
Index Only Scan只用索引不回表最好(覆盖索引)
Bitmap Scan索引批量定位再取数据好(返回行多时)
Nested Loop双层循环 join小结果集好,大结果集慢
Hash Join哈希表 join中大规模常见
Rows Removed by Filter扫出后又被过滤掉的行数数值巨大说明索引没建对
EXPLAIN vs EXPLAIN ANALYZE EXPLAIN 只显示「计划」(不执行);EXPLAIN ANALYZE 真的执行并给出实际行数、实际耗时。 注意 DML 语句用 ANALYZE 会真实改数据,练习时可包在 BEGIN; ... ROLLBACK; 里。

5.4 慢查询排查流程

  1. 定位慢语句:开启 log_min_duration_statement = 500(记录超 500ms 的语句),或查 pg_stat_statements 扩展统计 TOP 慢语句;
  2. 看计划:EXPLAIN (ANALYZE, BUFFERS),重点看估算行数与实际行数偏差(偏差大 = 统计信息过期);
  3. 判断问题:大表 Seq Scan?索引失效(函数包裹、隐式类型转换、前导 % 的 LIKE)?
  4. 修索引 / 改写法:加/改索引,避免 SELECT *,大偏移分页改键集分页;
  5. 刷新统计:大批量写入后手动 ANALYZE 表名;,让优化器拿到新鲜数据。
sql
-- pg_stat_statements:全局慢查询统计(postgresql.conf 里 preload 后 CREATE EXTENSION)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT calls, round(total_exec_time::numeric, 1) AS total_ms, left(query, 60)
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

-- 键集分页(keyset pagination):深分页时比 OFFSET 快几个量级
SELECT * FROM orders
WHERE created_at < '2026-09-03'   -- 上一页最后一行的值
ORDER BY created_at DESC
LIMIT 20;

5.5 VACUUM 与表膨胀

PostgreSQL 的 MVCC(详见第 6 章)删除数据时只是「标记失效」,旧版本行仍占磁盘, 长期大量 UPDATE/DELETE 会让表「膨胀」。清理机制就是 VACUUM:

sql
VACUUM orders;              -- 回收死元组空间(可复用,不归还 OS),不锁读写
VACUUM FULL orders;         -- 重写整表、彻底归还空间 —— 锁表!线上错峰执行
ANALYZE orders;             -- 更新统计信息(不清理空间,只刷新数据分布)

-- 日常交给 autovacuum 后台进程(默认开启,别关)
-- 查看表的死元组情况
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;

5.6 写代码层面的性能习惯

  • 不要 SELECT *,按需取列(网络、内存、覆盖索引三重收益);
  • 批量操作用多值 INSERT / COPY,别逐行提交事务;
  • 深分页用键集分页代替大 OFFSET;
  • count(*) 精确计数在大表上很贵,粗略计数可用 pg_class.reltuples;
  • OLTP 业务大事务拆小,锁持有时间短(第 6 章);
  • 上线前用真实量级数据做 EXPLAIN ANALYZE,别赌。
能看懂执行计划了吗?标记完成。
CHAPTER 06

事务与并发控制

事务保证「要么全做,要么全不做」。本章讲清 ACID、隔离级别与经典并发异常、 MVCC 的实现原理、锁的行为与死锁处理——这是后端面试与线上事故排查的高频区。

6.1 事务基础:ACID

字母含义生活化解释
Atomicity原子性转账扣款和入账必须同时成功或同时失败
Consistency一致性约束不被破坏,钱不会凭空多出来
Isolation隔离性并发事务互不干扰
Durability持久性提交即落盘,断电也不丢
sql
BEGIN;                              -- 或 START TRANSACTION,开启事务
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;                             -- 提交:两步同时生效

-- 出问题了?回滚:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
ROLLBACK;                           -- 一切恢复原样

-- SAVEPOINT:事务里的小抄本,可局部回滚
BEGIN;
INSERT INTO orders (id) VALUES (1);
SAVEPOINT sp1;
INSERT INTO orders (id) VALUES (2);
ROLLBACK TO sp1;                    -- 只撤销第 2 条
COMMIT;                             -- 第 1 条仍保留

6.2 并发异常:为什么需要隔离级别

多个事务并发读写同一批数据时,会出现几种经典「脏象」:

异常现象
脏读读到了别的事务未提交的数据(随后可能回滚)
不可重复读同一事务内两次读同一行,值变了(被别人 UPDATE 了)
幻读同一事务内两次按条件查,行数变了(被别人 INSERT 了)
丢失更新两个事务同时改一行,后提交的覆盖先提交的

6.3 PostgreSQL 的三个隔离级别

PG 用 MVCC 天然规避脏读,提供三个级别(READ UNCOMMITTED 语法保留但等同 READ COMMITTED):

隔离级别实现能防住
READ COMMITTED(默认)每条语句取最新快照脏读
REPEATABLE READ事务开始时取快照,整个事务复用脏读、不可重复读
SERIALIZABLESSI:快照 + 读写冲突检测全部,包括写倾斜
sql
-- 设置事务隔离级别(推荐每个事务级别设置)
BEGIN ISOLATION LEVEL SERIALIZABLE;
...
COMMIT;

-- 也可以会话级设置
SET default_transaction_isolation = 'repeatable read';

-- SERIALIZABLE 下检测到冲突会直接报错回滚,应用需捕获后重试:
-- ERROR: could not serialize access due to read/write dependencies
-- among transactions(SQLSTATE 40001 —— 重试即可)
读已提交的妙用 默认级别下 UPDATE/DELETE 若撞上已被别的事务锁住的行,会等待该事务结束再重新检查条件(EvalPlanQual 机制), 所以「热点行竞争」在 RC 下有自动二次机会;RR/SERIALIZABLE 下撞锁则可能直接报序列化冲突。

6.4 MVCC:多版本并发控制

PG 高并发的核心机密:写不阻塞读、读不阻塞写。原理是每行保存两个隐藏列:

  • xmin:创建这版数据的事务 ID;
  • xmax:删除(或更新前版本作废)这版数据的事务 ID,未删为 0。

UPDATE = 插入一个新版本行 + 把旧行的 xmax 打上标记,旧版本留给「还在运行的旧事务」继续读。 读操作永远只看「对自己可见的版本」,因此互不阻塞。代价就是第 5 章说的死元组,由 VACUUM 清理。

sql
-- 亲眼看看隐藏列
SELECT xmin, xmax, * FROM orders WHERE id = 101;

6.5 锁:行锁、表锁与死锁

sql
-- 显式行锁:SELECT ... FOR UPDATE(改前锁定,防丢失更新)
-- 两个事务同时执行下面语句时,后到者会阻塞等待
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;   -- 行锁
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- FOR UPDATE:锁定并等待;NOWAIT:锁不到立即报错;SKIP LOCKED:跳过(任务队列神器)
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY id LIMIT 1
FOR UPDATE SKIP LOCKED;     -- 多 worker 抢任务互不冲突

-- 表锁(锁住整张表,谨慎使用)
LOCK TABLE orders IN EXCLUSIVE MODE;

-- 咨询锁(advisory lock):应用层自定义锁,不绑任何表
SELECT pg_advisory_lock(12345);
SELECT pg_advisory_unlock(12345);
死锁 事务 A 锁行 1 想要行 2,事务 B 锁行 2 想要行 1 —— 互相等待。PostgreSQL 自动检测死锁, 中止其中一个事务并报 deadlock detected。预防方法:所有事务按相同顺序访问资源、 大事务拆小、必要时用 FOR UPDATE NOWAIT 快速失败重试。

6.6 长事务的危害

一个事务开着不提交,它持有的快照会阻止 VACUUM 清理「它还看得见的旧版本」, 导致表膨胀、索引膨胀;同时它持有的锁会阻塞别人。生产环境务必:

sql
-- 监控超过 1 分钟的空闲事务(配合 idle_in_transaction_session_timeout 自动终止)
SELECT pid, usename, state, now() - xact_start AS xact_age,
       now() - state_change AS idle_time, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
  AND now() - xact_start > INTERVAL '1 minute'
ORDER BY xact_age DESC;

-- 紧急终止某个卡死的会话
SELECT pg_terminate_backend(pid);
并发模型想明白了吗?标记完成。
CHAPTER 07

权限管理

数据库安全的基石:角色(Role)体系、GRANT/REVOKE 权限矩阵、默认权限、 行级安全(RLS)——从「谁能连库」到「谁能看见哪一行」的完整控制链。

7.1 角色模型:用户和组是同一个东西

PostgreSQL 里没有独立的「用户」概念——一切皆角色(Role)。带 LOGIN 属性的角色就是「用户」, 可以登录;不带的就是「组」,用来批量授权。角色还可以继承其他角色的权限。

sql
-- 创建可登录的用户角色
CREATE ROLE app_user LOGIN PASSWORD 'StrongPass!123';

-- 创建「组角色」,不登录、只作为权限集合
CREATE ROLE read_only NOLOGIN;

-- 角色属性速查
CREATE ROLE dev_admin LOGIN CREATEDB;           -- 可建数据库
CREATE ROLE super_x LOGIN SUPERUSER;            -- 超级用户(慎用,等同 root)
CREATE ROLE limited LOGIN CONNECTION LIMIT 10;   -- 限制并发连接数
ALTER ROLE app_user VALID UNTIL '2026-12-31';   -- 密码有效期

-- 用户加入组:获得组的所有权限
GRANT read_only TO app_user;

-- 查看现有角色与属性
\du
SELECT rolname, rolcanlogin, rolsuper FROM pg_roles;

7.2 GRANT / REVOKE:权限的授予与回收

权限是分层级的:数据库 → 模式 → 表(还有列级、序列、函数)。

sql
-- 库级:连接权限
GRANT CONNECT ON DATABASE shop TO app_user;

-- 模式级:能「看见」并使用该模式
GRANT USAGE ON SCHEMA public TO app_user;

-- 表级:增删改查
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only;   -- 模式下所有表

-- 只开放部分列(如隐藏手机号)
GRANT SELECT (id, name, city) ON users TO read_only;

-- 序列也要授权,否则 INSERT 会失败(自增主键取号需要)
GRANT USAGE, SELECT ON SEQUENCE orders_id_seq TO app_user;

-- 回收
REVOKE DELETE ON orders FROM app_user;

-- 默认权限:让「将来新建的表」自动授权(新表免手工 GRANT)
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
GRANT SELECT ON TABLES TO read_only;
常见坑 建完表只给应用账号 SELECT,结果 INSERT 报错 permission denied for sequence—— 忘了给自增序列授权。建表者拥有全部权限,但其他角色必须逐项授予。

7.3 权限查询与 PUBLIC

sql
-- 查看某张表的授权情况
SELECT grantee, privilege_type
FROM information_schema.role_table_grants
WHERE table_name = 'orders';

-- PUBLIC 是「所有角色的隐式组」。以下收走 PUBLIC 的默认权限(多租户环境常用加固)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON DATABASE shop FROM PUBLIC;

7.4 行级安全(RLS):同一张表,每人只看自己的行

传统 GRANT 只能控制到「表/列」。RLS 可以控制到行:给表绑定策略后, 每条查询都会自动套上 WHERE 条件。这是多租户 SaaS 的标配能力。

sql
-- 1. 建表 + 打开 RLS
CREATE TABLE tenant_data (
    id       integer PRIMARY KEY,
    tenant   text NOT NULL,          -- 租户标识
    content  text
);
ALTER TABLE tenant_data ENABLE ROW LEVEL SECURITY;

-- 2. 建立策略:每个用户只能看到自己租户的行
CREATE POLICY tenant_isolation ON tenant_data
    USING (tenant = current_user);            -- 读:过滤
--  更完整的语法:USING(读)+ WITH CHECK(写校验)
CREATE POLICY tenant_rw ON tenant_data
    FOR ALL
    USING (tenant = current_setting('app.tenant'))
    WITH CHECK (tenant = current_setting('app.tenant'));

-- 3. 应用侧设置会话变量(连接池里按请求设置)
SET app.tenant = 'company_a';
SELECT * FROM tenant_data;    -- 自动只返回 company_a 的行!

-- 表拥有者、超级用户默认不受 RLS 约束;需要约束他们时:
ALTER TABLE tenant_data FORCE ROW LEVEL SECURITY;
面试高频 RLS 策略对表的所有者默认不生效(OWNED BY 的用户绕过策略), 需 FORCE ROW LEVEL SECURITY 强制。生产上常见做法是应用连一个非 owner 账号。

7.5 安全清单

  • 应用绝不使用超级用户连接;按业务划分最小权限账号(读写分离更是如此);
  • pg_hba.conf 控制谁能从哪台机器连库(IP 白名单 + 认证方式 scram-sha-256);
  • 密码复杂度与有效期:ALTER ROLE ... PASSWORD + passwordcheck 扩展;
  • 审计敏感操作:开启 log_statement = 'ddl',或 pgaudit 扩展;
  • 敏感列考虑 pgcrypto 加密存储,而非明文落库。
权限体系理清了吗?标记完成。
CHAPTER 08

备份与恢复

没经过恢复演练的备份等于没备份。本章覆盖逻辑备份(pg_dump)、物理备份(pg_basebackup)、 WAL 归档与 PITR 时间点恢复,以及一套可落地的备份策略。

8.1 备份方式总览

方式粒度特点典型用途
pg_dump / pg_dumpall(逻辑)库 / 全实例导出 SQL 或归档;跨版本、跨平台日常备份、迁移、单库恢复
pg_basebackup(物理)整实例复制数据目录 + WAL从库搭建、全量恢复
WAL 归档 + 基础备份(PITR)整实例可恢复到任意时间点误操作急救、合规要求
快照(文件系统/云盘级)整实例依赖外部一致性快照能力云环境常用

8.2 逻辑备份:pg_dump

bash
# 四种格式:p 纯 SQL(默认)/ c 自定义压缩 / d 目录 / t tar
# 推荐 -Fc:压缩、支持 pg_restore 并行与选择性恢复
pg_dump -h localhost -p 5432 -U postgres -Fc -f shop_20260917.dump shop

# 只备份指定表
pg_dump -Fc -t orders -t users -f subset.dump shop

# pg_dumpall:备份「全部数据库」+ 全局对象(角色、表空间)——pg_dump 不含这些!
pg_dumpall -U postgres --globals-only > globals.sql
pg_dumpall -U postgres > all_databases.sql

8.3 逻辑恢复

bash
# 自定义格式用 pg_restore(-j 并行提速,多核恢复快数倍)
createdb shop_new                        # 先建好空库
pg_restore -U postgres -d shop_new -j 4 shop_20260917.dump

# 恢复单张表到指定库(自定义格式的杀手锏)
pg_restore -d shop_new -t orders shop_20260917.dump

# 纯 SQL 格式直接喂给 psql
psql -U postgres -d shop_new < shop_backup.sql

# 恢复全局对象(角色、密码)
psql -U postgres -f globals.sql
跨版本迁移 pg_dump 的产物可以恢复到更新的大版本(14 → 16 没问题,反之不行)。 大版本升级的经典路线:新版本起空实例 → pg_dump 老库 → pg_restore 进新实例。

8.4 物理备份:pg_basebackup

bash
# 前提:postgresql.conf 开启复制连接(在 pg_hba.conf 放行 replication)
# 基础备份:-Xs 一并打包 WAL,-P 显示进度,-Fp 平铺文件格式
pg_basebackup -h localhost -p 5432 -U replicator \
    -D /backup/base_20260917 -Fp -Xs -P

# 产物直接就是一份可启动的数据目录:
# 恢复 = 停库 → 用备份目录替换数据目录 → 启动

8.5 WAL 与 PITR:恢复到任意时间点

WAL(Write-Ahead Log,预写日志)记录了数据库的每一次变更。先写日志再改数据, 既是崩溃安全的根基,也让「重放到过去任意时刻」成为可能。PITR 三步走:

第一步:开启 WAL 归档

ini
# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'cp %p /archive/%f'    # %p WAL 源路径,%f 文件名
max_wal_senders = 3

第二步:定期做基础备份

用 8.4 的 pg_basebackup 每天做一次全量(生产建议配合 pgBackRest / Barman 等工具管理)。

第三步:恢复到指定时间点

bash
# 1) 停库,用基础备份替换数据目录
# 2) 数据目录下创建 recovery.signal(PG12+)
touch /var/lib/postgresql/18/main/recovery.signal

# 3) postgresql.conf(或 recovery 相关配置)指定归档位置与目标时间
restore_command = 'cp /archive/%f %p'
recovery_target_time = '2026-09-17 15:30:00+08'

# 4) 启动数据库 —— 自动重放 WAL 到目标时刻并暂停
# 5) 确认数据无误后:SELECT pg_wal_replay_resume();  正常对外服务
典型急救场景 15:35 有人无 WHERE 全表 DELETE——恢复流程:停库 → PITR 恢复到 15:29:59 → 验证 → 导出丢失的数据回补到主库。务必先演练,没跑通过恢复流程的备份只是心理安慰。

8.6 备份策略建议

  • 日常:pg_dump -Fc 每日全量(保留 7-30 天)+ pg_dumpall --globals-only 每周;
  • 进阶:pg_basebackup 每周全量 + WAL 持续归档 → 任意时间点恢复;
  • 异地:备份同步到对象存储 / 异地机房(3-2-1 原则:3 份副本、2 种介质、1 份异地);
  • 演练:每季度真实恢复一次,计时、验证数据完整性;
  • 验证:恢复后 pg_restore -l 备份文件 检查内容清单,别等出事才发现备份是空的。
能独立设计备份方案了吗?标记完成。
CHAPTER 09

向量插件专题:pgvector

AI 时代,数据库也要懂「语义」。pgvector 为 PostgreSQL 带来向量数据类型与高速相似度检索能力, 让「以文搜文、以图搜图、RAG 知识库、推荐召回」直接跑在你的 PG 上。本章从安装配置一路讲到生产实战。

9.1 背景知识:什么是向量检索

机器学习模型可以把文字、图片、音频映射成高维实数向量(Embedding),语义相近的内容在向量空间里距离也相近。 「找相似」的问题就变成了「在高维空间找近邻」(Nearest Neighbor)。例如:

  • "机器学习入门" → [0.12, -0.38, 0.77, ...](768 或 1536 维)
  • "人工智能教程" → 在向量空间里与上面距离很近
  • "今天中午吃什么" → 距离很远

传统数据库的 B-tree 只能比较大小,做不了高维近邻搜索;而专业向量库(Milvus、Pinecone)又引入了新的运维成本。 pgvector 的价值:向量与业务数据同库,一个 JOIN 就能把「语义检索」和「权限、价格、库存」等业务条件组合起来。

9.2 安装与配置

Linux(apt / yum 包管理器,最简单)

bash
# Debian / Ubuntu(需先配置 PGDG 源)
sudo apt install postgresql-17-pgvector

# RHEL / CentOS
sudo yum install pgvector_17

Windows(无官方包,需替换 DLL)

text
1) 从 pgvector 的 GitHub Releases 下载对应 PG 大版本的 Windows 构建包
   (或用 Visual Studio 开发命令行 + nmake 自行编译)
2) 复制文件到 PG 安装目录:
   vector.dll        -> D:\env\postgresql\18.6.3\lib\
   vector.control    -> D:\env\postgresql\18.6.3\share\extension\
   vector--0.8.0.sql -> D:\env\postgresql\18.6.3\share\extension\
3) 重启 PostgreSQL 服务(DLL 被加载锁定,必须先 Stop 再复制)
4) psql 连接后启用扩展
本机示例 以本教程配套环境为例:PG 18.6.3 安装在 D:\env\postgresql\18.6.3、运行在端口 5433。 Windows 下升级/替换 vector.dll 时的标准顺序:停服务 → 复制文件 → 启服务 → CREATE EXTENSION—— 已加载的 DLL 会被系统锁定,不停服务直接覆盖会报「访问被拒绝」。

启用扩展

sql
-- 每个需要使用的数据库执行一次;超级用户或有 CREATE 权限即可
CREATE EXTENSION vector;

-- 验证安装成功
SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';
--  vector | 0.8.0

9.3 向量数据类型

类型说明每维存储
vector(n)标准向量,float4 单精度,最常用4 字节
halfvec(n)半精度向量(0.7+),省一半空间,大规模场景划算2 字节
sparsevec(n)稀疏向量,大量维度为 0 时(如词袋)极省空间6 字节/非零项
bit(n)位向量,配汉明/杰卡德距离(哈希指纹去重场景)1 比特
sql
-- n 是维度,必须与你的 Embedding 模型一致(写死后插入不同维度会报错)
-- 常见模型维度:OpenAI text-embedding-3-small = 1536;bge-m3 = 1024
CREATE TABLE documents (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    content    text NOT NULL,             -- 原文
    embedding  vector(1536)              -- 向量列
);

-- 字面量:方括号内逗号分隔
INSERT INTO documents (content, embedding)
VALUES ('PostgreSQL 入门教程', '[0.1, 0.2, 0.3, ...]');   -- 实际 1536 个数

-- 类型转换
SELECT '[1,2,3]'::vector(3);

-- 维度查询与常用操作
SELECT embedding::text FROM documents LIMIT 1;
SELECT array_length(string_to_array(embedding::text, ','), 1) AS dims FROM documents LIMIT 1;
归一化建议 如果用余弦相似度(绝大多数文本/图像场景),建议入库前把向量归一化为单位长度(模长 1)。 归一化后三种距离的排序结果一致,且可用更快的内积计算。

9.4 距离与相似度运算符

运算符含义值越小代表典型场景
-> 等一下——见下(占位防误读,请看下方代码块)
sql
-- 三大距离运算符(值越小 = 越相似)
-- <->  欧氏距离(L2):最直观的"空间距离",通用
-- <=>  余弦距离:1 - cos(θ),文本语义检索最常用(对向量长度不敏感)
-- <#>  负内积:向量已归一化时最快,结果等价余弦排序

-- 精确检索(无索引时全表扫描,适合小数据量)
SELECT content, embedding <-> '[0.1, 0.2, ...]'::vector AS distance
FROM documents
ORDER BY embedding <-> '[0.1, 0.2, ...]'::vector
LIMIT 5;

-- 相似度(1 - 余弦距离)
SELECT 1 - (embedding <=> '[0.1, 0.2, ...]'::vector) AS similarity
FROM documents ORDER BY similarity DESC LIMIT 5;

选择建议:

  • 文本语义 / RAG:余弦 <=>(模型输出与文本长度无关)
  • 向量已归一化:改用 内积 <#>(数学等价且更快)
  • 图像原始特征 / 聚类:欧氏 <->

9.5 向量索引:HNSW 与 IVFFlat

数据到几万条以上时,精确检索的暴力扫描会变慢。pgvector 提供两种近似最近邻(ANN)索引: 用极小的召回损失换取数量级的提速。

HNSW(推荐)IVFFlat
原理分层可导航小世界图倒排文件 + 聚类中心
查询速度更快、召回更稳快,依赖参数调优
构建速度较慢、占空间更大更快
建索引时机随时(边插边建也行)建议数据灌完后建(聚类中心需要真实分布)
更新友好度更好较差

创建索引

sql
-- HNSW 索引:为余弦距离建(运算符要与查询一致!)
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 200);
--   m:每节点连接数(默认16,越大召回越高、内存越大)
--   ef_construction:建索引时的搜索宽度(默认64,越大质量越好、建得越慢)

-- 对应三种距离的 opclass:
--   vector_cosine_ops   配 <=>
--   vector_l2_ops       配 <->
--   ipops:vector_ip_ops  配 <#>

-- IVFFlat 索引:lists 经验值 = 表行数 / 1000(百万级数据取 1000 左右)
CREATE INDEX ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- 半精度列建 halfvec 索引(0.7+):空间减半
CREATE INDEX ON documents
USING hnsw ((embedding::halfvec(1536)) halfvec_cosine_ops);

查询侧调参:召回 vs 速度

sql
-- HNSW:增大 ef_search 提高召回(默认 40)
SET hnsw.ef_search = 100;

-- IVFFlat:增大 probes 探测更多聚类桶,提高召回(默认 1,往往太小)
SET ivfflat.probes = 10;

-- 验证召回率:对比"走索引的近似结果"与"精确结果"的重合度
SET enable_seqscan = off;   -- 强制走索引测试
SELECT id FROM documents ORDER BY embedding <=> '[...]'::vector LIMIT 20;
最常见翻车点 索引的 opclass 与查询运算符必须匹配:建了 vector_cosine_ops 却用 <-> 排序, 查询照样走不了索引(或结果错误)。其次是 IVFFlat 建在空表上——聚类中心无意义,检索质量很差。

9.6 相似度检索实战:完整流程

sql
-- 1) 建表 + 索引
CREATE TABLE kb_chunks (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    doc_title  text,
    chunk      text NOT NULL,          -- 切块后的文本(建议 200-500 字/块)
    embedding  vector(1536),
    created_at timestamptz DEFAULT now()
);

-- 2) 灌数据 + 建索引(先灌后建,IVFFlat 才准)
-- INSERT INTO kb_chunks (doc_title, chunk, embedding) VALUES (...);
CREATE INDEX ON kb_chunks USING hnsw (embedding vector_cosine_ops);

-- 3) 语义检索:给定问题向量,找最相似的 5 个知识块
SELECT doc_title, chunk,
       1 - (embedding <=> $1::vector) AS similarity
FROM kb_chunks
ORDER BY embedding <=> $1::vector
LIMIT 5;

-- 4) 向量 + 业务条件组合:pgvector 的核心优势(专业向量库做不到或很别扭)
--    "只在已发布、属于我可见范围的文章里做语义检索"
SELECT doc_title, chunk
FROM kb_chunks c
JOIN docs d ON d.id = c.doc_id
WHERE d.status = 'published'
  AND d.owner = current_user
ORDER BY c.embedding <=> $1::vector
LIMIT 5;

-- 5) 0.8+ 的迭代式索引扫描:WHERE 过滤掉大半时,先 ANN 多取再过滤,仍走索引
SET hnsw.iterative_scan = relaxed_order;

应用侧:用 Python 生成向量并检索(RAG 检索核心代码)

python
import psycopg2
from openai import OpenAI          # 或本地模型(ollama、sentence-transformers)

client = OpenAI()

def embed(text: str) -> list[float]:
    resp = client.embeddings.create(
        model="text-embedding-3-small", input=text)
    return resp.data[0].embedding   # 1536 维

conn = psycopg2.connect(host="localhost", port=5433,
                        dbname="ragdb", user="postgres", password="***")

# 入库:文档切块 + 向量化
chunks = split_into_chunks(markdown_text, size=400)
with conn.cursor() as cur:
    for c in chunks:
        cur.execute(
            "INSERT INTO kb_chunks (doc_title, chunk, embedding) VALUES (%s,%s,%s)",
            (title, c, str(embed(c))))

# 检索:问题向量化 + 相似度查询
question = "PostgreSQL 怎么做时间点恢复?"
with conn.cursor() as cur:
    cur.execute(
        """SELECT chunk, 1 - (embedding <=> %s::vector) AS sim
           FROM kb_chunks
           ORDER BY embedding <=> %s::vector
           LIMIT 5""",
        (str(embed(question)),) * 2)
    top_chunks = cur.fetchall()

# 把 top_chunks 拼进 Prompt,交给 LLM 生成最终回答(RAG 的 R 部分)
context = "\n---\n".join(row[0] for row in top_chunks)

9.7 向量扩展生态

扩展 / 工具定位与配合方式
pgvector事实标准:类型 + HNSW/IVFFlat,全生态兼容
pgvectorscale(Timescale)在 pgvector 之上提供 StreamingDiskANN 索引,超大规模 + 磁盘索引,与 pgvector 数据完全兼容,可叠加使用
lantern另一 ANN 实现,主打更高性能,类型同样兼容
pgai / pg_vectorize在库内自动调用 LLM 生成向量、自动化 RAG 管道
pg_embeddingAWS 的早期探索,已被 pgvector 取代,新项目不要用
LangChain / LlamaIndex框架侧官方 PGVector VectorStore,直接对接 pgvector
pgvector-pythonPython 驱动注册向量类型,免手写字符串拼接

9.8 四大实战应用场景

场景一:RAG 企业知识库(最主流)

流程:文档 → 切块 → Embedding → 入库 → 检索 Top-K → 拼进 Prompt → LLM 回答。 相对微调,RAG 便宜、知识可实时更新、答案可溯源(把 chunk 的原文出处一并返回给前端)。 上一节的代码就是这个场景的最小可用实现。生产化要点:

  • 切块保留标题/路径元数据,检索结果可溯源;
  • 混合检索:向量(语义)+ tsvector 全文(关键词)双路召回再融合,专有名词查准率大幅提升;
  • 相似度阈值兜底:低于阈值直接说「不知道」,避免幻觉。

场景二:以文搜图 / 以图搜图(CLIP 双塔)

sql
-- CLIP 模型:文本和图片映射到同一向量空间 → 文搜图天然可行
CREATE TABLE images (
    id          bigint PRIMARY KEY,
    url         text,
    text_vec    vector(512),    -- 用文字描述的向量(或查询时实时算)
    image_vec   vector(512)     -- 图片向量
);

-- "落日下的海滩" 搜图:文本向量 与 图片空间比对
SELECT url, image_vec <=> $1::vector AS distance
FROM images
ORDER BY image_vec <=> $1::vector   -- $1 = embed("落日下的海滩")
LIMIT 20;

场景三:推荐系统召回

sql
-- item2vec / 双塔召回:商品、用户都向量化
-- "看过 A 的用户还看了什么" = 查 A 的向量近邻,再叠加业务过滤
SELECT p.id, p.title
FROM products p
WHERE p.id != $1
  AND p.status = 'on_sale'
  AND p.stock > 0
ORDER BY p.embedding <=> (SELECT embedding FROM products WHERE id = $1)
LIMIT 50;

-- 用户长期兴趣 = 近 30 天行为向量的加权平均 → 再查近邻
-- 这类"向量 + 业务条件"的混合查询正是 pgvector 的主场

场景四:内容去重与查重

sql
-- 新文章入库前先查相似度,> 0.95 判定为重复/洗稿
SELECT id, title, 1 - (embedding <=> $1::vector) AS sim
FROM articles
ORDER BY embedding <=> $1::vector
LIMIT 1;
-- sim > 0.95 → 进入人工审核队列

9.9 生产环境检查清单

  • 维度写死在类型里,换模型 = 迁移数据(新列 + 回填,别原地改);
  • 索引 opclass 与查询运算符保持一致,上线前用 EXPLAIN ANALYZE 确认走了 Index Scan;
  • 参数化查询防注入:向量用 $1::vector 占位符拼接,别拼字符串;
  • 监控召回率:定期抽样对比近似/精确结果的 Top-K 重合度;
  • 大批量写入后 ANALYZE,HNSW 索引膨胀严重时 REINDEX;
  • 连接池 + SET LOCAL hnsw.ef_search 在事务级控制参数,避免会话间污染。
完成全部课程!标记完成最后一章,看看 100% 的进度环。

PostgreSQL 从入门到精通 · 交互式学习手册 — 学习进度与打卡记录保存在本地浏览器(localStorage)

建议结合实机练习:本机 PG 18 运行于 localhost:5433

© 2026 searchdb.cn · bowen · 联系:13098832914 · 920877898@qq.com
鄂ICP备2021008610号-7