非 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)可以看到,查询执行了三次且命中缓存两次。