where_clause
条件查询时,MySQL未必会选择开发人员自认为的索引,MySQL会优化选择成本最小的方式,但这个成本最小并不一定准确,不一定时间短,可以通过执行计划explain
和optimizer trace
工具来协助优化查询语句。select count(*) from tablename;
select count(1) from tablename;
对于如上述无查询条件where_clause
的select 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(*)
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | TEST_INFO | index | idx_apply_out_time | 6 | 33280695 | Using index |
explain select count(1) from TEST_INFO;
count(1)
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | TEST_INFO | index | idx_apply_out_time | 6 | 33280948 | Using index |
explain select count(id) from TEST_INFO;
count(id)
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | TEST_INFO | index | idx_apply_out_time | 6 | 33281534 | Using index |
explain select count(apply_no) from TEST_INFO;
count(apply_no)
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | TEST_INFO | index | idx_apply_no | 259 | 33281195 | Using index |
explain select count(is_deleted) from TEST_INFO;
count(is_deleted)
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | TEST_INFO | ALL | 33281230 |
可以看到,
count(*)
count(1)
)或者使用主键列(count(id)
),MySQL会优化使用成本最小的辅助索引;count
,如果该列有索引,则直接使用该列索引,否则就会进行全表扫描;数据从 磁盘 -> 内存 的成本,默认情况下,读取数据页的IO成本是1;
MySQL 是以页的形式读取数据的:当用到某个数据时,并不会只读取这个数据,而会把相邻的数据也一起读到内存中,即程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1,** IO 的成本主要和页的数量**有关。
将数据读入内存后,还要检测数据是否满足条件和排序等 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(*)
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | person | index | create_time | 4 | 100147 | Using index |
选择了create_time
辅助索引,显然 MySQL 认为使用此索引进行查询成本最小。
explain select * from person where name >'name1' and create_time>'2021-09-22 14:39:18';
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | person | ALL | name_score,create_time | 100147 | Using where |
全表扫描,理论上应该用 name_score
或者 create_time
索引才对,从 where
的查询条件来看确实都能命中索引,猜测是否是因为select *
需要回表代价太大所致,再测试:
explain select name from person where name >'name1';
执行计划:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | extra |
---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | person | ALL | name_score | 100147 | Using where |
依然是全表扫描,理论上采用覆盖索引进行查找性能肯定是比全表扫描更好。但MySQL认为全表扫描比使用覆盖索引的形式成本更小,性能更好,因此来看看具体成本。
对于全表扫描来说,IO成本与聚簇索引占用的页面数有关,CPU成本和表中的记录数有关
show table status like 'person';
结果:
Name | Engine | Version | Row_format | Rows | Avg_row_length | Data_length | Max_data_length | … |
---|---|---|---|---|---|---|---|---|
person | Innodb | 10 | Compact | 100147 | 57 | 5783552 | 0 | … |
说明:
也就是说 全表扫描的总成本 = 20029.4 + 353 = 20382.4
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 在查询前做的成本估算可能不准 ,所选择的执行计划未必是最佳的,原因有挺多,就比如
参考:
我说 SELECT COUNT(*) 会造成全表扫描,有人说我错了!
极客时间:数据库索引:索引并不是万能药