最近博主看完了《SQL进阶教程》这本书,看完后给博主打开了SQL国际的新大门,关于 SQL 的了解不在局限于以前的常规用法。借用其他读者的谈论,

读完醍醐灌顶,对SQL做到了知其然更能知其所以然。全书从头到尾强调了 SQL的内涵逻辑是根据调集论和谓词逻辑,而着两条主线恰恰在运用SQL起到了至关重要的指导作用。

本文给大家总结怎么让SQL起飞(优化

让SQL起飞(优化)

一、SQL写法优化

在SQL中,许多时分不同的SQL代码能够得出相同成果。从理论上来说,咱们以为得到相同成果的不同SQL之间应该有相同的功能,但惋惜的是,查询优化器生成的履行计划很大程度上受到SQL代码影响,有快有慢。因而假如想优化查询功能,咱们有必要知道怎么写出更快的SQL,才能使优化器的履行功率更高。

1.1 子查询用EXISTS替代IN

当IN的参数是子查询时,数据库首先会履行子查询,然后将成果存储在一张暂时的工作表里(内联视图),然后扫描整个视图。许多情况下这种做法都十分消耗资源。运用EXISTS的话,数据库不会生成暂时的工作表。可是从代码的可读性上来看,IN要比EXISTS好。运用IN时的代码看起来更加一目了然,易于了解。因而,假如坚信运用IN也能快速获取成果,就没有必要非得改成EXISTS了。

这儿用Class_A表和Class_B举例,

让SQL起飞(优化)
咱们试着从Class_A表中查出一起存在于Class_B表中的职工。下面两条SQL句子回来的成果是相同的,可是运用EXISTS的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时更快的原因有以下两个。

  1. 假如衔接列(id)上树立了索引,那么查询 tb_b 时不必查实际的表,只需查索引就能够了。(相同的IN也能够运用索引,这不是重要原因)
  2. 假如运用EXISTS,那么只要查到一行数据满足条件就会终止查询,不必像运用IN时相同扫描全表。在这一点上NOT EXISTS也相同。

实际上,大部分情况在子查询数量较小的场景下EXISTS和IN的查询功能平起平坐,由EXISTS查询更快第二点可知,子查询数量较大时运用EXISTS才会有明显优势。

1.2 避免排序并添加索引

在SQL语言中,除了ORDER BY子句会进行显示排序外,还有许多操作默许也会在私自进行排序,假如排序字段没有添加索引,会导致查询功能很慢。SQL中会进行排序的代表性的运算有下面这些。

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

如上列出的六种运算(除了调集运算符),它们后边跟从或许指定的字段都能够添加索引,这样能够加快排序。

实际上在DISTINCT关键字、GROUP BY子句、ORDER BY子句、聚合函数跟从的字段都添加索引,不仅能加快查询,还能加快排序。

1.3 用EXISTS替代DISTINCT

为了排除重复数据,咱们可能会运用DISTINCT关键字。如1.2中所说,默许情况下,它也会进行私自排序。假如需求对两张表的衔接成果进行去重,能够考虑运用EXISTS替代DISTINCT,以避免排序。这儿用Items表和SalesHistory表举例:

让SQL起飞(优化)
咱们考虑一下怎么从上面的产品表Items中找出一起存在于销售记录表SalesHistory中的产品。简而言之,便是找出有销售记录的产品。

在一(Items)对多(SalesHistory)的场景下,咱们需求对item_no去重,运用DISTINCT去重,因而SQL如下:

SELECT DISTINCT I.item_no
  FROM Items I INNER JOIN SalesHistory SH
    ON I. item_no = SH. item_no;
item_no
-------
    10
    20
    30

运用EXISTS替代DISTINCT去重,SQL如下:

SELECT item_no
  FROM Items I
 WHERE EXISTS
          (SELECTFROM SalesHistory SH
            WHERE I.item_no = SH.item_no);
item_no
-------
    10
    20
    30

这条句子在履行过程中不会进行排序。并且运用EXISTS和运用衔接相同高效。

1.4 调集运算ALL可选项

SQL中有UNION、INTERSECT、EXCEPT三个调集运算符。在默许的运用办法下,这些运算符会为了排除去重复数据而进行排序。

MySQL还没有实现INTERSECT和EXCEPT运算

假如不在乎成果中是否有重复数据,或许事前知道不会有重复数据,请运用UNION ALL替代UNION。这样就不会进行排序了。

1.5 WHERE条件不要写在HAVING字句

例如,这儿持续用SalesHistory表举例,下面两条SQL句子回来的成果是相同的:

--聚合后运用HAVING子句过滤
SELECT sale_date, SUM(quantity)
  FROM SalesHistory
 GROUP BY sale_date
HAVING sale_date = '2007-10-01';
--聚合前运用WHERE子句过滤
SELECT sale_date, SUM(quantity)
  FROM SalesHistory
 WHERE sale_date = '2007-10-01'
 GROUP BY sale_date;

可是从功能上来看,第二条句子写法功率更高。原因有两个:

  1. 运用GROUP BY子句聚合时会进行排序,假如事前通过WHERE子句挑选出一部分行,就能够减轻排序的担负。
  2. 在WHERE子句的条件里能够运用索引。HAVING子句是针对聚合后生成的视图进行挑选的,可是许多时分聚合后的视图都没有承继原表的索引结构。

二、真的用到索引了吗

2.1 隐式的类型转换

如下,col_1字段是char类型:

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)); -- 走了索引

