数据库性能优化之SQL语句优化(二)
性能不理想的系统中除了一部分是因为应用程序的负载确实超过了服务器的实际处理能力外,更多的是因为系统存在大量的SQL语句需要优化。为了获得稳定的执行性能,SQL语句越简单越好。对复杂的SQL语句,要设法对之进行简化。常见的简化规则如下:
1)不要有超过5个以上的表连接(JOIN)
2)考虑使用临时表或表变量存放中间结果
3)少用子查询
4)视图嵌套不要过深,一般视图嵌套不要超过2个为宜连接的表越多,其编译的时间和连接的开销也越大,性能越不好控制。最好是把连接拆开成较小的几个部分逐个顺序执行。优先执行那些能够大量减少结果的连接。拆分的好处不仅仅是减少SQL Server优化的时间,更使得SQL语句能够以你可以预测的方式和顺序执行。如果一定需要连接很多表才能得到数据,那么很可能意味着设计上的缺陷。
连接是outer join,非常不好。因为outer join意味着必须对左表或右表查询所有行。如果表很大而没有相应的where语句,那么outer join很容易导致table scan或index scan。要尽量使用inner join避免scan整个表。
优化建议:
1)使用临时表存放t1表的结果,能大大减少logical reads(或返回行数)的操作要优先执行。仔细分析语句,你会发现where中的条件全是针对表t1的,所以直接使用上面的where子句查询表t1,然后把结果存放再临时表#tt1中:
Select t1….. into #tt1 from t1 where…(和上面的where一样)
2)再把#tt1和其他表进行连接:
Select #tt1…
Left outer join …
Left outer join…
3)修改 like 程序,去掉前置百分号。like语句却因为前置百分号而无法使用索引
4)从系统设计的角度修改语句,去掉outer join
5)考虑组合索引或覆盖索引消除clustered index scan
上面1和2点建议立即消除了worktable,性能提高了几倍以上,效果非常明显。
1)限制结果集
要尽量减少返回的结果行,包括行数和字段列数。返回的结果越大,意味着相应的SQL语句的logical reads 就越大,对服务器的性能影响就越甚。
一个很不好的设计就是返回表的所有数据:Select * from tablename
即使表很小也会导致并发问题。更坏的情况是,如果表有上百万行的话,那后果将是灾难性的。它不但可能带来极重的磁盘IO,更有可能把数据库缓冲区中的其他缓存数据挤出,使得这些数据下次必须再从磁盘读取。必须设计良好的SQL语句,使得其有where语句或TOP语句来限制结果集大小。
2)合理的表设计
SQL Server 2005将支持表分区技术。利用表分区技术可以实现数据表的流动窗口功能。在流动窗口中可以轻易的把历史数据移出,把新的数据加入,从而使表的大小基本保持稳定。另外,表的设计未必需要非常范式化。有一定的字段冗余可以增加SQL语句的效率,减少JOIN的数目,提高语句的执行速度。
3)OLAP和OLTP模块要分开
OLAP和OLTP类型的语句是截然不同的。前者往往需要扫描整个表做统计分析,索引对这样的语句几乎没有多少用处。索引只能够加快那些如sum,group by之类的聚合运算。因为这个原因,几乎很难对OLAP类型的SQL语句进行优化。而OLTP语句则只需要访问表的很小一部分数据,而且这些数据往往可以从内存缓存中得到。为了避免OLAP 和OLTP语句相互影响,这两类模块需要分开运行在不同服务器上。
因为OLAP语句几乎都是读取数据,没有更新和写入操作,所以一个好的经验是配置一台standby 服务器,然后OLAP只访问standby服务器。
4)使用存储过程
可以考虑使用存储过程封装那些复杂的SQL语句或商业逻辑,这样做有几个好处。
一、是存储过程的执行计划可以被缓存在内存中较长时间,减少了重新编译的时间。
二、是存储过程减少了客户端和服务器的繁复交互。
三、是如果程序发布后需要做某些改变你可以直接修改存储过程而不用修改程序,避免需要重新安装部署程序。
处理百万级以上的数据提高查询速度的方法:
1. 应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
2. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
3. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
4. 应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20
5. 下面的查询也将导致全表扫描:(不能前置百分号)
select id from t where name like ‘%abc%’
若要提高效率,可以考虑全文检索。
6. in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
select xx,phone FROM send a JOIN (
select '13891030091' phone union select '13992085916' ………… UNION SELECT '13619100234' ) b
on a.Phone=b.phone
--替代下面 很多数据隔开的时候
in('13891030091','13992085916','13619100234'…………)
7. 如果在 where 子句中使用参数,也会导致全表扫描。因为SQL只有在运行时才会解析局部变量,但优化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:
select id from t where num=@num 可以改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
8. 应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2
9. 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)=’abc’–name以abc开头的id
select id from t where datediff(day,createdate,’2005-11-30′)=’2005-11-30′生成的id
应改为:
select id from t where name like ‘abc%’
select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′
10. 不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
11. 在使用索引字段作为条件时,如果该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才能保证系统使用该索引,否则该索引将不会被使用,并且应尽可能的让字段顺序与索引顺序相一致。
12. 不要写一些没有意义的查询,如需要生成一个空表结构:
select col1,col2 into #t from t where 1=0
这类代码不会返回任何结果集,但是会消耗系统资源的,应改成这样:
create table #t(…)
13. 很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)
14. 并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段 sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。
15. 索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
16. 应尽可能的避免更新 clustered 索引数据列,因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
17. 尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
18. 尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
19. 任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。
20. 尽量使用表变量来代替临时表。如果表变量包含大量数据,请注意索引非常有限(只有主键索引)。
21. 避免频繁创建和删除临时表,以减少系统表资源的消耗。
22. 临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使 用导出表。
23. 在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。
24. 如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。
25. 尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。
26. 使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。
27. 与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时 间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。
28. 在所有的存储过程和触发器的开始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在执行存储过程和触发器的每个语句后向客户端发送 DONE_IN_PROC 消息。
29. 尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
30. 尽量避免大事务操作,提高系统并发能力。
31. SQL中GROUP BY的这些使用
1). GROUP BY 功能概述
2). GROUP BY 过滤分组(HAVING)
3). HAVING和WHERE 可同时出现在GROUP BY 里
4). GROUP BY 和ORDER BY 协同使用
1)、GROUP BY 功能概述
GROUP BY的功能是对数据库中的记录按照某种规则或是属性(列)进行分组,即分门别类的过程。比如当前数据库中的表,其表结构字段如下:
//name-姓名 sex-性别 age-年龄 address-住址
CREATE TABLE gptest(id serial primary key, name varchar(20), sex char, age smallint, address varchar(20));
INSERT INTO gptest(name,sex,age,address) VALUES('小明', '男', 26, '杭州'),('小花', '女', 24, '四川'),('小强', '男', 29, '湖南');
INSERT INTO gptest(name,sex,age,address) VALUES('小刚', '男', 19, '贵州'),('小燕', '女', 27, '重庆'),('小燕', '女', 18, '山西');
然后向该表中插入上面的5个记录。
若是以name进行分组,则将会以name为基准,统计该test表中所有的名字,并且过滤掉重复的name,即多个重复的名字只显示一个。因为是分组,那么这个重复的“小燕”-name肯定归属于同一个组(name)中,虽然他们不是同一个人,但是属于同一组(name):
>select name from gptest group by name;
现在以name(姓名)进行分组,并统计每一个组的人数。可以看到name为小燕的组的人数共有两人,从test表记录也可以看到2个名为“小燕”的记录:
>select name,count(*) as name_count from gptest group by name;
对name进行分组并统计每组的人数
GROUP BY 中注意事项
(1)GROUP BY 必须出现在WHERE子句之后,ORDER BY 子句之前;
(2)GROUP BY 语句后面可以包含任意数目的列,但是这些任意数据的列需要和SELECT 后面的列数量(顺序可以不一致)保持一致。
2)、GROUP BY过滤分组(HAVING)
在SQL中,较为频繁的使用WHERE语句来对检索的记录(行)进行过滤。而在使用GROUP BY的时候,就不能使用WHERE来进行分组的过滤了。因为WHERE的过滤功能是针对行,而不是分组。事实上,WHERE是没有分组的概念的。因此,对于分组的过滤,SQL特意提供了另外的一个关键字 HAVING,用以过滤分组的功能。
HAVING和WHERE的区别
(1)WHERE过滤行,HAVING过滤分组;
(2)WHERE在数据分组前进行过滤,HAVING在数据分组之后进行过滤。WHERE排除后的行不会出现在分组之中。
以name进行分组,并且过滤掉name组中总人数小于2的分组。因为当前满足分组大于等于2的分组只有一组(“小燕”)。因此其他组会被过滤掉。如下所示:
>select name,count(*) as name_count from gptest group by name having count(*)>=2;
对name进行分组并过滤掉分组内总数小于2的组
3)、HAVING和WHERE可同时出现在GROUP BY中
HAVING子句和WHERE子句可以同时出现在GROUP BY 语句中。比如现在需要筛选出表中name是“小强”并且分组的总数(聚簇)不小于2的记录信息:
>SELECT name, COUNT(*) AS name_num FROM test WHERE name = '小强' GROUP BY name HAVING COUNT(*)>=2;
对表中name字段进行分组,并同时使用WHERE和HAVING
因为当前test表记录中没有满足name(名字)是“小强”并且分组后该组内人数大于等于2的记录数据,因此其统计结果为0。
4)、GROUP BY和ORDER BY协同使用
GROUP BY会对每个分组中的数据进行聚簇操作。但是输出的数据不保证有序,即按照该分组字段从大到小,或是从小到大的顺序去输出并展示。因此若想对GROUP BY分组后的数据以某种规则(字段-升序/降序),则可以使用ORDER BY来实现该功能。注意:ORDER BY出现在GROUP BY之后,SELECT中各子句的顺序如下图所示。

本文源自:互联网