Effective SQL

Posted on 2007-01-12 12:56 weibogao 阅读(973) 评论(2)  编辑  收藏 所属分类: database development

一.名词解释:
0。SQL 结构化查询语言(Structured Query Language)

1。非关系型数据库系统
做为第一代数据库系统的总称,其包括2种类型:“层次”数据库与“网状”数据库

“层次”数据库管理系统 eg:IBM&IMS (Information  Management System)
特点:数据按层次模型组织

"网状"数据库
特点:数据按网状模型组织

2。关系型数据库系统
关系性数据库管理系统 (RDBMS)
eg:SQL/DS , DB2, Oracle ,Informix ,Unity,dBASE等
特点:数据按二维的表格组织。

3。数据库(DataBase)
按一定结构存储在计算机中相互关联的数据的集合。

4。数据库管理系统DBMS(Database Management System)
一个通用的软件系统。就是让你怎么管理你的数据库。其中包括存储,安全,完整性管理
等。

5。数据库应用系统DBAS (Database Application System)
数据库应用程序系统,建立在DBMS基础之上的。就是一个面向用户的软件系统。

6。ANSI标准 (American National Standards Institute)美国国家标准委员会
因为1999年第2次更新SQL,所以SQL又称为SQL99或SQL3(第3版,前2个版本分别为1986年
的sql ,1992 年的sql2/sql92)。

7。SQL语句的3种类型
数据操作语句(Data Manipulation Language ) DML 关于数据操作命令的  eg:select,in
sert,update,delete
数据定义语句(Data Definition Language ) DDL     关于数据对象访问的  eg:create,
drop
数据控制语句(Data Control Language) DCL         关于权限的  eg:grant ,revoke


8。PL/SQL Procedural Language/sql
用于oracle的语言

9.T-SQL  transact-sql
用于 microsoft sql server 和sybase adaptive server

10。E.F.Codd关于关系型数据库12条检验原则(MYSQL,不支持视图和原子事物处理,所以
排除)
内容:暂略

11。数据库设计之新奥尔良方法。
需求分析==》概念设计==》逻辑设计==》物理设计.
4个步骤的具体中以需求分析最重要.
需求分析的内容:暂略
概念设计的内容:暂略
逻辑设计的内容:暂略
物理设计的内容:暂略


二.数据库优化方案
1.索引
一 概述

  可以利用索引快速访问数据库表中的特定信息。索引是对数据库表中一个或多个列的值
进行排序的结构。
  索引提供指针以指向存储在表中指定列的数据值,然后根据指定的排序次序排列这些指
针。
  数据库使用索引的方式与使用书的目录很相似:通过搜索索引找到特定的值,
  然后跟随指针到达包含该值的行

索引是一个单独的、物理的数据库结构,它是某个表中一列或若干列值的集合和相应的指
向表中物理标识这些值的数据页的逻辑指针清单。

一个表的存储是由两部分组成的,一部分用来存放表的数据页面,另一部分存放索引页面
。索引就存放在索引页面上

二 索引的两种类型:

聚集索引=簇集索引

聚集索引基于数据行的键值在表内排序和存储这些数据行。由于数据行按基于聚集索引键
的排序次序存储,
因此聚集索引对查找行很有效。每个表只能有一个聚集索引,因为数据行本身只能按一个
顺序存储。
数据行本身构成聚集索引的最低级别。

只有当表包含聚集索引时,表内的数据行才按排序次序存储。如果表没有聚集索引,
则其数据行按堆集方式存储。

聚集索引对于那些经常要搜索范围值的列特别有效。使用聚集索引找到包含第一个值的行
后,
便可以确保包含后续索引值的行在物理相邻。例如,如果应用程序执行的一个查询经常检
索某一日期范围
内的记录,则使用聚集索引可以迅速找到包含开始日期的行,然后检索表中所有相邻的行

直到到达结束日期。这样有助于提高此类查询的性能。同样,如果对从表中检索的数据进
行排序时
经常要用到某一列,则可以将该表在该列上聚集(物理排序),避免每次查询该列时都进
行排序,
从而节省成本

非聚集索引

非聚集索引具有完全独立于数据行的结构。非聚集索引的最低行包含非聚集索引的键值,

并且每个键值项都有指针指向包含该键值的数据行。数据行不按基于非聚集键的次序存储


在非聚集索引内,从索引行指向数据行的指针称为行定位器。
行定位器的结构取决于数据页的存储方式是堆集还是聚集。对于堆集,行定位器是指向行
的指针。
对于有聚集索引的表,行定位器是聚集索引键。
只有在表上创建了聚集索引时,表内的行才按特定的顺序存储。这些行就基于聚集索引键
按顺序存储。
如果一个表只有非聚集索引,它的数据行将按无序的堆集方式存储
非聚集索引可以建多个,两者都能改善查询性能

非聚集索引与聚集索引一样有 B 树结构,但是有两个重大差别:
数据行不按非聚集索引键的顺序排序和存储。
非聚集索引的叶层不包含数据页。
相反,叶节点包含索引行。每个索引行包含非聚集键值以及一个或多个行定位器,
这些行定位器指向有该键值的数据行(如果索引不唯一,则可能是多行)。
非聚集索引可以在有聚集索引的表、堆集或索引视图上定义


聚集索引-->顺序表结构.其物理数据和逻辑排序紧邻.
非聚集索引-->单链表结构.起物理和逻辑排序不按顺序排列.

打个比方.
一本字典,你现在查一个陈字.你有2种方法.首先,你在知道他念chen的情况下去按照拼音字
母去查找.他是排在字母A,B
于是你很容易的就找到"陈"字.第2种方法则是按编旁查找,先找到耳朵旁,去找到一个临时
的编旁表在去找"东"这个字,然后按照给出的
页数找到相应的位置.
显然,第一种方法就是聚集索引,按照物理位置根据排序来查找.
第2种方法则是非聚集索引,按照一个临时索引来查找.

另外
唯一索引

唯一索引可以确保索引列不包含重复的值。在多列唯一索引的情况下,该索引可以确保索
引列中每个值组
合都是唯一的。唯一索引既是索引也是约束。

复合索引
索引项是多个的就叫组合索引,也叫复合索引。复合索引使用时需要注意索引项的次序。


二 索引的创建

有两种方法可以在 SQL Server 内定义索引: CREATE INDEX 语句和CREATE TABLE 语句


CREATE TABLE支持在创建索引时使用下列约束:

PRIMARY KEY 创建唯一索引来强制执行主键
UNIQUE 创建唯一索引
CLUSTERED 创建聚集索引
NONCLUSTERED 创建非聚集索引

注: 1 定义索引时,可以指定每列的数据是按升序还是降序存储。如果不指定,则默认为
升序
   2 支持在计算列上创建索引
   3 为索引指定填充因子
     可标识填充因子来指定每个索引页的填满程度。索引页上的空余空间量很重要,
     因为当索引页填满时,系统必须花时间拆分它以便为新行腾出空间。


三 索引的维护语句

DBCC DBREINDEX    重建指定数据库中表的一个或多个索引
DBCC INDEXFRAG  整理指定的表或视图的聚集索引和辅助索引碎片

比较

            速度    兼容性     日志影响      数据访问影响       额外磁盘空间

DBCC        最快      最好     大,但能通过把   操作过程中数据不   需要大
DBREINDEX             可以重   故障还原模型设  能访问,影响大
                     建所有   为简单减少日志   
                     有索引

DBCC        慢       但可   必须分   小              数据未被锁定        需要小

INDEXDEFRAG          随时终 别指定
                    止执行  
                              

drop index    中等  必须分   大,但能通过把    仅在操作执行时    中等,操作在 
 
create index        别指定   故障还原模型设   锁定数据          tempdb中进行

                            为简单减少日志


四 查看索引的方法

sp_indexes        返回指定远程表的索引信息
INDEXKEY_PROPERTY 返回有关索引键的信息
sysindexes系统表  数据库中的每个索引和表在表中各占一行,该表存储在每个数据库中



五 可以通过执行计划
  查看sql语句执行时是否建立在索引之上

比如
CREATE TABLE Test
(Field_1 int NOT NULL,
Field_2 int CONSTRAINT PK_Test
PRIMARY KEY CLUSTERED (Field_1))

CREATE index IX_Test ON Test (Field_2)

1 SELECT * FROM Test WHERE Field_2 =408
执行计划可以看出使用了IX_Test索引
2 SELECT * FROM Test WHERE Field_1 =1
执行计划可以看出使用了PK_Test
3 但如果是SELECT * FROM Test with (index(IX_Test)) WHERE Field_1 =1
则指定使用索引


六 索引的具体使用

