Files
zhuangang/topics/backend/postgresql/interview.md
T

72 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.postgresql.interview
title: PostgreSQL 面试题
type: interview
domain: backend
stack: [postgresql]
tags: [PostgreSQL, SQL, 事务, 面试]
level: intermediate
related: [backend.postgresql.schema-sql, backend.postgresql.index-tx-mvcc, backend.mysql.interview]
week: [2, 16]
---
# PostgreSQL 面试题
> 在本计划里的体现:`todo-api` / `task-board` 的数据层(第 2~3 周),pgvector 对照(第 6 周)。MySQL 相关题目见 [MySQL 面试题](../mysql/interview.md)。
## 基础
### 【基础】`WHERE` 和 `HAVING` 的区别?
**参考答案**:`WHERE` 在分组前过滤行,不能使用聚合函数;`HAVING` 在 `GROUP BY` 之后过滤分组,可以使用聚合函数。能用 `WHERE` 过滤的尽量用 `WHERE`,减少参与分组的数据量。
### 【基础】`INNER JOIN` 和 `LEFT JOIN` 的区别?LEFT JOIN 的条件写在 ON 和 WHERE 里有什么不同?
**参考答案**:内连接只保留两边都匹配的行;左连接保留左表所有行,右表没匹配的列为 NULL。右表的过滤条件写在 `ON` 里时,不匹配的左表行仍然保留;写在 `WHERE` 里会把右表为 NULL 的行过滤掉,效果等同内连接。
### 【基础】为什么 `NOT IN` 遇到 NULL 会出问题?
**参考答案**:`x NOT IN (a, b, NULL)` 等价于 `x <> a AND x <> b AND x <> NULL`,而 `x <> NULL` 的结果是 UNKNOWN,整个条件永远不为真,查询返回空。用 `NOT EXISTS` 或先过滤掉 NULL。
### 【基础】金额应该用什么类型?时间呢?
**参考答案**:金额用 `NUMERIC(p, s)` 或以“分”为单位的整数,不用浮点数(有精度误差)。时间用 `TIMESTAMPTZ`,统一存 UTC,展示时按用户时区转换。
### 【基础】什么是 UPSERT?为什么比“先查再插”安全?
**参考答案**:`INSERT ... ON CONFLICT (唯一键) DO UPDATE / DO NOTHING`,一条语句完成“有则更新、无则插入”。它依赖唯一约束在数据库内部原子判断;“先查再插”在并发下两个请求都可能查不到然后都插入。
## 进阶
### 【进阶】PostgreSQL 的 MVCC 是怎么实现的?VACUUM 是干什么的?
**参考答案**:更新时写入一个新版本的行,旧版本标记为对新事务不可见但保留给仍在使用旧快照的事务;每个事务根据快照判断哪些版本可见,所以读写互不阻塞。旧版本(死元组)需要 VACUUM 回收空间并更新可见性信息,autovacuum 会自动执行。长事务会阻止死元组被回收,导致表膨胀和性能下降。
**追问**:和 MySQL InnoDB 的 MVCC 有什么不同?→ InnoDB 原地更新、旧版本放在 undo log 里由 purge 线程清理;PostgreSQL 新旧版本都在表里,需要 VACUUM。
### 【进阶】PostgreSQL 默认的隔离级别是什么?可重复读下修改冲突会怎样?
**参考答案**:默认读已提交。可重复读下,事务看到的是开始时的快照;如果要更新的行在快照之后被其他事务修改并提交了,会报 `could not serialize access due to concurrent update`(SQLSTATE 40001),应用需要重试整个事务。
### 【进阶】一条 SQL 很慢,你怎么排查?
**参考答案**:先用 `EXPLAIN (ANALYZE, BUFFERS)` 看执行计划:是不是全表扫描、预估行数和实际行数是否差距大(统计信息过时就 `ANALYZE`)、哪个节点耗时最多。常见优化:按查询条件建联合索引(注意最左前缀和排序方向)、避免在索引列上用函数、用覆盖索引、改写子查询、分页改成游标(按上一页最后一个 ID 继续)。线上用 `pg_stat_statements` 找出总耗时最高的语句。
### 【进阶】`FOR UPDATE SKIP LOCKED` 有什么用?
**参考答案**:选中并锁定行,遇到已被其他事务锁住的行直接跳过。适合“多个 worker 并发从任务表取任务”:每个 worker 拿到不同的任务,互不阻塞,不需要额外的消息队列。
## 场景题
### 【场景题】订单表有几千万行,按用户查最近订单的分页越往后越慢,怎么优化?
**参考答案**:`OFFSET` 越大,数据库要扫描并丢弃的行越多。改成游标分页(keyset):`WHERE user_id = ? AND (created_at, id) < (上一页最后一条的值) ORDER BY created_at DESC, id DESC LIMIT 20`,配合 `(user_id, created_at DESC, id DESC)` 联合索引,每页都是索引范围扫描。代价是不能直接跳到第 N 页,通常用“加载更多”交互。更大规模时按时间分区,历史数据归档。
### 【场景题】线上要给一张大表加索引和加字段,怎么做才不影响业务?
**参考答案**:索引用 `CREATE INDEX CONCURRENTLY`(不阻塞写,不能在事务里执行,失败要删掉无效索引重建)。加字段:PostgreSQL 11 起加带常量默认值的列不需要重写表,很快;但要注意 DDL 需要短暂的排他锁,如果前面有长事务会排队并阻塞后续查询,所以设置 `lock_timeout`,在低峰期执行。迁移脚本要向后兼容:先加字段、发新代码,再清理旧字段。
### 【场景题】用户和支付回调同时修改订单状态,怎么保证状态正确?
**参考答案**:状态流转规则集中定义,修改用带版本号(或旧状态)的条件 UPDATE 做乐观锁,影响行数为 0 时视为冲突,重新读取最新状态再决定;审计日志与状态修改放在同一事务。冲突频繁的场景可以用 `SELECT ... FOR UPDATE` 排队。详见本技术栈的[订单状态机场景](scenarios/order-state-audit.md)。