从第一条 SQL,
到向量检索与高可用。
一份循序渐进的 PostgreSQL 完整学习手册:基础语法 → 数据建模 → 查询进阶 → 性能调优 → 事务并发 → 权限安全 → 备份恢复,最后深入向量插件 pgvector,覆盖 AI 时代的相似度检索实战。 每章配有测试题,学完一章就打卡,进度自动保存。
基础语法入门
PostgreSQL 是世界上最先进的开源关系型数据库。本章从零开始:了解它的核心概念、用 psql 连接数据库、 掌握 SQL 四大语言分类和最基础的增删改查语句。
1.1 认识 PostgreSQL
PostgreSQL(常简写为 Postgres)诞生于加州大学伯克利分校的 Ingres 项目,自 1996 年开源以来, 以「正确性优先」的设计哲学著称。它是完全的事务型关系数据库,同时具备传统商用数据库的绝大部分能力:
- 标准兼容:SQL 标准符合度极高,同时大量扩展(窗口函数、CTE、JSON、数组、全文检索一应俱全)
- 可靠:ACID 事务、MVCC 并发控制、崩溃后自动恢复(WAL)
- 可扩展:自定义类型、函数、操作符、外部数据包装器(FDW)、插件生态(如 pgvector)
- 免费开源:PostgreSQL 许可证,无商业授权费用
EDB 安装包 或 winget install PostgreSQL.PostgreSQL,
Linux 用各发行版仓库或官方 PGDG 源,macOS 用 brew install postgresql@17。
1.2 使用 psql 连接数据库
psql 是 PostgreSQL 自带的命令行客户端,也是学习阶段最好的工具——它稳定、纯粹、所有教学资料通用。 连接数据库的基本语法:
# 基本连接格式
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 不是一种语言,而是四类语言的集合。理解分类,你就能把任何教程里的语句「对号入座」:
| 分类 | 全称 | 负责什么 | 代表语句 |
|---|---|---|---|
| DQL | Data Query Language | 查询数据 | SELECT |
| DML | Data Manipulation Language | 增删改数据 | INSERT UPDATE DELETE |
| DDL | Data Definition Language | 定义结构 | CREATE ALTER DROP TRUNCATE |
| DCL | Data Control Language | 权限控制 | GRANT REVOKE |
1.4 第一批 SQL 语句
建一张练习表
CREATE TABLE student (
id integer PRIMARY KEY,
name text NOT NULL,
major text,
score numeric(5, 2),
enrolled date DEFAULT CURRENT_DATE
);插入数据(INSERT)
-- 单条插入
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)
-- 查询所有列、所有行
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)
-- 更新:一定要带 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:
-- 错误:永远查不到 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 忽略大小写 |
| 注释 | -- 单行 /* 多行 */ | — |
数据库与表操作
数据库、模式、表是 PostgreSQL 的三级存储结构。本章掌握数据库与模式的组织方式、 建表的完整姿势、五大约束、以及表结构的日常维护操作。
2.1 数据库与模式(Schema)
PostgreSQL 的层级结构是:实例(集群)→ 数据库 → 模式 → 表。
- 一个实例可以承载多个数据库,数据库之间完全隔离;
- 每个数据库内有多个模式(schema),默认都有一个
public模式; - 表必须属于某个模式,全限定名写作
schema.table,省略时按search_path查找。
-- 创建 / 删除数据库(数据库级别操作,不能在事务里执行)
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; -- 切换默认查找路径public 模式不再对所有人开放 CREATE 权限(安全加固),
普通用户建表前可能需要 GRANT CREATE ON SCHEMA public TO 用户。
2.2 CREATE TABLE 与五大约束
建表的本质是定义「列 + 类型 + 约束」。约束是数据库替你把关数据质量的第一道防线:
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 |
| 非空 | 禁止 NULL | NOT NULL |
| 唯一 | 列值不重复(NULL 除外) | UNIQUE |
| 检查 | 自定义布尔规则 | CHECK (表达式) |
自增主键:IDENTITY vs 序列
-- 现代推荐: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:修改表结构
-- 加列(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 的区别
-- DROP:连表带结构、索引、约束一起删除
DROP TABLE IF EXISTS orders;
-- 若有别的对象依赖它(如视图),会报错;CASCADE 连依赖一起删(危险)
DROP TABLE orders CASCADE;
-- TRUNCATE:只清空数据、保留结构,比 DELETE 快得多(直接重写文件块)
TRUNCATE TABLE logs;
-- 多表一起清,并重置序列
TRUNCATE logs, audit RESTART IDENTITY CASCADE;| DELETE | TRUNCATE | DROP | |
|---|---|---|---|
| 删除内容 | 行(可带 WHERE) | 全部行 | 表 + 结构 |
| 速度 | 逐行删除,慢 | 极快 | 快 |
| 可回滚 | 可以 | 可以(PG 中是事务性的) | 可以 |
| 触发 DELETE 触发器 | 是 | 否 | — |
| 回收磁盘空间 | 需 VACUUM | 立即 | 立即 |
2.5 视图与物化视图
-- 普通视图:存储的是查询语句,每次查询实时计算
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 临时表
-- 会话结束自动销毁,常用于中间结果
CREATE TEMP TABLE tmp_calc AS
SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id;数据类型详解
PostgreSQL 拥有业界最丰富的类型系统。选对类型 = 更好的性能 + 更少的应用层校验。 本章覆盖数值、文本、时间、布尔四大基础,以及数组、JSON/JSONB、枚举、UUID、范围类型等 PG 特色。
3.1 数值类型
| 类型 | 范围 / 精度 | 适用场景 |
|---|---|---|
smallint | 16 位整数 ±3.2 万 | 省空间的枚举数值 |
integer | 32 位整数 ±21 亿 | 常规整数首选 |
bigint | 64 位整数 | 主键、计数器、时间戳 |
numeric(p,s) | 任意精度,精确 | 金额必选,无浮点误差 |
real / double precision | 浮点,有误差 | 科学计算、坐标(不要求精确) |
serial / bigserial | 自增整数 | 旧式自增主键 |
float / double 存钱:0.1 + 0.2 ≠ 0.3 的浮点误差会让你对账时痛苦万分。
金额一律 numeric(10,2)(或以「分」为单位存 bigint)。
3.2 文本类型
-- 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 时间与日期
-- 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 布尔类型
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)
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
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), -- PG13+ 内置函数
-- 优点:分布式生成不冲突、暴露在 URL 不泄露业务量级
-- 缺点:16 字节大于 bigint 的 8 字节,索引更大、无序写入更碎页3.7 数组类型
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。
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 / macaddr | IP 地址与网段,支持子网运算 |
int4range / tstzrange | 范围类型(如排班时间段),支持重叠、包含运算 |
bytea | 二进制大对象(图片、文件) |
bit(n) / bit varying(n) | 位串,支持位运算 |
tsvector | 全文检索类型,配合 GIN 索引 |
vector(n) | pgvector 提供的向量类型(第 9 章专讲) |
| 复合类型 | CREATE TYPE ... AS (...) 自定义行结构 |
常用 SQL 查询
查询是 SQL 的灵魂。本章从连接查询、聚合分组一路讲到 PostgreSQL 的杀手锏: 窗口函数、CTE(含递归)、DISTINCT ON、UPSERT——写出真正解决业务问题的查询。
4.1 示例数据模型
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
-- 模式匹配: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 聚合与分组
-- 常用聚合函数: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 | 表连自己(如查「员工-上级」) |
-- 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 集合运算
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 子查询
-- 标量子查询:返回单值
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」全都靠它。
-- 语法:函数(...) 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
-- 每个用户金额最高的 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
-- 计算每个用户订单金额的环比变化(当前行 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 子句与递归
-- 普通 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
-- 「每个用户金额最高的那一单」一行代码(其他数据库要羡慕哭)
SELECT DISTINCT ON (user_id)
user_id, id, amount
FROM orders
ORDER BY user_id, amount DESC;4.10 UPSERT 与 RETURNING
-- 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;索引与性能优化
90% 的「数据库好慢」都是索引问题。本章吃透六种索引类型及其适用场景, 学会用 EXPLAIN ANALYZE 读懂执行计划,掌握一套系统的慢查询排查方法。
5.1 索引的工作原理
没有索引时,查找一行数据只能顺序扫描(Sequential Scan)全表; 有了索引,数据库通过 B-tree 等结构把查找复杂度降到对数级别——就像书的目录, 从「翻遍整本书」变成「先查目录再翻页」。
-- 最基本的创建与查看
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;5.2 六种索引类型
| 类型 | 适用场景 | 典型示例 |
|---|---|---|
B-tree | 默认;等值、范围、排序(< > BETWEEN ORDER BY) | id、时间、状态列 |
Hash | 纯等值判断(=),不支持范围 | 大表精确匹配 |
GIN | 「包含」语义:jsonb、数组、全文检索 | data @> '...'、tags @> ARRAY[...] |
GiST | 几何、范围类型、地理(PostGIS)、KNN | 时间段重叠、空间检索 |
SP-GiST | 非平衡结构(基数树):IP 前缀、点 | 网段匹配 |
BRIN | 超大有物理顺序的大表(时序),块级范围索引,极小 | 日志表的时间列 |
-- 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:读懂执行计划
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;输出解读——每个节点代表一步操作,缩进表示先后(内层先执行):
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 只显示「计划」(不执行);EXPLAIN ANALYZE 真的执行并给出实际行数、实际耗时。
注意 DML 语句用 ANALYZE 会真实改数据,练习时可包在 BEGIN; ... ROLLBACK; 里。
5.4 慢查询排查流程
- 定位慢语句:开启
log_min_duration_statement = 500(记录超 500ms 的语句),或查pg_stat_statements扩展统计 TOP 慢语句; - 看计划:
EXPLAIN (ANALYZE, BUFFERS),重点看估算行数与实际行数偏差(偏差大 = 统计信息过期); - 判断问题:大表 Seq Scan?索引失效(函数包裹、隐式类型转换、前导 % 的 LIKE)?
- 修索引 / 改写法:加/改索引,避免
SELECT *,大偏移分页改键集分页; - 刷新统计:大批量写入后手动
ANALYZE 表名;,让优化器拿到新鲜数据。
-- 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:
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,别赌。
事务与并发控制
事务保证「要么全做,要么全不做」。本章讲清 ACID、隔离级别与经典并发异常、 MVCC 的实现原理、锁的行为与死锁处理——这是后端面试与线上事故排查的高频区。
6.1 事务基础:ACID
| 字母 | 含义 | 生活化解释 |
|---|---|---|
| Atomicity | 原子性 | 转账扣款和入账必须同时成功或同时失败 |
| Consistency | 一致性 | 约束不被破坏,钱不会凭空多出来 |
| Isolation | 隔离性 | 并发事务互不干扰 |
| Durability | 持久性 | 提交即落盘,断电也不丢 |
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 | 事务开始时取快照,整个事务复用 | 脏读、不可重复读 |
SERIALIZABLE | SSI:快照 + 读写冲突检测 | 全部,包括写倾斜 |
-- 设置事务隔离级别(推荐每个事务级别设置)
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 清理。
-- 亲眼看看隐藏列
SELECT xmin, xmax, * FROM orders WHERE id = 101;6.5 锁:行锁、表锁与死锁
-- 显式行锁: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);deadlock detected。预防方法:所有事务按相同顺序访问资源、
大事务拆小、必要时用 FOR UPDATE NOWAIT 快速失败重试。
6.6 长事务的危害
一个事务开着不提交,它持有的快照会阻止 VACUUM 清理「它还看得见的旧版本」, 导致表膨胀、索引膨胀;同时它持有的锁会阻塞别人。生产环境务必:
-- 监控超过 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);权限管理
数据库安全的基石:角色(Role)体系、GRANT/REVOKE 权限矩阵、默认权限、 行级安全(RLS)——从「谁能连库」到「谁能看见哪一行」的完整控制链。
7.1 角色模型:用户和组是同一个东西
PostgreSQL 里没有独立的「用户」概念——一切皆角色(Role)。带 LOGIN 属性的角色就是「用户」,
可以登录;不带的就是「组」,用来批量授权。角色还可以继承其他角色的权限。
-- 创建可登录的用户角色
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:权限的授予与回收
权限是分层级的:数据库 → 模式 → 表(还有列级、序列、函数)。
-- 库级:连接权限
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
-- 查看某张表的授权情况
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 的标配能力。
-- 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;FORCE ROW LEVEL SECURITY 强制。生产上常见做法是应用连一个非 owner 账号。
7.5 安全清单
- 应用绝不使用超级用户连接;按业务划分最小权限账号(读写分离更是如此);
pg_hba.conf控制谁能从哪台机器连库(IP 白名单 + 认证方式 scram-sha-256);- 密码复杂度与有效期:
ALTER ROLE ... PASSWORD+passwordcheck扩展; - 审计敏感操作:开启
log_statement = 'ddl',或pgaudit扩展; - 敏感列考虑
pgcrypto加密存储,而非明文落库。
备份与恢复
没经过恢复演练的备份等于没备份。本章覆盖逻辑备份(pg_dump)、物理备份(pg_basebackup)、 WAL 归档与 PITR 时间点恢复,以及一套可落地的备份策略。
8.1 备份方式总览
| 方式 | 粒度 | 特点 | 典型用途 |
|---|---|---|---|
pg_dump / pg_dumpall(逻辑) | 库 / 全实例 | 导出 SQL 或归档;跨版本、跨平台 | 日常备份、迁移、单库恢复 |
pg_basebackup(物理) | 整实例 | 复制数据目录 + WAL | 从库搭建、全量恢复 |
| WAL 归档 + 基础备份(PITR) | 整实例 | 可恢复到任意时间点 | 误操作急救、合规要求 |
| 快照(文件系统/云盘级) | 整实例 | 依赖外部一致性快照能力 | 云环境常用 |
8.2 逻辑备份:pg_dump
# 四种格式: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.sql8.3 逻辑恢复
# 自定义格式用 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.sql8.4 物理备份:pg_basebackup
# 前提: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 归档
# 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 等工具管理)。
第三步:恢复到指定时间点
# 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(); 正常对外服务8.6 备份策略建议
- 日常:pg_dump -Fc 每日全量(保留 7-30 天)+ pg_dumpall --globals-only 每周;
- 进阶:pg_basebackup 每周全量 + WAL 持续归档 → 任意时间点恢复;
- 异地:备份同步到对象存储 / 异地机房(3-2-1 原则:3 份副本、2 种介质、1 份异地);
- 演练:每季度真实恢复一次,计时、验证数据完整性;
- 验证:恢复后
pg_restore -l 备份文件检查内容清单,别等出事才发现备份是空的。
向量插件专题: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 包管理器,最简单)
# Debian / Ubuntu(需先配置 PGDG 源)
sudo apt install postgresql-17-pgvector
# RHEL / CentOS
sudo yum install pgvector_17Windows(无官方包,需替换 DLL)
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 连接后启用扩展D:\env\postgresql\18.6.3、运行在端口 5433。
Windows 下升级/替换 vector.dll 时的标准顺序:停服务 → 复制文件 → 启服务 → CREATE EXTENSION——
已加载的 DLL 会被系统锁定,不停服务直接覆盖会报「访问被拒绝」。
启用扩展
-- 每个需要使用的数据库执行一次;超级用户或有 CREATE 权限即可
CREATE EXTENSION vector;
-- 验证安装成功
SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';
-- vector | 0.8.09.3 向量数据类型
| 类型 | 说明 | 每维存储 |
|---|---|---|
vector(n) | 标准向量,float4 单精度,最常用 | 4 字节 |
halfvec(n) | 半精度向量(0.7+),省一半空间,大规模场景划算 | 2 字节 |
sparsevec(n) | 稀疏向量,大量维度为 0 时(如词袋)极省空间 | 6 字节/非零项 |
bit(n) | 位向量,配汉明/杰卡德距离(哈希指纹去重场景) | 1 比特 |
-- 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;9.4 距离与相似度运算符
| 运算符 | 含义 | 值越小代表 | 典型场景 |
|---|---|---|---|
-> 等一下——见下 | (占位防误读,请看下方代码块) | ||
-- 三大距离运算符(值越小 = 越相似)
-- <-> 欧氏距离(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 | |
|---|---|---|
| 原理 | 分层可导航小世界图 | 倒排文件 + 聚类中心 |
| 查询速度 | 更快、召回更稳 | 快,依赖参数调优 |
| 构建速度 | 较慢、占空间更大 | 更快 |
| 建索引时机 | 随时(边插边建也行) | 建议数据灌完后建(聚类中心需要真实分布) |
| 更新友好度 | 更好 | 较差 |
创建索引
-- 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 速度
-- 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;vector_cosine_ops 却用 <-> 排序,
查询照样走不了索引(或结果错误)。其次是 IVFFlat 建在空表上——聚类中心无意义,检索质量很差。
9.6 相似度检索实战:完整流程
-- 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 检索核心代码)
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_embedding | AWS 的早期探索,已被 pgvector 取代,新项目不要用 |
| LangChain / LlamaIndex | 框架侧官方 PGVector VectorStore,直接对接 pgvector |
pgvector-python | Python 驱动注册向量类型,免手写字符串拼接 |
9.8 四大实战应用场景
场景一:RAG 企业知识库(最主流)
流程:文档 → 切块 → Embedding → 入库 → 检索 Top-K → 拼进 Prompt → LLM 回答。 相对微调,RAG 便宜、知识可实时更新、答案可溯源(把 chunk 的原文出处一并返回给前端)。 上一节的代码就是这个场景的最小可用实现。生产化要点:
- 切块保留标题/路径元数据,检索结果可溯源;
- 混合检索:向量(语义)+ tsvector 全文(关键词)双路召回再融合,专有名词查准率大幅提升;
- 相似度阈值兜底:低于阈值直接说「不知道」,避免幻觉。
场景二:以文搜图 / 以图搜图(CLIP 双塔)
-- 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;场景三:推荐系统召回
-- 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 的主场场景四:内容去重与查重
-- 新文章入库前先查相似度,> 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在事务级控制参数,避免会话间污染。