MySQL 转 PostgreSQL 使用指南
- [MySQL 转 PostgreSQL 使用指南](#MySQL 转 PostgreSQL 使用指南)
-
- 目录
- [1. 整体差异与学习重点](#1. 整体差异与学习重点)
- [2. 数据库与 Schema](#2. 数据库与 Schema)
- [3. 建表与字段类型](#3. 建表与字段类型)
-
- [3.1 常用类型对照](#3.1 常用类型对照)
- [3.2 自增主键](#3.2 自增主键)
- [4. 时间类型与时间函数](#4. 时间类型与时间函数)
-
- [4.1 当前时间的不同含义](#4.1 当前时间的不同含义)
- [4.2 updated_at 不会自动更新](#4.2 updated_at 不会自动更新)
- [5. 日常 SQL 对照](#5. 日常 SQL 对照)
-
- [5.1 插入和更新时返回结果](#5.1 插入和更新时返回结果)
- [5.2 冲突更新](#5.2 冲突更新)
- [5.3 忽略指定的唯一冲突](#5.3 忽略指定的唯一冲突)
- [6. 容易改变查询结果的差异](#6. 容易改变查询结果的差异)
-
- [6.1 标识符引号和大小写](#6.1 标识符引号和大小写)
- [6.2 字符串内容的大小写](#6.2 字符串内容的大小写)
- [6.3 数值运算](#6.3 数值运算)
- [6.4 布尔和参数类型](#6.4 布尔和参数类型)
- [6.5 排序与分页](#6.5 排序与分页)
- [6.6 GROUP BY](#6.6 GROUP BY)
- [7. 事务与并发控制](#7. 事务与并发控制)
-
- [7.1 默认隔离级别不同](#7.1 默认隔离级别不同)
- [7.2 事务报错后的恢复](#7.2 事务报错后的恢复)
- [7.3 并发更新与行锁](#7.3 并发更新与行锁)
- [7.4 DDL 的事务行为](#7.4 DDL 的事务行为)
- [8. 索引设计](#8. 索引设计)
-
- [8.1 存储结构的区别](#8.1 存储结构的区别)
- [8.2 常用索引示例](#8.2 常用索引示例)
- [8.3 外键不会自动建立引用方索引](#8.3 外键不会自动建立引用方索引)
- [9. JSONB 的使用](#9. JSONB 的使用)
- [10. 执行计划与 VACUUM](#10. 执行计划与 VACUUM)
-
- [10.1 查看执行计划](#10.1 查看执行计划)
- [10.2 理解 VACUUM 与 autovacuum](#10.2 理解 VACUUM 与 autovacuum)
- [11. 开发工具与应用接入](#11. 开发工具与应用接入)
-
- [11.1 psql 常用命令](#11.1 psql 常用命令)
- [11.2 权限的层次](#11.2 权限的层次)
- [11.3 Python 与 SQLAlchemy 接入示例](#11.3 Python 与 SQLAlchemy 接入示例)
- [11.4 项目适配检查](#11.4 项目适配检查)
- [12. 建议的练习顺序](#12. 建议的练习顺序)
MySQL 转 PostgreSQL 使用指南
适用读者:已有 MySQL 使用经验,需要在新项目中使用 PostgreSQL 的开发者。
对比范围:现代 PostgreSQL 与 MySQL 8.x 的 InnoDB。具体功能以项目实际版本和配置为准。
关系建模、JOIN、索引、事务这些基础都能继续使用。真正需要调整的是:数据库对象的组织方式、字段类型、部分 SQL 写法,以及事务和性能排查习惯。
本文按"新项目实际开发"的顺序说明。下文将 PostgreSQL 简称为 PG。
目录
- 整体差异与学习重点
- [数据库与 Schema](#数据库与 Schema)
- 建表与字段类型
- 时间类型与时间函数
- [日常 SQL 对照](#日常 SQL 对照)
- 容易改变查询结果的差异
- 事务与并发控制
- 索引设计
- [JSONB 的使用](#JSONB 的使用)
- [执行计划与 VACUUM](#执行计划与 VACUUM)
- 开发工具与应用接入
- 建议的练习顺序
1. 整体差异与学习重点
| 方面 | MySQL 中的常见习惯 | PostgreSQL 中需要适应的地方 |
|---|---|---|
| 对象组织 | 实例 → database → table;schema 基本是 database 的同义词 | 实例 → database → schema → table |
| 自增主键 | AUTO_INCREMENT |
新表可以使用 GENERATED ... AS IDENTITY |
| 布尔字段 | TINYINT(1),常用 0/1 |
原生 boolean,用 true/false |
| 时间字段 | DATETIME、TIMESTAMP |
明确区分 timestamp 和 timestamptz |
| JSON | JSON |
通常优先考虑 jsonb |
| 冲突更新 | ON DUPLICATE KEY UPDATE |
ON CONFLICT ... DO UPDATE |
| 获取生成的主键 | LAST_INSERT_ID() 或驱动接口 |
常用 INSERT ... RETURNING id |
| 默认隔离级别 | InnoDB 默认可重复读 | PG 默认读已提交 |
| 事务中语句报错 | 很多错误只导致当前语句失败 | 通常需要回滚事务,或回滚到保存点 |
| 查询优化 | 熟悉 B-tree、联合索引、执行计划 | 基础相通,增加部分索引、GIN 等工具 |
| 日常维护 | 熟悉 undo、purge 等机制 | 需要理解 MVCC、VACUUM、autovacuum |
不要把学习重点只放在"函数名替换"上。事务行为、时间语义、大小写规则,往往比语法报错更容易造成业务问题。
2. 数据库与 Schema
PG 多了一层 schema,理解之后,建库和写表名就不会混乱。
你在 MySQL 中可能这样使用:
sql
SELECT * FROM business_db.app_user;
这里 business_db 是数据库。
在 PG 中:
sql
SELECT * FROM business.app_user;
这里 business 通常是 schema,数据库由当前连接决定。
例如,一个 PG 数据库可以这样组织:
text
appdb
├── public
├── business
│ ├── app_user
│ └── orders
└── audit
└── operation_log
同一个数据库中的不同 schema 可以直接关联:
sql
SELECT *
FROM business.app_user u
JOIN audit.operation_log l ON l.user_id = u.id;
但不同 database 之间,不能像 MySQL 那样直接通过 另一个库.表 做普通跨库查询;需要额外机制,例如外部数据包装器。
新项目可以使用默认的 public,也可以建立业务 schema:
sql
CREATE SCHEMA app;
CREATE TABLE app.app_user (
id bigint PRIMARY KEY
);
如果省略 schema:
sql
SELECT * FROM app_user;
PG 会按照 search_path 查找对象:
sql
SHOW search_path;
建议:项目尽早统一 schema;在建表脚本和重要 SQL 中显式写出 app.app_user,减少环境差异。
参考:官方说明:Schemas。
3. 建表与字段类型
建表时,重点换掉自增、布尔和时间字段的习惯。
下面是一张可以作为参考的 PG 用户表:
sql
CREATE TABLE app.app_user (
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username varchar(50) NOT NULL UNIQUE,
email varchar(255),
balance numeric(12, 2) NOT NULL DEFAULT 0
CHECK (balance >= 0),
is_active 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(),
deleted_at timestamptz
);
COMMENT ON TABLE app.app_user IS '用户表';
COMMENT ON COLUMN app.app_user.username IS '用户名';
本文 SQL 为分主题示例,不是需要整篇依次执行的初始化脚本。上节的简化建表示例与本节完整建表示例,应择一使用。
它与 MySQL 建表语句的主要区别是:
- 没有
ENGINE=InnoDB、DEFAULT CHARSET=utf8mb4这样的表选项。 - 自增使用 identity。
- 布尔字段使用
boolean。 - 注释通过独立的
COMMENT ON设置。 DEFAULT now()只提供默认值,不会让updated_at在更新时自动变化。
3.1 常用类型对照
| MySQL 类型 | PG 中的常见选择 | 说明 |
|---|---|---|
INT |
integer |
常规整数 |
BIGINT |
bigint |
常规长整数 |
INT UNSIGNED |
bigint,必要时加 CHECK |
PG 整数没有 UNSIGNED;有符号 integer 装不下完整范围 |
BIGINT UNSIGNED |
按范围选择 bigint 或 numeric(20,0) |
完整无符号范围不能放进 PG 的 bigint |
TINYINT |
smallint |
如果表达真假,改用 boolean |
DECIMAL(p,s) |
numeric(p,s) / decimal(p,s) |
金额仍使用精确小数 |
DOUBLE |
double precision |
浮点数 |
VARCHAR(n) |
varchar(n) |
有业务长度限制时使用 |
TEXT |
text |
不需要预设较小长度上限 |
DATETIME |
按语义选择 timestamp 或 timestamptz |
不要只按名称机械替换 |
JSON |
通常选择 jsonb |
适合 JSON 查询和索引 |
BLOB |
bytea |
二进制数据 |
PG 的 text 是正常的字符串类型,不必为了性能,习惯性地把所有字符串都定义成 varchar(255)。长度限制应该来自业务规则。
参考:官方说明:字符类型。
3.2 自增主键
sql
-- 允许显式提供 id,也允许省略 id 让数据库生成
id bigint GENERATED BY DEFAULT AS IDENTITY
-- 通常要求使用数据库生成值,显式覆盖需要特殊语法
id bigint GENERATED ALWAYS AS IDENTITY
旧项目中常见的 serial、bigserial 也能使用;新表建议优先采用 identity。
Identity 本身不等于唯一约束,仍然需要 PRIMARY KEY 或 UNIQUE。
另外,自增基于序列,序列取号不会因为事务回滚而退回。因此 ID 出现空洞很正常,不要把自增 ID 当作连续业务编号。导入历史 ID 后,也要检查序列是否需要同步。
参考:官方说明:序列与事务。
4. 时间类型与时间函数
时间类型一定要按业务含义选择,这是 MySQL 开发者转 PG 时值得单独理解的地方。
| PG 类型 | 表达的含义 | 适合的例子 |
|---|---|---|
date |
某个日期 | 生日、结算日期 |
timestamp |
不包含时区信息的日期和时间 | 某地日历上的"上午 9 点" |
timestamptz |
时间线上的一个确定时刻 | 创建时间、支付时间、消息发送时间 |
需要记住:
- PG 单独写
timestamp,就是timestamp without time zone。 timestamptz会按会话时区显示同一个时刻。timestamptz不会保留原始输入的时区名称 ;如果业务需要保存"用户所在时区",应额外存储,例如Asia/Shanghai。
参考:官方说明:日期和时间类型。
例如:
sql
SET TIME ZONE 'Asia/Shanghai';
SELECT TIMESTAMPTZ '2026-10-28 10:00:00+08';
-- 显示上海时间 10:00
SET TIME ZONE 'UTC';
SELECT TIMESTAMPTZ '2026-10-28 10:00:00+08';
-- 显示 UTC 时间 02:00,仍然是同一个时刻
对于普通业务系统的 created_at、updated_at,默认建议使用 timestamptz,并统一应用和连接的时区约定。
4.1 当前时间的不同含义
sql
now() -- 当前事务开始的时间
statement_timestamp() -- 当前语句开始的时间
clock_timestamp() -- 函数调用时的实际时间
所以在一个很长的事务中,多次调用 now(),值不会随着时间流逝而变化。
参考:官方说明:当前时间函数。
4.2 updated_at 不会自动更新
可以由应用统一更新:
sql
UPDATE app.app_user
SET email = 'new@example.com',
updated_at = now()
WHERE id = 1;
如果存在多个写入入口,也可以通过触发器统一维护。只写 DEFAULT now() 不够。
5. 日常 SQL 对照
日常 SQL 大部分相通,但下面的对照表可以帮你快速上手。
| 需求 | MySQL 常见写法 | PG 写法 |
|---|---|---|
| 空值替换 | IFNULL(a, b) |
COALESCE(a, b) |
| 条件表达式 | IF(condition, a, b) |
CASE WHEN condition THEN a ELSE b END |
| 分页 | LIMIT 20, 10 |
LIMIT 10 OFFSET 20 |
| 类型转换 | CAST(x AS SIGNED) |
CAST(x AS bigint) 或 x::bigint |
| 当前时间 | NOW() |
now(),注意事务时间语义 |
| 加一天 | DATE_ADD(t, INTERVAL 1 DAY) |
t + INTERVAL '1 day' |
| 日期格式化 | DATE_FORMAT(t, '%Y-%m-%d') |
to_char(t, 'YYYY-MM-DD') |
| 字符串聚合 | GROUP_CONCAT(name ORDER BY name SEPARATOR ',') |
string_agg(name, ',' ORDER BY name) |
| NULL 安全相等 | a <=> b |
a IS NOT DISTINCT FROM b |
| 冲突时更新 | ON DUPLICATE KEY UPDATE |
ON CONFLICT (...) DO UPDATE |
这里的"对应"不代表所有边界行为完全相同,尤其要留意 NULL、类型和时区。
最值得尽早掌握的两个 PG 写法是 RETURNING 和 ON CONFLICT。
5.1 插入和更新时返回结果
插入后直接返回主键:
sql
INSERT INTO app.app_user (username, email)
VALUES ('zhangsan', 'zhangsan@example.com')
RETURNING id, username, created_at;
不用再额外查询"刚才生成的 ID"。
更新也可以返回修改后的数据:
sql
UPDATE app.app_user
SET email = 'new@example.com',
updated_at = now()
WHERE id = 1
RETURNING id, email, updated_at;
5.2 冲突更新
sql
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;
这里:
(username)指定冲突判定目标,需要对应的唯一约束或合适的唯一索引。EXCLUDED.email表示这次原本准备插入的 email。- 冲突后更新已有记录,最后返回实际插入或更新的记录。
5.3 忽略指定的唯一冲突
sql
INSERT INTO app.app_user (username)
VALUES ('zhangsan')
ON CONFLICT (username) DO NOTHING;
DO NOTHING 不会吞掉所有错误。 类型错误、非空约束错误等仍会报错;该写法也不能机械替代 MySQL 的所有 INSERT IGNORE 或 REPLACE INTO 用法。
DO NOTHING 没有插入记录时,RETURNING 也不会自动返回已有记录。
参考:官方说明:INSERT、ON CONFLICT 与 RETURNING。
6. 容易改变查询结果的差异
有几类"SQL 能运行,但结果变了"的差异,要专门检查。
6.1 标识符引号和大小写
MySQL 常见写法:
sql
SELECT `username` FROM `app_user`;
PG 中通常直接写:
sql
SELECT username FROM app.app_user;
需要引用标识符时用双引号:
sql
SELECT "username" FROM app."app_user";
字符串依然使用单引号:
sql
WHERE username = 'zhangsan'
PG 会把未加双引号的名称转换成小写:
sql
CREATE TABLE UserInfo (...);
-- 实际名称是 userinfo
CREATE TABLE "UserInfo" (...);
-- 实际名称是大小写敏感的 UserInfo
建议统一用小写加下划线命名,避免 "UserInfo"、"createdAt" 这类必须持续加引号的设计。
参考:官方说明:SQL 标识符。
6.2 字符串内容的大小写
MySQL 中,字符串比较是否区分大小写由 collation 决定;常见的 _ci 排序规则不区分大小写。不能默认迁移后仍然如此。
PG 普通 text 在常见配置下会区分大小写。忽略大小写的模式匹配可以使用:
sql
SELECT *
FROM app.app_user
WHERE username ILIKE 'zhang%';
但 ILIKE 只影响这次匹配,不会让普通唯一约束自动忽略大小写。
参考:官方说明:模式匹配。
如果业务要求 email 忽略大小写且唯一,可以明确建立规则:
sql
CREATE UNIQUE INDEX uq_app_user_email_lower
ON app.app_user (lower(email));
查询也采用一致的表达式:
sql
SELECT *
FROM app.app_user
WHERE lower(email) = lower('Alice@example.com');
6.3 数值运算
sql
-- PG
SELECT 5 / 2; -- 2,整数除法
SELECT 5::numeric / 2; -- 2.5
从 MySQL 迁移计算比例、均值的表达式时,要检查是否发生整数截断。
参考:官方说明:数学运算。
6.4 布尔和参数类型
sql
WHERE is_active = true
-- 或
WHERE is_active
不要继续按照整数列去写:
sql
WHERE is_active = 1
应用传入 ID、布尔、UUID、时间时,也应使用正确的参数类型,不要依赖 MySQL 中熟悉的隐式转换行为。
6.5 排序与分页
PG 默认升序通常把 NULL 放到最后,降序把 NULL 放到前面。业务需要固定规则时应明确写出:
sql
ORDER BY deleted_at DESC NULLS LAST
分页则必须有稳定排序:
sql
SELECT id, username, created_at
FROM app.app_user
ORDER BY created_at DESC, id DESC
LIMIT 10 OFFSET 20;
加上 id 是为了给相同的 created_at 提供明确顺序。没有 ORDER BY 时,两种数据库都不保证记录返回顺序。
6.6 GROUP BY
不要简单记成"MySQL 的 GROUP BY 宽松,PG 严格"。现代 MySQL 默认启用 ONLY_FULL_GROUP_BY,两者对函数依赖等细节的判断仍有差异。迁移时应明确每个非聚合字段的含义。
7. 事务与并发控制
事务是最需要重新校准的部分。
7.1 默认隔离级别不同
MySQL InnoDB 默认是:
text
REPEATABLE READ:可重复读
PG 默认是:
text
READ COMMITTED:读已提交
PG 可以这样查看:
sql
SHOW transaction_isolation;
在默认的读已提交下:
sql
BEGIN;
SELECT balance FROM app.app_user WHERE id = 1;
-- 此时另一个事务修改余额并提交
SELECT balance FROM app.app_user WHERE id = 1;
COMMIT;
两次查询可能得到不同结果,因为每条语句会获取新的快照。
如果业务确实要求一个事务内多次读取保持一致,可以考虑:
sql
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- 多次查询
COMMIT;
但不要认为同名隔离级别在 MySQL 和 PG 中实现完全相同;选择隔离级别后,仍要设计并发冲突的处理方式。
参考:PG 官方说明:事务隔离、MySQL 官方说明:InnoDB 事务隔离。
7.2 事务报错后的恢复
sql
BEGIN;
SELECT 1 / 0; -- 报错
SELECT 1; -- 仍然失败:当前事务已经处于 aborted 状态
ROLLBACK;
因此,在应用中捕获 SQL 异常后,不能直接在同一个失败事务里继续查询。要整体回滚,或回滚到报错前建立的保存点:
sql
BEGIN;
SAVEPOINT sp1;
-- 执行可能失败的操作
-- 如果操作失败,由应用执行:
ROLLBACK TO SAVEPOINT sp1;
-- 然后才能继续事务中的其他操作
COMMIT;
ORM 中也一样:捕获异常不等于事务恢复。
参考:官方说明:事务与保存点。
7.3 并发更新与行锁
并发更新时,优先把条件和修改放进一条 SQL。例如余额不足时不允许扣款:
sql
UPDATE app.app_user
SET balance = balance - 100,
updated_at = now()
WHERE id = 1
AND balance >= 100
RETURNING id, balance;
没有返回记录,表示没有满足条件的用户。它比"先查余额,再用旧余额计算并覆盖"的方式更容易保证正确性;PG 默认隔离级别下,会在等待并发更新后重新检查目标行的条件。
如果业务需要锁定已有记录,仍然可以使用:
sql
SELECT *
FROM app.app_user
WHERE id = 1
FOR UPDATE;
但不要照搬 InnoDB 的间隙锁经验:在 PG 中,查不到记录的 FOR UPDATE,不能当作锁住了未来插入的位置。 防止重复记录应优先依靠唯一约束。
出现死锁或序列化失败时,应回滚并按策略重试整个业务事务,而不是只重复最后一条 SQL。
7.4 DDL 的事务行为
PG 中常见的 CREATE TABLE、ALTER TABLE 等 DDL 可以放在事务中回滚:
sql
BEGIN;
CREATE TABLE app.demo (
id bigint PRIMARY KEY
);
ROLLBACK;
-- app.demo 不会保留下来
MySQL 很多 DDL 会隐式提交,行为不同。但 PG 也有例外,例如 CREATE DATABASE、CREATE INDEX CONCURRENTLY 不能放进普通事务块。
参考:MySQL 官方说明:隐式提交、PG 官方说明:并发建索引。
8. 索引设计
索引经验可以继续用,但不要完全照搬 InnoDB 的存储结构。
两者都支持常用的 B-tree 索引、联合索引和唯一索引,基于查询条件和排序设计索引的思路仍然有效。
8.1 存储结构的区别
- InnoDB 的主键通常是聚簇索引,二级索引包含主键值。
- PG 普通表使用堆存储,索引指向表中的行位置;主键不会自动把表长期按主键顺序组织。
因此,MySQL 中"二级索引天然携带主键""这个查询一定是覆盖索引"的判断,不能原封不动移到 PG。PG 的 Index Only Scan 还涉及可见性检查。
8.2 常用索引示例
新项目首先掌握以下几种索引即可,按查询需求选择,不需要全部建上。
普通 B-tree 索引:
sql
CREATE INDEX idx_app_user_created
ON app.app_user (created_at DESC, id DESC);
表达式索引:
sql
CREATE INDEX idx_app_user_username_lower
ON app.app_user (lower(username));
它适合匹配这样的查询:
sql
WHERE lower(username) = 'zhangsan'
部分索引,只包含满足条件的记录:
sql
CREATE INDEX idx_app_user_active_created
ON app.app_user (created_at DESC, id DESC)
WHERE deleted_at IS NULL;
查询也应包含相应条件:
sql
SELECT id, username
FROM app.app_user
WHERE deleted_at IS NULL
ORDER BY created_at DESC, id DESC
LIMIT 20;
部分唯一索引还能表达业务规则。例如"只要求未删除用户的 email 忽略大小写且唯一":
sql
CREATE UNIQUE INDEX uq_app_user_live_email
ON app.app_user (lower(email))
WHERE deleted_at IS NULL;
这是一种可替代前面全表 email 唯一索引的设计,应根据业务选择。
参考:官方说明:部分索引。
JSON 查询则可以考虑 GIN:
sql
CREATE INDEX idx_app_user_profile
ON app.app_user USING gin (profile);
PG 还提供 GiST、BRIN 等索引。初学时先掌握 B-tree,再根据 JSON、空间数据或大规模有序数据等实际需求学习。
参考:官方说明:索引类型。
8.3 外键不会自动建立引用方索引
PG 不会因为你声明了外键,就自动在引用方字段上创建索引。
如果订单表的 user_id 经常用于关联,或用户删除、更新时需要检查关联订单,应评估是否建立该索引。
参考:官方说明:外键约束。
9. JSONB 的使用
JSONB 是值得利用的能力,但需要围绕查询设计。
PG 同时提供 json 和 jsonb:
json保留原始 JSON 文本形式。jsonb使用解析后的二进制结构,支持更丰富的查询和索引。jsonb不保留对象键顺序、格式空白和重复键。
普通业务扩展信息通常优先考虑 jsonb。
参考:官方说明:JSON 类型。
例如:
sql
INSERT INTO app.app_user (username, profile)
VALUES (
'lisi',
'{"city":"北京","tags":["vip","developer"]}'::jsonb
);
读取字符串字段:
sql
SELECT
username,
profile ->> 'city' AS city
FROM app.app_user;
常见运算符:
| 运算符 | 含义 |
|---|---|
-> |
取 JSON 字段,结果仍是 JSON/JSONB |
->> |
取字段,结果是文本 |
@> |
判断是否包含指定 JSON 结构 |
? |
判断顶层键或数组元素是否存在 |
例如:
sql
SELECT *
FROM app.app_user
WHERE profile @> '{"city":"北京"}'::jsonb;
这类包含查询可以利用适合的 GIN 索引。
不过:
sql
WHERE profile ->> 'city' = '北京'
不一定就能利用前面的整个 profile GIN 索引;这类查询也可能更适合表达式索引。索引要匹配运算符和查询表达式。
建模建议:经常参与关联、排序、唯一校验和关键筛选的字段,优先使用普通列;结构灵活的扩展信息使用 JSONB。
10. 执行计划与 VACUUM
性能排查时,除了 EXPLAIN,还要理解 VACUUM。
10.1 查看执行计划
先查看估算计划:
sql
EXPLAIN
SELECT *
FROM app.app_user
WHERE username = 'zhangsan';
需要实际执行数据时:
sql
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM app.app_user
WHERE username = 'zhangsan';
重点关注:
| 内容 | 你需要判断什么 |
|---|---|
Seq Scan |
是否扫描了大量不需要的数据;小表顺序扫描可能很合理 |
Index Scan |
索引筛选效果如何,是否还需要读取大量表数据 |
Bitmap Heap Scan |
通过索引找到位置后批量访问表 |
| 估算行数与实际行数 | 差距是否很大,统计信息是否准确 |
loops |
节点是否被重复执行很多次 |
Buffers |
读取和命中的数据页情况 |
EXPLAIN ANALYZE 会真实执行 SQL。 用在 UPDATE、DELETE 上会发生真实修改,不能当作纯粹的查看命令。
10.2 理解 VACUUM 与 autovacuum
PG 的 MVCC 会保留行的旧版本,直到它们不再被需要。需要认识三个维护操作:
| 操作 | 作用 |
|---|---|
VACUUM |
清理不再需要的旧版本,让空间可复用等 |
ANALYZE |
更新查询优化器使用的统计信息 |
autovacuum |
自动执行相关维护,默认配置中启用 |
例如批量导入后,可以按需要更新统计信息:
sql
ANALYZE app.app_user;
几个直接影响开发的习惯:
- 不要让事务长时间保持打开。
- 尤其不要在事务里等待用户输入、调用耗时外部接口或大模型。
- 大量更新、删除后,要关注旧版本清理和表膨胀。
- 普通
VACUUM主要让空间重新可用,不代表文件一定立即缩小。 VACUUM FULL会重写表并获取强锁,不适合作为日常随手执行的清理方式。
11. 开发工具与应用接入
开发工具和应用接入,也要一起切换。
11.1 psql 常用命令
MySQL 默认端口是 3306,PG 默认端口是 5432。PG 命令行客户端是 psql:
bash
psql -h localhost -p 5432 -U app_user -d appdb
| 目的 | psql 命令 |
|---|---|
| 列出数据库 | \l |
| 切换连接的数据库 | \c appdb |
| 列出 schema | \dn |
| 列出 app 下的表 | \dt app.* |
| 查看表结构 | \d app.app_user |
| 查看更详细的表信息 | \d+ app.app_user |
| 查看角色 | \du |
| 开关执行计时 | \timing |
| 退出 | \q |
这些反斜杠命令属于 psql,不是通用 SQL,不需要加分号。
参考:官方说明:psql。
11.2 权限的层次
能连接数据库,不代表能访问 schema;能访问 schema,不代表拥有表和序列的相应权限。应用账号、迁移账号可以按职责分别配置。
11.3 Python 与 SQLAlchemy 接入示例
如果项目使用 Python + SQLAlchemy,连接配置例如:
python
import os
from sqlalchemy import create_engine, text
engine = create_engine(
os.environ["DATABASE_URL"],
pool_pre_ping=True,
)
# DATABASE_URL 示例:
# postgresql+psycopg://用户名:密码@localhost:5432/appdb
with engine.begin() as conn:
user_id = conn.execute(
text("""
INSERT INTO app.app_user (username)
VALUES (:username)
RETURNING id
"""),
{"username": "wangwu"},
).scalar_one()
这里需要对应的 psycopg 驱动;engine.begin() 上下文正常结束时提交,异常退出时回滚。SQLAlchemy 的异步接入也有对应的 PG 驱动和方言配置。
参考:SQLAlchemy 官方说明:PostgreSQL。
11.4 项目适配检查
如果使用其他框架,同样需要检查:
- 数据库驱动和 ORM 方言。
- 主键生成方式。
boolean、JSON、时间字段的映射。- 原生 SQL 中的 MySQL 专用函数。
- 分页和批量插入组件。
- 事务异常后的回滚逻辑。
- 数据库迁移脚本的执行方式。
不要把"换了连接字符串,应用能启动"作为适配完成的标准。
12. 建议的练习顺序
建议用一个小业务闭环完成练习,再进入项目。
- 建 schema 和用户表:掌握 identity、boolean、numeric、timestamptz。
- 完成增删改查 :插入时用
RETURNING,分页加稳定排序。 - 实现用户名幂等写入 :使用唯一约束和
ON CONFLICT。 - 用两个连接测试并发:观察默认隔离级别、行锁和条件扣款。
- 故意制造一次事务错误:确认应用能正确回滚,连接还能继续使用。
- 添加一列 JSONB 并建立索引:理解查询表达式与索引的对应关系。
- 看一次真实执行计划:比较估算行数、实际行数和数据页访问。
对于当前的新项目,建议优先落实这些规范:小写下划线命名、明确 schema、合理选择时间类型、使用参数化 SQL、用约束保证唯一性、事务及时结束、异常后可靠回滚。 这些规则确定后,日常 CRUD 的切换会顺畅很多。