MySQL SQLオプティマイザのコスト計算アルゴリズム

Slides:



Advertisements
Similar presentations
土木計画学 第3回:10月19日 調査データの統計処理と分析2 担当:榊原 弘之. 標本調査において,母集団の平均や分散などを直接知ることは できない. 母集団の平均値(母平均) 母集団の分散(母分散) 母集団中のある値の比率(母比率) p Sample 標本平均 標本分散(不偏分散) 標本中の比率.
Advertisements

SQL による数独の解法 青山学院大学理工学部 矢吹太朗・佐久田博司. 数独とは何か ナンプレとも呼ばれ る制約充足問題 各行・列・ブロック に 1 から 9 の数字を一 つずつ当てはめる 新聞等に載っている ものはとても簡単 人間には難しいもの → もある.
Accessによるデータベース(1) Ver.1 /11.
プログラムのパタン演習 解説.
DB(データベース)のおはなし 作成者:小野正広 DBと言っても、  ドラゴンボール ではないですぞ! 3/1/2017.
SQLite3
情報理工学部 情報システム工学科 ラシキアゼミ 3年 H 井奈波 和也
SAP システムにおける SQL Server 運用ノウハウ
近似アルゴリズム 第10章 終了時刻最小化スケジューリング
3-1 MySQLについて 発表者:藤村元彦 自然言語処理研究室.
MySQLに接続するデータベースプログラム
Ex7. Search for Vacuum Problem
Ex8. Search for Vacuum Problem(2)
6-2 データベース 1.SQLite SQLを単純化した SQLite を使ってデータベースを操作 表「fruit」
技術トピックス 2015/03.
SQL J2EE I 第3回 /
RDBMSについて 2年7組  小鹿 慎太郎.
3-2.データを取り出す 2004年 5月20日(木) 01T6074X 茂木啓悟.
第7章 データベース管理システム 7.1 データベース管理システムの概要 7.2 データベースの格納方式 7.3 問合せ処理.
JQueryでAjax 藤田@ジャストプレイヤー ※参考しまくり文献 jQuery日本語リファレンス.
ML 演習 第 7 回 新井淳也、中村宇佑、前田俊行 2011/05/31.
パフォーマンスチューニング on Rails
SAP & SQL Server テクニカルアーキテクチャ概要 マイクロソフト株式会社 SAP/Microsoft コンピテンスセンター
14.テーブル定義,一対多の関係,多対多の関係, 外部キー,索引(インデックス),データベース操作
table 'results' SELECT name, teacher FROM results;
マイクロソフト Access での SQL 演習 第1回 SQL問い合わせ(クエリ)
データベースとJavaをつなげよう! ~JDBC~
データベース基礎 2016年3月10日 JWord株式会社 サービス開発部 中川 陽平.
第7回 条件による繰り返し.
2004/05/13 3-4 データ型(カラムタイプ) について 発表者:藤村元彦 自然言語処理研究室.
SQL パフォーマンス チューニング ~ カバーリングインデックス/クエリヒントの利用~
第2回.リレーショナルデータベース入門 SQL を用いたテーブルへの行の挿入 SQL 問い合わせの発行と評価結果の確認.
3-10. MySQLシステムの管理  2004年6月10日  大北高広                01T6010F.
第1回.リレーショナルデータベースを使ってみよう
第1回.リレーショナルデータベースを使ってみよう
第2回.リレーショナルデータベース入門 SQL を用いたテーブルへの行の挿入 SQL 問い合わせの発行と評価結果の確認.
SQL パフォーマンス チューニング ~ プランガイドの利用~
第3回.テーブルの結合 結合条件 SQL を用いた結合問い合わせ.
第3回.テーブルの結合 結合条件 SQL を用いた結合問い合わせ.
人工知能特論 9.パーセプトロン 北陸先端科学技術大学院大学 鶴岡 慶雅.
第7回 条件による繰り返し.
3-6.インデックスについて 3-7.関数と併用されることの 多いMySQLコマンド
3-3.テーブルを更新する 2004年 4月22日(木) 01T6074X 茂木啓悟.
「Webデータベースの構築技術」正誤表 ページ 項目 誤記 訂正 18 表1.4 アクセス 権限の削除 ・・・テーブル名 TO ユーザ名
マイクロソフト Access での SQL 演習 第4回 並べ替え(ソート)
講義ノート共有データベース NoteTotter?
テーブル設計を後から変更 現場で使える小技のご紹介 株式会社ジーワンシステム 生島 勘富(イクシマ サダヨシ)
Data Clustering: A Review
第4回 ファイル入出力方法.
Ex7. Search for Vacuum Problem
プログラミング 4 探索と計算量.
マイクロソフト Access での SQL 演習 第2回 集計,集約
3-8・関数を使ってデータを取り出す   2004年6月3日(木) 01T6010F               大北高広.
3.リレーショナルデータベース,主キー, SQL
Mondriaan Memory Protection の調査
再帰CTE を使って遊ぼう 大阪#9 2012/04/14.
小標本に関する平均の推定と検定 標本が小さい場合,標本分散から母分散を推定するときの不確実さを加味したt分布を用いて,推定や検定を行う
SQL パフォーマンス チューニング ~ パフォーマンス改善 最初の一歩 ~
CO-Client Opeartion 1.1 利用履歴データベースの設計 (スキーマ バージョン 対応)
09 06/23 PHP と SQL (MySQL) の連携 その3
Molecular Devices Japan
第2回.リレーショナルデータベース入門 SQL を用いたテーブルへの行の挿入 SQL 問い合わせの発行と評価結果の確認.
情報処理Ⅱ 2005年11月25日(金).
SQL J2EE I (データベース論) 第3回 /
プログラミング演習II 2004年11月 2日(第3回) 理学部数学科・木村巌.
プログラミング入門2 第5回 配列 変数宣言、初期化について
TList リスト構造とは? 複数のデータを扱うために、 データの内容と、次のデータへのポインタを持つ構造体を使う。
クリエイティブ リサーチ 2019/05/20 日本工学院八王子専門学校 M.Katsube.
SQL データベース論 第11回.
プログラミング論 バイナリーサーチ 1.
Presentation transcript:

