この記事では、MySQL のコストベースの最適化について理解します。

序文

MySQL にはクエリを実行するためのさまざまな実行スキームがあり、実際にクエリを実行するために最もコストが低いスキーム、または最もコストが低いスキームが選択されると以前説明しましたが、どうすれば詳しく理解できるでしょうか?

1. コストとは何か

MySQL はクエリを実行するためにさまざまな実行プランを持つことができ、そのうちの 1 つを選択するか成本最低代价最低そのプランを実際にクエリを実行すると言っていました。ただし、これまでのコストの説明は非常に曖昧で、実際には、MySQL でのクエリ ステートメントの実行コストは次の 2 つの側面から構成されます。

  • I/O成本

    テーブルがよく使用する MyISAM および InnoDB ストレージ エンジンは、データとインデックスの両方をディスクに保存します。テーブル内のレコードをクエリする場合は、操作する前にデータまたはインデックスをメモリにロードする必要があります。ディスクからメモリにロードするプロセスで失われる時間は、I/O コストと呼ばれます。

  • CPU成本

    レコードを読み込んで検索条件を満たしているかどうかを確認したり、結果セットをソートしたりする処理にかかる時間を CPU コストと呼びます。

InnoDB ストレージ エンジンの場合、ページはディスクとメモリ間の対話の基本単位です。MySQL では、ページを読み取るためのデフォルトのコストは ( 0.25MySQL 5.7 のデフォルト1.0)、読み取りとレコードが検索を満たすかどうかを確認するためのデフォルトのコストは次のように規定されています。基準は0.1(MySQL 5.7 のデフォルト0.2) です。これらの数値は と呼ばれ0.25これら 2 つのコスト定数が最も一般的に使用されます。残りのコスト定数については後で説明します。0.1成本常数

ヒント:
レコードを読み取るときに検索条件が満たされているかどうかを確認する必要があるかどうかに関係なく、コストは 0.1 であることに注意してください。
ここで使用する MySQL バージョンは 8.0.32 で、コストはバージョンによって異なります。詳細については、この章で後ほど説明します。

2. 単一テーブルクエリのコスト

2.1 データの準備

通常の研究では、まだ以前のものを使用していますdemo8. この時計がどのようなものであるかを誰もが忘れてしまうのではないかと思うので、記事をコピーします。

mysql> USE testdb;

mysql> create table demo8 (    
id int not null auto_increment,    
key1 varchar(100),    
key2 int,    
key3 varchar(100),    
key_part1 varchar(100),    
key_part2 varchar(100),    
key_part3 varchar(100),    
common_field varchar(100), 
primary key (id),
key idx_key1 (key1),    
unique key idx_key2 (key2),    
key idx_key3 (key3),    
key idx_key_part(key_part1, key_part2, key_part3));

合計 1 つのクラスター化 (主キー) インデックスと 4 つのセカンダリ インデックスが、demo8 テーブルに対して作成されました。

  • 列に対してid作成されたクラスター化インデックス。
  • 列に対してkey1作成されたセカンダリ インデックス。
  • 列に対してkey2作成された一意のセカンダリ インデックス。
  • 列に対してkey3作成されたセカンダリ インデックス。
  • key_part1key_part2、列に対してkey_part3作成された複合 (結合) セカンダリ インデックス。

20000次に、このテーブルにレコードを挿入し、id 列を除く他の列にランダムな値を挿入する必要があります。

mysql> delimiter //
create procedure demo8data()
begin    
	declare i int;    
	set i=0;    
	while i<20000 do        
		insert into demo8(key1,key2,key3,key_part1,key_part2,key_part3,common_field) values(
		substring(md5(rand()),1,2),
		i+1,
		substring(md5(rand()),1,3),
		substring(md5(rand()),1,4),
		substring(md5(rand()),1,5),
		substring(md5(rand()),1,6),
		substring(md5(rand()),1,7)
		);        
		set i=i+1;    
	end while;
end;
//
delimiter ;

mysql> call demo8data();

正式に勉強を始めましょう

2.2 コストベースの最適化手順

単一テーブルのクエリ ステートメントが実際に実行される前に、MySQL クエリ オプティマイザーはステートメントを実行するために考えられるすべてのソリューションを見つけ出し、比較した後、コストが最も低いソリューションを見つけます。この最もコストが低いソリューションは、いわゆる実行です。このプロセスの概要は次のとおりです。

  • 検索条件に基づいて、使用可能なすべてのインデックスを検索します。
  • フルテーブルスキャンのコストを計算する
  • さまざまなインデックスを使用してクエリを実行するコストを計算する
  • さまざまな実行計画のコストを比較して、最もコストが低い実行計画を見つけます。

以下では、例を使用してこれらの手順を分析します。単一テーブルのクエリ ステートメントは次のとおりです。

mysql> select * from demo8 where
	key1 in ('aa','bb','cc') and
	key2 > 10 and key2 < 1000 and
	key3 > key2 and
	key_part1 like '%3f%' and
	common_field='1281259';

複雑そうに見えますが、段階的に分析してみましょう

ステップ 1:検索基準に従って、適用可能なインデックスをすべて検索します。

前に述べたように、B+ ツリー インデックスの場合、インデックス列と定数が=<=>IN、 、NOT INIS NULLIS NOT NULL><>=、<=BETWEEN AND!=(等しくない場合も記述できます<>) またはLIKE演算子を使用して接続されている限り、 -call 範囲間隔 (文字列プレフィックスに一致する LIKE も OK)。つまり、これらの検索条件はインデックスを使用する可能性があり、MySQL はクエリで使用される可能性のあるインデックスを呼び出しますpossible keys

上記のクエリに含まれるいくつかの認可条件を分析してみましょう。

  • key1 in ('aa','bb','cc')、この検索条件ではセカンダリ インデックスを使用できます。idx_key1
  • key2 > 10 and key2 < 1000、この検索条件ではセカンダリ インデックスを使用できます。idx_key2
  • key3 > key2, この検索条件の検索列は定数と比較されないため、インデックスは使用できません。
  • key_part1 like '%3f%'、ワイルドカードで始まる文字列を比較するには演算子key_part1を使用しlike、インデックスは使用できません。
  • common_field=‘1281259’、列にはインデックスがまったくないため、インデックスは使用されません。

