├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── _config.yml ├── _includes ├── disqus.html ├── info.html ├── post_header.html └── warning.html ├── _layouts └── default.html ├── _posts ├── 2017-02-28-explain.md ├── 2017-02-28-introduction.md ├── 2017-02-28-server-architecture.md ├── 2017-03-01-btrees.md ├── 2017-03-09-optimizer-trace.md ├── 2017-03-10-logical-transformation.md ├── 2017-03-14-cost-based-optimization.md ├── 2017-03-14-hints.md ├── 2017-03-18-comparing-plans.md ├── 2017-03-25-composite-indexes.md ├── 2017-04-08-covering-indexes.md ├── 2017-04-09-visual-explain.md ├── 2017-04-15-transient-plans.md ├── 2017-05-15-subqueries.md ├── 2017-05-21-ctes-and-views.md ├── 2017-06-12-joins.md ├── 2017-06-25-aggregation.md ├── 2017-07-01-sorting.md ├── 2017-07-03-partitioning.md ├── 2017-07-04-invisible-indexes.md ├── 2017-07-04-query-rewrite.md ├── 2017-07-08-profiling-queries.md ├── 2017-07-09-json-and-generaged-columns.md └── 2017-07-10-character-sets.md ├── _sass ├── fonts.scss ├── jekyll-theme-minimal.scss └── rouge-github.scss ├── css └── style.css └── index.md /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | .sass-cache/ 3 | .jekyll-metadata 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', group: :jekyll_plugins 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.2.7) 5 | i18n (~> 0.7) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.5.0) 11 | public_suffix (~> 2.0, >= 2.0.2) 12 | coffee-script (2.4.1) 13 | coffee-script-source 14 | execjs 15 | coffee-script-source (1.12.2) 16 | colorator (1.1.0) 17 | ethon (0.10.1) 18 | ffi (>= 1.3.0) 19 | execjs (2.7.0) 20 | faraday (0.11.0) 21 | multipart-post (>= 1.2, < 3) 22 | ffi (1.9.17) 23 | forwardable-extended (2.6.0) 24 | gemoji (3.0.0) 25 | github-pages (124) 26 | activesupport (= 4.2.7) 27 | github-pages-health-check (= 1.3.1) 28 | jekyll (= 3.3.1) 29 | jekyll-avatar (= 0.4.2) 30 | jekyll-coffeescript (= 1.0.1) 31 | jekyll-default-layout (= 0.1.4) 32 | jekyll-feed (= 0.9.1) 33 | jekyll-gist (= 1.4.0) 34 | jekyll-github-metadata (= 2.3.1) 35 | jekyll-mentions (= 1.2.0) 36 | jekyll-optional-front-matter (= 0.1.2) 37 | jekyll-paginate (= 1.1.0) 38 | jekyll-readme-index (= 0.0.4) 39 | jekyll-redirect-from (= 0.12.1) 40 | jekyll-relative-links (= 0.3.0) 41 | jekyll-sass-converter (= 1.5.0) 42 | jekyll-seo-tag (= 2.1.0) 43 | jekyll-sitemap (= 1.0.0) 44 | jekyll-swiss (= 0.4.0) 45 | jekyll-theme-architect (= 0.0.3) 46 | jekyll-theme-cayman (= 0.0.3) 47 | jekyll-theme-dinky (= 0.0.3) 48 | jekyll-theme-hacker (= 0.0.3) 49 | jekyll-theme-leap-day (= 0.0.3) 50 | jekyll-theme-merlot (= 0.0.3) 51 | jekyll-theme-midnight (= 0.0.3) 52 | jekyll-theme-minimal (= 0.0.3) 53 | jekyll-theme-modernist (= 0.0.3) 54 | jekyll-theme-primer (= 0.1.7) 55 | jekyll-theme-slate (= 0.0.3) 56 | jekyll-theme-tactile (= 0.0.3) 57 | jekyll-theme-time-machine (= 0.0.3) 58 | jekyll-titles-from-headings (= 0.1.4) 59 | jemoji (= 0.8.0) 60 | kramdown (= 1.13.2) 61 | liquid (= 3.0.6) 62 | listen (= 3.0.6) 63 | mercenary (~> 0.3) 64 | minima (= 2.0.0) 65 | nokogiri (= 1.6.8.1) 66 | rouge (= 1.11.1) 67 | terminal-table (~> 1.4) 68 | github-pages-health-check (1.3.1) 69 | addressable (~> 2.3) 70 | net-dns (~> 0.8) 71 | octokit (~> 4.0) 72 | public_suffix (~> 2.0) 73 | typhoeus (~> 0.7) 74 | html-pipeline (2.5.0) 75 | activesupport (>= 2) 76 | nokogiri (>= 1.4) 77 | i18n (0.8.1) 78 | jekyll (3.3.1) 79 | addressable (~> 2.4) 80 | colorator (~> 1.0) 81 | jekyll-sass-converter (~> 1.0) 82 | jekyll-watch (~> 1.1) 83 | kramdown (~> 1.3) 84 | liquid (~> 3.0) 85 | mercenary (~> 0.3.3) 86 | pathutil (~> 0.9) 87 | rouge (~> 1.7) 88 | safe_yaml (~> 1.0) 89 | jekyll-avatar (0.4.2) 90 | jekyll (~> 3.0) 91 | jekyll-coffeescript (1.0.1) 92 | coffee-script (~> 2.2) 93 | jekyll-default-layout (0.1.4) 94 | jekyll (~> 3.0) 95 | jekyll-feed (0.9.1) 96 | jekyll (~> 3.3) 97 | jekyll-gist (1.4.0) 98 | octokit (~> 4.2) 99 | jekyll-github-metadata (2.3.1) 100 | jekyll (~> 3.1) 101 | octokit (~> 4.0, != 4.4.0) 102 | jekyll-mentions (1.2.0) 103 | activesupport (~> 4.0) 104 | html-pipeline (~> 2.3) 105 | jekyll (~> 3.0) 106 | jekyll-optional-front-matter (0.1.2) 107 | jekyll (~> 3.0) 108 | jekyll-paginate (1.1.0) 109 | jekyll-readme-index (0.0.4) 110 | jekyll (~> 3.0) 111 | jekyll-redirect-from (0.12.1) 112 | jekyll (~> 3.3) 113 | jekyll-relative-links (0.3.0) 114 | jekyll (~> 3.3) 115 | jekyll-sass-converter (1.5.0) 116 | sass (~> 3.4) 117 | jekyll-seo-tag (2.1.0) 118 | jekyll (~> 3.3) 119 | jekyll-sitemap (1.0.0) 120 | jekyll (~> 3.3) 121 | jekyll-swiss (0.4.0) 122 | jekyll-theme-architect (0.0.3) 123 | jekyll (~> 3.3) 124 | jekyll-theme-cayman (0.0.3) 125 | jekyll (~> 3.3) 126 | jekyll-theme-dinky (0.0.3) 127 | jekyll (~> 3.3) 128 | jekyll-theme-hacker (0.0.3) 129 | jekyll (~> 3.3) 130 | jekyll-theme-leap-day (0.0.3) 131 | jekyll (~> 3.3) 132 | jekyll-theme-merlot (0.0.3) 133 | jekyll (~> 3.3) 134 | jekyll-theme-midnight (0.0.3) 135 | jekyll (~> 3.3) 136 | jekyll-theme-minimal (0.0.3) 137 | jekyll (~> 3.3) 138 | jekyll-theme-modernist (0.0.3) 139 | jekyll (~> 3.3) 140 | jekyll-theme-primer (0.1.7) 141 | jekyll (~> 3.3) 142 | jekyll-theme-slate (0.0.3) 143 | jekyll (~> 3.3) 144 | jekyll-theme-tactile (0.0.3) 145 | jekyll (~> 3.3) 146 | jekyll-theme-time-machine (0.0.3) 147 | jekyll (~> 3.3) 148 | jekyll-titles-from-headings (0.1.4) 149 | jekyll (~> 3.3) 150 | jekyll-watch (1.5.0) 151 | listen (~> 3.0, < 3.1) 152 | jemoji (0.8.0) 153 | activesupport (~> 4.0) 154 | gemoji (~> 3.0) 155 | html-pipeline (~> 2.2) 156 | jekyll (>= 3.0) 157 | json (1.8.6) 158 | kramdown (1.13.2) 159 | liquid (3.0.6) 160 | listen (3.0.6) 161 | rb-fsevent (>= 0.9.3) 162 | rb-inotify (>= 0.9.7) 163 | mercenary (0.3.6) 164 | mini_portile2 (2.1.0) 165 | minima (2.0.0) 166 | minitest (5.10.1) 167 | multipart-post (2.0.0) 168 | net-dns (0.8.0) 169 | nokogiri (1.6.8.1) 170 | mini_portile2 (~> 2.1.0) 171 | octokit (4.6.2) 172 | sawyer (~> 0.8.0, >= 0.5.3) 173 | pathutil (0.14.0) 174 | forwardable-extended (~> 2.6) 175 | public_suffix (2.0.5) 176 | rb-fsevent (0.9.8) 177 | rb-inotify (0.9.8) 178 | ffi (>= 0.5.0) 179 | rouge (1.11.1) 180 | safe_yaml (1.0.4) 181 | sass (3.4.23) 182 | sawyer (0.8.1) 183 | addressable (>= 2.3.5, < 2.6) 184 | faraday (~> 0.8, < 1.0) 185 | terminal-table (1.7.3) 186 | unicode-display_width (~> 1.1.1) 187 | thread_safe (0.3.6) 188 | typhoeus (0.8.0) 189 | ethon (>= 0.8.0) 190 | tzinfo (1.2.2) 191 | thread_safe (~> 0.1) 192 | unicode-display_width (1.1.3) 193 | 194 | PLATFORMS 195 | ruby 196 | 197 | DEPENDENCIES 198 | github-pages 199 | 200 | BUNDLED WITH 201 | 1.10.6 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 本プロジェクトについて 2 | 3 | 本プロジェクトはMorgan Tockerさんの 4 | 5 | http://www.unofficialmysqlguide.com/ 6 | 7 | を日本語化するためのプロジェクトです。 8 | 9 | # 参加者募集中! 10 | 11 | ご協力いただける方、募集しております。 12 | 参加方法は下記をご覧ください。 13 | 14 | https://github.com/yakst/unofficialmysqlguide-ja/wiki 15 | 16 | 参加希望の方、不明なことある場合は下記slackチャンネルがありますのでご参加ください! 17 | slack参加については下記サイトの翻訳者にtwitterなどでご連絡ください 18 | 19 | https://yakst.com/ja 20 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | title: 非公式MySQL 8.0オプティマイザガイド 3 | permalink: :slug.html 4 | baseurl: /unofficialmysqlguide-ja 5 | -------------------------------------------------------------------------------- /_includes/disqus.html: -------------------------------------------------------------------------------- 1 |
2 | 20 | -------------------------------------------------------------------------------- /_includes/info.html: -------------------------------------------------------------------------------- 1 |
2 |
Tips
3 |
{{ include.info }}
4 |
-------------------------------------------------------------------------------- /_includes/post_header.html: -------------------------------------------------------------------------------- 1 |

{{ page.title }}

2 | 原文URL: {{ page.original_url }}
3 | 翻訳者: {{ page.translator }} 4 | -------------------------------------------------------------------------------- /_includes/warning.html: -------------------------------------------------------------------------------- 1 |
2 |
Warning
3 |
{{ include.warn }}
4 |
-------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ site.title | default: site.github.repository_name }} by {{ site.github.owner_name }} 7 | 8 | 9 | 10 | 13 | 14 | 15 |
16 |
17 |

{{ site.title | default: site.github.repository_name }}

18 | 19 | {% if site.github.is_project_page %} 20 |

View the Project on GitHub {{ github_name }}

21 | {% endif %} 22 | 23 | {% if site.github.is_user_page %} 24 |

View My GitHub Profile

25 | {% endif %} 26 | 27 | {% if site.show_downloads %} 28 | 33 | {% endif %} 34 |
    35 | {% assign sorted_posts = site.posts | sort: 'article_index' %} 36 | {% for post in sorted_posts %} 37 |
  1. {{ post.title }}
  2. 38 | {% endfor %} 39 |