当查询条件左边和右边类型不一致时会导致索引失效。

2.2 在索引字段进步行运算

如下:

SELECT *
  FROM SomeTable
 WHERE col_1 * 1.1 > 100;

在索引字段col_1进步行运算会导致索引不收效,把运算的表达式放到查询条件的右侧,就能用到索引了,像下面这样写就OK了。

WHERE col_1 > 100 / 1.1

假如无法避免在左边进行运算,那么运用函数索引也是一种办法,可是不太引荐随意这么做。运用索引时,条件表达式的左边应该是原始字段请紧记,这一点是在优化索引时首要重视的地方。

2.3 运用否定方式

下面这几种否定方式不能用到索引。

  • <>
  • !=
  • NOT

这个是跟具体数据库的优化器有关,假如优化器觉得即使走了索引,还是需求扫描许多许多行的哈,他能够挑选直接不走索引。平时咱们用!=、<>、not in的时分,要注意一下。

2.4 运用OR查询前后没有一起运用索引

例如下表:

CREATE TABLE test_tb (
	id int(11) NOT NULL AUTO_INCREMENT, 
	name varchar(55) NOT NULL
	PRIMARY KEY (id)
) 
ENGINE=InnoDB DEFAULT CHARSET=utf8;

运用OR条件进行查询

SELECT *
FROM test_tb 
WHERE id = 1 OR name = 'tom'

这个SQL的履行条件下,很明显id字段查询会走索引,可是关于OR后边name字段的查询是需求进行全表扫描的。在这个场景下,优化器直接进行一遍全表扫描就完事了。

2.5 运用联合索引时,列的顺序错误

运用联合索引需求满足最左匹配准则,即最左优先。假如你树立一个(col_1, col_2, col_3)的联合索引,相当于树立了 (col_1)、(col_1,col_2)、(col_1,col_2,col_3) 三个索引。如下例子:

-- 走了索引
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_1 = 10 AND col_3 = 500 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_3 = 500 ;
-- 没走索引
SELECT * FROM SomeTable WHERE col_2 = 100 AND col_1 = 10 ;

联合索引中的第一列(col_1)有必要写在查询条件的最初,并且索引中列的顺序不能倒置。

2.6 运用LIKE查询

并不是用了like通配符,索引一定会失效,而是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%';

2.7 衔接字段字符集编码不一致

假如两张表进行衔接,相关字段编码不一致会导致相关字段上的索引失效,这是博主在线上经历一次SQL慢查询后的得到的成果,举例如下,有如下两表,它们的name字段都建有索引,可是编码不一致,user表的name字段编码是utf8mb4,user_job表的name字段编码是utf8,

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER
  SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  `age` int NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE `user_job` (
  `id` int NOT NULL,
  `userId` int NOT NULL,
  `job` varchar(255) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

进行SQL查询如下:

EXPLAIN
SELECT * 
from `user` u 
join user_job j on u.name = j.name

让SQL起飞(优化)
由成果可知,user表的查询没有走索引。想要user表也走索引,那就需求把user表name字段的编码改成utf8即可。

三、削减中心表

在SQL中,子查询的成果会被看成一张新表,这张新表与原始表相同,能够通过代码进行操作。这种高度的相似性使得SQL编程具有十分强的灵活性,可是假如不加约束地很多运用中心表,会导致查询功能下降。

频繁运用中心表会带来两个问题,一是打开数据需求消耗内存资源,二是原始表中的索引不容易运用到(特别是聚合时)。因而,尽量削减中心表的运用也是提升功能的一个重要办法。

3.1 运用HAVING子句

对聚合成果指定挑选条件时,运用HAVING子句是基本准则。不习惯运用HAVING子句的人可能会倾向于像下面这样先生成一张中心表,然后在WHERE子句中指定挑选条件。例如下面:

SELECT *
  FROM (
    SELECT sale_date, MAX(quantity) max_qty
      FROM SalesHistory
       GROUP BY sale_date
     ) tmp
 WHERE max_qty >= 10

但是,对聚合成果指定挑选条件时不需求专门生成中心表,像下面这样运用HAVING子句就能够。

SELECT sale_date, MAX(quantity)
  FROM SalesHistory
 GROUP BY sale_date
HAVING MAX(quantity) >= 10;

HAVING子句和聚合操作是一起履行的,所以比起生成中心表后再履行的WHERE子句,功率会更高一些,并且代码看起来也更简洁。

3.2 对多个字段运用IN

当咱们需求对多个字段运用IN条件查询时,能够通过 || 操作将字段衔接在一起变成一个字符串处理。

SELECT *
  FROM Addresses1 A1
 WHERE id || state || city
    IN (SELECT id || state|| city
          FROM Addresses2 A2);

这样一来,子查询不必考虑相关性,并且只履行一次就能够。

3.3 先进行衔接再进行聚合

衔接和聚合一起运用时,先进行衔接操作能够避免发生中心表。原因是,从调集运算的角度来看,衔接做的是“乘法运算”。衔接表两边是1对1、一对多的联系时,衔接运算后数据的行数不会添加。并且,因为在许多设计中多对多的联系都能够分解成两个一对多的联系,因而这个技巧在大部分情况下都能够运用。

到此本文解说完毕,感谢大家阅览,感兴趣的朋友能够点赞加重视,你的支撑将是我更新动力。

大众号【waynblog】每周更新博主最新技术文章,欢迎大家重视

本文正在参与「金石计划」