要約すると、上記のクエリ ステートメントで使用できるインデックスはpossible keyssumidx_key1と Indexのみですidx_key2

ステップ 2:フルテーブルスキャンのコストを計算する

InnoDB ストレージ エンジンの場合、フル テーブル スキャンとは、クラスター化インデックス内のレコードを指定された検索条件と順番に比較し、検索条件を満たすレコードを結果セットに追加することを意味するため、クラスター化インデックスが対応している必要があります。がメモリにロードされ、レコードが検索条件に一致するかどうかがチェックされます。以来查询成本=I/O成本+CPU成本、テーブル全体のスキャンのコストを計算するには、次の 2 つの情報が必要です。

  • 聚簇索引占用的页面数
  • 该表中的记录数

これら 2 つの情報はどこから来たのでしょうか? MySQL はテーブルごとに一連の統計情報を維持します。これらの統計情報がどのように収集されるかについては、この章の後半で詳しく説明します。次に、これらの統計情報を表示する方法を見てみましょう。MySQL には、テーブルの統計情報を表示するためのステートメントが用意されています。指定したテーブルの統計情報を表示したい場合は、show table statusステートメントの後に
対応するステートメントを追加するだけです。likeたとえば、demo8このテーブルの統計情報を表示したい場合は、対応するステートメントをステートメントの後に追加します。 、次のように書くことができます。

mysql> show table status like 'demo8' \G;
*************************** 1. row ***************************
           Name: demo8
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 20187
 Avg_row_length: 78
    Data_length: 1589248
Max_data_length: 0
   Index_length: 2785280
      Data_free: 4194304
 Auto_increment: 20001
    Create_time: 2023-05-16 16:36:53
    Update_time: 2023-05-16 16:38:21
     Check_time: NULL
      Collation: utf8mb4_0900_ai_ci
       Checksum: NULL
 Create_options: 
        Comment: 
1 row in set (0.00 sec)

ERROR: 
No query specified

多くの統計オプションが表示されますが、今のところ考慮するのは 2 つだけです。

  • Rows: このオプションは、テーブル内のレコードの数を示します。ストレージ エンジンを使用するテーブルの場合MyISAM、この値は正確です。InnoDBストレージ エンジンを使用するテーブルの場合、この値は推定値です。クエリ結果からわかるように、demo8テーブルはストレージ エンジンを使用しているInnoDBため、テーブルには実際には 20,000 レコードがありますが、show table status表示されるRows値は 20,187 レコードです。

  • Data_length: このオプションは、占有されているストレージ領域のバイト数を示します。ストレージ エンジンを使用するテーブルの場合MyISAM、この値はデータ ファイルのサイズです。InnoDBストレージ エンジンを使用するテーブルの場合、この値はクラスター化インデックスが占有するストレージ スペースのサイズに相当します。

    つまり、値のサイズは次のように計算できます。

    Data_length = 聚簇索引的页面数量 * 每个页面的大小

    上記のクエリ結果に従って、テーブルでdemo8はデフォルトの16KBページ サイズが使用されているため、クラスタード インデックス内のページ数を計算できます。

    聚簇索引的页面数=1589248 ÷ 16 ÷ 1024 = 97

クラスター化インデックスが占有する推定ページ数とテーブル内のレコード数を取得したので、フル テーブル スキャンの計算プロセスを見てみましょう。

  • I/Oコスト: 97 * 0.25 =24.25
    97クラスター化インデックスが占有するページ数を指し、0.25ページをロードするコスト定数を指します。
  • CPUコスト: 20187 * 0.1 =2018.7
    20187統計データ内のテーブル内のレコード数を指します。これはInnoDBストレージ エンジンの推定値であり、0.1レコードにアクセスするために必要なコスト定数を指します。
  • 总成本:24.25 +2018.7 =2042.95

要約すると、demo8完全なテーブル スキャンに必要な総コストは、2042.95コードを直接アップロードすることと、無駄なことはなく、豪華な検証を行うことです。

mysql> explain format=json select * from demo8 ;

ここに画像の説明を挿入

小提示:
テーブル内のレコードは実際には B+ ツリーに対応するクラスタード インデックスのリーフ ノードに格納されていると前述しました。そのため、ルート ノードを介して左端のリーフ ノードを取得する限り、以下で構成される二重リンク リストをたどることができます。リーフノードをすべてチェックしてください。つまり、テーブル全体のスキャンのプロセスでは、B+ ツリー内の一部のノードにアクセスする必要はありませんが、MySQL はクラスタ化インデックスが占有するページ数を、I/O コストの計算の基礎として直接使用します。フル テーブル スキャンのコストの計算内部ノードとリーフ ノードの区別は少し単純化されているので、注意してください。

ステップ 3:さまざまなインデックスによって実行されるクエリ コストを計算する

ステップ 1 の分析から、上記のクエリではidx_key1これらidx_key22 つのインデックスが使用されている可能性があることがわかりました。これらのインデックスを単独で使用してクエリを実行する場合のコストを分析し、最後にインデックスの結合が使用できるかどうかを分析する必要があります。ここで言及する必要があるのは、MySQL クエリ オプティマイザーは最初に一意のセカンダリ インデックスを使用するコストを分析し、次に共通インデックスを使用するコストを分析するため、最初にコストを分析してから、使用コストを確認するということですidx_key2idx_key1

idx_key2 を使用して実行されるクエリのコスト

idx_key2対応する検索条件は次のとおりです。key2 > 10 and key2 < 1000つまり、対応する範囲間隔は ( 10, 1000) であり、idx_key2検索図は次のとおりです。

ここに画像の説明を挿入

