<aside> ✅
唯一事实源:全教程统一采用 PostgreSQL 18.4 + pgvector 0.8.2。以下镜像、表结构和种子数据替代各章零散的初始化片段。
</aside>
| 组件 | 教程基线 | 兼容说明 |
|---|---|---|
| PostgreSQL | 18.4 | 18 是当前主线;15–17 的差异会在正文标注 |
| pgvector | 0.8.2 | 启用迭代扫描;教程向量维度为 384 |
| Docker | Compose v2 | 使用命名卷保存数据 |
| 客户端 | psql 18 | 尽量让客户端主版本不低于服务端 |
pg-lab/
├── compose.yaml
├── secrets/postgres_password.txt
└── sql/
├── 00_setup.sql
├── 01_seed.sql
└── 99_reset.sql
services:
postgres:
image: pgvector/pgvector:0.8.2-pg18
container_name: pg-lab
environment:
POSTGRES_DB: pg_lab
POSTGRES_USER: postgres
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
secrets:
- postgres_password
ports:
- "5433:5432"
volumes:
- pg18_data:/var/lib/postgresql/data
- ./sql:/docker-entrypoint-initdb.d:ro
command:
- postgres
- -c
- shared_preload_libraries=pg_stat_statements
- -c
- compute_query_id=on
- -c
- track_io_timing=on
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d pg_lab"]
interval: 5s
timeout: 3s
retries: 20
secrets:
postgres_password:
file: ./secrets/postgres_password.txt
volumes:
pg18_data:
<aside> 🔐
实验密码文件不得提交到 Git。生产环境应使用密钥管理服务、TLS、最小权限和密码轮换;不要暴露 5432/5433 到公网。
</aside>
\set ON_ERROR_STOP on
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
CREATE SCHEMA IF NOT EXISTS sales;
CREATE SCHEMA IF NOT EXISTS kb;
CREATE TABLE IF NOT EXISTS sales.customer (
customer_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE,
tenant_id bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS sales.product (
product_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
price numeric(12,2) NOT NULL CHECK (price >= 0),
attrs jsonb NOT NULL DEFAULT '{}'::jsonb,
tags text[] NOT NULL DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS sales.orders (
order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES sales.customer,
status text NOT NULL CHECK (status IN ('pending','paid','cancelled')),
ordered_at timestamptz NOT NULL DEFAULT now(),
total_amount numeric(14,2) NOT NULL DEFAULT 0 CHECK (total_amount >= 0)
);
CREATE TABLE IF NOT EXISTS sales.order_item (
order_id bigint NOT NULL REFERENCES sales.orders ON DELETE CASCADE,
line_no integer NOT NULL CHECK (line_no > 0),
product_id bigint NOT NULL REFERENCES sales.product,
quantity integer NOT NULL CHECK (quantity > 0),
unit_price numeric(12,2) NOT NULL CHECK (unit_price >= 0),
PRIMARY KEY (order_id, line_no)
);
CREATE TABLE IF NOT EXISTS kb.document (
document_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id bigint NOT NULL,
title text NOT NULL,
source_uri text,
model_version text NOT NULL DEFAULT 'deterministic-384-v1',
content_hash text NOT NULL,
status text NOT NULL DEFAULT 'active'
CHECK (status IN ('active','deleted')),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (tenant_id, content_hash)
);
CREATE TABLE IF NOT EXISTS kb.chunk (
chunk_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
document_id bigint NOT NULL
REFERENCES kb.document(document_id) ON DELETE CASCADE,
chunk_no integer NOT NULL,
content text NOT NULL,
embedding vector(384) NOT NULL,
search_vector tsvector GENERATED ALWAYS AS
(to_tsvector('simple', content)) STORED,
UNIQUE (document_id, chunk_no)
);
CREATE INDEX IF NOT EXISTS idx_orders_customer_time
ON sales.orders (customer_id, ordered_at DESC);
CREATE INDEX IF NOT EXISTS idx_product_attrs_gin
ON sales.product USING gin (attrs);
CREATE INDEX IF NOT EXISTS idx_chunk_fts
ON kb.chunk USING gin (search_vector);
\set ON_ERROR_STOP on
INSERT INTO sales.customer(name,email,tenant_id) VALUES
('Alice','[email protected]',1),
('Bob','[email protected]',2)
ON CONFLICT (email) DO NOTHING;
INSERT INTO sales.product(sku,name,price,attrs,tags) VALUES
('PG-BOOK','PostgreSQL 实战',99.00,'{"level":"advanced"}','{database,postgresql}'),
('AI-BOOK','向量检索实战',129.00,'{"level":"advanced"}','{ai,vector}')
ON CONFLICT (sku) DO NOTHING;
WITH c AS (
SELECT customer_id FROM sales.customer WHERE email='[email protected]'
)
INSERT INTO sales.orders(customer_id,status,total_amount)
SELECT customer_id,'paid',228.00 FROM c
WHERE NOT EXISTS (
SELECT 1 FROM sales.orders o
WHERE o.customer_id=c.customer_id
AND o.status='paid' AND o.total_amount=228.00
);
WITH target_order AS (
SELECT min(o.order_id) AS order_id
FROM sales.orders o
JOIN sales.customer c USING(customer_id)
WHERE c.email='[email protected]'
AND o.status='paid' AND o.total_amount=228.00
), lines(line_no,sku) AS (
VALUES (1,'PG-BOOK'),(2,'AI-BOOK')
)
INSERT INTO sales.order_item(order_id,line_no,product_id,quantity,unit_price)
SELECT o.order_id,l.line_no,p.product_id,1,p.price
FROM target_order o CROSS JOIN lines l
JOIN sales.product p ON p.sku=l.sku
ON CONFLICT DO NOTHING;
INSERT INTO kb.document(tenant_id,title,source_uri,content_hash)
VALUES (1,'PostgreSQL 18 实验手册','local://pg18-lab','sha256:pg18-lab-v1')
ON CONFLICT (tenant_id,content_hash) DO NOTHING;
WITH d AS (
SELECT document_id FROM kb.document
WHERE tenant_id=1 AND content_hash='sha256:pg18-lab-v1'
)
INSERT INTO kb.chunk(document_id,chunk_no,content,embedding)
SELECT d.document_id, x.chunk_no, x.content,
ARRAY(
SELECT CASE WHEN i=x.hot_dim THEN 1.0 ELSE 0.0 END
FROM generate_series(1,384) AS i
)::vector
FROM d
CROSS JOIN (VALUES
(1,'PostgreSQL 使用 MVCC 提供一致性读',1),
(2,'HNSW 适合低延迟近似向量检索',2),
(3,'WAL 支持崩溃恢复与物理复制',3)
) AS x(chunk_no,content,hot_dim)
ON CONFLICT (document_id,chunk_no) DO NOTHING;
docker compose up -d
docker compose ps
docker compose exec postgres psql -U postgres -d pg_lab -c "SELECT version(), extversion FROM pg_extension WHERE extname='vector';"
# 保留数据停止
docker compose stop
docker compose start
# 删除容器但保留命名卷
docker compose down
# 彻底删除实验数据;不可恢复
docker compose down -v
\set ON_ERROR_STOP on
DROP SCHEMA IF EXISTS sales CASCADE;
DROP SCHEMA IF EXISTS kb CASCADE;
\ir 00_setup.sql
\ir 01_seed.sql
运行:
docker compose exec -T postgres psql -U postgres -d pg_lab -f /docker-entrypoint-initdb.d/99_reset.sql