加载中...

加载中...

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

PostgreSQL

一、PostgreSQL 是什么

PostgreSQL(简称 Postgres 或 PG)是一个对象关系型数据库,核心特点:

  • 严格遵循 SQL 标准:几乎不用"方言"就能写可移植的 SQL
  • 可扩展:能自定义数据类型、函数、操作符,甚至索引方法
  • 完整的事务支持(ACID)+ MVCC 多版本并发控制
  • 类型系统丰富:JSONB、数组、范围、网络地址、几何类型都是内置的
  • 扩展生态:PostGIS(地理空间)、TimescaleDB(时序)、pgvector(向量检索)、pg_partman(分区管理)

和 MySQL 怎么选

维度PostgreSQLMySQL
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@16

postgresql-contrib必装的——pg_stat_statementspgcryptocitext 这些常用扩展都在里面。

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 一定要指定。否则库属主是 postgresapp 用户连上去会发现什么都干不了——创建 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

规则匹配是"第一个匹配的行生效",从上往下,所以具体规则要写在通配规则前面

两个安全提醒

  1. 别用 trust。它表示"无条件信任、不验证密码"——任何能连上端口的人都能进来。
  2. 别写 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 / BIGINT2/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
JSONJSON / JSONB优先 JSONB(可索引)
数组INT[] / TEXT[] / JSONB[]原生支持
范围int4range / tsrange / daterange表示区间
网络INET / CIDR / MACADDR带校验的地址类型
UUIDUUID配合 gen_random_uuid()
几何POINT / LINE / POLYGON配合 PostGIS

TIMESTAMPTIMESTAMPTZ 的差别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 ONORDER 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_CONCATFILTERSUM(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 内置分词器不支持中文,需要装 zhparserpg_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 table
  • Nested 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_q1DELETE 快得多)、索引也可以按分区建。

除了 RANGE,还支持 LIST(按枚举值)和 HASH(散列均匀分布)。

十、事务与并发

10.1 隔离级别

SHOW default_transaction_isolation;                       -- 默认 read committed
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
级别脏读不可重复读幻读
READ UNCOMMITTEDPG 中视同 READ COMMITTED
READ COMMITTED(默认)可能可能
REPEATABLE READPG 中不会
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 SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVEVACUUM FULLALTER 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.00

IMMUTABLE性能关键:它告诉优化器"同样输入永远同样输出",这样函数可以被预计算、可用于表达式索引。分三类:

  • 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,INSERTOLD 是 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.sql

14.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.dump

14.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_commandrecovery_target_time)写进 postgresql.conf

另外做 PITR 前必须确认 WAL 归档完整——归档断了就只能恢复到断点之前。

十五、复制与高可用

15.1 流复制

# 主库 postgresql.conf
wal_level = replica
max_wal_senders = 10
hot_standby = on
CREATE 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.signalprimary_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 = 20

pool_mode 三种:

  • session:一个客户端连接独占一个服务端连接(最安全,复用率最低)
  • transaction:按事务复用(最常用
  • statement:按语句复用(不兼容事务,一般别用)

transaction 模式的代价:同一个客户端的多个事务可能落到不同的服务端连接上,所以不能用会话级状态SETPREPARELISTEN、临时表都会失效)。这是用连接池必须接受的约束。

十六、Python 集成

psycopg2pip 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()

几个要点:

  1. %s 是 psycopg2 的占位符(注意不是 ? 也不是 :1)。它由驱动做转义,是防注入的正道。
  2. conn.commit() 必须显式调用——psycopg2 默认不开 autocommit,不提交等于什么都没做。
  3. 出错要 rollback(),否则连接会一直停在失败事务里,后续语句全部报 current transaction is aborted
  4. 生产用连接池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 不含该 schemaSHOW 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 accessSERIALIZABLE 冲突应用层重试
too many clients already连接数满上连接池(pgbouncer)
canceling statement due to statement timeout语句超时statement_timeout 或优化查询

权限是最容易踩的一块:PG 里"能访问表"需要双重权限——schema 的 USAGE + 表的 SELECT。只授表权限而忘了 schema 权限,查询会报 permission denied for schema

学习资源

  1. PostgreSQL 官方文档 —— 每个版本都保留独立文档,查版本差异很方便
  2. PostgreSQL 官方 wiki —— 实战技巧、性能调优
  3. PGExercises —— 交互式练习
  4. Use The Index, Luke —— 索引与 SQL 性能(PG 例子最多)
  5. 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 和表双重授权。这三处是新手最常卡住的地方。剩下的能力,用到再查文档都有。

参考&致谢

系列教程

全部文章RSS订阅

数据库系列


PostgreSQL 使用全面指南:从入门到企业级应用
发布于
2025年11月22日
许可协议。转载请注明来源
评论
数据加载中 ...
 上一篇

阅读全文

PostgreSQL 实现原理深度剖析:高性能数据库引擎的核心机制
PostgreSQL 实现原理深度剖析:高性能数据库引擎的核心机制 PostgreSQL 实现原理深度剖析:高性能数据库引擎的核心机制
理解 PostgreSQL 的原理,不只是为了面试。知道 MVCC 是"保留旧版本"而非"undo 回滚",你才会明白为什么它需要 VACUUM、为什么长时间不清理表会膨胀。本文按架构、存储、MVCC、WAL、优化器、索引逐层拆解,每节都给
2025-11-22
下一篇 

阅读全文

MySQL 使用全面指南:从入门到高级实践
MySQL 使用全面指南:从入门到高级实践 MySQL 使用全面指南:从入门到高级实践
MySQL 是 Web 应用里最常打交道的数据库。它的坑大多不在语法,而在配置和设计——字符集选错存不了 emoji、金额用了 FLOAT、GROUP BY 报错、锁等待查不出原因。本文从安装配置讲到索引优化、事务锁、慢查询和主从复制,尽量
2025-11-22