二级索引+回表このメソッドのクエリの場合、MySQL はデータの 2 つの側面に応じてこのクエリのコストを計算します。

  • 范围区间的数量:特定の範囲内でセカンダリ インデックスが占めるページの数に関係なく、クエリ オプティマイザーはそれを大まかに考慮します读取索引的一个范围区间的I/O成本和读取一个页面是相同的この例では、 idx_key2: ( ) を使用する範囲が 1 つだけである10,1000ため、この範囲のセカンダリ インデックスにアクセスするのと同等のことは次I/O成本のようになります。1 * 0.25 = 0.25

  • 需要回表的记录数:オプティマイザは、セカンダリ インデックスの特定の範囲に含まれるレコードの数を計算する必要があります。比率については、10,1000idx_key2 が範囲 ( ) に含まれるセカンダリ インデックスのレコードの数を計算する必要があります。計算プロセスは次のとおりです。

    • ステップ 1:まず、この条件に従って対応する B+ ツリー インデックスkey2 > 10にアクセスし、この条件を満たす最初のレコードを見つけます。このレコードを間隔の左端のレコードと呼びます。B+ 数値ツリー内のレコードを検索するプロセスは非常に高速かつ一定であるため、このプロセスによるパフォーマンスの消費は無視できると前述しました。idx_key2key2 > 10

    • ステップ 2:次に、この条件に従って、対応する B+ ツリー インデックスkey2 < 1000からこの条件を満たす最初idx_key2のレコードを検索し続けます。このレコードを間隔内の右端のレコードと呼び、このプロセスのパフォーマンス消費も無視できます。

    • ステップ 3:間隔内の左端のレコードと右端のレコードの間の間隔がそれほど遠くない場合 (MySQL 5.7.21 のバージョンでは、間隔が 10 ページ以上であれば)、正確にカウントできます。条件を満たすセカンダリインデックスkey2> 10 AND key2 < 1000のレコード数。それ以外の場合は、間隔の左端のレコードに沿って右に 10 ページを読み取り、各ページに含まれるレコードの平均数を計算し、この平均に間隔の左端のレコードと右端のレコードの間のページ数を掛けます。次に、間隔内の左端のレコードと右端のレコードの間にあるページ数をどのように推定するかという質問が再び生じます。この問題を解決するには、B+ ツリー インデックスの構造に戻る必要があります。

      ここに画像の説明を挿入

    • 図に示すように、たとえば、間隔の左端のレコードは 34 ページにあり、間隔の右端のレコードは 40 ページにあります。次に、間隔の左端のレコードと間隔内の右端のレコード。これは、ページ 34 とページ 40 の間の数を計算することに相当します。ページは何ページあり、各ディレクトリ エントリ レコードはデータ ページに対応するため、ページ 34 とページ 41 の間のページ数を計算することは次のようになります。親ノード (つまり、42 ページ) 内の対応するディレクトリ エントリ レコード間の距離を計算するのと同等です。レコードがいくつかあれば十分ではないでしょうか。ページ 34 とページ 41 より前のページが多すぎる場合 (対応するディレクトリ項目が同じ親ノード ページにない場合)、再帰的にカウントを続けます。この統計プロセスは親ノード ページで実行されます。前に述べたように、 B+ ツリーには 4 層の高さがすでに非常に高いため、パフォーマンスはそれほど消費されません。

idx_key2二次インデックスの特定の範囲内のレコード数をカウントする方法がわかったら、実際の問題に戻る必要があります。上記のアルゴリズムによれば、区間 ( ) には10, 10009891 つのレコードがあります。この989セカンダリ インデックス レコードを読み取るための CPU コストは次のとおりです。989 * 0.1 = 98.9

ここで、989は読み取る必要があるセカンダリ インデックス レコードの数、0.1はレコードの読み取りにかかるコスト定数です。

セカンダリ インデックスを通じてレコードを取得した後、さらに 2 つの作業を行う必要があります。

  • これらのレコードの主キー値に従って、クラスター化インデックスに戻ってテーブル操作を実行します。

    MySQL によるテーブルの戻り操作の I/O コストの評価は、依然として非常に大胆です。彼らは、各テーブルの戻り操作は、ページへのアクセス、つまりそこにあるレコードの数と同等であると考えています。セカンダリ インデックスの範囲内にあるテーブルに何回戻るか、つまりページ I/O を何回実行する必要があるか。上記の統計によると、idx_key2 セカンダリ インデックスを使用してクエリを実行する場合、989 個のセカンダリ インデックス レコードをテーブルに返す必要があると推定されます。テーブルの戻り操作によって発生する I/O コストは次のとおりです。989 x 0.25 = 247.25

    ここで、989はセカンダリ インデックス レコードの予想数で、0.25はページの I/O コストの定数です。

  • テーブルに戻って完全なアカウントレコードを取得し、他の検索条件が真であるかどうかを確認します

    テーブル返却操作の本質は、セカンダリ インデックス レコードの主キー値を通じてクラスター化インデックス内の完全なユーザー レコードを検索し、key2 > 10 and key2 < 1000この検索条件以外の検索条件が true であるかどうかを確認することです。範囲間隔を通じて合計 989 個のセカンダリ インデックス レコードを取得しました。これは、クラスター化インデックス内の完全なユーザー レコードに相当します。これらの完全なユーザー レコードを読み取り、989残りの検索条件を満たしているかどうかを確認するための CPU コストは次のとおりです。989 x 0.1 = 98.9

    このうち、989 は検出するレコードの数であり、0.1レコードが指定された検索条件を満たすかどうかを検出するためのコスト定数です。

したがって、この例で idx_key2 を使用してクエリを実行するコストは次のようになります。

  • I/O成本: 1.0x 0.25 + 989 x 0.25 = 247.5(範囲間隔の数 + セカンダリ インデックス レコードの推定数)

  • CPU成本: 989 x 0.1 + 0.01 + 989 x 0.1 = 197.81(セカンダリ インデックス レコードの読み取りコスト + テーブルに戻った後のクラスター化インデックス レコードの読み取りと検出のコスト)

