让你的SQL飞起来

引言

本文是《SQL进阶教程》的读书笔记。这本书写的很好,推荐读一读。
这一章实在是太重要了,因此贴了出来。不仅工作中需要经常用到,面试的时候也喜欢问。

使用高效的查询

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

CREATE TABLE Class_A
(id char(1), 
 name varchar(30), 
 PRIMARY KEY(id));

CREATE TABLE Class_B
(id   char(1), 
 name varchar(30), 
 PRIMARY KEY(id));

INSERT INTO Class_A (id, name) VALUES('1', '田中');
INSERT INTO Class_A (id, name) VALUES('2', '铃木');
INSERT INTO Class_A (id, name) VALUES('3', '伊集院');

INSERT INTO Class_B (id, name) VALUES('1', '田中');
INSERT INTO Class_B (id, name) VALUES('2', '铃木');
INSERT INTO Class_B (id, name) VALUES('4', '西园寺');

从Class_A中查出同时存在于Class_B表中的员工。

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

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

使用 EXISTS 时更快的原因有以下两个:

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

IN 的参数是子查询时,数据库首先会执行子查询,然后将结果存储在一张临时的工作表里(内联视图),然后扫描整个视图。很多情况下这种做法都非常耗费资源。使用EXISTS 的话,数据库不会生成临时的工作表。

参数是子查询时,使用连接代替IN

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

这种写法至少能用到一张表的id列上的索引。而且,因为没有了子查询,所以数据库也不会生成中间表。

避免排序

会进行排序的代表性运算有下面这些:

  • GROUP BY子句
  • ORDER BY子句
  • 聚合函数(SUMCOUNTAVGMAXMIN)
  • DISTINCT
  • 集合运算符(UNIONINTERSECTEXCEPT)
  • 窗口函数(RANKROW_NUMBER等)

如果只在内存中进行排序,那么还好;但如果内存不足而需要在硬盘上排序,那么排序的性能会急剧恶化。

灵活使用集合运算符的ALL可选项

sql汇总有UNIONINTERSECTEXCEPT三个集合运算符,在默认的使用方式下,这些运算符会为了排除掉重复数据而进行排序。

SELECT * FROM Class_A
UNION
SELECT * FROM Class_B

结果为:

id	name
2	铃木
4	西园寺
3	伊集院
1	田中

如果不在乎结果中是否有重复数据,或事先知道不会有重复数据,那么使用UNION ALL代替UNION,这样就不会进行
排序了。

SELECT * FROM Class_A
UNION ALL
SELECT * FROM Class_B

结果:

id	name
1	田中
2	铃木
3	伊集院
1	田中
2	铃木
4	西园寺

对于INTERSECTEXCEPT 也是一样的,加上ALL可选项后就不会进行排序了。

使用EXISTS代替DISTINCT

为了排序重复元素,DISTINCT也会进行排序,。如果需要对两张表的连接结果进行去重,可以考虑使用EXISTS代替 DISTINCT,以避免排序。

CREATE TABLE Items
 (item_no INTEGER PRIMARY KEY,
  item    VARCHAR(32) NOT NULL);

INSERT INTO Items VALUES(10, 'FD');
INSERT INTO Items VALUES(20, 'CD-R');
INSERT INTO Items VALUES(30, 'MO');
INSERT INTO Items VALUES(40, 'DVD');

CREATE TABLE SalesHistory
 (sale_date DATE NOT NULL,
  item_no   INTEGER NOT NULL,
  quantity  INTEGER NOT NULL,
  PRIMARY KEY(sale_date, item_no));

INSERT INTO SalesHistory VALUES('2007-10-01',  10,  4);
INSERT INTO SalesHistory VALUES('2007-10-01',  20, 10);
INSERT INTO SalesHistory VALUES('2007-10-01',  30,  3);
INSERT INTO SalesHistory VALUES('2007-10-03',  10, 32);
INSERT INTO SalesHistory VALUES('2007-10-03',  30, 12);
INSERT INTO SalesHistory VALUES('2007-10-04',  20, 22);
INSERT INTO SalesHistory VALUES('2007-10-04',  30,  7);

