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
を介して制御できます。
注記:
実行プラン キャッシュ機能は
Prepare
Execute
にのみ適用され、通常のクエリには効果がありません。
実行プラン キャッシュ機能を有効にすると、セッション レベルのシステム変数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)
プリペアドプランキャッシュのメモリ管理
プリペアドプランキャッシュを使用すると、メモリのオーバーヘッドが発生します。内部テストでは、キャッシュされたプランごとに平均 100 KiB のメモリが消費されます。プラン キャッシュは現在SESSION
レベルであるため、合計メモリ消費量は約the number of sessions * the average number of cached plans in a session * 100 KiB
です。
たとえば、現在の TiDB インスタンスには同時実行セッションが 50 あり、各セッションには約 100 のキャッシュされたプランがあります。合計メモリ消費量は約50 * 100 * 100 KiB
= 512 MB
です。
システム変数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% が使用済み) になると、メモリ保護メカニズムがトリガーされることを意味します。
メモリ制限により、プラン キャッシュが失われる場合があります。
実行プランのキャッシュをクリアする
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)
監視
TiDB Cloudコンソールの監視ページで、 Queries Using Plan Cache OPS
メトリックをチェックして、すべての TiDB インスタンスで 1 秒あたりにプラン キャッシュを使用しているクエリまたはプラン キャッシュがないクエリの数を取得できます。