PostgreSQL学习笔记(入门-进阶-精通说明)

PG 的 SQL 没什么门槛,写过 MySQL 的人半天就能上手。真正卡人的在:建完表查询报 relation "xxx" does not exist;删了十万行数据,表反而更大了;压测刚起来,连接数还没到一百,内存先满了。这三个现象分别指向对象模型、VACUUM 和进程模型。搞不清它们,PG 就只是个"能连上的库"。

下面按这个顺序写:装库、把概念弄对、讲 SQL 和索引、说并发与存储,最后落到和 MySQL、Oracle 的对比。赶时间可以跳着看。只想知道能不能替掉 MySQL,直接翻第五章;已经在用 PG、被膨胀或者慢查询卡住的,看 4.1、4.2 和 3.3;完全没碰过,从头读,前三章够用很久。

1. 分析PG适合你吗

如果你的业务是标准的 Web 读写、SQL 不复杂、团队里也没人写过 PG,那 MySQL 更省事。这不代表 PG 弱,只是它的长处你用不上,而它的短板------连接池、VACUUM、运维门槛。

反过来,下面这些事在 PG 里通常一条 SQL 就够,在 MySQL 里得建中间表,或者把逻辑搬回应用层。

  • **按属性过滤半结构化数据。**jsonb 加一个 GIN 索引,where attrs @> '{"channel":"app"}' 直接走索引。MySQL 的 JSON 多数情况只能靠函数索引兜一小部分。

  • **分组取最新一条、累计、排名。**DISTINCT ON、窗口函数、LATERAL 都在 SQL 层解决,不用先 group by 再 join 回来。

  • **检索和业务数据放一个库。**pgvector、PostGIS 都是扩展,装完能和普通表 JOIN,不用再维护一套外部系统。

  • **大表按时间切段、整段删除。**声明式分区,DROP PARTITION 比 DELETE 干净得多。

短板也摆在这。连接是进程模型,不配 PgBouncer,几百个连接就能把内存吃穿;表只增不减这件事必须靠 VACUUM 和 pg_repack 处理;国内云厂商的支持力度和招人难度,仍然不如 MySQL;单机写入吞吐要打平调优过的 MySQL,得花不少功夫。

所以它更适合数据模型还会变、查询复杂度说不准、或者需要把检索、地理、向量塞进同一个库的场景。一个结构稳定、以高并发简单读写为主的系统,用 PG 属于给自己找事。

2. 入门安装弄清概念

2.1 安装

练手用 Docker,生产用官方源。截至 2026 年 11 月,生产建议 18.x(当前小版本 18.6),19 还在 RC,等正式发布后跑过一两个小版本再上。

复制代码
# 本地练手
docker run -d --name pg \
  -e POSTGRES_PASSWORD=secret \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  postgres:18

# Debian / Ubuntu
sudo apt install -y postgresql-18 postgresql-client-18
sudo systemctl enable --now postgresql

连上去先确认两件事:版本和编码。select current_setting('server_encoding') 应该是 UTF8。如果显示 SQL_ASCII,中文的排序和比较迟早出问题,而且建库之后再改很麻烦。

元命令不用背,\? 会列出全部。下面这几条是日常真会用的。

命令 什么时候用
\l / \c dbname 列出所有库 / 切库。跨库不能直接 JOIN,先切过去是最省事的办法
\dn / \dt 列模式 / 列当前 search_path 下的表。看不到表的时候先查 \dn
\d+ tablename 看结构、索引、触发器、所属模式、磁盘占用。排障第一个该敲的命令
\du / \df 查角色和权限 / 查函数。权限问题、扩展装了哪些函数都在这
\x / \timing 结果转纵向,宽表必用 / 打开计时

顺手养成一个习惯:任何"表不见了""权限不对"的问题,先 \d+ 表名 和 \dn,比翻文档快得多。

2.2 库、模式、用户

层级是实例 → 数据库 → 模式 → 表。两个硬约束:跨数据库不能直接 JOIN,要用 postgres_fdw 或 dblink;同一个库里跨模式可以随便 JOIN。

这一层三家的定义完全不同,迁移事故基本都出在这。