要約すると、クエリを実行するために idx_key2 を使用する場合の総コストは次のとおりです。247.5 + 197.81 = 445.31コードを直接アップロードします。ナンセンスではなく、ゴージャスな検証です。

mysql> explain format=json select * from demo8 where key2 > 10 and key2 < 1000 and key3 > key2 and key_part1 like '%3f%' and common_field='1281259';

ここに画像の説明を挿入

小提示:
インデックスを使用する場合、セカンダリ インデックスの読み取り条件は微調整されますが、クラスタード インデックスの読み取りには含まれません。スキャン間隔内でテーブルに戻されるすべてのレコードは、1 ページの読み取りと同等です。インデックスを使用しない場合は、前者とは異なり、微調整値が個別に分析されます。

idx_key2 を使用して実行されるクエリのコスト

idx_key1対応する検索条件key1 in ('aa','bb','cc')も、次の 3 つの単一点間隔に相当します。

  • ['aa','aa']
  • ['bb','bb']
  • ['cc','cc']

検索を使用する概略図はidx_key1次のとおりです。

ここに画像の説明を挿入

ユースケースと同様に、アクセスする必要がある範囲間隔の数と、テーブルに返す必要があるレコードの数もidx_key2必要です。idx_key1

  • 范围区间数量: クエリを使用するidx_key1ときは明らかに 3 つの単一ポイント間隔があるため、I/Oこれら 3 つの範囲間隔のセカンダリ インデックスにアクセスするコストは次のようになります。3 x 0.25 = 0.75
  • 需要回表的记录数
    • ['aa','aa']単一点間隔に対応する二次インデックス レコードの数の検索は、連続範囲間隔に対応する二次インデックス レコードの数の検索と同じです。間隔の左端のレコードと右端のレコードの両方が最初に計算され、次に、それらの間のレコードの数が計算されます['aa','aa']67
    • 単一点間隔検索['bb','bb']に対応する二次インデックス レコードは次のとおりです。88
    • 単一点間隔検索['cc','cc']に対応する二次インデックス レコードは次のとおりです。75

したがって、これら 3 つの単一ポイント間隔でテーブルに返す必要があるレコードの総数は次のようになります。67+88+75 = 230また、これらのセカンダリ インデックス レコードの読み取りコストは次のCPUようになります。230 x 0.1 + 0.01 = 23.01

テーブルに返す必要があるレコードの総数を取得した後、次のことを考慮してください。

  • これらのレコードの主キー値に従って、テーブル操作はクラスター化インデックスで実行され、必要なI/Oコストは次のとおりです。230 x 0.25 = 57.5
  • テーブルに戻った後、完全なユーザー レコードが取得され、CPU他の検索条件が true であるかどうかを比較するこのステップに対応するコストは次のとおりです。230 x 0.1 = 23

したがって、この例で idx_key1 を使用してクエリを実行するコストは次のようになります。

  • I/O成本:0.75 + 57.5 =58.25
  • CPU成本:23+23.01=46.01

要約すると、idx_key1 を使用してクエリを実行する総コストは次のとおりです。58.25 + 46.01 = 104.26コードを直接アップロードすると、ナンセンスではなく、豪華な検証が行われます。

mysql> explain format=json select * from demo8 where key1 in ('aa','bb','cc') and key2 > 10 and key2 < 1000 and key3 > key2 and key_part1 like '%3f%' and common_field='1281259';

ここに画像の説明を挿入
インデックスマージは使用できますか?

この例では、key1と のkey2検索条件はAND連結を使用して接続されていますが、idx_key1と はidk_key2範囲クエリです。つまり、見つかった非クラスター化インデックス レコードは主キー値に従ってソートされておらず、Intersection条件を満たしていません。インデックスのマージを使用するため、インデックスのマージは使用されません。

小提示:
MySQL クエリ オプティマイザーがインデックスのマージのコストを計算するために使用するアルゴリズムも複雑です。ここでは説明しませんが、コストの計算方法を理解し、MySQL がこのアルゴリズムに従ってインデックスを選択することを知っておいてください。

ステップ 4:さまざまな実行計画のコストを比較し、最もコストが低い実行計画を見つけます。

この例のクエリを実行するためのさまざまな実行可能スキームと、それらに対応するコストを以下に示します。

  • 全表扫描の費用:2042.95
  • 使用したidx_key2費用:445.31
  • 使用したidx_key1費用:104.26

明らかに、idx_key1使用コストが最も低いため、idx_key1クエリの実行を選択します。

2.3 インデックス統計に基づくコスト計算

インデックスを使用してクエリを実行する場合、多数の単一ポイント間隔が存在することがあります。たとえば、IN ステートメントを使用すると、以下のクエリのような多数の単一ポイント間隔を簡単に生成できます (以下のクエリ ステートメントの ...はパラメータがたくさんあることを示します):

select * from demo8 where key1 in ('aa', 'bb', 'cc', ... , 'ee');

明らかに、このクエリで使用される可能性のあるインデックスは idx_key1 です。このインデックスは唯一のセカンダリ インデックスではないため、単一点間隔に対応するセカンダリ インデックス レコードの数を決定することはできません。計算する必要があります。計算方法は上で紹介しましたが、まずインデックスに対応する B+ ツリーの区間の左端のレコードと右端のレコードを取得し、これら 2 つのレコードの間にレコードがいくつあるかを計算します (これは、レコード数が少ない場合)正確な計算、場合によっては推定のみ)。MySQL は、インデックスに対応する B+ ツリーに直接アクセスすることで、特定の範囲間隔に対応するインデックス レコードの数を計算するこのメソッドを呼び出しますindex dive

小提示:
dive を中国語に直訳すると、潜水と急降下という意味になります。英語を許してください。インデックスダイブ、インデックスダイブ?インデックス急上昇?適当ではなさそうなので一切訳しません。ただし、インデックス ダイブとは、インデックスに対応する B+ ツリーを直接使用して、特定の範囲に対応するレコードの数を計算することであることを誰もが理解する必要があります。

