SQL性能优化
- 1、参数是子查询时,使用EXISTS代替IN
- 2、参数是子查询时,使用连接代替IN
- 3、避免排序
- 4、灵活使用集合运算符的ALL可选项
- 5、使用EXISTS代替DISTINCT
- 6、在极值函数中使用索引(MAX/MIN)
- 7、能写在WHERE子句里的条件不要写在HAVING子句里
- 8、在GROUP BY子句和ORDER BY子句中使用索引
- 9、真的用到索引了吗
- 10、在索引字段上进行运算
- 11、索引字段中存在NULL
- 12、使用否定形式
- 13、使用联合索引时,列的顺序错误
- 14、使用LIKE谓词进行后方一致或中间一致的匹配
- 15、进行默认的类型转换
- 16、减少中间表
- 17、灵活使用HAVING子句
- 18、需要对多个字段使用IN谓词时,先将它们汇总到一处
- 19、先进行连接再进行聚合
- 20、合理地使用视图
1、参数是子查询时,使用EXISTS代替IN
IN谓词非常方便,而且代码也容易理解,所以使用的频率很高。但是方便的同时,IN谓词却有成为性能优化的瓶颈的危险。一般来说,如果代码中大量用到了IN谓词,那么我们只要对它们进行优化,就能大幅度地提升性能。
如果IN的参数是“1, 2, 3”这样的数值列表,一般还不需要特别注意。但是如果参数是子查询,那么就需要注意了。
在大多时候,[NOT] IN和 [NOT] EXISTS返回的结果是相同的。但是当它们用于子查询时,EXISTS的速度会更快一些。
我们先看一个例子。这里使用前面用过的两张用于管理班级和学生的表作为测试数据。

