2026-11-10 AI Agent 面试训练
今日学习主题
粒度对齐——测量、缓存、隔离与优化的粒度必须与决策对象的粒度一致。
今天的五道题共用一条主线:虚拟内存按页粒度管理而访问按对象粒度发生,THP 用错粒度会制造长尾延迟;提示缓存按「完整渲染前缀」粒度命中,收益核算必须按 token 粒度对总账;向量索引按「表示版本」粒度隔离,混存一个索引就是把两种度量空间压进一个几何结构;分区裁剪按「分区键上的裸谓词」粒度判定,表达式包裹一次就全部失效;Top-K 问题的计算粒度是 K 而不是全量排序。粒度错配时,优化不会变慢得 Visible,它会以长尾、静默退化和错误计费的形式出现。
题目速览
| 编号 | 题目名称 | 题型 | 一级类型 | 难度 | 一句话考察点 |
|---|---|---|---|---|---|
| B01 | 虚拟内存、按需分页与 THP 的粒度陷阱 | 基础(非 Agent) | 操作系统与网络 | 基础题(中高) | 申请 ≠ 保留;minor/major 缺页的分类;页粒度与访问粒度错配的代价 |
| B02 | 提示缓存的机制边界与跨厂商收益核算 | 基础(Agent) | LLM 应用工程 | 基础题(中高) | 完整前缀精确命中;写入加价与读取折扣的总账;与 KV Cache / 语义缓存的区别 |
| S01 | Embedding 模型迁移:新旧向量混存导致召回退化 | 场景(Agent) | RAG 与搜索 | 场景题(高) | 表示版本与索引的隔离;影子评测与切换门禁;评测集对齐表示版本 |
| S02 | 分区表与分区裁剪失效 | 场景(非 Agent) | 数据库与存储 | 场景题(高) | 计划期与执行期裁剪条件;表达式包裹分区键;唯一约束与分区键的耦合 |
| L01 | 347. 前 K 个高频元素(Top K Frequent Elements) | LeetCode | 哈希 + 堆 | medium | 计数 + 大小为 K 的最小堆;计算粒度对齐到 K |
Q-20261110-B01 | 虚拟内存、按需分页与 THP 的粒度陷阱
题目
定义按需分页(demand paging)与缺页中断,说明 minor fault 与 major fault 的区别;解释虚拟内存同时提供的三件事,并说明为什么「进程 malloc 了 8 GiB」不等于「机器已为它保留了 8 GiB 物理 / 交换空间」。最后解释透明大页(THP)为什么可能让延迟敏感服务的长尾延迟反而变差。
先思考
- 缺页发生时 MMU、内核、磁盘各自做什么?哪一类缺页需要磁盘 I/O,哪一类只补一个映射?
- RSS 与 VSZ 分别统计什么?RSS 什么时候增长?
- 4 KiB 页在大内存应用上意味着什么开销?THP 用什么粒度去换这个开销,又引入了什么新问题?
参考答案
定义与适用边界:虚拟内存给每个进程独立、连续的虚拟地址空间,内核通过页表把虚拟页映射到物理页帧,CPU 的 MMU 负责地址翻译(TLB 缓存翻译结果)。按需分页指页映射在首次访问时才真正建立:访问无有效映射的页触发缺页中断,由内核决定填入哪个物理页。边界是:虚拟内存是一种「懒惰建立 + 可超卖」的机制,它的懒惰与超卖正是性能与容量问题的两个来源。
minor 与 major fault:minor fault 发生时目标页已在物理内存中(共享库已加载、或进程从未映射过的已缓存页),内核只需补一个页表项,无磁盘 I/O,代价在微秒级;major fault 意味着数据不在内存,需要从文件或 swap 读入,进程阻塞在 I/O 上,代价高出数个量级。性能分析里两者必须分开看:minor fault 高可能只是正常的首次触碰,major/swap fault 持续非零才是内存不足的信号。
三件事与过度承诺:虚拟内存同时提供地址隔离(进程间互不可见)、按需加载与写时复制(fork 快的原因)、以及「承诺可以大于供给」——malloc 成功只表示内核登记了一个虚拟地址区间(VMA),不代表物理页已就位;首次写入时才逐页分配物理帧,在 overcommit 策略下甚至可能到 OOM killer 介入才失败。这就是「申请 8 GiB ≠ 保留 8 GiB」:容量规划必须看 RSS / working set 与 swap 行为,而不是申请量。
THP 的粒度错配:4 KiB 页在大内存应用上意味着海量页表项与 TLB miss。THP 让内核自动把满足条件的连续 2 MiB 区域合并为大页,降低 TLB 压力;但它要求物理上连续且对齐的 2 MiB 块,分配时可能触发内存整理(compaction),整理是同步代价;分配不到时回退行为取决于模式(always / madvise)。对延迟敏感、且大量使用 fork + 写时复制的内存数据库(Redis 官方运维文档明确建议对其关闭 THP,属常见工程实践;本轮未逐字核验该文档),COW 放大单位从 4 KiB 变成 2 MiB,单次缺页拷贝 512 倍的内存量——页粒度与访问模式的粒度不匹配时,「优化」变成退步。
简洁示例:一个 Java 服务堆设 8 GiB,容器 memory limit 8 GiB,启动后 VSZ 远超 8 GiB 但 RSS 缓慢爬升——堆页在触碰时才分配;某次 GC 后 RSS 骤降不明显但 major fault 飙升,说明内核开始换出堆页,真正的容量问题是 RSS + 内核侧占用逼近 limit,而不是「堆没满」。
相近概念区别:缺页中断 vs 普通中断(缺页是同步的、由该指令触发且处理后重执行同一条指令);RSS vs VSZ(驻留 vs 已登记虚拟区间);swap out vs page cache 回收(匿名页要有后备存储才能换出,文件页直接丢缓存);THP vs hugetlbfs 显式大页(自动合并 vs 应用预留大页池)。
典型应用(通用工程):数据库与 JVM 的内存排查要先分清 VSZ/RSS/working set;容器 OOM 判定看 cgroup 的 memory.working_set 与 memcg 统计而非 free;性能分析把 major fault/s 与 swap in/out 作为一级指标。
面试现场回答:虚拟内存用页表把进程虚拟页映射到物理页帧,页映射在首次访问时才建立,这就是按需分页;访问无映射页触发缺页,页已在内存只是没映射是 minor fault,微秒级、无 I/O,需要从磁盘或 swap 读入是 major fault,阻塞级代价。虚拟内存同时给进程隔离、按需加载和「承诺大于供给」的过度承诺——malloc 成功只是登记了虚拟区间,首次写才分配物理页,overcommit 下可能 OOM 才失败。THP 用 2 MiB 大页换 TLB 效率,但要求物理连续块并可能触发内存整理,对 fork 密集的内存数据库,COW 单位放大 512 倍,Redis 官方就建议关掉它。一句话:页的粒度和访问的粒度不一致时,优化会以长尾延迟的形式还回来。
关键点
- malloc 成功是登记 VMA,不是分配物理内存;容量判定看 RSS / working set 与 swap 行为。
- minor fault 无 I/O、正常代价;major fault 持续非零才是内存不足信号,两者必须分开监控。
- overcommit 意味着分配可能在任意时刻失败,关键服务的内存预留要有确定性手段(如关闭 overcommit 的严格模式或预留池)。
- THP 的收益(TLB)与代价(compaction、COW 放大、碎片)都来自 2 MiB 这个粒度;延迟敏感 + fork 密集的场景默认关闭。
- 缺页是同步异常,内核处理后重执行触发指令,程序无感知——这让问题静默化,必须靠指标而非报错发现。
常见误区
- 把 malloc 成功当成内存可用、把 VSZ 当成占用:overcommit 下两者都不可信,OOM 发生在触碰时而不是申请时。
- 见 RSS 高就判泄漏:共享映射、fork 后 COW、page cache 都会让 RSS 与「进程真实泄漏」脱钩,要对比随时间的斜率和 heap profile。
- 认为 THP 是免费优化:compaction 是同步代价,回退行为依赖模式配置,长尾 P99 恶化是常见后果。
- 把 minor fault 高当作内存问题:首次触碰和共享库加载都会产生大量 minor fault,属正常行为。
追问
追问:容器里进程 RSS 看着远低于 limit,但被 OOM killer 杀了,怎么排查?
回答要点:OOM 判定在 cgroup 层不在进程层——看 /sys/fs/cgroup 下 memory.max、memory.current 与 memory.stat(含 anon、file、sock 等),page cache 与内核内存(slab、socket buffer)都计入 cgroup;free 看到的是节点全局视图,与 memcg 视图不一致;THP 让单次分配粒度变大,瞬时跨越 limit 的概率上升;复现时对比 memory.peak 与触发时刻的 per-process RSS 差值,差值部分通常就是内核态与 cache 占用。
延伸学习
- Linux man-pages
mmap(2)(man7.org;2026-10-26 运行已核验 MAP_SHARED 回写、SIGBUS 等语义,可复用) - 内核文档 admin-guide/mm/transhuge.rst(THP 的 always/madvise/never 模式与 defrag 配置;本轮未核验具体条文,引用前请复核)
Q-20261110-B02 | 提示缓存的机制边界与跨厂商收益核算
题目
定义提示缓存(Prompt Caching),说明它的命中条件、成本模型(写入加价、读取折扣、TTL)与适用边界;解释它与推理引擎内 KV Cache、语义缓存的区别;说明在 Agent 系统中为什么「工具定义 + 系统提示 + 历史消息」的前缀布局决定命中率,以及跨厂商部署时为什么收益不能照搬。
先思考
- 缓存命中的比较单位是什么?「语义相近」的两个请求能命中吗?
- 一份缓存条目的成本由哪几笔构成?只看「缓存读取打折」会漏掉什么?
- 多轮 Agent 对话的前缀里,哪部分天然稳定、哪部分天然变化?断点应该放在哪里?
参考答案
定义与适用边界:提示缓存是模型服务端把「已渲染请求前缀」对应的注意力状态按前缀键复用的机制:后续请求与已缓存前缀逐字相同,前缀部分不再重新计算,按折扣计价(典型为标准输入价的 0.1 倍),TTFT 显著下降。边界有三条:它只作用于输入侧,不改变输出生成;命中要求完整渲染前缀 100% 精确匹配(Anthropic 官方原文 “Exact matching: Cache hits require 100 percent identical prompt segments”,前缀按 tools → system → messages 的层级组装,任一层变更使该层及其后全部失效);有最小可缓存长度,不达标静默不缓存(Anthropic 按模型 512/1024/2048/4096 分档,OpenAI GPT-5.6+ 为 1024 个可见 token),不会报错。
成本模型:读取折扣之外还有写入加价——Anthropic 5 分钟 TTL 写入为标准输入价 1.25 倍、1 小时 TTL 为 2 倍;OpenAI GPT-5.6+ 默认 30 分钟 TTL 写入 1.25 倍、12 小时 2.5 倍。TTL 方面,Anthropic 默认 5 分钟且每次命中免费续期、TTL 从请求开始时刻计算;缓存不跨组织共享(OpenAI 原文 “Caches are not shared across organizations and cannot be reused across regional processing boundaries”)。因此收益核算必须算总账:总成本 = 写入加价 × 未命中/新建部分 + 读取折扣 × 命中部分,低复用率的场景开了缓存反而更贵。
与相近概念的区别:推理引擎内的 KV Cache 服务于单请求/单批的解码过程(PagedAttention 管理的是显存里的页),是本地容量管理;提示缓存是跨请求的服务端机制,命中的是前缀状态。语义缓存则是把「语义相近的不同问题」映射到同一缓存答案,它改变行为语义(不同问题可能得到同一答案),不是本机制,也不能替代它。
典型应用(Agent 系统):多轮 Agent 的前缀天然单调增长,命中率由布局决定:工具定义保持稳定(Anthropic 文档明确警告 Swift/Go 等语言 JSON 序列化会随机化 key 顺序、直接打破缓存);系统提示与注入的静态上下文放前缀、时间戳等逐请求变化的内容放后缀;断点放在「最后一个跨请求不变的前缀块」上(Anthropic 支持最多 4 个断点,断点本身不计费)。工具定义变更会使全部缓存失效——这正是把工具清单变更视为破坏性变更、需要门禁的原因之一。
面试现场回答:提示缓存是服务端按「完整渲染前缀」复用注意力状态的机制,命中要求逐字节相同、有最小长度门槛、有 TTL 且不跨组织共享;成本上读取约 0.1 倍但写入要加价 1.25 到 2.5 倍,收益要看命中率算总账,低复用反而更贵。它和推理引擎内 KV Cache 的区别是跨请求复用 vs 单请求解码,和语义缓存的区别是不改变行为语义。Agent 系统里命中率靠布局挣出来:工具定义稳定排序、静态内容前置、变化内容放后缀、断点放稳定前缀末尾;工具定义一变缓存全失效,所以工具清单变更要走门禁。
关键点
- 命中的比较单位是完整渲染前缀,不是语义;任何一层(tools → system → messages)变化使其后全部失效。
- 成本是「写入加价 + 读取折扣 + TTL」三元组,收益必须按命中率对总账,不存在无条件省钱。
- 最小可缓存长度不达标时静默不缓存、不报错,只能靠 usage 字段(cached_tokens / cache_creation_input_tokens)观测。
- 断点放在跨请求稳定的前缀末尾;放在每次变化的内容上等于每次付写入价、永远没有读取。
- 缓存命中与路由耦合:请求被路由到不同实例/区域时可能 miss,多供应商部署时两者的 TTL、写入价、断点机制全部不同,收益不能照搬。
常见误区
- 只看「读取 0.1 倍」就断定必赚:忽略写入加价与 TTL 过期,低复用率场景总成本上升。
- 把语义缓存与提示缓存混为一谈:前者牺牲确定性换复用,会改变行为;后者不改变行为但要求逐字匹配,两者解决的问题不同。
- 用「大概相似的前缀」期待命中:一个时间戳插在系统提示中间就使其后全部失效,变化内容必须放后缀。
- 假设缓存跨组织/跨区域共享:官方明确不共享,压测时用单组织单通道流量才能测出真实命中率。
追问
追问:为什么工具定义变更要按破坏性变更处理,和缓存有什么关系?
回答要点:工具定义位于前缀层级的最外层,names/descriptions/parameters 任一修改使整条缓存链失效(Anthropic 原文 “Modifying tool definitions … invalidates the entire cache”),所有在途会话瞬间回到全价输入并伴随 TTFT 上升,在调用量大的 Agent 系统里表现为一次可观测的成本尖峰;同时工具定义变更本就可能改变模型行为,所以应与缓存影响的成本评估一起进变更门禁:低峰发布、灰度副本、变更前后对比 cached_tokens 与 TTFT。
延伸学习
- Anthropic 官方文档《Prompt caching》(platform.claude.com/docs/en/build-with-claude/prompt-caching;本轮已核验:1.25×/2× 写入、0.1× 读取、5 分钟 TTL 免费续期、精确前缀匹配、四层最小长度分档、断点不计费)
- OpenAI 官方文档《Prompt caching guide》(developers.openai.com/api/docs/guides/prompt-caching;本轮已核验:默认自动、GPT-5.6+ 显式模式、30m/12h TTL、0.1×/0.05× 读取、1024 token 门槛、跨组织不共享)
Q-20261110-S01 | Embedding 模型迁移:新旧向量混存导致召回退化
题目
业务背景:企业内部客服知识库 RAG 系统,服务 3000 名坐席,日均 2.8 万次检索。
系统规模:知识库 240 万个切片(含稳定切片 ID 与文档版本),pgvector 单表 + HNSW 索引,查询峰值 QPS 40,golden 集召回命中率(top-50 命中标准答案切片的比例)78%。
当前架构与数据流:文档解析 → 切片 → embedding 模型 A(1536 维)→ 写入向量表(单表单索引)→ 查询时 query 向量做 ANN 取 top-50 → 重排取 top-8 进上下文。
目标:升级到 embedding 模型 B(1024 维,检索基准分更高),要求不停服、可回滚。
限制条件:embedding API 有速率配额,全量重算约需 3 天配额;pgvector 索引的向量维度在建索引时固定;向量表无版本列。
已知故障现象:团队采用的灰度方案是「新入库文档用模型 B、旧文档维持 A 的向量」,查询统一用 B 生成向量做 ANN。两周后线上召回命中率从 78% 跌到 41%,用户投诉答非所问;离线评测集(只覆盖近两周新入库文档)全绿。
需要取舍的问题:一次性全量重算(成本、3 天配额、期间知识库仍在更新)vs 双索引并存(存储翻倍、路由复杂)vs 同索引加版本列过滤(HNSW 在过滤后候选不足时召回不稳)。
先思考
- 查询向量是 B 的度量空间,库里 99% 的向量是 A 的度量空间——这在几何上意味着什么?为什么「恰好都是文本向量」不能互相比较?
- 离线评测全绿与线上 41% 之间的落差,说明评测集的采样存在什么系统性偏差?
- 三种迁移方案各自把代价放在哪里?哪个方案的重算失败可以续跑、可以按版本对账?
参考答案
先给结论:故障根因是「查询向量与被检索向量必须来自同一表示版本」这条隐含不变量被灰度方案破坏——B 的查询向量在以 A 向量为主的索引里做 ANN,召回退化为近似随机;而离线评测集只含新文档(B 向量),所以全绿。方案:为向量引入不可变的表示版本,构建「影子新索引 + 全量重算 + 影子评测 + 版本切换门禁」,旧索引在回滚窗口内保持可服务。
问题拆解:这是三个叠加的问题。第一是表示隔离:embedding 空间没有跨模型可比性,1536 维与 1024 维连维度都不同,混存的几何结构无意义;第二是回填一致性:3 天重算期间知识库持续更新,重算必须按位点续跑并增量跟随;第三是评测对齐:评测集的表示版本必须与被评测的索引版本一致,否则评测在度量另一个系统。
核心方案与执行流程:①向量表增加 embedding_version 列,切片 ID + 模型版本构成幂等键(重算可重入、重复写覆盖同键);②新建影子表/影子索引,按版本隔离而不是行内过滤——避免 HNSW「先召回后过滤」在过滤后候选不足时召回不稳(pgvector 官方对带 WHERE 的 ANN 即是先召回后过滤,2026-10-12 运行已核验),维度不同也天然要求独立索引;③重算任务按切片 ID 位点推进,失败退避重试、断点续跑,期间新入库文档双写两个版本;④影子评测:同一批线上查询双跑新旧索引对比召回与重排后质量,达到门禁(命中率 ≥ 原 78%、且按文档入库时间分桶无退化)才切换查询路由;⑤旧索引保留一个回滚窗口后下线。
技术选型与取舍:选「双版本索引 + 路由切换」而非行内过滤,因为维度不同使行内混存根本不可行,而同维度换模型时行内过滤也有候选不足问题;选「影子重算」而非「一次性停写重算」,因为 3 天配额内知识库在更新,停写代价不可接受;备选的「逐分区渐进替换」在无时间分区键时难以界定迁移边界,放弃。重算成本用配额窗口 + 速率退避控制,不追求最快。
失败路径、边界与恢复:API 限流按 429/5xx 分类退避,配额耗尽则暂停任务而非降级用错版本;重算中断靠位点 + 幂等键恢复;切换路由是单点开关,可秒级回滚到旧索引;旧版本下线前对账两版本切片集合差集(应仅剩重算窗口内已删除文档)。已发生的错误检索结果不「重算修复」,通过正常会话重新提问覆盖——检索错误本身无副作用,但若下游已有基于错误上下文的写操作,须按副作用边界另行补偿。
能力归属:embedding 的语义质量与度量空间是模型概率性能力,只能靠评测度量;版本列、幂等键、影子索引、位点续跑、路由开关是传统软件的确定性能力;「何时允许切换、评测不过不放行」的门禁是编排层规则,必须落在确定性代码上,不能靠模型自评。
验证与观测:切换前后按「查询向量版本 × 索引版本 × 文档入库时间段」三个维度分桶观测命中率与重排通过率;监控重算任务的位点滞后续跑时长与配额消耗;上线后保留一段双跑期,命中率周环比与 golden 集周更评测作为长期门禁。
关键点
- 不同 embedding 模型的向量属于不同度量空间,没有任何「混着用也能凑合」的中间态;表示版本必须是一等公民字段。
- 索引按版本隔离优于行内过滤:维度不同必须隔离;同维度时过滤型 ANN 也有「先召回后过滤候选不足」的失效模式。
- 评测集必须与被测系统的表示版本对齐,迁移期评测要双跑对比而非复用旧集。
- 重算必须是幂等、可续跑、可对账的批处理:切片 ID + 模型版本为幂等键,位点推进,增量双写兜住窗口期更新。
- 切换与回滚都应是路由级单点开关;旧版本保留到对账通过为止。
常见误区
- 把召回退化归因为「模型 B 不行」:评测集全绿与线上崩塌并存时,先查评测集的采样偏差与版本对齐,而不是回滚模型选型。
- 在同一索引里用版本列过滤做迁移:维度不同不可行;维度相同时过滤后候选不足会让 ANN 召回不稳,且旧向量仍占用索引容量。
- 一次性全量重算期间假设知识库静止:3 天窗口内的增量如果不双写,切换后新内容反而缺失。
- 用「人工抽查几条」代替分桶评测:混存退化按文档新旧程度分布不均,抽样会系统性漏掉旧文档桶。
追问
追问一:离线评测全绿但线上 41%,除了版本对齐,还可能是评测体系的什么缺陷?
要点:评测集只覆盖近两周新文档,是入库时间维度的采样偏差——它度量的其实是「B 对新文档」而非「系统对全库」;正确设计是按入库时间分层抽样、新旧文档都覆盖,同时 golden 集要随知识更新维护,否则评测集本身漂移。
追问二:如果模型 B 与 A 维度相同(都是 1536 维),你的方案里哪些环节可以简化,哪些反而更危险?
要点:可以共用一份 schema 与索引定义,重算即原地按版本回填;更危险的是「维度相同」会诱导团队直接混存——几何上两个模型的空间同样不可比,且没有维度报错兜底,混存错误更隐蔽;版本隔离与门禁一个都不能省,反而要加 CI 断言防止有人绕过版本列。
追问三:重算 3 天期间发生了文档删除与更新,如何保证切换时新旧索引内容一致?
要点:更新 = 新切片 ID(内容变则切片变,沿用稳定切片 ID 规则),双写自然覆盖;删除需按删除事件流对两个版本回放,重算位点只保证「创建」不保证「删除」;切换前用文档表与两个向量表做差集对账,差集收敛到仅含「重算开始前的历史删除且已过保留窗口」的条目才放行。
小型实践
用 Python + FAISS 或 pgvector:取 1 万条文本切片,用两个不同的随机正交矩阵模拟「模型 A/B」的 64 维投影(模拟两个不可比的表示空间);先实现混存检索观察召回崩塌,再实现版本列 + 双索引 + 路由开关方案,用固定 golden 集对比混存与隔离两档的 top-10 命中率,并模拟「重算 60% 时切换」验证门禁拦截。
延伸学习
- pgvector 官方 README(github.com/pgvector/pgvector;2026-10-12 运行已核验 HNSW/IVFFlat 与带 WHERE 的 ANN「先召回后过滤」行为)
- OpenAI《Embeddings》官方指南(platform.openai.com/docs/guides/embeddings;含「不要跨模型混用向量」的工程提示;本轮未逐字核验该提示原文,引用前请复核)
Q-20261110-S02 | 分区表与分区裁剪失效
题目
业务背景:电商平台订单库,PostgreSQL,报表与运营查询走只读副本。
系统规模:orders 表按 created_at RANGE 月分区,共 36 个分区,总数据 22 亿行,单分区 2–4 亿行;90 天外的分区按月 detach 归档。
当前架构与数据流:OLTP 直连主库;分区初衷是归档与维护,不是查询加速;订单号使用 uuid_v7 作应用层主键。
目标与限制:不改变业务查询形态的前提下控制报表查询成本;不允许为归档便利引入跨库方案。
已知故障现象:一次「按天汇总」的报表查询 P95 从 120ms 涨到 9.5s,EXPLAIN 显示 Append 节点扫描了全部 36 个分区;同周运营后台的一条导入路径在月初零点后偶发报错 no partition of relation found for row。排查线索:报表查询是经 PREPARE 的语句,条件写作 WHERE date_trunc('day', created_at) = $1。
需要取舍的问题:报表 SQL 必须直接携带分区键裸谓词(业务方认为改 SQL 侵入业务)vs 引入 DEFAULT 分区兜底路由(ATTACH 新分区时对 DEFAULT 持 ACCESS EXCLUSIVE 锁并全扫校验);uuid_v7 全局唯一 vs 分区表唯一约束必须包含分区键。
先思考
- 裁剪的判定对象是什么形状的谓词?被
date_trunc()包裹后,计划器还能把条件与分区边界对上吗? - PREPARE 的参数值在计划时未知——官方文档说裁剪在这种情况下被推迟到哪里?怎么看
Subplans Removed? - 月初零点的 INSERT 报错和「未来分区没预建」是什么关系?DEFAULT 分区能兜住它,代价是什么?
参考答案
先给结论:两个故障同根。报表查询的分区键被表达式包裹,条件不再是「分区键与常量/参数的直接比较」,裁剪失去可判定的区间条件,于是全分区扫描;月初报错是未来分区未预建且无 DEFAULT 分区,路由失败直接抛错。修法:谓词改写为分区键范围比较 + 未来分区预建例行化 + 监控每查询扫描分区数;DEFAULT 分区按写入路径的容错需求决定要不要,且要知道它的锁代价。
问题拆解:分区裁剪只由「分区键上隐式定义的约束」驱动、与索引无关(官方文档原文 “partition pruning is driven only by the constraints defined implicitly by the partition keys, not by the presence of indexes”)。裁剪发生在两个时机:计划期,以及执行期(含计划初始化阶段的参数化裁剪——PREPARE 参数值未知时裁剪推迟到执行,EXPLAIN 里以 Subplans Removed 呈现;本轮已核验该页原文,但「表达式包裹分区键导致无法裁剪」的逐字表述不在该页,按工程机制陈述)。表达式包裹后,date_trunc(created_at) = $1 不是区间条件而是对每行的函数求值,计划器与执行器都无法据此排除分区——粒度错配:裁剪的判定粒度是「键的直接谓词」,不是「键的任意函数」。
核心方案与执行流程:①报表 SQL 改写为 created_at >= $1 AND created_at < $1 + interval '1 day',语义等价(前提是 created_at 无时区语义陷阱,timestamptz 需固定时区边界);②在数据访问层加规则:分区键必须以裸谓词出现,date_trunc 只允许出现在 SELECT 聚合里;③用 EXPLAIN (ANALYZE) 验证扫描分区数,并把「扫描分区数」「Subplans Removed」纳入报表 SQL 的回归清单;④分区预建例行化(提前建 N 个月分区 + 监控剩余未建月数),导入路径在预建失败时报错前先走明确的「分区不存在」检查而非依赖运行时报错;⑤若业务坚持不改 SQL,退路才是 DEFAULT 分区兜底——但须评估 ATTACH 时的锁:官方文档明确,若不建排除约束,ATTACH 新分区会在持有 DEFAULT 分区 ACCESS EXCLUSIVE 锁的同时扫描它校验无越界行。
技术选型与取舍:选「改写谓词 + 预建」而非「DEFAULT 兜底」,因为前者零运行时代价而后者在归档节奏(每月 ATTACH)上引入周期性长锁,与 OLTP 写入冲突;唯一约束的取舍:声明式分区表上唯一约束必须包含分区键,uuid_v7 全局唯一无法直接建——本例唯一性由发号器与应用层幂等键保证(uuid_v7 本身按时间有序且含随机尾,碰撞概率工程上可忽略),若业务必须数据库强制唯一,则用独立映射表承载唯一索引,代价是多一次写入与一致性窗口。
失败路径、边界与恢复:改写后的 SQL 走 PREPARE 时,计划初始化阶段裁剪仍生效(参数化裁剪),但注意执行期裁剪在开始执行时仍会锁住所有分区(官方原文 “any partitions removed by the partition pruning done at this stage are still locked at the beginning of execution”)——分区总数要有上限,36 个尚可,盲目按天分区到上千个会把锁与计划时间变成新问题;预建任务失败时导入路径报错是「快速失败」,可接受但要告警,不要静默引入 DEFAULT 让数据悄悄进兜底分区(归档 detach 时才发现);归档 detach 期间在途查询的行为依赖锁等待,运维窗口避开报表高峰。重试与幂等:导入报错路径按「分区不存在」重试前先触发预建检查,避免盲目重试放大写入。
性能、可用性与验证:验证分三层——EXPLAIN 静态检查(Append 子节点数)、pg_stat_statements 对比改写前后执行时间分布、按查询模板的扫描分区数巡检;上线后观测 detach 窗口的锁等待指标。与 2026-10-28 B01(统计信息与执行计划回归的一般机制)的分工:那题讲「统计过期导致计划变差」,本题讲「谓词形状使裁剪整体失效」——后者即使统计再新也不会好,属于结构性错误,要靠 SQL 规则与门禁防住。
关键点
- 裁剪只认分区键上的直接谓词,与索引无关;表达式包裹分区键后,计划期与执行期都无法裁剪。
- PREPARE 参数的裁剪推迟到执行期,
Subplans Removed是初始化阶段裁剪的观测点;执行期裁剪的分区在开始执行时仍被锁定。 - 分区表唯一约束必须包含分区键;全局唯一要么靠发号器与应用层保证,要么挪到独立映射表。
- 无 DEFAULT 分区时越界写入快速报错(可告警可重试),有 DEFAULT 分区则 ATTACH 时可能持 ACCESS EXCLUSIVE 全扫——兜底不是免费的。
- 分区数本身有成本(锁、计划时间),归档收益与查询谓词覆盖之间要平衡。
常见误区
- 认为「建了分区就会自动加速」:查询不带裸分区键谓词时,分区比单表更慢(36 个索引探测)。
- 把
date_trunc(created_at) = $1当作等价于范围条件的写法:语义近似但优化器视角完全不同,改写必须用>= / <区间。 - 用 DEFAULT 分区当「免维护」方案:数据静默落入 DEFAULT 后归档逻辑失效,且 ATTACH 校验的全扫与长锁常在业务高峰爆发。
- 忽略 timestamptz 的时区边界:改写区间时若会话时区不定,边界对不上当天,裁剪正确但语义错误。
追问
追问一:既然 PREPARE 参数裁剪推迟到执行期,为什么表达式包裹后连执行期也不行?
要点:执行期裁剪针对的是「参数值未知但谓词形状正确」的场景——键与参数的直接比较在拿到值后可换算为区间;表达式包裹后条件是「函数(键) = 值」,除非逐行求值否则无法反解出键的区间,分区边界集合与该条件没有可计算的交集关系。这也是为什么解法是改写谓词形状而不是等更聪明的优化器。
追问二:为什么唯一约束必须包含分区键?这对分库分表方案意味着什么?
要点:每条行路由到哪个分区由分区键决定,数据库无法在写入前全局判重,唯一性只能在「分区内」强制——包含分区键的唯一约束把唯一性分解到各分区;同理,分库分表下全局唯一约束要么靠发号器(确定性生成不冲突的 ID),要么靠独立的全局索引服务,本质都是把判重从存储层上移到确定性的应用层。
追问三:如果业务方拒绝改 SQL,你有什么中间态方案?各自的失效模式是什么?
要点:视图 + INSTEAD OF 规则不能帮裁剪(谓词仍在函数里);函数索引也无效(裁剪不看索引);可行中间态是查询改写在数据访问层/ORM 拦截器统一完成(业务代码无感),或报表改走按天预聚合表(物化视图/汇总表)——后者把「按天查询」从分区裁剪问题变成小表点查,但要处理预聚合的时效与回填。没有任何方案能让表达式谓词直接被裁剪,这是判定粒度决定的硬边界。
小型实践
在本地 PostgreSQL 建一张 RANGE 月分区表(含 3 个月分区、无 DEFAULT),分别用 date_trunc('day', created_at) = $t 与 created_at >= $t AND created_at < $t + interval '1 day' 两种谓词跑 EXPLAIN (ANALYZE),对比 Append 子节点数与 Subplans Removed;再用 PREPARE + 不同参数复跑,观察初始化期与执行期裁剪;最后手工 ATTACH 一个新分区,用另一会话对 DEFAULT 场景观察锁等待。
延伸学习
- PostgreSQL 官方文档《5.11 Table Partitioning》(postgresql.org/docs/current/ddl-partitioning.html;本轮已核验:计划期/执行期裁剪、Subplans Removed、执行期裁剪仍加锁、UPDATE 分区键移动行、DEFAULT 分区 ATTACH 校验与锁)
- PostgreSQL 官方文档《Partition Pruning》章节(postgresql.org/docs/current/ddl-partition-pruning.html;裁剪专门章节,本轮未单独抓取,引用具体条文前请复核)
Q-20261110-L01 | LeetCode 347. 前 K 个高频元素
题目
LeetCode 347 | 中文名:前 K 个高频元素 | 英文名:Top K Frequent Elements | 难度:medium(官方页面本轮未渲染难度标签,为连续第 8 次;medium 按 LeetCode 标准分类记录,未直接核验)
官方链接(本轮已核验):https://leetcode.com/problems/top-k-frequent-elements/
题目描述:给定整数数组 nums 和整数 k,返回其中出现频率前 k 高的元素。可以按任意顺序返回答案。
输入输出:输入数组 nums 与整数 k;输出含 k 个元素的数组(顺序不限)。
约束:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4k的取值范围是[1, 数组中不同元素的个数]- 题目数据保证答案唯一
示例(与官方页一致):
nums = [1,1,1,2,2,3], k = 2→[1,2]nums = [1], k = 1→[1]nums = [1,2,1,2,1,2,3,1,3,2], k = 2→[1,2]
Follow up:算法时间复杂度必须优于 O(n log n),n 为数组大小。
先思考
- 第一步几乎必然是统计频次——用什么结构?之后「从 m 个不同元素里取频次最大的 k 个」,全排序是 O(m log m),哪里还有冗余?
- 只需要「最大的 k 个」而不需要排序结果——维护一个大小固定为 k 的结构,每次淘汰谁?
- 如果连 O(m log k) 都想突破,频次的取值范围有什么性质可以利用?
参考答案
解题思路:哈希表统计每个元素的出现频次(O(n));然后用一个大小为 k 的最小堆维护当前「频次最高的 k 个元素」:遍历 m 个不同元素,依次压堆,堆超过 k 时弹出堆顶(当前频次最小者)。遍历结束后堆里就是答案。之所以用最小堆,是因为要被淘汰的永远是这 k 个里的最小者——堆的粒度恰好对齐到 k,这就是本题的「计算粒度对齐」。
正确性说明:不变式是「任意时刻,堆中保存着已处理元素中频次最高的至多 k 个元素」。新元素到来时,若它频次低于堆顶则不可能进入答案集合,弹出堆顶保留它才是正确动作;堆大小 ≤ k 保证每步成本 O(log k)。m 次操作后不变式覆盖全部元素,堆中即全局 top-k。频次相同元素互不影响正确性(答案保证唯一,频率第 k 名的元素唯一)。
复杂度:统计 O(n);建堆过程 m 次压弹,O(m log k);总时间 O(n log k),满足优于 O(n log n)(m ≤ n)。空间 O(m)(哈希表)+ O(k)(堆)。
Python 参考代码:
import heapqfrom collections import Counterfrom typing import List
class Solution: def topKFrequent(self, nums: List[int], k: int) -> List[int]: counts = Counter(nums) heap = [] for num, freq in counts.items(): heapq.heappush(heap, (freq, num)) if len(heap) > k: heapq.heappop(heap) return [num for _, num in heap]关键边界情况:k 等于不同元素总数(堆永不弹出,全部保留);所有元素相同(m=1,直接返回该元素);负数与重复值(哈希键无碍);答案唯一性由题目保证,若去掉该保证则第 k 名并列时需定义稳定性。
测试用例:
| 输入 | 期望输出 | 验证点 |
|---|---|---|
nums=[1,1,1,2,2,3], k=2 | {1,2}(集合相等即可) | 官方示例 1 |
nums=[1], k=1 | {1} | 单元素、k=1 下界 |
nums=[1,2,1,2,1,2,3,1,3,2], k=2 | {1,2} | 官方示例 3,频次 5 vs 3 vs 2 |
nums=[4,4,4,6,6,7], k=2 | {4,6} | 频次并列候选(3,2,1),正确淘汰 7 |
nums=[5,3,5,3,9], k=3 | {5,3,9} | k 等于不同元素数,全保留 |
变体与追问:面试官可能要求 O(n)——用桶排序:频次上限为 n,按频次建桶、桶内收集元素、从高频桶向低频桶收集直到取满 k 个;或追问「流式数据(元素只过一遍)怎么办」——哈希计数 + 固定大小最小堆仍是标准答案,这也是 Top-K 在监控告警、热门内容榜中的通用形态。与 AI Agent 工程的自然联系:从海量日志/trace 中取「最耗时的 K 个工具调用」就是同一模式——维护大小为 K 的堆,而不是全量排序。
可迁移解题能力:「频次统计 → 固定容量堆淘汰」的 Top-K 模式;「只要前 k 不必全排序」的计算粒度意识;最小堆用于「保留最大」、最大堆用于「保留最小」的对偶选择。
关键点
- 大小为 k 的最小堆维护「当前 top-k」:堆顶是 k 个中最小者,是被淘汰对象。
- 时间 O(n log k) 优于 follow-up 要求的 O(n log n);O(n) 解是频次桶排序。
- 题目保证答案唯一,因此不必处理第 k 名并列的稳定性;面试口头应主动指出该前提。
- 返回顺序任意,比较结果时按集合比较而非序列比较。
常见误区
- 先全排序再取前 k:O(m log m) 违背 follow-up,且没有利用「只要 k 个」的信息。
- 用最大堆存全部 m 个元素再弹 k 个:正确但 O(m + k log m),元素远多于 k 时不如固定 k 堆。
- 堆里存
(freq, num)却以为弹出顺序任意:Python 的最小堆按元组字典序比较,频次并列时按 num 比较——本题答案唯一所以无碍,但若要求稳定顺序须自定义比较。
追问
追问:如果把「频次最高」改成「频次第 k 名有并列,要求返回所有并列元素」,解法如何调整?
回答要点:先用堆确定第 k 高频次值 f_k(O(m log k)),再线性收集所有频次 ≥ f_k 的元素;此时输出规模可能超过 k,题目语义要先与面试官对齐——这正是「保证答案唯一」约束在被去掉后必须补的规格讨论。
延伸学习
- LeetCode 347 官方题解与「桶排序 O(n)」解法(leetcode.com/problems/top-k-frequent-elements/solution/;本轮未抓取题解正文,仅官方题目页已核验)
- heapq 官方文档(docs.python.org/3/library/heapq.html;heapq.nlargest / nsmallest 的内部实现同样是固定容量堆)
延伸资料
- Anthropic 官方文档《Prompt caching》(platform.claude.com/docs/en/build-with-claude/prompt-caching)——B02 主来源,本轮已核验:1.25×/2× 写入、0.1× 读取、5 分钟 TTL 命中免费续期、精确前缀匹配、tools→system→messages 失效层级、512–4096 最小长度分档、断点不计费。
- OpenAI 官方文档《Prompt caching guide》(developers.openai.com/api/docs/guides/prompt-caching)——B02 对照来源,本轮已核验:默认自动、GPT-5.6+ 显式断点、30m/12h TTL、写入 1.25×/2.5×、读取 0.1×/0.05×、1024 token 门槛、跨组织与跨区域不共享、usage 字段口径。
- PostgreSQL 官方文档《5.11 Table Partitioning》(postgresql.org/docs/current/ddl-partitioning.html)——S02 主来源,本轮已核验:计划期/执行期裁剪、
Subplans Removed、执行期裁剪仍加锁、UPDATE 分区键移动行、DEFAULT 分区 ATTACH 校验与 ACCESS EXCLUSIVE 锁。 - LeetCode 347 官方题目页(leetcode.com/problems/top-k-frequent-elements)——L01 主来源,本轮已核验:标题、三示例、四约束、follow-up;难度标签未渲染(连续第 8 次),medium 按标准分类记录并已在正文标注。
- 内核文档 admin-guide/mm/transhuge.rst 与 Redis 运维文档中关于关闭 THP 的建议——B01 延伸,本轮未逐字核验,引用前请复核。
当日重点
- 申请 ≠ 保留:malloc 只登记虚拟区间;容量判定看 RSS / working set,缺页要分 minor/major 两类监控。
- 缓存收益是三元组:写入加价、读取折扣、TTL 共同决定净收益;断点必须放在跨请求稳定的前缀末尾,工具定义变更等价于缓存全灭。
- 表示版本是一等公民:embedding 模型迁移的本质是度量空间切换,混存无任何合法中间态;评测集必须与被测系统的表示版本对齐。
- 裁剪只认裸谓词:表达式包裹分区键后,计划期与执行期都无法裁剪;分区表唯一约束必须包含分区键。
- Top-K 的计算粒度是 K:固定大小最小堆把每步成本压到 O(log k),是日志与监控场景的通用形态。
本次运行信息
- 运行日期:2026-11-10(Asia/Shanghai),第 23 次每日出题。
- 配置读取:成功,
题库配置.mdversion 8(last_maintained: 2026-11-02,mtime 11-02 10:37,为周维护产物,本次运行未修改该文件)。无暂停类型;观察中的新技术均未标记「允许试学」,未从中出题。目标目录及父目录无AGENTS.md/CLAUDE.md。 - 历史读取:
题目索引.md(120 条 / indexed_runs 22)、README.md、精选/README.md(4 条),未通读更早正文。滚动窗口 2026-08-10 至 2026-11-10,窗口内最早条目 2026-10-10,无需剪除。 - 去重结果:五题均与近 30 天无同题 / 换皮 / 同 LeetCode 题号。需说明的相邻关系:B02 与 2026-10-30 B02(提示缓存命中条件与前缀布局)同属提示缓存域,本题角度为跨厂商机制差异与收益核算总账(该角度为索引中 LLM 应用工程标注的下一轮换项);S01 与 2026-10-19 S01(RAG 知识更新滞后与引用失效)不同题——那是内容新鲜度问题,本题是表示版本迁移;S02 与 2026-10-28 B01(统计信息与执行计划回归)已在上文说明分工。
- 轮换执行:B01 填补操作系统与网络「虚拟内存与缺页」(该类型此前未覆盖此二级主题);B02 填补 LLM 应用工程(索引窗口内覆盖次数最低,5 次);S01 场景类型「Embedding 模型迁移」为首次;S02 填补数据库与存储标注缺口「分区表与分区裁剪」;L01 采用上轮建议轮换中的 347(堆模式,自 215 后未使用)。
- 本次核验来源:LeetCode 347 官方页、PostgreSQL 18 分区文档、Anthropic Prompt Caching、OpenAI Prompt Caching Guide(均见延伸资料)。
- 未核验项(正文中已标注):内核 THP 文档具体条文、Redis 关闭 THP 的运维文档原文、pgvector 带 WHERE 的 ANN 行为沿用 2026-10-12 核验结论、OpenAI Embeddings 指南提示原文、LeetCode 347 官方题解正文。
- 状态:训练文件本地写入成功;未发生精选与作答反馈;未修改任何历史训练文件正文;远端(iCloud)同步状态未验证。