层级 PostgreSQL MySQL Oracle
实例 cluster,一个实例下多个库 一个实例下多个 database 一个实例(RAC 可多实例),下含多个 schema
中间层 schema 是库里的命名空间,和用户解耦 没有独立 schema,database 就是 schema schema 就是用户名,用户与 schema 绑定
跨层查询 跨 schema 可 JOIN,跨库要 FDW 跨库要应用层或中间件 同实例跨 schema 可 JOIN
常见用法 按租户建 schema,共享一套表结构 按租户建 database,物理隔离 按业务建用户,用户即 schema
复制代码
# 多租户:一个 schema 一套表
CREATE SCHEMA tenant_a;
SET search_path = tenant_a, public;

CREATE TABLE orders (
  id     bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  amount numeric(12,2) NOT NULL
);

search_path 是"不写前缀时去哪找对象"。方便,也是攻击面:别把 public 模式的 CREATE 权限开给普通用户,否则可能被塞一个同名函数劫持调用。PG 15 起默认已经收紧了这块。

然后是这个最容易被误判的报错: ERROR: relation "Orders" does not exist

表确实在,只是名字被折成了 orders。PG 把不加引号的标识符折成小写,Oracle 折成大写,MySQL 看 lower_case_table_names。从 Oracle 迁过来的库,工具常为了保留原名加双引号,结果代码里必须处处带引号------迁移后"表不存在",十有八九是这个。

复制代码
CREATE TABLE Orders (id int);      -- 对象名是 orders,查询写 Orders 也命中
CREATE TABLE "Orders" (id int);    -- 对象名就是大写 Orders,查询必须带引号

2.3 字段类型对比

用途 PostgreSQL MySQL Oracle 注意避坑
整数 integer / bigint INT / BIGINT NUMBER(10) / NUMBER(19) PG 没有 unsigned,需要时加 CHECK
自增 GENERATED AS IDENTITY AUTO_INCREMENT IDENTITY 列或 SEQUENCE serial 是伪类型,新项目统一 identity
字符串 text / varchar(n) VARCHAR / CHAR / TEXT VARCHAR2 / CHAR / CLOB text 没有长度限制,不必刻意限长
布尔 boolean TINYINT(1) 没有原生布尔,用 NUMBER(1) 0/1 和 true/false 得转换
时间 timestamptz / date DATETIME / TIMESTAMP DATE(带时分秒)/ TIMESTAMP Oracle 的 DATE 带时间,不能直接映成 date
精确小数 numeric DECIMAL NUMBER 金额一律 numeric,别用 float
JSON jsonb(可索引) JSON(8.0 支持函数索引) JSON(21c 起)/ XMLType 只有 PG 能整体建 GIN 索引
数组 int\[\]、text\[\] 没有,用 JSON 模拟 VARRAY / 嵌套表 PG 数组能 ANY 查询并建 GIN
UUID uuid 没有原生,CHAR(36) 或 BINARY(16) RAW(16) PG 18 起内置 uuidv7(),时间有序,适合做主键
枚举与范围 enum、int4range、tsrange ENUM(改值要重建表) 没有对应 范围类型配 GiST 能做区间重叠

真正拉开差距的是 jsonb、数组、范围类型这几个:它们能被索引、能被约束、能参与运算,不是拿字符串凑合出来的。

复制代码
INSERT INTO orders (user_id, amount, attrs)
VALUES (1, 99.00, '{"channel":"app","tags":["new","promo"]}');

SELECT id, attrs->>'channel' AS channel, attrs->'tags' AS tags
FROM orders
WHERE attrs @> '{"tags":["promo"]}'::jsonb;   -- 包含判定,可走 GIN

CREATE INDEX idx_orders_attrs ON orders USING GIN (attrs jsonb_path_ops);

但别把 jsonb 当 schema 用。要外键、要范围查询、要高频率过滤的字段,老老实实建列;jsonb 留给结构不稳定、通常整块读的属性,比如渠道扩展字段、事件 payload。核心业务字段全塞进去,等于把优化器的统计信息扔了。

2.4 正式建表

复制代码
CREATE TABLE orders (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id     bigint        NOT NULL,
  amount      numeric(12,2) NOT NULL CHECK (amount >= 0),
  status      text          NOT NULL DEFAULT 'pending'
                            CHECK (status IN ('pending','paid','shipped','closed')),
  attrs       jsonb         NOT NULL DEFAULT '{}'::jsonb,
  created_at  timestamptz   NOT NULL DEFAULT now(),
  total_cents bigint GENERATED ALWAYS AS (round(amount * 100)::bigint) STORED
);

