EXPLAINウォークスルー

SQL は宣言型言語であるため、クエリが効率的に実行されたかどうかを自動的に判断することはできません。まずEXPLAINステートメントを使用して、現在の実行プランを確認する必要があります。

バイクシェアのサンプルデータベースからの次の文は、2017 年 7 月 1 日に何回旅行が行われたかを数えています。

EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+ | StreamAgg_20 | 1.00 | root | | funcs:count(Column#13)->Column#11 | | └─TableReader_21 | 1.00 | root | | data:StreamAgg_9 | | └─StreamAgg_9 | 1.00 | cop[tikv] | | funcs:count(1)->Column#13 | | └─Selection_19 | 250.00 | cop[tikv] | | ge(bikeshare.trips.start_date, 2017-07-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-07-01 23:59:59.000000) | | └─TableFullScan_18 | 10000.00 | cop[tikv] | table:trips | keep order:false, stats:pseudo | +------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+ 5 rows in set (0.00 sec)

子演算子└─TableFullScan_18から遡ると、その実行プロセスは次のようになりますが、これは現時点では最適ではありません。

  1. コプロセッサ (TiKV) は、 tripsテーブル全体をTableFullScan操作として読み取ります。次に、読み取った行を、TiKV 内にあるSelection_19演算子に渡します。
  2. 次に、述語WHERE start_date BETWEEN ..は演算子Selection_19でフィルタリングされます。この選択を満たす行は約250行と推定されます。この数は統計と演算子のロジックに基づいて推定されることに注意してください。演算子└─TableFullScan_18stats:pseudoを示します。これは、テーブルに実際の統計情報がないことを意味します。統計情報を収集するためにANALYZE TABLE trips実行すると、統計がより正確になると予想されます。
  3. 選択基準を満たす行には、 count関数が適用されます。これは、TiKV ( cop[tikv] ) 内にあるStreamAgg_9演算子内でも完了します。TiKV コプロセッサは、MySQL 組み込み関数の多くを実行できますが、 countもその 1 つです。
  4. StreamAgg_9の結果は、現在 TiDBサーバー内にあるTableReader_21演算子に送信されます ( rootのタスク)。この演算子のestRows列の値は1です。これは、演算子がアクセスする各 TiKV 領域から 1 行を受け取ることを意味します。これらの要求の詳細については、 EXPLAIN ANALYZEを参照してください。
  5. 次に、 StreamAgg_20演算子は└─TableReader_21演算子の各行にcount関数を適用します。これはSHOW TABLE REGIONSからわかるように約 56 行になります。これはルート演算子であるため、クライアントに結果を返します。

注記:

テーブルに含まれるリージョンの一般的なビューを表示するには、 SHOW TABLE REGIONS実行します。

現在のパフォーマンスを評価する

EXPLAINクエリ実行プランを返すだけで、クエリは実行しません。実際の実行時間を取得するには、クエリを実行するか、 EXPLAIN ANALYZEを使用します。

EXPLAIN ANALYZE SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
+------------------------------+----------+----------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+ | id | estRows | actRows | task | access object | execution info | operator info | memory | disk | +------------------------------+----------+----------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+ | StreamAgg_20 | 1.00 | 1 | root | | time:1.031417203s, loops:2 | funcs:count(Column#13)->Column#11 | 632 Bytes | N/A | | └─TableReader_21 | 1.00 | 56 | root | | time:1.031408123s, loops:2, cop_task: {num: 56, max: 782.147269ms, min: 5.759953ms, avg: 252.005927ms, p95: 609.294603ms, max_proc_keys: 910371, p95_proc_keys: 704775, tot_proc: 11.524s, tot_wait: 580ms, rpc_num: 56, rpc_time: 14.111932641s} | data:StreamAgg_9 | 328 Bytes | N/A | | └─StreamAgg_9 | 1.00 | 56 | cop[tikv] | | proc max:640ms, min:8ms, p80:276ms, p95:480ms, iters:18695, tasks:56 | funcs:count(1)->Column#13 | N/A | N/A | | └─Selection_19 | 250.00 | 11409 | cop[tikv] | | proc max:640ms, min:8ms, p80:276ms, p95:476ms, iters:18695, tasks:56 | ge(bikeshare.trips.start_date, 2017-07-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-07-01 23:59:59.000000) | N/A | N/A | | └─TableFullScan_18 | 10000.00 | 19117643 | cop[tikv] | table:trips | proc max:612ms, min:8ms, p80:248ms, p95:460ms, iters:18695, tasks:56 | keep order:false, stats:pseudo | N/A | N/A | +------------------------------+----------+----------+-----------+---------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+ 5 rows in set (1.03 sec)

上記の例のクエリの実行には1.03秒かかりますが、これは理想的なパフォーマンスです。

上記EXPLAIN ANALYZEの結果から、 actRows推定値の一部 ( estRows ) が不正確であることを示しています (1 万行を予想していたが、 1,900 万行が検出された)。これは└─TableFullScan_18operator info ( stats:pseudo ) ですでに示されています。 最初にANALYZE TABLE実行し、次にEXPLAIN ANALYZEもう一度実行すると、推定値がはるかに近くなることがわかります。

ANALYZE TABLE trips; EXPLAIN ANALYZE SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
Query OK, 0 rows affected (10.22 sec) +------------------------------+-------------+----------+-----------+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+ | id | estRows | actRows | task | access object | execution info | operator info | memory | disk | +------------------------------+-------------+----------+-----------+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+ | StreamAgg_20 | 1.00 | 1 | root | | time:926.393612ms, loops:2 | funcs:count(Column#13)->Column#11 | 632 Bytes | N/A | | └─TableReader_21 | 1.00 | 56 | root | | time:926.384792ms, loops:2, cop_task: {num: 56, max: 850.94424ms, min: 6.042079ms, avg: 234.987725ms, p95: 495.474806ms, max_proc_keys: 910371, p95_proc_keys: 704775, tot_proc: 10.656s, tot_wait: 904ms, rpc_num: 56, rpc_time: 13.158911952s} | data:StreamAgg_9 | 328 Bytes | N/A | | └─StreamAgg_9 | 1.00 | 56 | cop[tikv] | | proc max:592ms, min:4ms, p80:244ms, p95:480ms, iters:18695, tasks:56 | funcs:count(1)->Column#13 | N/A | N/A | | └─Selection_19 | 432.89 | 11409 | cop[tikv] | | proc max:592ms, min:4ms, p80:244ms, p95:480ms, iters:18695, tasks:56 | ge(bikeshare.trips.start_date, 2017-07-01 00:00:00.000000), le(bikeshare.trips.start_date, 2017-07-01 23:59:59.000000) | N/A | N/A | | └─TableFullScan_18 | 19117643.00 | 19117643 | cop[tikv] | table:trips | proc max:564ms, min:4ms, p80:228ms, p95:456ms, iters:18695, tasks:56 | keep order:false | N/A | N/A | +------------------------------+-------------+----------+-----------+---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------+-----------+------+ 5 rows in set (0.93 sec)

ANALYZE TABLE実行されると、 └─TableFullScan_18演算子の推定行数が正確であり、 └─Selection_19の推定値も大幅に近くなっていることがわかります。上記の 2 つのケースでは、実行プラン (TiDB がこのクエリを実行するために使用する演算子のセット) は変更されていませんが、統計が古くなっているために、最適ではないプランが頻繁に発生します。

ANALYZE TABLEに加えて、しきい値tidb_auto_analyze_ratioに達した後、TiDB はバックグラウンド操作として統計を自動的に再生成します。5 SHOW STATS_HEALTHYステートメントを実行すると、TiDB がこのしきい値にどれだけ近いか (TiDB が統計をどれだけ健全であると見なしているか) を確認できます。

SHOW STATS_HEALTHY;
+-----------+------------+----------------+---------+ | Db_name | Table_name | Partition_name | Healthy | +-----------+------------+----------------+---------+ | bikeshare | trips | | 100 | +-----------+------------+----------------+---------+ 1 row in set (0.00 sec)

最適化を特定する

現在の実行計画は、次の点で効率的です。

  • 作業のほとんどは TiKV コプロセッサ内で処理されます。処理のためにネットワーク経由で TiDB に送り返す必要があるのは 56 行だけです。これらの各行は短く、選択に一致するカウントのみが含まれています。

  • TiDB( StreamAgg_20 )とTiKV( └─StreamAgg_9 )の両方で行数を集計するには、メモリ使用量が非常に効率的なストリーム集計を使用します。

現在の実行プランの最大の問題は、述語start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59'すぐに適用されないことです。すべての行は最初にTableFullScan演算子で読み取られ、その後選択が適用されます。原因はSHOW CREATE TABLE tripsの出力からわかります。

SHOW CREATE TABLE trips\G
*************************** 1. row *************************** Table: trips Create Table: CREATE TABLE `trips` ( `trip_id` bigint(20) NOT NULL AUTO_INCREMENT, `duration` int(11) NOT NULL, `start_date` datetime DEFAULT NULL, `end_date` datetime DEFAULT NULL, `start_station_number` int(11) DEFAULT NULL, `start_station` varchar(255) DEFAULT NULL, `end_station_number` int(11) DEFAULT NULL, `end_station` varchar(255) DEFAULT NULL, `bike_number` varchar(255) DEFAULT NULL, `member_type` varchar(255) DEFAULT NULL, PRIMARY KEY (`trip_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=20477318 1 row in set (0.00 sec)

start_dateにはインデックスがありませ。この述語をインデックス リーダー演算子にプッシュするには、インデックスが必要です。次のようにインデックスを追加します。

ALTER TABLE trips ADD INDEX (start_date);
Query OK, 0 rows affected (2 min 10.23 sec)

注記:

ADMIN SHOW DDL JOBSコマンドを使用して、DDL ジョブの進行状況を監視できます。TiDB のデフォルトは、インデックスを追加しても本番ワークロードにあまり影響が及ばないように慎重に選択されています。テスト環境では、 tidb_ddl_reorg_batch_sizetidb_ddl_reorg_worker_cnt値を増やすことを検討してください。リファレンス システムでは、バッチ サイズを10240 、ワーカー数を32にすると、デフォルトよりも 10 倍のパフォーマンス向上が実現できます。

インデックスを追加したら、 EXPLAINのクエリを繰り返すことができます。次の出力では、新しい実行プランが選択され、 TableFullScanSelection演算子が削除されていることがわかります。

EXPLAIN SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
+-----------------------------+---------+-----------+-------------------------------------------+-------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +-----------------------------+---------+-----------+-------------------------------------------+-------------------------------------------------------------------+ | StreamAgg_17 | 1.00 | root | | funcs:count(Column#13)->Column#11 | | └─IndexReader_18 | 1.00 | root | | index:StreamAgg_9 | | └─StreamAgg_9 | 1.00 | cop[tikv] | | funcs:count(1)->Column#13 | | └─IndexRangeScan_16 | 8471.88 | cop[tikv] | table:trips, index:start_date(start_date) | range:[2017-07-01 00:00:00,2017-07-01 23:59:59], keep order:false | +-----------------------------+---------+-----------+-------------------------------------------+-------------------------------------------------------------------+ 4 rows in set (0.00 sec)

実際の実行時間を比較するには、再度EXPLAIN ANALYZE使用します。

EXPLAIN ANALYZE SELECT count(*) FROM trips WHERE start_date BETWEEN '2017-07-01 00:00:00' AND '2017-07-01 23:59:59';
+-----------------------------+---------+---------+-----------+-------------------------------------------+------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------+-----------+------+ | id | estRows | actRows | task | access object | execution info | operator info | memory | disk | +-----------------------------+---------+---------+-----------+-------------------------------------------+------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------+-----------+------+ | StreamAgg_17 | 1.00 | 1 | root | | time:4.516728ms, loops:2 | funcs:count(Column#13)->Column#11 | 372 Bytes | N/A | | └─IndexReader_18 | 1.00 | 1 | root | | time:4.514278ms, loops:2, cop_task: {num: 1, max:4.462288ms, proc_keys: 11409, rpc_num: 1, rpc_time: 4.457148ms} | index:StreamAgg_9 | 238 Bytes | N/A | | └─StreamAgg_9 | 1.00 | 1 | cop[tikv] | | time:4ms, loops:12 | funcs:count(1)->Column#13 | N/A | N/A | | └─IndexRangeScan_16 | 8471.88 | 11409 | cop[tikv] | table:trips, index:start_date(start_date) | time:4ms, loops:12 | range:[2017-07-01 00:00:00,2017-07-01 23:59:59], keep order:false | N/A | N/A | +-----------------------------+---------+---------+-----------+-------------------------------------------+------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------+-----------+------+ 4 rows in set (0.00 sec)

上記の結果から、クエリ時間は 1.03 秒から 0.0 秒に短縮されました。

注記:

ここで適用されるもう 1 つの最適化は、コプロセッサ キャッシュです。インデックスを追加できない場合は、 コプロセッサキャッシュ有効にすることを検討してください。有効にすると、演算子が最後に実行されてからリージョンが変更されていない限り、TiKV はキャッシュから値を返します。これにより、高価なTableFullScanおよびSelection演算子のコストを大幅に削減することもできます。

サブクエリの早期実行を無効にする

クエリの最適化中に、TiDB は直接計算できるサブクエリを事前に実行します。例:

CREATE TABLE t1(a int); INSERT INTO t1 VALUES(1); CREATE TABLE t2(a int); EXPLAIN SELECT * FROM t2 WHERE a = (SELECT a FROM t1);
+--------------------------+----------+-----------+---------------+--------------------------------+ | id | estRows | task | access object | operator info | +--------------------------+----------+-----------+---------------+--------------------------------+ | TableReader_14 | 10.00 | root | | data:Selection_13 | | └─Selection_13 | 10.00 | cop[tikv] | | eq(test.t2.a, 1) | | └─TableFullScan_12 | 10000.00 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | +--------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec)

前の例では、 a = (SELECT a FROM t1)サブクエリは最適化中に計算され、 t2.a=1に書き換えられます。これにより、最適化中に定数の伝播や折りたたみなどの最適化をさらに行うことができます。ただし、 EXPLAINステートメントの実行時間に影響します。サブクエリ自体の実行に時間がかかる場合、 EXPLAINステートメントが完了しない可能性があり、オンライン トラブルシューティングに影響する可能性があります。

v7.3.0 以降、TiDB ではtidb_opt_enable_non_eval_scalar_subqueryシステム変数が導入され、 EXPLAINでこのようなサブクエリの事前実行を無効にするかどうかを制御します。この変数のデフォルト値はOFFで、サブクエリが事前に計算されることを意味します。この変数をONに設定すると、サブクエリの事前実行を無効にすることができます。

SET @@tidb_opt_enable_non_eval_scalar_subquery = ON; EXPLAIN SELECT * FROM t2 WHERE a = (SELECT a FROM t1);
+---------------------------+----------+-----------+---------------+---------------------------------+ | id | estRows | task | access object | operator info | +---------------------------+----------+-----------+---------------+---------------------------------+ | Selection_13 | 8000.00 | root | | eq(test.t2.a, ScalarQueryCol#5) | | └─TableReader_15 | 10000.00 | root | | data:TableFullScan_14 | | └─TableFullScan_14 | 10000.00 | cop[tikv] | table:t2 | keep order:false, stats:pseudo | | ScalarSubQuery_10 | N/A | root | | Output: ScalarQueryCol#5 | | └─MaxOneRow_6 | 1.00 | root | | | | └─TableReader_9 | 1.00 | root | | data:TableFullScan_8 | | └─TableFullScan_8 | 1.00 | cop[tikv] | table:t1 | keep order:false, stats:pseudo | +---------------------------+----------+-----------+---------------+---------------------------------+ 7 rows in set (0.00 sec)

ご覧のとおり、スカラー サブクエリは実行中に展開されないため、このような SQL の具体的な実行プロセスを理解しやすくなります。

注記:

tidb_opt_enable_non_eval_scalar_subquery EXPLAIN番目のステートメントの動作にのみ影響し、 EXPLAIN ANALYZE番目のステートメントは引き続きサブクエリを事前に実行します。

このページは役に立ちましたか?