5.5 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,减少参与分组的数据量。
【基础】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 排队。详见本技术栈的订单状态机场景。