CREATE INDEX idx_orders_user_time ON orders (user_id, created_at DESC);

用 identity 不用 serial。serial 是个带默认值的伪类型,顺手给你建个序列,还允许应用往里写;identity 是标准实现,GENERATED ALWAYS 能挡住脏写。

时间统一 timestamptz。它内部存 UTC、按会话时区显示,跨时区部署不会错;用 timestamp 存"有时区含义"的时间,八小时偏差是迟早的事。

生成列 STORED 在写入时算好落盘,适合分转元这种确定性派生字段,省得每次查询重算一遍。

3. 进阶PG的写法

3.1 三条能省一半代码的语法

窗口函数、LATERAL、DISTINCT ON。这三样在 MySQL 里要么没有,要么得绕一大圈。

复制代码
SELECT user_id,
       id,
       amount,
       sum(amount) OVER (PARTITION BY user_id
                         ORDER BY created_at
                         ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_amount,
       row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders;

它和 GROUP BY 的区别是保留明细行,所以能同时输出"这一单"和"这个用户到现在的累计"。执行计划里是 WindowAgg,通常带一次 Sort,索引往这个方向调能省掉排序。

复制代码
SELECT u.id, u.name, t.order_id, t.amount
FROM users u
LEFT JOIN LATERAL (
  SELECT id AS order_id, amount
  FROM orders
  WHERE user_id = u.id
  ORDER BY created_at DESC
  LIMIT 1
) t ON true;

LATERAL 让右边的子查询能引用左边的列,等于每行跑一次。"分组取最新一条"用它最干净。MySQL 要到 8.0.14 才支持。

用错的地方是拿它替代普通 JOIN:驱动行数一大,子查询次数跟着翻,性能直接塌。判断标准很简单,子查询里有 LIMIT 或聚合才用它。

复制代码
-- 每个用户最新一单,比 row_number 子查询短
SELECT DISTINCT ON (user_id) user_id, id, created_at
FROM orders
ORDER BY user_id, created_at DESC;

-- 一次算出两个口径,比 CASE WHEN 清楚
SELECT status,
       count(*)                               AS orders,
       count(*) FILTER (WHERE amount >= 100)   AS big_orders,
       sum(amount)                            AS gmv
FROM orders
GROUP BY status;

3.2 六种索引

选错索引类型的代价不是慢一点,是根本用不上。

类型 擅长的查询 PG 里用在哪 另两库对应
B-tree 等值、范围、排序、前缀匹配 默认类型。主键、外键、时间列、复合索引 MySQL B+Tree、Oracle B-tree
Hash 只有等值 大表精确匹配且不排序,用得少 MySQL Memory、Oracle Hash Cluster
GIN 多值包含、倒排 jsonb、数组、tsvector 全文 MySQL 全文索引、Oracle Text
GiST 几何、范围、近邻 PostGIS 空间查询、区间重叠 Oracle Spatial(R-tree)
SP-GiST 非平衡空间划分 点数据、前缀树、四叉树 没有直接对应
BRIN 块级摘要,超大表范围扫描 日志、时序表按时间过滤,体积极小 没有直接对应
复制代码
-- 复合索引 + 覆盖列,避免回表
CREATE INDEX idx_orders_user_time ON orders (user_id, created_at DESC) INCLUDE (amount);

-- 部分索引:只给待处理的数据建,体积小一个数量级
CREATE INDEX idx_orders_pending ON orders (created_at) WHERE status = 'pending';

-- 表达式索引:让函数条件也能走索引
CREATE INDEX idx_orders_month ON orders ((date_trunc('month', created_at)));

-- 近邻检索(向量扩展)
CREATE INDEX idx_docs_embedding ON documents
  USING hnsw (embedding vector_cosine_ops);

这里有个容易忽略的差异。PG 是堆表,所有索引都是二级索引,主键也不例外,数据行不按主键排。InnoDB 反过来,主键就是数据本身,二级索引里存主键值。这个差异直接改主键设计:InnoDB 要用短且递增的主键避免页分裂;PG 不在乎主键连不连续,在乎你真正查询的列有没有索引。

代价在另一头:PG 里按主键范围扫一张大表并不天然快,因为数据散在堆里。

索引也不是越多越好。每个索引写入时都要维护,还要参与 VACUUM,索引膨胀和表膨胀一样真实。单表控制在五个以内,定期查 pg_stat_user_indexes 的 idx_scan,把从没被扫过的删掉。

3.3 看执行计划,别猜

复制代码
ANALYZE orders;   -- 先保证统计信息是新的

EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT user_id, sum(amount)
FROM orders
WHERE created_at >= now() - interval '30 days'
GROUP BY user_id
ORDER BY 2 DESC
LIMIT 20;

只跑 EXPLAIN 不给 ANALYZE,看的还是优化器的猜测。加上 BUFFERS 才能看到每个节点实际读了多少页------这是区分"索引不对"和"缓存不够"的关键。

节点 含义 看到它时想什么
Seq Scan 全表顺序扫描 小表正常。大表且过滤性强,说明缺索引,或者索引没被选中
Index Scan / Index Only Scan 走索引并回表 / 只读索引 Index Only 的 Heap Fetches 偏高,说明 visibility map 脏,该 VACUUM 了
Bitmap Heap Scan 先建位图再批量回表 中等选择性、多条件组合时的正常形态,不用强行改成 Index Scan
Nested Loop 嵌套循环连接 小结果集驱动大表最优。驱动行数一大就是灾难,去看上层 rows 估算
Hash Join / Merge Join 哈希 / 归并连接 大表连接的主要形态,通常是对的
Sort 排序 出现 external merge 说明 work_mem 不够。能靠索引顺序省掉最好
Gather 并行结果汇总 并行查询在工作,注意 workers 数和 work_mem 的乘积

顺序固定下来最省时间。先看估算行数和实际行数差多少,差一个数量级就先 ANALYZE,别急着改 SQL;再看 shared read 占比;最后看哪个节点的 actual time 最高,那才是贵的。cost 数值只能横向比同一份计划里的两个方案,别当绝对值看。

另一个高频问题是深分页。OFFSET 100000 意味着前面十万行白扫,改成 keyset:where id > 上一页最后一个 id。

4. 精通并发和存储才是分水岭

前三章的东西,认真读文档一周能会。这一章这几件事,是 PG 和另外两个库真正分开的地方,也最容易在生产上翻车。

4.1 MVCC:为什么 PG 不用 undo log

PG 的 MVCC 是物理多版本。行头记 xmin(谁创建的)和 xmax(谁删的),UPDATE 实际是写一条全新的行,旧版本留在堆里等清理,DELETE 只是打标记。可见性靠事务开始时的快照判断。

InnoDB 是行内放 trx_id 和回滚指针,旧版本进 undo log,由 purge 线程清;Oracle 用 undo 段配 SCN 构造一致性读。三者都能做到读不阻塞写,代价落在不同地方:PG 落在表空间和 VACUUM,MySQL 落在 undo 表空间,Oracle 落在 undo 表空间和 ORA-01555。

这条推论很实用:长事务、长查询、开着不动的游标,都会钉住最老的快照,让 VACUUM 无法回收它之后的死元组。PG 的头号纪律不是调参,是别让事务活太久。idle_in_transaction_session_timeout 一定要设。

隔离级别 PostgreSQL MySQL Oracle
Read Uncommitted 不支持,等同 Read Committed 支持 不支持
Read Committed 默认,每条语句取新快照 支持 默认
Repeatable Read 快照隔离,没有幻读;写冲突报错而不是等锁 默认,靠 next-key lock 防幻读 不支持,用 Serializable 或闪回查询替代
Serializable SSI 实现,冲突抛序列化失败,应用必须重试 加锁实现,阻塞多 加锁实现

两个实际影响。PG 默认是 Read Committed,MySQL 默认 Repeatable Read,从 MySQL 迁过来时,依赖 RR 语义的逻辑要重判。PG 的可串行化用 SSI,不加锁,冲突时抛 40001 让你重试------应用不实现重试,高并发下会莫名其妙地失败。

复制代码
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- 业务操作
COMMIT;

4.2 VACUUM:不做这件事,表只会越来越大

UPDATE 留旧版本,就得有人清,这个动作是 VACUUM。它回收死元组占用的空间给后面复用、更新 visibility map 让 Index Only Scan 生效、顺带更新统计信息。注意回收的空间是留在表内复用,文件不会变小------这就是膨胀。

复制代码
SELECT relname,
       n_live_tup,
       n_dead_tup,
       round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0), 3) AS dead_ratio,
       last_autovacuum,
       last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

