avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • 首页
  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
主页 16_01PostgreSQL 介绍
文章

16_01PostgreSQL 介绍

发表于 2026-06-12 更新于 2026-06- 12
作者 Administrator
197~253 分钟 阅读

从 MySQL 到 PostgreSQL:面向 AI / RAG 开发的完整入门

适合已经知道 MySQL 基本增删改查,但没有系统使用过 PostgreSQL 的 Java 后端开发者。本文先建立 PostgreSQL 的完整基础,再讲 pgvector、Embedding 和 RAG 数据库设计。不要把 PostgreSQL 只理解成“为了向量检索而安装的数据库”。

[TOC]

1. 学完这篇文档要达到什么程度

读完并完成练习后,你应该能够:

理解 PostgreSQL 的 database、schema、table、role 分别是什么  
把常见 MySQL 建表语句改写成 PostgreSQL 语法  
使用 Docker 启动 PostgreSQL + pgvector使用 psql 或图形化工具连接数据库  
完成表、索引、约束、事务、账号和权限的基本操作  
在 Spring Boot 中连接 PostgreSQL理解 JSONB、UUID、timestamptz、identity 等常用类型和语法  
理解 pgvector 负责什么、Embedding 模型负责什么  
建立一套可用于 RAG 的文档、文本、chunk、任务和向量表  
写出带业务过滤条件的 topK 向量检索 SQL判断什么时候继续使用 pgvector,什么时候评估专业向量数据库  

先给出全文最重要的结论:

PostgreSQL 是通用关系型数据库,不是 AI 模型,也不只是向量数据库。  
pgvector 是 PostgreSQL 扩展,为 PostgreSQL 增加向量类型和相似度检索能力。  
聊天模型和 Embedding 模型不是一回事。  
本项目聊天模型统一使用 qwen3.5-flash;向量维度必须根据单独选择的 Embedding 模型确定。  
第一版 RAG 使用 PostgreSQL + pgvector,通常比同时维护多个数据库更容易学习和排错。  

2. 为什么 AI / RAG 项目经常使用 PostgreSQL

传统业务系统通常保存:

用户  
角色和权限  
订单  
商品  
配置  
操作日志  

RAG 系统除了这些结构化数据,还需要保存:

知识库  
原始文档元数据  
文档解析后的全文  
清洗后的文本  
切片后的 chunk页码、标题路径等 metadataEmbedding 向量  
解析和向量化任务状态  
失败原因  

这些数据并不全是向量。RAG 仍然有大量普通关系型数据和业务过滤条件。

例如用户提问时,系统通常不能直接从全部向量里搜索,而要先满足:

只能搜索当前租户的数据  
只能搜索当前知识库  
只能搜索用户有权限查看的文档  
只能搜索状态为 EMBEDDED 的文档  
可能只搜索某种语言、文件类型或时间范围  

PostgreSQL 的优势在于可以在同一套系统中组合使用:

普通关系型表  
事务和外键  
SQL 条件过滤  
JSONB 半结构化数据  
全文检索  
pgvector 向量检索  

因此,第一版 RAG 可以采用下面的简单结构:

graph LR;  
 A["原始文件"] --> B["文件系统 / MinIO / OSS"]; A --> C["PostgreSQL 文档元数据"];  
 C --> D["解析文本"];  
 D --> E["Chunk"]; E --> F["pgvector Embedding"]; G["用户问题"] --> H["Embedding 模型"];  
 H --> I["SQL 过滤 + 向量检索"];  
 F --> I; I --> J["TopK Chunk"]; J --> K["qwen3.5-flash 生成回答"];  

每个组件的职责必须分清:

组件 主要职责
PostgreSQL 保存文档、chunk、状态、权限、metadata 等数据
pgvector 在 PostgreSQL 中保存向量并计算向量距离
Embedding 模型 把文本转换成固定维度的数字向量
qwen3.5-flash 根据问题和检索到的上下文生成答案
文件系统或对象存储 保存 PDF、Word 等原始文件

3. 先把 MySQL 和 PostgreSQL 的概念对应起来

3.1 二者相同的地方

MySQL 和 PostgreSQL 都是关系型数据库。你在 MySQL 中掌握的很多知识仍然有效:

表、行、列  
主键、唯一约束、外键  
INSERT、SELECT、UPDATE、DELETE  
JOIN、GROUP BY、ORDER BY、LIMIT  
事务、索引、执行计划  
JDBC、连接池、MyBatis、JPA  

所以学习 PostgreSQL 不是从零学习数据库,而是调整一部分概念、类型和语法。

3.2 最大的概念差异:database 和 schema

MySQL 用户最容易在这里混淆。

MySQL 中通常把下面两个词近似看待:

CREATE DATABASE ai_rag;  
USE ai_rag;  

在 MySQL 的日常使用中,database 和 schema 基本可以当作同义词。

PostgreSQL 的层级不同:

  
一个 PostgreSQL 服务实例(cluster)  
  ├── database A  
  │     ├── schema public  
  │     │     ├── table  
  │     │     └── view  
  │     └── schema rag  
  │           ├── table  
  │           └── index  
  └── database B  
        └── schema public  

可以这样记:

MySQL 使用习惯 PostgreSQL 对应概念
一个 MySQL Server 一个 PostgreSQL 实例或 cluster
database/schema database 下还包含 schema
USE ai_rag 连接到 ai_rag database
ai_rag.user 常见写法是 app.user,其中 app 是 schema

PostgreSQL 连接建立后,只能直接访问当前 database 中的对象。不能像 MySQL 那样随意写跨 database JOIN。

同一个 database 内可以创建多个 schema:

CREATE SCHEMA app;  
CREATE SCHEMA rag;  
CREATE SCHEMA audit;  

然后创建同名表也不会冲突:

CREATE TABLE app.task (...);  
CREATE TABLE rag.task (...);  

对于当前学习项目,可以采用:

database:ai_rag  
schema:rag  
table:rag.knowledge_base、rag.document、rag.chunk  

3.3 public schema 是什么

新建 PostgreSQL database 后,通常已经存在 public schema。

如果直接执行:

CREATE TABLE demo_user (  
 id bigint PRIMARY KEY);  

在默认配置下,它实际创建为:

public.demo_user  

学习简单 SQL 时可以使用 public。正式项目更建议创建明确的业务 schema,例如 rag,避免所有对象都堆在一起。

3.4 search_path 是什么

search_path 决定省略 schema 时,PostgreSQL 按什么顺序寻找对象。

查看当前配置:

SHOW search_path;  

设置当前会话优先使用 rag:

SET search_path TO rag, public;  

设置后:

SELECT * FROM document;  

会优先寻找:

rag.document  

不过在初始化脚本、迁移脚本和关键 SQL 中,推荐写完整名称:

SELECT * FROM rag.document;  

这样不容易因为环境中的 search_path 不同而查错表。

3.5 用户和 Role 的差异

PostgreSQL 使用 role 统一表示账号和权限角色。

带 LOGIN 属性的 role 可以登录:

CREATE ROLE ai_app LOGIN PASSWORD 'change_me';  

不带 LOGIN 的 role 可以只作为权限集合:

CREATE ROLE rag_readonly;  

再把权限角色授予登录账号:

GRANT rag_readonly TO report_user;  

CREATE USER 在 PostgreSQL 中可以理解为 CREATE ROLE ... LOGIN 的便捷写法。


4. 使用 Docker 启动 PostgreSQL + pgvector

4.1 为什么建议初学者先用 Docker

Docker 可以把数据库版本、pgvector 扩展和本地数据隔离开,避免手动安装扩展时出现版本不匹配。

先检查:

docker --versiondocker compose version```  
  
### 4.2 创建 docker-compose.yml  
  
在学习项目目录创建 `docker-compose.yml`:  
  
```yaml  
services:  
 postgres: image: pgvector/pgvector:pg16 container_name: ai-rag-postgres restart: unless-stopped environment: POSTGRES_DB: ai_rag POSTGRES_USER: ai POSTGRES_PASSWORD: ai_password TZ: Asia/Shanghai ports: - "5432:5432" volumes: - ai_rag_pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ai -d ai_rag"] interval: 5s timeout: 5s retries: 10  