MySQL SQLオプティマイザのコスト計算アルゴリズム InnoDB Deep Talk #1 2012/03/10 平塚 貞夫

自己紹介 DBエンジニアやってます。専門はOracleとMySQL。 システムインテグレータで主にRDBMSのトラブル対応をしています。 Twitter:@sh2nd はてな:id:sh2 写真は実家で飼っているミニチュアダックスのオス、アトムです。

本日のお題 MySQLのステータス変数にLast_query_costというものがありまして、これを参照すると直前に実行したSQLのコストを確認することができるようになっています。この値は実績値ではなく、SQLオプティマイザがSQL実行計画を選択するために算出した推定値を表しています。 本日は、みなさんにこれを計算できるようになっていただきます。 MySQL 5.6.4-m7を用いて説明していきます。 mysql> SELECT * FROM item WHERE i_name = 'NFOHP7ywvB'; +------+------------+---------+ | i_id | i_name | i_price | | 50 | NFOHP7ywvB | 10161 | mysql> SHOW LOCAL STATUS like 'Last_query_cost'; +-----------------+--------------+ | Variable_name | Value | | Last_query_cost | 20365.399000 |

実践ハイパフォーマンスMySQLについて 残念ながら、この説明は誤りです。忘れてください。  MySQLはコストベースのオプティマイザを使用するため、さまざまな実行プランのコストを見積もり、最も安価なものを選択しようとする。コストの単位は、ランダムな4KBデータページの読み取りである。オプティマイザが見積もったクエリのコストを確認するには、クエリを実行して、Last_query_costセッション変数を調べればよい。 mysql> SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor; +----------+ | count(*) | | 5462 | mysql> SHOW STATUS LIKE 'last_query_cost'; +-----------------+-------------+ | Variable_name | Value | | Last_query_cost | 1040.599000 |  この結果から、オプティマイザがクエリを実行するために1,040ページのランダムデータを読み取る必要があると見積もっていることがわかる。

