非 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 = ?
。 - 然后,使用参数化后的查询在 Plan Cache 中查找。
- 如果能找到可以直接复用的计划,则直接使用,并跳过整个优化过程。
- 否则,继续进行查询优化,并在最后将生成的计划放回到缓存中,以便下次复用。
使用方法
目前,你可以通过 tidb_enable_non_prepared_plan_cache
开启或关闭 Non-Prepared Plan Cache。同时,你还可以通过 tidb_session_plan_cache_size
来控制 Plan Cache 的大小。当缓存的计划数超过 tidb_session_plan_cache_size
时,TiDB 会使用 LRU (Least Recently Used) 策略进行逐出。
从 v7.1.0 开始,你可以通过变量 tidb_plan_cache_max_plan_size
来设置可以缓存的计划的最大大小,默认为 2 MB。超过该值的执行计划将不会被缓存到 Plan Cache 中。
示例
下面是一个使用示例:
创建用于测试的表
t
:CREATE TABLE t (a INT, b INT, KEY(b));开启 Non-Prepared Plan Cache:
SET tidb_enable_non_prepared_plan_cache = ON;依次执行以下查询:
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 也不支持。
- 不支持包含
Window
或Having
的查询。 - 不支持包含三张表及以上
Join
或子查询的查询。 - 不支持
ORDER BY
或者GROUP BY
后直接带数字或者表达式的查询,如ORDER BY 1
、GROUP BY a+1
。仅支持ORDER BY column_name
和GROUP BY column_name
。 - 不支持过滤条件中包含
JSON
、ENUM
、SET
或BIT
类型的列的查询,例如SELECT * FROM t WHERE json_col = '{}'
。 - 不支持过滤条件中出现
NULL
值的查询,例如SELECT * FROM t WHERE a is NULL
。 - 不支持参数化后参数个数超过 200 个的查询,例如
SELECT * FROM t WHERE a in (1, 2, 3, ... 201)
。 - 不支持访问分区表、虚拟列、临时表、视图、或内存表的查询,例如
SELECT * FROM INFORMATION_SCHEMA.COLUMNS
,其中COLUMNS
为 TiDB 内存表。 - 不支持带有 Hint 或有 Binding 的查询。
- 默认不支持 DML 语句或包含
FOR UPDATE
的查询语句。若要启用支持,你可以执行SET tidb_enable_non_prepared_plan_cache_for_dml = ON
。
开启此功能后,优化器会对查询进行快速判断,如果不满足 Non-Prepared Plan Cache 的支持条件,则会走正常的优化流程。
性能收益
在内部测试中,开启 Non-Prepared Plan Cache 功能在大多数 TP 场景下可以获得显著的性能收益。但是它也有代价,其自身也有一些额外的性能开销,包括判断查询是否支持、对查询进行参数化等。如果此功能不支持负载中的大多数查询,开启此功能反而可能影响性能。
此时,你需要观察 Grafana 监控中的 Queries Using Plan Cache OPS 面板中的 non-prepared
指标和 Plan Cache Miss OPS 面板中的 non-prepared-unsupported
指标。如果大多数查询都无法被支持,只有少部分查询能命中 Plan Cache,此时你可以关闭此功能。
诊断
开启 Non-Prepared Plan Cache 后,可以使用 EXPLAIN FORMAT='plan_cache' SELECT ...
语句验证查询是否能够命中缓存。对于无法命中缓存的查询,系统会通过 warning 的方式返回无法命中的原因。
需要注意的是,如果不加 FORMAT='plan_cache'
,则 EXPLAIN
语句永远不会命中缓存。
执行下面 EXPLAIN FORMAT='plan_cache'
语句,查看查询是否能够命中:
EXPLAIN FORMAT='plan_cache' SELECT * FROM (SELECT a+1 FROM t1) t;
输出结果示例如下:
3 rows in set, 1 warning (0.00 sec)
通过 SHOW warnings;
查看无法命中缓存的查询信息:
SHOW warnings;
输出结果示例如下:
+---------+------+-------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+-------------------------------------------------------------------------------+
| Warning | 1105 | skip non-prepared plan-cache: queries that have sub-queries are not supported |
+---------+------+-------------------------------------------------------------------------------+
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 = ON;依次执行以下三个查询:
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 query_sample_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 | 3 | 1 | 2 | +---------------------------------+------------------------------------------+------------+---------------+-----------------+ 1 row in set (0.01 sec)可以看到,查询执行了三次且命中缓存两次。