软件开发规范
1. 目的
为了保证企业编写出的程序都符合相同的规范,保证一致性、统一性而建立的程序编码规范。
编码规范对于程序员而言尤为重要,有以下几个原因:
- 一个软件的生命周期中,80%的花费在于维护
- 几乎没有任何一个软件,在其整个生命周期中,均由最初的开发人员来维护
- 编码规范可以改善软件的可读性,可以让程序员尽快而彻底地理解新的代码
- 如果你将源码作为产品发布,就需要确任它是否被很好的打包并且清晰无误,一如你已构建的其它任何产品
为了执行规范,每个软件开发人员必须一致遵守编码规范。每个人。
2. 范围
适用于企业所有基于C#.NET平台的软件开发工作。
3. 规范内容
3.1. 文件规范
3.1.1. 文件命名
与类名名称相同
3.1.2. 文件内容格式
开头注释
引用
命名空间
类体
变量(代理事件):用#region 变量………#endregion包括
属性:用#region属性………#endregion包括
构造:
方法(事件的实现):用#region方法………#endregion包括
3.2. 代码格式
所有的缩进以Tab代替空格。
在代码中垂直对齐左括号{和右括号}。
if(x==0)
{
Response.Write("用户编号必须输入!");
}
不允许以下情况:
if(x==0) {
Response.Write("用户编号必须输入!");
}
或者:
if(x==0){ Response.Write("用户编号必须输入!");}
为了防止在阅读代码时不得不滚动源代码编辑器,每行代码或注释在1024*800的显示频率下不得超过一显示屏
当一行被分为几行时,通过将串联运算符放在每一行的末尾而不是开头,清楚地表示没有后面的行是不完整的。
每一行上放置的语句避免超过一条,即一条语句一行。
在大多数运算符之前和之后使用空格,这样做时不会改变代码的意图却可以使代码容易阅读。
例:
int j = i + k;
而不应写为
int j=i+k;
将大的复杂代码节分为较小的、易于理解的模块。
编写 SQL 语句时,对于关键字使用全部大写,对于数据库元素(如表、列和视图)使用大写。
将每个主要的 SQL 子句放在不同的行上,这样更容易阅读和编辑语句,例如: SELECT FirstName, LastName
FROM Customers
WHERE State = 'WA'
3.3. 注释(Comment)规范
注释规范包括:模块(类)注释规范、类的属性、方法注释规范、代码间注释
3.3.1. 模块(类)注释规范
模块开始必须以以下形式书写模块注释:
///<summary>
///定义某某类的方法
///</summary>
///<see cref="Author">作者 </see>
///<see cref="Email">email@e-citycard.com </see>
///<see cref="Data">2003-12-17</see>
如果模块有修改,则每次修改必须添加以下注释:
///<remark>备注版本修改信息,修改了那些,时间,每次依次增加remark</remark>
3.3.2. 类属性注释规范
在类的属性必须以以下格式编写属性注释:
/// <summary>
///属性说明
/// </summary>
3.3.3. 方法注释规范
在类的方法声明前必须以以下格式编写注释
/// <summary>
/// 说明:<对该方法的说明>
/// </summary>
/// <param name="<参数名称>"><参数说明></param>
/// <returns> /<对方法返回值的说明,该说明必须明确说明返回的值代表什么含义>
/// </returns>
代码间注释规范
代码间注释分为单行注释和多行注释:
单行注释:
//<单行注释>
多行注释:
/*多行注释1
多行注释2
多行注释3*/
代码中遇到语句块时必须添加注释(if,for,foreach,……),添加的注释必须能够说明此语句块的作用和实现手段(所用算法等等)。
别每行代码,每个声明的变量都做注释
在需要的地方注释。可读性强的代码需要很少的注释。如果所有的变量和方法的命名都很有意义,会使代码可读性很强并无需太多注释。
行数不多的注释会使代码看起来优雅。但如果代码不清晰,可读性差,那就糟糕。
如果因为某种原因使用了复杂艰涩的原理,为程序配备良好的文档和重分的注释。
对一个数值变量采用不是0,-1等的数值初始化,给出选择该值的理由。
简言之,要写清晰,可读的代码以致无须什么注释就能理解。
对注释做拼写检查,保证语法和标点符号的正确使用
3.4. 变量(Variable)命名规范
3.4.1. 程序文件(*.cs)中的变量命名规则
程序中变量名称 = 变量的前缀 +代表变量含意的英文单词或单词缩写
- 不能使用“_”,必须用有意义的,描述性的词语来命名变量。
- 合理使用缩写。不能用nam, addr, sal等代替name, address, salary
- 别使用单个字母的变量象i, n, x 等. 使用 index, temp等 ,用于循环迭代的变量例外:
- 变量名中不使用数字 。
- 命名空间需按照标准的统一的模式命名
- 文件名要和类名匹配,例如,对于类HelloWorld, 相应的类文件名应为 helloworld.cs
- 如果变量只用于迭代计数,没有在循环的其他地方出现,可以使用i, n, x 等 ,而不是另外取名。for ( int i = 0; i < count; i++ ){ ...}
3.4.2. 类模块级的变量以变量类型的缩写作前缀
public class hello
{
private string strName;
private DateTime dtDate;
}
3.4.3. 类的属性所对应的变量,采用属性名前加类型的缩写作前缀前缀的形式
public class hello
{
private string s_Name;
public string Name
{
get
{
return m_Name;
}
}
}
3.4.4. 过程级的变量不使用前缀
public class hello
{
void say()
{
string SayWord;
}
}
3.4.5. 过程的参数使用“p”作为参数前缀
public class hello
{
void say(string pSayWord)
{
}
}
补充说明:
针对异常捕获过程中的Exception变量命名,在没有冲突的情况下,统一命名为e;
如果有冲突的情况下,可以重复e,比如:ee。
Try
{
//your code
try
{
//code
}
catch(Exception ee)
{
//your code
}
}
catch(Exception e)
{
//your code
}
补充:如果捕获异常不需要作任何处理,则不需要定义Exception实例
例:
try
{
//your code
}
catch( Exception )
{
}
3.4.6. 鉴于大多数名称都是通过连接若干单词构造的,请使用大小写混合的格式以简化它们的阅读。每个单词的第一个字母都是大写.
3.4.7. 即使对于可能仅出现在几个代码行中的生存期很短的变量,仍然使用有意义的名称。仅对于短循环索引使用单字母变量名,如 i 或 j。
3.4.8. 在变量名中使用互补对,如 min/max、begin/end 和 open/close。
3.4.9. 别在程序中使用固定数值,用常量代替。不要使用原义数字或原义字符串,如 for( i = 1,i< 7;i++)。而是使用命名常数,如 for( i = 1,i< NUM_DAYS_IN_WEEK ; i++) 以便于维护和理解。
3.4.10.使用C# 或 VB.NET的特有类型,而不是System命名空间中定义的别名类型,好: int age; string name; object contactInfo; 不好: Int16 age; String name; Object contactInfo
3.5. 控件命名规则
控件命名=Web控件缩写前缀 + “_” +变量名
控件
缩写
Label
lbl
TextBox
txt
CheckBox
ckb
Button
cmd
ListBox
lst
DropDownList
drp
等等
3.6. 常量命名规范
常量名也应当有一定的意义,格式为 NOUN 或 NOUN_VERB。常量名均为大写,字之间用下划线分隔。
例:
private const bool WEB_ENABLEPAGECACHE_DEFAULT = true;
private const int WEB_PAGECACHEEXPIRESINSECONDS_DEFAULT = 3600;
private const bool WEB_ENABLESSL_DEFAULT = false;
注:
变量名和常量名最多可以包含 255 个字符,但是,超过 25 到 30 个字符的名称比较笨拙。此外,要想取一个有实际意义的名称,清楚地表达变量或常量的用途,25 或 30 个字符应当足够了。
3.7. 类(Class)命名规范
1. 名字应该能够标识事物的特性。
2. 名字尽量不使用缩写,除非它是众所周知的。
3. 名字可以有两个或三个单词组成,但通常不应多于三个。
4. 在名字中,所有单词第一个字母大写,缩写都要大写。
例如 IsSuperUser,包含ID的,ID全部大写,如CustomerID。
5. 使用名词或名词短语命名类。
6. 少用缩写。
7. 不要使用下划线字符 (_)。
例:
public class FileStream
public class Button
public class String
3.8. 接口(Interface)命名规范
和类命名规范相同,唯一区别是 接口在名字前加上“I”前缀
例:
interface IDBCommand;
interface IButton;
3.9. 方法(Method)命名规范
和类命名规范相同。
另:方法开头尽量使用e文动词表示,
如:GetMoney , SetMoney
3.4.7 命名空间(NameSpace)命名规范
方法的命名.一般将其命名为动宾短语.
ShowDialog()
CreateFile()
GetPath()
3.10. 异常处理
A. 不必在所有方法中捕捉一般异常。不管它,让程序崩溃。这将帮助你在开发周期发现大多数的错误
B. 不必每个方法都用try-catch。当特定的异常可能发生时才使用。比如,当你写文件时,处理异常FileIOException
C. 别写太大的 try-catch 模块。如果需要,为每个执行的任务编写单独的 try-catch 模块。 这将帮你找出哪一段代码产生异常,并给用户发出特定的错误消息
D. 不要“捕捉了异常却什么也不做“,除非有特殊需求。如果隐藏了一个异常,你将永远不知道异常到底发生了没有
E. 错误消息需能帮助用户解决问题。永远别用象"应用程序出错", "发现一个错误" 等错误消息。而应给出象 "更新数据库失败。请确保登陆id和密码正确。" 的具体消息
F. 显示错误消息时,除了说哪里错了,还应提示用户如何解决问题。不要用 象 "更新数据库失败。"这样的,要提示用户怎么做:"更新数据库失败。请确保登陆id和密码正确。
G. 显示给用户的消息要简短而友好。但要把所有可能的信息都记录下来,以助诊断问题
3.11. 良好的编程习惯需注意以下几点:
1) 别用字符串常数。用资源文件。
2) 别把成员变量声明为 public 或 protected。都声明为 private 而使用 public/protected 的Properties
3) 不在代码中使用具体的路径和驱动器名。 使用相对路径,并使路径可编程。
4) 永远别设想你的代码是在“C:”盘运行。你不会知道,一些用户在网络或“Z:”盘运行程序。
5) 应用程序启动时作些“自检”并确保所需文件和附件在指定的位置。必要时检查数据库连接。出现任何问题给用户一个友好的提示。
6) 如果需要的配置文件找不到,应用程序需能自己创建使用默认值的一份。
7) 如果在配置文件中发现错误值,应用程序要抛出错误,给出提示消息告诉用户正确值。
8) 所有的成员变量声明在类的顶端,用一个换行把它和方法分开
9) 建议局部变量在最接近使用它时再声明
10) 使用某个控件的值时,尽量命名局部变量
11) 把引用的系统的namespace和自定义或第三方的分开
12) 目录结构中要反应出namespace的层次
13) 大括号"{"一定要新起一行
14) 尽量不要手工更改机器生成的代码,若必须更改,一定要改成和机器生成的代码风格一样
15) 文字常量和数字常量不要硬编码,应该用常量类或枚举代替
16) 不要用goto系列语句
17) 不要声明public的event,应用事件访问器
18) 生成和构建一个长的字符串时,一定要使用StringBuilder,而不用string
19) 始终使用"{ }"包含if下的语句,即使只有一条语句
20) switch语句一定要有default来处理意外情况
21) 尽量不用使用this引用,除非是要调用类中的另一个Constructor
public class Person
{
public Person(string name)
{
}
public Person() : this("Jim")
{
}
}
4.
5.
1. 书写格式
示例代码:
存储过程SQL文书写格式例
select
c.dealerCode,
round(sum(c.submitSubletAmountDLR + c.submitPartsAmountDLR + c.submitLaborAmountDLR) / count(*), 2) as avg,
decode(null, 'x', 'xx', 'CNY')
from (
select
a.dealerCode,
a.submitSubletAmountDLR,
a.submitPartsAmountDLR,
a.submitLaborAmountDLR
from SRV_TWC_F a
where (to_char(a.ORIGSUBMITTIME, 'yyyy/mm/dd') >= 'Date Range(start)'
and to_char(a.ORIGSUBMITTIME, 'yyyy/mm/dd') <= 'Date Range(end)'
and nvl(a.deleteflag, '0') <> '1')
union all
select
b.dealerCode,
b.submitSubletAmountDLR,
b.submitPartsAmountDLR,
b.submitLaborAmountDLR
from SRV_TWCHistory_F b
where (to_char(b.ORIGSUBMITTIME, 'yyyy/mm/dd') >= 'Date Range(start)'
and to_char(b.ORIGSUBMITTIME,'yyyy/mm/dd') <= 'Date Range(end)'
and nvl(b.deleteflag,'0') <> '1')
) c
group by c.dealerCode
order by avg desc;
C#中里的SQL字符串书写格式例
strSQL = "insert into Snd_FinanceHistory_Tb " +
"(DEALERCODE, " +
"REQUESTSEQUECE, " +
"HANDLETIME, " +
"JOBFLAG, " +
"FRAMENO, " +
"INMONEY, " +
"REMAINMONEY, " +
"DELETEFLAG, " +
"UPDATECOUNT, " +
"CREUSER, " +
"CREDATE, " +
"HONORCHECKNO, " +
"SEQ) " +
"values ('" + draftInputDetail.dealerCode + "', " +
"'" + draftInputDetail.requestsequece + "', " +
"sysdate, " +
"'07', " +
"'" + frameNO + "', " +
requestMoney + ", " +
remainMoney + ", " +
"'0', " +
"0, " +
"'" + draftStruct.employeeCode + "', " +
"sysdate, " +
"'" + draftInputDetail.honorCheckNo + "', " +
index + ")";
1.1. .缩进
对于存储过程文件,缩进为1个TAB
对于C#里的SQL字符串,不可有缩进,即每一行字符串不可以空格开头
1.2. 换行
1>.Select/From/Where/Order by/Group by等子句必须另其一行写
2>.Select子句内容如果只有一项,与Select同行写
3>.Select子句内容如果多于一项,每一项单独占一行,在对应Select的基础上向右缩进1个TAB(C#无缩进)
4>.From子句内容如果只有一项,与From同行写
5>.From子句内容如果多于一项,每一项单独占一行,在对应From的基础上向右缩进1个TAB(C#无缩进)
6>.Where子句的条件如果有多项,每一个条件占一行,以AND开头,且无缩进
7>.(Update)Set子句内容每一项单独占一行,无缩进
8>.Insert子句内容每个表字段单独占一行,无缩进;values每一项单独占一行,无缩进
9>.SQL文中间不允许出现空行
10>.C#里单引号必须跟所属的SQL子句处在同一行,连接符("+")必须在行尾
1.3. 空格
1>.SQL内算数运算符、逻辑运算符连接的两个元素之间必须用空格分隔
2>.逗号之后必须接一个空格
3>.关键字、保留字和左括号之间必须有一个空格
2. .不等于统一使用"<>"。虽然 SQLServer认为"!="和"<>"是等价的,都代表不等于的意义。为了统一,不等于一律使用"<>"表示
3. 使用表的别名, 数据库查询,必须使用表的别名。
4. SQL文对表字段扩展的兼容性。在C#里使用Select *时,严禁通过getString(1)的形式得到查询结果,必须使用getString("字段名")的形式;使用Insert时,必须指定插入的字段名,严禁不指定字段名直接插入values
5. 减少子查询的使用。子查询除了可读性差之外,还在一定程度上影响了SQL运行效率,请尽量减少使用子查询的使用,用其他效率更高、可读性更好的方式替代。
6. 适当添加索引以提高查询效率,适当添加索引可以大幅度的提高检索速度,请参看SQLSERVER SQL性能优化系列。
7. 对数据库表操作的特殊要求
本规范对数据库表的操作还有以下特殊要求:
1).以逻辑删除替代物理删除
注意:现在数据库表中数据没有物理删除,只有逻辑删除
以deleteflag字段作为删除标志,deleteflag='1'代表此记录被逻辑删除,因此在查询数据时必须考虑deleteflag的因素,deleteflag的标准查询条件:NVL(deleteflag, '0') <> '1'
2).增加记录状态字段
数据库中的每张表基本都有以下字段:INITIAL_DT、INITIAL_USR、REC_UPDATE_DT、REC_UPDATE_USR
要注意在对标进行操作时必须考虑以下字段
插入一条记录时要置INITIAL_DT =sysdate, INITIAL_USR =登录User;修改一条记录时要置REC_UPDATE_DT =sysdate, REC_UPDATE_USR =登录User,
8. 用执行计划分析SQL性能
EXPLAIN PLAN是一个很好的分析SQL语句的工具,它可以在不执行SQL的情况下分析语句。通过分析,我们就可以知道SQLSERVER是怎样连接表,使用什么方式扫描表(索引扫描或全表扫描),以及使用到的索引名称,按照从里到外,从上到下的次序解读分析的结果,EXPLAIN PLAN的分析结果是用缩进的格式排列的,最内部的操作将最先被解读,如果两个操作处于同一层中,带有最小操作号的将首先被执行。目前许多第三方的工具如PLSQL Developer和TOAD等都提供了极其方便的EXPLAIN PLAN工具,PG需要将自己添加的查询SQL文记入log,然后在EXPLAIN PLAN中进行分析,尽量减少全表扫描,
9. SQLSERVER SQL性能优化系列
9.1. 选择最有效率的表名顺序(只在基于规则的优化器中有效)
SQLSERVER的解析器按照从右到左的顺序处理FROM子句中的表名,因此FROM子句中写在最后的表(基础表driving table)将被最先处理,在FROM子句中包含多个表的情况下,必须选择记录条数最少的表作为基础表,当SQLSERVER处理多个表时,会运用排序及合并的方式连接它们,
首先,扫描第一个表(FROM子句中最后的那个表)并对记录进行排序;然后扫描第二个表(FROM子句中最后第二个表);最后将所有从第二个表中检索出的记录与第一个表中合适记录进行合并
例如: 表 TAB1 16,384 条记录表 TAB2 5 条记录,选择TAB2作为基础表 (最好的方法) select count(*) from tab1,tab2 执行时间0.96秒,选择TAB2作为基础表 (不佳的方法) select count(*) from tab2,tab1 执行时间26.09秒;
如果有3个以上的表连接查询,那就需要选择交叉表(intersection table)作为基础表,交叉表是指那个被其他表所引用的表
例如:
EMP表描述了LOCATION表和CATEGORY表的交集
SELECT *
FROM LOCATION L,
CATEGORY C,
EMP E
WHERE E.EMP_NO BETWEEN 1000 AND 2000
AND E.CAT_NO = C.CAT_NO
AND E.LOCN = L.LOCN
将比下列SQL更有效率
SELECT *
FROM EMP E ,
LOCATION L ,
CATEGORY C
WHERE E.CAT_NO = C.CAT_NO
AND E.LOCN = L.LOCN
AND E.EMP_NO BETWEEN 1000 AND 2000
9.2. WHERE子句中的连接顺序
SQLSERVER采用自下而上的顺序解析WHERE子句,根据这个原理,表之间的连接必须写在其他WHERE条件之前,那些可以过滤掉最大数量记录的条件必须写在WHERE子句的末尾
例如:
(低效,执行时间156.3秒)
SELECT *
FROM EMP E
WHERE SAL > 50000
AND JOB = 'MANAGER'
AND 25 < (SELECT COUNT(*) FROM EMP WHERE MGR=E.EMPNO);
(高效,执行时间10.6秒)
SELECT *
FROM EMP E
WHERE 25 < (SELECT COUNT(*) FROM EMP WHERE MGR=E.EMPNO)
AND SAL > 50000
AND JOB = 'MANAGER';
9.3. SELECT子句中避免使用'*'。当你想在SELECT子句中列出所有的COLUMN时,使用动态SQL列引用'*'是一个方便的方法,不幸的是,这是一个非常低效的方法。实际上,SQLSERVER在解析的过程中,会将'*'依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间
9.4. 减少访问数据库的次数。当执行每条SQL语句时,SQLSERVER在内部执行了许多工作:解析SQL语句,估算索引的利用率,绑定变量,读数据块等等
由此可见,减少访问数据库的次数,就能实际上减少SQLSERVER的工作量,例如:
以下有三种方法可以检索出雇员号等于0342或0291的职员
方法1 (最低效)
SELECT EMP_NAME, SALARY, GRADE
FROM EMP
WHERE EMP_NO = 342;
SELECT EMP_NAME, SALARY, GRADE
FROM EMP
WHERE EMP_NO = 291;
方法2 (次低效)
DECLARE
CURSOR C1 (E_NO NUMBER) IS
SELECT EMP_NAME,SALARY,GRADE
FROM EMP
WHERE EMP_NO = E_NO;
BEGIN
OPEN C1(342);
FETCH C1 INTO …,…,…;
…
OPEN C1(291);
FETCH C1 INTO …,…,…;
…
CLOSE C1;
END;
方法2 (高效)
SELECT A.EMP_NAME, A.SALARY, A.GRADE,
B.EMP_NAME, B.SALARY, B.GRADE
FROM EMP A, EMP B
WHERE A.EMP_NO = 342
AND B.EMP_NO = 291;
9.5. 使用DECODE函数来减少处理时间
使用DECODE函数可以避免重复扫描相同记录或重复连接相同的表
例如:
SELECT COUNT(*), SUM(SAL)
FROM EMP
WHERE DEPT_NO = '0020'
AND ENAME LIKE 'SMITH%';
SELECT COUNT(*), SUM(SAL)
FROM EMP
WHERE DEPT_NO = '0030'
AND ENAME LIKE 'SMITH%';
你可以用DECODE函数高效地得到相同结果
SELECT COUNT(DECODE(DEPT_NO, '0020', 'X', NULL)) D0020_COUNT,
COUNT(DECODE(DEPT_NO, '0030', 'X', NULL)) D0030_COUNT,
SUM(DECODE(DEPT_NO, '0020', SAL, NULL)) D0020_SAL,
SUM(DECODE(DEPT_NO, 0030, SAL, NULL)) D0030_SAL
FROM EMP
WHERE ENAME LIKE 'SMITH%';
'X'表示任何一个字段
类似的,DECODE函数也可以运用于GROUP BY和ORDER BY子句中
9.6. 用Where子句替换HAVING子句
避免使用HAVING子句,HAVING只会在检索出所有记录之后才对结果集进行过滤,这个处理需要排序、统计等操作
如果能通过WHERE子句限制记录的数目,那就能减少这方面的开销
例如:
低效
SELECT REGION, AVG(LOG_SIZE)
FROM LOCATION
GROUP BY REGION
HAVING REGION REGION != 'SYDNEY'
AND REGION != 'PERTH'
高效
SELECT REGION, AVG(LOG_SIZE)
FROM LOCATION
WHERE REGION REGION != 'SYDNEY'
AND REGION != 'PERTH'
GROUP BY REGION
9.7. 减少对表的查询
在含有子查询的SQL语句中,要特别注意减少对表的查询
例如:
低效
SELECT TAB_NAME
FROM TABLES
WHERE TAB_NAME = (SELECT TAB_NAME
FROM TAB_COLUMNS
WHERE VERSION = 604)
AND DB_VER = (SELECT DB_VER
FROM TAB_COLUMNS
WHERE VERSION = 604)
高效
SELECT TAB_NAME
FROM TABLES
WHERE (TAB_NAME, DB_VER) = (SELECT TAB_NAME, DB_VER
FROM TAB_COLUMNS
WHERE VERSION = 604)
Update多个Column例子:
低效
UPDATE EMP
SET EMP_CAT = (SELECT MAX(CATEGORY)
FROM EMP_CATEGORIES),
SAL_RANGE = (SELECT MAX(SAL_RANGE)
FROM EMP_CATEGORIES)
WHERE EMP_DEPT = 0020;
高效
UPDATE EMP
SET (EMP_CAT, SAL_RANGE) = (SELECT MAX(CATEGORY), MAX(SAL_RANGE)
FROM EMP_CATEGORIES)
WHERE EMP_DEPT = 0020;
9.8. 使用表的别名(Alias),当在SQL语句中连接多个表时,请使用表的别名并把别名前缀于每个Column上,这样可以减少解析的时间并减少那些由Column歧义引起的语法错误
9.9. 用EXISTS替代IN
在许多基于基础表的查询中,为了满足一个条件,往往需要对另一个表进行联接
在这种情况下,使用EXISTS(或NOT EXISTS)通常将提高查询的效率
低效
SELECT *
FROM EMP (基础表)
WHERE EMPNO > 0
AND DEPTNO IN (SELECT DEPTNO
FROM DEPT
WHERE LOC = 'MELB')
高效
SELECT *
FROM EMP (基础表)
WHERE EMPNO > 0
AND EXISTS (SELECT 'X'
FROM DEPT
WHERE DEPT.DEPTNO = EMP.DEPTNO
AND LOC = 'MELB')
9.10. 用NOT EXISTS替代NOT IN
在子查询中,NOT IN子句将执行一个内部的排序和合并
无论在哪种情况下,NOT IN都是最低效的,因为它对子查询中的表执行了一个全表遍历
为了避免使用NOT IN,我们可以把它改写成外连接(Outer Joins)或NOT EXISTS
例如:
SELECT …
FROM EMP
WHERE DEPT_NO NOT IN (SELECT DEPT_NO
FROM DEPT
WHERE DEPT_CAT = 'A');
为了提高效率改写为
高效
SELECT …
FROM EMP A, DEPT B
WHERE A.DEPT_NO = B.DEPT(+)
AND B.DEPT_NO IS NULL
AND B.DEPT_CAT(+) = 'A'
最高效
SELECT …
FROM EMP E
WHERE NOT EXISTS (SELECT 'X'
FROM DEPT D
WHERE D.DEPT_NO = E.DEPT_NO
AND DEPT_CAT = 'A');
9.11. 用表连接替换EXISTS
通常来说,采用表连接的方式比EXISTS更有效率
例如:
SELECT ENAME
FROM EMP E
WHERE EXISTS (SELECT 'X'
FROM DEPT
WHERE DEPT_NO = E.DEPT_NO
AND DEPT_CAT = 'A');
更高效
SELECT ENAME
FROM DEPT D, EMP E
WHERE E.DEPT_NO = D.DEPT_NO
AND DEPT_CAT = 'A';
9.12. 用EXISTS替换DISTINCT
当提交一个包含多表信息(比如部门表和雇员表)的查询时,避免在SELECT子句中使用DISTINCT,一般可以考虑用EXIST替换
例如:
低效
SELECT DISTINCT DEPT_NO, DEPT_NAME
FROM DEPT D, EMP E
WHERE D.DEPT_NO = E.DEPT_NO
高效
SELECT DEPT_NO, DEPT_NAME
FROM DEPT D
WHERE EXISTS (SELECT 'X'
FROM EMP E
WHERE E.DEPT_NO = D.DEPT_NO);
EXISTS使查询更为迅速,因为RDBMS核心模块将在子查询的条件一旦满足后,立刻返回结果
9.13. 用索引提高效率
索引是表的一个概念部分,用来提高检索数据的效率。实际上,SQLSERVER使用了一个复杂的自平衡B-tree结构
通常,通过索引查询数据比全表扫描要快。当SQLSERVER找出执行查询和Update语句的最佳路径时,SQLSERVER优化器将使用索引
同样,在联结多个表时使用索引也可以提高效率。另一个使用索引的好处是,它提供了主键(primary key)的唯一性验证
除了那些LONG或LONG RAW数据类型,你可以索引几乎所有的列
通常在大型表中使用索引特别有效,当然,在扫描小表时,使用索引同样能提高效率
虽然使用索引能得到查询效率的提高,但是我们也必须注意到它的代价
索引需要空间来存储,也需要定期维护,每当有记录在表中增减或索引列被修改时,索引本身也会被修改
这意味着每条记录的INSERT、DELETE、UPDATE将为此多付出4、5次的磁盘I/O
因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢
SQLSERVER对索引有两种访问模式:
9.13.1..索引唯一扫描(INDEX UNIQUE SCAN)
大多数情况下, 优化器通过WHERE子句访问INDEX
例如:
表LODGING有两个索引:建立在LODGING列上的唯一性索引LODGING_PK和建立在MANAGER列上的非唯一性索引LODGING$MANAGER
SELECT *
FROM LODGING
WHERE LODGING = 'ROSE HILL';
在内部,上述SQL将被分成两步执行:
首先,LODGING_PK索引将通过索引唯一扫描的方式被访问,获得相对应的ROWID;然后通过ROWID访问表的方式执行下一步检索
如果被检索返回的列包括在INDEX列中,SQLSERVER将不执行第二步的处理(通过ROWID访问表)
因为检索数据保存在索引中,单单访问索引就可以完全满足查询结果
9.13.2.索引范围查询(INDEX RANGE SCAN)
适用于两种情况:
1>.基于唯一性索引的一个范围的检索
2>.基于非唯一性索引的检索
例1
SELECT LODGING
FROM LODGING
WHERE LODGING LIKE 'M%';
WHERE子句条件包括一系列值,SQLSERVER将通过索引范围查询的方式查询LODGING_PK
由于索引范围查询将返回一组值,它的效率就要比索引唯一扫描低一些
例2
SELECT LODGING
FROM LODGING
WHERE MANAGER = 'BILL GATES';
这个SQL的执行分两步,LODGING$MANAGER的索引范围查询(得到所有符合条件记录的ROWID),通过ROWID访问表得到LODGING列的值
由于LODGING$MANAGER是一个非唯一性的索引,数据库不能对它执行索引唯一扫描
WHERE子句中,如果索引列所对应的值的第一个字符由通配符(WILDCARD)开始,索引将不被采用
SELECT LODGING
FROM LODGING
WHERE MANAGER LIKE '%HANMAN';
在这种情况下,SQLSERVER将使用全表扫描
9.14. 避免在索引列上使用计算
WHERE子句中,如果索引列是函数的一部分,优化器将不使用索引而使用全表扫描
例如:
低效
SELECT …
FROM DEPT
WHERE SAL * 12 > 25000;
高效
SELECT …
FROM DEPT
WHERE SAL > 25000/12;
请务必注意,检索中不要对索引列进行处理,如:TRIM,TO_DATE,类型转换等操作,破坏索引,使用全表扫描,影响SQL执行效率
9.15. 避免在索引列上使用IS NULL和IS NOT NULL
避免在索引中使用任何可以为空的列,SQLSERVER将无法使用该索引
对于单列索引,如果列包含空值,索引中将不存在此记录;
对于复合索引,如果每个列都为空,索引中同样不存在此记录。如果至少有一个列不为空,则记录存在于索引中
如果唯一性索引建立在表的A列和B列上,并且表中存在一条记录的A,B值为(123,null),
SQLSERVER将不接受下一条具有相同A,B值(123,null)的记录插入
如果所有的索引列都为空,SQLSERVER将认为整个键值为空,而空不可能等于空,因此你可以插入1000条具有相同键值的记录,当然它们都是空!
因为空值不存在于索引列中,所以WHERE子句中对索引列进行空值比较将使SQLSERVER停用该索引
低效(索引失效)
SELECT …
FROM DEPARTMENT
WHERE DEPT_CODE IS NOT NULL
9.16. 使用UNION-ALL和UNION
当SQL语句需要UNION两个查询结果集合时,这两个结果集合会以UNION-ALL的方式被合并,然后在输出最终结果前进行排序
如果用UNION ALL替代UNION,这样排序就不是必要了,效率就会因此得到提高
需要注意的是,UNION ALL将重复输出两个结果集合中相同记录,因此还是要从业务需求分析使用UNION ALL的可行性
关于索引下列经验请参考:
1).如果检索数据量超过30%的表中记录数,使用索引将没有显著的效率提高
2).在特定情况下,使用索引也许会比全表扫描慢,但这是同一个数量级上的差距;而通常情况下,使用索引比全表扫描要快几倍乃至几千倍!