题目与背景
一张多租户事件表拥有 B-tree 索引 (tenantid, createdat)。业务新增查询只给出 created_at 范围,旧版本常选择顺序扫描。请基于 PostgreSQL 18 的 skip scan 说明优化器如何利用后缀列、怎样验证收益,以及为什么不能把它当作所有场景的索引替代品。
面试官考察什么
重点是理解 B-tree 左侧前缀规则与 skip scan 的差别。PostgreSQL 18 可以枚举前导列的不同值,为每个值执行后缀条件的索引搜索;成本取决于前导列基数、后缀选择性、表与索引相关性及估算统计。候选人还应能用 EXPLAIN (ANALYZE, BUFFERS) 证明实际收益。
先问清楚的澄清问题
数据分布
确认租户数量、每个租户的行数、时间范围宽度和数据是否按时间聚集。前导列不同值很多时,重复搜索可能比顺序扫描更昂贵。
工作负载与版本
确认运行版本确实是 PostgreSQL 18、查询是否高频、是否允许新增覆盖索引,以及是否存在并发写入。skip scan 是计划选择,不是 SQL 语法保证。
观测基线
确认已有 EXPLAIN、缓冲命中率、执行时间和冷缓存基线。必须比较实际计划,不能只看估算成本或单次热缓存结果。
30 秒回答框架
“索引 (tenantid, createdat) 的传统规则要求先约束 tenantid;PostgreSQL 18 在成本合适时可以对 tenantid 的不同值逐一尝试,再利用 createdat 的范围条件跳过无关索引区间。我要先更新统计信息,用 EXPLAIN ANALYZE BUFFERS 比较 skip scan、顺序扫描和专门的 (createdat) 索引。租户基数高、范围宽或相关性差时,skip scan 可能更慢。”
深入解答步骤
第一步:明确索引与谓词关系
把索引列顺序、等值条件、范围条件和排序要求列出来。skip scan 主要帮助缺少前导列约束但后续列有选择性谓词的多列 B-tree,不会改变索引键的物理排序。
第二步:解释枚举前缀的成本
优化器可以把前导列的不同值当作隐含搜索入口,针对每个值查找后缀范围。枚举次数近似受前导列不同值和统计误差影响;前缀基数越高,随机访问和重复定位成本越大。
第三步:刷新统计并检查计划
先对表执行 ANALYZE,确保列不同值、直方图和相关性统计反映当前数据。用 EXPLAIN (ANALYZE, BUFFERS, SETTINGS) 记录实际行数、共享命中、读盘、计划节点和启用的优化器设置。
第四步:建立可比基线
在同一数据快照上比较三种方案:现有索引的 skip scan、顺序扫描,以及新增后缀列索引。分别测试冷缓存、热缓存、窄时间范围、宽时间范围和租户倾斜,避免用单一样本做结论。
第五步:识别覆盖与回表成本
如果查询还要读取大量非索引列,skip scan 之后的 heap 访问可能成为瓶颈。评估索引是否覆盖投影、可见性图是否支持 index-only scan,以及随机回表是否抵消索引过滤收益。
第六步:处理计划稳定性
数据增长会改变前导列基数和选择性,使优化器在 skip scan、顺序扫描和其他索引之间切换。记录计划指纹和 p95 延迟,必要时调整统计目标或单独创建符合主要访问路径的索引。
第七步:规划升级与回滚
在 PostgreSQL 18 升级后重新采集统计并做真实流量回放。发布时监控 buffer read、CPU、锁等待和尾延迟;若计划回归,先回到稳定索引或调整查询,再评估是否保留 skip scan 路径。
高质量示例回答
对于 (tenantid, createdat),我会把 skip scan 视为一种成本驱动的计划:优化器枚举 tenantid 的不同值,再对 createdat 范围做索引搜索。先 ANALYZE,使用 EXPLAIN ANALYZE BUFFERS 与顺序扫描、(created_at) 索引做冷热缓存和不同时间范围的对照。若租户基数、回表量或范围宽度使重复搜索昂贵,就建立符合主查询的后缀或覆盖索引,并监控升级后的计划稳定性。
常见错误
- 错误: 认为有多列索引就一定能高效过滤后缀列。→ 原因: 传统左前缀规则仍然存在,skip scan 由成本决定。→ 改进: 以实际计划和数据分布验证。
- 错误: 把 skip scan 当成新的索引类型。→ 原因: 它是优化器访问 B-tree 的策略。→ 改进: 说明物理索引未改变。
- 错误: 只比较估算成本。→ 原因: 统计误差会导致计划误判。→ 改进: 使用 ANALYZE BUFFERS 和多种缓存状态测量。
- 错误: 忽略回表和覆盖列。→ 原因: 过滤快不代表读取投影便宜。→ 改进: 评估 index-only 条件与 heap 访问。
追问与回答
追问 1:前导列只有两个租户时一定会使用 skip scan 吗?
不一定。还要看后缀谓词选择性、页面相关性、缓存状态和估算成本;优化器可能认为顺序扫描更便宜。
追问 2:skip scan 会跳过所有不匹配的叶页吗?
它通过不同前缀值建立多次索引搜索,减少无关范围扫描,但每次搜索仍有定位和可能的回表成本,并非免费跳跃。
追问 3:为什么 ANALYZE 后计划仍可能错误?
多列相关性、数据倾斜、参数值和缓存状态会超出基础统计模型。应结合扩展统计、真实参数回放和长期 p95 监控判断。
追问 4:什么时候直接建 (created_at) 索引更好?
当后缀查询是稳定主路径、前导列基数高、时间范围宽或回表量大时,专用索引能减少重复前缀搜索。需要权衡写放大、存储和其他查询对原索引的依赖。