我们试着从表Class_A中查出同时存在于表Class_B中的员工。下面两条SQL语句返回的结果是一样的,但是使用EXISTS的SQL语句更快一些。
-- 慢
SELECT *
FROM Class_A
WHERE id IN (SELECT id FROM Class_B);
+----+------+
| id | name |
+----+------+
| 1 | 田中 |
| 2 | 铃木 |
+----+------+
-- 快
SELECT *
FROM Class_A A
WHERE EXISTS (
SELECT *
FROM Class_B B
WHERE A.id = B.id
);
+----+------+
| id | name |
+----+------+
| 1 | 田中 |
| 2 | 铃木 |
+----+------+
使用EXISTS时更快的原因有以下两个。
- 如果连接列(id)上建立了索引,那么查询Class_B时不用查实际的表,只需查索引就可以了。
- 如果使用EXISTS,那么只要查到一行数据满足条件就会终止查询,不用像使用IN时一样扫描全表。在这一点上,NOT EXISTS也一样。
当IN的参数是子查询时,数据库首先会执行子查询,然后将结果存储在一张临时的工作表里(内联视图),接着再扫描整个视图。在很多情况下,这种做法非常耗费资源,而且工作表中通常没有索引。使用EXISTS的话,数据库不会生成临时的工作表。
但是从代码的可读性上来看,IN要比EXISTS好。使用IN时的代码看起来更加一目了然,易于理解。因此,如果确信使用IN也能快速获取结果,就没有必要非得改成EXISTS了。
而且,最近有很多DBMS也尝试着改善了IN的性能。例如,在Oracle数据库中,如果我们使用了建有索引的列,那么即使使用IN,也会先扫描索引。此外,PostgreSQL从版本7.4起也改善了使用子查询作为IN谓词参数时的查询速度。因此,也许在未来的某一天,使用IN谓词将不再被看作一种性能方面的反模式。
2、参数是子查询时,使用连接代替IN
要想改善IN的性能,除了使用EXISTS,还可以使用连接。前面的查询语句就可以像下面这样“扁平化”。
-- 使用连接代替 IN
SELECT A.id, A.name
FROM Class_A A INNER JOIN Class_B B
ON A.id = B.id;
+----+------+
| id | name |
+----+------+
| 1 | 田中 |
| 2 | 铃木 |
+----+------+
这种写法至少能用到一张表的id列上的索引。而且,因为没有了子查询,所以数据库也不会生成中间表。我们很难说这和写法与EXISTS相比哪个更好,但是如果没有索引,那么与连接相比,可能EXISTS会略胜一筹。
3、避免排序
与面向过程语言不同,在SQL语言中,用户不能显式地命令DBSM进行排序操作。对用户隐藏这样的操作正是SQL的设计思想。
但是,这样并不意味着在DBSM内部也不能进行排序。其实正好相反,DBSM的内部会频繁地在暗中进行排序。因此最终对于用户来说,了解都有哪些运算会进行排序很有必要(从这个意义上讲,“隐藏操作”这个目标的实现似乎还任重道远)。
会进行排序的具有代表性的运算有下面这些。
- GROUP BY子句
- ORDER BY子句
- 聚合函数(SUM、COUNT、AVG、MAX、MIN)
- DISTINCT
- 集合运算符(UNION、INTERSECT、EXCEPT)
- 窗口函数(RANK、ROW_NUMBER等)
排序如果只在内存中进行,那么还好;但是如果因内存不足而需要在硬盘上排序,那么排序的性能也会急剧恶化。
因此,我们的目标是尽量避免无谓的排序。
4、灵活使用集合运算符的ALL可选项
SQL中有UNION、INTERSECT、EXCEPT三个集合运算符。在默认的使用方式下,这些运算符会为了排除掉重复数据而进行排序。
SELECT * FROM Class_A
UNION
SELECT * FROM Class_B;
+----+--------+
| id | name |
+----+--------+
| 1 | 田中 |
| 2 | 铃木 |
| 3 | 伊集院 |
| 4 | 西园寺 |
+----+--------+
如果不在乎结果中是否有重复数据,或者事先知道不会有重复数据,那么请使用UNION ALL来代替UNION。这样就不会进行排序了。
SELECT * FROM Class_A
UNION ALL
SELECT * FROM Class_B;
+----+--------+
| id | name |
+----+--------+
| 1 | 田中 |
| 2 | 铃木 |
| 3 | 伊集院 |
| 1 | 田中 |
| 2 | 铃木 |
| 4 | 西园寺 |
+----+--------+
对于INTERSECT和EXCEPT也是一样的,加上ALL可选项后就不会进行排序了。加上ALL可选项是优化性能的一个非常有效的手段,但问题是各种DBMS对它的实现情况参差不齐,对此我们需要多加注意。人们很容易想当然地以为所有的数据库都支持ALL可选项,但事实并非如此,请大家记住这一点。
5、使用EXISTS代替DISTINCT
为了排除重复数据,DISTINCT也会进行排序。如果需要对两张表的连接结果进行去重,可以考虑使用EXISTS代替DISTINCT,以避免排序。

