【SQL】性能优化总结

目录

SQL 性能优化建议

1. 参数是子查询时,使用 EXISTS 代替 IN

查出同时存在于两个表的员工

A表
+------+------+
| id   | name | 
+------+------+
|    1 | 小明 |
|    2 | 小东 |
|    3 | 小强 |
+------+------+

B表
+------+------+
| id   | name | 
+------+------+
|    1 | 小明 |
|    2 | 小东 |
|    4 | 小红 |
+------+------+

#sql

-- 慢
SELECT * 
  FROM Class_A
WHERE id IN (SELECT id 
               FROM  CLASS_B);

-- 快
SELECT *
  FROM Class_A A 
 WHERE EXISTS
(SELECT * 
   FROM Class_B  B
  WHERE A.id = B.id);

为啥使用 EXISTS 的 SQL 运行更快呢,有两个原因

  1. 可以`用到索引,如果连接列 (id) 上建立了索引,那么查询 Class_B 时不用查实际的表,只需查索引就可以了。
  2. 如果使用 EXISTS,那么只要查到一行数据满足条件就会终止查询, 不用像使用 IN 时一样扫描全表。在这一点上 NOT EXISTS 也一样

另外如果 IN 后面如果跟着的是子查询,由于 SQL 会先执行 IN 后面的子查询,会将子查询的结果保存在一张临时的工作表里(内联视图),然后扫描整个视图,显然扫描整个视图这个工作很多时候是非常耗时的,而用 EXISTS 不会生成临时表。

当然了,如果 IN 的参数是子查询时,也可以用连接来代替,如下:

-- 使用连接代替 IN SELECT A.id, A.name
FROM Class_A A INNER JOIN Class_B B ON A.id = B.id;

用到了 「id」列上的索引,而且由于没有子查询,也不会生成临时表

2. 尽量避免排序

SQL 是声明式语言,即对用户来说,只关心它能做什么,不用关心它怎么做。这样可能会产生潜在的性能问题:排序,会产生排序的代表性运算有下面这些

  • GROUP BY 子句
  • ORDER BY 子句
  • 聚合函数(SUM、COUNT、AVG、MAX、MIN)
  • DISTINCT
  • 集合运算符(UNION、INTERSECT、EXCEPT)
  • 窗口函数(RANK、ROW_NUMBER 等)

如果在内存中排序还好,但如果内存不够导致需要在硬盘上排序上的话,性能就会急剧下降,所以我们需要减少不必要的排序。怎样做可以减少排序呢。

2.1【sql 优化】union 、union all、or使用

  1. union:对两个结果集进行并集操作,去重,按照默认规则排序
  2. union all:对两个结果集并集操作,不去重,不排序
  3. intersect:对两个结果集进行交集操作,去重,按照默认规则排序
  4. minus:对两个结果集进行差操作,去重,按照默认规则排序
  5. or:满足两个条件的并集,不去重,不排序

经以上的总结得出以下结论:

1、如果or字段是索引字段,那么使用union all代替or操作,可以走索引

2、如果能用union all ,尽量不要用union,相当于想distinct又执行了order by

3、这里所说的默认排序规则是按照select后边的字段顺序排序的,先按照第一个字段排序,如果第一个字段相同,就按照第二个字段排序

2.2 应尽量避免在where子句中使用or来连接条件

  • 使用or可能会使索引失效,从而全表扫描。

    对于or+没有索引的age这种情况,假设它走了userId的索引,但是走到age查询条件时,它还得全表扫描,也就是需要三步过程:全表扫描+索引扫描+合并 如果它一开始就走全表扫描,直接一遍扫描就完事。mysql是有优化器的,处于效率与成本考虑,遇到or条件,索引可能失效,看起来也合情合理。

  • 如果or字段是索引字段,那么使用union all代替or操作,可以走索引

    -- 反例
    select * from user where userid=1 or age =18
    
    -- 正例
    -- 使用union all
    select * from user where userid=1
    union all
    select * from user where age = 18
    
    //或者分开两条sql写:
    select * from user where userid=1
    select * from user where age = 18
    
  • 使用集合运算符的 ALL 可选项

    SQL 中有 UNION,INTERSECT,EXCEPT 三个集合运算符,默认情况下,这些运算符会为了避免重复数据而进行排序,对比一下使用 UNION 运算符加和不加 ALL 的情况:

    -- 显示排序后的结果
    select * from Class_A
    UNION
    select * from Class_B;
    
    +-----------+----------+
    | id      | name       |
    +-----------+----------+
    | 1       | 小明       |
    | 2       | 小红       |
    | 3       | 小强       |
    | 4       | 小东       |
    +-----------+----------+
    
    -- 不排除重复数据,所以无需排序
    select * from Class_A
    UNION ALL 
    select * from Class_B;
    
    +-----------+----------+
    | id      | name       |
    +-----------+----------+
    | 1       | 小明       |
    | 2       | 小红       |
    | 3       | 小强       |
    | 4       | 小东       |
    | 2       | 小红       |
    | 4       | 小东       |
    +-----------+----------+
    

    注意:加 ALL 是优化性能非常有效的手段,不过前提是不在乎结果是否有重复数据。

2.3 慎用distinct关键字

  • 使用 EXISTS 代表 DISTINCT

    为了排除重复数据, DISTINCT 也会对结果进行排序,如果需要对两张表的连接结果进行去重,可以考虑用 EXISTS 代替 DISTINCT,这样可以避免排序。既用到了索引,又避免了排序对性能的损耗。

    items
    +-----------+----------+
    | item_no | item       |
    +-----------+----------+
    | 1       | 面包       |
    | 2       | 香蕉       |
    | 3       | 苹果       |
    +-----------+----------+
    
    salesHistory
    +---------+----------+----------+
    | item_no | quantity | sale_time|
    +---------+----------+----------+
    | 1       | 4        | 2023-1-2 |
    | 2       | 10       | 2023-1-3 |
    | 3       | 30       | 2023-1-4 |
    | 1       | 25       | 2023-1-5 |
    +---------+----------+----------+
    
    # 如何找出有销售记录的商品,使用如下 DISTINCT 可以:
    SELECT DISTINCT I.item_no
    FROM Items I INNER JOIN SalesHistory SH
    ON I. item_no = SH. item_no;
    
    # 不过更好的方式是使用 EXISTS:
    SELECT item_no FROM Items I
    WHERE EXISTS 
            (SELECT *
               FROM SalesHistory SH
              WHERE I.item_no = SH.item_no);
    
  • distinct 关键字一般用来过滤重复记录,以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时,给查询带来优化效果。但是在字段很多的时候使用,却会大大降低查询效率。

    -- 反例
    SELECT DISTINCT * from  user;
    
    -- 正例
    select DISTINCT name from user;
    

    理由:带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较,过滤掉重复数据,然而这个比较,过滤的过程会占用系统资源,cpu时间。

2.4 在极值函数中使用索引(MAX/MIN)

使用 MAX/ MIN 都会对进行排序,如果参数字段上没加索引会导致全表扫描,如果建有索引,则只需要扫描索引即可。注意:极值函数参数推荐为索引列中并不是不需要排序,而是优化了排序前的查找速度(毕竟索引本身就是有序排列的)。

2.5 能写在 WHERE 子句里的条件不要写在 HAVING 子句里

-- 聚合后使用 HAVING 子句过滤
SELECT sale_date, SUM(quantity)
  FROM SalesHistory GROUP BY sale_date
HAVING sale_date = '2023-1-1';

-- 聚合前使用 WHERE 子句过滤
SELECT sale_date, SUM(quantity)
  FROM SalesHistory
 WHERE sale_date = '2023-1-1' 
 GROUP BY sale_date;

使用第二条语句效率更高,原因主要有两点

  1. 使用 GROUP BY 子句进行聚合时会进行排序,如果事先通过 WHERE 子句能筛选出一部分行,能减轻排序的负担
  2. 在 WHERE 子句中可以使用索引,而 HAVING 子句是针对聚合后生成的视图进行筛选的,但很多时候聚合后生成的视图并没有保留原表的索引结构

2.6 在 GROUP BY 子句和 ORDER BY 子句中使用索引

GROUP BY 子句和 ORDER BY 子句一般都会进行排序,以对行进行排列和替换,不过如果指定带有索引的列作为这两者的参数列,由于用到了索引,可以实现高速查询,由于索引是有序的,排序本身都会被省略掉

-- 对查询进行优化,应考虑在 where 及 order by 涉及的列上建立索引,尽量避免全表扫描
select * from user where address ='深圳' order by age ;

-- 添加索引
alter table user add index idx_address_age (address,age)

2.7 使用索引时,索引的列不能够是一个表达式

  • 所以我们在使用索引时,条件表达式的左侧应该是原始字段
  • 尽量避免在索引列上使用mysql的内置函数
-- 假设我们在 col 列上建立了索引,则下面这些 SQL 语句无法用到索引
SELECT *
  FROM SomeTable
 WHERE col * 1.1 > 100;

-- 尽量避免在索引列上使用mysql的内置函数
SELECT *
  FROM SomeTable
 WHERE SUBSTR(col, 1, 1) = 'a';

以上第一个 SQL 在索引列上进行了运算, 第二个 SQL 对索引列使用了函数,均无法用到索引,正确方式是把列单独放在左侧,如下:

SELECT *
  FROM SomeTable
 WHERE col_1 > 100 / 1.1;

3. 尽量避免使用否定形式

如下的几种否定形式不能用到索引:

  • <>
  • !=
  • NOT IN

所以以下 了SQL 语句会导致全表扫描

SELECT *
  FROM SomeTable
 WHERE col_1 <> 100;
 
 -- 可以改成以下形式
 
 SELECT *
  FROM SomeTable
 WHERE col_1 > 100 or col_1 < 100;

4. 查询条件中避免发生隐式类型转换

假设 col 是 char 类型,则推荐使用以下第二,三条 SQL 的写法,不推荐第一条 SQL 的写法

× SELECT * FROM SomeTable WHERE col_1 = 10;
○ SELECT * FROM SomeTable WHERE col_1 = '10';
○ SELECT * FROM SomeTable WHERE col_1 = CAST(10, AS CHAR(2));
# 注意:CAST函数用于将某种数据类型的表达式显式转换为另一种数据类型

虽然第一条 SQL 会默认把 10 转成 '10',但这种默认类型转换不仅会增加额外的性能开销,还会导致索引不可用,所以建议使用的时候进行类型转换。

5. 减少使用中间表

在 SQL 中,子查询的结果会产生一张新表,不过如果不加限制大量使用中间表的话,会带来两个问题,一是展示数据需要消耗内存资源,二是原始表中的索引不容易用到,所以尽量减少中间表也可以提升性能。

  • 例如:灵活使用 HAVING 子句
SELECT *
  FROM (SELECT sale_date, MAX(quantity) AS max_qty
          FROM SalesHistory 
         GROUP BY sale_date) TMP
         WHERE max_qty >= 10;
         
 -- 虽然上面这样的写法能达到目的,但会生成 TMP 这张临时表,所以应该使用下面这样的写法:
 SELECT sale_date, MAX(quantity) 
  FROM SalesHistory
 GROUP BY sale_date
HAVING MAX(quantity) >= 10;

HAVING 子句和聚合操作是同时执行的,所以比起生成中间表后再执行 HAVING 子句,效率会更高,代码也更简洁

  • 例如:需要对多个字段使用 IN 谓词时,将它们汇总到一处
-- 一个表的多个字段可能都使用了 IN 谓词,如下:
SELECT id, state, city 
  FROM Addresses1 A1
 WHERE state IN (SELECT state
                   FROM Addresses2 A2
                  WHERE A1.id = A2.id) 
    AND city IN (SELECT city
                   FROM Addresses2 A2 
                  WHERE A1.id = A2.id);
                  
-- 这段代码用到了两个子查询,也就产生了两个中间表,可以像下面这样写
-- 这样子查询不用考虑关联性,没有中间表产生,而且只执行一次即可。
SELECT *
  FROM Addresses1 A1
 WHERE id || state || city
 IN (SELECT id || state|| city
       FROM Addresses2 A2);

6. 使用延迟查询优化 limit [offset], [rows]

经常出现类似以下的 SQL 语句,当偏移量最大的时候,查询效率就会越低,因为Mysql并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。

-- offset 特别大!
SELECT * FROM film LIMIT 100000, 10

方案一:

可以通过延迟查询的方式来优化,假设有以下 SQL,有组合索引(sex, rating)

SELECT <cols> FROM profiles where sex='M' order by rating limit 100000, 10;

-- 则上述写法可以改成如下写法

SELECT <cols> 
  FROM profiles 
inner join
(SELECT id form FROM profiles where x.sex='M' order by rating limit 100000, 10)as x 
using(id);

-- 注意:using关键字的概念:连接查询时如果是同名字段作为连接条件,using可以代替on出现(比on更好)

可以看到,在上述语句的子查询中,“SELECT id FROM student LIMIT 100000,10”只查询了主键id一个字段,对于这样的索引覆盖情况,查询速度还是可以接受的。就等于说,该子查询只返回了10个id给上级查询。

上级查询接收到这10个id之后,迅速的查询出了这10个id的集合,速度得到了极大提升。

方案二:

可以使用 id > 10000 的方式诱导MySQL使用主键索引。

这样的Where语句MySQL是非常喜欢并且乐于执行的,因为你给到MySQL的是一个范围,它最容易执行的就是有序和范围的查询,这对它来说易如反掌。

当然,使用这个方法需要注意,id最好是连续的,中间的记录没有被物理删除过。如果其中有数据被物理删除过,用在分页场景的话,就看起来不是那么的优雅。当然,大部分数据库的参与者都倾向于逻辑删除。即便是物理删除的话,也可以使用程序去做区分显示,这不是

-- 返回上次查询的最大记录(偏移量)
select id,name from employee where id>10000 limit 10

-- 还可以在业务允许的情况下限制页数:

7. 利用 LIMIT 1 取得唯一行

数据库引擎只要发现满足条件的一行数据则立即停止扫描,这种情况适用于只需查找一条满足条件的数据的情况

-- 假设现在有employee员工表,要找出一个名字叫jay的人.

-- 反例
select id,name from employee where name='jay'

-- 正例
select id,name from employee where name='jay' limit 1;

理由:

  • 加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。
  • 当然,如果name是唯一索引的话,是不必要加上limit 1了,因为limit的存在主要就是为了防止全表扫描,从而提高性能,如果一个语句本身可以预知不用全表扫描,有没有limit ,性能的差别并不大。

8. 使用联合索引时,注意索引列的顺序,一般遵循最左匹配原则

  • 当我们创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。

  • 假设存在这样顺序的一个联合索引“col_1, col_2, col_3”。这时,指定条件的顺序就很重要。前面两条会命中索引,第三条由于没有先匹配 col_1,导致无法命中索引, 另外如果无法保证查询条件里列的顺序与索引一致,可以考虑将联合索引 拆分为多个索引。

    ○ SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 AND col_3 = 500;
    ○ SELECT * FROM SomeTable WHERE col_1 = 10 AND col_2 = 100 ;
    × SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ;
    
  • 使用 LIKE 谓词时,只有前方一致的匹配才能用到索引(最左匹配原则)

    × SELECT * FROM SomeTable WHERE col_1 LIKE '%a';
    × SELECT * FROM SomeTable WHERE col_1 LIKE '%a%';
    ○ SELECT * FROM SomeTable WHERE col_1 LIKE 'a%';
    
  • 简单字符串表达式,模型字符串可以使用 _ 时, 尽可能避免使用 %, 假设某一列上为 char(5)

    -- 不推荐
    SELECT 
        first_name, 
        last_name,
        homeroom_nbr
      FROM Students
     WHERE homeroom_nbr LIKE 'A-1%';
     
     -- 推荐
     SELECT first_name, last_name
    homeroom_nbr
      FROM Students
     WHERE homeroom_nbr LIKE 'A-1__'; --模式字符串中包含了两个下划线
    

9. 尽量使用自增 id 作为主键

比如现在有一个用户表,有人说身份证是唯一的,也可以用作主键,理论上确实可以,不过用身份证作主键的话,一是占用空间相对于自增主键大了很多,二是很容易引起频繁的页分裂,造成性能问题。

主键选择的几个原则:自增,尽量小,不要对主键进行修改

10. 在无 WHERE 条件下要计算表的行数,优先使用 count(*)

优先使用以下语句来统计行数, innoDB 5.6之后已经对此语句进行了优化

SELECT COUNT(*) FROM SomeTable

按照效率排序的话,count(字段)<count(主键 id)<count(1)≈count(),count() 会选用性能最好的索引来进行排序

11. 避免使用 SELECT * ,尽量利用覆盖索引来优化性能

SELECT * 会提取出一整行的数据,如果查询条件中用的是组合索引进行查找,还会导致回表(先根据组合索引找到叶子节点,再根据叶子节点上的主键回表查询一整行),降低性能,而如果我们所要的数据就在组合索引里,只需读取组合索引列,这样网络带宽将大大减少,假设有组合索引列 (col_1, col_2)

-- 推荐
SELECT col_1, col_2 
  FROM SomeTable 
 WHERE col_1 = xxx AND col_2 = xxx
 
-- 不推荐
SELECT *
  FROM SomeTable 
 WHERE col_1 = xxx AND  col_2 = xxx

理由:

  • 只取需要的字段,节省资源、减少网络开销。
  • select * 进行查询时,很可能就不会使用到覆盖索引了,就会造成回表查询。

12. 使用 force index() 强制走某个索引

例如:现类似以下的慢 SQL 查询

SELECT *
  FROM  SomeTable
 WHERE `status` = 0
   AND `gmt_create` > 1490025600
   AND `gmt_create` < 1490630400
   AND `id` > 0
   AND `post_id` IN ('67778', '67811', '67833', '67834', '67839', '67852', '67861', '67868', '67870', '67878', '67909', '67948', '67951', '67963', '67977', '67983', '67985', '67991', '68032', '68038'/*... omitted 480 items ...*/)
order by id asc limit 200;

post_id 也加了索引,理论上走 post_id 索引会很快查询出来,但实现了通过 EXPLAIN 发现走的却是 id 的索引(这里隐含了一个常见考点,在多个索引的情况下, MySQL 会如何选择索引),而 id > 0 这个查询条件没啥用,直接导致了全表扫描, 所以在有多个索引的情况下一定要慎用,可以使用 force index 来强制走某个索引,以这个例子为例,可以强制走 post_id 索引,效果立杆见影。

13. EXPLAIN 来查看 SQL 执行计划

使用 EXPLAIN 来分析 SQL 的执行情况,如怎么发现上文中的最左匹配原则不生效呢,执行 「EXPLAIN + SQL 语句」可以发现 key 为 None ,说明确实没有命中索引

1)、type

依次从好到差:system,const,eq_ref,ref,fulltext,ref_or_null,unique_subquery,index_subquery,range,index_merge,index,ALL。

除了all之外,其他的type都可以使用到索引,除了index_merge之外,其他的type只可以用到一个索引。

14. 批量插入,速度更快

当需要插入数据时,批量插入比逐条插入性能更高

-- 推荐
INSERT INTO TABLE (id, user_id, title) VALUES (1, 2, 'a'),(2,3,'b');

-- 不推荐用
INSERT INTO TABLE (id, user_id, title) VALUES (1, 2, 'a');
INSERT INTO TABLE (id, user_id, tit	le) VALUES (2,3,'b');

批量插入 SQL 执行效率高的主要原因是合并后日志量 MySQL 的 binlog 和 innodb 的事务让日志减少了,降低日志刷盘的数据量和频率,从而提高了效率

15. 慢日志 SQL 定位

sql定位主要用到了以下几个参数:

  • slow_query_log:这个参数设置为on,可以捕获执行时间超过一定数值的sql语句
  • slow_query_log_file:记录日志的文件名
  • long_query_time:当sql语句执行时间超过此数值时,就会被记录到日志中,建议设置为1或者更短

16. Inner join 、left join、right join

  • Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
  • left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
  • right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。

都满足SQL需求的前提下,推荐优先使用Inner join(内连接),如果要使用left join,左边表数据结果尽量小,如果有条件的尽量放到左边处理。

-- 反例
select * from tab1 t1 left join tab2 t2  on t1.size = t2.size where t1.id>2;

-- 正例
select * from (select * from tab1 where id >2) t1 left join tab2 t2 on t1.size = t2.size;

理由:

  • 如果inner join是等值连接,或许返回的行数比较少,所以性能相对会好一点。
  • 使用了左连接,左边表数据结果尽量小,条件尽量放到左边处理,意味着返回的行数可能比较少。

17. 删除冗余和重复索引

-- 反例:
KEY `idx_userId` (`userId`)
KEY `idx_userId_age` (`userId`,`age`)

-- 正例
-- 删除userId索引,因为组合索引(A,B)相当于创建了(A)和(A,B)索引
KEY `idx_userId_age` (`userId`,`age`)
  • 重复的索引需要维护,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能的。

18. 如果数据量较大,优化你的修改/删除语句

避免同时修改或删除过多数据,因为会造成cpu利用率过高,从而影响别人对数据库的访问。

-- 反例
//一次删除10万或者100万+?
delete from user where id <100000;
//或者采用单一循环操作,效率低,时间漫长
for(User user:list){
   delete from user;
}

-- 正例
//分批进行删除,如每次500
delete user where id<500
delete product where id>=500 and id<1000;
  • 一次性删除太多数据,可能会有lock wait timeout exceed的错误,所以建议分批操作

19. where子句中考虑使用默认值代替null

-- 反例
select * from user where age is not null;

-- 正例
//设置0为默认值
select * from user where age>0;

理由:

  • 并不是说使用了is null 或者 is not null 就会不走索引了,这个跟mysql版本以及查询成本都有关。

如果mysql优化器发现,走索引比不走索引成本还要高,肯定会放弃索引,这些条件!=,>is null,is not null经常被认为让索引失效,其实是因为一般情况下,查询的成本高,优化器自动放弃的。

  • 如果把null值,换成默认值,很多时候让走索引成为可能,同时,表达意思会相对清晰一点。

20. 索引不宜太多,一般5个以内

  • 索引并不是越多越好,索引虽然提高了查询的效率,但是也降低了插入和更新的效率。
  • insert或update时有可能会重建索引,所以建索引需要慎重考虑,视具体情况来定。
  • 一个表的索引数最好不要超过5个,若太多需要考虑一些索引是否没有存在的必要。

21. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型

-- 反例:
king_id` varchar(20) NOT NULL COMMENT '守护者Id'

-- 正例:
`king_id` int(11) NOT NULL COMMENT '守护者Id'`

理由:

  • 相对于数字型字段,字符型会降低查询和连接的性能,并会增加存储开销

22. 索引不适合建在有大量重复数据的字段上,如性别这类型数据库字段

因为SQL优化器是根据表中数据量来进行查询优化的,如果索引列有大量重复数据,Mysql查询优化器推算发现不走索引的成本更低,很可能就放弃索引了。

23. 如何字段类型是字符串,where时一定用引号括起来,否则索引失效

-- 反例
select * from user where userid =123;

-- 正例
select * from user where userid ='123';

理由:

  • 为什么第一条语句未加单引号就不走索引了呢?这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。
posted @ 2023-05-19 00:16  lihewei  阅读(175)  评论(1编辑  收藏  举报
-->