テストデータ 以下の商品テーブルを使って調べていきます。 10万レコード用意しました。商品名と商品価格はランダムな値になっています。 CREATE TABLE `item` ( `i_id` int(11) NOT NULL, `i_name` varchar(100) DEFAULT NULL, `i_price` int(11) DEFAULT NULL, PRIMARY KEY (`i_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 +--------+----------------------+---------+ | i_id | i_name | i_price | | 1 | Y7I8zlqJ3ZuYnBasy | 57231 | | 2 | wGDEP8MC3Gs | 89587 | | 3 | DvEFlHVcWLP3Zp | 67530 | | 4 | uoVnfCF0Omtg | 18176 | | 5 | tNTodSMwd3F13Np1 | 98690 | ~ | 100000 | cA7dnrmM32 | 14062 |

単一テーブルのフルスキャン

単一テーブルのフルスキャン (1/4) まずテーブルをフルスキャンしたときのコストを確認していきましょう。 商品名カラムにインデックスがないので、商品名を指定したSQLはテーブルフルスキャンになります。 このときのコストは20,365になりました。 mysql> EXPLAIN SELECT * FROM item WHERE i_name = 'NFOHP7ywvB'; +----+-------------+-------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | | 1 | SIMPLE | item | ALL | NULL | NULL | NULL | NULL | 100382 | Using where | mysql> SHOW LOCAL STATUS like 'Last_query_cost'; +-----------------+--------------+ | Variable_name | Value | | Last_query_cost | 20365.399000 |

単一テーブルのフルスキャン (2/4) SQLのコストは以下の二つのパラメータから計算されます。 found_records 読み取られるレコード数の推定値。 read_time ディスクアクセス回数の推定値。 フルスキャンの場合、 found_recordsにはテーブルの統計情報が用いられます。 ※InnoDBのテーブル統計情報はサンプリングで求めているため、正確に10万レコードにはなりません。 mysql> SHOW TABLE STATUS LIKE 'item'\G *************************** 1. row *************************** Name: item Engine: InnoDB Version: 10 Row_format: Compact Rows: 100382 ★これがそのままfound_recordsになります Avg_row_length: 47 Data_length: 4734976

単一テーブルのフルスキャン (3/4) read_timeはフルスキャンの場合、scan_time()という関数を通してストレージエンジンに問い合わせた結果が用いられます。 InnoDBの場合、scan_time()はテーブルのページ数を返します。 これはテーブルステータスのData_lengthを16KBで割り算した値と同じです。 UNIV_INTERN double ha_innobase::scan_time() { return((double) (prebuilt->table->stat_clustered_index_size)); } mysql> SHOW TABLE STATUS LIKE 'item'\G *************************** 1. row *************************** Name: item Engine: InnoDB Version: 10 Row_format: Compact Rows: 100131 Avg_row_length: 47 Data_length: 4734976 ★4,734,976÷16,384=289がread_timeになります。

コスト=read_time+found_records×ROW_EVALUATE_COST 単一テーブルのフルスキャン (4/4) ここまででfound_recordsとread_timeが求められました。 found_records:100,382 read_time:289 SQLのコストは、found_recordsとread_timeから以下の式で算出されます。 ROW_EVALUATE_COSTは0.20で固定です。 フルスキャンにおけるコスト計算は以上です。 計算アルゴリズムはそれほど難しくありませんが、何の根拠があって単位の異なるfound_recordsとread_timeを足しているのか、0.20という定数にどんな意図があるのかなど疑問点がいくつか出てくると思います。ちなみに私もさっぱり分かりませんので聞かれても困ります。とりあえず先へ進みましょう。 コスト=read_time+found_records×ROW_EVALUATE_COST =289+100,382×0.20 =20,365.4 #define ROW_EVALUATE_COST 0.20

単一テーブルのユニークスキャン

単一テーブルのユニークスキャン (1/2) 次は、ユニークインデックスを用いて1レコードを引き当てたときのコストです。 このときfound_recordsとread_timeは以下の値になります。 found_records:1 read_time:1 SQLオプティマイザは、検索条件にユニークインデックスの等価条件が含まれていることが分かると必ずこのインデックスを用いるSQL実行計画を選択します。他の選択肢については探索自体が行われません。このときfound_records、read_timeには統計情報から得られた値ではなく、固定値として1が入るようになっています。 つまり、主キー検索の場合SQLオプティマイザは何も考えないので、速いです。 mysql> EXPLAIN SELECT * FROM item WHERE i_id = 20000; +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | | 1 | SIMPLE | item | const | PRIMARY | PRIMARY | 4 | const | 1 | |

単一テーブルのユニークスキャン (2/2) SQLのコストは以下のように表示されます。 なぜかゼロになっていますが、調べたところこれはMySQLの不具合であることが分かりました。開発元にバグ報告済みです。 MySQL Bugs: #64567: Last_query_cost is not updated when executing an unique key lookup http://bugs.mysql.com/bug.php?id=64567 どうやら、内部処理をショートカットしすぎたためにステータス変数の更新までスキップしてしまったようです。 実際のコストは1.0となります。前述の式に当てはめると1.2になりますが、ユニークスキャンのコストは現状1.0でハードコーディングされています。 mysql> SHOW LOCAL STATUS like 'Last_query_cost'; +-----------------+----------+ | Variable_name | Value | | Last_query_cost | 0.000000 |

単一テーブルのレンジスキャン

範囲検索における読み取りレコード数の推定 次は、インデックスの張られたカラムに対して範囲検索をしたときのコストです。いくつか例を見てみましょう。 最後の例を除き、rowsに出力される読み取りレコード数の推定値がかなり正確であることが分かると思います。まずはこの仕組みを確認していきます。 mysql> EXPLAIN SELECT * FROM item WHERE i_id BETWEEN 10001 AND 10100; +----+-------------+-------+-------+---------------+---------+---------+------+------+---------- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 1 | SIMPLE | item | range | PRIMARY | PRIMARY | 4 | NULL | 100 | Using ... mysql> EXPLAIN SELECT * FROM item WHERE i_id BETWEEN 10001 AND 12000; | 1 | SIMPLE | item | range | PRIMARY | PRIMARY | 4 | NULL | 1999 | Using ... mysql> EXPLAIN SELECT * FROM item WHERE i_id BETWEEN 10001 AND 20000; +----+-------------+-------+-------+---------------+---------+---------+------+-------+---------- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 1 | SIMPLE | item | range | PRIMARY | PRIMARY | 4 | NULL | 20776 | Using ...

他のRDBMSの場合 Oracle Databaseの場合、このような範囲検索に対してはヒストグラム統計を用いて読み取りレコード数を推定していきます。Oracle Databaseでは二種類のヒストグラムが使い分けられています。 頻度ヒストグラム カラム値と該当レコード数を組にして格納します。カラム値の種類数が少ない場合に用いられます。 高さ調整済みヒストグラム バケット番号とそのバケットに格納されるカラム値の範囲を組にして格納します。カラム値の種類数が多い場合に用いられます。 例:No.1【1~100】 No.2【101~101】 No.3【102~500】 No.4【501~1000】 ⇒ カラム値=101で検索するとバケット一つ、全体の25%にヒットすると推定。 ⇒ カラム値>150で検索するとバケット二つ、全体の50%にヒットすると推定。 PostgreSQLもOracle Databaseと同様にヒストグラム統計を用いています。PostgreSQLの場合はCompressed Histogramという、Oracle Databaseにおける頻度ヒストグラムと高さ調整済みヒストグラムを組み合わせた形のヒストグラムを利用しています。 MySQLにはヒストグラムはありません。

レンジ分析 範囲検索における読み取りレコード数の推定は、MySQL本体ではなく各ストレージエンジンに任されています。この処理のことをレンジ分析(Range Analysis)といいます。 MySQLはストレージエンジンAPIのrecords_in_range()を呼び出すことで、ストレージエンジンに読み取りレコード数の見積もりを依頼します。 次のページから、InnoDBのレンジ分析アルゴリズムを見ていきます。 MySQL本体 InnoDBストレージエンジン mysql_select() JOIN::optimize() make_join_statistics() get_quick_record_count() handler::records_in_range() ha_innobase::records_in_range() btr_estimate_n_rows_in_range()

InnoDBのレンジ分析 (1/9) インデックスのB+ツリー構造を以下に示します。それぞれの箱はInnoDBがデータを取り扱う単位である、16KBのInnoDBページを表しています。ツリー構造の頂点のページをルートページ、末端のページをリーフページと呼びます。実際には3段以上の構造になることもあります。 ルートページ i_id 351 681 ~ i_id data 1 * 2 3 4 ~ 350 i_id data 351 * 352 353 354 ~ 680 i_id data ~ * 10001 10100 10101 i_id data ~ * 10315 10316 10317 10665 i_id data 11712 * ~ 11999 12000 12001 i_id data ~ * 19998 19999 20000 20001 i_id data ~ * 99999 100000 リーフページ

InnoDBのレンジ分析 (2/9) InnoDBは範囲検索の下限値と上限値を受け取ると、最初にそれらが格納されているリーフページを読み取ってしまいます。つまり、見積もるぐらいだったら実際に数えてしまえという…。 最初のi_id BETWEEN 10001 AND 10100という例では、以下のようにどちらの値も33番のリーフページに格納されていました。 i_id 351 681 ~ i_id data 1 * 2 3 4 ~ 350 i_id data 351 * 352 353 354 ~ 680 i_id data ~ * 10001 10100 10101 i_id data ~ * 10315 10316 10317 10665 i_id data 11712 * ~ 11999 12000 12001 i_id data ~ * 19998 19999 20000 20001 i_id data ~ * 99999 100000 Page#33

読み取りレコード数の推定値=nth_rec_2-nth_rec_1 InnoDBのレンジ分析 (3/9) リーフページを読み取ることで、以下の情報を取得することができます。 そのページに含まれるインデックスエントリの数 それぞれのインデックスエントリが、ページ内で何番目の位置にあるのか (上限値がその値を含む条件のときは、実際には次のエントリを参照します) 下限値、上限値におけるインデックスエントリの番号をnth_rec_1、nth_rec_2とすると、読み取りレコード数の推定値は以下の式で表されます。前ページの例のように下限値、上限値の双方が同じリーフページに格納されている場合、この式は正確なレコード数を返します。 i_id data ~ * 10001 10100 10101 10,001は38番目のエントリ インデックスエントリ数:350 10,100の次は138番目のエントリ Page#33 読み取りレコード数の推定値=nth_rec_2-nth_rec_1 =138-38 =100

InnoDBのレンジ分析 (4/9) 次に、i_id BETWEEN 10001 AND 12000の例を見てみましょう。 この例では上限値のインデックスエントリが下限値とは別の、少し離れたリーフページに格納されています。 i_id 351 681 ~ i_id data 1 * 2 3 4 ~ 350 i_id data 351 * 352 353 354 ~ 680 i_id data ~ * 10001 10100 10101 i_id data ~ * 10315 10316 10317 10665 i_id data 11712 * ~ 11999 12000 12001 i_id data ~ * 19998 19999 20000 20001 i_id data ~ * 99999 100000 Page#33 Page#66

読み取りレコード数の推定値=(n_recs_1-nth_rec1)+Σ(n_recs_middle)+(nth_rec_2 - 1) InnoDBのレンジ分析 (5/9) 下限値と上限値が異なるリーフページに格納されている場合、読み取りレコード数を推定するにはその間に挟まっているリーフページを考慮する必要があります。InnoDBはINDEX RANGE SCANを行ってこれらを実際に読み取っていきます。 リーフページに含まれるインデックスエントリの数をn_recs_Nとすると、読み取りレコード数の推定値は以下の式で表されます。この式も先ほどの例と同様に正確なレコード数を返すとされています。実際に試してみると1足りないのですが、下限値が格納されているリーフページについて計算誤りがあるのではないかと私は考えています。 i_id data ~ * 10001 10100 10101 i_id data ~ * 10315 10316 10317 10665 i_id data ~ * 10667 10668 10669 11015 i_id data ~ * 11017 11018 11019 11361 i_id data ~ * 11363 11364 11365 11711 i_id data 11712 * ~ 11999 12000 12001 290 312 352 350 346 350 Page#33 Page#34 Page#35 Page#64 Page#65 Page#66 読み取りレコード数の推定値=(n_recs_1-nth_rec1)+Σ(n_recs_middle)+(nth_rec_2 - 1) =312+(352+350+346+350)+(290-1) =1,999

InnoDBのレンジ分析 (6/9) 次はi_id BETWEEN 10001 AND 20000という例です。この例では読み取りレコード数の推定値が実際の値から大きくずれていることが分かります。 mysql> EXPLAIN SELECT * FROM item WHERE i_id BETWEEN 10001 AND 20000; +----+-------------+-------+-------+---------------+---------+---------+------+-------+---------- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 1 | SIMPLE | item | range | PRIMARY | PRIMARY | 4 | NULL | 20776 | Using ... i_id 351 681 ~ i_id data 1 * 2 3 4 ~ 350 i_id data 351 * 352 353 354 ~ 680 i_id data ~ * 10001 10100 10101 i_id data ~ * 10315 10316 10317 10665 i_id data 11712 * ~ 11999 12000 12001 i_id data ~ * 19998 19999 20000 20001 i_id data ~ * 99999 100000 Page#33 Page#89

InnoDBのレンジ分析 (7/9) このときもINDEX RANGE SCANを行ってリーフページを実際に読み取るという方針は変わりません。しかし、リーフページが大量にある場合InnoDBは読み取りを途中で打ち切るようになっている点が異なります。現在の実装では、InnoDBは中間のリーフページを9ページ目まで読み取るようになっています。 ルートページ i_id ~ 10314 19770 20125 n_rows_on_prev_level:28 i_id data ~ * 10001 10100 10101 i_id data ~ * 19998 19999 20000 20001 254 312 n_recs_middle_9:3,144 読まない Page#33 Page#89

=(検索範囲内のリーフページ数)×(リーフページあたりの推定レコード数)×2 InnoDBのレンジ分析 (8/9) 読み取りを途中で打ち切った場合は、ルートページの情報も見積もりに用いられます。ルートページにおいて下限値へのポインタと上限値へのポインタの間にあるエントリ数をn_rows_on_prev_levelとすると、これは検索範囲内にあるリーフページの数を表していることになります。 中間のリーフページに含まれるインデックスエントリの数をn_recs_middle_9として、読み取りレコード数の推定値は以下の式で表されます。 この式には定数が二つあります。 10:読み込んだリーフページの数です。実際には11ですが(1+9+1)、下限と上限はそれぞれ0.5ページ扱いになっていると思われます。 2:補正係数です。「このアルゴリズムでは推定値が実際の値より少なくなることが多いため、2倍にした」とソースコードのコメントに書いてありました。 読み取りレコード数の推定値 =(検索範囲内のリーフページ数)×(リーフページあたりの推定レコード数)×2 =n_rows_on_prev_level×((n_recs_1-nth_rec1)+n_recs_middle_9+(nth_rec_2 - 1))÷10×2 =28×(312+3,144+254)÷10×2 =20,776

InnoDBのレンジ分析 (9/9) 最後にもう一つ補正が入ります。ここまでの計算で得られた読み取りレコード数の推定値がテーブルのレコード数の半分を超えた場合、推定値はテーブルのレコード数の半分に補正されます。 mysql> EXPLAIN SELECT * FROM item WHERE i_id BETWEEN 1 AND 100000; +----+-------------+-------+-------+---------------+---------+---------+------+-------+---------- | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 1 | SIMPLE | item | range | PRIMARY | PRIMARY | 4 | NULL | 50137 | Using ...

そろそろ疲れてきたと思いますが、あと少しなので頑張りましょう。

単一テーブルのレンジスキャン (1/2) ここまででfound_recordsが求められました。最後の例を用いて説明を続けます。 次はread_timeを求めます。フルスキャンの場合、このパラメータにはscan_time()という関数を通してテーブルのページ数289が格納されていました。レンジスキャンの場合は、read_time()という関数を通して以下の値が格納されます。 (A)の部分はフルスキャンのときと同じ考え方で、レコードの選択率を加味した上で読み取りページ数を求めています。レコード数の上限値というのは、平均レコード長ではなく最小レコード長でデータを敷き詰めたときに格納できる最大のレコード数のことを表しています。最初の定数1はMySQL 5.6のMulti-Range Readという新機能を加味した値のようですが、MySQL 5.6.4-m7の時点では特に意味のある数値にはなっていないように見受けられます。 read_time =1+(読み取りレコード数の推定値)÷(レコード数の上限値)×(テーブルのページ数) … (A) +(読み取りレコード数の推定値)×ROW_EVALUATE_COST+0.01 … (B) =1+20,776÷324,290×289+20,776×0.20+0.01 =19.5+4,155.2 =4,174.7

コスト=read_time+found_records×ROW_EVALUATE_COST 単一テーブルのレンジスキャン (2/2) (B)の部分ですが、これはフルスキャンの場合にSQLのコストを算出するところで使われていた式と同じです。この式がread_timeを求める時点で登場しているのは、本来は誤りではないかと私は考えています。結果としてread_timeの値はフルスキャンにおける289に対し4,155.2と大きく増えてしまっています。 最終的なSQLのコストは以下の式で算出されます。この式はフルスキャンの場合と同じです。 コスト=read_time+found_records×ROW_EVALUATE_COST =4,174.7+20,776×0.20 =8,329.9 mysql> SHOW LOCAL STATUS LIKE 'Last_query_cost'; +-----------------+-------------+ | Variable_name | Value | | Last_query_cost | 8329.924107 |

まとめ

単一テーブルアクセスにおけるコスト計算 「レンジスキャンにおけるコスト計算アルゴリズムはそもそも誤っているのではないか」という懸念はいったん横に置いておいて、得られたコストが結局のところ何を表しているのかを考えてみましょう。 read_timeから算出されるページ数由来のコストは実際の値が小さいこともあり、InnoDBの場合ほとんど意味を持ちません。元々read_timeは、テーブルデータをキャッシュしないMyISAMのために用意されたパラメータだと考えられます。 InnoDBにおいては、found_recordsで示される読み取りレコード数の推定値がコストの大半を占めています。大雑把にまとめると、Last_query_costは以下のように概算することが可能です。 フルスキャン:テーブルのレコード数×0.2 ユニークスキャン:1.0 10ページ以下のレンジスキャン:読み取りレコード数×0.4 11ページ以上のレンジスキャン:読み取りレコード数×0.8 (ただし、テーブルのレコード数×0.2を超えた場合はその値に補正される) またここから、InnoDBがインデックスによるデータアクセスをフルスキャンに比べて4倍、検索範囲が狭い場合は2倍高コストだとみなしていることが分かります。

もっと詳しく調べるには

MySQL 5.6 Optimizer Tracing MySQL 5.6から、Optimizer Tracingという新機能によってSQLオプティマイザの挙動を確認できるようになります。 トレース結果はinformation_schema.OPTIMIZER_TRACEテーブルにJSON形式で出力されます。 mysql> SET LOCAL optimizer_trace = 'enabled=on,end_marker=on'; mysql> EXPLAIN SELECT * FROM item WHERE i_name = 'NFOHP7ywvB'; mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE\G *************************** 1. row *************************** QUERY: EXPLAIN SELECT * FROM item WHERE i_name = 'NFOHP7ywvB' TRACE: { ~ "rows_estimation": [ { "database": "scott", "table": "item", "table_scan": { "rows": 100274, "cost": 289 } /* table_scan */ } ] /* rows_estimation */

デバッグログ mysqldのデバッグ版バイナリにdebugオプションをつけて起動すると、mysqld.traceというファイルにデバッグログが出力されます。 T@5 : | | | | | | | >JOIN::optimize T@5 : | | | | | | | | >make_join_statistics T@5 : | | | | | | | | | opt: rows: 100274 T@5 : | | | | | | | | | opt: cost: 289 T@5 : | | | | | | | | | >Optimize_table_order::choose_table_order T@5 : | | | | | | | | | | >Optimize_table_order::greedy_search T@5 : | | | | | | | | | | | >Optimize_table_order::best_extension_by_limited_search T@5 : | | | | | | | | | | | | >Optimize_table_order::best_access_path T@5 : | | | | | | | | | | | | | opt: access_type: "scan" T@5 : | | | | | | | | | | | | | opt: rows: 100274 T@5 : | | | | | | | | | | | | | opt: cost: 289 T@5 : | | | | | | | | | | | | <Optimize_table_order::best_access_path 8276 T@5 : | | | | | | | | | | | | opt: cost_for_plan: 20344 T@5 : | | | | | | | | | | | | opt: rows_for_plan: 100274 full_plan; idx :1 best: 20343.8 accumulated: 20343.8 increment: 20343.8 count: 100274 POSITIONS: item BEST_POSITIONS: item BEST_REF: item(100274,100274,289) T@5 : | | | | | | | | | | | <Optimize_table_order::best_extension_by_limited_search 9350 optimal; idx :1 best: 20343.8 accumulated: 0 increment: 0 count: 1 T@5 : | | | | | | | | | | <Optimize_table_order::greedy_search 8852 T@5 : | | | | | | | | | <Optimize_table_order::choose_table_order 8395 T@5 : | | | | | | | | <make_join_statistics 5723 T@5 : | | | | | | | <JOIN::optimize 2779

デバッグ sql/sql_select.ccのmake_join_statistics()にブレークポイントを仕掛けてステップ実行していきます。10時間ぐらい粘れば見えてくるはずです。

参考文献 Sergey Petrunia氏のMySQL Conferenceプレゼンテーション資料 Sergey氏は旧MySQL ABのSQLオプティマイザ担当で、現在はMonty Program ABでSQLオプティマイザの開発を行っています。 Understanding and Control of MySQL Query Optimizer: Traditional and Novel Tools and Techniques (2009) http://www.mysqlconf.com/mysql2009/public/schedule/detail/6864 New Query Engine Features in MariaDB (2010) http://en.oreilly.com/mysql2010/public/schedule/detail/13509 Why Are The New Optimizer Features Important and How Can I Benefit From Them? (2011) http://en.oreilly.com/mysql2011/public/schedule/detail/19899 ⇒ MariaDB 5.3でついに待望のHASH JOINが実装されました。 奥野 幹也氏(@nippondanji)のEnterpriseZine連載記事 MySQLチューニング虎の巻 Block Nested Loop Join/Batched Key Access Join http://enterprisezine.jp/dbonline/detail/3606 ⇒ MySQL 5.6で追加されたテーブル結合アルゴリズムの解説記事です。

おわりに

おわりに MySQL SQLオプティマイザのコスト計算アルゴリズムについては、これまで解説記事がどこにもありませんでした。MySQL Forgeにもありませんし、私の知る限り書籍もないです。というわけで、この資料がみなさまのお役に立てば幸いです。 今回はさわりの部分しか紹介できなかったので、ブログなどで続きを解説していければと考えています。 テーブルを結合した場合のコスト計算と探索アルゴリズム サブクエリを用いた場合のコスト計算 他のRDBMSとの違い

宿題 以下のSQLについて調べてください。 テストデータをロードして、SQL実行計画とコストを確認してください。 ordersテーブルのo_dateカラムにインデックスを作成するとSQL実行計画とコストがどのように変化するか、確認してください。 なぜorder_lineテーブルを駆動表とするSQL実行計画が選ばれなかったのか、order_lineテーブルが駆動表になった場合のコストとあわせて説明してください。 これらのコストはそれぞれどのような計算アルゴリズムで求められたのか、説明してください。 Oracle DatabaseとPostgreSQLでSQL実行計画を確認し、MySQLと比較してください。 SELECT o.o_id, ol.ol_id, ol.i_id, ol.ol_quantity, o.o_date FROM orders o INNER JOIN order_line ol ON o.o_id = ol.o_id WHERE o_date BETWEEN '2011-12-01 00:00:00' AND '2011-12-02 00:00:00';