执行计划缓存

    • 使用 COM_STMT_PREPARECOM_STMT_EXECUTE 的协议功能;
    • 执行 Prepare / Execute SQL 语句查询;

    TiDB 优化器对这两类查询的处理是一样的:Prepare 时将参数化的 SQL 查询解析成 AST(抽象语法树),每次 Execute 时根据保存的 AST 和具体的参数值生成执行计划。

    当开启执行计划缓存后,每条 Prepare 语句的第一次 Execute 会检查当前查询是否可以使用执行计划缓存,如果可以则将生成的执行计划放进一个由 LRU 链表构成的缓存中;在后续的 Execute 中,会先从缓存中获取执行计划,并检查是否可用,如果获取和检查成功则跳过生成执行计划这一步,否则重新生成执行计划并放入缓存中。

    在当前版本中,当 Prepare 语句符合以下条件任何一条,查询或者计划不会被缓存:

    • SELECTUPDATEINSERTDELETEUnionIntersectExcept 以外的 SQL 语句;
    • 访问分区表、临时表或访问表中包含生成列的查询;
    • 包含子查询的查询,如 select * from t where a > (select ...)
    • 包含 ignore_plan_cache 这一 Hint 的查询,例如 select /*+ ignore_plan_cache() */ * from t
    • 包含除 ? 外其他变量(即系统变量或用户自定义变量)的查询,例如 select * from t where a>? and b>@x
    • 查询包含无法被缓存函数。目前不能被缓存的函数有:database()current_usercurrent_roleuserconnection_idlast_insert_idrow_countversionlike
    • ?Limit 后的查询,如 Limit ? 或者 Limit 10, ?,此时 ? 的具体值对查询性能影响较大,故不缓存;
    • ? 直接在 Order By 后的查询,如 Order By ?,此时 ? 表示根据 Order By 后第几列排序,排序列不同的查询使用同一个计划可能导致错误结果,故不缓存;如果是普通表达式,如 Order By a+? 则会缓存;
    • ? 紧跟在 Group by 后的查询,如 Group By ?,此时 表示根据 Group By 后第几列聚合,聚合列不同的查询使用同一个计划可能导致错误结果,故不缓存;如果是普通表达式,如 Group By a+? 则会缓存;
    • ? 出现在窗口函数 Window Frame 定义中的查询,如 (partition by year order by sale rows ? preceding);如果 ? 出现在窗口函数的其他位置,则会缓存;
    • 用参数进行 intstring 比较的查询,如 c_int >= ? 或者 c_int in (?, ?)等,其中 ? 为字符串类型,如 set @x='123';此时为了保证结果和 MySQL 兼容性,需要每次对参数进行调整,故不会缓存;
    • 会访问 TiFlash 的计划不会被缓存;
    • 大部分情况下计划中含有 TableDual 的计划将将不会被缓存,除非当前执行的 Prepare 语句不含参数,则对应的 TableDual 计划可以被缓存。

    LRU 链表是设计成 session 级别的缓存,因为 Prepare / Execute 不能跨 session 执行。LRU 链表的每个元素是一个 key-value 对,value 是执行计划,key 由如下几部分组成:

    • 执行 Execute 时所在数据库的名字;
    • Prepare 语句的标识符,即紧跟在 PREPARE 关键字后的名字;
    • 当前的 schema 版本,每条执行成功的 DDL 语句会修改 schema 版本;
    • 当前设置的时区,即系统变量 time_zone 的值;
    • 系统变量 sql_select_limit 的值;

    key 中任何一项变动(如切换数据库,重命名 Prepare 语句,执行 DDL,或修改 SQL Mode / time_zone 的值),或 LRU 淘汰机制触发都会导致 Execute 时无法命中执行计划缓存。

    成功从缓存中获取到执行计划后,TiDB 会先检查执行计划是否依然合法,如果当前 Execute 在显式事务里执行,并且引用的表在事务前序语句中被修改,而缓存的执行计划对该表访问不包含 UnionScan 算子,则它不能被执行。

    关于执行计划缓存和查询性能有几点值得注意:

    • 不管计划是否已经被缓存,都会受到 SQL Binding 的影响。对于没有被缓存的计划,即在第一次执行 Execute 时,会受到已有 SQL Binding 的影响;而对于已经缓存的计划,如果有新的 SQL Binding 被创建产生,则原有已经被缓存的计划会失效。
    • 已经被缓存的计划不会受到统计信息更新、优化规则和表达式下推黑名单更新的影响,仍然会使用已经保存在缓存中的计划。
    • 重启 TiDB 实例时(如不停机滚动升级 TiDB 集群),Prepare 信息会丢失,此时执行 execute stmt ... 可能会遇到 Prepared Statement not found 的错误,此时需要再执行一次 prepare stmt ...
    • 考虑到不同 Execute 的参数会不同,执行计划缓存为了保证适配性会禁止一些和具体参数值密切相关的激进查询优化手段,导致对特定的一些参数值,查询计划可能不是最优。比如查询的过滤条件为 where a > ? and a < ?,第一次 Execute 时参数分别为 2 和 1,考虑到这两个参数下次执行时可能会是 1 和 2,优化器不会生成对当前参数最优的 TableDual 执行计划。
    • 如果不考虑缓存失效和淘汰,一份执行计划缓存会对应各种不同的参数取值,理论上也会导致某些取值下执行计划非最优。比如查询过滤条件为 where a < ?,假如第一次执行 Execute 时用的参数值为 1,此时优化器生成最优的 IndexScan 执行计划放入缓存,在后续执行 Exeucte 时参数变为 10000,此时 TableScan 可能才是更优执行计划,但由于执行计划缓存,执行时还是会使用先前生成的 IndexScan。因此执行计划缓存更适用于查询较为简单(查询编译耗时占比较高)且执行计划较为固定的业务场景。

    目前执行计划缓存功能默认关闭,可以通过打开配置文件中 prepared-plan-cache启用这项功能。

    在开启了执行计划缓存功能后,可以通过 session 级别的系统变量 last_plan_from_cache 查看上一条 语句是否使用了缓存的执行计划,例如:

    如果发现某一组 Prepare / Execute 由于执行计划缓存导致了非预期行为,可以通过 SQL Hint ignore_plan_cache() 让该组语句不使用缓存。还是用上述的 stmt 为例:

    1. MySQL [test]> prepare stmt from 'select /*+ ignore_plan_cache() */ * from t where a = ?';
    2. Query OK, 0 rows affected (0.00 sec)
    3. MySQL [test]> set @a = 1;
    4. Query OK, 0 rows affected (0.00 sec)
    5. MySQL [test]> execute stmt using @a;
    6. Empty set (0.00 sec)
    7. MySQL [test]> select @@last_plan_from_cache;
    8. +------------------------+
    9. | @@last_plan_from_cache |
    10. +------------------------+
    11. +------------------------+
    12. 1 row in set (0.00 sec)
    13. MySQL [test]> execute stmt using @a;
    14. Empty set (0.00 sec)
    15. MySQL [test]> select @@last_plan_from_cache;
    16. +------------------------+
    17. | @@last_plan_from_cache |
    18. +------------------------+
    19. | 0 |
    20. +------------------------+
    21. 1 row in set (0.00 sec)

    该语句中的作用域 [SESSION | INSTANCE] 用于指定需要清空的缓存级别,可以为 SESSIONINSTANCE。如果不指定作用域,该语句默认清空 SESSION 级别的缓存。

    下面是一个清空计划缓存的例子:

    TiDB 暂不支持清空 GLOBAL 级别的计划缓存,即不支持一次性清空整个集群的计划缓存,使用时会报错:

    1. MySQL [test]> admin flush global plan_cache;
    2. ERROR 1105 (HY000): Do not support the 'admin flush global scope.'

    忽略 COM_STMT_CLOSE 指令和 DEALLOCATE PREPARE 语句

    为了减少每次执行 SQL 语句的语法分析,Prepared Statement 推荐的使用方式是,prepare 一次,然后 execute 多次,最后 deallocate prepare。例如:

    如果你习惯于在每次 execute 后都立即执行 deallocate prepare,如:

    1. MySQL [test]> prepare stmt from '...'; -- 第一次 prepare
    2. MySQL [test]> execute stmt using ...;
    3. MySQL [test]> deallocate prepare stmt; -- 一次使用后立即释放
    4. MySQL [test]> prepare stmt from '...'; -- 第二次 prepare
    5. MySQL [test]> execute stmt using ...;

    这样的使用方式会让第一次执行得到的计划被立即清理,不能在第二次被复用。