MySQL-索引选择

说明

  1. 实验平台:MySQL 5.6.35
  2. 下文说明了 当where_clause条件查询时,MySQL未必会选择开发人员自认为的索引,MySQL会优化选择成本最小的方式,但这个成本最小并不一定准确,不一定时间短,可以通过执行计划explainoptimizer trace工具来协助优化查询语句。

select count(*)

select count(*) from tablename;
select count(1) from tablename;

对于如上述无查询条件where_clauseselect count(*)或者select count(1)5.6版本后的MySQL是有优化的,并不一定进行全表扫描,MySQL会用成本最小的辅助索引查询方式来计数。

也就是说使用count(*),由于 MySQL 的优化已经保证了它的查询性能是最好的。

随带提一句,count(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直接使用count(*)查询表的行数。

验证

测试表TEST_INFO数据量为4188w,其表结构定义:

CREATE TABLE `TEST_INFO` (
  `ID` varchar(32) NOT NULL COMMENT '主键ID',
  `OPEN_ID` varchar(64) DEFAULT NULL COMMENT '客户ID',
  `APPLY_STEP` int(11) DEFAULT NULL COMMENT '申请步骤',
  `APPLY_STATUS` int(11) DEFAULT NULL COMMENT '申请状态',
  `MEMBER_NO` varchar(64) DEFAULT NULL COMMENT '钱包会员号',
  `JPA_VERSION` int(11) DEFAULT NULL COMMENT '乐观锁版本号',
  `FACE_FLOW_NO` varchar(64) DEFAULT NULL COMMENT '钱包人脸识别编号',
  `PRODUCT_NO` varchar(16) DEFAULT NULL COMMENT '产品编号',
  `CHANNEL` varchar(16) DEFAULT NULL COMMENT '申请渠道',
  `CUST_LEVEL` varchar(2) DEFAULT NULL COMMENT '客户风险等级(白名单获取)',
  `APPLY_NO` varchar(64) DEFAULT NULL COMMENT '申请编号',
  `REFUSE_DESC` varchar(50) DEFAULT NULL COMMENT '申请拒绝原因',
  `IS_SURE_CONTRACT` varchar(2) DEFAULT NULL COMMENT '是否勾选申请协议',
  `IS_OUTTIME` int(11) DEFAULT NULL COMMENT '申请是否过期',
  `APPLY_OUT_TIME` datetime DEFAULT NULL COMMENT '申请过期时间',
  `CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
  `UPDATE_TIME` datetime DEFAULT NULL COMMENT '修改时间',
  `FORWARD_STATUS` int(11) DEFAULT '0' COMMENT '前往申请点击状态',
  `APP_VERSION` varchar(20) DEFAULT NULL COMMENT '申请时客户端版本号',
  `BUSINESS_VERSION` int(11) DEFAULT '1' COMMENT '1或者0表示面客版本流程。2:表示绑卡后置流程。3配置化流程',
  `CREDIT_RESULT_TIME` datetime DEFAULT NULL COMMENT '授信结果时间',
  `RISK_TEST_FLAG` varchar(4) DEFAULT NULL COMMENT '风控测试标,0:非测试用户,1:测试用户-分到自营,2:测试用户-分到引流',
  `CHECK_SMS_STATUS` int(11) DEFAULT '0' COMMENT '授信流程是否做过验短,0否1是',
  `IS_DELETED` int(1) DEFAULT '0' COMMENT '逻辑删除,0:不删除,1:删除',
  `RISK_LEVEL` varchar(16) DEFAULT NULL COMMENT '用户风险层级',
  `APPLY_SUBMIT_TIME` datetime DEFAULT NULL COMMENT '用户授信提交时间',
  `CREDIT_REPORT_SOURCE` varchar(32) DEFAULT NULL COMMENT '人行征信调用源',
  PRIMARY KEY (`ID`),
  KEY `idx_open_id` (`OPEN_ID`),
  KEY `idx_apply_no` (`APPLY_NO`),
  KEY `idx_apply_out_time` (`APPLY_OUT_TIME`),
  KEY `idx_create_time` (`CREATE_TIME`),
  KEY `idx_member_no` (`MEMBER_NO`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户申请记录表';

explain select count(*) from TEST_INFO;

count(*)执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLETEST_INFOindexidx_apply_out_time633280695Using index

explain select count(1) from TEST_INFO;

count(1)执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLETEST_INFOindexidx_apply_out_time633280948Using index

explain select count(id) from TEST_INFO;

count(id)执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLETEST_INFOindexidx_apply_out_time633281534Using index

explain select count(apply_no) from TEST_INFO;

count(apply_no)执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLETEST_INFOindexidx_apply_no25933281195Using index

explain select count(is_deleted) from TEST_INFO;

count(is_deleted)执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLETEST_INFOALL33281230

可以看到,

  • 对于未具体指定某一列(count(*) count(1))或者使用主键列(count(id)),MySQL会优化使用成本最小的辅助索引;
  • 具体指定了某一列count,如果该列有索引,则直接使用该列索引,否则就会进行全表扫描;

如何衡量成本最小

IO成本

数据从 磁盘 -> 内存 的成本,默认情况下,读取数据页的IO成本是1;
MySQL 是以页的形式读取数据的:当用到某个数据时,并不会只读取这个数据,而会把相邻的数据也一起读到内存中,即程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1,** IO 的成本主要和页的数量**有关。

CPU成本

将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,默认情况下,检测记录的成本是 0.2,CPU成本显然与行数有关。

举例说明

实验表定义:

CREATE TABLE `person` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `score` int(11) NOT NULL,
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `name_score` (`name`(191),`score`),
  KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

使用存储过程造10w数据插入person

delimiter $$           #临时将MySQL的语句结束符改为$$,以便后面存储过程定义
CREATE PROCEDURE insert_person()
begin
    declare c_id integer default 1;
    while c_id<=100000 do
    insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second));
    set c_id=c_id+1;
    end while;
end$$                 #$$表示语句结束,存储过程定义完毕
delimiter ;         #将语句的结束符号恢复为分号
call insert_person(); #调用存储过程

explain select count(*) from person;

count(*)执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLEpersonindexcreate_time4100147Using index

选择了create_time辅助索引,显然 MySQL 认为使用此索引进行查询成本最小。


explain select * from person where name >'name1' and create_time>'2021-09-22 14:39:18';

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLEpersonALLname_score,create_time100147Using where

全表扫描,理论上应该用 name_score 或者 create_time 索引才对,从 where 的查询条件来看确实都能命中索引,猜测是否是因为select *需要回表代价太大所致,再测试:

explain select name from person where name >'name1';

执行计划:

idselect_typetabletypepossible_keyskeykey_lenrefrowsextra
1SIMPLEpersonALLname_score100147Using where

依然是全表扫描,理论上采用覆盖索引进行查找性能肯定是比全表扫描更好。但MySQL认为全表扫描比使用覆盖索引的形式成本更小,性能更好,因此来看看具体成本。

全表扫描成本

对于全表扫描来说,IO成本与聚簇索引占用的页面数有关,CPU成本和表中的记录数有关

show table status like 'person';

结果:

NameEngineVersionRow_formatRowsAvg_row_lengthData_lengthMax_data_length
personInnodb10Compact1001475757835520

说明:

  • 记录行数是 100147,这个数值是估算,则CPU成本是 100147 * 0.2 = 20029.4
  • 数据长度是 5783552,InnoDB 默认页大小是 16 KB,可以算出页面数量是 5783552 / (16 * 1024) = 353,即IO成本是 353

也就是说 全表扫描的总成本 = 20029.4 + 353 = 20382.4

optimizer trace 验证

MySQL 5.6 及之后的版本可以用optimizer trace功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,开发人员可以依赖这些信息来进一步优化 SQL。

set optimizer_trace="enabled=on";
select name from person where name >'name1';
select * from information_schema.optimizer_trace;
set optimizer_trace="enabled=off";

看看name_score索引的成本:

"range_scan_alternatives": [
  {
      "index": "name_score",
      "ranges": [
          "name1 <= name"
      ],
      "index_dives_for_eq_ranges": true,
      "rowid_ordered": false,
      "using_mrr": false,
      "index_only": false,
      "rows": 50073,
      "cost": 60089,
      "chosen": false,
      "cause": "cost"
  }
],

可以看到执行成本为 60089,高于之前算出来的全表扫描成本:20382.4,所以没选择此索引。

这里的 60089 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和。


看看全表扫描的成本:

  {
      "considered_execution_plans": [
          {
              "plan_prefix": [],
              "table": "`person`",
              "best_access_path": {
                  "considered_access_paths": [
                      {
                          "access_type": "scan",
                          "rows": 50073,
                          "cost": 20382,
                          "chosen": true
                      }
                  ]
              },
              "cost_for_plan": 20382,
              "rows_for_plan": 50073,
              "chosen": true
          }
      ]
  }

"cost": 20382,与之前算出来的一样,这个值比前面选择索引算出的执行成本中最小,所以最终 MySQL 选择了用全表扫描的方式来执行此 SQL。

实际上 optimizer trace 详细列出了覆盖索引、回表的成本统计情况。


但是,MySQL 在查询前做的成本估算可能不准 ,所选择的执行计划未必是最佳的,原因有挺多,就比如

  1. 上文说的行数统计信息不准;
  2. MySQL 认为的最优跟开发人员认为不一样,开发人员可以认为执行时间短的是最优的,但 MySQL 认为的成本小未必意味着执行时间短。

参考:

我说 SELECT COUNT(*) 会造成全表扫描,有人说我错了!

极客时间:数据库索引:索引并不是万能药

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值