volumes:  
 ai_rag_pg_data:  

说明:

配置 含义
pgvector/pgvector:pg16 已安装 pgvector 的 PostgreSQL 16 镜像
POSTGRES_DB 第一次初始化时创建的 database
POSTGRES_USER 第一次初始化时创建的管理员账号
POSTGRES_PASSWORD 账号密码,仅用于本地学习示例
5432:5432 把容器端口映射到本机
volume 保存数据库文件,重启容器不会丢失
healthcheck 检查数据库是否已经可以接受连接

版本说明:截至 2026 年 6 月,PostgreSQL 18 是当前主版本,PostgreSQL 16 仍处于官方支持期。本文继续使用 pg16,是为了和 day16 现有学习环境保持一致,并不表示 PostgreSQL 只有 16 可以使用。新项目可以选择团队验证过的受支持版本;升级镜像标签前,应同时确认 JDBC、pgvector、备份恢复和部署环境兼容性。

生产环境不要把真实密码直接提交到 Git。可以通过环境变量或密钥管理系统传入。

4.3 启动并检查

启动:

docker compose up -d```  
  
查看容器:  
  
```bash  
docker compose ps```  
  
查看日志:  
  
```bash  
docker compose logs -f postgres```  
  
看到数据库可以接受连接后,按 `Ctrl+C` 退出日志查看,不会停止容器。  
  
### 4.4 进入 psql  
  
直接进入容器中的命令行客户端:  
  
```bash  
docker exec -it ai-rag-postgres psql -U ai -d ai_rag```  
  
连接成功后会看到类似:  
  
```text  
ai_rag=#  

退出:

\q  

4.5 外部工具连接参数

DBeaver、DataGrip、IDEA Database 工具窗口都可以使用:

Host: 127.0.0.1  
Port: 5432  
Database: ai_rag  
User: ai  
Password: ai_password  

JDBC URL:

jdbc:postgresql://127.0.0.1:5432/ai_rag  

注意:PostgreSQL 连接参数中通常必须明确 database,不能只填服务地址。

4.6 停止和删除的区别

停止容器但保留数据:

docker compose down```  
  
重新启动后原数据仍在:  
  
```bash  
docker compose up -d```  
  
删除容器并删除 volume:  
  
```bash  
docker compose down -v```  
  
`-v` 会删除数据库数据。只有明确想重建学习环境时才使用。  
  
---  
  
## 5. psql 入门:不要把反斜杠命令当 SQL  
  
`psql` 中有两类命令:  
  
```text  
SQL:SELECT、CREATE TABLE、INSERT 等,通常以分号结束  
psql 元命令:以反斜杠开头,不需要分号  

常用元命令:

命令 作用
\l 查看 database 列表
\c ai_rag 切换连接到指定 database
\conninfo 查看当前连接信息
\dn 查看 schema
\dt 查看当前 search_path 下的表
\dt rag.* 查看 rag schema 下的表
\d rag.document 查看表结构
\di rag.* 查看索引
\du 查看 role
\dx 查看已安装扩展
\x 切换扩展显示,查看宽表结果时很方便
\timing 显示 SQL 执行时间
\i /path/file.sql 执行 SQL 文件
\q 退出 psql

查看 PostgreSQL 版本:

SELECT version();  

查看当前 database、用户和 schema:

SELECT current_database(), current_user, current_schema();  

如果提示符从:

ai_rag=#  

变成:

ai_rag-#  

通常表示上一条 SQL 没有结束,例如漏了分号、引号或右括号。可以输入:

\r  

清空当前未执行的输入缓冲区。


6. 从第一张表开始学习 PostgreSQL

6.1 创建 schema

CREATE SCHEMA IF NOT EXISTS app;  

6.2 创建用户表

CREATE TABLE app.app_user (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username varchar(64) NOT NULL, email varchar(255), enabled boolean NOT NULL DEFAULT true, profile jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT app_user_username_uk UNIQUE (username), CONSTRAINT app_user_email_uk UNIQUE (email));  

这张表包含几个 PostgreSQL 项目中常见的选择:

identity:自增主键  
boolean:真正的布尔类型  
jsonb:可查询和索引的 JSON 数据  
timestamptz:带时区语义的时间点  
constraint:显式命名约束,排错和迁移更清晰  

6.3 插入并立即返回数据

INSERT INTO app.app_user (username, email, profile)  
VALUES (  
 'zhangsan', 'zhangsan@example.com', '{"language":"zh-CN","theme":"dark"}'::jsonb)  
RETURNING id, username, created_at;  

RETURNING 非常实用,可以让 INSERT、UPDATE、DELETE 直接返回受影响的行。

MySQL 开发中经常依赖 JDBC 获取自增 ID;PostgreSQL 也支持 JDBC generated keys,但 SQL 原生的 RETURNING 更灵活。

6.4 查询

SELECT  
 id, username, email, enabled, profile ->> 'language' AS language, created_atFROM app.app_user  
WHERE enabled = true  
ORDER BY id DESC  
LIMIT 20 OFFSET 0;  

JSONB 运算符:

  
->  返回 JSON/JSONB  
->> 返回文本  

6.5 更新并返回结果

UPDATE app.app_user  
SET  
 profile = profile || '{"theme":"light"}'::jsonb, updated_at = now()WHERE username = 'zhangsan'  
RETURNING id, username, profile, updated_at;  

|| 在这里用于合并 JSONB 对象。

6.6 删除并返回结果

DELETE FROM app.app_user  
WHERE username = 'zhangsan'  
RETURNING id, username;  

6.7 Upsert

MySQL 常见:

INSERT INTO app_user (username, email)  
VALUES ('zhangsan', 'old@example.com')  
ON DUPLICATE KEY UPDATE email = VALUES(email);  

PostgreSQL 使用 ON CONFLICT:

INSERT INTO app.app_user (username, email)  
VALUES ('zhangsan', 'new@example.com')  
ON CONFLICT (username)  
DO UPDATE SET  
 email = EXCLUDED.email, updated_at = now()RETURNING id, username, email;  

EXCLUDED 表示本次原本准备插入的那一行。


7. MySQL 和 PostgreSQL 常用语法对照

7.1 快速对照表

需求 MySQL PostgreSQL 推荐写法
自增主键 AUTO_INCREMENT GENERATED ... AS IDENTITY
旧式自增 AUTO_INCREMENT serial / bigserial,旧项目常见
布尔值 常用 tinyint(1) boolean
大文本 longtext text
二进制 blob / longblob bytea
JSON json 通常优先 jsonb
日期时间 datetime timestamp 或 timestamptz
当前时间 now() now()
空值替换 ifnull(a, b) coalesce(a, b)
字符串连接 concat(a, b) concat(a, b) 或 a || b
分组拼接 group_concat string_agg
日期格式化 date_format to_char
Upsert ON DUPLICATE KEY UPDATE ON CONFLICT ... DO UPDATE
标识符引用 反引号 `name` 双引号 "name"
字符串 单引号 'value' 单引号 'value'
忽略大小写匹配 常受 collation 影响 ILIKE
返回新增数据 JDBC generated keys SQL RETURNING
查看执行计划 EXPLAIN EXPLAIN / EXPLAIN ANALYZE

7.2 自增主键

MySQL:

id bigint AUTO_INCREMENT PRIMARY KEY  

PostgreSQL 推荐:

id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  

两个 identity 模式:

GENERATED BY DEFAULT:允许显式插入 ID,数据迁移更方便  
GENERATED ALWAYS:默认禁止显式插入 ID,约束更严格  

旧教程中经常看到:

id bigserial PRIMARY KEY  

bigserial 仍可使用,但它本质上是整数列加 sequence 的语法糖。新表优先考虑 SQL 标准的 identity。

7.3 大小写和双引号

PostgreSQL 会把未加双引号的标识符折叠为小写。

下面三条通常都在寻找同一个小写表名:

