Files

5.8 KiB
Raw Permalink Blame History

id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
id title type domain stack tags level prerequisites est_hours related week order
backend.postgresql.index-tx-mvcc 索引、EXPLAIN、事务、隔离级别和 MVCC lesson backend
postgresql
索引
EXPLAIN
事务
隔离级别
MVCC
锁
intermediate
backend.postgresql.schema-sql
4
backend.postgresql.scenario.order-state-audit
backend.mysql.tx-locks-mvcc
backend.postgresql.interview
2
3
2

索引、EXPLAIN、事务、隔离级别和 MVCC

解释说明

索引

  • 默认是 B-tree,支持等值、范围、排序。其他类型:GIN(JSONB、数组、全文检索)、GiST(地理位置、范围类型)、BRIN(按时间顺序写入的超大表)、pgvector 的 HNSW / IVFFlat(向量)。
  • 联合索引按列顺序排序,查询条件要从最左列开始用(和 MySQL 一样)。
  • 部分索引:CREATE INDEX ... WHERE status = 'pending',只索引需要的行,体积小。
  • 表达式索引:CREATE INDEX ON users (lower(email)),查询时用同样的表达式才能命中。
  • 覆盖索引:INCLUDE (列) 把额外列放进索引叶子,查询可以只扫索引(Index Only Scan)。
  • 线上建索引用 CREATE INDEX CONCURRENTLY,不阻塞写入(不能放在事务里执行)。

EXPLAIN

  • EXPLAIN 看计划,EXPLAIN ANALYZE 真正执行并给出实际耗时和行数(对写语句要小心,会真的执行)。
  • 关注:Seq Scan(全表扫描)、Index Scan / Index Only Scan、Bitmap Heap Scan;预估行数(rows)和实际行数差很多时,说明统计信息过时,执行 ANALYZE 表名。

事务和隔离级别

  • PostgreSQL 默认 读已提交(Read Committed):每条语句看到语句开始时已提交的数据,所以同一事务里两次查询结果可能不同。
  • 可重复读(Repeatable Read):整个事务看到的是第一条语句开始时的快照;如果要修改的行已被别的事务改过,会报序列化错误,应用要重试。
  • 可串行化(Serializable):最严格,检测到可能的冲突时让事务失败,应用必须有重试逻辑。

MVCC

  • 每次更新不是原地修改,而是写一个新版本的行,旧版本对还在用旧快照的事务仍然可见。所以读不阻塞写、写不阻塞读。
  • 代价:旧版本(死元组)需要 VACUUM 清理(通常由 autovacuum 自动完成);长时间不结束的事务会让旧版本无法清理,导致表膨胀。

锁

  • SELECT ... FOR UPDATE 锁住选中的行,其他事务要修改或也想加锁时会等待;FOR UPDATE SKIP LOCKED 跳过已被锁的行——常用来实现“多个 worker 从任务表取任务”。
  • 乐观锁:表里加 version 列,UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?,影响行数为 0 就是被别人改过了。

案例

  • 订单列表按“用户 + 状态 + 时间”查询,建联合索引 (user_id, status, created_at DESC)。
  • 后台任务表多个 worker 并发取任务:SELECT id FROM jobs WHERE status = 'queued' ORDER BY id LIMIT 10 FOR UPDATE SKIP LOCKED,互不阻塞、不会重复领取。
  • 订单状态修改用乐观锁,避免“用户取消”和“支付回调”同时修改导致状态错乱(见场景文档)。

代码示例

CREATE TABLE jobs (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id    BIGINT NOT NULL,
  status     TEXT NOT NULL DEFAULT 'queued',
  payload    JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO jobs (user_id, status, created_at)
SELECT (random() * 1000)::int, (ARRAY['queued', 'running', 'done'])[1 + (random() * 2)::int],
       now() - (random() * interval '30 day')
FROM generate_series(1, 50000);

CREATE INDEX jobs_user_status_time ON jobs (user_id, status, created_at DESC);
CREATE INDEX jobs_queued ON jobs (id) WHERE status = 'queued';   -- 部分索引
ANALYZE jobs;

-- 命中联合索引
EXPLAIN ANALYZE SELECT * FROM jobs WHERE user_id = 42 AND status = 'done' ORDER BY created_at DESC LIMIT 20;
-- 只按 status 查:不满足最左前缀,通常是 Seq Scan(数据分布不同,计划可能不同)
EXPLAIN SELECT * FROM jobs WHERE status = 'done';

-- 多个 worker 取任务:在两个 psql 窗口里同时执行,互不阻塞、拿到不同的任务
BEGIN;
SELECT id FROM jobs WHERE status = 'queued' ORDER BY id LIMIT 5 FOR UPDATE SKIP LOCKED;
-- ……处理任务,然后:UPDATE jobs SET status = 'running' WHERE id IN (...);
COMMIT;

隔离级别的对比实验(两个 psql 窗口 A、B):

-- A
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM jobs WHERE status = 'queued';
-- B(自动提交)
UPDATE jobs SET status = 'done' WHERE id = (SELECT min(id) FROM jobs WHERE status = 'queued');
-- A:结果不变(快照);如果把 A 改成默认的 READ COMMITTED,结果会变
SELECT count(*) FROM jobs WHERE status = 'queued';
COMMIT;

易错点

  • 索引列上用函数或类型转换:WHERE date(created_at) = '2026-10-01' 用不上 created_at 的索引,改成范围条件 created_at >= ... AND created_at < ...。
  • LIKE '%关键词%' 用不上 B-tree 索引,需要 pg_trgm 的 GIN 索引或全文检索。
  • 索引越多越好:每个索引都会拖慢写入、占用空间、增加 VACUUM 工作量。
  • 长事务:事务里调用外部接口、等待用户输入,会长时间持有锁和旧快照。事务要短。
  • 可重复读 / 可串行化不写重试:序列化失败(SQLSTATE 40001)是正常现象,应用必须捕获并重试。
  • CREATE INDEX CONCURRENTLY 失败会留下无效索引(INVALID),要删掉重建。

面试常问

见 PostgreSQL 面试题:MVCC 和 VACUUM、隔离级别的默认值、SKIP LOCKED 的用途、乐观锁和悲观锁。