考虑一下如何从找出有销售记录的商品。

我们使用item_no对两张表进行连接。

SELECT I.item_no
FROM Items I INNER JOIN saleshistory SH
ON I.item_no = SH.item_no;

结果:

item_no
10
20
30
10
30
20
30

为了排除重复数据,我们需要使用DISTINCT

SELECT DISTINCT I.item_no
FROM Items I INNER JOIN saleshistory SH
ON I.item_no = SH.item_no;

结果:

item_no
20
10
30

其实更好的做法是使用EXISTS:

SELECT item_no
FROM Items I 
WHERE EXISTS (
			SELECT * FROM SalesHistory SH
			WHERE I.item_no = SH.item_no);

这条语句在啊执行过程中不会进行排序。而且使用EXISTS和使用连接一样高效。

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

使用这两个极值函数时都会进行排序。但如果参数字段上有索引,则只需扫描索引,不需要扫描整张表。以表Items为例:

--这样写需要全表扫描
SELECT MAX(item) FROM Items;
-- 这样写能用到索引
SELECT MAX(item_no) FROM Items;

因为 item_no 是表 Items 的唯一索引,所以效果更好。对于联合索引,只要查询条件是联合索引的第一个字段,索引就是有效的,所以也可以对表 SalesHistory 的 sale_date 字段使用极值函数。

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

下面两条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;

从性能上来看,第二条语句写法效率更高。原因通常有两个。第一个是在使用 GROUP BY子句聚合时会进行排序,如果事先通过
WHERE 子句筛选出一部分行,就能够减轻排序的负担。第二个是在WHERE 子句的条件里可以使用索引。HAVING子句是针对聚合后生成的视图进行筛选的,但是很多时候聚合后的视图都没有继承原表的索引结构 。

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

一般来说,GROUP BY 子句和 ORDER BY 子句都会进行排序,来对行进行排列和替换。不过,通过指定带索引的列作为GROUP BYORDER BY 的列,可以实现高速查询。

真的用到索引了吗

假设我们在一个叫col_1的列上建立了索引,然后看一下这条sql语句:

--在索引字段上进行运算
SELECT * FROM SomeTable WHERE col_1 * 1.1 > 100;

这条sql语句本来是想使用索引,但实际上却进行了全表扫描。

把运算的表达式放到查询条件的右侧,就能用到索引了:

SELECT * FROM SomeTable WHERE col_1 > 100 / 1.1;

同样,在查询条件的左侧使用函数时,也不能用到索引:

SELECT * FROM SomeTable WHERE SUBSTR(col_1,1,1) = 'a';

如果无法避免在左侧进行运算,那么使用函数索引也是一种办法,但是不推荐。

使用索引时,条件表达式的左侧应该是原始字段

这个我在工作中就碰到过这种SQL,比如查询一个数据量很大的表xxx_realtime,其中时间字段entry_time加了索引。
假设要查询某天的所有数据,优化前的写法如下:

SELECT ... FROM xxx_realtime WHERE 
to_char(entry_time,'YYYY-MM-DD) = #{param.date}

其中数据库是Postgresql,#{param.date}是传过来的日期字符串(eg: 2019-11-11),entry_time的格式是timestamp。根据这条优化建议,优化后的SQL如下:

SELECT ... FROM xxx_realtime WHERE 
entry_time BETWEEN TO_TIMESTAMP(#{param.date} || ' 00:00:00'),'yyyy-MM-dd hh24:mi:ss')
AND TO_TIMESTAMP(#{param.date} || ' 23:59:59'),'yyyy-MM-dd hh24:mi:ss')

在PG中,BETWEENAND是包含两端的

使用IS NULL谓词

通常索引字段是不存在NULL的,所以指定IS NULLIS NOT NULL的话会使得索引无法使用,进而导致查询性能低下。

SELECT * FROM SomeTable WHERE col_1 IS NULL;

在 DB2 和 Oracle 中,IS NULL 条件也能使用索引。这也许是因为它们在实现时为 NULL
赋了某个具有特殊含义的值。但是,这个特性不是所有数据库都有的。

如果需要使用类似IS NOT NULL的功能,又想用到索引,那么可以使用下面的方法,假设col_1列的最小值是1

SELECT * FROM SomeTable WHERE col_1 > 0;

只要使用非等号并指定一个比自小智还小的数,就可以选出col_1中所有的值。因为col_1 > NULL的执行结果是unknown,列值为NULL的行不会被选中。

使用否定形式

下面这几种否定形式不能用到索引:

  • <> (!=)
  • NOT IN

因此,下面的sql会进行全表扫描:

SELECT * FROM SomeTable WHERE col_1 <> 100;

使用OR

在col_1和col_2上分别建立了不同的索引,或建立了(col_1,col_2)这样的联合索引时,如果使用OR连接条件,那么要么用不到索引,要么用到了但是效率比AND要差很多。

SELECT * FROM SomeTable WHERE col_1 > 100 OR col_2 = 'abc';

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

假设存在这样顺序的一个联合索引"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 ;

可以把这三个联合索引想成一座桥,第一条sql顺序正确,并且是一个完整的桥,用到了整个联合索引。
第二条只是部分正确,因此用到了联合索引的部分。
后面的3条要么是桥中间或前面断了要么是顺序反了。

如果无法保证查询条件里列的顺序与索引一致,可以考虑将联合索引拆分成多个索引。

使用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%';

进行默认的类型转换

对char类型的col_1列指定条件的示例:

× 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));

默认的类型转换不仅会增加额外的性能开销,还会导致索引不可用。在需要类型转换时显示地进行类型转换吧(别忘了转换要写在条件表达式的右边)

减少中间表

在 sql 中,子查询的结果会被看成一张新表,这张新表与原始表一样,可以通过代码进行操作。这种高度的相似性使得 sql 编程具有非常强的灵活性,但是如果不加限制地大量使用中间表,会导致查询性能下降。

频繁使用中间表会带来两个问题,一是展开数据需要耗费内存资源,二是原始表中的索引不容易使用到(特别是聚合时)。

灵活使用HAVING子句

对聚合结果指定筛选条件时,使用HAVING子句是基本原则。不习惯使用HAVING子句可能会这样写:

SELECT *
FROM (SELECT sale_date, MAX(quantity) AS max_qty
FROM SalesHistory
GROUP BY sale_date) TMP ←----- 没用的中间表
WHERE max_qty >= 10;

然后,对聚合结果指定筛选条件时不需要生成中间表,像下面这样:

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

HAVING子句和聚合操作是同时执行的,所以比起生成中间表后再执行的WHERE子句,效率会更高一些,而且代码看起来更简洁。

需要对多个字段使用IN谓词时,将它们汇总到一处

sql-92中加入了行与行比较的功能。这样一来,比较谓词=><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);

这样一来,子查询不用考虑关联性,而且只执行一次就可以了。

此外,如果所用的数据库实现了行与行的比较,那么我也可以像下面这样,在IN中写多个字段的组合。

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

这种方法与前面的连接字段的方法相比有两个优点。一是不用担心连接字段时出现的类型转换问题,二是这种方法不会对字段进行加工,因此可以使用索引。

先进行连接再进行聚合

连接和聚合同时使用时,先进行连接操作可以避免产生中间表。原因是,从集合运算的角度来看,连接做的是“乘法运算”。
连接表双方是一对一、一对多的关系时,连接运算后数据的行数不会增加。而且,因为在很多设计中多对多的关系都可以分解成两个一对多的关系,因此这个技巧在大部分情况下都可以使用。

合理地使用视图

视图是非常方便的工具,相信日常工作中很多人都在频繁地使用。但是,如果没有经过深入思考就定义复杂的视图,可能会带来巨大的性能问题。特别是视图的定义语句中包含以下运算的时候,sql 会非常低效,执行速度也会变得非常慢。

  • 聚合函数(AVGCOUNTSUMMINMAX
  • 集合运算符(UNION 、*NTERSECTEXCEPT 等)

一般来说,要格外注意避免在视图中进行聚合操作。

发布了131 篇原创文章 · 获赞 38 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/102976843