SELECT * FROM APP_USER;  
SELECT * FROM App_User;  
SELECT * FROM app_user;  

但如果创建时强行加双引号:

CREATE TABLE "AppUser" (  
 "UserId" bigint);  

以后每次都必须精确引用:

SELECT "UserId" FROM "AppUser";  

推荐规则:

表名和列名统一使用小写 snake_case不要创建需要双引号才能访问的大小写混合名称  
Java 属性继续使用 camelCase通过 ORM 或 MyBatis 映射二者  

7.4 boolean

PostgreSQL 有真正的 boolean:

enabled boolean NOT NULL DEFAULT true  

查询:

WHERE enabled = true  

也可以简写为:

WHERE enabled  

不要继续把新字段设计成 smallint 的 0 和 1,除非需要表达多种状态。

7.5 字符串和文本

PostgreSQL 常用:

varchar(n):限制最大字符数  
text:不设置业务长度上限  

在 PostgreSQL 中,text 不是低性能的“特殊大对象类型”。对于文档解析文本和 chunk 内容,直接使用 text 很合适。

7.6 日期和时间

常见类型:

类型 含义 常见用途
date 日期 生日、结算日期
time 一天中的时间 营业时间
timestamp 不带时区语义的日期时间 本地墙上时间
timestamptz 表示确定的时间点 created_at、updated_at

业务系统中的创建时间、更新时间通常推荐:

created_at timestamptz NOT NULL DEFAULT now()  

timestamptz 内部表示确定的时间点,显示时会根据当前 session 时区转换。

查看当前时区:

SHOW timezone;  

设置当前会话时区:

SET TIME ZONE 'Asia/Shanghai';  

不要在数据库、JVM、JSON 序列化之间混用不明确的时间语义。Java 中保存时间点时优先考虑 Instant 或 OffsetDateTime。

7.7 无 unsigned 整数

PostgreSQL 常用整数类型没有 MySQL 风格的 UNSIGNED。

如果字段必须非负,可以增加检查约束:

file_size bigint NOT NULL CHECK (file_size >= 0)  

7.8 group_concat 改写

MySQL:

SELECT department_id, GROUP_CONCAT(username ORDER BY username)  
FROM app_user  
GROUP BY department_id;  

PostgreSQL:

SELECT  
 department_id, string_agg(username, ',' ORDER BY username)FROM app.app_user  
GROUP BY department_id;  

7.9 模糊匹配

SELECT *  
FROM app.app_user  
WHERE username ILIKE '%ZHANG%';  

LIKE 区分大小写,ILIKE 忽略大小写。具体语言排序和大小写规则还会受到 collation 影响。

7.10 DELETE、TRUNCATE 和 sequence

删除全部数据:

DELETE FROM app.app_user;  

快速清空并重置 identity:

TRUNCATE TABLE app.app_user RESTART IDENTITY;  

有外键依赖时不要随意使用 CASCADE:

TRUNCATE TABLE app.app_user RESTART IDENTITY CASCADE;  

它可能同时清空关联表,学习时也要先确认影响范围。


8. PostgreSQL 常用数据类型

8.1 类型选择表

数据 推荐类型 说明
普通整数主键 bigint identity 单库业务表常用
分布式实体 ID uuid 文档、chunk 等可使用
金额 numeric(18,2) 不要使用浮点数保存金额
普通短字符串 varchar(n) 有明确业务长度时使用
长文本 text 文档正文、chunk、错误堆栈
开关 boolean true/false
创建时间 timestamptz 表示确定时间点
扩展属性 jsonb 可查询、可建立 GIN 索引
标签列表 text[] 简单数组场景可用
IP 地址 inet 比 varchar 更有语义
二进制 bytea 小型二进制数据
Embedding vector(n) 来自 pgvector 扩展

8.2 UUID

RAG 文档和 chunk 常使用 UUID,原因包括:

不同服务可以独立生成 ID导入导出时较少发生主键冲突  
ID 不暴露简单递增规模  

启用扩展:

CREATE EXTENSION IF NOT EXISTS pgcrypto;  

创建默认 UUID:

CREATE TABLE app.demo_uuid (  
 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL);  

插入时可以不传 ID:

INSERT INTO app.demo_uuid (name)  
VALUES ('demo')  
RETURNING id;  

8.3 JSON 和 JSONB

二者都能保存 JSON,但日常业务更常使用 jsonb:

json:更接近保存原始 JSON 文本表示  
jsonb:使用二进制结构存储,便于查询和索引  

示例:

CREATE TABLE app.event_log (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, event_type varchar(64) NOT NULL, payload jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now());  

插入:

INSERT INTO app.event_log (event_type, payload)  
VALUES (  
 'DOCUMENT_UPLOADED', '{"documentId":"d-001","source":"web","pages":12}'::jsonb);  

查询 JSONB 字段:

SELECT *  
FROM app.event_log  
WHERE payload ->> 'source' = 'web';  

包含查询:

SELECT *  
FROM app.event_log  
WHERE payload @> '{"source":"web"}'::jsonb;  

建立 GIN 索引:

CREATE INDEX event_log_payload_gin_idx  
ON app.event_log  
USING gin (payload);  

JSONB 的使用边界:

经常 JOIN、排序、唯一约束、范围查询的内容:单独建列  
结构不稳定、不同来源差异较大的补充信息:放 JSONB不要为了少建字段,把全部业务数据塞进一个 JSONB  

8.4 数组

PostgreSQL 支持数组:

CREATE TABLE app.article (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, title text NOT NULL, tags text[] NOT NULL DEFAULT '{}');  

插入:

INSERT INTO app.article (title, tags)  
VALUES ('PostgreSQL 入门', ARRAY['database', 'postgresql']);  

查询包含某标签:

SELECT *  
FROM app.article  
WHERE tags @> ARRAY['postgresql'];  

数组适合简单标签等场景。如果标签本身有名称、状态、权限和统计需求,仍应拆成关联表。


9. 约束、外键和级联删除

9.1 约束不是多余检查

应用代码会出现 bug,也可能有脚本或其他服务直接写数据库。数据库约束是最后一道数据完整性保护。

常见约束:

PRIMARY KEY  
NOT NULL  
UNIQUE  
FOREIGN KEY  
CHECK  
EXCLUDE  

9.2 外键示例

CREATE TABLE app.department (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name varchar(128) NOT NULL UNIQUE);  
  
CREATE TABLE app.employee (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, department_id bigint NOT NULL, name varchar(128) NOT NULL, CONSTRAINT employee_department_fk FOREIGN KEY (department_id) REFERENCES app.department(id) ON DELETE RESTRICT);  

常见删除策略:

策略 含义
RESTRICT / NO ACTION 有子记录时拒绝删除父记录
CASCADE 删除父记录时自动删除子记录
SET NULL 删除父记录后把子表外键设为 null

RAG 文档和 chunk 通常是强从属关系:

document_id uuid NOT NULL  
 REFERENCES rag.document(id) ON DELETE CASCADE  

删除文档时自动删除其文本和 chunk 比较合理。但知识库、用户、权限等业务关系是否级联删除,需要谨慎设计。

9.3 CHECK 约束

file_size bigint NOT NULL CHECK (file_size >= 0)  

状态字段也可以限制允许值:

status varchar(32) NOT NULL  
 CHECK (status IN ('UPLOADED', 'PARSED', 'CHUNKED', 'EMBEDDED', 'FAILED'))  

对于经常变化的状态集合,也可以由应用层和字典表管理。不要为了使用 PostgreSQL 特性而过度使用数据库 enum,因为修改 enum 的迁移管理需要额外考虑。


10. 索引:不是越多越好

10.1 B-tree

默认索引通常是 B-tree,适合:

=、<、<=、>、>=  
排序  
唯一约束  
范围查询  
CREATE INDEX app_user_created_at_idx  
ON app.app_user (created_at DESC);  

10.2 联合索引

如果查询经常是:

WHERE tenant_id = ?  
 AND status = ?ORDER BY created_at DESC  

可以评估:

CREATE INDEX document_tenant_status_created_idx  
ON rag.document (tenant_id, status, created_at DESC);  

联合索引列顺序必须根据真实查询设计,不要机械地把所有过滤字段都放进去。

10.3 部分索引

只为满足条件的数据建立索引:

CREATE INDEX document_failed_idx  
ON rag.document (updated_at)  
WHERE status = 'FAILED';  

如果失败数据只占很小比例,部分索引可能比全表索引更小、更有效。

10.4 表达式索引

CREATE INDEX app_user_lower_email_idx  
ON app.app_user (lower(email));  

对应查询应使用相同表达式:

SELECT *  
FROM app.app_user  
WHERE lower(email) = lower('ZHANGSAN@EXAMPLE.COM');  

10.5 查看执行计划

只看计划,不执行:

EXPLAIN  
SELECT *  
FROM app.app_user  
WHERE username = 'zhangsan';  

实际执行并统计:

EXPLAIN (ANALYZE, BUFFERS)  
SELECT *  
FROM app.app_user  
WHERE username = 'zhangsan';  

EXPLAIN ANALYZE 会真的执行 SQL。对生产环境的更新、删除或重查询使用前必须谨慎。

10.6 为什么索引不是越多越好

每个索引都会带来:

额外磁盘空间  
INSERT、UPDATE、DELETE 的维护成本  
VACUUM 和备份成本  
查询优化器更多选择  

先根据真实 SQL 和执行计划建索引,不要给每一列都建索引。


11. 事务、MVCC 和并发基础

11.1 基本事务

BEGIN;  
  
UPDATE app.account  
SET balance = balance - 100  
WHERE id = 1;  
  
UPDATE app.account  
SET balance = balance + 100  
WHERE id = 2;  
  
COMMIT;  

发生错误时:

ROLLBACK;  

11.2 PostgreSQL 事务错误后的状态

在事务中某条 SQL 失败后,当前事务会进入失败状态。后续 SQL 通常会提示:

current transaction is aborted  

此时需要:

ROLLBACK;  

然后重新开始事务。

11.3 MVCC 的直观理解

PostgreSQL 使用 MVCC 管理并发。可以先简单理解为:

更新数据时不是直接让所有读操作停下来  
不同事务可能看到符合自身快照的数据版本  
旧版本最终由 VACUUM 清理  

这也是 PostgreSQL 日常维护中会遇到 VACUUM、死元组和表膨胀等概念的原因。

普通项目先使用自动清理 autovacuum,不要随意关闭。

11.4 隔离级别

PostgreSQL 支持:

READ COMMITTED  
REPEATABLE READ  
SERIALIZABLE  

默认是 READ COMMITTED。它与 MySQL InnoDB 常见默认配置的理解不同,迁移并发敏感逻辑时不能只看名字,要重新验证实际行为。

11.5 sequence 不保证连续

identity 通常依赖 sequence。即使事务回滚,已经获取的序列值也可能不会退回。

所以自增 ID 可能出现空洞:

1, 2, 5, 6  

这不是数据丢失。主键用于唯一标识,不应该承担“绝对连续编号”的业务含义。


12. Role、账号和权限

12.1 不要让应用一直使用超级用户

本地学习时使用初始化账号很方便,但正式项目应创建权限受限的应用账号。

CREATE ROLE ai_app LOGIN PASSWORD 'replace_with_strong_password';  

允许连接 database:

GRANT CONNECT ON DATABASE ai_rag TO ai_app;  

允许使用 schema:

GRANT USAGE ON SCHEMA rag TO ai_app;  

授予现有表权限:

GRANT SELECT, INSERT, UPDATE, DELETE  
ON ALL TABLES IN SCHEMA rag  
TO ai_app;  

如果使用 identity/sequence,还需要 sequence 权限:

GRANT USAGE, SELECT  
ON ALL SEQUENCES IN SCHEMA rag  
TO ai_app;  

12.2 默认权限

上面的 GRANT ON ALL TABLES 只覆盖当前已经存在的表。为了让将来由某个 owner 创建的新表也自动授权,需要设置默认权限。

以下语句必须以实际对象创建者的身份执行,或明确指定 owner:

ALTER DEFAULT PRIVILEGES IN SCHEMA rag  
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ai_app;  
  
ALTER DEFAULT PRIVILEGES IN SCHEMA rag  
GRANT USAGE, SELECT ON SEQUENCES TO ai_app;  

默认权限属于“以后由哪个 role 创建对象”的规则,不是全局规则。这一点非常容易踩坑。

12.3 只读账号

CREATE ROLE rag_readonly;  
  
GRANT CONNECT ON DATABASE ai_rag TO rag_readonly;  
GRANT USAGE ON SCHEMA rag TO rag_readonly;  
GRANT SELECT ON ALL TABLES IN SCHEMA rag TO rag_readonly;  

创建可登录用户并继承只读角色:

CREATE ROLE report_user LOGIN PASSWORD 'replace_me';  
GRANT rag_readonly TO report_user;  

13. Spring Boot 连接 PostgreSQL

13.1 JDBC 驱动

Maven:

<dependency>  
 <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope></dependency>  

如果使用 Spring JDBC:

<dependency>  
 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId></dependency>  

如果使用 JPA:

<dependency>  
 <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId></dependency>  

13.2 application.yml

spring:  
 datasource: url: jdbc:postgresql://127.0.0.1:5432/ai_rag?currentSchema=rag username: ai_app password: ${DB_PASSWORD} driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000  
 jackson: time-zone: Asia/Shanghai  

currentSchema=rag 可以让连接默认优先使用该 schema,但迁移脚本和重要 SQL 仍建议显式写 schema。

密码通过环境变量传入:

$env:DB_PASSWORD="replace_me"  

13.3 MyBatis 下划线映射

mybatis:  
 configuration: map-underscore-to-camel-case: true  

数据库字段:

document_id  
chunk_index  
created_at  

Java 属性:

documentId  
chunkIndex  
createdAt  

13.4 Java 类型建议

PostgreSQL Java 常用类型
bigint Long
uuid java.util.UUID
boolean Boolean / boolean
text / varchar String
numeric BigDecimal
date LocalDate
timestamptz OffsetDateTime 或 Instant
jsonb JSON 字符串、JsonNode 或自定义类型处理器
vector float[]、List<Float> 或框架提供的向量类型

13.5 Flyway 初始化建议

依赖:

<dependency>  
 <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId></dependency>  
  
<dependency>  
 <groupId>org.flywaydb</groupId> <artifactId>flyway-database-postgresql</artifactId></dependency>  

现代 Flyway 把 PostgreSQL 支持放在单独的 flyway-database-postgresql 模块中。实际版本通常交给 Spring Boot dependency management 管理,不要随意让两个 Flyway 模块使用不同版本。

迁移文件:

  
src/main/resources/db/migration  
├── V1__create_extensions_and_schema.sql  
├── V2__create_rag_tables.sql  
└── V3__create_rag_indexes.sql  

数据库结构应由版本化迁移脚本管理,不建议依赖应用启动时自动猜测并修改生产表结构。


14. 从 MySQL 迁移到 PostgreSQL 时的常见问题

14.1 不能只替换 JDBC URL

需要检查:

建表类型  
自增主键  
布尔字段  
时间字段和时区  
反引号  
函数  
Upsert  
分页 SQLJSON 查询  
排序规则和大小写  
事务隔离级别  
保留关键字  

14.2 不要照搬 MySQL DDL

典型 MySQL DDL:

