7.4 KiB
id, title, type, domain, stack, tags, level, related, week
| id | title | type | domain | stack | tags | level | related | week | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.postgresql.interview | PostgreSQL 面试题 | interview | backend |
|
|
intermediate |
|
|
PostgreSQL 面试题
在本计划里的体现:
todo-api/task-board的数据层(第 2~3 周),pgvector 对照(第 6 周)。MySQL 相关题目见 MySQL 面试题。
基础
【基础】WHERE 和 HAVING 的区别?
参考答案:WHERE 在分组前过滤行,不能使用聚合函数;HAVING 在 GROUP BY 之后过滤分组,可以使用聚合函数。能用 WHERE 过滤的尽量用 WHERE,减少参与分组的数据量。
追问:WHERE 中能使用 SELECT 里定义的别名吗?→ 不能,因为 WHERE 在 SELECT 之前执行;ORDER BY 可以使用别名。
【基础】INNER JOIN 和 LEFT JOIN 的区别?LEFT JOIN 的条件写在 ON 和 WHERE 里有什么不同?
参考答案:内连接只保留两边都匹配的行;左连接保留左表所有行,右表没匹配的列为 NULL。右表的过滤条件写在 ON 里时,不匹配的左表行仍然保留;写在 WHERE 里会把右表为 NULL 的行过滤掉,效果等同内连接。
追问:怎么查询“没有下过订单的用户”?→ LEFT JOIN orders ... WHERE orders.id IS NULL,或者 NOT EXISTS (SELECT 1 FROM orders WHERE ...)。
【基础】为什么 NOT IN 遇到 NULL 会出问题?
参考答案:x NOT IN (a, b, NULL) 等价于 x <> a AND x <> b AND x <> NULL,而 x <> NULL 的结果是 UNKNOWN,整个条件永远不为真,查询返回空。用 NOT EXISTS 或先过滤掉 NULL。
追问:NOT EXISTS 为什么没有这个问题?→ NOT EXISTS 只判断子查询是否有行,不对值做三值逻辑比较,NULL 不会影响结果。
【基础】金额应该用什么类型?时间呢?
参考答案:金额用 NUMERIC(p, s) 或以“分”为单位的整数,不用浮点数(有精度误差)。时间用 TIMESTAMPTZ,统一存 UTC,展示时按用户时区转换。
追问:timestamptz 存储的是什么?→ 存储的是 UTC 时间点,显示时按会话时区转换;timestamp(不带时区)只是一个“墙上时间”,不知道是哪个时区。
【基础】什么是 UPSERT?为什么比“先查再插”安全?
参考答案:INSERT ... ON CONFLICT (唯一键) DO UPDATE / DO NOTHING,一条语句完成“有则更新、无则插入”。它依赖唯一约束在数据库内部原子判断;“先查再插”在并发下两个请求都可能查不到然后都插入。
追问:ON CONFLICT DO NOTHING 怎么知道是否插入了?→ 加 RETURNING id,没有返回行就说明冲突了。
进阶
【进阶】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),应用需要重试整个事务。
追问:可重复读下序列化失败怎么处理?→ 应用层捕获错误码 40001,重试整个事务;事务尽量短,减少冲突。
【进阶】一条 SQL 很慢,你怎么排查?
参考答案:先用 EXPLAIN (ANALYZE, BUFFERS) 看执行计划:是不是全表扫描、预估行数和实际行数是否差距大(统计信息过时就 ANALYZE)、哪个节点耗时最多。常见优化:按查询条件建联合索引(注意最左前缀和排序方向)、避免在索引列上用函数、用覆盖索引、改写子查询、分页改成游标(按上一页最后一个 ID 继续)。线上用 pg_stat_statements 找出总耗时最高的语句。
追问:EXPLAIN 和 EXPLAIN ANALYZE 的区别?→ 前者只显示计划和估算;后者真正执行 SQL 并显示实际行数和耗时。对写操作使用 EXPLAIN ANALYZE 要放在事务里回滚。
【进阶】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,在低峰期执行。迁移脚本要向后兼容:先加字段、发新代码,再清理旧字段。
追问:CREATE INDEX CONCURRENTLY 失败了怎么办?→ 会留下一个标记为 INVALID 的索引,要先删除再重新创建;它不能在事务块里执行。
【场景题】用户和支付回调同时修改订单状态,怎么保证状态正确?
参考答案:状态流转规则集中定义,修改用带版本号(或旧状态)的条件 UPDATE 做乐观锁,影响行数为 0 时视为冲突,重新读取最新状态再决定;审计日志与状态修改放在同一事务。冲突频繁的场景可以用 SELECT ... FOR UPDATE 排队。详见本技术栈的订单状态机场景。
追问:为什么不用 SELECT ... FOR UPDATE 悲观锁?→ 也可以,适合冲突很多的场景;冲突少时乐观锁或条件更新不需要持有锁,并发更好。关键是状态迁移要有条件判断。