複数の単一点間隔がある場合、これらの単一点間隔に対応するレコードの数を計算するためにインデックス ダイブ法を使用することは問題ありませんが、IN ステートメントに内容を詰め込もうとする友人には我慢できません。 IN ステートメントのパラメータに 20,000 のレコードがある場合、これは、これらの単一ポイント間隔に対応するインデックス レコードの数を計算するために、MySQL クエリ オプティマイザが 20,000 のインデックス ダイブ操作を実行する必要があることを意味します。レコードのコストは、直接全テーブル スキャンのコストよりも高くなります。もちろん、MySQL はこの状況を考慮してシステム変数を提供していますeq_range_index_dive_limit。このシステム変数のデフォルト値を見てみましょう。

mysql> show variables like '%dive%';
+---------------------------+-------+
| Variable_name             | Value |
+---------------------------+-------+
| eq_range_index_dive_limit | 200   |
+---------------------------+-------+
1 row in set (0.00 sec)

つまり、IN ステートメントのパラメーターの数が 200 未満の場合は、インデックス ダイブを使用して、各単一点間隔に対応するレコードの数を計算します。200 以上の場合は、インデックス ダイブを使用します。は使用できません。いわゆるインデックス統計を使用して推定されます。どのような見積もりですか? 読む。

MySQL は各テーブルの統計データを維持し、MySQL はテーブル内の各インデックスの統計データも維持します。テーブル内のインデックスの統計データを表示するために使用できる構文。たとえば、それぞれを見ていきますshow index from 表名demo8Index 統計は次のように記述できます。

mysql> show index from demo8;
+-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| demo8 |          0 | PRIMARY      |            1 | id          | A         |       18750 |     NULL |   NULL |      | BTREE      |         |               | YES     | NULL       |
| demo8 |          0 | idx_key2     |            1 | key2        | A         |       18565 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| demo8 |          1 | idx_key1     |            1 | key1        | A         |         256 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| demo8 |          1 | idx_key3     |            1 | key3        | A         |        4053 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| demo8 |          1 | idx_key_part |            1 | key_part1   | A         |       16122 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| demo8 |          1 | idx_key_part |            2 | key_part2   | A         |       18570 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| demo8 |          1 | idx_key_part |            3 | key_part3   | A         |       18570 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
+-------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
7 rows in set (0.02 sec)

多くの属性がありますが、これらの属性を理解するのは難しくありません。ここでこれらの属性を簡単に紹介します。

属性名 説明
テーブル インデックスが属するテーブルの名前
一意ではありません インデックス列の値が一意であるかどうか、クラスター化インデックスと一意のセカンダリ インデックスの列の値が 0、通常のセカンダリ インデックスの列の値が 1 であるかどうか
キー名 インデックス名
Seq_in_index インデックス内のインデックス列の位置 (1 から数えます)。たとえば、ジョイント インデックス idx_key_part の場合、key_part1、key_part2、key_part3 の対応する位置はそれぞれ 1、2、3 になります。
列名 インデックス列の名前
照合 インデックス列に値を格納する際のソート方法で、値がAの場合は昇順、NULLの場合は降順に格納することを意味します
カーディナリティ インデックス列の一意の値の数。このプロパティについては後ほど取り上げます。
サブパート 文字列またはバイト文字列を格納する列の場合、これらの文字列の最初の n 文字またはバイトのみにインデックスを付けたい場合があり、この属性は n 値を表します。列全体にインデックスが付けられている場合、このプロパティの値は NULL になります。
詰め込まれた インデックス列の圧縮方法。NULL 値は圧縮されていないことを意味します。当面はこの属性が理解できないため、最初は無視してかまいません。
ヌル インデックス列に NULL 値の格納を許可するかどうか。
インデックスの種類 使用されるインデックスのタイプ。最も一般的に使用されるのは BTREE で、実際には B+ ツリー インデックスです。
コメント インデックス列のコメント情報
インデックスコメント 索引注釈情報

上記の属性Packedを除けば、理解できないことは何もないはずですが、もし理解できなかったとしても、前回の記事を読んだときに読み飛ばしたはずです。実際、現在最も懸念しているのは Cardinality 属性です。Cardinalityこれは文字通り基数インデックス列の一意の値の数を意味します。たとえば、10,000 個のレコードを持つテーブルの場合、インデックス列のカーディナリティ属性は 10000 であり、列に重複する値がないことを意味します。カーディナリティ属性が 1 の場合、すべての値が存在することを意味します。列の は重複しています。ただし、InnoDB ストレージ エンジンの場合、show indexステートメントによって表示されるインデックス列の Cardinality 属性は 1 であり估计值、正確ではないことに注意してください。この Cardinality 属性の値がどのように計算されるかについては後で説明し、それが何に使用されるかを見てみましょう。

前述したように、 IN ステートメントでパラメータ番号>=システム変数の値を指定する場合、各単一点間隔に対応するインデックス レコードの数を計算するeq_range_index_dive_limit方法は使用されませんが、インデックス統計データ、インデックス統計データが参照されます。index diveここまで 次の 2 つの値を指します。

  • show table statusステートメントによって表示される値Rows、つまりテーブル内のレコードの数を使用します。

  • show indexステートメントによって表示されるCardinalityプロパティの使用

前の行統計と組み合わせると、インデックス列で値が繰り返される平均回数を計算できます。テーブルのインデックスを例に一个值的重复次数≈Rows÷Cardinalityとると、その Rows 値は であり、インデックス列 key1 のに対応するため、 key1 列の平均単一値の繰り返し数を計算できます。demo8idx_key120187Cardinality25620187÷256≈79

次に、上記のクエリ ステートメントを見てみましょう。

select * from demo8 where key1 in ('aa', 'bb', 'cc', ... , 'ee');

