SQL 準備済み実行プラン キャッシュ
TiDB は、 PrepareとExecuteクエリの実行プラン キャッシュをサポートしています。これには、準備されたステートメントの両方の形式が含まれます。
- プロトコル機能
COM_STMT_PREPAREおよびCOM_STMT_EXECUTEを使用します。 - SQL ステートメント
PREPAREとEXECUTEを使用します。
TiDB オプティマイザーは、これら 2 種類のクエリを同じ方法で処理します。準備時に、パラメーター化されたクエリは AST (抽象構文ツリー) に解析され、キャッシュされます。その後の実行時に、保存された AST と特定のパラメーター値に基づいて実行プランが生成されます。
実行プラン キャッシュを有効にすると、最初の実行では、 Prepareステートメントごとに現在のクエリが実行プラン キャッシュを使用できるかどうかが確認され、使用できる場合は、生成された実行プランが LRU (Least Recently Used) リンク リストによって実装されたキャッシュに格納されます。後続のExecuteのクエリでは、実行プランがキャッシュから取得され、使用可能かどうかがチェックされます。チェックが成功した場合、実行プランを生成する手順はスキップされます。それ以外の場合は、実行プランが再生成され、キャッシュに保存されます。
TiDB は、 Prepare / Executeステートメントと同様に、一部のPREPARE以外のステートメントの実行プランのキャッシュもサポートしています。詳細については、 準備されていないプラン キャッシュを参照してください。
TiDB の現在のバージョンでは、 Prepareステートメントが次のいずれかの条件を満たす場合、クエリまたはプランはキャッシュされません。
- クエリには、
SELECT、UPDATE、INSERT、DELETE、Union、Intersect、およびExcept以外の SQL ステートメントが含まれています。 - クエリは、パーティション テーブルまたは一時テーブル、または生成された列を含むテーブルにアクセスします。
- クエリには、
SELECT * FROM t1 WHERE t1.a > (SELECT 1 FROM t2 WHERE t2.b < 1)などの相関のないサブクエリが含まれています。 - クエリには、実行プランに
SELECT * FROM t1 WHERE t1.a > (SELECT a FROM t2 WHERE t1.b > t2.b)などのPhysicalApplyの演算子を持つ相関サブクエリが含まれています。 - クエリには、
SELECT /*+ ignore_plan_cache() */ * FROM tやSELECT /*+ set_var(max_execution_time=1) */ * FROM tなどのignore_plan_cacheまたはset_varヒントが含まれています。 - クエリには、
select * from t where a>? and b>@xなどの?以外の変数 (システム変数またはユーザー定義変数を含む) が含まれています。 - クエリには、キャッシュできない関数
database()、current_user、current_role、user、connection_id、last_insert_id、row_count、version、およびlike含まれています。 - クエリでは、
LIMITパラメータとして変数 (LIMIT ?やLIMIT 10, ?など) が使用され、変数の値は 10000 を超えています。 - クエリには
Order By ?のようにOrder Byの後に?含まれます。このようなクエリは?で指定された列に基づいてデータをソートします。異なる列をターゲットとするクエリが同じ実行プランを使用すると、結果は間違ってしまいます。そのため、このようなクエリはキャッシュされません。ただし、Order By a+?などの一般的なクエリの場合はキャッシュされます。 - クエリには
Group By?のようにGroup Byの後に?含まれます。このようなクエリは?で指定された列に基づいてデータをグループ化します。異なる列をターゲットとするクエリが同じ実行プランを使用すると、結果は間違ってしまいます。そのため、このようなクエリはキャッシュされません。ただし、Group By a+?などの一般的なクエリの場合はキャッシュされます。 - クエリには、
Window Frameウィンドウ関数の定義に(partition by year order by sale rows ? preceding)などの?含まれています。ウィンドウ関数の他の場所に?出現する場合、クエリはキャッシュされます。 - クエリには、
intとstring比較するためのパラメータ(c_int >= ?やc_int in (?, ?)など)が含まれており、?set @x='123'などの文字列型を示します。クエリ結果が MySQL と互換性があることを保証するには、各クエリでパラメータを調整する必要があり、このようなクエリはキャッシュされません。 - この計画は
TiFlashにアクセスしようとします。 - ほとんどの場合、現在の
Prepareステートメントにパラメータがない限り、TableDual含むプランはキャッシュされません。 - クエリは、
information_schema.columnsなどの TiDB システム ビューにアクセスします。システム ビューにアクセスするためにPrepareおよびExecuteステートメントを使用することは推奨されません。
TiDB では、クエリ内の?の数に制限があります。クエリに 65535 を超える?が含まれている場合、エラーPrepared statement contains too many placeholdersが報告されます。
LRU リンク リストは、 Prepare間で実行できExecuteため、セッション レベルのキャッシュとして設計されています。LRU リストの各要素は、キーと値のペアです。値は実行プランであり、キーは次の部分で構成されます。
Execute実行されるデータベースの名前Prepare番目のステートメントの識別子、つまりPREPAREのキーワードの後の名前- 現在のスキーマバージョン。DDL文が正常に実行されるたびに更新されます。
Execute実行するときのSQLモード- 現在のタイムゾーン(
time_zoneシステム変数の値) sql_select_limitシステム変数の値
前述の情報の変更 (たとえば、データベースの切り替え、 Prepare文の名前変更、DDL 文の実行、SQL モード/ time_zoneの値の変更)、または LRU キャッシュ削除メカニズムにより、実行時に実行プランのキャッシュ ミスが発生します。
実行プラン キャッシュがキャッシュから取得された後、TiDB はまず実行プランがまだ有効かどうかを確認します。現在のExecuteステートメントが明示的なトランザクションで実行され、参照先のテーブルがトランザクション プリオーダー ステートメントで変更された場合、このテーブルにアクセスするキャッシュされた実行プランにUnionScan演算子が含まれていないため、実行できません。
検証テストに合格すると、実行プランのスキャン範囲が現在のパラメータ値に応じて調整され、データクエリの実行に使用されます。
実行プランのキャッシュとクエリ パフォーマンスに関して注目すべき点がいくつかあります。
- 実行プランがキャッシュされているかどうかに関係なく、SQL バインディングの影響を受けます。キャッシュされていない実行プラン (最初の
Execute) の場合、これらのプランは既存の SQL バインディングの影響を受けます。キャッシュされている実行プランの場合、新しい SQL バインディングが作成されると、これらのプランは無効になります。 - キャッシュされたプランは、統計、最適化ルール、および式によるブロックリストのプッシュダウンの変更の影響を受けません。
Executeのパラメータが異なることを考慮して、実行プラン キャッシュは、適応性を確保するために、特定のパラメータ値に密接に関連するいくつかの積極的なクエリ最適化方法を禁止します。これにより、クエリ プランが特定のパラメータ値に対して最適でない場合があります。たとえば、クエリのフィルタ条件がwhere a > ? And a < ?で、最初のExecuteのステートメントのパラメータがそれぞれ2と1であるとします。次の実行時にこれらの 2 つのパラメータが1と2になる可能性があることを考慮すると、オプティマイザは現在のパラメータ値に固有の最適なTableDual実行プランを生成しません。- キャッシュの無効化と削除を考慮しない場合、実行プラン キャッシュはさまざまなパラメーター値に適用され、理論的には特定の値に対して最適でない実行プランが生成されます。たとえば、フィルター条件が
where a < ?で、最初の実行に使用されたパラメーター値が1の場合、オプティマイザーは最適なIndexScan実行プランを生成し、それをキャッシュに格納します。後続の実行で値が10000になった場合、TableScanプランの方が適している可能性があります。ただし、実行プラン キャッシュがあるため、実行には以前に生成されたIndexScanが使用されます。したがって、実行プラン キャッシュは、クエリが単純 (コンパイル率が高い) で実行プランが比較的固定されているアプリケーション シナリオに適しています。
v6.1.0 以降では、実行プラン キャッシュがデフォルトで有効になっています。準備されたプラン キャッシュは、システム変数tidb_enable_prepared_plan_cacheを介して制御できます。
注記:
実行プラン キャッシュ機能は
PrepareExecuteにのみ適用され、通常のクエリには効果がありません。
実行プラン キャッシュ機能を有効にすると、セッション レベルのシステム変数last_plan_from_cacheを使用して、前のExecuteステートメントでキャッシュされた実行プランが使用されたかどうかを確認できます。次に例を示します。
MySQL [test]> create table t(a int);
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> prepare stmt from 'select * from t where a = ?';
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> set @a = 1;
Query OK, 0 rows affected (0.00 sec)
-- The first execution generates an execution plan and saves it in the cache.
MySQL [test]> execute stmt using @a;
Empty set (0.00 sec)
MySQL [test]> select @@last_plan_from_cache;
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 0 |
+------------------------+
1 row in set (0.00 sec)
-- The second execution hits the cache.
MySQL [test]> execute stmt using @a;
Empty set (0.00 sec)
MySQL [test]> select @@last_plan_from_cache;
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 1 |
+------------------------+
1 row in set (0.00 sec)
実行プラン キャッシュが原因で、特定のPrepareセットに予期しない動作があることがわかった場合は、 ignore_plan_cache() SQL ヒントをExecuteして、現在のステートメントの実行プラン キャッシュの使用をスキップできます。それでも、前述のステートメントを例として使用します。
MySQL [test]> prepare stmt from 'select /*+ ignore_plan_cache() */ * from t where a = ?';
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> set @a = 1;
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> execute stmt using @a;
Empty set (0.00 sec)
MySQL [test]> select @@last_plan_from_cache;
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 0 |
+------------------------+
1 row in set (0.00 sec)
MySQL [test]> execute stmt using @a;
Empty set (0.00 sec)
MySQL [test]> select @@last_plan_from_cache;
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 0 |
+------------------------+
1 row in set (0.00 sec)
プリペアドプランキャッシュの診断
一部のクエリまたはプランはキャッシュできません。 SHOW WARNINGSステートメントを使用して、クエリまたはプランがキャッシュされているかどうかを確認できます。 キャッシュされていない場合は、結果で失敗の理由を確認できます。 例:
mysql> PREPARE st FROM 'SELECT * FROM t WHERE a > (SELECT MAX(a) FROM t)'; -- The query contains a subquery and cannot be cached.
Query OK, 0 rows affected, 1 warning (0.01 sec)
mysql> show warnings; -- Checks the reason why the query plan cannot be cached.
+---------+------+-----------------------------------------------+
| Level | Code | Message |
+---------+------+-----------------------------------------------+
| Warning | 1105 | skip plan-cache: sub-queries are un-cacheable |
+---------+------+-----------------------------------------------+
1 row in set (0.00 sec)
mysql> prepare st from 'select * from t where a<?';
Query OK, 0 rows affected (0.00 sec)
mysql> set @a='1';
Query OK, 0 rows affected (0.00 sec)
mysql> execute st using @a; -- The optimization converts a non-INT type to an INT type, and the execution plan might change with the change of the parameter, so TiDB does not cache the plan.
Empty set, 1 warning (0.01 sec)
mysql> show warnings;
+---------+------+----------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------+
| Warning | 1105 | skip plan-cache: '1' may be converted to INT |
+---------+------+----------------------------------------------+
1 row in set (0.00 sec)
プリペアドプランキャッシュのメモリ管理
プリペアドプランキャッシュを使用すると、メモリのオーバーヘッドが発生します。各 TiDB インスタンスのすべてのセッションのキャッシュされた実行プランによる合計メモリ消費量を表示するには、Grafana のプランキャッシュメモリ使用量監視パネル使用します。
注記:
Golangのメモリ再利用メカニズムといくつかのカウントされないメモリ構造のため、Grafana に表示されるメモリは実際のヒープメモリ使用量と一致しません。Grafana に表示されるメモリと実際のヒープメモリ使用量の間には、約 ±20% の偏差があることがテストで判明しています。
各 TiDB インスタンスにキャッシュされている実行プランの合計数を表示するには、Grafana のプランキャッシュプラン番号パネル使用します。
以下は、Grafana のPlan Cache Memory UsageパネルとPlan Cache Plan Numパネルの例です。