1) 索引的设计
A:尽量避免表扫描
检查你的查询语句的where子句,因为这是优化器重要关注的地方。包含在where里面的每
一列(column)都是可能的侯选索引,为能达到最优的性能,考虑在下面给出的例子:对于
在where子句中给出了column1这个列。
下面的两个条件可以提高索引的优化查询性能!
第一:在表中的column1列上有一个单索引
第二:在表中有多索引,但是column1是第一个索引的列
避免定义多索引而column1是第二个或后面的索引,这样的索引不能优化服务器性能
例如:下面的例子用了pubs数据库。
SELECT au_id, au_lname, au_fname FROM authors
WHERE au_lname = ’White’
按下面几个列上建立的索引将会是对优化器有用的索引
?au_lname
?au_lname, au_fname
而在下面几个列上建立的索引将不会对优化器起到好的作用
?au_address
?au_fname, au_lname
考虑使用窄的索引在一个或两个列上,窄索引比多索引和复合索引更能有效。用窄的索引
,在每一页上
将会有更多的行和更少的索引级别(相对与多索引和复合索引而言),这将推进系统性能

对于多列索引,SQL Server维持一个在所有列的索引上的密度统计(用于联合)和在第一
个索引上的
histogram(柱状图)统计。根据统计结果,如果在复合索引上的第一个索引很少被选择使
用,那么优化器对很多查询请求将不会使用索引。
有用的索引会提高select语句的性能,包括insert,uodate,delete。
但是,由于改变一个表的内容,将会影响索引。每一个insert,update,delete语句将会使
性能下降一些。实验表明,不要在一个单表上用大量的索引,不要在共享的列上(指在多
表中用了参考约束)使用重叠的索引。
在某一列上检查唯一的数据的个数,比较它与表中数据的行数做一个比较。这就是数据的
选择性,这比较结果将会帮助你决定是否将某一列作为侯选的索引列,如果需要,建哪一
种索引。你可以用下面的查询语句返回某一列的不同值的数目。
select count(distinct cloumn_name) from table_name
假设column_name是一个10000行的表,则看column_name返回值来决定是否应该使用,及应
该使用什么索引。
Unique values Index

5000 Nonclustered index
20 Clustered index
3 No index


2) 镞索引和非镞索引的选择

<1:>镞索引是行的物理顺序和索引的顺序是一致的。页级,低层等索引的各个级别上都包
含实际的数据页。一个表只能是有一个镞索引。由于update,delete语句要求相对多一些的
读操作,因此镞索引常常能加速这样的操作。在至少有一个索引的表中,你应该有一个镞
索引。
在下面的几个情况下,你可以考虑用镞索引:
例如: 某列包括的不同值的个数是有限的(但是不是极少的)
顾客表的州名列有50个左右的不同州名的缩写值,可以使用镞索引。
例如: 对返回一定范围内值的列可以使用镞索引,比如用between,>,>=,<,<=等等来对列
进行操作的列上。
select * from sales where ord_date between ’5/1/93’ and ’6/1/93’
例如: 对查询时返回大量结果的列可以使用镞索引。
SELECT * FROM phonebook WHERE last_name = ’Smith’

当有大量的行正在被插入表中时,要避免在本表一个自然增长(例如,identity列)的列
上建立镞索引。如果你建立了镞的索引,那么insert的性能就会大大降低。因为每一个插
入的行必须到表的最后,表的最后一个数据页。
当一个数据正在被插入(这时这个数据页是被锁定的),所有的其他插入行必须等待直到
当前的插入已经结束。
一个索引的叶级页中包括实际的数据页,并且在硬盘上的数据页的次序是跟镞索引的逻辑
次序一样的。

<2:>一个非镞的索引就是行的物理次序与索引的次序是不同的。一个非镞索引的叶级包含
了指向行数据页的指针。
在一个表中可以有多个非镞索引,你可以在以下几个情况下考虑使用非镞索引。
在有很多不同值的列上可以考虑使用非镞索引
例如:一个part_id列在一个part表中
select * from employee where emp_id = ’pcm9809f’
查询语句中用order by 子句的列上可以考虑使用镞索引



3) 一个表列如果设为主键(primary key),它会自动生成一个聚簇索引
这时不能直接使用Drop index Table1.Tableindex1语句
必须删除主键约束,用语句:alter table table1 drop constraint 约束名(如pk_xxx)



七.全文索引
use pubs
  go

  --打开数据库全文索引的支持

 execute sp_fulltext_database 'enable'
 go

  --建立全文目录ft_titles

  execute sp_fulltext_catalog 'ft_titles', 'create'
  go

  --为titles表建立全文索引数据元,UPKCL_titleidind是主键所建立的唯一索引,可
由sp_help titles得知

  execute sp_fulltext_table 'titles','create', 'ft_titles', 'UPKCL_titleidin
d'
  go

  --设置全文索引列名

  exec sp_fulltext_column 'titles', 'title', 'add'
  go
  exec sp_fulltext_column 'titles', 'notes', 'add'
  go

  --建立全文索引

  exec sp_fulltext_table 'titles', 'activate'
  go

  --填充全文索引目录

  exec sp_fulltext_catalog 'ft_titles', 'start_full'
  go

  --使用contains和freetext

  select title, notes from titles
  where contains(title, '"computer Cooking"')
  go
  select title, notes from titles
  where freetext(title, 'computer Cooking')
  go
  select title, notes from titles
  where freetext(title, '"computer Cooking"')
  go
  select title, notes from titles
  where contains(title, 'computer')
  go
  select title, notes from titles
  where freetext (*, 'computer')
  go

这里提一下google的搜索引擎的原理.
他把每个字词都做为单元去查询.
打个比方:我在字典里查询,现在我要搜索"树型"这个词,他会把这个树型这个词全文扫描一
遍,生成一个二叉树.并记下他的页数.
然后当我第2次查找的时候显然这个"记忆"提示,然后"提取".如果你对某一个字段做了全文
索引的话,他会全文扫描表一遍,然后纪录下
相应的纪录,生成二叉树.
如果我要查找"树叶",同理也可以得出页数.但当我们去查找一下"树型结构"他则会把"树型
"和"树型结构"都"纪录"下来.

八.巧妙的使用索引.
SELECT SUM(quantity) AS quantity FROM test WHERE...
1.若WHERE 里用的是字段与常量比较,MSSQL会自动引用该字段上的索引;若用的是变量,
MSSQL不会自动引用该字段上的索引而是根据聚集索引进行扫描
2.加上with(index(索引名))指定索引,即:
SELECT SUM(quantity) AS quantity FROM with(index(索引名)) test WHERE...
指定索引后,WHERE 里不论是常量还是变量,MSSQL都根据指定的索引进行扫描
3.DBCC DBREINDEX执行并不一定能优化MSSQL性能,慎用
4.如果在pub_id上建立索引的话
select * from titles where pub_id-500 >1000   ---------(a)
select * from titles where pub_id >1000+500  -----------(b)
请选用(b)语句,这样的话,他会利用索引,而(a)的话由于对字段操作了,所以不会利用索引
.
5.尽量避免用like语句,
如果去查找baa%,caa%的话
如果是like '%aa%','_aa%','[m-z]o%'  则根本不会用到索引.
替换方法.columns like 'baa%' or  columns like 'caa %'
6什么情况下应不建或少建索引
a.表记录太少 .因为索引的话,要对数据库往返2次操作,如果1个表只有几行字段的话,数
据库会对他的纪录一次性全部取出来,这样的效率要远远高于索引.
b.经常insert,delete,update的表  对一些经常处理的业务表应在查询允许的情况下尽量
减少索引
c.数据重复且分布平均的表字段,如:性别字段,各占50%的话,你即使建了,也起不到明显的
作用.
d.经常和主字段一块查询但主字段索引值比较多的表字段
表经常按收费序号、户标识编号、抄表日期、电费发生年月、操作标志来具体查询某一笔
收款的情况,如果将所有的字段都建在一个索引里那将会增加数据的修改、插入、删除时
间,从实际上分析一笔收款如果按收费序号索引就已经将记录减少到只有几条,如果再按
后面的几个字段索引查询将对性能不产生太大的影响。
e.如果一个表的记录达到100万以上的话,要对其中一个字段建索引可能要花很长的时间,
甚至导致服务器数据库死机,因为在建索引的时候 ORACLE要将索引字段所有的内容取出并
进行全面排序,数据量大的话可能导致服务器排序内存不足而引用磁盘交换空间进行,这
将严重影响服务器数据库的工作。解决方法是增大数据库启动初始化中的排序内存参数,
如果要进行大量的索引修改可以设置10M以上的排序内存(ORACLE缺省大小为64K),在索
引建立完成后应将参数修改回来,因为在实际OLTP数据库应用中一般不会用到这么大的排
序内存。


以下转载
great_domino 的 Blog