IN ステートメントにパラメーターがあると仮定すると20000、統計データを直接使用して、これらのパラメーターの単一点範囲に対応するレコード数を推定します。各パラメーターは約791 レコードに対応するため、必要なレコードの総数は次のようになります。テーブルに返されるのは次のとおりです。20000 x 79 = 1580000

統計データを使用して単一ポイント間隔に対応するインデックス レコードの数を計算するのはindex diveはるかに簡単ですが、不精确!統計データを使用して計算されたクエリ コストが実際のコストと大きく異なる可能性があるという致命的な弱点があります。

小提示:
クエリで IN クエリが使用されているが、インデックスが実際には使用されていない場合は、eq_range_index_dive_limit 値が小さすぎるかどうかを考慮する必要があります。

3. 接続クエリのコスト

2.1 データの準備

接続クエリには少なくとも 2 つのテーブルが必要ですが、demo8 テーブルが 1 つだけでは十分ではないため、ストーリーをスムーズに進めるために、demo8 テーブルとまったく同じ 2 つの s1 テーブルと s2 テーブルを直接構築します。

mysql> create table s1 (    
id int not null auto_increment,    
key1 varchar(100),    
key2 int,    
key3 varchar(100),    
key_part1 varchar(100),    
key_part2 varchar(100),    
key_part3 varchar(100),    
common_field varchar(100), 
primary key (id),
key idx_key1 (key1),    
unique key idx_key2 (key2),    
key idx_key3 (key3),    
key idx_key_part(key_part1, key_part2, key_part3));
Query OK, 0 rows affected (0.04 sec)

mysql> create table s2 (    
id int not null auto_increment,    
key1 varchar(100),    
key2 int,    
key3 varchar(100),    
key_part1 varchar(100),    
key_part2 varchar(100),    
key_part3 varchar(100),    
common_field varchar(100), 
primary key (id),
key idx_key1 (key1),    
unique key idx_key2 (key2),    
key idx_key3 (key3),    
key idx_key_part(key_part1, key_part2, key_part3));
Query OK, 0 rows affected (0.04 sec)

mysql> insert into s1 select * from demo8;
Query OK, 20000 rows affected (0.83 sec)
Records: 20000  Duplicates: 0  Warnings: 0

mysql> insert into s2 select * from demo8;
Query OK, 20000 rows affected (0.89 sec)
Records: 20000  Duplicates: 0  Warnings: 0

2.2 条件フィルタリングの概要

前に述べたように、MySQL の結合クエリはネストされたループ結合アルゴリズムを使用し、駆動テーブルには 1 回アクセスされ、駆動テーブルには複数回アクセスされる可能性があるため、2 つのテーブルの結合クエリのクエリ コストは次のようになります。 2 つの部分で構成されます。

  • テーブルを駆動する単一のクエリのコスト
  • 駆動テーブルを複数回クエリするコスト ( 具体查询多少次取决于对驱动表查询的结果集中有多少条记录)

駆動テーブルを問い合わせて得られたレコード数を駆動テーブル扇出(英語名:fanout)と呼びます。明らかに、駆動テーブルのファンアウト値が小さいほど、駆動テーブルへのクエリの数が減り、接続クエリの総コストが低くなります。クエリ オプティマイザーが結合クエリ全体のコストを計算する場合、駆動テーブルのファンアウト値を計算する必要があります。次の 2 つのクエリのように、ファンアウト値の計算が非常に簡単な場合もあります。

クエリ 1:

select * from s1 inner join s2;

テーブルが駆動テーブルとして使用されると仮定するとs1、駆動テーブルの単一テーブル クエリはフル テーブル スキャンによってのみ実行できることは明らかであり、駆動テーブルのファンアウト値も非常に明確です。 、駆動テーブル内のレコードの数、ファンアウト値は数値です。統計データの s1 テーブルのレコード数は yes です20250。つまり、オプティマイザはそれをテーブル20250のファンアウト値として直接みなします。s1

クエリ 2:

select * from s1 inner join s2 where s1.key2 > 10 and s1.key2 < 1000;

引き続き s1 テーブルが駆動テーブルであると仮定すると、駆動テーブルに対する単一テーブル クエリが idx_key2 インデックスを使用してクエリを実行できることは明らかです。このとき、idx_key2の範囲間隔(10,1000)にレコードが何件あり、ファンアウト値はいくらになるでしょうか。先ほど、idx_key2 の範囲間隔 (10, 1000) を満たすレコードの数は 989 であると計算しました。これは、このクエリでは、オプティマイザが 95 を駆動テーブル s1 のファンアウト値と見なすことを意味します。

もちろん、物事が常にスムーズに進むわけではありません。さもなければ、プロットが平坦すぎる可能性があります。次のクエリのように、ファンアウト値の計算が複雑になる場合があります。

クエリ 3:

select * from s1 inner join s2 where s1.common_field > 'xyz'

このクエリはクエリ 1 と似ていますが、駆動テーブル s1 のcommon_field > 'xyz'検索条件が1 つ多い点が異なります。クエリ オプティマイザーは実際にはクエリを実行しないため、只能猜このレコード20250内で common_field > 'xyz' 条件を満たすレコードがいくつあるか

クエリ 4:

select * from s1 inner join s2 where s1.key2 > 10 and s1.key2 < 1000 and s1.common_field > 'xyz'

ただし、このクエリはインデックスを使用できるため、セカンダリインデックスの範囲を満たすレコードの中から、条件を満たすレコードが何件あるかを推測するだけ、つまり、条件を満たすレコードidx_key2何件あるかを推測するだけで済みます。common_field > 'xyz'989common_field > 'xyz'

クエリ 5:

select * from s1 inner join s2 where s1.key2 > 10 and s1.key2 < 1000 and s1.key1 in('aa','bb','cc') and s1.common_field > 'xyz'

このクエリはクエリ 2 に似ていますが、ドライブ テーブルがクエリを実行するインデックスをs1選択した後、オプティマイザはセカンダリ インデックスの範囲を満たすレコードから次の 2 つの条件を満たすレコードの数を選択する必要があります。idx_key1

  • key2 > 10 および key2 < 1000
  • 共通フィールド > 'xyz'

