调校SQL效能

 <转>

http://www.dotblogs.com.tw/skyline0217/category/4660.aspx调校SQL效能

好久没搞SQL资料库了,这篇文章蛮受用,为免消失,转载于此。
(原文转载自 http://www.wretch.cc/blog/hcu16b/10264132 )
有些程式员在撰写前端的应用程式时,会透过各种 OOP 语言将存取资料库的 SQL 陈述式串接起来,却忽略了 SQL 语法的效能问题。版工曾听过某半导体大厂的新进程式员,所兜出来的一段 PL/SQL 跑了好几分钟还跑不完;想当然尔,即使他前端的 AJAX 用得再漂亮,程式效能顶多也只是差强人意而已。以下是版工整理出的一些简单心得,让长年钻究 ASP.NET / JSP / AJAX 等前端应用程式,却无暇研究 SQL 语法的程式员,避免踩到一些 SQL 的效能地雷。
1、资料库设计与规划
‧ Primary Key 栏位的长度尽量小,能用 small integer 就不要用 integer。例如员工资料表,若能用员工编号当主键,就不要用身分证字号。
‧ 一般栏位亦同。若该资料表要存放的资料不会超过 3 万笔,用 small integer 即可,不必用 integer。
‧ 文字资料栏位若长度固定,如:身分证字号,就不要用 varchar 或 nvarchar,应该用 char 或 nchar。
‧ 文字资料栏位若长度不固定,如:地址,则应该用 varchar 或 nvarchar。除了可节省储存空间外,存取磁碟时也会较有效率。
‧ 设计栏位时,若其值可有可无,最好也给一个预设值,并设成「不允许 NULL」(一般栏位预设为「允许 NULL」)。因为 SQL Server 在存放和查询有 NULL 的资料表时,会花费额外的运算动作 [2]。
‧ 若一个资料表的栏位过多,应垂直切割成两个以上的资料表,并用同名的 Primary Key 一对多连结起来,如:Northwind 的 Orders、Order Details 资料表。以避免在存取资料时,以丛集索引扫描时会载入过多的资料,或修改资料时造成互相锁定或锁定过久。
------------------------------
2、适当地建立索引
‧ 记得自行帮 Foreign Key 栏位建立索引,即使是很少被 JOIN 的资料表亦然。
‧ 替常被查询或排序的栏位建立索引,如:常被当作 WHERE 子句条件的栏位。
‧ 用来建立索引的栏位,长度不宜过长,不要用超过 20 个位元组的栏位,如:地址。
‧ 不要替内容重复性高的栏位建立索引,如:性别;反之,若重复性低的栏位则适合建立索引,如:姓名。
‧ 不要替使用率低的栏位建立索引。
‧ 不宜替过多栏位建立索引,否则反而会影响到新增、修改、删除的效能,尤其是以线上交易 (OLTP) 为主的网站资料库。
‧ 若资料表存放的资料很少,就不必刻意建立索引。否则可能资料库沿著索引树状结构去搜寻索引中的资料,反而比扫描整个资料表还慢。
‧ 若查询时符合条件的资料很多,则透过「非丛集索引」搜寻的效能,可能反而不如整个资料表逐笔扫描。
‧ 建立「丛集索引」的栏位选择至为重要,会影响到整个索引结构的效能。要用来建立「丛集索引」的栏位,务必选择「整数」型别 (键值会较小)、唯一、不可为 NULL。
------------------------------
3、适当地使用索引
‧ 有些书籍会提到,使用 LIKE、% 做模糊查询时,即使您已替某个栏位建立索引 (如下方例子的 CustomerID),但以常数字元开头才会使用到索引,若以万用字元 (%) 开头则不会使用索引,如下所示:
USE Northwind;
GO
SELECT * FROM Orders WHERE CustomerID LIKE 'D%'; --使用索引
SELECT * FROM Orders WHERE CustomerID LIKE '%D'; --不使用索引
执行完成后按 Ctrl+L,可检阅如下图的「执行计划」。
图 1 可看出「查询最佳化程式」有使用到索引做搜寻

图 2 在此的丛集索引扫描,并未直接使用索引,效能上几乎只等于扫描整个资料表

但经版工反复测试,这种语法是否会使用到索引,抑或会逐笔扫描,并非绝对的。仍要看所下的查询关键字,以及栏位内储存的资料内容而定。但对于储存资料笔数庞大的资料表,最好还是少用 LIKE 做模糊查询。
‧ 以下的运算子会造成「负向查询」,常会让「查询最佳化程式」无法有效地使用索引,最好能用其他运算子和语法改写 (经版工测试,并非有负向运算子,就绝对无法使用索引):
NOT 、 != 、 <> 、 !> 、 !< 、 NOT EXISTS 、 NOT IN 、 NOT LIKE
‧ 避免让 WHERE 子句中的栏位,去做字串串接或数字运算,否则可能导致「查询最佳化程式」无法直接使用索引,而改采丛集索引扫描 (经版工测试并非绝对)。
‧ 资料表中的资料,会依照「丛集索引」栏位的顺序存放,因此当您下 BETWEEN、GROUP BY、ORDER BY 时若有包含「丛集索引」栏位,由于资料已在资料表中排序好,因此可提升查询速度。
‧ 若使用「复合索引」,要注意索引顺序上的第一个栏位,才适合当作过滤条件。
------------------------------
4、避免在 WHERE 子句中对栏位使用函数
对栏位使用函数,也等于对栏位做运算或串接的动作,一样可能会让「查询最佳化程式」无法有效地使用索引。但真正对效能影响最重大的,是当您的资料表内若有 10 万笔资料,则在查询时就需要呼叫函数 10 万次,这点才是真正的效能杀手。程式员应注意,在系统开发初期可能感觉不出差异,但当系统上线且资料持续累积后,这些语法细节所造成的效能问题就会逐步浮 现。
SELECT * FROM Orders WHERE DATEPART(yyyy, OrderDate) = 1996 AND DATEPART(mm, OrderDate)=7
可改成
SELECT * FROM Orders WHERE OrderDate BETWEEN '19960701' AND '19960731'

SELECT * FROM Orders WHERE SUBSTRING(CustomerID, 1, 1) = 'D'
可改成
SELECT * FROM Orders WHERE CustomerID LIKE 'D%'

注意当您在下 UPDATE、DELETE 陈述式时,若有采用 WHERE 子句,也应符合上述原则。
------------------------------
5、AND 与 OR 的使用
在 AND 运算中,「只要有一个」条件有用到索引 (如下方的 CustomerID),即可大幅提升查询速度,如下图 3 所示:
SELECT * FROM Orders WHERE CustomerID='VINET' AND Freight=32.3800 --使用索引,会出现下图 3 的画面

SELECT * FROM Orders WHERE Freight=32.3800 --不使用索引,会出现上图 2 的画面

图 3

但在 OR 运算中,则要「所有的」条件都有可用的索引,才能使用索引来提升查询速度。因此 OR 运算子的使用必须特别小心。
若您将上方 AND 的范例,逻辑运算子改成 OR 的话,如下所示:
SELECT * FROM Orders WHERE CustomerID='VINET' OR Freight=32.3800

由于无法有效地使用索引,也会出现图 2 的画面。
在使用 OR 运算子时,只要有一个条件 (栏位) 没有可用的索引,则其他所有的条件 (栏位) 都有索引也没用,只能如图 2 般,把整个资料表或整个丛集索引都扫描过,以逐笔比对是否有符合条件的资料。
据网路上文件的说法 [1],上述的 OR 运算陈述式,我们还可用 UNION 联集适当地改善,如下:
SELECT * FROM Orders WHERE CustomerID='VINET'
UNION
SELECT * FROM Orders WHERE Freight=32.3800

此时您再按 Ctrl+L 检阅「执行计划」,会发现上半段的查询会使用索引,但下半段仍用丛集索引扫描,对效能不无小补。
------------------------------
6、适当地使用子查询
相较于「子查询 (Subquery)」,若能用 JOIN 完成的查询,一般会比较建议使用后者。原因除了 JOIN 的语法较容易理解外,在多数的情况下,JOIN 的效能也会比子查询较佳;但这并非绝对,也有的情况可能刚好相反。
我们知道子查询可分为「独立子查询」和「关联子查询」两种,前者指子查询的内容可单独执行,后者则无法单独执行,亦即外层查询的「每一次」查询动作都需要引用内层查询的资料,或内层查询的「每一次」查询动作都需要参考外层查询的资料。
以下我们看一个比较极端的例子 [2]。若我们希望所有查询出来的资料,都能另外给一个自动编号,版工我在之前的文章「用 SQL Server 2005 新增的 ROW_NUMBER 函数撰写 GridView 分页」中有介绍过,可用 SQL Server 2005 中新增的 ROW_NUMBER 函数轻易地达成,且 ROW_NUMBER 函数还能再加上「分群 (PARTITION BY)」等功能,而且执行效能极佳。
图 4 将 Orders 资料表的 830 笔资料都捞出来,并在右侧给一组自动编号

现在我们要如上图 4 般,将 Northwind 中 Orders 资料表的 830 笔资料都捞出来,并自动给一组编号,若用 ROW_NUMBER 函数的写法如下所示,而且效能极佳,只要 2 ms (毫秒),亦即千分之二秒。
SET STATISTICS TIME ON
SELECT OrderID, ROW_NUMBER() OVER(ORDER BY OrderID) AS 编号
FROM dbo.Orders

但如果是在旧版的 SQL Server 2000 中,我们可能得用以下的「子查询」写法:
SET STATISTICS TIME ON
SELECT OrderID,
(SELECT COUNT(*) FROM dbo.Orders AS 内圈
WHERE 内圈.OrderID <= 外圈.OrderID) AS 编号
FROM dbo.Orders AS 外圈
ORDER BY 编号

但这种旧写法,会像先前所提到的,外层查询的「每一次」查询动作都需要引用内层查询的资料。以上方例子而言,外层查询的每一笔资料,都要等内层查询「扫描 整个资料表」并作比对和计数,因此 830 笔资料每一笔都要重复扫描整个资料表 830 次,所耗用的时间也因此爆增至 170 ms。
若您用相同的写法,去查询 AdventureWorks 资料库中,有 31,465 笔资料的 Sales.SalesOrderHeader 资料表,用 ROW_NUMBER 函数要 677 ms,还不到 1 秒钟;但用子查询的话,居然要高达 225,735 ms,将近快 4 分钟的时间。
虽然这是较极端的范例,但由此可知子查询的撰写,在使用上不可不慎,尤其是「关联子查询」。程式员在程式开发初期、资料量还很少时感受不到此种 SQL 语法的重大陷阱;但等到系统上线几个月或一两年后,可能就会有反应迟缓的现象。
------------------------------
7、其他查询技巧
‧ DISTINCT、ORDER BY 语法,会让资料库做额外的计算。此外联集的使用,若没有要剔除重复资料的需求,使用 UNION ALL 会比 UNION 更佳,因为后者会加入类似 DISTINCT 的演算法。
‧ 在 SQL Server 2005 版本中,存取资料库物件时,最好明确指定该物件的「结构描述 (Schema)」,也就是使用两节式名称。否则若呼叫者的预设 Schema 不是 dbo,则 SQL Server 在执行时,会先寻找该使用者预设 Schema 所搭配的物件,找不到的话才会转而使用预设的 dbo,会多耗费寻找的时间。例如若要执行一个叫做 dbo.mySP1 的 Stored Procedure,应使用以下的两节式名称:
EXEC dbo.mySP1

------------------------------
8、尽可能用 Stored Procedure 取代前端应用程式直接存取资料表
Stored Procedure 除了经过事先编译、效能较好以外,亦可节省 SQL 陈述式传递的频宽,也方便商业逻辑的重复使用。再搭配自订函数和 View 的使用,将来若要修改资料表结构、重新切割或反正规化时亦较方便。
------------------------------
9、尽可能在资料来源层,就先过滤资料
使用 SELECT 语法时,尽量避免传回所有的资料至前端而不设定 WHERE 等过滤条件。虽然 ASP.NET 中 SqlDataSource、ObjectDataSource 控制项的 FilterExpression 可再做筛选,GridView 控制项的 SortExpression 可再做排序,但会多消耗掉资料库的系统资源、Web server 的记忆体和网路频宽。最好还是在资料库和资料来源层,就先用 SQL 条件式筛选出所要的资料。
------------------------------

结论:
本文的观念,不管是写 SQL statement、Stored Procedure、自订函数或 View 皆然。本文只是挑出程式员较容易犯的 SQL 语法效能问题,以期能在短时间浏览过本文后,在写 ADO.NET 程式时能修正以往随兴的 SQL 撰写习惯。文中提到的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章