40 |
41 |
42 | {% include post_header.html %} 43 |
44 | 45 | {{ content }} 46 | 47 |
48 | {% include disqus.html %} 49 |
50 | 56 |
57 | 58 | 59 | 60 | {% if site.google_analytics %} 61 | 70 | {% endif %} 71 | 72 | 73 | -------------------------------------------------------------------------------- /_posts/2017-02-28-explain.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Explain 3 | date: 2017-02-28 00:00:04 -0900 4 | article_index: 4 5 | original_url: http://www.unofficialmysqlguide.com/explain.html 6 | translator: doublemarket 7 | --- 8 | 9 | `EXPLAIN`は、与えられたクエリーを実行するためにどのように「計画を立てたか」を表示します。これはつまり、実行前のビューであり、クエリーをプロファイリングするのと混同しないようにしてください。 10 | 11 | `EXPLAIN`は、場合によっては何千通りにもなりうるクエリーの実行方法を評価するプロセスの後で、「最適な計画」と考えられるものを表示します。他の計画がどのようなものだったかは`"possible_keys"`に簡単な情報がありますが、一般的にはこの情報を得るには`OPTIMIZER_TRACE`を確認する必要があります。 12 | 13 | 常に`EXPLAIN FORMAT=JSON`を使うことをおすすめします。これは、(デフォルトのフォーマットでは表示されない)クエリーコストを出力するからです。「どの程度の影響があるか」で考えて話ができるという点で、コストは重要です。ここで私が言いたいのは、データベースの専門家が「一時テーブルを作るのはダメ」「結合はダメ」と言っているのを聞いたことがあるということです。これらのコメントは、重要な背景情報がないので、直接回答するのは困難です。水だって十分な量があれば悪いとも言えます。 14 | 15 | 実行する可能性のあるクエリーに`EXPLAIN FORMAT=JSON`をつけて実行してみるのと同じように、実行中のコネクションに対して`EXPLAIN FORMAT=JSON FOR CONNECTION <コネクションID>`を実行して、実行時にどのような最適化がされているかを確認することもできます。これは、データ(とそれを元にした統計情報)に対する変更が計画の選択に影響を及ぼすにつれて起きる一時的なエラーを診断するのに役立つことがあります。 16 | 17 | ### 例1) テーブルスキャンの処理を表示するEXPLAIN 18 | 19 | ```sql 20 | EXPLAIN FORMAT=JSON 21 | SELECT * FROM Country WHERE continent='Asia' and population > 5000000; 22 | { 23 | "query_block": { 24 | "select_id": 1, 25 | "cost_info": { 26 | "query_cost": "53.80" # このクエリは53.80コストユニットかかる 27 | }, 28 | "table": { 29 | "table_name": "Country", 30 | "access_type": "ALL", # ALLはテーブルスキャンの意味 31 | "rows_examined_per_scan": 239, # テーブル内の239行全てにアクセス 32 | "rows_produced_per_join": 11, 33 | "filtered": "4.76", 34 | "cost_info": { 35 | "read_cost": "51.52", 36 | "eval_cost": "2.28", 37 | "prefix_cost": "53.80", 38 | "data_read_per_join": "2K" 39 | }, 40 | "used_columns": [ 41 | "Code", 42 | "Name", 43 | "Continent", 44 | "Region", 45 | "SurfaceArea", 46 | "IndepYear", 47 | "Population", 48 | "LifeExpectancy", 49 | "GNP", 50 | "GNPOld", 51 | "LocalName", 52 | "GovernmentForm", 53 | "HeadOfState", 54 | "Capital", 55 | "Code2" 56 | ], 57 | "attached_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000))" 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | 例1では`EXPLAIN`は、53.80コストユニットのコストをかけてテーブルスキャン(`access_type ALL`)を行ってクエリーを実行する予定であることを表しています。出力に`possible_keys`の項目がないことから(通常は`access_type`の下にあります)、使用できる可能性のあるインデックスはありません。 64 | 65 | `rows_examined_per_scan`の数は、世界に存在する国の数とほぼ一致していて、テーブルにある行数(239)と同じです。これはあくまで見積もりであって、実際にテーブルスキャンすることはパフォーマンスに影響を及ぼすので、100%正しいとは限りません。 66 | 67 | `attached_condition`は、行 ``((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000))`` を読み込む時に適用されるフィルターを表しています。インデックスがあれば、行を読み込む前にこの`attached_conditions`が充足される可能性もありますが、ここに表示があるということは、そうではないということになります。 68 | 69 | 私は、`rows_examined_per_scan`と、クエリーが実行される際にクライアントに送信される行数を比較するのが好きです[^1]。送信される行はクエリーを実行するまで分かりませんが、実行してみると、その数が32であることが分かります。つまり、このクエリーはクライアントに送信される7.5倍の行数を確認しているということであり、最適化の可能性を示唆しています。 70 | 71 | 注意として、全部で239行というのは大きなものではなく、全行がメモリーに収まるだろうということを言っておくのは重要でしょう。しかし、テーブルスキャンはメモリーに収まることを強く要求する性質があり、テーブルが大きくなるにつれてパフォーマンスの状況は「崖を転げ落ちるように」悪くなるでしょう。インデックスを追加すれば、パフォーマンスも良くなりますし、データの増大に対してパフォーマンスを一定に保てます。 72 | 73 | [^1]: この情報は`performance_schema.events_statements_history_long`にもあります。 74 | [^2]: 非常に小さな比率(1:1を含む)の場合でも、最適化が不可能であるということではありません。一致検索にも、パーティショニングやページサイズの変更といった最適化の可能性が残っています。 75 | -------------------------------------------------------------------------------- /_posts/2017-02-28-introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: はじめに 3 | date: 2017-02-28 00:00:01 -0900 4 | article_index: 1 5 | original_url: http://www.unofficialmysqlguide.com/introduction.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | ![オプティマイザの概要](http://www.unofficialmysqlguide.com/_images/optimizer-overview.png) 10 | 11 | クエリーオプティマイザはクエリーを「入力」とみなし、この章に記述したプロセスを経て実行計画を「出力」として生成します。私はよく、クエリー最適化をGPSのナビゲーションと対比して例えます。 12 | 13 | ## 1. 目的地として住所を入力します 14 | 15 | * メインストリート 3294番 16 | 17 | ## 2. どのように進むと一番効率的に目的地にたどり着けるかを、教えてくれます 18 | 19 | * 2マイル直進 20 | * マーカムストリート(Markham Street)で左折 21 | * 500フィート直進 22 | * 右折 23 | * 1000フィート直進 24 | * 右側に目的地あり 25 | 26 | 27 | 住所は目的地です。どのように行くかを指定しなくても、ナビゲーションシステムが考えうる経路について評価し、一番効率的に目的地に到着する経路をアドバイスしてくれることでしょう。 28 | 29 | SQL言語は「宣言的」であるという点で、住所とにています。どのようにするかという過程ではなく、最終的な状態を指定します。データベースシステムには、多くのインデックス(そして多くのテーブルとの結合)があるため、ナビゲーションシステムと同じように、同じ結果をえられる多くの方法があります。 30 | 31 | このアナロジーのしめくくりとして、GPSのナビゲーションが「常に絶対に一番速い道を案内してくれる」わけではないのと同じことが、クエリー最適化についてもいえます。全ての道に交通量のデータがあるとは限らないのと同様に、オプティマイザも不完全なモデルに対して処理をする必要があります。熟練した方にとっては、チューニングする必要が生じる状況が発生しえるわけです。 32 | -------------------------------------------------------------------------------- /_posts/2017-02-28-server-architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: サーバーアーキテクチャー 3 | date: 2017-02-28 00:00:02 -0900 4 | article_index: 2 5 | original_url: http://www.unofficialmysqlguide.com/server-architecture.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | 大まかにいうと、MySQLサーバーは2つの部分から構成されています。「サーバー」[^1]と「ストレージエンジン」です。クエリー最適化はサーバーレベル、そしてストレージエンジンAPI上で実行され、意味的には4つのステップに分けられます。 10 | 11 | * 論理変換 12 | * コストベース最適化の準備 13 | * コストベース最適化 14 | * プランの改善 15 | 16 | ![オプティマイザの役割](http://www.unofficialmysqlguide.com/_images/server-architecture.png) 17 | 18 | オプティマイザを定義されたレイヤーに分割するのは進化的プロセスでした。初期のMySQLでは、今のようにクエリー最適化はされてませんでしたし、性能上の理由からクエリー最適化のステップはうまく分離されていませんでしたし、また、定義もされていませんでした。 19 | 20 | 保守性を向上させた新機能を実現するには、MySQL 5.6, 5.7, そして8.0で実装されてきたリファクタリングが必要でした。ステップ化したことによって、新機能への道もまた開けました。例えば、実行計画のキャッシュに関しては、ステップを明確に区分しなければコネクション間の再利用が難しいため、実現が難しくなります。 21 | 22 | [^1]: "サーバー"という単語はあいまいでサーバーとストレージエンジンをあわせた上位集合としても使われることにご注意ください。 23 | -------------------------------------------------------------------------------- /_posts/2017-03-01-btrees.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: B+ツリー インデックス 3 | date: 2017-03-01 00:00:02 -0900 4 | article_index: 3 5 | original_url: http://www.unofficialmysqlguide.com/btrees.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | 「インデックスを追加する」といったら、それが主キーであれ、ユニークキーであれ、あるいはそうでなかったとしても、通常は「B+ツリーインデックスの追加」となります(他のインデックス種別も利用できます)。B+ツリーをよく理解することで、個々のクエリーのパフォーマンスを向上させられるだけでなく、メモリー上に必要なデータ量(ワーキングセット)を削減できます。これは結果的にデータベースの「全体的なスケーラビリティー」を向上させるのに役立ちます。 10 | 11 | B+ツリーの仕組みを説明するのに、Bツリーの仕組みの説明から始め、それから両者は何が違うのかを説明したいと思います。 12 | 13 | ![Bツリーの仕組み](http://www.unofficialmysqlguide.com/_images/binary-tree.png) 14 | 15 | Bツリーの主な特性は「二分探索」にあります。つまり、「100万」の(平衡)Bツリーから829813を探すときに最大でも20ホップで見つけられるのです。これは100万のリストをスキャンすることと比べると非常に大きな進歩です。 16 | 17 | ![Bツリーの探索パス](http://www.unofficialmysqlguide.com/_images/binary-search.png) 18 | 19 | 20ホップがそのまま20回のディスクアクセスを発生させる場合、少ない回数とはいえなくなり、この点からBツリーは性能上劣っています。小規模な複数のIOアクセスは比較的高コスト[^1]なのです。これに比べ、同じ検索をB+ツリー[^2]でおこなうと**わずか2ホップ**でよくなります。 20 | 21 | ![B+ツリー検索](http://www.unofficialmysqlguide.com/_images/btree.png) 22 | 23 | 上記の図ではルートインデックスページは25のインデックスページへのポインタを保持しており、このインデックスページ自体は平均で1538のリーフページヘのポインタをもっていることを示しています。829813への経路は図上でハイライトされておりこれは次のとおりです。 24 | 25 | * ルートページにて: 値は800788以上、829908より小さいので16386のページへ 26 | * ページ16386にて: 値は829804以上、829830より小さいのでリーフページ32012へ 27 | 28 | つまり、B+ツリーはBツリーに比べて2点拡張されているわけです。 29 | 30 | 1. **ページに至るまでのデータの組織化** : これはストレージからデータを読み書きするときの基本単位です。データベース用語としての「組織化」は**クラスタリング**とも呼ばれます(データベースクラスターとは異なります) 31 | 2. **ツリー自体が広く、そして深くない** : それぞれのインデックスページに1000強のキーを保存でき、これらが1000のキーをもつインデックスページのポインタをもてば、B+ツリーでリーフページ(ここの主キーに対して全ての行データをもつ)に達するまでに3ホップ以上必要なケースはあまりないでしょう。B+ツリーはBツリーに比べ遥かに高速に広がるためこのようになります。 32 | 33 | ルートページ、および25の内部インデックスページを加えても400KiBのメモリーしか必要としません(各ページはそれぞれ16KiB)。38463個のリーフページは600MiBの容量を必要としますが全てが同時にメモリ上にある必要があるわけではありません。大多数のデータセットは一部が「ホットな」ページで他のページは操作されません。内部的にInnoDBはページアクセスをLRUアルゴリズムのバリエーション[^3]を利用して追跡し、空きを作る必要が発生すればアクセス頻度の低いページをメモリ上から追い出します。 34 | 35 | 一般的には、データベースの総データサイズをRAMよりはるかに大きくすることができます。我々は、メモリー上に保持しなければならないデータのことを「ワーキングセット」と呼んでいます。ワーキングセットの要件はデータセット毎に異なり、データベース管理者のゴールはこれを減らすことのできる最適化を見つけ出すこととなります。 36 | 37 | ほとんどの場合で、300GBのワーキングセットを必要とする500GBのデータベースは、100GBのワーキングセットを必要とする1TBのデータベースより、はるかに最適化が難しくなります。 38 | 39 | [^1]: 通常、4Kの読み込みと500Bの読み込みは同じコストがかかります。B+ツリーはIOコストをうまく分割します (これはSSDの場合でも真です) 40 | [^2]: InnoDBのB+ツリーで分析される実際の値の表示は https://github.com/jeremycole/innodb_ruby で実施 41 | [^3]: https://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool.html#innodb-buffer-pool-lru 42 | -------------------------------------------------------------------------------- /_posts/2017-03-09-optimizer-trace.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: オプティマイザ トレース 3 | date: 2017-03-09 00:00:01 -0900 4 | article_index: 5 5 | original_url: http://www.unofficialmysqlguide.com/optimizer-trace.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | `EXPLAIN`はクエリーの実行計画以外は表示しません。すなわち、なぜ他の代替となる実行戦略が選ばれなかったのか、については表示しません。これを理解するためには次の観点を考える必要がありややこしいものです。 10 | 11 | * 選ばれなかった実行計画は適切でなかったのか?(例えば、ある最適化は特定のユースケースのみに利用できる、など) 12 | * 選ばれなかった実行計画の方が高コストと評価されたのか? 13 | * 高コストだとすると、どの程度か? 14 | 15 | `OPTIMIZER_TRACE` はこれらの疑問に答えます。オプティマイザ トレースは、オプティマイザのより詳細なデータを表示するように作られており、 16 | オプティマイザのコストモデルがどのように動作するかを学ぶことはもとより、日々のトラブルシューティングにもとても役に立ちます。 17 | 18 | ### 例2) EXPLAINにより新しく追加されたインデックスが使われていないことがわかる 19 | 20 | ```sql 21 | ALTER TABLE Country ADD INDEX p (population); 22 | EXPLAIN FORMAT=JSON 23 | SELECT * FROM Country WHERE continent='Asia' and population > 5000000; 24 | 25 | { 26 | "query_block": { 27 | "select_id": 1, 28 | "cost_info": { 29 | "query_cost": "53.80" 30 | }, 31 | "table": { 32 | "table_name": "Country", 33 | "access_type": "ALL", # このクエリーはテーブルスキャンとして実行されている 34 | "possible_keys": [ # オプティマイザは 35 | "p" # インデックスが利用可能と判断しているのに! 36 | ], 37 | "rows_examined_per_scan": 239, 38 | "rows_produced_per_join": 15, 39 | "filtered": "6.46", 40 | "cost_info": { 41 | "read_cost": "50.71", 42 | "eval_cost": "3.09", 43 | "prefix_cost": "53.80", 44 | "data_read_per_join": "3K" 45 | }, 46 | "used_columns": [ 47 | "Code", 48 | "Name", 49 | "Continent", 50 | "Region", 51 | "SurfaceArea", 52 | "IndepYear", 53 | "Population", 54 | "LifeExpectancy", 55 | "GNP", 56 | "GNPOld", 57 | "LocalName", 58 | "GovernmentForm", 59 | "HeadOfState", 60 | "Capital", 61 | "Code2" 62 | ], 63 | "attached_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000))" 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | 例2では、インデックス`p(population)`がテーブルに追加された後であるにもかかわらず、選択されていないことがわかります。`EXPLAIN`からこれが候補であったことはわかりますが、なぜ選択されなかったかはわかりません。この理由を知るためには、`OPTIMIZER_TRACE`を利用する必要があります。 70 | 71 | ### 例3: OPTIMIZER_TRACEによりインデックスが利用されなかった理由がわかる 72 | 73 | ```sql 74 | SET optimizer_trace="enabled=on" 75 | SELECT * FROM Country WHERE continent='Asia' and population > 5000000; 76 | SELECT * FROM information_schema.optimizer_trace; 77 | { 78 | "steps": [ 79 | { 80 | "join_preparation": { 81 | "select#": 1, 82 | "steps": [ 83 | { 84 | "expanded_query": "/* select#1 */ select `Country`.`Code` AS `Code`,`Country`.`Name` AS `Name`,`Country`.`Continent` AS `Continent`,`Country`.`Region` AS `Region`,`Country`.`SurfaceArea` AS `SurfaceArea`,`Country`.`IndepYear` AS `IndepYear`,`Country`.`Population` AS `Population`,`Country`.`LifeExpectancy` AS `LifeExpectancy`,`Country`.`GNP` AS `GNP`,`Country`.`GNPOld` AS `GNPOld`,`Country`.`LocalName` AS `LocalName`,`Country`.`GovernmentForm` AS `GovernmentForm`,`Country`.`HeadOfState` AS `HeadOfState`,`Country`.`Capital` AS `Capital`,`Country`.`Code2` AS `Code2` from `Country` where ((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 5000000))" 85 | } 86 | ] 87 | } 88 | }, 89 | { 90 | "join_optimization": { 91 | "select#": 1, 92 | "steps": [ 93 | { 94 | "condition_processing": { 95 | "condition": "WHERE", 96 | "original_condition": "((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 5000000))", 97 | "steps": [ 98 | { 99 | "transformation": "equality_propagation", 100 | "resulting_condition": "((`Country`.`Population` > 5000000) and multiple equal('Asia', `Country`.`Continent`))" 101 | }, 102 | { 103 | "transformation": "constant_propagation", 104 | "resulting_condition": "((`Country`.`Population` > 5000000) and multiple equal('Asia', `Country`.`Continent`))" 105 | }, 106 | { 107 | "transformation": "trivial_condition_removal", 108 | "resulting_condition": "((`Country`.`Population` > 5000000) and multiple equal('Asia', `Country`.`Continent`))" 109 | } 110 | ] 111 | } 112 | }, 113 | { 114 | "substitute_generated_columns": {} 115 | }, 116 | { 117 | "table_dependencies": [ 118 | { 119 | "table": "`Country`", 120 | "row_may_be_null": false, 121 | "map_bit": 0, 122 | "depends_on_map_bits": [] 123 | } 124 | ] 125 | }, 126 | { 127 | "ref_optimizer_key_uses": [] 128 | }, 129 | { 130 | "rows_estimation": [ 131 | { 132 | "table": "`Country`", 133 | "range_analysis": { 134 | "table_scan": { 135 | "rows": 239, 136 | "cost": 55.9 137 | }, 138 | "potential_range_indexes": [ 139 | { 140 | "index": "PRIMARY", 141 | "usable": false, 142 | "cause": "not_applicable" 143 | }, 144 | { 145 | "index": "p", 146 | "usable": true, 147 | "key_parts": [ 148 | "Population", 149 | "Code" 150 | ] 151 | } 152 | ], 153 | "setup_range_conditions": [], 154 | "group_index_range": { 155 | "chosen": false, 156 | "cause": "not_group_by_or_distinct" 157 | }, 158 | "analyzing_range_alternatives": { 159 | "range_scan_alternatives": [ 160 | { 161 | "index": "p", 162 | "ranges": [ 163 | "5000000 < Population" 164 | ], 165 | "index_dives_for_eq_ranges": true, 166 | "rowid_ordered": false, 167 | "using_mrr": false, 168 | "index_only": false, 169 | "rows": 108, 170 | "cost": 130.61, # これはインデックスを利用した場合のコスト。 171 | "chosen": false, # この実行計画は選択されなかった。なぜならば、 172 | "cause": "cost" # テーブルスキャンより高コストであるから 173 | } 174 | ], 175 | "analyzing_roworder_intersect": { 176 | "usable": false, 177 | "cause": "too_few_roworder_scans" 178 | } 179 | } 180 | } 181 | } 182 | ] 183 | }, 184 | { 185 | "considered_execution_plans": [ 186 | { 187 | "plan_prefix": [], 188 | "table": "`Country`", 189 | "best_access_path": { 190 | "considered_access_paths": [ 191 | { 192 | "rows_to_scan": 239, 193 | "access_type": "scan", 194 | "resulting_rows": 239, 195 | "cost": 53.8, 196 | "chosen": true 197 | } 198 | ] 199 | }, 200 | "condition_filtering_pct": 100, 201 | "rows_for_plan": 239, 202 | "cost_for_plan": 53.8, 203 | "chosen": true 204 | } 205 | ] 206 | }, 207 | { 208 | "attaching_conditions_to_tables": { 209 | "original_condition": "((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 5000000))", 210 | "attached_conditions_computation": [], 211 | "attached_conditions_summary": [ 212 | { 213 | "table": "`Country`", 214 | "attached": "((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 5000000))" 215 | } 216 | ] 217 | } 218 | }, 219 | { 220 | "refine_plan": [ 221 | { 222 | "table": "`Country`" 223 | } 224 | ] 225 | } 226 | ] 227 | } 228 | }, 229 | { 230 | "join_execution": { 231 | "select#": 1, 232 | "steps": [] 233 | } 234 | } 235 | ] 236 | } 237 | ``` 238 | 239 | 240 | 例3の`range_scan_alternatives` では`p(population)`は考慮されている一方で、コストに基いて選択肢から除外(`"chosen": false and "cause": "cost"`)されたことを示しています。出力にはインデックスを利用する場合のコスト見積もりも表示されており、130.61コストユニットとあります。これがテーブルスキャンの場合の55.9コストユニットと比較されます。値は小さいほうがよいため、テーブルスキャンの方が良いわけです。 241 | 242 | なぜこのようになるかを説明するには、インデックスにはやることが多く、これを減らす必要があることを理解する必要があります。このデータセットの大多数の国の人口は500万以上となります。オプティマイザがインデックスとデータを行ったりきたりするより、テーブルスキャンしたほうが速いと判断したことになります。上級ユーザーの方々は、この意思決定を行うためのコストを調整することができます。 243 | 244 | ![選択されないインデックス](http://www.unofficialmysqlguide.com/_images/non-selective-index.png) 245 | -------------------------------------------------------------------------------- /_posts/2017-03-10-logical-transformation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 論理変換 3 | date: 2017-03-10 00:00:01 -0900 4 | article_index: 6 5 | original_url: http://www.unofficialmysqlguide.com/logical-transformations.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQLのオプティマイザは実行結果に影響を与えないように、クエリーを変換することがあります。余分な操作を取り除き、クエリーをより速く実行できるようクエリーを書き換えることがこの「変換」のゴールです。例えば、次のクエリーについて考えてみましょう。 10 | 11 | ```sql 12 | SELECT * FROM Country 13 | WHERE population > 5000000 AND continent='Asia' AND 1=1; 14 | ``` 15 | 16 | 「1は常に1」ですので、この条件と`AND`(`OR`ではありません)は完全に冗長です。 17 | クエリーを実行中に各行に対して「1が1のままである」ことを確認することには何にも意味がありませんし、条件を削除しても同じ結果がえられます。`OPTIMIZER_TRACE`を利用すると、MySQLがこの「変換」およびその他の数々の変換を実施していることを確認できます。 18 | 19 | ``` 20 | .. 21 | { 22 | "join_optimization": { 23 | "select#": 1, 24 | "steps": [ 25 | { 26 | "condition_processing": { 27 | "condition": "WHERE", 28 | "original_condition": "((`Country`.`Population` > 5000000) and (1 = 1))", 29 | "steps": [ 30 | { 31 | "transformation": "equality_propagation", 32 | "resulting_condition": "((`Country`.`Population` > 5000000) and (1 = 1))" 33 | }, 34 | { 35 | "transformation": "constant_propagation", 36 | "resulting_condition": "((`Country`.`Population` > 5000000) and (1 = 1))" 37 | }, 38 | { 39 | "transformation": "trivial_condition_removal", 40 | "resulting_condition": "(`Country`.`Population` > 5000000)" 41 | } 42 | ] 43 | } 44 | }, 45 | .. 46 | ``` 47 | 48 | すべての変換が終わったあとの書き換えられたクエリーは、`EXPLAIN`が実行されたあとの`SHOW WARNINGS`でも確認できます。上記のステートメントは次のように書き換えられます。 49 | 50 | ```sql 51 | EXPLAIN FORMAT=JSON SELECT * FROM Country WHERE population > 5000000 AND 1=1; 52 | SHOW WARNINGS; 53 | /* select#1 */ select `world`.`Country`.`Code` AS `Code`,`world`.`Country`.`Name` AS `Name`,`world`.`Country`.`Continent` AS `Continent`,`world`.`Country`.`Region` AS `Region`,`world`.`Country`.`SurfaceArea` AS `SurfaceArea`,`world`.`Country`.`IndepYear` AS `IndepYear`,`world`.`Country`.`Population` AS `Population`,`world`.`Country`.`LifeExpectancy` AS `LifeExpectancy`,`world`.`Country`.`GNP` AS `GNP`,`world`.`Country`.`GNPOld` AS `GNPOld`,`world`.`Country`.`LocalName` AS `LocalName`,`world`.`Country`.`GovernmentForm` AS `GovernmentForm`,`world`.`Country`.`HeadOfState` AS `HeadOfState`,`world`.`Country`.`Capital` AS `Capital`,`world`.`Country`.`Code2` AS `Code2` from `world`.`Country` where (`world`.`Country`.`Population` > 5000000) 54 | ``` 55 | 56 | ## 変換の例 57 | 58 | 下記に論理変換されたクエリーの例をいくつか示します。いくつかの例で`PRIMARY`および`UNIQUE`インデックスがクエリーの実行フェーズの前に永続的に変換されていることに注意してください。クエリーのこの部分は既に最も効率的な形式となっているため、オプティマイザは実行計画を考慮する前に永続的な変換を行っています。 59 | ただし、考慮する実行計画の数を減らすための「他の最適化」は引き続き適用されます。 60 | 61 | ### Codeが主キー。クエリーは定数値に変換された値に書き換えられる 62 | 63 | #### 元のクエリー 64 | 65 | ```sql 66 | SELECT * FROM Country WHERE code='CAN' 67 | ``` 68 | 69 | #### 書き換え後のクエリー 70 | 71 | ```sql 72 | /* select#1 */ select 'CAN' AS `Code`,'Canada' AS `Name`,'North America' 73 | AS `Continent`, 'North America' AS `Region`,'9970610.00' AS 74 | `SurfaceArea`,'1867' AS `IndepYear`, '31147000' AS `Population`,'79.4' AS 75 | `LifeExpectancy`,'598862.00' AS `GNP`,'625626.00' AS `GNPOld`, 'Canada' AS 76 | `LocalName`,'Constitutional Monarchy, Federation' AS `GovernmentForm`, 77 | 'Elisabeth II' AS `HeadOfState`,'1822' AS `Capital`,'CA' AS `Code2` from 78 | `world`.`Country` where 1 79 | ``` 80 | 81 | ### Codeは主キーだが、主キーがテーブルにない場合(不可能なwhere) 82 | 83 | #### 元のクエリー 84 | 85 | ```sql 86 | SELECT * FROM Country WHERE code='XYZ' 87 | ``` 88 | 89 | #### 書き換え後のクエリー 90 | 91 | ```sql 92 | /* select#1 */ select NULL AS `Code`,NULL AS `Name`,NULL AS 93 | `Continent`,NULL AS `Region`, NULL AS `SurfaceArea`,NULL AS 94 | `IndepYear`,NULL AS `Population`,NULL AS `LifeExpectancy`,NULL AS `GNP`, 95 | NULL AS `GNPOld`,NULL AS `LocalName`,NULL AS `GovernmentForm`,NULL AS 96 | `HeadOfState`,NULL AS `Capital`, NULL AS `Code2` from `world`.`Country` 97 | where multiple equal('XYZ', NULL) 98 | ``` 99 | 100 | ### 不可能なwhereの他の例。Codeは存在するが1=0が不可能な条件 101 | 102 | 103 | #### 元のクエリー 104 | 105 | ```sql 106 | SELECT * FROM Country WHERE code='CAN' AND 1=0 107 | ``` 108 | 109 | #### 書き換え後のクエリー 110 | 111 | ```sql 112 | /* select#1 */ select `world`.`Country`.`Code` AS `Code`, 113 | `world`.`Country`.`Name` AS `Name`, `world`.`Country`.`Continent` 114 | AS `Continent`,`world`.`Country`.`Region` AS `Region`, 115 | `world`.`Country`.`SurfaceArea` AS  116 | `SurfaceArea`,`world`.`Country`.`IndepYear` AS `IndepYear`, 117 | `world`.`Country`.`Population` AS 118 | `Population`,`world`.`Country`.`LifeExpectancy` AS `LifeExpectancy`, 119 | `world`.`Country`.`GNP` AS `GNP`,`world`.`Country`.`GNPOld` AS `GNPOld`, 120 | `world`.`Country`.`LocalName` AS 121 | `LocalName`,`world`.`Country`.`GovernmentForm` AS `GovernmentForm`, 122 | `world`.`Country`.`HeadOfState` AS 123 | `HeadOfState`,`world`.`Country`.`Capital` AS `Capital`, 124 | `world`.`Country`.`Code2` AS `Code2` from `world`.`Country` where 0 125 | ``` 126 | 127 | ### 派生テーブルのサブクエリーがCountryテーブルのjoinに直接マージされる 128 | 129 | #### 元のクエリー 130 | 131 | ```sql 132 | SELECT City.* FROM City, (SELECT * FROM Country WHERE continent='Asia') as 133 | Country WHERE Country.code=City.CountryCode AND Country.population > 134 | 5000000; 135 | ``` 136 | 137 | #### 書き換え後のクエリー[^1] 138 | 139 | ```sql 140 | /* select#1 */ select `world`.`City`.`ID` AS `ID`,`world`.`City`.`Name` AS 141 | `Name`, `world`.`City`.`CountryCode` AS 142 | `CountryCode`,`world`.`City`.`District` AS `District`, 143 | `world`.`City`.`Population` AS `Population` from `world`.`City` join 144 | `world`.`Country` where ((`world`.`Country`.`Continent` = 'Asia') and 145 | (`world`.`City`.`CountryCode` = ` world`.`Country`.`Code`) and 146 | (`world`.`Country`.`Population` > 5000000)) 147 | ``` 148 | 149 | ### ビューの定義がクエリーで利用されるとともにマージされる 150 | 151 | #### 元のクエリー 152 | 153 | ```sql 154 | CREATE VIEW Countries_in_asia AS SELECT * FROM Country WHERE 155 | continent='Asia'; SELECT * FROM Countries_in_asia WHERE population > 156 | 5000000; 157 | ``` 158 | 159 | #### 書き換え後のクエリー 160 | 161 | ```sql 162 | /* select#1 */ select `world`.`Country`.`Code` AS 163 | `Code`,`wo`rld`.`Country`.`Name` AS `Name`, `world`.`Country`.`Continent` 164 | AS `Continent`,`world`.`Country`.`Region` AS `Region`, 165 | `world`.`Country`.`SurfaceArea` AS `SurfaceArea`,`world`.`Country`.`IndepYear` 166 | AS `IndepYear`, `world`.`Country`.`Population` AS 167 | `Population`,`world`.`Country`.`LifeExpectancy` AS `LifeExpectancy`, `world`.`Country`.`GNP` AS `GNP`,`world`.`Country`.`GNPOld` AS `GNPOld`, 168 | `world`.`Country`.`LocalName` AS 169 | `LocalName`,`world`.`Country`.`GovernmentForm` AS `GovernmentForm`, 170 | `world`.`Country`.`HeadOfState` AS `HeadOfState`,`world`.`Country`.`Capital` AS `Capital`, `world`.`Country`.`Code2` AS `Code2` from `world`.`Country` where ((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000)) 171 | ``` 172 | 173 | [^1]: この挙動は`optimizer_switch`の`derived_merge`(デフォルトではon)の設定に依存します 174 | -------------------------------------------------------------------------------- /_posts/2017-03-14-cost-based-optimization.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: コストベース最適化 3 | date: 2017-03-13 00:00:01 -0900 4 | article_index: 7 5 | original_url: http://www.unofficialmysqlguide.com/cost-based-optimization.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | オプティマイザはコストベース最適化と呼ばれる方法に基づいて、クエリーをどのように実行するかを決定しています。このプロセスを単純化すると次のようになります。 10 | 11 | - それぞれの操作にコストを割り当てる 12 | - 可能性のあるそれぞれの計画にいくつの操作があるかを見積もる 13 | - 値を合計する 14 | - 全体のコストが一番低い計画を選択する 15 | 16 | ![コストベース最適化](http://www.unofficialmysqlguide.com/_images/cost-based-optimization.png) 17 | 18 | 上記が単純化であると述べたのは、オプティマイザは可能性のあるそれぞれの実行計画を徹底的に検索するわけではないからです。例えば、5つのテーブルが結合される場合、各テーブルに5つのインデックスがあるとすると、`5! * 5! =14400` を「超える」クエリーの実行パターンがあることになります。 19 | 20 | * 各インデックスは1つ以上アクセス方法があります(例えば、インデックススキャン、範囲スキャンやインデックスルックアップです)。その上、各テーブルがテーブルスキャンを利用する可能性もあります。 21 | * `INNER JOIN`クエリーではテーブルはどの順序でも結合可能です(テーブルが指定された順序は関係ありません) 22 | * 結合する際に、複数の結合バッファーの方法あるいはサブクエリーの戦略が利用可能となります。 23 | 24 | オプティマイザが考えうる実行計画を全て評価するのは現実的に実行可能ではありません。例えば、最適化が実行より長い時間かかってしまう場合があるでしょう。このような理由からオプティマイザは、ある一定の計画の評価をデフォルトでスキップします[^1]。クエリー計画を検索する深さを制限するための`optimizer_search_depth`という設定パラメータもありますが、デフォルトでは有効化されていません[^2]。 25 | 26 | ## コスト係数を修正する 27 | 28 | それぞれの操作に使われるコストは`mysql`システムデータベース内の`server_cost`および`engine_cost`テーブルを利用して設定可能です。MySQL 8.0でのデフォルト値は次の通りです。 29 | 30 | | コスト | 操作 | 31 | |:-------|:--------------------------------| 32 | | 40 | disk\_temptable\_create\_cost | 33 | | 1 | disk\_temptable\_row\_cost | 34 | | 2 | memory\_temptable\_create\_cost | 35 | | 0.2 | memory\_temptable\_row\_cost | 36 | | 0.1 | key\_compare\_cost | 37 | | 0.2 | row\_evaluate\_cost | 38 | | 1 | io\_block\_read\_cost | 39 | | 1 | memory\_block\_read\_cost | 40 | 41 | {% include info.html info="MySQL 8.0からメモリ内にあるインデックスの割合をコストモデルに利用する新機能を追加しています。以前のバージョンまでのMySQLではMySQLはIOにはページアクセスが常に必要であると仮定していました" %} 42 | 43 | コスト自体はリソース利用量を表す論理的な単位です。1単位にもはや正確な意味はありませんが、起源としては1990年代のハードディスクの1回のランダムIOと遡れます。 44 | 45 | ハードウェアが進化したため、全ての構成要素でコストは一定とはみなせません(たとえばストレージのレイテンシーはSSDの出現と共に改善しました)。同様に、ソフトウェアもハードウェアの変化に対してとりくんでいるため(例えば、圧縮のような機能により)、リソース利用量も変化しうるでしょう。コンフィグ可能なコスト係数があれば、このようなケースに対して改善できます。 46 | 47 | 例4は`row_evaluate_cost`を5倍にしたため、テーブルスキャンのコストが(インデックスを利用することによってかかる余分なコストと比べて)とても大きくなっていることを示しています。これによってオプティマイザは例2で作成した`p(population)`インデックスを選択しています。 48 | 49 | ### 例4: 行あたりの見積もりが増えることでテーブルスキャンが高コストになる 50 | 51 | ```sql 52 | # コストは0.2から1.0に増える 53 | UPDATE mysql.server_cost SET cost_value=1 WHERE cost_name='row_evaluate_cost'; 54 | FLUSH OPTIMIZER_COSTS; 55 | 56 | # 新規セッションにて 57 | EXPLAIN FORMAT=JSON 58 | SELECT * FROM Country WHERE continent='Asia' and population > 5000000; 59 | { 60 | "select_id": 1, 61 | "cost_info": { # 行あたりの見積もりが5倍になったため 62 | "query_cost": "325.01" # クエリーに対する合計コストが 63 | }, # 増加します 64 | "table": { 65 | "table_name": "Country", 66 | "access_type": "range", # rangeアクセスで実行されます 67 | "possible_keys": [ 68 | "p" 69 | ], 70 | "key": "p", 71 | "used_key_parts": [ 72 | "Population" 73 | ], 74 | "key_length": "4", 75 | "rows_examined_per_scan": 108, 76 | "rows_produced_per_join": 15, 77 | "filtered": "14.29", 78 | "index_condition": "(`world`.`Country`.`Population` > 5000000)", 79 | "cost_info": { 80 | "read_cost": "309.58", 81 | "eval_cost": "15.43", 82 | "prefix_cost": "325.01", 83 | "data_read_per_join": "3K" 84 | }, 85 | "used_columns": [ 86 | "Code", 87 | "Name", 88 | "Continent", 89 | "Region", 90 | "SurfaceArea", 91 | "IndepYear", 92 | "Population", 93 | "LifeExpectancy", 94 | "GNP", 95 | "GNPOld", 96 | "LocalName", 97 | "GovernmentForm", 98 | "HeadOfState", 99 | "Capital", 100 | "Code2" 101 | ], 102 | "attached_condition": "(`world`.`Country`.`Continent` = 'Asia')" 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | {% include warning.html warn="コスト係数を修正すると多くのクエリーの計画は「悪くなる」可能性があることにご注意ください!ほとんどの本番環境ではクエリーヒントを追加するのがよいでしょう。" %} 109 | 110 | {% include warning.html warn="次の例に進む前に忘れずにコストを初期化するようにしてください。