つまり、オプティマイザは、230上記の 2 つの条件を満たすレコードがいくつあるかを推測する必要があります。

ここまで述べてきましたが、実際に言いたいのは、次の 2 つのケースでは、ドライブ テーブルのファンアウト値を計算するときに推測に頼る必要があるということです。

  • フルテーブルスキャンによって実行される単一テーブルクエリを使用する場合、駆動テーブルのファンアウトを計算するときに、検索条件を満たすレコードの数を推測する必要があります。
  • インデックスによって実行される単一テーブル スキャンを使用している場合、駆動テーブルのファンアウトを計算するときに、対応するインデックスを使用する条件以外の他の検索条件を満たすレコードの数を推測する必要があります。

MySQL では、この推測プロセスを呼び出しますcondition filteringもちろん、このプロセスではインデックスや統計データが使用される場合もあれば、MySQL による単なる推測である場合もありますが、評価プロセス全体は非常に複雑なので、ここでは省略します。

2.3 複数テーブル接続のコスト分析

ここではまず、マルチテーブル接続中に生成される接続シーケンスの数を検討します。

  • テーブル A とテーブル B 間の接続など、2 つのテーブルの接続の場合、接続順序は AB と BA の 2 つだけです。実際には、2 × 1 = 2 の接続シーケンスに相当します。
  • テーブル A、テーブル B、テーブル C の 3 つのテーブルの接続には、ABC、ACB、BAC、BCA、CAB、CBA の 6 つの接続順序があります。実際には、3 × 2 × 1 = 6 回の接続シーケンスに相当します。
  • 4 つのテーブル接続の場合、4 × 3 × 2 × 1 = 24 の接続シーケンスが存在します。
  • n 個のテーブルの接続には、n × (n-1) × (n-2) × · · · × 1 個の接続順序があり、これは n の階乗接続順序、つまり n!

4. 調整コスト定数

以前に 2 つを紹介しました成本常数

  • ページの読み取りにかかるデフォルトのコストは次のとおりです。0.25
  • レコードが検索条件に一致するかどうかを確認するためのデフォルトのコストは次のとおりです。0.1

実際、これら 2 つのコスト定数に加えて、MySQL は多くのコスト定数もサポートしており、それらはmysqlデータベースの 2 つのテーブル (これは前に紹介したシステム データベースです) に格納されます。

mysql> show tables from mysql like '%cost%';
+--------------------------+
| Tables_in_mysql (%cost%) |
+--------------------------+
| engine_cost              |
| server_cost              |
+--------------------------+
2 rows in set (0.06 sec)

前に述べたように、ステートメントの実行は実際には 2 つの層に分かれています。

  • サーバー層
  • ストレージエンジン層

接続server层管理、クエリ キャッシュ、構文分析、クエリの最適化などの操作では、特定のデータ アクセス操作がストレージ エンジン層で実行されます。つまり、サーバー層でステートメントを実行するコストは、ステートメントが操作されるテーブルで使用されるストレージ エンジンとは何の関係もありません。そのため、これらの操作に対応するコスト定数は、server_cost テーブルに格納され、いくつかのパラメータに依存します。ストレージ エンジンの操作に対応するコスト定数は、engine_cost テーブルに保存されます。

4.1 server_cost 表

server_cost テーブル内のサーバー層で実行される一部の操作に対応するコスト定数は次のとおりです。

mysql> select * from mysql.server_cost;
+------------------------------+------------+---------------------+---------+---------------+
| cost_name                    | cost_value | last_update         | comment | default_value |
+------------------------------+------------+---------------------+---------+---------------+
| disk_temptable_create_cost   |       NULL | 2023-04-24 19:39:12 | NULL    |            20 |
| disk_temptable_row_cost      |       NULL | 2023-04-24 19:39:12 | NULL    |           0.5 |
| key_compare_cost             |       NULL | 2023-04-24 19:39:12 | NULL    |          0.05 |
| memory_temptable_create_cost |       NULL | 2023-04-24 19:39:12 | NULL    |             1 |
| memory_temptable_row_cost    |       NULL | 2023-04-24 19:39:12 | NULL    |           0.1 |
| row_evaluate_cost            |       NULL | 2023-04-24 19:39:12 | NULL    |           0.1 |
+------------------------------+------------+---------------------+---------+---------------+
6 rows in set (0.00 sec)

まず、server_cost の各列の意味を見てみましょう。

  • cost_name: コスト定数の名前を示します。
  • cost_value:コスト定数に相当する値を示します。この列の値が NULL の場合、対応するコスト定数がデフォルト値を採用することを意味します。
  • last_update: レコードが最後に更新された時刻を示します。
  • comment: コメント
  • default_value:デフォルト

server_cost の内容から、サーバー層の一部の操作に対応するコスト定数は次のとおりであることがわかります。

コスト定数名 デフォルト 説明
disc_temptable_create_cost 40.0 ディスクベースの一時テーブルを作成するコスト。この値を増やすと、オプティマイザはディスクベースの一時テーブルをできるだけ少なく作成します。
disc_temptable_row_cost 1.0 ディスクベースの一時テーブルへのレコードの書き込みまたは読み取りのコスト。この値を増やすと、オプティマイザはディスクベースの一時テーブルをできるだけ少なく作成します。
key_compare_cost 0.1 2 つのレコードを比較するコストは、主にソート操作で使用されます。この値を増やすと、ファイルソートのコストが増加し、オプティマイザはファイルソートではなくインデックスを使用してソートを完了する傾向が強くなる可能性があります。
Memory_temptable_create_cost 2.0 メモリベースの一時テーブルを作成するコスト。この値を増やすと、オプティマイザはメモリベースの一時テーブルをできるだけ少なく作成します。
Memory_temptable_row_cost 0.2 メモリベースの一時テーブルへのレコードの書き込みまたは読み取りのコスト。この値を増やすと、オプティマイザは可能な限り少数のメモリベースの一時テーブルを作成します。
row_evaluate_cost 0.2 これは、レコードが以前に使用していた検索条件を満たしているかどうかを検出するコストです。この値を増やすと、オプティマイザが直接の全テーブル スキャンではなくインデックスを使用する傾向が強くなる可能性があります。