autovacuum 默认要等死元组超过 20%、且绝对数超过 50 行才动。大表等到触发时,通常已经很难看了。写入频繁的表按表下调。

复制代码
ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor  = 0.02,
  autovacuum_analyze_scale_factor = 0.05
);

空间真要还回去,别用 VACUUM FULL:它拿排他锁重写整表,生产上一执行就是停服。用 pg_repack 在线重建,或者干脆按时间分区、DROP PARTITION 整段释放。日志表用分区,主要就是为了这个。

4.3 分区、复制、高可用

分区解决两件事:大表切成能管的段,以及过期数据整段删掉,而不是产生一堆死元组。PG 10 之后是声明式分区,RANGE、LIST、HASH,支持子分区。

复制代码
CREATE TABLE events (
  id         bigint      NOT NULL,
  created_at timestamptz NOT NULL,
  payload    jsonb
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_10 PARTITION OF events
  FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');

CREATE TABLE events_2026_11 PARTITION OF events
  FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');

DROP TABLE events_2026_09;   -- 整段释放,不产生死元组

两个限制得提前知道。分区表上的唯一索引必须包含分区键,否则保证不了全局唯一。分区表没有全局索引,跨分区查询靠分区裁剪收敛扫描范围,所以 WHERE 里一定要带分区键。

复制是两条路线。物理流复制传 WAL,整实例级别,主备完全一致,用来做高可用和读写分离;逻辑复制是表级发布订阅,能跨大版本、能只同步部分表,但 DDL 和序列不同步。

复制代码
-- 发布端,wal_level 需为 logical,改完重启
CREATE PUBLICATION pub_orders FOR TABLE orders;

-- 订阅端
CREATE SUBSCRIPTION sub_orders
  CONNECTION 'host=10.0.0.12 port=5432 dbname=app user=repl password=***'
  PUBLICATION pub_orders;

4.4 PG 的能力边界在哪

MySQL 的能力边界由存储引擎定,Oracle 由版本和付费选件定,PG 由你装了哪些扩展定。这是三者最不像的地方,也是 PG 最有意思的地方。

扩展 给什么能力 什么时候装
pg_stat_statements 按 SQL 归一化统计调用次数、耗时、行数、缓存命中 生产必装,没有它就没法做性能分析
pgvector vector 类型 + HNSW / IVFFlat 索引 RAG、语义检索,省掉一套外部向量库
PostGIS 几何类型、空间索引、空间函数 地理围栏、路径规划、门店范围
TimescaleDB 按时间自动切 hypertable 指标、监控、IoT 时序
Citus 分布式表、分片、查询下推 单机容量到顶之后的水平扩展
pg_partman 分区自动创建与清理 日志、事件表的长期维护
pg_repack 在线重写表,回收膨胀 VACUUM FULL 之外唯一实用的选择
postgres_fdw 把远程库的表映射成本地外部表 分库联合查询、冷热数据分离

装之前想两件事:扩展跟不跟得上 PG 大版本,以及它坏的时候谁来修。pgvector、PostGIS 这类有稳定维护者的可以放心用,小众扩展要掂量一下。

4.5 参数和监控,先改这六个

默认值是按"能在一台笔记本上跑起来"设的,直接上生产会明显浪费机器。先改这几个,剩下的按压测结果调。

参数 建议起点 为什么
shared_buffers 内存的 25% PG 自己的缓存,要重启。再往上不会线性变好,剩下的留给 OS 做文件缓存
effective_cache_size 内存的 50%--75% 不占内存,只是告诉优化器"大概有多少缓存可用"。设小了它会误判,不肯走索引
work_mem 4--16 MB 每个排序或哈希节点单独占,按并发放大。别按"连接数 × work_mem"直接调
maintenance_work_mem 256 MB--1 GB 影响 VACUUM 和 CREATE INDEX 的速度,调大收益很明显
max_connections 100--200 一个连接一个进程,必须配 PgBouncer 的 transaction 模式。加连接解决不了并发
random_page_cost SSD 设 1.1 默认 4.0 是按机械盘定的,SSD 上不调,优化器会过度偏好全表扫描
复制代码
SELECT calls,
       round(total_exec_time::numeric, 1) AS total_ms,
       round(mean_exec_time::numeric, 2)  AS mean_ms,
       rows,
       left(query, 80) AS query_sample
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

监控至少盯四组:连接数和活跃会话(重点抓 idle in transaction)、缓存命中率、死元组与膨胀、复制延迟。这四组出问题,基本就是故障前兆。

5. 横向对比 MySQL&Oracle

5.1 MySQL:同一个 OLTP,两种做法

简单 CRUD 上两家都能跑,差异要等数据量、并发量和查询复杂度上来才显形。

维度 PostgreSQL MySQL(InnoDB)
进程模型 多进程,一连接一后端进程,开销大,必须池化 单进程多线程,连接开销小,高并发仍要池化
默认隔离 Read Committed Repeatable Read
MVCC 堆内多版本 + VACUUM 清理,代价是表膨胀 undo log + purge 线程,代价是 undo 空间涨
表与主键 堆表,所有索引都是二级索引 主键即聚簇索引,二级索引存主键值
锁与幻读 行级锁,没有间隙锁,快照隔离下不出幻读 next-key lock(行锁 + 间隙锁),RR 靠它防幻读
DDL 事务性 完全事务化,ALTER 失败能回滚 8.0 起原子 DDL,跨语句组合仍不是事务
索引能力 六种类型,部分索引、表达式索引、INCLUDE 覆盖 以 B+Tree 为主,辅以全文和多值索引,没有部分索引
JSON jsonb 可整体建 GIN,运算符完整 JSON 类型,8.0 支持函数索引和多值索引
复制 物理流复制 + 逻辑复制,可跨版本、可选表 binlog 复制,异步 / 半同步 / MGR
分区 声明式 RANGE / LIST / HASH,无全局索引 原生 RANGE / LIST / HASH / KEY,生态成熟
扩展性 极强。扩展、自定义类型、FDW 都是内置能力 受存储引擎和插件体系限制
运维门槛 较高。VACUUM、连接数、复制都得主动管 较低。国内云厂商和人才储备更厚
许可 PostgreSQL License(类 BSD,可闭源分发) GPL v2;商业版另有 Oracle 双许可

取舍就一句:要简单、要极致写入吞吐、要生态和招人方便,选 MySQL;要复杂查询、丰富类型、强扩展、混合负载,选 PG。

从 MySQL 迁过来,四个坑最多。无符号整数没有对应类型,得加 CHECK;ON DUPLICATE KEY UPDATE 要改成 INSERT ... ON CONFLICT DO UPDATE;间隙锁没了,依赖它做并发控制的逻辑要重审;字符串比较的大小写敏感性、NULL 排序规则都不一样。

5.2 Oracle:能力强,账也算得清

管 PG 叫"开源 Oracle",在能力上成立,在生态上不成立。PL/pgSQL 大量借鉴 PL/SQL,分区、并行、逻辑复制、行级安全这些都有,但 RAC、Data Guard、AWR 那一套,PG 没有等价物。

维度 PostgreSQL Oracle Database
授权成本 开源免费,商业支持可选,主要成本是人力 按处理器或命名用户授权,核心数和选件都会推高成本
架构 多进程 + 共享内存,单机为主,靠 Citus 做分布式 实例 + SGA/PGA + 后台进程,RAC 可多实例共享存储
SQL 方言 高度遵循标准 SQL,扩展语法自成体系 自有方言很强:ROWNUM、DUAL、CONNECT BY、MODEL
过程语言 PL/pgSQL,另有 Python、Perl、Tcl PL/SQL,成熟度最高,包(Package)是独有能力
诊断体系 EXPLAIN、pg_stat_*、pg_stat_statements,偏自己分析 AWR / ASH / ADDM 成体系,工具链完善
高可用 流复制 + Patroni,逻辑复制,没有内置多活 Data Guard(物理/逻辑备库)、RAC、GoldenGate
分区与索引 声明式分区,分区表没有全局索引 分区成熟,支持全局索引和 Interval 分区
安全审计 行级安全 RLS、列权限、SCRAM、TLS,18 起支持 OAuth 细粒度审计、TDE、Database Vault、Label Security
运维工具 pgBackRest、pgBadger、pg_repack,社区工具为主 RMAN、Data Pump、OEM,官方闭环

从 Oracle 迁过来,语言层比想象容易,语义层比想象难。下面五处是最常见的事故源。

  • **空字符串就是 NULL。**Oracle 里 '' 等于 NULL,PG 里 '' 是合法的空串。所有依赖"空串即空值"的判断、唯一约束和业务逻辑都得重写,这是第一大坑。

  • **DATE 不是同一个 DATE。**Oracle 的 DATE 带时分秒,PG 的 date 只有年月日,对应的是 timestamp。字段映错就直接丢时间精度。

  • **行号和层级查询。**ROWNUM 改成 LIMIT/OFFSET 或 row_number();CONNECT BY 改写成 WITH RECURSIVE;DUAL 直接去掉。

  • **包和重载。**PL/SQL 的 Package 在 PG 里没有对应物,一般拆成一个 schema 加一组函数,状态变量要重新设计。

  • **标识符大小写。**Oracle 折大写,PG 折小写。迁移工具为了保名常加双引号,结果代码必须处处带引号,否则就是"表不存在"。

5.3 选型与迁移:一张表和三句实话

场景 选谁 理由
简单高并发 OLTP,SQL 不复杂 MySQL 生态成熟、云厂商支持好、招人容易,调优路径清晰
复杂报表、多表关联、窗口函数密集 PostgreSQL 优化器和查询能力更强,很多逻辑不用拆到应用层
半结构化数据,要按属性过滤 PostgreSQL jsonb + GIN 是三者里唯一成熟的方案
向量检索、RAG 知识库 PostgreSQL pgvector 能和业务表同库 JOIN,少维护一套系统
地理围栏、轨迹、门店范围 PostgreSQL PostGIS 是事实标准
已有核心 ERP,要 RAC 多活和完善审计 Oracle Data Guard、RAC、审计合规没有等价替代
海量时序指标和日志 PostgreSQL 分区 + TimescaleDB + BRIN 的组合成本最低
预算敏感,或者要闭源分发 PostgreSQL 类 BSD 许可,商用没有授权限制

新项目、模型还会变、查询复杂度说不准,优先 PG。已有成熟 MySQL 体系且业务简单,没有理由为了技术偏好迁。Oracle 只在既有系统、强合规或者需要 RAC 多活的场景继续持有,新建系统要先把十年授权账算清楚。

6. 学习顺序

不用按"先读文档再看源码"来。按踩坑成本排,大致是这样。

**第一周。**装库、psql 元命令、建表建约束、pg_dump 备份再恢复。过关标准是恢复出来的库能直接跑。

**第二到三周。**窗口函数、LATERAL、DISTINCT ON 各写五条;同一张表设计四种索引;每条 EXPLAIN 能讲出优化器为什么选这个计划。

**第四到六周。**按月分区、一主一备流复制、PgBouncer、pg_stat_statements 加四组监控。过关标准是一次主备切换做完,应用没感觉。

**两个月以后。**pgbench 压测调参、复现一次膨胀并用 pg_repack 修、做一次跨库迁移评审。到这一步才算能拍板,而不是"加个索引试试"。

五个误区顺手记一下:把 PG 当 MySQL 用,不配连接池也不调 autovacuum;觉得索引越多越好;业务里跑着跨小时的长事务;分页用 OFFSET 翻深页;上线之后从不对大表 ANALYZE。

真要上手,别从复制和调优开始。找一张五百万行的表,按 3.3 的办法把它从两秒优化到两百毫秒,比读十篇长文都管用。

相关推荐
承渊政道35 分钟前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
橙橙笔记2 小时前
具身智能的学习 - 完成一个机械臂 第1节
python·学习·机械臂·具身智能
成都蜀味缘餐饮技术培训学校2 小时前
手撕烤兔技术培训实战指南,秘制腌料配方比例与调制手法
大数据·经验分享·笔记·其他·微信公众平台
希和20273 小时前
免费AI视频生成工具汇总(国内优先,适合做漫剧、短视频,Obsidian笔记版)
人工智能·笔记·音视频
蒸蒸yyyyzwd4 小时前
秋招学习笔记 day109
笔记·八股
Hhy_11075 小时前
《C++深度解构06》手撕 string —— 从底层内存管理到深浅拷贝彻底吃透
c语言·开发语言·c++·学习·visual studio
山甫aa6 小时前
锁一篇搞定:乐观锁、悲观锁、CAS、数据库锁与分布式锁【从零开始的 Web 后端学习】
java·数据库·分布式·后端·学习·web
m0_564876847 小时前
CMake学习
学习
只愿云淡风清7 小时前
正则表达式学习笔记 02:基础语法
笔记·学习·正则表达式