UPDATE mysql.server_cost SET cost_value=NULL WHERE cost_name='row_evaluate_cost';
111 | FLUSH OPTIMIZER_COSTS;# セッションを閉じる" %} 112 | 113 | ## メタデータと統計 114 | 115 | 例3で示されたように、データの分布は実行計画のコストに影響を与えます。オプティマイザは意思決定プロセスの一部でデータディクショナリと統計を利用します。 116 | 117 | ### メタデータ 118 | 119 | | | インデックス情報 | ユニーク性 | Nullability | 120 | |:-|:-|:-|:-| 121 | | 概要 | ディクショナリはテーブル毎にインデックスの一覧の情報を持ちます | インデックスがユニークの場合、永久変換に利用され、実行計画のある部分を短縮するのに利用されます | オプティマイザはnullの可能性がある値を正しく扱う必要があります。列のNullability(列がnullになりうるかどうか)は一部の実行計画に影響を与えます | 122 | 123 | ### 統計 124 | 125 | | | テーブルサイズ | カーディナリティー | 範囲の見積もり | 126 | |:-|:-|:-|:-| 127 | | 概要 | テーブルの合計サイズの見積もりです | 少数のページ(デフォルト 20)をランダムでサンプルし、インデックスカラムの固有の値の数を推測します | オプティマイザがInnoDBに最小値と最大値を入力として問合せ、その範囲にある行数の見積もりを受け取ります | 128 | | 適用対象 | 全カラム | インデックスカラム | インデックスカラム | 129 | | 計算 | 事前 | 事前 | 利用するときに実施 | 130 | | 自動更新 | 通常オペレーション[^3]中 | テーブルの10%に変更が発生したとき| - | 131 | | 手動更新 | `ANALYZE TABLE` | `ANALYZE TABLE` | - | 132 | | 設定オプション | - | ページのサンプル数[^4] | 最大インデックスダイブ数[^5]、最大メモリ利用量[^6] | 133 | | 正確性 | 最も正確でない | データ分布の偏りに依存 | 最も正確 | 134 | | 一般的な使われ方 | テーブルスキャンコストの決定に利用。インデックスがない場合の結合順に利用(大きいテーブルから先に) | 結合順の決定に利用。範囲の見積もりが最大インデックスダイブ数を超えた場合にも利用される | 述部の評価に利用。(利用可能なインデックスから何行マッチするかを推定)。population > 5000000がインデックスを使うべきでないかどうかを決めるのに利用する | 135 | 136 | {% include info.html info="統計を利用することで、品質保証のための環境と本番環境で同じようにみえるクエリーが全く異なった実行となることがあります。本番環境では、クエリーの計画はデータ分布が変わるにつれて変化する可能性もあります" %} 137 | 138 | [^1]: デフォルトの`optimizer_prune_level`は1です。この経験則を無効化し、全ての計画を評価するには値を0に設定してください。 [http://dev.mysql.com/doc/refman/5.7/en/controlling-query-plan-evaluation.html](http://dev.mysql.com/doc/refman/5.7/en/controlling-query-plan-evaluation.html) をあわせて参照してください。 139 | [^2]: デフォルトの`optimizer_search_depth`は64です。値を小さくすると見積もりにかかる時間は削減できますが、他方で計画の精度が悪くなる可能性が発生します。 140 | [^3]: 統計は通常のオペレーション中に更新されますが、正確性は保証されません。 141 | [^4]: サンプルするページ数は`innodb_stats_persistent_sample_pages`で変更できます。値を大きくすると見積もりがより正確になります(見積もり生成コストはいくらか増えます) 142 | [^5]: オプティマイザはINリストが`eq_range_index_dive_limit items`(デフォルト 200)を超えた場合にカーディナリティー評価に切り替えます。 143 | [^6]: rangeオプティマイザは`range_optimizer_max_mem_size`にサイズを制限しています(デフォルト 8M)。オプションを組み合わせた場合は内部で展開されるため、複数のINリストを利用するクエリーはこの値を超えます。 144 | 145 | -------------------------------------------------------------------------------- /_posts/2017-03-14-hints.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ヒント 3 | date: 2017-03-14 00:00:01 -0900 4 | article_index: 8 5 | original_url: http://www.unofficialmysqlguide.com/hints.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | `mysql`システムデータベースのコスト係数を調整するだけでなく、MySQLはどのように実行計画が選択されるかを「上書き」する方法も提供しています。クエリーヒントを利用するのは次の2つの明確な理由からとても有用です。 10 | 11 | 1. **デバッグにて** `EXPLAIN`は利用できたメタデータに基づいて決定された結果を表示します。ヒントを追加することによって、他の実行計画を強制的に実行させ、実際の実行時間を比較することができます。 12 | 2. **本番環境にて** デバッグの最中に代わりとなる実行計画が十分に速いと気づいたのであれば、実行を高速化するためにヒントを加えるとよいでしょう。 13 | 14 | 私はデバッグではヒントをよく利用しますが、本番環境に導入するときにはいつも気をつけています。ヒントはある時点で選択された特定の実行計画に固定するものであるため、データの分布やインデックスが時とともに変化した場合に維持管理上の重荷になりえます。 15 | 16 | ベストプラクティスとしては、MySQLのメジャーバージョンアップをした後(例えば、MySQL 9.0にアップグレードするときに)には毎回ヒントを見直すと良いでしょう[^1]。新しい最適化が導入されると、それに伴いたくさんのヒントを無効化できることがお分かりになることでしょう。 17 | 18 | ## 古い形式のヒント 19 | 20 | 以前のMySQLバージョンではSQL文法を直接的に拡張した一連のヒントのみをサポートしていました。例えば次のとおりです。 21 | 22 | ```sql 23 | # テーブルを指定順序で結合させる 24 | SELECT STRAIGHT_JOIN Country.Name as CountryName, City.Name AS City 25 | FROM Country INNER JOIN City ON City.CountryCode=Country.Code; 26 | 27 | # インデックスの利用を強制する 28 | SELECT * FROM Country FORCE INDEX (p) 29 | WHERE continent='Asia' and population > 5000000; 30 | 31 | # インデックスを無視させる 32 | SELECT * FROM Country IGNORE INDEX (p) 33 | WHERE continent='Asia' and population > 5000000; 34 | 35 | # 他のインデックスが存在する状況であるインデックスを優先させる 36 | SELECT * FROM Country USE INDEX (p) 37 | WHERE continent='Asia' and population > 5000000; 38 | ``` 39 | 40 | これらのヒントはMySQL 8.0でも継続してサポートされますが、一部はコメント形式のヒントに置き換えられます。例5では、`FORCE INDEX`ヒントを利用することで、例2で利用されなかった`p(population)` インデックスが選択されていることがわかります。`EXPLAIN`を参照することで、テーブルスキャンのコストがおよそ53であるのに対し、`FORCE INDEX`を利用しても、そのコストは152.21であることがわかります。 41 | 42 | ### 例5: コストによらずインデックスを強制する 43 | 44 | ```sql 45 | EXPLAIN FORMAT=JSON 46 | SELECT * FROM Country FORCE INDEX (p) WHERE continent='Asia' and population > 5000000; 47 | { 48 | "query_block": { 49 | "select_id": 1, 50 | "cost_info": { 51 | "query_cost": "152.21" # テーブルスキャンより高コスト 52 | }, 53 | "table": { 54 | "table_name": "Country", 55 | "access_type": "range", 56 | "possible_keys": [ 57 | "p" 58 | ], 59 | "key": "p", 60 | "used_key_parts": [ 61 | "Population" 62 | ], 63 | "key_length": "4", 64 | "rows_examined_per_scan": 108, 65 | "rows_produced_per_join": 15, 66 | "filtered": "14.29", 67 | "index_condition": "(`world`.`Country`.`Population` > 5000000)", 68 | "cost_info": { 69 | "read_cost": "149.12", 70 | "eval_cost": "3.09", 71 | "prefix_cost": "152.21", 72 | "data_read_per_join": "3K" 73 | }, 74 | "used_columns": [ 75 | "Code", 76 | "Name", 77 | "Continent", 78 | "Region", 79 | "SurfaceArea", 80 | "IndepYear", 81 | "Population", 82 | "LifeExpectancy", 83 | "GNP", 84 | "GNPOld", 85 | "LocalName", 86 | "GovernmentForm", 87 | "HeadOfState", 88 | "Capital", 89 | "Code2" 90 | ], 91 | "attached_condition": "(`world`.`Country`.`Continent` = 'Asia')" 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | ## 新しいコメント形式のヒント 98 | 99 | MySQL 8.0ではMySQL 5.7で導入された新しいコメント形式のヒントを拡張し、テーブルの結合順序を(`STRAIGHT_JOIN`と同様に)制御できるようになりました。私は次の3つの理由から、古いSQLの文法拡張したヒントよりこの新しいヒントの方が好みです。 100 | 101 | 1. 「どのように実行するかを教える」といったことは、SQLの宣言型の本質的なものと分離したほうが読みやすいし書きやすい 102 | 2. 「ヒント」であって命令でないことを構造的に明確に示していること。すなわち、ヒントが実行できなかった場合、ステートメントは警告を発生させるもののエラーは発生させないということになります。これは`FORCE INDEX`などのヒントとは対照的で、この場合はインデックスが存在しないとエラーを発生させます。これは、例えばOracleデータベースのヒントと似た挙動です。 103 | 3. きめ細やかな制御ができること。これにより、データベース管理者の方々はより柔軟にヒントを利用できます。 104 | 105 | | ヒント名称 | 概要 | 106 | |:-|:-| 107 | | BKA, NO\_BKA | 指定したテーブルに対してBatched Key Accessアルゴリズムの最適化を有効化するか、無効化するか(この最適化はデフォルトで無効ですが、 optimizer_switch変数によって制御可能です) | 108 | | BNL, NO\_BNL | 指定したテーブルに対してBlock Nested Loopアルゴリズムの最適化を有効化するか、無効化するか | 109 | | MAX\_EXECUTION\_TIME | クエリーに対する最大実行時間をミリ秒単位で設定します。このヒントは現在はSELECT文に対してのみ有効です | 110 | | MRR, NO\_MRR | Multi-Range Readの最適化に影響します | 111 | | NO\_ICP | インデックスコンディションプッシュダウンの最適化に影響します | 112 | | NO\_RANGE\_OPTIMIZATION | 指定されたテーブルあるいはインデックスにrangeの最適化を無効化します | 113 | | QB\_NAME | クエリーブロックに名前をつけます | 114 | | SEMIJOIN, NO\_SEMIJOIN | サブクエリーを実行する際の準結合戦略(DUPSWEEDOUT, FIRSTMATCH, LOOSESCAN, MATERIALIZATIONなど) | 115 | | SUBQUERY | SEMIJOIN/NO_SEMIJOINと類似しており、IN-to-EXISTSなども含めたサブクエリー実行の際の戦略を制御できます | 116 | 117 | 例6では、あるテーブルに対してrange最適化が無効化されています。p(population)インデックスを使用した方がずっと良いにも関わらず、無視されてしまいます。population > 10億であるものはテーブル中に2行しかありません。 118 | 119 | ### Example 6: range最適化を無効化しインデックスが利用できなくなる 120 | 121 | ```sql 122 | EXPLAIN FORMAT=JSON 123 | SELECT /*+NO_RANGE_OPTIMIZATION(Country) */ * FROM Country 124 | WHERE Population > 1000000000 AND Continent='Asia'; 125 | { 126 | "query_block": { 127 | "select_id": 1, 128 | "cost_info": { 129 | "query_cost": "56.80" 130 | }, 131 | "table": { 132 | "table_name": "Country", 133 | "access_type": "ALL", # アクセス方法はテーブルスキャン 134 | "possible_keys": [ # 利用可能なインデックスは 135 | "p" # 無効化されている範囲スキャンのみでしか使えない 136 | ], 137 | "rows_examined_per_scan": 239, 138 | "rows_produced_per_join": 11, 139 | "filtered": "4.76", 140 | "cost_info": { 141 | "read_cost": "54.52", 142 | "eval_cost": "2.28", 143 | "prefix_cost": "56.80", 144 | "data_read_per_join": "2K" 145 | }, 146 | "used_columns": [ 147 | "Code", 148 | "Name", 149 | "Continent", 150 | "Region", 151 | "SurfaceArea", 152 | "IndepYear", 153 | "Population", 154 | "LifeExpectancy", 155 | "GNP", 156 | "GNPOld", 157 | "LocalName", 158 | "GovernmentForm", 159 | "HeadOfState", 160 | "Capital", 161 | "Code2" 162 | ], 163 | "attached_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 1000000000))" 164 | } 165 | } 166 | } 167 | ``` 168 | 169 | [^1]: [http://mysqlserverteam.com/what-to-do-with-optimizer-hints-after-an-upgrade/](http://mysqlserverteam.com/what-to-do-with-optimizer-hints-after-an-upgrade/) 170 | 171 | -------------------------------------------------------------------------------- /_posts/2017-03-18-comparing-plans.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: プランの比較 3 | date: 2017-03-18 00:00:01 -0900 4 | article_index: 9 5 | original_url: http://www.unofficialmysqlguide.com/comparing-plans.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | 簡単な復習: オプティマイザの役割はたくさんの選択肢の中から最良の実行計画を選択することです。これらの選択肢には、それぞれいくつかの異なるインデックスや、アクセス方法があります。ここまで、われわれは`p(population)`に対する2つの実行計画を見てきました。 10 | 11 | 1. `p(population)`に対する範囲スキャン 12 | 2. テーブルスキャン 13 | 14 | `p(population)`インデックスは選択性が高くないことがわかったため、`c(continent)`に対して新しいインデックスを追加しようと思います。一般的な本番環境では、`p(population)`インデックスは意味を持たなくなるため削除したくなることでしょう。しかし、オプティマイザが複数の選択肢をうまく評価できたことを確認するためにこれを残すこととします。 15 | 16 | ![explain-c.png](http://www.unofficialmysqlguide.com/_images/explain-c.png) 17 | 18 | ### 例7) continent(大陸)にインデックスを追加する 19 | 20 | ```sql 21 | ALTER TABLE Country ADD INDEX c (continent); 22 | EXPLAIN FORMAT=JSON 23 | SELECT * FROM Country WHERE population > 5000000 continent='Asia'; 24 | { 25 | "query_block": { 26 | "select_id": 1, 27 | "cost_info": { 28 | "query_cost": "28.20" 29 | }, 30 | "table": { 31 | "table_name": "Country", 32 | "access_type": "ref", # このアクセス方法 ref は 33 | "possible_keys": [ # ユニークでないインデックスを利用していることを表します 34 | "p", 35 | "c" 36 | ], 37 | "key": "c", # continentインデックスが選択されています 38 | "used_key_parts": [ 39 | "Continent" 40 | ], 41 | "key_length": "1", 42 | "ref": [ 43 | "const" 44 | ], 45 | "rows_examined_per_scan": 51, 46 | "rows_produced_per_join": 23, 47 | "filtered": "45.19", 48 | "cost_info": { 49 | "read_cost": "18.00", 50 | "eval_cost": "4.61", 51 | "prefix_cost": "28.20", 52 | "data_read_per_join": "5K" 53 | }, 54 | "used_columns": [ 55 | "Code", 56 | "Name", 57 | "Continent", 58 | "Region", 59 | "SurfaceArea", 60 | "IndepYear", 61 | "Population", 62 | "LifeExpectancy", 63 | "GNP", 64 | "GNPOld", 65 | "LocalName", 66 | "GovernmentForm", 67 | "HeadOfState", 68 | "Capital", 69 | "Code2" 70 | ], 71 | "attached_condition": "(`world`.`Country`.`Population` > 5000000)" 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | 例7では、`c(continent)`インデックスが`p(population)`インデックスおよびテーブルスキャンよりも優先されていることがわかります。これでジョブはインデックスを利用しますが、これは作業量を減らすためです。オプティマイザはインデックスが利用されたあとに、51行(`rows_examined_per_scan`)のみを検査すれば良いと見積もってます。Asiaの大陸(continent)には国(country)の数が少ないので、インデックスを利用する方が選択性が高くて良いと判断された、とも解釈できます。 78 | 79 | 例8ではクエリーを少し修正し、クエリーの選択条件を >500万 から >5億 に変更することで、`p(population)`インデックスを選択するように変わったことがわかります。これは理にかなっており、人口(population) > 5億の国は2つしかないため、さらにインデックスの選択性が高くなるわけです。 80 | 81 | ![explain-example-8.png](http://www.unofficialmysqlguide.com/_images/explain-example-8.png) 82 | 83 | ### 例8: 人口が多い場合はpへのrangeアクセスがcに対するそれより優先される 84 | 85 | ```sql 86 | EXPLAIN FORMAT=JSON 87 | SELECT * FROM Country WHERE continent='Asia' and population > 500000000; 88 | { 89 | "query_block": { 90 | "select_id": 1, 91 | "cost_info": { 92 | "query_cost": "7.01" 93 | }, 94 | "table": { 95 | "table_name": "Country", 96 | "access_type": "range", # refではなくrangeが利用される 97 | "possible_keys": [ 98 | "p", 99 | "c" 100 | ], 101 | "key": "p", # 例7とkeyが異なる 102 | "used_key_parts": [ 103 | "Population" 104 | ], 105 | "key_length": "4", 106 | "rows_examined_per_scan": 2, 107 | "rows_produced_per_join": 0, 108 | "filtered": "21.34", 109 | "index_condition": "(`world`.`Country`.`Population` > 500000000)", 110 | "cost_info": { 111 | "read_cost": "6.58", 112 | "eval_cost": "0.43", 113 | "prefix_cost": "7.01", 114 | "data_read_per_join": "112" 115 | }, 116 | "used_columns": [ 117 | "Code", 118 | "Name", 119 | "Continent", 120 | "Region", 121 | "SurfaceArea", 122 | "IndepYear", 123 | "Population", 124 | "LifeExpectancy", 125 | "GNP", 126 | "GNPOld", 127 | "LocalName", 128 | "GovernmentForm", 129 | "HeadOfState", 130 | "Capital", 131 | "Code2" 132 | ], 133 | "attached_condition": "(`world`.`Country`.`Continent` = 'Asia')" 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | continentへインデックスを追加することで、少なくとも4つも実行計画をとりうるようになりました。 140 | 141 | 1. `p(population): "500000000 < Population"`への範囲スキャン 142 | 2. テーブルスキャン 143 | 3. `c(continent)`へのrefによるアクセス 144 | 4. `c(continent): "Asia <= Continent <= Asia"`へのrangeによるアクセス 145 | 146 | これらのプランに加え、インデックスマージを使って、`p(population)`と`c(continent)`を両方を使うこともできます。例9の`OPTIMIZER_TRACE`で、`PRIMARY`への範囲スキャンも検討されてたものの、採用されなかったことがわかります。 147 | 148 | `rows_examined_per_scan`がインデックスの選択性を示していることを説明しましたが、例7と例8の違いを説明するのために有用な2つの他の統計があります。 149 | 150 | 1. **Key_length** continentのデータ型は、populationのそれが4バイトであるのに対し、1バイトのenumです。選択性が同じ場合、キー長が短いほうがページ内に多くのキーを格納できるため有利であり、インデックスとしてもメモリーがうまく使えます。 151 | 2. **Access_type** すべての条件が同一であれば、ref によるアクセスは range によるアクセスより低コストです。 152 | 153 | ### 例9 人口が多い場合にインデックスpとcを比較したオプティマイザ トレース 154 | 155 | ```sql 156 | SET optimizer_trace="enabled=on"; 157 | SELECT * FROM Country WHERE continent='Asia' AND population > 500000000; 158 | SELECT * FROM information_schema.optimizer_trace; 159 | { 160 | "steps": [ 161 | { 162 | "join_preparation": { 163 | "select#": 1, 164 | "steps": [ 165 | { 166 | "expanded_query": "/* select#1 */ select `Country`.`Code` AS `Code`,`Country`.`Name` AS `Name`,`Country`.`Continent` AS `Continent`,`Country`.`Region` AS `Region`,`Country`.`SurfaceArea` AS `SurfaceArea`,`Country`.`IndepYear` AS `IndepYear`,`Country`.`Population` AS `Population`,`Country`.`LifeExpectancy` AS `LifeExpectancy`,`Country`.`GNP` AS `GNP`,`Country`.`GNPOld` AS `GNPOld`,`Country`.`LocalName` AS `LocalName`,`Country`.`GovernmentForm` AS `GovernmentForm`,`Country`.`HeadOfState` AS `HeadOfState`,`Country`.`Capital` AS `Capital`,`Country`.`Code2` AS `Code2` from `Country` where ((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 500000000))" 167 | } 168 | ] 169 | } 170 | }, 171 | { 172 | "join_optimization": { 173 | "select#": 1, 174 | "steps": [ 175 | { 176 | "condition_processing": { 177 | "condition": "WHERE", 178 | "original_condition": "((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 500000000))", 179 | "steps": [ 180 | { 181 | "transformation": "equality_propagation", 182 | "resulting_condition": "((`Country`.`Population` > 500000000) and multiple equal('Asia', `Country`.`Continent`))" 183 | }, 184 | { 185 | "transformation": "constant_propagation", 186 | "resulting_condition": "((`Country`.`Population` > 500000000) and multiple equal('Asia', `Country`.`Continent`))" 187 | }, 188 | { 189 | "transformation": "trivial_condition_removal", 190 | "resulting_condition": "((`Country`.`Population` > 500000000) and multiple equal('Asia', `Country`.`Continent`))" 191 | } 192 | ] 193 | } 194 | }, 195 | { 196 | "substitute_generated_columns": { 197 | } 198 | }, 199 | { 200 | "table_dependencies": [ 201 | { 202 | "table": "`Country`", 203 | "row_may_be_null": false, 204 | "map_bit": 0, 205 | "depends_on_map_bits": [ 206 | ] 207 | } 208 | ] 209 | }, 210 | { 211 | "ref_optimizer_key_uses": [ 212 | { 213 | "table": "`Country`", 214 | "field": "Continent", 215 | "equals": "'Asia'", 216 | "null_rejecting": false 217 | } 218 | ] 219 | }, 220 | { 221 | "rows_estimation": [ 222 | { 223 | "table": "`Country`", 224 | "range_analysis": { 225 | "table_scan": { 226 | "rows": 239, 227 | "cost": 247.1 228 | }, 229 | "potential_range_indexes": [ 230 | { 231 | "index": "PRIMARY", 232 | "usable": false, # 主キーへの範囲スキャンは 233 | "cause": "not_applicable" # 利用できない 234 | }, 235 | { 236 | "index": "p", 237 | "usable": true, # pへの範囲スキャンは利用可能 238 | "key_parts": [ # "analyzing_range_alternatives"下で 239 | "Population", # 評価されている 240 | "Code" 241 | ] 242 | }, 243 | { 244 | "index": "c", # cへの範囲スキャンは利用可能 245 | "usable": true, # "analyzing_range_alternatives"下で 246 | "key_parts": [ # 評価されているが 247 | "Continent", # 高コストと見積もられている 248 | "Code" 249 | ] 250 | } 251 | ], 252 | "setup_range_conditions": [ 253 | ], 254 | "group_index_range": { 255 | "chosen": false, 256 | "cause": "not_group_by_or_distinct" 257 | }, 258 | "analyzing_range_alternatives": { 259 | "range_scan_alternatives": [ 260 | { 261 | "index": "p", 262 | "ranges": [ 263 | "500000000 < Population" 264 | ], 265 | "index_dives_for_eq_ranges": true, 266 | "rowid_ordered": false, 267 | "using_mrr": false, 268 | "index_only": false, 269 | "rows": 2, 270 | "cost": 5.01, 271 | "chosen": true 272 | }, 273 | { 274 | "index": "c", 275 | "ranges": [ 276 | "Asia <= Continent <= Asia" 277 | ], 278 | "index_dives_for_eq_ranges": true, 279 | "rowid_ordered": true, 280 | "using_mrr": false, 281 | "index_only": false, 282 | "rows": 51, 283 | "cost": 103.01, 284 | "chosen": false, 285 | "cause": "cost" 286 | } 287 | ], 288 | "analyzing_roworder_intersect": { 289 | "usable": false, # インデックスマージは利用しない 290 | "cause": "too_few_roworder_scans" 291 | } 292 | }, 293 | "chosen_range_access_summary": { 294 | "range_access_plan": { 295 | "type": "range_scan", # rangeオプティマイザは 296 | "index": "p", # pへのrangeアクセスを選択 297 | "rows": 2, 298 | "ranges": [ 299 | "500000000 < Population" 300 | ] 301 | }, 302 | "rows_for_plan": 2, 303 | "cost_for_plan": 5.01, 304 | "chosen": true 305 | } 306 | } 307 | } 308 | ] 309 | }, 310 | { 311 | "considered_execution_plans": [ 312 | { 313 | "plan_prefix": [ 314 | ], 315 | "table": "`Country`", # ここは最善のアクセス戦略の 316 | "best_access_path": { # まとめ 317 | "considered_access_paths": [ # (refおよびrangeオプティマイザより) 318 | { 319 | "access_type": "ref", # cへのrefアクセス 320 | "index": "c", 321 | "rows": 51, 322 | "cost": 69, 323 | "chosen": true 324 | }, 325 | { 326 | "rows_to_scan": 2, 327 | "access_type": "range", # pへのrangeアクセス 328 | "range_details": { 329 | "used_index": "p" 330 | }, 331 | "resulting_rows": 2, 332 | "cost": 7.01, 333 | "chosen": true 334 | } 335 | ] 336 | }, 337 | "condition_filtering_pct": 100, 338 | "rows_for_plan": 2, 339 | "cost_for_plan": 7.01, 340 | "chosen": true 341 | } 342 | ] 343 | }, 344 | { 345 | "attaching_conditions_to_tables": { 346 | "original_condition": "((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 500000000))", 347 | "attached_conditions_computation": [ 348 | ], 349 | "attached_conditions_summary": [ 350 | { 351 | "table": "`Country`", 352 | "attached": "((`Country`.`Continent` = 'Asia') and (`Country`.`Population` > 500000000))" 353 | } 354 | ] 355 | } 356 | }, 357 | { 358 | "refine_plan": [ 359 | { 360 | "table": "`Country`", 361 | "pushed_index_condition": "(`Country`.`Population` > 500000000)", 362 | "table_condition_attached": "(`Country`.`Continent` = 'Asia')" 363 | } 364 | ] 365 | } 366 | ] 367 | } 368 | }, 369 | { 370 | "join_execution": { 371 | "select#": 1, 372 | "steps": [ 373 | ] 374 | } 375 | } 376 | ] 377 | } 378 | ``` 379 | -------------------------------------------------------------------------------- /_posts/2017-03-25-composite-indexes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 複合インデックス 3 | date: 2017-03-25 00:00:01 -0900 4 | article_index: 10 5 | original_url: http://www.unofficialmysqlguide.com/composite-indexes.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | アジア大陸のすべての国の人口が500万より多いわけではありませんので、2つの述部を合わせると「作業量を減らす」インデックスとなるはずです。つまり、このデータセットでは複合インデックスによって選択性が改善するわけです。 10 | 11 | ![explain-example-10.png](http://www.unofficialmysqlguide.com/_images/explain-example-10.png) 12 | 13 | 複合インデックスとしては2つの選択肢が考えられます。 14 | 15 | 1. `p_c(Population, Continent)`にインデックスを追加 16 | 2. `c_p(Continent, Population)`にインデックスを追加 17 | 18 | 複合インデックスにおける順序の違いは「非常に」重要です。population(人口)はある範囲ですので、オプティマイザは`p_c(population, continent)`インデックスの最初の部分[^1] だけが利用できます。`p_c(polulation, continent)`は`p(population)`インデックスのみを利用したときと比べてほとんど改善しないわけです。インデックスを強制的に利用するようにすると、これが明確にわかります。 19 | 20 | ### 例10: (population, continent)への複合インデックスは良い選択肢ではない 21 | 22 | ```sql 23 | ALTER TABLE Country ADD INDEX p_c (Population, Continent); 24 | EXPLAIN FORMAT=JSON 25 | SELECT * FROM Country FORCE INDEX (p_c) WHERE continent='Asia' and population > 5000000; 26 | { 27 | "query_block": { 28 | "select_id": 1, 29 | "cost_info": { # インデックスの利用が強制されている 30 | "query_cost": "152.21" # テーブルスキャンより高コスト 31 | }, 32 | "table": { 33 | "table_name": "Country", 34 | "access_type": "range", 35 | "possible_keys": [ 36 | "p_c" 37 | ], 38 | "key": "p_c", 39 | "used_key_parts": [ # population列のみ 40 | "Population" # 利用されている 41 | ], 42 | "key_length": "4", # populationは4バイトの整数値 43 | "rows_examined_per_scan": 108, 44 | "rows_produced_per_join": 15, 45 | "filtered": "14.29", 46 | "index_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000))", 47 | "cost_info": { 48 | "read_cost": "149.12", 49 | "eval_cost": "3.09", 50 | "prefix_cost": "152.21", 51 | "data_read_per_join": "3K" 52 | }, 53 | "used_columns": [ 54 | "Code", 55 | "Name", 56 | "Continent", 57 | "Region", 58 | "SurfaceArea", 59 | "IndepYear", 60 | "Population", 61 | "LifeExpectancy", 62 | "GNP", 63 | "GNPOld", 64 | "LocalName", 65 | "GovernmentForm", 66 | "HeadOfState", 67 | "Capital", 68 | "Code2" 69 | ] 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | この制約はB+ツリーのインデックス構造によるものです。これを簡潔に覚えるには複合インデックスでは「範囲は右側へ」ということになります。これを考慮して、`c_p(continent, population)`へのインデックスについて例11で示されています。continentのみのインデックスと比べて、2つの列の組み合わせに対してインデックスを利用することで選択性が改善されて、コストは28.20(例7)から下がっています。2つの列がインデックスで効率的に結合されているため、アクセス方法として「range」が選択されています。 76 | 77 | ![explain-cp.png](http://www.unofficialmysqlguide.com/_images/explain-cp.png) 78 | 79 | ### 例11: continent, populationへのより適切な複合インデックス 80 | 81 | ```sql 82 | ALTER TABLE Country ADD INDEX c_p (Continent, Population); 83 | EXPLAIN FORMAT=JSON 84 | SELECT * FROM Country WHERE continent='Asia' and population > 5000000; 85 | { 86 | "query_block": { 87 | "select_id": 1, 88 | "cost_info": { 89 | "query_cost": "24.83" # 複合インデックスp,cのコスト(152.21)より 90 | }, # はるかに低コスト 91 | "table": { 92 | "table_name": "Country", 93 | "access_type": "range", 94 | "possible_keys": [ 95 | "p", 96 | "c", 97 | "p_c", 98 | "c_p" 99 | ], 100 | "key": "c_p", 101 | "used_key_parts": [ # 両方の列が利用されている 102 | "Continent", # 1バイト(ENUM) 103 | "Population" # 4バイト(INT) 104 | ], 105 | "key_length": "5", # =5B 106 | "rows_examined_per_scan": 32, 107 | "rows_produced_per_join": 15, 108 | "filtered": "100.00", 109 | "index_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000))", 110 | "cost_info": { 111 | "read_cost": "18.00", 112 | "eval_cost": "3.09", 113 | "prefix_cost": "24.83", 114 | "data_read_per_join": "3K" 115 | }, 116 | "used_columns": [ 117 | "Code", 118 | "Name", 119 | "Continent", 120 | "Region", 121 | "SurfaceArea", 122 | "IndepYear", 123 | "Population", 124 | "LifeExpectancy", 125 | "GNP", 126 | "GNPOld", 127 | "LocalName", 128 | "GovernmentForm", 129 | "HeadOfState", 130 | "Capital", 131 | "Code2" 132 | ] 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | ## 複合インデックスの列順を決める 139 | 140 | 複合インデックス内の列の正しい順序を決める際には注意が必要です。ここに注意すべき考慮点をいくつか示します。 141 | 142 | 1. **左が最重要** : `(First Name, Last Name)`へのインデックスは、`(First Name)`へのインデックスを必要とするクエリーにも利用できますが、`(Last Name)`へのインデックスを必要とするクエリーには利用できません。一番多くのクエリで再利用できるように複合インデックスを設計しましょう。 143 | 2. **範囲は右へ** : `(Age, First Name)`へのインデックスは、`WHERE age BETWEEN x and y AND first_name = 'John'`[^2]というクエリーに対して活用できません。もっと具体的にいうと、最初の範囲の条件のあとの残りの複合インデックスは利用されません。 144 | 3. **選択性の高い列を左へ** : インデックスが低コストになり出来る限り高速となるよう考えてみてください。アクセスされるインデックスページ数が少なくなるため、通常はメモリーをよりうまく使えるようになります。 145 | 4. **インデックスの順序(order)の変更に注意** : ASCあるいはDESCを混合すると、複合インデックスがいくつ利用されるかに影響が発生する可能性があります。 146 | 147 | [^1]: 固定値に対する範囲に関しては例外となります。例えば、(1,2,3,4,5)はpupulationおよび`continent(p_c)`の両方に対して完全なインデックスが利用できます。 148 | [^2]: 脚注1参照。 149 | -------------------------------------------------------------------------------- /_posts/2017-04-08-covering-indexes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: カバリングインデックス 3 | date: 2017-04-08 00:00:01 -0900 4 | article_index: 11 5 | original_url: http://www.unofficialmysqlguide.com/covering-indexes.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | カバリングインデックスは複合インデックスの特異な形式で、インデックスに全ての列を包含するものです。このシナリオでは、MySQLはテーブルの行にアクセスすることがなく「インデックスから」データを返すという最適化をすることができます。 10 | 11 | ![explan-cpn](http://www.unofficialmysqlguide.com/_images/explain-cpn.png) 12 | 13 | `SELECT * FROM Country`以外で、`population > 5M and continent='Asia'`の条件を満たす国の名前のみが必要な場合を考えてみましょう。例12で示されるように、`c_p_n (Continent,Population,Name)`へのインデックスは、最初の2列は行を絞り込むために、そして残りの3列目は値を返すために利用できます。 14 | 15 | ### 例12: c_p_nへのカバリングインデックス 16 | 17 | ```sql 18 | ALTER TABLE Country ADD INDEX c_p_n (Continent,Population,Name); 19 | EXPLAIN FORMAT=JSON 20 | SELECT Name FROM Country WHERE continent='Asia' and population > 5000000; 21 | { 22 | "query_block": { 23 | "select_id": 1, 24 | "cost_info": { 25 | "query_cost": "8.07" # コストが67%削減されている 26 | }, 27 | "table": { 28 | "table_name": "Country", 29 | "access_type": "range", 30 | "possible_keys": [ 31 | "p", 32 | "c", 33 | "p_c", 34 | "c_p", 35 | "c_p_n" 36 | ], 37 | "key": "c_p_n", 38 | "used_key_parts": [ 39 | "Continent", 40 | "Population" 41 | ], 42 | "key_length": "5", 43 | "rows_examined_per_scan": 32, 44 | "rows_produced_per_join": 15, 45 | "filtered": "100.00", 46 | "using_index": true, # Using indexは「カバリングインデックス」を意味する 47 | "cost_info": { 48 | "read_cost": "1.24", 49 | "eval_cost": "3.09", 50 | "prefix_cost": "8.07", 51 | "data_read_per_join": "3K" 52 | }, 53 | "used_columns": [ 54 | "Name", 55 | "Continent", 56 | "Population" 57 | ], 58 | "attached_condition": "((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Population` > 5000000))" 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | カバリングインデックスが利用されたことが`EXPLAIN`の`"using_index": true`によって示されています。カバリングインデックスは正当に評価されない最適化の一つです。多くの方々が、カバリングインデックスではインデックスが利用される一方で、テーブルの行にアクセスしないため「コストが半分になる」と誤解されています。例12では例11に示されるカバリングインデックスでないインデックスの場合と比べて約1/3のコストになっていることが分かります。 65 | 66 | 本番環境ではインデックスのクラスタリング効果によって、他のクエリーに比べメモリーをよりうまく使えるでしょう。アクセスされるセカンダリインデックスがクラスタインデックス(主キー)と「相関[^1]」がなかったらより多くのクラスタ化されたキーのページへのアクセスが必要となるためです。 67 | 68 | [^1]: 相関というのは「だいたい同じ順序」ということを意図しています。例えば、セカンダリインデックスがtimestamp型で挿入された場合、auto_incrementの主キーと高い相関があります。また、populationとcontinentへのインデックスは3文字の国コードである主キーと相関がありそうにありません。 69 | -------------------------------------------------------------------------------- /_posts/2017-04-09-visual-explain.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Visual Explain 3 | date: 2017-04-09 00:00:01 -0900 4 | article_index: 12 5 | original_url: http://www.unofficialmysqlguide.com/visual-explain.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQL WorkbenchにはVisual Explainというツールが同梱されており、これは複雑な実行計画を人間にとって読みやすくするものです。この機能は`EXPLAIN FORMAT=JSON`を内部的に利用しているため、通常の`EXPLAIN FORMAT=JSON`以上の機能を「もたない」ことを述べておきます。実際のところ、シンプル化するためにカバリングインデックスの利用などのいくつかの出力を省略しています。 10 | 11 | Visual Explainの色はアクセスメソッドを示しています。 12 | 13 | * `ref`は緑 14 | * `range`はオレンジ 15 | * `ALL` (テーブルスキャン) と`INDEX` (インデックススキャン)は赤 16 | 17 | これまで例の中で見てきたように、選択性の高いrangeアクセスは選択性の低いrefアクセスより望ましく、これはおそらくちょっとした単純化でしょう。 18 | 19 | ![explain-c.png](http://www.unofficialmysqlguide.com/_images/explain-c.png) 20 | 21 | ![explain-cp.png](http://www.unofficialmysqlguide.com/_images/explain-cp.png) 22 | 23 | ![explain-cpn.png](http://www.unofficialmysqlguide.com/_images/explain-cpn.png) 24 | 25 | ![explain-force-p.png](http://www.unofficialmysqlguide.com/_images/explain-force-p.png) 26 | 27 | ![explain-fts.png](http://www.unofficialmysqlguide.com/_images/explain-fts.png) 28 | -------------------------------------------------------------------------------- /_posts/2017-04-15-transient-plans.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 変わりゆく実行計画(Transient Plans) 3 | date: 2017-04-15 00:00:01 -0900 4 | article_index: 13 5 | original_url: http://www.unofficialmysqlguide.com/transient-plans.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | 同じようにみえるクエリーも全く違った実行計画となることがあります。これまでpopulationの述部を修正することによって確認してきました。例8では、population > `500M`(5億)を指定することで population >`5M`(500万)を指定した場合と異なったインデックスが選択されました。 10 | 11 | クエリーの一部がユーザー入力から生成される本番環境では、このようなことはとても良く発生します[^1]。例としては、 12 | 13 | * 数カ月分のデータに対して「昨日から現在まで」の範囲の日付を指定した場合、おそらく日付の列のインデックスが利用されることでしょう。一方で「昨年から現在まで」を指定した場合はインデックスは利用されないかもしれません。 14 | * WHERE `is_deleted=1`のレコードを探すには、多くのレコードが除外され、したがってインデックスにとても適しています。同様にWHERE `is_deleted=0`はインデックスには適さないでしょう。 15 | 16 | これは予期された挙動で、[統計収集(「メタデータと統計」の章で言及済み)](https://yakst.github.io/unofficialmysqlguide-ja/cost-based-optimization.html)の結果です。次の表は、*population*と*continent* が変わった際にインデックスの相対的コストがどの程度変化するかを示しています。 17 | 18 | | | **p>5M,c=’Asia’** | **p>5M,c=’Antarctica’** | **p>50M, c=’Asia’** | **p>50M,c=’Antarctica’** | **p>500M, c=’Asia’** | **p>500M,c=’Antarctica’** | 19 | |:--|:--|:--|:--|:--|:--|:--| 20 | | p | 152.21 | 152.21 | 34.61 | 34.61 | 3.81 | 3.81 | 21 | | c | 28.20 | 6.00 | 28.20 | 6.00 | 28.20 | 6.00 | 22 | | c,p | 24.83 | 2.41 | 16.41 | 2.41 | 3.81 | 2.41 | 23 | | p,c | 152.21 | 152.21 | 34.61 | 34.61 | 3.81 | 3.81 | 24 | | テーブルスキャン | 53.80 | 53.80 | 53.80 | 53.80 | 53.80 | 53.80 | 25 | 26 | * テーブルスキャンは常にテーブル内の各行を探索するため、比較的固定されたコストとなります 27 | * `p`または`c`のインデックスを利用するときのコストは、入力値で効率的にフィルター出来るようになるにつれて変化します(Antarctica: 南極大陸には国の数が少なく、人口が多くなればなるほど対象となる国の数は少なくなります) 28 | * `c, p`への複合インデックスはほとんどのクエリーに対して比較的効率的にコストを削減できています。 29 | * `p`へのインデックスに対するコストと、`p,c`の複合インデックスに対するコストは既に説明済みの理由によって同一となります。`p,c`インデックスは"範囲が左側"にあるため`p`のインデックスしか利用されません。 30 | * このデータセットでは、南極大陸には人口0の4つの国があります。`c,p`への複合インデックスに対するクエリーが一番低コストなクエリーとなり、これは結果セットが0件であるためです。 31 | 32 | これは視覚的にも表すことが出来ます。continentへのテーブルスキャンおよびrefアクセスは一定コストであり、(Continent, Population)への複合インデックスがcontinentへのrefアクセスと同一コストで始まり、このときpopulationの選択性は高くありません。populationが大きくなるに連れて選択性が増します。 33 | 34 | 複合インデックスが効果的でなければ、個々のインデックス(pupulationまたはcontinent)と近いコストになるでしょう。最後に`population > 3M`(300万)の国はすべてアジアにあるため、選択性はpopulationへのインデックスを利用したときとほとんど変わりません。 35 | 36 | 37 | ![cost-as-afunction-of-p](http://www.unofficialmysqlguide.com/_images/cost-as-a-function-of-p.png) 38 | 39 | 次の可視化はインデックス`p`を強制的に利用させ、populationを(`WHERE population > 1M`(100万)から`WHERE population > 500M`(5億)まで)変えて100回実行した際の実行時間の中央値を示しています。実行時間は`performance_schema.events_statements_history_long`から取得し、マイクロ秒(μs)に変換しています。データセットが小規模で、人口が少ない国がとても少ないため、同一のページがアクセスされたと思われる実行時間のクラスターがみられるのではないかと思います。 40 | しかしながらいくらかの相関がみられます。 41 | ![execution-time-vs-cost](http://www.unofficialmysqlguide.com/_images/execution-time-vs-cost.png) 42 | 43 | {% include info.html info="この可視化の結果はデータ分布に強く依存します。サンプルのデータセットを確認すれば、不正確なところがあることが分かるでしょう。データはStatistics Finland(1990年代)のもので均一に更新されていません" %} 44 | 45 | [^1]: クエリーの分布によっては、これらの例の両方のケースでインデックスを生成するより、パーティショニングした方がうまく動作するかもしれません。 46 | -------------------------------------------------------------------------------- /_posts/2017-05-15-subqueries.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: サブクエリー 3 | date: 2017-05-15 00:00:01 -0900 4 | article_index: 14 5 | original_url: http://www.unofficialmysqlguide.com/subqueries.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | オプティマイザにはサブクエリーを最適化する多数の実行戦略があり、例えばjoinあるいはsemi-join(準結合)にクエリーを書き換える手法、あるいは実体化があります。利用される最適化戦略はどのようなサブクエリーであるか、やその配置に依存します。 10 | 11 | ## スカラーサブクエリー 12 | 13 | スカラーサブクエリーとは、きっかり1行を返すサブクエリーのことで、実行時には最適化しきった上で結果がキャッシュできる可能性があります。例13で、Torontoの`CountryCode`を取得するスカラーサブクエリーの例を示しています。オプティマイザがこれを2つのクエリーとみなしているのは重要で、それぞれコストが1.00および4213.00となっています(訳注: コストの値は修正もれで1.20および862.60と思われます)。 14 | 15 | ![explain-scalar](http://www.unofficialmysqlguide.com/_images/explain-scalar.png) 16 | 17 | 2番目のクエリー(`select_id: 2`)では利用できるインデックスがないため、テーブルスキャンが実行されています。attached\_condition(City.Name)にインデックスが付与されていないのが分かり、インデックスを追加した後クエリーが最適化されています。 18 | 19 | ### 例13: スカラーサブクエリー 20 | 21 | ```sql 22 | EXPLAIN FORMAT=JSON 23 | SELECT * FROM Country WHERE Code = (SELECT CountryCode FROM City WHERE name='Toronto'); 24 | { 25 | "query_block": { 26 | "select_id": 1, 27 | "cost_info": { 28 | "query_cost": "1.20" 29 | }, 30 | "table": { 31 | "table_name": "Country", 32 | "access_type": "ref", 33 | "possible_keys": [ 34 | "PRIMARY" 35 | ], 36 | "key": "PRIMARY", 37 | "used_key_parts": [ 38 | "Code" 39 | ], 40 | "key_length": "3", 41 | "ref": [ 42 | "const" 43 | ], 44 | "rows_examined_per_scan": 1, 45 | "rows_produced_per_join": 1, 46 | "filtered": "100.00", 47 | "cost_info": { 48 | "read_cost": "1.00", 49 | "eval_cost": "0.20", 50 | "prefix_cost": "1.20", 51 | "data_read_per_join": "264" 52 | }, 53 | "used_columns": [ 54 | "Code", 55 | "Name", 56 | "Continent", 57 | "Region", 58 | "SurfaceArea", 59 | "IndepYear", 60 | "Population", 61 | "LifeExpectancy", 62 | "GNP", 63 | "GNPOld", 64 | "LocalName", 65 | "GovernmentForm", 66 | "HeadOfState", 67 | "Capital", 68 | "Code2" 69 | ], 70 | "attached_condition": "(`world`.`Country`.`Code` = (/* select#2 */ select `world`.`City`.`CountryCode` from `world`.`City` where (`world`.`City`.`Name` = 'Toronto')))", 71 | "attached_subqueries": [ 72 | { 73 | "dependent": false, 74 | "cacheable": true, 75 | "query_block": { 76 | "select_id": 2, 77 | "cost_info": { 78 | "query_cost": "862.60" 79 | }, 80 | "table": { 81 | "table_name": "City", 82 | "access_type": "ALL", 83 | "rows_examined_per_scan": 4188, 84 | "rows_produced_per_join": 418, 85 | "filtered": "10.00", 86 | "cost_info": { 87 | "read_cost": "778.84", 88 | "eval_cost": "83.76", 89 | "prefix_cost": "862.60", 90 | "data_read_per_join": "29K" 91 | }, 92 | "used_columns": [ 93 | "Name", 94 | "CountryCode" 95 | ], 96 | "attached_condition": "(`world`.`City`.`Name` = 'Toronto')" 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | ### 例14: スカラーサブクエリーを改善するためにインデックスを追加 107 | 108 | ```sql 109 | # インデックスを追加 110 | ALTER TABLE City ADD INDEX n (Name); 111 | EXPLAIN FORMAT=JSON 112 | SELECT * FROM Country WHERE Code = (SELECT CountryCode FROM City WHERE name='Toronto'); 113 | { 114 | "query_block": { 115 | "select_id": 1, 116 | "cost_info": { 117 | "query_cost": "1.00" 118 | }, 119 | "table": { 120 | "table_name": "Country", 121 | "access_type": "const", 122 | "possible_keys": [ 123 | "PRIMARY" 124 | ], 125 | "key": "PRIMARY", 126 | "used_key_parts": [ 127 | "Code" 128 | ], 129 | "key_length": "3", 130 | "ref": [ 131 | "const" 132 | ], 133 | "rows_examined_per_scan": 1, 134 | "rows_produced_per_join": 1, 135 | "filtered": "100.00", 136 | "cost_info": { 137 | "read_cost": "0.00", 138 | "eval_cost": "1.00", 139 | "prefix_cost": "0.00", 140 | "data_read_per_join": "264" 141 | }, 142 | "used_columns": [ 143 | "Code", 144 | "Name", 145 | "Continent", 146 | "Region", 147 | "SurfaceArea", 148 | "IndepYear", 149 | "Population", 150 | "LifeExpectancy", 151 | "GNP", 152 | "GNPOld", 153 | "LocalName", 154 | "GovernmentForm", 155 | "HeadOfState", 156 | "Capital", 157 | "Code2" 158 | ] 159 | }, 160 | "optimized_away_subqueries": [ 161 | { 162 | "dependent": false, 163 | "cacheable": true, 164 | "query_block": { 165 | "select_id": 2, 166 | "cost_info": { 167 | "query_cost": "2.00" 168 | }, 169 | "table": { 170 | "table_name": "City", 171 | "access_type": "ref", 172 | "possible_keys": [ 173 | "n" 174 | ], 175 | "key": "n", 176 | "used_key_parts": [ 177 | "Name" 178 | ], 179 | "key_length": "35", 180 | "ref": [ 181 | "const" 182 | ], 183 | "rows_examined_per_scan": 1, 184 | "rows_produced_per_join": 1, 185 | "filtered": "100.00", 186 | "cost_info": { 187 | "read_cost": "1.00", 188 | "eval_cost": "1.00", 189 | "prefix_cost": "2.00", 190 | "data_read_per_join": "72" 191 | }, 192 | "used_columns": [ 193 | "Name", 194 | "CountryCode" 195 | ] 196 | } 197 | } 198 | } 199 | ] 200 | } 201 | } 202 | ``` 203 | 204 | ## INサブクエリー(結果がユニークな場合) 205 | 206 | 例15は、サブクエリーで返される行が主キーであるため、結果が一意で重複がない(ユニークな)ことが保証されます。これによって、サブクエリーは`INNER JOIN`クエリーに安全に変換され、同じ結果を返すことが出来ます。`EXPLAIN`を実行した後に`SHOW WARNINGS `を実行すれば実際に実行されているクエリーが確認できます。 207 | 208 | ```sql 209 | /* select#1 */ select `world`.`City`.`ID` AS `ID`,`world`.`City`.`Name` AS `Name`,`world`.`City`.`CountryCode` AS `CountryCode`,`world`.`City`.`District` AS `District`,`world`.`City`.`Population` AS `Population` from `world`.`Country` join `world`.`City` where ((`world`.`City`.`CountryCode` = `world`.`Country`.`Code`) and (`world`.`Country`.`Continent` = 'Asia')) 210 | 1 row in set (0.00 sec) 211 | ``` 212 | 213 | このサブクエリーはまずまず効率的です。Countryテーブルに最初にアクセス(カバリングインデックスを利用し、インデックスオンリースキャンを利用)し、一致した各行に対して一連の行がCityテーブル内を`c(CountryCode)`インデックスを利用して参照されます。 214 | 215 | ![explain-example-15](http://www.unofficialmysqlguide.com/_images/explain-example-15.png) 216 | 217 | ### 例15: 変換可能なINサブクエリー 218 | 219 | ``` 220 | EXPLAIN FORMAT=JSON 221 | SELECT * FROM City WHERE CountryCode IN (SELECT Code FROM Country WHERE Continent = 'Asia'); 222 | { 223 | "query_block": { 224 | "select_id": 1, 225 | "cost_info": { 226 | "query_cost": "1893.30" 227 | }, 228 | "nested_loop": [ 229 | { 230 | "table": { 231 | "table_name": "Country", 232 | "access_type": "ref", 233 | "possible_keys": [ 234 | "PRIMARY", 235 | "c" 236 | ], 237 | "key": "c", 238 | "used_key_parts": [ 239 | "Continent" 240 | ], 241 | "key_length": "1", 242 | "ref": [ 243 | "const" 244 | ], 245 | "rows_examined_per_scan": 51, 246 | "rows_produced_per_join": 51, 247 | "filtered": "100.00", 248 | "using_index": true, 249 | "cost_info": { 250 | "read_cost": "1.02", 251 | "eval_cost": "51.00", 252 | "prefix_cost": "52.02", 253 | "data_read_per_join": "13K" 254 | }, 255 | "used_columns": [ 256 | "Code", 257 | "Continent" 258 | ] 259 | } 260 | }, 261 | { 262 | "table": { 263 | "table_name": "City", 264 | "access_type": "ref", 265 | "possible_keys": [ 266 | "CountryCode" 267 | ], 268 | "key": "CountryCode", 269 | "used_key_parts": [ 270 | "CountryCode" 271 | ], 272 | "key_length": "3", 273 | "ref": [ 274 | "world.Country.Code" 275 | ], 276 | "rows_examined_per_scan": 18, 277 | "rows_produced_per_join": 920, 278 | "filtered": "100.00", 279 | "cost_info": { 280 | "read_cost": "920.64", 281 | "eval_cost": "920.64", 282 | "prefix_cost": "1893.30", 283 | "data_read_per_join": "64K" 284 | }, 285 | "used_columns": [ 286 | "ID", 287 | "Name", 288 | "CountryCode", 289 | "District", 290 | "Population" 291 | ] 292 | } 293 | } 294 | ] 295 | } 296 | } 297 | ``` 298 | 299 | ## INサブクエリー(ユニークでない場合) 300 | 301 | 例15では、サブクエリーは一意で重複がない(ユニークな)行セットを返すため`INNER JOIN`に書き換えられていました。サブクエリーがユニークではないとき、MySQLのオプティマイザは別の戦略を選択します。 302 | 303 | 例16では、少なくとも1つ公用語を持つ国を検索しようとしています。いくつかの国では複数の公用語があるため、サブクエリーの結果はユニークにはなりません。 304 | 305 | ![explain-example-16](http://www.unofficialmysqlguide.com/_images/explain-example-16.png) 306 | 307 | `OPTIMIZER_TRACE`からの出力(例17)ではクエリーが直接joinに書き換えられないとオプティマイザが判断し、代わりにsemi-join(準結合)を要求しています。オプティマイザには複数の準結合戦略(FirstMatch, MaterializeLookup, DuplicatedWeedout)があります。このケースでは実体化(一時的な結果を保存するためにバッファーを生成)が最もこのクエリーを実行するためのコストが低いと結論づけています。 308 | 309 | ### 例16: INNER JOINで書き換えられないサブクエリー 310 | 311 | ```sql 312 | EXPLAIN FORMAT=JSON 313 | SELECT * FROM Country WHERE Code IN (SELECT CountryCode FROM CountryLanguage WHERE isOfficial=1); 314 | { 315 | "query_block": { 316 | "select_id": 1, 317 | "cost_info": { 318 | "query_cost": "407.80" 319 | }, 320 | "nested_loop": [ 321 | { 322 | "table": { 323 | "table_name": "Country", 324 | "access_type": "ALL", 325 | "possible_keys": [ 326 | "PRIMARY" 327 | ], 328 | "rows_examined_per_scan": 239, 329 | "rows_produced_per_join": 239, 330 | "filtered": "100.00", 331 | "cost_info": { 332 | "read_cost": "9.00", 333 | "eval_cost": "47.80", 334 | "prefix_cost": "56.80", 335 | "data_read_per_join": "61K" 336 | }, 337 | "used_columns": [ 338 | "Code", 339 | "Name", 340 | "Continent", 341 | "Region", 342 | "SurfaceArea", 343 | "IndepYear", 344 | "Population", 345 | "LifeExpectancy", 346 | "GNP", 347 | "GNPOld", 348 | "LocalName", 349 | "GovernmentForm", 350 | "HeadOfState", 351 | "Capital", 352 | "Code2" 353 | ], 354 | "attached_condition": "(`world`.`Country`.`Code` is not null)" 355 | } 356 | }, 357 | { 358 | "table": { 359 | "table_name": "", 360 | "access_type": "eq_ref", 361 | "key": "", 362 | "key_length": "3", 363 | "ref": [ 364 | "world.Country.Code" 365 | ], 366 | "rows_examined_per_scan": 1, 367 | "materialized_from_subquery": { 368 | "using_temporary_table": true, 369 | "query_block": { 370 | "table": { 371 | "table_name": "CountryLanguage", 372 | "access_type": "ALL", 373 | "possible_keys": [ 374 | "PRIMARY", 375 | "CountryCode" 376 | ], 377 | "rows_examined_per_scan": 984, 378 | "rows_produced_per_join": 492, 379 | "filtered": "50.00", 380 | "cost_info": { 381 | "read_cost": "104.40", 382 | "eval_cost": "98.40", 383 | "prefix_cost": "202.80", 384 | "data_read_per_join": "19K" 385 | }, 386 | "used_columns": [ 387 | "CountryCode", 388 | "IsOfficial" 389 | ], 390 | "attached_condition": "(`world`.`CountryLanguage`.`IsOfficial` = 1)" 391 | } 392 | } 393 | } 394 | } 395 | } 396 | ] 397 | } 398 | } 399 | ``` 400 | 401 | ### 例17: サブクエリーの準結合戦略を分析する 402 | 403 | ```sql 404 | SET OPTIMIZER_TRACE="enabled=on"; 405 | SET optimizer_trace_max_mem_size = 1024 * 1024; 406 | SELECT * FROM Country WHERE Code IN (SELECT CountryCode FROM CountryLanguage WHERE isOfficial=1); 407 | SELECT * FROM information_schema.optimizer_trace; 408 | { 409 | "steps": [ 410 | { 411 | "join_preparation": { 412 | "select#": 1, 413 | "steps": [ 414 | { 415 | "join_preparation": { 416 | "select#": 2, 417 | "steps": [ 418 | { 419 | "expanded_query": "/* select#2 */ select `CountryLanguage`.`CountryCode` from `CountryLanguage` where (`CountryLanguage`.`IsOfficial` = 1)" 420 | }, 421 | { 422 | "transformation": { 423 | "select#": 2, 424 | "from": "IN (SELECT)", 425 | "to": "semijoin", 426 | "chosen": true 427 | } 428 | } 429 | ] 430 | } 431 | }, 432 | { 433 | "expanded_query": "/* select#1 */ select `Country`.`Code` AS `Code`,`Country`.`Name` AS `Name`,`Country`.`Continent` AS `Continent`,`Country`.`Region` AS `Region`,`Country`.`SurfaceArea` AS `SurfaceArea`,`Country`.`IndepYear` AS `IndepYear`,`Country`.`Population` AS `Population`,`Country`.`LifeExpectancy` AS `LifeExpectancy`,`Country`.`GNP` AS `GNP`,`Country`.`GNPOld` AS `GNPOld`,`Country`.`LocalName` AS `LocalName`,`Country`.`GovernmentForm` AS `GovernmentForm`,`Country`.`HeadOfState` AS `HeadOfState`,`Country`.`Capital` AS `Capital`,`Country`.`Code2` AS `Code2` from `Country` where `Country`.`Code` in (/* select#2 */ select `CountryLanguage`.`CountryCode` from `CountryLanguage` where (`CountryLanguage`.`IsOfficial` = 1))" 434 | }, 435 | { 436 | "transformation": { 437 | "select#": 2, 438 | "from": "IN (SELECT)", 439 | "to": "semijoin", 440 | "chosen": true, 441 | "evaluating_constant_semijoin_conditions": [ 442 | ] 443 | } 444 | }, 445 | { 446 | "transformations_to_nested_joins": { 447 | "transformations": [ 448 | "semijoin" 449 | ], 450 | "expanded_query": "/* select#1 */ select `Country`.`Code` AS `Code`,`Country`.`Name` AS `Name`,`Country`.`Continent` AS `Continent`,`Country`.`Region` AS `Region`,`Country`.`SurfaceArea` AS `SurfaceArea`,`Country`.`IndepYear` AS `IndepYear`,`Country`.`Population` AS `Population`,`Country`.`LifeExpectancy` AS `LifeExpectancy`,`Country`.`GNP` AS `GNP`,`Country`.`GNPOld` AS `GNPOld`,`Country`.`LocalName` AS `LocalName`,`Country`.`GovernmentForm` AS `GovernmentForm`,`Country`.`HeadOfState` AS `HeadOfState`,`Country`.`Capital` AS `Capital`,`Country`.`Code2` AS `Code2` from `Country` semi join (`CountryLanguage`) where (1 and (`CountryLanguage`.`IsOfficial` = 1) and (`Country`.`Code` = `CountryLanguage`.`CountryCode`))" 451 | } 452 | } 453 | ] 454 | } 455 | }, 456 | { 457 | "join_optimization": { 458 | "select#": 1, 459 | "steps": [ 460 | { 461 | "condition_processing": { 462 | "condition": "WHERE", 463 | "original_condition": "(1 and (`CountryLanguage`.`IsOfficial` = 1) and (`Country`.`Code` = `CountryLanguage`.`CountryCode`))", 464 | "steps": [ 465 | { 466 | "transformation": "equality_propagation", 467 | "resulting_condition": "(1 and (`CountryLanguage`.`IsOfficial` = 1) and multiple equal(`Country`.`Code`, `CountryLanguage`.`CountryCode`))" 468 | }, 469 | { 470 | "transformation": "constant_propagation", 471 | "resulting_condition": "(1 and (`CountryLanguage`.`IsOfficial` = 1) and multiple equal(`Country`.`Code`, `CountryLanguage`.`CountryCode`))" 472 | }, 473 | { 474 | "transformation": "trivial_condition_removal", 475 | "resulting_condition": "((`CountryLanguage`.`IsOfficial` = 1) and multiple equal(`Country`.`Code`, `CountryLanguage`.`CountryCode`))" 476 | } 477 | ] 478 | } 479 | }, 480 | { 481 | "substitute_generated_columns": { 482 | } 483 | }, 484 | { 485 | "table_dependencies": [ 486 | { 487 | "table": "`Country`", 488 | "row_may_be_null": false, 489 | "map_bit": 0, 490 | "depends_on_map_bits": [ 491 | ] 492 | }, 493 | { 494 | "table": "`CountryLanguage`", 495 | "row_may_be_null": false, 496 | "map_bit": 1, 497 | "depends_on_map_bits": [ 498 | ] 499 | } 500 | ] 501 | }, 502 | { 503 | "ref_optimizer_key_uses": [ 504 | { 505 | "table": "`Country`", 506 | "field": "Code", 507 | "equals": "`CountryLanguage`.`CountryCode`", 508 | "null_rejecting": false 509 | }, 510 | { 511 | "table": "`CountryLanguage`", 512 | "field": "CountryCode", 513 | "equals": "`Country`.`Code`", 514 | "null_rejecting": false 515 | }, 516 | { 517 | "table": "`CountryLanguage`", 518 | "field": "CountryCode", 519 | "equals": "`Country`.`Code`", 520 | "null_rejecting": false 521 | } 522 | ] 523 | }, 524 | { 525 | "pulled_out_semijoin_tables": [ 526 | ] 527 | }, 528 | { 529 | "rows_estimation": [ 530 | { 531 | "table": "`Country`", 532 | "table_scan": { 533 | "rows": 239, 534 | "cost": 9 535 | } 536 | }, 537 | { 538 | "table": "`CountryLanguage`", 539 | "table_scan": { 540 | "rows": 984, 541 | "cost": 6 542 | } 543 | } 544 | ] 545 | }, 546 | { 547 | "execution_plan_for_potential_materialization": { 548 | "steps": [ 549 | { 550 | "considered_execution_plans": [ 551 | { 552 | "plan_prefix": [ 553 | ], 554 | "table": "`CountryLanguage`", 555 | "best_access_path": { 556 | "considered_access_paths": [ 557 | { 558 | "access_type": "ref", 559 | "index": "PRIMARY", 560 | "usable": false, 561 | "chosen": false 562 | }, 563 | { 564 | "access_type": "ref", 565 | "index": "CountryCode", 566 | "usable": false, 567 | "chosen": false 568 | }, 569 | { 570 | "rows_to_scan": 984, 571 | "access_type": "scan", 572 | "resulting_rows": 492, 573 | "cost": 202.8, 574 | "chosen": true 575 | } 576 | ] 577 | }, 578 | "condition_filtering_pct": 100, 579 | "rows_for_plan": 492, 580 | "cost_for_plan": 202.8, 581 | "chosen": true 582 | } 583 | ] 584 | } 585 | ] 586 | } 587 | }, 588 | { 589 | "considered_execution_plans": [ 590 | { 591 | "plan_prefix": [ 592 | ], 593 | "table": "`Country`", 594 | "best_access_path": { 595 | "considered_access_paths": [ 596 | { 597 | "access_type": "ref", 598 | "index": "PRIMARY", 599 | "usable": false, 600 | "chosen": false 601 | }, 602 | { 603 | "rows_to_scan": 239, 604 | "access_type": "scan", 605 | "resulting_rows": 239, 606 | "cost": 56.8, 607 | "chosen": true 608 | } 609 | ] 610 | }, 611 | "condition_filtering_pct": 100, 612 | "rows_for_plan": 239, 613 | "cost_for_plan": 56.8, 614 | "semijoin_strategy_choice": [ 615 | ], 616 | "rest_of_plan": [ 617 | { 618 | "plan_prefix": [ 619 | "`Country`" 620 | ], 621 | "table": "`CountryLanguage`", 622 | "best_access_path": { 623 | "considered_access_paths": [ 624 | { 625 | "access_type": "ref", 626 | "index": "PRIMARY", 627 | "rows": 4.2232, 628 | "cost": 442.83, 629 | "chosen": true 630 | }, 631 | { 632 | "access_type": "ref", 633 | "index": "CountryCode", 634 | "rows": 4.2232, 635 | "cost": 1211.2, 636 | "chosen": false 637 | }, 638 | { 639 | "rows_to_scan": 984, 640 | "access_type": "scan", 641 | "using_join_cache": true, 642 | "buffers_needed": 1, 643 | "resulting_rows": 492, 644 | "cost": 23647, 645 | "chosen": false 646 | } 647 | ] 648 | }, 649 | "condition_filtering_pct": 50, 650 | "rows_for_plan": 504.67, 651 | "cost_for_plan": 499.63, 652 | "semijoin_strategy_choice": [ 653 | { 654 | "strategy": "FirstMatch", 655 | "recalculate_access_paths_and_cost": { 656 | "tables": [ 657 | ] 658 | }, 659 | "cost": 499.63, 660 | "rows": 239, 661 | "chosen": true 662 | }, 663 | { 664 | "strategy": "MaterializeLookup", 665 | "cost": 407.8, # 実体化が最低コスト 666 | "rows": 239, 667 | "duplicate_tables_left": false, 668 | "chosen": true 669 | }, 670 | { 671 | "strategy": "DuplicatesWeedout", 672 | "cost": 650.36, 673 | "rows": 239, 674 | "duplicate_tables_left": false, 675 | "chosen": false 676 | } 677 | ], 678 | "chosen": true 679 | } 680 | ] 681 | }, 682 | { 683 | "plan_prefix": [ 684 | ], 685 | "table": "`CountryLanguage`", 686 | "best_access_path": { 687 | "considered_access_paths": [ 688 | { 689 | "access_type": "ref", 690 | "index": "PRIMARY", 691 | "usable": false, 692 | "chosen": false 693 | }, 694 | { 695 | "access_type": "ref", 696 | "index": "CountryCode", 697 | "usable": false, 698 | "chosen": false 699 | }, 700 | { 701 | "rows_to_scan": 984, 702 | "access_type": "scan", 703 | "resulting_rows": 492, 704 | "cost": 202.8, 705 | "chosen": true 706 | } 707 | ] 708 | }, 709 | "condition_filtering_pct": 100, 710 | "rows_for_plan": 492, 711 | "cost_for_plan": 202.8, 712 | "semijoin_strategy_choice": [ 713 | { 714 | "strategy": "MaterializeScan", 715 | "choice": "deferred" 716 | } 717 | ], 718 | "rest_of_plan": [ 719 | { 720 | "plan_prefix": [ 721 | "`CountryLanguage`" 722 | ], 723 | "table": "`Country`", 724 | "best_access_path": { 725 | "considered_access_paths": [ 726 | { 727 | "access_type": "ref", 728 | "index": "PRIMARY", 729 | "rows": 1, 730 | "cost": 590.4, 731 | "chosen": true 732 | }, 733 | { 734 | "rows_to_scan": 239, 735 | "access_type": "scan", 736 | "using_join_cache": true, 737 | "buffers_needed": 1, 738 | "resulting_rows": 239, 739 | "cost": 23527, 740 | "chosen": false 741 | } 742 | ] 743 | }, 744 | "condition_filtering_pct": 100, 745 | "rows_for_plan": 492, 746 | "cost_for_plan": 793.2, 747 | "semijoin_strategy_choice": [ 748 | { 749 | "strategy": "LooseScan", 750 | "recalculate_access_paths_and_cost": { 751 | "tables": [ 752 | { 753 | "table": "`CountryLanguage`", 754 | "best_access_path": { 755 | "considered_access_paths": [ 756 | { 757 | "access_type": "ref", 758 | "index": "PRIMARY", 759 | "usable": false, 760 | "chosen": false 761 | }, 762 | { 763 | "access_type": "ref", 764 | "index": "CountryCode", 765 | "usable": false, 766 | "chosen": false 767 | }, 768 | { 769 | "rows_to_scan": 984, 770 | "access_type": "scan", 771 | "resulting_rows": 492, 772 | "cost": 202.8, 773 | "chosen": true 774 | } 775 | ] 776 | }, 777 | "unknown_key_1": { 778 | "searching_loose_scan_index": { 779 | "indexes": [ 780 | { 781 | "index": "PRIMARY", 782 | "ref_possible": false, 783 | "covering_scan_possible": false 784 | }, 785 | { 786 | "index": "CountryCode", 787 | "ref_possible": false, 788 | "covering_scan_possible": false 789 | } 790 | ] 791 | } 792 | } 793 | } 794 | ] 795 | }, 796 | "chosen": false 797 | }, 798 | { 799 | "strategy": "MaterializeScan", 800 | "recalculate_access_paths_and_cost": { 801 | "tables": [ 802 | { 803 | "table": "`Country`", 804 | "best_access_path": { 805 | "considered_access_paths": [ 806 | { 807 | "access_type": "ref", 808 | "index": "PRIMARY", 809 | "rows": 1, 810 | "cost": 590.4, 811 | "chosen": true 812 | }, 813 | { 814 | "rows_to_scan": 239, 815 | "access_type": "scan", 816 | "using_join_cache": true, 817 | "buffers_needed": 1, 818 | "resulting_rows": 239, 819 | "cost": 23527, 820 | "chosen": false 821 | } 822 | ] 823 | } 824 | } 825 | ] 826 | }, 827 | "cost": 992, 828 | "rows": 1, 829 | "duplicate_tables_left": true, 830 | "chosen": true 831 | }, 832 | { 833 | "strategy": "DuplicatesWeedout", 834 | "cost": 941.4, 835 | "rows": 239, 836 | "duplicate_tables_left": false, 837 | "chosen": true 838 | } 839 | ], 840 | "pruned_by_cost": true 841 | } 842 | ] 843 | }, 844 | { # 実体化が最終的に選択される 845 | "final_semijoin_strategy": "MaterializeLookup" 846 | } 847 | ] 848 | }, 849 | { 850 | "creating_tmp_table": { 851 | "tmp_table_info": { 852 | "row_length": 4, 853 | "key_length": 3, 854 | "unique_constraint": false, 855 | "location": "memory (heap)", 856 | "row_limit_estimate": 4194304 857 | } 858 | } 859 | }, 860 | { 861 | "attaching_conditions_to_tables": { 862 | "original_condition": "((``.`CountryCode` = `Country`.`Code`) and (`CountryLanguage`.`IsOfficial` = 1))", 863 | "attached_conditions_computation": [ 864 | { 865 | "table": "`CountryLanguage`", 866 | "rechecking_index_usage": { 867 | "recheck_reason": "not_first_table", 868 | "range_analysis": { 869 | "table_scan": { 870 | "rows": 984, 871 | "cost": 204.9 872 | }, 873 | "potential_range_indexes": [ 874 | { 875 | "index": "PRIMARY", 876 | "usable": true, 877 | "key_parts": [ 878 | "CountryCode", 879 | "Language" 880 | ] 881 | }, 882 | { 883 | "index": "CountryCode", 884 | "usable": true, 885 | "key_parts": [ 886 | "CountryCode", 887 | "Language" 888 | ] 889 | } 890 | ], 891 | "setup_range_conditions": [ 892 | ], 893 | "group_index_range": { 894 | "chosen": false, 895 | "cause": "not_single_table" 896 | } 897 | } 898 | } 899 | } 900 | ], 901 | "attached_conditions_summary": [ 902 | { 903 | "table": "`Country`", 904 | "attached": "(`Country`.`Code` is not null)" 905 | }, 906 | { 907 | "table": "``.``", 908 | "attached": null 909 | }, 910 | { 911 | "table": "`CountryLanguage`", 912 | "attached": "(`CountryLanguage`.`IsOfficial` = 1)" 913 | } 914 | ] 915 | } 916 | }, 917 | { 918 | "refine_plan": [ 919 | { 920 | "table": "`Country`" 921 | }, 922 | { 923 | "table": "``.``" 924 | }, 925 | { 926 | "table": "`CountryLanguage`" 927 | } 928 | ] 929 | } 930 | ] 931 | } 932 | }, 933 | { 934 | "join_execution": { 935 | "select#": 1, 936 | "steps": [ 937 | ] 938 | } 939 | } 940 | ] 941 | } 942 | ``` 943 | 944 | ## NOT INサブクエリー 945 | 946 | NOT INサブクエリーは実体化、あるいはEXISTS戦略をとることができます。2つの違いをみるために、次の2つの例について考えてみましょう。 947 | 948 | 1. `SELECT * FROM City WHERE CountryCode NOT IN (SELECT code FROM Country);` 949 | 2. `SELECT * FROM City WHERE CountryCode NOT IN (SELECT code FROM Country WHERE continent IN ('Asia', 'Europe', 'North America'));` 950 | 951 | 最初のクエリーでは、内部クエリーは最適な形です。code列はテーブルの主キーで、インデックススキャンで重複のないユニーク値が処理されます。唯一の否定的な側面は(もしあるとすれば)、主キーのインデックススキャンは、主キーは行の他の列の値も保持しているため、セカンダリキーのインデックススキャンより遅いかもしれないことです。`EXPLAIN`を実行すれば、オプティマイザはこのクエリーに対しては実体化戦略を選択したことがわかりますが、次のヒントによって上書きすることもできます。 952 | 953 | ```sql 954 | SELECT * FROM City WHERE CountryCode NOT IN (SELECT /*+ SUBQUERY(INTOEXISTS) */ code FROM Country); 955 | ``` 956 | 957 | 2番目のクエリーには、追加の述部(`continent IN ('Asia', 'Europe', 'North America')`)があります。`City`テーブルの各行をNOT INクエリーと比較する必要があるため、一致した行の一覧を実体化しキャッシュすることに合理性がうまれます。 958 | 959 | ![explain-example-18](http://www.unofficialmysqlguide.com/_images/explain-example-18.png) 960 | 961 | ### 実体化を利用するNOT INサブクエリー 962 | 963 | ```sql 964 | EXPLAIN FORMAT=JSON 965 | SELECT * FROM City WHERE CountryCode NOT IN (SELECT code FROM Country WHERE continent IN ('Asia', 'Europe', 'North America')); 966 | { 967 | "query_block": { 968 | "select_id": 1, 969 | "cost_info": { 970 | "query_cost": "862.60" 971 | }, 972 | "table": { 973 | "table_name": "City", 974 | "access_type": "ALL", 975 | "rows_examined_per_scan": 4188, 976 | "rows_produced_per_join": 4188, 977 | "filtered": "100.00", 978 | "cost_info": { 979 | "read_cost": "25.00", 980 | "eval_cost": "837.60", 981 | "prefix_cost": "862.60", 982 | "data_read_per_join": "294K" 983 | }, 984 | "used_columns": [ 985 | "ID", 986 | "Name", 987 | "CountryCode", 988 | "District", 989 | "Population" 990 | ], 991 | "attached_condition": "(not((`world`.`City`.`CountryCode`,`world`.`City`.`CountryCode` in ( (/* select#2 */ select `world`.`Country`.`Code` from `world`.`Country` where (`world`.`Country`.`Continent` in ('Asia','Europe','North America')) ), (`world`.`City`.`CountryCode` in on where ((`world`.`City`.`CountryCode` = `materialized-subquery`.`code`)))))))", 992 | "attached_subqueries": [ 993 | { 994 | "table": { 995 | "table_name": "", 996 | "access_type": "eq_ref", 997 | "key": "", 998 | "key_length": "3", 999 | "rows_examined_per_scan": 1, 1000 | "materialized_from_subquery": { 1001 | "using_temporary_table": true, 1002 | "dependent": true, 1003 | "cacheable": false, 1004 | "query_block": { 1005 | "select_id": 2, 1006 | "cost_info": { 1007 | "query_cost": "54.67" 1008 | }, 1009 | "table": { 1010 | "table_name": "Country", 1011 | "access_type": "range", 1012 | "possible_keys": [ 1013 | "PRIMARY", 1014 | "c", 1015 | "c_p" 1016 | ], 1017 | "key": "c", 1018 | "used_key_parts": [ 1019 | "Continent" 1020 | ], 1021 | "key_length": "1", 1022 | "rows_examined_per_scan": 134, 1023 | "rows_produced_per_join": 134, 1024 | "filtered": "100.00", 1025 | "using_index": true, 1026 | "cost_info": { 1027 | "read_cost": "27.87", 1028 | "eval_cost": "26.80", 1029 | "prefix_cost": "54.67", 1030 | "data_read_per_join": "34K" 1031 | }, 1032 | "used_columns": [ 1033 | "Code", 1034 | "Continent" 1035 | ], 1036 | "attached_condition": "(`world`.`Country`.`Continent` in ('Asia','Europe','North America'))" 1037 | } 1038 | } 1039 | } 1040 | } 1041 | } 1042 | ] 1043 | } 1044 | } 1045 | } 1046 | ``` 1047 | 1048 | ## 派生テーブル(Derived Table) 1049 | 1050 | from句内のサブクエリーは実体化する必要はありません。MySQLは通例、ビューが自身の定義をクエリーで利用する述部にマージするのと同じように、from句内のサブクエリーを「マージ」し直します。例19により、派生クエリーが外部クエリーにマージされているのがわかります。 1051 | 1052 | ![explain-example-19](http://www.unofficialmysqlguide.com/_images/explain-example-19.png) 1053 | 1054 | ### 例19: 派生テーブルが「マージ」し直される 1055 | 1056 | ```sql 1057 | EXPLAIN FORMAT=JSON 1058 | SELECT * FROM Country, (SELECT * FROM City WHERE CountryCode ='CAN' ) as CityTmp WHERE Country.code=CityTmp.CountryCode 1059 | AND CityTmp.name ='Toronto'; 1060 | { 1061 | "query_block": { 1062 | "select_id": 1, 1063 | "cost_info": { 1064 | "query_cost": "2.00" 1065 | }, 1066 | "nested_loop": [ 1067 | { 1068 | "table": { 1069 | "table_name": "Country", 1070 | "access_type": "const", 1071 | "possible_keys": [ 1072 | "PRIMARY" 1073 | ], 1074 | "key": "PRIMARY", 1075 | "used_key_parts": [ 1076 | "Code" 1077 | ], 1078 | "key_length": "3", 1079 | "ref": [ 1080 | "const" 1081 | ], 1082 | "rows_examined_per_scan": 1, 1083 | "rows_produced_per_join": 1, 1084 | "filtered": "100.00", 1085 | "cost_info": { 1086 | "read_cost": "0.00", 1087 | "eval_cost": "1.00", 1088 | "prefix_cost": "0.00", 1089 | "data_read_per_join": "264" 1090 | }, 1091 | "used_columns": [ 1092 | "Code", 1093 | "Name", 1094 | "Continent", 1095 | "Region", 1096 | "SurfaceArea", 1097 | "IndepYear", 1098 | "Population", 1099 | "LifeExpectancy", 1100 | "GNP", 1101 | "GNPOld", 1102 | "LocalName", 1103 | "GovernmentForm", 1104 | "HeadOfState", 1105 | "Capital", 1106 | "Code2" 1107 | ] 1108 | } 1109 | }, 1110 | { 1111 | "table": { 1112 | "table_name": "City", 1113 | "access_type": "ref", 1114 | "possible_keys": [ 1115 | "CountryCode", 1116 | "n" 1117 | ], 1118 | "key": "n", 1119 | "used_key_parts": [ 1120 | "Name" 1121 | ], 1122 | "key_length": "35", 1123 | "ref": [ 1124 | "const" 1125 | ], 1126 | "rows_examined_per_scan": 1, 1127 | "rows_produced_per_join": 0, 1128 | "filtered": "5.00", 1129 | "cost_info": { 1130 | "read_cost": "1.00", 1131 | "eval_cost": "0.05", 1132 | "prefix_cost": "2.00", 1133 | "data_read_per_join": "3" 1134 | }, 1135 | "used_columns": [ 1136 | "ID", 1137 | "Name", 1138 | "CountryCode", 1139 | "District", 1140 | "Population" 1141 | ], 1142 | "attached_condition": "(`world`.`City`.`CountryCode` = 'CAN')" 1143 | } 1144 | } 1145 | ] 1146 | } 1147 | } 1148 | ``` 1149 | 1150 | この「マージ」の否定的な側面はいくつかのクエリーが正当(legal)なものでなくなることです。 1151 | MySQLサーバーをアップグレードした際にwarningが発生する場合は、`derived_merge`最適化を無効化することができます。実体化は高コストとなりうるため、クエリーのコストが結果的に増えることになることもあるでしょう。 1152 | 1153 | 1154 | ![explain-example-20](http://www.unofficialmysqlguide.com/_images/explain-example-20.png) 1155 | 1156 | ### 例20: マージを無効化し実体化させる 1157 | 1158 | ```sql 1159 | SET optimizer_switch='derived_merge=off'; 1160 | EXPLAIN FORMAT=JSON 1161 | SELECT * FROM Country, (SELECT * FROM City WHERE CountryCode ='CAN' ) as CityTmp WHERE Country.code=CityTmp.CountryCode 1162 | AND CityTmp.name ='Toronto'; 1163 | { 1164 | "query_block": { 1165 | "select_id": 1, 1166 | "cost_info": { 1167 | "query_cost": "19.90" 1168 | }, 1169 | "nested_loop": [ 1170 | { 1171 | "table": { 1172 | "table_name": "CityTmp", 1173 | "access_type": "ref", 1174 | "possible_keys": [ 1175 | "" 1176 | ], 1177 | "key": "", 1178 | "used_key_parts": [ 1179 | "Name" 1180 | ], 1181 | "key_length": "35", 1182 | "ref": [ 1183 | "const" 1184 | ], 1185 | "rows_examined_per_scan": 5, 1186 | "rows_produced_per_join": 5, 1187 | "filtered": "100.00", 1188 | "cost_info": { 1189 | "read_cost": "4.90", 1190 | "eval_cost": "5.00", 1191 | "prefix_cost": "9.90", 1192 | "data_read_per_join": "360" 1193 | }, 1194 | "used_columns": [ 1195 | "ID", 1196 | "Name", 1197 | "CountryCode", 1198 | "District", 1199 | "Population" 1200 | ], 1201 | "materialized_from_subquery": { 1202 | "using_temporary_table": true, 1203 | "dependent": false, 1204 | "cacheable": true, 1205 | "query_block": { 1206 | "select_id": 2, 1207 | "cost_info": { 1208 | "query_cost": "98.00" 1209 | }, 1210 | "table": { 1211 | "table_name": "City", 1212 | "access_type": "ref", 1213 | "possible_keys": [ 1214 | "CountryCode" 1215 | ], 1216 | "key": "CountryCode", 1217 | "used_key_parts": [ 1218 | "CountryCode" 1219 | ], 1220 | "key_length": "3", 1221 | "ref": [ 1222 | "const" 1223 | ], 1224 | "rows_examined_per_scan": 49, 1225 | "rows_produced_per_join": 49, 1226 | "filtered": "100.00", 1227 | "cost_info": { 1228 | "read_cost": "49.00", 1229 | "eval_cost": "49.00", 1230 | "prefix_cost": "98.00", 1231 | "data_read_per_join": "3K" 1232 | }, 1233 | "used_columns": [ 1234 | "ID", 1235 | "Name", 1236 | "CountryCode", 1237 | "District", 1238 | "Population" 1239 | ] 1240 | } 1241 | } 1242 | } 1243 | } 1244 | }, 1245 | { 1246 | "table": { 1247 | "table_name": "Country", 1248 | "access_type": "eq_ref", 1249 | "possible_keys": [ 1250 | "PRIMARY" 1251 | ], 1252 | "key": "PRIMARY", 1253 | "used_key_parts": [ 1254 | "Code" 1255 | ], 1256 | "key_length": "3", 1257 | "ref": [ 1258 | "CityTmp.CountryCode" 1259 | ], 1260 | "rows_examined_per_scan": 1, 1261 | "rows_produced_per_join": 5, 1262 | "filtered": "100.00", 1263 | "cost_info": { 1264 | "read_cost": "5.00", 1265 | "eval_cost": "5.00", 1266 | "prefix_cost": "19.90", 1267 | "data_read_per_join": "1K" 1268 | }, 1269 | "used_columns": [ 1270 | "Code", 1271 | "Name", 1272 | "Continent", 1273 | "Region", 1274 | "SurfaceArea", 1275 | "IndepYear", 1276 | "Population", 1277 | "LifeExpectancy", 1278 | "GNP", 1279 | "GNPOld", 1280 | "LocalName", 1281 | "GovernmentForm", 1282 | "HeadOfState", 1283 | "Capital", 1284 | "Code2" 1285 | ] 1286 | } 1287 | } 1288 | ] 1289 | } 1290 | } 1291 | ``` 1292 | -------------------------------------------------------------------------------- /_posts/2017-05-21-ctes-and-views.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 共通テーブル式(CTE)とビュー 3 | date: 2017-05-21 00:00:01 -0900 4 | article_index: 15 5 | original_url: http://www.unofficialmysqlguide.com/ctes-and-views.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | ビューはクエリーをあとで再利用するために保存する方法の1つで、アプリケーションから見るとテーブルのようにみえます。これによって複雑なSQLクエリーをブレークダウンし、段階をおうことで単純化することが出来ます。共通テーブル式(CTE)自体はビューととても似通っており、両者の違いは共通テーブル式は寿命が短く、単一のステートメントのみに限定されることです。 10 | 11 | MySQLのオプティマイザには、共通テーブル式およびビューを実行するにあたって主に2つの実行戦略があります。 12 | 13 | 1. **マージ** : 共通テーブル式またはビューの定義が残りのクエリーとマージされるようにクエリーを変換します。`EXPLAIN`を実行した後に`SHOW WARNINGS`を実行することでどのようにマージされたかを確認することができます。 14 | 2. **実体化** : 共通テーブル式またはビューを実行しその結果を一時テーブルに保存します。残りのクエリーは一時テーブルに対して実行されます。実体化は一般的には遅い手法であり、マージの手法が適さないと判断されたときに選択されます。実体化の方が高速になる例外的なケースとして、早期に実体化することで工程が短縮され実行が高速化するというケースがあります。 15 | 16 | ### 例21: ビューに対しクエリーが発行されマージされる 17 | 18 | ```sql 19 | CREATE VIEW vCountry_Asia AS SELECT * FROM Country WHERE Continent='Asia'; 20 | EXPLAIN FORMAT=JSON 21 | SELECT * FROM vCountry_Asia WHERE Name='China'; 22 | { 23 | "query_block": { 24 | "select_id": 1, 25 | "cost_info": { 26 | "query_cost": "1.20" 27 | }, 28 | "table": { 29 | "table_name": "Country", # Table name is the base table name 30 | "access_type": "ref", 31 | "possible_keys": [ 32 | "c", 33 | "c_p", 34 | "Name" 35 | ], 36 | "key": "Name", 37 | "used_key_parts": [ 38 | "Name", 39 | "Continent" 40 | ], 41 | "key_length": "53", 42 | "ref": [ 43 | "const", 44 | "const" 45 | ], 46 | "rows_examined_per_scan": 1, 47 | "rows_produced_per_join": 1, 48 | "filtered": "100.00", 49 | "cost_info": { 50 | "read_cost": "1.00", 51 | "eval_cost": "0.20", 52 | "prefix_cost": "1.20", 53 | "data_read_per_join": "264" 54 | }, 55 | "used_columns": [ 56 | "Code", 57 | "Name", 58 | "Continent", 59 | "Region", 60 | "SurfaceArea", 61 | "IndepYear", 62 | "Population", 63 | "LifeExpectancy", 64 | "GNP", 65 | "GNPOld", 66 | "LocalName", 67 | "GovernmentForm", 68 | "HeadOfState", 69 | "Capital", 70 | "Code2" 71 | ] 72 | } 73 | } 74 | } 75 | 76 | SHOW WARNINGS; 77 | /* select#1 */ select `world`.`Country`.`Code` AS `Code`,`world`.`Country`.`Name` AS `Name`,`world`.`Country`.`Continent` AS `Continent`,`world`.`Country`.`Region` AS `Region`,`world`.`Country`.`SurfaceArea` AS `SurfaceArea`,`world`.`Country`.`IndepYear` AS `IndepYear`,`world`.`Country`.`Population` AS `Population`,`world`.`Country`.`LifeExpectancy` AS `LifeExpectancy`,`world`.`Country`.`GNP` AS `GNP`,`world`.`Country`.`GNPOld` AS `GNPOld`,`world`.`Country`.`LocalName` AS `LocalName`,`world`.`Country`.`GovernmentForm` AS `GovernmentForm`,`world`.`Country`.`HeadOfState` AS `HeadOfState`,`world`.`Country`.`Capital` AS `Capital`,`world`.`Country`.`Code2` AS `Code2` from `world`.`Country` where ((`world`.`Country`.`Continent` = 'Asia') and (`world`.`Country`.`Name` = 'China')) 78 | ``` 79 | 80 | ![explain-example-22](http://www.unofficialmysqlguide.com/_images/explain-example-22.png) 81 | 82 | ### 例22: ビューに対しクエリーが発行されるがマージできない 83 | 84 | ```sql 85 | CREATE VIEW vCountrys_Per_Continent AS SELECT Continent, COUNT(*) as Count FROM Country 86 | GROUP BY Continent; 87 | EXPLAIN FORMAT=JSON 88 | SELECT * FROM vCountrys_Per_Continent WHERE Continent='Asia'; 89 | { 90 | "query_block": { 91 | "select_id": 1, 92 | "cost_info": { 93 | "query_cost": "12.47" 94 | }, 95 | "table": { 96 | "table_name": "vCountrys_Per_Continent", # Table name is the view name 97 | "access_type": "ref", 98 | "possible_keys": [ 99 | "" 100 | ], 101 | "key": "", 102 | "used_key_parts": [ 103 | "Continent" 104 | ], 105 | "key_length": "1", 106 | "ref": [ 107 | "const" 108 | ], 109 | "rows_examined_per_scan": 10, 110 | "rows_produced_per_join": 10, 111 | "filtered": "100.00", 112 | "cost_info": { 113 | "read_cost": "10.39", 114 | "eval_cost": "2.08", 115 | "prefix_cost": "12.47", 116 | "data_read_per_join": "166" 117 | }, 118 | "used_columns": [ 119 | "Continent", 120 | "Count" 121 | ], 122 | "materialized_from_subquery": { 123 | "using_temporary_table": true, 124 | "dependent": false, 125 | "cacheable": true, 126 | "query_block": { 127 | "select_id": 2, 128 | "cost_info": { 129 | "query_cost": "56.80" 130 | }, 131 | "grouping_operation": { 132 | "using_filesort": false, 133 | "table": { 134 | "table_name": "Country", 135 | "access_type": "index", 136 | "possible_keys": [ 137 | "PRIMARY", 138 | "p", 139 | "c", 140 | "p_c", 141 | "c_p", 142 | "Name" 143 | ], 144 | "key": "c", 145 | "used_key_parts": [ 146 | "Continent" 147 | ], 148 | "key_length": "1", 149 | "rows_examined_per_scan": 239, 150 | "rows_produced_per_join": 239, 151 | "filtered": "100.00", 152 | "using_index": true, 153 | "cost_info": { 154 | "read_cost": "9.00", 155 | "eval_cost": "47.80", 156 | "prefix_cost": "56.80", 157 | "data_read_per_join": "61K" 158 | }, 159 | "used_columns": [ 160 | "Code", 161 | "Continent" 162 | ] 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | 171 | SHOW WARNINGS; 172 | /* select#1 */ select `vCountrys_Per_Continent`.`Continent` AS `Continent`,`vCountrys_Per_Continent`.`Count` AS `Count` from `world`.`vCountrys_Per_Continent` where (`vCountrys_Per_Continent`.`Continent` = 'Asia') 173 | ``` 174 | 175 | -------------------------------------------------------------------------------- /_posts/2017-06-12-joins.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 結合 3 | date: 2017-06-12 00:00:01 -0900 4 | article_index: 16 5 | original_url: http://www.unofficialmysqlguide.com/joins.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQLは、Nested Loop アルゴリズムを使用して結合(join)を実行します。MySQLは競合となるデータベースで利用可能なハッシュあるいはソートマージといったアルゴリズムはサポートしませんので、いくつかの分析/データウェアハウスのクエリーには適しません。しかしながら、MySQLのオプティマイザには、Nested Loopアルゴリズムの最悪のケースを緩和するためのいくつかのバッファリング戦略があります。 10 | 11 | ![explain-example-23](http://www.unofficialmysqlguide.com/_images/explain-example-23.png) 12 | 13 | ## Nested Loop結合 14 | 15 | 例23は、`Country`、`City`、`CountryLanguage`の3テーブルの結合例です。このクエリーを実行する全工程は次のようになります。 16 | 17 | 1. オプティマイザは最初に駆動表(`Country`)を決定し、同時にその他のテーブル(`City`、`CountryLanguage`)結合に利用するインデックスを決めなければなりません。 18 | 2. 実行は最初のテーブル(`Country`)から始まり、1度に1行ずつ確認します。フィルター条件(`Country.Continent='Asia'`)を満たす各行に対して、次のテーブル(`City`)内の検索が行われます。 19 | 3. `City`テーブル内の条件に一致した各行に対して、最後のテーブル(`CountryLanguage`)内の検索が行われます。`CountryLanguage`テーブルでは追加の条件(`IsOfficial=1`)が適用されます。 20 | 21 | Nested Loopアルゴリズムは、結合を開始する前に処理を省くことができれば一番うまく動作します。つまり「最善のケース」は駆動表に選択度の高い述部があるときである、といえます。 22 | 23 | 述部が複数のテーブルに分散しており、全てのテーブルが結合される必要がうまれる前にインデックスが処理を十分に取り除けない場合、最悪のケースとなりえます。このような状況では、通例*スキーマの非正規化*をおこないます。 24 | 25 | 駆動表に冗長に保持している列を付与し非正規化することにより、複合インデックスが追加可能となり、他のテーブルにJOINしたりアクセスする前にフィルタリング可能となります。 26 | 27 | ### 例23: 3テーブルに対するNested Loop結合 28 | 29 | ```sql 30 | EXPLAIN FORMAT=JSON 31 | SELECT 32 | Country.Name as Country, City.Name as Capital, Language 33 | FROM 34 | City 35 | INNER JOIN Country ON Country.Capital=City.id 36 | INNER JOIN CountryLanguage ON CountryLanguage.CountryCode=Country.code 37 | WHERE 38 | Country.Continent='Asia' and CountryLanguage.IsOfficial='T'; 39 | { 40 | "query_block": { 41 | "select_id": 1, 42 | "cost_info": { 43 | "query_cost": "3.42" 44 | }, 45 | "nested_loop": [ 46 | { 47 | "table": { 48 | "table_name": "Country", 49 | "access_type": "const", 50 | "possible_keys": [ 51 | "PRIMARY" 52 | ], 53 | "key": "PRIMARY", 54 | "used_key_parts": [ 55 | "Code" 56 | ], 57 | "key_length": "3", 58 | "ref": [ 59 | "const" 60 | ], 61 | "rows_examined_per_scan": 1, 62 | "rows_produced_per_join": 1, 63 | "filtered": "100.00", 64 | "cost_info": { 65 | "read_cost": "0.00", 66 | "eval_cost": "0.20", 67 | "prefix_cost": "0.00", 68 | "data_read_per_join": "264" 69 | }, 70 | "used_columns": [ 71 | "Code", 72 | "Name", 73 | "Capital" 74 | ] 75 | } 76 | }, 77 | { 78 | "table": { 79 | "table_name": "City", 80 | "access_type": "const", 81 | "possible_keys": [ 82 | "PRIMARY" 83 | ], 84 | "key": "PRIMARY", 85 | "used_key_parts": [ 86 | "ID" 87 | ], 88 | "key_length": "4", 89 | "ref": [ 90 | "const" 91 | ], 92 | "rows_examined_per_scan": 1, 93 | "rows_produced_per_join": 1, 94 | "filtered": "100.00", 95 | "cost_info": { 96 | "read_cost": "0.00", 97 | "eval_cost": "0.20", 98 | "prefix_cost": "0.00", 99 | "data_read_per_join": "72" 100 | }, 101 | "used_columns": [ 102 | "ID", 103 | "Name" 104 | ] 105 | } 106 | }, 107 | { 108 | "table": { 109 | "table_name": "CountryLanguage", 110 | "access_type": "ref", 111 | "possible_keys": [ 112 | "PRIMARY", 113 | "CountryCode" 114 | ], 115 | "key": "PRIMARY", 116 | "used_key_parts": [ 117 | "CountryCode" 118 | ], 119 | "key_length": "3", 120 | "ref": [ 121 | "const" 122 | ], 123 | "rows_examined_per_scan": 12, 124 | "rows_produced_per_join": 6, 125 | "filtered": "50.00", 126 | "cost_info": { 127 | "read_cost": "1.02", 128 | "eval_cost": "1.20", 129 | "prefix_cost": "3.42", 130 | "data_read_per_join": "240" 131 | }, 132 | "used_columns": [ 133 | "CountryCode", 134 | "Language", 135 | "IsOfficial" 136 | ], 137 | "attached_condition": "(`world`.`CountryLanguage`.`IsOfficial` = 'T')" 138 | } 139 | } 140 | ] 141 | } 142 | } 143 | ``` 144 | 145 | ## INNER JOIN 146 | 147 | `INNER JOIN`はその意味上からある行が結合テーブルの左、右両方にある必要があります。これを念頭に置くと、MySQLは2つのテーブルをどちらの順にでも結合できることを意味します。オプティマイザにはコストが最小となる順序を選択する役割があります。 148 | 149 | ## LEFT JOINおよびRIGHT JOIN 150 | 151 | `LEFT JOIN`は結合の右側の行が任意であり必須ではありません(すなわち`RIGHT JOIN`は左側が必須ではありません)。結合において片側が必須ではないため、データが存在する方から実行を開始します。結合順が決められるため、オプティマイザは`INNER JOIN`の際には考慮していたとりうる順序(実行計画)を考慮することができません。結果的に`LEFT JOIN`は比較的遅いということにつながります。 152 | 153 | ## Condition Fanout Filter 154 | 155 | MySQL 5.7から、オプティマイザは駆動表以外の絞込みの効果を考慮しています。`condition_filtering`は3つめ以降のテーブルの結合順をより良くするために利用されます。 156 | 157 | condition filterはヒストグラムに適しており、これを利用しないと、偏りがある、あるいは不均一なデータ分布のときに、より単純なヒューリスティックアルゴリズムで不正確な見積もりとなりえます。condition_filterはインデックスから利用できる統計(これはもうすこしコストがかかります)も利用しています。 158 | 159 | {% include info.html info="MySQL 5.7ではデータの偏りがある場合condition fanout filterにより多数の性能上のリグレッションがありました。MySQL 8.0では結合条件の考慮にヒストグラムを追加することを検討しています。" %} 160 | -------------------------------------------------------------------------------- /_posts/2017-06-25-aggregation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 集約 3 | date: 2017-06-25 00:00:01 -0900 4 | article_index: 17 5 | original_url: http://www.unofficialmysqlguide.com/aggregation.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | ## GROUP BY 10 | 11 | `GROUP BY`の操作をする際には、行がソート順に読み込まれるかあるいは一時テーブルが集約を行うための中間結果をバッファーする必要があります。言いかえれば、MySQLは`GROUP BY`で次のようにインデックスを利用することができます。 12 | 13 | 1. **ルースインデックススキャン**: `GROUP BY`条件にインデックスが付与されていれば、MySQLはインデックスを最初から最後までスキャンすることを選択し、中間的な結果の実体化を回避します。この操作は「述部の選択性が非常に高く、作成される一時テーブルが非常に大きい」というわけではない場合には良い選択となります。 14 | ![loose-index-scan-continent-index](http://www.unofficialmysqlguide.com/_images/loose-index-scan-continent-index.png) 15 | 2. **行の抽出**: インデックスは対象行があるかどうかを判別するのに利用され、その結果は一時テーブルに保存されます。結果はこの一時テーブルで集約され、デフォルトでは`GROUP BY`条件によってソートされます。[^1] 16 | ![group-by-filtering-rows](http://www.unofficialmysqlguide.com/_images/group-by-filtering-rows.png) 17 | 3. **行の抽出とソートの両方**: インデックスを利用し行を抽出した結果が`GROUP BY`操作における正しい順序となっている場合にこの最適化が適用されます。 18 | ![group-by-filtering-and-sort](http://www.unofficialmysqlguide.com/_images/group-by-filtering-and-sort.png) 19 | 20 | ### 例24: ルースインデックススキャンを利用する`GROUP BY` 21 | 22 | ```sql 23 | EXPLAIN FORMAT=JSON 24 | SELECT count(*) as c, continent FROM Country GROUP BY continent; 25 | { 26 | "query_block": { 27 | "select_id": 1, 28 | "cost_info": { 29 | "query_cost": "56.80" 30 | }, 31 | "grouping_operation": { 32 | "using_filesort": false, # <-- 33 | "table": { 34 | "table_name": "Country", 35 | "access_type": "index", # <-- 36 | "possible_keys": [ 37 | "PRIMARY", 38 | "p", 39 | "c", 40 | "p_c", 41 | "c_p", 42 | "Name" 43 | ], 44 | "key": "c", 45 | "used_key_parts": [ 46 | "Continent" 47 | ], 48 | "key_length": "1", 49 | "rows_examined_per_scan": 239, 50 | "rows_produced_per_join": 239, 51 | "filtered": "100.00", 52 | "using_index": true, 53 | "cost_info": { 54 | "read_cost": "9.00", 55 | "eval_cost": "47.80", 56 | "prefix_cost": "56.80", 57 | "data_read_per_join": "61K" 58 | }, 59 | "used_columns": [ 60 | "Code", 61 | "Continent" 62 | ] 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### 例25: インデックスを利用後ソートをする`GROUP BY` 70 | 71 | ```sql 72 | EXPLAIN FORMAT=JSON 73 | SELECT count(*) as c, continent FROM Country WHERE population > 500000000 GROUP BY continent; 74 | { 75 | "query_block": { 76 | "select_id": 1, 77 | "cost_info": { 78 | "query_cost": "3.81" 79 | }, 80 | "grouping_operation": { 81 | "using_temporary_table": true, # <-- 82 | "using_filesort": true, # <-- 83 | "cost_info": { 84 | "sort_cost": "2.00" 85 | }, 86 | "table": { 87 | "table_name": "Country", 88 | "access_type": "range", # <-- 89 | "possible_keys": [ 90 | "PRIMARY", 91 | "p", 92 | "c", 93 | "p_c", 94 | "c_p", 95 | "Name" 96 | ], 97 | "key": "p", 98 | "used_key_parts": [ 99 | "Population" 100 | ], 101 | "key_length": "4", 102 | "rows_examined_per_scan": 2, 103 | "rows_produced_per_join": 2, 104 | "filtered": "100.00", 105 | "using_index": true, 106 | "cost_info": { 107 | "read_cost": "1.41", 108 | "eval_cost": "0.40", 109 | "prefix_cost": "1.81", 110 | "data_read_per_join": "528" 111 | }, 112 | "used_columns": [ 113 | "Code", 114 | "Continent", 115 | "Population" 116 | ], 117 | "attached_condition": "(`world`.`Country`.`Population` > 500000000)" 118 | } 119 | } 120 | } 121 | } 122 | ``` 123 | 124 | ### 例26: インデックスを行のソートと抽出に利用する`GROUP BY` 125 | 126 | ```sql 127 | EXPLAIN FORMAT=JSON 128 | SELECT count(*) as c, continent FROM Country WHERE continent='Asia' GROUP BY continent; 129 | { 130 | "query_block": { 131 | "select_id": 1, 132 | "cost_info": { 133 | "query_cost": "11.23" 134 | }, 135 | "grouping_operation": { 136 | "using_filesort": false, # <-- 137 | "table": { 138 | "table_name": "Country", 139 | "access_type": "ref", # <-- 140 | "possible_keys": [ 141 | "PRIMARY", 142 | "p", 143 | "c", 144 | "p_c", 145 | "c_p", 146 | "Name" 147 | ], 148 | "key": "c", 149 | "used_key_parts": [ 150 | "Continent" 151 | ], 152 | "key_length": "1", 153 | "ref": [ 154 | "const" 155 | ], 156 | "rows_examined_per_scan": 51, 157 | "rows_produced_per_join": 51, 158 | "filtered": "100.00", 159 | "using_index": true, 160 | "cost_info": { 161 | "read_cost": "1.03", 162 | "eval_cost": "10.20", 163 | "prefix_cost": "11.23", 164 | "data_read_per_join": "13K" 165 | }, 166 | "used_columns": [ 167 | "Continent" 168 | ] 169 | } 170 | } 171 | } 172 | } 173 | ``` 174 | 175 | ## UNION 176 | 177 | `UNION`は2つのクエリーの結果を結合し重複を削除するものですが、MySQLは`UNION`には特別な最適化は行いません。例27に示されるように、中間の一時テーブルにて重複排除が実行されます。一時テーブルは全ての`UNION`クエリーで利用されるため、コストが割当られません(コストベースの最適化はできません)。 178 | 179 | ### 簡単なUnionの例 180 | 181 | ```sql 182 | SELECT * FROM City WHERE CountryCode = 'CAN' 183 | UNION 184 | SELECT * FROM City WHERE CountryCode = 'USA' 185 | ``` 186 | 187 | ### 仮説上の最適化 188 | 189 | ```sql 190 | SELECT * FROM City WHERE CountryCode IN ('CAN', 'USA') 191 | ``` 192 | 193 | ![explain-example-29](http://www.unofficialmysqlguide.com/_images/explain-example-29.png) 194 | 195 | サブクエリーやビューでは同一テーブルへの複数アクセスが内部で単一アクセスに統合されますが、他方でMySQLは`UNION`には同様の最適化を行いません。重複行は許容しないものの`UNION`が`UNION ALL`に書き換えられるケースに関する検討もまた実施しません。すなわち、スキルのある人が手動でクエリーを修正(アプリケーション内、あるいはクエリーリライトだったり)することで性能が向上できるケースが多数あることになります。 196 | 197 | ### 例27: 一時テーブルを必要とする`UNION` 198 | 199 | ```sql 200 | EXPLAIN FORMAT=JSON 201 | SELECT * FROM City WHERE CountryCode = 'CAN' 202 | UNION 203 | SELECT * FROM City WHERE CountryCode = 'USA' 204 | { 205 | "query_block": { 206 | "union_result": { 207 | "using_temporary_table": true, # 一時テーブルが必要 208 | "table_name": "", # 2つのクエリーの結果を結合 209 | "access_type": "ALL", 210 | "query_specifications": [ 211 | { 212 | "dependent": false, 213 | "cacheable": true, 214 | "query_block": { 215 | "select_id": 1, 216 | "cost_info": { 217 | "query_cost": "58.80" 218 | }, 219 | "table": { 220 | "table_name": "City", 221 | "access_type": "ref", 222 | "possible_keys": [ 223 | "CountryCode" 224 | ], 225 | "key": "CountryCode", 226 | "used_key_parts": [ 227 | "CountryCode" 228 | ], 229 | "key_length": "3", 230 | "ref": [ 231 | "const" 232 | ], 233 | "rows_examined_per_scan": 49, 234 | "rows_produced_per_join": 49, 235 | "filtered": "100.00", 236 | "cost_info": { 237 | "read_cost": "49.00", 238 | "eval_cost": "9.80", 239 | "prefix_cost": "58.80", 240 | "data_read_per_join": "3K" 241 | }, 242 | "used_columns": [ 243 | "ID", 244 | "Name", 245 | "CountryCode", 246 | "District", 247 | "Population" 248 | ] 249 | } 250 | } 251 | }, 252 | { 253 | "dependent": false, 254 | "cacheable": true, 255 | "query_block": { 256 | "select_id": 2, 257 | "cost_info": { 258 | "query_cost": "129.80" 259 | }, 260 | "table": { 261 | "table_name": "City", 262 | "access_type": "ref", 263 | "possible_keys": [ 264 | "CountryCode" 265 | ], 266 | "key": "CountryCode", 267 | "used_key_parts": [ 268 | "CountryCode" 269 | ], 270 | "key_length": "3", 271 | "ref": [ 272 | "const" 273 | ], 274 | "rows_examined_per_scan": 274, 275 | "rows_produced_per_join": 274, 276 | "filtered": "100.00", 277 | "cost_info": { 278 | "read_cost": "75.00", 279 | "eval_cost": "54.80", 280 | "prefix_cost": "129.80", 281 | "data_read_per_join": "19K" 282 | }, 283 | "used_columns": [ 284 | "ID", 285 | "Name", 286 | "CountryCode", 287 | "District", 288 | "Population" 289 | ] 290 | } 291 | } 292 | } 293 | ] 294 | } 295 | } 296 | } 297 | ``` 298 | 299 | 300 | ## UNION ALL 301 | 302 | `UNION ALL`は意味上は`UNION`と似ていますが、重複排除が必要がないという重要な違いがあります。これによって、いくつかのケースでMySQLは`UNION ALL`の結果を中間テーブルで実体化したり重複排除したりすることなく、そのまま伝えることができることになります。 303 | 304 | 内部的には`UNION ALL`のクエリーには毎回一時テーブルが作成されますが、`EXPLAIN`で行の実体化に必要だったかどうかを確認できます。例28に`UNION ALL`を利用したクエリー例を示します。`ORDER BY`を追加するとクエリーは中間一時テーブルを利用する必要がでてきます。 305 | 306 | ![](http://www.unofficialmysqlguide.com/_images/explain-example-30.png) 307 | 308 | ### 例28: 一時テーブルを利用しない`UNION ALL` 309 | 310 | ```sql 311 | EXPLAIN FORMAT=JSON 312 | SELECT * FROM City WHERE CountryCode = 'CAN' 313 | UNION ALL 314 | SELECT * FROM City WHERE CountryCode = 'USA'; 315 | { 316 | "query_block": { 317 | "union_result": { 318 | "using_temporary_table": false, # 一時テーブルは不要! 319 | "query_specifications": [ 320 | { 321 | "dependent": false, 322 | "cacheable": true, 323 | "query_block": { 324 | "select_id": 1, 325 | "cost_info": { 326 | "query_cost": "58.80" 327 | }, 328 | "table": { 329 | "table_name": "City", 330 | "access_type": "ref", 331 | "possible_keys": [ 332 | "CountryCode" 333 | ], 334 | "key": "CountryCode", 335 | "used_key_parts": [ 336 | "CountryCode" 337 | ], 338 | "key_length": "3", 339 | "ref": [ 340 | "const" 341 | ], 342 | "rows_examined_per_scan": 49, 343 | "rows_produced_per_join": 49, 344 | "filtered": "100.00", 345 | "cost_info": { 346 | "read_cost": "49.00", 347 | "eval_cost": "9.80", 348 | "prefix_cost": "58.80", 349 | "data_read_per_join": "3K" 350 | }, 351 | "used_columns": [ 352 | "ID", 353 | "Name", 354 | "CountryCode", 355 | "District", 356 | "Population" 357 | ] 358 | } 359 | } 360 | }, 361 | { 362 | "dependent": false, 363 | "cacheable": true, 364 | "query_block": { 365 | "select_id": 2, 366 | "cost_info": { 367 | "query_cost": "129.80" 368 | }, 369 | "table": { 370 | "table_name": "City", 371 | "access_type": "ref", 372 | "possible_keys": [ 373 | "CountryCode" 374 | ], 375 | "key": "CountryCode", 376 | "used_key_parts": [ 377 | "CountryCode" 378 | ], 379 | "key_length": "3", 380 | "ref": [ 381 | "const" 382 | ], 383 | "rows_examined_per_scan": 274, 384 | "rows_produced_per_join": 274, 385 | "filtered": "100.00", 386 | "cost_info": { 387 | "read_cost": "75.00", 388 | "eval_cost": "54.80", 389 | "prefix_cost": "129.80", 390 | "data_read_per_join": "19K" 391 | }, 392 | "used_columns": [ 393 | "ID", 394 | "Name", 395 | "CountryCode", 396 | "District", 397 | "Population" 398 | ] 399 | } 400 | } 401 | } 402 | ] 403 | } 404 | } 405 | } 406 | ``` 407 | 408 | ### 例29: `ORDER BY`により一時テーブルが必要となる`UNION ALL` 409 | 410 | ```sql 411 | EXPLAIN FORMAT=JSON 412 | SELECT * FROM City WHERE CountryCode = 'CAN' 413 | UNION ALL 414 | SELECT * FROM City WHERE CountryCode = 'USA' ORDER BY Name; 415 | { 416 | "query_block": { 417 | "union_result": { 418 | "using_temporary_table": true, # UNION ALLは一時テーブルを必要とする 419 | "table_name": "", # ORDER BYが指定されているため 420 | "access_type": "ALL", 421 | "query_specifications": [ 422 | { 423 | "dependent": false, 424 | "cacheable": true, 425 | "query_block": { 426 | "select_id": 1, 427 | "cost_info": { 428 | "query_cost": "58.80" 429 | }, 430 | "table": { 431 | "table_name": "City", 432 | "access_type": "ref", 433 | "possible_keys": [ 434 | "CountryCode" 435 | ], 436 | "key": "CountryCode", 437 | "used_key_parts": [ 438 | "CountryCode" 439 | ], 440 | "key_length": "3", 441 | "ref": [ 442 | "const" 443 | ], 444 | "rows_examined_per_scan": 49, 445 | "rows_produced_per_join": 49, 446 | "filtered": "100.00", 447 | "cost_info": { 448 | "read_cost": "49.00", 449 | "eval_cost": "9.80", 450 | "prefix_cost": "58.80", 451 | "data_read_per_join": "3K" 452 | }, 453 | "used_columns": [ 454 | "ID", 455 | "Name", 456 | "CountryCode", 457 | "District", 458 | "Population" 459 | ] 460 | } 461 | } 462 | }, 463 | { 464 | "dependent": false, 465 | "cacheable": true, 466 | "query_block": { 467 | "select_id": 2, 468 | "cost_info": { 469 | "query_cost": "129.80" 470 | }, 471 | "table": { 472 | "table_name": "City", 473 | "access_type": "ref", 474 | "possible_keys": [ 475 | "CountryCode" 476 | ], 477 | "key": "CountryCode", 478 | "used_key_parts": [ 479 | "CountryCode" 480 | ], 481 | "key_length": "3", 482 | "ref": [ 483 | "const" 484 | ], 485 | "rows_examined_per_scan": 274, 486 | "rows_produced_per_join": 274, 487 | "filtered": "100.00", 488 | "cost_info": { 489 | "read_cost": "75.00", 490 | "eval_cost": "54.80", 491 | "prefix_cost": "129.80", 492 | "data_read_per_join": "19K" 493 | }, 494 | "used_columns": [ 495 | "ID", 496 | "Name", 497 | "CountryCode", 498 | "District", 499 | "Population" 500 | ] 501 | } 502 | } 503 | } 504 | ] 505 | } 506 | } 507 | } 508 | ``` 509 | 510 | [^1]: この動作は廃止予定ですので将来的にはなくなります。順序の指定は必須ではありませんが明示的に`GROUP BY`に`ORDER BY NULL`を指定することが推奨されます。 511 | -------------------------------------------------------------------------------- /_posts/2017-07-01-sorting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ソート 3 | date: 2017-07-01 00:00:01 -0900 4 | article_index: 18 5 | original_url: http://www.unofficialmysqlguide.com/sorting.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQLには結果をソートされた順序で*返す*4つの方法があり、これらは`ORDER BY`または`GROUP BY`(`ORDER BY NULL`を除く)の機能の一部として必要とされます。`EXPLAIN`はソート操作が必要かどうかを表示しますが、どのソートアルゴリズムが利用されるかは表示**しません**。この情報は`OPTIMIZER_TRACE`によって取得できます。結果をソートするのに利用される4つの手法はそれぞれ次のとおりです。 10 | 11 | ![explain-example-27](http://www.unofficialmysqlguide.com/_images/explain-example-27.png) 12 | 13 | 1. **インデックスを利用** : B+ツリーインデックスはソート順に保たれるため、`ORDER BY`クエリーの中には全くソートする必要がないケースがあります。 14 | 2. **優先度つきキューを利用**: limitで小さな値が指定された`ORDER BY`は全ての結果セットを一時バッファーに格納できます。例えば、次のクエリーを考えてみてください。 15 | 16 | ```sql 17 | SELECT * FROM Country IGNORE INDEX (p, p_c) 18 | ORDER BY population LIMIT 10; 19 | ``` 20 | このクエリーはテーブルスキャンとなり、人口の多い10行をバッファーに持つことになります。新しくより人口の多い行が見つかれば、以前の行が優先度つきキューから押しだされます。 21 | 3. **代替ソートアルゴリズムを利用**: このアルゴリズムは`TEXT`または`BLOB`型の列がない場合に利用されます。MySQLのマニュアル[^1] に次のように定義されています。 22 | 1. `WHERE`句の条件に一致する行を読みこみます。 23 | 2. 各行に対してソートキーの値およびクエリーにより参照される追加のフィールドから構成されるタプルの値を記録します。 24 | 3. ソートバッファーが一杯になったら、メモリー内のソートキーの値でタプルを並び替え、一時ファイルに書き込みます。 25 | 4. 一時ファイルをマージソートした後、行をソートキーの値の順に並び替え、ソートされたタプルから必要な行を直接読み取ります。 26 | 4. **元のソートアルゴリズムを利用**: `TEXT`または`BLOB`型の列が存在する場合にこのアルゴリズムが利用されます。MySQLのマニュアル[^2] には次のように定義されています。 27 | 1. 全ての行をキーに従って、あるいはテーブルスキャンで読み込みます。WHERE条件に一致しない行はスキップします。 28 | 2. 各行について、ある値のペア(ソートキーの値と行ID)からなる値で構成されるタプルをソートバッファーに保存します。 29 | 3. 全てのペアがソートバッファーに収まれば、一時ファイルは作成されません。ソートバッファーが一杯になったら、メモリ上でqsort(クイックソート)を実行し一時ファイルに書き込みます。ソートされたブロックへのポインターを保存します。 30 | 4. 全ての行が読み込まれるまで上記のステップを実施します。 31 | 5. MERGEBUFF(7)までの領域を他の一時ファイル内のブロックへマルチマージします。最初のファイルの全てのブロックが2番目のファイルに移動されるまで繰り返します。 32 | 6. 残りがMERGEBUFF2(15)ブロックになるまで次のことを繰り返します。 33 | 7. マルチマージの一番最後に、結果ファイルに行ID(値のペアの一番最後)のみが書き込まれます。 34 | 8. 結果ファイルの行IDを使って行をソート順に読み込みます。これを最適化するためには、行IDを大きなブロックで読込んでソートし、それによって行を所定のソート順に行バッファーに読みこみます。行バッファーのサイズは`read_rnd_buffer_size`システム変数の値で規定されます。この手続きの内容は、ソースコードとして`sql/records.cc`に記載されています。 35 | 36 | ### 例30: インデックスによってソートされる例 37 | 38 | ```sql 39 | EXPLAIN FORMAT=JSON 40 | SELECT * FROM Country WHERE continent='Asia' ORDER BY population; 41 | { 42 | "query_block": { 43 | "select_id": 1, 44 | "cost_info": { 45 | "query_cost": "34.10" 46 | }, 47 | "ordering_operation": { 48 | "using_filesort": false, # c_pインデックスによってソートが実現されている 49 | "table": { 50 | "table_name": "Country", 51 | "access_type": "ref", 52 | "possible_keys": [ 53 | "c", 54 | "c_p" 55 | ], 56 | "key": "c_p", 57 | "used_key_parts": [ 58 | "Continent" 59 | ], 60 | "key_length": "1", 61 | "ref": [ 62 | "const" 63 | ], 64 | "rows_examined_per_scan": 51, 65 | "rows_produced_per_join": 51, 66 | "filtered": "100.00", 67 | "index_condition": "(`world`.`Country`.`Continent` <=> 'Asia')", 68 | "cost_info": { 69 | "read_cost": "23.90", 70 | "eval_cost": "10.20", 71 | "prefix_cost": "34.10", 72 | "data_read_per_join": "13K" 73 | }, 74 | "used_columns": [ 75 | "Code", 76 | "Name", 77 | "Continent", 78 | "Region", 79 | "SurfaceArea", 80 | "IndepYear", 81 | "Population", 82 | "LifeExpectancy", 83 | "GNP", 84 | "GNPOld", 85 | "LocalName", 86 | "GovernmentForm", 87 | "HeadOfState", 88 | "Capital", 89 | "Code2" 90 | ] 91 | } 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | ### 例31: `OPTIMIZER_TRACE`により優先度つきキューが利用されていることがわかる 98 | 99 | ```sql 100 | SET OPTIMIZER_TRACE="ENABLED=on"; 101 | SELECT * FROM Country IGNORE INDEX (p, p_c) ORDER BY population LIMIT 10; 102 | SELECT * FROM information_schema.optimizer_trace\G 103 | { 104 | "steps": [ 105 | { 106 | "join_preparation": { 107 | "select#": 1, 108 | "steps": [ 109 | { 110 | "expanded_query": "/* select#1 */ select `Country`.`Code` AS `Code`,`Country`.`Name` AS `Name`,`Country`.`Continent` AS `Continent`,`Country`.`Region` AS `Region`,`Country`.`SurfaceArea` AS `SurfaceArea`,`Country`.`IndepYear` AS `IndepYear`,`Country`.`Population` AS `Population`,`Country`.`LifeExpectancy` AS `LifeExpectancy`,`Country`.`GNP` AS `GNP`,`Country`.`GNPOld` AS `GNPOld`,`Country`.`LocalName` AS `LocalName`,`Country`.`GovernmentForm` AS `GovernmentForm`,`Country`.`HeadOfState` AS `HeadOfState`,`Country`.`Capital` AS `Capital`,`Country`.`Code2` AS `Code2` from `Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`) order by `Country`.`Population` limit 10" 111 | } 112 | ] 113 | } 114 | }, 115 | { 116 | "join_optimization": { 117 | "select#": 1, 118 | "steps": [ 119 | { 120 | "substitute_generated_columns": { 121 | } 122 | }, 123 | { 124 | "table_dependencies": [ 125 | { 126 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)", 127 | "row_may_be_null": false, 128 | "map_bit": 0, 129 | "depends_on_map_bits": [ 130 | ] 131 | } 132 | ] 133 | }, 134 | { 135 | "rows_estimation": [ 136 | { 137 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)", 138 | "table_scan": { 139 | "rows": 239, 140 | "cost": 9 141 | } 142 | } 143 | ] 144 | }, 145 | { 146 | "considered_execution_plans": [ 147 | { 148 | "plan_prefix": [ 149 | ], 150 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)", 151 | "best_access_path": { 152 | "considered_access_paths": [ 153 | { 154 | "rows_to_scan": 239, 155 | "access_type": "scan", 156 | "resulting_rows": 239, 157 | "cost": 56.8, 158 | "chosen": true 159 | } 160 | ] 161 | }, 162 | "condition_filtering_pct": 100, 163 | "rows_for_plan": 239, 164 | "cost_for_plan": 56.8, 165 | "chosen": true 166 | } 167 | ] 168 | }, 169 | { 170 | "attaching_conditions_to_tables": { 171 | "original_condition": null, 172 | "attached_conditions_computation": [ 173 | ], 174 | "attached_conditions_summary": [ 175 | { 176 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)", 177 | "attached": null 178 | } 179 | ] 180 | } 181 | }, 182 | { 183 | "clause_processing": { 184 | "clause": "ORDER BY", 185 | "original_clause": "`Country`.`Population`", 186 | "items": [ 187 | { 188 | "item": "`Country`.`Population`" 189 | } 190 | ], 191 | "resulting_clause_is_simple": true, 192 | "resulting_clause": "`Country`.`Population`" 193 | } 194 | }, 195 | { 196 | "reconsidering_access_paths_for_index_ordering": { 197 | "clause": "ORDER BY", 198 | "index_order_summary": { 199 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)", 200 | "index_provides_order": false, 201 | "order_direction": "undefined", 202 | "index": "unknown", 203 | "plan_changed": false 204 | } 205 | } 206 | }, 207 | { 208 | "refine_plan": [ 209 | { 210 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)" 211 | } 212 | ] 213 | } 214 | ] 215 | } 216 | }, 217 | { 218 | "join_execution": { 219 | "select#": 1, 220 | "steps": [ 221 | { 222 | "filesort_information": [ 223 | { 224 | "direction": "asc", 225 | "table": "`Country` IGNORE INDEX (`p_c`) IGNORE INDEX (`p`)", 226 | "field": "Population" 227 | } 228 | ], 229 | "filesort_priority_queue_optimization": { 230 | "limit": 10, 231 | "rows_estimate": 939, 232 | "row_size": 272, 233 | "memory_available": 262144, 234 | "chosen": true # 優先度つきキューの最適化が適用されている 235 | }, 236 | "filesort_execution": [ 237 | ], 238 | "filesort_summary": { 239 | "rows": 11, 240 | "examined_rows": 239, 241 | "number_of_tmp_files": 0, 242 | "sort_buffer_size": 3080, 243 | "sort_mode": "" 244 | } 245 | } 246 | ] 247 | } 248 | } 249 | ] 250 | } 251 | ``` 252 | 253 | [^1]: [http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html](http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html) 254 | [^2]: 脚注1と同様([http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html](http://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html)) 255 | -------------------------------------------------------------------------------- /_posts/2017-07-03-partitioning.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: パーティショニング 3 | date: 2017-07-03 00:00:01 -0900 4 | article_index: 19 5 | original_url: http://www.unofficialmysqlguide.com/partitioning.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | オプティマイザは*パーティションプルーニング*ができます。つまり、入力のクエリーを分析し、ディクショナリ情報と比較し、必要なテーブルパーティションだけにアクセスさせることができます。 10 | 11 | パーティショニングは、それが1つのテーブルの論理的な表現であり、一連のテーブルがその下にあるいう点で、ビューと似たように考えることができます。パーティショニングはインデックスを利用した上で、全てのクエリーが共通のパターンに従う場合に適しています。例えば、 12 | 13 | * **論理削除(soft delete)**がある場合: 多くのアプリケーションは論理削除を実装しており(例: is\_deleted)、削除済みあるいは削除されていないデータのみによくアクセスします。 14 | * **マルチバージョンスキームが適用されたスキーマ[^1]**がある場合: アプリケーションによってはデータを決して削除せず、古い世代の行を履歴を残す目的で保存するポリシーをとるものがあります。例えばあるユーザーの住所を更新するとき、全ての以前の住所を保持する場合です。住所が古く無効なものかどうかでパーティショニングするとメモリーを効率的に利用できます。 15 | * **時系列的(time oriented)**な場合: 例えば、パーティションが四半期あるいは会計周期で作成される場合です。 16 | * **局所性(locality)**がある場合: 例えば、パーティションがstore\_id(店舗ID)あるいは地域で作成される場合です。 17 | 18 | ![partition-countrylanguage.png](http://www.unofficialmysqlguide.com/_images/partition-countrylanguage.png) 19 | 20 | パーティショニングの性能は、ほとんどのクエリーが1つあるいはごく一部のパーティションにしか一度にアクセスしない場合に最善となります。我々が例でみてきたスキーマで考えてみると、`CountryLanguage`テーブルへの全てのクエリーが公用語のみに対して発行されるケースが該当するでしょう。もしこれが本当であれば、`isOfficial`によるパーティショニングは次のように実現できます。 21 | 22 | ```sql 23 | # IsOfficialはENUMですが、CHARに変換することができます 24 | # ENUM型はKEYパーティショニングのみをサポートしています 25 | # パーティショニングキーは主キーの一部であり、かつ全てユニークキーである必要があります 26 | 27 | ALTER TABLE CountryLanguage MODIFY IsOfficial CHAR(1) NOT NULL DEFAULT 'F', DROP PRIMARY KEY, ADD PRIMARY KEY(CountryCode, Language, IsOfficial); 28 | ALTER TABLE CountryLanguage PARTITION BY LIST COLUMNS (IsOfficial) ( 29 | PARTITION pUnofficial VALUES IN ('F'), 30 | PARTITION pOfficial VALUES IN ('T') 31 | ); 32 | ``` 33 | 34 | ### 例32: オプティマイザでパーティションプルーニングされている 35 | 36 | ```sql 37 | EXPLAIN FORMAT=JSON 38 | SELECT * FROM CountryLanguage WHERE isOfficial='T' AND CountryCode='CAN'; 39 | { 40 | "query_block": { 41 | "select_id": 1, 42 | "cost_info": { 43 | "query_cost": "2.40" 44 | }, 45 | "table": { 46 | "table_name": "CountryLanguage", 47 | "partitions": [ 48 | "pOfficial" # 公用語が含まれるパーティションのみがアクセスされる 49 | ], 50 | "access_type": "ref", 51 | "possible_keys": [ 52 | "PRIMARY", 53 | "CountryCode" 54 | ], 55 | "key": "PRIMARY", 56 | "used_key_parts": [ 57 | "CountryCode" 58 | ], 59 | "key_length": "3", 60 | "ref": [ 61 | "const" 62 | ], 63 | "rows_examined_per_scan": 2, 64 | "rows_produced_per_join": 0, 65 | "filtered": "10.00", 66 | "cost_info": { 67 | "read_cost": "2.00", 68 | "eval_cost": "0.04", 69 | "prefix_cost": "2.40", 70 | "data_read_per_join": "8" 71 | }, 72 | "used_columns": [ 73 | "CountryCode", 74 | "Language", 75 | "IsOfficial", 76 | "Percentage" 77 | ], 78 | "attached_condition": "(`world`.`CountryLanguage`.`IsOfficial` = 'T')" 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | {% include info.html info="MySQLはパーティションプルーニングにくわえて、選択されたパーティションのみを明示的に指定する構文をサポートしています。これは次のようにアプリケーションからクエリーヒントと似たように利用することができます。SELECT * FROM CountryLanguage PARTITION (pOfficial) WHERE CountryCode='CAN'; " %} 85 | 86 | [^1]: データウェアハウスにおいて、これはslowly changing dimensionsと呼ばれます。[https://en.wikipedia.org/wiki/Slowly_changing_dimension](https://en.wikipedia.org/wiki/Slowly_changing_dimension) 87 | -------------------------------------------------------------------------------- /_posts/2017-07-04-invisible-indexes.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 不可視インデックス 3 | date: 2017-07-04 00:00:01 -0900 4 | article_index: 21 5 | original_url: http://www.unofficialmysqlguide.com/invisible-indexes.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | 不可視インデックスはMySQL 8.0の新機能で、インデックスに「利用できない」と印をつけこれをオプティマイザが利用するためのものです。つまりインデックスはメンテナンスされデータが更新されれば最新の状態に保たれますが、インデックスを利用してクエリーを発行することができません(`FORCE INDEX インデックス名`クエリーだとしても、です) 10 | 11 | 不可視インデックスはMyISAMストレージエンジンが実装している無効化インデックス(disabled indexes)と混同してはなりません(無効化インデックスはインデックスのメンテナンスをやめます)。不可視インデックスには特筆すべき2つのユースケースがあります。 12 | 13 | 1. **論理削除(soft delete)**。商用環境で破壊的な操作を行うときは、変更を永続化する前に様子を見ることができることが望ましいでしょう。インデックスの「ごみ箱」のようであると考えてみてください。仮にあなたが誤りを犯しインデックスが利用されていた場合、再度可視化するためにはただメタ情報を変更すればよいことになります、バックアップから復元するよりはるかに高速です。例えば、次のとおりです 14 | `ALTER TABLE Country ALTER INDEX c INVISIBLE;` 15 | 2. **段階的な展開(staged rollout)**。インデックスを追加するときには、望まない変更が発生する場合もありますので、既存のクエリーの実行計画に変化が発生するかどうかを考慮することが常に大切となります。不可視インデックスを利用すると、ピーク負荷をさけシステムを能動的に見守ることができる任意の時間帯で、インデックスを段階的に展開することができます。例えば、次のとおりです。 16 | ```sql 17 | ALTER TABLE Country DROP INDEX c; 18 | ALTER TABLE Country ADD INDEX c (Continent) INVISIBLE; 19 | # after some time 20 | ALTER TABLE Country ALTER INDEX c VISIBLE; 21 | ``` 22 | 全てのインデックスは不可視であると指定されない限り、デフォルトで可視となります。全スキーマの不可視インデックスは次のようにして探すことができます。 23 | 24 | ```sql 25 | SELECT * FROM information_schema.statistics WHERE is_visible='NO'; 26 | *************************** 1. row *************************** 27 | TABLE_CATALOG: def 28 | TABLE_SCHEMA: world 29 | TABLE_NAME: Country 30 | NON_UNIQUE: 1 31 | INDEX_SCHEMA: world 32 | INDEX_NAME: c 33 | SEQ_IN_INDEX: 1 34 | COLUMN_NAME: Continent 35 | COLLATION: A 36 | CARDINALITY: 7 37 | SUB_PART: NULL 38 | PACKED: NULL 39 | NULLABLE: 40 | INDEX_TYPE: BTREE 41 | COMMENT: disabled 42 | INDEX_COMMENT: 43 | IS_VISIBLE: NO 44 | ``` 45 | 46 | {% include info.html info="不必要なインデックスを削除するのはいい考えです。ほとんどの方々はインデックスは変更(insert、update)の性能を損なうことはお気づきであるように、これ自体が単純化につながります。不要なインデックスは、オプティマイザがプラン選択のときに評価する必要があり読み取り性能にも影響を与えることでしょう。" %} 47 | -------------------------------------------------------------------------------- /_posts/2017-07-04-query-rewrite.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: クエリーリライト 3 | date: 2017-07-04 00:00:01 -0900 4 | article_index: 20 5 | original_url: http://www.unofficialmysqlguide.com/query-rewrite.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQLはサーバーサイドでステートメントを書き換える機能を提供しています。これはあるパターンに合致するステートメントを新しいパターンに書き換えられるサーバーサイドの正規表現のようなものであると考えることができます。 10 | 11 | この機能の設計のゴールはデータベース管理者がクエリーヒントをステートメントに挿入できるようにすることでした。これはORMを利用していたり、アプリケーションが[プロプライエタリ](https://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%83%97%E3%83%A9%E3%82%A4%E3%82%A8%E3%82%BF%E3%83%AA%E3%83%BB%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2)であったりして、アプリケーション自体を修正できない場合に状況を改善する手段を提供するものです。 12 | 13 | クエリーリライトは正規表現と似たものであると述べましたが、内部的にははるかに効率的なものであることはお伝えしておく必要があります。ステートメントがパースされる際に、ダイジェスト(プリペアードステートメントという形でも知られています)が内部のハッシュテーブルの書き換えるべき値と比較されます。ステートメントが「書き換えが必要である」と判断されれば、サーバーはこのクエリー書き換えのステップを実行し再度クエリーをパースします。書き換えの必要のないクエリーにもわずかなオーバーヘッドが発生し、書き換えの必要なクエリーは簡単にいうと2回パースする必要があります。 14 | 15 | 例33: クエリーリライトでサーバーサイドのクエリーを変更する 16 | 17 | ```sql 18 | # コマンドラインからクエリーリライトをインストールします 19 | mysql -u root -p < install_rewriter.sql 20 | 21 | # MySQLサーバーでリライトルールを追加し、フラッシュします 22 | INSERT INTO query_rewrite.rewrite_rules(pattern_database, pattern, replacement) VALUES ( 23 | "world", 24 | "SELECT * FROM Country WHERE population > ? AND continent=?", 25 | "SELECT * FROM Country WHERE population > ? AND continent=? LIMIT 1" 26 | ); 27 | CALL query_rewrite.flush_rewrite_rules(); 28 | 29 | # クエリーリライトはリライトが発生するとき毎回warningを発生させます 30 | SELECT * FROM Country WHERE population > 5000000 AND continent='Asia'; 31 | SHOW WARNINGS; 32 | Query 'SELECT * FROM Country WHERE population > 5000000 AND continent='Asia'' rewritten to 'SELECT * FROM Country WHERE population > 5000000 AND continent='Asia' LIMIT 1' by a query rewrite plugin 33 | 34 | SELECT * FROM query_rewrite.rewrite_rules\G 35 | *************************** 1. row *************************** 36 | id: 2 37 | pattern: SELECT * FROM Country WHERE population > ? AND continent=? 38 | pattern_database: world 39 | replacement: SELECT * FROM Country WHERE population > ? AND continent=? LIMIT 1 40 | enabled: YES 41 | message: NULL 42 | pattern_digest: 88876bbb502cef6efddcc661cce77deb 43 | normalized_pattern: select `*` from `world`.`Country` where ((`population` > ?) and (`continent` = ?)) 44 | ``` 45 | 46 | {% include info.html info="MySQLサーバーは各種Query Rewriteプラグインをサポートしています。ここで示される例では、MySQLサーバーディストリビューション同梱のポストパース クエリーリライトプラグイン(通称Rewriter)を利用しています。クエリーによってはプレパースプラグインを利用する必要があるかもしれません。詳細については次のMySQLのマニュアルを御覧ください。 https://dev.mysql.com/doc/refman/5.7/en/rewriter-query-rewrite-plugin.html" %} 47 | -------------------------------------------------------------------------------- /_posts/2017-07-08-profiling-queries.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: クエリープロファイリング 3 | date: 2017-07-08 00:00:01 -0900 4 | article_index: 22 5 | original_url: http://www.unofficialmysqlguide.com/profiling.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | `EXPLAIN`はクエリーのコストに関する実行前にえられる情報のみを表示します。これにはもう少し全体的な俯瞰をするためのクエリー実行に関する統計は一切含まれません。例えば、オプティマイザがインデックスで行を除外(`EXPLAIN`内でテーブルに対しattached\_conditionを付与)できなかった場合、何行が除外されたのかがわかりません。結合条件に対して[トリクルダウン理論](https://ja.wikipedia.org/wiki/%E3%83%88%E3%83%AA%E3%82%AF%E3%83%AB%E3%83%80%E3%82%A6%E3%83%B3%E7%90%86%E8%AB%96)が適用され、関連テーブルへの参照がとても多くなるのか、少なくなるのかわからない可能性があります。 10 | 11 | MySQLは実行の各ステップでどの程度時間がかかったかを表す基本的なプロファイリング機能をサポートしており、`performance_schema`からこの情報をえることができます。これは、以前は`SHOW PROFILES`コマンドでしたが、将来的に廃止予定となったためこれに取ってかわるものです。例としてご紹介するために、私は`SHOW PROFILES`を`SYS`スキーマベースで置き換えたものを利用しており、これは次のようにインストールできます。 12 | 13 | ``` 14 | # 下記はシェルにて実行 15 | wget http://www.tocker.ca/files/ps-show-profiles.sql 16 | mysql -u root -p < ps-show-profiles.sql 17 | ``` 18 | 19 | ### 例34: Performance Schemaでクエリーをプロファイリングする 20 | 21 | ``` 22 | # 下記はMySQLにて実行 23 | CALL sys.enable_profiling(); 24 | CALL sys.show_profiles; 25 | *************************** 1. row *************************** 26 | Event_ID: 22 27 | Duration: 495.02 us 28 | Query: SELECT * FROM Country WHERE co ... Asia' and population > 5000000 29 | 1 row in set (0.00 sec) 30 | 31 | CALL sys.show_profile_for_event_id(22); 32 | +----------------------+-----------+ 33 | | Status | Duration | 34 | +----------------------+-----------+ 35 | | starting | 64.82 us | 36 | | checking permissions | 4.10 us | 37 | | Opening tables | 11.87 us | 38 | | init | 29.74 us | 39 | | System lock | 5.63 us | 40 | | optimizing | 8.74 us | 41 | | statistics | 139.38 us | 42 | | preparing | 11.94 us | 43 | | executing | 348.00 ns | 44 | | Sending data | 192.59 us | 45 | | end | 1.17 us | 46 | | query end | 4.60 us | 47 | | closing tables | 4.07 us | 48 | | freeing items | 13.60 us | 49 | | cleaning up | 734.00 ns | 50 | +----------------------+-----------+ 51 | 15 rows in set (0.00 sec) 52 | ``` 53 | 54 | `SLEEP()`関数を利用すれば、実行の特定のステップでたくさんの時間を利用していることを実際に確認することができます。このクエリーでは、MySQLは`continent='Antarctica'`の条件に行が合致する毎に5秒スリープします。 55 | 56 | ```sql 57 | SELECT * FROM Country WHERE Continent='Antarctica' and SLEEP(5); 58 | CALL sys.show_profiles(); 59 | CALL sys.show_profile_for_event_id(); 60 | +----------------------+-----------+ 61 | | Status | Duration | 62 | +----------------------+-----------+ 63 | | starting | 103.89 us | 64 | | checking permissions | 4.48 us | 65 | | Opening tables | 17.78 us | 66 | | init | 45.75 us | 67 | | System lock | 8.37 us | 68 | | optimizing | 11.98 us | 69 | | statistics | 144.78 us | 70 | | preparing | 15.78 us | 71 | | executing | 634.00 ns | 72 | | Sending data | 116.15 us | 73 | | User sleep | 5.00 s | 74 | | User sleep | 5.00 s | 75 | | User sleep | 5.00 s | 76 | | User sleep | 5.00 s | 77 | | User sleep | 5.00 s | 78 | | end | 2.05 us | 79 | | query end | 5.63 us | 80 | | closing tables | 7.30 us | 81 | | freeing items | 20.19 us | 82 | | cleaning up | 1.20 us | 83 | +----------------------+-----------+ 84 | 20 rows in set (0.01 sec) 85 | ``` 86 | 87 | プロファイリングの出力が必ずしもきめ細かくなっているわけではないことが分かるでしょう。例えば、`Sending Data`は*ストレージエンジンとサーバー間でデータを転送している*ということしかわかりません。重要な点としては、一時テーブルの作成とソートの実行時間がブレークダウンされています。 88 | 89 | ```sql 90 | ELECT region, count(*) as c FROM Country GROUP BY region; 91 | CALL sys.show_profiles(); 92 | CALL sys.show_profile_for_event_id(); 93 | +----------------------+-----------+ 94 | | Status | Duration | 95 | +----------------------+-----------+ 96 | | starting | 87.43 us | 97 | | checking permissions | 4.93 us | 98 | | Opening tables | 17.35 us | 99 | | init | 25.81 us | 100 | | System lock | 9.04 us | 101 | | optimizing | 3.37 us | 102 | | statistics | 18.31 us | 103 | | preparing | 10.94 us | 104 | | Creating tmp table | 35.57 us | # < -- 105 | | Sorting result | 2.38 us | # < -- 106 | | executing | 741.00 ns | 107 | | Sending data | 446.03 us | # < -- 108 | | Creating sort index | 49.45 us | # < -- 109 | | end | 1.71 us | 110 | | query end | 4.85 us | 111 | | removing tmp table | 4.71 us | 112 | | closing tables | 6.12 us | 113 | | freeing items | 17.17 us | 114 | | cleaning up | 1.00 us | 115 | +----------------------+-----------+ 116 | 19 rows in set (0.01 sec) 117 | ``` 118 | 119 | `performance_schema`はこれらのヘルパープロシージャで示されるプロファイル情報に加えて、返却された行数だけでなくソートに必要とされた実際の行数に関する追加の統計情報を保持しています。実行レベルの分析は、`EXPLAIN`で確認できる実行前の解析に対して次のように追加の情報を提供します。 120 | 121 | ```sql 122 | SELECT * FROM performance_schema.events_statements_history_long 123 | WHERE event_id=\G 124 | *************************** 1. row *************************** 125 | THREAD_ID: 3062 126 | EVENT_ID: 1566 127 | END_EVENT_ID: 1585 128 | EVENT_NAME: statement/sql/select 129 | SOURCE: init_net_server_extension.cc:80 130 | TIMER_START: 588883869566277000 131 | TIMER_END: 588883870317683000 132 | TIMER_WAIT: 751406000 133 | LOCK_TIME: 132000000 134 | SQL_TEXT: SELECT region, count(*) as c FROM Country GROUP BY region 135 | DIGEST: d3a04b346fe48da4f1f5c2e06628a245 136 | DIGEST_TEXT: SELECT `region` , COUNT ( * ) AS `c` FROM `Country` GROUP BY `region` 137 | CURRENT_SCHEMA: world 138 | OBJECT_TYPE: NULL 139 | OBJECT_SCHEMA: NULL 140 | OBJECT_NAME: NULL 141 | OBJECT_INSTANCE_BEGIN: NULL 142 | MYSQL_ERRNO: 0 143 | RETURNED_SQLSTATE: NULL 144 | MESSAGE_TEXT: NULL 145 | ERRORS: 0 146 | WARNINGS: 0 147 | ROWS_AFFECTED: 0 148 | ROWS_SENT: 25 # < -- 149 | ROWS_EXAMINED: 289 # < -- 150 | CREATED_TMP_DISK_TABLES: 0 151 | CREATED_TMP_TABLES: 1 152 | SELECT_FULL_JOIN: 0 153 | SELECT_FULL_RANGE_JOIN: 0 154 | SELECT_RANGE: 0 155 | SELECT_RANGE_CHECK: 0 156 | SELECT_SCAN: 1 157 | SORT_MERGE_PASSES: 0 158 | SORT_RANGE: 0 159 | SORT_ROWS: 25 # < -- 160 | SORT_SCAN: 1 161 | NO_INDEX_USED: 1 162 | NO_GOOD_INDEX_USED: 0 163 | NESTING_EVENT_ID: NULL 164 | NESTING_EVENT_TYPE: NULL 165 | NESTING_EVENT_LEVEL: 0 166 | ``` 167 | -------------------------------------------------------------------------------- /_posts/2017-07-09-json-and-generaged-columns.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: JSONと生成列 3 | date: 2017-07-09 00:00:01 -0900 4 | article_index: 23 5 | original_url: http://www.unofficialmysqlguide.com/json.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQLサーバーは次の機能追加によってスキーマレスなデータの保存をサポートしています。 10 | 11 | 1. **JSONデータ型**: JSONの値がINSERT/UPDATEの際にパースされ、バリデートされバイナリーの最適化された形式で保存されます。JSONデータは値を読み込む際には、パースやバリデートは一切必要ありません。 12 | 2. **JSON関数**: 20を超えるJSON値の検索、操作および生成に関するSQL関数があります。 13 | 3. **生成列(generated columns)**: これはJSONに限ったことではありませんが、生成列は*関数インデックス*のように動作し、これによってJSONドキュメントの一部を抽出したりあるいはインデックスを作成したりすることができます。 14 | 15 | オプティマイザはJSONデータにクエリーを発行する場合、生成列から条件に一致するインデックスを自動的に探します[^1]。例25では、ユーザーの好みがJSON列に保存されています。初期状態では、更新があったらお知らせしてほしい(`notify_on_updates`)と希望しているユーザーを抽出するクエリーで、テーブルスキャンが行われています。インデックス付きの仮想生成列を追加することで、インデックスが利用できるようになったことが`EXPLAIN`によりわかります。 16 | 17 | ### 例35: ユーザーの好みに関するスキーマレスな表現 18 | 19 | ```sql 20 | CREATE TABLE users ( 21 | id INT NOT NULL auto_increment, 22 | username VARCHAR(32) NOT NULL, 23 | preferences JSON NOT NULL, 24 | PRIMARY KEY (id), 25 | UNIQUE (username) 26 | ); 27 | 28 | INSERT INTO users 29 | (id,username,preferences) 30 | VALUES 31 | (NULL, 'morgan', '{"layout": "horizontal", "warn_before_delete": false, "notify_on_updates": true}'), 32 | (NULL, 'wes', '{"layout": "horizontal", "warn_before_delete": false, "notify_on_updates": false}'), 33 | (NULL, 'jasper', '{"layout": "horizontal", "warn_before_delete": false, "notify_on_updates": false}'), 34 | (NULL, 'gus', '{"layout": "horizontal", "warn_before_delete": false, "notify_on_updates": false}'), 35 | (NULL, 'olive', '{"layout": "horizontal", "warn_before_delete": false, "notify_on_updates": false}'); 36 | 37 | EXPLAIN FORMAT=JSON 38 | SELECT * FROM users WHERE preferences->"$.notify_on_updates" = true; 39 | { 40 | "query_block": { 41 | "select_id": 1, 42 | "cost_info": { 43 | "query_cost": "2.00" 44 | }, 45 | "table": { 46 | "table_name": "users", # < -- 47 | "access_type": "ALL", # < -- 48 | "rows_examined_per_scan": 5, 49 | "rows_produced_per_join": 5, 50 | "filtered": "100.00", 51 | "cost_info": { 52 | "read_cost": "1.00", 53 | "eval_cost": "1.00", 54 | "prefix_cost": "2.00", 55 | "data_read_per_join": "280" 56 | }, 57 | "used_columns": [ 58 | "id", 59 | "username", 60 | "preferences" 61 | ], 62 | "attached_condition": "(json_extract(`test`.`users`.`preferences`,'$.notify_on_updates') = TRUE)" 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | ### 例36: インデックス付きの仮想生成列を追加する 69 | 70 | ```sql 71 | ALTER TABLE users ADD notify_on_updates TINYINT AS (preferences->"$.notify_on_updates"), 72 | ADD INDEX(notify_on_updates); 73 | 74 | EXPLAIN FORMAT=JSON SELECT * FROM users WHERE preferences->"$.notify_on_updates" = true; 75 | { 76 | "query_block": { 77 | "select_id": 1, 78 | "cost_info": { 79 | "query_cost": "1.20" 80 | }, 81 | "table": { 82 | "table_name": "users", # < -- 83 | "access_type": "ref", # < -- 84 | "possible_keys": [ 85 | "notify_on_updates" 86 | ], 87 | "key": "notify_on_updates", 88 | "used_key_parts": [ 89 | "notify_on_updates" 90 | ], 91 | "key_length": "2", 92 | "ref": [ 93 | "const" 94 | ], 95 | "rows_examined_per_scan": 1, 96 | "rows_produced_per_join": 1, 97 | "filtered": "100.00", 98 | "cost_info": { 99 | "read_cost": "1.00", 100 | "eval_cost": "0.20", 101 | "prefix_cost": "1.20", 102 | "data_read_per_join": "56" 103 | }, 104 | "used_columns": [ 105 | "id", 106 | "username", 107 | "preferences", 108 | "notify_on_updates" 109 | ] 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | [^1]: 例としては`JSON_EXTRACT`演算子の短縮形(->)の利用が挙げられます。文字列を抽出する際は、この省略演算子とクォート除去演算子(->>)を利用する必要があるでしょう。 116 | -------------------------------------------------------------------------------- /_posts/2017-07-10-character-sets.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 文字セット 3 | date: 2017-07-10 00:00:01 -0900 4 | article_index: 24 5 | original_url: http://www.unofficialmysqlguide.com/character-sets.html 6 | translator: taka-h (@takaidohigasi) 7 | --- 8 | 9 | MySQL 8.0では最新のUnicode 9.0が`utf8mb4`という名前でサポートされています。`utf8mb4`はそれぞれの文字が1から4バイトの*可変長*です。 10 | 可変長の文字数とバイト数が少し異なるケースはたくさんあり例えば次のようなケースがあげられます。 11 | 12 | 1. 列を`VARCHAR(n)`で作成した時、*n*は文字列長を表します。データの保存に必要なバイト数は(多くの場合はこれより小さいのですが)最大この4倍となりえます。 13 | 2. 内部的にはInnoDBストレージエンジンは常に[^1]`utf8mb4`を`VARCHAR`、`CHAR`、`TEXT`データ型の(インデックス内およびテーブルの行にて)可変長として保存しています。 14 | 3. マテリアライズの一部として利用されるメモリ内の一時テーブルは固定長です。`utf8mb4`文字セットを利用している場合は一時テーブルがより大きくなったり、より早くディスクにあふれたりすることになる場合があります。 15 | 4. データをソートするために利用されているバッファーは可変長です(MySQL 5.7より)。 16 | 5. `EXPLAIN`は常にインデックスの可変長(バイト長)の最大長を示します。もっと少ない領域しか必要とならない場合がしばしば発生します。 17 | 18 | 例37は、latin1文字セットで`CHAR(52)`の列へのインデックスを利用していることを伝えている`EXPLAIN`となります。テーブルを`utf8mb4`に変換した後、必要な保存容量は増えませんが、`EXPLAIN`では`key_length`が増えていることが確認できます。 19 | 20 | ### 例37: `EXPLAIN`がインデックスの最大キー長を示す(latin1文字セットにて) 21 | 22 | ```sql 23 | EXPLAIN FORMAT=JSON 24 | SELECT * FROM Country WHERE name='Canada'; 25 | { 26 | "query_block": { 27 | "select_id": 1, 28 | "cost_info": { 29 | "query_cost": "1.20" 30 | }, 31 | "table": { 32 | "table_name": "Country", 33 | "access_type": "ref", 34 | "possible_keys": [ 35 | "Name" 36 | ], 37 | "key": "Name", 38 | "used_key_parts": [ 39 | "Name" 40 | ], 41 | "key_length": "52", # CHAR(52) 42 | "ref": [ 43 | "const" 44 | ], 45 | "rows_examined_per_scan": 1, 46 | "rows_produced_per_join": 1, 47 | "filtered": "100.00", 48 | "cost_info": { 49 | "read_cost": "1.00", 50 | "eval_cost": "0.20", 51 | "prefix_cost": "1.20", 52 | "data_read_per_join": "264" 53 | }, 54 | "used_columns": [ 55 | "Code", 56 | "Name", 57 | "Continent", 58 | "Region", 59 | "SurfaceArea", 60 | "IndepYear", 61 | "Population", 62 | "LifeExpectancy", 63 | "GNP", 64 | "GNPOld", 65 | "LocalName", 66 | "GovernmentForm", 67 | "HeadOfState", 68 | "Capital", 69 | "Code2" 70 | ] 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## 例38: `EXPLAIN`がインデックスの最大キー長を示す(utfmb4文字セットにて) 77 | 78 | ```sql 79 | ALTER TABLE Country CONVERT TO CHARACTER SET utf8mb4; 80 | EXPLAIN FORMAT=JSON 81 | SELECT * FROM Country WHERE name='Canada'; 82 | { 83 | "query_block": { 84 | "select_id": 1, 85 | "cost_info": { 86 | "query_cost": "1.20" 87 | }, 88 | "table": { 89 | "table_name": "Country", 90 | "access_type": "ref", 91 | "possible_keys": [ 92 | "Name" 93 | ], 94 | "key": "Name", 95 | "used_key_parts": [ 96 | "Name" 97 | ], 98 | "key_length": "208", # CHAR(52) * 4 = 208 99 | "ref": [ 100 | "const" 101 | ], 102 | "rows_examined_per_scan": 1, 103 | "rows_produced_per_join": 1, 104 | "filtered": "100.00", 105 | "cost_info": { 106 | "read_cost": "1.00", 107 | "eval_cost": "0.20", 108 | "prefix_cost": "1.20", 109 | "data_read_per_join": "968" 110 | }, 111 | "used_columns": [ 112 | "Code", 113 | "Name", 114 | "Continent", 115 | "Region", 116 | "SurfaceArea", 117 | "IndepYear", 118 | "Population", 119 | "LifeExpectancy", 120 | "GNP", 121 | "GNPOld", 122 | "LocalName", 123 | "GovernmentForm", 124 | "HeadOfState", 125 | "Capital", 126 | "Code2" 127 | ] 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | [^1]: 行形式に`DYNAMIC`、`COMPACT`および`COMPRESSED`を利用しているときは常に。通常は、以前の`REDUNDANT`行形式は実用的な利用例がありません。 134 | -------------------------------------------------------------------------------- /_sass/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Noto Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot'); 6 | src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot?#iefix') format('embedded-opentype'), 7 | local('Noto Sans'), 8 | local('Noto-Sans-regular'), 9 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff2') format('woff2'), 10 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff') format('woff'), 11 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.ttf') format('truetype'), 12 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.svg#NotoSans') format('svg'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Noto Sans'; 17 | font-weight: 700; 18 | font-style: normal; 19 | src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot'); 20 | src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot?#iefix') format('embedded-opentype'), 21 | local('Noto Sans Bold'), 22 | local('Noto-Sans-700'), 23 | url('../fonts/Noto-Sans-700/Noto-Sans-700.woff2') format('woff2'), 24 | url('../fonts/Noto-Sans-700/Noto-Sans-700.woff') format('woff'), 25 | url('../fonts/Noto-Sans-700/Noto-Sans-700.ttf') format('truetype'), 26 | url('../fonts/Noto-Sans-700/Noto-Sans-700.svg#NotoSans') format('svg'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Noto Sans'; 31 | font-weight: 400; 32 | font-style: italic; 33 | src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot'); 34 | src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot?#iefix') format('embedded-opentype'), 35 | local('Noto Sans Italic'), 36 | local('Noto-Sans-italic'), 37 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff2') format('woff2'), 38 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff') format('woff'), 39 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.ttf') format('truetype'), 40 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.svg#NotoSans') format('svg'); 41 | } 42 | 43 | @font-face { 44 | font-family: 'Noto Sans'; 45 | font-weight: 700; 46 | font-style: italic; 47 | src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot'); 48 | src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot?#iefix') format('embedded-opentype'), 49 | local('Noto Sans Bold Italic'), 50 | local('Noto-Sans-700italic'), 51 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2') format('woff2'), 52 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff') format('woff'), 53 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf') format('truetype'), 54 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg#NotoSans') format('svg'); 55 | } 56 | -------------------------------------------------------------------------------- /_sass/jekyll-theme-minimal.scss: -------------------------------------------------------------------------------- 1 | @import "fonts"; 2 | @import "rouge-github"; 3 | 4 | body { 5 | background-color: #fff; 6 | padding:50px; 7 | font: 14px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | color:#727272; 9 | font-weight:400; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | color:#222; 14 | margin:0 0 20px; 15 | } 16 | 17 | p, ul, ol, table, pre, dl { 18 | margin:0 0 20px; 19 | } 20 | 21 | h1, h2, h3 { 22 | line-height:1.1; 23 | } 24 | 25 | h1 { 26 | font-size:28px; 27 | } 28 | 29 | h2 { 30 | color:#393939; 31 | } 32 | 33 | h3, h4, h5, h6 { 34 | color:#494949; 35 | } 36 | 37 | a { 38 | color:#39c; 39 | text-decoration:none; 40 | } 41 | 42 | a:hover { 43 | color:#069; 44 | } 45 | 46 | a small { 47 | font-size:11px; 48 | color:#777; 49 | margin-top:-0.3em; 50 | display:block; 51 | } 52 | 53 | a:hover small { 54 | color:#777; 55 | } 56 | 57 | .wrapper { 58 | width:860px; 59 | margin:0 auto; 60 | } 61 | 62 | blockquote { 63 | border-left:1px solid #e5e5e5; 64 | margin:0; 65 | padding:0 0 0 20px; 66 | font-style:italic; 67 | } 68 | 69 | code, pre { 70 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace; 71 | color:#333; 72 | font-size:12px; 73 | } 74 | 75 | pre { 76 | padding:8px 15px; 77 | background: #f8f8f8; 78 | border-radius:5px; 79 | border:1px solid #e5e5e5; 80 | overflow-x: auto; 81 | } 82 | 83 | table { 84 | width:100%; 85 | border-collapse:collapse; 86 | } 87 | 88 | th, td { 89 | text-align:left; 90 | padding:5px 10px; 91 | border-bottom:1px solid #e5e5e5; 92 | } 93 | 94 | dt { 95 | color:#444; 96 | font-weight:700; 97 | } 98 | 99 | th { 100 | color:#444; 101 | } 102 | 103 | img { 104 | max-width:100%; 105 | } 106 | 107 | header { 108 | width:270px; 109 | float:left; 110 | position:fixed; 111 | -webkit-font-smoothing:subpixel-antialiased; 112 | } 113 | 114 | header ul { 115 | list-style:none; 116 | height:40px; 117 | padding:0; 118 | background: #f4f4f4; 119 | border-radius:5px; 120 | border:1px solid #e0e0e0; 121 | width:270px; 122 | } 123 | 124 | //header li { 125 | // width:89px; 126 | // float:left; 127 | // border-right:1px solid #e0e0e0; 128 | // height:40px; 129 | //} 130 | 131 | header li:first-child a { 132 | border-radius:5px 0 0 5px; 133 | } 134 | 135 | header li:last-child a { 136 | border-radius:0 5px 5px 0; 137 | } 138 | 139 | header ul a { 140 | line-height:1; 141 | font-size:11px; 142 | color:#999; 143 | display:block; 144 | text-align:center; 145 | padding-top:6px; 146 | height:34px; 147 | } 148 | 149 | header ul a:hover { 150 | color:#999; 151 | } 152 | 153 | header ul a:active { 154 | background-color:#f0f0f0; 155 | } 156 | 157 | strong { 158 | color:#222; 159 | font-weight:700; 160 | } 161 | 162 | header ul li + li + li { 163 | border-right:none; 164 | width:89px; 165 | } 166 | 167 | header ul a strong { 168 | font-size:14px; 169 | display:block; 170 | color:#222; 171 | } 172 | 173 | section { 174 | width:500px; 175 | float:right; 176 | padding-bottom:50px; 177 | } 178 | 179 | small { 180 | font-size:11px; 181 | } 182 | 183 | hr { 184 | border:0; 185 | background:#e5e5e5; 186 | height:1px; 187 | margin:0 0 20px; 188 | } 189 | 190 | footer { 191 | width:270px; 192 | float:left; 193 | position:fixed; 194 | bottom:50px; 195 | -webkit-font-smoothing:subpixel-antialiased; 196 | } 197 | 198 | @media print, screen and (max-width: 960px) { 199 | 200 | div.wrapper { 201 | width:auto; 202 | margin:0; 203 | } 204 | 205 | header, section, footer { 206 | float:none; 207 | position:static; 208 | width:auto; 209 | } 210 | 211 | header { 212 | padding-right:320px; 213 | } 214 | 215 | section { 216 | border:1px solid #e5e5e5; 217 | border-width:1px 0; 218 | padding:20px 0; 219 | margin:0 0 20px; 220 | } 221 | 222 | header a small { 223 | display:inline; 224 | } 225 | 226 | header ul { 227 | position:absolute; 228 | right:50px; 229 | top:52px; 230 | } 231 | } 232 | 233 | @media print, screen and (max-width: 720px) { 234 | body { 235 | word-wrap:break-word; 236 | } 237 | 238 | header { 239 | padding:0; 240 | } 241 | 242 | header ul, header p.view { 243 | position:static; 244 | } 245 | 246 | pre, code { 247 | word-wrap:normal; 248 | } 249 | } 250 | 251 | @media print, screen and (max-width: 480px) { 252 | body { 253 | padding:15px; 254 | } 255 | 256 | header ul { 257 | width:99%; 258 | } 259 | 260 | header li, header ul li + li + li { 261 | width:33%; 262 | } 263 | } 264 | 265 | @media print { 266 | body { 267 | padding:0.4in; 268 | font-size:12pt; 269 | color:#444; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /_sass/rouge-github.scss: -------------------------------------------------------------------------------- 1 | .highlight table td { padding: 5px; } 2 | .highlight table pre { margin: 0; } 3 | .highlight .cm { 4 | color: #999988; 5 | font-style: italic; 6 | } 7 | .highlight .cp { 8 | color: #999999; 9 | font-weight: bold; 10 | } 11 | .highlight .c1 { 12 | color: #999988; 13 | font-style: italic; 14 | } 15 | .highlight .cs { 16 | color: #999999; 17 | font-weight: bold; 18 | font-style: italic; 19 | } 20 | .highlight .c, .highlight .cd { 21 | color: #999988; 22 | font-style: italic; 23 | } 24 | .highlight .err { 25 | color: #a61717; 26 | background-color: #e3d2d2; 27 | } 28 | .highlight .gd { 29 | color: #000000; 30 | background-color: #ffdddd; 31 | } 32 | .highlight .ge { 33 | color: #000000; 34 | font-style: italic; 35 | } 36 | .highlight .gr { 37 | color: #aa0000; 38 | } 39 | .highlight .gh { 40 | color: #999999; 41 | } 42 | .highlight .gi { 43 | color: #000000; 44 | background-color: #ddffdd; 45 | } 46 | .highlight .go { 47 | color: #888888; 48 | } 49 | .highlight .gp { 50 | color: #555555; 51 | } 52 | .highlight .gs { 53 | font-weight: bold; 54 | } 55 | .highlight .gu { 56 | color: #aaaaaa; 57 | } 58 | .highlight .gt { 59 | color: #aa0000; 60 | } 61 | .highlight .kc { 62 | color: #000000; 63 | font-weight: bold; 64 | } 65 | .highlight .kd { 66 | color: #000000; 67 | font-weight: bold; 68 | } 69 | .highlight .kn { 70 | color: #000000; 71 | font-weight: bold; 72 | } 73 | .highlight .kp { 74 | color: #000000; 75 | font-weight: bold; 76 | } 77 | .highlight .kr { 78 | color: #000000; 79 | font-weight: bold; 80 | } 81 | .highlight .kt { 82 | color: #445588; 83 | font-weight: bold; 84 | } 85 | .highlight .k, .highlight .kv { 86 | color: #000000; 87 | font-weight: bold; 88 | } 89 | .highlight .mf { 90 | color: #009999; 91 | } 92 | .highlight .mh { 93 | color: #009999; 94 | } 95 | .highlight .il { 96 | color: #009999; 97 | } 98 | .highlight .mi { 99 | color: #009999; 100 | } 101 | .highlight .mo { 102 | color: #009999; 103 | } 104 | .highlight .m, .highlight .mb, .highlight .mx { 105 | color: #009999; 106 | } 107 | .highlight .sb { 108 | color: #d14; 109 | } 110 | .highlight .sc { 111 | color: #d14; 112 | } 113 | .highlight .sd { 114 | color: #d14; 115 | } 116 | .highlight .s2 { 117 | color: #d14; 118 | } 119 | .highlight .se { 120 | color: #d14; 121 | } 122 | .highlight .sh { 123 | color: #d14; 124 | } 125 | .highlight .si { 126 | color: #d14; 127 | } 128 | .highlight .sx { 129 | color: #d14; 130 | } 131 | .highlight .sr { 132 | color: #009926; 133 | } 134 | .highlight .s1 { 135 | color: #d14; 136 | } 137 | .highlight .ss { 138 | color: #990073; 139 | } 140 | .highlight .s { 141 | color: #d14; 142 | } 143 | .highlight .na { 144 | color: #008080; 145 | } 146 | .highlight .bp { 147 | color: #999999; 148 | } 149 | .highlight .nb { 150 | color: #0086B3; 151 | } 152 | .highlight .nc { 153 | color: #445588; 154 | font-weight: bold; 155 | } 156 | .highlight .no { 157 | color: #008080; 158 | } 159 | .highlight .nd { 160 | color: #3c5d5d; 161 | font-weight: bold; 162 | } 163 | .highlight .ni { 164 | color: #800080; 165 | } 166 | .highlight .ne { 167 | color: #990000; 168 | font-weight: bold; 169 | } 170 | .highlight .nf { 171 | color: #990000; 172 | font-weight: bold; 173 | } 174 | .highlight .nl { 175 | color: #990000; 176 | font-weight: bold; 177 | } 178 | .highlight .nn { 179 | color: #555555; 180 | } 181 | .highlight .nt { 182 | color: #000080; 183 | } 184 | .highlight .vc { 185 | color: #008080; 186 | } 187 | .highlight .vg { 188 | color: #008080; 189 | } 190 | .highlight .vi { 191 | color: #008080; 192 | } 193 | .highlight .nv { 194 | color: #008080; 195 | } 196 | .highlight .ow { 197 | color: #000000; 198 | font-weight: bold; 199 | } 200 | .highlight .o { 201 | color: #000000; 202 | font-weight: bold; 203 | } 204 | .highlight .w { 205 | color: #bbbbbb; 206 | } 207 | .highlight { 208 | background-color: #f8f8f8; 209 | } 210 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 非公式MySQL 8.0オプティマイザガイド 日本語版 3 | original_url: http://www.unofficialmysqlguide.com/ 4 | translator: doublemarket (@dblmkt) 5 | --- 6 | 7 | [The Unofficial MySQL 8.0 Optimizer Guide](http://www.unofficialmysqlguide.com/)の日本語訳です。 8 | 9 | ## 原文のChangelog 10 | 11 | - 2016-09-26: Initial Release 12 | 13 | ## 原文の著者について 14 | 15 | 16 | 17 | Morgan Tockerは、OracleのMySQLプロダクトマネージャーです。以前はサポートやトレーニング、コミュニティー運営を含む様々な仕事に携わってきました。Morganはカナダのトロントをベースにしています。 18 | 19 | Twitterアカウントは[@morgo](https://twitter.com/morgo)です。 20 | 21 | ## 謝辞 22 | 23 | - サーバーアーキテクチャー、コストモデル、クエリーオプティマイザの図を提供してくれたMySQLオプティマイザチームに感謝します。 24 | - 編集してくれたChrissy Cuttingに感謝します。 25 | 26 | ## 重要事項 27 | 28 | ここに出てくる例は全てworldサンプルデータベースを元にしています。データベースは[http://dev.mysql.com/doc/index-other.html](http://dev.mysql.com/doc/index-other.html)からダウンロードできます。 29 | --------------------------------------------------------------------------------