探讨如何在有着1000万条数据的MS SQL SERVER数据库中实现快速的数据提取和数据分页。
以下代码说明了我们实例中数据库的“红头文件”一表的部分数据结构:

CREATE TABLE [dbo].[TGongwen] (    --TGongwen是红头文件表名

  [Gid] [int] IDENTITY (1, 1) NOT NULL ,
--本表的id号,也是主键

  [title] [varchar] (80) COLLATE Chinese_PRC_CI_AS NULL ,
--红头文件的标题

  [fariqi] [datetime] NULL ,
--发布日期

  [neibuYonghu] [varchar] (70) COLLATE Chinese_PRC_CI_AS NULL ,
--发布用户

  [reader] [varchar] (900) COLLATE Chinese_PRC_CI_AS NULL ,

--需要浏览的用户。每个用户中间用分隔符“,”分开

) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO


  下面,我们来往数据库中添加1000万条数据:

declare @i int

set @i=1

while @i<=250000

begin

   insert into Tgongwen(fariqi,neibuyonghu,reader,title) values('2004-2-5','通
信科','通信科,办公室,王局长,刘局长,张局长,admin,刑侦支队,特勤支队,交巡警支队,经
侦支队, 户政科,治安支队,外事科','这是最先的25万条记录')

   set @i=@i+1

end

GO



declare @i int

set @i=1

while @i<=250000

begin

   insert into Tgongwen(fariqi,neibuyonghu,reader,title) values('2004-9-16','办
公室','办公室,通信科,王局长,刘局长,张局长,admin,刑侦支队,特勤支队,交巡警支队,经
侦支队,户政科,外事科','这是中间的25万条记录')

   set @i=@i+1

end

GO



declare @h int

set @h=1

while @h<=100

begin

declare @i int

set @i=2002

while @i<=2003

begin

declare @j int

       set @j=0

       while @j<50

           begin

declare @k int

           set @k=0

           while @k<50

           begin

   insert into Tgongwen(fariqi,neibuyonghu,reader,title) values(cast(@i as var
char(4))+'-8-15 3:'+cast(@j as varchar(2))+':'+cast(@j as varchar(2)),'通信科'
,'办公室,通信科,王局长,刘局长,张局长,admin,刑侦支队,特勤支队,交巡警支队,经侦支
队,户政科,外事科','这是最后的50万条记录')

           set @k=@k+1

           end

set @j=@j+1

       end

set @i=@i+1

end

set @h=@h+1

end

GO



declare @i int

set @i=1

while @i<=9000000

begin

   insert into Tgongwen(fariqi,neibuyonghu,reader,title) values('2004-5-5','通
信科','通信科,办公室,王局长,刘局长,张局长,admin,刑侦支队,特勤支队,交巡警支队,经
侦支队, 户政科,治安支队,外事科','这是最后添加的900万条记录')

   set @i=@i+1000000

end

GO

通过以上语句,我们创建了25万条由于2004年2月5日发布的记录,25万条由办公室于2004
年9月6日发布的记录,2002年和2003年各 100个2500条相同日期、不同分秒的记录(共50
万条),还有由通信科于2004年5月5日发布的900万条记录,合计1000万条。

何时使用聚集索引或非聚集索引

  下面的表总结了何时使用聚集索引或非聚集索引(很重要)。

  动作描述
   使用聚集索引
   使用非聚集索引

  列经常被分组排序
   应
   应

  返回某范围内的数据
   应
   不应

  一个或极少不同值
   不应
   不应

  小数目的不同值
   应
   不应

  大数目的不同值
   不应
   应

  频繁更新的列
   不应
   应

  外键列
   应
   应

  主键列
   应
   应

  频繁修改索引列
   不应
   应


  事实上,我们可以通过前面聚集索引和非聚集索引的定义的例子来理解上表。如:返
回某范围内的数据一项。比如您的某个表有一个时间列,恰好您把聚合索引建立在了该列
,这时您查询2004年1月1日至2004年10月1日之间的全部数据时,这个速度就将是很快的,
因为您的这本字典正文是按日期进行排序的,聚类索引只需要找到要检索的所有数据中的
开头和结尾数据即可;而不像非聚集索引,必须先查到目录中查到每一项数据对应的页码
,然后再根据页码查到具体内容。

(三)结合实际,谈索引使用的误区

  理论的目的是应用。虽然我们刚才列出了何时应使用聚集索引或非聚集索引,但在实
践中以上规则却很容易被忽视或不能根据实际情况进行综合分析。下面我们将根据在实践
中遇到的实际问题来谈一下索引使用的误区,以便于大家掌握索引建立的方法。

  1、主键就是聚集索引

  这种想法笔者认为是极端错误的,是对聚集索引的一种浪费。虽然SQL SERVER默认是
在主键上建立聚集索引的。

  通常,我们会在每个表中都建立一个ID列,以区分每条数据,并且这个ID列是自动增
大的,步长一般为1。我们的这个办公自动化的实例中的列 Gid就是如此。此时,如果我们
将这个列设为主键,SQL SERVER会将此列默认为聚集索引。这样做有好处,就是可以让您
的数据在数据库中按照ID进行物理排序,但笔者认为这样做意义不大。

  显而易见,聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这
使得聚集索引变得更加珍贵。

  从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够
根据查询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为ID号是自动生成
的,我们并不知道每条记录的ID号,所以我们很难在实践中用ID号来进行查询。这就使让
ID号这个主键作为聚集索引成为一种资源浪费。其次,让每个ID号都不同的字段作为聚集
索引也不符合“大数目的不同值情况下不应建立聚合索引”规则;当然,这种情况只是针
对用户经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。


  在办公自动化系统中,无论是系统首页显示的需要用户签收的文件、会议还是用户进
行文件查询等任何情况下进行数据查询都离不开字段的是“日期”还有用户本身的“用户
名”。

  通常,办公自动化的首页会显示每个用户尚未签收的文件或会议。虽然我们的where语
句可以仅仅限制当前用户尚未签收的情况,但如果您的系统已建立了很长时间,并且数据
量很大,那么,每次每个用户打开首页的时候都进行一次全表扫描,这样做意义是不大的
,绝大多数的用户1个月前的文件都已经浏览过了,这样做只能徒增数据库的开销而已。事
实上,我们完全可以让用户打开系统首页时,数据库仅仅查询这个用户近3个月来未阅览的
文件,通过“日期”这个字段来限制表扫描,提高查询速度。如果您的办公自动化系统已
经建立的2年,那么您的首页显示速度理论上将是原来速度8倍,甚至更快。

  在这里之所以提到“理论上”三字,是因为如果您的聚集索引还是盲目地建在ID这个
主键上时,您的查询速度是没有这么高的,即使您在“日期”这个字段上建立的索引(非
聚合索引)。下面我们就来看一下在1000万条数据量的情况下各种查询的速度表现(3个月
内的数据为25万条):

  (1)仅在主键上建立聚集索引,并且不划分时间段:

Select gid,fariqi,neibuyonghu,title from tgongwen

  用时:128470毫秒(即:128秒)

  (2)在主键上建立聚集索引,在fariq上建立非聚集索引:

select gid,fariqi,neibuyonghu,title from Tgongwen

where fariqi> dateadd(day,-90,getdate())

  用时:53763毫秒(54秒)

  (3)将聚合索引建立在日期列(fariqi)上:

select gid,fariqi,neibuyonghu,title from Tgongwen

where fariqi> dateadd(day,-90,getdate())

  用时:2423毫秒(2秒)

  虽然每条语句提取出来的都是25万条数据,各种情况的差异却是巨大的,特别是将聚
集索引建立在日期列时的差异。事实上,如果您的数据库真的有 1000万容量的话,把主键
建立在ID列上,就像以上的第1、2种情况,在网页上的表现就是超时,根本就无法显示。
这也是我摒弃ID列作为聚集索引的一个最重要的因素。

  得出以上速度的方法是:在各个select语句前加:declare @d datetime

set @d=getdate()

并在select语句后加:

select [语句执行花费时间(毫秒)]=datediff(ms,@d,getdate())

  2、只要建立索引就能显著提高查询速度

  事实上,我们可以发现上面的例子中,第2、3条语句完全相同,且建立索引的字段也
相同;不同的仅是前者在fariqi字段上建立的是非聚合索引,后者在此字段上建立的是聚
合索引,但查询速度却有着天壤之别。所以,并非是在任何字段上简单地建立索引就能提
高查询速度。

  从建表的语句中,我们可以看到这个有着1000万数据的表中fariqi字段有5003个不同
记录。在此字段上建立聚合索引是再合适不过了。在现实中,我们每天都会发几个文件,
这几个文件的发文日期就相同,这完全符合建立聚集索引要求的:“既不能绝大多数都相
同,又不能只有极少数相同”的规则。由此看来,我们建立“适当”的聚合索引对于我们
提高查询速度是非常重要的。

  3、把所有需要提高查询速度的字段都加进聚集索引,以提高查询速度

  上面已经谈到:在进行数据查询时都离不开字段的是“日期”还有用户本身的“用户
名”。既然这两个字段都是如此的重要,我们可以把他们合并起来,建立一个复合索引(
compound index)。

  很多人认为只要把任何字段加进聚集索引,就能提高查询速度,也有人感到迷惑:如
果把复合的聚集索引字段分开查询,那么查询速度会减慢吗?带着这个问题,我们来看一
下以下的查询速度(结果集都是25万条数据):(日期列fariqi首先排在复合聚集索引的
起始列,用户名neibuyonghu排在后列)

  (1)select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>'2004-
5-5'

  查询速度:2513毫秒

  (2)select gid,fariqi,neibuyonghu,title from Tgongwen where fariqi>'2004-
5-5' and neibuyonghu='办公室'

  查询速度:2516毫秒

  (3)select gid,fariqi,neibuyonghu,title from Tgongwen where neibuyonghu='
办公室'

  查询速度:60280毫秒

  从以上试验中,我们可以看到如果仅用聚集索引的起始列作为查询条件和同时用到复
合聚集索引的全部列的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(
在查询结果集数目一样的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的
话,这个索引是不起任何作用的。当然,语句1、2的查询速度一样是因为查询的条目数一
样,如果复合索引的所有列都用上,而且查询结果少的话,这样就会形成“索引覆盖”,
因而性能可以达到最优。同时,请记住:无论您是否经常使用聚合索引的其他列,但其前
导列一定要是使用最频繁的列。

(四)其他书上没有的索引使用经验总结

  1、用聚合索引比用不是聚合索引的主键速度快

  下面是实例语句:(都是提取25万条数据)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-
16'

  使用时间:3326毫秒

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid<=250000

  使用时间:4470毫秒

  这里,用聚合索引比用不是聚合索引的主键速度快了近1/4。

  2、用聚合索引比用一般的主键作order by时速度快,特别是在小数据量情况下

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by fariqi

  用时:12936

select gid,fariqi,neibuyonghu,reader,title from Tgongwen order by gid

  用时:18843

  这里,用聚合索引比用一般的主键作order by时,速度快了3/10。事实上,如果数据
量很小的话,用聚集索引作为排序列要比使用非聚集索引速度快得明显的多;而数据量如
果很大的话,如10万以上,则二者的速度差别不明显。

  3、使用聚合索引内的时间段,搜索时间会按数据占整个数据表的百分比成比例减少,
而无论聚合索引使用了多少个

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-
1'

  用时:6343毫秒(提取100万条)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-6-
6'

  用时:3170毫秒(提取50万条)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-
16'

  用时:3326毫秒(和上句的结果一模一样。如果采集的数量一样,那么用大于号和等
于号是一样的)

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-
1' and fariqi<'2004-6-6'

  用时:3280毫秒

  4 、日期列不会因为有分秒的输入而减慢查询速度

  下面的例子中,共有100万条数据,2004年1月1日以后的数据有50万条,但只有两个不
同的日期,日期精确到日;之前有数据50万条,有5000个不同的日期,日期精确到秒。


select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi>'2004-1-
1' order by fariqi

  用时:6390毫秒

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi<'2004-1-
1' order by fariqi

  用时:6453毫秒

  (五)其他注意事项

  “水可载舟,亦可覆舟”,索引也一样。索引有助于提高检索性能,但过多或不当的
索引也会导致系统低效。因为用户在表中每加进一个索引,数据库就要做更多的工作。过
多的索引甚至会导致索引碎片。

  所以说,我们要建立一个“适当”的索引体系,特别是对聚合索引的创建,更应精益
求精,以使您的数据库能得到高性能的发挥。

  当然,在实践中,作为一个尽职的数据库管理员,您还要多测试一些方案,找出哪种
方案效率最高、最为有效。

二、改善SQL语句

  很多人不知道SQL语句在SQL SERVER中是如何执行的,他们担心自己所写的SQL语句会
被SQL SERVER误解。比如:

select * from table1 where name='zhangsan' and tID > 10000

  和执行:

select * from table1 where tID > 10000 and name='zhangsan'

  一些人不知道以上两条语句的执行效率是否一样,因为如果简单的从语句先后上看,
这两个语句的确是不一样,如果tID是一个聚合索引,那么后一句仅仅从表的10000条以后
的记录中查找就行了;而前一句则要先从全表中查找看有几个name='zhangsan'的,而后再
根据限制条件条件 tID>10000来提出查询结果。

  事实上,这样的担心是不必要的。SQL SERVER中有一个“查询分析优化器”,它可以
计算出where子句中的搜索条件并确定哪个索引能缩小表扫描的搜索空间,也就是说,它能
实现自动优化。

  虽然查询优化器可以根据where子句自动的进行查询优化,但大家仍然有必要了解一下
“查询优化器”的工作原理,如非这样,有时查询优化器就会不按照您的本意进行快速查
询。

  在查询分析阶段,查询优化器查看查询的每个阶段并决定限制需要扫描的数据量是否
有用。如果一个阶段可以被用作一个扫描参数(SARG),那么就称之为可优化的,并且可
以利用索引快速获得所需数据。

  SARG的定义:用于限制搜索的一个操作,因为它通常是指一个特定的匹配,一个值得
范围内的匹配或者两个以上条件的AND连接。形式如下:

列名 操作符 <常数 或 变量>



<常数 或 变量> 操作符列名

  列名可以出现在操作符的一边,而常数或变量出现在操作符的另一边。如:

Name=’张三’

价格>5000

5000<价格

Name=’张三’ and 价格>5000

  如果一个表达式不能满足SARG的形式,那它就无法限制搜索的范围了,也就是SQL SE
RVER必须对每一行都判断它是否满足WHERE子句中的所有条件。所以一个索引对于不满足S
ARG形式的表达式来说是无用的。

  介绍完SARG后,我们来总结一下使用SARG以及在实践中遇到的和某些资料上结论不同
的经验:

  1、Like语句是否属于SARG取决于所使用的通配符的类型

  如:name like ‘张%’ ,这就属于SARG

  而:name like ‘%张’ ,就不属于SARG。

  原因是通配符%在字符串的开通使得索引无法使用。

  2、or 会引起全表扫描

Name=’张三’ and 价格>5000 符号SARG,而:Name=’张三’ or 价格>5000 则不符合S
ARG。使用or会引起全表扫描。

  3、非操作符、函数引起的不满足SARG形式的语句

  不满足SARG形式的语句最典型的情况就是包括非操作符的语句,如:NOT、!=、<>、!
<、!>、NOT EXISTS、NOT IN、NOT LIKE等,另外还有函数。下面就是几个不满足SARG形式
的例子:

ABS(价格)<5000

Name like ‘%三’

  有些表达式,如:

WHERE 价格*2>5000

  SQL SERVER也会认为是SARG,SQL SERVER会将此式转化为:

WHERE 价格>2500/2

  但我们不推荐这样使用,因为有时SQL SERVER不能保证这种转化与原始表达式是完全
等价的。

  4、IN 的作用相当与OR

  语句:

Select * from table1 where tid in (2,3)

  和

Select * from table1 where tid=2 or tid=3

  是一样的,都会引起全表扫描,如果tid上有索引,其索引也会失效。

  5、尽量少用NOT

  6、exists 和 in 的执行效率是一样的

  很多资料上都显示说,exists要比in的执行效率要高,同时应尽可能的用not exists
来代替not in。但事实上,我试验了一下,发现二者无论是前面带不带not,二者之间的执
行效率都是一样的。因为涉及子查询,我们试验这次用SQL SERVER自带的pubs数据库。运
行前我们可以把SQL SERVER的statistics I/O状态打开。

  (1)select title,price from titles where title_id in (select title_id fro
m sales where qty>30)

  该句的执行结果为:

  表 'sales'。扫描计数 18,逻辑读 56 次,物理读 0 次,预读 0 次。

  表 'titles'。扫描计数 1,逻辑读 2 次,物理读 0 次,预读 0 次。

  (2)select title,price from titles where exists (select * from sales wher
e sales.title_id=titles.title_id and qty>30)

  第二句的执行结果为:

  表 'sales'。扫描计数 18,逻辑读 56 次,物理读 0 次,预读 0 次。

  表 'titles'。扫描计数 1,逻辑读 2 次,物理读 0 次,预读 0 次。

  我们从此可以看到用exists和用in的执行效率是一样的。

  7、用函数charindex()和前面加通配符%的LIKE执行效率一样

  前面,我们谈到,如果在LIKE前面加上通配符%,那么将会引起全表扫描,所以其执行
效率是低下的。但有的资料介绍说,用函数charindex()来代替LIKE速度会有大的提升,经
我试验,发现这种说明也是错误的:

select gid,title,fariqi,reader from tgongwen where charindex('刑侦支队',reader
)>0 and fariqi>'2004-5-5'

  用时:7秒,另外:扫描计数 4,逻辑读 7155 次,物理读 0 次,预读 0 次。

select gid,title,fariqi,reader from tgongwen where reader like '%' + '刑侦支队
' + '%' and fariqi>'2004-5-5'

  用时:7秒,另外:扫描计数 4,逻辑读 7155 次,物理读 0 次,预读 0 次。

  8、union并不绝对比or的执行效率高

  我们前面已经谈到了在where子句中使用or会引起全表扫描,一般的,我所见过的资料
都是推荐这里用union来代替or。事实证明,这种说法对于大部分都是适用的。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-
16' or gid>9990000

  用时:68秒。扫描计数 1,逻辑读 404008 次,物理读 283 次,预读 392163 次。


select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-
16'

union

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where gid>9990000

  用时:9秒。扫描计数 8,逻辑读 67489 次,物理读 216 次,预读 7499 次。

  看来,用union在通常情况下比用or的效率要高的多。

  但经过试验,笔者发现如果or两边的查询列是一样的话,那么用union则反倒和用or的
执行速度差很多,虽然这里union扫描的是索引,而or扫描的是全表。

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-
16' or fariqi='2004-2-5'

  用时:6423毫秒。扫描计数 2,逻辑读 14726 次,物理读 1 次,预读 7176 次。


select gid,fariqi,neibuyonghu,reader,title from Tgongwen where fariqi='2004-9-
16'

union

select gid,fariqi,neibuyonghu,reader,title from Tgongwen where  fariqi='2004-2
-5'

  用时:11640毫秒。扫描计数 8,逻辑读 14806 次,物理读 108 次,预读 1144 次。


  9、字段提取要按照“需多少、提多少”的原则,避免“select *”

  我们来做一个试验:

select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc

  用时:4673毫秒

select top 10000 gid,fariqi,title from tgongwen order by gid desc

  用时:1376毫秒

select top 10000 gid,fariqi from tgongwen order by gid desc

  用时:80毫秒

  由此看来,我们每少提取一个字段,数据的提取速度就会有相应的提升。提升的速度
还要看您舍弃的字段的大小来判断。

  10、count(*)不比count(字段)慢

  某些资料上说:用*会统计所有列,显然要比一个世界的列名效率低。这种说法其实是
没有根据的。我们来看:

select count(*) from Tgongwen

  用时:1500毫秒

select count(gid) from Tgongwen

  用时:1483毫秒

select count(fariqi) from Tgongwen

  用时:3140毫秒

select count(title) from Tgongwen

  用时:52050毫秒

  从以上可以看出,如果用count(*)和用count(主键)的速度是相当的,而count(*)却比
其他任何除主键以外的字段汇总速度要快,而且字段越长,汇总的速度就越慢。我想,如
果用count(*), SQL SERVER可能会自动查找最小字段来汇总的。当然,如果您直接写cou
nt(主键)将会来的更直接些。

  11、order by按聚集索引列排序效率最高

  我们来看:(gid是主键,fariqi是聚合索引列)

select top 10000 gid,fariqi,reader,title from tgongwen

  用时:196 毫秒。 扫描计数 1,逻辑读 289 次,物理读 1 次,预读 1527 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by gid asc

  用时:4720毫秒。 扫描计数 1,逻辑读 41956 次,物理读 0 次,预读 1287 次。


select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc

  用时:4736毫秒。 扫描计数 1,逻辑读 55350 次,物理读 10 次,预读 775 次。


select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi asc

  用时:173毫秒。 扫描计数 1,逻辑读 290 次,物理读 0 次,预读 0 次。

select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi desc

  用时:156毫秒。 扫描计数 1,逻辑读 289 次,物理读 0 次,预读 0 次。

  从以上我们可以看出,不排序的速度以及逻辑读次数都是和“order by 聚集索引列”
的速度是相当的,但这些都比“order by 非聚集索引列”的查询速度是快得多的。

  同时,按照某个字段进行排序的时候,无论是正序还是倒序,速度是基本相当的。


  12、高效的TOP

  事实上,在查询和提取超大容量的数据集时,影响数据库响应时间的最大因素不是数
据查找,而是物理的I/0操作。如:

select top 10 * from (

select top 10000 gid,fariqi,title from tgongwen

where neibuyonghu='办公室'

order by gid desc) as a

order by gid asc

  这条语句,从理论上讲,整条语句的执行时间应该比子句的执行时间长,但事实相反
。因为,子句执行后返回的是10000条记录,而整条语句仅返回 10条语句,所以影响数据
库响应时间最大的因素是物理I/O操作。而限制物理I/O操作此处的最有效方法之一就是使
用TOP关键词了。TOP关键词是 SQL SERVER中经过系统优化过的一个用来提取前几条或前几
个百分比数据的词。经笔者在实践中的应用,发现TOP确实很好用,效率也很高。但这个词
在另外一个大型数据库ORACLE中却没有,这不能说不是一个遗憾,虽然在ORACLE中可以用
其他方法(如:rownumber)来解决。在以后的关于“实现千万级数据的分页显示存储过程
”的讨论中,我们就将用到TOP这个关键词。

  到此为止,我们上面讨论了如何实现从大容量的数据库中快速地查询出您所需要的数
据方法。当然,我们介绍的这些方法都是“软”方法,在实践中,我们还要考虑各种“硬
”因素,如:网络性能、服务器的性能、操作系统的性能,甚至网卡、交换机等。

三、实现小数据量和海量数据的通用分页显示存储过程

  建立一个web 应用,分页浏览功能必不可少。这个问题是数据库处理中十分常见的问
题。经典的数据分页方法是:ADO 纪录集分页法,也就是利用ADO自带的分页功能(利用游
标)来实现分页。但这种分页方法仅适用于较小数据量的情形,因为游标本身有缺点:游
标是存放在内存中,很费内存。游标一建立,就将相关的记录锁住,直到取消游标。游标
提供了对特定集合中逐行扫描的手段,一般使用游标来逐行遍历数据,根据取出数据条件
的不同进行不同的操作。而对于多表和大表中定义的游标(大的数据集合)循环很容易使
程序进入一个漫长的等待甚至死机。

  更重要的是,对于非常大的数据模型而言,分页检索时,如果按照传统的每次都加载
整个数据源的方法是非常浪费资源的。现在流行的分页方法一般是检索页面大小的块区的
数据,而非检索所有的数据,然后单步执行当前行。

  最早较好地实现这种根据页面大小和页码来提取数据的方法大概就是“俄罗斯存储过
程”。这个存储过程用了游标,由于游标的局限性,所以这个方法并没有得到大家的普遍
认可。

  后来,网上有人改造了此存储过程,下面的存储过程就是结合我们的办公自动化实例
写的分页存储过程:

CREATE procedure pagination1

(@pagesize int,  --页面大小,如每页存储20条记录

@pageindex int   --当前页码

)

as

set nocount on

begin

declare @indextable table(id int identity(1,1),nid int)  --定义表变量

declare @PageLowerBound int  --定义此页的底码

declare @PageUpperBound int  --定义此页的顶码

set @PageLowerBound=(@pageindex-1)*@pagesize

set @PageUpperBound=@PageLowerBound+@pagesize

set rowcount @PageUpperBound

insert into @indextable(nid) select gid from TGongwen where fariqi >dateadd(da
y,-365,getdate()) order by fariqi desc

select O.gid,O.mid,O.title,O.fadanwei,O.fariqi from TGongwen O,@indextable t w
here O.gid=t.nid

and t.id>@PageLowerBound and t.id<=@PageUpperBound order by t.id

end

set nocount off

  以上存储过程运用了SQL SERVER的最新技术――表变量。应该说这个存储过程也是一
个非常优秀的分页存储过程。当然,在这个过程中,您也可以把其中的表变量写成临时表
: CREATE TABLE #Temp。但很明显,在SQL SERVER中,用临时表是没有用表变量快的。所
以笔者刚开始使用这个存储过程时,感觉非常的不错,速度也比原来的ADO的好。但后来,
我又发现了比此方法更好的方法。

  笔者曾在网上看到了一篇小短文《从数据表中取出第n条到第m条的记录的方法》,全
文如下:

从publish 表中取出第 n 条到第 m 条的记录:
SELECT TOP m-n+1 *
FROM publish
WHERE (id NOT IN
    (SELECT TOP n-1 id
     FROM publish))

id 为publish 表的关键字

  我当时看到这篇文章的时候,真的是精神为之一振,觉得思路非常得好。等到后来,
我在作办公自动化系统(ASP.NET+ C#+SQL SERVER)的时候,忽然想起了这篇文章,我想
如果把这个语句改造一下,这就可能是一个非常好的分页存储过程。于是我就满网上找这
篇文章,没想到,文章还没找到,却找到了一篇根据此语句写的一个分页存储过程,这个
存储过程也是目前较为流行的一种分页存储过程,我很后悔没有争先把这段文字改造成存
储过程:

CREATE PROCEDURE pagination2
(
@SQL nVARCHAR(4000),    --不带排序语句的SQL语句
@Page int,              --页码
@RecsPerPage int,       --每页容纳的记录数
@ID VARCHAR(255),       --需要排序的不重复的ID号
@Sort VARCHAR(255)      --排序字段及规则
)
AS

DECLARE @Str nVARCHAR(4000)

SET @Str='SELECT   TOP '+CAST(@RecsPerPage AS VARCHAR(20))+' * FROM ('+@SQL+')
T WHERE T.'+@ID+'NOT IN
(SELECT   TOP '+CAST((@RecsPerPage*(@Page-1)) AS VARCHAR(20))+' '+@ID+' FROM (
'+@SQL+') T9 ORDER BY '+@Sort+') ORDER BY '+@Sort

PRINT @Str

EXEC sp_ExecuteSql @Str
GO

  其实,以上语句可以简化为:

SELECT TOP 页大小 *

FROM Table1

WHERE (ID NOT IN

         (SELECT TOP 页大小*页数 id

        FROM 表

        ORDER BY id))

ORDER BY ID

  但这个存储过程有一个致命的缺点,就是它含有NOT IN字样。虽然我可以把它改造为


SELECT TOP 页大小 *

FROM Table1

WHERE not exists

(select * from (select top (页大小*页数) * from table1 order by id) b where b.
id=a.id )

order by id

  即,用not exists来代替not in,但我们前面已经谈过了,二者的执行效率实际上是
没有区别的。

  既便如此,用TOP 结合NOT IN的这个方法还是比用游标要来得快一些。

  虽然用not exists并不能挽救上个存储过程的效率,但使用SQL SERVER中的TOP关键字
却是一个非常明智的选择。因为分页优化的最终目的就是避免产生过大的记录集,而我们
在前面也已经提到了TOP的优势,通过TOP 即可实现对数据量的控制。

  在分页算法中,影响我们查询速度的关键因素有两点:TOP和NOT IN。TOP可以提高我
们的查询速度,而NOT IN会减慢我们的查询速度,所以要提高我们整个分页算法的速度,
就要彻底改造NOT IN,同其他方法来替代它。

  我们知道,几乎任何字段,我们都可以通过max(字段)或min(字段)来提取某个字段中
的最大或最小值,所以如果这个字段不重复,那么就可以利用这些不重复的字段的max或m
in作为分水岭,使其成为分页算法中分开每页的参照物。在这里,我们可以用操作符“>”
或“<”号来完成这个使命,使查询语句符合SARG形式。如:

Select top 10 * from table1 where id>200

  于是就有了如下分页方案:

select top 页大小 *

from table1

where id>

     (select max (id) from

     (select top ((页码-1)*页大小) id from table1 order by id) as T

      )    

order by id

  在选择即不重复值,又容易分辨大小的列时,我们通常会选择主键。下表列出了笔者
用有着1000万数据的办公自动化系统中的表,在以GID (GID是主键,但并不是聚集索引。
)为排序列、提取gid,fariqi,title字段,分别以第1、10、100、500、1000、1万、10 万
、25万、50万页为例,测试以上三种分页方案的执行速度:(单位:毫秒)

页  码
方案1
方案2
方案3

1
60
30
76

10
46
16
63

100
1076
720
130

500
540
12943
83

1000
17110
470
250

1万
24796
4500
140

10万
38326
42283
1553

25万
28140
128720
2330

50万
121686
127846
7168


  从上表中,我们可以看出,三种存储过程在执行100页以下的分页命令时,都是可以信
任的,速度都很好。但第一种方案在执行分页1000页以上后,速度就降了下来。第二种方
案大约是在执行分页1万页以上后速度开始降了下来。而第三种方案却始终没有大的降势,
后劲仍然很足。

  在确定了第三种分页方案后,我们可以据此写一个存储过程。大家知道SQL SERVER的
存储过程是事先编译好的SQL语句,它的执行效率要比通过WEB页面传来的SQL语句的执行效
率要高。下面的存储过程不仅含有分页方案,还会根据页面传来的参数来确定是否进行数
据总数统计。

-- 获取指定页的数据

CREATE PROCEDURE pagination3

@tblName   varchar(255),       -- 表名

@strGetFields varchar(1000) = '*',  -- 需要返回的列

@fldName varchar(255)='',      -- 排序的字段名

@PageSize   int = 10,          -- 页尺寸

@PageIndex  int = 1,           -- 页码

@doCount  bit = 0,   -- 返回记录总数, 非 0 值则返回

@OrderType bit = 0,  -- 设置排序类型, 非 0 值则降序

@strWhere  varchar(1500) = ''  -- 查询条件 (注意: 不要加 where)

AS

declare @strSQL   varchar(5000)       -- 主语句

declare @strTmp   varchar(110)        -- 临时变量

declare @strOrder varchar(400)        -- 排序类型



if @doCount != 0

begin

   if @strWhere !=''

   set @strSQL = "select count(*) as Total from [" + @tblName + "] where "+@st
rWhere

   else

   set @strSQL = "select count(*) as Total from [" + @tblName + "]"

end

--以上代码的意思是如果@doCount传递过来的不是0,就执行总数统计。以下的所有代码都
是@doCount为0的情况

else

begin



if @OrderType != 0

begin

   set @strTmp = "<(select min"

set @strOrder = " order by [" + @fldName +"] desc"

--如果@OrderType不是0,就执行降序,这句很重要!

end

else

begin

   set @strTmp = ">(select max"

   set @strOrder = " order by [" + @fldName +"] asc"

end



if @PageIndex = 1

begin

   if @strWhere != ''  

   set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "  from ["
+ @tblName + "] where " + @strWhere + " " + @strOrder

    else

    set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "  from [
"+ @tblName + "] "+ @strOrder

--如果是第一页就执行以上代码,这样会加快执行速度

end

else

begin

--以下代码赋予了@strSQL以真正执行的SQL代码

set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "  from ["

   + @tblName + "] where [" + @fldName + "]" + @strTmp + "(["+ @fldName + "])
from (select top " + str((@PageIndex-1)*@PageSize) + " ["+ @fldName + "] from
[" + @tblName + "]" + @strOrder + ") as tblTmp)"+ @strOrder



if @strWhere != ''

   set @strSQL = "select top " + str(@PageSize) +" "+@strGetFields+ "  from ["


       + @tblName + "] where [" + @fldName + "]" + @strTmp + "(["

       + @fldName + "]) from (select top " + str((@PageIndex-1)*@PageSize) + "
["

       + @fldName + "] from [" + @tblName + "] where " + @strWhere + " "

       + @strOrder + ") as tblTmp) and " + @strWhere + " " + @strOrder

end

end  

exec (@strSQL)

GO

  上面的这个存储过程是一个通用的存储过程,其注释已写在其中了。

  在大数据量的情况下,特别是在查询最后几页的时候,查询时间一般不会超过9秒;而
用其他存储过程,在实践中就会导致超时,所以这个存储过程非常适用于大容量数据库的
查询。

  笔者希望能够通过对以上存储过程的解析,能给大家带来一定的启示,并给工作带来
一定的效率提升,同时希望同行提出更优秀的实时数据分页算法。

四、聚集索引的重要性和如何选择聚集索引

  在上一节的标题中,笔者写的是:实现小数据量和海量数据的通用分页显示存储过程
。这是因为在将本存储过程应用于“办公自动化”系统的实践中时,笔者发现这第三种存
储过程在小数据量的情况下,有如下现象:

  1、分页速度一般维持在1秒和3秒之间。

  2、在查询最后一页时,速度一般为5秒至8秒,哪怕分页总数只有3页或30万页。

  虽然在超大容量情况下,这个分页的实现过程是很快的,但在分前几页时,这个1-3
秒的速度比起第一种甚至没有经过优化的分页方法速度还要慢,借用户的话说就是“还没
有ACCESS数据库速度快”,这个认识足以导致用户放弃使用您开发的系统。

  笔者就此分析了一下,原来产生这种现象的症结是如此的简单,但又如此的重要:排
序的字段不是聚集索引!

  本篇文章的题目是:“查询优化及分页算法方案”。笔者只所以把“查询优化”和“
分页算法”这两个联系不是很大的论题放在一起,就是因为二者都需要一个非常重要的东
西――聚集索引。

  在前面的讨论中我们已经提到了,聚集索引有两个最大的优势:

  1、以最快的速度缩小查询范围。

  2、以最快的速度进行字段排序。

  第1条多用在查询优化时,而第2条多用在进行分页时的数据排序。

  而聚集索引在每个表内又只能建立一个,这使得聚集索引显得更加的重要。聚集索引
的挑选可以说是实现“查询优化”和“高效分页”的最关键因素。

  但要既使聚集索引列既符合查询列的需要,又符合排序列的需要,这通常是一个矛盾


  笔者前面“索引”的讨论中,将fariqi,即用户发文日期作为了聚集索引的起始列,
日期的精确度为“日”。这种作法的优点,前面已经提到了,在进行划时间段的快速查询
中,比用ID主键列有很大的优势。

  但在分页时,由于这个聚集索引列存在着重复记录,所以无法使用max或min来最为分
页的参照物,进而无法实现更为高效的排序。而如果将ID主键列作为聚集索引,那么聚集
索引除了用以排序之外,没有任何用处,实际上是浪费了聚集索引这个宝贵的资源。

   为解决这个矛盾,笔者后来又添加了一个日期列,其默认值为getdate()。用户在写
入记录时,这个列自动写入当时的时间,时间精确到毫秒。即使这样,为了避免可能性很
小的重合,还要在此列上创建UNIQUE约束。将此日期列作为聚集索引列。

  有了这个时间型聚集索引列之后,用户就既可以用这个列查找用户在插入数据时的某
个时间段的查询,又可以作为唯一列来实现max或min,成为分页算法的参照物。

  经过这样的优化,笔者发现,无论是大数据量的情况下还是小数据量的情况下,分页
速度一般都是几十毫秒,甚至0毫秒。而用日期段缩小范围的查询速度比原来也没有任何迟
钝。

  聚集索引是如此的重要和珍贵,所以笔者总结了一下,一定要将聚集索引建立在:


  1、您最频繁使用的、用以缩小查询范围的字段上;

  2、您最频繁使用的、需要排序的字段上。

  结束语:

  希望这篇文章不仅能够给大家的工作带来一定的帮助,也希望能让大家能够体会到分
析问题的方法;最重要的是,希望这篇文章能够抛砖引玉,掀起大家的学习和讨论的兴趣
,以共同促进。
  最后需要说明的是,在试验中,发现用户在进行大数据量查询的时候,对数据库速度
影响最大的不是内存大小,而是CPU。在我的P4 2.4机器上试验的时候,查看“资源管理器
”,CPU经常出现持续到100%的现象,而内存用量却并没有改变或者说没有大的改变。即使
在我们的HP ML 350 G3服务器上试验时,CPU峰值也能达到90%,一般持续在70%左右。

  本文的试验数据都是来自我们的HP ML 350服务器。服务器配置:双Inter Xeon 超线
程 CPU 2.4G,内存1G,操作系统Windows Server 2003 Enterprise Edition,数据库SQL
Server 2000 SP3。

转载完毕.


作者补充:
1.columns in('aa','bb')
他等于columns = 'aa' or columns ='bb' 他先去查询columns ='aa'放在一个临时的空间
里,然后等columns ='bb'查询完后,做个or查询得出结果.
至于效率的话,在columns建立索引的话, columns ='aa' or columns ='bb'要来的效率高

语法分析器会将columns in('aa','bb')转化
为columns ='aa' or columns ='bb'来执行。我们期望它会根据每个or子句分别查找,再
将结果
相加,这样可以利用columns 上的索引;但实际上(根据showplan),它却采用了"OR策略
"
,即先取出满足每个or子句的行,存入临时数据库的工作表中,再建立唯一索引以去掉

重复行,最后从这个临时表中计算结果。因此,实际过程没有利用columns 上索引,并且

成时间还要受tempdb数据库性能的影响。


2.效率从高到低 count(1)>count(*)>count([id])

3.select max(cols) from table1 的效率>= select top 1 cols from table1 order by
cols desc

4.在where 做并列条件句时,where cols1='aa' and cols2='bb'
如果cols1 ='aa' 占95% cols2占5%的话,把cols2='bb'放在前面 ,因为他在检索cols ='
bb'的时候他只需查那5%,然后条件成立的话,去在这5%的纪录里
去查找cols1 ='aa'

5.避免用if条件句,可以用or来替代.
declare @vsql varchar(200)
set @vsql ='Renaski'
select * from titles where  @vsql ='Renaski' or price = 11.9500

如果@vsql为Renaski则把所有的纪录都选出来,如果不是的话,则只查询price = 11.9500
的纪录.

6.任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时
要尽可能将操作移至等号右边。

7.尽量避免使用游标.
如果使用了游标,就要尽量避免在游标循环中再进行表连接的操作

8.取一个表的纪录数
Select rows from sysindexes where id=object_id(N'titles') and indid<2
效率比
select count(1) from titles来的高.


9.取的一个表的数据信息.
SELECT
表名=case when a.colorder=1 then d.name else '' end,
表说明=case when a.colorder=1 then isnull(f.value,'') else '' end,
字段序号=a.colorder,
字段名=a.name,
标识=case when COLUMNPROPERTY( a.id,a.name,'IsIdentity')=1 then '√'else '' en
d,
主键=case when exists(SELECT 1 FROM sysobjects where xtype='PK' and name in (

SELECT name FROM sysindexes WHERE indid in(
SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid
))) then '√' else '' end,
类型=b.name,
占用字节数=a.length,
长度=COLUMNPROPERTY(a.id,a.name,'PRECISION'),
小数位数=isnull(COLUMNPROPERTY(a.id,a.name,'Scale'),0),
允许空=case when a.isnullable=1 then '√'else '' end,
默认值=isnull(e.text,''),
字段说明=isnull(g.[value],''),
索引名称=isnull(h.索引名称,''),
索引顺序=isnull(h.排序,'')
FROM syscolumns a
left join systypes b on a.xtype=b.xusertype
inner join sysobjects d on a.id=d.id and d.xtype='U' and d.status>=0
left join syscomments e on a.cdefault=e.id
left join sysproperties g on a.id=g.id and a.colid=g.smallid
left join sysproperties f on d.id=f.id and f.smallid=0
left join(--这部分是索引信息,如果要显示索引与表及字段的对应关系,可以只要此部分

select 索引名称=a.name,c.id,d.colid
,排序=case indexkey_property(c.id,b.indid,b.keyno,'isdescending')
when 1 then '降序' when 0 then '升序' end
from sysindexes a
join sysindexkeys b on a.id=b.id and a.indid=b.indid
join (--这里的作用是有多个索引时,取索引号最小的那个
select id,colid,indid=min(indid) from sysindexkeys
group by id,colid) b1 on b.id=b1.id and b.colid=b1.colid and b.indid=b1.indid

join sysobjects c on b.id=c.id and c.xtype='U' and c.status>=0
join syscolumns d on b.id=d.id and b.colid=d.colid
where a.indid not in(0,255)
) h on a.id=h.id and a.colid=h.colid
--where d.name='要查询的表' --如果只查询指定表,加上此条件
order by a.id,a.colorder


10.创建一个表结构.
select * into #b from authors where 1=2;
注意:
#table1
##table1
@table1

局部临时表
以一个井号(#)开头的那些表名。只有在创建本地临时表的连接上才能看到这些表。

全局临时表
以两个井号(##)开头的那些表名。在所有连接上都能看到全局临时表。如果在创建全局
临时表的连接断开前没有显式地除去这些表,那么只要所有其它任务停止引用它们,这些
表即被除去。当创建全局临时表的连接断开后,新的任务不能再引用它们。当前的语句一
执行完,任务与表之间的关联即被除去;因此通常情况下,只要创建全局临时表的连接断
开,全局临时表即被除去。

@和#有和不同:@@在内存,#在硬盘。我的体会是只要方便且数据量不大,使用@@。

11.视图
他只是记住要连接,关联列的信息,他不存放任何物理数据.
在调用的时候他还是去取各个表中的数据.

12.尽量不要用text属性
系统为他专门开辟一个空间来存放.
用t-sql/varchar替代
pl/sql  varchar2 替代.

13
GO语句是个命令识别并通过osql和isql和SQL 查询分析器非T-SQL语句进行识别。
如果你使用查询分析器作为你的主开发工具,其他语句和库文件将不会识别GO语句作为一
个T-SQL命令

14.
用exec 效率来的高.
declare @sql nvarchar(300)
  set @sql='select * from titles'
execute sp_executesql @sql

15,注意你的tempdb,使他自动增长.

16 使用no_log
select * from titles no_logs

17去不重复纪录时,尽量用dictinct

18.尽量避免反复访问同一张或几张表,尤其是数据量较大的表,可以考虑先根据条件提取
数据到临时表中,然后再做连接。


19 尽量使用“>=”,不要使用“>”。 他会找到某个确定的数字进行筛选,而>则没有.


20注意表之间连接的数据类型,避免不同类型数据之间的连接。

21.可用ASE调优命令:set statistics io on, set statistics time on , set showpla
n on 等,进行优化

22.truncate table 删除数据
而不是delete from table


三.死锁

像SQL server一样的关系数据库使用锁来防止用户“互相踩到对方的脚趾头”。也就是说
,锁可以防止用户造成修改数据时的碰撞。当一个用户锁住一段代码时候,其它的用户都
不能修改这段数据。另外,一个锁阻止了用户观看未被授权的数据修改。用户必须等待到
数据修改并保存之后才能够查看它。数据必须使用不同的方法来加锁。SQL Server 2000使
用锁来实现多用户同时修改数据库同一数据时的同步控制
如果数据量超过200个数据页面(400k),那么系统将会进行锁升级,页级锁会升级成表级
锁。   


死锁
一个数据库的死锁是发生在两个或多于两个访问一些资源的数据库会话中的,并且这些会
话相互之间有依赖关系。死锁是可以在任意一个多线程的系统成出现的一个情况,不仅仅
局限于关系数据库管理系统。一个多线程系统中的线程可能需要一个或多个资源(例如,锁
)。如果申请的资源正在被另外一个线程所使用,那么第一个线程就需要等待持有该资源的
线程的释放它所需要的资源。假设等待线程持有一个那个正拥有线程所依赖的资源。下面
的这一段代码就可以造成死锁异常现象的发生:
System.Data.SqlClient.SqlException: Transaction (Process ID 12) was deadlocked
on lock resources with another process and has been chosen as the deadlock vi
ctim. Rerun the transaction.

当一个SQL Server的调用和另外一个资源发生冲突时就会抛出异常,这个资源持有一个必
要的资源。结果是,一个进程就被终止了。当进程的ID号成为系统的唯一标识的时候,这
会是一个很平常死锁的消息错误。


锁的类型
一个数据库系统在许多情况下都有可能锁数据项。其可能性包括:

Rows—数据库表中的一整行
Pages—行的集合(通常为几kb)
Extents—通常是几个页的集合
Table—整个数据库表
Database—被锁的整个数据库表

除非有其它的说明,数据库根据情况自己选择最好的锁方式。不过值得感谢的是,SQL Se
rver提供了一种避免默认行为的方法。这是由锁提示来完成的。


提示
或许你有过许多如下的经历:需要重设SQL Server的锁计划,并且加强数据库表中锁范围
。Tansact-SQL提供了一系列不同级别的锁提示,你可以在SELECT,INSERT, UPDATE和DEL
ETE中使用它们来告诉SQL Server你需要如何通过重设任何的系统或事务级别来锁表格。可
以实现的提示包括:

FASTFIRSTROW—选取结果集中的第一行,并将其优化
HOLDLOCK—持有一个共享锁直至事务完成
NOLOCK—不允许使用共享锁或独享锁。这可能会造成数据重写或者没有被确认就返回的情
况;因此,就有可能使用到脏数据。这个提示只能在SELECT中使用。
PAGLOCK—锁表格
READCOMMITTED—只读取被事务确认的数据。这就是SQL Server的默认行为。
READPAST—跳过被其它进程锁住的行,所以返回的数据可能会忽略行的内容。这也只能在
SELECT中使用。
READUNCOMMITTED—等价于NOLOCK.
REPEATABLEREAD—在查询语句中,对所有数据使用锁。这可以防止其它的用户更新数据,
但是新的行可能被其它的用户插入到数据中,并且被最新访问该数据的用户读取。
ROWLOCK—按照行的级别来对数据上锁。SQL Server通常锁到页或者表级别来修改行,所以
当开发者使用单行的时候,通常要重设这个设置。
SERIALIZABLE—等价于HOLDLOCK.
TABLOCK—按照表级别上锁。在运行多个有关表级别数据操作的时候,你可能需要使用到这
个提示。
UPDLOCK—当读取一个表的时候,使用更新锁来代替共享锁,并且保持一直拥有这个锁直至
事务结束。它的好处是,可以允许你在阅读数据的时候可以不需要锁,并且以最快的速度
更新数据。
XLOCK—给所有的资源都上独享锁,直至事务结束。

对于数据库死锁,通常可以通过TRACE FLAG 1204、1205、1206,检查ERRORLOG里面的输出
,和分析SQLTRACE的执行上下文判断死锁问题的来由。
TRACEON函数的第三个参数设置为-1,表示不单单针对当前connection,而是针对所有包括
未来建立的connection。这样,才够完全,否则只是监视当前已经建立的数据库连接了。



执行下面的话可以把死锁记录到Errorlog中:

dbcc traceon (1204, 3605, -1)
go
dbcc tracestatus(-1)
go

 

得到的输出为:
DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。
TraceFlag Status
--------- ------
1204      1
1205      1
3605      1

(所影响的行数为 3 行)

DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。


此后,你可以查看数据库的例行日志,每隔一段时间,数据库都会检查死锁
2004-01-16 18:34:38.50 spid4     ----------------------------------
2004-01-16 18:34:38.50 spid4     Starting deadlock search 1976



2004-01-16 18:34:38.50 spid4     Target Resource Owner:
2004-01-16 18:34:38.50 spid4      ResType:LockOwner Stype:'OR' Mode: U SPID:55
ECID:0 Ec:(0xAA577570) Value:0x4c25cba0
2004-01-16 18:34:38.50 spid4      Node:1  ResType:LockOwner Stype:'OR' Mode: U
SPID:55 ECID:0 Ec:(0xAA577570) Value:0x4c25cba0
2004-01-16 18:34:38.50 spid4      Node:2  ResType:LockOwner Stype:'OR' Mode: U
SPID:71 ECID:0 Ec:(0xABF07570) Value:0x9bd0ba00
2004-01-16 18:34:38.50 spid4    
2004-01-16 18:34:38.50 spid4     -- next branch --
2004-01-16 18:34:38.50 spid4      Node:2  ResType:LockOwner Stype:'OR' Mode: U
SPID:71 ECID:0 Ec:(0xABF07570) Value:0x9bd0ba00
2004-01-16 18:34:38.50 spid4    
2004-01-16 18:34:38.50 spid4    
2004-01-16 18:34:38.50 spid4     End deadlock search 1976 ... a deadlock was n
ot found.
2004-01-16 18:34:38.50 spid4     ----------------------------------

DBCC TRACEON打开(启用)指定的跟踪标记。

注释跟踪标记用于自定义某些控制 Microsoft? SQL Server? 操作方式的特性。跟踪标记
在服务器中一直保持启用状态,直到通过执行 DBCC TRACEOFF 语句对其禁用为止。在发出
DBCC TRACEON 语句之前,连入到服务器的新连接看不到任何跟踪标记。一旦发出该语句
,该连接就能看到服务器中当前启用的所有跟踪标记(即使这些标记是由其它连接启用)

跟踪标记跟踪标记用于临时设置服务器的特定特征或关闭特定行为。如果启动 Microsoft
? SQL Server 时设置了跟踪标记 3205,将禁用磁带驱动程序的硬件压缩。跟踪标记经常
用于诊断性能问题,或调试存储过程或复杂的计算机系统。
下列跟踪标记在 SQL Server 中可用。跟踪标记 描述 1204 返回参与死锁的锁的类型以及
当前受影响的命令。
实际上可以在“错误 1000 -1999”中找到他们:
1204 19 SQL Server 此时无法获取 LOCK 资源。请在活动用户数较少时重新运行您的语句
,或者请求系统管理员检查 SQL Server 锁和内存配置。
1205 13 事务(进程 ID %1!)与另一个进程已被死锁在资源 {%2!} 上,且该事务已被选
作死锁牺牲品。请重新运行该事务。
1206 18 事务管理器已取消了分布式事务。

需要指出的是对锁的升级,完全是由系统自行判断的,而非人为.如果要避免死锁的话,其根
本还在与数据库的设计上

Feedback

# re: Effective SQL  回复  更多评论   

2007-02-06 10:19 by kwl
下载下来细细研读

# re: Effective SQL  回复  更多评论   

2007-05-10 15:07 by liu xiao gang
太好了,谢谢

只有注册用户登录后才能发表评论。


网站导航:
博客园   IT新闻   Chat2DB   C++博客   博问  
 

posts - 41, comments - 7, trackbacks - 0, articles - 0

Copyright © weibogao