PostgreSQL 常被称为"最先进的开源关系型数据库",它的优势不在速度,而在功能完整和可扩展性:
JSONB、数组、范围类型、多种索引方法、对 SQL 标准的严格遵守。它和 MySQL 不是替代关系而是取舍关系——PG 更"正统",MySQL 更"轻快"。本文从安装认证讲到查询进阶、索引选型、事务并发与备份复制。psql命令行工具见 PostgreSQL 命令行教程。

一、PostgreSQL 是什么
PostgreSQL(简称 Postgres 或 PG)是一个对象关系型数据库,核心特点:
- 严格遵循 SQL 标准:几乎不用"方言"就能写可移植的 SQL
- 可扩展:能自定义数据类型、函数、操作符,甚至索引方法
- 完整的事务支持(ACID)+ MVCC 多版本并发控制
- 类型系统丰富:JSONB、数组、范围、网络地址、几何类型都是内置的
- 扩展生态:PostGIS(地理空间)、TimescaleDB(时序)、pgvector(向量检索)、pg_partman(分区管理)
和 MySQL 怎么选:
| 维度 | PostgreSQL | MySQL |
|---|---|---|
| SQL 标准遵守 | 严格 | 有较多方言 |
| 复杂查询能力 | 强(窗口函数、CTE、LATERAL 早支持) | 8.0 才补齐窗口函数/CTE |
| JSON 支持 | JSONB(二进制、可索引) | JSON(较重,5.7+) |
| 并发模型 | MVCC,读写互不阻塞 | InnoDB MVCC |
| 主从复制 | 流复制、逻辑复制 | binlog 复制 |
| 上手难度 | 权限模型/认证配置略复杂 | 相对简单 |
| 典型场景 | 复杂查询、GIS、数据分析 | Web 应用、读多写少 |
一句话:要复杂查询和数据类型自由度选 PG,要生态和"随便搜都有答案"选 MySQL。
graph TD
PG[PostgreSQL] --> Core[核心能力]
PG --> Ext[扩展生态]
Core --> SQL[SQL 标准]
Core --> ACID[ACID 事务]
Core --> MVCC[MVCC]
Core --> Types[JSONB/数组/范围]
Ext --> PostGIS[PostGIS 地理]
Ext --> Timescale[TimescaleDB 时序]
Ext --> Vector[pgvector 向量]二、安装与初始化
# Ubuntu / Debian
sudo apt update && sudo apt install postgresql postgresql-contrib
# CentOS / RHEL
sudo dnf install postgresql-server postgresql-contrib
sudo postgresql-setup --initdb
# macOS
brew install postgresql@16 && brew services start postgresql@16postgresql-contrib 是必装的——pg_stat_statements、pgcrypto、citext 这些常用扩展都在里面。
sudo systemctl enable --now postgresql
sudo -u postgres psql # 用 postgres 系统用户进入PG 的权限模型和 MySQL 不同:安装后有一个 postgres 超级用户(对应系统用户),本地连接默认走 peer 认证(系统用户名 == 数据库用户名才放行),所以要用 sudo -u postgres 切过去。
2.1 创建业务用户和库
CREATE USER app WITH PASSWORD 'StrongPass!2026';
CREATE DATABASE sales_db OWNER app ENCODING 'UTF8';
-- 用户级默认权限(可选,免得到处 GRANT)
ALTER USER app SET search_path TO app_schema, public;建库时
OWNER一定要指定。否则库属主是postgres,app用户连上去会发现什么都干不了——创建 schema、建表都要额外授权。
三、连接认证:pg_hba.conf
这是 PG 新手最容易卡住的地方。PG 的客户端认证由 pg_hba.conf 控制,它是按空格/制表符分列的表格,每行五个字段:
# 类型 数据库 用户 地址 认证方法
local all all peer
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
host sales_db app 10.0.0.0/8 scram-sha-256字段含义:
| 字段 | 说明 |
|---|---|
| 类型 | local(Unix socket)/ host(TCP)/ hostssl(强制 SSL) |
| 数据库 | all / 具体库名 / 逗号分隔的列表 |
| 用户 | all / 具体用户名 |
| 地址 | CIDR 格式(如 10.0.0.0/8),local 行留空 |
| 认证方法 | peer / scram-sha-256 / md5 / trust / cert |
规则匹配是"第一个匹配的行生效",从上往下,所以具体规则要写在通配规则前面。
两个安全提醒:
- 别用
trust。它表示"无条件信任、不验证密码"——任何能连上端口的人都能进来。- 别写
host all all 0.0.0.0/0 md5。这等于对全世界开放。真要远程访问,限定到具体网段(10.0.0.0/8),并优先用scram-sha-256而非md5。另外
pg_hba.conf的列是空格分隔的,不是把字段粘在一起写(写成localallalltrust会直接解析失败)。
改完生效:
SELECT pg_reload_conf(); -- 或 sudo systemctl reload postgresql允许远程连接还要改监听地址:
# postgresql.conf
listen_addresses = '*'四、数据类型
PG 的类型系统是它最大的卖点之一。
| 类别 | 类型 | 说明 |
|---|---|---|
| 整数 | SMALLINT / INTEGER / BIGINT | 2/4/8 字节 |
| 自增 | SERIAL / BIGSERIAL,或 GENERATED ALWAYS AS IDENTITY | 见 §5.1 |
| 精确小数 | NUMERIC(p,s) | 金额用这个 |
| 浮点 | REAL / DOUBLE PRECISION | 有精度误差 |
| 字符 | VARCHAR(n) / TEXT / CHAR(n) | TEXT 无长度上限,性能与 VARCHAR 无差 |
| 时间 | DATE / TIME / TIMESTAMP / TIMESTAMPTZ | 跨时区用 TIMESTAMPTZ |
| 布尔 | BOOLEAN | 真正的 true/false |
| JSON | JSON / JSONB | 优先 JSONB(可索引) |
| 数组 | INT[] / TEXT[] / JSONB[] | 原生支持 |
| 范围 | int4range / tsrange / daterange | 表示区间 |
| 网络 | INET / CIDR / MACADDR | 带校验的地址类型 |
| UUID | UUID | 配合 gen_random_uuid() |
| 几何 | POINT / LINE / POLYGON | 配合 PostGIS |
TIMESTAMP 和 TIMESTAMPTZ 的差别:TIMESTAMP 不带时区(存字面时间),TIMESTAMPTZ 存 UTC、按会话时区显示。有跨时区需求就一律用 TIMESTAMPTZ,否则夏令时/多地域部署时会算错。
关于 VARCHAR(n):PG 里 VARCHAR(n) 和 TEXT 的性能完全一样,n 只起约束作用(超出报错)。约束长度用 VARCHAR(n),不约束就直接 TEXT,不必纠结。
JSON vs JSONB:
JSON存原始文本,保留键顺序和空白,每次查询都要重新解析JSONB存二进制,会去重键、不保留顺序,但可索引、查询快- 除非要保留原始 JSON 格式,否则一律用
JSONB
五、建表与数据操作
5.1 自增主键:SERIAL 还是 IDENTITY
-- 传统写法
CREATE TABLE t_serial (id SERIAL PRIMARY KEY, name TEXT);
-- SQL 标准写法(PG 10+,推荐)
CREATE TABLE t_identity (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT
);SERIAL 是 PG 的历史写法,本质是"建序列 + 设默认值"的语法糖。它有两个问题:序列可能被手工覆盖,且不是 SQL 标准。
GENERATED ALWAYS AS IDENTITY 是标准写法,ALWAYS 会拒绝你手工插入 id(想手工插就用 GENERATED BY DEFAULT AS IDENTITY)。新项目建议用 IDENTITY。
5.2 建表
CREATE TABLE employees (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
department VARCHAR(50),
salary NUMERIC(10,2) CHECK (salary >= 0),
skills TEXT[],
profile JSONB DEFAULT '{}'::jsonb,
hire_date DATE DEFAULT CURRENT_DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);5.3 插入(含 RETURNING 和 UPSERT)
-- 批量插入
INSERT INTO employees (name, email, department, salary) VALUES
('张伟', 'zhang@company.com', '研发部', 15000),
('李娜', 'li@company.com', '市场部', 12000);
-- RETURNING:插入的同时拿回生成的值(PG 特色,省一次查询)
INSERT INTO employees (name, email, salary)
VALUES ('王芳', 'wang@company.com', 13000)
RETURNING id, created_at;
-- UPSERT:冲突时更新
INSERT INTO employees (id, name, salary) VALUES (1, '张伟', 16000)
ON CONFLICT (id) DO UPDATE SET salary = EXCLUDED.salary;
-- 冲突时不做任何事
INSERT INTO employees (id, name, salary) VALUES (1, '张伟', 16000)
ON CONFLICT (id) DO NOTHING;EXCLUDED 代表"本次想插入的那一行",取它的值就能拿到新数据。这是 PG(和 SQLite)的 ON CONFLICT 写法,MySQL 对应的是 ON DUPLICATE KEY UPDATE。
5.4 更新与删除(也支持 RETURNING)
UPDATE employees SET salary = salary * 1.1
WHERE department = '研发部'
RETURNING id, name, salary; -- 直接看到改了什么
DELETE FROM employees WHERE id = 5 RETURNING *;RETURNING 是 PG 和 MySQL 的一个显著体验差异——MySQL 想拿到"更新后的值"得再查一次,PG 一步到位。
六、查询进阶
6.1 窗口函数
SELECT
name, department, salary,
AVG(salary) OVER (PARTITION BY department) AS dept_avg,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
salary - AVG(salary) OVER (PARTITION BY department) AS diff_from_avg
FROM employees;6.2 CTE 与递归
WITH dept_stats AS (
SELECT department, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
GROUP BY department
)
SELECT * FROM dept_stats WHERE cnt > 3 ORDER BY avg_salary DESC;递归遍历层级:
WITH RECURSIVE org AS (
SELECT id, name, manager_id, 1 AS level
FROM employees WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, o.level + 1
FROM employees e JOIN org o ON e.manager_id = o.id
)
SELECT * FROM org ORDER BY level, id;6.3 DISTINCT ON:取每组的第一条
PG 特有的语法,取"每个部门工资最高的那个人"特别方便:
SELECT DISTINCT ON (department) department, name, salary
FROM employees
ORDER BY department, salary DESC;等价于窗口函数 ROW_NUMBER() ... = 1,但更简洁。注意 DISTINCT ON 的 ORDER BY 必须以 DISTINCT ON 的列开头。
6.4 LATERAL:让子查询引用外层
普通 JOIN 的子查询不能引用左边的表,LATERAL 可以:
-- 每个部门取工资最高的 2 个人
SELECT d.name AS dept, e.name, e.salary
FROM departments d
CROSS JOIN LATERAL (
SELECT name, salary FROM employees
WHERE department = d.name
ORDER BY salary DESC LIMIT 2
) e;6.5 generate_series:生成序列
-- 生成日期序列(做报表补全空缺日期特别好用)
SELECT d::date AS day
FROM generate_series('2026-01-01'::date, '2026-01-10'::date, '1 day') d;
-- 生成 1-10
SELECT generate_series(1, 10);6.6 聚合进阶:FILTER 与 string_agg
-- FILTER:对聚合加条件(标准 SQL)
SELECT
department,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE salary > 13000) AS high_paid,
string_agg(name, ', ' ORDER BY salary DESC) AS members,
array_agg(name) AS names_array
FROM employees
GROUP BY department;string_agg 对应 MySQL 的 GROUP_CONCAT。FILTER 比 SUM(CASE WHEN ... THEN 1 END) 清晰得多。
6.7 数组操作
-- 查询包含某元素的数组
SELECT * FROM employees WHERE 'Python' = ANY(skills);
-- 展开数组为多行
SELECT name, unnest(skills) AS skill FROM employees;
-- 匹配数组
SELECT * FROM employees WHERE skills @> ARRAY['Python', 'SQL'];
-- 数组长度
SELECT name, array_length(skills, 1) FROM employees;七、JSONB 实操
JSONB 是 PG 相对 MySQL 的一个明显优势。
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
attributes JSONB
);
INSERT INTO products (name, attributes) VALUES
('Laptop', '{"color": "silver", "memory": "16GB", "ports": ["USB-C", "HDMI"]}'),
('Mouse', '{"color": "black", "wireless": true}');取值操作符:
| 操作符 | 返回类型 | 说明 |
|---|---|---|
-> | JSONB | 取 JSON 元素(键或数组下标) |
->> | TEXT | 取并转为文本 |
#> | JSONB | 按路径取('{a,b}') |
#>> | TEXT | 按路径取并转文本 |
SELECT name,
attributes ->> 'color' AS color, -- 文本
attributes -> 'ports' AS ports_jsonb, -- JSONB
attributes #>> '{ports,0}' AS first_port -- 路径取值
FROM products;条件查询:
-- 包含关系(GIN 索引可以加速)
SELECT * FROM products WHERE attributes @> '{"memory": "16GB"}';
-- 键存在
SELECT * FROM products WHERE attributes ? 'wireless';
-- 嵌套条件
SELECT * FROM products WHERE attributes ->> 'color' = 'silver';GIN 索引(JSONB 字段要建索引才能高效查询):
-- 通用包含查询索引
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);
-- 针对特定路径的索引(更小更快)
CREATE INDEX idx_products_color ON products ((attributes ->> 'color'));修改 JSONB:
UPDATE products
SET attributes = jsonb_set(attributes, '{memory}', '"32GB"')
WHERE name = 'Laptop';八、全文搜索
PG 内置全文搜索,中小规模场景不需要额外引入搜索引擎。
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT,
body TEXT
);
-- 建表达式索引(GIN 是文本搜索的标准选择)
CREATE INDEX idx_documents_fts ON documents
USING GIN (to_tsvector('english', body));
-- 搜索 + 高亮
SELECT title,
ts_headline('english', body, to_tsquery('english', 'database & performance')) AS snippet
FROM documents
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'database & performance');
-- 排序按相关度
SELECT title, ts_rank(to_tsvector('english', body), q) AS rank
FROM documents, to_tsquery('english', 'database') q
WHERE to_tsvector('english', body) @@ q
ORDER BY rank DESC;中文全文搜索:PG 内置分词器不支持中文,需要装 zhparser 或 pg_jieba 扩展,或者退回用 pg_trgm(三元组相似度)做模糊匹配:
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_title_trgm ON documents USING GIN (title gin_trgm_ops);
SELECT * FROM documents WHERE title ILIKE '%数据库%';九、索引
9.1 索引类型
| 类型 | 适用场景 |
|---|---|
| B-tree | 默认,等值 + 范围 + 排序,< <= = >= > |
| Hash | 仅等值查询(=),比 B-tree 小。PG 10+ 支持 WAL,可用于持久表 |
| GIN | 多值类型:JSONB、数组、全文搜索 |
| GiST | 几何、全文、范围类型 |
| SP-GiST | 非平衡结构:点、前缀树(如电话号码前缀) |
| BRIN | 超大表且数据按物理顺序写入(如时间序列) |
关于 Hash 索引:很多老资料说它"只能用于内存表/不安全",那是 PG 9.x 的旧况。PG 10 起 Hash 索引已支持 WAL,可用于常规表。不过实践中 B-tree 覆盖面更广,Hash 用得少。
9.2 表达式索引与部分索引
-- 表达式索引:查询里常用 lower(email) 就索引它
CREATE INDEX idx_users_email_lower ON users (lower(email));
-- 查询要写成同样的表达式才能命中:
SELECT * FROM users WHERE lower(email) = lower('A@B.com');
-- 部分索引:只索引满足条件的行(索引更小)
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending';
-- 覆盖索引:把查询需要的列放进索引,避免回表(PG 11+)
CREATE INDEX idx_orders_cover ON orders (customer_id) INCLUDE (amount, status);9.3 索引失效的常见原因
-- ❌ 对索引列做函数运算
SELECT * FROM orders WHERE date_trunc('day', created_at) = '2026-01-01';
-- ✅ 改写成范围
SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02';
-- ❌ 隐式类型转换(id 是 bigint,传了字符串)
SELECT * FROM orders WHERE id = '123';
-- ✅ 类型对上
SELECT * FROM orders WHERE id = 123;9.4 EXPLAIN 看执行计划
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE customer_id = 42;(ANALYZE, BUFFERS) 是调优标配:ANALYZE 实际执行并给出真实耗时和行数,BUFFERS 显示缓存命中(shared hit 是命中缓存,shared read 是磁盘读)。
看计划时的信号:
Seq Scan出现在大表上 → 全表扫描,考虑索引rows=预估与实际差一个数量级 → 统计信息过期,跑ANALYZE tableNested Loop配大表 → JOIN 方式可能选错shared read很高 → 缓存不足或没走索引
9.5 分区表
-- 声明式分区(PG 10+)
CREATE TABLE sales (
id BIGINT GENERATED ALWAYS AS IDENTITY,
sale_date DATE NOT NULL,
amount NUMERIC(12,2)
) PARTITION BY RANGE (sale_date);
CREATE TABLE sales_2026_q1 PARTITION OF sales
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE sales_2026_q2 PARTITION OF sales
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');分区的好处:查询能"分区裁剪"(只扫相关分区)、可以按分区快速删除历史数据(DROP TABLE sales_2025_q1 比 DELETE 快得多)、索引也可以按分区建。
除了 RANGE,还支持 LIST(按枚举值)和 HASH(散列均匀分布)。
十、事务与并发
10.1 隔离级别
SHOW default_transaction_isolation; -- 默认 read committed
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
READ UNCOMMITTED | PG 中视同 READ COMMITTED | ||
READ COMMITTED(默认) | 否 | 可能 | 可能 |
REPEATABLE READ | 否 | 否 | PG 中不会 |
SERIALIZABLE | 否 | 否 | 否 |
PG 默认是 READ COMMITTED,而 MySQL InnoDB 默认是 REPEATABLE READ。这个差异会让相同的并发代码表现不同——写并发逻辑时要留意。
PG 的 SERIALIZABLE 是真正可串行化(SSI 算法),冲突时会报 could not serialize access due to concurrent update,应用层需要重试。这是它和"用锁实现"的数据库的重要区别。
10.2 锁
-- 行级锁:锁定读到的行
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- FOR UPDATE NOWAIT:拿不到锁立即报错,不等待
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- FOR UPDATE SKIP LOCKED:跳过已被锁的行(做任务队列的经典技巧)
SELECT * FROM jobs WHERE status = 'pending' LIMIT 1 FOR UPDATE SKIP LOCKED;SKIP LOCKED 是 PG 9.5+ 的特性,用数据库做并发任务分发时非常好用——多个 worker 各取各的任务,不会互相阻塞。
表级锁模式(从弱到强):ACCESS SHARE → ROW SHARE → ROW EXCLUSIVE → SHARE UPDATE EXCLUSIVE → SHARE → SHARE ROW EXCLUSIVE → EXCLUSIVE → ACCESS EXCLUSIVE。VACUUM FULL、ALTER TABLE 会取 ACCESS EXCLUSIVE(阻塞一切)。
10.3 MVCC
PG 用 MVCC(多版本并发控制)实现"读不阻塞写、写不阻塞读":
stateDiagram-v2
[*] --> Active: BEGIN
Active --> Committed: COMMIT
Active --> Aborted: ROLLBACK / 出错
Committed --> Visible: 对其他事务可见
Aborted --> Reclaimed: VACUUM 回收
Note right of Active: 更新时不覆盖原行,<br/>而是写入新版本 + 标记旧版本关键机制:更新一行时,PG 不覆盖原行,而是写入新版本、把旧版本标记为过期。所以:
- 读操作能看到"自己事务开始时的快照"(一致性读)
- 旧版本要靠
VACUUM回收,否则表会"膨胀"
这就是 PG 需要 VACUUM 的根本原因——它没有 MySQL 的 undo log 机制,过期版本留在主表里。
十一、函数、过程与触发器
11.1 PL/pgSQL 函数
CREATE OR REPLACE FUNCTION calculate_tax(amount NUMERIC, rate NUMERIC DEFAULT 0.1)
RETURNS NUMERIC AS $$
BEGIN
RETURN ROUND(amount * rate, 2);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
SELECT calculate_tax(1000); -- 100.00
SELECT calculate_tax(1000, 0.06); -- 60.00IMMUTABLE 是性能关键:它告诉优化器"同样输入永远同样输出",这样函数可以被预计算、可用于表达式索引。分三类:
IMMUTABLE:纯函数,无副作用,结果只依赖参数STABLE:同一事务内结果稳定(如读表)VOLATILE(默认):每次调用都可能不同
11.2 返回结果集
CREATE OR REPLACE FUNCTION get_employees(dept VARCHAR)
RETURNS TABLE (id BIGINT, name TEXT, salary NUMERIC) AS $$
BEGIN
RETURN QUERY
SELECT e.id, e.name, e.salary
FROM employees e
WHERE e.department = dept;
END;
$$ LANGUAGE plpgsql STABLE;
SELECT * FROM get_employees('研发部');11.3 存储过程(PG 11+)
函数用 SELECT 调用,过程用 CALL 调用,过程里可以 COMMIT:
CREATE OR REPLACE PROCEDURE raise_salaries(pct NUMERIC)
LANGUAGE plpgsql AS $$
BEGIN
UPDATE employees SET salary = salary * (1 + pct / 100);
COMMIT; -- 过程内可以控制事务
END;
$$;
CALL raise_salaries(10);11.4 触发器
CREATE TABLE audit_log (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
table_name TEXT,
action TEXT,
old_data JSONB,
new_data JSONB,
change_time TIMESTAMPTZ DEFAULT now()
);
CREATE OR REPLACE FUNCTION log_employee_changes()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO audit_log (table_name, action, old_data)
VALUES (TG_TABLE_NAME, 'DELETE', to_jsonb(OLD));
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO audit_log (table_name, action, old_data, new_data)
VALUES (TG_TABLE_NAME, 'UPDATE', to_jsonb(OLD), to_jsonb(NEW));
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO audit_log (table_name, action, new_data)
VALUES (TG_TABLE_NAME, 'INSERT', to_jsonb(NEW));
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER employees_audit
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW EXECUTE FUNCTION log_employee_changes();用 to_jsonb(OLD) / to_jsonb(NEW) 直接把整行转成 JSONB 存进审计表,是 PG 做审计的经典手法。
RETURN COALESCE(NEW, OLD)——DELETE触发器里NEW是 NULL,INSERT里OLD是 NULL,所以用COALESCE兜住。
十二、扩展
PG 的扩展生态是它的一大优势。装扩展很简单:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- 慢查询统计(强烈推荐)
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- 三元组模糊匹配
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID 生成
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- 加密函数
CREATE EXTENSION IF NOT EXISTS postgis; -- 地理空间
CREATE EXTENSION IF NOT EXISTS vector; -- pgvector 向量检索
SELECT * FROM pg_extension; -- 查看已装扩展pg_stat_statements 是生产环境必装的——它统计每条 SQL 的调用次数、总耗时、平均耗时,是找慢查询的第一手数据:
-- 装完需要重启或改 shared_preload_libraries
CREATE EXTENSION pg_stat_statements;
SELECT queryid, calls, round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms, left(query, 60) AS q
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;十三、性能诊断与维护
13.1 当前活动与杀查询
-- 正在跑的查询
SELECT pid, usename, state, wait_event_type, wait_event,
now() - query_start AS duration, left(query, 60) AS q
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
-- 温和取消(等价 Ctrl+C)
SELECT pg_cancel_backend(12345);
-- 强制断开
SELECT pg_terminate_backend(12345);13.2 表与索引统计
-- 表的扫描情况
SELECT relname, seq_scan, idx_scan, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
ORDER BY seq_scan DESC;
-- 索引使用率(idx_scan=0 的索引可能多余)
SELECT relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
ORDER BY idx_scan LIMIT 10;13.3 空间占用
SELECT pg_size_pretty(pg_total_relation_size('employees')); -- 单表
SELECT pg_size_pretty(pg_database_size(current_database())); -- 当前库
-- 表大小排行
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;13.4 VACUUM 与膨胀治理
VACUUM employees; -- 回收死元组,不阻塞读写(可并发)
VACUUM ANALYZE employees; -- 回收 + 更新统计信息
VACUUM FULL employees; -- ⚠️ 整理并归还空间给 OS,会独占锁表!
ANALYZE employees; -- 只更新统计信息
REINDEX TABLE employees; -- 重建索引
REINDEX TABLE CONCURRENTLY employees; -- 并发重建(不阻塞写,但更慢)PG 的 autovacuum 默认开启,会自动回收死元组。但以下情况需要手工介入:
- 大批量
DELETE/UPDATE之后 - 长事务(阻止 autovacuum 回收)
- 表已经明显膨胀
-- 看 autovacuum 是否跟上
SELECT relname, n_dead_tup, last_autovacuum, last_vacuum
FROM pg_stat_user_tables
WHERE n_dead_tup > 10000
ORDER BY n_dead_tup DESC;
VACUUM FULL会取ACCESS EXCLUSIVE锁,期间该表完全不可读写。大表上执行可能锁几分钟到几十分钟。日常用普通VACUUM;确实要回收磁盘空间又要避免长时间锁表,用扩展pg_repack做在线整理。
十四、备份与恢复
14.1 逻辑备份
# 纯文本 SQL
pg_dump -U app -d sales_db -f sales_db.sql
# 自定义格式(推荐:压缩 + 支持并行/选择性恢复)
pg_dump -U app -d sales_db -Fc -f sales_db.dump
# 目录格式 + 并行(大库提速)
pg_dump -U app -d sales_db -Fd -j 4 -f /backup/sales_db_dir
# 所有数据库(含角色等全局对象)
pg_dumpall -U postgres -f all.sql
# 只导结构 / 只导数据 / 只导某表
pg_dump -U app -d sales_db --schema-only -f schema.sql
pg_dump -U app -d sales_db --data-only -f data.sql
pg_dump -U app -d sales_db -t employees -f employees.sql14.2 恢复
# 纯文本
psql -U app -d sales_db -f sales_db.sql
# 自定义 / 目录格式
pg_restore -U app -d sales_db sales_db.dump
pg_restore -U app -d sales_db -j 4 /backup/sales_db_dir
# 只恢复某张表
pg_restore -U app -d sales_db -t employees sales_db.dump14.3 物理备份与 PITR
物理备份 + WAL 归档可以做时间点恢复(恢复到某个具体时刻)。
# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'# 基础备份
pg_basebackup -D /backup/base -U replicator -P -Fp -Xs -R恢复流程(PG 12+):
# 1. 恢复基础备份到数据目录
tar -xf /backup/base.tar -C /var/lib/postgresql/data
# 2. 创建 recovery.signal 标记文件(PG 12+ 用这个,不再是 recovery.conf!)
touch /var/lib/postgresql/data/recovery.signal
# 3. 在 postgresql.conf 里配置恢复参数restore_command = 'cp /var/lib/postgresql/wal_archive/%f %p'
recovery_target_time = '2026-09-22 12:00:00'# 4. 启动,PG 会自动回放到目标时间点
sudo systemctl start postgresql重要变更:PG 12 起
recovery.conf已被移除。老教程里的"创建 recovery.conf"在新版本上根本不生效。现在用:
recovery.signal(普通时间点恢复)或standby.signal(做备库)- 恢复参数(
restore_command、recovery_target_time)写进postgresql.conf另外做 PITR 前必须确认 WAL 归档完整——归档断了就只能恢复到断点之前。
十五、复制与高可用
15.1 流复制
# 主库 postgresql.conf
wal_level = replica
max_wal_senders = 10
hot_standby = onCREATE USER replicator WITH REPLICATION PASSWORD 'ReplPass!2026';# 备库:用 pg_basebackup 基础备份 + -R 自动写连接配置
pg_basebackup -h 10.0.0.1 -U replicator -D /var/lib/postgresql/data -P -Xs -R-R 会自动在备库生成 standby.signal 和 primary_conninfo:
# postgresql.auto.conf
primary_conninfo = 'host=10.0.0.1 port=5432 user=replicator password=ReplPass!2026'sudo systemctl start postgresql # 备库自动进入流复制
# 主库查看复制状态
psql -c "SELECT client_addr, state, sync_state, replay_lag FROM pg_stat_replication;"replay_lag 就是复制延迟。流复制默认是异步的(主库不等备库确认);要强一致可以配 synchronous_standby_names 做同步复制,代价是写入延迟增加。
15.2 连接池:pgbouncer
PG 每个连接是一个独立进程,连接开销比 MySQL 大。高并发应用必须用连接池。
; /etc/pgbouncer/pgbouncer.ini
[databases]
sales_db = host=127.0.0.1 port=5432 dbname=sales_db
[pgbouncer]
listen_port = 6432
pool_mode = transaction ; transaction 模式并发能力最强
max_client_conn = 1000
default_pool_size = 20pool_mode 三种:
session:一个客户端连接独占一个服务端连接(最安全,复用率最低)transaction:按事务复用(最常用)statement:按语句复用(不兼容事务,一般别用)
transaction模式的代价:同一个客户端的多个事务可能落到不同的服务端连接上,所以不能用会话级状态(SET、PREPARE、LISTEN、临时表都会失效)。这是用连接池必须接受的约束。
十六、Python 集成
用 psycopg2(pip install psycopg2-binary):
import psycopg2
from psycopg2 import sql
conn = None
try:
conn = psycopg2.connect(
dbname='sales_db',
user='app',
password='StrongPass!2026',
host='localhost',
port=5432,
)
with conn.cursor() as cursor:
# 查询:%s 是占位符(不要自己拼字符串)
cursor.execute(
"SELECT id, name, salary FROM employees WHERE department = %s",
('研发部',),
)
for emp_id, name, salary in cursor.fetchall():
print(f"{emp_id}\t{name}\t{salary}")
# 插入并用 RETURNING 拿回自增 id
cursor.execute(
"""INSERT INTO employees (name, email, department, salary)
VALUES (%s, %s, %s, %s) RETURNING id""",
('王芳', 'wang@company.com', '市场部', 13000),
)
new_id = cursor.fetchone()[0]
print(f"新员工 ID: {new_id}")
conn.commit()
except Exception as e:
print(f"数据库错误: {e}")
if conn:
conn.rollback()
finally:
if conn:
conn.close()几个要点:
%s是 psycopg2 的占位符(注意不是?也不是:1)。它由驱动做转义,是防注入的正道。conn.commit()必须显式调用——psycopg2 默认不开 autocommit,不提交等于什么都没做。- 出错要
rollback(),否则连接会一直停在失败事务里,后续语句全部报current transaction is aborted。 - 生产用连接池:
psycopg2.pool或 SQLAlchemy,别每次请求新建连接(PG 建连接很贵)。
current transaction is aborted, commands ignored until end of transaction block是 PG 新手最常遇到的报错——事务里有一条语句失败后,后续所有语句都会被拒绝,直到ROLLBACK。这和 MySQL 的行为不同(MySQL 通常只影响失败的那条)。写批量脚本时要用SAVEPOINT或及时回滚。
十七、常见问题
| 报错 | 含义 | 处理 |
|---|---|---|
password authentication failed | 认证失败 | 查 .pgpass 权限、pg_hba.conf 规则 |
no pg_hba.conf entry for host | 主机未被允许 | pg_hba.conf 加规则 + pg_reload_conf() |
relation "x" does not exist | 表不存在,或 search_path 不含该 schema | SHOW search_path |
permission denied for schema | 缺 schema USAGE 权限 | GRANT USAGE ON SCHEMA ... TO ... |
permission denied for sequence | 缺序列权限 | GRANT USAGE, SELECT ON ALL SEQUENCES ... |
current transaction is aborted | 事务里有语句失败 | ROLLBACK 后重来 |
deadlock detected | 死锁,PG 已回滚一方 | 重试,统一加锁顺序 |
could not serialize access | SERIALIZABLE 冲突 | 应用层重试 |
too many clients already | 连接数满 | 上连接池(pgbouncer) |
canceling statement due to statement timeout | 语句超时 | 调 statement_timeout 或优化查询 |
权限是最容易踩的一块:PG 里"能访问表"需要双重权限——schema 的 USAGE + 表的 SELECT。只授表权限而忘了 schema 权限,查询会报 permission denied for schema。
学习资源
- PostgreSQL 官方文档 —— 每个版本都保留独立文档,查版本差异很方便
- PostgreSQL 官方 wiki —— 实战技巧、性能调优
- PGExercises —— 交互式练习
- Use The Index, Luke —— 索引与 SQL 性能(PG 例子最多)
- pgAdmin —— 官方图形化管理工具
graph LR
A[SQL 基础] --> B[PG 类型系统]
B --> C[JSONB 与索引]
C --> D[事务与 MVCC]
D --> E[性能诊断]
E --> F[备份与复制]
F --> G[高可用架构]用 PG 值得先记住三件事:认证看
pg_hba.conf(列是空格分隔的)、JSON 字段用JSONB+ GIN 索引、权限要 schema 和表双重授权。这三处是新手最常卡住的地方。剩下的能力,用到再查文档都有。
参考&致谢
系列教程
数据库系列
- SQL 命令使用教程:从入门到精通 —— SQL 标准命令,含 MySQL/PostgreSQL/SQLite 方言对照
- SQLite 使用全面教程:轻量级数据库的终极指南 —— 嵌入式数据库,类型系统、外键陷阱、WAL、FTS5
- MySQL 命令行使用全面教程:从入门到精通 ——
mysql客户端工具,批处理、备份、性能诊断 - MySQL 使用全面指南:从入门到高级实践 —— MySQL 安装配置、字符集、索引、事务锁、复制
- PostgreSQL 使用全面指南:从入门到企业级应用 —— PG 认证配置、JSONB、分区、备份与高可用
- PostgreSQL 命令行使用教程:掌握 psql 工具 ——
psql元命令、脚本化、\copy与pg_dump - PostgreSQL 实现原理深度剖析:高性能数据库引擎的核心机制 —— MVCC、WAL、B-tree 内核机制与可验证观察方法

