想象一家生意火爆的川菜馆。客人点一份“宫保鸡丁”,后厨不会每次都从研究菜谱开始——墙上的卡片架里,那张写满工序的配方卡会被翻出来,照做即可。只有第一次做这道新菜时,大厨才需要从选料、改刀、调汁一路设计,把整套流程写成卡片挂上墙。数据库处理 SQL 的道理如出一辙:一条 SQL 进来,先被解析、优化成一份“怎么查最快”的施工图,这份施工图就叫执行计划;而把常用计划保存下来的地方,就是执行计划缓存(Plan Cache)。本文我们就以“后厨配方卡”为线,讲透计划缓存是怎么命中、怎么参数化、什么时候失效,以及如何把它调校到最佳状态。搞懂这套机制,你写出的 SQL 会被数据库“秒复用”,而不是每次都被迫从零研发。
对 AI 从业者而言,Plan Cache 是数据库性能的隐形杠杆:高并发 AI 服务的吞吐瓶颈常藏在“硬解析风暴”里。理解并掌握它,能让你写 SQL、搭数据管道时少踩一个深坑,更快定位线上慢查询的根因。
可视化演示
展示Plan Cache核心机制:硬解析与软解析流程对比、SQL参数化匹配规则、以及参数嗅探导致的计划偏差与自适应修正。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+captions.cache+'
执行计划生成
后厨第一次接到“水煮鱼”这道没人做过的菜,大厨得从选鱼、片鱼、腌渍、炒料、淋油一步步设计,反复掂量每种做法的火候与成本,最后把定稿工序抄成配方卡。数据库同样如此:一条全新 SQL 进来,优化器要回答“怎么查最快”——是全表扫、走索引,还是先关联再过滤?这套定稿的施工方案,就叫执行计划。
执行计划本质是一棵“操作树”:叶子节点读表或索引,内部节点做连接、过滤、排序。生成它要经过三个车间:语法解析把 SQL 文本变成语法树(好比按语法把点单拆成“主料、配料、做法”);逻辑优化做等价变换,比如谓词下推——先把能过滤的条件提前执行,减少后面环节的原料量;物理优化则结合索引与统计信息,用代价模型选出最小成本的算子组合。
代价模型是优化器的“成本计算器”,它估计每条路径要读多少个数据页、处理多少行,再按权重折算成总代价,总代价最小的方案胜出:
研发一张卡片很贵,所以聪明的后厨绝不会做完就扔——把卡片挂上墙,下一个点同名菜的客人就能直接照做。这正是缓存存在的意义:把昂贵的“设计工序”沉淀下来,让后续请求坐享其成。
- IO 成本权重表示读取一个数据页的代价,磁盘访问越慢这个值越大
- 预计页数优化器根据索引和统计信息估算这条路径要扫描的数据页数量
- CPU 成本权重表示处理单行数据的计算代价,过滤、投影等算子都要计入
- 预计行数这条路径需要逐行处理的记录数,走索引时通常远小于全表扫
- 总代价所有算子的代价累加,优化器挑选总代价最小的方案作为最终执行计划
缓存命中条件
客人喊“宫保鸡丁,少辣”,后厨不会拿“宫保鸡丁,正常辣”的卡片应付——必须找到一张一字不差的卡片,连“少辣”这个备注都要对得上。数据库的命中规则比后厨更苛刻:不只是“大概一样”,而是字符级完全一致。
缓存命中的第一条件:SQL 文本逐字符一致。大小写不同、空格多一个、表名是否写全限定,都会让哈希值对不上。别小看这个,很多团队追问“为什么缓存不命中”时,答案就藏在语句格式的不一致里。
第二条件是会话环境一致:数据库名、Schema 相同,好比同名“鱼香肉丝”在不同分店其实是两道菜。第三条件是对象绑定一致:SQL 里引用的表、视图要解析到同一个物理对象,避免同名对象换了库仍被误认为可复用。工程实现上,数据库先对 SQL 文本做哈希,用哈希值快速定位候选,再逐项比对上下文:
点单用语不统一,卡片架再满也帮不上忙。这也是为什么规范 SQL 风格、让应用层统一生成语句,是提升命中率的第一块基石——后厨管这叫“接单前先对暗号”。
- 新查询哈希把新到的 SQL 文本做哈希计算,作为查找缓存的钥匙
- 缓存哈希比对只有哈希值相等才进入下一步细致校验,哈希不等直接判未命中
- 库环境一致当前会话连接的数据库必须与缓存生成时相同
- Schema 一致默认模式相同,避免同名表被解析到不同实体
- 与条件用逻辑“与”连接,三个条件要求全部成立才算一次真正命中
SQL参数化
后厨天天接到“少辣”“微辣”“加花生”“不要葱”的变体,每张点单都要重新研发一盘菜?大可不必——只需把口味备注抽成“辣度=?、花生=?”,一张工序卡就能服务所有微调。这就是参数化。
SQL 参数化,就是把查询中的字面量(具体常量值)替换成占位符?,让“结构相同、值不同”的 SQL 归一到同一个模板,从而命中同一份计划。不参数化的危害叫“字面量陷阱”:WHERE id = 5 和 WHERE id = 8 是两个不同的文本,各自硬解析,缓存被成千上万个一次性变体塞满。
数据库的补救手段有两级:简单参数化(自动把无风险的字面量替换为参数)和强制参数化(FORCE PARAMETERIZATION,把能用参数替换的都替换)。但参数化也引出“参数嗅探”风险:优化器在第一次硬解析时用当时的参数值做行数估算,定稿的计划可能不适配后来的数据分布——就像第一位客人的口味被写进了卡片,后面的客人未必觉得好吃:
统一点单口诀,后厨的卡片架利用率飙升;但也要记得,参数化不是银弹。面对数据分布极度不均的表,要结合监控及时“撤回”被带偏的次优卡片,别让一道菜把整本菜谱带沟里。
- 输入 SQL原始带字面量的查询,比如“查 id 等于 5 的用户”
- 归一化把常量替换为占位符,同时抹平多余空格,得到标准模板
- 字面量替换每个具体数值或字符串都变成问号,值本身不再参与匹配
- 缓存键归一化后的模板作为缓存查找键,结构相同的 SQL 共享同一计划
软硬解析对比
软解析像老厨师翻卡片:扫一眼、验证下食材,几十秒出餐。硬解析像研发新菜:试料、试火、试味,一上午就搭进去了。后厨最怕的是同一天来一百个客人,各点一道从没做过的新菜——这就是数据库的“解析风暴”。
术语上,硬解析(Hard Parse)指缓存未命中时走完整流程:语法解析、语义检查、逻辑/物理优化、生成计划并写入缓存;软解析(Soft Parse)指命中后跳过优化与生成,只做轻量校验直接复用。
硬解析是 CPU 密集且带锁的:优化器要做系统性代价估算,多个会话同时硬解析还会争抢共享内存的 latch,CPU 冲到 100% 而真正执行反而卡住。业界经验中,一次硬解析的开销可达软解析的百倍以上,具体差距视语句复杂度而定。软解析也非零成本——哈希查找、权限校验、对象解析仍在,只是量级小得多:
后厨防“研发风暴”靠的是卡片架上足够多的现成卡片;数据库防解析风暴,靠的是让每条业务 SQL 都能稳定命中——这是所有调优工作的原点,也是“复用价值”这四个字最直白的注脚。
- 硬解析成本完整的解析、优化、生成链条,CPU 密集且需要获取共享内存锁
- 软解析成本命中后的轻量校验与计划复用,数量级远小于硬解析
- 差距上界语句越复杂,优化空间越大,硬解析相对越昂贵
- 量级估计经验上两者可差两到三个数量级,因此命中率直接影响吞吐
计划失效机制
厨房换了新烤箱,旧卡片写“200 度烤 15 分钟”就不灵了;肉铺的进货价变了,原本“麻辣比清蒸便宜”的判断也要推翻。卡片挂久了,必须重写。计划失效正是数据库的自适应机制——它不是故障,而是纠偏。
导致执行计划失效的常见触发源有三个:统计信息更新(表的行数、数据分布变化超阈值,旧代价估算不再可信)、Schema/索引变更(重建索引、加列、删冗余索引,原计划可能不再可行或不再最优)、以及管理员显式刷新缓存(清空整个缓存池以释放内存或扫除污染)。
失效后会发生什么?缓存条目被标记无效或直接删除,下一次同样的 SQL 重新走硬解析。此外还有“软失效”:SQL Server 的重新编译阈值在数据量变化跨越一定百分比后触发自动重编译;计划老化则让长期不用的计划逐步降级直至被清除。用“有效期”来概括,就是三条倒计时谁先归零,计划就在那一刻作废:
卡片失效并不可怕,可怕的是失效后没有新卡片顶上。所以大促前预热高频计划、日常观察重编译次数,是后厨(DBA)的例行功课——世界变了,菜谱就要跟着变,这本就是自适应系统的本分。
- 统计信息倒计时距离上次统计信息更新的时间,更新后旧计划作废
- 结构变更倒计时索引或表结构发生 DDL 后需要重编译,此变量归零
- 老化上限计划在缓存中的最长存活时限,到期自动重建
- 取最小值三个触发源谁先到来,计划就在那一刻失效
缓存管理策略
卡片架不是无限的。常点的“宫保鸡丁”挂在顺手位,半年没人点的“九转大肠”要撤下来给新菜腾地方。后厨还要定期盘点:哪张卡片被翻得起了毛边(高频复用),哪张落满了灰(冷门)。
数据库的计划缓存是共享内存中的一块区域,有明确的大小上限。超过上限就必须淘汰——淘汰策略普遍是近似 LRU 的变体:越久没被复用的条目越先被清出。不少数据库还会做“代价加权”:一张虽不常用但重新硬解析极其昂贵的计划,值得比平庸的小计划多留一会儿。
缓存生命周期管理还包含冷热分层:热点计划长期驻留,冷计划逐步老化;有些系统支持“固定计划”,把关键 SQL 的计划钉在缓存里以防被挤出。内存配额要结合业务设置,太小则命中率上不去,太大则挤压数据页缓存,得不偿失。留存评分的直觉是:既看这张卡被翻了多少次,也看重写它要花多大代价:
好厨师长不会一味扩大卡片架,他懂得在“研发成本”与“占用空间”之间算账。缓存调优也是如此:在有限共享内存里平衡命中率与资源占用,而不是无脑加大内存。
- 复用频次计划在一段时间内被软解析命中的次数,越高说明越常用
- 重建代价如果该计划被淘汰,下次硬解析要付出的成本,越高越值得保留
- 空间占用计划在缓存中占用的内存字节数,越大越拖累整体利用率
- 留存评分评分高的计划优先保留,同时兼顾“常用”与“昂贵”两个维度
调优实践方法
厨师长每天看操作日志:宫保鸡丁被点了三百次、卡片被翻了多少回、哪张卡片总在重新写。这些数据帮他决定该备什么料、该固定哪些菜谱。DBA 的调优同样是“看数据说话”。
最核心的指标是缓存命中率(软解析占比)。命中率高说明大部分 SQL 在复用计划,但要清醒:命中率高不等于计划质量高——一份次优计划被反复复用,反而让慢查询长期潜伏。所以命中率之外,还要看重编译次数、缓存大小,以及 Top SQL 的单次耗时。
实操清单:应用层统一用预编译语句/绑定变量(JDBC PreparedStatement、MyBatis #{});统一 SQL 书写风格,杜绝大小写与空格的随机差异;批量写入用 executemany 而非循环拼接;避免在 SQL 里嵌入唯一性极高的常量(如时间戳)造成缓存污染;大促前主动预热高频计划;必要时对异常 SQL 刷新缓存,解除参数嗅探造成的次优绑定。衡量这一切的标尺,就是软解析占比:
让后厨的每一张卡片都“挂得对、用得上、不过期”,正是 SQL 优化的终局。你写出的每一行 SQL,都应该被数据库当作“熟客”对待——不用重新研发,直接按老规矩上菜,又快又稳。
- 软解析次数统计周期内命中缓存复用计划的次数
- 硬解析次数统计周期内完整解析生成计划的次数
- 分母总解析软硬解析之和代表全部解析工作量
- 命中率软解析占总解析次数的比例,越高说明复用越好
AI 应用场景
电商订单接口的软解析优化
阶段:数据库查询性能优化(在线推理服务的数据链路)
解决的问题:传统写法在应用层拼接带不同字面量的 SQL,每个请求都触发硬解析;AI 时代的模型服务追求低延迟,高并发下解析风暴直接拉高 P99 延迟。基于执行计划缓存机制,改用参数化/预编译后,命中率可从 40% 提升到 95% 以上。
具体案例:某订单查询接口 QPS 2000 时 CPU 打满,排查发现 Plan Cache 中同一逻辑的 SQL 有几千个变体。后端把 MyBatis 的 ${} 改成 #{},并统一 SQL 风格后,CPU 降至 40%,P99 下降约三倍。
大促前执行计划预热
阶段:容量规划与压测(大规模流量来临前的稳定性保障)
解决的问题:大促瞬间大量新 SQL 同时到达,共享内存锁竞争导致硬解析互相阻塞,形成“解析风暴”。提前把高频 SQL 的计划“加热”进缓存,能削平高峰期的解析开销。
具体案例:双十一前 DBA 用压测脚本把 Top 100 高频 SQL 各执行一遍,让计划在缓存中就位;再结合命中率曲线确认预热效果,确保零点流量洪峰到来时数据库能从容应对。
AI 特征服务的批量写入治理
阶段:离线特征回填与在线特征更新(AI 数据管道)
解决的问题:特征回填任务用一条条带唯一字面量的 INSERT/UPDATE 写库,把计划缓存塞满一次性计划,挤掉在线查询的高频计划,导致在线服务命中率崩塌。用批量绑定参数可让单条 SQL 骨架复用,并限制写入会话的缓存配额防止污染。
具体案例:某推荐系统特征回填每小时写入百万行,SQL 变体数量爆炸。数据工程师改用 executemany 批量参数化写入后,硬解析次数下降 90%,在线查询的命中率恢复稳定。
Python 演示
# -*- coding: utf-8 -*-# 模拟数据库执行计划缓存: 参数化, 命中统计与 LRU 淘汰import refrom collections import OrderedDict class PlanCache: def __init__(self, capacity=3): self.capacity = capacity self._plans_map = OrderedDict() # 归一化 SQL -> 执行计划 self._stats_map = {} # 表名 -> 统计信息版本号 self.hit_count = 0 self.miss_count = 0 def normalize(self, sql): # 把字符串常量和数字字面量替换为 ?, 结构相同的 SQL 归一到同一 key sql = re.sub(r"'[^']*'", "?", sql) sql = re.sub(r"\b\d+\b", "?", sql) return re.sub(r"\s+", " ", sql.strip()) def invalidate(self, table): # 模拟统计信息更新: 使涉及该表的缓存计划全部失效 if table not in self._stats_map: self._stats_map[table] = 0 self._stats_map[table] += 1 for key in list(self._plans_map.keys()): if table in key: del self._plans_map[key] def execute(self, sql): key = self.normalize(sql) if key in self._plans_map: # 软解析: 命中缓存直接复用 self.hit_count += 1 self._plans_map.move_to_end(key) return "soft parse, plan reused" # 硬解析: 生成新计划并写入缓存, 超容量按 LRU 淘汰 self.miss_count += 1 self._plans_map[key] = "plan_of_" + key[:16] if len(self._plans_map) > self.capacity: self._plans_map.popitem(last=False) return "hard parse, plan created" if __name__ == "__main__": pc = PlanCache(capacity=3) sqls = [ "SELECT * FROM users WHERE id = 5", "SELECT * FROM users WHERE id = 8", "UPDATE orders SET status = 1 WHERE id = 100", "SELECT * FROM users WHERE id = 12", "DELETE FROM logs WHERE id = 2", ] for s in sqls: print(pc.execute(s), "|", pc.normalize(s)) pc.invalidate("users") print("after stats update on users:") print(pc.execute("SELECT * FROM users WHERE id = 99")) total = pc.hit_count + pc.miss_count print("hit ratio:", round(pc.hit_count / total, 2))
sql = re.sub(r"'[^']*'", "?", sql)把所有字符串常量统一替换为问号,消除文本差异sql = re.sub(r"\b\d+\b", "?", sql)把数字字面量也替换为问号,让id = 5与id = 8共享模板if key in self._plans_map:判定命中,走软解析分支,直接复用已有计划self._plans_map.move_to_end(key)命中后把条目移到末尾,模拟 LRU 的“最近使用”标记self._plans_map.popitem(last=False)缓存超容量时,弹出最久没被使用的条目pc.invalidate("users")模拟统计信息更新,涉及 users 表的计划全部失效,下次查询重新硬解析round(pc.hit_count / total, 2)计算软解析占比,即文章反复强调的命中率指标
常见误区
误区我在数据库里看到两条 SQL 查的是同一张表、返回结果也差不多,但 Plan Cache 里却存了两个计划。我一直以为只要查询“长得像”就能复用。
正解缓存匹配是字符级的,不是语义级的。大小写、空格、注释、字面量差异都会导致文本不同;只有经过参数化之后,才升级为结构匹配。就像后厨要找的是一字不差的点单,而不是“大概类似的菜”——差一个“少辣”备注都得重新研发。
为何常见:人们常把“查询语义相同”误当成“SQL 文本相同”,而数据库为了快速检索,只认原文哈希,不做语义理解。
误区我把所有查询都改成绑定变量,以为这样性能一定提升,结果个别 SQL 反而变慢了,很困惑。
正解参数化提升的是解析阶段的复用率,但优化器依赖参数值估算行数,第一个参数值可能诱导生成次优计划(参数嗅探),执行阶段反而更慢。需要监控实际执行计划与耗时,必要时用 OPTION (RECOMPILE) 或计划提示纠正,让后厨重新写一张合口味的卡片。
为何常见:把“命中率高”等同于“性能好”,忽略了计划质量这个隐藏变量。
误区我把共享池调得特别大,希望缓存更多计划,结果数据库整体性能没提升,反而频繁换页。
正解计划缓存与数据页缓存共用内存池,过度分配会挤压热数据缓存,导致物理读增加。命中率存在边际效应,关键是把业务 SQL 的种类控制住,而不是无限扩容卡片架。
为何常见:直观上“存得多=命中多”,但忽略了内存资源此消彼长的关系。
误区我以为只要命中缓存就等于零开销,压测时却发现 CPU 依然很高,缓存好像“白做了”。
正解软解析仍需哈希查找、语法校验、权限检查与对象解析,只是省掉了代价最高的优化与生成环节。与硬解析相比是数量级的差距,但不是零。后厨翻卡片也要时间,只是远比研发新菜快。
为何常见:软硬解析差距太悬殊,让人误以为小的一方为 0,忽略了软解析在高 QPS 下的累计开销。
面试题
Q1. Plan Cache 的主要作用是什么?
- A.缓存查询结果集,避免重复读磁盘
- B.缓存解析后的执行计划,避免重复优化← 正确答案
- C.缓存数据库表结构,加快 DDL 速度
- D.缓存用户权限数据,加快权限校验
答案:B。Plan Cache 缓存的是解析优化后的执行计划,让相同 SQL 跳过最贵的优化环节。选 A 的人混淆了结果集缓存与计划缓存;C、D 分别属于元数据缓存与权限体系,与计划复用无关。
Q2. 「SELECT * FROM users WHERE id = 5」与下列哪条 SQL 最可能复用同一执行计划?
- A.SELECT * FROM users WHERE id = 6← 正确答案
- B.select * from users where id = 5
- C.SELECT id FROM users WHERE id = 5
- D.SELECT * FROM orders WHERE id = 5
答案:A。在简单参数化下,仅字面量 5 换成 6 的 A 会被归一为同一模板而命中。B 大小写改变导致文本不一致;C 查询列不同,结构已变;D 换了表对象,都不可能命中。
Q3. 硬解析相对软解析的主要额外开销是什么?
- A.网络传输查询结果数据的开销
- B.优化器做代价估算与计划生成← 正确答案
- C.客户端应用编译 SQL 的开销
- D.磁盘读取索引页数据的开销
答案:B。硬解析多出的核心环节是优化器基于统计信息做代价估算并生成计划,这是 CPU 密集且带锁的过程。A、D 是执行阶段的 IO/网络开销,C 发生在客户端,都不是解析环节的差异。
Q4. 哪些情况会使已缓存的执行计划失效?
- A.表上的统计信息被更新后← 正确答案
- B.相关索引被重建或删除← 正确答案
- C.优化器版本或参数变更← 正确答案
- D.应用服务器发生重启
答案:ABC。统计信息更新、索引结构变更、优化器版本变化都会让旧计划不再可靠。D 应用重启只会清空应用侧连接池,数据库中的计划缓存依然存活,这是区分前后端缓存的关键点。
Q5. SQL 参数化的核心思想是什么?
- A.将常量替换为占位符共享计划← 正确答案
- B.将所有 SQL 合并成一条
- C.对 SQL 文本进行压缩加密
- D.把查询拆成多个小查询
答案:A。参数化把字面量挖掉换成?,让结构相同的 SQL 归一到同一模板并共享计划。B 是语句合并,C 与安全相关,D 是查询拆分,都不描述参数化的本质。
Q6. 关于参数嗅探,下列说法正确的是?
- A.攻击者通过网络截获 SQL 参数
- B.首参定计划未必适配后续数据← 正确答案
- C.参数嗅探能提高缓存命中率
- D.参数嗅探只出现在 NoSQL 中
答案:B。参数嗅探指优化器在首次硬解析时用当时的参数值估算行数并定稿计划,后续数据分布变化时该计划可能次优。它是一把双刃剑,不代表命中率提升,也并非 NoSQL 专属概念。
Q7. 命中率很高但慢查询仍增加,最可能的原因?
- A.缓存命中率统计本身有误
- B.复用的计划本身非最优← 正确答案
- C.计划缓存的内存空间不足
- D.数据库的网络带宽不足
答案:B。高命中率只说明“复用得多”,不说明“计划好”。统计信息过期或参数嗅探会让一份次优计划被反复复用,慢查询自然居高不下。这正是“命中率不等于性能”的典型反例。
Q8. 下列哪项不是“代价加权淘汰”的设计目的?
- A.保留高价值重计划降低硬解析
- B.低频但昂贵的计划值得留存
- C.使缓存大小等于数据总量← 正确答案
- D.避免高频低成本计划挤掉贵计划
答案:C。代价加权淘汰的初衷是综合考虑复用频次与重建代价,避免“便宜货挤走贵货”。缓存大小永远受共享内存约束,不可能等于数据总量,C 是错误理解,因此不属于设计目的。
Q9. 哪些措施能提高执行计划缓存命中率?
- A.统一 SQL 书写风格与格式← 正确答案
- B.使用预编译语句绑定变量← 正确答案
- C.每条请求拼接不同常量
- D.避免动态改变查询结构← 正确答案
答案:ABD。统一书写风格保证文本一致,预编译绑定变量实现参数化,固定查询结构让模板稳定,三者都直接提升命中率。C 恰好制造大量一次性字面量变体,是缓存污染的反面教材。
Q10. 关于软解析与硬解析,说法正确的有?
- A.软解析仍需权限与语义校验← 正确答案
- B.硬解析包含代价估算与生成← 正确答案
- C.软解析完全不消耗 CPU 资源
- D.高频硬解析易引发 CPU 升高← 正确答案
答案:ABD。软解析省掉的是优化和生成,但哈希查找、权限校验仍在,并非零成本;硬解析包含完整代价估算与计划生成,高频并发硬解析会因 CPU 密集和锁竞争抬高负载。C 把“小”误当“无”,是典型认知偏差。