CREATE TABLE user_info (  
 id bigint unsigned NOT NULL AUTO_INCREMENT, enabled tinyint(1) NOT NULL DEFAULT 1, description longtext, created_at datetime NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  

PostgreSQL 改写:

CREATE TABLE app.user_info (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, enabled boolean NOT NULL DEFAULT true, description text, created_at timestamptz NOT NULL DEFAULT now());  

PostgreSQL 不需要 ENGINE=InnoDB 和 DEFAULT CHARSET=utf8mb4 这种表级语句。

14.3 零日期

MySQL 历史数据中可能存在:

0000-00-00  
0000-00-00 00:00:00  

PostgreSQL 不接受这种无效日期。迁移前应把它转换成 NULL 或明确的业务值。

14.4 大小写和排序规则

MySQL 某些默认 collation 下,字符串比较可能天然忽略大小写。PostgreSQL 中不要假设相同行为。

需要明确决定:

是否使用 ILIKE是否建立 lower(column) 表达式索引  
是否使用 citext 扩展  
排序和唯一性是否区分大小写  

14.5 数据迁移工具

小数据量可以导出 CSV 后使用 COPY。

导入示例:

COPY app.app_user (username, email, enabled)  
FROM '/data/app_user.csv'  
WITH (FORMAT csv, HEADER true, ENCODING 'UTF8');  

服务端 COPY 读取的是数据库服务器或容器内路径。psql 客户端本地文件可以使用 \copy:

\copy app.app_user(username, email, enabled) FROM 'D:/data/app_user.csv' WITH (FORMAT csv, HEADER true)  

真实迁移还可以评估 pgloader、ETL 工具或应用级迁移程序,并在迁移后校验:

总行数  
主键最大值  
空值数量  
金额汇总  
时间范围  
唯一约束  
外键完整性  

15. pgvector 到底是什么

15.1 pgvector 不是 Embedding 模型

pgvector 只负责:

提供 vector 数据类型  
保存向量  
计算向量距离  
提供精确和近似向量检索  
提供 HNSW、IVFFlat 等索引能力  

它不会:

读取 PDF理解文本  
把文本变成向量  
调用大模型  
自动完成 RAG  

文本向量来自 Embedding 模型:

  
"PostgreSQL 支持 JSONB"  
    -> Embedding API  
    -> [0.012, -0.087, 0.031, ...]  

项目中的聊天请求统一使用 qwen3.5-flash,但聊天模型名不能直接决定 vector(n) 的维度。必须根据实际使用的 Embedding 模型文档或 API 返回结果确定维度。

15.2 启用 vector 扩展

每个需要使用 vector 的 database 都要执行:

CREATE EXTENSION IF NOT EXISTS vector;  

检查:

SELECT extname, extversion  
FROM pg_extension  
WHERE extname = 'vector';  

或在 psql 中:

\dx  

15.3 最小向量示例

先用三维向量理解,不涉及真实 Embedding 模型:

CREATE SCHEMA IF NOT EXISTS vector_demo;  
  
CREATE TABLE vector_demo.item (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, content text NOT NULL, embedding vector(3) NOT NULL);  

插入:

INSERT INTO vector_demo.item (content, embedding)  
VALUES  
 ('苹果手机', '[0.10, 0.20, 0.30]'),  
 ('安卓手机', '[0.10, 0.20, 0.40]'),  
 ('数据库教程', '[0.80, 0.10, 0.20]');  

查询与 [0.10, 0.20, 0.35] 最接近的两条:

SELECT  
 id, content, embedding <=> '[0.10, 0.20, 0.35]' AS cosine_distanceFROM vector_demo.item  
ORDER BY embedding <=> '[0.10, 0.20, 0.35]'  
LIMIT 2;  

15.4 常见距离运算符

运算符 含义 越小越相似
<-> L2 欧氏距离 是
<#> 负内积 是,返回的是负内积
<=> Cosine distance 是
<+> L1 distance 是

RAG 文本检索常见 cosine distance:

ORDER BY embedding <=> :query_embedding  

如果想展示 cosine similarity,可以换算:

SELECT  
 1 - (embedding <=> :query_embedding) AS cosine_similarityFROM vector_demo.item;  

注意:排序检索时更建议直接按距离运算符排序,这样查询优化器更容易使用对应向量索引。

15.5 维度必须一致

如果列定义为:

embedding vector(1024)  

就只能保存 1024 维向量。传入 1536 维或其他维度会失败。

确定维度的正确步骤:

1. 选择 Embedding 模型  
2. 查看模型输出维度配置  
3. 实际调用一次并验证数组长度  
4. 建立 vector(n) 字段  
5. 记录 embedding_model 和 embedding_dimension  

更换 Embedding 模型时,即使维度碰巧相同,也通常要重新生成全部向量。不同模型产生的向量不在同一个语义空间中,不能直接混合比较。

还要区分“字段能够保存”和“近似索引能够索引”。当前 pgvector 的 HNSW / IVFFlat 对普通 vector 索引最多支持 2000 维,halfvec 索引最多支持 4000 维。若模型维度更高,需要评估 half precision、降维、子向量索引或其他检索方案,不能只把 vector(n) 中的数字改大就结束。


16. 一套完整、可扩展的 RAG 数据模型

下面使用 vector(1024) 作为教学示例。执行前必须确认你的 Embedding 模型是否输出 1024 维;不一致就统一修改字段和索引设计。

16.1 初始化扩展和 schema

CREATE EXTENSION IF NOT EXISTS pgcrypto;  
CREATE EXTENSION IF NOT EXISTS vector;  
  
CREATE SCHEMA IF NOT EXISTS rag;  

16.2 知识库表

CREATE TABLE rag.knowledge_base (  
 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id varchar(64) NOT NULL, name varchar(255) NOT NULL, description text, embedding_model varchar(128) NOT NULL, embedding_dimension integer NOT NULL CHECK (embedding_dimension > 0), enabled boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT knowledge_base_tenant_name_uk UNIQUE (tenant_id, name));  

为什么记录 Embedding 模型和维度:

防止同一知识库混入不同模型的向量  
便于排查向量维度错误  
更换模型时知道哪些知识库需要重建向量  

数据库列 vector(1024) 仍是固定的,所以第一版建议整个服务统一使用一种向量维度。如果未来要同时支持多维度模型,可以拆表、分 schema 或使用不同向量集合,不要在同一索引列中随意混用。

16.3 文档表

CREATE TABLE rag.document (  
 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), knowledge_base_id uuid NOT NULL, original_name varchar(512) NOT NULL, storage_path varchar(1024) NOT NULL, content_type varchar(128), file_size bigint NOT NULL CHECK (file_size >= 0), sha256 char(64), status varchar(32) NOT NULL DEFAULT 'UPLOADED', error_message text, version integer NOT NULL DEFAULT 1 CHECK (version > 0), metadata jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT document_knowledge_base_fk FOREIGN KEY (knowledge_base_id) REFERENCES rag.knowledge_base(id) ON DELETE CASCADE, CONSTRAINT document_status_ck CHECK (status IN ( 'UPLOADED', 'PARSING', 'PARSED', 'CLEANED', 'CHUNKED', 'EMBEDDING', 'EMBEDDED', 'FAILED' )));  

文档表保存文件级元数据,不建议把 PDF 或 Word 的完整二进制直接塞进该表。第一版可以把原文件放本地文件系统,正式环境通常放 MinIO、OSS 或 S3。

16.4 解析文本表

CREATE TABLE rag.document_text (  
 document_id uuid PRIMARY KEY, raw_text text NOT NULL, cleaned_text text, parser_name varchar(128), parser_version varchar(64), metadata jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT document_text_document_fk FOREIGN KEY (document_id) REFERENCES rag.document(id) ON DELETE CASCADE);  

保留 raw_text 和 cleaned_text 很重要。回答效果不好时,可以逐层检查:

原文件是否正确解析  
清洗是否误删正文  
切片是否破坏语义  
Embedding 是否成功  
检索是否找到正确片段  
Prompt 是否正确引用片段  

16.5 Chunk 表

CREATE TABLE rag.chunk (  
 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), document_id uuid NOT NULL, chunk_index integer NOT NULL CHECK (chunk_index >= 0), content text NOT NULL, title_path text, page_start integer, page_end integer, char_start integer, char_end integer, token_count integer CHECK (token_count IS NULL OR token_count >= 0), metadata jsonb NOT NULL DEFAULT '{}'::jsonb, embedding vector(1024), embedding_model varchar(128), embedded_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT chunk_document_fk FOREIGN KEY (document_id) REFERENCES rag.document(id) ON DELETE CASCADE, CONSTRAINT chunk_document_index_uk UNIQUE (document_id, chunk_index), CONSTRAINT chunk_page_range_ck CHECK ( page_start IS NULL OR page_end IS NULL OR page_end >= page_start ), CONSTRAINT chunk_char_range_ck CHECK ( char_start IS NULL OR char_end IS NULL OR char_end >= char_start ));  