v7.1.0 以降では、システム変数tidb_session_plan_cache_sizeを構成することで、各セッションでキャッシュできるプランの最大数を制御できます。さまざまな環境での推奨値は次のとおりで、監視パネルに応じて調整できます。
- TiDBサーバーインスタンスのメモリしきい値が 64 GiB 以下の場合は、
tidb_session_plan_cache_sizeを50に設定します。 - TiDBサーバーインスタンスのメモリしきい値が 64 GiB を超える場合は、
tidb_session_plan_cache_sizeを100に設定します。
v7.1.0 以降では、システム変数tidb_plan_cache_max_plan_sizeを使用して、キャッシュできるプランの最大サイズを制御できます。デフォルト値は 2 MB です。プランのサイズがこの値を超えると、プランはキャッシュされません。
TiDBサーバーの未使用メモリが一定のしきい値を下回ると、プラン キャッシュのメモリ保護メカニズムがトリガーされ、キャッシュされたプランの一部が削除されます。
システム変数tidb_prepared_plan_cache_memory_guard_ratioを設定することでしきい値を制御できます。しきい値はデフォルトで 0.1 に設定されており、これは TiDBサーバーの未使用メモリが合計メモリの 10% 未満 (メモリの 90% が使用済み) になると、メモリ保護メカニズムがトリガーされることを意味します。
メモリ制限により、プラン キャッシュが失われる場合があります。Grafana ダッシュボードでPlan Cache Miss OPSメトリック表示してステータスを確認できます。
実行プランのキャッシュをクリアする
ADMIN FLUSH [SESSION | INSTANCE] PLAN_CACHEステートメントを実行すると、実行プラン キャッシュをクリアできます。
このステートメントでは、 [SESSION | INSTANCE] 、現在のセッションまたは TiDB インスタンス全体のプラン キャッシュをクリアするかどうかを指定します。スコープが指定されていない場合、前述のステートメントはデフォルトでSESSIONキャッシュに適用されます。
以下は、 SESSION実行プラン キャッシュをクリアする例です。
MySQL [test]> create table t (a int);
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> prepare stmt from 'select * from t';
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> execute stmt;
Empty set (0.00 sec)
MySQL [test]> execute stmt;
Empty set (0.00 sec)
MySQL [test]> select @@last_plan_from_cache; -- Select the cached plan
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 1 |
+------------------------+
1 row in set (0.00 sec)
MySQL [test]> admin flush session plan_cache; -- Clear the cached plan of the current session
Query OK, 0 rows affected (0.00 sec)
MySQL [test]> execute stmt;
Empty set (0.00 sec)
MySQL [test]> select @@last_plan_from_cache; -- The cached plan cannot be selected again, because it has been cleared
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 0 |
+------------------------+
1 row in set (0.00 sec)
現在、TiDB はGLOBAL実行プラン キャッシュのクリアをサポートしていません。つまり、TiDB クラスター全体のキャッシュされたプランをクリアすることはできません。3 GLOBAL実行プラン キャッシュをクリアしようとすると、次のエラーが報告されます。
MySQL [test]> admin flush global plan_cache;
ERROR 1105 (HY000): Do not support the 'admin flush global scope.'
COM_STMT_CLOSEコマンドとDEALLOCATE PREPAREステートメントを無視します。
SQL ステートメントの構文解析コストを削減するには、 prepare stmt回実行し、次にexecute stmt複数回実行してからdeallocate prepare実行することをお勧めします。
MySQL [test]> prepare stmt from '...'; -- Prepare once
MySQL [test]> execute stmt using ...; -- Execute once
MySQL [test]> ...
MySQL [test]> execute stmt using ...; -- Execute multiple times
MySQL [test]> deallocate prepare stmt; -- Release the prepared statement
実際の練習では、以下に示すように、 execute stmt実行した後、毎回deallocate prepare実行することに慣れているかもしれません。
MySQL [test]> prepare stmt from '...'; -- Prepare once
MySQL [test]> execute stmt using ...;
MySQL [test]> deallocate prepare stmt; -- Release the prepared statement
MySQL [test]> prepare stmt from '...'; -- Prepare twice
MySQL [test]> execute stmt using ...;
MySQL [test]> deallocate prepare stmt; -- Release the prepared statement
このような方法では、最初に実行されたステートメントによって取得されたプランは、2 番目に実行されたステートメントでは再利用できません。
この問題を解決するには、システム変数tidb_ignore_prepared_cache_close_stmtをONに設定して、TiDB がprepare stmtを閉じるコマンドを無視するようにします。
mysql> set @@tidb_ignore_prepared_cache_close_stmt=1; -- Enable the variable
Query OK, 0 rows affected (0.00 sec)
mysql> prepare stmt from 'select * from t'; -- Prepare once
Query OK, 0 rows affected (0.00 sec)
mysql> execute stmt; -- Execute once
Empty set (0.00 sec)
mysql> deallocate prepare stmt; -- Release after the first execute
Query OK, 0 rows affected (0.00 sec)
mysql> prepare stmt from 'select * from t'; -- Prepare twice
Query OK, 0 rows affected (0.00 sec)
mysql> execute stmt; -- Execute twice
Empty set (0.00 sec)
mysql> select @@last_plan_from_cache; -- Reuse the last plan
+------------------------+
| @@last_plan_from_cache |
+------------------------+
| 1 |
+------------------------+
1 row in set (0.00 sec)
監視
Executorセクションの TiDB ページのGrafanaダッシュボードには、「プラン キャッシュ OPS を使用するクエリ」と「プラン キャッシュ ミス OPS」のグラフがあります。これらのグラフを使用して、TiDB とアプリケーションの両方が正しく構成され、SQL プラン キャッシュが正しく機能しているかどうかを確認できます。同じページのServerセクションには、「準備されたステートメントの数」のグラフがあります。アプリケーションが準備されたステートメントを使用している場合、このグラフにはゼロ以外の値が表示されます。これは、SQL プラン キャッシュが正しく機能するために必要なものです。
