现象
新建了一张员工表,插入了少量数据,索引中所有的字段均在where条件出现时,正确走到了idx_nap索引,但是where出现部分自左开始的索引时,却进行全表扫描,与MySQL官方所说的最左匹配原则“相悖”。
数据背景 CREATE TABLE `staffs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名', `age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄', `pos` varchar(20) NOT NULL DEFAULT '' COMMENT '职位', `add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入职时间', PRIMARY KEY (`id`), KEY `idx_nap` (`name`,`age`,`pos`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='员工记录表';
表中数据如下: id name age pos add_time 1 July 23 dev 2018-06-04 16:02:02 2 Clive 22 dev 2018-06-04 16:02:32 3 Cleva 24 test 2018-06-04 16:02:38 4 July 23 test 2018-06-04 16:12:22 5 July 23 pre 2018-06-04 16:12:37 6 Clive 22 pre 2018-06-04 16:12:48 7 July 25 dev 2018-06-04 16:30:17
Explain语句看下执行计划 -- 全匹配走了索引 explain select * from staffs where name = 'July' and age = 23 and pos = 'dev'; id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE staffs NULL ref idx_nap idx_nap 140 const,const,const 1 100.00 NULL
开启优化器跟踪优化过程 -- 左侧部分匹配却没有走索引,全表扫描 explain select * from staffs where name = 'July' and age = 23; id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE staffs2 NULL ALL idx_nap NULL NULL NULL 6 50.00 Using where -- 开启优化器跟踪 set session optimizer_trace='enabled=on'; -- 在执行完查询语句后,在执行以下的select语句可以查看具体的优化器执行过程 select * from information_schema.optimizer_trace;
Trace部分的内容 { "steps": [ { "join_preparation": { "select#": 1, "steps": [ { "expanded_query": "/* select#1 */ select `staffs`.`id` AS `id`,`staffs`.`name` AS `name`,`staffs`.`age` AS `age`,`staffs`.`pos` AS `pos`,`staffs`.`add_time` AS `add_time` from `staffs` where ((`staffs`.`name` = 'July') and (`staffs`.`age` = 23))" } ] } }, { "join_optimization": { "select#": 1, "steps": [ { "condition_processing": { "condition": "WHERE", "original_condition": "((`staffs`.`name` = 'July') and (`staffs`.`age` = 23))", "steps": [ { "transformation": "equality_propagation", "resulting_condition": "((`staffs`.`name` = 'July') and multiple equal(23, `staffs`.`age`))" }, { "transformation": "constant_propagation", "resulting_condition": "((`staffs`.`name` = 'July') and multiple equal(23, `staffs`.`age`))" }, { "transformation": "trivial_condition_removal", "resulting_condition": "((`staffs`.`name` = 'July') and multiple equal(23, `staffs`.`age`))" } ] } }, { "substitute_generated_columns": { } }, { "table_dependencies": [ { "table": "`staffs`", "row_may_be_null": false, "map_bit": 0, "depends_on_map_bits": [ ] } ] }, { "ref_optimizer_key_uses": [ { "table": "`staffs`", "field": "name", "equals": "'July'", "null_rejecting": false }, { "table": "`staffs`", "field": "age", "equals": "23", "null_rejecting": false } ] }, { "rows_estimation": [ { "table": "`staffs`", "range_analysis": { "table_scan": { "rows": 6, "cost": 4.3 }, "potential_range_indexes": [ { "index": "PRIMARY", "usable": false, "cause": "not_applicable" }, { "index": "idx_nap", "usable": true, "key_parts": [ "name", "age", "pos", "id" ] } ], "setup_range_conditions": [ ], "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" }, "analyzing_range_alternatives": { "range_scan_alternatives": [ { "index": "idx_nap", "ranges": [ "July <= name <= July AND 23 <= age <= 23" ], "index_dives_for_eq_ranges": true, "rowid_ordered": false, "using_mrr": false, "index_only": false, "rows": 3, "cost": 4.61, "chosen": false, "cause": "cost" } ], "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } } } } ] }, { "considered_execution_plans": [ { "plan_prefix": [ ], "table": "`staffs`", "best_access_path": { "considered_access_paths": [ { //可以看到这边MySQL计算得到使用索引的成本为2.6 "access_type": "ref", "index": "idx_nap", "rows": 3, "cost": 2.6, "chosen": true }, { //而全表扫描计算所得的成本为2.2 "rows_to_scan": 6, "access_type": "scan", "resulting_rows": 6, "cost": 2.2, "chosen": true } ] }, //因此选择了成本更低的scan "condition_filtering_pct": 100, "rows_for_plan": 6, "cost_for_plan": 2.2, "chosen": true } ] }, { "attaching_conditions_to_tables": { "original_condition": "((`staffs`.`age` = 23) and (`staffs`.`name` = 'July'))", "attached_conditions_computation": [ ], "attached_conditions_summary": [ { "table": "`staffs`", "attached": "((`staffs`.`age` = 23) and (`staffs`.`name` = 'July'))" } ] } }, { "refine_plan": [ { "table": "`staffs`" } ] } ] } }, { "join_execution": { "select#": 1, "steps": [ ] } } ] }
增加表数据量 -- 接下来增大表的数据量 INSERT INTO `staffs` (`name`, `age`, `pos`, `add_time`) VALUES ('July', 25, 'dev', '2018-06-04 16:30:17'), ('July', 23, 'dev1', '2018-06-04 16:02:02'), ('July', 23, 'dev2', '2018-06-04 16:02:02'), ('July', 23, 'dev3', '2018-06-04 16:02:02'), ('July', 23, 'dev4', '2018-06-04 16:02:02'), ('July', 23, 'dev6', '2018-06-04 16:02:02'), ('July', 23, 'dev5', '2018-06-04 16:02:02'), ('July', 23, 'dev7', '2018-06-04 16:02:02'), ('July', 23, 'dev8', '2018-06-04 16:02:02'), ('July', 23, 'dev9', '2018-06-04 16:02:02'), ('July', 23, 'dev10', '2018-06-04 16:02:02'), ('Clive', 23, 'dev1', '2018-06-04 16:02:02'), ('Clive', 23, 'dev2', '2018-06-04 16:02:02'), ('Clive', 23, 'dev3', '2018-06-04 16:02:02'), ('Clive', 23, 'dev4', '2018-06-04 16:02:02'), ('Clive', 23, 'dev6', '2018-06-04 16:02:02'), ('Clive', 23, 'dev5', '2018-06-04 16:02:02'), ('Clive', 23, 'dev7', '2018-06-04 16:02:02'), ('Clive', 23, 'dev8', '2018-06-04 16:02:02'), ('Clive', 23, 'dev9', '2018-06-04 16:02:02'), ('Clive', 23, 'dev10', '2018-06-04 16:02:02');
执行Explain -- 再次执行同样的查询语句,会发现走到索引上了 explain select * from staffs where name = 'July' and age = 23; id select_type table partitions type possible_keys key key_len ref rows filtered Extra 1 SIMPLE staffs NULL ref idx_nap idx_nap 78 const,const 13 100.00 NULL
查看新的Trace内容 -- 再看下优化器执行过程 { "steps": [ { "join_preparation": { "select#": 1, "steps": [ { "expanded_query": "/* select#1 */ select `staffs`.`id` AS `id`,`staffs`.`name` AS `name`,`staffs`.`age` AS `age`,`staffs`.`pos` AS `pos`,`staffs`.`add_time` AS `add_time` from `staffs` where ((`staffs`.`name` = 'July') and (`staffs`.`age` = 23))" } ] } }, { "join_optimization": { "select#": 1, "steps": [ { "condition_processing": { "condition": "WHERE", "original_condition": "((`staffs`.`name` = 'July') and (`staffs`.`age` = 23))", "steps": [ { "transformation": "equality_propagation", "resulting_condition": "((`staffs`.`name` = 'July') and multiple equal(23, `staffs`.`age`))" }, { "transformation": "constant_propagation", "resulting_condition": "((`staffs`.`name` = 'July') and multiple equal(23, `staffs`.`age`))" }, { "transformation": "trivial_condition_removal", "resulting_condition": "((`staffs`.`name` = 'July') and multiple equal(23, `staffs`.`age`))" } ] } }, { "substitute_generated_columns": { } }, { "table_dependencies": [ { "table": "`staffs`", "row_may_be_null": false, "map_bit": 0, "depends_on_map_bits": [ ] } ] }, { "ref_optimizer_key_uses": [ { "table": "`staffs`", "field": "name", "equals": "'July'", "null_rejecting": false }, { "table": "`staffs`", "field": "age", "equals": "23", "null_rejecting": false } ] }, { "rows_estimation": [ { "table": "`staffs`", "range_analysis": { "table_scan": { "rows": 27, "cost": 8.5 }, "potential_range_indexes": [ { "index": "PRIMARY", "usable": false, "cause": "not_applicable" }, { "index": "idx_nap", "usable": true, "key_parts": [ "name", "age", "pos", "id" ] } ], "setup_range_conditions": [ ], "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" }, "analyzing_range_alternatives": { "range_scan_alternatives": [ { "index": "idx_nap", "ranges": [ "July <= name <= July AND 23 <= age <= 23" ], "index_dives_for_eq_ranges": true, "rowid_ordered": false, "using_mrr": false, "index_only": false, "rows": 13, "cost": 16.61, "chosen": false, "cause": "cost" } ], "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } } } } ] }, { "considered_execution_plans": [ { "plan_prefix": [ ], "table": "`staffs`", "best_access_path": { "considered_access_paths": [ { //使用索引的成本变为了5.3 "access_type": "ref", "index": "idx_nap", "rows": 13, "cost": 5.3, "chosen": true }, { //scan的成本变为了6.4 "rows_to_scan": 27, "access_type": "scan", "resulting_rows": 27, "cost": 6.4, "chosen": false } ] }, //使用索引查询的成本更低,因此选择了走索引 "condition_filtering_pct": 100, "rows_for_plan": 13, "cost_for_plan": 5.3, "chosen": true } ] }, { "attaching_conditions_to_tables": { "original_condition": "((`staffs`.`age` = 23) and (`staffs`.`name` = 'July'))", "attached_conditions_computation": [ ], "attached_conditions_summary": [ { "table": "`staffs`", "attached": null } ] } }, { "refine_plan": [ { "table": "`staffs`" } ] } ] } }, { "join_execution": { "select#": 1, "steps": [ ] } } ] }
结论
MySQL表数据量的大小,会影响索引的选择,具体的情况还是通过Explain和Optimizer Trace来查看与分析。