ホームページ  >  記事  >  データベース  >  MySQL インデックス最適化事例の分析

MySQL インデックス最適化事例の分析

PHPz
PHPz転載
2023-05-28 12:25:58807ブラウズ

    データの準備

    CREATE TABLE `employees` (
     `id` int(11) NOT NULL AUTO_INCREMENT,
     `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
     `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
     `position` varchar(20) NOT NULL DEFAULT '' COMMENT '职位',
     `hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间',
     PRIMARY KEY (`id`),
     KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE
     ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='员工记录表';
    
     INSERT INTO employees(name,age,position,hire_time) VALUES('LiLei',22,'manager',NOW());
     INSERT INTO employees(name,age,position,hire_time) VALUES('HanMeimei', 23,'dev',NOW());
     INSERT INTO employees(name,age,position,hire_time) VALUES('Lucy',23,'dev',NOW());
    
     ‐‐ 插入一些示例数据
     drop procedure if exists insert_emp;
     delimiter ;;
     create procedure insert_emp()
     begin
     declare i int;
     set i=1;
     while(i<=100000)do
     insert into employees(name,age,position) values(CONCAT(&#39;zhuge&#39;,i),i,&#39;dev&#39;);
     set i=i+1;
     end while;
     end;;
     delimiter ;
     call insert_emp();

    上記のように、主キー インデックスと (名前、年齢、位置 ) ジョイント インデックス、 次のクエリの例を見てください: ジョイント インデックスの最初のフィールドに範囲クエリを使用します

    EXPLAIN SELECT * FROM employees WHERE name > &#39;LiLei&#39; AND age = 22 AND position =&#39;manager&#39;;

    MySQL インデックス最適化事例の分析結論: ユニオンは、インデックスの最初のフィールドに範囲検索を使用し、インデックスを経由しません。MySQL は内部的に、

    が最初のフィールドに範囲

    を使用すると考えている可能性があります。結果セットは非常に優れたものになるはずです。サイズが大きいため、テーブルの戻り効率が高くありません。テーブル全体をスキャンすることをお勧めします。強制インデックス作成

    EXPLAIN SELECT * FROM employees force index(idx_name_age_position) WHERE name > &#39;LiLei&#39; AND age = 22 AND position =&#39;manager&#39;;

    MySQL インデックス最適化事例の分析結論: 強制インデックス作成は使用されますが、 、結合インデックスの最初のフィールド範囲検索もインデックスを使用し、スキャンされた行は少し少ないように見えますが、テーブルの戻り効率が高くないため、最終的な検索効率は必ずしもフルテーブルスキャンよりも高いとは限りません。この方法は、強制インデックス作成後に効率が大幅に向上することを証明する証拠がない限り、通常は使用されません

    Covering Index Optimization

    EXPLAIN SELECT name,age,position FROM employees WHERE name > &#39;LiLei&#39; AND age = 22 AND position =&#39;manager&#39;;

    MySQL インデックス最適化事例の分析 Change

    select *

    から 名前、年齢、役職を選択し、インデックスに既に存在する項目のみを選択します 既存の列をテーブルに返す必要がないため、インデックスが使用されます in および or インデックスが使用される場合

    テーブル内のデータ量が比較的多い場合、インデックスが使用されます。データ量 まれに、テーブル全体のスキャンが実行されます。例は次のとおりです:

    in query

    EXPLAIN SELECT * FROM employees WHERE name in (&#39;LiLei&#39;,&#39;HanMeimei&#39;,&#39;Lucy&#39;) AND age = 22 AND position =&#39;manager&#39;;

    ## uses all Index MySQL インデックス最適化事例の分析or query

    EXPLAIN SELECT * FROM employees WHERE (name = &#39;LiLei&#39; or name = &#39;HanMeimei&#39;) AND age = 22 AND position =&#39;manager&#39;;

    すべてのインデックスを使用MySQL インデックス最適化事例の分析

    以下に新しいemployees_copyテーブルを作成します。構造はemployeeと同じですが、データは3つだけであり、実行します。上記 2 つのクエリ

    #クエリ内

    #

    EXPLAIN SELECT * FROM employees_copy WHERE name in (&#39;LiLei&#39;,&#39;HanMeimei&#39;,&#39;Lucy&#39;) AND age = 22 AND position =&#39;manager&#39;;

    フル テーブル スキャン

    MySQL インデックス最適化事例の分析 またはクエリ

    EXPLAIN SELECT * FROM employees_copy WHERE (name = &#39;LiLei&#39; or name = &#39;HanMeimei&#39;) AND age = 22 AND position =&#39;manager&#39;;

    フル テーブル スキャン

    like xx% は通常インデックスを使用し、データ量とは関係ありませんMySQL インデックス最適化事例の分析

    Large table

    EXPLAIN SELECT * FROM employees WHERE name like &#39;LiLei%&#39; AND age = 22 AND position =&#39;manager&#39;;

    Small table

    EXPLAIN SELECT * FROM employees_copy WHERE name like &#39;LiLei%&#39; AND age = 22 AND position =&#39;manager&#39;;

    MySQL インデックス最適化事例の分析

    テーブル内のデータのサイズに関係なく、なぜ毛織物なのか?

    実際には、インデックス プッシュダウンの最適化を使用するなど、MySQL インデックス最適化事例の分析

    インデックス プッシュダウン

    補助ジョイント インデックスの場合、通常の状況では、左端のプレフィックスの原則に従います。 SELECT * from members where name like 'LiLei%' and age = 22 and Position = 'dev'

    この場合、名前フィールドに基づいてフィルタリングした後、年齢は名前フィールドのインデックスのみ使用されます。取得されたインデックス行内の位置は順序付けされていないため、インデックスを有効に活用できません。

    MySQL5.6

    より前のバージョンでは、このクエリは結合インデックス内の 'LiLei' で始まるインデックスのみと一致し、これらのインデックスに対応する主キーをテーブル 1 に返すことができました。主キー インデックスで対応するレコードを見つけて、2 つのフィールド age とposition の値を比較して、それらが一致するかどうかを確認します。

    MySQL 5.6 では、インデックス プッシュダウンの最適化が導入されています。インデックス トラバーサル プロセス中、 はまずインデックスに含まれるすべてのフィールドを確認し、条件を満たさないレコードを除外してから、テーブル ## に戻ります。 # 、テーブルの戻り数を効果的に減らすことができます。インデックス プッシュダウン最適化を使用した後、上記のクエリは、結合インデックス内の名前が 'LiLei' で始まるインデックスと一致し、インデックス内の 2 つのフィールド age とposition をフィルタリングしてから、残りのインデックスをフィルタリングします。対応する主キー ID は次のとおりです。テーブルに戻ってデータ行全体をチェックします。

    インデックス プッシュダウンにより、テーブルが返される回数が減ります。innodb エンジンのテーブル インデックス プッシュダウンは、セカンダリ インデックスにのみ使用できます。Innodb のプライマリ キー インデックス (クラスター化インデックス) ツリー リーフ ノードには、完全な行データが格納されます。今回のインデックスのプッシュダウンには、データ行全体のクエリを減らす効果はありません。 なぜ範囲検索がインデックス プッシュダウンで最適化されないのですか?

    Mysql は、範囲検索とフィルタリングの結果セットが大きすぎると考えていると推定されます。 like KK% ほとんどの場合、フィルタリングされた結果セットは比較的小さいため、ここで Mysql はto Like KK% はインデックス プッシュダウンの最適化を使用します。もちろん、これは絶対的なものではありません。場合によっては、like KK% が必ずしもインデックス プッシュダウンを使用するとは限りません。

    インデックスの選択方法

    まず、次の 2 つのクエリを見てください:

    <p><img src="https://img.php.cn/upload/article/000/000/164/168524796360235.png" alt="MySQL インデックス最適化事例の分析"></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796379545.png" alt="MySQL インデックス最適化事例の分析"></p> <p>同样的表,同样的字段,因为条件的不同,选择的索引也不同,MySQL 是如何选择的呢?</p> <h3>Trace 工具</h3> <p><strong>MySQl 提供了一个工具,可以看到选择索引的计算过程, 用法如下:</strong></p><pre class="brush:sql;">mysql&gt; set session optimizer_trace=&quot;enabled=on&quot;,end_markers_in_json=on; --开启trace mysql&gt; select * from employees where name &gt; &amp;#39;a&amp;#39; order by position; mysql&gt; SELECT * FROM information_schema.OPTIMIZER_TRACE;</pre><p>下面是对 trace 字段的解析</p><pre class="brush:sql;">{ &quot;steps&quot;: [ { &quot;join_preparation&quot;: { //第一阶段:SQL准备阶段,格式化sql &quot;select#&quot;: 1, &quot;steps&quot;: [ { &quot;expanded_query&quot;: &quot;/* select#1 */ select `employees`.`id` AS `id`,`employees`.`name` AS `name`,`employees`.`age` AS `age`,`employees`.`position` AS `position`,`employees`.`hire_time` AS `hire_time` from `employees` where (`employees`.`name` &gt; &amp;#39;a&amp;#39;) order by `employees`.`position`&quot; } ] /* steps */ } /* join_preparation */ }, { &quot;join_optimization&quot;: { //第二阶段:SQL优化阶段 &quot;select#&quot;: 1, &quot;steps&quot;: [ { &quot;condition_processing&quot;: { //条件处理 &quot;condition&quot;: &quot;WHERE&quot;, &quot;original_condition&quot;: &quot;(`employees`.`name` &gt; &amp;#39;a&amp;#39;)&quot;, &quot;steps&quot;: [ { &quot;transformation&quot;: &quot;equality_propagation&quot;, &quot;resulting_condition&quot;: &quot;(`employees`.`name` &gt; &amp;#39;a&amp;#39;)&quot; }, { &quot;transformation&quot;: &quot;constant_propagation&quot;, &quot;resulting_condition&quot;: &quot;(`employees`.`name` &gt; &amp;#39;a&amp;#39;)&quot; }, { &quot;transformation&quot;: &quot;trivial_condition_removal&quot;, &quot;resulting_condition&quot;: &quot;(`employees`.`name` &gt; &amp;#39;a&amp;#39;)&quot; } ] /* steps */ } /* condition_processing */ }, { &quot;substitute_generated_columns&quot;: { } /* substitute_generated_columns */ }, { &quot;table_dependencies&quot;: [ //表依赖详情 { &quot;table&quot;: &quot;`employees`&quot;, &quot;row_may_be_null&quot;: false, &quot;map_bit&quot;: 0, &quot;depends_on_map_bits&quot;: [ ] /* depends_on_map_bits */ } ] /* table_dependencies */ }, { &quot;ref_optimizer_key_uses&quot;: [ ] /* ref_optimizer_key_uses */ }, { &quot;rows_estimation&quot;: [ //预估表的访问成本 { &quot;table&quot;: &quot;`employees`&quot;, &quot;range_analysis&quot;: { &quot;table_scan&quot;: { //全表扫描情况 &quot;rows&quot;: 10123, //扫描行数 &quot;cost&quot;: 2054.7 //查询成本 } /* table_scan */, &quot;potential_range_indexes&quot;: [ //查询可能使用的索引 { &quot;index&quot;: &quot;PRIMARY&quot;, //主键索引 &quot;usable&quot;: false, &quot;cause&quot;: &quot;not_applicable&quot; }, { &quot;index&quot;: &quot;idx_name_age_position&quot;, //辅助索引 &quot;usable&quot;: true, &quot;key_parts&quot;: [ &quot;name&quot;, &quot;age&quot;, &quot;position&quot;, &quot;id&quot; ] /* key_parts */ } ] /* potential_range_indexes */, &quot;setup_range_conditions&quot;: [ ] /* setup_range_conditions */, &quot;group_index_range&quot;: { &quot;chosen&quot;: false, &quot;cause&quot;: &quot;not_group_by_or_distinct&quot; } /* group_index_range */, &quot;analyzing_range_alternatives&quot;: { //分析各个索引使用成本 &quot;range_scan_alternatives&quot;: [ { &quot;index&quot;: &quot;idx_name_age_position&quot;, &quot;ranges&quot;: [ &quot;a &lt; name&quot; //索引使用范围 ] /* ranges */, &quot;index_dives_for_eq_ranges&quot;: true, &quot;rowid_ordered&quot;: false, //使用该索引获取的记录是否按照主键排序 &quot;using_mrr&quot;: false, &quot;index_only&quot;: false, //是否使用覆盖索引 &quot;rows&quot;: 5061, //索引扫描行数 &quot;cost&quot;: 6074.2, //索引使用成本 &quot;chosen&quot;: false, //是否选择该索引 &quot;cause&quot;: &quot;cost&quot; } ] /* range_scan_alternatives */, &quot;analyzing_roworder_intersect&quot;: { &quot;usable&quot;: false, &quot;cause&quot;: &quot;too_few_roworder_scans&quot; } /* analyzing_roworder_intersect */ } /* analyzing_range_alternatives */ } /* range_analysis */ } ] /* rows_estimation */ }, { &quot;considered_execution_plans&quot;: [ { &quot;plan_prefix&quot;: [ ] /* plan_prefix */, &quot;table&quot;: &quot;`employees`&quot;, &quot;best_access_path&quot;: { //最优访问路径 &quot;considered_access_paths&quot;: [ //最终选择的访问路径 { &quot;rows_to_scan&quot;: 10123, &quot;access_type&quot;: &quot;scan&quot;, //访问类型:为scan,全表扫描 &quot;resulting_rows&quot;: 10123, &quot;cost&quot;: 2052.6, &quot;chosen&quot;: true, //确定选择 &quot;use_tmp_table&quot;: true } ] /* considered_access_paths */ } /* best_access_path */, &quot;condition_filtering_pct&quot;: 100, &quot;rows_for_plan&quot;: 10123, &quot;cost_for_plan&quot;: 2052.6, &quot;sort_cost&quot;: 10123, &quot;new_cost_for_plan&quot;: 12176, &quot;chosen&quot;: true } ] /* considered_execution_plans */ }, { &quot;attaching_conditions_to_tables&quot;: { &quot;original_condition&quot;: &quot;(`employees`.`name` &gt; &amp;#39;a&amp;#39;)&quot;, &quot;attached_conditions_computation&quot;: [ ] /* attached_conditions_computation */, &quot;attached_conditions_summary&quot;: [ { &quot;table&quot;: &quot;`employees`&quot;, &quot;attached&quot;: &quot;(`employees`.`name` &gt; &amp;#39;a&amp;#39;)&quot; } ] /* attached_conditions_summary */ } /* attaching_conditions_to_tables */ }, { &quot;clause_processing&quot;: { &quot;clause&quot;: &quot;ORDER BY&quot;, &quot;original_clause&quot;: &quot;`employees`.`position`&quot;, &quot;items&quot;: [ { &quot;item&quot;: &quot;`employees`.`position`&quot; } ] /* items */, &quot;resulting_clause_is_simple&quot;: true, &quot;resulting_clause&quot;: &quot;`employees`.`position`&quot; } /* clause_processing */ }, { &quot;reconsidering_access_paths_for_index_ordering&quot;: { &quot;clause&quot;: &quot;ORDER BY&quot;, &quot;steps&quot;: [ ] /* steps */, &quot;index_order_summary&quot;: { &quot;table&quot;: &quot;`employees`&quot;, &quot;index_provides_order&quot;: false, &quot;order_direction&quot;: &quot;undefined&quot;, &quot;index&quot;: &quot;unknown&quot;, &quot;plan_changed&quot;: false } /* index_order_summary */ } /* reconsidering_access_paths_for_index_ordering */ }, { &quot;refine_plan&quot;: [ { &quot;table&quot;: &quot;`employees`&quot; } ] /* refine_plan */ } ] /* steps */ } /* join_optimization */ }, { &quot;join_execution&quot;: { //第三阶段:SQL执行阶段 &quot;select#&quot;: 1, &quot;steps&quot;: [ ] /* steps */ } /* join_execution */ } ] /* steps */ } // 结论:全表扫描的成本低于索引扫描,所以mysql最终选择全表扫描</pre><pre class="brush:sql;">mysql&gt; select * from employees where name &gt; &amp;#39;zzz&amp;#39; order by position; mysql&gt; SELECT * FROM information_schema.OPTIMIZER_TRACE; # 查看trace字段可知索引扫描的成本低于全表扫描,所以mysql最终选择索引扫描 mysql&gt; set session optimizer_trace=&quot;enabled=off&quot;; //关闭trace</pre><h3>深入优化</h3> <h4>order by 和 group by</h4> <p><strong>order by 和 group by 也会遵循左前缀法则, 如下例子 :</strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796434966.png" alt="MySQL インデックス最適化事例の分析"></p> <p>根据左前缀法则,用到了 name 字段的索引,同时使用 age 字段用来排序, 因为 extra 种没有 filesort</p> <p>order by 或者 group by 用到的索引不会参与到 key_len 的计算,索引 key_len 仍然只是 74, 即 name字段的长度</p> <p><strong>再看下面一个例子:</strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796417874.png" alt="MySQL インデックス最適化事例の分析"></p> <p>where 条件是name 排序字段是 position 跳过了age字段,所以只能用 name 索引,无法利用 position 索引进行索引排序,用到是文件排序</p> <p><strong>再看第三个例子:</strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796410497.png" alt="MySQL インデックス最適化事例の分析"></p> <p>使用name条件查询, 同时使用 age position 双字段排序,没有跳过联合索引的字段. 所以可以用索引排序</p> <p><strong>然后颠倒一下排序顺序,先position 再 age: </strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796431356.png" alt="MySQL インデックス最適化事例の分析"></p> <p>发现此时只能文件排序了</p> <p>再看下面的例子</p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796485551.png" alt="MySQL インデックス最適化事例の分析"></p> <p>虽然排序字段与索引字段不一样,但仍然是索引排序, 因为查询条件中 用到是 (name, age)索引,排序中用到是 position 索引,并没有颠倒顺序。所以还是索引排序</p> <p><strong>如果一个正序一个倒序呢? </strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796432204.png" alt="MySQL インデックス最適化事例の分析"></p> <p>虽然排序字段与索引字段顺序相同, 但是 age 是正序, position 是倒叙,导致与索引的排序方式不同,无法利用索引。Mysql8及以上版本可以使用降序索引来支持文件排序。</p> <p><strong>先 in 查询: </strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796494284.png" alt="MySQL インデックス最適化事例の分析"></p> <p> 对于排序来说,多个相等条件也是范围查询, 无法利用索引排序</p> <p><strong>先范围查询: </strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796573511.png" alt="MySQL インデックス最適化事例の分析"></p> <p><strong>这里发生了全表扫描,没有任何索引,排序自然也无法利用索引了,可以使用覆盖索引优化: </strong></p> <p><img src="https://img.php.cn/upload/article/000/000/164/168524796579087.png" alt="MySQL インデックス最適化事例の分析"></p> <h3>优化总结</h3> <ul class=" list-paddingleft-2"><li><p>MySQL支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序。index效率高,filesort效率低。</p></li></ul> <p>2、order by满足两种情况会使用Using index。</p> <ul class=" list-paddingleft-2"> <li><p>order by语句使用索引最左前列。</p></li> <li><p>使用where子句与order by子句条件列组合满足索引最左前列。</p></li> <li><p>尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀法则。</p></li> <li><p>如果order by的条件不在索引列上,就会产生Using filesort。</p></li> <li><p>能用覆盖索引尽量用覆盖索引</p></li> <li><p>group by与order by很类似,其实质是先排序后分组,遵照索引创建顺序的最左前缀法则。对于group by的优化如果不需要排序的可以加上<strong>order by null禁止排序</strong>。注意,<strong>where高于having,能写在where中的限定条件就不要去having限定了。</strong></p></li> </ul> <h3>Using filesort文件排序原理详解</h3> <h4>单路排序模式;</h4> <p>是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可以看到sort_mode信息里显示或者</p> <h4>双路排序(又叫回表排序模式)</h4> <p>是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行 ID,然后在 sort buffer 中进行排序,排序完后需要再次取回其它需要的字段;用trace工具可以看到sort_mode信息里显示</p> <p>MySQL 通过比较系统变量 <code>max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来判断使用哪种排序模式。

    • 如果 字段的总长度小于max_length_for_sort_data ,那么使用 单路排序模式;

    • 如果 字段的总长度大于max_length_for_sort_data ,那么使用 双路排序模·式。

    分页优化

    常规的limit分页

    有如下查询语句

    select * from employees limit 10000,10;

    该sql并不是只查询了10条,而是查找了10010条,然后把前10000条结果给舍弃掉, 因此要查询一个大表靠后的内容,执行效率是非常低的

    优化

    根据主键排序

    上面的下面的sql语句没有指定排序方式,默认使用ID排序。当使用ID排序时,我们可以使用下面的优化。

    select * from employees where id > 90000 limit 5;

    如果id是连续自增的,和limit 90000,5 结果没有差别,是 90001 ~ 90005 的数据。

    但是如果在90000之前删除了一条数据,结果就不一样了,id > 90000 limit 5 的结果是 90001 ~ 90005, 但是limit 90000, 5 的结果是 90002 ~ 90006, 很明显 90002 ~ 90006 才是符合我们直觉的。所以这个优化只能限制与排序条件是连续的。如果id不是自增的呢?会出现什么情况,假如 90000 这条数据有两个,limit 90000, 5 的结果是 90000 ~ 90004,而 id > 90000 limit 5 的结果仍是 90001 ~ 90005, 会把 id= 90000 的数据漏掉一条。

    所以这个优化只能用于排序的字段是连续自增的,并且不能重复

    非主键排序的优化

    有如下查询语句

    EXPLAIN select * from employees ORDER BY name limit 90000,5;

    MySQL インデックス最適化事例の分析

    发现并没有用上name的索引,因为 select * ,扫描联合索引时,无法的到全部数据,需要回表,成本比全表扫描更高,所以优化器放弃使用索引。

    可以使用索引覆盖的方法,使用分页查询仅仅找到少量的主键,然后在使用主键查找整行数据, 如下:

    select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;

    看下执行计划:

    MySQL インデックス最適化事例の分析

    原 SQL 使用文件排序,优化后的使用索引排序

    表关联优化

    先造一些数据:

    CREATE TABLE `t1` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `a` int(11) DEFAULT NULL,
      `b` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `idx_a` (`a`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    create table t2 like t1;
    
    -- 插入一些示例数据
    -- 往t1表插入1万行记录
    drop procedure if exists insert_t1; 
    delimiter ;;
    create procedure insert_t1()
    begin
      declare i int;
      set i=1;
      while(i<=10000)do
        insert into t1(a,b) values(i,i);  
        set i=i+1;
      end while;
    end;;
    delimiter ;
    call insert_t1();
    
    -- 往t2表插入100行记录
    drop procedure if exists insert_t2; 
    delimiter ;;
    create procedure insert_t2()
    begin
      declare i int;
      set i=1;
      while(i<=100)do
        insert into t2(a,b) values(i,i);  
        set i=i+1;
      end while;
    end;;
    delimiter ;
    call insert_t2();

    新建 t1 t2 表,结构一样, 都在a字段上有索引,b字段没有索引,t1表有 10000 行记录,t2表只有100条记录。

    常见的表关联算法

    • 内嵌循环连接算法 Nested-Loop Join

    • 基于块的嵌套循环连接算法 Block Nested-Loop Join

    内嵌循环连接算法

    一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。

    一般关联字段有索引的时候使用这种算法, 示例:

    EXPLAIN select * from t1 inner join t2 on t1.a= t2.a;

    MySQL インデックス最適化事例の分析

    从执行计划中可以看到这些信息:

    • 驱动表是 t2,被驱动表是 t1。先执行的就是驱动表;优化器一般会优先选择小表做驱动表,用where条件过滤完驱动表,然后再跟被驱动表做关联查询。所以使用 inner join 时,排在前面的表并不一定就是驱动表

    • 当使用left join时,左表是驱动表,右表是被驱动表,当使用right join时,右表时驱动表,左表是被驱动表

    • 使用了 NLJ算法。一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer 则表示使用的 join 算法是 NLJ。

    上面sql的大致流程如下:

    • 从表 t2 中读取一行数据(如果t2表有查询过滤条件的,用先用条件过滤完,再从过滤结果里取出一行数据);

    • 从第 1 步的数据中,取出关联字段 a,到表 t1 中查找;

    • 取出表 t1 中满足条件的行,跟 t2 中获取到的结果合并,作为结果返回给客户端;

    • 重复上面 3 步。

    整个过程会读取 t2 表的所有数据(扫描100行),然后遍历这每行数据中字段 a 的值,根据 t2 表中 a 的值索引扫描 t1 表中的对应行(扫描100次 t1 表的索引,1次扫描可以认为最终只扫描 t1 表一行完整数据,也就是总共 t1 表也扫描了100行)。因此整个过程扫描了 200 行

    基于块的嵌套循环算法

    当关联字段没有没有索引的时候会使用这种算法

    把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。

    如下:

    EXPLAIN select * from t1 inner join t2 on t1.b= t2.b;

    MySQL インデックス最適化事例の分析

    Extra 中 的Using join buffer (Block Nested Loop)说明该关联查询使用的是 BNL 算法。

    上面sql的大致流程如下:

    • 把 t2 的所有数据放入到 join_buffer 中

    • 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比

    • 返回满足 join 条件的数据

    整个过程对表 t1 和 t2 都做了一次全表扫描,因此扫描的总行数为10000(表 t1 的数据总量) + 100(表 t2 的数据总量) = 10100并且 join_buffer 里的数据是无序的,因此对表 t1 中的每一行,都要做 100 次判断,所以内存中的判断次数是 100 * 10000= 100 万次。

    这个例子里表 t2 才 100 行,要是表 t2 是一个大表,join_buffer 放不下怎么办呢?·

    join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。如果放不下表 t2 的所有数据话,策略很简单,就是分段放

    比如 t2 表有1000行记录, join_buffer 一次只能放800行数据,那么执行过程就是先往 join_buffer 里放800行记录,然后从 t1 表里取数据跟 join_buffer 中数据对比得到部分结果,然后清空 join_buffer ,再放入 t2 表剩余200行记录,再次从 t1 表里取数据跟 join_buffer 中数据对比。所以就多扫了一次 t1 表。

    为什么要使用 BNLJ 算法呢?

    如果上面第二条sql使用 Nested-Loop Join,那么扫描行数为 100 * 10000 = 100万次,这个是磁盘扫描。

    很显然,用BNL磁盘扫描次数少很多,相比于磁盘扫描,BNL的内存计算会快得多。

    通常情况下,MySQL会采用 BNL 算法进行关联查询,当被驱动表的关联字段没有索引时。当存在索引时,使用NLJ算法可以获得比BNL算法更好的性能

    对于关联sql的优化

    • 关联字段加索引,让mysql做join操作时尽量选择NLJ算法,驱动表因为需要全部查询出来,所以过滤的条件也尽量要走索引,避免全表扫描,总之,能走索引的过滤条件尽量都走索引

    • 小表驱动大表,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去mysql优化器自己判断的时间

    straight_join解释:straight_join功能同join类似,但能让左边的表来驱动右边的表,能改表优化器对于联表查询的执行顺序。

    比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表指定mysql选着 t2 表作为驱动表。

    • straight_join只适用于inner join,并不适用于left join,right join。由于使用left join和right join已经明确了表的执行顺序

    • 尽可能让优化器去判断,因为大部分情况下mysql优化器是比人要聪明的。使用straight_join一定要慎重,因为部分情况下人为指定的执行顺序并不一定会比优化引擎要靠谱。

    小表的定义

    在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。不单单是表的总数据量

    in和exsits优化

    原则:小表驱动大表,即小的数据集驱动大的数据集

    • in:当B表的数据集小于A表的数据集时,in优于exists

    select * from A where id in (select id from B)
    #等价于:   
    for(select id from B){
        select * from A where A.id = B.id
    }
    • exists:当A表的数据集小于B表的数据集时,exists优于in

    将主查询A的数据,放到子查询B中做条件验证,根据验证结果(true或false)来决定主查询的数据是否保留

    select * from A where exists (select 1 from B where B.id = A.id)
    #等价于: 
    for(select * from A){
      select * from B where B.id = A.id 
    } 
    #A表与B表的ID字段应建立索引

    关于Exists

    • EXISTS (subquery)只返回TRUE或FALSE,因此子查询中的SELECT * 也可以用SELECT 1替换,官方说法是实际执行时会忽略SELECT清单,因此没有区别

    • EXISTS子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比

    • EXISTS子查询往往也可以用JOIN来代替,何种最优需要具体问题具体分析

    count 查询优化

    有下面四条查询语句:

    EXPLAIN select count(1) from employees;
    EXPLAIN select count(id) from employees;
    EXPLAIN select count(name) from employees;
    EXPLAIN select count(*) from employees;

    只有 count(字段名) 不会把该字段为null 计入总数

    其实上面四条的查询计划都一样,效率上没有太大的差别

    MySQL インデックス最適化事例の分析

    当字段有索引

    count(*)≈count(1)>count(字段)>count(主键 id)

    字段有索引,count(字段)统计走二级索引,二级索引存储数据比主键索引少,所以count(字段)>count(主键 id)

    当字段没有索引

    count(*)≈count(1)>count(主键 id)>count(字段)

    字段没有索引count(字段)统计走不了索引,count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)

    count(1)

    count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出字段,所以理论上count(1)比count(字段)会快一点。

    count(*)

    count(*) 是例外,mysql并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用count(列名)或count(常量)来替代 count(*)

    为什么对于count(id),mysql最终选择辅助索引而不是主键聚集索引?因为二级索引相对主键索引存储数据更少,检索性能应该更高,mysql内部做了点优化(应该是在5.7版本才优化)。

    常见优化方法

    自己维护的总行数

    show table status 如果只需要知道表总行数的估计值可以用如下sql查询,性能很高 show table status like 'employee' 

    MySQL インデックス最適化事例の分析

    将总数维护到Redis里 插入或删除表数据行的时候同时维护redis里的表总行数key的计数值(用incr或decr命令),但是这种方式可能不准,很难保证表操作和redis操作的事务一致性

    索引设计原则

    索引设计原则:

    1、代码先行,索引后上

    等到主体业务功能开发完毕,把涉及到该表相关sql都要拿出来分析之后再建立索引。

    2、联合索引尽量覆盖条件

    比如可以设计一个或者两三个联合索引(尽量少建单值索引),让每一个联合索引都尽量去包含sql语句里的where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原则。

    3、不要在小基数字段上建立索引

    索引基数是指这个字段在表里总共有多少个不同的值,比如一张表总共100万行记录,其中有个性别字段,其值不是男就是女,那么该字段的基数就是2。

    如果对这种小基数字段建立索引的话,还不如全表扫描了,因为你的索引树里就包含男和女两种值,根本没法进行快速的二分查找,那用索引就没有太大的意义了。

    一般建立索引,尽量使用那些基数比较大的字段,就是值比较多的字段,那么才能发挥出B+树快速二分查找的优势来。

    4、长字符串我们可以采用前缀索引

    尽量对字段类型较小的列设计索引,比如说什么tinyint之类的,因为字段类型较小的话,占用磁盘空间也会比较小,此时你在搜索的时候性能也会比较好一点。

    当然,这个所谓的字段类型小一点的列,也不是绝对的,很多时候你就是要针对varchar(255)这种字段建立索引,哪怕多占用一些磁盘空间也是有必要的。

    对于这种varchar(255)的大字段可能会比较占用磁盘空间,可以稍微优化下,比如针对这个字段的前20个字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于 KEY index(name(20),age,position)

    where 条件で検索するときに、名前フィールドに基づいて検索する場合は、まず名前フィールドの最初の 20 文字に基づいてインデックス ツリー内を検索し、次に最初の 20 文字を見つけます。プレフィックスが付いた部分データは、クラスター化インデックスに戻って、比較のために完全な名前フィールド値を抽出します。

    しかし、名前で並べ替える場合、名前にはインデックス ツリーの最初の 20 文字しか含まれていないため、この並べ替えには インデックスを付けることができず、同じことが group by にも当てはまります。

    5,

    where と order by の間に矛盾がある場合、where が優先されます

    where と order by の間にインデックス設計の矛盾がある場合、デザインはどこに基づいていますか? インデックス、または注文のデザイン インデックス? where にインデックスを使用させるべきでしょうか?それともインデックスを使用して order by を使用させるべきですか?

    一般的に、現時点では、指定されたデータの一部をすばやくフィルタリングするためにインデックスを使用するために where 条件がよく使用されます。 、並べ替えます。

    なぜなら、ほとんどの場合、インデックスに基づいてフィルタリングすると、必要なデータのごく一部をできるだけ早く除外できるため、並べ替えのコストがはるかに小さくなる可能性があるからです。

    例:

    名前、年齢、性別、役職列と結合インデックス (名前、年齢、性別、役職) を含む従業員テーブルがあります。 ## 性別:

    性別、値は 0 または 1

    次のクエリがあります: select id from members where name = 'zhangsan'、age = 18、position = 'dev' ジャンプのため、性別フィールドの後に位置はインデックスを使用できません

    性別には 2 つの値しかないため、次のようにクエリ ステートメントで性別のすべての値を列挙します。

    名前 = 'zhangsan'、年齢 = 18、性別 (0, 1)、役職 = 'dev'の従業員から ID を選択します。

    これすべてのインデックスを使用できるようになります。 別の例

    先週ログインしたユーザーをクエリしたい場合、最初に思い浮かぶのは

    last_login_time > {time one year ago}# です。

    # #これは範囲クエリです。後続のフィールドはすべてインデックスを使用できません。別のフィールド、recent_login_flag(tinyint) を設計して、ユーザーが最近ログインしたかどうかを識別できます。スケジュールされたタスクを使用して、このフィールドの値を定期的に更新します。このように、範囲クエリは同等のクエリになりますが、ビジネスで許可されているかどうかに応じて、データは時間内に変更されない可能性があります。

    つまり、インデックスを最大限に活用する方法を見つける必要があります。

    以上がMySQL インデックス最適化事例の分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

    声明:
    この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。