字段说明:

字段 含义
chunk_index 片段在当前文档中的顺序,从 0 或 1 开始必须统一
content 实际参与 Embedding 和上下文拼接的文本
title_path 如“用户手册 > 文件上传 > 大小限制”
page_start/page_end 来源页码,用于答案引用
char_start/char_end 在清洗后全文中的字符位置
token_count 估算 token 数,用于控制上下文长度
metadata parser、语言、页标签等扩展信息
embedding Embedding 模型返回的向量
embedding_model 生成该向量的模型名
embedded_at 向量生成时间

16.6 入库任务表

CREATE TABLE rag.ingest_task (  
 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), document_id uuid NOT NULL, task_type varchar(32) NOT NULL, status varchar(32) NOT NULL DEFAULT 'PENDING', attempt_count integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0), max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts > 0), progress integer NOT NULL DEFAULT 0 CHECK (progress BETWEEN 0 AND 100), error_message text, started_at timestamptz, finished_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT ingest_task_document_fk FOREIGN KEY (document_id) REFERENCES rag.document(id) ON DELETE CASCADE, CONSTRAINT ingest_task_type_ck CHECK (task_type IN ('PARSE', 'CLEAN', 'CHUNK', 'EMBED')), CONSTRAINT ingest_task_status_ck CHECK (status IN ('PENDING', 'RUNNING', 'SUCCESS', 'FAILED', 'CANCELLED')));·  

任务表让系统能够支持:

异步处理  
进度查询  
失败重试  
错误追踪  
防止重复执行  
统计每个阶段耗时  

第一版同步执行也可以,但建议至少保存文档状态和错误信息。


17. RAG 表的普通索引设计

17.1 文档查询索引

CREATE INDEX document_kb_status_created_idx  
ON rag.document (knowledge_base_id, status, created_at DESC);  

17.2 文件 hash 去重索引

如果同一知识库内不允许重复文件:

CREATE UNIQUE INDEX document_kb_sha256_uk  
ON rag.document (knowledge_base_id, sha256)  
WHERE sha256 IS NOT NULL;  

这是部分唯一索引:只有 sha256 非空的数据参与唯一性检查。

17.3 Chunk 外键查询索引

PostgreSQL 不会因为创建外键就自动为外键列建立查询索引。经常按 document_id 查询 chunk 时应显式建立:

CREATE INDEX chunk_document_id_idx  
ON rag.chunk (document_id, chunk_index);  

17.4 Metadata GIN 索引

只有确认经常按 metadata 查询时再建立:

CREATE INDEX chunk_metadata_gin_idx  
ON rag.chunk  
USING gin (metadata);  

17.5 待执行任务索引

CREATE INDEX ingest_task_pending_idx  
ON rag.ingest_task (created_at)  
WHERE status = 'PENDING';  

后台 worker 查待处理任务时可以利用这个较小的部分索引。


18. 文档入库的数据操作顺序

完整入库链路:

graph TD;  
 A["上传文件"] --> B["插入 document"]; B --> C["保存原文件"];  
 C --> D["解析 raw_text"]; D --> E["生成 cleaned_text"]; E --> F["切分 chunk"]; F --> G["调用 Embedding 模型"];  
 G --> H["更新 chunk.embedding"]; H --> I["document.status = EMBEDDED"];  

18.1 创建知识库

INSERT INTO rag.knowledge_base (  
 tenant_id, name, description, embedding_model, embedding_dimension)  
VALUES (  
 'tenant-demo', 'AI Agent 学习资料',  
 '保存 AI Agent 学习文档',  
 'your-embedding-model', 1024)  
RETURNING id;  

18.2 创建文档记录

INSERT INTO rag.document (  
 knowledge_base_id, original_name, storage_path, content_type, file_size, sha256)  
VALUES (  
 :knowledge_base_id, 'postgresql-guide.pdf', '/data/uploads/postgresql-guide.pdf', 'application/pdf', 204800, :sha256)  
RETURNING id, status, created_at;  

:knowledge_base_id 和 :sha256 是参数占位表示,不是可以直接在 psql 中运行的字面值。实际项目通过 JDBC、MyBatis 或框架绑定参数。

18.3 保存解析文本

INSERT INTO rag.document_text (  
 document_id, raw_text, cleaned_text, parser_name, parser_version)  
VALUES (  
 :document_id, :raw_text, :cleaned_text, 'pdf-parser', '1.0.0')  
ON CONFLICT (document_id)  
DO UPDATE SET  
 raw_text = EXCLUDED.raw_text, cleaned_text = EXCLUDED.cleaned_text, parser_name = EXCLUDED.parser_name, parser_version = EXCLUDED.parser_version, updated_at = now();  

18.4 批量插入 Chunk

真实项目应使用 JDBC batch、MyBatis batch 或 COPY,不要逐条远程调用数据库。

示意:

INSERT INTO rag.chunk (  
 document_id, chunk_index, content, title_path, page_start, page_end, token_count, metadata)  
VALUES  
 (:document_id, 0, :content_0, :title_0, 1, 1, 420, :metadata_0), (:document_id, 1, :content_1, :title_1, 1, 2, 390, :metadata_1);  

18.5 更新 Embedding

UPDATE rag.chunk  
SET  
 embedding = :embedding, embedding_model = :embedding_model, embedded_at = now(), updated_at = now()WHERE id = :chunk_id;  

所有 chunk 完成后更新文档:

UPDATE rag.document  
SET  
 status = 'EMBEDDED', error_message = NULL, updated_at = now()WHERE id = :document_id;  

18.6 是否把整个流程放在一个事务中

不建议让“解析 PDF + 调用 Embedding API + 写数据库”一直占用一个长事务。

更合理的是按阶段提交:

上传完成:提交 document解析完成:提交 document_text 和状态  
切片完成:批量提交 chunk 和状态  
向量化完成:批量更新 embedding 和最终状态  

每个阶段通过状态和任务表保证可重试、可追踪。外部 API 调用期间不要长时间占用数据库事务和连接。


19. 从问题到答案的向量检索

19.1 检索流程

  
用户问题  
  -> 使用与文档相同的 Embedding 模型生成 query vector  
  -> 按租户、知识库、权限和状态过滤  
  -> 按向量距离排序  
  -> 取 topK chunk  
  -> 拼接上下文和来源  
  -> 调用 qwen3.5-flash  
  -> 返回答案和引用来源  

文档向量和问题向量必须来自相同 Embedding 模型配置。

19.2 最小 topK SQL

SELECT  
 c.id, c.document_id, c.content, c.title_path, c.page_start, c.page_end, c.metadata, c.embedding <=> :query_embedding AS distanceFROM rag.chunk c  
WHERE c.embedding IS NOT NULL  
ORDER BY c.embedding <=> :query_embedding  
LIMIT 5;  

19.3 带知识库和文档状态过滤

SELECT  
 c.id, c.document_id, d.original_name, c.content, c.title_path, c.page_start, c.page_end, c.embedding <=> :query_embedding AS distanceFROM rag.chunk c  
JOIN rag.document d  
 ON d.id = c.document_idJOIN rag.knowledge_base kb  
 ON kb.id = d.knowledge_base_idWHERE kb.tenant_id = :tenant_id  
 AND kb.id = :knowledge_base_id AND kb.enabled = true AND d.status = 'EMBEDDED' AND c.embedding IS NOT NULLORDER BY c.embedding <=> :query_embedding  
LIMIT :top_k;  

这段 SQL 展示了 pgvector 对 RAG 的核心价值:

普通 SQL JOIN 和权限过滤  
加上向量相似度排序  
在同一条查询中完成  