我们来思考一下如何从上面的商品表Items中找出同时存在于销售记录表SalesHistory中的商品。简而言之,就是找出有销售记录的商品。
使用IN是一种做法,但是前面我们说过,当IN的参数是子查询时,使用连接要比使用IN更好。因此,我们像下面这样使用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 |
+---------+
因为是一对多的连接,所以item_no列中会出现重复数据。为了排除重复数据,我们需要使用DISTINCT。
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。
SELECT item_no
FROM Items I
WHERE EXISTS (
SELECT *
FROM SalesHistory SH
WHERE I.item_no = SH.item_no
);
+---------+
| item_no |
+---------+
| 10 |
| 20 |
| 30 |
+---------+
这条语句在执行过程中不会进行排序。而且,使用EXISTS和使用连接一样高效。
6、在极值函数中使用索引(MAX/MIN)
SQL语言里有MAX和MIN两个极值函数,使用这两个函数时都会进行排序。但是如果参数字段上建有索引,则只需要扫描索引,不需要扫描整张表。以刚才的表Items为例来说,SQL语句可以像下面这样写。
-- 这样写需要扫描全表
SELECT MAX(item)
FROM Items;
-- 这样写能用到索引
SELECT MAX(item_no)
FROM Items;
因为item_no是表Items的唯一索引,所以效果更好。对于联合索引,只要查询条件是联合索引的第一个字段,索引就是有效的,所以也可以对表SalesHistory的sale_date字段使用极值函数。这种方法并不是去掉了排序这一过程,而是优化了排序前的查找速度,从而减弱排序对整体性能的影响。
7、能写在WHERE子句里的条件不要写在HAVING子句里
例如,下面两条SQL语句返回的结果是一样的。
-- 聚合后使用 HAVING 子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
GROUP BY sale_date
HAVING sale_date = '2018-10-01';
-- 聚合前使用 WHERE 子句过滤
SELECT sale_date, SUM(quantity)
FROM SalesHistory
WHERE sale_date = '2018-10-01'
GROUP BY sale_date;
+------------+---------------+
| sale_date | SUM(quantity) |
+------------+---------------+
| 2018-10-01 | 17 |
+------------+---------------+
但是从性能上来看,第二条语句写法效率更高。原因有两个:
- 其一,在使用GROUP BY子句聚合时会进行排序或散列运算,如果事先通过WHERE子句筛选出一部分行,就能够减轻排序的负担;
- 其二,在WHERE子句的条件里可以使用索引,sale_date可以说是非常重要的列,如果该列有索引,那么筛选的效率也会非常高。
HAVING子句是针对聚合后生成的视图进行筛选的,但是很多时候,聚合后的视图并没有继承原表的索引结构。
8、在GROUP BY子句和ORDER BY子句中使用索引
一般来说,GROUP BY子句和ORDER BY子句都会进行排序,以对行进行排列和替换。不过,通过指定带索引的列作为GROUP BY和ORDER BY的列,可以实现高速查询。特别是在一些数据库中,如果操作对象的列上建立的是唯一索引,那么排序过程本身就会被省略掉。如果大家有兴趣,可以确认一下自己使用的数据库是否支持这个功能。
9、真的用到索引了吗
一般情况下,我们会对数据量相对较大的表建立索引。简单地说,索引的工作原理与C语言中的指针数组是一样的,即相比查找复杂对象的数组,查找轻量的指针会更高效(我们可以想象一下最常见的图书目录)。而且,最流行的B树索引还进行了一些优化,以使用二分查找来提升查询的速度。
一般情况下,我们会对数据量相对较大的表建立索引。简单地说,索引的工作原理与C语言中的指针数组是一样的,即相比查找复杂对象的数组,查找轻量的指针会更高效(我们可以想象一下最常见的图书目录)。而且,最流行的B树索引还进行了一些优化,以使用二分查找来提升查询的速度。
10、在索引字段上进行运算
SELECT *
FROM SomeTable
WHERE col_1 * 1.1 > 100;
人们普遍认为,SQL语言的主要目的不是进行运算,但实际上,数据库引擎大多连这种程度的转换也不会为我们做。
把运算的表达式放到查询条件的右侧,就能用到索引了,也就是说像下面这样写就好了。
WHERE col_1 > 100 / 1.1
同样,在查询条件的左侧使用函数时,也不会用到索引。
SELECT *
FROM SomeTable
WHERE SUBSTR(col_1, 1, 1)= 'a';
如果无法避免在左侧进行运算,那么使用函数索引也是一种办法,但操作起来很麻烦,所以不太推荐大家随意使用。
使用索引时,列应该是原始字段。
11、索引字段中存在NULL
索引中的NULL不好处理,对此的实现也各不相同。这是因为如果使用IS NULL和IS NOT NULL,索引就无法使用了,而且NULL很多的字段也无法使用索引。
SELECT *
FROM SomeTable
WHERE col_1 IS NULL;
索引字段中的NULL之所以会成为一个难题,是因为从原则上来说NULL并不是字段的正常值(请参考1-4节)。因此,如何处理NULL并没有一个统一的标准,这导致情况变得很复杂。
然而,如果需要使用类似IS NOT NULL的功能,又想用到索引,那么可以使用下面的方法。这里假设col_1列的最小值是1。
--IS NOT NULL 的代替方案
SELECT * FROM SomeTable WHERE col_1 > 0;
原理很简单,只要使用不等号并指定一个比最小值还小的数,就可以选出col_1中所有的值。因为col_1 > NULL的执行结果是 unknown,所以当col_1列的值为NULL的行不会被选择。不过,如果要选择“非NULL的行”,正确的做法还是使用IS NOT NULL。上面这种写法在含义上有些容易混淆,所以笔者也不太推荐,请大家只在应急的情况下使用。
12、使用否定形式
下面这几种否定形式不能用到索引。
<>!=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';
如果无论如何都要使用OR,那么有一个办法是使用位图索引。但是如果使用这种索引,更新数据时的性能开销会增大,而且索引本身的用途也存在限制(通常用于在线更新处理较少的BI/DWH系统)。
13、使用联合索引时,列的顺序错误
假设存在一个顺序是“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 ;
联合索引中的第一列(col_1)必须写在查询条件的开头,而且索引中列的顺序不能颠倒。有些数据库里顺序颠倒后也能使用索引,但是性能还是比顺序正确时差一些。如果无法保证查询条件里列的顺序与索引一致,可以考虑将联合索引拆分为多个索引。
14、使用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%';
15、进行默认的类型转换
下面是对字符串类型的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));
默认的类型转换不仅会增加额外的性能开销,还会导致索引不可用,可以说是有百害而无一利。虽然这样写还不至于出错,但还是不要嫌麻烦,在需要类型转换时显式地进行类型转换吧(别忘了转换要写在值的一边,而不是列的一边)。在有些DBMS(如PostgreSQL)中,数据类型写在表达式的不同侧就会发生错误,显式地进行类型转换还有利于我们在开发时注意避免编写性能低下的查询。
16、减少中间表
在SQL中,子查询的结果会被看成一张新表。这张新表与原始表一样,可以通过代码进行操作。这种高度的相似性使得SQL编程具有非常强的灵活性,但是如果不加限制地大量使用中间表,也会导致查询性能的下降。
频繁地使用中间表会带来两个问题:一是展开数据需要耗费内存(或存储器)资源,二是原始表中的索引不容易被用到(特别是聚合时)。因此,尽量减少中间表的使用也是一个提升性能的重要方法。
17、灵活使用HAVING子句
对聚合结果指定筛选条件时,使用HAVING子句是基本原则。不习惯使用HAVING子句的数据库工程师可能会倾向于像下面这样先生成一张中间表,然后在WHERE子句中指定筛选条件。

