SQL语句性能优化

介绍一些只需调整 SQL 语句就能实现的通用的优化 Tips。

SQL优化基本出发点,即本质

其实不只是数据库和 SQL,计算机世界里容易成为性能瓶颈的也是对硬盘,也就是文件系统的访问(因此个人计算机还可以通过增加内存,或者使用访问速度更快的硬盘等方法来提升性能)。不管是减少排序还是使用索引,抑或是避免中间表的使用,都是为了减少对硬盘的访问。



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

例如:

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


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

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


避免排序

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

GROUP BY 子句

ORDER BY 子句

聚合函数(SUMCOUNTAVGMAXMIN

DISTINCT

集合运算符(UNIONINTERSECTEXCEPT

窗口函数(RANKROW_NUMBER 等)

排序如果只在内存中进行,那么还好;但是如果内存不足因而需要在硬盘上排序,那么伴随着“呲啦呲啦”的硬盘访问声,排序的性能也会急剧恶化(下面的数据可能不太准确……据说硬盘的访问速度比内存的要慢上 100 万倍)。因此,尽量避免(或减少)无谓的排序是我们的目标。

使用 EXISTS 代替 DISTINCT

因为后者会进行排序,示例,略

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

即max,或min 函数的参数列,在该列上建立索引。

这种方法并不是去掉了排序这一过程,而是优化了排序前的查找速度,从而减弱排序对整体性能的影响

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

原因通常有两个。第

一个是在使用 GROUP BY 子句聚合时会进行排序,如果事先通过 WHERE 子

句筛选出一部分行,就能够减轻排序的负担。第二个是在 WHERE 子句的条

件里可以使用索引。HAVING 子句是针对聚合后生成的视图进行筛选的,

但是很多时候聚合后的视图都没有继承原表的索引结构。

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

一般来说,GROUP BY 子句和 ORDER BY 子句都会进行排序,来对行

进行排列和替换。不过,通过指定带索引的列作为 GROUP BY 和 ORDER

BY 的列,可以实现高速查询。特别是,在一些数据库中,如果操作对象

的列上建立的是唯一索引,那么排序过程本身都会被省略掉。

使用索引注意事项

使用索引时,条件表达式的左侧应该是原始字段。如果左边不是原始字段,则SQL 语句本来是想使用索引,但实际上执行时却进行了全表扫描。

比如为col_1 建立了索引:

​ WHERE col_1 * 1.1 > 100;​​ 不如 ​​WHERE col_1 > 100 / 1.1;​

同样的,在查询条件左侧使用函数时,也不能用到索引,比如下面这句:

​ WHERE SUBSTR(col_1, 1, 1) = 'a';​

下面几种否定形式也不能用到索引,会全表扫描

  • <>
  • !=
  • not in

使用 or 的时候,两边的条件中用到的列如果是有索引的,也是达不到使用索引的理想效果的

使用 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 子句是基本原则。

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

--更优的查询。HAVING 子句和聚合操作是同时执行的,所以比起生成中间表后再执行的 WHERE 子句,效率会更高一些,而且代码看起来也更简洁。
SELECT sale_date, MAX(quantity)
FROM SalesHistory
GROUP BY sale_date
HAVING MAX(quantity) >= 10;


合理使用视图

特别是视图的定义语句中包含以下运算的时候,SQL 会非常低效,执

行速度也会变得非常慢。

  • 聚合函数(AVGCOUNTSUMMINMAX
  • 集合运算符(UNIONINTERSECTEXCEPT 等)


参考书籍:图灵社区的《SQL进价教程》