为什么SQL语句用了索引,还是会查询的很慢?

2024-08-22 00:20

本文主要是介绍为什么SQL语句用了索引,还是会查询的很慢?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

        • 慢查询和索引
        • 索引的过滤性
        • 索引的扫描行数
        • 什么情况下索引失效?
        • limit深度分页会导致慢查询
        • 单表数据量太大也会导致慢查询。
        • JOIN或者子查询过多也会造成慢查询。
        • IN后面括号内的元素过多也会造成慢查询。
        • 数据库在刷脏页也会造成慢查询。
        • order by 文件排序会导致查询变慢 (Using filesort)
        • 拿不到锁或者死锁。查询是相当的慢。。
        • delete in (子查询) 此时不走索引,也慢。
        • group by 使用临时表也会慢。
        • 参考文章
        • 创建测试数据的存储过程函数

慢查询和索引

慢查询和索引没有必然的联系。

  1. long_query_time这个参数可监听慢查询,只要查询结果大于该参数值,就会记录到慢查询日志。默认是10s,但是生产环境中一般设置1s,对于一些敏感业务,会设置更小的值。
  2. explain可查看SQL语句是否使用索引。
索引的过滤性

比如一句SQL,SELECT * FROM table WHERE id > 0;,id是非负数的主键索引,此SQL也是进行了全表扫描。不过是扫描的索引树。所以使用EXPLAIN解析的时候,也是属于使用了索引的。

因此InnoDB中没有使用索引的情况就是:从主键索引最左边的叶子节点开始,向右扫码整个索引树。

举例: 假设一张表记录了全国人口的信息,此时想获取10-15岁之间小朋友的数据,这时候查询结果可能上亿条。即使在age字段上加上索引,执行起来也会很慢。
SELECT * FROM t_people WHERE age BETWEEN 10 AND 15;
该SQL的执行流程如下:

  1. 在索引age上用树搜索,获取到第一个age=10的记录,得到它的主键ID
  2. 到主键索引行上获取整行信息,作为结果集的一部分返回。
  3. 再获取下一个ID值,再去主键索引上获取整行信息。
  4. 重复以上步骤直到碰到一个age>15的记录。

所以:加了索引不一定就可以查询的很快,我们关心的是扫描行数!

索引的扫描行数

如果在全国人口记录表中获取姓张且年龄为8岁的人的数据,SQL如下:
SELECT * FROM t_people WHERE name LIKE '张%' AND age = 8;
MySQL5.5之前的版本,这个语句的执行流程是这样的。

先在联合索引树上找到第一个姓名字段是张开头的记录,取出主键ID。根据ID获取整行信息,判断年龄是否等于8,不是的话就丢弃,是的话就存入结果集。

我们把根据主键ID查找整行数据的这个动作叫做回表。假设全国人口姓张的有8000万,那就要回表8000万次。不仅要回表,还得判断年龄是否等于8。
如何解决?
MySQL5.6以后引入了索引下推
有了索引下推后的执行流程:

先在联合索引数上找到第一个姓名字段是张开头的记录,然后继续判断这个索引记录中年龄的值是否等于8。如果不等于8就直接丢弃,如果等于8再去根据主键ID获取整行记录,记录结果集。这样就减少了回表的操作。

但是,索引下推的话,还是绕不开最左匹配原则,还是需要在联合索引中遍历8000万次,然后回表100万次!此时怎么解决?
可以使用MySQL5.7中引入的
虚拟列
来实现。

虚拟列是很实用且容易被忽视的功能!

