我的编程空间,编程开发者的网络收藏夹
学习永远不晚

SQL优化之SELECT COUNT(*)

短信预约 信息系统项目管理师 报名、考试、查分时间动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

SQL优化之SELECT COUNT(*)

SQL优化之SELECT COUNT(*)

前言

SQL优化之SQL 进阶技巧(上) SQL优化之SQL 进阶技巧(下)中提到使用以下 sql 会导致慢查询



SELECT COUNT(*) FROM SomeTable
SELECT COUNT(1) FROM SomeTable

原因是会造成全表扫描,有位读者说这种说法是有问题的,实际上针对无 where_clause 的 COUNT(*),MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能最高,这位读者的说法对不对呢

针对这个疑问,我首先去生产上找了一个千万级别的表使用  EXPLAIN 来查询了一下执行计划


EXPLAIN SELECT COUNT(*) FROM SomeTable

结果如下

我说 SELECT COUNT(*) 会造成全表扫描,面试官让我回去等通知

如图所示: 发现确实此条语句在此例中用到的并不是主键索引,而是辅助索引,实际上在此例中我试验了,不管是 COUNT(1),还是 COUNT(*),MySQL 都会用成本最小的辅助索引查询方式来计数,也就是使用 COUNT(*) 由于 MySQL 的优化已经保证了它的查询性能是最好的!随带提一句,COUNT(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直接使用COUNT(*)查询表的行数!

所以这位读者的说法确实是对的。但有个前提,在 MySQL 5.6 之后的版本中才有这种优化。

那么这个成本最小该怎么定义呢,有时候在 WHERE 中指定了多个条件,为啥最终 MySQL 执行的时候却选择了另一个索引,甚至不选索引?

本文将会给你答案,本文将会从以下两方面来分析

  • SQL 选用索引的执行成本如何计算
  • 实例说明

SQL 选用索引的执行成本如何计算

就如前文所述,在有多个索引的情况下, 在查询数据前,MySQL 会选择成本最小原则来选择使用对应的索引,这里的成本主要包含两个方面。

  • IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关
  • CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。

实例说明

为了根据以上两个成本来算出使用索引的最终成本,我们先准备一个表(以下操作基于 MySQL 5.7.18)


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;

这个表除了主键索引之外,还有另外两个索引, name_score 及 create_time。然后我们在此表中插入 10 w 行数据,只要写一个存储过程调用即可,如下:


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

插入之后我们现在使用 EXPLAIN 来计算下统计总行数到底使用的是哪个索引


EXPLAIN SELECT COUNT(*) FROM person
我说 SELECT COUNT(*) 会造成全表扫描,面试官让我回去等通知

从结果上看它选择了 create_time 辅助索引,显然 MySQL 认为使用此索引进行查询成本最小,这也是符合我们的预期,使用辅助索引来查询确实是性能最高的!

我们再来看以下 SQL 会使用哪个索引


SELECT * FROM person WHERE NAME >"name84059" AND create_time>"2020-05-23 14:39:18"

我说 SELECT COUNT(*) 会造成全表扫描,面试官让我回去等通知

用了全表扫描!理论上应该用 name_score 或者 create_time 索引才对,从 WHERE 的查询条件来看确实都能命中索引,那是否是使用 SELECT * 造成的回表代价太大所致呢,我们改成覆盖索引的形式试一下


SELECT create_time FROM person WHERE NAME >"name84059" AND create_time > "2020-05-23 14:39:18"

结果 MySQL 依然选择了全表扫描!这就比较有意思了,理论上采用了覆盖索引的方式进行查找性能肯定是比全表扫描更好的,为啥 MySQL 选择了全表扫描呢,既然它认为全表扫描比使用覆盖索引的形式性能更好,那我们分别用这两者执行来比较下查询时间吧


-- 全表扫描执行时间: 4.0 ms
SELECT create_time FROM person WHERE NAME >"name84059" AND create_time>"2020-05-23 14:39:18"

-- 使用覆盖索引执行时间: 2.0 ms
SELECT create_time FROM person force index(create_time) WHERE NAME >"name84059" AND create_time>"2020-05-23 14:39:18" 

从实际执行的效果看使用覆盖索引查询比使用全表扫描执行的时间快了一倍!说明 MySQL 在查询前做的成本估算不准!我们先来看看 MySQL 做全表扫描的成本有多少。

前面我们说了成本主要 IO 成本和 CPU 成本有关,对于全表扫描来说也就是分别和聚簇索引占用的页面数和表中的记录数。执行以下命令


SHOW TABLE STATUS LIKE "person"
我说 SELECT COUNT(*) 会造成全表扫描,面试官让我回去等通知

可以发现

  1. 行数是 100264,我们不是插入了 10 w 行的数据了吗,怎么算出的数据反而多了,其实这里的计算是估算,也有可能这里的行数统计出来比 10 w 少了,估算方式有兴趣大家去网上查找,这里不是本文重点,就不展开了。得知行数,那我们知道 CPU 成本是 100264 * 0.2 = 20052.8。

  2. 数据长度是 5783552,InnoDB 每个页面的大小是 16 KB,可以算出页面数量是 353。

也就是说全表扫描的成本是 20052.8 + 353 =  20406。

这个结果对不对呢,我们可以用一个工具验证一下。在 MySQL 5.6 及之后的版本中,我们可以用 optimizer trace 功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,我们可以依赖这些信息来进一步优化我们的 SQL。

optimizer_trace 功能使用如下



SET optimizer_trace="enabled=on";
SELECT create_time FROM person WHERE NAME >"name84059" AND create_time > "2020-05-23 14:39:18";
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";

执行之后我们主要观察使用 name_score,create_time 索引及全表扫描的成本。

先来看下使用 name_score 索引执行的的预估执行成本:


{
"index": "name_score",
"ranges": [
"name84059 <= name"
    ],
"index_dives_for_eq_ranges": true,
"rows": 25372,
"cost": 30447
}

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

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

再来看下使用 create_time 索引执行的的预估执行成本:


{
    "index": "create_time",
    "ranges": [
      "0x5ec8c516 < create_time"
    ],
    "index_dives_for_eq_ranges": true,
    "rows": 50132,
    "cost": 60159,
    "cause": "cost"
}

可以看到成本是 60159,远大于全表扫描成本 20406,自然也没选择此索引。

再来看计算出的全表扫描成本:



{
    "considered_execution_plans": [
      {
        "plan_prefix": [
        ],
        "table": "`person`",
        "best_access_path": {
          "considered_access_paths": [
            {
              "rows_to_scan": 100264,
              "access_type": "scan",
              "resulting_rows": 100264,
              "cost": 20406,
              "chosen": true
            }
          ]
        },
        "condition_filtering_pct": 100,
        "rows_for_plan": 100264,
        "cost_for_plan": 20406,
        "chosen": true
      }
    ]
}

 

注意看 cost:20406,与我们之前算出来的完全一样!这个值在以上三者算出的执行成本中最小,所以最终 MySQL 选择了用全表扫描的方式来执行此 SQL。

实际上 optimizer trace 详细列出了覆盖索引,回表的成本统计情况,有兴趣的可以去研究一下。

从以上分析可以看出, MySQL 选择的执行计划未必是最佳的,原因有挺多,就比如上文说的行数统计信息不准,再比如 MySQL 认为的最优跟我们认为不一样,我们可以认为执行时间短的是最优的,但 MySQL 认为的成本小未必意味着执行时间短。

总结

本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用  EXPLAIN, optimizer trace 来优化我们的查询语句。

相关文章

SQL优化之SQL 进阶技巧(上)

SQL优化之SQL 进阶技巧(下)

 

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

SQL优化之SELECT COUNT(*)

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

SQL优化之SELECT COUNT(*)

前言SQL优化之SQL 进阶技巧(上) SQL优化之SQL 进阶技巧(下)中提到使用以下 sql 会导致慢查询SELECT COUNT(*) FROM SomeTableSELECT COUNT(1) FROM SomeTable原因是会造成全表扫描,有位读者
SQL优化之SELECT COUNT(*)
2020-08-22

MySQL select count(*)计数很慢优化方案

目录前言1. MyISAM存储引擎计数为什么这么快?2. 能不能手动实现统计总行数3. InnoDB引擎能否实现快速计数4. 四种计数方式的性能差别前言在日常开发工作中,我经常会遇到需要统计总数的场景,比如:统计订单总数、统计用户总数等。
2022-08-11

故障分析 | MySQL 优化案例 - select count(*)

作者:xuty本文来源:原创投稿*爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。本文关键字:count、SQL、二级索引一、故事背景项目组联系我说是有一张 500w 左右的表做select count(*)速度特别慢。二、原 SQ
故障分析 | MySQL 优化案例 - select count(*)
2015-08-04

MySQL之优化SELECT语句

MySQL之优化SELECT语句 文章目录 MySQL之优化SELECT语句摘要:引言:1. MySQL性能提成优化概述2. WHERE子句优化3. 范围优化4. 哈希联接优化5. 储存引擎下的优化6. 索引条件下推优化7.嵌套循环
2023-08-16

SQL COUNT与性能优化的关系

SQL COUNT 是用来统计查询结果集中行的数量的函数。在很多情况下,使用 COUNT 是必要的,但它也可能影响查询的性能。以下是一些关于 SQL COUNT 与性能优化的关系的建议:避免在 WHERE 子句中使用 COUNT:在 WHE
SQL COUNT与性能优化的关系
2024-08-11

掌握SQL COUNT优化查询速度

在使用COUNT函数时,可以通过以下几种方法优化查询速度:使用索引:在查询涉及到COUNT函数时,可以使用索引来加快查询速度。在需要统计的字段上创建索引,可以减少数据库的扫描范围,提高查询效率。避免使用通配符:在COUNT函数中,尽量避免使
掌握SQL COUNT优化查询速度
2024-08-10

浅谈MySQL之select优化方案

目录生活中的例子慢查询如何去优化countlimit最大值最小值min&max生活中的例子我们是否看到过在公司中许多查询语句都是select * xxxx 心中的想法肯定是,别人写了select *,那我写吧,省去了不少麻烦事儿 慢查询首先
2022-05-31

优化SQL查询:减少COUNT使用误区

在优化SQL查询时,可以尽量避免使用COUNT函数,因为COUNT函数会对整个结果集进行计数,从而增加查询的开销。以下是一些减少COUNT使用误区的方法:使用WHERE子句过滤数据:在查询中使用WHERE子句来过滤需要统计的数据,可以减少C
优化SQL查询:减少COUNT使用误区
2024-08-10

了解 MySQL 查询优化器:COUNT(id) 与 COUNT(*)

在 MySQL 中,我们几乎每天都会使用“COUNT”函数来帮助我们计算给定查询的行数。每个开发者关于性能的最大困境是使用“COUNT(*)”还是“COUNT(id)”更好。MySQL优化器MySQL 优化器是 MySQL 的关键组件,负责
了解 MySQL 查询优化器:COUNT(id) 与 COUNT(*)
2024-07-10

SQL优化之SQL 进阶技巧(下)

上文( SQL优化之SQL 进阶技巧(上) )我们简述了 SQL 的一些进阶技巧,一些朋友觉得不过瘾,我们继续来下篇,再送你 10 个技巧一、 使用延迟查询优化 limit [offset], [rows]经常出现类似以下的 SQL 语句:SELECT * F
SQL优化之SQL 进阶技巧(下)
2021-02-19

MySQL之select in 子查询优化的实现

下面的演示基于MySQL5.7.27版本 一、关于MySQL子查询的优化策略介绍:子查询优化策略 对于不同类型的子查询,优化器会选择不同的策略。1. 对于 IN、=ANY 子查询,优化器有如下策略选择:semijoinMaterializa
2022-05-13

SQL优化之SQL 进阶技巧(上)

由于工作需要,最近做了很多 BI 取数的工作,需要用到一些比较高级的 SQL 技巧,总结了一下工作中用到的一些比较骚的进阶技巧,特此记录一下,以方便自己查阅,主要目录如下: SQL 的书写规范 SQL 的一些进阶使用技巧 SQL 的优化方法SQL 的书写规范在
SQL优化之SQL 进阶技巧(上)
2021-03-23

编程热搜

目录