小提示:
MySQL は、特定の特別な条件下で DISTINCT クエリ、グループ化クエリ、Union クエリ、ソート クエリなどのクエリを実行するときに内部で一時テーブルを作成する場合があり、この一時テーブルを使用してクエリの完了を支援します (たとえば、DISTINCT クエリの場合は、 UNIQUE インデックスの一時テーブルは、重複排除する必要があるレコードをこの一時テーブルに直接挿入し、挿入が完了した後のレコードが結果セットになります)。大量のデータの場合は、ディスクベースの一時テーブルを作成することができます。つまり、一時テーブルに MyISAM や InnoDB などのストレージ エンジンを使用し、必要に応じてメモリベースの一時テーブルを作成します。データ量は大きくありません。つまり、製品エンジンのメモリ ストレージを使用します。ここにいる誰もが、一時テーブルの作成とこの一時テーブルの書き込みと読み取りのコストが依然として非常に高いことを知っています。

これらのコスト定数server_costの初期値はNULLです。これは、オプティマイザがそのデフォルト値を使用して操作のコストを計算することを意味します。特定のコスト定数の値を変更したい場合は、2 つの手順を実行する必要があります:

ステップ 1:関心のあるコスト定数を更新する

たとえば、レコードが検索条件を満たしているかどうかを確認するコストを増やしたい場合は、0.3次のような更新ステートメントを作成できます。

update mysql.server_cost set cost_value = 0.4 where cost_name = 'row_evaluate_cost';

ステップ 2:システムにこのテーブルの値を再ロードさせます。次のステートメントを使用するだけです。

flush optimizer_costs;

もちろん、特定のコスト定数を変更した後にそれらを元に戻したい場合は默认值cost_value値を直接 に設定しNULLflush optimizer_costsステートメントを使用してシステムにその値を再ロードさせることができます。

4.2 engine_cost 表

engine_cost表内のストレージ エンジン層で実行される一部の操作に対応するコスト定数は次のとおりです。

mysql> select * from mysql.engine_cost;
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
| engine_name | device_type | cost_name              | cost_value | last_update         | comment | default_value |
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
| default     |           0 | io_block_read_cost     |       NULL | 2023-04-24 19:39:12 | NULL    |             1 |
| default     |           0 | memory_block_read_cost |       NULL | 2023-04-24 19:39:12 | NULL    |          0.25 |
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
2 rows in set (0.01 sec)

と比較するとserver_costengine_cost列が 2 つ増えています。

  • engine_name列: コスト定数が適用されるストレージ エンジン名を指します。値がデフォルトの場合、対応するコスト定数がすべてのストレージ エンジンに適用されることを意味します。
  • device_type列: ストレージ エンジンで使用されるデバイス タイプを指します。これは主に、従来のメカニカル ハードディスクとソリッド ステート ハードディスクを区別するためのものです。ただし、MySQL 5.7.21 では、メカニカル ハードディスクとソリッド ステート ハードディスクの
    コスト値はデフォルトでは 0 です

Engine_cost テーブルの内容から、現在サポートされているストレージ エンジンのコスト定数は 2 つだけであることがわかります。

コスト定数名 デフォルト 説明
io_block_read_cost 1.0 ディスクからブロックを読み取るコスト。ページではなくブロックという単語を使用していることに注意してください。InnoDB ストレージ エンジンの場合、ページは 1 ブロックですが、MyISAM ストレージ エンジンの場合、デフォルトは 4096 バイトのブロックです。この値を増やすと I/O コストが増加するため、オプティマイザはテーブル全体のスキャンを実行する代わりにインデックスを使用してクエリを実行することを選択する傾向が強まる可能性があります。
メモリブロック読み取りコスト 0.25 前のパラメータと似ていますが、メモリからのブロックの読み取りに対応するコストを測定する点が異なります。

これら 2 つのコスト定数のデフォルト値を読んだ後、少し混乱しませんか? メモリからのブロックの読み取りのデフォルト コストがディスクからのデフォルトのコストと異なるのはなぜですか? これは主に、MySQL が進化するにつれて、どのブロックがディスク上にあり、どのブロックがメモリ内にあるかを MySQL が正確に予測できるためです。

server_costテーブル内のレコードを更新するのと同様に、engine_costテーブル内のレコードを更新することでストレージ エンジンに関するコスト定数を変更することもできます。また、テーブルにengine_cost新しいレコードを挿入することで特定のストレージ エンジンに固有のコスト定数を追加することもできます。

ステップ 1:特定のストレージ エンジンのコスト定数を挿入します。
たとえば、InnoDB ストレージ エンジン ページの I/O コストを増加したい場合は、通常の挿入ステートメントを作成するだけです。

insert into mysql.engine_cost values ('innodb', 0, 'io_block_read_cost', 2.0, current_timestamp, 'increase innodb i/o cost');

ステップ 2:次のステートメントを使用して、システムにこのテーブルの値を再ロードさせます。

flush optimizer_costs;

今日の勉強はこれで終わりです、あなたが壊れない自分になれることを願っています
~~~

先を見据えて点と点を結ぶことはできません。過去を振り返って接続することしかできません。したがって、点と点が何らかの形であなたの将来につながると信じなければなりません。あなたは何かを信頼しなければなりません - 自分の直感、運命、人生、カルマ、何でも。このアプローチは私を決して失望させず、私の人生に大きな変化をもたらしました

私のコンテンツがあなたのお役に立てましたら、どうぞ点赞、、、創作は簡単ではありません、皆さんのサポートが私が頑張れる原動力です评论收藏

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/liang921119/article/details/130779501