19.4 是否设置相似度阈值

可以过滤距离过大的结果:

AND c.embedding <=> :query_embedding < :max_distance  

但阈值不能照抄固定值。它受到以下因素影响:

Embedding 模型  
距离度量  
文档领域  
chunk 长度  
问题类型  
是否经过 rerank  

应通过真实评测集确定阈值。没有评测数据时,先返回 topK 并记录距离分布,再逐步调优。

19.5 参数绑定

不要把向量字符串直接拼进 SQL,应该使用参数绑定,避免 SQL 注入和格式错误。

应用层常见流程:

  
List<Float> queryEmbedding  
  -> PostgreSQL JDBC / Spring AI VectorStore  
  -> 参数化查询  

如果使用 Spring AI 的 PGvector Vector Store,可以由框架处理一部分建表、向量写入和查询逻辑;但仍应理解底层 schema、维度、索引和过滤条件。


20. 精确检索、HNSW 和 IVFFlat

20.1 不建向量索引也能查询

下面的 SQL 可以进行精确检索:

ORDER BY embedding <=> :query_embedding  
LIMIT 5  

数据量小时,先不建向量索引最容易验证正确性。精确检索会计算更多数据,结果召回准确,但数据量大后速度会下降。

20.2 HNSW

Cosine distance 的 HNSW 索引:

CREATE INDEX chunk_embedding_hnsw_idx  
ON rag.chunk  
USING hnsw (embedding vector_cosine_ops)  
WHERE embedding IS NOT NULL;  

特点可以先理解为:

近似最近邻检索  
通常查询性能和召回率较好  
构建索引需要更多时间和内存  
索引本身占用额外空间  

查询必须使用匹配的距离运算符:

ORDER BY embedding <=> :query_embedding  
LIMIT 5  

如果索引使用 vector_cosine_ops,查询却使用不匹配的运算符,可能无法按预期使用该索引。

20.3 IVFFlat

示例:

CREATE INDEX chunk_embedding_ivfflat_idx  
ON rag.chunk  
USING ivfflat (embedding vector_cosine_ops)  
WITH (lists = 100)  
WHERE embedding IS NOT NULL;  

IVFFlat 的索引效果依赖数据量、lists 和查询时的 probes 等参数。通常应在表中已有一定数据后创建,并结合评测调参。

初学阶段推荐顺序:

1. 不建向量索引,先验证召回结果正确  
2. 准备真实数据和问题评测集  
3. 数据量和延迟确实需要时增加 HNSW4. 用 EXPLAIN ANALYZE、延迟和召回率共同评估  

20.4 为什么近似索引可能少找结果

向量近似索引为了速度,不一定扫描所有候选项。如果 SQL 还带严格 metadata 过滤,可能先从向量索引找到一批候选,再因过滤条件丢掉一部分,最终返回数量不足。

需要综合调整:

索引查询参数  
topK 候选数量  
业务过滤列索引  
查询结构  
数据分区方式  
是否按租户或知识库拆分数据  

不要只看到“建了 HNSW”就认为检索一定又快又准。


21. 混合检索:向量检索并不是全部

向量检索擅长语义相似,但对下面内容可能不够稳定:

产品编号  
错误码  
人名  
精确版本号  
短关键词  
罕见专有名词  

真实 RAG 经常组合:

向量检索  
关键词或全文检索  
metadata 过滤  
rerank  

PostgreSQL 有内置全文检索能力,但中文分词需要额外评估。默认配置不能简单等同于成熟的中文搜索方案。

中文项目可以根据规模考虑:

应用层分词后保存 tokenPostgreSQL 中文分词扩展  
Elasticsearch / OpenSearch  
专业检索服务  
向量召回后使用 reranker  

第一版先完成向量检索和结构化过滤;有明确评测结果后再加入混合检索,不要一次堆满所有组件。


22. PostgreSQL 日常运维基础

22.1 查看连接

SELECT  
 pid, usename, datname, client_addr, state, query_start, queryFROM pg_stat_activity  
WHERE datname = current_database()  
ORDER BY query_start;  

22.2 查看数据库大小

SELECT pg_size_pretty(pg_database_size(current_database()));  

查看表总大小:

SELECT pg_size_pretty(pg_total_relation_size('rag.chunk'));  

22.3 更新统计信息

ANALYZE rag.chunk;  

大量导入数据后,更新统计信息有助于查询优化器选择计划。

22.4 VACUUM

普通维护:

VACUUM (ANALYZE) rag.chunk;  

不要把 VACUUM FULL 当作日常命令。它会重写表并需要更强锁,只有理解影响后才使用。

22.5 备份

自定义格式备份:

docker exec ai-rag-postgres pg_dump -U ai -d ai_rag -Fc -f /tmp/ai_rag.dump```  
  
把文件复制到宿主机:  
  
```bash  
docker cp ai-rag-postgres:/tmp/ai_rag.dump ./ai_rag.dump```  
  
恢复前应先准备目标 database,并使用 `pg_restore`:  
  
```bash  
pg_restore -h 127.0.0.1 -U ai -d ai_rag_restore ai_rag.dump```  
  
备份是否有效必须通过恢复演练验证。只有备份文件、从未测试恢复,不算完整备份方案。  
  
---  
  
## 23. 常见报错和排查方式  
  
### 23.1 connection refused  
  
检查:  
  
```text  
容器是否启动  
端口是否映射为 5432本机 5432 是否被其他 PostgreSQL 占用  
Host 是否正确  
防火墙或网络是否阻断  

命令:

docker compose psdocker compose logs postgres```  
  
### 23.2 password authentication failed  
  
检查用户名和密码。  
  
特别注意:官方镜像的初始化环境变量只在数据目录第一次创建时生效。如果已经存在 volume,后来修改 `POSTGRES_PASSWORD` 不会自动修改数据库里的密码。  
  
学习环境确实不需要原数据时,可以删除 volume 后重建;有数据时应登录数据库执行安全的密码修改,而不是删除 volume。  
  
### 23.3 database does not exist  
  
PostgreSQL 连接需要明确 database:  
  
```text  
jdbc:postgresql://127.0.0.1:5432/ai_rag  

确认:

\l  

23.4 relation does not exist

relation 可能指表、索引、序列等对象。

常见原因:

连接了错误的 databaseschema 不在 search_path表名大小写被双引号固定  
迁移脚本没有执行  
当前账号没有权限  

检查:

SELECT current_database(), current_schema();  
SHOW search_path;  

并尝试完整表名:

SELECT * FROM rag.document;  

23.5 type vector does not exist

当前 database 尚未启用 pgvector:

CREATE EXTENSION IF NOT EXISTS vector;  

如果 CREATE EXTENSION 也失败,需要确认服务器是否安装了 pgvector。使用 pgvector/pgvector Docker 镜像可以减少这个问题。

23.6 expected N dimensions, not M

数据库列定义维度和传入向量长度不一致。

检查:

Embedding API 实际返回数组长度  
数据库 vector(n) 定义  
应用是否使用了错误的 Embedding 模型  
是否把聊天模型配置误当成 Embedding 模型配置  

23.7 duplicate key value violates unique constraint

违反唯一约束。错误信息通常会给出约束名称。

不要直接删唯一索引,应先确认:

是业务重复数据  
是接口重复提交  
是重试没有幂等设计  
还是唯一约束设计不合理  

可以使用业务唯一键和 ON CONFLICT 实现幂等写入。

23.8 current transaction is aborted

事务内已有 SQL 失败,需要先:

ROLLBACK;  

再修复原始错误并重新执行。

23.9 查询没有使用向量索引

检查:

距离运算符是否与 operator class 匹配  
是否包含 ORDER BY distance LIMIT topK表数据是否太少,优化器认为顺序扫描更便宜  
统计信息是否更新  
表达式是否被额外包装导致无法使用索引  
部分索引的 WHERE 条件是否满足  

使用:

EXPLAIN (ANALYZE, BUFFERS)  
SELECT ...  
ORDER BY embedding <=> :query_embedding  
LIMIT 5;  

