MySQL 转 PostgreSQL 使用指南

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。

目录

  1. 整体差异与学习重点
  2. [数据库与 Schema](#数据库与 Schema)
  3. 建表与字段类型
  4. 时间类型与时间函数
  5. [日常 SQL 对照](#日常 SQL 对照)
  6. 容易改变查询结果的差异
  7. 事务与并发控制
  8. 索引设计
  9. [JSONB 的使用](#JSONB 的使用)
  10. [执行计划与 VACUUM](#执行计划与 VACUUM)
  11. 开发工具与应用接入
  12. 建议的练习顺序

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。

参考:官方说明:Identity Columns。

另外,自增基于序列,序列取号不会因为事务回滚而退回。因此 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 排序规则不区分大小写。不能默认迁移后仍然如此。

参考:MySQL 官方说明:字符串搜索的大小写。

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,两者对函数依赖等细节的判断仍有差异。迁移时应明确每个非聚合字段的含义。

参考:MySQL 官方说明: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 还涉及可见性检查。

参考:官方说明:Index-Only Scans。

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 上会发生真实修改,不能当作纯粹的查看命令。

参考:官方说明:Using EXPLAIN。

10.2 理解 VACUUM 与 autovacuum

PG 的 MVCC 会保留行的旧版本,直到它们不再被需要。需要认识三个维护操作:

操作 作用
VACUUM 清理不再需要的旧版本,让空间可复用等
ANALYZE 更新查询优化器使用的统计信息
autovacuum 自动执行相关维护,默认配置中启用

例如批量导入后,可以按需要更新统计信息:

sql 复制代码
ANALYZE app.app_user;

几个直接影响开发的习惯:

  • 不要让事务长时间保持打开。
  • 尤其不要在事务里等待用户输入、调用耗时外部接口或大模型。
  • 大量更新、删除后,要关注旧版本清理和表膨胀。
  • 普通 VACUUM 主要让空间重新可用,不代表文件一定立即缩小。
  • VACUUM FULL 会重写表并获取强锁,不适合作为日常随手执行的清理方式。

参考:官方说明:Routine Vacuuming。

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. 建议的练习顺序

建议用一个小业务闭环完成练习,再进入项目。

  1. 建 schema 和用户表:掌握 identity、boolean、numeric、timestamptz。
  2. 完成增删改查 :插入时用 RETURNING,分页加稳定排序。
  3. 实现用户名幂等写入 :使用唯一约束和 ON CONFLICT。
  4. 用两个连接测试并发:观察默认隔离级别、行锁和条件扣款。
  5. 故意制造一次事务错误:确认应用能正确回滚,连接还能继续使用。
  6. 添加一列 JSONB 并建立索引:理解查询表达式与索引的对应关系。
  7. 看一次真实执行计划:比较估算行数、实际行数和数据页访问。

对于当前的新项目,建议优先落实这些规范:小写下划线命名、明确 schema、合理选择时间类型、使用参数化 SQL、用约束保证唯一性、事务及时结束、异常后可靠回滚。 这些规则确定后,日常 CRUD 的切换会顺畅很多。

相关推荐
晴天¥1 小时前
Oracle集群中是该如何管理控制文件和日志文件组?
数据库·oracle
NineData1 小时前
NineData 社区版 V5.4.0 上线:715 条复制对比链路,AI Agent 新增表结构设计发布能力
大数据·数据库·人工智能·mysql·数据库管理工具·ninedata·数据库迁移工具
云策数链1 小时前
用友 T+Cloud / U8 项目交接时的账套健康度核查清单
数据库·oracle·erp·用友·云策数链
Tt有了t1 小时前
Redis的数据结构
数据结构·数据库·redis
ss2731 小时前
PostgreSQL咨询锁的使用
数据库·postgresql
ycydynq1 小时前
django 添加信息时,对外键设置条件
数据库·python·django
m0_639906942 小时前
MySQL 索引底层原理|B + 树、聚簇索引、非聚簇索引、覆盖索引
数据库·mysql
全栈陈序员2 小时前
【Python】基础语法入门(二十八)—— 数据库操作入门:用 SQLite 持久化数据
数据库·人工智能·python
jnrjian2 小时前
Oracle RAC crs stop时force 关闭DB,另一个node会立即进行recover
数据库·oracle