MySQL-进阶篇-SQL优化

温馨提示:PC端浏览体验感最佳~~

  • SQL优化

    • 插入数据

      • insert优化

        • 批量插入

          sql 复制代码
            Insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
        • 手动提交事务

          sql 复制代码
            start transaction;
            insert into tb_test values(1,'Tom'),(2,'Cat'),(3,'Jerry');
            insert into tb_test values(4,'Tom').(5,'Cat'),(6,'Jerry');
            insert into tb_test values(7,'Tom'),(8,'Cat'),(9,'Jerry');
            commit;
        • 主键顺序插入

          • 主键乱序插入:8、1、9、21、88、2、4、15、89、5、7、3
          • 主键顺序插入:1、2、3、4、5、7、8、9、15、21、88、89
      • 大批量插入数据

        • 如果一次性需要插入大批量数据,使用insert语句插入性能较低,此时可以使用MySQL数据库提供的load指令进行插入。操作如下:

          sql 复制代码
            #客户端连接服务端时,加上参数 --local-infile
            mysql --local-infile -u root -p
            #设置全局参数local_infile为1,开启从本地加载文件导入数据的开关
            set global local_infile=1;
            #执行load指令将准备好的数据,加载到表结构中
            load data local infile '/root/sql1.log' into table 表名 fields terminated by ',' lines terminated by '\n';
          • 注意:主键顺序插入性能高于乱序插入。

    • 主键优化

      • 数据组织方式

        • 在InnoDB存储引擎中,表数据都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table IOT)。
      • 页分裂

        • 页可以为空,也可以填充一半,也可以填充100%。每个页包含了2-N行数据(如果一行数据多大,会行溢出),根据主键排列。
          • 主键顺序插入的情况
          • 主键乱序插入的情况
            • 假如当前数据页的情况如下图:
            • 现在我们要插入一条id为50的新数据(当前存在的两个Page都无法再存放下这条新数据),那么就会发生页分裂,页分裂后的结果如下图所示:
            • 页分裂过程说明:由于叶子节点是有序的,所以id为50的这条新数据应该插入在47这条数据之后,但是47这条数据所在的Page已经没有足够的空间存放50这条数据,此时就会开启一个新的数据页,然后找到47这条数据所在Page的50%的位置,将超出50%位置的数据移动至新的数据页,并将50这条数据也插入到这个新的数据页,最后重新设置链表指针。
      • 页合并

        • 当删除一行记录时,实际上记录并没有被物理删除,只是记录被标记(flaged)为删除并且它的空间变得允许被其他记录声明使用。
        • 当页中删除的记录达到MERGE_THRESHOLD(默认为页的50%),InnoDB会开始寻找最靠近的页(前或后)看看是否可以将两个页合并以优化空间使用。
          • 假设当前数据页的情况如下图(其中id为13、14、15、16的这4条数据都被标记为删除,且这4条数据占用数据页的空间大于等于50%):
          • 此时InnoDB发现第二个数据页及其相邻的第三个数据页的剩余空间都大于等于50%(MERGE_THRESHOLD),就会将第三个数据页的数据都迁移至第二个数据页,这个过程就叫做页合并。页合并之后数据页的情况如下图:
          • 若现在要新插入一条id为20的新数据,就会直接将数据存放在第三个数据页,如下图:
          • MERGE_THRESHOLD:合并页的阈值,默认为页的50%,可以自己设置,在创建表或者创建索引时指定。
      • 主键设计原则

        • 满足业务需求的情况下,尽量降低主键的长度。
          • 说明:二级索引的叶子节点存放主键,且二级索引可以有多个,主键长度越长占用的磁盘空间越大,搜索时也会耗费大量的磁盘IO。
        • 插入数据时,尽量选择顺序插入,选择使用AUTO_INCREMENT自增主键。
          • 说明:主键乱序插入容易出现页分裂现象。
        • 尽量不要使用UUID做主键或者是其他自然主键,如身份证号。
          • 说明:UUID、身份证号等自然主键无序且长度较长。
        • 业务操作时,避免对主键的修改。
          • 说明:修改主键需要同时修改索引结构,代价较大。
    • order by优化(看Explain执行计划中的Extra列)

      • Using filesort:通过表的索引或全表扫描,读取满足条件的数据行,然后在排序缓冲区sort buffer中完成排序操作,所有不是通过索引直接返回排序结果的排序都叫FileSort排序。

      • Using index:通过有序索引顺序扫描直接返回有序数据,这种情况即为using index,不需要额外排序,操作效率高。

      • 可以通过类似以下SQL语句进行验证:

        sql 复制代码
          #没有创建索引时,根据age,phone进行排序(Extra列的信息为Using filesort)
          explain select id,age,phone from tb_user order by age, phone;
          #创建索引
          create index idx_user_age_phone_aa on tb_user(age,phone);
          #创建索引后,根据age,phone进行升序排序(Extra列的信息为Using index)
          explain select id,age,phone from tb_user order by age, phone;
          #创建索引后,根据age,phone进行降序排序(倒序扫描索引,Extra列的信息为Backward index scan; Using index)
          explain select id,age,phone from tb_user order by age desc, phone desc;
          
          #根据age,phone进行排序,但一个升序,一个降序(Extra列的信息为Using index; Using filesort)
          explain select id,age,phone from tb_user order by age asc,phone desc;
          #创建索引
          create index idx_user_age_phone_ad on tb_user(age asc,phone desc);
          #根据age,phone进行排序,但一个升序,一个降序(走idx_user_age_phone_ad索引,Extra列的信息为Using index)
          explain select id,age,phone from tb_user order by age asc,phone desc;
        • 注意:以上排序规则都有一个前提条件,使用覆盖索引。若不使用覆盖索引,则需要回表查询数据,然后在数据缓冲区中对数据进行排序。
      • 优化总结

        • 根据排序字段建立合适的索引,多字段排序时,也遵循最左前缀法则。
        • 尽量使用覆盖索引。
        • 多字段排序,一个升序一个降序,此时需要注意联合索引在创建时的规则(ASC / DESC)。
        • 如果不可避免的出现filesort,大数据量排序时,可以适当增大排序缓冲区大小sort_buffer_size(默认256k)。
          • 说明:可以通过 show variables like 'sort_buffer_size'; 查看排序缓冲区的大小。若排序时排序缓冲区占满了,就会在磁盘文件中进行排序,性能低。
    • group by优化

      • 执行类似以下SQL语句并观察Extra列的信息:

        sql 复制代码
          #删除掉目前不用的索引,避免影响
          
          #执行分组操作,根据profession字段分组(Extra列出现了Using temporary,性能较差)
          explain select profession, count(*) from tb_user group by profession;
          
          #创建索引
          Create index idx_user_pro_age_sta on tb_user(profession,age,status);
          #执行分组操作,根据profession字段分组(Extra列没有出现Using temporary,性能较好)
          explain select profession, count(*) from tb_user group by profession;
          #执行分组操作,根据profession字段分组(符合最左前缀法则,Extra列没有出现Using temporary,性能较好)
          explain select profession, count(*) from tb_user group by profession,age;
          
          #执行分组操作,先根据profession字段过滤,再根据age字段分组(符合最左前缀法则,Extra列没有出现Using temporary,性能较好)
          explain select age, count(*) from tb_user where profession='软件工程' group by age;
      • 优化总结

        • 在分组操作时,可以通过索引来提高效率。
        • 分组操作时,索引的使用也是满足最左前缀法则的。
    • limit优化

      • 一个常见又非常头疼的问题就是limit 2000000,10,此时需要MySQL排序前2000010记录,仅仅返回2000000-2000010的记录,其他记录丢弃,查询排序的代价非常大。

      • 优化思路:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引加子查询形式进行优化。例如:

        sql 复制代码
          explain select * from tb_sku t, (select id from tb_sku order by id limit 2000000,10) a where t.id=a.id;
    • count优化

      sql 复制代码
        explain select count(*) from tb_user;
      • count操作的效率主要取决于存储引擎的处理方式,对于上面的这条SQL语句:

        • MylSAM引擎把一个表的总行数存在了磁盘上,因此执行count(*)的时候会直接返回这个数,效率很高;(前提是没有查询条件)
        • InnoDB引擎就麻烦了,它执行count(*)的时候,即使没有查询条件,也需要把数据一行一行地从引擎里面读出来,然后累积计数。
      • 优化思路:自己计数。(了解即可,实现起来比较麻烦)

      • count的几种用法

        • count() 是一个聚合函数,对于返回的结果集,一行行地判断,如果count函数的参数不是NULL,累计值就加1,否则不加,最后返回累计值。
        • 用法:count(*)、count(主键)、count(字段)、count(1)
        • count(主键)
          • InnoDB引擎会遍历整张表,把每一行的主键id值都取出来,返回给服务层。服务层拿到主键后,直接按行进行累加(主键不可能为null)。
        • count(字段)
          • 没有not null约束:InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,服务层判断是否为null,不为null,计数累加。
          • 有not null约束:InnoDB引擎会遍历整张表把每一行的字段值都取出来,返回给服务层,直接按行进行累加。
        • count(1)
          • InnoDB引擎遍历整张表,但不取值。服务层对于返回的每一行,放一个数字"1"进去,直接按行进行累加。
        • count(*)
          • InnoDB引擎并不会把全部字段取出来,而是专门做了优化,不取值,服务层直接按行进行累加。
        • 总结:按照效率排序的话,count(字段) < count(主键id) < count(1) ≈ count(*),所以尽量使用count( * )。(效率主要看是否需要取值)
    • update优化

      • InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则会从行锁升级为表锁(我们在跟新数据时,最好根据索引字段去更新,且索引不能失效,这样InnoDB只会对符合条件的数据加上行锁,而不会对整张表加锁,并发访问性能较好)。例如以下SQL语句:

        sql 复制代码
          #假设id为主键(主键索引),name字段没有索引
          
          #id字段有索引,此时只对id为1的这一行数据加锁。并发情况下,即使当前事务还没有提交,其他事务也可以操作其他行的数据
          update student set no='2000100100' where id=1;
          
          #name字段没有索引,此时会对整张表上锁。并发情况下,在当前事务提交之前,其他事务都不可以对这张表的任何数据进行操作
          update student set no='2000100105' where name='韦一笑';
          
          #对name字段建立索引
          create index idx_student_name on student(name);
          #此时name字段有索引,只会对name为"韦一笑"的行数据加锁。并发情况下,即使当前事务还没有提交,其他事务也可以操作其他行的数据
          update student set no='2000100105' where name='韦一笑';
相关推荐
Irene19911 小时前
MySQL 函数速查表:快速通过在线SQL测试
mysql
Irissgwe1 小时前
redis之典型应用-缓存cache
数据库·redis·缓存·缓存击穿·缓存雪崩·redis淘汰策略
Shely20171 小时前
数据库索引
数据库·mysql
我也不曾来过12 小时前
Mysql
数据库·mysql
小陈工2 小时前
Python异步编程进阶:asyncio高级模式与性能调优
开发语言·前端·数据库·人工智能·python·flask·numpy
gQ85v10Db2 小时前
Redis分布式锁进阶第三十一篇
数据库·redis·分布式
身如柳絮随风扬2 小时前
购物车服务设计:基于 Redis Hash 的高效实现
数据库·redis
观测云2 小时前
观测云4月产品升级报告 | 统一目录、Obsy AI 全新上线,基础设施、场景、监控告警、管理多项能力升级
数据库·人工智能·可观测性·产品迭代·观测云
Mike117.2 小时前
GBase 8c schema 和 search_path 引发的对象定位问题
数据库·sql·oracle