sale_date tot_qty
------------ ---------
18-10-01 10
18-10-03 32
18-10-04 22
然而,对聚合结果指定筛选条件时不需要专门生成中间表,像下页这样使用HAVING子句就可以了。
SELECT sale_date, MAX(quantity)
FROM SalesHistory
GROUP BY sale_date
HAVING MAX(quantity)>= 10;
HAVING子句和聚合操作是同时执行的,所以比起先生成中间表,然后再执行的WHERE子句,效率会更高一些,而且代码看起来也更简洁。
18、需要对多个字段使用IN谓词时,先将它们汇总到一处
SQL-92中加入了行与行比较的功能。这样一来,比较谓词=、<、>和IN谓词的参数就不能是标量值,而应是值列表了。
我们来看看下面这道例题。这里对多个字段使用了IN谓词,id列是主键。
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);
这种方法与前面连接字段的方法相比有两个优点:一是不用担心连接字段时出现的类型转换问题,二是这种方法不会对字段进行加工,因此可以使用索引。
19、先进行连接再进行聚合
连接和聚合同时使用时,先进行连接操作可以避免产生中间表。原因是,从集合运算的角度来看,连接做的是“乘法运算”。连接表双方是一对一、一对多的关系时,连接运算后数据的行数不会增加。而且,因为在很多设计中多对多的关系可以分解成两个一对多的关系,所以这个技巧可以应用在大部分情况中。
20、合理地使用视图
视图是非常方便的工具,相信有很多人会在日常工作中频繁地使用它。但是,如果没有经过深入思考就定义复杂的视图,可能会带来巨大的性能问题。特别是当视图的定义语句中包含以下运算时,SQL会非常低效,执行速度也会变得非常慢。
- 聚合函数(AVG、COUNT、SUM、MIN、MAX)
- 集合运算符(UNION、INTERSECT、EXCEPT等)
一般来说,我们需要格外注意,避免在视图中进行聚合操作。最近,越来越多的数据库为了解决视图的这个缺点,实现了物化视图(materialized view)等技术。当视图的定义变得复杂时,大家可以考虑使用那些技术。
341

被折叠的 条评论
为什么被折叠?



