PostgreSQL vs MySQL vs Oracle
本节把 PostgreSQL 放在主流关系数据库的横向坐标系里对比:许可协议、数据类型、扩展能力、JSON 支持、事务与并发、性能特点、生态工具与适用场景。学完本节,你将能对一次真实的选型讨论给出有依据的判断,而不是停留在"哪个更流行"。
前置知识
阅读本节前,建议先了解:发展历史与版本演进
基础概念
横向对比数据库时,最容易犯的错误是只比"功能列表"。更有效的做法是先想清楚负载画像(Workload Profile),再看每个数据库的设计取舍。对比前先明确几个术语:
- 许可协议(License):决定你能不能在商业闭源产品中自由使用、修改、再分发。
- 存储引擎(Storage Engine):数据落盘与索引的实现层。MySQL 有 InnoDB、MyISAM 等多套引擎;PostgreSQL 只有一套内置堆表(Heap)引擎,但其索引与类型可扩展。
- 多版本并发控制(MVCC, Multi-Version Concurrency Control):让读不阻塞写、写不阻塞读的机制。三者都实现了 MVCC,但默认隔离级别不同。
- 扩展(Extension):以插件形式向数据库内核注入新类型、函数、索引方法的能力。这是 PostgreSQL 最独特的开放点。
- 生态(Ecosystem):周边工具、云厂商托管形态、社区活跃度、资料丰富程度。
- 分布式(Distributed):跨多机横向扩展的能力。PostgreSQL 原生为单机主从架构,分布式依赖外部方案(如 Citus、Greenplum)或分库分表中间件。
三者的定位差异可以先用一张表概括:
| 维度 | PostgreSQL | MySQL | Oracle Database |
|---|---|---|---|
| 许可协议 | PostgreSQL License(类 BSD/MIT,极度宽松) | GPLv2 + 商业双许可(Oracle 持有) | 商业闭源授权 |
| 归属方 | 全球社区(无单一公司控制) | Oracle 公司 | Oracle 公司 |
| 定位 | 通用型对象关系数据库(可扩展为多模) | 互联网 OLTP 主力 | 大型企业核心业务 |
| 默认隔离级别 | READ COMMITTED | REPEATABLE READ(InnoDB) | READ COMMITTED |
| 扩展能力 | 极强(自定义类型/索引/函数/存储引擎接口) | 弱(仅存储引擎层面的插件) | 中(有 Extensibility Framework,但门槛与成本高) |
| 成本 | 免费 | 社区版免费,企业版收费 | 许可费高,按 CPU 核数计费 |
| 典型场景 | 复杂查询、地理空间、JSON、分析混合负载 | 高并发简单读写、Web 应用 | 金融、电信、大型 ERP |
语法与用法
三者在"同一件事怎么写"上差异明显。以下对比围绕最常用的几个语法点展开,便于迁移时快速定位。
1. 自增主键
-- PostgreSQL:标准 SQL 写法(推荐)
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL
);-- MySQL:AUTO_INCREMENT
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;-- Oracle:12c 之前必须借助 SEQUENCE + TRIGGER;12c 起支持 IDENTITY
CREATE TABLE users (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name VARCHAR2(100) NOT NULL
);2. 标识符引号规则
-- PostgreSQL:未加引号的标识符统一转为小写;双引号才是标识符引用
SELECT id, name FROM users WHERE name = 'Alice';
-- 不推荐:双引号引用会强制大小写敏感,后续必须一直加引号
SELECT "Id", "Name" FROM "Users";-- MySQL:默认双引号是字符串字面量,标识符用反引号
-- 在 ANSI_QUOTES 模式下双引号才等价于 PostgreSQL 的行为
SELECT `id`, `name` FROM `users` WHERE `name` = "Alice";3. 行数限制与分页
-- PostgreSQL / Oracle 12c+:标准 FETCH FIRST 语法
SELECT id, name FROM users ORDER BY id FETCH FIRST 10 ROWS ONLY;-- MySQL:LIMIT
SELECT id, name FROM users ORDER BY id LIMIT 10;4. UPSERT(插入或更新)
-- PostgreSQL 9.5+:ON CONFLICT,需要指定冲突目标
INSERT INTO users (id, name) VALUES (1, 'Alice')
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name;-- PostgreSQL 15+:标准 MERGE 语句,可覆盖更复杂的双表合并场景
MERGE INTO users AS t
USING (VALUES (1, 'Alice')) AS s (id, name)
ON t.id = s.id
WHEN MATCHED THEN UPDATE SET name = s.name
WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name);-- MySQL:ON DUPLICATE KEY UPDATE
INSERT INTO users (id, name) VALUES (1, 'Alice')
ON DUPLICATE KEY UPDATE name = VALUES(name);5. 完整外连接
-- PostgreSQL:FULL OUTER JOIN 原生支持
SELECT u.name, o.order_no
FROM users AS u
FULL OUTER JOIN orders AS o ON o.user_id = u.id;-- MySQL:不支持 FULL OUTER JOIN,必须用 LEFT JOIN UNION RIGHT JOIN 拼
SELECT u.name, o.order_no FROM users u LEFT JOIN orders o ON o.user_id = u.id
UNION
SELECT u.name, o.order_no FROM users u RIGHT JOIN orders o ON o.user_id = u.id;详细说明
许可协议与商业模式
PostgreSQL 采用 PostgreSQL License,本质是类 BSD/MIT 的宽松协议:可以自由用于闭源商业产品,修改后无需开源,也无需向任何实体付费或报备。这个"无商业实体控制"的特性,是很多团队把它作为长期基础设施底座的核心原因——不存在"上游公司被收购后改授权"的风险。
MySQL 采用 GPLv2 与商业许可的双授权模式,由 Oracle 持有。社区版免费,但企业版插件(如企业级备份、审计、线程池)与官方支持需要付费。GPLv2 对"分发衍生作品"有传染性要求,闭源分发时通常需要购买商业许可,这一点在商业合规评审中必须提前确认。
Oracle Database 是纯商业闭源授权,按 CPU 核数计费,且大量高级特性(分区、RAC、Advanced Compression、Active Data Guard)需要额外购买选件。它的优势不在单价低,而在于超大规模核心系统的稳定性验证与配套服务体系。
数据类型与扩展能力
这是 PostgreSQL 优势最明显的一栏。它的类型系统本身可扩展:你能用 CREATE TYPE 定义复合类型、枚举类型、范围类型,甚至为新类型定义专属的操作符与索引方法。
-- PostgreSQL:自定义复合类型 + 自定义枚举
CREATE TYPE money_breakdown AS (currency CHAR(3), amount NUMERIC(14, 2));
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'closed');
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
status order_status NOT NULL DEFAULT 'pending',
total money_breakdown NOT NULL
);
INSERT INTO orders (status, total) VALUES ('paid', ROW('CNY', 199.00));
-- 复合类型用括号访问字段,也可整体展开
SELECT status, (total).currency, (total).amount FROM orders;原生类型覆盖也最完整:ARRAY、JSONB、UUID、INET/CIDR/MACADDR、TSVECTOR、RANGE、MULTIRANGE(14+)、INTERVAL、任意精度 NUMERIC。
MySQL 的类型系统开放度低得多,没有数组类型,JSON 类型也不能索引内部路径(只能建函数索引或生成列),不具备自定义类型与操作符的机制。Oracle 有对象类型(Object Type)体系与 VARRAY、嵌套表,能力上接近,但定义与使用复杂,且 NUMBER、VARCHAR2、DATE(含时间)等类型语义与标准 SQL 有偏差异。
JSON 支持对比
三者的 JSON 能力有代际差异:
| 能力 | PostgreSQL | MySQL | Oracle |
|---|---|---|---|
| JSON 类型 | JSON(文本)与 JSONB(二进制,可索引) | JSON(5.7+,内部二进制存储) | JSON(12c+,21c 起有原生类型) |
| 路径查询 | -> / ->> / #> / #>> | -> / ->> / JSON_EXTRACT() | JSON_VALUE() / JSON_QUERY() |
| 包含查询 | @>、<@、?、`? | 、?&` | JSON_CONTAINS() |
| 索引方式 | GIN 索引直接建在 JSONB 上 | 需生成列 + 索引,或函数索引 | 需函数索引或 JSON 搜索索引 |
| SQL/JSON 标准 | 16 起 JSON_TABLE、IS JSON(16/17+) | 部分支持 JSON_TABLE(8.0.4+) | 支持 JSON_TABLE |
| JSONB 下标访问 | 14+ 支持 col['key'] 写法 | 不支持 | 不支持 |
-- PostgreSQL:GIN 索引让 JSONB 包含查询走索引
CREATE INDEX idx_products_attrs ON products USING GIN (attrs);
SELECT sku, name
FROM products
WHERE attrs @> '{"brand": "Logitech"}'::jsonb; -- 一行命中,走 GINPostgreSQL 的 JSONB 与索引的结合是最成熟的:不需要预先声明"要索引哪个路径",@> 这类包含查询可以直接利用 GIN 的倒排结构。对"半结构化字段多、查询模式不固定"的业务,这一点的收益非常直接。
事务与并发控制
三者都实现了 MVCC,但默认隔离级别与失败行为不同,这是迁移时最容易踩坑的地方。
| 维度 | PostgreSQL | MySQL(InnoDB) | Oracle |
|---|---|---|---|
| 默认隔离级别 | READ COMMITTED | REPEATABLE READ | READ COMMITTED |
| 快照建立时机 | 每条语句开始时(READ COMMITTED) | 事务中第一次读时 | 每条语句开始时 |
| 唯一约束冲突 | 事务整体回滚,需应用重试 | 语句失败,可重试单条语句 | 语句级回滚 |
| 写-写冲突 | 后到者等待,可能触发序列化失败 | 行锁等待,超时回滚 | 行锁等待 |
| 串行化实现 | SSI(可序列化快照隔离,9.1+) | 基于间隙锁的模拟 | 基于行锁的模拟 |
-- 显式提升隔离级别(PostgreSQL 语法)
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
COMMIT;
-- 或只在当前事务内设置
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 串行化下冲突会抛 40001,应用层必须实现重试
COMMIT;PostgreSQL 的 READ COMMITTED 有一个与 Oracle 相似、与 MySQL 不同的行为:同一事务内两次执行相同查询,可能看到不同结果(因为每条语句取新快照)。如果业务依赖"事务内读一致",必须显式升级到 REPEATABLE READ。
MySQL InnoDB 默认为 REPEATABLE READ,事务内读一致是"默认给到"的,迁移到 PostgreSQL 时这里往往是隐性 bug 的来源。反过来,PostgreSQL 的 REPEATABLE READ 是通过快照实现的,不会像 MySQL 那样出现间隙锁导致的额外死锁。
性能特点
三者的性能特征差异来自架构选择:
- PostgreSQL:单条复杂查询的优化器更强,支持 Hash Join、Merge Join、并行查询、JIT 编译、增量排序、B-tree 跳跃扫描(18+)。对分析型混合查询(HTAP)友好。写路径上每行更新都会产生新元组版本,需要 VACUUM 回收,高更新频率场景的表膨胀(Bloat)必须监控。
- MySQL:短事务、简单主键查询的吞吐表现优秀,主从复制与中间件分库分表生态成熟。但多表关联与复杂子查询的优化器能力弱于 PostgreSQL,容易退化为嵌套循环。
- Oracle:大规模核心交易系统的稳定性与优化器成熟度经过长期验证,有 AWR、ASH、SQL Plan Management 等体系化诊断工具。代价是成本与生态锁定。
-- PostgreSQL 18+:B-tree 跳跃扫描让"非前导列"查询也能用上多列索引
-- PostgreSQL 18+
CREATE INDEX idx_orders_status_created ON orders (status, created_at);
-- 即使只过滤 created_at,也可能借助 Skip Scan 使用该索引
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE created_at > now() - interval '1 day';-- PostgreSQL 14+:并行查询对分析型语句收益明显
-- PostgreSQL 14+
SET max_parallel_workers_per_gather = 4;
EXPLAIN (ANALYZE)
SELECT status, count(*) FROM orders GROUP BY status;生态与工具链
- PostgreSQL:psql、pgAdmin、pg_dump/pg_restore、pg_upgrade、pg_basebackup 等官方工具齐全;扩展生态是最大亮点(PostGIS、pgvector、TimescaleDB、Citus、pg_partman、pg_stat_statements);所有主流云厂商提供托管(RDS、Aurora、Cloud SQL、Azure Database);ORM 与驱动覆盖全语言。
- MySQL:生态成熟度同样很高,工具链偏向"运维友好"(mysqldump、XtraBackup、gh-ost、pt-toolkit);在互联网公司积累了大量分库分表与中间件实践(ShardingSphere、Vitess、MyCat)。
- Oracle:企业级工具链最完善(RMAN、Data Guard、Enterprise Manager、GoldenGate),但工具与云服务绑定深,迁移出口成本高。
实战示例
下面是一个真实场景:某项目从 MySQL 迁移到 PostgreSQL,需要改写 DDL、修正隐式类型转换问题,并用上 PostgreSQL 独有的特性。
第 1 步:原 MySQL 表结构
-- 迁移前(MySQL 语法,仅作对照,不在 PostgreSQL 中执行)
-- CREATE TABLE `t_order` (
-- `id` BIGINT NOT NULL AUTO_INCREMENT,
-- `order_no` VARCHAR(64) NOT NULL,
-- `user_id` BIGINT NOT NULL,
-- `amount` DOUBLE NOT NULL,
-- `is_paid` TINYINT(1) NOT NULL DEFAULT 0,
-- `created_at` DATETIME NOT NULL,
-- PRIMARY KEY (`id`),
-- UNIQUE KEY `uk_order_no` (`order_no`)
-- ) ENGINE = InnoDB;第 2 步:改写为 PostgreSQL DDL
-- demo 库中执行
CREATE TABLE t_order (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_no TEXT NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
amount NUMERIC(12, 2) NOT NULL CHECK (amount >= 0), -- 金额禁止用 FLOAT/DOUBLE
is_paid BOOLEAN NOT NULL DEFAULT false, -- TINYINT(1) → BOOLEAN
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- DATETIME → TIMESTAMPTZ
remark TEXT,
attrs JSONB NOT NULL DEFAULT '{}' -- 新增:半结构化扩展字段
);改写要点对照:
| MySQL 写法 | PostgreSQL 写法 | 原因 |
|---|---|---|
AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY | 标准 SQL,避免序列与列脱钩 |
VARCHAR(64) | TEXT | 长度约束对性能无收益,改为 CHECK 表达业务规则 |
DOUBLE 存金额 | NUMERIC(12, 2) | 二进制浮点有舍入误差,金额必须精确 |
TINYINT(1) | BOOLEAN | 语义明确,避免 0/1 与 true/false 混用 |
DATETIME | TIMESTAMPTZ | 带时区存储,避免跨时区歧义 |
| 反引号 | 无引号或双引号 | PostgreSQL 反引号非法 |
第 3 步:准备数据
INSERT INTO t_order (order_no, user_id, amount, is_paid, attrs) VALUES
('SO-20260901', 1001, 199.00, true, '{"channel": "app", "coupon": "C10"}'),
('SO-20260902', 1002, 59.90, false, '{"channel": "web"}'),
('SO-20260903', 1001, 520.00, true, '{"channel": "app", "gift": true}');第 4 步:用 RETURNING 一次拿到写入结果(MySQL 需再查一次)
INSERT INTO t_order (order_no, user_id, amount)
VALUES ('SO-20260904', 1003, 88.00)
RETURNING id, order_no, created_at; id | order_no | created_at
----+-------------+-------------------------------
4 | SO-20260904 | 2026-09-21 10:32:41.517823+08
(1 row)第 5 步:用 JSONB 做条件筛选
SELECT order_no, amount, attrs ->> 'channel' AS channel
FROM t_order
WHERE attrs @> '{"channel": "app"}'::jsonb
ORDER BY amount DESC; order_no | amount | channel
-------------+---------+---------
SO-20260903 | 520.00 | app
SO-20260901 | 199.00 | app
(2 rows)第 6 步:聚合校验迁移前后数据是否一致
SELECT
count(*) AS order_count,
count(*) FILTER (WHERE is_paid) AS paid_count, -- FILTER 是标准 SQL 聚合过滤
sum(amount) AS total_amount
FROM t_order; order_count | paid_count | total_amount
-------------+------------+--------------
4 | 2 | 866.90
(1 row)第 7 步:迁移完成后的回归检查
# 导出 MySQL 侧数据并在 PostgreSQL 侧比对行数与关键聚合值
psql -d demo -c "SELECT count(*) FROM t_order;"
psql -d demo -c "SELECT sum(amount) FROM t_order;"
# 用 pg_stat_statements 排查迁移后出现的高耗时 SQL
psql -d demo -c "SELECT queryid, calls, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;"注意事项
- 不要直接照搬 MySQL 的"列长度限制"习惯。PostgreSQL 的
TEXT与VARCHAR(255)性能完全相同,长度约束只应表达业务规则而非存储优化。
-- 不推荐:用 VARCHAR 长度做"性能优化"
CREATE TABLE t_bad (name VARCHAR(255), remark VARCHAR(255), note VARCHAR(255));
-- 推荐:需要业务约束时显式声明,且用 CHECK 表达更复杂的规则
CREATE TABLE t_good (
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 64),
remark TEXT
);- 金额、比例等精确值绝不能用浮点类型。MySQL 用
DOUBLE、PostgreSQL 用REAL/DOUBLE PRECISION存金额都会引入误差。
-- 错误!浮点存金额会累积舍入误差
CREATE TABLE t_pay_bad (amount DOUBLE PRECISION);
INSERT INTO t_pay_bad VALUES (0.1), (0.2);
SELECT sum(amount) FROM t_pay_bad; -- 结果 0.30000000000000004,而非 0.3
-- 正确:使用 NUMERIC
CREATE TABLE t_pay_good (amount NUMERIC(12, 2));
INSERT INTO t_pay_good VALUES (0.1), (0.2);
SELECT sum(amount) FROM t_pay_good; -- 结果 0.30- 小心 MySQL 的隐式类型转换习惯。MySQL 允许
WHERE varchar_col = 123这类隐式转换;PostgreSQL 更严格,会直接报错,因为它无法为字符串列使用数字索引。
-- 错误!PostgreSQL 中会报 operator does not exist: text = integer
-- SELECT * FROM t_order WHERE order_no = 20260901;
-- 正确:显式写成字符串字面量
SELECT * FROM t_order WHERE order_no = '20260901';- 不要在 PostgreSQL 中依赖事务内读一致性而不改隔离级别。默认
READ COMMITTED下,同一事务的两条SELECT之间若有其他事务提交,第二次会看到新数据。 - 不要用
LIMIT代替 "Top-N" 的业务保证。三者都要求ORDER BY与LIMIT搭配,否则顺序未定义。MySQL 的LIMIT offset, count与 PostgreSQL 的LIMIT count OFFSET offset语义相同但语法顺序相反,迁移时容易写错。 - 对比"性能"时必须先明确负载类型。拿 OLTP 短查询吞吐去比较 PostgreSQL 与 MySQL,与拿复杂分析查询去比较,结论会完全相反。脱离负载画像的性能结论没有意义。
- 不要低估迁移成本。真正的成本不在 DDL 改写,而在存储过程与触发器改写、驱动与 ORM 方言适配、SQL 语义差异(隐式转换、大小写、空字符串与 NULL 的区分)、运维工具链替换。
最佳实践
- 先做负载画像再做选型:写出"读/写比、单条 SQL 复杂度、数据量级、并发连接数、是否需要地理空间/向量/时序"这几个问题的答案,再对照上表判断。
- 需要复杂查询、多模能力、无授权风险时优先 PostgreSQL:它几乎没有"能力天花板",后续需求变化时不必换库。
- 团队已有大量 MySQL 运维积累、业务以简单高并发写入为主时,MySQL 仍是合理选择:不必为了"技术先进"而迁移。
- Oracle 的替换决策应从成本与合规出发:把许可、选件、硬件、运维人力折算成 TCO,再与 PostgreSQL 的迁移成本对比。
- 新项目默认统一到 PostgreSQL:可以显著减少技术栈数量,降低跨库 SQL 方言带来的认知与维护成本。
- 迁移用逻辑复制做增量同步,而不是停机
mysqldump。先用工具(如 pgloader)做全量,再用逻辑复制追增量,最后切换。 - 迁移后必须做数据一致性校验:逐表比对行数、关键字段的
sum/count聚合值、以及核心业务报表结果。 - 不要为了"兼容性"在 PostgreSQL 里模拟其他数据库的方言。例如把
JSON当字符串处理、用text模拟TINYINT。用原生写法才能拿到该数据库设计上的收益。 - 把选型结论写成 ADR(架构决策记录):注明负载假设、候选方案、放弃理由与复审触发条件,避免同一问题反复讨论。
下一节
继续学习:版本选择建议(LTS / 最新版)