'=============================
各种分页存储过程谁好谁坏,其实没有绝对,而各类存储过程快慢表现的差别,其实质是查询过程中能否有效果利用索引的差别,尤其是聚集索引的利用
'=============================
一个新闻模块,需要专题功能, 文章与专题是多对多关系,表数据结构如果下:
文章表:Article(Id,ClassId,Title....)
专题表:Special(SpecialId,Title,Url...)
文章专题表:ArticleSpecial(ArticleId,SpecialId,SectionId,OrderId) -- SectionId版块,
管理后台需要如下分页列表:
文章编号 文章标题 专题名称 版块号 排序号
对应:R(Article.Id,Article.Title,Special.Title,ArticleSpecial.SectionId,ArticleSpecial.OrderId)
另外在Asp.net 中采用ObjectDataSource + GridView , GridView中需要支持ArticleSpecial表的OrderId,SectionId,ArticleId等字段排序显示(每次只允许一个).
=============================================
目前在MSSQL2000下使用的分页存储过程基本就那么几个, 由于这里设计关系表(ArticleSpecial)是使用复合主键,应次Select Max(key) 方式, Set Rowcount 等方式不能用, 只能使用插入临时表方式跟服务器端游标(使用sp_cursoropen 等)方式,---注意这里不考虑网ArticleSpecial表中加入额外主键,加入额外主键后,分页管理到是可以方便解决,但是考虑上面三个表inner join 操作的性能问题故把主键(默认设置为聚集索引)分配给ArticleSpecial表的 SpecialD跟ArticleId列.
sp_cursoropen这些游标操作API,虽然在MS的帮助文档里找不到,但是如果使用asp + ADO 进行数据库操作时,其实可以看到(SQL跟踪)ADO调用的也就是这些游标操作API.
===============================
--//游标分页存储过程
ALTER procedure QueryByCursor
@sqlstr nvarchar(4000), --查询字符串
@StartRows int, --第N页
@pageSize int, --每页行数
@Total int OUTPUT
As
Set nocount on
Declare @P1 int --P1是游标的id
If @StartRows<=0
Begin
Set @StartRows=1
End
Else
Begin
Set @StartRows=@StartRows
End
exec sp_cursoropen @P1 output,@sqlstr,@scrollopt=1,@ccopt=1,@Total=@Total output
exec sp_cursorfetch @P1,16,@startRows,@pagesize
exec sp_cursorclose @P1
Return @Total
Set NoCount off
//-- asp.net 中对应的操作
//1.帮助函数,对QueryByCursor进行封装,注意这里反回的是第二个Table,第一个为空
public static DataSet Query(string connStr,int startRows,int pageSize,string sql,out int total)
{
SqlParameter sqlTotal = new SqlParameter("@total", 0);
total = 0;
sqlTotal.Direction = ParameterDirection.Output;
SqlParameter[] parameters ={
new SqlParameter("@startRows",startRows),
new SqlParameter("@pageSize",pageSize),
new SqlParameter("@sqlstr",sql),
sqlTotal
};
DataSet ds = SqlHelper.ExecuteDataset(connStr, CommandType.StoredProcedure, "QueryByCursor", parameters);
if (ds.Tables.Count >= 2)
{
ds.Tables.RemoveAt(0); //第一个为空
}
total = Convert.ToInt32(sqlTotal.Value);
return ds;
}
//过滤以及一些拼接操作,最后调用上面的帮助函数Query(...)
public static DataSet SelectByCursor(int startRowIndex, int maximumRows, int specialId, string key, string sortExpress)
{
key = StringHelper.FilterTSQL(key);
sortExpress = StringHelper.FilterTSQL(sortExpress);
string sql = @" Select a2s.*,a.Title,s.Title as SpecialName
From
ArticleSpecial a2s
inner join
Article a on a.Id=a2s.ArticleId
Inner join
Special s On s.SpecialId=a2s.SpecialId ";
string where = "Where 1=1 ";
string order = "";
if (specialId > 0)
{
where += " And a2s.SpecialId=" + specialId;
}
if (!string.IsNullOrEmpty(key))
{
where += " And (a.Title like '%" + key + "%' Or a.Keys like '%" + key + "%')";
}
if (!string.IsNullOrEmpty(sortExpress))
{
order = " Order By a2s." + sortExpress;
}
sql += where + order;
return DBH.Query(DBH.NewsDB, startRowIndex, maximumRows, sql, out _RecordCount);
}
========================================
使用插入临时表的方式:
//--存储过程入下:
---根据指定条件返回分页后的文章专题数据
---第二节的多个inner join 语句始终使用 t开始
ALTER Proc ArticleSpecial_Query
@starRows int =0,
@pageSize int =20,
@SpecialId int,
@Key nvarchar(100),
@OrderColumn nvarchar(50)
As
Declare @SQL nvarchar(4000)
Declare @where nvarchar(200)
Declare @order nvarchar
Set @where=' 1=1 '
--专题不为0
If @specialId>0
Begin
Set @Where=@Where + ' And a2s.SpecialId=' + cast(@SpecialId as nvarchar(50))
End
--关键字不为空
If @Key !=''
Begin
Set @where=@where + ' And (a.Title like ''%' +@key +'%'' Or a.Keys like ''%' + @key + '%'')'
End
If @OrderColumn=''
Begin
Set @OrderColumn='ArticleId'
End
Set @SQL='
Set Nocount ON
Create table #t (nid bigint identity(1,1) Primary Key,ArticleID int,SpecialID int)
Insert Into #t(ArticleId,SpecialId)
Select a2s.ArticleId,a2s.SpecialId
From
ArticleSpecial a2s inner join Article a on a.Id=a2s.ArticleId
Where '+ @where +'
Order By a2s.' + @OrderColumn +'
Select a2s.*,a.Title,s.Title as SpecialName
From
ArticleSpecial a2s
inner join
Article a on a.Id=a2s.ArticleId
Inner join
Special s On s.SpecialId=a2s.SpecialId
Inner join [#t] t
On a2s.ArticleID=t.ArticleID And t.SpecialID=a2s.SpecialID
Where nid >' + cast(@starRows as nvarchar) +' and nid <= ' + cast( (@starRows +@pageSize) as nvarchar) +'
Set NoCount Off
Drop Table #t
'
Exec(@SQL
=======================================
//测试代码: Article表中有10万条数据, Special表中有200表记录(200个专题),ArticleSpecial表中有1万2千条记录,
//这个数据级别基本符合公司5到10年的需求了
private delegate DataSet TestFunction(int startIndex, int pageSize, int specialId, string key, string sortExpress);
private void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
TestFunction fun1 = new TestFunction(ArticleSpecialDAL.Select);
TestFunction fun2 = new TestFunction(ArticleSpecialDAL.SelectByCursor);
Stopwatch sw1 = new Stopwatch();
Stopwatch sw2 = new Stopwatch();
#region 方式1
sw1.Start();
for (int i = 0; i < 12000; i = i + 500)
{
DataSet ds = DoFunction(fun1, i, 20, 0, "", "ArticleId");
}
sw1.Stop();
#endregion
#region 方式2
sw2.Start();
for (int i = 0; i < 12000; i = i + 500)
{
DataSet ds = DoFunction(fun2, i, 20, 0, "", "ArticleId");
}
sw2.Stop();
#endregion
label1.Text = "Select方式:" + sw1.ElapsedMilliseconds.ToString() +Environment.NewLine
+ "SelectByCursor:" + sw2.ElapsedMilliseconds.ToString();
button1.Enabled = true;
}
private DataSet DoFunction(TestFunction fun,int startIndex,int pageSize ,int specialId ,string key ,string sortExpress)
{
return fun(startIndex, pageSize, specialId, key, sortExpress);
}
//============================
测试结果:
使用服务器游标方式比使用插入临时表方式快了将近10倍,插入临时表方式完成上面操作一般需要2万毫秒,而使用游标方式只需要2千毫秒,当然网上经常可以看到的说法是游标方式比插入临时表方式要慢, 这个主要是大家面对的问题不同,数据量也不同,我上面的测试结果是在ArticleSpecial表有12000条记录下得出的,当数据量继续整加时是什么情况我没做测试.
另外发现当传入key关键字跟key保持为空时,插入临时表方式运行时间差不多,而使用游标方式者区别很到,另外两种方式的CUP站用率都比为空时高很多(我这达到50%,为空时10%),可见字符比较操作占用大量的cup操作时间