不要为了强迫使用索引而随意修改数据库全局参数,先理解优化器选择顺序扫描的原因。


24. PostgreSQL + pgvector 的适用边界

24.1 适合继续使用 pgvector 的情况

团队熟悉 SQL 和 PostgreSQL向量数据和业务过滤关系紧密  
希望事务内同时维护业务数据和向量  
项目处于学习、MVP 或中等规模阶段  
当前性能经过测试能够满足需求  
不希望额外维护一套专业向量数据库  

24.2 评估专业向量数据库的信号

向量规模持续增长并出现明确性能瓶颈  
高并发检索无法达到延迟目标  
需要独立扩缩容向量检索层  
需要更成熟的分片、副本和集群能力  
需要专业向量数据库提供的特定检索能力  
团队能够承担额外组件的运维成本  

可评估:

Milvus  
Qdrant  
Weaviate  
Pinecone  
Elasticsearch / OpenSearch 向量检索  

选型不能只看“多少条向量”这个单一数字,还要结合:

向量维度  
索引类型  
查询并发  
topK  
过滤条件  
召回率要求  
延迟目标  
硬件  
备份与高可用要求  
团队经验  

25. 推荐学习和实操顺序

不要直接跳到 HNSW。按照下面顺序完成更稳妥。

阶段一:PostgreSQL 基础

启动 Docker 容器  
使用 psql 连接  
理解 database 和 schema创建 app_user 表  
完成 CRUD 和 ON CONFLICT练习 identity、boolean、timestamptz、jsonb  
查看索引和 EXPLAIN  

阶段二:Java 接入

Spring Boot 连接 PostgreSQL用 JdbcTemplate 或 MyBatis 完成 CRUD正确映射 UUID 和 timestamptz使用 Flyway 管理建表脚本  
使用环境变量保存密码  

阶段三:pgvector

启用 vector 扩展  
完成 vector(3) 教学示例  
理解 cosine distance确认 Embedding 模型和输出维度  
保存一批真实 chunk 向量  
完成精确 topK 查询  

阶段四:完整 RAG 数据链路

创建知识库  
上传文档  
保存解析和清洗文本  
切片并保存 chunk生成并保存 Embedding按知识库过滤并检索 topK把 chunk 拼入 Prompt调用 qwen3.5-flash 生成回答  
返回答案、文件名和页码  

阶段五:性能和质量

准备真实问题评测集  
观察正确 chunk 是否进入 topK调整 chunk 策略和 topK增加 HNSW 并比较延迟和召回  
评估 metadata 过滤和混合检索  
增加 rerank  

26. 必做练习

练习一:MySQL DDL 改写

把下面 MySQL 表改写成 PostgreSQL:

CREATE TABLE knowledge_file (  
 id bigint unsigned NOT NULL AUTO_INCREMENT, file_name varchar(255) NOT NULL, file_size bigint unsigned NOT NULL, enabled tinyint(1) NOT NULL DEFAULT 1, extra_json json, created_at datetime NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  

参考答案:

CREATE TABLE rag.knowledge_file (  
 id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, file_name varchar(255) NOT NULL, file_size bigint NOT NULL CHECK (file_size >= 0), enabled boolean NOT NULL DEFAULT true, extra_json jsonb NOT NULL DEFAULT '{}'::jsonb, created_at timestamptz NOT NULL DEFAULT now());  

练习二:JSONB

完成:

插入三条带不同 metadata 的 chunk查询 language = zh-CN 的数据  
查询 metadata 包含 parser=markdown 的数据  
建立 GIN 索引  
使用 EXPLAIN 查看查询计划  

练习三:事务

完成:

开启事务  
插入一条 document插入两条 chunk故意制造唯一键冲突  
观察事务失败状态  
执行 ROLLBACK确认数据未提交  

练习四:向量检索

完成:

创建 vector_demo.item插入三维向量  
分别使用 L2 和 cosine distance 查询  
解释为什么距离越小越相似  
把 similarity 写成 1 - cosine_distance  

练习五:RAG 完整链路

完成:

创建一个知识库  
创建一份文档  
保存 raw_text 和 cleaned_text生成至少 10 个 chunk调用 Embedding 模型生成向量  
保存 embedding_model 和 embedded_at使用真实问题检索 top5检查正确片段是否出现  
把 top5 交给 qwen3.5-flash 回答  
返回引用文档和页码  

27. 常用命令速查

Docker

docker compose up -ddocker compose psdocker compose logs -f postgresdocker compose downdocker exec -it ai-rag-postgres psql -U ai -d ai_rag```  
  
### psql  
  
```text  
\l  
\c ai_rag  
\conninfo  
\dn  
\dt rag.*  
\d rag.document  
\di rag.*  
\du  
\dx  
\timing  
\q  

基本检查

SELECT version();  
SELECT current_database(), current_user, current_schema();  
SHOW search_path;  
SHOW timezone;  

pgvector

CREATE EXTENSION IF NOT EXISTS vector;  
  
SELECT  
 content, embedding <=> :query_embedding AS distanceFROM rag.chunk  
WHERE embedding IS NOT NULL  
ORDER BY embedding <=> :query_embedding  
LIMIT 5;  

28. 最终总结

对于会 MySQL 的开发者,学习 PostgreSQL 最关键的不是背更多 SQL,而是建立这些新的认知:

PostgreSQL 的 database 下还有 schema账号和权限统一围绕 role 管理  
标识符默认折叠成小写,不要滥用双引号和大小写混合表名  
新表自增优先使用 identity业务时间点优先理解并使用 timestamptzJSONB 很强,但不能替代正常关系模型  
外键列不会自动得到普通查询索引  
事务失败后需要 ROLLBACKsequence 不保证连续  
索引必须根据真实 SQL 和执行计划设计  

进入 AI / RAG 后,还要继续分清:

PostgreSQL 保存和管理数据  
pgvector 保存向量并完成距离检索  
Embedding 模型把文本转换成向量  
qwen3.5-flash 根据检索上下文生成答案  

第一版推荐架构:

PostgreSQL + pgvector:文档、文本、chunk、metadata、任务和向量  
文件系统 / MinIO / OSS:原始文件  
Java / Python:解析、切片、Embedding 和流程编排  
qwen3.5-flash:最终答案生成  

先用精确检索跑通正确链路,再根据数据量、延迟和召回评测增加 HNSW、混合检索、rerank 或专业向量数据库。这样学到的不只是某个框架的配置,而是一套可以迁移到真实项目的数据库和 RAG 工程能力。

参考资料

  • PostgreSQL 官方文档
  • PostgreSQL SQL 语言文档
  • PostgreSQL 数据类型
  • PostgreSQL Schema
  • PostgreSQL Role 和权限
  • PostgreSQL 并发控制
  • pgvector GitHub
  • Spring AI PGvector Vector Store
  • Spring AI Vector Databases
AI自学路线
PostgreSQL
许可协议: 
分享

相关文章

7月 13, 2026

55 异常兜底:工具失败、非法输出、空检索与超时

异常兜底:工具失败、非法输出、空检索与超时 Agent 的可靠性主要体现在失败时。工ࠥ

7月 13, 2026

54 Agent Memory:短期记忆、用户上下文与摘要

Agent Memory:短期记忆、用户上下文与摘要 Memory 的目标是保持任务连续性,不是永久保存所有聊天。记忆Š

7月 13, 2026

53 Human-in-the-loop:让关键动作暂停、确认与恢复

Human-in-the-loop:让关键动作暂停、确认与恢复 HITL 不是弹一个“确定吗”,而是让工作流在关键动作前持久

下一篇

17 文档上传

上一篇

16 数据库准备

最近更新

  • 55 异常兜底:工具失败、非法输出、空检索与超时
  • 54 Agent Memory:短期记忆、用户上下文与摘要
  • 53 Human-in-the-loop:让关键动作暂停、确认与恢复
  • 52 工单 Tool:字段校验、创建结果与失败补偿
  • 51 订单 Tool:Mock、鉴权、状态解析与脱敏

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy