非 Prepare 语句执行计划缓存
对于某些非 PREPARE 语句,TiDB 可以像 Prepare/Execute 语句一样支持执行计划缓存。这可以让这些语句跳过优化器阶段,以提升性能。
原理
Non-Prepared Plan Cache 为会话级别,并且与 Prepared Plan Cache 相互独立,缓存的计划互不影响。Non-Prepared Plan Cache 功能的基本原理如下:
- 开启 Non-Prepared Plan Cache 后,TiDB 首先根据 AST(抽象语法树)对查询进行参数化。例如,将
SELECT * FROM t WHERE b < 10 AND a = 1参数化为SELECT * FROM t WHERE b < ? and a = ?。 - 然后,使用参数化后的查询在 Non-Prepared Plan Cache 中查找。
- 如果能找到可以直接复用的计划,则直接使用,并跳过整个优化过程。
- 否则,继续进行查询优化,并在最后将生成的计划放回到缓存中,以便下次复用。
使用方法
目前,你可以通过 tidb_enable_non_prepared_plan_cache 开启或关闭 Non-Prepared Plan Cache。同时,你还可以通过 tidb_non_prepared_plan_cache_size 来控制 Non-Prepared Plan Cache 的大小。当缓存的计划数超过 tidb_non_prepared_plan_cache_size 时,TiDB 会使用 LRU (Least Recently Used) 策略进行逐出。
示例
下面是一个使用示例:
创建用于测试的表
t:CREATE TABLE t (a INT, b INT, KEY(b));开启 Non-Prepared Plan Cache:
SET tidb_enable_non_prepared_plan_cache = true;依次执行以下查询:
SELECT * FROM t WHERE b < 10 AND a = 1; SELECT * FROM t WHERE b < 5 AND a = 2;查看第二个查询语句是否命中缓存:
SELECT @@last_plan_from_cache;输出结果中
last_plan_from_cache的值为1,表示第二次执行的查询计划来自于缓存:+------------------------+ | @@last_plan_from_cache | +------------------------+ | 1 | +------------------------+ 1 row in set (0.00 sec)
限制
TiDB 对参数化后形式相同的查询,只能缓存一个计划。例如,对于 SELECT * FROM t WHERE a < 1 和 SELECT * FROM t WHERE a < 100000 这两个查询语句,由于参数化后的形式相同,均为 SELECT * FROM t WHERE a < ?,因此它们会共用一个计划。
如果由此产生性能问题,可以使用 ignore_plan_cache() Hint 忽略计划缓存中的计划,让优化器每次重新为 SQL 生成执行计划。如果无法修改 SQL,可以通过创建 binding 来解决,例如 CREATE BINDING FOR SELECT ... USING SELECT /*+ ignore_plan_cache() */ ...。
由于上述风险以及执行计划缓存只在简单查询上有明显收益(如果查询较为复杂,查询本身执行时间较长,使用执行计划缓存收益不大),TiDB 目前对 Non-Prepared Plan Cache 的生效范围有严格的限制。具体限制如下:
- Prepared Plan Cache 不支持的查询或者计划,Non-Prepared Plan Cache 也不支持。
- 目前仅支持包含
Scan、Selection或Projection算子的单表的点查或范围查询,例如SELECT * FROM t WHERE a < 10 AND b in (1, 2)。 - 不支持包含
Agg、Limit、Window或Sort等复杂算子的查询。 - 不支持包含非范围查询条件,例如:
- 不支持
LIKE,例如c LIKE 'c%' - 不支持
+操作,例如a+1 < 2
- 不支持
- 不支持过滤条件中包含
JSON、ENUM、SET或BIT类型的列的查询,例如SELECT * FROM t WHERE json_col = '{}'。 - 不支持过滤条件中出现
NULL值的查询,例如SELECT * FROM t WHERE a is NULL。 - 不支持参数化后参数个数超过 50 个的查询,例如
SELECT * FROM t WHERE a in (1, 2, 3, ... 51)。 - 不支持访问分区表、虚拟列、临时表、视图、或内存表的查询,例如
SELECT * FROM INFORMATION_SCHEMA.COLUMNS,其中COLUMNS为 TiDB 内存表。 - 不支持带有 Hint、子查询、Lock 的查询。
- 不支持 DML 语句。
诊断
开启 Non-Prepared Plan Cache 后,可以使用 EXPLAIN FORMAT='plan_cache' SELECT ... 语句验证查询是否能够命中缓存。对于无法命中缓存的查询,系统会通过 warning 的方式返回无法命中的原因。
需要注意的是,如果不加 FORMAT='plan_cache',则 EXPLAIN 语句永远不会命中缓存。
执行下面 EXPLAIN FORMAT='plan_cache' 语句,查看查询是否能够命中:
EXPLAIN FORMAT='plan_cache' SELECT * FROM t WHERE a+2 < 10;
输出结果示例如下:
3 rows in set, 1 warning (0.00 sec)
通过 SHOW warnings; 查看无法命中缓存的查询信息:
SHOW warnings;
输出结果示例如下:
+---------+------+-----------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------------------------------+
| Warning | 1105 | skip non-prep plan cache: query has some unsupported binary operation |
+---------+------+-----------------------------------------------------------------------+
1 row in set (0.00 sec)
可以看到在上述例子中,由于 Non-Prepared Plan Cache 不支持 + 操作,所以无法命中缓存。
监控
开启 Non-Prepared Plan Cache 后,可以在以下几个面板中查看缓存的内存使用情况、缓存中计划的个数、缓存命中的情况等信息。

statements_summary 表和慢查询日志也会体现缓存的命中情况。下面是查看 statements_summary 表中缓存命中情况的例子:
创建表
t:CREATE TABLE t (a int);打开 Non-Prepared Plan Cache 开关:
SET @@tidb_enable_non_prepared_plan_cache=1;依次执行以下三个查询:
SELECT * FROM t WHERE a<1; SELECT * FROM t WHERE a<2; SELECT * FROM t WHERE a<3;查询
statements_summary表查看查询命中缓存的情况:SELECT digest_text, query_sample_text, exec_count, plan_in_cache, plan_cache_hits FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY WHERE digest_text LIKE '%SELECT * FROM %';输出结果如下:
+---------------------------------+------------------------------------------+------------+---------------+-----------------+ | digest_text | query_sample_text | exec_count | plan_in_cache | plan_cache_hits | +---------------------------------+------------------------------------------+------------+---------------+-----------------+ | SELECT * FROM `t` WHERE `a` < ? | SELECT * FROM t WHERE a<1 [arguments: 1] | 3 | 1 | 2 | +---------------------------------+------------------------------------------+------------+---------------+-----------------+ 1 row in set (0.01 sec)可以看到,查询执行了三次且命中缓存两次。