-- 创建虚拟列
alter table t_people add name_first varchar(2) generated always as 
(left(name, 1)), add index (name_first, age);-- 建表
CREATE TABLE `t_people` (`id` int(11) DEFAULT NULL,`name` varchar(20) DEFAULT NULL,`age` int(11) DEFAULT NULL,`info` varchar(255) DEFAULT NULL,`name_first` varchar(2) GENERATED ALWAYS AS (left(`name`, 1)) VIRTUAL,KEY `name_first` (`name_first`, `age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

以上SQL直接创建了一个虚拟列字段name_first,该字段存储姓名中的第一个字符。虚拟列不可被修改,也不能指定值。
我们此时再查询姓张且年龄等于8的数据,SELECT * FROM t_people WHERE name_fist = '张' AND age = 8;
此时只需要扫描联合索引100万次并回表一百万次了。

这个优化的本质是创建了一个更紧凑的索引来加速了查询的过程。

什么情况下索引失效?
  1. 隐式的类型转换,索引失效
    EXPLAIN SELECT * FROM table where id = 1

    如果此时的id是varchar类型的话,索引不生效。因为此时是字符串类型和数字进行比较,MySQL会将它们都转成浮点型再比较。
    需要注意的是:如果此时id是int类型,传的是字符串类型的'1',此时是走索引的,至于为什么我就不知道了~

  2. 条件查询包含OR,可能导致索引失效。
    EXPLAIN SELECT * FROM user where age = '12' or userId = '12'

    如果userId加了索引,age没有加索引,此时索引不生效。将userId放在or前面,也是不生效。
    因为此时即便是userId走了索引,使用or查询的时候,到age字段的时候,还是会进行全表扫描。就等于是进行了全表扫描+索引扫描+合并三个操作。但是换成and的话,索引就会生效了。

  3. like通配符会导致索引失效
    EXPLAIN SELECT * FROM user where userId like '%12'

    以上SQL就不会走索引。但是如果将%放在后面就会走索引了。

  4. 查询条件不满足最左匹配原则不会走索引。
    假设你建表如下:

    CREATE TABLE test (id INT ( 11 ) NOT NULL AUTO_INCREMENT,user_id VARCHAR ( 32 ) NOT NULL,age VARCHAR ( 16 ) NOT NULL,`name` VARCHAR ( 255 ) NOT NULL,`remark` varchar(255) DEFAULT NULL,PRIMARY KEY ( id ),KEY idx_userid_age_name ( user_id, age, `name` ) USING BTREE 
    ) ENGINE = INNODB DEFAULT CHARSET = utf8;
    

    那么执行SQLexplain select * from test where age ='捡田螺的小男孩';的时候,索引是不生效的。

    你建立的一个(a,b,c)的联合索引,相当于建立了(a),(a,b),(a,c),(a,b,c)四个索引。必须第一个查询字段是a字段才可以生效。
    需要注意的是,如果上述建表语句中,没有remark字段的话,怎么查询都会生效。因为如果没有remark字段的话,所有字段都在索引中!!

  5. 在索引列上使用内置函数

    在字段create_time上加了索引,但是如果使用DATE_FORMATDATE_ADD等内置函数的话,索引是不会生效的。

  6. 对索引列进行+、-、*、/、运算的话,索引不会生效。

  7. 对索引列上进行!=not in 等操作时,索引不生效。

    in的话会走索引。其实和MySQL的优化器有关,优化器觉得这么操作即使走了索引也需要扫描很多行,不如直接不走。

  8. 索引列上使用is nullis not null有可能不走索引。

    在单个索引列上使用is null或者is not null的时候,是会走索引的。但是如果用or连接起来的话,就不会走索引了。
    因为在单个列上使用is null或者is not null的时候,索引类型是range。如果再加上or连接的话,那么优化器就会因为查询数据量的问题,放弃走索引。

  9. 字段编码的问题

    如果表a中name字段编码是utf8_mb4,而表b中name字段的编码是utf8,那么这俩表关联的时候。a left join b on a.name = b.name,此时不会走索引。如果把两个name的字段编码改成一致,就会走索引了。

  10. 优化器选错了索引

    不断地删除历史数据和新增数据的场景,有可能会导致MySQL选错索引。

    那么有哪些解决方案呢?

    • 使用force index 强行选择某个索引
    • 修改你的SQl,引导它使用我们期望的索引
    • 优化你的业务逻辑
    • 优化你的索引,新建一个更合适的索引,或者删除误用的索引。
limit深度分页会导致慢查询

如果数据量很大,你要获取第一百万页到第一百万零一页的数据,可想而知肯定会查询的慢。
MySQL会先扫描前面一百万页的数据,然后丢弃掉,再返回第一百万零一页的数据。

如何优化深度分页的问题?

  • 标签记录法:就是标记一下上次查询到哪一条了,下次再来查的时候,从该条开始往下扫描。
    获取100万零一页数据,可以使用以下SQL。
    SELECT * FROM table WHERE id > 1000000 limit 10;
  • 延迟关联法:就是把条件转移到主键索引树,然后减少回表。如下:
    select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > '2020-09-19' limit 100000, 10) AS acct2 on acct1.id= acct2.id;
单表数据量太大也会导致慢查询。

一个B+树大概可以存放多少数据量呢?
InnoDB存储引擎最小储存单元是页,一页大小就是16k。

  • 如果一行记录的数据大小为1k,那么单个叶子节点可以存的记录数 =16k/1k =16.
  • 非叶子节点内存放多少指针呢?我们假设主键ID为bigint类型,长度为8字节(面试官问你int类型,一个int就是32位,4字节),而指针大小在InnoDB源码中设置为6字节,所以就是8+6=14字节,16k/14B =16*1024B/14B = 1170

因此,一棵高度为2的B+树,能存放1170 * 16=18720条这样的数据记录。
同理一棵高度为3的B+树,能存放1170 *1170 *16 =21902400,也就是说,可以存放两千万左右的记录。
B+树高度一般为1-3层,已经满足千万级别的数据存储。

如果B+树想存储更多的数据,那树结构层级就会更高,查询一条数据时,需要经历的磁盘IO变多,因此查询性能变慢。
如何解决?—>分库分表
但是分库分表会带来很多问题,比如事务问题、夸库问题、排序问题、分页问题、分布式ID等等。

JOIN或者子查询过多也会造成慢查询。
IN后面括号内的元素过多也会造成慢查询。
数据库在刷脏页也会造成慢查询。
  1. 什么是脏页?

    当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。一般有更新SQL才可能会导致脏页。

  2. 一条更新SQL是如何执行的?
    update t set c=c+1 where id=666;

    • 对于这条更新SQL,执行器会先找引擎取id=666这一行。如果这行所在的数据页本来就在内存中的话,就直接返回给执行器。如果不在内存,就去磁盘读入内存,再返回。
    • 执行器拿到引擎给的行数据后,给这一行C的值加一,得到新的一行数据,再调用引擎接口写入这行新数据。
    • 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,但是此时redo log 是处于prepare状态的哈。
    • 执行器生成这个操作的binlog,并把binlog写入磁盘。
    • 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。

    InnoDB 在处理更新语句的时候,只做了写日志这一个磁盘操作。这个日志叫作redo log(重做日志)。平时更新SQL执行得很快,其实是因为它只是在写内存和redo log日志,等到空闲的时候,才把redo log日志里的数据同步到磁盘中。
    redo log日志不是在磁盘嘛?那为什么不慢?其实是因为写redo log的过程是顺序写磁盘的。磁盘顺序写会减少寻道等待时间,速度比随机写要快很多的。

  3. 为什么会出现脏页?

    更新SQL只是在写内存和redo log日志,等到空闲的时候,才把redo log日志里的数据同步到磁盘中。这时内存数据页跟磁盘数据页内容不一致,就出现脏页。

  4. 什么时候会刷脏?

    • redo log写满了,要刷脏页。这种情况要尽量避免的。因为出现这种情况时,整个系统就不能再接受更新啦,即所有的更新都必须堵住。
    • 内存不够了,需要新的内存页,就要淘汰一些数据页,这时候会刷脏页。
      (解释:InnoDB 用缓冲池(buffer pool)管理内存,而当要读入的数据页没有在内存的时候,就必须到缓冲池中申请一个数据页。这时候只能把最久不使用的数据页从内存中淘汰掉:如果要淘汰的是一个干净页,就直接释放出来复用;但如果是脏页呢,就必须将脏页先刷到磁盘,变成干净页后才能复用。)
    • MySQL 认为系统空闲的时候,也会刷一些脏页
    • MySQL 正常关闭时,会把内存的脏页都 flush 到磁盘上
  5. 为什么刷脏会导致SQL变慢呢?

    • redo log写满了,要刷脏页,这时候会导致系统所有的更新堵住,写性能都跌为0了,肯定慢呀。一般要杜绝出现这个情况。
    • 一个查询要淘汰的脏页个数太多,一样会导致查询的响应时间明显变长。
order by 文件排序会导致查询变慢 (Using filesort)

explain select name,age,city from staff where city = '深圳' order by age limit 10;
如果Extra中含有Using filesort,就表示它用到了文件排序。

order by排序,分为全字段排序和rowid排序。它是拿max_length_for_sort_data和结果行数据长度对比,如果结果行数据长度超过max_length_for_sort_data这个值,就会走rowid排序,相反,则走全字段排序。

  • rowid排序,一般需要回表去找满足条件的数据,所以效率会慢一点。
    如何优化? 建立索引!
拿不到锁或者死锁。查询是相当的慢。。
delete in (子查询) 此时不走索引,也慢。
group by 使用临时表也会慢。
参考文章
  1. 为什么 SQL 语句使用了索引,但却还是慢查询?
  2. 如何处理 MySQL 的慢查询问题?
创建测试数据的存储过程函数
-- 创建存储过程
CREATE DEFINER=`root`@`localhost` PROCEDURE `GenerateRandomChineseCharacters`(IN length INT)
BEGINDECLARE i INT DEFAULT 0;DECLARE char_set VARCHAR(255) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';WHILE i < length DOINSERT INTO zls_test (name, age) VALUES (CONCAT('张', SUBSTRING(char_set, FLOOR(RAND() * 52) + 1, 1), FLOOR(RAND() * 1000000)),FLOOR(RAND() * 80) + 1);SET i = i + 1;END WHILE;END
-- 执行200万次
CALL GenerateRandomChineseCharacters(2000000)   -- 会很慢  200万的话也测试不出来什么效果,建议没必要执行。

这篇关于为什么SQL语句用了索引,还是会查询的很慢?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1094702

相关文章

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Mysql表的简单操作(基本技能)

《Mysql表的简单操作(基本技能)》在数据库中,表的操作主要包括表的创建、查看、修改、删除等,了解如何操作这些表是数据库管理和开发的基本技能,本文给大家介绍Mysql表的简单操作,感兴趣的朋友一起看... 目录3.1 创建表 3.2 查看表结构3.3 修改表3.4 实践案例:修改表在数据库中,表的操作主要

mysql出现ERROR 2003 (HY000): Can‘t connect to MySQL server on ‘localhost‘ (10061)的解决方法

《mysql出现ERROR2003(HY000):Can‘tconnecttoMySQLserveron‘localhost‘(10061)的解决方法》本文主要介绍了mysql出现... 目录前言:第一步:第二步:第三步:总结:前言:当你想通过命令窗口想打开mysql时候发现提http://www.cpp

MySQL大表数据的分区与分库分表的实现

《MySQL大表数据的分区与分库分表的实现》数据库的分区和分库分表是两种常用的技术方案,本文主要介绍了MySQL大表数据的分区与分库分表的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. mysql大表数据的分区1.1 什么是分区?1.2 分区的类型1.3 分区的优点1.4 分

MySQL错误代码2058和2059的解决办法

《MySQL错误代码2058和2059的解决办法》:本文主要介绍MySQL错误代码2058和2059的解决办法,2058和2059的错误码核心都是你用的客户端工具和mysql版本的密码插件不匹配,... 目录1. 前置理解2.报错现象3.解决办法(敲重点!!!)1. php前置理解2058和2059的错误

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

MySQL INSERT语句实现当记录不存在时插入的几种方法

《MySQLINSERT语句实现当记录不存在时插入的几种方法》MySQL的INSERT语句是用于向数据库表中插入新记录的关键命令,下面:本文主要介绍MySQLINSERT语句实现当记录不存在时... 目录使用 INSERT IGNORE使用 ON DUPLICATE KEY UPDATE使用 REPLACE

MySQL Workbench 安装教程(保姆级)

《MySQLWorkbench安装教程(保姆级)》MySQLWorkbench是一款强大的数据库设计和管理工具,本文主要介绍了MySQLWorkbench安装教程,文中通过图文介绍的非常详细,对大... 目录前言:详细步骤:一、检查安装的数据库版本二、在官网下载对应的mysql Workbench版本,要是

mysql数据库重置表主键id的实现

《mysql数据库重置表主键id的实现》在我们的开发过程中,难免在做测试的时候会生成一些杂乱无章的SQL主键数据,本文主要介绍了mysql数据库重置表主键id的实现,具有一定的参考价值,感兴趣的可以了... 目录关键语法演示案例在我们的开发过程中,难免在做测试的时候会生成一些杂乱无章的SQL主键数据,当我们